在 上一篇文章 中,我们从零开始实现了一个 mini-GPT
,完成了以下内容:从 Tokenization
(分词) 到 Transformer
解码器 ,逐步拆解了 LLM
的大脑;完成了 mini-GPT
的训练 ,并成功让它生成了第一段文字;甚至还拿到了一个 Colab Notebook
彩蛋 ,可以随时实验和截图。在这篇下篇文章里,我们将针对这些问题,继续深入:
五、推理与生成
前面我们已经完成了模型的训练。接下来,就是让它"开口说话"了。 这一步就叫 推理(Inference
) 或 生成(Generation
) 。
5.1 训练 vs 推理:有什么不同?
- 训练:模型知道"前文 + 正确答案",目标是调整参数,让预测越来越准。
- 推理(生成) :只有"前文",没有答案。模型需要自己接龙:预测下一个
token
,再接着预测下一个......
换句话说:训练是在学,推理是在用。
5.2 推理的基本逻辑
推理的过程就像接龙游戏:
- 给定一个起始文本(
prompt
)。 - 模型预测下一个
token
的概率分布。 - 根据采样策略选一个
token
。 - 把它接到文本末尾。
- 重复步骤 2--4,直到达到最大长度或遇到停止条件。
5.3 generate
函数实现
下面是一版通用的 generate
函数,支持温度、top-k
、top-p
、惩罚项和停止条件。为了让逻辑更清晰,我们在代码中分步写了注释。
python
import torch
@torch.no_grad()
def generate(model, idx, max_new_tokens, temperature=1.0, top_k=None, top_p=None,
freq_penalty=0.0, pres_penalty=0.0, stop_strings=None):
"""
idx: 初始输入 (prompt),形状 [1, T]
max_new_tokens: 最大生成长度
"""
device = next(model.parameters()).device
generated = idx.clone().to(device)
token_counts = {} # 用来统计 token 出现次数
for _ in range(max_new_tokens):
# 1. 取最近 block_size 个 token 作为上下文
idx_cond = generated[:, -model.block_size:]
# 2. 前向传播,取最后一个位置的 logits
logits, _ = model(idx_cond)
logits = logits[:, -1, :] # 只取最后一个 token 的预测分布
# 3. 温度缩放
if temperature != 1.0:
logits = logits / temperature
# 4. 频率/出现惩罚,避免复读机
for t, count in token_counts.items():
logits[0, t] -= freq_penalty * count + pres_penalty
# 5. 转为概率分布
probs = torch.softmax(logits, dim=-1)
# 6. top-k 策略:只保留前 k 个高概率 token
if top_k is not None:
v, ix = torch.topk(probs, top_k)
probs = torch.zeros_like(probs).scatter_(1, ix, v)
probs = probs / probs.sum()
# 7. top-p 策略:只保留累积概率 <= p 的 token
if top_p is not None:
sorted_probs, sorted_idx = torch.sort(probs, descending=True)
cumulative_probs = torch.cumsum(sorted_probs, dim=-1)
mask = cumulative_probs <= top_p
# 保证至少保留一个 token
mask[..., 0] = True
probs = torch.zeros_like(probs).scatter_(1, sorted_idx, sorted_probs * mask)
probs = probs / probs.sum()
# 8. 抽样得到下一个 token
next_token = torch.multinomial(probs, num_samples=1)
# 9. 更新生成序列
generated = torch.cat((generated, next_token), dim=1)
# 10. 更新 token 计数(用于惩罚项)
t = next_token.item()
token_counts[t] = token_counts.get(t, 0) + 1
# 11. 停止条件
if stop_strings is not None:
decoded = decode(generated[0].tolist()) # 或 decode_bpe
for s in stop_strings:
if decoded.endswith(s):
return generated
return generated
5.4 参数详解(+ 推荐组合)
-
temperature
(温度) :控制随机性。- 小(
0.7
):更稳定 → 像背课文。 - 大(
1.2
):更发散 → 更有创意。
- 小(
-
top-k
:只在概率最高的k
个token
中挑,常用:k=50
。 -
top-p
(核采样) :动态控制候选集合,只保留累计概率≤p
的token
,常用:p=0.9
。 -
freq_penalty / pres_penalty
:控制"复读机"。freq_penalty=0.5
:出现越多,惩罚越大。pres_penalty=0.5
:只要出现过,就扣分。
-
stop_strings
:设置终止条件(如"\n\n"
)。 -
max_new_tokens
:限制最大生成长度,避免无限输出。
👉 常用配置推荐:
- 稳定输出 :
temperature=0.7, top_k=50
- 创意输出 :
temperature=1.2, top_p=0.9, freq_penalty=0.5
5.5 小实验:文本生成
假设我们已经训练过模型,现在让它生成一些句子。
python
# 假设我们用字符级分词
prompt = "Once upon a time"
idx = torch.tensor([encode(prompt)], dtype=torch.long).to(device)
# Sample 1:偏确定
out = generate(model, idx, max_new_tokens=100, temperature=0.7, top_k=50)
print("=== Sample 1 ===")
print(decode(out[0].tolist()))
# Sample 2:更有创意
out = generate(model, idx, max_new_tokens=100, temperature=1.2, top_p=0.9, freq_penalty=0.5)
print("=== Sample 2 ===")
print(decode(out[0].tolist()))
输出示例:
txt
=== Sample 1 ===
Once upon a time, there was a small language model. It tried to read books, tell stories, and learn from text.
=== Sample 2 ===
Once upon a time, there was a curious model. It played with symbols, invented new phrases, and even wrote in Chinese: 模型学习文字。
5.6 如何观察差异?
- 低温度 +
top-k
→ 输出更稳定,像复述语料。 - 高温度 +
top-p
→ 输出更自由,可能带来惊喜。 - 加惩罚项 → 避免复读机,让文本更有变化。
六、常见问题与报错修复
当你在 Google Colab
或本地训练 mini-GPT
时,可能会遇到各种报错或奇怪现象。这里可以帮你快速定位问题、理解原因、找到解决办法。
6.1 随机数范围错误(RuntimeError
)
常见报错:
bash
RuntimeError: random_ expects 'from' to be less than 'to', but got from=0 >= to=-91
原因 :在 get_batch
里,我们会随机选一个起点 ix
。如果 语料太短 ,而 block_size
设置得太大,就会出现 "可选范围是负数" 的情况。
解决方法:
- 扩充语料 :保证文本长度远大于
block_size
。 - 减小
block_size
:比如从 128 改为 32。
预防建议 :一般保证 len(text) ≥ 10 × block_size
,训练才比较稳定。
6.2 AMP 警告(FutureWarning
)
常见警告:
bash
FutureWarning: `torch.cuda.amp.GradScaler` is deprecated.
Please use `torch.amp.GradScaler('cuda', args...)` instead.
原因 :PyTorch
新版本更换了 AMP
(自动混合精度)的 API
。
解决方法:
把原来写的:
python
scaler = torch.cuda.amp.GradScaler(enabled=(device=="cuda"))
改成:
python
scaler = torch.amp.GradScaler("cuda", enabled=(device=="cuda"))
同样,把:
python
with torch.cuda.amp.autocast(enabled=(device=="cuda")):
改成:
python
with torch.amp.autocast("cuda", enabled=(device=="cuda")):
预防建议 :每次升级 PyTorch
版本,注意官方 release notes
。
6.3 CUDA
显存不足(Out of Memory
, OOM
)
常见报错:
bash
RuntimeError: CUDA out of memory
原因 :模型太大,或者 batch_size
太大,超出了 GPU
显存。
解决方法:
- 减小
batch_size
,比如从 64 改成 16。 - 减小 模型参数 ,比如
n_layer=2, n_head=2, n_embd=128
。 - 使用 混合精度训练(
AMP
) ,减少显存占用。
预防建议 :先用小模型、小 batch
跑通流程,再逐渐加大规模。
6.4 验证集损失(val_loss
)过高
训练日志可能是:
txt
step 100: train_loss=0.8 | val_loss=9.0
原因:
- 模型过拟合,只会背训练集,不会泛化。
- 验证集太小,
loss
波动大。 - 小模型本身泛化能力有限。
解决方法:
- 扩充训练语料(最好几
MB
起步)。 - 使用
BPE
分词 ,减少token
数量,提高效率。 - 调整数据划分,比如 80% 训练 / 20% 验证。
预防建议 :不要在意小模型的绝对 val_loss
数值,更关注"是否在下降"。
6.5 模型输出"复读机"
模型生成时不断重复同一句:
txt
Hello world! Hello world! Hello world! ...
原因:
- 小模型 + 小语料,只学到最常见的模式。
- 温度太低,模型总是选最稳妥的答案。
- 没有加惩罚项,导致重复越来越多。
解决方法:
- 调高 温度(1.0 ~ 1.2)。
- 使用
top_p=0.9
或top_k=50
。 - 设置
freq_penalty=0.5
,减少重复。 - 提供更丰富的语料。
预防建议:一开始就别指望小模型输出"长篇大论",要么会复读,要么会"胡言乱语",这是正常现象。
6.6 BPE
训练失败(词表太大)
常见报错:
bash
RuntimeError: Vocabulary size too high (8000).
Please set it to a value <= 620.
原因:语料太小,支持不了这么大的词表。
解决方法:
- 把
vocab_size_bpe
改小(200 ~ 1000)。 - 设置
hard_vocab_limit=False
,让实际词表小于目标值也能训练成功。
预防建议 :语料量 ≤ 1 MB 时,建议 vocab_size
不要超过 1000。
6.7 生成结果和预期不符
输出结果和训练文本几乎一模一样,像在"背书"。
原因:
- 模型规模太小。
- 数据太少。
- 训练步数不足。
解决方法:
- 增加训练步数(比如从 1000 → 5000)。
- 增大模型(更多层数/头数/
embedding
)。 - 用更丰富的语料。
预防建议:小语料实验本来就是"背书" → "复读" → "慢慢有点新意"的过程,不必焦虑。
遇到问题时,先对照这里找原因,再去调整参数或数据。记住:大多数问题并不是"你写错代码",而是 小模型 + 小语料的正常局限。
七、实验结果与分析
跑完 mini-GPT
之后,很多同学都会问:
"我跑出来的结果怎么像是在背书,这算成功吗?"
答案是:是的,这已经是成功了! 因为在小语料 + 小模型的实验里,能输出通顺的句子,就说明模型结构正确、训练流程跑通。
记住:第一次跑 mini-GPT
的目标,不是生成"惊艳的小说",而是确认 LLM
的基本机制你理解了。
7.1 什么算是"成功"?
- 输出是 通顺的文本(即使和训练语料很像)。
- 结果 不是乱码(说明分词器没问题)。
- loss 在训练中 有下降趋势。
真正的"失败"信号:
loss
一直不下降。- 输出全是乱码。
7.2 常见的生成现象
① 复读机
输出内容反复,比如:
txt
Once upon a time, Once upon a time, Once upon a time...
原因:
- 小模型参数少,只能捕捉最简单的模式。
- 推理时温度过低,总是选"最安全"的答案。
改善方法:
- 提高 温度(1.0 ~ 1.2)。
- 使用
top_p=0.9
或top_k=50
。 - 加入
freq_penalty=0.5
,减少重复。 - 增加语料,让模型学会更多样的表达。
② 背课文
模型直接输出训练语料里的句子:
txt
The quick brown fox jumps over the lazy dog.
原因:
- 模型参数少,只能死记硬背。
- 数据太少,没法学到更复杂的规律。
改善方法:
- 增加训练语料(从几百 KB → 几 MB)。
- 增大模型容量(更多层数、更多
embedding
维度)。 - 使用更高级的采样策略(
top-p
+penalty
)。
③ 夹杂语言
输出中英文混合:
txt
Hello world! 模型学习文字。
原因:
- 分词器里同时包含中英文。
- 语料双语混合,模型在概率分布上"左右摇摆"。
改善方法:
- 如果目标是单语模型,就只用一种语言的语料。
- 如果想做双语模型,就需要更多中英文混合的数据,让模型学会"自然切换"。
④ 偶尔胡言乱语
有时输出会像这样:
txt
Hello world! Soks, ck字frn...
原因:
- 模型参数不足,概率分布不够平滑。
- 数据太少,模型没学会长程依赖。
改善方法:
- 增加训练步数(比如从 1000 → 5000)。
- 增加语料。
- 增大模型规模。
7.3 如何评估模型效果?
训练阶段
train_loss
持续下降 → 模型在学习。val_loss
先降后升 → 过拟合,需要更多数据或正则化。
生成阶段
- 文本通顺 → 学到语言模式。
- 不是乱码 → 分词器/模型结构正常。
- 有多样性 → 采样策略发挥作用。
- 能生成语料外的新组合 → 模型开始"创作"。
👉 小模型的评估不看"多强大",而是"是不是在正确地学习"。
7.4 为什么小模型容易复读?
- 参数太少 → 只能捕捉最简单的统计规律。
- 语料太小 → 没有足够的"语言多样性"。
- 分布太尖锐 → 训练中模型对某些
token
过度自信。 - 采样策略过保守 → 温度太低,总是选同一个答案。
7.5 示例结果解读
在前面对话里,你的输出是:
txt
=== Sample 1 ===
Once upon a time, there was a small language model...
语言是人类的工具, 也是思想的载体。
Once upon a time...
=== Sample 2 ===
Once upon a time, there was a small language model...
诗言志,歌咏言。语言是人类的工具...
- 英文部分能续写 → 学到了英文语料。
- 中文部分能生成 → 分词器支持多语言。
- 出现复读 → 符合小模型的特点。
结论:模型结构正确,训练流程跑通,结果符合预期。
7.6 如何从"背书"进阶到"写作文"?
要让模型从"背语料"进化到"创作新内容",需要三方面提升:
短期优化(实验友好)
- 增加训练步数(1000 → 5000+)。
- 使用更好的采样策略(
temperature
+top-p
+penalty
)。 - 扩充语料(比如几
MB
的小说或文章)。
长期优化(研究/实用方向)
-
加大模型:
n_layer=2 → 8
n_head=4 → 8
n_embd=128 → 512
-
加大语料规模 :从几
MB
→ 几GB
。 -
使用更强硬件:单卡 → 多卡训练。
小结
- 想理解原理 →
mini-GPT
就够了。 - 想做实验玩具 → 扩充语料 + 参数调优。
- 想接近真实
LLM
→ 需要大规模训练 + 硬件支持。
八、进阶优化
在前面,我们已经跑通了一个 mini-GPT
,它能背书、能复读、能生成简单的句子。但如果你希望它更聪明、更像"ChatGPT
",就需要进阶优化。下面我们从数据、模型、训练、推理到应用,一步步升级。
8.1 扩充语料:从几 KB
到几 MB
问题:我们之前只喂了几十行文本。就好比让小孩只看了十几页书,他只能重复里面的内容,当然会"复读机"。
改进方法:
-
用 更大的语料库 ,至少几
MB
起步。 -
语料来源:
- 公共版权的小说(如《西游记》、《哈利波特》英文版)。
- 新闻文章。
- 维基百科
dump
。 - 开源数据集(
TinyStories
、OpenWebText
)。
👉 经验法则:
- 语料越大 → 模型学到的模式越丰富。
- 语料越多样 → 生成的内容越自然。
建议:
- 初学者:找一本英文小说或几万字中文故事即可。
- 进阶用户:用几十
MB
的开源数据集。
8.2 增大模型容量
问题 :mini-GPT
只有几十万参数,太"小脑袋",只能学点皮毛。
改进方法:逐步增加参数。
参数 | 当前设置 | 推荐进阶 |
---|---|---|
n_layer |
2 | 4~12 |
n_head |
4 | 8 |
n_embd |
128 | 256~512 |
block_size |
64 | 128~512 |
⚠️ 注意:
- 模型一旦变大,需要更多显存。
Colab
免费版大约能撑到n_layer=6, n_head=8, n_embd=256
。
建议:
- 初学者:保持小模型,先理解机制。
- 进阶用户:逐步加大,看看效果如何变化。
8.3 长上下文:从 64 → 512+
问题 :我们的 block_size=64
,模型的"记忆力"只有一句话长。
改进方法:
- 增大
block_size
,比如 256 或 512。 - 使用
RoPE
(旋转位置编码) 代替传统位置embedding
,支持更长上下文。 - 确保训练片段长度 ≥
block_size
。
👉 好处:模型能记住更长的对话或文章,而不是"一句话记忆"。
建议:
- 初学者:64 就够用,训练快。
- 进阶用户:尝试 256+,体验更长的上下文建模。
8.4 提升训练稳定性
问题:小实验里训练可能会崩溃或不稳定。
我们已经用过:
- 学习率调度(
warmup
+cosine
)。 - 梯度裁剪。
进阶方法:
- 混合精度训练(
AMP
) :显存占用减半,速度更快。 - 梯度累积 :把多个小
batch
合成大batch
。 - 检查点保存 :定期保存模型,避免断电或
Colab
崩掉时损失进度。
8.5 采样策略再优化
训练好后,生成文字的"风格"主要靠采样策略控制。
常见方法:
- 贪心搜索:每次都选最可能的词 → 最安全,但容易复读。
Temperature
(温度) :调节随机性,温度越高越大胆。Top-k
:只在概率最高的k
个里选。Top-p
(核采样) :保留累计概率 ≤p
的词,更灵活。- 惩罚项 :
freq_penalty=0.5
避免复读,pres_penalty=0.3
鼓励换话题。
建议:
- 故事生成 → 高温度 +
top-p
。 - 问答 → 低温度 + 贪心搜索。
8.6 微调方法(Finetuning
)
如果你不想从零训练,而是基于大模型做"定制",可以用:
- 全量微调:更新所有参数 → 成本高。
LoRA
(低秩适配) :只加"外挂模块",只更新外挂,不改大脑 → 轻量。QLoRA
:先把大脑压缩(量化),再加外挂 → 更省显存。
建议:
- 初学者:不用管微调,先跑通小模型。
- 进阶用户:尝试
LoRA
,很多开源工具已经封装好。
8.7 让模型"更懂人类"
训练好的语言模型会说话,但不一定符合人类喜好。要让它"听话",需要:
SFT
(监督微调) :在人工标注的问答对上训练。RLHF
(人类反馈强化学习) :收集人类打分,用奖励模型指导生成。DPO
(直接偏好优化) :替代RLHF
,更高效,近期很火。
这正是 GPT-3
变成 ChatGPT
的关键步骤。
建议:
- 初学者:知道就好。
- 进阶用户:可以尝试小规模
SFT
。
8.8 部署与应用
训练好模型之后,如何用起来?
最小可行方案 : 在 Colab
写个 while True: input()
的循环,就能做命令行聊天。
进阶方法:
- 推理优化 :
KV Cache
(避免每次都重算上下文)。 - 模型压缩 :量化成
int8/int4
,节省显存。 - 服务化 :用
FastAPI/Gradio
包装成网页或接口。
小结
- 初学者目标:扩充语料,尝试采样参数调节,能体验模型的改进效果。
- 进阶用户目标 :扩大模型,尝试
LoRA
微调,做更长上下文训练,甚至部署成小应用。
记住:mini-GPT
的价值在于让你"看懂和跑通"。真正的大模型,需要更多数据、显卡和工程手段。
九、总结与学习路径
9.1 我们做了什么?
在这篇长文里,我们从零开始,走完了一个 mini-GPT
的完整实现流程 。到这里,你已经能自信地说:我理解了 LLM
的核心原理;我能从头写出一个简化版 GPT
;我能让它在 Colab
上跑起来,并生成文字。换句话说,你已经从"只是用 ChatGPT
的人",升级为"知道 ChatGPT
内部怎么运作的人"。
9.2 mini-GPT
给了我们什么?
很多人觉得大语言模型是个"黑箱"。但通过这次实践,你应该收获了三个关键词:
- 透明性:每一步代码都能看懂,模型不再神秘。
- 可控性 :调温度、调
top-p
,就能立刻看到输出风格变化。 - 成长性:知道如何从"小实验"一步步扩展到"大模型"。
这就像学乐器:一开始你只能弹《小星星》,但乐理你已经掌握了,接下来就是练习、扩展、进阶。
9.3 零基础读者的学习路径(三步法)
- 跑:先反复跑通本教程的代码,确保理解每个环节。
- 改 :修改参数(
block_size
、n_layer
、n_head
、 温度、top-p
),观察生成差别。 - 扩展:换语料(诗歌、小说、新闻),感受模型如何学习不同风格。
9.4 进阶读者的学习路径(五步法)
- 加数据 :从
KB
文本扩展到MB
级别小说或开源数据集。 - 加模型 :把层数、头数、
embedding
提高,体验模型"更聪明"的差别。 - 用框架 :学习
Hugging Face
,加载预训练权重,再在本地语料上微调。 - 轻量微调 :尝试
LoRA/QLoRA
,这是工业界常用的技巧。 - 对齐优化 :理解并尝试
SFT
、RLHF
、DPO
,知道为什么ChatGPT
更贴近人类需求。
结语
大语言模型看似庞大复杂,但本质就是:预测下一个 token
,然后通过数据和计算,把这种预测能力放大到"像人一样对话"。你今天跑的 mini-GPT
,也许笨拙、爱复读,但它的原理和 GPT-4
完全一致。
所以,从今天起: 你不再只是"用 LLM
的人",而是"理解 LLM
的人"。 你已经从观众席走上舞台,手里有了属于自己的"小型 ChatGPT
"。这,就是你进入 AI
世界的重要一步。
🎁 彩蛋:一键运行 Notebook
如果你不想从零复制粘贴代码,或者想直接体验完整的 mini-GPT
实现,我已经准备了一份 Google Colab Notebook
: