Módulo / 7. Deep Learning
0%
Módulo 7 · Avanzado

Redes neuronales y Deep Learning

De la neurona artificial a los Transformers. Construiremos la intuición matemática del aprendizaje profundo: cómo fluye la información, cómo se calculan los gradientes y por qué las arquitecturas modernas funcionan.

10 lecciones 24 h estimadas Simulador de red neuronal
0 de 10 lecciones completadas
Lección 7.1

De la neurona artificial a la red

18 minIntuiciónActivaciones

Una red neuronal es una composición de funciones simples y diferenciables. Cada neurona calcula una combinación lineal de sus entradas y aplica una no linealidad. La magia no está en una neurona, sino en apilar muchas y entrenarlas con gradientes.

Al terminar sabrás

Explicar qué hace una neurona y por qué necesita sesgo.
Comparar ReLU, sigmoide, tanh y softmax.
Derivar un forward pass numérico.
Entender por qué la profundidad aumenta la capacidad expresiva.

La neurona

Neurona con activación $$z=\sum_{j=1}^{d}w_jx_j+b=\mathbf{w}^\top\mathbf{x}+b, \qquad a=\sigma(z)$$ El sesgo $b$ desplaza la frontera y permite que la neurona se active incluso con entradas cero. Sin sesgo, la frontera siempre pasa por el origen.
1

Forward pass de una neurona

Entrada $\mathbf{x}=(2,1)$, pesos $\mathbf{w}=(3,-1)$, sesgo $b=0.5$. Entonces $z=3\cdot2+(-1)\cdot1+0.5=5.5$.

Con ReLU, $a=\max(0,5.5)=5.5$. Con sigmoide, $a=1/(1+e^{-5.5})\approx0.996$. Con tanh, $a\approx0.9998$. La misma combinación lineal produce salidas distintas según la activación.

Funciones de activación

FunciónRangoVentajaProblema
Sigmoide(0,1)Interpretable como probabilidadSatisface y desvanece el gradiente
Tanh(−1,1)Centrada en ceroTambién se satura
ReLU[0,∞)Barata y con gradiente estable en positivosNeuronas muertas si el gradiente se anula
Leaky ReLU(−∞,∞)Evita neuronas muertasIntroduce un hiperparámetro
SoftmaxDistribuciónSalida multiclase normalizadaSolo para clases excluyentes
¿Por qué no linealidad? Sin activaciones no lineales, apilar capas equivale a una sola transformación lineal: $W_3(W_2(W_1x))=Wx$. La no linealidad es lo que permite aproximar funciones complejas.

Del perceptrón al MLP

El perceptrón de Rosenblatt es una neurona con umbral. No puede resolver XOR porque una sola frontera lineal no separa las clases. Un perceptrón multicapa con una capa oculta sí puede: combina varias fronteras y las compone.

x₁ x₂ h₁ h₂ h₃ ŷ entradacapa ocultasalida
Una red 2–3–1. Cada conexión es un peso; cada neurona suma sus entradas, añade sesgo y aplica activación.

Profundidad y composición

Una capa oculta puede aproximar funciones continuas con suficiente anchura (teorema de aproximación universal), pero no dice nada sobre cuántos datos o neuronas hacen falta. La profundidad permite construir representaciones jerárquicas: bordes → texturas → partes → objetos. Esa es la intuición del deep learning.

Universal no significa práctico El teorema de aproximación universal garantiza existencia, no que el descenso del gradiente encuentre esa solución con los datos disponibles. La arquitectura y la regularización siguen importando.
Ejercicio 7.1 · XOR con una capa oculta

Diseña a mano una red 2–2–1 con ReLU que calcule XOR para entradas en $\{0,1\}^2$. Pista: usa dos neuronas que detecten $(x_1+x_2\ge1)$ y $(x_1+x_2\ge2)$ y combínalas.

Orientación: una neurona calcula $h_1=\text{ReLU}(x_1+x_2-0.5)$ y otra $h_2=\text{ReLU}(x_1+x_2-1.5)$. La salida puede ser $h_1-2h_2$ con un sesgo adecuado: activa cuando exactamente una entrada es 1. Hay muchas soluciones equivalentes.

Lección 7.2

Perceptrón multicapa y forward pass

22 minPropagaciónSimulador

El forward pass es la propagación de la entrada a través de las capas hasta la salida. Es una secuencia de multiplicaciones de matrices y activaciones. Entenderlo es prerequisites para entender backpropagation.

Formulación matricial

Dos capas ocultas y salida $$\mathbf{h}^{(1)}=\sigma\left(W^{(1)}\mathbf{x}+\mathbf{b}^{(1)}\right), \qquad \mathbf{h}^{(2)}=\sigma\left(W^{(2)}\mathbf{h}^{(1)}+\mathbf{b}^{(2)}\right), \qquad \hat{\mathbf{y}}=\text{softmax}\left(W^{(3)}\mathbf{h}^{(2)}+\mathbf{b}^{(3)}\right)$$ Cada fila de $W^{(l)}$ es una neurona. En batch, las entradas se apilan por filas y las operaciones se vectorizan.

Ejemplo numérico completo

1

Una capa oculta con dos neuronas

Entrada $\mathbf{x}=(1,2)$. Capa oculta: $W^{(1)}=\begin{pmatrix}1&0\\0&1\end{pmatrix}$, $\mathbf{b}^{(1)}=(0,-1)$. Entonces $\mathbf{z}^{(1)}=(1,1)$ y con ReLU $\mathbf{h}^{(1)}=(1,1)$.

Salida: $W^{(2)}=(2,-1)$, $b^{(2)}=0.5$. $z^{(2)}=2\cdot1-1\cdot1+0.5=1.5$. Con sigmoide, $\hat y\approx0.818$. Si la etiqueta es 1, la pérdida de entropía cruzada es $-\log(0.818)\approx0.201$.

◉

Simulador · Red neuronal 2–h–1

Entrena una red pequeña para separar dos medias lunas y observa la frontera de decisión.

pérdida = —
accuracy = —
Con pocas neuronas la frontera es rígida; con muchas y learning rate alto puede volverse irregular. La pérdida debería bajar.

Funciones de pérdida

Clasificación y regresión $$L_{\text{CE}}=-\frac1n\sum_i\sum_k y_{ik}\log\hat y_{ik}, \qquad L_{\text{MSE}}=\frac1n\sum_i\|\mathbf{y}_i-\hat{\mathbf{y}}_i\|^2$$ Entropía cruzada para clasificación; MSE o MAE para regresión. La elección debe coincidir con la distribución de la salida.
Pythonmlp_numpy.py
import numpy as np

def relu(z):
    return np.maximum(0, z)

def sigmoid(z):
    return 1 / (1 + np.exp(-z))

def forward(X, params):
    W1, b1, W2, b2 = params
    h = relu(X @ W1.T + b1)
    out = sigmoid(h @ W2.T + b2)
    return h, out

X = np.array([[1.0, 2.0]])
params = (
    np.array([[1.0, 0.0], [0.0, 1.0]]),  # W1
    np.array([0.0, -1.0]),               # b1
    np.array([[2.0, -1.0]]),             # W2
    np.array([0.5])                      # b2
)
h, out = forward(X, params)
print("oculta:", h, "salida:", np.round(out, 3))
Inicialización Inicializar todos los pesos a cero rompe la simetría: todas las neuronas aprenden lo mismo. Se usa Xavier/Glorot para activaciones simétricas y He para ReLU.
Ejercicio 7.2 · Forward pass a mano

Con $\mathbf{x}=(2,-1)$, $W^{(1)}=\begin{pmatrix}1&2\\-1&1\end{pmatrix}$, $\mathbf{b}^{(1)}=(0,1)$, ReLU en la capa oculta y salida lineal $W^{(2)}=(1,1)$, $b^{(2)}=-2$, calcula la predicción.

Orientación: $z^{(1)}=(2\cdot1+(-1)\cdot2,\ 2\cdot(-1)+(-1)\cdot1+1)=(0,-2)$. ReLU da $(0,0)$. La salida es $0+0-2=-2$. Observa que una neurona puede quedar inactiva con esta entrada.

Lección 7.3

Backpropagation

26 minGradientesRegla de la cadena

Backpropagation no es un optimizador: es un algoritmo para calcular gradientes de forma eficiente. Aplica la regla de la cadena capa por capa, reutilizando cálculos intermedios.

La regla de la cadena en una red

Para una pérdida $L$ que depende de la salida $\hat y=f(g(x))$, la derivada es $\frac{dL}{dx}=\frac{dL}{df}\frac{df}{dg}\frac{dg}{dx}$. En una red, cada capa aporta un factor. Backpropagation propaga el gradiente desde la salida hacia la entrada.

Gradientes para una capa $$\delta^{(l)}=\frac{\partial L}{\partial \mathbf{z}^{(l)}}=\left(W^{(l+1)\top}\delta^{(l+1)}\right)\odot\sigma'(\mathbf{z}^{(l)}), \qquad \frac{\partial L}{\partial W^{(l)}}=\delta^{(l)}\,\mathbf{h}^{(l-1)\top}, \qquad \frac{\partial L}{\partial \mathbf{b}^{(l)}}=\delta^{(l)}$$ $\odot$ es producto elemento a elemento. Para la última capa con softmax y entropía cruzada, $\delta^{(L)}=\hat{\mathbf{y}}-\mathbf{y}$: una expresión notablemente simple.

Derivación con un ejemplo mínimo

1

Una neurona, una entrada

Sea $z=wx+b$, $a=\sigma(z)$ y $L=\frac12(y-a)^2$. Entonces $\frac{\partial L}{\partial a}=a-y$ y $\frac{\partial a}{\partial z}=a(1-a)$. Por la regla de la cadena, $\frac{\partial L}{\partial z}=(a-y)a(1-a)$. Finalmente, $\frac{\partial L}{\partial w}=\frac{\partial L}{\partial z}x$ y $\frac{\partial L}{\partial b}=\frac{\partial L}{\partial z}$.

Con $x=2$, $w=0.5$, $b=0$, $y=1$: $z=1$, $a=0.731$, $L\approx0.036$. El gradiente respecto a $w$ es $(0.731-1)\cdot0.731\cdot0.269\cdot2\approx-0.106$. Restarlo con tasa 0.1 aumenta $w$ y reduce la pérdida.

Un detalle crucial: el grafo computacional

Los frameworks construyen un grafo de operaciones durante el forward pass. Al llamar a backward(), recorren ese grafo en orden inverso y acumulan gradientes. Por eso es importante no modificar tensores in-place de forma que se rompa el grafo.

Pythonbackprop_numpy.py
import numpy as np

def sigmoid(z):
    return 1 / (1 + np.exp(-z))

def train_step(X, y, W, b, lr=0.1):
    # forward
    z = X @ W + b
    a = sigmoid(z)
    loss = np.mean(-(y * np.log(a + 1e-9) + (1 - y) * np.log(1 - a + 1e-9)))

    # backward (entropía cruzada + sigmoide)
    dz = (a - y) / len(X)          # dL/dz
    dW = X.T @ dz                  # dL/dW
    db = dz.sum(axis=0)            # dL/db

    # update
    W -= lr * dW
    b -= lr * db
    return loss, W, b

rng = np.random.default_rng(0)
X = rng.normal(size=(200, 3))
y = (X[:, 0] + X[:, 1] - X[:, 2] > 0).astype(float).reshape(-1, 1)
W = rng.normal(scale=0.1, size=(3, 1))
b = np.zeros((1, 1))

for epoch in range(500):
    loss, W, b = train_step(X, y, W, b)
print("loss final:", round(loss, 4), "W:", np.round(W.ravel(), 3))
Desvanecimiento y explosión En redes profundas, multiplicar muchos jacobianos puede hacer que el gradiente se vuelva diminuto o enorme. Las conexiones residuales, normalización y activaciones adecuadas mitigan el problema.
Ejercicio 7.3 · Gradiente de una capa

Para $L=\frac12(y-\hat y)^2$, $\hat y=w_2h$, $h=\text{ReLU}(w_1x)$, con $x=2$, $w_1=1$, $w_2=3$, $y=10$: calcula $\partial L/\partial w_2$ y $\partial L/\partial w_1$.

Orientación: $h=2$, $\hat y=6$, error $=-4$. $\partial L/\partial w_2=(6-10)\cdot2=-8$. $\partial L/\partial h=(6-10)\cdot3=-12$; como $w_1x=2>0$, ReLU tiene derivada 1, y $\partial L/\partial w_1=-12\cdot2=-24$.

Lección 7.4

Optimizadores y dinámica de entrenamiento

22 minSGD, AdamSchedules

El descenso del gradiente puro es simple pero frágil. Los optimizadores modernos adaptan la dirección y la magnitud del paso, y las schedules ajustan la tasa de aprendizaje a lo largo del entrenamiento.

SGD con momentum

Momentum $$\mathbf{v}_t=\beta\mathbf{v}_{t-1}+\nabla L(\theta_{t-1}), \qquad \theta_t=\theta_{t-1}-\eta\mathbf{v}_t$$ Acumula una media móvil del gradiente. Suaviza oscilaciones y acelera en direcciones consistentes, como una bola que gana inercia.

RMSProp y Adam

Adam (versión simplificada) $$m_t=\beta_1m_{t-1}+(1-\beta_1)g_t, \qquad v_t=\beta_2v_{t-1}+(1-\beta_2)g_t^2$$ $$\hat m_t=\frac{m_t}{1-\beta_1^t}, \qquad \hat v_t=\frac{v_t}{1-\beta_2^t}, \qquad \theta_t=\theta_{t-1}-\eta\frac{\hat m_t}{\sqrt{\hat v_t}+\epsilon}$$ Combina momento (dirección) y escala adaptativa por parámetro. Los valores por defecto $\beta_1=0.9$, $\beta_2=0.999$, $\epsilon=10^{-8}$ suelen funcionar bien.
OptimizadorVentajaCuándo usarlo
SGDSimple, buena generalización en visiónCon schedule y momentum bien ajustados
MomentumReduce oscilacionesSuperficies con valles estrechos
RMSPropEscala por parámetroGradientes de magnitudes muy distintas
AdamRobusto y rápido por defectoNLP, transformers, prototipos
AdamWAdam con weight decay correctoTransformers y modelos grandes

Schedules de tasa de aprendizaje

  • Step decay: reduce la tasa cada cierto número de épocas.
  • Cosine annealing: reduce suavemente siguiendo un coseno.
  • Warmup: empieza con una tasa muy baja y sube; estabiliza el inicio de transformers.
  • ReduceLROnPlateau: baja cuando la métrica de validación deja de mejorar.
Pythonoptimizadores.py
import torch

modelo = torch.nn.Sequential(
    torch.nn.Linear(20, 64), torch.nn.ReLU(),
    torch.nn.Linear(64, 1)
)

optimizador = torch.optim.AdamW(modelo.parameters(), lr=1e-3, weight_decay=1e-2)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizador, T_max=50)

loss_fn = torch.nn.BCEWithLogitsLoss()

for epoch in range(50):
    modelo.train()
    for xb, yb in train_loader:
        optimizador.zero_grad()
        logits = modelo(xb).squeeze(1)
        loss = loss_fn(logits, yb)
        loss.backward()
        torch.nn.utils.clip_grad_norm_(modelo.parameters(), 1.0)
        optimizador.step()
    scheduler.step()
    print(f"epoch {epoch}: lr={scheduler.get_last_lr()[0]:.5f}")
Batch size y learning rate Aumentar el batch reduce el ruido del gradiente y permite tasas mayores, pero generaliza algo peor en algunos casos. No existe una receta universal: ajusta ambos con validación.
Ejercicio 7.4 · Diagnosticar la curva de pérdida

La pérdida de entrenamiento baja, pero la de validación sube después de la época 10. ¿Qué está ocurriendo y qué tres acciones probarías?

Orientación: overfitting. Probar early stopping, regularización (dropout/weight decay) y más datos o data augmentation. También revisar si la tasa de aprendizaje es demasiado alta al final.

Lección 7.5

Regularización en redes profundas

20 minDropoutBatchNorm

Las redes tienen millones de parámetros y pueden memorizar. Regularizar es limitar su capacidad efectiva sin perder capacidad expresiva. No hay una técnica mágica: cada una ataca un mecanismo distinto.

Dropout

Desactivar al azar

Durante el entrenamiento se apagan neuronas con probabilidad $p$. Evita dependencias y actúa como ensemble implícito. En inferencia se desactiva.

Weight decay

Penalizar pesos grandes

Equivale a L2 en SGD. En Adam se prefiere AdamW, que desacopla el decay del gradiente adaptativo.

BatchNorm

Normalizar activaciones

Centra y escala por lote. Estabiliza y acelera, pero introduce dependencia del batch y puede ser problemático en lotes pequeños.

Early stopping

Parar a tiempo

Detiene el entrenamiento cuando la validación deja de mejorar. Es una de las formas más efectivas y baratas de regularizar.

Dropout en entrenamiento $$r_j\sim\text{Bernoulli}(1-p), \qquad \tilde h_j=\frac{r_j h_j}{1-p}$$ La división por $1-p$ mantiene la escala esperada de las activaciones entre entrenamiento e inferencia (inverted dropout).

Otras técnicas

  • Data augmentation: transformaciones válidas que amplían la variedad de ejemplos.
  • Label smoothing: suaviza las etiquetas duras y reduce la confianza excesiva.
  • Mixup / CutMix: combina ejemplos y etiquetas para crear datos virtuales.
  • Stochastic depth: omite bloques residuales al azar en redes muy profundas.
  • Gradient clipping: limita la norma del gradiente, útil en RNN y transformers.
No regularices a ciegas Si el modelo tiene underfitting, añadir dropout o weight decay empeora el resultado. Primero diagnostica: ¿la pérdida de entrenamiento baja? ¿La brecha con validación es grande?

Receta de diagnóstico

SíntomaCausa probableAcción
Train alta, val altaUnderfitting o bugMás capacidad, mejor arquitectura, revisar datos
Train baja, val altaOverfittingRegularizar, más datos, early stopping
Ambas estancadasLearning rate malo o gradientes nulosAjustar lr, revisar activaciones e inicialización
Pérdida NaNExplosión numéricaBajar lr, gradient clipping, revisar log(0)
Ejercicio 7.5 · Elegir regularización

Una CNN pequeña alcanza 99% en train y 68% en validación con 2 000 imágenes. Propón un plan de mejora ordenado por coste.

Orientación: (1) data augmentation, (2) early stopping y ajuste de lr, (3) weight decay/dropout, (4) transfer learning desde un modelo preentrenado, (5) recolectar más datos. La augmentation suele dar la mayor mejora por coste en visión.

Lección 7.6

Redes convolucionales

24 minConvoluciónPooling

Una capa densa sobre una imagen de 224×224×3 tendría 150 000 pesos por neurona y destruiría la estructura espacial. Una convolución comparte un filtro pequeño en todas las posiciones: menos parámetros, invariancia a traslaciones y detección local de patrones.

La operación

Convolución 2D discreta $$(I*K)(i,j)=\sum_{m}\sum_{n}I(i+m,j+n)K(m,n)$$ El kernel $K$ se desliza por la imagen. En deep learning los valores del kernel se aprenden; en visión clásica se diseñaban a mano (Sobel, Gabor).

Hiperparámetros de una capa

  • Filtros: cuántos patrones distintos aprende la capa.
  • Kernel size: 3×3 es el estándar; apilar dos 3×3 cubre un campo receptivo de 5×5 con menos parámetros.
  • Stride: paso del kernel. Stride 2 reduce la resolución.
  • Padding: mantiene el tamaño espacial con same o lo reduce con valid.
  • Canales: la profundidad de entrada y salida; cada filtro produce un mapa de activación.
Tamaño de salida $$H_{\text{out}}=\left\lfloor\frac{H_{\text{in}}+2P-K}{S}\right\rfloor+1$$ Ejemplo: entrada 32, kernel 3, padding 1, stride 1 → salida 32. Con stride 2 → salida 16.
1

Campo receptivo

Dos convoluciones 3×3 seguidas tienen un campo receptivo efectivo de 5×5. Tres convoluciones 3×3 cubren 7×7. Una sola convolución 7×7 tendría $49C^2$ parámetros por canal, mientras que tres capas 3×3 tienen $3\cdot9C^2=27C^2$ y más no linealidad intermedia. Por eso las arquitecturas modernas prefieren kernels pequeños apilados.

Pooling y estructura típica

El max pooling resume regiones locales y reduce la dimensión espacial. Una CNN clásica alterna convoluciones, activaciones y pooling, y termina con capas densas para clasificar.

Pythoncnn_pytorch.py
import torch
import torch.nn as nn

class CNN(nn.Module):
    def __init__(self, n_classes=10):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.ReLU(),
            nn.MaxPool2d(2),                       # 32x32 -> 16x16
            nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(),
            nn.MaxPool2d(2),                       # 16x16 -> 8x8
            nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.ReLU(),
            nn.AdaptiveAvgPool2d((1, 1))           # 8x8 -> 1x1
        )
        self.classifier = nn.Linear(128, n_classes)

    def forward(self, x):
        x = self.features(x)
        x = torch.flatten(x, 1)
        return self.classifier(x)

modelo = CNN()
print(sum(p.numel() for p in modelo.parameters()), "parámetros")
Inductive bias La convolución asume que los patrones son locales y se repiten en el espacio. Esa suposición es correcta en imágenes y por eso las CNN son tan eficientes. En datos tabulares, esa estructura no existe y las CNN suelen aportar poco.
Ejercicio 7.6 · Calcular dimensiones

Una entrada de 64×64×3 pasa por: Conv 3×3, 32 filtros, padding 1, stride 1; MaxPool 2×2; Conv 3×3, 64 filtros, padding 1; MaxPool 2×2. ¿Cuál es la forma de salida y cuántos parámetros tiene la primera convolución (incluyendo sesgos)?

Orientación: tras la primera conv: 64×64×32; tras pool: 32×32×32; tras segunda conv: 32×32×64; tras pool: 16×16×64. La primera conv tiene $3\cdot3\cdot3\cdot32+32=896$ parámetros.

Lección 7.7

Arquitecturas CNN y transferencia

18 minResNetTransfer learning

Las arquitecturas modernas no se inventan desde cero: se reutilizan modelos preentrenados en ImageNet y se ajustan a la tarea. Es la forma más práctica de obtener buenos resultados con pocos datos.

Evolución en una tabla

ArquitecturaIdea claveLegado
LeNet-5Conv + pooling + densasPrimera CNN exitosa en dígitos
AlexNetReLU, dropout, GPUPunto de inflexión de ImageNet 2012
VGGBloques de 3×3 apiladosSimplicidad y profundidad
ResNetConexiones residualesEntrenar cientos de capas
InceptionMódulos multi-escalaEficiencia computacional
EfficientNetEscalado compuestoBuen equilibrio precisión/coste
ViTTransformers sobre parchesCompiten con CNN a gran escala

ResNet: la conexión que lo cambió todo

Bloque residual $$\mathbf{y}=\mathcal{F}(\mathbf{x})+\mathbf{x}$$ La red aprende el residuo $\mathcal{F}$ en lugar de la transformación completa. La conexión de identidad facilita el flujo del gradiente y permite entrenar redes muy profundas.

Transfer learning

  1. Elige un modelo preentrenado en un dataset grande y similar.
  2. Congela el extractor de features y entrena una cabeza nueva para tu tarea.
  3. Si tienes suficientes datos, descongela algunas capas y ajusta con una tasa pequeña (fine-tuning).
  4. Usa augmentation y early stopping para evitar sobreajustar.
Pythontransfer.py
import torch
import torch.nn as nn
from torchvision import models

# Modelo preentrenado en ImageNet
modelo = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

# Congelar el extractor
for param in modelo.parameters():
    param.requires_grad = False

# Reemplazar la cabeza para nuestro número de clases
modelo.fc = nn.Linear(modelo.fc.in_features, n_clases)

# Solo la cabeza se entrena al principio
optimizador = torch.optim.AdamW(modelo.fc.parameters(), lr=1e-3)

# Fine-tuning opcional: descongelar el último bloque
# for param in modelo.layer4.parameters():
#     param.requires_grad = True
# optimizador = torch.optim.AdamW(modelo.parameters(), lr=1e-4)
Cuándo el transfer learning no ayuda Si tu dominio es muy distinto (por ejemplo, imágenes médicas de otro tipo de sensor) o el modelo preentrenado es demasiado pequeño, el beneficio puede ser limitado. Aun así, suele ser una baseline mejor que entrenar desde cero con pocos datos.
Ejercicio 7.7 · Diseñar un transfer learning

Tienes 800 imágenes de 5 tipos de plantas y acceso a ResNet-18 preentrenada. Describe el plan de entrenamiento, qué congelarías y cómo evaluarías.

Orientación: dividir estratificado; congelar el extractor y entrenar la cabeza con AdamW y augmentation; validar con 5-fold; si hay overfitting, aumentar regularización; si el rendimiento se estanca y hay datos, descongelar layer4 con lr baja; reportar matriz de confusión y F1 macro.

Lección 7.8

RNN, LSTM y secuencias

20 minRecurrenciaMemoria

En una secuencia, el orden importa. Una RNN procesa un elemento a la vez y mantiene un estado oculto que resume el pasado. Es la base histórica del modelado de lenguaje, series temporales y audio.

Recurrencia simple $$\mathbf{h}_t=\tanh\left(W_{xh}\mathbf{x}_t+W_{hh}\mathbf{h}_{t-1}+\mathbf{b}_h\right), \qquad \mathbf{y}_t=W_{hy}\mathbf{h}_t+\mathbf{b}_y$$ El mismo conjunto de pesos se reutiliza en cada paso. El estado oculto transporta información, pero sufre desvanecimiento del gradiente en secuencias largas.

LSTM y GRU

La LSTM introduce una celda de memoria y compuertas que deciden qué recordar, qué olvidar y qué exponer. La GRU simplifica la idea con dos compuertas. Ambas mitigan el problema del gradiente que se desvanece.

Compuertas LSTM $$f_t=\sigma(W_f[\mathbf{h}_{t-1},\mathbf{x}_t]+b_f), \quad i_t=\sigma(W_i[\mathbf{h}_{t-1},\mathbf{x}_t]+b_i), \quad o_t=\sigma(W_o[\mathbf{h}_{t-1},\mathbf{x}_t]+b_o)$$ $$c_t=f_t\odot c_{t-1}+i_t\odot\tanh(W_c[\mathbf{h}_{t-1},\mathbf{x}_t]+b_c), \qquad h_t=o_t\odot\tanh(c_t)$$ $f$ olvida, $i$ escribe y $o$ lee. La celda $c_t$ puede conservar información durante muchos pasos.
Pythonlstm.py
import torch
import torch.nn as nn

class ClasificadorSecuencia(nn.Module):
    def __init__(self, vocab, emb=64, hidden=128, clases=2):
        super().__init__()
        self.embedding = nn.Embedding(vocab, emb, padding_idx=0)
        self.lstm = nn.LSTM(emb, hidden, batch_first=True, bidirectional=True)
        self.dropout = nn.Dropout(0.3)
        self.fc = nn.Linear(hidden * 2, clases)

    def forward(self, x, lengths=None):
        e = self.embedding(x)
        packed = nn.utils.rnn.pack_padded_sequence(
            e, lengths.cpu(), batch_first=True, enforce_sorted=False
        )
        _, (h, _) = self.lstm(packed)
        h = torch.cat([h[-2], h[-1]], dim=1)  # último estado de ambas direcciones
        return self.fc(self.dropout(h))
Recurrencia vs. atención Las RNN procesan secuencialmente y no paralelizan bien. Los Transformers eliminan la recurrencia y usan atención, lo que permite entrenar con secuencias largas en paralelo. Por eso dominan el NLP moderno.

Series temporales

En series temporales, la validación debe respetar el tiempo. Además, features como lags, medias móviles y calendario suelen ser más importantes que la arquitectura. Una LSTM no compensa un mal diseño de ventanas o fugas del futuro.

Ejercicio 7.8 · Ventanas y fuga temporal

Para predecir la demanda de mañana, propones una LSTM con features de los últimos 30 días. ¿Qué harías para evitar fuga de información al normalizar y al construir las ventanas?

Orientación: calcular la normalización solo con el conjunto de entrenamiento (o usar estadísticas móviles causales), construir ventanas que terminen en $t-1$ para predecir $t$, y validar con particiones temporales, nunca con divisiones aleatorias.

Lección 7.9

Atención y Transformers

24 minSelf-attentionArquitectura

La atención permite que cada token consulte a todos los demás y decida a cuáles prestar atención. El Transformer eliminó la recurrencia y se convirtió en la arquitectura dominante de NLP y, cada vez más, de visión y audio.

Scaled dot-product attention $$\text{Attention}(Q,K,V)=\text{softmax}\left(\frac{QK^\top}{\sqrt{d_k}}\right)V$$ $Q$ consultas, $K$ claves, $V$ valores. El producto $QK^\top$ mide compatibilidad; la escala $1/\sqrt{d_k}$ evita saturar el softmax; el resultado es una mezcla ponderada de valores.

Multi-head attention

En lugar de una sola atención, se proyectan $Q,K,V$ en $h$ subespacios y se concatenan. Cada cabeza puede especializarse en relaciones distintas: sintaxis, correferencia, posición. La salida se proyecta de nuevo a la dimensión del modelo.

El bloque Transformer

  1. Embedding de tokens + codificación posicional.
  2. Multi-head self-attention con conexión residual y LayerNorm.
  3. Feed-forward por posición (dos capas lineales con activación).
  4. Otra conexión residual + LayerNorm.
  5. Se apilan $N$ bloques; la salida se usa para predecir o representar.
Entrada + posición Multi-Head Attention Feed-Forward + LayerNorm Salida contextual residual
Bloque Transformer simplificado. Las conexiones residuales permiten apilar decenas de bloques sin que el gradiente se desvanezca.
Pythontransformer.py
import torch
import torch.nn as nn

class BloqueTransformer(nn.Module):
    def __init__(self, d_model=128, n_heads=4, d_ff=256, dropout=0.1):
        super().__init__()
        self.attention = nn.MultiheadAttention(d_model, n_heads, dropout=dropout, batch_first=True)
        self.norm1 = nn.LayerNorm(d_model)
        self.ff = nn.Sequential(
            nn.Linear(d_model, d_ff), nn.GELU(), nn.Dropout(dropout),
            nn.Linear(d_ff, d_model)
        )
        self.norm2 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, mask=None):
        attn, _ = self.attention(x, x, x, attn_mask=mask, need_weights=False)
        x = self.norm1(x + self.dropout(attn))
        x = self.norm2(x + self.dropout(self.ff(x)))
        return x

modelo = BloqueTransformer()
entrada = torch.randn(8, 20, 128)  # batch, secuencia, dimensión
print(modelo(entrada).shape)
Complejidad cuadrática La atención compara todos los pares de tokens: $O(n^2d)$. Para secuencias muy largas se usan variantes eficientes, ventanas locales o aproximaciones. Es una de las limitaciones activas de los LLM.
Ejercicio 7.9 · Formas y máscaras

En un batch de 8 secuencias de longitud 20 con dimensión 128 y 4 cabezas, ¿qué forma tienen $Q,K,V$ dentro de una cabeza? ¿Para qué sirve una máscara causal?

Orientación: cada cabeza proyecta a $d_k=128/4=32$; las formas son $(8,4,20,32)$ o, con batch primero, $(8,20,4,32)$ según implementación. La máscara causal impide que un token vea el futuro durante el entrenamiento autoregresivo.

Lección 7.10

Entrenamiento práctico y depuración

20 minPipelineCierre

Entrenar una red no es solo elegir una arquitectura. Es construir un pipeline reproducible, comprobar que aprende, detectar fugas, controlar el overfitting y comparar contra baselines honestas.

Checklist antes de escalar

  1. Overfit un batch pequeño: si la red no puede memorizar 10 ejemplos, hay un bug en datos, etiquetas o gradientes.
  2. Verifica formas y rangos: imprime shapes, medias y mínimos/máximos de las entradas.
  3. Comprueba la pérdida inicial: para $K$ clases balanceadas, la entropía inicial ronda $\log K$.
  4. Visualiza ejemplos y augmentations: errores de etiqueta o transformaciones destructivas explican muchos fracasos.
  5. Fija semillas y registra versiones: sin reproducibilidad no puedes comparar experimentos.
El bug más común La mayoría de los «modelos que no aprenden» tienen un problema de datos: etiquetas desalineadas, normalización incorrecta, fuga de información o una tasa de aprendizaje absurda. Revisa el pipeline antes de cambiar la arquitectura.

Entrenamiento con buenas prácticas

Pythontrain_loop.py
import torch
from torch.utils.data import DataLoader

def evaluar(modelo, loader, loss_fn):
    modelo.eval()
    total, correct, perdida = 0, 0, 0.0
    with torch.no_grad():
        for xb, yb in loader:
            xb, yb = xb.to(device), yb.to(device)
            logits = modelo(xb)
            perdida += loss_fn(logits, yb).item() * len(xb)
            correct += (logits.argmax(1) == yb).sum().item()
            total += len(xb)
    return perdida / total, correct / total

mejor_val = float("inf")
paciencia = 5
espera = 0

for epoch in range(50):
    modelo.train()
    for xb, yb in train_loader:
        xb, yb = xb.to(device), yb.to(device)
        opt.zero_grad()
        loss = loss_fn(modelo(xb), yb)
        loss.backward()
        torch.nn.utils.clip_grad_norm_(modelo.parameters(), 1.0)
        opt.step()

    val_loss, val_acc = evaluar(modelo, val_loader, loss_fn)
    print(f"epoch {epoch:02d} val_loss={val_loss:.4f} val_acc={val_acc:.3f}")

    if val_loss < mejor_val - 1e-4:
        mejor_val = val_loss
        espera = 0
        torch.save(modelo.state_dict(), "mejor_modelo.pt")
    else:
        espera += 1
        if espera >= paciencia:
            print("early stopping")
            break
Ejercicio 7.10 · Cierre del módulo

Diseña un experimento para clasificar imágenes médicas con 3 000 ejemplos y 4 clases. Incluye arquitectura, inicialización, augmentations, validación, criterio de parada y qué reportarías. ¿Qué harías si el modelo tiene 99% en train y 55% en validación?

Orientación: transfer learning desde ResNet/EfficientNet, augmentation moderada y validada por un especialista, split estratificado y por paciente, early stopping, F1 macro y matriz de confusión. Ante la brecha, revisar fuga entre pacientes, aumentar regularización, congelar más capas y recolectar más datos.

Curso de Machine Learning · teoría, matemáticas y práctica en Python Progreso local · 0% completado · Hecho para estudiar en cualquier lugar