系列之4-單入單出的一層神經網絡能作什

Copyright © Microsoft Corporation. All rights reserved.
適用於License版權許可
更多微軟人工智能學習資源,請見微軟人工智能教育與學習共建社區python

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

簡單回憶一下什麼是線性迴歸:github

迴歸的目的是經過幾個已知數據來預測另外一個數值型數據的目標值。假設特徵和結果知足線性關係,即知足一個計算公式y(x),這個公式的自變量就是已知的數據x,函數值y(x)就是要預測的目標值。這個計算公式稱爲迴歸方程,獲得這個方程的過程就稱爲迴歸。線性迴歸就是假設這個方式是一個線性方程,一個多元一次方程,其形式爲:
\[y=a_0+a_1x_1+a_2x_2+\dots+a_kx_k\]
爲了簡化起見,咱們用一元一次的線性迴歸來舉例,即\(z = wx+b\)(z,w,x,b都是標量),由於這個函數的形式和神經網絡中的\(Z = WX + B\)(Z,W,X,B等都是矩陣)很是近似,能夠起到用簡單的原理理解複雜的事情的做用。算法

創造訓練數據

讓咱們先自力更生創造一些模擬數據:數組

import numpy as np
import matplotlib.pyplot as plt
from pathlib import Path

def create_sample_data(m):
    # check if saved before
    Xfile = Path("XData.npy")
    Yfile = Path("YData.npy")
    # load data from file
    if Xfile.exists() & Yfile.exists():
        X = np.load(Xfile)
        Y = np.load(Yfile)
    else: # generate new data
        X = np.random.random(m)
        # create some offset as noise to simulate real data
        noise = np.random.normal(0,0.1,X.shape)
        # genarate Y data
        W = 2
        B = 3
        Y = X * W + B + noise
        np.save("XData.npy", X)
        np.save("YData.npy", Y)
    return X, Y

因爲使用了文件存儲,因此在第二次運行本程序時,或者在調試代碼時,先後的結果是可比的,由於是同一批數據。網絡

獲得200個數據點以下:app

好了,模擬數據製做好了,目前X是一個200個元素的集合,裏面有0~1之間的隨機x點,Y是一個200個元素的集合,裏面有對應到每一個x上的\(y=2x+3\)的值,而後再加一個或正或負的上下偏移做爲噪音,來知足對實際數據的模擬效果(由於大部分真實世界的生產數據歷來都不是精確的,精確只存在於數學領域)。框架

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

最終,樣本數據的樣子是:機器學習

\[ \begin{pmatrix} x_1\\ x_2\\ \dots\\ x_m\\ \end{pmatrix} , \begin{pmatrix} y_1\\ y_2\\ \dots\\ y_m\\ \end{pmatrix} \]

其中,x就是上圖中藍色點的橫座標值,y是縱座標值。

最小二乘法與均方差

線性迴歸試圖學得 \(z(x_i)=wx_i+b\),使得\(z(x_i) \simeq y_i\)。如何學得w和b呢?均方差(MSE - mean squared error)是迴歸任務中經常使用的手段:
\[ Error = \frac{1}{m}\sum_{i=1}^m(z(x_i)-y_i)^2 = \frac{1}{m}\sum_{i=1}^m(y_i-wx_i-b)^2 \]
其中,\(x_i和y_i\)是樣本值,\(z_i\)是預測值。
實際上就是試圖找到一條直線,使全部樣本到直線上的歐氏距離之和最小。

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

若是想讓Error的值最小,經過對w和b求導,再令導數爲0(到達最小極值),就是w和b的最優解:

\[ w = \frac{\sum{y_i(x_i-\bar{x})}}{\sum{x_i^2}-\frac{1}{m}(\sum{x_i})^2}\tag{求和均爲i=1到m} \]
\[ b=\frac{1}{m}\sum_{i=1}^m(y_i-wx_i) \]

咱們先試一下上面這兩個公式是否好用:

x_sum = sum(X)                          # 求x之和
x_mean = x_sum/m                        # 求x平均
x_square = sum(X*X)                     # 求x平方之和
x_square_mean = x_sum * x_sum / m       # 求x之和之平方之均
xy = sum(Y*(X-x_mean))                  # 求w的公式的分子部分
w = xy / (x_square - x_square_mean)     # 求w
print(w)
b = sum(Y-w*X) / m                      # 求b
print(b)

結果爲:
w=1.9983541668129974
b=3.0128994960012876

能夠看到很是接近w=2,b=3的原始值。
既然咱們已經能夠用純數學方法的最小二乘法獲得w,b的值,爲何還要學機器學習的方法呢?由於最小二乘法能作的事情有兩種:
\[y=a_0+a_1x+a_2x^2+ \dots + a_mx^m \tag{單元x屢次方程}\]
\[y=a_0+a_1x_1+a_2x_2+ \dots + a_mx_m \tag{多元x線性方程}\]
前提條件是咱們預測到方程的形式。可是更復雜的形式就比較吃力甚至無能爲力了,好比:

\[y=0.4x^2 + 0.3xsin(15x) + 0.01cos(50x)-0.3\]
\[y=3x_1^2 + 4x_2\]

而在客觀世界中或實際的生產環境中,咱們其實根本不知道要擬合的曲線是什麼形式,就根本無從下手,這時只能用神經網絡來擬合了,而擬合的結果也不是一個公式,而是一個神經網絡模型。

定義神經網絡結構

咱們是首次嘗試創建神經網絡,先搞一個最簡單的單點神經元:

對於簡單的線性迴歸問題,咱們使用單層網絡單個神經元就足夠了。並且因爲是線性的,咱們不須要定義激活函數,這就大大簡化了程序,並且便於你們按部就班地理解。

樣本數據x,乘以相同的w值後相加,再加上偏移b,輸出z。

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

定義代價函數

咱們用傳統的均方差函數: \(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來求一個平均值。

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

# w:weight, b:bias, X,Y:sample data, count: count of sample, prev_loss:last time's loss
def check_diff(w, b, X, Y, count, prev_loss):
    Z = w * X + b
    LOSS = (Z - Y)**2
    loss = LOSS.sum()/count/2
    diff_loss = abs(loss - prev_loss)
    return loss, diff_loss

咱們計算這個loss值的目的是計算先後兩次迭代的loss值差別,當足夠小時,就結束訓練。

定義針對w和b的梯度函數

求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{(\frac{1}{2}(z-y)^2)}}{\partial{z}} = 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 \]

求b的梯度

因此咱們用loss的值做爲基準,去求w對它的影響,也就是loss對w的偏導數:

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

其中第一項前面算w的時候已經有了,而:

\[ \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 label, x:sample data, count:count of sample data
def dJwb_batch(X,Y,Z,count):
    p = Z - Y
    db = sum(p)/count
    q = p * X
    dw = sum(q)/count
    return dw, db

def dJwb_single(x,y,z):
    p = z - y
    db = p
    dw = p * x
    return dw, db

上面有兩個求梯度函數,第一個用於數組數據(當輸入的X/Y/Z都是數組時),第二個用於標量數據(x/y/z都是標量),但最後輸出的dw/db都是標量,由於只有一個神經元。

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

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

幫助函數

第一個show_result函數用於最後輸出結果。第二個print_progress函數用於訓練過程當中的輸出。

def show_result(X, Y, w, b, iteration, loss_his, w_his, b_his, n):
    # draw sample data
#    plt.figure(1)
    plt.subplot(121)
    plt.plot(X, Y, "b.")
    # draw predication data
    Z = w*X +b
    plt.plot(X, Z, "r")
    plt.subplot(122)
    plt.plot(loss_his[0:n], "r")
    plt.plot(w_his[0:n], "b")
    plt.plot(b_his[0:n], "g")
    plt.grid(True)
    plt.show()
    print(iteration)
    print(w,b)

def print_progress(iteration, loss, diff_loss, w, b, loss_his, w_his, b_his):
    if iteration % 10 == 0:
        print(iteration, diff_loss, w, b)
    loss_his = np.append(loss_his, loss)
    w_his = np.append(w_his, w)
    b_his = np.append(b_his, b)
    return loss_his, w_his, b_his

主程序初始化

# count of samples
m = 200
# initialize_data
eta = 0.01
# set w,b=0, you can set to others values to have a try
w, b = 0, 0
eps = 1e-10
iteration, max_iteration = 0, 10000
# calculate loss to decide the stop condition
prev_loss, loss, diff_loss = 0,0,0
# create mock up data
X, Y = create_sample_data(m)
# create list history
loss_his, w_his, b_his = list(), list(), list()

訓練方式的選擇

接下來,咱們會用三種方式來訓練神經網絡(神經元):

  1. 隨機梯度降低SGD (Stochastic Gradient Descent):每次迭代只使用一個樣本進行訓練,每次都更新梯度值
  2. 批量梯度降低BGD (Batch Gradient Descent):把全部樣本整批的輸入網絡進行訓練,每批樣本更新一次梯度值
  3. 小批量梯度降低MBGD (Mini-batch Gradient Descent):每次用一部分樣本進行訓練,每小批樣本更新一次梯度值

Pseudo code以下:

第一種方式:逐個樣本訓練即隨機梯度降低

repeat:
    for 每一個樣本x,y:
        標量計算獲得z的單值 z = w * x + b
        計算w的梯度
        計算b的梯度
        更新w,b的值
        計算本次損失
        與上一次的損失值比較,足夠小的話就中止訓練
    end for
until stop condition

第二種方式:批量樣本訓練即批量梯度降低

repeat:
    矩陣前向計算獲得Z值 = w * X + b(其中X是全部樣本的數組)
    計算w的梯度
    計算b的梯度
    更新w,b的值
    計算本批損失
    與上一批的損失值比較,足夠小的話就中止訓練
until stop condition

第三種方式:小批量樣本訓練即批量梯度降低

repeat:
    從樣本集X中得到一小批量樣本Xn
    矩陣前向計算獲得Z值 = w * Xn + b(其中Xn是一小批樣本的數組)
    計算w的梯度
    計算b的梯度
    更新w,b的值
    計算本批損失
    與上一批的損失值比較,足夠小的話就中止訓練
until stop condition

咱們看完它們的訓練結構後再來比較它門的好壞。

隨機梯度降低方式 - SGD

針對200個數據,每次迭代只使用一個樣本進行訓練,每次都更新梯度值。

程序主循環

while iteration < max_iteration:
    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 gradient of w and b
        dw, db = dJwb_single(x, y, z)
        # update w,b
        w, b = update_weights(w, b, dw, db, eta)
        # calculate loss for this batch
        loss, diff_loss = check_diff(w,b,X,Y,m,prev_loss)
        # condition 1 to stop
        if diff_loss < eps:
            break
        prev_loss = loss
        
    iteration += 1
    loss_his, w_his, b_his = print_progress(iteration, loss, diff_loss, w, b, loss_his, w_his, b_his)
    if diff_loss < eps:
        break

show_result(X, Y, w, b, iteration, loss_his, w_his, b_his, 200)

程序運行結果

1 0.0013946089980010831 1.7082689753500857 2.8635473444149815
2 1.2964547916170625e-05 1.8540100768184453 3.06775776515801
3 7.79019593934345e-07 1.8807160337440225 3.0745103188170186
......
19 8.734980997196495e-09 1.9871421670235265 3.0189893623564035
20 6.770768725197773e-09 1.9888753203686051 3.0180574393623383
21 1.4217967081453509e-13 1.9909568305589769 3.0231282539481192
21
1.9909568305589769 3.0231282539481192

一共迭代了21次(實際是21*200次),因爲diff_loss小於1e-10,因此中止了。可是,能夠看到w=1.99..,b=3.023...,與實際值w=2, b=3還有差距。
下圖右側圖,紅色線是loss值的變化,藍色線是w值的變化,綠色線是b值的變化。這三個值都很快從初始值趨近於理想值,可見這種方式的收斂速度較快。

批量梯度降低方式 - BGD

程序主循環

# condition 2 to stop
while iteration < max_iteration:
    # using current w,b to calculate Z
    Z = forward_calculation(w,b,X)
    # get gradient value
    dW, dB = dJwb_batch(X, Y, Z, m)
    # update w and b
    w, b = update_weights(w, b, dW, dB, eta)
#   print(iteration,w,b)
    iteration += 1
    # condition 1 to stop
    loss, diff_loss = check_diff(w,b,X,Y,m,prev_loss)
    if diff_loss < eps:
        break
    prev_loss = loss
    iteration += 1
    loss_his, w_his, b_his = print_progress(iteration, loss, diff_loss, w, b, loss_his, w_his, b_his)

show_result(X, Y, w, b, iteration, loss_his, w_his, b_his, 200)

程序運行結果

15580 1.0078619969849933e-10 1.9970527059142416 3.013622182124774
15590 1.0010891421385892e-10 1.9970570862183055 3.0136197497927952
15591
1.9970579605084025 3.013619264309657

訓練過程迭代了15591次,loss的先後差值小於1e-10了,達到了中止條件。能夠看到最後w = 1.997, b = 3.0136。
下圖右側圖,紅色線是loss值的變化,藍色線是w值的變化,綠色線是b值的變化。到第400次迭代時,w/b兩個值尚未到達理想值。loss的降低速度很快,可是在後期的變化很小,不能給w/b提供有效的反饋。

小批量梯度降低方式 - MBGD

程序主循環

batchNumber = 20 # 設置每批的數據量爲20

# condition 2 to stop
while iteration < max_iteration:
    # generate current batch
    batchX, batchY = generate_batch(X, Y, iteration, batchNumber, m)
    # using current w,b to calculate Z
    Z = forward_calculation(w,b,batchX)
    # get gradient value
    dW, dB = dJwb_batch(batchX, batchY, Z, batchNumber)
    # update w and b
    w, b = update_weights(w, b, dW, dB, eta)
    # calculate loss
    loss, diff_loss = check_diff(w,b,X,Y,m,prev_loss)
    # condition 1 to stop
    if diff_loss < eps:
        break
    prev_loss = loss
    iteration += 1

    loss_his, w_his, b_his = print_progress(iteration, loss, diff_loss, w, b, loss_his, w_his, b_his)

show_result(X, Y, w, b, iteration, loss_his, w_his, b_his, 300)

程序運行結果

4450 1.2225522157127688e-10 1.9753608229361126 3.0087345373193264
4460 1.1451271614976166e-10 1.9753871799671467 3.008717691199392
4470 1.068962461950318e-10 1.975413384400498 3.0087009426124025
4479
1.975439437119652 3.0086842909936786

訓練過程迭代了4479次,最後w=1.9754, b=3.0086。
下圖右側圖,紅色線是loss值的變化,藍色線是w值的變化,綠色線是b值的變化。到第300次迭代時,w/b值已經接近理想值了。loss的降低速度慢,可是在後期仍然能夠給w/b有效的反饋。

三種方式的比較

下圖是三種方式在向目標解迭代靠近時的示意圖:

隨機梯度降低

  1. 每次用一個樣本訓練,而後馬上更新權重,訓練速度最快。能夠簡單地理解爲「神通過敏」性格。
  2. 能夠設置一個適中(更多)的迭代次數,以便獲得更好的解
  3. 因爲使用單個樣本數據,會受數據噪音的影響,且先後兩個樣本的訓練效果可能會相互抵消。從軌跡上來看,跳躍性較大。
  4. 因爲數據隨機,因此有可能受訓練樣本噪音影響而跳到全局最優解,可是不保證。在某些博客中說「本方法只能得到局部最優解」,這其實是不對的。

批量梯度降低

  1. 每次用整批樣本訓練後,才更新一次權重,訓練速度最慢。能夠簡單地理解爲「老成持重」性格。
  2. 特定的樣本若是偏差較大,不會影響總體的訓練質量
  3. 從軌跡上來看,比較平穩地接近中心,可是在接近最優解時的迭代次數太多,很是緩慢
  4. 若是隻有一個極小值,能夠獲得相對全局較優的解。若是實際數據有兩個極小值,不必定能獲得全局最優解。在某些博客中說「本方法能夠得到全局最優解」,這其實是不能保證的,取決於初始值設置。

小批量梯度降低

  1. 每次使用一小批數據訓練,速度適中。能夠簡單地理解爲「穩重而不失靈活」的性格。
  2. 多了一個batchNumber參數須要設置,你們能夠試驗一下分別設置爲10,20,25,40,50,訓練效果如何
  3. 從軌跡上來看,有小的跳躍,可是降低方向還算是穩定,不會向反方向跳躍

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

點擊這裏提交問題與建議
聯繫咱們: msraeduhub@microsoft.com
學習了這麼多,還沒過癮怎麼辦?歡迎加入「微軟 AI 應用開發實戰交流羣」,跟你們一塊兒暢談AI,答疑解惑。掃描下方二維碼,回覆「申請入羣」,即刻邀請你入羣。

相關文章
相關標籤/搜索