过拟合问题的解决
过拟合是机器学习和深度学习中一个常见的问题,它发生在模型在训练数据上表现很好,但在未见过的新数据上表现不佳的情况下。以下是一些处理过拟合的常见方法:
- 更多数据集:最直接的方法是增加训练数据。更多的数据可以帮助模型更好地学习数据的真实分布,减少过拟合的风险。
- 简化模型:减少模型的复杂度是防止过拟合的关键。可以考虑以下方法:
- 减少模型的参数数量。
- 减小神经网络的层数。
- 减小决策树的深度。
- 正则化:正则化是通过向损失函数添加额外的项来限制模型参数的大小,从而减少过拟合的风险。常见的正则化技术包括:
- L1正则化:通过向损失函数添加L1范数的惩罚项,推动模型参数趋向于稀疏值。
- L2正则化:通过向损失函数添加L2范数的惩罚项,推动模型参数趋向于较小的值。
- Dropout:在训练过程中随机丢弃一部分神经元,以减少神经网络的复杂性。
- 交叉验证:使用交叉验证来评估模型的性能。这可以帮助检测模型是否过拟合,并调整模型的超参数。
- 提前停止:在训练过程中,监控验证集的性能,并在性能不再提高时停止训练,以防止模型继续过拟合训练数据。
- 特征选择:选择最重要的特征,以减少输入特征的维度,从而降低模型的复杂性。
- 集成方法:使用集成学习技术,如随机森林或梯度提升树,可以减少过拟合风险,因为它们结合多个模型的预测。
- 数据增强:对训练数据进行随机变换或扩充,以增加数据的多样性,有助于模型泛化到未见过的数据。
- 特征工程:精心设计和选择特征可以改善模型的泛化性能。
- 模型选择:尝试不同类型的模型,选择最适合问题的模型架构。
- 监督模型复杂性:确保模型不会过于复杂,适应问题的实际复杂性。
这里我们选择控制模型的容量。有两种办法:
- 减小模型大小:最直接的方法是减小模型的规模,即减少模型中可学习参数的数量。
- 缩小参数值的取值范围:有助于提高模型的稳定性和泛化性能,防止模型在训练过程中发生梯度爆炸或梯度消失等问题。
这里我们学习怎么缩小参数值的取值范围。
范数
范数(Norm)是线性代数中的一个概念,用来衡量向量的大小或长度。范数是一个非负的标量值,通常表示为 ||x||,其中 x 是向量。不同的范数定义方式会导致不同的向量大小度量方式,常见的范数包括:
- L1 范数(曼哈顿范数): L1 范数计算向量中所有元素的绝对值之和。对于一个 n 维向量 x,L1 范数定义如下: 或
- L2 范数(欧几里德范数): L2 范数计算向量中所有元素的平方和的平方根。对于一个 n 维向量 x,L2 范数定义如下: 或
- 无穷范数: 无穷范数计算向量中所有元素的绝对值,并返回其中最大的绝对值。对于一个 n 维向量 x,无穷范数定义如下:
这些范数都有各自的特点和应用场景。例如,L1 范数在稀疏性相关问题中常被用到,因为它倾向于将某些元素归零,从而产生稀疏解。而L2 范数在机器学习中常用于正则化,它有助于防止模型过拟合。无穷范数则用于寻找向量中绝对值最大的元素,常用于一些优化问题中。
总之,范数是一个重要的数学概念,可以用来衡量向量的大小,并在许多领域中有广泛的应用。不同的范数选择会导致不同的向量度量方式,适合不同的问题和需求。
权重衰减
在训练参数化机器学习模型时, 权重衰减(weight decay)是最广泛使用的正则化的技术之一, 它通常也被称为L2正则化,用于防止神经网络模型过拟合。过拟合是指模型在训练数据上表现良好,但在未见过的测试数据上表现较差的现象。权重衰减通过对模型的权重参数施加惩罚,防止它们变得过大,从而抑制模型的复杂性。
这项技术通过函数与零的距离来衡量函数的复杂度。一种简单的方法是通过线性函数中的权重向量的某个范数来度量其复杂性, 例如。要保证权重向量比较小, 最常用方法是将其范数作为惩罚项加到最小化损失的问题中。 将原来的训练目标最小化训练标签上的预测损失, 调整为最小化预测损失和惩罚项之和。 现在,如果我们的权重向量增长的太大, 我们的学习算法可能会更集中于最小化权重范数。
权重衰减的数学形式通常是在模型的原始损失函数上加上权重参数的平方和,乘以一个正的超参数。新的损失函数可以表示为:
- 时,恢复了原来的损失函数。
- 时,就等价于硬性限制中θ趋向于0,使得最优解w*也会慢慢趋向于0
- 可以通过增加λ来控制模型的复杂度(让模型不要太复杂) 较小的λ值对应较少约束的w, 而较大的λ值对w的约束更大。
公式解惑:
- 为什么除以2? 当我们取一个二次函数的导数时, 2和1/2会抵消,以确保更新表达式看起来既漂亮又简单。
- 为什么在这里我们使用平方范数而不是标准范数(即欧几里得距离)? 我们这样做是为了便于计算。 通过平方L2范数,我们去掉平方根,留下权重向量每个分量的平方和。 这使得惩罚的导数很容易计算:导数的和等于和的导数。 选择L2范数的一个主要原因是,它会对权重向量的大分量施加较大的惩罚,导致学习算法倾向于在多个特征上分布权重,从而使模型对观测误差更加稳定。与之相反,L1范数会使模型的权重集中在少数特征上,其他权重则会被置为零,这一特性也被称为特征选择,这在某些场景下可能是有用的。
岭回归和套索回归
岭回归(Ridge Regression)和套索回归(Lasso Regression)都是线性回归的扩展,用于处理共线性问题,并有助于防止过拟合。
-
岭回归:
岭回归在线性回归的基础上,对系数(权重)加入了L2正则化项。其优化目标函数为: 其中,X是输入数据,w是权重向量,y是目标输出,α是正则化系数,用于控制正则化的强度。岭回归通过加入L2正则项,对权重向量的大分量施加了较大的惩罚,这有助于防止模型过拟合,提高模型的泛化能力。
-
套索回归:
套索回归也是在线性回归的基础上加入了正则化项,但它加入的是L1正则化项。其优化目标函数为: 其中,表示权重向量的L1范数,即权重绝对值之和。与岭回归不同,套索回归的L1正则化会导致部分权重精确为0,这就实现了特征选择。这意味着模型会自动选择对目标变量有影响的特征,而忽略不重要的特征。
这两种回归方法都是通过引入正则化项来约束模型的复杂度,从而避免过拟合,提高模型的泛化能力。不过,它们的正则化形式不同,导致了模型的不同性质:岭回归倾向于分散权重,而套索回归则倾向于产生稀疏权重。
高维线性回归
我们通过一个简单的例子来演示权重衰减。
%matplotlib inline
import torch
from torch import nn
from d2l import torch as d2l
首先,我们像以前一样生成一些数据,生成公式如下:
我们选择标签是关于输入的线性函数。 标签同时被均值为0,标准差为0.01高斯噪声破坏。 为了使过拟合的效果更加明显,我们可以将问题的维数增加到d=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
# true_w 是一个形状为 (num_inputs, 1)d的张量,表示0.01权重
# true_b 是一个标量,表示0.05偏置。
train_data = d2l.synthetic_data(true_w, true_b, n_train)
#使用 true_w 和 true_b 来生成具有噪声的合成数据集,然后将这个数据集赋值给 train_data。
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)
从零开始实现
下面我们将从头开始实现权重衰减,只需将L2的平方惩罚添加到原始目标函数中。
初始化模型参数
首先,我们将定义一个函数来随机初始化模型参数。
def init_params():
w = torch.normal(0, 1, size=(num_inputs, 1), requires_grad=True)
# 从正态分布中抽取随机数填充该张量。该正态分布的均值是 0,标准差是 1。
b = torch.zeros(1, requires_grad=True)
return [w, b]
定义L2范数惩罚
实现这一惩罚最方便的方法是对所有项求平方后并将它们求和。
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_loss
num_epochs, lr = 100, 0.003 # 训练轮数、学习率
animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log',
xlim=[5, num_epochs], legend=['train', 'test'])
# 创建一个 Animator 对象来可视化训练和测试的损失
for epoch in range(num_epochs): # 开始进行训练,循环100轮
for X, y in train_iter: # 遍历训练数据集中的每个批次
# 增加了L2范数惩罚项,
# 广播机制使l2_penalty(w)成为一个长度为batch_size的向量
l = loss(net(X), y) + lambd * l2_penalty(w)
# 计算模型在当前批次的损失,加上L2范数惩罚项
l.sum().backward() # 对损失求和并进行反向传播,计算梯度
d2l.sgd([w, b], lr, batch_size) # 使用随机梯度下降(SGD)来更新参数
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())
忽略正则化直接训练
我们现在用lambd = 0
禁用权重衰减后运行这个代码。 注意,这里训练误差有了减少,但测试误差没有减少, 这意味着出现了严重的过拟合。
train(lambd=0)
Out: w的L2范数是: 12.963241577148438
使用权重衰减
下面,我们使用权重衰减来运行代码。 注意,在这里训练误差增大,但测试误差减小。 这正是我们期望从正则化中得到的效果。
train(lambd=3)
Out: w的L2范数是: 0.3556520938873291
简洁实现
由于权重衰减在神经网络优化中很常用, 深度学习框架为了便于我们使用权重衰减, 将权重衰减集成到优化算法中,以便与任何损失函数结合使用。 此外,这种集成还有计算上的好处, 允许在不增加任何额外的计算开销的情况下向算法中添加权重衰减。 由于更新的权重衰减部分仅依赖于每个参数的当前值, 因此优化器必须至少接触每个参数一次。
在下面的代码中,我们在实例化优化器时直接通过weight_decay
指定weight decay超参数。 默认情况下,PyTorch同时衰减权重和偏移。 这里我们只为权重设置了weight_decay
,所以偏置参数b不会衰减。
def train_concise(wd):
# 受一个参数 wd,表示权重衰减(weight decay)的超参数
net = nn.Sequential(nn.Linear(num_inputs, 1)) # 一个单层神经网络
for param in net.parameters():
param.data.normal_() # 使用正态分布随机初始化模型的权重参数。
loss = nn.MSELoss(reduction='none')
# 参数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())
# 打印训练结束后模型权重的L2范数,这是一种衡量权重参数大小的方式,可以用于观察权重衰减的效果
与从零实习的代码相比,这里的代码运行得更快,更容易实现。 对于更复杂的问题,这一好处将变得更加明显。
train_concise(0)
Out: w的L2范数: 13.727912902832031
train_concise(3)
out: w的L2范数: 0.3890590965747833