本篇是該系列的第三篇,建議在閱讀本篇文章之前先看前兩篇文章。
Mr.括號:神經網絡15分鐘入門!——反向傳播到底是怎麼傳播的?
在本文中將使用python實現之前描述的兩層神經網絡,並完成所提出的“象限分類”的問題。
需要注意的是,雖然標題叫做神經網絡15分鐘入門,但是到這篇文章,對於沒接觸過python的同學,15分鐘怕是不太夠。好在python本身不算太難,如果你有其他語言的基礎,結合本文儘量詳細的講解,對於算法層面的理解應該還是可以做到的。如果還是不能理解,建議先入門python再來看本文,畢竟想做深度學習,對語言的掌握是基本要求。
另外,這篇文章的正確食用方法是將代碼搞到自己的電腦上,然後單步調試逐行看參數的變化,如有不明白的地方再對照文章中的講解來理解。單純靠看文章是不太容易融會貫通的(可以腦內debug的同學可以忽略)。
一、運行環境
運行環境:Python 3.6.5+Anaconda3+VS Code
Anaconda是一個環境管理器,安裝Anaconda,就相當於安裝了python+各種工具包,這些工具包在我們進行神經網絡的應用時十分必要。
VS Code是微軟的免費代碼編輯器,功能相當強大,插件相當豐富,界面非常美觀。當然,你也可以用pycharm或者eclipse,看個人習慣。
運行環境的搭建就不詳細介紹了,在網上能找到很多教程,如果有疑問可以留言,需要的話我會寫一篇番外對環境搭建進行詳細講述。
二、編程實現
1.導入numpy包
import numpy as np
#numpy是一個強大的數學工具包
#我們後邊要用到的是numpy中的數組類型、矩陣運算等
#不明白沒關係,用到的時候會再解釋
2.前向傳播函數
# 前向傳播函數
# - x:包含輸入數據的numpy數組,形狀爲(N,d_1,...,d_k)
# - w:形狀爲(D,M)的一系列權重
# - b:偏置,形狀爲(M,)
def affine_forward(x, w, b):
out = None # 初始化返回值爲None
N = x.shape[0] # 重置輸入參數X的形狀
x_row = x.reshape(N, -1) # (N,D)
out = np.dot(x_row, w) + b # (N,M)
cache = (x, w, b) # 緩存值,反向傳播時使用
return out,cache
這一段程序是定義了了一個名爲affine_forward的函數,其功能就是計算這個公式(仿射):
如果不記得這個公式了就回去看一下第一篇文章
這個函數的輸入參數就是公式中的矩陣X,W1和b1,對應到程序中就是x,w和b。
不過需要注意的是,程序中的輸入參數x,其形狀可以是(N,d_1,...,d_k),這是什麼意思呢?在我們這個例子中,輸入參數x是:
[2,1],
[-1,1],
[-1,-1],
[1,-1]]
它是一個4行2列的二維數組,那麼x的形狀就是(4,2),對應的參數N=4,d_1=2。這是我們用來做訓練的座標數據,分別對應了I、II、III、IV象限。
在某些應用場景中,x的維度可能更高。比如對於一個20*20像素的4張灰度圖,x的形狀將是(4,20,20),對應的參數就是N=4,d_1=20,d_2=20。(這裏邊第一個參數用N表示,它代表的是同時用於計算前向傳播的數據有幾組,後邊的參數d_1~d_k代表的是數據本身的形狀。)
對於這種維度大於2的x來說,需要對其進行重新塑形,也就是將(4,20,20)的高維數組變化爲(4,20*20)這樣的二位數組。
爲什麼要這麼做呢?是爲了方便計算。這樣變換之後高維的向量被“拍扁”成一維向量(長度爲20*20的一維向量),對應的W和b也都是一維的,既統一了參數形式,又不會影響數據的正常使用。
這個“拍扁”的動作,是用上述代碼中的這兩行完成的:
N = x.shape[0] # 重置輸入參數X的形狀
x_row = x.reshape(N,-1) # (N,D)
x.shape[0]是獲取數組x的第0維長度,也就是數據的組數,對於上述的4行2列的數組,其值爲4;對於上述(4,20,20)的數組,其值也爲4.
x.reshape(N,-1)是對x重新塑形,即保留第0維,其他維度排列成1維。對於形狀爲(4,2)的數組,其形狀不變,對於形狀爲(4,20,20)的數組,形狀變爲(4,20*20)。以此類推。
在完成reshape後,就可以進行矩陣的線性運算了:
out = np.dot(x_row, w)+ b # (N,M)
.dot就是numpy中的函數,可以實現x_row與w的矩陣相乘。x_row的形狀爲(N,D),w的形狀爲(D,M),得到的out的形狀是(N,M)。
cache =(x, w, b) # 緩存值,反向傳播時使用
上面這句是將當前x,w和b的值緩存下來,留作反向傳播時使用。
3.反向傳播函數
# 反向傳播函數
# - x:包含輸入數據的numpy數組,形狀爲(N,d_1,...,d_k)
# - w:形狀(D,M)的一系列權重
# - b:偏置,形狀爲(M,)
def affine_backward(dout, cache):
x, w, b = cache # 讀取緩存
dx, dw, db = None, None, None # 返回值初始化
dx = np.dot(dout, w.T) # (N,D)
dx = np.reshape(dx, x.shape) # (N,d1,...,d_k)
x_row = x.reshape(x.shape[0], -1) # (N,D)
dw = np.dot(x_row.T, dout) # (D,M)
db = np.sum(dout, axis=0, keepdims=True) # (1,M)
return dx, dw, db
這一段是實現計算仿射層的反向傳播的函數。這篇文章的2.3節講的就是這段代碼的原理,如果不清楚可以先出門左轉看一下。
函數中第一句就是讀取緩存的x,w和b的值,爲什麼要這樣做呢?仿射變換反向傳播的最重要的3個目的,分別是:①更新參數w的值②計算流向下一個節點的數值③更新參數b的值。“更新”的時候需要“舊”值,也就是緩存值,具體操作如下:
①爲了得到w的值,要將上一節點輸入的值(dout)乘以x。
dw = np.dot(x_row.T, dout) # (D,M)
②爲了得到流入下一個節點的值(x),要將上一節點的輸入值(dout)乘以w。你可能發現了,①中爲了得到w是乘以的x,②中爲了得到x是乘以的w,也就是將係數交叉相乘了。
dx = np.dot(dout, w.T) # (N,D)
③爲了得到b,只需要將out直接傳過來就可以,爲了保持維度一致,這裏將out求和。
db = np.sum(dout, axis=0, keepdims=True) # (1,M)
在仿射變換反向傳播這裏,各種矩陣的維度可能會讓你感到困惑。這裏的維度包含三個,分別是D、M和N。
看一下下圖,其中包括兩個仿射變換,我們以第一個舉例,其變換公式爲H=X*W1+b1。該仿射變換對應到程序中的D的值爲2,M的值爲50,N的值爲4。怎麼理解呢?X的維度就是N*D,而M的值就是W1的第二個維度,這裏記住就好了,每個仿射變換都是這樣的(其實不記住也沒關係,這裏沒有什麼物理含義,就是單純的矩陣變換的維度而已。這幾個維度在反向傳播時可能難理解,這是數學公式推導來的,迷惑的時候找出這篇文章過來看一遍就明白了)。
注意看矩陣維度
4.參數初始化
X = np.array([[2,1],
[-1,1],
[-1,-1],
[1,-1]]) # 用於訓練的座標,對應的是I、II、III、IV象限
t = np.array([0,1,2,3]) # 標籤,對應的是I、II、III、IV象限
np.random.seed(1) # 有這行語句,你們生成的隨機數就和我一樣了
# 一些初始化參數
input_dim = X.shape[1] # 輸入參數的維度,此處爲2,即每個座標用兩個數表示
num_classes = t.shape[0] # 輸出參數的維度,此處爲4,即最終分爲四個象限
hidden_dim = 50 # 隱藏層維度,爲可調參數
reg = 0.001 # 正則化強度,爲可調參數
epsilon = 0.001 # 梯度下降的學習率,爲可調參數
# 初始化W1,W2,b1,b2
W1 = np.random.randn(input_dim, hidden_dim) # (2,50)
W2 = np.random.randn(hidden_dim, num_classes) # (50,4)
b1 = np.zeros((1, hidden_dim)) # (1,50)
b2 = np.zeros((1, num_classes)) # (1,4)
這一段程序對一些必要的參數進行了初始化,程序較爲簡單,看註釋即可,不再詳細解釋。
對於訓練數據以及訓練模型已經確定的網絡來說,爲了得到更好的訓練效果需要調節的參數就是上述的隱藏層維度、正則化強度和梯度下降的學習率,以及下一節中的訓練循環次數。
5.訓練與迭代
for j in range(10000): #這裏設置了訓練的循環次數爲10000
# ①前向傳播
H,fc_cache = affine_forward(X,W1,b1) # 第一層前向傳播
H = np.maximum(0, H) # 激活
relu_cache = H # 緩存第一層激活後的結果
Y,cachey = affine_forward(H,W2,b2) # 第二層前向傳播
# ②Softmax層計算
probs = np.exp(Y - np.max(Y, axis=1, keepdims=True))
probs /= np.sum(probs, axis=1, keepdims=True) # Softmax算法實現
# ③計算loss值
N = Y.shape[0] # 值爲4
print(probs[np.arange(N), t]) # 打印各個數據的正確解標籤對應的神經網絡的輸出
loss = -np.sum(np.log(probs[np.arange(N), t])) / N # 計算loss
print(loss) # 打印loss
# ④反向傳播
dx = probs.copy() # 以Softmax輸出結果作爲反向輸出的起點
dx[np.arange(N), t] -= 1 #
dx /= N # 到這裏是反向傳播到softmax前
dh1, dW2, db2 = affine_backward(dx, cachey) # 反向傳播至第二層前
dh1[relu_cache <= 0] = 0 # 反向傳播至激活層前
dX, dW1, db1 = affine_backward(dh1, fc_cache) # 反向傳播至第一層前
# ⑤參數更新
dW2 += reg * W2
dW1 += reg * W1
W2 += -epsilon * dW2
b2 += -epsilon * db2
W1 += -epsilon * dW1
b1 += -epsilon * db1
這段程序是網絡訓練的核心,我將按照①前向傳播②Softmax層③計算loss值④反向傳播⑤參數更新這五個小結的順序依次講解:
①前向傳播
# ①前向傳播
H,fc_cache = affine_forward(X,W1,b1) # 第一層前向傳播
H = np.maximum(0, H) # 激活
relu_cache = H # 緩存第一層激活後的結果
Y,cachey = affine_forward(H,W2,b2) # 第二層前向傳播
第一句H,fc_cache = affine_forward(X,W1,b1) 調用了之前寫的前向傳播的函數,完成了第一層網絡的矩陣線性代數運算。
第二句H = np.maximum(0, H)是從0和H中選擇較大的值賦給H,也就是實現了ReLU激活層函數。
第四句Y,cachey = affine_forward(H,W2,b2),完成了第二層網絡的矩陣線性代數運算。
②Softmax層計算
# ②Softmax層計算
probs = np.exp(Y - np.max(Y, axis=1, keepdims=True))
probs /= np.sum(probs, axis=1, keepdims=True) # Softmax算法實現
這兩行是爲了實現Softmax層的計算,在之前我們說過,Softmax的計算公式是:
不過在實際應用中會存在一個問題,比如i的值等於1000時,e^1000在計算機中會變成無窮大的inf,後續計算將無法完成,所以程序中會對計算公式做一些修改,實際使用的公式爲:
在指數上減去常數C不影響最終結果(證明略),而這個常數C通常取i中的最大值。
第一句probs = np.exp(Y - np.max(Y, axis=1, keepdims=True)) 就是求輸出各個行的指數值,舉個例子,Y的值如果是:
[[-4,17,20,-4],
[10,-2,5,3],
[-5,3,4,10],
[-5,5,5,2]]
np.max(Y, axis=1, keepdims=True)計算得到的是[[20],[10],[10],[5]],後邊括號裏的參數axis代表以豎軸爲基準 ,在同行中取值; keepdims=True代表保持矩陣的二維特性。
所以np.exp(Y - np.max(Y, axis=1, keepdims=True)) 代表:Y矩陣中每個值減掉改行最大值後再取對數。
第二句probs /= np.sum(probs, axis=1, keepdims=True) 以行爲單位求出各個數值對應的比例。也就是最終實現了Softmax層的輸出。
③計算loss值
# ③計算loss值
N = Y.shape[0] # 值爲4
print(probs[np.arange(N), t]) # 打印各個數據的正確解標籤對應的神經網絡的輸出
loss = -np.sum(np.log(probs[np.arange(N), t])) / N # 計算loss
複習一下:交叉熵損失的求法是求對數的負數。
第一句N = Y.shape[0]取了最終輸出的維度,這個例子中爲4,即四個象限。
第二句打印各個數據的正確解標籤對應的神經網絡的輸出。
其中probs[np.arange(N), t]講解一下:
N爲4時,np.arange(N)會生成一個Numpy數組[0,1,2,3]。t中標籤是以[0,1,2,3]的形式儲存的,所以probs[np.arange(N), t]能抽出各個數據的正確解標籤對應的神經網絡輸出,在這個例子中,probs[np.arange(N), t]會成成numpy數組[probs[0,0], probs[1,1], probs[2,2], probs[3,3]]。
第三句loss = -np.sum(np.log(probs[np.arange(N), t])) / N中先求了N維數據中的交叉熵損失,然後對這N個交叉熵損失求平均值,作爲最終loss值。
④反向傳播
# ④反向傳播
dx = probs.copy() # 以Softmax輸出結果作爲反向輸出的起點
dx[np.arange(N), t] -= 1 #
dx /= N # 到這裏是反向傳播到softmax前
dh1, dW2, db2 = affine_backward(dx, cachey) # 反向傳播至第二層前
dh1[relu_cache <= 0] = 0 # 反向傳播至激活層前
dX, dW1, db1 = affine_backward(dh1, fc_cache) # 反向傳播至第一層前
反向傳播計算是從Softmax層的輸出開始的。你是不是想問爲什麼不是從loss值開始算?
回看上一篇文章的2.5節,你會發現Softmax-with-Loss層的反向傳播結果計算,本身就是與loss無關的。而只與Softmax層輸出結果和教師標籤有關。換句話說,即使是從loss開始計算反向傳播,經過一系列化簡之後,這個loss值也會被化簡掉,化簡後的結果只包括Softmax層的輸出和教師標籤。
第一句代碼很簡單,就是將Softmax的輸出值賦給dx, 這裏dx代表反向傳播的主線值。dx[np.arange(N), t]-=1這句代碼
第二句代碼是實現上一篇文章中y-t的操作(y就是Softmax層的輸出)。dx[np.arange(N), t]-=1這句代碼中,dx是一個4*4的數組,而t是一個內容爲[0,1,2,3]的數組(見其初始化),N的值爲4。np.arrange(N)會生成一個從0到3的數組[0,1,2,3],因爲t中的標籤是以[0,1,2,3]的形式存儲的,所以dx[np.arange(N), t]能抽出各個數據的正確解標籤對應的神經網絡的輸出。在這個例子中dx[np.arange(N), t]會成成NumPy數組[dx[0,0],dx[1,1],dx[2,2],dx[3,3]。
第四、六句試一次仿射變幻的反向傳播,上邊說過了,不在具體解釋了。
第五句是ReLU激活層的反向傳播,至於爲什麼這樣寫,也去看上一篇文章吧~
⑤參數更新
# ⑤參數更新
dW2 += reg * W2
dW1 += reg * W1
W2 += -epsilon * dW2
b2 += -epsilon * db2
W1 += -epsilon * dW1
b1 += -epsilon * db1
前兩行是引入正則化懲罰項更新dW,後四行是引入學習率更新W和b。這部分理解起來比較簡單,如果有疑問可以參考上篇文章的第3節。
6.驗證
test = np.array([[2,2],[-2,2],[-2,-2],[2,-2]])
H,fc_cache = affine_forward(test,W1,b1) #仿射
H = np.maximum(0, H) #激活
relu_cache = H
Y,cachey = affine_forward(H,W2,b2) #仿射
# Softmax
probs = np.exp(Y - np.max(Y, axis=1, keepdims=True))
probs /= np.sum(probs, axis=1, keepdims=True) # Softmax
print(probs)
for k in range(4):
print(test[k,:],"所在的象限爲",np.argmax(probs[k,:])+1)
給出了一組數據test,對已經訓練好的網絡進行驗證。
其實驗證的方法和訓練時的正向傳播的過程基本一致,即第一層網絡線性計算→激活→第二層網絡線性計算→Softmax→得到分類結果。
這部分代碼在之前也大多講過,不再詳述。
三、運行結果
在運行10000次迭代後,loss值以肉眼可見的速度下降。
最終loss值爲:0.0040015
最終輸出結果爲:
可見分類正確。
四、總結
本例是一個很簡單的神經網絡的例子,我們只用了一組數據用來訓練,其訓練結果應該是比較勉強的。之所以最終效果還行,只是我們選擇驗證的例子比較合適。要想得到比較完美的模型,需要有大量的、分散的訓練數據,比如第一象限不僅要有[1,1]這種數據,還要有[1000,1],[1,1000]這種,這裏就不再詳述了。
“神經網絡15分鐘入門”系列到這裏就結束啦。如果這三篇文章裏的內容能夠融會貫通,相信對你後邊學習深度學習會有一些幫助。在神經網絡學習過程中能遇到的難點和坑我儘量都點出來了,如果還有什麼疑問請留言給我吧,也許會出一篇番外集中回答。
如果要獲取本文的完整代碼,可以關注我的公衆號“括號的城堡”,微信號爲“khscience”,回覆“神經網絡”就能拿到啦,公衆號裏可能還會有更多有趣的東西分享。
歡迎持續關注我的專欄與信號處理有關的那些東東
參考:
《深度學習入門:基於Python的理論與實現》