动手学深度学习——深层循环神经网络代码

张开发
2026/4/14 16:57:23 15 分钟阅读

分享文章

动手学深度学习——深层循环神经网络代码
1. 前言上一篇我们已经从概念上理解了深层循环神经网络单层循环网络只能在每个时间步做一次时序变换深层循环网络会在同一时间步上堆叠多层循环层这样可以让模型学习更有层次的序列表示这一篇就继续按李沐的节奏把它真正落到代码上。这一节最关键的不是新发明一个循环单元而是看清楚当循环层从 1 层变成多层时代码上到底变了什么你会发现变化其实没有想象中大核心集中在num_layers状态张量形状输出与状态的理解2. 深层循环网络代码的核心变化在哪里如果回顾前面的简洁实现你会发现单层 RNN / GRU / LSTM 的代码结构都很像定义循环层定义输出线性层输入序列进入循环层隐藏表示送入线性层输出对词表的打分深层循环网络也还是这条主线。真正变化的地方主要只有两个第一循环层层数变了通过num_layers来指定。第二状态张量第一维变了因为每一层都要维护自己的隐藏状态。所以这节代码的核心就是把“层”这个维度加进来。3. 最常见的写法直接设num_layers比如之前我们写单层 LSTM可能是lstm_layer nn.LSTM(input_sizevocab_size, hidden_sizenum_hiddens)如果要改成两层只需要写成lstm_layer nn.LSTM( input_sizevocab_size, hidden_sizenum_hiddens, num_layers2 )这就是最直接的深层循环网络代码实现。也就是说PyTorch 已经把多层堆叠封装好了。你不用手写“第 1 层传给第 2 层”的逻辑框架会自动完成。4.num_layers2到底发生了什么这一句num_layers2意味着在每个时间步上第 1 层接收原始输入第 2 层接收第 1 层当前时刻的输出同时第 1 层自己沿时间维护一条隐藏状态链第 2 层也沿时间维护自己的隐藏状态链所以它等价于“纵向堆了两层循环单元”。注意它不是序列看两遍或者把时间步翻倍而是每个时间步内部处理更深了。5. 输入张量形状变了吗通常不变。对于 PyTorch 默认格式输入仍然是(num_steps, batch_size, input_size)例如X torch.rand(size(35, 2, 28))表示序列长度 35batch size 为 2每个时间步输入维度为 28即使你把num_layers改成 2 或 3输入格式仍然是这个样子。这点很重要深层循环网络主要改变的是内部结构不是输入接口。6. 输出张量形状会变吗先看代码num_hiddens 256 lstm_layer nn.LSTM(28, num_hiddens, num_layers2) X torch.rand(size(35, 2, 28)) state ( torch.zeros((2, 2, num_hiddens)), torch.zeros((2, 2, num_hiddens)) ) Y, state_new lstm_layer(X, state)这里的输出Y形状通常仍然是(35, 2, 256)这说明什么说明输出张量最后只给你“最顶层”在每个时间步上的输出。也就是说虽然内部有两层但Y默认代表的是最后一层的时序输出。7. 为什么Y形状没有多出“层数维”因为 PyTorch 的设计里所有层内部堆叠是模块内部逻辑对外暴露的Y只保留顶层输出这很合理因为对于大多数任务来说我们最终更关心的是最顶层抽象后的时序表示而不是每一层都单独拿出来用。所以状态里会保留层数维输出Y不会显式多出层数维8. 状态张量形状为什么会变这一节最容易混的地方就在状态。对于两层单向 LSTM状态通常是H.shape (2, batch_size, num_hiddens) C.shape (2, batch_size, num_hiddens)这里第一维2就是层数。例如(2, 2, 256)表示2 层batch size 2hidden size 256也就是说第 1 层和第 2 层各自都有一份隐藏状态和记忆单元。9. 单层和双层状态形状怎么对比这个最好直接对照记。单层 LSTMH.shape (1, batch_size, hidden_size) C.shape (1, batch_size, hidden_size)双层 LSTMH.shape (2, batch_size, hidden_size) C.shape (2, batch_size, hidden_size)三层 LSTMH.shape (3, batch_size, hidden_size) C.shape (3, batch_size, hidden_size)所以规律非常清楚第一维就是层数 × 方向数在当前单向情况下就是层数本身。10. 深层 GRU / RNN 也是同样规律吗是的规律相同。例如两层 GRUgru_layer nn.GRU(input_sizevocab_size, hidden_sizenum_hiddens, num_layers2)那么状态形状就是(2, batch_size, num_hiddens)因为 GRU 没有单独的C所以只会返回一个状态张量。两层 RNN 也是一样。也就是说深层 RNN / GRU / LSTM 的主要区别仍然在单元内部“深层”这件事的代码接口几乎一致11. 深层语言模型封装类怎么改其实几乎不用大改。如果你前面写过这种模型class RNNModel(nn.Module): def __init__(self, rnn_layer, vocab_size): super().__init__() self.rnn rnn_layer self.vocab_size vocab_size self.num_hiddens self.rnn.hidden_size self.num_directions 1 self.linear nn.Linear(self.num_hiddens, self.vocab_size)那现在只要把rnn_layer nn.LSTM(..., num_layers2)传进去整体就已经是深层 LSTM 模型了。为什么几乎不用改因为Y的形状没变仍然是(num_steps, batch_size, hidden_size)线性层输入维度也没变仍然是hidden_size所以语言模型头部还能直接复用。12.begin_state需要改吗需要注意状态初始化要匹配层数。例如对两层 LSTM初始化时要写成def begin_state(self, device, batch_size1): return ( torch.zeros((self.rnn.num_layers, batch_size, self.num_hiddens), devicedevice), torch.zeros((self.rnn.num_layers, batch_size, self.num_hiddens), devicedevice) )这和单层相比区别就在于第一维从1变成了self.rnn.num_layers。如果是 GRU / RNN则只需要初始化一个张量torch.zeros((self.rnn.num_layers, batch_size, self.num_hiddens), devicedevice)所以深层循环网络最核心的代码适配点之一就是状态初始化。13. 为什么线性层不用跟着变成两层因为线性层接的是顶层输出Y而Y的最后一维仍然只是hidden_size不是num_layers * hidden_size所以输出层只需要继续做hidden_size - vocab_size即可。这说明深层循环网络的“深”主要发生在特征提取阶段而不是输出头阶段。14. 如何验证深层网络真的生效了最简单的方式就是打印模型层和状态形状。例如num_hiddens 256 num_layers 2 lstm_layer nn.LSTM(vocab_size, num_hiddens, num_layersnum_layers)然后喂一段输入检查Y.shape H.shape C.shape你会看到Y.shape仍然是(num_steps, batch_size, hidden_size)H.shape和C.shape变成(num_layers, batch_size, hidden_size)只要这两个地方对上了就说明深层结构已经真正启用了。15. 深层循环网络训练时有什么要特别注意的从训练代码框架来看基本流程不变输入 token 序列前向传播计算交叉熵损失反向传播梯度裁剪参数更新但是深层后通常要更注意以下几点。15.1 更容易过拟合层数增加后模型容量变大。15.2 更容易训练不稳定尤其在较长序列上多层递推会增加优化难度。15.3 更依赖合理超参数例如隐藏维度层数学习率dropout所以深层不是简单“加个层数就一定更好”。16. dropout 在深层循环网络里为什么常出现因为层数增加后模型更容易过拟合。所以实际中经常会在多层循环网络中加dropout...例如lstm_layer nn.LSTM( input_sizevocab_size, hidden_sizenum_hiddens, num_layers2, dropout0.5 )这里的 dropout 通常作用在层与层之间的输出上。它的目的是减少层间过度依赖增强泛化能力。这一点在深层循环网络中很常见。17. 深层循环网络和前面单层模型的本质关系这一节最重要的理解其实是深层循环网络不是新单元而是旧单元的堆叠。也就是说单层 LSTM 解决“单元如何记忆”深层 LSTM 解决“多层如何表达更丰富的时序特征”所以这不是互斥关系而是两个维度单元维度RNN / GRU / LSTM结构维度单层 / 多层把这两个维度分清后面你看到各种模型就不会乱。18. 这一节最该掌握什么如果从学习重点看最重要的是下面几件事。18.1 会用num_layers知道它控制的是层数不是时间步。18.2 看懂状态形状变化尤其是第一维为什么变成层数。18.3 明白输出Y仍然只代表顶层输出不会额外多出层数维。18.4 知道模型封装几乎可以复用很多时候只要改循环层定义和状态初始化。18.5 明白深层循环网络的收益和代价收益是表达更强代价是训练更难。19. 本节总结这一节我们学习了深层循环神经网络的代码实现核心内容可以总结为以下几点。19.1 深层循环网络在代码里主要通过num_layers实现框架已经帮我们完成层间堆叠。19.2 输入张量形状通常不变仍然是按时间步、batch、特征维组织。19.3 输出Y通常是顶层输出形状一般不显式包含层数维。19.4 状态张量第一维会变成层数因为每一层都要维护自己的时序状态。19.5 深层循环网络常和 LSTM / GRU 搭配使用这是实际中更常见、更稳定的做法。20. 学习感悟这一节很有意思因为它让你看到循环神经网络不仅能“沿时间递推”也能“沿层次递进”。这意味着序列建模不再只是“记住前面发生了什么”还可以进一步变成从低层模式到高层语义一层层抽象过去。这和前面卷积网络从浅层到深层提特征的思想其实是完全相通的。

更多文章