Guía de Trabajo Práctico 5#

Materia: Aprendizaje Profundo Basado en la Física (optativa del Instituto Balseiro)
Docente: José I. Robledo
Edición: abril-mayo 2026

Modelos generativos: VAE, NF, GAN y difusión#

Dependiendo del conjunto de datos, estos modelos ya se pueden volver demandantes en los recursos computacionales. Recomiendo utilizar las GPUs de Google Colab para el entrenamiento.


Ejercicio 1 — VAE sobre CelebA#

Vamos a usar un Variational Autoencoder sobre una versión reducida de CelebA. Para que el entrenamiento sea razonable dentro de una notebook, trabajaremos con imágenes en escala de grises de 56 x 56.

  1. Cargar un subconjunto con load_celeba_subset(...). El conjunto es grande, puede utilizar un conjunto reducido de 50000 datos si desea acelerar el aprendizaje.

  2. Inspeccionar algunas caras reales y comentar qué variaciones dominan en el dataset.

  3. Plantear un VAE que tome de entrada image_shape=(1, 56, 56).

  4. Entrenar el modelo usando ELBO. (opcional: agregar un schedule para el peso de la divergencia KL, para que comience como AE y termine como VAE).

  5. Mostrar reconstrucciones del conjunto de prueba.

  6. Muestrear del espacio latente y visualizar caras nuevas.

  7. Explorar que hiperparámetros cambiaría para mejorar la calidad de la generación

#!pip -q install kagglehub
import kagglehub

# Download latest version
path = kagglehub.dataset_download("jessicali9530/celeba-dataset")

print("Path to dataset files:", path)

Ejercicio 2 — Normalizing Flow con zuko y MAF#

En este ejercicio vas a ajustar un Masked Autoregressive Flow (MAF) usando zuko sobre un patrón bidimensional de dos medias lunas. El objetivo es que la idea de modelar densidades exactas se vea de forma geométrica y con una implementación compacta.

  1. Generar un dataset 2D cuya densidad de probabilidad conste de tres densidades gaussianas centradas en distintos valores medios y con distintos anchos. Puede modificar la densidad de probabilidad para simplificar/complicar la distribución objetivo si lo desea.

  2. Graficar la nube real para inspeccionar su forma.

  3. Instanciar un flow con zuko.flows.MAF(features=2, ...).

  4. Entrenarlo por máxima verosimilitud, minimizandoel negativo de la log verosimilitud -flow().log_prob(x).

  5. Muestrear del modelo entrenado con flow().sample((n,)) y comparar nube real vs nube generada.

  6. Repetir el ejercicio con un RealNVP y comparar resultado respecto a MAF.


Ejercicio 3 — GAN sobre configuraciones del modelo de Ising 2D#

Ahora trabajaremos con ising_dataset.pt, que contiene configuraciones del modelo de Ising ya simuladas. Cada muestra puede verse como una imagen binaria con espines -1/+1, según el preprocesado.

  1. Cargar el subconjunto de ising_dataset.pt utilizado anteriormente. Preprocesarlo para seleccionar solo las imagenes por encima de la T_c.

  2. Inspeccionar varias configuraciones reales y discutir qué estructura espacial aparece.

  3. Entrenar una GAN de las vistas en el curso a eleccion.

  4. Visualizar muestras generadas y compararlas con las reales.

  5. Monitorear d_loss y g_loss durante el entrenamiento.

  6. Repetir el ejercicio pero esta vez utilizando solo las imagenes por debajo de T_c. Las imágenes generadas, se parecen a las utilizadas para entrenar?

  7. (Opcional) repetir el ejercicio de la guía 02, pero con un conjunto de imágenes generadas por encima y por debajo de la T_c, utilizando ambos modelos entrenados. Logramos entrenar un clasificador con estos datos?


Ejercicio 4 — Modelo de difusión sobre señales amortiguadas#

Para cerrar, vamos a usar un DDPM simple sobre señales 1D. Acá las muestras son vectores cortos, así que un predictor de ruido tipo MLP resulta suficiente para experimentar.

  1. Generar un dataset con make_damped_signal_dataset(...).

  2. Visualizar algunas señales reales para identificar amplitud, frecuencia y amortiguamiento.

  3. Instanciar DiffusionSchedule y NoisePredictor.

  4. Entrenar el modelo con pérdida MSE sobre el ruido predicho.

  5. Muestrear señales nuevas con sample_ddpm(...) y evaluar la calidad de generación visualmente.

def make_damped_signal_dataset(n_samples=2048, n_points=96):
    t = torch.linspace(0.0, 2.0, n_points)
    signals = []
    params = []
    for _ in range(n_samples):
        amplitude = 0.7 + 0.5 * torch.rand(1)
        beta = 0.15 + 0.6 * torch.rand(1)
        omega = 3.0 + 4.5 * torch.rand(1)
        phase = 2 * torch.pi * torch.rand(1)
        signal = amplitude * torch.exp(-beta * t) * torch.sin(omega * t + phase)
        signal = signal + 0.03 * torch.randn_like(signal)
        signals.append(signal)
        params.append(torch.cat([amplitude, beta, omega, phase]))
    return t, torch.stack(signals), torch.stack(params)

# Clases auxiliares --
# Dejo estas clases por si las quieren utilizar. Pueden generar las suyas propias si lo desean.
class SinusoidalTimeEmbedding(nn.Module):
    def __init__(self, dim, max_period=10000, total_steps=100):
        super().__init__()
        self.dim = dim
        self.max_period = max_period
        self.total_steps = total_steps

    def forward(self, t):
        half_dim = self.dim // 2
        scale = math.log(self.max_period) / max(half_dim - 1, 1)
        freqs = torch.exp(-scale * torch.arange(half_dim, device=t.device, dtype=torch.float32))
        angles = (t.float().unsqueeze(1) / max(self.total_steps - 1, 1)) * freqs.unsqueeze(0) * 1000.0
        emb = torch.cat([torch.sin(angles), torch.cos(angles)], dim=1)
        if self.dim % 2 == 1:
            emb = F.pad(emb, (0, 1))
        return emb


class NoisePredictor(nn.Module):
    def __init__(self, data_dim, time_dim=128, hidden_dim=256, total_steps=100):
        super().__init__()
        self.time_embedding = nn.Sequential(
            SinusoidalTimeEmbedding(time_dim, total_steps=total_steps),
            nn.Linear(time_dim, hidden_dim),
            nn.SiLU(),
            nn.Linear(hidden_dim, hidden_dim),
        )
        self.net = nn.Sequential(
            nn.Linear(data_dim + hidden_dim, hidden_dim),
            nn.SiLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.SiLU(),
            nn.Linear(hidden_dim, data_dim),
        )

    def forward(self, x, t):
        t_emb = self.time_embedding(t)
        return self.net(torch.cat([x, t_emb], dim=1))
    
@torch.no_grad()
def sample_ddpm(model, schedule, n_samples, data_dim, device=DEVICE):
    model.eval()
    schedule = schedule.to(device)
    x = torch.randn(n_samples, data_dim, device=device)
    snapshots = []
    capture_steps = {schedule.T - 1, schedule.T // 2, schedule.T // 4, 0}

    for step in reversed(range(schedule.T)):
        t = torch.full((n_samples,), step, device=device, dtype=torch.long)
        predicted_noise = model(x, t)
        beta_t = schedule.extract(schedule.betas, t, x.shape)
        sqrt_one_minus_alpha_bar_t = schedule.extract(schedule.sqrt_one_minus_alpha_bars, t, x.shape)
        sqrt_recip_alpha_t = schedule.extract(schedule.sqrt_recip_alphas, t, x.shape)
        mean = sqrt_recip_alpha_t * (x - beta_t * predicted_noise / sqrt_one_minus_alpha_bar_t)
        if step > 0:
            posterior_var_t = schedule.extract(schedule.posterior_variance, t, x.shape).clamp_min(1e-20)
            x = mean + torch.sqrt(posterior_var_t) * torch.randn_like(x)
        else:
            x = mean
        if step in capture_steps:
            snapshots.append((step, x.detach().cpu()))

    return x, snapshots