参考
循环神经网络
一文搞懂RNN(循环神经网络)基础篇
零基础入门深度学习(5) - 循环神经网络
史上最详细循环神经网络讲解(RNN/LSTM/GRU)
RNN 中为什么要采用 tanh,而不是 ReLU 作为激活函数?

循环神经网络(recurrent neural network,RNN)

并不是所有的数据都是独立分布的,很多时候,先前的数据是可以推导猜测后续的发展(如天气预报),循环神经网络正是用来处理这类序列信息的。

先说明一点,循环神经网络并不真的如其名字一样在循环,这张图的”循环”也是如此。实际上,s出发循环返回的对象永远是s+1,拆开来看就如下图。

如果把上面有W的那个带箭头的圈去掉,也就是不传递s,它就变成了最普通的全连接神经网络。

有隐状态的循环神经网络

代码中的 H 被初始化为一个随机值,表示初始时刻的隐藏状态。这一点是非常重要的,因为在序列数据的处理过程中,我们通常需要为网络提供一个初始的隐藏状态,这个初始隐藏状态的选择可以影响模型的学习过程。

H 的初始化表示的是 初始时刻的隐藏状态,它在第一次前向传播时被赋值为随机值,之后的每个时间步中,隐藏状态会根据前一个时间步的隐藏状态和当前的输入来更新。

1
2
3
X, W_xh = torch.normal(0, 1, (3, 1)), torch.normal(0, 1, (1, 4))
H, W_hh = torch.normal(0, 1, (3, 4)), torch.normal(0, 1, (4, 4))
torch.matmul(X, W_xh) + torch.matmul(H, W_hh)

拼接的目的:合并计算,减少操作次数.只是计算上的重排和合并,模型逻辑没变,还是用两个不同的权重矩阵分别对不同输入部分作用,只是计算时合成一步。

1
torch.matmul(torch.cat((X, H), 1), torch.cat((W_xh, W_hh), 0))

循环神经网络的从零开始实现

独热编码

PyTorch 的 F.one_hot 函数将数字序列 [0, 2] 转换为 独热编码(one-hot encoding),是高维稀疏向量。

word embedding可以替代one hot

1
F.one_hot(torch.tensor([0, 2]), len(vocab))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# inputs的形状:(时间步数量,批量大小,词表大小)
def rnn(inputs, state, params):

# 拆包出五个参数,params 是一个元组或列表,里面按顺序包含这些权重和偏置
W_xh, W_hh, b_h, W_hq, b_q = params
# state 是只有一个元素的元组,把元组中第一个元素赋值给变量 H。且RNN 变体可能会返回多个状态这种写法安全而通用
H, = state
# 初始化一个空列表,用于收集每个时间步的输出
outputs = []
# X的形状:(批量大小,词表大小)
for X in inputs:
H = torch.tanh(torch.mm(X, W_xh) + torch.mm(H, W_hh) + b_h) # 引入非线性,摆脱线性约束
Y = torch.mm(H, W_hq) + b_q
outputs.append(Y)
return torch.cat(outputs, dim=0), (H,)

循环神经网络的简洁实现

256个隐藏单元,一维张量,长度256。

1
2
num_hiddens = 256
rnn_layer = nn.RNN(len(vocab), num_hiddens)
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
class RNNModel(nn.Module):
"""循环神经网络模型"""
# 定义继承自 nn.Module 的模型类,**kwargs 用于兼容父类 __init__ 的参数
def __init__(self, rnn_layer, vocab_size, **kwargs):
super(RNNModel, self).__init__(**kwargs)
self.rnn = rnn_layer
self.vocab_size = vocab_size
self.num_hiddens = self.rnn.hidden_size
# 如果RNN是双向的(之后将介绍),num_directions应该是2,否则应该是1
if not self.rnn.bidirectional:
self.num_directions = 1
self.linear = nn.Linear(self.num_hiddens, self.vocab_size)
else:
self.num_directions = 2
self.linear = nn.Linear(self.num_hiddens * 2, self.vocab_size)

def forward(self, inputs, state):
X = F.one_hot(inputs.T.long(), self.vocab_size)
X = X.to(torch.float32) # 转换为浮点数张量
Y, state = self.rnn(X, state)
# 全连接层首先将Y的形状改为(时间步数*批量大小,隐藏单元数)
# 它的输出形状是(时间步数*批量大小,词表大小)。
output = self.linear(Y.reshape((-1, Y.shape[-1])))
return output, state

def begin_state(self, device, batch_size=1):
if not isinstance(self.rnn, nn.LSTM):
# nn.GRU以张量作为隐状态
return torch.zeros((self.num_directions * self.rnn.num_layers,
batch_size, self.num_hiddens),
device=device)
else:
# nn.LSTM以元组作为隐状态
return (torch.zeros((
self.num_directions * self.rnn.num_layers,
batch_size, self.num_hiddens), device=device),
torch.zeros((
self.num_directions * self.rnn.num_layers,
batch_size, self.num_hiddens), device=device))

RNN

RNN 本质上就是一个非线性的、自适应的、神经网络形式的自回归模型。
只能从前往后建模:

  • 状态更新:

  • 输出计算:

权重在所有时间步(t=1 到 t=T)之间是相同的,同一组权重参数会被用于所有时间步的计算

双向 RNN

同时考虑过去和未来,本质仍是RNN。

向量化

神经网络的输入和输出都是向量,为了让语言模型能够被神经网络处理,我们必须把词表达为向量的形式,这样神经网络才能处理它。

深度循环神经网络

前面循环神经网络只有一个隐藏层,我们当然也可以堆叠两个以上的隐藏层,这样就得到了深度循环神经网络。

循环神经网络的训练算法:BPTT

BPTT(BackPropogation Through Time)与BP类似,是在时间上反传的梯度下降算法。与BP一样包含三个步骤:

前向计算每个神经元的输出值;
反向计算每个神经元的误差项值,它是误差函数E对神经元j的加权输入的偏导数;
计算每个权重的梯度。
最后再用随机梯度下降算法更新权重。

seq2seq (Sequence to Sequence)

Seq2Seq模型

Seq2Seq,即序列到序列模型,是一种能够根据给定的序列,通过特定的生成方法生成另一个序列的方法,同时这两个序列可以不等长。这种结构又叫Encoder-Decoder模型,即编码-解码模型。

特定的eos表示序列结束词元。 一旦输出序列生成此词元,模型就会停止预测。特定的bos表示序列开始词元,它是解码器的输入序列的第一个词元。

在编码过程中,输入序列通过Encoder将输入的文本序列压缩成指定长度的向量,得到语义向量C,语义向量C作为Decoder的初始状态 h0,参与解码过程,生成输出序列。

decoder处理方式还有另外一种,就是语义向量C参与了序列所有时刻的运算。

在训练时,有时会使用 Teacher Forcing,即每一步都把真实的标签词作为下一时刻输入,而不是用模型自己预测的词。

在推理时,如果某一步预测错了,比如应该是 good 但模型生成了 bad,这个 bad 就会被当作下一步输入,导致后续上下文全跑偏,这就是所谓的蝴蝶效应。

Scheduled Sampling训练时不总是喂真标签,偶尔喂自己预测的结果。在早期更多使用真实标签,稳定收敛,随着训练进行更多使用自己预测的结果,逼近真实推理场景,在训练过程中逐渐适应输入中可能出现的错误,减少推理时的崩溃风险。