2017年5月7日 星期日

深度學習(3)--循環神經網絡(RNN, Recurrent Neural Networks)


    本節介紹最簡單的循環神經網路,稱之為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)手寫數字辨識