1. 程式人生 > 其它 >優化演算法BGD、SGD、Momentum-SGD、Adagrad、RMSProp、Adam演算法python實現

優化演算法BGD、SGD、Momentum-SGD、Adagrad、RMSProp、Adam演算法python實現

技術標籤:機器學習python機器學習

概述

一般用一個通用框架來表述優化演算法
有如下定義:

  1. 待優化的引數 θ
  2. 目標函式 J ( θ )
  3. 學習率 α

有如下過程(每次迭代):

  1. 計算目標關於此時引數的梯度 ∇ θ ( J ( θ ) )
  2. 計算曆史梯度的一階動量和二階動量
  3. 計算下降梯度 g
  4. 根據梯度進行迭代 θ = θ − g

優化演算法目前有固 定 學 習 率 和自 適 應 學 習 率 兩種,差別也就體現在過程的第1和第2步
固定學習率優化演算法有:BGD、SGD、SGDM、NAG
自適應學習率優化演算法有:AdaGrad、AdaDelta、Adam、Nadam


經驗總結

現在用的最多的演算法是SGD和Adam,兩者各有好壞。SGD能夠到達全域性最優解,而且訓練的最佳精度也要高於其他優化演算法,但它對學習率的調節要求非常嚴格,而且容易停在鞍點;Adam很容易的跳過鞍點,而且不需要人為的干預學習率的調節,但是它很容易在區域性最小值處震盪,存在在特殊的資料集下出現學習率突然上升,造成不收斂的情況。

演算法優點缺點適用情況
BGD目標函式為凸函式時,可以找到全域性最優值收斂速度慢,需要用到全部資料,記憶體消耗大不適用於大資料集,不能線上更新模型
SGD避免冗餘資料的干擾,收斂速度加快,能夠線上學習更新值的方差較大,收斂過程會產生波動,可能落入極小值(卡在鞍點),選擇合適的學習率比較困難(需要不斷減小學習率)適用於需要線上更新的模型,適用於大規模訓練樣本情況
Momentum能夠在相關方向加速SGD,抑制振盪,從而加快收斂需要人工設定學習率適用於有可靠的初始化引數
Adagrad實現學習率的自動更改仍依賴於人工設定一個全域性學習率,學習率設定過大,對梯度的調節太大。中後期,梯度接近於0,使得訓練提前結束需要快速收斂,訓練複雜網路時;適合處理稀疏梯度1
Adadelta不需要預設一個預設學習率,訓練初中期,加速效果不錯,很快,可以避免參數更新時兩邊單位不統一的問題在區域性最小值附近震盪,可能不收斂需要快速收斂,訓練複雜網路時
Adam速度快,對記憶體需求較小,為不同的引數計算不同的自適應學習率在區域性最小值附近震盪,可能不收斂需要快速收斂,訓練複雜網路時;善於處理稀疏梯度和處理非平穩目標的優點,也適用於大多非凸優化 - 適用於大資料集和高維空間
  • 對於稀疏資料,儘量使用學習率可自適應的優化方法,不用手動調節,而且最好採用預設值
  • SGD通常訓練時間更長,但是在好的初始化和學習率排程方案的情況下,結果更可靠
  • 如果在意更快的收斂,並且需要訓練較深較複雜的網路時,推薦使用學習率自適應的優化方法。
  • Adadelta,RMSprop,Adam是比較相近的演算法,在相似的情況下表現差不多。
  • 在想使用帶動量的RMSprop,或者Adam的地方,大多可以使用Nadam取得更好的效果

批量梯度下降BGD

每一次迭代都用到訓練集的所有資料,用所有資料來計算梯度。
優劣點:迭代速度慢,全域性最優解

迭代公式
θ j = θ j − α m ∑ i = 0 m ( h θ ( x i ) − y i ) x i θ_j = θ_j - \frac{α}{m}\sum_{i=0}^m(h_θ(x^i) - y^i)x^i θj=θjmαi=0m(hθ(xi)yi)xi
以y=x1+2*x2為例

# 批量梯度下降BGD
# 擬合函式為:y = theta * x
# 代價函式為:J = 1 / (2 * m) * ((theta * x) - y) * ((theta * x) - y).T;
# 梯度迭代為: theta = theta - alpha / m * (x * (theta * x - y).T);
import numpy as np


# 1、單元資料程式
# 以 y=x為例,所以正確的結果應該趨近於theta = 1
def bgd_single():
    # 訓練集, 單樣本
    x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

    y = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

    # 初始化
    m = len(y)
    theta = 0  # 引數
    alpha = 0.01  # 學習率
    threshold = 0.0001  # 停止迭代的錯誤閾值
    iterations = 1500  # 迭代次數
    error = 0  # 初始錯誤為0

    # 迭代開始
    for i in range(iterations):
        error = 1 / (2 * m) * np.dot(((theta * x) - y).T, ((theta * x) - y))
        # 迭代停止
        if abs(error) <= threshold:
            break

        theta -= alpha / m * (np.dot(x.T, (theta * x - y)))

    print('單變數:', '迭代次數: %d' % (i + 1), 'theta: %f' % theta,
          'error1: %f' % error)


# 2、多元資料程式
# 以 y=x1+2*x2為例,所以正確的結果應該趨近於theta = [1,2]


def bgd_multi():
    # 訓練集,每個樣本有2個分量
    x = np.array([(1, 1), (1, 2), (2, 2), (3, 1), (1, 3), (2, 4), (2, 3), (3,
                                                                           3)])
    y = np.array([3, 5, 6, 5, 7, 10, 8, 9])

    # 初始化
    m, dim = x.shape
    theta = np.zeros(dim)  # 引數
    alpha = 0.01  # 學習率
    threshold = 0.0001  # 停止迭代的錯誤閾值
    iterations = 1500  # 迭代次數
    error = 0  # 初始錯誤為0

    # 迭代開始
    for i in range(iterations):
        error = 1 / (2 * m) * np.dot((np.dot(x, theta) - y).T,
                                     (np.dot(x, theta) - y))
        # 迭代停止
        if abs(error) <= threshold:
            break

        theta -= alpha / m * (np.dot(x.T, (np.dot(x, theta) - y)))

    print('多元變數:', '迭代次數:%d' % (i + 1), 'theta:', theta, 'error:%f' % error)


if __name__ == '__main__':
    bgd_single()
    bgd_multi()

輸出

單變數: 迭代次數: 14 theta: 0.998200 error1: 0.000062
多元變數: 迭代次數:454 theta: [1.01339617 1.98981777] error:0.000100

隨機梯度下降SGD

因為BGD的迭代速度在大資料量的情況下會變得非常慢,所以提出了隨機梯度下降演算法,即每一次迭代只使用一個樣本,根據這一個樣本來計算梯度。
優劣點:迭代速度快,不是全域性最優解

迭代公式
θ j = θ j − α ( h θ ( x i ) − y i ) x i θ_j = θ_j - α(h_θ(x^i) - y^i)x^i θj=θjα(hθ(xi)yi)xi
以y=x1+2*x2為例

# 隨機梯度下降SGD
# 以 y=x1+2*x2為例

import numpy as np


# 多元資料
def sgd():
    # 訓練集,每個樣本有2個分量
    x = np.array([(1, 1), (1, 2), (2, 2), (3, 1), (1, 3), (2, 4), (2, 3), (3, 3)])
    y = np.array([3, 5, 6, 5, 7, 10, 8, 9])

    # 初始化
    m, dim = x.shape
    theta = np.zeros(dim)  # 引數
    alpha = 0.01  # 學習率
    threshold = 0.0001  # 停止迭代的錯誤閾值
    iterations = 1500  # 迭代次數
    error = 0  # 初始錯誤為0

    # 迭代開始
    for i in range(iterations):

        error = 1 / (2 * m) * np.dot((np.dot(x, theta) - y).T, (np.dot(x, theta) - y))
        # 迭代停止
        if abs(error) <= threshold:
            break
        
        j = np.random.randint(0, m)

        theta -= alpha * (x[j] * (np.dot(x[j], theta) - y[j]))

    print('迭代次數:%d' % (i + 1), 'theta:', theta, 'error:%f' % error)


if __name__ == '__main__':
    sgd()

輸出:

迭代次數:420 theta: [1.01288885 1.99055896] error:0.000090

帶動量的隨機梯度下降Momentum-SGD

因為SGD只依賴於當前迭代的梯度,十分不穩定,加一個“動量”的話,相當於有了一個慣性在裡面,梯度方向不僅與這次的迭代有關,還與之前一次的迭代結果有關。“當前一次效果好的話,就加快步伐;當前一次效果不好的話,就減慢步伐”;而且在區域性最優值處,沒有梯度但因為還存在一個動量,可以跳出區域性最優值。

迭代公式
g = m o m e n t u m ∗ g − α ( h θ ( x i ) − y i ) x i g =momentum∗g - α(h_θ(x^i) - y^i)x^i g=momentumgα(hθ(xi)yi)xi
θ j = θ j − g θ_j = θ_j - g θj=θjg
以y=x1+2*x2為例

# 隨機梯度下降SGD
# 以 y=x1+2*x2為例

import numpy as np


# 多元資料
def sgd():
    # 訓練集,每個樣本有2個分量
    x = np.array([(1, 1), (1, 2), (2, 2), (3, 1), (1, 3), (2, 4), (2, 3), (3, 3)])
    y = np.array([3, 5, 6, 5, 7, 10, 8, 9])

    # 初始化
    m, dim = x.shape
    theta = np.zeros(dim)  # 引數
    alpha = 0.01  # 學習率
    threshold = 0.0001  # 停止迭代的錯誤閾值
    iterations = 1500  # 迭代次數
    error = 0  # 初始錯誤為0

    # 迭代開始
    for i in range(iterations):

        error = 1 / (2 * m) * np.dot((np.dot(x, theta) - y).T, (np.dot(x, theta) - y))
        # 迭代停止
        if abs(error) <= threshold:
            break
        
        j = np.random.randint(0, m)

        theta -= alpha * (x[j] * (np.dot(x[j], theta) - y[j]))

    print('迭代次數:%d' % (i + 1), 'theta:', theta, 'error:%f' % error)


if __name__ == '__main__':
    sgd()

輸出

迭代次數:497 theta: [1.0135894  1.99015619] error:0.000100

Adagrad

對學習率加了一個約束,但依賴於一個全域性學習率。
根據迭代公式,學習率 α 乘上了一個係數,這個係數與梯度有關,且當梯度大的時候,這個係數小;當梯度小的時候,這個係數大。這樣的實際表現就是在面對頻繁出現的特徵時,使用小學習率;在面對不頻繁出現的特徵時,使用大學習率。這就使得這一演算法非常適合處理稀疏資料。比如在訓練單詞嵌入時,常用單詞和不常用單詞分別用小學習率和大學習率能達到更好的效果。
問題在於公式中的 v t v_t vt會在後期變得非常大,使得矯正後的學習率趨近0,過早得結束迭代。

迭代公式
v t = v t − 1 + g t 2 v_t = v_{t-1}+g_t^2 vt=vt1+gt2
θ = θ − α ∗ g t v t + e θ = θ - α*\frac{g_t}{\sqrt{v_t+e}} θ=θαvt+e gt

RMSProp

Adagrad會累加之前所有的梯度平方,而RMSProp只累加固定大小的項,並且也不直接儲存這些項,僅僅是近似計算對應的平均值。不依賴全域性學習率。

迭代公式
v t = β v t − 1 + ( 1 − β ) g t 2 v_t = βv_{t-1}+(1-β)g_t^2 vt=βvt1+(1β)gt2
θ = θ − α ∗ g t v t + e θ = θ - α*\frac{g_t}{\sqrt{v_t+e}} θ=θαvt+e gt


Adam

Adam是一種自適應學習率的方法,在Momentum一階矩估計的基礎上加入了二階矩估計,也是在Adadelta的基礎上加了一階矩。它利用梯度的一階矩估計和二階矩估計動態調整每個引數的學習率。為了解決Adagrad演算法出現學習率趨向0的問題,還加入了偏置校正,這樣每一次迭代學習率都有個確定範圍,使得引數比較平穩。
優劣點:迭代速度快,效果也好,但可能不收斂。

根據下面的paper來快速描述一下Adam的algorithm:

  • 先初始化 m 0 = 0 m_0=0 m0=0 m 0 m_0 m0就是Momentum中,前一個時間點的movement

    再初始化 v 0 = 0 v_0=0 v0=0 v 0 v_0 v0就是RMSProp裡計算gradient的root mean square的 σ \sigma σ

    最後初始化 t = 0 t=0 t=0,t用來表示時間點

  • 先算出gradient g t g_t gt
    g t = ∇ θ f t ( θ t − 1 ) g_t=\nabla _{\theta}f_t(\theta_{t-1}) gt=θft(θt1)

  • 再根據過去要走的方向 m t − 1 m_{t-1} mt1和gradient g t g_t gt,算出現在要走的方向 m t m_t mt——Momentum
    m t = β 1 m t − 1 + ( 1 − β 1 ) g t m_t=\beta_1 m_{t-1}+(1-\beta_1) g_t mt=β1mt1+(1β1)gt

  • 然後根據前一個時間點的 v t − 1 v_{t-1} vt1和gradient g t g_t gt的平方,算一下放在分母的 v t v_t vt——RMSProp
    v t = β 2 v t − 1 + ( 1 − β 2 ) g t 2 v_t=\beta_2 v_{t-1}+(1-\beta_2) g_t^2 vt=β2vt1+(1β2)gt2

  • 接下來做了一個原來RMSProp和Momentum裡沒有的東西,就是bias correction,它使 m t m_t mt v t v_t vt都除上一個值,這個值本來比較小,後來會越來越接近於1 (原理詳見paper)
    m ^ t = m t 1 − β 1 t v ^ t = v t 1 − β 2 t \hat{m}_t=\frac{m_t}{1-\beta_1^t} \\ \hat{v}_t=\frac{v_t}{1-\beta_2^t} m^t=1β1tmtv^t=1β2tvt

  • 最後做update,把Momentum建議你的方向 m t ^ \hat{m_t} mt^乘上learning rate α \alpha α,再除掉RMSProp normalize後建議的learning rate分母,然後得到update的方向
    θ t = θ t − 1 − α ⋅ m ^ t v ^ t + ϵ \theta_t=\theta_{t-1}-\frac{\alpha \cdot \hat{m}_t}{\sqrt{\hat{v}_t}+\epsilon} θt=θt1v^t +ϵαm^t

以y=x1+2*x2為例

# ADAM
# 以 y=x1+2*x2為例
import math
import numpy as np


def adam():
    # 訓練集,每個樣本有三個分量
    x = np.array([(1, 1), (1, 2), (2, 2), (3, 1), (1, 3), (2, 4), (2, 3), (3,
                                                                           3)])
    y = np.array([3, 5, 6, 5, 7, 10, 8, 9])

    # 初始化
    m, dim = x.shape
    theta = np.zeros(dim)  # 引數
    alpha = 0.01  # 學習率
    momentum = 0.1  # 衝量
    threshold = 0.0001  # 停止迭代的錯誤閾值
    iterations = 3000  # 迭代次數
    error = 0  # 初始錯誤為0

    b1 = 0.9  # 演算法作者建議的預設值
    b2 = 0.999  # 演算法作者建議的預設值
    e = 0.00000001  #演算法作者建議的預設值
    mt = np.zeros(dim)
    vt = np.zeros(dim)

    for i in range(iterations):
        j = i % m
        error = 1 / (2 * m) * np.dot((np.dot(x, theta) - y).T,
                                     (np.dot(x, theta) - y))
        if abs(error) <= threshold:
            break

        gradient = x[j] * (np.dot(x[j], theta) - y[j])
        mt = b1 * mt + (1 - b1) * gradient
        vt = b2 * vt + (1 - b2) * (gradient**2)
        mtt = mt / (1 - (b1**(i + 1)))
        vtt = vt / (1 - (b2**(i + 1)))
        vtt_sqrt = np.array([math.sqrt(vtt[0]),
                             math.sqrt(vtt[1])])  # 因為只能對標量進行開方
        theta = theta - alpha * mtt / (vtt_sqrt + e)

    print('迭代次數:%d' % (i + 1), 'theta:', theta, 'error:%f' % error)


if __name__ == '__main__':
    adam()

輸出:

迭代次數:1987 theta: [1.01326533 1.98965863] error:0.000100