隨機梯度下降在機器學習應用中被廣泛使用,。與反向傳播相結合,,它在神經網絡訓練應用中占主導地位。
在本文中,,我們將一起學習:
如何應用梯度下降和隨機梯度下降來最小化 機器學習中的損失函數 什么是學習率 ,,為什么它很重要,以及它如何影響結果 基本梯度下降算法 梯度下降算法是一種數學優(yōu)化的近似和迭代方法,。我們可以用它來逼近任何可微分函數的最小值,。
雖然梯度下降有時會卡在局部最小值或鞍點,而不是找到全局最小值,,但它被廣泛應用于實踐中,。通常在數據科學和機器學習方法內部應用梯度下降來優(yōu)化模型參數。例如,,神經網絡用梯度下降法尋找權重和偏差,。
損失函數:優(yōu)化的目標 損失函數,或成本函數,,是指通過改變決策變量來實現最小化(或最大化)的函數,。 通常被許多機器學習方法在其內部用于解決優(yōu)化問題。它們一般通過調整模型參數(如神經網絡的權重和偏差,,隨機森林或梯度提升的決策規(guī)則,,等等),,使實際輸出和預測輸出之間的差異最小,。
在回歸問題中,通常有輸入變量的向量 和實際輸出 ,。我們想找到一個模型,,將 映射到預測響應 ,使 盡可能地接近于 ,。例如,,我們可能想預測一個輸出,如一個人的工資,,給定的輸入是這個人在公司的年數或教育水平,。
在一個分類問題中,輸出 是分類的,通常是0或1,。例如,,我們試圖預測一封電子郵件是否是垃圾郵件。在二進制輸出的情況下,,最小化交叉熵函數是很方便的,,它也取決于實際輸出 和相應的預測 。
在經常用于解決分類問題的邏輯回歸中,,函數 和 定義如下,。
同樣,我們需要找到權重 ,,但這次它們應該使交叉熵函數最小,。
函數的梯度:微積分 在微積分中,一個函數的導數表示在修改其參數(或參數)時,,一個值的變化程度,。導數對優(yōu)化很重要,尤其零導數可能表示一個最小,、最大或鞍點,。
幾個獨立變量的函數 的梯度 用 來表示,定義為 相對于每個獨立變量的偏導數的向量函數: ,,符號 被稱為nabla
一個函數 在某一點上的非零值,,定義了 的最快增長方向和速度。當使用梯度下降時,,我們對成本函數的最快下降 方向感興趣,。這個方向是由負梯度,即 決定的,。
梯度下降背后的思想 為了理解梯度下降算法,,想象一滴水從碗邊滑下,或者一個球從山上滾下,。水滴和球傾向于沿著最快的下降方向移動,,直到它們到達底部。隨著時間的推移,,它們會因收到引力獲得加速度而加速下落,。
梯度下降的思想是類似的:從一個任意選擇的點或向量 的位置開始,并向成本函數下降最快的方向反復移動,。如前所述,,這是負梯度向量的方向,即 ,。
一旦我們有了一個隨機的起點 ,,就更新 它,,或者把它移到負梯度方向的新位置: ,其中 (發(fā)音為 'ee-tah'
)是一個小正數值,,稱為學習率 ,。
學習率決定了更新或移動的步驟有多大。這是一個非常重要的參數,。如果 太小,,那么算法可能收斂得很慢。大的 值也會導致收斂問題或使算法發(fā)散,。
基本梯度下降的實現 現在我們知道了基本的梯度下降是如何工作的,,我們可以用Python實現它。我們將只使用數據科學計算包 NumPy 來實現,,該方法能夠幫助我們在僅使用幾行代碼就能處理數組(或向量),,且性能也非常棒。
這是一個梯度下降算法的基本實現:它從一個任意的點開始,,向最小值迭代移動,,并返回一個有望達到或接近最小值的點 。
def gradient_descent (gradient, start, learn_rate, n_iter) : vector = start for _ in range(n_iter): diff = -learn_rate * gradient(vector) vector += diff return vector
gradient_descent()
需要四個參數,。
gradient 是一個函數或任何 Python 可調用對象,,它接收一個向量并返回最小化目標的函數的梯度。start 是算法開始搜索的點,,以序列(元組,、列表、NumPy數組等)或標量(在一維問題中)的形式給出,。learn_rate 是控制向量更新幅度的學習率,。這個函數所做的正是上面所描述的:它需要一個起點(第2行),,根據學習率和梯度值迭代更新(第3至5行),,最后返回最后發(fā)現的位置。
在我們應用gradient_descent()
之前,,我們可以添加另一個終止標準,。
import numpy as npdef gradient_descent ( gradient, start, learn_rate, n_iter=50 , tolerance=1e-06 ) : vector = start for _ in range(n_iter): diff = -learn_rate * gradient(vector) if np.all(np.abs(diff) <= tolerance): break vector += diff return vector
我們現在有了額外的參數tolerance
(第4行),它指定了每個迭代中允許的最小移動量,。我們還定義了 tolerance
和 n_iter
的默認值,,所以我們不必在每次調用gradient_descent()
時指定它們,。
第9行和第10行使gradient_descent()
在達到n_iter
之前停止迭代并返回結果,;如果當前迭代中的向量更新小于或等于容忍度也同樣停止迭代并返回結果,這經常發(fā)生在最小值附近,,那里的梯度通常非常小,。有時它也可能發(fā)生在局部最小值或鞍點附近,,此時就容易陷入局部最小值。
第9行使用方便的NumPy函數numpy.all() [1] 和numpy.abs() [2] ,,在一條語句中比較diff和tolerance
的絕對值,。
現在有了gradient_descent()
的第一個版本,下面測試該函數:從一個小例子開始,,找到函數 的最小值 ,。
這個函數只有一個自變量( ),其梯度是導數 ,。它是一個可微調的凸函數 [3] ,,用分析方法找到它的最小值是很直接的。然而在實踐中,,很難分析函數的微分,,甚至是不可能的,一般使用? 數值方法 [4] 來近似,。
只需要一條語句來測試梯度下降,。
>>> gradient_descent(... gradient=lambda v: 2 * v, start=10.0 , learn_rate=0.2 ... )2.210739197207331e-06
使用 lambda
函數 lambda v: 2 * v
來提供 的梯度。設置初始值為10.0,,學習率設為0.2,。得到的結果非常接近于零,可以認為這就是正確的最小值,。
下圖顯示了解決方案在迭代中的運動軌跡? ,。
我們從最右邊的綠點( )開始,向最小值( )移動,。開始時,,梯度(和斜率)的值較高,更新量較大,。當接近最小值時,,梯度? 變得更低,接近于0,。
學習率的影響 學習率是算法的一個非常重要的參數,。不同的學習率值會顯著影響梯度下降的行為。繼續(xù)沿用前面的例子? ,,設置學習率不是0.2,,而是0.8。
>>> gradient_descent(... gradient=lambda v: 2 * v, start=10.0 , learn_rate=0.2 ... )2.210739197207331e-06
結果會得到另一個非常接近零的值,,但在算法的內部行為表現是不同的,。如下圖所示,是 的值在迭代中發(fā)生的情況,。
在這種情況下,,同樣初始值設置為 ,,但由于學習率很高,我們得到的 的變化很大,,傳遞到最佳狀態(tài)的另一邊,,變成了-6。它在接近零點之前又過了幾次,。
小的學習率會導致非常緩慢的收斂,。如果迭代次數有限,那么算法可能會在找到最小值之前返回,。否則,,整個過程可能需要大量的時間。為了說明這一點,,我們設置一個小得多的學習率0.005,,再次運行gradient_descent()
。
>>> gradient_descent(... gradient=lambda v: 2 * v, start=10.0 , learn_rate=0.005 ... )6.050060671375367
現在的結果是6.05,,遠遠沒有達到真正的最小值0,。由于小的學習率,向量的變化非常小,。
搜索過程和以前一樣從 開始,,但在五十次迭代中不可能達到零。然而,,一百次迭代后,,誤差會小得多,一千次迭代后就會非常接近于零,。
>>> gradient_descent(... gradient=lambda v: 2 * v, start=10.0 , learn_rate=0.005 ,... n_iter=100 ... )3.660323412732294 >>> gradient_descent(... gradient=lambda v: 2 * v, start=10.0 , learn_rate=0.005 ,... n_iter=1000 ... )0.0004317124741065828 >>> gradient_descent(... gradient=lambda v: 2 * v, start=10.0 , learn_rate=0.005 ,... n_iter=2000 ... )9.952518849647663e-05
非凸函數可能存在局部最小值或鞍點,,算法可能會陷入局部最小值而被困住。這種情況下,,我們對學習率或起點的選擇可以使我們找到局部最小值和找到全局最小值之間的區(qū)別,。
考慮函數 。它的全局最小點在 ,,局部最小點在 ,。這個函數的梯度是 。讓我們看看gradient_descent()
在這里如何工作,。
>>> gradient_descent(... gradient=lambda v: 4 * v**3 - 10 * v - 3 , start=0 ,... learn_rate=0.2 ... )-1.4207567437458342
這次我們從零開始,,算法在局部最小值附近結束。下面是在引擎蓋下發(fā)生的事情,。
在前兩次迭代中,,我們的向量是向全局最小值移動的,但后來它越過了對面,,一直被困在局部最小值中,。我們可以用一個較小的學習率來防止這種情況,。
>>> gradient_descent(... gradient=lambda v: 4 * v**3 - 10 * v - 3 , start=0 ,... learn_rate=0.1 ... )1.285401330315467
當我們把學習率從0.2降到0.1時,,得到一個非常接近全局最小值的解決方案,。這一次,避免了跳到另一邊的情況,。其實,,梯度下降是一種近似的方法。
較低的學習率可以防止向量出現大的跳躍,,在這種情況下,,向量仍然比較接近全局最優(yōu)。
調整學習率是很棘手的,,我們不可能事先知道最佳值,。有許多技術和啟發(fā)式方法試圖幫助解決這個問題。機器學習工程師們經常在模型選擇和評估期間調整學習率,。
除了學習率,,初始值通常也會對解決方案產生很大影響,特別是對于非凸函數,。
梯度下降算法的應用 在本節(jié)中,,一起學習兩個使用梯度下降的例子。我們還會了解到它可以用于現實生活中的機器學習問題,,如線性回歸,。在第二個例子中,我們需要修改 gradient_descent()
的代碼,,因為我們需要觀察到的數據來計算梯度,。
例一 首先,我們要將 gradient_descent()
應用于另一個一維問題,。以函數 為例,。這個函數的梯度是 。根據這些信息,,我們可以找到它的最小值,。
>>> gradient_descent(... gradient=lambda v: 1 - 1 / v, start=2.5 , learn_rate=0.5 ... )1.0000011077232125
通過傳入合適的參數集,gradient_descent()
正確地計算出該函數的最小值為 ,。我們可以用其他的學習率和起點的值來嘗試,。
我們也可以對多個變量的函數使用 gradient_descent()
。應用是一樣的,,但是我們需要提供梯度和起始點作為向量或數組,。例如,我們可以找到具有梯度向量( ) 的函數 的最小值,。
>>> gradient_descent(... gradient=lambda v: np.array([2 * v[0 ], 4 * v[1 ]**3 ]),... start=np.array([1.0 , 1.0 ]), learn_rate=0.2 , tolerance=1e-08 ... ) array([8.08281277e-12 , 9.75207120e-02 ])
在這種情況下,,我們的梯度函數的起始值是一個數組,,也返回一個數組結果。該結果值幾乎等于零,,所以我們可以說gradient_descent()
正確地發(fā)現這個函數的最小值是在 ,。
普通最小二乘法 相信大家都學習過線性回歸和普通最小二乘法 [5] ,從輸入 到輸出 ,定義了一個線性函數 ,,使它盡可能地接近于 ,。
這其實就是一個優(yōu)化問題,要找到權重值 ,,使殘差平方值之和 或平均平方誤差 最小,。這里 是觀測值的總數,
其實我們也可以使用成本函數 ,,這在數學上比SSR或MSE更方便,。
線性回歸的最基本形式是簡單線性回歸。它只有一組輸入 和兩個權重 和 ,,回歸線的方程是 ,。盡管 和 的最佳值可以用分析法計算 [6] ,但我們這里使用梯度下降法來確定結果,。
首先,,需要通過微積分來找到成本函數的梯度 。由于有兩個決策變量 和 ,,所以梯度 是一個有兩個分量的向量,。
1.
2.
我們需要 和 的值來計算這個成本函數的梯度,梯度函數不僅有 和 ,,還有 和 作為輸入,。
def ssr_gradient (x, y, b) : res = b[0 ] + b[1 ] * x - y return res.mean(), (res * x).mean() # .mean() is a method of np.ndarray
ssr_gradient()
接收包含觀測輸入和輸出的數組 和 ,以及保存決策變量 和 當前值的數組 ,。該函數首先計算每個觀察值的殘差數組(res),,然后返回 和 的一對值。
這里使用NumPy方法ndarray.mean()
對傳遞的NumPy數組參數求均值,。
gradient_descent()
需要兩個小調整:
1. 在第4行添加 和 作為gradient_descent()
的參數,。 2. 向梯度函數提供 和 ,并確保我們在第8行將梯度元組轉換為NumPy數組,。
以下是gradient_descent()
在這些變化后的樣子,。
import numpy as npdef gradient_descent ( gradient, x, y, start, learn_rate=0.1 , n_iter=50 , tolerance=1e-06 ) : vector = start for _ in range(n_iter): diff = -learn_rate * np.array(gradient(x, y, vector)) if np.all(np.abs(diff) <= tolerance): break vector += diff return vector
gradient_descent()
現在接受輸入 和輸出 ,并計算梯度。將gradient(x, y, vector)
的輸出轉換為NumPy數組使梯度元素與學習率相乘,,這對于單變量函數可以參略,。
現在得到新的 gradient_descent()
來尋找一些任意的x
和y
的值的回歸線。
>>> x = np.array([5 , 15 , 25 , 35 , 45 , 55 ])>>> y = np.array([5 , 20 , 14 , 32 , 22 , 38 ])>>> gradient_descent(... ssr_gradient, x, y, start=[0.5 , 0.5 ], learn_rate=0.0008 ,... n_iter=100 _000... ) array([5.62822349 , 0.54012867 ])
結果是一個有兩個數值的數組,,對應于決策變量: 和 ,。最佳回歸線是 。和前面的例子一樣,,這個結果在很大程度上取決于學習率,。如果學習率太低或太高,,可能不會得到這么好的結果,。
雖然這個例子并不完全是隨機的,但我們得到的結果與scikit-learn的線性回歸器幾乎一樣,。我們可視化該回歸線的數據和回歸結果,。
代碼改進 我們可以在不修改其核心功能的情況下,使 gradient_descent()
更強大,、更全面,、更美觀。
import numpy as npdef gradient_descent ( gradient, x, y, start, learn_rate=0.1 , n_iter=50 , tolerance=1e-06 , dtype='float64' ) : # 檢查梯度是否可調用 if not callable(gradient): raise TypeError(''gradient' must be callable' ) # 設置NumPy數組的數據類型 dtype_ = np.dtype(dtype) # 將x和y轉換為NumPy數組 x, y = np.array(x, dtype=dtype_), np.array(y, dtype=dtype_) if x.shape[0 ] != y.shape[0 ]: raise ValueError(''x' and 'y' lengths do not match' ) # 設置變量初始值 vector = np.array(start, dtype=dtype_) # 設置并檢查學習率 learn_rate = np.array(learn_rate, dtype=dtype_) if np.any(learn_rate <= 0 ): raise ValueError(''learn_rate' must be greater than zero' ) # 設置并檢查最大的迭代次數 n_iter = int(n_iter) if n_iter <= 0 : raise ValueError(''n_iter' must be greater than zero' ) # 設置并檢查公差 tolerance = np.array(tolerance, dtype=dtype_) if np.any(tolerance <= 0 ): raise ValueError(''tolerance' must be greater than zero' ) # 執(zhí)行梯度下降循環(huán) for _ in range(n_iter): # 重新計算差異 diff = -learn_rate * np.array(gradient(x, y, vector), dtype_) # 檢查絕對差異是否足夠小 if np.all(np.abs(diff) <= tolerance): break # 更新變量的值 vector += diff return vector if vector.shape else vector.item()
gradient_descent()
現在接受一個額外的dtype
參數,,用來定義函數內部NumPy數組的數據類型,。
在大多數應用中,我們不會注意到32位和64位浮點數之間的區(qū)別,,但當我們處理大數據集時,,這可能會大大影響內存的使用,甚至可能影響處理速度,。例如,,盡管NumPy默認使用64位浮點數,但TensorFlow經常使用32位浮點數,。
除了考慮數據類型外,,上面的代碼還引入了一些與類型檢查和確保使用NumPy能力有關的修改。
首先檢查gradient是否是一個Python可調用的對象,,以及它是否可以作為一個函數使用,。如果不是,那么這個函數將引發(fā)一個TypeError,。 設置numpy.dtype [7] 的一個實例,,用作整個函數中所有數組的數據類型。 接收參數 和 并產生具有所需數據類型的NumPy數組,。參數x和y可以是列表,、元組、數組或其他序列。 然后比較 和 的大小,,以此確定兩個數組有相同數量的觀察值,。如果它們不一樣,那么該函數將引發(fā)一個ValueError,。 將參數 start
轉換為NumPy數組,。這是一個有趣的技巧:如果 start
是一個Python標量,那么它將被轉換為一個相應的NumPy對象,。如果傳遞一個序列,,那么它將變成一個具有相同元素數的普通NumPy數組。 對學習率做同樣的事情,。這使我們能夠通過向 gradient_descent()
傳遞一個列表,、元組或NumPy數組,為每個決策變量指定不同的學習率,。 同樣設置n_iter
和tolerance
并檢查它們是否大于0。 從for 循環(huán)開始,,與之前的內容幾乎相同,。唯一的區(qū)別是計算差異時的梯度數組的類型。 如果我們有幾個決策變量,,最后會方便地返回結果數組,;如果我們有一個單一的變量,則返回一個Python標量,。 此時,,我們的 gradient_descent()
現在已經完成了。接下來我們學習隨機梯度下降算法的具體實現方法,。
隨機梯度下降算法 隨機梯度下降算法 是對梯度下降的一種修改,。在隨機梯度下降中,我們只用隨機的一小部分觀測值而不是所有的觀測值來計算梯度,。在某些情況下,,這種方法可以減少計算時間。
Online
隨機梯度下降 是隨機梯度下降的一個變種,,它對每個觀察的成本函數的梯度進行估計,,并相應地更新決策變量。這有助于找到全局最小值,,特別是當目標函數是凸的時候,。
Batch
隨機梯度下降法 介于普通梯度下降法和Online方法之間。梯度的計算和決策變量的更新是通過所有觀測數據的子集進行的,,稱為**minibatches
** 小批量,。這種變體在訓練神經網絡方面非常流行。
我們可以把Online
算法想象成一種特殊的批處理算法,其中每個 minibatch
只有一個觀測值,。經典梯度下降是另一種特殊情況,,其中只有一個批次包含所有觀測值。
隨機梯度下降中的最小batch 與普通梯度下降法一樣,,隨機梯度下降法從決策變量的初始向量開始,,通過幾次迭代進行更新。兩者的區(qū)別在于迭代過程中發(fā)生了什么,。
隨機梯度下降法將觀測數據集隨機劃分為 minibatches
,。 對于每一個 minibatch
,計算梯度,,移動向量,。 一旦用完所有的 minibatch
,我們就說這個迭代(epoch ),,已經結束,,并開始下一個迭代,。 這個算法會隨機選擇 minibatch
的觀察值,,我們可以用隨機數生成器來模擬這種隨機(或偽隨機)行為。Python有內置的ramdom模塊,,而NumPy有自己的隨機生成器,。當使用數組時,后者更方便,。
創(chuàng)建一個名為sgd()
的新函數,,它與gradient_descent()
非常相似,但使用隨機選擇的 minibatches
來沿著搜索空間移動,。
import numpy as npdef sgd ( gradient, x, y, start, learn_rate=0.1 , batch_size=1 , n_iter=50 , tolerance=1e-06 , dtype='float64' , random_state=None ) : # 檢查梯度是否可調用 if not callable(gradient): raise TypeError(''gradient' must be callable' ) # 設置NumPy數組的數據類型 dtype_ = np.dtype(dtype) # 將x和y轉換為NumPy數組 x, y = np.array(x, dtype=dtype_), np.array(y, dtype=dtype_) n_obs = x.shape[0 ] if n_obs != y.shape[0 ]: raise ValueError(''x' and 'y' lengths do not match' ) xy = np.c_[x.reshape(n_obs, -1 ), y.reshape(n_obs, 1 )] # 初始化隨機數發(fā)生器 seed = None if random_state is None else int(random_state) rng = np.random.default_rng(seed=seed) # 初始化變量的值 vector = np.array(start, dtype=dtype_) # 設置并檢查學習率 learn_rate = np.array(learn_rate, dtype=dtype_) if np.any(learn_rate <= 0 ): raise ValueError(''learn_rate' must be greater than zero' ) # 設置并檢查迷我們批的大小 batch_size = int(batch_size) if not 0 < batch_size <= n_obs: raise ValueError( ''batch_size' must be greater than zero and less than ' 'or equal to the number of observations' ) # 設置并檢查最大的迭代次數 n_iter = int(n_iter) if n_iter <= 0 : raise ValueError(''n_iter' must be greater than zero' ) # 設置并檢查公差 tolerance = np.array(tolerance, dtype=dtype_) if np.any(tolerance <= 0 ): raise ValueError(''tolerance' must be greater than zero' ) # 執(zhí)行梯度下降循環(huán) for _ in range(n_iter): # 打亂 x 和 y rng.shuffle(xy) # 執(zhí)行小批量移動 for start in range(0 , n_obs, batch_size): stop = start + batch_size x_batch, y_batch = xy[start:stop, :-1 ], xy[start:stop, -1 :] # 重新計算差異 grad = np.array(gradient(x_batch, y_batch, vector), dtype_) diff = -learn_rate * grad # 檢查絕對差異是否足夠小 if np.all(np.abs(diff) <= tolerance): break # 更新變量的值 vector += diff return vector if vector.shape else vector.item()
我們在這里有一個新的參數 batch_size
,,可以指定每個minibatch
中的觀察值的數量。這是隨機梯度下降法的一個重要參數,,可以顯著影響性能,,并確保batch_size
是一個正整數,且不大于觀察值的總數,。
另一個新參數是 random_state
,。它定義了隨機數發(fā)生器的種子。該種子在后面被用作 default_rng()
的參數,,它創(chuàng)建了一個Generator的實例,。
如果為 random_state
傳遞參數 None
,那么隨機數生成器每次實例化時都會返回不同的數字,。如果我們想讓生成器的每個實例表現得完全一樣,,那么需要指定隨機數種子——最簡單的方法是提供一個任意的整數。
用x.shape[0]
推斷出觀察值的數量。如果 是一個一維數組,,那么這就是它的大小,。如果 有兩個維度,那么.shape[0]
是行的數量,。
使用.reshape()
來確保 和 都成為具有n_obs
行的二維數組,,并且 正好有一列。numpy.c_[]
方便地將 和 的列連接成一個數組 ,。這是使數據適合隨機選擇的一種方法,。
最后,實現了隨機梯度下降的for循環(huán),,它與gradient_descent()
不同,。我們使用隨機數生成器和它的方法.shuffle()
來清洗觀察值。這是隨機選擇minibatches的方法之一,。
內層for循環(huán)對每個minibatch都是重復執(zhí)行的,。與普通梯度下降法的主要區(qū)別,是梯度針對minibatch的觀測值(x_batch和y_batch)
而不是針對所有觀測值( 和 )計算的,。
x_batch
成為xy
的一部分,,包含了當前minibatch
的行(從開始到結束)和對應于 的列。
現在我測試隨機梯度下降的實現,。
>>> sgd(... ssr_gradient, x, y, start=[0.5 , 0.5 ], learn_rate=0.0008 ,... batch_size=3 , n_iter=100 _000, random_state=0 ... ) array([5.63093736 , 0.53982921 ])
其結果與用gradient_descent()
得到的幾乎一樣,。如果省略 random_state
或者使用 None
,那么我們每次運行sgd()
都會得到一些不同的結果,,因為隨機數生成器會對xy
進行不同的打亂,。
隨機梯度下降法中的動量 我們知道,學習率會對梯度下降的結果產生重大影響,。我們可以使用幾種不同的策略來適應算法執(zhí)行過程中的學習率,,也可以在算法中應用動量 (Momentum
)。
可以使用動量來糾正學習率的影響,,這個想法是記住矢量的前一次更新,,并在計算下一次更新時應用它。不會完全沿著負梯度的方向移動向量,,但也傾向于保持之前移動的方向和幅度,。
被稱為衰變率 或衰變因子 的參數定義了先前更新的貢獻有多大。為了包括動量和衰減率,,我們可以修改sgd()
,,增加參數decay_rate
,用它來計算矢量更新的方向和幅度(diff
),。
import numpy as npdef sgd ( gradient, x, y, start, learn_rate=0.1 , decay_rate=0.0 , batch_size=1 , n_iter=50 , tolerance=1e-06 , dtype='float64' , random_state=None ) : # 檢查梯度是否可調用 if not callable(gradient): raise TypeError(''gradient' must be callable' ) # 設置NumPy數組的數據類型 dtype_ = np.dtype(dtype) # 將x和y轉換為NumPy數組 x, y = np.array(x, dtype=dtype_), np.array(y, dtype=dtype_) n_obs = x.shape[0 ] if n_obs != y.shape[0 ]: raise ValueError(''x' and 'y' lengths do not match' ) xy = np.c_[x.reshape(n_obs, -1 ), y.reshape(n_obs, 1 )] # 初始化隨機數發(fā)生器 seed = None if random_state is None else int(random_state) rng = np.random.default_rng(seed=seed) # 初始化變量的值 vector = np.array(start, dtype=dtype_) # 設置并檢查學習率 learn_rate = np.array(learn_rate, dtype=dtype_) if np.any(learn_rate <= 0 ): raise ValueError(''learn_rate' must be greater than zero' ) # update # 設置和檢查衰減率 decay_rate = np.array(decay_rate, dtype=dtype_) if np.any(decay_rate < 0 ) or np.any(decay_rate > 1 ): raise ValueError(''decay_rate' must be between zero and one' ) # 設置并檢查迷我們批的大小 batch_size = int(batch_size) if not 0 < batch_size <= n_obs: raise ValueError( ''batch_size' must be greater than zero and less than ' 'or equal to the number of observations' ) # 設置并檢查最大的迭代次數 n_iter = int(n_iter) if n_iter <= 0 : raise ValueError(''n_iter' must be greater than zero' ) # 設置并檢查公差 tolerance = np.array(tolerance, dtype=dtype_) if np.any(tolerance <= 0 ): raise ValueError(''tolerance' must be greater than zero' ) # 將第一次迭代的差值設置為零 diff = 0 # 執(zhí)行梯度下降循環(huán) for _ in range(n_iter): # 打亂 x 和 y rng.shuffle(xy) # 執(zhí)行小批量移動 for start in range(0 , n_obs, batch_size): stop = start + batch_size x_batch, y_batch = xy[start:stop, :-1 ], xy[start:stop, -1 :] # 重新計算差異 grad = np.array(gradient(x_batch, y_batch, vector), dtype_) # diff = -learn_rate * grad diff = decay_rate * diff - learn_rate * grad # 檢查絕對差異是否足夠小 if np.all(np.abs(diff) <= tolerance): break # 更新變量的值 vector += diff return vector if vector.shape else vector.item()
這里,,在參數行添加了decay_rate
參數,,在第34行將其轉換成所需類型的NumPy數組,并在第35和36行檢查其是否在0和1之間,。在第57行,,我們在迭代開始前初始化diff
,以確保它在第一次迭代時可用,。
最重要的變化發(fā)生在第71行,,用學習率和梯度重新計算diff
,但也加入了decay_rate
和diff
的舊值的乘積?,F在diff
有兩個組成部分,。
decay_rate * diff
是指momentum,或者說是前一個動作的影響,。**-learn_rate * grad
**是當前梯度的影響,。 衰減率和學習率作為權重,定義了兩者的貢獻度,。
隨機啟動值 相對于普通梯度下降,,初始值對于隨機梯度下降來說往往不是那么重要。并且,,如果每次都需要手動設置,,這其實是沒有必要的,想象一下,,如果我們需要手動初始化一個有數千個偏置和權重的神經網絡的值,,太麻煩了!
在實踐中,我們可以使用隨機數發(fā)生器來獲得它們,,并從一些小的任意值開始。
我們更新函數 sgd()
,。
def sgd ( gradient, x, y, n_vars=None, start=None, learn_rate=0.1 , decay_rate=0.0 , batch_size=1 , n_iter=50 , tolerance=1e-06 , dtype='float64' , random_state=None ) : # 初始化變量值 vector = ( rng.normal(size=int(n_vars)).astype(dtype_) if start is None else np.array(start, dtype=dtype_) ) # 其余不變
這里添加了新的參數 n_vars
,,定義了問題中決策變量的數量。參數start是可選的,,其默認值為None,,并且初始化了決策變量的起始值。
如果提供了一個除無以外的起始值,,那么它將被用于起始值,。 如果設置start
是None
,那么我們的隨機數生成器就會使用標準正態(tài)分布和NumPy.normal() [8] 方法創(chuàng)建起始值,。 試試sgd()
:
>>> sgd(... ssr_gradient, x, y, n_vars=2 , learn_rate=0.0001 ,... decay_rate=0.8 , batch_size=3 , n_iter=100 _000, random_state=0 ... ) array([5.63014443 , 0.53901017 ])
又得到了類似的結果,。
現在我們已經學會了如何編寫實現梯度下降和隨機梯度下降的函數。上面的代碼可以變得更加健壯和精煉,。另外,,我們還可以在一些著名的機器學習庫中找到這些方法的不同實現,。
總結 我們現在知道什么是梯度下降 和隨機梯度下降 算法,以及它們如何工作,,它們在人工神經網絡的應用中被廣泛使用,。
現在我們已經學會了:
梯度下降的主要特點和概念 是什么,如學習率或動力,,以及它的局限性 我們已經使用梯度下降和隨機梯度下降來尋找?guī)讉€函數的最小值,,并在一個線性回歸問題中擬合回歸線。
參考資料 [1] numpy.all(): https:///doc/stable/reference/generated/numpy.all.html
[2] numpy.abs(): https:///doc/stable/reference/generated/numpy.absolute.html
[3] 凸函數: https://en./wiki/Convex_function
[4] 數值方法: https://en./wiki/Numerical_method
[5] 普通最小二乘法: https://en./wiki/Ordinary_least_squares
[6] 分析法計算: https://en./wiki/Ordinary_least_squares#Simple_linear_regression_model
[7] numpy.dtype: https:///doc/stable/reference/generated/numpy.dtype.html
[8] NumPy.normal(): https:///doc/stable/reference/random/generated/numpy.random.Generator.normal.html