关于AI归一化的几点记录
先说清楚这次为什么写:模型训练到第 30 轮,loss 突然从 0.3 跳到 800,然后整串 NaN。
看日志确认了几个现象:梯度在某个 batch 突然放大、学习率不算大(1e-4)、网络深度也不夸张(8 层)。
训练又炸了
先说清楚这次为什么写:模型训练到第 30 轮,loss 突然从 0.3 跳到 800,然后整串 NaN。
这不是第一次,但这次我决定停下来仔细看看。
看日志确认了几个现象:梯度在某个 batch 突然放大、学习率不算大(1e-4)、网络深度也不夸张(8 层)。真正的问题在数据:某个特征的范围是 0-10000,另一个是 0-1,还有个是正负分布。
不同特征量级差太多,权重更新时根本不平衡——大数值的特征会抢走梯度,小数值的特征几乎不动。这个判断其实早就知道,但每次都是简单除以最大值,这次打算彻底梳理一下归一化的完整实践链路。
问题是什么
简单用两句话把问题说清楚:
- 数据未归一化:不同特征量级差异过大,导致权重更新不平衡、梯度爆炸/消失
- 内部协变量偏移:网络深层输入分布不断变化,每层都要重新适应,训练慢且不稳定
这两个问题在深层网络、大 batch 训练、复杂特征分布时会尤其明显。
先把数据归一化做了
最基础的一步是把输入数据拉到一个合理的范围。
Min-Max 归一化
把特征线性映射到 [0, 1] 或 [-1, 1]:
def minmax_normalize(X, feature_range=(0, 1)):
min_val = X.min(axis=0)
max_val = X.max(axis=0)
normalized = (X - min_val) / (max_val - min_val)
return normalized * (feature_range[1] - feature_range[0]) + feature_range[0]
这个方法适合特征分布比较均匀、没有明显离群值的情况。但如果有极端值,比如大部分样本在 0-10 之间,突然来个 10000,几乎所有样本都会被压缩到很小的区间。
Z-Score 标准化
减去均值,除以标准差:
def zscore_normalize(X):
mean = X.mean(axis=0)
std = X.std(axis=0)
return (X - mean) / std
这个对离群值更鲁棒一些,因为它是基于分布的统计量而不是极值。标准化后的数据均值为 0,标准差为 1,大部分值会落在 -3 到 3 之间。
踩坑记录
实际使用时遇到了几个问题:
- 训练/测试不一致:如果在训练集上算均值和标准差,但测试集用了不同的统计量,分布就对不上了。正确做法是把训练集的统计量保存下来,测试集直接用。
# 训练时
train_mean = X_train.mean(axis=0)
train_std = X_train.std(axis=0)
X_train_norm = (X_train - train_mean) / train_std
# 测试时
X_test_norm = (X_test - train_mean) / train_std # 用训练集的统计量
- 标准差为 0:某个特征在训练集上全部是同一个值,标准差就是 0,除法会爆炸。需要加一个小的 epsilon:
eps = 1e-8
X_norm = (X - mean) / (std + eps)
- Online 场景:如果数据是流式的,不能一次性拿到全部,就得用滑动窗口或增量更新的统计量。这会比全量统计更复杂,但在实时系统中常常是必须的。
BatchNorm:把归一化搬进网络
数据预处理解决了输入层的分布问题,但深层网络每层的输入分布都在不断变化——内部协变量偏移。2015 年的 BatchNorm 论文提出在每一层都做一次归一化。
基本原理
对一个 batch 的每个维度(特征通道)独立做标准化:
def batch_norm(x, gamma, beta, eps=1e-5, momentum=0.1, training=True):
if training:
# 训练时用当前 batch 的统计量
mean = x.mean(dim=(0, 2, 3), keepdim=True)
var = x.var(dim=(0, 2, 3), keepdim=True)
# 更新全局统计量(用于推理)
running_mean = momentum * running_mean + (1 - momentum) * mean.data
running_var = momentum * running_var + (1 - momentum) * var.data
else:
# 推理时用全局统计量
mean = running_mean
var = running_var
x_norm = (x - mean) / torch.sqrt(var + eps)
return gamma * x_norm + beta # 可学习的缩放和平移参数
这里的 gamma 和 beta 是关键:标准化会破坏网络的表达能力,比如强制均值为 0 标准差为 1,但某些层可能就需要非零均值或非单位标准差。这两个可学习参数让网络能"恢复"必要的表达能力。
BatchNorm 的完整计算流程可以概括为以下步骤:
这张图想说明的是:BatchNorm 的核心是"先归一化,再恢复表达能力"。归一化解决了分布不稳定问题,而可学习的参数让网络不会因此丢失必要的表达空间。
为什么有效
不只是缓解梯度问题,BatchNorm 还做了几件事:
- 允许更大的学习率:因为梯度对输入变化的敏感度降低了
- 减少初始化敏感度:不同初始化下的训练表现更稳定
- 轻微的正则化效果:每个 batch 的统计量都带噪声,相当于一种数据增强
但最后这点不是 BatchNorm 的设计目的,实际正则化效果也不如 Dropout 明显。
踩坑记录
BatchNorm 用起来有几个容易踩的坑:
- Batch 太小:如果 batch size 是 2 或 4,统计量的方差会非常大,训练会抖动。CNN 中 batch size 小于 8 时 BatchNorm 效果会明显下降。
# 小 batch 时考虑用 SyncBatchNorm 或换成 LayerNorm
if batch_size < 8:
model = layer_norm_model # 或者用 GroupNorm
- 推理时统计量错乱:如果模型在训练模式下保存,但推理时忘记切到 eval 模式,仍然会用当前 batch 的统计量,导致结果不稳定且不可复现。
model.train() # 训练模式,统计量会更新
model.eval() # 推理模式,统计量固定
- 多 GPU 同步问题:如果用 DistributedDataParallel,每个 GPU 会算自己的统计量,需要用 SyncBatchNorm 同步:
from torch.nn import SyncBatchNorm
# 把普通 BatchNorm 换成同步版本
model = SyncBatchNorm.convert_sync_batchnorm(model)
- RNN/LSTM 的尴尬:序列模型的每个时间步长度不一,BatchNorm 的维度对不上。这种场景更适合用 LayerNorm。
LayerNorm:给序列模型准备的
LayerNorm 不跨样本算统计量,而是在单个样本内部计算。
def layer_norm(x, gamma, beta, eps=1e-5):
mean = x.mean(dim=-1, keepdim=True) # 对最后一个维度求均值
var = x.var(dim=-1, keepdim=True)
x_norm = (x - mean) / torch.sqrt(var + eps)
return gamma * x_norm + beta
这个特性让 LayerNorm 天然适合变长序列、小 batch 场景,所以在 Transformer、RNN 中几乎成了标配。
下面这张图对比了 BatchNorm 和 LayerNorm 在统计量计算上的关键差异:
简单说:BatchNorm 是"横向"的,跨样本对齐同一通道的分布;LayerNorm 是"纵向"的,在单个样本内部把不同特征拉到同一个量级。
什么时候用 LayerNorm
简单判断:
- 序列数据(NLP、时间序列)优先用 LayerNorm
- 图像数据、固定长度的结构化数据优先用 BatchNorm
- Batch size 很小(小于 8)时考虑 LayerNorm 或 GroupNorm
其他归一化方案
除了上面两个,还有一些针对特定场景的方案。
InstanceNorm
实例归一化,在风格迁移中用得比较多。它对每个样本的每个通道独立归一化,不跨 batch 也不跨通道。风格迁移希望保留风格特征,抑制内容特征,InstanceNorm 正好满足这个需求。
GroupNorm
把通道分成若干组,每组内部归一化。是 LayerNorm 和 BatchNorm 的折中方案:既不依赖 batch size,又能利用通道间的信息。在目标检测、分割任务中常用(这些任务 batch size 通常很小)。
import torch.nn as nn
# 把 64 个通道分成 8 组,每组 8 个通道一起归一化
conv = nn.Conv2d(64, 64, 3, padding=1)
gn = nn.GroupNorm(8, 64) # num_groups, num_channels
WeightNorm
直接对权重进行归一化,而不是对输入。计算量更小,但在实践中用得不多,主要是 BatchNorm/GroupNorm 已经能覆盖大多数场景。
实践建议
最后总结一下不同场景下的选型建议。
图像分类/CNN
- Batch size 大于 8:用 BatchNorm
- Batch size 小于 8:用 GroupNorm 或 SyncBatchNorm
- 推理对延迟敏感:考虑 GroupNorm(没有统计量更新的开销)
NLP/Transformer
- 首选 LayerNorm,几乎都是这个
- Batch size 很大且训练非常稳定时可以试试 BatchNorm,但收益不明显
目标检测/分割
- Batch size 通常很小(2-4),GroupNorm 是主流选择
- Mask R-CNN、Detectron2 等框架默认都是 GroupNorm
实时/在线推理
- 推理时统计量固定,BatchNorm 和 GroupNorm 差不多
- 如果内存/延迟敏感,可以先把 BatchNorm 的统计量"固化"成固定计算:
# 把 BatchNorm 固化成普通算子
model = torch.jit.trace(model, example_input)
如果不确定该选哪种,可以参考下面的决策树:
这次解决了什么
回到开头的问题,模型训练崩溃的直接原因是特征量级差异导致梯度不平衡。
解决方案分两层:
- 数据层面:输入特征做 Min-Max 或 Z-Score 归一化,保证所有特征在相似的量级范围内
- 网络层面:用 BatchNorm/GroupNorm 缓解内部协变量偏移,允许更大的学习率和更稳定的训练
实际改完后,同样的学习率下 loss 曲线不再突然爆炸,收敛速度也快了大概 20%。更重要的是,不同初始化、不同 batch size 下训练结果的方差明显变小了。
归一化不是一个"神奇技巧",但它确实把很多本来会崩掉的训练救回来了。如果你也遇到过 loss 突然 NaN、梯度爆炸、调学习率像调参彩票这些问题,不妨先检查一下归一化是否到位——很多时候答案就藏在这里。
参考与延伸
- BatchNorm 原始论文:Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift
- LayerNorm 原始论文:Layer Normalization
- GroupNorm 论文:Group Normalization
- PyTorch 归一化层文档:torch.nn - Normalization Layers
版权声明: 本文首发于 指尖魔法屋-关于AI归一化的几点记录(https://blog.thinkmoon.cn/post/338-ai-normalization-data-training-stable-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。