训练数据:大模型能力的真正来源

“Garbage in, garbage out”——这句话在大模型领域体现得淋漓尽致。2026 年的研究表明,数据质量对模型性能的影响超过了参数量和计算量。本文系统解析从原始网页数据到高质量训练语料的完整清洗流程。

一、原始数据来源

1.1 数据源概览

数据源规模质量获取方式
Common Crawl250B+ 网页低-中公开免费
GitHub100TB+ 代码中-高API + 镜像
arXiv4M+ 论文公开 API
Wikipedia60M+ 文章公开数据集
PubMed35M+ 摘要公开 API
Stack Overflow50M+ 问答中-高数据转储
LibreText200K+ 教材公开
领域特定变化授权/采集

1.2 Common Crawl 的挑战

Common Crawl 是最大的公开网页数据,但质量参差不齐:

原始 Common Crawl 内容分布:
┌──────────────────────────────────────────┐
│  垃圾/广告内容     35%  │█████████████  │
│  低质量文本        25%  │█████████      │
│  重复内容          15%  │█████          │
│  非目标语言         8%  │██             │
│  有害内容           5%  │█              │
│  ──────────────────────                  │
│  高质量文本        12%  │████           │
│  ──────────────────────                  │
│  保留率: ~12%                             │
└──────────────────────────────────────────┘

从 250B 网页中清洗后,通常只保留约 5-15% 的高质量内容。

二、数据清洗流水线

2.1 总体流程

原始数据 → 1.格式提取 → 2.语言识别 → 3.去重 → 4.质量过滤 → 5.安全过滤 → 6.数据配比 → 训练语料

2.2 格式提取

从 HTML 中提取正文内容:

def extract_text(html):
    # 1. 移除脚本和样式
    soup = BeautifulSoup(html, 'html.parser')
    for tag in soup(['script', 'style', 'nav', 'footer', 'header']):
        tag.decompose()
    
    # 2. 提取正文
    text = soup.get_text(separator='\n')
    
    # 3. 清理空白
    lines = [line.strip() for line in text.split('\n')]
    text = '\n'.join(line for line in lines if line)
    
    # 4. 移除 boilerplate (模板文字)
    text = remove_boilerplate(text)
    
    return text

2.3 语言识别

使用 fastText 或 CLD3 进行语言分类:

import fasttext

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

def identify_language(text, threshold=0.5):
    predictions = model.predict(text, k=1)
    lang, confidence = predictions[0][0].replace('__label__', ''), predictions[1][0]
    
    if confidence < threshold:
        return 'unknown'
    return lang

# 保留目标语言
target_langs = {'zh', 'en', 'ja', 'ko', 'fr', 'de', 'es', 'ru', 'ar'}

2.4 去重(Deduplication)

去重是数据清洗中最重要的步骤,分为三个层级:

2.4.1 精确去重(Exact Deduplication)

使用哈希值检测完全相同的文档:

def exact_dedup(documents):
    seen_hashes = set()
    unique_docs = []
    
    for doc in documents:
        h = hashlib.md5(doc.encode()).hexdigest()
        if h not in seen_hashes:
            seen_hashes.add(h)
            unique_docs.append(doc)
    
    return unique_docs

2.4.2 模糊去重(Fuzzy Deduplication)

使用 MinHash + LSH 检测高度相似但不完全相同的文档:

from datasketch import MinHash, MinHashLSH

def fuzzy_dedup(documents, threshold=0.8, num_perm=128):
    lsh = MinHashLSH(threshold=threshold, num_perm=num_perm)
    
    for i, doc in enumerate(documents):
        m = MinHash(num_perm=num_perm)
        for word in doc.split():
            m.update(word.encode('utf-8'))
        lsh.insert(i, m)
    
    # 保留每组相似文档中的一个
    seen = set()
    unique_docs = []
    
    for i, doc in enumerate(documents):
        if i in seen:
            continue
        
        m = MinHash(num_perm=num_perm)
        for word in doc.split():
            m.update(word.encode('utf-8'))
        
        similar = lsh.query(m)
        for j in similar[1:]:  # 跳过自己
            seen.add(j)
        
        unique_docs.append(doc)
    
    return unique_docs

2.4.3 段落级去重

文档级去重不够——同一段落可能出现在不同文档中:

def paragraph_dedup(documents, threshold=0.9):
    para_lsh = MinHashLSH(threshold=threshold, num_perm=64)
    para_map = {}  # paragraph hash -> first occurrence
    
    for doc_id, doc in enumerate(documents):
        paragraphs = doc.split('\n\n')
        clean_paragraphs = []
        
        for para in paragraphs:
            if len(para) < 50:  # 太短的段落跳过
                clean_paragraphs.append(para)
                continue
            
            m = MinHash(num_perm=64)
            for word in para.split():
                m.update(word.encode('utf-8'))
            
            similar = para_lsh.query(m)
            if not similar:
                para_lsh.insert(f"{doc_id}_{len(clean_paragraphs)}", m)
                clean_paragraphs.append(para)
            # else: 跳过重复段落
        
        documents[doc_id] = '\n\n'.join(clean_paragraphs)
    
    return documents

去重效果:

去重层级数据保留率重复率
原始100%~45%
精确去重后75%~25%
模糊去重后55%~8%
段落级去重后45%~3%

2.5 质量过滤

2.5.1 启发式规则过滤

def heuristic_filter(text):
    # 1. 长度过滤
    if len(text) < 200 or len(text) > 100000:
        return False
    
    # 2. 语言模型困惑度 (太低=模板, 太高=乱码)
    ppl = compute_perplexity(text)
    if ppl < 5 or ppl > 500:
        return False
    
    # 3. 符号比例
    symbol_ratio = count_symbols(text) / len(text)
    if symbol_ratio > 0.1:
        return False
    
    # 4. 平均行长度
    avg_line_len = len(text) / text.count('\n')
    if avg_line_len < 10 or avg_line_len > 500:
        return False
    
    # 5. 重复行比例
    lines = text.split('\n')
    unique_ratio = len(set(lines)) / len(lines)
    if unique_ratio < 0.5:
        return False
    
    return True

2.5.2 模型质量打分

使用小模型(如 fasttext 分类器)对文本质量打分:

class QualityScorer:
    def __init__(self):
        # 用 Wikipedia + 高质量数据训练的分类器
        self.classifier = fasttext.load_model('quality_classifier.bin')
    
    def score(self, text):
        result = self.classifier.predict(text[:512])
        label, confidence = result[0][0], result[1][0]
        return confidence if label == '__label__high' else 1 - confidence
    
    def filter(self, documents, threshold=0.5):
        return [doc for doc in documents if self.score(doc) > threshold]

2.5.3 LLM 质量评估

2026 年的新方法:使用 LLM 对数据进行质量评估:

def llm_quality_filter(text, judge_model):
    prompt = f"""Rate the following text on a scale of 1-10 for:
    - Information density
    - Factual accuracy
    - Writing quality
    - Educational value
    
    Text: {text[:1000]}
    
    Output only the scores as JSON."""
    
    response = judge_model.generate(prompt)
    scores = json.loads(response)
    avg_score = sum(scores.values()) / len(scores)
    
    return avg_score >= 6.0

2.6 安全过滤

class SafetyFilter:
    def __init__(self):
        self.pii_patterns = [
            r'\b\d{3}-\d{2}-\d{4}\b',  # SSN
            r'\b\d{16}\b',  # Credit card
            r'\b[\w.-]+@[\w.-]+\.\w+\b',  # Email
            r'\b\d{11}\b',  # Phone number
        ]
        self.toxic_classifier = load_toxic_classifier()
    
    def filter(self, text):
        # 1. PII 脱敏
        for pattern in self.pii_patterns:
            text = re.sub(pattern, '[REDACTED]', text)
        
        # 2. 有害内容过滤
        toxicity = self.toxic_classifier.predict(text)
        if toxicity > 0.7:
            return None
        
        # 3. 版权内容检测
        if self.is_copyrighted(text):
            return None
        
        return text

三、数据配比

3.1 配比对模型能力的影响

配比方案MMLU代码能力中文能力
60%网页+20%代码+15%学术+5%其他82.171.378.5
40%网页+30%代码+20%数学+10%其他88.582.176.2
30%网页+25%代码+25%学术+20%多语言86.878.585.3

数据配比直接决定了模型在不同能力上的表现。

3.2 动态配比策略

2026 年的最佳实践是动态配比——在训练过程中调整数据分布:

class DynamicDataMixer:
    def __init__(self, total_steps=300000):
        self.total_steps = total_steps
        self.schedule = {
            # (start_step, end_step): {source: ratio}
            (0, 50000): {  # 阶段1: 基础语言能力
                "web": 0.50, "code": 0.20, "academic": 0.15, "book": 0.15
            },
            (50000, 150000): {  # 阶段2: 增加代码和数学
                "web": 0.35, "code": 0.30, "academic": 0.20, "book": 0.15
            },
            (150000, 300000): {  # 阶段3: 高质量数据精修
                "web": 0.25, "code": 0.25, "academic": 0.25, "book": 0.15, "synthetic": 0.10
            }
        }
    
    def get_distribution(self, current_step):
        for (start, end), dist in self.schedule.items():
            if start <= current_step < end:
                return dist
        return self.schedule[(0, 50000)]

四、代码数据清洗

代码数据需要特殊处理:

class CodeDataCleaner:
    def clean(self, repo_name, files):
        cleaned = []
        for file_path, content in files:
            # 1. 过滤自动生成文件
            if self.is_auto_generated(content):
                continue
            
            # 2. 过滤大文件 (如 minified JS)
            if len(content) / len(content.split('\n')) > 500:
                continue
            
            # 3. 移除注释中的个人信息
            content = self.remove_pii_from_comments(content)
            
            # 4. 质量打分 (基于 GitHub stars, commit 数等)
            quality_score = self.score_repo_quality(repo_name)
            if quality_score < 0.3:
                continue
            
            # 5. 保留有意义的代码结构
            if self.has_meaningful_structure(content):
                cleaned.append((file_path, content))
        
        return cleaned

五、合成数据生成

5.1 合成数据的角色

2026 年,合成数据在训练数据中的占比逐渐增加:

数据类型2023202420252026
真实数据95%85%70%50%
合成数据5%15%30%50%

5.2 合成数据的质量控制

class SyntheticDataPipeline:
    def generate_and_verify(self, topic, n_samples):
        # 1. 生成
        samples = self.teacher.generate_batch(topic, n=n_samples * 2)
        
        # 2. 过滤低质量
        filtered = [s for s in samples if self.quality_scorer(s) > 0.7]
        
        # 3. 去重
        unique = self.dedup(filtered)
        
        # 4. 验证正确性 (对数学/代码)
        verified = [s for s in unique if self.verify_correctness(s)]
        
        # 5. 多样性筛选
        diverse = self.diversity_filter(verified, target=n_samples)
        
        return diverse

六、总结

训练数据清洗是大模型工程中最被低估的环节:

  1. 去重是最重要的步骤——减少 50%+ 的冗余数据
  2. 质量过滤决定模型上限——“质量胜于数量”
  3. 数据配比决定能力分布——需要根据目标调整
  4. 合成数据是未来方向——2026 年占比已达 50%
  5. 安全过滤不可忽视——PII、有害内容、版权

好的数据是好的模型的基础。在 Scaling Laws 逐渐饱和的时代,数据质量是新的 Scaling 方向。

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。