← Back to DevBytes

Preparing Instruction Datasets for LLM Fine-Tuning

Preparing Instruction Datasets for LLM Fine-Tuning

Fine-tuning a Large Language Model (LLM) on custom instructions is one of the most effective ways to adapt a general-purpose model to a specific domain, task, or tone. However, the quality of the fine-tuned model depends almost entirely on the quality of the instruction dataset you feed it. In this tutorial, we will walk through what instruction datasets are, why they matter, how to build and format them, and the best practices that separate a mediocre fine-tune from a production-ready one.

What Is an Instruction Dataset?

An instruction dataset is a curated collection of examples that teach an LLM how to follow instructions. Unlike pretraining corpora, which consist of raw text from the internet, instruction datasets are structured around three core components: an instruction (the task description), an optional input (context or source material), and an output (the expected response). This structure aligns the model to behave as a helpful assistant rather than a text-completion engine.

The most common formats include the Alpaca-style schema, the ShareGPT-style conversation schema, and the OpenAI chat messages schema. Choosing the right format depends on your base model and the fine-tuning framework you intend to use.

Why Instruction Datasets Matter

Garbage in, garbage out. A model fine-tuned on noisy, inconsistent, or low-quality data will produce noisy, inconsistent, and low-quality outputs. The instruction dataset is the single largest lever you have over the final behavior of your model. It determines:

A small, high-quality dataset of 500 to 2,000 carefully crafted examples will almost always outperform a large, noisy dataset of 50,000 scraped examples. Quality, diversity, and consistency matter more than raw volume.

Structuring Your Dataset

Let us start with the Alpaca-style format, which is the simplest and most widely supported. Each example is a JSON object with three fields: instruction, input, and output.

{
  "instruction": "Summarize the following text in one sentence.",
  "input": "The Great Wall of China was built over many centuries, beginning as early as the 7th century BC. It was constructed to protect Chinese states from invasions and raids.",
  "output": "The Great Wall of China was built over centuries, starting in the 7th century BC, to defend against invasions."
}

For multi-turn conversations, the ShareGPT format is more appropriate. It uses a conversations array with alternating human and gpt roles.

{
  "conversations": [
    {"from": "human", "value": "What is the capital of France?"},
    {"from": "gpt", "value": "The capital of France is Paris."},
    {"from": "human", "value": "What is its population?"},
    {"from": "gpt", "value": "Paris has an estimated population of around 2.1 million people within the city proper."}
  ]
}

The OpenAI chat schema uses a messages array with explicit role labels, which is the format expected by most modern chat models and frameworks such as Hugging Face TRL.

{
  "messages": [
    {"role": "system", "content": "You are a concise, helpful assistant."},
    {"role": "user", "content": "Explain recursion in one sentence."},
    {"role": "assistant", "content": "Recursion is a programming technique where a function calls itself to break a problem into smaller, self-similar subproblems."}
  ]
}

Building a Dataset From Scratch

The first step is to define the task distribution. Decide which capabilities your model needs: summarization, classification, code generation, question answering, extraction, translation, and so on. Allocate examples across these categories in proportion to their importance. A common mistake is to over-index on one task type, which causes the model to over-apply that behavior to unrelated prompts.

Below is a Python script that generates a synthetic instruction dataset from a list of source documents. It uses a template approach to produce consistent examples, which you can later augment with human review or model-assisted generation.

import json
import random
from pathlib import Path

source_documents = [
    {
        "title": "Photosynthesis",
        "text": "Photosynthesis is the process by which green plants convert sunlight, water, and carbon dioxide into glucose and oxygen."
    },
    {
        "title": "Newton's First Law",
        "text": "An object at rest stays at rest, and an object in motion stays in motion unless acted upon by an external force."
    }
]

task_templates = [
    {
        "instruction": "Summarize the following passage in one sentence.",
        "output_fn": lambda doc: doc["text"]
    },
    {
        "instruction": "Write a comprehension question based on the following passage.",
        "output_fn": lambda doc: f"What is the main idea of the passage about {doc['title']}?"
    },
    {
        "instruction": "Extract the key terms from the following passage as a comma-separated list.",
        "output_fn": lambda doc: ", ".join(doc["text"].split()[:5])
    }
]

def build_dataset(documents, templates, samples_per_doc=3):
    dataset = []
    for doc in documents:
        chosen = random.sample(templates, min(samples_per_doc, len(templates)))
        for tpl in chosen:
            dataset.append({
                "instruction": tpl["instruction"],
                "input": doc["text"],
                "output": tpl["output_fn"](doc)
            })
    return dataset

dataset = build_dataset(source_documents, task_templates, samples_per_doc=3)

Path("instruction_dataset.json").write_text(
    json.dumps(dataset, indent=2, ensure_ascii=False)
)
print(f"Generated {len(dataset)} examples.")

This script produces a JSON array of Alpaca-style examples. In a real project, you would replace the template outputs with human-written or model-generated responses, then review each one for accuracy and style.

Loading and Validating the Dataset

Before fine-tuning, validate the dataset to catch formatting errors, empty fields, and outliers. The script below loads the JSON file with the Hugging Face datasets library and runs basic sanity checks.

from datasets import load_dataset, Dataset
import json

raw = json.loads(open("instruction_dataset.json").read())

def validate(example, idx):
    assert example.get("instruction"), f"Missing instruction at index {idx}"
    assert example.get("output"), f"Missing output at index {idx}"
    assert len(example["instruction"]) <= 2048, f"Instruction too long at index {idx}"
    assert len(example["output"]) <= 4096, f"Output too long at index {idx}"
    return True

for i, ex in enumerate(raw):
    validate(ex, i)

dataset = Dataset.from_list(raw)
print(dataset)
print(dataset[0])

For chat-formatted data, you can convert the Alpaca schema into a chat template compatible with most modern tokenizers. This is essential when fine-tuning models like Llama, Mistral, or Qwen that use a chat template during training and inference.

def to_chat(example):
    messages = [{"role": "user", "content": example["instruction"]}]
    if example.get("input"):
        messages[0]["content"] += "\n\n" + example["input"]
    messages.append({"role": "assistant", "content": example["output"]})
    return {"messages": messages}

chat_dataset = dataset.map(to_chat, remove_columns=dataset.column_names)
print(chat_dataset[0])

Deduplication and Balancing

Duplicate or near-duplicate examples waste training capacity and bias the model toward repeated patterns. Use a simple hashing approach to remove exact duplicates, and consider MinHash or embedding similarity for near-duplicates in larger datasets.

import hashlib

def deduplicate(examples):
    seen = set()
    unique = []
    for ex in examples:
        key = hashlib.md5(
            (ex["instruction"] + ex.get("input", "") + ex["output"]).encode()
        ).hexdigest()
        if key not in seen:
            seen.add(key)
            unique.append(ex)
    return unique

clean = deduplicate(raw)
print(f"Before: {len(raw)} | After: {len(clean)}")

To balance task categories, tag each example with a category label and resample so that no single category dominates. A reasonable target is for the largest category to represent no more than 30 to 40 percent of the total dataset.

Best Practices

Conclusion

Preparing an instruction dataset is the most consequential step in any LLM fine-tuning project. The format you choose, the diversity of tasks you include, and the care you put into each example will directly shape the behavior of the resulting model. By starting with a clear task distribution, enforcing consistent formatting, validating and deduplicating rigorously, and holding out a clean evaluation set, you set the foundation for a fine-tune that is reliable, controllable, and genuinely useful in production. Treat your dataset as a product in its own right, iterate on it as you would on code, and your fine-tuned models will reward the effort.

— Ad —

Google AdSense will appear here after approval

← Back to all articles