Quando addestriamo una rete neurale per classificare un’immagine, prevedere la parola successiva in un testo o identificare frodi finanziarie, l’output grezzo che il modello ci restituisce è quasi sempre una serie di numeri all’apparenza incomprensibili, chiamati logits.
Valori come [12.5, -3.2, 0.4] non ci dicono nulla di utile finché non li traduciamo in un linguaggio che noi, e le metriche di valutazione del modello, possiamo interpretare matematicamente: le probabilità.
È qui che entra in gioco la funzione Softmax. Non si tratta di una semplice formula algebrica, ma del “ponte” fondamentale che trasforma i punteggi grezzi di un algoritmo in decisioni pesate e misurabili, costringendoli a sommare esattamente a [math]1[/math].
In questo articolo esploreremo la Softmax: partiremo dalla sua base matematica (che affonda le radici nella statistica), la implementeremo da zero in Python per svelare i problemi di stabilità numerica dei calcolatori e, infine, simuleremo il suo comportamento dinamico introducendo concetti avanzati come la “Temperatura”.
1. Dalla Matematica al Codice: Cos’è la Softmax
Da un punto di vista statistico, la funzione Softmax non è un’invenzione recente del deep learning. È di fatto la generalizzazione multivariata della funzione logistica (sigmoide) ed è strettamente legata alla distribuzione di Boltzmann utilizzata in meccanica statistica.
La sua definizione matematica per un dato elemento [math]z_i[/math] all’interno di un vettore di logits [math]\mathbf{z}[/math] è la seguente:
[math]\displaystyle \text{softmax}(z_i) = \frac{e^{z_i}}{\sum_{j} e^{z_j}}[/math]
Cosa fa esattamente questa formula?
- Prende ogni logit e ne calcola l’esponenziale ([math]e^{z_i}[/math]). Questo passaggio garantisce che tutti i valori diventino rigorosamente positivi, accentuando al contempo le differenze tra i numeri.
- Divide ogni valore esponenziale per la somma di tutti gli esponenziali del vettore. Questo è il passaggio di normalizzazione che garantisce che la somma finale sia pari al [math]100\%[/math] (ovvero [math]1[/math]).
👉 Logit nel Deep Learning: il ruolo dei punteggi lineari prima della Softmax
L’implementazione base in NumPy
Tradurre questa formula in codice Python è immediato grazie alla libreria NumPy:
import numpy as np
def softmax(x):
exp_x = np.exp(x)
return exp_x / np.sum(exp_x)
# Esempio pratico con tre logits
logits = np.array([2.0, 1.0, 0.1])
probs = softmax(logits)
print("Probabilità:", probs)
print("Somma totale:", np.sum(probs))
Output atteso:
Probabilità: [0.65900114 0.24243297 0.09856589]
Somma totale: 1.0
Il risultato è una distribuzione probabilistica perfetta. Il logit più alto ([math]2.0[/math]) ottiene la fetta maggiore di probabilità ([math]\sim 66\%[/math]), mentre i valori più bassi si accontentano del resto.
2. Stabilità Numerica: Il limite hardware e l’invarianza matematica
Il codice appena visto è matematicamente ineccepibile, ma informaticamente disastroso se applicato nel mondo reale. Proviamo a capire perché.
I computer moderni elaborano i numeri decimali utilizzando standard a virgola mobile (generalmente Float32 nel machine learning). Se provassimo a calcolare l’esponenziale di un logit anche solo moderatamente grande, ad esempio [math]1000[/math], la funzione np.exp(1000) andrebbe immediatamente in overflow, restituendo inf (infinito). Questo genererebbe valori NaN (Not a Number) a cascata, distruggendo l’intera rete neurale.
Per risolvere questo limite fisico dell’hardware, i data scientist sfruttano una bellissima proprietà matematica: l’invarianza per traslazione. Se sottraiamo una costante [math]c[/math] da tutti gli elementi del vettore, la distribuzione probabilistica finale calcolata dalla Softmax non cambia di una virgola.
Dimostrazione algebrica:
[math]\displaystyle \frac{e^{x_i – c}}{\sum e^{x_j – c}} = \frac{e^{x_i} \cdot e^{-c}}{\sum (e^{x_j} \cdot e^{-c})} = \frac{e^{-c} \cdot e^{x_i}}{e^{-c} \cdot \sum e^{x_j}} = \frac{e^{x_i}}{\sum e^{x_j}}[/math]
Come traduciamo questo artificio matematico in una Softmax stabile?
Scegliendo come costante [math]c[/math] il valore massimo del vettore. In questo modo il logit più alto diventerà [math]0[/math] (il cui esponenziale è serenamente [math]1[/math]), e tutti gli altri logit diventeranno negativi (i cui esponenziali tenderanno asintoticamente a [math]0[/math] senza mai generare overflow).
def softmax_stable(x):
# Trasliamo i valori sottraendo il massimo
x = x - np.max(x)
exp_x = np.exp(x)
return exp_x / np.sum(exp_x)
# Test con logits enormi
logits_esplosivi = np.array([1000, 1001, 1002])
print("Risultato stabile:", softmax_stable(logits_esplosivi))
Output:
Risultato stabile: [0.09003057 0.24472847 0.66524096]
Il calcolo avviene senza alcun errore, permettendo ai modelli di convergere in sicurezza.
3. L’uso industriale: PyTorch
Nel deep learning applicato, raramente si scrive la Softmax da zero se non per scopi didattici. Librerie come PyTorch e TensorFlow integrano versioni della Softmax altamente ottimizzate in C++ e CUDA, che gestiscono la stabilità numerica automaticamente in background.
import torch
import torch.nn.functional as F
logits = torch.tensor([2.0, 1.0, 0.1])
probs = F.softmax(logits, dim=0)
print("PyTorch output:", probs)
# Restituisce: tensor([0.6590, 0.2424, 0.0986])
4. Simulazione didattica: La “Competizione tra Classi”
Immaginiamo di aver addestrato una rete neurale per riconoscere gli animali. Le diamo in pasto un’immagine e, nel suo ultimo strato, la rete produce tre punteggi grezzi (i logits) per le classi “Gatto”, “Cane” e “Volpe”.
In questa fase, i neuroni stanno letteralmente “competendo” tra loro. Come facciamo a stabilire il vincitore in modo equo e misurabile? Usiamo la nostra implementazione stabile della Softmax per trasformare questi score in percentuali.
Il Codice della Simulazione
import numpy as np
import matplotlib.pyplot as plt
# Definiamo le classi e i punteggi grezzi in uscita dalla rete
labels = ["Gatto", "Cane", "Volpe"]
logits = np.array([2.5, 1.2, 0.3])
# Funzione Softmax (versione numericamente stabile)
def softmax(x):
x = x - np.max(x) # Previene l'overflow
exp_x = np.exp(x)
return exp_x / np.sum(exp_x)
probs = softmax(logits)
# Stampiamo i risultati formattati in percentuale
for l, p in zip(labels, probs):
print(f"{l}: {p:.2%}")
Output Atteso e Analisi dei Risultati
L’esecuzione del codice produrrà il seguente risultato in console:
Gatto: 72.29%
Cane: 19.70%
Volpe: 8.01%
Cosa dobbiamo notare in questo output?
L’aspetto più interessante della Softmax è la sua natura non lineare (dovuta all’esponenziale).
Analizziamo i numeri:
- L’effetto amplificatore: Il logit del Gatto ([math]2.5[/math]) è circa il doppio rispetto a quello del Cane ([math]1.2[/math]). Tuttavia, se guardiamo le probabilità finali, il Gatto non ha il doppio della probabilità, ma ne ha quasi quattro volte tanto ([math]72.29\%[/math] contro [math]19.70\%[/math]). La funzione esponenziale premia in modo sproporzionato il punteggio più alto, aiutando il modello a prendere una decisione netta.
- La gestione dell’incertezza: La Volpe ha ricevuto un logit molto basso ([math]0.3[/math]). Una semplice funzione logica come il calcolo del massimo (ArgMax) le avrebbe assegnato [math]0\%[/math]. La Softmax, invece, le riserva un [math]8.01\%[/math]. Questo è fondamentale: il modello ci sta dicendo che è abbastanza sicuro che sia un gatto, ma lascia una finestra aperta al dubbio, mantenendo intatta la ricchezza informativa della previsione.
5. Visualizzazione Grafica
I numeri in console sono utili, ma nel machine learning la visualizzazione è tutto.
Trasformiamo la nostra distribuzione probabilistica in un grafico a barre pronto per essere inserito in un report o in una dashboard.
import matplotlib.pyplot as plt
# Creiamo la figura impostando dimensioni ottimali
fig, ax = plt.subplots(figsize=(8, 4))
# Generiamo le barre con colori esadecimali personalizzati
colors = ['#3498db', '#2ecc71', '#e74c3c']
ax.bar(labels, probs, color=colors)
# Personalizzazione degli assi e dei titoli
ax.set_ylabel('Probabilità')
ax.set_title('Distribuzione Softmax: Previsione della Rete Neurale')
ax.set_ylim(0, 1.1) # Lasciamo spazio extra in alto per le etichette
ax.grid(axis='y', alpha=0.3)
# Rimuoviamo i bordi superiori e destri per un look più moderno
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
# Aggiungiamo le percentuali esatte sopra ogni singola barra
for i, v in enumerate(probs):
ax.text(i, v + 0.03, f"{v:.2%}", ha='center', fontweight='bold', color='#333333')
plt.tight_layout()
plt.show()

Commento al Grafico
L’output visivo prodotto da questo script è il manifesto di come lavora l’algoritmo Softmax. Osservando il grafico generato, salta subito all’occhio come la probabilità totale (pari a [math]1[/math] o [math]100\%[/math]) sia stata “spalmata” su tutte le categorie disponibili.
Nessuna barra scompare del tutto. Questa visualizzazione dimostra graficamente che la Softmax agisce come una giuria democratica: ascolta il parere (il logit) di ogni neurone, ma alla fine redige un verdetto normalizzato dove la somma di tutte le opinioni equivale all’unità perfetta.
La Dinamica dei Logits
Come si comporta la distribuzione al variare della distanza tra i valori di input?
scenari = {
"Scenario equilibrato": np.array([1.0, 1.2, 0.9]),
"Dominanza moderata": np.array([2.0, 1.0, 0.5]),
"Dominanza forte": np.array([5.0, 1.0, 0.2])
}
def softmax(x):
x = x - np.max(x)
exp_x = np.exp(x)
return exp_x / np.sum(exp_x)
for name, logits in scenari.items():
probs = softmax(logits)
print(name, probs)
Scenario equilibrato [0.31987306 0.39069383 0.28943311]
Dominanza moderata [0.62853172 0.2312239 0.14024438]
Dominanza forte [0.97414105 0.01784202 0.00801693]
- Scenario equilibrato ([math][1.0, 1.2, 0.9][/math]): I logit sono molto vicini tra loro. La Softmax restituirà una distribuzione quasi uniforme (es. [math]31\%[/math], [math]39\%[/math], [math]29\%[/math]). Il modello è indeciso.
- Dominanza forte ([math][5.0, 1.0, 0.2][/math]): Il primo logit stacca nettamente gli altri. L’esponenziale amplifica questa distanza in modo non lineare, portando la prima classe a sfiorare il [math]97\%[/math] di probabilità.
5. Il parametro “Temperatura”: Controllare la confidenza del Modello
Andiamo oltre le basi esplorando un concetto che è il cuore pulsante dei moderni Large Language Models (LLM) come ChatGPT o Claude: la Temperatura ([math]T[/math]).
La Softmax può essere modificata introducendo un parametro al denominatore dei logits prima dell’esponenziazione:
[math]\text{softmax}(\mathbf{z}/T)[/math]
Questo parametro modella la “confidenza” o la “fiducia” del modello nelle proprie previsioni, permettendoci di controllare quanto debba essere conservativo o creativo.
def softmax_temp(x, T=1.0):
x = x / T
x = x - np.max(x) # Sempre stabile!
exp_x = np.exp(x)
return exp_x / np.sum(exp_x)
logits = np.array([2.0, 1.0, 0.1])
for T in [0.5, 1.0, 2.0]:
print(f"Temperatura {T}:", np.round(softmax_temp(logits, T), 3))
T = 0.5 [0.86377712 0.11689952 0.01932336]
T = 1.0 [0.65900114 0.24243297 0.09856589]
T = 2.0 [0.50168776 0.30428901 0.19402324]
Analizziamo i risultati applicativi:
- [math]T < 1[/math] (Raffreddamento): Esempio
[0.864, 0.117, 0.019]. La distribuzione si “irrigidisce”. Le differenze minime vengono amplificate e il modello diventa estremamente conservativo e deterministico. Questa configurazione si usa in ambiti critici (es. diagnostica medica da immagini) dove vogliamo che il modello scelga sempre l’opzione con il punteggio nettamente più alto. - [math]T = 1[/math] (Standard): La funzione calcola le probabilità naturali calcolate durante il training
[0.659, 0.242, 0.099]. - [math]T > 1[/math] (Riscaldamento): Esempio
[0.502, 0.304, 0.194]. La distribuzione si appiattisce. Le classi meno probabili guadagnano peso. Questo è il meccanismo esatto utilizzato dalle IA generative per risultare creative: forzando la rete a “pescare” ogni tanto opzioni statisticamente meno probabili, si evita che il testo generato risulti banale o ripetitivo.
6. L’Alleanza con la Cross-Entropy Loss
È impossibile parlare di Softmax senza citare il suo partner ideale durante la fase di addestramento: la Cross-Entropy Loss (entropia incrociata).
Nei framework di deep learning, queste due funzioni sono quasi sempre accorpate in una singola operazione matematica (in PyTorch, ad esempio, troviamo CrossEntropyLoss che applica LogSoftmax e NLLLoss insieme).
Questa combinazione non è solo una comodità di programmazione. Dal punto di vista del calcolo differenziale, il logaritmo presente nell’entropia incrociata elide matematicamente l’esponenziale della Softmax. Questo previene la saturazione dei gradienti durante la backpropagation, permettendo alla derivata di fluire in modo pulito e alla rete neurale di apprendere molto più velocemente.
PyTorch in Azione: Due Casi Studio Reali
Finora abbiamo esplorato la matematica e l’implementazione in NumPy per capire i meccanismi interni della Softmax. Ma come si traduce tutto questo nel codice industriale che fa girare i modelli in produzione?
In PyTorch, l’utilizzo della Softmax cambia drasticamente a seconda che ci troviamo nella fase di addestramento (Training) o nella fase di utilizzo reale (Inferenza). Vediamo due scenari concreti.
Esempio 1: Training vs Inferenza nella Classificazione Multiclasse
L’errore più comune che i principianti commettono in PyTorch è applicare la Softmax ai logits prima di calcolare l’errore (la loss) durante il training. In PyTorch, la funzione nn.CrossEntropyLoss è altamente ottimizzata e calcola matematicamente al suo interno sia il logaritmo, sia la Softmax (tramite LogSoftmax), garantendo una stabilità numerica perfetta.
La regola d’oro è: durante il training diamo in pasto alla Loss i logits grezzi; durante l’inferenza usiamo la Softmax per leggere le probabilità.
import torch
import torch.nn as nn
import torch.nn.functional as F
# Simuliamo l'output grezzo (logits) di una rete neurale per un batch di 2 immagini
# Le classi possibili sono 3: [Gatto, Cane, Volpe]
logits = torch.tensor([[2.5, 1.2, 0.3], # Immagine 1 (probabilmente Gatto)
[-1.0, 3.0, 0.5]]) # Immagine 2 (probabilmente Cane)
# Etichette reali (Target): l'Immagine 1 è un Gatto (indice 0), la 2 è un Cane (indice 1)
targets = torch.tensor([0, 1])
# ==========================================
# FASE 1: ADDESTRAMENTO (TRAINING)
# ==========================================
# Inizializziamo la funzione di costo (Loss)
loss_function = nn.CrossEntropyLoss()
# ATTENZIONE: Passiamo i logits grezzi, NON le probabilità!
# PyTorch applicherà la LogSoftmax internamente in modo efficiente.
loss = loss_function(logits, targets)
print(f"Valore della Loss calcolata: {loss.item():.4f}")
# Da qui partirà la backpropagation: loss.backward()
# ==========================================
# FASE 2: PREVISIONE IN PRODUZIONE (INFERENCE)
# ==========================================
# Il modello è addestrato e lo usiamo nel mondo reale.
# Ora ci servono probabilità leggibili, quindi applichiamo esplicitamente la Softmax.
with torch.no_grad(): # Disattiviamo il calcolo dei gradienti per risparmiare memoria
probabilita = F.softmax(logits, dim=1) # dim=1 calcola la softmax lungo le classi
print("\nProbabilità per l'Immagine 1:", probabilita[0].numpy())
print("Probabilità per l'Immagine 2:", probabilita[1].numpy())
# Estraiamo la classe vincente (ArgMax)
predizioni_finali = torch.argmax(probabilita, dim=1)
print(f"\nIndici delle classi predette: {predizioni_finali.tolist()}")
Valore della Loss calcolata: 0.2101
Probabilità per l'Immagine 1: [0.72289073 0.19701073 0.08009858]
Probabilità per l'Immagine 2: [0.01664452 0.9087599 0.07459556]
Indici delle classi predette: [0, 1]
Questo snippet mostra l’architettura standard di qualsiasi task di Computer Vision. Specificare
dim=1nella funzioneF.softmaxè fondamentale quando si lavora con i batch (matrici multidimensionali), per assicurarsi che le probabilità sommino a [math]1[/math] per ogni singola riga (ogni singola immagine) e non sull’intera matrice.
Esempio 2: Generazione di Testo (LLM) ed Effetto Temperatura
Come abbiamo visto nella sezione teorica, la Softmax con la Temperatura è il motore della generazione di testo. In questo esempio simuliamo l’ultimo strato di un modello linguistico (simile a GPT) che deve prevedere la prossima parola in una frase, estraendola da un vocabolario.
Utilizzeremo torch.multinomial, una funzione che campiona un indice basandosi proprio sulle probabilità generate dalla Softmax.
import torch
import torch.nn.functional as F
# Il nostro vocabolario ha 4 parole possibili
vocabolario = ["intelligente", "veloce", "pigro", "quantistico"]
# Logits generati dall'LLM per la parola successiva
# La rete neurale favorisce "veloce" e "intelligente"
logits = torch.tensor([2.0, 2.5, 0.1, -1.0])
def predici_prossima_parola(logits, temperatura=1.0):
"""
Applica la Softmax con temperatura e campiona una parola.
"""
# 1. Scaliamo i logits con la temperatura
# Clamp evita la divisione per zero e i limiti estremi
T = max(temperatura, 1e-5)
logits_scalati = logits / T
# 2. Calcoliamo la distribuzione probabilistica
probabilita = F.softmax(logits_scalati, dim=0)
# 3. Campioniamo un elemento in base alle probabilità (non prendiamo solo il massimo!)
# Questo introduce la "creatività" tipica degli LLM
indice_scelto = torch.multinomial(probabilita, num_samples=1).item()
return vocabolario[indice_scelto], probabilita
# --- Testiamo diversi scenari ---
print("=== EFFETTO TEMPERATURA NELLA GENERAZIONE TESTO ===")
# Scenario Conservativo (T = 0.2)
parola_fredda, prob_fredde = predici_prossima_parola(logits, temperatura=0.2)
print(f"\nT = 0.2 (Rigido) -> Distribuzione: {prob_fredde.numpy().round(3)}")
print(f"Parola estratta -> '{parola_fredda}' (quasi sicuramente sceglierà 'veloce')")
# Scenario Standard (T = 1.0)
parola_standard, prob_std = predici_prossima_parola(logits, temperatura=1.0)
print(f"\nT = 1.0 (Normale) -> Distribuzione: {prob_std.numpy().round(3)}")
print(f"Parola estratta -> '{parola_standard}'")
# Scenario Creativo (T = 2.0)
parola_calda, prob_calde = predici_prossima_parola(logits, temperatura=3.0)
print(f"\nT = 3.0 (Creativo)-> Distribuzione: {prob_calde.numpy().round(3)}")
print(f"Parola estratta -> '{parola_calda}' (ora anche 'pigro' e 'quantistico' hanno chance)")
=== EFFETTO TEMPERATURA NELLA GENERAZIONE TESTO ===
T = 0.2 (Rigido) -> Distribuzione: [0.076 0.924 0. 0. ]
Parola estratta -> 'veloce' (quasi sicuramente sceglierà 'veloce')
T = 1.0 (Normale) -> Distribuzione: [0.351 0.579 0.053 0.017]
Parola estratta -> 'quantistico'
T = 3.0 (Creativo)-> Distribuzione: [0.325 0.384 0.172 0.119]
Parola estratta -> 'pigro' (ora anche 'pigro' e 'quantistico' hanno chance)
Questo codice svela la vera “magia” dietro ai bot conversazionali. Un modello linguistico non seleziona quasi mai meccanicamente la parola con la probabilità più alta (che lo renderebbe noioso e ripetitivo). Usa invece
torch.multinomialper tirare un dado truccato, dove i “pesi” del dado sono esattamente le percentuali restituite dalla Softmax scalata per la temperatura. Mostrare questa interazione traF.softmaxetorch.multinomialdarà ai tuoi lettori una competenza ingegneristica molto ricercata in ambito NLP.
Conclusioni
La Softmax non è semplicemente l’ultimo layer di una rete neurale. È il meccanismo fondamentale che consente ai modelli di machine learning di trasformare matrici di numeri isolati in distribuzioni di probabilità azionabili e interpretabili.
Che si tratti di un sistema di Computer Vision che deve classificare un tumore (scegliendo una temperatura rigida per evitare falsi allarmi), di un algoritmo di Recommendation System che stila un ranking di prodotti, o di un LLM che deve calcolare la parola successiva in una frase, la Softmax fornisce la grammatica matematica necessaria per prendere decisioni ponderate, gestendo l’incertezza in modo controllato. Senza di essa, il deep learning perderebbe gran parte della sua coerenza statistica.
PyTorch, reti neurali e controllo dei modelli di AI
👉Guida completa a PyTorch: introduzione e reti neurali per principianti
👉PyTorch per principianti: 5 esercizi essenziali su tensori e training
👉Regressione lineare con PyTorch: perché il modello può prevedere valori negativi
👉Temperatura negli LLM: come controllare la creatività dell’IA
👉Burstiness nei testi: come riconoscere contenuti scritti da IA
“`





