當前位置:
首頁 > 最新 > 一文詳解循環神經網路的基本概念

一文詳解循環神經網路的基本概念

作者 | 李理

目前就職於環信,即時通訊雲平台和全媒體智能客服平台,在環信從事智能客服和智能機器人相關工作,致力於用深度學習來提高智能機器人的性能。

寫在前面

由於工作太忙,這個系列文章有一年多沒有更新了。最近在整理資料時用到了裡面的一些內容,覺得做事情應該有始有終,所以打算把它繼續完成。下面的系列文章會首先會介紹 vanilla RNN 的代碼,希望讀者能夠通過代碼更加深入的了解RNN的原理。代碼會著重於 forward 的介紹,而對 BPTT 一帶而過。之前的文章為了讓讀者了解原理,我們都是自己來實現梯度的計算和各種優化演算法。但是在實際的工作中,我們一般使用一些成熟的深度學習框架。因為框架把常用的演算法都做了封裝,我們的代碼會更加簡單而不易出錯;此外框架的實現效率一般會比我們的更高,會利用 GPU 來加速訓練。

我們之前在 CNN 的地方介紹了 theano,但是深度學習的發展變化也很快,theano目前已是一個死掉的項目。目前用戶最多的深度學習框架是TensorFlow,但是在 RNN 方面,基於動態圖的 PyTorch 更加方便,所以這個系列文章會使用 PyTorch。因此介紹過 vanilla RNN 之後會簡單的介紹一下 PyTorch,尤其是 PyTorch 在 RNN 方面相關模塊。然後會介紹一些 PyTorch 的例子,接下來會介紹 seq2seq(encoder-decoder) 模型和注意力機制,包括它在機器翻譯里的應用,我們會自己實現一個簡單的漢語-英語的翻譯系統。

最後一部分就是我們的主題—— Image Caption Generation,有了前面 CNN 和 RNN 的基礎,實現它就非常輕鬆了。

本章會介紹循環神經網路的基本概念。

基本概念

▌RNN

RNN 的特點是利用序列的信息。之前我們介紹的神經網路假設所有的輸入是相互獨立的。但是對於許多任務來說這不是一個好的假設。如果你想預測一個句子的下一個詞,知道之前的詞是有幫助的。RNN 被成為遞歸的 (recurrent) 原因就是它會對一個序列的每一個元素執行同樣的操作,並且之後的輸出依賴於之前的計算。另外一種看待 RNN 的方法是可以認為它有一些「記憶」能捕獲之前計算過的一些信息。理論上 RNN 能夠利用任意長序列的信息,但是實際中它能記憶的長度是有限的。

圖5.1顯示了怎麼把一個 RNN 展開成一個完整的網路。比如我們考慮一個包含5個詞的句子,我們可以把它展開成 5 層的神經網路,每個詞是一層。RNN 的計算公式如下:

1.是 t 時刻的輸入。

2.是 t 時刻的隱狀態。

它是網路的「記憶」。的計算依賴於前一個時刻的狀態和當前時刻的輸入:

。函數 f 通常是諸如 tanh 或者 ReLU 的非線性函數。,這是用來計算第一個隱狀態,通常我們可以初始化成0。

圖5.1: RNN 展開圖

3.是 t 時刻的輸出。

有一些事情值得注意:

1. 你可以把看成是網路的「記憶」。

捕獲了從開始到前一個時刻的所有(感興趣) 的信息。輸出只基於當前時刻的記憶。不過實際應用中很難記住很久以前的信息。

2. 參數共享

和傳統的深度神經網路不同,這些傳統的網路每層使用不同的參數,RNN 的參數(上文的 U, V, W ) 是在所有時刻共享(一樣) 的。這反映這樣一個事實:我們每一步都在執行同樣的操作,只不過輸入不同而已。這種結構極大的減少了我們需要學習的參數【同時也讓信息得以共享,是的訓練變得可能】

3. 每一個時刻都有輸出

上圖每一個時刻都有輸出,但我們不一定都要使用。比如我們預測一個句子的情感傾向是我們只關注最後的輸出,而不是每一個詞的情感。類似的,我們也不一定每個時刻都有輸入。RNN 最主要的特點是它有隱狀態(記憶),它能捕獲一個序列的信息。

▌RNN 的擴展

1. 雙向 RNN (Bidirectional RNNs)

它的思想是 t 時刻的輸出不但依賴於之前的元素,而且還依賴之後的元素。比如,我們做完形填空,在句子中「挖」掉一個詞,我們想預測這個詞,我們不但會看之前的詞,也會分析之後的詞。雙向 RNN 很簡單,它就是兩個 RNN 堆疊在一起。輸出依賴兩個 RNN 的隱狀態。

2. 深度(雙向) RNN (Deep (Bidirectional) RNNs)

和雙向 RNN 類似,不過多加幾層。當然它的表示能力更強,需要的訓練數據也更多。

▌RNN 代碼示例

接下來我們通過簡單的代碼來演示的 RNN,用 RNN 來實現一個簡單的 Char RNN 語言模型。為了讓讀者了解 RNN 的一些細節,本示例會使用 numpy 來實現 forward 和 backprop 的計算。RNN 的反向傳播演算法一般採用 BPTT,如果讀者不太明白也不要緊,但是 forward 的計算一定要清楚。本章後續的內容會使用 PyTorch 來實現更複雜的 seq2seq 模型、注意力機制來做機器翻譯以及 Image Caption Generation。這個 RNN 代碼來自 karpathy 的 blog 文章《The Unreasonable Effectiveness of Recurrent Neural Networks》附帶的代碼:

https://gist.github.com/karpathy/d4dee566867f8291f086

代碼總共一百來行,我們下面逐段來閱讀。

圖5.2: 雙向RNN

數據預處理

data = open("../data/tiny-shakespeare.txt","r").read()# should be simple

plain text file

chars = list(set(data))

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

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

char_to_ix =

ix_to_char =

上面的代碼讀取莎士比亞的文字到字元串 data 里,通過 set() 得到所有的字元並放到chars 這個 list 里。然後得到 char_to_ix 和 ix_to_char 兩個 dict,分別表示字元到 id 的映射和 id 到字元的映射( id 從零開始)

模型超參數和參數定義

# 超參數

hidden_size = 100# 隱藏層神經元的個數

seq_length = 25# BPTT時最多的unroll的步數

learning_rate = 1e-1

圖5.3: 多層雙向RNN

模型參數

上面的代碼定義超參數 hidden_size,seq_length 和 learning_rate,以及模型的參數 Wxh, Whh 和 Why。

lossFun

def lossFun(inputs, targets, hprev):

"""

inputs,targets都是整數的list

hprev是Hx1的數組,是隱狀態的初始值

返回loss,梯度和最後一個時刻的隱狀態

"""

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

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

http://cs231n.github.io/neural-networks-case-study/#grad 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]

我們這裡只閱讀一下 forward 的代碼,對 backward 代碼感興趣的讀者請參考:

https://github.com/pangolulu/rnn-from-scratch

# 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

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)

上面的代碼變數每一個時刻 t,首先把字母的 id 變成 one-hot 的表示,然後計算hs[t],計算方法是:hs[t] = np.tanh(np.dot(Wxh, xs[t]) + np.dot(Whh, hs[t - 1]) +bh)。也就是根據當前輸入 xs[t] 和上一個狀態 hs[t-1] 計算當前新的狀態 hs[t],注意如果 t=0 的時候 hs[t-1] = hs[-1] = np.copy(hprev),也就是函數參數傳入的隱狀態的初始值 hprev。接著計算 ys[t] = np.dot(Why, hs[t]) + by。然後用softmax 把它變成概率:ps[t] = np.exp(ys[t]) / np.sum(np.exp(ys[t]))。最後計算交叉熵的損失:loss +=-np.log(ps[t][targets[t], 0])。注意:ps[t] 的 shape 是 [vocab_size,1]

sample 函數

這個函數隨機的生成一個句子(字元串)。

def sample(h, seed_ix, n):

"""

使用rnn模型生成一個長度為n的字元串

h是初始隱狀態,seed_ix是第一個字元

"""

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

sample 函數會生成長度為n 的字元串。一開始 x 設置為 seed_idx:x[seed_idx]=1 (這是one-hot 表示),然後和 forward 類似計算輸出下一個字元的概率分布 p。然後根據這個分布隨機採樣一個字元 (id) ix,把 ix 加到結果 ixes 里,最後用這個ix 作為下一個時刻的輸入:x[ix]=1

訓練

n, p = 0, 0

mWxh, mWhh, mWhy = np.zeros_like(Wxh), np.zeros_like(Whh), np.zeros_like(Why)

mbh, mby = np.zeros_like(bh), np.zeros_like(by)# memory variables for

Adagrad

smooth_loss = -np.log(1.0 / vocab_size) * seq_length# loss at iteration 0

while True:

# prepare inputs (we"re sweeping from left to right in steps seq_length

long)

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

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

p = 0# go from start of data

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

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

# sample from the model now and then

if n % 1000 == 0:

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

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

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

# forward seq_length characters through the net and fetch gradient

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

smooth_loss = smooth_loss * 0.999 + loss * 0.001

if n % 1000 == 0:

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

# perform parameter update with Adagrad

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

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

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

mem += dparam * dparam

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

p += seq_length# move data pointer

n += 1# iteration counter

上面是訓練的代碼,首先初始化 mWxh, mWhh, mWhy。因為這裡實現的是Adgrad,所以需要這些變數來記錄每個變數的「delta」,有興趣的讀者可以參考:

http://cs231n.github.io/neural-networks-3/#ada

接下來是一個無限循環來不斷的訓練,首先是得到一個訓練數據,輸入是data[p:p + seq_length],而輸出是data[p+1:p +seq_length+1]。然後是lossFun 計算這個樣本的 loss,梯度和最後一個時刻的隱狀態(用於下一個時刻的隱狀態的初始值),然後用梯度更新參數。每 1000 次訓練之後會用sample 函數生成一下句子,可以通過它來了解目前模型生成的效果。

完整代碼:

https://github.com/fancyerii/deep_learning_theory_and_practice/blob/master/codes/ch05/rnn.py

▌LSTM/GRU

長距離依賴(Long Term Dependency) 問題

RNN 最有用的地方在於它(可能) 能夠把之前的信息傳遞到當前時刻,比如在理解一個視頻的當前幀時利用之前的幀是非常有用的。如果 RNN 可以做到這一點,那麼它會非常有用。但是它能夠實現這個目標嗎?

圖5.4: RNN 的短距離依賴

圖5.5: RNN 的長距離依賴

有的時候,我們只需要最近的一些信息就可以很好的預測當前的任務。比如在語言模型里,我們需要預測「the clouds are in the ?」的下一個單詞,我們很容易根據之前的這幾個此就可以預測最可能的詞是「sky」。如圖 5.4 所示,我們要預測的需要的信息距離不是太遠。

但是有的時候我們需要更多的上下文信息來預測。比如「I grew up in France…Ispeak fluent ?」。最近的信息「I speak fluent」 暗示後面很可能是一種語言,但是我們無法確定是哪種語言,除非我們有更久之前的上下文「I grew up in France」。因此為了準確的預測,我們可能需要依賴很長距離的上下文。如圖 5.5 所示,為了預測,我們需要很遠的。理論上,如果我們的參數學得足夠好,RNN 是可以學習到這種長距離依賴關係的。但是很不幸的是,在實際應用中 RNN 很難學到。

接下來會介紹的 LSTM 就是試圖解決這個問題。

圖5.6: RNN 的結構

圖5.7: LSTM

Long Short Term Memory(LSTM) 網路基本概念

本節內容主要來自Colah 的博客:

http://colah.github.io/posts/2015-08-Understanding-LSTMs/

LSTM 是一種特殊的RNN 網路,它使用門(Gate) 的機制來解決長距離依賴的問題。

回顧一下,所有的RNN 都是如圖 5.6 的結構,把 RNN 看成一個黑盒子的話,它會有一個「隱狀態」來「記憶」一些重要的信息。當前時刻的輸出除了受當前輸入影響之外,也受這個「隱狀態」影響。並且在這個時刻結束時,除了輸出之外,這個「隱狀態」的內容也會發生變化——可能「記憶」了新的信息同時有「遺忘」了一些舊的信息。

LSTM 也是這樣的結果,只不過相比於原始的 RNN,它的內部結構更加複雜。

普通的 RNN 就是一個全連接的層,而 LSTM 有四個用於控制」記憶「和運算的門,如圖5.7所示。

這個圖初看比較複雜,我們後面會詳細解釋裡面的細節。在介紹之前,我們首先來熟悉圖中的一下部件,如圖 5.8 所示。

圖5.8: LSTM 示意圖的組件

圖5.9: LSTM Cell State 的通道

在圖 5.8 中,每條有向邊代表向量,黃色的方框代表神經網路層,粉色的圓圈代表逐點運算(Pointwise Operation)。兩條邊的合併代表向量的拼接(concatenation),邊的分叉代表把一個向量複製到兩個地方。

LSTM 核心思想

LSTM 除了有普通 RNN 的隱狀態之外還有一個叫 Cell State 的 Cell 狀態,它基本是從上一個時刻直接通到下一個時刻的(後面會介紹修改它的操作),所以以前的重要」記憶「理論上可以很容易保存下來,如圖 5.9 所示,圖上從到存在直接的通道。

當然如果 LSTM 只是原封不動的保存之前的」記憶「,那就沒有太多價值,它還必須根據需要,能夠增加新的記憶同時擦除舊的無用的記憶。LSTM 是通過一種叫作門的機制來控制怎麼擦除舊記憶寫入新記憶的,下面我們逐步來介紹它的這種機制。

如圖 5.10 所示,門可以用來控制信息是否能夠通過,它一般是一個激活函數是sigmoid 的層,0 表示阻止任何信息通過,1 表示所有信息通過,而0-1 直接的值表示部分通過。

LSTM 門的細節

首先我們來了解 LSTM 的遺忘門(Forget Gate),它會決定遺忘多少之前的記憶。它的輸入是上一個時刻的隱狀態和當前時刻的輸入,它的輸出是 0-1 直接的數,0 表示完全遺忘之前的記憶,而 1 表示完全保留原來的記憶。

圖5.10: LSTM 的Gate

圖5.11: LSTM 的Forget Gate

圖5.12: LSTM 的Input Gate

如圖 5.11 所示:

這個乘以就表示上一個時刻的需要遺忘多少信息。

接下來LSTM 有一個輸入門,它用來控制輸入的信息多少可以進入LSTM。t 時刻的輸入候選,注意的激活函數是 tanh,因為輸入的範圍我們不能限制,因此用 (-1,1) 的 tanh;而門我們要求它的範圍是 (0,1),因此門用 sigmoid 激活。然後把輸入門和輸入候選點乘起來,表示當前時刻有多少信息應該進入 Cell State,如圖 5.12 所示。

接著把上一個時刻未遺忘的信息和當前時刻候選累加得到新的,如圖 5.13 所示:

最後我們需要計算當前時刻的輸出(它就是隱狀態),它是使用當前的使用tanh 計算後再通過一個輸出門(Output Gate) 得到,如圖 5.14 所示。

圖5.13: LSTM 計算t 時刻的Ct

圖5.14: ot 的計算

LSTM 的變種

下面介紹一些常見的 LSTM 變種,包括很流行的 GRU(Gated Recurrent Unit)。第一種變體是計算三個門時不只利用和,還使用,也就是從有一個 peephole 的邊,如圖 5.15 所示。

第二種變體就是遺忘門不但決定遺忘多少的信息,而且會乘以中用於控制多少新的信息進入,如圖 5.16 所示。

第三種就是 GRU,它把遺忘門和輸入門合併成一個更新門(Update Gate),並且

圖5.15: 有peephole 連接的LSTM

5.16: LSTM 變種2

圖5.17: GRU

把 Cell State 和 Hidden State 也合併成一個 Hidden State,它的計算如圖 5.17 所示。

和 LSTM 不同,在計算的時候會用乘以,類似與 LSTM 的遺忘門。而在計算新的時,表示從里保留的信息比例,而表示從里更新的信息比例。

下節預告:PyTorch 教程(敬請關注)


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 人工智慧頭條 的精彩文章:

AI變身記:不光能有人的智能,還要像狗一樣「思考」
商湯獲6億美元C輪融資,與曠視的戰爭將如何繼續?

TAG:人工智慧頭條 |