Data Cleaning Pipeline for Fine-Tuning LLMs
Imagine you've collected 100,000 examples for fine-tuning LLaMA 3, but the model produces incoherent responses and hallucinates on every third query. The cause is dirty data: 40% duplicates, 15% contain personal data, another 10% toxic content. Without quality cleaning, fine-tuning won't yield the desired result.
We've developed a pipeline that transforms a raw dataset into clean, training-ready data in 10–14 days. MinHash LSH for deduplication works 10 times faster than pairwise comparison when searching for near-duplicates on datasets of 50,000 examples. And toxicity filtering via Detoxify reduces the probability of undesirable model responses by 25% compared to simple regex.
Why Standard Cleaning Doesn't Work for LLMs?
Texts for fine-tuning contain specific artifacts: HTML tags (if scraped from the web), Unicode variations, meta-comments like "As an AI language model...". Simply removing punctuation doesn't solve the problem. Multi-layer filtering with context awareness is needed. For example, PII detection requires not only regex but also NER models (spaCy) to find "John Doe, Lenin St." — as important as card numbers. Before running the pipeline, it's recommended to review best practices from Hugging Face Datasets documentation.
How We Build the Cleaning Pipeline
The pipeline consists of sequential stages, each checking and transforming the sample. Critical not to overdo: excessive cleaning reduces data diversity.
import re
import unicodedata
from dataclasses import dataclass
@dataclass
class CleaningResult:
original: str
cleaned: str
removed: bool
removal_reason: str = None
class TextCleaner:
def clean(self, text: str) -> CleaningResult:
cleaned = text
# 1. Unicode normalization
cleaned = unicodedata.normalize('NFKC', cleaned)
# 2. Remove HTML/XML tags
cleaned = re.sub(r'<[^>]+>', ' ', cleaned)
# 3. Clean URLs (optional — replace with placeholder)
cleaned = re.sub(
r'https?://[^\s]+', '[URL]', cleaned
)
# 4. Normalize whitespace
cleaned = re.sub(r'\s+', ' ', cleaned).strip()
# 5. Remove repeated characters (ааааааа → а)
cleaned = re.sub(r'(.)\1{4,}', r'\1\1', cleaned)
# Check minimum length
if len(cleaned.split()) < 3:
return CleaningResult(text, cleaned, True, "too_short")
return CleaningResult(text, cleaned, False)
class DataFilter:
def __init__(self):
# Toxicity (can use detoxify or fasttext)
from detoxify import Detoxify
self.toxicity_model = Detoxify('multilingual')
def is_toxic(self, text: str, threshold: float = 0.7) -> bool:
result = self.toxicity_model.predict(text)
return result['toxicity'] > threshold
def has_pii(self, text: str) -> bool:
"""Simple heuristic for PII detection"""
patterns = [
r'\b\d{3}-\d{2}-\d{4}\b', # SSN
r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', # Email
r'\b(?:\+7|8)?[\s-]?\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{2}[\s-]?\d{2}\b', # RU phone
r'\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b', # Credit card
]
for pattern in patterns:
if re.search(pattern, text):
return True
return False
Step-by-Step Pipeline Configuration
- Define filtering thresholds. For toxicity, use threshold 0.7 — balances removing bad content while preserving useful. For duplicates, set similarity 0.8.
- Choose deduplication algorithm. Exact duplicates: exact matching; near-duplicates: MinHash LSH. SimHash suits streaming but gives more false positives.
- Run a test on 1000 samples. Check metrics: number removed, type-token ratio, residual toxicity. If OK, run full dataset.
Cleaning Output Fields
Assistant model responses often contain unwanted introductions: "Certainly! Here is my response". The algorithm detects these patterns and trims them, leaving only useful content.
class OutputCleaner:
def clean_output(self, output: str, task_type: str) -> tuple[str, bool]:
cleaned = output.strip()
# Remove unwanted model phrases
unwanted_starts = [
"As an AI language model",
"As a helpful assistant",
"I don't have access to real-time",
"I cannot browse the internet",
"Certainly! Here",
"Of course! I'd be happy to",
]
for phrase in unwanted_starts:
if cleaned.lower().startswith(phrase.lower()):
# Remove introductory phrase
cleaned = cleaned[len(phrase):].lstrip('.,! ')
# Check: output should not contain meta-comments
meta_indicators = [
"Note: This is a fictional",
"[This response was",
"Disclaimer:",
]
for indicator in meta_indicators:
if indicator in cleaned:
idx = cleaned.find(indicator)
cleaned = cleaned[:idx].strip()
# Minimum length
if len(cleaned.split()) < 5:
return cleaned, True # Mark for removal
return cleaned, False
Duplicate Detection at Different Levels
For exact duplicates, we use hashing; for near-duplicates — MinHash LSH. A similarity threshold of 0.8 removes almost identical examples while preserving variability.
from datasketch import MinHash, MinHashLSH
def find_near_duplicates(texts: list[str],
threshold: float = 0.8) -> list[tuple]:
"""MinHash LSH for efficient near-duplicate search O(n log n)"""
lsh = MinHashLSH(threshold=threshold, num_perm=128)
minhashes = {}
for i, text in enumerate(texts):
m = MinHash(num_perm=128)
for word in text.lower().split():
m.update(word.encode('utf8'))
lsh.insert(f"doc_{i}", m)
minhashes[f"doc_{i}"] = m
duplicates = []
for i, text in enumerate(texts):
key = f"doc_{i}"
result = lsh.query(minhashes[key])
result.remove(key)
if result:
duplicates.append((i, [int(r.split('_')[1]) for r in result]))
return duplicates
Comparison of Deduplication Methods
| Method | Speed | Precision | Application |
|---|---|---|---|
| Exact matching | O(n) | 100% | Exact duplicates |
| MinHash LSH | O(n log n) | ~95% | Near-duplicates |
| SimHash | O(n) | ~90% | Quick estimation |
Post-Cleaning Statistics
After the pipeline, we always check metrics:
| Metric | Normal Range | Purpose |
|---|---|---|
| Removed examples | 15–30% | Control aggressiveness of cleaning |
| Token count | >5 million | Enough for fine-tuning |
| Type-token ratio | >0.5 | Sufficient diversity |
| Task coverage | >90% | All needed scenarios |
| Toxicity | <1% | Model safety |
Typical result: from 50,000 raw examples, 35,000–42,000 high-quality ones remain. A 15–30% volume reduction is normal, and the final model quality only improves. Compared to rough cleaning (only regex), fine-tuning accuracy increases by 15–20%. A common issue is class imbalance: if 90% of examples are positive, the model won't learn to handle negative queries. We apply stratified sampling and augmentation of rare classes. Also important to remove LLM-specific stop words: 'As a language model', 'I cannot', 'I think'. This reduces noise by 5–10%.
What's Included in the Work
We prepare a full cleaning pipeline for your dataset:
- Raw data analysis (length distribution, language, toxicity)
- Filter configuration tailored to your task (RAG, generation, classification)
- Deduplication and PII removal
- Normalization and tokenization
- Report with metrics and visualizations
- Pipeline documentation and configuration
- Training for your team
Timeline — from 10 to 14 business days depending on volume. Contact us to evaluate your project — we guarantee confidentiality and result quality. Our experience: over 5 years in NLP, over 20 projects in fine-tuning models of various sizes. Get a consultation on dataset cleaning — we will prepare a custom pipeline.







