Accueil / Articles / Notes pratiques : Du ajustement fin à la précision : Réduction significative

Notes pratiques : Du ajustement fin à la précision : Réduction significative

Guide pratique détaillé : Des ajustements fins à la précision – Réduction significative des contrats, des vérifications et des emplacements de code à insérer pour les équipes utilisant ce modèle.

2822 mots

Ce guide reconstitue le parcours allant des matières premières à un système fonctionnel pour : De l’ajustement fin au niveau de précision : réduire significativement les hallucinations dans votre pipeline RAG. L’accent est mis sur des étapes opérationnelles, des vérifications explicites et du code que vous pouvez intégrer directement dans un dépôt sans devoir deviner l’intention.

1. Introduction

Pour la phase d’introduction, définissez les entrées, le responsable de l’étape et les critères d’achèvement avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans avoir à deviner l’état caché. Considérez cette phase comme un contrat entre les entrées et les sorties validées. Nommez les artefacts, définissez des vérifications de succès et refusez toute complétion partielle silencieuse. Citez les passages qui ont réellement servi de base à la réponse ; sans citations, les opérateurs ne peuvent pas distinguer une hallucination d’un manque d’indexation.

You: What was the revenue from contracts with customers in 2024?
Assistant: The revenue was €179,058,000.  ← Wrong! Correct answer is €159,088,000.

2. Pourquoi l’ajustement fin ?

Pour l’étape de fine-tuning « 2 Why », définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Enregistrez les temps d’exécution ainsi que le coût en tokens ou requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le processus passe d’un environnement de démonstration à des environnements partagés. Citez les passages qui ont réellement servi de base à la réponse. Sans citations, les opérateurs ne peuvent pas distinguer une hallucination d’un manque dans l’indexation.

La solution : le fine-tuning avec MLX

Pour le affinage de The Solution par étapes, définissez les entrées, le responsable de chaque étape ainsi que les critères d’arrêt avant de modifier du code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu, sans avoir à deviner l’état caché. Conservez la configuration en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les opérateurs peuvent auditer sans devoir lire l’ensemble du système. Citez les passages qui servent réellement de base à la réponse. Sans citations, les opérateurs ne peuvent pas distinguer une hallucination d’un manque d’indexation.

3. Diagramme d’architecture

Pour l’étape 3 des diagrammes d’architecture, définissez les entrées, le responsable de l’étape et les critères de fin avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Documentez ensemble le parcours idéal et le parcours de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non traités font partie du produit, et non d’une mise en forme ultérieure. Citez les passages qui ont réellement servi de base à la réponse. Sans citations, les opérateurs ne peuvent pas distinguer une hallucination d’un manque d’indexation.

┌─────────────────────────────────────────────────────────────────────────────┐
│                       PART 3: FINE-TUNING WORKFLOW (M1)                     │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   1. Dataset                2. MLX Fine-Tuning                              │
│  ┌──────────────────┐      ┌────────────────────────────────────────────┐   │
│  │ Chart Images     │─────▶│ Base Model (Qwen2-VL-2B)                   │   │
│  │ Q&A Pairs        │      │  + LoRA Adapters (mlx_vlm.lora)            │   │
│  │ (train.jsonl)    │      │  + Unified Memory Training on Apple Silicon│   │
│  └──────────────────┘      └────────────────┬───────────────────────────┘   │
│                                             │                               │
│                                             ▼                               │
│                            ┌──────────────────────────────────────────┐     │
│                            │ Fine-Tuned LoRA Adapters                 │     │
│                            │ (./fine_tuned_adapters/)                 │     │
│                            └────────────────┬─────────────────────────┘     │
│                                             │                               │
│                                             ▼                               │
│                            ┌──────────────────────────────────────────┐     │
│                            │ 3. Merge & Export to GGUF                │     │
│                            │ (mlx_vlm.fuse + convert_hf_to_gguf.py)   │     │
│                            └────────────────┬─────────────────────────┘     │
│                                             │                               │
│                                             ▼                               │
│                            ┌──────────────────────────────────────────┐     │
│                            │ 4. Deploy with Ollama                    │     │
│                            │ ollama create my-chart-model             │     │
│                            │ (text.gguf + mmproj.gguf)                │     │
│                            └────────────────┬─────────────────────────┘     │
│                                             │                               │
│                                             ▼                               │
│  ┌──────────────────────────────────────────────────────────────────────┐   │
│  │ 5. Update rag_engine.py                                              │   │
│  │                                                                      │   │
│  │   VISION_MODEL = "my-chart-model"  # ← Change ONE line               │   │
│  │   TEXT_MODEL   = "llama3.2:3b"     # Unchanged                       │   │
│  │                                                                      │   │
│  │   ✔ Existing app.py (from Part 2) automatically uses the new model!  │   │
│  └──────────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────────┘

4. Préparation du jeu de données

Pour l’étape 4 « Préparation du jeu de données », définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Préférez des unités petites et testables à des scripts complexes. Lorsqu’une étape échoue, l’échec doit indiquer une seule responsabilité plutôt qu’un pipeline embrouillé. Citez les passages qui ont réellement servi de base à la réponse. Sans citations, les opérateurs ne peuvent pas distinguer une hallucination d’un manque d’indexation. Pour l’étape 4 « Préparation du jeu de données », définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Enregistrez les temps d’exécution ainsi que le coût en tokens ou requêtes à côté des résultats fonctionnels. Une visibilité précoce du coût évite les factures inattendues lorsque le processus passe d’une démonstration à un environnement partagé.

Model sees:    (Chart image)
Model reads:   (Question)
Model learns:  (Correct answer)

Sources des ensembles de données

Lors de la phase des sources des ensembles de données, notez d’abord les exigences : entrées requises, signal de succès et conséquences en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Conservez la configuration en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les opérateurs peuvent auditer sans devoir lire l’ensemble du système. Mesurez le taux de rappel sur un ensemble de questions fixe avant d’ajuster les prompts. Un changement fréquent des prompts ne résout que rarement un système de récupération insuffisant.

Préparation des données étape par étape

Lors de la phase de préparation des données étape par étape, notez d’abord les exigences du contrat : entrées requises, signal de succès et comportement en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Documentez ensemble le parcours normal et les scénarios de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non traités font partie intégrante du produit, et non d’améliorations apportées ultérieurement. Mesurez le taux de rappel sur un ensemble de questions fixe avant d’ajuster les prompts. Un changement fréquent des prompts ne résout que rarement un système de récupération insuffisant.

Step 1: Create Raw Dataset       →  chart_dataset/raw_train.jsonl
Step 2: Convert to MLX Format    →  chart_dataset/train.jsonl
Step 3: Verify Dataset           →  Check that train.jsonl exists

Étape 1 : Créer l’ensemble de données brutes

Lors de l’exécution de l’Étape 1 « Créer l’étape », notez d’abord le contrat : les entrées requises, le signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Préférez des unités petites et testables plutôt que des scripts complexes. Lorsqu’une étape échoue, l’échec doit indiquer une seule responsabilité et non un processus embrouillé. Mesurez le taux de rappel sur un ensemble de questions fixe avant d’ajuster les prompts. Un changement fréquent de prompts ne résout que rarement un système de récupération insuffisant. Lors de l’exécution de l’Étape 1 « Créer l’étape », notez d’abord le contrat : les entrées requises, le signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Enregistrez les temps d’exécution ainsi que le coût en tokens ou en requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le processus passe de l’environnement de démonstration à des environnements partagés.

# download_chartqa.py
from datasets import load_dataset
import json
import os

os.makedirs("chart_dataset", exist_ok=True)

# Download ChartQA dataset
dataset = load_dataset("ahmed-masry/ChartQA", split="train")

# Convert to flat format with standard keys
converted = []
for item in dataset:
    converted.append({
        "image": item["imgname"],      # Path to chart image
        "question": item["query"],
        "answer": item["label"]
    })

# Save as raw dataset
with open("chart_dataset/raw_train.jsonl", "w") as f:
    for entry in converted:
        f.write(json.dumps(entry) + "\n")

print(f"✅ Converted {len(converted)} ChartQA samples to chart_dataset/raw_train.jsonl")
README.md: 100%|█████████████████████████████████████████████| 2.12k/2.12k [00:00<00:00, 3.72MB/s]
Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.
data/train-00000-of-00003.parquet: downloading bytes: ████████████████████████|  213MB, 17.3MB/s
......
Generating test split: 100%|████████████████████████| 2500/2500 [00:00<00:00, 20517.87 examples/s]
✅ Converted 28299 ChartQA samples to raw_train.jsonl

Option B : Générer des données synthétiques

L’étape de génération de données synthétiques de l’Option B fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un transcript idéal, un cas d’échec et la note de réversion avant d’élargir le périmètre. Conservez la configuration en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les opérateurs peuvent auditer sans devoir lire l’ensemble du système. Séparez la politique de segmentation de la politique de récupération. Modifier l’une ne doit pas obliger à réécrire l’autre lorsque les métriques de qualité évoluent.

# generate_synthetic_data.py
import json
import matplotlib.pyplot as plt
import numpy as np
import os

os.makedirs("chart_dataset/images", exist_ok=True)

dataset = []
for i in range(100):
    categories = ['Q1', 'Q2', 'Q3', 'Q4']
    values = np.random.randint(100, 500, 4)

    plt.figure()
    plt.bar(categories, values)
    plt.title(f"Quarterly Revenue {i}")
    plt.savefig(f"chart_dataset/images/chart_{i:03d}.png")
    plt.close()

    dataset.append({
        "image": f"images/chart_{i:03d}.png",
        "question": "Which quarter had the highest revenue?",
        "answer": f"Q{np.argmax(values) + 1} with ${max(values)} million"
    })

with open("chart_dataset/raw_train.jsonl", "w") as f:
    for entry in dataset:
        f.write(json.dumps(entry) + "\n")

print("✅ Generated 100 synthetic samples in chart_dataset/raw_train.jsonl")

Étape 2 : Convertir en format MLX

La deuxième étape, la conversion en phase, fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un transcript parfait, un cas d’échec et la note de réversion avant d’élargir le périmètre. Documentez ensemble le parcours idéal et le parcours de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non livrés font partie intégrante du produit, et non d’une mise en forme ultérieure. Séparez la politique de segmentation de la politique de récupération : modifier l’une ne doit pas obliger à réécrire l’autre lorsque les métriques de qualité évoluent.

# convert_to_mlx_format.py
import json
import os

def convert_raw_to_mlx_format(data_dir="chart_dataset"):
    """
    Convert raw dataset to MLX format.

    Input:  chart_dataset/raw_train.jsonl
    Output: chart_dataset/train.jsonl (MLX-compatible)
    """
    raw_file = os.path.join(data_dir, "raw_train.jsonl")
    out_file = os.path.join(data_dir, "train.jsonl")

    if not os.path.exists(raw_file):
        print(f"❌ {raw_file} not found. Run download_chartqa.py or generate_synthetic_data.py first.")
        return None

    dataset = []
    with open(raw_file, "r") as f:
        for line in f:
            item = json.loads(line)

            # Build full image path
            img_str = item["image"]
            image_path = img_str if img_str.startswith(data_dir) else os.path.join(data_dir, img_str)

            dataset.append({
                "images": [image_path],  # MUST be a list
                "messages": [
                    {"role": "user", "content": item["question"]},
                    {"role": "assistant", "content": item["answer"]}
                ]
            })

    with open(out_file, "w") as f:
        for entry in dataset:
            f.write(json.dumps(entry) + "\n")

    print(f"✅ Converted {len(dataset)} samples to {out_file}")
    return dataset

if __name__ == "__main__":
    convert_raw_to_mlx_format()
python convert_to_mlx_format.py

Étape 3 : Vérifier l’ensemble de données

La étape 3, Vérifier que l’étape fonctionne correctement, est optimale lorsqu’elle est considérée comme une surface mesurable. Capturez un exemple réussi, un cas d’échec ainsi que la note de réversion avant d’élargir le périmètre. Préférez des unités petites et testables aux scripts complexes. Lorsqu’une étape échoue, l’erreur doit indiquer une seule responsabilité plutôt qu’un processus embrouillé. Séparez la politique de segmentation de la politique de récupération. Modifier l’une ne doit pas obliger à réécrire l’autre lorsque les métriques de qualité changent.

ls -la chart_dataset/
chart_dataset/
├── raw_train.jsonl     # Raw Q&A pairs (flat keys)
├── train.jsonl         # MLX-formatted (overwritten by prepare_mlx_dataset)
└── images/             # Chart images

📌 Important: Before running the fine-tuning command, ensure train.jsonl exists in your dataset directory.
If you see a FileNotFoundError, you haven't run the conversion step yet.

5. Ajustements finaux avec MLX

La phase de réglage fin avec MLX fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un transcript parfait, un cas d’échec et la note de réversion avant d’élargir le périmètre. Considérez cette phase comme un contrat entre les entrées et les sorties validées. Nommez les artefacts, définites des critères de succès et refusez toute complétion partielle silencieuse. Séparez la politique de segmentation des données de la politique de récupération. Modifier l’une ne doit pas obliger à réécrire l’autre lorsque les métriques de qualité évoluent.

Prérequis

La phase des prérequis fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un transcript parfait, un cas d’échec et la note de réversion avant d’élargir le périmètre. Enregistrez les temps d’exécution ainsi que le coût des tokens ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le processus passe de l’environnement de démonstration aux environnements partagés. Séparez la politique de segmentation de la politique de récupération : modifier l’une ne doit pas obliger à réécrire l’autre lorsque les métriques de qualité changent.

# For fine-tuning vision models
pip install mlx-vlm

# For merging adapters (needed after training)
pip install mlx-lm

Exécuter le affinement

La phase de fine-tuning fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un transcript parfait, un cas d’échec et la note de rollback avant d’élargir le périmètre. Conservez la configuration en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les opérateurs peuvent auditer sans devoir lire l’ensemble du système. Séparez la politique de segmentation des données de la politique de récupération. Modifier l’une ne doit pas obliger à réécrire l’autre lorsque les métriques de qualité évoluent.

python -m mlx_vlm.lora \
    --model Qwen/Qwen2-VL-2B-Instruct \
    --dataset ./chart_dataset/train.jsonl \
    --iters 1000 \
    --batch-size 1 \
    --lora-rank 8 \
    --gradient-accumulation-steps 4 \
    --max-seq-length 512

Comprendre les résultats de l’entraînement

La phase de génération des résultats d’entraînement à la compréhension fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un transcript parfait, un cas d’échec et la note de réversion avant d’élargir le périmètre. Documentez ensemble le parcours optimal et le parcours de récupération. Les tentatives répétées, les contrôles humains et le traitement des messages non livrés font partie intégrante du produit, et non d’une mise en forme ultérieure. Séparez la politique de segmentation des données de la politique de récupération ; modifier l’une ne doit pas obliger à réécrire l’autre lorsque les métriques de qualité évoluent.

INFO:__main__:Loading model from Qwen/Qwen2-VL-2B-Instruct
Fetching 11 files: 100%|████████████████████████████████████████| 11/11 [00:00<00:00, 1465.47it/s]
Download complete: :                                                          |  0.00B
Reconstruction complete: |                                           |  0.00B /  0.00B
INFO:__main__:Loading dataset from ./chart_dataset/train.jsonl
INFO:__main__:Setting up LoRA
#trainable params: 9.232384 M || all params: 2208.9856 M || trainable%: 0.418%
INFO:__main__:Setting up optimizer
INFO:__main__:Training model (sft)
Starting training..., iterations: 1000
No validation dataset provided — training will run without validation.
......

Iter 120: Train loss 7.77592850, Learning Rate 2.000e-05,
It/sec 0.242, Tokens/sec 103.862, Trained Tokens 51480, Peak mem 8.069 GB
.....

Saved final adapter weights to adapters.safetensors.
INFO:__main__:Training completed! Model saved to adapters.safetensors

6. Résolution de problèmes

La phase de dépannage en 6 étapes fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un exemple idéal, un cas d’échec et la note de réversion avant d’élargir le champ d’action. Préférez des unités petites et testables plutôt que des scripts complexes. Lorsqu’une étape échoue, l’erreur doit indiquer une seule responsabilité plutôt qu’un processus embrouillé. Séparez la politique de segmentation de la politique de récupération. Modifier l’une ne doit pas obliger à réécrire l’autre lorsque les métriques de qualité changent.

Erreur : ensemble de données non trouvé

La phase d’erreur « Ensemble de données non trouvé » fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un exemple idéal, un cas d’échec et la note de réversion avant d’élargir le champ d’action.

FileNotFoundError: Couldn't find any data file at .../chart_dataset/train.jsonl
python convert_to_mlx_format.py

Erreur : manque de mémoire (OOM)

RuntimeError: [METAL] Command buffer execution failed: Insufficient Memory
python -m mlx_vlm.lora \
    --model Qwen/Qwen2-VL-2B-Instruct \
    --dataset ./chart_dataset/train.jsonl \
    --iters 1000 \
    --batch-size 1 \              # ← Reduced from 4 to 1
    --lora-rank 4 \               # ← Reduced from 8 to 4
    --gradient-accumulation-steps 8 \  # ← Added
    --max-seq-length 256               # ← Added

Erreur : chemin de l’adaptateur non trouvé

FileNotFoundError: The adapter path does not exist: fine_tuned_adapters
ls -la adapters.safetensors adapter_config.json
python -m mlx_lm fuse \
    --model Qwen/Qwen2-VL-2B-Instruct \
    --adapter-path . \
    --save-path ./fine_tuned_model_merged

Erreur : cache incomplet

IncompleteSnapshotError: The cached snapshot for 'Qwen/Qwen2-VL-2B-Instruct' is incomplete
rm -rf ~/.cache/huggingface/hub/models--Qwen--Qwen2-VL-2B-Instruct

7. Combien de temps durera la formation ?

Total Time (seconds) = Total Iterations ÷ It/sec
Total Time (minutes) = Total Time (seconds) ÷ 60
1000 ÷ 0.242 = 4,132 seconds
4,132 ÷ 60 = ~69 minutes

8. Fusion des adaptateurs LoRA

python -m mlx_lm fuse \
    --model Qwen/Qwen2-VL-2B-Instruct \
    --adapter-path . \
    --save-path ./fine_tuned_model_merged

9. Export vers GGUF pour Ollama

Étape 1 : Exporter MLX au format Hugging Face

#!/usr/bin/env python3
import mlx_lm
from mlx_lm import load, save

model, tokenizer, config = load("./fine_tuned_model_merged")
save(model, tokenizer, config, "./hf_export")
print("✅ Exported to ./hf_export")
python export_to_hf.py

Étape 2 : Convertir en GGUF

# Clone llama.cpp (if not already done)
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp

# Convert text model
python convert_hf_to_gguf.py ../hf_export \
    --outfile chart_model-text.gguf \
    --outtype f16

Étape 3 : Convertir le Vision Projector

python convert_hf_to_gguf.py ../hf_export \
    --outfile chart_model-mmproj.gguf \
    --outtype f16 \
    --mmproj
ls ~/.cache/huggingface/hub/models--Qwen--Qwen2-VL-2B-Instruct/snapshots/

# sample output . You'll see a hash directory (e.g., 895c3a49...)
# 895c3a49bc3fa70a340399125c650a463535e71c
cd llama.cpp
# replace <hash> with the output above (e.g., 895c3a49...)

python convert_hf_to_gguf.py ~/.cache/huggingface/hub/models--Qwen--Qwen2-VL-2B-Instruct/snapshots/<hash>/ \
    --outfile base-mmproj.gguf \
    --outtype f16 \
    --mmproj

10. Déploiement avec Ollama

FROM ./chart_model-text.gguf
FROM ./chart_model-mmproj.gguf
PARAMETER temperature 0.2

Créer le modèle

ollama create my-chart-model -f Modelfile

Vérifier le modèle

ollama list

# output :

# NAME                     ID              SIZE      MODIFIED
# my-chart-model:latest    b3fd6d8fd742    4.4 GB    7 hours ago
# qwen2.5vl:3b             fb90415cde1e    3.2 GB    9 days ago
# llama3.2:3b              a80c4f17acd5    2.0 GB    10 days ago
# llama3:latest            365c0bd3c000    4.7 GB    3 months ago

# You should see my-chart-model in the list.

Tester le modèle

ollama run my-chart-model "What is 2+2?"

11. Intégrer dans votre application RAG existante

# In rag_engine.py (from Parts 1 & 2)

# Before:
VISION_MODEL = "qwen2.5vl:3b"

# After:
VISION_MODEL = "my-chart-model"  # Your fine-tuned model
TEXT_MODEL = "llama3.2:3b"       # Unchanged
python app.py

12. Évaluation : modèle de base contre modèle affiné

Comparaison

13. Conclusion

Obtenir le code complet

git clone https://github.com/froilan-sia/m1_multimodal_rag.git
cd m1_multimodal_rag

Entrer en contact avec moi

Checklist opérationnelle