Fine-tuning : Spécialiser les LLMs
Le fine-tuning est la technique permettant d'adapter un modèle pré-entraîné (GPT, Llama, Claude) à un domaine ou tâche spécifique. Plutôt que d'entraîner un modèle from scratch (coût prohibitif : millions €), le fine-tuning tire parti des capacités générales déjà apprises et les affine sur vos données propriétaires.
Pourquoi fine-tuner ? :
- Performance spécialisée : +15-40% accuracy vs zero-shot sur domaine spécifique
- Ton et style : Adapter réponses à votre marque/guidelines
- Données propriétaires : Internaliser connaissances uniques
- Compliance : Respecter vocabulaire/procédures métier
- Coût inférence : Modèle plus petit fine-tuné > grand modèle zero-shot
Quand fine-tuner vs RAG ? :
RAG (Retrieval-Augmented Generation):
✓ Données changent fréquemment (docs, FAQ à jour)
✓ Besoin sources citées (traçabilité)
✓ Pas besoin changer "comportement" modèle
✓ Setup rapide (jours)
→ Use case: Chatbot support, Q&A documentation
FINE-TUNING:
✓ Tâche spécifique répétitive (classification, extraction)
✓ Ton/style particulier (humour, formalité, jargon métier)
✓ Données stables (pas updates quotidiens)
✓ Besoin performances maximales domaine étroit
→ Use case: Génération code entreprise, analyse sentiment, NER médical
COMBINAISON (RAG + Fine-tuning):
✓ Best of both: Modèle fine-tuné + retrieval docs
→ Use case: Assistant juridique (fine-tuné droit + RAG jurisprudence)
Adoption fine-tuning
Selon une étude Hugging Face 2025, 43% des entreprises déployant LLMs en production utilisent fine-tuning. Les techniques PEFT (Parameter-Efficient Fine-Tuning) comme LoRA représentent 78% des fine-tunings grâce à leur efficacité (coût 40x inférieur vs full fine-tuning).
Techniques de fine-tuning : Full, LoRA, QLoRA
1. Full Fine-Tuning (Fine-tuning complet)
Principe : Réentraîner tous les paramètres du modèle sur nouvelles données.
FULL FINE-TUNING:
Modèle base: Llama 4 70B (70 milliards paramètres)
↓
Training: Update ALL 70B parameters
↓
Modèle final: Llama 4 70B-MyCompany (70B params, tous modifiés)
AVANTAGES:
✓ Performance maximale (tous params optimisés pour tâche)
✓ Flexibilité totale (peut transformer radicalement modèle)
INCONVÉNIENTS:
✗ Coût GPU: 8× A100 80GB minimum (70B model)
✗ Temps: 2-7 jours training
✗ Données: 50k-100k+ exemples nécessaires
✗ Stockage: 140GB modèle final (vs 250MB LoRA)
✗ Catastrophic forgetting: Perd capacités générales
COÛT ESTIMÉ (Llama 4 70B):
├── GPUs: 8× A100 (80GB), 5 jours
├── Cloud (AWS p4d): $32/h × 8 GPU × 120h = $30,720
├── + Engineering: 1 ML engineer × 2 semaines = $8,000
└── TOTAL: ~$38,000 par fine-tune
→ Réservé grandes entreprises, budgets conséquents
2. LoRA (Low-Rank Adaptation)
Révolution 2021 : Au lieu de modifier tous paramètres, LoRA ajoute petites matrices (adapters) apprenables.
Principe mathématique :
Standard fine-tuning:
W (matrice poids) → W' (modifiée)
→ Nécessite stocker/update matrice complète
LoRA:
W reste FIXE (frozen)
Ajoute: ΔW = B × A (où B et A sont matrices low-rank)
W_effective = W + ΔW
→ Update seulement B et A (1% taille de W)
Exemple Llama 4 70B:
• Matrice attention: 8192 × 8192 = 67M paramètres
• LoRA rank r=16:
- Matrix A: 8192 × 16 = 131k params
- Matrix B: 16 × 8192 = 131k params
- Total LoRA: 262k params (0.4% original!)
→ 70B modèle → 67M LoRA params trainable (99.9% frozen)
Avantages LoRA :
COMPARAISON Full vs LoRA (Llama 4 70B):
┌──────────────────────────────────────────────────────────┐
│ Full FT LoRA (r=16) Ratio │
├──────────────────────────────────────────────────────────┤
│ Params: 70B 67M 1000x │
│ Storage: 140GB 250MB 560x │
│ GPU: 8× A100 1× A100 8x │
│ Training: 120h 8h 15x │
│ Cost: $38k $1,200 32x │
│ Quality: 100% 94-98% -2-6% │
└──────────────────────────────────────────────────────────┘
→ LoRA: 32x moins cher, 94-98% performance
→ Trade-off acceptable pour MAJORITÉ use cases
Code LoRA :
from transformers import AutoModelForCausalLM, TrainingArguments, Trainer
from peft import LoraConfig, get_peft_model, TaskType
from datasets import load_dataset
# 1. Load base model (Llama 4 70B)
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-4-70B",
load_in_8bit=True, # Quantization (fit in less GPU)
device_map="auto",
token="hf_..."
)
# 2. Configure LoRA
lora_config = LoraConfig(
r=16, # Rank (8-64 typical)
lora_alpha=32, # Scaling factor (typically 2×r)
target_modules=[ # Modules to adapt
"q_proj", # Query projection
"k_proj", # Key projection
"v_proj", # Value projection
"o_proj", # Output projection
# "gate_proj", "up_proj", "down_proj" # MLP (optional)
],
lora_dropout=0.05, # Dropout for regularization
bias="none", # Train bias? ("none", "all", "lora_only")
task_type=TaskType.CAUSAL_LM # Task type
)
# 3. Wrap model with LoRA
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# Output: "trainable params: 67,108,864 || all params: 70,014,220,288 || trainable%: 0.096"
# 4. Load dataset
dataset = load_dataset("json", data_files={
"train": "data/train.jsonl", # 10k examples
"test": "data/test.jsonl" # 1k examples
})
# Format: {"input": "...", "output": "..."}
# 5. Training arguments
training_args = TrainingArguments(
output_dir="./llama4-70b-lora-company",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-4, # LoRA typical LR
fp16=True, # Mixed precision
logging_steps=10,
save_strategy="epoch",
evaluation_strategy="epoch"
)
# 6. Train
trainer = Trainer(
model=model,
args=training_args,
train_dataset=dataset["train"],
eval_dataset=dataset["test"]
)
trainer.train()
# 7. Save LoRA adapters (250MB only!)
model.save_pretrained("./llama4-70b-lora-company")
# ════════════════════════════════════════════════════════
# INFERENCE avec LoRA
# ════════════════════════════════════════════════════════
from peft import PeftModel
# Load base model
base_model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-4-70B")
# Load LoRA adapters (fusion avec base)
model = PeftModel.from_pretrained(base_model, "./llama4-70b-lora-company")
# Generate
inputs = tokenizer("Question utilisateur...", return_tensors="pt")
outputs = model.generate(**inputs, max_new_tokens=200)
print(tokenizer.decode(outputs[0]))
3. QLoRA (Quantized LoRA)
Innovation 2023 : Combine LoRA + Quantization 4-bit pour fine-tuner sur 1 seul GPU consumer.
Principe :
LoRA: 70B model → Nécessite 1× A100 (80GB) minimum
QLoRA: 70B model quantizé 4-bit
├── Modèle base: 4-bit (70B × 0.5 bytes = 35GB)
├── LoRA adapters: FP16 (250MB)
├── Gradients + optimizer: ~10GB
└── TOTAL: ~45GB → FIT on 1× A100 (80GB) ou 2× RTX 4090 (48GB)!
→ Fine-tune 70B model sur consumer hardware ($3k GPU)
Code QLoRA :
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
# 1. Quantization config (4-bit)
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_use_double_quant=True, # Double quantization (better)
bnb_4bit_quant_type="nf4" # NormalFloat 4-bit
)
# 2. Load model en 4-bit
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-4-70B",
quantization_config=bnb_config,
device_map="auto"
)
# 3. Prepare for training
model = prepare_model_for_kbit_training(model)
# 4. LoRA config (identique)
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
model = get_peft_model(model, lora_config)
# 5. Train (identique à LoRA)
# ...
# RÉSULTAT:
# - Fit on 1× A100 (80GB) ou 2× RTX 4090 (48GB total)
# - Quality: ~94% vs full LoRA FP16 (minimal loss)
# - Training: +20% slower vs LoRA FP16 (acceptable)
Comparaison techniques :
┌────────────────────────────────────────────────────────────────┐
│ Full FT LoRA QLoRA When to use? │
├────────────────────────────────────────────────────────────────┤
│ Quality 100% 96% 94% │
│ GPU 8×A100 1×A100 1×A100 Full: Jamais (sauf R&D)│
│ Time 120h 8h 10h LoRA: Production std │
│ Cost $38k $1.2k $1.2k QLoRA: Budget limité │
│ Storage 140GB 250MB 250MB │
└────────────────────────────────────────────────────────────────┘
RECOMMANDATION 2025:
→ 95% use cases: LoRA ou QLoRA suffisants
→ Full fine-tuning: Seulement si budget illimité ET besoin 100% perf
Dataset preparation : La clé du succès
Taille dataset recommandée
RÈGLES GÉNÉRALES:
Tâche simple (classification, NER):
├── Minimum: 500 exemples
├── Recommandé: 2,000-5,000
└── Optimal: 10,000+
Tâche complexe (génération, raisonnement):
├── Minimum: 2,000 exemples
├── Recommandé: 10,000-50,000
└── Optimal: 100,000+
Changement ton/style (brand voice):
├── Minimum: 500 exemples qualité
├── Recommandé: 2,000-5,000
└── Optimal: 10,000+
QUALITÉ > QUANTITÉ:
→ 1,000 exemples parfaits > 10,000 bruités
Format dataset
Format standard : JSONL (1 exemple par ligne)
{"input": "User: Quelle est la procédure de remboursement?\nAssistant:", "output": "Voici la procédure de remboursement en 3 étapes:\n1. Contactez le SAV à sav@company.com\n2. Obtenez un numéro RMA\n3. Renvoyez le produit sous 14 jours\nRemboursement effectué sous 7-10 jours ouvrés."}
{"input": "User: Comment changer mon mot de passe?\nAssistant:", "output": "Pour changer votre mot de passe:\n1. Allez dans Paramètres > Sécurité\n2. Cliquez 'Changer mot de passe'\n3. Entrez ancien mot de passe puis nouveau (min 8 caractères)\n4. Validez\nVous recevrez email de confirmation."}
{"input": "User: Quels sont vos délais de livraison?\nAssistant:", "output": "Nos délais de livraison sont:\n• France métropolitaine: 2-3 jours ouvrés (Standard)\n• Express: 24h (supplément €5)\n• International: 5-7 jours ouvrés\nLivraison gratuite dès €50 d'achat."}
Format instruction-tuning (Alpaca-style) :
{
"instruction": "Réponds à cette question de support client de manière professionnelle et concise.",
"input": "Quelle est la procédure de remboursement?",
"output": "Voici la procédure de remboursement en 3 étapes:\n1. Contactez le SAV à sav@company.com\n2. Obtenez un numéro RMA\n3. Renvoyez le produit sous 14 jours\nRemboursement effectué sous 7-10 jours ouvrés."
}
Data augmentation
# Augmentation dataset avec GPT-4 (boostrap quality examples)
from openai import OpenAI
client = OpenAI()
def augment_example(original_example, num_variants=3):
"""Génère variants d'un exemple via GPT-4"""
prompt = f"""
Générez {num_variants} variations de cette paire question-réponse
de support client. Préservez le ton professionnel et les informations
clés, mais reformulez différemment.
ORIGINAL:
Input: {original_example['input']}
Output: {original_example['output']}
VARIANTS (JSON array):
"""
response = client.chat.completions.create(
model="gpt-4-turbo",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}
)
variants = json.loads(response.choices[0].message.content)
return variants
# Exemple usage
original = {
"input": "Quelle est la procédure de remboursement?",
"output": "Contactez SAV, obtenez RMA, renvoyez produit..."
}
variants = augment_example(original, num_variants=5)
# Résultat: 1 exemple → 6 exemples (original + 5 variants)
# 100 exemples manuels → 600 exemples total avec augmentation
Hyperparamètres : Optimiser le fine-tuning
Hyperparamètres clés
# Hyperparamètres critiques LoRA/QLoRA
from transformers import TrainingArguments
training_args = TrainingArguments(
# ════════════════════════════════════════════════════════
# LEARNING RATE (LR) - CRITIQUE
# ════════════════════════════════════════════════════════
learning_rate=2e-4, # LoRA: 1e-4 à 3e-4 (vs 1e-5 full FT)
# QLoRA: 2e-4 à 5e-4 (légèrement plus haut)
# ════════════════════════════════════════════════════════
# BATCH SIZE
# ════════════════════════════════════════════════════════
per_device_train_batch_size=4, # Par GPU
gradient_accumulation_steps=4, # Effective batch = 4×4 = 16
# Règle: Effective batch size 16-64 optimal
# Si GPU memory error → Réduire batch, augmenter gradient_acc
# ════════════════════════════════════════════════════════
# EPOCHS
# ════════════════════════════════════════════════════════
num_train_epochs=3, # LoRA: 3-5 epochs suffisent
# Full FT: 1-2 epochs (overfitting rapide)
# ════════════════════════════════════════════════════════
# SCHEDULER
# ════════════════════════════════════════════════════════
lr_scheduler_type="cosine", # "linear", "cosine", "constant"
warmup_ratio=0.1, # 10% steps warmup (LR graduel)
# ════════════════════════════════════════════════════════
# REGULARIZATION
# ════════════════════════════════════════════════════════
weight_decay=0.01, # L2 regularization (anti-overfitting)
max_grad_norm=1.0, # Gradient clipping
# ════════════════════════════════════════════════════════
# PRECISION
# ════════════════════════════════════════════════════════
fp16=True, # Mixed precision (A100, RTX 30xx/40xx)
# bf16=True, # BFloat16 (H100, A100 only, better than FP16)
# ════════════════════════════════════════════════════════
# LOGGING & SAVING
# ════════════════════════════════════════════════════════
logging_steps=10,
evaluation_strategy="epoch",
save_strategy="epoch",
save_total_limit=3, # Keep only 3 last checkpoints
# ════════════════════════════════════════════════════════
# OUTPUT
# ════════════════════════════════════════════════════════
output_dir="./checkpoints",
report_to="tensorboard" # ou "wandb" pour Weights & Biases
)
# LoRA-specific config
lora_config = LoraConfig(
r=16, # Rank: 8 (small), 16 (balanced), 32-64 (large)
# Higher = plus capacité, mais plus params
lora_alpha=32, # Scaling: Typically 2×r ou r
# Contrôle magnitude updates
lora_dropout=0.05, # 0.05-0.1 typique (anti-overfitting)
target_modules=[
"q_proj", "k_proj", "v_proj", "o_proj" # Attention (minimum)
# + "gate_proj", "up_proj", "down_proj" # MLP (plus params, mieux)
]
)
Grid search hyperparams (optionnel)
# Automatiser recherche meilleurs hyperparams
from itertools import product
# Define search space
lr_values = [1e-4, 2e-4, 3e-4]
rank_values = [8, 16, 32]
batch_sizes = [4, 8]
best_eval_loss = float('inf')
best_config = None
for lr, r, batch_size in product(lr_values, rank_values, batch_sizes):
print(f"Testing: LR={lr}, Rank={r}, Batch={batch_size}")
# Configure
lora_config = LoraConfig(r=r, lora_alpha=2*r, ...)
training_args = TrainingArguments(learning_rate=lr, per_device_train_batch_size=batch_size, ...)
# Train
model = get_peft_model(base_model, lora_config)
trainer = Trainer(model=model, args=training_args, ...)
trainer.train()
# Evaluate
eval_results = trainer.evaluate()
eval_loss = eval_results['eval_loss']
if eval_loss < best_eval_loss:
best_eval_loss = eval_loss
best_config = {"lr": lr, "rank": r, "batch_size": batch_size}
print(f"Best config: {best_config}, Loss: {best_eval_loss}")
# Note: Coûteux (9 runs dans exemple). Réserver petits datasets.
Évaluation et métriques
Métriques automatiques
# Évaluation quantitative fine-tuning
from transformers import Trainer
from datasets import load_metric
import numpy as np
# Load metrics
perplexity_metric = load_metric("perplexity")
bleu_metric = load_metric("sacrebleu")
rouge_metric = load_metric("rouge")
def compute_metrics(eval_pred):
predictions, labels = eval_pred
# Decode predictions et labels
decoded_preds = tokenizer.batch_decode(predictions, skip_special_tokens=True)
decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)
# BLEU (overlap n-grams)
bleu_result = bleu_metric.compute(
predictions=decoded_preds,
references=[[label] for label in decoded_labels]
)
# ROUGE (overlap séquences)
rouge_result = rouge_metric.compute(
predictions=decoded_preds,
references=decoded_labels
)
# Perplexity (lower = better)
loss = eval_pred.predictions[0] # Si disponible
perplexity = np.exp(loss) if loss is not None else None
return {
"bleu": bleu_result["score"],
"rouge1": rouge_result["rouge1"].mid.fmeasure,
"rouge2": rouge_result["rouge2"].mid.fmeasure,
"rougeL": rouge_result["rougeL"].mid.fmeasure,
"perplexity": perplexity
}
# Trainer with metrics
trainer = Trainer(
model=model,
args=training_args,
train_dataset=dataset["train"],
eval_dataset=dataset["test"],
compute_metrics=compute_metrics # ← Ajout
)
# Evaluate
results = trainer.evaluate()
print(results)
# Output:
# {
# "eval_loss": 0.87,
# "eval_bleu": 42.3,
# "eval_rouge1": 0.58,
# "eval_rouge2": 0.34,
# "eval_rougeL": 0.51,
# "eval_perplexity": 2.39
# }
A/B testing human evaluation
# Évaluation humaine (qualité réelle)
import random
def ab_test_models(base_model, finetuned_model, test_queries, num_evals=50):
"""Compare base vs fine-tuned via évaluations humaines"""
results = {"base_wins": 0, "finetuned_wins": 0, "ties": 0}
for i in range(num_evals):
query = random.choice(test_queries)
# Generate avec les 2 modèles
response_base = base_model.generate(query)
response_finetuned = finetuned_model.generate(query)
# Blind evaluation (ordre randomisé)
if random.random() > 0.5:
print(f"\nQuery: {query}\n")
print("Response A:", response_base)
print("Response B:", response_finetuned)
mapping = {"A": "base", "B": "finetuned"}
else:
print(f"\nQuery: {query}\n")
print("Response A:", response_finetuned)
print("Response B:", response_base)
mapping = {"A": "finetuned", "B": "base"}
# Human choice
choice = input("Meilleure réponse? (A/B/Tie): ").strip().upper()
if choice == "TIE":
results["ties"] += 1
elif mapping[choice] == "base":
results["base_wins"] += 1
else:
results["finetuned_wins"] += 1
# Stats
print("\n" + "="*50)
print("RÉSULTATS A/B TEST:")
print(f"Base model: {results['base_wins']} wins ({results['base_wins']/num_evals*100:.1f}%)")
print(f"Fine-tuned: {results['finetuned_wins']} wins ({results['finetuned_wins']/num_evals*100:.1f}%)")
print(f"Ties: {results['ties']} ({results['ties']/num_evals*100:.1f}%)")
print("="*50)
return results
# Exemple résultat:
# Base model: 8 wins (16%)
# Fine-tuned: 37 wins (74%)
# Ties: 5 (10%)
# → Fine-tuning améliore significativement qualité perçue
Cas d'usage production
1. Customer support chatbot
ENTREPRISE: E-commerce (Cdiscount-like)
PROBLÈME:
Chatbot GPT-4 zero-shot:
├── Réponses génériques (pas brand voice)
├── Manque procédures spécifiques (remboursement, SAV)
├── Coût: $0.03/requête × 50k/jour = $1,500/jour
SOLUTION: Fine-tuning Llama 4 13B
DATASET:
├── 12,000 conversations support réelles (annotées qualité)
├── 3,500 FAQ internes
├── 800 procédures SAV (docs transformés en Q&A)
└── Total: 16,300 exemples
FINE-TUNING:
├── Modèle: Llama 4 13B (self-hosted)
├── Technique: QLoRA (r=16, 1× A100)
├── Durée: 6 heures training
├── Coût: $290 (cloud GPU)
RÉSULTATS (vs GPT-4 zero-shot):
✓ Résolution autonome: 68% → 84% (+16 pts)
✓ Satisfaction (CSAT): 78 → 91 (+13 pts)
✓ Tone consistency: 82% → 96% (brand voice)
✓ Coût/requête: $0.03 → $0.002 (15x cheaper)
ROI: Break-even en 3 jours ($1,500/j économie)
2. Code generation entreprise
ENTREPRISE: FinTech (Banking)
PROBLÈME:
Devs perdent temps écrire boilerplate code (APIs, tests)
GPT-4 génère code générique (pas conventions internes)
SOLUTION: Fine-tuning CodeLlama 34B
DATASET:
├── 8,400 fonctions Python internes (avec tests)
├── 2,100 APIs FastAPI (architecture standard entreprise)
├── 1,200 configs Terraform (infra as code)
└── Total: 11,700 code examples
FINE-TUNING:
├── Modèle: CodeLlama 34B
├── Technique: LoRA (r=32, target all linear layers)
├── Durée: 14 heures
├── Coût: $680
RÉSULTATS:
✓ Code conventions: 89% respect guidelines (vs 34% GPT-4)
✓ Tests generated: 94% pass CI (vs 67%)
✓ Developer productivity: +32% (self-reported survey)
✓ Adoption: 87% devs utilisent quotidiennement
ROI: 450 devs × 1h/jour économisé × $80/h = $36k/jour
Payback time: <1 jour
Articles connexes
- Meta Llama 4 : L'open source qui défie les modèles propriétaires
- RAG et vector databases : Architecture moderne d'IA générative
- GPT-5 : OpenAI dévoile son modèle révolutionnaire pour 2026
Conclusion : Fine-tuning accessible à tous
Le fine-tuning est devenu accessible grâce aux techniques PEFT (LoRA, QLoRA). Ce qui coûtait $50k en 2023 coûte maintenant $1k en 2025, avec performance équivalente.
Quand fine-tuner :
- Tâche spécifique répétitive (classification, extraction, génération)
- Ton/style particulier (brand voice, jargon métier)
- Besoin performance maximale (vs zero-shot)
- Données stables (pas updates quotidiens)
Technique recommandée 2025 :
- 90% cas : LoRA ou QLoRA (r=16-32)
- 10% cas : Full fine-tuning (budgets illimités)
Best practices :
- Dataset : 2k-10k exemples qualité > 100k bruités
- Hyperparams : LR 2e-4, rank 16, 3 epochs (starting point)
- Évaluation : Métriques auto + A/B testing humain
- Production : Self-hosted Llama 4 fine-tuned < GPT-4 API (coût 15-30x moins cher)
2026 : L'explosion de fine-tunings spécialisés, chaque entreprise aura ses modèles customs. Le fine-tuning devient aussi standard que SQL databases.


