微调数据集准备与质量评估
*微调数据集准备完整流程 — 从原始数据到训练就绪*
微调数据集准备与质量评估
1. 为什么数据准备是微调中最重要的环节
微调数据集准备完整流程 — 从原始数据到训练就绪
很多人第一次做微调时,把大量时间花在选模型、调超参数、研究 LoRA 配置上,结果数据随便弄了一些就开始训练——然后发现效果惨不忍睹,比基础模型还差。
这是一个非常常见的错误。经验规律是:微调项目 60-70% 的工作量应该在数据准备上,而不是模型训练上。
原因很简单:微调的本质是监督学习(Supervised Learning),模型通过你的示例学习"输入→期望输出"的映射。如果你的示例本身有问题——格式不一致、输出质量低、覆盖不够、存在矛盾——模型就会忠实地学到这些问题,甚至把问题放大。
三个典型的数据问题:
格式不一致:同样是 JSON 输出,有些样本用双引号,有些用单引号;有些数字是字符串,有些是整数。模型学到的结论是"两种格式都对",于是在生产中随机选一种,导致格式错误率居高不下。
质量参差不齐:你用 GPT-4 生成了一批数据,然后为了凑数量,把 GPT-3.5 生成的那批质量稍差的数据也加进去了。模型会把"差一点的回答"学成"也是可以接受的",整体质量下滑。
矛盾示例:同一个类型的输入,有时候要求简短回答,有时候要求详细回答,两者混在一起。模型无法判断哪种是"正确"的,只能在两者之间随机跳动。
"100 条精心设计的数据往往超过 1000 条凑数的数据"——这不是夸张,而是实际经验。
数据质量决定微调质量。这句话听起来像废话,但实际工程中有大量的团队在数据上翻车:花了两周准备数据、一天跑完训练,结果模型表现比基础模型还差——原因往往就是数据质量不过关。
本章目标:让你能独立制作一批可以真正用于训练的高质量数据集。
2. 为什么数据质量对微调如此关键
微调的本质是监督学习(Supervised Learning):给模型展示"输入→期望输出"的范例,模型通过梯度下降调整参数,使得它的输出越来越接近这些范例。
这个学习机制意味着:模型会精确地学习你的示例,包括示例里的错误。
如果训练数据里有 10% 的格式不一致(比如同样是 JSON 输出,有些带空格,有些不带),模型就会以 10% 的概率随机决定是否加空格——它认为这两种格式都是"正确"的。如果数据里有两条相互矛盾的示例(同一种输入给了不同的期望输出),模型会陷入困惑,在这类输入上的表现会变得不稳定。
这就是为什么"100 条精心设计的数据往往超过 1000 条凑数的数据"。数量增加只是给模型更多样例参考,但如果这些样例本身有问题,增加数量反而会强化错误。
数据质量的三个关键维度:
- 一致性:相同类型的输入,期望输出的格式和风格必须完全一致。不一致是最常见的数据问题,也是最容易被忽视的。
- 代表性:训练数据要覆盖实际使用中可能出现的各种情况,包括边界情况。如果实际场景中有 20% 的输入是长文本,训练数据里长文本的比例也应该接近 20%。
- 准确性:每条数据的期望输出必须是真正"好"的回答,不能是"凑合能用"的回答。模型会把训练数据里的"凑合"当成标准。
3. 微调数据的格式
3.1. 指令微调格式(Alpaca格式)
最常见的格式,来自斯坦福的 Alpaca 项目(2023年斯坦福发布的开源指令微调数据集和训练方法,instruction/input/output 三字段结构被广泛沿用,成为指令微调的事实标准格式):
[
{
"instruction": "将以下中文翻译成英文",
"input": "今天天气很好",
"output": "The weather is very nice today."
},
{
"instruction": "用一句话总结以下文章的主要观点",
"input": "人工智能正在改变医疗诊断...(文章内容)",
"output": "AI技术正在通过提升诊断准确率和效率来革新医疗行业。"
}
]
三个字段的含义:
instruction:告诉模型"要做什么任务"input:任务处理的原始材料(可以为空字符串"")output:期望模型输出的高质量回答
3.2. 对话格式(Chat Format)
如果你的目标是训练一个对话助手,推荐使用多轮对话格式。这种格式也叫 ShareGPT 格式(因最早由 ShareGPT 网站收集的对话数据推广而来):
[
{
"messages": [
{"role": "system", "content": "你是一个Python专家助手,擅长用简单的语言解释编程概念。"},
{"role": "user", "content": "什么是装饰器?"},
{"role": "assistant", "content": "装饰器是Python中一种设计模式,允许你在不修改原函数代码的情况下,给函数添加额外功能。\n\n最简单的例子:\n```python\ndef log_decorator(func):\n def wrapper(*args, **kwargs):\n print(f'调用函数: {func.__name__}')\n return func(*args, **kwargs)\n return wrapper\n\n@log_decorator\ndef say_hello():\n print('Hello!')\n```\n\n调用 `say_hello()` 时会自动打印日志。"}
]
}
]
选哪种格式?
| 场景 | 推荐格式 |
|---|---|
| 文本分类、翻译、摘要等单轮任务 | 指令微调格式 |
| 客服机器人、问答助手等对话任务 | 对话格式 |
| 代码生成助手 | 对话格式(便于多轮澄清需求) |
4. 如何写好 instruction(最重要的技能)
这是本章最关键的内容,却是被大多数教程忽略的部分。
instruction 质量直接影响模型学到的行为。模型通过大量样本"感知"你想要它做什么。如果 instruction 写得含糊,模型就会学到含糊的行为模式。
理解这背后的学习机制:模型在训练时,会在 instruction 的表示(内部向量)和 output 的表示之间建立关联。如果 instruction 信息量不足(比如只是"回答用户问题"),模型无法从 instruction 中获取足够的约束信号,只能从 input 本身猜测应该怎么回答,泛化能力就会很差。反之,详细的 instruction 给了模型清晰的"任务规格",它就能学会在新输入上也按同样的规格输出。
4.1. 好的 instruction vs 坏的 instruction
示例1:客服回复场景
坏的 instruction:
"回答用户问题"
好的 instruction:
"你是一名电商客服,用户反映商品未按时到达。请以专业、同理心的态度回复,
说明可能的原因,并提供具体的解决方案(如查询物流、申请补偿)。
回复字数控制在100-200字。"
差距在哪里?坏的 instruction 太宽泛,模型不知道:是什么角色?回复要多长?什么语气?要不要给解决方案?
示例2:代码生成场景
坏的 instruction:
"写Python代码"
好的 instruction:
"用Python编写一个函数,实现以下功能:
1. 接受一个字符串列表作为输入
2. 移除列表中的重复元素,同时保持原有顺序
3. 函数需要有类型注解和docstring
4. 提供使用示例"
示例3:领域知识问答场景
坏的 instruction:
"解释这个医学概念"
好的 instruction:
"用通俗易懂的语言向没有医学背景的普通人解释以下医学概念。
避免使用专业术语,如需使用请加括号解释。回答结构清晰,
先给出定义,再举一个生活中的类比。"
4.2. instruction 质量检查清单
在制作数据集时,对每条 instruction 问自己以下问题:
- 角色清晰:模型知道自己扮演什么角色吗?
- 任务具体:任务的边界是否清楚?(输入是什么,输出是什么)
- 格式约束:对输出格式有要求吗?(字数、结构、语言风格)
- 质量标准:什么算好?(专业度、简洁性、完整性)
- 边界案例:覆盖了常见的边界情况吗?(空输入、超长输入等)
5. 数据量需求
| 任务类型 | 最少数据量 | 推荐数据量 | 说明 |
|---|---|---|---|
| 简单格式化任务 | 100-500条 | 1000-2000条 | 如固定模板回复、格式转换 |
| 领域知识注入 | 1000-5000条 | 5000-20000条 | 如医疗、法律专业问答 |
| 对话风格调整 | 500-1000条 | 2000-5000条 | 如语气、风格、角色扮演 |
| 复杂推理任务 | 5000-10000条 | 20000+条 | 如数学推理、代码调试 |
务实建议:
- 先用100-200条数据做一次"探索性微调",验证方向是否正确
- 确认方向对了,再花时间扩充到推荐量级
- 数据质量优先于数量:100条精心设计的数据往往超过1000条凑数的数据
6. 数据收集方法
6.1. 方法1:用GPT-4生成合成数据
对于大多数小团队,这是最高效的起点。
# 安装依赖
# pip install openai
from openai import OpenAI
import json
import time
client = OpenAI() # 需要设置 OPENAI_API_KEY 环境变量
def generate_training_data(topic: str, n_examples: int = 20) -> list:
"""
用GPT-4生成微调训练数据。
Args:
topic: 数据主题,如"Python异常处理"
n_examples: 每次生成的条数(建议不超过30,避免质量下降)
Returns:
包含 instruction/input/output 的字典列表
"""
prompt = f"""生成{n_examples}条关于"{topic}"的高质量训练数据。
每条数据必须包含以下三个字段:
- instruction: 具体、清晰的任务描述(不少于15个字)
- input: 任务处理的原始材料(如果任务不需要额外输入,填空字符串"")
- output: 高质量的期望回答(不少于50个字)
要求:
1. 覆盖不同难度(初级/中级/高级各占约三分之一)
2. 覆盖不同角度(概念解释、代码示例、常见错误、最佳实践等)
3. 回答要专业、准确、有实用价值
4. instruction字段要具体,不能是"解释X"这种过于宽泛的描述
必须以如下JSON格式返回,外层键名必须是"data":
{{"data": [
{{"instruction": "...", "input": "...", "output": "..."}},
...
]}}"""
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}, # 强制JSON格式输出
temperature=0.8 # 稍高的温度增加多样性
)
raw = json.loads(response.choices[0].message.content)
# 容错处理:GPT-4有时会用不同的键名
# 按优先级尝试常见键名
examples = (
raw.get("data") or # 我们要求的格式
raw.get("examples") or # 另一种常见键名
raw.get("training_data") or # 另一种可能的键名
raw.get("items") or
[]
)
# 如果上面都找不到,尝试找第一个列表类型的值
if not examples:
for value in raw.values():
if isinstance(value, list) and len(value) > 0:
examples = value
break
print(f"成功生成 {len(examples)} 条训练数据")
return examples
def batch_generate(topic: str, total: int = 200, batch_size: int = 20) -> list:
"""
分批生成大量训练数据,避免单次请求质量下降。
Args:
topic: 主题
total: 总条数
batch_size: 每批条数
"""
all_data = []
batches = (total + batch_size - 1) // batch_size # 向上取整
for i in range(batches):
print(f"生成第 {i+1}/{batches} 批...")
batch = generate_training_data(topic, batch_size)
all_data.extend(batch)
time.sleep(1) # 避免触发API限流
print(f"总计生成: {len(all_data)} 条")
return all_data
# 使用示例
if __name__ == "__main__":
# 生成关于"Python异常处理"的训练数据
data = batch_generate(topic="Python异常处理", total=100, batch_size=20)
# 保存到文件
with open("training_data_raw.json", "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f"数据已保存到 training_data_raw.json")
6.2. 方法2:从HuggingFace数据集获取
使用已有的开源数据集作为起点,再做领域适配:
# pip install datasets
from datasets import load_dataset
# 加载经典的Alpaca数据集(52000条英文指令数据)
dataset = load_dataset("tatsu-lab/alpaca", split="train")
print(f"Alpaca数据集: {len(dataset)} 条")
# 过滤出特定类型的数据(比如代码相关)
code_data = dataset.filter(
lambda x: any(kw in x['instruction'].lower()
for kw in ['python', 'code', 'function', 'program'])
)
print(f"代码相关数据: {len(code_data)} 条")
# 中文数据集推荐
# load_dataset("BelleGroup/train_1M_CN") # 中文指令数据集,100万条
# load_dataset("shibing624/alpaca-zh") # 中文Alpaca数据集
6.3. 方法3:从业务数据中提取
如果你有真实的业务数据(如客服对话记录、用户反馈等),这是质量最高的数据来源:
import pandas as pd
# 假设你有一份客服对话记录的CSV文件
# 列名:question, answer, satisfaction_score
df = pd.read_csv("customer_service_logs.csv")
# 只保留用户满意度高的对话(高质量数据)
high_quality = df[df['satisfaction_score'] >= 4]
# 转换为训练格式
training_data = []
for _, row in high_quality.iterrows():
training_data.append({
"instruction": "你是一名专业的客服代表,请回答用户问题。",
"input": row['question'],
"output": row['answer']
})
print(f"从业务数据提取了 {len(training_data)} 条训练样本")
7. 数据质量评估
在清洗之前,先了解数据的全貌:
import json
import statistics
from collections import Counter
def analyze_dataset(data_path: str) -> dict:
"""
全面分析数据集质量,输出关键统计指标。
Args:
data_path: JSON数据文件路径
Returns:
包含各项统计指标的字典
"""
with open(data_path, 'r', encoding='utf-8') as f:
data = json.load(f)
print(f"{'='*50}")
print(f"数据集分析报告")
print(f"{'='*50}")
print(f"总数据量: {len(data)} 条")
# 1. 长度分布分析
output_lengths = [len(d.get('output', '')) for d in data]
instruction_lengths = [len(d.get('instruction', '')) for d in data]
print(f"\n[输出长度分布]")
print(f" 最短: {min(output_lengths)} 字符")
print(f" 最长: {max(output_lengths)} 字符")
print(f" 平均: {statistics.mean(output_lengths):.0f} 字符")
print(f" 中位数: {statistics.median(output_lengths):.0f} 字符")
# 长度分段统计(直观了解分布)
length_buckets = Counter()
for l in output_lengths:
if l < 50:
length_buckets["<50字符(极短)"] += 1
elif l < 200:
length_buckets["50-200字符(短)"] += 1
elif l < 500:
length_buckets["200-500字符(中)"] += 1
elif l < 1000:
length_buckets["500-1000字符(长)"] += 1
else:
length_buckets[">1000字符(极长)"] += 1
print(f"\n[输出长度分段]")
for bucket, count in sorted(length_buckets.items()):
pct = count / len(data) * 100
print(f" {bucket}: {count}条 ({pct:.1f}%)")
# 2. 重复数据检测
outputs = [d.get('output', '') for d in data]
instructions = [d.get('instruction', '') + d.get('input', '') for d in data]
output_duplicates = len(outputs) - len(set(outputs))
instruction_duplicates = len(instructions) - len(set(instructions))
print(f"\n[重复数据]")
print(f" 重复输出: {output_duplicates} 条")
print(f" 重复指令+输入组合: {instruction_duplicates} 条")
# 3. 多样性分析(instruction 关键词分布)
# 统计 instruction 中出现频率最高的词,判断是否过于单一
all_words = []
for d in data:
words = d.get('instruction', '').split()[:5] # 取前5个词
all_words.extend(words)
top_words = Counter(all_words).most_common(10)
print(f"\n[Instruction 关键词 Top10](多样性参考)")
for word, count in top_words:
print(f" '{word}': {count}次")
# 4. 格式一致性检查
missing_instruction = sum(1 for d in data if not d.get('instruction', '').strip())
missing_output = sum(1 for d in data if not d.get('output', '').strip())
print(f"\n[格式检查]")
print(f" 缺少instruction字段: {missing_instruction} 条")
print(f" 缺少output字段: {missing_output} 条")
# 5. 质量评分(综合得分)
problems = output_duplicates + instruction_duplicates + missing_instruction + missing_output
short_count = length_buckets.get("<50字符(极短)", 0)
problems += short_count
quality_score = max(0, 100 - (problems / len(data)) * 100)
print(f"\n[综合质量评分]: {quality_score:.1f}/100")
if quality_score >= 90:
print(" 评级:优秀,可以直接用于训练")
elif quality_score >= 70:
print(" 评级:良好,建议清洗后再训练")
else:
print(" 评级:需要大量清洗工作")
return {
"total": len(data),
"output_lengths": output_lengths,
"duplicates": output_duplicates,
"quality_score": quality_score
}
# 使用示例
stats = analyze_dataset("training_data_raw.json")
8. 数据清洗
了解数据问题后,进行有针对性的清洗:
import re
def clean_dataset(data: list,
min_output_length: int = 50,
max_output_length: int = 2000) -> list:
"""
清洗数据集,移除低质量和重复数据。
参数说明:
- min_output_length=50:过滤掉50字符以下的输出。
依据:经验表明,少于50字符的回答通常是"好的"、"知道了"等无实质内容的
回复,或者是生成失败的截断输出。50字符大约等于2-3句完整的中文短句,
是一个有意义回答的最低标准。如果你的任务天然就需要短回答(如情感分类
返回"正面"/"负面"),应把这个值调小到5-10。
- max_output_length=2000:过滤掉2000字符以上的极长输出。
依据:过长的输出在训练时会截断,被截断的训练样本会让模型学到"回答可以
被中途打断"的错误行为。2000字符约等于800-1000汉字,对于大多数问答场景
已经足够。如果你的任务需要长文生成,应该增大这个值。
Args:
data: 原始数据列表
min_output_length: output字段的最小字符数
max_output_length: output字段的最大字符数
Returns:
清洗后的数据列表
"""
cleaned = []
seen_keys = set() # 用于去重(基于 instruction + input 的组合)
removed_stats = {
"empty": 0,
"too_short": 0,
"too_long": 0,
"duplicate": 0,
"format_error": 0
}
for item in data:
# 检查必填字段是否存在
if not isinstance(item, dict):
removed_stats["format_error"] += 1
continue
instruction = item.get('instruction', '').strip()
output = item.get('output', '').strip()
input_text = item.get('input', '').strip()
# 过滤空输出
if not output:
removed_stats["empty"] += 1
continue
# 过滤空 instruction
if not instruction:
removed_stats["format_error"] += 1
continue
# 过滤过短输出(去掉截断失败的生成结果)
if len(output) < min_output_length:
removed_stats["too_short"] += 1
continue
# 过滤过长输出(避免训练时截断)
if len(output) > max_output_length:
removed_stats["too_long"] += 1
continue
# 去除重复(相同的 instruction + input 组合)
dedup_key = instruction + "|||" + input_text
if dedup_key in seen_keys:
removed_stats["duplicate"] += 1
continue
seen_keys.add(dedup_key)
# 文本规范化:去除多余空白
item['instruction'] = instruction
item['input'] = input_text
item['output'] = output
cleaned.append(item)
# 输出清洗报告
print(f"清洗前: {len(data)} 条")
print(f"清洗后: {len(cleaned)} 条")
print(f"\n移除原因:")
for reason, count in removed_stats.items():
if count > 0:
labels = {
"empty": "空输出",
"too_short": f"输出过短(<{min_output_length}字符)",
"too_long": f"输出过长(>{max_output_length}字符)",
"duplicate": "重复数据",
"format_error": "格式错误"
}
print(f" {labels[reason]}: {count} 条")
return cleaned
# 使用示例
with open("training_data_raw.json", "r", encoding="utf-8") as f:
raw_data = json.load(f)
# 根据你的任务调整参数
# 如果是分类任务(输出是"是"/"否"/"正面"等),min_output_length 设为 1
cleaned_data = clean_dataset(
raw_data,
min_output_length=50, # 通用问答任务的推荐值
max_output_length=2000 # 根据你的模型最大序列长度调整
)
# 保存清洗后的数据
with open("training_data_clean.json", "w", encoding="utf-8") as f:
json.dump(cleaned_data, f, ensure_ascii=False, indent=2)
print(f"\n清洗后数据已保存到 training_data_clean.json")
9. 数据质量评估的具体指标
清洗完成后,用以下三个维度评估数据集是否达到训练标准:
9.1. 指标1:多样性(Diversity)
多样性差的数据集会导致模型只会回答同一类问题。检测方法:
from collections import Counter
import math
def measure_diversity(data: list) -> float:
"""
用词汇多样性粗略衡量数据集的多样性。
返回值越接近1,多样性越高。
"""
# 统计所有 instruction 中出现的词
all_words = []
for item in data:
words = item.get('instruction', '').replace(',', ' ').replace('。', ' ').split()
all_words.extend(words)
total_words = len(all_words)
unique_words = len(set(all_words))
diversity_ratio = unique_words / total_words if total_words > 0 else 0
print(f"词汇多样性: {diversity_ratio:.3f} (总词数{total_words}, 不重复词数{unique_words})")
if diversity_ratio > 0.5:
print(" -> 多样性良好")
elif diversity_ratio > 0.3:
print(" -> 多样性一般,建议增加更多覆盖角度")
else:
print(" -> 多样性较差,数据集过于单一")
return diversity_ratio
### 9.2. 指标2:长度分布(Length Distribution)
理想的数据集长度分布应接近正态分布,而不是两极分化:
def check_length_distribution(data: list):
"""检查输出长度是否分布均匀。"""
lengths = [len(d.get('output', '')) for d in data]
mean = statistics.mean(lengths)
std = statistics.stdev(lengths) if len(lengths) > 1 else 0
# 变异系数(Coefficient of Variation)衡量分布均匀程度
# CV < 0.5 表示分布较均匀,CV > 1 表示分布非常不均匀
cv = std / mean if mean > 0 else 0
print(f"长度分布: 均值={mean:.0f}, 标准差={std:.0f}, 变异系数={cv:.2f}")
if cv < 0.5:
print(" -> 长度分布均匀,良好")
elif cv < 1.0:
print(" -> 长度分布有些不均匀,可接受")
else:
print(" -> 长度分布非常不均匀,建议检查是否有数据生成异常")
9.3. 指标3:格式一致性(Format Consistency)
如果你的训练数据要求特定格式(如 JSON 输出、带编号的列表等),验证一致性:
def check_format_consistency(data: list, expected_pattern: str = None) -> float:
"""
检查数据集中有多少条符合预期格式。
Args:
expected_pattern: 正则表达式,如 r'^\d+\.' 检查是否以数字开头
Returns:
符合格式的比例(0-1)
"""
if not expected_pattern:
print("未指定格式要求,跳过格式一致性检查")
return 1.0
pattern = re.compile(expected_pattern)
consistent = sum(1 for d in data if pattern.search(d.get('output', '')))
ratio = consistent / len(data)
print(f"格式一致性: {ratio:.1%} ({consistent}/{len(data)} 条符合格式要求)")
return ratio
# 综合评估示例
print("\n=== 综合质量评估 ===")
with open("training_data_clean.json", "r", encoding="utf-8") as f:
clean_data = json.load(f)
diversity = measure_diversity(clean_data)
check_length_distribution(clean_data)
# 如果你的输出要求以序号开头,可以这样检查:
# check_format_consistency(clean_data, expected_pattern=r'^\d+[\.\、]')
10. 完整流程整合
以下是从零开始准备一批训练数据的完整脚本:
import json
import time
from openai import OpenAI
import statistics
client = OpenAI()
# 第1步:生成原始数据
print("步骤1:生成原始训练数据...")
raw_data = []
topics = [
"Python装饰器的使用场景和最佳实践",
"Python异常处理与错误调试",
"Python列表推导式和生成器表达式",
]
for topic in topics:
batch = generate_training_data(topic, n_examples=20)
raw_data.extend(batch)
time.sleep(2)
print(f"原始数据: {len(raw_data)} 条")
# 第2步:数据清洗
print("\n步骤2:数据清洗...")
cleaned = clean_dataset(raw_data, min_output_length=50, max_output_length=2000)
# 第3步:质量评估
print("\n步骤3:质量评估...")
measure_diversity(cleaned)
check_length_distribution(cleaned)
# 第4步:保存最终数据集
output_path = "training_data_final.json"
with open(output_path, "w", encoding="utf-8") as f:
json.dump(cleaned, f, ensure_ascii=False, indent=2)
print(f"\n最终数据集: {len(cleaned)} 条,已保存到 {output_path}")
print(f"下一步:用这份数据集运行 QLoRA 微调(见下一章)")
11. 小结
好的微调数据集需要做好五件事:
- 格式正确:选对 Alpaca 格式还是 Chat 格式(看任务类型)
- Instruction 质量高:具体、有角色定义、有格式约束(这是最容易被忽视的)
- 数量适当:先用100-200条验证方向,确认有效再扩充
- 清洗彻底:移除空数据、过短数据、重复数据
- 多维度评估:多样性、长度分布、格式一致性都要检查
数据准备占微调工作量的60-70%,但也是影响最终效果的最大变量。在这一步投入时间是值得的。
下一章我们将用这份准备好的数据集进行 QLoRA 微调训练。