2965 字
约 9 分钟
23
S22-A09 · 模型输入的预处理管线:清洗、去重与对齐

同一份 60 页 PDF,今天解析出 41 个段落,昨天 63 个;问同一个问题,两次引用的页码还不一样。这不是模型不稳定,是输入管线没有被当成有版本的工程:它由三段一次性脚本拼成,谁都能改一行正则,改完没人重跑历史数据,也没人知道结论为什么变了。

模型侧参数大多可观测,输入侧几乎不可观测。这篇只谈四件事:管到哪、清洗到哪该停、去重按什么粒度、对齐为何是溯源前提。


1. 四个环节与各自的坏法

环节 输入 → 输出 典型失败 外在表现
采集 源 → 原始文件 抓到登录页、旧版本、漏页 内容缺失但文件数正常
清洗 原始 → 纯文本 噪声残留、过度删除、编码错 命中乱码,或关键句消失
去重 文本 → 唯一集合 近重复漏判、异文被合并 同一事实反复引用,或内容串台
对齐 文本 → 带坐标的块 坐标错位、页眉混入 答案对但引用页码错
最小 DAG(每个节点都要回答:多少条进、多少条出、失败几条)

源 → [采集] → raw/ ─hash─┐
                          ├→ [清洗] → text/ ─统计─┐
          [版本登记] ─────┘                        ├→ [去重] → uniq/
                                                    │
                              [对齐] ← 原始版面坐标 ┘
                                 ↓
                            chunks/ + manifest.json

失败必须显式发生。把失败降级成"产出一段空文本继续走",后面所有指标都会失真且不报警。


2. 清洗的三类与"清洗过度"

清洗是有损压缩,删掉的每一处都可能是答案所在的句子。

类型 对象 处理原则 过度的后果
格式噪声 页眉页脚、页码、水印、断行连字符 按位置与重复模式删 删掉正文里的表格标题
编码问题 乱码、全半角混用、异常空白 先检测再转换,不认识的不动 整段被替换成空
结构混杂 目录、参考文献、脚注、图注 分类保留并打标签,不混正文 图表说明污染语义
# 规则的最小可审计写法:每条都有 id、作用域、预期命中量
rules:
  - id: strip-running-header
    scope: page_top_lines
    match: "第 \\d+ 页|^\\s*-\\s*\\d+\\s*-$"
    expect_hits: "每页 1—2 行"
  - id: join-hyphen-break
    scope: paragraph
    match: "(\\w)-\\n(\\w)"
    replace: "$1$2"
  - id: keep-footnote-tag
    scope: inline
    action: tag_only          # 不删,只打标

判断过度的两个证据:清洗前后字符数比值、抽样段落还能否回答原问题。干净但没信息的文本是负资产。


3. 去重的三个粒度

难点不是找出完全相同的两份,而是找出实质相同、措辞不同的两份,并判断该不该合并。

粒度 判据 成本 误判方向
字符级 归一化后 hash 相等 极低 漏判(改一个词就漏)
段落级 段落 hash / 最小哈希签名相似度 低 首尾差异导致漏判
语义级 向量相似度阈值 高(需嵌入) 把观点相反的两段合并
# 段落级近重复:先归一化再取签名,阈值须按语料类型核对
python3 - <<'PY'
import re, hashlib
def norm(t):
    t = re.sub(r'\s+', '', t).replace(',', ',').replace('。', '.')
    return re.sub(r'[^\w\u4e00-\u9fff]', '', t)   # 中文不必小写
def sig(t, k=5):
    n = norm(t)
    return {hashlib.md5(n[i:i+k].encode()).hexdigest() for i in range(len(n)-k+1)}
PY

合并要写清保留哪份:通常是元数据更完整的那份(含页码、版本、来源),而非更长的那份——丢掉的元数据正是溯源断掉的地方。


4. 对齐:坐标映射是溯源的前提

对齐最容易被跳过,也最贵。它要回答:这段文字出自原始文件的哪一页、哪一节。没有它,检索命中后无法回原文核对,页码只能靠模型编。

坐标类型 来源 精度 失效场景
页码 解析器分页标记 页级 双栏重排后页内顺序错
字符偏移 清洗后正文的位置 段落级 清洗删改后偏移漂移
结构路径 标题层级(H1/H2/H3) 节级 标题识别失败,路径断裂
版面框 原始 PDF bbox 行级 重排、缩放后失效
{
  "chunk_id": "doc-118-p07-c03",
  "text": "绿地滞蓄雨水的容量与土壤渗透率相关……",
  "anchor": {"page": 7, "char_span": [10432, 10711],
             "heading_path": ["第三章 城市水环境", "3.2 绿地系统"]},
  "pipeline": {"clean": "r2024-11-3", "chunk": "c1.4", "parser": "p2.1"},
  "source": {"path": "raw/kb/城市水环境.pdf", "sha256": "…", "version": "2024-09"}
}

三条实践:偏移算在清洗后文本上并保留原始映射;结构路径比页码稳;bbox 只作人工核对。


5. 结构保留与多源冲突

元素 是否保结构 理由 落地
标题层级 必须 决定块边界与路径 解析为树,不摊平
表格 必须 行列关系即语义 独立抽取,块类型标 table
代码块 必须 缩进与换行是语义 禁走正文清洗规则
页码 必须 溯源与引用 进 anchor,不进正文

最容易做错的是表格:摊平成一行看似信息都在,行列关系已丢,模型读到的是一串语义不明的数字。

多源冲突必须给裁决规则,否则检索会在两版间随机命中:

冲突类型 判据 裁决
版本升级 版本号/日期/hash 不同 新版生效,旧版标 superseded(不删)
同版异源 hash 不同但版本号相同 两份都留,标来源
无法判定 无版本、无日期 全留,冲突在检索层显式提示

禁止静默覆盖旧版本:删掉的那版常是解释"为什么改"的唯一证据。


6. 可重复性与版本绑定

同输入必同输出是管线能被信任的最低条件,要求同时固定解析器、清洗规则、分块参数。

变量 漂移来源 绑定方式
解析器 依赖升级、字体缺失 锁版本 + 记录 binary hash
清洗规则 有人改了一行正则 规则集版本号写入产物
分块参数 换 size / overlap 参数进 manifest
去重阈值 调参后忘了回滚 阈值进配置,不写进代码
manifest.json(每个产物目录一份,随产物提交)

{
  "pipeline_version": "2024-11-3",
  "parser": {"name": "pdf-parse", "version": "x.y", "binary_sha256": "…"},
  "clean_rules": "clean-r2024-11-3",
  "dedup": {"granularity": "paragraph", "threshold": 0.86},
  "chunk": {"size": 512, "overlap": 64, "by": "heading"},
  "inputs": [{"path": "…", "sha256": "…"}],
  "counts": {"raw": 128, "parsed": 126, "failed": 2, "chunks": 1902}
}

有了它,"两次结论不同"变成一次 diff:比版本号、比计数、比规则 id。


7. 质量评估与成本结构

方法 覆盖 能发现 成本
分层抽样人工核对 数十条 语义错误、结构丢失 中,需人
字符数/页数比 全量 扫描件空文本、解析失败 极低
引用回链抽样 中 对齐错位(页码/偏移) 低

自动检查抓不到语义错误,人工核对覆盖不了全量。资源有限时先做全自动检查,人工抽样只用于估计错误率量级。

成本项 驱动 优化方向 不能省
解析耗时 页数、版面复杂度、OCR 只对失败页走 OCR 结构抽取
存储 原文 + 文本 + 块 + 向量 原文只存一份,派生可重建 原文与 manifest
重跑 规则变更触发全量 按 hash 增量 影响范围内全量
人工核对 条数 × 单条耗时 分层抽样,先抽高风险类 一轮基线核对

多数组在存储上省、在重跑上花:省一份原文备份,规则一改就得回源头重抓。


8. 变更回归与下游契约

改清洗规则不是局部修改,它让历史结论过期。每次变更回答三问:影响哪些文档、哪些结论要重评、旧结论是否标失效。

变更 影响范围 回归动作
加/删噪声规则 全量块文本 全量重跑 + 重算向量
加结构规则(表格) 含表格文档 按文档筛选重跑
调分块参数 全量块边界 全量重跑并重建索引
调去重阈值 块集合 重跑去重,检查块数变化

硬规则:管线版本与结论版本绑定。对外给出的答案要能追到当时的 manifest,追不到就不能当结论引用。

对下游有两个契约,破坏任一个都会让上层莫名其妙地坏:

契约 内容 破坏后的症状
块元数据 schema 必含 chunk_id、anchor、doc_id、块类型 能命中但无法溯源、无法按类型过滤
块边界语义 块是自足语义单元,不切断句子与表头 答案缺半句,模型自行补全
文本归一约定 空白、全半角、换行约定一致 关键词漏命中,评测分数漂移

9. 排错:四种"同一份文档两种结论"

现象 原因 处理
两次解析块数不同 解析器版本或字体环境不同 锁版本,记录 binary hash
块数正常但内容缺失 清洗正则过宽误删 diff 清洗前后,逐条规则统计命中
命中重复段落 去重粒度太细 改段落级 + 签名相似度
答案对、页码错 偏移算在原始文本,清洗后未重算 双份偏移 + 映射表

10. 边界:什么时候不必建管线

场景 是否适用 理由
输入单一且干净(自家 JSON / 规范 Markdown) 不必 清洗与对齐收益接近零
一次性分析任务 不必 版本化与回归的价值来自重复运行
多源、长期维护、结论对外引用 必须 三条同时成立时,管线就是产品的一部分

11. 自测

  1. 为什么"失败被降级成空文本继续往下走"比直接报错更危险?
  2. 判断清洗过度,哪两个证据最直接?为什么"更干净"不等于"更好"?
  3. 去重的三个粒度各往哪个方向误判?合并时该保留哪一份?
  4. 对齐缺位时,模型给出的页码为什么必然不可信?
  5. 改了清洗规则后,哪些结论必须重评?依据什么判断影响范围?
  6. 什么条件下可以不建管线,用人工核对替代?
S22-A09 · 模型输入的预处理管线:清洗、去重与对齐
https://www.clxhxhhr.top/posts/6242/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。