从零实现 LLM(下):推理生成、常见问题与进阶优化

上一篇文章 中,我们从零开始实现了一个 mini-GPT,完成了以下内容:从 Tokenization(分词)Transformer 解码器 ,逐步拆解了 LLM 的大脑;完成了 mini-GPT 的训练 ,并成功让它生成了第一段文字;甚至还拿到了一个 Colab Notebook 彩蛋 ,可以随时实验和截图。在这篇下篇文章里,我们将针对这些问题,继续深入:


五、推理与生成

前面我们已经完成了模型的训练。接下来,就是让它"开口说话"了。 这一步就叫 推理(Inference生成(Generation

5.1 训练 vs 推理:有什么不同?

  • 训练:模型知道"前文 + 正确答案",目标是调整参数,让预测越来越准。
  • 推理(生成) :只有"前文",没有答案。模型需要自己接龙:预测下一个 token,再接着预测下一个......

换句话说:训练是在学,推理是在用

5.2 推理的基本逻辑

推理的过程就像接龙游戏:

  1. 给定一个起始文本(prompt)。
  2. 模型预测下一个 token 的概率分布。
  3. 根据采样策略选一个 token
  4. 把它接到文本末尾。
  5. 重复步骤 2--4,直到达到最大长度或遇到停止条件。

5.3 generate 函数实现

下面是一版通用的 generate 函数,支持温度、top-ktop-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 :只在概率最高的 ktoken 中挑,常用:k=50

  • top-p(核采样) :动态控制候选集合,只保留累计概率 ≤ptoken,常用: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.9top_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.9top_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 为什么小模型容易复读?

  1. 参数太少 → 只能捕捉最简单的统计规律。
  2. 语料太小 → 没有足够的"语言多样性"。
  3. 分布太尖锐 → 训练中模型对某些 token 过度自信。
  4. 采样策略过保守 → 温度太低,总是选同一个答案。

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
    • 开源数据集(TinyStoriesOpenWebText)。

👉 经验法则:

  • 语料越大 → 模型学到的模式越丰富。
  • 语料越多样 → 生成的内容越自然。

建议

  • 初学者:找一本英文小说或几万字中文故事即可。
  • 进阶用户:用几十 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 让模型"更懂人类"

训练好的语言模型会说话,但不一定符合人类喜好。要让它"听话",需要:

  1. SFT(监督微调) :在人工标注的问答对上训练。
  2. RLHF(人类反馈强化学习) :收集人类打分,用奖励模型指导生成。
  3. 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 零基础读者的学习路径(三步法)

  1. :先反复跑通本教程的代码,确保理解每个环节。
  2. :修改参数(block_sizen_layern_head、 温度、top-p),观察生成差别。
  3. 扩展:换语料(诗歌、小说、新闻),感受模型如何学习不同风格。

9.4 进阶读者的学习路径(五步法)

  1. 加数据 :从 KB 文本扩展到 MB 级别小说或开源数据集。
  2. 加模型 :把层数、头数、embedding 提高,体验模型"更聪明"的差别。
  3. 用框架 :学习 Hugging Face,加载预训练权重,再在本地语料上微调。
  4. 轻量微调 :尝试 LoRA/QLoRA,这是工业界常用的技巧。
  5. 对齐优化 :理解并尝试 SFTRLHFDPO,知道为什么 ChatGPT 更贴近人类需求。

结语

大语言模型看似庞大复杂,但本质就是:预测下一个 token ,然后通过数据和计算,把这种预测能力放大到"像人一样对话"。你今天跑的 mini-GPT,也许笨拙、爱复读,但它的原理和 GPT-4 完全一致。

所以,从今天起: 你不再只是"用 LLM 的人",而是"理解 LLM 的人"。 你已经从观众席走上舞台,手里有了属于自己的"小型 ChatGPT"。这,就是你进入 AI 世界的重要一步。


🎁 彩蛋:一键运行 Notebook

如果你不想从零复制粘贴代码,或者想直接体验完整的 mini-GPT 实现,我已经准备了一份 Google Colab Notebook

👉 点击这里直接运行 mini-GPT(Colab)

相关推荐
Juchecar8 小时前
PyTorch 的张量(Tensor)与 NumPy 的数组(Array)区别
人工智能
七牛云行业应用8 小时前
私有化存储架构演进:从传统NAS到一体化数据平台
大数据·人工智能·架构·云计算·七牛云存储
爱喝奶茶的企鹅8 小时前
Ethan独立开发新品速递 | 2025-09-02
人工智能·程序员
聚客AI8 小时前
🤖告别复杂粘合代码:LangGraph+OceanBase构建智能Agent蓝图
人工智能·llm·agent
爱喝奶茶的企鹅8 小时前
Ethan开发者创新项目日报 | 2025-09-02
人工智能
深度学习机器9 小时前
AI IDE如何构建高效代码索引?以一个MCP Server的开发过程进行阐述
llm·agent·cursor
前端开发工程师请求出战9 小时前
从“调旋钮”到“预训练”:一文看懂AI模型的“成长史”
人工智能
Ai工具分享9 小时前
如何用AI视频增强清晰度软件解决画质模糊问题
人工智能·音视频
Ronin-Lotus9 小时前
深度学习篇---ShuffleNet
人工智能·深度学习