简介:这份107页的PDF围绕ChatGPT的工作原理展开,面向希望系统理解大语言模型运行机制的技术读者、产品人员与AI学习者,可用于补齐自然语言处理基础、梳理GPT系列模型的关键概念。压缩包内仅含1个PDF文件,大小约4.98MB,便于集中阅读与检索;目前已有190人学习。内容从Transformer架构与预训练切入,依次说明自动生成文本、上下文理解、温度参数对输出多样性的影响,并延伸到微调、对话策略与反馈循环等环节,并把握记忆机制、对话状态跟踪等概念;结合温度0.8等具体设定,帮助读者理解模型如何在连贯性、创新性与准确性之间取舍。对于想弄清ChatGPT生成逻辑、对话管理及持续优化思路的读者,这份资料可作为结构化的入门与复习材料,也可用于理解预训练模型走向对话应用的关键链路。
1. ChatGPT 原理讲义怎么读:从「猜下一个词」开始,别从术语表开始
一次内部评审上,有人指着一份问答记录说“模型把知识记住了”,结果翻了半天才发现,真正起作用的只是上下文里塞进去的那段材料。这类误会几乎都来自同一个起点:把 ChatGPT 当成一个在查资料的知识库,而不是一台在算下一个词概率的机器。这份讲义做的事,就是把“算概率”这条线从头到尾走一遍,从最朴素的 n-gram 计数一路推到 Transformer 的注意力、温度参数 0.8、预训练与微调的分工,最后落到反馈循环和对话状态。
它适合两类人:写过调用接口、但说不清 logits 和温度到底改了什么的工程同学;以及要给团队做内部分享、需要一个能讲清楚的骨架的人。读法上有个反直觉建议:先读讲 2-gram 和温度的那几节,把“下一个词”这件事的手感建立起来,再回头看 Transformer 结构,不然很容易读成一堆名词。下面的章节按这个顺序展开,中间给可抄的算例和参数表,最后给一套能跑出数字的自检法。
2. 从 2-gram 到 Transformer:把「下一个词」的概率算给你看
2.1 讲义为什么先讲 n-gram,而不是直接上注意力
直接讲自注意力,读者会卡在“为什么需要它”。讲义的策略是先造一个能算的最小模型:统计语料里“某个词后面跟某个词”出现了多少次,用频次当概率。这个模型笨,但每一步都能在纸上验算,而且它的两个死穴会自然暴露出来——数据稀疏和组合爆炸,而这两点恰好是神经网络语言模型存在的理由。
组合爆炸的量级可以自己算。假设词表规模是 4 万(讲义里给的口径),那么:
| 模型阶数 | 需要估计的单元数 | 说明 |
|---|---|---|
| 1-gram | 4×10^4 | 每个词一个概率,语料够就能估 |
| 2-gram | 4×10^4 的平方 ≈ 1.6×10^9 | 已经远超任何语料的覆盖能力 |
| 3-gram | 4×10^4 的三次方 ≈ 6.4×10^13 | 绝大多数组合在语料里一次都不出现 |
讲义里举的例子更直观:一个只有 4 个词的短片段,按有序位置枚举就能列出 16 种 2-gram 组合,再往上加一阶,数量级立刻跳一大截。词数到 20 的时候,靠人脑已经数不过来了。这就是 n-gram 的天花板——它不是“不够聪明”,是“根本估不出来”。
所以中间章的第一层结论是:n-gram 的价值不在效果,在于它把“语言模型 = 条件概率”这个定义摆到了台面上。后面的神经网络,改的是估计这个条件概率的方式,不是这个定义本身。
2.2 跟着讲义手算一次条件概率,顺便看清平滑的必要性
拿一段小语料做演示,语料是:“chatgpt 是 一个 语言 模型 , chatgpt 是 一个 对话 系统”。统计步骤如下。
第一步,分词并在句首句尾加边界符,得到两条序列。第二步,统计相邻词对的频次。第三步,用“词对频次 / 前一个词的频次”算条件概率。第四步,对没出现的组合做平滑,避免概率直接为 0。
手算结果:
| 前词 | 后词 | 词对频次 | 前词频次 | 条件概率(未平滑) |
|---|---|---|---|---|
| 是 | 一个 | 2 | 2 | 1.00 |
| 一个 | 语言 | 1 | 2 | 0.50 |
| 一个 | 对话 | 1 | 2 | 0.50 |
| 语言 | 模型 | 1 | 1 | 1.00 |
| 对话 | 系统 | 1 | 1 | 1.00 |
看起来挺漂亮,但问题藏在“语言 → 模型”这一行:因为“语言”在语料里只出现了一次,所以它后面接“模型”的概率被算成了 1.0。换一段没见过的文本,“语言”后面接别的词时,这个模型给出的概率就是 0,整个句子的概率连乘下来也变成 0,模型直接失效。
这就是讲义反复强调的稀疏问题,工程上的常见做法是加一平滑或者回退:给每个没出现的组合补一个极小的计数。加一平滑后,“语言 → 模型”的概率变成 (1+1)/(1+40000),大约 5×10⁻⁵。这个数看着小得离谱,但它保证了概率不为零,句子还能被比较大小。
实际操作时我一般按这四步走,避免算到一半乱掉:
- 固定分词口径,中文要么按字要么按词,前后必须一致;
- 句首加起始符、句尾加结束符,否则句尾词的分布永远是错的;
- 先统计分母(前词频次),再统计分子(词对频次),顺序反了很容易重复计数;
- 平滑系数单独记一列,方便回头比较不同系数对最终概率的影响。
提示:手算这一步别跳过。后面看神经网络输出 softmax 概率时,脑子里如果没有“分母是前词出现次数”这个概念,温度参数的很多现象会解释不通。
2.3 从查表到向量:嵌入、上下文窗口和注意力各管什么
n-gram 是查表,表里没有就是零。神经网络语言模型换了个思路:把每个词映射成一个稠密向量(嵌入),再用这些向量去算下一个词的概率。好处是“语言”和“对话”这类从没在语料里相邻出现过的词,只要向量接近,概率就能被泛化出来,而不是硬邦邦的零。
讲义在这一段的推进顺序是:词表规模、嵌入维度、上下文窗口、自注意力。这里逐项说清楚各自的作用,别混成一团。
| 组件 | 管什么 | 工程上关心什么 |
|---|---|---|
| 词表与分词 | 决定一个句子被切成多少个单元 | 中文一个字一个 token 还是几个字一个 token,直接影响上下文占用 |
| 嵌入向量 | 把离散的词变成可计算的稠密表示 | 维度越高表达力越强,显存和计算量同步上涨 |
| 上下文窗口 | 决定模型一次能看多远 | 不是“记忆”,超出窗口的内容就是看不见 |
| 自注意力 | 让每个位置直接和窗口内所有位置交互 | 相比逐词递推的结构,可以并行计算,长距离依赖不用一步步传 |
自注意力的计算可以压缩成一句话:当前位置的向量,去和窗口内每个位置的向量做点积,得到一个权重,再按权重把其他位置的信息加权求和。点积大说明相关,权重就大。这个机制替代了早期循环结构“一个词一个词往后传”的做法,训练时可以整段并行,这也是为什么大模型能把语料规模堆上去。
对比一下两种路线的差别,就能明白讲义为什么要花篇幅做这个过渡:
| 维度 | n-gram | 神经网络语言模型 |
|---|---|---|
| 参数存什么 | 词对频次表 | 嵌入矩阵和网络权重 |
| 未见组合 | 概率为 0,需平滑兜底 | 靠向量相似度泛化 |
| 长距离依赖 | 阶数一高就爆表 | 靠注意力在窗口内直接建模 |
| 训练方式 | 数一遍就行 | 反向传播,需要大量算力和数据 |
| 可解释性 | 高,能查到具体计数 | 低,中间表示基本是黑匣子 |
黑匣子这个说法不是贬义,是提醒:读这份讲义的时候,别指望从权重里读出“它为什么这么答”,能读出来的是机制——输入怎么变成向量、向量怎么变成打分、打分怎么变成概率、概率怎么变成那个被输出的词。这条链路清楚了,后面调温度、调上下文、判断幻觉来源,才有依据。
3. 温度参数 0.8 到底改了什么:一次 softmax 手算加一段采样代码
3.1 温度不是「创造力旋钮」,它改的是分布的陡峭程度
模型对每个候选词输出一个原始打分(logits),softmax 把这些打分变成概率。温度 T 出现在 softmax 的分母里:先把每个 logit 除以 T,再取指数归一化。T 小于 1,logit 之间的差距被放大,概率分布变尖,高分词更容易被选中;T 大于 1,差距被压缩,低分词也有机会,输出就发散。
拿四个候选词的 logits [3.0, 2.0, 1.0, 0.5] 实算,结果如下:
| 候选词 | T=0.2 | T=0.8 | T=1.0 | T=1.5 |
|---|---|---|---|---|
| A | 0.993 | 0.708 | 0.631 | 0.509 |
| B | 0.007 | 0.203 | 0.232 | 0.261 |
| C | 0.000 | 0.058 | 0.085 | 0.134 |
| D | 0.000 | 0.031 | 0.052 | 0.096 |
这张表能解释很多现象。T=0.2 时 A 几乎垄断,输出稳定但容易重复;T=1.5 时 A 只剩一半概率,剩下四个词分走另一半,文本开始“活”,但跑题概率同步上升。讲义里提到的 0.8,就落在“主体还是最高分词、但保留了两成左右的备选空间”这个区间,属于对话场景里比较稳的取值。
有个常见误解要拆掉:温度不改变模型知道什么,只改变它在已知候选里怎么挑。知识错了,把温度调到 0 也还是错,只是错得更稳定、更容易复现。
3.2 一段能直接跑的采样代码:temperature、top-k 和随机种子
下面这段把上面的手算过程写成代码,同时加上 top-k 截断,方便自己改参数观察分布变化。
import numpy as np def softmax_with_temperature(logits, temperature): """logits: 一维打分数组;temperature: 必须大于 0""" z = np.asarray(logits, dtype=np.float64) / temperature z = z - z.max() # 减去最大值,防止 exp 溢出 e = np.exp(z) return e / e.sum() def sample_top_k(logits, temperature=0.8, k=3, seed=0): probs = softmax_with_temperature(logits, temperature) order = np.argsort(probs)[::-1] # 概率从大到小排序 keep = order[:k] # 只保留前 k 个候选 masked = np.zeros_like(probs) masked[keep] = probs[keep] masked = masked / masked.sum() # 截断后必须重新归一化 rng = np.random.default_rng(seed) # 固定种子,结果可复现 return rng.choice(len(probs), p=masked), masked logits = [3.0, 2.0, 1.0, 0.5] for t in (0.2, 0.8, 1.5): print(t, softmax_with_temperature(logits, t).round(4))逻辑说明:softmax 里先做z - z.max()是数值稳定性处理,因为 exp(3.0/0.2) 已经是六位数,温度再低一点直接溢出,减掉最大值不改变最终概率比例,但把指数压回安全区间。argsort倒序取前 k 个,是把“只在最可能的几个词里挑”这个工程约束落下来,避免极小概率的长尾词被抽中造成语句崩坏。截断后重新归一化这一步最容易漏,漏掉的话rng.choice会直接报概率和不等于 1 的错。
参数说明:temperature决定分布陡峭程度,取值越小越保守;k决定候选池大小,k=1等价于贪心解码;seed只是让实验可复现,线上服务里通常不固定。把t分别设成 0.2、0.8、1.5 跑一遍,打印出来的数字应该和 3.1 的表格对得上,对不上就说明归一化或者缩放的位置写错了。
3.3 参数怎么定:一张按任务分的取值表和三个真踩过的坑
温度没有万能值,但按任务类型分档是靠谱的做法:
| 任务类型 | 温度 | top-p / top-k | 原因 |
|---|---|---|---|
| 结构化抽取、字段填充 | 0~0.3 | top-k=1 或 top-p=0.9 | 只要最可能的那个答案,格式稳定优先 |
| 代码补全、表达式生成 | 0.2~0.5 | top-p=0.95 | 允许少量变通,但不能引入语法噪声 |
| 客服问答、知识答复 | 0.5~0.8 | top-p=0.9 | 主体确定,措辞需要一点自然度 |
| 文案、头脑风暴 | 0.9~1.2 | top-p=0.95 | 需要跳出最高频表达 |
| 创意发散、造词 | 1.2 以上 | 慎用 top-k | 多样性换准确性,必须人工筛 |
三个坑,都是血泪经验换来的。
第一个,temperature=0不等于确定性输出。不少推理实现会把 0 替换成一个极小值走采样路径,同一输入跑两次仍然可能不一样。要确定性就自己取 argmax,别指望参数名。
第二个,top-k 和 top-p 同时开,会双重截断。先按 p 截掉长尾,再按 k 砍一刀,候选池可能小到只剩一两个词,温度调高也没用,表现为“怎么调都像复读机”。
第三个,温度调高之后重复率不降反升。原因是模型进入了一个高频循环,越采样越强化。这种情况不是温度的问题,是上下文里已经出现了循环模式,需要换提示词或者清空部分历史,光拧温度只会更乱。
4. 预训练、微调与对话状态:讲义里的流程图怎么落到工程参数上
4.1 预训练和微调各改什么,别把两件事混成一件事
讲义把训练拆成几段:先在海量文本上做预训练,学习“给定前文,下一个词该是什么”;再在特定任务的数据上做微调,把通用的语言能力往具体场景上掰;最后通过反馈把回答的偏好调过来。这三段经常被混着讲,实际差别很大。
| 阶段 | 数据形态 | 优化目标 | 改动范围 | 能改什么 | 改不了什么 |
|---|---|---|---|---|---|
| 预训练 | 无标注长文本 | 下一个词的交叉熵 | 全部参数 | 语言规律、世界知识的粗分布 | 具体任务格式、礼貌程度 |
| 监督微调 | 指令与答复成对 | 答复部分的交叉熵 | 全参或部分参数 | 输出格式、任务习惯 | 补不进训练数据里没有的事实 |
| 偏好对齐 | 同一问题的多个回答及排序 | 让更优回答的概率上升 | 通常只调一小部分参数 | 语气、拒答边界、有用性 | 依然不能保证事实正确 |
看这张表能得出一个实用判断:如果问题是“格式不对、答非所问”,微调是有效手段;如果问题是“事实错误”,微调只能缓解,根治要靠检索或者外部工具。很多人拿着幻觉问题去做微调,最后发现模型把错误答案背得更熟了,这就是阶段选错。
落到操作上,微调的数据准备我一般按这个顺序走:
- 先定输出格式,把目标答复写成固定模板,字段顺序不能变;
- 同一类任务至少准备几十到上百条同构样本,太少模型学不到模式;
- 训练集和验证集按任务切分,别按样本随机切,否则同一模板会同时出现在两边,验证指标虚高;
- 保留一批“不该回答”的反例,用来约束拒答边界;
- 上线前用固定温度(比如 0.2)跑一遍全集,把格式错误率单独统计。
4.2 上下文理解:模型不是记住了对话,是每次都被重新喂了一遍
讲义里讲“上下文理解”时,容易被读成模型有记忆。实际机制朴素得多:每一轮请求,历史对话都会被拼进输入,和当前问题一起送进模型,模型在窗口内看到多少,就“记得”多少。超出窗口的部分,对它来说等于不存在。
这个认知直接决定工程上的历史管理策略。按中文对话估算,一轮问答大致占用几百个 token,二十轮下来就是几千,很容易顶到窗口边缘。常见做法有三种:
| 策略 | 做法 | 适合场景 | 代价 |
|---|---|---|---|
| 滑动窗口 | 只保留最近 N 轮 | 闲聊、连续追问 | 早期约定会被忘掉 |
| 摘要压缩 | 把旧对话压成一段要点再拼进去 | 长任务、多轮需求澄清 | 摘要本身可能丢关键细节 |
| 关键信息固化 | 把用户设定、约束单独抽成系统提示 | 客服、表单填报 | 需要额外维护抽取逻辑 |
我一般会把“用户明确说过的约束”单独固化,不放对话历史里,因为这类信息一旦被滑窗挤掉,模型就会开始自作主张。相反,寒暄类内容压掉无所谓。判断标准很简单:这条信息丢了之后,回答会不会跑偏。
4.3 一个具体例子:符号表达式的等价写法怎么让模型学会
讲义里举了个符号计算的例子:a + bx + cx²和多层嵌套的a + b/c + x·sin(x),两者结构差别很大,但都属于“表达式”这一类输入。想让模型理解这种等价关系,不是靠堆通用语料,而是靠针对性的样本。
按讲义思路落到实操,大概是这样几步。第一步,构造同构样本:给同一批数学对象准备两种写法,一种是展开式,一种是嵌套式,让模型看到“这两种写法指向同一个东西”。第二步,固定输出格式,要求模型只输出规范化后的表达式,不要解释。第三步,把温度压到 0.3 以下,减少格式漂移。第四步,用一批没见过的表达式做验证,检查的是“结构是否对”,不是“字符是否一模一样”。
这里有个细节值得注意:这类任务的难点不在计算,在解析。模型不需要真的做符号运算,它要做的是把不同写法规约到同一种表示。所以样本里的括号层级、运算符优先级必须覆盖全,缺了哪一类,模型遇到就会翻车。我见过一个案例,训练样本里全是二元运算,上线后遇到三元嵌套就乱套,补了两百条三层嵌套样本才压住。
5. 避坑与排查:读这份讲义最容易翻车的五个地方
5.1 三个概念坑:把比喻当机制,后面全歪
坑一:把“预测下一个词”理解成“检索知识库”。
现象:读完之后仍然觉得模型内部存了一份事实清单,遇到答错就归结为“资料没更新”。 原因:讲义强调的是条件概率建模,事实是压缩在参数里的统计规律,不是可检索的记录。 解决:遇到事实类问题,默认它可能编,重要结论必须外部核对;工程上直接接检索,把事实来源从参数里挪出来。
坑二:把讲义里的词表规模当成当前模型的真实词表。
现象:拿着“4 万词表”去估算自己的 token 消耗,结果偏差很大。 原因:那是讲义为了讲清楚组合爆炸给的一个示意口径,不同模型的切分方式完全不同,中文尤其差得多。 解决:token 估算不要靠推算,直接拿目标模型的分词器对真实文本跑一遍,用实测值做容量规划。
坑三:以为温度低就不会出错。
现象:把温度调到 0,答案稳定了,但错的地方一模一样,甚至还更自信。 原因:温度只调整概率分布的陡峭程度,不改变 logits 本身,知识错误和推理错误都不受它影响。 解决:温度用来控格式和稳定性,正确性靠检索、校验、多轮交叉验证,两件事分开管。
5.2 两个使用坑:参数和版本,最容易照抄出错
坑四:以为模型“记得”上一轮说过的话。
现象:多轮对话到第十几轮,前面约定的格式突然失效。 原因:历史被拼进输入,窗口一满就被截断,超出的部分模型根本看不见。 解决:把关键约束固化成系统提示,历史做滑动窗口或摘要压缩,并在长对话里定期重述约束。
坑五:把讲义里描述的产品能力当成当前规格照搬。
现象:拿着旧讲义的结论去写方案,上线后发现行为和描述对不上,白白返工。 原因:这份材料的时间背景是 2023 年,机制层面的内容长期有效,但具体能力边界、参数默认值、接口行为都在变。 解决:机制部分放心学,能力部分以自己实测为准,任何“它能做到某件事”的判断都补一个小样本验证再写进方案。
这五个坑里,前三个是理解偏差,后两个是使用偏差。前者靠回头重读 2-gram 和温度那两节就能纠,后者只能靠实测。我的习惯是:任何要写进设计文档的结论,先在本机跑十组样本,能复现再落笔,不能复现就标成待验证,别当成事实往下传。
6. 三步自检法:把讲义里的结论变成能在本机跑出来的数字
读完不等于会用,判断有没有真读懂,我一般用三步自检,每步都要求跑出一个具体数字,跑不出来就是没懂。
| 自检项 | 用什么验证 | 通过标准 |
|---|---|---|
| 条件概率与稀疏 | 用 2.2 的小语料统计词对频次 | 能说清为什么“语言 → 模型”会被算成 1.0,以及平滑后变成多少 |
| 温度的影响 | 用 3.2 的代码跑四组 logits | 打印出的概率与手算表三列对得上,误差在小数点后三位内 |
| 上下文的边界 | 构造一段超长历史,观察约束是否失效 | 能定位到第几轮开始丢约束,并给出截断或固化方案 |
第一步的脚本很短,把 2.2 的语料直接喂进去,验证手算没算错:
from collections import Counter corpus = [ ["<s>", "chatgpt", "是", "一个", "语言", "模型", "</s>"], ["<s>", "chatgpt", "是", "一个", "对话", "系统", "</s>"], ] bigram = Counter() unigram = Counter() for sent in corpus: for i in range(len(sent) - 1): bigram[(sent[i], sent[i + 1])] += 1 # 词对频次,做分子 unigram[sent[i]] += 1 # 前词频次,做分母 for (prev, nxt), c in bigram.items(): print(prev, nxt, c, unigram[prev], round(c / unigram[prev], 4))逻辑说明:外层遍历句子,内层只走到倒数第二个位置,避免越界;分子只在成对时累加,分母只统计作为“前词”出现过的次数,两个计数器不能合并成一个。参数说明:语料规模、平滑系数、边界符都可以改,改完看输出怎么变,比看讲解印象深得多。跑完你会看到“语言 → 模型”那条确实是 1.0,这就是稀疏的现场。
第二步是拿三个不同的温度值各跑一遍,把打印结果和 3.1 的表格并排放,对不上就回头查归一化那行代码。这一步是照妖镜,能立刻发现自己是不是把温度乘在了 exp 之后。第三步最容易被跳过,但它最有用:随便找一个多轮对话的日志,数一数从第几轮开始,前面说过的格式要求不再生效,然后把那个轮次换算成 token 数,你就能算出自己这套提示词的安全余量。
从那以后我每次评估一个模型能不能接活,都强制走一遍这三步:先在自己的语料上复算一次概率,再用固定种子跑三档温度,最后用真实日志压一次上下文边界。三步都过了,才敢在方案里写“可用”。这份讲义的真正价值,不是给你一堆结论去背,而是给出了一条每一步都能验算的链路,链路通了,后面换任何模型都不慌。希望帮到你。
本文还有配套的精品资源,点击获取