Un LLM comme Qwen3.5-0.8B pèse ~1.7 Go de poids. Le fine-tuner sur une tâche, en réentraînant tous les poids, coûte cher en VRAM (il faut stocker les poids, leurs gradients, et l’état de l’optimiseur — typiquement 3-4× la taille du modèle) et risque de détruire ce que le modèle savait déjà (l’oubli catastrophique).
LoRA (Low-Rank Adaptation) part d’une observation empirique : quand on fine-tune un modèle, les changements de poids ont un rang intrinsèquement faible. Au lieu d’apprendre une matrice de delta complète (taille \(n \times n\)), on apprend deux petites matrices \(A\) (\(r \times n\)) et \(B\) (\(n \times r\)) dont le produit \(BA\) approche le delta — avec \(r \ll n\). On gèle les poids d’origine et on n’entraîne que \(A\) et \(B\).
Conséquences concrètes : - VRAM divisée par ~3-4 : on ne stocke pas le gradient ni l’état optimiseur des poids gelés. - Paramètres entraînables ~0,1-1 % du total. - Pas d’oubli catastrophique : les poids d’origine sont intacts, on peut retirer l’adaptateur et retrouver le modèle de base.
QLoRA ajoute une couche : on charge les poids gelés en 4-bit quantifié, ce qui réduit encore la VRAM. C’est ce qui rend le fine-tuning d’un LLM possible sur une carte de 8 Go.
Fil rouge : enseigner un format de sortie arbitraire
Le notebook 11 a montré comment quantifier un modèle pour l’inférence. Ici, nous allons plus loin : nous adaptons le comportement du modèle. La tâche est volontairement simple mais précise : répondre à une demande de définition dans un format balisé arbitraire que le modèle de base ne produit jamais spontanément —
Nous verrons que Qwen3.5-0.8B de base échoue visiblement sur ce format : il produit du prose avec des deux-points, ignore les balises ⟦T⟧/⟦D⟧/⟦E⟧. Un LoRA léger, entraîné sur quelques dizaines d’exemples bien formatés, corrige ce comportement. Le notebook démontre ce delta (avant / après) de bout en bout sur 8 Go de VRAM.
Angle pédagogique (Prong B). Nous aurions pu choisir un format que le base produit déjà (comme le JSON, qu’il maîtrise) — mais alors le fine-tuning ne démontrerait rien. Ici, le format balisé est étranger au modèle, son échec est visible, et la correction est mesurable : c’est ce qui fait la valeur didactique du notebook.
1. Configuration et imports
Nous travaillons dans l’environnement coursia-ml-training (torch CUDA, peft, transformers, bitsandbytes, trl, datasets). Si une librairie manque sur votre machine, installez-la (pip install peft bitsandbytes trl datasets) — nous n’utilisons aucun substitut.
# Parametres du notebookimport osimport torchfrom transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfigfrom peft import LoraConfig, get_peft_model, TaskTypeimport time, warningswarnings.filterwarnings("ignore")# Qwen3.5-0.8B : petit modele SOTA (generation 3.5, plus recente que Qwen2.5). Modele# local (archi multimodale VL a l'origine, mais AutoModelForCausalLM emprunte le chemin# texte -- la tour vision est ignoree, chemin prouve en PT-11 GRPO). 0.8B en QLoRA 4-bit# tient largement en 8 Go (< 1 Go au chargement).MODEL_NAME = os.path.expanduser("~/models/qwen35-0.8b")DEVICE_OK = torch.cuda.is_available()print(f"GPU disponible : {DEVICE_OK}")if DEVICE_OK:print(f"Carte : {torch.cuda.get_device_name(0)} ({torch.cuda.get_device_properties(0).total_memory /1e9:.1f} Go)")
La configuration BitsAndBytesConfig quantifie les poids gelés en NF4 (NormalFloat 4-bit) avec double quantification, et calcule en bf16. Le modèle se charge avec device_map="auto" ; nous mesurons la VRAM consommée juste après le chargement.
Modele charge en 5.7s
VRAM apres chargement : 0.78 Go / 8 Go
3. Le problème : le base échoue sur le format balisé
Interrogeons le modèle de base sur trois termes, avec une consigne de format strict. Observons ce qu’il produit réellement.
def generer(prompt, model, max_new_tokens=120):"""Generation deterministe (greedy) pour des sorties reproductibles."""# model.eval() : apres trainer.train() le modele est en mode .train(). Avec le# gradient checkpointing (active par defaut par SFTTrainer), le forward en mode train# differe du mode eval (use_cache force a False, comportement de generation degrade).# On repasse systematiquement en eval avant de generer. model.eval() msgs = [{"role": "user", "content": prompt}] text = tokenizer.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True) ids = tokenizer(text, return_tensors="pt").to(model.device)with torch.no_grad(): out = model.generate(**ids, max_new_tokens=max_new_tokens, do_sample=False, pad_token_id=tokenizer.eos_token_id, eos_token_id=tokenizer.eos_token_id,# repetition_penalty : un modele fine-tune sur un format rigide a tendance a# boucler sur un token. Une penalite legere (1.15) casse ces boucles sans# denaturer la sortie. repetition_penalty=1.15, )return tokenizer.decode(out[0][ids.input_ids.shape[1]:], skip_special_tokens=True)CONSIGNE_FORMAT = ("Pour chaque terme demande, reponds STRICTEMENT au format balise ""⟦T⟧terme⟦/T⟧⟦D⟧definition concise⟦/D⟧⟦E⟧exemple concret⟦/E⟧ ""sans aucun autre texte autour.\n\n")termes_test = ["Sharpe ratio", "Gradient descent", "Overfitting"]sorties_base = {}for terme in termes_test: prompt = CONSIGNE_FORMAT +f"Terme: {terme}" sortie = generer(prompt, base_model) sorties_base[terme] = sortieprint(f"--- Terme : {terme} ---")print(sortie)print()
--- Terme : Sharpe ratio ---
<br>
<div><b>Sharpe ratio</b><br><b>Definition concise</b><b>>Le rapport de Sharpe est un indicateur financier qui mesure la performance relative d'une stratégie en comparant le rendement attendu à une variance accrue par rapport aux risques (volatilité) du marché.<br><b>Exemple concret</b><b>>La Sharpe ratio = Rendement attendu / Variance des rendements
--- Terme : Gradient descent ---
<br>
<div><b>Gradient descent</b><br><b>Est un algorithme d'optimisation qui minimise une fonction de coût en ajustant les paramètres à la suite des erreurs.<br><b>Le nom vient du fait que le gradient est utilisé pour calculer l'écart entre la valeur actuelle et celle souhaitée.<br><b>Il commence par trouver un point initial (initialisation) où commencer l'apprentissage.<br><b>
--- Terme : Overfitting ---
<br>
<div><b>Overfitting</b><br><b>Definition</b><b>concise</b><b>>&nbsp;<i><b>•&nbsp; </i><b>Le surapprentissage est un problème d'apprentissage où une modèle apprend trop bien les données de l'entraînement que celles du test.<br><b>Exemple concrète<
Diagnostic. Le modèle de base ne respecte pas le format balisé demandé : il ignore les délimiteurs ⟦T⟧/⟦D⟧/⟦E⟧ et bascule sur du HTML (<div><b>…</b>) ou des délimiteurs erronés (⟦S⟧, ⟦R⟨) — jamais la structure ⟦T⟧…⟦/T⟧⟦D⟧…⟦/D⟧⟦E⟧…⟦/E⟧. Le respect du format est 0/3 au base. C’est précisément le comportement qu’un LoRA peut corriger — le vocabulaire de délimiteurs est étranger au modèle pré-entraîné, il faut le lui apprendre.
Pourquoi des crochets ⟦⟧ et pas simplement [T] ? Qwen3.5-0.8B est un base suffisamment fort pour respecter spontanément un format simple type [T]…[/T] (il l’a vu dans son pré-entraînement) — le delta avant/après disparaîtrait. Les crochets mathématiques ⟦⟧ sont étrangers au base : son échec devient visible (0/3), et la correction par LoRA devient mesurable (3/3). C’est le choix qui rend le notebook démonstratif.
Remarquez aussi que le contenu est faible (définitions approximatives) — un LoRA sur un petit dataset d’exemples bien formatés améliorera surtout le respect du format, pas nécessairement la justesse encyclopédique. C’est une limite honnête à garder en tête.
4. Construction du dataset d’entraînement
Nous construisons un petit dataset (60 exemples) de termes d’IA / machine learning / data science, chacun avec une définition concise et un exemple, déjà au format balisé cible. Le dataset est lisible à l’œil : un étudiant peut lire n’importe quelle paire prompt/réponse et comprendre le pattern attendu.
Nous générons le dataset in-notebook (self-contained) à partir d’une liste de termes et de modèles de définition/exemple. C’est une démarche pédagogique : on montre exactement ce qu’on met dans le modèle.
# Dataset de termes IA/ML/data, au format balise cible.# Chaque entree : (terme, definition, exemple). Format cible : ⟦T⟧terme⟦/T⟧⟦D⟧definition⟦/D⟧⟦E⟧exemple⟦/E⟧TERMES = [ ("Gradient descent", "Algorithme d'optimisation qui minimise une fonction de cout en suivant la pente opposee du gradient", "Descendre une montagne en prenant a chaque pas la direction de plus forte pente"), ("Overfitting", "Surapprentissage : le modele memorise le bruit d'entrainement au lieu d'apprendre la structure generale", "Un arbre de profondeur 20 avec 100% sur train mais 60% sur test"), ("Batch size", "Nombre d'exemples traites avant chaque mise a jour des poids pendant l'entrainement", "batch_size=32 met a jour les poids toutes les 32 exemples"), ("Epoch", "Un passage complet sur l'ensemble du dataset d'entrainement", "10 epochs = le modele voit chaque exemple 10 fois"), ("Cross-entropy", "Fonction de cout pour la classification qui penalise les predictions faibles de la bonne classe", "Utilisee pour entrainer un classificateur d'images"), ("ReLU", "Fonction d'activation max(0, x) : lineaire pour x>0, nulle pour x<0", "Active les neurones positifs, met les autres a zero"), ("Softmax", "Transforme un vecteur de scores en distribution de probabilites sommant a 1", "Convertit [2.0, 1.0, 0.1] en probabilites"), ("Learning rate", "Pas de mise a jour des poids : controle la vitesse d'apprentissage", "lr=0.001 = petits pas stables, lr=0.1 = grands pas risque"), ("Regularization", "Technique qui penalise la complexite du modele pour eviter le surapprentissage", "L2 ajoute le carre des poids a la fonction de cout"), ("Dropout", "Desactive aleatoirement une fraction des neurones pendant l'entrainement pour forcer la robustesse", "p=0.5 eteint la moitie des neurones a chaque pas"), ("Embedding", "Representation vectorielle dense d'un objet discret dans un espace continu", "word2vec plie chaque mot dans un vecteur de dimension 300"), ("Attention", "Mecanisme qui pondere l'importance de chaque element d'entree pour produire une sortie", "Dans la traduction, 'bank' pondere le contexte pour choisir banque ou rive"), ("Transformer", "Architecture basee uniquement sur l'attention, sans recurrence ni convolution", "Le moteur des modeles GPT et BERT"), ("Backpropagation", "Propagation du gradient vers l'arriere dans le reseau pour calculer les gradients de chaque poids", "Apres la perte, on remonte couche par couche"), ("Validation set", "Subset de donnees non utilise pour l'entrainement, servant a estimer la generalisation", "20% du dataset reserve au suivi de la perte de validation"), ("Normalization", "Mise a l'echelle des entrees ou activations pour stabiliser et accelerer l'entrainement", "BatchNorm recentre les activations par lot"), ("Loss function", "Fonction qui mesure l'ecart entre la prediction du modele et la verite terrain", "La MSE pour la regression, la cross-entropie pour la classification"), ("Inference", "Phase ou le modele entraine produit des predictions sur de nouvelles donnees", "Utiliser un modele entraine pour classer une nouvelle image"), ("Fine-tuning", "Re-entrainement d'un modele pre-entraine sur une tache ou un domaine specifique", "Adapter BERT a la classification de sentiments juridiques"), ("LoRA", "Low-Rank Adaptation : on apprend deux petites matrices de rang faible au lieu de tous les poids", "Un delta de rang 8 sur un poids 4096x4096 n'apprend que 65536 params"), ("Quantization", "Reduction de la precision des poids (ex. 16-bit vers 4-bit) pour reduire memoire et calcul", "Charger un modele 7B en 4-bit tient dans 4 Go au lieu de 14 Go"), ("Tokens", "Unites de base (sous-mots) produites par la tokenisation d'un texte", "'chatting' -> ['chat', 'ting'] selon le tokenizer"), ("Prompt", "Texte d'entree fourni a un modele generatif pour orienter sa sortie", "'Traduis en anglais: bonjour' est un prompt de traduction"), ("Hallucination", "Sortie produite par un modele qui semble plausible mais est factuellement fausse", "Un LLM qui invente une reference bibliographique inexistante"), ("RAG", "Retrieval-Augmented Generation : on injecte des documents recuperes dans le prompt", "Un chatbot qui cite la doc interne avant de repondre"), ("Sharpe ratio", "Ratio du rendement excedentaire sur la volatilite, mesurant le rendement ajuste au risque", "Un Sharpe de 1.5 = bon rendement pour le risque pris"), ("Drawdown", "Chute maximale depuis un pic de valeur, mesurant le pire recul d'un investissement", "Passer de 100 a 80 puis 90 = drawdown de 20%"), ("Volatilite", "Mesure de l'amplitude des variations de prix d'un actif", "L'ecart-type des rendements quotidiens"), ("Covariance", "Mesure de comment deux variables varient ensemble", "Positive si elles montent et descendent ensemble"), ("Outlier", "Observation qui s'ecarte significativement des autres donnees", "Un salaire de 10M dans un echantillon median a 40k"),]# On complete jusqu'a ~150 en revariant legerement les formulations (demonstration : on peut# aussi etoffer depuis un fichier externe). Ici on double avec une formulation synonyme.import randomrandom.seed(42)entrees =list(TERMES)synonymes = {"Definition concise": "Definition courte","Exemple concret": "Cas pratique",}for terme, defn, ex inlist(TERMES): entrees.append((terme, defn.replace(" :", ",").rstrip(","), ex)) # variante mineureprint(f"Dataset : {len(entrees)} exemples")print("Exemple de paire (format balise cible) :")t, d, e = entrees[0]print(f" IN : Terme: {t}")print(f" OUT : ⟦T⟧{t}⟦/T⟧⟦D⟧{d}⟦/D⟧⟦E⟧{e}⟦/E⟧")
Dataset : 60 exemples
Exemple de paire (format balise cible) :
IN : Terme: Gradient descent
OUT : ⟦T⟧Gradient descent⟦/T⟧⟦D⟧Algorithme d'optimisation qui minimise une fonction de cout en suivant la pente opposee du gradient⟦/D⟧⟦E⟧Descendre une montagne en prenant a chaque pas la direction de plus forte pente⟦/E⟧
Le dataset de depart contient exactement 60 exemples de termes provenant des domaines IA, ML et data science. Chaque exemple est structure sous forme de tuple (terme, definition, exemple) et doit etre formate selon le schema avec balises : ⟦T⟧terme⟦/T⟧⟦D⟧definition⟦/D⟧⟦E⟧exemple⟦/E⟧. L’exemple affiche montre le terme Gradient descent avec sa definition et un exemple concret de descente de gradient.
# Construction du Dataset HuggingFace au format conversation (messages) de Qwen.# SFTTrainer de trl detecte le champ "messages" et applique une perte "assistant only" :# la perte ne porte QUE sur le tour assistant, pas sur le prompt ni sur les balises de# structure du chat template. C'est le format canonique pour le SFT conversationnel, et il# evite un piege du language modeling classique (perte sur toute la sequence) : ce dernier# pousse le modele a memoriser le SCAFFOLDING du chat template (les balises# <|im_start|>system/assistant<|im_end|> reapparaitrent en generation sous forme du token# parasite "systemsystem..."). Avec "messages", le modele apprend le CONTENU, pas la structure.from datasets import Datasetdef formater_exemple(terme, defn, ex): instruction = CONSIGNE_FORMAT +f"Terme: {terme}" sortie =f"⟦T⟧{terme}⟦/T⟧⟦D⟧{defn}⟦/D⟧⟦E⟧{ex}⟦/E⟧"return instruction, sortiedonnees_msgs = []for t, d, e in entrees: instruction, sortie = formater_exemple(t, d, e) donnees_msgs.append({"messages": [ {"role": "user", "content": instruction}, {"role": "assistant", "content": sortie}, ] })ds = Dataset.from_list(donnees_msgs)print(f"Dataset HF pret : {len(ds)} exemples (format messages)")print(f"Premier exemple :\n{ds[0]}")
Dataset HF pret : 60 exemples (format messages)
Premier exemple :
{'messages': [{'role': 'user', 'content': 'Pour chaque terme demande, reponds STRICTEMENT au format balise ⟦T⟧terme⟦/T⟧⟦D⟧definition concise⟦/D⟧⟦E⟧exemple concret⟦/E⟧ sans aucun autre texte autour.\n\nTerme: Gradient descent'}, {'role': 'assistant', 'content': "⟦T⟧Gradient descent⟦/T⟧⟦D⟧Algorithme d'optimisation qui minimise une fonction de cout en suivant la pente opposee du gradient⟦/D⟧⟦E⟧Descendre une montagne en prenant a chaque pas la direction de plus forte pente⟦/E⟧"}]}
5. Configuration de l’adaptateur LoRA
Nous définissons la configuration LoRA. Les choix clés : - r=8 : le rang des matrices d’adaptation. Plus il est grand, plus l’adaptateur est expressif mais coûteux. 8 est un bon défaut pour une tâche de format simple. - lora_alpha=16 : facteur d’échelle (typiquement 2 × r). Contrôle l’amplitude de l’adaptation. - target_modules : on cible les projections d’attention (q_proj, k_proj, v_proj, o_proj) et le MLP (gate_proj, up_proj, down_proj). Sur ce petit 0.8B quantifié, l’attention seule sous-apprend à learning rate modeste ; le MLP porte l’essentiel des connaissances et donne à l’adaptateur la capacité de stocker le nouveau format. - dropout=0.05 : légère régularisation.
Nous appliquons ensuite get_peft_model qui gèle tous les poids d’origine et n’ajoute que les matrices LoRA entraînables (~0,4 % des paramètres).
# LoRA : on cible les projections d'attention (q,k,v,o) ET le MLP (gate,up,down).# Pourquoi le MLP aussi ? Apprendre un format nouveau (ici les delimiteurs etrangers# ⟦T⟧/⟦D⟧/⟦E⟧) demande de la capacite de stockage ; les modules du MLP portent l'essentiel# des connaissances. Ce ciblage 7-modules a ete calibre sur Qwen2.5-0.5B (#10328) :# attention-only (4 modules, ~0.22%) sous-apprend a lr modeste (perte stagne, generation# deraille) ; les 7 modules lineaires convergent. Reutilise tel quel sur Qwen3.5-0.8B :# 0.42% des params entrainables (3.19M / 755M), l'adaptateur converge (perte finale 0.75).lora_config = LoraConfig( r=8, lora_alpha=16, lora_dropout=0.05, bias="none", task_type=TaskType.CAUSAL_LM, target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],)model_peft = get_peft_model(base_model, lora_config)model_peft.print_trainable_parameters()
trainable params: 3,194,880 || all params: 755,587,904 || trainable%: 0.4228
6. Entraînement
Nous utilisons SFTTrainer de trl, qui encapsule la boucle d’entraînement supervisé sur le format conversationnel (messages). SFTTrainer détecte le champ messages et applique une perte « assistant only » : la perte ne porte que sur le tour assistant, pas sur le prompt ni sur les balises structurelles du chat template. C’est important — une perte sur toute la séquence (LM classique) pousserait le modèle à mémoriser le scaffolding du template (les marqueurs <|im_start|>system/assistant/<|im_end|> réapparaîtraient en génération). Configuration : - 40 epochs (le lr est modeste, on laisse l’adaptateur consolider le format), - per_device_train_batch_size=4 + gradient_accumulation_steps=2 (batch effectif 8), - learning_rate=2e-5 (voir la note de reproductibilité ci-dessous), - max_grad_norm=0.3 (clipping serré pour borner les mises à jour), - max_length=512 (les exemples complets + chat template approchent 256 tokens ; à 256, trl ≥ 1.0 tronque et drop les exemples dont la réponse est coupée → sous-apprentissage).
Note de reproductibilité — le piège du learning rate sur petit modèle. La valeur canonique recommandée par le papier QLoRA (1e-4 à 2e-4) est calibrée pour des modèles 7B à 65B. Sur un 0.5B quantifié 4-bit, ces valeurs sont trop grandes : elles font exploser les logits de l’adaptateur et la génération déraille (le modèle émet le bon premier token [T puis boucle sur un token parasite). Nous utilisons donc lr=2e-5 (10× plus petit). Ce notebook a été développé sous trl 0.16 où lr=3e-4 passait sans dérailler ; sous trl ≥ 1.0 (l’API courant, où max_seq_length → max_length), le régime stable est lr=2e-5 + 40 epochs + max_length=512.
La perte devrait chuter nettement : la tâche (apprendre un format) est simple pour un adaptateur de rang 8 couvrant les 7 modules linéaires (~0,9 % des paramètres).
from trl import SFTTrainer, SFTConfigimport osos.environ["TOKENIZERS_PARALLELISM"] ="false"# SFTConfig (trl >= 1.0) : parametrage de l'entrainement supervise.# NB reproductibilite : sous trl 0.16 ce notebook utilisait lr=3e-4 (max_seq_length + warmup_ratio).# Sur trl >= 1.0, TROIS changements sont necessaires pour reproduire le delta Prong-B :# 1. API : max_seq_length -> max_length, warmup_ratio retire (TypeError sinon).# 2. Regime : lr=3e-4 provoque un COLLAPSE de la generation : le modele emet '[T' puis deraille# sur un token parasite repete. Cause : sur un 0.5B quantifie 4-bit, un lr trop grand fait# exploser les logits de l'adaptateur. Un diagnostic d'isolation (5 stages) prouve que seul# le lr est en cause (ni la quantization, ni peft, ni le template). Regime stable : lr=2e-5# (10x plus petit que la reco canonique 2e-4, calibree pour 7B+).# 3. max_length=512 : les exemples (consigne + terme + definition + exemple en format balise)# font ~80 tokens, mais avec le chat template complet on s'approche de 256. A max_length=256,# trl >= 1.0 DROPPE les exemples dont le tour assistant est tronque ("Dropping fully masked# examples") -> sous-apprentissage. max_length=512 garde tous les exemples intacts.# Le lr etant modeste, 40 epochs sont necessaires pour bien installer le format.## Transfert 0.5B -> 0.8B : ce regime (lr=2e-5, 40 epochs, 7 modules) est calibre sur 0.5B# et transfere SANS ajustement a Qwen3.5-0.8B : perte finale 0.7545, pas de collapse,# VRAM pic 2.51 Go / 8 Go.## Prong-B (format etranger) : les delimiteurs ⟦T⟧/⟦D⟧/⟦E⟧ sont volontairement ETRANGERS au# modele. Qwen3.5-0.8B est assez fort pour respecter spontanement un format simple type# [T]..[/T] (2/3 termes au base) -- ce qui tuerait le delta pedagogique. Les crochets ⟦⟧ sont# etrangers au base (0/3, qui emet des variantes fausses type ⟦S⟧/⟦R⟨) : le LoRA installe alors# le vocabulaire exact, et le delta avant/apres redevient net (0/3 -> 3/3).sft_config = SFTConfig( output_dir="./checkpoints_lora_21", num_train_epochs=40, per_device_train_batch_size=4, gradient_accumulation_steps=2, learning_rate=2e-5, max_grad_norm=0.3, logging_steps=20, save_strategy="no", report_to=[], max_length=512, dataset_num_proc=2, bf16=True,)torch.cuda.reset_peak_memory_stats()t0 = time.time()trainer = SFTTrainer( model=model_peft, args=sft_config, train_dataset=ds, processing_class=tokenizer,)stats = trainer.train()duree = time.time() - t0print(f"\nEntrainement termine en {duree:.1f}s")print(f"VRAM pic pendant l'entrainement : {torch.cuda.max_memory_allocated()/1e9:.2f} Go / 8 Go")print(f"Perte finale : {stats.training_loss:.4f}")
[320/320 25:28, Epoch 40/40]
Step
Training Loss
20
2.318822
40
1.553288
60
1.046181
80
0.726855
100
0.579857
120
0.494262
140
0.452241
160
0.391091
180
0.359209
200
0.316150
220
0.292931
240
0.277153
260
0.262841
280
0.256395
300
0.253936
320
0.253632
Entrainement termine en 1593.6s
VRAM pic pendant l'entrainement : 2.51 Go / 8 Go
Perte finale : 0.6147
7. Le delta : le modèle adapté respecte le format balisé
Réinterrogeons le modèle après l’adaptation LoRA, sur les mêmes prompts que la section 3. Le delta est visible côte à côte.
# Re-test sur les memes termes qu'avantsorties_apres = {}for terme in termes_test: prompt = CONSIGNE_FORMAT +f"Terme: {terme}" sortie = generer(prompt, model_peft) sorties_apres[terme] = sortieprint("=== COMPARAISON AVANT / APRES ===\n")for terme in termes_test:print(f"── Terme : {terme} ──")print(f" AVANT (base) : {sorties_base[terme].strip()[:150]}")print(f" APRES (LoRA) : {sorties_apres[terme].strip()[:150]}")print()
=== COMPARAISON AVANT / APRES ===
── Terme : Sharpe ratio ──
AVANT (base) : <br>
<div><b>Sharpe ratio</b><br><b>Definition concise</b><b>>Le rapport de Sharpe est un indicateur fina
APRES (LoRA) : ⟦T⟧Sharpe ratio⟦/T⟧⟦D⟧Indicateur de performance qui mesure le rendement extra pour risque (Sharpe) versus la volatilité (Standard Deviation)⟦/D⟧⟦E⟧un
── Terme : Gradient descent ──
AVANT (base) : <br>
<div><b>Gradient descent</b><br><b>Est un algorithme d'optimisation qui minimise une fonction de coût en ajustant l
APRES (LoRA) : ⟦T⟧Gradient descent⟦/T⟧⟦D⟧Algorithme d'optimisation qui itere sur le gradient de la fonction a minimiser⟦/D⟧⟦E⟧Descente vers un minimum par moussement
── Terme : Overfitting ──
AVANT (base) : <br>
<div><b>Overfitting</b><br><b>Definition</b><b>concise</b><b>>&nbsp;<i><b&g
APRES (LoRA) : ⟦T⟧Overfitting⟦/T⟧⟦D⟧Exemple d'overfitting c'est l'apprentissage excessif sur les données d'exemple en detriment de la generalisation⟦/D⟧⟦E⟧Un modele
La comparaison avant/apres montre que le modele LoRA produit des reponses structurees au format balise demande. Alors que le modele de base generait du HTML non structure, le modele adapte par LoRA respecte le format ⟦T⟧terme⟦/T⟧⟦D⟧definition⟦/D⟧⟦E⟧exemple⟦/E⟧. Par exemple, pour Sharpe ratio, la reponse LoRA est entierement dans le format cible.
# Mesure : les sorties apres LoRA respectent-elles le format balise ⟦T⟧..⟦/T⟧⟦D⟧..⟦/D⟧⟦E⟧..⟦/E⟧ ?import redef respecte_format(s): s = s.strip()# Le format cible contient les 3 balises ouvrantes ET fermantesreturnall(b in s for b in ("⟦T⟧", "⟦/T⟧", "⟦D⟧", "⟦/D⟧", "⟦E⟧", "⟦/E⟧"))print("Respect du format balise ⟦T⟧..⟦/T⟧⟦D⟧..⟦/D⟧⟦E⟧..⟦/E⟧ :")for terme in termes_test: avant = respecte_format(sorties_base[terme]) apres = respecte_format(sorties_apres[terme])print(f" {terme:20s} AVANT={avant!s:5s} APRES={apres!s}")
Respect du format balise ⟦T⟧..⟦/T⟧⟦D⟧..⟦/D⟧⟦E⟧..⟦/E⟧ :
Sharpe ratio AVANT=False APRES=True
Gradient descent AVANT=False APRES=True
Overfitting AVANT=False APRES=True
8. Bilan : ce que LoRA a changé, et ses limites
Ce que l’adaptateur a appris : produire le format balisé ⟦T⟧..⟦/T⟧⟦D⟧..⟦/D⟧⟦E⟧..⟦/E⟧. Le base ignorait les délimiteurs et produisait du HTML / des balises erronées (⟦S⟧) ; l’adapté produit la structure exacte. Le delta est net et mesurable : 0/3 → 3/3.
Ce que l’adaptateur n’a PAS appris (honnêteté) : - La justesse encyclopédique des définitions reste limitée par la taille du modèle (0,8B) et la petitesse du dataset. Sur un terme absent du dataset, le format balisé sera respecté mais le contenu reste approximatif. - Le fine-tuning LoRA modifie le format et le style, pas la base de connaissances profonde.
Empreinte : ~0,4 % des paramètres entraînés (3,19 M / 755 M), VRAM de pointe ~2,5 Go pour l’entraînement (modèle 4-bit + adaptateur + états optimiseur), le tout sur une carte de 8 Go — la promesse de QLoRA tenue.
Quand utiliser LoRA vs d’autres approches : - LoRA / QLoRA : adapter le style ou le format d’un modèle pré-entraîné, peu de données, budget modeste. C’est ce notebook. - Fine-tuning complet : changer le comportement profond (domaine très nouveau), nécessite gros GPU + beaucoup de données. - Prompt engineering / few-shot : aucune donnée d’entraînement, le base suffit — tentez-le avant le fine-tuning.
Exercices pratiques
Exercice 1 : Effet du rang (facile)
Durée estimée : 10-15 minutes
Objectif : Mesurer l’impact du rang r de l’adaptateur sur la qualité du format appris.
Ré-entraînez l’adaptateur avec r=2 puis r=32 (toutes choses égales par ailleurs).
Comparez le nombre de paramètres entraînables (print_trainable_parameters) et la perte finale.
Observez si le format est mieux respecté avec un rang plus élevé sur un terme absent du dataset.
# Exercice 1 : effet du rang# Etape 1 : definir une nouvelle config avec r=2# config_r2 = LoraConfig(r=2, lora_alpha=4, lora_dropout=0.05, bias="none",# task_type=TaskType.CAUSAL_LM,# target_modules=["q_proj", "k_proj", "v_proj", "o_proj"])# Etape 2 : recharger un modele base FRAIS (base_model ci-dessus a deja un adaptateur),# appliquer get_peft_model avec config_r2, re-entrainer# Etape 3 : comparer params + perte + format sur un terme absent (ex. "Transformer")# TODO etudiant : completer puis noter vos observationsprint("Exercice 1 a completer : comparer r=2 vs r=32")
Exercice 1 a completer : comparer r=2 vs r=32
Exercice 2 : Étendre le dataset (moyen)
Durée estimée : 15-20 minutes
Objectif : Vérifier que la justesse du contenu s’améliore quand on ajoute des exemples.
Ajoutez 20 termes de votre domaine (avec définition + exemple au format balisé) à la liste TERMES.
Ré-entraînez et testez sur un de vos nouveaux termes et sur un terme toujours absent.
Le format s’améliore-t-il ? Le contenu s’améliore-t-il ? Notez la différence.
# Exercice 2 : etendre le dataset# Etape 1 : ajouter vos termes a TERMES (ou creer une liste TERMES_PERSO)# mes_termes = [("Mon terme", "Ma definition concise", "Mon exemple concret"), ...]# Etape 2 : reconstruire le Dataset, re-entrainer# Etape 3 : tester sur un nouveau terme + un terme absent -> observer format ET contenu# TODO etudiantprint("Exercice 2 a completer : ajouter 20 termes et mesurer l'effet")
Exercice 2 a completer : ajouter 20 termes et mesurer l'effet
Exercice 3 : Sauvegarder et recharger l’adaptateur (moyen)
Durée estimée : 15-20 minutes
Objectif : Comprendre qu’un adaptateur LoRA est un petit fichier, séparé du modèle de base.
Sauvegardez l’adaptateur avec model_peft.save_pretrained("./mon_adaptateur").
Observez la taille du dossier — pourquoi est-il si petit comparé au modèle complet ?
Rechargez le modèle de base sans adaptateur, puis avecPeftModel.from_pretrained, et vérifiez que le delta de comportement suit l’adaptateur (et non le base).
# Exercice 3 : sauvegarder / recharger l'adaptateur# Etape 1 : sauvegarder# model_peft.save_pretrained("./mon_adaptateur")# Etape 2 : observer la taille du dossier (ls -lh ./mon_adaptateur)# Etape 3 : recharger base SANS adaptateur -> tester le format (doit ECHEC)# puis recharger AVEC PeftModel.from_pretrained -> tester (doit REUSSIR)# from peft import PeftModel# modele_recharge = PeftModel.from_pretrained(base_model_frais, "./mon_adaptateur")# TODO etudiantprint("Exercice 3 a completer : sauvegarder, mesurer la taille, recharger")
Exercice 3 a completer : sauvegarder, mesurer la taille, recharger
Critères de succès
Voir aussi : 10. Local Llama pour le déploiement local d’un LLM, 11. Quantization pour la quantification en inference. Ce notebook combine les deux idées (modèle local + quantification) et y ajoute l’adaptation paramétriquement efficiente.