正则化(regularization):减小方差的策略。
那什么是方差呢?
误差 = 偏差 + 方差 + 噪声
偏差:度量了学习算法的期望预测与真实结果之间的偏离程度,即刻画了学习算法的拟合能力;
方差:度量了同样大小的训练集的变动所导致的学习性能的变化,即刻画了数据扰动所造成的影响;
噪声:表达了当前任务上任何学习算法所能达到的期望泛化误差的下届。
可从下图来理解:
从上图中可看出方差就是描述训练集与验证集之间的差异,偏差是描述训练集和真实集之间的差异,刻画学习算法的拟合能力。正则化(regularization)就是解决方差过大的问题。下面通过一个线性回归的例子来加深理解一下方差和偏差的概念:
上图中蓝色的点是训练集,红色的点是测试集,假设训练的模型可以很好的拟合训练集的数据,而对测试集的数据拟合的不是很好,这就出现训练集和测试集相差较大,也就是高方差,通常将这种现象称为过拟合。引入正则化的目的就是降低方差,防止出现过拟合的现象。下面就来看一下正则化的方法。
正则化的思想就是在目标函数中加入正则项:
通常正则项是用来约束模型的复杂度的,有L1和L2正则项,表达式如下:
加入正则项就是希望代价函数小的同时Wi的值也小,这样每个样本的权重都很小,这样模型就不太会关注某种类型的样本,模型的参数也不会太复杂,有利于缓解过拟合现象。
上图的左侧是L1正则项,彩色的圆圈是Cost的等高线,也就是在同一条线上Cost的值是相同的,不如在A、B、C三点的Cost的值是相同的,但L1正则项的值是不同的,B点的正则项的值是最小的,也就是说B的目标函数是最小的,所以在L1正则项下找最优解,既能损失最小,又要保证权值最小,那么这个最优解在做坐标轴上,也就是上图中的B点,这时W1的值为0,有参数的权值为0。也就是说L1正则化会产生一些稀疏的解。
上图的右侧是L2正则化,同样在D、E、F三点的Cost值是相同的,但是E点的L2正则项的值是最小的,所以E的目标函数也是最小的。与L1正则化相比,L2正则化不会出现权值为0的情况,因此每一个参数的权值都不会是0.
【总结:L1和L2正则化的特点】
L1 正则化的特点:
L2 正则化的特点:
下面来通过代码学习一下pytorch中的L2正则项,L2 正则项又叫做 weight decay (权值衰减)。那为什么叫权值衰减?是如何衰减的呢?
参数的更新公式: w i + 1 = w i − ∂ L o s s ∂ w i w_{i+1} = w_{i} -\frac{\partial Loss }{\partial w_{i} } wi+1=wi−∂wi∂Loss
在Obj 加一个L2正则项 O b j = L o s s + λ 2 ∗ ∑ i N w i 2 Obj = Loss + \frac{\lambda }{2} * \sum_{i}^{N}w _{i}^{2} Obj=Loss+2λ∗∑iNwi2 ,那么参数的更新方式为:
λ \lambda λ的取值是 0-1 的,那么就是说每一次迭代之后,这个参数 Wi 本身也会发生一个衰减。也就是加上 L2 正则项与没有加 L2 正则项进行一个对比的话,加入 L2 正则项, 这里的 Wi 就会发生数值上的一个衰减。故这就是这个 L2 正则项称为权值衰减的原因。
# ============================ step 1/5 数据 ============================
def gen_data(num_data=10, x_range=(-1, 1)):
w = 1.5
train_x = torch.linspace(*x_range, num_data).unsqueeze_(1)
train_y = w*train_x + torch.normal(0, 0.5, size=train_x.size())
test_x = torch.linspace(*x_range, num_data).unsqueeze_(1)
test_y = w*test_x + torch.normal(0, 0.3, size=test_x.size())
return train_x, train_y, test_x, test_y
train_x, train_y, test_x, test_y = gen_data(x_range=(-1, 1))
# ============================ step 2/5 模型 ============================
class MLP(nn.Module):
def __init__(self, neural_num):
super(MLP, self).__init__()
self.linears = nn.Sequential(
nn.Linear(1, neural_num),
nn.ReLU(inplace=True),
nn.Linear(neural_num, neural_num),
nn.ReLU(inplace=True),
nn.Linear(neural_num, neural_num),
nn.ReLU(inplace=True),
nn.Linear(neural_num, 1),
)
def forward(self, x):
return self.linears(x)
# 建立两个模型,一个不带正则项,一个带正则项
net_normal = MLP(neural_num=n_hidden)
net_weight_decay = MLP(neural_num=n_hidden)
# ============================ step 3/5 优化器 ============================
optim_normal = torch.optim.SGD(net_normal.parameters(), lr=lr_init, momentum=0.9)
optim_wdecay = torch.optim.SGD(net_weight_decay.parameters(), lr=lr_init, momentum=0.9, weight_decay=1e-2)
# ============================ step 4/5 损失函数 ============================
loss_func = torch.nn.MSELoss()
# ============================ step 5/5 迭代训练 ============================
writer = SummaryWriter(comment='_test_tensorboard', filename_suffix="12345678")
for epoch in range(max_iter):
# forward
pred_normal, pred_wdecay = net_normal(train_x), net_weight_decay(train_x)
loss_normal, loss_wdecay = loss_func(pred_normal, train_y), loss_func(pred_wdecay, train_y)
optim_normal.zero_grad()
optim_wdecay.zero_grad()
loss_normal.backward()
loss_wdecay.backward()
optim_normal.step()
optim_wdecay.step()
if (epoch+1) % disp_interval == 0:
# 可视化
for name, layer in net_normal.named_parameters():
writer.add_histogram(name + '_grad_normal', layer.grad, epoch)
writer.add_histogram(name + '_data_normal', layer, epoch)
for name, layer in net_weight_decay.named_parameters():
writer.add_histogram(name + '_grad_weight_decay', layer.grad, epoch)
writer.add_histogram(name + '_data_weight_decay', layer, epoch)
test_pred_normal, test_pred_wdecay = net_normal(test_x), net_weight_decay(test_x)
# 绘图
plt.scatter(train_x.data.numpy(), train_y.data.numpy(), c='blue', s=50, alpha=0.3, label='train')
plt.scatter(test_x.data.numpy(), test_y.data.numpy(), c='red', s=50, alpha=0.3, label='test')
plt.plot(test_x.data.numpy(), test_pred_normal.data.numpy(), 'r-', lw=3, label='no weight decay')
plt.plot(test_x.data.numpy(), test_pred_wdecay.data.numpy(), 'b--', lw=3, label='weight decay')
plt.text(-0.25, -1.5, 'no weight decay loss={:.6f}'.format(loss_normal.item()), fontdict={'size': 15, 'color': 'red'})
plt.text(-0.25, -2, 'weight decay loss={:.6f}'.format(loss_wdecay.item()), fontdict={'size': 15, 'color': 'red'})
plt.ylim((-2.5, 2.5))
plt.legend(loc='upper left')
plt.title("Epoch: {}".format(epoch+1))
plt.show()
plt.close()
在优化器中加入weight_decay这个参数即可加入正则项,下面看一下加入正则项和不加入正则项的模型训练效果:
tensorboard可视化结果:
不带正则化的模型参数分布从迭代开始到结束整个权值的分布没有什么变化,而加入正则项之后权值随着迭代次数的增加不断减小,以至于模型不会过于复杂而产生过拟合。
Dropout是指在神经网络的训练过程中,按照一定的概率将部分神经元从网络中丢弃,相当于从原来的网络中寻找一个更瘦的网络。在训练大型神经网络时,如果训练数据过少,很容易引起过拟合,使用Dropout来减小网络的规模,能起到很好的作用。
在训练阶段,Dropout以一定的概率p随机丢弃一部分神经元节点,即这部分神经元节点不参与计算,如下图所示:
对应的公式如下:
没有dropout的神经网络:
有dropout的神经网络:
在训练过程中,每一个神经元加了一个概率,上式中的r是服从伯努利分布的,取值有0和1,概率分别是p和1-p。
预测的时候,每个神经元都是存在的,权重参数w要乘以p,成为:pw
# p 为舍弃概率
torch.nn.Dropout(p=0.5, inplace=False)
还是以上面的例子来看一下加入dropout和没有加入的模型的训练效果:
# 在激活层后加入dropout层
class MLP(nn.Module):
def __init__(self, neural_num, d_prob=0.5):
super(MLP, self).__init__()
self.linears = nn.Sequential(
nn.Linear(1, neural_num),
nn.ReLU(inplace=True),
nn.Dropout(d_prob),
nn.Linear(neural_num, neural_num),
nn.ReLU(inplace=True),
nn.Dropout(d_prob),
nn.Linear(neural_num, neural_num),
nn.ReLU(inplace=True),
nn.Dropout(d_prob),
nn.Linear(neural_num, 1),
)
def forward(self, x):
return self.linears(x)
#不带dropout和带dropout的网络
net_prob_0 = MLP(neural_num=n_hidden, d_prob=0.)
net_prob_05 = MLP(neural_num=n_hidden, d_prob=0.5)
# 优化器去掉L2正则项
optim_normal = torch.optim.SGD(net_prob_0.parameters(), lr=lr_init, momentum=0.9)
optim_reglar = torch.optim.SGD(net_prob_05.parameters(), lr=lr_init, momentum=0.9)
# 在模型测试部分加入
net_prob_0.eval()
net_prob_05.eval()
上面代码需要注意的是①dropout在网络层中的位置;②dropout的操作,模型的训练和测试是不一样的,因此用net.eval()和net.train()来区分模型的训练过程。
输出结果:
可看出不加入dropout的模型明显产生过拟合,而加入dropout和加入L2正则项效果差不多,下面通过tensorboard可视化来观察一下参数梯度的分布:
可看出加入dropout和L2有类似的效果,有利用权重收缩。
BN来源于论文《BatchNormalization:Accelerating Deep Network Train by Reducing Internal Covariate Shift》,在前面的文章中基于这篇论文已经对BN的原理做了详细的理解(链接)。具体来说BN有以下优点:
1、可以用更大的学习率,加速模型收敛;
2、可以不用精心设计权值初始化;
3、可以不用dropout或较小的dropout;
4、可以不用L2或者较小的weight decay;
5、可以不用LRN(local response normalization);
下面从代码中来看一下BN的作用
# 构建一个简单的网络结构
class MLP(nn.Module):
def __init__(self, neural_num, layers=100):
super(MLP, self).__init__()
self.linears = nn.ModuleList([nn.Linear(neural_num, neural_num, bias=False) for i in range(layers)])
self.bns = nn.ModuleList([nn.BatchNorm1d(neural_num) for i in range(layers)])
self.neural_num = neural_num
def forward(self, x):
for (i, linear), bn in zip(enumerate(self.linears), self.bns):
x = linear(x)
# x = bn(x)
x = torch.relu(x)
if torch.isnan(x.std()):
print("output is nan in {} layers".format(i))
break
print("layers:{}, std:{}".format(i, x.std().item()))
return x
def initialize(self):
for m in self.modules():
if isinstance(m, nn.Linear):
# method 1
# nn.init.normal_(m.weight.data, std=1) # normal: mean=0, std=1
# method 2 kaiming
nn.init.kaiming_normal_(m.weight.data)
neural_nums = 256
layer_nums = 100
batch_size = 16
net = MLP(neural_nums, layer_nums)
# net.initialize()
inputs = torch.randn((batch_size, neural_nums)) # normal: mean=0, std=1
output = net(inputs)
print(output)
在不加入BN时输出结果:
采用权值初始化,由于激活函数是relu因此用Kaiming 初始化方法,输出结果如下:
这里的权值初始化是根据激活函数来选择的,假设不用权值初始化方法,而是在网络层中加入BN,输出效果如下:
从上面的三个输出结果可看到,在网络中加入BN可以保证数据输出的尺度,这样就可以不用考虑用哪种初始化方法了。
Pytorch中的BN
pytorch中提供了三种BatchNorm方法:
nn.BatchNorm1d
nn.BatchNorm2d
nn.BatchNorm3d
上面三个BatchNorm 方法都继承了 __BatchNorm 这个基类,初始化参数如下:
参数:
num_features:一个样本特征数量;
eps:分母修正项;
momentum:指数加权平均,估计当前的mean/var
affine:是否需要 affine transform
track_running_stats:是训练状态还是测试状态,如果是训练状态是需要重新计算mean和var的,如果是测试状态就用训练时统计的均值和方差。
而BatchNorm的这三个方法也是有属性的:
running_mean:均值
running_var:方差
weight:affine transform 中的 γ \gamma γ
bias: affine transform 中的 β \beta β
这四个属性分别对应公式中的四个属性:
这里的均值和方差是采用指数加权平均进行计算的, 不仅要考虑当前 mini-batch 的均值和方差,还考虑上一个 mini-batch 的均值和方差(当然是在训练的时候,测试的时候是用当前的统计值。)
running_mean = (1-momentum) * pre_running_mean + momentum*mean_t
running_var = (1-momentum) * pre_running_var + momentum * var_t
下面来看一下这三个方法的区别:
1、nn.BatchNorm1d -> input = B ✖️ 特征数 ✖️ 1d 特征
假设有三个样本,每个样本有5个特征,每个特征是1维的,那么输入为:3 ✖️5 ✖️1,计算均值和标准差都是以特征为单位的,在这里就是横向计算的。
下面一维的BN的实现:
# 3个样本
batch_size = 3
# 5个特征
num_features = 5
# 计算均值和方差时用到的动量值
momentum = 0.3
features_shape = (1)
feature_map = torch.ones(features_shape) # 1D
feature_maps = torch.stack([feature_map*(i+1) for i in range(num_features)], dim=0) # 2D
feature_maps_bs = torch.stack([feature_maps for i in range(batch_size)], dim=0) # 3D
print("input data:\n{} shape is {}".format(feature_maps_bs, feature_maps_bs.shape))
bn = nn.BatchNorm1d(num_features=num_features, momentum=momentum)
running_mean, running_var = 0, 1
for i in range(2):
outputs = bn(feature_maps_bs)
print("\niteration:{}, running mean: {} ".format(i, bn.running_mean))
print("iteration:{}, running var:{} ".format(i, bn.running_var))
mean_t, var_t = 2, 0
running_mean = (1 - momentum) * running_mean + momentum * mean_t
running_var = (1 - momentum) * running_var + momentum * var_t
print("iteration:{}, 第二个特征的running mean: {} ".format(i, running_mean))
print("iteration:{}, 第二个特征的running var:{}".format(i, running_var))
输出结果:
所以当前mini-batch所得到的用于对数据Normalize的均值不是当前数据的均值,而是要考虑上一次跌代mini-batch的信息,加权平均后得到当前mini-batch的均值和方差。
2、nn.BatchNorm2d -> input = B * 特征数 * 2d 特征
这里有三个样本,每一个样本是3个特征图,每个特征图是2维的,所以输入的维度是33(2*2)
3、nn.BatchNorm3d -> input = B * 特征数 * 3d 特征
这里有3个样本,每个样本有3个特征,每个特征是3维的,输入的维度是33(223)
常用的Normalization有:Batch Normalization、Layer Normalization (LN)、Instance Normalization (IN)、Group Normalization (GN)
这四种方法的相同点就是公式上:
不同点是 μ B \mu _{B} μB 和 σ B \sigma_{B} σB 的方式不同。Batch Normalization 是在batch上计算均值和方差;Layer Normalization是以层为单位计算均值和方差的;Instance Normalization 主要在图像生成方法中使用;Group Normalization 是按组为单位计算均值和方差。
1、Layer Normalization
起因:BN不适合变长的网络,如RNN,所以提出了逐层计算均值和方差的思路。
如上图所以,由3个样本,但是每个样本的序列长度不一样,这样就没法以batch来计算均值和方差,所以通过按层来计算均值和方差。
BN和LN的区别:
LN中同层神经元输入具有相同的均值和方差,不同的输入样本由不同的均值和方差;
BN中则针对不同的神经元输入计算均值和方差,同一个batch中的输入具有相同的均值和方差;
还要注意, 在 LN 中不再有 running_mean 和 running_var, 并且 gamma 和 beta 为逐元素的。下面看一下Pytorch 中的 LN:
nn.LayerNorm (normalized_shape, eps=1e-05, elementwise_affine=True)
主要参数:
normalized_shape:该层的特征形状;
eps:分母修正项;
elementwise_affine:是否需要 affine transform
2、Instance Normalization
起因:BN 在图像生成中不适用, 思路就是逐个 Instance(channel) 计算均值和方差。不如在图像风格迁移中,每一个样本的风格是不一样的,所以不能像BN那样从多个样本中计算均值和方差,而是逐个样本计算均值和方差。
pytorch 中Instance Normalization的实现:
nn.InstanceNorm2d (num_features, eps=1e-05, momentum=0.1, affine=False, track_running_stats=False)
主要参数:
num_features:一个样本特征数量;
eps:分母修正项
momentum:指数加权平均计算当前的mean/var
affine:是否需要affine transform
track_running_stats:是训练状态还是测试状态;
3、Group Normalization
起因:小 batch 样本中, BN 估计的值不准, 这种 Normalization 的思路就是数据不够, 通道来凑。 一样用于大模型(小 batch size)的任务。
假设只有两个样本,如上图所示,如果使用BN ,以行计算均值和方差,样本太少数据不够,所以可以从调整图的维度上分组求均值和方差。
pytorch中Group Normalization的实现:
nn.GroupNorm (num_groups, num_channels, eps=1e-05, affine=True)
主要参数:
num_groups:分组数
num_channels:通道数
eps:分母修正项
affine:是否需要affine transform
上面就是四种标准化的方法了,BN, LN, IN 和 GN 都是为了克服Internal Covariate Shift (ICS)提出来的,它们的计算公式差不多,只不过计算均值和方差的时候采用的方式不同, 如下图所示: