Un interprete bravissimo, appena assunto in un cantiere, traduce «tirante» con la parola sbagliata per tre giorni. Non è incompetente: è che il gergo non lo conosce ancora. Ai modelli di embedding capita la stessa cosa, ed è la causa più frequente — e la meno sospettata — di un sistema di ricerca semantica che restituisce documenti quasi giusti.
Quando un RAG risponde male, il riflesso è cambiare modello linguistico o riscrivere il prompt. Ma se il pezzo di documento che conteneva la risposta non è mai arrivato nel contesto, nessun modello e nessun prompt possono salvarti: il guasto è a monte, nel modello che trasforma testo in vettori. Questo tutorial mostra come adattarlo, in tre situazioni diverse a seconda dei dati che hai davvero.
Le tre strade si distinguono per un solo criterio: quanto materiale etichettato hai. La casella tratteggiata in fondo non è opzionale — è il passo che rende gli altri misurabili.
Prima di toccare qualcosa: misurare
Un adattamento senza metrica è una scommessa. Servono cinquanta o cento domande reali — prese dai ticket, dalle ricerche degli utenti, dalle email — con l’indicazione di quale documento contiene la risposta. Costa un pomeriggio ed è l’investimento con il rendimento più alto di tutto il progetto: senza, l’unica valutazione disponibile è «mi sembra migliore», che è notoriamente influenzata dal tempo che ci hai messo.
pip install sentence-transformers datasets
from sentence_transformers import SentenceTransformer
from sentence_transformers.evaluation import InformationRetrievalEvaluator
modello = SentenceTransformer("intfloat/multilingual-e5-base")
# corpus: {id_documento: testo}; query: {id_query: domanda}
# rilevanti: {id_query: {id_documento, ...}}
valutatore = InformationRetrievalEvaluator(
queries=query, corpus=corpus, relevant_docs=rilevanti, name="dominio",
)
print(valutatore(modello)) # nDCG@10, recall@k, MRR: il punto di partenza
Il numero che esce da qui è la baseline. Qualunque cosa venga dopo si giudica contro quella, e capita più spesso di quanto si creda che un adattamento fatto male la peggiori.
Qui diamo per noto che cos’è un embedding e perché due testi vicini di significato finiscono vicini nello spazio: se serve il fondamento, il libro lo costruisce da capo, dalla bag of words fino ai vettori contestuali.
La strada corta: hai delle coppie
Se puoi mettere insieme qualche migliaio di coppie (domanda, passaggio che risponde) — e spesso si possono ricavare da FAQ, ticket risolti, log di ricerca con i click — la strada è breve e funziona bene. La funzione di perdita da usare è MultipleNegativesRankingLoss, che la documentazione di sentence-transformers indica come scelta consigliata per dati di questa forma e descrive come «comunemente usata per addestrare i modelli di embedding più performanti».
from datasets import Dataset
from sentence_transformers import (
SentenceTransformerTrainer, SentenceTransformerTrainingArguments,
)
from sentence_transformers.losses import MultipleNegativesRankingLoss
dati = Dataset.from_dict({
"anchor": ["come si annulla una polizza?", "chi firma il verbale?"],
"positive": ["Per il recesso il contraente invia…", "Il verbale è sottoscritto dal…"],
})
perdita = MultipleNegativesRankingLoss(modello)
args = SentenceTransformerTrainingArguments(
output_dir="modello-dominio",
num_train_epochs=1,
per_device_train_batch_size=32, # il batch è un iperparametro vero, qui
warmup_ratio=0.1,
)
SentenceTransformerTrainer(
model=modello, args=args, train_dataset=dati,
loss=perdita, evaluator=valutatore,
).train()
Il commento sul batch non è cosmetico. Questa perdita costruisce gli esempi negativi dentro il batch: per ogni domanda, i passaggi corretti di tutte le altre domande del batch fanno da risposte sbagliate. Un batch di 8 chiede al modello di distinguere fra otto candidati, uno di 128 fra centoventotto: il compito è più difficile e il modello che ne esce è migliore. È per questo che la documentazione segnala CachedMultipleNegativesRankingLoss, la variante che permette di alzare il batch oltre quello che la memoria consentirebbe.
Una conseguenza da conoscere: se due domande del batch hanno per caso la stessa risposta, il modello viene penalizzato per averlo capito. Con corpora piccoli e ripetitivi vale la pena deduplicare prima.
Non hai coppie: TSDAE
Il caso più comune nelle aziende è avere molti documenti e zero domande. TSDAE — Transformer-based Sequential Denoising Auto-Encoder, di Wang, Reimers e Gurevych (2021) — impara solo dai testi: rovina una frase cancellandone parole a caso, chiede al modello di comprimerla in un vettore, e a un decoder di ricostruirla intera partendo da quel vettore. Per riuscirci, il vettore deve conservare il senso della frase, gergo compreso.
from datasets import Dataset
from sentence_transformers.losses import DenoisingAutoEncoderLoss
from sentence_transformers.datasets import DenoisingAutoEncoderDataset
frasi = [r.strip() for r in open("documenti.txt", encoding="utf-8") if r.strip()]
# la colonna "damaged" è l'ingresso, "text" il bersaglio da ricostruire
dati_tsdae = Dataset.from_dict({"text": frasi})
dati_tsdae.set_transform(
lambda lotto: {"damaged": [DenoisingAutoEncoderDataset.delete(t) for t in lotto["text"]]},
columns=["text"], output_all_columns=True,
)
perdita = DenoisingAutoEncoderLoss(
modello,
decoder_name_or_path="intfloat/multilingual-e5-base",
tie_encoder_decoder=True,
)
I numeri del paper, riportati come li scrivono gli autori: TSDAE supera gli approcci precedenti «fino a 6,4 punti» e raggiunge «fino al 93,1% delle prestazioni degli approcci supervisionati nel dominio», misurato su quattro insiemi di dati di domini eterogenei. La documentazione di sentence-transformers riporta su compiti semantici una media che passa da 52,3 a 56,5.
Il difetto è dichiarato nella documentazione stessa: il costo. Va eseguito un pre-addestramento sul proprio corpus prima dell’addestramento supervisionato, e sono due giri invece di uno.
Farsi le coppie da soli: GPL
La terza strada rovescia il problema: se mancano le domande, le si genera. GPL — Generative Pseudo Labeling, degli stessi autori più Nandan Thakur (2021) — prende ogni passaggio del corpus, fa scrivere a un modello generativo la domanda a cui quel passaggio risponderebbe, poi recupera passaggi concorrenti e fa pesare ogni coppia da un cross-encoder, che assegna un punteggio di pertinenza invece di un secco sì o no.
Quel punteggio morbido è il punto dell’intero metodo. Le domande generate automaticamente sono spesso mediocri, e un passaggio pescato come «negativo» a volte risponde alla domanda meglio di quello ufficiale: trattarli come vero e falso insegnerebbe al modello degli errori. Il cross-encoder li ordina invece di etichettarli, e l’errore si diluisce.
Gli autori riportano fino a 9,3 punti di nDCG@10 sopra il modello di partenza, su sei insiemi di dati di domini specialistici, e un ulteriore guadagno medio di 1,4 punti se al metodo si antepone TSDAE. La documentazione, sul proprio confronto di ricerca semantica, riporta una media che va da 45,2 senza adattamento a 51,4 con GPL e 52,4 combinando i due. L’implementazione di riferimento è quella del gruppo UKP, e vuole in ingresso il solo corpus.
Cosa portarti a casa
Tre avvertenze pratiche, tutte imparate nel modo scomodo.
Un modello adattato obbliga a reindicizzare. I vettori vecchi e i nuovi non vivono nello stesso spazio: appena cambi il modello, l’intero indice va ricalcolato. Su qualche migliaio di documenti è questione di minuti, su qualche milione è un’operazione da pianificare — e va pianificata anche la convivenza delle due versioni durante il passaggio.
L’adattamento è specializzazione, non miglioramento. Un modello portato sul gergo assicurativo diventa in genere peggiore sul resto. Se la tua ricerca copre più domini distanti, tenere modelli separati batte quasi sempre il tentativo di farne uno buono per tutto.
Prima di adattare, controlla il chunking. Capita spesso che il recupero fallisca non per colpa dei vettori ma perché i documenti sono spezzati male, e la risposta cade a cavallo fra due pezzi. È un pomeriggio di verifica e costa meno di un addestramento.
Se il recupero non porta il documento giusto, il modello linguistico più intelligente del mondo può solo inventare con maggiore eleganza.
La ragione per cui vale la pena occuparsene è di rapporto fra costo ed effetto. Cambiare il modello generativo di un RAG costa a ogni singola chiamata, per sempre; adattare l’embedding costa qualche ora di GPU una volta sola, e agisce sul pezzo della catena che decide che cosa il modello avrà davanti quando risponde.

I commenti sono riservati agli iscritti.
Accedi per commentare