機器學習 | 梯度下降原理及Python實現


相關文章:

機器學習 | 目錄

機器學習 | 網絡搜索及可視化

監督學習 | 線性迴歸原理及Sklearn實現

An overview of gradient descent optimization algorithms

梯度下降優化算法綜述

Keras 中的優化程序

1. 梯度下降

梯度下降(Gradient Descent)是一種非常通用的優化算法,能夠爲大範圍的問題找到最優解。梯度下降的中心思想就是迭代地調整參數從而使成本函數最小化。

假設你迷失在山上的迷霧中,你能夠感覺到的只有你腳下路面的坡度。快速到達山腳的策略就是沿着最陡的方向下坡。這就是梯度下降的做法:通過測量參數向量 θ\theta 相關的誤差函數的局部梯度,並不斷沿着梯度的方向調整,演到梯度降爲 0,到達最小值!

具體來說,首先使用一個隨機的 θ\theta 值(這被稱爲隨機初始化),然後逐步改進,每次踏出一步,每一步都嘗試降低一點成本函數(如在線性迴歸中採用 MSE),直到算法收斂出一個最小值,如下所示:

梯度下降中一個重要參數就是每一步的步長,這卻取決於超參數學習率(Learning Rate)。如果學習率態度,算法需要經過大量迭代才能收斂,這將耗費很長時間,如圖所示,學習率太低:

反過來說,如果學習率太高,那可能會越過山谷直接到達山的另一邊(並沒有藍精靈),設置有可能比之前的起點還要高。這會導致算法發散,值越來越大,最後無法找到好的解決方案,如下所示,學習率太高:

最後,並不是所有的成本函數看起來都像一個漂亮的碗。有的可能看着像洞、像山脈、像高原或者是各種不規則的地形,導致很難收斂到最小值。

下圖顯示了梯度下降的兩個主要挑戰:如果隨機初始化,算法從左側起步,那麼會收斂到一個局部最小值,而不是全局最小值。如果從右側起步,那麼需要經過很長時間才能越過整片高原,如果停下來太早,將永遠達不到全局最小值。

以線性迴歸模型爲例,其成本函數 MSE 恰好是個凸函數,這意味着連接曲線上任意兩個點的線段永遠不會跟曲線相交。也就是說不存在局部最小,只有一個全局最小值,它同時也是一個連續函數,所以斜率不會產生陡峭的變化(即漢族利普西茨條件)。這兩點保證了即便是亂走,梯度下降都可以趨近到全局最小值(只要等待時間足夠長,學習率也不是太高)。

成本函數雖然是碗狀的,但如果不同特徵的尺寸差別巨大,那它可能是一個非常細長的碗。如下圖所示的梯度下降,左邊的訓練集上特徵 1 和特徵 2 具有相同的數值規模,而右邊的訓練集上,特徵 1 的數值則比特徵 2 要小得多(因爲特徵 1 的值較小,所以 θ1\theta_1 需要更大的變化來來影響成本函數,這就是爲什麼碗形會沿着 θ1\theta_1 軸拉長)。

特徵值無縮放和特徵值縮放的梯度下降:

正如你所見,左圖的梯度下降算法直接走向最小值,可以快速到達。而在右圖中,顯示沿着與全局最小值方向近乎垂直的方向前進,接下來是一段幾乎平坦的長長的山谷。最後還是會抵達最小值,但是這需要花費大量的時間。

應用梯度下降時,需要保證全有特徵值的大小比例都差不多(比如使用 Sklearn 的 StandardScaler 類),否則收斂的時間會長很多。

這張圖也說明,訓練模型也就是搜尋使成本函數(在訓練集上)最小化的參數組合。這是模型參數空間層面上的搜索:模型的參數越多,這個空間的維度就越多,搜索就越難。同樣是在乾草堆裏找尋一根針,在一個三百維的空間裏就比一個在三維空間裏要棘手得多,幸運的是,對於成本函數爲凸函數的,針就躺在碗底。[1]

1.1 批量梯度下降(BGD)

要實現梯度下降,需要計算每個模型關於參數 θj\theta_j 的成本函數的梯度。換言之,需要計算的是如果改變 θj\theta_j,成本函數會改變多少,即偏導數

以線性迴歸的成本函數 MSEMSE 爲例,其偏導數爲:

θjMSE(θ)=θj(1mi=1m(θTX(i)y(i))2)=2mi=1m(θTx(i)y(i))xj(i)(1) \begin{aligned} \frac{\partial}{\partial \theta_j}MSE(\theta) &=\frac{\partial}{\partial \theta_j} \bigg(\frac{1}{m}\sum_{i=1}^m(\theta^T \cdot X^{(i)}-y^{(i)})^2 \bigg)\\ &=\frac{2}{m}\sum_{i=1}^m(\theta^T \cdot x^{(i)}-y^{(i)})x_j^{(i)}\\ \end{aligned}\tag{1}

如果不想單獨計算這些梯度,可以使用公式 (2) 對其進行一次性計算。梯度向量 θMSE(θ)\nabla_\theta MSE(\theta),包含所有成本函數(每個模型參數一個)的偏導數。

成本函數 MSEMSE 的梯度向量:

θMSE(θ)=(θ0MSE(θ)θ1MSE(θ)θnMSE(θ))=2mXT(Xθy)(2) \nabla_\theta MSE(\theta)= \left( \begin{array}{cc} \frac{\partial}{\partial \theta_0}MSE(\theta) \\ \frac{\partial}{\partial \theta_1}MSE(\theta) \\ \vdots \\ \frac{\partial}{\partial \theta_n}MSE(\theta) \\ \end{array} \right) =\frac{2}{m}X^T \cdot(X \cdot \theta - y) \tag{2}

對於公式 (2),其在計算梯度下降的每一步時,都是基於完整的訓練集 XX 的。這就是爲什麼該算法被稱爲批量梯度下降(Batch Gradient Descent):每一步都使用整批訓練數據。因此,面對非常龐大的訓練集時,算法會變得極慢。但是,梯度下降算法隨特徵數量擴展的表現比較好:對於線性擬合,如果要訓練的線性模型擁有幾十萬個特徵,使用梯度下降仍比標準方法要快得多。

一旦有了梯度向量,那個點向上,就朝反方向下坡。也就是從 θ\theta 中減去 θMSE(θ)\nabla_\theta MSE(\theta) 。這時學習率 η\eta 就發揮作用了:用梯度向量乘以 η\eta 確定下坡步長的大小。

梯度下降步長:

θ(nextstep)=θηθMSE(θ)(3)\theta^{(next step)} = \theta - \eta \nabla_\theta MSE(\theta) \tag{3}

1.1.1 學習率的設置

我們來看一下分別使用三種不同學習率時,梯度下降的前十步(虛線表示起點):

import numpy as np
import matplotlib.pyplot as plt

X = 2 * np.random.rand(100, 1)
y = 4 + 3 * X + np.random.randn(100, 1)

X_b = np.c_[np.ones((100, 1)), X]  # add x0 = 1 to each instance
X_new = np.array([[0], [2]])
X_new_b = np.c_[np.ones((2, 1)), X_new]

eta = 0.1
n_iterations = 1000
m = 100
theta = np.random.randn(2,1)

for iteration in range(n_iterations):
    gradients = 2/m * X_b.T.dot(X_b.dot(theta) - y)
    theta = theta - eta * gradients
    
theta
X_new_b.dot(theta)
array([[4.04184331],
       [9.84392154]])
theta_path_bgd = []

def plot_gradient_descent(theta, eta, theta_path=None):
    m = len(X_b)
    plt.plot(X, y, "b.")
    n_iterations = 1000
    for iteration in range(n_iterations):
        if iteration < 10:
            y_predict = X_new_b.dot(theta)
            style = "b-" if iteration > 0 else "r--"
            plt.plot(X_new, y_predict, style)
        gradients = 2/m * X_b.T.dot(X_b.dot(theta) - y)
        theta = theta - eta * gradients
        if theta_path is not None:
            theta_path.append(theta)
    plt.xlabel("$x_1$", fontsize=18)
    plt.axis([0, 2, 0, 15])
    plt.title(r"$\eta = {}$".format(eta), fontsize=16)
np.random.seed(42)
theta = np.random.randn(2,1)  # random initialization

plt.figure(figsize=(10,4))
plt.subplot(131); plot_gradient_descent(theta, eta=0.02)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.subplot(132); plot_gradient_descent(theta, eta=0.1, theta_path=theta_path_bgd)
plt.subplot(133); plot_gradient_descent(theta, eta=0.5)
plt.show()

可以看出:

  • 左圖的學習率太低:在前十步依然無法找到解決方案,但是隻要結果長時間的迭代就一定可以找到解決方案;

  • 中間的學習率看起來非常的棒:幾次迭代就收斂出了最終解;

  • 而右邊的學習率太高:算法發散,直接跳過了數據區域,並且每一步都離實際解決方案越來越遠。

要找到合適的學習率,可以使用網絡搜索。但是你可能需要限制迭代次數,這樣網絡搜索就可以淘汰掉那些收斂耗時太長的模型。

然而怎麼限制迭代次數呢?如果設置太低,算法可能在離最優解還很遠時就停止了;但是如果設置得太高,模型到達最優解後,繼續迭代參數不再變化,又會浪費時間。

一個簡單的方法時,在開始設置一個非常大的迭代次數,但是當梯度向量的值變得很微小時中斷算法——也就是當他的範數變得低於 ε\varepsilon(稱爲容差)時,因爲這是梯度下降已經(幾乎)到達了最小值。

收斂率:當成本函數爲凸函數,並且斜率沒有陡峭的變化時(如 MSE 成本函數),通過批量梯度下降可以看出一個固定的學習率有一個收斂率,爲 o(1)o(\frac{1}{迭代次數})。換句話說,如果將容差 ε\varepsilon 縮小爲原來的1/10(以得到更精確的解),算法將不得不運行 10 倍的迭代次數。

1.1.2 Python 實現 BGD

Python 實現批量梯度下降計算線性迴歸模型 θ\theta

import numpy as np
import matplotlib.pyplot as plt

X = 2 * np.random.rand(100, 1)
y = 4 + 3 * X + np.random.randn(100, 1)

X_b = np.c_[np.ones((100, 1)), X]  # add x0 = 1 to each instance

eta = 0.1
n_iterations = 1000
m = 100
theta = np.random.randn(2,1)

for iteration in range(n_iterations):
    gradients = 2/m * X_b.T.dot(X_b.dot(theta) - y)
    theta = theta - eta * gradients
    
print('theta:\n{}\n'.format(theta))

X_new = np.array([[0], [2]])
X_new_b = np.c_[np.ones((2, 1)), X_new]  # add x0 = 1 to each instance
y_predict = X_new_b.dot(theta)

plt.plot(X_new, y_predict, "r-")
plt.plot(X, y, "b.")
plt.axis([0, 2, 0, 15])
plt.show()
theta:
[[4.20831857]
 [2.79226572]]

利用批量梯度下降法計算的 theta 結果與標準方程法的結果一致!

1.2 隨機梯度下降(SGD)

批量梯度下降的主要問題時它要用整個訓練集來計算每一步的梯度,所以訓練集很大時,算法會特別慢。與之相反的極端是隨機梯度下降(Stochastic Gradient Descent),每一步在訓練集中隨機選擇一個實例,並且僅基於該單個實例來計算梯度。顯然,這讓算法變得快多了,因爲每個迭代都只需要操作少量的數據。它也可以被用來訓練海量的數據集,因爲每次迭代只需要在內存中運行一個實例即可( SGD 可以作爲核外算法實現)。

另一方面,由於算法的隨機性質,它比批量梯度下降要不規則得多。成本函數將不再是緩緩降低知道抵達最小值,而是不斷上上下下,但是從整體來看,還是在慢慢下降。隨着時間的推移,最終會非常接近最小值,但是即使它到達了最小值,依然還會持續反彈,永遠不會停止。所以算法停下來的參數值肯定是足夠好的,但不是最優的。

當成本函數非常不規則時(如高原的例子),隨機梯度下降其實可以幫助算法跳出局部最小值,所以相比批量梯度下降,它對找到全局最小值更有優勢。

因爲,隨機性的好處在於可以逃離局部最優,但缺點是永遠定位不出最小值。要解決這個困境,有一個辦法時逐步降低學習率。開始的步長比較大(這有助於快速進展和逃離局部最小值),然後越來越小,讓算法儘量靠近全局最小值。這個過程叫做模擬退火,因爲它類似於冶金時融化的金屬慢慢冷卻的退火過程。確定每個迭代學習率的函數叫作學習計劃。如果學習率降得太快,可能會陷入局部最小值,甚至是停留在走向最小值的半途中。如果學習率太慢,你可能需要太長時間太能跳到差不多最小值附近,如果提早結束訓練,可能只得到一個次優的解決方案。

1.2.1 Python 實現 SGD

按照慣例,我們用 n_epochs 來表示迭代次數,每一次迭代稱爲一輪。

import numpy as np
import matplotlib.pyplot as plt

X = 2 * np.random.rand(100, 1)
y = 4 + 3 * X + np.random.randn(100, 1)
X_b = np.c_[np.ones((100, 1)), X]  # add x0 = 1 to each instance

theta_path_sgd = []
m = len(X_b)
np.random.seed(42)

n_epochs = 50
t0, t1 = 5, 50  # learning schedule hyperparameters

def learning_schedule(t):
    return t0 / (t + t1)

theta = np.random.randn(2,1)  # random initialization

for epoch in range(n_epochs):
    for i in range(m):
        if epoch == 0 and i < 20:                    
            y_predict = X_new_b.dot(theta)           
            style = "b-" if i > 0 else "r--"         
            plt.plot(X_new, y_predict, style)        
        random_index = np.random.randint(m)
        xi = X_b[random_index:random_index+1]
        yi = y[random_index:random_index+1]
        gradients = 2 * xi.T.dot(xi.dot(theta) - yi)
        eta = learning_schedule(epoch * m + i)
        theta = theta - eta * gradients
        theta_path_sgd.append(theta)                

plt.plot(X, y, "b.")                                
plt.xlabel("$x_1$", fontsize=18)                    
plt.ylabel("$y$", rotation=0, fontsize=18)          
plt.axis([0, 2, 0, 15])                             
plt.show()              
theta
array([[3.954103  ],
       [3.03548045]])

前面的批量梯度下降需要在整個訓練集上迭代 1000 次,而這段代碼只迭代了 50 次就得到了一個相當不錯的解。

1.2.2 Sklearn 實現 SGD

在 Scikit-Learn 裏,用 SGD 執行線性迴歸可以使用 sklearn.linear_model.SGDRegressor 類,其默認優化的成本函數是平方誤差。

我們從學習率爲 0.1(eta0=0.1)開始,使用默認學習計劃(與之前的學習計劃不同)運行了 50 輪,並且沒有使用任何的正則化(penalty=None),將得到一個跟標準方程的解非常相近的解決方案:

from sklearn.linear_model import SGDRegressor
sgd_reg = SGDRegressor(max_iter=50, tol=-np.infty, penalty=None, eta0=0.1, random_state=42)
sgd_reg.fit(X, y.ravel())
sgd_reg.intercept_, sgd_reg.coef_
(array([3.9539613]), array([3.05185657]))

1.3 小批量梯度下降(MBGD)

小批量梯度下降(Mini-Batch Gradient Descent):每一步的梯度計算,既不是基於整個訓練集(如批量梯度下降),也不是基於單個實例(如隨機梯度下降),而是基於一小部分隨機的實例(也就是小批量)。

相比隨機梯度下降,小批量梯度下降的主要優勢在於可以從矩陣運算的硬件優化中獲得顯著的性能提升,特別是需要用到圖形處理器時。MBGD 算法在參數空間層面的前進過程也不像 SGD 那麼不穩定,特別是批量較大時。所以小批量梯度下降最終會比 SGD 更接近最小值一些。但是另一方面,它可能更難從局部最小值中逃脫。

2. 三類梯度下降的比較

如下圖所示,三種梯度下降算法在訓練過程中參數空間裏的行進路線,它們最終都彙集在最小值附近,批量梯度下降最終停在了最小值上,而隨機梯度下降和小批量梯度下降還在繼續遊走。但是批量梯度下降花費了大量時間來計算每一步,而用好了學習計劃,梯度下降和小批量梯度下降也同樣能達到最小值。

theta_path_mgd = []

n_iterations = 50
minibatch_size = 20

np.random.seed(42)
theta = np.random.randn(2,1)  # random initialization

t0, t1 = 200, 1000
def learning_schedule(t):
    return t0 / (t + t1)

t = 0
for epoch in range(n_iterations):
    shuffled_indices = np.random.permutation(m)
    X_b_shuffled = X_b[shuffled_indices]
    y_shuffled = y[shuffled_indices]
    for i in range(0, m, minibatch_size):
        t += 1
        xi = X_b_shuffled[i:i+minibatch_size]
        yi = y_shuffled[i:i+minibatch_size]
        gradients = 2/minibatch_size * xi.T.dot(xi.dot(theta) - yi)
        eta = learning_schedule(t)
        theta = theta - eta * gradients
        theta_path_mgd.append(theta)
theta
array([[3.9746783 ],
       [3.07082231]])
theta_path_bgd = np.array(theta_path_bgd)
theta_path_sgd = np.array(theta_path_sgd)
theta_path_mgd = np.array(theta_path_mgd)

plt.figure(figsize=(7,4))
plt.plot(theta_path_sgd[:, 0], theta_path_sgd[:, 1], "r-s", linewidth=1, label="Stochastic")
plt.plot(theta_path_mgd[:, 0], theta_path_mgd[:, 1], "g-+", linewidth=2, label="Mini-batch")
plt.plot(theta_path_bgd[:, 0], theta_path_bgd[:, 1], "b-o", linewidth=3, label="Batch")
plt.legend(loc="upper left", fontsize=16)
plt.xlabel(r"$\theta_0$", fontsize=20)
plt.ylabel(r"$\theta_1$   ", fontsize=20, rotation=0)
plt.axis([2.5, 4.5, 2.3, 3.9])
plt.show()

最後,我們來比較一下目前爲止所討論過的線性迴歸算法( m 是訓練實例的數量,n 是特徵數量):

參考資料

[1] 周志華. 機器學習[M]. 北京: 清華大學出版社, 2016: 106-115.

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