01 / Quand fine-tuner ? Les bonnes et mauvaises raisons

Le fine-tuning est l'une des techniques les plus mal comprises de l'écosystème LLM. On le présente souvent comme la solution universelle à tous les problèmes d'un modèle, alors qu'il ne répond qu'à un sous-ensemble précis de cas. Avant de lancer un entraînement, la question à se poser est brutale : est-ce que le problème vient vraiment du modèle, ou de la façon dont je l'utilise ?

Les 3 bonnes raisons de fine-tuner

Style et ton. Vous avez besoin qu'un modèle réponde systématiquement dans un registre spécifique — vocabulaire technique d'un domaine réglementé, format de sortie rigide (JSON structuré, Markdown balisé), ton éditorial d'une marque. Le prompt engineering peut en approcher une partie, mais le fine-tuning l'encode au niveau des poids : plus fiable, moins de tokens dépensés à chaque inférence.

Domaine très spécifique et fermé. Votre corpus est propriétaire, très technique, et ne figure pas dans les données d'entraînement du modèle de base — droit minier gabonais, protocoles de maintenance d'une turbine précise, jargon interne d'un cabinet de conseil. Le modèle n'a pas les schémas de raisonnement pour ce domaine : le fine-tuning les encode.

Réduction de prompt. En production intensive, chaque token compte. Un modèle fine-tuné sur votre cas d'usage n'a pas besoin des 500 tokens d'instructions système qui définissent le contexte. Les économies de latence et de coût peuvent être significatives à l'échelle.

Les 3 mauvaises raisons de fine-tuner

Remplacer le RAG. Si votre problème est l'accès à de l'information — documents, bases de données, contenus mis à jour — le fine-tuning ne fait pas de vos connaissances des faits mémorisés fiables. Le RAG est la bonne réponse. Le fine-tuning ne stocke pas de l'information, il encode des comportements.

Ajouter des connaissances récentes. Vous voulez que le modèle connaisse les événements après sa date de coupure ? Fine-tuner sur des actualités récentes crée l'illusion de savoir sans la fiabilité. Le modèle extrapolera, confondra, hallusinera avec plus de confiance. RAG ou grounding externe.

Corriger des hallucinations génériques. Le modèle invente des faits ? Ce n'est pas un problème de fine-tuning. C'est un problème de température, de grounding, d'architecture de prompt, ou de retrievab. Fine-tuner sur des exemples corrects ne supprime pas le comportement hallucinatoire sur de nouveaux inputs.

Le problème est-il un manque de données ? Le problème est-il de style ou de comportement ? oui RAG suffit → Retrieval non Le prompt résout-il 80% du problème ? oui Prompt eng. d'abord non Avez-vous 500+ exemples de haute qualité ? oui Prompt eng. en premier non oui Fine-tuning pertinent ✓ non Collecter des données d'abord

Fig. 1 — Arbre de décision : faut-il fine-tuner ou choisir une autre approche ?

02 / Anatomie du fine-tuning : SFT, DPO, LoRA

Il existe plusieurs stratégies de fine-tuning, qui répondent à des objectifs différents. Les confondre mène à des pipelines mal configurés et des résultats décevants.

Supervised Fine-Tuning (SFT)

Le SFT est la forme la plus simple. Vous disposez de paires (instruction, réponse idéale) et vous entraînez le modèle à maximiser la probabilité de la réponse idéale étant donné l'instruction. Le modèle apprend à imiter vos exemples. La loss est une cross-entropie standard sur les tokens de la réponse. C'est le point de départ obligatoire de tout pipeline de fine-tuning sérieux.

Direct Preference Optimization (DPO)

Le DPO va un cran plus loin. Au lieu d'imiter des exemples parfaits, vous fournissez des paires (réponse choisie, réponse rejetée) pour le même input. Le modèle apprend à préférer les sorties choisies par les humains. C'est l'alternative moderne au RLHF (Reinforcement Learning from Human Feedback) : même résultat d'alignement, sans le reward model séparé ni l'instabilité du RL. Le DPO s'applique généralement après un SFT initial.

LoRA et QLoRA : le secret de l'accessibilité

LoRA (Low-Rank Adaptation) est la technique qui rend le fine-tuning accessible à des équipes sans cluster de GPU. L'idée : au lieu de mettre à jour tous les paramètres du modèle, on gèle les poids originaux et on ajoute de petites matrices de décomposition rang-faible à certaines couches. Seules ces matrices sont entraînées. Le gain en VRAM est massif : Mistral 7B en QLoRA 4-bit tient sur une RTX 4090 de 24 Go.

QLoRA pousse l'économie encore plus loin en combinant LoRA avec une quantification 4-bit du modèle de base (via BitsAndBytes). La qualité est légèrement inférieure au full fine-tuning, mais le compromis mémoire/performance est excellent pour la majorité des cas pratiques.

input x W préentraîné d × d 🔒 gelé A d × r B r × d rang r=16 α=32 + output Wx + BAx poids gelés adaptateurs LoRA (entraînés) ~0.1% des paramètres

Fig. 2 — Architecture LoRA : W est gelé, seules les matrices A et B de rang r sont entraînées. La sortie est Wx + BAx.

03 / Constitution du dataset

La qualité du dataset est le facteur le plus déterminant du résultat. Un bon modèle fine-tuné sur des données médiocres produit des résultats médiocres. L'inverse est également vrai : un dataset de 500 exemples impeccables peut surpasser un dataset de 50 000 exemples bruyants.

Formats et structures

Le format standard pour le SFT est le JSONL (JSON Lines), avec un objet par ligne. Pour les tâches d'instruction simple, on utilise les champs instruction et output. Pour les conversations multi-tours, le format messages (compatible OpenAI) est préférable : chaque message a un rôle (system, user, assistant) et un contenu.

La plupart des frameworks (TRL, Axolotl) acceptent les deux formats et les convertissent automatiquement selon le chat template du modèle cible.

// dataset-sft.jsonl — format instruction/output
// Exemple 1 — tâche de classification juridique
{"instruction": "Classe ce contrat selon l'EU AI Act. Identifie la catégorie de risque et les obligations applicables.\n\nContrat : Système de scoring automatisé des candidatures à un emploi.", "output": "**Catégorie : Risque élevé** (Annexe III, point 2 — emploi et gestion des travailleurs).\n\nObligations applicables :\n- Enregistrement dans la base de données EU (Art. 49)\n- Évaluation de la conformité avant mise sur le marché (Art. 43)\n- Documentation technique complète (Art. 11)\n- Journalisation automatique (Art. 12)\n- Supervision humaine obligatoire (Art. 14)\n- Transparence envers les sujets (Art. 13)"}

// Exemple 2 — génération de code avec contexte domaine
{"instruction": "Écris une fonction Python qui parse un fichier JSONL de dataset SFT et retourne les statistiques de longueur des instructions et outputs.", "output": "```python\nimport json\nfrom pathlib import Path\nfrom typing import NamedTuple\n\nclass DatasetStats(NamedTuple):\n    count: int\n    avg_instruction_tokens: float\n    avg_output_tokens: float\n    p95_output_tokens: int\n\ndef analyze_sft_dataset(path: str | Path) -> DatasetStats:\n    instructions, outputs = [], []\n    with open(path) as f:\n        for line in f:\n            ex = json.loads(line)\n            instructions.append(len(ex['instruction'].split()))\n            outputs.append(len(ex['output'].split()))\n    return DatasetStats(\n        count=len(instructions),\n        avg_instruction_tokens=sum(instructions)/len(instructions),\n        avg_output_tokens=sum(outputs)/len(outputs),\n        p95_output_tokens=sorted(outputs)[int(len(outputs)*0.95)]\n    )\n```"}

// Exemple 3 — format multi-turn conversation
{"messages": [
  {"role": "system", "content": "Tu es un expert en architecture de systèmes RAG. Réponds de façon technique et précise."},
  {"role": "user", "content": "Quelle est la différence entre un chunking fixe et un chunking sémantique ?"},
  {"role": "assistant", "content": "Le chunking fixe divise le texte par nombre de tokens (ex : 512) sans tenir compte du sens. Simple à implémenter, mais coupe souvent des idées au milieu.\n\nLe chunking sémantique utilise la similarité entre phrases consécutives pour détecter les ruptures thématiques : quand la similarité cosinus chute sous un seuil, on crée une nouvelle chunk. Produit des chunks plus cohérentes sémantiquement, au prix d'une complexité et d'un temps de traitement supérieurs."},
  {"role": "user", "content": "Et le late chunking ?"},
  {"role": "assistant", "content": "Le late chunking est une approche plus récente : on encode d'abord le document entier avec un modèle long-context, puis on découpe les embeddings produits plutôt que le texte brut. Chaque chunk hérite du contexte global du document. Nettement supérieur pour les documents avec des références internes (tableaux, acronymes définis en début de doc)."}
]}

Taille du dataset et qualité

Il n'existe pas de règle universelle, mais voici les ordres de grandeur observés en pratique : 500–2000 exemples suffisent pour encoder un style ou un format de sortie précis. 2000–10 000 exemples pour adapter un comportement sur un domaine spécifique. Au-delà de 10 000, les rendements décroissent sauf si la tâche est réellement complexe et diverse.

La qualité prime toujours sur la quantité. Un exemple de mauvaise qualité — réponse incorrecte, format incohérent, instruction ambiguë — n'est pas neutre : il dégrade activement l'entraînement. Avant d'entraîner, investissez dans le data cleaning : déduplication (MinHash ou embeddings + clustering), filtrage des exemples trop courts ou trop longs, vérification manuelle d'un échantillon.

Annoter et générer des données

LLM-as-annotator : utilisez GPT-4 ou Claude pour générer des exemples d'entraînement à partir de vos données brutes. Efficace pour bootstrapper un dataset, mais attention aux biais et erreurs qui se propagent. Toujours valider manuellement un sous-ensemble.

Argilla est l'outil open-source de référence pour l'annotation humaine collaborative. Interface web, gestion des conflits inter-annotateurs, export direct en JSONL. Indispensable pour les projets qui nécessitent une annotation humaine structurée.

Règle d'or Ne fine-tunez jamais sur des données que vous n'avez pas lues. Échantillonnez 50 exemples aléatoires, lisez-les un par un. Si vous trouvez des problèmes sur ces 50, le dataset entier en est rempli.

04 / Pipeline d'entraînement avec TRL

TRL (Transformer Reinforcement Learning) de Hugging Face est la bibliothèque de référence pour le SFT et le DPO. Son SFTTrainer est une surcouche de Trainer qui gère automatiquement le masquage des tokens d'instruction (on entraîne seulement sur les tokens de réponse), la mise en forme du dataset, et l'intégration avec PEFT pour LoRA.

Configuration QLoRA avec BitsAndBytes

La quantification 4-bit via BitsAndBytes réduit l'empreinte mémoire de Mistral 7B de ~14 Go (fp16) à ~4–5 Go pour le modèle de base, laissant de la VRAM disponible pour les activations et les adaptateurs LoRA. Sur une RTX 4090 (24 Go), cette configuration est confortable.

// finetune_mistral.py — SFTTrainer + QLoRA complet
import torch
from datasets import load_dataset
from transformers import (
    AutoModelForCausalLM, AutoTokenizer,
    BitsAndBytesConfig, TrainingArguments,
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer, SFTConfig

# ── 1. Configuration de la quantification 4-bit ──────────────────────
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",           # NF4 : meilleur que fp4 pour LLM
    bnb_4bit_compute_dtype=torch.bfloat16,  # calculs en bfloat16
    bnb_4bit_use_double_quant=True,        # double quantification → -0.4 GB VRAM
)

# ── 2. Chargement du modèle de base ──────────────────────────────────
model_id = "mistralai/Mistral-7B-Instruct-v0.3"
model = AutoModelForCausalLM.from_pretrained(
    model_id,
    quantization_config=bnb_config,
    device_map="auto",
    trust_remote_code=True,
)
tokenizer = AutoTokenizer.from_pretrained(model_id)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"  # évite les warnings de padding

# ── 3. Préparation pour l'entraînement kbit ──────────────────────────
model = prepare_model_for_kbit_training(model)

# ── 4. Configuration LoRA ────────────────────────────────────────────
lora_config = LoraConfig(
    r=16,                          # rang des matrices adaptateurs
    lora_alpha=32,                 # scaling : alpha/r = multiplicateur effectif
    target_modules=[              # couches à adapter (attention + FFN)
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj",
    ],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# → trainable params: 41,943,040 || all params: 3,793,147,904 || trainable%: 1.11%

# ── 5. Dataset ───────────────────────────────────────────────────────
dataset = load_dataset("json", data_files={
    "train": "data/train.jsonl",
    "eval":  "data/eval.jsonl",
})

# ── 6. Configuration de l'entraînement ──────────────────────────────
training_args = SFTConfig(
    output_dir="./checkpoints/mistral-7b-sft",
    num_train_epochs=3,
    per_device_train_batch_size=2,
    gradient_accumulation_steps=8,  # batch effectif = 16
    learning_rate=2e-4,
    lr_scheduler_type="cosine",
    warmup_ratio=0.05,              # 5% des steps en warmup
    fp16=False,
    bf16=True,                       # bfloat16 sur GPU Ampere+
    logging_steps=10,
    save_strategy="epoch",
    eval_strategy="epoch",
    load_best_model_at_end=True,
    max_seq_length=2048,
    dataset_text_field="text",     # ou utiliser formatting_func
    packing=False,                  # True → 2x plus vite, moins de contrôle
)

# ── 7. Entraînement ─────────────────────────────────────────────────
trainer = SFTTrainer(
    model=model,
    args=training_args,
    train_dataset=dataset["train"],
    eval_dataset=dataset["eval"],
    tokenizer=tokenizer,
)
trainer.train()

# ── 8. Sauvegarde des adaptateurs LoRA ──────────────────────────────
trainer.model.save_pretrained("./outputs/lora-adapter")
tokenizer.save_pretrained("./outputs/lora-adapter")

Axolotl : la config YAML simplifiée

Pour les équipes qui préfèrent une approche déclarative, Axolotl encapsule tout le pipeline dans un fichier YAML. Même résultat, moins de code Python à maintenir. Pratique pour standardiser les entraînements dans une équipe.

Hardware Mistral 7B en QLoRA 4-bit tient sur une RTX 4090 24 Go avec batch_size=2 et gradient_accumulation=8. Pour Mistral 22B, comptez au minimum 2×A100 80 Go. Pour Mistral 123B (Mistral Large), un cluster H100 est nécessaire même en QLoRA.
Empreinte VRAM — Mistral 7B Full fp16 LoRA fp16 QLoRA 4-bit 24 GB 40 GB 60 GB 80+ GB RTX 4090 ≥ 60 GB modèle (14 GB) + gradients + optimizer states ~20 GB ~10–14 GB ✓ RTX 4090 suffisante

Fig. 3 — Empreinte VRAM pour fine-tuner Mistral 7B selon la stratégie. QLoRA rend l'entraînement accessible sur GPU grand public.

05 / Évaluation post-entraînement

Le modèle est entraîné. La vraie question commence maintenant : est-ce qu'il est meilleur ? L'évaluation d'un LLM fine-tuné est plus subtile qu'un simple benchmark, car vous devez mesurer à la fois les gains sur votre tâche cible et les éventuelles régressions sur les capacités générales.

Métriques automatiques

Perplexité sur un ensemble de validation représentatif : indicateur de l'adéquation du modèle aux données du domaine. Une perplexité qui baisse est bon signe, mais ne garantit pas une meilleure qualité perçue.

Taux de completion : le modèle répond-il à la demande et ne coupe-t-il pas sa réponse prématurément ? Simple à mesurer sur un ensemble d'exemples de test.

Benchmarks domaine-spécifiques : construisez un ensemble de 50 à 200 questions représentatives avec des réponses de référence. Mesurez la similarité cosinus entre les embeddings des réponses générées et des réponses de référence, ou utilisez des métriques de correspondance exacte sur des tâches structurées.

LLM-as-judge

La méthode qui a le plus de valeur en pratique : soumettre les outputs du modèle fine-tuné et du modèle de base à un LLM puissant (GPT-4o ou Claude 3.5 Sonnet) qui joue le rôle de juge. On lui demande de comparer les deux réponses sur des critères précis — pertinence, précision, format, style — et de désigner un gagnant. Automatisable, scalable, corrélé avec les préférences humaines.

Détection de régression

Le risque le plus sous-estimé du fine-tuning : le catastrophic forgetting. Le modèle améliore ses performances sur la tâche cible mais perd des capacités générales — raisonnement mathématique, compréhension de l'anglais, suivi d'instructions complexes. Toujours évaluer le modèle fine-tuné sur des benchmarks généraux (MMLU, GSM8K) en plus des benchmarks domaine-spécifiques.

Évaluation comparative — Base vs Fine-tuned Base model Fine-tuned Score domaine 42% 87% Score général 74% 69% ⚠ -5% régression Taux de refus 18% 3% 0% 50% 100%

Fig. 4 — Résultats typiques après SFT sur un domaine spécifique. Gains importants sur la tâche cible, légère régression sur le score général à surveiller.

Le protocole de comparaison systématique

Ne vous fiez jamais à quelques exemples anecdotiques. Constituez un ensemble de test de 50 à 200 prompts représentatifs de l'usage réel, générez les réponses du modèle de base et du modèle fine-tuné, puis comparez-les systématiquement — par LLM-as-judge, par métriques automatiques, ou par annotation humaine. Documentez les résultats dans un tableau de bord reproductible.

06 / Conclusion et pièges à éviter

Le fine-tuning est un outil puissant, mais c'est le dernier recours dans la boîte à outils LLM, pas le premier. Avant d'y investir du temps et des ressources, assurez-vous d'avoir épuisé le prompt engineering, le few-shot, et — si le problème est lié à la connaissance — le RAG.

Les 3 pièges principaux

Overfitting sur un petit dataset. Symptôme : perplexité de validation qui remonte après quelques epochs, outputs qui ressemblent trop aux exemples d'entraînement sans généraliser. Remède : plus de données, moins d'epochs, augmentation du dropout LoRA, early stopping sur la validation loss.

Catastrophic forgetting. Le modèle excelle sur votre tâche mais régresse sur tout le reste. Visible quand on teste sur des prompts hors distribution. Partiellement atténué par LoRA (qui ne touche qu'une fraction des poids), mais pas éliminé. Remède : mélanger dans le dataset d'entraînement une proportion d'exemples généraux (10–20%), technique connue sous le nom de replay.

Distribution shift. Les données d'entraînement ne représentent pas fidèlement la distribution des inputs réels en production. Le modèle fine-tuné performe bien sur le test set mais déçoit en production. Remède : collecter des données représentatives du trafic réel, pas d'exemples idéaux construits en chambre.

À retenir SFT pour encoder un comportement. LoRA/QLoRA pour le faire sur votre GPU. DPO pour affiner l'alignement sur des préférences. Évaluez systématiquement, en comparant base model et fine-tuned sur des métriques domaine ET générales. Et n'entraînez jamais sur des données que vous n'avez pas vérifiées manuellement.

Un projet de fine-tuning Mistral ?

DEEP-5 accompagne les équipes de la constitution du dataset à la mise en production du modèle fine-tuné. Parlons-en.

Prendre contact →