0.1B小语言模型从零训练全流程:CPT、SFT、LoRA、蒸馏与DPO实践
发布时间:2026/9/28 9:00:01 锦皓数字建站

写这篇东西之前我先说个背景我自己训练了一个叫 Xihe 的小语言模型参数量只有约 1 亿0.1B。这个体量在大模型圈子里属于玩具级但整个链路一点没省数据清洗、分词器训练、预训练、CPT继续预训练、SFT监督微调、PEFTLoRA、知识蒸馏、DPO一个不落全跑了一遍。如果你也想从零走一遍小语言模型的完整生命周期这篇基本就是照着我的实验记录抄作业。为什么非要从零预训练而不是直接拿开源模型微调这是很多人问我的第一个问题。答案是预训练一个 0.1B 的模型成本低到可以接受单卡 A100 大概几天消费级 24G 显存也能慢慢跑而且你能真正看清每个环节的因果关系。微调开源模型当然快但你永远不知道数据清洗对最终效果的影响有多大分词器怎么设计才匹配领域这些底层问题。我的原则是如果你想把 SFT、PEFT、DPO 这些事真正吃透至少完整跑一遍从零预训练的过程否则你只是在黑盒外面瞎猜。这篇文章按项目推进的时间线写每步给出配置、代码、理由和你大概率会踩的坑。适合有深度学习基础、能跑 PyTorch 训练脚本、想动手训一个自己的小模型的读者。纯文科背景也建议先看到 SFT 部分再往后可能就要先补 PyTorch 底子了。1. 整体设计Xihe 项目的定位与全链路拆解1.1 为什么选 0.1B 这个体量我先算笔账。0.1B 参数的 decoder-only 模型fp16 训练显存占用大概 2~3GB 起步加上梯度、优化器状态单卡 24G 完全够用。训练数据我用的是 15 亿 token 左右的中文语料在单张 A100 上跑了不到三天。这个性价比意味着你可以随意试错数据清洗策略不对重跑一次也就几小时学习率没调好重新来一发成本也低。这就是小模型的独特价值——它是实验场不是生产线。但体量小也意味着天花板低。预训练阶段模型能学会流畅的文本接龙但常识覆盖、复杂推理能力都不够。所以我的整体思路是把预训练当作让模型学会说话的基础工程把后续的 SFT、蒸馏、DPO 当成让模型学会说人话、说有用的话的打磨工程。每一步解决一个层面的问题缺一环效果都立不住。1.2 技术栈与工具链选型我的技术栈很简单PyTorch HuggingFace Transformers Datasets Accelerate PEFT TRL。为什么不用 DeepSpeed因为 0.1B 的体量用不到反而会增加调试成本。为什么不用分布式单卡就能跑分布式纯属自找麻烦。分词器这块我犯过一个错误直接拿了现成的中文 BERT 词表来用。后来发现 RoBERTa 的中文词表对文言文、行业术语的覆盖非常差导致预训练 loss 一直降不下去。所以最后我基于全部语料重新训练了一个 BPE 分词器词表大小 25K。这个决策背后的逻辑是分词是模型认知文字的第一步词表和你领域的匹配度直接决定训练上限。1.3 全流程流水线总览训练链路分成五个阶段像流水线一样串起来预训练Pre-training从随机权重开始让模型学会中文文本的基本规律。CPTContinue Pre-Training在通用预训练基础上用领域语料继续训练让模型熟悉你的业务语境。SFTSupervised Fine-Tuning用指令-回复对让模型学会问答格式。蒸馏Knowledge Distillation用大模型当老师给 Xihe 灌软标签提升小模型的表达质量。DPODirect Preference Optimization用偏好对数据校准模型的审美让模型学会拒绝乱答。每个阶段结束后我都做了 checkpoint 保存和简单评测这是后面排查问题的根基。懒得分阶段保存的人后面出了问题连回滚都做不到。2. 预训练从随机权重到能说完整句子2.1 数据清洗与配比预训练语料我用了中文维基百科、开源中文书籍、新闻、社区问答四类数据总规模 15 亿 token。比例大约是 2:3:3:2。为什么这么配维基百科正规但术语密集书籍内容连贯但偏书面新闻时效性强但句式单调社区问答口语化但噪声大。四类混着来模型才不会变成偏科生。清洗这一步我在上面吃过亏。一版我只做了基础的 HTML 去标签和空行压缩结果明明 token 数量很大训练 loss 却一直下不去。后来排查发现是语料里有大量重复的广告文案、采集乱码、中英文混排的时间戳。补做一遍去重用 MinHash SimHash 做近重复检测、去低质量行按长度、符号占比、困惑度过滤之后同样的训练步数loss 降了 0.35 左右。数据质量是预训练的天花板清洗的每一分钟都换算成最终效果。2.2 分词器训练与参数细节分词器我用了 HuggingFace 的 BPE 实现关键参数如下from tokenizers import Tokenizer, models, trainers, pre_tokenizers tokenizer Tokenizer(models.BPE(unk_tokenunk)) tokenizer.pre_tokenizer pre_tokenizers.ByteLevel(add_prefix_spaceFalse) trainer trainers.BpeTrainer( vocab_size25000, min_frequency2, special_tokens[unk, s, /s, pad], show_progressTrue ) # 用全部清洗后的语料训练分词器 tokenizer.train(files[all_corpus.txt], trainertrainer) # 注意byte-level 模式下英文大小写和空格都会被拆成byte # 中文按字/词边界聚合成token词汇表里会混着大量中文单字和双字组合训练完成之后我用验证集测了三点词表覆盖率理想状态 OOV 率低于 0.2%、平均切分长度中文单字或双字的比例是否合理、乱码字符是否被正确映射到 unk。很多教程忽略这一步结果就是训练开始后才发现 vocab 有问题那时候返工成本极高。2.3 模型结构、训练超参与损失观察Xihe 的基础结构我直接沿用了 GPT-2 的 decoder-only 设计12 层 Transformerhidden size 76812 个注意力头参数量大约 0.1B。训练超参数如下表配置项数值备注序列长度512短序列能提升训练稳定性先跑通链路Batch Size128单卡 A100 刚好够学习率3e-4warmup 2000 步后 cosine 衰减优化器AdamWbeta20.95weight_decay0.1训练步数12 万步大约过了 3 个 epoch预训练阶段我看最关键的两个信号一是 train loss 的下降曲线是否顺畅二是生成质量是否从胡言乱语过渡到句子通顺。如果前 5000 步 loss 不降先别调模型结构检查数据流水线是不是有 bug比如数据是否喂错、mask 是否实现正确、batch 里是否混入了空序列。我踩过最大的坑是数据并行时忘记设置 seed 导致每卡数据不一致看起来 loss 在降实际上模型在学随机噪声。这类问题往往要到 SFT 阶段才发现排查成本巨大所以预训练阶段就要把可复现性做扎实。3. CPT 继续预训练把模型拉进垂直赛道3.1 什么是 CPT为什么需要它CPT 的全称是 Continue Pre-Training中文叫继续预训练。它和预训练的本质区别在于语料预训练用的是海量通用语料CPT 用的是定向领域语料。打个比方预训练是让一个人接受通识教育CPT 是让他去读硕博专业课程。这时候模型已经掌握了语言的基本规律CPT 的任务不是重新学说话而是强化特定领域的词汇和句式分布。我的 Xihe 做的是医疗问答方向的落地验证所以 CPT 语料用了一批脱敏的结构化医学问答、权威医学百科条目大约 2 亿 token。比例上我没有把通用语料全部丢弃而是按 7:3 混合领域语料和通用语料继续训练。为什么要掺通用语料防止灾难性遗忘。模型一旦只吃医学数据会很迅速地遗忘掉通用常识导致后面做开放问答时三句不离医学答非所问。3.2 CPT 训练参数小学习率少步数CPT 不是从零开始所以训练策略和预训练完全不同。我的配置是配置项数值说明学习率1e-4比预训练低一个量级防止破坏原有表示训练步数1.5 万步数据量小过 2 个 epoch 就够学习率调度warmup 500 步后线性衰减不要用 cosine步数太短效果不明显冻结层不冻结0.1B 的模型没必要用 layer freeze我还加了关键的一步CPT 训练全程记录领域困惑度perplexity的变化。这个指标能直观反映模型对医疗文本的敏感度是否提高。如果领域困惑度下降速度太慢大概率是领域语料里噪声太多或者领域专用 token 没有被分词器覆盖。此时你应该回头扩充分词器词表而不是硬着头皮增大训练步数——我试过后者效果极度有限。3.3 CPT 最容易犯的错误目标函数不对齐有人做 CPT 直接用 MLM掩码语言模型目标但 Xihe 是 decoder-only 架构对应的目标是自回归语言建模预测下一个 token。这事听起来简单实操中我被一个 bug 坑了两个小时加载预训练权重时没有正确映射 embedding 层的参数名导致 CPT 一开始模型权重是随机初始化的loss 直接从 9 开往下降。本来只想让小模型转专业结果等于从零重学了。所以我强烈建议每次接续训练前先打印模型参数量和加载的权重数量是否一致再对一条样本做前向看 logits 形状和随机初始化时是否有本质差异。4. SFT 监督微调教模型学会对话4.1 数据是 SFT 的灵魂模板决定格式感预训练和 CPT 之后Xihe 还是一个接龙机器。你输入今天天气它的输出往往是今天天气晴朗适合出行。云层较厚请注意防晒这类续写。SFT 要解决的就是把接龙变成问答。这一步的本质是让模型学习一种格式看到 Question 标记就启动回答看到 Answer 标记就停止生成。SFT 用的数据格式如下|user| 为什么感冒时会发烧 |assistant| 感冒引起发烧是人体免疫系统的正常反应。当病毒或细菌入侵时免疫细胞会释放致热因子作用于下丘脑体温调节中枢导致体温升高。这种模板必须从头到尾保持一致包括特殊 token 的写法。SFT 时我把样本按 9:1 切训练集和验证集总共用了约 20 万条指令-回复对。数据来源包括公开指令数据集、用大模型生成的多样化问答、带人工修正的领域问答三部分。注意数据不求多求质量。我在实验中发现只用 5 万条高质量数据的效果明显好于 20 万条未经严格清洗的混合数据。低质量数据会让模型学会正确但空洞的回话风格。4.2 SFT 的训练细节全程只更新回复部分SFT 的损失函数和预训练一样都是交叉熵但关键在于 mask 设置。我的做法是只在 Assistant 部分计算 loss用户指令部分不参与损失计算。这一步如果不做模型会把提问和回答当成同等重要的任务来学导致回答时注意力分散。TRL 的 SFTTrainer 内置了这个功能但我建议你手写两份数据处理逻辑一份 SQL 标注结果一份直接可视化摸查数据是否对齐尤其在 batch 组织时不要 shuffle 错位。超参数方面我的 SFT 阶段学习率设为 2e-5batch size 32训练 3 个 epoch。注意 SFT 非常容易过拟合如果验证集的 loss 在第 2 个 epoch 就开始反弹立即停止训练并保留第 2 个 epoch 的 checkpoint。别贪SFT 不是练得越久越好。4.3 如何判断 SFT 是否成功最直接的方法是裸用检查关闭任何采样技巧temperature1, top_p1随机挑 50 条训练集没见过的提问看模型能否产出格式正确、语义相关的回答。这里有一个我上过当的细节模型如果复读了用户的问题或者回答前带了一大堆您好我想问一下感谢您的提问说明 SFT 数据里的提问标签污染到了回答区。出现这种症状时优先检查模板拼接是否把 user 和 assistant 的边界搞混了而不是去调学习率。5. PEFT 参数高效微调用 LoRA 做最后一公里5.1 PEFT 选型为什么是 LoRA 而不是全量微调到这里Xihe 已经完成了预训练、CPT、SFT 三个阶段基础能力已经成型。但是我在实际应用中会遇到业务方频繁调整需求今天要适配医疗问答明天要加一个法律咨询场景。每次全量微调一个专用模型时间成本和存储成本都不划算。于是 PEFT 进入了流程。PEFT 全称是 Parameter-Efficient Fine-Tuning参数高效微调。它的思想很直白冻结原始模型的大部分参数只训练一小部分额外的参数学新任务。我在实验里试过三种Adapter、Prefix-Tuning、LoRA。最终选了 LoRA。原因很简单LoRA 训练完只多一个很小的 adapter 文件我这个规模只有几十 MB而且可以随时切换——同一个基座模型插上不同的 adapter 就变成不同场景的模型不需要维护多个完整副本。LoRA 的原理用大白话讲就是把权重更新矩阵分解成两个低秩矩阵的乘积。原始层的权重冻结不动训练时只学一个低秩的增量。推理时把增量和原始权重相加或保持分离效果差不多。5.2 LoRA 关键参数解析与配置我用的 LoRA 配置如下from peft import LoraConfig, get_peft_model lora_config LoraConfig( r16, # 低秩矩阵的秩决定额外参数量 lora_alpha32, # 缩放系数alpha/r 是实际缩放比例 lora_dropout0.05, # 防止小模型微调过拟合 target_modules[q_proj, v_proj], # 只作用于注意力层的 Q、V 矩阵 biasnone, # 不训练偏置进一步减少参数 task_typeCAUSAL_LM ) model get_peft_model(base_model, lora_config) print(model.print_trainable_parameters())为什么 target_modules 我选 q_proj 和 v_proj一个经验之谈在小模型上训练 Q查询和 V值矩阵已经能捕捉到绝大多数任务相关模式如果全矩阵投影层都注入 LoRA训练参数量虽然也不大还是远小于全量微调但过拟合风险明显上升尤其当你的 SFT 数据只有几万条时。r16 的参数量本来就够小模型挪用你完全不需要冒险去用 r64。LoRA 训练时学习率我设为 3e-4是 SFT 全量微调学习率的 10 倍左右。为什么可以这么大因为 LoRA 只动极小一部分参数学习率太小会训练半天没动静但学习率过大又会让新增增量破坏原有权重。3e-4 是实测下来比较稳妥的起点。5.3 LoRA 和 PEFT 的坑合并权重与批量推理LoRA 在训练和推理阶段有一个关键区别。训练时模型结构里多了一份 adapter 分支推理时要决定是否合并权重。我的实践经验是在评估阶段不合并、直接用 adapter 联合推理最稳妥如果线上部署为了速度必须合并用model model.merge_and_unload()合并后务必用一条训练集样本跑一遍前向和前一次输出对比验证 merge 是否正确。我遇到过一次 merge 之后输出发生漂移原因是 merge 过程把非 LoRA 注入层也调整了 scale复查之后发现是需要把lora_alpha和r的比例设对这个比例错了会在 merge 后引入额外缩放。PEFT 的另一个坑是适配器和基座模型的兼容性。我每次换基座模型版本后都重新训一次 LoRA即便只差一个小版本。不要图省事直接加载旧 adapter——张量名对不上是小事语义空间变了才是大事。6. 知识蒸馏与 DPO算法层面补齐最后短板6.1 蒸馏用大模型的软标签给小模型灌内力SFT 之后的 Xihe 已经能回答了但答案的流畅度和信息密度总感觉机械。这时候蒸馏出场。蒸馏的核心思想是让小模型学习大模型的输出分布而不是只学一个硬标签。硬标签是答案是A软标签是答案是A 且 B 也有一定概率、C 微乎其微。软标签里藏着大模型对语言不确定性的理解这是小模型自己学不来的。我的蒸馏方案选的是中间层蒸馏 输出层蒸馏的结合。输出层用 KL 散度对齐学生和老师的 logits中间层用 MSE 对齐某些层的 hidden state。具体实现时需要冻结老师模型只训练学生模型。因为 Xihe 本身只有 0.1B我选择的老师模型是一个 7B 级别的通用大模型对同一批指令生成带温度的回答作为软标签。损失函数大概长这样import torch import torch.nn.functional as F def distillation_loss(student_logits, teacher_logits, temperature3.0): # 软化双方概率分布 s F.log_softmax(student_logits / temperature, dim-1) t F.softmax(teacher_logits / temperature, dim-1) # KL散度衡量分布差异 kd_loss F.kl_div(s, t, reductionbatchmean) * (temperature ** 2) return kd_loss为什么要乘以 temperature 的平方因为蒸馏时温度调高了logits 分布变得更加平滑梯度尺度会变小。temperature ** 2这个系数是让损失尺度恢复到原始温度下的水平否则你会发现蒸馏 loss 在训练时损失数值小得令人困惑。6.2 DPO让模型学会拒绝乱答DPO 是 Direct Preference Optimization 的缩写直接偏好优化。很多同学对 DPO 的理解是这又是某种 RLHF但 DPO 相比 RLHF 的差别很大它不需要单独训练一个奖励模型也不需要复杂的强化学习采样过程。DPO 的数据是偏好对形如同一个问题回答 A 比回答 B 好。损失函数直接优化策略模型让它倾向好的回答、远离差的回答。为什么 SFT 之后还需要 DPO因为 SFT 只教模型标准答案长什么样没教模型什么叫不合适的答案。实际模型中我发现Xihe 面对不知道的问题时会强行编造答案这是 SFT 数据的天然缺陷——标注者只写了正确回复从没告诉模型不会的题要承认不会。DPO 专门解决这个问题。偏好数据我构造了三类事实正确 vs 事实错误幻觉诚实承认不知道 vs 胡编乱造简洁直接 vs 啰嗦空洞每类各收集了约 1 万对数据。构造方法上一部分是人工写的一部分用大模型先生成坏答案、再人工改写好答案。对偏好对的质量要求比对数量高得多——如果坏答案其实也不错DPO 就会学一个模棱两可的偏好模型反而变得胆小、什么都不敢答。6.3 DPO 训练的超参与细节DPO 训练时我基于一个关键前提不能直接把基础模型拿来做 DPO必须先完成 SFT。DPO 的目标是优化偏好如果模型连基本指令格式都不会它只能学会比较格式无法学会比较内容价值。DPO 的代表性超参如下参数数值作用beta温度0.1控制对偏好差异的敏感度越小越激进学习率1e-6极低DPO 非常容易过度优化batch size16小 batch防止偏好对内噪声干扰epoch1训练一轮就停多练必过拟合beta 的含义需要展开讲。beta 越大模型越保守只做微调beta 越小模型越激进地贴近好答案、远离坏答案。我试过 beta0.01训练到 1000 步后模型开始输出书面感十足但答案空洞的内容就是过度优化的典型症状。回到 beta0.1 之后正常。这里没有通用最优值必须拿着验证集看人工评测结果调。7. 评测结果与避坑经验实录7.1 各阶段效果对比量化与主观感受整个链路跑完我对 Xihe 做了阶段性的评测。结论用一张表说清楚阶段通用能力续写通顺度领域能力医疗问答准确率指令遵循格式准确性幻觉率事实性错误比例预训练完成高低领域词都不熟无高根本不会问答CPT 完成高中词汇熟悉了无高SFT 完成中中高回答格式正确中蒸馏完成中高中高高中DPO 完成中高高准确率明显提升高低实际的主观体验是DPO 这一步是画龙点睛。之前 Xihe 的回答是对但有点像背课文DPO 之后明显变得自然而且面对过敏史、剂量换算这类不擅长的问题会主动说建议咨询专业医师而不是硬编一个数字。7.2 我踩过的五个坑依次列给你第一数据流水线的可复现性问题。训练 SFT 时我发现结果忽好忽坏排查到最后是每次运行前数据集 shuffle 没固定 seed导致数据划分不一致。任何训练脚本里seed_everything(42)要放在读取数据之前而不是放在初始化模型之后。第二分词器词表与领域不匹配。这个最隐蔽。预训练阶段用通用词表没毛病但 CPT 之前我建议重新检查词表覆盖把领域高频词加入词表用 medium 级别的词表25K-32K做扩展。否则 CPT 训练时领域关键词会被切得稀碎模型学起来事倍功半。第三SFT 训练 loss 下降很快但生成质量差。有一次 SFT 的 loss 漂亮得让我以为万事大吉一生成发现模型只输出好的没问题这类短句。原因是数据集中短回复数量太多模型学会了走捷径。后来我在数据处理时专门过滤掉过短的回复并控制长回复的比例。数据分布天然决定模型行为这个规律越到后面越明显。第四蒸馏的 teacher 模型 logits 没加温度。如果不加温度teacher 的分布太尖锐小模型学到的几乎是硬标签蒸馏效果接近于零。一定记得在训练脚本里给 teacher 前向加上 temperature scaling。第五DPO 训练时 ref_model参考模型没冻结。DPO 原理里有一个关键设计是参考策略不能跟随训练更新否则模型会走捷径通过降低所有回答的概率来满足偏好约束。务必把 ref_model 的参数requires_grad False并切到 eval 模式。7.3 后续可以怎么扩展这套链路Xihe 的这套链路在工程上的意义是你完全可以在 24G 显存的消费级显卡上复制。如果你想把项目继续往下走我有几个方向给你参考。一是把 LoRA 用起来做多任务矩阵让 Xihe 同时挂医疗、法律、客服三个 adapter按路由分发请求。二是把蒸馏的环节做细比如引入多层特征蒸馏而不是只对齐输出层这对小模型的收敛帮助非常明显。三是在 DPO 之后再加一层 RLHF如果你有足够算力和标注人力DPO 是轻量偏好对齐RLHF 能做更复杂的奖励建模但工程复杂度翻倍小规模实验不划算。最后的实操心得单独提一个我体会最深的小技巧每个阶段训练完了不要只存最优 checkpoint把每个 epoch 末尾的 checkpoint 都存一份。我在 CPT 阶段犯过只存最终模型、结果发现最终模型过拟合领域语料导致通用能力骤降的错回退到倒数第二个 epoch 的 checkpoint 才恢复正常。多存几个 checkpoint 的成本很低它能让你在调参时随时回滚是性价比最高的好习惯。其次小模型训练的每一步都要可视化。我说的不是 tensorboard 那种曲线而是真实生成样例。预训练阶段每 2000 步打印一次生成结果SFT 阶段每 500 步拿验证集提问并打印回答。光看 loss 曲线你永远不知道模型实际在怎么回答用户。图像是骗不了你的文本照样会骗你。Xihe 这套流程我最满意的地方不在于最终效果有多惊艳而在于每一个环节的因果关系都是可解释的。这让我在遇到问题时能精准定位而不是在工程的泥潭里打转。希望这篇记录能帮你在从零训练小语言模型的路上少踩几个坑。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。