1. 为什么我要从零训练一个小语言模型1.1 大模型时代小模型反而更值得亲手做一遍过去两年大家都在卷参数量动辄 7B、13B、70B好像不堆到百亿参数都不好意思说自己在做语言模型。但我自己实际跑下来越来越觉得真正能让你把预训练、CPT、SFT、PEFT、蒸馏、DPO 这条完整链路吃透的恰恰是一个几百 M 到 1B 级别的小模型。原因很朴素——大模型你训不起也调不动一个 epoch 烧掉的钱够你买台新机器而小模型可以在单卡甚至消费级显卡上把全流程走通每一步的 loss 曲线、数据配比、超参影响你都能亲眼看到。Xihe 就是这样一个定位的项目。它不是要跟 GPT 系列掰手腕而是给你一个可以完整复现、可以随便改、可以反复实验的练手对象。你在这个小模型上踩过的每一个坑——数据清洗没做好导致 loss 震荡、学习率设大了直接发散、SFT 数据格式不对导致模型只会复读——放到大模型上是一模一样的只是大模型把这些坑放大了一百倍而且你还没机会重来。所以这篇东西写给谁看三类人一是想入门 LLM 训练但被大模型门槛劝退的工程师二是已经会调 API、但想搞清楚模型内部到底怎么被教出来的开发者三是做垂直领域应用、想用 CPT SFT PEFT 把通用模型改造成自己行业专家的人。不管你是哪一类只要跟着把 Xihe 这条链路走一遍你对语言模型的理解会从黑盒调参变成我知道它每一步在干什么。1.2 先把整条链路的地图摊开很多人一上来就问怎么训模型其实应该先问我要模型具备什么能力。语言模型的训练是一条流水线每个阶段解决不同的问题顺序不能乱乱了就白干。我先把 Xihe 这条链路的六个阶段用一句话讲清楚后面每个阶段再展开预训练Pretrain让模型学会说话。从随机初始化开始喂海量无标注文本目标是 next token prediction。这一步产出的是通顺但不懂事的基座模型。CPTContinued Pre-Training继续预训练让模型补课。在基座模型基础上用领域语料继续预训练把通用能力往垂直领域迁移比如医疗、法律、代码。SFTSupervised Fine-Tuning监督微调让模型听话。用指令-回答配对数据训练让模型学会按人类期望的格式回答问题。PEFTParameter-Efficient Fine-Tuning参数高效微调让模型省着改。只训练一小部分参数如 LoRA大幅降低显存和存储成本适合快速迭代。蒸馏Distillation让模型瘦身。用大模型教师的输出指导小模型学生训练让小模型学到接近大模型的能力。DPODirect Preference Optimization直接偏好优化让模型懂偏好。用人类偏好数据chosen/rejected 对直接优化让模型输出更符合人类喜好替代复杂的 RLHF。这六个阶段不是每个项目都要全走一遍。实际做的时候你要根据目标裁剪如果只是想让通用模型会回答你的领域问题预训练可以跳过直接 CPT SFT PEFT如果算力紧张又想效果好蒸馏是性价比最高的手段如果对回答质量要求高、有偏好数据最后再加一道 DPO。提示新手最容易犯的错是六个阶段全上结果每个阶段都做得半吊子。先把 SFT PEFT 跑通再回头补 CPT 和蒸馏最后考虑 DPO这个顺序对绝大多数人更友好。2. 预训练从随机权重到会说话的模型2.1 预训练到底在学什么预训练的目标函数简单到一句话给定前面的 token预测下一个 token。就这么个看似无聊的任务喂足够多的文本之后模型居然学会了语法、事实、推理甚至一些常识。为什么因为要准确预测下一个词模型被迫去理解上下文里的语义、句法、世界知识。这就像你逼一个人每天做完形填空做够几百万道他自然就懂了语言。Xihe 的预训练用的是标准的 decoder-only Transformer 架构和 GPT 系列同源。核心组件就三块多头自注意力Multi-Head Self-Attention、前馈网络FFN、以及把它们串起来的残差连接和 LayerNorm。我选 decoder-only 而不是 encoder-decoder是因为它结构简单、训练稳定、推理时天然支持自回归生成对小模型来说是最省心的选择。数据方面预训练吃的是纯文本不需要标注。我用的是一批中文为主的混合语料清洗流程后面细讲。这里先强调一个数字小模型的预训练数据量经验值是参数量的 20 倍以上。比如一个 100M 参数的模型至少要喂 2B token 的文本否则模型欠拟合生成的东西前言不搭后语。这个比例不是拍脑袋来的Chinchilla 那篇工作就指出模型参数量和训练 token 数应该大致同比例增长很多早期模型就是因为数据喂太少而营养不良。2.2 数据清洗预训练里最脏最累但最关键的活我可以很负责任地说预训练效果的 70% 取决于数据质量而不是模型结构或超参。你模型改得再花哨喂进去一堆乱码、重复、广告出来的就是个会喷垃圾话的复读机。Xihe 的数据清洗我走了这么几步每一步都有踩坑经验第一步是去重。别小看这一步网页爬下来的语料重复率高得吓人同一篇文章可能出现在几十个站点。重复数据会让模型过度记忆某些片段生成时反复吐同样的句子。我用的是 MinHash LSH 做近似去重阈值设在 0.8 左右。这里有个坑阈值设太低比如 0.6会把正常相似的文本也删掉导致语料多样性下降设太高0.95又去不干净。0.8 是我实测下来比较平衡的点。第二步是质量过滤。我用了几个启发式规则过滤掉长度过短少于 50 字或过长超过 10000 字的文档过滤掉符号占比过高的比如超过 30% 是非中文字符过滤掉包含大量重复 n-gram 的说明是机器生成的垃圾。这些规则看着土但比很多花哨的模型分类器更稳。第三步是敏感与低质内容剔除。这一步必须做而且要做得干净。我用关键词黑名单加规则匹配把明显不合规的内容全部清掉。这里的原则是宁可错杀不可放过因为一旦脏数据进了训练集模型会把它学进去后面 SFT 和 DPO 都救不回来。第四步是分词与打包。中文分词我用的是 BPEByte Pair Encoding词表大小设 32000。为什么是 32000太小了比如 8000会导致很多词被拆成单字序列变长、训练变慢太大了比如 100000会让 embedding 层参数暴涨小模型扛不住。32000 是中文场景下比较通用的经验值。打包就是把所有文档拼成固定长度的序列我用 1024中间用特殊 token 分隔避免跨文档污染注意力。注意数据清洗一定要在训练前一次性做完并落盘不要边训边洗。我早期图省事在 dataloader 里做实时清洗结果每个 epoch 洗出来的数据都不一样loss 曲线抖得没法看排查了两天才发现是数据不一致导致的。2.3 预训练的超参与实操配置预训练最怕的就是 loss 不收敛或者直接发散。我把 Xihe 预训练的关键超参列出来并解释每个值为什么这么设超参取值设置理由学习率3e-4小模型常用值配合 warmup 防止初期震荡Warmup steps2000占总步数约 5%让模型平稳进入训练学习率调度Cosine后期衰减到接近 0帮助收敛Batch size512梯度累积显存有限时用累积模拟大 batch序列长度1024平衡显存占用和上下文能力优化器AdamWβ10.9, β20.95, weight decay0.1梯度裁剪1.0防止梯度爆炸这个必须有Dropout0.1小模型容易过拟合需要正则这里重点说两个坑。第一个是学习率。我一开始用了 1e-3结果前 500 步 loss 直接飙到 nan模型废了。小模型对学习率比大模型更敏感因为参数量少、梯度噪声大学习率稍高就容易震荡。3e-4 是我反复试出来的稳定值配合 warmup 基本不会炸。第二个是梯度累积。单卡显存装不下大 batch但 batch 太小又会导致梯度噪声大、训练不稳。解决办法是用梯度累积比如实际 batch 设 32累积 16 次再更新一次参数等效 batch 就是 512。这里要注意梯度累积时 loss 要除以累积步数否则等效学习率会翻倍又炸了。这个细节很多教程不讲但特别重要。训练过程中我盯三个指标training loss、validation loss、以及生成样本。training loss 平稳下降是基本要求validation loss 如果开始上升说明过拟合了要早停或者加正则生成样本是最直观的每隔几千步让模型生成一段看看是不是从乱码逐渐变成通顺句子。我实测下来100M 参数的模型在 2B token 上训 1 个 epoch大概需要单卡 A100 跑 2-3 天loss 能从 10 左右降到 3.5 上下生成的中文已经基本通顺。3. CPT 继续预训练把通用模型改造成领域专家3.1 CPT 和预训练到底差在哪很多人搞不清 CPT 和预训练的区别以为就是再训一遍。其实差别很大。预训练是从随机初始化开始模型什么都不会需要海量通用语料打底CPT 是在已经训好的基座模型上用领域语料继续训练目标是让模型把通用语言能力迁移到特定领域。打个比方预训练像是让一个孩子从小学说话什么都学CPT 像是让这个已经会说话的人去读一个专业的学位比如医学。他不需要重新学语法只需要补充医学词汇、术语、表达习惯。所以 CPT 的数据量比预训练小得多通常几百万到几亿 token 就够学习率也要调小我用 1e-5 到 5e-5否则会把预训练学到的通用能力冲掉这叫灾难性遗忘。Xihe 的 CPT 我拿一个垂直领域做了实验用的是该领域的专业文档、问答、教程混合语料大概 5000 万 token。训练 1-2 个 epoch 后模型在该领域的术语使用准确率明显提升同时通用对话能力基本没退化。这个平衡点很关键后面会讲怎么监控。3.2 CPT 的数据配比与灾难性遗忘防治CPT 最容易翻车的地方就是灾难性遗忘领域数据训太狠模型把通用能力忘了问它个日常问题都答不上来。防治手段有这么几个我一个个说第一混入通用语料。不要纯喂领域数据按 7:3 或 8:2 的比例混入通用语料让模型在学新知识的同时复习旧知识。我实测 8:2 的配比在领域提升和通用保持之间比较平衡7:3 更保守但领域提升慢一些。第二控制学习率和步数。CPT 的学习率要比预训练小一个数量级步数也不要太多通常 1-3 个 epoch。训太多轮模型会把领域数据背下来泛化能力反而下降。第三用验证集监控双向指标。我准备了两个验证集一个是领域验证集看领域 loss 是否下降一个是通用验证集看通用 loss 是否上升。理想情况是领域 loss 降、通用 loss 基本持平。如果通用 loss 明显上升说明遗忘严重要减小学习率或增加通用语料比例。第四考虑用 LoRA 做 CPT。如果实在怕遗忘可以用 PEFT 的方式做 CPT只训练一小部分参数基座模型冻结这样通用能力几乎不会丢。代价是领域适配的深度不如全参 CPT。这个取舍后面 PEFT 章节会详细讲。提示CPT 之前一定要先确认基座模型的质量。如果基座本身就没训好生成都不通顺CPT 只会把问题放大。我见过有人拿一个 loss 都没收敛的模型做 CPT训完发现模型彻底废了还以为是 CPT 的问题。3.3 CPT 实操从数据准备到效果验证CPT 的实操流程和预训练类似但有几个关键差异。数据准备上领域语料要经过更严格的清洗因为领域文档里常有表格、公式、特殊符号这些直接喂进去会干扰训练。我的做法是把表格转成自然语言描述公式用 LaTeX 保留但加特殊标记特殊符号统一替换。训练配置上我用的学习率是 2e-5warmup 500 步cosine 调度batch size 256序列长度 1024训 2 个 epoch。优化器还是 AdamW但 weight decay 调到 0.01比预训练小因为不想过度正则化领域知识。效果验证我做了三件事。一是困惑度Perplexity对比在领域测试集上CPT 后的模型困惑度比 CPT 前下降了约 25%说明模型对领域文本的建模能力确实提升了。二是人工评估让模型回答 100 个领域问题人工打分CPT 后准确率从 40% 提升到 65%。三是通用能力回归测试用一批通用问题测确认没有明显退化。这里有个经验CPT 的效果不是线性的训到一定程度就饱和了。我训到第 2 个 epoch 时领域指标还在涨但第 3 个 epoch 就开始持平甚至下降过拟合。所以 CPT 不要贪多盯着验证集该停就停。4. SFT 监督微调让模型学会好好回答问题4.1 SFT 的本质是格式对齐预训练和 CPT 出来的模型能力是有了但它不知道你想要什么格式。你问它北京在哪它可能接着写北京在哪是一个常见问题很多人问……因为它只学会了续写没学会回答。SFT 就是解决这个问题的用大量指令-回答配对数据教模型看到指令就输出对应的回答。SFT 的本质是格式对齐 行为塑造。它不教模型新知识知识在预训练和 CPT 阶段已经灌进去了而是教模型什么时候该输出什么。所以 SFT 的数据质量比数量重要得多。我见过有人拿 10 万条低质 SFT 数据训效果还不如 5000 条精心构造的数据。Xihe 的 SFT 数据我按这个结构组织每条样本包含 system prompt角色设定、user用户指令、assistant期望回答三部分用特殊 token 分隔。训练时只对 assistant 部分计算 losssystem 和 user 部分 mask 掉。为什么因为我们要教模型生成回答而不是生成问题对输入部分算 loss 会干扰学习。4.2 SFT 数据构造质量远比数量重要SFT 数据从哪来三个来源人工标注、模型生成、开源数据集。人工标注质量最高但贵模型生成便宜但需要过滤开源数据集方便但良莠不齐。我的做法是三者混合人工标注打底保证质量模型生成扩充增加多样性开源数据补充覆盖长尾。数据构造有几个关键原则我踩过坑总结出来的第一指令要多样。不要全是请解释 X这种句式要覆盖问答、总结、改写、翻译、代码、推理等各种类型。指令单一会导致模型只会一种回答模式遇到没见过的指令就懵。第二回答要详细且准确。SFT 回答太短模型学会偷懒回答有错模型学会胡说。我要求每条回答至少 50 字且经过事实核查。这里有个技巧用强模型生成初稿人工修改润色比纯人工写快比纯模型生成准。第三格式要统一。所有样本用同一套模板特殊 token 的位置、数量都要一致。格式不统一会让模型困惑学出来的行为不稳定。第四要包含拒答样本。对于模型不该回答的问题比如超出能力范围的、有风险的要教它礼貌拒绝。这类样本占比不用高5% 左右就够但必须有否则模型会强行回答所有问题。注意SFT 数据里千万不要混入预训练语料那种纯文本。SFT 是指令-回答格式纯文本会破坏格式对齐。我早期图省事混了一批结果模型有时候会突然开始续写而不是回答排查很久才发现是数据格式污染。4.3 SFT 训练配置与过拟合防治SFT 的训练配置和预训练差别不小。学习率我用 2e-5比 CPT 还小一点因为 SFT 数据量小学习率大了容易过拟合。epoch 数通常 2-3 轮多了就过拟合。batch size 128序列长度 1024。优化器 AdamWweight decay 0.01。SFT 最大的敌人是过拟合。数据量小几千到几万条模型参数量大很容易把训练数据背下来。表现是训练 loss 一直降但验证 loss 开始上升生成时对训练集里的问题回答完美换个问法就崩。防治手段早停盯着验证 loss一旦连续几个 epoch 不降就停。数据增强对同一指令用不同措辞改写扩充数据量。降低学习率小学习率让模型学得更温和不容易记住噪声。加正则提高 dropout增大 weight decay。用 LoRAPEFT 方式做 SFT只训练少量参数天然抗过拟合。我实测下来SFT 训 2 个 epoch 效果最好第 3 个 epoch 验证 loss 就开始抬头了。生成质量上SFT 后的模型能稳定按指令格式回答不再续写这是最直观的变化。5. PEFT 参数高效微调用 1% 的参数撬动效果5.1 为什么需要 PEFT全参微调的问题很现实显存吃不下、存储扛不住、迭代太慢。一个 1B 参数的模型全参微调需要存模型参数、梯度、优化器状态显存需求是参数量的 4-6 倍消费级显卡根本跑不动。而且每换一个任务就要存一份完整模型几个任务下来硬盘就满了。PEFT 的思路是冻结大部分预训练参数只训练一小部分新增参数。这样显存需求大幅下降存储的也只是那一小部分参数通常几 MB 到几十 MB切换任务时换个 adapter 就行。最主流的 PEFT 方法是LoRALow-Rank Adaptation。LoRA 的原理很优雅它假设模型在适配新任务时权重的变化是低秩的。所以它不直接改原权重 W而是在旁边加一个低秩分解 ΔW BA其中 B 是 d×rA 是 r×dr 远小于 d比如 r8。训练时只更新 A 和 B原权重 W 冻结。推理时把 BA 加回 W 就行不增加推理延迟。这个低秩假设为什么成立因为微调本质上是小幅调整权重变化的信息量不大用低秩矩阵就能捕捉。5.2 LoRA 的关键参数怎么调LoRA 有几个关键参数调不好效果差很多。我把 Xihe 上实测的经验列出来参数含义推荐值调整经验r秩低秩矩阵的维度8-64任务越复杂 r 越大简单任务 8 够用alpha缩放系数16-32通常设为 r 的 2 倍dropoutLoRA 层 dropout0.05-0.1防过拟合target_modules应用 LoRA 的层q_proj, v_proj全应用效果更好但参数多r 的选择是最关键的。r 太小比如 4模型表达能力不够学不好复杂任务r 太大比如 128参数量上去了PEFT 省参数的优势就没了还容易过拟合。我的经验是简单任务如风格迁移r8 够用中等任务如领域问答r16-32复杂任务如多步推理r64。Xihe 上我用 r16alpha32效果和全参微调差距在 5% 以内但训练参数只有全参的 0.5%。target_modules 的选择也有讲究。最保守的做法是只对 attention 的 q_proj 和 v_proj 加 LoRA这是原论文的推荐。但我实测发现对 FFN 层也加 LoRAgate_proj、up_proj、down_proj效果更好代价是参数量翻倍。如果显存允许我建议全加如果紧张至少 q_proj 和 v_proj 要有。提示LoRA 的 alpha/r 比值决定了 LoRA 更新的缩放。alpha 固定时r 越大实际学习率越小。所以调 r 的时候要同步调 alpha保持 alpha/r 比值稳定否则学习动态会变。5.3 PEFT 实操LoRA 微调完整流程LoRA 微调的实操流程比全参微调简单因为不用管那么多显存优化。我用的是 HuggingFace 的 peft 库核心代码就几行from peft import LoraConfig, get_peft_model lora_config LoraConfig( r16, lora_alpha32, target_modules[q_proj, v_proj, gate_proj, up_proj, down_proj], lora_dropout0.05, biasnone, task_typeCAUSAL_LM ) model get_peft_model(base_model, lora_config) model.print_trainable_parameters() # 输出类似trainable params: 4.2M || all params: 110M || trainable%: 3.8%训练配置上LoRA 可以用比全参微调更大的学习率我用 1e-4 到 3e-4因为只训练少量参数不容易发散。epoch 数可以多一些3-5 轮因为过拟合风险低。batch size 可以开大因为显存占用小。训练完保存时只保存 LoRA adapter几十 MB不保存整个模型。推理时加载基座模型 adapter 即可。切换任务时换 adapter 就行基座模型共用。这个特性在多任务场景下特别香——我一个基座模型配了 5 个 adapter分别对应问答、总结、翻译、代码、写作硬盘占用比存 5 个完整模型小了一个数量级。这里有个坑LoRA 合并回基座模型后效果可能和分开加载略有差异。因为合并是 W BA浮点精度会有损失。如果对精度敏感建议推理时分开加载不要合并。我实测差异很小困惑度差 0.1 以内一般场景可以忽略。6. 蒸馏让小模型学到大师傅的本事6.1 知识蒸馏的核心思想蒸馏这个词听着玄乎其实逻辑很朴素让一个小模型学生去模仿一个大模型教师的输出。为什么有效因为大模型的输出不只是正确答案还包含了错误答案的概率分布这个分布携带了丰富的暗知识dark knowledge。比如教师模型看到一张图它说这是猫的概率 0.9狗 0.08兔子 0.02这个 0.08 和 0.02 就告诉学生猫和狗、兔子有点像这是硬标签one-hot给不了的信息。在语言模型里蒸馏通常用soft label教师模型对每个 token 位置输出一个概率分布学生模型去拟合这个分布而不是拟合硬标签。损失函数用 KL 散度衡量学生分布和教师分布的差异。温度参数 T 用来软化分布T 越大分布越平滑暗知识越明显。Xihe 的蒸馏我做了两种黑盒蒸馏只用教师的输出文本和白盒蒸馏用教师的 logits 分布。黑盒简单但信息少白盒效果好但需要能访问教师模型的内部输出。实际做的时候如果教师模型是开源的白盒蒸馏性价比最高。6.2 蒸馏实操从教师到学生的完整链路蒸馏的实操分几步。第一步是选教师。教师模型要足够强但也不能太强——如果教师是 GPT-4 级别学生太小根本学不动反而效果差。我的经验是教师参数量是学生的 10-50 倍比较合适。Xihe 上我用一个 1B 的模型当教师学生是 100M比例 10:1效果不错。第二步是准备蒸馏数据。可以用无标注文本让教师生成输出也可以用有标注数据让教师对每个样本输出分布。我用的是混合方式通用文本让教师自由生成任务数据让教师输出分布。第三步是训练学生。损失函数是硬标签 loss 和蒸馏 loss 的加权和import torch.nn.functional as F def distillation_loss(student_logits, teacher_logits, labels, T2.0, alpha0.5): # 蒸馏损失学生拟合教师的软分布 soft_loss F.kl_div( F.log_softmax(student_logits / T, dim-1), F.softmax(teacher_logits / T, dim-1), reductionbatchmean ) * (T * T) # 硬标签损失学生拟合真实标签 hard_loss F.cross_entropy(student_logits, labels) return alpha * soft_loss (1 - alpha) * hard_loss这里 T 和 alpha 是两个关键超参。T 我设 2.0alpha 设 0.5。T 太大分布太平学生学不到重点T 太小接近硬标签暗知识丢失。alpha 控制软硬损失的权重任务数据多时 alpha 可以小一点任务数据少时 alpha 大一点。第四步是评估。蒸馏效果看两个指标学生和教师输出的一致性用 KL 散度或准确率衡量以及学生在下游任务上的表现。我实测下来100M 的学生经过蒸馏在下游任务上能达到 1B 教师 80% 的水平而推理速度快了 10 倍性价比很高。注意蒸馏时教师模型要固定不能边训边更新。我早期试过让教师也微调结果教师和学生互相影响训练不稳定。教师就是标准答案要稳定。6.3 蒸馏的常见误区蒸馏看着简单坑不少。第一个误区是教师越强越好。前面说了教师太强学生学不动。我试过用 7B 教师蒸馏 100M 学生结果学生学了个四不像还不如用 1B 教师。原因是能力差距太大学生的容量装不下教师的知识。第二个误区是只蒸输出不蒸中间层。白盒蒸馏其实可以蒸中间层的特征让学生不仅学输出还学教师的内部表示。这个方法叫特征蒸馏效果通常比只蒸输出好。但实现复杂需要对齐学生和教师的层维度。Xihe 上我试了效果提升约 3-5%但工程量翻倍看性价比。第三个误区是蒸馏能替代预训练。蒸馏是锦上添花不是雪中送炭。学生模型本身要有基本的语言能力预训练过蒸馏才能把它拔高。如果学生是随机初始化的蒸馏根本训不动。7. DPO 直接偏好优化让模型懂人类喜好7.1 DPO 为什么能替代 RLHFSFT 之后的模型会回答问题了但回答质量参差不齐——有时候啰嗦有时候太简有时候答非所问。要让模型输出更符合人类喜好传统做法是 RLHF基于人类反馈的强化学习但 RLHF 流程复杂要训奖励模型再用 PPO 做强化学习超参多、不稳定、显存吃紧。DPO 的出现改变了这个局面。它的核心洞察是RLHF 的最优策略可以用一个简单的闭式解表示而这个解可以直接用偏好数据优化不需要显式训奖励模型。数学推导这里不展开你只需要知道DPO 把训奖励模型 PPO两步合并成一步直接用 chosen/rejected 对训练损失函数是def dpo_loss(policy_chosen_logps, policy_rejected_logps, ref_chosen_logps, ref_rejected_logps, beta0.1): chosen_rewards beta * (policy_chosen_logps - ref_chosen_logps) rejected_rewards beta * (policy_rejected_logps - ref_rejected_logps) loss -F.logsigmoid(chosen_rewards - rejected_rewards).mean() return loss其中 policy 是待训模型ref 是参考模型通常是 SFT 后的模型冻结beta 控制偏离参考模型的程度。这个损失函数的直觉是让 chosen 的奖励比 rejected 高同时不要偏离参考模型太远beta 控制。7.2 DPO 数据构造与训练要点DPO 的数据是偏好对同一个 prompt一个 chosen人类喜欢的回答一个 rejected人类不喜欢的回答。数据从哪来三个来源人工标注最准但贵、模型生成后人工排序性价比高、开源偏好数据集方便。数据构造的关键是chosen 和 rejected 的差异要有意义。如果两个回答差不多模型学不到东西如果差异太大比如一个完全跑题模型学到的可能是别跑题这种低级规则。理想的偏好对是两个回答都合理但一个更好更准确、更简洁、更符合指令。这种细微偏好最能提升模型。训练配置上DPO 的学习率要很小我用 5e-7 到 5e-6因为偏好优化是精细调整学习率大了会破坏 SFT 学到的能力。beta 我用 0.1这个值控制模型偏离参考模型的程度太小0.01学不动太大0.5容易过拟合偏好数据。epoch 数 1-2 轮就够多了会过拟合。提示DPO 之前一定要先做好 SFT。DPO 是在 SFT 基础上的精调如果 SFT 都没做好DPO 无从谈起。我见过有人跳过 SFT 直接 DPO结果模型输出乱七八糟因为参考模型本身就不行。7.3 DPO 效果评估与常见问题DPO 的效果评估比 SFT 难因为偏好是主观的。我用三个方法一是偏好准确率在留出的偏好测试集上看模型给 chosen 的分数是否高于 rejected这个指标直接反映 DPO 是否学到了偏好。二是人工评估让标注员对模型输出打分看 DPO 前后是否有提升。三是通用能力回归确认 DPO 没有破坏模型的其他能力。DPO 常见的问题有这么几个。第一个是奖励黑客模型学会了讨好偏好数据但泛化差。表现是在测试集上偏好准确率高但人工评估发现回答变得套路化。解决办法是增加偏好数据的多样性别让模型抓到单一模式。第二个是过度优化beta 太小或训太多轮模型偏离参考模型太远输出变得极端。表现是回答变得很长很啰嗦或者过度自信。解决办法是调大 beta减少训练轮数。第三个是参考模型选择参考模型通常是 SFT 后的模型但如果你有更好的基座也可以用。参考模型的质量决定了 DPO 的上限。我实测下来参考模型用 SFT 后的模型最稳用 CPT 后的模型效果差一些。8. 全链路串起来从零到可用的完整实操8.1 阶段衔接与数据流转把六个阶段串起来数据流转是这样的预训练用海量无标注文本产出基座模型CPT 用领域语料产出领域基座SFT 用指令-回答数据产出对话模型PEFT 可以在任意阶段插入用少量参数做适配蒸馏可以贯穿全程用大模型指导小模型DPO 在 SFT 之后用偏好数据精调。实际做的时候不是每个阶段都要全走。我给你三个典型组合快速上手组合预训练小数据→ SFT → PEFT。适合想快速跑通流程、验证想法的人。领域专家组合预训练 → CPT → SFT → PEFT。适合做垂直领域应用的人。极致性价比组合预训练 → 蒸馏 → SFT → DPO。适合算力有限但想要高质量输出的人。每个阶段的产出都要保存 checkpoint方便回滚和对比。我习惯每个阶段存三个版本最新、最佳验证 loss 最低、以及阶段性快照。这样出问题能快速定位是哪个阶段引入的。8.2 资源规划与时间估算全链路跑下来资源需求和时间我列个表方便你规划阶段数据量显存需求单卡时间A100关键产出预训练2B token40GB2-3 天基座模型CPT50M token40GB4-6 小时领域基座SFT1 万条24GB1-2 小时对话模型PEFT1 万条16GB30 分钟Adapter蒸馏100 万条40GB6-12 小时学生模型DPO5000 对24GB1-2 小时偏好模型显存不够的话用梯度累积、混合精度fp16/bf16、梯度检查点gradient checkpointing这些技术能省不少。我用 fp16 梯度检查点把预训练的显存需求从 80GB 压到了 40GB代价是训练速度慢 30%。这个取舍看你的硬件。8.3 效果评估体系训完模型怎么知道好不好我建了一套评估体系分三层第一层是自动指标困惑度看语言建模能力、BLEU/ROUGE看生成质量、准确率看任务表现。这些指标快但片面只能做粗筛。第二层是模型评估用强模型如 GPT-4给弱模型的输出打分或者做 pairwise 对比。这个方法比自动指标全面但有偏见强模型可能偏好自己的风格。第三层是人工评估让标注员对输出打分维度包括准确性、流畅性、有用性、安全性。这是最可靠的但最贵。我通常用自动指标做日常监控人工评估做关键节点验收。评估数据要和训练数据严格分离否则指标虚高。我见过有人评估集和训练集重叠指标漂亮得不行上线就崩。评估集要覆盖各种场景简单问答、复杂推理、长文本、多轮对话、边界情况。9. 踩坑实录与排查速查表9.1 训练不收敛的排查思路训练不收敛是最常见的问题表现是 loss 不降、震荡、或者变 nan。排查顺序我总结成一张表现象可能原因排查方法解决办法loss 变 nan学习率太大看前几百步 loss降学习率加 warmuploss 震荡batch 太小看 loss 曲线增大 batch 或梯度累积loss 不降数据有问题检查数据质量重新清洗数据loss 降但生成差过拟合看验证 loss早停加正则训练慢序列太长看显存占用缩短序列用梯度检查点我踩过最坑的一次是 loss 一直不降排查了半天发现是数据里混入了大量重复样本模型在背重复数据loss 卡在一个值下不去。重新去重后正常了。所以数据问题永远是第一嫌疑。9.2 生成质量差的常见原因模型训完了但生成质量差原因可能有很多。如果生成乱码多半是预训练没训好或者分词器有问题。如果生成重复是解码策略问题调一下 repetition penalty 或者用 beam search。如果答非所问是 SFT 数据质量问题指令和回答不匹配。如果回答太短是 SFT 数据里短回答太多模型学会了偷懒。如果回答有风险内容是数据清洗没做好或者缺少拒答样本。排查生成问题我习惯先看训练数据再看解码参数最后才怀疑模型结构。90% 的问题都在数据和配置上模型结构出问题的概率很低。9.3 显存不足的优化手段显存不足是实操中最现实的障碍。优化手段按性价比排序混合精度训练fp16/bf16省 30-50% 显存几乎无损首选。梯度检查点省 50-70% 显存代价是慢 20-30%。梯度累积用小 batch 模拟大 batch不省显存但能训。LoRA/PEFT只训少量参数省 80% 显存。模型并行多卡分摊省显存但通信开销大。CPU offload把优化器状态放 CPU省显存但极慢。我的建议是先上混合精度 梯度检查点还不够就上 LoRA再不够才考虑多卡。单卡能解决的事别搞多卡通信和调试成本太高。10. 我个人的一些实操体会10.1 小模型训练的几个反直觉经验做了这么多轮实验有几个经验是反直觉的分享出来。第一数据质量的影响远大于模型大小。我用 100M 模型 高质量数据效果超过 300M 模型 低质量数据。所以别急着加参数先把数据搞好。第二SFT 数据不是越多越好。5000 条精选数据的效果往往超过 5 万条普通数据。因为 SFT 是格式对齐少量高质量样本就能教会模型多了反而引入噪声。第三蒸馏的性价比在算力有限时最高。如果你只有一张卡与其硬训大模型不如训个小模型再蒸馏效果和成本都更优。第四DPO 不是必须的。很多场景 SFT 就够了DPO 是锦上添花。如果偏好数据不好搞跳过 DPO 也没问题。10.2 后续可以怎么扩展这条链路跑通之后能扩展的方向很多。一是多模态把文本模型扩展成能处理图像、音频的模型。二是 MoE混合专家用多个小专家替代一个大模型推理时只激活部分专家性价比高。三是长上下文把序列长度从 1024 扩展到 8K、32K处理长文档。四是量化部署把模型量化到 int8/int4降低推理成本。每个方向都有坑但底层逻辑是相通的——都是在这条预训练-微调-对齐的链路上做文章。把 Xihe 这条链路吃透再学新东西就是触类旁通。最后分享一个小技巧训练日志一定要详细记录包括超参、数据版本、loss 曲线、生成样本。我习惯用 tensorboard 文本日志双记录出问题能快速回溯。很多坑当时不觉得过两周再看日志才发现规律。这个习惯帮我省了无数排查时间。
