Qualité des données : nettoyage et validation
Mis à jour le 28 juillet 2026
Pourquoi le nettoyage est critique
Un fichier JSONL techniquement valide ne garantit pas un bon fine-tuning. Des données bruitées, incohérentes ou mal formatées vont dégrader les performances du modèle plutôt que les améliorer — et le pire est qu’elles ne provoqueront aucune erreur : le job réussira, le modèle sortira, et c’est en production que vous découvrirez le problème. Cette leçon couvre les techniques de nettoyage et de validation à passer avant l’upload.
Validation technique du fichier
Commencez par le plus mécanique : vérifier que votre fichier est syntaxiquement correct et que chaque ligne respecte le contrat attendu. La fonction ci-dessous parcourt le fichier ligne à ligne, accumule les erreurs au lieu de s’arrêter à la première (vous voulez la liste complète, pas un aller-retour par problème) et retourne au passage quelques statistiques utiles sur la présence de messages système et de conversations multi-tours.
import json
def valider_jsonl(chemin: str) -> dict:
"""Valide un fichier JSONL et retourne des statistiques."""
erreurs = []
stats = {"total": 0, "avec_system": 0, "multi_tour": 0}
roles_valides = {"system", "user", "assistant"}
with open(chemin, "r", encoding="utf-8") as f:
for i, ligne in enumerate(f, 1):
stats["total"] += 1
try:
obj = json.loads(ligne.strip())
except json.JSONDecodeError as e:
erreurs.append(f"Ligne {i} : JSON invalide — {e}")
continue
if "messages" not in obj:
erreurs.append(f"Ligne {i} : clé 'messages' manquante")
continue
messages = obj["messages"]
if not isinstance(messages, list) or len(messages) < 2:
erreurs.append(f"Ligne {i} : minimum 2 messages requis")
continue
for j, msg in enumerate(messages):
role = msg.get("role")
if role not in roles_valides:
erreurs.append(f"Ligne {i}, msg {j} : rôle invalide")
if not msg.get("content", "").strip():
erreurs.append(f"Ligne {i}, msg {j} : contenu vide")
if not any(m["role"] == "assistant" for m in messages):
erreurs.append(f"Ligne {i} : aucun message assistant")
if messages[0]["role"] == "system":
stats["avec_system"] += 1
tours_user = sum(1 for m in messages if m["role"] == "user")
if tours_user > 1:
stats["multi_tour"] += 1
return {"erreurs": erreurs, "stats": stats}
resultat = valider_jsonl("training_data.jsonl")
if resultat["erreurs"]:
print(f"Erreurs : {len(resultat['erreurs'])}")
for err in resultat["erreurs"][:10]:
print(f" - {err}")
else:
print(f"Fichier valide — {resultat['stats']['total']} exemples")
Nettoyage du contenu
Les doublons exacts ou quasi-identiques biaisent l’entraînement en sur-représentant certains patterns. Le cas se produit tout seul dès que vous agrégez plusieurs exports : la même question fréquente apparaît trente fois dans vos logs, et le modèle en conclut qu’elle mérite trente fois plus d’attention que les autres. Une empreinte sur le contenu concaténé des messages suffit à les éliminer.
import hashlib
def deduplication(exemples: list[dict]) -> list[dict]:
"""Supprime les doublons basés sur le contenu des messages."""
vus = set()
uniques = []
for ex in exemples:
contenu = "|".join(
f"{m['role']}:{m['content']}" for m in ex["messages"]
)
h = hashlib.md5(contenu.encode()).hexdigest()
if h not in vus:
vus.add(h)
uniques.append(ex)
print(f"Doublons supprimés : {len(exemples) - len(uniques)}")
return uniques
Vient ensuite la normalisation du texte, qui règle quatre problèmes distincts. Les espaces multiples et les retours à la ligne superflus, hérités des copier-coller, doivent disparaître. L’encodage doit être ramené en UTF-8 avec suppression des caractères de contrôle, invisibles mais bien présents dans les exports de bases de données. La ponctuation mérite un contrôle : une réponse tronquée en plein milieu apprend au modèle à s’interrompre. Enfin la cohérence de langue — si vous voulez du vouvoiement, vérifiez qu’aucun exemple ne tutoie, car le modèle reproduira fidèlement le mélange.
import re
import unicodedata
def normaliser_texte(texte: str) -> str:
"""Normalise un texte pour l'entraînement."""
texte = unicodedata.normalize("NFC", texte)
texte = re.sub(r"[\x00-\x09\x0b-\x0c\x0e-\x1f\x7f]", "", texte)
texte = re.sub(r"[ \t]+", " ", texte)
texte = re.sub(r"\n{3,}", "\n\n", texte)
return texte.strip()
Vérification de la cohérence
Si vous utilisez un message système, il doit être rigoureusement identique dans tous vos exemples. Deux variantes qui ne diffèrent que par un mot suffisent à diluer le signal : le modèle n’apprend plus une instruction, il apprend une famille floue d’instructions. Le contrôle est trivial et le résultat souvent surprenant sur un corpus construit à plusieurs.
def verifier_system_messages(exemples: list[dict]):
"""Vérifie la cohérence des messages système."""
system_msgs = set()
for ex in exemples:
for msg in ex["messages"]:
if msg["role"] == "system":
system_msgs.add(msg["content"].strip())
if len(system_msgs) > 1:
print(f"Attention : {len(system_msgs)} messages système différents")
for sm in system_msgs:
print(f" - {sm[:80]}...")
elif len(system_msgs) == 1:
print("Message système cohérent")
else:
print("Aucun message système utilisé")
La distribution des longueurs de réponses raconte la même histoire sous un autre angle. Des réponses très hétérogènes signalent que plusieurs personnes ont rédigé le corpus avec des idées différentes de ce qu’est une bonne réponse, ou qu’un exemple aberrant s’est glissé dans le lot. Le seuil retenu ici — un maximum supérieur à cinq fois la moyenne — n’a rien d’absolu, mais il attire l’œil au bon endroit.
def analyser_longueurs(exemples: list[dict]):
"""Analyse la distribution des longueurs de réponses."""
longueurs = []
for ex in exemples:
for msg in ex["messages"]:
if msg["role"] == "assistant":
longueurs.append(len(msg["content"]))
if longueurs:
moy = sum(longueurs) / len(longueurs)
mini, maxi = min(longueurs), max(longueurs)
print(f"Longueur réponses — Moy: {moy:.0f} | Min: {mini} | Max: {maxi}")
if maxi > moy * 5:
print("Forte dispersion — vérifiez les outliers")
Utiliser l’API de validation OpenAI
Une fois vos propres contrôles passés, uploadez le fichier et laissez OpenAI le valider à son tour. C’est gratuit, immédiat, et cela vous évite de découvrir un problème de format au moment où le job démarre.
from openai import OpenAI
client = OpenAI()
fichier = client.files.create(
file=open("training_data.jsonl", "rb"),
purpose="fine-tune"
)
print(f"Fichier uploadé : {fichier.id}")
print(f"Statut : {fichier.status}")
Si le fichier contient des erreurs, le statut passera à error avec un message détaillé. Corrigez les problèmes signalés avant de relancer.
Checklist avant fine-tuning
Passez cette liste dans l’ordre avant de créer le job — chaque point correspond à un contrôle vu plus haut, sauf le dernier, qu’aucun script ne remplace :
- Fichier JSONL syntaxiquement valide
- Aucun doublon
- Texte normalisé (UTF-8, espaces, ponctuation)
- Messages système cohérents
- Au moins 50 exemples (idéalement 100+)
- Split train/validation effectué
- Distribution des longueurs raisonnable
- Relecture humaine d’un échantillon aléatoire
Points clés à retenir
- Validez toujours la syntaxe JSONL avant l’upload
- Supprimez les doublons pour éviter le surapprentissage
- La cohérence du message système est essentielle
- Vérifiez la distribution des longueurs de réponses
- Faites relire un échantillon par un humain — c’est irremplaçable