通俗版
先说 Attention 要解决什么问题
考虑这句话:"猫坐在垫子上,它看起来很舒服。"
"它"指的是什么?你一眼就知道是猫。但模型怎么知道?模型拿到的是一串 token,每个 token 刚进来时只有自己的向量,完全不知道上下文。如果模型在处理"它"这个词时,只看"它"自己的向量,它永远不知道"它"指向什么——因为"它"本身没有具体含义,含义全在上下文里。
Attention 就是用来解决这件事的:让每个词在计算自己的最终表示时,能够"看"整个句子里其他词,并决定"我需要从哪些词借多少信息过来"。
用一个完整例子走一遍
句子:"猫追了狗,它跑掉了。"(假设"它"在这里指狗)
模型处理到"它"这个 token 的时候,要搞清楚它指什么。Attention 的做法是:
- 把"它"变成一个查询(Q)——"我在找什么?"
- 把句子里所有其他词(猫、追、了、狗……)都变成键(K)——"我这里能提供什么?"
- "它"的 Q 和每个词的 K 做点积,得到一个相似度分数——比如"它"和"狗"的分数是 4.2,"它"和"追"的分数是 0.3,"它"和"猫"的分数是 1.1
- 对这些分数做 softmax,变成概率分布:比如"狗"0.65,"猫"0.22,其他词加起来 0.13。Softmax 干的事一句话就能说清:把一堆可正可负、可大可小的分数,压成一组加起来等于 1、原本大的还是大的正数——之后就能当权重用了。全文之后再看到 softmax,说的都是这一步。
- 把每个词的**值(V)**按这个概率加权平均
最终"它"的向量 = 0.65 × 狗的V向量 + 0.22 × 猫的V向量 + 0.13 × 其他词的V向量
结果是:"它"的向量被"狗"的语义信息大量渗透,变成了一个"它≈狗"的向量表示。 后续层在处理"跑掉了"时,用这个已经知道"它是狗"的向量,就能做出更准确的判断。
为什么要有 Q、K、V 三个角色而不是一个?
因为"我想找什么"和"我能提供什么"是两件事。同一个词,作为被查询对象时需要暴露"我是什么"(K),作为查询发起者时需要表达"我在找什么"(Q),作为被选中提供信息时需要传递"我的具体内容"(V)。
类比一下:K 像书的目录项标题,Q 像你在图书馆查的关键词,V 像书本身的内容。你用关键词匹配目录标题,选中之后读的是书的内容——目录标题和书的内容可以不一样(一本书的标题是"网络编程",但内容包含了 socket、TCP、HTTP 等),这就是 K 和 V 分离的意义。
为什么 Q 和 K 也不能合成一个
有人会追问:既然 K 是"我能提供什么",Q 是"我在找什么",那能不能就用同一个向量既当 K 又当 Q?答案是不行,因为关系是有方向的。
举个例子:"工程师"和"报销单"之间存在关联——工程师会填报销单。但反过来"报销单"关注的东西里,"工程师"未必排在最前面,报销单可能更关注"金额""日期""审批人"。也就是说,A→B 的注意力权重和 B→A 的注意力权重不该对称。如果 Q=K,两个词之间的点积就永远对称,模型学不到"关系有方向"这件事。
Q 和 K 用两套不同的矩阵,就是给"我看向谁"和"谁看我"这两个方向留出各自的表示空间。同一个词作为"发起查询"和作为"被查询",可以给出完全不一样的向量。这是三个矩阵而不是两个的核心理由。
Q、K、V 是怎么算出来的
每个词进这一层的时候,手上只有一个 embedding 向量。模型给每一层都准备了三套可学的矩阵 W_Q、W_K、W_V,把这个向量分别乘一遍,就得到这个词在这一层里的 Q、K、V。三套矩阵在训练里各自朝不同方向长——W_Q 学"该拿什么去问",W_K 学"该拿什么去应答",W_V 学"该把什么信息拿出来"。
Q/K/V 不是词的固有属性,是模型每一层临时算出来的三个"角色扮演"。换一层,同一个词又得到全新的一套 Q、K、V——这也是为什么 Transformer 要堆几十层:每层从不同角度重新审视一次上下文。
Causal Mask 是什么
生成文字时,模型只能看到已经生成的内容,不能"偷看"未来。用一张表来理解,假设有 4 个 token"今|天|天|气",每行表示"这个 token 能看到哪些 token":
今 天 天 气
今 [ ✓ ✗ ✗ ✗ ]
天 [ ✓ ✓ ✗ ✗ ]
天 [ ✓ ✓ ✓ ✗ ]
气 [ ✓ ✓ ✓ ✓ ]
"今"只能看自己;"气"能看所有人。被打 ✗ 的位置,在计算时强制把分数设为 -∞,经过 softmax 之后变成概率 0,等于完全看不见。
FFN 是干什么的
每个 Transformer Block 里,Attention 之后还跟着一个 FFN(前馈网络)。
Attention 做的是"信息聚合"——我从别的词那里借来了信息,更新了自己的向量,但这个更新是各词向量的加权平均,是线性操作,表达能力有上限。
FFN 做的是"特征提取"——用非线性激活函数(ReLU/GELU)对 Attention 输出的向量进一步变换。非线性意味着"如果某个维度超过某个阈值才激活,否则置零",这让模型能表达"如果 A 且 B,则 C"这样的组合语义。
Attention 管词与词的关联,FFN 管非线性组合,缺一层网络就废。
一个 Transformer 模型里,大概 1/3 的参数在 Attention,2/3 的参数在 FFN。
进阶版
为什么除以 √d_k
Self-Attention 的计算是 Q·Kᵀ / √d_k。为什么要除这个数?
考虑向量维度 d_k = 64 的情况。Q 和 K 都是 64 维向量,每个维度的值大概是均值 0、标准差 1 的随机数。两个向量做点积,就是把 64 个维度的乘积加起来——每个维度贡献的方差是 1,64 个加起来标准差是 √64 = 8。
如果不缩放,点积的数值会随维度增大而暴涨。很大的数值输入 softmax 之后,会导致梯度消失:softmax 在极端值附近的梯度趋近于零,注意力分布会变成 one-hot(几乎全部权重集中在一个词上),模型失去了"综合多个词的信息"的能力,训练极不稳定。
除以 √d_k 之后,点积的标准差变回 1,softmax 在平坦区域工作,梯度正常。这是统计学推导出来的工程选择。
KV Cache 的内存占用计算
KV Cache 是推理加速的关键优化:生成第 N 个 token 时,前 N-1 个 token 的 K、V 向量已经算过了,缓存下来就不用重算。但这个缓存有多大?
以 Llama 3.1 70B 为例(80 层、64 个 Query 头、8 个 KV 头、head dimension 128,KV 使用 FP16,batch size 为 1):
- 每层每个 token 的 KV 大小:2(K 和 V)× 8 个 KV 头 × 128 维 × 2 字节 = 4096 字节 = 4 KiB
- 80 层合计:80 × 4 KiB = 320 KiB / token
- 128K context:128,000 × 320 KiB ≈ 39.1 GiB
这还没有算模型权重、运行时缓冲、batch 放大和推理引擎额外开销。KV Cache 是长上下文推理的重要显存瓶颈,但不能脱离模型结构、精度和 batch 单独报一个固定数字。
O(N²) 在实际工程里意味着什么
Self-Attention 的计算复杂度是 O(N²):每个 token 都要和所有其他 token 计算注意力分数。
- 1K token:1,000² = 100 万次计算
- 8K token:8,000² = 6,400 万次计算(64× 更多)
- 128K token:128,000² = 163 亿次计算(163,000× 更多)
从 1K 到 128K,context 长度涨了 128 倍,Attention 的计算量涨了 128² = 16,384 倍。128K context 的推理为什么比 4K 慢得多、贵得多,就是这条平方关系撑着。业界研究线性注意力,就是想替掉 O(N²) 的 Softmax Attention。
FFN 内部长什么样
通俗版说了 FFN 是干什么的,这里补一下它的形状。一层 FFN 其实就是两次线性变换夹一次非线性:向量先被一个大矩阵抬到 4 倍宽(比如 4096 → 16384),过一次 GELU 激活,再被另一个矩阵压回原来的宽度。"先抬高再压回"听着绕,但模型的知识和记忆主要就存在这两块大矩阵的权重里——Attention 只管调度信息,装东西的是 FFN。
一个 70B 模型里,FFN 大概占 2/3 参数,也就是四五百亿参数全在这。业界近两年推的 MoE(专家混合),本质就是把 FFN 拆成很多个"专家 FFN",每次只激活其中两三个——参数总量不变,每一步实际算的只有其中一小块,推理成本就下来了。DeepSeek-V3、Mixtral 用的都是这套。