摘要:梯度下降推導與優化算法的理解和Python實現。Adam 方法結合了上述的 動量(Momentum) 和 自適應(Adaptive) ,同時對梯度和學習率進行動態調整。

梯度下降推導與優化算法的理解和Python實現

目錄

  1. 梯度下降算法推導

  2. 優化算法的理解和Python實現

  3. SGD

  4. Momentum

  5. Nestrov

  6. AdaGrad

  7. RMSprop

  8. Adam

  9. 算法的表現

1

梯度下降算法推導

模型的算法就是爲了通過模型學習,使得訓練集的輸入獲得的實際輸出與理想輸出儘可能相近。 極大似然函數 的本質就是衡量在某個參數下, 樣本整體估計和真實情況一樣的概率 交叉熵函數 的本質是衡量樣本 預測值與真實值之間的差距 ,差距越大代表越不相似

1. 爲什麼要最小化損失函數而不是最大化模型模型正確識別的數目?

我們將不同的損失函數都定義爲損失函數: ;因爲最大化模型正確識別的數目的函數並不是關於  的平滑函數,而交叉熵等損失函數可以更容易地調整   來使得模型進行訓練,然後再進行模型準確率的計算,這是一種曲徑折躍的解決問題的方式。

2. 如何推導梯度下降?爲什麼梯度下降的更新方向是梯度的負方向?

損失函數 是一個包含多個參數的函數,假設將損失函數簡化爲只包含兩個參數的 , 如下圖所示,我們的目標就是找到函數 的全局最小值。當然,在實際工作中,得到一組參數使得損失函數達到全局最小值是一種理想情況,更一般的情況則是根據評價指標去評價模型是否可以得到一個我們能夠接受的結果。

下面開始推導

假設在的方向移動 ,在 的方向移動 ,那麼 的變化爲:

(1)

最小化損失函數簡而言之就是損失函數的值隨着時間越來越小,可得目標函數   ,因爲  ,   ,寫成向量表示,設   ,   ,(1)更新爲:

(2)

如何令 呢?假設令  ,那麼(2)更新爲:

(3)

因爲,那麼可以看到(3)中的  是符合優化目標的, 這從側面也解釋了爲什麼梯度下降的更新方向是梯度的負方向

將上述過程重複多次, 就會達到一個極小值,這就是梯度下降的推導,將其應用到神經網絡模型中,就是 用梯度向量和學習率調整 ,所以:

2

優化算法的理解和Python實現

在推導了梯度下降算法,再來看各個優化算法也就不難了。 引用【1】 中總結的框架,首先定義:待優化參數: ,目標函數: ,初始學習率 。

而後,開始進行迭代優化。在每個epoch  :

  1. 計算目標函數關於當前參數的梯度:

  2. 根據歷史梯度計算一階動量和二階動量:

  3. 計算當前時刻的下降梯度:

  4. 根據下降梯度進行更新:

掌握了這個框架,你可以輕輕鬆鬆設計自己的優化算法。步驟3、4對於各個算法都是一致的,主要的差別就體現在1和2上。

注:下面的內容大部分取自引用【2】和【3】

3

SGD

隨機梯度下降法不用多說,每一個參數按照梯度的方向來減小以追求最小化損失函數,梯度下降法目前主要分爲三種方法,區別在於每次參數更新時計算的樣本數據量不同:批量梯度下降法( BGD , Batch Gradient Descent),隨機梯度下降法( SGD , Stochastic Gradient Descent)及小批量梯度下降法(Mini-batch Gradient Descent)。

SGD缺點

  • 選擇合適的learning rate比較困難 ,學習率太低會收斂緩慢,學習率過高會使收斂時的 波動過大

  • 所有參數都是用同樣的learning rate

  • SGD容易收斂到局部最優,並且在某些情況下可能被困在鞍點

更新方式

Python實現

class SGD:
    def __init__(self, lr=0.01):
        self.lr = lr

    def update(self, params, grads):
        for key in params.keys():
            params[key] -= self.lr * grads[key]

4

Momentum

在梯度下降的基礎上加入了動量,動量優化方法引入物理學中的動量思想:當我們將一個小球從山上滾下來,沒有阻力時,它的動量會越來越大,但是如果遇到了阻力,速度就會變小。momentum算法思想:參數更新時在一定程度上保留之前更新的方向,同時又利用當前batch的梯度微調最終的更新方向,簡言之就是通過積累之前的動量來加速當前的梯度。下面的式子中,   表示動量,   表示動量因子,通常取值0.9或者近似值。

更新方式

Python實現

class Momentum:
    def __init__(self, lr=0.01, momemtum=0.9):
        self.lr = lr
        self.momemtum = momemtum
        self.v = None

    def update(self, params, grads):
        if self.v is None:
            self.v = {}
            for key, val in params.items():
                self.v[key] = np.zeros_like(val)

        for key in params.keys():
            self.v[key] = self.momemtum * self.v[key] - self.lr * grads[key]
            params[key] += self.v[key]

5

Nestrov

Nestrov也是一種動量更新的方式,但是與普通動量方式不同的是,Nestrov爲了加速收斂,提前按照之前的動量走了一步,然後求導後按梯度再走一步。

更新方式

但是這樣一來,就給實現帶來了很大的麻煩,因爲我們當前是在W的位置上,無法求得W+αv處的梯度,所以我們要進行一定改變。由於W與W+αv對參數來說沒有什麼區別,所以我們可以假設當前的參數就是W+αv。就像下圖,按照Nestrov的本意,在0處應該先按照棕色的箭頭走αv到1,然後求得1處的梯度,按照梯度走一步到2。

現在,我們假設當前的W就是1處的參數,但是,當前的動量v仍然是0處的動量,那麼更新方式就可以寫作:

爲了便於理解, W和v的更新可以看做是 空間中向量相加  的方式,這樣一來,動量v就由0處的動量更新到了下一步的2處的動量。但是下一輪的W相應的應該在3處,所以W還要再走一步αv,即完整的更新過程應該如下所示:

第二行的v是第一行更新的結果,爲了統一v的表示,更新過程還可以寫作:

Python實現

class Nestrov:
    def __init__(self, lr=0.01, momentum=0.9):
        self.lr = lr
        self.momentum = momentum
        self.v = None

    def update(self, params, grads):
        if self.v is None:
            self.v = {}
            for key, val in params.items():
                self.v[key] = np.zeros_like(val)

        for key in params.keys():
            self.v[key] = self.momentum * self.v[key] - self.lr * grads[key]
            params[key] += self.momentum * self.v[key] - self.lr * grads[key]

但是根據我看到的各個框架的代碼,它們好像都把動量延遲更新了一步,所以實現起來有點不一樣(或者說是上下兩個式子的順序進行了顛倒),我也找不到好的解釋,但是在MNIST數據集上最終的結果要好於原來的實現。

Python實現

class Nestrov:
    def __init__(self, lr=0.01, momentum=0.9):
        self.lr = lr
        self.momentum = momentum
        self.v = None

    def update(self, params, grads):
        if self.v is None:
            self.v = {}
            for key, val in params.items():
                self.v[key] = np.zeros_like(val)

        for key in params.keys():
            self.v[key] *= self.momentum
            self.v[key] -= self.lr * grads[key]
            params[key] += self.momentum * self.momentum * self.v[key]
            params[key] -= (1 + self.momentum) * self.lr * grads[key]

6

Adagrad

前面介紹了幾種動量法, 動量法 旨在通過每個參數在之前的迭代中的梯度,來改變當前位置參數的梯度, 在梯度穩定的地方能夠加速更新的速度,在梯度不穩定的地方能夠穩定梯度

而AdaGrad則是一種完全不同的思路,它是一種 自適應 優化算法。它通過每個參數的歷史梯度, 動態更新每一個參數的學習率 ,使得每個參數的更新率都能夠逐漸減小。前期梯度加大的,學習率減小得更快,梯度小的,學習率減小得更慢些。

Adagrad缺點

  • 仍需要手工設置一個全局學習率, 如果 設置過大的話,會使regularizer過於敏感,對梯度的調節太大

  • 中後期,分母上梯度累加的平方和會越來越大,使得參數更新量趨近於0,使得訓練提前結束,無法學習

更新方式

其中 δ 用於防止除零錯

Python實現

class AdaGrad:
    def __init__(self, lr=0.01):
        self.lr = lr
        self.h = None

    def update(self, params, grads):
        if self.h is None:
            self.h = {}
            for key, val in params.items():
                self.h[key] = np.zeros_like(val)

        for key in params.keys():
            self.h[key] += grads[key] * grads[key]
            params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)

7

RMSprop

AdaGrad有個問題,那就是學習率會不斷地衰退。這樣就會使得很多任務在達到最優解之前學習率就已經過量減小,所以 RMSprop 採用了使用 指數衰減平均 來慢慢丟棄先前的梯度歷史。這樣一來就能夠 防止學習率過早地減小

RMSprop特點

  • 其實RMSprop依然依賴於全局學習率

  • RMSprop算是Adagrad的一種發展,和Adadelta的變體,效果趨於二者之間

  • 適合處理非平穩目標——對於RNN效果很好

更新方式:

Python實現

class RMSprop:
    def __init__(self, lr=0.01, decay_rate=0.99):
        self.lr = lr
        self.decay_rate = decay_rate
        self.h = None

    def update(self, params, grads):
        if self.h is None:
            self.h = {}
            for key, val in params.items():
                self.h[key] = np.zeros_like(val)

        for key in params.keys():
            self.h[key] *= self.decay_rate
            self.h[key] += (1 - self.decay_rate) * grads[key] * grads[key]
            params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)

8

Adam

Adam 方法結合了上述的 動量(Momentum)自適應(Adaptive) ,同時對梯度和學習率進行動態調整。如果說動量相當於給優化過程增加了慣性,那麼自適應過程就像是給優化過程加入了阻力。速度越快,阻力也會越大。

Adam首先計算了梯度的一階矩估計和二階矩估計,分別代表了原來的動量和自適應部分

β_1 與 β_2 是兩個特有的超參數,一般設爲0.9和0.999。

但是,Adam還需要對計算出的矩估計進行修正

其中t是迭代的次數,修正的原因在

Why is it important to include a bias correction term for the Adam optimizer for Deep Learning? stats.stackexchange.com

這個問題中有非常詳細的解釋。 簡單來說就是由於m和v的初始值爲0,所以第一輪的時候會非常偏向第二項,那麼在後面計算更新值的時候根據β_1 與 β_2的初始值來看就會非常的大,需要將其修正回來。而且由於β_1 與 β_2很接近於1,所以如果不修正,對於最初的幾輪迭代會有很嚴重的影響。

最後就是更新參數值,和AdaGrad幾乎一樣,只不過是用上了上面計算過的修正的矩估計

Adam特點

  • Adam梯度經過偏置校正後,每一次迭代學習率都有一個固定範圍,使得參數比較平穩。

  • 結合了Adagrad善於處理稀疏梯度和RMSprop善於處理非平穩目標的優點

  • 爲不同的參數計算不同的自適應學習率

  • 也適用於大多非凸優化問題——適用於大數據集和高維空間。

爲了使得Python實現更加簡潔,將修正矩估計代入原式子,也就是重新表達成只關於m和v的函數,修改如下

python實現

class Adam:
    def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
        self.lr = lr
        self.beta1 = beta1
        self.beta2 = beta2
        self.iter = 0
        self.m = None
        self.v = None

    def update(self, params, grads):
        if self.m is None:
            self.m, self.v = {}, {}
            for key, val in params.items():
                self.m[key] = np.zeros_like(val)
                self.v[key] = np.zeros_like(val)

        self.iter += 1
        lr_t = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)

        for key in params.keys():
            self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])
            self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key])

            params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)

9

算法的表現

各個算法在等高線的表現,它們都從相同的點出發,走不同的路線達到最小值點。可以看到,Adagrad,Adadelta和RMSprop在正確的方向上很快地轉移方向,並且快速地收斂,然而Momentum和NAG先被領到一個偏遠的地方,然後才確定正確的方向,NAG比momentum率先更正方向。SGD則是緩緩地朝着最小值點前進。

參考文獻

  1. Juliuszh:一個框架看懂優化算法之異同 SGD/AdaGrad/Adam

  2. 深度學習中的優化算法(Optimizer)理解與python實現

  3. 優化算法Optimizer比較和總結

相關文章