En muchos proyectos reales de visión por computadora aparece una limitación muy concreta: no tenemos millones de imágenes ni semanas de entrenamiento para construir un modelo grande desde cero. Sin embargo, eso no significa que estemos condenados a obtener resultados pobres. Aquí entra una de las ideas más poderosas del Deep Learning aplicado: el transfer learning.
El transfer learning consiste en reutilizar conocimiento aprendido previamente por un modelo entrenado en un problema grande para adaptarlo a un problema nuevo. En visión por computadora esto ha sido especialmente exitoso, porque muchas representaciones visuales aprendidas en datasets masivos resultan útiles también en tareas diferentes.
En este tema veremos qué significa transferir conocimiento en una CNN, por qué esta estrategia funciona tan bien y qué formas prácticas existen para aplicarla en PyTorch.
Supongamos que un modelo fue entrenado sobre millones de imágenes de muchas categorías. Durante ese proceso, sus capas aprendieron a detectar bordes, texturas, formas, combinaciones de patrones y estructuras visuales de gran utilidad general.
Si luego queremos resolver un problema nuevo, por ejemplo clasificar tipos de hojas, frutas o piezas industriales, no siempre hace falta empezar desde cero. Podemos aprovechar parte de ese conocimiento previo y adaptar solo lo necesario.
Porque muchas características visuales básicas son compartidas entre muchísimos problemas. Bordes, curvas, texturas, contrastes, partes y composiciones aparecen una y otra vez, incluso en dominios distintos.
Las primeras capas de una CNN suelen aprender rasgos bastante generales. Las capas más profundas se vuelven más específicas del problema original. Esta jerarquía explica por qué resulta viable reutilizar parte del modelo y adaptar otra parte.
Entrenar desde cero tiene sentido cuando:
Pero en muchos casos prácticos ocurre lo contrario:
En ese contexto, el transfer learning suele ser muy superior como estrategia inicial.
Un modelo preentrenado es un modelo cuyos pesos ya fueron ajustados previamente sobre un dataset grande. En visión por computadora, muchos modelos clásicos fueron entrenados sobre ImageNet, un conjunto enorme con miles de categorías.
Esto significa que cuando cargamos una arquitectura preentrenada, no estamos empezando con pesos aleatorios, sino con una base visual ya muy rica.
Lo que se transfiere son los parámetros aprendidos por la red, es decir, los pesos de sus capas. En particular, esos pesos codifican filtros y representaciones internas que capturan patrones visuales útiles.
En una CNN profunda, las primeras capas suelen transferirse muy bien porque detectan rasgos generales. Las capas finales, en cambio, suelen necesitar más adaptación al nuevo problema.
En la práctica, el transfer learning suele aplicarse de dos formas principales:
Ambas estrategias reutilizan conocimiento previo, pero con diferente nivel de ajuste sobre las capas originales.
En este enfoque, se congelan las capas preentrenadas y solo se reemplaza y entrena la capa final de clasificación. La idea es usar la red ya aprendida como una máquina que extrae representaciones visuales útiles, mientras una nueva cabeza de clasificación aprende el problema específico.
Esta estrategia suele ser muy buena cuando el dataset nuevo es pequeño.
El fine tuning consiste en no limitarse a entrenar solo la capa final, sino permitir que algunas o todas las capas del modelo preentrenado se ajusten al nuevo problema.
Esto puede producir mejores resultados cuando:
Congelar una capa significa impedir que sus parámetros se actualicen durante el entrenamiento. En PyTorch esto se hace ajustando requires_grad a False.
for param in model.parameters():
param.requires_grad = False
Una vez hecho esto, el optimizador no modificará esos pesos.
La parte final del modelo original suele estar diseñada para el número de clases del dataset con que fue entrenado originalmente. Por eso, al reutilizarlo, normalmente se reemplaza la última capa para que coincida con nuestro nuevo problema.
Por ejemplo, si un modelo termina en 1000 clases y nuestro problema tiene 3 clases, debemos adaptar esa salida.
Podemos pensar el modelo preentrenado como una red que ya sabe “mirar” imágenes. Lo que hacemos al adaptarlo es enseñarle a tomar esa manera de mirar y dirigirla hacia una tarea nueva.
Cuanto más parecido sea el nuevo problema al dominio original, más útil suele ser la transferencia directa. Cuanto más diferente sea, más probable es que necesitemos un fine tuning más profundo.
Entre sus ventajas más importantes están:
Por eso es una técnica extremadamente común en problemas reales de visión.
Aunque muy útil, el transfer learning no es una solución mágica universal. Puede ser menos efectivo cuando:
Aun así, incluso en esos casos suele ser un excelente punto de partida para experimentar.
En visión por computadora, algunos modelos preentrenados muy utilizados son:
Todos ellos tienen versiones accesibles desde torchvision.models.
Con torchvision, cargar un modelo preentrenado suele ser directo. Por ejemplo, con ResNet18:
from torchvision import models
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
Esto descarga o reutiliza los pesos preentrenados y deja listo el modelo para adaptarlo.
Una estrategia muy habitual es:
Por ejemplo:
for param in model.parameters():
param.requires_grad = False
num_features = model.fc.in_features
model.fc = nn.Linear(num_features, 3)
Aquí suponemos un problema de 3 clases.
Si congelamos casi toda la red y solo queremos entrenar la nueva capa final, conviene pasar al optimizador únicamente los parámetros que siguen siendo entrenables.
optimizer = torch.optim.Adam(model.fc.parameters(), lr=0.001)
Esto deja muy claro qué parte del modelo estamos adaptando realmente.
Otra posibilidad es congelar casi todo el modelo y descongelar solo las últimas capas o bloques. Esto permite adaptar representaciones profundas sin perder completamente la base aprendida.
Es una estrategia intermedia muy útil cuando el problema nuevo no es idéntico al original, pero tampoco completamente diferente.
También es posible entrenar todo el modelo preentrenado, es decir, dejar todas las capas actualizables. En ese caso, suele usarse una tasa de aprendizaje relativamente pequeña, porque no queremos destruir demasiado rápido el conocimiento ya aprendido.
Esto puede dar muy buenos resultados si contamos con suficientes datos y si el dominio lo justifica.
Justamente porque el transfer learning se usa mucho con datasets pequeños, el riesgo de overfitting sigue presente. Un modelo potente adaptado a pocos datos puede memorizar con facilidad si no se lo controla bien.
Por eso siguen siendo importantes:
Entrenar desde cero puede sonar más “puro”, pero muchas veces es una mala decisión práctica. Si un modelo preentrenado ya aprendió representaciones robustas y nuestro dataset es limitado, empezar desde cero puede desperdiciar tiempo y rendimiento.
En la práctica profesional, reutilizar conocimiento previo suele ser una decisión muy sensata.
Este ejemplo muestra la estructura general de un pipeline de transfer learning con ResNet18 para un problema de 3 clases:
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms, models
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor()
])
train_dataset = datasets.FakeData(
size=300,
image_size=(3, 224, 224),
num_classes=3,
transform=transform
)
val_dataset = datasets.FakeData(
size=100,
image_size=(3, 224, 224),
num_classes=3,
transform=transform
)
train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=16, shuffle=False)
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
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)
for epoch in range(3):
model.train()
train_loss = 0.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)
avg_train_loss = train_loss / len(train_dataset)
model.eval()
correct = 0
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)
correct += (preds == labels).sum().item()
total += labels.size(0)
val_acc = correct / total
print(f"Epoca {epoch+1}: train_loss={avg_train_loss:.4f}, val_acc={val_acc:.4f}")
Este ejemplo no pretende resolver un problema real, pero muestra con claridad la lógica de cargar, congelar, reemplazar y entrenar una cabeza de clasificación sobre un modelo preentrenado.
Vale la pena leer el ejemplo en bloques:
Esta es una de las recetas más clásicas y más útiles para empezar con transfer learning.
Una estrategia práctica frecuente es empezar entrenando solo la cabeza final. Si el resultado no alcanza o si el dataset lo permite, luego se puede descongelar parte del backbone y hacer fine tuning adicional.
Eso da un proceso más controlado y reduce el riesgo de desestabilizar el entrenamiento desde el principio.
Algunos errores frecuentes son:
El transfer learning es una de las herramientas más valiosas en visión por computadora moderna porque permite aprovechar el enorme trabajo ya invertido en entrenar modelos sobre grandes colecciones de imágenes. En lugar de empezar siempre desde cero, podemos construir sobre representaciones ya aprendidas y adaptarlas a nuestros problemas concretos.
Su importancia es enorme no solo por eficiencia computacional, sino también porque hace posible obtener buenos resultados en escenarios donde los datos son limitados. Aprender a usarlo bien es una habilidad central para cualquier proyecto real de clasificación visual.
En el próximo tema profundizaremos justamente en el uso de modelos preentrenados, viendo con más detalle cómo cargarlos, adaptarlos y trabajar con ellos en PyTorch de forma práctica.