摘要

RMSNorm 的主要作用是稳定 Transformer 中各层输入向量的整体尺度。可以把它类比成电路里的“稳压器”:前面传来的数值可能忽大忽小,而 RMSNorm 会在共同归一化后由逐维权重再调尺度,从而让模型各层的计算更加稳定。

文本到向量的变化

先复习下文本是怎么变成向量的,懂的可以直接跳

先看一个中文句子: 乒乓球拍卖完了。

如果我们要通过某种方法把这段文字转换成可计算的数学形式,第一步要把它切成一个个小片段,这些小片段叫 token。切分之后每个 token 会到一个词表里查出对应的整数编号。

例如,为了方便理解,我们暂时假设 Tokenizer 把它切成:

[乒乓球,拍卖,完,了,。]⟶[18432,9271,643,352,17].[\texttt{乒乓球},\texttt{拍卖},\texttt{完}, \texttt{了},\texttt{。}] \longrightarrow [18432,9271,643,352,17].
Tokenizer、词表与 token ID 是怎么来的?

原始输入首先只是一个字符串,例如:

"乒乓球拍卖完了。"\texttt{"乒乓球拍卖完了。"}

它会先被送入 Tokenizer(分词器)。Tokenizer 的作用是按照一套固定规则,把字符串切分成若干个 token。

中文在这里有一个很直观的问题:原始字符串本身并没有空格明确告诉我们“词”在哪里结束。

例如:

乒乓球拍卖完了\texttt{乒乓球拍卖完了}

从人类语言的角度,就至少可以出现两种不同的划分:

[乒乓球拍,卖,完,了][\texttt{乒乓球拍},\texttt{卖},\texttt{完},\texttt{了}]

可以理解成“乒乓球拍已经卖完了”;而

[乒乓球,拍卖,完,了][\texttt{乒乓球},\texttt{拍卖},\texttt{完},\texttt{了}]

又可以理解成“乒乓球的拍卖结束了”。

也就是说,同一串连续的汉字并不存在天然写在字符串里的唯一“词语边界”。

不过,真实语言模型的 Tokenizer 并不是先像人一样理解句子含义,再决定应该按照哪个中文词语切分。它会根据训练语料统计出的词表和合并规则处理字符序列:某种连续片段出现得越稳定,就越可能作为一个 token;同一串汉字也可能因为词表不同而得到不同切分。因此,真实的 token 边界不一定等于我们通常理解的中文分词。 因此,真实的 token 边界也不一定等于我们通常理解的“中文分词”。

对于英语,最简单的方法就是按照空格划分,但是实际上Tokenizer不关心字符是空格、标点还是具体字母和字,所以“I love you.”,中可能出现“love”的token,也可能出现“ love”的token(含前导空格)。甚至完整单词,比如“unchanged”,也可能因为“changed”的单独出现频率太高而被分为“un”,“changed”。 所以,token实际上是 Tokenizer 根据自己的词表和切分规则得到的基本符号单位。

Tokenizer 同时拥有一个固定的 词表(vocabulary)。可以把词表简单理解成一张

token⟷整数编号\text{token}\longleftrightarrow\text{整数编号}

的映射表,例如:

乒乓球→18432,拍卖→9271,。→17.\texttt{乒乓球}\rightarrow18432,\qquad \texttt{拍卖}\rightarrow9271,\qquad \texttt{。}\rightarrow17.

因此,分词完成后,每个 token 都会根据词表被替换成对应的整数编号,即 token ID:

[乒乓球,拍卖,完,了,。]⟶[18432,9271,643,352,17].[\texttt{乒乓球},\texttt{拍卖},\texttt{完}, \texttt{了},\texttt{。}] \longrightarrow [18432,9271,643,352,17].

这里的编号只是为了说明数据流而举出的例子,真实编号由具体 Tokenizer 的词表决定。

这些整数本身没有大小或语义上的数值关系,例如编号 \(18432\) 并不意味着它比编号 \(9271\)“更大”或“更重要”,它们本质上只是为了方便寻址的一个索引。

接下来,这些 token ID 会被送入 Embedding 表。Embedding 表可以看作一个很大的矩阵,每个 token ID 对应其中的一行:

token ID⟶Embedding 表中的对应行⟶浮点向量.\text{token ID} \longrightarrow \text{Embedding 表中的对应行} \longrightarrow \text{浮点向量}.

因此,从原始文字到模型真正参与计算的数据,完整的数据流是:

原始字符串→Tokenizer 分词→tokens→词表查询→token IDs→Embedding 查询→浮点向量\boxed{ \text{原始字符串} \rightarrow \text{Tokenizer 分词} \rightarrow \text{tokens} \rightarrow \text{词表查询} \rightarrow \text{token IDs} \rightarrow \text{Embedding 查询} \rightarrow \text{浮点向量} }

也就是说,Tokenizer 负责把“文字”变成“整数索引”,Embedding 再负责把“整数索引”变成神经网络真正能够计算的浮点向量。

现在模型手里是一个1×51\times5的向量:1 个句子,5 个 token。每个 token 有对应的编号,再从 Embedding 表里取出对应编号索引的一个向量。为了说明方便,我们这里把向量长度设成 4,那么这句话的 5 个 token 从 Embedding 表中取出的向量可能长得类似于这样:

tokentoken IDEmbedding 向量
乒乓球18432[ 0.10, 0.25, -0.05, 0.20 ]
拍卖9271[ -0.30, 0.15, 0.40, 0.05 ]
完643[ 0.12, -0.18, 0.22, 0.31 ]
了352[ 0.08, 0.11, -0.14, 0.27 ]
。17[ -0.06, 0.09, 0.03, -0.12 ]

把这 5 个向量按 token 的顺序堆起来,就得到了这个句子的 Embedding 输出:

[0.100.25−0.050.20−0.300.150.400.050.12−0.180.220.310.080.11−0.140.27−0.060.090.03−0.12]\begin{bmatrix} 0.10 & 0.25 & -0.05 & 0.20 \\ -0.30 & 0.15 & 0.40 & 0.05 \\ 0.12 & -0.18 & 0.22 & 0.31 \\ 0.08 & 0.11 & -0.14 & 0.27 \\ -0.06 & 0.09 & 0.03 & -0.12 \end{bmatrix}

如果再把 batch 维度算上,那么它的形状就是

(B,T,D)=(1,5,4).(B,T,D)=(1,5,4).

这里 1 表示一个句子,5 表示 5 个 token,4 表示每个 token 用一个 4 维向量表示。

尺度漂移

经过 Embedding 之后,每个 token 都已经变成了一个浮点向量。接下来,这些向量会被送入 Transformer 的一层层计算中,并在计算过程中不断产生新的中间向量。

随着层数加深,这些向量的数值大小可能发生明显变化。例如某一层中,一个 token 的向量可能是

x=[1,2,3,4],x=[1,2,3,4],

但另一次训练,它可能变成

x′=[10,20,30,40].x'=[10,20,30,40].

虽然两个向量各个维度之间的比例完全相同,但整体尺度相差了 10 倍。这样的尺度变化如果不断累积,会让后续计算面对很不稳定的数值范围。

所以在向量进入后续计算之前,必须先对它的整体尺度进行控制,使不同层、不同 token 的向量不会因为整体数值过大或过小而给后续计算带来额外困难。

注意力层里还要算点积(两个向量逐项相乘再求和)和softmax(把一组数变成概率);前馈层里有激活函数(对每个数单独做非线性映射)。如果展示不知道这些什么意思也没关系,只需要明白这些运算对数值大小都很敏感。如果很多层连续把不同尺度的向量往后传,同一个子层一会儿接收 0.10.1 量级的数,一会儿接收 100100 量级的数,训练会变得不可控,低精度计算的风险也会变大。

所以我们需要把每个向量的整体大小控制住,同时保留内部各维度的相对关系。也就是归一化干的事情。

LayerNorm

LayerNorm 是较早用在 Transformer 里的归一化方法。它对每个 token 的向量分别算两个统计量:均值(平均数)和方差(数值偏离均值的程度)。

还是用 x=[1,2,3,4]x=[1,2,3,4] 当例子。四个数的均值是

μ=1+2+3+44=2.5.\mu=\frac{1+2+3+4}{4}=2.5.

每个数减掉均值:

x−μ=[−1.5,−0.5,0.5,1.5].x-\mu=[-1.5,-0.5,0.5,1.5].

这四个数的方差是

σ2=∑i=1D(xi−μ)2D=(−1.5)2+(−0.5)2+0.52+1.524=1.25,σ≈1.118.\sigma^2=\frac{\sum_{i=1}^{D}{(x_i-\mu)^2}}{D}=\frac{(-1.5)^2+(-0.5)^2+0.5^2+1.5^2}{4}=1.25, \sigma\approx1.118.

然后再让刚才得到的每个数都除以这个标准差σ\sigma:

x−μσ=[−1.51.118,−0.51.118,0.51.118,1.51.118].\frac{x-\mu}{\sigma} = \left[ \frac{-1.5}{1.118}, \frac{-0.5}{1.118}, \frac{0.5}{1.118}, \frac{1.5}{1.118} \right].

得到:

x−μσ≈[−1.342,−0.447,0.447,1.342].\frac{x-\mu}{\sigma} \approx [-1.342,-0.447,0.447,1.342].

现在这个新向量有两个特点:

均值μ=0,方差σ2=1.\text{均值$\mu$}=0, \qquad \text{方差$\sigma^2$}=1.

通过这套步骤

x⟶x−μ⟶x−μσ\boxed{ x \longrightarrow x-\mu \longrightarrow \frac{x-\mu}{\sigma} }

第一步减去均值,负责把整个向量“移到 0 附近”;第二步除以标准差,负责把向量的整体尺度调整到一个固定范围。 这实际上就是标准化,把一组均值、尺度各不相同的数字,转换成一组以 0 为中心、具有统一尺度的数字。

上面其实就是LayerNorm的核心思想了。而LayerNorm完整定义还会加入 γ/β\gamma/\beta 两个可训练的缩放/平移参数,并用 ε\varepsilon 防止除零和数值不稳定。

yi=γixi−μσ2+ε+βi\boxed{ y_i = \gamma_i \frac{x_i-\mu} {\sqrt{\sigma^2+\varepsilon}} + \beta_i }

LayerNorm 完整的数据流是

xi⟶xi−μ⟶xi−μσ2+ε⟶γixi−μσ2+ε+βi\boxed{ x_i \longrightarrow x_i-\mu \longrightarrow \frac{x_i-\mu}{\sqrt{\sigma^2+\varepsilon}} \longrightarrow \gamma_i \frac{x_i-\mu}{\sqrt{\sigma^2+\varepsilon}} +\beta_i }

其中:

  • ε\varepsilon 是一个很小的正数,用来避免分母为 0 或过小时出现数值不稳定,常见形式是 σ2+ε\sqrt{\sigma^2+\varepsilon};
  • γi\gamma_i 是第 ii 个维度的可训练缩放参数;
  • βi\beta_i 是第 ii 个维度的可训练平移参数。

其中 μ\mu、σ2\sigma^2 都是根据当前 token 的向量现场计算出来的,而 γ\gamma 和 β\beta 则是模型参数,会随着训练不断更新。

例如,如果向量长度为 4,那么 LayerNorm 自己还保存着两组长度为 4 的参数:

γ=[γ1,γ2,γ3,γ4],β=[β1,β2,β3,β4].\gamma=[\gamma_1,\gamma_2,\gamma_3,\gamma_4], \qquad \beta=[\beta_1,\beta_2,\beta_3,\beta_4].

在 LayerNorm 刚初始化时,通常有

γ=[1,1,1,1],β=[0,0,0,0].\gamma=[1,1,1,1], \qquad \beta=[0,0,0,0].

因此在训练刚开始时,LayerNorm 的输出基本就是前面算出的标准化结果;之后模型可以通过训练 γ\gamma 和 β\beta,重新调整每一个维度的尺度和位置。

RMSNorm :只缩放,不居中

RMSNorm 的想法更简单:Transformer 子层最需要的是稳定的输入尺度,减均值不一定是必需的。所以它不再强制均值为 0,只保留缩放这一步。

不减均值以后,原来围绕均值算的标准差就不适用了(因为不仅分子中的 x−μ 被去掉了,标准差 σ 本身也是通过 x−μ 计算出来的)。RMSNorm 改用均方根(root mean square,RMS)来衡量尺度:

RMS⁡(x)=1D∑i=1Dxi2.\operatorname{RMS}(x) =\sqrt{\frac{1}{D}\sum_{i=1}^{D}x_i^2}.

平方让正负数都变成正贡献,而取平均得到每个分量平均平方大小,开平方把单位恢复到原来的大小。

直观地理解,也可以认为 RMS 和标准差的形式非常相似,区别主要在于计算“距离”时选择的参考点不同:

  • σ\sigma:以均值 μ\mu 为参考点,衡量各个分量距离 μ\mu 的整体大小,即
  • σ=1D∑i=1D(xi−μ)2.\sigma = \sqrt{\frac{1}{D}\sum_{i=1}^{D}(x_i-\mu)^2}.
  • RMS⁡(x)\operatorname{RMS}(x):以 00 为参考点,衡量各个分量距离 00 的整体大小,即
  • RMS⁡(x)=1D∑i=1D(xi−0)2=1D∑i=1Dxi2.\operatorname{RMS}(x) = \sqrt{\frac{1}{D}\sum_{i=1}^{D}(x_i-0)^2} = \sqrt{\frac{1}{D}\sum_{i=1}^{D}x_i^2}.

所以从公式形式上看,可以直观地把 RMS 理解成:把标准差中作为参考点的均值 μ\mu 换成 00。

对最后一维长度为 DD 的向量 x=[x1,…,xD]x=[x_1,\ldots,x_D],RMSNorm 的完整公式是:

RMSNorm⁡(x)i=wixi1D∑j=1Dxj2+ε,i=1,…,D.\operatorname{RMSNorm}(x)_i =w_i\frac{x_i} {\sqrt{\frac{1}{D}\sum_{j=1}^{D}x_j^2+\varepsilon}}, \qquad i=1,\ldots,D.

这里 ε\varepsilon 是很小的正数,用来避免分母为 0 或过小时数值不稳定,常见形式是 mean⁡(x2)+ε\sqrt{\operatorname{mean}(x^2)+\varepsilon}。w=[w1,…,wD]∈RDw=[w_1,\ldots,w_D]\in\mathbb{R}^D 是参数向量,让模型在训练中自己决定每个维度的重要程度。所有通道共用同一个 RMS 分母,但各自有独立的 wiw_i;RMSNorm 没有 β\beta 偏移参数。

手动算一遍 RMSNorm

继续用 x=[1,2,3,4]x=[1,2,3,4]。它的 RMS 为

RMS⁡(x)=12+22+32+424=7.5≈2.739.\operatorname{RMS}(x) = \sqrt{\frac{1^2+2^2+3^2+4^2}{4}} = \sqrt{7.5} \approx2.739.

因此归一化后的结果为

x^=xRMS⁡(x)=[1,2,3,4]7.5≈[0.365,0.730,1.095,1.461].\widehat{x} = \frac{x}{\operatorname{RMS}(x)} = \frac{[1,2,3,4]}{\sqrt{7.5}} \approx [0.365,0.730,1.095,1.461].

在 w=1w=\mathbf{1}、ε=0\varepsilon=0 的简化条件下,四个坐标共同归一化后仍按 1:2:3:41:2:3:4 分布;完整 RMSNorm 再由逐维权重调整尺度。

两种归一化对比

忽略参数的影响,令 ε=0\varepsilon=0,可训练缩放系数都设为 1,看同一个输入 [1,2,3,4][1,2,3,4] 的结果:

方法输出输出均值输出 RMS居中
原始输入[1,2,3,4][1,2,3,4]2.5002.5002.7392.739否
LayerNorm[−1.342,−0.447,0.447,1.342][-1.342,-0.447,0.447,1.342]0011是
RMSNorm[0.365,0.730,1.095,1.461][0.365,0.730,1.095,1.461]0.9130.91311否

这个表只说明计算上的区别,不说明哪种方法一定更好。LayerNorm 同时控制均值和尺度;RMSNorm 只控制尺度。很多 decoder-only 模型用 RMSNorm 后训练效果没有下降,还省掉了一部分统计计算,所以慢慢流行起来。

RMSNorm 在 Transformer 里的位置

前面我们一直把 RMSNorm 当成一个单独的归一化操作来看,但在真正的 Transformer 中,它并不是独立存在的,而是夹在各个子层之前,负责先把输入向量的尺度整理稳定,再交给后面的计算模块。

现代 decoder-only Transformer 通常采用 pre-norm 结构。以一层 Transformer 为例,数据流大致是:

X⟶RMSNorm⁡(X)⟶Attention⁡⟶与原来的 X 相加.X \longrightarrow \operatorname{RMSNorm}(X) \longrightarrow \operatorname{Attention} \longrightarrow \text{与原来的 }X\text{ 相加}.

这里“与原来的 XX 相加”使用的是 Transformer 中常见的 残差连接(residual connection)。它的具体作用和原理后面会单独介绍,这里暂时只需要把它理解成:“我保留了一点大肠的味道,这样你才知道你吃的是大肠”。

写成公式就是:

X1=X⏟原始 X+Attention⁡(RMSNorm⁡(X))⏟经过 RMSNorm 和 Attention 后的新结果X_1 = \underbrace{X}_{\text{原始 }X} + \underbrace{ \operatorname{Attention} \bigl(\operatorname{RMSNorm}(X)\bigr) }_{\text{经过 RMSNorm 和 Attention 后的新结果}}

得到新的表示 X1X_1 后,还会经过一次类似的过程:

X1⟶RMSNorm⁡(X1)⟶MLP⁡⟶与原来的 X1 相加,X_1 \longrightarrow \operatorname{RMSNorm}(X_1) \longrightarrow \operatorname{MLP} \longrightarrow \text{与原来的 }X_1\text{ 相加},

也就是

X2=X1+MLP⁡(RMSNorm⁡(X1)).X_2 = X_1 + \operatorname{MLP} \bigl(\operatorname{RMSNorm}(X_1)\bigr).

可以看到,只要在进入计算模块前,都会经过RMSNorm,所以无论来的数是小卡拉米还是大罗金仙,都会强行压缩成普通人,以确保每个计算组件的计算尺度都是类似的。