前言
从零开始学习ai文章系列计划是个人在《动手学深度学习》和《磨菇书》两本书的学习中的个人笔记,文章也会以课本中的章节分开,即每个章节一片笔记。我会尽量的把主要内容以及遇到的难点进行记录与解决,如果哪里有错误的欢迎指正。或者不清晰的可以直接查看原文部分。
《动手学深度学习》原文(课本):https://tangshusen.me/Dive-into-DL-PyTorch/#/
《动手学深度学习》代码:https://github.com/ShusenTang/Dive-into-DL-PyTorch
(由于有时候公式太多,可能会直接贴图片)
1. 方法
3.8节(多层感知机)的图3.3描述了一个单隐藏层的多层感知机。
其中输入个数为4,隐藏单元个数为5,且隐藏单元hi(i=1,…,5)的计算表达式为
\[
h_i = ϕ(x_1w_{1i}+x_2w_{2i}+x_3w_{3i}+x_4w_{4i}+b_i)
\]
当对该隐藏层使用丢弃法时,该层的隐藏单元将有一定概率被丢弃掉。
设丢弃概率为p,那么有p的概率 \(h_i\)
会被清零,有1−p的概率会除以1−p做拉伸。
即使用丢弃法后,新的隐藏单元 \(h_i'\) 的结果如下 \[
h'_i =
\left\{
\begin{array}{ll}
0 & \text{概率 } p \\
\frac {h_i} {1-p} & \text{概率 } 1-p
\end{array}
\right.
\] 丢弃概率p是丢弃法的超参数。
这里我们统一成 \[
h'_i=\frac {ξ_i} {1-p}
\] 随机变量 \(ξ_i\) 为 0 和 1
的概率分别为 p 和 1−p。

由于 \(E(ξ_i)=1-p\) ,因此 \[
E(h'_i) = \frac {E(ξ_i)} {1-p} h_i = h_i
\] 即丢弃法不改变其输入的期望值。

让我们对图3.3中的隐藏层使用丢弃法,一种可能的结果如图3.5所示,其中h2和h5被清零。这时输出值的计算不再依赖h2和h5,在反向传播时,与这两个隐藏单元相关的权重的梯度均为0。

由于在训练中隐藏层神经元的丢弃是随机的,即h1,…,h5都有可能被清零,输出层的计算无法过度依赖h1,…,h5中的任一个,从而在训练模型时起到正则化的作用,并可以用来应对过拟合。在测试模型时,我们为了拿到更加确定性的结果,一般不使用丢弃法。
2. 从零开始实现

我们运行几个例子来测试一下dropout函数。其中丢弃概率分别为0、0.5和1。
2.1 定义模型参数
实验中,我们依然使用3.6节(softmax回归的从零开始实现)中介绍的Fashion-MNIST数据集。我们将定义一个包含两个隐藏层的多层感知机,其中两个隐藏层的输出个数都是256。
输入784,输出10,两个隐藏层都为256节点
1 2 3 4 5 6 7 8 9 10
| num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256
W1 = torch.tensor(np.random.normal(0, 0.01, size=(num_inputs, num_hiddens1)), dtype=torch.float, requires_grad=True) b1 = torch.zeros(num_hiddens1, requires_grad=True) W2 = torch.tensor(np.random.normal(0, 0.01, size=(num_hiddens1, num_hiddens2)), dtype=torch.float, requires_grad=True) b2 = torch.zeros(num_hiddens2, requires_grad=True) W3 = torch.tensor(np.random.normal(0, 0.01, size=(num_hiddens2, num_outputs)), dtype=torch.float, requires_grad=True) b3 = torch.zeros(num_outputs, requires_grad=True)
params = [W1, b1, W2, b2, W3, b3]
|
2.2 定义模型
下面定义的模型将全连接层和激活函数ReLU串起来,并对每个激活函数的输出使用丢弃法。
我们可以分别设置各个层的丢弃概率。通常的建议是把靠近输入层的丢弃概率设得小一点。
在这个实验中,我们把第一个隐藏层的丢弃概率设为0.2,把第二个隐藏层的丢弃概率设为0.5。我们可以通过参数is_training来判断运行模式为训练还是测试,并只需在训练模式下使用丢弃法。
1 2 3 4 5 6 7 8 9 10 11 12
| drop_prob1, drop_prob2 = 0.2, 0.5
def net(X, is_training=True): X = X.view(-1, num_inputs) H1 = (torch.matmul(X, W1) + b1).relu() if is_training: H1 = dropout(H1, drop_prob1) H2 = (torch.matmul(H1, W2) + b2).relu() if is_training: H2 = dropout(H2, drop_prob2) return torch.matmul(H2, W3) + b3
|
我们在对模型评估的时候不应该进行丢弃,所以我们修改一下d2lzh_pytorch中的evaluate_accuracy函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| def evaluate_accuracy(data_iter, net): acc_sum, n = 0.0, 0 for X, y in data_iter: if isinstance(net, torch.nn.Module): net.eval() acc_sum += (net(X).argmax(dim=1) == y).float().sum().item() net.train() else: if('is_training' in net.__code__.co_varnames): acc_sum += (net(X, is_training=False).argmax(dim=1) == y).float().sum().item() else: acc_sum += (net(X).argmax(dim=1) == y).float().sum().item() n += y.shape[0] return acc_sum / n
|
2.3 训练和测试模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| def train_ch3_2(net, train_iter, test_iter, loss, num_epochs, batch_size, params=None, lr=None, optimizer=None): for epoch in range(num_epochs): train_l_sum, train_acc_sum, n = 0.0, 0.0, 0 for X, y in train_iter: y_hat = net(X) l = loss(y_hat, y).sum()
if optimizer is not None: optimizer.zero_grad() elif params is not None and params[0].grad is not None: for param in params: param.grad.data.zero_()
l.backward() if optimizer is None: sgd(params, lr, batch_size) else: optimizer.step()
train_l_sum += l.item() train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item() n += y.shape[0] test_acc = evaluate_accuracy2(test_iter, net) print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f' % (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc)) num_epochs, lr, batch_size = 5, 100.0, 256 loss = torch.nn.CrossEntropyLoss() train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) d2l.train_ch3_2(net, train_iter, test_iter, loss, num_epochs, batch_size, params, lr)
|

3. 简洁实现
在训练模型时,Dropout层将以指定的丢弃概率随机丢弃上一层的输出元素;
在测试模型时(即model.eval()后,如下图),Dropout层并不发挥作用。
image-20250416165053106
在PyTorch中,我们只需要在全连接层后添加Dropout层并指定丢弃概率。

下面训练并测试模型。
1 2
| optimizer = torch.optim.SGD(net.parameters(), lr=0.5) d2l.train_ch3_2(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)
|

小结
个人理解,丢弃法怎么说呢,我原本理解是一个网络通过丢弃法内部产生多个网络,但实际上使用的时候还是最终网络,因此相互其实会有影响,一个网络单元也可能是另一个网络单元的一部分,因此不能直接理解成多网络。
感觉更好的理解是高影响的单元会有概率被去除,使得所有神经元都有机会更新,从而应对过拟合的问题。
除此外,直观的感受每个节点的能力更加综合,当某个其他节点不起作用时,自己也能顶上。
更加确保稳定性。
总有种俺寻思能行于是就上的感觉,因此丢弃法的使用仁者见仁智者见智,根据个人经验使用吧。