深度学习笔记(六):暂退法

1995年,Christopher Bishop证明了具有输入噪声的训练等价于吉洪诺夫正则化(Tikhonov regularization)。这项工作用数学证明了”要求函数平滑“和”要求函数对输入的随机噪声具有适应性“之间的联系。

前言

造成过拟合的情况分为如下三类:

  • 权重值的范围太大,导致模型极其不稳定。
  • 对于简单的数据选取的模型过于复杂,比如隐藏层过多,隐藏层的神经元过多。
  • 训练样本过少,导致模型对少样本完全拟合,对于新样本极其陌生。

针对第一类问题,我们采用权重衰退的方法优化,而第二类问题则需要对神经网络中的神经元进行必要的淘汰(即”暂退法“)。

暂退法(Dropout)是一种在深度学习中常用的正则化技术,它通过在训练过程中随机丢弃(置零)网络中的一部分神经元(包括它们的连接),来防止模型的过拟合。这种方法可以简单理解为每一次训练迭代时,网络都会以一定的概率随机地“瘦身”,只使用部分神经元进行前向传播和后向传播。通过这种方式,模型可以学习到更加鲁棒的特征表示。

保持中间期望值的不变性是暂退法的一个重要设计,原因包括:

  1. 避免放缩影响: 在使用暂退法时,由于一部分神经元被随机丢弃,直接导致了网络某一层的输出在数量级上减少了。如果在测试时仍以全量的神经元进行计算,而不对这种减少进行调整,就会导致测试时网络的行为与训练时不一致。为了解决这一问题,通常在训练时对那些未被丢弃的神经元的输出进行放缩(通常是乘以一个与丢弃概率相反的因子),以保证网络的输出期望不变。这样可以在训练和测试时保持网络的行为一致性。

  2. 维持网络性能: 暂退法通过随机丢弃神经元的方式来增强模型的泛化能力,但如果不通过调整输出以保持期望值不变,就可能导致网络的学习效率下降,因为网络的输出分布会随着丢弃率的变化而发生显著变化,这对于后续层的学习是不利的。通过保持输出期望值的不变,可以减少这种影响,帮助网络更稳定地学习。

  3. 简化模型评估: 如果训练和测试时网络行为大为不同,将很难评估模型的真实性能。保持中间期望值不变简化了从训练模式到测试模式的转换,因为在测试时不使用暂退法,但通过在训练时调整保持期望值不变,可以使得训练和测试时的网络行为更加一致,从而使得模型评估更为准确和简单。

在使用暂退法(Dropout)时,通常会关注两个关键的期望值:训练时保留的神经元的输出期望值 $h’$ 和在不使用暂退法(即不丢弃任何神经元)时神经元的输出期望值 $h$ 。为了确保模型在训练和测试时表现的一致性,需要在训练时对 $h’$ 进行适当的缩放,使得其期望值等于 $h$ 。

假设在训练时,每个神经元被保留(即不被丢弃)的概率是 $p$ (因此,被丢弃的概率是 $1-p$ )。那么,为了保持期望值不变,输出 $h’$ 应该被缩放一个因子,以补偿因丢弃神经元而减少的输出。这个缩放因子是 $1/p$ ,因为在期望意义下,只有 $p$ 比例的神经元在任何时间点被激活。

暂退法的原始论文提到了一个有关有性生殖的类比;神经网络过拟合与每一层都依赖前一层的激活值有关,这种情况称之为”共适应性“。共适应性(Co-adaptation)在机器学习领域通常指的是模型的不同部分在训练过程中相互适应,以优化整体性能的现象。这种相互适应可以是有益的,比如在深度学习中,网络的不同层次通过共适应学习到更有效的特征表示。然而,共适应性也可能导致模型对训练数据过度特化,这在一定程度上与过拟合的概念相关联。我们认为,暂退法会破坏共适应性,就像有性生殖会破坏共适应的基因一样。

暂退法的从头实现

导包

1
2
3
import torch
from torch import nn
from d2l import torch as d2l

丢失过程的定义

1
2
3
4
5
6
7
8
def dropout_layer(X, dropout):
assert 0<= dropout <=1
if dropout == 1:
return torch.zeros_like(X)
if dropout == 0:
return X
mask = (torch.rand(X.shape) > dropout).float()
return mask*X/(1.0-dropout)

这个函数定义了一个名为 dropout_layer 的操作,它实现了在深度学习中常用的 Dropout 技术,用于防止模型过拟合。下面逐行解释这个函数的代码:

  1. def dropout_layer(X, dropout):

    • 这行定义了函数 dropout_layer,它接受两个参数:XdropoutX 是输入的数据(通常是一个张量),dropout 是一个介于 0 和 1 之间的浮点数,表示 dropout 率,即随机丢弃神经网络中某些节点的比例。
  2. assert 0<= dropout <=1

    • 这行代码是一个断言语句,用来确保传入的 dropout 参数值在 0 到 1 之间。如果 dropout 不满足这个条件,程序将抛出异常。
  3. if dropout == 1:

    • 这行代码检查 dropout 是否等于 1。如果等于 1,意味着需要丢弃所有节点。
  4. return torch.zeros_like(X)

    • 如果 dropout 等于 1,这行代码将返回一个与输入 X 形状相同但所有元素都是 0 的张量。这表示所有的输入节点都被丢弃了。
  5. if dropout == 0:

    • 这行代码检查 dropout 是否等于 0。如果等于 0,意味着不丢弃任何节点。
  6. return X

    • 如果 dropout 等于 0,这行代码直接返回输入的张量 X,表示保留所有节点,不进行任何丢弃。
  7. mask = (torch.rand(X.shape) > dropout).float

    • 这行代码首先使用 torch.rand(X.shape) 生成一个与 X 形状相同的随机张量,其元素值在 0 到 1 之间。然后,这个随机张量与 dropout 进行比较,得到一个布尔型张量,其中大于 dropout 的元素被标记为 True(这些是将被保留的节点),否则为 False(这些节点将被丢弃)。最后,调用 .float() 方法将布尔型张量转换为浮点型张量,True 转换为 1.0,False 转换为 0.0,生成了一个掩码(mask)张量。
  8. return mask*X/(1.0-dropout)

    • 这行代码首先使用前面生成的掩码张量 mask 与输入 X 进行元素乘法,这样被标记为 0(即应该被丢弃)的节点的值将变为 0,而被保留的节点值不变。然后,将结果除以 (1.0-dropout) 进行缩放,以保持激活的总和的期望值不变。这是一种常见的做法,用以补偿因为部分节点的丢弃而可能导致的激活总量的减少。

这个函数通过随机丢弃网络中的一部分节点,帮助防止模型在训练过程中过拟合。

可能你会注意到,这里丢弃的神经元比率似乎并不是dropout,事实上,所谓dropout指的是每个神经元被丢弃的概率。

参数和模型的定义

定义参数

1
2
num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256
dropout1, dropout2 = 0.2, 0.5

模型定义

在这里,我们继承了nn.Module定义一个新的类Net。继承自 nn.Module,使用了 PyTorch 框架。

类定义与初始化方法

1
class Net(nn.Module):
  • 定义了一个名为 Net 的新类,这个类继承自 PyTorch 的 nn.Module 类。nn.Module 是所有神经网络模块的基类,你的网络应该也继承自这个类。
1
def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2, is_training = True):
  • 这是 Net 类的初始化方法,用于创建类的实例。它接受五个参数:输入层的大小 (num_inputs)、输出层的大小 (num_outputs)、两个隐藏层的大小 (num_hiddens1num_hiddens2),以及一个标志 is_training 指示网络是否处于训练模式。
1
super(Net, self).__init__()
  • 这行代码调用了父类 nn.Module 的初始化方法。这是继承中常见的做法,用于确保父类被正确初始化。
1
self.num_inputs = num_inputs self.num_outputs = num_outputs self.training = is_training
  • 这三行代码将传入的参数值分别赋给实例变量,以便后续使用。
1
self.lin1 = nn.Linear(num_inputs,num_hiddens1) self.lin2 = nn.Linear(num_hiddens1,num_hiddens2) self.lin3 = nn.Linear(num_hiddens2,num_outputs)
  • 这三行定义了网络中的三个线性层(也称为全连接层)。nn.Linear 创建一个线性转换层,它的参数分别指定了输入和输出特征的数量。

    1
    self.relu = nn.ReLU()
  • 创建了一个ReLU激活函数实例,用于在网络的隐藏层之后添加非线性。

    前向传播方法

1
def forward(self, X):
  • 定义了一个名为 forward 的方法,它覆盖了父类 nn.Moduleforward 方法。这是模型定义数据如何通过网络的关键部分。
1
H1 = self.relu(self.lin1(X.reshape((-1,self.num_inputs))))
  • 首先,输入 X 被重塑为一个二维张量,其中第二维大小为 num_inputs。然后,数据通过第一个线性层 lin1,最后通过ReLU激活函数。结果是第一个隐藏层的输出。
1
2
if self.training == True:
H1 = dropout_layer(H1,dropout1)
  • 如果 self.training 为真,则对第一个隐藏层应用dropout。这里似乎有一个小错误:dropout_layerdrop1 没有在前面的代码中定义。
1
H2 = self.relu(self.lin2(H1))
  • 第一个隐藏层的输出 H1 通过第二个线性层 lin2,然后通过ReLU激活函数。结果是第二个隐藏层的输出。
1
2
if self.training == True:
H2 = dropout_layer(H2,dropout2)
  • 同样地,如果 self.training 为真,则对第二个隐藏层应用dropout。这里同样存在一个未定义的 dropout_layerdrop2 的问题。
1
2
out = self.lin3(H2)
return out
  • 第二个隐藏层的输出 H2 通过第三个线性层 lin3,没有激活函数,结果是网络的最终输出。
  • 返回网络的最终输出。

网络实例化

1
net = Net(num_inputs,num_outputs,num_hiddens1,num_hiddens2)

代码合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class 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_inputs
self.num_outputs = num_outputs
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))))
if self.training == True:
H1 = dropout_layer(H1,dropout1)
H2 = self.relu(self.lin2(H1))
if self.training == True:
H2 = dropout_layer(H2,dropout2)
out = self.lin3(H2)
return out

net = Net(num_inputs,num_outputs,num_hiddens1,num_hiddens2)

模型训练

从上面的推导我们可以发现,我们想要改动网络架构进行训练过程的优化,只需要在net上做文章即可,事实上,后面的高级API实现也证明了这一点。

1
2
3
4
5
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)

暂退法的高级API实现

只需要单独定义net和初始化参数,训练过程等从前。事实上,PyTorch的优势在于把网络用连续过程进行定义,十分的直观。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
net = nn.Sequential(nn.Flatten(),
nn.Linear(784,256),
nn.ReLU()
nn.Dropout(0.2),
nn.Linear(256,256),
nn.ReLU()
nn.Dropout(0.5),
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)

欢迎关注我的其它发布渠道