📖 Teoría

Autoencoders

De la compresión inteligente a la generación creativa: fundamentos matemáticos del autoencoder, el espacio latente, variantes (denoising, sparse, convolucional), el Autoencoder Variacional (VAE) con ELBO, KL divergence y reparameterization trick, y su papel como piedra fundacional de la IA generativa moderna.

🔬 ¿Qué es un Autoencoder?

Un autoencoder es una red neuronal que aprende a copiar su entrada en su salida — pero pasando por un cuello de botella que la obliga a aprender una representación comprimida de los datos. Parece trivial, pero ese cuello de botella es lo que lo hace poderoso.

💡

Definición formal: Un autoencoder es una función \( f: \mathbb{R}^n \to \mathbb{R}^n \) compuesta por un encoder \( g_\phi: \mathbb{R}^n \to \mathbb{R}^d \) y un decoder \( f_\theta: \mathbb{R}^d \to \mathbb{R}^n \), donde \( d < n \), tal que:

$$ \hat{x} = f_\theta(g_\phi(x)) \approx x $$

El objetivo es minimizar la diferencia entre \( x \) y \( \hat{x} \), forzando a la red a aprender las características más importantes de los datos.

🏗️ Arquitectura Encoder-Decoder

Todo autoencoder tiene dos partes simétricas conectadas por el espacio latente:

ENCODER g_φ input x n dimensiones z espacio latente d dimensiones DECODER f_θ output x̂ d ≪ n (bottleneck)
🔽 Encoder \( g_\phi \)

Comprime la entrada \( x \in \mathbb{R}^n \) a una representación \( z \in \mathbb{R}^d \) donde \( d \ll n \). Aprende a extraer las características esenciales.

🎯 Espacio Latente \( z \)

Representación comprimida de baja dimensión. Captura la estructura esencial de los datos. Es el «corazón» del autoencoder.

🔼 Decoder \( f_\theta \)

Reconstruye la entrada original a partir de \( z \). Si la reconstrucción es buena, el espacio latente captura la información relevante.

📉 Función de Pérdida: Reconstrucción

El autoencoder se entrena minimizando la pérdida de reconstrucción: la diferencia entre la entrada \( x \) y la salida \( \hat{x} \). Las dos opciones más comunes:

Loss Fórmula Caso de uso
MSE (Mean Squared Error) $$ \mathcal{L}_\text{MSE} = \frac{1}{n}\sum_{i=1}^{n}(x_i - \hat{x}_i)^2 $$ Datos continuos (imágenes normalizadas [0,1], señales)
BCE (Binary Cross-Entropy) $$ \mathcal{L}_\text{BCE} = -\frac{1}{n}\sum_{i=1}^{n}\left[x_i\log\hat{x}_i + (1-x_i)\log(1-\hat{x}_i)\right] $$ Datos binarios o valores en [0,1] (imágenes binarizadas)
⚠️

La loss no mide «calidad visual»: MSE penaliza diferencias pixel a pixel, pero una imagen puede ser perceptualmente buena con MSE alto (o viceversa). Por eso existen losses perceptuales, pero para empezar, MSE y BCE funcionan bien.

📐 Undercomplete vs Overcomplete

La relación entre la dimensión de entrada \( n \) y la dimensión latente \( d \) define el tipo de autoencoder:

Undercomplete (d < n)

El bottleneck fuerza a la red a aprender las características más importantes. No puede simplemente copiar la entrada. Este es el caso estándar.

$$ d \ll n \implies \text{compresión} $$
Overcomplete (d ≥ n)

Sin restricciones adicionales, la red puede aprender la identidad (copiar todo sin aprender nada útil). Se necesita regularización (sparsity, denoising, etc.) para que sea útil.

$$ d \geq n \implies \text{necesita regularización} $$

🔗 Conexión con PCA

Si usamos un autoencoder lineal (sin funciones de activación) con loss MSE, la solución óptima es exactamente PCA (Principal Component Analysis):

$$ \text{AE lineal} + \text{MSE} \iff \text{PCA (subespacio de máxima varianza)} $$

Sea \( W_e \in \mathbb{R}^{d \times n} \) la matriz del encoder y \( W_d \in \mathbb{R}^{n \times d} \) la del decoder. La reconstrucción es:

$$ \hat{x} = W_d W_e x $$

Minimizar \( \|x - W_d W_e x\|^2 \) sobre todos los datos equivale a encontrar la proyección de rango \( d \) que maximiza la varianza explicada. Por el teorema de Eckart-Young, la solución óptima son los primeros \( d \) componentes principales de la matriz de covarianza de los datos.

Pero: al añadir no-linealidades (ReLU, sigmoid...), el autoencoder puede capturar manifolds no lineales que PCA no puede ver. Esa es su ventaja.

🎓

PCA vs Autoencoder no lineal:

  • PCA: encuentra el mejor subespacio lineal. Rápido, interpretable, limitado.
  • AE no lineal: encuentra un manifold no lineal. Más expresivo, puede capturar estructuras complejas.
  • El AE es una generalización no lineal de PCA.

🌌 El Espacio Latente: el corazón del autoencoder

El espacio latente es la representación de baja dimensión que el autoencoder aprende internamente. Es el punto donde la red ha destilado la información esencial, descartando redundancia y ruido.

🧠

Intuición: Imagina que tienes 10.000 fotos de caras. Cada imagen tiene 784 píxeles (28×28), pero la «esencia» de cada cara se puede describir con muchos menos números: forma de la nariz, color de ojos, ángulo de la cabeza... El espacio latente captura esos factores de variación en un vector compacto \( z \).

El Bottleneck y la Hipótesis del Manifold

La hipótesis del manifold dice que los datos reales de alta dimensión viven en un subespacio (manifold) de dimensión mucho menor. El autoencoder aprende este manifold:

1
Datos en alta dimensión: Una imagen MNIST es un vector en \( \mathbb{R}^{784} \), pero el «espacio de dígitos válidos» tiene dimensión intrínseca mucho menor (~10-20).
2
Bottleneck fuerza compresión: Si \( d = 2 \), el encoder debe comprimir 784 dimensiones en 2. Solo puede hacerlo si encuentra la estructura subyacente.
3
El decoder reconstruye: Si la reconstrucción es buena, el espacio latente de 2D captura la esencia del manifold de dígitos.
MANIFOLD HYPOTHESIS ℝ⁷⁸⁴ (alta dim) manifold encoder ℝ² (latente) clusters decoder ℝ⁷⁸⁴ (reconstruido)

🗺️ Visualización del Espacio Latente

Cuando \( d = 2 \), podemos visualizar directamente el espacio latente como un scatter plot. Para \( d > 2 \), usamos técnicas como t-SNE o UMAP para proyectar a 2D.

🖼️

Si entrenas un autoencoder con \( d = 2 \) sobre MNIST, cada dígito forma un cluster en el espacio latente. Los dígitos similares (como 4 y 9) estarán cerca. Esto demuestra que el espacio latente captura la semántica de los datos.

🔄 Interpolación en el Espacio Latente

Una propiedad fascinante: si tomamos dos puntos \( z_A \) y \( z_B \) en el espacio latente y generamos puntos intermedios, el decoder produce transiciones suaves entre las imágenes correspondientes:

$$ z_\alpha = (1 - \alpha) \cdot z_A + \alpha \cdot z_B, \quad \alpha \in [0, 1] $$
A α=0 α=0.25 α=0.5 α=0.75 B α=1 decoder(z_α) → transición suave

Esta interpolación es clave: demuestra que el espacio latente es continuo y tiene estructura semántica. Es el fundamento de la generación de contenido nuevo.

⚠️

Problema: En un autoencoder estándar, la interpolación no siempre funciona bien. Los puntos intermedios pueden caer en «zonas muertas» del espacio latente que nunca se vieron durante el entrenamiento. Este es uno de los problemas que el VAE resuelve.

🧪 Explorador interactivo: Espacio Latente 2D

Experimenta con un espacio latente bidimensional simulado. Mueve el punto en el espacio latente y observa cómo cambia la «reconstrucción»:

0.00
0.00
Espacio latente
«Reconstrucción» simulada

💡 Nota: esta es una simulación didáctica. z₁ controla la «forma» y z₂ el «estilo». En un autoencoder real, los ejes latentes suelen no estar tan perfectamente alineados con conceptos humanos (a menos que uses β-VAE).

🗂️ Tipos de Autoencoders

El autoencoder «vanilla» es solo el punto de partida. Existen variantes que añaden regularización o estructura para mejorar las representaciones aprendidas. Aquí están las más importantes:

🧹 Denoising Autoencoder (DAE)

Idea: Corrompe la entrada con ruido y entrena al autoencoder a reconstruir la versión limpia. Esto fuerza a la red a aprender la estructura real de los datos, no artefactos del ruido.

$$ \tilde{x} = x + \epsilon, \quad \epsilon \sim \mathcal{N}(0, \sigma^2 I) $$ $$ \mathcal{L}_\text{DAE} = \|x - f_\theta(g_\phi(\tilde{x}))\|^2 $$
x (limpio) +ruido x̃ (ruidoso) AE x̂ ≈ x ✓ L = ‖x − x̂‖²
💡

¿Por qué funciona? Al ver muchas versiones ruidosas del mismo dato, la red aprende la distribución subyacente, no los detalles superficiales. Es equivalente a aprender el score function de la distribución — ¡la misma idea que subyace a los modelos de difusión!

Sparse Autoencoder

Idea: Permite un espacio latente overcomplete (\( d \geq n \)), pero añade una penalización de sparsity para que solo unas pocas neuronas estén activas a la vez. Cada entrada activa un subconjunto diferente de neuronas.

$$ \mathcal{L}_\text{sparse} = \|x - \hat{x}\|^2 + \lambda \sum_{j} |\hat{\rho}_j - \rho| $$

Donde \( \hat{\rho}_j \) es la activación media de la neurona \( j \) y \( \rho \) es el nivel de sparsity objetivo (típicamente \( \rho = 0.05 \), es decir, solo el 5% de las neuronas activas).

Una alternativa más suave es usar la divergencia KL entre la distribución de activaciones y una Bernoulli con parámetro \( \rho \):

$$ \Omega_\text{sparse} = \sum_{j=1}^{d} \text{KL}(\rho \| \hat{\rho}_j) = \sum_j \rho \log\frac{\rho}{\hat{\rho}_j} + (1-\rho)\log\frac{1-\rho}{1-\hat{\rho}_j} $$

Esto penaliza suavemente las neuronas que se desvían del nivel de sparsity deseado.

🔍

Aplicación moderna: Los sparse autoencoders se están usando hoy (2024-2025) para interpretar redes neuronales grandes (LLMs). Anthropic y OpenAI los usan para descomponer las activaciones internas de modelos de lenguaje en features interpretables. ¡Un uso que nadie anticipó!

📏 Contractive Autoencoder (CAE)

Idea: Añade una penalización sobre el jacobiano del encoder, forzando a que la representación latente sea insensible a pequeñas perturbaciones en la entrada:

$$ \mathcal{L}_\text{CAE} = \|x - \hat{x}\|^2 + \lambda \left\| \frac{\partial g_\phi(x)}{\partial x} \right\|_F^2 $$

La norma de Frobenius del jacobiano mide cuánto cambia \( z \) cuando \( x \) cambia un poco. Minimizarla hace que el encoder sea «suave» — resistente a variaciones irrelevantes.

CriterioDAECAE
RegularizaciónImplícita (ruido)Explícita (jacobiano)
Coste computacionalBajo (solo añadir ruido)Alto (calcular jacobiano)
Conexión teóricaScore matchingManifold tangente
En la prácticaMás usadoMás elegante, menos práctico

Rifai et al. (2011) demostró que ambos aprenden representaciones similares cuando están bien ajustados. DAE es más práctico; CAE tiene garantías teóricas más fuertes.

🔲 Autoencoder Convolucional

Para imágenes, usar capas densas es ineficiente. Los autoencoders convolucionales usan Conv2D en el encoder y ConvTranspose2D (o upsampling) en el decoder:

E
Encoder: Conv2D → BatchNorm → ReLU → MaxPool (repite, reduciendo resolución) → Flatten → \( z \)
D
Decoder: \( z \) → Dense → Reshape → ConvTranspose2D → BatchNorm → ReLU (repite, aumentando resolución) → Sigmoid → \( \hat{x} \)
import torch
import torch.nn as nn

class ConvAutoencoder(nn.Module):
    def __init__(self, latent_dim=32):
        super().__init__()
        # Encoder: 1x28x28 → 32x7x7 → latent
        self.encoder = nn.Sequential(
            nn.Conv2d(1, 16, 3, stride=2, padding=1),  # 16x14x14
            nn.BatchNorm2d(16), nn.ReLU(),
            nn.Conv2d(16, 32, 3, stride=2, padding=1), # 32x7x7
            nn.BatchNorm2d(32), nn.ReLU(),
            nn.Flatten(),
            nn.Linear(32*7*7, latent_dim)
        )
        # Decoder: latent → 32x7x7 → 1x28x28
        self.decoder = nn.Sequential(
            nn.Linear(latent_dim, 32*7*7),
            nn.Unflatten(1, (32, 7, 7)),
            nn.ConvTranspose2d(32, 16, 3, stride=2, padding=1, output_padding=1),
            nn.BatchNorm2d(16), nn.ReLU(),
            nn.ConvTranspose2d(16, 1, 3, stride=2, padding=1, output_padding=1),
            nn.Sigmoid()
        )

    def forward(self, x):
        z = self.encoder(x)
        return self.decoder(z), z

model = ConvAutoencoder(latent_dim=16)
print(f"Parámetros: {sum(p.numel() for p in model.parameters()):,}")
# Loss: nn.MSELoss() o nn.BCELoss()
import tensorflow as tf
from tensorflow.keras import layers, Model

def build_conv_autoencoder(latent_dim=32):
    # Encoder
    encoder_input = layers.Input(shape=(28, 28, 1))
    x = layers.Conv2D(16, 3, strides=2, padding='same', activation='relu')(encoder_input)
    x = layers.BatchNormalization()(x)
    x = layers.Conv2D(32, 3, strides=2, padding='same', activation='relu')(x)
    x = layers.BatchNormalization()(x)
    x = layers.Flatten()(x)
    z = layers.Dense(latent_dim, name='latent')(x)
    encoder = Model(encoder_input, z, name='encoder')

    # Decoder
    decoder_input = layers.Input(shape=(latent_dim,))
    x = layers.Dense(7 * 7 * 32, activation='relu')(decoder_input)
    x = layers.Reshape((7, 7, 32))(x)
    x = layers.Conv2DTranspose(16, 3, strides=2, padding='same', activation='relu')(x)
    x = layers.BatchNormalization()(x)
    x = layers.Conv2DTranspose(1, 3, strides=2, padding='same', activation='sigmoid')(x)
    decoder = Model(decoder_input, x, name='decoder')

    # Full autoencoder
    ae_output = decoder(encoder(encoder_input))
    autoencoder = Model(encoder_input, ae_output, name='autoencoder')
    autoencoder.compile(optimizer='adam', loss='mse')
    return autoencoder, encoder, decoder

ae, enc, dec = build_conv_autoencoder(latent_dim=16)
ae.summary()

📋 Resumen comparativo

Tipo Regularización Fórmula clave Ventaja
Vanilla Solo bottleneck \( d < n \) Simple, línea base
Denoising Ruido en la entrada \( \tilde{x} = x + \epsilon \) Robusto, aprende distribución
Sparse Penalización activaciones \( \lambda \sum |\hat{\rho}_j - \rho| \) Features interpretables
Contractive Penalización jacobiano \( \lambda \|J\|_F^2 \) Insensible a perturbaciones
Convolucional Estructura espacial Conv2D + ConvTranspose2D Eficiente para imágenes
VAE → Probabilístico \( \text{KL}(q_\phi \| p) \) Generativo, espacio continuo

🌟 VAE: de reconstruir a generar

El autoencoder estándar aprende a reconstruir, pero no a generar. ¿Por qué? Porque su espacio latente no tiene estructura probabilística: los puntos se distribuyen de forma irregular, con «huecos» donde el decoder no sabe qué producir.

🎯

El problema fundamental: Si tomo un punto aleatorio \( z \) del espacio latente de un AE estándar y lo paso por el decoder, el resultado suele ser basura. El espacio latente no es continuo ni está regularizado.

La solución del VAE: Forzar al espacio latente a seguir una distribución conocida (Gaussiana estándar). Así, cualquier punto muestreado de \( \mathcal{N}(0, I) \) produce una salida coherente.

AE ESTÁNDAR vs VAE AE estándar — latente irregular zona muerta 💀 VAE — latente regularizado ~ N(0,I) cualquier z ~ N(0,I) → salida coherente ✓

📐 Formulación Probabilística del VAE

El VAE (Kingma & Welling, 2013) reformula el autoencoder como un modelo generativo probabilístico:

1
Prior: Asumimos que las variables latentes siguen una distribución conocida: \( p(z) = \mathcal{N}(0, I) \)
2
Likelihood (decoder): La distribución de los datos dado \( z \): \( p_\theta(x|z) \) — parametrizada por el decoder
3
Posterior: Queremos \( p(z|x) \), pero es intratable (requiere integrar sobre todo \( z \))
4
Aproximación variacional (encoder): Aproximamos \( p(z|x) \) con \( q_\phi(z|x) = \mathcal{N}(\mu_\phi(x), \sigma^2_\phi(x) I) \) — parametrizada por el encoder
💡

Clave: El encoder no produce un punto \( z \), sino los parámetros de una distribución (\( \mu \) y \( \sigma \)). Cada entrada \( x \) se mapea a una «nube» gaussiana en el espacio latente, no a un punto fijo.

📊 ELBO: la función objetivo del VAE

El objetivo del VAE es maximizar la Evidence Lower BOund (ELBO), que es una cota inferior de la log-verosimilitud de los datos:

$$ \log p(x) \geq \underbrace{\mathbb{E}_{q_\phi(z|x)}[\log p_\theta(x|z)]}_{\text{reconstrucción}} - \underbrace{D_\text{KL}(q_\phi(z|x) \| p(z))}_{\text{regularización}} = \text{ELBO} $$
📐 Término de Reconstrucción
$$ \mathbb{E}_{q_\phi(z|x)}[\log p_\theta(x|z)] $$

«¿Qué tan bien reconstruye el decoder?» Equivale a la loss de reconstrucción (MSE o BCE). Queremos maximizar esto.

🎯 Término de Regularización (KL)
$$ D_\text{KL}(q_\phi(z|x) \| p(z)) $$

«¿Qué tan cerca está la distribución latente de N(0,I)?» Penaliza desviaciones. Queremos minimizar esto.

En la práctica, la loss del VAE es:

$$ \mathcal{L}_\text{VAE} = \underbrace{\|x - \hat{x}\|^2}_{\text{reconstrucción}} + \underbrace{\frac{1}{2}\sum_{j=1}^{d}\left(\mu_j^2 + \sigma_j^2 - \log\sigma_j^2 - 1\right)}_{\text{KL con N(0,I)}} $$

Para dos gaussianas univariadas \( q = \mathcal{N}(\mu, \sigma^2) \) y \( p = \mathcal{N}(0, 1) \):

$$ D_\text{KL}(q \| p) = \int q(z) \log\frac{q(z)}{p(z)} dz $$

Desarrollando con las densidades gaussianas:

$$ = \frac{1}{2}\left(\mu^2 + \sigma^2 - \log\sigma^2 - 1\right) $$

Para \( d \) dimensiones independientes, simplemente sumamos. Esta fórmula cerrada es una de las razones por las que el VAE usa gaussianas: la KL se calcula analíticamente, sin necesidad de Monte Carlo.

🔧 Reparameterization Trick

El problema: En el forward pass, necesitamos muestrear \( z \sim q_\phi(z|x) = \mathcal{N}(\mu, \sigma^2) \). Pero el muestreo es una operación no diferenciable — no podemos hacer backpropagation a través de él.

La solución: Reparametrizar el muestreo separando la parte estocástica:

$$ z = \mu_\phi(x) + \sigma_\phi(x) \odot \epsilon, \quad \epsilon \sim \mathcal{N}(0, I) $$
❌ Sin reparametrización encoder sample decoder ∇ bloqueado ❌ no se puede hacer backprop ✅ Con reparametrización encoder → μ, σ z = μ + σ·ε diferenciable! ε~N(0,I) externo (no trainable) decoder ∇ fluye por μ y σ ✅
🧠

Genial truco: En vez de muestrear de \( \mathcal{N}(\mu, \sigma^2) \), muestreamos \( \epsilon \) de \( \mathcal{N}(0, 1) \) (fijo) y luego hacemos \( z = \mu + \sigma \cdot \epsilon \). Los gradientes fluyen a través de \( \mu \) y \( \sigma \) sin problemas. El \( \epsilon \) es solo ruido externo, no parte del grafo computacional.

🎨 Generación con VAE

Una vez entrenado, generar nuevas muestras es trivial:

1
Muestrear: \( z \sim \mathcal{N}(0, I) \) — un punto aleatorio del espacio latente
2
Decodificar: \( \hat{x} = f_\theta(z) \) — pasar por el decoder
3
Resultado: Una nueva muestra que parece «real» porque el espacio latente está organizado como \( \mathcal{N}(0, I) \)
⚖️

El trade-off reconstrucción vs regularización:

  • Si priorizas reconstrucción: El espacio latente puede ser irregular, pero las reconstrucciones son nítidas.
  • Si priorizas KL: El espacio latente es suave y regular, pero las reconstrucciones pueden ser borrosas.
  • Las imágenes VAE tienden a ser ligeramente borrosas — este es el «precio» de un espacio latente bien regularizado. Los GANs y modelos de difusión abordan esto de otras formas.

🧪 Explorador interactivo: VAE

Experimenta con los parámetros de un VAE y observa el trade-off entre reconstrucción y regularización:

0.50
1.00
1.00

🎛️ β-VAE: Representaciones Disentangled

Higgins et al. (2017) propusieron una modificación simple pero poderosa: multiplicar el término KL por un factor \( \beta \):

$$ \mathcal{L}_{\beta\text{-VAE}} = \mathbb{E}[\|x - \hat{x}\|^2] + \boldsymbol{\beta} \cdot D_\text{KL}(q_\phi(z|x) \| p(z)) $$
βEfectoTrade-off
β = 0 Autoencoder estándar (sin regularización) Buena reconstrucción, espacio latente irregular
β = 1 VAE estándar Balance reconstrucción / regularización
β > 1 Mayor presión hacia N(0,I) Representaciones disentangled, reconstrucción más borrosa
β ≫ 1 El latente colapsa a N(0,I) «Posterior collapse»: pierde toda la información
🧬

¿Qué significa «disentangled»? Que cada dimensión del espacio latente controla un factor de variación independiente. Por ejemplo, en caras: z₁ = sonrisa, z₂ = orientación, z₃ = gafas... Cambiar un eje cambia solo un atributo.

Con β > 1, el VAE está más «presionado» a distribuir la información en dimensiones ortogonales e independientes.

La KL con \( p(z) = \mathcal{N}(0, I) \) puede descomponerse (Chen et al. 2018) en:

$$ D_\text{KL}(q(z|x) \| p(z)) = \underbrace{I(x; z)}_{\text{info mutua}} + \underbrace{D_\text{KL}(q(z) \| p(z))}_{\text{marginal matching}} $$

Con β alto, se penaliza especialmente la información mutua, forzando al modelo a usar solo la información mínima necesaria en cada dimensión latente. Esto induce independencia estadística entre las dimensiones → disentanglement.

🏷️ Conditional VAE (CVAE)

El CVAE (Sohn et al. 2015) condiciona tanto el encoder como el decoder en una etiqueta \( y \) (clase, atributo, texto...):

$$ q_\phi(z|x, y), \quad p_\theta(x|z, y) $$

Esto permite generar con control: «genera un dígito 7» o «genera una cara con gafas».

E
Encoder: recibe \( [x, y] \) (concatenados) → produce \( \mu, \sigma \)
D
Decoder: recibe \( [z, y] \) (concatenados) → produce \( \hat{x} \)
G
Generación: elige \( y \) (e.g., clase «7»), muestrea \( z \sim \mathcal{N}(0,I) \), decodifica \( [z, y] \) → dígito 7 generado

🧊 VQ-VAE: Cuantización Vectorial

Van den Oord et al. (2017) propusieron el VQ-VAE, que usa un espacio latente discreto en lugar de continuo:

1
El encoder produce un vector continuo \( z_e(x) \)
2
Se busca el embedding más cercano en un codebook \( e_k \): \( z_q = e_k \) donde \( k = \arg\min_j \|z_e - e_j\|^2 \)
3
El decoder recibe \( z_q \) (el embedding discreto)
$$ \mathcal{L}_\text{VQ-VAE} = \|x - \hat{x}\|^2 + \|z_e - \text{sg}[e_k]\|^2 + \beta\|\text{sg}[z_e] - e_k\|^2 $$

Donde \( \text{sg}[\cdot] \) es «stop gradient». Los tres términos son: reconstrucción, commitment loss (acercar encoder a embeddings), y codebook loss (acercar embeddings a encoder).

🔥

¿Por qué importa? VQ-VAE es el ancestro directo de DALL-E (OpenAI, 2021), que usa VQ-VAE para tokenizar imágenes y luego un Transformer para generar secuencias de tokens de imagen. También inspira Stable Diffusion, que opera en el espacio latente de un autoencoder (de ahí «Latent Diffusion Model»).

💻 Implementación completa: VAE

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

class VAE(nn.Module):
    def __init__(self, input_dim=784, hidden_dim=400, latent_dim=20):
        super().__init__()
        # Encoder
        self.fc1 = nn.Linear(input_dim, hidden_dim)
        self.fc_mu = nn.Linear(hidden_dim, latent_dim)
        self.fc_logvar = nn.Linear(hidden_dim, latent_dim)
        # Decoder
        self.fc3 = nn.Linear(latent_dim, hidden_dim)
        self.fc4 = nn.Linear(hidden_dim, input_dim)

    def encode(self, x):
        h = F.relu(self.fc1(x))
        return self.fc_mu(h), self.fc_logvar(h)

    def reparameterize(self, mu, logvar):
        """El famoso reparameterization trick"""
        std = torch.exp(0.5 * logvar)  # σ = exp(logσ²/2)
        eps = torch.randn_like(std)     # ε ~ N(0, I)
        return mu + std * eps           # z = μ + σ·ε

    def decode(self, z):
        h = F.relu(self.fc3(z))
        return torch.sigmoid(self.fc4(h))

    def forward(self, x):
        mu, logvar = self.encode(x.view(-1, 784))
        z = self.reparameterize(mu, logvar)
        return self.decode(z), mu, logvar

def vae_loss(x_recon, x, mu, logvar):
    """ELBO loss = reconstrucción + KL"""
    # Reconstrucción (BCE porque sigmoid en decoder)
    recon = F.binary_cross_entropy(x_recon, x.view(-1, 784), reduction='sum')
    # KL divergence con N(0,I) — fórmula cerrada
    kl = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
    return recon + kl

# Entrenamiento
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = VAE(latent_dim=20).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

train_loader = DataLoader(
    datasets.MNIST('./data', train=True, download=True,
                   transform=transforms.ToTensor()),
    batch_size=128, shuffle=True
)

for epoch in range(10):
    model.train()
    total_loss = 0
    for x, _ in train_loader:
        x = x.to(device)
        x_recon, mu, logvar = model(x)
        loss = vae_loss(x_recon, x, mu, logvar)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        total_loss += loss.item()
    print(f"Epoch {epoch+1}, Loss: {total_loss/len(train_loader.dataset):.2f}")

# Generar nuevas muestras
model.eval()
with torch.no_grad():
    z = torch.randn(16, 20).to(device)  # Muestrear de N(0,I)
    samples = model.decode(z)            # Decodificar
    # samples tiene forma (16, 784) → reshape a (16, 1, 28, 28)
import tensorflow as tf
from tensorflow.keras import layers, Model
import numpy as np

class Sampling(layers.Layer):
    """Reparameterization trick como capa de Keras"""
    def call(self, inputs):
        mu, log_var = inputs
        eps = tf.random.normal(shape=tf.shape(mu))
        return mu + tf.exp(0.5 * log_var) * eps

class VAE(Model):
    def __init__(self, latent_dim=20, **kwargs):
        super().__init__(**kwargs)
        self.latent_dim = latent_dim

        # Encoder
        self.encoder_dense = layers.Dense(400, activation='relu')
        self.mu_layer = layers.Dense(latent_dim)
        self.logvar_layer = layers.Dense(latent_dim)
        self.sampling = Sampling()

        # Decoder
        self.decoder_dense1 = layers.Dense(400, activation='relu')
        self.decoder_out = layers.Dense(784, activation='sigmoid')

    def encode(self, x):
        h = self.encoder_dense(x)
        return self.mu_layer(h), self.logvar_layer(h)

    def decode(self, z):
        h = self.decoder_dense1(z)
        return self.decoder_out(h)

    def call(self, x):
        x_flat = tf.reshape(x, [-1, 784])
        mu, logvar = self.encode(x_flat)
        z = self.sampling([mu, logvar])
        x_recon = self.decode(z)

        # Añadir KL loss
        kl_loss = -0.5 * tf.reduce_sum(
            1 + logvar - tf.square(mu) - tf.exp(logvar), axis=1
        )
        self.add_loss(tf.reduce_mean(kl_loss))
        return x_recon

# Entrenamiento
(x_train, _), _ = tf.keras.datasets.mnist.load_data()
x_train = x_train.astype('float32') / 255.0

vae = VAE(latent_dim=20)
vae.compile(optimizer='adam', loss='binary_crossentropy')
vae.fit(x_train.reshape(-1, 784), x_train.reshape(-1, 784),
        epochs=10, batch_size=128, validation_split=0.1)

# Generar
z_sample = np.random.normal(size=(16, 20)).astype('float32')
generated = vae.decode(z_sample).numpy().reshape(-1, 28, 28)

🌍 Aplicaciones de los Autoencoders

🔍
Detección de anomalías
Entrena sobre datos «normales». Si la reconstrucción de un nuevo dato es mala (loss alta), es una anomalía. Usado en fraude, defectos industriales, ciberseguridad.
dato nuevo normal / anomalía
📦
Compresión aprendida
El espacio latente es una representación comprimida. Más eficiente que JPEG para dominios específicos. Usado en compresión de imágenes médicas y satelitales.
imagen z comprimido
🎨
Generación (VAE)
Muestrear z ~ N(0,I) y decodificar produce nuevas muestras. Caras, moléculas, diseños de fármacos, música, arquitectura.
z aleatorio muestra nueva
🧹
Denoising
Limpiar imágenes ruidosas, restaurar documentos antiguos, mejorar señales de audio. El DAE es especialmente bueno aquí.
dato ruidoso dato limpio
🧬
Drug Discovery
VAE sobre representaciones moleculares (SMILES) para generar nuevos fármacos candidatos interpolando en el espacio latente.
molécula molécula nueva
📊
Feature Learning
El espacio latente como input para otros modelos (clasificación, clustering). Preentrenamiento no supervisado antes de fine-tuning supervisado.
datos sin etiquetar features latentes

🧪 Widget: Detector de anomalías con AE

Simula cómo un autoencoder detecta anomalías basándose en el error de reconstrucción:

0.80
15
3

🌐 Los Autoencoders en el Ecosistema Generativo

Los autoencoders no son un modelo aislado — son el fundamento conceptual de gran parte de la IA generativa moderna:

ÁRBOL GENEALÓGICO DE LA IA GENERATIVA Autoencoders ~1986 (Rumelhart) VAE Kingma 2013 VQ-VAE van den Oord 2017 DAE / Score Vincent 2008 β-VAE / CVAE DALL-E Latent Diffusion Stable Diffusion Diffusion Models DDPM, Score SDE Modelos Modernos DALL-E 2/3, Midjourney, SD XL Los autoencoders proporcionan la base conceptual: espacio latente, encoder-decoder, reconstrucción
🔗

Conexiones clave con otros modelos (que verás en los siguientes submódulos):

  • GANs: Resuelven el problema de las imágenes borrosas del VAE usando un discriminador adversarial. El VAE-GAN combina ambos.
  • Difusión: El DAE es el ancestro conceptual de los modelos de difusión (denoising iterativo). Stable Diffusion opera en el espacio latente de un autoencoder.
  • Transformers/LLMs: VQ-VAE tokeniza imágenes para que los Transformers las procesen como secuencias. Los tokenizers de texto son, en cierto sentido, encoders.

🏛️ El Legado de los Autoencoders

Concepto del AEDónde aparece hoy
Espacio latente Latent Diffusion, CLIP, embeddings en general
Encoder-Decoder Seq2Seq, U-Net, Transformers (encoder-decoder)
Bottleneck Information bottleneck theory, compression
Reparameterization trick Gumbel-Softmax, normalizing flows, score models
ELBO Inferencia variacional en todo el ML bayesiano
Denoising DDPM, score matching, data augmentation
VQ (cuantización) DALL-E, Codex, tokenización de imágenes/audio
🎓

Lo que has aprendido en este submódulo:

  1. Qué es un autoencoder y cómo funciona la arquitectura encoder-decoder
  2. El espacio latente: compresión, manifolds, interpolación
  3. Variantes: denoising, sparse, contractive, convolucional
  4. El VAE: formulación probabilística, ELBO, KL divergence, reparameterization trick
  5. VAE avanzado: β-VAE (disentanglement), CVAE, VQ-VAE
  6. Implementación completa en PyTorch y TensorFlow
  7. Aplicaciones prácticas y la conexión con el resto de la IA generativa

Con estos fundamentos, estás preparado para entender GANs, modelos de difusión y Transformers — todos ellos construyen sobre las ideas que acabas de aprender.

🏭 Casos de uso