开始前,会这些就够了
- 会按概率加权求平均
- 知道
ReLU把负数变成零 - 能运行Python标准库脚本
Dropout随机把一些值变成零,同时把留下来的值放大,使每个位置在反复随机选择后的平均值保持不变。很容易接着推断:既然平均值没变,关闭Dropout时的网络输出应该等于训练时的平均输出。这个推断跨过了后面的计算。
我们不用训练大模型,也不用随机跑几万次。只给两个数各放一个独立的保留开关,穷举四种组合,再让结果经过求和、ReLU和平方损失。你会看到平均在哪一步保持、在哪一步改变,以及输出均值相同后损失为何仍可能不同。
把例子带到自己的电脑上
下载完整练习包
需要 Python 3.10 或更新版本,无第三方依赖;制作时实测 Python 3.14.0。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- dropout_expectation.py:完整示例
- README.md:环境、范围与独立练习
- observed.stdout.txt:制作时的实际输出
- verification.json:源码与产物验证记录
- SHA256SUMS.txt:文件内容摘要
在这个文件夹打开 PowerShell,逐行运行
python --version
python -B dropout_expectation.py解压到独立练习文件夹,在含源文件的目录打开终端运行。程序会在当前目录写入教学报告;先预测结果,再查看当次输出。两维固定输入与独立掩码的精确穷举,区分线性期望、ReLU 输出与平方损失;不运行框架、不训练,也不预测泛化效果。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
先把丢弃概率和保留概率写成不同符号
本文用p表示丢弃概率,q=1−p表示保留概率。每个位置各抽一个独立开关m:保留时为一,丢弃时为零。训练模式把输入x变成m×x/q;评估模式直接保留x。这种训练时放大、评估时不缩放的约定通常叫inverted dropout。
只要0≤p<1,开关的平均值为q,所以m×x/q的平均值等于x。这里说的是对随机掩码分布求平均,不是每次前向都等于x。p越大,留下来的值放得越大,某次输出仍可能与原值差很多。
脚本刻意排除p=1,避免除以零;PyTorch层本身接受p=1,有专门实现边界,不能把我们的练习限制写成框架不支持。所有输入和概率都用分数表示,没有训练、参数更新或随机种子问题。
两个数抵消时,四种开关并不对称地通过ReLU
主例输入x=[1,−1],丢弃概率p=1/2,保留时放大两倍。两个开关彼此独立,所以00、01、10、11四种掩码各占1/4。对应缩放输入是[0,0]、[0,−2]、[2,0]、[2,−2],求和依次为0、−2、2、0。
缩放后第一位置的平均值仍是一,第二位置仍是负一;求和后的平均值仍为零。接着过ReLU,四个输出变成0、0、2、0,平均输出便是1/2。评估模式没有掩码,输入和为零,ReLU输出也是零。两种输出平均不相等。
负的−2被ReLU改成零,正的2被保留,所以原先的正负抵消被打破。这是一个确定的分布计算结果,不是采样次数太少造成误差。即使无限次随机取掩码,理论平均仍是1/2,而不是评估模式的零。
线性运算能保留这个平均,非线性一般不能
固定权重的加权和可以把平均移进每一项:先对随机输入求平均再做线性层,与先做线性层再平均相同。固定偏置也不改变这个结论。因此本例在求和之后仍保住均值,不只是两项数碰巧抵消。
ReLU、阈值判断、softmax等非线性运算一般没有这种交换关系。写成E[f(Z)]与f(E[Z]),左边是先对每个随机结果计算再平均,右边是先把随机结果平均再计算。括号位置描述了不同流程,不能因为都含“平均”就直接约去。
我们也保留一个相等的对照:x=[1,1]、p=1/2时,四个ReLU输出是0、2、2、4,平均为二,评估输出也为二。因为这几种输入的和都不小于零,ReLU在此范围内相当于原样通过。所以正确结论是“通常不能保证相等”,而不是“凡有非线性必定不同”。
线性期望的保持本身不要求两个开关彼此独立,只需每个位置的保留概率与缩放相配。独立性在本实验中用来确定四种联合掩码的概率。如果两个位置共用同一个开关,组合分布就会改变,经过ReLU后的平均也可能改变;这时不能继续沿用四种组合各占四分之一的表。
输出平均相同,也不能直接替代平均损失
给主例指定目标值零,使用平方损失。训练模式四种输出的损失为0、0、4、0,平均损失为一。若先平均输出得到1/2,再计算平方损失,只得到1/4;评估输出零的损失则为零。这是三个不同对象,账本为它们分别命名。
平方损失有一个直接核对关系:E[(Y−t)²]=(E[Y]−t)²+Var(Y)。主例输出方差为3/4,因此1/4+3/4=1。先把输出压成一个平均数,会抹掉它围绕平均值波动的部分,这恰好是遗漏的方差项。
再看正输入对照,训练与评估输出平均同为二,但目标零下的训练平均损失为六,评估损失为四,差额来自输出方差二。主例若把目标改成一,训练平均损失与评估损失又恰好都为一。因此不能由某一次损失相等,反推两种输出分布相同。
换丢弃概率时,四种掩码不能再等权平均
脚本另外比较主例p为0、1/4和3/4的情况,连同p=1/2与正输入对照,共五组。每组始终列出四种掩码,但它们的概率由两个独立开关相乘得到。例如p=1/4时,00占1/16,01与10各占3/16,11占9/16。
因此看四行CSV时,不能简单把四个输出相加除以四。只有p=1/2时四种组合才等概率。p=0时只有11的概率为一,其余三行概率为零;这些零概率行仍留在表里,方便逐行检查,但不会影响期望。
掩码表把“事件发生的机会”和“发生后的数值”放在不同列。例如保留概率变小时,保留正项这一事件可能更少见,但一旦发生,除以q后的值也更大。不能只观察输出列的峰值判断均值变大多少,必须把它与同一行的概率相乘后再汇总。
对x=[1,−1],这套约定下平均ReLU输出恰好等于p:只有保留正项、丢弃负项时输出为1/q,该掩码概率为qp,相乘得到p。这个等式依赖特定两维输入与ReLU结构,不能当成任意网络的Dropout效果公式。
运行四种掩码的完整账本
脚本生成dropout-inputs.json、dropout-report.json和dropout-masks.csv。CSV共二十行,逐行保存概率、两个缩放输入、求和、ReLU输出以及两个目标值的平方损失;报告按五组输入汇总十个损失对照。每一步都能由分数重算。
验证同时检查掩码概率和为一、各输入位置的期望保持,以及平方损失等于均值处损失加方差。程序没有调用PyTorch,报告里的training和eval只标记采用哪套前向公式;不表示真正进行过优化训练、评估数据集或测量泛化表现。
如果把穷举改为随机抽样,有限次数得到的平均只是估计,通常需要报告波动。本实验只有四种可能,可以直接算完,因此没有置信区间或随机采样误差,也无需寻找一个让结果看起来更好的种子。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
"""Enumerate independent Bernoulli masks; all probability and loss math is exact."""
import csv
import json
from fractions import Fraction as F
from itertools import product
from pathlib import Path
ROOT = Path(__file__).resolve().parent
CASES = [
{"id": "cancel_p0", "x": ["1", "-1"], "drop_p": "0"},
{"id": "cancel_p25", "x": ["1", "-1"], "drop_p": "1/4"},
{"id": "cancel_p50", "x": ["1", "-1"], "drop_p": "1/2"},
{"id": "cancel_p75", "x": ["1", "-1"], "drop_p": "3/4"},
{"id": "positive_p50", "x": ["1", "1"], "drop_p": "1/2"},
]
def relu(value):
return max(F(0), value)
def main():
rows, reports = [], []
for case in CASES:
x = list(map(F, case["x"]))
p = F(case["drop_p"])
assert len(x) == 2 and 0 <= p < 1 # Toy excludes p=1 because it divides by 1-p.
q = 1 - p
masks = []
for mask in product((0, 1), repeat=2):
probability = F(1)
for bit in mask:
probability *= q if bit else p
dropped = [value * bit / q for value, bit in zip(x, mask)]
preactivation = sum(dropped, F(0))
output = relu(preactivation)
masks.append((probability, dropped, preactivation, output))
rows.append({"case": case["id"], "drop_p": str(p), "keep_q": str(q),
"mask": "".join(map(str, mask)), "probability": str(probability),
"dropped_0": str(dropped[0]), "dropped_1": str(dropped[1]),
"preactivation": str(preactivation), "relu_output": str(output),
"squared_loss_target0": str(output ** 2),
"squared_loss_target1": str((output - 1) ** 2)})
assert sum(probability for probability, *_ in masks) == 1
mean_dropped = [sum(prob * values[i] for prob, values, _, _ in masks) for i in range(2)]
mean_pre = sum(prob * pre for prob, _, pre, _ in masks)
mean_output = sum(prob * out for prob, _, _, out in masks)
second_moment = sum(prob * out ** 2 for prob, _, _, out in masks)
variance = second_moment - mean_output ** 2
eval_pre, eval_output = sum(x, F(0)), relu(sum(x, F(0)))
assert mean_dropped == x and mean_pre == eval_pre
losses = []
for target in (F(0), F(1)):
mean_loss = sum(prob * (out - target) ** 2 for prob, _, _, out in masks)
loss_at_mean = (mean_output - target) ** 2
assert mean_loss == loss_at_mean + variance
losses.append({"target": str(target), "mean_training_loss": str(mean_loss),
"loss_of_mean_training_output": str(loss_at_mean),
"eval_loss": str((eval_output - target) ** 2)})
reports.append({"case": case["id"], "drop_p": str(p), "keep_q": str(q),
"mean_dropped": list(map(str, mean_dropped)), "mean_preactivation": str(mean_pre),
"mean_training_output": str(mean_output), "eval_output": str(eval_output),
"output_variance": str(variance), "losses": losses})
assert reports[2]["mean_training_output"] == "1/2"
assert reports[2]["eval_output"] == "0"
assert reports[2]["losses"][0]["mean_training_loss"] == "1"
assert reports[4]["mean_training_output"] == reports[4]["eval_output"] == "2"
assert reports[4]["losses"][0]["mean_training_loss"] == "6"
inputs = {"mask_distribution": "two independent Bernoulli keep(q), q=1-drop_p",
"training_transform": "x_i * mask_i / q", "evaluation_transform": "identity",
"network": "ReLU(sum(transformed_x)); no parameters change", "cases": CASES}
report = {"scope": "Exact enumeration of four masks per case; no random sampling or training",
"toy_p_range": "0 <= drop_p < 1; does not cover framework p=1 boundary",
"cases": reports, "mask_rows": len(rows), "loss_summaries": 2 * len(reports)}
(ROOT / "dropout-inputs.json").write_text(json.dumps(inputs, indent=2) + "\n", encoding="utf-8")
(ROOT / "dropout-report.json").write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
with (ROOT / "dropout-masks.csv").open("w", encoding="utf-8", newline="") as handle:
writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
writer.writeheader()
writer.writerows(rows)
print(f"PASS dropout_expectation: {len(CASES)} cases, {len(rows)} mask rows, {report['loss_summaries']} loss summaries")
if __name__ == "__main__":
main()
怎样运行:Python 3.10+标准库;保存为dropout_expectation.py,在空练习目录运行 python dropout_expectation.py。不要加-O,验收使用assert。 2026-10-15 已用 Python 3.14.0 执行这份代码并通过自带检查;下方是当次输出。两维固定输入与独立掩码的精确穷举,区分线性期望、ReLU 输出与平方损失;不运行框架、不训练,也不预测泛化效果。
运行后对照结果
PASS dropout_expectation: 5 cases, 20 mask rows, 10 loss summaries原论文与框架的缩放约定要先对齐
PyTorch 2.9文档说明训练时按保留概率的倒数缩放,评估时该层为恒等映射;v2.9.1源码把模块的training状态传给函数。本文据此固定两套公式,但没有验证所有Dropout变体、不同维度的共享掩码或其他框架实现。
Srivastava等人的原论文用p表示保留概率,并讲解测试时缩放权重的约定;它与本文用p表示丢弃概率、训练时缩放的记号不同。阅读时先转换角色,别直接把同一个字母抄进分母。论文也把缩放推理当作近似处理,并比较显式平均多个子网络;本文没有复现那些训练与数据集结果。
检查代码之前,先说清要平均什么
看到训练与评估结果不同,先分别检查随机输入、线性层输出、激活后输出和损失;这些量的平均不必相同。如果做多次随机前向,明确你平均的是logits、概率还是最终损失,顺序变化本身就可能改变含义,不能只写一句“取平均预测”。
本实验既没有证明Dropout一定提升泛化,也没有提供选择丢弃概率的经验法则。它能支持的结论是:缩放保住某个位置的期望,后续非线性与损失仍需要独立计算。把这三层账本分开后,才能更清楚地检查真正训练中的差异来自哪一步。
记住这些要点
- inverted dropout保住输入位置的期望,不保住每次随机前向的值。
- 固定线性运算能交换求期望的次序,非线性一般不能。
- 平均输出的损失、平均损失和评估模式损失是不同对象。
- 独立掩码也未必等概率,期望必须按每种组合的概率加权。
别把例子用过头
- 把论文里的保留概率p与框架里的丢弃概率p当成同一含义。
- 将四种掩码直接等权平均,却忘了丢弃概率不一定是1
/2。 - 由输入均值保持推断完整网络输出均值保持。
- 把有限掩码的公式穷举写成模型训练、泛化或性能实测。
自己试一小步
你能独立建立一份三维输入的掩码期望账本吗?
- 自行选择含正负数的三维输入、丢弃概率和固定线性权重,明确每个掩码是否独立。
- 穷举八种掩码,记录各自概率、缩放输入、线性输出、激活输出及自选目标的损失。
- 分别比较各层期望与评估公式,另外用只读CSV的程序核验概率和与平方损失分解。
- 再设计一个均值相同的对照,解释相等依赖哪些输入范围;不直接套用正文特殊等式。
做到这些,就算完成
- 概率以丢弃或保留命名,缩放、边界与掩码相关性说清楚。
- 独立练习使用自己的输入,所有期望按概率加权。
- 结论只覆盖所计算的前向分布,没有未执行的训练或泛化承诺。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- PyTorch 2.9:torch.nn.Dropout (新标签页)↗
丢弃概率、训练时按1/(1-p)缩放及评估恒等映射。
- PyTorch v2.9.1:dropout.py (新标签页)↗
p允许0至1及
Dropout.forward把training状态传给函数。 - Dropout: A Simple Way to Prevent Neural Networks from Overfitting (新标签页)↗
独立保留、训练测试缩放约定及第7.5节对子网络平均的讨论。