☰
动手学深度学习:循环神经网络的简洁实现(RNN 高级 API 实战指南)
2026/10/2 1:37:18 网站建设 项目流程
  • 人工智能
  • 深度学习
  • 机器学习
  • 教程

【免费下载链接】d2l-zh

《动手学深度学习》:面向中文读者、能运行、可讨论。中英文版被70多个国家的500多所大学用于教学。

项目地址:https://gitcode.com/GitHub_Trending/d2/d2l-zh
点击查看免费下载

导读

在《动手学深度学习》中文版的循环神经网络章节中,从零开始实现 RNN(sec_rnn_scratch)帮助读者理解隐藏状态更新的数学细节,但手写反向传播与循环并不高效。本文基于本书「循环神经网络的简洁实现」一节,讲解如何借助深度学习框架高级 API(MXNet Gluon、PyTorch、TensorFlow、PaddlePaddle)在时光机器数据集上快速构建、训练并预测一个字符级语言模型。读完本文,你将掌握rnn.RNN/nn.RNN/SimpleRNNCell/SimpleRNN等高层循环层的正确用法、RNNModel包装类的设计思路(隐藏层与输出层分离、隐状态初始化的形状约定),以及如何复用d2l.load_data_time_machine、d2l.train_ch8、d2l.predict_ch8完成从数据装载到训练预测的完整流程。

为什么需要“简洁实现”

上一节的从零实现(RNNModelScratch)需要读者手动初始化循环核权重、逐时间步展开 forward 函数、自行实现梯度裁剪与更新器。虽然教学意义充分,但存在两点不足:

  1. 不便于复用:循环层、输出层、隐状态初始化全部手写,与框架生态脱节;
  2. 运行速度慢:缺乏框架层针对循环展开的算子融合与内存优化。

本节给出的简洁实现直接调用框架提供的 RNN 层,把“循环”本身交给框架,开发者只需关注把 RNN 层包装成完整语言模型这一件事。从源码结构看,d2l/torch.py与d2l/mxnet.py中同时保留了RNNModelScratch和RNNModel两个类,正是一对“从零实现 vs 高级 API”的对照设计,便于读者逐行对比。

1. 数据准备:时光机器数据集

无论使用哪个框架,第一步都相同:调用d2l.load_data_time_machine获取数据迭代器与词表。

batch_size, num_steps = 32, 35 train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
  • batch_size = 32:每个小批量包含 32 个子序列;
  • num_steps = 35:每个子序列的时间步长度(序列切分窗口);
  • 返回的train_iter是SeqDataLoader实例(可迭代对象),vocab是字符级词表Vocab。

从源码实现看,该函数的核心链路位于 d2l/torch.py(其余框架版本在 d2l/mxnet.py、d2l/tensorflow.py、d2l/paddle.py):

def load_data_time_machine(batch_size, num_steps, use_random_iter=False, max_tokens=10000): """返回时光机器数据集的迭代器和词表""" data_iter = SeqDataLoader( batch_size, num_steps, use_random_iter, max_tokens) return data_iter, data_iter.vocab

其中SeqDataLoader在构造时调用load_corpus_time_machine(max_tokens)(见 d2l/mxnet.py):对《时光机器》原文按字符分词(tokenize(lines, 'char'))、构建词表Vocab、并默认只取前max_tokens=10000个词元作为语料。use_random_iter参数控制小批量划分方式:

  • use_random_iter=False(默认):顺序分区,seq_data_iter_sequential将相邻子序列按顺序组织,上一个批量末尾的隐状态可以直接传给下一个批量;
  • use_random_iter=True:随机抽样,seq_data_iter_random从随机偏移开始切分并打乱子序列顺序,此时每个批量都必须重新初始化隐状态。

这一参数的选择会直接影响下一节隐状态传递的写法,值得记住。

2. 定义模型:用高级 API 构造 RNN 层

2.1 各框架的 RNN 层构造

本节构造一个隐藏层、256 个隐藏单元的循环层:

MXNet(Gluon)

num_hiddens = 256 rnn_layer = rnn.RNN(num_hiddens) rnn_layer.initialize()

PyTorch

num_hiddens = 256 rnn_layer = nn.RNN(len(vocab), num_hiddens)

注意 PyTorch 的nn.RNN必须显式传入输入特征维度,因此第一个参数是len(vocab)(词表大小),这与 Gluon 的用法不同。

TensorFlow(Keras)

num_hiddens = 256 rnn_cell = tf.keras.layers.SimpleRNNCell(num_hiddens, kernel_initializer='glorot_uniform') rnn_layer = tf.keras.layers.RNN(rnn_cell, time_major=True, return_sequences=True, return_state=True)

TensorFlow 采用“Cell + 包装层”的两段式构造:SimpleRNNCell描述单步计算单元,tf.keras.layers.RNN负责循环展开;time_major=True表示输入形状为(时间步,批量,特征),与num_steps在轴 0 的习惯一致。

PaddlePaddle

num_hiddens = 256 rnn_layer = nn.SimpleRNN(len(vocab), num_hiddens, time_major=True)

Paddle 的nn.SimpleRNN(input_size, hidden_size, time_major=True)与 PyTorch 类似需要输入维度,同时用time_major=True声明时间步优先。

关于多层 RNN:本书在 深度循环神经网络(sec_deep_rnn)中才展开讨论,这里只需理解多层就是把上一层 RNN 的输出作为下一层 RNN 的输入。

2.2 初始化隐状态

MXNet:调用rnn_layer.begin_state(batch_size=batch_size),返回一个列表state,列表中包含小批量中每个样本的初始隐状态,形状为(隐藏层数, 批量大小, 隐藏单元数),即(1, 32, 256)。对于后续章节要介绍的 LSTM 等模型,该列表还会包含额外信息(如记忆单元)。

state = rnn_layer.begin_state(batch_size=batch_size) len(state), state[0].shape # (1, (1, 32, 256))

PyTorch:直接用零张量初始化,形状同样是(隐藏层数, 批量大小, 隐藏单元数):

state = torch.zeros((1, batch_size, num_hiddens)) state.shape # torch.Size([1, 32, 256])

TensorFlow:通过rnn_cell.get_initial_state(batch_size=batch_size, dtype=tf.float32)获取。

Paddle:与 PyTorch 一致,paddle.zeros(shape=[1, batch_size, num_hiddens])。

2.3 前向:rnn_layer的输出到底是什么

给定隐状态和输入,调用rnn_layer(X, state)会返回(Y, state_new):

X = torch.rand(size=(num_steps, batch_size, len(vocab))) Y, state_new = rnn_layer(X, state) Y.shape, state_new.shape

必须强调:这里的“输出”Y不涉及输出层(全连接层)的计算,它指的是每个时间步的隐状态,形状为(num_steps, batch_size, num_hiddens),这些隐状态将作为后续输出层的输入。而:

  • state_new(MXNet 中为列表,PyTorch/Paddle 中为张量)是小批量最后时间步的隐状态,可用于顺序分区中下一个批量的隐状态初始化;
  • 在 MXNet 中,多隐藏层时每一层的隐状态都会存放在state_new中;
  • 后续的 LSTM 等模型返回的state_new还会携带更多信息(如记忆单元状态)。

2.4 包装完整模型:RNNModel类

rnn_layer只包含隐藏的循环层,因此还需要一个单独的输出层把每个时间步的隐状态映射到词表大小的 logits。本书定义了RNNModel类完成包装。以 PyTorch 版本为例(d2l/torch.py):

class RNNModel(nn.Module): """循环神经网络模型""" 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))

要点拆解:

  1. One-Hot 编码与转置:inputs.T把输入的(batch_size, num_steps)转置为(num_steps, batch_size),再经F.one_hot变成(num_steps, batch_size, vocab_size)——这正是time_major约定下 RNN 层期待的输入形状;
  2. 形状重塑:Y.reshape((-1, Y.shape[-1]))把(num_steps, batch_size, num_hiddens)展平为(num_steps * batch_size, num_hiddens),送入全连接层后输出(num_steps * batch_size, vocab_size),与训练时的标签y = Y.T.reshape(-1)对齐;
  3. 双向预留:bidirectional分支把num_directions设为 2,并令输出层输入维度翻倍(num_hiddens * 2),为后续 双向循环神经网络 留好了接口;
  4. 隐状态形态差异:nn.LSTM的隐状态是(h, c)元组,而普通 RNN/GRU 是单个张量,begin_state对此做了类型分支处理——这是 PyTorch/Paddle 版本与 MXNet 版本(后者直接透传self.rnn.begin_state(*args, **kwargs))最大的实现差异。

MXNet(d2l/mxnet.py)和 TensorFlow(d2l/tensorflow.py)版本的RNNModel结构类似,其中 TensorFlow 的call里用Y, *state = self.rnn(X, state)解包 Keras RNN 返回的多个值。

3. 训练与预测

3.1 用随机权重先“预演”一次预测

正式训练前,先基于随机初始化的权重生成 10 个字符,直观感受未训练模型的输出:

device = d2l.try_gpu() net = RNNModel(rnn_layer, vocab_size=len(vocab)) net = net.to(device) d2l.predict_ch8('time traveller', 10, net, vocab, device)

(MXNet 版本使用net.initialize(force_reinit=True, ctx=device)初始化参数;TensorFlow 版本用tf.distribute.OneDeviceStrategy(device_name)包装模型。)正如预期,随机权重下模型输出是乱码——这证明模型尚未学到任何语言规律。

从源码看,d2l.predict_ch8(d2l/torch.py)的执行流程为:

  1. net.begin_state(batch_size=1, device=device)初始化单样本隐状态;
  2. 预热期:用前缀prefix[1:]逐字符过网络,只更新状态、不生成新字符;
  3. 生成期:循环num_preds次,以y.argmax(dim=1)贪婪采样下一个字符,并拼接成输出字符串。

3.2 正式训练:train_ch8

复用与从零实现相同的超参数(num_epochs=500, lr=1),调用d2l.train_ch8训练:

num_epochs, lr = 500, 1 d2l.train_ch8(net, train_iter, vocab, lr, num_epochs, device)

train_ch8(d2l/torch.py)内部做了四件事:

  1. 选择损失函数nn.CrossEntropyLoss()(MXNet 为gluon.loss.SoftmaxCrossEntropyLoss,TensorFlow 为SparseCategoricalCrossentropy(from_logits=True));
  2. 构造优化器torch.optim.SGD(net.parameters(), lr);
  3. 每 10 个 epoch 调用predict_ch8(prefix, 50, net, vocab, device)打印生成结果并绘制困惑度曲线;
  4. 训练结束后输出最终困惑度、处理速度(词元/秒)与设备信息,并分别以'time traveller'和'traveller'为前缀展示生成文本。

在train_epoch_ch8(d2l/torch.py)中有一个与数据加载方式密切相关的细节:当use_random_iter=False(顺序分区)时,若网络是nn.Module且隐状态是单个张量(如nn.GRU),会调用state.detach_()截断反向传播——即跨小批量的隐状态延续计算图但不回传梯度;当使用随机抽样时,则每个批量都通过begin_state重新初始化隐状态。梯度裁剪阈值固定为 1(grad_clipping(net, 1)),防止循环展开带来的梯度爆炸。

3.3 效果与对比

训练完成后模型达到与从零实现相当的困惑度(perplexity),但由于高级 API 对循环计算做了算子级优化,训练时间更短、单位时间处理词元更多。这是本节选择高级 API 的核心收益:同样的语言模型任务,代码更少、更不易出错、运行更快。

4. 小结

  • 深度学习框架的高级 API 提供了现成的循环神经网络层(Gluonrnn.RNN、PyTorchnn.RNN、KerasSimpleRNNCell + RNN、Paddlenn.SimpleRNN),开发者无需手写循环展开与权重矩阵;
  • 高级 API 的 RNN 层返回每个时间步的隐状态(Y)与最后时间步的隐状态(state_new),前者用于输出层计算,后者用于跨小批量传递;注意Y并不包含输出层计算;
  • 还需自行添加一个输出层(全连接层),把隐状态映射到词表大小,RNNModel类就是这一包装的标准模板;
  • 相比从零实现,使用高级 API 可显著加速训练,并天然兼容顺序分区 / 随机抽样两种小批量策略以及 LSTM、GRU 等后续模型。

5. 练习与拓展

  1. 尝试用高级 API 让 RNN 模型过拟合(例如减少训练数据量或增大隐藏单元数),观察困惑度变化;
  2. 如果增加 RNN 的隐藏层数,训练会发生什么变化?在保持其他超参数不变的情况下,多层是否会让模型正常工作?(提示:回顾begin_state中num_layers对隐状态形状的影响,以及 深度循环神经网络 的内容)
  3. 尝试用 RNN 实现序列(sec_sequence)中的自回归模型,对比简单线性自回归模型的效果。

想对照“从零实现 vs 简洁实现”的完整代码,可以直接查看 d2l/torch.py 中的RNNModelScratch与RNNModel两个类,以及其余框架的对应实现(d2l/mxnet.py、d2l/tensorflow.py、d2l/paddle.py)。

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

【免费下载链接】d2l-zh

《动手学深度学习》:面向中文读者、能运行、可讨论。中英文版被70多个国家的500多所大学用于教学。

项目地址:https://gitcode.com/GitHub_Trending/d2/d2l-zh
点击查看免费下载

相关推荐

上一篇:Tails Components 模态框与通知组件:提升用户体验的交互设计指南 🎯
下一篇:音频转视频:Audiogram 开源项目推荐

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

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

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

立即咨询