预训练数据处理 Pipeline 设计文档

一套可复现的大模型预训练数据处理流水线设计,覆盖采集、清洗、去重、质量过滤全流程。

技术文档 数据处理 Pipeline 去重 质量过滤 工程实践

概述

本文档定义了一套标准化的预训练数据处理 Pipeline(以下简称 DataPipe),目标是将原始 Web Crawl 数据转化为可直接用于大模型预训练的高质量语料。设计参考了 FineWeb、DCLM、RedPajama 等开源方案的最佳实践。

系统架构

整体流程

┌─────────────────────────────────────────────────┐
│                   DataPipe v2.0                  │
├─────────────────────────────────────────────────┤
│                                                  │
│  Stage 1: 采集与提取                              │
│  ┌────────┐   ┌────────┐   ┌────────────┐       │
│  │ WARC   │──▶│ 正文   │──▶│ 格式标准化  │       │
│  │ 下载   │   │ 提取   │   │ (JSON-Lines)│       │
│  └────────┘   └────────┘   └────────────┘       │
│                     │                            │
│  Stage 2: 基础过滤                               │
│  ┌────────┐   ┌────────┐   ┌──────────┐         │
│  │ URL    │──▶│ 语言   │──▶│ 规则过滤  │         │
│  │ 过滤   │   │ 识别   │   │ (行+文档) │         │
│  └────────┘   └────────┘   └──────────┘         │
│                     │                            │
│  Stage 3: 去重                                   │
│  ┌────────┐   ┌────────┐   ┌──────────┐         │
│  │ URL    │──▶│ MinHash│──▶│ 精确子串  │         │
│  │ 去重   │   │ 去重   │   │ 去重     │          │
│  └────────┘   └────────┘   └──────────┘         │
│                     │                            │
│  Stage 4: 质量过滤                               │
│  ┌────────┐   ┌──────────┐  ┌──────────┐        │
│  │ PPL    │──▶│ 质量分类 │──▶│ 教育价值 │        │
│  │ 过滤   │   │ 器打分   │  │ 过滤     │        │
│  └────────┘   └──────────┘  └──────────┘        │
│                     │                            │
│  Stage 5: 后处理与打包                            │
│  ┌────────┐   ┌──────────┐  ┌──────────┐        │
│  │ PII    │──▶│ Tokenize │──▶│ 打包成   │        │
│  │ 脱敏   │   │ + 统计   │  │ 训练格式 │         │
│  └────────┘   └──────────┘  └──────────┘        │
│                                                  │
└─────────────────────────────────────────────────┘

技术选型

组件选型理由
任务调度Ray / Spark分布式处理,易扩展
存储HDFS / S3支持 PB 级数据
正文提取trafilatura准确率高,社区活跃
语言识别fastText lid.176176 种语言,ms 级延迟
去重text-dedupMinHash + SA 双引擎
质量分类自训练 DeBERTa-v3平衡精度和推理速度
数据格式JSON Lines + Parquet流式处理 + 高效查询

Stage 1:采集与提取

1.1 WARC 数据获取

从 Common Crawl 获取原始 WARC 文件:

# warc_downloader.py
import warcio
from dataclasses import dataclass

@dataclass
class WARCConfig:
    crawl_id: str = "CC-MAIN-2024-51"
    segment_list: str = "warc.paths.gz"
    max_workers: int = 64
    output_dir: str = "/data/raw_warc/"
    
    # 过滤条件
    min_content_length: int = 200  # bytes
    allowed_content_types: tuple = ("text/html",)

1.2 正文提取

使用 trafilatura 提取 HTML 正文:

import trafilatura

def extract_content(html: str, url: str) -> dict | None:
    result = trafilatura.extract(
        html,
        url=url,
        include_comments=False,
        include_tables=True,
        no_fallback=False,  # 允许回退到 readability
        favor_precision=True,
        deduplicate=True,
    )
    if result is None or len(result) < 100:
        return None
    
    return {
        "text": result,
        "url": url,
        "source": "common_crawl",
        "crawl_id": config.crawl_id,
        "extracted_at": datetime.utcnow().isoformat(),
    }

1.3 输出格式

统一为 JSON Lines 格式,每行一个文档:

{
    "id": "cc-2024-51-00001-a3f2b9c1",
    "text": "文档正文内容...",
    "url": "https://example.com/article/123",
    "source": "common_crawl",
    "crawl_id": "CC-MAIN-2024-51",
    "language": null,
    "metadata": {
        "content_length": 2847,
        "extracted_at": "2026-03-18T10:30:00Z"
    }
}

Stage 2:基础过滤

2.1 URL 过滤

维护黑名单 + 白名单:

# URL 黑名单规则
URL_BLACKLIST_PATTERNS = [
    r"\.xxx$", r"\.porn$",           # 成人内容
    r"buy.*cheap", r"viagra",         # 垃圾广告
    r"login|signup|register",         # 登录/注册页
    r"terms-of-service|privacy-policy",  # 法律页面
    r"\.pdf$|\.doc$|\.ppt$",         # 非 HTML 文件
]

# 高质量域名优先级
QUALITY_DOMAINS = {
    "tier1": ["wikipedia.org", "arxiv.org", "github.com"],
    "tier2": ["medium.com", "stackoverflow.com", "zhihu.com"],
    "tier3": ["*.edu", "*.gov", "*.ac.*"],
}

2.2 语言识别

import fasttext

model = fasttext.load_model("lid.176.bin")

def detect_language(text: str) -> tuple[str, float]:
    # 取前 500 字符,避免长文本影响速度
    predictions = model.predict(text[:500].replace("\n", " "))
    lang = predictions[0][0].replace("__label__", "")
    confidence = predictions[1][0]
    return lang, confidence

# 过滤策略
LANGUAGE_CONFIG = {
    "zh": {"min_confidence": 0.65, "target_ratio": 0.25},
    "en": {"min_confidence": 0.80, "target_ratio": 0.55},
    "code": {"min_confidence": 0.50, "target_ratio": 0.15},
    "other": {"min_confidence": 0.85, "target_ratio": 0.05},
}

2.3 规则过滤

分为行级过滤和文档级过滤两个层次:

行级过滤(移除特定行):

LINE_FILTERS = {
    "nav_elements": r"^(Home|About|Contact|Menu|Navigation|Copyright|©)",
    "social_media": r"(Share on|Follow us|Tweet|Like us)",
    "boilerplate": r"(Cookie|Accept|Subscribe|Newsletter|Click here)",
    "short_lines": lambda line: len(line.split()) < 3,
    "all_uppercase": lambda line: line == line.upper() and len(line) > 10,
}

文档级过滤(丢弃整个文档):

DOC_FILTERS = {
    "min_words": 50,
    "max_words": 100000,
    "min_avg_word_length": 3,       # 过短 → 可能是缩写/乱码
    "max_avg_word_length": 20,      # 过长 → 可能是 URL/代码
    "max_special_char_ratio": 0.3,  # 特殊字符占比
    "max_digit_ratio": 0.5,         # 数字占比
    "min_unique_word_ratio": 0.1,   # 去重词占比(过低=重复)
    "max_line_dup_ratio": 0.3,      # 重复行占比
    "required_stop_word_ratio": 0.06,  # (英文) stopword 最低占比
}

中文特殊规则:

ZH_DOC_FILTERS = {
    "min_chars": 100,               # 中文按字符数
    "max_chars": 200000,
    "min_cn_char_ratio": 0.3,       # 中文字符最低占比
    "max_punctuation_ratio": 0.15,  # 标点占比
    "no_garbled": True,             # 检测乱码 (编码问题)
}

Stage 3:去重

3.1 URL 去重

最简单但非常有效——完全相同的 URL 只保留一个:

def url_dedup(docs: Iterator[dict]) -> Iterator[dict]:
    seen_urls = BloomFilter(capacity=10_000_000_000, error_rate=0.001)
    for doc in docs:
        normalized_url = normalize_url(doc["url"])
        if normalized_url not in seen_urls:
            seen_urls.add(normalized_url)
            yield doc

3.2 MinHash 模糊去重

检测内容高度相似但不完全相同的文档:

from datasketch import MinHash, MinHashLSH

MINHASH_CONFIG = {
    "num_perm": 128,          # 签名长度
    "ngram_size": 5,          # 英文 5-gram
    "ngram_size_zh": 3,       # 中文 3-gram (字级别)
    "threshold": 0.8,         # Jaccard 相似度阈值
    "bands": 9,               # LSH bands
    "rows": 13,               # LSH rows per band
    # bands × rows ≈ num_perm, 阈值 ≈ (1/bands)^(1/rows) ≈ 0.8
}

3.3 精确子串去重

用于移除跨文档的重复段落(常见于新闻转载、模板内容):

SUBSTR_DEDUP_CONFIG = {
    "min_substr_length": 200,     # 最短重复子串长度 (bytes)
    "method": "suffix_array",     # 使用 suffix array
    "window_size": 6,             # sliding window for hash
}

3.4 各阶段去重效果(经验数据)

去重步骤数据缩减比例累计剩余
原始提取-100%
URL 去重~15%85%
MinHash 去重~25%60%
子串去重~10%50%
总计~50%50%

Stage 4:质量过滤

4.1 困惑度过滤

使用在高质量数据上训练的 KenLM 模型计算文档困惑度:

import kenlm

class PerplexityFilter:
    def __init__(self, model_path: str, percentile_range: tuple = (5, 95)):
        self.model = kenlm.Model(model_path)
        self.low_pct, self.high_pct = percentile_range
    
    def score(self, text: str) -> float:
        words = text.split()
        log_prob = self.model.score(" ".join(words))
        ppl = 10 ** (-log_prob / len(words))
        return ppl
    
    def filter(self, text: str) -> bool:
        ppl = self.score(text)
        # 困惑度过低 → 重复/模板内容
        # 困惑度过高 → 乱码/低质量
        return self.low_threshold <= ppl <= self.high_threshold

4.2 模型驱动的质量分类

训练一个轻量级质量分类器:

# 训练数据准备
# 正样本:Wikipedia, 教科书, 优质学术内容
# 负样本:从 CommonCrawl 随机采样的低质量文本

QUALITY_CLASSIFIER_CONFIG = {
    "model": "microsoft/deberta-v3-small",
    "max_length": 512,
    "batch_size": 256,
    "threshold": 0.5,        # 质量分 > 0.5 保留
    "training_samples": 500_000,
}

4.3 教育价值过滤(可选)

仿照 FineWeb-Edu 的方案,对筛选后的数据进一步按教育价值评分:

EDU_FILTER_CONFIG = {
    "model": "HuggingFaceTB/fineweb-edu-classifier",
    "min_score": 3,          # 满分 5,≥3 保留
    "apply_to": ["en"],      # 目前只对英文有效
}

Stage 5:后处理与打包

5.1 PII 脱敏

PII_PATTERNS = {
    "email": r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
    "phone_cn": r"1[3-9]\d{9}",
    "phone_us": r"\+?1?\d{10,11}",
    "id_card_cn": r"\d{17}[\dXx]",
    "ip_address": r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}",
    "credit_card": r"\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}",
}

def redact_pii(text: str) -> str:
    for pii_type, pattern in PII_PATTERNS.items():
        text = re.sub(pattern, f"[{pii_type.upper()}_REDACTED]", text)
    return text

5.2 Tokenization 与统计

from transformers import AutoTokenizer

def tokenize_and_stats(doc: dict, tokenizer) -> dict:
    tokens = tokenizer.encode(doc["text"])
    doc["token_count"] = len(tokens)
    doc["char_count"] = len(doc["text"])
    doc["compression_ratio"] = doc["char_count"] / doc["token_count"]
    return doc

5.3 训练数据打包

最终输出为训练框架可直接加载的格式:

# 方案 A: 预 tokenize 的 numpy memmap
# 适合 Megatron-LM / GPT-NeoX
OUTPUT_FORMAT_A = {
    "format": "numpy_memmap",
    "dtype": "uint16",  # 对应 vocab_size < 65536
    "chunk_size": 1024 * 1024 * 100,  # 100M tokens per file
}

# 方案 B: Arrow / Parquet
# 适合 HuggingFace datasets
OUTPUT_FORMAT_B = {
    "format": "parquet",
    "row_group_size": 10000,
    "compression": "zstd",
}

监控与质量保障

关键指标看板

dashboard_metrics:
  pipeline_health:
    - throughput_docs_per_second
    - stage_latency_p50_p99
    - error_rate_by_stage
    - retry_count
    
  data_quality:
    - language_distribution
    - quality_score_distribution
    - token_count_histogram
    - dedup_rate_by_stage
    
  output_stats:
    - total_tokens_by_language
    - domain_distribution_top100
    - category_distribution
    - daily_output_volume

质量抽检流程

每个 batch 产出后,随机抽样 1000 条进行人工审查:

QUALITY_AUDIT = {
    "sample_size": 1000,
    "dimensions": [
        "factual_accuracy",     # 事实准确性
        "coherence",            # 连贯性
        "relevance",            # 内容相关性
        "language_quality",     # 语言质量
        "formatting",           # 格式规范性
    ],
    "min_pass_rate": 0.95,     # 95% 通过率
    "escalation_threshold": 0.90,  # 低于 90% 触发告警
}

资源估算

处理 1T tokens 的参考资源

阶段CPU 核·时GPU 核·时存储 (TB)
采集与提取50K020
基础过滤10K015
去重100K030 (临时)
质量过滤20K2K (A100)10
后处理与打包10K05
总计~190K~2K~80

按云服务价格估算,处理 1T tokens 的总成本约 $2,000-5,000。

附录:配置模板

# datapipe_config.yaml
pipeline:
  name: "datapipe-v2.0"
  version: "2.0.0"
  
stages:
  extraction:
    tool: trafilatura
    min_content_length: 200
    
  language_detection:
    model: fasttext-lid-176
    languages:
      zh: {min_confidence: 0.65}
      en: {min_confidence: 0.80}
      
  deduplication:
    url_dedup: true
    minhash:
      enabled: true
      num_perm: 128
      threshold: 0.8
    suffix_array:
      enabled: true
      min_length: 200
      
  quality_filter:
    perplexity:
      model: kenlm-wiki
      range: [5, 95]  # percentile
    classifier:
      model: deberta-v3-small-quality
      threshold: 0.5
      
  output:
    format: parquet
    compression: zstd
    tokenizer: null  # 可选预 tokenize