Chapitre 1 : Approche d'Ajustement Fin Traditionnel
Mise à Jour de Tous les Paramètres du Modèle
L'ajustement fin traditionnel implique la mise à jour de chaque paramètre entraînable du modèle pendant l'entraînement. Cela offre la flexibilité maximale pour adapter les modèles à de nouvelles tâches.
Comment Fonctionne l'Ajustement Fin Complet
La Boucle d'Entraînement
- Passage Avant : Les données traversent l'ensemble du modèle
- Calcul de la Perte : Comparer la prédiction à la cible
- Passage Arrière : Calculer les gradients pour tous les paramètres
- Mise à Jour des Poids : Appliquer les gradients pour mettre à jour tous les poids
- Répéter : Continuer jusqu'à convergence
Exemple Complet d'Entraînement
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
TrainingArguments,
Trainer,
DataCollatorForLanguageModeling,
)
from datasets import load_dataset
# Charger le modèle et le tokenizer
model_name = "meta-llama/Llama-3.1-8B"
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
# Charger le modèle - tous les paramètres sont entraînables par défaut
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.bfloat16, # Utiliser bf16 pour l'efficacité mémoire
device_map="auto",
)
# Vérifier que tous les paramètres sont entraînables
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
total_params = sum(p.numel() for p in model.parameters())
print(f"Entraînable : {trainable_params:,} / {total_params:,} = 100%")
# Préparer le jeu de données
dataset = load_dataset("databricks/dolly-15k", split="train[:1000]")
def format_instruction(example):
return {
"text": f"### Instruction:\n{example['instruction']}\n\n### Réponse:\n{example['response']}"
}
dataset = dataset.map(format_instruction)
def tokenize(example):
return tokenizer(
example["text"],
truncation=True,
max_length=512,
padding="max_length",
)
tokenized_dataset = dataset.map(tokenize, remove_columns=dataset.column_names)
# Configuration de l'entraînement
training_args = TrainingArguments(
output_dir="./llama-full-ft",
num_train_epochs=3,
per_device_train_batch_size=1, # Limité par la mémoire
gradient_accumulation_steps=16, # Simuler une taille de lot de 16
learning_rate=2e-5,
weight_decay=0.01,
warmup_ratio=0.03,
logging_steps=10,
save_strategy="epoch",
bf16=True, # Utiliser bfloat16
gradient_checkpointing=True, # Économiser de la mémoire
optim="adamw_torch_fused", # Optimiseur efficace
)
# Créer le formateur
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset,
data_collator=DataCollatorForLanguageModeling(tokenizer, mlm=False),
)
# Entraîner
trainer.train()
# Sauvegarder
model.save_pretrained("./llama-full-ft-final")
tokenizer.save_pretrained("./llama-full-ft-final")
Composants Clés Expliqués
Taux d'Apprentissage
learning_rate = 2e-5 # Plage courante : 1e-5 à 5e-5
Pour l'ajustement fin complet, utilisez des taux d'apprentissage plus bas que PEFT :
- Trop élevé : Le modèle diverge ou oublie l'entraînement préalable
- Trop bas : L'entraînement est lent, peut ne pas s'adapter suffisamment
Décroissance des Poids
weight_decay = 0.01 # Régularisation pour éviter le surapprentissage
La décroissance des poids empêche le modèle de surapprendre en pénalisant les poids élevés.
Échauffement
warmup_ratio = 0.03 # 3% des étapes d'entraînement
Augmentez progressivement le taux d'apprentissage au début pour un entraînement stable.
Point de Contrôle des Gradients
gradient_checkpointing = True # Échanger le calcul contre la mémoire
Recalculer les activations pendant le passage arrière au lieu de les stocker—économise ~30% de mémoire.
Choix de l'Optimiseur
| Optimiseur | Mémoire | Vitesse | Qualité |
|---|---|---|---|
| AdamW | Élevée | Rapide | Meilleure |
| Adam 8-bit | Moyenne | Moyenne | Bonne |
| SGD | Faible | Lent | Nécessite un ajustement |
| Adafactor | Faible | Moyenne | Bonne |
# AdamW standard
optim = "adamw_torch"
# AdamW fusionné (plus rapide)
optim = "adamw_torch_fused"
# Adam 8-bit (moins de mémoire)
optim = "adamw_8bit"
# Adafactor (pas d'états de momentum)
optim = "adafactor"
Dynamiques d'Entraînement
Courbe de Perte
Époque 1 : Perte d'entraînement diminuant rapidement
(Le modèle s'adapte aux nouvelles données)
Époque 2 : Perte d'entraînement se stabilisant
(Surveiller la perte de validation pour le surapprentissage)
Époque 3 : Surapprentissage possible
(La perte d'entraînement continue de baisser, la validation peut augmenter)
Surveillance de l'Entraînement
# Utilisation de Weights & Biases
training_args = TrainingArguments(
# ... autres arguments ...
report_to="wandb",
run_name="llama-full-ft-experiment-1",
)
Principales métriques à surveiller :
- Perte d'Entraînement : Devrait diminuer
- Perte de Validation : Devrait diminuer mais surveiller la divergence par rapport à la perte d'entraînement
- Norme des Gradients : Les pics peuvent indiquer une instabilité
- Taux d'Apprentissage : Vérifier le fonctionnement de l'échauffement et de la décroissance
Entraînement en Précision Mixte
Pourquoi Utiliser la Précision Mixte ?
# Moitié de la mémoire, souvent un entraînement plus rapide
bf16 = True # Préféré pour les GPU plus récents (Ampere+)
# OU
fp16 = True # Pour les GPU plus anciens
| Précision | Mémoire | Plage | Stabilité |
|---|---|---|---|
| float32 | 4 octets | Complète | La plus élevée |
| bfloat16 | 2 octets | Complète | Élevée |
| float16 | 2 octets | Limitée | Moyenne |
Le Float16 a une plage plus étroite et peut causer des débordements/sous-débordements. Le BFloat16 maintient la plage du float32 avec une précision réduite, le rendant plus stable.
Sauvegarde et Chargement
Pendant l'Entraînement (Points de Contrôle)
training_args = TrainingArguments(
save_strategy="steps",
save_steps=500,
save_total_limit=3, # Conserver seulement les 3 derniers points de contrôle
)
Après l'Entraînement
# Sauvegarder le modèle complet
model.save_pretrained("./final-model")
tokenizer.save_pretrained("./final-model")
# Plus tard, le charger
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained("./final-model")
Avantages de l'Ajustement Fin Complet
Personnalisation Maximale
Tous les paramètres peuvent être ajustés à votre tâche.
Potentiellement la Meilleure Qualité
Aucune contrainte des poids gelés.
Simplicité
Pas besoin de configuration d'adaptateur.
Sortie Fusionnée
Fichier de modèle unique, pas de surcharge d'adaptateur à l'inférence.
Inconvénients
Exigences Élevées en Mémoire
Besoin de stocker le modèle, les gradients et les états de l'optimiseur.
Risque d'Oubli Catastrophique
Peut perdre les connaissances pré-entraînées.
Coûteux
Nécessite des ressources GPU importantes.
Entraînement Lent
Chaque paramètre doit être mis à jour.
Quand Utiliser l'Ajustement Fin Complet
| Cas d'Utilisation | Recommandation |
|---|---|
| Qualité maximale nécessaire | ✅ Ajustement Fin Complet |
| Ressources limitées | ❌ Utiliser PEFT |
| Expérimentation rapide | ❌ Utiliser PEFT |
| Production à grande échelle | ⚠️ Considérer les compromis |
| Recherche/benchmarking | ✅ Ajustement Fin Complet |
Points Clés à Retenir
✅ L'ajustement fin complet met à jour tous les paramètres du modèle
✅ Nécessite une mémoire GPU significative (modèle + gradients + optimiseur)
✅ Utiliser la précision mixte et le point de contrôle des gradients pour réduire la mémoire
✅ Utiliser des taux d'apprentissage plus bas que PEFT (1e-5 à 5e-5)
✅ Surveiller le surapprentissage avec la perte de validation
✅ Meilleur pour une personnalisation maximale lorsque les ressources le permettent
Prochaines Étapes
Continuez vers Chapitre 2 : Exigences Computationnelles pour comprendre les besoins exacts en mémoire et en calcul.
Vérification Rapide
Avant de continuer, assurez-vous de pouvoir répondre :
- Quels sont les quatre principaux composants stockés en mémoire GPU pendant l'entraînement ?
- Pourquoi le bfloat16 est-il souvent préféré au float16 ?
- Quelle plage de taux d'apprentissage est typique pour l'ajustement fin complet ?