☰
AI-For-Beginners 课程实战:用循环神经网络(RNN)与 LSTM 建模文本序列顺序
2026/10/5 3:21:51 网站建设 项目流程
  • 教程
  • 人工智能
  • 机器学习
  • 深度学习

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

12 Weeks, 24 Lessons, AI for All!

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

循环神经网络(RNN)解决了词向量嵌入聚合后丢失词序信息的问题,是文本生成、机器翻译、问答等复杂 NLP 任务的基础架构。本文基于AI-For-Beginners课程 第 16 课:循环神经网络,系统讲解 RNN 的数学原理、LSTM 门控机制、双向与多层网络结构,并结合仓库中的 PyTorch 与 TensorFlow 两个实战 Notebook,给出可直接运行的文本分类实现。

为什么需要 RNN:词嵌入丢失了"顺序"

在课程前几节(13-TextRep 与 14-Embeddings)中,我们使用嵌入表示文本,并在嵌入之上叠加一个简单的线性分类器。这种架构能捕获句子中单词的聚合语义,但不考虑词的顺序——因为嵌入上的聚合操作(如取平均)已经把词序信息从原始文本中抹掉了。

无法建模词序的模型,也就无法解决更复杂、更含歧义的任务,例如文本生成(text generation)或问答(question answering)。例如,not like与like not的聚合结果可能相同,但语义完全不同。

要捕获文本序列的含义,就需要另一种神经网络架构:循环神经网络(Recurrent Neural Network,RNN)。使用 RNN 时,我们把句子一个符号一个符号地送入网络,网络产出某种状态(state),再将这个状态与下一个符号一起再次送入网络,如此往复。

上图左侧是 RNN 的"展开"形式,右侧是更紧凑的循环反馈形式。注意所有 RNN 块共享同一套权重(shareable weights)。

RNN 的核心工作机制

给定输入 token 序列 X₀, X₁, ..., Xₙ,RNN 创建一串神经网络块,并通过反向传播(backpropagation)端到端地训练这一串块:

  • 每个网络块接收一对输入 (Xᵢ, Sᵢ),输出 Sᵢ₊₁;
  • 最终状态 Sₙ(或输出 Yₙ)送入线性分类器,产生分类结果;
  • 所有网络块的权重完全相同,整个序列只用一次反向传播即可完成训练。

因为状态向量 S₀, ..., Sₙ 在网络中被逐级传递,RNN 能够学习词之间的顺序依赖。例如,当序列中某处出现单词not时,网络可以学会对状态向量中的某些元素取反,从而表达否定语义。

解剖一个 RNN 单元(Cell)

一个简单的 RNN 单元接收前一状态 Sᵢ₋₁ 和当前符号 Xᵢ 作为输入,需要产出输出状态 Sᵢ(在生成式网络场景中,有时还关心另一个输出 Yᵢ)。

单元内部有两套权重矩阵:

  • 权重矩阵W:变换输入符号;
  • 权重矩阵H:变换输入状态。

网络输出按下式计算:

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

其中 σ 是激活函数(如 tanh),b 是额外偏置。TensorFlow Notebook(RNNTF.ipynb)给出了等价的数学形式:Sᵢ₊₁ = f(W_H × Sᵢ + W_I × Xᵢ + b);对于生成/翻译这类需要在每一步都产出值的任务,还会额外引入输出矩阵 W_O,Yᵢ = f(W_O × Sᵢ + b_O)。

嵌入层如何影响矩阵维度

在很多实际场景中,输入 token 在进入 RNN 前会先经过**嵌入层(embedding layer)**以降低维度。此时:

  • 若输入向量的维度为emb_size,状态向量的维度为hid_size;
  • 则矩阵 W 的尺寸为emb_size × hid_size,矩阵 H 的尺寸为hid_size × hid_size。

长短期记忆网络(LSTM):解决梯度消失

经典 RNN 的主要问题之一,是所谓的梯度消失(vanishing gradients)问题。由于 RNN 通过一次反向传播端到端训练,误差很难传播到网络最前面的层,因此网络无法学习相距很远 token 之间的关系。

解决这一问题的方法之一是引入显式状态管理,即所谓的门(gates)。最著名的两类门控架构是长短期记忆网络(Long Short Term Memory,LSTM)和门控循环单元(Gated Recurrent Unit,GRU)。

LSTM 的组织方式与 RNN 类似,但有两个状态在层与层之间传递:

  • 实际状态C(cell state);
  • 隐藏向量H(hidden vector)。

在每个单元中,隐藏向量 Hᵢ 与输入 Xᵢ 拼接,二者通过门控制状态 C 的更新。每个门都是带sigmoid 激活(输出范围 [0,1])的神经网络,与状态向量相乘时可以视作按位掩码(bitwise mask)。LSTM 包含以下三个门(对应上图中从左到右的顺序):

门作用
遗忘门(forget gate)接收隐藏向量,决定向量 C 中的哪些分量需要遗忘、哪些需要保留传递
输入门(input gate)从输入向量与隐藏向量中取信息,插入到状态中
输出门(output gate)先经带 tanh 激活的线性层变换状态,再用隐藏向量 Hᵢ 选择部分分量,产出新状态 Cᵢ₊₁

把状态 C 想象成一组开关

状态 C 的分量可以被看作可开关的标志位(flags)。例如:

  • 当序列中出现名字Alice时,我们推测它指代女性角色,于是在状态中把"句子中有阴性名词"的标志置位;
  • 当之后出现短语and Tom时,再把"有复数名词"的标志置位。

通过这种状态操纵,模型可以跟踪句子各部分的语法属性——这正是 LSTM 能够在长序列中保留信息的原因。

双向与多层 RNN

前面讨论的循环网络都从序列开头向末尾单向运行,这与我们阅读、听语音的方式一致,看起来自然。但许多实际场景中我们可以随机访问输入序列,因此把循环计算两个方向各跑一遍往往更合理——这类网络称为双向 RNN(bidirectional RNN)。使用双向网络时需要两个隐藏状态向量,每个方向各一个。

无论是单向还是双向,循环网络都会捕获序列中的某些模式,并将其存入状态向量或传给输出。与卷积网络类似,我们可以在第一层循环网络之上再叠加一层循环网络,从第一层提取的低层模式中构建更高层的模式——这就是多层 RNN(multi-layer RNN):由两个或更多循环网络组成,前一层的输出作为下一层的输入。

实战一:PyTorch 实现 RNN / LSTM 文本分类

仓库提供了 RNNPyTorch.ipynb 与配套工具模块 torchnlp.py,使用经典的AG_NEWS新闻四分类数据集(类别:World、Sports、Business、Sci/Tech)进行实验。

数据加载与预处理

工具模块 torchnlp.py 中load_dataset()负责加载数据集并构建词表:

train_dataset, test_dataset = torchtext.datasets.AG_NEWS(root='./data') classes = ['World', 'Sports', 'Business', 'Sci/Tech'] counter = collections.Counter() for (label, line) in train_dataset: counter.update(torchtext.data.utils.ngrams_iterator(tokenizer(line), ngrams=ngrams)) vocab = torchtext.vocab.vocab(counter, min_freq=min_freq)

encode()将文本 token 映射为词表索引,padify()把一个 minibatch 中的所有序列填充到相同长度(不足部分补 0),使其可以组成批量张量。

简单 RNN 分类器

在 PyTorch 中,单个循环单元对应torch.nn.RNNCell,由多个单元组成的层对应torch.nn.RNN。RNNPyTorch.ipynb中定义了如下的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))

关键点:

  • 这里出于简洁使用了未预训练的嵌入层;如需更好效果,可以换成上一单元介绍的 Word2Vec / GloVe 预训练嵌入;
  • torch.nn.RNN层返回两个输出:x是每一步单元输出的序列,h是序列最后一个元素的最终隐藏状态;
  • 我们取所有步输出的均值x.mean(dim=1)再送入全连接分类层。

训练配置:batch_size=16、embed_dim=64、hidden_dim=32、学习率lr=0.001。注意:RNN 按序列长度展开后,参与反向传播的层数很多,训练困难,需要较小的学习率与较大的数据集,强烈建议使用 GPU。

LSTM 分类器:只改一层

LSTM 的内部结构虽然复杂,但 PyTorch 把实现封装在torch.nn.LSTMCell与torch.nn.LSTM中,因此分类器结构与简单 RNN 几乎一致:

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): x = self.embedding(x) x, (h, c) = self.rnn(x) return self.fc(h[-1])

注意forward中取的是h[-1]——即最后一层、序列末尾的隐藏状态(LSTM 返回的(h, c)中,h是所有层各方向的最后隐藏状态)。

进阶:Packed Sequence 高效训练

用padify填充的序列会带来问题:为填充项额外创建的 RNN 单元也参与训练,却不携带有效信息,造成浪费。PyTorch 提供了packed sequence(打包序列)机制来只训练到序列的真实长度。

假设一个 minibatch 为:

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

其中 0 是填充值,真实长度向量为[5,3,1]。打包后表示为一个向量[1,6,9,2,7,3,8,4,5]加上长度向量[5,3,1],训练时按步收缩 minibatch,避免无效计算。

RNNPyTorch.ipynb中的LSTMPackClassifier展示了完整用法:

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): 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])

注意:pack_padded_sequence要求长度向量位于 CPU 上,因此训练函数中需要避免把长度数据搬到 GPU——这正是torchnlp.py中train_epoch_emb(..., use_pack_sequence=True)参数的作用(见 torchnlp.py)。RNN、LSTM、GRU 三类循环层都原生支持打包输入。

Notebook 中的实测效果(复现结果参考)

Notebook 中记录的训练日志显示(AG_NEWS 训练集,一个 epoch 内):

  • 简单 RNN 分类器:处理约 10.8 万样本后准确率约 0.806;
  • LSTM 分类器:约 11.8 万样本后准确率约 0.773,早期上升缓慢是正常现象;
  • Packed LSTM:约 11.8 万样本后准确率约 0.814,同时省去了填充 token 带来的无效计算。

这些数字来自 Notebook 运行输出的复现结果,仅供对照参考;实际效果会因设备、随机种子与训练时长而不同。

实战二:TensorFlow/Keras 实现 RNN 与双向 LSTM

仓库还提供了 RNNTF.ipynb(Keras 版本),使用tfds.load('ag_news_subset')加载数据集。首次运行前需要安装tensorflow_datasets并预取数据:

import tensorflow as tf from tensorflow import keras import tensorflow_datasets as tfds physical_devices = tf.config.list_physical_devices('GPU') if len(physical_devices) > 0: tf.config.experimental.set_memory_growth(physical_devices[0], True)

简单 RNN 分类器(Keras 版)

在 Keras 中,简单循环层对应keras.layers.SimpleRNN。模型结构为:TextVectorization(词表大小 20000)→Embedding(20000, 64)→SimpleRNN(16)→Dense(4, activation='softmax'),仅用新闻标题(title)训练:

model = keras.models.Sequential([ vectorizer, keras.layers.Embedding(vocab_size, embed_size), keras.layers.SimpleRNN(16), keras.layers.Dense(4, activation='softmax') ]) model.compile(loss='sparse_categorical_crossentropy', metrics=['acc'], optimizer='adam')

Notebook 记录该标题模型验证准确率约 0.80——只训练标题会限制精度上限。

处理变长序列:Masking

TextVectorization会自动用填充 token 对齐序列,但这些 token 同样参与训练,可能干扰收敛。Notebook 给出两种缓解手段:

  1. 用tf.data.experimental.bucket_by_sequence_length按序列长度分桶、重排数据集;
  2. 使用掩码(masking):在Embedding层上设置mask_zero=True,或在模型中显式加入Masking层。

在标题+描述全文本上启用掩码后,模型验证准确率提升到约 0.88(Notebook 记录值)。

Keras 版 LSTM 与双向多层 LSTM

LSTM 在 Keras 中同样只是一行替换——把SimpleRNN换成keras.layers.LSTM。双向网络则需要用keras.layers.Bidirectional包装循环层;该包装层内部复制两层,并将其中一份的go_backwards设为True。

多层 RNN 在 Keras 中就是简单地把多个循环层堆叠:除最后一层外,其余循环层都必须设置return_sequences=True,以输出每一步的中间状态。Notebook 最终构建了两层双向 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 中精度最高的模型(训练中即达约 0.87),代价是训练时间明显更长。

小结与进阶方向

RNN 不仅能用于序列分类,还能胜任更多任务:文本生成、机器翻译等——这些将在课程的下一单元(17-GenerativeNetworks、18-Transformers)中继续探讨。

本课程还布置了一个配套作业:assignment.md:使用本课的 PyTorch 或 TensorFlow Notebook,换用自己的数据集(例如天气相关的推文数据集,使用时注明出处),重跑并改写 Notebook,记录你的发现。这是把"会跑示例"变成"会用模型"的关键一步。

延伸阅读建议

  • 深入理解 LSTM 内部机制,推荐研读 Christopher Olah 的经典文章Understanding LSTM Networks;
  • 阅读 GRU(门控循环单元)相关资料,对比其与 LSTM 的差异与适用场景;
  • 关注 RNN 的注意力扩展:Show, Attend and Tell(图像描述中的视觉注意力)与Grid LSTM等变体,理解循环架构如何向生成任务与长程依赖演进。

运行环境提示:本课依赖 PyTorch/TensorFlow 生态。本地安装可参考 NLP 分节的依赖清单 requirements-pytorch.txt 与 requirements-tf.txt;训练较大模型时建议使用 GPU,并注意通过调整 minibatch 大小控制显存占用。

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

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

12 Weeks, 24 Lessons, AI for All!

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

相关推荐

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

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

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

立即咨询