|$ curl https://forge-ai.dev/api/markdown?path=docs/ai/datasets
$cat docs/dataset-curation.md
updated Recentlyยท30 min readยทpublished

Dataset Curation

โ—†AIโ—†Advanced
Introduction

Dataset curation is the process of collecting, cleaning, filtering, and formatting data for LLM training. The quality of your dataset is the single most important factor determining model performance โ€” a small, clean dataset consistently outperforms a large, noisy one across all training paradigms (pre-training, SFT, preference optimization).

Modern LLM datasets span orders of magnitude: pre-training datasets contain trillions of tokens from web crawls, while SFT datasets are typically 1K-100K high-quality examples. Each stage of training requires different curation strategies, quality thresholds, and data formats.

This guide covers the end-to-end dataset curation pipeline: collection strategies, quality filtering techniques, deduplication algorithms (MinHash, exact match), synthetic data generation, data augmentation, formatting standards (ChatML, ShareGPT), and dataset management best practices.

Data Collection Strategies

Data collection depends on the training stage. For pre-training, web crawls (Common Crawl, CCNet) are the primary source. For SFT, you need demonstration data โ€” human-written instruction-response pairs or distilled outputs from stronger models (e.g., GPT-4). For preference optimization, you need pairwise comparisons.

SourceTraining StageQualityVolume
Common CrawlPre-trainingLow (raw)Petabytes
Human AnnotationSFT / RLHFHighest1K-100K
Model DistillationSFTHigh10K-1M
Open DatasetsAll stagesVariableVarious
User LogsFine-tuningMedium100K-10M
โœ“

best practice

For SFT data, prefer human-written demonstrations over distilled data when possible. Distilled data inherits the teacher model's biases and failure modes. If using distillation, combine it with human validation โ€” have annotators review and edit a sample of distilled outputs to maintain quality.
Quality Filtering

Raw data contains low-quality content that degrades model performance if included in training. Filtering removes toxic content, spam, low-information text, and examples with incorrect formatting. Multiple filtering passes with different heuristics are more effective than a single complex filter.

Filtering Pipeline

quality-filter.py
Python
1import re
2from typing import List, Dict
3
4class QualityFilter:
5 def __init__(self):
6 self.min_length = 50
7 self.max_length = 10000
8 self.min_words = 10
9 self.max_repetition_ratio = 0.3
10 self.toxic_patterns = [
11 r'(?i)\b(spam|click here|buy now|limited offer)\b',
12 ]
13 self.url_threshold = 0.5 # Max fraction of URLs in text
14
15 def filter(self, examples: List[Dict]) -> List[Dict]:
16 filtered = []
17 for ex in examples:
18 text = ex.get("text", "")
19 if not text:
20 continue
21 if len(text) < self.min_length or len(text) > self.max_length:
22 continue
23 words = text.split()
24 if len(words) < self.min_words:
25 continue
26 if self.repetition_ratio(text) > self.max_repetition_ratio:
27 continue
28 if self.has_toxic_content(text):
29 continue
30 if self.url_ratio(text) > self.url_threshold:
31 continue
32 filtered.append(ex)
33 return filtered
34
35 def repetition_ratio(self, text: str) -> float:
36 ngrams = set()
37 total = 0
38 words = text.split()
39 for i in range(len(words) - 4):
40 ngram = " ".join(words[i:i+5])
41 ngrams.add(ngram)
42 total += 1
43 return 1 - len(ngrams) / max(total, 1)
44
45 def has_toxic_content(self, text: str) -> bool:
46 return any(
47 re.search(p, text) for p in self.toxic_patterns
48 )
49
50 def url_ratio(self, text: str) -> float:
51 urls = len(re.findall(r'https?://\S+', text))
52 return urls / max(len(text.split()), 1)

Language & Content Filtering

Use fastText language identification to filter for target languages. Remove documents with low perplexity under a language model (these are often repetitive or templated text). For SFT data, verify that every example has a properly formatted instruction and response pair.

lang-perplexity-filter.py
Python
1import fasttext
2
3# Load language identification model
4lid_model = fasttext.load_model("lid.176.bin")
5
6def filter_language(text: str, target_lang: str = "en", threshold: float = 0.5) -> bool:
7 predictions = lid_model.predict(text.replace("\n", " "))
8 lang = predictions[0][0].replace("__label__", "")
9 confidence = predictions[1][0]
10 return lang == target_lang and confidence >= threshold
11
12# Perplexity-based filtering
13from transformers import AutoModelForMaskedLM, AutoTokenizer
14
15mlm_model = AutoModelForMaskedLM.from_pretrained("bert-base-uncased")
16mlm_tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
17
18def compute_perplexity(text: str) -> float:
19 inputs = mlm_tokenizer(text, return_tensors="pt", truncation=True, max_length=512)
20 outputs = mlm_model(**inputs, labels=inputs["input_ids"])
21 return torch.exp(outputs.loss).item()
22
23# Filter out very low perplexity (template) and very high perplexity (gibberish)
24filtered = [
25 ex for ex in data
26 if 20 < compute_perplexity(ex["text"]) < 500
27]
Deduplication

Duplicate data inflates training metrics without providing new information and can cause the model to over-memorize specific examples. Deduplication operates at multiple levels: exact document match, near-duplicate (fuzzy) match, and paragraph/sentence-level deduplication.

Exact Deduplication

The simplest and fastest approach โ€” remove documents with identical content using hash-based comparison. Always normalize text (lowercase, strip whitespace) before hashing. Use SHA-256 or xxHash for fast hashing at scale.

exact-dedup.py
Python
1import hashlib
2
3def normalize(text: str) -> str:
4 return " ".join(text.lower().strip().split())
5
6def exact_dedup(examples: list, key: str = "text") -> list:
7 seen = set()
8 unique = []
9 for ex in examples:
10 normalized = normalize(ex[key])
11 doc_hash = hashlib.sha256(normalized.encode()).hexdigest()
12 if doc_hash not in seen:
13 seen.add(doc_hash)
14 unique.append(ex)
15 return unique
16
17# Fast hash with xxhash (better for large datasets)
18import xxhash
19
20def exact_dedup_fast(examples: list) -> list:
21 seen = set()
22 unique = []
23 for ex in examples:
24 h = xxhash.xxh64(ex["text"]).hexdigest()
25 if h not in seen:
26 seen.add(h)
27 unique.append(ex)
28 return unique
29
30print(f"Removed {len(examples) - len(exact_dedup_fast(examples))} exact duplicates")

MinHash โ€” Near-Duplicate Detection

MinHash with Locality-Sensitive Hashing (LSH) detects documents that are similar but not identical โ€” e.g., articles with slightly different headlines, versions of the same page with different timestamps, or paraphrased content. This is critical for web-scale deduplication where exact match catches only a fraction of duplicates.

minhash-dedup.py
Python
1from datasketch import MinHash, MinHashLSH
2import re
3
4def shingle_text(text: str, k: int = 5) -> set:
5 words = re.findall(r'\w+', text.lower())
6 return set(
7 " ".join(words[i:i+k])
8 for i in range(len(words) - k + 1)
9 )
10
11def build_minhash(text: str, num_perm: int = 128) -> MinHash:
12 m = MinHash(num_perm=num_perm)
13 for shingle in shingle_text(text):
14 m.update(shingle.encode())
15 return m
16
17# Create LSH index
18lsh = MinHashLSH(threshold=0.7, num_perm=128)
19
20for i, ex in enumerate(examples):
21 m = build_minhash(ex["text"])
22 lsh.insert(f"doc_{i}", m)
23
24# Find near-duplicates
25near_duplicates = set()
26for i, ex in enumerate(examples):
27 m = build_minhash(ex["text"])
28 results = lsh.query(m)
29 if len(results) > 1:
30 near_duplicates.add(i)
31
32print(f"Found {len(near_duplicates)} near-duplicate documents")
33filtered = [ex for i, ex in enumerate(examples) if i not in near_duplicates]
๐Ÿ”ฅ

pro tip

The MinHash threshold parameter controls how similar two documents must be to be considered duplicates. Threshold=0.7 is standard for web documents. For SFT data, use a higher threshold (0.8-0.9) since exact formatting variations matter. Always inspect a random sample of flagged duplicates to validate your threshold choice.
Synthetic Data Generation

Synthetic data generation uses existing models to create training examples. This is valuable for scenarios where human data is scarce, expensive, or impossible to collect. Common approaches include self-instruct, evol-instruct, back-translation, and contradiction generation.

synthetic-data-gen.py
Python
1from openai import OpenAI
2import json
3
4client = OpenAI()
5
6def generate_sft_examples(topic: str, n: int = 100) -> list:
7 prompt = f"""Generate {n} high-quality instruction-response pairs about {topic}.
8
9Each example should be a JSON object with:
10- "instruction": a clear, specific question or task
11- "output": a detailed, accurate response
12
13Make the instructions diverse: factual questions, how-to guides,
14explanations, comparisons, and creative tasks. Ensure responses
15are technically accurate and at least 3-4 sentences long."""
16
17 response = client.chat.completions.create(
18 model="gpt-4o",
19 messages=[{"role": "user", "content": prompt}],
20 response_format={"type": "json_object"},
21 temperature=0.8
22 )
23
24 data = json.loads(response.choices[0].message.content)
25 return data["examples"]
26
27# Evol-Instruct: Iteratively increase difficulty
28def evolve_instruction(instruction: str) -> str:
29 prompt = f"""Rewrite the following instruction to make it more
30challenging and specific. Add constraints, edge cases, or depth:
31
32Original: {instruction}
33
34Improved:"""
35 response = client.chat.completions.create(
36 model="gpt-4o",
37 messages=[{"role": "user", "content": prompt}],
38 temperature=0.7
39 )
40 return response.choices[0].message.content.strip()
โš 

warning

Synthetic data inherits the biases, errors, and limitations of the generating model. Always validate synthetic data with human reviewers. Models trained on synthetic data can exhibit model collapse โ€” degraded diversity and quality across generations. Mix synthetic data with real data to maintain quality.
Dataset Formatting

Consistent formatting is essential for training stability and model performance. Most modern LLMs use chat templates to structure conversations. The two dominant formats are ChatML (used by GPT-4, Llama 3, Mistral) and ShareGPT (used by Vicuna, some community models).

dataset-formatting.py
Python
1# ChatML format
2chatml_format = {
3 "messages": [
4 {"role": "system", "content": "You are an expert programmer."},
5 {"role": "user", "content": "Write a Python function to reverse a linked list."},
6 {"role": "assistant", "content": "Here's a recursive solution:\n\ndef reverse(head):\n if not head or not head.next:\n return head\n new_head = reverse(head.next)\n head.next.next = head\n head.next = None\n return new_head"}
7 ]
8}
9
10# Convert to tokenizer's expected format using apply_chat_template
11from transformers import AutoTokenizer
12
13tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.2-3B")
14formatted = tokenizer.apply_chat_template(
15 chatml_format["messages"],
16 tokenize=False,
17 add_generation_prompt=True
18)
19
20# For training: create packed sequences efficiently
21def pack_sequences(dataset, tokenizer, max_length=2048):
22 all_tokens = []
23 for example in dataset:
24 tokens = tokenizer(
25 example["text"],
26 truncation=True,
27 max_length=max_length
28 )["input_ids"]
29 all_tokens.extend(tokens)
30
31 packed = []
32 for i in range(0, len(all_tokens), max_length):
33 chunk = all_tokens[i:i + max_length]
34 packed.append({"input_ids": chunk, "labels": chunk.copy()})
35
36 return packed
โœ“

best practice

Always use the tokenizer's built-in apply_chat_template method instead of hand-crafting format strings. The tokenizer knows the exact format expected by the model, including special tokens, control tokens, and whitespace conventions. Hand-crafted templates are a common source of training instability.
Ethical Considerations

Dataset curation carries significant ethical responsibilities. Biases present in training data are learned and amplified by the model. Privacy violations in training data can lead to data leakage at inference. Always document data provenance, obtain consent for personal data, and conduct bias audits before training.

โ—†Document data sources, collection methodology, and filters applied
โ—†Remove personally identifiable information (PII) before training
โ—†Audit for demographic, cultural, and linguistic bias
โ—†Respect copyright and licensing terms of source data
โ—†Publish data cards (following Datasheets for Datasets standard)
$Blueprint โ€” Engineering DocumentationยทSection ID: AI-DC-01ยทRevision: 1.0