在训练机器学习模型时,正则化是一种常用的技术,用于缓解过拟合现象。当无法通过增加训练数据来改善模型表现时,正则化方法可以提供有效的替代方案。权重衰减(weight decay) 是最常用的正则化方法之一,也被称为 L2正则化,它通过限制模型的复杂度来提高其泛化能力。
在多项式回归问题中,我们可以通过限制多项式阶数来控制模型的容量,但直接减少特征数量可能过于粗暴。在更一般的场景中,我们可以引入权重向量的范数来度量模型复杂性,例如使用L 2范数。通过在损失函数中添加这一范数作为惩罚项,我们可以同时最小化预测误差和权重大小,从而实现复杂度控制。最终的损失函数定义如下:
其中,λ≥0为正则化强度超参数,控制了惩罚项的权重。通过调整 λ,我们可以在模型的复杂度和拟合能力之间找到平衡点。
相比标准 L2范数,使用平方范数
的优势在于计算方便,其梯度为 2w,简化了优化过程。此外,正则化方式的选择还取决于任务需求:
- L2范数:对所有权重均匀施加惩罚,使模型更稳定,适用于特征多且相关性较高的场景。
- L1范数:倾向于将部分权重置零,实现特征选择,适合希望聚焦少量重要特征的任务。
李沐老师B站视频课《权重衰退》
在小批量随机梯度下降中,结合正则化的权重更新规则为:
1754289073420.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,公式为:
其代码实现如下
def l2_penalty(w):
return torch.sum(w.pow(2)) / 2
2.3 定义训练代码实现
训练代码拟合模型到训练数据集,并评估测试数据集的性能。线性网络和平方损失的实现保持不变,只是损失函数中新增了权重惩罚项:
- 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
2.5 使用权重衰减
设置 λ=3 以启用权重衰减。训练误差增加,但测试误差减少,显示正则化的效果。
代码运行如下:
train(3)
输出:
w的L2范数是:0.37486475706100464
三、简洁实现
由于权重衰减在神经网络优化中非常常用,深度学习框架为了方便用户使用,将权重衰减集成到优化算法中。这种集成的好处有以下几点:
- 便捷性:能够与任意损失函数结合使用。
- 高效性:在计算上没有额外的开销,权重衰减部分仅依赖于每个参数的当前值,优化器在每次更新中只需访问每个参数一次。
在下面的代码中,我们通过 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范数。通过在损失函数中添加这一范数作为惩罚项,我们可以同时最小化预测误差和权重大小,从而实现复杂度控制。最终的损失函数定义如下:
其中,λ≥0为正则化强度超参数,控制了惩罚项的权重。通过调整 λ,我们可以在模型的复杂度和拟合能力之间找到平衡点。
相比标准 L2范数,使用平方范数
的优势在于计算方便,其梯度为 2w,简化了优化过程。此外,正则化方式的选择还取决于任务需求:
- L2范数:对所有权重均匀施加惩罚,使模型更稳定,适用于特征多且相关性较高的场景。
- L1范数:倾向于将部分权重置零,实现特征选择,适合希望聚焦少量重要特征的任务。
李沐老师B站视频课《权重衰退》
在小批量随机梯度下降中,结合正则化的权重更新规则为:
1754289073420.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,公式为:
其代码实现如下
def l2_penalty(w):
return torch.sum(w.pow(2)) / 2
2.3 定义训练代码实现
训练代码拟合模型到训练数据集,并评估测试数据集的性能。线性网络和平方损失的实现保持不变,只是损失函数中新增了权重惩罚项:
- 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
2.5 使用权重衰减
设置 λ=3 以启用权重衰减。训练误差增加,但测试误差减少,显示正则化的效果。
代码运行如下:
train(3)
输出:
w的L2范数是:0.37486475706100464
三、简洁实现
由于权重衰减在神经网络优化中非常常用,深度学习框架为了方便用户使用,将权重衰减集成到优化算法中。这种集成的好处有以下几点:
- 便捷性:能够与任意损失函数结合使用。
- 高效性:在计算上没有额外的开销,权重衰减部分仅依赖于每个参数的当前值,优化器在每次更新中只需访问每个参数一次。
在下面的代码中,我们通过 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
可视化效果:
使用权重衰减(wd = 3):
train_concise(3)
输出:
权重的L2范数:0.3597058951854706
小结
正则化:这是解决过拟合问题的常用方法。它通过在损失函数中添加一个 “惩罚项”,来限制模型的复杂度,从而帮助模型在新数据上表现更好。
L2正则化:保持模型简单的一种特别方法。它通过惩罚过大的权重,避免模型过度依赖某些特征,通常表现为“权重衰减”,也就是在训练过程中逐渐减少权重值。
权重衰减:这是实现L2\mathbf{L}_2L2正则化的一种常见方式。在深度学习框架中,优化器通常会内置这个功能,帮助我们自动在每次更新权重时做出调整。
参数的更新方式不同:在同一个训练过程中,我们可以对不同的参数设置不同的更新策略。也就是说,不同类型的参数可以有各自独特的学习方式。
。