【PyTorch學習筆記】21:nn.RNN和nn.RNNCell的使用

這節學習PyTorch的循環神經網絡層nn.RNN,以及循環神經網絡單元nn.RNNCell的一些細節。

1 nn.RNN涉及的Tensor

PyTorch中的nn.RNN的數據處理如下圖所示。每次向網絡中輸入batch個樣本,每個時刻處理的是該時刻的batch個樣本,因此xtx_t是shape爲[batch,feature_len][batch, feature\_len]的Tensor。例如,輸入3句話,每句話10個單詞,每個單詞用100維的向量表示,那麼seq_len=10seq\_len=10batch=3batch=3feature_len=100feature\_len=100
在這裏插入圖片描述
需要特別注意的是,隱藏記憶單元hh的shape是二維的[batch,hidden_len][batch,hidden\_len],其中hidden_lenhidden\_len是一個可以自定的超參數,如可以取爲20,表示每個樣本用20長度的向量記錄。

圖中左側看似隱藏單元hh是隻有hidden_lenhidden\_len的Tensor,實際上還是要board cast成[batch,hidden_len][batch,hidden\_len]的shape再做運算。

圖中xt@wxhx_t@w_{xh}是對當前時刻處理的Tensorxtx_t的線性變換,對應圖上第一行公式,可以看到這個變換矩陣的shape是[hidden_len,feature_len][hidden\_len,feature\_len]

ht@whhh_t@w_{hh}是對當前時刻取得的隱藏記憶單元的線性變換,對應圖上第二行公式,可以看到這個變換方陣的階是hidden_lenhidden\_len

2 nn.RNN的構造方法

注意nn.RNN構造時傳入的是feature_len和hidden_len,至於有多少個特徵(seq_len)、一次輸入多少樣本(batch)都是可以在運行時候動態決定的。

from torch import nn

# 表示feature_len=100(如每個單詞用100維向量表示), hidden_len=10(隱藏單元的尺寸)
rnn = nn.RNN(100, 10)
# odict_keys(['weight_ih_l0', 'weight_hh_l0', 'bias_ih_l0', 'bias_hh_l0'])
print(rnn._parameters.keys())

這裏可以查看到wihw_{ih}(也就是前面學的wxhw_{xh})和whhw_{hh}以及兩個偏置bias共四個共享參數,參數名後面的_l0\_l0是layer 0的縮寫,表示這些參數是在空間上的第0號層上的(循環網絡在空間上也可以有多層,這裏只有_l0這一層)。


驗證一下這幾個Tensor的shape:

print(rnn.weight_ih_l0.shape)  # torch.Size([10, 100])
print(rnn.weight_hh_l0.shape)  # torch.Size([10, 10])
print(rnn.bias_ih_l0.shape)  # torch.Size([10])
print(rnn.bias_hh_l0.shape)  # torch.Size([10])

這裏bias只取hidden_len,等到作加法時會廣播到所有的batch上。


總結一下,使用nn.RNN構造時傳入的三個參數是feature_lenhidden_lennum_layers,默認空間層數=1。

3 nn.RNN的forward方法

out, ht=forward(x, h0) out, \ h_t = forward(x, \ h_0)

這裏是nn.RNN的前向計算過程,具體調用.forward(x,h_0)時,輸入的第一參數x對應着最前面那張圖裏右下角的xx,即它是一次性將所有時刻特徵喂入的,而不需要每次喂入當前時刻的xtx_t,所以其shape是[seq_len,batch,feature_len][seq\_len,batch, feature\_len]

輸入的第二參數h_0第一個時刻空間上所有層的記憶單元的Tensor,和第一張圖上描述的一樣,只是還要考慮循環網絡空間上的層數,所以這裏輸入的shape是[num_layer,batch,hidden_len][num\_layer,batch,hidden\_len]

返回值有兩部分,其中h_t最後一個時刻空間上所有層的記憶單元,所以它和h_0的shape是一樣的,即[num_layer,batch,hidden_len][num\_layer,batch,hidden\_len]

返回的out每一個時刻上空間上最後一層的輸出,所以它的shape是[seq_len,batch,hidden_len][seq\_len,batch, hidden\_len]
在這裏插入圖片描述
用一個程序驗證一下:

import torch
from torch import nn

# 表示feature_len=100, hidden_len=20, 層數=1
rnn = nn.RNN(100, 20, 1)
# 輸入3個樣本序列(batch=3), 序列長爲10(seq_len=10), 每個特徵100維度(feature_len=100)
x = torch.randn(10, 3, 100)
# 傳入RNN處理, 另外傳入h_0, shape是<層數, batch, hidden_len=20>
out, h = rnn(x, torch.zeros(1, 3, 20))
# 輸出返回的out和最終的隱藏記憶單元的shape
print(out.shape)  # torch.Size([10, 3, 20])
print(h.shape)  # torch.Size([1, 3, 20])

4 使用nn.RNN構建多層循環網絡

4.1 構造方法和網絡參數

和前面講的一樣,多層的只要構造時設置第三個參數大於1,對於每一層都有在時間線上的共享參數:

from torch import nn

# 表示feature_len=100, hidden_len=20, 層數=2
rnn = nn.RNN(100, 20, num_layers=2)
print(rnn._parameters.keys())

運行結果:

odict_keys(['weight_ih_l0', 'weight_hh_l0', 'bias_ih_l0', 'bias_hh_l0',
'weight_ih_l1', 'weight_hh_l1', 'bias_ih_l1', 'bias_hh_l1'])

需要注意的是,l1層開始接受的輸入都是下面層的輸出,也就是說接受的輸入的特徵數不再是feature_len而是hidden_len了,所以這裏參數weight_ih_l1的shape應是[hidden_len,hidden_len][hidden\_len, hidden\_len]

print(rnn.weight_ih_l0.shape)  # torch.Size([20, 100])
print(rnn.weight_hh_l0.shape)  # torch.Size([20, 20])
print(rnn.weight_ih_l1.shape)  # torch.Size([20, 20]) 注意這裏
print(rnn.weight_hh_l1.shape)  # torch.Size([20, 20])

總結一下,nn.RNN的最底下一層l0將外部的feature_len轉化爲隱藏記憶單元的內部表示即hidden_len,而其它層都是輸入hidden_len輸出hidden_len的。

4.2 forward方法

符合前面講的結果,即是涉及layer_num的地方不再是1了:

import torch
from torch import nn

# 表示feature_len=100, hidden_len=20, 層數=4
rnn = nn.RNN(100, 20, num_layers=4)
# 輸入3個樣本序列(batch=3), 序列長爲10(seq_len=10), 每個特徵100維度(feature_len=100)
x = torch.randn(10, 3, 100)
# 傳入RNN處理, 另外傳入h_0, shape是<層數, batch, hidden_len=20>
out, h = rnn(x, torch.zeros(4, 3, 20))
# 輸出返回的out和最終的隱藏記憶單元的shape
print(out.shape)  # torch.Size([10, 3, 20])
print(h.shape)  # torch.Size([4, 3, 20])

5 nn.RNNCell

5.1 簡述

相比一步到位的nn.RNN,也可以使用nn.RNNCell,它將序列上的每個時刻分開來處理。

也就是說,如果要處理的是3個句子,每個句子10個單詞,每個單詞用長100的向量,那麼送入nn.RNN的Tensor的shape就是[10,3,100]。

但如果使用nn.RNNCell,則將每個時刻分開處理,送入的Tensor的shape是[3,100],但要將此計算單元運行10次。顯然這種方式比較麻煩,但使用起來也更靈活。

5.2 構造方法

構造方法和nn.RNN類似,依次傳入feature_len和hidden_len,因爲這只是一個計算單元,所以不涉及層數

5.3 forward方法

前向計算的輸入輸出和nn.RNN是不一樣的,具體是:
ht=forward(xt,ht1) h_t = forward(x_t, h_{t-1})

這裏輸入的第一參數x_t是最上面的圖中右下角當前時刻的輸入xtx_t,所以其shape是[batch,feature_len][batch, feature\_len]

輸入的第二參數h_{t-1}這個時刻運行之前記憶單元的Tensor,也就是前一時刻的單元輸出,所以這裏輸入的shape是[batch,hidden_len][batch,hidden\_len]

返回值h_t這個時刻運行之後記憶單元的Tensor,也就是下一時刻(如果有)的單元輸入,所以它和h_{t-1}的shape是一樣的,即[batch,hidden_len][batch,hidden\_len]

顯然,使用nn.RNNCell沒法像nn.RNN那樣直接求得網絡的輸出out,如果需要,可以將最後一層每個時刻ii該單元的輸出hih_i組合起來:

out = torch.stack([h1,h2,...,ht])

5.4 一層的例子

import torch
from torch import nn

# 表示feature_len=100, hidden_len=20
cell = nn.RNNCell(100, 20)
# 某一時刻的輸入, 共3個樣本序列(batch=3), 每個特徵100維度(feature_len=100)
x = torch.randn(3, 100)
# 所有時刻的輸入, 一共有10個時刻, 即seq_len=10
xs = [torch.randn(3, 100) for i in range(10)]
# 初始化隱藏記憶單元, batch=3, hidden_len=20
h = torch.zeros(3, 20)
# 對每個時刻的輸入, 傳入這個nn.RNNCell計算單元, 還要傳入上一時h, 以進行前向計算
for xt in xs:
    h = cell(xt, h)
# 查看一下最終輸出的h, 其shape還是<batch, hidden_len>
print(h.shape)  # torch.Size([3, 20])

5.5 兩層的例子

同一層共用一個nn.RNNCell(因爲要共享裏面的參數),多層的時候就要建立多個nn.RNNCell。下面這個例子,取batch=3,feature_len=100,則l0層輸入的維度是[3,100],經過這一層的計算單元變換,得到輸出的維度是[3,30],向右傳給這一層下一個單元,並向上傳給l1層的同時刻單元進行計算。而l1層的計算單元接收[3,30]的輸入,輸出維度是[3,20]。如果取seq_len=4,這個計算過程中維度的變化如下圖:
在這裏插入圖片描述
每一層在當前時刻的值都要進行計算,所以for循環裏要把l0層和l1層在當前時刻的值依次計算完。

import torch
from torch import nn

# 第0層和第1層的計算單元
cell_l0 = nn.RNNCell(100, 30)  # feature_len=100, hidden_len_l0=30
cell_l1 = nn.RNNCell(30, 20)  # hidden_len_l0=30, hidden_len_l1=20

# 第0層和第1層使用的隱藏記憶單元(圖中黃色和綠色)
h_l0 = torch.zeros(3, 30)  # batch=3, hidden_len_l0=30
h_l1 = torch.zeros(3, 20)  # batch=3, hidden_len_l1=20

# 原始輸入, batch=3, feature_len=100
xs = [torch.randn(3, 100) for i in range(4)]  # seq_len=4, 即共4個時刻

for xt in xs:
    h_l0 = cell_l0(xt, h_l0)
    h_l1 = cell_l1(h_l0, h_l1)

# 圖中最右側兩個輸出
print(h_l0.shape)  # torch.Size([3, 30])
print(h_l1.shape)  # torch.Size([3, 20])
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章