1. 引言
过拟合是深度学习中绕不开的问题:模型在训练集上表现优异,一到测试集就"翻车"。丢弃法(Dropout) 是应对过拟合最经典、最常用的正则化手段之一。本文从原理出发,先手写 dropout_layer 理解其内部机制,再用 PyTorch 框架的 nn.Dropout 做简洁实现,并在 Fashion-MNIST 上对比训练效果。
2. 丢弃法原理
丢弃法在训练时以概率 ppp 随机将隐藏层单元的输出置零,被"丢弃"的神经元不参与本次前向与反向传播。这样每次迭代都在训练一个结构不同的子网络,相当于对大量子网络做集成,从而降低模型对特定神经元的依赖,缓解过拟合。
关键细节在于缩放 :若某单元以概率 ppp 被置零,则其输出期望变为原来的 (1−p)(1-p)(1−p) 倍。为了让训练与测试时的期望一致,需要对保留下来的输出除以 1−p1-p1−p:
xi′={0,以概率 pxi1−p,以概率 1−p x_i' = \begin{cases} 0, & \text{以概率 } p \\ \dfrac{x_i}{1-p}, & \text{以概率 } 1-p \end{cases} xi′=⎩ ⎨ ⎧0,1−pxi,以概率 p以概率 1−p
这里除以 1−p1-p1−p 是为了 xi′x_i'xi′ 与原来的 xix_ixi 的期望相同。
需要注意:
- 丢弃法只在训练时启用,测试/推理阶段直接使用完整网络,不做随机丢弃。
- 丢弃概率 ppp 是超参数,通常隐藏层取 0.2 ~ 0.5,输入层可适当小一些。
- 输出层一般不加 Dropout。
3. 从零实现丢弃法
3.1 实现 dropout_layer 函数
该函数以 dropout 的概率丢弃张量输入 X 中的元素:
python
import torch
from torch import nn
from d2l import torch as d2l
def dropout_layer(X, dropout):
assert 0 <= dropout <= 1 # dropout 必须在 [0, 1] 区间内,否则报错
if dropout == 1:
return torch.zeros_like(X) # dropout 为 1,则 X 全部置零
if dropout == 0:
return X # dropout 为 0,则原样返回
# 生成与 X 同形状的均匀分布随机数,大于 dropout 的位置保留
mask = (torch.rand(X.shape) > dropout).float()
return mask * X / (1.0 - dropout)
说明:原笔记中此处写作
torch.randn,但randn是标准正态分布,取值可能为负,会导致掩码几乎全为 1,达不到丢弃效果。正确做法是使用torch.rand([0,1)[0,1)[0,1) 均匀分布)。
3.2 测试不同丢弃概率
python
X = torch.arange(16, dtype=torch.float32).reshape((2, 8))
print(X)
print(dropout_layer(X, 0.))
print(dropout_layer(X, 0.5)) # 约 50% 的元素被置零
print(dropout_layer(X, 1.))
输出示例:
text
tensor([[ 0., 1., 2., 3., 4., 5., 6., 7.],
[ 8., 9., 10., 11., 12., 13., 14., 15.]])
tensor([[ 0., 1., 2., 3., 4., 5., 6., 7.],
[ 8., 9., 10., 11., 12., 13., 14., 15.]])
tensor([[ 0., 0., 4., 6., 0., 0., 0., 14.],
[16., 0., 0., 0., 0., 0., 0., 30.]])
tensor([[0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0.]])
可以看到:dropout=0 时输出不变;dropout=0.5 时约一半元素被置零,保留的元素被放大为原来的 2 倍;dropout=1 时全部置零。
3.3 定义带 Dropout 的多层感知机
构建一个含两个隐藏层、每层 256 个单元的多层感知机,两个隐藏层的丢弃概率分别为 0.2 和 0.5:
python
num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256
dropout1, dropout2 = 0.2, 0.5
class Net(nn.Module):
def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2,
is_training=True):
super(Net, self).__init__()
self.num_inputs = num_inputs
self.training = is_training
self.lin1 = nn.Linear(num_inputs, num_hiddens1)
self.lin2 = nn.Linear(num_hiddens1, num_hiddens2)
self.lin3 = nn.Linear(num_hiddens2, num_outputs)
self.relu = nn.ReLU()
def forward(self, X):
H1 = self.relu(self.lin1(X.reshape((-1, self.num_inputs))))
if self.training: # 仅在训练时施加 dropout
H1 = dropout_layer(H1, dropout1)
H2 = self.relu(self.lin2(H1))
if self.training:
H2 = dropout_layer(H2, dropout2)
out = self.lin3(H2) # 输出层不施加 dropout
return out
net = Net(num_inputs, num_outputs, num_hiddens1, num_hiddens2)
3.4 训练与测试
python
num_epochs, lr, batch_size = 10, 0.5, 256
loss = nn.CrossEntropyLoss()
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
trainer = torch.optim.SGD(net.parameters(), lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
4. 简洁实现(使用框架)
PyTorch 已内置 nn.Dropout,无需手写掩码逻辑,直接插入到网络层之间即可:
python
import torch
from torch import nn
from d2l import torch as d2l
num_epochs, lr, batch_size = 10, 0.5, 256
dropout1, dropout2 = 0.2, 0.5
loss = nn.CrossEntropyLoss()
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
net = nn.Sequential(
nn.Flatten(),
nn.Linear(784, 256), nn.ReLU(), nn.Dropout(dropout1),
nn.Linear(256, 256), nn.ReLU(), nn.Dropout(dropout2),
nn.Linear(256, 10)
)
def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)
net.apply(init_weights)
trainer = torch.optim.SGD(net.parameters(), lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
nn.Dropout 会自动根据 model.train() / model.eval() 切换行为:训练时按概率丢弃并缩放,评估时直接透传,无需手动判断。
5. 两种实现对比
| 对比项 | 从零实现 | 框架实现 |
|---|---|---|
| 核心逻辑 | 手写掩码 + 缩放 | nn.Dropout 封装 |
| 训练/测试切换 | 手动判断 self.training |
自动跟随 train() / eval() |
| 代码量 | 较多,便于理解原理 | 简洁,适合工程使用 |
| 适用场景 | 学习原理、自定义变体 | 实际项目开发 |
6. 小结
- 丢弃法通过在训练时随机置零隐藏单元,抑制神经元间的共适应,是缓解过拟合的有效正则化手段。
- 为保证期望一致,保留的输出需除以 1−p1-p1−p 进行缩放。
- 丢弃法只在训练阶段生效,测试阶段使用完整网络。
- 实际开发中优先使用
nn.Dropout,理解原理时手写dropout_layer更有帮助。





