循環神經網路RNN打開手冊

最近朋友前小夥伴都已經傳播瘋了的谷歌翻譯,實現了令人驚艷的性能。這裡的技術核心, 就是RNN- 我們常說的傳說中的循環神經網路。 RNN可以稱得上是深度學習未來最有前景的工具之一。它在時間序列(比如語言文字,股票價格)的處理和預測上具有神功, 你想了解它的威力的根源嗎? 你想知道一些最新的RNN應用?請看下文。

為什麼RNN會有如此強大的效力? 讓我們從基礎學起。首先, 要看RNN和對於圖像等靜態類變數處理立下神功的卷積網路CNN的結構區別來看, 「循環」兩個字,已經點出了RNN的核心特徵, 即系統的輸出會保留在網路里, 和系統下一刻的輸入一起共同決定下一刻的輸出。這就把動力學的本質體現了出來, 循環正對應動力學系統的反饋概念,可以刻畫複雜的歷史依賴。另一個角度看也符合著名的圖靈機原理。 即此刻的狀態包含上一刻的歷史,又是下一刻變化的依據。 這其實包含了可編程神經網路的核心概念,即, 當你有一個未知的過程,但你可以測量到輸入和輸出, 你假設當這個過程通過RNN的時候,它是可以自己學會這樣的輸入輸出規律的, 而且因此具有預測能力。 在這點上說, RNN是圖靈完備的。

圖: 圖1即CNN的架構, 圖2到5是RNN的幾種基本玩法。圖2是把單一輸入轉化為序列輸出,例如把圖像轉化成一行文字。 圖三是把序列輸入轉化為單個輸出, 比如情感測試,測量一段話正面或負面的情緒。 圖四是把序列轉化為序列, 最典型的是機器翻譯,n注意輸入和輸出的「時差」。 圖5是無時差的序列到序列轉化, 比如給一個錄像中的每一幀貼標籤。 圖片來源 The unreasonableneffective RNN。

我們用一段小巧的python代碼讓你重新理解下上述的原理:

classRNN:

# ...

def step(self, x):

# update the hidden state

self.h = np.tanh(np.dot(self.W_hh, self.h) + np.dot(self.W_xh, x))

# compute the output vector

y = np.dot(self.W_hy, self.h)

return y

這裡的h就是hidden variable 隱變數,即整個網路每個神經元的狀態,x是輸入, y是輸出, 注意著三者都是高維向量。隱變數h,就是通常說的神經網路本體,也正是循環得以實現的基礎, 因為它如同一個可以儲存無窮歷史信息(理論上)的水庫,一方面會通過輸入矩陣W_xh吸收輸入序列x的當下值,一方面通過網路連接W_hh進行內部神經元間的相互作用(網路效應,信息傳遞),因為其網路的狀態和輸入的整個過去歷史有關, 最終的輸出又是兩部分加在一起共同通過非線性函數tanh。 整個過程就是一個循環神經網路「循環」的過程。 W_hh理論上可以可以刻畫輸入的整個歷史對於最終輸出的任何反饋形式,從而刻畫序列內部,或序列之間的時間關聯, 這是RNN強大的關鍵。

那麼CNN似乎也有類似的功能? 那麼CNN是不是也可以當做RNN來用呢? 答案是否定的,RNN的重要特性是可以處理不定長的輸入,得到一定的輸出。當你的輸入可長可短, 比如訓練翻譯模型的時候, 你的句子長度都不固定,你是無法像一個訓練固定像素的圖像那樣用CNN搞定的。而利用RNN的循環特性可以輕鬆搞定。

圖, CNN(左)和RNN(右)的結構區別, 注意右圖中輸出和隱變數網路間的雙向箭頭不一定存在,往往只有隱變數到輸出的箭頭。

RNN的本質是一個數據推斷(inference)機器, 只要數據足夠多,就可以得到從x(t)到y(t)的概率分布函數, 尋找到兩個時間序列之間的關聯,從而達到推斷和預測的目的。 這裡我們無疑回想到另一個做時間序列推斷的神器- HMM, 隱馬爾科夫模型,n在這個模型里, 也有一個輸入x和輸出y,和一個隱變數h, 而這的h和剛剛的RNN里的h區別是迭代法則,n隱馬通過躍遷矩陣把此刻的h和下一刻的h聯繫在一起。躍遷矩陣隨時間變化, 而RNN中沒有躍遷矩陣的概念,取而代之的是神經元之間的連接矩陣。 HMM本質是一個貝葉斯網路, 因此每個節點都是有實際含義的,而RNN中的神經元只是信息流動的樞紐而已,並無實際對應含義。兩者還是存在千絲萬縷的聯繫, 首先隱馬能幹的活RNN幾乎也是可以做的,比如語言模型,但是就是RNN的維度會更高。在這些任務上RNN事實上是用它的網路表達了隱馬的躍遷矩陣。在訓練方法上, 隱馬可以通過類似EM來自最大後驗概率的演算法得出隱變數和躍遷矩陣最可能的值。 而RNN可以通過一般的梯度回傳演算法訓練。

那麼我們看一些RNN處理任務的具體案例吧:

這段代碼來自gist.github.com/karpath, 大家有興趣的可以下載去訓練一訓練。

比如說, 學說話! 如何叫計算機說出一段類似人話的東西呢?

此處我們從一個非常具體的程序講起, 看你如何一步步的設計一個程序做最簡單的語言生成任務,這個任務的目標類似是讓神經網路做一個接龍, 給它一個字母,讓它猜後面的, 比如給它Hell, 它就跟著街上o。 示意圖如下:

data = open(input.txt, rw).read() # should be simple plain text file

chars = list(set(data)) # vocabulary

data_size, vocab_size = len(data), len(chars)

print data has %d characters, %d unique. % (data_size, vocab_size)

char_to_ix = { ch:i for i,ch in enumerate(chars) } # vocabulary

ix_to_char = { i:ch for i,ch in enumerate(chars) } # index

首先我們把字母表達成向量,用到一個叫enumerate的函數, 這如同在構建語言的數字化詞典(vocabulary), 在這一步之後, 語言信息就變成了數字化的時間序列

hidden_size = 100 # size of hidden layer of neurons

seq_length = 25 # number of steps to unroll the RNN for

learning_rate = 1e-1

# model parameters

Wxh = np.random.randn(hidden_size, vocab_size)*0.01 # input to hidden

Whh = np.random.randn(hidden_size, hidden_size)*0.01 # hidden to hidden

Why = np.random.randn(vocab_size, hidden_size)*0.01 # hidden to output

bh = np.zeros((hidden_size, 1)) # hidden bias

by = np.zeros((vocab_size, 1)) # output bias

下一步我們要初始化三個矩陣,即W_xh, W_hh,W_hy 分別表示輸入和隱層,n隱層和隱層, 隱層和輸出之間的連接,以及隱層和輸出層的激活函數中的bias( bh和by):

Loss=[ ]

Out=[ ]

while True:

# prepare inputs (were sweepingnfrom left to right in steps seq_length long)

if p+seq_length+1 >=nlen(data) or n == 0:

hprev =nnp.zeros((hidden_size,1)) # reset RNN memory

p = 0 # go from start of data

inputs = [char_to_ix[ch] for chnin data[p:p+seq_length]]

targets = [char_to_ix[ch] for chnin data[p+1:p+seq_length+1]]

下一步是正是開始程序, 首先準備輸入:

# sample from the model now andnthen

if n % 100 == 0:

sample_ix = sample(hprev,ninputs[0], 200)

txt = .join(ix_to_char[ix]nfor ix in sample_ix)

print ----n %s n---- % (txt, )

這一步要做的是每訓練一百步看看效果, 看RNN生成的句子是否更像人話。 Sample的含義就是給他一個首字母,然後神經網路會輸出下一個字母,然後這兩個字母一起作為再下一個字母的輸入,依次類推,最後會給出這個函數的定義:

# forward seq_length charactersnthrough the net and fetch gradient

loss, dWxh, dWhh, dWhy, dbh,ndby, hprev,y = lossFun(inputs, targets, hprev)

smooth_loss = smooth_loss *n0.999 + loss * 0.001

if n % 100 == 0:

print iter %d,nloss: %f % (n, smooth_loss) # print progress

這一步是尋找梯度, loss function即計算梯度 , loss function的具體內容關鍵即測量回傳的信息以供學習。函數內容再最後放出

最後一步是根據梯度調整參數的值,即學習的過程。

# perform parameter update withnAdagrad

for param, dparam, mem innzip([Wxh, Whh, Why, bh, by],

[dWxh, dWhh, dWhy, dbh, dby],

[mWxh, mWhh, mWhy, mbh, mby]):

mem += dparam * dparam

param += -learning_rate *ndparam / np.sqrt(mem + 1e-8) # adagrad update

p += seq_length # move datanpointer

n += 1 # iteration counter

Loss.append(loss)

Out.append(txt)

這就是主程序,沒錯, 就是這麼簡單, 剛剛省略的loss function 如下,這個函數的輸出就是錯誤的梯度:

def lossFun(inputs, targets, hprev):

"""

xs, hs, ys, ps = {}, {}, {}, {}

hs[-1] = np.copy(hprev)

loss = 0

# forward pass

for t in xrange(len(inputs)):

xs[t] = np.zeros((vocab_size,1)) # encode in 1-of-k representation

xs[t][inputs[t]] = 1

hs[t] = np.tanh(np.dot(Wxh, xs[t]) + np.dot(Whh, hs[t-1]) + bh) # hidden state

#Whh*hs-->Whh*y_syn*hs; y_syn[t+1]=MishaModel(y_syn[t],tau,U,hs) xe*xg(t)

ys[t] = np.dot(Why, hs[t]) + by # unnormalized log probabilities for next chars

ps[t] = np.exp(ys[t]) / np.sum(np.exp(ys[t])) # probabilities for next chars

loss += -np.log(ps[t][targets[t],0]) # softmax (cross-entropy loss)

# backward pass: compute gradients going backwards

dWxh, dWhh, dWhy = np.zeros_like(Wxh), np.zeros_like(Whh), np.zeros_like(Why)

dbh, dby = np.zeros_like(bh), np.zeros_like(by)

dhnext = np.zeros_like(hs[0])

for t in reversed(xrange(len(inputs))):

dy = np.copy(ps[t])

dy[targets[t]] -= 1

# backprop into y. see CS231n Convolutional Neural Networks for Visual Recognition if confused here

dWhy += np.dot(dy, hs[t].T)

dby += dy

dh = np.dot(Why.T, dy) + dhnext # backprop into h

dhraw = (1 - hs[t] * hs[t]) * dh # backprop through tanh nonlinearity

dbh += dhraw

dWxh += np.dot(dhraw, xs[t].T)

dWhh += np.dot(dhraw, hs[t-1].T)

dhnext = np.dot(Whh.T, dhraw)

for dparam in [dWxh, dWhh, dWhy, dbh, dby]:

np.clip(dparam, -5, 5, out=dparam) # clip to mitigate exploding gradients

return loss, dWxh, dWhh, dWhy, dbh, dby, hs[len(inputs)-1],ys

還有剛剛生成sample的函數,這個東西讓整個程序根據第一個首字母,採樣生成下一個字母, 再迭代推測第三字母, 直到指定字數,形式上看,是得到RNN的輸出,然後這個由輸出得出字母採樣的概率分布:

def sample(h, seed_ix, n):

"""

sample a sequence of integers from the model

h is memory state, seed_ix is seed letter for first time step

"""

x = np.zeros((vocab_size, 1))

x[seed_ix] = 1

ixes = []

for t in xrange(n):

h = np.tanh(np.dot(Wxh, x) + np.dot(Whh, h) + bh)

y = np.dot(Why, h) + by

p = np.exp(y) / np.sum(np.exp(y))

ix = np.random.choice(range(vocab_size), p=p.ravel())

x = np.zeros((vocab_size, 1))

x[ix] = 1

ixes.append(ix)

return ixes

讓我們看看RNN得到的一些訓練結果,訓練素材是網上隨便找的一小段莎劇評論文章:

期初一些亂碼:

T. TpsshbokKbpWWcTxnsOAoTn:og?eunl0op,vHH4tag4,y.ciuf?w4SApx?neh:dfokdrlKvKnaTd?bdvabr.0rSuxaurobkbTf,mb,Htl0uma4HHpeasnn4ub::wslmpscsWmtm?xbH us:HOug4nvdWS4nil hTkbH Smeunwo0tocvTAfyuvme0vihkpviiHT0:

過一會開始有一些單詞模樣的東西出來, 甚至有Shakespear:

am Shakespeare brovid thiais on an 4iwpes cisnoets, primarar Sorld soenth and hathiare orthispeathames ses, An ss porkssork.nutles thake be ynlises hed and porith thes, proy ditsor thake provf provrde

最後已經像是人話了,那真的是人模狗樣的句子啊,以至於讓我猜測它是不是開始思考了,也就是訓練了半小時樣子:

of specific events in his life and provide littlenon the person who experis somewhat a mystery. There are two primary sourcesnthat provide historians with a basic outline of his life…

語言結構通過神經網路可以從一堆亂碼中湧現出來, 這正是目前機器翻譯的state of the art SMT(統計機器翻譯)的基礎, 下面讓我們了解一下大明明鼎鼎的google翻譯又是用了哪些炫技。 首先google翻譯的基礎正是這個遊戲般容易, 卻思想內容極為深刻的RNN。 但是這裡卻做了若干步變化。 這裡要提到RNN的一個變種LSTM。

LSTM(Long short term memory)顧名思義, 是增加了記憶功能的RNN, 首先為什麼要給RNN增加記憶呢? 這裡就要提到一個有趣的概念叫梯度消失(Vanishing Gradient),剛剛說RNN訓練的關鍵是梯度回傳,梯度信息在時間上傳播是會衰減的, 那麼回傳的效果好壞, 取決於這種衰減的快慢, 理論上RNN可以處理很長的信息, 但是由於衰減, 往往事與願違, 如果要信息不衰減,n我們就要給神經網路加記憶,這就是LSTM的原理了。 這裡我們首先再增加一個隱變數作為記憶單元,然後把之前一層的神經網路再增加三層, 分別是輸入門,輸出門,遺忘門, 這三層門就如同信息的閘門, 控制多少先前網路內的信息被保留, 多少新的信息進入,而且門的形式都是可微分的sigmoid函數,確保可以通過訓練得到最佳參數。

信息閘門的原理另一個巧妙的理解是某種「慣性」 機制,隱變數的狀態更新不是馬上達到指定的值,而是緩慢達到這個值, 如同讓過去的信息多了一層緩衝,而要多少緩衝則是由一個叫做遺忘門的東西決定的。 如此我們發現其實這幾個新增加的東西最核心的就是信息的閘門遺忘門。 根據這一原理,我們可以抓住本質簡化lstm,如GRU或極小GRU。 其實我們只需要理解這個模型就夠了,而且它們甚至比lstm更快更好。

我們看一下最小GRU的結構:

摘自論文: Minimal Gated Unit fornRecurrent Neural Networks

第一個方程f即遺忘門, 第二方程如果你對比先前的RNN會發現它是一樣的結構, 只是讓遺忘門f來控制每個神經元放多少之前信息出去(改變其它神經元的狀態), 第三個方程描述「慣性」 ,即最終每個神經元保持多少之前的值,更新多少。

這個結構你理解了就理解了記憶體RNN的精髓。

好了是時候看一下google 翻譯大法是怎麼玩的, 首先,翻譯是溝通兩個不同的語言, 而你要這個溝通的本質是因為它們所表達的事物是相同的, 我們自己的大腦做翻譯的時候,也是根據它們所表達的概念相同比如蘋果-vs-apple來溝通兩個語言的。如果漢語是輸入,英語是輸出,神經網路事實上做的是這樣一件事:

Encoding: 用一個LSTM把漢語變成神經代碼

Decoding:用另一個LSTM把神經代碼轉化為英文。

第一個lstm的輸出是第二個lstm的輸入, 兩個網路用大量語料訓練好即可。 Google這一次2016寄出的大法, 是在其中加入了attention機制 ,這樣google的翻譯系統就更接近人腦。

運用記憶神經網路翻譯的核心優勢是我們可以靈活的結合語境,實現句子到句子,段落到段落的過度, 因為記憶特性使得網路可以結合不同時間尺度的信息而並非只抓住個別單詞, 這就好像你能夠抓住語境而非只是望文生義。也是因為這個RNN有著無窮無盡的應用想像力, 我們將在下一篇繼續講解google翻譯以及rnn的各種應用。

參考文獻 :

The unreasonable effective RNN

Google』s Neural Machine Translation System:nBridging the Gap between Human and Machine Translation

Minimal Gated Unit for Recurrent NeuralnNetworks


推薦閱讀:

小米智能音箱能否成下一個小米手機?
還在下圍棋?足球賽才是人工智慧進擊人類的真正目標
過去十年,我們用了哪些即時戰略遊戲訓練AI?
顛覆者的自我顛覆,榮耀以AI破局行業死水
Unity 中的 AI 模擬群組行為

TAG:机器学习 | 人工智能 | 神经网络 |