在瞭解梯度下降(Gradient Descent)之前,我們先要知道有關線性迴歸的基本知識,這樣可以進一步的加深對梯度下降的理解,當然梯度下降(Gradient Descent)並不單單隻能進行迴歸預測,它還可以進行諸如分類等操作。
關於線性迴歸的具體講解本文不詳細涉及,只簡單列出幾個相關公式。(關於線性迴歸可以看這篇 ?傳送門)
線性迴歸
公式 4-1:線性迴歸模型預測
- 是預測值
- 是特徵的數量
- 是 個特徵值
- 是第 個模型參數 (包括偏置項 以及特徵權重 )
也可以用更爲簡潔的向量化形式表達
公式 4-2:線性迴歸模型預測 (向量化)
- 是模型的參數向量,包括偏置項 以及特徵權重 到
- 是 的轉置向量 (爲行向量,而不再是列向量)
- 是實例的特徵向量,包括從 到 永遠爲
- 是 和 的點積
- 是模型參數 的假設函數
公式 4-3:線性迴歸模型的 成本函數
標準方程
爲了得到使成本函數最小的 值,有一個閉式解方法——也就是一個直接得出結果的數學方程,即標準方程。
公式 4-4:標準方程
- 是使成本函數最小的 值
- 是包含 到 的目標值量
我們生成一些線性數據來測試這個公式:
import numpy as np
X = 2 * np.random.rand(100, 1)
y = 4 + 3 * X + np.random.randn(100, 1)
import matplotlib.pyplot as plt
%matplotlib inline
# 可視化
fig, ax = plt.subplots(figsize=(12,8))
ax.plot(X, y, "b.")
plt.show()
現在我們使用標準方程來計算 。使用 Numpy 的線性代數模塊 (np.linalg
) 中的 inv()
函數來對矩陣求逆,並用 dot()
方法計算矩陣的內積:
X_b = np.c_[np.ones((100, 1)), X] # add xo = 1 to each instance
theta_best = np.linalg.inv(X_b.T.dot(X_b)).dot(X_b.T).dot(y)
我們實際用來生成數據的函數是 。
theta_best
array([[4.0939709 ],
[3.08934507]])
我們期待的是 得到的是 。非常接近了,因爲噪聲的存在使其不可能完全還原爲原本的函數。
現在可以用 做出預測:
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_best)
y_predict
array([[ 4.0939709 ],
[10.27266104]])
# 繪製模型的預測結果
fig, ax = plt.subplots(figsize=(12,8))
ax.plot(X_new, y_predict, "r-")
ax.plot(X, y, "b.")
plt.show()
# Scikit-Learn 的等效代碼
from sklearn.linear_model import LinearRegression
lin_reg = LinearRegression()
lin_reg.fit(X, y)
LinearRegression(copy_X=True, fit_intercept=True, n_jobs=None,
normalize=False)
lin_reg.intercept_, lin_reg.coef_
(array([4.0939709]), array([[3.08934507]]))
lin_reg.predict(X_new)
array([[ 4.0939709 ],
[10.27266104]])
梯度下降
梯度下降是一種非常通用的優化算法,能夠爲大範圍的問題找到最優解。梯度下降的中心思想就是迭代地調整參數從而使成本函數最小化。
假設你迷失在山上的濃霧之中,你能感覺到的只有你腳下路面的坡度。快速到達山腳的一個策略就是沿着最陡的方向下坡。這就是梯度下降的做法:通過測量參數向量 相關的誤差函數的局部梯度,並不斷沿着降低梯度的方向調整,直到梯度降爲0,到達最小值!
具體來說,首先使用一個隨機的 值(這被稱爲隨機初始化), 然後逐步改進,每次踏出一步,每一步都嘗試降低一點成本函數(如 ),直到算法收斂出一個最小值(參見圖4-3)
梯度下降中一個重要參數是每一步的步長,這取決於超參數學習率。如果學習率太低,算法需要經過大量迭代才能收斂,這將耗費很長時間(參見圖4-4)。
反過來說,如果學習率太高,那你可能會越過山谷直接到達山的另一邊,甚至有可能比之前的起點還要高。這會導致算法發散,值越來越大,最後無法找到好的解決方案(參見圖4-5)。
最後,並不是所有的成本函數看起來都像一個漂亮的碗。有的可能看着像洞、像山脈、像高原或者是各種不規則的地形,導致很難收斂到最小值。圖4-6顯示了梯度下降的兩個主要挑戰:如果隨機初始化,算法從左側起步,那麼會收斂到一個局部最小值,而不是全局最小值。如果算法從右側起步,那麼需要經過很長時間才能越過整片高原,如果你停下得太早,將永遠達不到全局最小值。
幸好,線性迴歸模型的 成本函數恰好是個凸函數,這意味着連接曲線上任意兩個點的線段永遠不會跟曲線相交。也就是說不存在局部最小,只有一個全局最小值。它同時也是一個連續函數,所以斜率不會產生陡峭的變化1。這兩件事保證的結論是:即便是亂走,梯度下降都可以趨近到全局最小值(只要等待時間足夠長,學習率也不是太高)。
成本函數雖然是碗狀的,但如果不同特徵的尺寸差別巨大,那它可能是一個非常細長的碗。如圖4-7所示的梯度下降,左邊的訓練集上特徵1和特徵2具有相同的數值規模,而右邊的訓練集上,特徵1的值則比特徵2要小得多。因爲特徵1的值較小,所以 需要更大 的變化來影響成本函數,這就是爲什麼碗形會沿着 軸拉長。)
正如你所見,左圖的梯度下降算法直接走向最小值,可以快速到達。而在右圖中,先是沿着與全局最小值方向近乎垂直的方向前進, 接下來是一段幾乎平坦的長長的山谷。最終還是會抵達最小值,但是這需要花費大量的時間。
注意: 應用梯度下降時,需要保證所有特徵值的大小比例都差不多 (比如使用Scikit-Learn的StandardScaler類),否則收斂的時間會長很多。
這張圖也說明,訓練模型也就是搜尋使成本函數(在訓練集上)最小化的參數組合。這是模型參數空間層面上的搜索:模型的參數越多,這個空間的維度就越多,搜索就越難。同樣是在乾草堆裏尋找一根針,在一個三百維的空間裏就比在一個三維空間裏要棘手得多。幸運的是,線性迴歸模型的成本函數是凸函數,針就躺在碗底。
批量梯度下降
要實現梯度下降,你需要計算每個模型關於參數 的成本函數的梯度。換言之,你需要計算的是如果改變 ,成本函數會改變多少。 這被稱爲偏導數。這就好比是在問 “如果我面向東,我腳下的坡度斜率是多少?” 然後面向北問同樣的問題(如果你想象超過三個維度的宇宙,對於其他的維度以此類推)。公式4-5計算了關於參數 的成本函數的偏導數,計作
公式 4-5 :成本函數的偏導數
如果不想單獨計算這些梯度,可以使用公式4-6對其進行一次性計算。梯度向量,記作 ,包含所有成本函數(每個模型參數一個)的偏導數。
公式 4-6 :成本函數的梯度向量
注意: 公式4-6在計算梯度下降的每一步時,都是基於完整的訓練集 的。這就是爲什麼該算法會被稱爲批量梯度下降:每一步都使用整批訓練數據。因此,面對非常龐大的訓練集時,算法會變得極慢(不過我們即將看到快得多的梯度下降算法)。但是,梯度下降算法隨特徵數量擴展的表現比較好:如果要訓練的線性模型擁有幾十萬個特徵,使用梯度下降比標準方程要快得多。
一旦有了梯度向量,哪個點向上,就朝反方向下坡。也就是從 中減去 。這時學習率 就發揮作用了:用梯度向量乘以 確定下坡步長的大小(公式4-7)。
公式 4-6 :梯度下降步長
eta = 0.1 # learning rate
n_iterations = 1000
m = 100
theta = np.random.randn(2,1) # random initialization
for iteration in range(n_iterations):
gradients = 2/m * X_b.T.dot(X_b.dot(theta) - y)
theta = theta - eta * gradients
theta
array([[4.0939709 ],
[3.08934507]])
這不正是標準方程的發現麼!梯度下降表現完美。如果使用了其他的學習率 呢?圖4-8展現了分別使用三種不同的學習率時, 梯度下降的前十步(虛線表示起點)。
左圖的學習率太低:算法最終還是能找到解決方法,就是需要太長時間。中間的學習率看起來非常棒:幾次迭代就收斂出了最終解。 而右邊的學習率太高:算法發散,直接跳過了數據區域,並且每一步都離實際解決方案越來越遠。
要找到合適的學習率,可以使用網格搜索。但是你可能需要限制迭代次數,這樣網格搜索可以淘汰掉那些收斂耗時太長的模型。
你可能會問,要怎麼限制迭代次數呢?如果設置太低,算法可能在離最優解還很遠時就停了;但是如果設置得太高,模型達到最優解後,繼續迭代參數不再變化,又會浪費時間。一個簡單的辦法是,在 開始時設置一個非常大的迭代次數,但是當梯度向量的值變得很微小時中斷算法——也就是當它的範數變得低於 (稱爲容差)時,因爲這時梯度下降已經(幾乎)到達了最小值。
收斂率
成本函數爲凸函數,並且斜率沒有陡峭的變化時(如 成本函數),通過批量梯度下降可以看出一個固定的學習率有一個收斂率,爲 。換句話說,如果將容差 縮小爲原來的 (以得到更精確的解),算法將不得不運行10倍的迭代次數
隨機梯度下降
批量梯度下降的主要問題是它要用整個訓練集來計算每一步的梯度,所以訓練集很大時,算法會特別慢。與之相反的極端是隨機梯度下降,每一步在訓練集中隨機選擇一個實例,並且僅基於該單個實例來計算梯度。顯然,這讓算法變得快多了,因爲每個迭代都只需要操作少量的數據。它也可以被用來訓練海量的數據集,因爲每次迭代只需要在內存中運行一個實例即可( 可以作爲核外算法實現)。
另一方面,由於算法的隨機性質,它比批量梯度下降要不規則得多。成本函數將不再是緩緩降低直到抵達最小值,而是不斷上上下下,但是從整體來看,還是在慢慢下降。隨着時間推移,最終會非常接近最小值,但是即使它到達了最小值,依舊還會持續反彈,永遠不會停止(見圖4-9)。所以算法停下來的參數值肯定是足夠好的,但不是最優的。
當成本函數非常不規則時(見圖4-6),隨機梯度下降其實可以幫助算法跳出局部最小值,所以相比批量梯度下降,它對找到全局最小值更有優勢。
因此,隨機性的好處在於可以逃離局部最優,但缺點是永遠定位不出最小值。要解決這個困境,有一個辦法是逐步降低學習率。開始的步長比較大(這有助於快速進展和逃離局部最小值),然後越來越小,讓算法儘量靠近全局最小值。這個過程叫作模擬退火,因爲它類似於冶金時熔化的金屬慢慢冷卻的退火過程。確定每個迭代學習率的函數叫作學習計劃。如果學習率降得太快,可能會陷入局部最小值, 甚至是停留在走向最小值的半途中。如果學習率降得太慢,你需要太長時間才能跳到差不多最小值附近,如果提早結束訓練,可能只得到一個次優的解決方案。
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):
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
array([[4.11135275],
[3.06756448]])
圖 4-10 顯示了訓練過程的前 10 步 (注意不規則的步子)
因爲實例是隨機挑選,所以在同一輪裏某些實例可能被挑選多次,而有些實例則完全沒被選到。如果你希望每一輪算法都能遍歷每個實例,有一種辦法是將訓練集洗牌打亂,然後一個接一個的使用實例,用完再重新洗牌,以此繼續。不過這種方法通常收斂得更慢。
在 Scikit-Learn
裏,用 執行線性迴歸可以使用 SGDRegressor
類,其默認優化的成本函數是平方誤差。下面這段代碼從學習率 0.1 開始(eta0=0.1
),使用默認的學習計劃(跟前面的學習計劃不同) 運行了50 輪,而且沒有使用任何正則化(penalty=None
):
import warnings
warnings.filterwarnings('ignore')
from sklearn.linear_model import SGDRegressor
sgd_reg = SGDRegressor(n_iter=50, penalty=None, eta0=0.1)
sgd_reg.fit(X, y.ravel())
SGDRegressor(alpha=0.0001, average=False, early_stopping=False, epsilon=0.1,
eta0=0.1, fit_intercept=True, l1_ratio=0.15,
learning_rate='invscaling', loss='squared_loss', max_iter=None,
n_iter=50, n_iter_no_change=5, penalty=None, power_t=0.25,
random_state=None, shuffle=True, tol=None, validation_fraction=0.1,
verbose=0, warm_start=False)
你再次得到了一個跟標準方程的解非常相近的解決方案:
sgd_reg.intercept_, sgd_reg.coef_
(array([4.08805401]), array([3.08242337]))
小批量梯度下降
我們要了解的最後一個梯度下降算法叫作小批量梯度下降。一旦理解了批量梯度下降和隨機梯度下降,這個算法就非常容易理解了: 每一步的梯度計算,既不是基於整個訓練集(如批量梯度下降)也不是基於單個實例(如隨機梯度下降),而是基於一小部分隨機的實例集也就是小批量。相比隨機梯度下降,小批量梯度下降的主要優勢在於可以從矩陣運算的硬件優化中獲得顯著的性能提升,特別是需要用到圖形處理器時。
這個算法在參數空間層面的前進過程也不像 那樣不穩定,特別是批量較大時。所以小批量梯度下降最終會比 更接近最小值一 些。但是另一方面,它可能更難從局部最小值中逃脫(不是我們前面看到的線性迴歸問題,而是對於那些深受局部最小值陷阱困擾的問題)。圖 4-11 顯示了三種梯度下降算法在訓練過程中參數空間裏的行進路線。它們最終都匯聚在最小值附近,批量梯度下降最終停在了最小值上,而隨機梯度下降和小批量梯度下降還在繼續遊走。但是,別忘了批量梯度可是花費了大量時間來計算每一步的,如果用好了學習計劃,隨機梯度下降和小批量梯度下降也同樣能到達最小值。
最後,我們來比較一下到目前爲止所討論過的線性迴歸算法 ( 是訓練實例的數量, 是特徵數量)。
參考: