神經網絡15分鐘入門!使用python從零開始寫一個兩層神經網絡

本篇是該系列的第三篇,建議在閱讀本篇文章之前先看前兩篇文章。

Mr.括號:神經網絡15分鐘入門!足夠通俗易懂了吧

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的理論與實現》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章