№ xlii L'Almanacco di GST · EN IT

Enrico·rubbo.li

Tech · Longevity · Mercati · Opinioni Enrico Rubboli, propr. Dubai, UAE
← I · Scritti
essay July 6, 2026 7 min

Build a Tiny LLM in Go, Parte 2: Il muro contro cui si infrange il conteggio

La Parte 1 ci ha lasciato con un modello che scrive quasi-parole per poi perdersi, poiché ricorda esattamente una sola lettera di ciò che ha scritto. La soluzione appare ovvia: ricordare di più. Proviamo dunque a farlo, per davvero, e vediamo fin dove si riesce ad arrivare prima di precipitare nel vuoto.

Ricordare due lettere, poi tre

Il modello a bigrammi manteneva una tabella indicizzata per un singolo carattere: per ciascuno dei nostri settanta caratteri, c’era una riga contenente il conteggio di ciò che veniva dopo. Per ricordare due lettere, si indicizza invece la tabella per coppie di caratteri. Ora la domanda non è più “cosa segue la t”, bensì “cosa segue th”, e la risposta per th è dominata dalla e, poiché “the” è la parola più comune in inglese.

Questo approccio funziona oggettivamente meglio. “Cosa segue th” è un interrogativo più preciso rispetto a “cosa segue la t”, di conseguenza le ipotesi risultano più accurate e il testo mantiene una coerenza per qualche istante in più. Tre lettere funzionano ancora meglio: “cosa segue the” punta con forza verso uno spazio. Ogni lettera di contesto aggiunta rende la previsione meno casuale.

Dunque, si dovrebbe semplicemente continuare su questa strada. Ricordando dieci lettere, o magari venti, il modello dovrebbe diventare sempre più intelligente. In teoria dovrebbe essere così. Il problema non risiede nel fatto che il contesto smetta di essere utile, bensì nelle dimensioni della tabella.

Facciamo i calcoli

La tabella per una singola lettera aveva una riga per carattere, per un totale di settanta righe. Quella per due lettere richiede una riga per ogni coppia di caratteri. Quante coppie esistono? Settanta possibilità per la prima lettera e settanta per la seconda, ovvero settanta moltiplicato per settanta: 4.900 righe. Fin qui, tutto bene.

Tre lettere: settanta per settanta per settanta, circa 343.000 righe. Le dimensioni iniziano a farsi importanti, ma un computer non fa una piega.

Lo schema è chiaro: ogni lettera di memoria in più moltiplica il numero di righe per settanta. L’intero problema si riassume in questa frase. Si tratta dell’unica funzione al centro di questa parte, ed è abbastanza breve da poter essere letta per intero:

// NGramTableSize returns how many rows a full lookup table would need to
// remember the previous n characters: vocabSize^n. Returned as float64 because
// the true count overflows int64 almost immediately, which is exactly the
// point. Counting cannot scale to real context; the model must learn instead.
func NGramTableSize(vocabSize, n int) float64 {
	return math.Pow(float64(vocabSize), float64(n))
}

Moltiplicare per settanta all’infinito genera una crescita esponenziale, e la crescita esponenziale è la forza più sottovalutata nell’informatica. Eseguendo la demo, è possibile osservare il fenomeno in tempo reale:

go run ./cmd/stage2_context
context | table rows needed
      1 | 70
      2 | 4.9e+03
      3 | 3.43e+05
      5 | 1.68e+09
     10 | 2.82e+18
     20 | 7.98e+36

Dieci caratteri di memoria, che non compongono nemmeno una parola lunga, richiedono 2,82 miliardi di miliardi di righe. Venti caratteri, l’equivalente di una breve frase, necessitano di un numero a trentasette cifre: all’incirca un 8 seguito da trentasei zeri. L’intero corpo umano contiene solo circa 10^27 atomi. Una tabella di ricerca in grado di ricordare una frase di venti caratteri avrebbe bisogno di miliardi di volte più righe di quanti siano gli atomi del corpo umano, e la demo lo dice chiaramente quando la si esegue.

Inoltre, occorre tenere a mente quale sia l’obiettivo. Il modello con cui si conclude la serie possiede una finestra di contesto di 128 caratteri. Una tabella di conteggio per una simile ampiezza richiederebbe 70^128 righe, un numero talmente smisurato che scriverlo per esteso richiederebbe più tempo della lettura di questo stesso paragrafo. L’approccio basato sul conteggio non diventa semplicemente dispendioso. Diventa fisicamente impossibile, e lo fa quasi subito.

Due modi in cui fallisce, non uno

L’esplosione dimensionale non riguarda soltanto lo spazio di archiviazione. Nasconde infatti un secondo fallimento, più silenzioso.

Supponiamo che, in qualche modo, si disponga dello spazio necessario. Per compilare la riga relativa a un contesto di dieci caratteri come “the cat sa”, sarebbe necessario aver incontrato quegli esatti dieci caratteri nel testo di addestramento, e per giunta numerose volte, al fine di ottenere un conteggio affidabile di ciò che segue. Tuttavia, la maggior parte delle stringhe di dieci caratteri non compare nemmeno una volta in alcun libro, poiché il linguaggio è ricombinabile all’infinito. Di conseguenza, quasi ogni riga della gigantesca tabella rimarrebbe vuota, e una riga vuota non fornisce alcuna informazione. La tabella non è soltanto troppo grande per essere memorizzata. È troppo grande per poter essere mai riempita.

Questo è il muro. Il conteggio funziona a meraviglia per uno o due caratteri, ma come idea è già morta nel momento in cui si desidera ricordare un’intera parola. Non si può arrivare a padroneggiare il linguaggio attraverso la memorizzazione, perché il linguaggio presenta più contesti possibili di quanto l’universo abbia spazio per contenerli, e la maggior parte di essi non si presenterà mai due volte.

La via di fuga

Ecco il punto di svolta da cui dipende tutto ciò che segue, e si tratta a tutti gli effetti di un cambiamento di natura, non di grado.

La tabella di conteggio tratta ogni contesto come se fosse slegato dagli altri. La riga per “the cat sa” e quella per “the dog sa” sono celle completamente separate che non condividono nulla, sebbene chiunque possa intuire che dovrebbero prevedere quasi la stessa lettera successiva. Il conteggio non possiede la nozione che due contesti possano essere simili. Ciascuno di essi rappresenta unicamente un indirizzo all’interno di una tabella.

Cosa succederebbe se, invece di una tabella con una riga per ogni possibile contesto, si disponesse di un insieme più ristretto di numeri in grado di esaminare qualsiasi contesto e calcolare una previsione? Numeri capaci di cogliere la vicinanza tra “the cat sa” e “the dog sa”, in modo tale che imparare qualcosa sul primo insegni qualcosa anche sul secondo? A quel punto, non servirebbe più una riga per ogni contesto. Basterebbero numeri sufficienti a rappresentare gli schemi del linguaggio, e gli schemi sono infinitamente meno numerosi dei contesti.

Quei numeri hanno un nome: si chiamano pesi, e trovare i valori corretti per essi è ciò che si definisce apprendimento. Un modello dotato di qualche migliaio di pesi può rispondere alla domanda su cosa venga dopo anche per contesti che non ha mai visto, generalizzando a partire dagli schemi anziché cercare una corrispondenza esatta. Questa è la differenza tra memorizzare e imparare, ed è la differenza che intercorre tra una tabella di ricerca e una rete neurale.

Tutto ciò, tuttavia, solleva una questione complessa che finora è stata elusa. Se il modello non si limita a contare, se è un ammasso di numeri che calcola un’ipotesi, come si fa a trovare i numeri giusti? Ce ne sono migliaia. Non si può provare ogni singola combinazione, né li si può contare. Bisogna in qualche modo indirizzarli, partendo da valori casuali e privi di senso verso valori che producano buone previsioni.

Questa azione di indirizzamento prende il nome di discesa del gradiente, e costituisce il vero motore all’interno di ogni rete neurale. Nella Parte 3 lo costruiremo: vedremo cosa significhi per un modello essere “sbagliato” sotto forma di un singolo numero, e come spingere migliaia di pesi, tutti in una volta, nella direzione che li renda un po’ meno sbagliati. È l’unica parte della serie in cui si guarderà la matematica dritto negli occhi. Ed è anche la parte in cui il programma smette di contare e inizia a imparare.

Il codice per questa parte si trova in cmd/stage2_context su github.com/erubboli/go-tiny-llm.