之前翻译了一篇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数据集进行训练的

本文既是实验记录,也是入门教程。建议按目录顺序阅读:先建立直觉,再对照参数表与命令复现。


目录

  1. 用一句话理解整条流水线
  2. 环境与依赖
  3. 阶段 0~3:数据与分词器(训练前奏)
  4. 阶段 4 详解:从分词器到可训练的 GPT-2重点
  5. 本项目实际跑过的全部实验(A~F)
  6. 重点对比:50000 步 → 再训 150000 步改了什么
  7. 为什么新增 QA 语料?从哪来?怎么用?
  8. 每个模型目录适用于什么场景
  9. 指标怎么读、生成时怎么调
  10. 踩坑与经验
  11. 端到端复现命令
  12. 变更日志

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. 环境与依赖

项目本机实际值
Python3.12(uv 管理;venv 已加载)
PyTorch2.13.0+cu130
Transformers5.12.1
GPURTX 5090,约 31.4 GB
训练精度优先 bf16
python 00.py   # 环境自检

至少需要:torchtransformersdatasetstokenizersacceleratesafetensors


3. 阶段 0~3:数据与分词器(训练前奏)

步骤脚本干什么产出
0[00.py](./00.py)检查 PyTorch / CUDA / HF 库
101.load_dataset.py加载 wikimedia/wikipedia 中文train_data_chunks/ ≈2.5G
202.clean_data.py去 URL/HTML/脏字符,过滤过短行train_data_cleaned_chunks/ ≈1.8G,28 片
303.tokenizer.py训练 BPE 分词器tokenized_data/tokenizer.json

00.py

01.load_dataset.py

02.clean_data.py

03.tokenizer.py

3.1 为什么必须先有分词器?

神经网络只吃数字。分词器负责:

"中国的首都是北京"  →  [id1, id2, id3, ...]

本项目 BPE 设定:

含义
vocab_size50000词表里大约有 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

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_layern_embdn_headn_positions(默认)约参数量用途
tiny42564512~16M冒烟测试
small65128512~45M小规模试训
base12768121024~124M主实验(接近 GPT-2 small)

逐项通俗解释:

参数含义(人话)调大/调小的影响
n_layerTransformer 堆了多少层更深:表达力↑,算力/显存↑,更难训
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.1Dropout,防止死记硬背预训练常见 0.1

4.3 数据如何变成训练样本(block packing)

GPT-2 训练不是「一行原文 = 一个 batch」,而是:

  1. 每行文本后拼 eos</s>
  2. tokenize 成一长串 id
  3. 把很多行的 id 首尾相接
  4. 切成固定长度 block_size 的块(例如 512)
  5. 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_accum4梯度累加几步再更新;有效 batch ≈ 8×4=32
learning_rate见各 Run步子迈多大;太大震荡,太小学不动
warmup_steps数百~两千开头几步从很小 LR 爬升,避免一上来冲飞
lr_schedulerlinear / cosineLR 随训练如何下降
weight_decay0.1权重衰减,减轻过拟合
max_steps5000 / 5万 / 15万…总共更新多少次参数(本项目主要用步数控制,而不是 epoch)
num_epochs仅当 max_steps≤0扫几遍数据
save_steps2500 / 5000…每隔多少步存 checkpoint
logging_steps50~100多久打一次 loss 日志
eval_strategy=steps与 save 对齐周期性在验证集算 eval_loss
bf16 / fp16bf16 优先半精度加速、省显存
seed42随机种子,便于复现
num_workers4数据加载子进程数

有效 batch 记忆公式:

有效 batch size = batch_size × grad_accum × GPU数量
本项目单卡:8 × 4 × 1 = 32 个 block / 次参数更新

4.5 学习率调度:linear vs cosine(后面 5 万→15 万的关键)

调度曲线形状行为
linearwarmup 后直线降到接近 0跑满 max_steps 时 LR≈0
cosinewarmup 后按余弦平滑降中后期仍有一定学习率,适合加长训
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-sizetiny(~16M)
max-steps30
问题曾因 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 / PPL5.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
有效 batch32
训练 blocks≈797,324
约 steps/epoch797324/32 ≈ 24,916
50000 步约~2.0 epoch
调度当时默认 linear(跑满后 LR→0)
时长~2.45 小时
eval_loss / PPL4.12 / ≈61.6
产出gpt2-zh-base-checkpoints/
日志train_base.log

观感: 比 small 流畅,仍常事实错误(高温度下可能乱说首都)。

Run D:base 热启动再训 150000 步(5 万之后的加长)

见下一节完整参数对比。

方式--init-from(不是 resume)
max-steps150000(本轮从 0 计步)
时长~7.16 小时
eval_loss / PPL3.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_loss0.038(任务 loss,不可与 CLM PPL 直接比)
效果「中国的首都是」→ 北京

Run F:SFT 后 CPT + QA 回放

init-fromgpt2-zh-sft-qa
数据train_data_cpt_mix/(4 片维基 + QA 回放×20)
max-steps30000,lr=5e-5,cosine
产出gpt2-zh-sft-cpt/
eval_loss / PPL3.946 / ≈51.7(混合小集,勿与全量维基硬比)
复测首都仍答北京 ✓

指标总览

Run阶段stepseval_lossPPL备注
Bsmall 预训练8k5.02≈152短训
Cbase 从零50k4.12≈61.6linear 跑满
Dbase 热启动 CPT+150k3.885≈48.7cosine 加长
E问答 SFT6000.038事实对齐
FSFT→CPT+QA30k3.946≈51.7防遗忘续训

6. 重点对比:50000 步 → 再训 150000 步改了什么

这是用户最关心的一段:为什么不接着 resume?具体改了哪些旋钮?

6.1 背景:Run C 结束后发生了什么

  1. 模型已有一定中文续写能力(PPL≈61.6)。
  2. 用户希望语义更好 → 需要更多更新次数
  3. Run C 使用 linear 调度且 max_steps=50000 已跑满 → 学习率几乎降到 0
  4. 若直接 resume 到更大 max_steps,等于让一个「学习率已熄火」的优化器继续跑 → 无效续训
  5. 代码因此增加了 --init-from--lr-scheduler

6.2 参数对照表(Run C vs Run D)

参数Run C(50000)Run D(150000 热启动)为什么改
model-sizebasebase结构不变,继续挖同一模型潜力
权重来源随机初始化--init-from gpt2-zh-base-checkpoints继承已学到的表示
resume不用避免恢复 LR≈0 的调度器
max_steps50000150000加长训练(本轮重新计 15 万步)
learning_rate3e-41.5e-4已是半成品,用更小 LR 精细调,防破坏
warmup_steps20001500新日程仍需短 warmup,但不必和从零一样长
lr_schedulerlinear(默认/当时)cosine中后期仍保持有效学习率,适合长训
block-size512512上下文长度不变(与已训位置编码一致)
batch-size88显存与吞吐稳定
grad-accum44有效 batch 仍为 32
save_steps25005000步数变多,降低存盘频率
logging_steps100100日志粒度不变
eval-ratio0.010.01验证集比例不变
data-dir全量 cleaned全量 cleaned仍用中文维基清洗语料
output-dirgpt2-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_lossPPL约 epoch
Run C 起点~11极大0
Run C 终点~4.14.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.jsonl249{"question","answer"}
覆盖领域地理(各国首都,中国首都多模板强化)、科学、历史、CS/AI 常识、节日等
生成方式编写 06.sft_qa.py 时一并创建的本地语料,用于演示 SFT
qa_data/qa_replay.txt06.sft_qa.py 自动从 jsonl 导出的纯文本,供续训「回放」

zh_qa.jsonl

qa_replay.txt

首都类会故意写很多同义问法,例如:

中国的首都是哪里? → 北京。
中国的首都是 → 北京。
上海是中国的首都吗? → 不是。中国的首都是北京……
填写:中国的首都是____。 → 北京。

7.4 SFT 怎么训?(06.sft_qa.py

  1. 用多种模板把问答拼成字符串,例如:
    问题:中国的首都是哪里?\n答案:北京。
  2. Tokenize 后,默认 mask 掉「问题」部分的 labels(=-100),只对「答案」算 loss → 典型指令/问答 SFT。
  3. 较小学习率 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

06.sft_qa.py

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_k0.9 / 50 常见截断低概率词
max_new_tokens20~100最多新写多长
提示格式SFT 后加 问题:\n答案:对齐微调分布

10. 踩坑与经验

  1. Transformers 5.xoverwrite_output_dirsave_safetensorsTrainer(tokenizer=) 等 API 有变;本项目已改用 processing_class= 等兼容写法。
  2. special tokens 必须与 03 一致,否则 BOS/EOS/PAD 全错。
  3. linear 跑满后再 resume ≈ 白忙;加长训用 --init-from + 新 LR + cosine。
  4. 预训练 ≠ 问答;要事实对齐就 SFT。
  5. SFT 后纯 CLM 会遗忘;续训请混 QA 回放或之后再 SFT 一次。
  6. init-fromoutput-dir 同路径会覆盖指标文件;重要实验先 cp -a 备份。
  7. 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

05.generate.py

进度查看

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.py05.generate.py
2026-08-07适配 Transformers 5.x TrainingArguments / Trainer
2026-08-07Run 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-07Run 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 完整训练手册

https://github.com/zhangrr/distill-opus-mt-en-zh