1. 程式人生 > >[ch04-03] 用神經網路解決線性迴歸問題

[ch04-03] 用神經網路解決線性迴歸問題

系列部落格,原文在筆者所維護的github上:https://aka.ms/beginnerAI,
點選star加星不要吝嗇,星越多筆者越努力。

4.3 神經網路法

在梯度下降法中,我們簡單講述了一下神經網路做線性擬合的原理,即:

  1. 初始化權重值
  2. 根據權重值放出一個解
  3. 根據均方差函式求誤差
  4. 誤差反向傳播給線性計算部分以調整權重值
  5. 是否滿足終止條件?不滿足的話跳回2

一個不恰當的比喻就是穿糖葫蘆:桌子上放了一溜兒12個紅果,給你一個足夠長的竹籤子,選定一個角度,在不移動紅果的前提下,想辦法用竹籤子穿起最多的紅果。

最開始你可能會任意選一個方向,用竹籤子比劃一下,數數能穿到幾個紅果,發現是5個;然後調整一下竹籤子在桌面上的水平角度,發現能穿到6個......最終你找到了能穿10個紅果的的角度。

4.3.1 定義神經網路結構

我們是首次嘗試建立神經網路,先用一個最簡單的單層單點神經元,如圖4-4所示。

圖4-4 單層單點神經元

下面,我們用這個最簡單的線性迴歸的例子,來說明神經網路中最重要的反向傳播和梯度下降的概念、過程以及程式碼實現。

輸入層

此神經元在輸入層只接受一個輸入特徵,經過引數w,b的計算後,直接輸出結果。這樣一個簡單的“網路”,只能解決簡單的一元線性迴歸問題,而且由於是線性的,我們不需要定義啟用函式,這就大大簡化了程式,而且便於大家循序漸進地理解各種知識點。

嚴格來說輸入層在神經網路中並不能稱為一個層。

權重w/b

因為是一元線性問題,所以w/b都是一個標量。

輸出層

輸出層1個神經元,線性預測公式是:

\[z_i = x_i \cdot w + b\]

z是模型的預測輸出,y是實際的樣本標籤值,下標 \(i\) 為樣本。

損失函式

因為是線性迴歸問題,所以損失函式使用均方差函式。

\[loss(w,b) = \frac{1}{2} (z_i-y_i)^2\]

4.3.2 反向傳播

由於我們使用了和上一節中的梯度下降法同樣的數學原理,所以反向傳播的演算法也是一樣的,細節請檢視4.2.2。

計算w的梯度

\[ {\partial{loss} \over \partial{w}} = \frac{\partial{loss}}{\partial{z_i}}\frac{\partial{z_i}}{\partial{w}}=(z_i-y_i)x_i \]

計算b的梯度

\[ \frac{\partial{loss}}{\partial{b}} = \frac{\partial{loss}}{\partial{z_i}}\frac{\partial{z_i}}{\partial{b}}=z_i-y_i \]

為了簡化問題,在本小節中,反向傳播使用單樣本方式,在下一小節中,我們將介紹多樣本方式。

4.3.3 程式碼實現

其實神經網路法和梯度下降法在本質上是一樣的,只不過神經網路法使用一個嶄新的程式設計模型,即以神經元為中心的程式碼結構設計,這樣便於以後的功能擴充。

在Python中可以使用面嚮物件的技術,通過建立一個類來描述神經網路的屬性和行為,下面我們將會建立一個叫做NeuralNet的class,然後通過逐步向此類中新增方法,來實現神經網路的訓練和推理過程。

定義類

class NeuralNet(object):
    def __init__(self, eta):
        self.eta = eta
        self.w = 0
        self.b = 0

NeuralNet類從object類派生,並具有初始化函式,其引數是eta,也就是學習率,需要呼叫者指定。另外兩個成員變數是w和b,初始化為0。

前向計算

    def __forward(self, x):
        z = x * self.w + self.b
        return z

這是一個私有方法,所以前面有兩個下劃線,只在NeuralNet類中被呼叫,不對外公開。

反向傳播

下面的程式碼是通過梯度下降法中的公式推導而得的,也設計成私有方法:

    def __backward(self, x,y,z):
        dz = z - y
        db = dz
        dw = x * dz
        return dw, db

dz是中間變數,避免重複計算。dz又可以寫成delta_Z,是當前層神經網路的反向誤差輸入。

梯度更新

    def __update(self, dw, db):
        self.w = self.w - self.eta * dw
        self.b = self.b - self.eta * db

每次更新好新的w和b的值以後,直接儲存在成員變數中,方便下次迭代時直接使用,不需要在全域性範圍當作引數內傳來傳去的。

訓練過程

只訓練一輪的演算法是:


for 迴圈,直到所有樣本資料使用完畢:

  1. 讀取一個樣本資料
  2. 前向計算
  3. 反向傳播
  4. 更新梯度

    def train(self, dataReader):
        for i in range(dataReader.num_train):
            # get x and y value for one sample
            x,y = dataReader.GetSingleTrainSample(i)
            # get z from x,y
            z = self.__forward(x)
            # calculate gradient of w and b
            dw, db = self.__backward(x, y, z)
            # update w,b
            self.__update(dw, db)
        # end for

推理預測

    def inference(self, x):
        return self.__forward(x)

推理過程,實際上就是一個前向計算過程,我們把它單獨拿出來,方便對外介面的設計,所以這個方法被設計成了公開的方法。

主程式

if __name__ == '__main__':
    # read data
    sdr = SimpleDataReader()
    sdr.ReadData()
    # create net
    eta = 0.1
    net = NeuralNet(eta)
    net.train(sdr)
    # result
    print("w=%f,b=%f" %(net.w, net.b))
    # predication
    result = net.inference(0.346)
    print("result=", result)
    ShowResult(net, sdr)

4.3.4 執行結果視覺化

列印輸出結果:

w=1.716290,b=3.196841
result= [3.79067723]

最終我們得到了W和B的值,對應的直線方程是\(y=1.71629x+3.196841\)。推理預測時,已知有346臺伺服器,先要除以1000,因為橫座標是以K(千臺)伺服器為單位的,代入前向計算函式,得到的結果是3.74千瓦。

結果顯示函式:

def ShowResult(net, dataReader):
    ......

對於初學神經網路的人來說,視覺化的訓練過程及結果,可以極大地幫助理解神經網路的原理,Python的Matplotlib庫提供了非常豐富的繪圖功能。

在上面的函式中,先獲得所有樣本點資料,把它們繪製出來。然後在[0,1]之間等距設定10個點做為x值,用x值通過網路推理方法net.inference()獲得每個點的y值,最後把這些點連起來,就可以畫出圖4-5中的擬合直線。

圖4-5 擬合效果

可以看到紅色直線雖然穿過了藍色點陣,但是好像不是處於正中央的位置,應該再逆時針旋轉幾度才會達到最佳的位置。我們後面小節中會講到如何提高訓練結果的精度問題。

4.3.5 工作原理

就單純地看待這個線性迴歸問題,其原理就是先假設樣本點是呈線性分佈的,注意這裡的線性有可能是高維空間的,而不僅僅是二維平面上的。但是高維空間人類無法想象,所以我們不妨用二維平面上的問題來舉例。

在4.2的梯度下降法中,首先假設這個問題是個線性問題,因而有了公式\(z=xw+b\),用梯度下降的方式求解最佳的\(w、b\)的值。

在本節中,用神經元的程式設計模型把梯度下降法包裝了一下,這樣就進入了神經網路的世界,從而可以有成熟的方法論可以解決更復雜的問題,比如多個神經元協同工作、多層神經網路的協同工作等等。

如圖4-5所示,樣本點擺在那裡,位置都是固定的了,神經網路的任務就是找到一根直線(注意我們首先假設這是線性問題),讓該直線穿過樣本點陣,並且所有樣本點到該直線的距離的平方的和最小。

可以想象成每一個樣本點都有一根橡皮筋連線到直線上,連線點距離該樣本點最近,所有的橡皮筋形成一個合力,不斷地調整該直線的位置。該合力具備兩種調節方式:

  1. 如果上方的拉力大一些,直線就會向上平移一些,這相當於調節b值;
  2. 如果側方的拉力大一些,直線就會向側方旋轉一些,這相當於調節w值。

直到該直線處於平衡位置時,也就是線性擬合的最佳位置了。

如果樣本點不是呈線性分佈的,可以用直線擬合嗎?

答案是“可以的”,只是最終的效果不太理想,誤差可以做到線上性條件下的最小,但是誤差值本身還是比較大的。比如一個半圓形的樣本點陣,用直線擬合可以達到誤差值最小為1.2(不妨假設這個值的單位是釐米),已經盡力了但能力有限。如果用弧線去擬合,可以達到誤差值最小為0.3。

所以,當使用線性迴歸的效果不好時,即判斷出一個問題不是線性問題時,我們會用第9章的方法來解決。

程式碼位置

ch04, Level3

思考和練習

  1. 請把上述程式碼中的dw和db也改成私有屬性,然後試著執行程式。