17. Entrenamiento de un modelo en PyTorch

17.1 Introducción

En el tema anterior aprendimos a construir una red neuronal simple en PyTorch. Definimos su arquitectura, sus capas, su función forward y vimos cómo producir una salida a partir de una entrada.

Pero hasta ese momento el modelo todavía no había aprendido. Solo habíamos creado su estructura y lo habíamos hecho funcionar.

En este tema vamos a estudiar el paso que realmente convierte a una red en un modelo útil: el entrenamiento. Aquí es donde la red ajusta sus parámetros para reducir el error y mejorar sus predicciones.

17.2 Qué significa entrenar un modelo

Entrenar un modelo significa mostrarle ejemplos de datos y permitir que ajuste sus parámetros internos para producir mejores resultados.

En términos prácticos, entrenar consiste en repetir un ciclo como este:

  1. Dar una entrada a la red.
  2. Obtener una predicción.
  3. Comparar la predicción con el valor correcto.
  4. Medir el error.
  5. Calcular cómo deberían cambiar los parámetros.
  6. Actualizar esos parámetros.

Ese ciclo se repite muchas veces hasta que el modelo mejora lo suficiente.

17.3 Relación con la teoría ya estudiada

Todo lo que ocurre durante el entrenamiento ya lo habíamos visto conceptualmente en temas anteriores.

Por ejemplo:

  • La red hace forward propagation.
  • Se calcula una función de pérdida.
  • Se obtienen gradientes mediante backpropagation.
  • Los parámetros se actualizan con un optimizador.

La diferencia es que ahora veremos cómo todo eso se implementa concretamente en PyTorch.

17.4 Qué necesitamos para entrenar

Para entrenar un modelo en PyTorch, en general necesitamos cuatro elementos básicos:

  • Un conjunto de datos de entrada.
  • Un modelo.
  • Una función de pérdida.
  • Un optimizador.

Sin estos componentes no podemos cerrar el ciclo de aprendizaje.

El modelo produce predicciones, la pérdida mide el error y el optimizador usa ese error para ajustar parámetros.

17.5 Un ejemplo pequeño y didáctico

Para aprender, conviene trabajar con un caso muy simple. Imaginemos que queremos que una red aprenda una relación entre entradas y salidas numéricas.

Podríamos tener datos de este tipo:

entradas = [[1.0], [2.0], [3.0], [4.0]]
salidas = [[2.0], [4.0], [6.0], [8.0]]

Este ejemplo representa un problema de regresión, no de clasificación. Decimos que es regresión porque la salida esperada es un valor numérico continuo y no una categoría como “sí” o “no”.

Si observamos los datos, vemos además una relación muy clara:

salida = 2 * entrada

Por ejemplo, cuando la entrada es 3.0, la salida esperada es 6.0; cuando la entrada es 4.0, la salida esperada es 8.0. Entonces, lo que queremos que la red aprenda es esa regla numérica sencilla que conecta cada entrada con su salida.

No buscamos todavía un problema realista, sino un ejemplo que permita entender el mecanismo.

17.6 Representar los datos como tensores

Como estamos trabajando en PyTorch, los datos deben representarse con tensores.

import torch
X = torch.tensor([[1.0], [2.0], [3.0], [4.0]])
y = torch.tensor([[2.0], [4.0], [6.0], [8.0]])

Aquí:

  • X contiene las entradas.
  • y contiene las salidas correctas.

Usamos X en mayúscula porque representa el conjunto completo de datos de entrada, es decir, varias observaciones juntas. En muchos contextos se reserva la minúscula x para una sola observación o un solo dato de entrada.

X = lo que le damos al modelo.

y = lo que queremos que aprenda a predecir.

Observa que ambos tensores tienen forma de lote: varias filas y una sola característica o salida por fila.

17.7 Definir un modelo sencillo

Para este ejemplo podemos usar una red muy simple con una sola capa lineal.

import torch.nn as nn

class ModeloSimple(nn.Module):
    def __init__(self):
        super().__init__()
        self.lineal = nn.Linear(1, 1)

    def forward(self, x):
        return self.lineal(x)

Este modelo recibe una entrada de tamaño 1 y produce una salida de tamaño 1.

Es un caso muy pequeño, pero perfecto para estudiar el entrenamiento sin distraernos con una arquitectura complicada.

17.8 Crear la instancia del modelo

Una vez definida la clase, debemos crear el modelo:

modelo = ModeloSimple()

Desde ese momento, el modelo ya tiene parámetros iniciales. Esos parámetros todavía no están bien ajustados, pero ya existen y pueden modificarse durante el entrenamiento.

17.9 Elegir una función de pérdida

Para entrenar, necesitamos medir qué tan equivocada es la red. Para eso usamos una función de pérdida.

En problemas sencillos de regresión, una elección muy común es el error cuadrático medio, representado en PyTorch por nn.MSELoss().

criterio = nn.MSELoss()

Esta función compara la salida predicha con la salida real y produce un valor numérico que resume el error.

17.10 Elegir un optimizador

Después de medir el error, necesitamos un mecanismo para ajustar los parámetros. Ese mecanismo es el optimizador.

Una opción clásica y muy didáctica es SGD (descenso del gradiente estocástico).

import torch.optim as optim
optimizador = optim.SGD(modelo.parameters(), lr=0.01)

Aquí le estamos diciendo a PyTorch:

  • Qué parámetros debe actualizar.
  • Qué estrategia de optimización utilizará.
  • Qué tasa de aprendizaje usará, en este caso 0.01.

17.11 Qué es la tasa de aprendizaje

La tasa de aprendizaje, o learning rate, indica qué tan grande será cada ajuste de parámetros.

Si es demasiado grande, el modelo puede volverse inestable. Si es demasiado pequeña, el entrenamiento puede volverse muy lento.

Por eso, elegirla bien es importante. En ejemplos sencillos suele usarse un valor pequeño como punto de partida.

17.12 El ciclo básico de entrenamiento

Con todos los elementos listos, el entrenamiento sigue casi siempre una secuencia fundamental:

  1. Hacer la predicción.
  2. Calcular la pérdida.
  3. Poner gradientes en cero.
  4. Calcular gradientes con backward().
  5. Actualizar parámetros con step().

Este es el corazón del entrenamiento en PyTorch.

17.13 Primera etapa: forward

El primer paso del ciclo es hacer que el modelo procese las entradas y produzca una salida:

prediccion = modelo(X)

Aquí el modelo toma el tensor de entrada X y genera una salida.

Esta etapa corresponde a la propagación hacia adelante que ya habíamos estudiado en teoría.

17.14 Segunda etapa: calcular la pérdida

Una vez obtenida la predicción, debemos medir qué tan lejos está del valor correcto:

perdida = criterio(prediccion, y)

El resultado será un tensor escalar que resume el error total según la función elegida.

Cuanto más pequeña sea la pérdida, mejor estará funcionando el modelo sobre esos datos.

17.15 Por qué hay que poner gradientes en cero

Antes de calcular nuevos gradientes, en PyTorch normalmente debemos limpiar los gradientes anteriores:

optimizador.zero_grad()

Esto es necesario porque PyTorch acumula gradientes por defecto. Si no los reiniciamos, los nuevos gradientes se sumarían a los anteriores y obtendríamos actualizaciones incorrectas.

Este paso es muy importante y suele olvidarse al principio.

17.16 Tercera etapa: backward

Después de calcular la pérdida, le pedimos a PyTorch que obtenga automáticamente los gradientes:

perdida.backward()

Esta llamada activa el sistema de autograd y calcula cómo afecta cada parámetro al valor final de la pérdida.

En otras palabras, aquí ocurre la parte práctica del backpropagation.

17.17 Cuarta etapa: actualizar parámetros

Una vez que los gradientes están disponibles, el optimizador puede usarlos para cambiar los parámetros:

optimizador.step()

Este paso modifica pesos y bias intentando reducir la pérdida en la siguiente iteración.

Si repetimos este proceso muchas veces, el modelo debería ir mejorando gradualmente.

17.18 Un ciclo completo en pocas líneas

Si juntamos todo, el ciclo básico queda así:

prediccion = modelo(X)
perdida = criterio(prediccion, y)
optimizador.zero_grad()
perdida.backward()
optimizador.step()

Estas pocas líneas representan el núcleo del entrenamiento de muchísimos modelos en PyTorch.

17.19 Qué es una época

Un modelo no aprende con una sola actualización. Normalmente necesitamos repetir el ciclo muchas veces.

Cada vez que el modelo recorre el conjunto de datos y realiza un paso completo de ajuste, hablamos de una época.

Por eso es habitual envolver el ciclo de entrenamiento en un bucle:

for epoca in range(100):
    ...

La cantidad de épocas dependerá del problema, del modelo y de la velocidad de aprendizaje.

17.20 Ejemplo completo de entrenamiento

Veamos un ejemplo completo y sencillo:

import torch
import torch.nn as nn
import torch.optim as optim

X = torch.tensor([[1.0], [2.0], [3.0], [4.0]])
y = torch.tensor([[2.0], [4.0], [6.0], [8.0]])

class ModeloSimple(nn.Module):
    def __init__(self):
        super().__init__()
        self.lineal = nn.Linear(1, 1)

    def forward(self, x):
        return self.lineal(x)

modelo = ModeloSimple()
criterio = nn.MSELoss()
optimizador = optim.SGD(modelo.parameters(), lr=0.01)

for epoca in range(100):
    prediccion = modelo(X)
    perdida = criterio(prediccion, y)
    optimizador.zero_grad()
    perdida.backward()
    optimizador.step()

Este ejemplo ya entrena de verdad un modelo simple.

17.21 Mostrar la pérdida durante el entrenamiento

Para entender si el modelo está mejorando, suele ser útil imprimir la pérdida cada cierta cantidad de épocas.

for epoca in range(100):
    prediccion = modelo(X)
    perdida = criterio(prediccion, y)
    optimizador.zero_grad()
    perdida.backward()
    optimizador.step()

    if (epoca + 1) % 10 == 0:
        print(epoca + 1, perdida.item())

De esta forma podemos observar si la pérdida baja con el tiempo.

17.22 Qué significa perdida.item()

La variable perdida es un tensor. Si queremos obtener su valor numérico como un dato de Python, usamos item().

print(perdida.item())

Esto resulta muy útil para mostrar el error en pantalla o guardarlo en una lista para luego graficarlo.

17.23 Qué esperamos ver durante el entrenamiento

En un caso bien planteado, la pérdida debería tender a bajar a medida que pasan las épocas.

No necesariamente disminuirá de forma perfecta en cada iteración, pero en general esperamos una tendencia descendente.

Si eso ocurre, es una señal de que el modelo está aprendiendo a ajustar sus parámetros en la dirección correcta.

17.24 Qué pasa si la pérdida no baja

Si la pérdida no disminuye, puede deberse a varias causas:

  • La tasa de aprendizaje es inadecuada.
  • La arquitectura del modelo no sirve para el problema.
  • Los datos están mal preparados.
  • La función de pérdida no corresponde al tipo de tarea.
  • Existe un error en el código.

Por eso, observar la pérdida no solo sirve para medir progreso, sino también para detectar problemas.

17.25 Entrenamiento no es evaluación

Otro punto importante es no confundir el entrenamiento con la evaluación del modelo.

Durante el entrenamiento, el modelo ajusta sus parámetros. Durante la evaluación, en cambio, verificamos cómo se comporta una vez entrenado.

En este tema nos enfocamos principalmente en el proceso de aprendizaje. Más adelante veremos con más detalle cómo evaluar correctamente el desempeño del modelo.

17.26 Entrenamiento con lotes

En ejemplos pequeños podemos usar todo el conjunto de datos de una sola vez. Sin embargo, en problemas reales es habitual dividir los datos en lotes o batches.

Eso significa que en cada iteración el modelo ve solo una parte del conjunto total.

Aunque todavía no profundizaremos en DataLoader, conviene saber desde ahora que entrenar por lotes es una práctica muy común.

17.27 El papel de model.train()

En PyTorch existe un modo de entrenamiento que se activa con:

modelo.train()

En redes muy simples esto puede no cambiar demasiado, pero en modelos que incluyen capas como Dropout o Batch Normalization es muy importante.

Por eso es una buena costumbre activar explícitamente el modo entrenamiento antes de comenzar el ciclo.

17.28 Inspeccionar la predicción después de entrenar

Una vez terminado el entrenamiento, podemos probar el modelo con una entrada y observar la salida:

prueba = torch.tensor([[5.0]])
print(modelo(prueba))

Si el modelo aprendió bien la relación de los datos, debería producir un valor razonable para esa nueva entrada.

Esto ayuda a ver que el entrenamiento produjo un cambio real en el comportamiento del modelo.

17.29 Qué no hace falta memorizar

Al comenzar, no es necesario memorizar cada línea del ciclo de entrenamiento de manera mecánica. Lo verdaderamente importante es comprender el sentido de cada paso.

Un estudiante debería poder responder preguntas como estas:

  • ¿Por qué calculamos una pérdida?
  • ¿Por qué limpiamos gradientes?
  • ¿Qué hace backward()?
  • ¿Qué hace el optimizador?

Si esas ideas están claras, el código se vuelve mucho más fácil de recordar y usar correctamente.

17.30 Errores comunes al entrenar en PyTorch

Algunos errores muy frecuentes al empezar son:

  • Olvidar llamar a zero_grad().
  • Usar una función de pérdida incorrecta para el problema.
  • Elegir una tasa de aprendizaje demasiado grande o demasiado pequeña.
  • No respetar la forma correcta de entradas y salidas.
  • Pensar que una sola época alcanza para aprender.

Todos estos errores son normales al principio. Lo importante es detectarlos y entender por qué afectan al entrenamiento.

17.31 Buenas prácticas para estudiantes

Si estás empezando a entrenar modelos, estas recomendaciones suelen ser muy útiles:

  • Comenzar con datasets pequeños y fáciles de interpretar.
  • Usar modelos simples al principio.
  • Imprimir la pérdida durante el entrenamiento.
  • Verificar siempre la forma de entradas, salidas y predicciones.
  • Entender cada línea del ciclo antes de pasar a ejemplos más grandes.

Estas prácticas ayudan a construir una base sólida y evitar frustraciones innecesarias.

17.32 Un resumen del ciclo de entrenamiento

Podemos resumir el entrenamiento de un modelo en PyTorch así:

  1. Se preparan los datos como tensores.
  2. Se define el modelo.
  3. Se elige una función de pérdida.
  4. Se elige un optimizador.
  5. El modelo hace forward propagation.
  6. Se calcula la pérdida.
  7. Se limpian gradientes anteriores.
  8. Backpropagation obtiene nuevos gradientes.
  9. El optimizador actualiza los parámetros.
  10. Este proceso se repite durante muchas épocas.

Ese es el mecanismo fundamental por el cual una red neuronal aprende.

17.33 Qué debes recordar de este tema

  • Entrenar un modelo significa ajustar sus parámetros para reducir el error.
  • Para entrenar necesitamos datos, modelo, función de pérdida y optimizador.
  • El ciclo básico incluye forward, cálculo de pérdida, zero_grad, backward y step.
  • La pérdida mide qué tan lejos está la predicción del valor correcto.
  • El optimizador usa los gradientes para actualizar parámetros.
  • El proceso debe repetirse durante muchas épocas.
  • Observar la pérdida ayuda a verificar si el modelo está aprendiendo.
  • Comprender el sentido de cada paso es más importante que memorizar el código sin entenderlo.

17.34 Conclusión

El entrenamiento de un modelo en PyTorch es el momento en que la red deja de ser una estructura estática y comienza realmente a aprender a partir de datos. Todo lo que estudiamos antes converge aquí: arquitectura, forward propagation, pérdida, gradientes y optimización.

Dominar este tema significa haber dado un paso enorme en la parte práctica del Deep Learning. A partir de aquí, ya es posible construir modelos sencillos y entrenarlos de verdad.

En el próximo tema veremos cómo realizar la evaluación del modelo, es decir, cómo medir su desempeño una vez entrenado.