ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

神经网络归一化全解析:从BN到LN,打通训练稳定与模型选型

神经网络归一化全解析:从BN到LN,打通训练稳定与模型选型

最近帮人调一个图像分类网络,结构不复杂,几层卷积加全连接,但训练 loss 落到某个平台期后怎么都不肯再降。查来查去,根子出在归一化层的设置上——batch size 变小之后,Batch Normalization 的统计估计开始失真,把整个训练带偏了。这件事让我把深度学习神经网络里绕不开的归一化问题从头到尾重新捋了一遍,所以有了这篇梳理,专门聊聊归一化神经网络。无论你是刚开始看神经网络,还是已经写了两年训练脚本,这篇都值得抽出半小时慢慢读。

1. 为什么神经网络一深、一优化就“水土不服”:归一化要解决的三个核心问题

很多人把归一化当成一种“加了就更稳”的玄学技巧,好像谁都在用,但问一句“它到底解决了什么问题”,能答上来的人并不多。其实归一化不是为提速而生的花活,它在早期是决定深层网络能不能收敛的关键。要理解后面那些五花八门的变体,得先回到问题本身。

1.1 层与层之间的内协变量偏移

想象一支接力队,每一棒拿到的接力棒大小都不一样:第一棒拿的是 0 到 1 的细棍,第二棒拿到的是 0 到 1000 的粗柱,第三棒又变成 -5 到 5 的刻度尺。那这个队伍不用跑,光是换棒就乱套了。神经网络层间传播也是同一个道理。

网络每一层的输入,是上一层输出的函数;而上一层参数不断更新,它输出的分布就会持续变化。对当前层来说,它面对的输入分布每过几步就换一副面孔,得不断地重新适应。这个现象在经典论文里被称为 internal covariate shift,也就是“内协变量偏移”。虽然后来的研究逐渐证明,Batch Normalization 的优越性并不完全来自“消除内协变量偏移”这个解释,但作为一个入门理解框架,它依然是最直观的入口。

归一化要做的,就是给每一层的输入“定个标准刻度”:均值拉到 0 附近,方差固定到某个可控范围。这样无论前一层怎么折腾,后一层看到的输入分布都相对稳定。训练自然就稳下来了。

1.2 激活函数的饱和区与神经元死亡

没有归一化的时候,还有一个更直接的问题:激活函数会进入饱和区,或者干脆死掉。

以 sigmoid、tanh 这类函数为例,它们的输入端如果落在绝对值很大的区间,导数会趋近于 0。梯度从输出端一路反传回来,经过这个饱和区域就几乎被“吞光”了。前面的网络层收不到有效梯度,训练就变成空转。就算换成 ReLU,也有隐患——如果某层输出整体偏移到负区间,ReLU 会把大部分负值截成 0,梯度也会跟着变 0,神经元直接“罢工”。这就是常说的 dead ReLU。

把激活前的输入分布归一化到均值 0、标准差可控的范围,能保证大多数输入落在激活函数的敏感区间,梯度不至于一开始就被削没。可以说,归一化是在给激活函数“喂”一个它最容易消化吸收的输入尺度。

1.3 学习率选择的困境与优化景观的形状

第三个问题藏得深一点,但调参的时候感受特别明显:没有归一化的网络,学习率特别难选。调小了收敛慢如蜗牛,调大了 loss 直接发散;感觉永远找不到那个“刚刚好”的点。

原因是各层输出的尺度不一致时,损失函数的曲面在不同方向上的曲率差异巨大。有些方向特别陡,有些方向特别平缓。SGD 只用一个学习率去更新所有参数,相当于穿着一双尺码固定的鞋,既要爬陡坡又要走平路,很难兼顾。

归一化能让损失景观变“圆润”一些,至少让各个方向的尺度更均衡。经典的经验是,加了 Batch Normalization 之后,学习率往往可以放大 5 到 10 倍而不会崩。这不是玄学,而是归一化让梯度的变化范围变得可控,优化器更容易找到合适步长。理解了这一点,你就能明白为什么很多训练技巧讨论里,第一步永远是“先把归一化配置搞对”。

2. 归一化到底归了什么:四种主流方法的数学定义与直观理解

搞清楚“为什么归一化”之后,下一步是“归一化什么”。同样是做标准化,Batch Normalization、Layer Normalization、Instance Normalization、Group Normalization 的统计轴完全不同。很多人在这一步踩坑,是因为只记住了 API 的调用方式,没搞明白它内部究竟沿着哪个维度在统计。

2.1 Batch Normalization:跨样本、同通道的统计归一

Batch Normalization,也就是常说的 BN,操作对象是一整个 batch 的中间特征。在卷积网络里,一个特征张量通常是四维的:(N, C, H, W),N 是 batch size,C 是通道数,H 和 W 是特征图的高宽。BN 做的事情是:对第 c 个通道,把所有样本、所有位置上的数值拉在一起,算一个均值和方差,然后做标准化。

公式可以写成:

y = gamma * (x - mu_b) / sqrt(sigma_b^2 + epsilon) + beta

mu_b 和 sigma_b 是当前 batch 的统计数据,gamma 和 beta 是可学习参数,每个通道一组,epsilon 是防止分母为 0 的小常数,一般取 1e-5。gamma 初始化为 1,beta 初始化为 0,也就是说网络一开始先“照单全收”标准化后的分布,然后再自己学要不要缩放和平移。

BN 最大的特点是强依赖 batch size。统计量是从当前 batch 里估出来的,batch 越小,估计越不准。这个特性在后面的实践章节还会反复提到。

2.2 Layer Normalization:单样本内全特征归一的 Transformer 标配

Layer Normalization,LN,统计轴换了方向。它不再跨样本,而是对单个样本内部的所有特征做归一化。在最常见的 Transformer 结构里,是对每个 token 的 hidden 维求均值和方差。

为什么 NLP 场景几乎清一色用 LN?因为序列任务里样本长度经常不一样,batch 里各样本能处理的信息量也不同;而且推理时输入可能是一个一个来的,batch size 等于 1。如果用 BN,统计量根本不稳定。LN 不依赖 batch,输入多长都能稳定工作,训练和推理行为一致,不需要维护额外的全局统计量。

代价是 LN 对单个样本内部的分布做了一个较强的假设:它认为同一个样本不同特征维之间的相对关系可以压平。在大多数序列任务里,这个假设是成立的,而且效果好得惊人,所以如今几乎所有主流语言模型都默认叠 LN。

2.3 Instance Normalization 与 Group Normalization:风格化与小 batch 的答案

Instance Normalization,IN,比 LN 更“狠”:它只对单个样本、单个通道做归一化。在图像生成、风格迁移任务里,IN 的表现往往比 BN 好。原因是这类任务里,图像的全局对比度和颜色信息经常是风格的一部分,不应该被保留在内容特征里。IN 把每个通道的均值和方差都强制归一化,相当于抹掉了对比度信息,让网络集中精力学结构特征,因此成了风格迁移的标配。

Group Normalization,GN,是 IN 和 LN 之间的一种折中。它把通道分成若干组,在每组内部做归一化,既不需要 batch,又保留了一定的通道间相关性。典型设置是分成 32 组。GN 是在“batch size 很小,BN 失效”的背景下火的。当显存只允许跑 batch size 2 甚至 batch size 1 的时候,BN 的统计量会剧烈抖动,而 GN 几乎不受影响。这个特性让它在分割、检测等吃显存的任务里特别受欢迎。

2.4 四种方法横向对比:统计维度、参数与适用场景

方法归一化轴是否依赖 batch训练/推理一致性典型适用场景
BN跨样本,按通道统计 (N,H,W)是不一致,推理用 running statsCNNs 图像分类、目标检测
LN单个样本内部全部特征否一致Transformer、RNN、NLP 任务
IN单个样本、单个通道否一致风格迁移、图像生成
GN单个样本、通道分组内否一致小 batch 的视觉任务、视频理解

这里所说的“维度”,指的是统计均值方差时沿着哪些位置做聚合,不是在说线性代数里的向量维度。BN 沿着“同通道的所有样本位置”聚合;LN 沿着“同一样本的所有特征位置”聚合;IN 是“同一样本同通道”聚合;GN 则折中为“同一样本通道组内”聚合。选哪一种,本质上是选择“你相信哪些位置之间应该共享同一个分布”。

3. 训练前向、反向与推理:归一化层那些容易忽略的细节

归一化的很多坑不在前向公式里,而在训练和推理的“行为差异”里。尤其 BN,一套逻辑在训练状态跑,另一套逻辑在推理状态跑,稍不留意就翻车。

3.1 训练期与推理期的行为差异:running statistics 的秘密

训练时,BN 用的是当前 batch 的均值和方差,这是“活”的统计量,每步都在变。但推理时,你不可能依赖未来样本,所以 BN 内部维护了一组 running_mean 和 running_var,用训练过程中的统计量做移动平均,推理时直接用这组固定值。

这里就藏着一个经典失误:模型训练完直接拿去推理,忘了切换 model.eval() 模式。训练模式下 BN 仍然用当前 batch 的统计量,导致单张图推理时 BN 实际是在用“一个样本的均值方差”做标准化,输出结果可想而知。我自己就见过有人花了一整天才排查出来,原来是测试脚本里少了一行 eval() 切换。

running stats 的更新公式也值得看一眼:

running_mean = (1 - momentum) * running_mean + momentum * mu_b running_var = (1 - momentum) * running_var + momentum * sigma_b

momentum 默认常见值是 0.1 或 0.01,代表新 batch 统计量有多大权重进来。这个值不是调参重点,但知道它存在,至少不会懵。

LN、IN、GN 没有 running stats 这套东西,训练和推理行为一致,所以从工程角度讲,它们比 BN 更好伺候。

3.2 归一化层对梯度流动的缩放效果

很多人以为归一化只影响前向,不影响反向。实际上它最重要的作用之一发生在反向传播里。看 BN 的公式,前向里有除以标准差这一步,反向传播梯度时,这个因子会以类似 1 / sqrt(sigma^2 + epsilon) 的形式作用到梯度上,相当于给梯度做了一次动态缩放。

这意味着什么?当某一层输出的方差比较大时,归一化会把前向信号压回正常尺度,同时反向梯度的量级也被限制住,不会突然大爆发。当方差比较小时,梯度又不会被过分压缩。这种“自平衡”机制,正是 BN 能让训练大幅稳定的核心原因之一,也是它能解锁更大学习率的底层逻辑。

理解这一点后,再看到有人说“归一化就是减均值除方差”时,你就知道那只是皮毛。真正有价值的是它对整个优化过程的梯度尺度调节。

3.3 一个可以直接复现的 PyTorch 对比实验

为了直观感受归一化对训练的影响,可以跑一个最简单的多层感知机对比。同一个网络结构,分别用带 BatchNorm1d 和不带归一化的版本,在同一学习率下训练:

import torch import torch.nn as nn import torch.nn.functional as F class SimpleMLP(nn.Module): def __init__(self, use_bn=True): super().__init__() self.fc1 = nn.Linear(784, 256) self.bn1 = nn.BatchNorm1d(256) if use_bn else nn.Identity() self.fc2 = nn.Linear(256, 128) self.bn2 = nn.BatchNorm1d(128) if use_bn else nn.Identity() self.fc3 = nn.Linear(128, 10) def forward(self, x): x = x.view(x.size(0), -1) x = F.relu(self.bn1(self.fc1(x))) x = F.relu(self.bn2(self.fc2(x))) return self.fc3(x)

你可能注意到我把 BN 放在了全连接层之后、激活函数之前。这是 BN 的通行用法:对线性层的输出做归一化,让激活函数接收一个分布更规整的输入。在同样的学习率、同样迭代轮数下,你会看到有 BN 的版本 loss 下降更快,曲线更平滑;没 BN 的版本要么下降很慢,要么 loss 出现明显波动。这个实验十分钟就能跑完,对理解归一化远比看十篇博客实在。

4. 从 CV 到 NLP 再到 GAN:不同场景下的归一化选型与避坑实录

归一化不是“一种方法打天下”。CV 里最好用的 BN,到 NLP 里就未必;生成模型里大家又偏爱 IN、AdaIN。选型不匹配,轻则效果打折,重则直接训崩。

4.1 CNN 与 BN:通道统计、batch size 与 SyncBN

在卷积网络里,BN 统计时是把 (N, H, W) 三个维度合在一起,对每个通道单独算。也就是说,通道 C 的位置上有多少个参与统计的值,取决于 N × H × W。H 和 W 比较大的时候,即便 batch 小一些,统计量也还算稳;但如果输入分辨率很低,比如 CIFAR 这种 32×32 的图,N 又小,BN 的估计误差就会凸显出来。

还有一个容易被忽略的点:数据加载顺序。BN 假设一个 batch 里的样本近似独立同分布。如果你的 DataLoader 把同一个类别的图片堆在一起,batch 内的统计量就严重偏向该类,会让 BN 的估计产生偏差。所以在用 BN 时,shuffle 不能省。

多卡分布式训练时又有一层新的坑。默认情况下,每张卡各自算自己的 BN 统计量,卡与卡之间数据分布不完全一致,会导致模型输出的语义出现偏差。这时候需要 SyncBN,也就是把各卡的统计量同步后再归一化。代价是额外的通信开销,但一致性会好很多。

4.2 Transformer 与 NLP:LayerNorm 为什么是铁打的首选

Transformer 类模型里,藏着一句话:Norm 层基本都是 LN,几乎没人用 BN。原因我在前面提过,核心是序列任务的长度可变和 batch 敏感性。还有一个工程原因:BN 需要维护 running stats,这在模型序列化导出时多一层状态;LN 不需要,部署更干净。

看 Transformer 的结构,LN 通常放在残差块的一个特定位置,要么是 post-norm,要么是 pre-norm。早期 Transformer 是 post-norm,后来的实践逐渐偏向 pre-norm,也就是先做 LN,再进注意力/前馈网络。pre-norm 的训练更稳定,对学习率不那么敏感,成为现代大模型的主流。

如果你在自己的 Transformer 模型里试过把 LN 换成 BN,大概率会发现训练很不稳,而且 batch 一大一小效果忽好忽坏。这不是你的实现问题,是归一化轴的选择和任务特性根本不匹配。

4.3 生成模型与风格迁移:InstanceNorm 和自适应归一化的特殊地位

GAN、风格迁移这类生成任务,对归一化的需求很不一样。图像生成网络通常希望保留局部结构信息,同时又不希望网络太依赖全局对比度。IN 在这里大放异彩,因为它在单个样本单通道上做归一化,等于是把每张图的亮度、对比度信息标准化掉,留下纹理和结构的相对关系。

风格迁移里还有一个升级操作:AdaIN,自适应实例归一化。它不再是简单地归一化,而是把内容图的归一化结果,用风格图的均值和方差重新“染色”,实现风格快速迁移。这个思路的核心,就是把“均值和方差看成风格的载体”这个假设用到了极致。

实际训练生成模型时,还有一个常见误区:生成器和判别器的归一化配置不一致,或者随意更换,导致训练动态剧烈改变。生成模型本来就脆弱,动归一化层之前,最好先做消融实验,而不是一次改三四个地方。

4.4 三个真实的踩坑经历与排查链路

第一个坑:训练好好的,推理结果明显不对。当时的表现是训练 loss 正常下降,但验证集输出全是一坨无意义的数值。排查链路是先怀疑预处理,再怀疑权重加载,最后发现是测试脚本没有调用 model.eval(),BN 和 Dropout 的模式都不对。这个坑看起来低级,但很多人都会栽一次。

第二个坑:batch size 从 32 减到 2,loss 开始周期性地剧烈波动,训练几乎不收敛。一开始以为是学习率太大了,调小后波动幅度变小但收敛变慢,始终找不到合适的值。后来把主要卷积层的 BN 换成 GN,再把学习率调回原来的量级,问题立刻缓解。从此我记住了:BN 的统计量依赖 batch size,不是所有模型都能无脑上 BN。

第三个坑:BN 是放在激活函数之前还是之后?这个讨论至今还有人问。通行做法是放在线性层/卷积层之后、激活函数之前,也就是 Conv-BN-ReLU 的顺序。但如果网络很深,某些实现里放在激活之后也能训练,只是效果通常不如前者。关键是同一个网络里所有 BN 的位置要统一,不要一半在激活前、一半在激活后,否则梯度流动的逻辑就乱了。

5. 归一化的演化与我对它的最终理解:从动态归一化到“去归一化”

技术在往前走,归一化也不是一成不变。搞清楚近期的一些演进方向,对我们理解“归一化到底在干嘛”很有帮助。

5.1 从固定归一化到可学习的动态选择

既然 BN、LN、IN 各有各的适用场景,那有没有可能让网络自己选?这是 Switchable Normalization 这类工作的出发点。它把 BN、LN、IN 的统计量加权组合在一起,权重由网络自己学习,在不同任务、不同阶段自动偏向不同的归一化方式。从实验结果看,它在某些视觉任务上确实比单一归一化更强,尤其当数据分布比较复杂时。

这个方向的启示是:归一化的“统计轴”本身也可以成为可学习的对象。不要把自己局限在“某种归一化一定最好”的思维里,而是思考数据和任务到底更符合哪个轴的统计假设。

5.2 “去归一化”网络给了我们什么启发

还有一类更激进的工作,试图把归一化层整个去掉,只通过精细的初始化、缩放系数和残差连接设计来稳定训练。乍一听很反直觉,但它能训练得很深,这让研究者重新反思:深度网络训练不稳定的根源,是“没有归一化”,还是“信号和梯度的尺度失控”?

如果网络设计能让每一层的输出方差和梯度范数保持稳定,那么即使不做显式的归一化,训练也一样能进行。换句话说,归一化只是实现尺度控制的一种手段,不是目的本身。理解这个区别后,我再看任何新模型时,第一反应不再是“它有没有 BN”,而是“它的信号尺度控制机制是什么”。这个视角的转变,对我后续调模型帮助非常大。

5.3 如何快速判断归一化配置合不合理

踩过不少坑之后,我沉淀出一套自查方法,分享出来供参考。模型刚训练几十步时,打印一下各层输出的均值和方差,或者看梯度范数变化。如果输出均值明显偏离 0、方差忽大忽小,或者梯度范数在几百步内从 1e+4 跳到 1e-2,那归一化配置很可能有问题。

再用一个低成本消融:把模型里的归一化层临时替换成另一种,只改一行代码,比较前几百步的 loss 曲线形态。如果差异巨大,说明归一化选型不合;如果差异很小,说明这个问题对当前数据不敏感。这种消融比完整训练到收敛省时间得多,也更容易定位问题。

最后说一点个人体会。做了这么多实验,最深的感受是:归一化不是“锦上添花”的正则化技巧,而是决定网络能不能顺利训练的地基工程。遇到奇怪的 loss 曲线、发散的梯度、怎么调学习率都没用的窘境,别第一个怀疑模型结构,先看看归一化对不对。很多时候,问题就藏在这层看似简单的“减均值除方差”里。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进