Dropout:均值不变,为何评估输出仍不同

黎 浩然/ 20 10 月, 2026/ 大语言模型/LARGELANGUAGEMODEL/LLM, 教程/TUTORIAL, 计算机/COMPUTER/ 0 comments

Dropout在训练时保留均值,不代表一次训练前向等于评估前向,也不代表整张网络的平均输出等于关闭Dropout后的输出。最容易混淆的地方,是把“某一层的期望相同”扩大成“后续任意计算都相同”。一个只有两个输入的例子,就能把这两件事分开。

文章目录
  1. 缩放究竟补偿了什么
  2. 四种掩码,没有一种等于评估输出
  3. 经过ReLU,均值为什么又不同了
  4. 用有理数检查,避免把误差当机制
  5. 模块切到eval,函数调用仍要检查参数
  6. 把验收条件写成预期行为
  7. 来源与补发说明

这里针对搜索问题“Dropout为什么除以1−p,eval之后还要缩放吗”,先推导缩放,再给出非线性反例和可复现检查。Read in English。

缩放究竟补偿了什么

PyTorch 2.8的Dropout定义在训练时以概率p置零元素,并将保留下来的值除以1−p;评估时该模块为恒等映射。以下推导限定0≤p<1,输入x视为固定值,掩码m以概率q=1−p取1,否则取0。

写成y=mx/q,就有E[y]=E[m]x/q=qx/q=x。这里的期望仅针对掩码随机性。补偿的是对随机掩码取平均后的单元素输出。不是把丢失的某个具体激活重建回来,也不是保证每批恰好保留q比例的元素。特别是小张量,实际保留数可能与平均值差得很远。

由E[m²]=q可得E[y²]=x²/q,所以Var(y)=x²/q−x²=p x²/(1−p)。均值不变,波动却随p增大。p接近1时,稀少的保留值会被放大;只说“均值不变”会掩盖这项变化。p=1不能直接代入这个分式,本例不讨论该边界的具体实现。

固定输入x
以概率q保留:x/q
以概率p置零:0
均值x;方差p x²/q
原创概率分流图:均值补偿没有消除输出波动。

四种掩码,没有一种等于评估输出

固定x=[2,4],p=0.5,逐元素独立取掩码。四种等概率结果分别是[0,0]、[0,8]、[4,0]、[4,8],概率各为1/4。它们的逐元素平均恰为[2,4],但没有一个结果等于[2,4]。这说明“输出保持期望”与“某次输出保持原值”完全可以同时一真一假。

不要用一次训练输出与一次评估输出逐元素相等,作为Dropout正确性的验收条件。也不要固定随机种子后观察到相同掩码,就断言随机层已经关闭。固定随机过程和移除随机过程,回答的是两个问题;前者有助于复现一次实验,后者改变前向函数。

这个例子枚举全部掩码,所以平均值是精确期望,不是有限随机采样碰巧接近的均值。扩大到n个独立二元掩码会有2ⁿ种组合,因此枚举只适合小规模机制核验;真实大张量应根据测试目的设计统计检验,不把本例当作高效训练实现。

经过ReLU,均值为什么又不同了

只看第一个坐标,训练Dropout后有一半概率为0,一半概率为4。接一个简单头部h(z)=max(0,z−3):两种结果变成0与1,平均为0.5。评估时该坐标固定为2,头部输出max(0,2−3)=0。

因此E[h(y)]=0.5,而h(E[y])=0。差异来自非线性阈值,不需要浮点误差、梯度或者复杂网络。这也解释了为什么训练损失和评估损失不应被要求逐批一致:即使上游某层满足均值相等,损失函数与中间非线性也可能破坏这种等价。

对固定仿射函数h(z)=az+b,期望可以穿过运算,E[h(y)]=h(E[y]);这提供了一个有用的控制组。比较时先明确下游是什么函数,再决定能否搬动期望符号。不能因为最后一层是线性的,就忽略Dropout与最后一层之间的非线性路径。

训练:0或4
阈值头max(0,z−3):0或1
平均0.5;评估头输出0
原创非线性反例:先平均与先经过阈值头得到不同结果。

用有理数检查,避免把误差当机制

以下标准库代码枚举两个坐标的四种独立掩码,使用Fraction做精确算术。八项断言检查期望、完整输出集合、单次不等、方差、非线性头部两条路径及坐标协方差;没有执行PyTorch模型,也没有测量硬件性能。

# Exact finite-mask arithmetic, not a PyTorch runtime test.
from fractions import Fraction as F
from itertools import product

x = (F(2), F(4))
p = F(1, 2)
q = 1 - p

def output(mask):
    return tuple(v * m / q for v, m in zip(x, mask))

ys = [output(m) for m in product((0, 1), repeat=2)]
means = tuple(sum(y[i] for y in ys) / len(ys) for i in range(2))
assert means == x
assert ys == [(0, 0), (0, 8), (4, 0), (4, 8)]
assert all(y != x for y in ys)
var0 = sum((y[0] - x[0]) ** 2 for y in ys) / len(ys)
assert var0 == p / q * x[0] ** 2 == 4
relu = lambda t: max(F(0), t)
random_head = sum(relu(y[0] - 3) for y in ys) / len(ys)
fixed_head = relu(x[0] - 3)
assert random_head == F(1, 2)
assert fixed_head == 0
assert random_head != fixed_head
cov = sum((y[0]-2)*(y[1]-4) for y in ys) / len(ys)
assert cov == 0
print('8 exact dropout arithmetic checks passed')

零协方差依赖这里明确设定的独立掩码,不应推广到共享通道掩码的其他Dropout变体。整个计算还把输入当作固定量;若输入本身随机,分析需先对给定输入求条件期望,再处理输入分布。把前提写清,比反复增加随机采样次数更能防止误读。

这些检查分开了两种失败:保留值缩放写错,会在头部之前就改变期望;错误要求非线性头部两条路径相等,则会在实现正确时仍然失败。小规模精确算例能区分实现偏差与不成立的测试要求,而不是把所有数值差异都归因于框架。

模块切到eval,函数调用仍要检查参数

Module.eval定义等价于train(False),会切换受训练模式影响的模块行为。对普通nn.Dropout,这条模块路径适用;不要在评估后额外再乘一次1−p,否则会人为缩小输出。

但functional.dropout有独立的training参数,默认True。若forward里调用它时未传training=self.training,父模块eval并不会自动改写那个函数实参。检查代码时,应沿实际调用路径找开关,不能只在入口看到了eval就结束排查。

还有一类不同需求是主动保留随机前向、重复采样。此时要明确哪些层允许随机、哪些层必须保持评估状态;直接把整个网络切回训练模式,还可能改变BatchNorm等层的行为。此前的BatchNorm统计来源排查解释了为什么这些状态不能混为一谈。这里不声称多次随机输出天然就是经过校准的不确定性估计。

把验收条件写成预期行为

若目标是固定评估行为,记录模型模式和函数式调用参数,在相同输入、状态与预处理下重复前向,再定位变化来源。输出稳定只能支持这个受控实验,不能证明所有设备、算子与输入都确定;还需要分别排查其他随机或非确定因素。

若目标是验证缩放机制,就使用小输入、已知掩码与解析期望,像上面的例子一样把层级边界限制住。不要要求非线性网络满足本来就不成立的期望等价。这样失败的断言才指向实现问题,而不是错误的测试假设。

将算术例子接入项目时,分开记录测试要核验的数学关系,以及实际执行的运行时配置。本程序只用精确数值验证前者;框架集成测试还应记录模块或函数调用、模式、版本和观测输出。这些观测应在真实运行后填写,不能由教学模型代替。

如果这组推导与反例帮你写出了更准确的Dropout测试,欢迎通过文末支持区自愿支持。可复用的成果是区分单层期望、单次输出与整网行为的检查方法。

来源与补发说明

Dropout · functional.dropout · Module.eval

依据原计划时段前已有的PyTorch 2.8版本定义核对;原创推导和图示。本次补发保留2026年10月20日09:00北京时间文章日期,实际公开时间另行核验。执行了标准库算术检查,没有执行PyTorch集成测试。

支持

如果文中的解释或示例帮你解决了问题、节省了时间,欢迎自愿打赏支持本站。

微信支持二维码,点击查看大图
微信
支付宝支持二维码,点击查看大图
支付宝
Buy Me a Coffee,点击支持本站
Buy Me a Coffee

二维码可点击放大。更多支持方式见支持页面。

Share this Post

Leave a Comment

您的邮箱地址不会被公开。 必填项已用 * 标注

*
*