1. 程式人生 > >機器學習中梯度下降法原理及用其解決線性迴歸問題的C語言實現

機器學習中梯度下降法原理及用其解決線性迴歸問題的C語言實現

本文講梯度下降(Gradient Descent)前先看看利用梯度下降法進行監督學習(例如分類、迴歸等)的一般步驟:

1, 定義損失函式(Loss Function)

2, 資訊流forward propagation,直到輸出端

3, 誤差訊號back propagation。採用“鏈式法則”,求損失函式關於引數Θ的梯度

4, 利用最優化方法(比如梯度下降法),進行引數更新

5, 重複步驟2、3、4,直到收斂為止

所謂損失函式,就是一個描述實際輸出值和期望輸出值之間落差的函式。有多種損失函式的定義方法,常見的有均方誤差(error of mean square)、最大似然誤差(maximum likelihood estimate)、最大後驗概率(maximum posterior probability)、交叉熵損失函式(cross entropy loss)。本文就以均方誤差作為損失函式講講梯度下降的演算法原理以及用其解決線性迴歸問題。在監督學習下,對於一個樣本,它的特徵記為x(如果是多個特徵,x表示特徵向量),期望輸出記為t(t為target的縮寫),實際輸出記為o(o為output的縮寫)。兩者之間的誤差e可用下式表達(為了節省時間,各種算式就用手寫的了):

前面的係數1/2主要是為了在求導時消掉差值的平方項2。如果在訓練集中有n個樣本,可用E來表示所有樣本的誤差總和,並用其大小來度量模型的誤差程度,如下式所示:

 對於第d個例項的輸出可記為下式:

對於特定的訓練資料集而言, 只有Θ是變數,所以E就可以表示成Θ的函式,如下式:

所以,對於神經網路學習的任務,就是求到一系列合適的Θ值,以擬合給定的訓練資料,使實際輸出儘可能接近期望輸出,使得E取得最小值。

 

再來看梯度下降。上式中損失函式E對權值向量Θ的梯度如下式所示:

它確定了E最快上升的方向。在梯度前面加上負號“-”,就表示E最快下降的方向。所以梯度下降的訓練法則如下式所示:

, 其中

這裡的負號“-”表示和梯度相反的方向。η表示學習率。下面給出各個權值梯度計算的數學推導:

所以最終的梯度下降的訓練法則如下式:

    

 這個式子就是用於程式中計算引數Θ的。

下面看怎麼用梯度下降法解決線性迴歸問題。線性迴歸就是能夠用一個直線較為精確地描述資料之間的關係。這樣當出現新的資料的時候,就能夠預測出一個簡單的值。線性迴歸函式可寫成 。線性迴歸問題常用最小二乘法解決,這裡用梯度下降法解決主要是通過例項加深對梯度下降法的理解。先假設Y = 2X + 3=2*X + 3*1,取X的四個值分別為1,4,5,8,相應的Y為5,11,13,19。這樣就可以描述為有四個樣本分別為(1,1),(4,1),(5,1),(8,1),對應的期望值是5,11,13,19.5(這個值做了微調,從19變成了19.5,是為了讓四個樣本不在一根直線上)。通過梯度下降法求Θ值(最終Θ逼近2和3)。C語言實現的程式碼如下:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) { double matrix[4][2]={{1,1},{4,1},{5,1},{8,1}}; //樣本 double result[4]={5,11,13,19.5}; //期望值 double err_sum[4] = {0,0,0,0}; //各個樣本的誤差 double theta[2] = {1,6}; //Θ,初始值隨機 double err_square_total = 0.0; //方差和 double learning_rate = 0.01; //學習率 int ite_num; //迭代次數 for(ite_num = 0; ite_num <= 10000; ite_num++) { int i,j,k; err_square_total = 0.0; for(i = 0; i < 4; i++) { double h = 0; for(j = 0; j < 2; j++) h += theta[j]*matrix[i][j]; err_sum[i] = result[i] - h; err_square_total += 0.5*err_sum[i]*err_sum[i]; } if(err_square_total < 0.05) //0.05表示精度 break; for(j = 0; j < 2; j++) { double sum = 0; for(k = 0; k < 4; k++) //所有樣本都參與計算 sum += err_sum[k]*matrix[k][j]; theta[j] = theta[j] + learning_rate*sum; //根據上面的公式計算新的Θ } } printf(" @@@ Finish, ite_number:%d, err_square_total:%lf, theta[0]:%lf, theta[1]:%lf\n", ite_num, err_square_total, theta[0], theta[1]); return 0; }

程式執行後的結果為:@@@ Finish, ite_number:308, err_square_total:0.049916, theta[0]:2.037090, theta[1]:3.002130。發現迭代了308次,最終的線性方程為Y=2.037090X + 3.002130,是逼近2和3的。當再有一個新的X時就可以預測出Y了。學習率是一個經驗值,一般是0.01--0.001,當我把它改為0.04再執行時就不再收斂了。

 

上面的梯度下降叫批量梯度下降法(Batch Gradient Descent, BGD), 它是指在每一次迭代時使用所有樣本來進行梯度的更新。當樣本數目很大時,每迭代一步都需要對所有樣本計算,訓練過程會很慢。於是人們想出了隨機梯度下降法(Stochastic Gradient Descent, SGD),每次只隨機取一個樣本計算梯度,訓練速度變快了,但是迭代次數變多了(表示不是一直向最快方向下降,但總體上還是向最低點逼近)。還是上面的例子,只不過每次只從四個樣本中隨機取一個計算梯度。C語言實現的程式碼如下:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) { double matrix[4][2]={{1,1},{4,1},{5,1},{8,1}}; //樣本 double result[4]={5,11,13,19.5}; //期望值 double err_sum[4] = {0,0,0,0}; //各個樣本的誤差 double theta[2] = {1,6}; //Θ,初始值隨機 double err_square_total = 0.0; //方差和 double learning_rate = 0.01; //學習率 int ite_num; //迭代次數 for(ite_num = 0; ite_num <= 10000; ite_num++) { int i,j,seed; err_square_total = 0.0; for(i = 0; i < 4; i++) { double h = 0;
for(j = 0; j < 2; j++) h += theta[j]*matrix[i][j];
err_sum[i] = result[i] - h; err_square_total += 0.5*err_sum[i]*err_sum[i]; } if(err_square_total < 0.05) break; seed = rand()%4; for(j = 0; j < 2; j++) theta[j] = theta[j] + learning_rate*err_sum[seed]*matrix[seed][j]; //隨機選一個樣本參與計算 } printf(" @@@ Finish, ite_number:%d, err_square_total:%lf, theta[0]:%lf, theta[1]:%lf\n", ite_num, err_square_total, theta[0], theta[1]); return 0; }

程式執行後的結果為:@@@ Finish, ite_number:1228, err_square_total:0.049573, theta[0]:2.037240, theta[1]:3.000183。發現迭代了1228次(迭代次數變多了),最終的線性方程為Y=2.037240X + 3.000183,也是逼近2和3的。

 

後來人們又想出了在BGD和SGD之間的一個折中方法,即mini-batch SGD方法,即每次隨機的取一組樣本來計算梯度。mini-batch SGD是實際使用中用的最多的。還是上面的例子,只不過每次只從四個樣本中隨機取兩個作為一組個計算梯度。C語言實現的程式碼如下:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) { double matrix[4][2]={{1,1},{4,1},{5,1},{8,1}}; double result[4]={5,11,13,19.5}; double err_sum[4] = {0,0,0,0}; double theta[2] = {1,6}; double err_square_total = 0.0; double learning_rate = 0.01; int ite_num; for(ite_num = 0; ite_num <= 10000; ite_num++) { int i,j,k,seed; err_square_total = 0.0;
for(i = 0;i<4;i++) { double h = 0;
for(j = 0; j < 2; j++) h += theta[j]*matrix[i][j]; err_sum[i] = result[i] - h; err_square_total += 0.5*err_sum[i]*err_sum[i]; } if(err_square_total < 0.05) break; seed = rand()%4; k = (seed +1)%4; for(j = 0; j < 2; j++) { double sum = 0; sum += err_sum[seed]*matrix[seed][j]; //隨機取兩個作為一組計算梯度 sum += err_sum[k]*matrix[k][j];
theta[j] = theta[j] + learning_rate*sum; } } printf(" @@@ Finish, ite_number:%d, err_square_total:%lf, theta[0]:%lf, theta[1]:%lf\n", ite_num, err_square_total, theta[0], theta[1]); return 0; }

程式執行後的結果為: @@@ Finish, ite_number:615, err_square_total:0.047383, theta[0]:2.039000, theta[1]:2.987382。發現迭代了615次,最終的線性方程為Y=2.039000X + 2.987382,也是逼近2和3的。迭代次數介於BGD和SGD中間。在用mini-batch SGD時batch size的選擇很關鍵。