PyTorch per Principianti: 5 Esercizi Essenziali (Tensori, Reti Neurali, Addestramento, Loss)

Cerca:

Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors
PyTorch Esercizi Base
Per consolidare la tua comprensione dei concetti fondamentali di PyTorch esplorati nel tutorial (Guida Completa a PyTorch: Introduzione e Implementazione di Reti Neurali per Principianti), ecco 5 esercizi pratici con le relative soluzioni.Prova a risolverli autonomamente prima di guardare la soluzione!

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.

Pubblicità

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.

Forse potrebbe interessarti anche:  Analisi del Sentiment: Perché le Regex non Bastano Più e Come BERT Cambia le Regole del Gioco

 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.CrossEntropyLoss per 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.

Forse potrebbe interessarti anche:  Analisi Avanzata Churn Rate e CLV: Modello Python + Excel per Ottimizzare la Retention Clienti

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_dict per salvare e ricaricare i pesi e i bias in un’istanza del modello, e come preparare un modello caricato per l’inferenza usando eval() e torch.no_grad().

Pubblicità