模型的水平,一半在结构,一半在数据。这一节讲数据从哪来、脏在哪、怎么洗。 拿一份两百多行、中了七种毛病的语料过五道工序,每道工序砍掉多少行、多少字,全部记账。
上一节讲模型内部怎么算,这一节讲喂进去的是什么。 38 课 AI训练师手册 里讲的是「人怎么标数据」, 这一节讲的是「机器怎么洗数据」,两件事在真实项目里是接着做的。
一、数据从哪来
预训练语料的大头是网页。 最常用的是 Common Crawl,一个从 2008 年起持续抓取网页的公开档案,每个月新增几十亿个页面。 它便宜、量大、覆盖广,也脏得彻底。 第二块是书和论文。质量高,但版权麻烦,公开可用的部分不多。 第三块是代码,从开源仓库里抓,对模型的推理能力帮助比想象中大。 第四块是对话与问答,量小但最贵,主要用在微调阶段。 Llama 2 的预训练用了 2 万亿 token,来源是公开的在线数据。 它的语料里英文占 89.7%,中文只占千分之一出头。 这个比例直接决定了它对中文的手感——第 7 章会回来说这件事。
还有一条容易被忽略的:许可。哪些数据能商用、哪些只能研究,是要在训练之前查清的,不是训完再补。 这一条在法律上比技术上的去重更要紧。
二、洗数据洗掉什么
第一道是去标签。网页里混着 HTML、脚本、导航栏、页脚,这些都不是正文。 做法是解析出正文块,再用正则剥掉残留的尖括号。 第二道是去乱码。编码错误会留下替换符,二进制内容会混进控制字符。 这类东西进了词表就是纯浪费——训练时每个都要占一个输出维度,而它们永远不该被生成。 第三道是去重,也是收益最大的一道。网页里大量内容是转载、镜像、模板生成,同一段文字可能出现几十次。 精确去重靠哈希,近似去重靠 MinHash 加局部敏感哈希,后者能把「改了几个字」的重复也揪出来。
重复的代价不是占空间,是改变梯度。同一句话出现五次,它贡献的损失就是五份,模型会先学会背它。 这也是小数据集微调时最容易翻车的地方。 第四道是长度过滤。太短的多半是导航文字、广告语;太长的多半是拼接错误。 阈值按语种定,中文和英文的合理区间差得远。 第五道是内容过滤:压掉多余空白,去掉引流广告、辱骂、个人隐私信息。 工业上还会再加一层质量分类器,给每条数据打一个「像不像正经文本」的分。
三、洗完还有一步:配比
洗干净的语料不是混在一起就喂。每个领域占多少比例,是要调的——代码给多了,模型话痨且爱输出格式; 百科给多了,它答什么都像词条。 另一个维度是顺序。 早期用随机打乱,后来发现先把高质量数据放后面(叫课程学习)效果更好, 相当于最后几个月专门对着好材料复习。 还有一个新趋势是合成数据:让一个大模型生成训练语料,喂给一个小模型。 便宜、量大、可控,但风险是「模型学模型」,几轮之后会把上一代的毛病放大。
这一节要记住的判断只有一句: 数据的问题不会在训练时暴露,只会在上线后暴露。清洗省下来的每一小时,后面都要用十小时排查。
四、动手 Demo:二百五十行脏语料,过五道工序逐道记账
造一份两百五十行的语料,里面按比例掺了七种毛病:HTML 标签、完全重复、过短、超长、替换符、 全角空格与连续空格、引流广告。 然后过五道工序,每道都记「少了多少行」和「少了多少字」。 最后再看两件事:字表被撑大了多少,以及重复样本让某句话的权重变成了几倍。
# 五道工序,每道都记账
def rec(name, before, after, why):
cb = sum(len(x) for x in before)
ca = sum(len(x) for x in after)
steps.append((name, len(before), len(after),
len(before) - len(after), cb - ca, why))
cur = [re.sub(r'<[^>]{1,40}>', u'', x) for x in cur] # 1 去标签
cur = [x for x in cur if u'\ufffd' not in x] # 2 去乱码
cur = list(dict.fromkeys(cur)) # 3 去重
cur = [x for x in cur if 8 <= len(x) <= 500] # 4 长度过滤
cur = [re.sub(u'[\u3000\\s]+', u' ', x).strip() for x in cur] # 5 规范空白
cur = [x for x in cur if u'微信' not in x] # 5 去广告
# 不清洗的代价:字表被撑大多少
print(len(set(u''.join(rows_in))), len(set(u''.join(cur))))
实跑输出:
--------------------------------------------------------------------
一份脏语料过五道工序,每道砍掉多少行
--------------------------------------------------------------------
① 这批数据脏在哪
--------------------------------------------------------------------
共 250 行,平均每行 51.9 字。
| 毛病 | 长什么样 | 行数 |
|---|---|---:|
| HTML 标签 | <p> … </p><br/> | 30 |
| 完全重复 | 同一句出现两次以上 | 20 |
| 过短 | 少于 8 字 | 25 |
| 超长 | 超过 500 字 | 10 |
| 替换符 | � 这类不可读字符 | 15 |
| 空白脏 | 全角空格、连续空格 | 20 |
| 广告 | 「加微信」这类引流 | 10 |
七种毛病加起来 130 处,比行数还多——一行常常同时中好几枪。
② 五道工序,逐道记账
--------------------------------------------------------------------
| 工序 | 前 | 后 | 少行 | 少字 | 做什么 |
|---|---:|---:|---:|---:|---|
| 原样 | 250 | 250 | 0 | 0 | 进场 |
| 去标签 | 250 | 250 | 0 | 360 | 正则剥掉尖括号 |
| 去乱码 | 250 | 235 | 15 | 345 | 含替换符或控制字符的整行扔 |
| 去重 | 235 | 185 | 50 | 1000 | 完全一样只留第一条 |
| 长度过滤 | 185 | 150 | 35 | 8125 | 8 到 500 字之外不要 |
| 规范空白+去广告 | 150 | 140 | 10 | 310 | 空白压成一个,含引流的删 |
250 行进来,140 行留下,走了 44%。
注意第一道「去标签」一行没删,却删掉了 360 个字符——清洗不只砍行,也砍行里的垃圾。
③ 不清洗的代价:字表被撑大
--------------------------------------------------------------------
| 版本 | 行数 | 不同字符数 | 多出来的是什么 |
|---|---:|---:|---|
| 清洗前 | 250 | 51 | 含标签符号、替换符、全角空格 |
| 清洗后 | 140 | 38 | 只剩正文里真用到的字 |
字表从 51 降到 38,少了 13 个从来没在正文里出现过的符号。
! 这些符号进了词表就是纯浪费:训练时每个都要占一个输出维度,而它们永远不该被生成。
④ 重复样本的隐性代价
--------------------------------------------------------------------
| 重复最多的句子 | 出现次数 |
|---|---:|
| 甲方乙方验收报价甲方客户,本周继… | 5 |
| 预算图纸图纸进度验收客户,本周继… | 5 |
| 工期结算项目变更客户进度,本周继… | 5 |
同一句出现 5 次,梯度就被它牵着走 5 次。
模型会先学会背这几句,因为它们贡献的损失最多。
! 去重不是为了省空间,是为了让每条样本的权重一样重。
先看毛病统计:七种加起来 130 处,比 250 行还多——一行常常同时中好几枪。 一行带 HTML 标签的同时可能又是重复的,还带全角空格。所以清洗不是七选一,是七道都要过。 五道工序走完,250 行进、140 行出,走了 44%。 这个比例在真实网页语料上还算温和的,Common Crawl 那种原始数据,洗完剩一成是常事。 最值得看的是「少行」与「少字」两列的差别。 去标签那一行一行没删,却删掉 360 个字符——清洗不只砍行,也砍行里的垃圾。 如果只盯行数,这一道的功劳会被完全漏掉。 去重砍掉 50 行、1000 字。 重复样本的代价不在空间,在梯度:后面那段统计里,重复最多的句子出现了 5 次, 它一句话就贡献了五份损失。模型会先学会背这几句,因为它们最「划算」。 长度过滤砍掉 8125 字,是五道里最多的。 这一道拦的是过短的导航文字和过长的拼接错误,两者对训练都是噪音——前者学不到东西, 后者会把无关内容拼成一句话。 最后看字表:从 51 降到 38,少了 13 个从来没在正文里出现过的符号。 这些符号如果进了词表,每个都要占一个输出维度,而它们永远不该被生成。 词表大小是模型输出层的宽度,浪费一格就是浪费一格算力。
30 秒实验
- 把长度下限从 8 改成 20,重跑。看「少行」那一列涨多少——然后想想:这个阈值定高了会误删什么? (提示:短句、公式、表格行)
- 把「去广告」那条规则的条件从
微信改成加或联系,看它会误删多少正常行。 这就是关键词过滤的通病:召回与误杀永远在打架。 - 把去重那一步去掉,重新算重复句子的出现次数与字表大小。 想一想:如果这批数据要训三轮(三个 epoch),重复的代价会变成几倍?
常见误区
- 以为清洗是「跑个脚本就完了」。 真实的清洗流水线有几十道规则,每道都要人盯误杀率,而且规则是随着语料变化不断加的, 不是一次写死。
- 以为去重只是为了省磁盘。它改的是梯度分布——同一句话的权重被放大几倍,模型就会优先背它。 小数据集微调时这个问题最致命。
- 以为数据越多越好。同样 2 万亿 token,洗过的和没洗过的,训出来的模型差得很远。 数据的质量上限,就是模型的能力上限。
自测三问(点开对答案)
1. 为什么「去标签」这一道一行都没删,却仍然值得单独记一笔? —— 因为它删的是字符,不是行。 如果只统计行数,这一道的效果看不见,但它把混在正文里的尖括号清掉了——那些符号会进词表、 占输出维度。所以清洗效果的度量必须同时看行与字。 2. 精确去重和近似去重,差在哪里? —— 精确去重只认完全一样的字符串,靠哈希一次比完; 近似去重能认出「改了几个字、换了个标题」的重复,靠 MinHash 把文本压成签名再比对。 网页语料里后者更重要,因为转载几乎从不逐字照抄。 3. 合成数据便宜量大,为什么不能无限用? —— 因为它是「模型学模型」。上一代的偏差会被当成正确答案学进去,几轮之后放大。 实践中的做法是混着用,并且保留一份真实数据做锚点,定期检查有没有跑偏。下一节看这些数据喂进去之后,模型到底在学什么——预训练、指令微调、偏好学习三段,目标函数各不相同。
- ☐ (本篇完,下一篇 1.4 大模型的预训练及微调)
相关笔记
- 38 课 AI训练师手册:人工标注那一侧的做法
- 40 课 图解机器学习:特征工程与数据清洗的通用思路
- 🦙 Llama 大模型实践 · 索引