Introduction
Nous voici dans la phase 2 de notre aventure technique. Après avoir réalisé un audit SEO local directement sur le MacBook, il est temps de passer à l’étape supérieure : le fine-tuning du modèle Mistral 7B Instruct.
L’objectif est clair : entraîner notre propre version du modèle, de manière logique et méthodique, à partir d’un dataset spécifique. Une fois ce modèle ajusté, nous le mettrons à l’épreuve sur des sites web réels, afin de mesurer ses performances face à GPT-5, notre modèle de référence.
Cette V2 s’annonce comme un vrai défi technique — entre gestion des ressources locales, optimisation du pipeline et comparaison qualitative des sorties. Mais chaque itération va nous rapprocher d’un objectif plus ambitieux : maîtriser le fine-tuning local d’un grand modèle de langage, et comprendre en profondeur comment ajuster un LLM pour des besoins SEO et rédactionnels concrets.
Le Dataset : le carburant du fine-tuning
Comme pour tout fine-tuning de modèle de langage, la clé du succès réside dans la qualité et la pertinence des données. Sans dataset bien pensé, impossible de transformer un modèle généraliste en un assistant spécialisé. Ici, notre but n’est pas de rivaliser avec GPT-5, mais de créer un Mistral 7B Instruct capable de comprendre les enjeux SEO, d’analyser des sites, et d’adapter son langage aux contraintes du référencement naturel.
Dans l’écosystème de l’intelligence artificielle, difficile de passer à côté de Hugging Face — la plateforme de référence pour tout ce qui touche aux modèles de langage, jeux de données et pipelines d’entraînement. Fondée par une équipe française, cette plateforme est devenue le GitHub du machine learning : on y trouve de tout, pour tout. Des modèles d’images, de texte, de code… et bien sûr, une quantité impressionnante de datasets prêts à être utilisés pour affiner nos modèles.
Pour notre expérimentation, nous allons utiliser le dataset metehan777/global-seo-knowledge.
Ce jeu de données rassemble un large éventail de connaissances SEO, incluant des pratiques internationales, des techniques d’optimisation on-page/off-page, ainsi que des extraits liés aux algorithmes de moteurs de recherche.
En clair, ce dataset va servir de matière première à notre fine-tuning : il va permettre à notre modèle Mistral 7B d’apprendre le vocabulaire, les patterns et les raisonnements propres au SEO, afin de devenir un assistant expert en référencement plutôt qu’un simple modèle de langage généraliste.
Le professeur et l’élève : la logique de distillation appliquée à Mistral 7B
Dans le monde des grands modèles de langage, la hiérarchie est souvent la clé de la performance. Les géants comme OpenAI ou Anthropic s’appuient sur un principe bien connu : la distillation de connaissances.
En clair, un gros modèle (plus puissant, plus coûteux) entraîne un plus petit, qui devient alors plus efficace sur des tâches spécifiques tout en consommant beaucoup moins de ressources.
C’est ce mécanisme qui permet l’existence de familles de modèles comme Claude Opus 4.1, Claude Sonnet 4.5 ou Claude Haiku : chaque version est une itération condensée de la précédente, optimisée pour un usage différent.
Le principe est simple : on rend le modèle plus fin, plus rapide et plus ciblé, tout en lui transmettant les compétences essentielles de son modèle parent.
Prenons un exemple concret :
– La génération d’un boilerplate d’e-mail peut parfaitement être gérée par un GPT-5 Nano, rapide et économique.
– En revanche, la gestion d’un portefeuille d’actions ou la planification stratégique à long terme nécessiteront un modèle plus complexe, comme un GPT-5 Big Thinking, conçu pour raisonner à plusieurs niveaux de profondeur.
Et ce schéma ne se limite pas aux modèles propriétaires. Dans l’open source, des projets comme Qwen (le modèle d’Alibaba, souvent confondu avec “Qwent”) appliquent la même philosophie : un grand modèle supervise plusieurs versions plus petites, chacune fine-tunée pour un usage précis.
C’est exactement ce que nous faisons ici :
notre Mistral 7B Instruct devient l’élève, et GPT-5 joue le rôle du professeur.
Le premier apprend du second, en observant, en corrigeant ses erreurs et en intégrant progressivement des schémas de raisonnement plus raffinés. Le résultat attendu : un petit modèle open source, capable de performances très solides en analyse et génération SEO, sans dépendre du cloud ni d’une infrastructure massive.
Python à 100 % : le cœur du système
Dans cette phase, on garde les choses simples, claires et modulaires.
Pas besoin d’orchestration complexe ou de serveurs distribués — ici, Python est notre base solide.
L’idée est de construire un petit écosystème local, capable de :
- Charger notre modèle Mistral 7B Instruct en mémoire pour exécuter des analyses SEO.
- Comparer les résultats en temps réel avec ceux d’un modèle distant (GPT-5 Nano, via l’API OpenAI).
- Présenter le tout via une interface web minimaliste, accessible depuis n’importe quel navigateur.
Pour ce faire, on s’appuie sur des briques simples et éprouvées :
- Flask : framework web léger, parfait pour créer une UI rapide sans s’encombrer d’un front-end lourd.
- Bootstrap : un peu de CSS préfabriqué pour une interface claire et responsive.
- OpenAI API : utilisée ici comme fallback pour comparer nos résultats locaux avec GPT-5 Nano.
- BeautifulSoup et Requests : pour scraper et analyser le contenu HTML d’un site avant de le transmettre au modèle.
Le pipeline de traitement est le suivant :
on insère l’URL d’un site web, on clique sur Analyser, et le script :
- télécharge le HTML du site,
- le nettoie via BeautifulSoup,
- le passe au modèle Mistral 7B local,
- et en parallèle, interroge GPT-5 Nano pour obtenir une comparaison qualitative.
L’architecture Python pourrait ressembler à ceci :
import os
import sys
import time
import threading
from flask import Flask, render_template_string, request, jsonify
from flask_socketio import SocketIO, emit
from openai import OpenAI
from bs4 import BeautifulSoup
import requestsCe bloc de base met en place tous les imports nécessaires :
os,sys,time,threading: pour la gestion des processus, du temps et des threads.FlasketSocketIO: servent à gérer la communication entre le backend Python et le front (par exemple, pour suivre la progression d’une analyse).OpenAI: permet d’interroger GPT-5 Nano via API avec un simple appelclient.chat.completions.create(...).BeautifulSoup: nettoie et parse le code HTML avant de l’envoyer au modèle local.requests: récupère le contenu du site web distant.
Cette base va nous permettre d’ajouter, dans les sections suivantes, le chargement du modèle Mistral 7B, puis le mécanisme de comparaison des réponses.
Le but est d’obtenir une architecture propre, en duplex local/cloud, où chaque modèle joue son rôle :
– le local pour la rapidité et la personnalisation,
– GPT-5 Nano pour la précision et la vérification.
L’idée de fond : une boucle d’apprentissage continue
Le concept derrière ce setup est élégant et terriblement efficace.
Au lieu de simplement fine-tuner Mistral 7B sur un dataset figé, on lui donne la possibilité d’évoluer au fil du temps, grâce à un mécanisme de contre-analyse effectué par GPT-5 Nano.
Le processus est simple :
lorsqu’on lance une analyse SEO, le modèle local (Mistral 7B) produit son évaluation, pendant que GPT-5 Nano génère la sienne.
On compare ensuite les deux résultats.
Si le score d’écart entre les deux est trop important — c’est-à-dire si Mistral s’éloigne trop de la référence — on stocke cette divergence sous forme d’un embedding JSON, qui contient :
- la réponse initiale de Mistral,
- la réponse corrigée ou réévaluée par GPT-5,
- et le delta qualitatif entre les deux.
Ces embeddings forment un nouveau corpus d’entraînement, qui servira à raffiner le modèle lors des prochaines sessions de fine-tuning.
On obtient donc un système où le modèle apprend de ses erreurs sans nécessiter d’entraînement en continu, mais simplement en accumulant des feedbacks intelligents.
D’un point de vue économique, l’approche est redoutable :
GPT-5 Nano n’entraîne pas directement le modèle — ce qui serait coûteux et techniquement interdit — mais fournit des analyses de correction.
Ce sont ces corrections, intégrées dans le dataset, qui guideront les itérations futures du fine-tuning.
En somme, tu appliques à Mistral 7B la même logique que dans la classe virtuelle :
un élève (le modèle local) reçoit des évaluations, et les erreurs deviennent la matière première de son apprentissage futur.
Résultat : un système auto-améliorant, peu coûteux, et hautement spécialisé pour le SEO.
Le Fine-Tuning : apprentissage local et efficacité maximale
Ce qui rend cette expérimentation vraiment intéressante, c’est qu’elle a été réalisée entièrement en local, sans GPU externe, sur un MacBook équipé de la puce Apple M4.
Cette puce, pensée pour le calcul mixte (CPU + GPU + Neural Engine), offre un équilibre remarquable entre puissance et consommation énergétique, ce qui en fait un terrain de jeu idéal pour du fine-tuning expérimental.
L’entraînement du modèle Mistral-7B Instruct a duré environ 20 minutes, à partir d’un petit dataset Hugging Facecombiné à des exemples locaux. Le tout piloté via un dashboard Flask + Socket.IO affichant les logs et les métriques en temps réel.
Bien sûr, cet exploit a un coût mémoire : le processus a consommé près de 28 Go de RAM.
C’est la limite naturelle de ce type d’expérience : il faut une machine disposant d’une mémoire unifiée conséquente (ici 48 Go), car Mistral 7B reste un modèle massif, même en quantification partielle (float16).
Mais grâce à l’architecture ARM unifiée d’Apple Silicon, le comportement reste parfaitement stable, fluide et sans crash, même en pleine charge GPU Metal (MPS).
Résultat : un fine-tuning stable, rapide, silencieux et énergétiquement propre, sans dépendre du cloud ni d’une carte graphique dédiée.
Un modèle local supervisé par GPT-5
Une fois le fine-tuning terminé, le modèle Mistral 7B local devient un élève entraîné, prêt à recevoir la supervision de son professeur GPT-5 Nano.
L’idée est simple mais puissante :
le modèle local produit une réponse SEO, GPT-5 Nano fournit une contre-analyse, et la différence entre les deux est stockée sous forme d’embedding JSON.
Ces embeddings constituent un corpus d’amélioration continue : à chaque itération, le modèle Mistral s’affine, apprend de ses écarts et devient plus précis, sans qu’on ait besoin de le réentraîner en continu.
Le tout pour un coût dérisoire, puisque GPT-5 n’entraîne pas directement le modèle — il se contente d’évaluer et de corriger.
C’est donc un système auto-évolutif, où un petit modèle open source apprend des observations d’un modèle plus grand, tout en restant local, économique et totalement sous contrôle.
Interface de supervision : le Mistral SEO Training Monitor
L’un des éléments centraux du projet est le dashboard de suivi développé en Flask + Socket.IO, visible ci-dessus.
Cette interface web, légère et réactive, permet de piloter le fine-tuning de Mistral 7B et d’en observer la progression en direct, sans terminal ni ligne de commande.
On y retrouve :
- Le nombre d’exemples locaux intégrés dans le dataset,
- Le volume d’échantillons tirés depuis Hugging Face (
HF Mix), - L’heure et le statut du dernier entraînement,
- Un log en direct affichant la perte, le runtime et les étapes de sauvegarde.
Tout le processus se déroule en une seule page :
l’utilisateur clique sur Launch Training, le script seo_training_loop.py démarre, et les données remontent automatiquement au navigateur via les sockets Web.
Ce tableau de bord rend le fine-tuning intuitif, transparent et mesurable — une approche rarement vue dans les expérimentations locales, où tout se passe d’ordinaire en ligne de commande.
Le script de fine-tuning complet en Python
Pour les développeurs souhaitant reproduire l’expérience, voici le script complet de fine-tuning local utilisé dans cette V2 du projet Mistral SEO Audit.
Entièrement écrit en Python 3.11, ce code gère tout le pipeline :
- détection du matériel et sélection automatique du backend (MPS sur Mac, CUDA ou CPU fallback),
- téléchargement du dataset public
metehan777/global-seo-knowledgedepuis Hugging Face, - préparation et tokenisation des données au format Mistral
[INST]...[/INST], - configuration LoRA + PEFT pour réduire la charge mémoire,
- entraînement complet via la classe
Trainerde Hugging Face Transformers, - sauvegarde automatique du modèle et test de génération SEO intégré.
Le tout est conçu pour fonctionner en local sur macOS, sans dépendre d’un GPU externe.
Grâce à Metal Performance Shaders (MPS), le modèle Mistral-7B-Instruct-v0.2 est entraîné directement sur la puce M4, en float16, tout en restant stable et silencieux.
Ce script est la colonne vertébrale du projet : il automatise le fine-tuning, assure la compatibilité entre architectures ARM et CUDA, et permet d’obtenir un modèle SEO-centré exploitable ensuite via API ou dans un environnement Flask.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script de Fine-Tuning de Mistral-7B-Instruct-v0.2 pour SEO
==========================================================
Ce script fine-tune le modèle Mistral-7B-Instruct-v0.2 sur le dataset
metehan777/global-seo-knowledge en utilisant PEFT + LoRA pour optimiser
la mémoire sur MacBook Pro M4 (GPU Metal via MPS).
Auteur: Projet Mistral SEO Audit V2
Date: 2025-11-09
Environnement: Python 3.11, PyTorch avec MPS
"""
import os
import sys
import time
import warnings
from typing import Dict, List, Optional
import psutil
# Suppression des warnings non critiques
warnings.filterwarnings('ignore')
os.environ['TOKENIZERS_PARALLELISM'] = 'false'
# ============================================================================
# IMPORTS DES LIBRAIRIES PRINCIPALES
# ============================================================================
try:
import torch
from transformers import (
AutoTokenizer,
AutoModelForCausalLM,
BitsAndBytesConfig,
TrainingArguments,
Trainer,
DataCollatorForLanguageModeling
)
from datasets import load_dataset
from peft import (
LoraConfig,
get_peft_model,
prepare_model_for_kbit_training,
TaskType
)
import pandas as pd
except ImportError as e:
print(f"❌ Erreur d'import : {e}")
print("\nInstallez les dépendances requises :")
print("pip install torch transformers peft bitsandbytes datasets pandas accelerate")
sys.exit(1)
# ============================================================================
# CONFIGURATION GLOBALE
# ============================================================================
# Modèle et dataset
MODEL_NAME = "mistralai/Mistral-7B-Instruct-v0.2"
DATASET_NAME = "metehan777/global-seo-knowledge"
OUTPUT_DIR = "./mistral-seo-local"
# Hyperparamètres LoRA
LORA_R = 8 # Rang des matrices LoRA (plus petit = moins de paramètres)
LORA_ALPHA = 32 # Facteur de scaling LoRA
LORA_DROPOUT = 0.05 # Dropout pour régularisation
LORA_TARGET_MODULES = ["q_proj", "v_proj"] # Modules attention à fine-tuner
# Hyperparamètres d'entraînement
LEARNING_RATE = 2e-4 # Taux d'apprentissage
NUM_EPOCHS = 1 # Nombre d'époques (1 pour test rapide)
BATCH_SIZE = 1 # Batch size par device
GRADIENT_ACCUMULATION = 8 # Accumulation de gradients (batch effectif = 8)
MAX_LENGTH = 512 # Longueur maximale des séquences
# ============================================================================
# FONCTIONS UTILITAIRES
# ============================================================================
def print_separator(title: str = ""):
"""Affiche un séparateur visuel avec titre optionnel."""
if title:
print(f"\n{'='*80}")
print(f" {title}")
print(f"{'='*80}\n")
else:
print(f"{'='*80}")
def get_memory_usage():
"""Retourne l'utilisation mémoire système en GB."""
process = psutil.Process(os.getpid())
mem_info = process.memory_info()
return mem_info.rss / 1024**3 # Conversion en GB
def check_mps_availability():
"""
Vérifie la disponibilité du backend MPS (Metal Performance Shaders).
Retourne le device approprié et affiche les informations système.
"""
print_separator("DÉTECTION DU MATÉRIEL")
# Vérification MPS
mps_available = torch.backends.mps.is_available()
mps_built = torch.backends.mps.is_built()
print(f"PyTorch version : {torch.__version__}")
print(f"MPS disponible : {mps_available}")
print(f"MPS compilé : {mps_built}")
# Détermination du device
if mps_available and mps_built:
device = torch.device("mps")
print(f"✅ GPU Metal (MPS) détecté - Utilisation du M4")
# Activation du fallback MPS
os.environ['PYTORCH_ENABLE_MPS_FALLBACK'] = '1'
elif torch.cuda.is_available():
device = torch.device("cuda")
print(f"✅ GPU CUDA détecté")
else:
device = torch.device("cpu")
print(f"⚠️ Aucun GPU détecté - Utilisation du CPU (lent)")
# Mémoire système
total_ram = psutil.virtual_memory().total / 1024**3
available_ram = psutil.virtual_memory().available / 1024**3
print(f"\nMémoire système : {total_ram:.1f} GB totale, {available_ram:.1f} GB disponible")
print(f"Mémoire processus : {get_memory_usage():.2f} GB")
return device
def load_and_prepare_dataset():
"""
Charge le dataset SEO depuis Hugging Face et le prépare pour le fine-tuning.
Transforme les données en paires instruction/réponse au format Mistral :
[INST] Donne une définition précise du terme '{term}' en SEO. [/INST] {definition}
Returns:
datasets.Dataset: Dataset préparé et tokenisé
"""
print_separator("CHARGEMENT DU DATASET SEO")
# Chargement depuis Hugging Face
print(f"📥 Téléchargement du dataset : {DATASET_NAME}")
dataset = load_dataset(DATASET_NAME, split="train")
print(f"✅ Dataset chargé : {len(dataset)} exemples")
print(f"Colonnes : {dataset.column_names}")
# Affichage d'un exemple
print(f"\n📋 Exemple de donnée brute :")
example = dataset[0]
print(f" Terme : {example['term']}")
print(f" Définition : {example['definition'][:100]}...")
print(f" Catégorie : {example['category']}")
print(f" Importance : {example['importance']}")
return dataset
def format_instruction(example: Dict) -> Dict:
"""
Formate un exemple du dataset en prompt Mistral instruction.
Format :
[INST] Donne une définition précise du terme '{term}' en SEO. [/INST] {definition}
Args:
example: Dictionnaire avec les clés 'term' et 'definition'
Returns:
Dictionnaire avec la clé 'text' contenant le prompt formaté
"""
term = example['term']
definition = example['definition']
# Format Mistral Instruct
prompt = f"[INST] Donne une définition précise du terme '{term}' en SEO. [/INST] {definition}"
return {"text": prompt}
def tokenize_dataset(dataset, tokenizer):
"""
Tokenise le dataset pour l'entraînement.
Args:
dataset: Dataset Hugging Face
tokenizer: Tokenizer Mistral
Returns:
Dataset tokenisé avec padding et troncature
"""
print_separator("PRÉPARATION DES DONNÉES")
# Si le dataset ne contient pas encore la colonne "text", on applique le formatage
if "text" not in dataset.column_names:
print("🔄 Formatage des exemples en prompts Mistral...")
dataset = dataset.map(format_instruction, remove_columns=dataset.column_names)
else:
print("ℹ️ Dataset déjà formatté (colonne 'text' détectée)")
# Tokenisation
print(f"🔄 Tokenisation (max_length={MAX_LENGTH})...")
def tokenize_function(examples):
# Tokenisation avec padding et troncature
result = tokenizer(
examples["text"],
padding="max_length",
truncation=True,
max_length=MAX_LENGTH,
return_tensors=None # Pas de tensors pour l'instant
)
# Les labels sont identiques aux input_ids pour le language modeling
result["labels"] = result["input_ids"].copy()
return result
tokenized_dataset = dataset.map(
tokenize_function,
batched=True,
remove_columns=["text"],
desc="Tokenisation en cours"
)
print(f"✅ Dataset tokenisé : {len(tokenized_dataset)} exemples")
print(f"Colonnes finales : {tokenized_dataset.column_names}")
# Statistiques sur les longueurs
lengths = [len(x) for x in tokenized_dataset["input_ids"]]
print(f"\nStatistiques de longueur des séquences :")
print(f" Min : {min(lengths)} tokens")
print(f" Max : {max(lengths)} tokens")
print(f" Moyenne : {sum(lengths)/len(lengths):.1f} tokens")
return tokenized_dataset
def load_model_and_tokenizer(device):
"""
Charge le modèle Mistral-7B en quantification 4-bit avec PEFT/LoRA.
Args:
device: Device PyTorch (mps, cuda, ou cpu)
Returns:
tuple: (model, tokenizer)
"""
print_separator("CHARGEMENT DU MODÈLE MISTRAL-7B")
# ===== CHARGEMENT DU TOKENIZER =====
print(f"📥 Chargement du tokenizer : {MODEL_NAME}")
try:
tokenizer = AutoTokenizer.from_pretrained(
MODEL_NAME,
trust_remote_code=True,
padding_side="right", # Important pour le training
)
except Exception as err:
print(f"⚠️ Fast tokenizer failed ({err}); retrying with use_fast=False...")
tokenizer = AutoTokenizer.from_pretrained(
MODEL_NAME,
trust_remote_code=True,
padding_side="right",
use_fast=False,
)
# Ajout du pad_token si absent (Mistral n'en a pas par défaut)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
tokenizer.pad_token_id = tokenizer.eos_token_id
print(f"✅ Tokenizer chargé")
print(f" Vocabulaire : {len(tokenizer)} tokens")
print(f" PAD token : {tokenizer.pad_token} (ID: {tokenizer.pad_token_id})")
print(f" EOS token : {tokenizer.eos_token} (ID: {tokenizer.eos_token_id})")
# ===== CONFIGURATION DE LA QUANTIFICATION 4-BIT =====
print(f"\n🔧 Configuration de la quantification 4-bit...")
# Note : bitsandbytes peut ne pas supporter MPS, on va charger en float16 à la place
use_4bit = device.type != "mps" # 4-bit uniquement pour CUDA
if use_4bit:
print(" Mode : 4-bit avec bitsandbytes (CUDA)")
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_use_double_quant=True,
)
quantization_config = bnb_config
torch_dtype = torch.float16
else:
print(" Mode : float16 (MPS ne supporte pas 4-bit)")
quantization_config = None
torch_dtype = torch.float16
# ===== CHARGEMENT DU MODÈLE =====
print(f"\n📥 Chargement du modèle : {MODEL_NAME}")
print(f" Cela peut prendre 30-60 secondes...")
start_time = time.time()
mem_before = get_memory_usage()
model = AutoModelForCausalLM.from_pretrained(
MODEL_NAME,
quantization_config=quantization_config,
torch_dtype=torch_dtype,
device_map="auto" if use_4bit else None, # Auto device map pour 4-bit
trust_remote_code=True,
cache_dir="./models" # Cache local comme dans V1
)
# Déplacement vers MPS si nécessaire
if device.type == "mps" and not use_4bit:
print(f" Déplacement du modèle vers {device}...")
model = model.to(device)
load_time = time.time() - start_time
mem_after = get_memory_usage()
print(f"✅ Modèle chargé en {load_time:.1f}s")
print(f" Mémoire utilisée : {mem_after - mem_before:.2f} GB")
print(f" Mémoire totale processus : {mem_after:.2f} GB")
print(f" Device : {next(model.parameters()).device}")
print(f" Dtype : {next(model.parameters()).dtype}")
# ===== PRÉPARATION POUR K-BIT TRAINING =====
if use_4bit:
print(f"\n🔧 Préparation du modèle pour k-bit training...")
model = prepare_model_for_kbit_training(model)
# ===== CONFIGURATION LORA =====
print(f"\n🔧 Configuration LoRA...")
print(f" Rang (r) : {LORA_R}")
print(f" Alpha : {LORA_ALPHA}")
print(f" Dropout : {LORA_DROPOUT}")
print(f" Modules cibles : {LORA_TARGET_MODULES}")
lora_config = LoraConfig(
r=LORA_R,
lora_alpha=LORA_ALPHA,
target_modules=LORA_TARGET_MODULES,
lora_dropout=LORA_DROPOUT,
bias="none",
task_type=TaskType.CAUSAL_LM
)
# Application de LoRA
model = get_peft_model(model, lora_config)
# Statistiques sur les paramètres
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
total_params = sum(p.numel() for p in model.parameters())
trainable_percent = 100 * trainable_params / total_params
print(f"\n📊 Paramètres du modèle :")
print(f" Total : {total_params:,}")
print(f" Entraînables : {trainable_params:,} ({trainable_percent:.2f}%)")
print(f" Réduction : {total_params / trainable_params:.1f}x")
return model, tokenizer
def train_model(model, tokenizer, train_dataset, device):
"""
Lance l'entraînement du modèle avec les hyperparamètres définis.
Args:
model: Modèle PEFT
tokenizer: Tokenizer
train_dataset: Dataset tokenisé
device: Device PyTorch
"""
print_separator("LANCEMENT DU FINE-TUNING")
# Configuration de l'entraînement
training_args = TrainingArguments(
output_dir=OUTPUT_DIR,
num_train_epochs=NUM_EPOCHS,
per_device_train_batch_size=BATCH_SIZE,
gradient_accumulation_steps=GRADIENT_ACCUMULATION,
learning_rate=LEARNING_RATE,
fp16=device.type == "cuda", # FP16 uniquement sur CUDA
logging_steps=10,
save_strategy="epoch",
save_total_limit=1,
report_to="none", # Pas de reporting externe
remove_unused_columns=False,
# Optimisations mémoire
gradient_checkpointing=False, # Désactivé car peut causer des problèmes avec LoRA
optim="adamw_torch",
warmup_steps=10,
)
# Data collator pour le language modeling
data_collator = DataCollatorForLanguageModeling(
tokenizer=tokenizer,
mlm=False # Causal LM, pas masked LM
)
print(f"📋 Configuration de l'entraînement :")
print(f" Époques : {NUM_EPOCHS}")
print(f" Batch size : {BATCH_SIZE}")
print(f" Gradient accumulation : {GRADIENT_ACCUMULATION}")
print(f" Batch effectif : {BATCH_SIZE * GRADIENT_ACCUMULATION}")
print(f" Learning rate : {LEARNING_RATE}")
print(f" FP16 : {training_args.fp16}")
print(f" Steps totaux : ~{len(train_dataset) // (BATCH_SIZE * GRADIENT_ACCUMULATION) * NUM_EPOCHS}")
# Création du Trainer
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
data_collator=data_collator,
)
# Lancement de l'entraînement
print(f"\n🚀 Démarrage de l'entraînement...")
print(f" Mémoire avant training : {get_memory_usage():.2f} GB\n")
start_time = time.time()
try:
trainer.train()
except Exception as e:
print(f"\n❌ Erreur pendant l'entraînement : {e}")
print(f" Type d'erreur : {type(e).__name__}")
raise
train_time = time.time() - start_time
print(f"\n✅ Entraînement terminé en {train_time/60:.1f} minutes")
print(f" Mémoire après training : {get_memory_usage():.2f} GB")
return trainer
def save_model(model, tokenizer):
"""
Sauvegarde le modèle fine-tuné et le tokenizer.
Args:
model: Modèle PEFT entraîné
tokenizer: Tokenizer
"""
print_separator("SAUVEGARDE DU MODÈLE")
print(f"💾 Sauvegarde dans : {OUTPUT_DIR}")
# Sauvegarde du modèle LoRA (uniquement les adaptateurs)
model.save_pretrained(OUTPUT_DIR)
# Sauvegarde du tokenizer
tokenizer.save_pretrained(OUTPUT_DIR)
# Vérification des fichiers créés
files = os.listdir(OUTPUT_DIR)
print(f"✅ Modèle sauvegardé")
print(f" Fichiers créés : {len(files)}")
print(f" Fichiers principaux :")
for f in sorted(files)[:10]: # Affiche les 10 premiers
size = os.path.getsize(os.path.join(OUTPUT_DIR, f)) / 1024**2
print(f" - {f} ({size:.1f} MB)")
def test_generation(model, tokenizer, device):
"""
Teste le modèle fine-tuné avec une requête SEO.
Args:
model: Modèle fine-tuné
tokenizer: Tokenizer
device: Device PyTorch
"""
print_separator("TEST DE GÉNÉRATION")
# Requête de test
test_query = "backlink"
prompt = f"[INST] Donne une définition précise du terme '{test_query}' en SEO. [/INST]"
print(f"🔍 Requête de test : \"{test_query}\"")
print(f"📝 Prompt : {prompt}\n")
# Tokenisation
inputs = tokenizer(prompt, return_tensors="pt")
# Déplacement vers le bon device
if device.type == "mps":
inputs = {k: v.to(device) for k, v in inputs.items()}
# Génération
print("🤖 Génération en cours...")
start_time = time.time()
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=256,
temperature=0.3,
top_p=0.95,
do_sample=True,
pad_token_id=tokenizer.pad_token_id,
eos_token_id=tokenizer.eos_token_id
)
gen_time = time.time() - start_time
# Décodage
generated_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
# Extraction de la réponse (après [/INST])
if "[/INST]" in generated_text:
response = generated_text.split("[/INST]")[1].strip()
else:
response = generated_text
print(f"✅ Génération terminée en {gen_time:.2f}s\n")
print(f"{'─'*80}")
print(f"RÉPONSE DU MODÈLE :")
print(f"{'─'*80}")
print(response)
print(f"{'─'*80}")
# ============================================================================
# FONCTION PRINCIPALE
# ============================================================================
def main():
"""
Fonction principale orchestrant le pipeline de fine-tuning.
"""
print("\n")
print("╔" + "═"*78 + "╗")
print("║" + " "*20 + "MISTRAL SEO FINE-TUNING V2" + " "*32 + "║")
print("║" + " "*15 + "Fine-Tuning de Mistral-7B sur Dataset SEO" + " "*22 + "║")
print("╚" + "═"*78 + "╝")
try:
# ===== ÉTAPE 1 : Détection du matériel =====
device = check_mps_availability()
# ===== ÉTAPE 2 : Chargement du dataset =====
dataset = load_and_prepare_dataset()
# ===== ÉTAPE 3 : Chargement du modèle et tokenizer =====
model, tokenizer = load_model_and_tokenizer(device)
# ===== ÉTAPE 4 : Tokenisation du dataset =====
train_dataset = tokenize_dataset(dataset, tokenizer)
# ===== ÉTAPE 5 : Fine-tuning =====
trainer = train_model(model, tokenizer, train_dataset, device)
# ===== ÉTAPE 6 : Sauvegarde =====
save_model(model, tokenizer)
# ===== ÉTAPE 7 : Test de génération =====
test_generation(model, tokenizer, device)
# ===== RÉSUMÉ FINAL =====
print_separator("RÉSUMÉ DU FINE-TUNING")
print(f"✅ Fine-tuning terminé avec succès !")
print(f" Modèle sauvegardé : {OUTPUT_DIR}")
print(f" Dataset : {DATASET_NAME} ({len(dataset)} exemples)")
print(f" Modèle : {MODEL_NAME}")
print(f" Device : {device}")
print(f" Mémoire finale : {get_memory_usage():.2f} GB")
print(f"\n📚 Pour utiliser le modèle fine-tuné :")
print(f" from peft import PeftModel, PeftConfig")
print(f" from transformers import AutoModelForCausalLM, AutoTokenizer")
print(f" ")
print(f" config = PeftConfig.from_pretrained('{OUTPUT_DIR}')")
print(f" model = AutoModelForCausalLM.from_pretrained(config.base_model_name_or_path)")
print(f" model = PeftModel.from_pretrained(model, '{OUTPUT_DIR}')")
print(f" tokenizer = AutoTokenizer.from_pretrained('{OUTPUT_DIR}')")
print_separator()
except KeyboardInterrupt:
print("\n\n⚠️ Interruption manuelle (Ctrl+C)")
print("Le processus a été arrêté par l'utilisateur.")
sys.exit(1)
except Exception as e:
print(f"\n\n❌ ERREUR FATALE : {e}")
print(f"Type d'erreur : {type(e).__name__}")
import traceback
traceback.print_exc()
sys.exit(1)
# ============================================================================
# POINT D'ENTRÉE
# ============================================================================
if __name__ == "__main__":
main()
Conclusion
Ce n’est pas un exercice simple, et encore moins quand on cherche à comprendre pourquoi et comment fonctionne réellement le fine-tuning d’un modèle IA.
Mais une fois qu’on a mis les mains dedans, on découvre que ce n’est pas de la magie — c’est surtout beaucoup de rigueur, de logique et de patience.
Ici, le modèle de base reste modeste, Mistral-7B-Instruct, mais il est largement suffisant pour un usage SEO.
Et surtout, il prouve qu’un fine-tuning local sur MacBook est non seulement possible, mais aussi performant et stable.
La suite est déjà tracée pour la V3 :
explorer des datasets plus riches, créer des boucles d’apprentissage supervisées par GPT-5, et surtout faire évoluer le modèle grâce à ses propres erreurs.
Une vraie boucle vertueuse où un grand modèle corrige un petit, jusqu’à produire un LLM SEO auto-évolutif et hébergeable en local.
Ce projet montre aussi à quel point Python et l’écosystème IA sont accessibles, même pour un développeur issu du monde Symfony ou du web.
On n’est plus très loin du moment où un site pourra embarquer son propre mini-LLM — un modèle affiné, auto-hébergé, spécialisé dans un métier précis, sans dépendre totalement d’un service cloud.
Et ça, c’est sans doute la prochaine étape naturelle :
mixer le monde du web et celui du machine learning pour créer des applications vraiment intelligentes, ancrées dans la donnée métier.
Laisser un commentaire