ML之梯度下降算法 機器學習初學者的梯度下降算法
基本概念
本節(jié)嘗試獨立于機器學習算法, 單純地來講梯度下降算法 [Gradient Descent, GD], 以使梯度下降更具一般性。
開始之前, 先放 2 個基本概念, 帶著這 2 個認識, 在幾乎不具備機器學習知識的前提下, 應該也能很好地讀懂本節(jié)內容:
- 機器學習的主要任務之一, 就是通過訓練, 習得一組最優(yōu)的參數(shù). 常以成本函數(shù) [Cost Function] 作為參數(shù)估計的函數(shù), 因此, 機器學習的任務就轉變?yōu)榱俗钚』杀竞瘮?shù).
- 優(yōu)化是機器學習算法非常重要的組成部分, 幾乎每個機器學習算法都有一個優(yōu)化算法.
梯度下降算法就是一個被廣泛使用的優(yōu)化算法, 它可以用于尋找最小化成本函數(shù)的參數(shù)值. 用中學數(shù)學的語言來描述梯度下降, 是這樣的: 當函數(shù) _ 取得最小值時, 求所對應的自變量 的過程_ 此處, 就是機器要學習的參數(shù), 就是用于參數(shù)估計的成本函數(shù), 是關于 的函數(shù). 因此, 基本上具備中學數(shù)學知識的, 都能理解梯度下降算法.
梯度下降的基本步驟是:
- 對成本函數(shù)進行微分, 得到其在給定點的梯度. 梯度的正負指示了成本函數(shù)值的上升或下降:
- 選擇使成本函數(shù)值減小的方向, 即梯度負方向, 乘以以學習率 計算得參數(shù)的更新量, 并更新參數(shù):
- 重復以上步驟, 直到取得最小的成本
以上就是梯度下降算法最基礎也是最核心的概念, 很簡單吧.
下面講講梯度下降算法的幾個變種, 包括: 批量梯度下降 [Batch Gradient Descent, BGD], 隨機梯度下降 [Stochastic Gradient Descent, SGD], 小批量梯度下降 [Mini-Batch Gradient Descent, MBGD]
算法簡介
BGD
BGD 是梯度下降算法最原始的形式, 其特點是每次更新參數(shù) 時, 都使用整個訓練集的數(shù)據(jù).
BGD 的具體實現(xiàn)是這樣的:
- 設假設函數(shù)為:
- 所謂假設函數(shù), 就是用于將輸入映射為輸出的工具, 其返回值也稱為估計值
- 設成本函數(shù)為:
- 注意, 才是函數(shù)自變量, 是模型輸入, 是輸入對應的真實值
- 該成本函數(shù)中, 真正有效的部分是 , 前面的 是為后續(xù)計算方便添加的
- 對成本函數(shù)求導, 需要對每一個參數(shù) 分別求偏導, 得到它們各自的梯度:
- 機器學習模型通常不止一個參數(shù). 成本函數(shù)作為參數(shù)估計的工具, 要估計每個參數(shù)的最優(yōu)值, 因此需要對每一個參數(shù)分別求偏導數(shù)
- 每個參數(shù)都按梯度負方向進行更新:
因此, BGD 的偽代碼形式可以簡單地寫成:
- repeat {
- (for every j = 0, 1, .. n)
- }
上式中的求和部分 就體現(xiàn)了每一次迭代, 都以整個訓練集為對象進行梯度計算.
BGD 得到的是全局最優(yōu)解, 因為它總是以整個訓練集來計算梯度, 這是 BGD 的優(yōu)點. 但也因此帶來了巨大的計算量, 計算迭代速度很很慢.
SGD
SGD 每次以一個樣本, 而不是整個數(shù)據(jù)集來計算梯度. 因此, SGD 從成本函數(shù)開始, 就不必再求和了, 針對單個樣例的成本函數(shù)可以寫成: (此處的 同樣是為了后續(xù)計算方便設置的)
于是, SGD 的參數(shù)更新規(guī)則就可以寫成:
SGD 的偽代碼形式如下:
- repeat {
- for i = 1, .., m {
- (for every j = 0, 1, .. n)
- }
- }
SGD 的關鍵點在于以隨機順序選取樣本. 因為 SGD 存在局部最優(yōu)困境, 若每次都以相同的順序選取樣本, 其有很大的可能會在相同的地方陷入局部困境, 或者收斂減緩. 因此, 欲使 SGD 發(fā)揮更好的效果, 應充分利用隨機化 [Randomise] 帶來的優(yōu)勢: 可以在每次迭代之前 (偽代碼中最外圍循環(huán)), 對訓練集進行隨機排列.
因為每次只取一個樣本來進行梯度下降, SGD 的訓練速度很快, 但會引入噪聲, 使準確度下降. 這意味著并不是每次迭代都向著全局最優(yōu)而去, 即并不是每次迭代都能使成本函數(shù)值降低. 不過換個思路的話, 噪聲在一定程度上以使算法避免了局部最優(yōu).
SGD 的另一個好處是, 可以使用在線學習 [online learning]. 也就是說, 在模型訓練好之后, 只要有新的數(shù)據(jù)到來, 模型都可以利用新的數(shù)據(jù)進行再學習, 更新參數(shù),以適應新的變化.
MBGD
MBGD 是為解決 BGD 與 SGD 各自缺點而發(fā)明的折中算法, 或者說它利用了 BGD 和 SGD 各自優(yōu)點. 其基本思想是: 每次更新參數(shù)時, 使用 n 個樣本, 既不是全部, 也不是 1. (SGD 可以看成是 n=1 的 MBGD 的一個特例)
此處就不再給出 MBGD 的成本函數(shù)或其求導公式或參數(shù)更新規(guī)則公式了, 基本同 BGD, 見上.
MBGD 的偽代碼如下:
- say b=10, m=1000,
- repeat {
- for i = 1, 11, 21, .., 991 {
- (for every j = 0, 1, .. n)
- }
- }
總結一下以上 3 個梯度下降算法的優(yōu)缺點:
梯度下降算法 | 優(yōu)點 | 缺點 |
---|---|---|
BGD | 全局最優(yōu)解 | 計算量大, 迭代速度慢, 訓練速度慢 |
SGD | 1.訓練速度快 2. 支持在線學習 |
準確度下降, 有噪聲, 非全局最優(yōu)解 |
MBGD | 1. 訓練速度較快, 取決于小批量的數(shù)目 2. 支持在線學習 |
準確度不如 BGD, 仍然有噪聲, 非全局最優(yōu)解 |
Tips
下面將介紹一些梯度下降算法的使用技巧:
- 挑選合適的學習率 . 最好的選擇方法就是監(jiān)測目標函數(shù)值 (本文中就是成本函數(shù)值) 隨時間的學習曲線.
- 繪制成本-時間的曲線, 觀察成本隨迭代的變化情況, 即梯度的變化情況. 若成本沒有隨著迭代而下降, 說明學習率過大了 (發(fā)散了).
- 學習率通常是一個很小的實值, 比如 0.1, 0.001, 0.0001. 學習率如果過大, 成本函數(shù)可能無法收斂, 一直處于發(fā)散狀態(tài).
- 學習算法之前, 進行特征縮放能夠改善梯度下降. (親測! 原來使用較大學習率, 成本函數(shù)無法收斂; 使用特征縮放之后, 收斂了)
- 使用 MBGD 時, 在學習過程中, 可以逐漸增大小批量的大小, 更好地綜合發(fā)揮 BGD 與 SGD 的優(yōu)勢.
進階
以下內容有點深奧, 筆者對一些概念也不甚熟悉, 僅僅是做個記錄
我注意到 Keras 實現(xiàn)的 SGD 提供了 3 個關鍵字參數(shù): decay, momentum, nestrov:
- decay – 衰減的意思, 表示每一次迭代, 學習率 的衰減量.
- momentum – 動量的意思, 旨在加速學習, 稍后介紹.
- nesterov – 算法名, 表示是否使用 Nesterov 動量算法.
使用 BGD 達到極小值時, 整個成本函數(shù)的真實梯度會變得很小, 最終減小為 0, 因此 BGD 可以使用固定的學習率; 然而, SGD 中梯度估計引入的噪聲源不會在極小點處消失, 因此有必要隨著時間的推移逐漸降低學習率, 以保證 SGD 收斂.
實踐中, 一般讓學習率線性衰減, 直到第 次迭代:
其中, 表示第 k 次迭代, 表示第 次迭代, .
上式中, 需要設置的量包括: 初始學習率 , 最終學習率 以及 終止迭代次數(shù) . 通常取幾百的大小, 則設為 的 . 因此, 剩下的主要問題就是選擇一個合適的 : 取值太大, 學習曲線會劇烈抖動, 成本會明顯增加; 取值太小, 學習過程會變得很緩慢.
上面提到, 在梯度下降中引入動量的概念, 是為了加速學習, 特別是對于處理高曲率 (曲率: 曲線偏離直線的程度), 小但一致的梯度, 或者帶噪聲的梯度, 有明顯的加速效果. 因為動量算法積累了之前梯度指數(shù)級衰減的移動平均 (移動平均: 分析時間序列數(shù)據(jù)的有效工具), 能夠繼續(xù)沿該方向移動, 從而使成本持續(xù)減小.
從形式上看, 動量算法引入了 充當速度的角色, 代表參數(shù)在參數(shù)空間移動的方向和速率. 被設為負梯度的指數(shù)衰減平均, 其更新規(guī)則如下:
從上式可以得出的一個結論是: 相對于 , 越大, 之前梯度對現(xiàn)在方向的影響就越大.
在引入動量的概念之前, 的更新步長只是梯度范數(shù)乘以學習率, 引入動量之后則取決于梯度序列的大小和排列. 當許多連續(xù)的梯度指向相同時, 步長最大
Nesterov 動量算法是標準動量算法的變種, 其更新規(guī)則如下:
Nesterov 動量算法與標準動量算法的區(qū)別在于梯度的計算, 其梯度計算是在施加了當前速度之后, 可以解釋為向標準動量方法中添加了一個校正因子.
題外話
研究優(yōu)化算法的收斂率時, 一般會衡量額外誤差: , 即當前成本超出最低可能成本的量. SGD 應用于凸問題 (研究定義于凸集中的凸函數(shù)最小化的問題)時, k 步迭代的額外誤差量級是 , 在強凸情況下是 . 除非假定額外條件, 否則不能進一步改進.
Cramer-Rao 界限指出, 泛化誤差的下降速度不會快于 . Bottou and Bousquet 因此認為機器學習任務, 不值得探尋收斂快于 的優(yōu)化算法, 因為:
更快的收斂可能對應過擬合
樣例代碼
以下是 BGD, SGD, MBGD 的 Python 代碼實現(xiàn), 暫時不包括進階部分提到的高級內容.
- import numpy as np
- import pylab
- from sklearn.datasets.samples_generator import make_regression
- def bgd(alpha, x, y, numIterations):
- """Copied from Internet"""
- m = x.shape[0] # number of samples
- theta = np.ones(2)
- J_list = []
- x_transpose = x.transpose()
- for iter in range(0, numIterations):
- hypothesis = np.dot(x, theta)
- loss = y - hypothesis
- J = np.sum(loss ** 2) / (2 * m) # cost
- J_list.append(J)
- print("iter %s | J: %.3f" % (iter, J))
- gradient = np.dot(x_transpose, loss) / m
- theta += alpha * gradient # update
- pylab.plot(range(numIterations), J_list, "k-")
- return theta
- def sgd(alpha, x, y, num_iter):
- """Writtern by kissg"""
- m = x.shape[0] # number of samples
- theta = np.ones(2)
- J_list = []
- # 隨機化序列
- idx = np.random.permutation(y.shape[0])
- x, y = x[idx], y[idx]
- for j in range(num_iter):
- for i in idx:
- single_hypothesis = np.dot(x[i], theta)
- single_loss = y[i] - single_hypothesis
- gradient = np.dot(x[i].transpose(), single_loss)
- theta += alpha * gradient # update
- hypothesis = np.dot(x, theta)
- loss = y - hypothesis
- J = np.sum(loss ** 2) / (2 * m) # cost
- J_list.append(J)
- print("iter %s | J: %.3f" % (j, J))
- pylab.plot(range(num_iter), J_list, "r-")
- return theta
- def mbgd(alpha, x, y, num_iter, minibatches):
- """Writtern by kissg"""
- m = x.shape[0] # number of samples
- theta = np.ones(2)
- J_list = []
- for j in range(num_iter):
- idx = np.random.permutation(y.shape[0])
- x, y = x[idx], y[idx]
- mini = np.array_split(range(y.shape[0]), minibatches)
- for i in mini:
- mb_hypothesis = np.dot(x[i], theta)
- mb_loss = y[i] - mb_hypothesis
- gradient = np.dot(x[i].transpose(), mb_loss) / minibatches
- theta += alpha * gradient # update
- hypothesis = np.dot(x, theta)
- loss = y - hypothesis
- J = np.sum(loss ** 2) / (2 * m) # cost
- J_list.append(J)
- print("iter %s | J: %.3f" % (j, J))
- pylab.plot(range(num_iter), J_list, "y-")
- return theta
- if __name__ == '__main__':
- x, y = make_regression(n_samples=100, n_features=1, n_informative=1,
- random_state=0, noise=35)
- m, n = np.shape(x)
- x = np.c_[np.ones(m), x] # insert column, bias
- alpha = 0.01 # learning rate
- pylab.plot(x[:, 1], y, 'o')
- print("\n#***BGD***#\n")
- theta_bgd = bgd(alpha, x, y, 800)
- for i in range(x.shape[1]):
- y_bgd_predict = theta_bgd * x
- pylab.plot(x, y_bgd_predict, 'k--')
- print("\n#***SGD***#\n")
- theta_sgd = sgd(alpha, x, y, 10)
- for i in range(x.shape[1]):
- y_sgd_predict = theta_sgd * x
- pylab.plot(x, y_sgd_predict, 'r--')
- print("\n#***MBGD***#\n")
- theta_mbgd = mbgd(alpha, x, y, 50, 10)
- for i in range(x.shape[1]):
- y_mbgd_predict = theta_mbgd * x
- pylab.plot(x, y_mbgd_predict, 'y--')
- pylab.show()
- print("Done!")
執(zhí)行以上代碼, 得到 3 類梯度下降算法的函數(shù)圖像如下圖所示.
- 黑色是 BGD 的圖像, 是一條光滑的曲線, 因為 BGD 每一次迭代求得的都是全局最優(yōu)解;
- 紅色是 SGD 的圖像, 可見抖動很劇烈, 有不少局部最優(yōu)解;
- 黃色是 MBGD 的圖像, 相對 SGD 的成本-時間曲線平滑許多, 但仔細看, 仍然有抖動.