RNN, LSTM , GRU 结构解释和其在Pytorch中的使用

1. RNN 结构和内部计算

RNN结构示意图(这里没画bias, 但公式里写了,借的李宏毅教授PPT图)

image-20210406154230330

$x$为当前状态下数据的输入, $h$表示接收到的上一个节点的输入。

$y$为当前节点状态下的输出,而 $h^{\prime} $为传递到下一个节点的输出。

真实计算公式为:

通过上图的公式可以看到,输出$\boldsymbol{h}^{\prime}$与 $\boldsymbol{x}$ 和 $\boldsymbol{h}$的值都相关。

而 $\boldsymbol{y}$ 则常常使用 $\boldsymbol{h}^{\prime}$投入到一个线性层(主要是进行维度映射)然后使用softmax进行分类得到需要的数据。

对这里的$\boldsymbol{y}$如何通过 $\boldsymbol{h}^{\prime}$ 计算得到往往看具体模型的使用方式。

简单使用rnn实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#输入尺寸, 隐藏层神经元个数, 层数
rnn = nn.RNN(input_size=20,hidden_size=50,num_layers=2)
input_data = Variable(torch.randn(100,32,20)) #[seq_len, batch_size, word_dim]
#如果传入网络时,不特别注明隐状态,那么输出的隐状态默认参数全是0
h_0 = Variable(torch.randn(2,32,50)) #[lnum_layers*direction, batch_size, hidden_size]
output,h_t = rnn(input_data,h_0)

print(output.size()) #seq,batch,hidden_size
print(h_t.size()) #layer*direction,batch,hidden_size
print(rnn.weight_ih_l0.size())
++++++++++++++++++++++++++++++++++++++++++++++
torch.Size([100, 32, 50])
torch.Size([2, 32, 50])
torch.Size([50, 20])

通过序列形式的输入,我们能够得到如下形式的RNN。

image-20210406170914456

序列模型中RNN的搭建:

使用 nn.RNN(input_size, hidden_size, num_layers)中参数解释

  • input_size – The number of expected features in the input x
  • hidden_size – The number of features in the hidden state h
  • num_layers – Number of recurrent layers.
  • bidirectionalIf True, becomes a bidirectional RNN. Default: False

输出是$y_t, h_t$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class RNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size, num_layers=1):
super(RNN, self).__init__()
"""
分别代表输入最后一维尺寸
隐藏层最后一维尺寸
输出层最后一维尺寸
层数
"""
self.input_size = input_size
self.hidden_size = hidden_size
self.output_size = output_size
self.num_layers = num_layers

#实例化RNN,参数分别是输入,隐藏层神经元数目,层数
self.rnn = nn.RNN(input_size, hidden_size, num_layers)

#实例化线性层,将RNN输出转化为指定维度
self.linear = nn.Linear(hidden_size, output_size)
#实例化softmax将输出变为类别概率
self.softmax = nn.LogSoftmax(dim=-1)

def forward(self, input1, hidden):
"""
:param input: 输入, 1xn_letters
:param hidden: 隐藏层张量, self.layers x 1 x self.hidden_size
:return:
"""
#RNN输入预定义为3为将其拓展一个维度
input1 = input1.unsqueeze(0)
#将input, hidden输入rnn,如果layer=1,那么rr恒等于hn
rr, hn = self.rnn(input1, hidden)
#将rnn输出通过线性变换和softmax,同时返回hn作为后续RNN输入
return self.softmax(self.linear(rr)), hn

def initHidden(self):
#初始化一个self.num_layers, 1, self.hidden_size形状的0张量
return torch.zeros(self.num_layers, 1, self.hidden_size)

2. LSTM

LSTM是long short-term memory,与RNN区别是

  • LSTM两个传递状态:$c^t $ cell state, $h^t$ hidden state
  • RNN一个传递状态: $h^t$

注: RNN中$h^t$等价于 LSTM中 $c^t$

image-20210406172803901

而对于input gate, forget gate, output gate中是$[x^t, h^{t-1}]$向量拼接后乘以相应权重矩阵再过sigmoid函数转换为0到1的数值,作为门控状态。

需要注意的是z是tanh函数,转换为-1到1,因为是输入数据不是门控状态。

因此有下面4个公式:(忽略bias)

注:[]表示两个向量拼接,而 $\boldsymbol{W}$四个权重矩阵都是训练学习得到的。

image-20210406173622441

然后根据上面四个信号$ \boldsymbol{z}, \boldsymbol{z}^f, \boldsymbol{z}^i, \boldsymbol{z}^o $, 由下列公式得到输出, cell state和隐藏状态。

  • cell state :等于 上一个cell state 与遗忘门信号 $\boldsymbol{z}^f $ Hadamard product,和 输入门信号 $ \boldsymbol{z}^i $与输入信号$\boldsymbol{z}$ 的Hadamard product之和
  • hidden state: 等于输出门信号 $\boldsymbol{z}^o $ 与 过tanh后的 cell state 的Hadamard product
  • 输出: 等于上一个时间步后权重矩阵$\boldsymbol{W}$ 与 隐藏状态过sigmoid

image-20210406213738525

Pytorch中LSTM实例使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
lstm = nn.LSTM(input_size=20,hidden_size=50,num_layers=2)#[input_size hidden_size num_layers]
#输入变量
input_data = Variable(torch.randn(100,32,20)) #[seq_len, batch_size, word_dim]
#初始隐状态
h_0 = Variable(torch.randn(2,32,50)) #[num_layers*direction, batch_size, hidden_size]
#输出记忆细胞
c_0 = Variable(torch.randn(2,32,50)) #[num_layers*direction, batch_size, hidden_size]
#输出变量
output,(h_t,c_t) = lstm(input_data,(h_0,c_0))
print(output.size()) #[seq_len, batch_size, hidden_size]
print(h_t.size())
print(c_t.size())
#参数大小为(50x4,20),是RNN的四倍
print(lstm.weight_ih_l0)
print(lstm.weight_ih_l0.size())

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
torch.Size([100, 32, 50])
torch.Size([2, 32, 50])
torch.Size([2, 32, 50])
Parameter containing:
tensor([[ 0.1123, -0.0734, -0.0408, ..., -0.0715, 0.0920, 0.0409],
[-0.1310, -0.0566, -0.0761, ..., -0.0909, 0.0081, -0.0258],
[ 0.0264, 0.0297, 0.0800, ..., 0.0869, -0.0008, -0.0621],
...,
[-0.0116, 0.0778, -0.1181, ..., 0.1066, 0.0610, 0.0846],
[ 0.0216, -0.0159, 0.1354, ..., 0.0726, -0.0238, -0.1158],
[-0.1081, -0.0760, -0.0722, ..., 0.0506, 0.0550, 0.1033]],
requires_grad=True)
torch.Size([200, 20])

在seq2seq任务中,使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class LSTM(nn.Module):
def __init__(self, input_size, hidden_size, output_size, num_layers=1):
super(LSTM, self).__init__()
"""
分别代表输入最后一维尺寸
隐藏层最后一维尺寸
输出层最后一维尺寸
层数
"""
self.input_size = input_size
self.hidden_size = hidden_size
self.output_size = output_size
self.num_layers = num_layers

#实例化RNN,参数分别是输入,隐藏层神经元数目,层数
self.lstm = nn.LSTM(input_size, hidden_size, num_layers)

#实例化线性层,将RNN输出转化为指定维度
self.linear = nn.Linear(hidden_size, output_size)
#实例化softmax将输出变为类别概率
self.softmax = nn.LogSoftmax(dim=-1)

def forward(self, input1, hidden, c):
"""
LSTM输入有3个
:param input1: 输入, 1xn_letters
:param hidden: 隐藏层张量, self.layers x 1 x self.hidden_size
:param c 张量, self.layers x 1 x self.hidden_size
:return:
"""
#RNN输入预定义为3为将其拓展一个维度
input1 = input1.unsqueeze(0)
#将input, hidden输入rnn,如果layer=1,那么rr恒等于hn
rr, (hn, cn) = self.lstm(input1, (hidden, c))
#将rnn输出通过线性变换和softmax,同时返回hn作为后续RNN输入
return self.softmax(self.linear(rr)), hn, cn

def initHidden(self):
#初始化一个self.num_layers, 1, self.hidden_size形状的0张量
c = hidden = torch.zeros(self.num_layers, 1, self.hidden_size)
return hidden, c

3. GRU

GRU(Gate Recurrent Unit)结构示意图如下:

image-20210407123554544

最重要的是,两个门控reset gate 和 update gate:

v2-7fff5d817530dada1b279c7279d73b8a_r

然后得到重置的隐藏层状态${\boldsymbol{h}^{t-1}}^\prime= \boldsymbol{h}^{t-1} \odot\boldsymbol{r}^{t} $. 将这个隐藏状态与$\boldsymbol{x}^t$ 拼接再过tanh将重置的隐藏状态缩放为-1~1.得到${\boldsymbol{h}^{t}}^\prime$

v2-390781506bbebbef799f1a12acd7865b_r

这里${\boldsymbol{h}^{t}}^\prime$ 主要包含了当前输入$ {\boldsymbol{x}^{t}} $数据, 还有有选择地添加上一个时间隐藏状态,这里主要是重置门信号作用(你可以理解为乘以一个0~1的数来控制添加隐藏状态的多少)。

image-20210407151241968

最后一步是$\boldsymbol{h}^{t}$,也是输出。计算公式为:

  • ${\boldsymbol{h}^{t}}^\prime$ 相当于LSTM中的hidden state : $ \boldsymbol{h}^t = \boldsymbol{z}^o \odot \text{tanh }\boldsymbol{c}^{t} $

  • 而$\boldsymbol{h}^{t} $ 相当于cell state

实际上,GRU是用一个update gate来替换LSTM的input gate 和 forget gate.

在 LSTM 中这两个门分别是

  • 控制输入多少(输入是上一个时间状态和当前输入总的信息量)
  • 控制遗忘信息量的多少

而update gate就是上面的$\boldsymbol{z}^t$ ,是一个0~1的控制信号。那么:

  • $\boldsymbol{z}^t $理解等价于input gate 控制信号, $\boldsymbol{z}^t \odot {\boldsymbol{h}^{t}}^\prime$ 就等效于经过input gate后的信息
  • $(1-\boldsymbol{z}^t )$ 等价于 forget gate 控制信号, $(1-\boldsymbol{z}^t ) \odot \boldsymbol{h}^{t-1}$ 等效于 经过 forget gate后的信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
gru = nn.GRU(input_size=20,hidden_size=50,num_layers=2)
#输入变量
input_data = Variable(torch.randn(100,32,20)) #[seq_len, batch_size, word_dim]
#初始隐状态
h_0 = Variable(torch.randn(2,32,50)) #[num_layers*direction, batch_size, hidden_size]
#输出变量
output,(h_n,c_n) = gru(input_data)
print(output.size()) #[seq_len, batch_size, word_dim]
print(h_n.size()) #[batch_size, word_dim]
print(gru.weight_ih_l0)
print(gru.weight_ih_l0.size()) #[hidden_size+seq_len, word_dim]

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
torch.Size([100, 32, 50])
torch.Size([32, 50])
Parameter containing:
tensor([[ 0.0465, 0.0917, 0.1357, ..., -0.1283, 0.1264, -0.0320],
[ 0.1384, 0.1072, 0.0073, ..., -0.0772, -0.1295, 0.0233],
[-0.0627, -0.1012, -0.0415, ..., 0.1311, 0.1374, -0.0566],
...,
[-0.0131, 0.0995, 0.0054, ..., -0.0350, -0.1220, 0.1143],
[-0.0836, 0.1239, 0.0860, ..., 0.0837, 0.0172, -0.0170],
[ 0.0637, 0.0501, 0.0091, ..., -0.1201, 0.0788, 0.0468]],
requires_grad=True)
torch.Size([150, 20])

seq2seq中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class GRU(nn.Module):
def __init__(self, input_size, hidden_size, output_size, num_layers=1):
super(GRU, self).__init__()
self.input_size = input_size
self.hidden_size = hidden_size
self.num_layers = num_layers
self.output_size = output_size

# Instantiate the predefined nn.GRU, its three parameters are input_size, hidden_size, num_layers
self.gru = nn.GRU(input_size, hidden_size, num_layers)
self.linear = nn.Linear(hidden_size, output_size)
self.softmax = nn.LogSoftmax(dim=-1)

def forward(self, input, hidden):
input = input.unsqueeze(0)
rr, hn = self.gru(input, hidden)
return self.softmax(self.linear(rr)), hn

def initHidden(self):
return torch.zeros(self.num_layers, 1, self.hidden_size)

Inference

[1] Seq-to-seq Learning.pdf )

[2] 人人都能看懂的LSTM

[3] NLP: text classification 图示漂亮