【機器學習基礎】——梯度下降

Uniqe發表於2021-10-12

梯度下降是機器學習中一種重要的優化演算法,不單單涉及到經典機器學習演算法,在神經網路、深度學習以及涉及到模型引數訓練的很多場景都要用到梯度下降演算法,因此在此單獨作為1節對這部分進行總結,主要從梯度下降的原理,優化的梯度下降方法包括SGD、MBGD、Adagrad、Momentm、RMSprop、Adam等演算法,並依據視覺化比較各演算法的效能,資料主要來源於視訊、論文和部落格,參考資料會在末尾貼出。


梯度下降原理

常規梯度下降

  前面提到梯度下降演算法就是損失函式Loss梯度方向的反方向前進,即Loss等高線的發現方向前進,不斷尋找使得Loss減小的引數,直至損失收斂,不再減小。我們用Θ表示引數,用▽L表示Loss的梯度,那麼引數的更新就是:

  假設Θ是2維的,有兩個引數Θ1和Θ2,那麼在二維空間中,引數的更新過程如圖所示:

  上圖中紅色的線是梯度的方向,引數朝著梯度的反方向移動,上述就是引數更新的過程,每次更新我們計算所有樣本的損失對引數的導數的和(因為損失是一個加和公式),然後更新引數,這裡就會有個問題,當我們樣本量巨大時,我們每更新迭代一次,都要去對所有樣本都計算以便,這樣就會造成速度非常慢,因此在此基礎上產生了隨機梯度下降演算法(SGD)和小批量(批量)梯度下降演算法(MBGD)。

所謂隨機梯度下降演算法,就是在訓練過程中僅選取一個樣本即損失函式相當於從L變成了L':

  然後計算梯度,計算梯度,更新引數,隨機梯度下降雖然提升了執行速度,但是也存在一定的缺點:

  (1)每次僅選取一個樣本,會造成損失的嚴重震盪,即有時大有時小,其收斂性和原始梯度下降是一樣的,但有時也可能會跳到更優的區域性最小值。

  而小批量梯度下降類似於隨機梯度下降,不過是每次從僅選取一個樣本變成將樣本分成多個batch,每次採用一小批樣本對引數進行更新。小批量梯度下降與原始梯度下降一樣也會受到學習率的影響,後面即將說到,當到達鞍點時,會在鞍點附近來回震盪。

  從原始梯度下降的圖中可以看出,每次移動的長度由梯度和學習率共同決定,這裡就有個重要的引數——學習率η,那麼這裡就有個問題,到底每次移動多長合適呢,下面有張圖來說明:

  圖中黑色的曲線表示Loss的曲線,有①、②、③、④四個不同大小的學習率,學習率其中②是學習率最小,①次之,③學習率稍大,④學習率最大,那麼從圖中我們可以看到,②的步長很小,每次前進一小步,而③則是一次性跨過鴻溝,錯過了Local Minma,而④則步長過大,甚至朝著損失增大的方向移動了,只有②是最理想的前進步伐,因此設定合適的學習率不但可以使得優化次數減小,同時可以獲得更好的優化效果。

  那麼如何調整學習率呢?從上面的圖來看,紅色的線是理想的梯度下降的前進方向和步伐,我們期望在梯度比較大的地方,即比較陡峭的地方,下降的快一點,步伐快一點,而在比較平緩的地方,希望步伐小一點,那麼我們如何做到在訓練過程中自動調整學習率的大小呢?於是就衍生出了自動調整學習率的梯度下降演算法Adagrad、Momentum、RMSprop、Adam等演算法,其中除了Momentum、RMSprop、Adam主要在深度學習中用的比較多,由於這些都屬於梯度下降,且都是動態調整學習率,再次一併說了。

Adagrad

  Adagrad中的Ada全稱Adaptive Learning Rates,是一種動態調整學習率的過程,其主旨思想是:在剛開始訓練階段,我們離目標還比較遠,可以步伐大一點,當訓練幾次後,我們更接近目標值,此時我們要減小學習的步伐。每次乘上一個1/√t+1,那麼在第t次訓練時學習率就變成了:

  這時隨著訓練次數變大,學習率逐漸減小,而Adagrad更進一步地在每次訓練時會再除以過去所有的梯度的均方誤差(RMS),用σt表示,設當前一輪迭代的梯度為gt,那麼引數的更新即為:

   結合上面每次學習率每次衰減1/√t+1,進一步化簡上式,那麼最終的引數更新公式為:

  這樣會在梯度平緩的地方,學習的更加緩慢,更加平穩,從而取得更大的進步。

RMSprop

  但是Adagrad也會有缺點,當學習到一定程度時,分母項變得越來越大,最終導致學習率為0,引數不再更新。為了改善這個問題,就出現了RMSprop演算法,RMSprop不再是將過去的梯度直接求平方和,而是通過給予過去梯度都賦予一定的權重,然後再求平方和,相當於是逐漸遺忘過去的梯度,將新的梯度資訊更加的突出出來,也就是“移動平均”的做法。用數學的描述RMSprop的引數更新過程如下:

 Momentum

  另一種類似於RMSprop算的優化演算法就是Momentum,其在梯度下降過程中不僅考慮了梯度的方向,同時運用了物理學中動量的原理,在下降過程中考慮了動量的因素,想象一輛雲霄飛車,在比較陡峭的地方下降的快,動量大,在平緩的地方就速度小,動量小,如下圖所示:

  那麼將其運用在梯度下降的過程中就是這樣的:

  上面的圖中紅色是梯度的方向,藍色的線是動量方向,從θ0開始,梯度方向與動量進行合成,合成以後形成綠色的虛線,到達新的點後,重複以上步驟(這個圖有時間將其製作成動圖可能看的更清楚)。那麼在每次前進中,動量用v表示,那麼動量的變化過程如下:

  那麼引數 Θ的更新過程就是考慮了動量,其更新過成為:

  仔細觀察動量變化的公式,將t之前的動量帶到t的動量公式中,即:

  可以看到動量其實就是之前的梯度乘以一個權重λ在加和的結果,至此可以看出Momentum演算法其實跟RMSprop其實很像,不過RMSprop是加權的平方和。

  上面的演算法都是需要給定初始化學習率η,那麼還有一種是不需要給出初始學習率類似於Adagrad的演算法Adadelta。

Adadelta

  通過牛頓法知道,利用牛頓迭代公式求最小值,牛頓法的迭代步長為f(x)'',那麼:

  可以看出引數的迭代的步長至於f(x)的二階導數有關,不需要指定學習率。而高階的牛頓法迭代步長為Hessian矩陣,因此Adadelta就是採用了這種思想,採用Hessian矩陣的對角線近似Hessian矩陣,於是:

   那麼:

  假設x附近的曲率是平滑的,xt+1近似為xt,那麼:

   這一部分暫時理解不是很透徹,後面會搜尋一些資料,暫時先放這裡吧。

Adam

  Adam演算法是剛好整合上面兩種RMSprop和Monentum的方法,因此其全程為自適應時刻估計方法(Adaptive Moment Estimation),其實就是帶有動量的RMSprop演算法。既然是結合RMSprop和Momentum,我們首先給出引數更新公式:

  其中v是關於動量的變數,m是過去梯度的加權的平方和,與RMSprop類似,二者的更新過程如下:

  由於訓練初始化v和m為0,則會導致vt和mt也偏向於0,尤其是在訓練初期,為了防止偏差對訓練初期的影響,需要對vt和mt進行修正:

  修正完成後,即可帶入上面引數更新的公式即可。

  然後就是還有一些其他的優化演算法,如NAG、Adamax、Nadam等比較新的方法,這裡就暫時不作介紹了。

  接下來就是對上面一些演算法的實現,每一個演算法會用一個小例項看一下演算法的迭代過程,演算法程式碼僅作為學習演算法加深印象,不考慮演算法的效能和速度。

演算法的實現

常規梯度下降

  這個比較簡單,就簡單對其做一個實現,然後順便驗證一下前面的線性迴歸演算法,首先就是寫一個梯度下降的函式:

import numpy as np
import matplotlib.pyplot as plt

def gd(x, y, w, b):
    m, n = np.shape(x)
    hypo = 2 * (y.reshape(m) - (np.dot(w, x.T) + b))
    grad_w = -np.dot(hypo, x)/m
    grad_b = -np.sum(hypo)/m
    loss = np.sum((hypo/2) ** 2)/m
    return grad_w, grad_b, loss

  這樣一個簡單的梯度下降就完成了,先建立一些簡單的資料,對梯度下降演算法進行驗證:

# f(x1, x2) = 3x + 1
x = [[0], [1], [3], [2.2], [9], [6], [4], [4.5], [5.5], [7.7]]
def f(x):
    z = []
    for x_ in x:
        z.append(3 * x_[0] + 1)
    return z

y = f(x)
data_x = np.array(x)
labels = np.array(y)
m, n = np.shape(data_x)

  然後就可以利用梯度下降對演算法進行訓練了:

w = np.array([0] * n)
b = 0

eta = 0.0001
w_list = []
b_list = []
loss_list = []
for i in range(0, 10000):
    grad_w, grad_b, loss = gd(data_x, labels, w, b)
    w = w - eta * grad_w
    w_list.append(w)
    b = b - eta * grad_b
    b_list.append(b)
    loss_list.append(loss)

  然後畫圖看下引數的更新過程:

W = np.arange(0, 5, 0.1)
B = np.arange(0, 2, 0.01)
Z = np.zeros((len(W), len(B)))
for i in range(len(W)):
    for j in range(len(B)):
        w = W[i]
        b = B[j]
        Z[i][j] = 0
        for k in range(len(data_x)):
            Z[i][j] += (y[k] - w * data_x[k][0] - b) ** 2
        Z[i][j] /= len(data_x)


plt.figure()
plt.contourf(B, W, Z, 50, alpha=0.5, cmap=plt.get_cmap('jet'))
plt.plot([1], [3.], 'x', ms=6, marker=10, color='r')

plt.plot(b_list, w_list, 'o-', ms=3, lw=1.5, color='black')

  背景顏色深淺表示loss的大小,紅色的點是標準方程的引數(3,1),黑色線就是引數逐漸向目標值靠近的過程,最終得到一組引數(w=3.0003,b=0.998)。

  然後用sklearn自帶的資料集跑一下上面的過程,首先讀入資料,在重複上面的過程:

from sklearn.datasets import load_boston

data_x = load_boston()['data']
labels = load_boston()['target']

m, n = np.shape(data_x)

w = np.array([0] * n)
b = 0
eta = 0.0001
w_list = []
b_list = []
loss_list = []
for i in range(0, 10000):
    grad_w, grad_b, loss = gd(data_x, labels, w, b)
    w = w - eta * grad_w
    w_list.append(w)
    b = b - eta * grad_b
    b_list.append(b)
    loss_list.append(loss)

  最終loss收斂在57左右,前面提到學習率大小的問題,剛開始設定0.001的學習率時,loss範圍越來越大,最終發散了,大概就是上面那個圖中④中的線的樣子,查詢原因發現在寫gd函式時沒有除以樣本數,這也就間接導致了學習率變大,因此後面減小學習率後結果才會收斂。

 SGD

  每次迭代僅選取一個樣本對引數進行更新,新增一個隨機選取的索引就可以了:

def sgd(x, y, w, b):
    idx = random.randint(0, len(x)-1)
    select_x = x[idx]
    select_y = y[idx]
    hypo = 2 * (select_y - (np.dot(w, select_x) + b))
    grad_w = -np.dot(hypo, select_x)
    grad_b = -hypo
    loss = (np.sum((y.reshape(m) - (np.dot(w, x.T) + b)) ** 2))/len(x)
    return grad_w, grad_b, loss

  替換掉上面迭代的函式“gd”為“sgd”,看一下迭代過程:

  對比一下上面那個過程,可以看出前面的迭代過程對比全量資料梯度下降還是有點曲折的,只不過這裡資料量比較少,不是那麼明顯。最終學習到的引數基本與全量梯度下降一致。

Adagrad

  Adagrad要疊加過去所有梯度的和的平方再開根號,程式碼如下:

def adagrad(x, y, w, b, grad_w_list, grad_b_list):

    grad_w, grad_b, loss = gd(x, y, w, b)

    if len(grad_w_list) == 0:
        sum_grad_w = 0
        sum_grad_b = 0
    else:
        sum_grad_w = 0
        sum_grad_b = 0
        for i in range(len(grad_w_list)):
            sum_grad_w += grad_w_list[i] ** 2
            sum_grad_b += grad_b_list[i] ** 2

    sum_grad_w = sum_grad_w + grad_w**2
    sum_grad_b = sum_grad_b + grad_b**2
    delta_w = grad_w/np.sqrt(sum_grad_w)
    delta_b = grad_b/np.sqrt(sum_grad_b)
    return delta_w, delta_b, grad_w, grad_b, loss

  然後就是訓練:

w = np.array([0] * n)
b = 0
eta = 1
w_list = []
grad_w_list = []
b_list = []
grad_b_list = []
loss_list = []
for i in range(0, 10000):
    delta_w, delta_b, grad_w, grad_b, loss = adagrad(data_x, labels, w, b, grad_w_list, grad_b_list)
    w = w - eta * delta_w
    w_list.append(w)
    grad_w_list.append(grad_w)
    b = b - eta * delta_b
    b_list.append(b)
    grad_b_list.append(grad_b)
    loss_list.append(loss)
    print(i, loss)

  剛開始設定的eta值很小,收斂極慢,當設定足夠大時很快就收斂了,這也正是adagrad的特性:可以剛開始設定較大的學習率,隨後隨著迭代次數的增加,學習率自動調整。從圖中可以看到一開始步長較大,直接飛出去了,隨後又被拉了回來,從輸出的loss可以看出,adagrad的效果還是不錯的,最終找到的w、b值與真實值一致,學習效果比上面兩個都要好。

Momentum

  Momentum方法在下降過程中帶有動量,可以幫助在優化過程中突破鞍點,同時可以使得梯度方向不變的維度上速度變快,梯度方向有所改變的維度上的更新速度變慢,這樣就可以加快收斂,下面是Momentum的實現:

def momentum(lamda, eta, w_v_list, b_v_list, grad_w_list, grad_b_list):
    if len(w_v_list) == 0:
        w_v = 0
        b_v = 0
    else:
        w_v = lamda * w_v_list[-1] + eta * grad_w_list[-1]
        b_v = lamda * b_v_list[-1] + eta * grad_b_list[-1]
    return w_v, b_v

# 訓練
w = np.array([0] * n)
b = 0
eta = 0.01
lamda = 0.01
w_list = []
grad_w_list = []
b_list = []
grad_b_list = []
loss_list = []
w_v_list = []
b_v_list = []
for i in range(0, 10000):
    grad_w, grad_b, loss = gd(data_x, labels, w, b)
    w_v, b_v = momentum(lamda, eta, w_v_list, b_v_list, grad_w_list, grad_b_list)
    grad_w_list.append(grad_w)
    grad_b_list.append(grad_b)
    w = w - w_v
    b = b - b_v
    loss_list.append(loss)
    w_v_list.append(w_v)
    b_v_list.append(b_v)
    w_list.append(w)
    b_list.append(b)
    print(i, loss)

  再看一下Momentum的優化過程:

  該樣本資料其實是不存在鞍點的,可以看到,開始時由於動量較大,稍微向外衝出去了,但隨後由於梯度的存在,再次將方向拉了回來,最終收斂,收斂後的引數w、b與真實值一致,並且收斂速度很快。

RMSprop

  RMSprop類似於Adagrad,不過在前面的梯度平方和進行了加權,下面是RMSprop的實現:

def rmsprop(x, y, w, b, sigma_w_list, sigma_b_list, alpha):
    grad_w, grad_b, loss = gd(x, y, w, b)
    if len(sigma_w_list) == 0:
        sigma_w = grad_w
        sigma_b = grad_b
    else:
        sigma_w = np.sqrt(alpha * sigma_w_list[-1] ** 2 + (1 - alpha) * grad_w ** 2)
        sigma_b = np.sqrt(alpha * sigma_b_list[-1] ** 2 + (1 - alpha) * grad_b ** 2)
    return sigma_w, sigma_b, grad_w, grad_b, loss

# 訓練
w = np.array([0] * n)
b = 0
eta = 0.001
alpha = 0.9
w_list = []
sigma_w_list = []
b_list = []
sigma_b_list = []
loss_list = []
for i in range(0, 10000):
    sigma_w, sigma_b, grad_w, grad_b, loss = rmsprop(data_x, labels, w, b, sigma_w_list, sigma_b_list, alpha)
    w = w - eta * grad_w/sigma_w
    w_list.append(w)
    sigma_w_list.append(sigma_w)
    b = b - eta * grad_b/sigma_b
    b_list.append(b)
    sigma_b_list.append(sigma_b)
    loss_list.append(loss)
    print(i, loss)

  得到的結果如圖所示:

  這個訓練過程相較於Adagrad看著就比較“光滑”,訓練到最後收斂結果滿足要求。

  後面的梯度下降演算法就不再進一步實現和敘述了,大多都是公式的編輯和實現,比較簡單,其實在實現過程中w、b是可以作為一個引數進行學習和訓練的,在此只是為了容易看的明白,因此分開來了,最後附上其他部落格中的幾種演算法動態效果圖,來更直觀顯示每一種演算法的特點,一個是沒有鞍點的和一個是有鞍點存在的情況:

梯度下降演算法的選擇

  那麼眾多梯度下降演算法在使用時應該選擇哪一種呢,這要根據樣本的特性來選擇:

  如果資料是稀疏的,或者對訓練時間要求較高,就採用自適應調整學習率的方法,如Adagrad、RMSprop、Adadelta、Adam,而後面三者由於加入了加權平均,在效果上是相似的,隨著梯度變得稀疏,Adam整體效果要比RMSprop好,如果不知道選用哪種方法,Adam是最好的選擇。

  在很多研究中經常用的SGD演算法,SGD一般來說效果比較好,但相比其他演算法時間可能較長,同時可能會被困在鞍點,但SGD依舊在論文中很受歡迎。

  以上就是梯度下降部分內容了,其實還有一些其他的比較新的梯度下降演算法,後面附上梯度下降overview文獻,其實還有一些其他的優化演算法,如最小二乘、牛頓法、擬牛頓法等,後面有時間會再進行學習。

 

參考資料:

梯度下降overview原文:https://arxiv.org/pdf/1609.04747.pdf

如何選擇梯度下降演算法文獻:http://www.redcedartech.com/pdfs/Select_Optimization_Method.pdf

優化演算法的視覺化:http://louistiao.me/notes/visualizing-and-animating-optimization-algorithms-with-matplotlib/

梯度下降總結:https://www.cnblogs.com/guoyaohua/p/8542554.html

 


因為時間關係,最近比較忙,這一篇更新時間較長,而且因為梯度下降是一個比較重要的演算法,所以就自己實現了一遍(但程式碼很爛),再涉及這方面內容就簡單回顧一下不再敘述了,參考資料的文獻還是有時間有必要再仔細研讀一下。

相關文章