Accéder au contenu principal

Gradient checkpointing : un affinage efficace avec Unsloth et NVIDIA

Découvrez comment le gradient checkpointing optimisé pour NVIDIA d'Unsloth et QLoRA réduisent l'usage de la VRAM lors de l'affinage de Qwen3.5 4B pour l'OCR de documents médicaux.
Actualisé 14 août 2026  · 12 min lire

Explorer avec l’IA

ChatGPTClaudePerplexity

La collaboration d’Unsloth avec NVIDIA vise à accélérer l’affinage en réduisant les surcoûts cachés d’entraînement. Plutôt que de compter uniquement sur des GPU plus puissants ou des modèles plus petits, les améliorations ciblent les goulots d’étranglement au sein du processus d’entraînement, comme la reconstruction répétée des métadonnées, les délais de rechargement des activations et un routage de jetons inefficace. Pour les utilisateurs, cela se traduit par un entraînement plus rapide et un flux d’affinage plus fluide sur les GPU NVIDIA pris en charge.

Dans ce guide, nous allons découvrir ces nouvelles améliorations de performance d’Unsloth et appliquer le workflow d’affinage optimisé d’Unsloth à une tâche vision-langage concrète. Nous allons affiner Qwen3.5 Vision 4B pour de l’OCR médical, où le modèle apprend à extraire du texte structuré à partir d’images de documents médicaux en utilisant un petit sous-ensemble « d’allure médicale » d’un jeu de données OCR.

Nous utiliserons :

  • Qwen3.5 4B (Vision) comme modèle de base
  • QLoRA en 4 bits pour réduire l’usage de la VRAM
  • Adaptateurs LoRA pour un affinage efficace
  • Gradient checkpointing Unsloth pour économiser la mémoire pendant l’entraînement
  • Un sous-ensemble de 300 exemples d’un jeu de données d’OCR médical
  • Un prétraitement d’images à taille fixe pour un entraînement vision plus fluide
  • Une évaluation avant/après pour comparer les sorties du modèle de base et du modèle affiné

Utiliser le workflow d’affinage optimisé pour NVIDIA d’Unsloth

Avant de commencer l’affinage, il est utile de comprendre ce que la collaboration d’Unsloth avec NVIDIA améliore et comment cela s’articule avec ce guide.

Unsloth indique que sa collaboration avec NVIDIA rend l’entraînement des LLM environ 25 % plus rapide, sans perte de précision, en plus de ses accélérations d’affinage existantes de 2à 5×. Ces gains proviennent de la réduction des surcoûts cachés autour du processus principal d’entraînement plutôt que de la modification de l’objectif d’apprentissage du modèle. En d’autres termes, l’objectif est de rendre l’affinage plus rapide et plus efficace tout en maintenant la précision.

Source : How to Make LLM Training Faster with Unsloth and NVIDIA

Source : How to Make LLM Training Faster with Unsloth and NVIDIA 

Vous souhaitez vous lancer dans l'IA générative ?

Apprenez à travailler avec des LLM en Python directement dans votre navigateur

Commencez Maintenant

Des performances d’entraînement améliorées

La collaboration met en avant plusieurs gains de performance, notamment :

  • 14,3 % plus rapide par lot sur un benchmark Qwen3-14B QLoRA SFT grâce à la mise en cache des métadonnées de séquences compactées
  • 8,4 % de gain sur les modèles 8B, 6,7 % sur les 14B et 4,6 % sur les 32B via un gradient checkpointing asynchrone à double tampon
  • Environ 10à 15 % d’accélération pour l’entraînement GPT-OSS MoE, avec des passes avant 23 % plus rapides et arrière 13 % plus rapides sur le chemin de routage ciblé

Les plus gros gains issus de la collaboration Unsloth + NVIDIA concernent l’entraînement texte seul compacté et les modèles Mixture-of-Experts. Nous ne les utilisons pas ici, car notre flux de travail cible l’affinage OCR avec Qwen3.5 Vision.

Dans ce guide, nous utilisons un GPU NVIDIA RTX 3090 ; le workflow s’appuie donc sur l’accélération GPU NVIDIA et le chemin d’affinage optimisé d’Unsloth. Nous ne comparons pas Unsloth à un autre entraîneur ; ce guide ne doit donc pas être lu comme une preuve indépendante des gains annoncés. Nous appliquons plutôt le workflow optimisé d’Unsloth à une véritable tâche vision-langage.

Gradient checkpointing

Pour ce workflow, l’optimisation la plus pertinente est le gradient checkpointing d’Unsloth. Il aide à réduire l’usage mémoire pendant l’entraînement en évitant de stocker toutes les activations en mémoire GPU. C’est particulièrement utile pour l’affinage vision-langage, où le modèle doit traiter à la fois des entrées image et des sorties texte.

1. Préparer Unsloth pour un affinage plus rapide

Pour suivre ce guide, vous avez besoin d’un GPU NVIDIA. Vous pouvez en louer un sur des plateformes comme RunPod, Vast.ai ou tout autre fournisseur de GPU cloud. J’ai d’abord tenté RunPod, généralement rapide et fiable, mais les options RTX 3090 étaient alors limitées. J’ai donc utilisé une machine RTX 3090 Vast.ai pour ce workflow.

Pour comparer les différentes plateformes, consultez notre guide des meilleurs fournisseurs de GPU cloud.

Instance Vast.ai RTX 3090

Source : Vast.ai | Console 

Après le lancement de l’instance, j’ai ouvert Jupyter Notebook et créé un nouveau notebook. Sur Vast.ai, j’ai sélectionné le noyau d’environnement principal disponible pour installer les paquets Python requis dans l’environnement du notebook sans impacter les dépendances système.

Installer les paquets requis

Commencez par installer les paquets nécessaires pour Unsloth, PyTorch, l’entraînement du modèle de vision, le chargement du jeu de données et l’intégration Hugging Face :

!pip install --upgrade \
    "torch>=2.8.0" "triton>=3.4.0" \
    numpy pillow torchvision bitsandbytes \
    unsloth "unsloth_zoo>=2026.4.6" \
    "datasets>=4.0.0" huggingface_hub hf_transfer pandas \
    transformers==5.2.0 torchcodec timm

Ces paquets s’appuient sur la configuration officielle du notebook Unsloth et incluent les bibliothèques essentielles pour charger Qwen3.5 Vision, préparer des données image‑texte et affiner le modèle avec Unsloth.

Configurer le périphérique CUDA

Ensuite, nous configurons le périphérique CUDA et vérifions que le bon GPU NVIDIA est disponible. Ce guide utilisant une RTX 3090, le code contrôle si CUDA est activé, confirme le GPU sélectionné, affiche les versions de CUDA et PyTorch et vérifie que la machine dispose de suffisamment de VRAM pour cet essai.

import os
import platform

CUDA_DEVICE_INDEX = 0
TARGET_GPU_NAME = "3090"

# Must be set before CUDA / Unsloth are initialized. Restart the kernel if you change these.
os.environ["CUDA_VISIBLE_DEVICES"] = str(CUDA_DEVICE_INDEX)

# RunPod + Qwen3.5 Vision OCR can hit Torch Dynamo fullgraph recompile limits.
# This disables Unsloth's torch.compile path while keeping Unsloth model loading,
# LoRA, gradient checkpointing, collator, and 8-bit optimizer benefits.
os.environ["UNSLOTH_COMPILE_DISABLE"] = "1"
os.environ["TORCH_COMPILE_DISABLE"] = "1"

import torch

DEVICE = torch.device("cuda:0")

print("Python:", platform.python_version())
print("PyTorch:", torch.__version__)
print("CUDA available:", torch.cuda.is_available())

if not torch.cuda.is_available():
    raise RuntimeError("CUDA is not available. Select a GPU instance before continuing.")

torch.cuda.set_device(0)
props = torch.cuda.get_device_properties(0)
gpu_name = torch.cuda.get_device_name(0)
total_gpu_memory_gb = props.total_memory / 1024**3

print("Selected device:", DEVICE)
print("GPU:", gpu_name)
print("CUDA version:", torch.version.cuda)
print("BF16 supported:", torch.cuda.is_bf16_supported())
print("Total GPU memory:", round(total_gpu_memory_gb, 2), "GB")

if TARGET_GPU_NAME not in gpu_name:
    raise RuntimeError(f"Expected an RTX {TARGET_GPU_NAME}, but CUDA device 0 is: {gpu_name}")

if total_gpu_memory_gb < 20:
    raise RuntimeError(f"Expected a 24 GB class 3090, but only found {total_gpu_memory_gb:.2f} GB VRAM.")

Dans mon environnement, la configuration GPU renvoyée était la suivante :

Python: 3.12.13
PyTorch: 2.12.0+cu130
CUDA available: True
Selected device: cuda:0
GPU: NVIDIA GeForce RTX 3090
CUDA version: 13.0
BF16 supported: True
Total GPU memory: 23.56 GB

Cela confirme que le notebook s’exécute sur une NVIDIA GeForce RTX 3090 avec suffisamment de VRAM pour l’expérience d’affinage.

Définir les paramètres d’entraînement et les invites

Après vérification du GPU, nous définissons le modèle, le jeu de données, les paramètres d’entraînement, les répertoires de sortie, la taille des images et les invites d’OCR.

MODEL_NAME = "unsloth/Qwen3.5-4B"
DATASET_NAME = "naazimsnh02/medocr-vision-dataset"

SAMPLE_COUNT = 300
EVAL_INDEX = 0
MAX_LENGTH = 4096
MAX_STEPS = 30

PER_DEVICE_BATCH_SIZE = 4
GRADIENT_ACCUMULATION_STEPS = 2
LEARNING_RATE = 2e-4
SEED = 3407

OUTPUT_DIR = "outputs/qwen35_vision_medical_ocr"
ADAPTER_DIR = "qwen35-vision-medical-ocr-lora"

# Medical document images vary heavily in size. Fixed-size canvases avoid
# repeated Torch Dynamo recompiles during vision training.
# 768x1024 is a practical portrait-page compromise for a 24 GB 3090 smoke test.
FIXED_IMAGE_SIZE = (768, 1024)

# Official Unsloth Qwen3.5 Vision notebook uses False here for 16-bit LoRA.
# Set True only if you hit VRAM limits.
LOAD_IN_4BIT = True

SYSTEM_PROMPT = "You are a medical OCR transcription engine. Return only the exact text visible in the medical document image."
INSTRUCTION = "Extract all readable text from this medical document exactly. Preserve structure when possible. Return only the OCR text, with no explanation, no diagnosis, no medical advice, and no reasoning."

Ici, nous utilisons Qwen3.5 Vision 4B d’Unsloth et le jeu de données vision pour l’OCR médical. Pour ce guide, nous sélectionnons 300 échantillons et entraînons pendant 30 itérations, ce qui reste léger tout en montrant comment le modèle s’adapte au format OCR ciblé.

La taille d’image fixe de 768×1024 aide à garder des entrées cohérentes pendant l’entraînement. Les documents médicaux varient fortement en résolution et ratio ; les redimensionner sur une toile fixe fluidifie le workflow et réduit les problèmes liés aux formes pendant l’affinage vision‑langage.

2. Charger le modèle

Maintenant que l’environnement est prêt, nous pouvons charger le modèle Qwen3.5 Vision 4B avec FastVisionModel d’Unsloth.

import unsloth
from unsloth import FastVisionModel
torch.cuda.set_device(0)

model, tokenizer = FastVisionModel.from_pretrained(
    MODEL_NAME,
    load_in_4bit=LOAD_IN_4BIT,
    use_gradient_checkpointing="unsloth",
)

print("Loaded:", MODEL_NAME)
print("4-bit:", LOAD_IN_4BIT)
print("Model device:", next(model.parameters()).device)

Après chargement, la sortie confirme que le bon modèle est chargé, le mode 4 bits est activé et le modèle est placé sur le GPU :

Loaded: unsloth/Qwen3.5-4B
4-bit: True
Model device: cuda:0

Ici, FastVisionModel.from_pretrained() charge le modèle vision‑langage et applique les optimisations d’Unsloth pour un affinage plus rapide et plus sobre en mémoire. Nous activons également load_in_4bit, ce qui réduit l’usage de VRAM en chargeant le modèle en précision 4 bits : idéal avec une RTX 3090 de 24 Go.

Nous activons aussi le gradient checkpointing d’Unsloth avec use_gradient_checkpointing="unsloth"

Cela aide à réduire l’usage mémoire pendant l’entraînement, point crucial pour les modèles vision-langage qui traitent simultanément images et texte.

3. Ajouter des adaptateurs LoRA

Nous ajoutons ensuite des adaptateurs LoRA au modèle. LoRA permet d’affiner un plus petit ensemble de paramètres entraînables au lieu de mettre à jour le modèle complet. L’entraînement devient ainsi plus rapide, plus sobre en mémoire et plus simple à exécuter sur un seul GPU.

 
model = FastVisionModel.get_peft_model(
    model,
    finetune_vision_layers=True,
    finetune_language_layers=True,
    finetune_attention_modules=True,
    finetune_mlp_modules=True,
    r=16,
    lora_alpha=16,
    lora_dropout=0,
    bias="none",
    random_state=SEED,
    use_rslora=False,
    loftq_config=None,
)

Pour ce guide, les adaptateurs sont appliqués aux parties vision et langage du modèle. Cela aide le modèle à lire les images de documents médicaux et à produire le texte OCR structuré attendu. Le modèle est désormais prêt à être entraîné sur le jeu de données d’OCR médical.

4. Charger le jeu de données d’OCR médical

Nous chargeons à présent le jeu de données d’OCR médical depuis Hugging Face et préparons un petit sous-ensemble pour l’affinage.

from datasets import load_dataset
from PIL import Image

raw_dataset = load_dataset(DATASET_NAME, split="train")

MEDICAL_KEYWORDS = [
    "doctor", "dr.", "clinic", "hospital", "patient", "medication",
    "medications", "prescription", "signature", "department", "report",
    "diagnosis", "lab", "laboratory", "blood", "hemoglobin", "mg", "dose",
    "<s_ocr>",
]

Le jeu de données contient des images de documents et leur texte OCR associé. Comme nous souhaitons ici des exemples à tonalité médicale, nous filtrons le jeu de données à l’aide d’un simple ensemble de mots-clés. Le code recherche des termes fréquents dans des documents médicaux : doctor, clinic, patient, medication, prescription, diagnosis, ainsi que des mots liés au dosage.

def looks_medical(sample):
    text = str(sample.get("text", "")).lower()
    return any(keyword in text for keyword in MEDICAL_KEYWORDS)

medical_indices = []
for idx, sample in enumerate(raw_dataset):
    if looks_medical(sample):
        medical_indices.append(idx)
        if len(medical_indices) >= SAMPLE_COUNT:
            break

if not medical_indices:
    raise RuntimeError("No medical-looking OCR samples found. Broaden MEDICAL_KEYWORDS or inspect the dataset text field.")

print(f"Selected {len(medical_indices)} medical-looking samples.")

Cela nous permet de sélectionner rapidement des exemples pertinents pour la tâche d’OCR médical. Pour cette exécution, nous retenons 300 échantillons.

Ensuite, nous normalisons chaque image sur une toile fixe en 768×1024. Les documents médicaux ayant des tailles et ratios très variables, cette étape homogénéise les données d’entraînement. L’image est redimensionnée en conservant son ratio puis placée sur un fond blanc.

def normalize_ocr_image(image, size=FIXED_IMAGE_SIZE):
    image = image.convert("RGB")
    target_w, target_h = size
    scale = min(target_w / image.width, target_h / image.height)
    new_w = max(1, int(image.width * scale))
    new_h = max(1, int(image.height * scale))
    resized = image.resize((new_w, new_h), Image.Resampling.LANCZOS)

    canvas = Image.new("RGB", size, "white")
    left = (target_w - new_w) // 2
    top = (target_h - new_h) // 2
    canvas.paste(resized, (left, top))
    return canvas

Plutôt que d’utiliser datasets.map, nous construisons manuellement une simple liste Python. Cela évite certains blocages possibles dans des notebooks cloud lors de la réécriture d’images PIL.

dataset = []
for idx in medical_indices:
    sample = raw_dataset[idx]
    dataset.append(
        {
            "image": normalize_ocr_image(sample["image"]),
            "text": sample["text"],
        }
    )

print("Examples:", len(dataset))
print("Columns:", list(dataset[0].keys()))
print("Fixed image size:", dataset[EVAL_INDEX]["image"].size)
print("Sample text:", dataset[EVAL_INDEX]["text"])

Après prétraitement, chaque exemple contient deux champs : l’image normalisée et le texte OCR cible.

Examples: 300
Columns: ['image', 'text']
Fixed image size: (768, 1024)
Sample text: <s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>

Nous pouvons aussi prévisualiser un exemple redimensionné :

dataset[EVAL_INDEX]["image"].resize((384, 512))

La prévisualisation montre une image de document à l’allure médicale avec les coordonnées de la clinique, le nom du médecin, les informations patient, les médicaments et la signature. Cela confirme la pertinence du jeu de données pour l’affinage OCR.

Ordonnance médicale.

5. Convertir les exemples en conversations vision

Maintenant que le jeu de données est chargé et les images normalisées, nous devons convertir chaque exemple au format de conversation attendu par Qwen3.5 Vision.

Chaque échantillon d’entraînement doit inclure trois parties :

  • Un message système définissant le rôle du modèle comme moteur d’OCR médical
  • Un message utilisateur contenant l’image et l’instruction d’OCR
  • Un message assistant avec la sortie OCR attendue
def build_ocr_messages(image=None, target_text=None, instruction=INSTRUCTION):
    user_content = [
        {"type": "image"},
        {"type": "text", "text": instruction},
    ]

    if image is not None:
        user_content[0]["image"] = image

    messages = [
        {"role": "system", "content": [{"type": "text", "text": SYSTEM_PROMPT}]},
        {"role": "user", "content": user_content},
    ]

    if target_text is not None:
        messages.append(
            {
                "role": "assistant",
                "content": [{"type": "text", "text": target_text}],
            }
        )

    return messages

La fonction utilitaire ci-dessus crée la structure de messages pour l’entraînement et l’inférence. En entraînement, nous incluons le texte OCR cible comme réponse de l’assistant. En inférence, nous fournissons seulement l’image et l’instruction, puis demandons au modèle de générer le texte OCR.

Nous convertissons ensuite chaque échantillon dans ce format de conversation :

def convert_to_conversation(sample):
    return {
        "messages": build_ocr_messages(
            image=sample["image"],
            target_text=sample["text"],
        )
    }


converted_dataset = [convert_to_conversation(sample) for sample in dataset]
converted_dataset[0]

Après conversion, chaque échantillon contient une liste de messages. Le premier exemple inclut l’invite système, l’image du document médical, l’instruction d’OCR et la transcription OCR structurée attendue. Ce format permet au modèle d’apprendre à relier une image et une instruction à la bonne sortie texte.

{'messages': [{'role': 'system',
   'content': [{'type': 'text',
     'text': 'You are a medical OCR transcription engine. Return only the exact text visible in the medical document image.'}]},
  {'role': 'user',
   'content': [{'type': 'image',
     'image': <PIL.Image.Image image mode=RGB size=768x1024>},
    {'type': 'text',
     'text': 'Extract all readable text from this medical document exactly. Preserve structure when possible. Return only the OCR text, with no explanation, no diagnosis, no medical advice, and no reasoning.'}]},
  {'role': 'assistant',
   'content': [{'type': 'text',
     'text': '<s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>'}]}]}

6. Évaluer le modèle de base avant l’affinage

Avant l’entraînement, testons le modèle de base sur un exemple d’OCR. Cela nous donne un point de comparaison avant/après l’affinage.

Nous définissons d’abord une fonction utilitaire pour appliquer le chat template du modèle. Certaines versions de tokenizer gèrent enable_thinking=False, d’autres non ; la fonction prévoit donc un repli pour rester compatible.

def render_ocr_chat_template(tokenizer, messages):
    try:
        return tokenizer.apply_chat_template(
            messages,
            tokenize=False,
            add_generation_prompt=True,
            enable_thinking=False,
        )
    except TypeError:
        return tokenizer.apply_chat_template(
            messages,
            tokenize=False,
            add_generation_prompt=True,
        )

Nous définissons ensuite la fonction de génération. Elle construit l’invite d’OCR, passe l’image et les instructions au tokenizer, génère la sortie du modèle et ne décode que les nouveaux jetons générés.

def generate_ocr_text(model, tokenizer, image, instruction=INSTRUCTION, max_new_tokens=512):
    messages = build_ocr_messages(instruction=instruction)
    input_text = render_ocr_chat_template(tokenizer, messages)
    inputs = tokenizer(
        images=image,
        text=input_text,
        add_special_tokens=False,
        return_tensors="pt",
    ).to(DEVICE)

    with torch.inference_mode():
        outputs = model.generate(
            **inputs,
            max_new_tokens=max_new_tokens,
            use_cache=True,
            do_sample=False,
            temperature=None,
            top_p=None,
        )

    prompt_length = inputs["input_ids"].shape[-1]
    generated_tokens = outputs[:, prompt_length:]
    return tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)[0]

Passons le modèle en mode inférence et générons le texte OCR pour la première image d’évaluation :

FastVisionModel.for_inference(model)
eval_image = dataset[EVAL_INDEX]["image"]
base_output = generate_ocr_text(model, tokenizer, eval_image)

print("Target:")
print(dataset[EVAL_INDEX]["text"])
print("\nBase model output:")
print(base_output)

La sortie du modèle de base est lisible, mais elle ne suit pas exactement la structure cible :

Target:
<s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>

Base model output:
Meadowview Health
45 Oak Ave.
Prescribed by: Dr. A. Smith
Date: 2024-12-16
Patient: John Doe, Age: 35
Hydrochlorothiazide 25 mg - Before meals
Signature: Dr. A. Smith

C’est un point de départ utile : le modèle de base lit déjà une grande partie du document, mais restitue un style OCR naturel plutôt que le format structuré du jeu de données. L’affinage doit aider à aligner la sortie sur le format cible et à améliorer la cohérence.

7. Entraîner le modèle

Le jeu de données étant au bon format de conversation vision, nous pouvons entraîner le modèle avec SFTTrainer de TRL et le collateur de données vision d’Unsloth.

from unsloth.trainer import UnslothVisionDataCollator
from trl import SFTTrainer, SFTConfig

FastVisionModel.for_training(model)

trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    data_collator=UnslothVisionDataCollator(model, tokenizer),
    train_dataset=converted_dataset,
    args=SFTConfig(
        per_device_train_batch_size=PER_DEVICE_BATCH_SIZE,
        gradient_accumulation_steps=GRADIENT_ACCUMULATION_STEPS,
        warmup_steps=5,
        max_steps=MAX_STEPS,
        learning_rate=LEARNING_RATE,
        logging_steps=1,
        optim="adamw_8bit",
        weight_decay=0.001,
        lr_scheduler_type="linear",
        seed=SEED,
        output_dir=OUTPUT_DIR,
        report_to="none",
        remove_unused_columns=False,
        dataset_text_field="",
        dataset_kwargs={"skip_prepare_dataset": True},
        max_length=MAX_LENGTH,
    ),
)
trainer_stats = trainer.train()

Nous passons d’abord le modèle en mode entraînement avec FastVisionModel.for_training(model). Puis nous créons le trainer avec le jeu de données OCR converti.

L’élément clé ici est le UnslothVisionDataCollator. Comme il s’agit d’une tâche vision-langage, le trainer doit gérer correctement à la fois les images de documents médicaux et le texte OCR cible. Le collateur prépare ces exemples multimodaux pour les transmettre au modèle pendant l’affinage supervisé.

Dans ce guide, nous entraînons 30 itérations avec une taille de lot par périphérique de 4 et une accumulation de gradients de 2, soit une taille de lot effective de 8. Cela reste léger tout en montrant comment le modèle commence à s’adapter au format OCR structuré.

Affinage du modèle vision-langage Qwen 3.5 4B

Pendant l’entraînement, Unsloth affiche des informations utiles : nombre d’exemples, d’itérations et de lots, nombre de paramètres entraînables, et fonctionnalités d’économie de mémoire. Dans cette exécution, Unsloth signale que le double buffering est activé pour la passe arrière, ce qui réduit les temps d’attente pendant le gradient checkpointing.

8. Évaluer le modèle affiné

Après l’entraînement, nous repassons le modèle en mode inférence et générons le texte OCR pour la même image d’évaluation qu’avant l’affinage.

FastVisionModel.for_inference(model)
fine_tuned_output = generate_ocr_text(model, tokenizer, eval_image)

print("Target:")
print(dataset[EVAL_INDEX]["text"])
print("\nBase model output:")
print(base_output)
print("\nFine-tuned output:")
print(fine_tuned_output)

Après affinage, la sortie du modèle se rapproche nettement de la structure cible du jeu de données :

Target:
<s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>

Base model output:
Meadowview Health
45 Oak Ave.
Prescribed by: Dr. A. Smith
Date: 2024-12-16
Patient: John Doe, Age: 35
Hydrochlorothiazide 25 mg - Before meals
Signature: Dr. A. Smith


Fine-tuned output:
<s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>

Le modèle affiné a appris le format de réponse OCR attendu. Le modèle de base extrayait déjà l’essentiel, mais l’affinage a aligné la sortie sur le format structuré utilisé en entraînement.

Nous pouvons aussi tester le modèle sur un autre exemple du jeu de données :

EVAL_INDEX_2 = 35
eval_image_2 = dataset[EVAL_INDEX_2]["image"]

fine_tuned_output = generate_ocr_text(model, tokenizer, eval_image_2)

print("Target:")
print(dataset[EVAL_INDEX]["text"])
print("\nFine-tuned output:")
print(fine_tuned_output)

Sur ce second exemple, le modèle respecte la structure attendue, mais commet une petite erreur d’OCR en générant Amoxicillin au lieu de Amlodipine :

Target:
<s_ocr> doctor_name: Dr. C. Rossi clinic_name: Riverside Clinic clinic_address: 45 Oak Ave. patient_name: Wei Li patient_age: 70 date: 2024-12-16 medications: - Acetaminophen 20 mg - Take twice daily - Amlodipine 20 mg - After meals signature: Dr. C. Rossi </s>

Fine-tuned output:
<s_ocr> doctor_name: Dr. C. Rossi clinic_name: Riverside Clinic clinic_address: 45 Oak Ave. patient_name: Wei Li patient_age: 70 date: 2024-12-16 medications: - Acetaminophen 20 mg - Take twice daily - Amoxicillin 20 mg - After meals signature: Dr. C. Rossi </s>

C’est un rappel utile : le modèle progresse sur l’alignement de format, mais la justesse OCR dépend encore de la qualité des données, de la clarté des images, de la taille d’entraînement et du nombre d’étapes. En production, il faut entraîner sur un jeu plus large et diversifié et évaluer précisément sur un large éventail d’exemples.

9. Enregistrer l’adaptateur affiné

Une fois l’entraînement terminé, nous enregistrons localement l’adaptateur LoRA et le tokenizer.

model.save_pretrained(ADAPTER_DIR)
tokenizer.save_pretrained(ADAPTER_DIR)

print("Saved adapter to:", ADAPTER_DIR)

La sortie confirme l’enregistrement de l’adaptateur :

Saved adapter to: qwen35-vision-medical-ocr-lora

Seuls les poids de l’adaptateur affiné sont sauvegardés, pas une copie complète du modèle de base. Vous pourrez ensuite recharger le modèle Qwen3.5-4B de base et appliquer cet adaptateur pour réutiliser le comportement OCR affiné. Le modèle sauvegardé reste donc léger, plus simple à stocker, partager ou déployer.

En conclusion

Le processus d’entraînement a été léger et pratique sur une seule NVIDIA RTX 3090. Bien que l’affinage vision-langage soit généralement gourmand en mémoire, la session a utilisé bien moins de VRAM qu’attendu : un maximum autour de 14 Go, avec une moyenne proche de 9 Go — impressionnant pour affiner un modèle Qwen3.5 Vision.

Le modèle s’est aussi adapté rapidement. Après quelques itérations seulement, la sortie s’est nettement rapprochée de la structure OCR cible. Le modèle de base lisait déjà le document, mais l’affinage a rendu le formatage bien plus cohérent.

Cela dit, la mise en place n’a pas été parfaite. L’installation d’Unsloth a demandé de nombreux essais. La configuration peut s’avérer délicate, surtout en jonglant entre environnements locaux, environnements virtuels, versions de CUDA et fournisseurs de GPU cloud.

Dans certains cas, des problèmes de compatibilité CUDA peuvent casser l’environnement, et le débogage prend plus de temps que prévu. Même en partant d’une image Docker Unsloth sur un GPU cloud, l’amorcage peut être chronophage si l’environnement ne fonctionne pas parfaitement dès le départ.

Autre enseignement clé : le template du modèle compte. Si le jeu de données n’est pas converti au bon format de conversation (chat ou vision), le modèle peut ne pas apprendre correctement. Pour Qwen3.5 Vision, utiliser la bonne structure message image‑texte est essentiel. Sans le bon template, l’entraînement peut tourner, mais le modèle ne s’adaptera pas vraiment à la tâche.

Globalement, Unsloth est une option solide pour les utilisateurs avec un accès GPU limité souhaitant affiner efficacement des modèles sur des machines locales ou des GPU loués. Il réduit l’usage mémoire, valorise un matériel plus modeste et peut accélérer l’expérimentation. Toutefois, pour celles et ceux qui affinent et entraînent régulièrement, la complexité de mise en place peut être frustrante. Les entraînements basés sur Transformers sont souvent plus stables, plus simples à installer et plus faciles à reproduire entre environnements.

Si la friction d’installation vous décourage, nous vous recommandons de lire notre guide d’Unsloth Studio, qui montre comment affiner Qwen3.5-9B sans configuration manuelle de l’environnement dans l’interface web locale d’Unsloth.


Abid Ali Awan's photo
Author
Abid Ali Awan
LinkedIn
Twitter

En tant que data scientist certifié, je suis passionné par l'utilisation des technologies de pointe pour créer des applications innovantes d'apprentissage automatique. Avec une solide expérience en reconnaissance vocale, en analyse de données et en reporting, en MLOps, en IA conversationnelle et en NLP, j'ai affiné mes compétences dans le développement de systèmes intelligents qui peuvent avoir un impact réel. En plus de mon expertise technique, je suis également un communicateur compétent, doué pour distiller des concepts complexes dans un langage clair et concis. En conséquence, je suis devenu un blogueur recherché dans le domaine de la science des données, partageant mes idées et mes expériences avec une communauté grandissante de professionnels des données. Actuellement, je me concentre sur la création et l'édition de contenu, en travaillant avec de grands modèles linguistiques pour développer un contenu puissant et attrayant qui peut aider les entreprises et les particuliers à tirer le meilleur parti de leurs données.

Sujets

Meilleures formations en IA

Cours

Introduction au Deep Learning avec PyTorch

4 h
88.5K
Apprenez à créer votre réseau neuronal, ajuster les hyperparamètres et résoudre les problèmes de classification et régression avec PyTorch.
Afficher les détailsRight Arrow
Commencer Le Cours
Voir plusRight Arrow
Contenus associés

blog

Comprendre les TPU et les GPU dans l'IA : Un guide complet

L'essor du développement de l'intelligence artificielle (IA) a entraîné une augmentation notable de la demande en matière de calcul, d'où la nécessité de disposer de solutions matérielles robustes. Les unités de traitement graphique (GPU) et les unités de traitement tensoriel (TPU) sont devenues des technologies essentielles pour répondre à ces demandes.
Kurtis Pykes 's photo

Kurtis Pykes

9 min

blog

Les 20 meilleures questions d'entretien pour les flocons de neige, à tous les niveaux

Vous êtes actuellement à la recherche d'un emploi qui utilise Snowflake ? Préparez-vous à répondre à ces 20 questions d'entretien sur le flocon de neige pour décrocher le poste !
Nisha Arya Ahmed's photo

Nisha Arya Ahmed

15 min

blog

ROI de l'IA en 2026 : pourquoi les compétences des équipes déterminent le retour sur investissement

Seuls 21 % des dirigeants font état d'un retour sur investissement « significatif » de leurs investissements dans l'IA.
Lynn Heidmann's photo

Lynn Heidmann

blog

Célébration de Saghar Hazinyar : Une boursière de DataCamp Donates et une diplômée de Code to Inspire

Découvrez le parcours inspirant de Saghar Hazinyar, diplômée de Code to Inspire, qui a surmonté les défis en Afghanistan et s'est épanouie grâce à une bourse de DataCamp Donates.
Fereshteh Forough's photo

Fereshteh Forough

4 min

cursor ai code editor

Tutoriel

Cursor AI : Un guide avec 10 exemples pratiques

Apprenez à installer Cursor AI sur Windows, macOS et Linux, et découvrez comment l'utiliser à travers 10 cas d'utilisation différents.

Tutoriel

Cache Python : Deux méthodes simples

Apprenez à utiliser des décorateurs tels que @functools.lru_cache ou @functools.cache pour mettre en cache des fonctions en Python.
Stephen Gruppetta's photo

Stephen Gruppetta

Voir PlusVoir Plus