import torch
import torch.nn as nn
import torch.nn.functional as F
from torch import Tensor
print('PyTorch version:', torch.__version__)PyTorch version: 2.12.1+cpu
jshn9515
2026-06-29
2026-06-29
上一节介绍了 Group Normalization。到目前为止,我们已经看到,BatchNorm、LayerNorm、InstanceNorm 和 GroupNorm 虽然使用场景不同,但都包含两个基本步骤:
其中,LayerNorm 已经成为 Transformer 中最经典的归一化方法。对于每个 token 的隐藏向量,LayerNorm 会先减去特征均值,再根据特征方差调整整体尺度。
不过,均值中心化是否一定是必要的?
Root Mean Square Normalization (RMSNorm) (Zhang and Sennrich 2019) 给出了一种更简单的答案:它不再减去均值,而是只根据特征的均方根调整向量尺度。
近年来,RMSNorm 已经成为大语言模型中非常常见的归一化方法。它和 LayerNorm 一样,不依赖 batch 中的其他样本,也不需要维护运行时统计量,但计算形式更加简单。
这一节将回答以下问题:
nn.RMSNorm(hidden_size) 对哪些维度进行归一化?PyTorch version: 2.12.1+cpu
对于一个长度为 \(D\) 的向量:
\[ x=[x_1,x_2,\ldots,x_D] \]
它的均方根定义为:
\[ \operatorname{RMS}(x) = \sqrt{\frac{1}{D}\sum_{i=1}^{D}x_i^2} \]
它和标准差看起来很相似,但两者并不相同。
标准差会先减去均值:
\[ \sigma(x) = \sqrt{\frac{1}{D}\sum_{i=1}^{D}(x_i-\mu)^2} \]
而均方根直接根据原始数值的平方计算整体尺度。
Mean: 2.5
Standard deviation: 1.1180340051651
Root mean square: 2.7386128902435303
如果一个向量的均值不是 0,那么它的 RMS 通常会大于标准差。两者满足:
\[ \operatorname{RMS}(x)^2 = \operatorname{Var}(x)+\mu^2 \]
RMS squared: 7.500000476837158
Variance + mean squared: 7.5
Difference: 4.76837158203125e-07
这说明,RMS 同时包含了向量的波动和整体偏移信息,而标准差只描述围绕均值的波动。
RMSNorm 首先根据最后一个或最后若干个特征维度计算均方根:
\[ \operatorname{RMS}(x) = \sqrt{\frac{1}{D}\sum_{i=1}^{D}x_i^2+\epsilon} \]
然后用它调整特征向量的尺度:
\[ \hat{x}_i = \frac{x_i}{\operatorname{RMS}(x)} \]
最后乘上可学习的缩放参数:
\[ y_i = \gamma_i\hat{x}_i \]
完整写成:
\[ \operatorname{RMSNorm}(x) = \frac{x}{\sqrt{\frac{1}{D}\sum_{i=1}^{D}x_i^2+\epsilon}} \odot\gamma \]
和 LayerNorm 相比,这里没有减去均值,也通常没有可学习的 bias。
RMS: 2.738614559173584
Normalized values: tensor([0.3651, 0.7303, 1.0954, 1.4606])
RMS after normalization: 0.9999993443489075
Mean after normalization: 0.9128703474998474
归一化之后,向量的 RMS 接近 1,但均值不一定为 0。这正是 RMSNorm 和 LayerNorm 最直接的区别:
对于同一个向量,LayerNorm 计算:
\[ \operatorname{LayerNorm}(x) = \frac{x-\mu}{\sqrt{\frac{1}{D}\sum_{i=1}^{D}(x_i-\mu)^2+\epsilon}} \odot\gamma+\beta \]
RMSNorm 计算:
\[ \operatorname{RMSNorm}(x) = \frac{x}{\sqrt{\frac{1}{D}\sum_{i=1}^{D}x_i^2+\epsilon}} \odot\gamma \]
可以直接比较两者的输出:
x = torch.tensor([[1.0, 2.0, 3.0, 4.0]])
layer_norm = nn.LayerNorm(4, elementwise_affine=False)
rms_norm = nn.RMSNorm(4, elementwise_affine=False)
ln_output = layer_norm(x)
rms_output = rms_norm(x)
print('LayerNorm output:', ln_output)
print('LayerNorm mean:', ln_output.mean(dim=-1))
print('LayerNorm RMS:', ln_output.square().mean(dim=-1).sqrt())
print('RMSNorm output:', rms_output)
print('RMSNorm mean:', rms_output.mean(dim=-1))
print('RMSNorm RMS:', rms_output.square().mean(dim=-1).sqrt())LayerNorm output: tensor([[-1.3416, -0.4472, 0.4472, 1.3416]])
LayerNorm mean: tensor([0.])
LayerNorm RMS: tensor([1.0000])
RMSNorm output: tensor([[0.3651, 0.7303, 1.0954, 1.4606]])
RMSNorm mean: tensor([0.9129])
RMSNorm RMS: tensor([1.])
LayerNorm 输出的均值接近 0,方差接近 1;RMSNorm 输出的 RMS 接近 1,但均值仍然保留。
注意,我们不应该把 RMSNorm 简单理解成少了一个减法的 LayerNorm。它们对表示施加的约束不同:
RMSNorm 对正比例缩放具有近似不变性。假设:
\[ x' = a x, \quad a > 0 \]
那么:
\[ \operatorname{RMSNorm}(ax) \approx \operatorname{RMSNorm}(x) \]
Maximum difference after positive scaling: 1.1920928955078125e-07
但是,给所有特征加上相同常数时,RMSNorm 的输出会发生变化:
Maximum difference after shifting: 2.3022541999816895
LayerNorm 因为会减去均值,所以对整体平移近似不变:
normalized_shape 的含义nn.RMSNorm 和 nn.LayerNorm 使用相同的 normalized_shape 约定:
RMSNorm 会对输入最后若干个维度计算均方根,同时将可学习参数 weight 设置为 normalized_shape 对应的形状。
最常见的 Transformer 输入形状是:
\[ (N,L,D) \]
其中,\(N\) 是 batch size,\(L\) 是 sequence length,\(D\) 是 hidden size。
当我们写:
Input shape: torch.Size([2, 4, 8])
Output shape: torch.Size([2, 4, 8])
Normalized shape: (8,)
Weight shape: torch.Size([8])
RMSNorm 会分别处理每个 token 的隐藏向量。
对于每个位置 \((n,l)\):
\[ \operatorname{RMS}_{n,l} = \sqrt{\frac{1}{D}\sum_{d=1}^{D}x_{n,l,d}^2+\epsilon} \]
不同 token 之间不会共享统计量,batch 中不同样本之间也不会共享统计量。
PyTorch 的 nn.RMSNorm 默认包含一个可学习的缩放参数 weight:
Weight: Parameter containing:
tensor([1., 1., 1., 1., 1., 1.], requires_grad=True)
它默认不包含 bias。这和常见的 RMSNorm 定义一致:
\[ y=\hat{x}\odot\gamma \]
由于 RMSNorm 不进行均值中心化,额外加入 bias 并不是其标准定义的一部分。不过,后续线性层通常已经包含偏置或其他可学习变换,因此模型仍然可以调整表示的位置。
如果不需要可学习缩放,可以设置:
RMSNorm 的统计量来自当前 token 或当前样本自身的特征,不依赖 batch 中其他样本。因此它不需要像 BatchNorm 一样维护:
running_mean;running_var;num_batches_tracked。{'weight': tensor([1., 1., 1., 1., 1., 1.])}
默认情况下,state_dict 中只有 weight。
也因为没有运行时统计量,RMSNorm 在 train() 和 eval() 模式下使用相同的计算方式:
Maximum difference: 0.0
train() 和 eval() 仍然会递归修改模块的 training 属性,但这个属性不会改变 RMSNorm 的前向计算。
一个最小的函数版 RMSNorm 只需要以下步骤:
rsqrt 计算均方根的倒数;def rms_norm(
x: Tensor,
normalized_shape: int | tuple[int, ...],
weight: Tensor | None = None,
eps: float | None = None,
) -> Tensor:
"""Apply root mean square normalization to an input tensor."""
if isinstance(normalized_shape, int):
normalized_shape = (normalized_shape,)
if x.shape[-len(normalized_shape) :] != normalized_shape:
raise AssertionError(
f'Expected the trailing input dimensions to match '
f'`normalized_shape={normalized_shape}`, '
f'but got input shape {tuple(x.shape)}.'
)
if eps is None:
eps = torch.finfo(x.dtype).eps
# Normalize over the trailing dimensions specified by normalized_shape.
reduce_dims = tuple(range(x.ndim - len(normalized_shape), x.ndim))
mean_square = x.square().mean(dim=reduce_dims, keepdim=True)
y = x * (mean_square + eps).rsqrt()
if weight is not None:
# (..., normalized_shape) -> (1, ..., 1, normalized_shape)
broadcast_shape = (1,) * (x.ndim - len(normalized_shape)) + normalized_shape
y = y * weight.reshape(broadcast_shape)
return y和 F.rms_norm 对比:
Maximum difference: 0.0
这里使用 torch.rsqrt,是因为:
\[ \operatorname{rsqrt}(z)=\frac{1}{\sqrt{z}} \]
因此可以直接计算归一化所需的倒数平方根。
下面把函数包装成一个模块:
class RMSNorm(nn.Module):
"""Apply root mean square normalization over the trailing input dimensions."""
weight: Tensor | None
def __init__(
self,
normalized_shape: int | tuple[int, ...],
eps: float | None = None,
elementwise_affine: bool = True,
):
"""Initialize a root mean square normalization module"""
super().__init__()
if isinstance(normalized_shape, int):
normalized_shape = (normalized_shape,)
self.normalized_shape = normalized_shape
self.eps = eps
self.elementwise_affine = elementwise_affine
if elementwise_affine:
self.weight = nn.Parameter(torch.empty(self.normalized_shape))
else:
self.register_parameter('weight', None)
self.reset_parameters()
def reset_parameters(self) -> None:
if self.weight is not None:
nn.init.ones_(self.weight)
def forward(self, x: Tensor) -> Tensor:
return rms_norm(x, self.normalized_shape, weight=self.weight, eps=self.eps)
def extra_repr(self) -> str:
return (
f'normalized_shape={self.normalized_shape}, eps={self.eps}, '
f'elementwise_affine={self.elementwise_affine}'
)验证自定义实现:
Maximum difference: 0.0
这个实现保留了 RMSNorm 最核心的行为,但实际 PyTorch 实现还会处理更多设备、dtype 和性能优化细节。
在现代大语言模型中,RMSNorm 已经成为非常常见的归一化方法。早期 Transformer 更普遍地使用 LayerNorm,而随着模型规模、序列长度和训练成本不断增加,研究者开始更加关注归一化层本身的计算复杂度、数值稳定性以及它与高效实现之间的关系。
RMSNorm 保留了 LayerNorm 中最关键的尺度归一化作用,但省略了均值中心化步骤,因此计算路径更短、形式更简单。与此同时,它仍然像 LayerNorm 一样只依赖当前 token 或当前样本内部的隐藏特征,不依赖 batch 中的其他样本,也不需要维护运行时统计量。这使它很适合大语言模型常见的训练与推理场景,例如变长序列、梯度累积、小 batch 训练和逐 token 解码。
因此,关于 RMSNorm 在现代大语言模型中流行,我们可以从以下几个角度理解。
不过,不能由此得出 RMSNorm 永远优于 LayerNorm 的结论。归一化方式仍然是模型架构设计的一部分:
RMSNorm 的核心公式是:
\[ \operatorname{RMSNorm}(x) = \frac{x}{\sqrt{\operatorname{mean}(x^2)+\epsilon}} \odot\gamma \]
这一节最重要的结论包括:
normalized_shape 表示被归一化的最后若干个维度,也是 weight 的形状;train() 和 eval() 模式下的计算相同;下一节,我们将把 BatchNorm、LayerNorm、InstanceNorm、GroupNorm 和 RMSNorm 放进同一个框架,统一比较它们的统计维度、参数化方式和适用场景。