【学习率】梯度下降学习率的设定策略

转载 卢明冬

参考 

https://www.cnblogs.com/yumoye/p/11055813.html

http://www.elecfans.com/d/722425.html

深度学习论文 - Cyclical Learning Rates for Training Neural Networks

(pytorch-CLR实现)https://blog.csdn.net/weixin_41993767/article/details/87934941

(NN训练 trick - lr 设置)https://baijiahao.baidu.com/s?id=1589357694710519732&wfr=spider&for=pc

手把手教你估算深度神经网络的最优学习率(附代码 & 教程)https://www.sohu.com/a/207909331_197042

https://blog.csdn.net/wja8a45tj1xa/article/details/78630058

bckenstler/CLR keras 实现 https://github.com/bckenstler/CLR

 https://blog.csdn.net/qq_38410428/article/details/88061738

https://www.jianshu.com/p/ad90d747ddb5

 

目录

1.学习率的重要性

2.学习率的设定类型

1)固定学习率

2)不同的参数使用不同的学习率

3)动态调整学习率

4)自适应学习率

 

3.学习率的设定策略

3.1.固定学习率(Fixed Learning Rate)

3.2.学习率衰减(Learning Rate Decay)

3.3.找到合适的学习率

3.4.基于Armijo准则的线性回溯搜索算法

1)二分线性搜索(Bisection Line Search)

2)回溯线性搜索 (Backing Line Search)

3)二次插值法

示例代码

3.5.循环学习率(Cyclical Learning Rate)

3.6.余弦退火(Cosine annealing)

3.7.热重启随机梯度下降(SGDR)

3.8.不同网络层使用不同学习率(Differential Learning Rates)

3.9.快照集成和随机加权平均(Snapshot Ensembling And Stochastic Weight Averaging)

从传统的集成学习一路走来

权重空间内的解

窄极值和宽极值

快照集成(Snapshot Ensembling)

快速几何集成(Fast Geometric Ensembling,FGE)

随机加权平均(Stochastic Weight Averaging,SWA)

方法实现

4.小结

5.参考资料


 

1.学习率的重要性

如果把梯度下降算法比作机器学习中的一把 “神兵利器”,那么学习率就是梯度下降算法这把武器对应的 “内功心法”,只有调好学习率这个超参数,才能让梯度下降算法更好地运作,让模型产生更好的效果。

在《梯度下降算法总结》一文中,我们已经谈到过在实际应用中梯度下降学习算法可能会遇到局部极小值和鞍点两大挑战。那么,什么样的梯度下降才算是 “合格” 的,简单总结一下其实就两个字,“快” 和 “准”。“快”,即收敛速度要尽量快,“准”,即能够准确找到最优解。

也就是说,好的梯度下降是尽量在快的时间找到最优的解。我们结合 《梯度下降算法总结》中的内容,看看可能的影响因素有哪些:

    1)学习率设置太小,需要花费过多的时间来收敛

    2)学习率设置较大,在最小值附近震荡却无法收敛到最小值

    3)进入局部极值点就收敛,没有真正找到的最优解

    4)停在鞍点处,不能够在另一维度继续下降

那么除了一些客观的因素外,可能还会有一些主观因素,如业务需求对时间和准确率的侧重点不一样,或者有的场景可能为了减少过拟合,还会适当降低对训练数据准确性来提高模型的泛化能力,比如深度学习中早停止策略,通过合理地提前结束迭代避免过拟合。

梯度下降算法有两个重要的控制因子:一个是步长,由学习率控制;一个是方向,由梯度指定。

因此,要想对梯度下降的 “快” 和 “准” 实现调控,就可以通过调整它的两个控制因子来实现。因梯度方向已经被证明是变化最快的方向,很多时候都会使用梯度方向,而另外一个控制因子学习率则是解决上述影响的关键所在,换句话说,学习率是最影响优化性能的超参数之一。

2.学习率的设定类型

1)固定学习率

介绍梯度下降时,我们讲到的学习率都是固定不变的,每次迭代每个参数都使用同样的学习率。找到一个比较好的固定学习率非常关键,否则会导致收敛太慢或者不收敛。

2)不同的参数使用不同的学习率

如果数据是稀疏的且特征分布不均,似乎我们更应该给予较少出现的特征一个大的更新。这时可能需要对不同特征对应的参数设定不同的学习率。深度学习的梯度下降算法中 Adagrad 和 Adam 方法都针对每个参数设置了相应的学习率,这部分内容详见《梯度下降算法总结》,本篇不作讨论。

3)动态调整学习率

动态调整就是我们根据应用场景,在不同的优化阶段能够动态改变学习率,以得到更好的结果。动态调整学习率是本篇的重点内容,为了解决梯度学习在一些复杂问题时出现的挑战,数据科学家们在动态调整学习率的策略上做了很多研究和尝试。

4)自适应学习率

自适应学习率从某种程度上讲也算是动态调整学习率的范畴,不过更偏向于通过某种算法来根据实时情况计算出最优学习率,而不是人为固定一个简单策略让梯度下降按部就班地实行。

 

3.学习率的设定策略

3.1.固定学习率(Fixed Learning Rate)

固定学习率适用于那些目标函数是凸函数的模型,通常为了保证收敛会选一个稍微小的数值,如 0.01、0.001。固定学习率的选择对梯度下降影响非常大,下图展示了不同学习率对梯度下降的影响。

 

3.2.学习率衰减(Learning Rate Decay)

一般情况下,初始参数所得目标值与要求的最小值距离比较远,随着迭代次数增加,会越来越靠近最小值。学习率衰减的基本思想是学习率随着训练的进行逐渐衰减,即在开始的时候使用较大的学习率,加快靠近最小值的速度,在后来些时候用较小的学习率,提高稳定性,避免因学习率太大跳过最小值,保证能够收敛到最小值。

衰减方式既可以是线性衰减也可以是指数衰减,可参考以下几种方式 [1]:

 

逐步下降的方法适用于目标函数不太复杂的情况,相比固定学习率方法主要体现在速度提升上,但在神经网络和深度学习场景中,对于跳出众多鞍点和局部极小值的帮助并不大。

3.3.找到合适的学习率

Leslie N. Smith 在一篇 《Cyclical Learning Rates for Training Neural Networks》论文中提出一个非常简单的寻找最佳学习率的方法。这种方法可以用来确定最优的初始学习率,也可以界定适合的学习率的取值范围 [2]。

在这种方法中,我们尝试使用较低学习率来训练神经网络,但是在每个批次中以指数形式增加(或线性增加)。

目前,该方法在 Fast.ai 包中已经作为一个函数可直接进行使用。Fast.ai 包是由 Jeremy Howard 开发的一种高级 pytorch 包(就像 Keras 之于 Tensorflow)。

相应代码如下:

1

2

3

4

# 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

# plots the loss against the learning rate

learn.sched.plot()

 

通过找出学习率最高且 Loss 值仍在下降的值来确定最佳学习率。在上述情况中,该值将为 0.01。

3.4.基于Armijo准则的线性回溯搜索算法

基于 Armijo 准则的线性回溯搜索算法主要目的是通过计算的方式得到合适的学习率 [5]。

学习率的计算标准

1)二分线性搜索(Bisection Line Search)

2)回溯线性搜索 (Backing Line Search)

3)二次插值法

二次插值法是回溯线性搜索的继续优化,利用了多项式插值 (Interpolation) 方法。多项式插值的思想是通过多项式插值法拟合简单函数,然后根据该简单函数估计原函数的极值点,这里我们使用二次函数来拟合。例如在上面的算法中,我们需要通过使得 h′(α)=0 来求极值,而使用二次插值法是找到同样过 α 点的二次函数求极值,结果近似 h(α) 求极值。

先来看看怎样构造这个二次函数。

如果知道 3 个点,那就可以确定一个二次曲线经过这三个已知点,换句话说为了确定一个二次曲线就需要 3 个类型的信息,因此我们就可以这样想:如果题目给定了在一个点 x1 处的函数值 y1=f(x1)、在该点处的切线值即 x1 处的导数值 f′(x1)、x2 点处的函数值 y2=f(x2),那么也是能唯一的确定一个二次函数,看下图:

 

示例代码

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

228

229

230

231

232

233

#! /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()

3.5.循环学习率(Cyclical Learning Rate)

使用较快的学习率也有助于我们在训练中更早地跳过一些局部极小值。

人们也把早停和学习率衰减结合起来,在迭代 10 次后损失函数没有改善的情况下学习率开始衰减,最终在学习率低于某个确定的阈值时停止。

近年来,循环学习率变得流行起来,在循环学习率中,学习率是缓慢增加的,然后缓慢减小,以一种循环的形式持续着 [3]。

 

上图是 Leslie N. Smith 提出的 Triangular 和 Triangular2 循环学习率方法。左侧的最大学习率和最小学习率保持不变。右侧的区别在于每个周期之后学习率减半。

3.6.余弦退火(Cosine annealing)

余弦退火可以当做是学习率衰减的一种方式,早些时候都是使用指数衰减或者线性衰减,现在余弦衰减也被普遍使用 [2]。

在采用小批量随机梯度下降(MBGD/SGD)算法时,神经网络应该越来越接近 Loss 值的全局最小值。当它逐渐接近这个最小值时,学习率应该变得更小来使得模型不会超调且尽可能接近这一点。

余弦退火利用余弦函数来降低学习率,进而解决这个问题,如下图所示:

 

从上图可以看出,随着 x 的增加,余弦值首先缓慢下降,然后加速下降,再次缓慢下降。这种下降模式能和学习率配合,以一种十分有效的计算方式来产生很好的效果。

我们可以用 Fast.ai 库中的 learn.fit() 函数,来快速实现这个算法,在整个周期中不断降低学习率。

1

2

# Calling learn fit automatically takes advantage of cosine annealing

learn.fit(0.1, 1)

效果如下图所示,在一个需要 200 次迭代的周期中学习率不断降低,:

 

同时,在这种方法基础上,我们可以进一步引入重启机制。

3.7.热重启随机梯度下降(SGDR)

在训练时,梯度下降算法可能陷入局部最小值,而不是全局最小值。下图展示了陷入局部最小值的梯度下降算法 [2]。

梯度下降算法可以通过突然提高学习率,来 “跳出” 局部最小值并找到通向全局最小值的路径。Loshchilov 和 Hutter 在《SGDR: Stochastic Gradient Descent with Warm Restarts》论文中提出了热重启随机梯度下降(Stochastic Gradient Descent with Warm Restarts, SGDR)方法,这种方法将余弦退火与热重启相结合,使用余弦函数作为周期函数,并在每个周期最大值时重新开始学习速率。“热重启” 是因为学习率重新开始时并不是从头开始的,而是由模型在最后一步收敛的参数决定的。

 

用 Fast.ai 库可以快速导入 SGDR 算法。当调用 learn.fit(learning_rate, epochs) 函数时,学习率在每个周期开始时重置为参数输入时的初始值,然后像上面余弦退火部分描述的那样,逐渐减小。

上图中每 100 次迭代,学习率下降到最小点,我们称为一个循环。

 

循环的迭代次数也可以是不一样的,下面设定了每个循环所包含的周期都是上一个循环的 2 倍。

1

2

3

4

5

6

7

8

9

10

11

12

# decide how many epochs it takes for the learning rate to fall to

# its minimum point. In this case, 1 epoch

cycle_len = 1

 

# at the end of each cycle, multiply the cycle_len value by 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.

learn.fit(0.1, 3, cycle_len=2, cycle_mult=2)

结果如下图表示:

 

3.8.不同网络层使用不同学习率(Differential Learning Rates)

一般情况下,在训练时通过优化网络层会比提高网络深度要更重要,在网络中使用有差别的学习率(Differential Learning rates),可以很好的提高模型性能 [2]。

 

在计算机视觉深度学习中,通过已有模型来训练深度学习网络,是一种已经被验证过非常可靠高效的方法。目前大部分网络(如 Resnet、VGG 和 Inception 等)都是在 ImageNet 数据集训练的,因此我们要根据所用数据集与 ImageNet 图像的相似性,来适当改变网络权重。

在修改这些权重时,我们通常要对模型的最后几层进行修改,因为这些层被用于检测基本特征(如边缘和轮廓),不同数据集有着不同基本特征。

首先,要使用 Fast.ai 库来获得预训练的模型,代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

# import library for creating learning object for convolutional #networks

from fastai.conv_learner import *

 

# assign model to resnet, vgg, or even your own custom model

model = VVG16()

 

# create fast ai data object, in this method we use from_paths where 

# inside PATH each image class is separated into different folders

PATH = './folder_containing_images' 

data = ImageClassifierData.from_paths(PATH)

 

# create a learn object to quickly utilise state of the art

# techniques from the fast ai library

learn = ConvLearner.pretrained(model, data, precompute=True)

创建学习对象之后(learn object),通过快速冻结前面网络层并微调后面网络层来解决问题:

1

2

3

4

5

6

# freeze layers up to the last one, so weights will not be updated.

learn.freeze()

 

# train only the last layer for a few epochs

learning_rate = 0.1

learn.fit(learning_rate, epochs=3)

当后面网络层产生了良好效果,我们会应用 “有差别学习率” 的方法来改变前面网络层。在实际中,一般将学习率的缩小倍数设置为 10 倍:

1

2

3

4

5

6

7

8

9

# set requires_grads to be True for all layers, so they can be updated

learn.unfreeze()

 

# 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.

learning_rate = [0.001, 0.01, 0.1]

 

# train model for three epoch with using differential learning rates

learn.fit(learning_rate, epochs=3)

 

3.9.快照集成和随机加权平均(Snapshot Ensembling And Stochastic Weight Averaging)

最后将要提到的策略可以说是多个优化方法综合应用的策略,可能已经超出了 “学习率的设定” 主题的范围了,不过,我觉得下面的方法是最近一段时间研究出来的一些非常好的优化方法,因此也包括了进来,权当做是学习率优化的综合应用了。

本小节主要涉及三个优化策略:快照集成(Snapshot Ensembling)、快速几何集成(Fast Geometric Ensembling,FGE)、随机加权平均(Stochastic Weight Averaging,SWA)

相关内容可参考以下论文:

《Snapshot Ensembles: Train 1, get M for free》by Gao Huang et. al

《Loss Surfaces, Mode Connectivity, and Fast Ensembling of DNNs》by Garipov et. al

《Averaging Weights Leads to Wider Optima and Better Generalization》by Izmailov et. al

下面我们详细了解这些优化策略。

从传统的集成学习一路走来

在经典机器学习中,集成学习(Ensemble learning)是非常重要的思想,很多情况下都能带来非常好的性能,因此几乎是机器学习比赛中必用的 “神兵利器”。

集成学习算法本身不算一种单独的机器学习算法,而是通过构建并结合多个机器学习器来完成学习任务。可以说是 “集百家之所长”,完美的诠释了 “三个臭皮匠赛过诸葛亮”。集成学习在机器学习算法中拥有较高的准确率,不足之处就是模型的训练过程可能比较复杂,效率不是很高。

强力的集成学习算法主要有 2 种:基于 Bagging 的算法和基于 Boosting 的算法,基于 Bagging 的代表算法有随机森林,而基于 Boosting 的代表算法则有 Adaboost、GBDT、XGBOOST 等,这部分内容我们后面会单独讲到。

集成学习的思路就是组合若干不同的模型,让它们基于相同的输入做出预测,接着通过某种平均化方法决定集成模型的最终预测。这个决定过程可能是通过简单的投票或取均值,也可能是通过另一个模型,该模型能够基于集成学习中众多模型的预测结果,学习并预测出更加准确的最终结果。岭回归是一种可以组合若干个不同预测的结果的方法,Kaggle 上卫星数据识别热带雨林竞赛的冠军就使用过这一方法 [4]。  

集成学习的思想同样适用于深度学习,集成应用于深度学习时,组合若干网络的预测以得到一个最终的预测。通常,使用多个不同架构的神经网络得到的性能会更好,因为不同架构的网络一般会在不同的训练样本上犯错,因而集成学习带来的收益会更大。

当然,你也可以集成同一架构的模型,也许效果会出乎意料的好。就好比本小节将要提到的快照集成方法,在训练同一个网络的过程中保存了不同的权值快照,然后在训练之后创建了同一架构、不同权值的集成网络。这么做可以提升测试的表现,同时也超省事,因为你只需要训练一个模型、训练一次就好,只要记得随时保存权值就行。

快照集成应用了我们刚才提到的热重启随机梯度下降(Stochastic Gradient Descent with Warm Restarts, SGDR),这种循环学习率几乎为快照集成量身打造,利用热重启随机梯度下降法的特点,每次收敛到局部极值点的时候就可以缓存一个权重快照,缓存那么几个就可以做集成学习了。

无论是经典机器学习中的集成学习,还是深度学习里面的集成学习,抑或是改良过的快照集成方法,都是模型空间内的集成,它们均是组合若干模型,接着使用这些模型的预测以得到最终的预测结果。而一些数据科学家还提出了一种全新的权值空间内的集成,这就是随机加权平均法,该方法通过组合同一网络在训练的不同阶段的权值得到一个集成,接着使用组合后的权值做出预测。这种方法有两个好处:

    • 组合权重后,我们最终仍然得到一个模型,这有利于加速预测。
    • 事实证明,这种方法胜过当前最先进的快照集成。

在了解其实现原理之前,我们首先需要理解损失平面(loss surface)和泛化解(generalizable solution)。

权重空间内的解

第一个不得不提到的是,经过训练的网络是高维权值空间中的一个点。对给定的架构而言,每个不同的网络权值组合都代表了一个不同的模型。任何给定架构都有无穷的权重组合,因而有无穷多的解。训练神经网络的目标是找到一个特定的解(权值空间中的点),使得训练数据集和测试数据集上的损失函数的值都比较低。

在训练期间,训练算法通过改变权值来改变网络并在权值空间中漫游。梯度下降算法在一个损失平面上漫游,该平面的海拔为损失函数的值。

窄极值和宽极值

坦白的讲,可视化并理解高维权值空间的几何特性非常困难,但我们又不得不去了解它。因为随机梯度下降的本质是,在训练时穿过这一高维空间中的损失平面,试图找到一个良好的解——损失平面上的一个损失值较低的 “点”。不过后来我们发现,这一平面有很多局部极值。但这些局部极值并不都有一样好的性质。

一般极值点会有宽的极值和窄的极值,如下图所示:

 

数据科学家研究试验后发现:宽的局部极小值在训练和测试过程中产生类似的损失;但对于窄的局部极小值而言,训练和测试中产生的损失就会有很大区别。这意味着,宽的极值比窄的极值有更好的泛化性。

平坦度可以用来衡量一个解的优劣。其中的原理是,训练数据集和测试数据集会产生相似但不尽相同的损失平面。你可以将其想象为测试平面相对训练平面而言平移了一点。对窄的解来说,一个在测试的时候损失较低的点可能因为这一平移产生变为损失较高的点。这意味着窄的(尖锐的)解的泛化性不好——训练损失低,测试损失高。另一方面,对于宽的(平坦的)解而言,这一平移造成的训练损失和测试损失间的差异较小。

之所以提到窄极值和宽极值,是因为随机加权平均(SWA)就能带来讨人喜欢的、宽的(平坦的)解

快照集成(Snapshot Ensembling)

快照集成应用了应用了热重启随机梯度下降(SGDR),最初,SGD 会在权值空间中跳出一大步。接着,由于余弦退火,学习率会逐渐降低,SGD 逐步收敛到局部极小值,缓存权重作为一个模型的 “快照”,把它加入集成模型。然后将学习率恢复到更高的值,这种更高的学习率将算法从局部极小值推到损失面中的随机点,然后使算法再次收敛到另一个局部极小值。重复几次,最后,他们对所有缓存权重集的预测进行平均,以产生最终预测。

 

上图对比了使用固定学习率的单个模型与使用循环学习率的快照集成的收敛过程,快照集成是在每次学习率周期末尾保存模型,然后在预测时使用。 

快照集成的周期长度为 20 到 40 个 epoch。较长的学习率周期是为了在权值空间中找到足够具有差异化的模型,以发挥集成的优势。如果模型太相似,那么集成模型中不同网络的预测将会过于接近,以至于集成并不会带来多大益处了。

快照集成表现优异,提升了模型的表现,但快速几何集成效果更好。

快速几何集成(Fast Geometric Ensembling,FGE)

《Loss Surfaces, Mode Connectivity, and Fast Ensembling of DNNs》中提出的快速几何集成 FGE 和快照集成非常像,但是也有一些独特的特点。它们的不同主要有两点。第一,快速几何集成使用线性分段周期学习率规划,而不是余弦变化。第二,FGE 的周期长度要短得多——2 到 4 个 epoch。乍一看大家肯定直觉上觉得这么短的周期是不对的,因为每个周期结束的时候的得到的模型互相之间离得太近了,这样得到的集成模型没有什么优势。然而作者们发现,在足够不同的模型之间,存在着损失较低的连通路径。我们有机会沿着这些路径用较小的步长行进,同时这些模型也能够有足够大的差异,足够发挥集成的优势。因此,相比快照集成, FGE 表现更好,搜寻模型的步长更小(这也使其训练更快)。

 

左图:根据传统的直觉,良好的局部极小值被高损失区域分隔开来(虚线)

中/右图:局部极小值之间存在着路径,这些路径上的损失都很低(实线)。

FGE 沿着这些路径保存快照,从而创建快照的集成。

要从快照集成或 FGE 中受益,需要存储多个模型,接着让每个模型做出预测,之后加以平均以得到最终预测。因此,我们为集成的额外表现支付了更高的算力代价。所以天下没有免费的午餐。真的没有吗?这就是随机加权平均的用武之地了。

随机加权平均(Stochastic Weight Averaging,SWA)

随机加权平均只需快速几何集成的一小部分算力,就可以接近其表现。SWA 可以用在任意架构和数据集上,都会有不错的表现。根据论文中的实验,SWA 可以得到我之前提到过的更宽的极小值。在经典认知下,SWA 不算集成,因为在训练的最终阶段你只得到一个模型,但它的表现超过了快照集成,接近 FGE。

 

左图:W1、W2、W3 分别代表 3 个独立训练的网络,WSWA 为其平均值。

中图:WSWA 在测试集上的表现超越了 SGD。

右图:WSWA 在训练时的损失比 SGD 要高。

结合 WSWA 在测试集上优于 SGD 的表现,这意味着尽管 WSWA 训练时的损失较高,它的泛化性更好。

SWA 的直觉来自以下由经验得到的观察:每个学习率周期得到的局部极小值倾向于堆积在损失平面的低损失值区域的边缘(上图左侧的图形中,褐色区域误差较低,点 W1、W2、W3 分别表示 3 个独立训练的网络,位于褐色区域的边缘)。对这些点取平均值,可能得到一个宽阔的泛化解,其损失更低(上图左侧图形中的 WSWA)。

下面是 SWA 的工作原理。它只保存两个模型,而不是许多模型的集成:

    • 第一个模型保存模型权值的平均值(WSWA)。在训练结束后,它将是用于预测的最终模型。
    • 第二个模型(W)将穿过权值空间,基于周期性学习率规划探索权重空间。

SWA 权重更新公式

 

WSWA←WSWA⋅nmodels+Wnmodels+1

在每个学习率周期的末尾,第二个模型的当前权重将用来更新第一个模型的权重。因此,在训练阶段,只需训练一个模型,并在内存中储存两个模型。预测时只需要平均模型,基于其进行预测将比之前描述的集成快很多,因为在那种集成中,你需要使用多个模型进行预测,最后再进行平均。

方法实现

论文的作者自己提供了一份 PyTorch 的实现https://github.com/timgaripov/swa

此外,基于 fast.ai 库的 SWA 可见https://github.com/fastai/fastai/pull/276/commits

4.小结

本文主要介绍了几种梯度下降学习率的设定策略,其中 “固定学习率”、“学习率衰减” 适用于简单不太复杂的应用场景,“基于 Armijo 准则的线性回溯搜索算法” 可以当做一种自适应学习率调整,不过由于计算复杂且无法有效解决陷入局部极小值点和鞍点处的问题,使用的人并不多。在 “找到合适的学习率” 一小节中,我们介绍了一种简单有效的方法,可以快速找到一个适合的学习率,同时这种方法也可以界定学习率设定的合理范围,推荐使用。“热重启随机梯度下降” 是 “循环学习率” 和 “余弦退火” 的结合,可以非常有效的解决梯度下降容易陷入局部极值点和鞍点等问题,它正在成为当前效果最好的、也是最标准的做法,它简单易上手,计算量很轻,可以说事半功倍,尤其在深度学习中表现非常好,推荐使用。最后介绍了 “分层学习率”、“快照集成”,“随机加权平均”,是近段时间比较好的研究成果,也是不错的综合优化方法。

梯度下降是机器学习和深度学习中非常重要的优化算法,而学习率是用好梯度下降法的关键。除了一些其他客观的原因,学习率的设定是影响模型性能好坏的非常重要的因素,所以应该给予足够的重视。最后,还记得上面提到过的 “梯度下降算法有两个重要的控制因子:一个是步长,由学习率控制;一个是方向,由梯度指定。” 吗?我们已经在学习率上有了深入的探索和研究,那在方向上是否还有可以优化的方法?如果搜索方向不是严格的梯度方向是否也可行?这里就涉及使用二阶导数的牛顿法和拟牛顿法了。不过个人觉得它们远不及梯度下降重要,如果有时间再更新这方面的内容吧。

5.参考资料

[1] 小胖蹄儿. learning rate 四种改变方式. CSDN

[2] Samuel Lynn-Evans. Ten Techniques Learned From fast.ai. FloydHub Blog

[3] Hafidz Zulkifli. Understanding Learning Rates and How It Improves Performance in Deep Learning. Towards Data Science

[4] Max Pechyonkin. Stochastic Weight Averaging — a New Way to Get State of the Art Results in Deep Learning. Towards Data Science

[5] 邹博. 机器学习

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章