transformer论文学习

通过B站视频学习《attention is all you need》论文,终于学会了。

网页2的B站视频很详细,主要看的这个。

网页3的B站视频可以了解一些 transformer 作者们的八卦历史信息。

网页4是另一套视频课程。

背景-RNN与transformer

RNN 有2大缺点:

  1. 容易遗忘早期数据
  2. 顺序计算,不能并行计算

虽然RNN的解码器中也可以加入注意力机制来缓解遗忘早期数据的问题,但是不能并行计算还是无解的,这就导致RNN不能使用GPU加速。

transformer 架构只基于注意力机制,完全抛弃了 RNN 模型,并且可以并行计算。

transformer架构

transformer 论文中的模型架构如下

解码器与编码器

这里这个架构是为了语言翻译任务设置的,有2大块构成

  • 编码器(encoder):图中左边这部分,编码器由 N = 6 个相同的层堆叠而成。每一层包含两个子层,第一个是多头自注意力机制,第二个是一个简单的全连接前馈网络。我们在每个子层周围引入残差连接和层归一化层。也就是说,每个子层的输出为 LayerNorm(x + Sublayer(x)),其中 Sublayer(x) 是该子层自身实现的函数。为了便于进行残差连接,模型中的所有子层以及嵌入层均生成维度为 \(d_{model} = 512\) 的输出。
  • 解码器(decoder):解码器同样由 N = 6 个相同的层堆叠而成。除了每个编码器层中的两个子层外,解码器还插入了一个第三子层,用于对编码器堆栈的输出执行多头注意力机制。与编码器类似,我们在每个子层周围引入残差连接,然后进行层归一化。此外,我们对解码器堆栈中的自注意力子层进行了修改,以防止位置在注意力时关注后续位置。这种掩码机制,加上输出嵌入向量被偏移一个位置的事实,确保了第 i 个位置的预测结果只能依赖于小于 i 的已知输出位置。

注意图中左右两块都有 Nx 的表示,就是说相应的模块顺序连接N个(论文中 N = 6)。之所以可以顺序连接,是因为这里输出和输入的维度相同,均为 512 。

这里其实是由几个重复出现的子模块组成的,下面分别进行介绍

词嵌入和位置编码

输入那一块:在训练数据中,左边的 input 就是要翻译的句子(如“我爱水课”),右边的 ouyput 就是提供好的理想翻译结果(如“I love easy course”),然后对输入数据进行分词(分词 tokenization 就是拆分为 词元 token),然后词嵌入(现在的大模型一般也就是将词嵌入的矩阵也作为训练参数一块训练,这样效果更好),这两步都是常规做法。

但是这种并行化处理的数据没有保存 token 在句子中的位置信息,因此这里创新增加了一个位置编码,对每一个 token 生成一个与 \(d_{model}\) 相同维度(这里是 512)的向量,然后与词嵌入向量直接相加(没错,这里就是两个向量直接相加),通过这种方式注入位置信息。

论文中的位置编码向量(Positional Encoding)是通过下面的正弦余弦函数获得的: \[ \begin{align*} PE_{(pos, 2i)} &= \sin\left(pos / 10000^{2i / d_{\text{model}}}\right) \\ PE_{(pos, 2i+1)} &= \cos\left(pos / 10000^{2i / d_{\text{model}}}\right) \end{align*} \] 其中(公式看懂了,但是没太看懂为什么用这个公式,不过貌似现在用的都是可训练的位置编码了):

  • 分子 \(pos\) :词在句子中的绝对位置(从 0 开始,如第1个词是0,第2个词是1)。
  • \(i\):向量维度的索引(\(i=0,1,...,d/2-1\))。
  • \(2i\) 和 \(2i+1\):公式巧妙地区分了奇偶维度。偶数维(0, 2, 4…)使用 正弦(sin),奇数维(1, 3, 5…)使用 余弦(cos)。这使得不同维度拥有不同的波长。
  • 分母 \(10000^{\frac{2i}{d_{\text{model}}}}\):这是一个指数衰减的基准频率。当 \(i\) 较小时,频率很高(波长很短),数值在维度间变化剧烈;当 \(i\) 较大时,频率很低(波长很长),数值变化平缓。这就像给位置信息打上了不同频率的“波”标签。

位置编码之所以要用这种看上去比较复杂的形式,是因为我们同样要通过位置编码向量的点积来获得不同位置的相对关系,因此不能直接用 0、1、2这种直接编码的方式。

为什么需要位置编码?

疑问:词嵌入本身不就是按照token的顺序得到的输入吗?为什么还需要单独加入位置编码?

因为你“按顺序把 token 放进去”,不代表模型“知道顺序”。Transformer 在计算时,会把你的句子当成一袋没有先后关系的向量。关键原因在于,Self-Attention 是「排列不变友好」的。


回顾一下注意力机制的权重计算方式

1
attn = q @ k.transpose(1, 2)
  • query₁ 和 key₀ 点积
  • query₁ 和 key₁ 点积
  • query₁ 和 key₂ 点积

👉 Attention 只问一个问题:

“我的内容和你的内容有多像?”

它从来不问:

“你是第几个?”

举个具体例子

假设有两个句子,token 相同但顺序不同:

1
2
A = [猫, 追, 老鼠]
B = [老鼠, 追, 猫]

Embedding 后:

  • A 里有向量:x₀=猫, x₁=追, x₂=老鼠
  • B 里有向量:x₀=老鼠, x₁=追, x₂=猫

Attention 矩阵是这样算的:

1
2
3
4
5
6
7
A: 猫·猫  猫·追  猫·老鼠
追·猫 追·追 追·老鼠
老鼠·猫 ...

B: 老鼠·老鼠 老鼠·追 老鼠·猫
追·老鼠 追·追 追·猫
猫·老鼠 ...

你会发现:B 的注意力矩阵,就是把 A 的行和列重新排列了一下。

Transformer 并不知道“猫本来在第一位”,它只看到向量集合 {猫, 追, 老鼠}。

这在数据上叫做 Permutation Equivariance

  • 你把输入 token 的顺序洗牌(shuffle)
  • Attention 的输出也会跟着同样洗牌
  • 模型不会报错,也不会察觉“不对劲”

而 RNN/LSTM 不一样,它们是“一步一步算”的,天然带时间轴。

不加位置编码会怎么样?

  • 模型只能学到:
    • 哪些词经常一起出现(共现统计)
    • 但学不到“语法结构”
  • 比如:
    • “苹果 吃 我” 和 “我 吃 苹果”
    • 模型会认为意思差不多 😂

实验证明:去掉位置编码,BLEU 分数会直接崩掉。位置编码是人为注入的“第几步”信息,否则它就只是个 Bag-of-Words 加强版。

词嵌入和位置编码举例

输入的“我爱水课”和输出的““I love easy course”” 通过词嵌入和位置编码后,每个 token 均会转为一个 512 维度的向量。

这两步的意义为:

  • 词嵌入:获得 token 的语义信息
  • 位置编码:获得位置信息

但是同一个单词其实在不同的上下文语境中的含义其实是不同的。比如“水”这个字,在“水课”和“西瓜水”这两个例子中的含义完全不同,一个表示容易的意思,一个表示一氧化二氢。

因此下面我们通过注意力机制将上下文信息加入到 token 的信息中,下面介绍注意力机制。

多头注意力机制 - 核心

首先看注意力机制,再看多头,其实所谓的多头就是并行跑多个相同结构的注意力模块,最后将所有模块输出结果堆叠成一个结果。

注意力机制

图中的三个箭头对应注意力的3个权重矩阵 \(W_Q\) , \(W_K\), \(W_V\) (这三个矩阵的值也是模型训练中得到的)。这3个矩阵的维度均为 \(512×512\) 。这三个矩阵的下标缩写分别对应查询(Query)、键(Key)、值(Value)。这三个术语借鉴自信息检索系统。当你在谷歌搜索时,搜索引擎会把你的查询(query,即搜索词)与一组键(key,即被索引的网页)进行匹配,然后返回最相关的值(value,即搜索结果)。

更具体地解释这三个组件:

  • Query(查询)——表示我们在序列的每个位置上”正在找什么”。
  • Key(键)——表示序列每个位置上”有什么可能被匹配到的信息”。
  • Value(值)——当查询和键匹配成功时,实际被提取出来的内容。

然后我们用“水”这个词为例,用“水”的词嵌入向量分别与这三个权重矩阵进行矩阵乘法(如 \(\boldsymbol{xW_{q}}\)),得到 3 个 \(1 \times 512\) 维度的向量,分别为 \(\text{水}_q\) ,\(\text{水}_k\), \(\text{水}_v\) 。 当然,这里其实每一个 token 都会得到这3个“分身”向量,合并在一块就得到了 Q,K,V 矩阵。

这里的 q , k, v 怎么理解呢?举个例子,q 是问题,k 是每个token 基于这个问题的关键词,v 其实还是这个token的信息向量。比如,水q 的提问是“你是名词吗?”,那么每个token基于这个问题会给出 yes 或 no 的回答,这就是 k 向量(4个字的答案分别为 no ,no, yes, yes)。然后我们用 q 和 每个 token 的 k 矩阵进行点积,如果是名词,点积的值就越大,反之就越小。然后进行缩放(为了不出现梯度消失),然后采用 softmax 函数转为和为1的值,这样就得到了最终的权重。这个权重就是所有 token 对于当前水这个词的影响的重要程度(也包含水这个词自己本身)。然后得到这个权重之后,再与 V 做矩阵乘法,也就是一个加权平均,也就是

1
水的新向量 = 权重1 * 我v + 权重2 * 爱v + 权重3 * 水v + 权重4 * 课v 

因此,这里有可能最后2个字 “水” 和 “课” 对 水的影响比较大,这就会导致得到的水的全新向量向“课”的反向进行一定的偏转。这个过程就是所谓的综合上下文的信息。

通过这个方式,输出和输入的维度是一样的。输入是4个 1×512 的向量,输出也是4个 1×512 的编码了上下文信息的新向量。

缩放

在Transformer论文《Attention Is All You Need》的经典注意力公式中:

\[ \text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V \]

\(d_k\) 代表的是“键(Key)向量的维度”,也就是你输入序列中每个位置经过线性变换后,所得到的向量的长度(特征数)。

在原始Transformer论文中,整个模型的隐藏层维度设为 \(d_{\text{model}} = 512\)。由于论文使用的是多头注意力(Multi-Head Attention),且默认设置了 \(h=8\) 个头。

为了计算每个头,需要把总维度切分: \[ d_k = d_{\text{model}} / h = 512 / 8 = 64 \]

所以,在原始论文中,\(d_k\) 的数值是 64。这意味着,在每个注意力头里,每个单词的查询(Q)和键(K)向量都是 64 维的浮点数。


为什么要除以 \(\sqrt{d_k}\) ?- 梯度消失

这是面试中最常追问的点。公式中分母有一个 \(\sqrt{d_k}\),而不是直接除以 \(d_k\),这是为了防止梯度消失。

  • 原理:\(Q\) 和 \(K\) 的点积运算本质上是向量的内积。如果两个向量是 64 维,且每个维度都是均值为0、方差为1的数据,那么它们的点积结果的方差就会变成 64(因为方差会累加)。
  • 后果:方差变大意味着点积结果会变得很大(比如几十或几百)。当这些很大的数值输入到后面的 \(\text{softmax}\) 函数中时,会导致 \(\text{softmax}\) 的输出变成一个独热(One-hot)似的极端分布(概率极度集中在某个位置,其他位置几乎为0)。
  • 后果的影响:此时计算出的梯度会无限接近于0,模型参数几乎无法更新(梯度消失)。
  • 解法:除以 \(\sqrt{d_k}\)(即 \(\sqrt{64}=8\)),可以将点积结果的方差缩放回 1,让 \(\text{softmax}\) 的输入保持在梯度敏感的正常范围内(比如 -3 到 3 之间)。

缩放的影响

缩放(除以 \(\sqrt{d_k}\))和不缩放(直接使用 \(QK^T\))计算出的 Softmax 概率分布完全不同。缩放操作本质上是在调整 Softmax 函数的“温度”参数。

为了让你直观地看到区别,我们直接算一笔账:

1. 直观数学对比(假设 \(d_k=64\))

假设在计算某个 Query 和所有 Keys 的点积后,我们得到的原始分数(Logits)是: [10, 0, 0](即第一个词与当前词极度相关,另外两个词毫不相关)。

  • 情况 A:不缩放(直接 Softmax) \[ \text{Softmax}([10, 0, 0]) = \frac{[e^{10}, e^{0}, e^{0}]}{\sum} \] 计算结果约为:[0.9999, 0.00005, 0.00005] -> 分布极度尖锐,几乎成了 One-hot(独热),模型几乎完全忽视另外两个词。

  • 情况 B:缩放(除以 \(\sqrt{d_k} = 8\)) 分数变成:[10/8, 0, 0] = [1.25, 0, 0] \[ \text{Softmax}([1.25, 0, 0]) = \frac{[e^{1.25}, e^{0}, e^{0}]}{\sum} \] 计算结果约为:[0.78, 0.11, 0.11] -> 分布变得平缓,虽然第一个词依然最重要,但另外两个词也保留了超过 20% 的关注度权重。


2. 为什么说“本质不同”?

从数学上看,Softmax 函数对输入具有尺度敏感性。它并不是线性函数,因此:

  • 不缩放:输入值很大(因为 \(Q\) 和 \(K\) 的维度相乘导致方差变大),Softmax 会落入饱和区,输出概率极度极端。
  • 缩放后:输入值被拉回均值为0、方差为1的标准范围,Softmax 处于梯度敏感区,输出概率相对平滑。

如果缩放和不缩放计算出的概率一样,那只有当所有输入分数完全相同(如全为0),或者缩放系数恰好为1时才成立,但这在实际情况中几乎不可能。


3.用“温度”来理解(你的直觉校准)

你甚至可以理解为:不缩放相当于将 Softmax 的“温度参数”设为 \(T=1\),而缩放相当于把温度设为 \(T=\sqrt{d_k}\)(即 8)。

  • 高温(缩放后):概率分布更平滑,模型能够“兼听则明”,关注到上下文中的次要信息。
  • 低温(不缩放):概率分布更尖锐,模型几乎只盯着最相似的那个词看,忽略了其他上下文。

引申:Softmax 的梯度

一、Softmax 定义

对向量 \(\mathbf{z} = (z_1, \dots, z_n)\): \[ p_i = \text{softmax}(z_i) = \frac{e^{z_i}}{\sum_{j=1}^{n} e^{z_j}} \] 记 \(\mathbf{p} = \text{softmax}(\mathbf{z})\),显然 \(p_i > 0\) 且 \(\sum_i p_i = 1\)。

二、Softmax 的梯度(雅可比矩阵)

对 \(p_i\) 关于 \(z_j\) 求偏导: \[ \frac{\partial p_i}{\partial z_j} = \begin{cases} p_i(1 - p_i), & i = j \\ -p_i p_j, & i \neq j \end{cases} \] 这就是 softmax 的逐元素梯度公式。

三、矩阵(雅可比)形式

把所有偏导数排成矩阵 \(\frac{\partial \mathbf{p}}{\partial \mathbf{z}} \in \mathbb{R}^{n \times n}\): \[ \frac{\partial \mathbf{p}}{\partial \mathbf{z}} = \operatorname{diag}(\mathbf{p}) - \mathbf{p} \mathbf{p}^{\top} \] 其中:

  • \(\operatorname{diag}(\mathbf{p})\):把 \(p_i\) 放在对角线上
  • \(\mathbf{p}\mathbf{p}^{\top}\):外积,第 \((i,j)\) 元素是 \(p_i p_j\)

✅ 这个公式在 PyTorch / TensorFlow 的 autograd 内部就是这么用的。

四、梯度大小的行为

  • 当 \(p_i \approx 1\):\(\partial p_i/\partial z_i = p_i(1-p_i) \approx 0\) → 梯度消失
  • 当 \(p_i \approx 0.5\)(且 n=2):\(\partial p_i/\partial z_i = 0.25\) → 梯度最大

这就是为什么当 \(\text{softmax}\) 的输出变成一个独热(One-hot)似的极端分布时,梯度会消失,因此才需要进行缩放。

多头的含义

首先输入是4个 1×512 的向量,但是这个时候的权重矩阵不再是 512×512 ,而是变成了 512×64,因此此时向量和权重矩阵相乘得到的 q,k,v 向量维度是 1×64 ,Q,K,V 矩阵就是 4×64 ,注意力机制的输出也是 4×64 (单个向量维度变成 64)。

这里所谓多头,这里我们有 8 组这样的注意力模块,也就是 8组Q,K,V 权重矩阵,因此我们得到了 8组 4×64 的的结果。最后我们将这8组结果拼接(concat),就又得到了 4×512 的结果。

多头的作用

多头注意力使模型能够在不同位置同时关注来自不同表示子空间的信息。而使用单个注意力头时,平均化会抑制这种能力。

也就是不同的头关注的点不一样,用我们的例子来说,就是用多头时,不同的注意力头能提出多个不一样的问题,比如是不是名词,褒贬义等等。

由于每个头的维度减小,总计算成本与全维度单头注意力相似。也就是单头和多头的计算量差不多(按照数学公式来说是一样的,但是由于要单独训练,我感觉多头的训练计算量要大一点;推理是完全一样的)。

解码器的特殊之处

第一个多头注意力层 - 掩码

解码层第一个多头注意力层多了一个掩码(MASK)的功能,掩码(Mask)是确保模型“不 cheating(作弊)”的核心机制。根据这个掩码的功能,这个掩码又叫做因果掩码(Causal masking)

Transformer的解码器是“自回归(Auto-regressive)”的,其核心约束是:在预测第 t 个位置的输出(即预测下一个词 \(y_{t+1}\))时,绝对不允许使用第 t+1 个及以后(未来)位置的信息。

但在我们之前讲的自注意力机制中,计算某个词时会自动计算它与句子中所有词(包括未来的词)的相关性得分。如果在训练时让解码器看到了完整的目标句子(Ground Truth),那模型就相当于考试时提前看了答案,它学不会真正的推理能力。

掩码怎么做?

就是将初步计算得到的原始注意力权重矩阵 \(QK^T\) 的上三角元素全部改为 -inf ,即负无穷。

这样,这些位置在后续 softmax 时概率会变为 0(因为 exp(-inf) = 0),从而实现屏蔽未来信息。

假如掩码后:位置 0 只能看到自己(不能看到位置 1),位置 1 可以看到自己以及位置 0。这正是自回归生成所需要的“未来不可见”约束。

举个例子,假如总共有4个token,ABCD。在真实世界里(推理时),它是串行的:先有 A,猜出 B;有了 A、B,再猜 C;有了 A、B、C,再猜 D。

通过掩码,我们强行把 ABCD 变成 4 个独立的任务,就像真实实际推理那样。如下表(实际项目应该是有开始符和结束符的,A 正常应该是人为设置的开始符)

时间步 模型看到的上下文(输入 X) 要预测的目标(标签 Y) 对应你例子中的描述
位置 0 [A](只看第1个) B “查看 A 的信息预测 B” ✅
位置 1 [A, B](看前2个) C “查看 AB 的信息预测 C” ✅
位置 2 [A, B, C](看前3个) D (对应你例子中 AB->C,逻辑顺延)
位置 3 [A, B, C, D](看前4个) E(或结束符)

掩码的作用 - 并行

掩码的作用就是为了训练的时候能够并行。

这就是 Transformer 相比 RNN 最革命性的进步。

  • RNN(循环神经网络):必须老老实实先算 A→B,再算 AB→C,再算 ABC→D。串行,无法利用 GPU 并行,速度慢。
  • Transformer(带掩码):一次性把完整的 ABCD 矩阵扔进去,通过掩码矩阵把未来的位置遮住。GPU 可以在同一时刻并行计算出 4 个位置的损失。

核心逻辑:掩码的存在,使得“并行输入”不会导致“信息泄露”,从而实现了 “训练时并行,推理时串行” 的完美解耦。

模型推理时怎么做?

首先,因果掩码的原则在推理过程中必须始终遵守。 但是,在实际的工程实现(标准推理)中,我们通常不需要显式地创建一个下三角掩码矩阵。

在实际的高效推理(如 GPT、LLaMA)中,我们不会每次都把整个历史序列重新输入模型,而是使用 KV 缓存(KV Cache)。

  • 训练时(带掩码): 输入完整序列 [A, B, C, D],计算所有位置对之间的注意力分数(4x4 矩阵),然后用下三角矩阵把右上角全部遮住。

  • 推理时(带 KV 缓存): 模型是一个一个词往外蹦的。当我们要生成第 4 个词 D 时,我们只把最新的词 C 作为 Query(查询)送进去,而 Key 和 Value 则来自之前缓存好的 [A, B, C]。

    关键点来了: 因为此时 Query 只有一个向量(代表当前的 C),而 Key/Value 都是过去的旧词。计算注意力分数时,矩阵形状是 1 × 序列长度(一行 vs 多列)。

    • 这种情况下,没有“未来位置”可以看,因为当前这一步只有一个 Query,它只能看过去的键值。所以,显式的下三角掩码矩阵在数学上完全多余,直接去掉不写就能获得完全相同的效果。

我的理解如下,我们捋一下,按照一般的思路:

  • 输入第一个字符 A ,推理过程中会生成 A 的所有需要 q,k,v 向量信息,预测得到 B ;
  • 第二次输入字符A和B,推理过程中生成 A 和 B 的所有需要 q,k,v 向量信息,预测得到 C。这里有2个细节:
    1. 这里要采用掩码,因此字符 A 的q,k,v 向量信息和第一次推理时完全一样,不会改变,因为掩码的存在或者说不泄露原则,字符A看不到未来字符的信息,其他同理
    2. 字符 A 的q向量没用,因为我们不再需要字符A的下个字符的预测结果了(包括中间结果,中间结果也是第一步已经得到的,只要保存好即可)
  • 第三次输入字符A,B 和 C,预测得到 D 。同理,这里 A和B的q,k,v 向量信息不变,C的q,k,v 向量信息都是这一次新生成得到的。

根据这个推理过程,我们只需要保存历史序列的 k 和 v 信息,就可以快速推理新字符的结果,改进如下

  • 第一次推理同上,保留字符 A 的所有 k, v 向量信息
  • 第二次推理输入字符 B ,生成字符 B 的所有 q,k,v 向量。注意力机制中,利用字符 B 的 q 向量,与 A 和 B 的 k 向量做点积等计算,等到注意力权重矩阵,再去 A 和 B 的 v 向量做加权和得到字符 B 的新向量。最终基于字符B的输出结果,得到字符B的预测结果,也就是 C
  • 第三次同理同上

因此,我们只需要保存了历史序列的 K 和 V 矩阵信息,我们就只需要使用当前字符的信息就能预测下一个字符。而在这个过程中,我们没有显式利用因果掩码,但是我们始终遵守着因果掩码的原则。即无论如何优化,“位置 i 只能看到 ≤ i 的位置”这条铁律在推理时必须和训练时完全一致。

反过来说,如果不做KV缓存,在推理时,你把到目前为止生成的整个序列 [A, B, C] 全部灌进模型(不缓存),那么模型会计算出 3x3 的注意力矩阵。这时候,你必须显式加上因果掩码,否则 A 会看到 B 和 C(属于未来),导致预测出错。

第二个多头注意力层

这里 K 和 V 来自于编码器,也就是对编码器的输出乘以两个权重矩阵得到 K 和 V 。

这里的 Q 来自于解码器自身。

线性层和softmax层

这两个作用是将解码器的输出结果转为实际预测的单词。

首先前面第二个注意力层的结果还是 4×512 维度,线性层的作用是维度变换,将其从 512 维度转为词表的 30000 维度,值的大小表示词表中每个词的得分。之后再使用 softmax 转为概率。

解码器为什么比编码器多一层注意力层?

编码器很好理解,就是一个注意力层,后面跟一个前馈神经网络层。前馈网络将多头注意力的低维度的拼接结果转为高纬度结果。

解码器这里则多了一个注意力层,首先是一个掩码自注意力层,更着一个交叉注意力层,最后是前馈神经网络层。这里掩码自注意力层和编码层一样,综合了”输出“的上下文信息(准确地说,只有上文信息)。交叉注意力层就是综合使用编码层的输入信息。前馈网络同上。

这里能不能省略第一个注意层呢?不能,因为我们必须要通过第一个注意力层综合全文信息。比如我们预测D的时候是输入ABC预测D,此时我们要在C中加入A和B的所谓的上下文的信息;如果去掉第一个注意力层,就变成了单独输入C预测D了,这就不对了。

但是如果只有解码器结构,没有编码器的话,那么此时第二个交叉注意力层就没有了。此时,解码器结构只需要一个掩码自注意力层 + 前馈神经网络层即可。

残差连接与层归一化

残差连接不必说了,主要就是解决梯度消失问题。

层归一化说一下,我之前以为还是习惯的批归一化,结果发现看错了,论文中是层归一化。

它的计算过程如下:

  1. 计算均值和方差:对于输入张量 x(形状为 [Batch, 序列长度, 特征维度]),它会对每个样本、每个位置,独立地在最后一维(特征维度) 上计算均值和方差。
  2. 归一化:用计算出的均值和方差,对该位置上的整个特征向量进行归一化,使其分布接近均值为0、方差为1。
  3. 缩放与平移(可学习参数):最后,为了保留模型的学习能力,归一化后的结果会乘以一个可学习的缩放参数 γ (gamma) 并加上一个可学习的偏移参数 β (beta)。

用公式表示就是:

输出 = γ * ( (输入 - 均值) / sqrt(方差 + ε) ) + β

这里和层归一化有一点不同

批归一化 vs. 层归一化

这两种归一化方法的核心区别在于它们进行标准化操作时所依据的“维度”不同:

  • 批归一化(Batch Normalization):在一个批次(batch)内,针对每个特征维度,计算所有样本在该维度上的均值和方差,然后进行标准化。可以理解为“纵向”归一化。
  • 层归一化(Layer Normalization):对于每个独立的样本,计算该样本所有特征维度上的均值和方差,然后进行标准化。可以理解为“横向”归一化。

为什么 Transformer 选择层归一化?

Transformer 选择层归一化,主要是出于对自然语言处理(NLP)任务特性的考量:

  1. 处理变长序列:NLP 中的序列长度不一。BatchNorm 依赖于批次维度计算统计量,但不同长度的句子、甚至同一句子中不同位置的词,其统计特性都不同,这使得 BatchNorm 在此场景下并不适用。

  2. 不受批次大小影响:BatchNorm 的效果依赖于足够大的批次(batch size)来估算稳定的统计量。而层归一化在每个样本内部独立计算,因此不受批次大小的影响,在小批次训练时也更稳定。

  3. 训练与推理行为一致:BatchNorm 在训练和推理时的行为不同(训练时使用当前批次统计量,推理时使用历史移动平均),这种不一致性可能导致模型在 NLP 任务中表现不佳。层归一化在训练和推理时的计算方式完全相同,行为更稳定、可预测。

    LayerNorm 推理时的均值和方差也是现算的,和训练时完全一致。

补充说明:Pre-LN 与 Post-LN

值得一提的是,尽管原始论文采用了层归一化,但其在架构中的具体位置也存在演变。原始论文图示和描述中,层归一化位于残差连接之后(称为 Post-LN)。然而,后续的实践发现,将层归一化放在残差连接之前(称为 Pre-LN)能使训练更加稳定。因此,你会在许多现代 Transformer 实现中看到 Pre-LN 的结构。

注意力层后的线形层

注意力机制后面跟着一个线性层,线形层同样不改变维度,输出还是 512 维。

为什么要加一个线性层呢?网页2的B站视频的解释是多头注意力输出结果是由 64 维拼接成的,不是天然的 512 维度的结果。通过线性层,将其从 64 维的拼接“低清”结果,转为“高清的” 512 维度。

这个线性层由两个线性变换组成,并在中间加入了ReLU激活函数。 \[ \mathrm{FFN}(x) = \max(0,\; xW_1 + b_1)\,W_2 + b_2 \]

  • 第一层线性:nn.Linear(d_model, d_ff),把 512 → 2048
  • ReLU 激活
  • 第二层线性:nn.Linear(d_ff, d_model),把 2048 → 512

“Position-wise” 的含义:对序列每个位置独立做同一套线性变换(参数跨位置共享,但计算互不干扰),等价于核大小为 1 的逐点卷积。

维度流(base 模型): (B, T, 512) → Linear1 → (B, T, 2048) → ReLU → Linear2 → (B, T, 512)

为什么使用自注意力

自注意力(Self-Attention)和 交叉注意力(Cross-Attention) 定义如下

  • 自注意力(Self-Attention)定义:一种注意力机制,其中查询(Query)、键(Key)和值(Value)向量均来自同一输入序列。它通过计算序列中每个元素与所有其他元素(包括自身)的相关性,来捕捉序列内部的依赖关系,并聚合上下文信息以生成该元素的表示。
  • 交叉注意力(Cross-Attention)定义:一种注意力机制,其中查询(Query)向量来自一个序列(如目标序列),而键(Key)和值(Value)向量来自另一个不同的序列(如源序列)。它使一个序列能够从另一个序列中提取或对齐相关信息,常用于序列到序列的建模(如机器翻译)或多模态学习。

名字里的 “自”(Self) 是关键:Query、Key、Value 全部来自同一个输入序列,而不是像交叉注意力那样 Query 来自 A、Key/Value 来自 B(如解码器的第二个子层)。

自注意力的优点:

  1. 每层的总计算量复杂度
  2. 并行计算能力
  3. 长距离依赖能力

参考资料

  1. Vaswani A , Shazeer N , Parmar N ,et al.Attention Is All You Need[J].arXiv, 2017.DOI:10.48550/arXiv.1706.03762.
  2. https://www.bilibili.com/video/BV1xoJwzDESD?spm_id_from=333.788.recommend_more_video.-1&trackid=web_related_0.router-related-2479604-ptszh.1785719544086.434&vd_source=584d44ba7fed271fb82848e7894a53c8
  3. https://www.bilibili.com/video/BV1dyW9zsEk1/?vd_source=584d44ba7fed271fb82848e7894a53c8
  4. https://www.bilibili.com/video/BV1ej1EBWEWu?spm_id_from=333.788.videopod.episodes&vd_source=584d44ba7fed271fb82848e7894a53c8
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2019-2026 Vincere Zhou
  • 访问人数: | 浏览次数:

请我喝杯茶吧~

支付宝
微信