Fine-tuning di Qwen3-VL 8B: guida passo dopo passo
Effettua il fine-tuning di Qwen3-VL-8B su schemi elettronici con LoRA. Impara l’intera pipeline, dalla preparazione dei dati alla pubblicazione del modello su Hugging Face Hub.
Qwen3‑VL‑8B è il modello visione‑linguaggio più potente e più usato della serie Qwen, progettato per una comprensione unificata di testo, immagini e video.
Offre miglioramenti significativi nella generazione di testo, nel ragionamento visivo, nella percezione spaziale, nella gestione di contesti lunghi e nell’interazione con agenti, rendendolo adatto sia alla ricerca sia alla distribuzione in scenari reali su edge e cloud.
In questo tutorial effettueremo il fine‑tuning di Qwen3‑VL‑8B‑Instruct su diagrammi di schemi elettronici. Addestrando il modello a interpretare simboli, connessioni e relazioni spaziali degli schemi, lo metteremo in grado di comprendere con precisione i circuiti e determinare quali componenti elettronici aggiungere in un circuito reale.
Imparerai a:
Caricare e ripulire il dataset da Hugging Face.
Creare un template di chat multimodale per l’addestramento.
Impostare la pipeline visione‑linguaggio di Qwen3-VL.
Configurare LoRA per un training efficiente in memoria.
Eseguire il fine-tuning del modello e salvare il checkpoint.
Pubblicare l’adapter addestrato su Hugging Face.
Confrontare i risultati prima e dopo il fine-tuning.
1. Configurazione dell’ambiente per il fine-tuning di Qwen3-VL-8B
Quando lavori con modelli visione–linguaggio, la memoria GPU diventa un vincolo critico. Immagini ad alta risoluzione e encoder multimodali possono consumare rapidamente VRAM, quindi è vivamente consigliata una GPU con molta memoria.
Per questo tutorial avvieremo un’istanza RunPod A100 (80GB) usando la più recente immagine PyTorch. Questa configurazione offre margine sufficiente di VRAM per l’addestramento ed evita colli di bottiglia di memoria durante il fine-tuning.
Scorri fino alla sezione Pod Template. Qui seleziona la più recente immagine PyTorch. In questa sezione puoi anche regolare lo storage e aggiungere variabili d’ambiente.
Fai clic su Edit, quindi applica le seguenti modifiche:
Dimensione disco del container: imposta a 40 GB
Dimensione del volume: imposta a 40 GB
Variabili d’ambiente: aggiungi HF_TOKEN e imposta il suo valore sul tuo token di accesso Hugging Face (generane uno da Hugging Face Settings)
Una volta fatto, salva il template e distribuisci il pod.
Una volta che il pod è in esecuzione:
Apri JupyterLab.
Crea un nuovo notebook Python.
Installa le dipendenze necessarie.
Per installare le dipendenze, esegui la seguente cella di codice. In Jupyter, un punto esclamativo all’inizio indica al notebook di eseguire la riga come comando di shell invece che come codice Python.
Poi imposta un seed casuale fisso per la riproducibilità e abilita ottimizzazioni specifiche per A100.
import torch
from transformers import set_seed
set_seed(42)# A100: TF32 gives speedups without changing your bf16 training setup
torch.backends.cuda.matmul.allow_tf32 =True
torch.backends.cudnn.allow_tf32 =Trueprint("CUDA:", torch.cuda.is_available(), torch.cuda.get_device_name(0)if torch.cuda.is_available()elseNone)print("bf16 supported:", torch.cuda.is_available()and torch.cuda.is_bf16_supported())Offerto da
CUDA: True NVIDIA A100 80GB PCIe
bf16 supported: TrueOfferto da
2. Download del dataset Open Schematics da Hugging Face
Ora caricheremo il datasetOpen Schematics dall’Hub di Hugging Face. Questo dataset contiene immagini di schemi elettronici con ricchi metadati che descrivono ciascun circuito, rendendolo adatto all’addestramento visione–linguaggio.
import torch
from datasets import load_dataset
DATASET_ID ="bshada/open-schematics"
ds_all = load_dataset(DATASET_ID, split="train")print(ds_all)Offerto da
Il dataset contiene oltre 84K campioni, ognuno che abbina un’immagine di schema a informazioni strutturate, come liste di componenti e formati leggibili da macchina (JSON e YAML).
3. Esplorazione della struttura del dataset
Ispezioniamo un singolo campione per capire meglio la struttura del dataset.
# quick peek
ex = ds_all[0]print("\nSample keys:", ex.keys())print("name:", ex.get("name"))print("type:", ex.get("type"))print("components_used:",(ex.get("components_used")or[])[:10])print("has schematic:",bool(ex.get("schematic")))print("has json/yaml:",bool(ex.get("json")),bool(ex.get("yaml")))print("image:", ex.get("image"))Offerto da
Sample keys: dict_keys(['schematic', 'image', 'components_used', 'json', 'yaml', 'name', 'description', 'type'])
name: TiebeDeclercq/Uart-programmer
type: .kicad_sch
components_used: ['Conn_01x01_Pin', 'Conn_01x06_Pin', 'USB_A', 'Conn_02x05_Odd_Even', 'Conn_01x06_MountingPin', 'C', 'Fuse_Small', 'LED', 'R', 'CH340C']
has schematic: True
has json/yaml: True True
image: <PIL.PngImagePlugin.PngImageFile image mode=RGBA size=1123x794 at 0x7FBC6FD060F0>Offerto da
Questo conferma che ogni campione include un’immagine di schema ad alta risoluzione, una lista di componenti e rappresentazioni strutturate del circuito.
Ora possiamo renderizzare l’immagine dello schema direttamente nel notebook Jupyter.
Questa lista di componenti fornisce un chiaro ancoraggio tra l’immagine dello schema e gli elementi elettronici presenti nel circuito.
4. Pulizia e filtraggio del dataset
Prima dell’addestramento, ripuliamo e filtriamo il dataset per assicurarci che ogni campione contenga le informazioni minime richieste per l’apprendimento visione–linguaggio. In particolare, ci concentriamo sul mantenere esempi con annotazioni valide dei componenti e la relativa immagine dello schema.
Per prima cosa, verifichiamo quanti campioni hanno voci components_used mancanti, vuote o non valide.
need_cols =[c for c in["components_used","schematic","name","type"]if c in ds_all.column_names]
ds_small = ds_all.select_columns(need_cols)
missing_key = none_components = empty_components = missing_any =0
has_schematic_but_missing =0for ex in ds_small:if"components_used"notin ex:
missing_key +=1
missing_any +=1if ex.get("schematic"):
has_schematic_but_missing +=1continue
cu = ex["components_used"]
bad =(cu isNone)or(isinstance(cu,list)andlen(cu)==0)if cu isNone:
none_components +=1elifisinstance(cu,list)andlen(cu)==0:
empty_components +=1if bad:
missing_any +=1if ex.get("schematic"):
has_schematic_but_missing +=1print("\n=== Missing components report ===")print("Total:",len(ds_all))print("Missing key:", missing_key)print("None:", none_components)print("Empty list:", empty_components)print("Missing (any):", missing_any)if"schematic"in need_cols:print("Has schematic but missing components:", has_schematic_but_missing)Offerto da
=== Missing components report ===
Total: 84470
Missing key: 0
None: 47558
Empty list: 8
Missing (any): 47566
Has schematic but missing components: 47566Offerto da
Il riepilogo qui sotto mostra che una grande parte dei campioni contiene dati di schema ma manca di annotazioni utilizzabili dei componenti.
Per evitare uso di memoria non necessario durante il filtraggio, disabilitiamo esplicitamente la decodifica delle immagini. Così Hugging Face non carica le immagini in memoria durante l’applicazione dei filtri.
from datasets.features import Image as HFImage
ds_all = ds_all.cast_column("image", HFImage(decode=False))Offerto da
Definiamo quindi un filtro che mantiene solo i campioni con una lista di componenti non vuota e un riferimento immagine valido.
defkeep_components_and_image(components_used, image):# keep only rows with componentsifnot(isinstance(components_used,list)andlen(components_used)>0):returnFalse# image must existif image isNone:returnFalse# when decode=False, image is dict-like: {"path": ...} or {"bytes": ...}ifisinstance(image,dict):returnbool(image.get("path"))orbool(image.get("bytes"))returnTrueOfferto da
Applicare questo filtro riduce sensibilmente il dataset a campioni di alta qualità e pienamente utilizzabili.
ds_clean = ds_all.filter(
keep_components_and_image,
input_columns=["components_used","image"],)print("Original:",len(ds_all))print("Clean:",len(ds_clean))print("Dropped:",len(ds_all)-len(ds_clean))Offerto da
Original: 84470
Clean: 33275
Dropped: 51195Offerto da
Dopo il filtraggio, restano oltre 33K campioni puliti con immagini di schemi valide e annotazioni esplicite dei componenti. Questo dataset pulito rappresenta una base affidabile per le fasi successive di pre‑processing e addestramento del modello.
5. Caricamento del modello visione‑linguaggio Qwen3-VL-8B
Ora carichiamo il modello Qwen3-VL-8B-Instruct insieme al relativo processor. Questo modello di larga scala è capace di ragionare congiuntamente su immagini e testo, rendendolo adatto ai compiti di comprensione degli schemi.
Il modello è caricato in precisione bfloat16 per ridurre l’uso di memoria mantenendo la stabilità numerica. Abilitiamo anche Flash Attention 2 per un’attenzione più veloce ed efficiente in memoria su GPU A100. L’opzione device_map="auto" posiziona automaticamente i layer del modello sulla GPU disponibile.
from transformers import Qwen3VLForConditionalGeneration, AutoProcessor
MODEL_ID ="Qwen/Qwen3-VL-8B-Instruct"
model = Qwen3VLForConditionalGeneration.from_pretrained(
MODEL_ID,
dtype=torch.bfloat16,
device_map="auto",
attn_implementation="flash_attention_2",)
processor = AutoProcessor.from_pretrained(MODEL_ID)Offerto da
6. Progettazione dei prompt per l’analisi dei circuiti
In questo passaggio definiamo utility leggere per preparare prompt, target e immagini per l’addestramento visione–linguaggio. Una singola pipeline viene usata per più task cambiando la variabile TASK (components, YAML, JSON o ricostruzione dello schema).
Si applicano limiti di sicurezza di base per controllare la lunghezza del target e la dimensione dell’immagine, aiutando a mantenere l’addestramento stabile ed efficiente in memoria.
from PIL import Image
TASK ="components"# "components" | "yaml" | "json" | "schematic"
MAX_TARGET_CHARS =5000# safety cap for long targets like schematic/json
MAX_IMAGE_SIDE =1024# bigger side
MAX_IMAGE_PIXELS =1024*1024# safety cap (1.0 MP). raise to 1.5MP if stableOfferto da
Creazione del prompt
La funzione build_prompt() costruisce il testo di istruzione passato al modello. Usa i metadati del dataset come contesto e impone vincoli di output rigorosi per ridurre le allucinazioni e mantenere una supervisione coerente tra i task.
defbuild_prompt(example):# Use dataset fields to give better context (name/type are helpful)
name = example.get("name")or"Unknown project"
ftype = example.get("type")or"unknown format"if TASK =="components":return(f"Project: {name}\nFormat: {ftype}\n""From the schematic image, extract all component labels and identifiers exactly as shown ""(part numbers, values, footprints, net labels like +5V/GND).\n""Output only a comma-separated list. Do not generalize or add extra text.")if TASK =="yaml":return(f"Project: {name}\nFormat: {ftype}\n""From the schematic image, produce YAML metadata for the design.\n""Return valid YAML only. No markdown, no explanations.")if TASK =="json":return(f"Project: {name}\nFormat: {ftype}\n""From the schematic image, produce a JSON representation of the schematic structure.\n""Return valid JSON only. No markdown, no explanations.")if TASK =="schematic":return(f"Project: {name}\nFormat: {ftype}\n""From the schematic image, reconstruct the raw KiCad schematic content.\n""Return only the schematic text. No markdown, no explanations.")raise ValueError("Unknown TASK")Offerto da
Creazione del target
La funzione build_target() estrae direttamente dal dataset l’output ground-truth per il task selezionato. Il contenuto viene restituito alla lettera per addestrare il modello alla riproduzione esatta, non alla parafrasi.
La funzione clamp_text() applica un limite massimo di caratteri ai target. Questo evita che JSON, YAML o file di schema troppo grandi causino problemi di memoria durante l’addestramento.
defclamp_text(s:str, max_chars:int= MAX_TARGET_CHARS)->str:
s =(s or"").strip()return s iflen(s)<= max_chars else s[:max_chars].rstrip()Offerto da
Ridimensionamento delle immagini
La funzione _resize_pil() normalizza e ridimensiona le immagini degli schemi prima dell’elaborazione. Impone sia una lunghezza massima del lato sia un conteggio massimo di pixel totali, garantendo un uso prevedibile della memoria GPU preservando il dettaglio visivo.
def_resize_pil(pil: Image.Image, max_side:int= MAX_IMAGE_SIDE, max_pixels:int= MAX_IMAGE_PIXELS)-> Image.Image:
pil = pil.convert("RGB")
w, h = pil.size
# Scale down if max side too large
scale_side =min(1.0, max_side /float(max(w, h)))# Scale down if too many pixels (area cap)
scale_area =(max_pixels /float(w * h))**0.5if(w * h)> max_pixels else1.0
scale =min(scale_side, scale_area)if scale <1.0:
nw, nh =max(1,int(w * scale)),max(1,int(h * scale))
pil = pil.resize((nw, nh), resample=Image.BICUBIC)return pilOfferto da
7. Impostazione del template di chat multimodale
In questo passaggio convertiamo ogni campione pulito del dataset in un formato stile chat multimodale che può essere consumato direttamente dal modello visione–linguaggio Qwen. Questo formato allinea esplicitamente l’immagine dello schema con un’istruzione testuale e il relativo target.
Mescoliamo il dataset per rimuovere bias di ordinamento e selezioniamo un piccolo sottoinsieme per i primi esperimenti.
Il dataset viene quindi mappato tramite to_messages() per generare esempi di training multimodali. Infine, la decodifica delle immagini viene riabilitata, in modo che le immagini vengano caricate solo in fase di training, mantenendo il pre‑processing leggero ed efficiente in memoria.
# Start small (increase later)
train_ds = ds_clean.shuffle(seed=42).select(range(min(800,len(ds_clean)))).map(to_messages)
train_ds = train_ds.cast_column("image", HFImage(decode=True))Offerto da
8. Valutazione di Qwen3-VL-8B (prima del fine-tuning)
Prima del fine-tuning, valutiamo le prestazioni out‑of‑the‑box del modello Qwen3‑VL 8B Instruct sul nostro task. Questa baseline ci aiuta a capire quanto il modello preaddestrato riesca a estrarre informazioni dalle immagini degli schemi senza alcun adattamento specifico.
La funzione run_inference() esegue un singolo forward pass su un esempio usando la stessa logica di prompting e pre‑processing delle immagini che verrà poi usata durante il training.
import torch
defrun_inference(model_, example, max_new_tokens=256):
prompt = build_prompt(example)
messages =[{"role":"user","content":[{"type":"image","image": _resize_pil(example["image"])},{"type":"text","text": prompt},],}]
inputs = processor.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
return_dict=True,
return_tensors="pt",).to(model_.device)with torch.inference_mode():
out = model_.generate(**inputs, max_new_tokens=max_new_tokens, do_sample=False)
gen = out[0][inputs["input_ids"].shape[1]:]return processor.decode(gen, skip_special_tokens=True)
baseline_ex = train_ds.shuffle(seed=120).select(range(1))[0]Offerto da
Valutiamo prima il modello su un campione selezionato casualmente dal training set.
In questo esempio, il modello identifica correttamente alcuni elementi strutturali ma sovragenera nomi di pin e segnali, senza riuscire a recuperare gli identificatori esatti dei componenti usati nel dataset. Ho riscontrato lo stesso schema in diversi altri esempi durante la valutazione.
Nel complesso, questi risultati di baseline mostrano che, pur avendo una forte comprensione visiva e testuale generale, il modello manca di allineamento con le annotazioni specifiche del dataset. Questo comportamento evidenzia la necessità di fine-tuning per ridurre le allucinazioni e migliorare la precisione.
9. Creazione di un data collator visione‑linguaggio per il training
Questa sezione definisce un data collator personalizzato che prepara batch in stile chat multimodale per l’addestramento. Converte ogni esempio in tensori pronti per il modello codificando congiuntamente testo e immagini, assicurando che la loss sia calcolata solo sulla risposta dell’assistente.
Il collator costruisce due versioni del testo di chat: una completa (prompt e target) per l’encoding dell’input e una solo‑prompt per calcolare la lunghezza dei token di prompt. Usando queste lunghezze, tutti i token di prompt e padding sono mascherati nelle label, così solo l’output dell’assistente contribuisce alla loss. Le immagini sono ridimensionate in modo coerente e viene imposto un massimo fisso di lunghezza di sequenza per il controllo della memoria.
from typing import List, Dict, Any
import torch
MAX_LEN =1500defcollate_fn(batch: List[Dict[str, Any]]):# 1) Build full chat text (includes assistant answer)
full_texts =[
processor.apply_chat_template(
ex["messages"],
tokenize=False,
add_generation_prompt=False,)for ex in batch
]# 2) Build prompt-only text (up to user turn; generation prompt on)
prompt_texts =[
processor.apply_chat_template(
ex["messages"][:-1],
tokenize=False,
add_generation_prompt=True,)for ex in batch
]# 3) Images
images =[_resize_pil(ex["image"])for ex in batch]# 4) Tokenize full inputs ONCE (text + images)
enc = processor(
text=full_texts,
images=images,
return_tensors="pt",
padding=True,
truncation=True,
max_length=MAX_LEN,)
input_ids = enc["input_ids"]
pad_id = processor.tokenizer.pad_token_id
# 5) Compute prompt lengths with TEXT-ONLY tokenization (much cheaper than text+images)
prompt_ids = processor.tokenizer(
prompt_texts,
return_tensors="pt",
padding=True,
truncation=True,
max_length=MAX_LEN,
add_special_tokens=False,# chat template already includes special tokens)["input_ids"]# Count non-pad tokens in prompt
prompt_lens =(prompt_ids != pad_id).sum(dim=1)# 6) Labels: copy + mask prompt tokens + mask padding
labels = input_ids.clone()
bs, seqlen = labels.shape
for i inrange(bs):
pl =int(prompt_lens[i].item())
pl =min(pl, seqlen)
labels[i,:pl]=-100# Mask padding positions too
labels[labels == pad_id]=-100# If your processor produces pixel_values / image_grid_thw, keep them
enc["labels"]= labels
return encOfferto da
Questo collator abilita una supervisione efficiente e corretta per il fine-tuning visione–linguaggio.
10. Configurazione di LoRA per un fine-tuning efficiente di Qwen3-VL-8B
Ora configureremo LoRA (Low-Rank Adaptation) per effettuare il fine-tuning del modello Qwen3‑VL in modo efficiente senza aggiornare tutti i pesi. LoRA inietta matrici addestrabili a basso rango in layer di proiezione selezionati, riducendo significativamente l’uso di memoria preservando le prestazioni.
from peft import LoraConfig, TaskType, get_peft_model
lora = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
bias="none",
task_type=TaskType.CAUSAL_LM,
target_modules=["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"],)Offerto da
Definiamo quindi la configurazione di training usando SFTConfig, impostando batch size, learning rate, precisione e opzioni di logging adatte a un fine-tuning stabile su GPU A100.
Una volta iniziato l’addestramento, puoi monitorare la dashboard di telemetria di RunPod. Su un’istanza A100 80GB, il processo usa tipicamente circa 40–45 GB di VRAM e raggiunge un utilizzo quasi pieno della GPU, indicando un impiego efficiente dell’hardware.
Con l’avanzare del training, dovresti osservare la training loss diminuire gradualmente prima di stabilizzarsi. Nel nostro caso, la loss converge e si assesta intorno a 6,5, un indicatore di base che il modello si è adattato al task di estrazione dei componenti dallo schema.
A questo punto, gli adapter LoRA sono stati sottoposti con successo a fine-tuning e sono pronti per la valutazione e l’export.
12. Pubblicazione del modello fine-tuned su Hugging Face Hub
Al termine del fine-tuning, salviamo prima in locale gli adapter LoRA addestrati e il processor associato.
out_dir = trainer.args.output_dir # from your SFTConfig/TrainingArguments
trainer.save_model(out_dir)# saves model/adapters into output_dir
processor.save_pretrained(out_dir)# save processor (tokenizer + image processor)Offerto da
Successivamente, pubblichiamo il modello fine‑tuned su Hugging Face Hub. Questo permette di riutilizzare adapter e processor per inferenza o ulteriore fine-tuning.
import os
repo_id ="kingabzpro/qwen3vl-open-schematics-lora"# replace with your username/repo# Push model/adapters
trainer.model.push_to_hub(
repo_id,
token=os.getenv("HF_TOKEN"),)# Push processor
processor.push_to_hub(
repo_id,
token=os.getenv("HF_TOKEN"),)Offerto da
Una volta completato l’upload, gli adapter LoRA fine‑tuned e il processor sono disponibili pubblicamente nel repository specificato.
12. Valutazione del modello Qwen3-VL-8B fine-tuned
Dopo il fine-tuning, ricarichiamo modello e processor direttamente da Hugging Face Hub. Questo garantisce che la valutazione sia eseguita usando gli adapter LoRA esportati, esattamente come verrebbero usati in un contesto di inferenza reale.
model = Qwen3VLForConditionalGeneration.from_pretrained(
repo_id,
dtype=torch.bfloat16,
device_map="auto",
attn_implementation="flash_attention_2",)
processor = AutoProcessor.from_pretrained(repo_id)
```
We now repeat the same inference procedure used during the baseline evaluation, allowing for a direct comparison between pre-fine-tuning and post-fine-tuning behavior.
```python
baseline_ex = train_ds.shuffle(seed=120).select(range(1))[0]print("\n--- FINETUNED OUTPUT ---\n", run_inference(model, baseline_ex))print("\n--- TARGET (dataset) ---\n", clamp_text(build_target(baseline_ex),1500))Offerto da
Rispetto alla baseline, il modello fine‑tuned produce un output molto più breve e mirato, evitando la sovragenerazione massiccia di nomi di pin e segnali dedotti.
Sebbene la previsione sia ancora incompleta e contenga piccoli errori, mostra un chiaro movimento verso identificatori di componenti allineati al dataset.
Valutiamo ora un secondo esempio per confermare questo comportamento.
Qui, il modello fine‑tuned identifica correttamente componenti fondamentali come il microcontrollore e le net di alimentazione, riducendo sensibilmente le allucinazioni a livello di segnale. Tuttavia, astrae o generalizza ancora alcuni componenti invece di riprodurre gli identificatori specifici del dataset.
Nel complesso, questi risultati dimostrano che il fine‑tuning sopprime con successo la sovragenerazione e migliora l’allineamento con l’estrazione di componenti a livello di schema. Sebbene la precisione possa essere ulteriormente migliorata con più epoche, set di training più grandi o vincoli di output più rigorosi, il comportamento post‑fine‑tuning rappresenta un chiaro miglioramento rispetto alla baseline.
Se riscontri problemi nell’esecuzione del codice in questo tutorial, fai riferimento al notebook di accompagnamento.
Considerazioni finali
I modelli visione–linguaggio sono fondamentalmente diversi dai modelli solo‑testo, e trattarli allo stesso modo porta quasi sempre a risultati scarsi.
Se non stai attento, è molto facile incorrere in errori di out‑of‑memory anche con batch size pari a uno, o fare il fine‑tuning di un modello che sembra addestrarsi ma non impara realmente il task. L’ho imparato a mie spese.
Ciò che ha fatto davvero la differenza è stato prestare attenzione ai dettagli che contano specificamente per il training multimodale.
Ridimensionare le immagini entro limiti sicuri, ripulire il dataset rimuovendo campioni danneggiati o inutilizzabili e assicurarsi che al modello arrivino solo coppie immagine‑testo valide sono passaggi essenziali. Saltarne anche solo uno porta rapidamente a instabilità o spreco di calcolo.
Dal lato del modello, usare solo i layer target LoRA rilevanti ha aiutato a mantenere l’addestramento efficiente e focalizzato, mentre una messa a punto attenta degli argomenti di training ha migliorato la convergenza senza aumentare la pressione sulla memoria.
Ottimizzare per la GPU A100, abilitare Flash Attention e usare bfloat16 ha permesso di mantenere stabile l’addestramento riducendo significativamente i tempi di esecuzione. In pratica, queste ottimizzazioni hanno quasi dimezzato i tempi di training senza sacrificare la qualità.
I risultati finali mostrano che anche un solido modello visione–linguaggio preaddestrato trae grande beneficio da un fine‑tuning specifico per il dominio. Con il giusto pre‑processing, una configurazione adeguata e ottimizzazioni consapevoli dell’hardware, è possibile adattare in modo affidabile ed efficiente grandi modelli multimodali.
Se vuoi fare ulteriore pratica con il fine-tuning, ti consiglio il corso Fine-Tuning with Llama 3.
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.
Scopri i 15 migliori server MCP remoti che stanno trasformando lo sviluppo AI nel 2026. Scopri come migliorano automazione, ragionamento, sicurezza e velocità dei workflow.
Scopri i modelli di IA open source, i dataset e le app più recenti, crea agenti di IA e metti a punto gli LLM con Hugging Face. Entra oggi nella più grande community di IA!
Applica PyTorch alle immagini e usa modelli di deep learning per il rilevamento di oggetti con bounding box e generazione di segmentazione delle immagini.
Qwen3‑VL‑8B è il modello visione‑linguaggio più potente e più usato della serie Qwen, progettato per una comprensione unificata di testo, immagini e video.
Offre miglioramenti significativi nella generazione di testo, nel ragionamento visivo, nella percezione spaziale, nella gestione di contesti lunghi e nell’interazione con agenti, rendendolo adatto sia alla ricerca sia alla distribuzione in scenari reali su edge e cloud.
In questo tutorial effettueremo il fine‑tuning di Qwen3‑VL‑8B‑Instruct su diagrammi di schemi elettronici. Addestrando il modello a interpretare simboli, connessioni e relazioni spaziali degli schemi, lo metteremo in grado di comprendere con precisione i circuiti e determinare quali componenti elettronici aggiungere in un circuito reale.
Imparerai a:
Se Hugging Face è una novità per te, lo skill track Hugging Face Fundamentals fa per te!
1. Configurazione dell’ambiente per il fine-tuning di Qwen3-VL-8B
Quando lavori con modelli visione–linguaggio, la memoria GPU diventa un vincolo critico. Immagini ad alta risoluzione e encoder multimodali possono consumare rapidamente VRAM, quindi è vivamente consigliata una GPU con molta memoria.
Per questo tutorial avvieremo un’istanza RunPod A100 (80GB) usando la più recente immagine PyTorch. Questa configurazione offre margine sufficiente di VRAM per l’addestramento ed evita colli di bottiglia di memoria durante il fine-tuning.
Scorri fino alla sezione Pod Template. Qui seleziona la più recente immagine PyTorch. In questa sezione puoi anche regolare lo storage e aggiungere variabili d’ambiente.
Fai clic su Edit, quindi applica le seguenti modifiche:
HF_TOKENe imposta il suo valore sul tuo token di accesso Hugging Face (generane uno da Hugging Face Settings)Una volta fatto, salva il template e distribuisci il pod.
Una volta che il pod è in esecuzione:
Per installare le dipendenze, esegui la seguente cella di codice. In Jupyter, un punto esclamativo all’inizio indica al notebook di eseguire la riga come comando di shell invece che come codice Python.
Poi imposta un seed casuale fisso per la riproducibilità e abilita ottimizzazioni specifiche per A100.
2. Download del dataset Open Schematics da Hugging Face
Ora caricheremo il dataset Open Schematics dall’Hub di Hugging Face. Questo dataset contiene immagini di schemi elettronici con ricchi metadati che descrivono ciascun circuito, rendendolo adatto all’addestramento visione–linguaggio.
Il dataset contiene oltre 84K campioni, ognuno che abbina un’immagine di schema a informazioni strutturate, come liste di componenti e formati leggibili da macchina (JSON e YAML).
3. Esplorazione della struttura del dataset
Ispezioniamo un singolo campione per capire meglio la struttura del dataset.
Questo conferma che ogni campione include un’immagine di schema ad alta risoluzione, una lista di componenti e rappresentazioni strutturate del circuito.
Ora possiamo renderizzare l’immagine dello schema direttamente nel notebook Jupyter.
Infine, ispezioniamo l’elenco completo dei componenti usati in questo schema.
Questa lista di componenti fornisce un chiaro ancoraggio tra l’immagine dello schema e gli elementi elettronici presenti nel circuito.
4. Pulizia e filtraggio del dataset
Prima dell’addestramento, ripuliamo e filtriamo il dataset per assicurarci che ogni campione contenga le informazioni minime richieste per l’apprendimento visione–linguaggio. In particolare, ci concentriamo sul mantenere esempi con annotazioni valide dei componenti e la relativa immagine dello schema.
Per prima cosa, verifichiamo quanti campioni hanno voci
components_usedmancanti, vuote o non valide.Il riepilogo qui sotto mostra che una grande parte dei campioni contiene dati di schema ma manca di annotazioni utilizzabili dei componenti.
Per evitare uso di memoria non necessario durante il filtraggio, disabilitiamo esplicitamente la decodifica delle immagini. Così Hugging Face non carica le immagini in memoria durante l’applicazione dei filtri.
Definiamo quindi un filtro che mantiene solo i campioni con una lista di componenti non vuota e un riferimento immagine valido.
Applicare questo filtro riduce sensibilmente il dataset a campioni di alta qualità e pienamente utilizzabili.
Dopo il filtraggio, restano oltre 33K campioni puliti con immagini di schemi valide e annotazioni esplicite dei componenti. Questo dataset pulito rappresenta una base affidabile per le fasi successive di pre‑processing e addestramento del modello.
5. Caricamento del modello visione‑linguaggio Qwen3-VL-8B
Ora carichiamo il modello Qwen3-VL-8B-Instruct insieme al relativo processor. Questo modello di larga scala è capace di ragionare congiuntamente su immagini e testo, rendendolo adatto ai compiti di comprensione degli schemi.
Il modello è caricato in precisione bfloat16 per ridurre l’uso di memoria mantenendo la stabilità numerica. Abilitiamo anche Flash Attention 2 per un’attenzione più veloce ed efficiente in memoria su GPU A100. L’opzione
device_map="auto"posiziona automaticamente i layer del modello sulla GPU disponibile.6. Progettazione dei prompt per l’analisi dei circuiti
In questo passaggio definiamo utility leggere per preparare prompt, target e immagini per l’addestramento visione–linguaggio. Una singola pipeline viene usata per più task cambiando la variabile
TASK(components, YAML, JSON o ricostruzione dello schema).Si applicano limiti di sicurezza di base per controllare la lunghezza del target e la dimensione dell’immagine, aiutando a mantenere l’addestramento stabile ed efficiente in memoria.
Creazione del prompt
La funzione
build_prompt()costruisce il testo di istruzione passato al modello. Usa i metadati del dataset come contesto e impone vincoli di output rigorosi per ridurre le allucinazioni e mantenere una supervisione coerente tra i task.Creazione del target
La funzione
build_target()estrae direttamente dal dataset l’output ground-truth per il task selezionato. Il contenuto viene restituito alla lettera per addestrare il modello alla riproduzione esatta, non alla parafrasi.La funzione
clamp_text()applica un limite massimo di caratteri ai target. Questo evita che JSON, YAML o file di schema troppo grandi causino problemi di memoria durante l’addestramento.Ridimensionamento delle immagini
La funzione
_resize_pil()normalizza e ridimensiona le immagini degli schemi prima dell’elaborazione. Impone sia una lunghezza massima del lato sia un conteggio massimo di pixel totali, garantendo un uso prevedibile della memoria GPU preservando il dettaglio visivo.7. Impostazione del template di chat multimodale
In questo passaggio convertiamo ogni campione pulito del dataset in un formato stile chat multimodale che può essere consumato direttamente dal modello visione–linguaggio Qwen. Questo formato allinea esplicitamente l’immagine dello schema con un’istruzione testuale e il relativo target.
Mescoliamo il dataset per rimuovere bias di ordinamento e selezioniamo un piccolo sottoinsieme per i primi esperimenti.
Il dataset viene quindi mappato tramite
to_messages()per generare esempi di training multimodali. Infine, la decodifica delle immagini viene riabilitata, in modo che le immagini vengano caricate solo in fase di training, mantenendo il pre‑processing leggero ed efficiente in memoria.8. Valutazione di Qwen3-VL-8B (prima del fine-tuning)
Prima del fine-tuning, valutiamo le prestazioni out‑of‑the‑box del modello Qwen3‑VL 8B Instruct sul nostro task. Questa baseline ci aiuta a capire quanto il modello preaddestrato riesca a estrarre informazioni dalle immagini degli schemi senza alcun adattamento specifico.
La funzione
run_inference()esegue un singolo forward pass su un esempio usando la stessa logica di prompting e pre‑processing delle immagini che verrà poi usata durante il training.Valutiamo prima il modello su un campione selezionato casualmente dal training set.
In questo esempio, il modello identifica correttamente alcuni elementi strutturali ma sovragenera nomi di pin e segnali, senza riuscire a recuperare gli identificatori esatti dei componenti usati nel dataset. Ho riscontrato lo stesso schema in diversi altri esempi durante la valutazione.
Nel complesso, questi risultati di baseline mostrano che, pur avendo una forte comprensione visiva e testuale generale, il modello manca di allineamento con le annotazioni specifiche del dataset. Questo comportamento evidenzia la necessità di fine-tuning per ridurre le allucinazioni e migliorare la precisione.
9. Creazione di un data collator visione‑linguaggio per il training
Questa sezione definisce un data collator personalizzato che prepara batch in stile chat multimodale per l’addestramento. Converte ogni esempio in tensori pronti per il modello codificando congiuntamente testo e immagini, assicurando che la loss sia calcolata solo sulla risposta dell’assistente.
Il collator costruisce due versioni del testo di chat: una completa (prompt e target) per l’encoding dell’input e una solo‑prompt per calcolare la lunghezza dei token di prompt. Usando queste lunghezze, tutti i token di prompt e padding sono mascherati nelle label, così solo l’output dell’assistente contribuisce alla loss. Le immagini sono ridimensionate in modo coerente e viene imposto un massimo fisso di lunghezza di sequenza per il controllo della memoria.
Questo collator abilita una supervisione efficiente e corretta per il fine-tuning visione–linguaggio.
10. Configurazione di LoRA per un fine-tuning efficiente di Qwen3-VL-8B
Ora configureremo LoRA (Low-Rank Adaptation) per effettuare il fine-tuning del modello Qwen3‑VL in modo efficiente senza aggiornare tutti i pesi. LoRA inietta matrici addestrabili a basso rango in layer di proiezione selezionati, riducendo significativamente l’uso di memoria preservando le prestazioni.
Definiamo quindi la configurazione di training usando
SFTConfig, impostando batch size, learning rate, precisione e opzioni di logging adatte a un fine-tuning stabile su GPU A100.Infine, inizializziamo l’
SFTTrainer, combinando modello, dataset, collator personalizzato e configurazione LoRA per avviare il fine-tuning supervisionato.11. Fine-tuning di Qwen3‑VL-8B sul dataset Open Schematics
Ora avviamo il processo di fine-tuning usando il trainer configurato.
Una volta iniziato l’addestramento, puoi monitorare la dashboard di telemetria di RunPod. Su un’istanza A100 80GB, il processo usa tipicamente circa 40–45 GB di VRAM e raggiunge un utilizzo quasi pieno della GPU, indicando un impiego efficiente dell’hardware.
Con l’avanzare del training, dovresti osservare la training loss diminuire gradualmente prima di stabilizzarsi. Nel nostro caso, la loss converge e si assesta intorno a 6,5, un indicatore di base che il modello si è adattato al task di estrazione dei componenti dallo schema.
A questo punto, gli adapter LoRA sono stati sottoposti con successo a fine-tuning e sono pronti per la valutazione e l’export.
12. Pubblicazione del modello fine-tuned su Hugging Face Hub
Al termine del fine-tuning, salviamo prima in locale gli adapter LoRA addestrati e il processor associato.
Successivamente, pubblichiamo il modello fine‑tuned su Hugging Face Hub. Questo permette di riutilizzare adapter e processor per inferenza o ulteriore fine-tuning.
Una volta completato l’upload, gli adapter LoRA fine‑tuned e il processor sono disponibili pubblicamente nel repository specificato.
12. Valutazione del modello Qwen3-VL-8B fine-tuned
Dopo il fine-tuning, ricarichiamo modello e processor direttamente da Hugging Face Hub. Questo garantisce che la valutazione sia eseguita usando gli adapter LoRA esportati, esattamente come verrebbero usati in un contesto di inferenza reale.
Rispetto alla baseline, il modello fine‑tuned produce un output molto più breve e mirato, evitando la sovragenerazione massiccia di nomi di pin e segnali dedotti.
Sebbene la previsione sia ancora incompleta e contenga piccoli errori, mostra un chiaro movimento verso identificatori di componenti allineati al dataset.
Valutiamo ora un secondo esempio per confermare questo comportamento.
Qui, il modello fine‑tuned identifica correttamente componenti fondamentali come il microcontrollore e le net di alimentazione, riducendo sensibilmente le allucinazioni a livello di segnale. Tuttavia, astrae o generalizza ancora alcuni componenti invece di riprodurre gli identificatori specifici del dataset.
Nel complesso, questi risultati dimostrano che il fine‑tuning sopprime con successo la sovragenerazione e migliora l’allineamento con l’estrazione di componenti a livello di schema. Sebbene la precisione possa essere ulteriormente migliorata con più epoche, set di training più grandi o vincoli di output più rigorosi, il comportamento post‑fine‑tuning rappresenta un chiaro miglioramento rispetto alla baseline.
Se riscontri problemi nell’esecuzione del codice in questo tutorial, fai riferimento al notebook di accompagnamento.
Considerazioni finali
I modelli visione–linguaggio sono fondamentalmente diversi dai modelli solo‑testo, e trattarli allo stesso modo porta quasi sempre a risultati scarsi.
Se non stai attento, è molto facile incorrere in errori di out‑of‑memory anche con batch size pari a uno, o fare il fine‑tuning di un modello che sembra addestrarsi ma non impara realmente il task. L’ho imparato a mie spese.
Ciò che ha fatto davvero la differenza è stato prestare attenzione ai dettagli che contano specificamente per il training multimodale.
Ridimensionare le immagini entro limiti sicuri, ripulire il dataset rimuovendo campioni danneggiati o inutilizzabili e assicurarsi che al modello arrivino solo coppie immagine‑testo valide sono passaggi essenziali. Saltarne anche solo uno porta rapidamente a instabilità o spreco di calcolo.
Dal lato del modello, usare solo i layer target LoRA rilevanti ha aiutato a mantenere l’addestramento efficiente e focalizzato, mentre una messa a punto attenta degli argomenti di training ha migliorato la convergenza senza aumentare la pressione sulla memoria.
Ottimizzare per la GPU A100, abilitare Flash Attention e usare bfloat16 ha permesso di mantenere stabile l’addestramento riducendo significativamente i tempi di esecuzione. In pratica, queste ottimizzazioni hanno quasi dimezzato i tempi di training senza sacrificare la qualità.
I risultati finali mostrano che anche un solido modello visione–linguaggio preaddestrato trae grande beneficio da un fine‑tuning specifico per il dominio. Con il giusto pre‑processing, una configurazione adeguata e ottimizzazioni consapevoli dell’hardware, è possibile adattare in modo affidabile ed efficiente grandi modelli multimodali.
Se vuoi fare ulteriore pratica con il fine-tuning, ti consiglio il corso Fine-Tuning with Llama 3.