当前位置:   article > 正文

【PyTorch学习笔记】21:nn.RNN和nn.RNNCell的使用

nn.rnn

这节学习PyTorch的循环神经网络层nn.RNN,以及循环神经网络单元nn.RNNCell的一些细节。

1 nn.RNN涉及的Tensor

PyTorch中的nn.RNN的数据处理如下图所示。每次向网络中输入batch个样本,每个时刻处理的是该时刻的batch个样本,因此 x t x_t xtshape [ b a t c h , f e a t u r e _ l e n ] [batch, feature\_len] [batch,feature_len]的Tensor。例如,输入3句话,每句话10个单词,每个单词用100维的向量表示,那么 s e q _ l e n = 10 seq\_len=10 seq_len=10 b a t c h = 3 batch=3 batch=3 f e a t u r e _ l e n = 100 feature\_len=100 feature_len=100
在这里插入图片描述
需要特别注意的是,隐藏记忆单元 h h h的shape是二维的 [ b a t c h , h i d d e n _ l e n ] [batch,hidden\_len] [batch,hidden_len],其中 h i d d e n _ l e n hidden\_len hidden_len是一个可以自定的超参数,如可以取为20,表示每个样本用20长度的向量记录。

图中左侧看似隐藏单元 h h h是只有 h i d d e n _ l e n hidden\_len hidden_len的Tensor,实际上还是要board cast成 [ b a t c h , h i d d e n _ l e n ] [batch,hidden\_len] [batch,hidden_len]的shape再做运算。

图中 x t @ w x h x_t@w_{xh} xt@wxh是对当前时刻处理的Tensor x t x_t xt的线性变换,对应图上第一行公式,可以看到这个变换矩阵的shape是 [ h i d d e n _ l e n , f e a t u r e _ l e n ] [hidden\_len,feature\_len] [hidden_len,feature_len]

h t @ w h h h_t@w_{hh} ht@whh是对当前时刻取得的隐藏记忆单元的线性变换,对应图上第二行公式,可以看到这个变换方阵的阶是 h i d d e n _ l e n hidden\_len hidden_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())
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这里可以查看到 w i h w_{ih} wih(也就是前面学的 w x h w_{xh} wxh)和 w h h w_{hh} whh以及两个偏置bias共四个共享参数,参数名后面的 _ l 0 \_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])
  • 1
  • 2
  • 3
  • 4

这里bias只取hidden_len,等到作加法时会广播到所有的batch上。


总结一下,使用nn.RNN构造时传入的三个参数是feature_lenhidden_lennum_layers,默认空间层数=1。

3 nn.RNN的forward方法

o u t ,   h t = f o r w a r d ( x ,   h 0 ) out, \ h_t = forward(x, \ h_0) out, ht=forward(x, h0)

这里是nn.RNN的前向计算过程,具体调用.forward(x,h_0)时,输入的第一参数x对应着最前面那张图里右下角的 x x x,即它是一次性将所有时刻特征喂入的,而不需要每次喂入当前时刻的 x t x_t xt,所以其shape是 [ s e q _ l e n , b a t c h , f e a t u r e _ l e n ] [seq\_len,batch, feature\_len] [seq_len,batch,feature_len]

输入的第二参数h_0第一个时刻空间上所有层的记忆单元的Tensor,和第一张图上描述的一样,只是还要考虑循环网络空间上的层数,所以这里输入的shape是 [ n u m _ l a y e r , b a t c h , h i d d e n _ l e n ] [num\_layer,batch,hidden\_len] [num_layer,batch,hidden_len]

返回值有两部分,其中h_t最后一个时刻空间上所有层的记忆单元,所以它和h_0的shape是一样的,即 [ n u m _ l a y e r , b a t c h , h i d d e n _ l e n ] [num\_layer,batch,hidden\_len] [num_layer,batch,hidden_len]

返回的out每一个时刻上空间上最后一层的输出,所以它的shape是 [ s e q _ l e n , b a t c h , h i d d e n _ l e n ] [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])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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())
  • 1
  • 2
  • 3
  • 4
  • 5

运行结果:

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'])
  • 1
  • 2

需要注意的是,l1层开始接受的输入都是下面层的输出,也就是说接受的输入的特征数不再是feature_len而是hidden_len了,所以这里参数weight_ih_l1的shape应是 [ h i d d e n _ l e n , h i d d e n _ l e n ] [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])
  • 1
  • 2
  • 3
  • 4

总结一下,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])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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是不一样的,具体是:
h t = f o r w a r d ( x t , h t − 1 ) h_t = forward(x_t, h_{t-1}) ht=forward(xt,ht1)

这里输入的第一参数x_t是最上面的图中右下角当前时刻的输入 x t x_t xt,所以其shape是 [ b a t c h , f e a t u r e _ l e n ] [batch, feature\_len] [batch,feature_len]

输入的第二参数h_{t-1}这个时刻运行之前记忆单元的Tensor,也就是前一时刻的单元输出,所以这里输入的shape是 [ b a t c h , h i d d e n _ l e n ] [batch,hidden\_len] [batch,hidden_len]

返回值h_t这个时刻运行之后记忆单元的Tensor,也就是下一时刻(如果有)的单元输入,所以它和h_{t-1}的shape是一样的,即 [ b a t c h , h i d d e n _ l e n ] [batch,hidden\_len] [batch,hidden_len]

显然,使用nn.RNNCell没法像nn.RNN那样直接求得网络的输出out,如果需要,可以将最后一层每个时刻 i i i该单元的输出 h i h_i hi组合起来:

out = torch.stack([h1,h2,...,ht])
  • 1
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])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
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])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/秋刀鱼在做梦/article/detail/897196
推荐阅读
相关标签
  

闽ICP备14008679号