Vai al contenuto principale

Fine-tuning di Ministral 3: guida passo passo all’addestramento di un modello visione-linguaggio per la classificazione delle radiografie

Scopri come effettuare il fine-tuning del modello visione-linguaggio Ministral 3 per la classificazione di immagini a raggi X usando LoRA, strumenti Hugging Face, prompt personalizzati e TRL.
· 10 min leggi

Scopri con l'IA

Ministral 3 è una nuova famiglia di modelli multimodali ottimizzati per l’edge, disponibili nelle dimensioni 3B, 8B e 14B, ciascuna con varianti Base, Instruct e Reasoning e pieno supporto alla visione. 

Progettato per offrire prestazioni di frontiera restando al contempo abbastanza leggero per il deployment locale e privato, Ministral 3 unisce capacità avanzate a vincoli hardware pratici: il suo modello di punta 14B Instruct entra in 24 GB di VRAM in FP8 e rivaleggia con sistemi più grandi come Mistral Small 3.2 24B. Dispone di un ampio contesto da 256k, solida copertura multilingue, function calling nativo, forte aderenza al system prompt e licenza open Apache 2.0. 

In questo tutorial, effettueremo il fine-tuning del modello Ministral 3 14B Instruct su un dataset di radiografie di fratture ossee per migliorarne la capacità di rilevare le fratture e classificarne correttamente le categorie. 

Imparerai a:

  1. Caricare il dataset da Kaggle.
  2. Convertirlo in un dataset Hugging Face.
  3. Creare un prompt di training chiaro per la classificazione delle radiografie.
  4. Impostare la pipeline di training visione-linguaggio di Ministral 3 14B.
  5. Effettuare il fine-tuning del modello e salvare il checkpoint.
  6. Caricare l’adapter del modello addestrato per l’inferenza.
  7. Confrontare i risultati prima e dopo il fine-tuning.

Se vuoi saperne di più sui modelli di Mistral AI, ti consiglio la nostra guida Mistral 3 e il tutorial Mistral 3 Large. 

Disclaimer: questo tutorial illustra un processo tecnico di fine-tuning a solo scopo didattico. Il modello risultante non è destinato all’uso medico o clinico e non deve essere utilizzato per diagnosi, decisioni o assistenza ai pazienti.

1. Configurazione dell’ambiente per il fine-tuning visione-linguaggio di Ministral 3

Anche se stiamo effettuando il fine-tuning del modello 14B, l’addestramento visione-linguaggio richiede molta più memoria rispetto al solo testo. L’encoder di immagini aumenta il carico sulla GPU, quindi per eseguire il processo in modo fluido serve una GPU A100 con 80 GB di VRAM. 

Per questo progetto utilizzo Runpod e avvio un pod A100.

RunPod. A100 Pod.

Dopo l’avvio del pod, aumenta lo storage del container a 50 GB. Poi aggiungi le variabili d’ambiente per le API key di Kaggle e Hugging Face. 

Kaggle ci permetterà di scaricare direttamente il dataset di radiografie ossee, mentre Hugging Face ci consentirà di caricare l’adapter LoRA fine-tunato sull’hub dei modelli.

Impostazione delle variabili d’ambiente in Runpod

Apri quindi l’istanza di JupyterLab e crea un nuovo notebook. Prima del training, dobbiamo installare tutti i pacchetti Python necessari. 

Assicurati che la libreria Transformers sia aggiornata all’ultima versione, perché il supporto a Ministral 3 è stato aggiunto di recente e le build precedenti non caricheranno correttamente il modello.

! pip install --upgrade --quiet bitsandbytes datasets evaluate hf_transfer peft scikit-learn matplotlib Pillow kaggle ipywidgets
!pip install --quiet git+https://github.com/huggingface/transformers.git@bf3f0ae70d0e902efab4b8517fce88f6697636ce
!pip install --quiet --no-deps trl==0.22.2

Una volta installato tutto, sei pronto per caricare il dataset, preparare il collator e iniziare il fine-tuning.

2. Download del dataset di fratture ossee da Kaggle

Nel prossimo passaggio, impostiamo una cartella radice per il nostro dataset e usiamo la Python API di Kaggle per scaricare il dataset “Bone Break Classification Image”. 

Questo dataset contiene varie categorie di fratture ossee in formato radiografico e sarà usato per il fine-tuning di Ministral 3 per la classificazione visiva.

import os
import glob
from kaggle.api.kaggle_api_extended import KaggleApi

DATA_ROOT = "/bone-break"
os.makedirs(DATA_ROOT, exist_ok=True)

kaggle_dataset = "pkdarabi/bone-break-classification-image-dataset"

# Initialize and authenticate Kaggle API (it will read KAGGLE_USERNAME / KAGGLE_KEY)
api = KaggleApi()
api.authenticate()

# Download and unzip the dataset using the Python API
api.dataset_download_files(
    kaggle_dataset,
    path=DATA_ROOT,
    unzip=True,
)

Puoi visualizzare il dataset su Kaggle qui:

Dataset URL: https://www.kaggle.com/datasets/pkdarabi/bone-break-classification-image-dataset

3. Creazione di un dataset Hugging Face

Il dataset scaricato non è organizzato nella struttura abituale. Ogni categoria di frattura contiene al suo interno sia una cartella di training sia una di test. 

Quindi, per prima cosa, ci serve un metodo per iterare su ogni cartella di classe, separare i due split, e poi creare un dataset Hugging Face combinato con una mappatura pulita di train e test.

import glob, os
from typing import Any, Dict, List

from datasets import Dataset, DatasetDict, Features, ClassLabel, Image

inner_root = os.path.join(
    DATA_ROOT, "Bone Break Classification", "Bone Break Classification"
)

classes = []
train_records: List[Dict[str, Any]] = []
test_records: List[Dict[str, Any]] = []

for class_name in sorted(os.listdir(inner_root)):
    class_dir = os.path.join(inner_root, class_name)
    if not os.path.isdir(class_dir):
        continue

    classes.append(class_name)

    for split_name, split_key in [("Train", "train"), ("Test", "test")]:
        split_dir = os.path.join(class_dir, split_name)
        if not os.path.isdir(split_dir):
            continue

        for ext in ("*.png", "*.jpg", "*.jpeg"):
            for img_path in glob.glob(os.path.join(split_dir, ext)):
                rec = {"image": img_path, "label_name": class_name}
                if split_key == "train":
                    train_records.append(rec)
                else:
                    test_records.append(rec)

print("Classes:", classes)
print("Train samples:", len(train_records))
print("Test samples:", len(test_records))

Abbiamo un totale di 10 classificazioni, con 989 campioni di training e 140 di test.

Classes: ['Avulsion fracture', 'Comminuted fracture', 'Fracture Dislocation', 'Greenstick fracture', 'Hairline Fracture', 'Impacted fracture', 'Longitudinal fracture', 'Oblique fracture', 'Pathological fracture', 'Spiral Fracture']
Train samples: 989
Test samples: 140

Ora codificheremo le etichette per il dataset Hugging Face in modo che ogni classe sia trattata come label di classe e ogni immagine come caratteristica immagine. 

Creeremo due dataset: uno per il training e uno per il test. Infine li combineremo in un unico dataset che contiene entrambi i sottoinsiemi.

# Encode labels
classes = sorted(list(set(classes)))
label2id = {name: i for i, name in enumerate(classes)}

for rec in train_records:
    rec["label"] = label2id[rec["label_name"]]
for rec in test_records:
    rec["label"] = label2id[rec["label_name"]]

features = Features(
    {
        "image": Image(),
        "label": ClassLabel(names=classes),
    }
)

train_ds = Dataset.from_list(
    [{"image": r["image"], "label": r["label"]} for r in train_records],
    features=features,
)
test_ds = Dataset.from_list(
    [{"image": r["image"], "label": r["label"]} for r in test_records],
    features=features,
)

data = DatasetDict({"train": train_ds, "test": test_ds})
data

Ecco una panoramica del dataset. 

DatasetDict({
    train: Dataset({
        features: ['image', 'label'],
        num_rows: 989
    })
    test: Dataset({
        features: ['image', 'label'],
        num_rows: 140
    })
})

4. Esplorazione della distribuzione delle classi e di esempi di radiografie

Prima del fine-tuning, è utile capire quanto il dataset sia bilanciato e che aspetto abbiano le immagini. Inizieremo verificando quanti campioni di training esistono per ciascuna categoria di frattura.

from collections import Counter

label_names = data["train"].features["label"].names
label_ids = data["train"]["label"]

counts = Counter(label_ids)
print("Training Class counts:")
for lbl_id, cnt in counts.items():
    print(f"{lbl_id:2d} ({label_names[lbl_id]}): {cnt}")

FRACTURE_CLASSES = label_names

Le categorie sono piuttosto bilanciate, anche se la dimensione complessiva del campione è ridotta per un compito complesso di classificazione visiva. 

Per ora procederemo con questo dataset, ma i risultati andrebbero interpretati con aspettative realistiche a causa del volume limitato di campioni.

Training Class counts:
 0 (Avulsion fracture): 109
 1 (Comminuted fracture): 134
 2 (Fracture Dislocation): 137
 3 (Greenstick fracture): 106
 4 (Hairline Fracture): 101
 5 (Impacted fracture): 75
 6 (Longitudinal fracture): 68
 7 (Oblique fracture): 69
 8 (Pathological fracture): 116
 9 (Spiral Fracture): 74

Successivamente, osserviamo alcune immagini casuali per comprendere meglio il dataset dal punto di vista visivo.

import matplotlib.pyplot as plt
from datasets import Image

# Cast the 'image' column to Hugging Face Image feature
dataset_images = data.cast_column("image", Image())

# Select random samples from the training set
samples = dataset_images["train"].shuffle(seed=420).select(range(4))

# Plot
fig, axes = plt.subplots(1, 4, figsize=(16, 5))

for i, sample in enumerate(samples):
    img_object = sample["image"]              # PIL Image
    label_id = sample["label"]               # int
    label_name = label_names[label_id]       # string

    axes[i].imshow(img_object)
    axes[i].set_title(f"Label: {label_name}")
    axes[i].axis("off")

plt.tight_layout()
plt.show()

Guardando le immagini di esempio, si notano differenze in dimensione, risoluzione, orientamento e qualità della scansione. Questo non impedisce il fine-tuning di un modello visione-linguaggio, ma può influenzare la coerenza con cui il modello apprende ciascuna categoria di frattura. 

Rilevare visivamente una frattura è spesso immediato, ma classificarla in uno dei dieci tipi medici sottilmente differenti richiede dati più strutturati rispetto a quelli forniti qui.

4 campioni casuali dal dataset di fratture ossee

5. Progettazione del prompt per la classificazione delle fratture ossee

Dobbiamo ora decidere come vogliamo che il modello si comporti. Per questo compito, l’obiettivo è molto rigido: data un’immagine a raggi X, il modello deve restituire solo il nome di una classe di frattura e nient’altro. Niente spiegazioni, niente markdown, nessun testo extra.

Scriveremo un prompt di istruzioni chiaro e poi lo useremo per costruire una colonna messages nel dataset. 

PROMPT = f"""
You are a radiology assistant specialised in bone fracture classification.

You must classify the X-ray image into exactly ONE of the following classes:
{", ".join(FRACTURE_CLASSES)}

Rules:
- Reply with ONLY the class name.
- No explanations, no extra words, no markdown, no punctuation.
- Output must be EXACTLY one of: {", ".join(FRACTURE_CLASSES)}
""".strip()


from typing import Dict, Any

def convert_to_conversation(example: Dict[str, Any]) -> Dict[str, Any]:
    conversation = [
        {
            "role": "user",
            "content": [
                {"type": "text",  "text": PROMPT},
                {"type": "image"},     # no image bytes here
            ],
        },
        {
            "role": "assistant",
            "content": [
                {"type": "text", "text": FRACTURE_CLASSES[example["label"]]},
            ],
        },
    ]
    example["messages"] = conversation
    return example




formatted_data = data["train"].map(convert_to_conversation)
formatted_data["messages"][0]

Questo crea una colonna messages in cui ogni record contiene una mini conversazione completa: l’istruzione e la classe di frattura corretta. È esattamente ciò che il modello visione-linguaggio vedrà durante il fine-tuning.

Dataset in stile conversazionale.

6. Caricamento del modello visione-linguaggio Ministral 3

Ora carichiamo la versione BF16 del modello Ministral-3 14B Instruct dall’Hugging Face Hub, insieme al processor. Il processor raggruppa il tokenizer e la pipeline di pre-processing delle immagini, necessaria per gli input visione-linguaggio.

import torch
from transformers import AutoProcessor, AutoModelForImageTextToText, BitsAndBytesConfig

model_id = "mistralai/Ministral-3-14B-Instruct-2512-BF16"


model_kwargs = dict(
    attn_implementation="eager",
    torch_dtype=torch.bfloat16,
    device_map="auto",
)

model = AutoModelForImageTextToText.from_pretrained(model_id, **model_kwargs)
processor = AutoProcessor.from_pretrained(model_id)

# Use right padding to avoid issues during training
processor.tokenizer.padding_side = "right"

7. Impostazione di un template chat personalizzato

Il template chat originale fornito da Mistral AI è pensato per casi d’uso conversazionali lunghi. Per questo progetto, è diventato un problema. Il modello ha iniziato a concentrarsi sulle lunghe istruzioni di ruolo e sul testo di sistema, ignorando l’obiettivo reale: prevedere una singola classe di frattura dall’immagine a raggi X.

Per risolvere, definiamo un template visione-linguaggio più corto che mantenga solo le parti importanti: l’istruzione dell’utente e la risposta dell’assistente. Questo riduce il rumore e aiuta il modello a imparare una mappatura pulita da immagine a etichetta di classe.

SHORT_VLM_TEMPLATE = r"""{{ bos_token }}[INST]
{% for message in messages if message['role'] == 'user' -%}
  {%- for content in message['content'] -%}
    {%- if content['type'] == 'text' -%}
{{ content['text'] }}

    {%- elif content['type'] == 'image' -%}
[IMG]
    {%- endif -%}
  {%- endfor -%}
{%- endfor -%}
[/INST]{% if not add_generation_prompt %}
  {%- for message in messages if message['role'] == 'assistant' -%}
{{ message['content'][0]['text'] }}{{ eos_token }}
  {%- endfor -%}
{%- endif %}"""


# Attach to processor / tokenizer
processor.chat_template = SHORT_VLM_TEMPLATE
processor.tokenizer.chat_template = SHORT_VLM_TEMPLATE

8. Test di Ministral 3 (pre-fine-tuning)

Prima di iniziare il fine-tuning, è utile testare il modello Ministral 3 14B Instruct originale per creare una baseline. 

Per prima cosa, forniamo al modello un’immagine casuale dal dataset e poniamo una domanda generale, giusto per vedere come comprende il contenuto della radiografia.

image = formatted_data[200]["image"]

messages = [
    {"role": "user", "content": [
        {"type": "text", "text": "What is the image about?"},
        {"type": "image"},
        
    ]}
]

input_text = processor.apply_chat_template(messages, add_generation_prompt = True)
inputs = processor(
    image,
    input_text,
    add_special_tokens = False,
    return_tensors = "pt",
).to(device="cuda", dtype=torch.bfloat16)

from transformers import TextStreamer
text_streamer = TextStreamer(processor, skip_prompt = True)
_ = model.generate(**inputs, streamer = text_streamer, max_new_tokens = 1000,
                   use_cache = True, temperature = 1.5, min_p = 0.1)

Il modello ha compreso il contenuto generale della radiografia, ma poi è scivolato in descrizioni non affidabili per uso medico.

Poi proviamo qualcosa di più vicino al nostro obiettivo finale. Forniamo la stessa immagine, ma questa volta usiamo il nostro prompt rigoroso per la classificazione delle fratture invece di una domanda aperta.

image = formatted_data[200]["image"]

messages = [
    {"role": "user", "content": [
        {"type": "text", "text": PROMPT},
        {"type": "image"},
        
    ]}
]

input_text = processor.apply_chat_template(messages, add_generation_prompt = True)
inputs = processor(
    image,
    input_text,
    add_special_tokens = False,
    return_tensors = "pt",
).to(device="cuda", dtype=torch.bfloat16)

from transformers import TextStreamer
text_streamer = TextStreamer(processor, skip_prompt = True)
_ = model.generate(**inputs, streamer = text_streamer, max_new_tokens = 1000,
                   use_cache = True, temperature = 1.5, min_p = 0.1

Per questo campione, il modello ha risposto con:

Hairline Fracture</s>

Ora controlliamo la ground truth per la stessa immagine.

FRACTURE_CLASSES[formatted_data[200]["label"]]

Questo restituisce:

'Comminuted fracture'

Quindi, il modello ha seguito correttamente il formato dell’istruzione e prodotto un singolo nome di classe, ma la classificazione era errata. 

Ed è proprio per questo che serve il fine-tuning. Il modello base ha una forte comprensione visiva, ma non è stato addestrato specificamente a distinguere tra i dettagliati tipi di fratture ossee. 

Nei prossimi passaggi, lo fine-tuneremo sul nostro dataset di fratture per migliorare questo comportamento.

9. Configurazione di LoRA per un fine-tuning efficiente di Ministral 3

Ora impostiamo LoRA per effettuare il fine-tuning di Ministral 3 in modo efficiente senza aggiornare tutti i parametri. Questa configurazione prende di mira i principali strati di attenzione e proiezione MLP usati in un modello visione-linguaggio in stile Mistral.

from peft import LoraConfig, TaskType, get_peft_model

# LoRA config for a Mistral-style VLM used as a causal LM
peft_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    r=16,
    lora_alpha=32,
    lora_dropout=0.05,
    bias="none",
    target_modules=[
        "q_proj",
        "k_proj",
        "v_proj",
        "o_proj",
        "gate_proj",
        "up_proj",
        "down_proj",
    ],
    modules_to_save=None,
)

Poi definiamo gli argomenti di training per il supervised fine-tuning (SFT).

Questa configurazione controlla l’intero processo di training, inclusa la gestione dei batch, l’ottimizzazione della memoria, il comportamento del learning rate, il logging, la strategia di salvataggio e l’integrazione con l’Hugging Face Hub. 

from trl import SFTConfig

args = SFTConfig(
    output_dir="ministral-3-bone-fracture",            
    num_train_epochs=1,                       
    per_device_train_batch_size=2,                           
    per_device_eval_batch_size=2,                            
    gradient_accumulation_steps=4,                           
    gradient_checkpointing=True,                             
    optim="adamw_8bit",                               
    logging_steps=0.1,                                        
    save_strategy="epoch",                                                                                                             
    learning_rate=2e-4,                             
    bf16=True,
    fp16=False,
    max_grad_norm=0.3,                                       
    warmup_steps=25,                                       
    lr_scheduler_type="linear",                              
    push_to_hub=True,                                        
    report_to="none",
    gradient_checkpointing_kwargs={"use_reentrant": False},  
    dataset_kwargs={"skip_prepare_dataset": True},           
    remove_unused_columns = False,                           
    label_names=["labels"],                                  
)

10. Creazione di un data collator visione-linguaggio per il training

Per il fine-tuning di un modello visione-linguaggio come Ministral 3, ci serve un data collator personalizzato che possa raggruppare immagini e testo in stile chat in un formato coerente. 

Il collator seguente si occupa di: configurare tokenizer e token di padding, applicare il template chat, tokenizzare congiuntamente testo e immagini, imporre una lunghezza massima di sequenza sicura e creare le label in cui padding e token immagine sono mascherati (-100) così da non influenzare la loss. 

from typing import Any, Dict, List
import torch

# --- One-time processor setup ---
tokenizer = processor.tokenizer
tokenizer.padding_side = "right"

# Ensure we have a pad token
if tokenizer.pad_token is None:
    tokenizer.add_special_tokens({"pad_token": tokenizer.eos_token})

pad_token_id = tokenizer.pad_token_id
image_token_id = getattr(processor, "image_token_id", None)

# Hard cap for sequence length
MAX_SEQ_LEN = 2048 


def collate_fn(examples: List[Dict[str, Any]]) -> Dict[str, torch.Tensor]:
    """
    Collate function for VLM fine-tuning.

    Each example must contain:
      - "image": a PIL image
      - "messages": chat-style conversation (list[dict])
    """
    images = [ex["image"] for ex in examples]
    conversations = [ex["messages"] for ex in examples]

    # Build chat prompts for the whole batch
    chat_texts = processor.apply_chat_template(
        conversations,
        add_generation_prompt=False,
        tokenize=False,
    )

    # Tokenize + process images
    batch = processor(
        text=chat_texts,
        images=images,
        padding="longest", 
        truncation=True,
        max_length=MAX_SEQ_LEN,
        return_tensors="pt",
    )

    # Labels: same as input_ids but mask pad & image tokens
    labels = batch["input_ids"].clone()

    if pad_token_id is not None:
        labels[labels == pad_token_id] = -100

    if image_token_id is not None and image_token_id != pad_token_id:
        labels[labels == image_token_id] = -100

    batch["labels"] = labels
    return batch

11. Fine-tuning di Ministral 3 con TRL

Con l’adapter LoRA, la configurazione di training e il data collator pronti, possiamo ora collegare tutto all’SFTTrainer di trl. 

Questo trainer gestisce l’intero ciclo di supervised fine-tuning per il nostro modello visione-linguaggio Ministral 3, usando il dataset formattato, la config LoRA e la funzione di collate personalizzata per batch immagine–testo.

from trl import SFTTrainer

trainer = SFTTrainer(
    model=model,
    args=args,
    train_dataset=formatted_data,
    peft_config=peft_config,
    processing_class=processor,
    data_collator=collate_fn,
)

Esegui il codice seguente per avviare il processo di training. 

trainer.train()

Durante la mia esecuzione, il fine-tuning ha utilizzato circa 57 GB di VRAM, e un po’ di più durante gli step di valutazione che avvengono ogni poche iterazioni. In pratica, ciò significa che puoi effettuare il fine-tuning comodamente su hardware con meno di 70 GB di VRAM.

Telemetria di Runpod durante il fine-tuning del modello Ministral 3 14B.

L’intero training è durato circa mezz’ora e la training loss è diminuita costantemente nel tempo, un buon segnale che il modello sta imparando dal dataset.

Andamento della training loss durante il fine-tuning del modello Ministral 3 14B

Una volta completato l’addestramento, salviamo il modello adattato su Hugging Face Hub. Questo creerà un nuovo repository e invierà tutti i file necessari affinché tu possa ricaricare ed eseguire il modello fine-tunato da qualsiasi ambiente.

trainer.save_model()

Puoi vedere il modello su kingabzpro/ministral-3-bone-fracture. 

Modello fine-tunato salvato su Hugging Face Hub.

12. Valutazione del modello Ministral 3 fine-tunato

Ora carichiamo il modello e il processor fine-tunati da Hugging Face Hub. Dietro le quinte, verrà scaricato l’adapter LoRA salvato, caricato il checkpoint base di Ministral 3 e unito l’adapter al modello base così da essere pronto per l’inferenza.

import torch
from transformers import AutoProcessor, AutoModelForImageTextToText, BitsAndBytesConfig

model_id = "kingabzpro/ministral-3-bone-fracture"


model_kwargs = dict(
    attn_implementation="eager",
    dtype=torch.bfloat16,
    device_map="auto",
)

model = AutoModelForImageTextToText.from_pretrained(model_id, **model_kwargs)
processor = AutoProcessor.from_pretrained(model_id)

processor.tokenizer.padding_side = "left"

Per mantenere il confronto equo, riutilizziamo prima la stessa immagine testata prima del fine-tuning (indice 200) e la passiamo nella pipeline di inferenza con il nostro prompt di classificazione rigoroso.

image = formatted_data[200]["image"]

messages = [
    {"role": "user", "content": [
        {"type": "text", "text": PROMPT},
        {"type": "image"},
        
    ]}
]

input_text = processor.apply_chat_template(messages, add_generation_prompt = True)
inputs = processor(
    image,
    input_text,
    add_special_tokens = False,
    return_tensors = "pt",
).to(device="cuda", dtype=torch.bfloat16)

from transformers import TextStreamer
text_streamer = TextStreamer(processor, skip_prompt = True)
_ = model.generate(**inputs, streamer = text_streamer, max_new_tokens = 100)

Abbiamo ottenuto un risultato perfetto. Non è servito alcun post-processing aggiuntivo. Il modello ha seguito le regole del prompt e ha prodotto direttamente la classe corretta.

Comminuted fracture</s>

Poi proviamo un altro campione casuale dal dataset per confermare che questo comportamento non sia limitato a un solo esempio.

image = formatted_data[400]["image"]

messages = [
    {"role": "user", "content": [
        {"type": "text", "text": PROMPT},
        {"type": "image"},
        
    ]}
]

input_text = processor.apply_chat_template(messages, add_generation_prompt = True)
inputs = processor(
    image,
    input_text,
    add_special_tokens = False,
    return_tensors = "pt",
).to(device="cuda", dtype=torch.bfloat16)

from transformers import TextStreamer
text_streamer = TextStreamer(processor, skip_prompt = True)
_ = model.generate(**inputs, streamer = text_streamer, max_new_tokens = 100)

Output del modello:

Greenstick fracture</s>

Ground truth:

FRACTURE_CLASSES[formatted_data[400]["label"]]

Output:

'Greenstick fracture'

Anche in questo caso, la previsione è corretta e perfettamente allineata con l’etichetta.

Con queste verifiche, possiamo dire che il nostro modello Ministral 3 14B è stato fine-tunato con successo sul dataset di radiografie di fratture ossee ed è ora in grado di seguire il prompt di istruzioni e produrre previsioni accurate a singola classe per questi esempi.

Considerazioni finali

Il fine-tuning del modello Ministral 3 richiede pazienza e aggiustamenti graduali. Mi ci sono voluti davvero due giorni pieni per capire perché il modello non si addestrasse come mi aspettavo e perché, pur essendo relativamente piccolo, continuasse a mandare la GPU in out-of-memory. 

La ragione principale era semplice, ma facile da trascurare. Non è solo un modello linguistico. È un modello visione-linguaggio, il che significa che incorpora un encoder di immagini che aggiunge ulteriore utilizzo di memoria. 

Risoluzioni d’immagine più grandi portano direttamente a un maggiore consumo di VRAM. Ridurre la dimensione delle immagini o iniziare con batch più piccoli rende il processo di training molto più stabile.

Un altro problema che ho incontrato è stato l’uso del template chat predefinito. Era troppo lungo e strutturato per questo tipo di compito. Il modello impiegava più sforzi a seguire la formattazione del template invece di imparare a leggere le immagini e produrre una classificazione chiara della frattura. 

Una volta semplificato il prompt in un formato di istruzione diretta, il modello ha finalmente iniziato a rispondere correttamente all’input visivo invece di modellare ogni risposta come una conversazione completa.

Davo anche per scontato che, poiché il modello ha forti capacità visive, si sarebbe comportato naturalmente bene sulle scansioni mediche. Questa supposizione mi ha rallentato. Chiamarlo “MedGemma” nella mia testa non è stato corretto né per il modello né per il processo. Non è un modello medico specializzato out-of-the-box, e aspettarmi che si comportasse come tale senza un adeguato training è stato un mio errore.

Alla fine, ho superato ogni ostacolo e ho trasformato ciò che ho imparato in questa guida. L’obiettivo è aiutarti a evitare la stessa confusione e arrivare più rapidamente a risultati significativi. 

“Con immagini più piccole, prompt più semplici e aspettative realistiche, Ministral 3 può essere fine-tunato per la comprensione delle radiografie in modo molto più fluido e affidabile.”

Se vuoi fare più pratica pratica con il fine-tuning, ti consiglio il corso Fine-tuning con Llama 3.

Abid Ali Awan

Autore

Abid Ali Awan

In quanto data scientist certificato, sono appassionato di sfruttare tecnologie all’avanguardia per creare applicazioni di machine learning innovative. Con una solida esperienza in riconoscimento vocale, analisi e reportistica dei dati, MLOps, AI conversazionale e NLP, ho affinato le mie competenze nello sviluppo di sistemi intelligenti in grado di avere un impatto concreto. Oltre alla mia expertise tecnica, sono anche un comunicatore efficace, con il talento di rendere chiari e sintetici concetti complessi. Di conseguenza, sono diventato un blogger molto seguito in ambito data science, condividendo idee ed esperienze con una community in crescita di professionisti dei dati. Attualmente mi concentro sulla creazione e sull’editing di contenuti, lavorando con large language model per sviluppare contenuti potenti e coinvolgenti che possano aiutare aziende e singoli a valorizzare al meglio i propri dati.

Argomenti

Correlato