尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

深入理解Batch Normalization:从内部协变量偏移到训练稳定

发布时间:2026/9/15 12:00:34

资讯中心
01
ARTICLE

深入理解Batch Normalization:从内部协变量偏移到训练稳定

深入理解Batch Normalization:从内部协变量偏移到训练稳定
如果你和我一样在2015年前后扎进过那种十几层、没有Batch Normalization的网络里大概体会过一种状态白天调学习率晚上怀疑人生。隐层一多、激活函数据说得很美loss简直像弹簧前层权重稍微动一下后面几层的输入分布就跳起来梯度也跟着一起发疯。这个现象背后有一个经典的名词Internal Covariate Shift内部协变量偏移。而把训练从“玄学调参”里救出来的关键操作就是Batch Normalization批归一化。这一篇我想从“为什么会出现ICS”讲起再把BN的设计思路、数学原理、PyTorch里的具体用法和踩坑经验一次说透。不搞速成式的八股也不堆公式吓人尽量把每一步背后的考虑讲明白。适合刚入门深度学习、正在手撕CNN或训练深层网络的读者也适合那些用过BN但一直没搞懂它到底做了什么的同学。1. Internal Covariate Shift 到底在说啥1.1 从一个调参翻车现场说起很多人在第一次搭深层网络时会经历一个特别经典的场面网络层数从5层加到15层激活函数换成sigmoid或tanh然后训练就突然变得极其脆弱。学习率设到0.01loss在前几个step还能往下走再跑几步突然变成NaN学习率降到0.001loss又像蜗牛一样不动了。你试了各种初始化方法Xavier、He都上了问题还是时好时坏。问题出在哪儿呢把网络的前向过程拆开看每一层的输入都是由前面所有层共同计算出来的。训练过程中前层权重一旦更新后层的输入分布就会跟着变。对第10层来说它根本不知道自己下一个step收到的输入会是什么分布。今天的输入均值为0、方差为1明天可能均值变成5、方差变成0.01第10层只能被动地去适应。就像你每天上班工位被同事来回搬今天椅子往左挪五厘米明天桌子又往后拉三米你没法稳稳当当地干活。这个“后层输入分布训练中不断变化”的现象就是Internal Covariate Shift。Ioffe和Szegedy在2015年那篇提出Batch Normalization的论文里把它定义为训练过程中网络激活分布的变化。它不是外部数据分布的变化而是网络自己内部产生的所以叫“内部”协变量偏移。1.2 协变量偏移的“内部”版本协变量偏移这个词最早出现在传统机器学习里。比如你在A市的街道上训练了一个垃圾分类模型测试时把它放到B市的街道上B市的垃圾桶长相、拍摄角度、光照条件都和A市不一样模型就会水土不服。这就是外部协变量偏移——输入数据的分布变了但条件输出分布没变模型需要重新适应。深度网络里的ICS是同一件事的“内部版本”。只不过此时输入分布的变化不是来自数据集而是来自网络前层参数的更新。假设现在有一个L层的网络第l层的输入是前面l-1层共同作用的产物用符号 z^{(l-1)} 表示。训练第t步时前层参数从 θ_{1:l-1}^{(t)} 更新到 θ_{1:l-1}^{(t1)}那么第l层的输入分布 p(z^{(l-1)} | θ_{1:l-1}) 也会发生改变。第l层如果想保持输出正确它的参数就必须不断追赶这些变化。关键是这种变化不是线性的、缓慢的。深层网络里每一层都在做非线性变换前层的微小变动经过多层叠加后可能被放大得面目全非。于是网络越深后层输入分布的波动就越剧烈训练也就越难稳定下来。这也是为什么早期深层网络训练极依赖初始化、学习率需要反复试探甚至有人一度觉得“网络太深就是练不动”。1.3 ICS 是怎么把训练搞崩的ICS带来的麻烦远不止“loss曲线不好看”这么简单。往细了拆它至少在三处地方卡住了深度网络的脖子。第一它让非线性层容易进入饱和区。以sigmoid为例它只在输入接近0的区间梯度比较大输入绝对值一大梯度就趋近于0。ICS导致层输入分布不断漂移一旦某个时刻大规模进入正负很大的区域sigmoid直接饱和。饱和意味着反向传播时梯度几乎为零前层几乎收不到有效的更新信号等于整个网络停摆。第二它放大了梯度消失和梯度爆炸。深层网络反向传播时梯度要经过链式法则逐层回传。每一层的梯度里都包含输入分布的因素如果分布不稳定梯度的大小就无法预测。有时候连续几个小梯度的连乘导致梯度消失有时候又连续几个大于1的梯度相乘导致爆炸。看不透、摸不准调起来极其痛苦。第三它直接限制了学习率的选择范围。学习率太大参数更新幅度大前层输出分布剧烈变化后层刚适应的输入分布又变得面目全非loss直接飞掉。学习率太小整个训练慢如龟速。很多人以为这是优化算法的问题换一个Adam就好一点但治标不治本——问题出在网络内部每层输入的稳定性上。更麻烦的是ICS还会让网络对初始化高度敏感。同一个网络换一套初始化参数可能一个收敛一个发散。因为初始权重决定了最初每一层输入的分布而ICS又让这种分布随训练不断变化初始条件的一点点差异会被层层放大。这也是为什么在Batch Normalization出现之前“如何初始化”能成为一个独立的研究方向。2. Batch Normalization 的设计方案2.1 朴素的起点把每层输入拉回标准正态既然ICS的根源是“每层输入分布随前层参数变化”一个非常自然的想法就是能不能在每层网络之前把输入分布固定住如果能把输入拉回到均值为0、方差为1的标准正态分布后层网络永远面对相对一致的输入分布也就不必费劲去追赶前层的波动了。这就是Batch Normalization的核心构思。具体做法是在每一层的激活函数之前或者之后这个后面细说插入一个归一化步骤先计算当前mini-batch内每个通道的均值和方差然后把输入减均值、除标准差让数据回到标准正态分布附近。由于SGD训练本身就是基于mini-batch的用batch内的统计量来做归一化既自然又不需要额外扫一遍数据。用生活里的例子来类比这就像是给每层输入设了一个“标准度量尺”不管你前层输出什么量纲、什么分布我都先换算成统一的“标准尺寸”再交给后面的层处理。后层不需要关心前层到底输出了什么只要在这个标准尺下处理问题就行。2.2 引入 γ 和 β避免把网络“锁死”如果单纯把每层输入归一化到标准正态其实是有隐患的。归一化会把数据的表达能力压缩得太死所有层都只能处理均值为0、方差为1的输入网络能够表达的数据分布范围就被大大限制。而且强行把输入钉在零附近对某些任务来说可能并不是最优的。所以Ioffe和Szegedy在归一化之后又加了一步通过两个可学习的参数γ和β对归一化结果做一次线性变换。用公式来表达的话就是x_hat (x - μ_B) / sqrt(σ_B² ε) y γ * x_hat β其中μ_B和σ_B是当前mini-batch的均值和标准差ε是一个很小的常数防止除以零通常取1e-5。γ初始化为1β初始化为0这样刚插入BN层时相当于没做多余变换网络可以随着训练自行决定要不要把分布拉伸或平移。这两个参数的意义是给网络留出“反悔”的余地。有些层可能确实希望输入保持归一化状态那γ和β就维持在接近默认值有些层可能发现稍微放大某个维度的贡献、平移一点分布效果更好那它就可以通过梯度更新把γ和β调成所需要的数值。本质上是让网络自己决定每一层输入分布的形状而不是强制标准正态。2.3 训练与推理为什么使用不同的统计量BN层在实际使用中有一个特别容易让人迷糊的地方训练时它使用当前mini-batch的均值和方差做归一化推理时它就不应该再用batch统计量了而是使用训练阶段累积下来的全局统计量也就是PyTorch里常说的moving average滑动平均。原因也很直接。推理时你通常一次只处理一个样本或者一个很小的batch。单个样本的均值和方差本身没有意义如果强行用当前batch统计量归一化结果会因为batch内容不同而剧烈抖动。而且模型部署上线时输入数据的batch大小可能与训练时完全不一样这会直接导致输出不稳定。因此BN层在训练过程中会额外维护一组running_mean和running_var用滑动平均的方式记录整个训练集的统计特性running_mean (1 - momentum) * running_mean momentum * batch_mean running_var (1 - momentum) * running_var momentum * batch_varPyTorch中momentum的默认值是0.1意思就是当前batch统计量只占10%的权重历史累计值占90%这样可以平滑掉个别batch的异常波动。推理时直接用这份running_mean和running_var做归一化不再更新它们。2.4 BN 的计算流程与 PyTorch 里的手动实现把BN的计算流程拆开看其实就是一个“归一化缩放平移”的组合。以图像数据为例假设输入x的形状是(N, C, H, W)N是batch大小C是通道数H和W是空间尺寸。BN对每个通道单独计算均值和方差也就是说一个通道内的所有像素点、所有batch样本会一起参与统计。用PyTorch代码实现一个简化的BN计算逻辑大致长这样import torch import torch.nn as nn def manual_bn(x, gamma, beta, eps1e-5): # x: (N, C, H, W)这里按每个通道计算统计量 mean x.mean(dim(0, 2, 3), keepdimTrue) var x.var(dim(0, 2, 3), unbiasedFalse, keepdimTrue) x_hat (x - mean) / torch.sqrt(var eps) return gamma.view(1, -1, 1, 1) * x_hat beta.view(1, -1, 1, 1)注意这里的var使用了unbiasedFalse也就是计算总体方差而不是样本方差目的是与PyTorch内置BatchNorm的实现保持一致避免分母差一个“n-1”带来的小偏差。实际使用中直接调用nn.BatchNorm2d即可它会把训练时的batch统计量更新、推理时的滑动平均、可学习参数γβ这些细节全部处理好。3. BN 的核心作用从“玄学调参”到“正向训练”3.1 让每层输入分布稳定躲开饱和区BN给人最直观的改变就是深层网络终于可以老老实实地训练了。原因在于每一层输入在进入非线性激活函数之前先被拉到了零附近。对于sigmoid、tanh这类两端饱和的函数零附近正是梯度最充沛的区域。我自己的感觉是原来那种“网络跑到一半梯度消失、loss横盘不动”的情况加了BN之后会明显减少。因为即便前层参数发生较大更新BN层也会立刻把输入的均值和方差拉回到相对稳定的范围内后续层看到的数据不会出现剧烈的“过山车式”变化。这相当于给每一层装了一个自动校准器前层再怎么折腾后层收到的东西仍然在可控范围内。3.2 为什么可以放心地调大学习率很多人第一次感受到BN的威力是在把SGD的学习率从0.001直接调到0.01甚至0.1之后网络不仅没有炸收敛速度反而显著加快。放以前这几乎是不可能的事情。BN能撑起大学习率的原因可以从损失曲面的角度来理解。Santurkar等人在2018年的论文里做过一个很有说服力的实验对比有BN和无BN的网络画出它们在参数空间中的损失曲面。结果发现无BN的损失曲面像一片陡峭崎岖的山地梯度方向变化剧烈一步迈大了就容易掉下悬崖而加了BN的损失曲面明显更平滑梯度的大小和方向都更稳定所以大步长也相对安全。大白话解释BN让损失函数这个“地形”变得温顺了地面不再到处是坑坑洼洼的隆起和断崖。你调大学习率相当于从“在山路上开快车”变成了“在高速上开快车”只要方向别太离谱不容易翻车。3.3 降低对初始化的敏感度在BN出现之前初始化策略是深度学习的“玄学核心”。权重初始化得不好网络直接不收敛初始化得凑巧训练就顺利。Xavier初始化、He初始化这些方法本质都是在尽量控制每层输入输出的方差让信号在网络中传播时不至于过大或过小。BN的作用是在每层输入进入非线性之前强行标准化一次主动控制了方差。所以即便你的初始化略差一些前几轮训练中BN也能逐步把分布拉回合理范围。当然这并不意味着可以完全无视初始化差的初始化仍然会拖慢训练但BN明显降低了“一步初始化错步步错”的风险。在实践里我通常会配合He初始化一起使用双保险。3.4 附带的隐式正则化效果BN还有一个容易被忽视的作用它会给训练过程带来一点微小的扰动噪声。因为在每个mini-batch中一个样本的归一化结果会受同一batch里其他样本的影响。换句话说同一个样本在不同batch里经过BN层之后的具体数值是不一样的。这种由batch内样本组合引入的随机性跟Dropout有一点相似都是给网络加点“干扰”让模型不容易过度依赖某个样本的精确值。很多实验里也发现加了BN的网络即便完全不用Dropout也不会轻易过拟合。当然这只是一个附带福利BN的主要目标仍然是稳定训练不能把它当成正则化的万能替代品。3.5 一个重要的“反转”BN真的在解决ICS吗这里必须澄清一个容易被误解的知识点。2015年的原始论文把BN提出时的动机归结为“减少Internal Covariate Shift”大家也就习惯性地认为BN是因为解决了ICS才有效的。2018年前后不少研究者深入验证之后发现事情没那么简单。Santurkar等人的论文通过实验证明BN在某些设置下并没有真正减少内部协变量偏移甚至有可能让层输入分布变化更大但网络的训练却依然更稳定、收敛更快。他们提出了另一种解释BN真正的作用在于让损失曲面的曲率更小、更平滑使得基于梯度的优化更容易走通。这就好比同一台机器说明书上说“换了个更精密的零件所以跑得更顺”后来拆机发现真正起作用的是“整体结构变得更稳定了”。作为使用者我们不需要被这两种解释捆住手脚但知道这个背景能帮助你更准确地在论文答辩或技术分享中讲清楚BN的机理而不是背一个可能过时的结论。4. 实操PyTorch 中如何用好 Batch Normalization4.1 BatchNorm1d、2d、3d 到底有什么区别在PyTorch里BN家族有三个常用的类nn.BatchNorm1d、nn.BatchNorm2d、nn.BatchNorm3d。它们的算法本质完全一样区别只在于处理的数据形状不同。模块适用输入形状常见场景BatchNorm1d(N, C) 或 (N, C, L)全连接层、一维卷积、序列特征BatchNorm2d(N, C, H, W)图像分类、目标检测、二维卷积网络BatchNorm3d(N, C, D, H, W)视频理解、医学影像、三维卷积网络它们统计均值和方差的方式也对应不同BatchNorm1d在最后一个维度上统计BatchNorm2d在H和W上统计BatchNorm3d在D、H、W上统计。使用的时候num_features参数要传通道数C这一点千万别弄错。4.2 CNN 中 BN 放在哪里Conv-BN-ReLU 还是 Conv-ReLU-BN这是个高频问题。绝大多数经典模型包括ResNet采用的都是Conv - BN - ReLU的顺序。先把卷积输出归一化再送入ReLU激活理由是在激活函数之前归一化可以把输入稳定在ReLU输出较鲁棒的区间避免某些神经元一直处于死区。不过也有相反的流派。在网络较深时把BN放在卷积之后、激活之前会让“残差块”的路径更干净训练也更稳定。比如ResNet v2就是调整成了BN放在加和之后、激活之前效果和训练稳定性都优于v1。对日常使用来说我的建议很直接CNN里无脑Conv BN ReLU先跑通一个基线有问题再调位置。4.3 迁移学习、微调场景下的 BN 处理技巧过来人经验在迁移学习里BN是个坑。预训练模型在ImageNet这种大数据集上训练时running_mean和running_var已经被校准得很好它们记录的是源域数据的分布。当你把模型迁移到自己的小数据集上微调如果数据分布差异比较大BN的统计量会随训练更新这是好事但如果你用很小的学习率只想微调最后几层又让所有BN层继续更新统计量就可能出现一种奇怪的现象前面的特征提取层基本没变后面BN统计量却被小数据集的少数batch带偏了。我的处理办法分场景如果数据量充足就正常微调让BN跟着更新如果数据量很少则建议冻结大部分底层参数同时把BN层也冻结也就是设置running_mean和running_var不再更新γβ也保持不动。在PyTorch里可以通过requires_gradFalse和track_running_statsFalse配合控制或者直接把批量大小调大一点给BN统计量一个稳定的估计基础。4.4 一个直接能跑的对比实验有 BN vs 无 BN下面给一个非常简化的对比框架方便你直观感受BN在真实训练中的影响。这里用CIFAR-10和一个小型CNN模型结构一样只是是否插入BN层的区别。import torch import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, use_bnTrue): super().__init__() self.use_bn use_bn self.conv1 nn.Conv2d(3, 16, 3, padding1) self.bn1 nn.BatchNorm2d(16) if use_bn else nn.Identity() self.conv2 nn.Conv2d(16, 32, 3, padding1) self.bn2 nn.BatchNorm2d(32) if use_bn else nn.Identity() self.pool nn.MaxPool2d(2) self.fc nn.Linear(32 * 8 * 8, 10) def forward(self, x): x torch.relu(self.bn1(self.conv1(x))) x self.pool(x) x torch.relu(self.bn2(self.conv2(x))) x self.pool(x) return self.fc(x.flatten(1))训练时无BN模型一般需要把SGD学习率压到0.001左右还要小心初始化有BN模型可以直接用0.1的SGD甚至不用花大心思调学习率。我在类似实验里观察到的差距是同样的epoch数量下无BN模型loss下降缓慢收敛后的验证集准确率可能不到60%有BN模型在第一个epoch就能看到loss明显下降最终验证集准确率能高出好几个点。当然这个实验设计并不严谨只是一个帮助你建立体感的demo。重点是当你亲手把同一个网络分别跑一遍看到有BN和无BN在训练曲线上的差异会比任何公式都更有说服力。5. 踩坑集BN 的常见问题和排查技巧5.1 Batch Size 太小BN 效果会崩训练时batch size对BN影响非常大。batch size为2、4这种极端情况下每个batch的均值和方差估计噪声极大归一化后的数据忽高忽低模型训练起来反而更不稳定。尤其是做语义分割、目标检测这类显存受限的任务时batch size往往很小直接上BN会让人头疼。解决思路有三个方向一是换用GroupNorm或LayerNorm这类不依赖batch维度的归一化方式二是使用同步BN即SyncBatchNorm在分布式多卡训练中跨卡汇拢统计量相当于把多个卡上的样本拼成一个大的batch来计算三是尝试梯度累积技术先累积若干step的梯度再更新一次参数尽量保持有效batch size不过小。5.2 训练和推理模式切换带来的诡异 Bug这是一个非常经典的坑。PyTorch中model.train()和model.eval()切换时BN层的行为会发生改变。在train模式下BN使用当前batch的统计量并更新running_mean和running_var在eval模式下BN使用训练阶段累积的running_mean和running_var不再更新。很多人的模型在训练时表现正常一到推理阶段输出却离谱甚至loss突然升高最常见的元凶就是忘记在推理前调用model.eval()。反过来如果你在训练过程中错误地把模型设成了eval模式BN就不会更新统计量也会导致训练异常。排查这个问题时第一步永远先去检查当前模型处于什么模式这一点比检查数据都要优先。5.3 用了 BN 之后还要不要 DropoutBN自带一点正则化效果很多人在网络里加了BN后发现即使去掉Dropout模型也不会明显过拟合。所以在CNN网络中我的习惯是先加BN然后观察验证集表现。如果训练loss和验证loss差距不大就不必再加Dropout如果仍然过拟合再在最后的全连接层之前加一个轻量Dropout。这里有一个值得注意的细节Dropout和BN不要盲目叠加。Dropout通过随机丢弃神经元来制造扰动而BN在小batch下本身就有扰动两者叠加可能导致训练目标过于“飘”让模型难收敛。尤其是batch size已经很小的情况下再加高比例Dropout训练和验证之间的差距不降反升。场景建议做法图像分类batch size较大且有BN可先关掉Dropout观察是否过拟合小batch size网络较深优先保证BN统计量稳定Dropout比例降低全连接网络为主BN与Dropout可同时使用Dropout建议设0.3左右5.4 序列模型和 Transformer 里为什么更常用 LayerNormRNN、LSTM这类序列模型使用BN会存在麻烦。序列数据的长度不一、每个时间步的统计量波动而BN在batch维度上归一化天然受到batch内样本长度差异的影响另外RNN在时间维度上共享参数不同时间步的输入分布差异很大一个BN层很难同时适配所有时间步。Transformer里几乎清一色使用LayerNorm就是因为它不依赖batch维度而是在每个样本内部的特征维度上做归一化。这样样本之间互不干扰也更贴合自注意力机制“每个token独立处理”的设计。所以如果你在做NLP或者时间序列预测不要在模型里堆BN直接用LayerNorm会更稳。5.5 BN 在 GAN、强化学习等非平稳训练中的怪异表现GAN训练里生成器和判别器是一个动态博弈的过程双方的数据分布始终在变。BN在生成器中会引入batch内样本之间的关联往往让生成图像出现“batch风格统一”的痕迹判别器对单张图的判断也会受同batch其他图影响。因此很多GAN模型会改用Instance Normalization或Spectral Normalization来处理这个问题。强化学习也有类似情况数据由智能体自身探索产生batch里样本之间的相关性很强BN统计量不稳定所以RNN策略网络里普遍用LayerNorm。这个知识点不是让你马上就用上但遇到训练发散时可以多一个排查方向。6. BN 的“亲戚们”LayerNorm、InstanceNorm、GroupNorm6.1 一次看懂各种 Normalization 的维度差异围绕“归一化”这件事情学术界后来发展出很多变体。它们数学形式很像唯一区别就是归一化时沿着哪些维度计算均值和方差。以形状为(N, C, H, W)的特征图为例方法计算统计量的维度特点与适用场景BatchNormN、H、W依赖batch size适合大batch的CNN视觉任务LayerNormC、H、W不依赖batch适合NLP、Transformer、序列模型InstanceNormH、W只归一化单个样本、单个通道适合风格迁移、GANGroupNorm先把C分组在每组内的H、W上归一化不依赖batch适合小batch的检测分割任务从表格里能直观看到BN是“跨样本算全局”LayerNorm是“单样本算全局”InstanceNorm是“单样本单通道算局部”GroupNorm则是单样本分组算局部。选哪种本质上取决于你的任务是否允许样本之间共享统计信息。6.2 选型建议什么场景用什么归一化实际问题里选择归一化方法我是按这个思路来判断的。图像分类、目标检测这类数据量大、输入分布相对固定的任务BN永远是第一选择尤其批量大小正常时它效果最好、代码最省事。如果batch size很小比如在显存受限的检测、分割任务里GroupNorm是BN的可靠替代品。它不依赖batch维度效果也比LayerNorm稳定是在小batch场景下最接近BN的选择。风格迁移、图像生成任务里InstanceNorm往往更合适因为它本身就是按单张图单通道做归一化能更好地保留单张图像的风格信息。序列模型和NLP任务则直接选择LayerNorm这一点在Transformer结构中已经形成了标准做法。不需要基于旧习惯硬套BN。6.3 对训练稳定性的一点延伸数据的“尺度”敏感也不全是坏事最后多说一句很多人以为输入分布越标准化越好其实任务不同标准也不一样。BN类方法适合那些需要“统一尺度”的中间特征但有些模型反而需要保留一些尺度信息比如某些生成任务中特征的绝对大小本身就携带语义。这也是为什么会有Conditional Normalization之类的研究方向。理解了归一化的本质——“在某个维度范围内消除分布波动再通过参数恢复需要的表达”你就能自如地在不同任务之间做选择而不是死记某一种方法最好。在我自己的实践中BN给我最深的体会是它把深度学习训练从“猜初始化、猜学习率、猜衰减”的泥潭里拉了出来。以前训练一个12层的卷积网络可能要在调参上花掉两三天现在只要结构正确、默认加上BN再用一个偏大的学习率基本都能顺利跑起来。如果你刚开始学深度学习我建议不要在经典结构上省略BN先感受一下有它和没它的差距再去深入理解背后的原理这比背十篇论文的印象都深。最后再分享一个实用小技巧训练深度模型时可以顺手把BN层的running_mean打印出来观察如果它在训练中后期还在大幅波动通常说明batch size太小或学习率偏大这比只看loss曲线更能提前暴露问题。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。