本蒟蒻笔记存在诸多不足,此处仅为抛砖引玉,欢迎各位大佬在评论区指正orz
问题的产生
当我们构建模型时,总会希望假设空间参数尽可能多,系统越复杂,拟合得越好嘛!我们还希望我们的优化算法能使我们的模型产生的损失函数的值尽可能小(即我们的假设空间能够贴合每一个训练样本点)。但这样真的好吗?奥卡姆剃刀貌似又胜利了。然而假设我们的模型达成了上述的情况,有很大概率产生一个ML界非常令人头疼的一件事:过拟合(Overfitting)。
什么是过拟合呢?举一个简单的例子:我们设计了一个模型来判断一件物品是否为树叶。喂养这个模型的数据集中含有几张带有尖刺边缘的树叶。模型的设计者希望模型能满足每一个训练数据,模型就将尖刺边缘也纳入了参数中。当我们测试这个模型的泛化性能时,就会发现效果很差,因为模型钻牛角尖,它认为树叶必须带有尖刺边缘,所以它排除了所有没有带有尖刺边缘的树叶,但事实上,我们知道树叶并不一定带有尖刺边缘。结果为什么会这样?因为模型设计者的强迫症使得这个模型过分贴合整个训练集,结果把噪音点也拟合上了。
欠拟合与过拟合
我们以“波士顿房价预测”为例,来从图像的角度直观感受一下什么叫过拟合,顺便也介绍一下欠拟合:
例子1:线性回归(房价预测)
Fig.1 Linear regression(截屏自吴恩达机器学习)
我们看到第一张图的模型过于简单,而且损失函数的收敛速度很慢。这就使得优化算法做得再好,我们的模型的泛化性能也会很差,因为这条直线在训练集上的cost就很大,我们把这种训练集上的偏差很大的情况叫做欠拟合(Underfitting),也有一种历史叫法:高偏差(High bias)。
第二张图是我们合理假设的一个模型。可以看到,选取了合理的模型后,图像大致穿过了样本点。像极了做物理实验时,最后用一条曲线大致地穿过既定的样本点;和第一张图比起来,至少损失值大大下降了。
第三张图引入了高次项,就题论题来说,这太复杂了。虽然事实上对于是任意n个点,总能找到n+1次曲线方程将这n个点全部穿过,但是从模型的角度来讲,这显然不是好模型(姑且不说要用一个高次模型去拟合离散点要迭代多少次,学习率要调到多低)。即使它一板一眼地穿过了所有样本点,但谁能保证这种奇形怪状的曲线能穿过下一个随机给出的样本点呢?我们把这种和预测值和样本标签值几乎完全一致的情况叫做过拟合(Overfitting),历史上也称为高方差(High variance)。
我们再看看分类问题中的过拟合。
例子2:Logistic回归
Fig2.Logistic regression(截屏自吴恩达机器学习)
三幅图哪个更好呢?不多说,第二张图应该是合理的划分方式,而不是像第三张图那样一板一眼。
定义
我们给出过拟合的定义:
Overfitting : If we have too many features, the learned hypothesis may fit the training set vey well, but fail to generalize to new examples.
其中的'fit the training set very well'的数学语言是:
所以过拟合就是损失函数极小但泛化性能差的情况。落实在分类问题上就是训练集的损失函数值很小,但是验证集/测试集上的损失函数值很大。
这也说明了我们在训练模型时,损失函数关于迭代次数的图像一直下降到很小的数值并不是什么好事,这恰恰暗示了我们的模型存在过拟合的风险。
解决过拟合
我们一般有两种方法来减小过拟合的影响:
1.减少属性值(特征值)的数量。
- 人工选择哪些特征需要保留。
- 使用模型选择算法。
2.实行正则化
- 保留所有特征值,但是减小参数θ_j的值或数量级。
- 当我们有许多特征时,效果较好。其中每一个特征值都会对y造成影响。
正则化(Regularization)
基本思路
下面的几张图是对房价问题实际构建了神经网络后得到的结果:
假设空间为一次函数,学习率=0.01,迭代1000次
假设空间为二次函数,学习率=0.0001,迭代1000次
假设空间为四次函数,学习率=0.0000001,迭代10000次
对于刚才线性回归的第三张图,为了使它演化到第二张图,我们其实只要把三次项和四次项扔掉就行了,对吧?怎么个扔法呢?实际上,我们只要对参数3和参数4实行一定的惩罚就行了。我们考察这个损失函数(1000只是随手写的一个比较大的数字):
由于1000很大,这就使得为了使损失函数降到最小,损失函数收敛的过程中不得不把参数3和参数4压缩到很小的数字,即:
这也就相当于去掉了这两个参数。更一般的,给出正则化后面的想法。我们都默认这样一个事实:对于多项式:
λ越小,函数图像越平坦(可以自己画图试试)。也可以这么想:λ=0时,图像是一条平行于x轴的直线,绝对是“最平坦”的了。类似极限的“保序性”,那么λ往0靠近时,图像也应该是平坦的。那么将这个效果施加在我们预测房价的第二张图上,就可以使得那条歪歪曲曲的多项式曲线平坦拉伸,拉成类似于第二张图的样子。
因此,我们可以重新定义损失函数:
其中我们把多出来的那一项称为正则化项,lambda称为正则化系数。
下面我们简单讨论λ的取值。如果λ太小的话,形同虚设,继续过拟合;如果λ太大的话,那么除了常量参数θ_0外的其余所有参数都会被压缩到0,最终这就是一条平行于x轴的直线,很明显会造成欠拟合。所以λ的取值不能大,也不能小。如果我们取了一个恰当的值(比如0.001)我们原本的蓝线就会变成红线,虽然不是真正的二次曲线,但是过拟合的痕迹被压缩小了。
所以,我们就得到了含有正则表达式的梯度下降:
当然,第二行也可以写成:
那么我们更新梯度的时候,只要在原来的基础上把右式的系数改成这样就行了。总的效果是使得我们的四次曲线更加的平坦、柔和,这样做可以防止曲线收到噪声点的干扰过于严重。我们将以上式子带入,得到以下代码,尝试使用:
我想起来了,我当初学的时候还没开始用机器学习框架,numpy裸打的代码现在感觉又臭又长( ̄ω ̄;)
代码
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as pat
plt.figure(dpi=150,edgecolor="yellow")
class Network(object):
def __init__(self,weights_num):
np.random.seed(0)
self.w = np.array([[ 2.5],
[-0.1138],
[-0.0409],
[ 0.0038]])
self.b = -0.42
self.lam = 10500 # 正则化系数
def forward(self,x):
z = np.dot(x,self.w) + self.b
return z
def loss(self,z,y):
return np.mean((z - y)**2)
def gradient(self,x,y):
z = self.forward(x)
gradient_w = np.mean((z - y) * x,axis=0)
gradient_w.resize(4,1)
gradient_b = np.mean(z - y)
return gradient_w,gradient_b
def update(self, gradient_w, gradient_b, eta=0.01): # 梯度下降,学习率默认0.01
self.w -= eta * gradient_w # 往梯度相反的方向走
self.b -= eta * gradient_b
def train(self, x, y, iteration=100, eta=0.01): # 训练函数,默认迭代100次
losses = [] # 采集每次迭代的数据,用来绘制图像
for i in range(iteration):
z = self.forward(x)
L = self.loss(z, y)
gradient_w, gradient_b = self.gradient(x, y)
self.update(gradient_w, gradient_b, eta)
losses.append(L)
if (i + 1) % 100 == 0: # 每十个数据打印一下情况
print("iter {} times,loss value is {}".format(i, L))
return self.w,self.b,losses
def re_loss(self,z,y): #正则化下的损失函数
cost1 = np.mean((z - y) ** 2)
cost2 = np.mean(self.w ** 2)
return cost1 + cost2
def re_update(self,gradient_w, gradient_b, eta=0.01): # 正则化下的梯度下降更新参数
self.w = (1 - eta * self.lam / 5) * self.w - eta * gradient_w
self.b -= eta * gradient_b
def re_train(self, x, y, iteration=100, eta=0.01): # 正则化训练函数,默认迭代100次
re_losses = [] # 采集每次迭代的数据,用来绘制图像
for i in range(iteration):
z = self.forward(x)
L = self.re_loss(z, y)
gradient_w, gradient_b = self.gradient(x, y)
self.re_update(gradient_w, gradient_b, eta)
re_losses.append(L)
if (i + 1) % 100 == 0: # 每十个数据打印一下情况
print("iter {} times,loss value is {}".format(i, L))
return self.w, self.b, re_losses
def Draw_sample_point(x,y): # 画出样本点
plt.grid(True)
plt.xlabel("Size of house", fontsize=15)
plt.ylabel("Price", fontsize=15)
#plt.text(4, 1, r'$theta_0+theta_1 x+theta_2 x^2$''n'r'$+theta_3 x^3+theta_4 x^4$', fontsize=15)
plt.plot(x, y, 'rx')
plt.ylim(0, 10)
plt.xlim(0, 10)
def Draw_fitted_curve(w,b,color="b"): # 画出拟合后的曲线
plt.grid(True)
x = np.arange(0.2, 9.8, 0.01)
y = b + w[0][0] * x + w[1][0] * x ** 2 + w[2][0] * x ** 3 + w[3][0] * x ** 4
plt.plot(x, y,color)
def Draw_loss_function(iteration_time,losses,color="b"): # 画出损失函数的值关于迭代次数的函数图像
plt.grid(True)
plot_x = np.arange(iteration_time)
plot_y = np.array(losses)
plt.xlabel("times of iterations", fontsize=15)
plt.ylabel("values of loss function", fontsize=15)
plt.plot(plot_x, plot_y,color)
def get_data(x): # 获取四次函数的数据[x,x^2,x^3,x^4]
x1 = x.tolist()
x2 = (x ** 2).tolist()
x3 = (x ** 3).tolist()
x4 = (x ** 4).tolist()
x_new = []
for i in range(len(x)):
roll = []
roll.append(x1[i])
roll.append(x2[i])
roll.append(x3[i])
roll.append(x4[i])
x_new.append(roll)
return np.array(x_new)
x = np.array([1,2,3.5,6,8])
y = np.array([2,4,6,7,7.5])
plt.subplot(121)
Draw_sample_point(x,y)
x = get_data(x)
y.resize(5,1)
net = Network(4)
iteration_time = 1000 #迭代次数
w,b,losses = net.train(x,y,iteration = iteration_time,eta = 0.0000001) #
print("Done")
re_w,re_b,re_losses = net.re_train(x,y,iteration = iteration_time,eta = 0.0000001)
print("w is{},b is {}nre_w is {},re_b is {}".format(w,b,re_w,re_b))
blue_patch = pat.Patch(color="blue")
green_patch = pat.Patch(color="green")
Draw_fitted_curve(w,b,"b")
Draw_fitted_curve(re_w,re_b,"g")
plt.legend(handles=[blue_patch,green_patch],labels=["Original curve","Regularization"])
plt.subplot(122)
Draw_loss_function(iteration_time,losses,"b")
Draw_loss_function(iteration_time,re_losses,"g")
plt.legend(loc = "upper left",handles=[blue_patch,green_patch],labels=["Original curve","Regularization"])
plt.show()
运行结果
四次函数拟合离散点正则化尝试。正则化系数=10500,学习率=0.0000001
可以看到我们的正则化后的四次曲线虽然偏离了样本点,但是却变得更加平坦了。正如上面所论述的一样,更加平坦的曲线拥有更加好的泛用性能。当然,笔者手残,参数调不好,最理想的正则化情况应该是介于二次曲线和四次曲线之间的状态,参照这个模型可以接着调整。(其实这个例子不是特别好,因为正则化得作用就是使得高次项参数变小。但是这个例子中,各个次项得系数本身就很小,所以才导致结果不太明显)