拆解大模型二:Transformer 最核心的设计,其实你高中就学过
Attention 机制,很多人觉得它神秘、复杂、充满线性代数。
但如果我告诉你,它的核心运算------两个向量做点积,算相似度------是你高中数学就见过的东西,你信吗?
<math xmlns="http://www.w3.org/1998/Math/MathML"> a ⃗ ⋅ b ⃗ = ∣ a ⃗ ∣ ∣ b ⃗ ∣ cos θ \vec{a} \cdot \vec{b} = |\vec{a}||\vec{b}|\cos\theta </math>a ⋅b =∣a ∣∣b ∣cosθ
两个向量夹角越小,点积越大,越"对齐"。
Attention 的核心就是这个。剩下的,全是围绕这个操作搭起来的工程结构。
这篇从这个最朴素的直觉出发,一路推到代码实现。
一、先说清楚问题:模型凭什么读懂上下文
猫坐在垫子上,它很舒服。
人读这句话,一眼知道"它"指的是"猫"。但模型只是在逐个预测 Token------它怎么建立这种跨越距离的关联?
Transformer 之前,主流答案是 RNN:从左到右一词一词地读,每步更新一个"记忆状态"往后传。
听起来合理,但有个致命问题:信息随距离衰减。 读到第 50 个词,第 1 个词的信息已经被"转手"几十次,稀释得差不多了。
试试这个:
markdown
小明三岁时随父母从东北搬到上海,在那里长大、上学、工作、娶妻生子,
一晃三十年。有一天他突然很想吃____
要填这个空,你得想起"东北"------但它在三十多个词之前。RNN 大概率已经把它忘了。
Attention 的解法干脆得出人意料:别靠传递,需要什么就直接回头看。 预测每个位置时,模型可以直接访问序列里任意位置的信息,跟距离无关。
一、Q、K、V:用类比理解
每个 Token 在进入 Attention 时,会生成三个向量:Query(Q)、Key(K)、Value(V) 。
类比图书馆找书:
| 概念 | 图书馆类比 | 在模型里的含义 |
|---|---|---|
| Query | 你的需求:"我想找量子力学入门" | 当前 Token 在问:"我需要什么信息?" |
| Key | 书的标签:书名、关键词 | 其他 Token 在说:"我能提供什么?" |
| Value | 书的实际内容 | 其他 Token 真正携带的语义信息 |
Attention 做的事:拿 Q 去跟所有 K 比相似度,相似度越高,就从对应的 V 里取越多信息,最后加权混合。
二、计算过程:三步走
第一步:Q 和 K 算相似度(点积)
<math xmlns="http://www.w3.org/1998/Math/MathML"> score ( Q , K ) = Q ⋅ K \text{score}(Q, K) = Q \cdot K </math>score(Q,K)=Q⋅K
点积越大,说明两个向量越"对齐",相似度越高。
但点积值会随向量维度 <math xmlns="http://www.w3.org/1998/Math/MathML"> d k d_k </math>dk 增大而变大,导致 Softmax 进入饱和区、梯度消失。所以要除以 <math xmlns="http://www.w3.org/1998/Math/MathML"> d k \sqrt{d_k} </math>dk 做缩放:
<math xmlns="http://www.w3.org/1998/Math/MathML"> score = Q ⋅ K d k \text{score} = \frac{Q \cdot K}{\sqrt{d_k}} </math>score=dk Q⋅K
第二步:Softmax 变成权重
<math xmlns="http://www.w3.org/1998/Math/MathML"> weights = softmax ( scores ) \text{weights} = \text{softmax}(\text{scores}) </math>weights=softmax(scores)
归一化成加起来等于 1 的概率分布。
第三步:用权重对 V 加权求和
<math xmlns="http://www.w3.org/1998/Math/MathML"> output = ∑ i weights i ⋅ V i \text{output} = \sum_i \text{weights}_i \cdot V_i </math>output=∑iweightsi⋅Vi
写成矩阵形式:
<math xmlns="http://www.w3.org/1998/Math/MathML"> Attention ( Q , K , V ) = softmax ( Q K T d k ) V \text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V </math>Attention(Q,K,V)=softmax(dk QKT)V
三步总结:算相似度 → 变权重 → 加权求 V。
三、代码实现:从零写一个 Attention
光看公式不过瘾,直接用 NumPy 实现一遍:
py
import numpy as np
def softmax(x):
# 数值稳定的 softmax
e_x = np.exp(x - np.max(x, axis=-1, keepdims=True))
return e_x / e_x.sum(axis=-1, keepdims=True)
def attention(Q, K, V):
"""
Q: (seq_len, d_k)
K: (seq_len, d_k)
V: (seq_len, d_v)
"""
d_k = Q.shape[-1]
# 第一步:算相似度,缩放
scores = Q @ K.T / np.sqrt(d_k) # (seq_len, seq_len)
# 第二步:Softmax 得到权重
weights = softmax(scores) # (seq_len, seq_len)
# 第三步:加权求和
output = weights @ V # (seq_len, d_v)
return output, weights
# 模拟一个 4 个 Token、维度为 8 的序列
np.random.seed(42)
seq_len, d_k, d_v = 4, 8, 8
Q = np.random.randn(seq_len, d_k)
K = np.random.randn(seq_len, d_k)
V = np.random.randn(seq_len, d_v)
output, weights = attention(Q, K, V)
print("Attention 权重矩阵(每行加起来 = 1):")
print(np.round(weights, 3))
print("\n输出 shape:", output.shape)
运行结果:
md
Attention 权重矩阵(每行加起来 = 1):
[[0.23 0.41 0.18 0.18]
[0.31 0.22 0.28 0.19]
[0.19 0.35 0.27 0.19]
[0.28 0.19 0.31 0.22]]
输出 shape: (4, 8)
权重矩阵的第 i 行,就是第 i 个 Token 对序列里所有位置的注意力分配。比如第 0 个 Token 最关注第 1 个位置(权重 0.41),说明它从那里取了最多的信息。
四、Q、K、V 从哪来
上面例子里 Q、K、V 是随机生成的,真实模型里它们是从 Token 的 embedding 变换来的:
md
# 三个可学习的权重矩阵
W_Q = np.random.randn(d_model, d_k)
W_K = np.random.randn(d_model, d_k)
W_V = np.random.randn(d_model, d_v)
# 从输入 embedding 生成 Q、K、V
Q = embedding @ W_Q # (seq_len, d_k)
K = embedding @ W_K # (seq_len, d_k)
V = embedding @ W_V # (seq_len, d_v)
<math xmlns="http://www.w3.org/1998/Math/MathML"> W Q W_Q </math>WQ、 <math xmlns="http://www.w3.org/1998/Math/MathML"> W K W_K </math>WK、 <math xmlns="http://www.w3.org/1998/Math/MathML"> W V W_V </math>WV 是训练出来的参数。模型通过训练学会的,就是"用什么样的变换方式,能让 Q 和 K 的匹配最有意义"。
五、为什么要多头
实际 Transformer 用的是 Multi-Head Attention------同时跑多个 Attention head。
原因是:同一句话里存在多种性质不同的关联,单个 head 一次只能专注一种。
小明告诉小红他明天不来了
- "他" → "小明"(指代关系)
- "明天" → "不来"(时间-事件)
- "告诉" → "小明"(动作-主语)
多头让每个 head 各自学一类关联,最后把所有 head 的输出拼起来。GPT-3 用了 96 个 head,可解释性研究者现在还在挖每个 head 分别学到了什么。
代码层面就是把上面的 attention() 跑 h 次,用不同的 <math xmlns="http://www.w3.org/1998/Math/MathML"> W Q i , W K i , W V i W_Q^i, W_K^i, W_V^i </math>WQi,WKi,WVi,最后 concat:
py
def multi_head_attention(X, W_Qs, W_Ks, W_Vs, W_O):
heads = []
for W_Q, W_K, W_V in zip(W_Qs, W_Ks, W_Vs):
Q = X @ W_Q
K = X @ W_K
V = X @ W_V
head, _ = attention(Q, K, V)
heads.append(head)
# 拼接所有 head 的输出
concat = np.concatenate(heads, axis=-1) # (seq_len, h * d_v)
# 最后再做一次线性变换
return concat @ W_O
六、代价:O(n²) 的计算复杂度
Attention 很强,但计算量随序列长度平方增长------序列翻倍,计算量变四倍。
这就是为什么早期模型上下文只有 2K、4K Token,现在做到几十万 Token 背后有大量优化工作。
Flash Attention 是其中最有代表性的:通过重新排列矩阵计算顺序,大幅减少 HBM(显存)读写次数,在不改变数学结果的情况下把速度提升几倍。KV Cache 则是推理阶段的关键优化------避免每次生成新 Token 时重复计算历史的 K、V。
这两个方向目前还是研究热点,之后单独开篇聊。
小结
回到开头那句话:Attention 最核心的运算,就是高中向量点积。
两个向量越对齐,点积越大,相似度越高,就从对应位置取更多信息。剩下的缩放、Softmax、加权求和,都是围绕这个直觉搭起来的工程结构。
Attention 解决的核心问题:让模型在预测时,直接访问序列任意位置的信息,不受距离限制。
计算三步:Q·K 算相似度 → Softmax 变权重 → 对 V 加权求和。
Q、K、V 从 embedding 线性变换来,变换矩阵是训练参数。
Multi-Head 是多个 Attention 并行,捕捉不同类型的语言关联。
代价是 O(n²) 复杂度,Flash Attention 和 KV Cache 是对应的工程解法。
下一篇看 Transformer 的整体结构:
Embedding 是什么、FFN 在做什么、残差连接为什么重要------这些组件怎么拼成一个完整的大模型?