最近在学习
GPT_teacher-3.37M-cn这个教学项目。它用约 337 万个参数实现了一个可以在普通电脑上训练、推理的中文 GPT。本文先不深入数学公式和模型内部细节,而是从全局视角整理:一份中文问答数据,经过哪些步骤,最终变成一个能够生成回答的模型。
目录
1. 为什么从一个小 GPT 项目开始
平时所说的大语言模型往往有几十亿甚至上千亿个参数,训练数据、计算资源和工程系统都非常庞大,很难由个人从头跑一遍。
GPT_teacher-3.37M-cn 把规模缩小到了教学级别:
- 模型只有 4 层 Transformer,约 337 万个参数;
- 上下文长度为 128 个 token;
- 使用约 600 条中文问答数据;
- 分词、训练、验证、推理和 Web 演示都包含在同一个项目中。
它和真正的通用大模型在能力上相差很远,但保留了 GPT 的核心工作方式。因此,学习它的价值不在于训练出一个“小号 ChatGPT”,而在于亲手跑通一个语言模型从数据到回答的完整闭环。
2. GPT 到底在学习什么
GPT 的核心任务可以概括成一句话:
根据前面已经出现的 token,预测下一个 token。
例如训练数据中有这样一组问答:
1 | 用户:什么是注意力机制? |
模型并不是一次性生成整句话,而是依次学习:
1 | “助手:”后面应该出现什么? |
对每一个位置,模型都会给词表中的所有 token 计算一个分数,再把分数转换成概率。训练的过程,就是不断纠正这些预测,让正确 token 的概率越来越高。
一开始,模型参数是随机的,输出也接近随机。经过大量重复训练后,参数逐渐记录下数据中的语言规律和问答模式,模型才开始生成像样的内容。
3. 完整训练流程
整个项目可以先简化成下面这条主线:
1 | 准备问答数据 |
3.1 准备训练数据
本项目使用 JSONL 格式保存问答数据,每一行是一组 prompt 和 completion:
1 | {"prompt": "什么是注意力机制?", "completion": "注意力机制通过计算相关性分配权重。"} |
项目把它整理成统一的对话格式:
1 | <bos>用户:什么是注意力机制? |
其中 <bos> 表示序列开始,<eos> 表示序列结束。统一格式很重要,因为模型不仅要学习答案内容,还要学习“问题结束后应该开始回答”这种结构。
3.2 用分词器把文字变成数字
神经网络不能直接处理字符串,它接收的是数字组成的张量。因此,文本要先经过分词器:
1 | 中文文本 → token → token id → 模型 |
分词器决定文本如何切分,以及每个 token 对应哪个整数。模型的输入词嵌入层和输出层都依赖分词器的词表大小,所以必须先确定分词器,再创建和训练模型。
分词器的结构、BPE 合并规则和词表绑定关系比较复杂,我单独整理在下一篇文章中:分词器(Tokenizer)通俗详解。
3.3 构造输入和训练目标
假设分词后的序列是:
1 | [A, B, C, D] |
要训练“预测下一个 token”,输入和目标就要错开一位:
1 | 输入: [A, B, C] |
模型看到 A 时预测 B,看到 A、B 时预测 C,以此类推。
在这个问答项目中,模型会读取“用户问题 + 助手前缀 + 答案”,但问题部分不会参与 Loss 计算,真正被评分的是答案部分。这样可以把训练重点放在“如何根据问题生成回答”上。
3.4 模型进行前向计算
token id 首先经过词嵌入层,变成模型可以计算的向量,然后依次经过多层 Transformer,最后输出每个位置对整个词表的预测分数。
可以暂时把模型内部理解成:
1 | token id |
项目内部还使用了因果注意力、RoPE、RMSNorm、GQA 和 SwiGLU 等结构。这些技术分别解决位置、信息关联、训练稳定性和计算效率等问题,但不影响当前对主流程的理解:模型读取已有 token,输出下一个 token 的预测结果。
3.5 计算 Loss
模型的预测不一定正确,因此需要把预测结果和训练数据中的正确 token 进行比较。这个差距就是 Loss(损失)。
- 正确 token 的预测概率越高,Loss 越小;
- 正确 token 的预测概率越低,Loss 越大。
Loss 是训练过程中的“评分标准”。但它只是一个统计指标,Loss 下降表示模型对当前数据的预测在改善,不等于模型已经具备通用知识或推理能力。
3.6 反向传播和参数更新
得到 Loss 后,程序通过反向传播计算:模型中的每个参数应该向哪个方向调整,才能让预测误差减小。
优化器再根据这些信息更新参数。一次“预测 → 计算 Loss → 反向传播 → 更新参数”就是一个基本训练循环。这个过程会重复很多次:
1 | 预测错误 |
因此,模型训练不是把问答文本原封不动写入某个文件,而是把数据中的规律逐步编码进大量数值参数中。
3.7 验证和保存模型
除了训练数据,项目还准备了验证数据。验证时只检查模型表现,不更新参数,用来观察模型对未直接参与当前训练的数据是否也有效。
项目会定期计算验证 Loss:
- 验证 Loss 变小,说明模型还在改善;
- 长时间不再改善,可能已经没有继续训练的必要;
- 训练 Loss 持续下降、验证 Loss 反而上升,通常意味着模型开始过度记忆训练数据。
训练过程中会保存表现最好的模型权重为 best.pt。以后进行推理时,真正加载的是这个权重文件,而不是重新读取训练数据来现场查找答案。
4. 模型训练完成后如何回答问题
训练和推理使用的是同一种能力:预测下一个 token。区别是训练时有正确答案用来纠错,推理时没有正确答案,只能使用模型自己的预测继续生成。
1 | 输入用户问题 |
这叫做自回归生成。我们看到的是一次生成出的完整句子,但模型内部实际上是一个 token、一个 token 地往后写。
推理时还可以使用温度、Top-k、Top-p、重复惩罚等参数控制如何从候选 token 中选择。不过这些参数只能改变输出的稳定性和随机性,不能让模型凭空获得训练中没有学到的知识。
5. 项目代码与训练环节的对应关系
理解全局流程后,再看项目目录就会清晰很多:
| 文件 | 作用 |
|---|---|
data/*.jsonl |
保存训练、验证和测试问答 |
src/build_tokenizer.py |
根据语料训练 BPE 分词器 |
src/tokenizer.py |
加载分词器,完成编码和解码 |
src/data.py |
拼接问答、错位构造目标、补齐序列 |
src/model.py |
定义 GPT 模型结构和前向计算 |
src/train.py |
执行训练、验证、保存权重 |
src/infer.py |
加载权重并逐 token 生成回答 |
src/evaluate.py |
用测试问题验收模型效果 |
run.py |
串联分词器、训练和 Web 演示 |
这些文件不是互相独立的功能,而是一条数据流水线上的不同环节。
6. 教学项目与真实大模型的区别
这个项目直接使用少量中文问答,从随机参数开始训练一个小型 GPT。它适合展示训练原理,但不能代表工业界训练通用大模型的全部过程。
真实的大语言模型通常会经历:
- 预训练:使用海量文本学习语言、知识和基本能力,核心任务仍然是预测下一个 token。
- 指令微调:使用高质量问答和任务数据,让模型学会按照人类指令回答。
- 对齐:进一步利用人类偏好或规则,让回答更有用、更安全、更符合预期。
而本项目更接近把“从头训练”和“问答格式训练”合在一起完成。由于参数和数据都很少,它主要能学习有限的问答模式,不具备真正通用大模型的知识覆盖和泛化能力。
二者的规模不同、阶段不同,但最底层的主线仍然一致:文本经过分词后进入 Transformer,模型预测下一个 token,再通过误差不断更新参数。
7. 几个容易混淆的概念
7.1 分词器不是 GPT 模型
分词器负责文本和 token id 之间的转换;GPT 模型负责根据 token 之间的关系进行预测。分词器有自己的训练过程,但它不是通过反向传播训练的神经网络。
7.2 模型文件不是训练数据的压缩包
best.pt 保存的是模型结构中各个参数训练后的数值,不是可以直接还原成原始问答的数据库。模型可能记住部分数据,但更准确的理解是:它通过参数拟合训练数据中的统计规律。
7.3 训练 Loss 低不代表模型什么都会
小模型可能把训练问题记得很好,却无法回答换一种说法的问题。判断模型效果不能只看训练 Loss,还要结合验证集、测试集以及未见过的问题。
7.4 参数量不是能力的唯一来源
更大的模型通常具有更强的表达能力,但最终效果还取决于数据的数量、质量、覆盖范围、训练方法和计算资源。对本项目而言,少量、高质量且有一定变化的问答数据,比单纯把某个配置调大更有意义。
8. 总结
把整个项目压缩成一句话:
先用分词器把文本转换成 token id,再让 GPT 根据已有 token 预测下一个 token;训练时通过正确答案计算 Loss、反向传播并更新参数,推理时则不断把预测结果接回输入,最终生成完整回答。
目前可以先记住下面这条主线:
1 | 数据 → 分词 → 构造样本 → 模型预测 → Loss → 反向传播 → 保存权重 → 推理生成 |
后续再分别深入分词器、注意力机制、Transformer、Loss 和推理策略时,每一个概念都能放回这条主线中理解,而不是变成彼此孤立的知识点。