前言
过拟合(Overfitting)是指为了得到一致假设而使假设变得过度严格,这是一个在机器学习中无法避免的问题。虽然模型对训练集有着优异的预测表现,但面对新的数据时拟合能力有所下降。应对过拟合的最好方法是尽可能增大数据集。然而,在无法扩大数据集时,怎样才能尽可能防止过拟合的发生?
过拟合示意图
今天小派将列出三种实用的检验+防止过拟合的方法,并在Python中展示如何使用。
理解过拟合
过拟合,就是模型在训练集和测试集上的表现有差异。一个假设在训练数据上能够获得比其他假设更好的拟合,但是在训练数据外的数据集上却不能很好地拟合数据,此时认为这个假设出现了过拟合的现象。其常见原因是训练集数据量不够大或特征中存在噪声。模型是通过对训练集的数据有记忆性而达到了虚高的拟合结果。
过拟合处理方法
换一种通俗的说法,一个人复习了诗人苏轼的古诗情感,考试考的是陶渊明的诗歌,他用苏轼的情感去分析陶渊明的诗,结果不一定好。这之间的差异就如同训练集和新数据的差异,训练集的理论不一定对新数据行得通,训练表现好并不一定是真的好,这种现象就是过拟合。
最后,还需阐述下最重要的概念:
过拟合是无法被消除的,只能尽力抑制。
处理过拟合方法
验证:交叉验证(Cross-Validation)
抑制:Dropout
抑制:正则化(Regularization)
交叉验证(Cross-Validation)
交叉验证是初学者常用的一种检验过拟合方法,适合小型数据集。缺点在于不适合应用于大型数据集,耗时较长。最重要的一点是,交叉验证(Cross-Validation)只可以检测是否发生了过拟合,并不能去过拟合。
交叉验证的原理为在给定的建模样本中,拿出大部分样本进行建模型,留小部分样本用于建立的模型进行预报,并求这小部分样本的预报误差,记录它们的平方加和。一般有三种常见形式:简单交叉验证(Holdout验证);K折交叉验证(K-fold cross-validation);留一验证(leave-one-out cross validation)。其中K折交叉验证又是最常用的一种。
K-fold Cross-validation
K折交叉验证在初始采样时将全部训练数据分割成K个子样本,一个单独的子样本被保留作为验证模型的数据,其他K-1个样本用来训练。通俗的理解就是有k个人,第一个人学苏轼的诗歌,第二个人学李白的,到第k个人,每个人都学一套不一样的诗人的诗集(实际过程中将随机分配训练对象)。最后,统一拿一首没学过古诗进行考试,将所有人的成绩取均值,就是最终成绩。这个方法的优势在于同时重复运用随机产生的子样本进行训练和验证,每次的结果验证一次,一般10折交叉验证是最常用的。
使用Python时,sklearn包中的cross_val_score()在进行交叉验证时非常便捷。我们接下来就在Python中验证一下10折交叉验证的作用。
小派搭建了一个11*6*6*2的人工神经网络分类器,在代码中分类器变量名为classifier。使用训练集数据训练模型后,我们看一下模型在测试集的表现。
未经过cross-validation的模型结果
准确率(Accuracy rate)为:
接下来我们进行10折交叉验证:
from sklearn.model_selection import cross_val_score
accuracy_cross=cross_val_score(estimator=classifier,X=x_train,y=y_train,cv=10)
print(accuracy_cross.mean())
结果如下:
经过10折训练后,准确率比单次训练下降了4.34%,也证明了单次分割测试集与训练集出来的结果确实存在过拟合的现象。k折训练效果通常来说十分有效且便捷。除了神经网络以外,一般的数学机器学习模型都可以直接在训练模型后使用。但缺点也非常明显,速度慢,尤其是搭建深度学习时,结合10折交叉验证与反向传播学习,耗时非常长。
面对该问题,我们一起来看看第二个方法Dropout是如何抑制过拟合的。
Dropout
Dropout被称为去过拟合的大杀器,在深度学习神经网络中非常受用。
其工作原理可分为以下两个步骤:
1.随机删除处于隐蔽层(Hidden Layer)的神经元,删除比例由人为设定。
2.通过重复执行随机删除神经元,形成不同结构的神经网络后,取各个网络结果的均值,达到正则化的效果。
其优势在于节省时间,同时减少了神经元之间复杂的共适性。因为一个神经元不能依赖其他特定的神经元。因此,不得不去学习随机子集神经元间的鲁棒性*的有用连接。对于较复杂的算法和大型的数据来说是一个非常好的选择。
*鲁棒性:即抗变换性,用以表征控制系统对特性或参数扰动的不敏感性。
Dropout示意图
接下来我们在Python中验证一下dropout的作用,依然使用上例中提到的数据与11*6*6*2的人工神经网络模型,其单次训练的准确率为83.96%。
import keras
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
NN_class=Sequential()
NN_class.add(Dense(output_dim=6,init='uniform',activation='relu',input_dim=11))
NN_class.add(Dropout(p=0.3))
NN_class.add(Dense(output_dim=6,init='uniform',activation='relu'))
NN_class.add(Dropout(p=0.2))
NN_class.add(Dense(output_dim=1,init='uniform',activation='sigmoid'))
代码中在每一个隐层后面增加了dropout代码,我们再看一下dropout之后的测试结果:
Dropout的预测准确度相比单次训练的结果要降低一些,说明原始模型确实拥有过拟合的现象。不同的是在随机删除的过程中,神经元一直在学习当遇到不同组合时如何去更好的预测,所以结果比交叉验证后的准确度79.63%高,说明模型拥有了更好的拟合能力。
正则化(Regularization)
如果有非常大的数据,不适合交叉检验,又不需要去使用深度学习训练且无法使用Dropout时我们该怎么办?
这时候就可以考虑使用正则化(Regularization)来抑制过拟合。正则化(regularization),是指在线性代数理论中,不适定问题通常是由一组线性代数方程定义的,而且这组方程组通常来源于有着很大的条件数的不适定反问题。大条件数意味着舍入误差或其它误差会严重地影响问题的结果。
正则化可以有效地减少特征共线性、过滤噪音并抑制过拟合。其原理是加入新的偏差来惩罚较大的权重参数。
J(θ)=(Predy-y)2+[θ12+...+θn2]
J(θ)=(Predy-y)2+[|θ1|+...+|θn|]
公式中括号中的部分分别为L2、L1范数,也就是我们用来调整损失函数、惩罚较大的权重参数的部分。在Python中使用L2范数的方式与使用Dropout非常相似,可以在训练模型时直接加入定义好的L2范数。一个完整的逻辑回归+L2范数正则化的示例如下:
class LogisticRegression():
""" A simple logistic regression model with L2 regularization (zero-mean
Gaussian priors on parameters). """
def __init__(self, x_train=None, y_train=None, x_test=None, y_test=None,
alpha=.1, synthetic=False):
# Set L2 regularization strength
self.alpha = alpha
# Set the data.
self.set_data(x_train, y_train, x_test, y_test)
# Initialize parameters to zero, for lack of a better choice.
self.betas = np.zeros(self.x_train.shape[1])
def negative_lik(self, betas):
return -1 * self.lik(betas)
def lik(self, betas):
""" Likelihood of the data under the current settings of parameters. """
# Data likelihood
l = 0
for i in range(self.n):
l += log(sigmoid(self.y_train[i] *
np.dot(betas, self.x_train[i,:])))
# Prior likelihood
for k in range(1, self.x_train.shape[1]):
l -= (self.alpha / 2.0) * self.betas[k]**2
return l
def train(self):
""" Define the gradient and hand it off to a scipy gradient-based
optimizer. """
# Define the derivative of the likelihood with respect to beta_k.
# Need to multiply by -1 because we will be minimizing.
dB_k = lambda B, k : (k > 0) * self.alpha * B[k] - np.sum([
self.y_train[i] * self.x_train[i, k] *
sigmoid(-self.y_train[i] * np.dot(B, self.x_train[i,:]))
for i in range(self.n)])
# The full gradient is just an array of componentwise derivatives
dB = lambda B : np.array([dB_k(B, k)
for k in range(self.x_train.shape[1])])
# Optimize
self.betas = fmin_bfgs(self.negative_lik, self.betas, fprime=dB)
def set_data(self, x_train, y_train, x_test, y_test):
""" Take data that's already been generated. """
self.x_train = x_train
self.y_train = y_train
self.x_test = x_test
self.y_test = y_test
self.n = y_train.shape[0]
def training_reconstruction(self):
p_y1 = np.zeros(self.n)
for i in range(self.n):
p_y1[i] = sigmoid(np.dot(self.betas, self.x_train[i,:]))
return p_y1
def test_predictions(self):
p_y1 = np.zeros(self.n)
for i in range(self.n):
p_y1[i] = sigmoid(np.dot(self.betas, self.x_test[i,:]))
return p_y1
def plot_training_reconstruction(self):
plot(np.arange(self.n), .5 + .5 * self.y_train, 'bo')
plot(np.arange(self.n), self.training_reconstruction(), 'rx')
ylim([-.1, 1.1])
def plot_test_predictions(self):
plot(np.arange(self.n), .5 + .5 * self.y_test, 'yo')
plot(np.arange(self.n), self.test_predictions(), 'rx')
ylim([-.1, 1.1])
if __name__ == "__main__":
from pylab import *
# Create 20 dimensional data set with 25 points -- this will be
# susceptible to overfitting.
data = SyntheticClassifierData(25, 20)
# Run for a variety of regularization strengths
alphas = [0, .001, .01, .1]
for j, a in enumerate(alphas):
# Create a new learner, but use the same data for each run
lr = LogisticRegression(x_train=data.X_train, y_train=data.Y_train,
x_test=data.X_test, y_test=data.Y_test, alpha=a)
print "Initial likelihood:"
print lr.lik(lr.betas)
# Train the model
lr.train()
# Display execution info
print "Final betas:"
print lr.betas
print "Final lik:"
print lr.lik(lr.betas)
# Plot the results
subplot(len(alphas), 2, 2*j + 1)
lr.plot_training_reconstruction()
ylabel("Alpha=%s" % a)
if j == 0:
title("Training set reconstructions")
subplot(len(alphas), 2, 2*j + 2)
lr.plot_test_predictions()
if j == 0:
title("Test set predictions")
show()
总结
过拟合是一个无法避免的现象,除去文中提到的三种方法外,还有很多种抑制和检验过拟合的方法,如early stopping、utilize invariance、Bayesian等方法。在处理数据时,要结合数据与模型的特殊性,选择合适的方法处理过拟合现象。