循環神經網路RNN介紹1:什麼是RNN、為什麼需要RNN、前後向傳播詳解、Keras實現

該文章的主要內容來自:Fundamentals of Deep Learning – Introduction to Recurrent Neural Networks,筆者對該文章進行了翻譯、注釋、糾錯、修改、公式編輯等,使文章更容易理解。

簡介

讓我們從一個問題開始,你能理解下面這句英文的意思嗎?「working love learning we on deep」,答案顯然是無法理解。那麼下面這個句子呢?「We love working on deep learning」,整個句子的意思通順了!我想說的是,一些簡單的詞序混亂就可以使整個句子不通順。那麼,我們能期待傳統神經網路使語句變得通順嗎?不能!如果人類的大腦都感到困惑,我認為傳統神經網路很難解決這類問題。

在日常生活中有許多這樣的問題,當順序被打亂時,它們會被完全打亂。例如,

  • 我們之前看到的語言——單詞的順序定義了它們的意義
  • 時間序列數據——時間定義了事件的發生
  • 基因組序列數據——每個序列都有不同的含義

有很多這樣的情況,序列的信息決定事件本身。如果我們試圖使用這類數據得到有用的輸出,就需要一個這樣的網路:能夠訪問一些關於數據的先前知識(prior knowledge),以便完全理解這些數據。因此,循環神經網路(RNN)粉墨登場。

在這篇文章中,我假設讀者了解神經網路的基本原理,如果你不知道的話,請在繼續閱讀之前先看知乎的相關文章,或者作者知乎專欄的相關文章。

目錄

  1. 我們需要一個用於處理序列的神經網路
  2. 什麼是循環神經網路(RNN)
  3. 理解循環神經元(Recurrent Neuron)的細節
  4. 用Excel實現循環神經元的前向傳播
  5. 循環神經網路的後向傳播(BPTT)
  6. Keras部署循環神經網路
  7. 梯度爆炸和消失問題
  8. 其他RNN框架

我們需要一個用於處理序列的神經網路

在深入了解循環神經網路的細節之前,讓我們考慮一下我們是否真的需要一個專門處理序列信息的網路。還有,我們可以使用這樣的網路實現什麼任務。

遞歸神經網路的優點在於其應用的多樣性。當我們使用RNN時,它有強大的處理各種輸入和輸出類型的能力。看下面的例子。

  • 情感分析(Sentiment Classification) – 這可以是簡單的把一條推文分為正負兩種情緒的任務。所以輸入是任意長度的推文, 而輸出是固定的長度和類型.

  • 圖像標註(Image Captioning) – 假設我們有一個圖片,我們需要一個對該圖片的文本描述。所以,我們的輸入是單一的圖像,輸出是一系列或序列單詞。這裡的圖像可能是固定大小的,但輸出是不同長度的文字描述。

  • 語言翻譯(Language Translation) – 這裡假設我們想將英文翻譯為法語. 每種語言都有自己的語義,對同一句話有不同的長度。因此,這裡的輸入和輸出是不同長度的。

因此,RNNs可用於將輸入映射到不同類型、長度的輸出,並根據實際應用泛化。讓我們看看RNN的架構是怎樣的。

什麼是循環神經網路(RNN)

假設我們的任務是預測句子中的下一個詞。讓我們嘗試使用MLP(多層感知機)完成它。先來看最簡單的形式,我們有一個輸入層、一個隱藏層和一個輸出層。輸入層接收輸入,隱藏層激活,最後接收層得到輸出。

接下來搭建更深層的網路,其中有多個隱藏層。在這裡,輸入層接收輸入,第一層隱藏層激活該輸入,然後將這些激活發送到下一個隱藏層,並層層進行連續激活以得到輸出。每個隱藏層有自己的權重和偏差。

由於每個隱藏層都有自己的權重和激活,所以它們具有獨立的行為。現在的目標是確定連續輸入之間的關係。我們能直接把輸入給隱藏層嗎?當然可以!

這些隱藏層的權重和偏差是不同的。因此,每一層都是獨立的,不能結合在一起。為了將這些隱藏層結合在一起,我們使這些隱藏層具有相同的權重和偏差。

我們現在可以將這些隱藏層結合在一起,所有隱藏層的權重和偏差相同。所有這些隱藏層合併為一個循環層

這就像將輸入給隱藏層一樣。在所有時間步(time steps)(後面會介紹什麼是時間步),循環神經元的權重都是一樣的,因為它現在是單個神經元。因此,一個循環神經元存儲先前輸入的狀態,並與當前輸入相結合,從而保持當前輸入與先前輸入的某些關係。

理解循環神經元(Recurrent Neuron)的細節

讓我們先做一個簡單的任務。讓我們使用一個字元級別的RNN,在這裡我們有一個單詞「Hello」。所以我們提供了前4個字母h、e、l、l,然後讓網路來預測最後一個字母,也就是「o」。所以這個任務的辭彙表只有4個字母h、e、l、o。在涉及自然語言處理的實際情況中,辭彙表一般會包括整個維基百科資料庫中的單詞,或一門語言中的所有單詞。為了簡單起見,這裡,我們使用了非常小的辭彙表。

讓我們看看上面的結構是如何被用來預測「hello」這個單詞的第五個字母的。在上面的結構中,藍色RNN塊,對輸入和之前的狀態應用了循環遞歸公式。在我們的任務中,字母「h」前面沒有任何其他字母,我們來看字母「e」。當字母e被提供給網路時,將循環遞歸公式應用於輸入(也就是字母e)和前一個狀態(也就是字母h),得到新的狀態。也就是說,在t-1的時候,輸入是h,輸出是h_{t-1},在t時刻,輸入是e和h_{t-1},輸出是h_t,這裡每次應用循環遞歸公式稱為不同的時間步

描述當前狀態的循環遞歸公式如下:

h_t = f(h_{t-1},x_t)

這裡h_t是t時刻的狀態, h_{t-1}是前一時刻的狀態,x_t是當前的輸入。我們有的是前一時刻的狀態而不是前一時刻的輸入, 因為輸入神經元將前一時刻的輸入轉換為前一時刻的狀態。所以每一個連續的輸入被稱為時間步

在我們的案例中,我們有四個輸入(h、e、l、l),在每一個時間步應用循環遞推公式時,均使用相同的函數和相同的權重。

考慮循環神經網路的最簡單形式,激活函數是tanh,權重是W_{hh},輸入神經元的權重是W_{xh},我們可以寫出t時刻的狀態公式如下

h_t = tanh(W_{hh}h_{t-1}+W_{xh}x_{t})

在上述情況下,循環神經元僅僅是將之前的狀態考慮進去。對於較長的序列,方程可以包含多個這樣的狀態。一旦最終狀態被計算出來我們就可以得到輸出了。

現在,一旦得到了當前狀態,我們可以計算輸出了。

y_t = W_{yh}h_t

Ok,我們來總結一下循環神經元的計算步驟:

  1. 將輸入時間步提供給網路,也就是提供給網路x_t
  2. 接下來利用輸入和前一時刻的狀態計算當前狀態,也就是h_t
  3. 當前狀態變成下一步的前一狀態h_{t-1}
  4. 我們可以執行上面的步驟任意多次(主要取決於任務需要),然後組合從前面所有步驟中得到的信息。
  5. 一旦所有時間步都完成了,最後的狀態用來計算輸出y_t
  6. 輸出與真實標籤進行比較並得到誤差。
  7. 誤差通過後向傳播(後面將介紹如何後向傳播)對權重進行升級,進而網路訓練完成。

接下來,我們用Excel來計算一下這些狀態,並得到輸出。

用Excel實現循環神經元的前向傳播

我們先來看看輸入。

我們對輸入進行one-hot編碼。這是因為我們的整個辭彙表只有四個字母{h,e,l,o}。

接下來我們將利用w_{xh}把輸入轉換為隱藏狀態,這裡我們採取隨機的方式將權重初始化為3x4的矩陣。

(註:這裡矩陣的大小為什麼是3x4?因為我們想計算w_{xh}x_t,其中x_t是4x1的one-hot矩陣,根據矩陣乘法運算的法則,w_{xh}大小必須是nx4,n一般取比4小的值,因此w_{xh}的矩陣維度取3x4)

步驟1:

輸入網路的第一個字母是「h」, 我們想要得到隱藏層狀態,那麼首先我們需要計算W_{xh}x_t。通過矩陣相乘,我們得到

步驟2:

現在我們看一下循環神經元, 權重W_{hh}是一個1x1的矩陣,值為0.427043,偏差也是1x1的矩陣,值為0.56700.

對於字母「h」,沒有前一個狀態,所以我們也可以認為前一個狀態是[0,0,0,0]。

接下來計算

w_{hh}h_{t-1}+bias

(譯者註:讀者一定注意到了,1x1的矩陣與一個4x1的矩陣相乘再加上一個1x1的矩陣,根據矩陣乘法的規則,是無法運算的。因此這裡應該是使用了矩陣廣播運算,而得到的結果應該是4x1的矩陣,而不是3x1的矩陣,但是被強制轉換為了3x1的矩陣,原因是步驟1的輸出結果是3x1的矩陣,接下來的步驟3將計算步驟1和步驟2的相加,所以步驟2的輸出必須是3x1)

步驟3:

Ok,有了前兩步,我們就可以計算當前循環神經元的狀態了,根據以下公式

h_t = tanh(W_{hh}h_{t-1}+W_{xh}x_{t})

將前兩步的結果代入公式即可得到當前步的狀態,計算如下

步驟4:

現在我們繼續關注下一個字母「e」被傳入網路。上一步計算得到的h_t現在變成了這一步的h_{t-1},而e的one-hot向量是x_t.現在我們來計算以下這一步的h_t.

h_t = tanh(W_{hh}h_{t-1}+W_{xh}x_{t})

首先計算W_{hh}h_{t-1} + bias

再計算W_{xh}x_t

(譯者註:注意觀察,計算上一步的狀態與計算此步驟的狀態使用的權重是一樣的,也就是說使用的是同樣的W_{hh}W_{xh},所以說循環神經元的特點是**權重共享)

步驟5:

有了步驟4的結果,代入公式可得輸入字母「e」後的狀態

同樣,這一步得到的狀態將變成下一步的h_{t-1},而循環神經元將使用這個狀態和新輸入字母來計算下一個狀態.

步驟6:

在每一個狀態,循環神經元還會計算輸出. 現在我們來計算一下字母e的輸出y_t.

y_t = W_{hy}h_t

(譯者註:注意,一個循環神經元根據輸入和前一時間步的狀態計算當前時間步的狀態,然後根據當前時間步的狀態計算輸出。另外需要注意的是,這裡的W_{hy}的維度大小是4x3,這是因為我們想得到4x1的輸出,因為one-hot的維度是4x1,而通過下一步的計算每一個維度可以代表該維度的字母出現的概率)

步驟7:

通過應用softmax函數,我們可以得到辭彙表中一個特定字母的出現的概率,所以我們現在計算softmax(y_t)

我們來理解一下得到的概率值。我們看到,這個模型認為字母e後面將出現的字母是h,因為概率最高的是代表字母h的那一維。可是實際上下一個字母應該是l,我們哪裡做錯了嗎?並沒有,只是我們還沒有訓練我們的網路。

好,我們面對的下一個大問題就是:RNN網路中如何實現後向傳播?如何通過反饋循環來升級我們的權重?

循環神經網路的後向傳播(BPTT)

很難憑想像理解一個遞歸神經網路的權重是如何更新的。因此,為了理解和可視化反向傳播,讓我們按照時間步展開網路。在其中我們可以計算也可以不計算每一個時間步的輸出。

在向前傳播的情況下,輸入隨著每一個時間步前進。在反向傳播的情況下,我們「回到過去」改變權重,因此我們叫它通過時間的反向傳播(BPTT)

如果y_t是預測值,hat y_t是對應的實際值,那麼,誤差通過交叉熵損失來計算:

E_t(hat y_t,y_t) = – hat y_t log(y_t)

E(hat y,y) = – sum hat y_t log(y_t)

我們通常把整個序列(單詞)看作一個訓練樣本,所以總的誤差是每個時間步(字元)中誤差的和。權重在每一個時間步長是相同的(所以可以計算總誤差後一起更新)。讓我們總結一下反向傳播的步驟。

  1. 首先使用預測輸出和實際輸出計算交叉熵誤差
  2. 網路按照時間步完全展開
  3. 對於展開的網路,對於每一個實踐步計算權重的梯度
  4. 因為對於所有時間步來說,權重都一樣,所以對於所有的時間步,可以一起得到梯度(而不是像神經網路一樣對不同的隱藏層得到不同的梯度)
  5. 隨後對循環神經元的權重進行升級

展開的網路看起來像一個普通的神經網路。反向傳播也類似於普通的神經網路,只不過我們一次得到所有時間步的梯度。我知道你在擔心什麼,現在如果有100個時間步,那麼網路展開後將變得非常巨大(這是個挑戰性的問題,我們後面講介紹如何克服)。

如果你不想深入了解這背後的數學,所有你需要知道的是,按照時間步展開後的反向傳播類似於常規神經網路的反向傳播。我還將寫一個有詳細數學公式的關於循環神經網路的詳細文章。

Keras部署循環神經網路

讓我們使用循環神經網路來預測推文代表的情緒。我們希望將這些推文標記為正或負。你可以在這下載數據集。

我們有大約1600000條推文來訓練我們的網路。如果你不熟悉自然語言處理的基礎知識,我強烈建議你閱讀這篇文章). 或者這篇關於詞嵌入(word embedding)的詳細文章。

下面讓我們來使用RNN來將推文分為正類或負類。

# import all librariesimport kerasfrom keras.models import Sequentialfrom keras.layers import Dense, Activation, Dropoutfrom keras.layers.convolutional import Conv1Dfrom keras.preprocessing.text import Tokenizerfrom keras.preprocessing.sequence import pad_sequencesimport pandas as pdimport numpy as npimport spacynlp=spacy.load("en")#load the datasettrain=pd.read_csv("../datasets/training.1600000.processed.noemoticon.csv" , encoding= "latin-1")Y_train = train[train.columns[0]]X_train = train[train.columns[5]]# split the data into test and trainfrom sklearn.model_selection import train_test_splittrainset1x, trainset2x, trainset1y, trainset2y = train_test_split(X_train.values, Y_train.values, test_size=0.02,random_state=42 )trainset2y=pd.get_dummies(trainset2y)# function to remove stopwordsdef stopwords(sentence): new=[] sentence=nlp(sentence) for w in sentence: if (w.is_stop == False) & (w.pos_ !="PUNCT"): new.append(w.string.strip()) c=" ".join(str(x) for x in new) return c# function to lemmatize the tweetsdef lemmatize(sentence): sentence=nlp(sentence) str="" for w in sentence: str+=" "+w.lemma_ return nlp(str)#loading the glove modeldef loadGloveModel(gloveFile): print("Loading Glove Model") f = open(gloveFile,r) model = {} for line in f: splitLine = line.split() word = splitLine[0] embedding = [float(val) for val in splitLine[1:]] model[word] = embedding print ("Done."),len(model),(" words loaded!") return model# save the glove modelmodel=loadGloveModel("/mnt/hdd/datasets/glove/glove.twitter.27B.200d.txt")#vectorising the sentencesdef sent_vectorizer(sent, model): sent_vec = np.zeros(200) numw = 0 for w in sent.split(): try: sent_vec = np.add(sent_vec, model[str(w)]) numw+=1 except: pass return sent_vec#obtain a clean vectorcleanvector=[]for i in range(trainset2x.shape[0]): document=trainset2x[i] document=document.lower() document=lemmatize(document) document=str(document) cleanvector.append(sent_vectorizer(document,model))#Getting the input and output in proper shapecleanvector=np.array(cleanvector)cleanvector =cleanvector.reshape(32000,200,1)#tokenizing the sequencestokenizer = Tokenizer(num_words=16000)tokenizer.fit_on_texts(trainset2x)sequences = tokenizer.texts_to_sequences(trainset2x)word_index = tokenizer.word_indexprint(Found %s unique tokens. % len(word_index))data = pad_sequences(sequences, maxlen=15, padding="post")print(data.shape)#reshape the data and preparing to traindata=data.reshape(32000,15,1)from sklearn.model_selection import train_test_splittrainx, validx, trainy, validy = train_test_split(data, trainset2y, test_size=0.3,random_state=42 )#calculate the number of wordsnb_words=len(tokenizer.word_index)+1#obtain theembedding matrixembedding_matrix = np.zeros((nb_words, 200))for word, i in word_index.items(): embedding_vector = model.get(word) if embedding_vector is not None: embedding_matrix[i] = embedding_vectorprint(Null word embeddings: %d % np.sum(np.sum(embedding_matrix, axis=1) == 0))trainy=np.array(trainy)validy=np.array(validy)#building a simple RNN modeldef modelbuild(): model = Sequential() model.add(keras.layers.InputLayer(input_shape=(15,1))) keras.layers.embeddings.Embedding(nb_words, 15, weights=[embedding_matrix], input_length=15, trainable=False) model.add(keras.layers.recurrent.SimpleRNN(units = 100, activation=relu, use_bias=True)) model.add(keras.layers.Dense(units=1000, input_dim = 2000, activation=sigmoid)) model.add(keras.layers.Dense(units=500, input_dim=1000, activation=relu)) model.add(keras.layers.Dense(units=2, input_dim=500,activation=softmax)) model.compile(loss=categorical_crossentropy, optimizer=adam, metrics=[accuracy] return model#compiling the modelfinalmodel = modelbuild()finalmodel.fit(trainx, trainy, epochs=10, batch_size=120,validation_data=(validx,validy))

如果你運行上述模型,效果可能不是特別完美,因為這是一個非常簡單的架構和相當淺的網路。我強烈建議讀者自己調一調網路結構以取得更好的結果。另外,有多種方法可以對數據進行預處理。預處理將完全取決於手頭的任務。

梯度爆炸和消失問題

RNN基於這樣的機制,信息的結果依賴於前面的狀態或前N個時間步。普通的RNN可能在學習長距離依賴性方面存在困難。例如,如果我們有這樣一句話,「The man who ate my pizza has purple hair」。在這種情況下,purple hair描述的是The man,而不是pizza。所以這是一個長距離的依賴關係。

如果我們在這種情況下後向傳播,我們就需要應用鏈式法則。在三個時間步後對第一個求梯度的公式如下:

?E/?W = ?E/?y3 ?y3/?h3 ?h3/?y2 *?y2/?h1 .. 這就是一個長距離的依賴關係.

在這裡,我們應用了鏈式規則,如果任何一個梯度接近0,所有的梯度都會成指數倍的迅速變成零。這樣將不再有助於網路學習任何東西。這就是所謂的消失梯度問題。

消失梯度問題與爆炸梯度問題相比,對網路更有威脅性,梯度爆炸就是由於單個或多個梯度值變得非常高,梯度變得非常大。

之所以我們更關心梯度消失問題,是因為通過一個預定義的閾值可以很容易地解決梯度爆炸問題。幸運的是,也有一些方法來處理消失梯度問題。如LSTM結構(長短期記憶網路)和GRU(門控性單位)可以用來處理消失的梯度問題。

其他RNN框架

當我們考慮長距離依賴性時,RNNs面臨梯度消失問題。當參數的數量變得非常大時,它們也變得難以訓練。如果我們展開網路,它變得如此巨大以至於如何收斂將是一個棘手的挑戰。

長短期記憶網路–通常被稱為「LSTMs」–是一種特殊的RNN網路,能夠學習長距離依賴性。由Hochreiter & Schmidhuber首先提出。他們在各種各樣的問題上工作得非常出色,現在得到了廣泛的應用。LSTMs也有這種鏈狀結構,但重複的模塊有一些稍微不同的結構。不是有一個單一的神經網路層,而是有多個層,以非常特殊的方式進行交互。它們有輸入門、忘記門和輸出門。我們將很快推出LSTMs的詳細文章。

另一個有效的RNN網路架構是門控循環單元即GRUs。他們是LSTMs的變體,但是在結構上更簡單,也更容易訓練。它們的成功主要是由於門控網路信號控制當前輸入和先前記憶如何使用,從而更新當前的激活並生成當前狀態。這些門有自己的權重集,在學習階段自適應地更新。我們這裡只有兩個門,重置門和更新門。敬請關注GRUs更詳細的文章。

結束語

希望這篇文章能讓你對循環神經網路有一個初步的了解。在以後的文章中我們將深入了解循環神經網路背後的數學,以及LSTMs和GRUs。試著使用這些RNNs並被他們的性能和應用所震撼。請在評論部分分享你的看法。


推薦閱讀:

TAG:深度学习DeepLearning | RNN |