En los temas anteriores vimos qué es el transfer learning y cómo usar modelos preentrenados. Ahora vamos a concentrarnos en un caso concreto y muy importante: la clasificación de imágenes con ResNet.
ResNet es una de las arquitecturas más influyentes de la historia del Deep Learning visual. Además de su valor conceptual, es una herramienta extremadamente práctica, porque existe en múltiples variantes preentrenadas y funciona muy bien como base para muchos problemas reales.
En este tema veremos cómo tomar una ResNet preentrenada, adaptarla a un problema nuevo de clasificación y organizar un pipeline completo de entrenamiento, validación e inferencia.
ResNet combina varias ventajas:
torchvision.Por eso, aprender a usar ResNet es una forma excelente de dominar una gran parte del flujo práctico de visión por computadora con PyTorch.
ResNet introdujo conexiones residuales que facilitan el entrenamiento de redes profundas. Eso permitió construir modelos más profundos y estables, con gran capacidad representacional.
Para usar una ResNet en la práctica no necesitamos reimplementar toda esa lógica desde cero, pero sí conviene recordar que esa arquitectura ya viene optimizada para aprender representaciones visuales ricas.
En torchvision suelen aparecer variantes como:
Las variantes más profundas suelen tener más capacidad, pero también más costo computacional. Para empezar y para fines didácticos, ResNet18 suele ser una excelente elección.
La forma moderna de hacerlo en torchvision es:
from torchvision import models
weights = models.ResNet18_Weights.DEFAULT
model = models.resnet18(weights=weights)
Con esto obtenemos una ResNet18 con pesos preentrenados listos para reutilizarse.
Antes de adaptarla, conviene inspeccionar la parte final del modelo:
print(model.fc)
En ResNet, la última capa de clasificación se encuentra en model.fc. Esa capa fue entrenada originalmente para 1000 clases de ImageNet.
Si nuestro nuevo problema tiene, por ejemplo, 3 clases, la salida de 1000 categorías ya no sirve. Debemos reemplazar la capa final:
import torch.nn as nn
num_features = model.fc.in_features
model.fc = nn.Linear(num_features, 3)
Ahora ResNet sigue usando toda su base preentrenada, pero su salida está adaptada a nuestro nuevo conjunto de clases.
Una estrategia inicial muy común es congelar todas las capas y entrenar solo la nueva capa final:
for param in model.parameters():
param.requires_grad = False
num_features = model.fc.in_features
model.fc = nn.Linear(num_features, 3)
Como la nueva capa se crea después del congelamiento, queda lista para entrenarse.
Porque es una estrategia simple, estable y muy útil cuando el dataset nuevo es pequeño. Aprovechamos las representaciones ya aprendidas por ResNet y solo enseñamos una nueva capa a mapear esas características hacia nuestras clases específicas.
Es una forma muy práctica de obtener una línea base fuerte sin necesidad de ajustar toda la red.
Como todo modelo preentrenado, ResNet espera imágenes procesadas de una forma determinada. Lo más recomendable es reutilizar las transforms asociadas a los pesos:
preprocess = weights.transforms()
Esto asegura coherencia entre el modo en que las imágenes fueron vistas durante el preentrenamiento y el modo en que las alimentamos ahora.
El pipeline de datos se organiza igual que en otros clasificadores. Necesitamos:
Para mantener el ejemplo autocontenido, en este tema seguiremos usando FakeData.
El flujo completo de trabajo con ResNet adaptada suele ser:
Si solo queremos entrenar la última capa, conviene pasar al optimizador únicamente sus parámetros:
optimizer = torch.optim.Adam(model.fc.parameters(), lr=0.001)
Esto deja claro que no vamos a modificar el resto de la red en esta primera etapa.
Como seguimos en un problema de clasificación multiclase, una opción natural es:
criterion = nn.CrossEntropyLoss()
La salida de ResNet adaptada serán logits, así que esta función de pérdida encaja perfectamente.
Este ejemplo reúne carga del modelo, adaptación y entrenamiento básico:
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torchvision import datasets, models
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
weights = models.ResNet18_Weights.DEFAULT
preprocess = weights.transforms()
train_dataset = datasets.FakeData(
size=300,
image_size=(3, 224, 224),
num_classes=3,
transform=preprocess
)
val_dataset = datasets.FakeData(
size=100,
image_size=(3, 224, 224),
num_classes=3,
transform=preprocess
)
train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=16, shuffle=False)
model = models.resnet18(weights=weights)
for param in model.parameters():
param.requires_grad = False
num_features = model.fc.in_features
model.fc = nn.Linear(num_features, 3)
model = model.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.fc.parameters(), lr=0.001)
num_epochs = 3
for epoch in range(num_epochs):
model.train()
train_loss = 0.0
train_correct = 0
train_total = 0
for images, labels in train_loader:
images = images.to(device)
labels = labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
train_loss += loss.item() * images.size(0)
preds = outputs.argmax(dim=1)
train_correct += (preds == labels).sum().item()
train_total += labels.size(0)
avg_train_loss = train_loss / train_total
train_acc = train_correct / train_total
model.eval()
val_correct = 0
val_total = 0
with torch.no_grad():
for images, labels in val_loader:
images = images.to(device)
labels = labels.to(device)
outputs = model(images)
preds = outputs.argmax(dim=1)
val_correct += (preds == labels).sum().item()
val_total += labels.size(0)
val_acc = val_correct / val_total
print(
f"Epoca {epoch+1}/{num_epochs} | "
f"train_loss={avg_train_loss:.4f} | train_acc={train_acc:.4f} | "
f"val_acc={val_acc:.4f}"
)
Este código ya expresa de forma bastante completa la lógica de un clasificador basado en ResNet con transferencia de aprendizaje.
Conviene leer el ejemplo por etapas:
Este patrón es uno de los más usados en problemas reales con datasets pequeños o medianos.
Una vez entrenado el modelo, podemos usarlo para clasificar nuevas imágenes siguiendo el flujo habitual:
eval().Un ejemplo mínimo sería:
from PIL import Image
model.eval()
imagen = Image.open("foto1.jpg").convert("RGB")
entrada = preprocess(imagen).unsqueeze(0).to(device)
with torch.no_grad():
salida = model(entrada)
pred = salida.argmax(dim=1).item()
print("Clase predicha:", pred)
En un proyecto real, ese índice debería mapearse luego a nombres de clase concretos.
Si el entrenamiento produce buenos resultados, podemos guardar el estado del modelo:
torch.save(model.state_dict(), "resnet_clasificador.pth")
Esto permite reutilizar el clasificador más adelante sin repetir todo el entrenamiento.
Para volver a usarlo después, se reconstruye la arquitectura y se cargan los pesos:
model = models.resnet18(weights=weights)
num_features = model.fc.in_features
model.fc = nn.Linear(num_features, 3)
model.load_state_dict(torch.load("resnet_clasificador.pth"))
model.eval()
Este paso es muy importante: la arquitectura debe coincidir con la que tenía el modelo cuando fue guardado.
Si entrenar solo la cabeza final no alcanza, una siguiente estrategia razonable es descongelar los últimos bloques de ResNet y hacer un ajuste más profundo con una tasa de aprendizaje baja.
Esto puede mejorar el desempeño cuando el nuevo dominio se aleja más del problema original de ImageNet.
ResNet resulta especialmente cómoda porque:
fc es fácil de localizar.Aunque muy útil, ResNet no es la solución ideal para todos los escenarios. Puede resultar pesada para algunos dispositivos, y no siempre será la arquitectura más eficiente para despliegues con recursos muy limitados.
Además, como cualquier modelo preentrenado, su utilidad depende de qué tan compatible sea el dominio nuevo con el tipo de imágenes sobre las que fue entrenado originalmente.
Entre los errores más frecuentes están:
model.eval() en inferencia.torchvision.models.model.fc.La clasificación de imágenes con ResNet es uno de los puntos más clásicos y productivos de entrada al trabajo práctico con modelos preentrenados en visión por computadora. Reúne en un solo flujo muchas de las ideas fundamentales del curso: datasets, transforms, entrenamiento, validación, evaluación y transferencia de conocimiento.
Dominar este pipeline significa contar ya con una herramienta muy potente para resolver numerosos problemas reales de clasificación visual. Además, deja una base sólida para dar el siguiente salto hacia tareas más complejas.
En el próximo tema abriremos justamente esa nueva etapa con una introducción a detección de objetos, donde ya no bastará con clasificar una imagen completa, sino que tendremos que localizar objetos dentro de ella.