0. Abstract
很早以前就遇到了 BatchNorm
和 LayerNorm
, 当时只是粗略地知道它们是对数据进行了标准化:
x
=
x
−
μ
σ
\bm{x} = \frac{\bm{x} - \bm{\mu}}{\bm{\sigma}}
x=σx−μ 这当然很简单, 但实际的数据是比较复杂的. 对于 CV 任务的数据 image 而言, 一个 batch 的数据通常是这样的: [B, C, H, W]
; 对于 NLP 任务的数据 sentence 而言, 一个 batch 的数据通常是这样的: [B, T, D]
. 那 XXNorm
是怎么算的? 肯定不是一下算整个 batch 的所有数的
μ
\bm{\mu}
μ 和
σ
\bm{\sigma}
σ, 且它们是有区别的.
于是查阅了很多资料, 真是应了导师的话: 别去看那些乱七八糟的博客, 他们水平还不如你呢! 还是老外厉害, 仅仅是在评论区回答问题就把问题讲得清清楚楚: 《Why do transformers use layer norm instead of batch norm?》
1. 如何计算?
本节从理论和代码两个角度详细说明 BatchNorm
和 LayerNorm
的计算方式.
1.1 先从理论上说一下, 询问通义千问
>>>>> 通义千问 >>>>>
问: layer norm 和 batch norm 的区别? 给出公式解释.
答: Layer Normalization (LayerNorm) 和 Batch Normalization (BatchNorm) 都是用于加速神经网络训练的技术, 通过规范化层的输入或激活值, 使网络的学习更加稳定和高效. 但是, 它们在实施方式和适用场景上有所不同.
Batch Normalization (BatchNorm)
BatchNorm
主要在训练期间对一个 mini-batch 的数据进行规范化. 它的主要目标是减少内部协变量移位(internal covariate shift), 即在网络的前向传播过程中, 中间层的输入分布随训练迭代而变化的现象. BatchNorm
通过计算当前 mini-batch 的均值和方差来规范化输入, 公式如下:
设
x
\bm{x}
x 是一个 mini-batch 内的输入向量,
μ
B
\bm{\mu}_B
μB 和
σ
B
2
\bm{\sigma}^2_B
σB2 分别是该 mini-batch 的均值和方差, 则 BatchNorm
的输出
y
\bm{y}
y 可以表示为:
x
^
=
x
−
μ
B
μ
B
2
+
ϵ
y
^
=
γ
x
^
+
β
\begin{aligned} \hat{\bm{x}} &= \frac{\bm{x} - \bm{\mu}_B}{\sqrt{\bm{\mu}_B^2 + \epsilon}} \\ \hat{\bm{y}} &= \gamma\hat{\bm{x}} + \beta \end{aligned}
x^y^=μB2+ϵx−μB=γx^+β 其中,
γ
\gamma
γ 和
β
\beta
β 是可学习的参数,
ϵ
\epsilon
ϵ 是一个很小的常数, 用于数值稳定性.
Layer Normalization (LayerNorm)
LayerNorm
目的是在每个样本级别上进行规范化, 而不依赖于 mini-batch 的统计信息. LayerNorm
更适用于序列模型(如 LSTM
或 Transformer
)等, 其中样本间可能存在依赖关系或 mini-batch 大小可能不是固定的. LayerNorm
计算每个样本特征的均值和方差, 公式如下:
设
x
\bm{x}
x 是单个样本的特征向量,
μ
L
\bm{\mu}_L
μL 和
σ
L
2
\bm{\sigma}^2_L
σL2 分别是该样本特征的均值和方差, 则 LayerNorm
的输出
y
\bm{y}
y 可以表示为:
x
^
=
x
−
μ
L
μ
L
2
+
ϵ
y
^
=
γ
x
^
+
β
\begin{aligned} \hat{\bm{x}} &= \frac{\bm{x} - \bm{\mu}_L}{\sqrt{\bm{\mu}_L^2 + \epsilon}} \\ \hat{\bm{y}} &= \gamma\hat{\bm{x}} + \beta \end{aligned}
x^y^=μL2+ϵx−μL=γx^+β 这里同样,
γ
\gamma
γ 和
β
\beta
β 是可学习的参数,
ϵ
\epsilon
ϵ 是一个很小的常数.
区别
- 依赖于数据:
BatchNorm
依赖于 mini-batch 的统计信息, 而LayerNorm
依赖于单个样本的特征统计信息; - 应用场景:
BatchNorm
更适用于计算机视觉任务, 而LayerNorm
更适用于自然语言处理任务, 尤其是在处理序列数据时; - 计算效率:
BatchNorm
在并行计算上可能更高效, 因为它可以利用 mini-batch 内的并行性, 而LayerNorm
在每个样本上的计算独立, 更适合处理不同长度的序列数据.
这两种方法各有优劣, 选择哪种方法通常取决于具体的应用场景和数据特性.
<<<<< 通义千问 <<<<<
清楚了吗? 迷迷糊糊, 什么叫做 “当前 mini-batch 的均值和方差”? 什么叫做 “每个样本特征的均值和方差”? 还是老外回答的好:
先搞清基本概念:
- instance: 一个样本, image for CV, sentence for NLP;
- element: 样本的组件, pixel for image, word for sentence;
- feature: element 的特征, RGB 值 for pexel, embedding for word.
体现到数据上就是:
[B, C, H, W]
是一个 batch,[C, H, W]
是一个 instance,[C]
是一个 pixel, 包含了C
个 feature.[B, T, L]
是一个 batch,[T, L]
是一个 instance,[L]
是一个 word, 包含了L
个 feature.
如下图:
从 Batch Dimension 那一侧看, 每个小方格往背后延申代表一个 element, 如左图中的紫色长条, 一个 pixel 的 RGB 特征, 或者一个词向量. LayerNorm
给每个 element 计算均值和方差, 可得 BxL
个均值和方差(或BxHxW
个). 然后各 element 独立地进行标准化. 右图中的紫色片是一个 feature, 批次中所有 word 的第一个 feature. 每一个这样的片是一个特征, BatchNorm
给每个 feature 计算均值和方差, 可得 L
个均值和方差(或C
个). 然后各 feature 独立地进行标准化.
需要注意的是, Transformer 中并不是按上面所说的 LayerNorm
计算的, 而是给每个 instance 计算均值和方差, 可得 B
个均值和方差, 然后各 instance 独立地进行标准化. 确切说是下图的样子:
1.2 PyTorch 中的 BatchNorm
和 LayerNorm
1.2.1 BatchNorm
在 PyTorch 中, BatchNorm
分 nn.BatchNorm1d
, nn.BatchNorm2d
和 nn.BatchNorm3d
, 分别针对不同维度的数据:
nn.BatchNorm1d
:(N, C)
or(N, C, L)
nn.BatchNorm2d
:(N, C, H, W)
nn.BatchNorm3d
:(N, C, D, H, W)
查看源码:
class BatchNorm1d(_BatchNorm):
r"""
Args:
num_features: number of features or channels `C` of the input
Shape:
- Input: `(N, C)` or `(N, C, L)`, where `N` is the batch size,
`C` is the number of features or channels, and `L` is the sequence length
- Output: `(N, C)` or `(N, C, L)` (same shape as input)
"""
def _check_input_dim(self, input):
if input.dim() != 2 and input.dim() != 3:
raise ValueError(f"expected 2D or 3D input (got {input.dim()}D input)")
Examples:
>>> m = nn.BatchNorm1d(100) # C=100 # With Learnable Parameters
>>> m = nn.BatchNorm1d(100, affine=False) # Without Learnable Parameters
>>> input = torch.randn(20, 100) # (N, C)
>>> output = m(input)
>>> # 或者
>>> input = torch.randn(20, 100, 30) # (N, C, L)
>>> output = m(input)
γ
,
β
\bm{\gamma}, \bm{\beta}
γ,β 是可学习的参数, 且 shape=(C,)
, 参数名是 .weight
和 .bias
:
>>> m = nn.BatchNorm1d(100)
>>> m.weight
Parameter containing:
tensor([1., 1., ..., 1.], requires_grad=True)
>>> m.weight.shape
torch.Size([100])
>>> m.bias
Parameter containing:
tensor([0., 0., ..., 0.], requires_grad=True)
BatchNorm2d
和 BatchNorm3d
是一样的, 不同之处在于 _check_input_dim(input)
:
class BatchNorm2d(_BatchNorm):
r"""
Args:
num_features: `C` from an expected input of size `(N, C, H, W)`
Shape:
- Input: :math:`(N, C, H, W)`
- Output: :math:`(N, C, H, W)` (same shape as input)
"""
def _check_input_dim(self, input):
if input.dim() != 4:
raise ValueError(f"expected 4D input (got {input.dim()}D input)")
Examples:
>>> m = nn.BatchNorm2d(100)
>>> input = torch.randn(20, 100, 35, 45)
>>> output = m(input)
class BatchNorm3d(_BatchNorm):
r"""
Args:
num_features: `C` from an expected input of size `(N, C, D, H, W)`
Shape:
- Input: :math:`(N, C, D, H, W)`
- Output: :math:`(N, C, D, H, W)` (same shape as input)
"""
def _check_input_dim(self, input):
if input.dim() != 5:
raise ValueError(f"expected 5D input (got {input.dim()}D input)")
Examples:
>>> m = nn.BatchNorm3d(100)
>>> input = torch.randn(20, 100, 35, 45, 10)
>>> output = m(input)
1.2.2 LayerNorm
不同于 BatchNorm(num_features)
, LayerNorm(normalized_shape)
的参数是 input.shape
的后 x
个 dim
, 如 [B, T, L]
的后两维 [T, L]
, 则每个句子会被独立地标准化; 若 L
或 [L]
, 则每个词向量被独立地标准化.
NLP Example
>>> batch, sentence_length, embedding_dim = 20, 5, 10
>>> embedding = torch.randn(batch, sentence_length, embedding_dim)
>>> layer_norm = nn.LayerNorm(embedding_dim)
>>> layer_norm(embedding) # Activate module
Image Example
>>> N, C, H, W = 20, 5, 10, 10
>>> input = torch.randn(N, C, H, W)
>>> # Normalize over the last three dimensions (i.e. the channel and spatial dimensions)
>>> layer_norm = nn.LayerNorm([C, H, W])
>>> output = layer_norm(input)
也就是说, 它不仅仅包含了上面理论计算所说的 “各 element 独立地进行标准化” 和 “各 instance 独立地进行标准化”, 而且可以计算任何的 normalize over the last x dimensions.
1.3 计算过程考察
import torch
from torch import nn
# >>> 手动计算 BatchNorm2d >>>
weight = torch.ones([1, 3, 1, 1])
bias = torch.zeros([1, 3, 1, 1])
x = 10 * torch.randn(2, 3, 4, 4) + 100
mean = x.mean(dim=[0, 2, 3], keepdim=True)
std = x.std(dim=[0, 2, 3], keepdim=True, unbiased=False)
print(x)
print(mean)
print(std)
y = (x - mean) / std
y = y * weight + bias
print(y)
# <<< 手动计算 BatchNorm2d <<<
# >>> nn.BatchNorm2d >>>
bnm2 = nn.BatchNorm2d(3)
z = bnm2(x)
print(z)
# <<< nn.BatchNorm2d <<<
print(torch.norm(z - y, p=1))
会发现手动计算和 nn.BatchNorm
计算的几乎完全一致, 可能是有一些
ϵ
\epsilon
ϵ 的影响吧. 注意, 这里的 unbiased=False
有讲究, 官方文档有说明:
"""
At train time in the forward pass, the standard-deviation is calculated via the biased estimator,
equivalent to `torch.var(input, unbiased=False)`.
However, the value stored in the moving average of the standard-deviation is calculated via
the unbiased estimator, equivalent to `torch.var(input, unbiased=True)`.
Also by default, during training this layer keeps running estimates of its computed mean and
variance, which are then used for normalization during evaluation.
"""
这里只是想验证计算过程, 不重点关注 unbiased
. 简单地提一下:
- training 阶段计算的是方差的有偏估计, 而存在方差的 moving average 中的方差是无偏估计;
- during training, 会保存 mean 和 var 的滑动平均, 然后用于 testing 阶段.