数据构建深度解析:LLM 训练数据的艺术与科学
7815 字
39 分钟
数据构建深度解析:LLM 训练数据的艺术与科学
1. 引言:数据是 LLM 的灵魂
1.1 数据决定模型上限
在 LLM 领域,有一个被反复验证的铁律:
“Garbage in, garbage out.” — 数据的质量决定了模型能力的上限,训练只是无限逼近这个上限。
数据 vs 模型能力的相关性:
GPT-3 (300B tokens) → GPT-4 (?) → 能力跃升的核心原因之一是数据质量的提升 → 不仅仅是参数规模的扩大
LLaMA 2 (2T tokens) vs LLaMA 3 (15T tokens) → LLaMA 3 用更多但更高质量的数据 → 在更少参数下超越了 LLaMA 2
Phi-3 (3.8B 参数) → 仅用 3.3T tokens 训练 → 超越大它 10 倍的模型 → 秘诀:高质量"教科书级"数据1.2 LLM 数据的三大类型
LLM 训练涉及三类核心数据,它们的角色和构建方法截然不同:
┌─────────────────────────────────────────────────────────────────┐│ LLM 训练数据的三种类型 │├─────────────────────────────────────────────────────────────────┤│ ││ 类型 1: 预训练数据 (Pre-training Data) ││ 用途: 学习语言模型的基础能力(语法、语义、世界知识) ││ 规模: trillions of tokens(数万亿) ││ 来源: 网页、书籍、代码、论文、百科等 ││ 构建: 爬取 → 去重 → 质量过滤 → 格式转换 ││ ││ 类型 2: SFT 数据 (Supervised Fine-Tuning Data) ││ 用途: 学习指令遵循、对话格式、特定任务能力 ││ 规模: thousands to millions(数千到数百万) ││ 来源: 人类标注、LLM 生成、公开数据集 ││ 构建: 指令设计 → 回答生成 → 质量筛选 ││ ││ 类型 3: 偏好数据 (Preference Data) ││ 用途: 学习人类偏好(RLHF/DPO/KTO 对齐) ││ 规模: thousands to hundreds of thousands(数千到数十万) ││ 来源: 人类标注、LLM 评判 ││ 构建: 候选生成 → 偏好标注 → 质量过滤 ││ │└─────────────────────────────────────────────────────────────────┘1.3 本系列文章关联
| 文章 | 关联 |
|---|---|
| 后训练深度解析 | 数据在后训练流水线中的使用方式 |
| 偏好对齐深度解析 | 偏好数据的具体应用 |
| 知识蒸馏深度解析 | 蒸馏数据的特殊构建方法 |
| DeepSeek-R1 | R1 的数据构建实践 |
2. 预训练数据构建
2.1 数据来源
预训练数据的来源决定了模型的知识面和能力边界:
class PretrainingDataSources: """ 预训练数据来源及其特点 """
SOURCES = { # 网页数据:最大、最多样、但噪声最多 "web": { "examples": ["CommonCrawl", "WebText", "CCNet", "C4"], "size": "~trillions of tokens", "pros": ["多样性高", "覆盖广", "成本相对低"], "cons": ["噪声多", "隐私问题", "质量参差不齐"], },
# 书籍:高质量、连贯、但覆盖有限 "books": { "examples": ["BookCorpus", "Books3", "Gutenberg", "Libgen"], "size": "~hundreds of billions", "pros": ["质量高", "长程依赖", "结构化"], "cons": ["版权问题", "覆盖有限", "可能过时"], },
# 代码:GPT-4/Codex 能力的关键 "code": { "examples": ["GitHub", "The Stack", "Python data"], "size": "~hundreds of billions", "pros": ["结构化", "逻辑性强", "可执行验证"], "cons": ["语言有限", "注释质量差", "版权问题"], },
# 学术论文:知识和推理能力 "papers": { "examples": ["arXiv", "Semantic Scholar"], "size": "~hundreds of billions", "pros": ["知识密集", "推理严谨", "引用丰富"], "cons": ["格式复杂", "数学公式多", "需要处理"], },
# 百科:结构化知识 "encyclopedia": { "examples": ["Wikipedia", "Wikidata"], "size": "~billions", "pros": ["知识准确", "结构化", "引用丰富"], "cons": ["覆盖面有限", "可能有偏见"], }, }2.2 网页数据流水线:CCNet
CommonCrawl 是预训练语料的最大来源,CCNet 是处理它的经典流水线:
class CCNetPipeline: """ CCNet: CommonCrawl Net
处理 CommonCrawl 网页数据的完整流水线 论文: Wenzek et al., 2020 """
def __init__(self): self.language = "en" self.min_quality_score = 0.7 self.min_length = 100
def process_common_crawl(self, raw_warc_files): """ 完整流水线: 1. 下载 CommonCrawl WARC 文件 2. 提取文本(Text Extraction) 3. 语言识别(Language Identification) 4. 质量过滤(Quality Filtering) 5. 去重(Deduplication) """ all_documents = []
for warc_file in tqdm(raw_warc_files): # Step 1: 下载并解压 WARC 文件 warc_records = self.parse_warc(warc_file)
# Step 2: 文本提取 for record in warc_records: text = self.extract_text(record)
# Step 3: 语言识别 if self.is_target_language(text, self.language): all_documents.append(text)
# Step 4: 质量过滤 filtered = self.quality_filter(all_documents)
# Step 5: 去重 deduped = self.deduplicate(filtered)
return deduped
def extract_text(self, warc_record): """ 从 HTML 中提取文本
使用 jusText 或类似的启发式提取器 移除导航栏、页脚、广告等噪音内容 """ html = warc_record.content
# 方法 1: jusText # 基于马尔可夫链的文本提取 # 区分"正文内容"和"模板内容"
# 方法 2: trafilatura # 更准确的提取,保留表格和列表
# 方法 3: boilerplate removal # 基于规则的噪音移除
text = trafilatura.extract(html)
return text
def is_target_language(self, text, target_lang): """ 语言识别
使用 fastText 的 LID (Language Identification) 模型 在 Wikipedia 上预训练,支持 176 种语言 """ # 加载预训练的 fastText 模型 lid_model = fasttext.load_model('lid.176.bin')
# 预测语言和置信度 predictions = lid_model.predict(text, k=1)
lang = predictions[0][0].replace('__label__', '') confidence = predictions[1][0]
return lang == target_lang and confidence > 0.8
def quality_filter(self, documents): """ 质量过滤
使用多个质量指标组合: 1. 语言模型困惑度(LM Perplexity) 2. 单词/字符比率 3. 特殊字符密度 4. 停用词比率 """ scored_docs = []
for doc in documents: score = 0.0
# 指标 1: 句子完整度 # 好的文档应该有完整的句子 sentences = sent_tokenize(doc) complete_ratio = sum(1 for s in sentences if s.endswith(('.','!','?'))) / len(sentences) score += 0.3 * complete_ratio
# 指标 2: 词/字符比率 # 合理范围:4-10 chars/word words = doc.split() if len(words) > 0: avg_word_len = len(doc) / len(words) if 3 < avg_word_len < 15: score += 0.2 else: score += 0.1 * (10 - abs(avg_word_len - 6)) / 10
# 指标 3: 停用词比率 stopwords = set(stopwords_list) stopword_ratio = sum(1 for w in words if w.lower() in stopwords) / len(words) if 0.3 < stopword_ratio < 0.7: score += 0.2
# 指标 4: 重复度 # 高重复度可能是模板/垃圾内容 repeat_ratio = self.compute_repeat_ratio(doc) if repeat_ratio < 0.2: score += 0.3 * (1 - repeat_ratio)
if score >= self.min_quality_score: scored_docs.append((score, doc))
# 按分数排序 scored_docs.sort(reverse=True)
return [doc for _, doc in scored_docs]
def compute_repeat_ratio(self, text, n=5): """ 计算 n-gram 重复比率
重复比率 = 1 - (唯一 n-gram 数 / 总 n-gram 数) """ words = text.split() if len(words) < n: return 0.0
ngrams = [tuple(words[i:i+n]) for i in range(len(words)-n+1)] unique_ngrams = len(set(ngrams)) total_ngrams = len(ngrams)
return 1 - unique_ngrams / total_ngrams2.3 RefineWeb: 高质量网页数据
RefineWeb(Faoorosh et al., 2023)提出了更精细的过滤策略:
class RefineWebPipeline: """ RefineWeb: Refined Web Dataset
相比 CCNet 的改进: 1. 更激进的模板过滤 2. 使用更强的质量分类器 3. 保留文档级去重 """
def __init__(self): self.quality_classifier = None # 用高质量数据训练的分类器
def build_quality_classifier(self, positive_samples, negative_samples): """ 训练质量分类器
positive: Wikipedia, Books, 人工筛选的高质量网页 negative: 广告、导航栏、低质量网页 """ from sklearn.linear_model import LogisticRegression
def extract_features(text): """提取质量相关特征""" features = {}
# 词汇丰富度 words = text.split() unique_words = set(words) features['vocab_richness'] = len(unique_words) / (len(words) + 1)
# 平均句子长度 sentences = sent_tokenize(text) features['avg_sent_len'] = np.mean([len(s.split()) for s in sentences])
# 标点符号密度 punct_count = sum(1 for c in text if c in '.,!?;:') features['punct_density'] = punct_count / (len(text) + 1)
# 大写字母比率 upper_count = sum(1 for c in text if c.isupper()) features['upper_ratio'] = upper_count / (len(text) + 1)
# URL 密度(低质量内容 URL 多) url_count = len(re.findall(r'http[s]?://', text)) features['url_density'] = url_count / (len(text) + 1)
return list(features.values())
# 训练 X_pos = [extract_features(s) for s in positive_samples] X_neg = [extract_features(s) for s in negative_samples]
X = X_pos + X_neg y = [1] * len(X_pos) + [0] * len(X_neg)
self.quality_classifier = LogisticRegression().fit(X, y)
def heuristic_filters(self, doc): """ 启发式过滤器 RefineWeb 使用的 10 个启发式规则 """ words = doc.split()
# 规则 1: 最小长度 if len(words) < 50: return False
# 规则 2: 最大长度(避免文档过长) if len(words) > 100000: return False
# 规则 3: 最小平均词长 avg_word_len = len(doc) / len(words) if avg_word_len < 2.5 or avg_word_len > 15: return False
# 规则 4: 最大特殊字符密度 special_chars = sum(1 for c in doc if not c.isalnum() and not c.isspace()) if special_chars / len(doc) > 0.3: return False
# 规则 5: 最小句子数 sentences = sent_tokenize(doc) if len(sentences) < 3: return False
# 规则 6: 检查"段落"结构 # 好的文档应该有多个段落 paragraphs = doc.split('\n\n') if len(paragraphs) < 2: return False
# 规则 7: 检查"锚文本"密度 # 锚文本(链接文字)过多可能是导航/列表页 anchor_ratio = doc.count('<a ') / (len(words) + 1) if anchor_ratio > 0.3: return False
# 规则 8: 检查"call to action" cta_patterns = ['click here', 'sign up', 'subscribe', 'buy now', 'learn more'] if any(p in doc.lower() for p in cta_patterns): return False
# 规则 9: 检查"contact us" if 'contact' in doc.lower() and 'email' in doc.lower(): return False # 可能是联系页面
# 规则 10: 检查"terms of service" if 'terms of service' in doc.lower() or 'privacy policy' in doc.lower(): return False # 法律页面
return True2.4 去重策略
去重是预训练数据处理中最关键的步骤之一:
class DeduplicationStrategies: """ 多层次去重策略 """
def exact_deduplication(self, documents): """ 精确匹配去重(MinHash)
时间复杂度: O(n) 空间复杂度: O(n) """ from datasketch import MinHash, MinHashEncoder
minhashes = [] for doc in documents: m = MinHash(num_perm=128) for word in doc.split(): m.update(word.encode('utf8')) minhashes.append(m)
# 编码并去重 encoder = MinHashEncoder(num_perm=128) encoded = [encoder.encode(m) for m in minhashes]
unique_idx = [] seen = set() for i, e in enumerate(encoded): if e not in seen: seen.add(e) unique_idx.append(i)
return [documents[i] for i in unique_idx]
def near_deduplication(self, documents, threshold=0.8): """ 近似去重(SimHash 或 MinHash Jaccard)
用于检测高度相似的文档 threshold: Jaccard 相似度阈值 """ from datasketch import MinHash, MinHashLSH
# 构建 MinHash 索引 lsh = MinHashLSH(threshold=threshold, num_perm=128)
for i, doc in enumerate(documents): m = MinHash(num_perm=128) for word in doc.split(): m.update(word.encode('utf8')) lsh.insert(f"doc_{i}", m)
# 查找并移除重复 unique_docs = [] duplicate_indices = set()
for i, doc in enumerate(documents): if i in duplicate_indices: continue
unique_docs.append(doc)
# 查找相似文档 m = MinHash(num_perm=128) for word in doc.split(): m.update(word.encode('utf8'))
duplicates = lsh.query(m) for dup_id in duplicates: dup_idx = int(dup_id.split('_')[1]) if dup_idx != i: duplicate_indices.add(dup_idx)
return unique_docs
def sentence_deduplication(self, text, min_occurrence=10): """ 句子级去重
移除在整个语料中出现次数过多的句子 这些通常是模板文本或版权声明 """ from collections import Counter
sentences = sent_tokenize(text)
# 统计句子出现次数 sentence_counts = Counter(sentences)
# 过滤出现过多的句子 filtered = [ s for s in sentences if sentence_counts[s] <= min_occurrence ]
return ' '.join(filtered)
def document_level_deduplication(self, documents): """ 文档级去重(基于内容哈希)
方法: 1. 提取文档的 k-mer 2. 计算 MinHash 3. 用 LSH 找相似文档 4. 保留最长/质量最高的版本 """ from datasketch import MinHash, MinHashLSH
lsh = MinHashLSH(threshold=0.9, num_perm=256)
# 构建索引 indexed = [] for i, doc in enumerate(documents): m = self._compute_minhash(doc) key = f"doc_{i}" lsh.insert(key, m) indexed.append((i, doc, m))
# 聚类并保留代表 clusters = defaultdict(list)
for i, doc, m in indexed: # 查找相似文档 candidates = lsh.query(m) if candidates: cluster_id = int(candidates[0].split('_')[1]) clusters[cluster_id].append((i, doc)) else: clusters[i].append((i, doc))
# 从每个 cluster 选代表(最长/最高质量) representatives = [] for cluster in clusters.values(): best = max(cluster, key=lambda x: len(x[1].split())) representatives.append(best[1])
return representatives
def _compute_minhash(self, text, k=5): """ 计算文本的 MinHash """ from datasketch import MinHash
words = text.split() m = MinHash(num_perm=256)
for i in range(len(words) - k + 1): kmer = ' '.join(words[i:i+k]) m.update(kmer.encode('utf8'))
return m3. SFT 数据构建
3.1 SFT 数据的核心要素
SFT 数据与预训练数据有本质区别:它需要包含**指令(Input)和回答(Output)**的配对。
class SFTDataCore: """ SFT 数据的核心要素 """
@dataclass class SFTSample: """ SFT 样本的三个核心要素 """ instruction: str # 指令/问题/任务描述 input: str # 可选的输入上下文 output: str # 期望的回答
def format(self, template="chatml"): """ 格式化为训练格式 """ if template == "chatml": return f"<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n<|im_start|>user\n{self.instruction}\n{self.input}<|im_end|>\n<|im_start|>assistant\n{self.output}<|im_end|>" elif template == "alpaca": return f"Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.\n\n### Instruction:\n{self.instruction}\n\n### Input:\n{self.input}\n\n### Response:\n{self.output}" elif template == "llama3": return f"<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\nYou are a helpful assistant.<|eot_id|>\n<|start_header_id|>user<|end_header_id|>\n\n{self.instruction}\n{self.input}<|eot_id|>\n<|start_header_id|>assistant<|end_header_id|>\n\n{self.output}<|eot_id|>"3.2 指令模板设计
指令模板的设计直接影响模型学到的能力:
class InstructionTemplates: """ 指令模板设计 """
TEMPLATES = { # 开放式问答 "qa_open": [ "{instruction}", "请回答以下问题:{instruction}", "Question: {instruction}\nAnswer:", ],
# 带上下文的问答 "qa_context": [ "根据以下信息回答问题:\n{input}\n\n问题:{instruction}", "Context: {input}\n\nQuestion: {instruction}\n\nAnswer:", ],
# 总结任务 "summarize": [ "请总结以下文本:\n{input}", "Summarize this: {input}", ],
# 翻译任务 "translate": [ "请将以下文本翻译成{target_lang}:\n{input}", "Translate to {target_lang}: {input}", ],
# 代码任务 "code": [ "请用{language}编写代码:\n{instruction}", "Write a {language} function that {instruction}", ],
# 数学任务 "math": [ "求解:{instruction}", "Calculate: {instruction}", "Given that {input}, {instruction}", ],
# 分析任务 "analysis": [ "分析以下内容:\n{input}", "请分析{instruction},参考:\n{input}", ], }
def generate_instruction_variants(self, base_instruction, num_variants=5): """ 生成指令变体
使用 LLM 重写指令,增加多样性 """ prompt = f""" 请将以下指令改写为 {num_variants} 种不同的表述方式。 保持原意,但改变措辞和句式。
原始指令:{base_instruction}
改写要求: 1. 使用不同的开头词 2. 使用不同的句式结构 3. 保持相同的任务要求 4. 不要改变任务的核心目标 """
response = llm.generate(prompt) variants = extract_variants(response)
return variants3.3 LLM 生成 SFT 数据
class LLMDataGenerator: """ 使用 LLM 生成 SFT 数据 """
def __init__(self, generator_model, judge_model=None): self.generator = generator_model self.judge = judge_model
def generate_from_prompts(self, prompts, task_type="general"): """ 从提示生成 SFT 数据 """ data = []
for prompt in tqdm(prompts): response = self.generator.generate(prompt)
# 质量检查 if self.judge: quality = self.judge.score(prompt, response) if quality < 0.7: continue
data.append({ "instruction": self.extract_instruction(prompt), "input": self.extract_input(prompt), "output": response, "source": "llm_generated", })
return data
def self_generate(self, model, instructions, num_samples_per_inst=1): """ Self-Generation: 用模型自己生成回答
典型流程: 1. 准备多样化的指令集 2. 用模型生成多个回答(不同 temperature) 3. 过滤低质量回答 4. 得到 (instruction, response) 对 """ data = []
for instruction in tqdm(instructions): for _ in range(num_samples_per_inst): # 随机 temperature temp = random.choice([0.5, 0.7, 0.9, 1.1])
response = model.generate( instruction, temperature=temp, max_tokens=2048, do_sample=True, )
# 质量检查 if self._passes_quality_check(response): data.append({ "instruction": instruction, "input": "", "output": response, "temperature": temp, })
return data
def _passes_quality_check(self, response): """ 简单的质量检查 """ # 长度检查 if len(response.split()) < 20: return False
# 完整性检查 if not response.strip().endswith(('.', '!', '?', '"', "'")): # 不完整可能表示截断 if len(response) < 200: return False
# 重复检查 if self._has_excessive_repetition(response): return False
return True
def _has_excessive_repetition(self, text, n=3): """ 检查过度重复 """ words = text.lower().split() if len(words) < 10: return False
# 检查连续 n 个词是否重复 for i in range(len(words) - n * 2): window = ' '.join(words[i:i+n]) next_window = ' '.join(words[i+n:i+2*n]) if window == next_window: return True
return False3.4 人类标注流程
class HumanAnnotationWorkflow: """ 人类标注工作流 """
def __init__(self, annotation_platform): self.platform = annotation_platform
def setup_task(self, task_config): """ 配置标注任务 """ task = { "name": task_config["name"], "description": task_config["description"],
# 输入字段 "input_fields": [ {"name": "instruction", "type": "text", "required": True}, {"name": "context", "type": "text", "required": False}, ],
# 输出字段 "output_fields": [ {"name": "response", "type": "textarea", "required": True}, {"name": "quality_score", "type": "rating", "min": 1, "max": 5}, {"name": "issues", "type": "multiselect", "options": ["incomplete", "incorrect", "harmful", "off-topic"]}, ],
# 质量控制 "qualification_requirements": [ {"type": "approval_rate", "threshold": 0.85}, {"type": "completed_tasks", "threshold": 100}, ],
# 奖励设置 "reward": task_config.get("reward_per_task", 0.10), }
return self.platform.create_task(task)
def annotate_instructions(self, instructions, num_workers=10): """ 标注指令-回答对
典型流程: 1. 分发任务给标注者 2. 每个任务多人标注 3. 收集并合并结果 """ # 分配任务 tasks = self._distribute_tasks(instructions, num_workers)
# 收集结果 all_results = []
for worker_id, worker_tasks in tasks.items(): results = self.platform.get_worker_results(worker_id) all_results.extend(results)
# 合并重复任务(多人标注同一任务) merged = self._merge_annotations(all_results)
# 过滤低质量标注 filtered = self._filter_by_agreement(merged)
return filtered
def _distribute_tasks(self, instructions, num_workers): """ 分发任务,确保每个任务被多人标注 """ import random
tasks_per_worker = len(instructions) // num_workers
tasks = {} for i in range(num_workers): start = i * tasks_per_worker end = start + tasks_per_worker if i < num_workers - 1 else len(instructions) tasks[f"worker_{i}"] = instructions[start:end]
return tasks
def _merge_annotations(self, annotations): """ 合并同一任务的多个标注
策略: - 如果多数一致,选择多数结果 - 如果一致,选择质量最高的 """ from collections import defaultdict
by_instruction = defaultdict(list)
for ann in annotations: key = ann["instruction"] by_instruction[key].append(ann)
merged = []
for instruction, anns in by_instruction.items(): if len(anns) == 1: merged.append(anns[0]) continue
# 检查一致性 responses = [a["response"] for a in anns] response_counts = Counter(responses)
if response_counts.most_common(1)[0][1] >= len(anns) * 0.5: # 多数一致,选择多数结果 best_response = response_counts.most_common(1)[0][0] else: # 不一致,选择质量最高的 best_ann = max(anns, key=lambda a: a["quality_score"]) best_response = best_ann["response"]
merged.append({ "instruction": instruction, "output": best_response, "agreement": response_counts.most_common(1)[0][1] / len(anns), "num_annotations": len(anns), })
return merged
def _filter_by_agreement(self, annotations, min_agreement=0.5): """ 按一致性过滤 """ return [a for a in annotations if a.get("agreement", 1.0) >= min_agreement]4. 偏好数据构建
4.1 偏好数据的格式
偏好数据是对齐训练的核心,其基本格式为:
class PreferenceDataFormat: """ 偏好数据的三种格式 """
@dataclass class PreferencePair: """ 标准偏好对 """ prompt: str chosen: str # 被选中的回答 rejected: str # 被拒绝的回答
@dataclass class PreferenceTriple: """ 偏好三元组(包含多个候选) """ prompt: str responses: List[str] # 候选回答列表 ranking: List[int] # 排序(0 = 最好)
@dataclass class BinaryPreference: """ 二元偏好(正/负样本) """ prompt: str response: str label: bool # True = desirable, False = undesirable4.2 偏好标注流水线
class PreferenceAnnotationPipeline: """ 偏好标注流水线 """
def __init__(self, annotation_platform, judge_model=None): self.platform = annotation_platform self.judge = judge_model
def generate_candidates(self, prompts, generator, num_candidates=4): """ 为每个 prompt 生成多个候选回答 """ candidates_data = []
for prompt in tqdm(prompts): candidates = []
for temp in [0.3, 0.5, 0.7, 0.9]: response = generator.generate( prompt, temperature=temp, max_tokens=1024, ) candidates.append(response)
candidates_data.append({ "prompt": prompt, "candidates": candidates, })
return candidates_data
def annotation_task(self, candidates_data, num_annotators=3): """ 偏好标注任务
标注者需要: 1. 阅读 prompt 和多个候选回答 2. 选择最好的一个(或排序) 3. 标记理由(可选) """ task_config = { "name": "Preference Annotation", "instruction": """ 请阅读以下问题及其多个回答,选择最好的一个。
评分标准: - 准确性:回答是否正确 - 完整性:回答是否全面 - 清晰度:回答是否易于理解 - 有用性:回答是否有帮助
请为每个回答打分(1-5)并选出最佳。 """, "input_fields": [ {"name": "prompt", "type": "display"}, # 只读 {"name": "candidate_1", "type": "display"}, {"name": "candidate_2", "type": "display"}, {"name": "candidate_3", "type": "display"}, {"name": "candidate_4", "type": "display"}, ], "output_fields": [ {"name": "scores", "type": "rating_group", "count": 4, "min": 1, "max": 5}, {"name": "best", "type": "select", "options": ["1", "2", "3", "4"]}, {"name": "reason", "type": "textarea"}, ], }
return task_config
def build_preference_pairs(self, annotations): """ 从标注结果构建偏好对
策略:两两比较构建 pair 如果 A 得分 > B 得分,则 (prompt, A, B) 是一个正例 """ pairs = []
for item in annotations: prompt = item["prompt"] candidates = item["candidates"] scores = item["scores"] # [score_1, score_2, ...]
# 构建所有两两比较 for i in range(len(candidates)): for j in range(i+1, len(candidates)): if scores[i] > scores[j]: pairs.append({ "prompt": prompt, "chosen": candidates[i], "rejected": candidates[j], }) elif scores[j] > scores[i]: pairs.append({ "prompt": prompt, "chosen": candidates[j], "rejected": candidates[i], })
return pairs4.3 LLM-as-Judge
使用 LLM 作为评判者可以大幅降低标注成本:
class LLMAsJudge: """ LLM-as-Judge: 用 LLM 进行偏好判断 """
def __init__(self, judge_model): self.judge = judge_model
def judge_preference(self, prompt, response_a, response_b): """ 判断哪个回答更好 """ judge_prompt = f""" You are an expert evaluator. Given a question and two responses, determine which response is better.
Question: {prompt}
Response A: {response_a}
Response B: {response_b}
Consider: - Accuracy and correctness - Completeness and thoroughness - Clarity and readability - Helpfulness and relevance
Output your decision in JSON format: {{"winner": "A" or "B", "reason": "brief explanation"}} """
result = self.judge.generate(judge_prompt) parsed = json.loads(result)
return parsed
def score_and_rank(self, prompt, responses): """ 给多个回答打分并排序 """ judge_prompt = f""" Evaluate and rank the following responses to the question.
Question: {prompt}
{''.join([f'Response {i+1}: {r}\n\n' for i, r in enumerate(responses)])}
Score each response (1-10) and provide rankings. Output in JSON format: {{ "rankings": [3, 1, 2, 4], # Response indices in rank order "scores": [7.5, 9.0, 8.5, 6.0], "justifications": ["..."] }} """
result = self.judge.generate(judge_prompt) parsed = json.loads(result)
return parsed
def batch_judge_preferences(self, data, batch_size=8): """ 批量判断偏好(提高效率) """ results = []
for i in range(0, len(data), batch_size): batch = data[i:i+batch_size]
# 构建批量评判 prompt batch_prompt = self._build_batch_prompt(batch) batch_result = self.judge.generate(batch_prompt)
# 解析结果 parsed = self._parse_batch_result(batch_result, len(batch)) results.extend(parsed)
return results
def _build_batch_prompt(self, batch): """ 构建批量评判 prompt """ prompt = "Evaluate the following preference pairs:\n\n"
for i, item in enumerate(batch): prompt += f"--- Pair {i+1} ---\n" prompt += f"Question: {item['prompt']}\n" prompt += f"Response A: {item['response_a']}\n" prompt += f"Response B: {item['response_b']}\n\n"
prompt += "\nOutput JSON array of results:\n" prompt += '[{"pair_id": 1, "winner": "A", "reason": "..."}, ...]'
return prompt4.4 偏好数据质量控制
class PreferenceDataQualityControl: """ 偏好数据质量控制 """
def __init__(self): self.agreement_threshold = 0.6 self.min_annotators = 3
def compute_inter_annotator_agreement(self, annotations): """ 计算标注者一致性
方法:Fleiss' Kappa 或 Krippendorff's Alpha """ import numpy as np from scipy import stats
# 将评分转换为矩阵 # 行:样本 # 列:标注者 n_samples = len(set(a["sample_id"] for a in annotations)) n_annotators = self.min_annotators
ratings = np.zeros((n_samples, n_annotators))
for ann in annotations: sid = ann["sample_id"] aid = ann["annotator_id"] score = ann["score"] ratings[sid, aid] = score
# 计算 Fleiss' Kappa kappa = self._fleiss_kappa(ratings)
return kappa
def _fleiss_kappa(self, ratings): """ Fleiss' Kappa 计算 """ n_subjects, n_raters = ratings.shape
# 观测一致性 observed = self._compute_observed_agreement(ratings)
# 期望一致性(随机) expected = self._compute_expected_agreement(ratings)
kappa = (observed - expected) / (1 - expected)
return kappa
def filter_by_consistency(self, preference_pairs): """ 过滤不一致的偏好
检测方法: 1. A > B, B > C, 但 A < C(循环偏好) 2. 同一 prompt 多次标注结果差异大 """ from collections import defaultdict
# 按 prompt 分组 by_prompt = defaultdict(list) for pair in preference_pairs: by_prompt[pair["prompt"]].append(pair)
filtered = []
for prompt, pairs in by_prompt.items(): # 检查是否有循环偏好 if self._has_cycle_preference(pairs): continue # 跳过不一致的数据
# 检查标注一致性 consistency = self._compute_pairwise_consistency(pairs) if consistency >= self.agreement_threshold: filtered.extend(pairs)
return filtered
def _has_cycle_preference(self, pairs): """ 检测循环偏好 """ # 如果有 A > B > C > A 的循环,返回 True # 简化为:检查是否有传递性冲突 responses = {}
for pair in pairs: chosen = pair["chosen"] rejected = pair["rejected"]
if chosen not in responses: responses[chosen] = set() if rejected not in responses: responses[rejected] = set()
# chosen 优于 rejected responses[chosen].add(rejected)
# 检查传递性 for a in responses: for b in responses[a]: for c in responses[b]: if a in responses[c]: # a > b > c > a,存在循环 return True
return False
def filter_by_length_ratio(self, pairs, max_ratio=5.0): """ 过滤长度差异过大的偏好
理由:长度偏差可能影响人类判断 """ filtered = []
for pair in pairs: len_chosen = len(pair["chosen"].split()) len_rejected = len(pair["rejected"].split())
ratio = max(len_chosen, len_rejected) / (min(len_chosen, len_rejected) + 1)
if ratio <= max_ratio: filtered.append(pair)
return filtered5. 测试集设计
5.1 测试集的核心原则
class TestSetDesign: """ 测试集设计的核心原则 """
PRINCIPLES = { # 1. 不泄漏训练数据 "no_leakage": """ 测试集不应该出现在训练数据中。
检查方法: 1. 用 MinHash 检测与训练数据的重叠 2. 过滤重叠度 > 5% 的样本 3. 定期审计测试集 """,
# 2. 代表性 "representativeness": """ 测试集应该代表模型将面对的真实分布。
检查方法: 1. 与训练数据分布对比 2. 覆盖所有能力维度 3. 考虑真实使用场景 """,
# 3. 难度分布 "difficulty_distribution": """ 测试集应该有合理的难度分布。
建议分布: - Easy: 30% - Medium: 50% - Hard: 20%
理由: - 太难:无法区分模型能力 - 太简单:无法区分模型能力 """,
# 4. 抗污染 "contamination_resistance": """ 测试集应该能抵抗数据污染。
方法: 1. 使用闭卷测试(模型未见过的场景) 2. 使用需要推理的问题 3. 定期更新测试集 """, }5.2 能力维度覆盖
class CapabilityCoverage: """ 能力维度的测试覆盖 """
CAPABILITIES = { # 知识与问答 "knowledge": { "description": "知识问答和事实准确性", "tests": [ "Factual QA (TriviaQA, NaturalQuestions)", "World Knowledge (MMLU subjects)", "Reading Comprehension (SQuAD, HotpotQA)", ], "difficulty_levels": ["factual", "reasoning", "multi-hop"], },
# 推理能力 "reasoning": { "description": "逻辑和数学推理", "tests": [ "Math (GSM8K, MATH, MATH-500)", "Logic (LogiQA, ReClor)", "Commonsense (HellaSwag, PIQA)", "Code (HumanEval, MBPP, LiveCodeBench)", ], "difficulty_levels": ["single-step", "multi-step", "adversarial"], },
# 代码能力 "coding": { "description": "代码生成和理解", "tests": [ "Code Generation (HumanEval, MBPP)", "Code Completion (Fill-in-the-middle)", "Code Explanation (Code Understanding)", "Code Debugging (Bug Detection)", ], "difficulty_levels": ["simple", "medium", "complex"], },
# 对齐质量 "alignment": { "description": "指令遵循和偏好对齐", "tests": [ "Instruction Following (IFEval)", "Helpfulness (MT-Bench, AlpacaEval)", "Safety (ToxiGen, HarmBench)", "Honesty (TruthfulQA)", ], "difficulty_levels": ["basic", "complex", "adversarial"], },
# 多语言能力 "multilingual": { "description": "多语言理解和生成", "tests": [ "Translation (WMT, Flores)", "Multilingual QA (XQuAD, MLQA)", "Language Understanding (XGLUE)", ], "difficulty_levels": ["high-resource", "low-resource"], }, }5.3 测试集去污染
class ContaminationChecker: """ 测试集污染检测 """
def __init__(self, training_data): self.training_minhash = self._build_minhash_index(training_data)
def _build_minhash_index(self, documents): """ 为训练数据构建 MinHash 索引 """ from datasketch import MinHash, MinHashLSH
lsh = MinHashLSH(threshold=0.5, num_perm=128)
for i, doc in enumerate(documents): m = MinHash(num_perm=128) for word in doc.split()[:1000]: # 只用前1000词 m.update(word.encode('utf8')) lsh.insert(f"doc_{i}", m)
return lsh
def check_contamination(self, test_sample): """ 检查测试样本是否与训练数据重叠 """ m = MinHash(num_perm=128) for word in test_sample.split()[:1000]: m.update(word.encode('utf8'))
candidates = self.training_minhash.query(m)
return len(candidates) > 0, len(candidates)
def compute_overlap_score(self, test_sample): """ 计算与训练数据的重叠分数 """ test_words = set(test_sample.lower().split())
# 采样检查(完整检查太慢) overlap_count = 0 sample_size = min(len(test_words), 1000) sampled_words = random.sample(list(test_words), sample_size)
for word in sampled_words: if self._word_in_training(word): overlap_count += 1
return overlap_count / sample_size
def filter_contaminated(self, test_set, overlap_threshold=0.1): """ 过滤被污染的测试样本 """ clean_set = [] contaminated_count = 0
for sample in tqdm(test_set): overlap = self.compute_overlap_score(sample["text"])
if overlap < overlap_threshold: clean_set.append(sample) else: contaminated_count += 1
print(f"Filtered {contaminated_count}/{len(test_set)} contaminated samples")
return clean_set6. Scaling Law 与数据配比
6.1 数据 Scaling Law
class DataScalingLaw: """ 数据 Scaling Law
核心发现(Hoffmann et al., 2022): 计算最优:C ≈ 20 * N 其中 C 是 tokens 数量,N 是参数量
Chinchilla 结论: 模型大小和数据量应该同比 scaling """
def compute_optimal_tokens(self, model_params, total_compute): """ Chinchilla 最优:20 tokens per parameter
LLaMA 2 (70B) 应该用 1.4T tokens 实际用了 2T,已经超过 Chinchilla 最优 """ optimal_tokens = 20 * model_params
return optimal_tokens
def compute_optimal_model_size(self, total_tokens, total_compute): """ 给定 tokens 和 compute,估算最优模型大小 """ # total_compute ∝ N * T # 最优点:N ≈ C / 20
optimal_params = total_compute / 20
return optimal_params
def predict_performance(self, model_params, num_tokens, quality_factor=1.0): """ 预测模型性能(基于 scaling law)
损失 ~ (C / (N * T))^(alpha)
其中: - C 是计算量 - N 是参数量 - T 是 tokens - alpha ≈ 0.5 """ alpha = 0.5 quality_factor = quality_factor # 数据质量因子
compute = model_params * num_tokens
# 简化的损失预测 loss = (1.0 / compute) ** alpha * quality_factor
# 转换为困惑度 perplexity = np.exp(loss)
return perplexity6.2 数据配比策略
class DataMixingStrategy: """ 数据配比策略 """
def compute_optimal_mix( self, domain_importance, domain_difficulty, current_performance, target_capabilities, ): """ 计算最优数据配比
考虑因素: 1. 领域重要性(任务占比) 2. 领域难度(需要多少数据才能掌握) 3. 当前性能(短板优先) 4. 目标能力(重点发展的方向) """ mix = {}
for domain, importance in domain_importance.items(): difficulty = domain_difficulty.get(domain, 1.0) current = current_performance.get(domain, 0.0) target = target_capabilities.get(domain, 1.0)
# 基础权重 = 重要性 weight = importance
# 难度调整:越难需要越多数据 weight *= (1 + 0.5 * difficulty)
# 性能调整:短板优先 gap = target - current weight *= (1 + gap)
mix[domain] = weight
# 归一化 total = sum(mix.values()) mix = {k: v / total for k, v in mix.items()}
return mix
def expert_mixture(self, num_experts=8, expert_domain_size=100e9): """ 专家混合数据配比
DeepSeek-V2 的做法: 每个"专家"专注一个领域 """ mix = {}
domains = [ "code", # 代码 "math", # 数学 "reasoning", # 逻辑推理 "science", # 自然科学 " humanities", # 人文 "social", # 社会科学 "news", # 新闻 "web", # 网页 ]
# 基于能力目标分配 base_mix = { "code": 0.20, # 代码是关键 "math": 0.15, # 数学推理 "reasoning": 0.15, # 逻辑推理 "science": 0.15, # 自然科学 "humanities": 0.10, "social": 0.10, "news": 0.05, "web": 0.10, }
return base_mix
def progressive_mix(self, training_steps, total_steps): """ 渐进式数据混合
训练过程中动态调整数据配比 """ progress = training_steps / total_steps
if progress < 0.3: # 早期:更多通用数据 return { "general": 0.8, "domain": 0.2, } elif progress < 0.7: # 中期:增加领域数据 return { "general": 0.5, "domain": 0.5, } else: # 后期:领域数据为主 return { "general": 0.2, "domain": 0.8, }6.3 数据质量 vs 数量
class QualityVsQuantity: """ 数据质量 vs 数量的权衡 """
def analyze_tradeoff(self): """ 分析质量与数量的权衡
关键发现: 1. Phi-3: 3.3T 高质量 tokens ≈ 15T 混合质量 tokens 2. 数据质量提升 2x ≈ 数据量提升 10x 3. "Textbook quality" 数据最有效 """ return { "phi_insight": { "description": "Phi-3 的数据策略", "high_quality_tokens": 3.3e12, "equivalent_mixed_tokens": 15e12, "quality_premium": "5x", "key": "Textbook-level quality", },
"scaling_curve": { "x": "Data quality (normalized)", "y": "Model performance (normalized)", "shape": "log-linear", "slope": 0.5, # 质量翻倍 → 性能提升 2^0.5 ≈ 1.4x },
"practical_recommendation": { "high_budget": "Maximize quality, moderate quantity", "medium_budget": "Balance quality and quantity", "low_budget": "Prioritize quality in key domains", }, }7. 数据清洗工具链
7.1 完整清洗流水线
class DataCleaningPipeline: """ 完整数据清洗流水线 """
def __init__(self): self.stages = [ "deduplication", "language_filter", "quality_filter", "safety_filter", "format_filter", ]
def clean(self, documents, config=None): """ 完整清洗流水线 """ if config is None: config = self.default_config()
current = documents stats = {"input": len(documents)}
for stage in self.stages: current = getattr(self, f"stage_{stage}")(current, config[stage]) stats[stage] = len(current)
stats["output"] = len(current) stats["retention_rate"] = len(current) / stats["input"]
return current, stats
def default_config(self): """ 默认配置 """ return { "deduplication": { "threshold": 0.85, # MinHash Jaccard 阈值 "method": "minhash", }, "language_filter": { "target_languages": ["en", "zh", "es", "fr", "de", "ja", "ko"], "min_confidence": 0.8, }, "quality_filter": { "min_length": 100, "max_length": 100000, "min_sentence_count": 3, "quality_threshold": 0.5, }, "safety_filter": { "toxicity_threshold": 0.5, "pii_removal": True, }, "format_filter": { "remove_html": True, "remove_urls": False, "normalize_whitespace": True, }, }
def stage_deduplication(self, documents, config): """ 去重阶段 """ return self.near_deduplication(documents, threshold=config["threshold"])
def stage_language_filter(self, documents, config): """ 语言过滤阶段 """ from fasttext import load_model
lid_model = load_model('lid.176.bin')
filtered = [] for doc in documents: predictions = lid_model.predict(doc, k=1) lang = predictions[0][0].replace('__label__', '') confidence = predictions[1][0]
if lang in config["target_languages"] and confidence >= config["min_confidence"]: filtered.append(doc)
return filtered
def stage_quality_filter(self, documents, config): """ 质量过滤阶段 """ filtered = []
for doc in documents: words = doc.split()
# 长度检查 if len(words) < config["min_length"]: continue if len(words) > config["max_length"]: continue
# 句子数检查 sentences = sent_tokenize(doc) if len(sentences) < config["min_sentence_count"]: continue
# 质量评分 quality = self.compute_quality_score(doc) if quality < config["quality_threshold"]: continue
filtered.append(doc)
return filtered
def stage_safety_filter(self, documents, config): """ 安全过滤阶段 """ # 简化实现 toxic_patterns = [ "hate speech patterns", "violent content patterns", ]
filtered = [] for doc in documents: # 检查毒性 toxicity_score = self.estimate_toxicity(doc) if toxicity_score > config["toxicity_threshold"]: continue
# PII 移除 if config.get("pii_removal", False): doc = self.remove_pii(doc)
filtered.append(doc)
return filtered
def stage_format_filter(self, documents, config): """ 格式过滤阶段 """ cleaned = []
for doc in documents: if config.get("remove_html", True): doc = self.remove_html_tags(doc)
if config.get("remove_urls", True): doc = re.sub(r'http[s]?://\S+', '', doc)
if config.get("normalize_whitespace", True): doc = re.sub(r'\s+', ' ', doc)
cleaned.append(doc.strip())
return cleaned8. 主流数据集汇总
8.1 预训练数据集
| 数据集 | 规模 | 来源 | 特点 |
|---|---|---|---|
| CommonCrawl | ~数十 TB | 网页 | 最大、噪声多 |
| The Pile | 825 GB | 22 个来源混合 | 多样、学术导向 |
| RedPajama | 1.2T | C4 + Reddit + Wikipedia + etc. | 开源完整 |
| RefineWeb | 500B | 网页 | 高质量 |
| Dolma | 3T | 多种来源 | 最新、高质量 |
| FineWeb | 15T | 网页 | 高质量过滤 |
| Cosmopedia | 25B | Synthetic | 高质量教科书级 |
8.2 SFT 数据集
| 数据集 | 规模 | 来源 | 特点 |
|---|---|---|---|
| FLAN | 1.8M | 62 任务 | 多任务指令微调 |
| Alpaca | 52K | LLM 生成 | 简单、便宜 |
| Vicuna | 70K | LLM 对话 | 多轮对话 |
| WizardLM | 180K | LLM 进化 | 复杂指令 |
| ShareGPT | ~150K | 用户共享 | 真实对话 |
| OpenOrca | ~1M | LLM 生成 | CoT 推理 |
8.3 偏好数据集
| 数据集 | 规模 | 来源 | 特点 |
|---|---|---|---|
| HH-RLHF | 160K | 人类标注 | Helpfulness/Harmlessness |
| SHP | 385K | Reddit 投票 | 人类偏好 |
| PKU-Alignment | 340K | 人类标注 | 安全对齐 |
| Anthropic HH | 42K | 人类标注 | 对话偏好 |
| UltraFeedback | 64K | LLM 评判 | 高质量 |
9. 核心公式汇总
9.1 MinHash 去重
其中 是元素 的哈希函数,MinHash 是多个哈希函数的集合。
9.2 Jaccard 相似度
9.3 质量评分(简化)
其中 是完整性、 是词汇丰富度、 是重复度。
9.4 Chinchilla Scaling Law
其中 是参数量, 是数据量, 是损失。
10. 总结
10.1 数据构建的核心原则
┌─────────────────────────────────────────────────────────────┐│ 数据构建的五大核心原则 │├─────────────────────────────────────────────────────────────┤│ ││ 1. 质量第一 ││ → 高质量 3.3T tokens > 低质量 15T tokens ││ → Textbooks are all you need ││ → 投资在质量过滤上有最高的 ROI ││ ││ 2. 去重不可忽视 ││ → 去重可移除 20-40% 的重复数据 ││ → MinHash 是工业级去重的标准方法 ││ → 文档级 + 句子级去重组合使用 ││ ││ 3. 多样性很重要 ││ → 单一来源的数据会导致能力偏差 ││ → 平衡不同领域、风格、难度的数据 ││ → 渐进式调整数据配比 ││ ││ 4. 测试集要独立 ││ → 严格防止训练数据泄漏测试集 ││ → 定期审计和更新测试集 ││ → 使用闭卷测试验证真实能力 ││ ││ 5. 迭代改进 ││ → 训练实验 → 分析失败案例 → 针对性补充数据 ││ → 数据构建是一个持续的过程 ││ → 不要期望一次就做到完美 ││ │└─────────────────────────────────────────────────────────────┘10.2 方法选择指南
数据构建方法选择:
预训练数据: → 质量优先:RefineWeb 流水线 > CCNet → 开源首选:RedPajama-Data > 从零爬取 → 大规模:FineWeb / Dolma
SFT 数据: → 资源充足:人类标注 → 资源有限:LLM 生成 + 质量过滤 → 最佳平衡:混合(50% 人工 + 50% LLM)
偏好数据: → LLM-as-Judge 是降低成本的关键 → 多标注者一致性 > 标注数量 → 偏好对质量比数量更重要推荐阅读
- CCNet(Wenzek et al., 2020)—— 网页数据处理流水线
- RefineWeb(Faoorosh et al., 2023)—— 高质量网页数据构建
- The Pile(Gao et al., 2020)—— 多样化预训练语料
- Phi-3 数据(Microsoft, 2024)—— Textbooks are all you need
- HH-RLHF(Anthropic, 2022)—— 偏好数据标注
- LLM-as-Judge(Zheng et al., 2023)—— LLM 评判方法
参考资料
- Wenzek, G., et al. (2020). “CCNet: Extracting High Quality Monolingual Datasets from Web Crawl Data.” LREC.
- Faoorosh, G., et al. (2023). “RefineWeb: A Recipe for Efficient Data Cleaning.” arXiv.
- Gao, L., et al. (2020). “The Pile: An 800GB Dataset of Diverse Text for Language Modeling.” arXiv.
- Penedo, G., et al. (2023). “The RefinedWeb Dataset for Falcon LLM.” arXiv.
- Soldaini, L., et al. (2024). “Dolma: An Open Corpus of 3 Trillion Tokens for Language Model Pretraining.” arXiv.
- Labrak, Y., et al. (2024). “FineWeb: 15T tokens of high-quality web data.” arXiv.
- Azeret, A., et al. (2024). “Cosmopedia: Synthetic Data for Education.” arXiv.
- Taori, R., et al. (2023). “Alpaca: A Strong, Replicable Instruction-Following Model.” Stanford CRFM.
- Peng, B., et al. (2023). “Vicuna: An Open-S chatbot Impressing GPT-4 with 90%* ChatGPT Quality.” LMSYS.
- Xu, C., et al. (2023). “WizardLM: Empowering Large Language Models to Follow Complex Instructions.” ICLR.
- Bai, Y., et al. (2022). “Training a Helpful and Harmless Assistant with Reinforcement Learning from Human Feedback.” arXiv.
- Zheng, L., et al. (2023). “Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena.” NeurIPS.
- Hoffmann, J., et al. (2022). “Training Compute-Optimal Large Language Models.” NeurIPS.
- Touvron, H., et al. (2023). “LLaMA: Open and Efficient Foundation Language Models.” Meta AI.
- Team, G., et al. (2024). “Phi-3 Technical Report.” Microsoft.
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
数据构建深度解析:LLM 训练数据的艺术与科学
https://aiattnstudio.link/posts/data-construction/
