Funzione Softmax nel Machine Learning: formula, stabilità numerica e applicazioni in Python e PyTorch

Cerca:

Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors
implementazione Softmax in Python

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”.

Pubblicità

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?

  1. 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.
  2. 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”.

Forse potrebbe interessarti anche:  Regressione Quadratica: Guida Pratica con Esercizi Svolti (Parabola e Dati)

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]).

Forse potrebbe interessarti anche:  Teorema di Bayes per Data Scientist: 6 Esercizi Reali che Ti Salvano dal Licenziamento

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=1 nella funzione F.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.

Forse potrebbe interessarti anche:  Network Science per il Marketing: La Guida Pratica per Campagne Virali con Dati e Python

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.multinomial per tirare un dado truccato, dove i “pesi” del dado sono esattamente le percentuali restituite dalla Softmax scalata per la temperatura. Mostrare questa interazione tra F.softmax e torch.multinomial darà 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.

“`

Pubblicità