有了上一节的基础理论铺垫之后,接下来,我们讨论Batch Normalization在PyTorch中的实现方法。尽管BN只是一个归一化方法,但其使用过程并不像一个“方法”这么简单。
在PyTorch中,我们使用nn.Linear构建线性层,类似的,我们通过使用nn.BatchNorm类来构建BN层,进而可以进行Batch Normalization归一化操作,并且在后续操作过程中我们会发现,BN层和线性层之间由诸多相似之处。同时,出于对不同类型数据的不同处理需求,nn.BatchNorm1d类主要用于处理2d数据,nn.BatchNorm2d则主要用来处理3d数据。简单来说,目前我们所使用的面板数据(二维表格数据)都属于2d数据,使用nn.BatchNorm1d处理即可。而后续在处理图像数据时,则需要视情况和使用nn.BatchNorm2d类。
# 查看帮助
nn.BatchNorm1d?
首先我们解释其中三个核心参数:
简单进行尝试
f = torch.arange(9).reshape(3, 3).float()
f
#tensor([[0., 1., 2.],
# [3., 4., 5.],
# [6., 7., 8.]])
# 实例化一个BN类
bn1 = nn.BatchNorm1d(3)
bn1(f)
#tensor([[-1.2247, -1.2247, -1.2247],
# [ 0.0000, 0.0000, 0.0000],
# [ 1.2247, 1.2247, 1.2247]], grad_fn=)
# Z-Score计算结果
(f - torch.mean(f, 0)) / torch.sqrt(torch.var(f, 0))
#tensor([[-1., -1., -1.],
# [ 0., 0., 0.],
# [ 1., 1., 1.]])
注,此处课程视频有误,更正内容如下:
pytorch中默认的方差计算是样本无偏估计,而BN再进行归一化计算时实际上采用的是样本方差,二者区别如下,样本方差为: V a r ( x ) = ∑ i = 1 n ( x i − x ˉ ) 2 n Var(x) = \frac{\sum^n_{i=1}(x_i-\bar x)^2}{n} Var(x)=n∑i=1n(xi−xˉ)2但对样本整体方差的无偏估计为: V a r u n b i a s e d ( x ) = ∑ i = 1 n ( x i − x ˉ ) 2 n − 1 Var_{unbiased}(x) = \frac{\sum^n_{i=1}(x_i-\bar x)^2}{n-1} Varunbiased(x)=n−1∑i=1n(xi−xˉ)2
torch.var?
t = torch.tensor([1., 2])
t
#tensor([1., 2.])
# 整体方差的无偏估计
torch.var(t)
#tensor(0.5000)
# 样本方差
torch.var(t, unbiased=False)
#tensor(0.2500)
torch.var(f, 0)
#tensor([9., 9., 9.])
torch.var(f, 0, unbiased=False)
#tensor([6., 6., 6.])
# 样本方差计算结果
(f - torch.mean(f, 0)) / torch.sqrt(torch.var(f, 0, unbiased=False))
#tensor([[-1.2247, -1.2247, -1.2247],
# [ 0.0000, 0.0000, 0.0000],
# [ 1.2247, 1.2247, 1.2247]])
注,torch.var中可通过设置unbiased参数来进行样本方差计算
当然,根据此前的介绍,BN层和线性层有许多类似的地方,这里我们可以查看BN层的参数情况。
list(bn1.parameters())
#[Parameter containing:
# tensor([1., 1., 1.], requires_grad=True),
# Parameter containing:
# tensor([0., 0., 0.], requires_grad=True)]
据此也可确认在迭代开始之前, γ 和 β \gamma和\beta γ和β的初始值。同时,根据参数的可微性,我们也能看出其最终也是需要通过反向传播计算梯度,然后利用梯度下降进行求解的。当然,此处如果我们将affine参数设置为False,则无法查看参数,并且归一化过程就是简单的在无偏估计下对数据进行Z-Score归一化。
bn2 = nn.BatchNorm1d(3, affine=False)
list(bn2.parameters()) # 此时无法查看bn2参数
#[]
bn2(f) # 处理后的数据也不是可微的,无法进行反向传播,因此也无法修改BN参数
#tensor([[-1.2247, -1.2247, -1.2247],
# [ 0.0000, 0.0000, 0.0000],
# [ 1.2247, 1.2247, 1.2247]])
对于BN层来说,计算过程并不复杂,但有一个核心需要解决的问题:在测试阶段、针对可能出现的一条条测试集输入的情况,应该如何对测试数据进行归一化处理。
针对此问题,BN层采用了机器学习一以贯之的基本思路,那就是所有用于处理测试数据的参数都在训练数据中得出,包括归一化所需要用到的均值和方差,并且对于测试集在归一化过程中所用到的均值和方差,都是训练数据整体均值和方差。但是,在模型训练阶段,数据是分一个个小批输入的,而针对小批训练数据的归一化处理也没有采用所有训练数据的均值和方差,而是采用训练数据总体均值和方差的无偏估计带入进行计算。
值得注意的是,针对测试集计算的均值和方差就是真实计算出的训练数据的均值和方差,而不是一个估计的结果。
为了能够获取训练数据整体均值和方差,BN采用了迭代累计计算的方法,通过类似动量法的方法对每一个小批数据的均值和方差进行累计统计,最终计算训练数据整体均值和方差,具体过程如下。
f = torch.arange(9).reshape(3, 3).float()
f
#tensor([[0., 1., 2.],
# [3., 4., 5.],
# [6., 7., 8.]])
bn1 = nn.BatchNorm1d(3)
bn1.running_mean
bn1.running_var
#tensor([0., 0., 0.])
#tensor([1., 1., 1.])
bn1(f)
#tensor([[-1.2247, -1.2247, -1.2247],
# [ 0.0000, 0.0000, 0.0000],
# [ 1.2247, 1.2247, 1.2247]], grad_fn=)
f.mean(0)
#tensor([3., 4., 5.])
bn1.running_mean
#tensor([0.3000, 0.4000, 0.5000])
BN层在每一次进行归一化时,都会通过“某种”方法累计输入数据集的均值和方差,在每一次处理完一批数据之后,我们都可以通过调用BN层的running_mean和running_var查看BN层所累计的均值和方差结果:
bn1.running_mean
bn1.running_var
#tensor([0.3000, 0.4000, 0.5000])
#tensor([1.8000, 1.8000, 1.8000])
不过值得注意的是,BN层并不是记录输入数据集的均值和方差,而是采用了一种类似动量法的方法在累计每一个小批数据的统计量。这种累计过程如下:首先,当我们每次实例化一个BN层之后,都会获得一组原始的running_mean和running_var
bn2 = nn.BatchNorm1d(3)
bn2.running_mean
bn2.running_var
#tensor([0., 0., 0.])
#tensor([1., 1., 1.])
初始条件下,running_mean取值为0,running_var取值为1。而当我们在使用BN层进行每一次归一化数据时,BN层的running_mean和running_var将按照如下公式进行调整:
nn.BatchNorm1d?
r u n n i n g _ m e a n = ( 1 − m o m e n t u m ) ∗ r u n n i n g _ m e a n + m o m e n t u m ∗ s a m p l e _ m e a n running\_mean = (1-momentum) * running\_mean + momentum * sample\_mean running_mean=(1−momentum)∗running_mean+momentum∗sample_mean r u n n i n g _ v a r = ( 1 − m o m e n t u m ) ∗ r u n n i n g _ v a r + m o m e n t u m ∗ s a m p l e _ v a r running\_var = (1-momentum) * running\_var + momentum * sample\_var running_var=(1−momentum)∗running_var+momentum∗sample_var 其中,是BN层实例化过程中的可选参数,默认值为0.1,sample表示当前输入的小批数据。上述过程可简单理解为BN层每每次归一化一批数据,都会针对围绕已经记录的running_mean/var和小批数据的sample_mean/var进行加权求和,在momentum取值为0.1的情况下,原running_mean/var权重是sample_mean/var的九倍,也就是说,当前数据归一化完成后,running_mean/var会朝向sample_mean/var小幅移动。
例如,bn2迭代一轮(训练一批数据)后,bn2的running_mean/var取值结果如下:
bn2(f)
#tensor([[-1.2247, -1.2247, -1.2247],
# [ 0.0000, 0.0000, 0.0000],
# [ 1.2247, 1.2247, 1.2247]], grad_fn=)
bn2.running_mean
bn2.running_var
#tensor([0.3000, 0.4000, 0.5000])
#tensor([1.8000, 1.8000, 1.8000])
bn3 = nn.BatchNorm1d(3)
torch.mean(f, 0)
#tensor([3., 4., 5.])
bn3.running_mean * 0.9 + 0.1 * torch.mean(f, 0)
#tensor([0.3000, 0.4000, 0.5000])
bn3.running_var * 0.9 + 0.1 * torch.var(f, 0)
#tensor([1.8000, 1.8000, 1.8000])
当然,在这种设置下,如果同一批数据,当迭代很多轮之后,running_mean/var就会最终取到sample_mean/var。
bn3 = nn.BatchNorm1d(3)
mean_l = []
var_l = []
for i in range(50):
temp = bn3(f)
mean_l.append(bn3.running_mean.clone().tolist())
var_l.append(bn3.running_var.clone().tolist())
这里用到Lesson 1中提到的深拷贝,大家可以简单思考这么做的原因。
查看同一批数据迭代50轮之后BN层记录的统计量和数据真实统计量之间区别
bn3.running_mean
torch.mean(f, 0)
#tensor([2.9845, 3.9794, 4.9742])
#tensor([3., 4., 5.])
bn3.running_var
torch.var(f, 0)
#tensor([8.9588, 8.9588, 8.9588])
#tensor([9., 9., 9.])
能够看出,已经非常接近了,当然,我们也可以通过绘制图像来可视化呈现BN层记录的统计量的变化情况。此处我们查看数据集第二列均值在BN层统计结果中的变化情况
mean_t = torch.tensor(mean_l)
plt.plot(range(50), mean_t[:, 1], torch.full_like(mean_t[:, 1], torch.mean(f, 0)[1]))
当然,如果是小批量输入数据,即每次输入原数据的一部分时,经过多轮epoch迭代,BN最终也将累计记录到整体均值和方差。
# 设置随机数种子
torch.manual_seed(420)
features, labels = tensorGenCla()
plt.scatter(features[:, 0], features[:, 1], c = labels)
features
#tensor([[-4.0141, -2.9911],
# [-2.6593, -4.7657],
# [-3.9395, -3.2347],
# ...,
# [ 4.4622, 2.1406],
# [ 3.7278, 7.2208],
# [ 2.9705, 1.1236]])
# 进行数据集切分与加载
train_loader, test_loader = split_loader(features, labels)
train_loader.dataset[:]
#(tensor([[-0.9717, -0.9087],
# [-6.2780, -5.3359],
# [ 2.3067, -2.8697],
# ...,
# [ 3.5654, 6.1413],
# [-3.7873, -1.9385],
# [-2.4863, 0.3824]]),
# tensor([[1],
# [0],
# [1],
# ...,
# [2],
# [0],
# [1]]))
# 训练集特征
features_train = train_loader.dataset[:][0]
features_train
#tensor([[-0.9717, -0.9087],
# [-6.2780, -5.3359],
# [ 2.3067, -2.8697],
# ...,
# [ 3.5654, 6.1413],
# [-3.7873, -1.9385],
# [-2.4863, 0.3824]])
len(features)
#1500
len(features_train)
#1050
1050/1500
#0.7
# 查看训练集特征的均值和方差
torch.mean(features_train, 0)
torch.var(features_train, 0)
#tensor([0.0890, 0.0311])
#tensor([14.7865, 14.2480])
# 实例化一个BN类
bn4 = nn.BatchNorm1d(2)
bn4.running_mean
bn4.running_var
#tensor([0., 0.])
#tensor([1., 1.])
for X, y in train_loader:
print(X)
break
#tensor([[-4.5046, -4.0710],
# [ 5.3773, 1.1910],
# [ 6.1152, 3.1348],
# [ 3.6007, 5.9519],
# [ 2.0717, 1.5093],
# [-8.3814, -2.0817],
# [-6.7716, -4.1947],
# [-4.0627, -5.7047],
# [-4.4566, -2.4398],
# [ 0.6234, -0.2532]])
# 执行小批数据归一化计算
for X, y in train_loader:
temp = bn4(X)
bn4.running_mean
bn4.running_var
#tensor([ 0.2417, -0.0743])
#tensor([14.2721, 13.3134])
能够发现,BN层记录的统计量已经朝向训练数据真实结果方向移动,此时多迭代几轮epoch进行尝试
# 实例化一个BN类
bn4 = nn.BatchNorm1d(2)
bn4.running_mean
bn4.running_var
#tensor([0., 0.])
#tensor([1., 1.])
# 执行小批数据归一化计算,遍历五次数据
torch.manual_seed(22)
for epochs in range(5):
for X, y in train_loader:
temp = bn4(X)
bn4.running_mean
bn4.running_var
#tensor([0.2914, 0.0028])
#tensor([13.7399, 15.4879])
# 查看训练集特征的均值和方差
torch.mean(features_train, 0)
torch.var(features_train, 0)
#tensor([0.0890, 0.0311])
#tensor([14.7865, 14.2480])
能够发现,BN记录结果朝向整体真实结果靠拢。
迭代记录的原因
至于为什么神经网络为什么采用迭代方法求解训练集特征的均值和方差,而不是直接直接按列进行统计。BN通过迭代求解的主要原因有两点,其一是面对超大规模数据时候求解均值或方差可能无法直接进行求解,尤其是如果数据是分布式存储的话一次性求解均值和方差就会更加困难;其二,迭代计算是深度学习求解参数的基本过程,所有深度学习框架为了更好地满足迭代计算,都进行了许多精巧的设计,从而使得迭代计算更加高效和平稳,而在计算训练集整体均值时,如果借助神经网络向前传播的过程进行迭代记录,便能够在控制边际成本的情况下快速达成目标。
momentum参数设置
关于momentum的设置,一般来说,为了尽可能获取到更加准确的训练集整体统计量,当每一个小批数据数据量比较小时,我们应该将历史数据比重调高,也就是降低momentum取值,以减少局部规律对获取总体规律的影响,当然,此时我们也需要增加遍历数据的次数epochs;而反之则可以考虑增大momentum取值。
track_running_stats参数
对于BN来说,最后一个参数就是track_running_stats,默认取值为True。当此参数为True时,BN层会在每次迭代过程中会结合历史记录更新running_mean/var,当track_running_stats=False时,BN层将running_mean/var为None,并且在进行预测时会根据输入的小批数据进行均值和方差计算。在大多数情况下,并不推荐修改该参数的默认取值。
注意,迭代过程需要设置随机数种子,否则每一轮迭代都将创建不同的小批数据,也将影响最终BN层记录结果:
# 两次调用train_loader,生成两类不同小批数据
for epochs in range(2):
for X, y in train_loader:
print(X)
break
#tensor([[-7.5423, -4.7466],
# [-0.8141, 1.4812],
# [-2.0414, -3.9627],
# [-2.3532, -4.5938],
# [-4.2370, -4.2045],
# [ 5.4768, 1.2268],
# [-1.5763, 0.6214],
# [-4.8817, 0.4160],
# [-2.4502, -4.3577],
# [-5.4342, -5.2988]])
#tensor([[-1.1590, 3.7442],
# [ 1.0554, 0.9682],
# [-6.0871, -2.3277],
# [ 5.1658, 1.3549],
# [-8.2258, -6.1138],
# [ 3.3588, 2.2425],
# [-2.1994, -3.8662],
# [ 4.5528, 3.6807],
# [ 0.8180, 1.6713],
# [ 6.0824, 4.5515]])
尽管BN找到了能够计算训练集整体均值和方差的方法,但截至目前,还是没有解决如何将在训练过程中记录的running_mean/var应用到测试集当中的方法。核心原因在于,BN层的running_mean/var是伴随向前传播同步调整的。也就是说,在track_running_stats开启时,只要进行一次向前传播,就会更新一次running_mean/var,这明显是不合适的(因为我们不能根据测试集数据修改参数)。但是我们也知道,只要要进行测试集的测试,就一定需要模型执行测试数据的向前传播以得出模型结果,此时应该怎么办呢?
此前我们没有遇到该难题的主要原因是,线性层的参数调整是通过optimizer.step()完成的,也就是说我们需要手动输入代码才能完成线性层参数调整,在模型训练过程,我们每一次迭代都会手动调整一次线性层参数,而在输入测试数据时我们只会执行向前传播,并不会在向前传播结束后进行optimizer.step(),因此输入测试数据不会影响模型参数调整。
此时就要用到PyTorch中适用于nn模块中所有模型的一种方法——model.train()与model.eval()。其中,model.train()表示开启模型训练模式,在默认情况下,我们实例化的每一个模型都是出于训练模式的,而model.eval()则表示将模型转化为测试模式。我们可以简单查看该方法的相关说明。
bn5 = nn.BatchNorm1d(3)
bn5.train()
#BatchNorm1d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
当然,开始训练模式时参数取值为False则代表开启测试模式,即bn5.train(False)和bn5.eval()等效。
那么,训练模式和测试模式的根本区别是什么?简单来说就是,在PyTorch中,其实有很多类似BN类一样,会在向前传播过程中直接自动修改模型参数,而当模型出于测试模式时,会避免这种情况发生,即模型出于测试模式时不会根据当前输入数据情况调整running_mean/var。并且,模型出于测试模式时,BN层会利用已经记录下的running_mean/var对数据进行归一化。
在PyTorch中,训练模式和测试模式的区分,相当于是给模型提供了可设置的两种行为模式。
f
#tensor([[0., 1., 2.],
# [3., 4., 5.],
# [6., 7., 8.]])
bn5 = nn.BatchNorm1d(3)
bn5.train() # 进入训练模式
bn5.running_mean
bn5.running_var
#BatchNorm1d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
#tensor([0., 0., 0.])
#tensor([1., 1., 1.])
bn5(f) # 对输入数据的统计量无偏估计
#tensor([[-1.2247, -1.2247, -1.2247],
# [ 0.0000, 0.0000, 0.0000],
# [ 1.2247, 1.2247, 1.2247]], grad_fn=)
bn5.running_mean # 并且更新记录的统计结果
bn5.running_var
#tensor([0.3000, 0.4000, 0.5000])
#tensor([1.8000, 1.8000, 1.8000])
bn5 = nn.BatchNorm1d(3)
bn5.eval() # 进入测试模式
bn5.running_mean
bn5.running_var
#BatchNorm1d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
#tensor([0., 0., 0.])
#tensor([1., 1., 1.])
bn5(f)
#tensor([[0.0000, 1.0000, 2.0000],
# [3.0000, 4.0000, 5.0000],
# [6.0000, 7.0000, 8.0000]], grad_fn=)
f
#tensor([[0., 1., 2.],
# [3., 4., 5.],
# [6., 7., 8.]])
注意该结果的解读,此时是以running_mean/var作为均值和方差对数据集进行归一化处理,并且,此时的 γ 和 β \gamma和\beta γ和β分别是1和0。
list(bn5.parameters())
#[Parameter containing:
# tensor([1., 1., 1.], requires_grad=True),
# Parameter containing:
# tensor([0., 0., 0.], requires_grad=True)]
因此最终处理结果还是原始数据本身。另外,在测试模式下,BN层也不会再记录running_mean/var。
bn5.running_mean
bn5.running_var
#tensor([0., 0., 0.])
#tensor([1., 1., 1.])
在训练模式下,BN处于另一种完全不同的行为模式。
当然,对于一个模型,我们也可通过.training属性来查看模型处于训练模式还是测试模式。
bn5.training
#False
另外我们需要知道,当模型(model)由多个模块(module)共同集成时,各模块的状态由模型整体状态决定。
有了对nn.BatchNorm类基本了解之后,我们来尝试构建带有BN层的神经网络模型。根据上一节的介绍,我们知道BN本质上是一种自适应的数据分布调整算法,同时我们也知道,数据分布的调整并不影响,因此我们可以在任何需要的位置都进行BN归一化。另外,根据Glorot条件,我们知道在模型构建过程中需要力求各梯度计算的有效性和平稳性,因此我们可以考虑在每一个线性层前面或者后面进行数据归一化处理。
在实际模型构建过程中,在模型中添加BN层和添加线性层类似,我们只需要在自定义模型类的init方法中添加BN层,并在向前传播方法中确定BN层的调用方法即可。而在具体BN层位置选择方面,是放在线性层前面还是放在线性层后面,目前业内没有定论,既没有理论论证BN层的最佳位置,也没有严谨的实验证明在哪个位置放置BN层效果最好。因此我们大可根据自己的习惯,以及自身经验的判断选择BN层的位置。当然,为了实验方便,我们会同时创建可以前置或者后置BN层的模型。(这里我们将放在隐藏层前面(也就是线性层后面)称为BN层前置,放在隐藏层后面称为BN层后置。)
接下来,我们创建一个同时能够调整激活函数、可选是否包含BN层、以及BN层放置位置的模型,当然,该模型仍然是分层创建,在包含了上述参数后相当于此前模型的一个集成。我们先从简单入手,创建一个两层神经网络、BN层可选、激活函数可选的模型。
class net_class1(nn.Module):
def __init__(self, act_fun=torch.relu, in_features=2, n_hidden=4, out_features=1, bias=True, BN_model=None, momentum=0.1):
super(net_class1, self).__init__()
self.linear1 = nn.Linear(in_features, n_hidden, bias=bias)
self.normalize1 = nn.BatchNorm1d(n_hidden, momentum=momentum)
self.linear2 = nn.Linear(n_hidden, out_features, bias=bias)
self.BN_model = BN_model
self.act_fun = act_fun
def forward(self, x):
if self.BN_model == None:
z1 = self.linear1(x)
p1 = self.act_fun(z1)
out = self.linear2(p1)
elif self.BN_model == 'pre':
z1 = self.normalize1(self.linear1(x))
p1 = self.act_fun(z1)
out = self.linear2(p1)
elif self.BN_model == 'post':
z1 = self.linear1(x)
p1 = self.act_fun(z1)
out = self.linear2(self.normalize1(p1))
return out
接下来,我们尝试调用此前的fit函数来进行模型训练。注意,此处开始我们将显式注明模型的训练和测试阶段。
# 设置随机数种子
torch.manual_seed(420)
# 创建最高项为2的多项式回归数据集
features, labels = tensorGenReg(w=[2, -1], bias=False, deg=2)
# 进行数据集切分与加载
train_loader, test_loader = split_loader(features, labels)
# 实例化一个前置BN层的神经网络
torch.manual_seed(24)
relu_model1_norm = net_class1(BN_model='pre')
# 设置模型为训练模式
relu_model1_norm.train()
#net_class1(
# (linear1): Linear(in_features=2, out_features=4, bias=True)
# (normalize1): BatchNorm1d(4, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
# (linear2): Linear(in_features=4, out_features=1, bias=True)
#)
这里我们只需要对模型整体进行训练模式设置,模型中所有模块都将调整为训练模式。
list(relu_model1_norm.modules())
#[net_class1(
# (linear1): Linear(in_features=2, out_features=4, bias=True)
# (normalize1): BatchNorm1d(4, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
# (linear2): Linear(in_features=4, out_features=1, bias=True)
#),
# Linear(in_features=2, out_features=4, bias=True),
# BatchNorm1d(4, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True),
# Linear(in_features=4, out_features=1, bias=True)]
for m in list(relu_model1_norm.modules()):
print(m.training == True)
#True
#True
#True
#True
relu_model1_norm.eval()
#net_class1(
# (linear1): Linear(in_features=2, out_features=4, bias=True)
# (normalize1): BatchNorm1d(4, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
# (linear2): Linear(in_features=4, out_features=1, bias=True)
#)
for m in list(relu_model1_norm.modules()):
print(m.training == True)
#False
#False
#False
#False
relu_model1_norm.train()
#net_class1(
# (linear1): Linear(in_features=2, out_features=4, bias=True)
# (normalize1): BatchNorm1d(4, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
# (linear2): Linear(in_features=4, out_features=1, bias=True)
#)
为了后续验证训练模式和测试模式是否生效,此处我们可以查看模型BN层的running_mean/var,BN层是模型的第二个模块。
list(relu_model1_norm.modules())
#[net_class1(
# (linear1): Linear(in_features=2, out_features=4, bias=True)
# (normalize1): BatchNorm1d(4, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
# (linear2): Linear(in_features=4, out_features=1, bias=True)
# ),
# Linear(in_features=2, out_features=4, bias=True),
# BatchNorm1d(4, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True),
# Linear(in_features=4, out_features=1, bias=True)]
list(relu_model1_norm.modules())[2]
#BatchNorm1d(4, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
list(relu_model1_norm.modules())[2].running_mean
list(relu_model1_norm.modules())[2].running_var
#tensor([0., 0., 0., 0.])
#tensor([1., 1., 1., 1.])
features
#tensor([[-0.0070, 0.5044],
# [ 0.6704, -0.3829],
# [ 0.0302, 0.3826],
# ...,
# [-0.9164, -0.6087],
# [ 0.7815, 1.2865],
# [ 1.4819, 1.1390]])
值得注意的是,此时BN层记录的均值和方差都有4个分量,而原始训练数据只有两个特征。本质上是因为此时BN层是围绕第一个线性层的输出结果进行归一化处理,而数据经过第一个线性层时将转化为N*4的形状。
当然,我们也可查看当前BN层的参数情况
list(relu_model1_norm.modules())[2].weight
#Parameter containing:
#tensor([1., 1., 1., 1.], requires_grad=True)
list(relu_model1_norm.modules())[2].bias
#Parameter containing:
#tensor([0., 0., 0., 0.], requires_grad=True)
注意,当我们调用BN层的模块来查看参数时,weight就是 γ \gamma γ,bias就是 β \beta β。
接下来即可开始进行模型训练
# 进行模型训练,迭代三轮
fit(net=relu_model1_norm,
criterion=nn.MSELoss(),
optimizer=optim.SGD(relu_model1_norm.parameters(), lr = lr),
batchdata=train_loader,
epochs=20,
cla=False)
重点查看BN层变化情况
list(relu_model1_norm.modules())[2].weight
list(relu_model1_norm.modules())[2].bias
#Parameter containing:
#tensor([2.1052, 2.7661, 3.2005, 2.0476], requires_grad=True)
#Parameter containing:
#tensor([-1.4605, -1.5263, -2.0342, -1.5288], requires_grad=True)
我们发现BN层确实得到了训练。当然,我们也可以查看BN层记录的统计指标
list(relu_model1_norm.modules())[2].running_mean
list(relu_model1_norm.modules())[2].running_var
#tensor([-0.1037, -0.2748, -0.1374, 0.0303])
#tensor([4.2056, 5.7263, 6.9815, 4.9587])
从BN层的记录结果来看,隐藏层输入的数据基本是零均值的,从侧面也证明了BN调整效果的有效性。
接下来我们将模型调整为测试模式,测试模型在训练集和测试集上的表现。
relu_model1_norm.eval()
#net_class1(
# (linear1): Linear(in_features=2, out_features=4, bias=True)
# (normalize1): BatchNorm1d(4, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
# (linear2): Linear(in_features=4, out_features=1, bias=True)
#)
mse_cal(train_loader, relu_model1_norm)
#tensor(0.5811, grad_fn=)
mse_cal(test_loader, relu_model1_norm)
#tensor(1.0448, grad_fn=)
观察running_mean和running_var的取值变化情况,判断测试模式是否生效。
list(relu_model1_norm.modules())[2].weight
list(relu_model1_norm.modules())[2].bias
list(relu_model1_norm.modules())[2].running_mean
list(relu_model1_norm.modules())[2].running_var
#Parameter containing:
#tensor([2.1052, 2.7661, 3.2005, 2.0476], requires_grad=True)
#Parameter containing:
#tensor([-1.4605, -1.5263, -2.0342, -1.5288], requires_grad=True)
#tensor([-0.1037, -0.2748, -0.1374, 0.0303])
#tensor([4.2056, 5.7263, 6.9815, 4.9587])
至此,我们就完成了带BN的模型从训练到测试的全流程。当然,为了方便后续直接使用带BN层或者其他需要灵活调整训练模式和测试模式的其他方法,我们将模型模式调整也集成到此前定义的model_train_test和model_comparison函数中。
def model_train_test(model,
train_data,
test_data,
num_epochs = 20,
criterion = nn.MSELoss(),
optimizer = optim.SGD,
lr = 0.03,
init = False,
cla = False,
eva = mse_cal):
"""模型误差测试函数:
:param model_l:模型
:param train_data:训练数据
:param test_data: 测试数据
:param num_epochs:迭代轮数
:param criterion: 损失函数
:param lr: 学习率
:param cla: 是否是分类模型
:return:MSE列表
"""
# 模型评估指标矩阵
train_l = []
test_l = []
# 模型训练过程
for epochs in range(num_epochs):
model.train()
fit(net = model,
criterion = criterion,
optimizer = optimizer(model.parameters(), lr = lr),
batchdata = train_data,
epochs = epochs,
cla = cla)
model.eval()
train_l.append(eva(train_data, model).detach())
test_l.append(eva(test_data, model).detach())
return train_l, test_l
def model_comparison(model_l,
name_l,
train_data,
test_data,
num_epochs = 20,
criterion = nn.MSELoss(),
optimizer = optim.SGD,
lr = 0.03,
cla = False,
eva = mse_cal):
"""模型对比函数:
:param model_l:模型序列
:param name_l:模型名称序列
:param train_data:训练数据
:param test_data:测试数据
:param num_epochs:迭代轮数
:param criterion: 损失函数
:param lr: 学习率
:param cla: 是否是分类模型
:param eva: 模型评估指标
:return:评估指标张量矩阵
"""
# 模型评估指标矩阵
train_l = torch.zeros(len(model_l), num_epochs)
test_l = torch.zeros(len(model_l), num_epochs)
# 模型训练过程
for epochs in range(num_epochs):
for i, model in enumerate(model_l):
model.train()
fit(net = model,
criterion = criterion,
optimizer = optimizer(model.parameters(), lr = lr),
batchdata = train_data,
epochs = epochs,
cla = cla)
model.eval()
train_l[i][epochs] = eva(train_data, model).detach()
test_l[i][epochs] = eva(test_data, model).detach()
return train_l, test_l
上述修改的函数和类都需要写入torchLearning.py中。
接下来,我们简单尝试在自己生成的数据集上使用带BN层的模型。
# 设置随机数种子
torch.manual_seed(420)
# 创建最高项为2的多项式回归数据集
features, labels = tensorGenReg(w=[2, -1], bias=False, deg=2)
# 进行数据集切分与加载
train_loader, test_loader = split_loader(features, labels)
我们先尝试使用Sigmoid激活函数
# 设置随机数种子
torch.manual_seed(24)
# 实例化模型
sigmoid_model1 = net_class1(act_fun= torch.sigmoid)
sigmoid_model1_norm = net_class1(act_fun= torch.sigmoid, BN_model='pre')
# 创建模型容器
model_l = [sigmoid_model1, sigmoid_model1_norm]
name_l = ['sigmoid_model1', 'sigmoid_model1_norm']
# 核心参数
lr = 0.03
num_epochs = 40
# 模型训练
train_l, test_l = model_comparison(model_l = model_l,
name_l = name_l,
train_data = train_loader,
test_data = test_loader,
num_epochs = num_epochs,
criterion = nn.MSELoss(),
optimizer = optim.SGD,
lr = lr,
cla = False,
eva = mse_cal)
# 训练误差
for i, name in enumerate(name_l):
plt.plot(list(range(num_epochs)), train_l[i], label=name)
plt.legend(loc = 1)
plt.title('mse_train')
# 测试误差
for i, name in enumerate(name_l):
plt.plot(list(range(num_epochs)), test_l[i], label=name)
plt.legend(loc = 1)
plt.title('mse_test')
至此,我们就完成了一个完整的从构建带BN层的模型到建模输出结果并进行评估的全过程,但是我们发现,加入BN层之后,模型效果反而不如原始模型。不难发现,BN优化算法使用起来其实并不简单,并不是只要加入BN层模型效果就一定会有所提升。要充分发挥BN层的效果,需要结合其他模型参数进行综合调参。由于针对带BN层的模型调参将涉及到非常多其他优化方法的联合调参,因此我们将放在下一节统一进行介绍。
当然,为了为下一节做准备,我们仿造net_class1继续编写拥有两个隐藏层、三个隐藏层和四个隐藏层的对应模型类。
class net_class2(nn.Module):
def __init__(self, act_fun= torch.relu, in_features=2, n_hidden1=4, n_hidden2=4, out_features=1, bias=True, BN_model=None, momentum=0.1):
super(net_class2, self).__init__()
self.linear1 = nn.Linear(in_features, n_hidden1, bias=bias)
self.normalize1 = nn.BatchNorm1d(n_hidden1, momentum=momentum)
self.linear2 = nn.Linear(n_hidden1, n_hidden2, bias=bias)
self.normalize2 = nn.BatchNorm1d(n_hidden2, momentum=momentum)
self.linear3 = nn.Linear(n_hidden2, out_features, bias=bias)
self.BN_model = BN_model
self.act_fun = act_fun
def forward(self, x):
if self.BN_model == None:
z1 = self.linear1(x)
p1 = self.act_fun(z1)
z2 = self.linear2(p1)
p2 = self.act_fun(z2)
out = self.linear3(p2)
elif self.BN_model == 'pre':
z1 = self.normalize1(self.linear1(x))
p1 = self.act_fun(z1)
z2 = self.normalize2(self.linear2(p1))
p2 = self.act_fun(z2)
out = self.linear3(p2)
elif self.BN_model == 'post':
z1 = self.linear1(x)
p1 = self.act_fun(z1)
z2 = self.linear2(self.normalize1(p1))
p2 = self.act_fun(z2)
out = self.linear3(self.normalize2(p2))
return out
class net_class3(nn.Module):
def __init__(self, act_fun= torch.relu, in_features=2, n_hidden1=4, n_hidden2=4, n_hidden3=4, out_features=1, bias=True, BN_model=None, momentum=0.1):
super(net_class3, self).__init__()
self.linear1 = nn.Linear(in_features, n_hidden1, bias=bias)
self.normalize1 = nn.BatchNorm1d(n_hidden1, momentum=momentum)
self.linear2 = nn.Linear(n_hidden1, n_hidden2, bias=bias)
self.normalize2 = nn.BatchNorm1d(n_hidden2, momentum=momentum)
self.linear3 = nn.Linear(n_hidden2, n_hidden3, bias=bias)
self.normalize3 = nn.BatchNorm1d(n_hidden3, momentum=momentum)
self.linear4 = nn.Linear(n_hidden3, out_features, bias=bias)
self.BN_model = BN_model
self.act_fun = act_fun
def forward(self, x):
if self.BN_model == None:
z1 = self.linear1(x)
p1 = self.act_fun(z1)
z2 = self.linear2(p1)
p2 = self.act_fun(z2)
z3 = self.linear3(p2)
p3 = self.act_fun(z3)
out = self.linear4(p3)
elif self.BN_model == 'pre':
z1 = self.normalize1(self.linear1(x))
p1 = self.act_fun(z1)
z2 = self.normalize2(self.linear2(p1))
p2 = self.act_fun(z2)
z3 = self.normalize3(self.linear3(p2))
p3 = self.act_fun(z3)
out = self.linear4(p3)
elif self.BN_model == 'post':
z1 = self.linear1(x)
p1 = self.act_fun(z1)
z2 = self.linear2(self.normalize1(p1))
p2 = self.act_fun(z2)
z3 = self.linear3(self.normalize2(p2))
p3 = self.act_fun(z3)
out = self.linear4(self.normalize3(p3))
return out
class net_class4(nn.Module):
def __init__(self, act_fun= torch.relu, in_features=2, n_hidden1=4, n_hidden2=4, n_hidden3=4, n_hidden4=4, out_features=1, bias=True, BN_model=None, momentum=0.1):
super(net_class4, self).__init__()
self.linear1 = nn.Linear(in_features, n_hidden1, bias=bias)
self.normalize1 = nn.BatchNorm1d(n_hidden1, momentum=momentum)
self.linear2 = nn.Linear(n_hidden1, n_hidden2, bias=bias)
self.normalize2 = nn.BatchNorm1d(n_hidden2, momentum=momentum)
self.linear3 = nn.Linear(n_hidden2, n_hidden3, bias=bias)
self.normalize3 = nn.BatchNorm1d(n_hidden3, momentum=momentum)
self.linear4 = nn.Linear(n_hidden3, n_hidden4, bias=bias)
self.normalize4 = nn.BatchNorm1d(n_hidden4, momentum=momentum)
self.linear5 = nn.Linear(n_hidden4, out_features, bias=bias)
self.BN_model = BN_model
self.act_fun = act_fun
def forward(self, x):
if self.BN_model == None:
z1 = self.linear1(x)
p1 = self.act_fun(z1)
z2 = self.linear2(p1)
p2 = self.act_fun(z2)
z3 = self.linear3(p2)
p3 = self.act_fun(z3)
z4 = self.linear4(p3)
p4 = self.act_fun(z4)
out = self.linear5(p4)
elif self.BN_model == 'pre':
z1 = self.normalize1(self.linear1(x))
p1 = self.act_fun(z1)
z2 = self.normalize2(self.linear2(p1))
p2 = self.act_fun(z2)
z3 = self.normalize3(self.linear3(p2))
p3 = self.act_fun(z3)
z4 = self.normalize4(self.linear4(p3))
p4 = self.act_fun(z4)
out = self.linear5(p4)
elif self.BN_model == 'post':
z1 = self.linear1(x)
p1 = self.act_fun(z1)
z2 = self.linear2(self.normalize1(p1))
p2 = self.act_fun(z2)
z3 = self.linear3(self.normalize2(p2))
p3 = self.act_fun(z3)
z4 = self.linear4(self.normalize3(p3))
p4 = self.act_fun(z4)
out = self.linear5(self.normalize4(p4))
return out
课程结束后,需要将上述代码写入torchLeaning.py文件中。