动手学深度学习 3.13 丢弃法

前言

从零开始学习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() # 评估模式, 这会关闭dropout
acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
net.train() # 改回训练模式
else: # 自定义的模型
if('is_training' in net.__code__.co_varnames): # 如果有is_training这个参数
# 将is_training设置成False
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() # “softmax回归的简洁实现”一节将用到

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)

小结

个人理解,丢弃法怎么说呢,我原本理解是一个网络通过丢弃法内部产生多个网络,但实际上使用的时候还是最终网络,因此相互其实会有影响,一个网络单元也可能是另一个网络单元的一部分,因此不能直接理解成多网络。

感觉更好的理解是高影响的单元会有概率被去除,使得所有神经元都有机会更新,从而应对过拟合的问题。

除此外,直观的感受每个节点的能力更加综合,当某个其他节点不起作用时,自己也能顶上。 更加确保稳定性。

总有种俺寻思能行于是就上的感觉,因此丢弃法的使用仁者见仁智者见智,根据个人经验使用吧。