从算法到人工智能 · 第 26 课:大语言模型——Transformer 长成 GPT
上一课我们搭好了 Transformer 的骨架。但一个"骨架"本身不会说话——它只会把输入"重新表示"一下。那 ChatGPT 到底是怎么做到"你问一句、它像人一样回一句"的?
答案藏在三个关键点里:
- 自回归生成:一个字一个字地"续写"。
- 预训练:在几乎整个互联网的文本上,学"下一个字是什么"。
- 缩放(scaling):模型越大、数据越多、算力越强,能力越强。
这一课,我们把 GPT 的"引擎"拆开看清楚。
一、GPT 到底在做什么?——预测下一个字
GPT 的核心任务简单到不可思议:给定前面所有的字,预测下一个字是什么。
比如输入 "The cat sat on the",模型输出一个概率分布,告诉你下一个字最可能是 "mat"(也可能 "floor"、"roof"……)。
输入:The cat sat on the ___
输出:下一个字的概率分布
mat 0.52
floor 0.19
roof 0.12
...就这么一件事。 但这件事一旦做得足够好,就会涌现出惊人的能力:会翻译、会写作、会推理、会写代码。因为"续写"逼着模型去理解语言、世界知识和逻辑。
二、先过第一关:把文字变成数字(Tokenization)
模型只能算数字,所以第一步要把文字切成一串token(词元),每个 token 映射成一个整数。
text = "the cat sat on the mat"
# 1. 词级切分
words = text.split()
print("词级 tokens:", words)
# ['the', 'cat', 'sat', 'on', 'the', 'mat']
# 2. 字符级切分(更简单,演示用)
chars = sorted(set(text))
stoi = {c: i for i, c in enumerate(chars)} # 字符 → 数字
itos = {i: c for i, c in enumerate(chars)} # 数字 → 字符
ids = [stoi[c] for c in text]
print("字符 ids:", ids)
print("解码回来:", ''.join(itos[i] for i in ids))
# 还原成原文 "the cat sat on the mat"真实的大模型用的是子词(subword)切分(如 BPE),比"词"和"字符"折中:既能处理生僻词,又不会切得太碎。但原理一样——把文字变成一串整数,喂给神经网络。
三、自回归生成:一个字一个字往外"蹦"
拿到"预测下一个字"的模型后,怎么生成一整句话?用自回归(autoregressive):
1. 给定开头 "从前"
2. 预测下一个字 → "有"
3. 把 "从前有" 当新输入,预测下一个 → "座"
4. 把 "从前有座" 当新输入,预测下一个 → "山"
5. ……一直生成到结束每生成一个字,就把它拼回输入里,再预测下一个字。 这就是 ChatGPT 打字"一个字一个字冒出来"的原因。
温度(temperature):控制"创造力"
"预测下一个字"其实给的是概率分布。怎么从分布里挑一个字?这里有个关键参数——温度:
import numpy as np
def softmax(x):
x = x - np.max(x)
e = np.exp(x)
return e / e.sum()
def next_probs(logits, temperature=1.0):
# temperature 越高,分布越"平";越低,分布越"尖"
return softmax(logits / temperature)
logits = np.array([2.0, 1.0, 0.5, 0.2, 0.1]) # 5 个候选词的原始分数
for T in [0.5, 1.0, 2.0]:
print(f"温度 T={T}: {next_probs(logits, T).round(3)}")跑出来你会看到:
| 温度 | 效果 | 用在 |
|---|---|---|
| 低(如 0.2) | 分布尖锐,几乎总选最可能的词 → 保守、确定 | 写代码、事实问答 |
| 高(如 2.0) | 分布平滑,冷门词也有机会 → 多样、有创意 | 写诗、头脑风暴 |
温度就是"创造力旋钮":调低就稳妥,调高就放飞。
采样策略:温度之外,还有 top-k 和 top-p
温度是"缩放"概率分布,但实际部署模型时,光有温度不够。因为即使温度很低,模型仍然有机会抽到一个极冷门的词,导致胡言乱语。于是有了两个更"硬"的截断手段:
① Top-k 采样:每步只从"概率最高的 k 个候选词"里挑,其余直接砍掉。比如 top-k=50,就只看前 50 个最可能的词。
② Top-p(核采样 / nucleus sampling):每步从"概率累加到 p 为止"的那批词里挑。比如 top-p=0.9,就把最可能的词一个个累加,直到加起来 ≥90%,剩下的全砍掉。候选数量是动态的——分布尖时就挑几个,分布平时就挑很多。
import numpy as np
def sample(logits, temperature=1.0, top_k=None, top_p=None):
probs = np.exp(logits/temperature)
probs = probs / probs.sum()
if top_k: # 只留前 k 个
idx = np.argsort(probs)[-top_k:]
probs = np.array([p if i in idx else 0 for i, p in enumerate(probs)])
probs = probs / probs.sum()
if top_p: # 累加到 p 为止
order = np.argsort(probs)[::-1]
cum = np.cumsum(probs[order])
keep = order[cum <= top_p]
mask = np.zeros_like(probs, dtype=bool); mask[keep] = True
probs = np.where(mask, probs, 0); probs = probs / probs.sum()
return np.random.choice(len(probs), p=probs)体会:真实的大模型生成 = 温度缩放 + top-k/top-p 截断 + 随机采样,三者配合。这是"让 AI 说话既有创意、又不发疯"的工程关键。
四、预训练:在互联网上"读"一遍世界
GPT 是怎么学会"预测下一个字"的?预训练(pre-training):
收集海量文本(网页、书籍、代码、对话……),让模型反复玩"猜下一个字"的游戏。猜错了就调参数(梯度下降,第 22 课学的),猜对越多,模型对语言、事实、逻辑的把握就越强。
这个阶段不需要人工标注——因为"下一个字"就在文本里,天然是标准答案。这叫自监督学习:数据自己给自己出题。
预训练完的模型叫基座模型(base model),它已经"知道"很多,但还不太会"聊天"。
五、微调与对齐:从"会续写"到"会聊天"
基座模型只会续写,你问它"1+1=?",它可能续写成"1+1=2 是小学数学……"(因为它见过无数类似文本)。但它不一定"听话"。
于是有了两步:
| 阶段 | 做什么 | 目的 |
|---|---|---|
| 监督微调(SFT) | 用"问题-标准答案"数据对训练 | 学会"问答"格式 |
| RLHF 对齐 | 让人给多个回答排序,训练奖励模型,再优化 | 让回答有用、诚实、无害 |
这就是为什么 ChatGPT 用起来"顺"——它被额外训练成了"会聊天"的样子。
六、LoRA 与 PEFT:花小钱微调大模型
上一节说微调(SFT)能教模型"会聊天"。但问题来了:GPT 这类模型有上千亿参数,全量微调意味着要更新所有参数、存好几份完整模型——显存和算力都烧不起,普通人根本玩不动。
于是有了 PEFT(Parameter-Efficient Fine-Tuning,参数高效微调):只动一小部分参数,也能达到接近全量微调的效果。
LoRA:冻结大模型,只训练"补丁"
PEFT 里最流行的是 LoRA(Low-Rank Adaptation,低秩适配)。它的思路特别妙:
大模型本身的权重全部冻结(不动)。在旁边挂两个很小的矩阵 A、B,让它们学一个"增量"。推理时把增量加回原权重:W + A×B。
因为 A、B 很小(秩 r 很低,常取 8、16),需要训练的参数可能只有原模型的 0.1%,显存和算力大幅下降。
原权重: W (冻结,不更新)
增量: ΔW = A × B (A、B 很小,只训练这两个)
新权重: W + ΔW = W + A × B# LoRA 核心思路(概念示意)
# 原线性层: y = W @ x # W 形状 (d, d),冻结
# 挂两个低秩矩阵: A (d, r)、B (r, d),r 远小于 d
delta = B @ A @ x # 低秩增量
y = W @ x + delta # 冻结的 W 加上增量为什么 LoRA 有效
直觉:大模型虽然参数多,但真正需要"适应新任务"的信息量没那么大——用一个低维的"补丁"就能装下。冻结主模型、只调补丁,既省钱又不破坏模型已有的能力。
PEFT 家族(知道名字即可)
| 方法 | 思路 |
|---|---|
| LoRA | 冻结权重,加低秩矩阵 A×B |
| Adapter | 在层之间插小模块,只训这些小模块 |
| Prefix Tuning | 只训练一段"前缀向量" |
它们共同的信念:微调不必动全量参数。
一句话记住
- 全量微调:贵,普通玩家玩不起。
- PEFT:只动一小撮参数,省钱省显存。
- LoRA:最流行的 PEFT,冻结主模型 + 训练低秩补丁 A×B,参数量可降到 0.1%。
这也是为什么现在开源社区能"人手一个大模型微调"——LoRA 让微调门槛大幅降低。
七、缩放定律:为什么"大"就是强
GPT 系列最重要的一个发现,叫缩放定律(scaling law):
模型参数、训练数据、计算量,这三样越大,模型能力就越强,而且这种提升是可预测、平滑的。
GPT-1(1.17 亿参数)→ GPT-2(15 亿)→ GPT-3(1750 亿)→ GPT-4(未公开,传闻上万亿)。能力随规模一路上涨,甚至涌现出小模型没有的能力(in-context learning、推理)。
记住:Transformer 这个架构本身没大变,是"规模"带来了质变。
八、上下文窗口与提示工程
大模型没有"记忆",它每次只看你喂给它的上下文(context)。上下文能装多少 token,叫上下文窗口。
- 早期 GPT-3 是 2048 token,现在动辄几十万 token。
- 所以和模型交流靠提示(prompt):把你想要的信息、格式、例子都写进提示里。
提示工程就是"把话说清楚",让模型在上下文里得到足够信息、按你要的方式回答。这不神秘——本质是通过上下文引导自回归生成。
九、复杂度与要点
| 概念 | 说明 |
|---|---|
| Tokenization | 文字 → 整数序列,子词切分 |
| 自回归 | 每步预测下一个 token,拼回输入再预测 |
| 温度 | 控制采样多样性,越低越确定 |
| 预训练 | 海量文本上"猜下一个字",自监督 |
| 缩放定律 | 参数/数据/算力越大,能力越强 |
十、动手时间 🎯
实验 1:手写一个"字符级"小模型
跑一个极简的自回归演示——用字符级 n-gram 猜下一个字符:
text = "the cat sat on the mat the cat sat on the floor"
# 统计每个字符后面最常跟的字符
from collections import defaultdict, Counter
next_chars = defaultdict(Counter)
for i in range(len(text) - 1):
next_chars[text[i]][text[i+1]] += 1
# 从 't' 开始,生成 20 个字符
current = 't'
result = current
for _ in range(20):
if not next_chars[current]:
break
nxt = next_chars[current].most_common(1)[0][0] # 贪心选最常见
result += nxt
current = nxt
print("生成:", result)体会:这已经是"自回归"的雏形了——每步根据上文选下一个字符。真实 GPT 用的是神经网络 + 概率采样,但套路一模一样。
实验 2:感受温度
跑第三节代码,改 temperature 从 0.1 到 5.0,观察概率分布怎么从"尖"变"平"。这就是 ChatGPT 的"creative"旋钮。
实验 3(挑战):调用真实的大模型 API
如果你有 OpenAI 兼容的 API key,试着调用一次补全接口,把 temperature 设成 0 和 1 各问一次,对比回答的确定性和多样性。
十一、小结
- GPT = 预测下一个字:给定上文,输出下一个 token 的概率分布,然后自回归地一个字一个字生成。
- 预训练 + 微调:先在互联网上"猜字"学知识(自监督),再用问答数据和人类反馈学"会聊天"。
- 缩放定律:Transformer 架构没大变,是"规模"带来了智能的涌现。