Self-Instruct:利用大模型自举生成指令数据
合成数据生成:Self-Instruct 完整教程
引言
在训练指令跟随型大语言模型时,高质量、多样化的指令数据至关重要。然而人工标注成本高昂、覆盖面有限。Self-Instruct 提供了一种近乎零成本的自举方法:让大模型自己“举一反三”,从一个极小的种子集出发,自动生成海量指令数据。本教程将从零带你理解并实现这一技术。
什么是 Self-Instruct?
背景与动机
指令微调(Instruction Tuning)需要模型学习理解并执行各种自然语言指令。早期工作如 InstructGPT 依赖大量人工撰写或改写指令,耗时且昂贵。Self-Instruct 的提出(Wang et al., 2023)解决了这一痛点:仅需约 175 条人工编写的种子任务,即可引导大模型生成超过 8 万条训练指令,在多个基准上取得与人工标注数据训练相抗衡的效果。
核心思想
Self-Instruct 的本质是“模型自举”:
- 以一个小规模人工编写的种子指令集作为起点。
- 让大模型阅读已有指令,模仿其格式与多样性,生成全新的指令。
- 生成过程中自动产生指令对应的输入样例和期望输出。
- 通过自动过滤与去重提升数据质量,再将高质量生成数据回填到种子池中,循环迭代。
整个过程完全由模型自己完成,无需人工介入,因此得名 Self-Instruct。
Self-Instruct 的工作流程
种子任务与指令生成
初始种子集通常包含 175 个覆盖多种任务类型的指令,例如:文本分类、开放式生成、改写、摘要、头脑风暴等。每条种子数据包含:
- 指令:对任务的描述。
- 输入(可选):任务的具体输入内容,部分指令无需输入。
- 输出:期望的回答。
在生成阶段,我们从现有池中随机抽取 8 条指令作为示例(6 条从人工种子中选,2 条从之前模型合成的优质数据中选),拼接成一个提示词模板,要求模型逐条生成新的指令。提示词明确指导模型:
- 产出可独立阅读的任务指令。
- 任务应多样化,覆盖不同能力和场景。
- 区分“有输入”和“无输入”的任务类型。
- 为每条指令生成一个合理的输入样例和相应的输出。
分类与过滤
模型生成的原始数据往往包含质量低下、重复或含义模糊的条目。Self-Instruct 设计了多层级自动过滤:
- 有效性检查:指令是否与已有池中的任何指令 ROUGE-L 相似度过高(阈值通常设为 0.7),过高则丢弃。
- 质量判断:利用大模型自身(或一个更小的判别器)对生成的指令进行分类——该指令是否包含明确的预期输出格式、是否可用文本解决、是否不涉及图像/链接等无法处理的内容。
- 任务分类:将有效指令标记为“分类类”、“生成类”、“改写类”等,便于后续统计分析。
- 输入输出一致性:检查生成的输入和输出是否与指令的逻辑一致(例如生成类任务不应给出只有一个固定答案的输出)。
生成实例与输出
经过过滤的指令需要扩充出更多输入-输出对。因为指令生成时仅各自附带了一个输入样例,直接使用会导致数据单一。Self-Instruct 的做法是:
- 输入优先法:对于某条指令,提示模型生成多个不同的输入实例。
- 输出预测法:将指令和生成的输入组合,再次调用模型以零样本方式预测对应输出。
这巧妙地实现了数据量的倍增,同时保留了指令的复杂性和多样性。
数据后处理与降噪
- 格式清洗:去除生成文本中可能出现的提示词残留、多余的符号或编号。
- 去重泛化:使用嵌入相似度或 n-gram 重叠再次剔除高相似数据。
- 难度平衡:统计输出文本的长度、词汇丰富度等,避免数据全部偏向简单或极复杂任务。
- 最终池构造:将经多重筛选的数据合并为数据集(如著名的 Alpaca 数据集就是基于 Self-Instruct 流程从 text-davinci-003 生成的 52K 指令遵循数据)。
实践:一步步实现 Self-Instruct
以下用 Python 和 OpenAI API 为例完成一个最小可行版本。你可以替换为开源模型(如 Llama 3)配合自己的推理端点。
环境准备
pip install openai tiktoken numpy scikit-learn
配置 API 密钥:
import openai
openai.api_key = "your-api-key"
定义默认模型(可根据情况选择 gpt-3.5-turbo 或 gpt-4):
MODEL = "gpt-3.5-turbo"
调用大模型生成指令
准备种子数据(示例仅三条,实践中至少 100+):
seed_tasks = [
{"instruction": "将以下句子翻译成英文。", "input": "今天天气真好。", "output": "The weather is really nice today."},
{"instruction": "根据给定的主题写一首短诗。", "input": "月亮", "output": "银盘挂夜天,清辉洒人间。"},
{"instruction": "判断下列陈述是否事实。", "input": "地球绕着太阳转。", "output": "是"},
]
构建提示生成新指令:
def generate_new_instructions(seed_pool, num_new=5):
# 从种子池随机抽样示例(6人工+2生成,此处简化为随机8个)
import random
examples = random.sample(seed_pool, min(8, len(seed_pool)))
example_text = ""
for i, ex in enumerate(examples):
example_text += f"{i+1}. 指令: {ex['instruction']}\n"
if ex['input']:
example_text += f" 输入: {ex['input']}\n"
example_text += f" 输出: {ex['output']}\n\n"
prompt = f"""你需要生成多样化的任务指令。以下是已有任务举例:
{example_text}
请基于这些示例,继续生成 {num_new} 个全新的、不同种类的任务指令。
每个任务需包含:
- 指令(清晰、完整)
- 输入(如果任务不需要输入,则留空)
- 输出(符合指令的期望回答)
请以 JSON 列表格式输出,每个元素包含 instruction, input, output 字段。"""
response = openai.ChatCompletion.create(
model=MODEL,
messages=[{"role": "user", "content": prompt}],
temperature=0.7,
max_tokens=2000
)
# 简单解析返回的 JSON(实际需增加异常处理和解析逻辑)
import json
try:
new_tasks = json.loads(response.choices[0].message.content)
except:
new_tasks = []
return new_tasks
调用一次,获得一批原始指令:
raw_instructions = generate_new_instructions(seed_tasks, num_new=3)
指令质量自动评估与过滤
主要过滤策略:ROUGE-L 重复检查和有效性提示分类。
ROUGE-L 去重
from rouge_score import rouge_scorer
def rouge_l_similarity(text_a, text_b):
scorer = rouge_scorer.RougeScorer(['rougeL'], use_stemmer=True)
scores = scorer.score(text_a, text_b)
return scores['rougeL'].fmeasure
def filter_similar(instructions, existing_instructions, threshold=0.7):
kept = []
for inst in instructions:
max_sim = 0
for exist in existing_instructions:
sim = rouge_l_similarity(inst['instruction'], exist['instruction'])
max_sim = max(max_sim, sim)
if max_sim < threshold:
kept.append(inst)
return kept
使用模型分类有效性
判断指令是否适合文本模型处理:
def is_valid_instruction(instruction_text):
classify_prompt = f"""请判断以下指令是否可以被一个纯文本语言模型完美完成。若包含图像、音频、外部链接或需要实时信息则回答“无效”,否则回答“有效”。只输出“有效”或“无效”。
指令:{instruction_text}"""
resp = openai.ChatCompletion.create(
model=MODEL,
messages=[{"role": "user", "content": classify_prompt}],
temperature=0
)
answer = resp.choices[0].message.content.strip()
return answer == "有效"
对生成结果进行过滤:
filtered = filter_similar(raw_instructions, seed_tasks)
valid_instructions = [inst for inst in filtered if is_valid_instruction(inst['instruction'])]
生成输入输出对
为了让每个指令拥有多个样例,我们对有效指令执行“输入扩展”:
def expand_inputs(instruction, num_inputs=3):
prompt = f"""对于以下任务指令,请生成 {num_inputs} 个不同的合理输入,每个输入单独一行,不要添加编号。
指令:{instruction}"""
response = openai.ChatCompletion.create(
model=MODEL,
messages=[{"role": "user", "content": prompt}],
temperature=0.8
)
inputs = response.choices[0].message.content.strip().split('\n')
# 去除空行和可能的前缀
inputs = [inp.strip() for inp in inputs if inp.strip()]
return inputs
接着为每个(指令,输入)对生成输出:
def generate_outputs(instruction, inputs_list):
pairs = []
for inp in inputs_list:
if inp:
user_msg = f"{instruction}\n\n输入:{inp}\n输出:"
else:
user_msg = f"{instruction}\n\n输出:"
resp = openai.ChatCompletion.create(
model=MODEL,
messages=[{"role": "user", "content": user_msg}],
temperature=0.3
)
output = resp.choices[0].message.content.strip()
pairs.append({"instruction": instruction, "input": inp, "output": output})
return pairs
组合使用:
expanded_data = []
for inst in valid_instructions:
inputs = expand_inputs(inst['instruction'], num_inputs=3)
outputs_pairs = generate_outputs(inst['instruction'], inputs)
expanded_data.extend(outputs_pairs)
最终数据集构建
最后进行简单的去重和格式标准化,保存为 JSON 文件。
import json
# 基于指令+输入的 MD5 去重
import hashlib
def dedup(data):
seen = set()
unique = []
for item in data:
key = hashlib.md5((item['instruction'] + item['input']).encode()).hexdigest()
if key not in seen:
seen.add(key)
unique.append(item)
return unique
final_data = dedup(expanded_data)
with open("self_instruct_dataset.json", "w", encoding="utf-8") as f:
json.dump(final_data, f, ensure_ascii=False, indent=2)
print(f"最终数据集大小: {len(final_data)}")
现在你便拥有了一个完全由模型自动生成的指令数据集,可用于微调你自己的模型。
优化与高级技巧
多样性与难度控制
- 基于标签的抽样:对生成的指令自动分类(摘要、翻译、创作等),在下一轮生成时按类别平衡抽样,防止模式崩塌。
- 长度与复杂度过滤:计算输出的 token 数,剔除过短(<10 tokens)和过长(>500 tokens)的回复,保留中等复杂度样本。
- 主动加入“非自然”模式:在提示生成指令时,明确要求包含某些具体类型,如“需要逻辑推理的数学题”、“多轮对话型任务”等。
利用检索增强生成
为了进一步提升指令的真实性和知识密集度,可在生成输入时将相关维基百科片段或文档作为上下文注入:
# 伪代码:先检索相关内容,再嵌入生成提示
context = retrieve_wikipedia(topic)
prompt = f"参考以下知识:{context}\n\n请生成一个基于该知识的问答指令..."
这能显著减少模型幻觉,产出事实性更强的数据。
多轮自举迭代
Self-Instruct 的核心是迭代:将当前轮次生成并通过过滤的数据加入种子池,使下一轮生成拥有更丰富的示范,模型能逐渐发现更多长尾任务。
典型迭代策略:
- 第一轮:仅用人工种子生成初步数据。
- 第二轮起:每次随机从“人工种子 + 前几轮高质量生成”中抽取示例,给模型更大的指令空间。
- 收敛监控:当新生成指令的 ROUGE-L 相似度与已有池整体超过某一阈值时,可停止迭代,避免大量冗余。
常见问题与解决方案
1. 生成数据中包含大量格式错误(缺失字段、非 JSON)
- 解决:在提示词中明确 JSON 格式要求,并增加后处理正则修复;也可分步生成,先生产指令,再在其他步骤生成输入/输出。
2. 数据偏向某一类型(如开放域问答过多)
- 解决:在提取示例时强制按类别分层抽样;在提示中加入“请生成与以上示例类别不同的任务”的强制约束。
3. 模型生成不可靠或幻觉严重的输出
- 解决:引入一个独立的“评判模型”(可以是同模型但低温采样)对输出进行事实核查,或使用 NLI 模型判断输入输出逻辑一致性。
4. API 调用成本过高
- 解决:使用性价比更高的开源模型部署本地推理(如 Llama 3 8B,通过 vLLM 加速);在过滤阶段优先使用基于规则的快速检查,减少模型调用次数。
5. 生成数据与人工标注数据的性能差距
- 解决:组合使用少量高质量人工标注数据与大量合成数据进行混合训练(如 1:10 比例),可有效弥补差距。
总结
Self-Instruct 通过巧妙的“模型自举”流程,将昂贵的人工指令标注变为自动化生产。关键要点回顾:
- 始于小种子:最少 100+ 人工指令即可启动。
- 生成-过滤-扩充:三步循环,保证质量与多样性。
- 迭代增强:利用生成的高质量数据反哺自身,逐步拓展任务边界。
- 实践即用:整套流程易于实现,配合开源模型可完全零成本构建专属指令数据集。
你现在已经掌握了 Self-Instruct 的完整理论和可执行代码。立即动手,用一份小种子为自己的模型造出大规模训练数据吧!