《Attention is All You Need》提出了Transformer架构及Attention机制,引发了人工智能领域的浪潮。站在ChatGPT-6 Astra、DeepSeek V4.1等新模型、新架构不断迭代更新的时间点上,我决定从头学习一遍Attention机制、架构、计算方式的演进,以对大模型发展有一个更加全面的了解。
苏剑林大佬在他的博客有过一个《Transformer升级之路》 系列,主要聚焦了Transformer架构中位置编码的内容及其数学原理和推导。此外,还有一篇缓存与效果的极限拉扯:从MHA、MQA、GQA到MLA 。受该系列博客启发,本篇就将作为《Attention计算进化史》的第一篇,从Full Attention讲起,开启对于Attention机制发展的学习与梳理,更加侧重于具体流程并伴有部分代码实现,希望能帮到和我一样没有那么强数学直觉的人。
什么是Attention?
缩放点积注意力
对于Attention,大家的第一反应应该都是《Attention is All You Need》论文中的经典公式:
A t t e n t i o n ( Q , K , V ) = S o f t m a x ( Q K T d k ) V (1.1) \mathrm{Attention}(Q, K, V) = \mathrm{Softmax} ( \dfrac{QK^T}{\sqrt{d_k}})V \tag{1.1} Attention ( Q , K , V ) = Softmax ( d k Q K T ) V ( 1.1 )
这没有什么问题。但是单从这个公式来看,其实很难了解到背后的实现细节,Q , K , V Q,K,V Q , K , V 是如何被计算出来的,整个流程如何通过代码实现的等等。
下面,我们从Attention计算的输入序列开始,假设输入是一个批次大小为 B B B 的序列数据,第 b b b 个输入数据可以表示为:
X ( b ) = [ x 1 ( b ) , x 2 ( b ) , … , x L ( b ) ] ∈ R L × d m o d e l (1.2) X^{(b)} = [x_1^{(b)}, x_2^{(b)}, \dots,x_L^{(b)}] \in \mathbb{R}^{L \times d_{model}} \tag{1.2} X ( b ) = [ x 1 ( b ) , x 2 ( b ) , … , x L ( b ) ] ∈ R L × d m o d e l ( 1.2 )
其中,L L L 是序列长度,d m o d e l d_{model} d m o d e l 是特征维度,x i ( b ) x_i^{(b)} x i ( b ) 即为第 b b b 个序列中第 i i i 个Token的嵌入向量,最终输入即为:
X = S t a c k ( X ( 1 ) , X ( 2 ) , … , X ( B ) ) ∈ R B × L × d m o d e l (1.3) X = Stack(X^{(1)},X^{(2)}, \dots, X^{(B)}) \in \mathbb{R}^{B \times L \times d_{model}} \tag{1.3} X = S t a c k ( X ( 1 ) , X ( 2 ) , … , X ( B ) ) ∈ R B × L × d m o d e l ( 1.3 )
输入 X X X 不是直接作为 Q , K , V Q,K,V Q , K , V 进行注意力计算,对于自注意力而言,是经过三个不同的、可学习的权重矩阵进行线性变换后,分别得到 Q , K , V Q,K,V Q , K , V 矩阵。
Q = X W Q ∈ R B × L × d k , W Q ∈ R d m o d e l × d k K = X W K ∈ R B × L × d k , W K ∈ R d m o d e l × d k V = X W V ∈ R B × L × d v , W V ∈ R d m o d e l × d v (1.4) \begin{gather}
Q = XW_Q \in \mathbb{R}^{B \times L \times d_k} ,\space\space W_{Q} \in \mathbb{R}^{d_{model} \times d_{k}} \\
K = XW_K \in \mathbb{R}^{B \times L \times d_k} ,\space\space W_{K} \in \mathbb{R}^{d_{model} \times d_{k}} \\
V = XW_V \in \mathbb{R}^{B \times L \times d_v} ,\space\space W_{V} \in \mathbb{R}^{d_{model} \times d_{v}}
\end{gather} \tag{1.4} Q = X W Q ∈ R B × L × d k , W Q ∈ R d m o d e l × d k K = X W K ∈ R B × L × d k , W K ∈ R d m o d e l × d k V = X W V ∈ R B × L × d v , W V ∈ R d m o d e l × d v ( 1.4 )
然后,才对得到的 Q , K , V Q,K,V Q , K , V 进行式 1.1 的计算,下面进一步拆解其中的计算过程。
第一步,计算注意力分数,表示查询 i i i 和键 j j j 的相似度(通过点积的方式)
( Q K T ) b i j = q i ( b ) ⋅ k j ( b ) (1.5) (QK^T)_{bij} = q_{i}^{(b)} \cdot k_{j}^{(b)} \tag{1.5} ( Q K T ) bij = q i ( b ) ⋅ k j ( b ) ( 1.5 )
经过缩放后,对每个 Query,在最后一个维度,也就是 Key 维度上进行 s o f t m a x \mathrm{softmax} softmax :
S = Q K T d k ∈ R B × L × L A = S o f t m a x ( Q K T d k ) ∈ R B × L × L A b i j = e S b i j ∑ j ′ = 1 L e S b i j ′ ∈ R 1 (1.6) \begin{gather} \\
S = \dfrac{QK^T}{\sqrt{ d_{k} }} \in \mathbb{R}^{B \times L \times L} \\
A = \mathrm{Softmax} ( \dfrac{QK^T}{\sqrt{d_k}}) \in \mathbb{R}^{B \times L \times L} \\ \\
A_{bij} = \dfrac{e^{S_{bij}}}{\sum_{j'=1}^{L} e^{S_{bij'}}} \in \mathbb{R}^1
\end{gather} \tag{1.6} S = d k Q K T ∈ R B × L × L A = Softmax ( d k Q K T ) ∈ R B × L × L A bij = ∑ j ′ = 1 L e S bi j ′ e S bij ∈ R 1 ( 1.6 )
将归一化的注意力权重再与值矩阵进行张量乘法,对位置 i i i 的Token计算出新的表示:
O = A V ∈ R B × L × d v o i b = ∑ j = 1 L A b i j ⋅ v j b ∈ R d v (1.7) \begin{gather}
O = AV \in \mathbb{R}^{B \times L \times d_{v}} \\
o_{i}^{b} = \sum_{j=1}^{L} A_{bij} \cdot v_{j}^{b} \in \mathbb{R}^{d_{v}}
\end{gather} \tag{1.7} O = A V ∈ R B × L × d v o i b = j = 1 ∑ L A bij ⋅ v j b ∈ R d v ( 1.7 )
在 Next Token Prediction 的自回归建模范式下,位置 i i i 在预测下一个 Token 时不能利用未来位置的信息。因此,Attention 需要满足因果性约束:位置 i i i 只能关注位置 1 , … , i 1,\dots,i 1 , … , i 。训练阶段虽然一次性输入完整序列,但通过 Causal Mask 屏蔽未来 Token,就可以在不泄露未来信息的前提下并行计算所有位置。具体而言,生成一个掩码矩阵 M ∈ R L × L M \in \mathbb{R}^{L \times L} M ∈ R L × L ,对于矩阵位置索引 i i i (查询 Q Q Q 的索引)和索引 j j j (键 K K K 的索引)的定义如下:
M i j = { 0 , if i ≥ j − ∞ , if i < j (1.8) M_{i j} = \begin{cases}
0, &\text{if }i\geq j\\
- \infty, & \text{if } i < j
\end{cases} \tag{1.8} M ij = { 0 , − ∞ , if i ≥ j if i < j ( 1.8 )
再将该掩码矩阵直接与缩放后的注意力分数相加,从而带有掩码的注意力计算为:
A t t e n t i o n = s o f t m a x ( Q K T d k + M ) V (1.9) \mathrm{Attention} = \mathrm{softmax}\left( \frac{QK^T}{\sqrt{ d_{k} }} + M \right) V \tag{1.9} Attention = softmax ( d k Q K T + M ) V ( 1.9 )
经过 s o f t m a x \mathrm{softmax} softmax 后,对应未来位置的权重会变为零,从而实现因果性。
注意力计算为什么要除以 d k \sqrt{ d_{k} } d k 根据式 1.5 ,点积由 d k d_{k} d k 个 q i , t ⋅ k j , t q_{i,t} \cdot k_{j,t} q i , t ⋅ k j , t 乘积相加得到,在初始化附近,q q q 和 k k k 的各维分量大致独立,一般初始化的均值为0,方差为1,那么乘积的均值也仍为0,方差为1。把 d k d_{k} d k 个乘积相加后,整体的均值为0,但是方差会积累到 d k d_k d k ,标准差为 d k \sqrt{ d_{k} } d k ,也就是说,维度越高,点积波动的范围越大。
但是,s o f t m a x \mathrm{softmax} softmax 函数由于指数函数的特性,对于输入的尺度比较敏感,如何存在较大的输入,s o f t m a x \mathrm{softmax} softmax 的分布会变得“尖”,即较大的输入会被分配到接近所有的权重,这会导致其他位置的权重较小,在反向传播中的梯度也很小,容易训练不稳定。除以 d k \sqrt{ d_{k} } d k 后,方差会被缩放到1,使得分布更加平缓,更加容易训练。
用代码实现以上注意力计算如下:
from math import sqrt
from einops import einsum
import torch.nn.functional as F
mask = torch.tril(
torch.ones(L, L, dtype = torch.bool)
)
def scaled_dot_attention (q, k, v, mask = None ):
"""
q, k: [B, L, d_k]
v: [B, L, d_v]
mask: [L, L]
"""
d_k = q.size( - 1 )
# 1. Query-Key 相似度
scores = einsum(q, k, "... i d, ... j d -> ... i j" )
# 2. Scale
scores = scores / sqrt(d_k)
# 3. Causal Mask
if mask is not None :
scores = scores.masked_fill( ~ mask, float ( "-inf" ))
# 4. Normalize over all Key positions
attn_weights = F.softmax(scores, dim =- 1 )
# 5. Weighted sum of Values
output = einsum(attn_weights, v, "... i j, ... j d -> ... i d" )
return output
通过 einsum 可以方便地利用 pattern 字符串的方式说明张量维度的变化,从而执行批张量乘法,此处的mask为上三角为0,下三角(包括对角线)为1的布尔张量,从而实现 1.9 中加性掩码的效果(与其等价)。
Multi-head Attention
以上为单头的自注意力,多头注意力通过不同的可学习投影矩阵,将输入映射到多个不同的 Query、Key、Value 表示空间,使不同注意力头能够学习不同的注意力模式,并从不同的表示子空间聚合信息。对同样的输入,会计算多遍自注意力,每一组的可学习矩阵权重是不同的,然后再把 h h h 个头的计算结果拼接,最终经过一个输出矩阵进行线性变换后输出。
仍然考虑 1.3 式作为输入,假设有 h h h 个注意力头,每个头的Query/Key的维度为 d k d_{k} d k ,value的维度为 d v d_{v} d v ,通常取:
d k = d v = d m o d e l / h (1.10) d_{k} = d_{v} = d_{model} / h \tag{1.10} d k = d v = d m o d e l / h ( 1.10 )
对于第 r r r 个头,其 Q 、 K 、 V Q、K、V Q 、 K 、 V 计算如下:
Q ( r ) = X W Q ( r ) ∈ R B × L × d k , W Q ( r ) ∈ R d m o d e l × d k K ( r ) = X W K ( r ) ∈ R B × L × d k , W K ( r ) ∈ R d m o d e l × d k V ( r ) = X W V ( r ) ∈ R B × L × d v , W V ( r ) ∈ R d m o d e l × d v (1.11) \begin{gather}
Q^{(r)} = XW_{Q}^{(r)} \in \mathbb{R}^{B \times L \times d_{k}}, \space\space W_{Q}^{(r)} \in \mathbb{R}^{d_{model}\times d_{k}} \\
K^{(r)} = XW_{K}^{(r)} \in \mathbb{R}^{B \times L \times d_{k}}, \space\space W_{K}^{(r)} \in \mathbb{R}^{d_{model}\times d_{k}} \\
V^{(r)} = XW_{V}^{(r)} \in \mathbb{R}^{B \times L \times d_{v}}, \space\space W_{V}^{(r)} \in \mathbb{R}^{d_{model}\times d_{v}}
\end{gather} \tag{1.11} Q ( r ) = X W Q ( r ) ∈ R B × L × d k , W Q ( r ) ∈ R d m o d e l × d k K ( r ) = X W K ( r ) ∈ R B × L × d k , W K ( r ) ∈ R d m o d e l × d k V ( r ) = X W V ( r ) ∈ R B × L × d v , W V ( r ) ∈ R d m o d e l × d v ( 1.11 )
按照一样的计算逻辑,每个头的输出为:
head r = s o f t m a x ( Q ( r ) ( K ( r ) ) T d k + M ) V ( r ) (1.12) \text{head}_{r} = \mathrm{softmax}\left( \frac{Q^{(r)}(K^{(r)})^T}{\sqrt{d_{k}}} + M\right)V^{(r)} \tag{1.12} head r = softmax ( d k Q ( r ) ( K ( r ) ) T + M ) V ( r ) ( 1.12 )
最终将所有注意力头的输出结果在特征维度拼接起来,并经过一个线性变换,使得不同的注意力头之间能相互交互:
MultiHead ( X ) = C o n c a t ( head 1 , head 2 , … , head h ) W O (1.13) \text{MultiHead}(X) = \mathrm{Concat}(\text{head}_{1}, \text{head}_{2}, \dots, \text{head}_{h}) W_{O} \tag{1.13} MultiHead ( X ) = Concat ( head 1 , head 2 , … , head h ) W O ( 1.13 )
其中,拼接后的张量的维度为 R B × L × h d v \mathbb{R}^{B \times L \times hd_{v}} R B × L × h d v ,W O ∈ R h d v × d m o d e l W_{O} \in \mathbb{R}^{hd_{v}\times d_{model}} W O ∈ R h d v × d m o d e l 。
但在实际工程使用中,计算不会按照每个头单独计算再Concat的方式进行,而是直接通过并行化的方式,将多个头原本独立的矩阵运算,组织成包含 Head 维度的批量矩阵运算。Head 维度在这里相当于额外的批次维度,使不同头的注意力计算可以通过批量矩阵乘法并行完成。不同头之间并不会在 Attention 权重计算阶段相互混合。具体计算过程如下:
首先,将每个头各自的 Q 、 K 、 V Q、K、V Q 、 K 、 V 投影矩阵沿输出特征维度分别拼接,形成三个大的投影矩阵,通过矩阵乘法一次性生成所有注意力头对应的 Q 、 K 、 V Q、K、V Q 、 K 、 V 。
Q = X W Q ∈ R B × L × h d k , W Q ∈ R d m o d e l × h d k K = X W K ∈ R B × L × h d k , W K ∈ R d m o d e l × h d k V = X W V ∈ R B × L × h d v , W V ∈ R d m o d e l × h d v (1.14) \begin{gather}
Q = XW_{Q} \in \mathbb{R}^{B \times L \times hd_{k}}, \space\space W_{Q} \in \mathbb{R}^{d_{model} \times hd_{k}} \\
K = XW_{K} \in \mathbb{R}^{B \times L \times hd_{k}}, \space\space W_{K} \in \mathbb{R}^{d_{model} \times hd_{k}} \\
V = XW_{V} \in \mathbb{R}^{B \times L \times hd_{v}}, \space\space W_{V} \in \mathbb{R}^{d_{model} \times hd_{v}} \\
\end{gather} \tag{1.14} Q = X W Q ∈ R B × L × h d k , W Q ∈ R d m o d e l × h d k K = X W K ∈ R B × L × h d k , W K ∈ R d m o d e l × h d k V = X W V ∈ R B × L × h d v , W V ∈ R d m o d e l × h d v ( 1.14 )
在进行注意力计算时,将整个的 Q , K , V Q,K,V Q , K , V 矩阵进行 reshape 和 transpose,将每个头的特征拆分开来:
Q ′ = transpose ( reshape ( Q ) ) : R B × L × h d k → R B × L × h × d k → R B × h × L × d k K ′ = transpose ( reshape ( K ) ) : R B × L × h d k → R B × L × h × d k → R B × h × L × d k V ′ = transpose ( reshape ( V ) ) : R B × L × h d v → R B × L × h × d v → R B × h × L × d v (1.15) \begin{gather}
Q' = \text{transpose}(\text{reshape}(Q)): \mathbb{R}^{B \times L \times hd_{k}} \to \mathbb{R}^{B \times L \times h \times d_{k}} \to \mathbb{R}^{B \times h \times L \times d_{k}} \\
K' = \text{transpose}(\text{reshape}(K)): \mathbb{R}^{B \times L \times hd_{k}} \to \mathbb{R}^{B \times L \times h \times d_{k}} \to \mathbb{R}^{B \times h \times L \times d_{k}} \\
V' = \text{transpose}(\text{reshape}(V)): \mathbb{R}^{B \times L \times hd_{v}} \to \mathbb{R}^{B \times L \times h \times d_{v}} \to \mathbb{R}^{B \times h \times L \times d_{v}}
\end{gather} \tag{1.15} Q ′ = transpose ( reshape ( Q )) : R B × L × h d k → R B × L × h × d k → R B × h × L × d k K ′ = transpose ( reshape ( K )) : R B × L × h d k → R B × L × h × d k → R B × h × L × d k V ′ = transpose ( reshape ( V )) : R B × L × h d v → R B × L × h × d v → R B × h × L × d v ( 1.15 )
然后进行注意力分数计算:
S = Q ′ K ′ T d k ∈ R B × h × L × L (1.16) S = \frac{Q'{K'}^T}{\sqrt{ d_{k} }} \in \mathbb{R}^{B \times h \times L \times L} \tag{1.16} S = d k Q ′ K ′ T ∈ R B × h × L × L ( 1.16 )
这里的转置只作用在Key的最后两个维度,Head维度也不参与不同头之间的注意力分数计算,只是作为一个额外的批次维度,使得计算并行化。因此,对第 b b b 个批次的第 r r r 个注意力头,其注意力分数的元素计算为:
S b r i j = q b r i ⋅ k b r j d k ∈ R 1 (1.17) S_{brij}= \frac{q_{bri} \cdot k_{brj}}{\sqrt{ d_{k} }} \in \mathbb{R}^1 \tag{1.17} S b r ij = d k q b r i ⋅ k b r j ∈ R 1 ( 1.17 )
然后,和单头的自注意力一致,在最后一个维度(Key维度)上进行 S o f t m a x \mathrm{Softmax} Softmax ,再与Value矩阵相乘:
A = S o f t m a x ( S + M ) ∈ R B × h × L × L O = A V ′ ∈ R B × h × L × d v (1.18) \begin{gather}
A = \mathrm{Softmax}(S + M) \in \mathbb{R}^{B \times h \times L \times L} \\
O = A V' \in \mathbb{R}^{B \times h \times L \times d_{v}} \\
\end{gather} \tag{1.18} A = Softmax ( S + M ) ∈ R B × h × L × L O = A V ′ ∈ R B × h × L × d v ( 1.18 )
从逐行展开:
O b r i = ∑ j = 1 L A b r i j V b r j ′ ∈ R d v (1.19) O_{bri} = \sum_{j=1}^{L} A_{brij} V'_{brj} \in \mathbb{R}^{d_{v}} \tag{1.19} O b r i = j = 1 ∑ L A b r ij V b r j ′ ∈ R d v ( 1.19 )
下面,再将各个注意力头的输出合并,再经过一个输出投影矩阵线性变换:
O ′ = Reshape ( Transpose ( O ) ) : R B × h × L × d v → R B × L × h × d v → R B × L × h d v Y = O ′ W O ∈ R B × L × d m o d e l , W O ∈ R h d v × d m o d e l (1.20) \begin{gather}
O' = \text{Reshape}(\text{Transpose}(O)):\mathbb{R}^{B \times h \times L \times d_{v}} \to \mathbb{R}^{B \times L \times h \times d_{v}} \to \mathbb{R}^{B \times L \times hd_{v}} \\
Y = O'W_{O} \in \mathbb{R}^{B \times L \times d_{model}}, \space\space W_{{O}} \in \mathbb{R}^{hd_{v}\times d_{model}}
\end{gather} \tag{1.20} O ′ = Reshape ( Transpose ( O )) : R B × h × L × d v → R B × L × h × d v → R B × L × h d v Y = O ′ W O ∈ R B × L × d m o d e l , W O ∈ R h d v × d m o d e l ( 1.20 )
在各个 Head 内部进行注意力计算时,不同 Head 的注意力权重和 Value 加权求和相互独立。随后,将所有 Head 的输出沿特征维度拼接,再通过输出投影矩阵 W O W_O W O 对不同 Head 的输出特征进行线性组合。
用代码实现多头注意力计算如下:
class Multihead_self_attention ( nn . Module ):
def __init__ (
self,
d_model: int ,
num_heads: int ,
device: torch.device | None = None ,
dtype: torch.dtype | None = None
):
super (). __init__ ()
self .d_model = d_model
self .num_heads = num_heads
self .d_k = self .d_model // self .num_heads
self .device = device
self .dtype = dtype
self .W_q = Linear( self .d_model, self .d_model, device = device, dtype = dtype)
self .W_k = Linear( self .d_model, self .d_model, device = device, dtype = dtype)
self .W_v = Linear( self .d_model, self .d_model, device = device, dtype = dtype)
self .W_o = Linear( self .d_model, self .d_model, device = device, dtype = dtype)
self .attention = Scaled_dot_product_attention( device = device, dtype = dtype)
def forward (self, x: torch.Tensor):
q = self .W_q(x)
k = self .W_k(x)
v = self .W_v(x)
seq_len = x.shape[ - 2 ]
masks = torch.tril(torch.ones((seq_len, seq_len), dtype = torch.bool, device = x.device))
attention_values = self .attention(
rearrange(q, "... s (h d) -> ... h s d" , h = self .num_heads),
rearrange(k, "... s (h d) -> ... h s d" , h = self .num_heads),
rearrange(v, "... s (h d) -> ... h s d" , h = self .num_heads),
masks
)
return self .W_o(rearrange(attention_values, "... h s d -> ... s (h d)" ))
以上,就是最基础的标准点积注意力和多头注意力的定义及其计算流程,下面将进一步从Attention走向一个完整的Transformer Block,再进而到近年来的演进。