第 4 章 第二部分:预训练数据工程

语种识别与质量过滤

CommonLingua语种识别(334语种,仅8MB)、fastText基线、Gopher启发式8条规则、Perplexity/FineWeb-Edu/DCLM模型打分、DSIR/QuRating重要性采样——把垃圾数据挡在门外

语种识别 CommonLingua fastText UniLID Gherbal Gopher规则 FineWeb-Edu Perplexity DSIR QuRating WebOrganizer

3T 原始数据 → 1T 清洁数据。

质量过滤这道关卡,决定了你模型上限的 60%。


4.1 为什么语种识别是第一步

在数据流水线中,语种识别(Language Identification, LangID)是质量过滤的第一道关卡,原因有三:

  1. 下游所有过滤都语种相关:英文停用词表对中文无效,中文Gopher规则对英文无效
  2. 配比计算需要语种标签:训练时按 zh/en/code 分别配比
  3. 多语种模型需要平衡:知道每条数据的语种才能控制配比

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 AccCommonLID Macro F1模型大小架构亮点
CommonLingua2.35M33477.63%0.7888MB无分词器,UTF-8字节级因果Conv1D×3+双向Attention×1+RoPE
fastText lid.218 (NLLB)~600M21859.53%0.659~300MBn-gram哈希+线性分类
fastText lid.176~130M176~55%~0.60126MB同上(旧版)
GlotLID~500M166566.23%0.715~2GBTransformer+CLS分类
Gherbal v4~85M214—0.836(Avg)200MB阿拉伯方言专用优化
UniLID (Qwen2.5)0 (共享分词器)——0.929(GlotLID-C)0 UnigramLM分词器复用
XLM-R LID v2~278M200+—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数字无语种信息先剥离数字
阿拉伯语方言 → MSAar (现代标准阿语)方言与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_words5030-100太严会丢列表页
max_words100,00050,000-200,000严一点防爬虫残留
symbol_ratio0.100.08-0.15代码数据要放宽到0.20
stop_ratio0.030.02-0.05中文/多语种需独立参数
alpha_ratio0.600.50-0.70代码/数学可放宽到0.50
dup_line_ratio0.300.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%边缘保留
1SEO、广告、模板~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 不是”过滤”,而是”加权采样”:

  1. 选定一个目标分布(如高质量教材的 n-gram 分布)
  2. 计算每篇文档在目标分布下的”重要性分数”
  3. 按重要性分数采样,使采样后数据更接近目标分布
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 多维打分的优势

  1. 可解释:知道每个数据样本为什么被选中
  2. 可调度:不同下游任务用不同加权
  3. 可对齐:与后训练需求对齐(如教育价值高的样本用于教育场景)

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 精筛,速度与准确兼顾

课后练习

  1. 在你的数据上跑 lid.176,统计语种分布
  2. 实现 Gopher 8 规则,对比过滤前后数据规模与质量
  3. 训练一个 FineWeb-Edu 分类器(用 1000 条标注数据)
  4. 用 DSIR 做一次重要性采样,对比采样前后数据质量

下一章我们将进入安全过滤与去重——把有毒内容挡在门外,把重复内容去掉,这是数据工程的合规底线和质量基础。