Articoli approfonditi sulla tecnologia che plasma il futuro.

Constrained Decoding: LLM come classificatori veloci

Il constrained decoding trasforma un LLM in un classificatore rapido. Logit masking, calibrazione e temperature scaling per decisioni affidabili.

Un raggio di luce che si divide in cinque raggi colorati attraverso dei cancelli, a illustrare l'output di token vincolato.
Mascherare il vocabolario costringe l'intera distribuzione di output del modello a passare attraverso una manciata di cancelli consentiti.

Ecco un trucco economico e silenziosamente utile: se ti interessa solo il primo token che un LLM emetterebbe, puoi porgli una domanda a risposta multipla e ottenere la risposta con un singolo forward pass. Constrained decoding — mascherare ogni token del vocabolario tranne i pochi che sei disposto ad accettare — trasforma un modello generativo in qualcosa che somiglia molto a un classificatore. Niente parsing di JSON, niente loop di retry, niente undici passi autoregressivi per produrre undici caratteri. Un passaggio, un softmax, una risposta con una probabilità associata a ciascuna opzione.

Questa idea circola da tempo con nomi come "decision models" o inferenza "system one", e di recente ha preso fuoco su Hacker News con una guida che mostra come costruirne uno a partire da un modello Qwen da 1,7 miliardi di parametri in circa quaranta righe di Python. I veterani del machine learning nei commenti, prevedibilmente, urlavano nelle tastiere: è un classificatore, ne abbiamo da quando esiste il percettrone. Hanno ragione, e allo stesso tempo mancano un po' il punto. La novità non è il concetto — è che ottieni un classificatore zero-shot da un modello linguistico general-purpose senza addestrare nulla. La questione ingegneristica interessante è quando questo approccio vincolato batte il semplice lasciar parlare il modello, e quando ti mente in silenzio con probabilità troppo sicure di sé.

Due modi per ottenere una risposta da un LLM

La modalità predefinita di interazione con un modello linguistico è la generazione. Fai una domanda, il modello emette token uno alla volta e, a valle, fai il parsing di ciò che è uscito. Se ti servono output strutturati, aggiungi uno schema: JSON mode, sampling vincolato da grammatica, macchine a stati finiti in stile outlines. Funzionano, ma il modello percorre comunque token per token l'intera risposta. Una semplice risposta a scelta multipla può richiedere undici passi di decoding, e ogni passo costa un forward pass completo su miliardi di parametri.

L'alternativa è non lasciarlo mai camminare. Dopo che il prompt è stato elaborato, guardi i logit sul vocabolario nell'ultima posizione, scarti tutto tranne gli ID dei token corrispondenti alle tue opzioni — diciamo "A", "B", "C", "D", "E" — e applichi il softmax solo su quelli. L'argmax è la tua previsione; i valori del softmax sono i punteggi. Costo totale: un forward pass, lo stesso prefill che stavi già pagando. Ecco il nucleo, adattato dall'approccio basato su Qwen che sta circolando:

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
model_name = "Qwen/Qwen3-1.7B"
options = ["A", "B", "C", "D", "E"]
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(
model_name, torch_dtype="auto", device_map="auto"
)
# First token the model would emit for each option
option_token_ids = [
tokenizer.encode(opt, add_special_tokens=False)[0] for opt in options
]
prompt = (
"What color is the sky?\n"
"A. Red\nB. Blue\nC. Green\nD. Purple\nE. I don't know\nAnswer:"
)
messages = [{"role": "user", "content": prompt}]
text = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True,
enable_thinking=False
)
inputs = tokenizer(text, return_tensors="pt").to(model.device)
with torch.no_grad():
logits = model(**inputs).logits[0, -1]
# Constrained decoding: softmax over only the option tokens
probs = torch.softmax(logits[option_token_ids].float(), dim=-1)
print(options[probs.argmax().item()])  # -> B

Questo è l'intero motore. Il vocabolario supera i 150.000 token e abbiamo ridotto la decisione a cinque numeri. Nessun gioco di temperatura di sampling in fase di generazione, nessun parser che possa fallire, nessun modo per il modello di inventare un'opzione che non è nella lista. Lo spazio di output è chiuso per costruzione.

È un classificatore, e va benissimo così

Diamo agli scettici il giusto merito. Quello che abbiamo costruito è un classificatore discriminativo su un insieme fisso di etichette — un discendente di idee che risalgono al percettrone di Rosenblatt degli anni '50, passando per la regressione logistica, fino a ogni rete neurale con testa softmax dell'era del deep learning. Se guardi bene, il constrained decoding è solo una lettura lineare dello stato nascosto finale, che è ciò che una classification head è sempre stata. L'MLE che da anni implora il team di addestrare semplicemente un classificatore ha ogni diritto di sentirsi un po' stizzito nel vedere questo venire ribattezzato "decision model".

Ma la storia mostra anche perché la versione nuova conta. I vecchi classificatori erano ristretti: raccoglievi dati etichettati, addestravi, ottenevi un modello che conosceva un solo compito e nient'altro. La ragione per cui i sistemi basati su LLM continuano a mangiarsi compiti che "dovrebbero" usare un classificatore vero e proprio è la proprietà zero-shot — il modello base ha già assorbito abbastanza del mondo che un prompt equivale ai dati di addestramento. Quando ho testato una configurazione del genere su un holdout di CommonsenseQA, il modello da 1,7B si è attestato intorno al 59% di accuratezza senza alcun fine-tuning, salendo a circa il 62% dopo un rapido tuning sullo split di training. Non è lo stato dell'arte, ma è costato un pomeriggio e nessun dato etichettato mio. Un classificatore costruito ad hoc potrebbe fare meglio; avrebbe però richiesto una pipeline, un dataset e una strategia di riaddestramento ogni volta che cambiano le etichette.

Qui torna utile una distinzione che viene dal vecchio dibattito tra modelli generativi e discriminativi. I classificatori generativi modellano l'intera distribuzione e sono dispendiosi ma flessibili; quelli discriminativi modellano il confine decisionale e sono efficienti ma rigidi. Il constrained decoding è un ibrido curioso: un modello generativo piegato a un uso discriminativo al momento dell'inferenza. Ottieni l'efficienza della lettura discriminativa con l'ampiezza del pretraining generativo. Questa combinazione non era davvero disponibile prima, anche se i pezzi sono antichi.

Dove il constrained decoding vince

  • Latenza. Un forward pass invece di N passi autoregressivi. Su modelli piccoli è la differenza tra "abbastanza veloce per un percorso di richiesta" e "serve una coda". Chi esegue modelli decisionali puri nel browser riporta risposte sotto i 200ms — prova a fare lo stesso con JSON generativo.
  • Correttezza strutturale. Il modello non può letteralmente produrre output al di fuori dell'insieme consentito. Niente JSON malformato, niente divagazioni tipo "La risposta è probabilmente B perché...", nessun livello di guardrail necessario per intercettare violazioni del formato.
  • Throughput. Poiché ogni richiesta è un singolo passaggio di forma identica, il batching è banale e prevedibile. Generare risposte di lunghezza variabile distrugge l'efficienza del batching.
  • Un punteggio per opzione. Ottieni l'intera distribuzione, non solo il vincitore. Questo apre la porta alla logica di astensione: se la probabilità più alta è sotto una soglia, passi il caso a un umano o a un modello più grande.

Il punto sull'astensione merita enfasi. Una risposta generativa è un unico artefatto di cui ti fidi o no. Una distribuzione di probabilità sulle opzioni ti permette di costruire un routing: i casi ad alta confidenza passano in automatico, quelli a bassa confidenza vengono escalati. È il pattern dietro molti sistemi di triage in produzione — instradamento dei ticket di supporto, pre-screening della moderazione dei contenuti, rilevamento dell'intento — ed è qui che questa tecnica si guadagna il suo posto. Se il tuo compito si scompone naturalmente in "scegli una di K etichette e dimmi quanto sei sicuro", il constrained decoding è quasi certamente lo strumento giusto.

Dove vince l'output generativo

Ora l'altro lato. Nel momento in cui il tuo compito non si adatta a un insieme fisso di etichette, il constrained decoding crolla. Se la risposta è un'entità in forma libera, un numero, un pezzo di codice o qualsiasi cosa composizionale, ti serve la generazione — eventualmente con vincoli di output strutturato, ma comunque generazione. C'è anche una perdita più sottile: il ragionamento. Quando un modello genera una catena di pensiero prima di rispondere, spesso fa sensibilmente meglio sulle domande difficili. La testa decisionale a passaggio singolo non ti dà alcun brogliaccio. Stai chiedendo un pensiero di sistema uno, veloce e intuitivo, e ottieni esattamente quello — compresi i suoi tipici fallimenti.

C'è poi il problema della sensibilità alla formulazione. Un classificatore vincolato su "A/B/C/D/E" misura in realtà la preferenza del modello per il testo di ciascuna opzione in quella posizione. Riformula leggermente l'opzione C, riordina la lista o cambia "Answer:" in "The best answer is" e puoi spostare i punteggi. Le risposte generative con ragionamento tendono a essere più robuste alle perturbazioni superficiali perché il modello deve impegnarsi sul contenuto, non solo su un token. Se stai valutando uno dei due approcci, perturba la presentazione e guarda cosa si rompe — è un test di robustezza economico e scomodo.

Due sentieri attraverso un paesaggio, uno diretto e uno tortuoso, a simboleggiare inferenza veloce contro inferenza deliberativa.
Il constrained decoding è la via diretta; la generazione con ragionamento è il sentiero lungo che a volte raggiunge un terreno più alto.

Il problema della calibrazione: i tuoi punteggi di confidenza mentono

Ecco la trappola in cui cade chiunque costruisca uno di questi sistemi. Ottieni probabilità da un softmax, quindi devono essere probabilità, giusto? Non lo sono. Sono la confidenza del modello che un dato token venga dopo, che è un'affermazione sul linguaggio, non sulla correttezza. Chiedi al modello "Dove troveresti più probabilmente un pipistrello?" con opzioni come "Grotta" e "Partita di baseball", e assegnerà qualcosa come 0,998 a "Grotta" — una domanda ambigua senza una risposta difendibilmente certa, risposta con una certezza quasi totale.

Se raggruppi le previsioni per confidenza su una valutazione reale, il quadro peggiora. In una run su CommonsenseQA, il bucket di confidenza 0,9–1,0 era corretto solo circa nel 70% dei casi, e quello 0,8–0,9 a malapena arrivava al 40%. Un modello ben calibrato dovrebbe avere ragione circa il 90% delle volte quando dice 0,9. Questo è sistematicamente troppo sicuro di sé — il che, a pensarci, rispecchia la vecchia osservazione che le reti profonde moderne sono in generale overconfident. Guo et al. hanno mostrato nel 2017 che i valori softmax di una ResNet semplice sono malamente mal calibrati rispetto alle reti superficiali degli anni '90. Tutto ciò che è vecchio torna nuovo; abbiamo solo reinventato il problema a 1,7 miliardi di parametri.

La soluzione, per fortuna, è anch'essa vecchia e quasi imbarazzantemente semplice: il temperature scaling. Dividi i logit per un singolo scalare T appreso, prima del softmax:

def scaled_probs(logits, option_ids, temperature):
selected = logits[option_ids].float() / temperature
return torch.softmax(selected, dim=-1)
# Fit T on a validation set by minimizing negative log-likelihood
# of the correct option. For the CommonsenseQA run above, T ~= 3.8
temperature = 3.7973
probs = scaled_probs(logits, option_token_ids, temperature)

T maggiore di 1 appiattisce la distribuzione; T minore di 1 la rende più netta. Stimare un solo numero su un set held-out ha trasformato quella miserabile tabella di calibrazione in qualcosa di onesto: il bucket 0,9–1,0 ora si attesta intorno al 95% di accuratezza, quello 0,5–0,6 intorno al 55%. L'accuratezza non cambia affatto — l'argmax è invariante rispetto a uno scaling monotono — ma ora i punteggi significano ciò che pensavi significassero. Se intendi instradare in base a quelle probabilità, calibra prima o non prenderti nemmeno la briga di raccoglierle.

Tarare la temperatura su un benchmark è barare?

Una domanda legittima emersa nella discussione: non è un po' p-hacking stimare T in modo che il modello risulti calibrato su un benchmark? Lo sarebbe, se lo stimassi sul test set. Fatto bene — stimare T su uno split di validazione e riportare la calibrazione su uno split di test held-out — è solo una regressione a un parametro, ed è lo standard in letteratura proprio per questo motivo. La disciplina che conta è la stessa di ovunque nel ML: mantieni gli split onesti e diffida di qualsiasi affermazione di calibrazione misurata su dati che hanno toccato la stima. Se il dominio di deployment si discosta da quello di valutazione, anche il tuo T si sposta, quindi la ricalibrazione va inclusa nel loop di manutenzione insieme al resto.

Questo è in realtà un caso di un problema più ampio: il divario tra ciò che un sistema è specificato a significare e ciò che effettivamente calcola, un divario che ho già sostenuto essere dove vivono i bug. La "confidenza" è specificata come probabilità di correttezza; l'implementazione ti dà la plausibilità del token successivo. Il temperature scaling è una pezza su quel divario, non una sua risoluzione. Tieni a mente questa distinzione ogni volta che sei tentato di collegare gli output del softmax a una soglia di alerting.

Una procedura decisionale pratica

Quando scelgo oggi tra le due modalità, passo in rassegna una breve checklist:

  1. L'output è un insieme fisso di etichette? Se sì, il constrained decoding è sul tavolo. Se no, genera.
  2. Mi serve il ragionamento per ottenere un'accuratezza accettabile? Prototipa entrambi. Se la testa a passaggio singolo resta indietro rispetto alla chain-of-thought oltre quanto puoi tollerare, vince la generazione nonostante la latenza.
  3. Mi servono punteggi per opzione per routing o astensione? Se sì, constrained decoding più temperature scaling costa quasi nulla e la generazione non ti dà nulla di paragonabile.
  4. Quanto è grande l'insieme delle etichette? Cinque opzioni sono banali; cinquemila sono territorio da retrieval. Con spazi di etichette grandi, incorpora le etichette, crea una shortlist con la vector search e solo allora usa il modello per scegliere tra i finalisti — il trucco di scalabilità dei classificatori che è standard dall'era dei sistemi di raccomandazione.
  5. Le etichette cambieranno spesso? La proprietà zero-shot è il punto centrale. Se le etichette cambiano ogni settimana, un classificatore riaddestrato è un peso di manutenzione; una modifica al prompt no.

Il softmax del modello è un'affermazione su quale token viene dopo, non su ciò che è vero. Calibralo, o non fidarti.

Un'ulteriore considerazione: la dimensione del modello interagisce con la scelta. La testa a passaggio singolo di un modello piccolo è abbastanza economica da girare su ogni richiesta, anche lato client; la gente sta già distribuendo modelli decisionali che girano nel browser con risposte sotto i 200ms. Un design a cascata — modello piccolo vincolato per l'80% dei casi facili, modello generativo grande per la coda difficile — spesso batte entrambi gli estremi sia in costo sia in accuratezza. È lo stesso istinto dietro lo speculative decoding, applicato a livello di sistema anziché di token.

La mia raccomandazione

La mia posizione: se il tuo compito è davvero una decisione a scelta fissa — routing, triage, intento, valutazione a risposta multipla — costruisci la testa decisionale vincolata e non guardarti indietro. Il vantaggio di latenza da solo la giustifica, le garanzie strutturali eliminano un'intera classe di errori di parsing, e i punteggi per opzione ti danno una logica di routing che la generazione non può eguagliare. Ma tratta il softmax grezzo come uno strumento non calibrato. Fai il fine-tuning sul tuo compito reale se riesci a mettere insieme anche un dataset modesto, stima una temperatura su uno split di validazione pulito e verifica la calibrazione su dati che la stima non ha mai visto.

Riserva l'output generativo — con vincoli di output strutturato dove servono — ai compiti davvero composizionali o che traggono beneficio da un ragionamento visibile. E non lasciare che nessuno ti venda il "decision model" come categoria nuova: è un classificatore con il soprabito di un LLM, discendente di sessant'anni di modellazione discriminativa, ed è più utile proprio quando rispetti abbastanza quella genealogia da fare il lavoro di calibrazione che i veterani hanno sempre preteso. Gli strumenti sono nuovi. La disciplina no.