SLM · Studio e Proposta
Fondamenti tecnici dei parametri di fine-tuning, analisi del corpus letterario, framework di valutazione per confrontare Run #3, Run #4 (RoPE) e il modello specializzato.
Il fine-tuning (continued pre-training) parte da un modello già addestrato. L'obiettivo non è re-imparare la lingua italiana, ma spostare la distribuzione di probabilità verso un registro specifico — in questo caso, prosa narrativa non accademica. Ogni parametro controlla un aspetto di questo spostamento.
Regola il ritmo con cui i gradienti del corpus letterario sovrascrivono la rappresentazione interna già formata. Un valore troppo alto cancella la conoscenza enciclopedica (catastrofic forgetting); troppo basso e il segnale del corpus non si propaga.
Derivazione teorica: LR_ft = √(n_ft / n_pre) × LR_pre
= √(925 K / 47 800 K) × 3×10⁻⁴ = √0.0194 × 3×10⁻⁴ ≈ 0.139 × 3×10⁻⁴ ≈ 4.2×10⁻⁵
Il range 5×10⁻⁵ – 1×10⁻⁴ copre questa stima con un margine conservativo.
Segnale di allarme: se la val loss su un probe set Wikipedia aumenta di più del 10% rispetto al baseline (attuale perplexity ~59), il LR è troppo alto.
Con batch 8 e seq 256 ogni step vede 8 × 256 = 2 048 token. Il corpus ha 925 K token: un'epoca = ⌊925 000 / 2 048⌋ = 452 step.
Ridurre il batch serve principalmente a ottenere più aggiornamenti per token (452 step/epoca vs 226 con batch 16), aumentando la granularità dell'adattamento. Il gradiente è più rumoroso, il che agisce come regolarizzatore implicito contro l'overfitting su un corpus piccolo.
Il checkpoint di Run #3 contiene pos_encoding.pe con shape [1, 256, 512]. Cambiare seq_len richiederebbe di reinizializzare il positional encoding, distruggendo l'allineamento tra le posizioni imparate durante il pretraining. Il modello userebbe pesi posizionali randomici per le posizioni esistenti o nuove.
Questa è una limitazione dell'approccio sinusoidale fisso. RoPE (Run #4) non ha questa restrizione: le rotazioni sono calcolate al volo in base alla posizione, quindi seq_len è modificabile senza ricaricare pesi.
Il warmup serve a portare il LR da zero al massimo senza destabilizzare i momenti di AdamW nel momento iniziale. Nel pretraining, il modello parte da pesi casuali e l'ottimizzatore non ha storia: il warmup lungo (200 step) è necessario.
Nel fine-tuning, lo stato dell'ottimizzatore (m₁, m₂) viene caricato dal checkpoint. I momenti sono già orientati verso le direzioni del pretraining Wikipedia. Un warmup breve (50 step) è sufficiente per smorzare la transizione verso i nuovi gradienti letterari senza perdere questa "memoria" dell'ottimizzatore.
Con 925 K token e batch 8: il range 700–1 200 corrisponde a 1.5–2.7 passaggi sul corpus. Oltre le 3 epoche (1 356 step) il rischio di overfitting aumenta significativamente su un corpus di questa dimensione.
Non è un numero fisso: la val loss è il segnale corretto. Salvare checkpoint ogni 100 step e fermarsi quando la val loss inizia a risalire e rimane sopra il minimo per due checkpoint consecutivi.
Il val set letterario misura l'adattamento allo stile target. Ma questa metrica da sola è insufficiente: un modello che dimentica Wikipedia avrà val loss bassa sul letterario semplicemente perché ha "dimenticato" ogni alternativa.
È necessario un secondo val set: un probe di ~50 K token da Wikipedia IT (estratti dal corpus di pretraining, tenuti da parte). La val loss su questo probe deve rimanere entro il 5–8% del baseline (perplexity attuale ~59 → non oltre ~65).
python3 train.py \ --resume \ --data data/corpus_ft.txt \ --tokenizer bpe-spm \ --preset medium \ --seq 256 \ --batch 8 \ --steps 1200 \ --lr 7e-5 \ --min-lr 7e-6 \ --warmup 50 \ --eval-interval 100 \ --device mps
| Parametro | Pretraining | Fine-tuning | Motivazione |
|---|---|---|---|
| max_lr | 3×10⁻⁴ | 7×10⁻⁵ | scala sul ratio dei corpus |
| min_lr | 3×10⁻⁵ | 7×10⁻⁶ | mantiene il rapporto 10× |
| warmup | 200 | 50 | ottimizzatore già caldo |
| batch | 16 | 8 | più step/epoch, regolarizzazione |
| seq | 256 | 256 | vincolato dal checkpoint |
| steps | 30 000 | 700–1 200 | 1.5–2.7 epoche sul corpus |
| tempo M2 | ~230 h | ~7–12 h | stima a ~35 s/step con batch 8 |
Il corpus attuale copre un asse stilistico coerente: prosa italiana non accademica. I testi identificati per campionamento:
| Testo/Autore | Registro | Stima token |
|---|---|---|
| Amedeo Balbi (astrophysics) | divulgazione scientifica narrativa | ~150 K |
| Narrativa contemporanea italiana (prima persona) | emotivo, confessionale | ~200 K |
| Elena Ferrante — L'Amica Geniale | narrativa femminile, Napoli | ~200 K |
| Il Colpo Gobbo + hard-boiled noir | crime/heist, prima persona | ~130 K |
| Traduzione Vonnegut + noir americano | ironia dark, americano tradotto | ~100 K |
| corpus.txt — Cercando Natasha | techno-fiction, prima persona | ~145 K |
| Totale attuale | — | 925 K |
Regola empirica per CPT: il corpus di fine-tuning dovrebbe essere il 5–10% del volume del pretraining per un adattamento stabile. Con 47.8 M token Wikipedia: target minimo 2.4 M, ottimale 4.8 M. Il gap attuale è di 1.5–3.9 M token.
| Fonte | Volume stim. | Registro | Coerenza stilistica | Note |
|---|---|---|---|---|
| Altri EPUB dallo stesso asse narrativa contemporanea IT, noir, divulgazione |
300 K–1 M | contemporaneo | alta | Tool esistente: epub_to_text.py. Massimo allineamento con il corpus attuale. |
| Project Gutenberg IT Pirandello, Svevo, Deledda, Fogazzaro |
500 K–2 M | letterario classico | media | Italiano ottocentesco/novecentesco. Distanza linguistica dal contemporaneo. Utile per volume, rischio di spostare il registro verso l'arcaico. |
| Testi personali manoscritti, blog, note lunghe |
10 K–200 K | voce individuale | massima | Segnale più forte per un writing assistant. Anche 50 K token di testi propri valgono più di 500 K di Gutenberg per questo obiettivo. |
| Wikipedia IT slice (anti-forgetting) 50 K token dal corpus di pretraining |
50 K | enciclopedico | ancora | Non per ampliare il registro ma per ridurre il forgetting. Mischiare 10–15% di Wikipedia nel corpus di fine-tuning mantiene il modello ancorato alla conoscenza del mondo. |
| Long-form journalism IT Internazionale, Limes, Il Post (articoli lunghi) |
300 K–1 M | saggistico-narrativo | media | Registro intermedio tra divulgazione Balbi e Wikipedia. Aggiunge densità informativa con stile narrativo accessibile. Richiede scraping. |
Run #3 è un modello di linguaggio autocompletivo, senza instruction fine-tuning né RLHF. Questa distinzione è fondamentale per calibrare le aspettative.
| Capacità | Run #3 ora | Post fine-tuning | Mai (senza arch. change) |
|---|---|---|---|
| Generazione testo italiano grammaticale | ✓ alta qualità | ✓ registro raffinato | — |
| Conoscenza enciclopedica (geografia, storia) | ✓ presente ma imprecisa | ⚠ parziale perdita | — |
| Continuazione stilistica (stesso registro del prompt) | ⚠ tende all'enciclopedico | ✓ narrativo | — |
| Risposta a domande fattuali accurate | ✗ allucinazioni | ✗ | senza RAG |
| Seguire istruzioni ("scrivi una email su...") | ✗ | ✗ | richiede SFT + chat template |
| Coerenza narrativa oltre 256 token | ⚠ degrada | ⚠ stessa finestra | senza ctx esteso |
| Generazione oltre seq=256 (extrapolazione) | ✗ PE sinusoidale | ✗ (Run #3) | ✓ Run #4 RoPE |
| Scoring perplexity su un testo dato | ✓ direttamente | ✓ | — |
| Metrica | Formula | Cosa misura | Target |
|---|---|---|---|
| Perplexity | PP = eval_loss | quanto il modello è "sorpreso" dal testo di test; più basso = migliore | Run #3: 59 → fine-tuning: <50 su literary val |
| Forgetting ratio | ΔPP/PP_base | incremento percentuale di perplexity sul probe Wikipedia dopo fine-tuning | <8% (safe) · <15% (accettabile) |
| Dist-N | |unique n-grams| / |total n-grams| | diversità lessicale nelle generazioni; basso = testo ripetitivo | Dist-4 > 0.70 |
| Rep-3 | 3-gram ripetuti / totale | tasso di ripetizione in una generazione; alto = loop o degenerazione | <0.05 (5%) |
| Extrapolation loss | val_loss a seq=512 | solo per confronto Run #3 vs Run #4 — la PE sinusoidale degrada fuori distribuzione | Run #4 << Run #3 a seq 512 |
20 prompt fissi, 5 categorie, usati con tutti i modelli comparati. Temperatura 0.8, top-k 40, 128 token generati.
Per ogni prompt, votare 1–5 su quattro dimensioni. Valutazione cieca: shuffle dei modelli, nessuna etichetta visibile.
| Dimensione | 1 — scarso | 3 — accettabile | 5 — ottimo |
|---|---|---|---|
| Correttezza grammaticale | frasi spezzate, errori di accordo | grammatica corretta, alcune costruzioni goffe | nativo, fluente, costruzioni complesse corrette |
| Coerenza di registro | salta tra enciclopedico, narrativo, frammentato | registro prevalente ma con discontinuità | registro mantenuto per tutti i 128 token |
| Coerenza tematica | il soggetto cambia entro 20 token | il soggetto si mantiene ma devia | il filo narrativo/tematico è sostenuto |
| Aderenza stilistica al corpus | suona come Wikipedia | accenni al registro target ma ibrido | riconoscibile come prosa narrativa italiana |
head -c 50M data/corpus_bpe.txt > data/wiki_probe.txttools/slm_to_gguf.py dopo la comparazione.Il Continued Pre-Training (CPT, sezioni 1–5) e il Supervised Fine-Tuning (SFT) sembrano la stessa operazione — "training su un checkpoint esistente" — ma differiscono in modo sostanziale nel formato dei dati, nella funzione di loss e negli obiettivi. Capire la differenza è necessario per progettare train_sft.py correttamente.
Pre-training / CPT (Causal Language Modeling su testo continuo): il corpus è una sequenza piatta di token. Il training chunka questa sequenza in finestre di seq_len token e per ogni finestra calcola la loss su tutti i token: ogni token deve predire il successivo. Non c'è struttura semantica esplicita tra le posizioni — il modello impara la distribuzione generale della lingua.
SFT (coppie prompt → completion): i dati sono strutturati in coppie dove il prompt è fisso (l'istruzione, il template parziale) e la completion è ciò che il modello deve imparare a produrre. Non si può trattarle come testo continuo, perché il modello imparerebbe anche a "riprodurre la domanda" — il che è inutile e introduce segnale di gradiente verso la distribuzione sbagliata.
| Aspetto | CLM / CPT | SFT |
|---|---|---|
| Input | testo continuo, nessuna struttura | coppie (prompt, completion) |
| Loss calcolata su | tutti i token | solo i token di completion |
| Dataset tipico | milioni – miliardi di token | centinaia – migliaia di esempi |
| Obiettivo | modellare la lingua in generale | orientare il comportamento su un task |
| Format file | .txt sliding window | .jsonl con campi prompt/completion |
ignore_index=-100)In PyTorch, F.cross_entropy(logits, labels) accetta un parametro ignore_index (default -100): ogni posizione in labels con valore -100 viene ignorata nel calcolo della loss — contribuisce zero al gradiente, nessun aggiornamento dei pesi.
In SFT si costruisce il batch così:
# Esempio batch SFT: una convocazione condominiale prompt = "Redigi la convocazione dell'assemblea ordinaria del 15/10/2026." completion = "Gentili condomini, si convoca l'assemblea ordinaria del condominio..." # Tokenizziamo la concatenazione input_ids = tokenize(prompt + "\n\n" + completion) # len(prompt_tokens) = 18, len(completion_tokens) = 64 # Labels: -100 sul prompt, token reale sulla completion (shiftati di 1) labels = [-100] * 18 + input_ids[19:] # completion[1:] è il target
Senza questa maschera il modello riceve gradiente anche dai token del prompt — impara a massimizzare la probabilità delle parole della domanda, ottimizzando la distribuzione sbagliata. La convergenza è molto più lenta e il modello tende a confondere cosa deve ricevere (prompt) con cosa deve produrre (completion).
labels[i] = input_ids[i+1]. L'implementazione corretta usa input_ids[1:] come labels e input_ids[:-1] come input, oppure si gestisce internamente con uno shift di offset. Errare su questo dettaglio è la causa più comune di training SFT che converge ma produce output errati.
Il pre-training ha codificato la distribuzione dell'italiano in 30 000 step su 47.8 M token. Il SFT spinge i pesi verso un sotto-dominio di questa distribuzione. Se la spinta è troppo forte — LR alta, molte epoche, dataset grande — il modello sovrascrive le rappresentazioni generali e "dimentica" come scrivere di altro.
| Condizione SFT | Forgetting atteso | Comportamento |
|---|---|---|
| 200–300 esempi, LR 1e-5, 3 epoche | trascurabile (<5%) | modello scrive ancora italiano generico correttamente |
| 1 000–5 000 esempi, LR 3e-5, 5 epoche | moderato (5–15%) | italiano generico leggermente degradato, dominio OK |
| 10 000+ esempi, LR 1e-4, 10+ epoche | severo (>20%) | il modello "parla solo" di condominio / moto / etc. |
Mitigazioni standard:
| Parametro | Pre-training | CPT letterario | SFT (Branch A) |
|---|---|---|---|
| max_lr | 3×10⁻⁴ | 7×10⁻⁵ | 1×10⁻⁵ – 3×10⁻⁵ |
| min_lr | 3×10⁻⁵ | 7×10⁻⁶ | 1×10⁻⁶ |
| warmup_steps | 200 | 50 | 10–30 |
| batch_size | 16 | 8 | 4–8 |
| epochs | ~2.6 pass sul corpus | 1.5–2.7 | 2–5 |
| loss target | tutti i token | tutti i token | solo completion |
| val set | Wikipedia slice | letterario + Wikipedia probe | domain test set + Wikipedia probe |
| Motivazione LR bassa | Evitare catastrophic forgetting. Il modello si trova già in un minimo buono — passi piccoli. | ||
Opzione A — completamento con separatore testuale (consigliata per Branch A)
# data/sft_condominio.jsonl
{"prompt": "Redigi una convocazione assemblea ordinaria. Data: 15/10/2026. Luogo: sala condominiale. ODG: bilancio preventivo, lavori straordinari.", "completion": "Gentili condomini,\n\nSi convoca l'assemblea ordinaria del condominio..."}
{"prompt": "Scrivi una diffida per morosità. Condomino: Mario Rossi, scala B, interno 4. Debito: €1.234,00 per quote condominiali arretrate.", "completion": "Raccomandata A/R\n\nEgregio Sig. Mario Rossi..."}
Il separatore \n\n (o "### Risposta:\n") nella concatenazione identifica il punto di switch da labels=-100 a labels=token. Nessun token speciale aggiunto al vocabolario. Nessun rischio di destabilizzare gli embedding esistenti.
Opzione B — chat template (ChatML / LLaMA-style)
<|im_start|>user Redigi una convocazione... <|im_end|> <|im_start|>assistant Gentili condomini... <|im_end|>
Richiede l'aggiunta di token speciali (<|im_start|>, <|im_end|>, user, assistant) al vocabolario BPE-SPM. I nuovi embedding vengono inizializzati casualmente e non hanno training — fonte di instabilità nelle prime iterazioni. Appropriato per modelli destinati a interfaccia chat. Overkill per Branch A.
Opzione C — testo puro con mascheratura by offset
Concatenazione raw senza separatore esplicito, con mascheratura calcolata dal numero di token del prompt. Fragile (un token in più/meno nel prompt sposta tutto) e non consigliata.
La val loss su testo continuo (usata in pre-training e CPT) è insufficiente per valutare l'SFT. Una val loss bassa dice che il modello assegna alta probabilità ai token del test set — non che il testo generato abbia la struttura corretta.
| Metrica | Cosa misura | Quando usarla |
|---|---|---|
| PPL su domain test set | adattamento al dominio SFT (20% holdout del dataset) | segnale primario durante training |
| PPL su Wikipedia probe | catastrophic forgetting | segnale secondario durante training |
| chrF / BLEU | sovrapposizione con reference su task template (convocazione, verbale) | post-training su completions di test set |
| Validità strutturale | la convocazione ha data + luogo + ODG? il JSON è parsabile? | rule-based check automatizzabile |
| Ispezione manuale | qualità percepita, appropriatezza del registro, errori factual | dopo ogni esperimento, su 5–10 prompt non visti |
SLM · Fine-tuning e Valutazione — ← Diario · Documentazione completa · Home