行业数据和对话数据不一样:它是文档,不是问答对。 里面混着页眉页脚、软换行、表格碎行、乱码、重复段落——每一样都会让检索找不到东西、让微调学歪。 这一节把十八篇脏文档真的洗一遍,六道工序,逐道量去掉多少。
上一节定了做哪一块,这一节备料。 文本抽取与编码,10 课 计算机基础 里讲过; 这一节讲的是从 PDF 里抽出来的文本为什么是脏的,以及脏在哪几个固定位置。
一、数据源:四类,四种脏法
图纸与技术方案:表格多、图注多,抽出来是一堆碎行,还常带页眉页脚。 规范与标准:条文编号密集,跨页断句严重,一条规范被切成两半是常态。 工单与聊天记录:口语、错别字、指代满天飞,但它最贴业务,价值最高。
合同与报价:格式最规整,但一个字都不能错,只适合做检索,不适合做生成。 四类里最该先做的是工单——它不用清洗太多,而且直接对应 5.1 算出来的高频类别。
二、六道清洗工序
第一道去页眉页脚。 这类行的特征是「每页都出现、内容几乎一样」,按行频统计,出现次数接近页数的行就是它。 第二道合软换行。PDF 抽出来的文本按物理行断,一句话常被切成三四行。 规则是「行尾没有标点就与下一行合并」。 第三道去乱码与控制字符。替换符、换页符、零宽字符,这些进模型只会变成噪声。
第四道去重。分两层:篇内重复(同一段写了两遍)和全库重复(同一段在多个文档里都有)。 演示里篇内去重砍掉 1110 字,占全部的 20%。 第五道空白规范化。多空格合一、全角空格转半角、连续空行压成一个。 演示里这道没减少字符数,但它让后面的切块稳定。 第六道切块。按段落累加到 400 字切一刀,切完再看块长分布——块长差别太大,说明前面的清洗没做到位。
三、切块:粒度的取舍
块太大,检索到一大段无关内容,模型的注意力被稀释;块太小,答案被切断,检索出来一半。 常见做法 300 到 500 字,块间留 50 字重叠。重叠是为了「答案正好跨在两块之间」时不被切死。 重叠的代价是同一段内容被检索到两次,所以检索之后要做去重——按内容哈希去,不要按块编号去。
演示里九块,平均 399 字,块内平均 448 token。 这个量级塞三五块进上下文刚好,塞十块就开始挤占生成空间。
四、合规:脱敏与授权
脱敏要在入库前做,不是在使用前做。库里有原始数据,早晚会以某种方式漏出去。 要脱的三类:人名与联系方式、金额与报价、项目地址与甲方名称。前两类用替换,第三类用代号。 替换要一致:同一个客户在全文里都替换成同一个代号,否则检索会把同一家的资料拆散。
授权要留痕。 行业语料常来自甲方文档,能不能拿来训模型,要写在合同里,不能默认「给我们了就能用」。
五、动手 Demo:十八篇脏文档,六道清洗工序,看每道去掉多少
造十八篇「PDF 抽出来」的技术文档,每篇都带:页眉页脚、软换行、表格碎行、替换符、控制字符、 重复段落。 六道工序逐道跑,每道都量前后字符数。最后看切块之后的块长分布。
SEP = u'\n===DOC===\n'
# 1 去页眉页脚:每页都出现的那一行
t = re.sub(u'^.*第 \\d+ 页.*$\\n?', u'', txt, flags=re.M)
# 2 合软换行:行尾不是标点就并下一行
if re.search(u'[。;!?:]$', ln):
buf.append(acc); acc = u''
# 3 去乱码与控制字符
t = txt.replace(u'\ufffd', u'').replace(u'\x0b', u'')
# 4 篇内段落去重:按篇切段,各自记 seen
for seg in txt.split(SEP):
...
# 6 切块:按段落累加到 400 字切一刀
if len(cur) + len(p) > 400 and cur:
chunks.append(cur); cur = p
实跑输出:
--------------------------------------------------------------------
十八篇脏文档,六道清洗工序,看每道去掉多少
--------------------------------------------------------------------
① 先看脏在哪
--------------------------------------------------------------------
| 指标 | 值 |
|---|---:|
| 篇数 | 18 |
| 字符数 | 5407 |
| 行数 | 233 |
| 含替换符 | 36 |
| 含控制字符 | 18 |
| 表格碎行 | 72 |
这六项是 PDF 抽文本之后最常见的残留:页眉页脚、软换行、表格碎行、乱码、重复段落。
! 真实文档比这脏得多,还会混进扫描件的 OCR 错字。
清洗的目标不是「干净」,是「不把噪声喂进模型」——两个目标不一样,前者做不到,后者做得到。
② 六道工序,逐道量
--------------------------------------------------------------------
| 工序 | 前 | 后 | 变化 |
|---|---:|---:|---:|
| 1 去页眉页脚 | 5407 | 5101 | -306 |
| 2 合软换行 | 5101 | 5029 | -72 |
| 3 去乱码与控制字符 | 5029 | 4975 | -54 |
| 4 篇内段落去重 | 4975 | 3865 | -1110 |
| 5 空白规范化 | 3865 | 3865 | +0 |
| 6 切块(400 字) | 3865 | 3596 | -269 |
合计从 5407 字降到 3596 字,保留 67%。
! 第 4 道去重砍得最多——因为我在造数据时把每段正文写了两遍。
真实文档里重复来自「同一段话在多个文档里都出现」,去重不能只看篇内,要看全库。
③ 切块之后的形状
--------------------------------------------------------------------
| 指标 | 值 |
|---|---:|
| 块数 | 9 |
| 最短 / 最长 | 398 / 400 |
| 平均 | 399 |
| 块内平均 token | 448 |
切块大小决定了检索的粒度:块太大,检索到一大段无关内容;块太小,答案被切断。
! 常见做法是 300~500 字、块间留 50 字重叠。
重叠是为了「答案正好跨在两块之间」时不被切死——代价是同一段内容会被检索到两次,
去重要在检索之后做。
④ 真机上的流水线
--------------------------------------------------------------------
[真机代码 · 本沙箱不执行]
| pip install unstructured[pdf] langchain-text-splitters
|
| from langchain_text_splitters import RecursiveCharacterTextSplitter
| spl = RecursiveCharacterTextSplitter(chunk_size=400, chunk_overlap=50)
| docs = spl.split_text(text)
要 langchain 与 pdf 解析库,本沙箱没有,不执行。
它做的事跟上面六道工序一样,只是把「按什么切」做得更细:先按段落切,不行按句号,再不行按逗号,
保证不把一句话切断。
! 清洗这一步最容易被跳过,因为「跑通了」。
但脏数据带来的损失是隐性的:模型答得不好,你查不出是数据脏还是模型弱。
①先看脏在哪:18 篇、5407 字、36 个替换符、18 个控制字符、72 行表格碎行。
这几项就是 PDF 抽文本的固定残留,位置固定,所以能用规则清掉。
②六道工序里砍得最多的是第四道去重,1110 字,占全部的 20%。因为我在造数据时把每段正文写了两遍。
真实文档的重复不是这样来的,
它来自「同一段话在多个文档里都出现」——比如同一份规范被三个项目各存了一份。
所以去重不能只按篇做,要按全库做。
第一道去页眉页脚砍掉 306 字,看着不多,但它是「每页都错」的那种脏:不清掉,
检索到任何一段都会带着「XX 方案 第 3 页」这种噪声。
第三道只砍掉 54 字,但替换符的危害不在于占地方,在于它把一句话打断成两半,让检索和生成都读不通。
合计从 5407 字降到 3596 字,保留 67%。
清洗不是越多越好——砍掉一半以上就要警惕,是不是把正文也砍了。
③切完九块,平均 399 字,块内平均 448 token。
这个粒度下,检索取三块进上下文是 1300 多个 token,对 4k 上下文来说刚好。
如果块太大(比如 1000 字),三块就 3000 token,生成空间被挤没了;
如果块太小(比如 100 字),一句话被切成三段,检索出来接不上。
④那段真机代码本沙箱跑不了。
要注意的是 RecursiveCharacterTextSplitter 的切法是「先按段落,不行按句号,再不行按逗号」,
它保证不把一句话切断——这才是它比手写切块强的地方。
30 秒实验
- 把第六道的 400 改成 800,看块数从九降到几,块内平均 token 涨到多少。 再想想:这个大小适不适合 4k 上下文的模型?
- 给切块加上 50 字重叠,看总字符数涨多少。再想想:重叠的那部分,检索时会带来什么麻烦?
- 把第四道去重改成「全库去重」(不按篇切),重跑。这次会砍掉多少? 想一想:全库去重会不会误删不同文档里恰好相同的标题?
常见误区
- 跳过清洗直接切块。页眉页脚会被切进每一个块里,检索时块块都像,排序全乱。
- 以为清洗越狠越好。砍掉一半以上的字,多半是把正文也砍了。清洗之后要抽三段人工读一遍,比对原文。
- 先入库再脱敏。 库里有原始数据,早晚会以某种方式漏出去——导出的 CSV、备份的压缩包、同事的截图, 任何一个环节都不受你控制。
自测三问(点开对答案)
1. 页眉页脚为什么必须先去掉? —— 因为它每页都出现、内容几乎一样。 切块之后每个块都带它,检索时任何查询都跟它有点相似,排序会被它拉平; 生成时它还会被当成正文复述出来。 2. 切块为什么要留重叠? —— 因为答案常常跨在两块之间。 不留重叠,答案的前半句在第一块末尾、后半句在第二块开头,两块都不完整,检索出来都答不上。 3. 脱敏为什么要在入库前做? —— 因为入库之后的复制不受你控制:导出、备份、截图、外包。 入库前脱敏只需要守住一个入口,入库后要守住无数个出口。料备好了,下一节把基座模型加载起来——先读配置,再算体积,最后决定怎么切。
- ☐ (本篇完,下一篇 5.3 Llama 2 大模型导入初始化)
相关笔记
- 10 课 计算机基础:文本编码与字符集
- 数据收集与处理:语料怎么洗(对话版)
- 部署前的准备:三笔显存账
- 🦙 Llama 大模型实践 · 索引