ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从下一个词概率到自注意力:拆解ChatGPT工作原理

从下一个词概率到自注意力:拆解ChatGPT工作原理

简介:《ChatGPT的工作原理(2023)》是一份面向大模型入门者、产品与技术爱好者的107页PDF专题资料,聚焦ChatGPT背后的LLM与GPT系列机制,帮助读者建立从模型结构到对话生成流程的整体认知。包内仅含1个PDF文件,压缩包约4.98MB,篇幅完整、便于通读与检索。资料围绕Transformer架构展开,依次讲解预训练、按上下文预测下一个词的生成方式、对话历史理解、温度参数对输出多样性的影响,以及面向特定任务的微调、对话状态管理与用户反馈优化等关键环节。已有190人学习,可作为理解生成式AI基本概念、梳理ChatGPT工作链条的参考读物,也适合在阅读论文或动手实验前补充背景知识。

1. ChatGPT 的工作原理:它做的全部事情,是在算下一个词的概率

很多人第一次听到「ChatGPT 的工作原理」,脑子里浮现的是一个检索系统:先把知识存进去,再按问题把答案捞出来。这份文档开篇就否定了这个印象——它给出的结论更简单,也更反直觉:模型在每一次输出时,做的唯一动作是拿到已有的 token 序列,算出词表上四万多个候选 token 各自成为「下一个」的概率,然后按这个分布抽一个出来,把它接回序列,再重复一遍。没有检索,没有查表,没有先想好整段话再抄写。所谓对话、推理、写代码,都是这个循环跑几百次之后涌现出来的表象。

理解这件事的价值在于,它能一次性解释掉大量让人困惑的现象:为什么同一个问题问两次答案不一样,为什么它会一本正经地编造不存在的引用,为什么聊得久了它会把前面交代的约束忘掉。这些不是 bug,是这个机制在特定参数下的必然表现。后面的几章会沿着「概率怎么算出来 → 上下文怎么被压进向量 → 参数从哪来 → 出问题去哪儿查」这条线,把这台机器拆到能看见齿轮的程度。

2. 从 n-gram 到概率分布:为什么「数频次」这条路走到 40,000 词表就断了

在神经网络接管这件事之前,文本生成有一个非常朴素的做法:统计。给定前 n-1 个词,数一数语料里下一个词分别出现了多少次,用频次除以总数当作概率。这就是 n-gram 模型,也是理解大模型最合适的起点——因为它和 GPT 系列解决的是同一个问题,只是解法从「查表」换成了「拟合函数」。

2.1 2-gram 的表有多大:40,000 词表下的组合爆炸

先把最直观的 2-gram 写出来。它的假设极其粗暴:下一个词只跟前一个词有关,跟更早的内容无关。

from collections import Counter, defaultdict # 假设语料已经被切成 token 序列 corpus = ["the", "best", "thing", "about", "ai", "is", "its", "ability", "to", "..."] bigram = defaultdict(Counter) for i in range(len(corpus) - 1): bigram[corpus[i]][corpus[i + 1]] += 1 # 统计 (当前词 -> 下一个词) 的共现次数 def next_word_dist(w, table): counts = table[w] total = sum(counts.values()) # 分母:这个前缀在语料里出现的总次数 if total == 0: return {} # 没见过这个前缀,直接空手而归 return {token: c / total for token, c in counts.items()}

这段代码的含义很直白:counts是「当前词后面跟过哪些词」的计数器,total是这个前缀出现的总次数,两者相除得到条件概率 P(下一个词 | 当前词)。参数只有语料本身,没有可训练的东西。

问题出在规模上。词表按四万个 token 算,2-gram 需要覆盖的格子是 40,000 × 40,000,也就是十六亿个位置;换成 3-gram,条件是前两个词,格子数变成 40,000³,六千四百亿。而真实语料里出现的组合只是这个空间里极稀疏的一小撮,绝大多数格子永远是 0。更难受的是「没见过的组合直接概率为 0」——模型遇到一个训练时没出现过的短语,就彻底失语,这在开放域对话里是致命的。

文档里举了一个很能说明问题的例子:字母 q 后面几乎必然跟着 u。这条规律在 2-gram 里能被统计出来,因为样本足够多;但「某个特定人名后面该接什么动词」这种长尾规律,语料里可能只出现三五次,统计结果噪声极大。n-gram 的死穴不是算力,是数据稀疏——它把语言当成一张巨大的查找表,而这张表永远填不满。

2.2 从查表到拟合:把离散索引换成连续函数

神经网络换掉的正是「表」这个隐喻。它不再问「这个前缀在语料里出现过几次」,而是把所有前缀压缩进一个高维向量空间,用一个含参数的函数去逼近 P(下一个 token | 前文)。这一步的意义在于泛化:即使某个具体组合从没出现过,只要它在向量空间里和见过的组合足够接近,模型就能给出一个合理的概率分布。

文档里用一条多项式曲线做了类比:a + bx + cx²能用三个参数描述一条抛物线,参数不是数据点本身,但能把数据点的走向概括出来。换到模型上,1750 亿个参数不是 1750 亿条事实,而是一个极其庞大的可微函数里的系数,它概括的是「什么样的前文后面倾向于接什么」这种统计规律。

这个区别带来两个直接后果。第一,可微意味着可以用梯度下降来调参数——只要定义一个衡量「预测得准不准」的损失函数,就能沿着梯度把参数往更准的方向推。第二,连续意味着插值——两个从没一起出现过的词,只要它们的向量位置合理,模型依然能算出有意义的概率。n-gram 做不到插值,它只会说「没见过」。

2.3 temperature=0.8 到底改了什么:softmax 上的一个除法

模型最后一层输出的是 logits,也就是词表上每个 token 的一个实数打分,还不是概率。要变成概率得经过 softmax,而 temperature 就是在 softmax 之前做的一次缩放。

import math def softmax_with_temperature(logits, T): # T -> 0 时退化为 argmax(永远选最高分,输出死板) # T = 1 时是模型训练出来的原始分布 # T > 1 时分布被压平,低分 token 也有机会被抽中 scaled = [z / T for z in logits] m = max(scaled) # 减去最大值,防止 exp 上溢 exps = [math.exp(z - m) for z in scaled] s = sum(exps) return [e / s for e in exps]

logits是模型输出的原始打分向量,长度等于词表大小;T就是温度。关键在z / T这一步:T 小于 1 时,分数差距被放大,softmax 之后高分项更接近 1,分布变尖;T 大于 1 时差距被压缩,分布变平,长尾 token 也有了被抽中的机会。

文档里提到的 0.8,落在这个区间的偏尖一侧——比原始分布稍微保守一点,既不至于每次都选同一个词,也不会跑到太发散的地方去。这个参数不改变模型权重,不改变任何一条学到的规律,它只改变采样时分布的形状。所以同一段前文、同一个模型,温度不同,出来的句子就完全不同。理解这一点,后面「为什么两次回答不一样」的疑问就解决了一半。

3. 嵌入与自注意力:Transformer 怎么把上下文压进一个向量

概率分布算出来了,但前提是「前文」得先变成一个模型能处理的东西。文本是离散符号,神经网络只认数字,中间必须有一层转换;而且光转成数字还不够,模型还得知道这些词之间谁该关注谁。这两件事分别由嵌入层和自注意力机制完成,也是 Transformer 架构里最值得掰开看的部分。

3.1 词表到向量:embedding 表里存的到底是什么

最底层是一张查找表,行数是词表大小,列数是隐藏维度。每个 token 对应其中一行,取出来就是一个定长向量。这张表本身是训练出来的参数,初始化时是随机数,随着训练不断被调整。

向量里的每一维没有人类可读的含义,不要指望第 37 维代表「褒贬」、第 512 维代表「时态」。意义存在于相对位置上:语义相近的 token,向量方向也相近;某些语义关系甚至能表现为向量差——经典的例子是「某国首都」减去「某国」再相加,结果落在另一个国家首都的附近。这说明模型在训练中自己学会了把语义关系编码成空间里的平移。

维度怎么选是个权衡。维度太低,四万个 token 挤在几十维空间里必然重叠,区分不开;维度太高,参数量和计算量都上去,而收益递减。常见的量级是几千维,配合四万左右的词表,光嵌入层就是几亿个参数,在总参数量里占比不算大。

还有一件事绕不开:自注意力本身对顺序不敏感。把输入打乱,注意力算出来的加权和是一样的。所以必须在嵌入向量上再加一个位置编码,把「第几个位置」的信息注入进去,模型才知道词序。这一步经常被忽略,但少了它,模型看到的就是一袋词而不是一句话。

3.2 自注意力:Q、K、V 与那句「q 后面跟 u」

注意力机制要解决的问题是:处理某个位置的 token 时,应该看前文的哪些位置,看多重的权重。做法是给每个 token 生成三个向量——Query(我在找什么)、Key(我能提供什么)、Value(我实际携带的信息),然后用 Query 和所有 Key 做点积算相关性。

import numpy as np def attention(Q, K, V, causal_mask=True): d_k = Q.shape[-1] scores = Q @ K.T / np.sqrt(d_k) # 缩放点积:维度越大点积越大,必须归一 if causal_mask: # 上三角置为极小值,保证第 t 个位置只能看到 <= t 的位置 mask = np.tril(np.ones_like(scores)) scores = np.where(mask > 0, scores, -1e9) scores = scores - scores.max(axis=-1, keepdims=True) # 数值稳定 weights = np.exp(scores) / np.exp(scores).sum(axis=-1, keepdims=True) return weights @ V, weights # 输出是 Value 的加权和

Q @ K.T得到的是一个方阵,第 i 行第 j 列表示「第 i 个位置对第 j 个位置的关注分数」。除以sqrt(d_k)是必须的:维度一高,点积的方差就大,softmax 会被推到极端饱和的状态,梯度几乎消失。causal_mask是生成式模型特有的约束——预测第 t 个 token 时不能偷看 t 之后的答案,所以要把上三角全部压成负无穷。

权重矩阵的可读性不错。拿文档里那个例子:处理字母 q 的时候,它的 Query 向量和后面 u 的 Key 向量点积会明显偏高,softmax 之后 u 的权重接近 1,于是这个位置的输出向量几乎就是 u 的 Value。模型没有一条「q 后面必须跟 u」的规则,这个行为是从注意力权重里长出来的。

3.3 多头、残差与层归一化:为什么能堆几十层还不崩

单头注意力只能学一种关注模式,多头则是把 Q、K、V 各自投影到若干组低维子空间里并行算,最后拼接起来。不同的头会分化出不同的职责——有的盯紧邻词,有的盯句法结构,有的跨很远去抓指代关系。这种分工不是人为指定的,是训练中自发形成的。

真正让深层网络能训起来的是残差连接和层归一化。残差把子层的输入直接加到输出上,梯度可以沿这条捷径回传,不用穿过几十层变换;层归一化把每个位置的激活值重新标准化,避免数值在层间累积放大或衰减。这两样东西看起来是工程细节,实际上是「深」这件事的前提——没有它们,堆到十几层梯度就散掉了。

把这套结构重复几十次,就得到了一个深层堆叠的编码器。每一层都在前一层的表示上再做一次「谁该关注谁」的重新加权,浅层抓局部搭配,深层抓语义和指代。文档里提到的 GPT-2 到 GPT-3 的演进,架构上的变化远小于规模上的变化,主要就是层数、维度、头数一起往上推。

4. 参数量与训练目标:1750 亿参数为什么不是一座知识库

到这里,前向计算的链路已经通了:token 变向量,向量过几十层注意力,最后一层投到词表上出 logits。剩下的问题是,这些参数是怎么从无到有被定下来的,以及「1750 亿」这个数字究竟意味着什么。

4.1 预训练目标:交叉熵就是「正确答案那一位的负对数概率」

预训练做的事情朴素到令人意外:拿一段文本,遮住最后一个 token,让模型预测它是什么,然后看模型给正确答案分配了多少概率。概率越高,损失越小。

import math def cross_entropy(logits, target_id): # logits: 词表上每个 token 的打分;target_id: 语料里真实的下一个 token m = max(logits) # 数值稳定 log_sum_exp = m + math.log(sum(math.exp(z - m) for z in logits)) log_p = logits[target_id] - log_sum_exp # log softmax 的写法 return -log_p # 取负,越准越小

如果模型给正确答案的概率是 0.9,损失约 0.105;如果只给了 0.01,损失约 4.6。训练就是不断调参数让这个值变小。整个互联网规模的语料被切成无数个这样的「预测下一位」任务,梯度沿着几十层网络回传,参数一点点挪动。

值得强调的是,这个目标不含任何「事实性」约束。模型从来没被要求「说的必须是真的」,只被要求「让真实文本的概率更高」。它学到的是文本的统计规律,而「符合事实」只是这些规律的一个相关性很强的副产品,不是硬约束。所有幻觉的根源,都能追到这句话上。

4.2 1750 亿怎么分布:一张粗略的参数账

参数量不是一个笼统的数字,它按结构分布,知道分布才能判断「加参数加在哪儿」。

组件大致构成占比量级
词嵌入词表大小 × 隐藏维度个位数百分比
注意力每层 4 个投影矩阵(Q/K/V/输出)约三分之一
前馈网络每层两个大矩阵,中间维度通常是隐藏维度的 4 倍约三分之二
层归一化每层两个向量可忽略

前馈网络占大头这件事经常被低估。注意力负责「信息在位置之间流动」,前馈层负责「在每个位置上做非线性变换」,后者才是参数的主要存放地。所谓「模型的知识存哪儿」,更准确的说法是:知识弥散地分布在前馈层的权重里,没有任何一个神经元对应一条事实。

这也解释了为什么参数量的增长能带来能力跃迁。更多的参数意味着函数容量更大,能拟合更细的规律;同时更多的层意味着更长的推理链条。文档里从 GPT-2 到 GPT-3 的对比之所以重要,不在于架构变了,而在于规模跨过某个门槛之后,一些之前做不好的任务突然能做了——这种「量变引起质变」目前还只有经验观察,没有严格解释。

4.3 从「会续写」到「会对话」:微调和反馈的作用

预训练完的模型只是一个续写器。你给它「人工智能最好的地方是」,它会接着写下去,写得很像那么回事,但它不知道自己在回答谁的问题,也不会遵守「用三句话回答」这种指令。

微调阶段做的就是把这台续写器掰成对话助手。常见做法分两步:先用人工编写的高质量对话样本做监督微调,让模型学会「用户提问 → 助手回答」这个格式;再用人类对多个候选回答的偏好排序训练一个打分模型,用这个打分去优化生成策略。第二步的意义在于,很多「好回答」的标准难以用规则写出来——语气是否得体、是否该拒绝、是否该反问——但人可以比较两个回答哪个更好,这种相对判断比绝对标注容易得多。

还有一个容易混淆的点:微调改变的仍是参数,不是记忆。模型不会因为微调就「记住」了某次对话,它学到的是一种回答风格和格式。真正的对话历史是在推理时拼进输入里的,属于上下文,不是权重。这个区分很关键,第五章的几条排查全都建立在它之上。

5. 排查清单:同一个提示词为什么会给出两种答案

前面几章讲的是机制,这一章讲机制在真实使用中留下的痕迹。下面四条是我在拆这套原理时反复撞上的现象,每条都按「看到什么 → 为什么 → 怎么办」写。

5.1 现象:同样的输入,两次输出差异明显

原因:采样过程带随机性。温度大于 0 时,模型是从概率分布里抽 token 而不是取最大值,每次抽的结果都可能不同;一旦某个位置抽到了不同的词,后续所有位置的上下文都变了,误差会沿序列放大。这不是模型不稳定,是它在按设计工作。

解决:需要可复现就显式固定采样策略——把温度调到接近 0,或者用固定的随机种子。要判断一个输出是不是「模型真的这么认为」,别只看一次,跑五到十次看分布,高频出现的才是它的主流倾向。

5.2 现象:聊到二十轮以后,它把最初交代的约束忘了

原因:上下文窗口是有限的。对话历史被拼接成一个序列送进模型,超出窗口的部分会被截断。更要紧的是,注意力对远距离位置的权重天然偏弱,即使内容还在窗口里,早期信息的实际影响力也已经衰减。窗口是硬边界,衰减是软边界,两者叠加就表现为「遗忘」。

解决:把关键约束放在靠近末尾的位置重述一遍,或者每轮都以结构化的方式带上「当前任务」「已确认条件」这类摘要。不要把重要设定埋在几十轮之前指望它一直记得。

5.3 现象:它给出的引用、数据、人名看起来完全可信,但查不到

原因:回到 4.1 节那句话——训练目标是让真实文本的概率更高,不是让输出符合事实。当模型在某个话题上缺乏具体记忆时,它会生成「在这个语境下最像样子的 token 序列」。一个格式规范、语气笃定的假引用,在统计上比「我不知道」更接近训练语料里的常见模式。

解决:把模型输出当成草稿而不是结论。所有可核验的信息——数字、日期、引用、接口签名——都必须回到一手来源确认。这不是对模型的不信任,是对「概率生成」这个机制的尊重。

5.4 现象:输出开始复读同一句话,或者陷进一个循环出不来

原因:生成是自回归的,已经输出的内容会成为下一步的上下文。一旦某句话被生成出来,它就把后续的概率分布往「再说一遍」的方向推了一把,形成正反馈。温度偏低时这个效应更明显,因为每次都在选最高分的那个 token。

解决:适度提高温度或引入重复惩罚,让分布别那么尖;在提示里明确要求「不要重复上文」。如果循环出现在长文本生成里,把任务拆成几段、每段重新给一次上下文,通常比一口气生成到底更稳。

6. 设计取舍:把概率采样包装成对话,代价落在哪

把这套机制看完整,会发现它做的每一个选择都在拿一样东西换另一样。

设计选择换来了什么付出了什么
用连续函数替代频次表对没见过的组合也能给出合理概率参数规模爆炸,训练成本极高
输出概率分布而非单一答案表达多样,能处理开放式任务不确定性无法根除,需要外部核验
用固定上下文窗口承载记忆结构简单,无需外部状态管理长对话必然遗忘,且无法真正「记住」
用偏好反馈做对齐回答更有用、更符合人类期待可能过度保守,也可能迎合而非求真

最值得琢磨的是第二条。整个行业都在追求「更准」,但底层机制决定了它输出的本质是一个分布,不是一条事实。所有让它更「确定」的手段——降温度、加约束、做检索增强——都是在分布外面套壳,没有改变内核。承认这一点,用起来会踏实很多:它是个极强的语言接口,不是一个可靠的知识库,两者需要的能力完全不同。

还有一个常被忽略的取舍是上下文窗口。很多人期待「聊得越久它越懂我」,但机制上模型对每次请求都是无状态的——所谓记忆全靠把历史拼进输入。这意味着「记住」的成本随对话长度线性增长,而收益因为注意力衰减是递减的。理解这条,设计交互时就会主动做摘要、做状态外置,而不是指望模型自己扛。

我自己的习惯是,任何要依赖模型输出的决策,都强制走一遍「这条信息能不能在一手来源里找到」的检查,找不到就当它是草稿。这个习惯是被一次格式完美、来源全假的引用逼出来的——那次之后我才真正把「概率生成」这四个字当回事。这份文档的价值也在这里:它没有教你写提示词,而是把这个机制的每一层拆开摆在桌上,看完之后再回头看那些玄学现象,基本都能对上号。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进