Instruction Data Creation › Quality Filtering

Michael BrenndoerferDecember 16, 202516 min read
Language AI HandbookAll chapters
Part 36 of 67Instruction Tuning
Chapter 267Instruction Data Creation › Quality Filtering
Reading section 5 of 9 · Quality Filtering · 16 minSection 5 of 9 · 16 min

Book contents

Section 5 of 9 · 16 min read

Instruction Data Creation › Quality FilteringOne section · Sections in this chapter5 / 9 ↓
Reading tools Definitions & inline links

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

Whether data comes from human annotation, template conversion, or seed expansion, quality filtering is essential. Raw data inevitably contains errors and duplicates, along with low-quality examples that can harm model training. A systematic filtering pipeline removes these problems while preserving valuable examples. The goal is to maximize the signal-to-noise ratio in the training data. This keeps every example the model sees contributes positively to its learning.

Think of quality filtering as the editorial process for a textbook. Even if every chapter is written by a qualified expert, the final textbook benefits from editing to remove redundancy, fix errors, standardize formatting, and improve clarity. The editing process does not diminish the textbook; it makes it more effective as a teaching tool. Quality filtering serves the same function for instruction datasets: it turns a collection of examples with mixed quality into a coherent, high-quality training set.

Quality filtering operates on several levels simultaneously. Some problems are structural (the instruction is too short to be meaningful), some are semantic (the output does not answer the instruction), and some are relational (this example is nearly identical to one already in the dataset). A complete filtering pipeline addresses all three levels, using different techniques suited to each type of problem. Applying only one level of filtering leaves the others unaddressed, producing a dataset that may look clean on surface metrics while still containing significant problems.

Length and Format Filters

Basic heuristics catch many obvious problems. These filters are fast to compute and can be applied to millions of examples without significant computational cost. While simple, they remove a substantial portion of problematic data before more expensive filtering stages. Running cheap filters first and expensive filters later is a key design principle for production-scale filtering pipelines, because it reduces the number of examples that need to pass through the costly stages.

In[20]:
Code
def apply_basic_filters(example: dict) -> tuple:
    """Apply basic quality filters to an instruction example.

    Returns (passed: bool, rejection_reason: str or None)
    """
    instruction = example.get("instruction", "")
    output = example.get("output", "")

    # Filter 1: Instruction too short
    if len(instruction.split()) < 3:
        return False, "instruction_too_short"

    # Filter 2: Output too short (might indicate incomplete response)
    if len(output.split()) < 2:
        return False, "output_too_short"

    # Filter 3: Output too long (might indicate runaway generation)
    if len(output.split()) > 2000:
        return False, "output_too_long"

    # Filter 4: Instruction is just a single word repeated
    words = instruction.lower().split()
    if len(set(words)) == 1:
        return False, "repetitive_instruction"

    # Filter 5: Output contains error markers
    error_markers = [
        "I cannot",
        "I'm not able to",
        "Error:",
        "undefined",
        "NaN",
    ]
    if any(marker.lower() in output.lower() for marker in error_markers):
        return False, "error_in_output"

    # Filter 6: Instruction-output mismatch (output repeats instruction verbatim)
    if instruction.lower().strip() == output.lower().strip():
        return False, "output_copies_instruction"

    return True, None


# Test examples
test_examples = [
    {
        "instruction": "Hi",
        "output": "Hello! How can I help you today?",
    },  # Too short instruction
    {"instruction": "Write a poem", "output": ""},  # Empty output
    {
        "instruction": "Explain quantum computing",
        "output": "Quantum computing uses quantum mechanical phenomena...",
    },  # Good
    {
        "instruction": "What is 2+2?",
        "output": "I cannot answer mathematical questions.",
    },  # Error marker
    {
        "instruction": "Say hello",
        "output": "Say hello",
    },  # Output copies instruction
]

filter_results = [(ex, *apply_basic_filters(ex)) for ex in test_examples]
Out[21]:
Console
Basic filter results:

  Instruction: "Hi..."
  Status: ✗ FAIL (instruction_too_short)

  Instruction: "Write a poem..."
  Status: ✗ FAIL (output_too_short)

  Instruction: "Explain quantum computing..."
  Status: ✓ PASS

  Instruction: "What is 2+2?..."
  Status: ✗ FAIL (error_in_output)

  Instruction: "Say hello..."
  Status: ✗ FAIL (instruction_too_short)

The filter rules above capture several common failure modes. Too-short instructions often arise from truncation errors during data processing or from annotators who did not complete their work. Error markers in outputs indicate cases where the model used to generate responses refused or failed, producing training examples that teach refusal rather than capability. Instructions that are simply repeated in the output represent a degenerate case where no actual task was performed. Each of these failure modes, if left unfiltered, teaches the model a bad behavior pattern that will manifest during inference.

The specific thresholds (minimum 3 words for instructions, maximum 2000 words for outputs) require calibration based on the expected task distribution. A dataset focused on creative writing might need higher maximum output length thresholds, while one focused on classification might allow shorter outputs. Understanding the data well enough to set appropriate thresholds is itself a form of quality work.

Deduplication

Duplicate or near-duplicate examples waste training compute and can cause the model to memorize specific examples rather than learn general patterns. Deduplication operates at multiple levels, from exact string matching to semantic similarity detection. Each level catches different types of redundancy, and a complete deduplication strategy applies multiple levels in sequence. Different types of duplicates have different effects: exact duplicates cause straightforward overfitting, while near-duplicates can make the model generalize too rigidly from a family of similar examples.

Exact deduplication removes identical instruction-output pairs. This is the simplest form of deduplication but catches surprisingly many duplicates, especially in data generated through automated expansion or scraped from multiple sources. When multiple data pipelines contribute to the same dataset, it is common for the same examples to appear multiple times under slightly different formatting. Exact deduplication on normalized text (lowercased, whitespace-standardized) catches these cases efficiently.

In[22]:
Code
def exact_deduplicate(examples: list) -> list:
    """Remove exact duplicate examples."""
    seen = set()
    unique = []

    for ex in examples:
        # Create a hashable key from instruction and output
        key = (
            ex.get("instruction", "").strip().lower(),
            ex.get("output", "").strip().lower(),
        )

        if key not in seen:
            seen.add(key)
            unique.append(ex)

    return unique


# Example with duplicates
examples_with_dupes = [
    {
        "instruction": "What is Python?",
        "output": "Python is a programming language.",
    },
    {
        "instruction": "Explain machine learning",
        "output": "Machine learning is a subset of AI.",
    },
    {
        "instruction": "What is Python?",
        "output": "Python is a programming language.",
    },  # Exact dupe
    {
        "instruction": "WHAT IS PYTHON?",
        "output": "PYTHON IS A PROGRAMMING LANGUAGE.",
    },  # Case variant
    {
        "instruction": "Explain machine learning",
        "output": "ML is a field of artificial intelligence.",
    },  # Same instruction, different output
]

deduped = exact_deduplicate(examples_with_dupes)
Out[23]:
Console
Before deduplication: 5 examples
After deduplication: 3 examples

Remaining examples:
  - "What is Python?" -> "Python is a programming langua..."
  - "Explain machine learning" -> "Machine learning is a subset o..."
  - "Explain machine learning" -> "ML is a field of artificial in..."

Removing exact duplicates reduces the dataset size but ensures that the model encounters unique examples, preventing overfitting to repeated tokens. In this case, identical and case-variant duplicates were successfully consolidated. Examples with the same instruction but different outputs are preserved because they provide different training signals. A model that sees two different high-quality responses to the same instruction learns that multiple valid approaches exist, which can improve response diversity in deployment.

Near-duplicate detection catches examples that are semantically identical but superficially different. Two instructions might ask the same thing in slightly different words, and training on both provides little additional value. This typically uses embedding similarity to identify examples that are close in semantic space. The challenge is setting an appropriate similarity threshold: too low, and distinct examples are incorrectly removed; too high, and near-duplicates slip through. The right threshold depends on the expected diversity of the dataset and the sensitivity of the downstream task.

In[24]:
Code
!uv pip install scikit-learn
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np

def find_near_duplicates(instructions: list, threshold: float = 0.85) -> list:
    """Find pairs of instructions that are near-duplicates using TF-IDF similarity."""

    # Vectorize instructions
    vectorizer = TfidfVectorizer(ngram_range=(1, 2), max_features=1000)
    tfidf_matrix = vectorizer.fit_transform(instructions)

    # Compute pairwise similarities
    similarities = cosine_similarity(tfidf_matrix)

    # Find pairs above threshold (excluding self-similarity)
    near_dupes = []
    n = len(instructions)
    for i in range(n):
        for j in range(i + 1, n):
            if similarities[i, j] >= threshold:
                near_dupes.append((i, j, similarities[i, j]))

    return near_dupes

# Test with similar instructions
instructions = [
    "Write a Python function to check if a number is prime.",
    "Create a Python function that determines if a number is prime.",
    "Explain the concept of machine learning in simple terms.",
    "Explain machine learning to a beginner.",
    "What is the capital of France?",
    "Translate 'Hello' to Spanish."
]

near_dupes = find_near_duplicates(instructions, threshold=0.5)
Out[25]:
Console
Near-duplicate detection results:
Out[26]:
Visualization
Heatmap showing pairwise cosine similarity scores between instruction embeddings, with darker cells indicating higher similarity and clusters of near-duplicate examples visible off the main diagonal.
Pairwise cosine similarity matrix for six instructions with varying degrees of overlap. Higher similarity scores (darker cells) reveal pairs that use different surface forms to express the same underlying task, such as the two prime-checking function requests. The matrix guides deduplication by identifying which pairs exceed the similarity threshold set for filtering.

The high similarity scores identify pairs that differ only slightly in wording, allowing us to flag potential redundancies that exact matching misses. Filtering these near-duplicates ensures the training compute is spent on distinct examples. The threshold for near-duplicate detection requires careful tuning: too low and you remove legitimate variations; too high and redundant examples slip through. TF-IDF similarity is a useful approximation, but production systems typically use learned text embeddings that better capture semantic similarity across diverse phrasings of the same task.

Model-Based Quality Scoring

Human-written heuristics can only catch obvious problems. For subtler quality issues, including unclear instructions, factually incorrect outputs, or stylistically poor responses, model-based filtering provides a more detailed assessment. These approaches use the judgment capabilities of language models to evaluate aspects of quality that are difficult to capture in simple rules. Think of model-based scoring as hiring a skilled editor to review each example after the initial mechanical checks have been completed.

The approach uses a language model to score or classify the quality of each example. The model can be prompted to evaluate various dimensions of quality: clarity of the instruction, helpfulness of the response, correctness of any factual claims, and appropriateness of the tone. This multi-dimensional evaluation captures quality aspects that a single score would obscure. An instruction might be perfectly clear but receive a poor response; a response might be helpful but slightly off-topic; a factually correct response might be formatted in a way that makes it hard to read.

The key insight is that a language model used for quality scoring does not need to be the same model being trained. A smaller but capable model, perhaps the best publicly available open model at the time, can serve as an effective quality filter for data that will train a larger model. This asymmetry makes model-based quality scoring practical even when the target model is very large and expensive to evaluate.

In[27]:
Code
# Simulating model-based quality scoring
# In practice, this would use an actual language model


def simulate_quality_scores(examples: list, seed: int = 42) -> list:
    """Simulate quality scores that a model might assign.

    Real implementations would use prompts like:
    "Rate the quality of this instruction-response pair from 1-5..."
    """
    np.random.seed(seed)

    scored_examples = []
    for ex in examples:
        # Simulate scores based on heuristic proxies
        instruction_length = len(ex.get("instruction", "").split())
        output_length = len(ex.get("output", "").split())

        # Base score with some randomness
        base_score = 3.0

        # Longer, more detailed outputs tend to score higher
        if output_length > 20:
            base_score += 0.5
        if output_length > 50:
            base_score += 0.3

        # Very short outputs score lower
        if output_length < 5:
            base_score -= 1.0

        # Add noise to simulate model uncertainty
        score = base_score + np.random.normal(0, 0.3)
        score = max(1.0, min(5.0, score))  # Clamp to 1-5

        scored_examples.append({**ex, "quality_score": round(score, 2)})

    return scored_examples


# Example scoring
examples_to_score = [
    {"instruction": "What is 2+2?", "output": "4"},
    {
        "instruction": "Explain the theory of relativity.",
        "output": "Einstein's theory of relativity actually consists of two interrelated theories: special relativity and general relativity. Special relativity, published in 1905, deals with objects moving at constant speeds, particularly those approaching the speed of light. It introduced the famous equation E=mc², showing that mass and energy are equivalent.",
    },
    {
        "instruction": "Write a story.",
        "output": "Once upon a time, there was a kingdom.",
    },
]

scored = simulate_quality_scores(examples_to_score)
Out[28]:
Console
Model-based quality scoring results:

Score: 2.15/5.0
  Instruction: "What is 2+2?"
  Output: "4"

Score: 3.46/5.0
  Instruction: "Explain the theory of relativity."
  Output: "Einstein's theory of relativity actually consists ..."

Score: 3.19/5.0
  Instruction: "Write a story."
  Output: "Once upon a time, there was a kingdom."

The simulated scores reflect the heuristic that detailed, well-structured responses (like the relativity explanation) are more valuable for instruction tuning than brief or generic outputs. By scoring examples, we can filter out low-value data or curriculum-train the model on high-quality examples first. This curriculum approach exposes the model to the best examples early in training, establishing good patterns before introducing noisier data. Research suggests that curriculum learning, where training proceeds from simpler, higher-quality examples to more complex ones, can improve both convergence speed and final performance.

In production systems, quality filtering often uses a tiered approach: fast heuristic filters remove obvious problems, then more expensive model-based scoring handles the remaining examples. This balances thoroughness with computational cost. Running a language model over every example is expensive, so reserving model-based filtering for examples that pass initial heuristic checks makes the pipeline practical at scale. A typical production pipeline might reduce the dataset by 20-30% through heuristic filters, then apply model-based scoring only to the remaining examples, reducing the overall compute burden by an equivalent proportion.

Building a Complete Filtering Pipeline

Combining all filtering stages into a unified pipeline ensures consistent quality across the dataset. A well-designed pipeline applies filters in order of computational cost, with cheap heuristic filters first and expensive model-based scoring last. This design minimizes the total compute required while still achieving thorough filtering. The pipeline should also track statistics at each stage, enabling retrospective analysis of where data quality problems originate, which informs improvements to the upstream data creation processes.

In[29]:
Code
class InstructionDataFilter:
    """Pipeline for filtering instruction-tuning data."""

    def __init__(
        self, min_quality_score: float = 3.0, similarity_threshold: float = 0.9
    ):
        self.min_quality_score = min_quality_score
        self.similarity_threshold = similarity_threshold
        self.filter_stats = {
            "total_input": 0,
            "failed_basic": 0,
            "failed_duplicate": 0,
            "failed_quality": 0,
            "passed": 0,
        }

    def _apply_basic_filters(self, example: dict) -> bool:
        """Apply length and format filters."""
        instruction = example.get("instruction", "")
        output = example.get("output", "")

        if len(instruction.split()) < 3:
            return False
        if len(output.split()) < 2:
            return False
        if len(output.split()) > 2000:
            return False

        return True

    def filter_dataset(self, examples: list) -> list:
        """Run the complete filtering pipeline."""
        self.filter_stats["total_input"] = len(examples)

        # Stage 1: Basic filters
        stage1_passed = []
        for ex in examples:
            if self._apply_basic_filters(ex):
                stage1_passed.append(ex)
            else:
                self.filter_stats["failed_basic"] += 1

        # Stage 2: Exact deduplication
        seen = set()
        stage2_passed = []
        for ex in stage1_passed:
            key = (
                ex["instruction"].strip().lower(),
                ex["output"].strip().lower(),
            )
            if key not in seen:
                seen.add(key)
                stage2_passed.append(ex)
            else:
                self.filter_stats["failed_duplicate"] += 1

        # Stage 3: Quality scoring (simulated)
        stage3_passed = []
        scored = simulate_quality_scores(stage2_passed)
        for ex in scored:
            if ex["quality_score"] >= self.min_quality_score:
                stage3_passed.append(ex)
            else:
                self.filter_stats["failed_quality"] += 1

        self.filter_stats["passed"] = len(stage3_passed)
        return stage3_passed

    def get_stats(self) -> dict:
        """Return filtering statistics."""
        stats = self.filter_stats.copy()
        if stats["total_input"] > 0:
            stats["pass_rate"] = stats["passed"] / stats["total_input"]
        return stats


# Create a test dataset with various quality issues
test_dataset = [
    {
        "instruction": "Explain photosynthesis",
        "output": "Photosynthesis is the process by which plants convert sunlight into energy. It occurs in the chloroplasts of plant cells.",
    },
    {"instruction": "Hi", "output": "Hello!"},  # Too short
    {
        "instruction": "Write a poem",
        "output": "Roses are red, violets are blue, this is a poem, written for you.",
    },
    {
        "instruction": "What is AI?",
        "output": "AI stands for artificial intelligence, which refers to computer systems that can perform tasks typically requiring human intelligence.",
    },
    {
        "instruction": "Explain photosynthesis",
        "output": "Photosynthesis is the process by which plants convert sunlight into energy. It occurs in the chloroplasts of plant cells.",
    },  # Duplicate
    {"instruction": "Help", "output": "Sure!"},  # Low quality
]

# Run the pipeline
filter_pipeline = InstructionDataFilter(min_quality_score=2.5)
filtered_dataset = filter_pipeline.filter_dataset(test_dataset)
stats = filter_pipeline.get_stats()
Out[30]:
Console
Filtering Pipeline Results:
  Total input: 6
  Failed basic filters: 4
  Failed deduplication: 0
  Failed quality check: 0
  Passed all stages: 2
  Pass rate: 33.3%

Filtered examples (2):
  - "Write a poem" (score: 3.15)
  - "What is AI?" (score: 2.96)
Out[31]:
Visualization
Bar chart showing 6 input examples, 2 after basic filters, 2 after deduplication, and 2 after quality scoring; labels between stages show removals of 4, 0, and 0 examples.
Data volume retention through the demonstration quality-filtering pipeline. Basic filters retain 2 of 6 examples; deduplication and quality scoring remove no additional examples in this small sample. Monitoring stage-by-stage retention reveals where a dataset loses the most material.