深度学习-多层感知机5-权重衰减

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

在多项式回归问题中,我们可以通过限制多项式阶数来控制模型的容量,但直接减少特征数量可能过于粗暴。在更一般的场景中,我们可以引入权重向量的范数来度量模型复杂性,例如使用L 2范数
1754288943069.png

。通过在损失函数中添加这一范数作为惩罚项,我们可以同时最小化预测误差和权重大小,从而实现复杂度控制。最终的损失函数定义如下:


1754288967400.png

其中,λ≥0为正则化强度超参数,控制了惩罚项的权重。通过调整 λ,我们可以在模型的复杂度和拟合能力之间找到平衡点。
相比标准 L2范数,使用平方范数
1754288943069.png

的优势在于计算方便,其梯度为 2w,简化了优化过程。此外,正则化方式的选择还取决于任务需求:

  • L2范数:对所有权重均匀施加惩罚,使模型更稳定,适用于特征多且相关性较高的场景。
  • L1范数:倾向于将部分权重置零,实现特征选择,适合希望聚焦少量重要特征的任务。
    李沐老师B站视频课《权重衰退》
    在小批量随机梯度下降中,结合正则化的权重更新规则为:
    1754289073420.png

    其中,η为学习率。可以看出,每次迭代中,权重都会因惩罚项的作用而向零方向收缩,这就是 “权重衰减” 名称的由来。
    权重衰减为模型提供了连续调整复杂度的机制。较小的 λ值对应较少的正则化,限制较弱;而较大的 λ 值会显著限制权重大小,防止过拟合。在实际应用中,是否对偏置项进行正则化视情况而定,例如在神经网络输出层通常不对偏置项施加正则化。
    通过权重衰减,我们可以有效降低模型复杂度,提高其对未知数据的泛化能力,同时避免过拟合带来的问题。这使得它成为解决过拟合问题的一种核心技术。

一、高维线性回归

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

数据生成

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


1754289148204.png
模型设置

为了更显著地观察到过拟合的效果,我们可以:
增加问题的维度到 𝑑=200,只使用一个小的训练集,其中包含 𝑛=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初始化真实权重 w^*,其中每个元素都为 0.01。
  • 偏置 b^*=0.05。
  • 训练集和测试集分别由 d2l.synthetic_data 函数生成,它按照公式 (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 定义L 2范数惩罚

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


1754289326945.png

其代码实现如下

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

2.3 定义训练代码实现

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


1754289364591.png
  • Squared Loss: 平方损失
    代码如下:
def train(lambd):
    w, b = init_params()
    net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss#定义线性回归模型 net 和平方损失函数 loss。
    num_epochs, lr = 100, 0.003#设置训练的轮数 num_epochs 和学习率 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:#遍历训练数据集中的每个批次。
            # 增加了 L2 范数惩罚项
            # 广播机制使 l2_penalty(w) 成为一个长度为 batch_size 的向量
            l = loss(net(X), y) + lambd * l2_penalty(w)#计算损失,包括L2正则化项。
            l.sum().backward(retain_graph=True)#计算梯度并进行反向传播。
            d2l.sgd([w, b], lr, batch_size)#使用随机梯度下降法更新参数。
记录和显示损失:
        if (epoch + 1) % 5 == 0:#每5个epoch记录一次损失。
            animator.add(epoch + 1, (d2l.evaluate_loss(net, train_iter, loss),
                                     d2l.evaluate_loss(net, test_iter, loss)))#添加当前epoch的训练和测试损失。
    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 时,禁用权重衰减。此时训练误差减少,但测试误差未减少,出现严重过拟合现象。
代码运行如下:

train(0)

输出:
w的L2范数是:14.025191307067871


image.png

2.5 使用权重衰减

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

train(3)

输出:
w的L2范数是:0.37486475706100464


image.png

三、简洁实现

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

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

在下面的代码中,我们通过 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) 是最常用的正则化方法之一,也被称为 L2正则化,它通过限制模型的复杂度来提高其泛化能力。

在多项式回归问题中,我们可以通过限制多项式阶数来控制模型的容量,但直接减少特征数量可能过于粗暴。在更一般的场景中,我们可以引入权重向量的范数来度量模型复杂性,例如使用L 2范数
1754288943069.png

。通过在损失函数中添加这一范数作为惩罚项,我们可以同时最小化预测误差和权重大小,从而实现复杂度控制。最终的损失函数定义如下:


1754288967400.png

其中,λ≥0为正则化强度超参数,控制了惩罚项的权重。通过调整 λ,我们可以在模型的复杂度和拟合能力之间找到平衡点。
相比标准 L2范数,使用平方范数
1754288943069.png

的优势在于计算方便,其梯度为 2w,简化了优化过程。此外,正则化方式的选择还取决于任务需求:

  • L2范数:对所有权重均匀施加惩罚,使模型更稳定,适用于特征多且相关性较高的场景。
  • L1范数:倾向于将部分权重置零,实现特征选择,适合希望聚焦少量重要特征的任务。
    李沐老师B站视频课《权重衰退》
    在小批量随机梯度下降中,结合正则化的权重更新规则为:
    1754289073420.png

    其中,η为学习率。可以看出,每次迭代中,权重都会因惩罚项的作用而向零方向收缩,这就是 “权重衰减” 名称的由来。
    权重衰减为模型提供了连续调整复杂度的机制。较小的 λ值对应较少的正则化,限制较弱;而较大的 λ 值会显著限制权重大小,防止过拟合。在实际应用中,是否对偏置项进行正则化视情况而定,例如在神经网络输出层通常不对偏置项施加正则化。
    通过权重衰减,我们可以有效降低模型复杂度,提高其对未知数据的泛化能力,同时避免过拟合带来的问题。这使得它成为解决过拟合问题的一种核心技术。

一、高维线性回归

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

数据生成

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


1754289148204.png
模型设置

为了更显著地观察到过拟合的效果,我们可以:
增加问题的维度到 𝑑=200,只使用一个小的训练集,其中包含 𝑛=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初始化真实权重 w^*,其中每个元素都为 0.01。
  • 偏置 b^*=0.05。
  • 训练集和测试集分别由 d2l.synthetic_data 函数生成,它按照公式 (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 定义L 2范数惩罚

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


1754289326945.png

其代码实现如下

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

2.3 定义训练代码实现

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


1754289364591.png
  • Squared Loss: 平方损失
    代码如下:
def train(lambd):
    w, b = init_params()
    net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss#定义线性回归模型 net 和平方损失函数 loss。
    num_epochs, lr = 100, 0.003#设置训练的轮数 num_epochs 和学习率 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:#遍历训练数据集中的每个批次。
            # 增加了 L2 范数惩罚项
            # 广播机制使 l2_penalty(w) 成为一个长度为 batch_size 的向量
            l = loss(net(X), y) + lambd * l2_penalty(w)#计算损失,包括L2正则化项。
            l.sum().backward(retain_graph=True)#计算梯度并进行反向传播。
            d2l.sgd([w, b], lr, batch_size)#使用随机梯度下降法更新参数。
记录和显示损失:
        if (epoch + 1) % 5 == 0:#每5个epoch记录一次损失。
            animator.add(epoch + 1, (d2l.evaluate_loss(net, train_iter, loss),
                                     d2l.evaluate_loss(net, test_iter, loss)))#添加当前epoch的训练和测试损失。
    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 时,禁用权重衰减。此时训练误差减少,但测试误差未减少,出现严重过拟合现象。
代码运行如下:

train(0)

输出:
w的L2范数是:14.025191307067871


image.png

2.5 使用权重衰减

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

train(3)

输出:
w的L2范数是:0.37486475706100464


image.png

三、简洁实现

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

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

在下面的代码中,我们通过 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
可视化效果:


image.png

使用权重衰减(wd = 3):

train_concise(3)

输出:
权重的L2范数:0.3597058951854706


image.png

小结

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

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

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

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

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容