文章目录
PyTorch实现L1和L2正则化
L1正则化和L2正则化是两种常用的正则化方法,用于防止模型过拟合,提高模型的泛化能力,它们是通过向损失函数中添加惩罚项来限制模型的复杂度。
1.L1正则化与L2正则化介绍
L1正则化(Lasso回归):
L1正则化的惩罚项是模型权重的绝对值之和,即:
L1正则化项
=
λ
∑
i
∣
w
i
∣
\text{L1正则化项}=\lambda\sum_i|w_i|
L1正则化项=λ∑i∣wi∣,其中
λ
\lambda
λ是正则化参数,用于控制正则化的强度,
w
i
w_i
wi是模型的权重。带有L1正则化的损失函数通常表示为:
L
1
损失
=
原始损失
+
λ
∑
i
∣
w
i
∣
\mathrm{L}1\text{损失}=\text{原始损失}+\lambda\sum_i|w_i|
L1损失=原始损失+λi∑∣wi∣
L1正则化倾向于使一些权重变为零,从而使模型更加稀疏。由于使用绝对值,L1正则化可以使得小的权重趋向于零,但不会对大的权重有很大影响。
L2正则化(Ridge回归):
L2正则化的惩罚项是模型权重的平方和,即:
L2正则化项
=
λ
∑
i
w
i
2
\text{L2正则化项}=\lambda\sum_iw_i^2
L2正则化项=λ∑iwi2,同样
λ
\lambda
λ是正则化参数,用于控制正则化的强度,
w
i
w_i
wi是模型的权重。带有L2正则化的损失函数通常表示为:
L
2
损失
=
原始损失
+
λ
∑
i
w
i
2
\mathrm{L}2\text{损失}=\text{原始损失}+\lambda\sum_iw_i^2
L2损失=原始损失+λi∑wi2
L2正则化会使权重逐渐减小,但不会使它们变为零。这使得模型的权重分布更加平滑。由于使用平方和,L2正则化对大的权重有更大的影响,能有效防止权重变得过大。
L1和L2正则化是控制模型复杂度的有效手段,如果希望模型具有特征选择功能,可以选择L1正则化,因为它会使一些权重变为零,从而自动选择特征。如果希望模型权重更加平滑且不希望一些权重变为零,可以选择L2正则化。也可以同时使用L1和L2正则化,这被称为Elastic Net正则化,结合了两者的优点。
Elastic Net正则化:
Elastic Net正则化(弹性网络正则化)结合了L1正则化和L2正则化的优点,是一种在模型训练过程中防止过拟合的强大工具。它通过在损失函数中同时包含L1和L2正则化项来实现。Elastic Net正则化的损失函数如下:
Elastic Net损失
=
原始损失
+
α
(
λ
1
∑
i
∣
w
i
∣
+
λ
2
∑
i
w
i
2
)
\text{Elastic Net损失}=\text{原始损失}+\alpha\left(\lambda_1\sum_i|w_i|+\lambda_2\sum_iw_i^2\right)
Elastic Net损失=原始损失+α(λ1i∑∣wi∣+λ2i∑wi2)
其中
α
\alpha
α是总体正则化的权重参数,
λ
1
\lambda_1
λ1是L1正则化的权重参数,
λ
2
\lambda_2
λ2是L2正则化的权重参数,
w
i
w_i
wi是模型的权重。
Elastic Net正则化通过结合L1和L2正则化,可以同时实现特征选择和权重平滑。L1正则化可以使一些权重变为零,从而选择重要特征;L2正则化则可以防止权重过大,保持权重的平滑性。Elastic Net特别适用于高维数据(特征数多于样本数)和存在多重共线性的情况。与L1正则化不同,Elastic Net可以选择一组相关特征,而不仅仅是单个特征。
2.利用torch.optim实现正则化
PyTorch中的官方优化器集成了L2正则化,使用很简单,只需要在创建优化器时,添加weight_decay
参数即可。该参数相当于L2正则化项中的
λ
\lambda
λ参数,用于指定权重衰减的程度。
optimizer = optim.Adam(model.parameters(),lr=learning_rate,weight_decay=0.01)
注意:
- torch.optim所集成的优化器只有L2正则化方法,并且自带的L2正则化是将所有参数都进行了正则化,包括权重和偏置。但是一般的正则化,我们只是对权重W进行正则化,对偏置B是不进行惩罚的。
- 在应用正则化技术时,我们会观察到正则化项的增加会导致整体损失(loss)变大。例如,当我们设置weight_decay为1时,损失值为10;当weight_decay增加到100时,损失值理论上应增加到1000。然而,如果我们使用torch.optim的优化器,并依然使用nn.CrossEntropyLoss()计算损失,我们会发现,无论如何改变weight_decay的值,损失值几乎与未加正则化时的损失值相差无几。这是因为在这种情况下,损失函数nn.CrossEntropyLoss()并未包含权重W的正则化项。(注意:并不是没有实现正则化)
3.利用torch.optim实现正则化进阶(建议使用👏)
当模型过拟合的时候,大多数其实我们用L2正则化就可以了。但是在网上查了很多的资料,都说利用torch.optim所实现的L2正则化有误,因为其不仅对权重W进行了惩罚,还对偏置B也进行了惩罚。但是一般,我们实现正则化是不对偏置B进行惩罚的。故学习了一些自定义正则化的方法,譬如下面的方法一和方法二,但是当偶然看到官方文档的一个技术点后,我发现可以通过下面的方式来实现L2正则化,并且自定义对哪些参数进行惩罚。实现方式如下:
# 指定了不进行权重衰减的参数名称,包括偏置项(bias)和 LayerNorm 的权重(LayerNorm.weight)
no_decay = ["bias", "LayerNorm.weight"]
"""
* optimizer_grouped_parameters 列表包含两个字典,用于分别处理需要进行权重衰减和不需要进行权重衰减的参数。
* 第一个字典中的 "params" 键:筛选出参数名称中不包含 no_decay 列表中任一元素的参数,并为这些参数设置权重衰减率 decay。 (需要进行权重衰减)
* 第二个字典中的 "params" 键:筛选出参数名称中包含 no_decay 列表中任一元素的参数,并将这些参数的权重衰减率设为 0.0。 (不需要进行权重衰减)
"""
optimizer_grouped_parameters = [
{
"params": [parameter for name, parameter in model.named_parameters() if not any(nd in name for nd in no_decay)],
"weight_decay": decay,
},
{
"params": [p for n, p in model.named_parameters() if any(nd in n for nd in no_decay)],
"weight_decay": 0.0,
},
]
# 定义优化器,并传入参数列表
optimizer = torch.optim.AdamW(params=optimizer_grouped_parameters, lr=lr, amsgrad=False)
优化器支持指定每个参数的选项。为此,不要传递变量s的iterable,而是传递dict s的iterable。它们中的每一个都将定义一个单独的参数组,并且应该包含一个params键,其中包含属于它的参数列表。其他键应该与优化器接受的关键字参数相匹配,并将用作该组的优化选项。
4.方法一:直接在loss后面加对应的惩罚项
4.1 L1正则化实现思路
损失函数后面加惩罚项torch.sum(torch.abs(param))
weight_decay = 0.01 # 权重系数
regularization_loss = 0 # L1正则化的损失
for param in model.parameters():
# L1正则化的方式,参数的绝对值求和
regularization_loss += torch.sum(torch.abs(param))
origin_loss = criterion(input=output, target=label) # 计算原始损失
loss = origin_loss + weight_decay * regularization_loss # 计算总损失
optimizer.zero_grad()
loss.backward()
optimizer.step()
4.2 L2正则化实现思路
损失函数后面加惩罚项torch.sum(torch.pow(param, 2))
weight_decay = 0.01 # 权重系数
regularization_loss = 0 # L2正则化的损失
for param in model.parameters():
# L2正则化的方式,将参数中的权重全部求平方后再求和,结果就是L2范数
regularization_loss += torch.sum(torch.pow(param, 2))
origin_loss = criterion(input=output, target=label) # 计算原始损失
loss = origin_loss + weight_decay * regularization_loss # 计算总损失
optimizer.zero_grad()
loss.backward()
optimizer.step()
5.方法二:通过封装一个正则化类实现正则化
5.1 封装一个实现正则化的Regularization类
import torch
from torch import nn
class Regularization(nn.Module):
def __init__(self, model, weight_decay, p=2):
"""
:param model 模型
:param weight_decay:正则化参数
:param p: 范数计算中的幂指数值,默认求2范数,
当p=2为L2正则化,p=1为L1正则化
"""
super().__init__()
if weight_decay <= 0:
print("param weight_decay can not <=0")
exit(0)
self.model = model
self.weight_decay = weight_decay
self.p = p
self.weight_list = self.get_weight(model)
self.weight_info(self.weight_list)
def forward(self, model):
self.weight_list = self.get_weight(model) # 获得最新的权重
reg_loss = self.regularization_loss(self.weight_list, self.weight_decay, p=self.p)
return reg_loss
def get_weight(self, model):
"""
获得模型的权重列表
:param model:
:return:
"""
weight_list = []
for name, param in model.named_parameters():
if 'weight' in name:
weight = (name, param)
weight_list.append(weight)
return weight_list
def regularization_loss(self, weight_list, weight_decay, p=2):
"""
计算张量范数
:param weight_list:
:param p: 范数计算中的幂指数值,默认求2范数
:param weight_decay:
:return:
"""
reg_loss = 0
for name, w in weight_list:
l2_reg = torch.norm(w, p=p)
reg_loss = reg_loss + l2_reg
reg_loss = weight_decay * reg_loss
return reg_loss
def weight_info(self, weight_list):
"""
打印权重列表信息
:param weight_list:
:return:
"""
print("---------------regularization weight---------------")
for name, w in weight_list:
print(name)
print("---------------------------------------------------")
5.2 利用Regularization类添加正则化
首先初始化Regularization类,然后在计算损失时,加上Regularization类所返回的损失就可以了(其实加的就是正则化项)。实现思路如下:
weight_decay = 100.0 # 正则化参数
model = Model()
# 初始化正则化
if weight_decay>0:
reg_loss=Regularization(model, weight_decay, p=2)
else:
print("no regularization")
criterion= nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(),lr=learning_rate) # 注意不需要指定参数weight_decay
# 训练数据和对应的标签
batch_train_data=...
batch_train_label=...
out = model(batch_train_data)
# loss and regularization
loss = criterion(input=out, target=batch_train_label)
if weight_decay > 0:
loss = loss + reg_loss(model) # 添加正则化
total_loss = loss.item()
# backprop
optimizer.zero_grad() # 清除当前所有的累积梯度
total_loss.backward()
optimizer.step()
这种方法相比torch.optim优化器自带的L2正则化,没有将偏置B加入正则化,并且可以自由的选择L1或者L2正则化。通过大倍数的改变weight_decay,如果loss的变化很大,那么正则化的加入就成功了。
参考
😃😃😃