Addestramento e finetuning di modelli multimodali di embedding e reranker con Sentence Transformers
Sentence Transformers è una libreria Python per l'utilizzo e l'addestramento di modelli di embedding e reranker per applicazioni come la generazione aumentata da recupero (RAG), la ricerca semantica e altro ancora. Nel mio precedente post sul blog, ho introdotto le nuove capacità multimodali, mostrando come utilizzare modelli di embedding e reranker che gestiscono testo, immagini, audio e video. In questo post, mostrerò come addestrare o ottimizzare questi modelli multimodali sui tuoi dati.
Come esempio pratico, illustrerò l'ottimizzazione del modello Qwen/Qwen3-VL-Embedding-2B per il Recupero Visuale di Documenti (VDR), il compito di recuperare pagine di documenti pertinenti (come immagini, con grafici, tabelle e layout intatti) per una data query testuale. Il modello risultante, tomaarsen/Qwen3-VL-Embedding-2B-vdr, dimostra quanto si possa migliorare le prestazioni con il finetuning sui propri dati di dominio. Sulla mia valutazione, il modello ottimizzato raggiunge un NDCG@10 di 0,947 rispetto allo 0,888 del modello base e supera tutti i modelli VDR esistenti che ho testato, inclusi modelli fino a 4 volte più grandi.
Se sei nuovo ai modelli multimodali in Sentence Transformers, ti consiglio di leggere prima "Modelli multimodali di embedding e reranker con Sentence Transformers". Per l'addestramento di modelli di embedding solo testuali, reranker o sparse embedding, consulta la sezione "Post precedenti del blog" alla fine.
Perché il Finetuning?
I modelli di embedding multimodali generici come Qwen/Qwen3-VL-Embedding-2B sono addestrati su dati diversificati per performare bene in un'ampia gamma di lingue e compiti: abbinamento immagine-testo, risposta a domande visive, comprensione di documenti e altro ancora. Ma questa generalità significa che il modello è raramente la scelta migliore per qualsiasi compito specifico.
Considera il Recupero Visuale di Documenti: data una query testuale come "Qual è stato il fatturato del terzo trimestre dell'azienda?", il modello deve trovare lo screenshot del documento più pertinente da un corpus di migliaia di documenti. Questo richiede la comprensione dei layout dei documenti, dei grafici, delle tabelle e del testo, il che è un'abilità molto diversa dall'abbinare, ad esempio, immagini di scarpe con descrizioni di prodotti.
Mediante il finetuning su dati specifici del dominio, il modello può apprendere questi schemi specializzati. Nel mio esperimento, il finetuning ha migliorato l'NDCG@10 da 0,888 a 0,947, superando ogni modello multimodale recente che ho testato, inclusi quelli fino a 4 volte più grandi. Questo dimostra l'enorme potenziale di miglioramento delle prestazioni quando un modello viene adattato con precisione al proprio caso d'uso specifico.
Componenti dell'addestramento multimodale
L'addestramento dei modelli multimodali di Sentence Transformer implica gli stessi componenti dell'addestramento dei modelli solo testuali. La pipeline di addestramento multimodale utilizza lo stesso SentenceTransformerTrainer dell'addestramento solo testuale. La differenza fondamentale è che i tuoi dataset contengono immagini (o altre modalità) insieme al testo, e il processore del modello gestisce automaticamente la pre-elaborazione delle immagini. Questo semplifica notevolmente il processo, eliminando la necessità di gestire manualmente le complessità della pre-elaborazione per diverse modalità.
Analizziamo ciascun componente, utilizzando il Recupero Visuale di Documenti (VDR - l'abbinamento di query testuali con screenshot di documenti) come esempio continuo.
Modelli con consapevolezza delle modalità
L'approccio più comune è ottimizzare un modello di embedding multimodale esistente o partire da un checkpoint di un Modello Visione-Linguaggio (VLM). Il modulo Transformer rileva automaticamente le modalità supportate dal processore del modello.
Finetuning di un modello multimodale esistente
Per ottimizzare un modello di embedding multimodale esistente (ad esempio, uno che ha già un file modules.json), puoi passare processor_kwargs e model_kwargs per controllare rispettivamente la pre-elaborazione e il caricamento del modello. I processor_kwargs vengono passati direttamente a AutoProcessor.from_pretrained(...) (ad esempio, limiti di risoluzione dell'immagine: un max_pixels più alto significa maggiore qualità ma più memoria), mentre i model_kwargs vengono passati alla chiamata appropriata di AutoModel.from_pretrained(...) (ad esempio, precisione, implementazione dell'attenzione):
model = SentenceTransformer(
model_name_or_path="Qwen/Qwen3-VL-Embedding-2B",
processor_kwargs={
"max_pixels": 4096 * 4096, # 16M pixels
"image_size": 448,
},
model_kwargs={
"torch_dtype": torch.bfloat16,
"attn_implementation": "flash_attention_2",
},
)
Partire da un checkpoint VLM
Puoi anche partire da un nuovo checkpoint VLM che non è ancora stato addestrato per gli embedding. Sentence Transformers tenterà di riconoscere l'architettura, inferire le modalità supportate dal processore e configurare il metodo forward e il pooling appropriati. Se il rilevamento automatico non funziona perfettamente per un particolare modello, la configurazione nel file salvato sentence_bert_config.json può essere modificata per regolare le impostazioni della modalità, i metodi forward e la gestione dell'output:
{
"encoder_id": "qwen/qwen3-vl-2b",
"processor_id": "qwen/qwen3-vl-2b",
"pooling_mode": "mean",
"pooling_kwargs": {},
"output_dimension": 2048,
"forward_method": "encode_multimodal",
"modality_configs": {
"text": {
"token_ids_key": "input_ids",
"attention_mask_key": "attention_mask",
"token_type_ids_key": "token_type_ids"
},
"image": {
"pixel_values_key": "pixel_values"
}
}
}
In entrambi i casi, il modulo Transformer ispeziona il processore per determinare quali modalità sono disponibili, e il Pooling viene aggiunto automaticamente se necessario. Puoi verificare le modalità supportate con il seguente codice:
print(model.get_supported_modalities())
# Out: {'text', 'image'}
Comporre encoder con Router
Invece di usare un singolo backbone VLM, puoi comporre encoder separati per diverse modalità usando il modulo Router. Questo ti permette di combinare qualsiasi encoder esistente e instradare gli input a quello appropriato in base alla modalità rilevata:
model = SentenceTransformer(
modules=[
Router(
route_mappings={
"text": "text_encoder",
"image": "image_encoder",
},
encoders={
"text_encoder": Transformer("BAAI/bge-small-en-v1.5"),
"image_encoder": Transformer("openai/clip-vit-base-patch32"),
},
),
Dense(
in_features=768 + 512, # BGE (768) + CLIP (512)
out_features=512,
),
Normalize(),
]
)
Dato che i modelli multimodali basati su Router utilizzano encoder separati per modalità, i loro spazi di embedding sono inizialmente non allineati. L'addestramento è necessario per allineare gli spazi per una significativa similarità cross-modale. Lo strato di proiezione Dense mostrato sopra aiuta a mappare gli embedding da diversi encoder in uno spazio condiviso, consentendo la comparazione efficace di input provenienti da modalità diverse.
Questo approccio è utile quando si desidera utilizzare encoder leggeri e specializzati piuttosto che un VLM di grandi dimensioni. Puoi anche combinare la multimodalità basata su Router con l'instradamento basato sul compito (ad esempio, diversi encoder per query rispetto ai documenti) usando route_mappings. Consulta la documentazione di Router per scenari di routing avanzati e configurazioni personalizzate.
Dataset
Per questo esempio, utilizzo il dataset tomaarsen/llamaindex-vdr-en-train-preprocessed, un sottoinsieme inglese pre-elaborato di llamaindex/vdr-multilingual-train. Il dataset sorgente è stato rilasciato insieme al blogpost "Visual Document Retrieval Goes Multilingual" di LlamaIndex e consiste in circa 500.000 campioni multilingue query-immagine raccolti da PDF pubblici di internet, con query generate sinteticamente usando VLM (gemini-1.5-pro e Qwen2-VL-72B).
La mia versione pre-elaborata filtra a 53.512 campioni inglesi e risolve 4 dei 16 'hard negatives' basati su ID per campione in immagini reali di screenshot di documenti, in modo che possa essere usata direttamente per l'addestramento senza ulteriore pre-elaborazione. Questo rende il dataset immediatamente utilizzabile per il finetuning, riducendo i passaggi preliminari necessari:
from datasets import load_dataset
dataset = load_dataset(
"tomaarsen/llamaindex-vdr-en-train-preprocessed",
name="full",
)
Il trainconfig contiene i primi 10.000 campioni, e l'evalconfig contiene i successivi 300 campioni (è disponibile anche un fullconfig con tutti i 53.512 campioni). Per l'addestramento, seleziono query, image e negative_0 per formare triplette (anchor, positivo, hard negative). Includere hard negative aggiuntivi migliorerebbe probabilmente il segnale di addestramento, ma ogni negativo extra aumenta anche l'utilizzo della memoria e il tempo di addestramento, quindi mi attengo a uno. Per la valutazione, mantengo tutti e quattro gli hard negative per query per costruire un corpus di recupero più impegnativo (maggiori dettagli nella sezione 'Evaluator').
Proprio come l'addestramento solo testuale, il formato del dataset deve corrispondere alla funzione di perdita scelta. Le regole sono le stesse:
- Una lista di dizionari per la funzione di perdita ContrastiveLoss o OnlineContrastiveLoss.
- Una lista di oggetti InputExample per MultipleNegativesRankingLoss o CachedMultipleNegativesRankingLoss.
Il 'data collator' chiama automaticamente model.preprocess(), che rileva la modalità di ciascun input e applica la pre-elaborazione appropriata. Non è necessaria alcuna tokenizzazione manuale o elaborazione delle immagini, semplificando ulteriormente la gestione dei dati multimodali.
Molti dataset di Hugging Face che funzionano immediatamente con Sentence Transformers sono stati etichettati con sentence-transformers, permettendoti di trovarli facilmente su https://huggingface.co/datasets?other=sentence-transformers.
Funzione di perdita
Per questo addestramento, utilizzo CachedMultipleNegativesRankingLoss, una scelta comune per i compiti di recupero. Accetta coppie (query, positivo) con un numero qualsiasi di colonne aggiuntive 'hard negative', da 0 a n, a condizione che ogni campione abbia lo stesso numero di negativi. Questa flessibilità permette di configurare la perdita in base alla disponibilità e alla qualità dei dati negativi nel dataset.
Durante l'addestramento, la funzione di perdita spinge la similarità di ogni query al suo positivo in su e la sua similarità a ogni negativo in giù. I negativi provengono da due fonti:
- In-batch negatives: tutte le altre entità positive nel batch corrente, tranne l'entità positiva della query.
- Hard negatives: colonne designate di negativi, specificamente incluse nel tuo dataset, che sono particolarmente difficili da distinguere dal positivo.
Più negativi per query significano un segnale di addestramento più forte, quindi una dimensione del batch maggiore migliora direttamente la qualità dell'addestramento. Oltre a ciò, la variante 'cached' della funzione di perdita utilizza il caching del gradiente per rendere possibili grandi dimensioni effettive del batch anche quando la memoria della GPU è limitata, un aspetto cruciale per i modelli multimodali che tendono ad essere molto grandi.
Il parametro mini_batch_size controlla quanti campioni vengono elaborati contemporaneamente durante i passaggi forward 'cached'. Per modelli multimodali di grandi dimensioni, impostarlo su un valore piccolo (ad esempio, 1) è importante per evitare errori di 'out-of-memory' senza sacrificare i benefici delle grandi dimensioni effettive del batch:
from sentence_transformers.losses import CachedMultipleNegativesRankingLoss
loss = CachedMultipleNegativesRankingLoss(
model=model,
scale=20.0, # Raccomandato per CMRNL
mini_batch_size=1, # Cruciale per i modelli multimodali di grandi dimensioni
)
Matryoshka Loss
Per produrre embedding che funzionino bene a più dimensionalità, avvolgo la funzione di perdita di base con MatryoshkaLoss. Questo addestra il modello in modo che il troncamento dell'embedding a un numero inferiore di dimensioni produca comunque buone prestazioni, offrendo un'efficienza notevole in termini di memoria e velocità inferenziale.
from sentence_transformers.losses import MatryoshkaLoss
loss = MatryoshkaLoss(
model=model,
loss=CachedMultipleNegativesRankingLoss(
model=model,
scale=20.0,
mini_batch_size=1,
),
matryoshka_dimensions=[2048, 1024, 512, 256, 128, 64],
)
Questo è particolarmente utile per i modelli multimodali, dove gli embedding possono essere grandi (2048 dimensioni per Qwen3-VL). Con l'addestramento Matryoshka, è possibile ottenere buoni risultati di recupero e ricerca anche utilizzando embedding troncati, riducendo i requisiti di memoria e calcolo senza compromettere l'efficacia del modello. Questa flessibilità rende i modelli addestrati più versatili per diverse applicazioni con vincoli di risorse variabili, permettendo di scegliere la dimensione dell'embedding più adatta alle proprie esigenze specifiche.