章节导航 第 4 章 / 共 20 章
语种识别与质量过滤
CommonLingua语种识别(334语种,仅8MB)、fastText基线、Gopher启发式8条规则、Perplexity/FineWeb-Edu/DCLM模型打分、DSIR/QuRating重要性采样——把垃圾数据挡在门外
3T 原始数据 → 1T 清洁数据。
质量过滤这道关卡,决定了你模型上限的 60%。
4.1 为什么语种识别是第一步
在数据流水线中,语种识别(Language Identification, LangID)是质量过滤的第一道关卡,原因有三:
- 下游所有过滤都语种相关:英文停用词表对中文无效,中文Gopher规则对英文无效
- 配比计算需要语种标签:训练时按 zh/en/code 分别配比
- 多语种模型需要平衡:知道每条数据的语种才能控制配比
4.1.1 踩坑:不做LangID直接训练的灾难
某团队用未做语种识别的混合数据训练,结果模型在中文benchmark上表现极差——因为训练数据里 80% 是英文,中文只有 5%。模型把中文当成”奇怪符号”处理,而不是真正的语言。
4.2 语种识别:从 fastText 到 CommonLingua
语种识别(LangID)工具在2026年有了重大进展。此前 fastText lid.176 是事实上的工业标准,但随着 CommonLingua 的发布(Pleias + GSMA “African Languages in Africa” 项目,2026-04-28,Apache 2.0),这一格局已经被打破。本节从全景对比开始,依次介绍新一代默认方案、传统基线和轻量替代方。
4.2.1 2026年 LangID 模型全景对比
| 模型 | 参数量 | 覆盖语种 | CommonLID Strict Acc | CommonLID Macro F1 | 模型大小 | 架构亮点 |
|---|---|---|---|---|---|---|
| CommonLingua | 2.35M | 334 | 77.63% | 0.788 | 8MB | 无分词器,UTF-8字节级因果Conv1D×3+双向Attention×1+RoPE |
| fastText lid.218 (NLLB) | ~600M | 218 | 59.53% | 0.659 | ~300MB | n-gram哈希+线性分类 |
| fastText lid.176 | ~130M | 176 | ~55% | ~0.60 | 126MB | 同上(旧版) |
| GlotLID | ~500M | 1665 | 66.23% | 0.715 | ~2GB | Transformer+CLS分类 |
| Gherbal v4 | ~85M | 214 | — | 0.836(Avg) | 200MB | 阿拉伯方言专用优化 |
| UniLID (Qwen2.5) | 0 (共享分词器) | — | — | 0.929(GlotLID-C) | 0 | UnigramLM分词器复用 |
| XLM-R LID v2 | ~278M | 200+ | — | 0.966(语码转换) | ~1.1GB | transformers 原生,跨语言编码器 |
CommonLID评测集:376K样本,覆盖200+语种,是迄今最全面的开源LangID基准测试。UniLID采用GlotLID-C评测集(不同基准,数据不可横向对比)。
评估指标外推需谨慎(社区实践):CommonLingua/CommonLID 的官方指标来自受控评测集,在真实 Web 噪声场景下表现通常更严格——噪声文本更易被判为低置信而被丢弃。一次 Common Crawl 清洗实战中,fastText + langid.py 双模型在受控样本上一致性达 98.91%,可作为工程上的交叉验证参考。
4.2.2 CommonLingua:新一代默认选择
CommonLingua 在仅 8MB 的模型文件上实现了对 334 种语言的顶级识别精度,全面超越参数量大 75 倍的 fastText lid.218。其架构设计有几个关键创新:
- 无分词器:直接在 raw UTF-8 字节上操作,彻底避免了 tokenizer 偏置
- Trigram 哈希嵌入:固定词汇量,跨语言同质化训练
- Causal Conv1D × 3 + Bidirectional Attention × 1 + RoPE:兼顾序列建模与全局上下文
- Apache 2.0:无许可障碍
# CommonLingua 语种识别
# 安装:pip install commonlingua
from commonlingua import CommonLingua
model = CommonLingua.from_pretrained("PleIAs/CommonLingua")
def detect_language_cl(text, threshold=0.5):
"""
CommonLingua 语种识别
返回 (language_code, confidence)
"""
if len(text.strip()) < 10:
return 'unknown', 0.0
predictions = model.predict(text, k=3)
top_lang = predictions[0].code # ISO 639-3
top_prob = predictions[0].confidence
if top_prob < threshold:
return 'unknown', top_prob
return top_lang, top_prob
# 批量识别示例(比fastText更快,支持内置批处理)
texts = ["The quick brown fox jumps over the lazy dog.",
"知识就是力量。",
"Le savoir est le pouvoir."]
results = model.predict_batch(texts)
for text, pred in zip(texts, results):
print(f"{text[:30]}... → {pred[0].code} ({pred[0].confidence:.2f})")
4.2.3 fastText lid.176:作为对比基线
fastText lid.176 曾是2019-2025年间的事实标准,本章保留其完整工程代码作为”对比基线”,方便你(1)理解LangID基本原理(2)在需要传统兼容方案时使用。
fastText 的 lid.176 特点:
- 支持 176 种语言:覆盖几乎所有有文字系统的语言
- 模型大小:压缩后约 126MB,可单机加载
- 速度:单核 10,000+ docs/s
- 准确率:在 FLORES-200 上 95%+(常见语言 99%+),但在 CommonLID 基准上明显低于 CommonLingua
# fasttext 语种识别的最简用法
import fasttext
# 下载:https://dl.fbaipublicfiles.com/fasttext/supervised-models/lid.176.bin
model = fasttext.load_model('lid.176.bin')
def detect_language(text, threshold=0.5):
"""
返回 (language_code, confidence)
threshold 低于此置信度的标记为 'unknown'
"""
# fasttext 要求输入是单行(不能有换行)
text = text.replace('\n', ' ').strip()
if len(text) < 10: # 太短的文本不可靠
return 'unknown', 0.0
predictions = model.predict(text, k=3) # 取top-3
langs, probs = predictions
top_lang = langs[0].replace('__label__', '')
top_prob = probs[0]
if top_prob < threshold:
return 'unknown', top_prob
return top_lang, top_prob
工程参数调优(fastText):
# 生产环境推荐配置
LANGID_CONFIG = {
'model': 'lid.176.bin', # 176语种版本
'min_text_length': 10, # 小于此长度标记 unknown
'confidence_threshold': 0.5, # 低于此置信度标记 unknown
'top_k': 3, # 返回top-3用于多语种文本
'mixed_lang_threshold': 0.3, # top-2与top-1差距<0.3视为多语种
# 对于代码/数学公式密集的文本,需要预处理
'strip_code_blocks': True, # 去除```...```代码块
'strip_latex': True, # 去除$...$公式
'strip_urls': True # 去除URL
}
4.2.4 CommonLingua 工程参数
# CommonLingua 推荐配置
COMMONLINGUA_CONFIG = {
'model': 'PleIAs/CommonLingua', # HuggingFace 模型ID
'min_text_length': 15, # 略高于fastText(以充分利用更深层特征)
'confidence_threshold': 0.6, # CommonLingua置信度分布与fastText不同,建议0.6
'top_k': 5, # 334语种覆盖面广,建议取top-5检查多语种
'batch_size': 64, # 支持内置批处理,大幅提升吞吐
# CommonLingua 原生处理代码/公式,无需strip预处理
# 这是相比fastText的一大优势
}
4.2.5 常见误判
| 文本类型 | 误判为 | 原因 | 解决 |
|---|---|---|---|
| 代码 + 注释 | en | 注释主导 | 先剥离代码再识别 |
| 短文本(<10字符) | unknown | 信息不足 | 拒绝短文本 |
| 拉丁化中文(pinyin) | en/id | 拼音被识别为印尼语 | 用CommonLingua/GlotLID补充 |
| 历史文本(古文) | ja | 繁体字相似 | 用专门古文分类器 |
| 含大量数字 | unknown | 数字无语种信息 | 先剥离数字 |
| 阿拉伯语方言 → MSA | ar (现代标准阿语) | 方言与MSA混淆 | 用 Gherbal v4 (Omneity Labs, 2026),16/16 阿拉伯方言分类,Avg Acc 0.836,200MB |
4.2.6 轻量方案:UniLID — 无模型的语种识别
如果你不想为语种识别单独部署一个模型,UniLID(Clara Meister et al., ICML 2026)提供了一个极简方案:直接复用 LLM 自带的 UnigramLM 分词器做语种识别,零额外模型成本。
核心原理:UnigramLM 分词器中不同语种的子词单元分布差异显著,通过计算测试文本在各语种子词单元上的负对数似然即可推断语种。
关键数据(GlotLID-C 基准):
- UniLID (via Qwen2.5 tokenizer):Macro F1 = 0.929
- fastText 基线:Macro F1 = 0.944
- 差距仅 0.015,但 UniLID 零额外模型、零额外内存
适用场景:
- 你的流水线已经加载了 LLM 分词器(如训练场景)
- 追求最小依赖的简化流水线
- 对准确率要求接近 fastText 即可(差距 <2%)
4.2.7 LLM 驱动的语种识别
另一个有趣的产业级实践是 Ada CX Labs(2026)的方案:用 LLM 的 tool-call 能力替换 standalone LangID 模型。
实验数据(367K 样本,55 语种):
- 独立 fastText 分类器:91.8% 准确率
- LLM tool-call 方案:94.9% 准确率
思路是让 LLM 在预处理阶段同时完成语种识别(作为”摘要”tool_call 的副产品),避免了单独的 LangID 模块。但这需要你的流水线已经集成 LLM 推理,否则代价超过收益。
趋势观察:2024年”语种识别=fastText”是一个不假思索的结论;2026年,你需要根据场景选择 CommonLingua(最强精度)、UniLID(零模型成本)、或 LLM tool-call(流水线整合)。
4.2.8 混合语言与语码转换的标注边界
前面讨论的 LangID 工具默认给整段文本一个单一语种标签。但在真实 Web 抓取语料中,大量文本是语码转换(code-switching)——同一句话内嵌多种语言片段(如”这篇 tutorial 的 performance 很好”)。这类文本是 LangID 漏判/误判的主要来源。
工程上存在两种取舍:
- 单一标签:整段文本只输出一个主导语种(简单,但会丢失混合语言信息,配比统计失真)
- 分段边界标注:先做语种分段,再对每段独立标注(准确,但成本高、边界判定本身是难题)
一篇 2026 年关于哈萨克语-俄语语码转换标注的工作(arXiv:2608.00581)将 XLM-R LID v2 作为语种识别基线,报告宏 F1 达 0.966,其核心关切正是”混合语言句子的标注边界”。对多语种/低资源语种场景,建议在 LangID 流水线中额外加入语码转换检测,而非仅依赖单一标签模型。
4.3 Gopher 启发式规则
DeepMind 在 Gopher 论文(Rae et al., 2022)中提出了 8 条经典的启发式质量过滤规则,至今仍是工业界基础:
4.3.1 八条规则
def gopher_filter(text):
"""
Gopher 8条启发式规则
任一规则不通过即拒绝
"""
if not text or len(text.strip()) == 0:
return False, 'empty'
words = text.split()
word_count = len(words)
# 规则1:单词数 50-100,000
if word_count < 50:
return False, 'too_few_words'
if word_count > 100000:
return False, 'too_many_words'
# 规则2:平均词长 3-10
avg_word_len = sum(len(w) for w in words) / word_count
if avg_word_len < 3 or avg_word_len > 10:
return False, 'abnormal_word_length'
# 规则3:符号占比 <10%
symbol_count = sum(1 for c in text if c in '!@#$%^&*()[]{}|\\:";\'<>?,./`~')
symbol_ratio = symbol_count / len(text)
if symbol_ratio > 0.10:
return False, 'too_many_symbols'
# 规则4:停用词占比 ≥3%
# 英文停用词表(简化版)
stop_words = {'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be',
'been', 'being', 'have', 'has', 'had', 'do', 'does',
'did', 'will', 'would', 'could', 'should', 'may',
'might', 'must', 'shall', 'can', 'need', 'dare',
'ought', 'used', 'to', 'of', 'in', 'for', 'on',
'with', 'at', 'by', 'from', 'as', 'into', 'through'}
stop_count = sum(1 for w in words if w.lower() in stop_words)
stop_ratio = stop_count / word_count
if stop_ratio < 0.03:
return False, 'too_few_stopwords'
# 规则5:字母占比 ≥60%
alpha_count = sum(1 for c in text if c.isalpha())
alpha_ratio = alpha_count / len(text)
if alpha_ratio < 0.60:
return False, 'too_few_alpha'
# 规则6:重复行占比 <30%
lines = text.split('\n')
if len(lines) > 1:
line_set = set(lines)
dedup_ratio = 1 - len(line_set) / len(lines)
if dedup_ratio > 0.30:
return False, 'too_many_dup_lines'
# 规则7:n-gram重复检测
# 前100个3-gram中重复 >0.4 拒绝
ngrams = [tuple(words[i:i+3]) for i in range(min(100, word_count-2))]
if ngrams:
ngram_set = set(ngrams)
ngram_dup_ratio = 1 - len(ngram_set) / len(ngrams)
if ngram_dup_ratio > 0.40:
return False, 'too_many_dup_ngrams'
# 规则8:文档末尾不能是单一字符重复(爬虫残留)
if len(text) > 100:
tail = text[-100:]
if len(set(tail)) < 10: # 末尾100字符只有<10种
return False, 'tail_repetition'
return True, 'pass'
4.3.2 Gopher规则调优经验
| 参数 | 默认值 | 推荐范围 | 说明 |
|---|---|---|---|
| min_words | 50 | 30-100 | 太严会丢列表页 |
| max_words | 100,000 | 50,000-200,000 | 严一点防爬虫残留 |
| symbol_ratio | 0.10 | 0.08-0.15 | 代码数据要放宽到0.20 |
| stop_ratio | 0.03 | 0.02-0.05 | 中文/多语种需独立参数 |
| alpha_ratio | 0.60 | 0.50-0.70 | 代码/数学可放宽到0.50 |
| dup_line_ratio | 0.30 | 0.20-0.40 | 严一点防模板页 |
踩坑经验:直接套英文Gopher规则到中文会过滤掉 80%+ 优质中文数据(中文没有空格分词,词长/停用词规则全部失效)。中文必须用单独的规则集:
def gopher_chinese(text):
"""中文专用Gopher规则"""
char_count = len(text)
# 规则1:字符数 50-100,000
if char_count < 50 or char_count > 100000:
return False
# 规则2:中文字符占比 ≥60%
cn_chars = sum(1 for c in text if '\u4e00' <= c <= '\u9fff')
if cn_chars / char_count < 0.60:
return False
# 规则3:标点符号占比 5%-30%
punctuations = ',。!?;:、""''()【】《》…—'
punct_count = sum(1 for c in text if c in punctuations)
punct_ratio = punct_count / char_count
if punct_ratio < 0.05 or punct_ratio > 0.30:
return False
# 规则4:常用字覆盖率 ≥90%
common_chars = load_common_3500_chars() # 国家3500常用字
char_set = set(c for c in text if '\u4e00' <= c <= '\u9fff')
common_coverage = len(char_set & common_chars) / max(len(char_set), 1)
if common_coverage < 0.90:
return False
# 规则5:重复行占比 <30%
lines = text.split('\n')
if len(lines) > 1:
line_set = set(lines)
if 1 - len(line_set) / len(lines) > 0.30:
return False
return True
4.3.3 质量过滤的技术演进路线
在进入下一节的模型打方前,有必要理解质量过滤在2020-2026年间的演进路线:
2020-2022: 启发式规则 (Gopher 8条)
→ 2022-2024: 分类器打分 (FineWeb-Edu, DCLM)
→ 2024-2025: 多维度评分 (QuRating, DSIR)
→ 2025-2026: LLM辅助审查 (NeMo Curator Ensemble Classifier)
2026年的新趋势:NeMo Curator 的质量过滤器引入了 ensemble quality classifier(集成质量分类器),将数据分为 20 个质量桶(quality buckets),允许用户按桶自由选择数据比例(如只保留质量桶 8-19)。这代表了从”二值过滤(好/坏)“到”连续质量分级(1-20级)“的范式转换。
此外,LLM-as-judge 开始被用于对边缘样本进行二次审查——当分类器信心不足时(如评分 2-3 之间),用 LLM 做最终裁定。但需权衡成本:100B tokens 数据中,如果 LLM 审查 10%(10B tokens),按 GPT-4 定价约为 $10,000-50,000,需评估收益是否匹配。
4.4 模型打分:从 Perplexity 到 FineWeb-Edu
启发式规则的局限在于”只看表面”。更高级的是用模型本身来评估文本质量。
4.4.1 Perplexity 过滤
思路:用一个参考模型(通常是较小但训练良好的模型)评估每篇文档的 Perplexity(PPL)。
- PPL 低 = 文本流畅、符合语言模型预期 = 高质量
- PPL 高 = 文本怪异、乱码、非常规表达 = 低质量
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
class PPLFilter:
def __init__(self, model_name='gpt2-medium'):
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
self.model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.bfloat16,
device_map='auto'
)
self.model.eval()
def compute_ppl(self, text, max_length=1024):
"""计算文本的Perplexity"""
encodings = self.tokenizer(
text, return_tensors='pt',
max_length=max_length, truncation=True
)
input_ids = encodings.input_ids.to(self.model.device)
with torch.no_grad():
outputs = self.model(input_ids, labels=input_ids)
loss = outputs.loss.item()
return loss # PPL = exp(loss)
# 使用示例
ppl_filter = PPLFilter()
text_ppl = ppl_filter.compute_ppl(some_text)
# PPL > 阈值的过滤掉
if text_ppl > 5.5: # 阈值需根据数据集调整
reject()
4.4.2 Perplexity 阈值的踩坑
- PPL 阈值不能太低:会把专业领域文本(数学、医学)当成低质
- PPL 阈值不能太高:会让乱码文本通过
- 不同语种需要不同阈值:中文 PPL 普遍比英文高 1-2
推荐做法:用 PPL 分布的百分位数而非绝对值:
# 推荐做法:用百分位
def ppl_threshold_filter(ppl, all_ppls):
"""
根据PPL分布的百分位数过滤
推荐去掉top 20%最高PPL的文本
"""
p80 = np.percentile(all_ppls, 80)
p95 = np.percentile(all_ppls, 95)
if ppl > p95: # 极端值一定拒绝
return False
if ppl > p80 and random() < 0.5: # 高PPL按50%概率丢弃
return False
return True
4.4.3 FineWeb-Edu 教育价值分类器
FineWeb-Edu 是当前最强开源质量过滤方案。核心是用一个分类器打”教育价值 1-5 分”:
# FineWeb-Edu 分类器基于 Llama3-8B 微调
# 训练数据:约 5000 篇人工标注的样本
# 输入:网页文本
# 输出:1-5 分整数评分
# 用 transformers 调用
from transformers import pipeline
classifier = pipeline(
'text-classification',
model='HuggingFaceFW/fineweb-edu-classifier',
device='cuda:0'
)
# 单条打分
result = classifier(text[:4096], truncation=True)
# 输出: [{'label': '4', 'score': 0.85}]
# 批量打分
texts = [doc.text[:4096] for doc in documents]
results = classifier(texts, batch_size=32, truncation=True)
# 推荐保留 ≥3 分的,作为高质量数据
# ≥4 分的作为退火数据
4.4.4 FineWeb-Edu 评分标准
| 评分 | 标准 | 占比 | 用途 |
|---|---|---|---|
| 5 | 大学级以上深度内容,结构化、信息密集 | <1% | 退火精选 |
| 4 | 高中-大学级,有论证结构,事实准确 | ~4% | 高质量训练集 |
| 3 | 初中-高中级,常规知识性内容 | ~10% | 主训练数据 |
| 2 | 简单叙述,新闻列表式 | ~25% | 边缘保留 |
| 1 | SEO、广告、模板 | ~60% | 拒绝 |
4.4.5 DCLM fastText 分类器
DCLM(Data Comp for Language Models)项目的方案,用更轻量的 fastText 做质量分类:
# DCLM fastText 分类器
# 模型下载:https://huggingface.co/matorgii/dclm-fasttext-classifier
import fasttext
model = fasttext.load_model('dclm_quality.bin')
def dclm_quality_score(text):
"""返回 0-1 的质量分数"""
text = text.replace('\n', ' ').strip()
if len(text) < 50:
return 0.0
predictions = model.predict(text)
label, score = predictions
# fastText 输出的是类别和概率
# label='__label__high' 表示高质量
if '__label__high' in label[0]:
return score[0]
return 1 - score[0]
# 推荐阈值:保留 score > 0.5 的
fastText 优势:速度是 BERT 类的 100倍,适合做大规模粗过滤;劣势是准确率略低,建议先用 fastText 粗筛再用 FineWeb-Edu 精筛。
4.4.6 另一种视角:领域构建 vs 质量筛选
前面讨论的方法都假设”质量”是一个客观可测量的属性。但 WebOrganizer(Allen AI / Princeton,“Organize the Web”,2024)提出了一个重要反例:在某些场景下,“质量”本质上是”领域对齐”。
核心实验:FineWeb-Edu 通过教育价值筛选带来的 MMLU 增益,其中 高达 84% 可以通过简单的 Topic × Format 领域混合(domain mixing)复现——即不删除任何”低质量”数据,而是调整训练数据中不同主题(Topic,如科学/人文/编程)和格式(Format,如文章/问答/代码)的比例。
这意味着:
- 如果你训练的是通用模型,质量过滤确实能提升平均性能
- 如果你训练的是专项模型(如教育场景),与其花精力做质量二值分类,不如用领域混合做数据配比优化
- 过度依赖”教育价值”打分可能导致领域偏置——所有数据都像教科书,但现实中用户需要处理口语、方言、网络用语
实践建议:在数据工程早期,先做领域分析(你的目标场景需要哪些Topic×Format),再做质量过滤。避免”先过滤,后才发现关键领域被误删”。
4.5 模型打分的两阶段策略
工业级生产环境的推荐策略:
全量数据 → fastText 粗筛(保留60%)
→ FineWeb-Edu 精筛(保留20%)
→ 进入下游流水线
def two_stage_quality_filter(text):
"""两阶段质量过滤"""
# Stage 1: fastText 粗筛(速度优先)
dclm_score = dclm_quality_score(text)
if dclm_score < 0.3:
return False, 'rejected_by_dclm'
# Stage 2: FineWeb-Edu 精筛(准确优先)
if dclm_score > 0.6:
# 高分直接通过
return True, 'high_quality_pass'
# 中间分数用 FineWeb-Edu 二次确认
edu_score = fineweb_edu_classify(text)
if edu_score >= 3:
return True, 'pass_with_edu_check'
return False, 'rejected_by_edu'
4.6 DSIR 重要性采样
DSIR(Data Selection for Instruction-following Retrieval, Xie et al., 2023)是一种重要性采样方法,让数据分布更接近目标分布。
4.6.1 核心思想
DSIR 不是”过滤”,而是”加权采样”:
- 选定一个目标分布(如高质量教材的 n-gram 分布)
- 计算每篇文档在目标分布下的”重要性分数”
- 按重要性分数采样,使采样后数据更接近目标分布
import numpy as np
from collections import Counter
class DSIRSampler:
def __init__(self, target_corpus, source_corpus, n=2):
"""
target_corpus: 目标高质量文本列表
source_corpus: 待采样的源文本列表
n: n-gram阶数
"""
self.n = n
# 计算目标分布的 n-gram 频率
self.target_dist = self._compute_ngram_dist(target_corpus)
# 计算源分布的 n-gram 频率
self.source_dist = self._compute_ngram_dist(source_corpus)
# 计算每个 n-gram 的对数比
self.log_ratio = {}
all_ngrams = set(self.target_dist.keys()) | set(self.source_dist.keys())
for ngram in all_ngrams:
p_target = self.target_dist.get(ngram, 1e-10)
p_source = self.source_dist.get(ngram, 1e-10)
self.log_ratio[ngram] = np.log(p_target / p_source)
def _compute_ngram_dist(self, corpus):
counter = Counter()
total = 0
for text in corpus:
words = text.split()
for i in range(len(words) - self.n + 1):
ngram = tuple(words[i:i+self.n])
counter[ngram] += 1
total += 1
return {k: v/total for k, v in counter.items()}
def compute_importance(self, text):
"""计算单条文本的重要性分数"""
words = text.split()
score = 0.0
for i in range(len(words) - self.n + 1):
ngram = tuple(words[i:i+self.n])
score += self.log_ratio.get(ngram, 0.0)
return score / max(len(words), 1) # 归一化
def sample(self, texts, target_size):
"""按重要性采样"""
scores = [self.compute_importance(t) for t in texts]
# softmax 转概率
scores = np.array(scores)
probs = np.exp(scores - scores.max())
probs /= probs.sum()
# 按概率采样
indices = np.random.choice(
len(texts), size=target_size,
replace=False, p=probs
)
return [texts[i] for i in indices]
4.7 QuRating 多维评分
QuRating(Pratap et al., 2024)是另一种思路——不是过滤,而是用多维评分做加权采样:
# QuRating 评估的维度
QUALITY_DIMENSIONS = {
'educational_value': '该文本的教育/学习价值',
'factuality': '事实准确性',
'reasoning_required': '是否需要推理才能理解',
'language_style': '语言风格(正式/口语化)',
'audience': '目标读者(专业/大众)',
'information_density': '信息密度',
'originality': '原创性(避免模板化)'
}
# 每个维度用一个分类器打分(1-5)
# 然后按任务需要做加权采样
# 例如训练数学模型:
# educational_value: 高权重
# factuality: 高权重
# language_style: 偏正式
4.7.1 多维打分的优势
- 可解释:知道每个数据样本为什么被选中
- 可调度:不同下游任务用不同加权
- 可对齐:与后训练需求对齐(如教育价值高的样本用于教育场景)
4.8 质量过滤的踩坑经验
坑1:分类器训练数据偏置
某团队用维基百科+arXiv训练”高质量”分类器,结果把所有非学术内容都判为低质。模型生成时只会输出”教科书式”内容,闲聊能力崩塌。分类器训练数据必须覆盖多领域、多风格。
坑2:过度依赖单一指标
某团队只用 PPL 做过滤,结果过滤后的数据 PPL 都很低,但都是同质化的新闻列表。必须用 PPL + 教育价值 + 多样性 三个指标组合。
坑3:过滤过严导致数据池过小
某团队用”教育价值 ≥4”过滤,结果只剩 5% 数据,根本不够训练大模型。粗过滤用 ≥2,精筛用 ≥4,组合使用。
坑4:忽略了域内-域外差异
某团队在英文数据上训练分类器,直接用到中文上,准确率只有 60%。每个语种要单独训练分类器。
坑5:质量过滤后忘记去重
某团队过滤后没去重,结果发现过滤后的”高质量”数据里大量是同一篇文章的多次出现(同一篇文章多版本通过过滤)。质量过滤与去重必须协同,通常先过滤再去重。
4.9 质量过滤的工业级流水线
class QualityFilteringPipeline:
"""工业级质量过滤流水线"""
def __init__(self):
self.langid = fasttext.load_model('lid.176.bin')
self.dclm = fasttext.load_model('dclm_quality.bin')
self.edu_classifier = pipeline(
'text-classification',
model='HuggingFaceFW/fineweb-edu-classifier'
)
def filter(self, doc):
"""主过滤函数"""
text = doc['text']
# Step 1: 语种识别
lang, conf = self._detect_lang(text)
if lang == 'unknown' or conf < 0.5:
return None
doc['lang'] = lang
# Step 2: 启发式规则
if lang == 'en':
passed, reason = gopher_filter(text)
elif lang == 'zh':
passed, reason = gopher_chinese(text)
else:
passed, reason = self._generic_filter(text, lang)
if not passed:
return None
# Step 3: fastText 粗筛
dclm_score = self._dclm_score(text)
if dclm_score < 0.3:
return None
# Step 4: FineWeb-Edu 精筛(仅对中间分数)
if dclm_score < 0.7:
edu_score = self._edu_score(text)
if edu_score < 2:
return None
doc['edu_score'] = edu_score
else:
doc['edu_score'] = self._edu_score(text) # 高分也打标
return doc
4.10 本课总结
- 语种识别:CommonLingua 是2026年推荐首选(334语种,8MB,CommonLID Macro F1 0.788),fastText lid.176 作为对比基线。UniLID(复用分词器)和 Gherbal(阿拉伯方言)为特殊场景提供补充
- Gopher 8规则:单词数/词长/符号/停用词/字母/重复行/n-gram/末尾重复
- 模型打分:Perplexity(粗筛)+ FineWeb-Edu(精筛)两阶段
- 重要性采样:DSIR 按 n-gram 重要性加权,比硬过滤更优雅
- 多维打标:QuRating 7个维度可解释、可调度
- 两阶段策略:fastText 粗筛 → FineWeb-Edu 精筛,速度与准确兼顾
课后练习
- 在你的数据上跑 lid.176,统计语种分布
- 实现 Gopher 8 规则,对比过滤前后数据规模与质量
- 训练一个 FineWeb-Edu 分类器(用 1000 条标注数据)
- 用 DSIR 做一次重要性采样,对比采样前后数据质量
下一章我们将进入安全过滤与去重——把有毒内容挡在门外,把重复内容去掉,这是数据工程的合规底线和质量基础。