這節學習PyTorch的循環神經網絡層nn.RNN
,以及循環神經網絡單元nn.RNNCell
的一些細節。
1 nn.RNN涉及的Tensor
PyTorch中的nn.RNN
的數據處理如下圖所示。每次向網絡中輸入batch個樣本,每個時刻處理的是該時刻的batch個樣本,因此是shape爲的Tensor。例如,輸入3句話,每句話10個單詞,每個單詞用100維的向量表示,那麼,,。
需要特別注意的是,隱藏記憶單元的shape是二維的,其中是一個可以自定的超參數,如可以取爲20,表示每個樣本用20長度的向量記錄。
圖中左側看似隱藏單元是隻有的Tensor,實際上還是要board cast成的shape再做運算。
圖中是對當前時刻處理的Tensor的線性變換,對應圖上第一行公式,可以看到這個變換矩陣的shape是。
而是對當前時刻取得的隱藏記憶單元的線性變換,對應圖上第二行公式,可以看到這個變換方陣的階是。
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())
這裏可以查看到(也就是前面學的)和以及兩個偏置bias共四個共享參數,參數名後面的是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_len
、hidden_len
、num_layers
,默認空間層數=1。
3 nn.RNN的forward方法
這裏是nn.RNN
的前向計算過程,具體調用.forward(x,h_0)
時,輸入的第一參數x
對應着最前面那張圖裏右下角的,即它是一次性將所有時刻特徵喂入的,而不需要每次喂入當前時刻的,所以其shape是。
輸入的第二參數h_0
是第一個時刻空間上所有層的記憶單元的Tensor,和第一張圖上描述的一樣,只是還要考慮循環網絡空間上的層數,所以這裏輸入的shape是。
返回值有兩部分,其中h_t
是最後一個時刻空間上所有層的記憶單元,所以它和h_0
的shape是一樣的,即。
返回的out
是每一個時刻上空間上最後一層的輸出,所以它的shape是。
用一個程序驗證一下:
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應是:
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
是不一樣的,具體是:
這裏輸入的第一參數x_t
是最上面的圖中右下角當前時刻的輸入,所以其shape是。
輸入的第二參數h_{t-1}
是這個時刻運行之前記憶單元的Tensor,也就是前一時刻的單元輸出,所以這裏輸入的shape是。
返回值h_t
是這個時刻運行之後記憶單元的Tensor,也就是下一時刻(如果有)的單元輸入,所以它和h_{t-1}
的shape是一樣的,即。
顯然,使用nn.RNNCell
沒法像nn.RNN
那樣直接求得網絡的輸出out,如果需要,可以將最後一層每個時刻該單元的輸出組合起來:
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])