权重衰退
如何控制一个模型的容量呢,有下面的方法:
- 将模型变得比较小,参数变得比较少
- 使得每个参数选择的值的范围比较小
权重衰退就是使用上述第二种方法来进行模型容量的控制。
使用均方范数作为硬性限制
- 通过限制参数值的选择范围来控制模型容量
min ℓ ( w , b ) subject to ∥ w ∥ 2 ≤ θ \min \ell(w, b) \quad \text{subject to} \quad \|w\|^2 \leq \theta minℓ(w,b)subject to∥w∥2≤θ
- 通常不限制偏移 b b b (限不限制都差不多)
- 小的 θ \theta θ 意味着更强的正则项
但是一般来说,我们是不会直接使用上述的优化函数的,因为其优化起来相对来说麻烦一点。
常用的是下面的这个函数:
使用均方范数作为柔性限制
- 对每个 θ \theta θ,都可以找到 λ \lambda λ 使得之前的目标函数等价于下面
min ℓ ( w , b ) + λ 2 ∥ w ∥ 2 \min \ell(w, b) + \frac{\lambda}{2} \|w\|^2 minℓ(w,b)+2λ∥w∥2
- 可以通过拉格朗日乘子来证明
- 超参数 λ \lambda λ 控制了正则项的重要程度
- λ = 0 \lambda = 0 λ=0: 无作用
- λ → ∞ , w ∗ → 0 \lambda \to \infty, w^* \to 0 λ→∞,w∗→0
上述图片可以理解加入柔性限制后对模型是如何影响的。
具体来说:假设上述的绿色等高线就是 ℓ \ell ℓ , 那么我只想优化 w ~ ∗ \tilde{\mathbf{w}}^* w~∗ 的话,那么我的最优解就是在图中的绿色圆点处。这是只优化我的损失的情况。
要是我在上述损失函数的基础上加入了 λ 2 ∥ w ∥ 2 \frac{\lambda}{2} \|\mathbf{w}\|^2 2λ∥w∥2 这一项,由于其也是一个二次函数,则可以认为其等高线的位置在以原点为中心(图中橙色的线)。那么既然引入了这一项,就导致原始的绿色圆点这个点就不是特别优了,因为这个点对于我的黄线来说其损失项非常大。对于平方损失函数来说,我在原点附近的位置对值的拉伸是比较的小的,因为其梯度相对来说比较的小,但是在离原点比较远的时候对值的拉伸是比较的大的。因此在绿色圆点处。 λ 2 ∥ w ∥ 2 \frac{\lambda}{2} \|\mathbf{w}\|^2 2λ∥w∥2 对该点的拉力大于 w ~ ∗ \tilde{\mathbf{w}}^* w~∗ 对该点的拉力。最后拉到 w ∗ \mathbf{w}^* w∗ 处形成平衡点。
参数更新法则
- 计算梯度 ∂ ∂ w ( ℓ ( w , b ) + λ 2 ∥ w ∥ 2 ) = ∂ ℓ ( w , b ) ∂ w + λ w \frac{\partial}{\partial \mathbf{w}} \left( \ell(\mathbf{w}, b) + \frac{\lambda}{2} \|\mathbf{w}\|^2 \right) = \frac{\partial \ell(\mathbf{w}, b)}{\partial \mathbf{w}} + \lambda \mathbf{w} ∂w∂(ℓ(w,b)+2λ∥w∥2)=∂w∂ℓ(w,b)+λw
- 时间 t 更新参数 w t + 1 = ( 1 − η λ ) w t − η ∂ ℓ ( w t , b t ) ∂ w t \mathbf{w}_{t+1} = (1 - \eta \lambda) \mathbf{w}_t - \eta \frac{\partial \ell(\mathbf{w}_t, b_t)}{\partial \mathbf{w}_t} wt+1=(1−ηλ)wt−η∂wt∂ℓ(wt,bt)
- 通常 η λ < 1 \eta \lambda < 1 ηλ<1,在深度学习中通常叫做权重衰退
我们可以发现,上述更新函数和之前唯一不同的地方在于,其多了一项 − η λ w t - \eta \lambda\mathbf{w}_t −ηλwt。因此因为 λ \lambda λ 的引入,我们在每次更新前就会将权重进行缩小,于是叫做权重衰退。
代码实现
下面使用一个简单的例子来展示线性回归:
首先导入必要的包:
%matplotlib inline
import torch
from torch import nn
from d2l import torch as d2l
接着,依旧生成一些数据,生成公式如下: y = 0.05 + ∑ i = 1 d 0.01 x i + ϵ where ϵ ∼ N ( 0 , 0.0 1 2 ) . y = 0.05 + \sum_{i = 1}^d 0.01 x_i + \epsilon \text{ where }\epsilon \sim \mathcal{N}(0, 0.01^2). y=0.05+i=1∑d0.01xi+ϵ where ϵ∼N(0,0.012).我们选择标签是关于输入的线性函数。标签同时被均值为0,标准差为0.01高斯噪声破坏。为了使过拟合的效果更加明显,我们可以将问题的维数增加到 d = 200 d = 200 d=200 ,并使用一个只包含20个样本的小训练集。
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)
# 使用 d2l.synthetic_data 函数根据给定的权重和偏置生成训练数据。
# 这个函数通常会向线性关系中添加一些噪声以模拟真实世界的数据集。
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)
从 0 开始实现
只需要将 L 2 L_2 L2 的平方惩罚添加到原始目标函数中。
初始化模型参数
def init_params():
# 创建一个形状为(num_inputs,1)的张量
w = torch.normal(0, 1, size=(num_inputs, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
return [w, b]
定义 L 2 L_2 L2 范数惩罚
# 定义范数惩罚是为了防止模型过拟合
def l2_penalty(w):
return torch.sum(w.pow(2)) / 2
定义训练代码实现
def train(lambd):#lambd是一个超参数,用于控制L2范数惩罚项的强度
w, b = init_params()
# net用于线性回归模型的预测,loss定义了损失函数为平方损失函数
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:
# 增加了L2范数惩罚项,
# 广播机制使l2_penalty(w)成为一个长度为batch_size的向量
l = loss(net(X), y) + lambd * l2_penalty(w) # 计算损失函数加上L2范数惩罚项
l.sum().backward() # 计算损失l的梯度
d2l.sgd([w, b], lr, batch_size) # 使用随机梯度下降法更新w和b
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())
忽略正则化直接训练
现在用 lambd = 0
禁用权重衰减后运行上述代码。发现,训练误差有了减少,但测试误差没有减少,这意味着出现了严重的过拟合。
使用权重衰退
使用权重衰减来运行代码。发现,训练误差增大,但测试误差减小。这正是期望从正则化中得到的效果。
简洁实现
下面的代码在实例化优化器时直接通过 weight_decay
指定 weight decay 超参数。默认情况下, PyTorch 同时衰减权重和偏移。这里我们只为权重设置了 weight_decay
,所以偏置参数 b b b 不会衰减。
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, 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:
trainer.zero_grad() # 清空梯度
l = loss(net(X), y)
l.mean().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())
设置不同的 wd 进行运行,结果如下:
忽略正则化 :
使用权重衰退 :
QA思考
Q1:为什么模型参数不过大复杂度就比较的低?
A1:其实不是说参数不大复杂度就低,实际上是限制整个模型在优化的时候是在一个很小的范围里面取参数。要是在一个小范围里面取参数,你的模型空间就会变小。
简单来说就是一个多项式中的高次项的系数变小了,函数也就变平滑了。
如下图:
如果要拟合上述红色点的话,要是允许模型的参数选的比较的大的话,可以做出来一个很复杂的曲线使其拟合(图中蓝色的线),这样会造成一个非常不平滑的曲线。现在就是限制 weight 不能太大,只能在一些平滑的曲线里面选择,不让学特别大的曲线,那么这样的话模型的复杂度就变低。
后记
附上我根据理解后写的代码:
import torch
from torch import nn
import matplotlib.pyplot as plt
class Animator:
def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None,
ylim=None, xscale='linear', yscale='linear',
fmts=('-', 'm--', 'g-.', 'r:'), figsize=(3.5, 2.5)):
if legend is None:
legend = []
self.xlabel = xlabel
self.ylabel = ylabel
self.legend = legend
self.xlim = xlim
self.ylim = ylim
self.xscale = xscale
self.yscale = yscale
self.fmts = fmts
self.figsize = figsize
self.X, self.Y = [], []
def add(self, x, y):
if not hasattr(y, "__len__"):
y = [y]
n = len(y)
if not hasattr(x, "__len__"):
x = [x] * n
if not self.X:
self.X = [[] for _ in range(n)]
if not self.Y:
self.Y = [[] for _ in range(n)]
for i, (a, b) in enumerate(zip(x, y)):
if a is not None and b is not None:
self.X[i].append(a)
self.Y[i].append(b)
def show(self):
plt.figure(figsize=self.figsize)
for x_data, y_data, fmt in zip(self.X, self.Y, self.fmts):
plt.plot(x_data, y_data, fmt)
plt.xlabel(self.xlabel)
plt.ylabel(self.ylabel)
if self.legend:
plt.legend(self.legend)
if self.xlim:
plt.xlim(self.xlim)
if self.ylim:
plt.ylim(self.ylim)
plt.xscale(self.xscale)
plt.yscale(self.yscale)
plt.grid()
plt.show()
# 自定义 load_array 函数
def load_array(data_arrays, batch_size, is_train=True):
dataset = torch.utils.data.TensorDataset(*data_arrays)
return torch.utils.data.DataLoader(dataset, batch_size, shuffle=is_train)
# 生成数据集(weight, bias, data_num)
def synthetic_data(w, b, num_examples):
X = torch.normal(0, 1, (num_examples, len(w)))
y = torch.matmul(X, w) + b
y += torch.normal(0, 0.01, y.shape) # 加噪
return X, y.reshape((-1, 1)) # 将y转换为列向量
# 定义线性模型
def linreg(X, w, b):
return torch.matmul(X, w) + b
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]
# 定义损失函数
def squared_loss(y_hat, y):
return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
# 定义范数惩罚是为了防止模型过拟合
def l2_penalty(w):
return torch.sum(w.pow(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_() # 梯度清零
# 积累 n 个指标
class Accumulator:
def __init__(self, n):
self.data = [0.0] * n
def add(self, *args):
self.data = [a + float(b) for a, b in zip(self.data, args)]
def reset(self):
self.data = [0.0] * len(self.data)
def __getitem__(self, idx):
return self.data[idx]
def evaluate_loss(net, data_iter, loss):
metric = Accumulator(2)
for X, y in data_iter:
out = net(X)
y = y.reshape(out.shape)
l = loss(out, y)
metric.add(l.sum(), l.numel())
return metric[0] / metric[1]
def train(lambd): # lambd 是一个超参数,用于控制 L2 范数惩罚项的强度
w, b = init_params()
net, loss = lambda X: linreg(X, w, b), squared_loss
num_epochs, lr = 100, 0.003 # 训练总轮次和学习率
animator = 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:
l = loss(net(X), y) + lambd * l2_penalty(w) # 计算损失函数加上 L2 范数惩罚项
l.sum().backward() # 计算损失 l 的梯度
sgd([w, b], lr, batch_size) # 使用随机梯度下降法更新 w 和 b
if (epoch + 1) % 5 == 0: # 周期性评估和绘图
animator.add(epoch + 1, (evaluate_loss(net, train_iter, loss),
evaluate_loss(net, test_iter, loss)))
animator.show()
print('w的L2范数是:', torch.norm(w).item())
if __name__ == "__main__":
# 参数设置
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 = synthetic_data(true_w, true_b, n_train)
train_iter = load_array(train_data, batch_size)
test_data = synthetic_data(true_w, true_b, n_test)
test_iter = load_array(test_data, batch_size, is_train=False)
# 训练模型
train(lambd=0)
train(lambd=3)
train(lambd=5)