跟着李沐老师学习深度学习(七)
权重衰退 + 丢弃法
权重衰退(Weight Decay)
回忆:如何控制一个模型的容量(方法1:把模型变得比较小;方法2:使得模型参数的选择范围比较小)
是一种正则化模型的技术;
使用均方范数作为硬性限制
- 通过限制参数值的选择范围来控制模型的容量
(最小化损失的时候,限制权重向量 w 的范数平方不能超过 θ)- 通常不限制偏移b(限不限制都差不多)
- 小的 θ 意味着更强的正则项
-
直观解释理解: θ 确实是把w限制在一个范围里面,但实际上使用以下的柔性限制。
使用均方范数作为柔性限制
- 对每个 θ ,都可以找到 λ 使得之前的目标函数等价于下面:
- 可以通过拉格朗日乘子来证明
- 超参数λ控制了正则项的重要程度
- λ = 0:无作用
- λ → ∞,w* → 0
对最优解的影响:
理解:橙色是惩罚项:lamda/2*||w||平方的图像,绿色是原来损失函数的图像;曲线代表损失函数,有几个圈就是对不同的w来说,一个圈对应一个参数,最中间的点是参数的最优解。
大概应该就是:新的损失函数由两项组成,此时求导后,梯度有两项了,一项将w向绿线中心拉,一项将w向原点拉进,最后将在w*点达到一个平衡
-
参数更新法则
-
计算梯度
-
时间 t 更新参数
- 通常 nλ<1,在深度学习中通常叫做权重衰退
- 主要变化:先把当前的值 缩小再 沿着负梯度方向学习
总结
- 权重衰退通过L2 正则项使得模型参数不会过大,从而控制模型复杂度
- 正则项权重是控制模型复杂度的超参数。
代码实现- 从零实现
# 权重衰退
# 像之前一样 导入相关包
import torch
from torch import nn
from d2l import torch as d2l
# 像以前一样生成一些数据:
# ![image.png](attachment:image.png)
# 将训练数据设置的很小(当训练数据比较少的时候容易 过拟合)
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)
# 从零开始实现
#下面我们将从头开始实现权重衰减,只需将 𝐿2 的平方惩罚添加到原始目标函数中
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]
# 定义L2范数惩罚
# 实现这一惩罚最方便的方法是对所有项求平方后并将它们求和
def l2_penalty(w):
return torch.sum(w.pow(2)) / 2
# 定义训练代码实现:
def train(lambd):
w,b = init_params()
# 定义了一个线性回归模型,
net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss
num_epochs, lr = 100, 0.003
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:
# 增加了L2范数惩罚项
# 广播机制使 l2_penalty(w) 成为了长度为batch_size的向量
l = loss(net(X), y) + lambd * l2_penalty(w) # 唯一的区别
l.sum().backward()
d2l.sgd([w, b], lr, batch_size)
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范数是:', torch.norm(w).item())
# 忽略正则化进行训练
train(lambd = 0)
# 这里训练误差有了减少,但测试误差没有减少, 这意味着出现了严重的过拟合。
# 使用权重衰退进行 训练
train(lambd=3)
# 这里 训练误差增大,但测试误差减小;
忽略正则化得到的结果:
当没有进行正则化的时候,训练误差有了减少,但测试误差没有减少, 这意味着出现了严重的过拟合。
使用权重衰退得到的结果:
可以看出:过拟合有一定的缓解;可能经过多次迭代会得到更好的效果;
也可以进行将lambd调整更大一些,效果会更好;(或者增加迭代次数)
代码- 简洁实现
# 简洁实现
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)
-
加权重衰退
train_concise(3)
由以上结果可以看到结果和之前是差不多的。
丢弃法(dropout)
动机
- 一个好的模型需要对输入数据的扰动鲁棒
- 使用噪音的数据等价于Tikhonov 正则
- 丢弃法:在层之间加入噪音
(输入数据加入随机扰动可以防止过拟合,泛化性更好,等价于一种正则方法。现在对噪音的添加方式从输入位置放到了层间位置)
无偏差的加入噪音
-
对x加入噪音得到x’, 我们希望(不改变期望):
-
丢弃法对每个元素进行如下扰动:(给定一个概率p)
为什么要出一个1-p: 对向量的每个元素操作,每个元素都有p的概率变为0,又有1-p的概率变成它的1/(1-p),这样才能保持期望不变
使用丢弃法
- 通常将丢弃法作用在隐藏全连接层的输出上
推理中的丢弃法
- 正则项只在训练中使用:他们影响模型参数的更新
- 在推理中,丢弃法直接返回输入:
- 这样保证确定性的输出
比较正则化的实验结果,因此丢弃法可看作是正则化方法;
总结
- 丢弃法将一些输出项随机置0来控制模型复杂度
- 常作用在多层感知机的隐藏层输出上
- 丢弃概率是控制模型复杂度的超参数
代码实现
# Dropout
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.randn(X.shape) > dropout).float()
# mask 是一个与输入张量 X 形状相同的浮点类型张量,其中元素的值要么是 1.0,要么是 0.0
return mask * X / (1.0 - dropout)
# 测试dropout_layer函数
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):
def __init__(self, num_inputs, num_outputs, num_hiddend1, 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)
-
结果:
-
使用 dropout 都是0 的情况:
代码-简洁实现
# 简洁实现
# 只需在每个全连接层之后添加一个Dropout层, 将暂退概率作为唯一的参数传递给它的构造函数。
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)
总结:dropout主要是针对过拟合问题的,这个模型没有过拟合所以没必要简化模型。