本章重点,如何更好地优化神经网络:
首先,要理解为什么优化会失败,收敛在局部极限值与 鞍点会导致优化失败。
其次,可以对学习率进行调整,使用自适应学习率和学习率调度。
最 后,批量归一化可以改变误差表面,这对优化也有帮助。
3.1 局部极小值与鞍点
把深层网络(deep network)、线性模型和浅层网络(shallow network) 做比较,可以发现深层网络没有做得更好。但有时候,模型一开始就训练不起来,不管我们怎么更新参数,损失都降不下去。
3.1.1 临界点及其种类
我们 把梯度为零的点统称为临界点(critical point)。在以前我们将梯度为0的点叫做极值点,但是还有其他可能会让梯度是零的点,比 如鞍点(saddle point)。鞍点其实就是梯度是零且区别于局部极小值和局部极大值(local maximum)的点。
图 3.2b 红色的点在 y 轴方向是比较高的,在 x 轴方向是比较低的,这就 是一个鞍点。鞍点的叫法是因为其形状像马鞍。鞍点的梯度为零,但它不是局部极小值。损失没有办法再下降,也许是因为收敛在了 临界点,但不一定收敛在局部极小值,因为鞍点也是梯度为零的点。
3.1.2 判断临界值种类的方法
判断一个临界点到底是局部极小值还是鞍点需要知道损失函数的形状。鉴于函数十分复杂,但是如果给定某一组参数,比如 θ ′,在 θ ′ 附近的损失函数是有办 法写出来的——虽然 L(θ) 完整的样子写不出来。θ ′ 附近的 L(θ) 可通过(是泰勒级数近似)近似:
一元函数的泰勒公式:
可推广到更复杂的多元函数L(θ) :
近似:
其中,第一项 L(θ) ′ 告诉我们, 当 θ 跟 θ ′ 很近的时候,L(θ) 应该跟 L(θ ′ ) 还蛮靠近的;第二项g 代表梯度, 它是一个向量,可以弥补 L(θ ′ ) 跟 L(θ) 之间的差距。有时候梯度 g 会写成 ∇L(θ ′ )。gi 是向 量 g 的第 i 个元素,就是 L 关于 θ 的第 i 个元素的微分,即
光看 g 还是没有办法完整地描述 L(θ),还要看式 (3.1) 的第三项。第三项跟海森矩阵(Hessian matrix)H 有关。H 里面放的是 L 的二次微分,它第 i 行, 第 j 列的值 Hij 就是把 θ 的第 i 个元素对
作微分,再把 θ 的第 j 个元素对
作 微分后的结果,即
总结一下,损失函数 L(θ) 在 θ ′ 附近可近似,跟梯度和海森矩阵有关,梯 度就是一次微分,海森矩阵里面有二次微分的项。
当在临界点,梯度 g 为零,因此第二项就为0了,式子近似为
根据这个式子,判断在 θ ′ 附近的误差表面(error surface)到底长 什么样子。知道误差表面的“地貌”,我们就可以判断 L(θ ′ ) 是局部极小值、局部极大值,还是 鞍点。为了符号简洁,我们用向量 v 来表示 θ − θ ′,即
。有如下三种情况:
(1)如果对所有 v, > 0. 这意味着对任意 θ,L(θ) > L(θ ′ ). 只要 θ 在 θ ′ 附近, L(θ) 都大于 L(θ ′ ). 这代表 L(θ ′ ) 是附近的一个最低点。 局部极小值。
口述一下怎么理解为什么>就是局部极小值:因为这个式子
,只要这个
>0,L(θ) > L(θ ′ ),这个函数恒大于这个点的函数值 L(θ ′ ),就代表周围的值都大于这个,所以是极小值。
(2)如果对所有 v, < 0. 这意味着对任意 θ,L(θ) < L(θ ′ ),θ ′ 是附近最高的一 个点,L(θ ′ ) 是局部极大值。
(3)如果对于 v,有时候大于零,有时候小于零。这意味着在 θ ′ 附近,有时候 L(θ) > L(θ ′ ),有时候 L(θ) < L(θ ′ ). 因此在 θ ′ 附近,L(θ ′ ) 既不是局部极大值,也不是局部 极小值,而是鞍点。
有一个问题,通过 判断临界点是局部极小值还是鞍点还是局部极 大值,需要代入所有的θ. 但我们不可能把所有的 v 都拿来试试看,所以有一个更简便的方法 来判断
的正负。
直接判断海森矩阵的类型:
若 H 的所有特征值都是正的,H 为正定矩阵,则 > 0,临界点是局部极小值。
若 H 的所有特征值都是负的,H 为负定矩阵,则 < 0,临界点是局部极大值。
若 H 的特征值有正有负,临界点是鞍点。
补充正定、负定知识:
如果 n 阶对称矩阵 A 对于任意非零的 n 维向量 x 都有 x TAx > 0,则称矩阵 A 为正 定矩阵。如果 n 阶对称矩阵 A 对于任意非零的 n 维向量 x 都有 x TAx < 0,则称矩 阵 A 为负定矩阵。
举个例子,我们有一个简单的神经网络,它只有两个神经元,而且这个神经元还没有激活 函数和偏置。输入 x,x 乘上 w1 以后输出,然后再乘上 w2,接着再输出,最终得到的数据就 是 y。
我们还有一个简单的训练数据集,这个数据集只有一组数据 (1,1),也就是 x = 1 的标签 是 1. 所以输入 1 进去,我们希望最终的输出跟 1 越接近越好,如图 3.3 所示。
可以直接画出这个神经网络的误差表面,如图 3.4 所示,
可以取 [−2,2] 之间的 w1 跟 w2 的数值,算出这个范围内 w1, w2 数值所带来的损失,四个角落的损失是高的。我们用黑色的 点来表示临界点,原点 (0,0) 是临界点,另外两排点是临界点。我们可以进一步地判断这些临 界点是鞍点还是局部极小值。原点是鞍点,因为我们往某个方向走,损失可能会变大,也可能 会变小。而另外两排临界点都是局部极小值。这是我们取 [−2,2] 之间的参数得到的损失函数 以后,得到的损失的值后,画出误差表面后得到的结论。
除了尝试取所有可能的损失,我们还有其他的方法,比如把损失的函数写出来。对于图 3.3 所示的神经网络,损失函数 L 是正确答案 y 减掉模型的输出 yˆ = w1w2x 后取平方误差(square error),这里只有一组数据,因此不会对所有的训练数据进行加和。令 x = 1, y = 1,损失函 数为
可以求出损失函数的梯度
什么时候梯度会为零(也就是到一个临界点)呢?比如,在原点时,w1 = 0, w2 = 0,此 时的梯度为零,原点就是一个临界点,但通过海森矩阵才能判断它是哪种临界点。刚才我们通 过取 [−2,2] 之间的 w1 和 w2 来判断出原点是一个鞍点,但是假设我们还没有取所有可能的损失,我们要看看能不能够用海森矩阵来判断原点是什么临界点。 海森矩阵 H 收集了 L 的二次微分:
对于原点,只要把 w1 = 0, w2 = 0 代进去,有海森矩阵这个矩阵有两个特征值:-2和2,特征值有正有负,因此原点是鞍点。
如果我们当前处于鞍点,就不用那么害怕了。H 不只可以帮助我们判断是不是在一个鞍点,还指出了参数可以更新的方向。之前我们参数更新的时候,都是看梯度 g,但是我们走到 某个地方以后发现 g 变成 0 了,就不能再看 g 了,g 不见了。但如果临界点是一个鞍点,还 可以再看 H,怎么再看 H 呢,H 怎么告诉我们怎么更新参数呢?
设 λ 为 H 的一个特征值 λ,u 为其对应的特征向量。对于我们的优化问题,可令 u = θ − θ ′,(1)
注意:知识点:特征值,特征向量。
若 λ < 0,则 。所以
.此时,L(θ) < L(θ ′ ),且
θ = θ ′ + u(2)
沿着 u 的方向更新 θ,损失就会变小。因为根据式 (1)和式 (2),只要 θ = θ ′ + u,沿着特征向量 u 的方向去更新参数,损失就会变小,所以虽然临界点的梯度为零,如果我们是在一个鞍点,只要找出负的特征值,再找出这个特征值对应的特征向量。将其与 θ ′ 相加,就可以找到一个损失更低的点。
在前面的例子中,原点是一个临界点,此时的海森矩阵如式H所示,该海森矩阵有一 个负的特征值:−2,特征值 −2 对应的特征向量有无穷多个。不妨取 u = [1, 1]T,作为 −2 对 应的特征向量。我们其实只要顺着 u 的方向去更新参数,就可以找到一个比鞍点的损失还要 更低的点。以这个例子来看,原点是鞍点,其梯度为零,所以梯度不会告诉我们要怎么更新参 数。但海森矩阵的特征向量告诉我们只要往 [1, 1]T 的方向更新。损失就会变得更小,就可以 逃离鞍点。 所以从这个角度来看,鞍点似乎并没有那么可怕。但实际上,我们几乎不会真的把海森矩 阵算出来,因为海森矩阵需要算二次微分,计算这个矩阵的运算量非常大,还要把它的特征值 跟特征向量找出来,所以几乎没有人用这个方法来逃离鞍点。还有一些其他逃离鞍点的方法 的运算量都比要算海森矩阵小很多。
3.1.3 逃离鞍点的方法
一维空间中的误差表面,有一个局部极小值。但是在二维空间(如 图 3.5(b) 所示),这个点就可能只是一个鞍点。常常会有人画类似图 3.5(c) 这样的图来告诉 我们深度学习的训练是非常复杂的。如果我们移动某两个参数,误差表面的变化非常的复杂, 有非常多局部极小值。低维度空间中的局部极小值点,在更高维的空间中,实际是鞍点。同样地,如果在二维的空间中没有路可以走,会不会在更高维的空间中,其实有路可以走?更高的维度难以视化它,但我们在训练一个网络的时候,参数数量动辄达百万千万级,所以误差表面其实有非常高的维度—— 参数的数量代表了误差表面的维度。既然维度这么高,会不会其实就有非常多的路可以走呢?既然有非常多的路可以走,会不会其实局部极小值就很少呢? 而经验上,我们如果自己做一些实验,会发现实际情况也支持这个假说。
图 3.6 是训练某不同神经网络的结果,每个点对应一个神经网络。纵轴代表训练网络时,损失收敛到临界点,损失没法下降时的损失。我们常常会遇到两种情况:
1.损失仍然很高,却遇到了临界点而不再下降;
2.损失降得很低,才遇到临界点。
图 3.6 中,横轴代表最小值比例(minimum ratio),最小值比例定义为:
正特征值对应的方向是“上升”的方向,而负特征值对应的方向是“下降”的方向。图中横轴为最小值比例,纵轴为训练损失。根据这一比例,可以判断一个点是局部极小值、鞍点还是其他类型的临界点。
-
最小值比例接近1:表示大多数甚至全部特征值为正。此时,所有的方向几乎都是“上升”的,意味着临界点附近的函数表面形状像一个局部极小值。但这里强调的是,“看起来像”局部极小值,因为损失虽然没有再下降,但实际上这些点可能只是鞍点或其他非极小值的点,表面看起来很平坦或有微小的起伏。
-
最小值比例接近0.5:表示正特征值和负特征值的比例接近相等。在这种情况下,临界点附近的损失函数表面既有“上升”方向,也有“下降”方向,这种情况通常对应的是鞍点,而非真正的局部极小值。
-
真正的局部极小值:在真正的局部极小值处,所有特征值都应该是正的,这样才能确保在所有方向上的损失都不会进一步下降。但实际情况中,这种真正的局部极小值是很少见的,更多时候我们遇到的是具有较低训练损失的点,而这些点的最小值比例往往不到1,意味着它们并不是所有方向都是“上升”的,也就是可能只是鞍点。
实际上,我们几乎找不到所有特征值都为正的临界点。在图 3.6 所示的例子中,最小值比例最大也不过处于 0.5 ~ 0.6 的范围,代表只有约一半的特征值为正,另一半的特征值为负,代表在所有的维度里面有约一半的路可以让损失上升,还有约一半的路可以让损失下降。虽然在这个图上,越靠近右侧代表临界点“看起来越像”局部极小值,但是这些点都不是真正的局部极小值。所以从经验上看起来,局部极小值并没有那么常见。多数的时候,我们训练到一个梯度很小的地方,参数不再更新,往往只是遇到了鞍点。
3.2 批量和动量
实际上在计算梯度的时候,并不是对所有数据的损失 L 计算梯度,而是把所有的数据分成一个一个的批量(batch),如图 3.7 所示。每个批量的大小是 B ,即带有 B 笔数据。每次在更新参数的时候,会去取出 B 笔数据用来计算出损失和梯度更新参数。遍历所有批量的过程 称为一个回合(epoch)。事实上,在把数据分为批量的时候,我们还会进行随机打乱(shuffle)。 随机打乱有很多不同的做法,一个常见的做法是在每一个回合开始之前重新划分批量,也就是说,每个回合的批量的数据都不一样。
3.2.1 批量大小对梯度下降法的影响
假设现在我们有 20 笔训练数据,先看下两个最极端的情况,如图 3.8 所示。
图 3.8 (a)的情况是没有用批量,批量大小为训练数据的大小,这种使用全批量(full batch)的数据来更新参数的方法即批量梯度下降法(Batch Gradient Descent,BGD)。 此时模型必须把 20 笔训练数据都看完,才能够计算损失和梯度,参数才能够更新一次。
图 3.8(b)中,批量大小等于 1,此时使用的方法即随机梯度下降法(Stochastic Gra-dient Descent,SGD),也称为增量梯度下降法。批量大小等于 1 意味着只要取出一笔数据即可计算损失、更新一次参数。如果总共有 20 笔数据,那么在每一个回合里面, 参数会更新 20 次。用一笔数据算出来的损失相对带有更多噪声,因此其更新的方向如 图 3.8 所示,是曲曲折折的 。
实际上,批量梯度下降并没有“划分批量”:要把所有的数据都看过一遍,才能够更新一次参数,因此其每次迭代的计算量大。但相比随机梯度下降,批量梯度下降每次更新更稳定、更准确。(这里的准确和下面的准确不一样:这里☞这种方法能提供一个相对精确的梯度估计,因为它考虑了更多的样本,从而减少了梯度计算中的随机噪声。但这种“准确”的梯度估计可能导致算法容易陷入局部最小值或者鞍点,尤其是在高维非凸优化问题中。)
随机梯度下降的梯度上引入了随机噪声,因此在非凸优化问题中,其相比批量梯度下降更容易逃离局部最小值。
实际上,考虑并行运算,批量梯度下降花费的时间不一定更长;对于比较大的批量,计算损失和梯度花费的时间不一定比使用小批量的计算时间长 。使用 Tesla V100 GPU 在 MNIST 数据集得到的实验结果如图 3.9 所示。图 3.9 中横坐标表示批量大小,纵坐标表示给定批量大小的批量,计算梯度并更新参数所耗费的时间。批量大小从 1 到 1000,需要耗费的时间几乎是一样的,因为在实际上 GPU 可以做并行运算,这 1000 笔数据是并行处理的,所以 1000笔数据所花的时间并不是一笔数据的 1000 倍。当然 GPU 并行计算的能力还是存在极限的,当批量大小很大的时候,时间还是会增加的。 当批量大小非常大的时候,GPU 在“跑”完一个批量,计算出梯度所花费的时间还是会随着批量大小的增加而逐渐增长 。当批量大小增加到10000,甚至增加到 60000 的时候,GPU 计算梯度并更新参数所耗费的时间确实随着批量大小的增加而逐渐增长。
但是因为有并行计算的能力,因此实际上当批量大小小的时候,要“跑”完一个回合,花的时间是比大的。假设训练数据只有 60000 笔,批量大小设 1,要 60000 个更新才能“跑”完一个 回合;如果批量大小等于 1000,60 个更新才能“跑”完一个回合,计算梯度的时间差不多。但60000 次更新跟 60 次更新比起来,其时间的差距量就非常大了。
图 3.10(a) 是用一个批量计算梯度并更新一次参数所需的时间。假设批量大小为 1,“跑”完一个回合,要更新 60000 次参数,其时间是非常大的。但假设批量大小是 1000,更新 60 次参数就会“跑”完一个回合。
图 3.10(b)是“跑”完一个完整的回合需要花的时间。如果批量大小为 1000 或 60000,其时间比批量大小设 1 还要短 。图 3.10(a) 和图 3.10(b) 的趋势正好是相反的。因此实际上,在有考虑并行计算的时候,大的批量大小反而是较有效率的,一个回合大的批量花的时间反而是比较少的。
大的批量更新比较稳定,小的批量的梯度的方向是比较有噪声的(noisy)。但实际上有噪声的的梯度反而可以帮助训练,如果拿不同的批量来训练模型来做图像识别问题,实验结果如图 3.11 所示,横轴是批量大小,纵轴是正确率。图 3.11(a) 是 MNIST 数据集上的结果, 图 3.11(b) 是 CIFAR-10 数据集上的结果。批量大小越大,验证集准确率越差。但这不是过拟 合,因为批量大小越大,训练准确率也是越低。因为用的是同一个模型,所以这不是模型偏见 的问题。 但大的批量大小往往在训练的时候,结果比较差。这个是优化的问题,大的批量大小优化可能会有问题,小的批量大小优化的结果反而是比较好的。
一个可能的解释如图 3.12 所示,批量梯度下降在更新参数的时候,沿着一个损失函数来更新参数,走到一个局部最小值或鞍点显然就停下来了。梯度是零,如果不看海森矩阵,梯度下降就无法再更新参数了 。但小批量梯度下降法(mini-batch gradient descent)每次是挑一个批量计算损失,所以每一次更新参数的时候所使用的损失函数是有差异的。选到第一个批量的时候,用 L1 计算梯度;选到第二个批量的时候,用 L2 计算梯度。假设用 L1 算梯度的时候,梯度是零,就会卡住。但 L2 的函数跟 L1 又不一样,L2 不一定会卡住,可以换下个批量的损失 L2 计算梯度,模型还是可以训练,还是有办法让损失变小,所以这种有噪声的更新方式反而对训练其实是有帮助的。
总之:
- 大的批量大小可能在训练时表现较差,尤其是在非凸优化问题中,这是因为它们缺乏小批量中的“有益噪声”,从而导致优化陷入次优解或局部极小值。
- 小的批量大小在优化过程中引入的噪声虽然使得梯度更新更加波动,但有助于探索参数空间,找到更优的解,因此在某些任务中,较小的批量大小可能会带来更好的模型性能。
其实小的批量也对测试有帮助。假设有一些方法(比如调大的批量的学习率)可以把大的批量跟小的批量训练得一样好。实验结果发现小的批量在测试的时候会是比较好的[1]。在论文 “On Large-Batch Training for Deep Learning: Generalization Gap and Sharp Minima”中,作者在不同数据集上训练了六个网络(包括全连接网络、不同的卷积神经网络),在很多不同的情况都观察到一样的结果。在小的批量,一个批量里面有 256 笔样本。在大的批量中,批量大小等于数据集样本数乘 0.1。比如数据集有 60000 笔数据,则一个批量里面有 6000 笔数据 。大的批量跟小的批量的训练准确率(accuracy)差不多,但就算是在训练的时候结果差不 多,测试的时候,大的批量比小的批量差,代表过拟合。
这篇论文给出了一个解释,如图 3.13 所示,训练损失上面有多个局部最小值,这些局部 最小值的损失都很低,其损失可能都趋近于 0。但是局部最小值有好最小值跟坏最小值之分, 如果局部最小值在一个“峡谷”里面,它是坏的最小值;如果局部最小值在一个平原上,它是好的最小值。训练的损失跟测试的损失函数是不一样的,这有两种可能。一种可能是本来训练跟测试的分布就不一样;另一种可能是因为训练跟测试都是从采样的数据算出来的,训练跟测试采样到的数据可能不一样,所以它们计算出的损失是有一点差距。
对在一个“盆地”里面的最小值,其在训练跟测试上面的结果不会差太多,只差了一点点。但对在右边在“峡谷”里面的 最小值,一差就可以天差地远(图中红色虚线) 。虽然它在训练集上的损失很低,但训练跟测试之间的损失函 数不一样,因此测试时,损失函数一变,计算出的损失就变得很大。
大的批量大小会让我们倾向于走到“峡谷”里面,而小的批量大小倾向于让我们走到“盆地” 里面。小的批量有很多的损失,其更新方向比较随机,其每次更新的方向都不太一样。即使 “峡谷”非常窄,它也可以跳出去,之后如果有一个非常宽的“盆地”,它才会停下来。
大的批量跟小的批量的对比结果如表 3.1 所示。在有并行计算的情况下,小的批量跟大的批量运算的时间并没有太大的差距。除非大的批量非常大,才会显示出差距。但是一个回合需要的时间,小的批量比较长,大的批量反而是比较快的,所以从一个回合需要的时间来看,大的批量是较有优势的。 而小的批量更新的方向比较有噪声的,大的批量更新的方向比较稳定。但是有噪声的更新方向反而在优化的时候有优势,而且在测试的时候也会有优势。所以大的批量跟小的批量各有优缺点,批量大小是需要去调整的超参数。
其实用大的批量大小来做训练,用并行计算的能力来增加训练的效率,并且训练出的结 果很好是可以做到的[2-3]。比如 76 分钟训练 BERT[4],15 分钟训练 ResNet[5],一分钟训练 ImageNet[6] 等等。这些论文批量大小很大,比如论文“Large Batch Optimization for Deep Learning: Training BERT in 76 minutes ”中批量大小为三万。批量大小很大可以算得很快, 这些论文有一些特别的方法来解决批量大小可能会带来的劣势。
3.2.2 动量法
动量法(momentum method)是另外一个可以对抗鞍点或局部最小值的方法。如图 3.14所示,假设误差表面就是真正的斜坡,参数是一个球,把球从斜坡上滚下来,如果使用梯度下降,球走到局部最小值或鞍点就停住了。 但是在物理的世界里,一个球如果从高处滚下来,就算滚到鞍点或鞍点,因为惯性的关系它还是会继续往前走。如果球的动量足够大,其甚至翻过小坡继续往前走。 因此在物理的世界里面,一个球从高处滚下来的时候,它并不一定会被鞍点或局部最小值卡住,如果将其应用到梯度下降中,这就是动量。
一般的梯度下降(vanilla gradient descent)如图 3.15 所示。初始参数为 θ0,计算一下梯度,计算完梯度后,往梯度的反方向去更新参数 θ1 = θ0 − ηg0。有了新的参数 θ1 后,再计算一次梯度,再往梯度的反方向,再更新一次参数,到了新的位置以后再计算一次梯度,再往梯度的反方向去更新参数。
引入动量后,每次在移动参数的时候,不是只往梯度的反方向来移动参数,而是根据梯度的反方向加上前一步移动的方向决定移动方向。 图 3.16 中红色虚线方向是梯度的反方向,蓝色虚线方向是前一次更新的方向,蓝色实线的方向是下一步要移动的方向。把前一步指示的方向跟梯度指示的方向相加就是下一步的移动方向。
如图 3.16 所示,
初始的参数值为 θ0 = 0,前一步的参数的更新量为 m0 = 0。接下来在 θ0 的地方,计算梯度的方向 g0。下一步的方向是梯度的方向加上前一步的方向,不过因为前一步正好是 0,所以更新的方向跟原来的梯度下降是相同的。但从第二步开始就不太一样了。从第二步开始,计算 g1,接下来更新的方向为m2 = λm1 − ηg1,参数更新为 θ2 ,接下来就反复进行同样的过程。每一步的移动都用 m 来表示。m 其实可以写成之前所有计算的梯度的加权和,如式所示。
m0 = 0
m1 = −ηg0
m2 = −ληg0 − ηg1
…………
其中 η 是学习率,λ 是前一个方向的权重参数,也是需要调的。引入动量后,可以从两个角度来理解动量法。一个角度是动量是梯度的负反方向加上前一次移动的方向。另外一个角度是当加上动量的时候,更新的方向不是只考虑现在的梯度,而是考虑过去所有梯度的总和。
动量的简单例子如图 3.17 所示。红色表示负梯度方向,蓝色虚线表示前一步的方向,蓝色实线表示真实的移动量。一开始没有前一次更新的方向,完全按照梯度给指示往右移动参数。负梯度方向跟前一步移动的方向加起来,得到往右走的方向。一般梯度下降走到一个局部最小值或鞍点时,就被困住了。但有动量还是有办法继续走下去,因为动量不是只看梯度,还看前一步的方向。即使梯度方向往左走,但如果前一步的影响力比梯度要大,球还是有可能继续往右走,甚至翻过一个小丘,也许可以走到更好的局部最小值,这就是动量有可能带来的好处 。
3.3 自适应学习率
临界点其实不一定是在训练一个网络的时候会遇到的最大的障碍。图 3.18 中的横坐标代 表参数更新的次数,竖坐标表示损失。一般在训练一个网络的时候,损失原来很大,随着参 数不断的更新,损失会越来越小,最后就卡住了,损失不再下降。当我们走到临界点的时候, 意味着梯度非常小,但损失不再下降的时候,梯度并没有真的变得很小,图 3.19 给出了示例。 图 3.19 中横轴是迭代次数,竖轴是梯度的范数(norm),即梯度这个向量的长度。随着迭代 次数增多,虽然损失不再下降,但是梯度的范数并没有真的变得很小。
在局部最小值或鞍点,只是单纯的损失无法再下降。
我们现在训练一个网络,训练到现在参数在临界点附近,再根据特征值的正负号判断该 临界点是鞍点还是局部最小值。实际上在训练的时候,要走到鞍点或局部最小值,是一件困难 的事情。一般的梯度下降,其实是做不到的。用一般的梯度下降训练,往往会在梯度还很大的 时候,损失就已经降了下去,这个是需要特别方法训练的。要走到一个临界点其实是比较困难 的,多数时候训练在还没有走到临界点的时候就已经停止了。
举个例子,我们有两个参数 w 和 b,这两个参数值不一样的时候,损失值也不一样,得 到了图 3.21 所示的误差表面,该误差表面的最低点在叉号处。事实上,该误差表面是凸的形 状。凸的误差表面的等高线是椭圆形的,椭圆的长轴非常长,短轴相比之下比较短,其在横轴 的方向梯度非常小,坡度的变化非常小,非常平坦;其在纵轴的方向梯度变化非常大,误差表 面的坡度非常陡峭。现在我们要从黑点(初始点)来做梯度下降。
学习率 η = 10^−2 的结果如图 3.22(a) 所示。参数在峡谷的两端,参数在山壁的两端不断第“震荡”,损失降不下去,但是梯度仍然是很大的。我们可以试着把学习率设小一点,学习率 决定了更新参数的时候的步伐,学习率设太大,步伐太大就无法慢慢地滑到山谷里面。调学习 率从 10^−2 调到 10^−7 的结果如图 3.22(b) 所示,参数不再“震荡”了。参数会滑到山谷底后左 转,但是这个训练永远走不到终点,因为学习率已经太小了。AB 段的坡度很陡,梯度的值很 大,还能够前进一点。左拐以后,BC 段的坡度已经非常平坦了,这种小的学习率无法再让训 练前进。事实上在 BC 段有 10 万个点(10 万次更新),但都无法靠近局部最小值,所以显然 就算是一个凸的误差表面,梯度下降也很难训练。 最原始的梯度下降连简单的误差表面都做不好,因此需要更好的梯度下降的版本。在梯 度下降里面,所有的参数都是设同样的学习率,这显然是不够的,应该要为每一个参数定制 化学习率,即引入自适应学习率(adaptive learning rate)的方法,给每一个参数不同的学习 率。如图 3.23 所示,如果在某一个方向上,梯度的值很小,非常平坦,我们会希望学习率调 大一点;如果在某一个方向上非常陡峭,坡度很大,我们会希望学习率可以设得小一点。
3.3.1 AdaGrad
典型的自适应学习率方法,其能够根据梯度大小自 动调整学习率。AdaGrad 可以做到梯度比较大的时候,学习率就减小,梯度比较小的时候,学 习率就放大。
梯度下降更新某个参数 的过程为
在第 t 个迭代的值减掉在第 t 个迭代参数 i 算出来的梯度.
代表在第 t 个迭代,即
时, 参数
对损失 L 的微分,学习率是固定的。
现在要有一个随着参数定制化的学习率,即把原来学习率 η 变成
的上标为 i,这代表参数 σ 与 i 相关,不同的参数的 σ 不同。
的下标为 t,这代表 参数 σ 与迭代相关,不同的迭代也会有不同的 σ。
学习率从 η 改成 的时候,m学习率就变 得参数相关(parameter dependent)。
参数相关的一个常见的类型是算梯度的均方根(root mean square)。参数的更新过程为
是初始化参数。而
计算
第一次在更新参数,要么是加上 η,要么是减掉 η,跟梯度的大小无关,这个是第一步的 情况。
第二次更新参数过程为:
其中 是过去所有计算出来的梯度的平方的平均再开根号,即均方根:
同样的操作反复继续下去:
第 t + 1 次更新参数的时候,即
当作是新的学习率来更新参数。
图 3.24 中有两个参数: 和
。
坡度小,
坡度大。因为
坡度小,根据式 (3.22),
这个参数上面算出来的梯度值都比较小,因为梯度算出来的值比较小,所以算出来的
就 小,
小学习率就大。反过来,
坡度大,所以计算出的梯度都比较大,
就比较大,在更 新的时候,步伐(参数更新的量)就比较小。因此有了
这一项以后,就可以随着梯度的不 同,每一个参数的梯度的不同,来自动调整学习率的大小。
3.3.2 RMSProp
同一个参数需要的学习率,也会随着时间而改变。在图 3.25 中的误差表面中,如果考虑 横轴方向,绿色箭头处坡度比较陡峭,需要较小的学习率,但是走到红色箭头处,坡度变得平 坦了起来,需要较大的学习率。因此同一个参数的同个方向,学习率也是需要动态调整的,于 是就有了一个新的方法———RMSprop(Root Mean Squared propagation)。
RMSprop 没有论文,Geoffrey Hinton 在 Coursera 上开过深度学习的课程,他在他的课 程里面讲了 RMSprop,如果要引用,需要引用对应视频的链接。 RMSprop 第一步跟 Adagrad 的方法是相同的,即
第二步更新过程为
其中 0 < α < 1,其是一个可以调整的超参数。计算 的方法跟 AdaGrad 算均方根不一样, 在算均方根的时候,每一个梯度都有同等的重要性,但在 RMSprop 里面,可以自己调整现在 的这个梯度的重要性。如果 α 设很小趋近于 0,代表
相较于之前算出来的梯度而言,比较 重要;如果 α 设很大趋近于 1,代表
比较不重要,之前算出来的梯度比较重要。 同样的过程就反复继续下去:
RMSProp 通过 α 可以决定,相较于之前存在
里面的
的重要 性有多大。如果使用 RMSprop,就可以动态调整
这一项。图 3.26 中黑线是误差表面,球 就从 A 走到 B,AB 段的路很平坦,g 很小,更新参数的时候,我们会走比较大的步伐。走动 BC 段后梯度变大了,AdaGrad 反应比较慢,而 RMSprop 会把 α 设小一点,让新的、刚看 到的梯度的影响比较大,很快地让
的值变大,很快地让步伐变小,RMSprop 可以很快地 “踩刹车”。如果走到 CD 段,CD 段是平坦的地方,可以调整 α,让其比较看重最近算出来的 梯度,梯度一变小,
的值就变小了,走的步伐就变大了。
3.4 学习率调度
1.学习率下降
如图 3.22 所示的简单的误差表面,我们都训练不起来,加上自适应学习率以后,使用 AdaGrad 方法优化的结果如图 3.27 所示。一开始优化的时候很顺利,在左转的时候,有 AdaGrad 以后,可以再继续走下去,走到非常接近终点的位置。走到 BC 段时,因为横轴方向的梯 度很小,所以学习率会自动变大,步伐就可以变大,从而不断前进。接下来的问题走到图 3.27 中红圈的地方,快走到终点的时候突然“爆炸”了。是把过去所有的梯度拿来作平均。在 AB 段梯度很大,但在 BC 段,纵轴的方向梯度很小,因此纵轴方向累积了很小的
,累积到一定 程度以后,步伐就变很大,但有办法修正回来。因为步伐很大,其会走到梯度比较大的地方。 走到梯度比较大的地方后,
会慢慢变大,更新的步伐大小会慢慢变小,从而回到原来的路 线。
通过学习率调度(learning rate scheduling)可以解决这个问题。之前的学习率调整 方法中 η 是一个固定的值,而在学习率调度中 η 跟时间有关,如下式所示。学习率调度 中最常见的策略是学习率衰减(learning rate decay),也称为学习率退火(learning rate annealing)。随着参数的不断更新,让 η 越来越小,如图 3.28 所示。图 3.22b 的情况,如果 加上学习率下降,可以很平顺地走到终点,如图 3.29 所示。在图 3.22b 红圈的地方,虽然步伐很大,但 η 变得非常小,步伐乘上 η 就变小了,就可以慢慢地走到终点。
注:步伐描述的是每一步梯度下降时参数更新的幅度,而学习率 η 决定了这个更新幅度的大小。因此,这提到“步伐”时,它实际上是在描述整个参数更新过程的大小和速度,而不仅仅是单纯的学习率 η大小。
2.预热
除了学习率下降以外,还有另外一个经典的学习率调度的方式———预热。预热的方法 是让学习率先变大后变小,至于变到多大、变大的速度、变小的速度是超参数。残差网络[8] 里 面是有预热的,在残差网络里面,学习率先设置成 0.01,再设置成 0.1,并且其论文还特别说 明,一开始用 0.1 反而训练不好。除了残差网络,BERT 和 Transformer 的训练也都使用了预 热。
Q:为什么需要预热?
A:当我们使用 Adam、RMSprop 或 AdaGrad 时,需要计算 σ。而 σ 是一个统计的结 果。从 σ 可知某一个方向的陡峭程度。统计的结果需要足够多的数据才精准,一开始 统计结果 σ 是不精准的。一开始学习率比较小是用来探索收集一些有关误差表面的情 报,先收集有关 σ 的统计数据,等 σ 统计得比较精准以后,再让学习率慢慢爬升。如 果读者想要学更多有关预热的东西可参考 Adam 的进阶版———RAdam[9]。
3.5 优化总结
所以我们从最原始的梯度下降,进化到这一个版本,如下式所示。
其中是动量。这个版本里面有动量,其不是顺着某个时刻算出的梯度方向来更新参数,而是把过去所有算出梯度的方向做一个加权总和当作更新的方向。接下来的步伐大小为
。最后通过
来实现学习率调度。这个是目前优化的完整的版本,这种优化器除了 Adam 以外,还有各种 变形。但其实各种变形是使用不同的方式来计算
或
,或者是使用不同的学习率调度的 方式。
Q:动量
考虑了过去所有的梯度,均方根
考虑了过去所有的梯度,一个放在分 子,一个放在分母,并且它们都考虑过去所有的梯度,不就是正好抵消了吗?
A:
和
在使用过去所有梯度的方式是不一样的,动量是直接把所有的梯度都加起来,所以它有考虑方向,它有考虑梯度的正负。但是均方根不考虑梯度的方向,只考虑梯度的小,计算
的时候,都要把梯度取一个平方项,把平方的结果加起来,所 以只考虑梯度的大小,不考虑它的方向,所以动量跟
计算出来的结果并不会互相抵消。
3.6 分类
3.6.1 分类与回归的关系
和回归类似都是输入x,输出y。
较常见的做法也是用独热向量表示类.用数字来表示类 会预设 1 和 2 有比较近的关系,1 和 3 有比较远的关系。但假设三个类本身没有特定的关系, 类 1 是 1,类 2 是 2 类 3 是 3。这种情况,需要引入独热向量来表示类。
如果有三个类,标签 y 就是一个三维的向量,比如类 1 是 [1, 0, 0]T,类 2 是 [0, 1, 0]T,类 3 是 [0, 0, 1]T。如果每个类都用一个独热向量来表示,就没有类 1 跟类 2 比较接近,类 1 跟 类 3 比较远的问题。如果用独热向量计算距离的话,类两两之间的距离都是一样的。
如果目标 y 是一个向量,比如 y 是有三个元素的向量,网络也要输出三个数字才行。如 图 3.31 所示,输出三个数值就是把本来输出一个数值的方法,重复三次。把 a1、a2 和 a3 乘上三个不同的权重,加上偏置,得到;再把 a1、a2 和 a3 乘上另外三个权重,再加上另外 一个偏置得到
;把 a1、a2 和 a3 再乘上另外一组权重,再加上另外一个偏置得到
.输入 一个特征向量,产生
、
、
,跟目标越接近越好。
3.6.2 带有 softmax 的分类
按照上述的设定,分类实际过程是:输入 x,乘上 W,加上 b,通过激活函数 σ,乘上 W′,再加上 b ′ 得到向量 。但实际做分类的时候,往往会把
通过 softmax 函数得到 y ′,才 去计算 y ′ 跟
之间的距离。
Q:为什么分类过程中要加上 softmax 函数?
A:一个比较简单的解释是,y 是独热向量,所以其里面的值只有 0 跟 1,但是
里面 有任何值。既然目标只有 0 跟 1,但
有任何值,可以先把它归一化到 0 到 1 之间, 这样才能跟标签的计算相似度。
softmax 的计算如下式所示,先把所有的 y 取一个指数(负数取指数后也会变成正 的),再对其做归一化(除掉所有 y 的指数值的和)得到 y ′。图 3.33 是 softmax 的块(block), 输入 y1、y2 和 y3,产生 y ′ 1、y ′ 2 和 y ′ 3。比如 y1 = 3,y2 = 1,y3 = −3,取完指数的时候, exp(3) = 20、exp(1) = 2.7 和 exp(−3) = 0.05,做完归一化后,就变成 0.88、0.12 跟 0。−3取完指数,再做归一化以后,会变成趋近于 0 的值。所以 softmax 除了归一化,让 y ′ 1、y ′ 2 和 y ′ 3,变成 0 到 1 之间,和为 1 以外,它还会让大的值跟小的值的差距更大。
图 3.33 考虑了三个类的状况,两个类也可以直接套 softmax 函数。但一般有两个类的时 候,我们不套 softmax,而是直接取 sigmoid。当只有两个类的时候,sigmoid 和 softmax 是 等价的。
3.6.3 分类损失
当我们把 x 输入到一个网络里面产生 后,通过 softmax 得到 y ′,再去计算 y ′ 跟 y 之 间的距离 e,如图 3.34 所示。
计算 y ′ 跟 y 之间的距离不只一种做法,可以是如式 所示的均方误差,即把 y 里 面每一个元素拿出来,计算它们的平方和当作误差。
但如下式所示的交叉熵更常用,当 跟 y ′ 相同时,可以最小化交叉熵的值,此时均 方误差也是最小的。最小化交叉熵其实就是最大化似然(maximize likelihood)。
接下来从优化的角度来说明相较于均方误差,交叉熵是被更常用在分类上。如图 3.35 所 示,有一个三类的分类,网络先输出 y1、y2 和 y3,在通过 softmax 以后,产生 y ′ 1、y ′ 2 和 y ′ 3。 假设正确答案是 [1, 0, 0]T,要计算 [1, 0, 0]T 跟 y ′ 1、y ′ 2 和 y ′ 3 之间的距离 e,e 可以是均方误差 或交叉熵。假设 y1 的变化是从-10 到 10,y2 的变化也是从-10 到 10,y3 就固定设成-1000。因 为 y3 的值很小,通过 softmax 以后,y ′ 3 非常趋近于 0,它跟正确答案非常接近,且它对结果 影响很少。总之,我们假设 y3 设一个定值,只看 y1 跟 y2 有变化的时候,对损失 e 的影响。
图 3.36 是分别在 e 为均方误差和交叉熵时,y1、y2 的变化对损失的影响,对误差表面的 影响,红色代表损失大,蓝色代表损失小。如果 y1 很大,y2 很小,代表 y ′ 1 会很接近 1,y ′ 2 会很接近 0。所以不管 e 取均方误差或交叉熵,如果 y1 大、y2 小,损失都是小的;如果 y1 小,y2 大,y ′ 1 是 0,y ′ 2 是 1,这个时候损失会比较大。
左图 (a): 交叉熵损失的变化。当 y1和 y2的值改变时,交叉熵损失的等值线分布如图所示。颜色越红代表损失越大,颜色越蓝代表损失越小。交叉熵对于分类问题表现更好,因为它能够更好地区分不同的类。
右图 (b): 均方误差的变化。相较于交叉熵,均方误差的等值线更加平滑,这意味着在 y1y和 y2y变化时,损失的变化不如交叉熵敏感。这种特性在分类问题上可能会导致“卡住”的现象(即训练变得困难),尤其是在预测值接近真实标签的时候。
图 3.36 中左上角损失大,右下角损失小,所以期待最后在训练的时候,参数可以“走” 到右下角的地方。假设参数优化开始的时候,对应的损失都是左上角。如果选择交叉熵,如 图 3.36(a) 所示,左上角圆圈所在的点有斜率的,所以可以通过梯度,一路往右下的地方“走”; 如果选均方误差,如图 3.36(b) 所示,左上角圆圈就卡住了,均方误差在这种损失很大的地方, 它是非常平坦的,其梯度是非常小趋近于 0 的。如果初始时在圆圈的位置,离目标非常远,其 梯度又很小,无法用梯度下降顺利地“走”到右下角。
因此做分类时,选均方误差的时候,如果没有好的优化器,有非常大的可能性会训练不起 来。如果用 Adam,虽然图 3.36(b) 中圆圈的梯度很小,但 Adam 会自动调大学习率,还有机 会走到右下角,不过训练的过程比较困难。总之,改变损失函数可以改变优化的难度。
3.7 批量归一化
如果误差表面很崎岖,它比较难训练。能不能直接改误差表面的地貌,“把山铲平”,让它 变得比较好训练呢?批量归一化(Batch Normalization,BN)就是其中一个“把山铲平”的 想法。不要小看优化这个问题,有时候就算误差表面是凸(convex)的,它就是一个碗的形状, 都不一定很好训练。如图 3.37 所示,假设两个参数对损失的斜率差别非常大,在 w1 这个方 向上面,斜率变化很小,在 w2 这个方向上面斜率变化很大。
如果是固定的学习率,可能很难得到好的结果,所以我们才需要自适应的学习率、Adam 等比较进阶的优化的方法,才能够得到好的结果。从另外一个方向想,直接把难做的误差表 面把它改掉,看能不能够改得好做一点。在做这件事之前,第一个要问的问题就是:w1 跟 w2 斜率差很多的这种状况,到底是从什么地方来的。
图 3.38 是一个非常简单的模型,其输入是 x1 跟 x2,对应的参数为 w1 跟 w2,它是一个 线性的模型,没有激活函数。w1 乘 x1,w2 乘 x2 加上 b 以后就得到 yˆ,然后会计算 yˆ 跟 y 之 间的差距当做 e,把所有训练数据 e 加起来就是损失,然后去最小化损失。
什么样的状况会产生像上面这样子,比较不好训练的误差表面呢?对 w1 有一个小小的改 变,比如加上 ∆w1 的时候,L 也会有一个改变,那这个 w1 呢,是通过 w1 改变的时候,就 改变了 y,y 改变的时候就改变了 e,接下来就改变了 L。
简单来说:就是x输入的值大范围大,稍稍改变w就可能照成y的巨大变化,L就会发生巨变。
什么时候 w1 的改变会对 L 的影响很小呢,也就是它在误差表面上的斜率会很小呢一个可能性是当输入很小的时候,假设 x1 的值在不同的训练样本里面,它的值都很小,那因为 x1 是直接乘上 w1,如果 x1 的值都很小,w1 有一个变化的时候,它得到的,它对 y 的影响也是 小的,对 e 的影响也是小的,它对 L 的影响就会是小的
反之,如图 3.39 所示,如果是 x2 的话,假设 x2 的值都很大,当 w2 有一个小小的变化 的时候,虽然 w2 这个变化可能很小,但是因为它乘上了 x2,x2 的值很大,那 y 的变化就很 大,e 的变化就很大,L 的变化就会很大,就会导致我们在 w 这个方向上,做变化的时候,我 们把 w 改变一点点,误差表面就会有很大的变化。所以既然在这个线性的的模型里面,当输 入的特征,每一个维度的值,它的范围差距很大的时候,我们就可能产生像这样子的误差表 面,就可能产生不同方向,斜率非常不同,坡度非常不同的误差表面所以怎么办呢,有没有可 能给特征里面不同的维度,让它有同样的数值的范围。如果我们可以给不同的维度,同样的数 值范围的话,那我们可能就可以制造比较好的误差表面,让训练变得比较容易一点其实有很 多不同的方法,这些不同的方法往往就合起来统称为特征归一化(feature normalization)。
以下所讲的方法只是特征归一化的一种可能性,即 Z 值归一化(Z-score normalization), 也称为标准化(standardization)。它并不是特征归一化的全部,假设 x 1 到 x R,是我们所有 的训练数据的特征向量。我们把所有训练数据的特征向量,统统都集合起来。向量 x1 里面就 x 1 1 代表 x1 的第一个元素,x 2 1 代表 x2 的第一个元素,以此类推。我们把不同笔数据即不同特 征向量,同一个维度里面的数值,把它取出来,对于每个维度 i,计算其平均值(mean) mi 和标准差(standard deviation)σi。接下来我们就可以做一种归一化。
我们就是把这边的某一个数值 x,减掉这一个维度算出来的平均值,再除掉这个维度,算 出来的标准差,得到新的数值 x˜。得到新的数值以后,再把新的数值把它塞回去。
归一化有个好处,做完归一化以后,这个维度上面的数值就会平均是 0,其方差是 1,所 以这一排数值的分布就都会在 0 上下;对每一个维度都做一样的归一化,所有特征不同维度的数值都在 0 上下,可能就可以制造一个比较好的误差表面。所以像这样子的特征归一化方 式往往对训练有帮助,它可以让在做梯度下降的时候,损失收敛更快一点,训练更顺利一点。
3.7.1 考虑深度学习
x˜ 代表归一化的特征,把它丢到深度网络里面,去做接下来的计算和训练。如图 3.41 所 示,x˜1 通过第一层得到 z 1,有可能通过激活函数,不管是选 sigmoid 或者 ReLU 都可以,再 得到 a 1,接着再通过下一层等等。对每个 x 都做类似的事情。
虽然 x˜ 已经做归一化了,但是通过 W1 以后,没有做归一化。如果 x˜ 通过 W1 得到 z 1, 而 z 1 不同的维度间,它的数值的分布仍然有很大的差异,训练 W2 第二层的参数也会有困 难。对于 W2,a 或 z 其实也是一种特征,也应该要对这些特征做归一化。如果选择 sigmoid, 比较推荐对 z 做特征归一化,因为 sigmoid 是一个 s 的形状,其在 0 附近斜率比较大,如果 对 z 做特征归一化,把所有的值都挪到 0 附近,到时候算梯度的时候,算出来的值会比较大。 如果使用别的激活函数,可能对 a 归一化也会有好的结果。一般而言,特征归一化,要放在 激活函数之前,之后都是可以的,在实现上,没有太大的差别。
如何对 z 做特征归一化?z 可以看成另外一种特征。首先计算下 z 1 , z 2 , z 3 的平均值,即
平方就是指对每一个元素都去做平方,开根号指的是对向量里面的每一 个元素开根号。
其中,除号代表逐元素的除,即分子分母两个向量对应元素相除。
如图 3.43 所示,接下来可以通过激活函数得到其他向量,µ 跟 σ 都是根据 z 1 , z 2 , z 3 计 算出来的。改变了 z 1 的值,a 1 的值也会改变,µ 和 σ 也会改变。µ,σ 改后,z 2 , a 2 , z 3 , a 3 的值也会改变。之前的 x˜1, x˜2 x˜3 是独立分开处理的,但是在做特征归一化以后,这三个样本 变得彼此关联了。所以有做特征归一化的时候,可以把整个过程当做是网络的一部分。即有一 个比较大的网络,该网络吃一堆输入,用这堆输入在这个网络里面计算出 µ,σ,接下来产生 一堆输出。这边就会有一个问题了,因为训练数据非常多,现在一个数据集可能有上百万笔数据,GPU 的显存无法把它整个数据集的数据都加载进去。因此,在实现的时候,我们不会 让这一个网络考虑整个训练数据里面的所有样本,而是只会考虑一 个批量里面的样本。比如 批量设 64,这个网络就是把 64 笔数据读进去,计算这 64 笔数据的 µ,σ,对这 64 笔数据做 归一化。因为实际实现的时候,只对一个批量里面的数据做归一化,所以技巧称为批量归一 化。一定要有一个够大的批量,才算得出 µ,σ。所以批量归一化适用于批量大小比较大的时 候,批量大小如果比较大,也许这个批量大小里面的数据就足以表示整个数据集的分布。这个 时候就不需要对整个数据集做特征归一化,而改成只在一个批量上做特征归一化作为近似。
在做批量归一化的时候,如图 3.44 所示,往往还会做如下操作:
其中,⊙ 代表逐元素的相乘。β, γ 可以想成是网络的参数,需要另外再被学习出来。
Q:为什么要加上 β 跟 γ 呢?
A:如果做归一化以后,z˜ 的平均值一定是 0,如果平均值是 0 的话,这会给网络一些 限制,这个限制可能会带来负面的影响,所以需要把 β, γ 加回去,让网络隐藏层的输 出平均值不是 0。让网络学习 β, γ 来调整一下输出的分布,从而来调整 zˆ 的分布。
Q:批量归一化是为了要让每一个不同的维度的范围相同,如果把 γ 跟 β 加进去,这 样不同维度的分布,其范围不会又都不一样了吗?
A:有可能,但是实际上在训练的时候,γ 的初始值都设为 1,所以 γ 值都为 1 的向 量。β 是值全部都是 0 的向量,即零向量。所以让网络在一开始训练的时候,每一个 维度的分布,是比较接近的,也许训练到后来,已经训练够长的一段时间,已经找到一 个比较好的误差表面,走到一个比较好的地方以后,再把 γ, β 慢慢地加进去,所以加 了 γ, β 的批量归一化,往往对训练是有帮助的。
3.7.2 测试时的批量归一化
以上说的都是训练的部分,测试有时候又称为推断(inference)。批量归一化在测试的时 候,会有什么样的问题呢?在测试的时候,我们一次会得到所有的测试数据,确实也可以在测试的数据上面,制造一个一个批量。但是假设系统上线,做一个真正的线上的应用,比如批量 大小设 64,我一定要等 64 笔数据都进来,才做一次做运算,这显然是不行的。
但是在做批量归一化的时候,µ,σ 是用一个批量的数据算出来的。但如果在测试的时候, 根本就没有批量,如何算 µ,σ 呢?所以真正的实现上的解法是这个样子的。批量归一化在测 试的时候,并不需要做什么特别的处理,PyTorch 已经处理好了。在训练的时候,如果有在做 批量归一化,每一个批量计算出来的 µ,σ,都会拿出来算移动平均(moving average)。假 设现在有各个批量计算出来的 µ 1 , µ 2 , µ 3 , · · · · · · , µ t,则可以计算移动平均
其中,µ¯ 是 µ 的个平均值,p 是因子,这也是一个常数,这也是一个超参数,也是需要调的 那种。在 PyTorch 里面,p 设 0.1。计算滑动平均来更新 µ 的平均值。最后在测试的时候,就 不用算批量里面的 µ 跟 σ 了。因为测试的时候,在真正应用上也没有批量,就可以就直接拿 µ¯ 跟 σ¯ ,也就是 µ,σ 在训练的时候,得到的移动平均来取代原来的 µ 跟 σ,如图 3.45 所示, 这就是批量归一化在测试的时候的运作方式。
图 3.46 是从批量归一化原始文献的实验结果,横轴代表的是训练的过程,纵轴代表的是 验证集上的准确率。黑色的虚线是没有做批量归一化的结果,它用的是 inception 的网络(一 种网络以 CNN 为基础的网络架构)。如果有做批量归一化,则是红色的这一条虚线。红色虚 线的训练速度显然比黑色的虚线还要快很多。虽然只要给模型足够的训练的时间,最后会收敛都差不多的准确率。但是红色虚线可以在比较短的时间内跑到一样的准确率。蓝色的菱形 代表说几个点的准确率是一样的。粉红色的线是 sigmoid 函数,一般的认知,但一般都会选择 ReLU,而不是用 sigmoid 函数,因为 sigmoid 函数的训练是比较困难的。但是这边想要强调 的点是,就算是 sigmoid 比较难搞的加批量归一化,还是可以训练的,这边没有 sigmoid,没 有做批量归一化的结果。因为在这个实验上,sigmoid 不加批量归一化,根本连训练都训练不 起来。蓝色的实线跟这个蓝色的虚线呢是把学习率设比较大一点,×5 就是学习率变原来的 5 倍;×30 就是学习率变原来的 30 倍。因为如果做批量归一化,误差表面会比较平滑,比较容 易训练,所以就可以把学习率设大一点。这边有个不好解释的地方,学习率设 30 倍的时候比 5 倍差,作者也没有解释。
3.7.3 内部协变量偏移
接下来的问题就是批量归一化为什么会有帮助呢?原始的批量归一化论文里面提出内部 协变量偏移(internal covariate shift)概念。如图 3.47 所示,假设网络有很多层,-x 通 过第一层后得到 a,a 通过第二层以后得到 b;计算出梯度以后,把 A 更新成 A ′,把 B 这 一层的参数更新成 B ′。但是作者认为说,我们在计算 B 更新到 B ′ 的梯度的时候,这个时 候前一层的参数是 A,或者是前一层的输出是 a。那当前一层从 A 变成 A ′ 的时候,其输出 就从 a 变成 a ′ 。但是我们计算这个梯度的时候,是根据 a 算出来,所以这个更新的方向也 许它适合用在 a 上,但不适合用在 a ′ 上面。因为我们每次都有做批量归一化,就会让 a 和 a ′ 的分布比较接近,(后文又有论文不认可这个观点)也许这样就会对训练有帮助。但是论文“How Does Batch Normalization Help Optimization?” [11] 认为内部协变量偏移有问题。这篇论文从不同的角度来说明内部协变 量偏移不一定是训练网络的时候的一个问题。批量归一化会比较好,可能不一定是因为它解 决了内部协变量偏移。这篇论文里面做了很多实验,比如其比较了训练的时候 a 的分布的变 化,发现不管有没有做批量归一化,其变化都不大。就算是变化很大,对训练也没有太大的伤 害。不管是根据 a 算出来的梯度,还是根据 a ′ 算出来的梯度,方向居然都差不多。内部协变 量偏移可能不是训练网络的时候,最主要的问题,它可能也不是批量归一化会好的一个的关 键。
协变量偏移(covariate shift),训练集和预测集样本分布不一致的问题就叫做协变量偏 移现象,这个词汇是原来就有的,内部协变量偏移是批量归一化的作者自己发明的。
为什么批量归一化会比较好呢,那在这篇“How Does Batch Normalization Help Optimization?”这篇论文从实验和理论上,至少支持批量归一化可以改变误差表面,让误差表面比较不 崎岖这个观点。所以这个观点是有理论的支持,也有实验的佐证的。如果要让网络误差表面 变得比较不崎岖,其实不一定要做批量归一化,还有很多其他的方法都可以让误差表面变得 不崎岖,这篇论文就试了一些其他的方法,发现跟批量归一化表现也差不多,甚至还稍微好 一点,这篇论文的作者也觉得批量归一化是一种偶然的发现,但无论如何,其是一个有用的 方法。其实批量归一化不是唯一的归一化,还有很多归一化方法,比如批量重归一化(batch renormalization)[12]、层归一化(layer normalization)[13]、实例归一化(instance normalization) [14]、组归一化(group normalization)[15]、权重归一化(weight normalization)[16] 和谱归一 化(spectrum normalization)[17]。
HW3(CNN)卷积神经网络-图像分类
myseed = 6666 # set a random seed for reproducibility
# 确保在使用CUDA时,卷积运算具有确定性,以增强实验结果的可重复性
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
# 为numpy和pytorch设置随机种子
np.random.seed(myseed)
torch.manual_seed(myseed)
# 如果使用CUDA,为所有GPU设置随机种子
if torch.cuda.is_available():
torch.cuda.manual_seed_all(myseed)
-
torch.backends.cudnn.deterministic = True
设置PyTorch的CuDNN(NVIDIA的深度学习加速库)以确保卷积操作的确定性。在默认情况下,CuDNN可能会选择不同的算法,这可能会导致相同输入产生不同的输出。设置deterministic
为True
会使CuDNN选择确定性算法,确保卷积操作的一致性。 -
torch.backends.cudnn.benchmark = False
禁用CuDNN的自动选择优化算法(benchmark)。默认情况下,CuDNN会自动选择最优算法以加速计算,这可能会影响结果的可重复性。设置benchmark
为False
可以避免这种情况,以确保每次运行时使用相同的算法。 -
np.random.seed(myseed)
为NumPy的随机数生成器设置种子。这样,在使用NumPy的随机函数(如np.random.rand()
、np.random.randn()
等)时,会生成相同的随机数序列,从而使实验结果在不同的运行中保持一致。 -
torch.manual_seed(myseed)
为PyTorch的CPU随机数生成器设置种子。这样,当使用PyTorch的随机函数(如torch.rand()
、torch.randn()
等)时,会生成相同的随机数序列,确保实验结果的可重复性。 -
if torch.cuda.is_available(): torch.cuda.manual_seed_all(myseed)
如果CUDA(NVIDIA的计算平台)可用,为所有GPU上的随机数生成器设置种子。这确保在使用GPU进行计算时,随机数的生成也是确定性的,使得不同的GPU设备在相同输入下产生相同的随机数序列。
# Normally, We don't need augmentations in testing and validation.
# All we need here is to resize the PIL image and transform it into Tensor.
# 在测试和验证阶段,通常不需要图像增强。
# 我们所需要的只是调整PIL图像的大小并将其转换为Tensor
test_tfm = transforms.Compose([
transforms.Resize((128, 128)), # 将图像调整为大小为128x128的固定形状
transforms.ToTensor(), # 将PIL图像或NumPy数组转换为Tensor
])
# However, it is also possible to use augmentation in the testing phase.
# You may use train_tfm to produce a variety of images and then test using ensemble methods
# 不过,在测试阶段使用图像增强也是有可能的。
# 你可以使用train_tfm生成多种图像,然后使用集成方法进行测试。
train_tfm = transforms.Compose([
# 将图像调整为固定大小(高度和宽度均为128)
transforms.Resize((128, 128)),
# 可能添加一些图像增强变换
transforms.RandomChoice(transforms=[
# 应用TrivialAugmentWide数据增强方法
transforms.TrivialAugmentWide(),
# 返回原始图像
transforms.Lambda(lambda x: x),
],
p=[0.95, 0.05]), # 使用TrivialAugmentWide的概率为0.95,使用原始图像的概率为0.05
# ToTensor()应该是所有变换中的最后一个
transforms.ToTensor(),
])
在训练阶段,我们可以使用图像增强来增加数据的多样性,并改善模型的泛化能力。你也可以在测试阶段使用训练阶段的增强策略,特别是在使用集成方法时,以增加测试的鲁棒性。
transforms.RandomChoice(transforms=[...], p=[0.95, 0.05])
从提供的变换列表中随机选择一个进行应用。transforms.TrivialAugmentWide()
是一种简单的增强方法,会对图像应用多种基本变换,如旋转、翻转等。transforms.Lambda(lambda x: x)
是一个占位符变换,返回原始图像。p
参数指定了选择每种变换的概率:95%的概率选择TrivialAugmentWide
,5%的概率选择原始图像。
- transforms中的Compose是将多个transform组合,形成新的transforms。
- testing时不做data augmentations。
- 可以修改train的data augmentations。
- TrivialAugmentWide 简单数据增强
- transforms.Lambda(lambda x: x)`:这是一个无操作的转换,它会将输入数据原样返回,即不做任何修改。
class FoodDataset(Dataset):
"""
用于加载食品图像数据集的类。
该类继承自Dataset,提供了对食品图像数据集的加载和预处理功能。
它可以自动从指定路径加载所有的jpg图像,并对这些图像应用给定的变换。
"""
def __init__(self,path,tfm=test_tfm,files = None):
"""
初始化FoodDataset实例。
参数:
- path: 图像数据所在的目录路径。
- tfm: 应用于图像的变换方法(默认为测试变换)。
- files: 可选参数,用于直接指定图像文件的路径列表(默认为None)。
"""
super(FoodDataset).__init__()
self.path = path
# 列出目录下所有jpg文件,并按顺序排序
self.files = sorted([os.path.join(path,x) for x in os.listdir(path) if x.endswith(".jpg")])
if files != None:
self.files = files
self.transform = tfm
def __len__(self):
"""
返回数据集中图像的数量。
返回:
- 数据集中的图像数量。
"""
return len(self.files)
def __getitem__(self,idx):
"""
获取给定索引的图像及其标签。
参数:
- idx: 图像在数据集中的索引。
返回:
- im: 应用了变换后的图像。
- label: 图像对应的标签(如果可用)。
"""
fname = self.files[idx]
im = Image.open(fname)
im = self.transform(im)
# 尝试从文件名中提取标签
try:
label = int(fname.split("/")[-1].split("_")[0])
except:
label = -1 # test has no label如果无法提取标签,则设置为-1(测试数据无标签)
return im,label
这段代码定义了一个名为 FoodDataset
的类,用于加载和处理食品图像数据集。这个类继承自 PyTorch 的 Dataset
类,并提供了对图像数据的加载、预处理和标签提取的功能。
-
类的用途:
FoodDataset
类用于加载和预处理食品图像数据集,特别适合在训练、验证和测试阶段使用。它能够自动从指定目录加载图像,并提供了灵活的变换功能,支持数据增强和预处理。 -
变换与数据增强: 在测试和验证阶段,通常只需对图像进行基本的调整(如大小调整),而在训练阶段可以应用更多的变换来增强数据集的多样性。这个类的设计允许用户在初始化时指定不同的变换策略(通过
tfm
参数)。 -
标签提取:
__getitem__
方法中设计了从文件名中提取标签的机制,这对于文件名包含类别信息的情况非常有用。对于测试数据,没有标签时会自动处理为-1
。
class Classifier(nn.Module):
"""
定义一个图像分类器类,继承自PyTorch的nn.Module。
该分类器包含卷积层和全连接层,用于对图像进行分类。
"""
def __init__(self):
"""
初始化函数,构建卷积神经网络的结构。
包含一系列的卷积层、批归一化层、激活函数和池化层。
"""
super(Classifier, self).__init__()
# 定义卷积神经网络的序列结构
self.cnn = nn.Sequential(
nn.Conv2d(3, 64, 3, 1, 1), # 输入通道3,输出通道64,卷积核大小3,步长1,填充1
nn.BatchNorm2d(64), # 批归一化,作用于64个通道
nn.ReLU(), # ReLU激活函数
nn.MaxPool2d(2, 2, 0), # 最大池化,池化窗口大小2,步长2,填充0
nn.Conv2d(64, 128, 3, 1, 1), # 输入通道64,输出通道128,卷积核大小3,步长1,填充1
nn.BatchNorm2d(128), # 批归一化,作用于128个通道
nn.ReLU(),
nn.MaxPool2d(2, 2, 0), # 最大池化,池化窗口大小2,步长2,填充0
nn.Conv2d(128, 256, 3, 1, 1), # 输入通道128,输出通道256,卷积核大小3,步长1,填充1
nn.BatchNorm2d(256), # 批归一化,作用于256个通道
nn.ReLU(),
nn.MaxPool2d(2, 2, 0), # 最大池化,池化窗口大小2,步长2,填充0
nn.Conv2d(256, 512, 3, 1, 1), # 输入通道256,输出通道512,卷积核大小3,步长1,填充1
nn.BatchNorm2d(512), # 批归一化,作用于512个通道
nn.ReLU(),
nn.MaxPool2d(2, 2, 0), # 最大池化,池化窗口大小2,步长2,填充0
nn.Conv2d(512, 512, 3, 1, 1), # 输入通道512,输出通道512,卷积核大小3,步长1,填充1
nn.BatchNorm2d(512), # 批归一化,作用于512个通道
nn.ReLU(),
nn.MaxPool2d(2, 2, 0), # 最大池化,池化窗口大小2,步长2,填充0
)
# 定义全连接神经网络的序列结构
self.fc = nn.Sequential(
nn.Linear(512*4*4, 1024), # 输入大小512*4*4,输出大小1024
nn.ReLU(),
nn.Linear(1024, 512), # 输入大小1024,输出大小512
nn.ReLU(),
nn.Linear(512, 11) # 输入大小512,输出大小11,最终输出11个类别的概率
)
def forward(self, x):
"""
前向传播函数,对输入进行处理。
参数:
x -- 输入的图像数据,形状为(batch_size, 3, 128, 128)
返回:
输出的分类结果,形状为(batch_size, 11)
"""
out = self.cnn(x) # 通过卷积神经网络处理输入
out = out.view(out.size()[0], -1) # 展平输出,以适配全连接层的输入要求
return self.fc(out) # 通过全连接神经网络得到最终输出
卷积层(CNN)和全连接层(FC),用于提取图像特征并进行分类。
# "cuda" only when GPUs are available.
device = "cuda" if torch.cuda.is_available() else "cpu"
# Initialize a model, and put it on the device specified.
# 初始化模型,并将其放置在指定的设备上
model = Classifier().to(device)
# The number of batch size.
batch_size = 64
# The number of training epochs.
# 增加训练轮数
n_epochs = 8
# If no improvement in 'patience' epochs, early stop.
# 如果在'patience'轮中没有改进,则提前停止
patience = 5
# For the classification task, we use cross-entropy as the measurement of performance.
# 对于分类任务,我们使用交叉熵作为性能衡量标准
criterion = nn.CrossEntropyLoss()
# Initialize optimizer, you may fine-tune some hyperparameters such as learning rate on your own.
# 初始化优化器,您可以自行调整一些超参数,如学习率
optimizer = torch.optim.Adam(model.parameters(), lr=0.0003, weight_decay=1e-5)