循环神经网络(RNN)堪称当前最强大的深度学习模型之一,它催生了诸多有趣的应用:文本分类、序列数据标注、文本序列生成(例如SwiftKey键盘根据上文预测下一个词),甚至能实现序列到序列的转换——比如将法语自动翻译成英语。大多数传统模型架构,如前馈神经网络,天生不适合处理序列数据。它们要求将所有特征压缩成一个固定长度的向量,比如把一句话、一个段落甚至整篇文档的所有token强行打包。前馈网络的设计逻辑是“一次性看完所有特征,然后直接映射到输出”。但这种方法真的可靠吗?来看两个英文句子:I had cleaned my car 和 I had my car cleaned。单词完全相同,意思却截然相反——只有当你考虑单词的先后顺序时,才能捕捉到这种细微差别。
人类理解文本时,习惯从左到右逐词阅读,逐步构建完整语义。RNN的思路与此相似:每次只处理一个词。本质上,RNN也是一种神经网络,但内部含有一层特殊结构,并非一次性处理全部数据,而是通过“循环”机制逐步运算。正因如此,RNN能按顺序处理数据,其输入和输出的长度均可灵活变化——想多长就多长。图6.3展示了RNN的几种典型结构形态。

图6.3
图6.3源自Andrej Karpathy关于RNN的知名博文,其中详细讲解了如何用Python从零实现一个RNN,并将其用作序列生成器。
6.4.1 通过示例了解RNN如何使用
假设我们已经训练好了一个RNN模型,先别急着深究内部原理,而是看看它到底能做什么。等摸清它的能力边界,再探讨内部工作机制也不迟。
以这句话为例:the action scenes were top notch in this movie...。将第一个词“the”输入模型,它会输出两个结果:状态向量(隐藏状态)和输出向量。状态向量会随下一个词“action”一起再次送入模型,模型又生成新的状态向量。以此类推,直到最后一个词处理完毕,我们只需关注模型最终输出的结果。图6.4概括了这一过程。

图6.4
图6.4演示了两个关键要点:
- RNN如何通过“展开”方式工作;
- 状态(隐藏向量)如何递归地传递给同一个模型。
到目前为止,我们只了解了RNN能做什么,但内部实现细节还不清楚。别急,先看一段代码,这会让理解更加具体。暂时把RNN当作黑盒:
hidden = None
for word in sentence:
output, hidden = rnn(word, hidden)
这里的 hidden 就是状态向量,也常被称为隐藏状态。现在你应该知道如何使用RNN了。接下来我们看看实现RNN的代码,揭开内部的神秘面纱:
class RNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(RNN, self).__init__()
self.hidden_size = hidden_size
self.i2h = nn.Linear(input_size + hidden_size, hidden_size)
self.i2o = nn.Linear(input_size + hidden_size, output_size)
self.softmax = nn.LogSoftmax(dim=1)
def forward(self, input, hidden):
combined = torch.cat((input, hidden), 1)
hidden = self.i2h(combined)
output = self.i2o(combined)
output = self.softmax(output)
return output, hidden
def initHidden(self):
return torch.zeros(1, self.hidden_size)
除了类名是“RNN”之外,其他部分与前面章节用到的层没什么本质区别——因为PyTorch已经帮我们把反向传播的复杂细节封装好了。下面我们逐一解析 __init__ 和 forward 函数,看看内部到底发生了什么。
__init__ 初始化了两个线性层:一个用于计算输出,另一个用于计算隐藏状态(即状态向量)。
forward 函数将输入的向量与隐藏向量拼接起来,然后同时送入这两个线性层,分别得到输出和新的隐藏状态。对于输出层,还应用了 log_softmax 函数。
initHidden 函数负责生成初始隐藏向量——这样第一次调用RNN时,我们就能从一个干净的零状态开始。图6.5直观展示了RNN内部的计算流程。

图6.5
图6.5清晰地呈现了RNN的计算过程:输入和隐藏状态合并后,经过两个线性变换,分别得到输出和新的隐藏状态,而这个新的隐藏状态又会参与下一时间步的计算。
