1. 程式人生 > >系列之5-多入單出的一層神經網路能做什麼

系列之5-多入單出的一層神經網路能做什麼

全套教程請點選:微軟 AI 開發教程

知識點

  • 多元線性迴歸
  • 輸入層的矩陣運算設計
  • 樣本資料歸一化

提出問題

假設有如下這樣一個問題:有1000個樣本,每個樣本有三個特徵值,一個標籤值。想讓你預測一下給定任意三個特徵值組合,其y值是多少?

樣本序號 1 2 3 4 ... 1000
樣本特徵值1 1 4 2 4 ... 2
樣本特徵值2 3 2 6 3 ... 3
樣本特徵值3 96 100 54 72 ... 69
樣本標籤值y 434 500 321 482 ... 410

這就是典型的多元線性迴歸。函式模型如下:

\[y=a_0+a_1x_1+a_2x_2+\dots+a_kx_k\]

為了方便大家理解,咱們具體化一下上面的公式,按照本系列文章的符號約定就是:

\[ Z = w_1x_1+w_2x_2+w_3x_3+b = WX + B \]

定義神經網路結構

我們定義一個一層的神經網路,輸入層為3或者更多,反正大於2了就沒區別。這個一層的神經網路沒有中間層,只有輸出層,而且只有一個神經元,並且神經元有一個線性輸出,不經過啟用函式處理。亦即在下圖中,經過\(\Sigma\)

求和得到Z值之後,直接把Z值輸出。

這樣的一個神經網路能做什麼事情呢?

輸入層

假設一共有m個樣本,每個樣本n個特徵值,X就是一個\(n \times m\)的矩陣,模樣是這樣紫的(n=3,m=1000,亦即3行1000列):

\[ X = \begin{pmatrix} x1_1 & x2_1 & \dots & xm_1 \\ \\ x1_2 & x2_2 & \dots & xm_2 \\ \\ x1_3 & x2_3 & \dots & xm_3 \end{pmatrix} = \begin{pmatrix} 3 & 2 & \dots & 3 \\ \\ 1 & 4 & \dots & 2 \\ \\ 96 & 100 & \dots & 54 \end{pmatrix} \]

\[ Y = \begin{pmatrix} y1 & y2 & \dots & ym \\ \end{pmatrix}= \begin{pmatrix} 434 & 500 & \dots & 410 \\ \end{pmatrix} \]

單獨看一個樣本是這樣的:

\[ x1 = \begin{pmatrix} x1_1 \\ \\ x1_2 \\ \\ x1_3 \end{pmatrix} = \begin{pmatrix} 3 \\ \\ 1 \\ \\ 96 \end{pmatrix} \]

\[ y1 = \begin{pmatrix} 434 \end{pmatrix} \]

\(x1\)表示第一個樣本,\(x1_1\)表示第一個樣本的一個特徵值。

權重W和B

有人問了,為何不把這個表格轉一下,變成橫向是樣本特徵值,縱向是樣本數量?那樣好像更符合思維習慣?

確實是!但是在實際的矩陣運算時,由於是\(Z=W*X+B\),W在前面,X在後面,所以必須是這個樣子的:

\[ \begin{pmatrix} w1 & w2 & w3 \end{pmatrix} \times \begin{pmatrix} x1_1 \\ \\ x1_2 \\ \\ x1_3 \end{pmatrix}+B= w1*x1_1+w2*x1_2+w3*x1_3+B \]

假設每個樣本X有n個特徵向量,上式中的W就是一個\(1 \times n\)行向量,讓每個w都對應一個x:
\[ \begin{pmatrix}w_1 & w_2 \dots w_n\end{pmatrix} \]

B是個單值,因為只有一個神經元,所以只有一個bias,每個神經元對應一個bias,如果有多個神經元,它們都會有各自的b值。

輸出層

由於我們只想完成一個迴歸(擬合)任務,所以輸出層只有一個神經元。由於是線性的,所以沒有用啟用函式。

收集訓練資料

房價預測問題,成為了機器學習的一個入門話題。我們也不能免俗,但是,不要用美國的什麼多少平方英尺,多少個房間的例子來說事兒了,中國人不能理解。我們來個北京的例子!

影響北京房價的因素有很多,幾個最重要的因子和它們的取值範圍是:

  • 朝向(在北方地區,窗戶面向陽光的房子要搶手一些):北=1,西=2,東=3,南=4
  • 地理位置:二環,三環,四環,五環,六環,分別取值為2,3,4,5,6,二環的房子單價最貴
  • 面積:40~120平米,連續值

由於一些原因,收集的資料不能用於教學,所以咱們根據以上規則創造一些資料:

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

# y = w1*x1 + w2*x2 + w3*x3 + b
# W1 = 朝向:1,2,3,4 = N,W,E,S
# W2 = 位置幾環:2,3,4,5,6
# W3 = 面積:平米
def TargetFunction(x1,x2,x3):
    w1,w2,w3,b = 2,10,5,10
    return w1*x1 + w2*(10-x2) + w3*x3 + b

def CheckFileData():
    Xfile = Path("HouseXData.npy")
    Yfile = Path("HouseYData.npy")
    if Xfile.exists() & Yfile.exists():
        XData = np.load(Xfile)
        YData = np.load(Yfile)
        return True,XData,YData
    
    return False,None,None

def create_sample_data(m):
    flag,XData,YData = CheckFileData()
    if flag == False:
        X0 = np.random.randint(1,5,m)
        X1 = np.random.randint(2,7,m)
        X2 = np.random.randint(40,120,m)
        XData = np.zeros((3,m))
        XData[0] = X0
        XData[1] = X1
        XData[2] = X2
        Y = TargetFunction(X0,X1,X2)
        noise = 20
        Noise = np.random.randint(1,noise,(1,m)) - noise/2
        YData = Y.reshape(1,m)
        np.save("HouseXData.npy", XData)
        np.save("HouseYData.npy", YData)
    return XData, YData

在TargetFunction函式中,\(w2*(10-x2)\),是因為越靠近二環的房子越貴,所以當x2=2時(二環),10-2=8;當x2=6時(六環),10-6=4。

令w1=2, w2=10, w3=5, b=10,所以最後的公式是:
\[z = w1·x_1 + w2·(10-x_2)+w3·x_3+b \\ = 2x_1 - 10(10-x_2)+5x_3+10 \\ = 2x_1 - 10x_2+5x_3+110 \]

定義前向計算過程

def forward_calculation(Xm,W,b):
    z = np.dot(W, Xm) + b
    return 

定義代價函式

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

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

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

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

求針對W和B的梯度函式

求解W和B的梯度方法與我們前面的文章“單入單出的一層神經網路”完全一樣,所以不再贅述,只說一下結論:
因為:

\[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}} = (z-y)x \]

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

變成程式碼:

def dJwb_single(Xm,Y,Z):
    dloss_z = Z - Y
    db = dloss_z
    dw = np.dot(dloss_z, Xm.T)
    return dw, db

每次迭代後更新W,B的值

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

主程式初始化

m = 1000    # 創造1000個樣本
XData, Y = create_sample_data(m)
X = XData
n = X.shape[0]  # 每個樣本的特徵數
eta = 0.1   # 學習率0.1
loss, diff_loss, prev_loss = 10, 10, 5
eps = 1e-10
max_iteration = 100 # 最多100次迴圈
# 初始化w,b
b = np.zeros((1,1))
w = np.zeros((1,n))

程式主迴圈

for iteration in range(max_iteration):
    for i in range(m):
        Xm = X[0:n,i].reshape(n,1)
        Ym = Y[0,i].reshape(1,1)
        Z = forward_calculation(Xm, W, B)
        dw, db = dJwb_single(Xm, Ym, Z)
        W, B = update_weights(W, B, dw, db, eta)
        
        loss, diff_loss = check_diff(w,b,xxx,y,1,prev_loss)
        if diff_loss < eps:
            print(i)
            break
        prev_loss = loss
    print(iteration, w, b, diff_loss)
    if diff_loss < eps:
        break

懷著期待的心情用顫抖的右手按下了執行鍵......but......what happened?

C:\Users\Python\LinearRegression\MultipleInputSingleOutput.py:61: RuntimeWarning: overflow encountered in square
  LOSS = (Z - Y)**2
C:\Users\Python\LinearRegression\MultipleInputSingleOutput.py:63: RuntimeWarning: invalid value encountered in double_scalars
  diff_loss = abs(loss - prev_loss)
C:\Users\Python\LinearRegression\MultipleInputSingleOutput.py:55: RuntimeWarning: invalid value encountered in subtract
  w = w - eta*dw
0 [[nan nan nan]] [[nan]] nan
1 [[nan nan nan]] [[nan]] nan
2 [[nan nan nan]] [[nan]] nan
3 [[nan nan nan]] [[nan]] nan

怎麼會overflow呢?於是右手的顫抖沒有停止,左手也開始顫抖了。

我們遇到了傳說中的梯度爆炸!數值太大,導致計算溢位了。第一次遇到這個情況,但相信不會是最後一次,因為這種情況在神經網路中太常見了。技術只服務於相信技術的人,技能只給與培養技能的人!別慌,讓我們debug一下。

解決梯度爆炸

檢查迭代中的數值變化情況

先把迭代中的關鍵值打印出來:

0 -----------
Z: [[0.]]
Y: [[469]]
dLoss/dZ: [[-469.]]
dw: [[  -938.  -1876. -37051.]]
db: [[-469.]]
W: [[  93.8  187.6 3705.1]]
B: [[46.9]]
1 -----------
Z: [[289982.7]]
Y: [[464]]
dLoss/dZ: [[289518.7]]
dw: [[  579037.4         1158074.8        22582458.60000001]]
db: [[289518.7]]
W: [[  -57809.94  -115619.88 -2254540.76]]
B: [[-28904.97]]
2 -----------
Z: [[-2.62364972e+08]]
Y: [[634]]
dLoss/dZ: [[-2.62365606e+08]]
dw: [[-5.24731213e+08 -1.57419364e+09 -3.04344103e+10]]
db: [[-2.62365606e+08]]
W: [[5.24153113e+07 1.57303744e+08 3.04118649e+09]]
B: [[26207655.65900001]]
......

最開始的W,B的值都是0,三次迭代後,W,B的值已經大的超乎想象了。可以停止執行程式了,想一想為什麼。

難道是因為學習率太大嗎?目前是0.1,設定成0.01試試看:

0 ----------
Z: [[0.]]
Y: [[469]]
dLoss/dZ: [[-469.]]
dw: [[  -938.  -1876. -37051.]]
db: [[-469.]]
W: [[ 0.938  1.876 37.051]]
B: [[0.469]]
1 -----------
Z: [[2899.827]]
Y: [[464]]
dLoss/dZ: [[2435.827]]
dw: [[  4871.654   9743.308 189994.506]]
db: [[2435.827]]
W: [[  -3.933654   -7.867308 -152.943506]]
B: [[-1.966827]]
2 ----------
Z: [[-17798.484679]]
Y: [[634]]
dLoss/dZ: [[-18432.484679]]
dw: [[  -36864.969358  -110594.908074 -2138168.222764]]
db: [[-18432.484679]]
W: [[  32.93131536  102.72760007 1985.22471676]]
B: [[16.46565768]]

沒啥改進。

回想一個問題:為什麼在“單入淡出的一層神經網路”一文的程式碼中,我們沒有遇到這種情況?因為所有的X值都是在[0,1]之間的,而神經網路是以樣本在事件中的統計分佈概率為基礎進行訓練和預測的,也就是說,樣本的各個特徵的度量單位要相同。我們並沒有辦法去比較1米和1公斤的區別,但是,如果我們知道了1米在整個樣本中的大小比例,以及1公斤在整個樣本中的大小比例,比如一個處於0.2的比例位置,另一個處於0.3的比例位置,就可以說這個樣本的1米比1公斤要小。這就提出了樣本的歸一化或者正則化的理論。

資料歸一化

更多的資料歸一化的問題,我們會另文給出,下面只提對我們解決當前問題有用的方法。

min-max標準化

也叫離差標準化,是對原始資料的線性變換,使結果落到[0,1]區間,轉換函式如下:

\[ x_{new} = \frac{x-x_{min}}{x_{max}-x_{min}} \]

其中max為樣本資料的最大值,min為樣本資料的最小值。
如果想要將資料對映到[-1,1],則將公式換成:
\[ x_{new} = \frac{x-x_{mean}}{x_{max}-x_{min}} \]
mean表示資料的均值。

樣本分析

再把這個表拿出來分析一下:
|樣本序號|1|2|3|4|...|1000|
|---|---|----|---|--|--|--|
|樣本特徵值1:窗戶朝向|1|3|2|4|...|2|
|樣本特徵值2:地理位置|3|2|6|3|...|4|
|樣本特徵值3:居住面積|96|100|54|72|...|69|
|樣本標籤值y:房價(萬元)|434|500|321|482|...|410|

  • 特徵值1 - 窗戶朝向
    一共有”東”“南”“西”“北“四個值,用數字化表示是:
    東:3
    南:4
    西:2
    北:1
    因為超南的房子比較貴,其次為東,西,北。為啥東比西貴?因為夏天時朝西的窗戶西晒時間長,比較熱。

  • 特徵值2 - 地理位置
    二環:2 - 單價最貴
    三環:3
    四環:4
    五環:5
    六環:6 - 單價最便宜

  • 特徵值3 - 房屋面積
    統計所有樣本資料得到房屋的面積範圍是[40,120]

我們用min-max標準化來歸一以上資料,得到下表:

樣本序號 1 2 3 4 ... 1000
樣本特徵值1:窗戶朝向 0 0.667 0.333 1 ... 0.33
樣本特徵值2:地理位置 0.25 0 1 0.25 ... 0.75
樣本特徵值3:居住面積 0.7 0.75 0.175 0.4 ... 0.36
樣本標籤值y:房價(萬元) 434 500 321 482 ... 410

還有一個問題:標籤值y是否需要歸一化?沒有任何理論說需要歸一化標籤值,所以我們先把這個問題放一放。

資料歸一化的好處

下圖展示了歸一化前後的情況,左側為前,右側為後:

房屋面積的取值範圍是[40,120],而地理位置的取值範圍是[2,6],二者會形成一個很扁的橢圓,如左側。這樣在尋找最優解的時候,過程會非常曲折。運氣不好的話,如同我們上面的程式碼,根本就沒法訓練。

歸一化後,地理位置和房屋面積二者都歸一到[0,1]之間,變成了可比的了,而且尋找最優解的路徑也很直接,節省了時間。

歸一化的實現

我們把歸一化的函式寫好:

def Normalize(X):
    X_new = np.zeros(X.shape)
    n = X.shape[0]
    w_num = np.zeros((1,n))
    for i in range(n):
        v = X[i,:]
        max = np.max(v)
        min = np.min(v)
        w_num[0,i] = max - min
        v = (v - min)/(max-min)
        X_new[i,:] = v
    return X_new, w_num

然後再改一下主程式,加上歸一化的呼叫:

m = 1000
XData, Y = create_sample_data(m)
X, W_num = Normalize(XData)
n = X.shape[0]
eta = 0.1
loss, diff_loss, prev_loss = 10, 10, 5
eps = 1e-10
max_iteration = 1

B = np.zeros((1,1))
W = np.zeros((1,n))

for iteration in range(max_iteration):
    for i in range(m):
        Xm = X[0:n,i].reshape(n,1)
        Ym = Y[0,i].reshape(1,1)
        Z = forward_calculation(Xm, W, B)
        dw, db = dJwb_single(Xm, Ym, Z)
        W, B = update_weights(W, B, dw, db, eta)
        loss, diff_loss = check_diff(W,B,Xm,Ym,1,prev_loss)
        if diff_loss < eps:
            print(i)
            break
        prev_loss = loss
    print(iteration, W, B, diff_loss)
    if diff_loss < eps:
        break

用顫抖的雙手同時按下Ctrl+F5,執行開始,結束,一眨眼!仔細看列印結果:

[[ 5.94417016 -40.05847929 394.83396979]] [[292.15951172]]

\[ w1=5.94417016 \\ w2=-40.05847929 \\ w3=394.83396979 \\ b=292.15951172 \]

比較一下原始公式:
\[z = w1·x_1 + w2·(10-x_2)+w3·x_3+b \\ = 2x_1 - 10(10-x_2)+5x_3+10 \\ = 2x_1 - 10x_2+5x_3+110 \\ w1=2 \\ w2=-10 \\ w3=5 \\ b=110 \]

什麼鬼!怎麼相差這麼多?!

歸一化的後遺症

仔細想想,訓練居然收斂了,出結果了,但是和我們預期的不太一致。我們沒有做歸一化前,根本沒結果。歸一化後有結果,那麼就是歸一化起作用了,但它有什麼副作用呢?一起逐個分析一下4個值。

w1是窗戶朝向,取值範圍[1,4],即:\(x_{min}=1, x_{max}=4\),目前\(w1=5.94417\),試著變換一下:

\[ \frac{w1}{x_{max}-x_{min}} = \frac{5.94417}{4-1}=1.98139 \simeq 2 \]
是不是和w1=2這個值非常的接近呢?!WoW!我從澡盆裡跳了出來,給阿基米德打了一個電話,說:“我找到啦!”

再看W2是地理位置:
\[ \frac{w2}{x_{max}-x_{min}} = \frac{-40.058}{6-2}=-10.0145 \simeq -10 \]

再看W3是房屋面積:
\[ \frac{w3}{x_{max}-x_{min}} = \frac{394.8339}{119-40}=4.997 \simeq 5 \]

程式碼如下:

W_rel = np.zeros((1,n))
for i in range(n):
    W_rel[0,i] = W[0,i] / W_num[0,i]
print(W_rel)

其中W_num是在呼叫Normalize函式時返回的三個特徵值的範圍,亦即(3,4,79)。

再看b值......"元芳,你怎麼看?"

“卑職以為,b值不是樣本值,沒有經過歸一化。”

“你說得很對!但我們怎麼解出b呢?”

“大人,這個好辦,我們如此這樣這樣......把一堆樣本值代入w1,w2,w3的公式,令b=0,看看計算出來的結果是什麼,再和樣本標籤值去比較就可以了!”
\[ z = w1*x_1+w2*x_2+w3*x_3 +0\\ b = y - z \]

為了避免單個的樣本誤差,用100個樣本做比較,得到差值之和,再平均,程式碼如下:

B = 0
for i in range(10):
    xm = XData[0:n,i].reshape(n,1)
    zm = forward_calculation(xm, W_rel, 0)
    ym = Y[0,i].reshape(1,1)
    B = B + (ym - zm)
b = B / 10
print(b)

我們在計算z的時候,故意把第三個引數設定為0,即令b=0。ym是每個樣本的標籤值,它與z的差值自然就是真正的b值。

上述兩段程式碼得到的輸出如下:

[[  1.98139005 -10.01461982   4.99789835]]
[[110.29389945]]

至此,我們完美地解決了北京地區的房價預測問題!但是還沒有解決自己可以有能力買一套北京的房子的問題......

最後遺留一個問題,如果標籤值y也歸一化的話,也可以得到訓練結果,但是怎麼解釋呢?大家有興趣的話可以研究一下,分享出來。反正筆者是沒研究出來。

全套教程請點選:微軟 AI 開發教程