浅尝辄止

理论是灰色的,而生命之树常青。这里是@Dilettante258 的个人博客,用于记载和分享学习。

权重衰减

Dilettante258's avatar
| 0 views

过拟合问题的解决

过拟合是机器学习和深度学习中一个常见的问题,它发生在模型在训练数据上表现很好,但在未见过的新数据上表现不佳的情况下。以下是一些处理过拟合的常见方法:

  1. 更多数据集:最直接的方法是增加训练数据。更多的数据可以帮助模型更好地学习数据的真实分布,减少过拟合的风险。
  2. 简化模型:减少模型的复杂度是防止过拟合的关键。可以考虑以下方法:
    • 减少模型的参数数量。
    • 减小神经网络的层数。
    • 减小决策树的深度。
  3. 正则化:正则化是通过向损失函数添加额外的项来限制模型参数的大小,从而减少过拟合的风险。常见的正则化技术包括:
    • L1正则化:通过向损失函数添加L1范数的惩罚项,推动模型参数趋向于稀疏值。
    • L2正则化:通过向损失函数添加L2范数的惩罚项,推动模型参数趋向于较小的值。
    • Dropout:在训练过程中随机丢弃一部分神经元,以减少神经网络的复杂性。
  4. 交叉验证:使用交叉验证来评估模型的性能。这可以帮助检测模型是否过拟合,并调整模型的超参数。
  5. 提前停止:在训练过程中,监控验证集的性能,并在性能不再提高时停止训练,以防止模型继续过拟合训练数据。
  6. 特征选择:选择最重要的特征,以减少输入特征的维度,从而降低模型的复杂性。
  7. 集成方法:使用集成学习技术,如随机森林或梯度提升树,可以减少过拟合风险,因为它们结合多个模型的预测。
  8. 数据增强:对训练数据进行随机变换或扩充,以增加数据的多样性,有助于模型泛化到未见过的数据。
  9. 特征工程:精心设计和选择特征可以改善模型的泛化性能。
  10. 模型选择:尝试不同类型的模型,选择最适合问题的模型架构。
  11. 监督模型复杂性:确保模型不会过于复杂,适应问题的实际复杂性。

这里我们选择控制模型的容量。有两种办法:

  • 减小模型大小:最直接的方法是减小模型的规模,即减少模型中可学习参数的数量。
  • 缩小参数值的取值范围:有助于提高模型的稳定性和泛化性能,防止模型在训练过程中发生梯度爆炸或梯度消失等问题。

这里我们学习怎么缩小参数值的取值范围。

范数

范数(Norm)是线性代数中的一个概念,用来衡量向量的大小或长度。范数是一个非负的标量值,通常表示为 ||x||,其中 x 是向量。不同的范数定义方式会导致不同的向量大小度量方式,常见的范数包括:

  1. L1 范数(曼哈顿范数): L1 范数计算向量中所有元素的绝对值之和。对于一个 n 维向量 x,L1 范数定义如下: x1=x1+x2++xn||x||₁ = |x₁| + |x₂| + ⋯ + |xₙ|x1=i=1nxi.\|\mathbf{x}\|_1 = \sum_{i=1}^n \left|x_i \right|.
  2. L2 范数(欧几里德范数): L2 范数计算向量中所有元素的平方和的平方根。对于一个 n 维向量 x,L2 范数定义如下: x2=x12+x22++xn2||x||₂ = \sqrt{x₁² + x₂² + ⋯ + xₙ²}x2=i=1nxi2,\|\mathbf{x}\|_2 = \sqrt{\sum_{i=1}^n x_i^2},
  3. 无穷范数: 无穷范数计算向量中所有元素的绝对值,并返回其中最大的绝对值。对于一个 n 维向量 x,无穷范数定义如下: x=max(x1,x2,,xn)||x||∞ = max(|x₁|, |x₂|, ⋯, |xₙ|)

这些范数都有各自的特点和应用场景。例如,L1 范数在稀疏性相关问题中常被用到,因为它倾向于将某些元素归零,从而产生稀疏解。而L2 范数在机器学习中常用于正则化,它有助于防止模型过拟合。无穷范数则用于寻找向量中绝对值最大的元素,常用于一些优化问题中。

总之,范数是一个重要的数学概念,可以用来衡量向量的大小,并在许多领域中有广泛的应用。不同的范数选择会导致不同的向量度量方式,适合不同的问题和需求。

权重衰减

在训练参数化机器学习模型时, 权重衰减(weight decay)是最广泛使用的正则化的技术之一, 它通常也被称为L2正则化,用于防止神经网络模型过拟合。过拟合是指模型在训练数据上表现良好,但在未见过的测试数据上表现较差的现象。权重衰减通过对模型的权重参数施加惩罚,防止它们变得过大,从而抑制模型的复杂性。

这项技术通过函数与零的距离来衡量函数的复杂度。一种简单的方法是通过线性函数f(x)=wxf(\mathbf{x}) = \mathbf{w}^\top \mathbf{x}中的权重向量的某个范数来度量其复杂性, 例如w2\| \mathbf{w} \|^2要保证权重向量比较小, 最常用方法是将其范数作为惩罚项加到最小化损失的问题中。 将原来的训练目标最小化训练标签上的预测损失, 调整为最小化预测损失和惩罚项之和。 现在,如果我们的权重向量增长的太大, 我们的学习算法可能会更集中于最小化权重范数。

权重衰减的数学形式通常是在模型的原始损失函数LL上加上权重参数的平方和,乘以一个正的超参数λλ。新的损失函数LL^{\prime}可以表示为:

L(w,b)+λ2w2L(\mathbf{w}, b) + \frac{\lambda}{2} \|\mathbf{w}\|^2
  • λ0λ\to 0时,恢复了原来的损失函数。
  • λλ\to \infty时,就等价于硬性限制中θ趋向于0,使得最优解w*也会慢慢趋向于0
  • 可以通过增加λ来控制模型的复杂度(让模型不要太复杂) 较小的λ值对应较少约束的w, 而较大的λ值对w的约束更大。

公式解惑:

  1. 为什么除以2? 当我们取一个二次函数的导数时, 2和1/2会抵消,以确保更新表达式看起来既漂亮又简单。
  2. 为什么在这里我们使用平方范数而不是标准范数(即欧几里得距离)? 我们这样做是为了便于计算。 通过平方L2范数,我们去掉平方根,留下权重向量每个分量的平方和。 这使得惩罚的导数很容易计算:导数的和等于和的导数。 选择L2范数的一个主要原因是,它会对权重向量的大分量施加较大的惩罚,导致学习算法倾向于在多个特征上分布权重,从而使模型对观测误差更加稳定。与之相反,L1范数会使模型的权重集中在少数特征上,其他权重则会被置为零,这一特性也被称为特征选择,这在某些场景下可能是有用的。

岭回归和套索回归

岭回归(Ridge Regression)和套索回归(Lasso Regression)都是线性回归的扩展,用于处理共线性问题,并有助于防止过拟合。

  1. 岭回归

    岭回归在线性回归的基础上,对系数(权重)加入了L2正则化项。其优化目标函数为: J(w)=Xwy2+αw2J(w)=\|X w-y\|^{2}+\alpha\|w\|^{2} 其中,X是输入数据,w是权重向量,y是目标输出,α是正则化系数,用于控制正则化的强度。岭回归通过加入L2正则项,对权重向量的大分量施加了较大的惩罚,这有助于防止模型过拟合,提高模型的泛化能力。

  2. 套索回归

    套索回归也是在线性回归的基础上加入了正则化项,但它加入的是L1正则化项。其优化目标函数为: J(w)=Xwy2+αw1J(w)=\|X w-y\|^{2}+\alpha\|w\|_1 其中,w1\|w\|_1表示权重向量的L1范数,即权重绝对值之和。与岭回归不同,套索回归的L1正则化会导致部分权重精确为0,这就实现了特征选择。这意味着模型会自动选择对目标变量有影响的特征,而忽略不重要的特征。

这两种回归方法都是通过引入正则化项来约束模型的复杂度,从而避免过拟合,提高模型的泛化能力。不过,它们的正则化形式不同,导致了模型的不同性质:岭回归倾向于分散权重,而套索回归则倾向于产生稀疏权重。

高维线性回归

我们通过一个简单的例子来演示权重衰减。

%matplotlib inline
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.01 x_i + \epsilon \text{ where } \epsilon \sim \mathcal{N}(0, 0.01^2).

我们选择标签是关于输入的线性函数。 标签同时被均值为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.linregd2l.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

../_images/output_weight-decay_ec9cc0_81_1.svg

使用权重衰减

下面,我们使用权重衰减来运行代码。 注意,在这里训练误差增大,但测试误差减小。 这正是我们期望从正则化中得到的效果。

train(lambd=3)
Out: w的L2范数是: 0.3556520938873291

1

简洁实现

由于权重衰减在神经网络优化中很常用, 深度学习框架为了便于我们使用权重衰减, 将权重衰减集成到优化算法中,以便与任何损失函数结合使用。 此外,这种集成还有计算上的好处, 允许在不增加任何额外的计算开销的情况下向算法中添加权重衰减。 由于更新的权重衰减部分仅依赖于每个参数的当前值, 因此优化器必须至少接触每个参数一次。

在下面的代码中,我们在实例化优化器时直接通过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

../_images/output_weight-decay_ec9cc0_130_1.svg

train_concise(3)
out: w的L2范数: 0.3890590965747833

../_images/output_weight-decay_ec9cc0_131_1.svg