Quando si inizia a lavorare con PyTorch, una delle righe più misteriose è spesso questa:
loss.backward()
Sembra quasi una magia.
Abbiamo calcolato una loss, chiamiamo backward(), e improvvisamente PyTorch sa come modificare milioni o miliardi di parametri del modello.
Ma cosa succede davvero?
La risposta è: PyTorch costruisce e attraversa un grafo di operazioni matematiche per calcolare le derivate.
Capire questo meccanismo è fondamentale non solo per il deep learning in generale, ma anche per capire algoritmi come PPO, GRPO e RLVR.
1. Il problema che dobbiamo risolvere
Immaginiamo di avere un parametro del modello:
e una funzione di loss:
Vogliamo sapere:
Se modifico leggermente , come cambia la loss?
Questa informazione è la derivata:
Se la derivata è positiva, aumentare tende ad aumentare la loss.
Se la derivata è negativa, aumentare tende a diminuire la loss.
Il gradient descent sfrutta proprio questa informazione.
L’aggiornamento più semplice è:
dove è il learning rate.
Quindi il problema fondamentale è:
Come calcoliamo
quando la rete contiene milioni di operazioni e milioni di parametri?
Qui entra in gioco il computational graph.
2. Un esempio semplicissimo
Consideriamo:
e poi:
Quindi:
Supponiamo:
Allora:
e:
Possiamo rappresentare le operazioni come un grafo:
x = 2 ───┐
↓
multiply
↓
y = 3 ───┘
↓
z = 6
↓
square
↓
L = 36
Questo è il computational graph.
Non è altro che una rappresentazione delle operazioni matematiche che hanno prodotto il risultato.
3. Perché il grafo è utile?
Perché vogliamo andare nella direzione opposta.
Durante il forward abbiamo fatto:
x
\
→ multiply → z → square → L
/
y
Per calcolare le derivate facciamo concettualmente:
x
\
← multiply ← z ← square ← L
/
y
Partiamo dalla loss e torniamo indietro.
Questo è ciò che intendiamo con:
loss.backward()
backward = percorri il grafo all’indietro calcolando le derivate.
4. La chain rule è il cuore di tutto
Supponiamo:
e:
Vogliamo:
La chain rule dice:
Abbiamo:
e:
quindi:
Con e :
Analogamente:
PyTorch fa esattamente questo, ma automaticamente e su grafi enormemente più complessi.
5. Cosa succede dentro PyTorch?
Guardiamo questo codice:
import torch
x = torch.tensor(2.0, requires_grad=True)
y = torch.tensor(3.0, requires_grad=True)
z = x * y
loss = z ** 2
loss.backward()
print(x.grad)
print(y.grad)
Il risultato sarà:
tensor(36.)
tensor(24.)
Perché:
e:
La cosa importante è capire da dove sono arrivati quei numeri.
6. requires_grad=True
Questa istruzione:
x = torch.tensor(2.0, requires_grad=True)
dice a PyTorch:
“Mi interessa sapere come la funzione finale dipende da
x.”
Quindi PyTorch tiene traccia delle operazioni che coinvolgono x.
Lo stesso vale per y.
Se invece scrivessimo:
x = torch.tensor(2.0)
PyTorch non avrebbe motivo di costruire il percorso necessario per calcolare:
7. Il grafo viene costruito durante il forward
Questa è una delle idee più importanti da capire.
Quando scriviamo:
z = x * y
PyTorch non calcola solamente il numero 6.
Registra anche:
“Questo
zè stato ottenuto moltiplicandoxey.”
Poi:
loss = z ** 2
registra:
“Questa loss è stata ottenuta elevando
zal quadrato.”
Quindi alla fine PyTorch conosce una struttura concettualmente simile a:
x ────────┐
↓
multiply
↓
y ────────┘
↓
z
↓
square
↓
loss
Questo grafo permette di ricostruire le derivate durante il backward.
8. loss.backward() non aggiorna i pesi
Questo è un errore concettuale molto comune.
Quando scrivi:
loss.backward()
PyTorch non modifica ancora i parametri.
Calcola i gradienti.
Per esempio:
loss.backward()
print(x.grad)
produce:
36
A questo punto PyTorch sa:
Ma x è ancora:
L’aggiornamento avviene successivamente.
9. È l’optimizer che modifica i parametri
Per esempio:
optimizer = torch.optim.SGD([x, y], lr=0.01)
optimizer.zero_grad()
loss.backward()
optimizer.step()
La sequenza è:
forward
↓
calcolo loss
↓
backward()
↓
calcolo gradienti
↓
optimizer.step()
↓
modifica parametri
Quindi bisogna distinguere:
backward()
Calcola “come dovrei cambiare i parametri?”
optimizer.step()
Cambia effettivamente i parametri.
10. Perché si chiama computational graph?
Perché non rappresenta soltanto le variabili.
Rappresenta soprattutto le operazioni.
Per esempio:
diventa:
x ──┐
├── add ──→ a ──→ square ──→ b ──┐
y ──┘ ├── add ──→ L
5 ───┘
Ogni operazione ha una regola matematica per la derivata.
Il backward applica queste regole usando la chain rule.
11. Il concetto di “gradiente locale”
Immaginiamo:
L’operazione multiply sa che:
e:
Poi abbiamo:
L’operazione square sa che:
Durante il backward questi pezzi vengono combinati.
L
│
dL/dz = 2z
↓
z
│
┌──────────┴──────────┐
↓ ↓
d z/dx d z/dy
↓ ↓
y x
Per :
Questo meccanismo si ripete per ogni operazione del modello.
12. Ora immaginiamo una rete neurale
Una rete neurale è semplicemente un computational graph gigantesco.
In forma molto semplificata:
input
↓
Linear
↓
ReLU
↓
Linear
↓
ReLU
↓
Linear
↓
logits
↓
loss
Durante il forward:
input → layer 1 → layer 2 → layer 3 → loss
Durante il backward:
input ← layer 1 ← layer 2 ← layer 3 ← loss
Per ogni parametro PyTorch calcola quanto quel parametro ha contribuito alla loss.
13. Ecco perché possono essere allenati miliardi di parametri
Supponiamo che un modello abbia:
La loss è:
Il backward calcola:
Questi valori costituiscono il gradiente.
Concettualmente:
loss
│
├──→ ∂L/∂θ1
├──→ ∂L/∂θ2
├──→ ∂L/∂θ3
├──→ ∂L/∂θ4
├──→ ...
└──→ ∂L/∂θN
Poi l’optimizer usa questi gradienti per aggiornare i parametri.
14. Collegamento con un LLM
Ora torniamo al caso che ci interessa.
Un LLM produce dei logits:
input
↓
Transformer
↓
hidden states
↓
linear head
↓
logits
↓
probabilità dei token
Supponiamo che abbia generato:
"The answer is 42"
Per ogni token il modello ha prodotto una distribuzione:
"The" → probabilità
" answer" → probabilità
" is" → probabilità
" 42" → probabilità
Possiamo calcolare:
Poi costruiamo la loss.
La parte davvero potente è che la loss può essere un singolo scalare, ma quel singolo scalare è collegato, attraverso il computational graph, a tutte le operazioni e a tutti i parametri che l’hanno prodotto.
È per questo che una singola chiamata:
loss.backward()
può produrre gradienti per milioni o miliardi di parametri.
Nella prossima approfondiremo ancora con un esempio piu pratico.

