丢弃法
动机
- 一个好的模型需要对输入数据的扰动鲁棒
- 使用有噪音的数据等价于 Tikhonov 正则
- 丢弃法:在层之间加入噪音
无偏差的加入噪音
- 对 x \mathbf{x} x 加入噪音得到 x ′ \mathbf{x}' x′,我们希望
E [ x ′ ] = x \mathbf{E}[\mathbf{x}'] = \mathbf{x} E[x′]=x - 丢弃法对每个元素进行如下扰动
x i ′ = { 0 with probability p x i 1 − p otherwise x_i' = \begin{cases} 0 & \text{with probability } p \\ \frac{x_i}{1 - p} & \text{otherwise} \end{cases} xi′={01−pxiwith probability potherwise
实际上,上述 Dropout 后期望是没有发生变化的,具体期望计算如下: E [ x i ′ ] = p ⋅ 0 + ( 1 − p ) ⋅ x i 1 − p = x i \mathbf{E}[x_i'] = p \cdot 0 + (1 - p) \cdot \frac{x_i}{1 - p} = x_i E[xi′]=p⋅0+(1−p)⋅1−pxi=xi
# 推理中的丢弃法
- 正则项只在训练中使用:他们影响模型参数的更新
- 在推理过程中,丢弃法直接返回输入,无需做任何操作 h = dropout ( h ) \mathbf{h} = \text{dropout}(\mathbf{h}) h=dropout(h)
- 这样也能保证确定性的输出
总结
- 丢弃法将一些输出项随机置 0 来控制模型复杂度
- 常作用在多层感知机的隐藏层输出上,很少用在 CNN 之类的模型上
- 丢弃概率是控制模型复杂度的超参数
代码实现
从零开始实现
首先导入必要的库,同时实现一个 dropout_layer
函数,该函数以dropout
的概率丢弃张量输入X
中的元素:
import torch
from torch import nn
from d2l import torch as d2ldef dropout_layer(X, dropout): # X是张量,dropout是丢弃率assert 0 <= dropout <= 1# 在本情况中,所有元素都被丢弃if dropout == 1: # 为1表示所有元素都丢弃,返回一个与X形状相同的全0张量return torch.zeros_like(X)# 在本情况中,所有元素都被保留if dropout == 0: # 所有元素都保留return X# 生成一个与X形状相同的随机张量,值在[0, 1)之间,然后与dropout进行比较,大于的赋值为1.0,小于的赋值为0.0mask = (torch.rand(X.shape) > dropout).float()# 得到一个二值掩码张量 mask,用于指示哪些元素应该保留(值为1)return mask * X / (1.0 - dropout)
接下来来测试一下该函数:
X= torch.arange(16, dtype = torch.float32).reshape((2, 8))
print(X)
print(dropout_layer(X, 0.))
print(dropout_layer(X, 0.5)) # 有一半的概率将其中的元素变为0
print(dropout_layer(X, 0.7))
print(dropout_layer(X, 1.))
下面定义模型参数(定义具有两个隐藏层的多层感知机,每个隐藏层包含256个单元):
输入还是 28*28,输出是 10 个类别
num_inputs, num_outputs, num_hiddens1, num_hiddens2= 784, 10, 256, 256
下面定义模型:
dropout1, dropout2= 0.2, 0.5class Net(nn.Module):def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2,is_training = True): # 要区别是在训练,还是在测试super(Net, self).__init__()self.num_inputs = num_inputsself.training = is_trainingself.lin1 = nn.Linear(num_inputs, num_hiddens1) # 定义第一个全连接层,输入为 num_inputs,输出为 num_hiddens1self.lin2 = nn.Linear(num_hiddens1, num_hiddens2)self.lin3 = nn.Linear(num_hiddens2, num_outputs)self.relu = nn.ReLU()# 定义激活函数def forward(self, X):H1 = self.relu(self.lin1(X.reshape((-1, self.num_inputs)))) # 第一个隐藏层的输出# 只有在训练模型时才使用dropoutif self.training == True:# 在第一个全连接层之后添加一个dropout层H1 = dropout_layer(H1, dropout1)H2 = self.relu(self.lin2(H1))if self.training == True:# 在第二个全连接层之后添加一个dropout层H2 = dropout_layer(H2, dropout2)out = self.lin3(H2)return outnet = Net(num_inputs, num_outputs, num_hiddens1, num_hiddens2)
下面训练和测试:
num_epochs, lr, batch_size = 10, 0.5, 256
loss = nn.CrossEntropyLoss(reduction='none') # 不对每个样本的损失进行平均或求和,而是返回每个样本的损失值
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
trainer = torch.optim.SGD(net.parameters(), lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
简洁实现
net = nn.Sequential(nn.Flatten(),# 将输入张量展平为一维向量nn.Linear(784, 256), # 定义一个全连接层nn.ReLU(),# 在第一个全连接层之后添加一个dropout层nn.Dropout(dropout1),nn.Linear(256, 256),nn.ReLU(),# 在第二个全连接层之后添加一个dropout层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)
QA 思考
Q1:Dropout 随机丢弃,如何保证结果的正确性和可重复性?
A1:实际上,机器学习是没有正确性这一说的,只有效果好不好。没必要可重复,只需要精度差不多就可以。
概念1:丢弃法是在训练中把神经元丢弃后训练,在预测时网络中的神经元没有丢弃。
概念2:丢弃法是每次迭代一次,随机丢弃一次。也就是每一个层在调用前向运算的时候,随机丢弃一次。
Q2:训练时使用dropout,推理时不用。那会不会导致推理时输出结果翻倍了?比如dropout=0.5,推理时输出结果是训练时2个子神经网络的叠加而翻倍?
A2:这就是为啥需要除以一个 (1 - p),保证期望不会发生变化,避免我的输出结果是我的训练时的 N 倍。
Q3:在同样的Ir下,dropout的介入会不会造成参数收敛更慢,需要比没有dropout的情况下适当调大Ir吗?
A3:由于Dropout的加入,导致梯度值减小,因此参数收敛汇编慢,但是并没有研究表明一定需要适当增大 lr 的。
Q4:为什么推理中的 Dropout 是直接返回输入?
A4:预测,也就是不对权重做更新的时候,Dropout 是不需要的,因为 Dropout 是一个正则项,正则项唯一的作用就是在更新权重的时候让模型复杂度变低一点,在做推理的时候是不会更新模型的,因此是不需要 Dropout 的,当然也可以用,但是用的话就会出现随机性了。因此为了避免这些随机性,这样需要多算几次推理来将这个方差降下来,但是训练的时候是没有问题的,因为我训练的时候需要跑很多轮,这样对整个系统的稳定性来说是没有问题的,但是在推理的时候,就关心某一个样本结果的话,可能就需要做平均了。
对于这个的理解是,训练集的分布与真实分布可能并不完全相同,因此引用dropout来对训练集加入一些噪音,使得训练数据的分布更加普适,从而保证模型的鲁棒性。所以预测的时候就没必要再加入噪声了。
后记
理解了之后写的一段代码:
import torch
import torchvision
from torchvision import transforms
from torch.utils import data
import matplotlib.pyplot as plt
from tqdm import tqdm # 导入 tqdm 库# 定义 Dropout 层
def dropout_layer(X, dropout):assert 0 <= dropout <= 1if dropout == 1:return torch.zeros_like(X)if dropout == 0:return Xmask = (torch.rand(X.shape) > dropout).float()return mask * X / (1.0 - dropout)# 定义神经网络
class Net(torch.nn.Module):def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2, is_training=True):super(Net, self).__init__()self.num_inputs = num_inputsself.training = is_trainingself.lin1 = torch.nn.Linear(num_inputs, num_hiddens1)self.lin2 = torch.nn.Linear(num_hiddens1, num_hiddens2)self.lin3 = torch.nn.Linear(num_hiddens2, num_outputs)self.relu = torch.nn.ReLU()def forward(self, X):H1 = self.relu(self.lin1(X.reshape((-1, self.num_inputs))))if self.training:H1 = dropout_layer(H1, dropout=0.2)H2 = self.relu(self.lin2(H1))if self.training:H2 = dropout_layer(H2, dropout=0.5)return self.lin3(H2) # 输出层不需要激活函数# 加载 Fashion-MNIST 数据集
def load_data_fashion_mnist(batch_size, resize=None):trans = [transforms.ToTensor()]if resize:trans.insert(0, transforms.Resize(resize))trans = transforms.Compose(trans)mnist_train = torchvision.datasets.FashionMNIST("../data", train=True, transform=trans, download=True)mnist_test = torchvision.datasets.FashionMNIST("../data", train=False, transform=trans, download=True)return (data.DataLoader(mnist_train, batch_size, shuffle=True),data.DataLoader(mnist_test, batch_size, shuffle=False))# 累积器类
class Accumulator:def __init__(self, n):self.data = [0.0] * ndef add(self, *args):self.data = [a + float(b) for a, b in zip(self.data, args)]def reset(self):self.data = [0.0] * len(self.data)def __getitem__(self, idx):return self.data[idx]# 计算准确率
def accuracy(y_hat, y):if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:y_hat = y_hat.argmax(axis=1)cmp = y_hat.type(y.dtype) == yreturn float(cmp.type(y.dtype).sum())# 评估模型在数据集上的准确率
def evaluate_accuracy(net, data_iter):if isinstance(net, torch.nn.Module):net.eval()metric = Accumulator(2)with torch.no_grad():for X, y in data_iter:metric.add(accuracy(net(X), y), y.numel())return metric[0] / metric[1]# 单个 epoch 的训练
def train_epoch_ch3(net, train_iter, loss, updater):if isinstance(net, torch.nn.Module):net.train()metric = Accumulator(3)# 使用 tqdm 添加进度条for X, y in tqdm(train_iter, desc="Training"):y_hat = net(X)l = loss(y_hat, y)if isinstance(updater, torch.optim.Optimizer):updater.zero_grad()l.mean().backward()updater.step()else:l.sum().backward()updater(X.shape[0])metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())return metric[0] / metric[2], metric[1] / metric[2]# 绘制动画的类
class Animator:def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None,ylim=None, xscale='linear', yscale='linear',fmts=('-', 'm--', 'g-.', 'r:'), figsize=(3.5, 2.5)):if legend is None:legend = []self.xlabel = xlabelself.ylabel = ylabelself.legend = legendself.xlim = xlimself.ylim = ylimself.xscale = xscaleself.yscale = yscaleself.fmts = fmtsself.figsize = figsizeself.X, self.Y = [], []def add(self, x, y):if not hasattr(y, "__len__"):y = [y]n = len(y)if not hasattr(x, "__len__"):x = [x] * nif not self.X:self.X = [[] for _ in range(n)]if not self.Y:self.Y = [[] for _ in range(n)]for i, (a, b) in enumerate(zip(x, y)):if a is not None and b is not None:self.X[i].append(a)self.Y[i].append(b)def show(self):plt.figure(figsize=self.figsize)for x_data, y_data, fmt in zip(self.X, self.Y, self.fmts):plt.plot(x_data, y_data, fmt)plt.xlabel(self.xlabel)plt.ylabel(self.ylabel)if self.legend:plt.legend(self.legend)if self.xlim:plt.xlim(self.xlim)if self.ylim:plt.ylim(self.ylim)plt.xscale(self.xscale)plt.yscale(self.yscale)plt.grid()plt.show()# 主训练函数
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],legend=['train loss', 'train acc', 'test acc'])for epoch in range(num_epochs):train_metrics = train_epoch_ch3(net, train_iter, loss, updater)test_acc = evaluate_accuracy(net, test_iter)animator.add(epoch + 1, train_metrics + (test_acc,))print(f"Epoch {epoch + 1}: Train Metrics = {train_metrics}, Test Acc = {test_acc}")train_loss, train_acc = train_metricsanimator.show() # 展示最终结果图# 主程序
if __name__ == "__main__":# 参数设置num_epochs, lr, batch_size = 10, 0.5, 256num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256# 加载数据train_iter, test_iter = load_data_fashion_mnist(batch_size)# 定义模型、损失函数和优化器net = Net(num_inputs, num_outputs, num_hiddens1, num_hiddens2)loss = torch.nn.CrossEntropyLoss(reduction='none')trainer = torch.optim.SGD(net.parameters(), lr=lr)# 开始训练train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
扔一个简化版:
import torch
from torch import nn
from package import load_data_fashion_mnist, train_ch3# 定义 Dropout 层
def dropout_layer(X, dropout):assert 0 <= dropout <= 1if dropout == 1:return torch.zeros_like(X)if dropout == 0:return Xmask = (torch.rand(X.shape) > dropout).float()return mask * X / (1.0 - dropout)# 定义神经网络(复杂版)
class Net(torch.nn.Module):def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2, is_training=True):super(Net, self).__init__()self.num_inputs = num_inputsself.training = is_trainingself.lin1 = torch.nn.Linear(num_inputs, num_hiddens1)self.lin2 = torch.nn.Linear(num_hiddens1, num_hiddens2)self.lin3 = torch.nn.Linear(num_hiddens2, num_outputs)self.relu = torch.nn.ReLU()def forward(self, X):H1 = self.relu(self.lin1(X.reshape((-1, self.num_inputs))))if self.training:H1 = dropout_layer(H1, dropout=0.2)H2 = self.relu(self.lin2(H1))if self.training:H2 = dropout_layer(H2, dropout=0.5)return self.lin3(H2) # 输出层不需要激活函数# 定义神经网络(简化版)
class Net_Simple(nn.Module):def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2):super(Net_Simple, self).__init__()self.net = nn.Sequential(nn.Flatten(),nn.Linear(num_inputs, num_hiddens1),nn.ReLU(),nn.Dropout(0.2),nn.Linear(num_hiddens1, num_hiddens2),nn.ReLU(),nn.Dropout(0.5),nn.Linear(num_hiddens2, num_outputs))def forward(self, X):return self.net(X)# 权重初始化函数
def init_weights(m):if type(m) == nn.Linear:nn.init.normal_(m.weight, std=0.01)# 主程序
if __name__ == "__main__":# 参数设置num_epochs, lr, batch_size = 10, 0.5, 256num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256# 加载数据train_iter, test_iter = load_data_fashion_mnist(batch_size)# 定义复杂模型、损失函数和优化器net = Net(num_inputs, num_outputs, num_hiddens1, num_hiddens2)loss = nn.CrossEntropyLoss(reduction='none')trainer = torch.optim.SGD(net.parameters(), lr=lr)# 开始训练复杂模型print("Training complex model...")train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)print("===========================================================")# 定义简化模型、损失函数和优化器net_simple = Net_Simple(num_inputs, num_outputs, num_hiddens1, num_hiddens2)net_simple.apply(init_weights) # 初始化权重loss = nn.CrossEntropyLoss(reduction='none')trainer = torch.optim.SGD(net_simple.parameters(), lr=lr)# 开始训练简化模型print("Training simple model...")train_ch3(net_simple, train_iter, test_iter, loss, num_epochs, trainer)