文章目錄
一、RNN的理論部分
1.1 Why Recurrent Neural Network
我們之前學習的 DNN,CNN。在某一些領域都取得了顯著的成效(例如 CNN 在 CV 領域的卓越成績)。但是他們都只能單獨的取處理一個個的輸入,前一個輸入和後一個輸入是完全沒有關係的。但是,某些任務需要能夠更好的處理序列的信息,即前面的輸入和後面的輸入是有關係的。
但是,當我們在理解一句話意思時,孤立的理解這句話的每個詞是不夠的,我們需要處理這些詞連接起來的整個序列; 當我們處理視頻的時候,我們也不能只單獨的去分析每一幀,而要分析這些幀連接起來的整個序列。
所以爲了解決一些這樣類似的問題,能夠更好的處理序列的信息,RNN就誕生了
1.2 RNN 的工作原理解析
1.2.1 數據的定義部分
首先,我們約定一個數學符號:我們用 表示輸入的時間序列。舉一個最常見的例子:如果我們需要進行文本人名的識別。我們將會給 RNN 輸入這樣一個時間序列 :
我們定義 作爲 時刻序列對應位置的輸入。 表示序列的長度。也就是說,我們現在是把這一句完整的話拆分成了 個單詞。其中每一個單詞用 表示。例如這裏的 就表示成 、 就表示成 。因此,現在整個句子就可以表示成:
下一步:因爲我們的任務是找出這一句話裏面是人名的部分,而我們知道,每一個詞都有可能是人名,所以現在看起來,我們的 RNN 的輸出應該要和這個句子的長度保持一致。我們用 來表示 RNN 在 時刻的輸出。所以,RNN 的輸出可以表示成:
用 表示輸出的長度,在這裏 。但是當然 ,他麼兩個可以不相等,這將在後面介紹。
我們用 0 表示不是人名,1 表示是人名。所以上面這個句子對應的標籤 應該是:
下面,我們應該如何表示 呢?首先能夠想到的一種方法是建立一個 庫,這個庫儘可能包含大部分的詞。例如像下面這樣:
假設這個 庫 是一個 10000x1 的向量。
然後我們對準備作爲輸入的這句話:
的每一個單詞 都可以表示成這個 10000 x 1 的向量,其中 和 詞彙庫裏面相等的那個位置記爲 1 ,不相等的地方記爲 0。也就是構成一個 編碼。這個 10000 ,將會是我們後面將要提到的
那麼,這個是一個樣本的情況。如果存在多個樣本(也就是採用 的方法,那麼我們定義 表示爲第 個樣本在第 時刻的詞。
1.2.2 RNN 的具體運算過程
首先看一個單層的 RNN 結構:
那麼大家可能會產生疑問:這裏看起來不是已經好多層了嗎?怎麼還是單層的?—— 其實,這就是 RNN 有別於 DNN, CNN 的一點了, RNN 的拓撲結構發生了很大的改變。我們需要明確一點:對於 RNN 而言,橫向對齊的就視爲同一層—— 這是因爲:這一層所有的參數都是共享的!
既然談到了參數,那麼我們就有必要看看 RNN 是如何進行前向傳播的:
RNN 需要有兩個輸入:
- 原本該時刻的單詞輸入
- 上一個時刻的激活值(或者說隱藏值)
我們這裏的矩形框代表了類似於 DNN 裏面的一個隱藏層,它執行的是下面的計算過程:
那麼,對於第一個時刻的輸入,它確實有 ,但是此時並沒有上一個時刻的激活值 (因爲現在就是第一個時刻)。此時我們可以給 賦值成 0 向量作爲輸入)
下面我們就以對句子:
進行名字識別爲例,假設我們對每一個詞用 10000x1 的詞彙表進行獨熱編碼,那麼很容易想到,我們整個句子就是一個 10000 x 9 的矩陣:
假設我們的權值 是一個維度爲 100 x 10000 的矩陣。我們設 激活值的維度是 100 x 100, 的維度也是 100 x 100。根據式子:
如果我們把這兩個權值合併爲一個:,那麼這個 其實就是 和 的合併。合併方法就是水平合併:
如果我們把輸入也合併成一個矩陣,那麼應該是縱向合併:
這樣一來,我們 RNN 的激活值輸出就可以簡化地表示成:
看到這兒,可能大家又會有疑問了:RNN 的輸出 呢?它怎麼辦?
我們現在就畫出 RNN 一次前向傳播完整的計算圖:
1.2.3 幾種不同類型的 RNN
我們上面所討論的是輸入長度 等於輸出長度 的情況,當然 也有 不等於 的情況——例如:多對多、多對一、一對多、一對一等等情況。我們可以根據需要再深入學習。
二、基於Pytorch的RNN實踐部分
2.1 在Pytorch裏面對 RNN 輸入參數的認識
Pytorch 裏面爲我們封裝好了 ,每次向網絡中輸入batch個樣本,每個時刻處理的是該時刻的 batch 個樣本。我們首先來看看 Pytorch 裏面 的參數:
- :輸入 的特徵大小,比如說我們剛剛用一個 10000 x 1的詞彙庫去表示一個句子裏面的其中一個詞,所以,此時的 就是 10000
- : 可以理解爲隱藏層神經元的數目
- : RNN 裏面層的數量
- : 激活函數,默認爲 ,可以設置爲
- : 是否設置偏置,默認爲
- : 默認爲 , 設置爲 之後,輸入輸出爲
- : 默認爲0(當層數較多,神經元數目較多時, 特別有用)
- : 默認爲 , 則設置 RNN 爲雙向
上面的參數介紹裏面提到了幾個詞: 這該怎麼理解呢?
比如說,我們還是以找尋句子裏面的人名爲例,但是這次的情況是:我一次給 RNN 輸入3句話,每句話10個單詞,每個單詞用 10000維 的向量(10000 行的詞彙表)表示。那麼對應的 就是 3; 就是 10 ; 就是 10000.(值得注意的是: 應該就是 RNN 的時間步)
說到這裏,我們再舉一個例子:
self.rnn = nn.RNN(
input_size=INPUT_SIZE,
hidden_size=32, # rnn hidden unit
num_layers=1, # number of rnn layer
batch_first=True, # input & output will has batch size as 1s dimension. e.g. (batch, time_step, input_size)
)
這樣,我們就定義好了一個 RNN 層。
2.2 nn.RNN 裏面的 forward 方法:
在對 RNN 進行前向傳播時,注意這裏調用的不是我們自己寫的 ,而是 Pytorch裏面 nn.RNN 的方法。具體格式如下:
rnn_out, h_state = self.rnn(x, h_state)
輸入的第一參數 ,它是一次性將所有時刻特徵喂入的,而不需要每次喂入當前時刻的 ,所以其 是
輸入的第二參數 是第一個時刻空間上所有層的記憶單元的Tensor,,只是還要考慮循環網絡空間上的層數,所以這裏輸入的 是
如上圖所示,返回值有兩個 和 ,其中,
每一個時刻上空間上最後一層的輸出(但是注意:這個輸出不是我們所說的 ,要產生 還需要我們再設計一個 ),所以它的shape是
是最後一個時刻空間上所有層的記憶單元,它和 的維度應該是一樣的:
Example:利用RNN進時間序列的預測
在本次的例子裏面,我們的目的是用 函數預測 函數。主要還是爲了熟悉 RNN 關於輸入輸出的一些細節。那麼第一步就是導入必要的包啦:
import torch
from torch import nn
from torch.autograd import Variable
import numpy as np
import matplotlib.pyplot as plt
下面我們定義一些超參數:
# Hyper Parameters
TIME_STEP = 10 # rnn time step
INPUT_SIZE = 1 # 說明一下:因爲在每一個時間節上我們輸入的數據就只是一個數據,並不像詞那樣用一個詞彙表編碼,所以這裏input size就是1
LR = 0.02 # learning rate
展示一下我們的數據:
# show data
steps = np.linspace(0, np.pi*2, 100, dtype=np.float32) # float32 for converting torch FloatTensor
x_np = np.sin(steps)
y_np = np.cos(steps)
plt.plot(steps, y_np, 'r-', label='target (cos)')
plt.plot(steps, x_np, 'b-', label='input (sin)')
plt.legend(loc='best')
plt.show()
下面是重點部分:我們開始構造我們的 RNN model:下面細節的解釋都會在註釋裏面
class RNN(nn.Module):
def __init__(self):
super(RNN, self).__init__()
self.rnn = nn.RNN(
input_size=INPUT_SIZE,
hidden_size=32, # rnn hidden unit
num_layers=1, # number of rnn layer
batch_first=True, # input & output will has batch size as 1s dimension. e.g. (batch, time_step, input_size)
)
self.out = nn.Linear(32, 1) #說明:這裏是 RNN 之外再加入的一個全連接層
def forward(self, x, h_state):
# x(輸入)的維度就是(batch, time_step, input_size)
# h_state (n_layers, batch, hidden_size)
# r_out (batch, time_step, hidden_size)
r_out, h_state = self.rnn(x, h_state) #注意:這裏調用了nn.RNN的forward方法,輸出兩個,請看上文對它們的解釋
#print('第step次迭代, RNN所有時間結點上隱藏層的輸出維度:', r_out.size()) #[batch, seq_len, hidden_len]
outs = [] # save all predictions 這裏我們需要定義一個空的列表,用於存放每一個時間節真正的輸出(而不是r_out)
for time_step in range(r_out.size(1)): # calculate output for each time step r_out.size(1)seq_len也即是時間節的長度
outs.append(self.out(r_out[:, time_step, :])) #這裏用[:, time_step,:]取出第time_step時刻的r_out作爲nn.Linear的輸入,用於計算該時刻真正的輸出
return torch.stack(outs, dim=1), h_state #最後我們需要把每一個時間節得到的output按照第二個維度拼起來
好的,在搞清楚 Pytorch 裏面 RNN 的輸入輸出以及前向傳播的計算過程之後,我們就要開始訓練了:
rnn = RNN()
optimizer = torch.optim.Adam(rnn.parameters(), lr=LR) # optimize all cnn parameters
loss_func = nn.MSELoss()
h_state = None # for initial hidden state 因爲第1個時間節沒有前一時刻的激活值,這裏我們可以用None作爲輸入
plt.figure(1, figsize=(12, 5))
plt.ion() # continuously plot
for step in range(100): #訓練100代
start, end = step * np.pi, (step+1)*np.pi # time range
# use sin predicts cos
steps = np.linspace(start, end, TIME_STEP, dtype=np.float32, endpoint=False) # float32 for converting torch FloatTensor
x_np = np.sin(steps)
y_np = np.cos(steps)
x = Variable(torch.from_numpy(x_np[np.newaxis, :, np.newaxis])) # shape (batch, time_step, input_size) 給 x_np加上第一個和第三個維度,都是1,因爲這裏默認batch = 1, input_size=1
#print('x的維度:', x.shape) [1, 10, 1]
y = Variable(torch.from_numpy(y_np[np.newaxis, :, np.newaxis]))
#print('y的維度:', y.shape) [1, 10, 1]
prediction, h_state = rnn(x, h_state)
#Be careful!!!!#####
h_state = Variable(h_state.data) # repack the hidden state, break the connection from last iteration
#上面這一步我們需要把 RNN 第n次迭代生成的激活值作爲下一代訓練裏面的 h0 輸入,要重新打包成 Variable
loss = loss_func(prediction, y) # calculate loss
optimizer.zero_grad() # clear gradients for this training step
loss.backward() # backpropagation, compute gradients
optimizer.step() # apply gradients
#plotting
plt.plot(steps, y_np.flatten(), 'r-')
plt.plot(steps, prediction.data.numpy().flatten(), 'b-')
plt.draw();
plt.pause(0.05)
plt.ioff()
plt.show()
至此,我們應該對 RNN 的工作機理有了一個較爲深入的瞭解。但是,在實際工程中,數據清洗與數據集的製作將會遠遠難於 RNN 本身的構造。這也需要我們有一個較深入的編程能力。雖然 Pytorch 等深度學習框架可以如此方便地自動計算梯度等等,但是數據集製作效果的好壞直接影響了我們 的表現。
然而,你以爲故事到這兒就結束了嗎?
如果我們現在的工作是讓機器填詞:假設我們給機器輸入這樣一段話:
我們希望機器正確地填出最後一個詞:當然希望是 ,然而假設中間這1000個詞都和 沒什麼大關係,那麼機器就需要記住句子一開始的 。這無疑會給 RNN 反向傳播帶來極大的困難,可能會造成梯度消失。那麼如何解決這個問題呢?—— 因此 和它的變體 應運而生。
在之後的 裏面,我們會詳細地學習 的工作機理,以及如何在 裏面實現 LSTM