💡 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.


⚙️ 8.1 Concetti Comuni ai Framework DL

Alla base di PyTorch e TensorFlow risiedono alcuni concetti fondamentali che abilitano il Deep Learning su larga scala.

📊 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)

  1. Operazioni Tensoriali: Crea un tensore 3x3 sia in PyTorch che in TensorFlow contenente numeri da 1 a 9. Esegui un'operazione di moltiplicazione elemento per elemento ( o tf.multiply) con un altro tensore 3x3 contenente solo il valore 2.
  2. Autograd Manuale: Definisci una funzione 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.