Retour au blog
#LLM#Fine-tuning#GPT#BERT#Machine Learning

Fine-tuning des LLM : Guide pratique complet

Apprenez à fine-tuner efficacement les grands modèles de langage pour vos applications spécifiques.

SS
Simon Stephan
5 min read
1.2k vues

#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

SS

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