WXL's blog

Talk is cheap, show me your work.

0%

权重衰减

学习《动手学习深度学习》的权重衰减这一节有些你内容做一下笔记,原文如下:

https://zh-v2.d2l.ai/chapter_multilayer-perceptrons/weight-decay.html

为什么要有正则化?

在训练参数化机器学习模型时,权重衰减(通常称为L2正则化)是最广泛使用的正则化的技术之一。这项技术是基于一个基本直觉,即在所有函数f中,函数f=0(所有输入都得到值0)在某种意义上是最简单的,我们可以通过函数与零的距离来衡量函数的复杂度。

一种简单的方法是通过线性函数f(x)=wxf(x)=w^⊤x中的权重向量的某个范数来度量其复杂性,例如w2∥w∥^2​​。要保证权重向量比较小,最常用方法是将其范数作为惩罚项加到最小化损失的问题中,将原来的训练目标最小化训练标签上的预测损失,调整为最小化预测损失和惩罚项之和

果我们的权重向量增长的太大,我们的学习算法可能会更集中于最小化权重范数w2∥w∥^2​​

其实仔细想一下还是容易理解的,为了防止权重增量过大,我们将权重本身加入损失中进行迭代,如果权重大,那么损失值就大,就可以提升优化效果。

我们常用的处理过拟合的方法除了正则化权重衰减)之外,还有:

  • 增加训练数据;
  • 使用适当复杂度的模型;
  • 调节学习率的大小等。

如何将正则化添加入损失?

在线性回归中,我们的损失函数可以定义如下:

L(w,b)=1ni=1n12(wx(i)+by(i))2L(\mathbf{w}, b)=\frac{1}{n} \sum_{i=1}^{n} \frac{1}{2}\left(\mathbf{w}^{\top} \mathbf{x}^{(i)}+b-y^{(i)}\right)^{2}

为了惩罚权重向量的大小,我们需要以某种方式在损失函数中添加w2∥w∥^2,但是模型应该如何平衡这个新的额外惩罚的损失呢?实际上,我们通过正则化常数λ\lambda来描述这种权衡,这是一个非负超参数,我们使用验证数据拟合:

L(w,b)+λ2w2L(\mathbf{w}, b)+\frac{\lambda}{2}\|\mathbf{w}\|^{2}

对于λ>0\lambda > 0,我们恢复了原来的损失函数;对于λ>0\lambda>0,我们限制w∥w∥​​的大小。

为什么除以2?

当我们取一个二次函数的导数的时候,就可以将前面的参数抵消,确保表达式简单。

为什么使用平方范数而不是标准范数(欧几里得距离)?

为了便于计算。通过平方L2L_2范数,去掉平方根,留下权重向量每个分量的平方和,这使得惩罚的导数很容易计算:导数的和等于和的导数。

为什么首先使用L2L_2范数而不是L1L_1范数?

L2L_2正则化线性模型构成经典的岭回归(ridge regression)算法,L1L_1正则化线性回归是统计学中类似的基本模型,通常被称为套索回归(lasso regression)。

使用L2L_2范数的一个原因是它对权重向量的大分量施加了巨大的惩罚。这使得我们的学习算法偏向于在大量特征上均匀分布权重的模型。在实践中,这可能使它们对单个变量中的观测误差更为鲁棒。相比之下,L1L_1惩罚会导致模型将其他权重清除为零而将权重集中在一小部分特征上。这称为特征选择(feature selection),这可能是其他场景下需要的。

L2L_2正则化回归的小批量随机梯度下降

w(1ηλ)wηBiBx(i)(wx(i)+by(i))\mathbf{w} \leftarrow(1-\eta \lambda) \mathbf{w}-\frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \mathbf{x}^{(i)}\left(\mathbf{w}^{\top} \mathbf{x}^{(i)}+b-y^{(i)}\right)

我们根据估计值与观测值之间的差异来更新ww。然而,我们同时也在试图将ww的大小缩小到零。这就是为什么这种方法有时被称为权重衰减。我们仅考虑惩罚项,优化算法在训练的每一步衰减权重。与特征选择(L1L_1正则化)相比,权重衰减为我们提供了一种连续的机制来调整函数的复杂度。较小的λ值对应较少约束的ww,而较大的λ\lambda值对ww​的约束更大。

是否对相应的偏置b2b^2进行惩罚在不同的实现中会有所不同。在神经网络的不同层中也会有所不同。通常,我们不正则化网络输出层的偏置项。

代码实现

从零实现

我们根据下面这个公式来生成数据:

y=0.05+i=1d0.01xi+ϵ where ϵN(0,0.012)y=0.05+\sum_{i=1}^{d} 0.01 x_{i}+\epsilon \text { where } \epsilon \sim \mathcal{N}\left(0,0.01^{2}\right)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 将训练数据数量弄少点:
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, is_train=False)

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]

# L2惩罚项
def l2(w):
return torch.sum(w.pow(2)) / 2

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:
with torch.enable_grad():
l = loss(net(X), y) + l2(w)
l.sum().backward()
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())

我们忽略惩罚项测试一下:

1
train(lambd=0)

输出:w的L2范数是: 13.433332443237305.

设置惩罚项测试一下:

1
train(lambd=4)

输出:w的L2范数是: 0.3840215802192688.

可以看见,没有L2L_2范数的时候,测试误差几乎不变,而有L2L_2范数的时候,测试误差在不断减小。

使用深度学习框架实现

深度学习框架为了便于使用权重衰减,便将权重衰减集成到优化算法中,以便与任何损失函数结合使用。此外,这种集成还有计算上的好处,允许在不增加任何额外的计算开销的情况下向算法中添加权重衰减。由于更新的权重衰减部分仅依赖于每个参数的当前值,因此优化器必须至少接触每个参数一次。

在实例化优化器时直接通过weight_decay指定weight decay超参数。默认情况下,PyTorch同时衰减权重和偏移。这里我们只为权重设置了weight_decay,所以bias参数b不会衰减。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def train_concise(wd):
net = nn.Sequential(nn.Linear(num_inputs, 1))
for param in net.parameters():
param.data.normal_()

loss = nn.MSELoss()
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='epochs', ylabel='loss', yscale='log',
xlim=[5, num_epochs], legend=['train', 'test'])
for epoch in range(num_epochs):
for X, y in train_iter:
with torch.enable_grad():
trainer.zero_grad()
l = loss(net(X), y)
l.backward()
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('w的L2范数:', net[0].weight.norm().item())
print(net)

正则化系数与train_acc、test_acc的关系

修改代码,取λ\lambda为[0, 59],得到的train_acc, test_acc, λ\lambda之间的关系曲线如下:

一开始λ=0\lambda=0的时候,训练误差和测试误差相差非常大,随着λ\lambda的不断增加,大概增加到32了之后,训练误差和测试误差曲线基本重合,但是λ>4\lambda>4之后,最终的损失值没有较大变化。

如果单单通过验证集来找最佳的λ\lambda是不合适的,我们希望测试集的损失降下来,越低越好,如果去除训练集的曲线(蓝色),我们大概会确认最合适的λ\lambda值为23左右,和实际上最合理的选择有较大出入,从上述曲线观察,实际较为合理的λ\lambda值在5~10左右。

发现新正则

偶然的一次尝试发现这种正则的效果很好,所谓“很好”指的是训练损失和测试损失曲线在较少的epoch次之后就可以几乎重合。表达式如下:

L=\frac{1}{2}\sum e^{\abs{w}}

将正则换成新的正则函数之后,我们设置λ=6\lambda=6,然后观察train_losstest_loss曲线:

我们将代码改为不同的λ\lambda值和loss的关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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, is_train=False)

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]

# L惩罚项
def l2(w):
return torch.sum(torch.exp(abs(w))) / 2

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

for _ in range(num_epochs):
for X, y in train_iter:
with torch.enable_grad():
l = loss(net(X), y) + l2(w) * lambd
l.sum().backward()
d2l.sgd([w, b], lr, batch_size)

return (d2l.evaluate_loss(net, train_iter, loss),
d2l.evaluate_loss(net, test_iter, loss))
1
2
3
4
5
6
animator = d2l.Animator(xlabel='lambda', ylabel='loss', yscale='log',
xlim=[1, 30], legend=['train', 'test'])

for i in range(1, 30):
train_loss, test_loss = train(lambd=i)
animator.add(i + 1, (train_loss, test_loss))

下图是取不同的λ\lambda​值得到的训练损失曲线和测试损失曲线,可以发现,当λ\lambda取值从6~12的时候效果最好:

行行好,赏一杯咖啡吧~