动手学人工智能-多层感知机5-权重衰减

221 阅读1分钟

在训练机器学习模型时,正则化是一种常用的技术,用于缓解过拟合现象。当无法通过增加训练数据来改善模型表现时,正则化方法可以提供有效的替代方案。权重衰减(weight decay) 是最常用的正则化方法之一,也被称为 L2\mathbf{L}_2 正则化,它通过限制模型的复杂度来提高其泛化能力。

在多项式回归问题中,我们可以通过限制多项式阶数来控制模型的容量,但直接减少特征数量可能过于粗暴。在更一般的场景中,我们可以引入权重向量的范数来度量模型复杂性,例如使用 L2\mathbf{L}_2 范数 w22\|\mathbf{w}\|_2^2。通过在损失函数中添加这一范数作为惩罚项,我们可以同时最小化预测误差和权重大小,从而实现复杂度控制。最终的损失函数定义如下:

L(w,b)=1ni=1n(f(xi),yi)+λ2w22L(\mathbf{w}, b) = \frac{1}{n} \sum_{i=1}^n \ell(f(\mathbf{x}_i), y_i) + \frac{\lambda}{2} \|\mathbf{w}\|_2^2

其中,λ0\lambda \geq 0 为正则化强度超参数,控制了惩罚项的权重。通过调整 λ\lambda,我们可以在模型的复杂度和拟合能力之间找到平衡点。

相比标准 L2\mathbf{L}_2 范数,使用平方范数 w22\|\mathbf{w}\|_2^2 的优势在于计算方便,其梯度为 2w2\mathbf{w},简化了优化过程。此外,正则化方式的选择还取决于任务需求:

  • L2\mathbf{L}_2范数:对所有权重均匀施加惩罚,使模型更稳定,适用于特征多且相关性较高的场景。
  • L1\mathbf{L}_1范数:倾向于将部分权重置零,实现特征选择,适合希望聚焦少量重要特征的任务。

李沐老师B站视频课《权重衰退》

在小批量随机梯度下降中,结合正则化的权重更新规则为:

wwη(w1ni=1n(f(xi),yi)+λw)\mathbf{w} \leftarrow \mathbf{w} - \eta \left( \frac{\partial}{\partial \mathbf{w}} \frac{1}{n} \sum_{i=1}^n \ell(f(\mathbf{x}_i), y_i) + \lambda \mathbf{w} \right)

其中,η\eta 为学习率。可以看出,每次迭代中,权重都会因惩罚项的作用而向零方向收缩,这就是 “权重衰减” 名称的由来。

权重衰减为模型提供了连续调整复杂度的机制。较小的 λ\lambda 值对应较少的正则化,限制较弱;而较大的 λ\lambda 值会显著限制权重大小,防止过拟合。在实际应用中,是否对偏置项进行正则化视情况而定,例如在神经网络输出层通常不对偏置项施加正则化。

通过权重衰减,我们可以有效降低模型复杂度,提高其对未知数据的泛化能力,同时避免过拟合带来的问题。这使得它成为解决过拟合问题的一种核心技术。

一、高维线性回归

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

数据生成

首先,像以往一样生成一些数据,生成公式如下:

y=Xw+b+ϵ(3.5.1)y = Xw + b + \epsilon \tag{3.5.1}
  • XRn×dX \in \mathbb{R}^{n \times d}是输入特征矩阵,
  • wRdw \in \mathbb{R}^d是权重向量,
  • bRb \in \mathbb{R}是偏置项,
  • ϵN(0,σ2)\epsilon \sim \mathcal{N}(0, \sigma^2)是均值为 0,标准差为σ=0.01\sigma = 0.01的高斯噪声。

在这个例子中,标签是关于输入特征的线性函数,并被高斯噪声破坏。

模型设置

为了更显著地观察到过拟合的效果,我们可以:

  • 增加问题的维度到 𝑑=200𝑑=200
  • 只使用一个小的训练集,其中包含 𝑛=20𝑛=20 个样本。

我们使用以下代码来生成数据和加载数据集:

import torch
from torch import nn
import d2l

# 数据参数
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)

上述代码的解释:

  • torch.ones((num_inputs, 1)) * 0.01初始化真实权重 ww^*,其中每个元素都为 0.010.01
  • 偏置 b=0.05b^*=0.05
  • 训练集和测试集分别由 d2l.synthetic_data 函数生成,它按照公式 (3.5.1)(3.5.1) 合成数据。
  • 数据加载器使用 d2l.load_array 创建,支持小批量随机梯度下降。

二、从零开始实现

我们从头开始实现权重衰减,其核心是将权重的平方惩罚项添加到原始目标函数中。

2.1 初始化模型参数

我们定义一个函数,用于随机初始化模型参数:

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]

2.2 定义L2L_2范数惩罚

为了实现 L2\mathbf{L}_2 范数惩罚,我们对所有权重项的平方求和后再除以 2,公式为:

L2 penalty(惩罚):12iwi2L2 penalty(惩罚): \frac{1}{2} \sum_i w_i^2

其代码实现如下

def l2_penalty(w):
    return torch.sum(w.pow(2)) / 2

2.3 定义训练代码实现

训练代码拟合模型到训练数据集,并评估测试数据集的性能。线性网络和平方损失的实现保持不变,只是损失函数中新增了权重惩罚项:

L=Squared Loss+λ12iwi2\mathcal{L} = \text{Squared Loss} + \lambda \cdot \frac{1}{2} \sum_i w_i^2
  • Squared LossSquared 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'])
    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(retain_graph=True)
            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())
    animator.show()

补充说明

  • yscale='log' 的是将图表的 y 轴变成对数刻度。这样做的好处是:

    • 数据范围大时更清晰:比如损失值从 1000 到 0.1,普通的线性刻度会让小值部分看不清,而对数刻度可以均匀显示整个变化过程。

    • 更容易观察趋势:对数刻度能把指数下降的曲线变成直线,方便看出损失值下降的速度和收敛情况。

    简单来说,yscale='log' 就是为了让大跨度的数据看起来更直观、更容易分析。

  • d2l.py

# d2l.py

def linreg(X, w, b):
    """线性回归模型"""
    return torch.matmul(X, w) + b


def squared_loss(y_hat, y):
    """均方损失"""
    return (y_hat - torch.reshape(y, y_hat.shape)) ** 2 / 2
    
def sgd(params, lr, batch_size):
    """小批量随机梯度下降"""
    with torch.no_grad():
        for param in params:
            param -= lr * param.grad / batch_size
            param.grad.zero_()

2.4 忽略正则化直接训练

当设置 λ=0λ=0 时,禁用权重衰减。此时训练误差减少,但测试误差未减少,出现严重过拟合现象。

代码运行如下:

train(0)

输出:

wL2范数是:14.025191307067871w 的 L2 范数是: 14.025191307067871

myplot.png

2.5 使用权重衰减

设置 λ=3λ=3 以启用权重衰减。训练误差增加,但测试误差减少,显示正则化的效果。

代码运行如下:

train(3)

输出:

wL2范数是:0.37486475706100464w 的 L2 范数是: 0.37486475706100464

myplot.png

三、简洁实现

由于权重衰减在神经网络优化中非常常用,深度学习框架为了方便用户使用,将权重衰减集成到优化算法中。这种集成的好处有以下几点:

  1. 便捷性:能够与任意损失函数结合使用。
  2. 高效性:在计算上没有额外的开销,权重衰减部分仅依赖于每个参数的当前值,优化器在每次更新中只需访问每个参数一次。

在下面的代码中,我们通过 weight_decay 参数直接设置权重衰减超参数。需要注意,默认情况下,PyTorch 会同时对权重和偏置应用权重衰减。在这里,我们仅对权重设置了 weight_decay,偏置参数不会受到影响。

使用权重衰减的代码实现

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')
    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='训练轮数', ylabel='损失', yscale='log',
                            xlim=[5, num_epochs], legend=['训练误差', '测试误差'])
    for epoch in range(num_epochs):
        for X, y in train_iter:
            trainer.zero_grad()
            l = loss(net(X), y)
            l.mean().backward(retain_graph=True)
            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('权重的 L2 范数:', net[0].weight.norm().item())
    animator.show()

运行结果对比

我们可以通过设置不同的 weight_decay 参数来观察结果:

  • 无权重衰减时(wd = 0
train_concise(0)

输出:

权重的L2范数:13.211384773254395权重的 L2 范数: 13.211384773254395

可视化效果:

myplot.png

  • 使用权重衰减(wd = 3
train_concise(3)

输出:

权重的L2范数:0.3597058951854706权重的 L2 范数: 0.3597058951854706

可视化效果:

myplot.png

小结

  • 正则化:这是解决过拟合问题的常用方法。它通过在损失函数中添加一个 “惩罚项”,来限制模型的复杂度,从而帮助模型在新数据上表现更好。

  • L2\mathbf{L}_2正则化:保持模型简单的一种特别方法。它通过惩罚过大的权重,避免模型过度依赖某些特征,通常表现为“权重衰减”,也就是在训练过程中逐渐减少权重值

  • 权重衰减:这是实现L2\mathbf{L}_2正则化的一种常见方式。在深度学习框架中,优化器通常会内置这个功能,帮助我们自动在每次更新权重时做出调整。

  • 参数的更新方式不同:在同一个训练过程中,我们可以对不同的参数设置不同的更新策略。也就是说,不同类型的参数可以有各自独特的学习方式。