從 0 開始機器學習 - 手把手用 Python 實現梯度下降法!

機器學習課程也上了一段時間了,今天就帶大家從 0 開始手把手用 Python 實現第一個機器學習算法:單變量梯度下降(Gradient Descent)!

我們從一個小例子開始一步步學習這個經典的算法。

一、如何最快下山?

在學習算法之前先來看一個日常生活的例子:下山。想象一下你出去旅遊爬山,爬到山頂後已經傍晚了,很快太陽就會落山,所以你必須想辦法儘快下山,然後去喫海底撈。

那最快的下山方法是什麼呢?沒錯就是縮成一個球,然後從最陡的方向直接滾下去,可是我們是人不是球,不能直接滾下去,但是可以借鑑這種方式,改變一下策略。

要想最快下山,其實只需要循環執行以下 3 步驟:

  1. 環顧周圍找到最陡的一段路
  2. 在最陡的一段路上走一段距離
  3. 重複以上步驟直到山底

假設你擁有找到目前所在位置最陡路線的能力,那麼你只需要重複以上步驟就能以最短時間,最短路程下到山底去喫海底撈啦!

這就是梯度下降法的現實例子,下面來正式學習下梯度下降法的基本思想。

二、梯度下降法基本思想

數學上的梯度下降法步驟跟下山的例子一模一樣,只不過換成了數學公式具體表達出來而已,這裏用個函數圖像來形象解釋下:

把這個函數圖像想象成一座山,此刻你正在山頂,並且要尋找最快的下山路線,那麼按照上面講的下山 3 步驟,你要做的就是找最陡的下山方向,然後走一段路,再找最陡的下山方向,再走一段路,以此類推,最後就得到上面的這條下山路線。

2.1 算法解決什麼問題?

我們爲了儘快下山,用了梯度下山法;那麼對應到數學曲線上,對一個函數應用梯度下降法,就是爲了最快地求出函數的全局最小值或者局部最小值;再對應到機器學習問題上,梯度下降法就是爲了儘快求出模型代價函數最小值,進而得到模型參數;

所以梯度下降法要解決的問題就是:以最快速度求函數最小值。

2.2 如何用數學公式表達?

先來用公式表達出下山的步驟:

= 下一個位置 = 當前位置 - 帶方向的下山距離

與下山公式一樣,一行公式即可寫出梯度下降法公式:

θj=θjαθjJ(θ) {\theta_{j}}={\theta_{j}}-\alpha \frac{\partial }{\partial {\theta_{j}}}J\left(\theta \right)

我們來對照下山的例子,詳細解釋這個公式:

  • 等式左邊的 θj\theta_j 是下一時刻我在山頂的位置:下一函數值
  • 等式右邊的 θj\theta_j 是當前時刻我在山頂的位置:當前函數值
  • 學習率 α\alpha :算法迭代步長
  • θjJ(θ)-\frac{\partial }{\partial {\theta_j}}J(\theta) 是最快的下山方向:當前時刻函數值下降最快的方向
  • αθjJ(θ)-\alpha\frac{\partial }{\partial {\theta_j}}J(\theta) 是當前時刻的打算邁出的距離:算法當前迭代下降的距離
  • J(θ)J(\theta) 可以理解爲你要下的那座山:算法執行的函數

這裏的 α\alpha 在算法中叫做學習率,雖然從名字上不好理解,不過它的作用就是控制算法每次迭代下降的步長,也就是每次下山打算邁開多大步。

你可能疑惑的是下山方向爲何加負號,其實在數學上,這個公式 θjJ(θ)-\frac{\partial }{\partial {\theta_j}}J(\theta) 的意思是求偏導數,也叫做求梯度,函數梯度是函數值增加速度最快的方向,而這裏加上一個負號就表示函數值下降最快的方向,也就表示下山速度最快(最陡)的方向。

但是學習率和梯度都不是當前時刻函數值要減少的量,當前函數下降的量等於這兩者的乘積 αθjJ(θ)-\alpha\frac{\partial }{\partial {\theta_j}}J(\theta) ,一定不要誤以爲學習率就是當前函數下降的距離,它只是一種度量方式,可以理解爲一個尺度,不是實際的值。

讓我們再從實際的梯度下降曲線中直觀的看下算法的迭代過程。

三、梯度下降法的直觀理解

下面這個例子可以很好地解釋單變量(θ1\theta_1)梯度下降的過程:

算法從曲線的右上角紫色的數據點開始迭代下降,最終下降到底部綠色點找到函數最小值,算法結束,來詳細分析一次下降的過程:

  • 計算第一個數據點處的梯度(斜率或偏導數),這裏梯度大於 0
  • 計算下一函數值:θ1=θ1α\theta_1 = \theta_1 - \alpha * 梯度
  • 重複以上步驟,直到底部綠色點梯度爲 0

這裏簡單說下學習率 α\alpha 對算法的影響:

  • 如果 $ \alpha $ 太小,每次更新的步長很小,導致要很多步才能才能到達最優點;
  • 如果 α\alpha 太大,每次更新步長很大,在快接近最優點時,容易因爲步長過大錯過最優點,最終導致算法無法收斂,甚至發散;

雖然學習率會影響迭代步長,那是否需要我們每次手動更新學習率呢?

不需要!因爲迭代的步長每次都會自動減小!隨着數據點越來越靠近最低點,在該點處的斜率越來越小,即梯度值 θjJ(θ)\frac{\partial }{\partial {\theta_j}}J(\theta) 越來越小,而 α>0\alpha > 0,所以兩者相乘後 αθjJ(θ)\alpha \frac{\partial }{\partial {\theta_j}}J(\theta) 也越來越小,進一步導致 θj\theta_j 值減小的越來越慢。

當算法最後迭代到最低點時,綠色點處斜率爲 0,即梯度爲 0,此時梯度下降公式將不再變化:

θj=θjα0=θj \theta_j = \theta_j - \alpha * 0 = \theta_j

所以算法認爲已經找到最優值,不再迭代下降,算法至此結束。

這個例子是從右向左迭代下降,如果起始數據點是在左邊,算法是否能正常運行呢?完全沒問題,唯一的不同處是梯度小於 0,公式裏面負號變爲正號,你可以試着自己分析下。

四、梯度下降法擬合函數直線

理論介紹完了,下面進入實戰部分,登龍手把手用 Python 帶你實現一個梯度下降法,並用這個的算法來擬合下面的數據點(人口 - 利潤):

因爲篇幅限制,這裏只講解我認爲比較關鍵的代碼,其他比較基礎的加載數據,導包就不介紹了,文末有完整代碼,裏面的註釋非常詳細,建議下載食用,有收穫記得回來給我個 Star 哦!

4.1 模型選擇

這個數據集比較簡單,只有一個人口特徵,但是爲了方便代碼計算我們人爲增加一個特徵 x0=1x_0 = 1,並且使用線性迴歸的函數模型:

hθ(x)=θ0x0+θ1x1 h_\theta(x) = \theta_0x_0 + \theta_1 x_1

那先來定義這兩個參數:

# X.shape[1] = 2,表示參數數量 n = 2
# theta = [theta_0 = 0, theta_1 = 0]
theta = np.zeros(X.shape[1])

我們的最終目的就是找出最優的參數(θ0\theta_0θ1\theta_1),使得直線擬合的總均方誤差達到最小,那如何表示均方誤差呢?這就需要代價函數登場了。

4.2 代價函數

代價函數顧名思義就是每組參數所對應的擬合誤差量,要想擬合數據集的效果最好,那就要求參數所對應的代價函數取最小值,這裏的我選擇常用的均方誤差來作爲代價函數:

J(θ0,θ1)=12mi=1m(hθ(x(i))y(i))2 J \left( \theta_0, \theta_1 \right) = \frac{1}{2m}\sum\limits_{i=1}^m \left( h_{\theta}(x^{(i)})-y^{(i)} \right)^{2}

這個代價函數計算的是一組參數(θ0\theta_0θ1\theta_1)擬合的數據預測值與真實值均方誤差,看下這個函數如何用代碼寫出來,這裏要用點線性代數的知識:

# Cost Function
# X: R(m * n) 特徵矩陣
# y: R(m * 1) 標籤值矩陣
# theta: R(n) 線性迴歸參數
def cost_function(theta, X, y):
    # m 爲樣本數
    m = X.shape[0]
    
    # 誤差 = theta * x - y
    inner = X @ theta - y
    
    # 將向量的平方計算轉換爲:列向量的轉置 * 列向量
    square_sum = inner.T @ inner
    
    # 縮小成本量大小,這裏無特殊含義
    cost = square_sum / (2 * m)
    
    # 返回 theta 參數對應的成本量
    return cost;

我們的梯度下降法就是應用在這個代價函數上,來尋找代價函數的最小值,進而找到取最小值時對應的參數 (θ0\theta_0θ1\theta_1)。

4.3 計算梯度

梯度下降需要用到某點的梯度,即導數,看下求梯度的代碼:

# 計算偏導數
def gradient(theta, X, y):
    # 樣本數量
    m = X.shape[0]
    
    # 用向量計算複合導數
    inner = X.T @ (X @ theta - y)
    
    # 不要忘記結果要除以 m
    return inner / m

這個其實也不難,就是代價函數對 θi\theta_i 進行復合求導:

J(θ0,θ1)=212mi=1m(hθ(xi)y(i))hθ(xi) J( \theta_0, \theta_1)' = 2 * \frac{1}{2m}\sum\limits_{i=1}^m \left( h_{\theta}(x_i)-y^{(i)} \right) * h_\theta(x_i)'

hθ(xi)=(θ0x0+θ1x1)=xi h_\theta(x_i)' = (\theta_0x_0 + \theta_1x_1)' = x_i

因爲上面的代碼是用向量表示的,一列向量裏面包含所有參數,所以對包含參數的向量進行乘積,就相當於公式裏面的求和符號了,而且使用向量計算,順序會有點不一樣,這裏就不詳細展開講了,暫時能理解就可以。

4.4 執行批量梯度下降算法

準備就緒,下面就到了最重要的部分,批量梯度下降法的邏輯代碼,其實也很簡單,就是執行一個循環 =_=:

# 批量梯度下降法
# epoch: 下降迭代次數 500
# alpha: 初始學習率 0.01
def batch_gradient_decent(theta, X, y, epoch, alpha = 0.01):
    # 計算初始成本:theta 都爲 0
    cost_data = [cost_function(theta, X, y)]
    
    # 創建新的 theta 變量,不與原來的混淆
    _theta = theta.copy()
    
    # 迭代下降 500 次
    for _ in range(epoch):
        # 核心公式:theta = theta - 學習率 * 梯度
        _theta = _theta - alpha * gradient(_theta, X, y)
        
        # 保存每次計算的代價數據用於後續分析
        cost_data.append(cost_function(_theta, X, y))
    
    # 返回最終的模型參數和代價數據
    return _theta, cost_data

傳入的參數我們之前都介紹過了,再複習下:

  • theta:待預測的直線參數 θj\theta_j
  • X:樣本橫座標值
  • y:樣本縱座標值
  • epoch:迭代下降次數
  • alpha:學習率

最終返回的結果是迭代 500 次後對原始數據擬合誤差最小的參數 θ0,θ1\theta_0, \theta_1 以及每次計算的代價數據 costcost

補充下:這裏批量的意思是每次迭代都計算全部樣本的均方誤差,不是一次計算一個樣本,只是我們常常省略批量這兩個字,直接叫梯度下降法。

4.5 測試擬合結果

我們來調用上面的批量梯度下降法,看下預測的參數:

epoch = 500
final_theta, cost_data = batch_gradient_decent(theta, X, y, epoch)

final_theta

輸出 θ0=2.2828,θ1=1.0309\theta_0 = -2.2828, \theta_1 = 1.0309

array([-2.28286727,  1.03099898])

來可視化代價數據看下是否連續下降:

cost_data

代價函數隨着迭代次數變多逐漸減小,直到 4.713809 基本不變,至此我們已經找到了我們認爲的最優的擬合數據的直線模型參數,因爲代價函數取得了最小值。

那來看下擬合的效果,看起來還不錯:

五、總結

以上就是我對單變量梯度下降法的基本理解,還有很多不足,希望大家多多指正,文中部分代碼用到微積分和線性代數的知識,建議回頭複習下,可以更好的理解算法 _

另外,關於多變量的梯度下降法我也寫了點自己的總結:從 0 開始機器學習 - 一文入門多維特徵梯度下降法!,原理幾乎相同,強烈推薦實踐一下。

文中項目超詳細註釋完整代碼:AI-Notes,學會了記得回來給我個 Star 哈。

本文原創首發於 同名微信公號「登龍」,微信搜索關注回覆「1024」你懂的

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