參考文章:rnn循環神經網絡介紹
循環神經網絡 (RNN) 是一種專門處理序列的神經網絡。它們通常用于自然語言處理?(NLP) 任務,因為它們在處理文本方面很有效。在這篇文章中,我們將探討什么是 RNN,了解它們是如何工作的,并在?Python 中從頭開始(僅使用?numpy)構建一個真正的 RNN。
這篇文章假設你對神經網絡有基本的了解。
我的另一篇文章(從零開始實現神經網絡(一)_NN神經網絡)涵蓋了你需要知道的一切,所以我建議先閱讀它。
讓我們開始吧!
一. 使用rnn的原因
普通神經網絡(以及CNN)的一個問題是,它們只在預定大小的情況下工作:它們接受固定大小的輸入并產生固定大小的輸出。RNN 很有用,因為它們讓我們可以將可變長度的序列作為輸入和輸出。以下是 RNN 的幾個示例:
輸入為紅色,RNN 本身為綠色,輸出為藍色
這種處理序列的能力使得RNN非常有用。例如:
- 機器翻譯(例如谷歌翻譯)是用“多對多”RNN完成的。原始文本序列被輸入到 RNN 中,然后生成翻譯后的文本作為輸出。
- 情感分析(例如,這是正面評論還是負面評論?)通常使用“多對一”RNN來完成。要分析的文本被輸入到RNN中,然后生成單個輸出分類(例如,這是一個正面評論)。
在本文的后面,我們將從頭開始構建一個“多對一”的 RNN 來執行基本的情感分析。
二. 如何實現rnn
讓我們考慮一個帶有輸入的“多對多”RNN ,輸入是x0,x1,x2.......xn,想要產生輸出y0?,y1?,y2......yn?.這些xi?和yi?是向量,可以具有任意維度。
RNN 通過迭代更新隱藏狀態(h)來工作,這也是一個可以具有任意維度的向量。在任何給定步數(t)時
- 下一個隱藏狀態 (?ht? )使用先前的隱藏狀態( ht-1)和下一個輸入xt?進行計算。舉例:如果要求h2的值,那么就是h1和x2的值進行計算得出。
- 下一個輸出yt?是由ht計算得來?.
這就是 RNN?循環的原因:它對每個步驟使用相同的權重。更具體地說,一個典型的 vanilla RNN 只使用 3 組權重來執行其計算:
?,權重用于所有
→?
之間的運算。
,權重用于所有
??→?
之間的運算。
?,權重用于所有
?→?
之間的運算。
我們還將對 RNN 使用兩種偏差:
,在計算
時被加上。
?,在計算
時被加上.
我們將權重表示為矩陣,將偏差表示為向量。這 3 個權重和 2 個偏差構成了整個 RNN!
以下是將所有內容組合在一起的方程式:
不要略過這些方程式。停下來盯著它看一分鐘。另外,請記住,權重是矩陣,其他變量是向量
使用矩陣乘法應用所有權重,并將偏差添加到所得產品中。然后我們使用?tanh?作為第一個方程的激活函數(但也可以使用其他激活,如?sigmoid)。
三. rnn要解決的問題
讓我們親自動手!我們將從頭開始實現一個 RNN 來執行一個簡單的情感分析任務:確定給定的文本字符串是正向情感的還是負向情感。
以下是我為這篇文章整理的小型數據集中的一些示例:
文本字符串 | 正向? |
---|---|
i am good | ? |
i am bad | ? |
this is very good | ? |
this is not bad | ? |
i am bad not good | ? |
i am not at all happy | ? |
this was good earlier | ? |
i am not at all bad or sad right now | ? |
四. 使用方案
由于這是一個分類問題,我們將使用“多對一”RNN。這類似于我們之前討論的“多對多”RNN,但它只使用最終的隱藏狀態來產生一個輸出y:
每個將是一個向量,表示文本中的單詞。輸出 y 將是一個包含兩個數字的向量,一個表示正數,另一個表示負數。我們將應用?Softmax?將這些值轉換為概率,并最終在正/負之間做出決定。
讓我們開始構建我們的 RNN!
五. 預處理
我前面提到的數據集由兩個 Python 字典組成,True = 正,False = 負
#this file is data.pytrain_data = {'good': True,'bad': False,# ... more data
}test_data = {'this is happy': True,'i am good': True,# ... more data
}
我們必須進行一些預處理,以將數據轉換為可用的格式。首先,我們將構建數據中存在的所有單詞的詞匯表:
#this file is main.py
from data import train_data, test_data# Create the vocabulary.
vocab = list(set([w for text in train_data.keys() for w in text.split(' ')]))
vocab_size = len(vocab)
print('%d unique words found' % vocab_size) # 18 unique words found
vocab
現在包含至少一個訓練文本中出現的所有單詞的列表。接下來,我們將分配一個整數索引來表示詞匯中的每個單詞。
#this file is main.py
# Assign indices to each word.
word_to_idx = { w: i for i, w in enumerate(vocab) }
idx_to_word = { i: w for i, w in enumerate(vocab) }
print(word_to_idx['good']) # 16 (this may change)
print(idx_to_word[0]) # sad (this may change)
現在,我們可以用其相應的整數索引來表示任何給定的單詞!這是必要的,因為 RNN 無法理解單詞——我們必須給它們數字。
最后,回想一下每個輸入? ?我們的RNN是一個向量。我們將使用?one-hot?(獨熱)向量,它包含除了一個是 1 之外其他都為零的向量。每個 one-hot 向量中的“one”將位于單詞的相應整數索引處。
由于我們的詞匯表中有 18 個獨特的單詞,因此每個單詞?將是一個 18 維的獨熱向量。
#this file is main.pyimport numpy as npdef createInputs(text):'''Returns an array of one-hot vectors representing the wordsin the input text string.- text is a string- Each one-hot vector has shape (vocab_size, 1)'''inputs = []for w in text.split(' '):v = np.zeros((vocab_size, 1))v[word_to_idx[w]] = 1inputs.append(v)return inputs
我們稍后將使用向量輸入來傳遞到我們的 RNN。createInputs()
六. 前向傳播
是時候開始實現我們的 RNN 了!我們將首先初始化我們的 RNN 需要的 3 個權重和 2 個偏差:
#this file is rnn.pyimport numpy as np
from numpy.random import randnclass RNN:# A Vanilla Recurrent Neural Network.def __init__(self, input_size, output_size, hidden_size=64):# Weightsself.Whh = randn(hidden_size, hidden_size) / 1000self.Wxh = randn(hidden_size, input_size) / 1000self.Why = randn(output_size, hidden_size) / 1000# Biasesself.bh = np.zeros((hidden_size, 1))self.by = np.zeros((output_size, 1))
注意:我們將除以 1000 以減少權重的初始方差。這不是初始化權重的最佳方法,但它很簡單,適用于這篇文章。
我們使用?np.random.randn()?從標準正態分布初始化我們的權重。
接下來,讓我們實現 RNN 的前向傳播。還記得我們之前看到的這兩個方程式嗎?
以下是代碼實現這個方程式:
#this file is rnn.py
class RNN:# ...def forward(self, inputs):'''Perform a forward pass of the RNN using the given inputs.Returns the final output and hidden state.- inputs is an array of one-hot vectors with shape (input_size, 1).'''h = np.zeros((self.Whh.shape[0], 1))# Perform each step of the RNNfor i, x in enumerate(inputs):h = np.tanh(self.Wxh @ x + self.Whh @ h + self.bh)# Compute the outputy = self.Why @ h + self.byreturn y, h
很簡單,對吧?請注意,我們初始化了?h?到第一步的零向量,因為沒有上一步?h?我們可以在當前節點上使用。
#this file is main.py
# ...def softmax(xs):# Applies the Softmax Function to the input array.return np.exp(xs) / sum(np.exp(xs))# Initialize our RNN!
rnn = RNN(vocab_size, 2)inputs = createInputs('i am very good')
out, h = rnn.forward(inputs)
probs = softmax(out)
print(probs) # [[0.50000095], [0.49999905]]
如果您需要復習 Softmax,請閱讀我當前系列的cnn文章,里面有詳細介紹。
我們的 RNN 有效,但還不是很有用。讓我們改變一下......
七. 反向傳播
為了訓練我們的RNN,我們首先需要一個損失函數。我們將使用交叉熵損失,它通常與 Softmax 配對。以下是我們的計算方式:
pc是我們的 RNN 對正確類別(正或負)的預測概率。例如,如果我們的 RNN 預測一個正文本為 90%,則損失為:
想要更長的解釋嗎?閱讀我的CNN中的交叉熵損失部分。
現在我們有一個損失,我們將使用梯度下降來訓練我們的 RNN,以盡量減少損失。這意味著是時候推導出一些梯度了!
???以下部分假定您具備多變量微積分的基本知識。如果你愿意,你可以跳過它,但我建議你略讀一下,即使你不太了解。在得出結果時,我們將逐步編寫代碼,即使是表面的理解也會有所幫助。
7.1 定義
首先,定義一些變量:
- 讓 y 表示 RNN 的原始輸出。
- 讓 p 表示最終概率:
- 讓?c?引用某個文本樣本的真實標簽,也稱為“正確”類。
- 讓?L?是交叉熵損失:
- 讓
?,
,
成為我們 RNN 中的 3 個權重矩陣。
- 讓
?和
成為我們 RNN 中的 2 個偏置向量。
7.2 設置
接下來,我們需要修改我們的正向傳播,以緩存一些數據以在反向傳播使用。與此同時,我們還將為反向傳播搭建骨架。
rnn.py
class RNN:# ...def forward(self, inputs):'''Perform a forward pass of the RNN using the given inputs.Returns the final output and hidden state.- inputs is an array of one-hot vectors with shape (input_size, 1).'''h = np.zeros((self.Whh.shape[0], 1))self.last_inputs = inputsself.last_hs = { 0: h }# Perform each step of the RNNfor i, x in enumerate(inputs):h = np.tanh(self.Wxh @ x + self.Whh @ h + self.bh)self.last_hs[i + 1] = h# Compute the outputy = self.Why @ h + self.byreturn y, hdef backprop(self, d_y, learn_rate=2e-2):'''Perform a backward pass of the RNN.- d_y (dL/dy) has shape (output_size, 1).- learn_rate is a float.'''pass
是不是好奇我們為什么要做這個緩存?在前面cnn的章節中有相關解釋
?
7.3 梯度(求偏導)
現在是數學時間!我們將從計算開始.
我們知道:
我將留下?使用鏈式法則:
這里假設當前要計算的索引值的i,那么當i是正確的類c時,有:
?
例如,如果我們有p=[0.?2,0.?2,0.6],正確的類是c=0,那么我們會得到=[?0.8,0.2,0.6]。這也很容易轉化為代碼:
# Loop over each training example
for x, y in train_data.items():inputs = createInputs(x)target = int(y)# Forwardout, _ = rnn.forward(inputs)probs = softmax(out)# Build dL/dyd_L_d_y = probsd_L_d_y[target] -= 1# Backwardrnn.backprop(d_L_d_y)
?好。接下來,讓我們來看看偏導和
,僅用于將最終的隱藏狀態轉換為 RNN 的輸出。我們有
?是最終的隱藏狀態。因此
同樣地
我們現在可以開始實現反向傳播了!backprop()
class RNN:# ...def backprop(self, d_y, learn_rate=2e-2):'''Perform a backward pass of the RNN.- d_y (dL/dy) has shape (output_size, 1).- learn_rate is a float.'''n = len(self.last_inputs)# Calculate dL/dWhy and dL/dby.d_Why = d_y @ self.last_hs[n].T d_by = d_y
?
提醒:我們之前創建過。
self.last_hs? ?
forward()
最后,我們需要偏導,
,
,在 RNN 期間的每一步都會使用。我們有:
因為改變?影響每一個?
,這些都會影響y,并最終影響L。為了充分計算
,我們需要通過所有時間步進反向傳播,這稱為時間反向傳播?(BPTT):
用于所有xt??→?ht?前向鏈接,因此我們必須向反傳播回每個鏈接。
一旦我們到達給定的步驟t,我們需要計算:
我們所知道的tanh求導:
我們像之前那樣使用鏈式法則:
同理:
最后一步,我們需要,我們可以遞歸計算:
我們將從最后一個隱藏狀態開始實現 BPTT 并反向傳播,因此我們已經有了?到我們要計算
的時候?!最后一個隱藏狀態是例外,
:
我們現在擁有最終實現 BPTT 并完成了所需的一切:backprop()
class RNN:# ...def backprop(self, d_y, learn_rate=2e-2):'''Perform a backward pass of the RNN.- d_y (dL/dy) has shape (output_size, 1).- learn_rate is a float.'''n = len(self.last_inputs)# Calculate dL/dWhy and dL/dby.d_Why = d_y @ self.last_hs[n].Td_by = d_y# Initialize dL/dWhh, dL/dWxh, and dL/dbh to zero.d_Whh = np.zeros(self.Whh.shape)d_Wxh = np.zeros(self.Wxh.shape)d_bh = np.zeros(self.bh.shape)# Calculate dL/dh for the last h.d_h = self.Why.T @ d_y# Backpropagate through time.for t in reversed(range(n)):# An intermediate value: dL/dh * (1 - h^2)temp = ((1 - self.last_hs[t + 1] ** 2) * d_h)# dL/db = dL/dh * (1 - h^2)d_bh += temp# dL/dWhh = dL/dh * (1 - h^2) * h_{t-1}d_Whh += temp @ self.last_hs[t].T# dL/dWxh = dL/dh * (1 - h^2) * xd_Wxh += temp @ self.last_inputs[t].T# Next dL/dh = dL/dh * (1 - h^2) * Whhd_h = self.Whh @ temp# Clip to prevent exploding gradients.for d in [d_Wxh, d_Whh, d_Why, d_bh, d_by]:np.clip(d, -1, 1, out=d)# Update weights and biases using gradient descent.self.Whh -= learn_rate * d_Whhself.Wxh -= learn_rate * d_Wxhself.Why -= learn_rate * d_Whyself.bh -= learn_rate * d_bhself.by -= learn_rate * d_by
?需要注意的幾點:
- 我們已經合并了
到
- 我們會不斷更新一個包含最新變量的變量
d_h,
?,我們需要計算
?.
- 完成 BPTT 后,我們?np.clip()?限制低于 -1 或高于 1 的梯度值。這有助于緩解梯度爆炸問題,即梯度由于具有大量相乘項而變得非常大。對于普通 RNN 來說,梯度的爆炸或消失是相當成問題的——更復雜的 RNN(如?LSTM)通常更有能力處理它們。
- 計算完所有梯度后,我們使用梯度下降更新權重和偏差。
我們做到了!我們的 RNN 已經完成了。
八. 進行預測
終于到了我們期待的時刻——讓我們測試一下我們的 RNN!
首先,我們將編寫一個輔助函數來使用 RNN 處理數據:
import randomdef processData(data, backprop=True):'''Returns the RNN's loss and accuracy for the given data.- data is a dictionary mapping text to True or False.- backprop determines if the backward phase should be run.'''items = list(data.items())random.shuffle(items)loss = 0num_correct = 0for x, y in items:inputs = createInputs(x)target = int(y)# Forwardout, _ = rnn.forward(inputs)probs = softmax(out)# Calculate loss / accuracyloss -= np.log(probs[target])num_correct += int(np.argmax(probs) == target)if backprop:# Build dL/dyd_L_d_y = probsd_L_d_y[target] -= 1# Backwardrnn.backprop(d_L_d_y)return loss / len(data), num_correct / len(data)
?現在,我們可以編寫訓練循環:
# Training loop
for epoch in range(1000):train_loss, train_acc = processData(train_data)if epoch % 100 == 99:print('--- Epoch %d' % (epoch + 1))print('Train:\tLoss %.3f | Accuracy: %.3f' % (train_loss, train_acc))test_loss, test_acc = processData(test_data, backprop=False)print('Test:\tLoss %.3f | Accuracy: %.3f' % (test_loss, test_acc))
?運行應該輸出如下內容:main.py
--- Epoch 100
Train: Loss 0.688 | Accuracy: 0.517
Test: Loss 0.700 | Accuracy: 0.500
--- Epoch 200
Train: Loss 0.680 | Accuracy: 0.552
Test: Loss 0.717 | Accuracy: 0.450
--- Epoch 300
Train: Loss 0.593 | Accuracy: 0.655
Test: Loss 0.657 | Accuracy: 0.650
--- Epoch 400
Train: Loss 0.401 | Accuracy: 0.810
Test: Loss 0.689 | Accuracy: 0.650
--- Epoch 500
Train: Loss 0.312 | Accuracy: 0.862
Test: Loss 0.693 | Accuracy: 0.550
--- Epoch 600
Train: Loss 0.148 | Accuracy: 0.914
Test: Loss 0.404 | Accuracy: 0.800
--- Epoch 700
Train: Loss 0.008 | Accuracy: 1.000
Test: Loss 0.016 | Accuracy: 1.000
--- Epoch 800
Train: Loss 0.004 | Accuracy: 1.000
Test: Loss 0.007 | Accuracy: 1.000
--- Epoch 900
Train: Loss 0.002 | Accuracy: 1.000
Test: Loss 0.004 | Accuracy: 1.000
--- Epoch 1000
Train: Loss 0.002 | Accuracy: 1.000
Test: Loss 0.003 | Accuracy: 1.000
?
九. 結束
就是這樣!在這篇文章中,我們完成了循環神經網絡的演練,包括它們是什么、它們是如何工作的、為什么它們有用、如何訓練它們以及如何實現它們。