底层实现dropout——【torch学习笔记】

2023年7月25日10:06:26

实现dropout

dropout是指在深度学习网络的训练过程中,对于神经网络单元,按照一定的概率将其暂时从网络中丢弃。注意是暂时,对于随机梯度下降来说,由于是随机丢弃,故而每一个mini-batch都在训练不同的网络。

引用翻译:《动手学深度学习》

一、重新审视过度拟合

鉴于特征比例子多得多,线性模型可以过度拟合。但是,当例子比特征多时,我们通常可以指望线性模型不会过度拟合。不幸的是,线性模型归纳的可靠性是有代价的。线性模型不能考虑到特征之间的相互作用。对于每个特征,线性模型必须分配一个正的或负的权重。它们缺乏考虑上下文的灵活性。

在更正式的文本中,你会看到这种可概括性和灵活性之间的基本矛盾被讨论为偏倚-变异权衡。线性模型有很高的偏差(它们只能代表一小类函数),但方差很低(它们在数据的不同随机样本中给出相似的结果)。

深度神经网络将我们带到了偏差-方差光谱的另一端。神经网络之所以如此灵活,是因为它们并不局限于单独观察每个特征。相反,它们可以学习各组特征之间的相互作用。例如,它们可以推断出 "尼日利亚 "和 "西联汇款 "同时出现在一封电子邮件中表明是垃圾邮件,但没有 "西联汇款 "的 "尼日利亚 "则不是。

即使我们只有少量的特征,深度神经网络也有能力进行过度拟合。2017年,一组研究人员提出了一个现在众所周知的关于神经网络难以置信的灵活性的演示。他们向一个神经网络展示了随机标记的图像(没有真正的模式将输入和输出联系起来),并发现由SGD优化的神经网络可以完美地标记训练集中的每一张图像。

考虑一下这意味着什么。如果标签是统一随机分配的,并且有10个类别,那么没有一个分类器能在保持数据上获得优于10%的准确性。然而,即使在这种情况下,当没有真正的模式可以学习时,神经网络也能完美地适应训练标签。

二、通过扰动的鲁棒性

让我们简单思考一下我们对一个好的统计模型的期望。我们希望它在未见过的测试数据上表现良好。我们可以通过询问什么是 "简单 "的模型来实现这一目标?简洁性可以以少量维度的形式出现,这就是我们在讨论用单项式基函数拟合模型时的做法。简洁性也可以以基函数的小规范的形式出现。这使我们想到了权重衰减(ℓ2正则化)。然而,我们可以施加的第三个简单性概念是,函数在输入的小变化下应该是稳健的。例如,当我们对图像进行分类时,我们希望在像素上添加一些随机噪声应该是无害的。

1995年,Christopher Bishop正式提出了这个想法的一种形式,他证明了用输入噪声进行训练等同于Tikhonov正则化。换句话说,他在要求一个函数是平滑的(因而也是简单的)(我们在关于权重衰减的章节中讨论过)与要求它对输入的扰动有弹性之间建立了明确的数学联系。

然后在2014年,Srivastava等人提出了一个聪明的想法,即如何将Bishop的想法也应用到网络的内部层。也就是说,他们提议在训练过程中,在计算后续层之前向网络的每一层注入噪声。他们意识到,在训练有很多层的深层网络时,仅仅在输入-输出映射上强制执行平滑性会忽略网络内部发生的事情。他们提出的想法被称为dropout,它现在是一种标准技术,被广泛用于训练神经网络。在整个训练过程中,在每一次迭代中,dropout正则化仅仅包括在计算下一层之前将每一层中的一些节点清零(通常是50%)。

那么关键的挑战是如何在不引入不适当的统计偏差的情况下注入这种噪音。换句话说,我们希望在训练过程中对每一层的输入进行扰动,使该层的预期值等于我们没有引入任何噪声时的值。
在Bishop的例子中,当我们在线性模型中加入高斯噪声时,这很简单。在每个训练迭代中,只需向输入的????∼(0,????2)添加从均值为零的分布中采样的噪声,产生一个扰动点 ????′=????+????。在期望值中,????[????′]=????。

在Dropout正则化的情况下,我们可以通过对未被Dropout的节点的比例进行归一化,来对每一层进行debias。换句话说,掉线概率为????的掉线被应用如下。

h

=

{

0

 with probability 

p

h

1

p

 otherwise

\begin{aligned} h' = \begin{cases} 0 & \text{ with probability } p \\ \frac{h}{1-p} & \text{ otherwise} \end{cases} \end{aligned}

h={01ph with probability p otherwise

根据设计,期望值保持不变,即????[ℎ′]=ℎ。中间激活ℎ被一个具有匹配期望值的随机变量ℎ′所取代。Dropout "这一名称源于这样一个概念,即一些神经元为了计算最终结果而 “Dropout”。在训练过程中,我们用随机变量取代中间激活。

三、实践中的dropout

回顾多层感知器(:numref:chapter_mlp),有一个隐藏层和5个隐藏单元。它的结构是这样的

h

=

σ

(

W

1

x

+

b

1

)

o

=

W

2

h

+

b

2

y

^

=

s

o

f

t

m

a

x

(

o

)

\begin{aligned} h & = \sigma(W_1 x + b_1) \\ o & = W_2 h + b_2 \\ \hat{y} & = \mathrm{softmax}(o) \end{aligned}

hoy^=σ(W1x+b1)=W2h+b2=softmax(o)

当我们对隐藏层应用dropout时,我们基本上是以概率????移除每个隐藏单元,(即把它们的输出设置为0)。我们可以把这个结果看作是一个只包含原始神经元子集的网络。在下面的图片中,ℎ2和ℎ5被删除。因此,????的计算不再依赖于ℎ2和ℎ5,它们各自的梯度也在执行Backprop时消失。这样一来,输出层的计算就不能过度依赖ℎ1,…,ℎ5中的任何一个元素。直观地说,深度学习的研究者们经常这样解释这个inutition:我们不希望网络的输出过于不稳定地依赖于通过网络的确切激活途径。dropout技术的原作者将他们的直觉描述为防止特征检测器的共同适应的一种努力。

from IPython.display import SVG
SVG(filename = '../img/dropout2.svg')

底层实现dropout——【torch学习笔记】

在测试时,我们通常不使用辍学。然而,我们注意到有一些例外情况:一些研究人员在测试时使用辍学作为估计神经网络预测的信心的启发式评估:如果预测在许多不同的辍学掩码中都一致,那么我们可以说网络更有信心。现在,我们将把不确定性估计的高级话题推迟到以后的章节和卷中。

四、从零开始实施dropout

为了实现单层的剔除功能,我们必须从伯努利(二进制)随机变量中抽取尽可能多的样本,因为我们的层有维数,其中随机变量的值为1(保留),概率为1-????,0(剔除),概率为????。一个简单的实现方法是首先从均匀分布????[0,1]中抽取样本。然后我们可以保留那些相应样本大于????的节点,放弃其余的节点。

在下面的代码中,我们实现了一个dropout函数,该函数以drop_prob的概率丢掉了张量输入X中的元素,如上所述重新调整了剩余部分的比例(将幸存者除以1.0-drop_prob)。

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

import sys
sys.path.insert(0, '..')
def load_data_fashion_mnist(batch_size, resize=None, root=os.path.join(
        '~', '.pytorch', 'datasets', 'fashion-mnist')):
    """下载数据集并存入内存."""
    root = os.path.expanduser(root)
    print(root)
    transformer = []
    if resize:
        transformer += [transforms.Resize(resize)]
    transformer += [transforms.ToTensor()]
    transformer = transforms.Compose(transformer)
    
    # 如果存在数据集则不再下载,若没有则下载
    mnist_train = torchvision.datasets.FashionMNIST(root=root, train=True, transform=transformer, download=True)
    mnist_test = torchvision.datasets.FashionMNIST(root=root, train=False, transform=transformer, download=True)
    num_workers = 0 if sys.platform.startswith('win32') else 4
    
    # 使用封装好的DataLoader载入即可。
    train_iter = DataLoader(mnist_train, batch_size, shuffle=True, num_workers=num_workers)
    test_iter = DataLoader(mnist_test, batch_size, shuffle=False, num_workers=num_workers)
    return mnist_train,mnist_test,train_iter, test_iter


dropout的底层实现函数:

def dropout(X, drop_prob):
    assert 0 <= drop_prob <= 1
    if drop_prob == 1:  # 在这种情况下,所有元素都被剔除
        return torch.zeros_like(X)
    # mask是计算是否保留的0,1值
    mask = (torch.FloatTensor(X.shape).uniform_(0, 1) > drop_prob).float()
    # 将幸存者除以1.0-drop_prob
    return mask * X / (1.0 - drop_prob)

我们可以在几个例子上测试一下dropout函数。在下面几行代码中,我们将我们的输入X通过dropout操作,概率分别为0、0.5和1。

# mask = tensor([[0., 0., 0., 0., 0., 1., 1., 0.],
#        [0., 0., 1., 0., 0., 1., 0., 1.]])
X = torch.arange(16, dtype = torch.float32).reshape((2, 8))
print(X)
print(dropout(X, 0.)) # 相当于将幸存者除(1-0)(即不变)
print(dropout(X, 0.5))  # 相当于将幸存者除(1-0.5)(即乘以2)
print(dropout(X, 1.)) # 相当于将幸存者除(1-1)(0)
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.,  0.,  0., 10.,  0.,  0.],
        [16., 18., 20.,  0.,  0.,  0.,  0., 30.]])
tensor([[0., 0., 0., 0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0., 0., 0., 0.]])

五、定义模型参数

同样,我们可以使用 :numref:chapter_softmax_scratch中介绍的Fashion-MNIST数据集。我们将定义一个有两个隐藏层的多层感知器。这两个隐藏层都有256个输出。

六、应用dropout定义模型

下面定义的模型将全连接层和激活函数ReLU串联起来,对每个激活函数的输出使用dropout。我们可以分别设置每一层的丢弃概率。一般来说,建议在靠近输入层的地方设置较低的丢弃概率。下面我们将第一和第二隐藏层分别设置为0.2和0.5。通过使用:numref:chapter_autograd中描述的is_training函数,我们可以确保dropout只在训练期间有效。

drop_prob1, drop_prob2 = 0.2, 0.5

class Net(nn.Module):
    def __init__(self, num_inputs = 784, num_outputs = 10, num_hiddens1 = 256, num_hiddens2 = 256, is_training = True):
        super(Net, self).__init__()
        
        self.num_inputs = num_inputs
        self.num_outputs = num_outputs
        self.num_hiddens1 = num_hiddens1
        self.num_hiddens2 = num_hiddens2
        self.is_training = is_training
        
        self.linear_1 = nn.Linear(num_inputs, num_hiddens1)
        self.linear_2 = nn.Linear(num_hiddens1, num_hiddens2)
        self.linear_3 = nn.Linear(num_hiddens2, num_outputs)
        
        self.relu = nn.ReLU()
    
    def forward(self, X):
        # 可以分别设置每一层的丢弃概率
        X = X.reshape((-1, self.num_inputs))
        H1 = self.relu(self.linear_1(X))
        # 只有在训练阶段使用dropout
        if self.is_training == True:
            # 在第一个全连接层之后添加一个dropout层
            # 靠近输入层的地方设置较低的丢弃概率
            H1 = dropout(H1, drop_prob1)
        H2 = self.relu(self.linear_2(H1))
        if self.is_training == True:
            # 在第二个全连接层之后添加一个dropout层
            # 后面层的地方设置较中等的丢弃概率
            H2 = dropout(H2, drop_prob2)
        out = self.linear_3(H2)
        return out

net = Net()
print(net)
Net(
  (linear_1): Linear(in_features=784, out_features=256, bias=True)
  (linear_2): Linear(in_features=256, out_features=256, bias=True)
  (linear_3): Linear(in_features=256, out_features=10, bias=True)
  (relu): ReLU()
)

七、训练和测试

这与前面描述的多层感知器的训练和测试类似。

import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.autograd import Variable
import numpy as np
import math
import time
def train_ch3(net, train_iter, test_iter, criterion, num_epochs, batch_size, lr=None):
    """使用CPU训练或评估模型."""
    optimizer = optim.SGD(net.parameters(), lr=lr)
    for epoch in range(num_epochs):
        train_l_sum, train_acc_sum, n = 0.0, 0.0, 0
        for X, y in train_iter:
            # 即将梯度初始化为零
            optimizer.zero_grad()
            # 通过训练的网络net处理该批次数据集X,得到预测的类别输出
            y_hat = net(X)
            # 通过预测的类别 ,结合真实类别,通过交叉熵计算损失函数
            loss = criterion(y_hat, y)
            # loss.backward()函数的作用是根据loss来计算网络参数的梯度,其对应的输入默认为网络的叶子节点
            loss.backward()
            # 所有的optimizer都实现了step()方法,这个方法会更新所有的参数
            optimizer.step()
            # 将真实类别转化为数值型
            y = y.type(torch.float32)
            # 计算损失的总和,pytorch中的.item()用于将一个零维张量转换成浮点数。可以减少gpu内存消耗
            train_l_sum += loss.item()
            # 如果真实label和预测的y_hat相同,则计数正确一个,以此计算训练集准确率
            train_acc_sum += torch.sum((torch.argmax(y_hat, dim=1).type(torch.FloatTensor) == y).detach()).float()
            n += list(y.size())[0]
        # 通过评估函数计算测试集的准确率
        test_acc = evaluate_accuracy(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 = 10, 0.5, 256
train_iter, test_iter = load_data_fashion_mnist(batch_size)
criterion = nn.CrossEntropyLoss()
train_ch3(net, train_iter, test_iter, criterion, num_epochs, batch_size, lr)
epoch 1, loss 0.0036, train acc 0.659, test acc 0.770
epoch 2, loss 0.0021, train acc 0.803, test acc 0.801
epoch 3, loss 0.0018, train acc 0.831, test acc 0.781
epoch 4, loss 0.0017, train acc 0.845, test acc 0.837
epoch 5, loss 0.0016, train acc 0.851, test acc 0.812
epoch 6, loss 0.0015, train acc 0.860, test acc 0.846
epoch 7, loss 0.0014, train acc 0.864, test acc 0.843
epoch 8, loss 0.0014, train acc 0.869, test acc 0.851
epoch 9, loss 0.0014, train acc 0.873, test acc 0.861
epoch 10, loss 0.0013, train acc 0.875, test acc 0.853

八、摘要

1、除了控制维数和权重向量的大小之外,dropout是另一个避免过度拟合的工具。通常情况下,这三者是联合使用的。

2、Dropout用随机变量ℎ′代替激活ℎ,其期望值为ℎ,方差由dropout概率????给出。
辍学只在训练期间使用。

3、经过交叉验证,隐含节点dropout率等于0.5的时候效果最好,因为这时候dropout随机生成的网络结构最多。

4、dropout也可以被用作一种添加噪声的方法,直接对input进行操作,输入层设为更接近1的数,使得输入变化不会太大,一般选0.8。

5、超参数的采样概率一般选1。

6、dropout常用于提升模型泛化能力

7、dropout也常用于解决模型费时的问题。做完dropout,相当于从原始的网络中找到一个更瘦的网络。因而,对于一个有N个节点的神经网络,有了dropout后,就可以看做是2^n个模型的集合了,但此时要训练的参数数目却是不变的,这就解决了费时的问题。

九、练习

1、试试如果你改变第1层和第2层的辍学概率会发生什么。特别是,如果你改变两层的概率会发生什么?

2、增加历时的数量,比较使用放弃和不使用放弃时的结果。

3、计算应用dropout后的激活随机变量的方差。

4、为什么你通常不使用dropout?

5、如果对模型进行修改,使其更加复杂,如增加隐藏层单元,使用dropout来应对过拟合的效果是否会更加明显?

6、以本节中的模型为例,比较使用dropout和权重衰减的效果。如果同时使用dropout和权重衰减会怎样?

7、如果我们对权重矩阵中的单个权重而不是激活应用dropout会怎样?

8、用一个随机变量来代替dropout激活,该变量的值为[0,????/2,????] 。你能设计出比二进制dropout函数效果更好的东西吗?你为什么要使用它?为什么不呢?

  • 作者:一个语文不好的NLPer
  • 原文链接:https://blog.csdn.net/weixin_43180762/article/details/124260592
    更新时间:2023年7月25日10:06:26 ,共 8472 字。