3.9 优化与正则化
1.梯度下降:在参数的山坡上找谷底
说起来,前面几章我们三番五次提到一个词,叫梯度下降。这一章不妨把它和它那几位朋友一起好好说一说。训练一个模型,说穿了就是在一大堆参数铺成的那片山坡上,找一个让损失最低的谷底。我们先把所有参数统一记作 ,训练目标就是让损失 尽可能小,这里的 就是衡量当前参数下模型表现好坏的那个标量函数。梯度 告诉我们损失会朝哪个方向涨, 就是损失 对参数 求偏导得到的向量。学习率记作 ,它控制每一步迈多大。参数更新的基本公式是:
这里的下标 表示第几次更新, 是更新前的参数, 是更新后的参数。
不过实际训练的时候,没有人会把全部数据一次搬出来算梯度,那样实在太慢。随机梯度下降(Stochastic Gradient Descent,简称SGD)每次只随机取一小撮样本来估算梯度,这一小撮样本就叫一个批量。用一小撮样本估出来的梯度肯定带些噪声,未必指向真正的最优方向,但它胜在算得快,而且这点噪声偶尔还能帮模型跳出那种不上不下的局部小坑,因祸得福。SGD算是整个优化器家族里最朴素的一员,思路最直白,就是顺着负梯度的方向走一步,正因为简单,它常常被拿来做baseline(这个概念后面会专门讲),后面那些花样更多的优化器到底有没有真本事,先跟SGD比一比再说。
批量大小是个需要拿捏的数。批量太小,梯度估计抖得厉害,训练曲线上蹿下跳,批量太大,单步计算又慢又吃显存。批量大小还会影响显卡这种并行硬件能不能被喂饱,得综合着调。
2.学习率和动量:步子和惯性
学习率可以说是整个训练里最重要的一个旋钮,调好了事半功倍,调砸了满盘皆输。其实不少人一上来就随便给学习率填个数,结果模型怎么都不收敛,折腾半天才发觉是这个数在作怪。
学习率设得太大,参数会在低损失的谷底附近来回蹦跳,严重的时候损失直接发散到爆炸,整个训练报废。学习率设得太小,模型挪得跟蜗牛一样,训练半天没什么动静,还可能卡在一个不理想的位置里出不来。
不妨把这件事想成一个小球在崎岖的山坡上往下滚,损失就是海拔。学习率太大,相当于小球每一步弹得老高,越弹越离谱,最后飞出地图。学习率太小,相当于小球几乎不动,永远滚不到谷底。
为了改善这种在狭长山谷里来回蹦跳的毛病,人们给优化器装上了动量(momentum)。动量会把过去几步的更新方向累加起来,形成一种惯性,让小球顺着大方向稳稳地往下滑,避免在两边的山壁之间左右横跳。我之前看过一本讲体育训练的书,里头写到长跑运动员在冲刺阶段更多是顺着前几十公里积累下来的节奏顺势冲过终点,单靠每一步临时发力是撑不到最后的,动量的道理和这个很像。落实到数学上,做法是额外引入一个变量 ,用它来表示当前累积下来的速度,你可以把 想成那个小球带着的动量。每一步更新的时候,新速度 由两部分拼成,一部分是上一轮的老速度 打个折留下来的惯性,另一部分是当前这一步新算出来的梯度,写成式子就是:
这里的 是动量系数,取值在 到 之间,它决定老速度还能保留多少惯性。 越接近 ,惯性越大,小球越刹不住车, 越接近 ,就越退化回原来那个没有动量的普通更新。注意现在更新参数用的就是这个累积起来的速度 ,跟原来那种直接减梯度的做法已经不一样了。
光靠一个动量其实还不够用。动量虽然解决了在狭长山谷里横跳的问题,但它对每一个参数还是一视同仁,用的还是同一个学习率 。可问题是,有些参数对应的那个维度坡面特别陡,稍微一碰梯度就剧烈变化,有些维度又特别平缓,挪半天才走一点点。一套学习率硬套到所有参数头上,结果往往是陡的维度蹦跳过头、平缓的维度又原地磨蹭。这就好比一所学校里排齐步走,腿长的体育生和腿短的同学被要求迈完全一样的步幅,谁都别扭。
为了治这个毛病,自适应优化器登场了,其中最具代表性的一个叫RMSProp。它给每个参数单独维护一个最近梯度平方的平均值,我们把这个量记作 ,它反映这个参数最近梯度折腾得有多厉害。 大,说明这个参数最近梯度起伏剧烈,那这一步的更新幅度就除以 来收一收。反过来 小,说明这个参数最近安安静静,更新幅度相对就被放大了。参数更新的样子大致是:
分母里那个 是一个非常小的正数,比如取 ,专门用来防止分母变成零导致算式爆炸。RMSProp这一手,等于让每个参数自己管自己的步子,陡的维度自动收着点,平缓的维度自动放开点,训练一下子顺畅不少。
那能不能把动量那种带惯性、看历史方向的好处,和RMSProp那种按参数自适应步幅的好处,一并要过来呢?Adam就是干这个的。你可以把Adam粗略理解成动量加自适应的合体。它一边像动量那样维护一个历史更新方向的累积,让优化有惯性、方向走得稳,同时又像RMSProp那样维护一个梯度平方的累积,让每个参数自动调步幅。两份历史信息一拼,参数更新的方向又稳、步子又合适,基本成了现在训练神经网络默认的优化器,做深度学习的人几乎都从Adam起步。当然Adam也不是万能的,在有些任务上带动的SGD反而能找到泛化更好的解,所以具体用哪个还得看任务和调参的手感。
光选好优化器还不够,学习率 本身怎么随着训练进程变化,也是个大学问。从头到尾都用同一个学习率,往往前期太大把训练搞炸,后期又降不到精细的位置。学习率调度就是专门处理这个的,它让 在训练的不同阶段自动变化。最常用的一种叫warmup,翻译过来就是热身。训练刚开始的时候,参数都是随机初始化的,离靠谱的解十万八千里,这时候学习率一大,参数就被梯度带着乱蹿,损失直接飞掉。所以warmup让学习率先从一个很小的值起步,随着训练进行慢慢爬到目标值,相当于让模型先小心翼翼地找方向,方向大致找准了再放开脚步跑。
热身完之后训练进入正轨,这时候又得反过来把学习率慢慢降下来。因为越往后参数离谷底越近,步子还那么大的话,就会在谷底附近反复蹦跳,怎么也落不到底。一种特别流行的降法叫余弦衰减,它让学习率按照一条余弦曲线的形状,从大值平滑地滑到小值。一开始降得慢,中段降得快,最后又缓缓贴到最低值,整个曲线特别平滑,没有突然的跳变,模型从大步流星到精雕细琢过渡得很自然。warmup加上余弦衰减这套搭配,几乎是现在训练大模型的标准配置,许多看起来惊艳的效果,基本都是靠这种细节一点一点抠出来的。
3.正则化:压制过拟合的几种常用手段
模型在训练集上表现生猛,一上测试集就不行,这就是我们的老熟人过拟合。正则化(regularization)是一类专门压制过拟合的手段,它的思路是限制模型的有效复杂度,让模型去学背后的规律,避免陷入死记硬背。
最常见的一种叫权重衰减(weight decay)。它在损失里额外加进去一项,这一项跟参数的平方和挂钩,会一直推着参数往小的方向走,让模型不愿意把参数学得过大、过专断。参数小一点,模型就温和一点,没那么容易走极端去死记硬背。
另一个常用手段叫随机失活(Dropout),招数特别有意思。它在训练过程中,每次都随机屏蔽掉一部分神经元,被屏蔽的神经元这一次完全不参与计算,就跟掉线了一样。这样一来,网络就不能太依赖某一条特定的路径,因为这条路径下次可能就被掐断了,模型被迫学会用多条路径来表达同一件事,泛化能力一下就强了。不妨把它想成一支球队训练时,每次随机让几个人坐冷板凳,剩下的人就得学会互相补位、随时顶上,整支球队反而更不容易在缺人的时候崩盘。
正则化的强度也得跟数据量、模型容量搭配合适。正则化太狠,模型被绑住手脚,该学的学不动,正则化太弱,又压不住过拟合。调到刚刚好那个位置,才是这门手艺的真功夫。
4.光练还不够,还得会算分
模型训完了,损失也降下来了,那怎么知道这个模型到底行不行呢?这事儿光看训练集的损失肯定不够,前面说过,训练集表现好可能只是模型在死记硬背。要客观地评价一个模型,得有一套大家都服气的规矩,这就涉及到两个常被一起提到的词,baseline和benchmark。
baseline通常翻译成基线,指的是你拿来当对照的那个已知模型。它一般是个比较简单、效果已经被大家摸透的模型,比如做图像分类的时候,你先弄一个简单的卷积网络出来,它在测试集上能拿多少分是明摆着的。然后你新搞出来的那些花样更多的模型,得先在这个baseline面前立得住,分数超过它,你才有底气说自己的新设计是真的有用。要是费半天劲训出来的大模型,分数跟一个最简单的baseline差不多,那新加的那些复杂结构基本就是白搭,并没有带来真正的提升。
benchmark则是大家公认的标准评测场,翻译过来叫基准测试。它通常是一个公开的数据集配上一套固定的任务,谁都可以拿自己的模型上去跑,跑出来的分数也能跟别人横向对比。做图像分类的圈子里,ImageNet就是大家公认的一个benchmark,模型在ImageNet上的分类准确率,基本就是衡量它强不强的一把公共尺子。你在自己私下搞的小数据集上跑出97%的准确率,可能只是因为那个数据集太简单,谁上都能拿高分,这种分数一点说服力都没有。把同一个模型放到ImageNet这个benchmark上一跑,跟全世界的模型排在一块儿,到底强不强,立马就清楚了。
记得小张刚开始做实验的时候,在一个特别简单的小数据集上跑出99%的准确率,高兴了半天,结果一放到公开的benchmark上,分数立刻平平无奇,这才意识到之前那个高分只是数据集太容易,跟模型本身好坏没多大关系。
所以做实验讲究先把baseline定清楚,再把模型拉到benchmark上去跟别人比,这样得到的结论才靠谱。这跟考试排名是一个道理,你考了90分到底是高是低,得看这次卷子难不难、全班平均分多少,光盯着自己的分数自顾自地高兴是没什么意义的。
练习
Q1. Adam 优化器为什么能成为训练神经网络默认的选择?它把哪两样好处合到了一起?
Adam 把动量和自适应两样好处合到了一起。一边像动量那样维护历史更新方向的累积,让优化有惯性、方向走得稳;另一边像 RMSProp 那样维护梯度平方的累积,给每个参数单独调步幅,陡的维度收着点、平缓的维度放开点。两份历史信息一拼,更新方向又稳、步子又合适,所以成了默认首选。当然有些任务上带动的 SGD 反而泛化更好,具体还得看任务和调参手感。
Q2. 用动量的更新式 、 来说,动量系数 取 0 和取接近 1 时分别是什么效果?学习率设太大或太小又会怎样?
取 0 时没有惯性,退化回普通 SGD,只按当前梯度走一步; 越接近 1 惯性越大,小球越刹不住车,能顺着大方向稳稳往下滑。学习率太大会在谷底附近来回蹦跳甚至发散爆炸,训练报废;学习率太小则像蜗牛挪动,训练半天没动静,还可能卡在次优位置出不来。
Q3. 模型在训练集上准确率很高、一上测试集就拉胯,这是怎么回事?Dropout 和权重衰减分别怎么压住它?
这是过拟合,模型把训练集死记硬背了,没学到背后的规律。Dropout 在训练时每次随机屏蔽一部分神经元,逼网络用多条路径表达同一件事,不能太依赖某条特定路径,泛化能力变强;权重衰减则在损失里加一项跟参数平方和挂钩的项,一直推着参数往小的方向走,让模型不那么走极端。两样都是压制过拟合的常用手段,强度还得跟数据量、模型容量配合好。