本節介紹最簡單的循環神經網路,稱之為Simple-RNN,它是LSTM的基礎。Simple-RNN與BP一樣都有前饋層與反饋層。但是Simple-RNN引入了基於時間(狀態)的循環機制。
下圖一所示為Simple-RNN的神經網路示意圖。神經網路為A,通過讀取某個時間(狀態)的
輸入xt,然後輸出一個值ht,循環可以使得信息從當前時間步傳遞到下一時間步。
這些循環使得RNN可以被看作同一網路在不同時間步的多次循環,每個神經元會把更新的結果傳遞給下一個時間步,下圖一右側即為循環展開的情形。
<圖一>
下圖二網路的某一時刻輸入xt,與之前介紹的BP網路的輸入一樣,xt是一個n維的向量,不同是,RNN的輸入將是一整個序列,也就是X=[x0,x1,..xt-1,xt,xt+1,...xT],對於語言模型,每一個xt將代表一個詞向量,一整個序列就代表一句話。ht代表時刻t的隱含狀態,yt代表時刻t的輸出。
輸入層到隱含層權重由U表示,它將原始輸入進行抽象作為隱含層的輸入。
隱含層到隱含層的權重為W,它是網路的記憶控制者,負責調度記憶。
隱含層到輸出層的權重為V,從隱含層學習到的表示將通過它再一次抽象,並作為最終輸出。
前向傳播(Forward Propagation) 依次按照時間的順序計算一次即可,反向傳播(Back Propagation)從最後一個時間將累積的殘差傳遞回來即可,跟普通的BP神經網路訓練本質上基本一樣,由於加入了時間順序,計算方式有所不同,這稱之為BPTT(Back Propagation Through Time)算法,有關BPTT詳細推導可以參考底下連結:
http://ir.hit.edu.cn/~jguo/docs/notes/bptt.pdf
<圖二>
底下簡單的描述前向傳播(Forward Propagation)及反向傳播(Back Propagation)公式:
1.前向傳播(Forward Propagation)
首先在t=0的時刻,U,V,W,都被隨機初始化好,h0通常初始化為0,然後進行如下計算:
其中f(.)是隱含層的激活函數,g(.)是輸出層的激活函數。一般f(.)可以選擇tanh,relu,sigmoid等等,如果是序列標註問題,則g(.)一般選擇Softmax。h1為第一層的輸出,y1為預測的結果。
這樣,這樣當時間步為t時,公式如下:
循環網路是擁有記憶能力的,而這種能力是通過W記錄了以往的輸入狀態,作為下次的輔助輸入。故隱藏狀態可以理解為如下內容:
其中全局誤差如下:
這裡E是全局誤差,ei是第i個時間步的誤差,y是輸出層預測結果,d是實際結果,誤差函數fe()可以選擇是交叉商(Cross Entropy),也可以是平方誤差。
2.反向傳播(Back Propagation)
這與之前BP神經網路用到的反向傳播方法思想是一致的,也就是利用輸出層的誤差e,求解個權重的梯度,▽V,▽U,▽W,然後利用梯度下降法更新個個權重,求解各個權重公式如下:
接著求取個權重的梯度:
對於▽V,由於它不依賴之前的狀態,可以直接求導獲得,然後加和即可:
但是▽U,▽W依賴之前的狀態,不能直接求導,需定義中間變量:
收先計算出輸出層的δy,在向後傳播至各層δh,依次類推,直至輸入層:
*表示點積,只要計算δy及所有δh,即可算出▽U,▽W
有關BPTT詳細推導可以參考底下連結:
http://ir.hit.edu.cn/~jguo/docs/notes/bptt.pdf
以上公式省略bias。根據詢問前輩的指導,認為在較為複雜的神經網路裡,bias偏誤,是可以被省略的。
底下圖三給出了一個RNN向前傳播的計算例子是帶有bias的。
假設狀態的維度為2,輸入輸出維度都是1:
初始化值
h0=[0.0,0.0]
x1=[1.0]
U=[0.5,0.6]
V=[1.0,2.0]
W=[0.1,0.2]
[0.3,0.4]
隱含層bias=[1.0,-1.0]
輸出層bias=[0.1]
<圖三>
我們可以合併狀態h0及輸入x1為向量[0.0,0.0,1.0]
合併權重V及U為矩陣:
輸出層權重為:
於是計算狀態隱含層輸出為:
那麼輸出層的值為:
同樣類推t1時刻的狀態為[0.860,0.884],而t1時刻的輸出為2.73,這個例子最後的輸出未再經過
激活函數,只是簡單的計算如何向前輸出。
其python 程式計算如下:
import numpy as np
#定義輸入及初始狀態
X=[1,2]
h=[0.0,0.0]
#定義權重及bias
w=np.asarray([[0.1,0.2],[0.3,0.4]])
u=np.asarray([0.5,0.6])
bh=np.asarray([0.1,-0.1])
v=np.asarray([[1.0],[2.0]])
by=0.1
#計算向前傳播網路值
for i in range(len(X)):
neti=np.dot(h,w)+X[i]*u+bh
h=np.tanh(neti)
y=np.dot(h,v)+by
print("before activation:",neti)
print("state:",h)
print("output:",y)
輸出結果如下圖:
另一個完整範例,簡單的預測a+b=c的值,首先將a,b,c的數值由10進制轉成二進制當成輸入序列,藉由輸入的a及b預測出正確的c值。如下圖所示經由RNN網路訓練後,就可由輸入a及b的二進制值,可以正確地預測出c的二進值,即為a+b的和。在這個例子裡,從程式裡可以看出並未計算bias值,如同上述的公式描述。
其python 程式計算如下:
# -*- coding: UTF-8 -*-
import os,sys
import copy, numpy as np
np.random.seed(0)
# sigmoid 函數
def sigmoid(x):
return 1/(1+np.exp(-x))
# sigmoid 導函數
def dlogit(output): # dlogit
return output*(1-output)
# 十進位轉二進位數字組
def int2binary(bindim,largest_number):
int2bindic = {}
binary = np.unpackbits(np.array([range(largest_number)],dtype=np.uint8).T,axis=1)
for i in range(largest_number):
int2bindic[i] = binary[i]
return int2bindic
# 樣本發生器:實現一個簡單的(a + b = c)的加法
def gensample(dataset,largest_number):
# 實現一個簡單的(a + b = c)的加法
a_int = np.random.randint(largest_number/2) # 十進位 產生0-256/2 之間的亂數
a = dataset[a_int] # 二進位
b_int = np.random.randint(largest_number/2) # 十進位
b = dataset[b_int] # 二進位
c_int = a_int + b_int # 十進位的結果
c = dataset[c_int] # 十進位轉二進位的結果
return a,a_int,b,b_int,c,c_int
def showresult(j,overallError,d,c,a_int,b_int):
if(j % 1000 == 0):
print ("Error:" + str(overallError))
print ("Pred:" + str(d))
print ("True:" + str(c))
out = 0
for index,x in enumerate(reversed(d)):
out += x*pow(2,index)
print (str(a_int) + " + " + str(b_int) + " = " + str(out))
print ("------------")
#1. 產生的訓練集樣本
binary_dim=8 # 生成的二進位bitset的寬度
largest_number = pow(2,binary_dim) # 最大數2^8=256
dataset = int2binary(binary_dim,largest_number) # 產生資料集
#2. 初始化網路參數
alpha = 0.1 # 學習速率
input_dim = 2 # 輸入神經元個數
hidden_dim = 16 # 隱藏層神經元個數
output_dim = 1 # 輸出神經元個數
maxiter = 10000 # # 最大反覆運算次數
# 初始化 LSTM 神經網路權重 synapse是神經元突觸的意思
synapse_I = 2*np.random.random((input_dim,hidden_dim)) - 1 # 連接了輸入層與隱含層的權值矩陣
synapse_O = 2*np.random.random((hidden_dim,output_dim)) - 1 # 連接了隱含層與輸出層的權值矩陣
synapse_h = 2*np.random.random((hidden_dim,hidden_dim)) - 1 # 連接了隱含層與隱含層的權值矩陣
# 權值更新緩存:用於存儲更新後的權值。np.zeros_like:返回全零的與參數同類型、同維度的陣列
synapse_I_update = np.zeros_like(synapse_I) #
synapse_O_update = np.zeros_like(synapse_O) #
synapse_h_update = np.zeros_like(synapse_h) #
#3. 主程序--訓練過程:
for j in range(maxiter):
# 在實際應用中,可以從訓練集中查詢到一個樣本: 生成形如[a] [b]--> [c]這樣的樣本:
a,a_int,b,b_int,c,c_int = gensample(dataset,largest_number)
# 初始化一個空的二進位數字組,用來存儲神經網路的預測值
d = np.zeros_like(c)
overallError = 0 # 重置全域誤差
layer_2_deltas = list(); # 記錄layer 2的導數值
layer_1_values = list(); # 與layer 1的值。
layer_1_values.append(np.zeros(hidden_dim)) # 初始化時無值,存儲一個全零的向量
# 正向傳播過程:逐個bit位(0,1)的遍歷二進位數字字。
for position in range(binary_dim):
indx = binary_dim - position - 1 # 陣列索引7,6,5,...,0
# X 是樣本集的記錄,來自a[i]b[i]; y是樣本集對應的標籤,來自c[i]
X = np.array([[a[indx],b[indx]]])
y = np.array([[c[indx]]]).T
# 隱含層 (input ~+ prev_hidden)
# 1. np.dot(X,synapse_I):從輸入層傳播到隱含層:輸入層的資料*(輸入層-隱含層的權值)
# 2. np.dot(layer_1_values[-1],synapse_h):從上一次的隱含層[-1]到當前的隱含層:上一次的隱含層權值*當前隱含層的權值
# 3. sigmoid(input + prev_hidden)
layer_1 = sigmoid(np.dot(X,synapse_I) +np.dot(layer_1_values[-1],synapse_h))
# 輸出層 (new binary representation)
# np.dot(layer_1,synapse_O):它從隱含層傳播到輸出層,即輸出一個預測值。
layer_2 = sigmoid(np.dot(layer_1,synapse_O))
# 計算預測誤差
layer_2_error = y - layer_2
layer_2_deltas.append((layer_2_error)*dlogit(layer_2)) # 保留輸出層每個時刻的導數值
overallError += np.abs(layer_2_error[0]) # 計算二進位位元的誤差絕對值的總和,標量
d[indx] = np.round(layer_2[0][0]) # 存儲預測的結果--顯示使用
layer_1_values.append(copy.deepcopy(layer_1)) # 存儲隱含層的權值,以便在下次時間反覆運算中能使用
future_layer_1_delta = np.zeros(hidden_dim) # 初始化下一隱含層的誤差
# 反向傳播:從最後一個時間點開始,反向一直到第一個: position索引0,1,2,...,7
for position in range(binary_dim):
X = np.array([[a[position],b[position]]])
layer_1 = layer_1_values[-position-1] # 從列表中取出當前的隱含層。從最後一層開始,-1,-2,-3
prev_layer_1 = layer_1_values[-position-2] # 從列表中取出當前層的前一隱含層。
layer_2_delta = layer_2_deltas[-position-1] # 取出當前輸出層的誤差
# 計算當前隱含層的誤差:
# future_layer_1_delta.dot(synapse_h.T): 下一隱含層誤差*隱含層權重
# layer_2_delta.dot(synapse_O.T):當前輸出層誤差*輸出層權重
# dlogit(layer_1):當前隱含層的導數
layer_1_delta = (future_layer_1_delta.dot(synapse_h.T) +layer_2_delta.dot(synapse_O.T)) *dlogit(layer_1)
# 反向更新權重: 更新順序輸出層-->隱含層-->輸入層
# np.atleast_2d:輸入層reshape為2d的陣列
synapse_O_update +=np.atleast_2d(layer_1).T.dot(layer_2_delta)
synapse_h_update +=np.atleast_2d(prev_layer_1).T.dot(layer_1_delta)
synapse_I_update += X.T.dot(layer_1_delta)
future_layer_1_delta = layer_1_delta # 下一隱含層的誤差
# 更新三個權值
synapse_I += synapse_I_update * alpha
synapse_O += synapse_O_update * alpha
synapse_h += synapse_h_update * alpha
# 所有權值更新項歸零
synapse_I_update *= 0; synapse_O_update *= 0; synapse_h_update *= 0
# 逐次列印輸出
showresult(j,overallError,d,c,a_int,b_int)
加入阿布拉機的3D列印與機器人的FB專頁
https://www.facebook.com/arbu00/
<參考資料>
[1]書名:Tensorflow 實戰Google深度學習框架 作者:鄭澤宇 顧思宇
[2]書名:NLP漢語自然語言處裡原理與實踐 作者:鄭捷
[3]
http://ir.hit.edu.cn/~jguo/docs/notes/bptt.pdf
[4]
零基础入门深度学习(5) - 循环神经网络
<其他相關文章>
人工神經網路(1)--使用Python實作perceptron(感知器)
人工神經網路(2)--使用Python實作後向傳遞神經網路演算法(Backprogation artificial neature network)
深度學習(1)-如何在windows安裝Theano +Keras +Tensorflow並使用GPU加速訓練神經網路
深度學習(2)--使用Tensorflow實作卷積神經網路(Convolutional neural network,CNN)
機器學習(1)--使用OPENCV KNN實作手寫辨識
機器學習(2)--使用OPENCV SVM實作手寫辨識
演算法(1)--蒙地卡羅法求圓周率及橢圓面積(Monte carlo)
機器學習(3)--適應線性神經元與梯度下降法(Adaline neuron and Gradient descent)
機器學習(4)--資料標準常態化與隨機梯度下降法( standardization & Stochastic Gradient descent)
機器學習(5)--邏輯斯迴歸,過度適合與正規化( Logistic regression,overfitting and regularization)
機器學習(6)--主成分分析(Principal component analysis,PCA)
機器學習(7)--利用核主成分分析(Kernel PCA)處理非線性對應
機器學習(8)--實作多層感知器(Multilayer Perceptron,MLP)手寫數字辨識