1. 程式人生 > >AI應用開發基礎傻瓜書系列4-用線性迴歸來理解神經網路的訓練過程

AI應用開發基礎傻瓜書系列4-用線性迴歸來理解神經網路的訓練過程

下面我們舉一個簡單的線性迴歸的例子來說明實際的反向傳播和梯度下降的過程。完全看懂此文後,會對理解後續的文章有很大的幫助。

為什麼要用線性迴歸舉例呢?因為\(y = wx+b\) (其中,y,w,x,b都是標量)這個函式的形式和神經網路中的\(Y = WX + B\)(其中,Y,W,X,B等都是矩陣)非常近似,可以起到用簡單的原理理解複雜的事情的作用。

創造訓練資料

讓我們先自力更生創造一些模擬資料:

import numpy as np
import matplotlib.pyplot as plt
# create mock up data
# count of samples
m = 100
X = np.random.random(m)
# create some offset as noise to simulate real data
noise = np.random.normal(0,0.1,x.shape)
W = 2
B = 3
Y = W * X + B + noise
plt.plot(X, Y, "b.")
plt.show()

得到100個數據點如下:

好了,模擬資料製作好了,目前X是一個100個元素的集合,裡面有0~100之間的隨機x點,Y是一個100個元素的集合,裡面有對應到每個x上的\(y=2x+3\)的值,然後再加一個或正或負的上下偏移作為噪音,來滿足對實際資料的模擬效果(因為大部分真實世界的生產資料從來都不是精確的,精確只存在於數學領域)。

現在我們要忘記這些模擬資料(樣本值)是如何製作出來的,也就是要忘記W,B的值。我們就假設這是實際應用中收集到的模擬資料,但是我們並不知道它的原始函式是什麼引數,只知道是公式\(y = wx + b\),我們的任務就是要根據這些樣本值,通過神經網路訓練的方式,得到w和b的值。注意這裡x和y是樣本的輸入和輸出,不是目標變數,這一點和常見的初等數學題不一樣,要及時轉變概念。

訓練方式的選擇

接下來,我們會用兩種方式來訓練神經網路(神經元):

  1. 把所有樣本逐個地輸入網路訓練
  2. 把所有樣本整批的輸入網路訓練

Pseudo code虛擬碼如下:

第一種方式:逐個樣本訓練

for 每個樣本x,y:
    標量前向計算得到z值 = wx+b
    計算損失(optional)
    計算w的梯度(輸入Z,Y,X的值)
    計算b的梯度(輸入Z,Y,X的值)
    更新w,b的值

第一種方式的好處是每次計算都是標量計算,不涉及到向量或者矩陣,便於大家理解。但是有個問題就是,如果最後幾個樣本的誤差較大的話,會把前面已經訓練得差不多的w,b的值變壞。

第二種方式:批量樣本訓練

while 停止條件不滿足
    矩陣前向計算得到Z值 = wX+b(其中X是所有樣本的一個數組/集合)
    計算損失(optional)
    計算w的梯度
    計算b的梯度
    更新w,b的值

第二種方式我們用了矩陣和標量的運算,以及矩陣和矩陣的運算。由於是批量樣本做為輸入,所以某些個樣本的誤差不會對整體造成影響。

使用第一種方式訓練

定義神經網路結構

對於簡單的線性迴歸問題,我們使用單層網路單個神經元就足夠了。而且由於是線性的,我們不需要定義啟用函式,這就大大簡化了程式,而且便於大家循序漸進地理解。

def forward_calculation(w,b,X):
    z = w * x + b
    return z

其中,由於X是一組資料(100個),所以它是一個向量,或者理解為一維陣列。w和b都是一個標量,Z的計算結果也是一個向量,尺寸和X一樣。

上面的寫法,實際上是每次迭代都用所有的樣本做訓練,因為輸入是X,是所有樣本的集合。還有另外一種做法,就是每次訓練,只用一個訓練樣本,那麼就需要在主迴圈中進行排程,一次使用一個樣本。

定義損失函式

我們用傳統的均方差函式: \(loss = \frac{1}{2}(z-y)^2\),其中,z是每一次迭代的預測輸出,y是樣本標籤資料。這個損失函式的直觀理解如下圖:

假設我們計算出初步的結果是紅色虛線所示,這條直線是否合適呢?我們來計算一下圖中每個點到這條直線的距離(黃色線),把這些距離的值都加起來(都是正數,不存在互相抵消的問題)成為loss,然後想辦法不斷改變紅色直線的角度和位置,讓loss最小,就意味著整體偏差最小,那麼最終的那條紅色直線就是我們要的結果。

下面是Python的code,用於計算損失:

# w:weight, y:sample data, m:count of sample
def loss_calculation(z,y):
    loss = (z-y)**2    # cannot use (Z-Y)^2
    cost = loss/2
    return cost

其實,這個loss值可以不用計算的,因為我們使用這個損失函式的目的是要反向傳播,而不是真的用這個loss值去做什麼具體的運算。具體的計算是體現在求導梯度的函式中。

搞明白為何用均方差MSE函式後,我們再看看MSE如何應用到反向傳播中。

定義針對w的梯度函式

因為:

\[z = wx+b\]

\[loss = \frac{1}{2}(z-y)^2\]

所以我們用loss的值作為基準,去求w對它的影響,也就是loss對w的偏導數:

\[ \frac{\partial{loss}}{\partial{w}} = \frac{\partial{loss}}{\partial{z}}*\frac{\partial{z}}{\partial{w}} \]

其中:

\[ \frac{\partial{loss}}{\partial{z}} = \frac{\partial{}}{\partial{z}}[\frac{1}{2}(z-y)^2] = z-y \]

而:

\[ \frac{\partial{z}}{\partial{w}} = \frac{\partial{}}{\partial{w}}(wx+b) = x \]

所以:

\[ \frac{\partial{loss}}{\partial{w}} = \frac{\partial{loss}}{\partial{z}}*\frac{\partial{z}}{\partial{w}} = (z-y)x \]

寫成code:

# w:weight, X,Y:sample data, m:count of sample
def dJw(z,y,x):
    dw = (z-y)*x
    return dw

定義針對b的梯度函式

因為:

\[Z = wX+b\]

\[loss = \frac{1}{2}(Z-Y)^2\]

所以我們用loss的值作為基準,去求w對它的影響,也就是loss對w的偏導數:

\[ \frac{\partial{loss}}{\partial{b}} = \frac{\partial{loss}}{\partial{Z}}*\frac{\partial{Z}}{\partial{b}} \]

其中:

\[ \frac{\partial{loss}}{\partial{Z}} = \frac{\partial{}}{\partial{Z}}[(z-y)^2] = z-y \]

而:

\[ \frac{\partial{z}}{\partial{b}} = \frac{\partial{(wx+b)}}{\partial{b}} = 1 \]

所以:

\[ \frac{\partial{loss}}{\partial{b}} = \frac{\partial{loss}}{\partial{Z}}*\frac{\partial{Z}}{\partial{b}} = z-y \]

# Z:predication value, Y:sample data, m:count of sample
def dJb(z,y):
    db = z - y
    return db

每次迭代後更新w,b的值

def update_weights(w, b, dw, db, eta):
    w = w - eta*dw
    b = b - eta*db
    return w,b

eta在本程式中恆等於0.1,這是隨機梯度下降法。也可以在迭代到一定次數後,把eta的值逐步減小,變成0.01,這樣會形成開始時大步前進,到後面時小步快跑的局面,利於訓練準確度提高。

初始化變數及引數

# initialize_data
# step for each iteration
eta = 0.1
# set w,b=0, you can set to others values to have a try
w = 0
b = 0

程式主迴圈

j是外迴圈的次數,先只訓練一次看看效果。

for j in range(1):
    for i in range(m):
        # get x and y value for one sample
        x = X[i]
        y = Y[i]
        # get z from x,y
        z = forward_calculation(w, b, x)
        # calculate lost (optional)
        #loss = loss_calculation(z, y)
        # calculate gradient of w and b
        dw = dJw(z, y, x)
        db = dJb(z, y)
        # update w,b
        w, b = update_weights(w, b, dw, db, eta)
        print(w,b)

程式執行結果如下:

0.11278289694938642 0.34606738038593576
0.4048467024747609 0.7556962791118582
0.46002740765953076 1.0072739217390403
0.6629665909817659 1.3133134892057239
......
1.8800223947881818 3.054203494854314
1.8798053702497237 3.0511601599536635
1.8722710744969668 3.0374415142130298
1.8761825822484357 3.0437127218759885

目標是w=2,b=3,看上去誤差還比較大。我們設定外迴圈次數為3,再看看效果。

#for j in range(1):
for j in range(3):
......
1.950551908871318 3.01593409919309
1.9512023724525054 3.0181474143771783
1.9512839743332555 3.0189251294358055
1.950145626519112 3.0148986986722246
1.9387958775488612 3.0023925945934153

貌似距離理想值更進了一步。但其實這兩次的結果不可比,因為我們每次都用新的隨機數做為樣本,而不是同一批隨機數。所以大家可以自己試著把隨機數儲存到檔案裡,每次訓練時讀出來,這樣就可以比較效果了。

使用第二種方式訓練

# use all the samples as a batch to train, then iteration on batch
import numpy as np
import matplotlib.pyplot as plt
# create mock up data
# count of samples
m = 100
X = np.random.random(m)
# create some offset as noise to simulate real data
noise = np.random.normal(0,0.1,X.shape)
W = 2
B = 3
Y = X * W + B + noise
plt.plot(X, Y, "b.")
plt.show()


# 由於X是一組資料(100個),所以它是一個向量,或者理解為一維陣列。w和b都是一個標量,Z的計算結果也是一個向量,尺寸和X一樣。
def forward_calculation(w,b,X):
    Z = w * X + b
    return Z

# 由於是m個訓練樣本批量訓練,所以結果要除以m,下同
# 注意X,Y,Z都是陣列
def loss_calculation(Z,Y,m):
    loss = (Z-Y)**2    # cannot use (Z-Y)^2
    cost = loss.sum()/m/2
    return cost

def dJw(Z,Y,X,m):
    q = (Z-Y)*X
    dw = sum(q)/m
    return dw

def dJb(Z,Y,m):
    q = Z - Y
    db = sum(q)/m
    return db

# w,b是標量,所以程式碼和第一種方式相同

def update_weights(w, b, dw, db, eta):
    w = w - eta*dw
    b = b - eta*db
    return w,b

# initialize_data
# step for each iteration
eta = 0.1
# set w,b=0, you can set to others values to have a try
w = 0
b = 0
# condition 1 to stop iteration: when Q - prevQ < error
error = 1e-10
prevQ = 10
# condition 2 to stop iteration
max_iteration = 10000
# counter of iteration
iteration = 0

# condition 2 to stop
while iteration < max_iteration:
    # using current w,b to calculate Z
    Z = forward_calculation(w,b,X)
    # compare Z and Y
    Q = loss_calculation(Z, Y, m)
    # get gradient value
    dW = dJw(Z, Y, X, m)
    dB = dJb(Z, Y, m)
    # update w and b
    w, b = update_weights(w, b, dW, dB, eta)
    print(iteration,w,b)
    iteration += 1
    # condition 1 to stop

#    if abs(Q - prevQ) < error:
#        break
    prevQ = Q

print(Q,prevQ)
print(w,b)

損失函式的微小變化

我們用傳統的均方差函式: \(loss = \frac{1}{2}(Z-Y)^2\),其中,Z是每一次迭代的預測輸出,Y是樣本標籤資料。我們使用所有樣本參與訓練,因此損失函式實際為:

\[loss = \frac{1}{2m}\sum_{i=1}^{m}(Z_i - Y_i) ^ 2\]

其中的分母中有個2,實際上是想在求導數時把這個2約掉,沒有什麼原則上的區別。

由於loss是所有樣本的集合,我們先對其中的所有值求總和,樣本數量是m,然後除以m來求一個平均值。

其實,這個loss值可以不用計算的,因為我們使用這個損失函式的目的是要反向傳播,而不是真的用這個loss值去做什麼具體的運算。具體的計算是體現在求導梯度的函式中。

定義針對w的梯度函式

因為:

\[Z = wX+b\]

\[loss = \frac{1}{2m}(Z-Y)^2\]

所以我們用loss的值作為基準,去求w對它的影響,也就是loss對w的偏導數:

\[ \frac{\partial{loss}}{\partial{w}} = \frac{\partial{loss}}{\partial{Z}}*\frac{\partial{Z}}{\partial{w}} \]

其中:

\[ \frac{\partial{loss}}{\partial{Z}} = \frac{\partial{}}{\partial{Z}}[\frac{1}{2m}(Z-Y)^2] = \frac{1}{m}(Z-Y) \]

而:

\[ \frac{\partial{z}}{\partial{w}} = \frac{\partial{}}{\partial{w}}(wX+b) = X \]

所以:

\[ \frac{\partial{loss}}{\partial{w}} = \frac{\partial{loss}}{\partial{Z}}*\frac{\partial{Z}}{\partial{w}} = \frac{1}{m}(Z-Y)X \]

寫成code:

# w:weight, X,Y:sample data, m:count of sample
def dJw(Z,Y,X,m):
    q = (Z-Y)*X
    # because w is a scalar, so dw should be a scalar too
    dw = sum(q)/m
    return dw

定義針對b的梯度函式

因為:

\[Z = wX+b\]

\[loss = \frac{1}{2m}(Z-Y)^2\]

所以我們用loss的值作為基準,去求w對它的影響,也就是loss對w的偏導數:

\[ \frac{\partial{loss}}{\partial{b}} = \frac{\partial{loss}}{\partial{Z}}*\frac{\partial{Z}}{\partial{b}} \]

其中:

\[ \frac{\partial{loss}}{\partial{Z}} = \frac{\partial{}}{\partial{Z}}[\frac{1}{2m}(Z-Y)^2] = \frac{1}{m}(Z-Y) \]

而:

\[ \frac{\partial{Z}}{\partial{b}} = \frac{\partial{(wX+b)}}{\partial{b}} = 1 \]

所以:

\[ \frac{\partial{loss}}{\partial{b}} = \frac{\partial{loss}}{\partial{Z}}*\frac{\partial{Z}}{\partial{b}} = \frac{1}{m}(Z-Y) \]

# Z:predication value, Y:sample data, m:count of sample
def dJb(Z,Y,m):
    q = Z - Y
    db = sum(q)/m
    return db

程式執行結果如下:

0 0.204633398307696 0.3943112518285292
1 0.3842082815875446 0.7395242688455653
2 0.5418315944143854 1.0417326666889068
3 0.6802247732855068 1.3062739239107688
......
935 2.014844080911897 2.9912924671594148
936 2.014846970318595 2.9912909940330077
937 2.014849838514886 2.991289531720451
938 2.014852685656469 2.991288080142363
939 2.0148555118979017 2.9912866392199446
940 2.014858317392607 2.9912852088749755
941 2.0148611022928815 2.9912837890298087
0.004078652569361402 0.004078652668164296
2.0148611022928815 2.9912837890298087

訓練過程迭代了941次,loss的前後差值小於1e-10了,達到了停止條件。可以看到最後w = 2.0148, b = 2.9912, 非常接近W=2, B=3的真實值。

也可以註釋掉condition 1,讓迭代達到10000次,但其實結果並不會好到哪裡去。

孔子說:點贊是人類的美德!如果覺得有用,關閉網頁前,麻煩您給點個贊!然後準備學習下一週的內容。