單個輸入Demo
輸入層
它在輸入層只接受一個輸入,經過參數w,b的計算後,直接輸出結果。這樣一個簡單的“網絡”,只能解決簡單的一元線性迴歸問題,而且由於是線性的,我們不需要定義激活函數,這就大大簡化了程序,而且便於大家循序漸進地理解各種知識點。下面,我們在這個最簡的線性迴歸的例子中,來說明神經網絡中最重要的反向傳播和梯度下降的概念和過程以及編碼實現。
其中,x就是上圖中紅色點的橫座標值(服務器數),y是縱座標值(空調功率)。
權重W/B
因爲是一元線性問題,所以W/B都是一個標量,記爲w和b,我們認爲這組數據有這個關係:
輸出層
輸出層1個神經元,是上述預測公式的直接輸出,但定義上有所變化,應該是,z是我們的預測輸出,y是實際的樣本標籤值。
讀取文件數據
注意:從本章開始,所有的樣本數據的格式都是:每一列是一個樣本的所有特徵,每一行是某個特徵的所有樣本。
比如,如果有一個4個樣本的數據集,每個樣本有兩個特徵,其數據結構是這樣的: 最終,樣本數據的樣子是:
樣本Id | 1 | 2 | 3 | 4 | (更多樣本向右擴展) |
---|---|---|---|---|---|
特徵1 | … | ||||
特徵2 | … | ||||
(更多特徵向下擴展) | … | … | … | … | … |
對於標籤數據Y,一般只有一個標籤值,所以是個(1,m)的二維矩陣,與X對應的。
import numpy as np
import matplotlib.pyplot as plt
from pathlib import Path
x_data_name = "X04.dat"
y_data_name = "Y04.dat"
def ReadData():
Xfile = Path(x_data_name)
Yfile = Path(y_data_name)
if Xfile.exists() & Yfile.exists():
X = np.load(Xfile)
Y = np.load(Yfile)
# 注意這裏和前面的例子不同
return X.reshape(1,-1),Y.reshape(1,-1)
else:
return None,None
在上面的code裏的reshape,實際上是把一個一維數組(m,)變成1行m列的二維數組(1,m)。這個例子中,X只有一個特徵,所以只有一行。
前向計算
def ForwardCalculation(w,b,x):
z = np.dot(w, x) + b
return z
關於Python的函數命名規範,一般是用aa_bb這種形式,而不是AaBb的形式,這是個人習慣而已,大家自己隨意。而關於變量命名規範,我個人習慣用大寫X表示一個矩陣,用小寫x表示一個變量或一個樣本。
損失函數
我們用傳統的均方差函數,其中,z是每一次迭代的預測輸出,y是樣本標籤數據。我們使用所有樣本參與計算,因此損失函數實際爲:
其中的分母中有個2,實際上是想在求導數時把這個2約掉,沒有什麼原則上的區別。
爲什麼使用所有樣本參與計算呢?因爲單個樣本或少量樣本的損失並不能代表廣大人民羣衆的利益。一條直線可能正好穿過一個點,那麼對於這個點來說,它的誤差爲0,但對於其它樣本來說,誤差就可能很大。
我們暫時不需要實現這個損失函數,只是用來定義梯度下降時的求導過程。
反向傳播
下面的代碼是通過梯度下降法中的公式推導而得的。
def BackPropagation(x,y,z):
dZ = z - y
dB = dZ
dW = np.dot(dZ, x)
return dW, dB
dZ是中間變量,避免重複計算。dZ又可以寫成delta_Z,是某一層神經網絡的反向誤差輸入。
梯度更新
def UpdateWeights(w, b, dW, dB, eta):
w = w - eta*dW
b = b - eta*dB
return w,b
推理
def Inference(w,b,x):
z = ForwardCalculation(w,b,x)
return z
推理過程,實際上就是一個前向計算過程,我們把它單獨拿出來,方便對外接口的設計。
獲得指定的一個訓練樣本
def GetSample(X,Y,i):
x = X[0,i]
y = Y[0,i]
return x,y
結果顯示函數
def ShowResult(X, Y, w, b, iteration):
# draw sample data
plt.plot(X, Y, "b.")
# draw predication data
PX = np.linspace(0,1,10)
PZ = w*PX + b
plt.plot(PX, PZ, "r")
plt.title("Air Conditioner Power")
plt.xlabel("Number of Servers(K)")
plt.ylabel("Power of Air Conditioner(KW)")
plt.show()
print(iteration)
print(w,b)
對於初學神經網絡的人來說,可視化的訓練過程及結果,可以極大地幫助理解神經網絡的原理。
主程序
if __name__ == '__main__':
# learning rate
eta = 0.1
# set w,b=0, you can set to others values to have a try
#w, b = np.random.random(),np.random.random()
w, b = 0, 0
# create mock up data
X, Y = ReadData()
# count of samples
num_example = X.shape[1]
for i in range(num_example):
# get x and y value for one sample
x,y = GetSample(X,Y,i)
# get z from x,y
z = ForwardCalculation(w, b, x)
# calculate gradient of w and b
dW, dB = BackPropagation(x, y, z)
# update w,b
w, b = UpdateWeights(w, b, dW, dB, eta)
ShowResult(X, Y, w, b, 1)
result = Inference(w,b,0.346)
print("result=", result)
運行結果
epoch=1,iteration=200,w=1.918221,b=3.077801
result= 3.741505043252122
最終我們得到了W=1.918,B=3.077,然後根據這兩個值畫出了上圖中的紅線:
預測時,已知有346臺服務器,先要除以1000,因爲橫座標是以K(千臺)服務器爲單位的,代入前向計算函數,得到的結果是3.74千瓦。
多個輸入Demo
我們定義一個一層的神經網絡,輸入層爲3或者更多,反正大於2了就沒區別。這個一層的神經網絡沒有中間層,只有輸入項和輸出層(輸入項不算做一層),而且只有一個神經元,並且神經元有一個線性輸出,不經過激活函數處理。亦即在下圖中,經過求和得到Z值之後,直接把Z值輸出。
矩陣運算過程:
上述公式中括號中的數字表示該矩陣的 (行,列) 數,如W(1,3)表示W是一個1行3列的矩陣。
輸入層
我們先看一下樣本數據的樣子:
樣本序號 | 1 | 2 | 3 | … | 1000 |
---|---|---|---|---|---|
朝向(東南西北) | 2 | 2 | 3 | … | 3 |
地理位置(幾環) | 4 | 6 | 3 | … | 3 |
居住面積(平米) | 79 | 116 | 109 | … | 98 |
整套價格(萬元) | 469 | 631 | 323 | … | 576 |
單獨看一個樣本是這樣的:
一共有1000個樣本,每個樣本3個特徵值,X就是一個的矩陣,模樣是醬紫的:
表示第一個樣本,表示第一個樣本的一個特徵值,是第一個樣本的標籤值。
權重W和B
木頭:老師,爲何不把這個表格轉一下,變成橫向是樣本特徵值,縱向是樣本數量?那樣好像更符合思維習慣?
鐵柱:確實是!但是在實際的矩陣運算時,由於是,W在前面,X在後面,所以必須是這個樣子的:
假設每個樣本x有n個特徵向量,上式中的W就是一個的向量,讓每個w都對應一個x:
B是個單值,因爲只有一個神經元,所以只有一個bias,每個神經元對應一個bias,如果有多個神經元,它們都會有各自的b值。
輸出層
由於我們只想完成一個迴歸(擬合)任務,所以輸出層只有一個神經元。由於是線性的,所以沒有用激活函數。
對於擬合,可以想象成用一支筆在一堆點中畫一條直線或者曲線,而那一個神經元就是這支筆。如果有多個神經元,可以畫出多條線來,就不是擬合了,而是分類。
運行
懷着期待的心情用顫抖的右手按下了運行鍵…but…what happened?
epoch=0
0 0 55883834476.133575 [[ 101.0507623 201.66401831 3960.19459875]] [[50.13519931]]
0 1 2.075927906874647e+16 [[ -61796.96353768 -123594.36458166 -2410062.36310063]] [[-30898.87195068]]
0 2 3.7752686053459638e+22 [[5.60309885e+07 1.68154762e+08 3.25097149e+09]] [[28015493.83866825]]
0 3 5.332628913256499e+28 [[-1.06460908e+11 -1.06348785e+11 -3.86686449e+12]] [[-3.5477631e+10]]
......
0 110 inf [[5.83856788e+303 1.17197663e+304 3.22400448e+305]] [[5.86699589e+303]]
0 111 inf [[-1.00706180e+307 -1.00647368e+307 -inf]] [[-3.35295186e+306]]
0 112 nan [[inf inf nan]] [[inf]]
0 113 nan [[nan nan nan]] [[nan]]
0 114 nan [[nan nan nan]] [[nan]]
......
怎麼會overflow呢?於是右手的顫抖沒有停止,左手也開始顫抖了。
數值太大,導致計算溢出了。第一次遇到這個情況,但相信不會是最後一次,因爲這種情況在神經網絡中太常見了。我們再看看損失函數歷史記錄:
損失函數值隨着迭代次數快速上升,說明訓練沒有收斂,而是發散了。
解決訓練不收斂問題
仔細分析一下屏幕打印信息:
0 0 48904435754.68428 [[ 93.8 187.6 3705.1]] [[46.9]]
0 1 1.8166533368107932e+16 [[ -57809.94 -115619.88 -2254540.76]] [[-28904.97]]
前兩次迭代的損失值已經是天文數字了,後面的W和B的值也在不斷變大,說明網絡發散了。
難度我們遇到了傳說中的梯度爆炸!數值太大,導致計算溢出了。第一次遇到這個情況,但相信不會是最後一次,因爲這種情況在神經網絡中太常見了。別慌,擦乾淨頭上的冷汗,踩着自己的屍體繼續前行!
回想一個問題:爲什麼在前幾章中,我們沒有遇到這種情況?把樣本拿來看一看:
樣本序號 | 1 | 2 | 3 | … | 200 |
---|---|---|---|---|---|
服務器數量(千) | 0.928 | 0.0469 | 0.855 | … | 0.373 |
空調功率(千瓦) | 4.824 | 2.950 | 4.643 | … | 3.594 |
因爲所有的X值(服務器數量)都是在[0,1]之間的,而本次的數據有三個特徵值,全都是不是在[0,1]之間的,並且取值範圍還不相同。我們不妨把本次樣本數據也做一下這樣的處理,亦即“歸一化”。
其實,數據歸一化是深度學習的必要步驟之一,已經是魔法師們家喻戶曉的技能,也因此它很少被各種博客/文章所提及,以至於麻瓜們經常被坑。
爲什麼要做歸一化
理論層面上,神經網絡是以樣本在事件中的統計分佈概率爲基礎進行訓練和預測的,也就是說:
- 樣本的各個特徵的取值要符合概率分佈,即[0,1]
- 樣本的度量單位要相同。我們並沒有辦法去比較1米和1公斤的區別,但是,如果我們知道了1米在整個樣本中的大小比例,以及1公斤在整個樣本中的大小比例,比如一個處於0.2的比例位置,另一個處於0.3的比例位置,就可以說這個樣本的1米比1公斤要小!
- 神經網絡假設所有的輸入輸出數據都是標準差爲1,均值爲0,包括權重值的初始化,激活函數的選擇,以及優化算法的的設計。
- 數值問題:歸一化/標準化可以避免一些不必要的數值問題。因爲sigmoid/tanh的非線性區間大約在**[-1.7,1.7]**。意味着要使神經元有效,tanh( w1x1 + w2x2 +b) 裏的 w1x1 +w2x2 +b 數量級應該在 1 (1.7所在的數量級)左右。這時輸入較大,就意味着權值必須較小,一個較大,一個較小,兩者相乘,就引起數值問題了。
- 初始化:在初始化時我們希望每個神經元初始化成有效的狀態,tansig函數在[-1.7, 1.7]範圍內有較好的非線性,所以我們希望函數的輸入和神經元的初始化都能在合理的範圍內使得每個神經元在初始時是有效的。(如果權值初始化在[-1,1]且輸入沒有歸一化且過大,會使得神經元飽和)
- 梯度:以輸入-隱層-輸出這樣的三層BP爲例,我們知道對於輸入-隱層權值的梯度有2ew(1-a^2)*x的形式(e是誤差,w是隱層到輸出層的權重,a是隱層神經元的值,x是輸入),若果輸出層的數量級很大,會引起e的數量級很大,同理,w爲了將隱層(數量級爲1)映身到輸出層,w也會很大,再加上x也很大的話,從梯度公式可以看出,三者相乘,梯度就非常大了。這時會給梯度的更新帶來數值問題。
- 學習率:知道梯度非常大,學習率就必須非常小,因此,學習率(學習率初始值)的選擇需要參考輸入的範圍,不如直接將數據歸一化,這樣學習率就不必再根據數據範圍作調整。 對w1適合的學習率,可能相對於w2來說會太小,若果使用適合w1的學習率,會導致在w2方向上步進非常慢,會消耗非常多的時間,而使用適合w2的學習率,對w1來說又太大,搜索不到適合w1的解。如果使用固定學習率,而數據沒歸一化,則後果可想而知。
回到房價數據問題
房價數據中,地理位置的取值範圍是[2,6],而房屋面積的取值範圍爲[40,120],二者相差太遠,就不可以放在一起計算了。
在這個式子中,如果X1的取值是[2,6],X2的取值是[40,120],相差太遠,而且都不在[0,1]之間,所以對於神經網絡來說很難理解。下圖展示了歸一化前後的情況Loss值的等高圖,意思是地理位置和房屋面積取不同的值時,作爲組合來計算損失函數值時,形成的類似地圖的等高圖。左側爲歸一化前,右側爲歸一化後
房屋面積的取值範圍是[40,120],而地理位置的取值範圍是[2,6],二者會形成一個很扁的橢圓,如左側。這樣在尋找最優解的時候,過程會非常曲折。運氣不好的話,如同我們上面的代碼,根本就沒法訓練。
幾個基本數學概念
- 均值mean:
- 標準差stdandard deviation:
- 方差variance
- 協方差covariance
結果爲正,表示X,Y是正相關。
歸一化
把數據線性地變成[0,1]或[-1,1]之間的小數,把帶單位的數據(比如米,公斤)變成無量綱的數據,區間縮放。
歸一化有三種方法:
- Min-Max歸一化:
- 平均值歸一化:
- 非線性歸一化:
標準化
把每個特徵值中的所有數據,變成平均值爲0,標準差爲1的數據,最後爲正態分佈。
Z-score規範化(標準差標準化 / 零均值標準化,其中std是標準差):
中心化
平均值爲0,無標準差要求:
再次返回房價問題,歸一化後的損失
損失雖然收斂,但是和預期相差甚遠
正規方程的解是:w1=2.000000,w2=-10.000000,w3=5.000000,b=110.000000
神經網絡的解是:
w= [[ 5.99981695 -40.00019134 394.99950271]] b= [[292.00048552]]
差距有點大,如下表:
方法 | W1 | W2 | W3 | B | 預測結果 |
---|---|---|---|---|---|
正規方程 | 2 | -10 | 5 | 110 | 529萬元 |
神經網絡 | 5.99 | -40.00 | 394.99 | 292.00 | 36838萬元 |
樣本數據:
特徵 | 朝向 | 位置 | 面積 |
---|---|---|---|
最小值 | 1 | 2 | 40 |
最大值 | 4 | 6 | 119 |
待預測房子參數 | 2 | 5 | 93 |
計算一下W的變化率,再和特徵縮放值去比較
結果 | W1 | W2 | W3 | B |
---|---|---|---|---|
正規方程 | 2 | -10 | 5 | 110 |
神經網絡 | 5.99 | -40.00 | 394.99 | 292.00 |
倍差 | 5.99/2=3 | -40/-10=4 | 394.99/5=79 | 292/110=2.65 |
歸一化前後的數據
特徵 | 朝向 | 位置 | 面積 |
---|---|---|---|
最小值 | 1 | 2 | 40 |
最大值 | 4 | 6 | 119 |
範圍 | 4-1=3 | 6-2=4 | 119-40=79 |
發現:通過對比發現,第一張表最後一行的數據,和第二張表最後一行的數據,有驚人的相似之處
還原真實的W,B值
唯一修改的地方,就是樣本數據特徵值的歸一化,我們並沒有修改標籤值!,所以必須將權重和偏置還原到未歸一化之前。
假設在歸一化之前,真實的樣本值是,真實的權重值是;在歸一化之後,樣本值變成了,訓練出來的權重值是:
由於訓練時標籤值(房價)並沒有做歸一化,意味着我們是用真實的房價做的訓練,所以預測值和標籤值應該相等,所以:
歸一化的公式是:
把公式2代入公式1:
第二項是個常數,即: 如果想讓公式3等式成立,則變量項和常數項分別相等,即: 從公式4,兩邊除以X,可以得到:
正確預測方法(不需要還原到正規方程需要的權重和偏置)
在線性問題中,還能夠還原W和B的值,
在非線性問題中或者深層網絡,可能根本做不到這一點。
所以,可以把需要預測的數據,也先做歸一化,然後用訓練出來的W/B的值來計算,得到預測結果。
https://github.com/microsoft/ai-edu/blob/master/B-教學案例與實踐/B6-神經網絡基本原理簡明教程/04.3-神經網絡法.md
https://github.com/microsoft/ai-edu/blob/master/B-教學案例與實踐/B6-神經網絡基本原理簡明教程/05.5-正確的推理方法.md