系列之8-單入單出的兩層神經網絡能作什麼

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

定義神經網絡結構

咱們定義一個兩層的神經網絡,輸入層不算,一個隱藏層,含128個神經元,一個輸出層。git

數學理論證實:具備足夠數量神經元的兩層神經網絡可以擬合任意精度的連續函數。因此,今天我們就用實際數據來驗證一下這個理論。咱們假設一個連續函數的形式爲:github

\[y=0.4x^2 + 0.3xsin(15x) + 0.01cos(50x)-0.3\]算法

輸入層

輸入層就是一個標量X值。網絡

權重矩陣W1/B1

它是鏈接兩層之間的紐帶,有的人理解它應該屬於輸入層,有的人理解應該屬於隱藏層,各有各的道理,我我的傾向於把它歸到隱藏層,理由是\(Z1=W1*X+B1\),在X固定的前提下,W1決定了Z1的值。另一個理由是B1的存在位置,在本例中B1是一個128x1的矩陣,它是隱藏層128個神經元的偏移,因此它應該屬於隱藏層。框架

其實這裏的B1所在的圓圈裏應該是個常數1,而B1鏈接到Z1-1...Z1-128的權重線B1-1...B1-128應該是個浮點數。咱們爲了說明問題方便,就寫了個B1,而實際的B1是指B1-1...B1-128的矩陣/向量。dom

W1的尺寸是128x1,B1的尺寸是128x1。函數

隱藏層

咱們用一個128個神經元的網絡來模擬函數,這個你們能夠本身試驗一下,把代碼中的神經元數量修改一下,而後在保持迭代次數和其它(超)參數不變的狀況,看看最終的精確度有何區別,訓練時間的差別,以及內存佔用有何差別。學習

每一個神經元的輸入\(Z1 = W1 * X + B1\),咱們在這裏使用雙曲sigmoid正切函數,因此輸出是\(A1 = sigmoid(Z1)\)。固然也可使用其它激活函數若是tanh, Relu等等。測試

權重矩陣W2/B2

與W1/B1相似,我我的認爲它屬於輸出層。W2的尺寸是1x128,B2的尺寸是1x1。

輸出層

因爲咱們只想完成一個擬合任務,因此輸出層只有一個神經元。它們的左側是\(Z2=W2*A1+B2\),右側是\(A2=Z2\)

爲何在最後一步沒有用激活函數,而是直接令A2=Z2呢?咱們後面再說。

創造訓練數據

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

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

def TargetFunction(x):
    p1 = 0.4 * (x**2)
    p2 = 0.3 * x * np.sin(15 * x)
    p3 = 0.01 * np.cos(50 * x)
    y = p1 + p2 + p3 - 0.3
    return y

def CreateSampleDataXY(m):
    S = np.random.random((m,2))
    S[:,1] = TargetFunction(S[:,0])
    return S

def CreateTestData(n):
    TX = np.linspace(0,1,100)
    TY = TargetFunction(TX)
    TZ = np.zeros(n)
    return TX, TY, TZ

其函數圖像在[0,1]之間的樣子是:

生成的數據格式以下:

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

其中,x就是上圖中藍色點的橫座標值,y是縱座標值。在[0,1]以外的函數曲線沒這麼複雜,彷佛擬合起來沒什麼難度,因此咱們特色選擇了[0,1]之間這一段來作試驗。

定義前向計算過程

至此,咱們獲得瞭如下一串公式:

\[Z1=W1*X+B1\]
\[A1=sigmoid(Z1)\]
\[Z2=W2*A1+B2\]
\[A2=Z2 \tag{這一步能夠省略}\]

def ForwardCalculation(x, dictWeights):
    W1 = dictWeights["W1"]
    B1 = dictWeights["B1"]
    W2 = dictWeights["W2"]
    B2 = dictWeights["B2"]

    Z1 = np.dot(W1,x) + B1
    A1 = sigmoid(Z1)
    Z2 = np.dot(W2,A1) + B2
    A2 = Z2  # 這一步能夠省略

    dictCache ={"A1": A1, "A2": A2}
    return A2, dictCache

因爲參數較多,因此咱們用一個dictionary(dictWeights)來保存W,B這些參數,若是是更多層的神經網絡,就會有更多的參數,咱們這裏使用的仍是一些最基本的參數。

定義代價函數

咱們用傳統的均方差函數: \(loss = \frac{1}{2}(Z-Y)^2\),其中,Z是每一次迭代的預測輸出,Y是樣本標籤數據。咱們使用全部樣本參與訓練,所以損失函數實際爲:

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

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

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

看一下計算圖,而後用鏈式求導法則反推:

求W2的梯度

由於:

\[Z2 = W2*A1+B2\]

\[Loss = \frac{1}{2}(Z2-Y2)^2\]

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

\[ \frac{\partial{Loss}}{\partial{W2}} = \frac{\partial{Loss}}{\partial{Z2}}*\frac{\partial{Z2}}{\partial{W2}} \]

其中:

\[ \frac{\partial{Loss}}{\partial{Z2}} = \frac{\partial{}}{\partial{Z2}}[\frac{(Z2-Y)^2}{2}] = Z2-Y \]

而:

\[ \frac{\partial{Z2}}{\partial{W2}} = \frac{\partial{}}{\partial{W2}}(W2*A1+B2) = A1^T \]

因此:

\[ \frac{\partial{Loss}}{\partial{W2}} = \frac{\partial{Loss}}{\partial{Z2}}*\frac{\partial{Z2}}{\partial{W2}} = (Z2-Y)*A1^T \]

矩陣求導的理論部分較爲複雜,請你們參考咱們的《基本數學導數公式》章節。

求B2的梯度

\[ \frac{\partial{Loss}}{\partial{B2}} = \frac{\partial{Loss}}{\partial{Z2}}*\frac{\partial{Z2}}{\partial{B2}} \]

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

\[ \frac{\partial{Z2}}{\partial{B2}} = \frac{\partial{(W2*A1+B2)}}{\partial{B2}} = 1 \]

因此:

\[ \frac{\partial{Loss}}{\partial{B2}} = \frac{\partial{Loss}}{\partial{Z2}}*\frac{\partial{Z2}}{\partial{B2}} = Z2-Y \]

求W1的梯度

由於:

\[A1 = sigmoid(Z1)\]

\[Z1 = W1*X+B1\]

對Z1求導:

\[ \frac{\partial{Loss}}{\partial{Z1}} = \frac{\partial{Loss}}{\partial{Z2}}*\frac{\partial{Z2}}{\partial{A1}}*\frac{\partial{A1}}{\partial{Z1}} \]

其中前面推導過:

\[ \frac{\partial{Loss}}{\partial{Z2}} = Z2-Y = dZ2 \]

而:

\[ \frac{\partial{Z2}}{\partial{A1}} = \frac{\partial{}}{\partial{A1}}(W2*A1+B2) = W2^T \]

\[ \frac{\partial{A1}}{\partial{Z1}} = \frac{\partial{}}{\partial{Z1}}(sigmoid(Z1)) = A1*(1-A1) \]

因此:

\[ \frac{\partial{Loss}}{\partial{Z1}} = W2^T * dZ2 * A1 * (1-A1) = dZ1 \]
而W1,B1的求導結果和W2,B2相似:

\[ \frac{\partial{Loss}}{\partial{W1}} = \frac{\partial{Loss}}{\partial{Z1}}*\frac{\partial{Z1}}{\partial{W1}}=dZ1*\frac{\partial{(W1*X+B1)}}{\partial{W1}}=dZ1*X^T \]

\[ \frac{\partial{Loss}}{\partial{B1}} = \frac{\partial{Loss}}{\partial{Z1}}*\frac{\partial{Z1}}{\partial{B1}}=dZ1*\frac{\partial{(W1*X+B1)}}{\partial{B1}}=dZ1 \]

變成代碼:

def BackPropagation(x, y, dictCache, dictWeights):
    A1 = dictCache["A1"]
    A2 = dictCache["A2"]
    W2 = dictWeights["W2"]

    dLoss_Z2 = A2 - y
    dZ2 = dLoss_Z2
    dW2 = dZ2 * A1.T
    dB2 = dZ2

    dZ2_A1 = W2.T * dZ2
    dA1_Z1 = A1 * (1 - A1)
    # dZ1 is dLoss_Z1
    dZ1 = dZ2_A1 * dA1_Z1
    dW1 = dZ1 * x
    dB1 = dZ1

    dictGrads = {"dW1":dW1, "dB1":dB1, "dW2":dW2, "dB2":dB2}
    return dictGrads

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

def UpdateWeights(dictWeights, dictGrads, learningRate):
    W1 = dictWeights["W1"]
    B1 = dictWeights["B1"]
    W2 = dictWeights["W2"]
    B2 = dictWeights["B2"]

    dW1 = dictGrads["dW1"]
    dB1 = dictGrads["dB1"]
    dW2 = dictGrads["dW2"]
    dB2 = dictGrads["dB2"]

    W1 = W1 - learningRate * dW1
    W2 = W2 - learningRate * dW2
    B1 = B1 - learningRate * dB1
    B2 = B2 - learningRate * dB2

    dictWeights = {"W1": W1,"B1": B1,"W2": W2,"B2": B2}

    return dictWeights

幫助函數

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

def sigmoid(x):
    s=1/(1+np.exp(-x))
    return s

def initialize_with_zeros(n_x,n_h,n_y):
    np.random.seed(2)
    # W1=np.random.randn(n_h,n_x)*0.00000001    # W1=np.random.randn(n_h,n_x)
    W1=np.random.uniform(-np.sqrt(6)/np.sqrt(n_x+n_h),np.sqrt(6)/np.sqrt(n_h+n_x),size=(n_h,n_x))
    # W1=np.reshape(32,784)
    B1=np.zeros((n_h,1))
    # W2=np.random.randn(n_y,n_h)*0.00000001  # W2=np.random.randn(n_y,n_h)
    W2=np.random.uniform(-np.sqrt(6)/np.sqrt(n_y+n_h),np.sqrt(6)/np.sqrt(n_y+n_h),size=(n_y,n_h))
    B2=np.zeros((n_y,1))

    assert (W1.shape == (n_h, n_x))
    assert (B1.shape == (n_h, 1))
    assert (W2.shape == (n_y, n_h))
    assert (B2.shape == (n_y, 1))

    dictWeights = {"W1": W1,"B1": B1,"W2": W2,"B2": B2}

    return dictWeights

主程序初始化

m = 1000
S = CreateSampleDataXY(m)
#plt.scatter(S[:,0], S[:,1], 1)
#plt.show()
n_input, n_hidden, n_output = 1, 128, 1
learning_rate = 0.1
eps = 1e-10
dictWeights = initialize_with_zeros(n_input, n_hidden, n_output)
max_iteration = 1000
loss, prev_loss, diff_loss = 0, 0, 0

程序主循環

for iteration in range(max_iteration):
    for i in range(m):
        x = S[i,0]
        y = S[i,1]
        A2, dictCache = ForwardCalculation(x, dictWeights)
        dictGrads = BackPropagation(x,y,dictCache,dictWeights)
        dictWeights = UpdateWeights(dictWeights, dictGrads, learning_rate)
    print("iteration", iteration)

測試並輸出擬合結果

tm = 100
TX, TY, TZ = CreateTestData(tm)
correctCount = 0
for i in range(tm):
    x = TX[i]
    y = TY[i]
    a2, dict = ForwardCalculation(x, dictWeights)
    TZ[i] = a2

plt.scatter(TX, TY)
plt.plot(TX, TZ, 'r')
str = str.format("cell:{0} sample:{1} iteration:{2} rate:{3}", n_hidden, m, max_iteration, learning_rate)
plt.title(str)
plt.show()

上面的TX是[0,1]之間的連續數,共100個,間隔相同。TY是更加被模擬的函數計算出來的精確值。TZ是咱們訓練的模型的預測值。咱們的目的就是要比較TY和TZ之間的差距。
下圖就是擬合結果,還比較使人滿意。

參數調整

常常聽人提及「調參」,此次我們親身經歷一下調參的痛(快)苦(樂)!咱們下面一切的比較都是如下面這組參數爲基準:

  1. 神經元數=128
  2. 輸入訓練數據量=1000
  3. 迭代次數=1000
  4. 權重調整步進值=0.1

以上這些標準值如何獲得呢?試了不少組合後獲得的,這就是所謂「試錯」的過程了。

神經元數量的變化(標準值128)

神經元數量=64

神經元數量=96

神經元數量=256,迭代次數=500

基準神經元數爲128,在96時,擬合效果不好,在64時,儘管咱們增長了迭代次數爲2000,仍然不好。
第三張圖,儘管神經元數量翻了一倍,成爲256個,可是迭代次數爲500,少了一倍,也會形成奇怪的結果。

樣本量的變化(標準值1000)

輸入數據量=500

輸入數據量=1500

樣本數據量不夠時,擬合效果很差。可是當樣本數據量超過必定值後,就沒多大做用了。

迭代次數的變化(標準值1000)

迭代次數=500

輸入數據量=1500

迭代次數少,擬合效果很差。迭代次數超過必定值後,容易形成過擬合,效果不大。

步長值的變化(標準值0.1)

步長=0.5

步長=0.01

步長值太大或者過小,都會形成很差的效果。

總結以下(效果5分爲最好):

神經元數量 樣本量 迭代次數 步長值 效果
0 128 1000 1000 0.1 5
1 64 1000 2000 0.1 3
2 96 1000 1000 0.1 2.5
3 256 1000 500 0.1 0
4 128 500 1000 0.1 2
5 128 1500 1000 0.1 5
6 128 1000 500 0.1 2.5
7 128 1000 1500 0.1 5
8 128 1000 1000 0.5 1
9 128 1000 1000 0.05 2

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

相關文章
相關標籤/搜索