Pytorch 2 Chainrule Autograd
Posted on 15/10/2021, in Italiano. Reading time: 18 minsIndice Globale degli argomenti tra i vari post:
1.1. Indice degli argomenti.
1.1. Introduzione e fonti.
1.1. Lingo - Gergo utilizzato.
1.2. Tensori in Pytorch.
2.1. Chainrule e Autograd.
2.2. Backpropagation.
3.1. Loss e optimizer.
3.2. Modelli di Pytorch.
3.3. Dataset e Dataloader.
3.4. Dataset Transforms.
3.5. Softmax e Cross-Entropy Loss.
3.6. Activation Function.
4.1. Feed Forward Neural Network.
5.1. Convolutional Neural Network.
5.2. Transfer Learning.
5.3. Tensorboard.
6.1. I/O Saving and Loading Models.
7.1. Recurrent Neural Networks.
7.2. RNN, GRU e LSTM.
8.1. Pytorch Lightning.
8.2. LR Scheduler.
9.1. Autoencoder.
ChainRule e Autograd
Il pacchetto Autograd
fornisce differenziazione automatica per le operazioni (funzioni) sui tensori:
requires_grad=True
Immagina un tensore come una semplice variabile (multidimensionale) che entra in un grafo computazionale. Alla fine del grafo ho uno scalare (in genere) e voglio sapere come dipende questo scalare dal un particolare tensore, allora devo usare Autograd.
\(\displaystyle
L(z(x)): R^n \rightarrow R\)
Introduzione ai grafi computazionali
Per esempio:
- costruisco un tensore x (1D con 3 ingressi random)
- costruisco un tensore y funzione di x: y=x+2
- costruisco un tensore z funzione di y: z= 3y$^2$
- ATTENTO il gradiente si puo’ calcolare solo se alla fine si hanno dei valori SCALARI (altrimenti ho un numero di gradienti pari alle componenti del vettore). La logica e’ chiara, alla fine io voglio vedere come varia una funzione di LOSS rispetto ai parametri che metto nella rete neurale. La LOSS e’ una funzione scalare e quindi non sono state implementate delle variazioni per funzioni vettoriali.
- calcolo quindi il valore medio di u=<z> (per avere uno scalare)
\({\bf y}= \left( \begin{eqnarray} y_1 \\ y_2 \\ y_3 \end{eqnarray} \right)\) \(= \left( \begin{eqnarray} x_1+2 \\ x_2+2 \\ x_3+2 \end{eqnarray} \right)\)
\({\bf z} = \left( \begin{eqnarray} 3y_1^2 \\ 3y_2^2 \\ 3y_3^2 \end{eqnarray} \right)\) \(= \left( \begin{eqnarray} 3(x_1+2)^2 \\ 3(x_2+2)^2 \\ 3(x_3+2)^2 \end{eqnarray} \right)\) \(= \left( \begin{eqnarray} 3(x_1^2+4x_1+4) \\ 3(x_2^2+4x_2+4) \\ 3(x_3^2+4x_3+4) \end{eqnarray} \right)\)
\[\displaystyle u = \frac{1}{3}\sum_{i=1}^3z_i = \frac{1}{3} \sum 3y_i^2 = \sum y_i^2 \mbox{ Derivata parziale} \Rightarrow \frac{\partial u}{\partial y_k} = 2 y_k\]Se voglio conoscere la dipendenza di ${\bf u}$ da parte di ${\bf x}$, dal punto di vista matematico devo calcolare la derivata parziale di u rispetto a x:
\(\displaystyle \frac{ \partial u } {\partial x_j} = \frac{\partial u}{\partial y_i} \frac{\partial y_i}{\partial x_j}\) (indici ripetuti sono sommati)
\[\displaystyle \frac{\partial u}{\partial y_k} = 2 y_k = 2(x_k+2)\] \[\displaystyle \frac{\partial y_k}{\partial x_j} = \delta_{kj}\]Quindi: \(\displaystyle \frac{ \partial u } {\partial x_k} = 2(x_k+2)\)
Se il valore del tensore ${\bf x_0} = (1, 1, 1)$, alora il gradiente rispetto alla variaibile x della funzione u e’ un vettore che vale:
\(\nabla_x u |_{x_0} = = \left( \begin{eqnarray} 2(x_1+2) \\ 2(x_2+2) \\ 2(x_3+2) \end{eqnarray} \right)\) \(= \left( \begin{eqnarray} 6 \\ 6 \\ 6 \end{eqnarray} \right)\)
Pensala cosi’: C’e’ una funzione di molte variabili che vengono combinate passo passo. Queste variabili pensale come proprio i pesi della rete neurale. Alla fine noi vogliamo minimizzare la LOSS. Quindi prendiamo il gradiente per trovare la pendenza massima e scendiamo lungo il gradiente di queste variabili con piccoli passi, sperando di raggiungere un buon minimo (occhio che la cosa non e’ garantita banalmente in quanto non siamo in un caso semplice di un solo massimo, potremmo finire in un minimo locale!).
Attenzione
Quando si fa il .backward() i valori dei gradienti vengono ACCUMULATI nell’attributo .grad
x = torch.ones(3, requires_grad=True) # x = [x_1, x_2, x_3] = [1, 1, 1]
y = x + 2 # y = [y_1, y_2, y_3]
##### Occhio y e' funzione di x, che ha requires_grad=True. Quindi ha come attributo grad_fn
print(y.grad_fn)
z = y * y * 3 # z = [3 y_1^2, 3 y_2^2, 3 y_3^2] Lavorano sul singolo ingresso!
z = z.mean() # zmean = 1/3 ( 3 y_1^2 + 3 y_2^2 + 3 y_3^2 ) calcolo la media
z.backward() # back propagation
print(x.grad) # dz/dx = dz/ dy * dy/dx CALCOLATO nel valore corrente delle x
<AddBackward0 object at 0x000001EE446B2BB0>
tensor([6., 6., 6.])
Se l’output non e’ uno scalare si devono specificare gli argomenti per il metodo .backward(), non mi e’ chiaro come questi argomenti vengano usati.
#x = torch.randn(3, requires_grad=True) # tensore 1D
x = torch.tensor([1.1,1.1, 1.1], requires_grad=True) # tensore 1D
y = x * 2 # altro tensore 1D
for _ in range(10):
y = y * 2 # y * y * y * ... * y (10 volte +1 del passo precedente) = x * 2**11
print(y)
print(y.shape)
v = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float32)
y.backward(v) # qui ho specificato che voglio il gradiente rispetto a ... v? non chiaro forse fa derivata direzionale
print(x.grad)
tensor([2252.8000, 2252.8000, 2252.8000], grad_fn=<MulBackward0>)
torch.Size([3])
tensor([2.0480e+02, 2.0480e+03, 2.0480e-01])
Stop tracking
- Supponiamo di volere fare un’update dei pesi durante il loop del training.
- questo implica fare delle nuove funzioni sui pesi (le update), e quindi quando si fa la back propagation si rischia che questa tenga conto anche delle update! Bisogna quindi dire al TENSORE di non tenere conto delle update. Ovvero si deve dire al TENSORE che deve essere tracciato solo lungo il network computazionale
(non del tutto chiaro devo fare esperimenti)
- x.requires_grad_(False) (nota l’underscore _ finale per INPLACE)
- x.detach()
- wrap in with torch.no_grad():
Se si usa il metodo .zero_() questo riempie il gradiente prima di un nuovo passo di ottimizzazione.
a = torch.randn(2, 2) # qui NON accendiamo il requires_grad
print(a.requires_grad) # e appunto se controlliamo da': False
b = ((a * 3) / (a - 1)) # costruisco un nuovo Tensore b
print(b.grad_fn) # e per qusto non c'e' l'attributo grad_fn che indica che c'e' una gradiente
a.requires_grad_(True) # accendiamo INPLACE(_) il gradiente
print(a.requires_grad) # ora il risultato e' True
b = (a * a).sum() # creiamo uno scalare b con sum() fa la somma del Tensore.
print(b.grad_fn) #
# .detach(): get a new Tensor with the same content but no gradient computation:
a = torch.randn(2, 2, requires_grad=True)
print(a.requires_grad)
b = a.detach()
print(b.requires_grad)
# wrap in 'with torch.no_grad():'
a = torch.randn(2, 2, requires_grad=True)
print(a.requires_grad)
with torch.no_grad():
print((x ** 2).requires_grad) # qui ho fatto un'altra funzione con x ma non contribuisce al gradiente!
False
None
True
<SumBackward0 object at 0x000001A0C4C411F0>
True
False
True
False
# -------------
# backward() accumulates the gradient for this tensor into .grad attribute.
# !!! We need to be careful during optimization !!!
# Use .zero_() to empty the gradients before a new optimization step!
weights = torch.ones(4, requires_grad=True)
for epoch in range(3):
# just a dummy example
model_output = (weights*3).sum()
model_output.backward()
print(weights.grad)
# optimize model, i.e. adjust weights...
with torch.no_grad(): # quando faccio l'ottimizzazione dei valori del tensore
weights -= 0.1 * weights.grad # non voglio che facciano parte del grafo computazionale!
# this is important! It affects the final weights & output
weights.grad.zero_() # se non azzeri c'e' accumulo (?)
print(weights)
print(model_output)
# Optimizer has zero_grad() method
# optimizer = torch.optim.SGD([weights], lr=0.1)
# During training:
# optimizer.step()
# optimizer.zero_grad()
tensor([3., 3., 3., 3.])
tensor([3., 3., 3., 3.])
tensor([3., 3., 3., 3.])
tensor([0.1000, 0.1000, 0.1000, 0.1000], requires_grad=True)
tensor(4.8000, grad_fn=<SumBackward0>)
Backpropagation
un esempio semplice di backpropagation.
- costruisco un tensore 0D x=1 (e’ il
predictor
) - costruisco un tensore 0D y=2 (e’ la funzione obiettivo)
- costruisco un tensore 0D w=1 (sono i pesi che voglio ottimizzare)
- calcolo le y_predicted =w*x
- calcolo la LOSS (y_predicted-y)$^2$ (tutto questo e’ il forward pass)
- calcolo la BACKPROPAGATION (stando attento a non farla entrare nel grafo computazionale)
- azzero i gradienti e ripeto varie epoche
x = torch.tensor(1.0) # costruisco un tensore 0D (1 oggetto): i predictors
y = torch.tensor(2.0) # un altro tensore 0D: la risposta ESATTA
w = torch.tensor(1.0, requires_grad=True) # questo tensore ha accesa la condizione requires_grad, i PESI
# FORWARD PASS
y_predicted = w * x # costruisco un grafo computazionale, ora y =w*x, la risposta CALCOLATA
loss = (y_predicted - y)**2 # la funzione di LOSS
print(loss)
# BACKWARD PASS dLoss/dw # calcolo la dipendenza della LOSS in funzione dei PESI
loss.backward()
#print(w.grad)
# A questo punto voglio fare una update dei PESI per cercare di fare predizioni migliori
#
# l'update dei PESI NON deve entrare nel grafo computazionale
with torch.no_grad():
w -= 0.01 * w.grad # mi muovo lungo la direzione di massima crescita... al negativo di un passetto
w.grad.zero_() # NON dimenticare di azzerare i gradienti
# FORWARD PASS
y_predicted = w * x # nuovo forward pass
loss = (y_predicted - y)**2 # nuova funzione di LOSS
print(loss)
############# faccio ora un ciclo ################
for epoch in range(100):
with torch.no_grad():
w -= 0.01 * w.grad # mi muovo lungo la direzione di massima crescita... al negativo di un passetto
w.grad.zero_() # AZZERO i gradienti
y_predicted = w * x # nuovo forward pass
loss = (y_predicted - y)**2 # nuova funzione di LOSS
loss.backward() # backward! se non lo faccio il gradiente e' stato azzerato!
if (int(epoch/10)*10==epoch):
print(loss, i)
tensor(1., grad_fn=<PowBackward0>)
tensor(0.9604, grad_fn=<PowBackward0>)
tensor(0.9604, grad_fn=<PowBackward0>) 99
tensor(0.6412, grad_fn=<PowBackward0>) 99
tensor(0.4281, grad_fn=<PowBackward0>) 99
tensor(0.2858, grad_fn=<PowBackward0>) 99
tensor(0.1908, grad_fn=<PowBackward0>) 99
tensor(0.1274, grad_fn=<PowBackward0>) 99
tensor(0.0850, grad_fn=<PowBackward0>) 99
tensor(0.0568, grad_fn=<PowBackward0>) 99
tensor(0.0379, grad_fn=<PowBackward0>) 99
tensor(0.0253, grad_fn=<PowBackward0>) 99
Discesa del gradiente Manuale
proviamo ora con un esempio 1D (prima era 0D) con una regressione lineare.
- i vari passi vengono calcolati MANUALMENTE senza usare Torch
- costruisco una funzione che fa il forward pass
- costruisco una funzione che fa il backward pass
- calcolo il gradiente (senza autograd)
La cosa interessante e’ che qui ho un array in ingresso. Prendo tutti i valori dell’array di ingresso e con essi faccio il training tutti insieme (calcolo infatti la LOSS su tutti). Poi il passo forward lo faccio su uno scalare! per vedere se la predizione funziona
import numpy as np
# Regressione Lineare
# f = w * x
# here : f = 2 * x
X = np.array([1, 2, 3, 4], dtype=np.float32) # PREDICTORS
Y = np.array([2, 4, 6, 8], dtype=np.float32) # OBIETTIVO
w = 0.0 # pesi (ma non e' un tensore...)
# MODEL OUTPUT
def forward(x):
return w * x # FORWARD PASS
# LOSS MSE
def loss(y, y_pred): # LOSS MSE
return ((y_pred - y)**2).mean() # uso un metodo dei tensori .mean()
# J = MSE = 1/N * (w*x - y)**2
# dJ/dw = 1/N * 2x(w*x - y)
def gradient(x, y, y_pred): # calcolo il gradiente
return np.dot(2*x, y_pred - y).mean()
print(f'Predizione prima del training: f(5) = {forward(5):.3f}')
# Training
learning_rate = 0.01
n_iters = 20
for epoch in range(n_iters):
y_pred = forward(X) # FORWARD
l = loss(Y, y_pred) # LOSS
dw = gradient(X, Y, y_pred) # GRADIENTE (senza autograd)
w -= learning_rate * dw # UPDATE
if epoch % 2 == 0:
print(f'epoch {epoch+1}: w = {w:.3f}, loss = {l:.8f}')
print(f'Predizione dopo il training: f(5) = {forward(5):.3f}')
Prediction before training: f(5) = 0.000
epoch 1: w = 1.200, loss = 30.00000000
epoch 3: w = 1.872, loss = 0.76800019
epoch 5: w = 1.980, loss = 0.01966083
epoch 7: w = 1.997, loss = 0.00050332
epoch 9: w = 1.999, loss = 0.00001288
epoch 11: w = 2.000, loss = 0.00000033
epoch 13: w = 2.000, loss = 0.00000001
epoch 15: w = 2.000, loss = 0.00000000
epoch 17: w = 2.000, loss = 0.00000000
epoch 19: w = 2.000, loss = 0.00000000
Prediction after training: f(5) = 10.000
Discesa del gradiente Autograd
come il punto precedente ma usando Autograd
- ATTENZIONE il print non legge bene il formato dei “tensori”, devo usare il metodo .item() per ottenere il valore.
- .backward() va fatto sulla loss
- .grad e’ automaticamente ottenuto come parametro del tensore (per esempio dei pesi)
import numpy as np
import torch
# Regressione Lineare
# f = w * x
# here : f = 2 * x
X = np.array([1, 2, 3, 4], dtype=np.float32) # PREDICTORS
Y = np.array([2, 4, 6, 8], dtype=np.float32) # OBIETTIVO
#Nota che posso vedere sia dal punto di vista spaziale che temporale.
# dal punto di vista temporale passo alla mia rete neurale un predictor per volta
# (ma non e' manco piu' un vettore).
# dal punto di vista spaziale, passo tutti i predictor e ottengo tutti gli obiettivi.
# trasformo in TENSORI (avrei potuto direttamente usare torch.tensor(), ma cosi' uso from_numpy())
X = torch.from_numpy(X) # autograd non serve
Y = torch.from_numpy(Y) # neanche qui
w = torch.tensor([0.0], requires_grad=True) # pesi: accendo Autograd
# MODEL OUTPUT
def forward(x):
return w * x # FORWARD PASS
# LOSS MSE
def loss(y, y_pred): # LOSS MSE
return ((y_pred - y)**2).mean() # mean() e' un metodo dei tensori
print(f'Predizione prima del training: f(5) = {forward(5).item():.3f}')
# Parametri del Training
learning_rate = 0.01
n_iters = 50
for epoch in range(n_iters):
y_pred = forward(X) # FORWARD
LOSS = loss(Y, y_pred) # LOSS
LOSS.backward() # BACKPROPAGATION (Autograd)
with torch.no_grad():
w -= learning_rate * w.grad # UPDATE
w.grad.zero_() # AZZERO i gradienti
if epoch % 10 == 0:
print(f'epoch {epoch+1}: w = {w.item():.3f}, loss = {LOSS.item():.8f}')
#print(w)
#print(f'Predizione dopo il training: f(5) = {forward(5):.3f}')
Predizione prima del training: f(5) = 0.000
epoch 1: w = 0.300, loss = 30.00000000
epoch 11: w = 1.665, loss = 1.16278565
epoch 21: w = 1.934, loss = 0.04506890
epoch 31: w = 1.987, loss = 0.00174685
epoch 41: w = 1.997, loss = 0.00006770
Comments