Esercizio 1 – Somma e prodotto di tensori
Obiettivo: Comprendere le operazioni di base sui tensori PyTorch.
Testo:
Crea due tensori 2×2 contenenti valori casuali campionati da una distribuzione uniforme e:
- Calcolane la somma elemento per elemento.
- Calcolane il prodotto elemento per elemento.
- Calcolane il prodotto matriciale.
- Stampa tutti i tensori risultanti.
✅ Soluzione:
import torch
# Creiamo i due tensori 2x2 con valori casuali
a = torch.rand(2, 2)
b = torch.rand(2, 2)
# Calcoliamo la somma (elemento per elemento)
somma = a + b
# Calcoliamo il prodotto elemento per elemento
elementwise = a * b
# Calcoliamo il prodotto matriciale (torch.matmul o l'operatore @)
prodotto_matriciale = torch.matmul(a, b) # oppure prodotto_matriciale = a @ b
# Stampiamo i tensori originali e i risultati
print("Tensore A:\n", a)
print("\nTensore B:\n", b)
print("\nSomma (A + B):\n", somma)
print("\nProdotto elemento per elemento (A * B):\n", elementwise)
print("\nProdotto matriciale (A @ B):\n", prodotto_matriciale)
Argomenti acquisiti:
Hai imparato che i tensori sono la struttura dati fondamentale in PyTorch e come eseguire le operazioni matematiche base su di essi, sia elemento per elemento (somma, prodotto) che operazioni lineari fondamentali come il prodotto matriciale.
Esercizio 2 – Costruzione e forward pass di una rete
Obiettivo: Definire la struttura di un semplice modello PyTorch e testare il forward pass.
Testo:
Crea una rete neurale personalizzata estendendo nn.Module. La rete deve avere la seguente struttura:
- Un layer lineare di input con 4 neuroni.
- Un layer lineare nascosto con 5 neuroni, seguito da una funzione di attivazione ReLU.
- Un layer lineare di output con 2 neuroni.
Esegui un forward pass su un tensore di input di dimensione [math](1, 4)[/math] (cioè, un singolo campione con 4 feature).
✅ Soluzione:
import torch
import torch.nn as nn
import torch.nn.functional as F # Importiamo le funzioni non parametriche come ReLU
# Definiamo la classe della nostra rete neurale
class MyNet(nn.Module):
def __init__(self):
# Chiamiamo il costruttore della classe base
super(MyNet, self).__init__()
# Definiamo i layer lineari (fully connected)
# nn.Linear(input_features, output_features)
self.fc1 = nn.Linear(4, 5) # Layer input (4 neuroni) -> Layer nascosto (5 neuroni)
self.fc2 = nn.Linear(5, 2) # Layer nascosto (5 neuroni) -> Layer output (2 neuroni)
# Definiamo il forward pass
def forward(self, x):
# Passiamo l'input attraverso il primo layer lineare e applichiamo ReLU
x = F.relu(self.fc1(x))
# Passiamo il risultato attraverso il secondo layer lineare (output layer)
x = self.fc2(x) # L'output saranno i logit per 2 classi
return x
# Creiamo un'istanza del modello
model = MyNet()
# Creiamo un tensore di input di dimensione (1, 4)
# La dimensione (1, ...) rappresenta la batch size (1 campione)
x = torch.rand(1, 4)
# Eseguiamo il forward pass passando il tensore di input al modello
output = model(x)
# Stampiamo l'input e l'output risultante
print("Input al modello (forma {}):\n".format(x.shape), x)
print("\nOutput del modello (forma {}):\n".format(output.shape), output)
# L'output dovrebbe avere dimensione (1, 2), corrispondente a 1 campione e 2 neuroni di output.
Argomenti acquisiti:
Hai compreso come definire l’architettura di una semplice rete neurale (un
nn.Module) combinando layer lineari (nn.Linear) e funzioni di attivazione non-lineari (F.relu). Hai anche imparato a eseguire il “forward pass”, ovvero far passare un input attraverso la rete per ottenere le sue previsioni.
Esercizio 3 – Calcolo della loss
Obiettivo: Utilizzare la funzione di costo CrossEntropyLoss per un semplice caso di classificazione.
Testo:
Dati i seguenti output (logit) da un modello di classificazione a 3 classi e l’etichetta corretta:
- Output del modello (logit):
logits = torch.tensor([[2.0, 1.0, 0.1]]) - Etichetta corretta (classe vera):
target = torch.tensor([0])(l’etichetta è la classe 0)
Calcola la loss usando nn.CrossEntropyLoss().
✅ Soluzione:
import torch
import torch.nn as nn
# Output del modello (logit) per 1 campione e 3 classi
# La forma è (batch_size, num_classes)
logits = torch.tensor([[2.0, 1.0, 0.1]])
# Etichetta corretta per lo stesso campione
# La forma per CrossEntropyLoss è (batch_size) e contiene l'indice della classe corretta (integer)
target = torch.tensor([0]) # L'etichetta vera per il campione è la classe con indice 0
# Creiamo un'istanza della funzione di costo CrossEntropyLoss
criterion = nn.CrossEntropyLoss()
# Calcoliamo la loss passando i logit (output del modello) e le etichette vere
loss = criterion(logits, target)
# Stampiamo il valore scalare della loss
print("Logit di output:", logits)
print("Etichetta vera:", target)
print("Loss (CrossEntropyLoss):", loss.item())
# loss.item() estrae il valore numerico da un tensore con un singolo elemento.
Argomenti acquisiti:
Hai capito il ruolo cruciale della funzione di costo (loss function) nell’apprendimento supervisionato. Hai imparato a usare
nn.CrossEntropyLossper misurare l’errore tra l’output grezzo del modello (logit) e le etichette vere, un passo fondamentale per guidare l’addestramento.
Esercizio 4 – Allenamento su un dataset sintetico
Obiettivo: Mettere in pratica il ciclo di allenamento su un piccolo dataset generato manualmente.
Testo:
Crea un dataset sintetico con 4 esempi. Gli input sono coppie di valori binari ([0,0], [0,1], [1,0], [1,1]) e il target è il risultato di un AND logico (0, 0, 0, 1). Definisci e addestra una rete neurale semplice con un singolo layer lineare di input 2 e output 2 (per classificazione binaria) per 100 epoche. Usa SGD come ottimizzatore e CrossEntropyLoss.
✅ Soluzione:
import torch
import torch.nn as nn
import torch.optim as optim
# Dati sintetici: 4 esempi con 2 feature di input e 1 etichetta target (0 o 1)
# Input X: rappresentano le coppie (0,0), (0,1), (1,0), (1,1). Convertiti in float.
X = torch.tensor([[0, 0], [0, 1], [1, 0], [1, 1]]).float()
# Etichette y: il risultato dell'AND logico (0, 0, 0, 1). Convertiti in long (richiesto da CrossEntropyLoss per le etichette).
y = torch.tensor([0, 0, 0, 1]).long() # solo (1,1) -> 1; gli altri sono 0
# Definiamo una rete neurale molto semplice con un singolo layer lineare
class BinaryNet(nn.Module):
def __init__(self):
super().__init__() # Chiama il costruttore della classe base
# Layer lineare: 2 input feature -> 2 output (logit per classe 0 e classe 1)
self.fc = nn.Linear(2, 2)
def forward(self, x):
# Passa l'input attraverso il layer lineare. Nessuna attivazione finale qui (CrossEntropyLoss lavora sui logit).
return self.fc(x)
# Creiamo un'istanza del modello
model = BinaryNet()
# Definiamo la funzione di costo
criterion = nn.CrossEntropyLoss()
# Definiamo l'ottimizzatore (Stochastic Gradient Descent)
# model.parameters() sono i pesi e i bias del layer fc
optimizer = optim.SGD(model.parameters(), lr=0.1) # learning rate 0.1
# Ciclo di addestramento per 100 epoche
epochs = 100
print("Avvio addestramento...")
for epoch in range(epochs):
# --- Passi fondamentali dell'addestramento ---
optimizer.zero_grad() # 1. Azzeramento dei gradienti
outputs = model(X) # 2. Forward pass: calcola gli output (logit)
loss = criterion(outputs, y) # 3. Calcolo della loss
loss.backward() # 4. Backward pass: calcola i gradienti (Autograd!)
optimizer.step() # 5. Aggiornamento dei pesi
# Stampa opzionale della loss ogni tot epoche per monitoraggio
# if (epoch + 1) % 10 == 0:
# print(f'Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}')
print("Addestramento completato.")
# Dopo l'addestramento, facciamo una previsione per vedere se il modello ha imparato l'AND logico
# Usiamo torch.argmax per trovare l'indice (classe) con il logit più alto per ogni esempio
print("\nOutput finali predetti dal modello:", torch.argmax(model(X), dim=1))
print("Etichette vere (Target): ", y)
# Se l'addestramento è andato bene, l'output predetto dovrebbe corrispondere a [0, 0, 0, 1]
Argomenti acquisiti:
Hai assemblato i concetti precedenti per implementare un ciclo di addestramento basilare. Hai imparato la sequenza standard di operazioni in ogni epoca/batch (azzerare gradienti, forward, loss, backward, step dell’ottimizzatore) e come l’ottimizzatore utilizza i gradienti per aggiornare i pesi del modello e ridurne l’errore.
Esercizio 5 – Salvataggio e caricamento modello
Obiettivo: Salvare lo stato di un modello addestrato e ricaricarlo per verificarne la persistenza.
Testo:
Utilizza il modello BinaryNet addestrato nell’Esercizio 4. Salva il suo state_dict su disco in un file. Successivamente, crea una nuova istanza vuota della classe BinaryNet e carica lo state_dict salvato. Infine, verifica che il modello ricaricato produca le stesse previsioni del modello originale per gli input [math]X[/math] del precedente esercizio.
✅ Soluzione:
# Assumiamo che il modello 'model' dell'Esercizio 4 sia già addestrato e disponibile.
# Assumiamo anche che il tensore di input 'X' dell'Esercizio 4 sia disponibile.
# 1. Salvataggio dello stato del modello addestrato
model_save_path = 'binary_and_model.pth' # Definisci il nome del file
torch.save(model.state_dict(), model_save_path) # Salva solo lo state_dict
print(f"Stato del modello salvato in '{model_save_path}'")
# 2. Caricamento dello stato in una nuova istanza del modello
new_model = BinaryNet() # Crea una nuova istanza della stessa architettura
# Carica lo state_dict dal file
state_dict = torch.load(model_save_path)
# Carica lo stato nella nuova istanza del modello
new_model.load_state_dict(state_dict)
# Imposta il nuovo modello in modalità valutazione (eval mode)
# Questo è importante per garantire che layer come dropout (se presenti) si comportino correttamente durante l'inferenza.
new_model.eval()
print(f"Modello caricato da '{model_save_path}'")
# 3. Verifica: confronta gli output del modello originale e del nuovo modello
# Ottieni le previsioni del modello originale (addestrato)
original_output_classes = torch.argmax(model(X), dim=1)
# Ottieni le previsioni del nuovo modello (caricato)
# Assicurati di usare torch.no_grad() per l'inferenza per evitare il calcolo dei gradienti
with torch.no_grad():
new_model_output_classes = torch.argmax(new_model(X), dim=1)
print("\nPrevisioni Modello Originale:", original_output_classes)
print("Previsioni Nuovo Modello: ", new_model_output_classes)
# Verifica che siano uguali
are_outputs_equal = torch.equal(original_output_classes, new_model_output_classes)
print("\nLe previsioni del modello originale e del nuovo modello sono uguali?", are_outputs_equal)
# Se are_outputs_equal è True, significa che il salvataggio e caricamento hanno funzionato correttamente
# e il modello ricaricato ha mantenuto i pesi appresi.
Argomenti acquisiti:
Hai imparato a rendere persistente il lavoro svolto salvando i parametri addestrati di un modello. Hai visto come utilizzare lo
state_dictper salvare e ricaricare i pesi e i bias in un’istanza del modello, e come preparare un modello caricato per l’inferenza usandoeval()etorch.no_grad().





