数据截至 (上游 commit 669f534823b0)
04 · 标注器图鉴:从启发式到模型
这一章讲什么: 内置 tagger 的全景分类,以及每类里最值得读的那个实现。tagger 的共同接口已在第 01 章讲过(
predict进、span 列表出);这一章只看它们各自「怎么量」。
1. 它要解决的小问题
「语料质量」不是一个东西,而是一堆互不相同的测量:
- 这篇是不是模板/机器生成的垃圾?(统计启发式)
- 有没有邮箱、电话、IP 地址?(PII)
- 这个 URL 是不是成人/钓鱼/恶意站点?(外部黑名单)
- 这段话毒不毒、黄不黄?(内容分类模型)
- 这是什么语言写的?(语言识别)
- 以及一个元问题:怎么做 3% 抽样?(随机数)
Dolma 把每种测量都做成一个注册进 TaggerRegistry 的类(docs/taggers.md 有完整清单)。这一章按手段分四类讲。
2. 四类手段一览
| 类别 | 代表 tagger | 测量方式 | 输出粒度 |
|---|---|---|---|
| 启发式规则 | gopher_v1、c4_v1、tokenizer_repetitions | 统计量 + 论文给定的阈值信号 | 整文档 span,score 是测量值 |
| 正则 | pii_regex_v2 系列 | 模式匹配 + 上下文后处理 | PII 片段 span + 文档计数 |
| 外部名单 | blocklist_*、oisd_*、allowlist_* | hosts 文件集合 / adblock 引擎 | 命中则 [0, len, 1.0] |
| 模型 | jigsaw_*(fastText)、cld2/cld3/ft_lang_id_* | 轻量分类器打分 | 文档/ 句子/段落 span |
共同点:都只输出测量值(第 01 章的契约),阈值由 mixer 配方决定——所以你会在 configs/dolma-v1_6/mixing/c4.yaml 里看到「word_count[0][2] < 50 才丢」这种把阈值外置的写法。
3. 启发式规则:把论文的过滤器变成测量值
Gopher 规则
DeepMind 的 Gopher 论文给了一组文本质量过滤规则(词数范围、词长中位数、重复 ngram 占比……)。Dolma 的实现不「执行」这些规则,而是把每个规则背后的统计量算出来:
GopherAttributes(python/dolma/taggers/gopher.py:25)一口气定义 12 个统计量——词数、词长中位数、符号词比、含字母词占比、必备英文词(the/be/to/of/and/that/have/with,python/dolma/taggers/gopher.py:13)出现数、bullet 开头行占比、省略号结尾行占比、重复行占比、2-10 gram 的「最常见 ngram 字符占比」与「重复 ngram 字符占比」。
计算在 get_attributes(python/dolma/taggers/gopher.py:139):一趟 split + Counter 搞定,每个量作为一个 Span(0, character_count, type=量名, score=值) 发出。ngram 部分分两组:2/3/4-gram 记「最频繁那个占全文字符比」,5-10-gram 记「所有出现 ≥2 次的占字符比」(python/dolma/taggers/gopher.py:163-176)。
C4 规则
同款思路,对应 Google C4 的清洗规则:get_attributes(python/dolma/taggers/c4.py:44)逐行检查——行无结尾标点、行词数 < 3 的各记一个 span;整文档级别的 has_naughty_word(脏词表 python/dolma/data/naughty_words_en.txt,单词精确匹配 + 短语子串匹配,python/dolma/taggers/c4.py:64)、has_javascript、has_lorem_ipsum、has_curly_brace 各记一个布尔 span。
重复序列
BaseRepetitionsTagger(python/dolma/taggers/repetitions/repetitions_taggers.py:21)找出文本里的周期性重复段(find_periodic_sequences,python/dolma/taggers/repetitions/utils.py:40),并汇总三个文档级指标:最大重复得分、最长重复长度、重复字符占比(_compute_document_stats,python/dolma/taggers/repetitions/repetitions_taggers.py:30-50)。C4 配方里「doc_max_score_repetition >= 100 即弃」用的就是它。
这一类的心智模型:论文说「X 指标超过 T 就过滤」,Dolma 说「我先把 X 量出来存着,T 你慢慢调」。数据消融实验(ablation)就靠这个分离才能做。
4. 正则 + 后处理:PII 检测
BasePiiFilter(python/dolma/taggers/pii.py:27)检测三类 PII:邮箱、电话、IP。
两种检测器
- 正则:三条手写 regex(
pii_type_to_regex,python/dolma/taggers/pii.py:55-61)。邮箱 pattern 刻意吃掉周围的标点空白再捕获核心部分。 - Presidio:微软的 PII 分析库,软依赖,装了才能用(
python/dolma/taggers/pii.py:13-15的necessary包装)。
关键在「后处理」而不是「正则」
纯正则误报极高——ISBN、DOI、URL 里的数字串都长电话号的样子。_postprocess(python/dolma/taggers/pii.py:116-141)做三件事:
- 邮箱:再过一道
_is_email规则复查。 - 电话/IP:取命中位置前后 100 字符的窗口(
WINDOW = 100,python/dolma/taggers/pii.py:36),上下文含isbn/doi/#就丢弃。 - 电话专属:整篇含 URL 就丢弃(数字串更可能是 URL 参数)。
最后还附送一个文档级 span doc,score = PII 命中数 ÷ 词数(_score,python/dolma/taggers/pii.py:95)。PiiRegexWithCountV2(python/dolma/taggers/pii.py:284)则直接产出 doc_count,配方里「PII 超过 5 处即弃」查的就是它。
5. 外部名单:URL 黑名单的三种吃法
Dolma 的 URL 过滤不是「维护一个列表」这么简单,python/dolma/taggers/url.py 里有三个层次:
5.1 名单解析是个小工程
BaseUrlTagger.parse_line(python/dolma/taggers/url.py:84-112)兼容 hosts 文件的三种行格式:纯域名、IP 域名 对(IP 要过 IPv4/IPv6 校验、忽略 127.0.0.1 等)、adblock 的 ||domain^ 格式。clean_url(python/dolma/taggers/url.py:114-120)统一归一化:去协议、去 query、小写、去尾斜杠。
5.2 域名级 vs 链接级
BaseDomainTagger(python/dolma/taggers/url.py:136-147)在归一化后同时产出example.com和www.example.com两个候选——名单按域名封,文档 URL 可能带或不带 www。- 链接级(如钓鱼库)则保留完整 host+path 精确匹配。
5.3 hosts 集合 vs adblock 引擎
大多数 tagger 把名单装进 set 查成员;AdbUrlTagger(python/dolma/taggers/url.py:174-180)则把 EasyList 风格的规则喂给 Rust 的 adblock 引擎(UrlBlocker,pyo3 封装在 src/lib.rs:60-117,底层是 adblock crate 的 Engine::check_network_request)——通配、子域、规则优先级都由引擎处理。
名单本身托管在 dolma-artifacts.org,启动时下载缓存(cached_path)。细分很全:UTP 成人名单、PhishingDB、OISD 大/小/NSFW、Brave 核心与 NSFW、blocklist project 的犯罪/ vice /广告/社媒、firebog 的恶意/追踪/加密……外加一个反向的 allowlist_wikidata_v1(python/dolma/taggers/url.py:381 起):Wikidata 里的报刊/网站域名做白名单,并带一套 NSFW 词表与 TLD 黑名单对 Wikidata 条目再清洗(AllowlistWikidataCleanedTagger.is_valid_row)。
6. 轻量模型:fastText 与语言识别
6.1 fastText 内容分类
BaseFastTextTagger(python/dolma/core/ft_tagger.py:28)封装 fastText 监督模型,支持文档/句子/段落三种切分粒度。
代表:jigsaw_hatespeech_document_v2(python/dolma/taggers/jigsaw.py:17-26)——Jigsaw 毒性数据集上训的 bigram fastText,模型文件直接从 dolma-artifacts.org 下载;predict_slice 把换行替换为空格后预测,同时吐出正负两个 label 的分数。NSFW 版同构换模型文件。
句子级版本(如 jigsaw_hatespeech_sentence_v2)产出的是句子 span,于是 mixer 可以「只删掉 toxic 的那句话」而不是丢整篇——configs/dolma-v1_6/mixing/c4.yaml 里 jigsaw_*_sentence_v2 的 span_replacement(阈值 0.4)就是这么用的。
6.2 语言识别
BaseLanguageTagger(python/dolma/taggers/language.py:37)抽象出同一模式:对文本预测 (语言, 置信度) 列表,文档级一个 span、段落级每段一个 span;INCLUDE_NEGATIVE 还会补 not_<lang> 的互补分数 span。具体实现挂四种引擎:cld2、cld3、langdetect、lingua,全部软依赖(necessary(..., soft=True)),装哪个用哪个。fastText 语言识别则走 ft_lang_id_en_doc_v2 这一路,直接给「是英文的概率」——英文语料构建里这个分数就是过滤依据。
7. 最不起眼的神器:随机数 tagger
RandomNumberTagger(python/dolma/taggers/sampling.py:10-20)给每篇文档打一个 [0,1) 均匀随 机数 span。
一行初始化细节:种子要乘上进程号((pid + 1) * seed,python/dolma/taggers/sampling.py:13-15)——否则多进程下每个 worker 拿到相同种子、产生相同的随机序列,抽样会整体偏掉。
它的威力在组合:采样从此是一条属性查询。configs/dolma-v1_6/sample.yaml 用一条 exclude random[0][2] >= 0.003 就从全量里均匀抽出 0.3%;train/valid/test 切分同理,三条不相交的区间表达式而已。
8. 关键细节与坑
- 阈值全在配方里,不在 tagger 里:看 tagger 代码找不到「50 词以下丢弃」这种数字——全在 mixing YAML。调阈值不需要重跑 tagger,但需要知道属性 key 的三段式命名(第 01 章)。
- 软依赖遍地:presidio / cld2 / cld3 / lingua / fastText 都是装了才注册对应 tagger;
dolma list的输出因环境而异,别照抄别人环境的 tagger 名。 - PII 后处理的语境是英文中心:窗口里查的是
isbn/doi/#这些英文线索,非英文语料误报率会不同(inferred)。 - 黑名单 tagger 依赖外部工件:
dolma-artifacts.org上的名单是 2024-02 前后的快照,时效性不在代码控制内。 - 随机数 tagger 的进程种子技巧只在同一台机器同一批次内有效:换机器/换批次重跑,pid 不同随机数不同——抽样文件若需可复现,应保留首次产出的 attributes 而不是重跑(inferred)。
- 启发式全是英文假设:Gopher 的必备词表、C4 的脏词表都是英文的;多语种语料要换 tagger 或接受偏差。