注:书中对代码的讲解并不详细,本文对很多细节做了详细注释。另外,书上的源代码是在Jupyter Notebook上运行的,较为分散,本文将代码集中起来,并加以完善,全部用vscode在python 3.9.18下测试通过。
Chapter4 Multilayer Perceptron
4.6 Dropout Regularization
4.6.1 Reexamine Overfitting
当面对更多的特征而样本不足时,线性模型往往会过拟合。相反,当给出更多样本而不是特征,通常线性模型不会过拟合。不幸的是,线性模型泛化的可靠性是有代价的,即线性模型没有考虑到特征之间的交互作用。对于每个特征,线性模型必须指定正的或负的权重,而忽略其他特征。
泛化性和灵活性之间的这种基本权衡被描述为偏差-方差权衡(bias-variance tradeoff)。线性模型有很高的偏差,然而,这些模型的方差很低,即它们在不同的随机数据样本上可以得出相似的结果。
深度神经网络位于偏差-方差谱的另一端。与线性模型不同,神经网络并不局限于单独查看每个特征,而是学习特征之间的交互。例如,神经网络可能推断"尼日利亚"和"西联汇款"一起出现在电子邮件中表示垃圾邮件,但单独出现则不表示垃圾邮件。
即使我们有比特征多得多的样本,深度神经网络也有可能过拟合。本节中,我们将探究改进深层网络的泛化性的工具。
4.6.2 Robustness of Disturbances
经典泛化理论认为,为了缩小训练和测试性能之间的差距,应该以简单的模型为目标。简单性以较小维度的形式展现,参数的范数也代表了一种有用的简单性度量。简单性的另一个角度是平滑性,即函数不应该对其输入的微小变化敏感。例如,当我们对图像进行分类时,我们预计向像素添加一些随机噪声应该是基本无影响的。
在训练过程中,我们可以在计算后续层之前向网络的每一层注入噪声。因为当训练一个有多层的深层网络时,注入噪声只会在输入-输出映射上增强平滑性。这个想法被称为暂退法 (dropout)。暂退法在前向传播过程中,计算每一内部层的同时注入噪声,这已经成为训练神经网络的常用技术。这种方法之所以被称为暂退法 ,因为我们从表面上看是在训练过程中丢弃一些神经元。在整个训练过程的每一次迭代中,标准暂退法包括在计算下一层之前将当前层中的一些节点置零。
关键的挑战是如何注入这种噪声。一种想法是以一种无偏向 (unbiased)的方式注入噪声。这样在固定住其他层时,每一层的期望值等于没有噪音时的值。我们可以在每次训练迭代中,从均值为零的分布 ϵ ∼ N ( 0 , σ 2 ) \epsilon \sim \mathcal{N}(0,\sigma^2) ϵ∼N(0,σ2)采样噪声添加到输入 x \mathbf{x} x,从而产生扰动点 x ′ = x + ϵ \mathbf{x}' = \mathbf{x} + \epsilon x′=x+ϵ,预期是 E [ x ′ ] = x E[\mathbf{x}'] = \mathbf{x} E[x′]=x。
在标准暂退法正则化中,通过按保留(未丢弃)的节点的分数进行规范化来消除每一层的偏差,如下所示:
h ′ = { 0 概率为 p h 1 − p 其他情况 \begin{aligned} h' = \begin{cases} 0 & \text{ 概率为 } p \\ \frac{h}{1-p} & \text{ 其他情况} \end{cases} \end{aligned} h′={01−ph 概率为 p 其他情况
根据此模型的设计,其期望值保持不变,即 E [ h ′ ] = h E[h'] = h E[h′]=h。
4.6.3 Implementation
当我们将暂退法应用到隐藏层,以 p p p的概率将隐藏单元置为零时,结果可以看作一个只包含原始神经元子集的网络。比如在下图中,删除了 h 2 h_2 h2和 h 5 h_5 h5,因此输出的计算不再依赖于 h 2 h_2 h2或 h 5 h_5 h5,并且它们各自的梯度在执行反向传播时也会消失。这样,输出层的计算不会过度依赖于 h 1 , ... , h 5 h_1, \ldots, h_5 h1,...,h5的任何一个元素。
通常,我们在测试时不用暂退法,然而也有一些例外,比如一些研究人员在测试时使用暂退法,用于估计神经网络预测的"不确定性":如果通过许多不同的暂退法遮盖后得到的预测结果都是一致的,那么我们可以说网络发挥更稳定。
import torch
from torch import nn
from d2l import torch as d2l
import matplotlib.pyplot as plt
#以dropout的概率丢弃X中的元素
def dropout_layer(X, dropout):
assert 0 <= dropout <= 1
# 在本情况中,所有元素都被丢弃
if dropout == 1:
return torch.zeros_like(X)
# 在本情况中,所有元素都被保留
if dropout == 0:
return X
#从均匀分布$U[0, 1]$中抽取样本,使得样本和节点一一对应,然后保留那些对应样本大于p的节点
mask = (torch.rand(X.shape) > dropout).float()
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))
#print(dropout_layer(X, 1.))
#定义模型
num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256
dropout1, dropout2 = 0.2, 0.5#常见的技巧是在靠近输入层的地方设置较低的暂退概率
class Net(nn.Module):#indicates that Net is inheriting from the nn.Module class
def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2,is_training = True):
super(Net, self).__init__()
self.num_inputs = num_inputs
self.training = is_training
self.lin1 = nn.Linear(num_inputs, num_hiddens1)
self.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))))
# 只有在训练模型时才使用dropout
if 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 out
net = 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)
plt.show()
#简洁实现
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)
plt.show()