注意力机制与 Transformer一切的地基2017 年八个人发了一篇论文标题嚣张得像在挑衅Attention Is All You Need——“注意力就够了”。当时大家精心搭了好多年的循环、门控、记忆结构被这一句话全扔了。这篇讲的就是他们扔掉的那些东西、留下的那个机制以及它后来凭什么成了所有大模型的地基。本文的关系图一堆向量进去一堆新向量出来中间只发生四件事Token 向量每个词一个坐标 ↓ ① 三个投影 Q我在找什么 K我是什么 V我身上带着什么内容 ↓ ② 打分 Q·Kᵀ/√d —— 每个词问一遍所有词「咱俩相关吗」 ↓ ③ 归一 softmax —— 分数压成加起来为 1 的权重 ↓ ④ 加权 A·V —— 按权重把所有人的内容混成自己的新向量 上下文相关的新向量 ← 本篇讲的就是这四步 ↓ 外面套上残差、归一化、前馈网络堆 N 层 Transformer 的骨架一、先说说没有它的日子在 Transformer 出现之前处理文本的主流是RNN循环神经网络后来升级成 LSTM、GRU。它们的思路特别符合人的直觉一个字一个字读读完一个字把记忆往后传。听起来没错。问题在于这就是一场传话游戏。一个班 50 个人排成一排第一个看到纸条上写着明天下午三点在操场集合记得带球然后耳语给第二个人第二个人再传给第三个……传到第 50 个人嘴里大概率只剩三个字“明天踢球”。RNN 面对的就是这个困境而且它有两个要命的后果后果一信息会衰减。句子开头那个主语要传到第 100 个词的位置中间已经被揉搓了 100 遍早就面目全非。这就是经典的长距离依赖问题——LSTM 用门控机制缓解了一部分但没有根治。后果二只能排队没法并行。第 5 个词必须等第 4 个词算完第 4 个必须等第 3 个。50 个词的句子就得老老实实串 50 步。你的显卡有几千个核心此刻全在围观。这是训练慢的根源。所以 2017 年那篇论文的本质不是发明了注意力——注意力在 2014 年就被用在机器翻译里了只是当时是挂在 RNN 身上的一个配件。那篇论文真正干的事是把 RNN 整个拆掉只留注意力。然后所有人发现不但没变差还快得多、好得多。二、注意力的全部思想一句话说完别传话了。让每个词直接去看所有词自己决定该听谁的。还是那场传话游戏换个玩法把 50 个人拉进一间会议室每人手上摊开全部 50 份纸条然后每个人自己判断我该重点参考哪几份。它这个词要搞清指谁就重点看前面出现过的人名和物名“银行要判断是金融机构还是河岸就看旁边有没有河”“存款”句号没什么可参考的随便看看就行。这就是注意力。每个词产出一组权重描述它把多少注意力分给了其他每个词。权重高的地方信息就流过去。三个关键的好处在这一刻同时出现了任意两个词之间的距离都是 1 步。第 1 个词和第 100 个词直接连线不用经过中间 98 个人揉搓。长距离依赖问题从根上没了。所有词同时算没有先后。第 5 个词的运算不需要等第 4 个。整个句子一次性扔进去GPU 终于不围观了。权重是可查的。想知道模型凭什么这么答把注意力权重矩阵打出来看至少有一份它刚才看了哪里的账。顺手说一句听着很玄的话这里的注意力和人类的注意力没什么关系。它就是个加权平均一个能自己学出权重该给谁的平均。名字起得浪漫而已。三、Q / K / V一场图书馆检索上面说自己决定该听谁的但**该听谁的这个判断是怎么算出来的**借一个每个人都干过的事去图书馆找书。你走进图书馆脑子里想着怎么炖牛肉。你脑子里的那个问题叫Query查询——我想找什么。书脊上的标签、书名、目录叫Key键——我是关于什么的。书里的正文内容叫Value值——我身上真正带着什么。检索的过程是拿你的 Query去跟每一本书的 Key 比一比哪本对得上就把那本书的 Value 拿来读。注意力一模一样只不过每个词同时扮演三个角色角色含义谁生成的Q我这个位置想从别人那儿找什么信息自己的向量 × 一个学习到的矩阵 W_QK我这个位置能对外提供我是什么的标签自己的向量 × W_KV我这个位置真正要交出去的内容自己的向量 × W_V三个矩阵 W_Q、W_K、W_V 都是训练出来的不是人设计的。所以每个词想找什么、能提供什么这件事是模型自己学会的。于是计算就是三步① 分数 Q · Kᵀ / √d 拿我的问题跟所有人的标签对一遍 ② 权重 softmax(分数) 缩放成加起来为 1 的分配比例 ③ 输出 权重 · V 按比例把所有人的内容混起来那个 √d 是怎么回事d 是向量的维度。维度越高点积的结果数值上就越大softmax 会被推到极端——一个位置拿到 1其余全是 0梯度直接消失训练不动。除以 √d 就是把这个数值规模拉回来。它不是数学上必须的是工程上必须的少这一个除法训练就崩。还有一步叫因果掩码causal maskGPT 这类模型是接着往下写的第 5 个词绝不能提前看到第 6 个词。做法很暴力——把所有未来位置的分数直接设成负无穷softmax 之后权重就是 0。这就是自回归三个字的技术实现。四、别光听现场算一遍下面这段代码零依赖只用标准库手写一个完整的单头自注意力。它跑出来的是真实的数字不是示意图。importmath TOKENS[小猫,追,老鼠,。]D3# 每个向量的维度# 为了演示这里手写一组 Q / K / V。真实模型里这三个矩阵是训练出来的。Q[[1.0,0.2,0.0],[0.1,1.0,0.3],[0.9,0.4,0.2],[0.2,0.1,0.7]]K[[1.0,0.3,0.1],[0.2,0.9,0.4],[0.8,0.2,0.3],[0.1,0.2,0.6]]V[[1.0,0.0,0.0],[0.0,1.0,0.0],[0.0,0.0,1.0],[0.5,0.5,0.0]]Nlen(TOKENS)CELL8defdot(a,b):两个向量的点积越像值越大。returnsum(x*yforx,yinzip(a,b))defsoftmax(xs):把任意一组数压成「加起来等于 1」的权重。mmax(xs)# 减去最大值只为数值稳定不改变结果e[math.exp(x-m)forxinxs]ssum(e)return[v/sforvine]defdisp_width(s):显示宽度中日韩字符占 2 格用来对齐表格。returnsum(2iford(c)0x2E80else1forcins)defpad(s,n,rightFalse):fill *max(0,n-disp_width(s))returnfillsifrightelsesfilldefshow(matrix):print(pad(,CELL).join(pad(t,CELL,True)fortinTOKENS))fori,rowinenumerate(matrix):print(pad(TOKENS[i],CELL).join(pad(f{x:.3f},CELL,True)forxinrow))# ① 打分scores[[dot(q,k)/math.sqrt(D)forkinK]forqinQ]print(f句子{ .join(TOKENS)}{N}个 Token每个向量{D}维)print()print(① 打分每个词都去问一遍所有词「咱俩相关吗」)print( 除以 √d 是为了防止维数一高分数就被拉得极端)show(scores)print()# ② 归一weights[softmax(row)forrowinscores]print(② softmax把分数压成权重每行加起来 1)show(weights)print( 每行之和str([round(sum(r),6)forrinweights]))print()# ③ 因果掩码masked[[(-1e9ifjielsescores[i][j])forjinrange(N)]foriinrange(N)]causal[softmax(row)forrowinmasked]print(③ 因果掩码第 i 个词只能看第 1..i 个词不许偷看未来)show(causal)print( 「小猫」那一行变成了 [1.000, 0.000, 0.000, 0.000]——只能盯着自己)print()# ④ 加权求和out[[sum(causal[i][j]*V[j][k]forjinrange(N))forkinrange(D)]foriinrange(N)]print(④ 输出用权重把所有人的 V 加权平均得到每个词的新向量)fori,rowinenumerate(out):print( pad(TOKENS[i],CELL).join(pad(f{x:.3f},CELL,True)forxinrow))print()# ⑤ 代价print(⑤ 为什么上下文窗口不能无限大分数矩阵是 n×n)print(pad(上下文长度token,22)pad(要算的分数个数,16,True))fornin(1000,4000,32000,128000,1000000):print(pad(f{n:,},22)pad(f{n*n:,},16,True))print( 长度翻 10 倍计算量和显存占用翻 100 倍。这就是天花板的位置。)真实运行结果本机实测完整脚本见code/05-attention-demo.py句子小猫 追 老鼠 。 4 个 Token每个向量 3 维 ① 打分每个词都去问一遍所有词「咱俩相关吗」 除以 √d 是为了防止维数一高分数就被拉得极端 小猫 追 老鼠 。 小猫 0.612 0.219 0.485 0.081 追 0.248 0.600 0.214 0.225 老鼠 0.600 0.358 0.497 0.167 。 0.173 0.237 0.225 0.266 ② softmax把分数压成权重每行加起来 1 小猫 追 老鼠 。 小猫 0.318 0.215 0.280 0.187 追 0.229 0.326 0.221 0.224 老鼠 0.300 0.235 0.270 0.194 。 0.237 0.253 0.250 0.260 每行之和[1.0, 1.0, 1.0, 1.0] ③ 因果掩码第 i 个词只能看第 1..i 个词不许偷看未来 小猫 追 老鼠 。 小猫 1.000 0.000 0.000 0.000 追 0.413 0.587 0.000 0.000 老鼠 0.372 0.292 0.336 0.000 。 0.237 0.253 0.250 0.260 「小猫」那一行变成了 [1.000, 0.000, 0.000, 0.000]——只能盯着自己 ④ 输出用权重把所有人的 V 加权平均得到每个词的新向量 小猫 1.000 0.000 0.000 追 0.413 0.587 0.000 老鼠 0.372 0.292 0.336 。 0.367 0.383 0.250 ⑤ 为什么上下文窗口不能无限大分数矩阵是 n×n 上下文长度token 要算的分数个数 1,000 1,000,000 4,000 16,000,000 32,000 1,024,000,000 128,000 16,384,000,000 1,000,000 1,000,000,000,000 长度翻 10 倍计算量和显存占用翻 100 倍。这就是天花板的位置。盯着 ③ 那张表看几秒有几件事值得点出来第一每一行加起来都是 1。这是 softmax 的硬性保证。注意力本质上是分配——每个词手里有 100% 的注意力怎么分是它自己的事。第二因果掩码的效果一目了然。第 1 行是[1, 0, 0, 0]——小猫没得选只能看自己上面是斜着一排 0。第 2 行只能看前两个第 3 行只能看前三个。这张三角形的空白就是不许偷看未来的全部实现。注意第三行老鼠分给小猫0.372、给追0.292、给自己 0.336——三个位置权重接近。这就是未训练的样子它还没学会该看谁只是在平均分配。真实模型里这一行会明显偏向追因为谁追老鼠是理解这句话的关键。权重是由数据训出来的不是写死的——这一点请务必记住后面所有关于模型为什么关注这里的讨论都建立在它之上。第三输出就是 V 的加权平均。看 ④ 的第一行[1.000, 0.000, 0.000]——因为权重只有自己为 1所以输出就等于自己的 V。而第二行[0.413, 0.587, 0.000]已经是前两个词 V 的混合了。这里藏着整件事最反直觉的一点经过这一层每个词的向量已经不再是它自己了。它变成了根据上下文重新拼装过的自己。这就是上下文相关表示的字面含义——上一层的输出不是每个词的意思而是每个词在这句话里的意思。五、多头注意力为什么要同时开好几场会上面算的是一个头。一个头的问题在于它只能输出一种关注模式。softmax 逼着它做单选题——要么看主语要么看宾语不能都看。可一句话里的关系往往是同时好几个它指代谁指代关系谁是动作的发出者主谓关系这个形容词修饰哪个名词修饰关系这个词在句子里是什么成分句法关系解决方案特别朴素开好几个头一起来的。比如 8 个头就把每个词的向量切成或者投影成8 份各自独立算一遍注意力最后把 8 份结果拼回去再过一次线性变换。每个头维度只有 1/8所以总计算量基本没变但模型能同时维持 8 套不同的关注模式。这些头学到的分工事后观察通常会发现有的头专门盯相邻词有的头专门找句法上的依赖还有的头负责找开头那个词——当然也存在大量看不出在干嘛的头这属于模型自己的事没人给它们派活。六、注意力有个致命缺陷它不知道顺序这是最容易被忽略、也最要命的一点。把输入的词打乱顺序注意力的输出会跟着怎么变答案是一模一样。因为每个词去看所有词这件事本质是集合运算。集合没有顺序——{小猫, 追, 老鼠}和{老鼠, 追, 小猫}是同一个集合。也就是说纯注意力眼里的世界是一袋词不是一个句子。“猫追老鼠和老鼠追猫”它会认为完全一样。这显然不行。解决方案叫位置编码Positional Encoding把位置这个信息直接加到词向量上去。做法很直白——给第 1 个位置、第 2 个位置、第 3 个位置各准备一个专属的向量然后加到那个位置原本的词向量上。位置 1 的向量 Embedding(小猫) 位置向量(1) 位置 2 的向量 Embedding(追) 位置向量(2)注意是加不是拼。这意味着位置信息和语义信息被强行塞进了同一个空间里。这件事粗暴但有效——模型能在学习过程中自己把两者区分开。具体怎么造这些位置向量各有各的方案方案怎么做的谁在用正弦位置编码用不同频率的 sin / cos 算出来固定的不训练原始 Transformer可学习位置编码每个位置一个可训练向量跟模型一起学BERT、GPT-2RoPE旋转位置编码不加位置而是按位置旋转Q 和 K 的向量Llama、Qwen 等绝大多数现代模型RoPE 值得多说一句因为它是现在的主流而且思路很漂亮让两个词的注意力分数只取决于它们的相对距离而不取决于它们各自在第几位。第 5 个词和第 8 个词的关系应该等同于第 500 个词和第 503 个词的关系。用一个旋转角度刚好能实现这件事。这也顺带解释了一个现象为什么把上下文窗口从 8K 硬扩到 128K不是改个配置就行因为模型训练时见过的位置只有那么多突然塞进去一个没见过的位置它会懵。所以长上下文扩展要额外做训练。七、把零件拼起来Transformer 长什么样到这儿核心机制讲完了。剩下的问题是怎么把它砌成一栋楼。一层 Transformer 里除了注意力还有三样东西每一件都解决一个具体问题① 残差连接Residual输出 子层输出 输入——把输入原封不动地抄一份加到输出上。为什么需要因为模型要堆几十层。往后传的时候有用的信息很容易在一层层变换中被磨掉。残差等于给信息修了条高速公路即使某一层什么也没学会输出约等于零信息照样能原样传过去。没有残差深模型根本训不起来。② 层归一化LayerNorm把每一层输出的数值拉回稳定的范围均值为 0、方差为 1 那一带。为什么需要没有它数值会随着层数指数级放大或缩小几十层下来直接溢出。它就是个减震器。③ 前馈网络FFN注意力后面接一个小的两层全连接网络升维 → 激活 → 降维。它是干嘛的注意力负责在词之间搬运信息但它自己基本不做非线性变换。真正加工信息的是 FFN。一个很多人不知道的事实模型的参数量大头在这里。以原始配置为例注意力部分约占三分之一FFN 占约三分之二。你听到模型有 70B 参数其中一大半其实是这些一层层堆起来的小前馈网络。然后就是堆叠输入向量Embedding 位置编码 ↓ ┌─ Transformer 层 × N ──────────────┐ │ LayerNorm → 多头注意力 → 残差 │ │ LayerNorm → 前馈网络 → 残差 │ └──────────────────────────────────┘ ↓ 输出原始论文里 N 6。今天的模型N 可以是 24、32、80甚至更多——层数就是你常听到的那个词多少个 Transformer block 摞在一起。八、三种形态为什么 GPT 只用了半边原始 Transformer 是编码器-解码器结构为翻译设计的编码器把整句原文读进去双向看每个词能看左右两边产出一份理解了原文的表示解码器一边生成译文一边通过注意力去看编码器的输出。翻译这个任务天然需要这两半。但后来大家发现只留半边也完全够用于是分化出三条路形态用哪半边能不能看未来擅长什么代表Encoder-only只要编码器能双向理解分类、抽取、判断BERTDecoder-only只要解码器不能因果掩码生成接着往下写GPT、Llama、QwenEncoder-Decoder两半都要编码器能解码器不能转换翻译、摘要T5、原始 Transformer今天的大模型绝大多数是 Decoder-only。为什么因为**“预测下一个词这一件事被证明能顺带学会几乎所有事。** 你把问题写成问…… 答”它接着往下写就答完了。翻译、摘要、分类全都能塞进续写这个形式里。这意味着训练目标只需要一个给定前面的文字预测下一个 Token。简单到令人发指但它能一直扩展到几千亿参数——这才是它真正的威力所在。一个足够简单的目标才能吃掉足够多的数据和算力。九、这份强大的账单注意力把长距离和并行两个问题一起解决了代价是把计算量变成了O(n²)。回到第四节的 ⑤上下文长度 1,000 时分数矩阵有约 100 万个格子128,000 时是163 亿个。长度翻 10 倍计算量翻 100 倍。这一条约束牵出了你今天听到的一大半工程概念上下文窗口为什么不能无限开大不只是显存放不下输入更是那个 n×n 的分数矩阵放不下、算不完。上下文长度的天花板本质上是这个平方项定的。为什么会有 KV Cache生成时是一个词一个词吐的。每吐一个新词就要拿它的 Q 去跟前面所有词的 K、V算一遍。如果不缓存每次都得把前面所有词重算一遍——每生成一个 token 的代价是 O(n)总共就是 O(n²)。所以把历史 K 和 V 存下来复用这就是 KV Cache是推理加速里最基础的一招。但缓存不是白拿的它要占显存而且随上下文长度线性增长。这解释了为什么长上下文对话跑到后面越来越慢、越来越吃显存——不是模型累了是那份缓存越堆越厚。还有一堆名字里带高效注意力的东西FlashAttention、稀疏注意力、滑动窗口注意力……目标基本是同一个想办法绕开或者缓解这个平方项。有的靠更省显存的算法重排有的靠让每个词只看附近一段有的靠近似。这是个至今仍在活跃的方向也说明这个平方项到现在都没有被真正解决。十、四个常见误区误区一以为注意力是模型在关注重点。它是个加权平均。你看到的权重图确实漂亮但别过度解读——很多头的权重模式人类根本解释不了它关注了这里和它因为这里才这么答是两回事。误区二以为 Transformer 是更快的 RNN。不是加速版是结构性的替换。RNN 的顺序依赖是它的定义的一部分Transformer 直接把这个依赖删掉了代价是必须额外补上位置编码。两者的取舍完全不同。误区三以为层数越多参数越多。层数只是其中一个乘数。参数量 ≈ 每层参数 × 层数而每层参数里大头是 FFN。层数、隐藏维度、FFN 宽度、词表大小四个一起决定总参数量。所以32 层的模型不一定比24 层的大。误区四以为上下文窗口的能力等于参数量的能力。这是两件事。窗口决定它能同时看到多少参数决定它有多聪明。窗口开得再大模型学不会用那些远处的信息也是白搭——长上下文能力同样需要专门训练不是改个数字就有。十一、把这件事放回地图上把大模型的概念按依赖关系从下往上摞成几层——最底下是算力和数据往上依次是文字的表示、模型结构、训练、调用最顶上才是增强和智能体——本篇讲的是第三层L6 智能体层 Agent / Function Calling / ReAct / 记忆 / MCP L5 增强层 RAG / 向量检索 / Rerank / GraphRAG / 微调 L4 调用层 Prompt / Few-shot / CoT / 温度 / Top-p L3 训练层 预训练 / SFT / RLHF / DPO / LoRA / Scaling Law L2 结构层 ★ Transformer / 注意力 / 参数量 / 上下文窗口 ★ ← 本篇在这里 L1 表示层 Token / 分词 / 词表 / Embedding L0 底座 算力 / 数据 / 成本这一层为什么叫地基因为上面所有的东西——训练方法、提示技巧、检索增强、智能体——全都跑在这套结构上。换个注意力实现、改下位置编码方案往上每一层的行为都会跟着变。而且这一层定下的三个数——层数、隐藏维度、上下文窗口——是后面一切讨论的物理边界。你在 L4 写多花哨的提示词都改不了它一次只能看 128K 个 Token 这件事。这一层留下的窟窿也很清楚O(n²) 这个平方项。它逼出了 KV Cache、逼出了高效注意力这一整个研究方向也逼出了外挂记忆的思路——既然塞不进窗口那就干脆把知识放到外面去查。这个思路后来长成了一整套东西。三条可以带走的结论注意力就是加权平均Q 找、K 对、V 取。没有玄学也没有理解本质是让每个词自己决定该听谁的。Transformer 的胜利不是因为它更聪明而是因为它不排队。删掉顺序依赖换来并行代价是必须补上位置编码且计算量变成 O(n²)。上下文窗口的天花板是平方项定的。KV Cache 缓解的是重复计算缓解不了这个平方——这是当前架构最硬的约束之一。《大模型概念地图》专栏 · 第 5 篇