梯度下降学习率的设定策略
1.学习率的重要性
如果把梯度下降算法比作机器学习中的一把 “神兵利器”,那么学习率就是梯度下降算法这把武器对应的 “内功心法”,只有调好学习率这个超参数,才能让梯度下降算法更好地运作,让模型产生更好的效果。
在《梯度下降算法总结》一文中,我们已经谈到过在实际应用中梯度下降学习算法可能会遇到局部极小值和鞍点两大挑战。那么,什么样的梯度下降才算是 “合格” 的,简单总结一下其实就两个字,“快” 和 “准”。“快”,即收敛速度要尽量快,“准”,即能够准确找到最优解。
也就是说,好的梯度下降是尽量在快的时间找到最优的解。我们结合 《梯度下降算法总结》中的内容,看看可能的影响因素有哪些:
1)学习率设置太小,需要花费过多的时间来收敛
2)学习率设置较大,在最小值附近震荡却无法收敛到最小值
3)进入局部极值点就收敛,没有真正找到的最优解
4)停在鞍点处,不能够在另一维度继续下降
那么除了一些客观的因素外,可能还会有一些主观因素,如业务需求对时间和准确率的侧重点不一样,或者有的场景可能为了减少过拟合,还会适当降低对训练数据准确性来提高模型的泛化能力,比如深度学习中早停止策略,通过合理地提前结束迭代避免过拟合。
梯度下降算法有两个重要的控制因子:一个是步长,由学习率控制;一个是方向,由梯度指定。
因此,要想对梯度下降的 “快” 和 “准” 实现调控,就可以通过调整它的两个控制因子来实现。因梯度方向已经被证明是变化最快的方向,很多时候都会使用梯度方向,而另外一个控制因子学习率则是解决上述影响的关键所在,换句话说,学习率是最影响优化性能的超参数之一。
(注:本篇为第二版,有很多更新,初版发布时间为:2018-08-30)
2.学习率的设定类型
固定学习率
介绍梯度下降时,我们讲到的学习率都是固定不变的,每次迭代每个参数都使用同样的学习率。找到一个比较好的固定学习率非常关键,否则会导致收敛太慢或者不收敛。
差异化学习率
如果数据是稀疏的且特征分布不均,似乎我们更应该给予较少出现的特征一个大的更新。这时可能需要对不同特征对应的参数设定不同的学习率。深度学习的梯度下降算法中 Adagrad 和 Adam 方法都针对每个参数设置了自适应学习率,能起到不错的效果。另外一种差异化设置学习率的方式是对深度学习中不同网络层进行层级学习率调整,在有些场景下可以带来很多好处。
动态调整学习率
动态调整就是我们根据应用场景,在不同的优化阶段能够动态改变学习率,以得到更好的结果。动态调整学习率是本篇的重点内容,为了解决梯度学习在一些复杂问题时出现的挑战,数据科学家们在动态调整学习率的策略上做了很多研究和尝试。
自适应学习率
自适应学习率从某种程度上讲也算是动态调整学习率的范畴,不过更偏向于通过某种算法来根据实时情况计算出最优学习率,而不是人为固定一个简单策略让梯度下降按部就班地实行,常见的自适应学习率算法包括 AdaGrad、RMSprop、Adadelta、Adam 等。这些优化算法会根据梯度的大小、方向或历史信息等自适应地调整学习率的大小,以提高训练的效果和速度。
自适应学习率在 《梯度下降算法总结》中已经详细讲解过,本篇不作讨论。
3.学习率的设定策略
3.1.固定学习率(Fixed Learning Rate)
固定学习率就是在整个优化过程中使用一个固定的学习率,适用于一些小型数据集和简单的任务,也可用于那些目标函数是凸函数的模型或预训练模型的微调。通常为了保证收敛会选择一个稍微小的数值,如 0.01、0.001。
固定学习率的选择对梯度下降影响非常大,当数据集较大或者任务较复杂时,固定学习率可能无法充分优化模型,因为较高的学习率可能导致模型在训练初期不稳定,而较低的学习率可能导致训练过程过于缓慢。下图展示了不同学习率对梯度下降效果的影响:
3.2.使用不同的学习率(Differential Learning Rates)
3.2.1.不同参数使用不同的学习率
通常情况下,我们使用相同的学习率来更新所有的参数。然而,有时候不同的参数可能需要不同的学习率来获得更好的性能。
不同参数使用不同学习率的方法有很多种,下面列举几种常见的方法:
- 手动设置:通过手动设置每个参数的学习率来实现。这需要根据经验进行调整,通常需要花费大量的时间和精力来确定适当的学习率。手动设置学习率的优点是灵活性高,可以根据实际情况进行调整。然而,它需要手动调整参数,并且对于大型模型和复杂任务可能不够高效。
- 分组学习率:将参数分成几个不同的组,每个组使用不同的学习率。这种方法通常用于具有不同属性或特性的参数。例如,在卷积神经网络中,可以将卷积层的参数和全连接层的参数分成两组,然后分别使用不同的学习率进行更新。这种方法的优点是简单易懂,并且可以更好地控制每个参数组的学习过程。
- 自适应学习率方法:这类方法根据参数的更新情况自动调整学习率。其中一种常见的方法是 AdaGrad,它根据参数的梯度平方和来自适应地缩放学习率。另一个常见的方法是 Adam,它结合了梯度的一阶矩估计和二阶矩估计来调整学习率。这些方法的优点是可以自动适应参数的不同性质,并且在训练过程中动态地调整学习率。
不同参数使用不同学习率的方法的优点如下:
- 提高模型性能:使用不同的学习率可以更好地适应不同参数的特性,以及数据中的不同模式。这可以帮助模型更好地收敛,并提高模型的性能。
- 适应稀疏数据:在稀疏数据中,大部分特征都是零或接近零,而只有少数特征具有较大的非零值。通过为不同特征设置不同的学习率,可以更好地适应数据的稀疏性。对于非零特征,可以使用较小的学习率来精细调整它们的权重,以防止过拟合。而对于零特征,可以使用较大的学习率来更快地将它们的权重归零,从而减少对模型的影响。通过这种方式,可以更好地平衡稀疏数据中的非零特征和零特征的学习过程,提高模型的性能和泛化能力。
- 加速训练过程:通过为不同的参数设置合适的学习率,可以加速模型的训练过程。对于某些参数,使用较大的学习率可以更快地更新它们,而对于其他参数,使用较小的学习率可以更细致地调整它们。
- 增强模型的稳定性:不同参数使用不同学习率的方法可以使模型对学习率的选择更加鲁棒。它可以减少训练过程中的震荡,并有助于避免参数更新过大或过小的问题。
3.2.2.不同网络层使用不同的学习率
在深度学习中,将不同网络层使用不同学习率的做法被称为” 层级学习率调整”(Layer-wise Learning Rate Adjustment)。一般情况下,在训练时通过优化网络层会比提高网络深度要更重要,在网络中使用有差别的学习率(Differential Learning rates),可以很好的提高模型性能 。
这种做法的好处如下:
- 改善收敛性:不同网络层的参数可能具有不同的尺度和敏感度。通过为每个层设置适当的学习率,可以更好地平衡参数的更新速度,使得整个网络更容易收敛。较小的学习率可以使参数更新更加稳定,避免在训练过程中出现梯度爆炸或梯度消失的问题。
- 提高性能:某些网络层可能对模型的性能影响更大,因为它们处理的特征更加复杂或关键。通过为这些层设置较小的学习率,可以更加精细地调整它们的权重,提高模型在关键特征上的表达能力,从而提高模型的性能和泛化能力。
- 控制参数更新:有些网络层的参数可能需要更频繁地进行更新,而另一些层可能需要更稳定的更新。通过为不同层设置不同的学习率,可以在一定程度上控制参数的更新速度。例如,对于较浅的网络层,可以使用较大的学习率来更快地学习低级特征,而对于较深的网络层,可以使用较小的学习率来更稳定地学习高级抽象特征。
- 加速训练过程:某些网络层的参数可能需要更多的训练迭代才能收敛,而其他层则可能在较早的训练阶段就能得到较好的收敛。通过为不同层设置不同的学习率,可以加速整个训练过程。较大的学习率可以使收敛较快的层更快地收敛,从而减少整个训练过程的时间。
在计算机视觉深度学习中,通过已有模型来训练深度学习网络,是一种已经被验证过非常可靠高效的方法。目前大部分网络(如 Resnet、VGG 和 Inception 等)都是在 ImageNet 数据集训练的,因此我们要根据所用数据集与 ImageNet 图像的相似性,来适当改变网络权重。当涉及到修改这些权重时,模型的最后一层通常需要最多的变化,而已经经过良好训练以检测基本特征(如边缘和轮廓)的更深层次则需要更少的变化。
在修改这些权重时,我们通常要对模型的最后几层进行修改,因为这些层被用于检测基本特征(如边缘和轮廓),不同数据集有着不同基本特征。
首先,要使用 Fast.ai 库来获得预训练的模型,代码如下1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | from fastai.conv_learner import * # import library for creating learning object for convolutional #networks model = VVG16() # assign model to resnet, vgg, or even your own custom model PATH = './folder_containing_images' data = ImageClassifierData.from_paths(PATH) # create fast ai data object, in this method we use from_paths where # inside PATH each image class is separated into different folders learn = ConvLearner.pretrained(model, data, precompute=True) # create a learn object to quickly utilise state of the art # techniques from the fast ai library |
创建学习对象之后(learn object),通过快速冻结前面网络层并微调后面网络层来解决问题:
1 2 3 4 5 6 7 8 9 | learn.freeze() # freeze layers up to the last one, so weights will not be updated. learning_rate = 0.1 learn.fit(learning_rate, epochs=3) # train only the last layer for a few epochs |
当后面网络层产生了良好效果,我们会应用 “有差别学习率” 的方法来改变前面网络层。在实际中,一般将学习率的缩小倍数设置为 10 倍:
1 2 3 4 5 6 7 8 9 10 | learn.unfreeze() # set requires_grads to be True for all layers, so they can be updated learning_rate = [0.001, 0.01, 0.1] # learning rate is set so that deepest third of layers have a rate of 0.001, # middle layers have a rate of 0.01, and final layers 0.1. learn.fit(learning_rate, epochs=3) # train model for three epoch with using differential learning rates |
3.3.基于 Armijo 准则的动态学习率计算
Armijo 准则是一种用于选择学习率的准则,常用于梯度下降等迭代优化算法中。在实际应用中,通常会使用一种称为线性回溯搜索的方法来满足 Armijo 准则,所以称为基于 Armijo 准则的线性回溯搜索算法。该方法通过不断减小学习率的大小,直到满足 Armijo 准则,来确定合适的学习率。
Armijo 准则基于损失函数值的下降和函数梯度的方向来判断步长的适当性。具体观察:在梯度下降等迭代优化算法中,我们希望学习率足够小,以充分利用当前搜索方向上的优化信息,但又不能过小,以保证算法能够快速收敛。因此,Armijo 准则提供了一个合理的学习率选择策略。
Armijo 准则的表达式如下:
$$f(x_k + \alpha_k \cdot d_k) ≤ f(x_k) + c \cdot \alpha_k \cdot \nabla f(x_k)^T d_k$$
其中,$x_k$ 是当前参数的取值,$d_k$ 是搜索方向,$f(x_k)$ 是目标函数在 $x_k$ 处的值,$c$ 是一个小于 1 的常数。$α_k$ 是学习率,也称为步长。
Armijo 准则要求在当前搜索方向上,学习率乘以搜索方向的梯度的内积要足够小,以保证在该方向上能够获得足够的优化收益。参数 $c$ 控制了这个条件的松紧程度,较小的 $c$ 会要求更严格的条件。
基于 Armijo 准则的线性回溯搜索算计算学习率是在每个迭代过程中通过计算确定单次迭代步长,虽然也是自动调整学习率,但思想和自适应学习率优化算法还是有所不同,自适应学习率算法是指那些根据梯度信息自动调整学习率的优化算法,如 Adam。
学习率的计算标准
既然是计算学习率,我们就需要转换视角,将学习率 $\alpha$看作是未知量,因当前点 $x_k$、当前搜索方向 $d_k$都是已知的,故有下面的关于 $\alpha$的函数:
$$h(\alpha)=f(x_k+\alpha d_k), \;\; \alpha>0$$
如果学习率为 0,则 $h(0)=f(x_k)$,表示 $x$在当前点位 $x_k$没有移动,注意,这里公式中的 $x$就是梯度下降中的参数 $\theta$。
梯度下降的目标是寻找 $f(x)$的最小值,那么在 $x_k$和 $d_k$给定的前提下,即寻找函数 $f(x_k+\alpha d_k)$的最小值,即:
$$\alpha=\arg\underset{\alpha>0}\min{h(\alpha)}=\arg\underset{\alpha>0}\min{f(x_k+\alpha d_k)}$$
这也就意味着,如果 $h(\alpha) $可导,局部最小值处的 $\alpha$满足:
$$ h'(\alpha)=\nabla {f(x_k+\alpha d_k)^Td_k}=0$$
将 $\alpha=0$代入,得:
$$h'(0)=\nabla f(x_k)^T d_k$$
取 $d_k$为负梯度方向,即
$$d_k= – \nabla f(x_k)^T$$
则有:
$$h'(0) = -(\nabla f(x_k)^T)^2 \leq 0$$
如果能够找到足够大的 $\alpha$,使得 $h'(\alpha)>0$,因为是连续函数,则必存在某个值 $\alpha_0$,使得 $h'(\alpha_0)=0$,$\alpha_0$即为要寻找的学习率。
因为不同的问题有不同的目标函数,$f(x_x+\alpha d_k)$求导及其复杂多变,所以我们只能够通过搜索逼近的方式来近似确定最优的 $\alpha$。下面我们看看搜索方法有哪些。
1)二分线性搜索(Bisection Line Search)
二分线性搜索是最简单的处理方式,不断将区间 $[α_1, α_2]$分成两半,选择端点异号的一侧,直到区间足够小或者找到当前最优学习率。
比如:
对于 $h’(α_1)< 0$、$h’(α_2) > 0$: 若 $h’(\frac{α_1+α_2}{2} ) < 0$,那就令 $a_1 = \frac{α_1+α_2}{2}$ 若 $h’( \frac{α_1+α_2}{2} ) > 0$,那就另 $a_2 = \frac{α_1+α_2}{2}$ 重复上面的步骤,直到找到 h’(α)=0 或区间足够小,这样就找到了最优学习率。
2)回溯线性搜索 (Backing Line Search)
基于 Armijo 准则计算搜素方向上的最大步长,其基本思想是沿着搜索方向移动一个较大的步长估计值,然后以迭代形式不断缩减步长,直到该步长使得函数值 $ f(x_k +αd_k) $相对与当前函数值 $f(x_k )$的减小程度大于预设的期望值 (即满足 Armijo 准则) 为止。
$f(x_k+\alpha d_k) \leq f(x_k)+c_1\alpha \nabla{f(x_k)^T} d_k, \;\;c_1\in(0,1)$
公式中 $f(x_k + \alpha d_k)$ 是新位置的函数值, $f(x_k)$是原来的函数值, 新位置比原位置小是基本需求,还要小”$c_1\alpha\nabla{f(x_k)^T} d_k$“这么多,这个就是 Armijo 准则。通过调整 $c_1$可以改变预设期望值,当 $c_1$比较小时,一般迭代次数就会减少。
回溯与二分线性搜索的异同:
- 二分线性搜索的目标是求得满足 $h'(\alpha)≈0$ 的最优步长近似值,而回溯线性搜索放松了对步长的约束,只要步长能使函数值有足够大的变化即可。
- 二分线性搜索可以减少下降次数,但在计算最优步长上花费了不少代价;回溯线性搜索找到一个差不多的步长即可。
3)二次插值法
二次插值法是回溯线性搜索的继续优化,利用了多项式插值 (Interpolation) 方法。多项式插值的思想是通过多项式插值法拟合简单函数,然后根据该简单函数估计原函数的极值点,这里我们使用二次函数来拟合。例如在上面的算法中,我们需要通过使得 $h'(\alpha)=0$来求极值,而使用二次插值法是找到同样过 $\alpha$点的二次函数求极值,结果近似 $h(\alpha)$求极值。
先来看看怎样构造这个二次函数。
如果知道 3 个点,那就可以确定一个二次曲线经过这三个已知点,换句话说为了确定一个二次曲线就需要 3 个类型的信息,因此我们就可以这样想:如果题目给定了在一个点 $x1$处的函数值 $y1=f(x1)$、在该点处的切线值即 x1 处的导数值 $f’(x1)$、$x2$点处的函数值 $y2=f(x2)$,那么也是能唯一的确定一个二次函数,看下图:
而如果 $x1=0$,$x2=a$的话,那这个二次函数的方程就是下面的样子:
$$f(x)=\frac{f(a)-f'(0)a-f(0)}{a^2}x^2+f'(0)x+f(0)$$
上式是这样算出来的: 假设这个二次函数的方程是 $ f(x) = px^2 + qx + z$,因为 $f(0)$、$f’(0)$、$f(a)$是已知的,$f(x)$的导数也可以求出来,即 $f’(x) = 2px + q$,那把这三个值代入 $f(x)$和 $f’(x)$就可以把 $p$、$q$、$z$求出来了,就得出上面的式子了。
对这个式子求极值的方法是:
假设式子为:$h(a) = qx^2 + px + z$
则根据二次函数的特点,$x$在 $- p/(2q)$ 处可以取得极值。
同样的,换到我们当前的问题,当前 $x_k$处的的导数可以求出来,即 $f’(x_k)$已知,也就是 $h’(0)$已知。即 :已知 $h(0)$、$h’(0)$、$h(\alpha_0)$
接下来我们就可以构造出一个二次函数来近似学习率 a 的曲线了,即:
$$h_q(\alpha)=\frac{h(\alpha_0)-h'(0)\alpha_0-h(0)}{\alpha_0^2}\alpha^2+h'(0)\alpha+h(0)$$
注意,这个只是近似,因为只是通过 3 个数据来确定的曲线,和真正的 $h(\alpha)$是有些不同的,不过,虽然这条曲线和真正的曲线有些误差,但可以使用,这就足够了。
而我们的目标是求 $h’(\alpha)=0$时的 $\alpha$,也就是求 $h(\alpha)$的极值,根据上面的结论,极值点为
$$a_1=\frac{h'(0)\alpha_0^2}{2[h'(0)\alpha_0+h(0)-h(\alpha_0)]}$$
接下来求 $h_q (\alpha_1)$的即可2。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | #! /usr/bin/env python # -*- coding: UTF-8 -*- """ @Author: lumingdong.cn @Project: ML @File: learning_rate.py @Create Date: 2018/8/28 0028 9:49 @Version: 1.0 @Description: Now is better than never. ——The Zen of Python """ import numpy as np def Bisection(dfun, dir, x, alpha): """ :param dfun: 梯度函数 :param dir: 梯度方向,-1 为负方向, 1位正方向 :param x: 当前点,向量 :param alpha: 初始学习速率 :return: 返回找到的学习速率 """ # d: 当前点x处的导数,因为要寻找的是当前点处的最佳学习速率alpha,当前点的梯度是固定的,是个值,向量 d = dir * dfun(x) v_ha = np.dot(dfun(x + alpha * d), d) eps = 1e-6 # 设置返回阈值 if abs(v_ha) < eps: # 对于部分函数,似乎不需要迭代很多次就得到很小的值了,所以加了判断,符合条件就不再更新alpha了 # (不知道代码有没有错误,对于二分搜索的性能表现不太清楚,不清楚这种表现正不正常。) return alpha a1 = alpha a2 = alpha v_ha1 = v_ha v_ha2 = v_ha """找到另外一个学习率,确定一个区间""" if v_ha > 0: while v_ha1 > 0: a1 /= 10 v_ha1 = np.dot(dfun(x + a1 * d), d) elif v_ha < 0: while v_ha2 < 0: a2 *= 10 v_ha2 = np.dot(dfun(x + a2 * d), d) else: return alpha """二分线性搜索""" iter_num = 1 maxiter = 1000 while iter_num < maxiter: mid = (a1 + a2) / 2 v_mid = np.dot(dfun(x + mid * d), d) if abs(v_mid) < eps or abs(a2 - a1) < eps: return mid elif v_mid < 0: a1 = mid else: a2 = mid iter_num += 1 return mid def ArmijoBacktrack(fun, dfun, dir, x, alpha, c=0.3): """ 基于Armijo的回溯线性搜索 :param fun: 目标函数,是个函数 :param dfun: 梯度函数 :param dir: 梯度方向,-1 为负方向,1位正方向 :param x: 当前点,向量 :param alpha: 初始学习速率 :param c: 参数c, 一般小于0.5 :return: 返回找到的学习速率 """ d = dir * dfun(x) now = fun(x) nextv = fun(x + alpha * d) count = 50 while nextv < now and count > 0: """寻找最大的alpha""" alpha = alpha * 2 nextv = fun(x + alpha * d) count -= 1 iterstep = 50 slope = np.dot(dfun(x), d) while nextv > now + slope * c * alpha and iterstep > 0: """折半搜索""" alpha = alpha / 2 nextv = fun(x + alpha * d) iterstep -= 1 return alpha def ArmijoQuad(fun, dfun, dir, x, alpha, c=0.3): """ 基于Armijo的二次插值线性搜索 :param fun: 目标函数,是个函数 :param dfun: 梯度函数 :param dir: 梯度方向,-1 为负方向,1位正方向 :param x: 当前点,向量 :param alpha: 初始学习速率 :param c: 参数c, 一般小于0.5 :return: 返回找到的学习速率 """ d = dir * dfun(x) now = fun(x) nextv = fun(x + alpha * d) count = 50 while nextv < now and count > 0: """寻找最大的alpha""" alpha = alpha * 2 nextv = fun(x + alpha * d) count -= 1 iterstep = 50 slope = np.dot(dfun(x), d) while nextv > now + slope * c * alpha and iterstep > 0: """二次插值""" # h'(0) = slope # h(0) = now # h(alpha) = nextv a1 = (slope * alpha * alpha) / (2 * (slope * alpha + now - nextv)) if a1 < 0: # 不满足a > 0,按原来的折半 alpha = alpha / 2 else: alpha = a1 nextv = fun(x + alpha * d) iterstep -= 1 return alpha def GradientDescent(k, fun, dfun, dir, x, alpha, itersteps): """ 梯度下降 :param k: 搜索alpha的算法类型 :param fun: 目标函数,是个函数 :param dfun: 梯度函数 :param dir: 梯度方向,-1 为负方向,1位正方向 :param x: 当前点,向量 :param alpha: 初始学习速率 :param c: 参数c, 一般小于0.5 :return: 返回学习率和函数值的过程数据 """ alpha_list = [] x_list = [] fx_list = [] for i in range(itersteps): if k == 0: # 固定学习率 alpha = alpha elif k == 1: # 二分线性搜索 alpha = Bisection(dfun, dir, x, alpha) elif k == 2: # 回溯搜索 alpha = ArmijoBacktrack(fun, dfun, dir, x, alpha, c=0.3) elif k == 3: # 二次插值 alpha = ArmijoQuad(fun, dfun, dir, x, alpha, c=0.3) else: raise Exception("k must be one of [0, 1, 2, 3]") d = dir * dfun(x) x = x + alpha * d # 保存过程数据 alpha_list.append(alpha) x_list.append(x) fx_list.append(fun(x)) return alpha_list, x_list, fx_list def fun(args): """ x^2+y^4+z^6 :param args: 参数 :return: 函数值 """ return args[0] ** 2 + args[1] ** 4 + args[2] ** 6 # return args[0] ** 4 def dfun(args): """ x^2+y^4+z^6 :param args: 参数 :return: 各参数梯度,向量 """ return np.array([2 * args[0], 4 * args[1] ** 3, 6 * args[2] ** 5]) # return 4*args[0]**3 if __name__ == "__main__": # 基础参数 args = np.array([4, 3, 2], dtype=float) # x k = 3 # 0:固定学习率 1:二分搜索 2:回溯搜索 3:二次查找值 dir = -1 alpha = 0.01 itersteps = 100 # 梯度下降 a, theta, fx = GradientDescent(k, fun, dfun, dir, args, alpha, itersteps) # 画图 import matplotlib.pyplot as plt x = range(itersteps) plt.plot(x, fx) # 设置坐标轴刻度 # my_x_ticks = np.arange(0, itersteps, 5) # my_y_ticks = np.arange(0, 10, 0.05) # plt.xticks(my_x_ticks) # plt.yticks(my_y_ticks) plt.show() |
基于 Armijo 准则的线性回溯搜索算法是一种简单而直接的学习率确定方法,只关注目标函数值的减小程度和当前梯度方向之间的平衡。而自适应学习率优化算法则更加复杂,根据梯度的一阶或二阶信息、统计信息等动态地调整学习率,以适应目标函数的变化情况。相比自适应学习率优化算法,基于 Armijo 准则的线性回溯搜索算法具有以下特点:
优势:
- 精确性:线性回溯搜索算法可以精确地确定每次迭代的步长,以确保在每一步都能够获得足够的下降。这种精确性对于一些特定的优化问题可能更加重要,特别是当函数具有非常陡峭的区域或存在局部极小值时。
- 理论支持:线性回溯搜索算法基于 Armijo 准则,这是一种被广泛研究和验证的优化准则。它能够提供理论保证,确保算法收敛性和全局收敛性。
劣势:
- 计算开销:线性回溯搜索算法需要进行迭代搜索来确定合适的步长,这可能导致额外的计算开销。每次迭代都需要计算新的函数值和梯度值,以及进行试探点的计算。相比之下,自适应调节学习率的算法(如 Adam)通常不需要进行这样的搜索过程,因此计算开销较低。
- 局部最优性:线性回溯搜索算法只能保证找到满足 Armijo 准则的步长,但无法保证找到全局最优解或者跳出局部最优解。这意味着在存在多个局部最优解的问题中,线性回溯搜索算法可能会陷入其中一个局部最优解,而无法跳出。
- 不适用于非光滑问题:线性回溯搜索算法通常用于处理光滑函数。对于非光滑函数(如具有不可导点或不连续点的函数),线性回溯搜索算法可能无法正常工作或者需要进行适当的修改。
基于 Armijo 准则的线性回溯搜索算法在现代优化算法中使用较少,特别是在深度学习等大规模机器学习任务中。
一方面,线性回溯搜索算法需要进行迭代搜索来确定每次迭代的步长,这可能导致较大的计算开销,尤其是对于计算复杂的问题。相比之下,自适应学习率算法(如 AdaGrad、RMSprop、Adadelta、Adam 等)能够根据梯度信息自适应地调整学习率,减少了手动调参的工作,并且在训练过程中通常能够更稳定地收敛。
另一方面,自适应学习率算法在实践中已经被广泛采用,并且在大规模机器学习任务中取得了良好的表现。这些算法结合了梯度信息的统计特性或历史更新的信息,能够更好地适应不同的优化问题,提高了训练的效果和速度。
尽管如此,基于 Armijo 准则的线性回溯搜索算法仍然在某些情况下可能有其应用价值。例如,在某些特殊的优化问题中,可能需要对步长进行更细粒度的控制,或者需要更严格的理论保证。此时,基于 Armijo 准则的线性回溯搜索算法可能是一个合理的选择。
3.4.学习率衰减(Learning Rate Decay)
一般情况下,初始参数所得目标值与要求的最小值距离比较远,随着迭代次数增加,会越来越靠近最小值。学习率衰减的基本思想是学习率随着训练的进行逐渐衰减,即在开始的时候使用较大的学习率,加快靠近最小值的速度,在后来些时候用较小的学习率,提高稳定性,避免因学习率太大跳过最小值,保证能够收敛到最小值。
学习率衰减应用广泛,在深度学习很多模型的优化中起到不错的效果,除了直接应用衰减规则,也有人把早停止和学习率衰减结合起来,比如在迭代 10 次后损失函数没有改善的情况下学习率开始衰减,最终在学习率低于某个确定的阈值时停止。
衰减方式多种多样,主要可参考以下几种方式。
3.4.1.固定衰减率(Fixed Decay Rate)
固定衰减率是最简单的学习率衰减方式,它在每个固定的训练步骤或训练轮数中以固定比率降低学习率的大小。
计算方式:
$${\text {learning_rate}} = {\text{initial_learning_rate}} \times {\text {decay_rate}}$$
其中,
initial_learning_rate
是初始学习率;decay_rate
是衰减率,通常在 0 到 1 之间;
代码示例:
每迭代 1000 次,学习率减半,直到迭代完。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import numpy as np def train_model(learning_rate, num_iterations, iterations_to_decay, decay_rate): for i in range(num_iterations): # 执行模型训练的代码 # 检查是否需要降低学习率 if (i+1) % decay_steps == 0: learning_rate *= decay_rate print(f'{i+1}次迭代,学习率降低为: {learning_rate}') # 继续进行下一次迭代 # 参数 initial_learning_rate = 0.1 num_iterations = 5000 decay_steps = 1000 decay_rate = 0.5 print(f'初始学习率: {initial_learning_rate}') train_model(initial_learning_rate, num_iterations, decay_steps, decay_rate) |
1 2 3 4 5 6 7 | 初始学习率: 0.1 1000次迭代,学习率降低为: 0.05 2000次迭代,学习率降低为: 0.025 3000次迭代,学习率降低为: 0.0125 4000次迭代,学习率降低为: 0.00625 5000次迭代,学习率降低为: 0.003125 |
衰减图示:
3.4.2.分段常数衰减(Piecewise Constant Decay)
分段常数衰减也被称为阶梯衰减,和固定衰减类似,只不过不是按固定比例衰减,而是可以灵活设定规则,在每个阶段使用不同的常数学习率。
计算方式:
无需计算,设定不同的分段规则,在不同的迭代次数范围设定不同的学习率。
代码示例:
TensorFlow 中提供了 PiecewiseConstantDecay 方法可用:
1 2 3 4 | tf.keras.optimizers.schedules.PiecewiseConstantDecay( boundaries, values, name=None ) |
例如可设定分段 boundaries = [1000, 2000]
和学习率 values = [1.0, 0.5, 0.1]
,来表示迭代次数 0 ~ 1000 步时学习率为 1.0,1000 ~ 2000 步时学习率为 0.5,2000 步之后时学习率为 0.1。
1 2 3 4 5 6 7 8 9 | step = tf.Variable(0, trainable=False) boundaries = [1000, 2000] values = [1.0, 0.5, 0.1] learning_rate_fn = keras.optimizers.schedules.PiecewiseConstantDecay( boundaries, values) # Later, whenever we perform an optimization step, we pass in the step. learning_rate = learning_rate_fn(step) |
衰减图示:
注:在 TensorFlow 2.x 中,学习率衰减的功能全被整合到了 keras 的优化器中,参数和 1.x 基本一致,主要差别在于 2.x 中是一个类,用以创建学习率调度器对象,不再是方法,这样可以更好的与优化器结合,如果衰减学习率调度对象直接传入优化器中,则不需要显示传递迭代次数
global_step
变量。另外也可以调用学习率调度器,显示传入迭代次数变量,进行学习率的计算,代码的应用方式可参考下面指数衰减的代码部分。以下是 TensorFlow 1.x 的写法:
tf.train.piecewise_constant(x, boundaries, values, name=None )
# x 相当于global_step
,迭代次数;在 TensorFlow 2.x 中也可以兼容模式调用:
tf.compat.v1.train.piecewise_constant(x, boundaries, values, name=None)
3.4.3.指数衰减(Exponential Decay)
指数衰减是通过指数函数将学习率逐渐降低。指数衰减的一个重要特点是,随着训练的进行,学习率下降得越来越慢。而这种特性,恰恰非常符合梯度下降的优化需求:在训练初期,较大的学习率可以加快模型的收敛速度,帮助模型在初始阶段快速探索损失函数的空间,随着训练的进行,学习率逐渐减小,使模型在接近最优解时更加稳定。
指数衰减在深度学习中广泛应用,特别是在训练过程的后期阶段,可以帮助模型更好地收敛并获得更好的性能。
计算方式:
$$\text{learning_rate} = \text{initial_learning_rate} \times \text{decay_rate}^{\left(\frac{\text{current_step}}{\text{decay_steps}}\right)}$$
其中,
initial_learning_rate
是初始学习率;decay_rate
是衰减率,通常在 0 到 1 之间,较小的衰减率会导致学习率缓慢下降,适合于长时间的训练任务,或者在已经接近最优解时希望继续微调模型。较大的衰减率会使学习率快速下降,适合于快速收敛的训练任务或者初始学习率设置较大的情况。;current_step
是当前的训练步数,表示模型已经进行了多少个训练步骤;decay_steps
是衰减步数,表示在多少个训练步骤后开始进行学习率衰减,衰减步数决定了衰减率指数函数中指数部分的增长速度。较小的衰减步数会导致学习率更快地下降,而较大的衰减步数会使学习率下降得更为缓慢。
随着训练步数的增加,衰减步数的比值 $ (\frac{\text{current_step}}{\text{decay_steps}})$ 会逐渐增大,由于衰减率是一个小于 1 的值,因此衰减率的指数部分逐渐减小,学习率下降也逐渐变慢。
代码示例:
TensorFlow 中提供了 ExponentialDecay 方法可用:
1 2 3 4 | tf.keras.optimizers.schedules.ExponentialDecay( initial_learning_rate, decay_steps, decay_rate, staircase=False, name=None ) |
其计算方式是:
1 2 3 4 5 6 | def decayed_learning_rate(step, staircase=False): if staircase: return initial_learning_rate * decay_rate ^ (step / decay_steps) else: return initial_learning_rate * decay_rate ^ floor(step / decay_steps) |
除了公式中的参数,这里多了一个 staircase
参数,它用于控制学习率在衰减过程中是否采用阶梯状(staircase)的方式。
- 当
staircase
设置为True
时,学习率的衰减会在每个整数倍的衰减步数时发生跳变。换句话说,学习率在衰减步数处会被截断为整数(向下取整 floor),然后再进行指数衰减。这种方式使得学习率在每个整数步数阶段保持不变,然后在下一个整数步数阶段再次衰减。 - 相反,当
staircase
设置为False
时,学习率的衰减是平滑的,没有明显的跳变。学习率会按照指数衰减函数的规则进行连续的衰减。
这两种方式的选择取决于具体的训练需求和问题。在某些情况下,阶梯状的衰减方式可以帮助模型在特定的训练阶段进行快速收敛,或者在某些训练数据集中减少震荡。而平滑衰减方式则更适合需要连续、平滑变化的学习率调整。
实际应用中,可将学习率衰减策略(实例化学习率调度器)直接传入到优化器中,这样不用显式传入当前迭代步数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | initial_learning_rate = 0.1 decay_steps = 500 decay_rate = 0.9 lr_schedule = tf.keras.optimizers.schedules.ExponentialDecay( initial_learning_rate, decay_steps=decay_steps, decay_rate=decay_rate, staircase=True) model.compile(optimizer=tf.keras.optimizers.SGD(learning_rate=lr_schedule), loss='sparse_categorical_crossentropy', metrics=['accuracy']) model.fit(data, labels, epochs=5) |
也可以调用调度器对象,传入迭代步数,计算衰减学习率:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | N = 10000 # number of iterations initial_learning_rate = 0.1 decay_steps = 500 decay_rate = 0.9 learning_rate_fn1 = tf.keras.optimizers.schedules.ExponentialDecay( initial_learning_rate, decay_steps=decay_steps, decay_rate=decay_rate, staircase=True) learning_rate_fn2 = tf.keras.optimizers.schedules.ExponentialDecay( initial_learning_rate, decay_steps=decay_steps, decay_rate=decay_rate, staircase=False) # Later, whenever we perform an optimization step, we pass in the step. Y1 = [] Y2 = [] for step in range(N): lr1 = learning_rate_fn1(step) lr2 = learning_rate_fn2(step) Y1.append(lr1) Y2.append(lr2) # plotting X = range(N) fig = plt.figure() ax = fig.add_subplot(111) # ax.set_ylim([0, 0.55]) plt.plot(X, Y1, 'r-', linewidth=2) plt.plot(X, Y2, 'g-', linewidth=2) plt.title('exponential decay') ax.set_xlabel('step') ax.set_ylabel('learing rate') plt.legend(labels = ['staircase', 'continus'], loc = 'upper right') plt.show() |
衰减图示:
从图中可以看到指数衰减的阶梯式衰减和连续衰减的区别。
3.4.4.倒数衰减(Inverse Decay)
倒数衰减和指数衰减很相似,衰减的方式都是让学习率的衰减速度逐渐变慢,这有助于模型在训练后期更加稳定地收敛到最优解,避免在接近最优解时学习率下降过快导致震荡或错过最优点。
计算方式:
$$\text{learning_rate} = \text{initial_learning_rate} \; /\ ( \text{decay_rate} \times {\left(\frac{\text{current_step}}{\text{decay_steps}}\right) + 1)}$$
其中,
initial_learning_rate
是初始学习率;decay_rate
是衰减率,通常在 0 到 1 之间,较小的衰减率会导致学习率缓慢下降,适合于长时间的训练任务,或者在已经接近最优解时希望继续微调模型。较大的衰减率会使学习率快速下降,适合于快速收敛的训练任务或者初始学习率设置较大的情况。;current_step
是当前的训练步数,表示模型已经进行了多少个训练步骤;decay_steps
是衰减步数,表示在多少个训练步骤后开始进行学习率衰减。
倒数衰减中的 (current_step / decay_steps)
部分决定了学习率的下降速度。随着训练步数的增加,这个比值会逐渐增大,公式的分母部分越大,学习率的下降速度逐渐变慢。倒数衰减中的 (decay_rate * (current_step / decay_steps) + 1)
部分确保了学习率始终保持在一个较小的值。当 current_step
达到 decay_steps
时,分母部分稍大于 1,此时学习率将保持在 initial_learning_rate / decay_rate
的一个较小值。
分母部分加上 + 1
是为了避免学习率衰减为零,以维持训练的持续进行。考虑当 current_step
达到 decay_steps
时,(current_step / decay_steps)
的值将等于 1。如果没有 + 1
的修正,学习率计算公式将变为:
1 2 | learning_rate = initial_learning_rate / (decay_rate * 1) |
此时,学习率将变为初始学习率除以 decay_rate
。这可能导致学习率衰减得过快,甚至衰减为零。当学习率为零时,模型将无法进行进一步的参数更新,导致训练过早停止或无法达到最优解。通过在计算公式中添加 + 1
,可以确保在 current_step
达到 decay_steps
时,(current_step / decay_steps) + 1
的值为 2。这样,即使 decay_rate
较小,学习率仍会保持一个较小的非零值,以便模型可以继续进行参数调整和训练。
代码示例: TensorFlow 中提供了 InverseTimeDecay 方法可用:
1 2 3 4 | tf.keras.optimizers.schedules.InverseTimeDecay( initial_learning_rate, decay_steps, decay_rate, staircase=False, name=None ) |
其计算方式为:
1 2 3 4 5 6 | def decayed_learning_rate(step, staircase=True): if staircase: return initial_learning_rate / (1 + decay_rate * step / decay_step) else: return initial_learning_rate / (1 + decay_rate * floor(step / decay_step)) |
和指数衰减一样,参数中也有 staircase
参数,staircase=False
时连续衰减,staircase=True
时,阶梯式衰减,此时公式中的迭代相关部分加一个向下取整的处理:floor(current_step / decay_steps)
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | ... initial_learning_rate = 0.1 decay_steps = 20 decay_rate = 0.2 learning_rate_fn = keras.optimizers.schedules.InverseTimeDecay( initial_learning_rate, decay_steps, decay_rate) model.compile(optimizer=tf.keras.optimizers.SGD( learning_rate=learning_rate_fn), loss='sparse_categorical_crossentropy', metrics=['accuracy']) model.fit(data, labels, epochs=5) |
衰减图示:
3.4.5.多项式衰减(Polynomial Decay)
多项式衰减中,学习率的衰减率通常由一个多项式函数来定义。最常见的形式是使用多项式的幂函数。在每个训练步骤或训练轮数结束时,通过将学习率乘以一个小于 1 的多项式衰减因子,学习率会逐渐减小。
多项式衰减与指数衰减和倒数衰减目的一样,让学习率的下降更加平滑,在训练早期较高以加快收敛速度,在训练后期逐渐降低以提高稳定性和精度。
计算公式:
$$ \text{learning_rate} = (\text{initial_learning_rate} – \text{end_learning_rate}) \times (1 – \frac{\text{step}}{\text{decay_steps}})^{\text{power}} + \text{end_learning_rate} $$
其中,
initial_learning_rate
是初始学习率;end_learning_rate
设定衰减最终的最小学习率,通常是一个比较小的值,它可以防止学习率在训练过程中减小到零,导致模型无法收敛;step
是当前的训练步数,表示模型已经进行了多少个训练步骤,注意,一个周期内,step
最大为decay_steps
,此时加号前面等于零,学习率为设定的end_learning_rate
;decay_steps
是衰减步数,表示在多少个训练步骤后开始进行学习率衰减;power
是多项式衰减的幂指数,决定了衰减的速度。一般情况下,power
取 1,即线性衰减。如果power
大于 1,则衰减更加缓慢。如果power
小于 1,则衰减更加快速;
这个公式表示了学习率如何随着训练步数 step
的增加而逐渐衰减。随着 step
的增加,衰减率计算核心部分 $(1 – \frac{\text{step}}{\text{decay_steps}}) ^{\text {power}}$会逐渐减小,从而使得学习率逐渐接近 end_learning_rate
,当 step
等于 decay_steps
时,学习率为 end_learning_rate
。这里需要额外处理,step
大于 decay_steps
时,则设定 step=decay_steps
,此时学习率固定为 end_learning_rate
。
代码示例:
TensorFlow 中提供了 PolynomialDecay 方法可用:
1 2 3 4 5 6 7 8 9 | tf.keras.optimizers.schedules.PolynomialDecay( initial_learning_rate, decay_steps, end_learning_rate=0.0001, power=1.0, cycle=False, name=None ) |
其计算方式为:
1 2 3 4 5 6 7 8 9 10 11 12 | def decayed_learning_rate(step, cycle=False): if cycle: decay_steps = decay_steps * ceil(step / decay_steps) return ((initial_learning_rate - end_learning_rate) * (1 - step / decay_steps) ^ (power) ) + end_learning_rate else: step = min(step, decay_steps) return ((initial_learning_rate - end_learning_rate) * (1 - step / decay_steps) ^ (power) ) + end_learning_rate |
这里多了一个 cycle
参数,可以设置是否循环,表示达到最低学习率时,是否升高再降低。循环学习率的方式在有些情况下可以起到非常不错的效果,详解本文后续内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | ... initial_learning_rate = 0.1 end_learning_rate = 0.01 decay_steps = 50 power=0.5 learning_rate_fn = tf.keras.optimizers.schedules.PolynomialDecay( initial_learning_rate, decay_steps, end_learning_rate, power) model.compile(optimizer=tf.keras.optimizers.SGD( learning_rate=learning_rate_fn), loss='sparse_categorical_crossentropy', metrics=['accuracy']) model.fit(data, labels, epochs=5) |
衰减图示:
从图中可以看到学习率是否循环变化的区别。
3.4.6.余弦衰减(Cosine Decay)
学习率的余弦衰减是一种常用的学习率衰减策略,它基于余弦函数的特点,学习率首先缓慢下降,然后加速下降,最后又缓慢下降。这种下降方式非常匹配梯度下降优化过程,初始阶段稳定进入,即便梯度爆炸;中间快速下降,加快收敛;最后阶段以更稳定的方式精细参数,所以余弦衰减可以产生很好的效果,因此被广泛使用。这种学习率余弦衰减机制有些地方也称为余弦退火(Cosine Annealing)。
计算方式:
$${\text {learning_rate}} = {\text {initial_learning_rate}} \times \left((1 – \text{alpha}) \times 0.5 \times \left(1 + \cos(\frac{\pi \times {\text {step}}} {\text {decay_steps}} )\right) + \text{alpha}\right)$$
其中,
initial_learning_rate
是初始学习率;step
是当前的训练步数,表示模型已经进行了多少个训练步骤;decay_steps
是衰减步数,表示在多少个训练步骤后开始进行学习率衰减;alpha
是最小学习率;
注,在实际应用中,余弦衰减也和多项式衰减一样,可以增加最小学习率 alpha
(含义与 end_learning_rate
一样,只是论文的变量命名不同)并限制 step
最大为 decay_step
,以实现设定衰减的最小学习率,具体可参考代码部分。
余弦函数的取值范围为 [-1, 1]
,若当前步数接近衰减步数时,余弦函数的取值接近-1,此时学习率趋近于初始学习率的一半。而在训练的早期阶段,当前步数接近 0,余弦函数的取值接近 1,学习率接近初始学习率。
代码示例:
TensorFlow 中提供了 CosineDecay 方法可用(TensorFlow 2.13):
1 2 3 4 5 6 7 8 9 | tf.keras.optimizers.schedules.CosineDecay( initial_learning_rate, decay_steps, alpha=0.0, name=None, warmup_target=None, warmup_steps=0 ) |
TensorFlow 的计算方式是参考论文 《Loshchilov & Hutter, ICLR2016, SGDR: Stochastic Gradient Descent with Warm Restarts》,可以看到,除了最小学习率 alpha
,代码中还增加了一种额外的 “warmup” 功能,也就是学习率的预热机制。Warmup 是一种学习率调试技巧,它在训练的初始阶段使用较小的学习率,并逐渐增加学习率到设定的初始学习率,以帮助模型更好地启动和稳定训练过程。其中,warmup_target
表示预热阶段的目标学习率,默认为 None
时,则不使用预热机制,warmup_steps
表示预热阶段的持续步数,通常情况下,warmup_target
比 initial_learning_rate
小很多,在 warmup_steps
的持续时间内,学习率从 initial_learning_rate
到 warmup_target
逐步线性增加。
学习率的预热技巧可以带来如下好处:
- 稳定梯度更新:在训练的初始阶段,模型可能会遇到梯度爆炸或不稳定的情况。通过使用较小的学习率进行 warmup,可以帮助模型更稳定地更新梯度,避免训练过程中出现梯度的剧烈波动。
- 促进收敛:在训练初期,模型的参数通常初始值较接近于随机值,可能还没有收敛到良好的区域。通过使用较小的学习率进行 warmup,可以让模型在训练的早期更好地探索参数空间,有助于模型快速适应训练数据,并更快地达到收敛状态。
- 防止过拟合:在训练过程中,学习率过大可能导致模型过早地陷入局部最优解或过拟合现象。通过在训练的初始阶段使用较小的学习率进行 warmup,可以减缓模型对训练数据的过拟合,提高模型的泛化能力。
具体的,预热计算过程为:
1 2 3 4 5 | def warmup_learning_rate(step): completed_fraction = step / warmup_steps total_delta = target_warmup - initial_learning_rate return completed_fraction * total_delta |
衰减计算过程:
1 2 3 4 5 6 7 8 9 10 11 | if warmup_target is None: initial_decay_lr = initial_learning_rate else: initial_decay_lr = warmup_target def decayed_learning_rate(step): step = min(step, decay_steps) cosine_decay = 0.5 * (1 + cos(pi * step / decay_steps)) decayed = (1 - alpha) * cosine_decay + alpha return initial_decay_lr * decayed |
不使用预热的用法:
1 2 3 4 5 | decay_steps = 1000 initial_learning_rate = 0.1 lr_decayed_fn = tf.keras.optimizers.schedules.CosineDecay( initial_learning_rate, decay_steps) |
使用预热的用法:
1 2 3 4 5 6 7 8 9 | decay_steps = 1000 initial_learning_rate = 0 warmup_steps = 1000 target_learning_rate = 0.1 lr_warmup_decayed_fn = tf.keras.optimizers.schedules.CosineDecay( initial_learning_rate, decay_steps, warmup_target=target_learning_rate, warmup_steps=warmup_steps ) |
衰减图示:
3.4.7.重启余弦衰减(Cosine Decay Restarts)
重启余弦衰减也是 《Loshchilov & Hutter, ICLR2016, SGDR: Stochastic Gradient Descent with Warm Restarts》提到的方法,其实就是支持周期性重启的余弦衰减,即在每个周期结束时将学习率重置为初始学习率,并按照余弦衰减的方式重复对学习率衰减。学习率重启方式有助于在训练过程中跳出局部最优解、避免陷入平原区域,并提高模型的泛化能力。
计算方式:
当学习率使用余弦衰减进行调整时,公式可以表示为:
$$\text{learning_rate} = \text{initial_learning_rate} \cdot \text{m_mul} \cdot \left(1 + \cos\left(\frac{\pi \cdot \text{step}}{\text{decay_steps} \cdot \text{t_mul}}\right)\right) / 2 + \text{alpha}$$
其中:
initial_learning_rate
是初始学习率;step
是当前的训练步数,表示模型已经进行了多少个训练步骤;decay_steps
是衰减步数,表示在多少个训练步骤后开始进行学习率衰减;alpha
是最小学习率;t_mul
是周期长度倍增因子,只在重启阶段生效,指定每个周期的长度相对于上一个周期的倍增因子。例如,如果第一个周期的长度为first_decay_steps
,那么第二个周期的长度将为first_decay_steps * t_mul
,第三个周期的长度将为first_decay_steps * t_mul * t_mul
,以此类推。m_mul
初始学习率倍增因子,只在重启阶段生效,指定每个周期的初始学习率相对于上一个周期的倍增因子。例如,如果第一个周期的长度为initial_learning_rate
,那么第二个周期的长度将为initial_learning_rate * m_mul
,第三个周期的长度将为initial_learning_rate * t_mul * t_mul
,以此类推。
这个公式中,$\cos\left(\frac{\pi \cdot \text{step}}{\text{decay_steps} \cdot \text{t_mul}}\right)$ 控制了学习率的余弦衰减部分,它将 $\text{step}$ 限制在一个周期的范围内,并根据当前步数与周期长度的比例计算余弦值。$1 + \cos\left(\frac{\pi \cdot \text{step}}{\text{decay_steps} \cdot \text{t_mul}}\right)$ 与原始余弦函数的取值范围为 $[-1, 1]$,通过将其除以 $2$ 进行缩放,可以将学习率的范围控制在 $[0, \; \text{{initial_learning_rate}}]$。
在每个周期结束时(即 $\text{step} \% ({\text {decay_steps} \cdot \text{t_mul}}) = 0$),学习率会被重置为 $\text{learning_rate} \cdot \text{m_mul}$,并继续按照余弦衰减模式进行调整。
TensorFlow 中提供了 CosineDecayRestarts 方法可用(TensorFlow 2.13):
1 2 3 4 5 6 7 8 9 | tf.keras.optimizers.schedules.CosineDecayRestarts( initial_learning_rate, first_decay_steps, t_mul=2.0, m_mul=1.0, alpha=0.0, name=None ) |
参数同公式。
应用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | N = 600 # number of iterations initial_learning_rate = 0.1 decay_steps = 50 end_learning_rate = 0.01 learning_rate_fn2 = tf.keras.optimizers.schedules.CosineDecayRestarts( initial_learning_rate, decay_steps, t_mul=2.0, m_mul=0.5, alpha=0.0, name=None ) |
上述 t_mul=2
表示每一重启周期的周期长度将是上个周期的 2 倍,t_mul=0.5
表示每一重启周期的初始学习率将是上个周期初始学习率的一半。
衰减图示:
3.5.探索最优初始学习率
Leslie Smith 在一篇 《Cyclical Learning Rates for Training Neural Networks》论文中提出一个非常简单的确定最佳学习率的方法。这种方法可以用来确定最优的初始学习率,也可以界定适合的学习率的取值范围1。
在这种方法中,尝试使用较低学习率来训练神经网络,但是在每个批次中以指数形式增加(或线性增加)。
目前,该方法在 fast.ai 包中已经作为一个函数可直接进行使用。fast.ai 包是由 Jeremy Howard 开发的一种高级 pytorch 包(就像 Keras 之于 Tensorflow)。
相应代码如下:
1 2 3 4 5 | # run on learn object where learning rate is increased exponentially learn.lr_find() # plot graph of learning rate against iterations learn.sched.plot_lr() |
每次迭代后学习率以指数形式增长:
同时,记录每个学习率对应的 Loss 值,然后画出学习率和 Loss 值的关系图:
1 2 3 | # plots the loss against the learning rate learn.sched.plot() |
通过找出学习率最高且 Loss 值仍在下降的值来确定最佳学习率。在上述情况中,该值将为 0.01。
3.6.循环学习率(Cyclical Learning Rate)
循环学习率(Cyclical Learning Rate,CLR)是一种优化算法,通过在训练过程中周期性地改变学习率的大小来帮助模型更好地收敛和泛化。与传统的固定学习率相比,循环学习率允许学习率在一定范围内进行周期性的变化。
循环学习率的核心思想是通过在训练过程中改变学习率的大小来探索更广阔的学习率空间。它通过在一个周期内逐渐增加学习率,然后再逐渐减小学习率,以帮助模型跳出局部最优解、更好地探索全局最优解。循环学习率的变化可以是线性的、三角形的、余弦曲线的等等。
在学习率衰减中,多项式衰减和余弦衰减也有应用循环学习率的思想,不过不同的是,在循环学习率中,学习率是缓慢增加的,然后缓慢减小,以一种循环的形式持续着,个人理解,循环学习率相比于衰减学习率的循环周期应该要更短一些,这种循环起到的效果类似于学习率预热
+ 学习率衰减
+ 学习率重启
。
下图图是 Leslie N. Smith 提出的 Triangular 和 Triangular2 循环学习率方法。左侧的最大学习率和最小学习率保持不变。右侧的区别在于每个周期之后学习率减半3。
3.7.热重启随机梯度下降(SGDR)
梯度下降算法可以通过突然提高学习率,来 “跳出” 局部最小值并找到通向全局最小值的路径,循环学习率和重启的目的都是如此。
Loshchilov 和 Hutter 在 《SGDR: Stochastic Gradient Descent with Warm Restarts》论文中提出了热重启随机梯度下降(Stochastic Gradient Descent with Warm Restarts, SGDR)方法,这种方法将余弦退火与热重启相结合,使用余弦函数作为周期函数,并在每个周期最大值时重新开始学习速率。作者将学习率重启称为 “热重启”,主要有以下几个原因:
- 与冷启动相对:在传统的随机梯度下降(SGD)算法中,当学习率被降低时,模型的参数会从头开始训练。这被称为 “冷启动”。而热重启则不同,它在降低学习率后,模型的参数是从上一次训练的终点继续训练的。这样可以避免模型在每次学习率降低后都要重新学习,从而加快收敛速度。
- 类比于物理学中的热重启:在物理学中,热重启是指将一个系统加热到一定温度,然后突然冷却到初始状态,SGDR 中的学习率也是类似,降低后又突然增加学习率到高值处。
我们在重启余弦衰减部分已经详细讲解过这种方法的实现,TensorFlow 也提供了热重启余弦衰减的调度器,这里重点关注这种设定思路。
除了 TensorFlow,Fast.ai 库也集成了 SGDR 算法,可以快速导入,当调用 learn.fit(learning_rate, epochs)
函数时,学习率在每个周期开始时重置为参数输入时的初始值,然后像上面余弦退火部分描述的那样,逐渐减小1。
也可以设置周期步长递增因子和周期初始学习率递增因子来控制周期重启的方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 | cycle_len = 1 # decide how many epochs it takes for the learning rate to fall to # its minimum point. In this case, 1 epoch cycle_mult=2 # at the end of each cycle, multiply the cycle_len value by 2 learn.fit(0.1, 3, cycle_len=2, cycle_mult=2) # in this case there will be three restarts. The first time with # cycle_len of 1, so it will take 1 epoch to complete the cycle. # cycle_mult=2 so the next cycle with have a length of two epochs, # and the next four. |
3.8.快照集成和随机加权平均
最后将要提到的策略可以说是多个优化方法综合应用的策略,可能已经超出了 “学习率的设定” 主题的范围了,不过,我觉得下面的方法是最近一段时间研究出来的一些非常好的优化方法,因此也包括了进来,权当做是学习率优化的综合应用了。
本小节主要涉及三个优化策略:快照集成(Snapshot Ensembling)、快速几何集成(Fast Geometric Ensembling,FGE)、随机加权平均(Stochastic Weight Averaging,SWA)。
相关内容可参考以下论文:
下面我们详细了解这些优化策略。
从传统的集成学习一路走来
在经典机器学习中,集成学习(Ensemble learning)是非常重要的思想,很多情况下都能带来非常好的性能,因此几乎是机器学习比赛中必用的” 神兵利器 “。
集成学习算法本身不算一种单独的机器学习算法,而是通过构建并结合多个机器学习器来完成学习任务。可以说是” 集百家之所长 “,完美的诠释了” 三个臭皮匠赛过诸葛亮 “。集成学习在机器学习算法中拥有较高的准确率,不足之处就是模型的训练过程可能比较复杂,效率不是很高。
强力的集成学习算法主要有 2 种:基于 Bagging 的算法和基于 Boosting 的算法,基于 Bagging 的代表算法有随机森林,而基于 Boosting 的代表算法则有 Adaboost、GBDT、XGBOOST 等,这部分内容我们后面会单独讲到。
集成学习的思路就是组合若干不同的模型,让它们基于相同的输入做出预测,接着通过某种平均化方法决定集成模型的最终预测。这个决定过程可能是通过简单的投票或取均值,也可能是通过另一个模型,该模型能够基于集成学习中众多模型的预测结果,学习并预测出更加准确的最终结果。岭回归是一种可以组合若干个不同预测的结果的方法,Kaggle 上卫星数据识别热带雨林竞赛的冠军就使用过这一方法。
集成学习的思想同样适用于深度学习,集成应用于深度学习时,组合若干网络的预测以得到一个最终的预测。通常,使用多个不同架构的神经网络得到的性能会更好,因为不同架构的网络一般会在不同的训练样本上犯错,因而集成学习带来的收益会更大。
当然,你也可以集成同一架构的模型,也许效果会出乎意料的好。就好比本小节将要提到的快照集成方法,在训练同一个网络的过程中保存了不同的权值快照,然后在训练之后创建了同一架构、不同权值的集成网络。这么做可以提升测试的表现,同时也超省事,因为你只需要训练一个模型、训练一次就好,只要记得随时保存权值就行。
快照集成应用了我们刚才提到的热重启随机梯度下降(Stochastic Gradient Descent with Warm Restarts, SGDR),这种循环学习率几乎为快照集成量身打造,利用热重启随机梯度下降法的特点,每次收敛到局部极值点的时候就可以缓存一个权重快照,缓存那么几个就可以做集成学习了。
无论是经典机器学习中的集成学习,还是深度学习里面的集成学习,抑或是改良过的快照集成方法,都是模型空间内的集成,它们均是组合若干模型,接着使用这些模型的预测以得到最终的预测结果。而一些数据科学家还提出了一种全新的权值空间内的集成,这就是随机加权平均法,该方法通过组合同一网络在训练的不同阶段的权值得到一个集成,接着使用组合后的权值做出预测。这种方法有两个好处:
- 组合权重后,我们最终仍然得到一个模型,这有利于加速预测。
- 事实证明,这种方法胜过当前最先进的快照集成。
在了解这其实现原理之前,我们首先需要理解损失平面(loss surface)和泛化的解(generalizable solution)。
权重空间内的解
第一个不得不提到的是,经过训练的网络是高维权值空间中的一个点。对给定的架构而言,每个不同的网络权值组合都代表了一个不同的模型。任何给定架构都有无穷的权重组合,因而有无穷多的解。训练神经网络的目标是找到一个特定的解(权值空间中的点),使得训练数据集和测试数据集上的损失函数的值都比较低。
在训练期间,训练算法通过改变权值来改变网络并在权值空间中漫游。梯度下降算法在一个损失平面上漫游,该平面的海拔为损失函数的值。
窄极值和宽极值
坦白的讲,可视化并理解高维权值空间的几何特性非常困难,但我们又不得不去了解它。因为随机梯度下降的本质是,在训练时穿过这一高维空间中的损失平面,试图找到一个良好的解:损失平面上的一个损失值较低的 “点”。不过后来我们发现,这一平面有很多局部极值。但这些局部极值并不都有一样好的性质。
一般极值点会有宽的极值和窄的极值,如下图所示:
数据科学家研究试验后发现:宽的局部极小值在训练和测试过程中产生类似的损失;但对于窄的局部极小值而言,训练和测试中产生的损失就会有很大区别。这意味着,宽的极值比窄的极值有更好的泛化性。4
平坦度可以用来衡量一个解的优劣。其中的原理是,训练数据集和测试数据集会产生相似但不尽相同的损失平面。你可以将其想象为测试平面相对训练平面而言平移了一点。对窄的解来说,一个在测试的时候损失较低的点可能因为这一平移产生变为损失较高的点。这意味着窄的(尖锐的)解的泛化性不好——训练损失低,测试损失高。另一方面,对于宽的(平坦的)解而言,这一平移造成的训练损失和测试损失间的差异较小。
我解释了两种解决方案之间的区别,是因为这篇论文的提出的方法、也是我这篇文章重点介绍的方法,就能带来讨人喜欢的、宽的(平坦的)解。
3.8.1.快照集成(Snapshot Ensembling)
快照集成应用了应用了热重启随机梯度下降(SGDR),最初,SGD 会在权值空间中跳出一大步。接着,由于余弦退火,学习率会逐渐降低,SGD 逐步收敛到局部极小值,缓存权重作为一个模型的 “快照”,把它加入集成模型。然后将学习率恢复到更高的值,这种更高的学习率将算法从局部极小值推到损失面中的随机点,然后使算法再次收敛到另一个局部极小值。重复几次,最后,他们对所有缓存权重集的预测进行平均,以产生最终预测。
上图对比了使用固定学习率的单个模型与使用循环学习率的快照集成的收敛过程,快照集成是在每次学习率周期末尾保存模型,然后在预测时使用。
快照集成的周期长度为 20 到 40 个 epoch。较长的学习率周期是为了在权值空间中找到足够具有差异化的模型,以发挥集成的优势。如果模型太相似,那么集成模型中不同网络的预测将会过于接近,以至于集成并不会带来多大益处了。
快照集成表现优异,提升了模型的表现,但快速几何集成效果更好。
3.8.2.快速几何集成(Fast Geometric Ensembling,FGE)
《Loss Surfaces, Mode Connectivity, and Fast Ensembling of DNNs》中提出的快速几何集成 FGE 和快照集成非常像,但是也有一些独特的特点。它们的不同主要有两点。第一,快速几何集成使用线性分段周期学习率规划,而不是余弦变化。第二,FGE 的周期长度要短得多——2 到 4 个 epoch。乍一看大家肯定直觉上觉得这么短的周期是不对的,因为每个周期结束的时候的得到的模型互相之间离得太近了,这样得到的集成模型没有什么优势。然而作者们发现,在足够不同的模型之间,存在着损失较低的连通路径。我们有机会沿着这些路径用较小的步长行进,同时这些模型也能够有足够大的差异,足够发挥集成的优势。因此,相比快照集成, FGE 表现更好,搜寻模型的步长更小(这也使其训练更快)。
- 左图:根据传统的直觉,良好的局部极小值被高损失区域分隔开来(虚线)
- 中/右图:局部极小值之间存在着路径,这些路径上的损失都很低(实线)。
FGE 沿着这些路径保存快照,从而创建快照的集成。
要从快照集成或 FGE 中受益,需要存储多个模型,接着让每个模型做出预测,之后加以平均以得到最终预测。因此,我们为集成的额外表现支付了更高的算力代价。所以天下没有免费的午餐。真的没有吗?这就是随机加权平均的用武之地了。
3.8.3.随机加权平均(Stochastic Weight Averaging,SWA)
随机加权平均只需快速几何集成的一小部分算力,就可以接近其表现。SWA 可以用在任意架构和数据集上,都会有不错的表现。根据论文中的实验,SWA 可以得到之前提到过的更宽的极小值。在经典认知下,SWA 不算集成,因为在训练的最终阶段你只得到一个模型,但它的表现超过了快照集成,接近 FGE。
- 左图:$\text{W}_1$、$\text{W}_2$、$\text{W}_3$分别代表 3 个独立训练的网络,$\text{W}_{\text{SWA}}$为其平均值。
- 中图:$\text{W}_{\text{SWA}}$ 在测试集上的表现超越了 SGD。
- 右图:$\text{W}_{\text{SWA}}$ 在训练时的损失比 SGD 要高。
- 结合 $\text{W}_{\text{SWA}}$ 在测试集上优于 SGD 的表现,这意味着尽管 $\text{W}_{\text{SWA}}$ 训练时的损失较高,它的泛化性更好。
SWA 的直觉来自以下由经验得到的观察:每个学习率周期得到的局部极小值倾向于堆积在损失平面的低损失值区域的边缘(上图左侧的图形中,褐色区域误差较低,点 W1、W2、3 分别表示 3 个独立训练的网络,位于褐色区域的边缘)。对这些点取平均值,可能得到一个宽阔的泛化解,其损失更低(上图左侧图形中的 WSWA)。
下面是 SWA 的工作原理。它只保存两个模型,而不是许多模型的集成:
- 第一个模型保存模型权值的平均值($\text{W}_{\text{SWA}}$)。在训练结束后,它将是用于预测的最终模型。
- 第二个模型($\text{W}$)将穿过权值空间,基于周期性学习率规划探索权重空间。
SWA 权重更新公式
$$\text{W}_{\text{SWA}} \leftarrow \frac{\text{W}_\text{SWA} \cdot \text{n}_{\text{models}}+\text{W}}{\text{n}_{\text{models}}+1}$$
在每个学习率周期的末尾,第二个模型的当前权重将用来更新第一个模型的权重(公式如上)。因此,在训练阶段,只需训练一个模型,并在内存中储存两个模型。预测时只需要平均模型,基于其进行预测将比之前描述的集成快很多,因为在那种集成中,你需要使用多个模型进行预测,最后再进行平均。
方法实现
论文的作者自己提供了一份 PyTorch 的实现 https://github.com/timgaripov/swa。
此外,基于 fast.ai 库的 SWA 可见 https://github.com/fastai/fastai/pull/276/commits。
4.小结
本文分析了学习率设定在模型优化的重要性,并详细介绍了几种不同学习率设定策略,这里简单总结一下设定学习率的一些经验:
- 固定学习率适合一些少量数据的简单模型,也可用于那些目标函数是凸函数的模型或预训练模型的微调。
- 差异化设置学习率有时候能够起到更好的效果,比如不同参数设置不同学习率,不同网络层设置不同的学习率。
- 基于 Armijo 准则的线性回溯搜索算法可以当做一种动态学习率调整,不过由于计算复杂且无法有效解决陷入局部极小值点和鞍点处的问题,当下较少使用。
- 通过逐步增加学习率并对比损失下降过程,可以用来确定最优的初始学习率,也可以界定适合的学习率的取值范围
- 学习率衰减方法是目前使用最广泛的学习率设定策略,衰减计算方法有很多,可以根据实际场景有很多选择,不过基本思想都是前期使用较大的学习率,加快下降速度,然后逐步衰减到较小的学习率,后期使用较小的学习率精确调参,更稳定地收敛。
- 余弦衰减是一个典型的衰减方法,其学习率的变化特性非常适合梯度下降优化机制,并且可以使用预热机制来稳定前期优化过程的稳定性,避免训练过程中出现梯度的剧烈波动。
- 使用循环和重启,通过突然提高学习率,来 “跳出” 局部最小值并找到通向全局最小值的路径,非常有效的解决梯度下降容易陷入局部极值点和鞍点等问题,也是目前较常用的技巧。
- 最后介绍了快照集成、快速几何集成、随机加权平均等方法,是近段时间比较好的研究成果,也是不错的综合优化方法。
梯度下降是机器学习和深度学习中非常重要的优化算法,而学习率是用好梯度下降法的关键。除了一些其他客观的原因,学习率的设定是影响模型性能好坏的非常重要的因素,所以应该给予足够的重视。
5.参考资料
© 本文链接:https://lumingdong.cn/setting-strategy-of-gradient-descent-learning-rate.html
非常厉害~
good good good!
博主您好,喜欢你的博文,包括推荐,摄影,音乐。想认识您
感谢你的肯定,很高兴认识你~
您好,您写的文章非常非常好,能否有机会认识您!!!我把我的邮箱留下了!!!期待!!!
多谢关注,你可以点击首页右侧信息页的微信 logo,扫描二维码加我微信。
博主您好,想问一下,机器学习实战这本书里说要使学习率避免严格下降,这是什么原因呢?学习率随着迭代的深入而逐渐衰减有什么问题?严格下降怎么理解
理解一:“避免太快下降到很低的值”,下降太快,模型还没迭代几次学习率就非常低了,但模型远没到收敛的时候,这个时候用非常小的学习率继续迭代,速度会很慢,就跟你初始就用了一个非常小的学习率的效果差不多;
理解二:“避免随着迭代一直衰减而不升”,我觉得通常情况下是没问题的,没有必要避免,因为平时用得比较多的也是这种方法,除非在某些场景下,为了避免陷入到局部最小值中,后面会提高一下学习率为了跳出局部最小值,可参考循环学习率的设置方法。
具体哪种意思,你参考书中上下文理解吧。