☰
AI-For-Beginners 递归神经网络(RNN)实战:从 RNN 细胞解剖到 LSTM、双向与多层网络的序列建模指南
2026/10/8 18:56:06 网站建设 项目流程
  • 教程
  • 人工智能
  • 机器学习
  • 深度学习

【免费下载链接】AI-For-Beginners

12 Weeks, 24 Lessons, AI for All!

项目地址:https://gitcode.com/GitHub_Trending/ai/AI-For-Beginners
点击查看免费下载

递归神经网络(Recurrent Neural Network,RNN)是自然语言处理中处理序列数据的核心架构。在 AI-For-Beginners 课程的 NLP 模块中,第 16 课(lessons/5-NLP/16-RNN)系统讲解了 RNN 的动机、细胞结构、LSTM 门控机制,并提供了 PyTorch 与 TensorFlow 两套可运行的新闻分类实战 Notebook。本文以该课文档为主体,结合仓库内的 RNNPyTorch.ipynb、RNNTF.ipynb 与 torchnlp.py 源码,完整覆盖从理论到代码的全部内容。读完本文,你将掌握:RNN 为何能建模词序、RNN 细胞的权重结构与计算公式、LSTM 三门机制如何缓解梯度消失、变长序列的 padding 处理技巧(PyTorch packed sequence 与 Keras masking),以及双向/多层 RNN 的构造方法。

为什么需要 RNN:词嵌入无法建模词序

在课程前面的单元中,我们使用词嵌入(Embedding)的富语义表示,再叠加一个简单的线性分类器来对文本分类。这种架构能捕捉句子中单词的聚合含义,但聚合操作(如取平均)会把词序信息从原始文本中抹掉,因此这些模型无法建模单词的顺序。而像文本生成、问答这类更复杂或带有歧义的任务,恰恰高度依赖词序——例如 "not" 对句子语义的否定作用,只有在序列上下文中才能被理解。

为了捕捉文本序列的含义,我们需要另一种神经网络架构——递归神经网络(RNN)。在 RNN 中,我们把句子一个符号一个符号地送入网络:网络读取当前符号并产生一个状态(state),这个状态又连同下一个符号一起再次送入网络。这样,状态向量 S₀, …, Sₙ 在整个序列中不断传递,网络便能够学习单词之间的顺序依赖关系。例如,当序列中出现 "not" 这个词时,RNN 可以学会对状态向量中的某些分量做"取反"操作,从而实现否定语义。

✅ 提示:由于上图中所有 RNN 块的权重是共享的,同一幅图也可以表示成右侧那种带"循环反馈回路"的单个块——网络的输出状态被重新反馈回输入端。这也是"递归(recurrent)"一词的来源。

RNN 的端到端训练机制

给定输入 token 序列 X₀, …, Xₙ,RNN 会创建一串神经网络块,并用**反向传播(backpropagation)**对这一串网络进行端到端训练:

  • 每个网络块以(Xᵢ, Sᵢ)为输入,产生 Sᵢ₊₁ 作为输出;
  • 最终状态 Sₙ(或输出 Yₙ)送入一个线性分类器得到最终结果;
  • 所有网络块共享同一组权重,整个序列只用一次反向传播过程完成训练。

TensorFlow 版 Notebook 补充了每一步输出 Yᵢ 的情况:对于文本生成、机器翻译等任务,我们希望在每一步都拿到输出,此时细胞内还存在另一组矩阵 Wₒ,输出按Yᵢ = f(Wₒ × Sᵢ + bₒ)计算(见 RNNTF.ipynb)。

RNN 细胞解剖:输入矩阵 W 与状态矩阵 H

一个简单 RNN 细胞接收前一个状态 Sᵢ₋₁ 和当前符号 Xᵢ 作为输入,并产生输出状态 Sᵢ(生成式网络有时还关心额外输出 Yᵢ)。细胞内包含两组内部权重矩阵:

  • W:变换输入符号;
  • H:变换输入状态。

网络的输出按如下公式计算:

Sᵢ = σ(W × Xᵢ + H × Sᵢ₋₁ + b)

其中 σ 是激活函数,b 是额外偏置(bias)。TensorFlow Notebook 中把这两个矩阵记作 W_I 与 W_H,并指出激活函数常用 tanh。

在多数情况下,token 会先经过**嵌入层(embedding layer)**降维后再进入 RNN。此时若输入向量维度为emb_size、状态向量维度为hid_size,则 W 的尺寸为emb_size × hid_size,H 的尺寸为hid_size × hid_size——这一点直接决定了你在代码里构造nn.RNN/nn.LSTM时各层维度的对应关系。

实战一:用 PyTorch 构建简单 RNN 新闻分类器

PyTorch Notebook 用 AG_NEWS 新闻数据集(World / Sports / Business / Sci-Tech 四分类)演示了完整流程。数据加载、词汇表构建、编码、训练循环等通用逻辑都封装在 torchnlp.py 中:

  • load_dataset():加载 AG_NEWS 训练/测试集,用 Counter 统计词频并构建 vocab(torchtext.vocab.vocab(counter, min_freq=min_freq));
  • encode():通过get_stoi()(或旧版stoi属性)把 token 序列映射为整数索引;
  • padify():将 batch 内序列按最大长度补齐为等长张量(F.pad(..., value=0)),返回标签与特征两个张量;
  • offsetify():拼接所有序列并计算 offsets,供EmbeddingBag使用;
  • train_epoch()/train_epoch_emb():标准的 Adam 优化 + CrossEntropyLoss 训练循环,支持use_pack_sequence参数控制长度张量是否留在 CPU。

在 Notebook 中,RNNClassifier的结构与前面提到的权重维度一一对应:

class RNNClassifier(torch.nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim, num_class): super().__init__() self.hidden_dim = hidden_dim self.embedding = torch.nn.Embedding(vocab_size, embed_dim) self.rnn = torch.nn.RNN(embed_dim, hidden_dim, batch_first=True) self.fc = torch.nn.Linear(hidden_dim, num_class) def forward(self, x): batch_size = x.size(0) x = self.embedding(x) x, h = self.rnn(x) return self.fc(x.mean(dim=1))

要点解读:

  • torch.nn.RNNCell表示单个细胞,torch.nn.RNN表示由若干细胞组成的完整层;
  • self.rnn(x)返回两个输出:x是每个时间步的细胞输出序列,h是序列最后一个元素的最终隐藏状态;本分类器取x.mean(dim=1)聚合后过全连接层;
  • Notebook 使用未训练的嵌入层,并提示可以换用上一单元讲过的 Word2Vec / GloVe 预训练嵌入以获得更好效果;
  • 训练时使用batch_size=16、lr=0.001。Notebook 强调:RNN 一旦沿序列长度展开,反向传播涉及的层数会非常多,训练困难、速度慢,应使用较小的学习率并在更大数据集上训练,且优先使用 GPU。示例训练日志显示,经过约 10 万个样本后准确率逐步爬升到 0.8 左右。

实战二:用 TensorFlow / Keras 构建 SimpleRNN 分类器

TensorFlow Notebook 同样以 AG_NEWS(ag_news_subset,通过tfds.load获取)为数据集,其模型结构如下(RNNTF.ipynb):

vocab_size = 20000 vectorizer = keras.layers.experimental.preprocessing.TextVectorization( max_tokens=vocab_size, input_shape=(1,)) model = keras.models.Sequential([ vectorizer, keras.layers.Embedding(vocab_size, embed_size), keras.layers.SimpleRNN(16), keras.layers.Dense(4, activation='softmax') ])

要点解读:

  • Keras 中用SimpleRNN层表示简单 RNN,Embedding负责降维(embed_size = 64),Dense(4, softmax)输出四类新闻的概率;
  • model.summary()显示参数分布:Embedding 层约 128 万参数(20000 × 64),SimpleRNN 层 1296 参数(来自 W、H、b),Dense 层 68 参数;
  • 训练时先用vectorizer.adapt(ds_train.take(2000).map(extract_title))在新闻标题上训练向量化器,然后compile(loss='sparse_categorical_crossentropy', metrics=['acc'], optimizer='adam')并fit。由于只用了标题(未用正文),准确率会偏低(示例中约 0.80 的验证准确率);
  • Notebook 提示:TextVectorization会自动为变长序列补齐 pad token,而这些 pad token 也会参与训练、干扰收敛。这引出了下面两个关键优化手段。

LSTM:用门控显式管理状态

经典 RNN 的核心问题是梯度消失(vanishing gradients):RNN 在一次反向传播中端到端展开训练,误差很难传播到网络最早的那些层,导致网络无法学习相距较远的 token 之间的关系。解决办法之一是引入显式状态管理,即所谓的门(gates)。两种最著名的门控架构是LSTM(Long Short Term Memory,长短期记忆)与GRU(Gated Recurrent Unit,门控循环单元),本课重点讲解 LSTM。

LSTM 的组织方式与 RNN 类似,但层与层之间传递两个状态:真正的状态 C 与隐藏向量 H。在每个单元中,隐藏向量 Hᵢ 与输入 Xᵢ 拼接,通过门来控制状态 C 的变化。每个门都是一个带 sigmoid 激活(输出范围 [0,1])的神经网络,与状态向量相乘时可视为一个"位掩码(bitwise mask)"。LSTM 共有三道门(对应示意图从左到右):

  • 遗忘门(forget gate):取隐藏向量,决定状态 C 中哪些分量需要遗忘、哪些需要原样通过;
  • 输入门(input gate):从输入向量与隐藏向量中取信息,写入状态;
  • 输出门(output gate):先用带tanh激活的线性层变换状态,再用隐藏向量 Hᵢ 挑选其中部分分量,产生新状态 Cᵢ₊₁。

状态 C 的分量可以理解为一系列可以"开/关"的旗标(flags)。例如,当序列中出现名字Alice时,我们可以假设它指代女性角色,于是在状态中把"句子中有阴性名词"的旗标打开;稍后读到and Tom时,再打开"名词为复数"的旗标。通过这样操作状态,网络得以跟踪句子各部分的语法属性。

✅ 深入学习 LSTM 内部细节的经典资料是 Christopher Olah 的文章《Understanding LSTM Networks》(本课在"复习与自学"一节中也推荐了它)。

PyTorch 中的 LSTM 分类器

PyTorch 把 LSTM 的内部实现隐藏在LSTMCell/LSTM类中,因此代码与简单 RNN 几乎一样,只需替换循环层(RNNPyTorch.ipynb):

class LSTMClassifier(torch.nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim, num_class): super().__init__() self.hidden_dim = hidden_dim self.embedding = torch.nn.Embedding(vocab_size, embed_dim) self.embedding.weight.data = torch.randn_like(self.embedding.weight.data) - 0.5 self.rnn = torch.nn.LSTM(embed_dim, hidden_dim, batch_first=True) self.fc = torch.nn.Linear(hidden_dim, num_class) def forward(self, x): batch_size = x.size(0) x = self.embedding(x) x, (h, c) = self.rnn(x) return self.fc(h[-1])

这里self.rnn(x)返回(x, (h, c))——h 是最终隐藏向量,c 是最终单元状态。分类时取h[-1]。训练同样使用lr=0.001,Notebook 提醒:LSTM 训练也很慢,训练初期准确率提升不明显,需要耐心调学习率(既能保证合理收敛速度,又不至于浪费内存)。

TensorFlow / Keras 中的 LSTM 层

Keras 同样把 LSTM 封装为现成层,只需替换循环层(RNNTF.ipynb):

model = keras.models.Sequential([ vectorizer, keras.layers.Embedding(vocab_size, embed_size), keras.layers.LSTM(8), keras.layers.Dense(4, activation='softmax') ])

处理变长序列:PyTorch packed sequence 与 Keras masking

真实文本的句子长度各不相同,而神经网络要求同一 batch 内的序列等长。之前我们用零向量(pad token)补齐,但这会带来两个问题:一是内存浪费,二是为填充项额外创建的 RNN 细胞也参与训练,却不携带任何有效输入信息。理想做法是让 RNN 只训练到真实序列长度。

PyTorch 的 packed sequence

PyTorch 为此引入了一种特殊的打包存储格式。假设输入的一个 mini-batch 是:

[[1,2,3,4,5], [6,7,8,0,0], [9,0,0,0,0]]

其中 0 是填充值,各序列真实长度为[5,3,1]。为了高效训练,我们希望先用大 mini-batch([1,6,9])启动第一批 RNN 细胞,然后结束第三条序列的处理,继续用缩短的 mini-batch([2,7]、[3,8])训练,依此类推。因此 packed sequence 被表示为一个向量[1,6,9,2,7,3,8,4,5]加上长度向量[5,3,1],据此即可重构出原 padded mini-batch。

具体实现(RNNPyTorch.ipynb):

def pad_length(b): v = [encode(x[1]) for x in b] # 向量化序列 len_seq = list(map(len, v)) # 每个序列的真实长度 l = max(len_seq) return ( torch.LongTensor([t[0]-1 for t in b]), # 标签 torch.stack([torch.nn.functional.pad(torch.tensor(t), (0, l - len(t)), mode='constant', value=0) for t in v]), # 补齐后的特征 torch.tensor(len_seq) # 长度向量 ) train_loader_len = torch.utils.data.DataLoader( train_dataset, batch_size=16, collate_fn=pad_length, shuffle=True)

对应的LSTMPackClassifier在forward中同时接收补齐后的 mini-batch 与长度向量,先算嵌入,再打包、过 LSTM、解包:

class LSTMPackClassifier(torch.nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim, num_class): super().__init__() self.hidden_dim = hidden_dim self.embedding = torch.nn.Embedding(vocab_size, embed_dim) self.embedding.weight.data = torch.randn_like(self.embedding.weight.data) - 0.5 self.rnn = torch.nn.LSTM(embed_dim, hidden_dim, batch_first=True) self.fc = torch.nn.Linear(hidden_dim, num_class) def forward(self, x, lengths): batch_size = x.size(0) x = self.embedding(x) pad_x = torch.nn.utils.rnn.pack_padded_sequence( x, lengths, batch_first=True, enforce_sorted=False) pad_x, (h, c) = self.rnn(pad_x) x, _ = torch.nn.utils.rnn.pad_packed_sequence(pad_x, batch_first=True) return self.fc(h[-1])

关键 API 与注意事项:

  • 打包用torch.nn.utils.rnn.pack_padded_sequence,解包用torch.nn.utils.rnn.pad_packed_sequence;RNN、LSTM、GRU 等循环层都支持 packed 输入并产出 packed 输出;
  • 示例中其实并未用到解包结果x(后续计算只用隐藏层输出),保留解包是为了方便你改成需要网络输出的场景;
  • 训练时必须传use_pack_sequence=True,因为pack_padded_sequence要求长度张量位于 CPU,训练函数需要避免把长度数据搬到 GPU(详见 torchnlp.py 中train_epoch_emb的实现)。示例日志显示,packed LSTM 最终把验证准确率提升到约 0.81,优于简单 RNN。

Keras 的两种 masking 方案

TensorFlow 侧提供了两种减少 padding 干扰的手段(RNNTF.ipynb):

  1. 按长度分桶:用tf.data.experimental.bucket_by_sequence_length按序列长度重排数据集、把相近长度的序列分到一组;
  2. masking(掩码):Keras 部分层支持额外输入来指明哪些 token 参与训练。实现方式有两种——单独加一个Masking层,或给Embedding层设置mask_zero=True:
model = keras.models.Sequential([ vectorizer, keras.layers.Embedding(vocab_size, embed_size, mask_zero=True), keras.layers.SimpleRNN(16), keras.layers.Dense(4, activation='softmax') ])

开启 masking 后,模型就可以放心地使用"标题 + 正文"的全量数据训练(示例中验证准确率提升到约 0.88)。Notebook 还提示:此前向量化器只在标题上训练,可能忽略正文中的部分 token,严格来说应重新训练向量化器,但为简单起见保留旧向量化器影响也不大。

双向与多层 RNN

此前讨论的循环网络都只沿一个方向运行——从序列开头到结尾。这符合我们阅读和听说的自然方式。但在很多实际场景中,我们对输入序列是随机访问的,因此让循环计算在两个方向上都运行是合理的,这类网络称为双向 RNN(bidirectional RNN),它需要两个隐藏状态向量(每个方向各一个)。

与卷积网络类似,我们也可以在第一个循环层之上再堆叠一层循环网络,以捕捉更高层次的模式——这就是多层 RNN(multi-layer RNN):由两个或更多循环网络组成,前一层输出作为后一层输入。

PyTorch 实现:两个构造参数

PyTorch 把双向和多层都抽象成了构造参数(RNNPyTorch.ipynb):

  • 传bidirectional=True给 RNN/LSTM/GRU 构造函数即可创建双向网络。此时 PyTorch 把两个方向的隐藏状态编码为一个尺寸翻倍的向量,方便直接送入全连接层——只需在创建线性层时把输入维度按翻倍后的尺寸计算;
  • 传num_layers参数即可自动构建多层循环。注意隐藏/状态向量尺寸会按层数成比例增大,处理循环层输出时要相应调整下游层的维度。

TensorFlow 实现:Bidirectional 包装层与 return_sequences

Keras 用Bidirectional层包装循环层,其内部会复制两份循环层,并把其中一份的go_backwards属性设为True,使其沿序列反向运行(RNNTF.ipynb)。构造多层网络时,除最后一层外,所有循环层都必须设置return_sequences=True,因为我们需要中间各层的完整输出序列,而不仅仅是最后一步的状态。一个两层双向 LSTM 分类器如下:

model = keras.models.Sequential([ vectorizer, keras.layers.Embedding(vocab_size, 128, mask_zero=True), keras.layers.Bidirectional(keras.layers.LSTM(64, return_sequences=True)), keras.layers.Bidirectional(keras.layers.LSTM(64)), keras.layers.Dense(4, activation='softmax') ])

Notebook 指出,这个模型训练耗时很长,但能给出到目前为止最高的准确率(训练日志中 acc 已超过 0.87),值得等待。

动手练习与课后作业

本课在 README.md 中提供了两个配套 Notebook,用于随堂练习(标题为"Exercises: Embeddings"):

  • RNNs with PyTorch
  • RNNs with TensorFlow

作业说明 assignment.md 要求:使用本课配套 Notebook(PyTorch 或 TensorFlow 任一版本),用你自己的数据集重新运行一遍——例如带来源标注的 Kaggle 数据集(文中示例是天气推文数据集),改写 Notebook 以突出你的发现,尝试不同类型的任务并记录结论。

此外,文档还布置了 🚀 挑战:阅读关于 LSTM 的文献并思考其应用,包括《Grid Long Short-Term Memory》以及《Show, Attend and Tell: Neural Image Caption Generation with Visual Attention》(后者展示了 LSTM 在图像描述生成中的应用);复习与自学一节则再次推荐 Christopher Olah 的《Understanding LSTM Networks》。

结论

在本单元中我们看到,RNN 不仅可用于序列分类,还能处理更多任务——文本生成、机器翻译等,这些内容将在下一单元继续展开。核心要点回顾:

  1. 词嵌入的聚合表示丢失词序信息,而 RNN 通过逐个符号传递状态向量,天然建模顺序依赖;
  2. 简单 RNN 细胞由输入矩阵 W 与状态矩阵 H 组成,输出为 σ(W×Xᵢ + H×Sᵢ₋₁ + b);嵌入层将 token 降维后,W 尺寸为 emb_size×hid_size、H 为 hid_size×hid_size;
  3. 经典 RNN 存在梯度消失问题,LSTM/GRU 通过遗忘门、输入门、输出门显式管理状态 C,把状态分量当作可开关的旗标以跟踪句子语法属性;
  4. 变长序列处理:PyTorch 用pack_padded_sequence/pad_packed_sequence,Keras 用mask_zero=True或Masking层,避免填充项干扰训练;
  5. 双向与多层 RNN 分别通过bidirectional=True、num_layers(PyTorch)与Bidirectional包装层、return_sequences=True(Keras)构建,多层双向 LSTM 在本课分类任务上取得了最佳效果。

全部代码与数据均可直接在本仓库 lessons/5-NLP/16-RNN 目录下查看与运行,配套的通用训练工具函数位于 torchnlp.py。

  • 教程
  • 人工智能
  • 机器学习
  • 深度学习

【免费下载链接】AI-For-Beginners

12 Weeks, 24 Lessons, AI for All!

项目地址:https://gitcode.com/GitHub_Trending/ai/AI-For-Beginners
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询