权重衰退 + 丢弃法
权重衰退(Weight Decay)
回忆:如何控制一个模型的容量(方法1:把模型变得比较小;方法2:使得模型参数的选择范围比较小)
是一种正则化模型的技术;
使用均方范数作为硬性限制
- 通过限制参数值的选择范围来控制模型的容量
(最小化损失的时候,限制权重向量 w 的范数平方不能超过 θ)- 通常不限制偏移b(限不限制都差不多)
- 小的 θ 意味着更强的正则项
-
直观解释理解: θ 确实是把w限制在一个范围里面,但实际上使用以下的柔性限制。
使用均方范数作为柔性限制
- 对每个 θ ,都可以找到 λ 使得之前的目标函数等价于下面:
- 可以通过拉格朗日乘子来证明
- 超参数λ控制了正则项的重要程度
- λ = 0:无作用
- λ → ∞,w* → 0
对最优解的影响:
理解:橙色是惩罚项:lamda/2*||w||平方的图像,绿色是原来损失函数的图像;曲线代表损失函数,有几个圈就是对不同的w来说,一个圈对应一个参数,最中间的点是参数的最优解。
大概应该就是:新的损失函数由两项组成,此时求导后,梯度有两项了,一项将w向绿线中心拉,一项将w向原点拉进,最后将在w*点达到一个平衡
总结
- 权重衰退通过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, 5true_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_lossnum_epochs, lr = 100, 0.003animator = 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来控制模型复杂度
- 常作用在多层感知机的隐藏层输出上
- 丢弃概率是控制模型复杂度的超参数
代码实现
# Dropoutimport torch
from torch import nn
from d2l import torch as d2ldef dropout_layer(X, dropout):assert 0 <= dropout <= 1# 在本情况中,所有元素都被丢弃if dropout == 1:return torch.zeros_like(X)# 在本情况中,所有元素都被保留if dropout == 0:return Xmask = (torch.randn(X.shape) > dropout).float()# mask 是一个与输入张量 X 形状相同的浮点类型张量,其中元素的值要么是 1.0,要么是 0.0return 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.5class 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_inputsself.training = is_trainingself.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))))# 只有在训练模型的时候才使用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)
-
结果:
-
使用 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主要是针对过拟合问题的,这个模型没有过拟合所以没必要简化模型。