09e-斯坦福CS336作业四:大规模语言模型训练数据收集与处理

09e-斯坦福CS336作业四:大规模语言模型训练数据收集与处理 💻

本文档详细讲解斯坦福 CS336 课程作业四的核心内容,涵盖从 Common Crawl 原始网页数据到高质量预训练数据集的完整处理流程,包括 HTML 转纯文本、内容过滤策略、大规模数据去重算法,以及高效数据流水线构建 🛠️

This document explains Assignment 4 of Stanford CS336, covering the complete pipeline from raw Common Crawl web data to high-quality pretraining datasets, including HTML-to-text extraction, content filtering strategies, large-scale deduplication algorithms, and efficient data pipeline construction 🛠️


术语表 / Terminology

术语 / Term 中文 说明 / Description
Common Crawl 通用爬虫数据 开源的网页爬虫数据集,包含数十亿网页的原始 HTML
WET File 提取文本文件 Common Crawl 提供的已提取文本格式,包含网页的纯文本内容
WARC File 网页存档文件 Common Crawl 原始格式,包含完整的 HTTP 响应和 HTML 内容
HTML Extraction HTML 提取 从 HTML 文档中提取主要文本内容,去除标签、脚本等噪声
Content Filtering 内容过滤 识别并移除有害、低质量或不合适的文本内容
PII Redaction 个人隐私信息脱敏 检测并删除个人敏感信息(如邮箱、电话、身份证号等)
Deduplication 数据去重 识别并移除重复或高度相似的文档
MinHash 最小哈希 一种高效的集合相似度估计算法,用于近似去重
LSH 局部敏感哈希 Locality-Sensitive Hashing,快速筛选候选重复对
Jaccard Similarity 杰卡德相似度 两个集合的交集与并集的比值,衡量相似度
n-gram n元语法 连续的 n 个字符或词元,用于构建文本特征集合
Data Pipeline 数据流水线 将多个处理步骤串联成自动化流程

章节阅读路线图 🗺️ / Chapter Reading Roadmap

  1. 作业概述 📋 / Assignment Overview → 理解作业目标和整体架构
  2. HTML 转纯文本 🌐 / HTML to Text Extraction → 从网页数据中提取干净文本
  3. 内容过滤策略 🛡️ / Content Filtering Strategies → 过滤有害和低质量内容
  4. 大规模数据去重 🔍 / Large-Scale Deduplication → MinHash-LSH 算法原理与实现
  5. 构建数据流水线 🔄 / Building Data Pipeline → 整合所有步骤的完整流程
  6. 总结 📝 / Summary → 回顾核心要点

1. 作业概述 📋 / Assignment Overview

📖 Note: 本章介绍 CS336 作业四的目标、数据源和整体架构 / This chapter introduces the goals, data sources, and overall architecture of CS336 Assignment 4.

1.1 作业目标 🎯 / Assignment Goals

斯坦福 CS336(Language Modeling from Scratch)的作业四专注于 大规模训练数据的收集和处理 。在真实的大语言模型训练中,数据质量直接决定了模型性能的上限 ------ "Garbage In, Garbage Out"(垃圾进,垃圾出)。

核心任务 📝:

  1. 将 Common Crawl 的原始 HTML 转换为干净的纯文本
  2. 过滤有害内容(暴力、色情、仇恨言论等)和低质量文本
  3. 移除个人隐私信息(PII),如邮箱、电话、身份证号
  4. 实现高效的文本去重算法,消除重复内容
  5. 构建完整的数据处理流水线,优化处理效率

1.2 数据源:Common Crawl 🌐 / Data Source

Common Crawl 是一个非营利组织,自 2007 年以来持续爬取互联网网页,提供开放的网页数据集。它是大多数大语言模型(如 GPT-3、LLaMA、Mistral 等)的主要训练数据来源。

数据格式 📦:

  • WARC 文件:原始网页存档,包含完整的 HTTP 响应头、HTML 内容、图片等
  • WET 文件:已提取的纯文本文件,去除了 HTML 标签,但包含大量噪声(导航栏、广告、脚本残留等)

数据规模 📊:

  • 每次爬取包含 数十亿个网页
  • 压缩后数据量达 数百 TB
  • 经过过滤和去重后,可获得 数万亿高质量 Token

直观类比 🎨:想象 Common Crawl 是一座巨大的"原始矿山"

  • WARC 文件是"未加工的矿石"------包含所有杂质
  • WET 文件是"粗炼的金属"------去除了大石块,但仍有杂质
  • 经过过滤和去重后的数据才是"精炼的钢材"------可以直接用于训练

参考资料:


2. HTML 转纯文本 🌐 / HTML to Text Extraction

🌐 Note: 本章讲解如何从网页 HTML 中提取干净的纯文本内容 / This chapter explains how to extract clean plain text from web HTML.

2.1 为什么需要 HTML 提取?🤔 / Why HTML Extraction?

网页的 HTML 结构包含大量非内容元素:

  • HTML 标签 :<div><span><a> 等结构标签
  • 脚本代码 :<script> 中的 JavaScript 代码
  • 样式代码 :<style> 中的 CSS 样式
  • 导航和广告:侧边栏、页脚、弹窗等无关内容
  • 注释和元数据 :<!-- comment --><meta> 标签

直观类比 🎨:HTML 就像一个"包装精美的礼盒"

  • 礼盒的包装纸、丝带、卡片 = HTML 标签、脚本、样式
  • 礼盒里的礼物 = 网页的主要文本内容
  • HTML 提取就是"拆礼盒"------去掉包装,拿到核心内容

2.2 提取方法对比 ⚔️ / Extraction Methods Comparison

方法 优点 缺点 适用场景
BeautifulSoup 🍲 灵活、可控性强 需要手动编写规则,易出错 简单网页、定制化需求
Trafilatura 🚀 高精度、自动识别主内容 对特殊网页可能失效 大规模爬虫、通用场景
Readability 📖 模仿浏览器阅读模式 速度较慢 新闻文章提取
Newspaper3k 📰 针对新闻优化 对非新闻网站效果差 新闻网站专用

性能对比 📊(基于学术研究):

  • Trafilatura 的提取准确率 显著高于 BeautifulSoup 的全文本提取
  • Trafilatura 能自动过滤导航栏、广告、页脚等噪声
  • 处理速度:单个网页 < 1 秒,适合大规模处理

2.3 Trafilatura 实战示例 💻 / Trafilatura Example

python 复制代码
import trafilatura                                        # 导入 Trafilatura 库,用于网页文本提取 🚀

"""使用 Trafilatura 从 HTML 提取主文本内容

参数:
    html_content: HTML 字符串,示例:"<html><body><h1>标题</h1><p>正文...</p></body></html>"
    
返回:
    extracted_text: 提取的纯文本,示例:"标题\n正文..."
    
示例:
    html = requests.get(url).text
    text = extract_main_text(html)
"""
def extract_main_text(html_content):
    # 使用 trafilatura 提取主文本,自动过滤导航、广告等噪声 🎯
    # 数据流动:html_content[str] → trafilatura.extract() → extracted_text[str]
    extracted_text = trafilatura.extract(
        html_content,                                       # 输入 HTML 字符串
        include_comments=False,                             # 不包含 HTML 注释
        include_tables=True,                                # 包含表格内容
        include_links=False,                                # 不包含链接文本
        output_format="txt"                                 # 输出纯文本格式
    )
    
    return extracted_text if extracted_text else ""         # 返回提取结果,失败时返回空字符串

2.4 提取后的后处理 🔧 / Post-Processing

提取后的文本仍需进一步清理:

python 复制代码
import re                                                   # 导入正则表达式模块,用于文本清洗 🔧

"""清理提取后的文本,移除残留噪声

参数:
    text: 原始提取文本,示例:"  \n  这是一段正文  \n  \n  \n  另一段...  "
    
返回:
    cleaned_text: 清理后的文本,示例:"这是一段正文\n另一段..."
    
示例:
    cleaned = clean_extracted_text(raw_text)
"""
def clean_extracted_text(text):
    # 1️⃣ 移除多余空白行,保留最多连续 2 个换行符
    # 数据流动:text[str] → re.sub() → text[str](多余换行被替换)
    text = re.sub(r'\n{3,}', '\n\n', text)                  # 将 3+ 个连续换行替换为 2 个
    
    # 2️⃣ 移除行首行尾空白
    # 数据流动:text[str] → strip() → text[str](首尾空格被移除)
    text = text.strip()
    
    # 3️⃣ 移除残留的 HTML 实体(如 &nbsp;、&amp;)
    # 数据流动:text[str] → re.sub() → text[str](HTML 实体被移除)
    text = re.sub(r'&[a-z]+;', ' ', text)                   # 移除 HTML 实体引用
    
    return text

关键挑战 ⚠️:

  • 不同网站结构差异巨大,难以用单一规则处理
  • 需要平衡"提取完整性"和"噪声过滤率"
  • 动态加载内容(JavaScript)无法通过静态 HTML 获取

参考资料:


3. 内容过滤策略 🛡️ / Content Filtering Strategies

🛡️ Note: 本章讲解如何识别并过滤有害、低质量内容和个人隐私信息 / This chapter explains how to identify and filter harmful, low-quality content and PII.

3.1 为什么需要内容过滤?🤔 / Why Content Filtering?

Common Crawl 包含互联网上的所有网页,其中不可避免地存在:

  • 有害内容:暴力、色情、仇恨言论、极端主义
  • 低质量内容:乱码、机器生成文本、SEO 垃圾内容
  • 个人隐私信息(PII):邮箱、电话、身份证号、地址
  • 代码和数据:非自然语言内容(如 JSON、XML、代码片段)

不过滤的后果 ⚠️:

  • 模型学习到有害行为,产生不安全输出
  • 模型性能下降,因为低质量数据干扰学习信号
  • 隐私泄露风险,模型可能"记住"并输出 PII

直观类比 🎨:内容过滤就像"食品安检"

  • 有害内容 = "有毒食品"------必须严格检测并移除
  • 低质量内容 = "过期食品"------营养价值低,影响健康
  • PII = "个人标签"------需要撕掉以保护隐私

3.2 有害内容检测 🔍 / Harmful Content Detection

检测方法:

  1. 基于关键词的启发式规则 📝

    python 复制代码
    # 简单关键词匹配(仅作示例,实际需要更复杂的规则)
    harmful_keywords = ["暴力关键词", "仇恨言论关键词", "色情关键词"]
    
    def is_harmful_heuristic(text):
        text_lower = text.lower()                             # 转换为小写,示例:"This is BAD content" → "this is bad content"
        return any(keyword in text_lower for keyword in harmful_keywords)  # 检查是否包含任何有害关键词
  2. 基于分类器的检测 🤖

    • 使用预训练的分类模型(如 Perspective API、Detoxify)
    • 对文本进行毒性评分(Toxicity Score)
    • 超过阈值则过滤
    python 复制代码
    from detoxify import Detoxify                           # 导入 Detoxify 毒性检测库 🤖
    
    """使用预训练分类器检测有害内容
    
    参数:
        text: 待检测文本,示例:"这是一段正常的内容"
        threshold: 毒性阈值(默认 0.5),示例:0.5 表示毒性概率超过 50% 则判定有害
        
    返回:
        is_harmful: 是否有害(布尔值),示例:True 表示有害
        toxicity_score: 毒性评分,示例:0.85 表示 85% 的毒性概率
        
    示例:
        harmful, score = detect_harmful_content("有害内容示例")
    """
    def detect_harmful_content(text, threshold=0.5):
        model = Detoxify('original')                          # 加载预训练毒性检测模型
        scores = model.predict(text)                          # 预测毒性评分,返回:{'toxicity': 0.85, 'severe_toxicity': 0.12, ...}
        
        toxicity_score = scores['toxicity']                   # 获取综合毒性评分
        is_harmful = toxicity_score > threshold               # 判断是否超过阈值
        
        return is_harmful, toxicity_score

3.3 质量过滤策略 📊 / Quality Filtering

常见质量指标:

  1. 文本长度 📏

    • 过短(< 50 字符):可能是标题、导航、广告
    • 过长(> 100,000 字符):可能是代码、日志文件
  2. 词元与字符比例 🔤

    • 正常文本:词元数 / 字符数 ≈ 0.15-0.25
    • 代码/乱码:比例异常高或低
  3. 特殊字符比例 ⚙️

    • 高比例的非字母数字字符(如 {}, <>, ##)可能表示代码
  4. 语言识别 🌍

    • 使用 langdetect 或 fasttext 识别语言
    • 仅保留目标语言(如英文)
python 复制代码
from langdetect import detect                               # 导入语言检测库 🌍

"""基于启发式规则过滤低质量文本

参数:
    text: 待过滤文本,示例:"这是一段质量不错的英文文本..."
    min_length: 最小长度(默认 50),示例:50 表示少于 50 字符则过滤
    max_length: 最大长度(默认 100000),示例:100000 表示超过 10 万字符则过滤
    
返回:
    is_quality: 是否高质量(布尔值),示例:True 表示通过质量检查
    
示例:
    if is_quality_text(text):  # 仅处理高质量文本
        process(text)
"""
def is_quality_text(text, min_length=50, max_length=100000):
    # 1️⃣ 检查文本长度是否在合理范围内
    # 数据流动:text[str] → len() → length[int] → 判断范围
    if len(text) < min_length or len(text) > max_length:
        return False
    
    # 2️⃣ 检查词元与字符比例(英文文本通常 0.15-0.25)
    # 数据流动:text[str] → split() → word_count[int] → ratio[float]
    word_count = len(text.split())                          # 统计词元数量
    char_count = len(text)                                  # 统计字符数量
    word_char_ratio = word_count / char_count if char_count > 0 else 0  # 计算比例
    
    if word_char_ratio < 0.10 or word_char_ratio > 0.30:
        return False
    
    # 3️⃣ 检测是否为英文(可根据需求调整)
    # 数据流动:text[str] → detect() → language_code[str] → 判断是否"en"
    try:
        language = detect(text)                             # 检测语言,返回:"en"、"zh"、"fr" 等
        if language != 'en':                                # 如果不是英文则过滤
            return False
    except:
        return False                                        # 检测失败则过滤
    
    return True

3.4 PII 脱敏 🔒 / PII Redaction

常见 PII 模式:

  • 邮箱:user@example.com
  • 电话号码:+1-234-567-8900
  • 身份证号/社保号:123-45-6789
  • IP 地址:192.168.1.1
python 复制代码
import re                                                   # 导入正则表达式模块 🔧

"""使用正则表达式移除文本中的个人隐私信息(PII)

参数:
    text: 包含 PII 的文本,示例:"联系邮箱:user@example.com,电话:123-456-7890"
    
返回:
    redacted_text: 移除 PII 后的文本,示例:"联系邮箱:[EMAIL_REDACTED],电话:[PHONE_REDACTED]"
    
示例:
    clean_text = redact_pii("我的邮箱是 test@test.com")
    # 返回:"我的邮箱是 [EMAIL_REDACTED]"
"""
def redact_pii(text):
    # 1️⃣ 移除邮箱地址
    # 数据流动:text[str] → re.sub() → text[str](邮箱被替换为 [EMAIL_REDACTED])
    text = re.sub(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', '[EMAIL_REDACTED]', text)
    
    # 2️⃣ 移除电话号码(多种格式)
    # 数据流动:text[str] → re.sub() → text[str](电话号码被替换为 [PHONE_REDACTED])
    text = re.sub(r'\b(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b', '[PHONE_REDACTED]', text)
    
    # 3️⃣ 移除 IP 地址
    # 数据流动:text[str] → re.sub() → text[str](IP 地址被替换为 [IP_REDACTED])
    text = re.sub(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b', '[IP_REDACTED]', text)
    
    return text

参考资料:


4. 大规模数据去重 🔍 / Large-Scale Deduplication

🔍 Note: 本章讲解 MinHash-LSH 算法原理和实现,用于高效识别重复文档 / This chapter explains MinHash-LSH algorithm for efficient duplicate detection.

4.1 为什么需要去重?🤔 / Why Deduplication?

Common Crawl 中存在大量重复内容:

  • 镜像网站:相同内容在不同域名下
  • 转载和复制:新闻、博客被多次转载
  • 模板内容:电商产品页、法律文档仅替换少量信息
  • 爬虫重复:同一页面被多次爬取

不去重的后果 ⚠️:

  • 模型记忆:重复内容会被模型"过度学习",导致过拟合
  • 训练效率浪费:计算资源用于处理冗余数据
  • 评估偏差:测试集和训练集的重复导致性能虚高

直观类比 🎨:去重就像"整理书架"

  • 重复的书 = 重复的文档------占用空间但不增加知识
  • 保留一本 = 保留一个文档副本------节省空间,提高效率
  • 整理后的书架 = 去重后的数据集------每本书都有价值

4.2 Jaccard 相似度 📐 / Jaccard Similarity

去重的核心是衡量两个文档的相似度 。最常用的指标是 Jaccard 相似度

定义 📝:

对于两个集合 A 和 B,Jaccard 相似度定义为:

J(A, B) = \\frac{\|A \\cap B\|}{\|A \\cup B\|}

  • ( |A \cap B| ):交集大小(两个集合共有的元素)
  • ( |A \cup B| ):并集大小(两个集合的所有不重复元素)
  • 取值范围:0, 1,1 表示完全相同,0 表示完全不同

如何将文本转换为集合? 🤔

使用 n-gram 集合:

  • 将文本拆分为连续的 n 个字符(或词)
  • 例如,文本 "hello" 的 3-gram 集合:{"hel", "ell", "llo"}

示例计算 🧮:

文档 A: "猫喜欢鱼"

文档 B: "猫爱吃鱼"

使用 2-gram:

  • A 的 2-gram 集合:{"猫喜", "喜欢", "欢鱼"}
  • B 的 2-gram 集合:{"猫喜", "喜欢", "欢鱼", "爱吃"}
  • 交集:{"猫喜", "喜欢", "欢鱼"} → 大小 3
  • 并集:{"猫喜", "喜欢", "欢鱼", "爱吃"} → 大小 4
  • Jaccard 相似度 = 3/4 = 0.75

4.3 朴素去重 vs 高效去重 ⚔️ / Naive vs Efficient Deduplication

朴素方法 ❌:

python 复制代码
# 对每对文档计算 Jaccard 相似度 → O(n²) 复杂度
for i in range(len(documents)):
    for j in range(i+1, len(documents)):
        similarity = jaccard(documents[i], documents[j])
        if similarity > threshold:
            mark_as_duplicate(j)

问题 📊:

  • 100 万文档 → 需要计算 5000 亿次 Jaccard 相似度
  • 计算量随文档数平方增长,无法扩展到大规模数据

高效方法:MinHash + LSH ✅:

  • MinHash:将文档压缩为紧凑签名,快速估算 Jaccard 相似度
  • LSH:只对"可能相似"的文档对进行详细比较
  • 复杂度从 O(n²) 降低到 接近 O(n)

4.4 MinHash 算法原理 🔐 / MinHash Algorithm

核心思想 🎯:

MinHash 基于一个关键观察:两个集合的 Jaccard 相似度 = 它们的最小哈希值相同的概率

算法步骤 📝:

  1. 构建 n-gram 集合:将每个文档转换为 n-gram 集合
  2. 应用多个哈希函数:对每个 n-gram 计算多个哈希值
  3. 取最小哈希值:对每个哈希函数,取集合中所有 n-gram 的最小哈希值
  4. 形成签名矩阵:将所有最小哈希值组合成文档的"签名"
  5. 比较签名:两个文档签名相同的比例 ≈ Jaccard 相似度

直观类比 🎨:MinHash 就像"指纹识别"

  • 完整文档 = "完整的人"------信息量巨大,难以直接比较
  • MinHash 签名 = "指纹"------紧凑但具有代表性
  • 比较指纹 = 比较签名------快速判断是否"同一个人"
python 复制代码
import hashlib                                              # 导入哈希库,用于计算哈希值 🔐

def get_ngrams(text, n=5):
    """将文本拆分为 n-gram 集合
    
    参数:
        text: 输入文本,示例:"hello world"
        n: n-gram 大小(默认 5),示例:5 表示每个 gram 包含 5 个字符
        
    返回:
        ngrams: n-gram 集合,示例:{"hello", "ello ", "llo w", "lo wo", "o wor", " worl", "world"}
    """
    return set(text[i:i+n] for i in range(len(text) - n + 1))  # 生成所有 n-gram 并转为集合

def minhash_signature(ngram_set, num_hashes=100):
    """计算文档的 MinHash 签名
    
    参数:
        ngram_set: n-gram 集合,示例:{"hello", "world", ...}
        num_hashes: 哈希函数数量(默认 100),示例:100 表示使用 100 个不同的哈希函数
        
    返回:
        signature: MinHash 签名列表,示例:[23, 45, 12, 89, ...],长度为 num_hashes
    """
    signature = []                                          # 存储每个哈希函数的最小值
    
    for i in range(num_hashes):                             # 遍历每个哈希函数
        # 使用不同的种子生成不同的哈希函数
        min_hash = float('inf')                             # 初始化最小值为无穷大
        
        for ngram in ngram_set:                             # 遍历集合中的每个 n-gram
            # 计算哈希值,示例:hashlib.md5(b"hello_0").hexdigest() → "d8e8fca2dc0f896fd7cb4cb0031ba249"
            hash_value = int(hashlib.md5(f"{ngram}_{i}".encode()).hexdigest(), 16)
            min_hash = min(min_hash, hash_value)            # 保留最小哈希值
        
        signature.append(min_hash)                          # 将当前哈希函数的最小值加入签名
    
    return signature

4.5 LSH 局部敏感哈希 🎯 / Locality-Sensitive Hashing

问题 🤔:即使有了 MinHash 签名,仍然需要比较所有文档对(O(n²))。

LSH 解决方案 🎯:

  • 将 MinHash 签名分块(Band)
  • 同一块内完全相同的文档对才进行详细比较
  • 大幅减少需要比较的文档对数量

算法步骤 📝:

  1. 分割签名:将长度为 100 的签名分成 20 个 Band,每个 Band 包含 5 个哈希值
  2. 哈希分桶:对每个 Band 计算哈希值,将文档放入对应的"桶"
  3. 候选对:同一个桶中的文档对成为"候选重复对"
  4. 精确验证:只对候选对计算精确的 Jaccard 相似度

直观类比 🎨:LSH 就像"快递分拣"

  • 所有包裹 = 所有文档------数量巨大
  • 按地区分拣 = 按 Band 分桶------相似地区的包裹放在一起
  • 只比较同一地区的包裹 = 只比较同一桶的文档------大幅减少工作量
python 复制代码
from collections import defaultdict                         # 导入默认字典,用于构建哈希桶 📦

def lsh_deduplicate(documents, num_bands=20, band_size=5, threshold=0.8):
    """使用 LSH 进行大规模文档去重
    
    参数:
        documents: 文档列表,示例:["文档1内容", "文档2内容", ...]
        num_bands: Band 数量(默认 20),示例:20
        band_size: 每个 Band 的大小(默认 5),示例:5
        threshold: Jaccard 相似度阈值(默认 0.8),示例:0.8 表示超过 80% 相似度则判定重复
        
    返回:
        unique_docs: 去重后的文档列表
        duplicate_pairs: 检测到的重复对列表,示例:[(0, 5), (2, 10), ...]
    """
    # 1️⃣ 计算所有文档的 MinHash 签名
    # 数据流动:documents[list] → [minhash_signature() → signatures[list of lists]]
    signatures = []
    for doc in documents:
        ngrams = get_ngrams(doc)                            # 提取 n-gram 集合
        sig = minhash_signature(ngrams, num_hashes=num_bands * band_size)  # 计算 MinHash 签名
        signatures.append(sig)
    
    # 2️⃣ 使用 LSH 分桶
    # 数据流动:signatures → buckets[dict] → candidate_pairs[list]
    buckets = defaultdict(list)                             # 哈希桶:bucket_key → [doc_index, ...]
    
    for doc_idx, sig in enumerate(signatures):              # 遍历每个文档的签名
        for band_idx in range(num_bands):                   # 遍历每个 Band
            # 提取当前 Band 的签名片段
            start = band_idx * band_size                    # 计算起始位置,示例:band_idx=2, band_size=5 → start=10
            end = start + band_size                         # 计算结束位置,示例:end=15
            band = tuple(sig[start:end])                    # 提取 Band 片段,示例:(23, 45, 12, 89, 34)
            
            # 将文档索引放入对应的桶
            buckets[(band_idx, band)].append(doc_idx)       # 键:(band_idx, band_signature),值:[文档索引列表]
    
    # 3️⃣ 收集候选对(同一桶中的文档)
    candidate_pairs = set()
    for bucket_docs in buckets.values():                    # 遍历每个桶
        if len(bucket_docs) > 1:                            # 桶中有多个文档才需要比较
            for i in range(len(bucket_docs)):               # 生成所有文档对
                for j in range(i+1, len(bucket_docs)):
                    candidate_pairs.add((bucket_docs[i], bucket_docs[j]))
    
    # 4️⃣ 对候选对进行精确验证(计算实际 Jaccard 相似度)
    duplicate_pairs = []
    unique_indices = set(range(len(documents)))             # 初始假设所有文档都唯一
    
    for doc_i, doc_j in candidate_pairs:
        # 计算实际 Jaccard 相似度
        ngrams_i = get_ngrams(documents[doc_i])
        ngrams_j = get_ngrams(documents[doc_j])
        intersection = len(ngrams_i & ngrams_j)             # 交集大小
        union = len(ngrams_i | ngrams_j)                    # 并集大小
        jaccard_sim = intersection / union if union > 0 else 0  # Jaccard 相似度
        
        if jaccard_sim > threshold:                         # 超过阈值则判定重复
            duplicate_pairs.append((doc_i, doc_j))
            unique_indices.discard(doc_j)                   # 标记为重复(保留第一个)
    
    # 5️⃣ 返回去重后的文档
    unique_docs = [documents[i] for i in unique_indices]    # 根据唯一索引提取文档
    return unique_docs, duplicate_pairs

4.6 去重效果评估 📊 / Deduplication Evaluation

关键指标:

  • 去重率:重复文档数 / 总文档数
  • 召回率:正确识别的重复对 / 实际重复对
  • 精确率:正确识别的重复对 / 预测的重复对
  • 处理速度:文档数 / 秒

典型结果 📈:

  • Common Crawl 去重率:30%-50%(取决于数据集)
  • MinHash-LSH 召回率:> 90%(参数合理时)
  • 处理速度:单线程 ~1000 文档/秒,分布式可达数百万/秒

参考资料:


5. 构建数据流水线 🔄 / Building Data Pipeline

🔄 Note: 本章将所有步骤整合为完整的数据处理流程 / This chapter integrates all steps into a complete data processing pipeline.

5.1 流水线架构 🏗️ / Pipeline Architecture

完整的数据处理流水线包含以下阶段:

css 复制代码
原始 WET 文件 → HTML 提取 → 内容过滤 → PII 脱敏 → 数据去重 → 高质量数据集
     ↓              ↓           ↓           ↓           ↓           ↓
   Common      提取主文本    移除有害/    移除个人    MinHash-    用于训练
   Crawl       清理噪声      低质量内容   隐私信息    LSH 去重    的最终数据

数据流动示例 📊:

假设有 100 万个网页:

  1. HTML 提取:100 万 → 100 万(提取文本,可能损失部分页面)
  2. 质量过滤:100 万 → 60 万(过滤掉 40% 低质量内容)
  3. PII 脱敏:60 万 → 60 万(文本数量不变,仅移除敏感信息)
  4. 数据去重:60 万 → 40 万(移除 33% 重复内容)
  5. 最终输出:40 万高质量文档

5.2 完整流水线实现 💻 / Complete Pipeline Implementation

python 复制代码
import os                                                     # 导入操作系统接口模块 📁
import glob                                                   # 导入文件路径匹配模块 🔍

"""完整的数据处理流水线

参数:
    input_dir: 输入目录(包含 WET 文件),示例:"./data/wet_files/"
    output_file: 输出文件路径,示例:"./data/cleaned_corpus.txt"
    
返回:
    stats: 处理统计信息,示例:{'total': 1000000, 'extracted': 950000, 'filtered': 600000, 'deduplicated': 400000}
    
示例:
    stats = run_data_pipeline("./raw_data/", "./cleaned_data.txt")
    print(f"最终文档数:{stats['deduplicated']}")
"""
def run_data_pipeline(input_dir, output_file):
    stats = {
        'total': 0,                                           # 总文档数
        'extracted': 0,                                       # 成功提取的文档数
        'quality_passed': 0,                                  # 通过质量检查的文档数
        'pii_redacted': 0,                                    # PII 脱敏的文档数
        'deduplicated': 0                                     # 去重后的文档数
    }
    
    # 1️⃣ 读取所有 WET 文件
    wet_files = glob.glob(os.path.join(input_dir, "*.wet"))   # 匹配所有 .wet 文件,示例:["./data/001.wet", "./data/002.wet", ...]
    stats['total'] = len(wet_files)
    
    extracted_docs = []                                       # 存储提取后的文档
    
    # 2️⃣ HTML 提取 + 质量过滤 + PII 脱敏(逐文件处理)
    for wet_file in wet_files:                                # 遍历每个 WET 文件
        with open(wet_file, 'r', encoding='utf-8') as f:      # 打开文件读取
            html_content = f.read()                           # 读取 HTML 内容
        
        # 提取主文本
        text = extract_main_text(html_content)                # 调用 HTML 提取函数
        if not text:
            continue                                          # 提取失败则跳过
        
        stats['extracted'] += 1                               # 统计提取成功数
        
        # 质量过滤
        if not is_quality_text(text):                         # 检查文本质量
            continue                                          # 低质量则跳过
        
        stats['quality_passed'] += 1                          # 统计通过质量检查的文档数
        
        # PII 脱敏
        text = redact_pii(text)                               # 移除个人隐私信息
        stats['pii_redacted'] += 1                            # 统计 PII 脱敏的文档数
        
        extracted_docs.append(text)                           # 添加到文档列表
    
    # 3️⃣ 大规模数据去重
    unique_docs, duplicates = lsh_deduplicate(
        extracted_docs,                                       # 输入的文档列表
        num_bands=20,                                         # Band 数量
        band_size=5,                                          # 每个 Band 的大小
        threshold=0.8                                         # Jaccard 相似度阈值
    )
    
    stats['deduplicated'] = len(unique_docs)                  # 统计去重后的文档数
    
    # 4️⃣ 保存到文件
    with open(output_file, 'w', encoding='utf-8') as f:       # 打开输出文件
        for doc in unique_docs:                               # 遍历每个去重后的文档
            f.write(doc + '\n\n')                             # 写入文档,用两个换行分隔
    
    return stats

5.3 性能优化策略 ⚡ / Performance Optimization

处理数百 TB 级别的数据需要高效策略:

  1. 并行处理 🚀

    • 使用多进程或多线程同时处理多个文件
    • 分布式计算框架(如 Apache Spark、Dask)
  2. 流式处理 💧

    • 不将所有数据加载到内存
    • 逐批次处理,实时写入
  3. 增量去重 🔄

    • 维护已处理文档的签名索引
    • 新文档到来时只与已有文档比较
  4. 检查点机制 💾

    • 定期保存中间结果
    • 失败后可从检查点恢复
python 复制代码
from multiprocessing import Pool                            # 导入多进程池模块 🚀

"""使用多进程并行处理文档

参数:
    documents: 文档列表,示例:["文档1", "文档2", ...]
    num_processes: 进程数(默认 4),示例:4 表示使用 4 个 CPU 核心并行处理
    
返回:
    processed_docs: 处理后的文档列表
    
示例:
    results = parallel_process(docs, num_processes=8)
"""
def parallel_process(documents, num_processes=4):
    # 创建进程池,自动分配任务到多个 CPU 核心
    with Pool(processes=num_processes) as pool:             # 创建进程池,示例:4 个进程
        # 使用 map 并行处理每个文档
        results = pool.map(process_single_doc, documents)   # process_single_doc 处理单个文档
    
    return [r for r in results if r is not None]            # 过滤掉处理失败的文档

def process_single_doc(doc):
    """处理单个文档的完整流程"""
    # 1. 质量检查
    if not is_quality_text(doc):
        return None
    
    # 2. PII 脱敏
    doc = redact_pii(doc)
    
    return doc

5.4 流水线监控 📊 / Pipeline Monitoring

python 复制代码
import time                                                   # 导入时间模块,用于计算处理时间 ⏱️

"""运行流水线并输出详细统计信息

参数:
    input_dir: 输入目录
    output_file: 输出文件
    
返回:
    无(打印统计信息到控制台)
    
示例:
    run_pipeline_with_stats("./raw/", "./output.txt")
"""
def run_pipeline_with_stats(input_dir, output_file):
    start_time = time.time()                                  # 记录开始时间
    
    print("=" * 60)                                          # 打印分隔线
    print("🚀 开始数据处理流水线")                            # 打印标题
    print("=" * 60)                                          # 打印分隔线
    
    stats = run_data_pipeline(input_dir, output_file)         # 运行流水线
    
    elapsed_time = time.time() - start_time                   # 计算总耗时(秒)
    
    print("\n📊 处理统计:")                                    # 打印统计标题
    print(f"  总文档数: {stats['total']:,}")                   # 打印总文档数,示例:1,000,000
    print(f"  成功提取: {stats['extracted']:,} ({stats['extracted']/stats['total']*100:.1f}%)")  # 打印提取成功数和比例
    print(f"  质量通过: {stats['quality_passed']:,} ({stats['quality_passed']/stats['total']*100:.1f}%)")  # 打印质量通过数
    print(f"  PII 脱敏: {stats['pii_redacted']:,}")           # 打印 PII 脱敏数
    print(f"  去重后: {stats['deduplicated']:,} ({stats['deduplicated']/stats['total']*100:.1f}%)")  # 打印去重后文档数
    print(f"  总耗时: {elapsed_time:.2f} 秒")                  # 打印总耗时
    print(f"  处理速度: {stats['total']/elapsed_time:.2f} 文档/秒")  # 计算并打印处理速度
    print("=" * 60)                                          # 打印分隔线

参考资料:


6. 总结 📝 / Summary

本章我们完成了 CS336 作业四的核心内容学习,回顾了从原始网页数据到高质量预训练数据集的完整流程:🎯

步骤 操作 关键技术
1️⃣ HTML 转纯文本 Trafilatura、BeautifulSoup、后处理清洗
2️⃣ 内容过滤 毒性分类器、启发式规则、质量指标
3️⃣ PII 脱敏 正则表达式匹配、模式替换
4️⃣ 数据去重 MinHash-LSH、Jaccard 相似度、n-gram
5️⃣ 流水线构建 多进程并行、流式处理、检查点机制

🔴 关键理解:

  • 💡 数据质量决定模型上限 ------ "Garbage In, Garbage Out",高质量数据是训练优秀模型的前提 🏆
  • MinHash-LSH 是去重利器 ------ 将 O(n²) 复杂度降低到接近 O(n),使大规模去重成为可能 🚀
  • 🛡️ 多层过滤必不可少 ------ 有害内容、低质量文本、PII 都必须严格过滤,确保数据安全合规 🔒
  • 🔄 流水线设计影响效率 ------ 并行处理、流式处理、增量更新是处理 TB 级数据的关键 🔑

最后更新时间:2026-06-23

相关推荐
oil欧哟1 小时前
Codex 最佳实践(超级长文):先搞懂 AI,再用好 AI
前端·人工智能·后端
甲维斯2 小时前
日本发布比肩Fable5的模型?Fugu Ultra初探!
人工智能·ai编程
雪隐2 小时前
个人电脑玩AI-04让5060 Ti给你打工——本地FLUX.2 Klein 的 AI 图片生成
人工智能·后端
腾讯云开发者2 小时前
腾讯云TVP走进香港数码港,解码AI出海新范式
人工智能
用户47949283569152 小时前
又当又立: Anthropic 这篇安全白皮书,为什么让人恶心
人工智能
Darling噜啦啦2 小时前
AI Loop 自迭代循环实战:让 AI 自动写文案直到完美——从 Prompt 工程到 Loop 工程
人工智能
vanuan2 小时前
MCP协议实战(Python版):让AI直接查你的数据库
人工智能
Vuhao3 小时前
为什么同样的问题,别人的AI回答质量高40%?
人工智能
Vuhao3 小时前
如何创造自己的工作流
人工智能