【Pytorch 】笔记九:正则化与标准化大总结

时间:2022-07-24
本文章向大家介绍【Pytorch 】笔记九:正则化与标准化大总结,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

1. 写在前面

疫情在家的这段时间,想系统的学习一遍 Pytorch 基础知识,因为我发现虽然直接 Pytorch 实战上手比较快,但是关于一些内部的原理知识其实并不是太懂,这样学习起来感觉很不踏实, 对 Pytorch 的使用依然是模模糊糊, 跟着人家的代码用 Pytorch 玩神经网络还行,也能读懂,但自己亲手做的时候,直接无从下手,啥也想不起来, 我觉得我这种情况就不是对于某个程序练得不熟了,而是对 Pytorch 本身在自己的脑海根本没有形成一个概念框架,不知道它内部运行原理和逻辑,所以自己写的时候没法形成一个代码逻辑,就无从下手。这种情况即使背过人家这个程序,那也只是某个程序而已,不能说会 Pytorch, 并且这种背程序的思想本身就很可怕, 所以我还是习惯学习知识先有框架(至少先知道有啥东西)然后再通过实战(各个东西具体咋用)来填充这个框架。而这个系列的目的就是在脑海中先建一个 Pytorch 的基本框架出来,学习知识,知其然,知其所以然才更有意思;)**。

今天是该系列的第九篇文章,也即将接近尾声了,今天的正则化可是非常重要且实用的知识点,所以借着这个机会看看 Pytorch 中的各种正则化方法。首先, 会学习什么是正则化以及偏差方差之间的关联,然后学习 L2 正则化方法,然后介绍 Dropout 的相关知识, 然后是 Batch Normalization 和正则化层。

大纲如下:

  • 正则化之 weight_decay (正则化与偏差和方差, L2 正则化)
  • 正则化之 Dropout(概念以及如何用)
  • 标准化之 Batch Normalization
  • Normalization-layers(Layer Normalization、Instance Normalization、Groupb Normalization)

下面依然是一张思维导图把知识拎起来:

2. 正则化之 weight_decay

正则化从字面意思上可能一下子就懵逼,其实这是个纸老虎, 它就是一个减少方差的策略。那么这里就涉及到了一个概念方差, 什么是方差呢?

误差可分解为:偏差,方差与噪声之和。即误差 = 偏差 + 方差 + 噪声

  • 偏差度量了学习算法的期望预测与真实结果的偏离程度, 即刻画了学习算法本身的拟合能力
  • 方差度量了同样大小的训练集的变动所导致的学习性能的变化,即刻画了数据扰动所造成的影响
  • 噪声则表达了在当前任务上任何学习算法所能达到的期望泛化误差的下界。

看下面这个图:

我们通常所说的过拟合现象,也就是指的高方差,就是模型在训练集上训练的超级好,几乎全部都能拟合。但是这种情况如果换一个数据集往往就会非常差, 看看下面这个图像:

L1 正则化的特点:

  • 不容易计算, 在零点连续但不可导, 需要分段求导
  • L1 模型可以将 一些权值缩小到零(稀疏)
  • 执行隐式变量选择。这意味着一些变量值对结果的影响降为 0, 就像删除它们一样
  • 其中一些预测因子对应较大的权值, 而其余的(几乎归零)
  • 由于它可以提供稀疏的解决方案, 因此通常是建模特征数量巨大时的首选模型
  • 它任意选择高度相关特征中的任何一个,并将其余特征对应的系数减少到 0**
  • L1 范数对于异常值更具提抗力

L2 正则化的特点:

  • 容易计算, 可导, 适合基于梯度的方法
  • 将一些权值缩小到接近 0
  • 相关的预测特征对应的系数值相似
  • 当特征数量巨大时, 计算量会比较大
  • 对于有相关特征存在的情况,它会包含所有这些相关的特征, 但是相关特征的权值分布取决于相关性。
  • 对异常值非常敏感
  • 相对于 L1 正则会更加准确
#============== step 1/5 数据 ================
def gen_data (num_data=10, x_range=(-1, 1)):
 w = 1.5
 train_x = troch.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=200)
net_weight_decay = MLP (neural_num=200)

#===================step 3/5 优化器 ===================
optim_normal = torch.optim.SGD (net_normal.parameters (), lr=0.01, momentum=0.9)
optim_wdecay = torch.optim.SGD (net_weight_decay.parameters (), lr=0.01, 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 (2000):
 # forward
 pred_normal, pred_decay = 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) % 200 == 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 ()

L2 正则使用也比较简单,就是在优化器里面指定 weight_decay 这个参数即可。我们可以看一下正则化模型和不带正则化模型的效果:

可以清楚的发现,不带正则化的红色线发生了过拟合现象。下面通过 Tensorboard 观察一下梯度参数的一个分布情况,这里面可以明显的看出衰减表示的意思:

左边的是不带正则化的模型参数的分布情况,我们可以看到从迭代开始到结束整个权值的分布都没有什么变化,右边是加入了 weight decay 的分布, 可以看到这个衰减的趋势, 这说明 L2 正则起作用了,使得在迭代过程中权值在不断的缩减,以至于模型不会过于复杂产生过拟合。

那么这个 L2 正则是怎么实现的呢? 我们再通过调试的方式看看背后的运行机制, 在 optim_wdecay.step () 这一句代码前打上断点,然后 debug,步入,就进入了 sgd 的 step 方法:

好了,L2 正则化的使用和内部实现机制就到这里吧,要知道 L2 正则化干啥用,怎么用差不多就行了。一般是在模型过拟合的时候用到这个方式, 当然除了 L2 正则化,在模型发生过拟合的时候还有其他的方式,比如 Dropout,也是常用的一种方式,我们可以看看这是个什么东西?

3. 正则化之 Dropout

Dropout 技术也是解决过拟合问题的一个非常重要的一个手段, 那么什么叫做 Dropout 呢?Dropout 又应该怎么用呢?下面就分为两个方面来分别叙述:

3.1 Dropout 概念

Dropout 叫做随机失活。就是我们给出一个概率 (随机),让某个神经元的权重为 0 (失活)。下面给出个图直观感受一下:

就是每一层,让某些神经元不起作用,这样就就相当于把网络进行简化了 (左边和右边可以对比),我们有时候之所以会出现过拟合现象,就是因为我们的网络太复杂了,参数太多了,并且我们后面层的网络也可能太过于依赖前层的某个神经元,加入 Dropout 之后, 首先网络会变得简单,减少一些参数,并且由于不知道浅层的哪些神经元会失活,导致后面的网络不敢放太多的权重在前层的某个神经元,这样就减轻了一个过渡依赖的现象, 对特征少了依赖, 从而有利于缓解过拟合。 这个类似于我们期末考试的时候有没有,老师总是会给我们画出一个重点,但是由于我们不知道这些重点哪些会真的出现在试卷上,所以就得把精力分的均匀一些,都得看看, 这样保险一些,也能泛化一点,至少只要是这些类型的题都会做。而如果我们不把精力分的均匀一些,只关注某种题型, 那么准糊一波。

好了,上面就是我们 Dropout 的一个原理了, 也不是太难理解,尤其是有了这个期末考试的例子, 那么关于 Dropout 还有一个注意的问题,就是数据的尺度变化。这个是什么意思呢? 我们用 Dropout 的时候是这样用的:只在训练的时候开启 Dropout,而测试的时候是不用 Dropout 的,也就是说模型训练的时候会随机失活一部分神经元, 而测试的时候我们用所有的神经元,那么这时候就会出现这个数据尺度的问题,所以测试的时候,所有权重都乘以 1-drop_prob, 以保证训练和测试时尺度变化一致, drop_prob 是我们的随机失活概率。 这个应该怎么理解呢?我们依然拿上面那个图来说:

3.2 nn.Dropout

Pytorch 中给我们提供了 Dropout 层, nn.Dropout

这里的 p 就是被舍弃概率,也就是失活概率。

下面就用上面正则化 L2 的代码实例看看不用 L2,而是加上 Dropout 的效果:

# ============================ 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, 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),             # 注意这里用上了 Dropout, 我们看到这个 Dropout 是接在第二个 Linear 之前,Dropout 通常放在需要 Dropout 网络的前一层
            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),  # 通常输出层的 Dropout 是不加的,这里由于数据太简单了才加上
            nn.Linear (neural_num, 1),
        )

    def forward (self, x):
        return self.linears (x)


net_prob_0 = MLP (neural_num=n_hidden, d_prob=0.)
net_prob_05 = MLP (neural_num=n_hidden, d_prob=0.5)

# ============================ step 3/5 优化器 ============================
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)

# ============================ 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):

    pred_normal, pred_wdecay = net_prob_0 (train_x), net_prob_05 (train_x)
    loss_normal, loss_wdecay = loss_func (pred_normal, train_y), loss_func (pred_wdecay, train_y)

    optim_normal.zero_grad ()
    optim_reglar.zero_grad ()

    loss_normal.backward ()
    loss_wdecay.backward ()

    optim_normal.step ()
    optim_reglar.step ()

    if (epoch+1) % disp_interval == 0:

        net_prob_0.eval ()    # 这里要注意一下,Dropout 在训练和测试阶段不一样,这时候需要对网络设置一个状态
        net_prob_05.eval () # 这个.eval () 函数表示我们的网络即将使用测试状态, 设置了这个测试状态之后,才能用测试数据去测试网络, 否则网络怎么知道啥时候测试啥时候训练?

        # 可视化
        for name, layer in net_prob_0.named_parameters ():
            writer.add_histogram (name + '_grad_normal', layer.grad, epoch)
            writer.add_histogram (name + '_data_normal', layer, epoch)

        for name, layer in net_prob_05.named_parameters ():
            writer.add_histogram (name + '_grad_regularization', layer.grad, epoch)
            writer.add_histogram (name + '_data_regularization', layer, epoch)

        test_pred_prob_0, test_pred_prob_05 = net_prob_0 (test_x), net_prob_05 (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_prob_0.data.numpy (), 'r-', lw=3, label='d_prob_0')
        plt.plot (test_x.data.numpy (), test_pred_prob_05.data.numpy (), 'b--', lw=3, label='d_prob_05')
        plt.text (-0.25, -1.5, 'd_prob_0 loss={:.8f}'.format (loss_normal.item ()), fontdict={'size': 15, 'color': 'red'})
        plt.text (-0.25, -2, 'd_prob_05 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 ()

        net_prob_0.train ()   # 这里我们还得告诉网络接下来我们又要进入训练状态了
        net_prob_05.train ()

所以上面的代码里面要注意两点, 第一点就是 Dropout 加的时候注意放置的位置,第二点就是由于 Dropout 操作,模型训练和测试是不一样的,上面我们说了,训练的时候采用 Dropout 而测试的时候不用 Dropout, 那么我们在迭代的时候,就得告诉网络目前是什么状态,如果要测试,就得先用 .eval () 函数告诉网络一下子,训练的时候就用 .train () 函数告诉网络一下子。我们下面看一下 Dropout 正则化后的效果:

可以清楚的发现, 不带正则化的红色线发生了过拟合现象,并且 Dropout 的效果和 L2 正则差不多,下面通过 Tensorboard 观察一下梯度参数的一个分布情况,看看是不是也和 L2 正则一样:

从上面这两个图,也可以看出 Dropout 有利于收缩权重的分布。类似于 L2 的一个功能,但是这里可看不出衰减, 不信?我们对比一下 Dropout 和 L2:

好了,Dropout 的内容就是这些了,下面我们还要学习一个在网络中很实用的一个技术叫做 BatchNormalization。

4. 标准化之 BN

4.1 BN 是什么以及为啥用?

BatchNormalization 就是批标准化, 批指的是 mini-batch, 标准化也就是 0 均值 1 方差,看似这个东西比较简单,但是威力却是很强, 有下面几个优点(来自 2015 年原文《BatchNormalization:Accelerating Deep Network Train by Reducing Internal Covariate Shift》, 这篇论文堪称这一年深度学习界最重要的一篇论文):

  • 可以用更大学习率,加速模型收敛
  • 可以不用精心设计权值初始化
  • 可以不用 Dropout 或者较小的 Dropout
  • 可以不用 L2 或者较小的 weight decay
  • 可以不用局部响应标准化(AlexNet 中用到过)

好了,既然优点辣么多,肯定是要好好学习这个方法, 那么下面就看看到底 BatchNormlization 是怎么做到这么强大的, 计算方式到底是啥?下面就是 BatchNormlization 的算法流程:

还记得这几个公式吗:

begin {aligned} &mathrm {H}_{11}=sum_{i=0}^{n} X_{i} * W_{1 i}\ &mathbf {D}left (mathrm {H}_{11}right)=sum_{i=0}^{n} Dleft (X_{i}right) * Dleft (W_{1 i}right)\ &begin {array}{l} =n *(1 * 1) \ =n end {array}\ &operatorname {std}left (mathrm {H}_{11}right)=sqrt {mathbf {D}left (mathrm {H}_{11}right)}=sqrt {n}\ &mathbf {D}left (mathrm {H}_{1}right)=mathbf {n} * mathbf {D}(mathbf {X}) * mathbf {D}(mathbf {W})=mathbf {1}\ &D (W)=frac {1}{n} Rightarrow operatorname {std}(W)=sqrt {frac {1}{n}} end {aligned}

我们发现每一层的方差竟然是前面所有层方差的连乘,那么假设有一层尺度上有点不正常,那么随着网络的加深,很容易引起梯度消失或者爆炸。所以权值初始化那里就考虑采用一种初始化的方式控制网络输出层的一个尺度。 所以 BN 的提出, 也是为了解决这个问题的,只不过解决了这个问题之后,竟然发现带来了一系列的优点,上面提到的那些。

下面就通过代码看一下,加了 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 初始化方法, 看下效果:

这里是我们精心设计了权值初始化的一个方法,考虑到 relu, 我们得用 Kaiming 初始化,考虑到 tanh,我们还得用 Xavier, 还是挺复杂的, 那么我们假设不用权值初始化,而是在网络层的激活函数前加上 BN 呢?

可以发现,强大的 BN 依然可以保证数据的尺度,并且好处就是我们不用再考虑用什么样的方式进行权值的初始化。下面再从人民币二分类的实验中就加权值初始化,加 BN 和啥都不加三者产生的一个效果:

这里也可以看出 BN 层的作用,可以约束我们特征输入层的一个尺度范围,让它保持一个良好的分布,让模型训练起来更加简单。

4.2 Pytorch 中的 BN

Pytorch 中提供了三种 BatchNorm 方法:

  • nn.BatchNorm1d
  • nn.BatchNorm2d
  • nn.BatchNorm3d

上面三个 BatchNorm 方法都继承 __BatchNorm 这个基类,初始化参数如下:

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 特征

下面直接动过代码看看上面这个一维的这个 BN 方法要怎么用:

batch_size = 3    # 3 个样本
num_features = 5    # 5 个特征
momentum = 0.3     # 这个计算后面均值,方差的时候用到

features_shape = (1)   # 我们特征的维度是 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   上面那 3 列数据
print ("input data:n {} shape is {}".format (feature_maps_bs, feature_maps_bs.shape))

bn = nn.BatchNorm1d (num_features=num_features, momentum=momentum)   # BN1d 在这里定义

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-bath 所得到的用于对数据进行 Normalize 的这个均值,不是当前 mini-batch 得到的均值,而是会考虑前面 mini-batch 的数据信息, 加权平均的一个均值和方差。下面通过调试看看 BN 里面的四个非常重要的参数:均值,方差, gamma 和 beta 它们的 shape:

2 nn.BatchNorm2d -> input = B * 特征数 * 2d 特征

卷积图像中经常是这种 2d 的。

3 nn.BatchNorm3d -> input = B * 特征数 * 3d 特征

这个在时空序列中会用到。

这就是 BatchNormalization 的内容了,下面再学习一些其他的 Normalization 的一些知识。

4.3 其他的 Normalization 方法

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。

好了,下面又得看看怎么用了,它是怎么进行计算的, 这个才是关键, LayerNorm 常用在 RNN 当中,在 CNN 和 FCN 中不如 BN 的效果好。

上面是代码部分, 下面我们看看结果:

所以从这里我们也能基本上看出 LN 和 BN 的一个区别了, LN 它是以层为单位的, 而 BN 是以 batch 为单位的,后面还有个神图, 一看就差不多知道这几种标准化方法到底是怎么算的了。

如果我们把参数 elementwise_affine 设置为 False, 会报 AttributeError: 'NoneType' object has no attribute'shape', 另外还得说一下 normalized_shape, 这个我们也可以自己指定, 但得注意一定要从最后维度开始, 这是啥意思?

最后这种情况的报错:

RuntimeError: Given normalized_shape=[6, 3], expected input with shape [*, 6, 3], but got input of size [8, 6, 3, 4]

「2 Instance Normalization」起因:BN 在图像生成中不适用, 思路就是逐个 Instance(channel) 计算均值和方差。比如在图像风格迁移中,每一个样本的风格是不一样的,所以我们不能像 BN 那样从多个样本里面去计算平均值和方差了。那么应该怎么算呢? 还是以上面的一个图为例:

下面看看 Pytorch 提供的 InstanceNorm 这个网络层怎么使用:

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 表示训练状态还是测试状态。这个和 BN 的参数是一样的,并且也有 1d/2d/3d, 当然区分方法和 BN 那边一样。下面依然是代码中看一下 Instance Normalization 是如何计算的:

下面看看结果:

「3 Group Normalization」起因:小 batch 样本中, BN 估计的值不准, 这种 Normalization 的思路就是数据不够, 通道来凑。 一样用于大模型(小 batch size)的任务。

这个有点 LayerNorm 的感觉,只不过相当于 LayerNorm 进行了分组, 这个和 LN 一样,不再有 running_mean 和 running_var, gamma 和 beta 为逐个通道的。看看 Pytorch 提供的 GroupNorm:

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)提出来的,它们的计算公式差不多,只不过计算均值和方差的时候采用的方式不同, 下面来一个神图:

好吧,这个图把四维的数据画成了维的,如果不太好理解的话, 那么就采用上面的几个图理解一下, 把上面四种方式的计算合起来:

5. 总结

这次的学习内容就到这里了,这次的整理依然挺多, 主要是总结了一下 Pytorch 中常用的正则化和标准化的一些方法,依然是快速梳理一遍。

首先是正则化那块,正则化主要是缓解模型的过拟合问题,我们从 L2 正则化开始,L2 正则化也叫权重衰减,我们学习了 L2 正则的原理,L1 正则和 L2 正则的区别,然后学习了 L2 正则在 Pytorch 中的使用。 然后又学习了 Dropout 正则化,依然是原理和使用,并且对比了一下 L2 正则和 Dropout 正则的效果。

标准化主要是解决网络层输出的数据尺度变化不一致的问题, 首先学习了 Batch Normalization,这个非常重要,有很多的优点, 学习了它的原理和具体的使用方法,然后又介绍了其他三种标准化方法, LayerNorm Normalization、Instance Normalization 和 Group Normalization, 分别看了一下是如何计算的并且在 Pytorch 中如何使用的。最后对比了一下这四种方法。

好了, 到这里为止,关于机器学习模型训练的五个步骤的细节部分就结束了, 我们从数据模块捋一捋, 数据模块,主要学习了 DataLoader 和 Dataset 的工作原理,构建数据生成器, 还顺带着学习了 transforms 处理图像。模型模块,学习了 Module, 模型容器, 还有各种卷积,池化,激活函数等层, 还学习了数据初始化的一些方法。 损失函数模块,介绍了各种损失函数的使用。 优化器模块,介绍了优化器和学习率调整策略。迭代训练模块学习了 Tensorboard 可视化方法,学习了正则化和标准化。通过上面的学习,基本上可以把机器学习模型训练的五个步骤的各个细节搭建一个框架出来了。

最后打算再安排一篇类似后记的东西,通过上面五个步骤,我们就可以通过 Pytorch 搭建一个模型并且进行有效的训练,而模型搭建完了之后我们要保存下来,以备后面的使用,并且在大型任务中我们不可能从头自己搭建模型,往往需要模型的迁移, 为了提高训练效率,我们往往需要使用 GPU, 最后再整理一些 Pytorch 中常见的报错作为结束。所以下一篇的内容,我们从模型的保存与加载, 模型的微调技术, GPU 使用和 Pytorch 常见报错四方面来整理。最后一篇了,继续 Rush 吧;)

所有代码链接:

链接:https://pan.baidu.com/s/1c5EYdd0w8j6w3g54KTxJJA 提取码:k7rh