news 2026/6/10 21:35:43

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
动手学深度学习——双向循环神经网络代码

1. 前言

上一篇我们已经从概念上理解了双向循环神经网络

  • 它会同时建立前向和后向两条循环链

  • 每个位置都能同时利用左上下文和右上下文

  • 它更适合理解型任务,而不适合标准自回归语言模型

这一篇就继续按李沐的节奏,把它真正落到代码上。

这一节最关键的,不是重新发明一个新单元,
而是看清楚:

当循环网络从单向变成双向时,代码里到底发生了什么变化?

你会发现,核心变化其实非常集中:

  • 多了bidirectional=True

  • 输出维度翻倍

  • 状态第一维乘上 2

  • 线性层输入维度也要跟着变

也就是说,这一节的重点不是“代码特别多”,
而是要把形状变化真正看明白。


2. 双向循环网络在代码里怎么开启

PyTorch 对双向循环神经网络的支持非常直接。
无论是:

  • nn.RNN

  • nn.GRU

  • nn.LSTM

都可以通过同一个参数开启双向模式:

bidirectional=True

例如:

lstm_layer = nn.LSTM( input_size=vocab_size, hidden_size=num_hiddens, bidirectional=True )

这就表示:

  • 使用 LSTM 作为循环单元

  • 同时构建一条前向链和一条后向链

所以从 API 使用上看,双向循环网络非常“轻量化”。


3. 为什么说双向代码改动不大

因为框架已经把最复杂的部分封装好了:

  • 前向链怎么递推

  • 后向链怎么递推

  • 两个方向怎么拼接

  • 状态怎么管理

这些你都不用手写。

你真正需要做的,是理解双向后这几件事:

第一,隐藏表示维度变了

输出不再只是hidden_size,而是2 * hidden_size

第二,状态维度变了

状态第一维要乘上方向数。

第三,输出层输入维度也要变

因为最后送给线性层的特征维度翻倍了。

所以双向循环网络的代码重点,是接口和形状,不是底层递推本身。


4. 先看一个最小例子

先写一个最简单的双向 LSTM:

import torch from torch import nn vocab_size = 28 num_hiddens = 256 lstm_layer = nn.LSTM( input_size=vocab_size, hidden_size=num_hiddens, bidirectional=True )

这里:

  • 输入维度是vocab_size

  • 隐藏状态维度是256

  • 开启双向

到这里为止,和单向 LSTM 唯一的区别就是多了:

bidirectional=True

5. 输入张量形状会变化吗

不会。

双向循环网络的输入格式和单向完全一样。
例如:

X = torch.rand(size=(35, 2, vocab_size))

这表示:

  • 35:时间步长度num_steps

  • 2:批量大小batch_size

  • vocab_size:每个时间步输入向量维度

也就是:

(num_steps, batch_size, input_size)

所以你要先记住一点:

双向不会改变输入接口。

它改变的是模型内部结构和输出表示。


6. 初始状态为什么变了

这才是双向代码里最重要的变化之一。

对于单层单向 LSTM,状态形状通常是:

(1, batch_size, hidden_size)

因为:

  • 1 层

  • 1 个方向

而双向后会变成:

(2, batch_size, hidden_size)

因为:

  • 1 层

  • 2 个方向(前向 + 后向)

所以初始化通常要写成:

state = ( torch.zeros((2, 2, num_hiddens)), torch.zeros((2, 2, num_hiddens)) )

这里两个2分别表示:

  • 第一维:方向数 = 2

  • 第二维:batch_size = 2


7. 如果是双向 LSTM,为什么状态有两个张量

因为 LSTM 本来就维护两个状态:

  • 隐藏状态H

  • 记忆单元C

所以完整状态是:

(H, C)

而双向之后,HC各自都要为两个方向保存状态。

因此:

  • H.shape = (2, batch_size, hidden_size)

  • C.shape = (2, batch_size, hidden_size)

这点和单向 LSTM 的区别,本质在于第一维翻倍。


8. 跑一次前向传播看看

现在可以直接跑一下:

Y, state_new = lstm_layer(X, state)

如果打印形状:

Y.shape, state_new[0].shape, state_new[1].shape

通常会得到:

(torch.Size([35, 2, 512]), torch.Size([2, 2, 256]), torch.Size([2, 2, 256]))

这三个形状一定要完全看懂。


9. 为什么Y.shape变成了(35, 2, 512)

原来单向 LSTM 的输出通常是:

(num_steps, batch_size, hidden_size)

也就是:

(35, 2, 256)

但双向后会变成:

(num_steps, batch_size, 2 * hidden_size)

也就是:

(35, 2, 512)

原因很简单:

每个时间步现在有两个方向的隐藏表示,通常会拼接在一起。

也就是说:

  • 前向给 256 维

  • 后向给 256 维

  • 拼起来就是 512 维

所以双向最直接的结果就是:

输出特征维度翻倍。


10. 为什么状态形状不是(35, 2, 512)

因为状态保存的是:

每个方向在最终时刻的内部状态

而不是每个时间步的全部输出。

所以状态仍然是:

(num_layers * num_directions, batch_size, hidden_size)

对于单层双向 LSTM 来说:

(1 * 2, batch_size, hidden_size) = (2, batch_size, hidden_size)

注意这里最后一维仍然是:

hidden_size

不会翻倍。

因为每个方向本身还是一个hidden_size维的状态,
只是方向数从 1 变成了 2。


11. 双向 GRU / RNN 也是同样规律吗

完全同样。

例如双向 GRU:

gru_layer = nn.GRU( input_size=vocab_size, hidden_size=num_hiddens, bidirectional=True )

那么:

  • 输出Y形状会变成(num_steps, batch_size, 2 * hidden_size)

  • 状态形状会变成(2, batch_size, hidden_size)

如果是双向普通 RNN,也是一样。

这说明:

双向机制和具体循环单元类型是解耦的。

它不是 LSTM 专属,也不是 GRU 专属,而是一种方向维扩展。


12. 如果再加上多层,会变成什么样

如果你同时使用:

  • 多层

  • 双向

那么状态第一维就会变成:

num_layers * num_directions

例如两层双向 LSTM:

lstm_layer = nn.LSTM( input_size=vocab_size, hidden_size=num_hiddens, num_layers=2, bidirectional=True )

这时状态形状会是:

(4, batch_size, hidden_size)

因为:

2 层 × 2 方向 = 4

这点非常重要,后面做编码器时经常会遇到。


13. 双向语言模型封装类哪里需要改

前面我们封装过这样的模型:

  • 循环层提特征

  • 线性层映射到词表空间

双向之后,最大改动通常只在线性层输入维度

例如原来单向时写:

self.linear = nn.Linear(self.num_hiddens, self.vocab_size)

双向后就要改成:

self.num_directions = 2 self.linear = nn.Linear(self.num_hiddens * self.num_directions, self.vocab_size)

为什么?

因为Y的最后一维已经变成了:

2 * hidden_size

如果线性层还是只接hidden_size,维度就对不上了。


14. 这通常是双向代码里最容易忘的一点

很多人第一次写双向循环网络时,往往只记得加:

bidirectional=True

但忘了改:

Linear 的输入维度

结果就会报 shape mismatch 错误。

所以双向网络代码里一定要形成条件反射:

双向一开,输出维度翻倍;输出维度翻倍,线性层输入也要翻倍。

这是这节代码最实用的一条经验。


15. 一个典型的双向模型封装可以怎么写

思路上通常类似这样:

class BiRNNModel(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 = 2 if self.rnn.bidirectional else 1 self.linear = nn.Linear(self.num_hiddens * self.num_directions, vocab_size)

这里最关键的一句就是:

self.num_directions = 2 if self.rnn.bidirectional else 1

它让模型可以自动适配单向或双向情况。

这样后面:

  • 单向时线性层输入是hidden_size

  • 双向时线性层输入是2 * hidden_size

都能统一处理。


16. 前向传播里还需要改很多吗

其实不需要。

前向传播主体还是:

  1. 输入索引转 one-hot

  2. 喂给循环层

  3. 把输出 reshape 成二维

  4. 送入线性层

例如:

def forward(self, inputs, state): X = nn.functional.one_hot(inputs.T.long(), self.vocab_size).type(torch.float32) Y, state = self.rnn(X, state) output = self.linear(Y.reshape((-1, Y.shape[-1]))) return output, state

你会发现,这段代码和单向版本几乎一样。

为什么还能直接用?

因为这里用了:

Y.shape[-1]

它会自动适配:

  • 单向时hidden_size

  • 双向时2 * hidden_size

所以只要线性层定义对了,前向传播主体甚至都不需要大改。


17.begin_state要怎么改

主要还是改状态第一维。

对于双向 RNN / GRU,通常是:

def begin_state(self, device, batch_size=1): return torch.zeros( (self.rnn.num_layers * self.num_directions, batch_size, self.num_hiddens), device=device )

对于双向 LSTM,则要返回:

def begin_state(self, device, batch_size=1): shape = (self.rnn.num_layers * self.num_directions, batch_size, self.num_hiddens) return (torch.zeros(shape, device=device), torch.zeros(shape, device=device))

也就是说,双向本质上就是把状态的第一维乘了个 2。


18. 双向循环网络能直接拿来做字符级语言模型吗

从“代码能不能跑”来说,当然能。
但从“任务是否合理”来说,通常不推荐。

原因前一篇已经讲过:

字符级语言模型属于自回归预测任务,它不应该看到未来字符。

如果你用双向结构做标准语言模型,
那么当前位置的表示已经包含了右侧未来信息,训练目标会不干净。

所以在课程里,双向循环网络更多是作为:

  • 结构理解

  • 编码器思路铺垫

  • 序列理解模型介绍

而不是为了说“语言模型就该用双向”。

这点一定要分清。


19. 双向代码和单向代码最本质的区别总结

如果一句话概括,就是:

输入不变,内部方向翻倍,输出维度翻倍,状态第一维乘 2,线性层输入也翻倍。

展开来说:

不变的

  • 输入组织方式

  • 前向传播主流程

  • 训练损失和优化流程

变化的

  • 循环层多了bidirectional=True

  • 输出最后一维从hidden_size变成2 * hidden_size

  • 状态第一维从num_layers变成num_layers * 2

  • 输出层输入维度也要翻倍

只要把这四点记清楚,双向循环网络代码就不容易乱。


20. 这一节最该掌握什么

如果从学习重点看,最关键的是下面几件事。

20.1 会用bidirectional=True

知道它的作用是增加方向维,而不是增加层数。

20.2 看懂输出维度翻倍

因为前向和后向隐藏状态会拼接。

20.3 看懂状态第一维乘上 2

这是最容易在代码里出错的地方。

20.4 知道线性层输入维度也要翻倍

这点非常实用。

20.5 分清“双向能做什么、不能做什么”

它适合理解类任务,不适合标准自回归预测。


21. 本节总结

这一节我们学习了双向循环神经网络的代码实现,核心内容可以总结为以下几点。

21.1 双向循环网络在框架中通常通过bidirectional=True开启

这是最直接的实现接口。

21.2 输入格式通常不变

仍然按时间步、batch、特征维组织。

21.3 输出最后一维会翻倍

因为前向和后向隐藏状态通常会拼接。

21.4 状态第一维会变成num_layers * 2

因为每层有两个方向的状态。

21.5 使用双向结构时,输出层输入维度也必须相应翻倍

这是代码里最容易忽视但最关键的一点。


22. 学习感悟

这一节很有意思,因为它让你真正感受到:

一个模型变强,不一定总是“更深”或者“更大”,也可能只是“看问题的角度更多了”。

单向循环网络只会从前往后看,
而双向循环网络多出来的,其实是一种“反向阅读能力”。

这在很多理解类任务中非常有价值。
因为现实里,我们理解一句话中的某个词,本来也常常会参考前后文,而不是只看左边。

从这个角度看,双向循环网络的增强,并不是数学技巧而已,
而是一种非常符合语言理解直觉的结构设计。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/14 14:34:13

金融、游戏、IoT应用怎么选安卓安全加固?行业定制化方案解析

同样是做安卓安全加固,金融App和游戏App的防护重点完全不同。给银行App套上游戏防外挂的方案,就像给装甲车装跑车引擎,既浪费又不对路。真正懂行的负责人,会要求服务商提供“懂我行业”的定制化方案。这篇文章,我们就从…

作者头像 李华
网站建设 2026/4/14 14:32:31

Steam成就管理终极方案:3分钟掌握SAM工具的核心用法

Steam成就管理终极方案:3分钟掌握SAM工具的核心用法 【免费下载链接】SteamAchievementManager A manager for game achievements in Steam. 项目地址: https://gitcode.com/gh_mirrors/st/SteamAchievementManager 你是否曾经因为游戏bug导致成就无法解锁而…

作者头像 李华
网站建设 2026/4/14 14:31:33

FreeRTOS 移植完整笔记

一、前置准备:FreeRTOS 源码获取与结构 1. 下载源码 官网:https://www.freertos.org/ GitHub:https://github.com/FreeRTOS/FreeRTOS/releasesFreeRTOS/Source/ # 内核核心(必须用)include/ # 通用头文件7个.c文件 # t…

作者头像 李华
网站建设 2026/4/14 14:31:31

故障记录:Windows 资源管理器点击文件卡死 (WPS 触发)

现象描述症状:进入文件夹正常,但只要点击/选中文件夹中的文件,Windows 资源管理器 (explorer.exe) 立即卡死。资源状态:非硬件瓶颈。内存占用 72%,CPU占用 22%,固态硬盘占用 1%。其他应用程序(如…

作者头像 李华
网站建设 2026/4/14 14:26:44

LingBot-Depth在安防场景实战:单目摄像头实现精准3D行为分析

LingBot-Depth在安防场景实战:单目摄像头实现精准3D行为分析 1. 安防监控的维度升级:从2D到3D的跨越 传统安防监控系统长期受限于二维图像分析的固有缺陷。当我们需要判断一个人是否越过了虚拟警戒线时,实际上是在处理一个三维空间问题&…

作者头像 李华
网站建设 2026/4/14 14:25:43

FLUX.1-dev-fp8-dit文生图+SDXL_Prompt风格应用:智能硬件产品宣传图生成

FLUX.1-dev-fp8-dit文生图SDXL_Prompt风格应用:智能硬件产品宣传图生成 你是不是也遇到过这样的烦恼?公司新出了一款智能硬件,产品经理催着要宣传图,设计团队排期排到下个月,自己用PS捣鼓半天,出来的效果总…

作者头像 李华