./run fine-tuning-estrazione-knowledge-graph-lezione-dati
Relation F1 a 0.38: quando la lezione non era il modello, ma i dati
Diario tecnico di un fine-tuning: LoRA su un modello da 27B per estrarre knowledge graph da documenti disordinati. La svolta non è stata un modello più grande, ma ri-annotare i dati.
- status
- draft
- project
- crimezero
- updated
- 2026-07-07
- tags
Questo è il progetto di cui posso raccontare meno nei dettagli, e va bene così: il dominio resta fuori. Ma la lezione tecnica la posso spiegare tutta, e vale per chiunque lavori con gli LLM. In una riga: prima di cambiare modello, guarda i dati.
Il compito
Testo lungo, sporco, in italiano → un knowledge graph in JSON. Ogni documento è spezzato in segmenti; il target di ognuno è un oggetto con questa forma:
entities: lista di{name, type}— nodi tipati con nome canonicorelations: lista di{subject, predicate, object, evidence_text}— archi diretti tipati, più la citazione testuale che li prova
Con un vincolo duro imposto nello schema: subject e object devono essere esattamente un name presente in entities — il grafo deve essere internamente coerente. Il vocabolario è chiuso: un enum fisso di tipi entità e predicati.
Il modello e il fine-tuning
Parto da un modello base denso da ~27B, e faccio LoRA/SFT (PEFT + TRL). Nessun addestramento da zero: aggiorno lo 0.58% dei parametri.
mods7 = ["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"]
target = r".*language_model.*\.(" + "|".join(mods7) + r")$" # salta la vision tower
LoraConfig(r=32, lora_alpha=64, lora_dropout=0.1,
target_modules=target, bias="none", task_type="CAUSAL_LM")
# bf16, grad-checkpointing, lr=2e-4 cosine, micro_bs=1 x grad_accum=8,
# cutoff 16384 token, ~4 epoche, best su eval_loss + early stopping
Il servizio è vLLM con multi-LoRA: il base resta residente una volta, gli adapter si attaccano come moduli nominati — più fine-tuning condividono lo stesso base. Gira su HPC on-prem (2 GPU classe H200, Slurm), bindato solo su loopback, tracciato in MLflow — dove finiscono solo aggregati, mai testo grezzo o generazioni.
La svolta (che è tutto il progetto)
La v1 l’ho addestrata, congelata e messa in produzione. Funzionava. E poi ho fatto l’errore classico: pensare che per migliorare servisse un modello più grande.
Sbagliato. Guardando dove sbagliava, il quadro era netto:
- Entità: F1 ~0.68-0.71, decente.
- Relazioni: F1 ~0.38-0.41, debole.
E la debolezza era concentrata nella coda rara: i predicati con meno di 500 esempi segnavano ~0.00, con una correlazione chiara tra rarità e F1. La causa radice, quando l’ho trovata, era imbarazzante e istruttiva: lo schema ricco era stato aggiunto dopo che il corpus era già annotato. Documenti che erano esempi da manuale di una relazione nuova ne estraevano ~0, perché a mancare non era il testo — era lo strato di annotazione.
Quindi la leva non era “più documenti” (provato: rende ~0). Era ri-annotare il testo esistente sotto lo schema nuovo.
La v2 è ingegneria dei dati, non del modello
Tre mosse, tutte quantificate:
- Allargamento schema — nuovi predicati enumerati dai dati, mai inventati.
- Ri-annotazione fine — stesso testo, etichette più nette (rimappare il volume di un catch-all grossolano sui figli direzionali precisi).
- Densificazione della coda — predicati prima vuoti riempiti con ri-annotazione delta su ~1.800 chunk candidati.
E una disciplina che mi è piaciuta molto: learnable vs measurable come vincolo di split. Il test set è scelto da un selettore greedy deterministico che deve coprire ogni predicato prioritario con ≥30 istanze di test (misurabilità) senza far scendere nessun predicato sotto le 50 in training (apprendibilità). Dove i due obiettivi confliggono, il predicato è dichiarato “apprendibile ma non misurabile su questo split” — tenuto e addestrato, escluso dalla metrica. Non buttato.
Misurare onestamente
- Micro P/R/F1 separati per entità e relazioni, con matching greedy uno-a-uno.
- Scoring alias-aware: dà credito se la forma corrisponde a qualsiasi variante nota dell’entità gold, non solo al nome canonico — separa i veri errori dalla semplice varianza di naming (relation F1 0.38→0.41).
- Due audit oltre l’F1: campiono i falsi positivi e controllo se entrambi gli estremi e la citazione compaiono davvero nel chunk (≈47% degli “errori” erano estrazioni corrette mancanti dal gold — incompletezza dell’annotazione, non del modello); e verifico che l’
evidence_textdei veri positivi sia verbatim nel testo (~87.5%, un check anti-allucinazione).
Cosa ho imparato
Il base era già grande, e le sue entità erano forti; scalarlo non avrebbe fabbricato relazioni che il gold non conteneva. Gli audit lo dicevano chiaro: una fetta grossa dei “falsi positivi” erano estrazioni giuste assenti dall’annotazione. È lo stesso pattern del record pubblico (TACRED→Re-TACRED, +16 F1 solo pulendo il gold, architettura invariata).
Poco eccitante da mettere in una slide. Ma è quello che ha spostato l’ago: in un progetto di estrazione, il prodotto non è il modello — è il dataset curato che ci sta dietro. Il modello è sostituibile. Il gold standard costruito bene, no.
Per riservatezza, dominio e contenuti restano fuori dal diario. Racconto il metodo, non la materia.