前面我们已经介绍学习了过拟合的问题,这次我们来讲讲一个正则化模型的技术。
我们总是可以通过去收集更多的训练数据来缓解过拟合,但这可能成本很高,耗时颇多,或者完全超出我们的控制,因而在短期内不可能做到。
假设我们已经拥有了尽可能多的高质量的数据,我们便可以把重心放在正则化技术上。
在多项式回归的例子中,我们可以通过调整拟合多项式的阶数来限制模型的容量。
实际上,限制特征的数量是缓解过拟合的一种常用技术。然而,简单的丢弃特征对于这项工作来说可能过于生硬。我们继续思考多项式回归的例子,考虑高维输入可能发生的情况。 多项式对多变量数据的自然扩展称为单项式(monomials), 也可以说是变量幂的乘积。 单项式的阶数是幂的和。例如,x12x2x_1^2x_2x12x2和x3x52x_3x_5^2x3x52都是三次多项式。
注意,随着结束ddd的增长,带有结束ddd的项数迅速增加。给定kkk个变量,阶数为ddd的项的个数为(k−1+dk−1)\binom{k-1+d}{k-1}(k−1k−1+d),即Ck−1+dk−1=(k−1+d)!d!(k−1)!{C_{k-1+d}^{k-1}} =\frac{(k-1+d)!}{d!(k-1)!}Ck−1+dk−1=d!(k−1)!(k−1+d)!。因此,即便是阶数上的微小变化,比如从2到3,也会显著增加我们模型的复杂性。
在之前的学习中,我们已经接触了L2L_2L2范数和L1L_1L1范数,它们是更为一般的LpL_pLp范数的特殊情况。
在训练参数化机器学习模型时, 权重衰减(weight decay)是最广泛使用的正则化的技术之一, 它通常也被称为L2L_2L2正则化。 这项技术通过函数与零的距离来衡量函数的复杂度, 因为在所有函数fff中,函数f=0f=0f=0(所有输入都得到值0)在某种意义上是最简单的。 但是我们应该如何精确地测量一个函数和零之间的距离呢? 没有一个正确的答案。 事实上,函数分析和巴拿赫空间理论的研究,都在致力于回答这个问题。
一种简单的方法是通过线性函数f(X)=wTXf(\textbf{X})=\textbf{w}^T\textbf{X}f(X)=wTX中的权重向量的某个范数来度量其复杂性, 例如∣∣w∣∣2||\textbf{w}||^2∣∣w∣∣2。 要保证权重向量比较小, 最常用方法是将其范数作为惩罚项加到最小化损失的问题中。 将原来的训练目标最小化训练标签上的预测损失, 调整为最小化预测损失和惩罚项之和。 现在,如果我们的权重向量增长的太大, 我们的学习算法可能会更集中于最小化权重范数∣∣w∣∣2||\textbf{w}||^2∣∣w∣∣2。这正是我们想要的。我们的损失由下式给出:
L(w,b)=1n∑i=1n12(wTx(i)+b−y(i))L(\textbf{w},b)=\frac{1}{n}\sum_{i=1}^{n}\frac{1}{2}(\textbf{w}^T\textbf{x}^{(i)}+b-y^{(i)} ) L(w,b)=n1i=1∑n21(wTx(i)+b−y(i))
回想一下,x(i)\textbf{x}^{(i)}x(i)是样本iii的特征,y(i)y^{(i)}y(i)是样本iii的标签,(w,b)(\textbf{w},b)(w,b)是权重和偏置参数。 为了惩罚权重向量的大小, 我们必须以某种方式在损失函数中添加∣∣w∣∣2||\textbf{w}||^2∣∣w∣∣2, 但是模型应该如何平衡这个新的额外惩罚的损失? 实际上,我们通过正则化常数λ\lambdaλ来描述这种权衡, 这是一个非负超参数,我们使用验证数据拟合:
L(w,b)+λ2∣∣w∣∣2L(\textbf{w},b)+\frac{\lambda }{2}||\textbf{w}||^2 L(w,b)+2λ∣∣w∣∣2
对于λ=0\lambda=0λ=0,我们恢复了原来的损失函数。
对于λ>0\lambda>0λ>0,我们限制∣∣w∣∣||\textbf{w}||∣∣w∣∣的大小。
这里我们仍然除以2,当我们取一个二次函数的导数时,2和1/2会抵消掉。以确保更新表达式看起来既漂亮又简单。 你可能会想知道为什么我们使用平方范数而不是标准范数(即欧几里得距离)? 我们这样做是为了便于计算。 通过平方L2L_2L2范数,我们去掉平方根,留下权重向量每个分量的平方和。 这使得惩罚的导数很容易计算:导数的和等于和的导数。
L2L_2L2正则化回归的小批量随机梯度下降更新如下式:
w←(1−ηλ)w−η∣β∣∑i∈βx(i)(wTx(i)+b−y(i))\textbf{w}\leftarrow (1-\eta \lambda )\textbf{w}-\frac{\eta }{|\beta|}\sum_{i\in \beta}\textbf{x}^{(i)}(\textbf{w}^T\textbf{x}^{(i)}+b-y^{(i)}) w←(1−ηλ)w−∣β∣ηi∈β∑x(i)(wTx(i)+b−y(i))
根据之前章节所讲的,我们根据估计值与观测值之间的差异来更新w\textbf{w}w。 然而,我们同时也在试图将w\textbf{w}w的大小缩小到零。 这就是为什么这种方法有时被称为权重衰减。 我们仅考虑惩罚项,优化算法在训练的每一步衰减权重。 与特征选择相比,权重衰减为我们提供了一种连续的机制来调整函数的复杂度。 较小的λ\lambdaλ值对应较少约束的w\textbf{w}w, 而较大的λ\lambdaλ值对w\textbf{w}w的约束更大。
是否对相应的偏置bbb进行惩罚在不同的实践中会有所不同, 在神经网络的不同层中也会有所不同。 通常,网络输出层的偏置项不会被正则化。
我们通过一个简单的例子来演示权重衰减。
import torch
from torch import nn
from d2l import torch as d2l
首先,我们像以前一样生成一些数据,生成公式如下:
y=0.05+∑i=1d0.01xi+ϵwhereϵ∈N(0,0.012)y=0.05+\sum_{i=1}^{d}0.01x_i+\epsilon \quad where\quad \epsilon \in N(0,0.01^2) y=0.05+i=1∑d0.01xi+ϵwhereϵ∈N(0,0.012)
我们选择标签是关于输入的线性函数。 标签同时被均值为0,标准差为0.01高斯噪声破坏。 为了使过拟合的效果更加明显,我们可以将问题的维数增加到d=200d=200d=200, 并使用一个只包含20个样本的小训练集。
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) # 这里的is_train是表示是否打乱
首先,我们将定义一个函数来随机初始化模型参数。
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 l2_penalty(w):return torch.sum(w.pow(2) / 2)
下面的代码将模型拟合训练数据集,并在测试数据集上进行评估。 线性网络和平方损失没有变化, 所以我们通过d2l.linreg和d2l.squared_loss导入它们。 唯一的变化是损失现在包括了惩罚项。
def train(lambd):w, b = init_params()net, loss = lambda X : d2l.linreg(X, w, b), d2l.squared_lossnum_epochs, lr = 100, 0.01animator = 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: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))d2l.plt.show()
train(0)
输出结果为:

此时:
w的L2范数是: tensor(13.1169, grad_fn=)
选择λ=1\lambda = 1λ=1
train(1)

此时:
w的L2范数是: tensor(0.2394, grad_fn=)
由于权重衰减在神经网络优化中很常用, 深度学习框架为了便于我们使用权重衰减, 将权重衰减集成到优化算法中,以便与任何损失函数结合使用。 此外,这种集成还有计算上的好处, 允许在不增加任何额外的计算开销的情况下向算法中添加权重衰减。 由于更新的权重衰减部分仅依赖于每个参数的当前值, 因此优化器必须至少接触每个参数一次。
在下面的代码中,我们在实例化优化器时直接通过weight_decay指定weight decay超参数。 默认情况下,PyTorch同时衰减权重和偏移。 这里我们只为权重设置了weight_decay,所以偏置参数bbb不会衰减。
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.01trainer = 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) % 10 == 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())d2l.plt.show()
无正则化的时候:
train_concise(0)

w的L2范数: tensor(13.3643, grad_fn=)
加上正则化的时候:
train_concise(3)

w的L2范数: tensor(0.1036, grad_fn=)
# 通过一个例子来演示权重衰减
import torch
from torch import nn
from d2l import torch as d2ldef 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 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.01animator = 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: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))d2l.plt.show()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.01trainer = 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) % 10 == 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())d2l.plt.show()if __name__ == '__main__':n_train, n_test, num_inputs, batch_size = 20, 100, 200, 5true_w, true_b = torch.ones((num_inputs, 1)) * 0.01, 0.05train_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) # 这里的is_train是表示是否打乱train_concise(3)
下一篇:计算机网络(下)