优化神经网络:
为什么优化会失败,收敛在局部极限值与鞍点会导致优化失败。
对学习率进行调整,使用自适应学习率和学习率调度。
批量归一化可以改变误差表面。
3.1 局部极小值与鞍点
3.1.1 临界点及其种类
当参数对损失微分为零的时候,梯度下降不再更新参数,训练停下,损失不再下降。
梯度为零-->局部极小值(local minimum)-->深度学习时使用梯度下降会收敛在局部极小值,梯度下降不起作用
梯度是零-->鞍点(saddle point)
梯度为零的点统称为临界点(critical point)
损失没有办法再下降,也许是因为收敛在了临界点,但不一定收敛在局部极小值,因为鞍点也是梯度为零的点。
-
损失收敛在局部极小值,所在位置已经是损失最低的,往四周走损失都会比较高,就没有路可以走。
-
但鞍点没有这个问题,旁边还是有路可以让损失更低的。只要逃离鞍点,就有可能让损失更低。
3.1.2 判断临界值种类的方法
判断一个临界点到底是局部极小值还是鞍点需要知道损失函数的形状。
给定某一组参数,θ′ 附近的L(θ)可近似为
第一项:当θ跟θ′ 很近的时候,L(θ)应该跟L(θ′)还蛮靠近的;
第二项:g 代表梯度, 它是一个向量,可以弥补L(θ′)跟L(θ)之间的差距。有时候梯度g 会写成∇L(θ′)。
第三项:跟海森矩阵(Hessian matrix)H 有关。H 里面放的是 L 的二次微分。
式 (3.1) 跟梯度和海森矩阵有关,梯度就是一次微分,海森矩阵里面有二次微分的项。
在临界点的附近,损失函数可被近似为
有一个简单的神经网络,它只有两个神经元,而且这个神经元还没有激活函数和偏置。
有一个简单的训练数据集,这个数据集只有一组数据(1,1),也就是x=1的标签是1. 所以输入1进去,最终输出跟1越接近越好。
import numpy as np
import matplotlib.pyplot as plt
# 定义损失函数
def loss(w1, w2, x, y_true):
y_pred = w1 * w2 * x
return (y_pred - y_true) ** 2
# 生成w1和w2的值
w1_values = np.linspace(-2, 2, 100)
w2_values = np.linspace(-2, 2, 100)
# 创建网格
W1, W2 = np.meshgrid(w1_values, w2_values)
Z = loss(W1, W2, 1, 1) # x=1, y_true=1
# 绘制误差表面
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(W1, W2, Z, cmap='viridis')
ax.set_xlabel('w1')
ax.set_ylabel('w2')
ax.set_zlabel('Loss')
plt.show()
import numpy as np
import matplotlib.pyplot as plt
# 定义损失函数
def loss(w1, w2, x, y_true):
y_pred = w1 * w2 * x
return (y_pred - y_true) ** 2
# 生成w1和w2的值
w1_values = np.linspace(-2, 2, 100)
w2_values = np.linspace(-2, 2, 100)
# 创建网格
W1, W2 = np.meshgrid(w1_values, w2_values)
Z = loss(W1, W2, 1, 1) # x=1, y_true=1
# 绘制误差表面的等高线图
plt.contourf(W1, W2, Z, levels=50, cmap='plasma')
plt.colorbar()
plt.xlabel('w1')
plt.ylabel('w2')
plt.title('error surface')
# 标记临界点
critical_points = [(0, 0), (-1, -1), (1, 1), (-1, 1), (1, -1)]
for point in critical_points:
plt.plot(point[0], point[1], 'ko') # 'ko'表示黑色的点
plt.show()
如图3.4所示,可以取[−2,2]之间的w1 跟w2 的数值,算出这个范围内w1,w2 数值所带来的损失,四个角落的损失是高的。我们用黑色的点来表示临界点,原点(0,0)是临界点,另外两排点是临界点。原点是鞍点,因为我们往某个方向走,损失可能会变大,也可能会变小。而另外两排临界点都是局部极小值。
损失函数为
H 不只可以帮助我们判断是不是在一个鞍点,还指出了参数可以更新的方向。
沿着u的方向更新θ,损失就会变小。
虽然临界点的梯度为零,如果我们是在一个鞍点,只要找出负的特征值,再找出这个特征值对应的特征向量。将其与θ′相加,就可以找到一个损失更低的点。
3.1.3 逃离鞍点的方法
低维度空间中的局部极小值点,在更高维的空间中,实际是鞍点。
误差表面其实有非常高的维度——参数的数量代表了误差表面的维度。
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# 一维误差表面
x = np.linspace(-3, 3, 400)
y = x**4 - 4*x**2 + 4
local_min_x = 0 # 局部极小值点
local_min_y = local_min_x**4 - 4*local_min_x**2 + 4
# 二维误差表面
X = np.linspace(-3, 3, 100)
Y = np.linspace(-3, 3, 100)
X, Y = np.meshgrid(X, Y)
Z = X**4 - 4*X**2 + 4 + Y**2
saddle_point_x = 0 # 鞍点
saddle_point_y = 0
saddle_point_z = saddle_point_x**4 - 4*saddle_point_x**2 + 4 + saddle_point_y**2
# 绘制一维误差表面
plt.figure(figsize=(12, 4))
plt.subplot(1, 3, 1)
plt.plot(x, y)
plt.scatter(local_min_x, local_min_y, color='red') # 标记局部极小值点
plt.title('1D Error Surface')
plt.xlabel('Parameter')
plt.ylabel('Error')
plt.grid(True)
# 绘制二维误差表面
ax = plt.subplot(1, 3, 2, projection='3d')
ax.plot_surface(X, Y, Z, cmap='viridis')
ax.scatter(saddle_point_x, saddle_point_y, saddle_point_z, color='red') # 标记鞍点
ax.set_title('2D Error Surface')
ax.set_xlabel('Parameter 1')
ax.set_ylabel('Parameter 2')
ax.set_zlabel('Error')
# 绘制二维误差表面的等高线图
plt.subplot(1, 3, 3)
contour = plt.contour(X, Y, Z, levels=20, cmap='viridis')
plt.scatter(saddle_point_x, saddle_point_y, color='red') # 标记鞍点
plt.title('2D Error Surface Contour')
plt.xlabel('Parameter 1')
plt.ylabel('Parameter 2')
plt.colorbar(contour)
plt.tight_layout()
plt.show()
损失仍然很高,却遇到了临界点而不再下降; 或者损失降得很低,才遇到临界点。
在图3.6所示的例子中,最小值比例最大也不过处于0.5~0.6 的范围,代表只有约一半的特征值为正,另一半的特征值为负,代表在所有的维度里面有约一半的路可以让损失上升,还有约一半的路可以让损失下降。
虽然在这个图上,越靠近右侧代表临界点“看起来越像”局部极小值,但是这些点都不是真正的局部极小值。所以从经验上看起来,局部极小值并没有那么常见。多数的时候,我们训练到一个梯度很小的地方,参数不再更新,往往只是遇到了鞍点。
3.2 批量和动量
实际计算梯度:并不是对所有数据的损失L计算梯度,而是把所有的数据分成一个一个的批量(batch)。
每个批量的大小是B,即带有B笔数据。每次在更新参数的时候,会去取出B笔数据用来计算出损失和梯度更新参数。遍历所有批量的过程称为一个回合(epoch)。事实上,在把数据分为批量的时候,我们还会进行随机打乱(shuffle)。 随机打乱有很多不同的做法,一个常见的做法是在每一个回合开始之前重新划分批量,也就是说,每个回合的批量的数据都不一样。
3.2.1 批量大小对梯度下降法的影响
-
使用全批量(full batch)的数据来更新参数的方法即批量梯度下降法(BatchGradientDescent,BGD)。
-
随机梯度下降法(Stochastic Gra dient Descent,SGD),也称为增量梯度下降法。
-
批量梯度下降并没有“划分批量”,每次迭代的计算量大。相比随机梯度下降,批量梯度下降每次更新更稳定、更准确
-
随机梯度下降的梯度上引入了随机噪声,因此在非凸优化问题中,其相比批量梯度下降更容易逃离局部最小值。
实际上,在有考虑并行计算的时候,大的批量大小反而是较有效率的,一个回合大的批量花的时间反而是比较少的。
大的批量更新比较稳定,小的批量的梯度的方向是比较有噪声的(noisy)。但实际上有噪声的的梯度反而可以帮助训练。
批量梯度下降在更新参数的时候,沿着一个损失函数来更新参数,走到一个局部最小值或鞍点显然就停下来了。梯度是零,如果不看海森矩阵,梯度下降就无法再更新参数了 。但小批量梯度下降法(mini-batchgradient descent)每次是挑一个批量计算损失,所以每一次更新参数的时候所使用的损失函数是有差异的。
在论文 “On Large-Batch Training for Deep Learning: Generalization Gap and Sharp Minima”中,作者在不同数据集上训练了六个网络(包括全连接网络、不同的卷积神经网络),在很多不同的情况都观察到一样的结果。在小的批量,一个批量里面有256笔样本。在大的批量中,批量大小等于数据集样本数乘0.1。比如数据集有60000笔数据,则一个批量里面有6000笔数据 。大的批量跟小的批量的训练准确率(accuracy)差不多,但就算是在训练的时候结果差不多,测试的时候,大的批量比小的批量差,代表过拟合。
训练损失上面有多个局部最小值,这些局部最小值的损失都很低,其损失可能都趋近于0。但是局部最小值有好最小值跟坏最小值之分, 如果局部最小值在一个“峡谷”里面,它是坏的最小值;如果局部最小值在一个平原上,它是好的最小值。
训练的损失跟测试的损失函数是不一样的,这有两种可能。一种可能是本来训练跟测试的分布就不一样;另一种可能是因为训练跟测试都是从采样的数据算出来的,训练跟测试采样到的数据可能不一样,所以它们计算出的损失是有一点差距。 对在一个“盆地”里面的最小值,其在训练跟测试上面的结果不会差太多,只差了一点点。但对在右边在“峡谷”里面的最小值,一差就可以天差地远 。虽然它在训练集上的损失很低,但训练跟测试之间的损失函数不一样,因此测试时,损失函数一变,计算出的损失就变得很大。
大的批量大小会让我们倾向于走到“峡谷”里面,而小的批量大小倾向于让我们走到“盆地” 里面。小的批量有很多的损失,其更新方向比较随机,其每次更新的方向都不太一样。即使 “峡谷”非常窄,它也可以跳出去,之后如果有一个非常宽的“盆地”,它才会停下来。
3.2.2 动量法
动量法(momentum method)是另外一个可以对抗鞍点或局部最小值的方法。
假设误差表面就是真正的斜坡,参数是一个球,把球从斜坡上滚下来,如果使用梯度下降,球走到局部最小值或鞍点就停住了。 但是在物理的世界里,一个球如果从高处滚下来, 就算滚到鞍点或局部最小值,因为惯性的关系它还是会继续往前走。如果球的动量足够大,其甚至翻过小坡继续往前走。 因此在物理的世界里面,一个球从高处滚下来的时候,它并不一定会被鞍点或局部最小值卡住,如果将其应用到梯度下降中,这就是动量。
引入动量后,每次在移动参数的时候,不是只往梯度的反方向来移动参数,而是根据梯度的反方向加上前一步移动的方向决定移动方向。
图3.16中红色虚线方向是梯度的反方向,蓝色虚线方向是前一次更新的方向,蓝色实线的方向是下一步要移动的方向。把前一步指示的方向跟梯度指示的方向相加就是下一步的移动方向。
引入动量后,可以从两 个角度来理解动量法。一个角度是动量是梯度的负反方向加上前一次移动的方向。另外一个角度是当加上动量的时候,更新的方向不是只考虑现在的梯度,而是考虑过去所有梯度的总和。
红色表示负梯度方向,蓝色虚线表示前一步的方向,蓝色实线表示真实的移动量。一开始没有前一次更新的方向,完全按照梯度给指示往右移动参数。负梯度方向跟前一步移动的方向加起来,得到往右走的方向。一般梯度下降走到一个局部最小值或鞍点时,就被困住了。但有动量还是有办法继续走下去,因为动量不是只看梯度, 还看前一步的方向。即使梯度方向往左走,但如果前一步的影响力比梯度要大,球还是有可能继续往右走,甚至翻过一个小丘,也许可以走到更好的局部最小值,这就是动量有可能带来的好处。