Esercizio - Keras e PyTorch a confronto
Questa pagina non ha ancora la versione ripasso: qui sotto c'è il testo completo.
In questa pagina 4
Testo (laboratorio sulle reti neurali, seconda parte). La stessa rete per MNIST (Flatten, strato denso da con ReLU, dropout , uscita da ) si scrive in Keras e in PyTorch. Si chiede di:
- contare i parametri nei due framework;
- spiegare le differenze nella definizione del modello, della loss e dell'ottimizzatore;
- spiegare perché il primo ciclo di addestramento in PyTorch del laboratorio (un solo passo per epoca) impara molto meno del secondo (con
DataLoader) e quante iterazioni per epoca fa ciascuno; - scrivere la sottoclasse
nn.Moduleequivalente.
Teoria usata: Reti neurali - neuroni e funzioni di attivazioneUn neurone calcola $\hat y=g(w_0+w^\top x)$: somma pesata degli ingressi più bias, poi una funzione di attivazione $g$ non lineare (Perceptron a soglia, sigmoide, tanh, ReLU e varianti). Senza non linearità ogni rete è equivalente a un solo modello lineare. Una rete feed-forward impila strati di neuroni: $a^{(k)}=g(W^{(k)}a^{(k-1)}+b^{(k)})$; con uno strato nascosto è già un approssimatore universale, ma più strati rappresentano funzioni complesse con molti meno neuroni e imparano feature gerarchiche (bordi, parti, oggetti). Lo strato di uscita e la loss si scelgono dal compito: lineare+MSE (regressione), sigmoide+cross-entropy binaria, softmax+cross-entropy (multiclasse). Il numero di parametri di uno strato denso è $n_{in}n_{out}+n_{out}$. Nel lab (Keras, MNIST) una rete 784-512-10 ha 407 050 parametri e supera il 98% di accuratezza.Reti neurali - neuroni e funzioni di attivazione →, Addestramento delle reti neurali - backpropagation e ottimizzatoriAddestrare una rete significa minimizzare la loss empirica $J(W)=\frac1n\sum_i\mathcal L(f(x^{(i)};W),y^{(i)})$ con la discesa del gradiente $W\leftarrow W-\eta,\partial J/\partial W$; in pratica a mini-batch (SGD). Il gradiente di tutti i pesi si ottiene con la backpropagation, cioè la regola della catena applicata all'indietro: $\delta^{(L)}=\partial J/\partial a^{(L)}\odot g'(z^{(L)})$, $\delta^{(l)}=(W^{(l+1)\top}\delta^{(l+1)})\odot g'(z^{(l)})$, $\partial J/\partial W^{(l)}=\delta^{(l)}a^{(l-1)\top}$ (con sigmoide e cross-entropy $\delta=\hat y-y$). Per far funzionare reti profonde: attivazioni ReLU, inizializzazione di Xavier o He (varianza $2/(n_{in}+n_{out})$ e $2/n_{in}$), batch normalization, ottimizzatori con momento o adattivi (Momentum, AdaGrad, RMSProp, Adam con $\beta_1=0{,}9$, $\beta_2=0{,}999$, lr $10^{-3}$) e un learning rate che varia nel tempo (a gradini, coseno). Si addestra tenendo d'occhio la loss di training e di validazione.Addestramento delle reti neurali - backpropagation e ottimizzatori →.
1. Parametri
Identici: in Keras Dense(512) dopo Flatten() su ingressi ha parametri e Dense(10) ne ha ; in PyTorch Linear(784, 512) e Linear(512, 10) hanno gli stessi pesi e bias. Totale in entrambi (ReLU, Flatten e Dropout non hanno parametri).
2. Differenze di scrittura
| aspetto | Keras | PyTorch |
|---|---|---|
| dimensione di ingresso | dichiarata una volta (Input(shape=(28, 28))), le altre dimensioni si deducono |
si scrivono sia ingresso sia uscita di ogni strato (Linear(in_features=784, out_features=512)) |
| attivazione | argomento del layer (Dense(512, activation="relu")) |
strato separato (ReLU()) |
| softmax finale | activation="softmax" nell'ultimo strato |
assente: la rete restituisce i logits |
| loss e ottimizzatore | parte della rete: model.compile(loss, optimizer, metrics) |
oggetti separati: CrossEntropyLoss() e Adam(model.parameters()) |
| loss di classificazione | sparse_categorical_crossentropy, applicata alle probabilità |
CrossEntropyLoss applica già la softmax e vuole etichette intere (tipo long) |
| dati | array NumPy | tensori (torch.tensor), suddivisione e mini-batch gestiti a mano |
| addestramento | una chiamata: model.fit(...) |
ciclo scritto a mano |
Errore tipico: mettere una softmax nel modello PyTorch e poi usare CrossEntropyLoss applica la softmax due volte.
3. I due cicli di addestramento
Primo ciclo del laboratorio. Per ogni epoca: optimizer.zero_grad(), forward sull'intero training (tutti i campioni), loss, backward(), optimizer.step(). Una sola iterazione per epoca: in cinque epoche aggiornamenti dei pesi in tutto. È una discesa del gradiente a batch completo.
Secondo ciclo. Con DataLoader(batch_size=32, shuffle=True): iterazioni per epoca (il laboratorio stampa batch di training; di validazione), quindi aggiornamenti: una discesa a mini-batch, come fa fit di Keras con batch_size=32. Moltissimi più passi, quindi molto più apprendimento a parità di epoche. È il motivo per cui nel primo ciclo la loss scende lentamente.
Nel ciclo si ricordano: model.train() per attivare il dropout e model.eval() con torch.no_grad() per la valutazione; zero_grad() a ogni passo perché PyTorch somma i gradienti; shuffle=True solo sul training.
4. Il modello come classe
import torch.nn as nn
class MioModello(nn.Module):
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()
self.linear1 = nn.Linear(784, 512)
self.relu = nn.ReLU()
self.dropout = nn.Dropout(0.2)
self.linear2 = nn.Linear(512, 10)
def forward(self, x): # equivalente di call() in Keras
x = self.flatten(x)
x = self.relu(self.linear1(x))
x = self.dropout(x)
return self.linear2(x) # logits__init__ dichiara gli strati (e quindi i parametri), forward descrive il percorso dei dati (in Keras call). Salvataggio: il laboratorio salva l'intero oggetto con torch.save(model, "m.pth") e lo ricarica con torch.load("m.pth", weights_only=False); è più sicuro salvare solo i pesi, torch.save(model.state_dict(), "m.pth") e model.load_state_dict(torch.load("m.pth", weights_only=True)), perché ricaricare un oggetto intero esegue codice contenuto nel file (in Keras model.save e load_model).
Conclusione del laboratorio. Keras è più semplice, PyTorch richiede più codice ma rende ogni passo visibile ed è più flessibile e comodo da debuggare: per questo è molto usato nella ricerca.