Fine-tuning des LLM : Guide pratique complet
Apprenez à fine-tuner efficacement les grands modèles de langage pour vos applications spécifiques.
#Fine-tuning des LLM : Du concept à la production
Le fine-tuning est devenu une technique essentielle pour adapter les grands modèles de langage (LLM) à des tâches spécifiques. Dans cet article, nous explorons les meilleures pratiques pour fine-tuner efficacement vos modèles.
#Concepts fondamentaux
#Qu'est-ce que le fine-tuning ?
Le fine-tuning consiste à adapter un modèle pré-entraîné à une tâche spécifique en continuant l'entraînement sur un dataset ciblé. Cette approche permet de :
- Réduire le temps d'entraînement comparé à un entraînement from scratch
- Améliorer les performances sur des tâches spécifiques
- Nécessiter moins de données pour obtenir de bons résultats
#Types de fine-tuning
#Full Fine-tuning
Mise à jour de tous les paramètres du modèle :
# Configuration pour full fine-tuning
optimizer = AdamW(model.parameters(), lr=2e-5)
scheduler = get_linear_schedule_with_warmup(
optimizer,
num_warmup_steps=warmup_steps,
num_training_steps=total_steps
)
for batch in dataloader:
optimizer.zero_grad()
outputs = model(**batch)
loss = outputs.loss
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
scheduler.step()
#Parameter Efficient Fine-tuning (PEFT)
#LoRA (Low-Rank Adaptation)
LoRA permet de fine-tuner efficacement en ajoutant des matrices de rang faible :
from peft import LoraConfig, get_peft_model
# Configuration LoRA
config = LoraConfig(
r=16, # Rang des matrices de décomposition
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.1,
)
# Application de LoRA au modèle
model = get_peft_model(model, config)
# Seulement ~1% des 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"Paramètres entraînables : {trainable_params}/{total_params} ({100*trainable_params/total_params:.2f}%)")
#QLoRA (Quantized LoRA)
Combine quantification 4-bit et LoRA pour une efficacité maximale :
from transformers import BitsAndBytesConfig
# Configuration de quantification
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
# Chargement du modèle quantifié
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
torch_dtype=torch.bfloat16,
device_map="auto"
)
#Préparation des données
#Format des données
Pour le fine-tuning d'instruction, utilisez un format structuré :
def format_instruction(sample):
"""
Formate les données d'entraînement pour instruction following
"""
return f"""### Instruction:
{sample['instruction']}
### Input:
{sample['input']}
### Response:
{sample['output']}"""
# Preprocessing du dataset
def preprocess_function(examples):
model_inputs = tokenizer(
[format_instruction(ex) for ex in examples],
truncation=True,
padding="max_length",
max_length=512,
return_tensors="pt"
)
return model_inputs
#Augmentation de données
Techniques pour enrichir votre dataset :
def augment_data(samples):
"""
Augmente le dataset avec des variations
"""
augmented = []
for sample in samples:
# Paraphrase de l'instruction
paraphrased = paraphrase_instruction(sample['instruction'])
augmented.append({
'instruction': paraphrased,
'input': sample['input'],
'output': sample['output']
})
# Variation du contexte
if sample['input']:
variations = generate_input_variations(sample['input'])
for var in variations:
augmented.append({
'instruction': sample['instruction'],
'input': var,
'output': adapt_output(sample['output'], var)
})
return augmented
#Stratégies d'entraînement
#Learning Rate Scheduling
# Warmup puis décroissance linéaire
def get_cosine_schedule_with_warmup(optimizer, num_warmup_steps, num_training_steps):
def lr_lambda(current_step):
if current_step < num_warmup_steps:
return float(current_step) / float(max(1, num_warmup_steps))
progress = float(current_step - num_warmup_steps) / float(max(1, num_training_steps - num_warmup_steps))
return max(0.0, 0.5 * (1.0 + math.cos(math.pi * progress)))
return LambdaLR(optimizer, lr_lambda)
#Gradient Accumulation
Pour gérer la mémoire avec de gros modèles :
accumulation_steps = 8
optimizer.zero_grad()
for i, batch in enumerate(dataloader):
outputs = model(**batch)
loss = outputs.loss / accumulation_steps
loss.backward()
if (i + 1) % accumulation_steps == 0:
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
scheduler.step()
optimizer.zero_grad()
#Évaluation et monitoring
#Métriques d'évaluation
def evaluate_model(model, eval_dataloader):
model.eval()
total_loss = 0
predictions = []
references = []
with torch.no_grad():
for batch in eval_dataloader:
outputs = model(**batch)
total_loss += outputs.loss.item()
# Génération pour évaluation qualitative
generated = model.generate(
batch['input_ids'],
max_length=256,
temperature=0.7,
do_sample=True,
pad_token_id=tokenizer.eos_token_id
)
predictions.extend(tokenizer.batch_decode(generated, skip_special_tokens=True))
references.extend(batch['labels'])
# Calcul des métriques
perplexity = math.exp(total_loss / len(eval_dataloader))
bleu_score = compute_bleu(predictions, references)
return {
'perplexity': perplexity,
'bleu': bleu_score,
'loss': total_loss / len(eval_dataloader)
}
#Early Stopping
class EarlyStopping:
def __init__(self, patience=3, min_delta=0.001):
self.patience = patience
self.min_delta = min_delta
self.counter = 0
self.best_loss = float('inf')
def __call__(self, val_loss):
if val_loss < self.best_loss - self.min_delta:
self.best_loss = val_loss
self.counter = 0
return False
else:
self.counter += 1
return self.counter >= self.patience
#Optimisations avancées
#DeepSpeed ZeRO
Pour l'entraînement distribué :
# Configuration DeepSpeed
deepspeed_config = {
"train_batch_size": 64,
"gradient_accumulation_steps": 8,
"optimizer": {
"type": "AdamW",
"params": {"lr": 2e-5}
},
"zero_optimization": {
"stage": 2,
"offload_optimizer": {"device": "cpu"},
"offload_param": {"device": "cpu"}
}
}
# Initialisation
model_engine, optimizer, _, _ = deepspeed.initialize(
model=model,
config=deepspeed_config
)
#Gradient Checkpointing
Pour réduire l'utilisation mémoire :
# Activation du gradient checkpointing
model.gradient_checkpointing_enable()
# Configuration fine
if hasattr(model.config, 'use_cache'):
model.config.use_cache = False
#Déploiement et production
#Optimisation pour l'inférence
# Compilation avec TorchScript
model.eval()
traced_model = torch.jit.trace(model, example_input)
traced_model.save("model_traced.pt")
# Quantification dynamique
quantized_model = torch.quantization.quantize_dynamic(
model, {torch.nn.Linear}, dtype=torch.qint8
)
#Monitoring en production
def monitor_model_performance(model, inputs, outputs):
"""
Surveille les performances du modèle en production
"""
metrics = {
'latency': measure_latency(model, inputs),
'perplexity': calculate_perplexity(outputs),
'confidence': calculate_confidence(outputs),
'token_count': count_tokens(outputs)
}
# Alertes si dégradation
if metrics['perplexity'] > threshold_perplexity:
send_alert("Model performance degradation detected")
return metrics
#Conclusion
Le fine-tuning des LLM est un art qui combine technique et intuition. Les approches PEFT comme LoRA permettent aujourd'hui de fine-tuner efficacement même les plus gros modèles avec des ressources limitées.
Les clés du succès sont :
- Qualité des données d'entraînement
- Choix de la stratégie de fine-tuning adaptée
- Monitoring continu des performances
- Optimisation pour la production
Cette approche permet de créer des modèles hautement spécialisés tout en conservant les capacités générales acquises lors du pré-entraînement.
Partager cet article
Simon Stephan
Senior AI Researcher & Developer spécialisé en Deep Learning et NLP. Passionné par l'innovation et le partage de connaissances.
Voir mon profil complet →