之前翻译了一篇GPT2的文章:如何训练你的 GPT
https://github.com/zhangrr/how-to-train-your-gpt
对分词、嵌入、位置编码、QKV、多头注意力、自我注意力、transformer块,前馈理解了一部分后;
最好的办法还是去实践、实践、实践,然后再不断的实践!
从零训练中文 GPT-2:通俗教程与项目复盘
项目路径:
/workspace/gpt2
文档更新:2026-08-08
目标读者:想弄懂「分词之后怎么训 GPT-2、改了哪些参数、每个模型能干什么」的人注意:tokenizer的过程需要大量内存,32G是不够的,本文是基于wikimedia/wikipedia 20231101.zhs数据集进行训练的
本文既是实验记录,也是入门教程。建议按目录顺序阅读:先建立直觉,再对照参数表与命令复现。
目录
- 用一句话理解整条流水线
- 环境与依赖
- 阶段 0~3:数据与分词器(训练前奏)
- 阶段 4 详解:从分词器到可训练的 GPT-2(重点)
- 本项目实际跑过的全部实验(A~F)
- 重点对比:50000 步 → 再训 150000 步改了什么
- 为什么新增 QA 语料?从哪来?怎么用?
- 每个模型目录适用于什么场景
- 指标怎么读、生成时怎么调
- 踩坑与经验
- 端到端复现命令
- 变更日志
1. 用一句话理解整条流水线
原始中文维基
→ 清洗成纯文本
→ 训练分词器(把文字变成数字 ID)
→ 搭一个「只会猜下一个词」的 GPT-2(随机初始化)
→ 用大量文本做预训练(CLM:因果语言建模)
→ 发现:会写中文,但不保证答对常识
→ 用问答对做 SFT(监督微调)
→ 再小幅续训语言流畅度(并混入问答,防止忘掉事实)
1.1 两个容易混的概念
| 概念 | 通俗解释 | 本项目里对应 |
|---|---|---|
| 预训练(Pretrain / CLM) | 让模型读海量文章,学会「下一个字/词大概是什么」 | 04.train_gpt2.py |
| 监督微调(SFT) | 拿「问题→标准答案」专门教它怎么答 | 06.sft_qa.py |
重要结论:
- 只做预训练:文笔可能变好,但「中国的首都是?」不一定稳。
- 只做 SFT:事实题可以变准,但语料太少时,开放写作可能变「死板」。
- 本项目最终路径:预训练加长 → 问答 SFT → 带问答回放的续训。
1.2 全局鸟瞰图
00 环境检查
01 下载维基 → train_data_chunks/
02 清洗 → train_data_cleaned_chunks/
03 训分词器 → tokenized_data/tokenizer.json
04 预训练 → gpt2-zh-checkpoints / gpt2-zh-base-checkpoints
05 生成测试 → 05.generate.py
06 问答SFT → gpt2-zh-sft-qa
04 再续训 → gpt2-zh-sft-cpt(热启动 + QA 混合数据)
2. 环境与依赖
| 项目 | 本机实际值 |
|---|---|
| Python | 3.12(uv 管理;venv 已加载) |
| PyTorch | 2.13.0+cu130 |
| Transformers | 5.12.1 |
| GPU | RTX 5090,约 31.4 GB |
| 训练精度 | 优先 bf16 |
python 00.py # 环境自检
至少需要:torch、transformers、datasets、tokenizers、accelerate、safetensors。
3. 阶段 0~3:数据与分词器(训练前奏)
| 步骤 | 脚本 | 干什么 | 产出 |
|---|---|---|---|
| 0 | [00.py](./00.py) | 检查 PyTorch / CUDA / HF 库 | — |
| 1 | 01.load_dataset.py | 加载 wikimedia/wikipedia 中文 | train_data_chunks/ ≈2.5G |
| 2 | 02.clean_data.py | 去 URL/HTML/脏字符,过滤过短行 | train_data_cleaned_chunks/ ≈1.8G,28 片 |
| 3 | 03.tokenizer.py | 训练 BPE 分词器 | tokenized_data/tokenizer.json |
3.1 为什么必须先有分词器?
神经网络只吃数字。分词器负责:
"中国的首都是北京" → [id1, id2, id3, ...]
本项目 BPE 设定:
| 项 | 值 | 含义 |
|---|---|---|
| vocab_size | 50000 | 词表里大约有 5 万种 token |
| special tokens | <s> <pad> </s> <unk> <mask> | 句首/填充/句尾/未知/掩码 |
3.2 复现数据阶段
python 01.load_dataset.py --output-dir train_data_chunks --chunk-mb 64 --cache-dir ./hf_cache
python 02.clean_data.py --input train_data_chunks --output-dir train_data_cleaned_chunks
python 03.tokenizer.py --input train_data_cleaned_chunks --output-dir tokenized_data --vocab-size 50000
4. 阶段 4 详解:从分词器到可训练的 GPT-2
脚本:04.train_gpt2.py
这是最核心的一步。下面按「代码真实在干什么」讲解,而不是只列参数名。
4.1 整体流水线(04 内部)
① 加载 tokenizer.json,转成 HuggingFace 格式
② 新建(或加载)GPT2LMHeadModel
③ 读入清洗后的 .txt,过滤空行,划分 train/eval
④ 每行末尾加 </s>,tokenize,再拼成固定长度 block
⑤ 用 Trainer 做「预测下一个 token」的训练
⑥ 存盘 + 可选生成几句样例
4.2 模型结构参数(建网时)
代码里用预设 MODEL_PRESETS,再构造 GPT2Config / GPT2LMHeadModel:
| 预设名 | n_layer | n_embd | n_head | n_positions(默认) | 约参数量 | 用途 |
|---|---|---|---|---|---|---|
| tiny | 4 | 256 | 4 | 512 | ~16M | 冒烟测试 |
| small | 6 | 512 | 8 | 512 | ~45M | 小规模试训 |
| base | 12 | 768 | 12 | 1024 | ~124M | 主实验(接近 GPT-2 small) |
逐项通俗解释:
| 参数 | 含义(人话) | 调大/调小的影响 |
|---|---|---|
| n_layer | Transformer 堆了多少层 | 更深:表达力↑,算力/显存↑,更难训 |
| n_embd | 每个 token 的向量维度 | 更宽:容量↑,参数量近似按平方涨 |
| n_head | 多头注意力头数 | 须能整除 n_embd;头多可关注多种关系 |
| n_positions / n_ctx | 模型能看多长的上下文 | 本项目实际训练常用 block_size=512,因此有效上下文是 512(即使 base 预设写 1024) |
| vocab_size | 词表大小 | 必须 等于分词器长度(本项目 50000) |
| bos/eos/pad_token_id | 特殊符号的数字编号 | 必须与分词器一致,否则生成/padding 会乱 |
| resid/embd/attn_pdrop=0.1 | Dropout,防止死记硬背 | 预训练常见 0.1 |
4.3 数据如何变成训练样本(block packing)
GPT-2 训练不是「一行原文 = 一个 batch」,而是:
- 每行文本后拼
eos(</s>) - tokenize 成一长串 id
- 把很多行的 id 首尾相接
- 切成固定长度 block_size 的块(例如 512)
labels = input_ids(标准 CLM:每个位置预测下一个)
| 参数 | 含义 |
|---|---|
| block_size | 每条训练样本的 token 长度(上下文窗口) |
| eval_ratio | 验证集比例(如 0.01 = 1%) |
| max_files | 只用前 N 个数据分片;0=全部(调试时减小) |
为什么用 packing?
逐句 pad 到 512 会浪费大量 pad 计算;拼起来切块,GPU 吃得更满。
4.4 优化与 Trainer 参数(怎么学)
| 参数 | 本项目常用值 | 含义(通俗) |
|---|---|---|
batch_size (per_device_train_batch_size) | 8 | 一次前向吃几条样本 |
| grad_accum | 4 | 梯度累加几步再更新;有效 batch ≈ 8×4=32 |
| learning_rate | 见各 Run | 步子迈多大;太大震荡,太小学不动 |
| warmup_steps | 数百~两千 | 开头几步从很小 LR 爬升,避免一上来冲飞 |
| lr_scheduler | linear / cosine | LR 随训练如何下降 |
| weight_decay | 0.1 | 权重衰减,减轻过拟合 |
| max_steps | 5000 / 5万 / 15万… | 总共更新多少次参数(本项目主要用步数控制,而不是 epoch) |
| num_epochs | 仅当 max_steps≤0 | 扫几遍数据 |
| save_steps | 2500 / 5000… | 每隔多少步存 checkpoint |
| logging_steps | 50~100 | 多久打一次 loss 日志 |
| eval_strategy=steps | 与 save 对齐 | 周期性在验证集算 eval_loss |
| bf16 / fp16 | bf16 优先 | 半精度加速、省显存 |
| seed | 42 | 随机种子,便于复现 |
| num_workers | 4 | 数据加载子进程数 |
有效 batch 记忆公式:
有效 batch size = batch_size × grad_accum × GPU数量
本项目单卡:8 × 4 × 1 = 32 个 block / 次参数更新
4.5 学习率调度:linear vs cosine(后面 5 万→15 万的关键)
| 调度 | 曲线形状 | 行为 |
|---|---|---|
| linear | warmup 后直线降到接近 0 | 跑满 max_steps 时 LR≈0 |
| cosine | warmup 后按余弦平滑降 | 中后期仍有一定学习率,适合加长训 |
LR
^
| /‾‾‾\ cosine 更平滑
| / \
| / \___
| / linear \___→ 0
+------------------→ steps
4.6 --resume vs --init-from(续训必懂)
| 方式 | 加载什么 | 适用场景 |
|---|---|---|
--resume path | 权重 + 优化器 + 调度器 + 全局 step | 同一轮训练中断后续跑(超参不变) |
--init-from path | 只加载权重,优化器/LR 日程全新 | 加长训练、改 LR、改总步数、换阶段 |
本项目血泪教训:
Run C 用 linear 跑满 50000 步后,LR 已经接近 0。若此时:
--resume checkpoint-50000 --max-steps 200000
会带着「快停机」的调度器状态继续,几乎学不动。
正确加长做法是:
--init-from gpt2-zh-base-checkpoints # 只继承脑子,不继承「疲惫的学习节奏」
--max-steps 150000
--learning-rate 1.5e-4
--lr-scheduler cosine
4.7 04 的命令行参数一览(对照脚本)
python 04.train_gpt2.py \
--model-size base \ # tiny|small|base 结构预设
--tokenizer-dir tokenized_data \
--data-dir train_data_cleaned_chunks \
--output-dir gpt2-zh-base-checkpoints \
--block-size 512 \ # 训练上下文长度
--batch-size 8 \
--grad-accum 4 \
--learning-rate 3e-4 \
--warmup-steps 2000 \
--lr-scheduler cosine \ # 或 linear(早期默认行为)
--max-steps 50000 \
--save-steps 2500 \
--logging-steps 100 \
--eval-ratio 0.01 \
--num-workers 4 \
--init-from 某目录 \ # 可选:热启动
--resume 某checkpoint \ # 可选:完整恢复
--seed 42
4.8 训练目标函数(CLM)
对序列 (x_1, x_2, \ldots, x_T):
[ \mathcal{L} = -\sum_{t=1}^{T} \log P(x_t \mid x_{<t}) ]
人话:每个位置都根据前面内容猜当前 token,猜错就有 loss。
这和「做阅读理解选择题」不同,所以预训练模型擅长续写,不保证百科问答。
5. 本项目实际跑过的全部实验(A~F)
硬件共性:RTX 5090;base 配置下约 17~24GB 显存;吞吐约 5.8~6.0 steps/s。
Run A:tiny 冒烟(验证管道)
| 项 | 值 |
|---|---|
| 目的 | 证明数据→分词→Trainer→保存→生成能跑通 |
| model-size | tiny(~16M) |
| max-steps | 30 |
| 问题 | 曾因 Transformers 5.x 去掉 overwrite_output_dir 报错,已修 |
Run B:small 短训 8000 步
python 04.train_gpt2.py \
--model-size small --block-size 512 \
--batch-size 16 --grad-accum 2 \
--max-steps 8000 --learning-rate 5e-4 --warmup-steps 400 \
--save-steps 1000 --eval-ratio 0.01
| 指标 | 结果 |
|---|---|
| 参数量 | ~44.8M |
| eval_loss / PPL | 5.02 / ≈152 |
| 产出 | gpt2-zh-checkpoints/ |
| 日志 | train.log |
| 观感 | 有中文感,语义很弱 |
Run C:base 从零主训 50000 步
python 04.train_gpt2.py \
--model-size base \
--output-dir gpt2-zh-base-checkpoints \
--block-size 512 --batch-size 8 --grad-accum 4 \
--max-steps 50000 \
--learning-rate 3e-4 --warmup-steps 2000 \
--save-steps 2500 --logging-steps 100 --eval-ratio 0.01
| 项 | 值 |
|---|---|
| 参数量 | 123.8M |
| 有效 batch | 32 |
| 训练 blocks | ≈797,324 |
| 约 steps/epoch | 797324/32 ≈ 24,916 |
| 50000 步约 | ~2.0 epoch |
| 调度 | 当时默认 linear(跑满后 LR→0) |
| 时长 | ~2.45 小时 |
| eval_loss / PPL | 4.12 / ≈61.6 |
| 产出 | gpt2-zh-base-checkpoints/ |
| 日志 | train_base.log |
观感: 比 small 流畅,仍常事实错误(高温度下可能乱说首都)。
Run D:base 热启动再训 150000 步(5 万之后的加长)
见下一节完整参数对比。
| 项 | 值 |
|---|---|
| 方式 | --init-from(不是 resume) |
| max-steps | 150000(本轮从 0 计步) |
| 时长 | ~7.16 小时 |
| eval_loss / PPL | 3.885 / ≈48.7 |
| 日志 | train_base_continue.log |
Run E:问答 SFT
| 项 | 值 |
|---|---|
| 脚本 | 06.sft_qa.py |
| 数据 | qa_data/zh_qa.jsonl(~249 条,模板扩写→996) |
| 起点 | Run D 最终权重 |
| 产出 | gpt2-zh-sft-qa/ |
| eval_loss | ≈0.038(任务 loss,不可与 CLM PPL 直接比) |
| 效果 | 「中国的首都是」→ 北京 |
Run F:SFT 后 CPT + QA 回放
| 项 | 值 |
|---|---|
| init-from | gpt2-zh-sft-qa |
| 数据 | train_data_cpt_mix/(4 片维基 + QA 回放×20) |
| max-steps | 30000,lr=5e-5,cosine |
| 产出 | gpt2-zh-sft-cpt/ |
| eval_loss / PPL | 3.946 / ≈51.7(混合小集,勿与全量维基硬比) |
| 复测 | 首都仍答北京 ✓ |
指标总览
| Run | 阶段 | steps | eval_loss | PPL | 备注 |
|---|---|---|---|---|---|
| B | small 预训练 | 8k | 5.02 | ≈152 | 短训 |
| C | base 从零 | 50k | 4.12 | ≈61.6 | linear 跑满 |
| D | base 热启动 CPT | +150k | 3.885 | ≈48.7 | cosine 加长 |
| E | 问答 SFT | 600 | 0.038 | — | 事实对齐 |
| F | SFT→CPT+QA | 30k | 3.946 | ≈51.7 | 防遗忘续训 |
6. 重点对比:50000 步 → 再训 150000 步改了什么
这是用户最关心的一段:为什么不接着 resume?具体改了哪些旋钮?
6.1 背景:Run C 结束后发生了什么
- 模型已有一定中文续写能力(PPL≈61.6)。
- 用户希望语义更好 → 需要更多更新次数。
- Run C 使用 linear 调度且
max_steps=50000已跑满 → 学习率几乎降到 0。 - 若直接 resume 到更大 max_steps,等于让一个「学习率已熄火」的优化器继续跑 → 无效续训。
- 代码因此增加了
--init-from与--lr-scheduler。
6.2 参数对照表(Run C vs Run D)
| 参数 | Run C(50000) | Run D(150000 热启动) | 为什么改 |
|---|---|---|---|
| model-size | base | base | 结构不变,继续挖同一模型潜力 |
| 权重来源 | 随机初始化 | --init-from gpt2-zh-base-checkpoints | 继承已学到的表示 |
| resume | 无 | 不用 | 避免恢复 LR≈0 的调度器 |
| max_steps | 50000 | 150000 | 加长训练(本轮重新计 15 万步) |
| learning_rate | 3e-4 | 1.5e-4 | 已是半成品,用更小 LR 精细调,防破坏 |
| warmup_steps | 2000 | 1500 | 新日程仍需短 warmup,但不必和从零一样长 |
| lr_scheduler | linear(默认/当时) | cosine | 中后期仍保持有效学习率,适合长训 |
| block-size | 512 | 512 | 上下文长度不变(与已训位置编码一致) |
| batch-size | 8 | 8 | 显存与吞吐稳定 |
| grad-accum | 4 | 4 | 有效 batch 仍为 32 |
| save_steps | 2500 | 5000 | 步数变多,降低存盘频率 |
| logging_steps | 100 | 100 | 日志粒度不变 |
| eval-ratio | 0.01 | 0.01 | 验证集比例不变 |
| data-dir | 全量 cleaned | 全量 cleaned | 仍用中文维基清洗语料 |
| output-dir | gpt2-zh-base-checkpoints | 同目录覆盖写 | 注意会改写该目录下指标文件 |
6.3 Run D 完整命令
python 04.train_gpt2.py \
--model-size base \
--init-from gpt2-zh-base-checkpoints \
--output-dir gpt2-zh-base-checkpoints \
--block-size 512 \
--batch-size 8 \
--grad-accum 4 \
--max-steps 150000 \
--learning-rate 1.5e-4 \
--warmup-steps 1500 \
--lr-scheduler cosine \
--save-steps 5000 \
--logging-steps 100 \
--num-workers 4 \
--eval-ratio 0.01 \
2>&1 | tee train_base_continue.log
6.4 效果变化(数字)
| 阶段 | train loss 量级 | eval_loss | PPL | 约 epoch |
|---|---|---|---|---|
| Run C 起点 | ~11 | — | 极大 | 0 |
| Run C 终点 | ~4.1 | 4.12 | ≈61.6 | ~2 |
| Run D 热启动起点 | ~4.07 | — | — | 承接 C |
| Run D ~50% | ~4.09 | — | — | — |
| Run D 终点 | ~3.9 附近 | 3.885 | ≈48.7 | 本轮再扫 ~6 epoch |
PPL 从 61.6 → 48.7,大约 再降 21%。
语言更顺,但仍不能保证百科题总对——这是下一节引入 QA 的原因。
6.5 直观类比
- 50000 步(Run C):从零学中文,像「读完两年维基速成班」。
- 再 150000 步(Run D):同一学生换新课表(cosine + 较低 LR)继续深造,而不是把旧课表最后一天的「学习热情=0」硬续上。
- 只 resume:相当于期末已疲惫,却强制按「期末疲劳状态」再上五学期 → 无效。
7. 为什么新增 QA 语料?从哪来?怎么用?
7.1 现象
预训练后即使用较低温度,模型有时能说出「北京」,但在较高温度或换种问法时仍会跑题(历史输出里出现过离谱续写)。
用户反馈:「中国的首都是北京」这种常识应该答对。
7.2 根因(不是简单「学反了」)
| 原因 | 解释 |
|---|---|
| 训练目标是 CLM | 优化「像不像维基续写」,不是「答案是否事实正确」 |
| 维基表述多样 | 「首都」周围出现很多国家/历史/城市,短问答句式未必占优 |
| 模型偏小 + 轮数有限 | ~124M、数个 epoch,流畅度先于稳健知识 |
| 采样温度 | temperature 高 → 更敢胡说 |
所以:不是一定把首都学成错误知识点,而是「事实约束不够稳」。
7.3 QA 语料从何而来?
不是重新下载外网大数据,而是项目内人工整理的教学用问答集:
| 文件 | 内容 |
|---|---|
qa_data/zh_qa.jsonl | 约 249 条 {"question","answer"} |
| 覆盖领域 | 地理(各国首都,中国首都多模板强化)、科学、历史、CS/AI 常识、节日等 |
| 生成方式 | 编写 06.sft_qa.py 时一并创建的本地语料,用于演示 SFT |
qa_data/qa_replay.txt | 由 06.sft_qa.py 自动从 jsonl 导出的纯文本,供续训「回放」 |
首都类会故意写很多同义问法,例如:
中国的首都是哪里? → 北京。
中国的首都是 → 北京。
上海是中国的首都吗? → 不是。中国的首都是北京……
填写:中国的首都是____。 → 北京。
7.4 SFT 怎么训?(06.sft_qa.py)
- 用多种模板把问答拼成字符串,例如:
问题:中国的首都是哪里?\n答案:北京。 - Tokenize 后,默认 mask 掉「问题」部分的 labels(=-100),只对「答案」算 loss → 典型指令/问答 SFT。
- 较小学习率
5e-5,多 epoch(本项目 10 epoch,共 600 steps),从gpt2-zh-base-checkpoints热启动。
python 06.sft_qa.py \
--model-dir gpt2-zh-base-checkpoints \
--qa-file qa_data/zh_qa.jsonl \
--output-dir gpt2-zh-sft-qa \
--epochs 10 --batch-size 16 --learning-rate 5e-5 --augment 4
7.5 为什么 SFT 之后还要 CPT?又为什么混 QA?
| 步骤 | 目的 |
|---|---|
| SFT | 把「问→答」行为刻进模型(事实对齐) |
| 再 CPT | 用户希望语言能力继续保持/提升 |
| 混合 QA 回放 | 纯维基续训容易 灾难性遗忘 SFT;把 qa_replay 复制多份混进 train_data_cpt_mix/,续训时反复看到事实句 |
Run F 要点:
--init-from gpt2-zh-sft-qa--data-dir train_data_cpt_mix- 更小 LR
5e-5(保护 SFT) max-steps 30000
8. 每个模型目录适用于什么场景
按「你想拿模型干什么」选目录:
| 模型目录 | 怎么来的 | 更适合 | 不太适合 | 推荐用法 |
|---|---|---|---|---|
gpt2-zh-checkpoints/ | Run B small 8k | 流水线调试、对比「小模型」 | 正经生成 | 教学对比 |
gpt2-zh-base-checkpoints/ | Run C+D base 预训练 | 开放中文续写、看 PPL/流畅度 | 严格知识问答 | 05.generate.py,温度 0.7~0.9 创意写 |
gpt2-zh-sft-qa/ | Run E 问答 SFT | 常识/百科短问答(首都、定义题) | 长文创作多样性 | 优先 问题:…\n答案:,温度 0.2~0.4 |
gpt2-zh-sft-cpt/ | Run F SFT 后再 CPT | 问答 + 稍好的通顺续写 折中 | 极致任务专项 | 综合默认推荐 |
checkpoint-* 子目录 | 训练中途快照 | 回滚、对比过拟合 | 当最终产品前先 eval | 出问题可回退 |
8.1 为什么要分这么多阶段?(设计思想)
预训练(C/D) → 先学会「中文长什么样」
↓
SFT(E) → 再学会「这种问法应对什么」
↓
CPT+回放(F) → 在不忘答案的前提下继续泡语言数据
这与工业界常见顺序一致:
Pretrain → (可选 Continue Pretrain) → SFT → (可选 RLHF/DPO…)
本项目在 SFT 后做了带回放的短 CPT,是为了响应用户「先问答、再热启动续训」的需求,并用 QA 回放降低遗忘。
8.2 一句话选型
- 只想玩续写、看从零 GPT-2 长什么样 →
gpt2-zh-base-checkpoints - 只想演示「首都是北京」→
gpt2-zh-sft-qa - 默认给别人试用 →
gpt2-zh-sft-cpt
# 综合模型
python 05.generate.py --model-dir gpt2-zh-sft-cpt \
--prompt "中国的首都是" --temperature 0.2
# 问答格式更稳
python 06.sft_qa.py --skip-train --output-dir gpt2-zh-sft-qa \
--prompt "中国的首都是哪里?"
9. 指标怎么读、生成时怎么调
9.1 训练日志字段
| 字段 | 含义 |
|---|---|
| loss | 训练集交叉熵,越小越好 |
| eval_loss | 验证集交叉熵 |
| perplexity (PPL) | exp(eval_loss),可理解为「平均在多少选项里纠结下一个词」 |
| grad_norm | 梯度大小;长期爆炸/为 0 都不正常 |
| learning_rate | 当前学习率 |
| epoch | 数据大约扫了几遍 |
粗对照:
| PPL | 阶段体感 |
|---|---|
| >100 | 刚会一点中文结构 |
| ~60 | 可读,事实不稳(Run C) |
| ~50 | 更流畅(Run D/F 量级) |
| SFT 的 0.03 | 只说明问答任务拟合好,不是通用 PPL |
9.2 生成参数
| 参数 | 建议 | 含义 |
|---|---|---|
| temperature | 问答 0.2~0.4;创意 0.7~0.9 | 越低越保守、越稳 |
| top_p / top_k | 0.9 / 50 常见 | 截断低概率词 |
| max_new_tokens | 20~100 | 最多新写多长 |
| 提示格式 | SFT 后加 问题:\n答案: | 对齐微调分布 |
10. 踩坑与经验
- Transformers 5.x:
overwrite_output_dir、save_safetensors、Trainer(tokenizer=)等 API 有变;本项目已改用processing_class=等兼容写法。 - special tokens 必须与 03 一致,否则 BOS/EOS/PAD 全错。
- linear 跑满后再 resume ≈ 白忙;加长训用
--init-from+ 新 LR + cosine。 - 预训练 ≠ 问答;要事实对齐就 SFT。
- SFT 后纯 CLM 会遗忘;续训请混 QA 回放或之后再 SFT 一次。
init-from与output-dir同路径会覆盖指标文件;重要实验先cp -a备份。- block_size 与已训模型 n_positions:Run C/D 实际按 512 建/训,续训勿盲目加大到 1024(位置嵌入可能不匹配)。
11. 端到端复现命令
# 0) 环境
python 00.py
# 1~3) 数据与分词(若已有产物可跳过)
python 01.load_dataset.py
python 02.clean_data.py
python 03.tokenizer.py
# 4a) base 从零 5 万步
python 04.train_gpt2.py \
--model-size base --output-dir gpt2-zh-base-checkpoints \
--block-size 512 --batch-size 8 --grad-accum 4 \
--max-steps 50000 --learning-rate 3e-4 --warmup-steps 2000 \
--lr-scheduler cosine --save-steps 2500 --eval-ratio 0.01 \
2>&1 | tee train_base.log
# 4b) 热启动再 15 万步(不要 resume 硬续 linear 残骸)
python 04.train_gpt2.py \
--model-size base \
--init-from gpt2-zh-base-checkpoints \
--output-dir gpt2-zh-base-checkpoints \
--block-size 512 --batch-size 8 --grad-accum 4 \
--max-steps 150000 --learning-rate 1.5e-4 --warmup-steps 1500 \
--lr-scheduler cosine --save-steps 5000 --eval-ratio 0.01 \
2>&1 | tee train_base_continue.log
# 6) 问答 SFT
python 06.sft_qa.py \
--model-dir gpt2-zh-base-checkpoints \
--output-dir gpt2-zh-sft-qa \
--epochs 10 --batch-size 16 --learning-rate 5e-5 --augment 4 \
2>&1 | tee train_sft.log
# 4c) SFT 后短 CPT + QA 回放
mkdir -p train_data_cpt_mix
cp train_data_cleaned_chunks/train_data_cleaned_00000{0,1,2,3}.txt train_data_cpt_mix/
for i in $(seq -w 1 20); do cp qa_data/qa_replay.txt train_data_cpt_mix/qa_replay_$i.txt; done
python 04.train_gpt2.py \
--model-size base --init-from gpt2-zh-sft-qa \
--output-dir gpt2-zh-sft-cpt --data-dir train_data_cpt_mix \
--block-size 512 --batch-size 8 --grad-accum 4 \
--max-steps 30000 --learning-rate 5e-5 --warmup-steps 500 \
--lr-scheduler cosine --save-steps 3000 --eval-ratio 0.02 \
2>&1 | tee train_sft_cpt.log
# 生成
python 05.generate.py --model-dir gpt2-zh-sft-cpt \
--prompt "中国的首都是" --temperature 0.2
进度查看
pgrep -af 'python 04.train_gpt2'
tr '\r' '\n' < train_base_continue.log | grep -E "it/s\]|'loss':|eval_loss" | tail -20
cat gpt2-zh-base-checkpoints/all_results.json
12. 变更日志
| 日期 | 事项 |
|---|---|
| 2026-08-07 | 新增 04.train_gpt2.py、05.generate.py |
| 2026-08-07 | 适配 Transformers 5.x TrainingArguments / Trainer |
| 2026-08-07 | Run A/B/C:tiny 冒烟、small 8k、base 50k |
| 2026-08-07 | 增加 --init-from、--lr-scheduler;Run D 热启动 150k |
| 2026-08-07 | 新增 06.sft_qa.py + qa_data/;Run E SFT |
| 2026-08-07 | Run F:SFT 后 CPT 30k + QA 回放;首都事实保留 |
| 2026-08-08 | 重写 train.md 为通俗教程:参数详解、5万→15万对比、QA 来源、各模型适用场景 |
附录:文件地图
gpt2/
├── 00.py … 03.tokenizer.py # 环境 / 数据 / 分词
├── 04.train_gpt2.py # 预训练与热启动续训
├── 05.generate.py # 生成
├── 06.sft_qa.py # 问答 SFT
├── qa_data/zh_qa.jsonl # 人工问答集
├── qa_data/qa_replay.txt # SFT 导出回放文本
├── train_data_cleaned_chunks/ # 清洗维基
├── train_data_cpt_mix/ # 续训混合数据
├── tokenized_data/ # BPE
├── gpt2-zh-checkpoints/ # small
├── gpt2-zh-base-checkpoints/ # base 预训练(C+D)
├── gpt2-zh-sft-qa/ # 问答微调
└── gpt2-zh-sft-cpt/ # SFT 后再续训
读完本文你应能回答:分词后模型怎么建、batch/LR/scheduler 是什么、为什么 5 万步后再训要改 init-from 与 cosine、QA 从哪来、以及该加载哪一个目录做生成。
那更进阶的训练文章:模型 opus-mt-small320d-opus100-joint32k-ft-money-coffee-ct2-int8 完整训练手册