Part of Language AI Handbook
Explains how Self-Instruct enables language models to generate their own training data through iterative bootstrapping from minimal human-written seed tasks.
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Self-Instruct
In the previous chapter, we explored methods for creating instruction-tuning datasets, from expensive human annotation to various synthetic approaches. Each method involved trade-offs between quality and cost at different scales. What if a language model could generate its own training data? This circular idea can produce useful datasets without substantial annotation budgets. Self-Instruct implements a constrained version of self-improvement by using a model to generate examples for its own training.
Self-Instruct is a framework that enables large language models to bootstrap their instruction-following capabilities by generating their own training examples. Starting with just a handful of human-written seed tasks, the model iteratively produces thousands of diverse instructions along with their inputs and outputs. The approach addresses a basic bottleneck in instruction tuning: the difficulty of creating large-scale, diverse instruction datasets without massive human effort. By using the model's own capabilities, Self-Instruct turns data creation from a labor-intensive process into an automated pipeline. When the original paper applied this technique to GPT-3, the resulting model matched the instruction-following quality of InstructGPT while using only 175 human-written seeds as a starting point.
The key insight behind Self-Instruct is that language models, even before instruction tuning, already possess substantial knowledge from pre-training. They understand language structure, know facts about the world, and can follow patterns demonstrated in context. This latent capability exists because pre-training on large text corpora exposes models to countless examples of instructions and their completions, embedded naturally within documents and tutorials as well as conversations. Self-Instruct uses these capabilities, applying in-context learning (which we covered in Part XXVIII) to guide the model toward generating useful instruction-following examples. Rather than teaching the model something entirely new, the framework activates and structures knowledge the model already possesses.
Think of Self-Instruct as a skilled apprentice who has read every textbook in a library but has never been formally tested on any subject. Given a handful of sample exam questions, the apprentice can generate hundreds more questions of similar quality and variety, drawing on everything they absorbed from all that reading. They may not get every question perfectly right, but the sheer volume and coverage of their output vastly exceeds what a small team of human test-writers could produce in the same time. The filtering pipeline plays the role of a quality reviewer, discarding badly formed or redundant questions before they enter the study set.
Understanding Self-Instruct requires appreciating the broader context it emerged from. In 2022, instruction-following models like InstructGPT demonstrated that the gap between raw language models and helpful assistants could be bridged through careful training data curation. But that curation process was expensive: InstructGPT relied on human contractors writing thousands of prompts and responses. Self-Instruct provided an alternative that researchers could implement with access to a large language model and a modest compute budget. This lower-cost approach helped the technique spread through later instruction-tuning pipelines.
The chapter walks through each stage of the pipeline in detail, from designing seed tasks to evaluating the diversity of the final dataset. We build up a working implementation that mirrors the original paper's approach, examine the statistical properties of self-generated data, and confront the limitations that arise when a model is asked to supervise itself. By the end, you will understand both how to apply Self-Instruct and when to look for alternatives.
Self-Instruct was introduced by Wang et al. in a 2022 paper titled "Self-Instruct: Aligning Language Model with Self Generated Instructions." The paper applied the technique to GPT-3 (text-davinci-001) using 175 human-written seed tasks, ultimately generating 52,000 instructions and 82,000 instances. The resulting model, GPT3-Self-Instruct, achieved performance comparable to InstructGPT (text-davinci-001 fine-tuned with RLHF) on the SUPER-NATURALINSTRUCTIONS benchmark. The work appeared at a moment when the research community was actively searching for cheaper alternatives to reinforcement learning from human feedback, and it opened the door to a wave of open-source instruction-tuned models. Alpaca, one of the most influential early open-source instruction-following models, used Self-Instruct to generate its training data from LLaMA. This shows that the technique transferred cleanly to models beyond GPT-3.
The Self-Instruct Pipeline
Self-Instruct operates as an iterative bootstrapping process that gradually builds a diverse instruction dataset from minimal human input. The pipeline begins with a small set of human-written seed tasks and progressively expands this collection by having the model generate new tasks, validate them, and add the high-quality ones back to the pool. This cyclical approach means that each iteration enriches the context available for subsequent generations, creating a flywheel where more diverse context produces more diverse outputs.
Think of the pipeline as a snowball rolling down a hill. The seed tasks are the initial compact snowball: carefully formed, high-quality, but small. As the snowball rolls, it picks up more snow (generated tasks) and grows larger. The filtering stages are like removing clumps of dirt and ice: the snowball keeps growing, but only with the right kind of material. The process is self-sustaining but not unlimited. Just as a snowball eventually runs out of fresh snow, the self-instruct pipeline eventually runs out of truly novel instructions to generate, reaching a natural ceiling as duplicates accumulate faster than fresh content.
The iterative architecture also provides compounding benefits for diversity. Early iterations rely primarily on the seed tasks for in-context demonstrations. Later iterations have access to a much richer pool of demonstrations spanning many task types and domains with varied output structures. This expanded context helps the model identify and explore corners of the instruction space that the original seeds did not cover. A model prompted with demonstrations of cooking instructions, legal text analysis, and creative writing will generate more varied new instructions than one prompted only with sentiment classification examples.
The core loop consists of four stages that repeat until the desired dataset size is reached. Each stage serves a specific purpose in so both the quality and diversity of the final dataset.
- Instruction Generation: Sample existing tasks from the pool and prompt the model to generate new, diverse instructions. This stage uses in-context learning to demonstrate the desired format and style.
- Classification Identification: Determine whether each generated instruction is a classification task (requiring a fixed set of output labels) or an open-ended generation task. This distinction is important because it affects how we generate training instances.
- Instance Generation: For each instruction, generate appropriate inputs and outputs using either input-first or output-first approaches. The choice of approach depends on the task type identified in the previous stage.
- Filtering: Remove low-quality, duplicate, or problematic examples before adding them to the task pool. This quality control step prevents degradation of the dataset over iterations.
# Conceptual representation of the Self-Instruct pipeline
class SelfInstructPipeline:
def __init__(self, seed_tasks, model, target_size=50000):
self.task_pool = list(seed_tasks)
self.model = model
self.target_size = target_size
def run(self):
while len(self.task_pool) < self.target_size:
# Step 1: Generate new instructions
new_instructions = self.generate_instructions()
# Step 2: Classify each instruction
classified = self.classify_instructions(new_instructions)
# Step 3: Generate instances (inputs and outputs)
instances = self.generate_instances(classified)
# Step 4: Filter and add to pool
filtered = self.filter_instances(instances)
self.task_pool.extend(filtered)
return self.task_poolThis iterative approach creates a flywheel effect: as the task pool grows with diverse examples, the model has richer context for generating even more varied instructions. The expanding diversity of demonstrations enables the model to explore new directions it might not have discovered with only the original seed tasks. The process is self-reinforcing, though it requires careful filtering to prevent degradation. Without proper quality controls, errors and biases could accumulate across iterations, leading to progressively worse outputs. This is why the filtering stage is an needed structural component of the approach rather than a nice-to-have optimization.

Seed Task Design
The quality of Self-Instruct outputs depends critically on the initial seed tasks. These human-written examples establish formatting and diversity expectations as well as quality standards that the model will follow when generating new tasks. Think of seed tasks as the DNA of the dataset: they carry the basic blueprint that all subsequent generations inherit and recombine. Poor seed design leads to limited, repetitive outputs, while thoughtful seed curation enables the generation of rich, diverse datasets that cover a wide range of task types and subject areas.
The seed tasks perform several functions simultaneously. They demonstrate the desired format, showing the model what a well-formed instruction-input-output triple looks like. They establish quality standards, giving the model examples of the level of specificity and coherence expected in each field. And they introduce topical diversity. This keeps the model's initial generation attempts span multiple domains rather than clustering around a single subject area. Because the model cannot generate task types it has never seen demonstrated, the seeds act as templates for the entire generation process.
Designing effective seeds requires thinking carefully about coverage across multiple dimensions. A seed set that contains only text classification tasks will produce a dataset heavily weighted toward classification. A seed set that contains only short-output tasks will fail to demonstrate the formatting conventions for longer, structured outputs. The original Self-Instruct paper addressed this by manually curating 175 seeds to span a broad range of task categories, output lengths, and topic areas. This investment in seed quality paid dividends throughout the generation process, as the diverse demonstrations guided the model toward similarly varied outputs.
The relationship between seed quality and generation quality is not merely correlational. In the in-context learning regime, the model explicitly uses the seed demonstrations to identify patterns and generate continuations. If all demonstrated instructions start with "Classify," the model will generate instructions that start with "Classify." If seeds include a mix of verbs ("Write," "Explain," "Translate," "Identify," "Compare"), the model will diversify its generated instruction vocabulary accordingly. This direct causal link between seed design and output variety makes seed curation one of the highest-impact activities in the entire Self-Instruct workflow.
The original Self-Instruct paper used 175 seed tasks, each containing an instruction, optional input, and expected output. These seeds were designed to cover a broad range of task types, so the model had exposure to many different patterns from the very beginning:
# Example seed tasks spanning different categories
seed_tasks = [
{
"instruction": "Classify the sentiment of the given movie review.",
"input": "This film was absolutely wonderful. The acting was superb!",
"output": "Positive",
},
{
"instruction": "Write a haiku about autumn.",
"input": "",
"output": "Leaves drift slowly down\nCrisp air fills the empty woods\nNature's final bow",
},
{
"instruction": "Convert the following temperature from Celsius to Fahrenheit.",
"input": "25 degrees Celsius",
"output": "77 degrees Fahrenheit",
},
{
"instruction": "List three potential causes of the given historical event.",
"input": "The French Revolution",
"output": "1. Economic crisis and heavy taxation of the poor\n2. Social inequality between estates\n3. Enlightenment ideas spreading democratic principles",
},
{
"instruction": "Rewrite the sentence to make it more formal.",
"input": "Hey, can you help me out with this thing?",
"output": "Would you be able to assist me with this matter?",
},
]The seed tasks should exhibit several key properties that together establish a strong foundation for generation.
- Format diversity: Mix of tasks with and without inputs, varying output lengths and structures. Some tasks require only an instruction, while others need additional context provided as input.
- Topic coverage: Span different domains like science, arts, daily life, and abstract reasoning. This breadth ensures the model generates instructions across many subject areas rather than clustering around a single theme.
- Task type variety: Include classification and generation tasks, along with transformation and reasoning tasks. Each task type exercises different capabilities and produces different output patterns.
- Clear patterns: Demonstrate the expected instruction-input-output format unambiguously. The model should have no confusion about what each field contains or how they relate to one another.
The key insight is that seeds function as active teachers, not merely examples. Every seed simultaneously teaches the model what format to follow, what topics are in scope, and what level of detail is appropriate. Spending extra time crafting high-quality seeds with broad coverage pays compounding returns as the pipeline scales.



Instruction Generation
With seed tasks established, the model generates new instructions through prompted generation. The approach samples several existing tasks from the pool and asks the model to create novel instructions inspired by, but different from, the examples. This in-context learning approach uses the model's ability to identify patterns and generate variations that maintain the same structure while introducing new content. The technique requires no special training objective: we simply ask the model to continue a numbered list of instructions, and it does exactly that.
The prompt structure is carefully designed to encourage diversity while maintaining quality. By showing the model a numbered list of existing instructions and asking it to continue the list, we tap into the model's natural tendency to generate coherent continuations. This is a direct application of the few-shot learning capability we studied in Part XXVIII. The model recognizes that it is creating items in a series and attempts to match the style and format as well as the diversity level of the demonstrated items. If the demonstrated items vary widely in topic and structure, the model's continuations will similarly vary.
A key design decision in the original paper was using a mix of human-authored seeds and machine-generated tasks in the in-context demonstration. This blend serves two purposes. The human seeds ensure a quality floor: the model always has access to carefully crafted examples as anchors. The machine-generated tasks provide additional diversity and demonstrate that the range of acceptable instructions extends beyond the narrow set of hand-crafted examples. Over time, as the pool grows, the proportion of machine-generated demonstrations increases, but the human seeds remain available for sampling.
The sampling strategy for selecting which tasks to include as demonstrations significantly affects what kinds of instructions the model produces. Purely random sampling tends to reflect the current distribution of the task pool, which means popular task types get over-represented in demonstrations and thus in new generations. More sophisticated sampling strategies can counteract this tendency by deliberately seeking out underrepresented task types or recently discovered categories that the model should explore further.
def create_instruction_generation_prompt(sampled_tasks, num_to_generate=8):
"""
Create a prompt for generating new instructions.
Samples existing tasks and formats them as few-shot examples,
then asks for new, diverse instructions.
"""
prompt = """Come up with a series of tasks:
"""
# Add sampled existing tasks as demonstrations
for i, task in enumerate(sampled_tasks, 1):
prompt += f"{i}. {task['instruction']}\n"
# Request new instructions
next_num = len(sampled_tasks) + 1
prompt += f"{next_num}."
return promptimport random
# Simulate instruction generation with sampled context
sampled = random.sample(seed_tasks, 3)
generation_prompt = create_instruction_generation_prompt(sampled)=== Instruction Generation Prompt === Come up with a series of tasks: 1. Classify the sentiment of the given movie review. 2. Rewrite the sentence to make it more formal. 3. Convert the following temperature from Celsius to Fahrenheit. 4.
The sampling strategy for selecting demonstration tasks is important for diversity. In practice, rather than purely random uniform sampling, you can use a balanced approach that considers multiple factors.
- Recency bias: Slightly favor recently added tasks to explore new directions. This helps the generation process branch out from recently discovered instruction patterns rather than always returning to the same seed tasks.
- Diversity sampling: Ensure sampled tasks span different categories. If all demonstrations are sentiment classification tasks, the model will likely generate more sentiment classification tasks.
- Human seed inclusion: Always include some original seed tasks to maintain quality anchoring. The human-written examples serve as a quality floor, preventing drift toward lower-quality patterns.
When the model generates instructions, it often produces multiple candidates at once. The prompt asks for several new instructions, and the model continues generating until it hits a stop condition or maximum length. This batch generation approach is more efficient than requesting one instruction at a time, and it provides the model with a longer rolling context that encourages diversity: if the model has already generated a classification task in this batch, it tends to generate something different next.
def parse_generated_instructions(model_output):
"""
Extract individual instructions from model output.
The model typically generates numbered lists like:
4. Instruction one
5. Instruction two
"""
instructions = []
lines = model_output.strip().split("\n")
for line in lines:
line = line.strip()
# Match numbered items like "4. Instruction text"
if line and line[0].isdigit():
# Remove the number and period
parts = line.split(".", 1)
if len(parts) > 1:
instruction = parts[1].strip()
if instruction:
instructions.append(instruction)
return instructions# Example model output
mock_model_output = """4. Summarize the main argument of the given paragraph.
5. Write a product description for a smartwatch.
6. Identify the logical fallacy in the given statement.
7. Translate the English sentence to Spanish.
8. Suggest three alternative titles for the given book."""
parsed_instructions = parse_generated_instructions(mock_model_output)Parsed instructions: 1. Summarize the main argument of the given paragraph. 2. Write a product description for a smartwatch. 3. Identify the logical fallacy in the given statement. 4. Translate the English sentence to Spanish. 5. Suggest three alternative titles for the given book.
The parsing function successfully extracts the core instruction text from the raw model output, removing numbering and whitespace. This step normalizes the data into a structured list ready for filtering and instance generation. The simplicity of this parsing reflects a key design principle of Self-Instruct: use straightforward, predictable formats that are easy to both generate and parse. Complex formats introduce parsing failures and model confusion; simple numbered lists are reliable to the minor formatting variations that naturally occur in model outputs.
Balancing Human and Machine Demonstrations
One of the subtler aspects of the Self-Instruct prompting strategy is deciding how to mix human-authored seed tasks with machine-generated tasks in the in-context demonstrations. Including only human seeds produces high-quality but potentially narrow demonstrations. Including only machine-generated tasks risks drifting toward lower-quality patterns over time. The original paper found that always including some human seeds alongside machine-generated tasks struck the best balance.
In practice, you can implement this mixed sampling as a two-stage draw: first, always sample a fixed number of human seeds (typically 6 out of 8 demonstration slots), then fill the remaining slots from the broader machine-generated pool. This ensures that quality anchors are always present while still benefiting from the diversity of the larger pool. As the pool grows, the machine-generated tasks contribute increasingly varied demonstrations, but the human seeds provide a constant quality reference point.
The number of demonstrations to include in each prompt also matters. Too few demonstrations and the model lacks sufficient context to identify the desired format and diversity level. Too many demonstrations and the prompt approaches the model's context window limit, leaving less room for the generated output. The original Self-Instruct paper used 8 demonstrations per prompt, a compromise that fits comfortably within the context window while giving enough examples to establish clear patterns.
Instruction Diversity Through Prompt Variants
An advanced technique for improving instruction diversity is using multiple prompt templates that frame the generation task differently. Rather than always asking the model to "come up with a series of tasks," you might sometimes ask it to "write instructions for tasks a teacher might assign" or "list tasks that would require looking something up in a reference book." These different framings encourage the model to explore different regions of the instruction space.
The framing effect in large language models is powerful and well-documented. The same underlying knowledge manifests very differently depending on how the question is posed. A model asked to generate "creative writing tasks" will produce different instructions than one asked to generate "analytical reasoning tasks," even though both are drawing on the same pre-trained knowledge. Systematic variation of prompt framing can therefore significantly increase the coverage of the final instruction dataset without requiring additional seed tasks.
Classification Task Identification
After generating instructions, the pipeline determines whether each represents a classification task or an open-ended generation task. This distinction matters because the two task types require different instance generation strategies. Classification and generation tasks have fundamentally different output structures, and treating them identically would lead to suboptimal training data.
Classification tasks have a fixed, limited set of valid outputs (like sentiment labels or categories). The outputs for these tasks are constrained to a small vocabulary of options, and the model's job is essentially to select the correct label. Open-ended tasks can have diverse, creative outputs (like writing stories or answering questions). For these tasks, there is no single "correct" answer, and the space of valid responses is large. The model makes this determination through another prompted generation. Think of this classification step as a routing switch: it sends each instruction down one of two paths, each optimized for a different type of output structure.
The need for explicit task type identification reveals an important property of natural language instructions: they do not always make their expected output type obvious. "Evaluate the quality of the given code" could be a classification task (good/bad/needs improvement) or a generation task (write a detailed review). The Self-Instruct framework resolves this ambiguity explicitly by asking the model to classify each instruction before generating instances. This keeps the subsequent generation strategy is appropriate for the task type.
This classification step also has broader implications for the quality of the training data. Classification examples trained without output-first generation tend to be heavily imbalanced. This reflects the natural distribution of examples rather than a balanced cross-section of labels. Generation examples trained with input-first approaches are more coherent because the input provides clear context for the expected output. By routing tasks through type-specific generation strategies, Self-Instruct produces better-calibrated training data than a one-size-fits-all approach would allow.
def create_classification_prompt(instruction):
"""
Prompt to determine if an instruction represents a classification task.
"""
prompt = f"""Determine whether the following task output is a
fixed set of categories or labels (classification) or free-form text (generation).
Task: {instruction}
If this is a classification task, respond with "Classification".
If this is an open-ended generation task, respond with "Generation".
Answer:"""
return prompt# Example classification decisions
test_instructions = [
"Classify the sentiment of the given review as positive, negative, or neutral.",
"Write a short story about a robot learning to paint.",
"Determine if the given statement is a fact or an opinion.",
"Explain the concept of photosynthesis to a 10-year-old.",
]
# In practice, these would be model outputs
classification_labels = [
"Classification",
"Generation",
"Classification",
"Generation",
]Task type classification: [Classification] Classify the sentiment of the given review as positive, nega... [Generation ] Write a short story about a robot learning to paint.... [Classification] Determine if the given statement is a fact or an opinion.... [Generation ] Explain the concept of photosynthesis to a 10-year-old....
The model correctly identifies the nature of each task, distinguishing between those requiring constrained labels (Classification) and those inviting open-ended text (Generation). Notice how the presence of explicit label options in the instruction (like "positive, negative, or neutral") provides a strong signal for classification tasks, while verbs like "write" or "explain" suggest generation tasks.
The classification distinction guides the next stage of the pipeline. Classification tasks benefit from output-first generation (where you first generate possible labels, then create inputs that would warrant each label). This approach ensures balanced representation across all label categories. Generation tasks work better with input-first approaches (where you create a plausible input, then generate an appropriate output). This order is more natural for open-ended tasks where the input context shapes what constitutes a good response.
Edge Cases and Ambiguous Instructions
Not every instruction falls cleanly into the classification or generation category. Some instructions occupy a middle ground: "Rate the difficulty of the math problem on a scale from 1 to 10" has constrained output values but a continuous rather than categorical output space. "Identify all named entities in the passage" is neither a fixed set of labels nor free-form generation but a structured extraction task. These edge cases require additional handling in production pipelines.
The original Self-Instruct paper simplified this ambiguity by treating any task with a fixed, small output space as classification and everything else as generation. This binary split is pragmatically effective even if it is not theoretically precise. A more sophisticated system might distinguish among classification and regression tasks, along with extraction and generation tasks, applying different instance generation strategies to each. But the additional complexity often does not justify the marginal improvement in training data quality, particularly given the other sources of noise in the pipeline.
For practical implementations, the most important refinement is handling tasks where the output space is small but the valid outputs are not enumerable in advance. Question answering tasks, for example, have open-ended outputs but tend to be short and factual. Treating these as generation tasks with input-first instance generation works reasonably well, even though the resulting instances may lack the label balance that output-first generation achieves. The key is to apply the strategy that produces the most coherent instances for the particular instruction type, even if that means using judgment rather than a strict rule.
Instance Generation
Once instructions are classified, the pipeline generates complete task instances consisting of inputs and outputs. This stage uses one of two strategies depending on the task type. The choice of strategy directly impacts the quality and balance of the resulting training data. Getting this right is what separates a Self-Instruct pipeline that produces useful training data from one that produces plausible-looking but poorly calibrated examples.
Think of instance generation as the difference between writing an exam question by starting with the answer versus starting with the scenario. If you need to write a question whose correct answer is "photosynthesis," you design a scenario that naturally leads to that answer, creating a coherent and well-motivated question. If you start with the scenario instead, you might accidentally write a question whose obvious answer is something other than what you intended. Output-first generation for classification tasks applies exactly this logic: starting with the intended label ensures that the generated input warrants that label.
The dual-strategy approach also helps with coherence in a more subtle way. For generation tasks, the input context determines what constitutes a good output. A summarization task with a specific article as input calls for a summary that accurately reflects that article's content. If the input and output are generated independently, they may be inconsistent with each other: the summary might not match the article. Input-first generation avoids this problem by establishing the context before generating the response, maintaining a natural dependency between the two.
Instance generation is also where the scale of Self-Instruct becomes most apparent. For each instruction in the pipeline, the model must make an additional API call (or inference pass) to generate the instance. This means that for a dataset of 52,000 instructions, the original paper required roughly 52,000 additional model calls just for instance generation, on top of the calls required for instruction generation and task classification. Understanding this computational cost is important for planning large-scale Self-Instruct runs.
Input-First Approach
For open-ended generation tasks, the model first creates a plausible input, then generates the corresponding output. This approach works well when the input constrains what kind of output makes sense: given a specific article to summarize, there are many valid summaries, but they should all reflect the article's content. By generating the input first, we establish a concrete context that grounds the output generation. The causal arrow runs from input to output, just as it would in a real task scenario.
The input-first approach also tends to produce more natural-sounding instances. When a model generates an input and then responds to it, the result has the coherence of a question-and-answer exchange rather than the slightly artificial feeling of a fabricated example. This naturalness is valuable for training because it teaches the model to handle the kinds of inputs that real users provide, rather than abstract or overly formal inputs that would never appear in practice.
def create_input_first_prompt(instruction, demonstrations):
"""
Generate input first, then output.
Used for open-ended generation tasks.
"""
prompt = f"""For the following task, first generate an appropriate input,
then provide the expected output.
Task: {instruction}
"""
# Add demonstrations
for demo in demonstrations:
prompt += f"Input: {demo['input']}\n"
prompt += f"Output: {demo['output']}\n\n"
prompt += "Input:"
return prompt# Example: generating an instance for a summarization task
summarization_instruction = "Summarize the main points of the given article."
# Mock demonstrations
summarization_demos = [
{
"input": "Scientists discovered a new species of deep-sea fish that produces its own light...",
"output": "Researchers found a bioluminescent fish species in deep ocean waters.",
}
]
input_first_prompt = create_input_first_prompt(
summarization_instruction, summarization_demos
)=== Input-First Prompt === For the following task, first generate an appropriate input, then provide the expected output. Task: Summarize the main points of the given article. Input: Scientists discovered a new species of deep-sea fish that produces its own light... Output: Researchers found a bioluminescent fish species in deep ocean waters. Input:
The prompt explicitly instructs the model to generate the input context first, establishing a scenario that makes the subsequent output generation more natural and coherent. The demonstrations show the model what kind of inputs are appropriate for this task type and how outputs should relate to those inputs. This sequential structure mimics how humans would approach such tasks: first understand the context, then produce a response.
Output-First Approach
For classification tasks, generating labels first and then creating matching inputs produces more balanced datasets. This ordering might seem counterintuitive at first, but it addresses an important practical problem. If you generate inputs first, the model might repeatedly create inputs that warrant the same label, leading to imbalanced class distributions. For example, when generating sentiment examples, a model might produce mostly positive reviews simply because they are easier to write or more common in training data.
The output-first approach inverts the natural language production order to serve a data engineering goal. We are not trying to mimic how humans naturally produce text; we are trying to ensure that our training data has specific distributional properties. By explicitly specifying which label to generate an example for, we force the model to construct inputs that warrant that label, and we can cycle through labels systematically to achieve balance. This is a case where the needs of the training data pipeline diverge from the natural flow of language generation, and the design of the instance generation strategy accommodates that divergence.
def create_output_first_prompt(instruction, possible_labels, demonstrations):
"""
Generate output (label) first, then create matching input.
Used for classification tasks to ensure balanced labels.
"""
prompt = f"""For the following classification task, generate an example
for each possible output label.
Task: {instruction}
Possible labels: {", ".join(possible_labels)}
"""
for demo in demonstrations:
prompt += f"Label: {demo['output']}\n"
prompt += f"Input: {demo['input']}\n\n"
# Request a new example
prompt += "Label:"
return prompt# Example: generating instances for sentiment classification
sentiment_instruction = "Classify the sentiment of the given text."
sentiment_labels = ["Positive", "Negative", "Neutral"]
sentiment_demos = [
{
"output": "Positive",
"input": "This product exceeded all my expectations!",
},
{"output": "Negative", "input": "Terrible service, would not recommend."},
]
output_first_prompt = create_output_first_prompt(
sentiment_instruction, sentiment_labels, sentiment_demos
)=== Output-First Prompt === For the following classification task, generate an example for each possible output label. Task: Classify the sentiment of the given text. Possible labels: Positive, Negative, Neutral Label: Positive Input: This product exceeded all my expectations! Label: Negative Input: Terrible service, would not recommend. Label:
The output-first approach cycles through labels, requesting inputs for each one in turn. This ensures roughly equal representation of each class in the final dataset. By explicitly specifying which label to generate an example for, we force the model to create inputs that warrant that label rather than defaulting to the most common or easiest case. The result is a more balanced dataset that trains models to distinguish between all categories effectively.


Filtering Strategies
Raw generated data contains significant noise: duplicate instructions, low-quality outputs, formatting errors, and examples that are too similar to existing ones. Filtering removes these problematic cases before they contaminate the task pool. Without rigorous filtering, the iterative nature of Self-Instruct would cause errors to compound over time, degrading the quality of each successive generation. Think of the filtering stage as the immune system of the pipeline: it cannot prevent all errors from being generated, but it prevents them from taking hold and multiplying.
The filtering pipeline operates across multiple dimensions simultaneously, each catching a different class of problematic data. ROUGE similarity filtering addresses redundancy. This keeps the dataset grows in new directions rather than repeatedly covering the same ground. Length and format filters address structural quality, catching cases where the model's generation drifted from the expected format. Keyword filters address semantic quality, removing instructions that reference capabilities the model lacks or contexts that cannot be reliably completed without additional information.
A necessary design principle in the filtering architecture is that each filter should be fast and cheap. The entire point of Self-Instruct is to automate data creation at scale; if the filters require expensive model calls, the cost savings evaporate. ROUGE similarity computation is a simple string comparison algorithm. Length checking is a word count. Keyword filtering is substring matching. These cheap filters catch the majority of problematic cases before any expensive model-based evaluation is applied. Reserve expensive filtering for edge cases that heuristics cannot handle.
The composition of filters also matters. Applying them in the right order maximizes efficiency. Keyword filtering should come first because it is the cheapest operation: a simple string check that eliminates instructions referencing images or URLs before any further processing. Length filtering comes next, catching structurally invalid instances before the more expensive ROUGE similarity computation. Similarity filtering comes last because it requires comparing against the entire existing pool, which makes it the most computationally intensive operation. This ordering ensures that the expensive operations see only the candidates that have already passed cheaper checks.
ROUGE-Based Similarity Filtering
The most important filter prevents near-duplicate instructions from entering the pool. New instructions that are too similar to existing ones add no diversity and waste annotation budget. If two instructions are nearly identical, including both provides little additional signal during training. Self-Instruct uses ROUGE-L similarity to measure instruction overlap, comparing the longest common subsequence between instruction texts.
ROUGE-L (Recall-Oriented Understudy for Gisting Evaluation, using Longest Common Subsequence) was originally developed as an automatic evaluation metric for text summarization, but it serves a different purpose here: measuring lexical overlap between instructions to detect near-duplicates. The LCS-based approach is more sensitive to word order and phrase-level matching than bag-of-words alternatives, which makes it effective for catching paraphrases and minor rewrites of existing instructions. An instruction like "Summarize the article in three sentences" scores high ROUGE-L against "Provide a three-sentence summary of the article" because both share most of the same key words in similar order.
The ROUGE-L score between two texts and is computed as an F1 measure that combines precision and recall of the longest common subsequence. Let denote the length of the longest common subsequence between the token sequences of and , denote the number of tokens in , and denote the number of tokens in . The score is:
where:
- : the fraction of 's tokens that participate in the longest common subsequence
- : the fraction of 's tokens that participate in the longest common subsequence
- The F1 combination of the two balances the tendency of precision alone to reward short matches and recall alone to reward long matches
Intuitively, a high ROUGE-L score means the two texts share a long sequence of tokens in the same relative order, suggesting they convey similar content. A score close to 1.0 means the texts are nearly identical. A score close to 0.0 means they share very few tokens in common order.
def compute_rouge_l(reference, candidate):
"""
Compute ROUGE-L score between two texts.
ROUGE-L uses longest common subsequence.
"""
ref_tokens = reference.lower().split()
cand_tokens = candidate.lower().split()
# Compute LCS length using dynamic programming
m, n = len(ref_tokens), len(cand_tokens)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if ref_tokens[i - 1] == cand_tokens[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
lcs_length = dp[m][n]
if lcs_length == 0:
return 0.0
# Compute precision, recall, and F1
precision = lcs_length / n if n > 0 else 0
recall = lcs_length / m if m > 0 else 0
if precision + recall == 0:
return 0.0
f1 = 2 * precision * recall / (precision + recall)
return f1def filter_by_similarity(new_instruction, existing_instructions, threshold=0.7):
"""
Check if a new instruction is too similar to existing ones.
Returns True if the instruction should be kept (is sufficiently novel).
"""
for existing in existing_instructions:
similarity = compute_rouge_l(existing, new_instruction)
if similarity > threshold:
return False
return True# Test similarity filtering
existing = [
"Summarize the given article in three sentences.",
"Translate the text from English to French.",
"Write a poem about nature.",
]
candidates = [
"Summarize the given paragraph in three sentences.", # Too similar
"Write a haiku about the ocean.", # Different enough
"Classify the sentiment of the review.", # Novel
]
filter_results = []
for cand in candidates:
is_novel = filter_by_similarity(cand, existing)
# Find the most similar existing instruction
max_sim = max(compute_rouge_l(e, cand) for e in existing)
filter_results.append((cand, is_novel, max_sim))Similarity filtering results (threshold=0.7): ---------------------------------------------------------------------- [REJECT] (sim=0.86) Summarize the given paragraph in three sentences. [KEEP ] (sim=0.55) Write a haiku about the ocean. [KEEP ] (sim=0.15) Classify the sentiment of the review.
The threshold of 0.7 ROUGE-L similarity balances diversity against strictness. Lower thresholds enforce more diversity but risk rejecting valid variations that might provide useful training signal. Higher thresholds allow more similar instructions, potentially reducing dataset diversity. The choice of 0.7 reflects empirical testing showing it effectively removes near-duplicates while accepting distinct instructions. An instruction that scores 0.71 against an existing task is likely a minor rewrite; one that scores 0.65 is typically different enough to contribute novel information.

Length and Format Filtering
Simple heuristics catch many problematic generations without requiring complex analysis. These filters check basic structural properties that valid instructions should satisfy. A well-formed instruction must be long enough to communicate a meaningful task but short enough to be actionable without becoming a paragraph-length specification. Outputs must exist and be within a reasonable length range. These seemingly obvious constraints catch a surprising number of failures in practice, because language models occasionally generate degenerate outputs: a single word, an incomplete sentence, or a multi-page stream-of-consciousness.
Length filters also enforce indirect quality signals. Instructions shorter than three words are almost certainly too vague to be useful: "Write." or "Explain something." provide no meaningful task specification. Instructions longer than 150 words are typically compound tasks that would be better split into separate, more focused instructions. Outputs longer than 1000 words suggest the model has generated a lengthy text in response to what should have been a shorter task, possibly because the prompt was misinterpreted or the model entered a generation loop.
def apply_length_filters(instruction, input_text, output_text):
"""
Apply length-based quality filters.
Returns (is_valid, reason) tuple.
"""
# Instruction should be meaningful but not excessively long
inst_words = len(instruction.split())
if inst_words < 3:
return False, "Instruction too short"
if inst_words > 150:
return False, "Instruction too long"
# Output should exist and be reasonable length
if not output_text.strip():
return False, "Empty output"
out_words = len(output_text.split())
if out_words > 1000:
return False, "Output too long"
# Input can be empty (for tasks like "Write a poem about...")
# but shouldn't be excessively long
if input_text:
inp_words = len(input_text.split())
if inp_words > 500:
return False, "Input too long"
return True, "Passed"def apply_format_filters(instruction, input_text, output_text):
"""
Filter based on formatting issues.
"""
# Check for incomplete generations (common failure mode)
if output_text.rstrip().endswith("..."):
return False, "Incomplete output"
# Filter obvious repetitions
words = output_text.lower().split()
if len(words) > 10:
unique_ratio = len(set(words)) / len(words)
if unique_ratio < 0.3:
return False, "Too much repetition"
# Check instruction doesn't start with prohibited phrases
prohibited_starts = [
"write a program", # Often leads to code that can't be verified
"create a code",
"as an ai",
"i cannot",
]
inst_lower = instruction.lower()
for phrase in prohibited_starts:
if inst_lower.startswith(phrase):
return False, f"Prohibited start: {phrase}"
return True, "Passed"# Test filtering pipeline
test_instances = [
{
"instruction": "Summarize the article.",
"input": "Long article text here...",
"output": "This article discusses...",
},
{
"instruction": "Hi", # Too short
"input": "",
"output": "Hello",
},
{
"instruction": "Write a poem about rain.",
"input": "",
"output": "the the the the the the the the the the the", # Repetitive
},
{
"instruction": "Explain quantum computing.",
"input": "",
"output": "", # Empty output
},
]
filter_outcomes = []
for inst in test_instances:
length_ok, length_reason = apply_length_filters(
inst["instruction"], inst["input"], inst["output"]
)
format_ok, format_reason = apply_format_filters(
inst["instruction"], inst["input"], inst["output"]
)
if not length_ok:
filter_outcomes.append(
(inst["instruction"][:40], "REJECT", length_reason)
)
elif not format_ok:
filter_outcomes.append(
(inst["instruction"][:40], "REJECT", format_reason)
)
else:
filter_outcomes.append(
(inst["instruction"][:40], "KEEP", "All filters passed")
)Filter results: ---------------------------------------------------------------------- [REJECT] Summarize the article. | Incomplete output [REJECT] Hi | Instruction too short [REJECT] Write a poem about rain. | Too much repetition [REJECT] Explain quantum computing. | Empty output
The filters successfully accept valid instructions while rejecting those that are too short, repetitive, or empty, preventing low-quality data from polluting the pool. These simple checks are computationally cheap but catch a surprising number of problematic cases, making them an efficient first line of defense. The repetition filter in particular catches a common failure mode where models fall into generation loops, repeating the same word or phrase indefinitely. These outputs look valid superficially (they have the right format) but would provide no training signal.
Keyword-Based Filtering
Certain keywords indicate problematic instructions that should be excluded. These include self-references, meta-commentary, and requests for capabilities the model lacks. Keyword filtering acts as a semantic safety net, catching issues that structural filters would miss. An instruction that passes all length and format checks can still be semantically invalid: "Describe the image shown above" has a perfectly reasonable length and format but cannot be completed by a text-only model.
The keyword list reflects the types of instructions that appear reasonable in isolation but become problematic in the context of a text-only instruction-following dataset. References to images and audio, as well as video indicate multimodal tasks that cannot be completed with text alone. URLs and links indicate tasks that require accessing external resources, which the model cannot do at inference time. References to specific model names or previous conversations indicate tasks that assume context not present in the instruction-input-output structure. Filtering all these cases ensures that every instance in the final dataset represents a self-contained, completable task.
BLOCKED_KEYWORDS = [
"image",
"images",
"picture",
"graph",
"figure", # Visual content
"video",
"audio",
"voice", # Non-text modalities
"http",
"www",
"link",
"url", # External resources
"gpt",
"chatgpt",
"openai",
"anthropic", # Self-reference
"previous conversation",
"our earlier", # Context dependency
]
def filter_by_keywords(instruction):
"""
Filter instructions containing problematic keywords.
"""
inst_lower = instruction.lower()
for keyword in BLOCKED_KEYWORDS:
if keyword in inst_lower:
return False, f"Contains blocked keyword: {keyword}"
return True, "Passed"keyword_test_cases = [
"Describe what you see in the image.",
"Summarize the main points of the article.",
"Based on our previous conversation, continue the story.",
"Explain the theory of relativity.",
]
keyword_results = [filter_by_keywords(inst) for inst in keyword_test_cases]Keyword filter results: [REJECT] Describe what you see in the image.... | Contains blocked keyword: image [KEEP ] Summarize the main points of the article.... | Passed [REJECT] Based on our previous conversation, continue the s... | Contains blocked keyword: previous conversation [KEEP ] Explain the theory of relativity.... | Passed
These results demonstrate that the keyword filter effectively catches blocked terms like "image" or context-dependent phrases that would be inappropriate for a standalone instruction dataset. Instructions referencing images cannot be completed by text-only models, and those depending on previous conversation lack the necessary context. By filtering these early, we avoid generating instances that would be useless or misleading during training.
The keyword list requires careful maintenance as the instruction domain expands. A list designed for a general-purpose instruction dataset may need extensions when applying Self-Instruct to specialized domains. A medical instruction dataset might block terms like "examine the X-ray" or "review the MRI scan." A coding instruction dataset might relax the prohibition on code-related terms while adding blocks for instructions that require running external programs. The core principle, blocking instructions that cannot be reliably completed within the constraints of the dataset format, remains constant across domains.

Worked Example: End-to-End Self-Instruct Run
To solidify the concepts, let us trace through a complete mini Self-Instruct run step by step, with concrete numbers and decisions at each stage. We will use three seed tasks and run one full iteration of the pipeline, tracking every decision from instruction generation through final acceptance or rejection.
Setup. We begin with three seed tasks:
- "Classify the sentiment of the movie review." (input: "The film was boring.", output: "Negative")
- "Write a haiku about spring." (input: "", output: "Cherry blossoms fall / Soft petals on the warm breeze / New life just begins")
- "Convert 100 degrees Fahrenheit to Celsius." (input: "100 F", output: "37.78 C")
We sample all three as demonstrations and prompt the model to generate new instructions.
Step 1: Instruction Generation. The model receives a prompt showing the three seed tasks and produces the following candidates:
- "4. Determine if the given statement is true or false."
- "5. Write a short poem about autumn."
- "6. Translate the phrase to German."
- "7. Classify the given news headline as sports, politics, or entertainment."
Step 2: Similarity Filtering. We compute ROUGE-L scores between each candidate and all existing instructions. The scores for "Write a short poem about autumn" against our haiku instruction are:
The two instruction strings are "Write a haiku about spring" (reference, 5 tokens) and "Write a short poem about autumn" (candidate, 6 tokens).
The longest common subsequence of these two token lists is ["Write", "a", "about"] with length 3. We compute:
where:
- is the LCS length (tokens "Write", "a", "about" appear in both instructions in the same relative order)
- is the number of tokens in the candidate instruction
- is the number of tokens in the reference instruction
A score of 0.545 is below our threshold of 0.7, so "Write a short poem about autumn" passes the similarity filter. If the candidate had been "Write a haiku about autumn," the LCS would be ["Write", "a", "haiku", "about"] with length 4, giving ROUGE-L of approximately 0.86, which would trigger rejection. The other three candidates score below 0.3 against all existing instructions and pass easily.
Step 3: Task Classification. We prompt the model to classify each passing candidate:
- "Determine if the given statement is true or false." → Classification (binary labels: true/false)
- "Write a short poem about autumn." → Generation
- "Translate the phrase to German." → Generation
- "Classify the given news headline as sports, politics, or entertainment." → Classification
Step 4: Instance Generation. For each passing instruction, we apply the appropriate strategy:
For "Classify the given news headline as sports, politics, or entertainment" (output-first): We first request a "Sports" example. The model generates: Label: Sports, Input: "Local team wins championship in overtime thriller." We then request "Politics": Label: Politics, Input: "Senate votes on new infrastructure bill." Finally "Entertainment": Label: Entertainment, Input: "Pop star announces world tour dates."
For "Write a short poem about autumn" (input-first): There is no separate input needed for this task (it is an instruction-only task), so the model generates only an output. The model produces: "Golden leaves descend / The earth prepares for its rest / Quiet beauty fades."
Step 5: Validation. All four instances pass length and format filters. The final accepted instances join the task pool, growing it from 3 to 7 tasks. In the next iteration, all 7 tasks are available as demonstration candidates. This provides richer context for the next round of instruction generation.
This walkthrough reveals the compounding nature of the pipeline: each accepted task makes future generations richer. After 100 such iterations, the pool contains hundreds of tasks spanning many domains, and the model's demonstrations at each step reflect that accumulated diversity.
Complete Self-Instruct Implementation
Let us bring together all components into a working implementation that demonstrates the full pipeline. For demonstration purposes, we use a mock language model, but the structure mirrors how you would integrate with real APIs. The modular design makes it straightforward to swap in actual model calls when deploying this approach in practice:
import random
class MockLanguageModel:
"""
Simulates LLM responses for demonstration.
In practice, replace with API calls to GPT-3, LLaMA, etc.
"""
def __init__(self):
self.instruction_templates = [
"Rewrite the given sentence in passive voice.",
"Identify the main theme of the passage.",
"List the pros and cons of the given topic.",
"Explain the concept to a five-year-old.",
"Write a professional email about the topic.",
"Compare and contrast the two given items.",
"Provide three examples of the given concept.",
"Summarize the key takeaways from the text.",
"Correct the grammatical errors in the sentence.",
"Generate a creative title for the story.",
"Classify the text as formal or informal.",
"Extract the named entities from the sentence.",
"Paraphrase the following paragraph.",
"Write a brief description of the product.",
"Determine the tone of the message.",
]
def generate(self, prompt: str, max_tokens: int = 200) -> str:
"""Generate response based on prompt type."""
if "Come up with a series of tasks" in prompt:
return self._generate_instructions()
elif "Input:" in prompt:
return self._generate_instance()
elif "determine whether" in prompt.lower():
return random.choice(["Classification", "Generation"])
else:
return "Sample output for the given input."
def _generate_instructions(self) -> str:
selected = random.sample(self.instruction_templates, 5)
return "\n".join(f"{i + 4}. {inst}" for i, inst in enumerate(selected))
def _generate_instance(self) -> str:
inputs = [
"The cat sat on the mat.",
"Climate change affects biodiversity.",
"Machine learning transforms industries.",
]
outputs = [
"The mat was sat on by the cat.",
"Climate change has significant impacts on biodiversity.",
"Industries are being transformed by machine learning.",
]
idx = random.randint(0, len(inputs) - 1)
# Include Input: prefix so parser can find it
return f"Input: {inputs[idx]}\nOutput: {outputs[idx]}"import random
from typing import Dict, List
class SelfInstructGenerator:
"""
Complete Self-Instruct implementation.
Generates new instruction-tuning examples by leveraging
an existing language model to create diverse tasks.
"""
def __init__(
self, seed_tasks: List[Dict], model, similarity_threshold: float = 0.7
):
self.task_pool = list(seed_tasks)
self.model = model
self.similarity_threshold = similarity_threshold
self.generated_instructions = set()
# Track existing instructions for similarity checking
for task in seed_tasks:
self.generated_instructions.add(task["instruction"].lower())
def generate_batch(self, batch_size: int = 10) -> List[Dict]:
"""Generate a batch of new task instances."""
new_tasks = []
attempts = 0
max_attempts = batch_size * 3 # Allow some failures
while len(new_tasks) < batch_size and attempts < max_attempts:
attempts += 1
# Step 1: Generate new instructions
sampled = random.sample(self.task_pool, min(3, len(self.task_pool)))
prompt = create_instruction_generation_prompt(sampled)
raw_output = self.model.generate(prompt)
instructions = parse_generated_instructions(raw_output)
for instruction in instructions:
# Step 2: Apply filters
if not self._passes_filters(instruction):
continue
# Step 3: Classify task type
is_classification = self._classify_task(instruction)
# Step 4: Generate instance
instance = self._generate_instance(
instruction, is_classification
)
if instance and self._validate_instance(instance):
new_tasks.append(instance)
self.generated_instructions.add(instruction.lower())
if len(new_tasks) >= batch_size:
break
return new_tasks
def _passes_filters(self, instruction: str) -> bool:
"""Check if instruction passes all filters."""
# Similarity filter
if not filter_by_similarity(
instruction,
list(self.generated_instructions),
self.similarity_threshold,
):
return False
# Keyword filter
passed, _ = filter_by_keywords(instruction)
if not passed:
return False
# Length check
if len(instruction.split()) < 3 or len(instruction.split()) > 150:
return False
return True
def _classify_task(self, instruction: str) -> bool:
"""Determine if task is classification."""
prompt = create_classification_prompt(instruction)
response = self.model.generate(prompt, max_tokens=20)
return "classification" in response.lower()
def _generate_instance(
self, instruction: str, is_classification: bool
) -> Dict:
"""Generate input-output pair for instruction."""
# Use demonstrations from pool
demos = random.sample(self.task_pool, min(2, len(self.task_pool)))
if is_classification:
# Output-first for classification
prompt = create_output_first_prompt(
instruction,
["Yes", "No"], # Simplified for demo
demos,
)
else:
# Input-first for generation
prompt = create_input_first_prompt(instruction, demos)
response = self.model.generate(prompt)
# Parse response to extract input and output
return self._parse_instance(instruction, response)
def _parse_instance(self, instruction: str, response: str) -> Dict:
"""Parse model response into structured instance."""
# Simple parsing - production code would be more reliable
input_text = ""
output_text = ""
if "Input:" in response and "Output:" in response:
parts = response.split("Output:")
input_part = parts[0]
output_text = parts[1].strip() if len(parts) > 1 else ""
if "Input:" in input_part:
input_text = input_part.split("Input:")[1].strip()
else:
output_text = response.strip()
return {
"instruction": instruction,
"input": input_text,
"output": output_text,
}
def _validate_instance(self, instance: Dict) -> bool:
"""Final validation of complete instance."""
length_ok, _ = apply_length_filters(
instance["instruction"], instance["input"], instance["output"]
)
format_ok, _ = apply_format_filters(
instance["instruction"], instance["input"], instance["output"]
)
return length_ok and format_ok# Run Self-Instruct generation
model = MockLanguageModel()
generator = SelfInstructGenerator(seed_tasks, model, similarity_threshold=0.7)
# Generate a small batch
generated_batch = generator.generate_batch(batch_size=5)Generated 5 new task instances: ============================================================ --- Task 1 --- Instruction: List the pros and cons of the given topic. Input: The cat sat on the mat. Output: The mat was sat on by the cat. --- Task 2 --- Instruction: Extract the named entities from the sentence. Input: The cat sat on the mat. Output: The mat was sat on by the cat. --- Task 3 --- Instruction: Identify the main theme of the passage. Input: Machine learning transforms industries. Output: Industries are being transformed by machine learning. --- Task 4 --- Instruction: Classify the text as formal or informal. Input: Machine learning transforms industries. Output: Industries are being transformed by machine learning. --- Task 5 --- Instruction: Correct the grammatical errors in the sentence. Input: Machine learning transforms industries. Output: Industries are being transformed by machine learning.
The mock implementation demonstrates the full pipeline structure. In a production deployment, you replace MockLanguageModel with an actual API client, adjust the filtering thresholds based on empirical analysis of your generated data, and add logging and checkpointing to enable long-running generation jobs to resume after interruptions. The modular design of the SelfInstructGenerator class makes these substitutions straightforward.
One practical consideration when deploying Self-Instruct with real models is batching API calls efficiently. Instead of making one API call per instruction, you can group multiple instructions into a single prompt and ask the model to generate instances for all of them at once. This reduces the number of API calls by an order of magnitude and often improves consistency because the model can see the range of instructions it is being asked to complete. The trade-off is a more complex parsing step, since the model's output now contains multiple instruction-output pairs that must be correctly attributed to their respective instructions.
Quality and Diversity Metrics
Evaluating Self-Instruct outputs requires measuring both quality and diversity. A large dataset of repetitive instructions is no better than a small, diverse one. In fact, redundant data wastes computational resources during training and may cause the model to overfit to particular patterns. Several metrics help us understand whether our generation pipeline is creating useful training data, and they provide actionable signals for improving the pipeline when quality falls short.
The tension between scale and quality is a recurring theme in synthetic data generation. Self-Instruct makes it easy to generate hundreds of thousands of examples, but raw volume does not guarantee training value. A dataset of 100,000 near-identical sentiment classification examples will produce a model that performs well on sentiment classification and poorly on everything else. Measuring diversity across multiple dimensions, not just instruction count, is needed for understanding what a Self-Instruct dataset can teach a model.
Quality and diversity are not independent dimensions. High-quality instructions that are all variations on the same theme add diversity in name only. Diverse instructions that are poorly formed add variety in topic but fail to provide clear training signal. The best Self-Instruct datasets combine both: they are diverse in topic, task type, and output structure, and every individual instance is high quality in the sense that the instruction is clear, the input (when present) is relevant, and the output correctly responds to the instruction.
Tracking metrics over the course of a Self-Instruct run also reveals important dynamics. Diversity metrics typically peak in early iterations when the pool is small and every new instruction is novel, then gradually decline as the pool saturates and new instructions are increasingly variations on existing themes. Monitoring this trend helps you decide when to stop the generation process, adjust the filtering threshold, or inject new seed tasks to reopen the exploration frontier.
Diversity Metrics
Diversity can be measured at multiple levels, from individual word choices to overall structural patterns. Each metric captures a different aspect of what makes a dataset varied and complete:
def compute_diversity_metrics(tasks: List[Dict]) -> Dict:
"""
Compute diversity metrics for a task collection.
"""
instructions = [t["instruction"] for t in tasks]
# Vocabulary diversity
all_words = []
for inst in instructions:
all_words.extend(inst.lower().split())
vocab_size = len(set(all_words))
total_words = len(all_words)
type_token_ratio = vocab_size / total_words if total_words > 0 else 0
# Root word diversity (approximation via first word)
verb_starts = [inst.split()[0].lower() for inst in instructions if inst]
unique_verbs = len(set(verb_starts))
verb_diversity = unique_verbs / len(verb_starts) if verb_starts else 0
# Length diversity
lengths = [len(inst.split()) for inst in instructions]
length_std = (
sum((l - sum(lengths) / len(lengths)) ** 2 for l in lengths)
/ len(lengths)
) ** 0.5
return {
"vocabulary_size": vocab_size,
"type_token_ratio": type_token_ratio,
"unique_verb_starts": unique_verbs,
"verb_diversity": verb_diversity,
"avg_instruction_length": sum(lengths) / len(lengths),
"length_std": length_std,
}# Combine seed tasks with generated ones for analysis
all_tasks = seed_tasks + generated_batch
diversity_metrics = compute_diversity_metrics(all_tasks)Diversity Metrics: ---------------------------------------- vocabulary_size: 52 type_token_ratio: 0.693 unique_verb_starts: 8 verb_diversity: 0.800 avg_instruction_length: 7.500 length_std: 1.118
These metrics provide a quantitative view of the dataset's richness. A type-token ratio above 0.5 and high verb diversity suggest the instructions cover a wide range of actions and topics, rather than repeating the same few patterns. The length standard deviation indicates whether the dataset includes both short, focused instructions and longer, more detailed ones. Together, these measurements give a combined picture of diversity that helps guide further data generation or filtering decisions. When these metrics start declining over successive Self-Instruct iterations, it is a signal that the generation process is saturating and additional seed tasks or domain expansion may be needed.

Pairwise Similarity Distribution
The distribution of pairwise similarities reveals whether the dataset has clusters of similar instructions or maintains broad diversity. This analysis goes beyond simple metrics to show the actual structure of relationships within the dataset. A dataset where 80 percent of instruction pairs score near zero and only 5 percent score above 0.5 is much more diverse than one where pairs cluster around 0.4. The shape of the distribution, not just its mean, tells the story of dataset coverage.
Pairwise similarity analysis is particularly useful for diagnosing problems with the generation process. If the distribution shows a secondary peak near the filter threshold (0.7), it suggests that many nearly-duplicate instructions are entering the pool just under the rejection cutoff. Tightening the threshold or diversifying the seed tasks can address this issue. If the distribution shows an unusually large peak near 0.0, it might indicate that the model is generating instructions that are syntactically diverse but semantically similar, a subtler form of redundancy that ROUGE-L cannot detect.
def compute_pairwise_similarities(tasks):
"""
Compute all pairwise ROUGE-L similarities between instructions.
"""
instructions = [t["instruction"] for t in tasks]
similarities = []
for i in range(len(instructions)):
for j in range(i + 1, len(instructions)):
sim = compute_rouge_l(instructions[i], instructions[j])
similarities.append(sim)
return similaritiespairwise_sims = compute_pairwise_similarities(all_tasks)
A well-functioning Self-Instruct pipeline produces a left-skewed distribution where most instruction pairs have low similarity. This pattern indicates that the dataset contains many distinct instructions rather than variations on a few themes. The similarity threshold (typically 0.7) prevents the right tail from growing with near-duplicate instructions, maintaining the overall diversity of the pool.
Scaling Self-Instruct
The original Self-Instruct paper generated over 52,000 instructions using GPT-3, starting from just 175 seed tasks. This dramatic expansion demonstrates the power of the bootstrapping approach: minimal human effort yields a large-scale dataset. However, the iterative process shows diminishing returns: early iterations add many novel instructions, while later iterations increasingly generate duplicates or near-duplicates that get filtered out.
The scaling dynamics of Self-Instruct reflect a basic property of any corpus-based generation approach. The model generates instructions by drawing on patterns observed in pre-training. These patterns are large but finite. As the pool grows, the probability that a newly generated instruction is novel relative to all existing instructions decreases. Eventually, the generation process effectively exhausts the novel combinations that the model can produce for a given set of seed demonstrations, and further iterations produce mostly filtered-out duplicates.
Understanding this saturation dynamic is important for planning Self-Instruct runs. If you need 50,000 diverse instructions, you should expect to generate and filter several times that number of candidates, and you should plan for the per-iteration acceptance rate to decline significantly over the course of the run. Starting with more diverse seeds, using multiple model variants, or periodically injecting domain-specific seed tasks can extend the useful range of the pipeline before saturation sets in.
from typing import List, Tuple
def simulate_scaling_curve(
initial_pool_size: int = 175,
target_size: int = 5000,
acceptance_rate_decay: float = 0.95,
) -> List[Tuple[int, int, float]]:
"""
Simulate how acceptance rate changes as pool grows.
As the pool grows, new instructions are more likely to be
similar to existing ones, reducing acceptance rate.
"""
pool_size = initial_pool_size
history = [(0, pool_size, 1.0)]
iteration = 0
acceptance_rate = 1.0
while pool_size < target_size:
iteration += 1
# Acceptance rate decays as pool grows
acceptance_rate = acceptance_rate_decay ** (
pool_size / initial_pool_size
)
# Generate batch of candidates
candidates = 100
accepted = int(candidates * acceptance_rate)
pool_size += accepted
history.append((iteration, pool_size, acceptance_rate))
return historyscaling_history = simulate_scaling_curve()

The acceptance rate decay creates a natural ceiling on dataset size. As more instructions enter the pool, the probability that any new generation is sufficiently different from all existing instructions decreases. This phenomenon reflects a basic constraint: the space of "useful, distinct instructions" is large but finite for any given domain and prompt structure.
To push beyond this ceiling, you can employ several strategies that help explore new regions of the instruction space.
- Expanding seed diversity: Adding more diverse seed tasks opens new instruction spaces. If seeds cover a new domain or task type, the model can generate variations in that direction.
- Topic-constrained generation: Prompting for instructions in specific underrepresented domains. By explicitly asking for instructions about particular subjects, you can fill gaps in coverage.
- Relaxing similarity thresholds: Trading some diversity for volume (with quality trade-offs). This allows more similar instructions through but requires careful evaluation of the resulting dataset.
- Multiple model sources: Using different models to generate instructions, each with its own biases. Different models may explore different parts of the instruction space, leading to greater overall diversity.

Downstream Effects on Model Quality
The relationship between Self-Instruct dataset properties and downstream model quality is not always straightforward. Naive intuition suggests that more data always helps, but the Self-Instruct experience shows that diversity and quality matter more than raw count. Models trained on 10,000 diverse, well-filtered instructions consistently outperform models trained on 100,000 instructions that include significant redundancy and noise.
The original Self-Instruct paper evaluated downstream performance using both automatic metrics (SUPER-NATURALINSTRUCTIONS benchmark) and human evaluation. The results showed that GPT3-Self-Instruct achieved competitive performance with InstructGPT despite using no human-written instruction-output pairs in its fine-tuning data. This result was surprising to the community because it suggested that the quality gap between human-annotated and model-generated data was smaller than expected, at least for general instruction-following tasks.
Subsequent work has refined this picture. Self-Instruct data tends to perform well on tasks that are well-represented in pre-training data and for which the model can generate plausible instances. It tends to underperform on tasks requiring specialized knowledge, precise factual accuracy, or complex multi-step reasoning. For these harder tasks, human annotation or more sophisticated synthetic data techniques provide better training signal. Understanding this performance profile helps you decide when Self-Instruct is sufficient and when to invest in higher-quality data generation.
Limitations and Impact
Self-Instruct democratized instruction tuning by eliminating the need for large human annotation budgets. Before Self-Instruct, creating instruction-following models required either access to proprietary datasets (like InstructGPT's human demonstrations) or significant manual effort. After Self-Instruct, you could bootstrap instruction data from any sufficiently capable language model. This shift opened up instruction tuning research to a much broader community. Graduate students, small research groups, and organizations without large annotation budgets could now build instruction-following models using only compute resources, which are much more widely accessible than skilled human annotators.
The approach demonstrated that language models contain latent instruction-following knowledge from pre-training that can be accessed through carefully structured prompting and filtering. This insight influenced subsequent work on prompting strategies and synthetic data generation, showing that creative use of existing capabilities can substitute for expensive data collection. The Alpaca model, which applied Self-Instruct to LLaMA using just 52,000 examples generated at a cost of less than $500 in API fees, demonstrated that capable instruction-following models were within reach for individual researchers and small teams.
However, Self-Instruct has important limitations that you must understand before deploying it in production systems. The generated data inherits biases and errors from the source model. If the model has misconceptions about certain topics, those misconceptions propagate into the training data. A model that believes a historical event occurred in the wrong year will generate training examples that teach that incorrect date. The iterative nature can amplify these issues: errors in early generations influence later generations, creating feedback loops that entrench problematic patterns. A model trained on this data will then generate even more confident but incorrect statements, potentially further polluting any data generated from it in a subsequent round of Self-Instruct.
Quality control remains the most significant unsolved challenge in Self-Instruct pipelines. While heuristic filters catch obvious problems (length issues, duplicates, formatting errors), they cannot verify factual accuracy or catch subtle logical errors. A Self-Instruct dataset might contain confident but incorrect explanations that a human annotator would catch immediately. Consider an instruction like "Explain why the Great Wall of China is visible from space" with an output that accepts the premise uncritically rather than correcting the common misconception. The output has the right length and format with superficial coherence, but it teaches a factually wrong claim. This limitation means that Self-Instruct data often requires additional human review for applications where factual accuracy is necessary.
The approach also struggles with task types that require creative judgment or specialized knowledge. Generated instructions tend to cluster around patterns the model has seen frequently during pre-training. Truly novel task formulations, or tasks requiring deep domain expertise in fields like law, medicine, or advanced mathematics, rarely emerge from the self-instruct process. The model essentially remixes what it knows rather than inventing fundamentally new concepts. If you need a dataset that covers rare but important edge cases, Self-Instruct alone is unlikely to generate sufficient coverage.
Finally, there are concerns about model collapse, a phenomenon where models trained on synthetic data from other models progressively lose capability or diversity across successive training generations. Training exclusively on Self-Instruct data without human-quality checks can lead to models that generate plausible-sounding but degraded outputs, particularly for tasks that require fine-grained judgment. The theoretical analysis of model collapse suggests that even small amounts of synthetic data contamination can have disproportionate effects on model capability over time. Mixing Self-Instruct data with human-annotated examples, even at a relatively small ratio, appears to maintain quality and prevent the worst forms of capability degradation.
Despite these limitations, Self-Instruct remains influential and widely used. It established synthetic data generation as a viable paradigm for instruction tuning and paved the way for more sophisticated approaches. Evol-Instruct (used in WizardLM) extends the idea by asking the model to progressively rewrite instructions to make them harder and more complex. Orca and its successors combine Self-Instruct-style generation with system prompts that encourage richer reasoning traces. These techniques, which we will encounter when discussing instruction format evolution in the next chapter, all build directly on the foundation that Self-Instruct established: the idea that a model's own generation capabilities, carefully guided and filtered, can produce training data good enough to improve that model.
Summary
Self-Instruct enables language models to generate their own instruction-tuning data through an iterative bootstrapping process. Starting with a small set of human-written seed tasks, the pipeline generates new instructions, classifies them as classification or generation tasks, creates input-output instances, and filters for quality before adding them to the growing task pool.
The key components of the pipeline include the following.
- Instruction generation using in-context learning with sampled demonstrations, mixing human seeds with machine-generated tasks for quality anchoring
- Task classification to determine whether output-first or input-first instance generation is appropriate, so balanced label distributions for classification tasks and coherent input-output pairs for generation tasks
- Instance generation strategies tailored to task type, using output-first approaches for classification and input-first approaches for generation
- Multi-stage filtering using ROUGE-L similarity, length constraints, format checks, and keyword blocking to prevent noise from accumulating in the pool
Diversity emerges from careful prompt design and the iterative nature of the process, though acceptance rates decay as the pool grows and novel instructions become harder to generate. The approach trades some quality for scale, which makes it suitable for bootstrapping but often requiring human curation for production use in high-stakes applications.
Self-Instruct demonstrated that large language models contain substantial instruction-following capability that can be activated through the right training data, even when that data is generated by the model itself. This insight, that models can help supervise their own training data creation, became a foundational principle of modern instruction tuning and continues to shape how researchers and practitioners approach the challenge of building capable AI assistants without unlimited human annotation budgets.
Quiz
Ready to test your understanding? Take this quick quiz to reinforce what you've learned about Self-Instruct and bootstrapping instruction-tuning datasets.
Self-Instruct
Reference
Citation details
Cite or share this article.
Continue with the full handbook
This chapter is part of Language AI Handbook. Use the handbook page to browse the complete table of contents and continue reading in sequence.
Explore Language AI HandbookStay up to date
Get articles, book updates, and news delivered to your inbox.
No spam, unsubscribe anytime.
Join the community
Sign in to remove popups, track your reading progress, and join the discussion.

Comments
No comments yet. Be the first to share your thoughts!