文章目录
- 一、实验介绍
- 二、实验环境
- 1. 配置虚拟环境
- 2. 库版本介绍
- 三、优化算法
- 0. 导入必要的库
- 1. 随机梯度下降SGD算法
- a. PyTorch中的SGD优化器
- b. 使用SGD优化器的前馈神经网络
- 2.随机梯度下降的改进方法
- a. 学习率调整
- b. 梯度估计修正
- 3. 梯度估计修正:动量法Momentum
- 4. 自适应学习率
- 5. Adam算法
- 四、参数初始化
- 五、数据预处理
- 六、逐层归一化
- 1. 批量归一化
- a. 理论基础
- b. 代码实现
- c. 测试
- 2. 层归一化
- a. 理论基础
- b. 代码实现
- c. 测试
- 3. 权重归一化
- 4. 局部响应归一化
- 5. 代码整合
一、实验介绍
深度神经网络在机器学习中应用时面临两类主要问题:优化问题和泛化问题。
-
优化问题:深度神经网络的优化具有挑战性。
- 神经网络的损失函数通常是非凸函数,因此找到全局最优解往往困难。
- 深度神经网络的参数通常非常多,而训练数据也很大,因此使用计算代价较高的二阶优化方法不太可行,而一阶优化方法的训练效率通常较低。
- 深度神经网络存在梯度消失或梯度爆炸问题,导致基于梯度的优化方法经常失效。
-
泛化问题:由于深度神经网络的复杂度较高且具有强大的拟合能力,很容易在训练集上产生过拟合现象。因此,在训练深度神经网络时需要采用一定的正则化方法来提高网络的泛化能力。
目前,研究人员通过大量实践总结了一些经验方法,以在神经网络的表示能力、复杂度、学习效率和泛化能力之间取得良好的平衡,从而得到良好的网络模型。本系列文章将从网络优化和网络正则化两个方面来介绍如下方法:
- 在网络优化方面,常用的方法包括优化算法的选择、参数初始化方法、数据预处理方法、逐层归一化方法和超参数优化方法。
- 在网络正则化方面,一些提高网络泛化能力的方法包括ℓ1和ℓ2正则化、权重衰减、提前停止、丢弃法、数据增强和标签平滑等。
本文将介绍神经网络优化的逐层归一化方法,包括批量归一化、层归一化、权重归一化(略)、局部响应归一化(略)等
二、实验环境
本系列实验使用了PyTorch深度学习框架,相关操作如下:
1. 配置虚拟环境
conda create -n DL python=3.7
conda activate DL
pip install torch==1.8.1+cu102 torchvision==0.9.1+cu102 torchaudio==0.8.1 -f https://download.pytorch.org/whl/torch_stable.html
conda install matplotlib
conda install scikit-learn
2. 库版本介绍
软件包 | 本实验版本 | 目前最新版 |
---|---|---|
matplotlib | 3.5.3 | 3.8.0 |
numpy | 1.21.6 | 1.26.0 |
python | 3.7.16 | |
scikit-learn | 0.22.1 | 1.3.0 |
torch | 1.8.1+cu102 | 2.0.1 |
torchaudio | 0.8.1 | 2.0.2 |
torchvision | 0.9.1+cu102 | 0.15.2 |
三、优化算法
神经网络的参数学习是一个非凸优化问题.当使用梯度下降法来进行优化网络参数时,参数初始值的选取十分关键,关系到网络的优化效率和泛化能力.参数初始化的方式通常有以下三种:
0. 导入必要的库
from torch import nn
1. 随机梯度下降SGD算法
随机梯度下降(Stochastic Gradient Descent,SGD)是一种常用的优化算法,用于训练深度神经网络。在每次迭代中,SGD通过随机均匀采样一个数据样本的索引,并计算该样本的梯度来更新网络参数。具体而言,SGD的更新步骤如下:
- 从训练数据中随机选择一个样本的索引。
- 使用选择的样本计算损失函数对于网络参数的梯度。
- 根据计算得到的梯度更新网络参数。
- 重复以上步骤,直到达到停止条件(如达到固定的迭代次数或损失函数收敛)。
a. PyTorch中的SGD优化器
Pytorch官方教程
optimizer = torch.optim.SGD(model.parameters(), lr=0.2)
b. 使用SGD优化器的前馈神经网络
【深度学习实验】前馈神经网络(final):自定义鸢尾花分类前馈神经网络模型并进行训练及评价
2.随机梯度下降的改进方法
传统的SGD在某些情况下可能存在一些问题,例如学习率选择困难和梯度的不稳定性。为了改进这些问题,提出了一些随机梯度下降的改进方法,其中包括学习率的调整和梯度的优化。
a. 学习率调整
- 学习率衰减(Learning Rate Decay):随着训练的进行,逐渐降低学习率。常见的学习率衰减方法有固定衰减、按照指数衰减、按照时间表衰减等。
- Adagrad:自适应地调整学习率。Adagrad根据参数在训练过程中的历史梯度进行调整,对于稀疏梯度较大的参数,降低学习率;对于稀疏梯度较小的参数,增加学习率。这样可以在不同参数上采用不同的学习率,提高收敛速度。
- Adadelta:与Adagrad类似,但进一步解决了Adagrad学习率递减过快的问题。Adadelta不仅考虑了历史梯度,还引入了一个累积的平方梯度的衰减平均,以动态调整学习率。
- RMSprop:也是一种自适应学习率的方法,通过使用梯度的指数加权移动平均来调整学习率。RMSprop结合了Adagrad的思想,但使用了衰减平均来减缓学习率的累积效果,从而更加稳定。
b. 梯度估计修正
- Momentum:使用梯度的“加权移动平均”作为参数的更新方向。Momentum方法引入了一个动量项,用于加速梯度下降的过程。通过积累之前的梯度信息,可以在更新参数时保持一定的惯性,有助于跳出局部最优解、加快收敛速度。
- Nesterov accelerated gradient:Nesterov加速梯度(NAG)是Momentum的一种变体。与Momentum不同的是,NAG会先根据当前的梯度估计出一个未来位置,然后在该位置计算梯度。这样可以更准确地估计当前位置的梯度,并且在参数更新时更加稳定。
- 梯度截断(Gradient Clipping):为了应对梯度爆炸或梯度消失的问题,梯度截断的方法被提出。梯度截断通过限制梯度的范围,将梯度控制在一个合理的范围内。常见的梯度截断方法有阈值截断和梯度缩放。
3. 梯度估计修正:动量法Momentum
【深度学习实验】网络优化与正则化(一):优化算法:使用动量优化的随机梯度下降算法(Stochastic Gradient Descent with Momentum)
4. 自适应学习率
【深度学习实验】网络优化与正则化(二):基于自适应学习率的优化算法详解:Adagrad、Adadelta、RMSprop
5. Adam算法
Adam算法(Adaptive Moment Estimation Algorithm)[Kingma et al., 2015]可以看作动量法和 RMSprop 算法的结合,不但使用动量作为参数更新方向,而且可以自适应调整学习率。
【深度学习实验】网络优化与正则化(三):随机梯度下降的改进——Adam算法详解(Adam≈梯度方向优化Momentum+自适应学习率RMSprop)~入选综合热榜
四、参数初始化
【深度学习实验】网络优化与正则化(四):参数初始化及其Pytorch实现——基于固定方差的初始化(高斯、均匀分布),基于方差缩放的初始化(Xavier、He),正交初始化
五、数据预处理
【深度学习实验】网络优化与正则化(五):数据预处理详解——标准化、归一化、白化、去除异常值、处理缺失值~入选综合热榜
六、逐层归一化
逐层归一化(Layer-wise Normalization)是将传统机器学习中的数据归一化方法应用到深度神经网络中,对神经网络中隐藏层的输入进行归一化,从而使得网络更容易训练,进而获得更好的性能和训练效果。它具有:
- 更好的尺度不变性
- 逐层归一化可以使输入数据的尺度保持一致,从而提高模型的鲁棒性和泛化能力。通过将每一层的输入数据归一化到相似的尺度,可以减轻不同层之间数据分布差异带来的问题。
- 内部协变量偏移:在深度神经网络中,每层的输入分布会随着网络参数的更新而发生变化,这被称为内部协变量偏移。逐层归一化可以通过标准化每一层的输入,使得每层的激活函数的输入分布更加稳定,有助于网络的训练和收敛。
- 更平滑的优化地形
- 逐层归一化可以使得损失函数的优化地形更加平滑,从而加速模型的训练过程。通过减少梯度的变化范围,逐层归一化可以提高梯度下降算法的效率和稳定性。
1. 批量归一化
a. 理论基础
- 批量归一化(Batch Normalization,BN):
- 对神经网络中任意的中间层的单个神经元通过一个Batch数据进行标准化。
- BN通常应用于卷积神经网络(CNN)和全连接神经网络(FCN)中。
- 优点:
- 提高优化效率:通过标准化每一层的输入数据,批量归一化可以缓解梯度消失和梯度爆炸问题,有助于加速优化算法的收敛过程。它可以使每一层的激活函数的输入保持在一个合适的范围内,提高了网络的稳定性和训练效率。、
- 隐式的正则化方法:批量归一化在每个小批量样本上计算均值和方差,并将其用于标准化数据。这种标准化过程可以看作是一种正则化方法,有助于减少模型的过拟合风险。它在某种程度上充当了正则化的效果,使得模型在一定程度上具有更好的泛化能力。
- 缺点:
- 小批量样本的数量不能太小:批量归一化的效果受到小批量样本数量的影响。如果小批量样本数量太小,计算的均值和方差可能不准确,导致归一化效果不佳。通常建议使用较大的批量大小以获得更好的结果。
- 无法应用到循环神经网络(RNN):批量归一化的计算是基于每个小批量样本的统计信息,而在循环神经网络中,由于神经元状态随时间变化,无法同时处理所有时间步的样本。因此,常规的批量归一化方法无法直接应用于循环神经网络。针对RNN,可以使用层归一化(Layer Normalization)来实现类似的效果。
b. 代码实现
def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):# 通过is_grad_enabled来判断当前模式是训练模式还是预测模式if not torch.is_grad_enabled():# 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)else:assert len(X.shape) in (2, 4)if len(X.shape) == 2:# 使用全连接层的情况,计算特征维上的均值和方差mean = X.mean(dim=0)var = ((X - mean) ** 2).mean(dim=0)else:# 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。# 这里我们需要保持X的形状以便后面可以做广播运算mean = X.mean(dim=(0, 2, 3), keepdim=True)var = ((X - mean) ** 2).mean(dim=(0, 2, 3), keepdim=True)# 训练模式下,用当前的均值和方差做标准化X_hat = (X - mean) / torch.sqrt(var + eps)# 更新移动平均的均值和方差moving_mean = momentum * moving_mean + (1.0 - momentum) * meanmoving_var = momentum * moving_var + (1.0 - momentum) * varY = gamma * X_hat + beta # 缩放和移位return Y, moving_mean.data, moving_var.dataclass BatchNorm(nn.Module):# num_features:完全连接层的输出数量或卷积层的输出通道数。# num_dims:2表示全连接层,4表示卷积层def __init__(self, num_features, num_dims):super().__init__()if num_dims == 2:shape = (1, num_features)else:shape = (1, num_features, 1, 1)# 参与求梯度和迭代的拉伸和偏移参数,分别初始化成1和0self.gamma = nn.Parameter(torch.ones(shape))self.beta = nn.Parameter(torch.zeros(shape))# 非模型参数的变量初始化为0和1self.moving_mean = torch.zeros(shape)self.moving_var = torch.ones(shape)def forward(self, X):# 如果X不在内存上,将moving_mean和moving_var 复制到X所在显存上if self.moving_mean.device != X.device:self.moving_mean = self.moving_mean.to(X.device)self.moving_var = self.moving_var.to(X.device)# 保存更新过的moving_mean和moving_varY, self.moving_mean, self.moving_var = batch_norm(X, self.gamma, self.beta, self.moving_mean,self.moving_var, eps=1e-5, momentum=0.9)return Y
c. 测试
batch_size = 20
train_data = CIFAR10Dataset('cifar10_tiny', 'trainLabels.csv')
train_iter = DataLoader(train_data, batch_size=batch_size)num_classes = 10
# 定义模型
net = nn.Sequential(nn.Conv2d(3, 6, kernel_size=5), BatchNorm(6, num_dims=4), nn.Sigmoid(),nn.AvgPool2d(kernel_size=2, stride=2),nn.Conv2d(6, 16, kernel_size=5), BatchNorm(16, num_dims=4), nn.Sigmoid(),nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),nn.Linear(400, 120), BatchNorm(120, num_dims=2), nn.Sigmoid(),nn.Linear(120, 84), BatchNorm(84, num_dims=2), nn.Sigmoid(),nn.Linear(84, 10))
# 定义损失函数
loss_fn = F.cross_entropy
# 定义优化器
optimizer = torch.optim.SGD(net.parameters(), lr=0.1)runner = Runner(net, optimizer, loss_fn, metric=None)
runner.train(train_iter, num_epochs=10, save_path='BatchNorm')# 第一个批量规范化层中学到的拉伸参数gamma和偏移参数beta
print(net[1].gamma.reshape((-1,)), net[1].beta.reshape((-1,)))
2. 层归一化
a. 理论基础
- 层归一化(Layer Normalization):
- 对一个中间层的所有神经元进行归一化。
- 与批量归一化不同,层归一化是在每一层的特征维度上进行归一化,而不是在批次维度上。这使得层归一化更适用于递归神经网络(RNN)等具有变长输入的模型。
b. 代码实现
class LayerNorm(nn.Module):def __init__(self, eps=1e-7, gamma=1.0, beta=0.0):super().__init__()self.gamma = torch.tensor(gamma)self.beta = torch.tensor(beta)self.eps = epsdef forward(self, x):# x为规范化层的输入,请注意x的维度mean = x.mean(dim=0)var = x.var(dim=0)output = (x - mean) / torch.sqrt(var + self.eps)output = output * self.gamma + self.betareturn output
c. 测试
batch_size = 20
train_data = CIFAR10Dataset('cifar10_tiny', 'trainLabels.csv')
train_iter = DataLoader(train_data, batch_size=batch_size)num_classes = 10
# 定义模型
net = nn.Sequential(nn.Conv2d(3, 6, kernel_size=5),LayerNorm(gamma=1.0, beta=0.0), # 使用自定义的LayerNorm类,并设置gamma和beta的初始值nn.Sigmoid(),nn.AvgPool2d(kernel_size=2, stride=2),nn.Conv2d(6, 16, kernel_size=5), LayerNorm(gamma=1.0, beta=0.0), nn.Sigmoid(),nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),nn.Linear(400, 120),LayerNorm(), nn.Sigmoid(),nn.Linear(120, 84),LayerNorm(),nn.Sigmoid(),nn.Linear(84, 10)
)# 定义损失函数
loss_fn = F.cross_entropy
# 定义优化器
optimizer = torch.optim.SGD(net.parameters(), lr=0.1)runner = Runner(net, optimizer, loss_fn, metric=None)
runner.train(train_iter, num_epochs=10, save_path='LayerNorm')
3. 权重归一化
- 权重归一化(Weight Normalization)
- 权重归一化是通过对模型权重进行归一化,而不是对输入数据进行归一化。它可以在训练过程中动态地调整权重的尺度,以改善模型的训练效果。
4. 局部响应归一化
- 局部响应归一化(Local Response Normalization,LRN)
- LRN是一种在卷积神经网络中常用的归一化方法,它通过对每个神经元的输出进行归一化,以增强模型对局部输入模式的响应能力。
- 局部响应归一化和层归一化都是对同层的神经元进行归一化.不同的是,局部响应归一化应用在激活函数之后,只是对邻近的神经元进行局部归一化,并且不减去均值。
5. 代码整合
(以批量归一化为例)
import os
import torch
from torch import nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
from d2l import torch as d2l
from sklearn.datasets import load_iris
from torchvision.io import read_image
from torch.utils.data import Dataset, DataLoaderclass Runner(object):def __init__(self, model, optimizer, loss_fn, metric=None):self.model = modelself.optimizer = optimizerself.loss_fn = loss_fn# 用于计算评价指标self.metric = metric# 记录训练过程中的评价指标变化self.dev_scores = []# 记录训练过程中的损失变化self.train_epoch_losses = []self.dev_losses = []# 记录全局最优评价指标self.best_score = 0def train(self, train_loader, dev_loader=None, **kwargs):# 将模型设置为训练模式,此时模型的参数会被更新self.model.train()num_epochs = kwargs.get('num_epochs', 0)log_steps = kwargs.get('log_steps', 100)save_path = kwargs.get('save_path', 'best_model.pth')eval_steps = kwargs.get('eval_steps', 0)# 运行的step数,不等于epoch数global_step = 0if eval_steps:if dev_loader is None:raise RuntimeError('Error: dev_loader can not be None!')if self.metric is None:raise RuntimeError('Error: Metric can not be None')# 遍历训练的轮数for epoch in range(num_epochs):total_loss = 0# 遍历数据集for step, data in enumerate(train_loader):x, y = datalogits = self.model(x.float())loss = self.loss_fn(logits, y.long())total_loss += lossif step % log_steps == 0:print(f'loss:{loss.item():.5f}')loss.backward()self.optimizer.step()self.optimizer.zero_grad()# 每隔一定轮次进行一次验证,由eval_steps参数控制,可以采用不同的验证判断条件if eval_steps != 0:if (epoch + 1) % eval_steps == 0:dev_score, dev_loss = self.evaluate(dev_loader, global_step=global_step)print(f'[Evalute] dev score:{dev_score:.5f}, dev loss:{dev_loss:.5f}')if dev_score > self.best_score:self.save_model(f'model_{epoch + 1}.pth')print(f'[Evaluate]best accuracy performance has been updated: {self.best_score:.5f}-->{dev_score:.5f}')self.best_score = dev_score# 验证过程结束后,请记住将模型调回训练模式self.model.train()global_step += 1# 保存当前轮次训练损失的累计值train_loss = (total_loss / len(train_loader)).item()self.train_epoch_losses.append((global_step, train_loss))self.save_model(f'{save_path}.pth')print('[Train] Train done')# 模型评价阶段def evaluate(self, dev_loader, **kwargs):assert self.metric is not None# 将模型设置为验证模式,此模式下,模型的参数不会更新self.model.eval()global_step = kwargs.get('global_step', -1)total_loss = 0self.metric.reset()for batch_id, data in enumerate(dev_loader):x, y = datalogits = self.model(x.float())loss = self.loss_fn(logits, y.long()).item()total_loss += lossself.metric.update(logits, y)dev_loss = (total_loss / len(dev_loader))self.dev_losses.append((global_step, dev_loss))dev_score = self.metric.accumulate()self.dev_scores.append(dev_score)return dev_score, dev_loss# 模型预测阶段,def predict(self, x, **kwargs):self.model.eval()logits = self.model(x)return logits# 保存模型的参数def save_model(self, save_path):torch.save(self.model.state_dict(), save_path)# 读取模型的参数def load_model(self, model_path):self.model.load_state_dict(torch.load(model_path, map_location=torch.device('cpu')))def plot_training_loss_acc(runner, fig_name, fig_size=(16, 6), sample_step=20, loss_legend_loc='upper right',acc_legend_loc='lower right',train_color='#8E004D', dev_color='#E20079', fontsize='x-large', train_linestyle='-',dev_linestyle='--'):plt.figure(figsize=fig_size)plt.subplot(1, 2, 1)train_items = runner.train_epoch_losses[::sample_step]train_steps = [x[0] for x in train_items]train_losses = [x[1] for x in train_items]plt.plot(train_steps, train_losses, color=train_color, linestyle=train_linestyle, label='Train loss')if len(runner.dev_losses) > 0:dev_steps = [x[0] for x in runner.dev_losses]dev_losses = [x[1] for x in runner.dev_losses]plt.plot(dev_steps, dev_losses, color=dev_color, linestyle=dev_linestyle, label='dev loss')plt.ylabel('loss')plt.xlabel('step')plt.legend(loc=loss_legend_loc)if len(runner.dev_scores) > 0:plt.subplot(1, 2, 2)plt.plot(dev_steps, runner.dev_scores, color=dev_color, linestyle=dev_linestyle, label='dev accuracy')plt.ylabel('score')plt.xlabel('step')plt.legend(loc=acc_legend_loc)# 将绘制结果保存plt.savefig(fig_name)plt.show()def read_csv_labels(fname):"""读取fname来给标签字典返回一个文件名"""with open(fname, 'r') as f:# 跳过文件头行(列名)lines = f.readlines()[1:]tokens = [l.rstrip().split(',') for l in lines]return dict(((name, label) for name, label in tokens))class CIFAR10Dataset(Dataset):def __init__(self, folder_path, fname):self.labels = read_csv_labels(os.path.join(folder_path, fname))self.folder_path = os.path.join(folder_path, 'train')def __len__(self):return len(self.labels)def __getitem__(self, idx):img = read_image(self.folder_path + '/' + str(idx + 1) + '.png')label = self.labels[str(idx + 1)]return img, torch.tensor(int(label))def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):# 通过is_grad_enabled来判断当前模式是训练模式还是预测模式if not torch.is_grad_enabled():# 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)else:assert len(X.shape) in (2, 4)if len(X.shape) == 2:# 使用全连接层的情况,计算特征维上的均值和方差mean = X.mean(dim=0)var = ((X - mean) ** 2).mean(dim=0)else:# 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。# 这里我们需要保持X的形状以便后面可以做广播运算mean = X.mean(dim=(0, 2, 3), keepdim=True)var = ((X - mean) ** 2).mean(dim=(0, 2, 3), keepdim=True)# 训练模式下,用当前的均值和方差做标准化X_hat = (X - mean) / torch.sqrt(var + eps)# 更新移动平均的均值和方差moving_mean = momentum * moving_mean + (1.0 - momentum) * meanmoving_var = momentum * moving_var + (1.0 - momentum) * varY = gamma * X_hat + beta # 缩放和移位return Y, moving_mean.data, moving_var.dataclass BatchNorm(nn.Module):# num_features:完全连接层的输出数量或卷积层的输出通道数。# num_dims:2表示全连接层,4表示卷积层def __init__(self, num_features, num_dims):super().__init__()if num_dims == 2:shape = (1, num_features)else:shape = (1, num_features, 1, 1)# 参与求梯度和迭代的拉伸和偏移参数,分别初始化成1和0self.gamma = nn.Parameter(torch.ones(shape))self.beta = nn.Parameter(torch.zeros(shape))# 非模型参数的变量初始化为0和1self.moving_mean = torch.zeros(shape)self.moving_var = torch.ones(shape)def forward(self, X):# 如果X不在内存上,将moving_mean和moving_var 复制到X所在显存上if self.moving_mean.device != X.device:self.moving_mean = self.moving_mean.to(X.device)self.moving_var = self.moving_var.to(X.device)# 保存更新过的moving_mean和moving_varY, self.moving_mean, self.moving_var = batch_norm(X, self.gamma, self.beta, self.moving_mean,self.moving_var, eps=1e-5, momentum=0.9)return Ybatch_size = 20
# cifar10_tiny是卷积神经网络那节课的数据集的文件夹
train_data = CIFAR10Dataset('cifar10_tiny', 'trainLabels.csv')
train_iter = DataLoader(train_data, batch_size=batch_size)num_classes = 10
# 定义模型
net = nn.Sequential(nn.Conv2d(3, 6, kernel_size=5), BatchNorm(6, num_dims=4), nn.Sigmoid(),nn.AvgPool2d(kernel_size=2, stride=2),nn.Conv2d(6, 16, kernel_size=5), BatchNorm(16, num_dims=4), nn.Sigmoid(),nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),nn.Linear(400, 120), BatchNorm(120, num_dims=2), nn.Sigmoid(),nn.Linear(120, 84), BatchNorm(84, num_dims=2), nn.Sigmoid(),nn.Linear(84, 10))
# 定义损失函数
loss_fn = F.cross_entropy
# 定义优化器
optimizer = torch.optim.SGD(net.parameters(), lr=0.1)runner = Runner(net, optimizer, loss_fn, metric=None)
runner.train(train_iter, num_epochs=10, save_path='BatchNorm')# 第一个批量规范化层中学到的拉伸参数gamma和偏移参数beta
print(net[1].gamma.reshape((-1,)), net[1].beta.reshape((-1,)))