处理过拟合-权重衰退和dropout
最常见的处理过拟合的方法:控制模型的容量
- 将模型变得比较小,减少里面的参数的数量
- 缩小参数值的取值范围
1. 权重衰退(Weight decay)
1.1 方法
在训练参数化机器学习模型时, 权重衰减(weight decay)是最广泛使用的正则化的技术之一, 它通常也被称为 L 2 L_2 L2正则化。 这项技术通过函数与零的距离来衡量函数的复杂度, 因为在所有函数 f f f中,函数 f = 0 f = 0 f=0(所有输入都得到值) 在某种意义上是最简单的。 但是我们应该如何精确地测量一个函数和零之间的距离呢? 没有一个正确的答案。
一种简单的方法是通过线性函数 f ( x ) = W T x f(x) = W^Tx f(x)=WTx中的权重向量的某个范数来度量其复杂性, 例如 ∣ ∣ W ∣ ∣ 2 ||W||^2 ∣∣W∣∣2。 要保证权重向量比较小, 最常用方法是将其范数作为惩罚项加到最小化损失的问题中。 将原来的训练目标最小化训练标签上的预测损失, 调整为最小化预测损失和惩罚项之和。 现在,如果我们的权重向量增长的太大, 我们的学习算法可能会更集中于最小化权重范数 ∣ ∣ W ∣ ∣ 2 ||W||^2 ∣∣W∣∣2。
对于损失函数
L
(
W
,
b
)
L(W,b)
L(W,b)来说,在这个基础上添加一个
L
2
L_2
L2罚函数项,同时为了平衡这个新的额外惩罚的损失,通过正则项权重
λ
\lambda
λ来描述这种权衡,因此带有
L
2
L_2
L2罚函数项的新损失函数为:
L
(
W
,
b
)
+
λ
2
∣
∣
W
∣
∣
2
L(W,b) + \frac{\lambda}{2}||W||^2
L(W,b)+2λ∣∣W∣∣2
正则项权重 λ \lambda λ是控制模型复杂度的超参数,其是非负的。
1.2 参数更新法则
计算梯度:
∂
∂
W
(
L
(
W
,
b
)
+
λ
2
∣
∣
W
∣
∣
2
)
=
∂
L
(
W
,
b
)
∂
W
+
λ
W
\frac{\partial}{\partial W} (L(W,b) + \frac{\lambda}{2}||W||^2)= \frac{\partial L(W,b)}{\partial W} + \lambda W
∂W∂(L(W,b)+2λ∣∣W∣∣2)=∂W∂L(W,b)+λW
更新参数:
W
t
+
1
=
(
1
−
η
λ
)
w
i
−
η
∂
L
(
W
t
,
b
)
∂
W
t
W_{t+1} = (1 - \eta \lambda) w_i - \eta\frac{\partial L(W_t,b)}{\partial W_t}
Wt+1=(1−ηλ)wi−η∂Wt∂L(Wt,b)
通常 η λ \eta \lambda ηλ<1;可以看出,相较于不使用罚函数的参数更新来说,使用了罚函数的会先将 W t W_t Wt减小,再减去对应的梯度值,因此称为权重衰退。
1.3 代码实现
代码来自:动手学深度学习 v2
%matplotlib inline
import torch
from torch import nn
from d2l import torch as d2l
n_train, n_test, num_inputs, batch_size = 20, 100, 200, 5
true_w, true_b = torch.ones((num_inputs, 1)) * 0.01, 0.05
train_data = d2l.synthetic_data(true_w, true_b, n_train)
train_iter = d2l.load_array(train_data, batch_size)
test_data = d2l.synthetic_data(true_w, true_b, n_test)
test_iter = d2l.load_array(test_data, batch_size, is_train=False)
def init_params():
w = torch.normal(0, 1, size=(num_inputs, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
return [w, b]
def train_concise(wd):
net = nn.Sequential(nn.Linear(num_inputs, 1))
for param in net.parameters():
param.data.normal_()
loss = nn.MSELoss(reduction='none')
num_epochs, lr = 100, 0.003
# 偏置参数没有衰减
trainer = torch.optim.SGD([
{"params":net[0].weight,'weight_decay': wd},
{"params":net[0].bias}], lr=lr)
animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log',
xlim=[5, num_epochs], legend=['train', 'test'])
for epoch in range(num_epochs):
for X, y in train_iter:
trainer.zero_grad()
l = loss(net(X), y)
l.mean().backward()
trainer.step()
if (epoch + 1) % 5 == 0:
animator.add(epoch + 1,
(d2l.evaluate_loss(net, train_iter, loss),
d2l.evaluate_loss(net, test_iter, loss)))
print('w的L2范数:', net[0].weight.norm().item())
train_concise(0)
当 λ = 0 \lambda = 0 λ=0时,训练误差有了减少,但测试误差没有减少, 这意味着出现了严重的过拟合。
train_concise(3)
当使用权重衰减来运行代码。 训练误差虽然会增大,但测试误差减小。 这正是我们期望从正则化中得到的效果。
2. 丢弃法(Dropout)
对于
X
X
X中加入噪音
ϵ
\epsilon
ϵ得到
X
′
X'
X′,我们希望:
E
[
X
′
]
=
X
E[X'] = X
E[X′]=X
丢弃法中,对每个元素进行如下的扰动:
x
i
′
=
{
0
if with probablity
p
x
i
1
−
p
otherise
x_i' = \begin{cases} 0 & \text{if with probablity }p \\ \frac{x_i}{1-p}& \text{otherise} \end{cases}
xi′={01−pxiif with probablity potherise
这样的得到的模型其期望着不变,因为:
E
[
x
i
′
]
=
p
×
0
+
x
i
1
−
p
×
(
1
−
p
)
=
x
i
E[x_i'] = p \times 0 + \frac{x_i}{1-p} \times (1-p) = x_i
E[xi′]=p×0+1−pxi×(1−p)=xi
2.1 实际中的丢弃法
当我们将暂退法应用到隐藏层,以 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,\cdots,h_5 h1,⋯,h5的任何一个元素。
注意是dropout hidden layer的输出
2.2 代码实现
import torch
from torch import nn
from d2l import torch as d2l
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)
num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256
dropout1, dropout2 = 0.2, 0.5
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.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)
2.3 小结
- dropout仅在训练期间使用。它只会对权重产生影响,在预测的时候,权重不需要发生变化的情况下,不需要正则,所以在推理中不需要正则,确保在推理过程中能够有一个确定性的输出
- dropout在前向传播过程中,计算每一内部层的同时丢弃一些神经元。
- dropout常用在MLP的隐藏层输出上,一般是用在全连接层上,在卷积层不会使用
这里是引用
。