LECCIÓN 14 · AVANZADO

Técnicas pro

Tres herramientas que separan un entrenamiento amateur de uno profesional: ajustar el learning rate sobre la marcha, controlar gradientes que explotan, y crear tus propias capas.

Recuerda de las últimas lecciones: tienes GPU (lección 10), regularización (lección 11), CNNs (lección 12) y transfer learning (lección 13) en tu caja de herramientas. Hoy tres ajustes finos que marcan la diferencia en proyectos reales.

1. Learning rate schedulers: bajar el paso con el tiempo

Un learning rate fijo es un compromiso: grande al principio (avanzar rápido) pero pequeño al final (afinar sin pasarse). Un scheduler lo reduce automáticamente durante el entrenamiento. StepLR lo multiplica por gamma cada step_size épocas:

# SGD = Stochastic Gradient Descent, el optimizador más básico
# params = net.parameters() — los pesos del modelo que quieres ajustar
opt = torch.optim.SGD(net.parameters(), lr=0.1)
sched = torch.optim.lr_scheduler.StepLR(opt, step_size=2, gamma=0.5)

for epoca in range(6):
    # ... entrenar una época ...
    opt.step()
    sched.step()      ← actualizar el learning rate
salida real (lr por época)epoca 0: lr=0.1000 epoca 1: lr=0.1000 epoca 2: lr=0.0500 ← se redujo a la mitad epoca 3: lr=0.0500 epoca 4: lr=0.0250 ← otra vez epoca 5: lr=0.0250
Cada 2 épocas el lr se multiplica por 0.5. Empiezas avanzando rápido y terminas dando pasitos finos para asentarte en el mínimo. Mejora la precisión final notablemente. Llama sched.step() una vez por época, después de opt.step().

2. Gradient clipping: domar gradientes que explotan

A veces los gradientes se vuelven gigantescos y un solo paso destroza el entrenamiento (típico en redes profundas o recurrentes). El clipping los recorta para que su "longitud" (llamada norma) no supere un máximo, conservando su dirección:

w = torch.tensor([3.0, 4.0], requires_grad=True)
(w.sum() * 100).backward()      # gradiente enorme

torch.nn.utils.clip_grad_norm_([w], max_norm=1.0)
salida realgrad antes: [100.0, 100.0] (norma = 141.42) grad después: [0.7071, 0.7071] (norma = 1.0)
✍️ A mano: cómo se recorta
1) norma del gradiente (longitud del vector):
     √(100² + 100²) = √(10000 + 10000) = √20000 = 141.42
2) como 141.42 > max_norm (1.0), se reduce por el factor:
     factor = max_norm / norma = 1.0 / 141.42 = 0.007071
3) multiplicar cada componente por el factor:
     100 · 0.007071 = 0.7071
     100 · 0.007071 = 0.7071
   → misma dirección (ambos iguales), pero norma ahora = 1.0  ✓

El gradiente se redujo para que su "longitud" (norma) no pase de 1, pero mantiene su dirección (ambos componentes iguales). Va en el bucle entre backward() y step().

3. Capas y modelos a medida

Ya sabes crear modelos con nn.Module. La misma idea sirve para inventar tu propia capa: hereda de nn.Module y define su forward. Por ejemplo, una capa que aplica una transformación lineal y le suma la entrada (una "conexión residual", como en el Transformer):

class BloqueResidual(nn.Module):
    def __init__(self, dim):
        super().__init__()
        self.lin = nn.Linear(dim, dim)

    def forward(self, x):
        return x + torch.relu(self.lin(x))   # salida + entrada (residual)

bloque = BloqueResidual(4)
x = torch.tensor([[1., 2., 3., 4.]])
print(bloque(x).shape)
salida realtorch.Size([1, 4]) (misma forma que la entrada)
Esto es lo poderoso de PyTorch: como todo es nn.Module con un forward, puedes componer y crear arquitecturas arbitrarias. Las capas que ya viste (Linear, Conv2d…) son exactamente esto, escritas por el equipo de PyTorch.

4. Lo que aprendiste

Lo que aprendiste hoy: schedulers reducen el learning rate automáticamente durante el entrenamiento (StepLR lo multiplica por gamma cada N épocas); gradient clipping evita pasos catastróficos recortando la norma del gradiente antes de opt.step(); las capas custom son nn.Modules con su propio forward — igual que todo en PyTorch.

Con esto completas la teoría de PyTorch. Solo queda aplicar todo en un proyecto real de principio a fin.