PyTorch中的L1/L2正则化

113

如何在PyTorch中添加L1 / L2正则化,而无需手动计算?

8个回答

92

使用 weight_decay > 0 进行L2正则化:

optimizer = torch.optim.Adam(model.parameters(), lr=1e-4, weight_decay=1e-5)

3
在 SGD 优化器中,可以通过设置 weight_decay 来实现 L2 正则化。但是在 Adam 优化器中,weight_decay 和 L2 正则化是不同的。更多内容可以在这里阅读:https://openreview.net/pdf?id=rk6qdGgCZ - Ashish
1
@Ashish 您的评论是正确的,即 weight_decay 和 L2 正则化是不同的,但在 PyTorch 的 Adam 实现中,他们实际上实现了 L2 正则化而不是真正的权重衰减。请注意,在优化器步骤之前,权重衰减项被应用于梯度 此处 - Eric Wiener
1
L1正则化怎么样? - Ynjxsjmh

81

请查看文档。向优化器中添加weight_decay参数以进行L2正则化。


10
Adagrad是一种优化技术,我正在谈论正则化。你能给我一个具体的例子,使用L1和L2损失函数吗? - Wasi Ahmad
19
L2正则化通常被添加到优化函数中,因为在优化过程中使用了损失函数。您可以在此处找到相关讨论:https://discuss.pytorch.org/t/simple-l2-regularization/139/3 - Kashyap
5
我有一些分支使用L2损失函数,所以这并不实用。(我有不同的损失函数) - dashesy
6
如果我想使用 L1 或其他一些损失函数进行正则化,该怎么办? - mrgloom
1
@mrgloom 你可以自己实现。它不包含在优化器中。 - Eric Wiener

48
以前的答案在技术上是正确的,但在性能上效率低下,并且不够模块化(很难按层应用,就像keras层所提供的那样)。
PyTorch L2实现
为什么PyTorch在torch.optim.Optimizer实例中实现了L2?
让我们来看一下torch.optim.SGD源代码(目前作为功能优化过程),特别是这部分内容:
for i, param in enumerate(params):
    d_p = d_p_list[i]
    # L2 weight decay specified HERE!
    if weight_decay != 0:
        d_p = d_p.add(param, alpha=weight_decay)

可以看到,参数的导数(梯度)d_p被修改和重新分配,以便进行更快的计算(不保存临时变量)。
它的复杂度为O(N),没有像pow这样的复杂数学运算。 它不涉及autograd,无需扩展图形
与此相比,O(n)**2操作,还参与反向传播。

数学

让我们看看带有正则化因子alphaL2方程(当然也可以对L1进行相同操作):

L2

如果我们对任何带有L2正则化的损失函数对参数w求导(它与损失函数无关),我们得到:

L2 deriv

所以,对于每个权重的梯度,只需简单地将alpha乘以权重进行相加!这正是PyTorch所做的!
L1正则化层
使用这个(以及一些PyTorch的魔法),我们可以得到一个相当通用的L1正则化层,但让我们先看一下L1的一阶导数(sgn是符号函数,对于正输入返回1,对于负输入返回-1,对于0返回0):

L1 derivative

完整的代码与WeightDecay接口位于torchlayers第三方库中,提供了一些只对权重/偏置/特定命名参数进行正则化的功能(免责声明:我是作者),但下面概述的思想的本质如下(请参见注释):
class L1(torch.nn.Module):
    def __init__(self, module, weight_decay):
        super().__init__()
        self.module = module
        self.weight_decay = weight_decay

        # Backward hook is registered on the specified module
        self.hook = self.module.register_full_backward_hook(self._weight_decay_hook)

    # Not dependent on backprop incoming values, placeholder
    def _weight_decay_hook(self, *_):
        for param in self.module.parameters():
            # If there is no gradient or it was zeroed out
            # Zeroed out using optimizer.zero_grad() usually
            # Turn on if needed with grad accumulation/more safer way
            # if param.grad is None or torch.all(param.grad == 0.0):

            # Apply regularization on it
            param.grad = self.regularize(param)

    def regularize(self, parameter):
        # L1 regularization formula
        return self.weight_decay * torch.sign(parameter.data)

    def forward(self, *args, **kwargs):
        # Simply forward and args and kwargs to module
        return self.module(*args, **kwargs)

阅读有关hooks的更多信息在这个答案中或者需要的PyTorch文档。
而且使用也非常简单(应该适用于梯度累积和PyTorch层):
layer = L1(torch.nn.Conv2d(in_channels=3, out_channels=32, kernel_size=3)) 

你是否想为 torchlayers 创建一个新的标签,并发布带有 L1L2 的版本,因为它们在一年多之前发布的 0.1.1 版本中仍然缺失? - Maxim Egorushkin
1
夜间版有L1L2,谢谢。不过有一个警告:~/anaconda3/envs/torch/lib/python3.8/site-packages/torch/nn/modules/module.py:785: UserWarning: Using a non-full backward hook when outputs are generated by different autograd Nodes is deprecated and will be removed in future versions. This hook will be missing some grad_output. Please use register_full_backward_hook to get the documented behavior. - Maxim Egorushkin
"L1正则化并未实现,因为它实际上并不会引起稀疏性。"你的意思是这是神经网络/PyTorch的一个特定限制吗?对于使用坐标下降的普通Lasso实现来说,这并不正确。我从头开始实现了它,并且L1/Lasso正则化的理论结果在实践中有效,无需手动使用阈值将系数舍入为零。 - undefined
2
@phydev - 是的,这个是针对神经网络的一个附注(不是针对套索回归),因为它可能会让用户感到困惑,并且没有任何来源来支持它,所以我会将其删除。请参考上面Kevin Yin的评论,因为它与此有些相关。 - undefined
1
我认为你的侧记实际上很重要,只是想澄清一下。我尝试了在PyTorch中不同的L1正则化实现,我认为它们是错误的,因为我无法达到稀疏性,所以很高兴看到了你的回答。对于为什么会发生这种情况,我还不太清楚,但我怀疑这与最小化过程有关,这个过程与Lasso有很大的不同。顺便说一句,感谢你的出色回答,我一直在使用你的L1层。 - undefined
显示剩余5条评论

28

2
应该排除不可训练的参数吗? - Girishkumar
6
这里的 torch.norm 使用的是 2-范数,而不是 2-范数的平方。因此,我认为应该对范数进行平方以获得正确的正则化结果。 - John Liu
3
没有requires_grad并使用+=会导致错误。以下是可行的代码:l2_reg = torch.tensor(0., requires_grad=True) l2_reg = l2_reg + torch.norm(param)翻译完毕,没有额外内容返回。 - cswu
警告:torch.norm已被弃用 - iacob

24

开箱即用的L2正则化

是的,PyTorch的优化器有一个称为weight_decay的参数,对应着L2正则化因子:

sgd = torch.optim.SGD(model.parameters(), weight_decay=weight_decay)

L1正则化实现

对于L1正则化,没有类似的参数,但是手动实现起来很简单:

loss = loss_fn(outputs, labels)
l1_lambda = 0.001
l1_norm = sum(torch.linalg.norm(p, 1) for p in model.parameters())

loss = loss + l1_lambda * l1_norm

L2的等效手动实现将是:

l2_reg = sum(p.pow(2).sum() for p in model.parameters())

来源:使用PyTorch进行深度学习(8.5.2)


对于 l2_norm,难道不应该平方参数而不是取 2-范数吗?即 sum(p**2 for p in model.parameters()) - Loqz
你的 l2_norm 是不正确的,因为权重矩阵的 L2 范数并不等同于扁平化权重向量的 L2 范数。据我所知,机器学习文献中将正则化称为权重向量的平方 L2 范数,即每个单独的权重元素的平方之和。为了修复你的代码,我会使用 norm(p.flatten(), 2)**2 而不是 norm(p, 2) - A Kareem

18

对于L1正则化,只包括weight

l1_reg = torch.tensor(0., requires_grad=True)

for name, param in model.named_parameters():
    if 'weight' in name:
        l1_reg = l1_reg + torch.linalg.norm(param, 1)

total_loss = total_loss + 10e-4 * l1_reg

警告:torch.norm已被弃用 - iacob

6

有趣的是,在CPU上torch.norm比直接方法慢,在GPU上比直接方法快。

import torch
x = torch.randn(1024,100)
y = torch.randn(1024,100)

%timeit torch.sqrt((x - y).pow(2).sum(1))
%timeit torch.norm(x - y, 2, 1)

输出:

1000 loops, best of 3: 910 µs per loop
1000 loops, best of 3: 1.76 ms per loop

另一方面:

import torch
x = torch.randn(1024,100).cuda()
y = torch.randn(1024,100).cuda()

%timeit torch.sqrt((x - y).pow(2).sum(1))
%timeit torch.norm(x - y, 2, 1)

输出:

10000 loops, best of 3: 50 µs per loop
10000 loops, best of 3: 26 µs per loop

3
我也确认了这一点。在这个例子中,torch.norm要慢大约60%。 - Muppet
这个答案是不正确的,GPU 计算是非阻塞的,这意味着 timeit 不会正确工作,因为即使在 CPU(timeit 发生的地方)控制之后,计算仍在 GPU 上继续进行。为了获得正确的时间,必须在停止计时器之前进行同步。 - seermer
如果你正确计时,你会发现torch.norm比sqrt方法快近一倍(通过在停止计时器之前使用torch.cuda.synchronize) - seermer

1

在好的答案上进行扩展:正如所说,将L2范数添加到损失中等同于权重衰减,但前提是你使用的是没有动量的普通SGD。否则,例如使用Adam优化器时,情况就不完全相同了。AdamW论文[1]指出,权重衰减实际上更加稳定。这就是为什么你应该使用权重衰减,它是优化器的一个选项。并且考虑使用AdamW而不是Adam

另外请注意,你可能不希望对所有参数(model.parameters())进行权重衰减,而只想对其中的一部分进行。请参考这里的示例:

[1] 解耦的权重衰减正则化(AdamW),2017


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接