Batch Norm, Layer Norm, Instance Norm, Group Norm

自 Batch Normalization 从 2015 年被 Google 提出来之后,又诞生了很多 Normalization 方法,如 Layer Normalization、Instance Normalization、Group Normalization。这些方法作用、效果各不相同,但却有着统一的内核和本质:计算输入数据在某些维度上的方差和均值,归一化,最后用可学习参数映射归一化后的特征。这可以统一表达为:

我们以图像数据为例子,给定输入数据 ,其中 分别为 batch size, 通道数,图像高和宽。

BN、LN、IN、GN 四种归一化方法计算统计量的维度对比
蓝色部分为同一组均值和方差所覆盖的范围

如上图所示,BN 计算在 维度上的均值方差,LN 计算在 维度上的均值方差,IN 计算在 维度上的均值方差,GN 计算在 维度上的均值方差,其中 是分组后每组的通道个数。

统计量在哪些维度上计算
G
样本 1H×WC样本 2样本 3样本 4样本 5

第 1 / 6 组通道 1,跨全部样本

归约维度
N, H, W
统计量组数
6 C
每组元素
24 N×H×W
均值形状
(1, C, 1, 1)
依赖 batch size
是
batch size N
4

切换 BN / LN / IN / GN,高亮的格子共享同一组均值和方差;用 ‹ › 或悬停、点按格子查看其他组,改变 N 看看谁依赖 batch size。

计算维度的不同是这些方法的唯一区别。也正是因为计算维度的不同,也导致了不同的效果和特性。

  • BN 计算依赖 ,因此当 batch size 较小时效果可能并不理想,且 batch size 对结果影响较大。另外,BN 只在训练时使用当前 batch 的统计量,推理时改用训练阶段累计的滑动平均(running mean / var)。
  • IN 计算依赖 ,不依赖 batch size,相当于计算每个单独的 instance 不同通道的特征,IN 也因此常用于风格转换。
  • LN 计算依赖 ,舍弃了对 batch size 的依赖,因此常用在 batch size 变化的模型中,如 RNN,以及如今的 Transformer。另外 LN 与 BN 和 IN 不同的是,BN 和 IN 整个通道用的是同一个标量进行映射,而 LN 通道内每一个元素都采用不同的标量进行映射。因此前者可学习参数的形状为 ,而 LN 可学习参数的形状为 。
  • GN 首先将通道分组 (其中 ),计算依赖 ,显然不依赖 batch size。而将特征分组,有点类似将类似特征归一化(比如形状、亮度和纹理等),实验证明 GN 效果很好。而且事实上,GN 可以看作是 IN 和 LN 的中间体:当分组个数等于 1 时相当于不分组,是计算 上的均值方差,而当分组个数等于通道个数时(),相当于计算在 上的均值方差,于是退化成了 IN。

我们可以很轻松地用 PyTorch 实现每个方法的等效版本,下面的代码都与官方实现做了数值对比。先看 BN,在 上计算统计量,可学习参数形状为 :

import torch
from torch import nn

inputs = torch.randn(5, 256, 32, 32)  # (N, C, H, W)
bn = nn.BatchNorm2d(256)  # Weight shape: (C, )

# weight default is 1
bn.weight.data = torch.rand_like(bn.weight)

# compute on (N, H, W)
var, mean = torch.var_mean(inputs, dim=(0, 2, 3), keepdim=True, correction=0)  # (1, C, 1, 1)
std = (var + bn.eps).sqrt()  # (1, C, 1, 1)
norm = (inputs - mean) / std  # (N, C, H, W)

print(torch.allclose(
    norm * bn.weight.view(1, 256, 1, 1) + bn.bias.view(1, 256, 1, 1),
    bn(inputs))
)  # True

IN 只在 上计算,统计量的形状变成了 :

inputs = torch.randn(5, 256, 32, 32)  # (N, C, H, W)
ins = nn.InstanceNorm2d(256, affine=True)  # Weight shape: (C, )

# weight default is 1
ins.weight.data = torch.rand_like(ins.weight)

# compute on (H, W)
var, mean = torch.var_mean(inputs, dim=(2, 3), keepdim=True, correction=0)  # (N, C, 1, 1)
std = (var + ins.eps).sqrt()  # (N, C, 1, 1)
norm = (inputs - mean) / std  # (N, C, H, W)

print(torch.allclose(
    norm * ins.weight.view(1, 256, 1, 1) + ins.bias.view(1, 256, 1, 1),
    ins(inputs))
)  # True

LN 在 上计算,可学习参数与输入的后三维同形,所以直接逐元素相乘即可:

inputs = torch.randn(5, 256, 32, 32)  # (N, C, H, W)
normalized_shape = inputs.shape[1:]  # Normalize on (C, H, W)
ln = nn.LayerNorm(normalized_shape)

# weight default is 1
ln.weight.data = torch.rand_like(ln.weight)

# compute on (C, H, W)
var, mean = torch.var_mean(inputs, dim=(1, 2, 3), keepdim=True, correction=0)  # (N, 1, 1, 1)
std = (var + ln.eps).sqrt()  # (N, 1, 1, 1)
norm = (inputs - mean) / std  # (N, C, H, W)

print(torch.allclose(norm * ln.weight + ln.bias, ln(inputs)))  # True

GN 需要先把通道拆成 组,再在 上计算:

inputs = torch.randn(5, 256, 32, 32)  # (N, C, H, W)
num_groups = 32
gn = nn.GroupNorm(num_groups=num_groups, num_channels=256)  # Weight shape: (C, )

# weight default is 1
gn.weight.data = torch.rand_like(gn.weight)

grouped_inputs = inputs.view(5, num_groups, 256 // num_groups, 32, 32)  # (N, G, C', H, W)

# compute on (C', H, W)
var, mean = torch.var_mean(grouped_inputs, dim=(2, 3, 4), keepdim=True, correction=0)  # (N, G, 1, 1, 1)
std = (var + gn.eps).sqrt()  # (N, G, 1, 1, 1)
norm = (grouped_inputs - mean) / std  # (N, G, C', H, W)

print(torch.allclose(
    norm.view(5, 256, 32, 32) * gn.weight.view(1, 256, 1, 1) + gn.bias.view(1, 256, 1, 1),
    gn(inputs))
)  # True

四段代码的骨架完全一致,区别只在于对哪些维度求均值和方差。