My Machine Learn(一): 線性迴歸

一、前言

從上一次發的關於機器學習的文章到現在已經快一年了。 期間懈怠了很長一段時間,最近重新開啓學習機器學習之路,準備一邊學習,一邊記錄下這個過程, 所以就有了寫 《My Machine Learn》系列文章的想法。當然所思縮寫均是我自己的理解,不能保證完全正確,如果疏漏的地方,還請看官不靈賜教。 閒話少敘,言歸正傳。

二、簡介

說道機器學習,深度學習之類的東西,大家首先想到的可能就是神經網路(或者說人工神經網路), 一說神經網路,大家同樣會很自然的聯想到,咱人類大腦內部數之不盡的神經元,大概就是下圖所示的樣子。

生物神經細胞

從生物仿生學方面來說,或許真是如此。 但如果從數學角度來說的話,機器學習,神經網絡更像是一堆數學公式的集合。

因此首先就來看看這一切的基礎中的基礎: 線性迴歸。

什麼是線性迴歸呢? 從百度百科中可以看到有如下解釋:

線性迴歸是利用數理統計中迴歸分析,來確定兩種或兩種以上變量間相互依賴的定量關係的一種統計分析方法,運用十分廣泛。其表達形式爲y = w’x+e,e爲誤差服從均值爲0的正態分佈。

迴歸分析中,只包括一個自變量和一個因變量,且二者的關係可用一條直線近似表示,這種迴歸分析稱爲一元線性迴歸分析。如果迴歸分析中包括兩個或兩個以上的自變量,且因變量和自變量之間是線性關係,則稱爲多元線性迴歸分析

因此咱就來聊聊這個一元線性迴歸(主要是俺們對多元線性迴歸也不熟悉 (⊙o⊙)… )

一元線性方程這玩意兒,應該從小學的時候就開始學了吧,或者應該說一元一次方程,形如: ax + b = 0
太細節的這裏就不再囉嗦了,有興趣的可以去翻翻小學或者初中課本。

我還是比較喜歡按照機器學習的風格來定義,這裏就將方程定義爲:

y=wx+b

其大致圖形如下所示:


一次函數

結合上面的方程,其中,w表示 直線的斜率, b 表示直線在y軸上的截距。

三、應用

在線性迴歸的定義中提到, 線性迴歸是利用數理統計中迴歸分析,來確定兩種或兩種以上變量間相互依賴的定量關係的一種統計分析方法。 也就是說它可以用來進行比較神祕的“預測”操作。 簡單來說,給一堆數據,這對數據包含自變量部分與因變量部分。 自變量對應方程中的“X”, 因變量對應方程中的“Y”。 拿機器學習的術語來說,就是 數據集和標籤集。

我們拿到這些數據與標籤之後,就可以進行迴歸分析,也就是確定這個數據與標籤之間的依賴關係,最終得到一條“合理”的直線,而成對的數據(X) 和 標籤(y)就是分佈在這條直線周圍“點”。 而且這條“合理”的直線必須滿足,所有點到直線距離之和最小。

四、訓練

所謂“訓練”,就是上面說的, 找到這條“合理”直線的過程, 或者說數據擬合直線的過程。這裏就舉一些例子來簡單說明。

4.1. 首先給出一條直線的方程以及其圖形

方程(爲了後面好記,在公式當中添加 f(x) 字樣):

y=f(x)=2x+1

圖形:


y=2x + 1 示例圖

上圖就是方程對應的圖形(用excel畫的)。 前面我們說到,要找到這條“合理”的直線,必須要滿足,所有的點離直線的距離的總和最近。

咱們先從單個的點開始, 先看圖中的點A, 其座標是(6, 20)。 這個點在直線f(x)的上方,如果我們要讓f(x)靠近點A有什麼方法呢?

答案很簡單,就是修改f(x)的參數。 從直觀上就可以看出, 要讓直線f(x)靠近點A, 可以將直線向上平移, 也可以改變直線的傾斜度(即斜率),還可以同時平移和改變斜率。

4.2. 移動直線

移動直線的實現方法,我們這裏講3種,絕對值技巧, 平方技巧, 梯度下降。接下來我們就依次來看這3個東東到底是什麼玩意兒。

  • 絕對值技巧

首先要說明的一點是,所謂絕對值技巧,並不是對某個公式取絕對值,而是非常簡單粗暴的方式在f(x)的常參數(w和b)加上或減去一個絕對的值, 比如需要增大斜率時,就給f(x)中的 w 加上一個絕對的值,反之就減去一個絕對的值。

假如有如下直線,以及A,B,C,D四個點:


絕對值技巧圖1

如果想讓直線靠近點A, 那麼我們可以 給f(x)中的 b 加 1,這樣直線就像上平移了一個單位。同時我們也可以給w加上一個值,如何來決定這個“絕對的值”呢, 這裏我們直接取點A的x座標,即p1, 移動之後的直線方程爲:

f(x)=(w+p1)x+(b+1)

同理,我們可以處理讓直線靠近點B, 因爲點B在直線下方,所以對b 減 1,讓直線向下平移,對w減去一個“絕對的值”來減小斜率,讓直線傾斜度變小,更靠近點B, 同樣,我們以點B的x座標p1作爲這個“絕對的值”。移動之後的直線方程爲:

f(x)=(wp1)x+(b1)

注: 上面的 p1和1都是“絕對的值”,當然也可以選取其他的值作爲“絕對的值”, 比可以選 3,4 分別作爲對w和b操作的“絕對”的值

從上面兩點其實可以總結出,如果直線要靠近的點在直線的上方,就加上一個絕對的值,如果在直線的下方就減去一個絕對的值。同理,在處理點C的時候,就對f(x)的w和b加上一個“絕對的值”。看官你可能要問了, 對於點C, 應該要減小直線的傾斜度(即w減去某個值)才能靠近點C。 因爲對於點C, 其x座標p2是一個負值, 所以會有 w + p2 < w 的結果。 這裏就能明白在對絕對值技巧的定義解釋的時候,爲什麼要說是“絕對的值”,而不是“絕對值(形如: |a|)”了。

一切看上去很美好,但這都是表象,絕對值技巧這部電影的反派們終於登場了。 第一位就是, 我們在使用絕對值技巧對直線進行移動之後,很可能出現移動過頭的情況,還是用上面的例子來說明,讓直線f(x)靠近點A, 其移動之後的直線可能會是下面的樣子:


絕對值技巧圖2

這可能並不是我們想要的,爲什麼會出現這個問題呢? 是因爲點A的x座標p1可能很大,這樣對直線f(x)的斜率增加的幅度就會很大。 還好這個反派不是很厲害,咱們還有解決掉它的方法。

這個方法也是比較簡單粗暴的,就是給這個“絕對的值”加一個係數 α , 也就是機器學習中的學習率,一般它是一個0 ~ 1 之間的值。增加了學習率α 之後的公式如下:

y=f(x)=(w+p1α)x+(b+α)

so,第一個反派已被咱KO了, 接下來看第二個反派。

因爲咱們是通過 w加 目標點的x座標來調整直線的斜率。 所以這裏就有一個隱藏的Bug, 假設現在出現了新的點E(p3, q1), 如下圖所示:


絕對值技巧圖3

從圖上可以看出,要讓直線f(x)靠近點E,需要調整的斜率和靠近點A需要調整的斜率相比,是要小很多的。但是根據“絕對值技巧”的方法,在直線上方的點,調整斜率都是 w 加上 x座標乘以學習率α , 而點E的x座標p3比點A的x座標p1要大一些,得到的新斜率 w+p3α 要大於 w+p1α , 這與我們的想要的結果可不太一樣。

這一個反派比較厲害一點, 俺們有點招架不住了。 所以先跑了再說,看下一個方法。

  • 平方技巧

上一個技巧是使用到了目標點的x座標, 而這裏是在絕對值技巧的基礎上增加了對目標點y座標的使用。


平方技巧圖1

如上圖所示, 直線f(x)=wx+b 上的點F與直線上方的點A其x座標是一樣的,通過直線的方程可以計算得到,點F對應的y座標爲:

y^=wp1+b

其中 y^ 表示預期值,也就是根據自變量x得到的因變量y。

同樣的原理,如果點A是目標點的話,p1就是輸入數據(自變量),q1就是標籤結果(因變量)。 那麼點A和點F的y座標差值 就表示預期值和目標值的誤差,可表示爲:

yerror=q1y^

所以在絕對值技巧的基礎上再加上這裏的y座標誤差之後的平方技巧對移動直線的結果爲:

y=f(x)=(w+p1αyerror)x+(b+αyerror)

似乎這也不復雜。 而且這個技巧還幹掉了前面 絕對值技巧難以面對的反派二,因爲在絕對值技巧遇到的反派二中,雖然x座標變大了,但是這個目標點距離直線的距離卻變小了, 所以也能在一定程度上減小直線的移動範圍。

其實從這裏就可以看出,平方技巧有一個比較厲害的技能,那就是根據目標點和直線之間的距離來決定直線移動的範圍。

比如目標點的y座標很大, 也就表示目標點在直線f(x)上方更遠的地方。因此,如果想讓直線靠近目標點的話,就需要在較大的範圍來調整直線的斜率和截距。

當然只要使用了平方技巧一切都不用怕了。 因爲我們得到的直線和目標點的誤差值yerror 也同樣不小。也就是說 (w+pαyerror)(b+αyerror) 這兩個公式對直線f(x)的斜率和截距的影響很大,妥妥的能夠滿足咱們的需求。

同理,如果目標點與直線距離很近,那麼yerror 的值也就相對較小,咱們對直線的移動範圍也就相對較小。

不但如此,平方技巧還能解決絕對值技巧中的一個尷尬, 在絕對值技巧中,目標點在直線上方,對w和b的操作就要用“+”, 如果目標點在直線下方,就要用“-”。

平方技巧就比較厲害了,我們不需要去關心這些東東。因爲當目標點在直線下方時,yerror 的值就會是一個負值, 加上這個負值,就相當於減去|yerror|

最後關於爲啥這個技巧叫平方技巧呢? 俺的理解是, 對w和b都使用了 (qy^) 進行調整,是不是就因此叫“平方”了呢? 不知對錯,還請看官們指點指點。

  • 梯度下降

一說起梯度下降,咱們首先想到的可能就是神經網絡了。 但梯度下降是一種方法或者說思想, 既然神經網絡能用,那咱線性迴歸應該也未嘗不可用吧。

其實梯度下降在線性迴歸的應用,形式和上面的平方技巧很相似,其結果也是等價的。 爲何會這樣呢? 咱且看梯度下降是如何表現的。


gradient

還是和平方技巧一樣的圖,直線需要靠近點A。 首先咱們在x軸的p1對應到直線上的值爲:

y^=wp1+b

在梯度下降中,我們需要計算誤差(也就是y座標值得誤差),其計算公式爲:

Error=12mi=0m(qy^)2

其中: m表示點的數目,q表示目標值,y^ 表示預期值。

這裏只有點A一個點, 所以,點A和直線的誤差爲:

Error=12(q1y^)2

要使用梯度下降,就需要對“Error”求導,更確切的說,是分別對直線函數的w和b求偏導。這個Error其實是一個複合函數。假定Error的函數爲h(x), 而直線函數爲f(x), 那麼就有:

Error=h(x)=h(12(q1f(x))2)

根據複合函數的求導法則:

如果: h(x)=f(g(x))

那麼: h(x)=f(g(x))g(x)

那麼咱就分別對w和b來求偏導(也就是說: 當對w求偏導時,把f(x)=wx+b 中的w看做自變量,x和b都看做常量)。

首先是對w求偏導:

wError=h(x)f(x)

h(x) 中自變量爲y^ , 在f(x) 中自變量爲w

wError=(12(q1y^)2)(wx+b)

wError=(q1y^)x

同理可得:

bError=(q1y^)

以上就是求導的結果,也就是所謂的梯度,而梯度下降就是減去這個梯度, 即:

wwαwError=w+α(q1y^)x

bbαbError=b+α(q1y^)

這是不是和上面的平方技巧很像啊。

五、示例

以上聊了那麼多,現在咱就來實戰一把。

這裏的示例,是根據BMI指數來預測人的壽命。

BMI指數:即身體質量指數,簡稱體質指數,英文爲Body Mass Index,簡稱BMI

要預測就得先對咱們的“直線”進行訓練,即將訓練數據用一條直線來擬合。這個數據時以前老美統計的數據,看看就好,不必當真,重要的是理解線性迴歸這東東。

這個數據所在的文件名(此文件會包含在源碼的git倉庫中,後續會提到): bmi_and_life_expectancy.csv

因爲有一百多條數據,也就不全部貼出來了,這裏就只貼前面幾條,以展示數據的結構:

Country,Life expectancy,BMI

Afghanistan,52.8,20.62058

Albania,76.8,26.44657

Algeria,75.5,24.5962

Andorra,84.6,27.63048

Angola,56.7,22.25083

Armenia,72.3,25.355420000000002

Australia,81.6,27.56373

Austria,80.4,26.467409999999997

Azerbaijan,69.2,25.65117

Bahamas,72.2,27.24594

Bangladesh,68.3,20.39742

Barbados,75.3,26.384390000000003

Belarus,70.0,26.16443

Belgium,79.6,26.75915

Belize,70.7,27.02255

Benin,59.7,22.41835

Bhutan,70.7,22.8218

Bolivia,71.2,24.43335

從上面可以看出,數據分三列,國家,預期壽命,BMI值。這裏咱們只需要預期生命和BMI值兩列數據。

5.1 數據分佈情況

首先咱先整理的看一下數據分佈狀況,這裏有兩種方式:

  • Excel 展示

在excel上可以根據這兩列數據繪製散點圖。 慚愧的是,我試了幾次,弄出來的散點圖總是不對。

它excel把x軸(BMI)的取值範圍自動改了,我怎麼弄都搞不定,也只能怪我是excel的菜鳥。

  • 使用Python來展示

Python這東東不管是在機器學習還是數據可視化方面都是很牛叉的存在,這裏就用簡單的幾行代碼就能夠讀取這個csv數據,並以散點圖的形式顯示出來,其代碼如下所示:

#導入需要的用到的庫
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

#讀取csv數據
bmi_life_data = pd.read_csv('bmi_and_life_expectancy.csv')

#分別以BMI值和預期壽命的數據創建numpy的數組
x = np.array(bmi_life_data[["BMI"]])
y = np.array(bmi_life_data["Life expectancy"])

#兩BMI值和預期壽命以x和y座標對的方式輸入到plot中
for i in range(len(x)):
    plt.scatter(x[i], y[i], color='blue', edgecolor='k')

#設置x軸和Y軸標籤(名稱)
plt.xlabel('BMI')
plt.ylabel('Life expectancy')

#顯示散點圖
plt.show()


數據散點圖

5.2 代碼實現

  • c/c++實現

接下來, 咱們首先以c/c++ 語言(沒辦法他兩纔是俺們的老搭檔,老相好)來實現這個線性迴歸的算法。

二話不說,直接上硬菜。爲了簡單,這裏將 epoch, learning rate,weight, bias等參數均定義爲全局變量,

如下所示:

int gEpochs = 1000; /** 訓練(或迭代)的次數(或代數) */
double gLearning_rate = 0.001; /** 學習率 */

double gW = 1.0; /** 初始權重(或者說斜率) w */
double gB = 0.0; /** 初始偏置(或者說截距) b */

接着是重頭戲,訓練函數,這裏使用的是前面的平方技巧,但是有點差別,那就是對“絕對的值”的選擇不太一樣。 前面在介紹概念的時候,是選擇的x座標。在這裏咱們直接選擇權重(或者說斜率)作爲“絕對的值”(咱線性迴歸也可以向神經網絡致敬吧)。

/**
 * x[]   是輸入數據,自變量
 * lenx  表示輸入數據的數量
 * y[]   標籤值
 */
void linear_regression(double x[], int lenx, double y[])
{

    for(int i = 0; i < gEpochs; i++) /** 要迭代(或訓練) gEpochs次 */
    {
        for (int j = 0; j < lenx; j++)
        {
            double y_hat = gW * x[j] + gB; /** 計算,對於輸入x[j]的預期輸出 */
            /** 計算本次調整幅度, 其中(y[j] - y_hat) 表示
             *  線性迴歸的輸出誤差(即實際值和預測值的誤差) 
             */
            double delta = gLearning_rate * (y[j] - y_hat); 

            /** 調整權重(或斜率),這裏使用權重本身替代了x座標作爲“絕對的值” */
            gW += gW * delta; 
            gB += delta; /** 調整偏置(或截距) */
        }
    }
}

意不意外,驚不驚喜,沒想到會是如此這般的簡單吧。 最後就是預測函數, 即輸入一個BMI值預測預期壽命,當然這個函數就更簡單了。

/**
 *  w 訓練後的權重(或斜率)
 *  b 訓練後的偏置(或截距)
 *  bmi 輸入的BMI值
 */
double predict(double w, double b, double bmi)
{
    return w * bmi + b; /** 根據線性迴歸函數 f(x) = wx + b 計算預測壽命值 */
}

最後斷後結尾的是咱們的測試代碼:


int main(int argc, char* argv[])
{
    /** 這裏的gX和gY是兩個全局數組,分別對應BMI值和預期壽命,
     *  由於較多就不貼出來了,看官們可取完整源碼中查看 */

    /** 訓練 線性迴歸函數 */
    linear_regression(gX, sizeof(gX) / sizeof(gX[0]), gY);

    double preBMI = 21.07931; /** 用於測試的BMI值 */
    double output = predict(gW, gB, preBMI); /** 預測預期壽命 */

    /** 注意: 這裏格式化輸出會出現小數點精度損失的問題, 可以使用
     *        visual studio等工具單步調試的時候查看精確的雙精度
     *        預測預期壽命
     */

    printf("parameters after train, w: %lf, b: %lf\n", gW, gB);
    printf("BMI: %lf, output life expectancy: %lf\n", preBMI, output);

    getchar();

    return 0;
}

其輸出結果爲(使用的visual studio編譯和運行):


c_cpp_output

  • Python實現

Python實現同樣是無與倫比的簡單。

和上面一樣,首先來看一些默認參數的定義:

#首先是訓練(或迭代)的次數 和 學習率的定義
epochs = 1000 
learning_rate = 0.001

看官們看到這裏可能會有些奇怪,默認的權重(或斜率)和偏置(或截距)值得定義呢, 上哪兒去了? 被你吃了?

看官們莫急,且聽在下娓娓道來。

#實現線性迴歸訓練函數
def linear_regression(x, y):
    #啊哈,咱們的默認權重和偏置跑這裏來了
    w = 1
    b = 0
    for epoch in range(epochs):
        for i in range(len(x)):
            #計算預測輸出
            y_hat = w * x[i] + b
            #計算調整幅度,(y[i] - y_hat)爲輸出偏差
            delt = learning_rate * (y[i] - y_hat)

            #調整權重和偏置
            w += w * delt
            b += delt
    return w, b

#實現預測函數
def predict(w, b, bmi):
    return w[0] * bmi + b[0]

#根據前面繪製散點圖時分離出來的x和y作爲輸入數據和標籤數據
#訓練線性迴歸函數
w, b = linear_regression(x, y)

#測試代碼,預測預期壽命
predBMI = 21.07931
output = predict(w, b, predBMI)

print("parameters after train, w: %f, b: %f" %(w, b))

print("test BMI: %lf output life expectancy: %lf" %(predBMI, output))

#注意: 這裏使用了 %lf或%f格式化輸出,這樣打印出的精度會有損失。
#      可使用形如print(output)這樣的方式,可以打印出完整的雙精度浮點

其輸出結果爲(使用的是pycharm 運行):


"pyhong版輸出結果"

對了順便貼一下訓練之後的圖形(數據散點圖,以及線性迴歸直線 ):


"訓練後的圖形"

六、附錄

所有的源碼和數據文件均在 開源中國的 碼雲當中, 其地址如下:

https://gitee.com/xunawolanxue.com/my_demo_code/tree/master/My_Machine_Learn/%E7%BA%BF%E6%80%A7%E5%9B%9E%E5%BD%92

整個目錄樹爲:

  • c_cpp :c/c++版本的線性迴歸實現
    • line_regression:visual studio工程所在目錄
    • README.md :c/c++實現版本的說明
  • Python :Python版本的線性迴歸實現(包括繪圖部分)
    • bmi_and_life_expectancy.csv : csv數據
    • draw_scatter.py : 繪製數據的散點圖
    • line_regression.py : 線性迴歸實現
    • draw_after_train.py : 繪製訓練後的散點圖(包含線性迴歸的直線)
    • README.md : python實現版本的說明
  • README.md :整個項目的說明

【2018-06-05】更新了demo代碼的網絡鏈接

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