ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PyTorch与Paddle中Dropout的eval模式陷阱:nn.Dropout与F.dropout差异解析

PyTorch与Paddle中Dropout的eval模式陷阱:nn.Dropout与F.dropout差异解析 做深度学习的朋友一定都遇到过这个迷惑场面训练完一个模型推理前也喊了model.eval()结果每次跑出的结果还是不一样或者训练集上 loss 死活降不下去。这些破事十有八九是 Dropout 的“第二人格”没管好。torch.nn.Dropout和torch.nn.functional.dropout名字就差一个点行为却可能在 eval 模式下南辕北辙。而且这套坑不光 PyTorch 有Paddle 那边长得一模一样换汤不换药。今天我就把这两种 API 的差异、model.eval()的作用机制、还有几类我实测过的诡异现象全掰开揉碎讲一遍顺便附上 Paddle 下的对应写法希望你能一次避坑。1. Dropout 的前世今生模块版与函数版到底差在哪1.1 为什么同一个功能要提供两种 API在 PyTorch 里你想给网络加一招“随机失活”官方给了两条路torch.nn.Dropout下面简称nn.Dropout和torch.nn.functional.dropout下面简称F.dropout。很多新手不理解明明是同一个功能为什么官方要搞两个接口这不是徒增学习成本吗其实这是 PyTorch 的一贯设计哲学高度封装的“模块”与轻量灵活的“函数”并存。模块适合用来搭模型因为它会把自身状态比如失活概率 p、是否原地操作 inplace全都封装进去还能自动响应model.train()/model.eval()的切换不需要调用者反复传参。函数则适合做临时性的、一次性的操作比如在自定义损失函数里偶尔加一个随机失活没必要为了它特地建模块。用生活里的例子理解nn.Dropout像一个带遥控器的自动感应灯你只需要设定一次“亮度范围”之后它自己会根据“现在是白天还是晚上”决定亮不亮F.dropout则像一盏手动开关的灯泡你必须亲手决定“现在该不该亮”如果忘了关那半夜它照样亮瞎你眼睛。问题也就出在这。自动感应灯固然省心但手动的灯泡一旦用错场合坑起来非常隐蔽——尤其在 eval 模式下。1.2 nn.Dropout 的封装逻辑与自我管理nn.Dropout实现原理并不复杂它是一个nn.Module子类核心的 forward 逻辑大致可以理解为class Dropout(nn.Module): def __init__(self, p0.5, inplaceFalse): super().__init__() self.p p self.inplace inplace def forward(self, input): if self.training: return 带随机失活的处理(input) else: return input # 推理时直接返回原样注意看最后一行当self.training为 False 时模块版 Dropout 直接当恒等映射处理什么都不做。那么self.training怎么变正是通过model.eval()和model.train()全局切换的。这种封装带来的好处很明显只要你在模型定义里写了self.dropout nn.Dropout(0.5)然后在推理前调用一次model.eval()所有 Dropout 层都会自动“哑火”你完全不用去记哪里用了 Dropout、哪里没有。这也是我推荐大家在搭建模型时优先使用nn.Dropout的原因少一个需要操心的细节就少一分出错的机会。1.3 functional.dropout 的直白与隐雷再来看F.dropout。它的签名在 PyTorch 中是这样torch.nn.functional.dropout(input, p0.5, trainingTrue, inplaceFalse)你仔细看training这个参数默认值死死地写在True上。这意味着什么意味着你写F.dropout(x, p0.5)的时候不管你在训练还是推理它都默认执行随机失活除非你手动改成trainingFalse或传入self.training。我自己刚用 PyTorch 那会儿就因为这个默认值吃过亏。有次我图方便在一个模块的 forward 里写了def forward(self, x): x F.dropout(x, p0.5) ...模型训练时倒是正常可后来要推理我发现输出结果每次都不一样像见了鬼。排查了半天最后发现就是这行代码搞的鬼。它使用默认的trainingTrue所以即使模型已经eval()了它照样以 50% 的概率把神经元的输出随机清零。正确写法是把它跟当前模型的状态绑定起来def forward(self, x): x F.dropout(x, p0.5, trainingself.training) ...或者干脆在推理时直接传F.dropout(x, p0.5, trainingFalse)。总之别让 F.dropout 的默认参数替你决定当前是训练还是推理。2. model.eval() 到底做了什么以及 Dropout 的生死开关2.1 eval() 切换的是训练与推理的“上下文”要理解上面这些差异就得先搞清楚model.eval()的本质。它并不是一层魔法它的作用其实很朴素递归地把模型里所有nn.Module的training属性都置为 False。同理model.train()就是把training属性都置为 True。在 PyTorch 中nn.Module.eval()的注释里写着这个操作对 Dropout 和 BatchNorm 这类“在训练与推理中行为不同”的模块有影响。BatchNorm 在训练时用 batch 的均值方差做归一化并滑动更新全局统计量在 eval 时则用全局统计量来归一化。Dropout 则简单粗暴训练时随机失活eval 时直接放行。所以model.eval()相当于一个总开关把模型从“训练模式”拧到“推理模式”。但注意这个开关只能遥控那些“接入了遥控系统”的模块——也就是基于nn.Module的模块。对于F.dropout这种裸函数它自身并不存在于模块树里也没有 self.training 一说它只会傻傻地听你传给它的training参数。如果你不给它传那它永远按默认值True来执行完全无视外面的model.eval()。这就是“模块”和“函数”在模式联动上的本质差异。2.2 两种 API 在 eval 下的四种组合为了让大家一眼看懂我整理了一张行为对照表包含了nn.Dropout和F.dropout在不同写法、不同模式下的效果使用方式调用处状态评估/推理时行为是否正常nn.Dropout(p)model.eval()不做任何操作原样输出正常nn.Dropout(p)model.train()按概率 p 随机失活正常F.dropout(x, p)model.eval()仍按概率 p 随机失活默认 trainingTrue异常F.dropout(x, p, trainingself.training)model.eval()不做任何操作原样输出正常这张表请大家务必存一下因为第三行就是最常见翻车现场。很多人写完F.dropout就忘了管最后一个参数结果测试集和部署时被随机失活坑得一头包。2.3 手写一个最小实验验证行为不亲手验证一下总感觉不踏实。我用一个最简单的例子测试过效果非常直观import torch import torch.nn as nn import torch.nn.functional as F class MyNet(nn.Module): def __init__(self): super().__init__() self.dropout nn.Dropout(0.5) # 模块版 def forward(self, x, use_functionFalse, use_training_flagFalse): x self.dropout(x) if use_function: if use_training_flag: x F.dropout(x, p0.5, trainingself.training) else: x F.dropout(x, p0.5) # 默认 trainingTrue return x net MyNet() x torch.ones(1, 10) net.eval() print(模块版输出:, net(x)) print(函数版默认输出:, net(x, use_functionTrue)) print(函数版绑定模式输出:, net(x, use_functionTrue, use_training_flagTrue))我跑了一次结果类似这样模块版输出: tensor([[1., 1., 1., ..., 1.]]) 函数版默认输出: tensor([[0., 2., 0., ..., 2.]]) 函数版绑定模式输出: tensor([[1., 1., 1., ..., 1.]])第一个和第三个都是全 1说明 eval 模式下 Dropout 被正确关闭了第二个因为F.dropout用了默认的trainingTrue即使model.eval()也照样把部分 1 置成了 0并且按1/(1-p)2放大了其他值。如果这种随机失活出现在你的推理链路里每次结果都不一样那基本可以断定是这一处漏传参数导致的。3. 迁移到 Paddlepaddle.nn.Dropout 与 functional.dropout 也一样3.1 Paddle 的对应 API 和相似设计PyTorch 的这套 Dropout 设计在国产框架 PaddlePaddle 里几乎原样复刻了一版。Paddle 对应提供了paddle.nn.Dropout和paddle.nn.functional.dropout两者在设计思路上和 PyTorch 完全一致模块版自动感知model.eval()/model.train()函数版需要手动传training参数。所以在 PyTorch 踩过的坑换到 Paddle 里一个都躲不掉。我个人体验是Paddle 官方文档对paddle.nn.functional.dropout的说明里同样标注了trainingTrue这个默认值。你要是把它直接写进网络里跑推理一样会遇到随机输出。3.2 实际代码对比Paddle 下的正确写法我们直接对比一下两种框架的正确和错误写法你会发现惊人相似# PyTorch 正确写法 class Net(nn.Module): def __init__(self): super().__init__() self.drop nn.Dropout(0.5) def forward(self, x): return self.drop(x) # Paddle 正确写法 import paddle import paddle.nn as nn class Net(nn.Layer): def __init__(self): super().__init__() self.drop nn.Dropout(0.5) def forward(self, x): return self.drop(x)两者都用了模块版所以只要在推理前调用对应框架的eval()Paddle 里是net.eval()Dropout 就会被关闭。再看不推荐的函数版写法# 不推荐PyTorch 中 F.dropout 默认 trainingTrue x F.dropout(x, p0.5) # 不推荐Paddle 中 paddle.nn.functional.dropout 默认 trainingTrue x paddle.nn.functional.dropout(x, p0.5)这两种方法在 eval 模式下都会继续随机失活。正确做法同样是绑定当前模块的模式标志# PyTorch x F.dropout(x, p0.5, trainingself.training) # Paddle x paddle.nn.functional.dropout(x, p0.5, trainingself.training)Paddle 的模块类基于nn.Layer但self.training的语义和 PyTorch 保持一致所以这套写法在两边都能跑通。3.3 一个小差异Paddle 的 dropout 实现细节虽然整体设计如出一辙但 Paddle 在 Dropout 的实现细节上有一个值得一提的差异它默认使用的是modeupscale_in_train翻译过来就是“训练时放大、推断时不缩放”。具体来说Paddle 的 Dropout 是在训练时随机置零并且会把保留下来的元素乘以1 / (1 - p)这样可以让输出在训练时保持期望不变在 eval 时直接原样输出。PyTorch 的nn.Dropout也是类似策略。所以从数学行为上看两者是可以平替的。但如果你习惯用paddle.nn.functional.dropout一定要留意这个mode参数。有些老代码可能用了modedownscale_in_infer那是另一种旧策略推理时会走“对输出做缩放”的分支。这种边角差异在跨框架迁移模型时最容易踩中我的建议是用模块版paddle.nn.Dropout并保持默认 mode别给自己找不痛快。4. 实战踩坑这些诡异现象都是 dropout 惹的祸4.1 场景一明明 eval 了推理结果仍然随机这个场景我见过不止一次甚至在一些开源项目的 issue 里也反复出现。现象就是模型训练完保存、加载、eval()一条龙全做了但推理结果每次都不一样。排查步骤很简单先看代码里有没有出现F.dropout或者paddle.nn.functional.dropout再看这些函数有没有把training参数绑定到self.training上最后看是不是有某些地方忘了调用eval()比如只对部分子网络调用或者调用了之后再加载权重导致状态被重置。这里有个小技巧你可以在eval()之后打印任意一个 Dropout 模块的training属性确认状态# PyTorch print(model.dropout.training) # 期望 False # Paddle print(model.drop.training) # 期望 False如果输出是True那说明 eval 调用不到位。如果模块版是正确的那就要考虑是不是自己在 forward 里混用了函数版 Dropout。4.2 场景二训练 loss 不下降反而波动大另一种常见的坑是训练阶段就出问题。有些朋友喜欢在 forward 里用F.dropout(x, p0.5)省事结果这个 Dropout 完全不受model.train()/model.eval()控制导致 eval 的时候它还在随机失活于是评估集上表现忽高忽低看起来像训练没收敛或者模型泛化特别差。还有一种情况是在某些不该加 Dropout 的地方加了太高的失活概率。比如在 LSTM/Transformer 的 hidden state 上p0.5可能会导致梯度信号被过分冲淡loss 迟迟降不下去。这时候你可以把 p 调小到 0.1 ~ 0.3 试试或者换成nn.Dropout模块至少能确保 eval 时行为正确。我的习惯是模型定义里的 Dropout 一律用模块版函数版只用来处理临时性的单独调用。这样训练和推理之间不会出现诡异的“状态割裂”。4.3 场景三模型导出——inference.json 与 nb 的困惑最近热搜词里有个挺有意思的问题“paddle 训练的模型怎么是 inference.json 转为 nb”。估计很多人都被 Paddle 的模型导出搞懵过。我在这里说下我理解的来龙去脉。Paddle 官方现在推荐使用paddle.jit.save将动态图模型导出为推理模型导出后通常会得到.pdmodel结构文件和.pdiparams参数文件。但在一些上层套件比如 PaddleX、PaddleDetection 等中导出过程除了生成模型结构文件还会额外生成inference.yml或inference.json这类配置文件里面记录的是输入输出名、均值方差预处理参数、标签列表等内容。而问题里提到的“nb”我猜有两种可能一种是某些可视化工具如 Netron 导入模型后可以保存为 notebook 形式的实验记录的扩展名另一种是部署环境中自定义的模型打包格式类似 aistudio 里的.nb模型包。不管哪种核心思路都是用真正的模型文件去转换而不是拿着 json 文件去转。json 只是配置不是可训练、可推理的模型权重它本身无法单独转为可部署的模型包。如果你想把 Paddle 训练好的模型转成一个部署可用的“nb 包”或其他格式建议按这个流程走在训练代码中通过paddle.jit.to_static将动态图模型转为静态图程序使用paddle.jit.save(model, path)将模型保存为model.pdmodel和model.pdiparams检查同目录下是否生成了inference.yml或inference.json这些文件保留给部署工具读取根据你的部署目标选择使用 Paddle Inference 直接加载或继续转换为 ONNX、TensorRT 等格式如果目标平台要求特定的模型包扩展名如 nb通常需要在对应部署框架的转换工具里导入之前的.pdmodel与.pdiparams再执行打包命令而不是拿inference.json去转。说得再直白一点inference.json是“说明书”模型文件才是“产品”。你照着说明书去做产品转换但别指望把说明书本身变成产品。4.4 排查 Dropout 问题的检查清单最后整理一份我把这么多年踩坑经验浓缩成的排查清单遇到诡异问题可以照着过一遍检查点操作方法达标标准模块版 Dropout模型定义中使用nn.Dropout或paddle.nn.Dropouteval 后training为 FalseDropout 不生效函数版 Dropout检查是否绑定trainingself.training或手动传trainingFalseeval 后没有随机失活eval()调用位置在模型加载权重之后、推理之前调用model.eval()/net.eval()所有子模块的training都为 False模型导出文件确保导出的是.pdmodel.pdiparams而非只有 json 配置部署工具能成功构建推理引擎随机种子设置torch.manual_seed/paddle.seed并固定推理种子相同输入多次推理结果一致关于随机种子的问题也有不少人忽略。就算你正确关了 Dropout如果网络里还有其他随机采样操作比如可变形卷积、随机 padding 等推理结果依然可能有微小波动。设置好推理阶段的随机种子可以进一步保证结果可复现。最后聊点我的实操体会Dropout 这类“训练专用”模块看起来简单真正玩明白还是得靠踩坑积累。我在 PyTorch 和 Paddle 两边都折过腰以后现在写代码形成了一套习惯模型层级的 Dropout 一律用nn.Dropout/paddle.nn.Dropout自定义损失或者辅助网络里的临时失活用函数版但一定显式传入trainingself.training。每次推理前我一定会打印一个 Dropout 层的training属性做状态确认哪怕多花两秒也值。另外模型导出这块千万别把 json 配置和真正模型文件搞混。以前我也干过把inference.json当成模型到处打听怎么转格式的蠢事后来用paddle.jit.save一次性把.pdmodel和.pdiparams导出后部署工具一读就通根本没有想象中复杂。如果你现在也被“eval 模式但结果随机”折磨先别急着调包调参回去翻翻代码里所有 Dropout 的长相。这年头吧能在model.eval()面前依旧我行我素进行随机失活的十有八九是那个没传training参数的函数版 Dropout。
返回列表