前阵子帮一个朋友排查模型训练效果,loss曲线看着挺正常,但生成结果前言不搭后语。后来一层层查下去,问题不在模型结构,也不在训练框架,而是它喂进去的SFT数据里混了一大批重复条目和几个漏清洗的网页标签。
这件事其实特别典型:从0做轻量级大模型全链路训练与对齐框架,最花时间的往往不是写训练脚本,而是把“数据适配”这件事做扎实。预训练要用干净语料,指令微调要用对话结构,偏好对齐要构造成对样本,每个阶段对数据的形态、质量和数量要求都不一样。这套数据工程做不好,后面所有环节都会被拖累。
这篇内容围绕数据适配展开,覆盖预训练、SFT、DPO三个阶段的数据处理思路、格式规范、清洗方法和踩坑记录。适合准备从0搭建轻量级训练流水线的开发者,也适合那些已经在跑开源训练框架、但效果始终不太理想的团队参考。
1. 全链路框架概览与数据适配的定位
1.1 轻量级训练与对齐框架的典型组成
先明确“轻量级”的边界。我习惯把它理解为10B参数以下、单机多卡或单卡就能完成主要训练流程的模型规模。对应的训练链路一般是四段:继续预训练或领域增量预训练、SFT指令微调、偏好对齐(DPO或RLHF)、评估验证。
这四段里,前两段负责“能力”,后两段负责“听话”。继续预训练让模型掌握领域知识,SFT让模型学会按照指令输出,偏好对齐让模型学会选择人类更认可的答案。每一个阶段都在改模型权重,但权重改得好不好,很大程度取决于数据和标签。
很多人把全链路训练理解成“从头预训练一个大模型”,实际工程里更常见的是在开源底座上做继续预训练加微调对齐。即便是这样的轻量路线,数据适配的工作量依然能占到整个项目周期的六成以上。我见过有些项目训练脚本两周就写好,数据修了两个月还在改。
1.2 数据适配为什么是隐形瓶颈
原因很朴素:模型结构是固定的,训练框架是现成的,只有数据是每个项目独有的。同样一段开源训练脚本,换一批数据,效果可以天差地别。
数据适配要解决的典型问题包括:原始语料格式混乱,CSV、HTML、PDF、TXT什么都有;文本质量参差不齐,重复、广告、乱码、无关内容混杂;每个训练阶段的数据形态完全不同,无法共用一套处理逻辑;数据量一旦上来,CPU侧的数据处理速度跟不上GPU消费速度,训练时GPU经常空转。
我之前帮某个模拟项目做诊断,发现SFT训练集里约12%的样本是重复的,还有大约3%的样本没清洗干净,带着HTML标签。模型学到的不是任务逻辑,而是“标签结构和重复句子的概率”。这就是典型的数据适配没做到位。
另外,各阶段对数据的关注点完全不同。可以用一张表概括。
| 训练阶段 | 数据形态 | 典型规模 | 核心目标 | 常见问题 |
|---|---|---|---|---|
| 预训练 / 继续预训练 | 纯文本片段 | 数十万至上百万条文档 | 知识注入、语言建模 | 重复、质量低、语种混杂 |
| SFT | 多轮对话结构 | 1万至10万条 | 指令跟随、输出格式对齐 | 模板单一、答案质量差 |
| DPO / RLHF | 偏好二元组 | 数千至两万对 | 价值观对齐、风格偏好 | 正负样本差异不明显 |
认清这个结构之后,再去看数据适配,就不再是“写两个正则清理脚本”那么简单了。它是一条完整的工程流水线。
2. 数据管线设计与工具链选型
2.1 先统一“落盘格式”
数据适配的第一步不是写清洗函数,而是确定所有阶段共用的落盘格式。我强烈推荐JSONL,一行一条样本,理由有三个:一是流式读取友好,几十GB的文件不需要一次性加载;二是断点续传容易,处理到一半崩了,从最后成功行继续即可;三是每条样本自包含,字段可以灵活扩展。
各阶段建议的字段结构如下。
预训练数据:
{ "id": "pretrain_0000001", "text": "这是一段清洗后的自然文本……", "source": "article", "lang": "zh", "quality_score": 0.92, "dup_hash": "a3f2e9b8c7d6" }SFT数据:
{ "id": "sft_0000001", "messages": [ {"role": "system", "content": "你是一个乐于助人的中文助手。"}, {"role": "user", "content": "介绍一下数据工程中的清洗流程。"}, {"role": "assistant", "content": "数据清洗通常包括去重、格式归一化、质量过滤等步骤……"} ], "task_type": "explanation", "difficulty": "normal" }DPO数据:
{ "id": "dpo_0000001", "prompt": "写一封简短的工作周报,包含三方面内容。", "chosen": "本周完成了数据管线的重构……", "rejected": "这周做了很多事情,详见文档。", "task_type": "writing", "length_chosen": 120, "length_rejected": 15 }统一格式还有个额外好处:每个阶段的数据都能用同一套校验脚本检查字段完整性、统计长度分布。后面训练出问题,直接回到数据层查元信息就行。
2.2 工具链怎么选才不折腾
工具选择上,我吃过亏,所以给出的建议比较保守。数据规模在几GB级别,用Python自带的json、re配合并行处理就足够了;数据量到了几十GB,优先考虑polars或duckdb这类列式处理工具,内存占用比pandas低很多,过滤和聚合操作写起来也更舒服。
不要去搞一整套大数据平台。某个项目我见过一上来就部署了完整的任务调度系统,配置管理、失败重试都齐全,结果数据团队每天都在调调度配置,而不是在处理数据。轻量级项目,最合理的方式是用Python脚本加Makefile管理,或者直接用Shell脚本串起阶段任务。
语言过滤可以用fasttext的文本分类模型,重复检测可以用MinHash加LSH,网页正文提取可以基于文本密度算法实现。这些都是开源算法工具,按需引入就好。关键是不要追求工具的大而全,要追求“一个阶段一个明确工具,输出格式始终统一”。
2.3 一条可复用的ETL流水线
数据适配我习惯分成三步:抽取、转换、加载,也就是ETL。这里不是泛泛讲概念,而是直接落成代码层面的目录结构。
data_pipeline/ extract/ raw_to_jsonl.py transform/ clean_text.py dedup.py filter_rules.py format_sft.py format_dpo.py load/ build_dataset.py split_train_eval.py run_all.sh每个脚本只做一件事。clean_text.py只负责文本清洗,dedup.py只负责去重,输入是上一环节的JSONL,输出是加了新字段的JSONL。这样哪个环节出了问题,单独重跑那一段就行,不会把前面的清洗结果也覆盖掉。
我在实际执行时,会在每个环节输出一个统计文件,记录处理前后的条数、总字符数、平均长度、重复率。这个习惯帮我排查了很多问题,后面会单独讲。
3. 三个阶段的数据构造与实操细节
3.1 预训练阶段:从“裸语料”到“干净文本”
继续预训练的数据适配,核心目标是给模型提供高质量、低重复、分布合理的文本。这个阶段不讲究格式,不讲究对话结构,只讲究文本本身的纯净度。
首先是清洗。我用一个相对标准的清洗流程,按顺序处理:移除HTML标签和转义实体、把全角空格和特殊空白统一为普通空格、删除控制字符、过滤过短文本、识别并移除样板内容。这个流程里的关键点是“顺序”,先做结构层面的清洗,再做内容层面的规则过滤。
import re import html def clean_text(text, min_len=80): # 1. 去除HTML标签 text = re.sub(r'<[^>]+>', '', text) text = html.unescape(text) # 2. 统一空白字符 text = text.replace('\u3000', ' ').replace('\t', ' ') text = re.sub(r'[ \t]{2,}', ' ', text) # 3. 删除控制字符,保留换行和制表符 text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', '', text) # 4. 合并多余空行 text = re.sub(r'\n{3,}', '\n\n', text) text = text.strip() if len(text) < min_len: return None return text这只是一个示例函数,实际生产里还会加语言识别、重复句比例计算。我通常按“文档级重复”和“句子级重复”两个维度过滤。文档级重复用MinHash算全文指纹,句子级重复则是把长文本切成连续片段,统计片段之间的重合度。如果一个文档里超过30%的句子片段与自身其他部分重复,大概率是拼凑或低质量内容,可以直接淘汰。
过滤规则要分优先级。第一优先级是硬性规则,比如长度、语言、特殊字符比例;第二优先级是软性规则,比如重复率、质量分。硬性规则直接丢,软性规则按分数排序截断。
行业公开语料、技术文档、代码片段都是常见来源,但混配比要控制。一般技术类语料可以占到30%左右,通用自然文本占其余部分。配比要看使用场景,代码相关需求多就把代码语料提高,但不要让单一来源的文本占比过高,否则模型会出现局部过拟合,表现为对特定写作风格的强模仿。
3.2 指令微调阶段:把“文本”变成“对话示例”
SFT是数据适配环节里最讲究“设计感”的阶段。因为同样的内容,不同的模板结构、不同的角色切分,训练出的结果完全不同。
SFT数据的核心结构是messages数组,包含system、user、assistant三种角色。这里的system提示词不是随意写的,它定义了模型在整段对话中的行为基准。我见过不少人把所有样本都塞一个很长的system提示,强调“你是可靠的助手请不要幻觉”,结果模型在推理时过度谨慎,动不动就拒绝回答。这个在SFT阶段尤其要注意,system提示词保持简洁,让任务特征从user和assistant的交互里体现。
多轮对话结构的构造也有讲究。如果原始数据只有单轮问答,可以适当改造成多轮,但改造要保证上下文衔接自然。我常用的做法是从真实对话日志里找多轮片段,而不是强行拼接单轮样本。强行拼接会让模型学到“每轮都重新自我介绍”之类的怪异行为。
数据平衡是SFT的另一大难题。直接把1万条原始数据灌进去,大概率会发现模型只会输出高频任务的风格。实际操作时要按任务类型采样,比如把任务分成问答、写作、翻译、代码、推理、改写等类别,每个类别设数量上限。比较稳妥的比例是:核心任务类型占大头,其他类型保底各占一定比例。另外要保留一部分带“拒绝回答”语义的样本,但比例控制在5%以内,让模型学会承认不知道,而不是编造。
再补充一个答案质量筛选的经验。答案是SFT数据里最值钱的部分。一个结构清晰的错误答案,比一个含糊不清的“正确”答案危害更大。我用两级筛选:第一级是规则过滤,检查是否含空话套话、是否语义重复;第二级是模型辅助打分,用一个小模型对答案的相关性、完整性、格式规范性打1到5分,只保留3分以上的样本。这套流程看起来繁琐,但对最终对齐效果影响特别明显。
3.3 偏好对齐阶段:构造有效的chosen和rejected
到了DPO阶段,数据形态变成了三元组:prompt加上chosen和rejected两条回复。这个阶段的适配重点不是清洗,而是偏好对的质量。
首先明确一个原则:chosen和rejected的差异不能仅仅是“一个比另一个好一点”。差异必须足够明显,模型才能学到边界。如果两个回答只是措辞略有不同,DPO会把模型推到没有意义的极端偏好上,甚至产生重复和退化。我一般要求chosen和rejected在语义、长度或结构上至少要有一项显著区别。
构造偏好对最可靠的方式是自己采样。给定一批prompt,用当前模型或一个略弱的模型生成多条候选回复,然后通过规则和人工排序选出最好和最差的各一条。这样做的好处是正负样本来自同分布,不会出现“金标答案风格和模型风格差异过大”的问题。
人工排序是DPO数据里最耗时的一环。我有一个替代经验:先用规则粗筛,比如过滤回答长度过短、包含明显错误句式的候选;再让多个标注者对剩余候选做两两对比,最后用majority vote确定顺序。标注者数量3个以上会比较稳。需要注意一个常见作弊点:不要为了容易区分而让rejected变成“胡说八道”或“明显违规回答”。这样的偏好对虽然容易学习,但会让模型产生过度敏感的偏好偏移,一遇到正常问题反而表现变得僵硬。
DPO数据量不需要很大。轻量级模型用几千到两万对偏好数据就能看到明显的对齐效果。关键是把每对数据的差异做充足、标注标准统一、prompt覆盖面广。
还有一个细节:chosen和rejected在tokenize时要保持长度相近,或者至少不要让长度差异过大。DPO的损失函数对长度差异比较敏感,chosen很短而rejected很长,很容易让模型学会“短就是好”的偏见。
4. 数据量、训练步数与效果复盘
4.1 各阶段数据量与epoch的参考区间
轻量级模型各阶段数据量到底多少才够,我直接给一组实测参考区间。这些数值来自几个模拟项目,配置在1B到7B之间,使用参数高效微调或小规模全参数训练。
继续预训练或领域增量预训练,一般准备10万到100万条清洗好的文档。如果只做领域适配,比如让通用模型更懂某个行业术语和表达习惯,10万到30万条高质量文档通常就有明显效果。数据量低于这个区间,模型记不住领域知识;高于太多,又可能冲掉原先的通用能力。
SFT阶段,轻量级模型比较理想的样本量是1万到5万条。少于1万条时任务覆盖不足,多于10万条时会明显增加训练成本和过拟合风险。epoch控制在1到3轮。这里要特别强调:SFT数据里任务类别越丰富,需要的轮数越少;类别越单一,越多轮容易导致模板化。我的习惯是从1.5轮起步,每评测一个checkpoint,效果好就继续,出现模板化就立刻停止。
DPO阶段,5000到20000对偏好数据比较合适。这个量级下,训练速度很快,确定epoch为1到1.5轮就够了。DPO对重复学习比较敏感,我见过把DPO数据跑到3轮之后,模型输出的句式变得极其单调,几乎每个回答都带固定前后缀。
4.2 配比怎么定
配比问题具体体现在两个层面。
第一个层面是预训练语料的来源配比。我做过一个领域适配实验,纯粹堆领域文本,结果通用问答能力明显下滑。后来把配比调整为领域文本70%、通用文本20%、代码文本10%,整体表现就稳住了。这个调整背后逻辑是:如果模型持续看到同一个领域的句式,它会把参数往这个方向倾斜,原有的表达能力会被“覆盖”,通用数据相当于一个阻尼器。
第二个层面是SFT任务类型的配比。一个容易犯的错误是把最容易收集的数据放得过多。比如爬虫很容易拿到海量翻译样本,就把翻译数据做到40%,结果模型对翻译格式过度敏感。我的折中方案是任何单一任务类型不超过30%,重要任务确保20%左右,剩下按难度和多样性分配。
有个项目复盘时发现,把SFT数据中“指令简单但回答丰富”的样本比例适当提高,对模型长回答能力的帮助比多训几千条复杂指令更明显。这其实就是难度分布的调节,不能只按任务类型比。
4.3 三个踩坑记录复盘
第一个坑:预训练阶段清洗了文本格式,但没做充分去重。有一个语料源里大量文章来自同一个模板,只是中间段落稍作改动。结果是模型对模板开头产生了过强的记忆,输入任意开头词都倾向于顺着模板继续生成。后来加了MinHash去重之后,这个现象基本消除。
第二个坑:SFT数据的messages里,assistant回答用了太多“好的,我来帮你……”这类开头。训练完模型之后,几乎每个回答都以“好的”开头。修改方案是去掉这批模板化开头,并控制训练轮数,效果恢复正常。
第三个坑:DPO的chosen和rejected里,有一批样本的差异主要来自长度,rejected回答极短。模型学到的偏好变成了“长回答更优”。这是典型的混淆变量。后面我规定rejected长度不得低于chosen的50%,不足的在构造阶段就丢弃。
这三个坑的共同点在于:问题都不在模型侧,而在数据侧。这也是数据适配最“隐形”的特点。
5. 常见问题与排查技巧实录
5.1 数据泄漏:评测集混进训练集
数据泄漏最隐蔽。表现是训练loss正常、评测指标亮眼,但模型在真实场景里表现打折。我见过一个案例,开发者在网络上抓取了一批问答数据,其中有一部分和评测集同源。模型在评测集上准确率接近满分,但换一批人工测试就明显下降。
排查方式:对训练集和评测集做MinHash去重比对,重点检查高相似度样本。还有一个更轻量的手段,训练数据里的高频n-gram如果在评测集里也高频出现,就要警惕同源泄漏。把去重比对做成流水线固定环节,每次生成新数据集都跑一遍。
5.2 模板过拟合:模型只会套格式,不会的内容也硬套
模板过拟合在SFT阶段很常见。模型输出的结构“看起来很专业”,但事实性内容是错的,甚至编造来源。这通常是三个原因叠加:SFT数据里高难度样本占比不足、同一模板出现频次过高、训练轮数偏多。
排查时先检查数据分布,统计首句、尾句、固定用语的占比。如果“第一”“总之”“综上所述”这类标记词出现频率异常,就要考虑在清洗时做句式多样化。另外可以做一个专门测试集,把同类型任务但不同格式要求的样本放进去,看模型是否能脱离模板正确回答。
5.3 数据处理导致训练效率低
全链路训练里,GPU等数据的现象很普遍。除了合理配置数据加载器的并行度和预取数量之外,还有一个常被忽略的问题:数据的预处理逻辑没有做到样本级复用。比如每次都重新清洗一遍加载的文本,而不是清洗一次存成JSONL后直接使用。
我的排查手法是在训练脚本里记录每个batch的等待时间。如果等待时间占比超过30%,优先检查数据管线的瓶颈。多数情况下,提前把所有清洗、格式化、过滤操作做完,训练阶段只做tokenize和打包,问题就解决了。还有一个细节值得提醒:大文件用流式读取而不是一次性load进内存,能避免不少内存抖动导致的性能下降。
5.4 问题速查表
我把日常遇到比较多的数据适配问题整理成一张表,方便按症状定位。
| 症状 | 数据层可能原因 | 排查手法 |
|---|---|---|
| 训练loss突降但生成内容混乱 | 训练集与评测集泄漏或文本重复率高 | 对训练集、评测集做重复指纹比对 |
| 模型输出固定模板开头 | SFT模板单一、高频句式过多 | 统计assistant首句话频次 |
| 领域知识记不住 | 预训练数据量不足或重复度过高 | 检查领域文档去重率与有效增量token |
| DPO后回答风格生硬 | chosen与rejected差异不足或长度混淆 | 计算正负样本长度差与语义相似度 |
| GPU利用率偏低 | 数据预处理耗时过长、加载阻塞 | 记录每个batch等待时间,前置清洗 |
| 模型普遍答非所问 | 标签答案质量低、存在错配 | 对assistant内容做相关性打分 |
| 多轮对话上下文断裂 | 多轮样本由单轮强行拼接而成 | 抽查早轮与后轮话题一致性 |
这张表不解决所有问题,但能帮你在训练效果异常时快速判断“是不是数据的问题”。实际排查顺序我建议是:先查数据集统计报告,再查重复率和格式异常,最后再动训练参数。
排查时一定要看原始样本,不要只看统计数字。统计报告能告诉你有问题,但问题长什么样,必须通过抽样阅读才能确认。我会在不同阶段的样本里各抽200条,人工过一遍,这个习惯每次都能发现自动脚本查不出来的问题。
6. 最后补充一点个人实操体会
我自己在跑这套轻量级训练框架时,数据适配部分的投入产出比是最高的。某一个模拟项目里,我用了大概两周时间把预训练、SFT、DPO三份数据逐步建立起来。刚开始觉得慢,但进入训练阶段后,整个调参周期被大幅压缩,因为几乎所有能想到的问题都已经在数据层暴露并解决过了。
有一个技巧值得单独提出来:每一份数据集构建完成后,顺手生成一份报告,包括条数、总字符、平均长度、重复率、任务类型占比、质量分分布。训练阶段遇到任何异常,先对照这份报告,很多时候问题一眼就能定位。这个报告文件只有几KB,却能在后续排查中节省几小时。
数据适配本质上是在和不确定性做对抗。你无法预见模型会学到文本里的什么规律,但可以通过把数据做干净,减少那些“不该学到的规律”。这也是为什么我一直觉得,模型训练项目里最耗费心力的,从来不是跑起来的脚本,而是那些看似简单又永远不简单的数据。