深度学习笔记(五):权重衰减

我们所知的一切定理和理论都只能作为假想和猜测。

卡尔 波普尔

权重衰减

权重衰减是机器学习中常用的正则化方法之一,它通过在模型的损失函数中添加一个与权重平方成正比的正则项来工作。这种方法也被称为L2正则化。其基本思想是在模型训练的过程中,除了最小化原始的损失函数(例如均方误差损失),还要最小化权重向量的L2范数的平方,从而防止权重过大,减少过拟合的风险。

L2范数

在介绍权重衰减之前,首先明确L2范数的定义。对于一个向量$\mathbf{w} = [w_1, w_2, …, w_n]^\top$,其L2范数定义为:

L2范数的平方(即L2范数的平方),则为:

损失函数及权重衰减项

在机器学习模型中,假设原始的损失函数为$L(\mathbf{w})$,其中$\mathbf{w}$表示模型参数。加入权重衰减后的损失函数$L_{\text{new}}(\mathbf{w})$可以表示为:

其中,$\lambda$是一个正的超参数,用于控制正则项的影响程度。第二项$\frac{\lambda}{2} | \mathbf{w} |_2^2$就是权重衰减项,即L2正则化项。

权重衰减的效果

权重衰减对模型的主要影响是限制了权重的大小。通过在损失函数中添加权重的L2范数的平方,模型在训练过程中会倾向于选择较小的权重。较小的权重可以减少模型的复杂度,从而降低过拟合的风险。此外,L2正则化还有利于提高模型的泛化能力。

梯度下降中的权重更新规则

在使用梯度下降法进行优化时,权重的更新规则需要考虑权重衰减项的影响。对于权重$\mathbf{w}$的每个元素$w_i$,其更新规则为:

其中,$\eta$是学习率,$\frac{\partial L(\mathbf{w})}{\partial w_i}$是原始损失函数对$w_i$的偏导数。可以看出,权重衰减实际上在每次更新权重时引入了权重本身的一个比例,这有助于防止权重增长过快。

通过这种方式,权重衰减(L2正则化)有效地减轻了过拟合问题,使模型能够在未见数据上表现得更好。

权重衰减的从头实现

数据生成

本例中,我们按照如下公式实现一个高维线性的数据采集:

其中,$\epsilon \sim N(0, 0.01^2)$ 。

1
2
3
4
5
6
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)

初始化模型参数

1
2
3
def init_params():
w = torch.normal(0, 1, size =(num_inputs, 1), requires_grad = True)
b = torch.zeros(1,requires_grad = True)

定义惩罚项

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

训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def train(lambd):
w, b = init_params()
net = lambda X: d2l.linreg(X, w, b)
loss = d2l.squared_loss
#从这里我们可以知道,net和loss只需要导入抽象的方法即可,不需要具体的参数。
num_epochs = 100
lr = 0.003
animator = d2l.Animator(xlim = [5,num_epochs],legend=['train','test'])
for epoch in range(num_epochs):
for X, y in train_iter:
l = loss(net(X), y) + lambd * l2_penalty(w)
l.sum().backward()
d2l.sgd([w, b], lr, batch_size)
if epoch%5 == 1:
animator.add(epoch+1, (d2l.evaluate_loss(net,train_iter,loss),
d2l.evaluate_loss(net,test_iter,loss)))
print("w2的范数是:{}",torch.norm(w).item())
#torch.norm(w) 用于计算范数

权重衰减的高级API实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def train_concise(wd):
net = nn.Sequential(nn.Linear(num_inputs, 1))
for param in net.parameters():
param.data.normal_()
loss = nn.MSELoss(reduction='none')
num_epochs = 100
lr = 0.003
trainer = torch.optim.SGD([
{"params":net[0].weight,"weight_decay":wd},
{"params":net[0].bias}
],lr = lr)
#trainer中放的是优化参数和学习率
animator = d2l.Animator(xlim = [5,num_epochs],legend=['train','test'])
for epoch in range(num_epochs):
for X, y in train_iter:
trainer.zero_grad()
l = loss(net(X),y)
l.mean().backward()
trainer.step()
if epoch%5 == 1:
animator.add(epoch+1, (d2l.evaluate_loss(net,train_iter,loss),
d2l.evaluate_loss(net,test_iter,loss)))
print("w2的范数是:{}",net[0].weight.norm().item())

trainer.zero_grad()

  • 在训练循环的每一次迭代开始前,必须清除(归零)旧的梯度,因为默认情况下,梯度是累加的。这意味着如果不手动清零,每次调用 .backward() 方法时计算出的梯度会加到已有的梯度上,这会导致错误的梯度值。
  • trainer.zero_grad() 函数就是用来做这个的,它将模型参数的梯度全部设置为0。这里的 trainer 通常指的是优化器对象,它负责更新模型的权重。

l = loss(net(X), y)

  • 这一行计算当前模型 net 对输入 X 的预测和真实标签 y 之间的损失(loss)。
  • net(X) 表示将输入数据 X 传递给模型 net,得到预测输出。
  • loss(net(X), y) 计算预测输出和真实标签之间的差异。这个差异由一个损失函数来衡量,损失函数的选择取决于特定的任务(例如回归或分类)。

l.mean().backward()

  • 由于损失 l 可能是一个向量(例如,如果使用了一个批次(batch)的数据进行计算),因此首先使用 .mean() 函数计算所有损失值的平均值,从而得到一个标量损失。
  • 然后,调用 .backward() 在这个标量损失上执行反向传播,自动计算模型参数相对于这个平均损失的梯度。这是利用链式法则进行的,是深度学习中参数更新的核心。

trainer.step()

  • 最后一步是调用优化器的 .step() 方法,这会根据计算出的梯度更新模型的权重。优化器根据其内部定义的规则(如 SGD、Adam 等)来决定如何更新模型的参数。
  • 梯度告诉我们损失函数在当前参数位置的斜率方向,优化器利用这些信息来调整权重,目的是减少损失。

欢迎关注我的其它发布渠道