凡是能解决模型泛化误差而不是训练误差的方法,都被称为正则化。
模型的泛化误差主要是由模型过拟合引起的,所以正则化的各种方法用于解决模型过拟合的问题。
L1和L2正则化的核心思想就是限制模型参数的取值范围。 模型取值范围大同样可以训练出一个泛化能力强的模型,那为什么要限制模型参数的取值范围呢?
模型取值范围大同样可以训练出一个泛化能力强的模型,但是出现过拟合的几率也大大提升了(可以选择的范围大,自然就选了一整套相互配合起来可以让损失最小的参数,但是这些参数有可能只是在迎合训练集)。另一方面,参数取得太大会放大输入模型的样本之中的噪声,让输出结果失真。
综上所述,无论是从参数取值范围大会提高过拟合几率的角度来看,还是从参数太大会放大噪声的角度来看,参数取值范围太大都是非常不利的,所以需要对范围进行限制。
明白了L1和L2正则化的核心思想就是限制模型参数的取值范围之后,来解决下一个问题:如何减小模型参数的取值范围?
首先,理解一下L1和L2正则化中的L1和L2是什么意思。L1和L2就是L1范数和L2范数。L1范数是我们非常熟悉的曼哈顿距离,L2范数也是非常熟悉的欧式距离。对于一个向量 ω ⃗ \vec{\omega} ω 而言,其L1范数和L2范数分别是:
L 1 范数: ∣ ∣ ω ⃗ ∣ ∣ 1 = ∣ ω 1 ∣ + ∣ ω 2 ∣ + … + ∣ ω n ∣ L 2 范数: ∣ ∣ ω ⃗ ∣ ∣ 2 = ω 1 2 + ω 2 2 + … + ω n 2 L1范数:||\vec{\omega}||_1=|\omega_1|+|\omega_2|+…+|\omega_n| \\ L2范数:||\vec{\omega}||_2=\sqrt{\omega_1^2+\omega_2^2+…+\omega_n^2} L1范数:∣∣ω∣∣1=∣ω1∣+∣ω2∣+…+∣ωn∣L2范数:∣∣ω∣∣2=ω12+ω22+…+ωn2
在损失函数之中,在尾项之中加入L2正则项,为梯度下降加入减小权重的目标,就可以在减小损失的同时减小权重。假设原本的损失函数是 ι ( ω ⃗ , b ⃗ ) \iota(\vec{\omega},\vec{b}) ι(ω,b),改正之后的损失函数就是:
ι ′ ( ω ⃗ , b ⃗ ) = ι ( ω ⃗ , b ⃗ ) + λ 2 L 2 2 ( w ⃗ ) \iota'(\vec{\omega},\vec{b})=\iota(\vec{\omega},\vec{b})+\frac{\lambda}{2} L_2^2(\vec{w}) ι′(ω,b)=ι(ω,b)+2λL22(w)其中, λ \lambda λ是一个超参数,用来控制正则项的惩罚力度。越大,则最终权重会越小。
L1范数和L2范数作为正则项的区别在于,L1范数可以带来稀疏性。从L1范数的图像之中可以看出,L1正则化之后的损失函数想要最小化, ω ⃗ \vec{\omega} ω 的取值相比起L2正则化更容易接近或者落在坐标轴上,这意味着会将某些权重的值设置为0或者接近于0,权重消失或者接近于消失,就是所谓“带来稀疏性”。
ι ′ ( ω ⃗ , b ⃗ ) \iota'(\vec{\omega},\vec{b}) ι′(ω,b)对 ω \omega ω求梯度,之后利用梯度下降更新权重,可以得到:
ω ⃗ t + 1 = ω ⃗ t − η ( ∂ ι ( ω ⃗ , b ⃗ ) ∂ ω ⃗ + λ ω ⃗ ) = ( 1 − λ η ) ω ⃗ t − η ∂ ι ( ω ⃗ , b ⃗ ) ∂ ω ⃗ t \vec{\omega}_{t+1}=\vec{\omega}_t-\eta(\frac{\partial \iota(\vec{\omega},\vec{b})}{\partial \vec{\omega}}+\lambda\vec{\omega})=(1-\lambda\eta)\vec{\omega}_t-\eta\frac{\partial \iota(\vec{\omega},\vec{b})}{\partial \vec{\omega}_t} ωt+1=ωt−η(∂ω∂ι(ω,b)+λω)=(1−λη)ωt−η∂ωt∂ι(ω,b)由于 λ η \lambda\eta λη是小于1的,所以每一次梯度下降的时候,权重都会衰减。
故L2正则化也称为权重衰减。
在pytorch之中,进行权重衰减非常简单,只需要在梯度下降的函数中加入一个参数即可。如下:
# 梯度下降优化器(对权重w设置权重衰减。默认会对权重和偏置都做权重衰减,虽然偏置没有必要做权重衰减)
trainer = torch.optim.SGD(net.parameters(), lr=lr, weight_decay=1e-4)
训练一个模型的完整源码如下:
import torch
import matplotlib.pyplot as plt
import numpy as np
from torch.utils import data
from torchvision import datasets
from torchvision import transforms
from torch import nn
def get_train_test_loader(image_size=28, train_batch_size=10, test_batch_size=10, num_workers=0, is_download=True):
"""
获得训练数据生成器和验证数据生成器,这个数据集总共有10个类别(即10个标签)
:param image_size: 图片的大小,取28
:param train_batch_size: 数据生成器的批量大小
:param num_workers: 数据生成器每次读取时调用的线程数量
:param is_download: 是否要下载数据集(如果还未下载设置为True)
:return: 训练数据生成器和验证数据生成器
"""
data_transform = transforms.Compose([
# 设置图片大小
transforms.Resize(image_size),
# 转化为tensor张量
transforms.ToTensor()
])
train_data = datasets.FashionMNIST(root='../data', train=True, download=is_download, transform=data_transform)
test_data = datasets.FashionMNIST(root='../data', train=False, download=is_download, transform=data_transform)
train_loader = data.DataLoader(train_data, batch_size=train_batch_size, shuffle=True, num_workers=num_workers,
drop_last=True)
test_loader = data.DataLoader(test_data, batch_size=test_batch_size, shuffle=False, num_workers=num_workers,
drop_last=True)
return train_loader, test_loader
def accuracy(y_hat, y):
"""模型训练完成后,判断预测结果的准确率"""
if y_hat.shape[0] < 2 and y_hat.shape[1] < 2:
raise ValueError("dimesion error")
# 得到预测的y_hat每一行中最大概率所在的索引(索引即类别)
y_hat = y_hat.argmax(axis=1)
# 判断预测类别是否与实际类别相等
judge = y_hat.type(y.dtype) == y
# 现在cmp是一个bool类型的向量,转成0和1,统计1的数量
return float(judge.type(y.dtype).sum()) / len(y)
def init_weights(m):
"""将网络中每一个线性层的所有权重都利用标准差为0.01的正态分布进行初始化,b没有初始化,所以初始为0"""
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)
# 学习率
lr = 0.03
# 批量大小
batch_size = 100
test_batch_size = 10000
# dropout概率
p = 0.5
# 最开始一个展平层用来给输入的x整形,接下来第一层是线性层,结果经过RuLU激活之后,进入下一个线性层,之后结果进行输出。隐藏层共有392个神经元
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 392), nn.ReLU(), nn.Dropout(p),
nn.Linear(392,100), nn.ReLU(), nn.Dropout(p), nn.Linear(100, 10))
# apply会将net中的每一层都作为参数进入init_weights,当发现是线性层,会对线性层的w自动初始化
net.apply(init_weights)
# 损失函数是交叉熵函数,参数是y_hat和y,注意,会对传入的y_hat先进行一次softmax处理
loss = nn.CrossEntropyLoss()
# 梯度下降优化器(对权重w设置权重衰减。默认会对权重和偏置都做权重衰减,虽然偏置没有必要做权重衰减)
trainer = torch.optim.SGD(net.parameters(), lr=lr, weight_decay=1e-4)
# 获取数据生成器以及数据
train_loader, test_loader = get_train_test_loader(train_batch_size=batch_size, test_batch_size=test_batch_size,
is_download=False)
train_loader_test, _ = get_train_test_loader(train_batch_size=60000, is_download=False)
# 学习代数
num_epoch = 10
# 用来正常显示中文标签
plt.rcParams['font.sans-serif'] = ['SimHei']
# 用来正常显示负号
plt.rcParams['axes.unicode_minus'] = False
# 交互模式
plt.ion()
fig = plt.figure()
ax = fig.add_subplot()
ax.grid(True, axis="y")
epoch_x, validate_accuracy_y, test_accuracy_y, loss_y = [], [], [], []
line_validate, = ax.plot(epoch_x,validate_accuracy_y,color='black',linestyle="--",label="训练集准确率")
line_test, = ax.plot(epoch_x,test_accuracy_y,color='blue',label="测试集准确率")
line_loss, = ax.plot(epoch_x,loss_y,color='red',label="损失函数")
ax.set(xticks=np.arange(0,12,1),xlim=(0,11),yticks=np.arange(0,1.1,0.05),ylim=(0,1))
ax.legend()
for i in range(num_epoch):
epoch_x.append(i + 1)
for x, y in train_loader:
# 模型得到预测值
y_hat = net(x)
# 损失函数
l = loss(y_hat, y)
print(f"\r{batch_size}个批量样本损失为{l}", end="", flush=True)
trainer.zero_grad()
# 求偏导
l.backward()
# 梯度下降
trainer.step()
with torch.no_grad():
for x, y in train_loader_test:
y_hat = net(x)
l = loss(y_hat, y)
loss_y.append(l)
print(f"\n第{i}代,所有训练样本损失为{l}")
validate_accuracy = accuracy(y_hat, y)
print(f"验证集预测正确率:{validate_accuracy}")
validate_accuracy_y.append(validate_accuracy)
for x, y in test_loader:
test_accuracy = accuracy(net(x), y)
print(f"测试集预测正确率:{test_accuracy}")
test_accuracy_y.append(test_accuracy)
print("=" * 25)
line_validate.set_data(epoch_x,validate_accuracy_y)
line_test.set_data(epoch_x,test_accuracy_y)
line_loss.set_data(epoch_x,loss_y)
plt.draw()
plt.pause(0.1)
plt.ioff()
plt.show()