系统学习Pytorch笔记七:正则化与标准化总结、模型保存与加载

正则化与标准化总结、模型保存与加载

背景

学习知识先有框架(至少先知道有啥东西)然后再通过实战(各个东西具体咋用)来填充这个框架。 而这个系列的目的就是在脑海中先建一个Pytorch的基本框架出来。

通过上一次的学习,完成了优化器和学习率调整策略的学习,,接下来主要是进行迭代训练模块的学习。

正则化是迭代训练中一个重要的知识点,学习Pytorch中的各种正则化方法。 首先, 会学习什么是正则化以及偏差方差之间的关联, 然后学习L2正则化方法, 然后介绍Dropout的相关知识, 然后是Batch Normalization和正则化层。

通过学习前面的内容,可以使用Pytorch搭建一个模型并且进行有效的训练,而模型搭建完了之后我们要保存下来,以备后面的使用,并且在大型任务中我们不可能从头自己搭建模型,往往需要模型的迁移, 为了提高训练效率,我们往往需要使用GPU, 最后再整理一些Pytorch中常见的报错作为结束。

正则化与标准化总结

方差、偏差

正则化就是一个减少方差的策略。

误差可分解为:偏差(Biase)、方差(Variance)和噪声(Noise)之和。
泛化误差=错误率(error) = b i a s 2 ( x ) + v a r ( x ) + α = bias^2(x)+var(x)+\alpha =bias2(x)+var(x)+α ,这个可以数学推导证明。

  • 偏差度量了学习算法的期望预测与真实结果的偏离程度, 即刻画了学习算法本身的拟合能力

  • 方差度量了同样大小的训练集的变动所导致的学习性能的变化,即刻画了数据扰动所造成的影响

  • 噪声则表达了在当前任务上任何学习算法所能达到的期望泛化误差的下界,即刻画了学习问题本身的难度。

为什么会有偏差和方差?

在机器学习中,我们用训练数据集去训练一个模型,通常的做法是定义一个误差函数,通过将这个误差的最小化过程,来提高模型的性能。然而我们学习一个模型的目的是为了解决训练数据集这个领域中的一般化问题,单纯地将训练数据集的损失最小化,并不能保证在解决更一般的问题时模型仍然是最优,甚至不能保证模型是可用的。这个训练数据集的损失与一般化的数据集的损失之间的差异就叫做泛化误差(generalization error)。

泛化误差可以分解为偏差(Biase)、方差(Variance)和噪声(Noise)

如果我们能够获得所有可能的数据集合,并在这个数据集合上将损失最小化,那么学习得到的模型就可以称之为“真实模型”。当然,在现实生活中我们不可能获取并训练所有可能的数据,所以“真实模型”肯定存在,但是无法获得,即我们获得的数据是真实数据的一个抽样。我们的最终目的是学习一个模型使其更加接近这个真实模型。

BiasVariance分别从两个方面来描述我们学习到的模型真实模型之间的差距。

偏差Bias是用所有可能的训练数据集训练出的所有模型的输出的平均值与真实模型的输出值之间的差异。

方差Variance是不同的训练数据集训练出的模型输出值之间的差异。

噪声的存在是学习算法所无法解决的问题,数据的质量决定了学习的上限。假设在数据已经给定的情况下,此时上限已定,我们要做的就是尽可能的接近这个上限

“偏差-方差分解”说明,泛化性能是由学习算法的能力、数据的充分性以及学习任务本身的难度所共同决定的。给定学习任务,为了取得好的泛化性能,则需使偏差较小,即能够充分拟合数据,并且使方差较小,即使得数据扰动产生的影响小,即好的模型有低方差、低偏差。

一般来说,偏差与方差是有冲突的,这称为偏差-方差窘境(bias-variance dilemma)。

简单的模型会有一个较大的偏差和较小的方差,复杂的模型偏差较小方差较大。所以正确选择模型的复杂度,可以减少误差对模型的影响。复杂度高的模型通常对训练数据有很好的拟合能力,但是对测试数据就不一定了。而复杂度太低的模型又不能很好的拟合训练数据,更不能很好的拟合测试数据。因此,模型复杂度和模型偏差和方差具有如下图所示关系。
在这里插入图片描述

正则化weight_decay(L2正则)

正则化,其实就是在降低方差,这样就有利于解决过拟合的问题, 那么是怎么做到的呢? 我们先来学习L1正则和L2正则。

正则化的思想修改目标函数,在原来目标函数上再加上正则项,即
O b j = C o s t + R e g u l a r i z a t i o n = 1 N ∑ i N f ( y i ∧ , y i ) + R Obj=Cost+Regularization=\frac{1}{N}∑_{i}^N f(y_{i}^∧,y_{i})+R Obj=Cost+Regularization=N1iNf(yi,yi)+R

正则项部分就是有L1正则项和L2正则项两种

  • L1 Regularization: ∑ i N ∣ w i ∣ \sum_{i}^{N}\left|{w}_{i}\right| iNwi
  • L2 Regularization: ∑ i N w i 2 \sum_{i}^{N} w_{i}^{2} iNwi2

加上这种正则项,就是希望我们的代价函数小,同时也希望我们这里的 w i w_{i} wi 小,这样就说明每个样本的权重都很小,这样模型就不会太多的关注某种类型的样本, 模型参数也不会太复杂,有利于缓解过拟合现象。

如何控制一个模型的容量,第一种是将模型变得比较小,即减少模型的参数。第二种是限制模型参数的范围变得比较小。权重衰退就是控制参数值的选择范围来控制模型容量

L1正则和L2正则区别

在这里插入图片描述
看上面的图,左边这个是涉及L1正则化的, 先看看这个, 上面彩色的是Cost的等高线图,也就是在同一条线上Cost是相等的,比如A,B,C三点, 产生的Cost是相等的,比如Cost等于0.03. 黑色的矩形表示L1正则项的一个等高线, ∣ w 1 ∣ + ∣ w 2 ∣ = r ∣w1∣+∣w2∣=r w1+w2∣=r, 假设这里的常数r为1。那么矩形框上任意一个点产生的正则项都是1。 我们现在考虑A, B,C三点的目标函数,他们的Cost是相等的,那么这三个点哪个点的Regularization最小呢? C点的正则项是1, 而我们会发现A点和B点的正则项都比C大(其实A,B,C这一条Cost等高线上就是C的正则项最小), 所以C点的目标函数最小。所以我们如果在L1的情况下找个最优解,既能损失最小,又能权重最小,那么往往这个最优解就发生在坐标轴上,也就是上面的C点。 这样一个神奇的现象发生了,w1竟然等于0, 就说明参数解有0项了, w1就消失了。所以L1正则项一般会产生稀疏的解,也就是有0项的解。这是因为加上L1之后,我们参数的解往往会发生在坐标轴上导致某些参数的值为0.

我们看右边,这是L2正则。彩色的圈还是Cost等高线,下面黑色的圆圈是L2正则等高线( ∣ w 1 2 ∣ + ∣ w 2 2 ∣ = 1 ∣ w_{1}^ 2 ∣ + ∣ w_{2} ^2 ∣ = 1 w12+w22∣=1), 和上面的分析一样,如果我们在A’, B’, C’点确定最优解的话,依然是C’点, 因为它在Cost相等的情况下正则最小。但是我们发现L2正则下不过出现某个参数为0的情况,而是w1和w2都比较小。所以L2正则项的最优的参数值很小概率出现在坐标轴上,因此每一维的参数都不会是0。当最小化||w||时,就会使每一项趋近于0.

总结一下L1和L2正则化的特点

L1正则化的特点:

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

L2正则化的特点:

  • 容易计算, 可导, 适合基于梯度的方法
  • 将一些权值缩小到接近0
  • 相关的预测特征对应的系数值相似
  • 当特征数量巨大时, 计算量会比较大
  • 对于有相关特征存在的情况, 它会包含所有这些相关的特征, 但是相关特征的权值分布取决于相关性。
  • 对异常值非常敏感
  • 相对于L1正则会更加准确
Pytorch中的L2正则项

Pytorch中, L2正则项又叫做weight decay(权值衰减)。那么为啥这个东西叫做权值衰减呢?

因为在原有目标函数上增加了一个L2正则项,即 O b j = L o s s + λ 2 ∗ ∑ i N w i 2 O b j=L o s s+\frac{\lambda}{2} * \sum_{i}^{N} w_{i}^{2} Obj=Loss+2λiNwi2,所以参数更新为:

在这里插入图片描述
我们知道 λ \lambda λ的取值是0-1的,那么就是说每一次迭代之后,这个参数 w i w_{i} wi 本身也会发生一个衰减。也就是说我们加上L2正则项与没有加L2正则项进行一个对比的话,加入L2正则项,这里的 w i w_i wi 就会发生数值上的一个衰减。故这就是这个L2正则项称为权值衰减的原因

通过代码来学习L2正则项

#============== 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这个参数即可。我们可以看一下正则化模型和不带正则化模型的效果
在这里插入图片描述
可以清楚的发现, 不带正则化的红色线发生了过拟合现象.

一般是在模型过拟合的时候用到这个方式, 当然除了L2正则化,在模型发生过拟合的时候还有其他的方式,比如Dropout,也是常用的一种方式。

正则化之Dropout

Dropout概念(暂退法)

Dropout叫做随机失活。就是我们给出一个概率(随机),让某个神经元的权重为0(失活)。下面给出个图直观感受一下:
在这里插入图片描述
就是每一层,让某些神经元不起作用,这样就就相当于把网络进行简化了(左边和右边可以对比),我们有时候之所以会出现过拟合现象,就是因为我们的网络太复杂了,参数太多了,并且我们后面层的网络也可能太过于依赖前层的某个神经元,加入Dropout之后, 首先网络会变得简单,减少一些参数,并且由于不知道浅层的哪些神经元会失活,导致后面的网络不敢放太多的权重在前层的某个神经元,这样就减轻了一个过度依赖的现象, 对特征少了依赖, 从而有利于缓解过拟合。

在这里插入图片描述

关于Dropout还有一个注意的问题,就是数据的尺度变化。 这个是什么意思呢? 我们用Dropout的时候是这样用的: 只在训练的时候开启Dropout,而测试的时候是不用Dropout的,也就是说模型训练的时候会随机失活一部分神经元, 而测试的时候我们用所有的神经元,那么这时候就会出现这个数据尺度的问题, 所以测试的时候,所有权重都乘以1-drop_prob, 以保证训练和测试时尺度变化一致, drop_prob是我们的随机失活概率。

nn.Dropout

Pytorch中给我们提供了Dropout层
在这里插入图片描述
用上面正则化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正则差不多.

上面就是如何在Pytorch中如何使用Dropout了,但是在实现上,还有一个小细节得说一下:Pytorch在实现Dropout的时候, 是权重乘以 1 − 1 1 − p 1 − \frac{1}{1-p} 11p1 的,也就是除以1-p, 这样就不用再测试的时候权重乘以1-p了, 也没有改变原来数据的尺度.

标准化之BatchNormalization

BatchNormalization是什么

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

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

下面就是BatchNormlization的算法流程

在这里插入图片描述
最后这句称为affine transform, 可以增强模型的容纳能力,使得模型自己更加灵活,让模型自己去判断是否需要去改变数据的一个分布(这里的 γ \gamma γ β \beta β是可学习参数, 类似于神经网络的权值w和b ), 如果模型发现改变分布不是太好,那么让 γ = σ B 2 + ϵ \gamma = \sqrt{\sigma_{\mathcal{B}}^{2}+\epsilon} γ=σB2+ϵ β = μ B \beta = \mu_{\mathcal{B}} β=μB , 这样我们的 y i y_i yi 依然是等于 x i x_i xi,没有改变分布。这就是affine transform的功能,提供了一个可逆的操作, 到底需不需要改变数据分布,把这个权利交给模型自己学习。

BatchNormlization(BN)既然这么简单,为啥能起那么大的作用呢?

看上面BN论文的标题,会发现这个算法提出本来是想解决Internal Covariate Shift这个问题的,就是网络输出层的数值尺度的变化导致网络无法训练。在神经网络模型训练的时候,发现每一层的方差竟然是前面所有层方差的连乘,那么假设有一层尺度上有点不正常,那么随着网络的加深,很容易引起梯度消失或者爆炸。所以权值初始化那里就考虑采用一种初始化的方式控制网络输出层的一个尺度。 所以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层的作用,可以约束我们特征输入层的一个尺度范围,让它保持一个良好的分布,让模型训练起来更加简单。

Pytorch中的BatchNormalization

Pytorch中提供了多种BatchNorm方法
在这里插入图片描述
BatchNorm1d
在这里插入图片描述
这里的num_features表示一个样本的特征数量,这是最重要的一个参数。eps表示分母修正项, momentum表示指数加权平均估计当前mean/var。 affine表示是否需要affine transform, track_running_stats表示是训练状态还是测试状态,这个也是非常关键的,因为我们发现momentum那里有个均值和方差,如果是训练状态,那么就需要重新估计mean和方差,而如果是测试状态,就用训练时候统计的均值和方差。

BatchNorm的三个方法也是有属性的

  • running_mean: 均值
  • running_var: 方差
  • weight: affine transform中的 γ \gamma γ
  • bias: affine transforom中的 β \beta β

这四个属性分别对应我们公式中用到的四个属性,
在这里插入图片描述

这里的均值和方差是采用指数加权平均进行计算的, 不仅要考虑当前mini-batch的均值和方差,还考虑上一个mini-batch的均值和方差(当然是在训练的时候,测试的时候是用当前的统计值。)

小例子

了解了三个方法的基本属性,下面得看看这三个方法到底用的时候有啥区别?

  • 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

在这里插入图片描述

  • nn.BatchNorm2d -> input = B * 特征数 * 2d特征
    在这里插入图片描述
  • nn.BatchNorm3d -> input = B * 特征数 * 3d特征
    在这里插入图片描述
其他的Normalization方法

常见的Normalization方法其实有四种,分别是Batch Normalization(BN)、Layer Normalization(LN)、Instance Normalization(IN)、Group Normalization(GN)。这四种方式既然都是Normalization,那么有什么相同点和异同点呢?

相同点就是公式上:

在这里插入图片描述
而不同点,就是求取 μ B \mu_B μB σ B \sigma_B σB的方式不同。BatchNormalization我们上面已经学习了,这个是在一个batch上去计算均值和方差,Layer Normalization是以层为单位去计算均值和方差, Instance Normalization主要在图像生成方法中使用的一个方法,Group Normalization是按组为单位计算均值和方差。

  • Layer Normalization
    起因: BN不适用于变长的网络,如RNN, 所以提出了逐层计算均值和方差的思路

在这里插入图片描述
BN与LN的区别:

  1. LN中同层神经元输入拥有相同的均值和方差,不同的输入样本有不同的均值和方差;

  2. BN中则针对不同神经元输入计算均值和方差,同一个batch中的输入拥有相同的均值和方差。

  3. LN它是以层为单位的, 而BN是以batch为单位的

  • Instance Normalization

起因: BN在图像生成中不适用, 思路就是逐个Instance(channel)计算均值和方差。比如在图像风格迁移中,每一个样本的风格是不一样的,所以我们不能像BN那样从多个样本里面去计算平均值和方差了。
在这里插入图片描述

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

在这里插入图片描述

关于这四种方法的详细介绍,可以参考这篇文章:https://blog.csdn.net/liuxiao214/article/details/81037416 或者参考官方文档:https://pytorch.org/docs/stable/nn.html#normalization-layers

模型保存与加载总结

模型的保存与加载

序列化与反序列化

序列化就是说内存中的某一个对象保存到硬盘当中,以二进制序列的形式存储下来,这就是一个序列化的过程。 而反序列化,就是将硬盘中存储的二进制的数,反序列化到内存当中,得到一个相应的对象,这样就可以再次使用这个模型了。

序列化和反序列化的目的就是将我们的模型长久的保存。

Pytorch中序列化和反序列化的方法:

  • torch.save(obj, f): obj表示对象, 也就是我们保存的数据,可以是模型,张量, dict等等, f表示输出的路径
  • torch.load(f, map_location): f表示文件的路径, map_location指定存放位置, CPU或者GPU, 这个参数挺重要,在使用GPU训练的时候再具体说。
模型保存与加载的两种方式

Pytorch的模型保存有两种方法, 一种是保存整个Module, 另外一种是保存模型的参数

  • 保存和加载整个Module: torch.save(net, path), torch.load(fpath)
  • 保存模型参数: torch.save(net.state_dict(), path), net.load_state_dict(torch.load(path))

第一种方法是保存整个的模型架构, 比较费时占内存, 第二种方法是只保留模型上的可学习参数, 等建立一个新的网络结构,然后放上这些参数即可,所以推荐使用第二种。

下面通过代码看看具体怎么使用:

class LeNet2(nn.Module):
    def __init__(self, classes):
        super(LeNet2, self).__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 6, 5),
            nn.ReLU(),
            nn.MaxPool2d(2, 2),
            nn.Conv2d(6, 16, 5),
            nn.ReLU(),
            nn.MaxPool2d(2, 2)
        )
        self.classifier = nn.Sequential(
            nn.Linear(16*5*5, 120),
            nn.ReLU(),
            nn.Linear(120, 84),
            nn.ReLU(),
            nn.Linear(84, classes)
        )

    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size()[0], -1)
        x = self.classifier(x)
        return x

    def initialize(self):
        for p in self.parameters():
            p.data.fill_(20191104)
     
## 建立一个网络
net = LeNet2(classes=2019)

# "训练"
print("训练前: ", net.features[0].weight[0, ...])
net.initialize()
print("训练后: ", net.features[0].weight[0, ...])

保存整个模型

下面就是保存整个模型和保存模型参数的方法

在这里插入图片描述
我们已经把模型保存到硬盘里面了,那么如果要用的时候,应该怎么导入呢? 如果我们保存的是整个模型的话,只需要执行如下代码:

path_model = "./model.pkl"
net_load = torch.load(path_model)

并且我们可以直接打印出整个模型的结构
在这里插入图片描述

保留模型参数

在这里插入图片描述

模型断点续训练

断点续训练技术就是当我们的模型训练的时间非常长,而训练到了中途出现了一些意外情况,比如断电了,当再次来电的时候,我们肯定是希望模型在中途的那个地方继续往下训练,这就需要我们在模型的训练过程中保存一些断点,这样发生意外之后,我们的模型可以从断点处继续训练而不是从头开始。 所以模型训练过程中设置checkpoint也是非常重要的。

checkpoint里面需要保留哪些参数呢?
模型训练的五个步骤: 数据 -> 模型 -> 损失函数 -> 优化器 -> 迭代训练中数据,损失函数不会改变,模型学习的参数, 优化器里的一些缓存是会变的。所以我们的checkpoint里面需要保存模型的数据优化器的数据,还有迭代到了第几次

通过人民币二分类的实验,模拟一个训练过程中的意外中断和恢复,看看怎么使用这个断点续训练:

在这里插入图片描述
上面的第6个epoch开始中断,看看怎么恢复断点训练

在这里插入图片描述
所以在模型的训练过程当中, 以一定的间隔去保存我们的模型,保存断点,在断点里面不仅要保存模型的参数,还要保存优化器的参数。这样才可以在意外中断之后恢复训练。

模型的finetune (微调技术)

在说模型的finetune之前,先学习一个概念,迁移学习。

迁移学习

迁移学习(Transfer Learning)通俗来讲就是学会举一反三的能力,通过运用已有的知识来学习新的知识,其核心是找到已有知识和新知识之间的相似性,通过这种相似性的迁移达到迁移学习的目的。世间万事万物皆有共性,如何合理地找寻它们之间的相似性,进而利用这个桥梁来帮助学习新知识,是迁移学习的核心问题。

迁移学习: 机器学习分支, 研究源域的知识如何应用到目标域,将源任务中学习到的知识运用到目标任务当中,用来提升目标任务里模型的性能
在这里插入图片描述

想训练一个模型但是没有足够的数据库,这时候要使用别人已经训练好的模型,这个模型其实相当于一个已经学习到的数据库,把这个模型加入到自己的网络中,从某种角度来看就扩大了自己的数据库,而且节省了大量的计算力。

什么是模型微调

给定预训练模型(Pre_trained model),基于模型进行微调(Fine Tune)。相对于从头开始训练(Training a model from scatch),微调为你省去大量计算资源和计算时间,提高了计算效率,甚至提高准确率。

模型微调的步骤:

  • 获取预训练模型参数(源任务当中学习到的知识)
  • 加载模型(load_state_dict)将学习到的知识放到新的模型
  • 修改输出层, 以适应新的任务

模型微调的训练方法:

  • 固定预训练的参数(requires_grad=False; lr=0)
  • Features Extractor较小学习率(params_group)
为什么要微调

通常是因为数据集的数量太少,计算资源太少,或训练的神经网络正确率太低,所以会使用微调进行提升模型效果。

使用的数据集和若与预训练模型的数据集相似,通常预训练模型使用了大型数据集做训练且由大神们调好网络结构,已经具备了提取浅层基础特征和深层抽象特征的能力,通常的做法是截断预先训练好的网络的最后一层(softmax层),并用与我们自己的问题相关的新的softmax层替换它。例如,ImageNet上预先训练好的网络带有1000个类别的softmax图层。如果我们的任务是对10个类别的分类,则网络的新softmax层将由10个类别组成,而不是1000个类别。然后,我们在网络上运行预先训练的权重。确保执行交叉验证,以便网络能够很好地推广。

微调指导注意事项

使用较小的学习率来训练网络。由于预先训练的权重相对于随机初始化的权重已经相当不错,我们不想过快地扭曲它们太多。通常的做法是使初始学习率比用于从头开始训练(Training from scratch)的初始学习率小10倍。

如果数据集数量过少,我们进来只训练最后一层,如果数据集数量中等冻结预训练网络的前几层的权重也是一种常见做法。这是因为前几个图层捕捉了与我们的新问题相关的通用特征,如曲线和边。我们希望保持这些权重不变。相反,我们会让网络专注于学习后续深层中特定于数据集的特征。

迁移学习和微调的区别

迁移学习 feature + classifier;通常做法就是在大的数据集(比如ImageNet)上训练出一个CNN,然后提取最后一层卷积层或者倒数第二层全连接层的输出作为CNN 特征,然后直接使用 SVM、贝叶斯或softmax等分类器进行分类,即将模型的输出作为特征输入分类器

微调 Fine-tuning:将在大数据集上训练得到的weights作为特定任务(小数据集)的初始化权重,重新训练该网络(根据需要,修改全连接层输出);至于训练的方式可以是:1.微调所有层 2.固定网络前面几层权重,只微调网络的后面几层,这样做有两个原因:A. 避免因数据量小造成过拟合现象;B.CNN前几层的特征中包含更多的一般特征(比如,边缘信息,色彩信息等),这对许多任务来说是非常有用的,但是CNN后面几层的特征学习注重高层特征,也就是语义特征,这是针对于数据集而言的,不同的数据集后面几层学习的语义特征也是完全不同的;Finetune是把别人的模型的已经训练好的参数,作为我们的初始化参数,这样,收敛速度快,而且需要的计算力也小。

微调是迁移学习的一种具体方法,是将迁移学习应用到神经网络中的一种形式。迁移学习和微调的联系在于,微调是一种使用预先训练好的模型来学习新任务的迁移学习方法。微调可以利用预训练模型在目标任务上进行快速学习,从而提高模型的准确性和泛化能力。

但是,微调和迁移学习之间还存在一些区别。迁移学习的定义更广泛,可以包括多种迁移学习方法,如多任务学习、迁移特征学习等。微调是迁移学习的一种具体形式,它通常是指在目标任务上微调预先训练好的模型来进行学习。另外,迁移学习的目标是将从源任务中学到的知识或经验应用到目标任务中,以提高模型的性能。微调的目标是调整预训练模型以适应目标任务的需求,通常需要使用目标数据进行微调。

迁移学习更像是一种想法,模型微调是一种技术。微调预先训练好的模型是一种迁移学习方法,但并不等同于迁移学习。微调是迁移学习的一种具体方法,它利用预先训练好的模型作为源模型,在目标任务上进行微调以适应目标任务的特定需求。微调通常包括在预训练模型的基础上添加新的层,然后使用目标数据集来训练这些层。微调过程中,预训练模型的权重被冻结,只有新添加的层会被训练调整。

模型的finetune例子

下面使用训练好的ResNet-18进行二分类: 让模型分出蚂蚁和蜜蜂

训练集120张, 验证集70张,所以我们可以看到这里的数据太少了,如果我们新建立模型进行训练预测,估计没法训练。所以看看迁移技术, 我们用训练好的ResNet-18来完成这个任务。
首先我们看看ResNet-18的结构,看看我们需要在哪里进行改动:
在这里插入图片描述

具体应该怎么使用(固定前面卷积层,微调全连接层):

在这里插入图片描述

注:上面代码有个小错误,就是冻结层那里所有的层,其实应该是冻结卷积层(特征提取层), 全连接层的参数不能冻结掉, 如果都冻结了,那还咋训练,所以在遍历网络参数的时候,应该有个判断(if 是卷积层参数)或者是遍历resnet18_ft.features.parameters()。

当然,训练时的trick还有第二个,就是不冻结前面的层,而是修改前面的参数学习率,因为我们的优化器里面有参数组的概念,我们可以把网络的前面和后面分成不同的参数组,使用不同的学习率进行训练,当前面的学习率为0的时候,就是和冻结前面的层一样的效果了,但是这种写法比较灵活。

在这里插入图片描述

GPU使用

CPU VS GPU

CPU(Central Processing Unit, 中央处理器): 主要包括控制器和运算器
GPU(Graphics Processing Unit, 图形处理器): 处理统一的, 无依赖的大规模数据运算。

在处理大规模与高速数据时,CPU很难满足需要。GPU就是专为满足此要求设计的处理器,计算速度快、拥有强大的浮点运算能力,特别适合矩阵运算。

想了解更多CPU、GPU相关知识,可参考文档 什么是GPU?跟CPU有什么区别?

数据迁移至GPU

这个数据主要有两种: Tensor和Module

  • CPU -> GPU: data.to(“cpu”)
  • GPU -> CPU: data.to(“cuda”)

to函数: 转换数据类型/设备

1、tensor.to(*args, **kwargs)

x = torch.ones((3,3))
x = x.to(torch.float64)    # 转换数据类型

x = torch.ones((3,3))
x = x.to("cuda")    # 设备转移

2、module.to(*args, **kwargs)

linear = nn.Linear(2,2)
linear.to(torch.double)  # 这样模型里面的可学习参数的数据类型变成float64

gpu1 = torch.device("cuda")
linear.to(gpu1)    # 把模型从CPU迁移到GPU

上面两个方法的区别: 张量不执行inplace, 所以上面看到需要等号重新赋值,而模型执行inplace, 所以不用等号重新赋值。

代码中学习上面的两个方法:
在这里插入图片描述
下面看一下Module的to函数
在这里插入图片描述

如果模型在GPU上, 那么数据也必须在GPU上才能正常运行。也就是说数据和模型必须在相同的设备上。

torch.cuda常用的方法:

  • torch.cuda.device_count(): 计算当前可见可用的GPU数
  • torch.cuda.get_device_name(): 获取GPU名称
  • torch.cuda.manual_seed(): 为当前GPU设置随机种子
  • torch.cuda.manual_seed_all(): 为所有可见可用GPU设置随机种子
  • torch.cuda.set_device(): 设置主GPU(默认GPU)为哪一个物理GPU(不推荐)
多GPU并行运算

多GPU并且运算, 简单的说就是我又很多块GPU,比如4块, 而这里面有个主GPU, 当拿到样本数据之后,比如主GPU拿到了16个样本, 那么它会经过16/4=4的运算,把数据分成4份, 自己留一份,然后把那3份分发到另外3块GPU上进行运算, 等其他的GPU运算完了之后, 主GPU再把结果收回来负责整合。 这时候看到主GPU的作用了吧。多GPU并行运算可以大大节省时间。所以, 多GPU并行运算的三步:分发 -> 并行计算 -> 收回结果整合。

torch.nn.DataParallel: 包装模型,实现分发并行机制。

在这里插入图片描述
主要参数:

  • module: 需要包装分发的模型
  • device_ids: 可分发的gpu, 默认分发到所有的可见可用GPU, 通常这个参数不管它,而是在环境变量中管这个。
  • output_device: 结果输出设备, 通常是输出到主GPU

下面从代码中看看多GPU并行怎么使用
在这里插入图片描述
由于这里没有多GPU,所以可以看看在多GPU服务器上的一个运行结果
在这里插入图片描述

 def get_gpu_memory():
    import platform
    if 'Windows' != platform.system():
        import os
        os.system('nvidia-smi -q -d Memory | grep -A4 GPU | grep Free > tmp.txt')
        memory_gpu = [int(x.split()[2]) for x in open('tmp.txt', 'r').readlines()]
        os.system('rm tmp.txt')
    else:
        memory_gpu = False
        print("显存计算功能暂不支持windows操作系统")
    return memory_gpu


gpu_memory = get_gpu_memory()
if not gpu_memory:
    print("\ngpu free memory: {}".format(gpu_memory))
    gpu_list = np.argsort(gpu_memory)[::-1]

    gpu_list_str = ','.join(map(str, gpu_list))
    os.environ.setdefault("CUDA_VISIBLE_DEVICES", gpu_list_str)
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

在GPU模型加载当中常见的两个问题

1、模型是以cuda的形式进行保存的,也就是在GPU上训练完保存的,保存完了之后我们想在一个没有GPU的机器上使用这个模型,就会报下面的错误
在这里插入图片描述
解决办法就是:
torch.load(path_state_dict, map_location="cpu"), 这样既可以在CPU设备上加载GPU上保存的模型了

2、用多GPU并行运算的机制训练好了某个模型并保存,然后想再建立一个普通的模型使用保存好的这些参数,就会报这个错误

在这里插入图片描述因为我们在多GPU并行运算的时候,我们的模型net先进行一个并行的一个包装,这个包装使得每一层的参数名称前面会加了一个module。 这时候,如果我们想把这些参数移到我们普通的net里面去,发现找不到这种module.开头的这些参数,即匹配不上,因为我们普通的net里面的参数是没有前面的module的。这时候我们就需要重新创建一个字典,把名字改了之后再导入。
我们首先先在多GPU的环境下,建立一个网络,并且进行包装,放到多GPU环境上训练保存:

在这里插入图片描述
加载的时候是怎么报错

在这里插入图片描述
解决方法

from collections import OrderedDict
    new_state_dict = OrderedDict()
    for k, v in state_dict_load.items():
        namekey = k[7:] if k.startswith('module.') else k
        new_state_dict[namekey] = v
    print("new_state_dict:\n{}".format(new_state_dict))

    net.load_state_dict(new_state_dict)

查看效果
在这里插入图片描述

Pytorch的常见报错

Pytorch常见错误与坑的一份文档:https://shimo.im/docs/PvgHytYygPVGJ8Hv,这里面目前有一些常见的报错信息,可以查看。

总结

今天的内容主要是分为2大块, 第一块就是正则化、标准化,总结了一下Pytorch中常用的正则化和标准化的一些方法。首先是正则化那块,正则化主要是缓解模型的过拟合问题,我们从L2正则化开始,L2正则化也叫权重衰减,学习了L2正则的原理,L1正则和L2正则的区别,然后学习了L2正则在Pytorch中的使用。 然后又学习了Dropout正则化,依然是原理和使用,并且对比了一下L2正则和Dropout正则的效果。标准化主要是解决网络层输出的数据尺度变化不一致的问题, 首先学习了Batch Normalization,这个非常重要,有很多的优点, 学习了它的原理和具体的使用方法,然后又介绍了其他三种标准化方法, LayerNorm Normalization、Instance Normalization和Group Normalization, 分别看了一下是如何计算的并且在Pytorch中如何使用的。

然后学习了模型保存和加载的相关知识, 首先学习了模型的保存与加载问题,介绍了两种模型保存与加载的方法, 然后学习了模型的微调技术,这个在迁移学习中用处非常大,还介绍了迁移学习中常用的两个trick。 然后学习了如何使用GPU加速训练和GPU并行训练方式, 最后整理了Pytorch中常见的几种报错信息。

关于系统学习Pytorch,逻辑上就是按照机器学习的那五大步骤进行的, 步骤为:数据模块 -> 模型模块 -> 损失函数 -> 优化器 -> 迭代训练。到这里为止,关于机器学习模型训练的五个步骤的细节部分就结束了,希望通过这几个模块的学习,可以建立一个关于Pytorch的框架,真正的做到Pytorch的入门。

  • 数据模块,主要学习了DataLoader和Dataset的工作原理,构建数据生成器, 还顺带着学习了transforms处理图像。
  • 模型模块,学习了Module, 模型容器, 还有各种卷积,池化,激活函数等层, 还学习了数据初始化的一些方法。
  • 损失函数模块,介绍了各种损失函数的使用。
  • 优化器模块,介绍了优化器和学习率调整策略。
  • 迭代训练模块学习了正则化和标准化,模型的保存与加载, 模型的微调技术, GPU使用和Pytorch常见报错。

参考:
[1]: https://zhongqiang.blog.csdn.net/article/details/105612578
[2]: https://pytorch.org/docs/stable/torch.html
[3]: https://zh.d2l.ai/chapter_multilayer-perceptrons/weight-decay.html
[4]: https://blog.csdn.net/qq_42250789/article/details/108832004
[5]: https://www.zhihu.com/tardis/zm/art/438117211?source_id=1005
[6]: https://zhuanlan.zhihu.com/p/365343501
[7]: https://www.zhihu.com/question/595469317
[8]: https://zhongqiang.blog.csdn.net/article/details/105612625

  • 13
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值