层层逼近,搞懂 Norm 系列到底在解决什么




2019-07-16

blog_main_img

深度学习里的 Norm 家族很热闹:BN、LN、IN、GN、SN,看名字像一串缩写暗号。 但它们的出发点并不玄乎:网络越深,中间层的数据分布越容易乱跑。分布一乱,后面的层就要一边学习任务,一边适应前面层不断变化的输出,训练自然会变慢、变抖,甚至变得难收敛。

先聊一个很朴素的问题:标准不统一

假设你要判断一个学生是否优秀,有几组指标:

文化课成绩
体育测试
身高体重

文化课满分可能是 100,体育可能是另一套分数,身高体重又是完全不同的量纲。

如果直接把这些数混在一起比较,就会很别扭。

一个自然的做法是:把它们拉到相近的尺度里,再比较。

神经网络里也类似。每一层都会把输入变成新的特征。如果这些特征的均值、方差、尺度一直乱飘,后面的层就会很难受。

方差、协方差和分布漂移

单个特征的波动,我们可以用方差描述:

Var(X) = E[(X - μ)²]

如果有多个特征,只看每个特征自己的方差还不够,还要看不同特征之间的关系。

这就会用到协方差:

Cov(X, Y) = E[(X - μx)(Y - μy)]

如果把很多特征两两之间的协方差放在一起,就得到协方差矩阵。

直观地说:

方差:自己和自己的波动
协方差:自己和别人的联动

深层网络训练时,前面层的参数一直在更新,后面层看到的输入分布也会跟着变化。

分布漂移示意

这类分布变化会让优化变得更麻烦。Norm 系列方法的核心想法就是:别让中间层输出太放飞,先拉回一个更稳定的分布,再继续往后传。

Norm 的基本套路

大多数归一化方法都绕不开这几步:

算均值
算方差
标准化
再用可学习参数缩放和平移

公式可以写成:

x_hat = (x - μ) / sqrt(σ² + ε)

然后:

y = γx_hat + β

其中:

  • μ 是均值
  • σ² 是方差
  • ε 用来避免除零
  • γ 是可学习缩放
  • β 是可学习平移

Norm 基本流程

为什么标准化后还要加 γβ

因为完全把数据拉成固定分布,可能会限制模型表达能力。γβ 相当于告诉模型:我先帮你整理一下分布,但你觉得哪里需要放大、缩小、平移,可以自己学回来。

BN:Batch Normalization

BN 是最常见的归一化方法之一。

对于 CNN 输入:

N × C × H × W

BN 通常对每个通道 C 分别统计均值和方差,统计范围覆盖:

N、H、W

也就是说,同一个通道里,整个 batch 的所有空间位置一起参与统计。

PyTorch 用法:

import torch
import torch.nn as nn

bn = nn.BatchNorm2d(num_features=64)

x = torch.randn(8, 64, 32, 32)
y = bn(x)

print(y.shape)

BN 的优点:

  • CNN 里非常常用
  • 能稳定中间层分布
  • 常常能加快训练
  • 对较大的 batch 比较友好

BN 的问题也很明确:

  • batch 太小时,统计量容易不稳定
  • 训练和推理行为不同
  • 分布变化很大的任务里要小心

训练时,BN 使用当前 batch 的均值和方差;推理时,使用训练过程中累计的 running mean 和 running var。

这也是为什么验证或推理时要调用:

model.eval()

LN:Layer Normalization

LN 不依赖 batch 维度。

它通常对单个样本内部的特征做归一化。

如果输入是:

batch × hidden_dim

LN 会对每个样本的 hidden_dim 做统计。

PyTorch 用法:

ln = nn.LayerNorm(normalized_shape=128)

x = torch.randn(32, 128)
y = ln(x)

print(y.shape)

LN 很适合序列模型和 Transformer 类结构,因为这些场景里 batch 大小可能变化,而且不同样本长度、结构差异较大。

LN 的特点:

  • 不依赖 batch 大小
  • 每个样本自己归一化
  • 常用于 NLP 和序列模型
  • 对 CNN 的空间结构利用不如 BN 直接

IN:Instance Normalization

IN 常见于图像生成、风格迁移等任务。

对于输入:

N × C × H × W

IN 对每个样本、每个通道分别统计 H × W 上的均值和方差。

也就是:

每张图,每个通道,自己归一化

PyTorch 用法:

inorm = nn.InstanceNorm2d(num_features=64)

x = torch.randn(8, 64, 32, 32)
y = inorm(x)

print(y.shape)

为什么风格迁移里喜欢 IN?

因为图像风格往往和通道统计量有关。IN 会弱化单张图里的对比度、亮度、风格统计,让模型更关注内容结构。

它的特点:

  • 不依赖 batch
  • 对每张图单独处理
  • 常用于生成任务
  • 可能削弱图像本身的风格信息

GN:Group Normalization

GN 可以看成在 LN 和 IN 之间找了一个折中。

它把通道分成若干组,每组内部做归一化。

对于:

N × C × H × W

如果分成 G 组,每组有 C / G 个通道。GN 会在每个样本内部,对每组的通道和空间位置一起统计。

PyTorch 用法:

gn = nn.GroupNorm(num_groups=8, num_channels=64)

x = torch.randn(4, 64, 32, 32)
y = gn(x)

print(y.shape)

GN 的优势是:不依赖 batch 统计。

所以在目标检测、分割这类显存吃紧、batch 较小的任务里,GN 经常比 BN 更稳。

它的特点:

  • 不依赖 batch 大小
  • 保留一定通道结构
  • 小 batch 场景更友好
  • 需要选择合适的 group 数量

SN:Switchable Normalization

SN 的思路更像“别急着站队,让模型自己选”。

BN、LN、IN 各有优势,那能不能把它们混合起来?

SN 会把多种归一化统计方式组合起来,并给它们学习权重。

可以粗略理解成:

SN = α_BN × BN + α_LN × LN + α_IN × IN

这里的权重不是手写死的,而是让模型学习。

它的目标是:不同任务、不同层,可能适合不同的归一化方式,那就让网络自己决定比例。

这类方法实现会比 BN、LN、IN、GN 更复杂,但思想很直观:

归一化也可以自适应

一张图看区别

Norm 系列区别

简单记法:

BN:看一个 batch
LN:看一个样本的一层
IN:看一张图的一个通道
GN:看一个样本的一组通道
SN:把几种方式混起来学

如果你只记一句话:

Norm 方法的核心差别,是统计均值和方差时,到底沿哪些维度统计。

PyTorch 小实验:看看它们的形状

下面用同一个输入跑几个 Norm:

import torch
import torch.nn as nn

x = torch.randn(4, 8, 16, 16)

layers = {
    "BatchNorm2d": nn.BatchNorm2d(8),
    "InstanceNorm2d": nn.InstanceNorm2d(8),
    "GroupNorm": nn.GroupNorm(num_groups=4, num_channels=8),
}

for name, layer in layers.items():
    y = layer(x)
    print(name, y.shape, y.mean().item(), y.std().item())

它们输出形状都一样:

batch × channel × height × width

但统计均值和方差的维度不一样,所以数值行为也不一样。

Keras 里的常见写法

Keras 中常见的是 BatchNormalization 和 LayerNormalization:

import tensorflow as tf
from tensorflow.keras import layers, models

model = models.Sequential([
    layers.Conv2D(32, 3, padding="same", input_shape=(32, 32, 3)),
    layers.BatchNormalization(),
    layers.ReLU(),
    layers.Conv2D(64, 3, padding="same"),
    layers.BatchNormalization(),
    layers.ReLU(),
    layers.GlobalAveragePooling2D(),
    layers.Dense(10, activation="softmax"),
])

model.summary()

LayerNormalization:

x = layers.Input(shape=(128,))
y = layers.LayerNormalization()(x)
model = tf.keras.Model(x, y)

如果需要 GN、IN 或 SN,通常要使用扩展库,或者自己写自定义层。

自己写一个简单 LayerNorm

用 PyTorch 手写一个简化版 LN:

import torch
import torch.nn as nn


class SimpleLayerNorm(nn.Module):
    def __init__(self, hidden_dim, eps=1e-5):
        super().__init__()
        self.gamma = nn.Parameter(torch.ones(hidden_dim))
        self.beta = nn.Parameter(torch.zeros(hidden_dim))
        self.eps = eps

    def forward(self, x):
        mean = x.mean(dim=-1, keepdim=True)
        var = x.var(dim=-1, keepdim=True, unbiased=False)
        x_hat = (x - mean) / torch.sqrt(var + self.eps)
        return self.gamma * x_hat + self.beta


x = torch.randn(32, 128)
ln = SimpleLayerNorm(128)
y = ln(x)

print(y.shape)

这个小例子能看出 Norm 的本质:选定统计维度,算均值方差,标准化,再缩放平移。

怎么选 Norm

没有一个 Norm 能通吃所有任务,可以按场景先做保守选择。

图像分类、batch 比较正常:

优先试 BN

NLP、Transformer、序列建模:

优先试 LN

图像生成、风格迁移:

可以关注 IN

目标检测、分割、小 batch:

可以试 GN

不确定哪种统计最合适,并且愿意接受更复杂实现:

可以了解 SN

常见误区

Norm 不是简单把数据缩到 0 到 1。

很多 Norm 做的是均值方差标准化,不是 min-max 缩放。

BN 不等于所有场景最优。

batch 太小的时候,BN 的统计量可能不稳,这时 GN 或 LN 可能更舒服。

γβ 不是多余的。

它们让模型在标准化之后还能学回需要的尺度和偏移。

训练和推理模式要分清。

BN 在训练和推理时行为不同,忘记 model.eval() 会让验证结果变得很奇怪。

Norm 不是让模型变聪明的魔法,但它能让训练环境更干净、更稳定。把数据分布收拾利索,后面的优化器才好干活。