Self-Instruct:利用大模型自举生成指令数据

FreeGuideOnline 最新 2026-06-14

合成数据生成:Self-Instruct 完整教程

引言

在训练指令跟随型大语言模型时,高质量、多样化的指令数据至关重要。然而人工标注成本高昂、覆盖面有限。Self-Instruct 提供了一种近乎零成本的自举方法:让大模型自己“举一反三”,从一个极小的种子集出发,自动生成海量指令数据。本教程将从零带你理解并实现这一技术。

什么是 Self-Instruct?

背景与动机

指令微调(Instruction Tuning)需要模型学习理解并执行各种自然语言指令。早期工作如 InstructGPT 依赖大量人工撰写或改写指令,耗时且昂贵。Self-Instruct 的提出(Wang et al., 2023)解决了这一痛点:仅需约 175 条人工编写的种子任务,即可引导大模型生成超过 8 万条训练指令,在多个基准上取得与人工标注数据训练相抗衡的效果。

核心思想

Self-Instruct 的本质是“模型自举”:

  1. 以一个小规模人工编写的种子指令集作为起点。
  2. 让大模型阅读已有指令,模仿其格式与多样性,生成全新的指令。
  3. 生成过程中自动产生指令对应的输入样例期望输出
  4. 通过自动过滤与去重提升数据质量,再将高质量生成数据回填到种子池中,循环迭代。

整个过程完全由模型自己完成,无需人工介入,因此得名 Self-Instruct。

Self-Instruct 的工作流程

种子任务与指令生成

初始种子集通常包含 175 个覆盖多种任务类型的指令,例如:文本分类、开放式生成、改写、摘要、头脑风暴等。每条种子数据包含:

  • 指令:对任务的描述。
  • 输入(可选):任务的具体输入内容,部分指令无需输入。
  • 输出:期望的回答。

在生成阶段,我们从现有池中随机抽取 8 条指令作为示例(6 条从人工种子中选,2 条从之前模型合成的优质数据中选),拼接成一个提示词模板,要求模型逐条生成新的指令。提示词明确指导模型:

  • 产出可独立阅读的任务指令。
  • 任务应多样化,覆盖不同能力和场景。
  • 区分“有输入”和“无输入”的任务类型。
  • 为每条指令生成一个合理的输入样例和相应的输出。

分类与过滤

模型生成的原始数据往往包含质量低下、重复或含义模糊的条目。Self-Instruct 设计了多层级自动过滤:

  1. 有效性检查:指令是否与已有池中的任何指令 ROUGE-L 相似度过高(阈值通常设为 0.7),过高则丢弃。
  2. 质量判断:利用大模型自身(或一个更小的判别器)对生成的指令进行分类——该指令是否包含明确的预期输出格式、是否可用文本解决、是否不涉及图像/链接等无法处理的内容。
  3. 任务分类:将有效指令标记为“分类类”、“生成类”、“改写类”等,便于后续统计分析。
  4. 输入输出一致性:检查生成的输入和输出是否与指令的逻辑一致(例如生成类任务不应给出只有一个固定答案的输出)。

生成实例与输出

经过过滤的指令需要扩充出更多输入-输出对。因为指令生成时仅各自附带了一个输入样例,直接使用会导致数据单一。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-turbogpt-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 的完整理论和可执行代码。立即动手,用一份小种子为自己的模型造出大规模训练数据吧!