【神經網絡和深度學習】—— 從理論到實踐深入理解RNN(Recurrent Neural Network) 基於Pytorch實現

一、RNN的理論部分

1.1 Why Recurrent Neural Network

我們之前學習的 DNN,CNN。在某一些領域都取得了顯著的成效(例如 CNN 在 CV 領域的卓越成績)。但是他們都只能單獨的取處理一個個的輸入,前一個輸入和後一個輸入是完全沒有關係的。但是,某些任務需要能夠更好的處理序列的信息,即前面的輸入和後面的輸入是有關係的。

但是,當我們在理解一句話意思時,孤立的理解這句話的每個詞是不夠的,我們需要處理這些詞連接起來的整個序列; 當我們處理視頻的時候,我們也不能只單獨的去分析每一幀,而要分析這些幀連接起來的整個序列。

所以爲了解決一些這樣類似的問題,能夠更好的處理序列的信息,RNN就誕生了

1.2 RNN 的工作原理解析

1.2.1 數據的定義部分

首先,我們約定一個數學符號:我們用 xx 表示輸入的時間序列。舉一個最常見的例子:如果我們需要進行文本人名的識別。我們將會給 RNN 輸入這樣一個時間序列 xxHarry  Potter  and  Hermione  Granger  invented  a  new  spell Harry\space\space Potter\space\space and\space\space Hermione\space\space Granger\space\space invented\space\space a\space\space new\space\space spell
我們定義 x<t>x^{<t>} 作爲 tt 時刻序列對應位置的輸入TxTx 表示序列的長度。也就是說,我們現在是把這一句完整的話拆分成了 TxTx 個單詞。其中每一個單詞用 x<t>x^{<t>} 表示。例如這裏的 HarryHarry 就表示成 x<1>x^{<1>}GrangerGranger 就表示成 x<5>x^{<5>}。因此,現在整個句子就可以表示成:[x<1>x<2>x<3>x<4>x<5>x<6>x<7>x<8>x<9>] \begin{bmatrix} x^{<1>} & x^{<2>} & x^{<3>} & x^{<4>} & x^{<5>}& x^{<6>}& x^{<7>} & x^{<8>} & x^{<9>} \end{bmatrix}

下一步:因爲我們的任務是找出這一句話裏面是人名的部分,而我們知道,每一個詞都有可能是人名,所以現在看起來,我們的 RNN 的輸出應該要和這個句子的長度保持一致。我們y<t>y^{<t>} 來表示 RNN 在 tt 時刻的輸出。所以,RNN 的輸出可以表示成:[y<1>y<2>y<3>y<4>y<5>y<6>y<7>y<8>y<9>] \begin{bmatrix} y^{<1>} & y^{<2>} & y^{<3>} & y^{<4>} & y^{<5>} & y^{<6>} & y^{<7>} & y^{<8>} & y^{<9>} \end{bmatrix}

TyTy 表示輸出的長度,在這裏 Tx=TyTx = Ty。但是當然 ,他麼兩個可以不相等,這將在後面介紹。

我們用 0 表示不是人名,1 表示是人名。所以上面這個句子對應的標籤 labellabel 應該是:[1100110000] \begin{bmatrix} 1 & 1 & 0 & 0 & 1 & 1 & 0 & 0 & 0 & 0 \end{bmatrix}

下面,我們應該如何表示 x<t>x^{<t>} 呢?首先能夠想到的一種方法是建立一個 VocabularyVocabulary 庫,這個庫儘可能包含大部分的詞。例如像下面這樣:[aabackharrypotterzulu] \begin{bmatrix} a\\ aback\\ \vdots\\ harry\\ \vdots\\ potter\\ \vdots\\ zulu \end{bmatrix}
假設這個 VocabularyVocabulary 庫 是一個 10000x1 的向量。

然後我們對準備作爲輸入的這句話:Harry  Potter  and  Hermione  Granger  invented  a  new  spell Harry\space\space Potter\space\space and\space\space Hermione\space\space Granger\space\space invented\space\space a\space\space new\space\space spell
的每一個單詞 x<t>x^{<t>} 都可以表示成這個 10000 x 1 的向量,其中 x<t>x^{<t>} 和 詞彙庫裏面相等的那個位置記爲 1 ,不相等的地方記爲 0。也就是構成一個 onehotone-hot 編碼。這個 10000 ,將會是我們後面將要提到的 input_sizeinput\_size

那麼,這個是一個樣本的情況。如果存在多個樣本(也就是採用 mini_batchmini\_batch 的方法,那麼我們定義 X(i)<t>X(i)^{<t>}表示爲第 ii 個樣本在第 tt 時刻的詞。

1.2.2 RNN 的具體運算過程

首先看一個單層的 RNN 結構:
在這裏插入圖片描述那麼大家可能會產生疑問:這裏看起來不是已經好多層了嗎?怎麼還是單層的?—— 其實,這就是 RNN 有別於 DNN, CNN 的一點了, RNN 的拓撲結構發生了很大的改變。我們需要明確一點:對於 RNN 而言,橫向對齊的就視爲同一層—— 這是因爲:這一層所有的參數都是共享的!

既然談到了參數,那麼我們就有必要看看 RNN 是如何進行前向傳播的:

RNN 需要有兩個輸入:

  1. 原本該時刻的單詞輸入 x<t>x^{<t>}
  2. 上一個時刻的激活值(或者說隱藏值)a<t1>a^{<t-1>}

我們這裏的矩形框代表了類似於 DNN 裏面的一個隱藏層,它執行的是下面的計算過程:
a<t>=tanh(Waaa<t1>+Waxx<t>+ba) y<t>=g(Wyaa<t>+by) a^{<t>} = tanh(W_{aa}a^{<t-1>} + W_{ax}x^{<t>} + b_a)\\ \space\\ y^{<t>}=g(W_{ya}a^{<t>}+b_y)

那麼,對於第一個時刻的輸入,它確實有 x<1>x^{<1>},但是此時並沒有上一個時刻的激活值 a<0>a^{<0>}因爲現在就是第一個時刻)。此時我們可以給 a<0>a^{<0>} 賦值成 0 向量作爲輸入

下面我們就以對句子:Harry  Potter  and  Hermione  Granger  invented  a  new  spell Harry\space\space Potter\space\space and\space\space Hermione\space\space Granger\space\space invented\space\space a\space\space new\space\space spell

進行名字識別爲例,假設我們對每一個詞用 10000x1 的詞彙表進行獨熱編碼,那麼很容易想到,我們整個句子就是一個 10000 x 9 的矩陣:[00100000001000001000] \begin{bmatrix} 0 & 0 & \cdots & 1 &0 & 0\\ 0 & 0 & \cdots & 0 & 0 & 0\\ \vdots & \vdots & & \vdots & \vdots & \vdots\\ 1 & 0 &\cdots &0 & 0 & 0\\ \vdots & \vdots & & \vdots & \vdots & \vdots\\ 0 & 1 &\cdots &0 & 0 &0\\ \vdots & \vdots & & \vdots & \vdots & \vdots\\ \end{bmatrix}

假設我們的權值 WaxW_{ax} 是一個維度爲 100 x 10000 的矩陣。我們設 激活值的維度是 100 x 100,WaaW_{aa} 的維度也是 100 x 100。根據式子:a<t>=tanh(Waaa<t1>+Waxx<t>+ba) a^{<t>} = tanh(W_{aa}a^{<t-1>} + W_{ax}x^{<t>} + b_a)

如果我們把這兩個權值合併爲一個:WaW_a,那麼這個 WaW_a 其實就是 WaaW_{aa}WaxW_{ax} 的合併。合併方法就是水平合併:Waa=[WaaWax] W_{aa} = [W_{aa} \quad|\quad W_{ax}]
如果我們把輸入也合併成一個矩陣,那麼應該是縱向合併:[a<t1>x<t>] \begin{bmatrix} a^{<t-1>}\\ ——\\ x^{<t>} \end{bmatrix}
這樣一來,我們 RNN 的激活值輸出就可以簡化地表示成:a<t>=WaX+baa^{<t>} = W_aX+b_a

看到這兒,可能大家又會有疑問了:RNN 的輸出 y<t>y^{<t>} 呢?它怎麼辦?

我們現在就畫出 RNN 一次前向傳播完整的計算圖:
在這裏插入圖片描述

1.2.3 幾種不同類型的 RNN

我們上面所討論的是輸入長度 TxT_x 等於輸出長度 TyT_y 的情況,當然 也有 TxT_x 不等於 TyT_y 的情況——例如:多對多、多對一、一對多、一對一等等情況。我們可以根據需要再深入學習。

二、基於Pytorch的RNN實踐部分

2.1 在Pytorch裏面對 RNN 輸入參數的認識

Pytorch 裏面爲我們封裝好了 nn.RNNnn.RNN,每次向網絡中輸入batch個樣本,每個時刻處理的是該時刻的 batch 個樣本。我們首先來看看 Pytorch 裏面 nn.RNNnn.RNN 的參數:

  1. input_sizeinput\_size :輸入 xx 的特徵大小,比如說我們剛剛用一個 10000 x 1的詞彙庫去表示一個句子裏面的其中一個詞,所以,此時的 input_sizeinput\_size 就是 10000
  2. hidden_sizehidden\_size: 可以理解爲隱藏層神經元的數目
  3. num_layersnum\_layers: RNN 裏面層的數量
  4. nonlinearitynonlinearity: 激活函數,默認爲 tanhtanh,可以設置爲 relurelu
  5. biasbias: 是否設置偏置,默認爲 TrueTrue
  6. batch_firstbatch\_first: 默認爲 falsefalse, 設置爲 TrueTrue 之後,輸入輸出爲 (batch_size,seq_len,input_size)(batch\_size, seq\_len, input\_size)
  7. dropoutdropout: 默認爲0(當層數較多,神經元數目較多時,dropoutdropout 特別有用)
  8. bidirectionalbidirectional: 默認爲 FalseFalseTrueTrue 則設置 RNN 爲雙向

上面的參數介紹裏面提到了幾個詞:batch_size,seq_len,input_sizebatch\_size, seq\_len, input\_size 這該怎麼理解呢?

比如說,我們還是以找尋句子裏面的人名爲例,但是這次的情況是:我一次給 RNN 輸入3句話,每句話10個單詞,每個單詞用 10000維 的向量(10000 行的詞彙表)表示。那麼對應的 batch_sizebatch\_size 就是 3;seq_lenseq\_len 就是 10 ;input_sizeinput\_size 就是 10000.值得注意的是:aeq_lenaeq\_len 應該就是 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 進行前向傳播時,注意這裏調用的不是我們自己寫的 forwardforward,而是 Pytorch裏面 nn.RNN 的方法。具體格式如下:

rnn_out, h_state = self.rnn(x, h_state)

輸入的第一參數 xx ,它是一次性將所有時刻特徵喂入的,而不需要每次喂入當前時刻的 x<t>x^{<t>},所以其 shapeshape[batch_size,seq_len,input_size][batch\_size, seq\_len, input\_size]

輸入的第二參數 h_stateh\_state第一個時刻空間上所有層的記憶單元的Tensor,,只是還要考慮循環網絡空間上的層數,所以這裏輸入的 shapeshape[num_layer,batch_size,hidden_size][num\_layer,batch\_size ,hidden\_size]

在這裏插入圖片描述

如上圖所示,返回值有兩個 rnn_outrnn\_outh_stateh\_state,其中,
rnn_outrnn\_out每一個時刻上空間上最後一層的輸出(但是注意:這個輸出不是我們所說的 y^\hat{y},要產生y^\hat{y} 還需要我們再設計一個 nn.Linearnn.Linear),所以它的shape是 [batch_size,seq_len,hidden_size][batch\_size, seq\_len, hidden\_size]

h_stateh\_state最後一個時刻空間上所有層的記憶單元,它和 h0h_0 的維度應該是一樣的:[num_layer,batch_size,hidden_size][num\_layer,batch\_size ,hidden\_size]

Example:利用RNN進時間序列的預測

在本次的例子裏面,我們的目的是用 sinsin 函數預測 coscos 函數。主要還是爲了熟悉 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 等深度學習框架可以如此方便地自動計算梯度等等,但是數據集製作效果的好壞直接影響了我們 modelmodel 的表現。

然而,你以爲故事到這兒就結束了嗎?

如果我們現在的工作是讓機器填詞:假設我們給機器輸入這樣一段話:I  am  Chinese(1000  words  later)I  can  speak  fluent  _____ I \space\space am\space\space Chinese \cdots (1000\space\space words\space\space later)\cdots I \space\space can \space\space speak \space\space fluent \space\space \_\_\_\_\_
我們希望機器正確地填出最後一個詞:當然希望是 ChineseChinese,然而假設中間這1000個詞都和 ChineseChinese 沒什麼大關係,那麼機器就需要記住句子一開始的 ChineseChinese。這無疑會給 RNN 反向傳播帶來極大的困難,可能會造成梯度消失。那麼如何解決這個問題呢?—— 因此 LSTMLSTM 和它的變體 GRUGRU 應運而生。

在之後的 BlogBlog 裏面,我們會詳細地學習 LSTMLSTM 的工作機理,以及如何在 PytorchPytorch 裏面實現 LSTM

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章