寒假和开学这段时间过的兵荒马乱,辗转多回终于又回到了这本书的记录。
这几天看视频总算是理解了之前一直很模糊的transformer和注意力机制,感谢小破站神经网络的视频和up主们。
接下来学习的是按理来说机器学习的入门——优化算法。
在深度学习中,我们通常会定义“损失函数”,我们使用优化算法使损失函数达到最小从而得到好的模型。
在之前的代码中不少看到“SGD”、“Adam”几个词,它们的真正面目也在本章中揭开。
第十一章:优化算法
11.1 优化和深度学习
这里需要先说明一点,就是优化和深度学习的目标其实不是完全一致的。优化的目标只有最小化损失函数,而深度学习的目标主要在选取最能拟合数据的模型,也就是实际损失最小,需要模型的泛化性。
在之前的章节我们大多是学习去寻找这个最好的模型。
而本章只考虑优化方面。
深度学习中的优化挑战主要来自于三个方面:局部最小值、鞍点、梯度消失。
局部最小值
这个很好理解。
鞍点
函数除了在最小点之外在鞍点也会陷入梯度为0,鞍点既不是局部最大也不是局部最小,但它梯度就是为0,并且在高维中出现得很频繁,让人苦恼。
可以通过黑塞矩阵判断是否为鞍点。
-
当函数在零梯度位置处的Hessian矩阵的特征值全部为正值时,我们有该函数的局部最小值;
-
当函数在零梯度位置处的Hessian矩阵的特征值全部为负值时,我们有该函数的局部最大值;
-
当函数在零梯度位置处的Hessian矩阵的特征值为负值和正值时,我们有该函数的一个鞍点
对于高维问题,鞍点出现的可能性非常大,起码要比局部最小值大。
实际问题中为什么不能通过计算黑塞矩阵特征值判断鞍点呢?因为实际参数量非常非常之大,可以达到几百亿,计算起来不现实。
所以在实际问题中一般采用动量法等方法防止陷入鞍点。
这里还要提一嘴,就是凸函数黑塞矩阵特征值永远不为负,意思是凸函数没有鞍点和局部最大值,非常好优化,但是实际问题很少很少有优化函数是凸函数的……
梯度消失
梯度消失是可能遇到的最隐蔽的问题,在从前使用sigmoid函数和tanh函数作为激活函数时非常常见,但引入ReLU函数后就得到了很好的解决。
出现原因是梯度随着反向传播逐渐变小变小变小最后变得非常小,使得训练过程变得十分缓慢甚至停滞。
之前学的引入残差快也很好的解决了这个问题,因为这样保证了每一层的梯度至少都有保底1。
总之虽然遇到了这么多棘手的问题,但还是有一系列强大的算法表现良好脱颖而出,况且我们也不一定追求完美主义,得到局部最优或其他近似解也不是不可以。
11.2 凸性
这里涉及到一些数学概念,比如凸集。
在凸集里,两个点的连线上所有点也在这个凸集里。
凸集的交集一定是凸集;凸集的并集不一定是凸集。
有了凸集,就可以引入凸函数。
其实也很好理解,将函数上面的部分视为一个集合,如果这个区域是凸集,那么函数就是凸函数。
比如抛物线和指数函数就是凸函数,余弦函数就不是。
(虽然深度学习中优化函数大部分都是非凸的,但是可能是局部凸的,所以学习理解凸函数还是有必要的。)
詹森不等式是凸函数定义的一种推广,大概意思是凸函数的期望不小于期望的凸函数。
接下来是凸函数的一些性质,主要是数学证明,这里不多证明,之说结论。
1.局部极小值是全局最小值
感觉这个不需要证明,看图就很容易理解。
这个性质也是凸函数为什么会出现在这本书的原因,优化时只要找到梯度为0的点就找到了全局最优解。
2.凸函数的下水平集是凸的
下水平集是指函数内f(x)小于某个值的x集合。
就是在函数图像上画一条横线,在横线下面的部分就是下水平集。
这个也很好理解,毕竟凸函数下水平集肯定是连续的,凸函数中连续的集合就凸。
3.当且仅当二阶导数f''(x)>=0时函数是凸的
意思就是函数是凸函数和函数的f''(x)>=0是充分必要条件。
这个证明也不难。
意思就是可以理解为凸函数的梯度是不断上升的。
这个点也非常重要,它意味着函数黑塞矩阵特征值不为负数,也就是没有鞍点和局部最大值点,当梯度为0时就一定是局部最小值点。
凸函数可以很好处理“约束”,比如一个约束优化问题:
f是目标函数,要使其最小;c是约束函数,必须满足c(x)<=0,可能有多个约束条件。
凸约束可以通过拉格朗日函数进行添加,实践中只需要在目标函数中加上一个惩罚就可以了。(权重衰减)
也可以使用投影,将原本最优解投影为在约束内最优解(区域内距离原本最优解最近的解)。
11.3 梯度下降
梯度下降可以优化目标函数主要源自泰勒展开。
比如说,将 x 移动一小段距离 :
其中O(…)是个很小很小的值,可以忽略。
这个是单变量函数,所以导数f'(x)就是梯度,可以看出 和 f'(x) 方向一致时f会增大,也就是在负梯度上移动的
会减小 f 的值。
所以我们就可以将 设为负的导数乘以一个学习率。这样的话:
学习率固定为正,所以这样就保证了每次都在往 f 变小的方向行走。
只要导数不为0,就可以一直优化下去。
所以机器学习中,学习率的选取很重要,选不好容易陷入局部最小值或者收敛过慢或者无法收敛。
这就是一维梯度下降的基本原理。
再考虑多元梯度下降,其实和一维的差不多,只不过梯度应该是下降最快的那个方向。
所以 就设为
,选择合适的学习率
就变成了熟悉的梯度下降算法。
解释完梯度,接着就是学习率选取的问题。
比如说我们可以利用牛顿法,使用黑塞矩阵(但对于深度学习来说其实并不现实,因为要储存和计算 个参数,所以仅作一种思路)。
一个函数的泰勒展开:
黑塞矩阵可以理解为是目标函数的二阶导,。
由于我们要找的点是 的点,通过泰勒展开忽略O(…)这个很小的值,再对两边进行求导可以得到:
然后由于要找到点 ,所以令左边为0:
因此
发现学习率取到 收敛效果最好,所以每次都取这个值会大大加快收敛。
但是!
很遗憾这种方法只能在凸函数用,之前说过在非凸函数中黑塞矩阵特征值不一定都为正,而学习率必须得是正的,并且之前也说过参数量巨大的情况下计算黑塞矩阵不现实。
根据收敛性分析,这样的做法确实很有效,但是存储黑塞矩阵的成本高昂,或许可以对其改良,比如只计算黑塞矩阵对角线的值。
虽然不如完整黑塞矩阵精确,但也不错,相当于预估了黑塞矩阵的值,起码预估了它的大概尺度。
另外还有一个关于解决梯度下降可能会超过目标点的想法,就结合线搜索。
也就是说使用梯度给出的方向,然后进行二分搜索,确定哪个学习率能使 f 取得最小值。
这样的算法收敛迅速,不过对于深度学习,这还是不太可行,每一步都要评估目标函数,成本太高。
11.4 随机梯度下降
在之前的章节中我们一直都是使用随机梯度下降。
这一节解释一下为什么要用这个。
在深度学习中,目标函数通常是训练集数据样本中每一个样本的损失函数的平均值。
损失函数
意思是等于所有训练样本损失函数之和。
而损失函数梯度计算公式为 ,随着n线性增长,计算复杂度较高。
随机梯度下降(SGD)可以减小这种复杂度,就是每次迭代随机抽取索引 i ,计算梯度以更新x:
这样做可以对平均梯度进行估计,大大减小复杂度,但在接近极小值的时候,因为数据噪杂,迭代不稳定,质量不一定会好。
所以我们转向学习率 η 改善方案。
我们首先想到可以使用动态变化的学习率,用随时间变化的 η(t) 代替原本固定的 η ,迭代次数越多,学习率降得越低,这样能使在接近最小值时能够更好收敛。
一些基本策略:
随后作者根据实验说明了原本随机迭代1000次都无法收敛得很好的函数在使用了多项式衰减后,仅迭代50次就能得到很好收敛。
不过注意以上前提都是凸函数,最大限度减少非线性非凸函数问题是NP困难的,很难获得收敛保证。
另外,在实际当中,我们使用的是无替换随机抽取。
11.5 小批量随机梯度下降
之前我们讲解的都是两者极端情况:一种是每次选取所有样本的损失计算梯度,一种是每次只随机取一个样本的损失计算梯度。
两者折中方案就是“小批量随机梯度下降”。
主要是想让CPU和GPU充分利用向量化(计算矩阵可以并行化,会更高效)。
巧的是最近选修了并行计算这门课,做了很多并行计算矩阵乘法,对这个还是有一定了解的。
总之计算小批量会比计算单个元素更充分地利用计算资源,所以实际上使用小批量会更好,在之前章节中有很多地方也使用了批量计算,不出意外也是这个原因。
作者做了实验,比较不同梯度下降方法收敛的速度:
可以看出梯度下降(gd)和随机梯度下降(sgd)走向两个极端,SGD收敛速度快,但是需要更多时间达到同样损失,而小批量则是取了两者折中。
一般来说,小批量随机梯度下降比另外两个收敛要快,且风险小。
11.6 动量法
动量法的意思大概是,将前面的迭代用的方向加一些加到现在迭代的方向上。
按照公式大概是这样:
当前迭代的方向:
后面那一坨就是根据批量数据损失函数梯度计算的当前应该迭代的方向简化的版本,不要想的太复杂。总之就是当前迭代的方向。
然后可以计算当前的动量:
当前的动量为取了之前的动量的一部分再加上现在的方向。
在一般梯度下降中,可能会出现在不该收敛快的方向上收敛快,在不该收敛慢的方向收敛慢,导致迟迟达不到想要的最低点。
比如:
可以看出在x2方向收敛快,不断抖动,在最该收敛的x1方向反而收敛慢。
动量法可以很好解决这个问题,因为每次迭代方向不仅取决于原本梯度,还取决于上一次收敛方向。
比如在第二步时,原本要往下收敛这么大的距离,如果考虑上一次收敛方向,就可以抵消一部分这个方向的距离,并且增加往右收敛的距离。
回到上面的公式,就可以使用 而不是
产生如下更新式子:
意思就是虽然还是要计算 ,但不以它作为收敛方向了,而是以动量
,这个动量结合了
和前面的动量。
使用动量法后:
肉眼可见变好了很多。
实际中可以通过:
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, momentum=momentum)
来使用动量法。添加momentum超参数加入动量的影响,就是之前公式里的 β 。默认不加动量影响时 β = 0 。
然后作者分析了在凸二次函数中动量法的有效性,但是在一维函数中,由于没有方向这个东西,动量法可能没有那么适用(需要对学习率η和动量参数β还要优化函数有限定范围)。
11.7 AdaGrad算法
在自然语言处理中,我们经常得到稀疏矩阵,只有那个预测的字符概率大,剩下的数值都非常小。
在梯度下降中,一般要求学习率慢慢降低,诸如自然语言处理的任务中那么多特征,某几个参数可能已经达到很好的结果,但是一些不常见的特征有关的参数还没有收敛。
为了解决这个问题,可以使用为每个参数设置独立学习率解决。用代替原来的η。其中 s(i,t) 代表直到 t 时刻时观测到 i 的次数。(c是个很小的常数确保分母不为零)
而AdaGrad算法中 s(i,t) 代表之前梯度的平方和,目的差不多。
s(i,t+1) 更新:
就是每一次增加此次梯度的平方,使学习率慢慢下降,如果梯度增长得慢,学习率下降得也慢,就使步幅大一些;反之如果梯度变化快,就使学习率下降快一些。
这样的话梯度较小的坐标就会更平滑的处理。
好吧用人话解释解释将梯度下降看作下山,如果在一个方向上走了很久就适当减小这个方向的学习率。
并且这个方法有个很好的地方在于它不像引入动量法那样会显著增加计算成本,计算成本还是主要集中在计算损失函数和其梯度上。
实际中可以通过:
trainer = torch.optim.Adagrad(model.parameters(), lr=0.01)
来使用自适应梯度算法。
这个方法优点是对稀疏矩阵特别有效,但缺点是可能面临学习率降得特别快的情况,如何缓解这一点我们在后面展开讨论。
11.8 RMSProp算法
AdamGrad算法固然好,但通常只适合于凸问题,并且看它的公式,每次都要加当前梯度,
到后面越加越大,缺乏约束。
解决此问题的方法有二:①使用 ,但这样需要 t 慢慢变大之后才有效;②使用泄露平均值,即
,其中 γ > 0。这就是RMSProp算法。
详细写一遍公式就是这样:
取各种 γ 可以看出梯度下降变化:
这和上一节差不多,没什么好讲的,直接上简洁实现代码。
实际中可以通过:
trainer = torch.optim.RMSprop
来使用RMSProp算法。
11.9 Adadelta算法
这个也是AdaGrad的变体,Adadelta减少了学习率适应坐标的数量,广义上这个算法没有学习率,而是使用变化量本身作为未来变化的基准。
和RMSProp算法类似,相当于使用更新
。
区别在于 的更新,这里不使用学习率,而是直接这样更新 x:
。
这个 是由
计算来的:
。(
是一个很小的数,用于保持数值稳定)
也就是相当于将学习率和损失函数梯度融合了。
所以就有了上面的。
实际中可以通过:
trainer = torch.optim.Adadelta
来使用Adadelta算法。
11.10 Adam算法
回顾一下之前的梯度下降算法:
随机梯度下降:每次取一个随机样本损失函数计算梯度,大大减少了计算量。
小批量随机梯度下降:每次取小批量样本损失函数计算梯度,高效,加快收敛。
动量法:每次迭代还要考虑之前迭代的方向。
AdaGrad:根据先前的梯度动态改变不同方向的学习率。
RMSProp:对改变学习率进行缩放。
Adadelta:不再使用学习率,将梯度、先前的梯度融合在一起。
而本节要学的Adam算法则是将这些方法全部融合在一起。
Adam最关键的组成部分之一是指数加权移动平均值估计梯度的动量和二次矩。
其中 和Adadelta里的类似,是学习率(步长)和梯度(方向)的结合:
。
然后 代表动量,
代表“梯度”(更新方向)。β 就是一个权重,是个非负加权参数。
设置 β 的初始值为一个较大的数,v 和 s 初始值都为0,但这样一开始就会有很大的初始值偏差。解决方法是根据,重新调整设置 v 和 s :
最开始的时候就可以直接是,
。
然后就可以写出更新方程:
以上就是Adam算法的思想,可以看出它的设计灵感很清晰,动量和方向在状态变化中清晰可见,并且明确学习率,使我们还是可以对步长进行一定的控制。
实际中可以通过:
trainer = torch.optim.Adam
来使用Adam算法。
Yogi更新
尽管如此,Adam算法还是有一个缺陷。
但 的梯度爆炸时,可能无法收敛,因为
每次要加
,对噪声也很敏感。
一开始提出的改进是换成,但这样如果
太小或者更新很慢时,
又会容易忽略前面的
。
于是一个更有效的解决方法就诞生了:
这就是Yogi更新。更新规模不再取决于偏差的量。
11.11 学习率调度器
之前我们一直在谈论如何更新权重向量,而不是更新速率,这一节主要讲学习率的调节。
按理来说,我们希望学习率衰减,但要比 慢,才能达到理想效果。
作者使用了一个LeNet实验对此进行说明:
默认设置下(不调整学习率),准确率变化:
每次迭代之和下调学习率,:
可以看出曲线更加平滑,并且很有效的缓解了过拟合(test acc比train acc小)的情况,不过并不能完全解释为什么缓解了过拟合。
接着来讲讲有哪些边训练边降低学习率的策略,也就是学习率变化的公式。
1.因子调度器
乘法衰减,每次都将学习率乘以一个在(0,1)间的 α。
学习率变化图像:
然后为了避免学习率太小,通常要设置一个阈值 :
。
2.多因子调度器
分段,每一段保持恒定学习率。
设置一个个迭代次数区域,下一段就是上一段学习率乘以一个 α 。
学习率变化图像:
只有达到某一个驻点才降低学习率。
3.余弦调度器
想一想余弦函数的图像,一开始降得很慢,然后慢慢降得快,最后又变得平稳。
余弦调度器就是这个思路,直接将余弦函数设为学习率变化趋势,对于超过半个周期了的 t ,就将学习率固定为那个最小值。
这个方法要规定学习率的初始值和最小值,设置一个合适的周期。
学习率变化图像:
预热
预热不是一种调度器,和上面两个不一样,就单独划分出来了。
预热是一个对最一开始学习率进行调整的思路,想法是初始学习率不能太高导致发散也不能一直太低更新太慢,于是就可以增加一个预热期。
学习率变化图像:
最开始的时候先添加一个直线上升的学习率直到调度器的最大值,然后再接入使用不同调度器。
到此这一节就学完了,“优化”在神经网络中有多种用途,对于同样的训练,选择不同的优化算法和学习率调度可以有效减少训练时间,甚至防止过拟合。