1. 大模型训练第一步到底在干什么
很多人第一次接触大模型训练,脑子里浮现的画面是一排排显卡嗡嗡作响,然后模型就“变聪明”了。实际上,在显卡真正开始跑之前,有一件更枯燥、更耗时、也更容易被低估的事情——数据准备。我见过不少团队,模型结构调了三个月,loss 曲线还是不收敛,最后发现问题出在数据上:要么脏得离谱,要么格式乱七八糟,要么量根本不够。所以这个系列的第一篇,我想把数据准备这件事掰开揉碎讲清楚。
所谓 LLM 的数据准备,说白了就是把互联网上、书籍里、代码仓库中那些原始文本,经过采集、清洗、去重、过滤、配比、分词、打包这一整套流程,变成模型能“吃”的 token 序列。它解决的核心问题是:让模型在有限算力下,见到尽可能高质量、多样化、无重复的语言样本。这件事适合谁看?如果你是刚入行做算法工程的同学,或者想自己动手训一个小模型练手的开发者,又或者你只是好奇 ChatGPT 这类东西背后的数据从哪来,这篇都能给你一个能落地的全景图。
我个人的经验是,数据准备阶段的工作量,往往占到整个训练项目的六成以上。而且它不像调模型那样有即时反馈,你清洗了一周数据,可能指标只涨了零点几个点,但如果不做,模型直接废掉。下面我按实际工程顺序,把这条链路完整走一遍。
2. 数据来源与采集:原料从哪来
2.1 常见语料类型与各自特点
大模型的“食物”大致分几类,每类的处理难度和用途都不一样。我整理了一张表,方便你对照理解。
| 语料类型 | 典型来源 | 优点 | 主要问题 |
|---|---|---|---|
| 网页文本 | 公开网页抓取 | 量大、覆盖广、时效新 | 噪声多、广告多、重复高 |
| 书籍文献 | 电子书、论文 | 语言规范、逻辑连贯 | 版权敏感、格式复杂 |
| 代码仓库 | 开源代码托管平台 | 结构强、利于推理能力 | 注释混杂、许可证问题 |
| 问答社区 | 技术论坛、问答站 | 口语化、贴近真实对话 | 质量参差、水帖多 |
| 百科类 | 在线百科 | 事实性强、中立 | 量相对有限 |
网页文本是绝对主力,通常占到一个通用大模型预训练语料的七八成。但它的脏也是出了名的:导航栏、版权声明、弹窗文字、乱码、外链堆砌,全混在一起。书籍和论文质量高,但获取和清洗成本大,而且版权是个绕不开的坎,实际项目中要么用已授权数据,要么用公开版权过期的内容。代码数据这几年越来越受重视,因为它能显著提升模型的逻辑推理和结构化输出能力,很多团队会专门配比百分之十到二十的代码语料。
2.2 采集环节的实操要点
采集不是简单地“把网页存下来”。我踩过的坑里,最常见的就是采集频率和礼貌性问题。你如果对一个站点疯狂发请求,轻则被封 IP,重则收到警告。合理的做法是控制并发、加延迟、遵守站点的 robots 协议。虽然我们是做技术,但基本的规矩要守。
另一个要点是原始数据的留存格式。我建议采集阶段就统一存成 JSONL,每行一条记录,字段至少包含url、timestamp、raw_html、text。为什么要留raw_html?因为后面清洗规则可能会变,如果你只存了提取后的文本,想重新提取就得再抓一遍,成本极高。留一份原始快照,等于给自己留了后悔药。
提示:采集阶段一定要记录来源 URL 和时间戳,后面做去重、质量过滤、溯源都靠它。没有这两个字段,数据基本等于废料。
还有一点,采集要分批次、分来源打标签。比如这批是新闻站,那批是论坛,标签留着后面做数据配比时用。我见过有人把所有来源混成一锅粥,最后想调整某个领域的比例,根本无从下手。
3. 数据清洗:把垃圾挡在门外
3.1 清洗的核心目标与优先级
清洗的目标不是把数据洗得“完美”,而是在保留语言多样性的前提下,去掉明显有害和无用的部分。这里有个误区:很多人追求极致干净,结果把口语化、带点错别字但很自然的表达也洗掉了,模型反而变得死板。所以清洗要有优先级。
我的优先级排序是:先去掉完全无意义的(乱码、纯符号),再去掉明显有害的(违规内容、垃圾广告),然后处理格式问题(HTML 残留、多余空白),最后才考虑语言层面的规整。顺序不能乱,否则你会在细节上浪费大量时间。
3.2 具体清洗步骤与代码示例
第一步是HTML 解析与正文提取。原始网页里真正有用的正文可能只占百分之十几。常用的做法是用解析库把 DOM 树建出来,然后根据标签密度、文本长度等启发式规则定位正文块。下面是一个简化示例:
from bs4 import BeautifulSoup import re def extract_main_text(html): soup = BeautifulSoup(html, "html.parser") # 去掉脚本、样式、导航等明显非正文标签 for tag in soup(["script", "style", "nav", "footer", "header", "aside"]): tag.decompose() # 取所有段落文本 paragraphs = soup.find_all("p") text = "\n".join(p.get_text() for p in paragraphs) # 压缩多余空白 text = re.sub(r"\s+", " ", text).strip() return text这段代码很粗糙,但能过滤掉大部分噪声。实际工程中会用更复杂的正文提取算法,比如基于文本密度的打分,或者用训练好的小模型来判断哪块是正文。
第二步是去重。去重分三个层次:完全重复、近似重复、模板重复。完全重复用哈希就行,一行hashlib.md5(text.encode()).hexdigest()搞定。近似重复要用 MinHash 或 SimHash,计算文档间的相似度,超过阈值就保留一份。模板重复最隐蔽,比如同一个新闻站的不同页面,正文不同但页脚、免责声明一模一样,这种要在句子级别做去重。
from datasketch import MinHash, MinHashLSH def get_minhash(text, num_perm=128): m = MinHash(num_perm=num_perm) for token in set(text.split()): m.update(token.encode("utf8")) return m lsh = MinHashLSH(threshold=0.8, num_perm=128) # 对每篇文档计算 minhash 并插入,查询时能快速找到近似重复第三步是质量过滤。这一步决定数据的天花板。常见的过滤规则包括:文本长度过短或过长都剔除、符号与字母比例异常剔除、包含大量重复字符剔除、语言检测非目标语言剔除。我一般会设一个“困惑度”阈值,用一个小型语言模型给每段文本打分,困惑度太高的说明语言不通顺,直接扔掉。
注意:质量过滤的阈值不要一刀切。代码数据和自然语言的统计特征完全不同,要分开设规则。我见过有人用同一套规则过滤代码,结果把大量正常代码当垃圾扔了。
3.3 清洗中的常见陷阱
第一个陷阱是过度清洗导致数据同质化。如果你把所有口语、方言、网络用语都过滤掉,模型就学不会这些表达,生成的内容会非常“新闻联播”。所以清洗规则要留有余地,宁可保留一点噪声,也不要误杀多样性。
第二个陷阱是忽略编码问题。网页编码五花八门,UTF-8、GBK、Latin-1 混在一起,处理不当就会出现乱码。我的做法是统一转成 UTF-8,遇到无法解码的字符用替换策略,而不是直接丢弃整篇文档。
第三个陷阱是清洗顺序错误。比如你先做了语言检测,但文本里还残留 HTML 标签,检测结果就会偏。正确的顺序是:解析正文 → 去重 → 基础清洗 → 质量过滤 → 语言检测。每一步都建立在上一步干净的基础上。
4. 数据配比与格式化:让模型营养均衡
4.1 为什么要做数据配比
数据配比是很多人忽略但极其关键的一步。你手里可能有网页、书籍、代码、问答四类数据,如果直接按采集量混在一起,网页可能占九成,模型就会变成一个“网页复读机”,代码能力和对话能力都很差。配比的目的就是人为控制不同领域数据的比例,让模型各项能力均衡发展。
业界常见的通用模型配比大致是:网页文本六到七成,书籍和论文一成到一成五,代码一成到两成,问答和对话数据半成到一成。这不是铁律,要根据你的目标调整。如果你想要一个编程助手,代码比例可以拉到三成以上;如果想要一个客服机器人,对话数据就要加重。
4.2 配比的计算与采样方法
配比不是简单地把数据按比例抽出来混合,因为不同来源的数据量差异巨大。实际操作中,我们通常先给每个来源定一个“目标 token 数”,然后从该来源中采样出对应数量。
假设总目标是一万亿 token,网页占 65%,那就是六千五百亿。如果网页清洗后有两万亿 token,你就需要从中随机采样六千五百亿。采样时要注意分层采样,比如按域名、按时间、按语言分层,避免某个大站点的数据垄断整个网页部分。
import random def sample_by_target(docs, target_tokens, tokenizer): random.shuffle(docs) selected = [] total = 0 for doc in docs: n = len(tokenizer.encode(doc["text"])) if total + n > target_tokens: break selected.append(doc) total += n return selected这段代码是简化版,实际中还要考虑去重后的唯一性、各子领域的配额等。我一般会维护一个配比表,每类数据设上下限,采样时动态调整。
4.3 格式化与分词打包
数据配比完成后,要转成模型训练需要的格式。现在主流做法是先把所有文本拼成一个超长的 token 流,然后按固定长度切块。比如设定序列长度 4096,就把 token 流每 4096 个切一段。这样做的好处是没有 padding 浪费,训练效率最高。
但拼接也有讲究。不同文档之间要加分隔符,比如<|endoftext|>,让模型知道这里换文档了。否则模型会把上一篇的结尾和下一篇的开头当成连续内容,学出奇怪的关联。
def pack_tokens(docs, tokenizer, seq_len=4096, eos_token="<|endoftext|>"): buffer = [] for doc in docs: ids = tokenizer.encode(doc["text"]) buffer.extend(ids) buffer.append(tokenizer.encode(eos_token)[0]) while len(buffer) >= seq_len: yield buffer[:seq_len] buffer = buffer[seq_len:]分词器的选择也很关键。中文场景下,词表大小、是否支持多语言、对代码的切分效果都要考虑。我一般会先用小规模数据训一个分词器,看看压缩率(平均每个 token 对应多少字符),压缩率太低说明词表不合适,训练成本会飙升。
提示:打包前一定要做一次全局 shuffle。如果数据是按来源顺序排列的,打包后每个 batch 可能全是同一类数据,训练时 loss 会剧烈震荡。
5. 实操全流程与参数记录
5.1 一条完整的数据流水线
把前面几步串起来,一条典型的数据流水线是这样的:
- 采集:从多个来源抓取原始数据,存为 JSONL,保留 URL 和时间戳。
- 正文提取:解析 HTML,提取正文,去掉导航、广告等。
- 基础清洗:统一编码,去除乱码、控制字符、多余空白。
- 去重:完全重复用哈希,近似重复用 MinHash,句子级模板去重。
- 质量过滤:长度、符号比例、困惑度、语言检测多重过滤。
- 敏感与有害内容过滤:按规则和分类器剔除违规内容。
- 配比采样:按目标比例从各来源采样。
- 分词打包:训练或加载分词器,转成 token 流,按序列长度切块。
- 全局 shuffle:打乱顺序,写入最终训练文件。
每一步都要有日志和统计。我习惯在每一步后输出一份报告:输入多少条、输出多少条、剔除了多少、剔除原因分布。这份报告在排查问题时价值巨大。
5.2 关键参数的计算与选择
序列长度怎么定?这取决于你的算力和目标。序列越长,模型能处理的上下文越长,但显存和计算量是平方级增长。一般预训练阶段用 2048 到 8192 之间。我个人的经验是,如果算力有限,先用 2048 跑通流程,后面再逐步加长。
词表大小怎么定?太小会导致一个词被切成很多 token,序列变长;太大则 embedding 层参数多,训练慢。中文为主的模型,词表通常在五万到十万之间。可以用 BPE 或 Unigram 算法在采样数据上训练,看压缩率来调。
去重阈值怎么定?MinHash 的相似度阈值一般设在 0.7 到 0.9 之间。太低会误删不同文档,太高则去重不彻底。我一般先用小样本人工检查,确定一个合理值再全量跑。
5.3 实操现场记录
我拿一个模拟项目举例。假设我们要为一个中文通用模型准备五百亿 token 的预训练数据。采集阶段拿到约两亿条网页记录,原始体积约 2TB。正文提取后剩 1.2TB,去重后剩 800GB,质量过滤后剩 600GB。按配比采样,网页取 65%,书籍 15%,代码 15%,问答 5%。分词后统计,平均压缩率约 1.5 字符每 token,最终得到约五百亿 token。整个过程在一台 32 核机器上跑了大约三天,瓶颈在去重和困惑度计算。
这个过程中最大的坑是内存。MinHash 去重如果全量加载,内存直接爆掉。解决办法是分片处理,每片算完 MinHash 后只保留签名,再合并。困惑度计算也是,要批量推理,不能一条条来。
6. 常见问题与排查技巧实录
6.1 数据准备高频问题速查表
| 问题现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 模型 loss 不降 | 数据太脏或重复太多 | 检查去重率、质量过滤报告 |
| 生成内容重复 | 训练数据模板重复高 | 加强句子级去重 |
| 模型只会说一种风格 | 数据同质化严重 | 调整配比,增加多样性来源 |
| 训练速度慢 | 序列 padding 多 | 改用 token 流打包 |
| 中文输出乱码 | 编码未统一 | 全量转 UTF-8,检查分词器 |
| 代码能力差 | 代码语料比例低 | 提高代码数据占比 |
| 显存溢出 | 序列过长或 batch 过大 | 降低序列长度或梯度累积 |
6.2 独家避坑技巧
第一个技巧:先小后大。不要一上来就处理全量数据。先抽一万条跑通全流程,看看每一步的输出是否符合预期,再放大到全量。我见过太多人直接跑全量,跑到一半发现规则有问题,三天白干。
第二个技巧:保留中间产物。每一步的输出都存下来,哪怕占空间。因为后面调参时,你可能需要回到某一步重新处理,如果中间产物没了,就得从头再来。
第三个技巧:人工抽检。自动化指标再好看,也要定期人工看样本。我一般每个阶段随机抽一百条,逐条读,看看有没有奇怪的残留。机器判断不了的语义问题,只有人眼能发现。
第四个技巧:版本化管理。数据流水线的每一步都要有版本号,配置文件、代码、输出都要对应。否则你改了规则,却忘了哪份数据是用哪版规则生成的,实验无法复现。
注意:数据准备阶段最忌讳“差不多就行”。这里的每一个疏忽,都会在训练阶段被放大成几倍的时间浪费。宁可前期慢一点,也不要后期返工。
6.3 一个真实的排查案例
某次实验,模型训练到一半 loss 突然飙升。排查后发现,是数据流水线在某个批次混入了一批未清洗的原始 HTML,里面全是标签和脚本。原因是那批数据的来源标签写错了,被错误地跳过了清洗步骤。解决办法是加一道校验:任何进入训练文件的样本,都要通过一个“最小质量检查”,比如不能包含连续的特殊符号、不能有 HTML 标签残留。这个检查很便宜,但能挡住大部分低级错误。
这件事给我的教训是:流水线的每一步都要有防御性校验,不能假设上游一定正确。数据工程和写代码一样,边界情况永远比你想的多。
7. 数据质量评估与迭代
7.1 怎么判断数据准备得好不好
数据准备不是一次性的,而是一个迭代过程。评估数据质量,我一般看几个指标:去重率、平均长度分布、语言分布、困惑度分布、各来源占比。这些指标要和你的目标对齐。比如目标是一个中文模型,那中文占比就应该在九成以上;如果发现英文占了三成,说明语言过滤有问题。
除了统计指标,还要做下游任务验证。最直接的办法是用一小部分数据训一个微型模型,看它在几个基准任务上的表现。如果微型模型表现异常,大概率是数据问题。这个方法成本不高,但能提前暴露大问题。
7.2 迭代优化的方向
数据准备的迭代通常围绕几个方向:提高质量、增加多样性、修正配比、降低成本。提高质量可以引入更强的分类器来过滤;增加多样性可以拓展来源;修正配比要根据下游表现调整;降低成本则靠优化流水线效率,比如用分布式处理去重。
我个人的习惯是,每轮训练后都回头看看数据报告,找出表现最差的领域,针对性地补充或清洗那部分数据。这个过程可能重复很多轮,但每一轮都会让模型更稳。
数据准备这件事,说到底就是用工程手段把混乱的原始信息变成有序的训练信号。它没有模型结构那么光鲜,但决定了模型的上限。你把数据这关过了,后面的训练才有意义。下一篇我会接着讲模型结构设计和训练策略,那是另一个同样深不见底的坑。