【學習率】梯度下降學習率的設定策略

轉載 盧明冬

參考 

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] 鄒博. 機器學習

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