深度學習優化算法最耳熟能詳的就是GD(Gradient Descend)梯度下降,然后又有一個所謂的SGD(Stochastic Gradient Descend)隨機梯度下降,其實還是梯度下降,只不過每次更新梯度不用整個訓練集而是訓練集中的隨機樣本。梯度下降的好處就是用到了當前迭代的一些性質,以至於總能較快找到駐點。而像遺傳算法等智能優化算法,則是基於巨大的計算資源上的。它們並不使用函數特性,通常需要“種群”幾乎遍布整個參數空間才能進行有效的優化,更像是一種暴力搜索。而因為神經網絡參數量太大,幾百萬維的參數量不可能讓種群達到“密布”(即使每個維度兩個“個體”,種群規模也是2的百萬次冪),所以神經網絡優化通常都用基於梯度下降的算法。
現在,神經網絡優化常用的都是梯度下降算法的變體,下面介紹Momentum, RMSProp, Adam這三種最常用的。它們都對梯度下降的某些不足做出了改進。
Momentum
Momentum是動量的意思。它對每次迭代進行平滑處理,以至於不會像GD一樣,當梯度很大時一下子跳地太遠,在梯度較小時又跳得太少。定義很簡單,就比GD多了一步:
$V_t = \beta V_{t-1} + (1-\beta) \nabla \theta_t$
$\theta_{t+1} = \theta_t - \eta V_t$
其中$\theta_t$表示第$t$次迭代的參數張量,$\nabla \theta_t$表示關於它的梯度。$\eta$是模型學習率。$\beta$是平滑因子,也就是積累梯度的權重,越大則之前的梯度對當前影響越大,越小則此次迭代的梯度對當前影響越大,而等於0時Momentum就退化成GD了。$V_{t-1}$表示第$t$步迭代以前累積的梯度動量。
相較GD,加上平滑的概念以后,Momentum會更加注重優化的全局性,這是因為它每次迭代都取決於之前所有梯度的加權和。拿跑步來舉例,GD的每一步都是當前地面坡度最陡的方向,而Momentum則是添加了慣性,每一步都將跑步速度向坡度最陡的方向調整。
RMSProp
RMSProp改進了GD的擺動幅度過大的問題,從而加快收斂速度。迭代式如下:
$\begin{gather}S_t = \beta S_{t-1} + (1-\beta)( \nabla \theta_t)^2 \label{}\end{gather}$
$\begin{gather}\displaystyle \theta_{t+1} = \theta_t - \eta \frac{\nabla \theta_t}{\sqrt{S_t}+\varepsilon}\label{}\end{gather}$
$(1)$式中的$( \nabla \theta_t)^2$和$(2)$式中的分式分別表示按元素進行的平方操作和除法操作。$(2)$式中的$\varepsilon$是為了防止除數為0而設置的較小數,它與張量$\sqrt{S_t}$執行的是按元素進行的加法。
按迭代式可以看出,RMSProp對梯度的方向進行了較弱的規范化,讓梯度的每一個元素向單位值1或-1靠近,這樣一來,優化時的擺動幅度就會減小一些。而又為了不至於靠得太近而直接變成全1和-1,用於規范化的是之前梯度平方的累積量而不是直接用當前梯度的平方。因此,如果$\beta=0$,那就是用當前梯度的平方來規范化,每次更新方向的每個元素值就都是1和-1了。下面來分析各種情況。
以下是分別用GD、Momentum、RMSProp對二元函數$z = x^2+10y^2$進行優化的例子,並且將numpy與pytorch方式都實現了一遍加以比較。初始點位於$(250,-150)$和$(-250,150)$,學習率分別為$0.02,0.02,20$,Momentum和RMSProp的$\beta$都為0.9。以函數值小於1為迭代結束的判斷條件。圖例中顯示了迭代次數,如下:
可以看出RMSProp少走了很多彎路,路線更平緩。Momentum雖然比GD多走很多彎路,但是迭代的次數還是有所下降的。
Adam
以上介紹的兩個優化算法在不同的起始位置似乎各有優缺點,而Adam就是將Momentum和RMSProp的優勢結合起來的算法。下面是迭代式:
$\begin{gather}V_t =\beta_1 V_{t-1} + (1-\beta_1) \nabla \theta_t\label{}\end{gather}$
$\begin{gather}S_t =\beta_2 S_{t-1} + (1-\beta_2)\nabla \theta_t^2 \label{}\end{gather}$
$\begin{gather}\hat{V_t} =\frac{V_t}{1-\beta_1^t}\label{}\end{gather}$
$\begin{gather}\hat{S_t} =\frac{S_t}{1-\beta_2^t} \label{}\end{gather}$
$\begin{gather}\theta_{t+1} = \theta_t - \eta \frac{\hat{V_t}}{\sqrt{\hat{S_t}}+\varepsilon} \label{}\end{gather}$
Adam實際上就是前面兩個算法的簡單結合,它還除以了$1-\beta^t$($\beta^t$表示$\beta$的$t$次方),這是為了讓開始的幾次更新不至於太小(因為乘了$1-\beta$)。隨着迭代往后,$1-\beta^t$接近於1。
依舊是對二元函數$z = x^2+10y^2$進行優化,初始點位於$(250,-150)$和$(-250,150)$,學習率分別為為$0.02,0.02,20,20$。Momentum和RMSProp的$\beta=0.9$,Adam的$\beta_1=0.9$,$\beta_2=0.999$。如下:
Adam繞得厲害,迭代次數也並不是最少的。把$\beta1$改成0.5后,Adam的迭代次數變為了30:
$\beta1$越小,Adam越退化為RMSProp。以上說明不同情況下Adam的參數設置會直接影響迭代效率。
總的來說,Adam就是加上動量的同時,又使用了規范化,讓每次迭代能用上之前的梯度,並且更新步伐的每個元素稍微靠近1和-1一些。而這樣有什么好處,只能實踐出真知。
總結
這些基於梯度下降的優化算法都大同小異,它們在不同情況下發揮時好時壞,有的改良算法在某些情況下效果可能比原始的GD還差。所以選擇優化算法還是要視情況而定,我們應該選擇對高概率情況優化效果好的算法。另外,以上使用迭代次數來比較算法的優劣性並不嚴謹,因為不同算法每次迭代的計算量都不同。像Adam的計算量差不多是Momentum和RMSProp的兩倍,如果要嚴謹,還是得用計算時間作比較。當然,相較於原始SGD,Adam等帶動量的優化算法的優勢並不只在於迭代消耗上,它們也為迭代跳出局部最優提供了幫助。所以,僅僅分析時間也並不全面,應該把跳出局部最優等能力也考慮在內。
代碼
完整比較代碼:
1 import numpy as np 2 import matplotlib.pyplot as plt 3 import torch 4 from torch.optim import RMSprop, Adam, SGD 5 6 def func(x, y): 7 return x ** 2 + 10 * y ** 2 8 def grad(x, y): 9 return np.array([2 * x, 20 * y]) 10 def paint(histories:dict): 11 x, y = np.linspace(-400,400,1000), np.linspace(-200,200,1000) 12 X, Y = np.meshgrid(x, y) 13 Z = func(X, Y) 14 plt.contourf(X,Y,Z,levels=20) 15 16 for his in histories: 17 plt.plot(histories[his][:,0],histories[his][:,1], label = his + '-' + str(histories[his].shape[0])) 18 19 plt.legend() 20 plt.show() 21 def optimization(point, opt, if_torch = False): 22 his = [] 23 if not if_torch: 24 t = 0 25 while(True): 26 his.append(point) 27 z = func(point[0], point[1]) 28 if z < 1: 29 print(z) 30 break 31 t+=1 32 point = point - opt.step(point, t) 33 else: 34 while(True): 35 his.append(np.array(point.detach())) 36 z = func(point[0], point[1]) 37 if z < 1: 38 print(z) 39 break 40 opt.zero_grad() 41 z.backward() 42 opt.step() 43 his = np.array(his) 44 return his 45 46 class update_SGD(): 47 def __init__(self, lr = 0.02): 48 self.lr = lr 49 def step(self, point, *arg): 50 return grad(point[0], point[1]) * self.lr 51 class update_Momentum(): 52 def __init__(self, lr = 0.02, beta = 0.9): 53 self.lr = lr 54 self.v = np.array([0,0]) 55 self.beta = beta 56 def step(self, point, *arg): 57 g = grad(point[0], point[1]) 58 self.v = self.beta * self.v + (1 - self.beta) * g 59 return self.v * self.lr 60 class update_RMSProp(): 61 def __init__(self, lr = 0.02, beta = 0.9): 62 self.lr = lr 63 self.s = np.array([0,0]) 64 self.beta = beta 65 def step(self, point, *arg): 66 g = grad(point[0], point[1]) 67 self.s = self.beta * self.s + (1 - self.beta) * (g ** 2) 68 return self.lr * g / (self.s ** 0.5) #* np.sum(self.s)**0.5 69 class update_Adam(): 70 def __init__(self, lr = 0.02, beta1 = 0.9, beta2=0.999): 71 self.lr = lr 72 self.v = np.array([0,0]) 73 self.s = np.array([0,0]) 74 self.beta1 = beta1 75 self.beta2 = beta2 76 def step(self, point, t:int): 77 g = grad(point[0], point[1]) 78 self.v = self.beta1 * self.v + (1 - self.beta1) * g 79 self.s = self.beta2 * self.s + (1 - self.beta2) * (g ** 2) 80 v_ = self.v/(1 - self.beta1 ** t) 81 s_ = self.s/(1 - self.beta2 ** t) 82 return v_/(s_**0.5) * self.lr #* np.sum(self.s)**0.5 83 start_point = np.array([250., -150]) 84 t_start_point = -start_point 85 histories = {} 86 87 opt = update_SGD(0.02) 88 his = optimization(start_point[:], opt) 89 histories["GD"] = his 90 91 opt = update_Momentum(0.02, 0.9) 92 his = optimization(start_point[:], opt) 93 histories["Momentum"] = his 94 95 opt = update_RMSProp(20, 0.9) 96 his = optimization(start_point[:], opt) 97 histories["RMSProp"] = his 98 99 opt = update_Adam(20, 0.5, 0.999) 100 his = optimization(start_point[:], opt) 101 histories["Adam"] = his 102 103 104 point = torch.tensor(t_start_point, requires_grad=True) 105 opt = SGD([point], 0.02) 106 his = optimization(point, opt, True) 107 histories["Torch SGD"] = his 108 109 point = torch.tensor(t_start_point, requires_grad=True) 110 opt = RMSprop([point], 20, 0.9) 111 his = optimization(point, opt, True) 112 histories["Torch RMSProp"] = his 113 114 point = torch.tensor(t_start_point, requires_grad=True) 115 opt = Adam([point], 20, (0.5, 0.999)) 116 his = optimization(point, opt, True) 117 histories["Torch Adam"] = his 118 119 paint(histories)