機器學習:從零開始學習梯度下降

作者:SETHNEHA 翻譯:王可汗 校對:陳丹



梯度下降是一個需要理解的重要算法,因爲它是機器學習和深度學習中使用的許多更先進算法的基礎。因此,掌握梯度下降的內部工作原理對任何計劃進一步探索機器學習算法的人來說都是非常有益的。

最好的學習方法是實踐,因此在本文中,我將一步步介紹梯度下降過程是如何工作的 ,而不使用像scikit-learn這樣的ML庫。在日常工作中,使用這些庫當然會更快、更簡潔,但在學習過程中,我發現手工實現對這個特定算法來說是非常寶貴的。

梯度下降的目標

梯度下降的目標是最小化模型預測與原始數據間的誤差。在本文的背景下,我們將着眼於二次多項式模型,也稱爲二次方程:


將二階多項式畫出來後, 看起來是這樣的:



多項式迴歸

我們這裏專門觀察多項式迴歸, 即自變量x與因變量y之間的關係被建模爲x的n階多項式。簡單地說,我們的二次多項式的係數a、b、c將被估計、評價和修改,直到我們可以將線準確地擬合到輸入的x數據上。梯度下降是這個過程中的優化步驟,它修改和優化這些係數的值。
 
現在,我們將看看如何創建和繪製這樣的曲線,並建立一個初始模型來擬合這個數據,然後我們將使用梯度下降來優化和改進它。如果我們能得到一個可以準確描述數據的模型,希望它能夠準確預測另一組x值的y值。
 
我們可以開始爲二次多項式方程(𝑎𝑥²+𝑏𝑥+𝑐)選擇係數, 並應用到我們將嘗試建模的數據:

coeffs = [2,-5, 4]

這將是我們希望我們的預測模型儘可能接近的真值模型的係數 [5]  。接下來,我們需要一個二次多項式的評估函數,在給定一組係數和給定輸入𝑥的情況下,返回相應的𝑦。

def eval_2nd_degree(coeffs, x):    """    Function to return the outputof evaluating a second degree polynomial,    given a specific x value.
Args: coeffs: List containingthe coefficients a,b, and c for the polynomial. x: The input x value tothe polynomial.
Returns: y: The correspondingoutput y value for the second degree polynomial.
""" a = (coeffs[0]*(x*x)) b = coeffs[1]*x c = coeffs[2] y = a+b+c return y

當x=3時,我們可以看看它的作用:

coeffs = [2, -5, 4]x=3eval_2nd_degree(coeffs, x)
7

創建數據和基礎模型

定義一些x數據(輸入),我們希望預測y(輸出):

import numpy as npimport matplotlib.pyplot as plt
hundred_xs=np.random.uniform(-10,10,100)print(hundred_xs)
x_y_pairs = []for x in hundred_xs: y =eval_2nd_degree(coeffs, x) x_y_pairs.append((x,y))
xs = []ys = []for a,b in x_y_pairs: xs.append(a) ys.append(b)
plt.figure(figsize=(20,10))plt.plot(xs, ys, 'g+')plt.title('Original data')plt.show()


這很好,但是我們可以通過讓事情變得更真實來改進它。你可以添加噪音或“抖動”的值,使他們可以類似於現實世界的數據:

defeval_2nd_degree_jitter(coeffs, x, j):    """    Function to return the noisy output ofevaluating a second degree polynomial,    given a specific x value. Output values canbe within [y−j,y+j].
Args: coeffs: List containing thecoefficients a,b, and c for the polynomial. x: The input x value to the polynomial. j: Jitter parameter, to introduce noiseto output y.
Returns: y: The corresponding jittered output yvalue for the second degree polynomial.
""" a = (coeffs[0]*(x*x)) b = coeffs[1]*x c = coeffs[2] y = a+b+c print(y)
interval = [y-j, y+j] interval_min = interval[0] interval_max = interval[1] print(f"Should get value in the range{interval_min} - {interval_max}") jit_val = random.random() *interval_max # Generate a randomnumber in range 0 to interval max
while interval_min > jit_val: # While the random jittervalue is less than the interval min, jit_val = random.random() *interval_max # it is not in the rightrange. Re-roll the generator until it # give a number greater than the interval min.
return jit_val

測試一下:

7Should get value in the range 3 - 116.233537936801398

這個更新的函數將接受二階多項式的輸入和爲這個輸入添加噪聲的抖動值,以給我們一個更現實的輸出,而不僅僅是一個完美的曲線:

x_y_pairs = []for x in hundred_xs:    y  =eval_2nd_degree_jitter(coeffs, x, j)    x_y_pairs.append((x,y))
xs = []ys = []for a,b in x_y_pairs: xs.append(a) ys.append(b)
plt.figure(figsize=(20,10))plt.plot(xs, ys, 'g+')plt.title('Original data')plt.show()


當我們建立我們的預測模型,並通過梯度下降來優化它時,我們希望能夠得到儘可能接近這些值的結果。
 
建模第一步:嘗試一個隨機模型

建模的第一步是爲二次多項式(𝑦=𝑎𝑥²+𝑏𝑥+𝑐)生成和存儲隨機係數。這將是我們的初始模型,它很可能不那麼精確,我們的目標是改進它,直到它與數據足夠吻合。

rand_coeffs=(random.randrange(-10,10),random.randrange(-10,10),random.randrange(-10,10))rand_coeffs
(7, 6, 3)

通過計算輸入值的預測輸出值來檢查這個模型的準確性:

y_bar =eval_2nd_degree(rand_coeffs, hundred_xs)
plt.figure(figsize=(20,10))plt.plot(xs, ys, 'g+', label ='original')plt.plot(xs, y_bar, 'ro',label='prediction')plt.title('Original data vsfirst prediction')plt.legend(loc="lowerright")plt.show()


從上面的圖中可以明顯看出,這個帶有隨機係數的新模型並不完全符合我們的數據。爲了定量描述這個模型有多不正確,我們計算了模型的均方誤差。這是實際輸出和預測輸出之差 的平方和的平均值:


def loss_mse(ys, y_bar):    """    Calculates MSE loss.
Args: ys: training data labels y_bar: prediction labels
Returns: Calculated MSE loss. """ return sum((ys - y_bar)*(ys - y_bar)) /len(ys)
initial_model_loss = loss_mse(ys, y_bar)
initial_model_loss
47922.39790821987

相當大的數。現在讓我們看看我們能否通過梯度下降優化模型來改善這個相當高的損失值。
 
梯度下降和損失減少

我們希望改進我們的模型。因此,我們想要改變它的係數a, b和c,以減少誤差。因此我們需要知道每個係數是如何影響誤差的。這是通過計算損失函數對每個單獨係數的偏導數來實現的。

在這個案例中,我們使用MSE作爲我們的損失函數——這即是我們希望計算偏導數的函數:

 
我們模型的輸出預測如下:


因此,損失可以重新表述爲:

 
在這個特定的例子中,我們損失函數的偏導數如下:


  • 如果你計算每個導數的值,你會得到每個係數的梯度。
  • 這些值給出了損失函數相對於每個特定係數的斜率。
  • 它們表明你應該增加還是減少它來減少損失,以及這樣做的安全程度。
 
給定係數𝑎,𝑏和𝑐,計算的梯度𝑔𝑎,𝑔𝑏和𝑔𝑐和學習率𝑙𝑟,通常會更新系數,更新的值定義如下:


一旦您將新模型應用到數據中,您的損失應該會減少。
 
減少損失

我們需要一個梯度計算函數,在給定一個二次多項式的係數,以及一組輸入𝑥和一組相應的實際輸出𝑦,將返回每個係數的梯度。

defcalc_gradient_2nd_poly(rand_coeffs, hundred_xs, ys):    """    calculates the gradient for a second degreepolynomial.
Args: coeffs: a,b and c, for a 2nd degreepolynomial [ y = ax^2 + bx + c ] inputs_x: x input datapoints outputs_y: actual y output points
Returns: Calculated gradients for the 2nddegree polynomial, as a tuple of its parts for a,b,c respectively.
"""
a_s = [] b_s = [] c_s = []

y_bars = eval_2nd_degree(rand_coeffs,hundred_xs)
for x,y,y_bar in list(zip(hundred_xs, ys,y_bars)): # take tuple of (xdatapoint, actual y label, predicted y label) x_squared = x**2 partial_a = x_squared * (y - y_bar) a_s.append(partial_a) partial_b = x * (y-y_bar) b_s.append(partial_b) partial_c = (y-y_bar) c_s.append(partial_c)
num = [i for i in y_bars] n = len(num)
gradient_a = (-2 / n) * sum(a_s) gradient_b = (-2 / n) * sum(b_s) gradient_c = (-2 / n) * sum(c_s) return(gradient_a, gradient_b,gradient_c) # return calculatedgradients as a a tuple of its 3 parts

我們現在要:

  • 使用上面的函數來計算我們表現不佳的隨機模型的梯度。
  • 相應調整模型係數。
  • 驗證模型的損失現在更小了——梯度下降起作用了!
 
讓我們設定一個實驗的初始學習速率。這應該保持在很小的範圍內,以避免錯過全局最小值,但也不能小到要花費無窮長的 [8]  時間或陷入局部最小值。lr = 0.0001是一個很好的起點。

calc_grad= calc_gradient_2nd_poly(rand_coeffs, hundred_xs, ys)
lr =0.0001a_new= rand_coeffs[0] - lr * calc_grad[0]b_new= rand_coeffs[1] - lr * calc_grad[1]c_new= rand_coeffs[2] - lr * calc_grad[2]
new_model_coeffs= (a_new, b_new, c_new)print(f"Newmodel coeffs: {new_model_coeffs}")print("")
#updatewith these new coeffs:new_y_bar= eval_2nd_degree(new_model_coeffs, hundred_xs)updated_model_loss= loss_mse(ys, new_y_bar)
print(f"Nowhave smaller model loss: {updated_model_loss} vs {original_model_loss}")
New model coeffs: 5.290395171471687 5.903335222089396 2.9704266522693037Now have smaller model loss: 23402.14716735533 vs 47922.39790821987

通過將訓練數據、原始隨機模型和更新後的低損失模型一起繪製出來,來可視化這個改進:

plt.figure(figsize=(20,10))plt.plot(xs, ys, 'g+', label ='original model')plt.plot(xs, y_bar, 'ro', label= 'first prediction')plt.plot(xs, new_y_bar, 'b.',label = 'updated prediction')plt.title('Original model vs1st prediction vs updated prediction with lower loss')plt.legend(loc="lower right")plt.show()


多輪迭代梯度下降

我們幾乎準備好了。最後一步是在多個輪(週期或迭代)中迭代地執行梯度下降。我們希望每 一輪迭代都能看到在降低損失和更好地對原始數據進行模型擬合方面的改進。
 
讓我們對上面的calc_gradient_2nd_poly函數進行改進,使其更適用於梯度下降迭代過程:

defcalc_gradient_2nd_poly_for_GD(coeffs, inputs_x, outputs_y, lr):    """    calculates the gradient for a second degreepolynomial.
Args: coeffs: a,b and c, for a 2nd degreepolynomial [ y = ax^2 + bx + c ] inputs_x: x input datapoints outputs_y: actual y output points lr: learning rate
Returns: Calculated gradients for the 2nddegree polynomial, as a tuple of its parts for a,b,c respectively.
""" a_s = [] b_s = [] c_s = []
y_bars = eval_2nd_degree(coeffs, inputs_x)
for x,y,y_bar in list(zip(inputs_x,outputs_y, y_bars)): # take tuple of(x datapoint, actual y label, predicted y label) x_squared = x**2 partial_a = x_squared * (y - y_bar) a_s.append(partial_a) partial_b = x * (y-y_bar) b_s.append(partial_b) partial_c = (y-y_bar) c_s.append(partial_c)
num = [i for i in y_bars] n = len(num)
gradient_a = (-2 / n) * sum(a_s) gradient_b = (-2 / n) * sum(b_s) gradient_c = (-2 / n) * sum(c_s)

a_new = coeffs[0] - lr * gradient_a b_new = coeffs[1] - lr * gradient_b c_new = coeffs[2] - lr * gradient_c
new_model_coeffs = (a_new, b_new, c_new)
#update with these new coeffs: new_y_bar = eval_2nd_degree(new_model_coeffs,inputs_x)
updated_model_loss = loss_mse(outputs_y,new_y_bar) return updated_model_loss,new_model_coeffs, new_y_bar

這將作爲gradient_descent函數的一部分被調用:

def gradient_descent(epochs,lr):    """    Perform gradient descent for a seconddegree polynomial.
Args: epochs: number of iterations to performof finding new coefficients and updatingt loss. lr: specified learning rate
Returns: Tuple containing (updated_model_loss,new_model_coeffs, new_y_bar predictions, saved loss updates)
""" losses = [] rand_coeffs_to_test = rand_coeffs for i in range(epochs): loss =calc_gradient_2nd_poly_for_GD(rand_coeffs_to_test, hundred_xs, ys, lr) rand_coeffs_to_test = loss[1] losses.append(loss[0]) print(losses) return loss[0], loss[1], loss[2],losses #(updated_model_loss,new_model_coeffs, new_y_bar, saved loss updates)

最後,讓我們 訓練 1500輪,看看我們的模型是否學到了什麼:

GD = gradient_descent(1500,0.0001)
plt.figure(figsize=(20,10))plt.plot(xs, ys, 'g+', label ='original')plt.plot(xs, GD[2], 'b.', label= 'final_prediction')plt.title('Original vs Finalprediction after Gradient Descent')plt.legend(loc="lowerright")plt.show()

 
經過完整的訓練後,這個訓練過的模型顯示出了巨大的改進。我們可以通過檢查它的最終預測係數a、b和c來進一步檢驗:

print(f"FinalCoefficients predicted: {GD[1]}")print(f"OriginalCoefficients: {coeffs}")
  
  
  
Final Coefficients predicted: (2.0133237089326155, -4.9936501002139275, 3.1596042252126195)Original Coefficients: [2, -5, 4]

不太遠!這是對初始隨機模型的一大改進。通過觀察訓練時減少損失的情況,我們可以獲得更深入的見解:

plt.figure(figsize=(20,10))plt.plot(GD[3], 'b-', label ='loss')plt.title('Loss over 1500iterations')plt.legend(loc="lowerright")plt.xlabel('Iterations')plt.ylabel('MSE')plt.show()


我們觀察到爲了得到更精確的係數,模型損耗接近於零。我們還可以看到,在大約400輪之後,損失並沒有明顯的改善——絕對不需要1500輪 [11]  。另一種策略是在訓練步驟中添加某種條件,當達到某個最小損失閾值時停止訓練。這將防止過度訓練和潛在的模型過擬合。
 
我希望你喜歡這篇關於多項式迴歸的梯度下降的文章。某些概念乍一看可能會讓人望而生畏,但隨着時間的推移,如果我們堅持足夠長的時間,我們會逐漸熟悉一個問題的“具體細節”。我發現這個練習對我來說確實是這樣的,我覺得這是一個很值得學習的經驗。

原文標題:
PolynomialRegression — Gradient Descent from Scratch
原文鏈接:
https://towardsdatascience.com/polynomial-regression-gradient-descent-from-scratch-279db2936fe9?source=collection_home

 
    
    
    
也可以加一下老胡的微信
圍觀朋友圈~~~


推薦閱讀

(點擊標題可跳轉閱讀)

麻省理工學院計算機課程【中文版】
【清華大學王東老師】現代機器學習技術導論.pdf
機器學習中令你事半功倍的pipeline處理機制
機器學習避坑指南:訓練集/測試集分佈一致性檢查
機器學習深度研究:特徵選擇中幾個重要的統計學概念

老鐵,三連支持一下,好嗎?↓↓↓

本文分享自微信公衆號 - 機器學習算法與Python實戰(tjxj666)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

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