3.13 丢弃法

方法

除了权重衰减以外,深度学习模型常常使用丢弃法(dropout)来应对过拟合问题。丢弃法有一些不同的变体。本节中提到的丢弃法特指倒置丢弃法(inverted dropout)。

一个单隐藏层的多层感知机,其中输入个数为4,隐藏单元个数为5,且隐藏单元$h_i$($i=1, \ldots, 5$)的计算表达式为

$$
h_i = \phi\left(x_1 w_{1i} + x_2 w_{2i} + x_3 w_{3i} + x_4 w_{4i} + b_i\right)
$$

这里$\phi$是激活函数,$x_1, \ldots, x_4$是输入,隐藏单元$i$的权重参数为$w_{1i}, \ldots, w_{4i}$,偏差参数为$b_i$。当对该隐藏层使用丢弃法时,该层的隐藏单元将有一定概率被丢弃掉。设丢弃概率为$p$,那么有$p$的概率$h_i$会被清零,有$1-p$的概率$h_i$会除以$1-p$做拉伸。丢弃概率是丢弃法的超参数。具体来说,设随机变量$\xi_i$为0和1的概率分别为$p$和$1-p$。使用丢弃法时计算新的隐藏单元$h_i’$

$$
h_i’ = \frac{\xi_i}{1-p} h_i
$$

由于$E(\xi_i) = 1-p$,因此

$$
E(h_i’) = \frac{E(\xi_i)}{1-p}h_i = h_i
$$

丢弃法不改变其输入的期望值。对上述隐藏层使用丢弃法,一种可能的结果如下图所示,其中$h_2$和$h_5$被清零。这时输出值的计算不再依赖$h_2$和$h_5$,在反向传播时,与这两个隐藏单元相关的权重的梯度均为0。由于在训练中隐藏层神经元的丢弃是随机的,即$h_1, \ldots, h_5$都有可能被清零,输出层的计算无法过度依赖$h_1, \ldots, h_5$中的任一个,从而在训练模型时起到正则化的作用,并可以用来应对过拟合。在测试模型时,为了拿到更加确定性的结果,一般不使用丢弃法。

从零开始实现

import torch
import torch.nn as nn
import numpy as np

def dropout(X, drop_prob):
    X = X.float()
    assert 0 <= drop_prob <= 1 # assert是断言函数,如果后面的条件为假,程序会报错
    keep_prob = 1 - drop_prob # 保留概率
    # 这种情况下把全部元素都丢弃
    if keep_prob == 0:
        return torch.zeros_like(X) # zeros_like函数返回一个和输入形状相同的全0张量  
    mask = (torch.rand(X.shape) < keep_prob).float() # rand函数返回一个张量,包含了从区间[0, 1)的均匀分布中抽取的一组随机数

    return mask * X / keep_prob 

X = torch.arange(16).view(2, 8)
print(dropout(X, 0.5))

定义模型参数

使用Fashion-MNIST数据集。定义一个包含两个隐藏层的多层感知机,其中两个隐藏层的输出个数都是256。

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]

定义模型

模型中将全连接层和激活函数ReLU串起来,并对每个激活函数的输出使用丢弃法。分别设置各个层的丢弃概率。通常的建议是把靠近输入层的丢弃概率设得小一点。在这个实验中,把第一个隐藏层的丢弃概率设为0.2,把第二个隐藏层的丢弃概率设为0.5。通过参数is_training来判断运行模式为训练还是测试,并只需在训练模式下使用丢弃法。

drop_prob1, drop_prob2 = 0.2, 0.5 # 丢弃概率

def net(X, is_training=True):
    X = X.view(-1, num_inputs) # view函数将X转换为2D张量,第一维度保持不变,第二维度设为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

评估函数,根据模型的定义方式,进入评估模式。

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是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

训练和测试模型

这部分和之前的一样

def get_dataloader_workers():
    return 0 if sys.platform.startswith('win') else 4

def load_data_fashion_mnist(batch_size, resize=None): 
    # 下载Fashion-MNIST数据集,然后将其加载到内存中,返回训练集和测试集的数据迭代器
    mnist_train = torchvision.datasets.FashionMNIST(root="data", train=True, transform=transforms.ToTensor(), download=True)
    mnist_test = torchvision.datasets.FashionMNIST(root="data", train=False, transform=transforms.ToTensor(), download=True)
    return (data.DataLoader(mnist_train, batch_size, shuffle=True, num_workers=get_dataloader_workers()),
            data.DataLoader(mnist_test, batch_size, shuffle=False, num_workers=get_dataloader_workers()))

def sgd(params,lr,batch_size):  #定义优化算法,params:待优化参数,lr:学习率,batch_size:批量大小
    for param in params:
        param.data-=lr*param.grad/batch_size  #注意这里更改param时用的param.data

def train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, params=None, lr=None, optimizer=None): 
    for epoch in range(num_epochs):  # 训练模型一共需要num_epochs个迭代周期
        train_l_sum, train_acc_sum, n = 0.0, 0.0, 0 # 训练损失总和,训练准确度总和,样本数
        for X, y in train_iter: # X是图像,y是标签,数量为batch_size
            y_hat = net(X) # 预测概率
            l = loss(y_hat, y).sum() # 计算损失,sum()将所有loss值相加得到一个标量

            # 梯度清零
            if optimizer is not None: # 使用PyTorch内置的优化器和损失函数
                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: # 使用PyTorch内置的优化器和损失函数
                sgd(params, lr, batch_size) # 更新模型参数
            else:
                optimizer.step()  # “softmax回归的简洁实现”一节将用到

            train_l_sum += l.item() # 将当前批次loss值相加得到一个总的loss值
            train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item() # 计算总准确率
            n += y.shape[0] # y.shape[0]是y的行数,也就是batch_size,计算总样本数

        test_acc = evaluate_accuracy(test_iter, net) # 计算测试集准确率
        print('周期 %d, 损失 %.4f, 数据集准确率 %.3f, 测试集准确率 %.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 = load_data_fashion_mnist(batch_size)
train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, params, lr)

简洁实现

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

from torch import nn

class FlattenLayer(nn.Module):
    def __init__(self):
        super(FlattenLayer, self).__init__()
    def forward(self, x): 
        return x.view(x.shape[0], -1) # x 的形状转换成(batch, 784),x.shape[0]表示batch_size,-1表示自动推测

net = nn.Sequential(
        FlattenLayer(), # 先将输入x展平,即将形状为(batch, 1, 28, 28)的输入转换成(batch, 784)的输出
        nn.Linear(num_inputs, num_hiddens1), # 隐藏层1
        nn.ReLU(), # 激活函数
        nn.Dropout(drop_prob1), # 丢弃法
        nn.Linear(num_hiddens1, num_hiddens2),  # 隐藏层2
        nn.ReLU(), # 激活函数
        nn.Dropout(drop_prob2), # 丢弃法
        nn.Linear(num_hiddens2, 10) # 输出层
        )

for param in net.parameters():
    nn.init.normal_(param, mean=0, std=0.01)

训练并测试模型

optimizer = torch.optim.SGD(net.parameters(), lr=0.5)
train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)
知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议
上一篇
下一篇