💡 Introduzione
Questo capitolo introduce i due framework predominanti per lo sviluppo, l'addestramento e il deployment di modelli di Deep Learning (DL): PyTorch e TensorFlow. Ne esploreremo i concetti fondanti, le filosofie operative, le differenze chiave e l'applicazione pratica. L'obiettivo è fornire una comprensione solida di questi strumenti essenziali per chiunque operi nel campo dell'Intelligenza Artificiale e del Machine Learning. Questo capitolo è rivolto a studenti e professionisti che necessitano di basi operative sui framework DL, coprendo dai tensori e grafi computazionali fino a tecniche di training avanzate e deployment. Verranno analizzati i componenti principali, gli ecosistemi e le best practice associate a ciascun framework, concludendo con un confronto diretto e cenni agli strumenti ausiliari.
Alla base di PyTorch e TensorFlow risiedono alcuni concetti fondamentali che abilitano il Deep Learning su larga scala.
ndarray).@tf.function): Il grafo è definito prima dell'esecuzione. Ottimizzato per performance/deployment, ma debugging meno diretto..backward() in PyTorch, tf.GradientTape in TF).Dense, Conv2D, LSTM) o custom.torch.nn.Module (PyTorch), tf.keras.Model/tf.keras.layers.Layer (TensorFlow/Keras).📊 Visualizzazione: Immaginare un grafo computazionale per c = a*b + d aiuta a capire il flusso dei tensori e come Autograd risalga il grafo per i gradienti.
🏢 Caso Reale: I tensori rappresentano tutto nel DL: input (immagini 4D: Batch x H x W x C), pesi, output, gradienti. Autograd permette di addestrare modelli con milioni di parametri.
▶️ Codice: Tensori e Autograd (PyTorch & TensorFlow)
Python
import torch # PyTorch
import tensorflow as tf # TensorFlow
import numpy as np # Necessario per alcuni esempi TF
print("PyTorch Version:", torch.__version__)
print("TensorFlow Version:", tf.__version__)
# --- Tensori ---
# PyTorch
pt_scalar = torch.tensor(3.14)
pt_vector = torch.tensor([1.0, 2.0, 3.0])
pt_matrix = torch.randn(2, 3) # Matrice 2x3 con valori casuali normali
print("\\n--- PyTorch Tensori ---")
print("Scalar:", pt_scalar)
print("Vector:", pt_vector)
print("Matrix:\\n", pt_matrix)
print(f"Device di pt_matrix: {pt_matrix.device}") # Di default è 'cpu'
# Spostare su GPU (se disponibile)
if torch.cuda.is_available():
device = torch.device("cuda")
pt_matrix_gpu = pt_matrix.to(device)
print(f"Device dopo .to('cuda'): {pt_matrix_gpu.device}")
else:
print("GPU non disponibile per PyTorch.")
# TensorFlow
tf_scalar = tf.constant(3.14)
tf_vector = tf.constant([1.0, 2.0, 3.0])
tf_matrix = tf.random.normal(shape=(2, 3)) # Matrice 2x3 casuale normale
print("\\n--- TensorFlow Tensori ---")
print("Scalar:", tf_scalar)
print("Vector:", tf_vector)
print("Matrix:\\n", tf_matrix)
print(f"Device di tf_matrix: {tf_matrix.device}") # Gestione device più automatica in TF2
# Per specificare GPU in TF (di solito automatico se disponibile)
# with tf.device('/GPU:0'):
# tf_matrix_gpu = tf.random.normal(shape=(2,3))
# print(f"Device forzato su GPU: {tf_matrix_gpu.device}")
# --- Autograd ---
# PyTorch: richiede requires_grad=True per tracciare gradienti
pt_a = torch.tensor(2.0, requires_grad=True)
pt_b = torch.tensor(3.0, requires_grad=True)
pt_c = pt_a ** 2 + pt_b * 3 # c = a^2 + 3b = 4 + 9 = 13
# Calcola gradiente di c rispetto ad a e b
pt_c.backward() # Popola .grad per i tensori con requires_grad=True
print("\\n--- PyTorch Autograd ---")
print(f"c = {pt_c.item()}") # .item() per scalari Python
print(f"dc/da (atteso 2a = 4): {pt_a.grad}")
print(f"dc/db (atteso 3): {pt_b.grad}")
# TensorFlow: usa tf.GradientTape per tracciare
tf_a = tf.Variable(2.0) # tf.Variable è tracciabile di default
tf_b = tf.Variable(3.0)
with tf.GradientTape() as tape:
# Le operazioni che coinvolgono tf.Variable dentro il tape vengono registrate
tape.watch(tf_a) # Esplicito watch utile per tf.Tensor non-Variable
tape.watch(tf_b)
tf_c = tf_a ** 2 + tf_b * 3
# Calcola i gradienti di tf_c rispetto a [tf_a, tf_b]
gradients = tape.gradient(tf_c, [tf_a, tf_b])
print("\\n--- TensorFlow Autograd ---")
print(f"c = {tf_c.numpy()}") # .numpy() per convertire a valore NumPy/Python
print(f"dc/da (atteso 2a = 4): {gradients[0]}")
print(f"dc/db (atteso 3): {gradients[1]}")
# Verifica gradienti per tensori costanti (richiede watch esplicito)
tf_const_a = tf.constant(2.0)
tf_const_b = tf.constant(3.0)
with tf.GradientTape() as tape:
tape.watch(tf_const_a) # NECESSARIO per costanti
tape.watch(tf_const_b)
tf_const_c = tf_const_a ** 2 + tf_const_b * 3
gradients_const = tape.gradient(tf_const_c, [tf_const_a, tf_const_b])
print("\\n--- TensorFlow Autograd (Costanti) ---")
print(f"dc/da: {gradients_const[0]}")
print(f"dc/db: {gradients_const[1]}")
(Fine contenuto del blocco Toggle)
(Crea un blocco Toggle in Notion con il titolo seguente)
▶️ Mostra Esercizi (8.1)
(Contenuto del blocco Toggle)
tf.multiply) con un altro tensore 3x3 contenente solo il valore 2.f(x, w) = sigmoid(w*x) (usa torch.sigmoid o tf.sigmoid). Fissa x (es. x=0.5) e inizializza w come parametro tracciabile (es. w=1.0). Usando Autograd (.backward() o tf.GradientTape), calcola il gradiente df/dw. Verifica il risultato manualmente usando la derivata di sigmoid(z) (che è sigmoid(z)*(1-sigmoid(z))) e la regola della catena.