第五周是整个课程的分水岭——从「理解模型」跨到「生成模型」。
内容很密:BERT 为什么做不了生成、Causal Mask 的本质、SFT 到底改变了什么、五种采样策略、ICL 与 CoT、Scaling Law,最后还有 API 调用的工程实践(断路器模式)。
作业是写一个单向语言模型(week5/task/单向语言模型.py),把 Causal Mask 和自回归生成完整跑一遍。
一、BERT 的三宗罪
为什么不能直接拿 BERT 做生成?三个原因,一个比一个致命:
① 预训练目标不匹配
MLM 训练的是「完形填空」——给定双向上下文,填中间的洞。 生成任务需要的是「续写」——给定前文,预测下一个词。
② 注意力是双向的
BERT 里每个位置都能看到所有位置,包括它后面的词。生成时第 t 步不该看到 t+1 以后的内容,否则等于提前看答案。
③ 输出长度固定
MLM 的预测位置数 = 输入里 [MASK] 的个数,由输入结构决定。而真正的生成要模型自己决定:生成多长、什么时候停。
一句话:BERT 的架构和训练目标是为「理解」优化的,不是「生成」。
改造需要做三件事:预训练目标换成 next-token、禁止看到未来的词、一个一个往外输出。
二、Attention Mask:换个矩阵就换个模型
这是我这周觉得最漂亮的一个点。
Mask 作用在哪一步
① scores = Q·Kᵀ
② weights = softmax(scores/√d) ← Mask 加在这一步之前
③ output = weights·V
把不允许看的位置的 score 设为 −∞,过完 softmax 权重就趋近于 0,这个位置彻底看不见了。
直觉上,Mask 就是 Attention 的「权限控制」。
Mask 的本质是一个 L×L 矩阵
行 i = Query 位置,列 j = Key 位置
M[i][j] = 0 → 第 i 个 token 可以看见第 j 个
M[i][j] = 1 → 屏蔽(score = −∞)
然后是关键的一句:
同一个 Transformer 架构,换一个矩阵就能从「理解模型」变成「生成模型」。
全 0 → 完全双向 → BERT
下三角 → 因果生成 → GPT / Llama
自定义 → 前缀 / 局部 → T5 / GLM
Causal Mask 的实现
mask = torch.tril(torch.ones(L, L)) # j ≤ i 处为 1
scores = scores.masked_fill(mask == 0, -inf)第 i 行就是第 i 个 token 能看见的位置。每新生成一个 token,下面就多出一行。
自回归生成
「今天 天气」 → 前向一次 → 预测「真」
「今天 天气 真」 → 再前向 → 预测「好」
… 直到输出 <EOS> 才停
有个重要推论:Causal Mask 保证前文的 embedding 不受后续 token 影响,所以前面算过的表示是稳定可复用的——这正是 KV Cache 能成立的前提。
三、SFT:从「续写机」到「问答助手」
Base Model 到底学到了什么
它的训练目标只有一个:给定前 t 个 token 预测第 t+1 个。学到的是语料的分布规律。
所以有个很经典的现象:
输入:什么是人工智能?
输出:什么是机器学习?什么是深度学习?什么是神经网络?……
它没有回答,而是继续列问题。
原因很朴素——语料里 FAQ 标题、目录、问题集这类文本,问题后面接的经常还是问题。它在忠实地续写。
Base Model 是一个概率续写器,不是一个问答助手。
SFT 怎么做
数据:大量(指令, 回答)成对数据,只需几万到几十万条,远小于预训练的万亿 token。
用特殊 token 标注角色:
<|user|> … <|assistant|> … <|end|>
训练目标完全不变,仍然是 next-token prediction,没有引入任何新结构。
关键技巧:Label Mask
labels = [-100] * len(prompt) + input_ids[len(prompt):]user 部分只作为输入、不计算 loss;assistant 部分才计算 loss 并反向传播。-100 的位置被 ignore_index 跳过。
为什么?我们只想让模型学会「如何回答」,而不是让它背熟用户的提问。
两个阶段的信号密度对比:
| labels | 有效信号密度 | |
|---|---|---|
| 预训练 | input_ids 整体左移一位 | 100% |
| SFT | prompt 段全部 −100 | ≈ 50% |
SFT 到底改变了什么
| 没变 | 变了 |
|---|---|
| 模型架构(同一个 Transformer) | 输出分布(学会在指令后输出回答) |
| Causal Mask | 对话协议(角色轮换) |
| next-token 目标 | 停止行为(学会生成 `< |
| 大部分事实知识 | 回答风格(结构化、有条理) |
一句话记忆:知识来自预训练,回答的形式来自 SFT。
四、采样策略:五种
模型每步输出的是 logits(维度 = 词表大小 5万~15万)→ softmax → 概率分布 → 采样出 1 个 token。
两个极端都不好:永远选最大(确定但死板重复)、完全按概率随机(多样但长尾累积概率大,容易胡言乱语)。工程就是在中间找平衡。
| 策略 | 做法 | 适合 |
|---|---|---|
| Greedy | 每步取 argmax | 代码生成、数学推理 |
| Beam Search | 保留 k 条最优路径 | 机器翻译、摘要 |
| Temperature | softmax(logits/T) | 调节整体发散程度 |
| Top-K | 只从概率最高的 K 个里采 | 切长尾 |
| Top-P(Nucleus) | 取累积概率首次达 P 的最小集合 | 通用默认 |
Temperature 的方向:T↓ 分布更尖锐、更保守;T↑ 分布更均匀、低概率词也可能被选中。
Top-P 比 Top-K 好在哪:K 是固定数,不适配分布形态——尖锐分布 K=50 太宽松,平坦分布 K=50 又太紧。而 Top-P 的候选数会随分布自动调整:尖锐时只留 1~2 个,平坦时自动放宽到 10+ 个。
五、ChatGPT 改变了什么
课上用了一个比喻:弓箭与导弹,不是量变是质变。
| 传统 NLP / BERT | LLM | |
|---|---|---|
| 任务 | 一个模型 = 一个任务 | 一个模型 ≈ 任意任务 |
| 数据 | 必须有标注才能学 | 用自然语言描述即可 |
| 新任务 | 需重新训练 | 零样本泛化 |
| 使用者 | 需要 ML 工程师 | 普通人就能驱动 |
In-Context Learning
模型不更新任何权重,仅凭 Prompt 里的几个示例,就能学会全新任务。
传统微调:准备标注数据 → 改结构 → 反向传播 → 部署 → 换任务从头再来
ICL: 任务描述 + 几个示例写进 Prompt → 直接给出答案
三种形式:Zero-shot(不给示例)、One-shot(一个,显著提升格式遵从)、Few-shot(2~8 个,进一步提精度)。
它的革命性在于打破了「训练数据 = 能力」这条铁律。
Chain-of-Thought
在示例里带上中间推理步骤,引导模型展示完整推理过程。
为什么有效:
- 复杂问题分解 —— 避免一次跳跃到答案,每步只处理简单子问题
- 推理过程可纠错 —— 中间步骤生成出来后成为后续 token 的上下文,模型能自我校正
- 顺应自回归的工作方式 —— LLM 本就是逐 token 生成,写出推理链正合适
Zero-shot CoT 更神奇:只在 Prompt 末尾加一句「Let's think step by step」,模型就自动展开推理链。
有个关键限制:CoT 只在足够大的模型(>100B)上有效,小模型用 CoT 有时反而更差。
Scaling Law
L(N) = (Nc/N)^α α ≈ 0.076
参数量 N、数据量 D、算力 C 与 Loss 之间存在稳定幂律,而且跨越多个数量级都成立。
伴随而来的是涌现能力——某些能力在小模型上几乎为零,参数量超过某个阈值后突然出现,而不是平滑提升。
六、工程实践:API 的健康检测与断路器
这一节是我最意外的收获——没想到大模型课会讲这么工程的东西,但它确实是实际项目里绕不开的。
核心矛盾
LLM API 和普通 HTTP 服务一样会出故障:限流(429)、宕机、网络抖动、Key 失效。难点在于:
- 单次失败不能立刻切走(可能只是偶发抖动)
- 持续失败必须快速切走(避免每次都等超时)
- 切走后还要能自动恢复(不能永远用备用)
断路器的三态状态机
CLOSED(正常放行)
│ 连续失败 N 次
↓
OPEN(快速失败,不发任何网络请求)
│ 冷却 recovery_timeout 秒
↓
HALF_OPEN(只放行一条探针)
├─ 探针成功 → CLOSED
└─ 探针失败 → 回到 OPEN,重置冷却
核心价值是「快速失败 Fail Fast」——不等 API 超时。
生产推荐参数:
failure_threshold = 3~5 太小容易误触发,太大切换太慢
recovery_timeout = 60~120s 给对方足够的恢复时间
success_threshold = 2 防止偶发成功就过早关闭
为什么还需要主动探针
这个点很妙:断路器进入 OPEN 后,没有用户请求会到达这个 Provider,那段判断逻辑就不会被调用,它永远没机会转 HALF_OPEN。
所以要有个后台常驻任务定时探测。探针的三条设计原则:
- 轻量 ——
max_tokens=5,只验证可达性 - 有代表性 —— 用和真实请求相同的 endpoint 和 model,而不是
/health(有些 Provider 根本没这个接口) - 静默 —— 成功就更新状态,不打扰用户
这周的收获
1. Mask 是一个被低估的设计。
「换一个 L×L 矩阵,同一个架构就从理解模型变成生成模型」——这句话让我重新理解了 Transformer 的通用性。架构没变,变的是信息可见性的约束。
2. SFT 没有引入任何新东西。
我原以为对话能力是训出来的某种「新结构」,实际上架构、目标、Mask 全没变,只是换了数据格式 + 加了 Label Mask。能力的形式变了,能力的来源没变。
3. 工程和算法一样重要。
断路器那一节是纯后端知识(我以前在 Java 项目里用过类似的东西),但它出现在大模型课里恰恰说明:把模型用起来,一半的工作量在模型之外。这对我这种后端转过来的人反而是优势。
下周开始进入具体任务——文本分类,做七组对照实验。