丢弃法(Dropout)

2 minute read

Published:

本文介绍丢弃法的基本思想,说明如何通过无偏噪声缓解过拟合,区分训练与预测阶段的处理方式,并给出 Dropout 的从零实现与 PyTorch 简洁实现。

🎦 本节课程视频地址 👇

Bilibil

动机

  • 一个好的模型需要对输入数据的扰动鲁棒(Robust)
    • 使用有噪音的数据等价于Tikhonov 正则(又称:岭回归)
    • 丢弃法:相当于在层之间加入噪音。

无偏差的加入噪音

  • 对\({\bf x}\)加入噪音得到\({\bf x\prime}\),我们希望不改变原有期望\(\bf E\)
\[{\bf E[x\prime]=x}\]
  • 丢弃法对每个元素进行如下扰动:
\[x_i^\prime=\begin{cases} 0 & \sf{with\ pobability}\ \it p\\ {x_i\over1-p} & \sf otherwise\end{cases}\]

相当于有一定概率 \(p\) 使一个值变为零,否则使之变大。这种定义下,可保证期望\(E\)不变。

使用丢弃法训练过程

使用丢弃法

通常将丢弃法\(dropout()\)作用在多层感知机的隐藏层输出上:

\[{\bf h}=\sigma({\bf W_1x+b_1}) \\ {\bf h^\prime}=dropout({\bf h}) \\ {\bf o}={\bf W_2h^\prime+b_2} \\ {\bf y}=softmax({\bf o}) \\\]
  • 丢弃概率\(p\)是控制模型复杂度的超参数,\(p\)越大,模型复杂度越小,反之亦然
  • 一般只用于全连接神经网络(多层感知机),而权重衰退(Weight Decay)通用性更强
  • 一般可以将模型复杂度设置得大一些(比如隐藏层数量更多、隐藏层更大),再添加 Dropout 操作,效果可能会好于一个无 Dropout 的稍简单的网络

推理(预测)中的丢弃法

  • 正则项只在训练中使用:他们影响模型参数的更新
  • 在推理过程中,丢弃法直接返回输入
\[{\bf h}=dropout({\bf h})\]

这样也能保证预测时模型结构的固定,有确定性的输出,否则模型结构将会发生随机改变,导致结果不可控。

当年 Hinton 老爷子认为,dropout 相当于每一次采样一个子神经网络做集成训练。在后来人们的研究中,Dropout 表现出的效果更像是一个正则项。

代码实现

  • 定义 dropout 函数
import torch
from torch import nn
from d2l import torch as d2l

def dropout_layer(X, dropout):
    assert 0 <= dropout <= 1    # assert如果不满足条件,则报错

    if dropout == 1:    # 丢弃所有
        return torch.zeros_like(X)
    if dropout == 0:    # 保留所有
        return X

    # 生成掩码mask,先使用判断生成Bool类型,再转换成浮点类型
    # mask中小于指定概率将被置0,剩余为1
    mask = (torch.randn(X.shape)  > dropout).float()

    return mask * X / (1.0 -dropout)    # 工程小技巧,使用mask而不是随机选取再运算可以提高运算效率
  • 定义模型
#x.shape=(1,784),y.shape=(1,10),w1.shape=(784,256),w2.shape=(256,256),w3.shape=(256,10)

num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256
dropout1, dropout2 = 0.2, .5

class Net(nn.Module):
    def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2, is_training=True):
        super(Net, self).__init__()     # super().__init__() 用于继承父类初始化方法

        self.num_inputs = num_inputs
        self.training = is_training

        # 定义各层神经元
        self.lin1 = nn.Linear(num_inputs, num_hiddens1) # (784,256) h1=xWT
        self.lin2 = nn.Linear(num_hiddens1, num_hiddens2)   # (256,256)
        self.lin3 = nn.Linear(num_hiddens2, num_outputs)    # (256,10)
        self.relu = nn.ReLU()

    def forward(self, X):
        H1 = self.relu(self.lin1(X.reshape((-1, self.num_inputs)))) # (1,784)*(784,256)=(1*256)

        if self.training == True:
            H1 = dropout_layer(H1, dropout1)
        H2 = self.relu(self.lin2(H1))   #(1,256)*(256,256)=(1,256)

        if self.training == True:
            H2 = dropout_layer(H2, dropout2)
        out = self.lin3(H2) #(256,256)*(256,10)=(1,10)
        return out

net = Net(num_inputs, num_outputs, num_hiddens1, num_hiddens2)
  • 训练
num_epochs, lr, batch_size = 10, .5, 256
loss = nn.CrossEntropyLoss()    #打包了softmax

train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
trainer = torch.optim.SGD(net.parameters(), lr=lr)  #net.parameters(recurse=True)返回模块和所有子模块的参数。

d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
训练结果
  • 接着进行对比试验,看如果其他条件不变,不使用 Dropout 结果有什么区别,这里再定义一个 Net2 类,继承自 Net,去掉 Dropout 操作,其他不变:
class Net2(Net):
    def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2,
                 is_training = True):
        super().__init__(num_inputs, num_outputs, num_hiddens1, num_hiddens2,
                 is_training = True)
    def forward(self, X):
        H1 = self.relu(self.lin1(X.reshape((-1, self.num_inputs))))
        H2 = self.relu(self.lin2(H1))
        out = self.lin3(H2)
        return out

net2 = Net2(num_inputs, num_outputs, num_hiddens1, num_hiddens2)
  • 训练对照组
trainer2 = torch.optim.SGD(net2.parameters(), lr=lr)
d2l.train_ch3(net2, train_iter, test_iter, loss, num_epochs, trainer2)
  • 对照结果(左图:有 Dropout;右图:无 Dropout)
dropout_or_not

可看出有 Dropout,训练误差稍高于无 Dropout,但测试精度也稍高于无 Dropout 网络,泛化能力增强。

  • 简洁实现
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模块在评估(推理)时,会自动停止 Dropout 操作

Pytorch 模块参考文档


Q&A🤓

Q:Dropout 在训练的时候进行随机丢弃,在推理的时候又都不丢弃,这样在预测时候,不就相当于还在一个“可能”过拟合的网络结构上使用(所有神经元都为可用)?

🙋‍♂️:关于这点,可以点击以下视频 👇 观看吴恩达 DeepLearning.ai 中一节课程《Why does Dropout work?》

Bilibil

大致观点是,Dropout 在训练过程中,通过随机置零神经元结构,使得 Dropout 下层神经元不能过分依赖上层的某些输入,而不得不 Spread out 权重,这会导致权重收缩(Shrink Weights),从而达到和 L2 正则一样的效果。

结合吴恩达的观点,私以为神经网络在预测时虽然网络结构依旧完整(相比训练时,反而可用的神经元更多),但经过 Dropout 训练时的“调教”,使得神经元权重分配更加“合理”,不会过分地关注某些特征而造成过拟合,同时在数学上相当于增加了正则项,所以限制了模型复杂度。

Q:Dropout和前一节的权重衰退等正则化方法相比具体有什么关系?有没有一个综述性的对比讲解?

🙋‍♂️:Of Course!在我们笔记主页推荐过的博客TheAISummer上有一篇综述性文章可供参考👉Regularization techniques for training deep neural networks

配图说明:部分旧版图片链接已失效,现采用教材对应章节中的 PyTorch 示例图;图中数值可能与笔记保留的历史运行输出略有不同。