Transferencia de aprendizaje con PyTorch
La transferencia de aprendizaje (Transfer Learning) se refiere a la técnica de tomar un modelo preentrenado en un conjunto de datos a gran escala y transferirlo a una tarea nueva con menos datos para entrenarlo.
Es una de las técnicas más utilizadas en la práctica actual del aprendizaje profundo: en la mayoría de los casos, la transferencia de aprendizaje produce mejores resultados que entrenar desde cero, es más rápida y requiere menos datos.
1. Idea central de la transferencia de aprendizaje
Las características aprendidas por las redes neuronales profundas en ImageNet son generalizables:
- Capas superficiales: aprenden características de bajo nivel generales (bordes, texturas, gradientes de color)
- Capas intermedias: aprenden características de nivel medio generales (formas, partes, combinaciones de texturas)
- Capas profundas: aprenden características de alto nivel relacionadas con la tarea (rostros, ruedas, texto)
Estas características de nivel bajo y medio son útiles para la gran mayoría de tareas visuales y no es necesario volver a aprenderlas.
¿Cuándo utilizar la transferencia de aprendizaje?
| Cantidad de datos | Similitud con la tarea original | Estrategia recomendada |
|---|---|---|
| Poca (< 1000) | Alto | Solo reemplazar el clasificador final y congelar todo el backbone |
| Poca (< 1000) | bajo | Ajustar las capas más superficiales y congelar las profundas |
| Media (1000~10000) | Alto | Ajustar toda la red con una tasa de aprendizaje pequeña |
| Media (1000~10000) | bajo | Ajustar las capas más profundas y congelar las superficiales |
| Mucha (> 10000) | Cualquiera | Ajustar todo, o considerar entrenar desde cero |
Comparación de tres estrategias principales
La estructura jerárquica de un modelo preentrenado (como ResNet50) es la siguiente:
- Capas convolucionales 1~3 (características de bajo nivel: bordes/texturas): generalmente congeladas
- Capas convolucionales 4~6 (características de nivel medio: formas/partes): opcionalmente congeladas
- Capas convolucionales 7~N (características de alto nivel: información semántica): ajuste fino
- Clasificador (Classifier Head): reemplazar y entrenar
┌─────────────────────────────────────────────────┐ │ 预训练模型(如 ResNet50) │ │ ┌──────────────────────────────────────────┐ │ │ │ Conv Layer 1~3(低级特征:边缘/纹理) │ ← 通常冻结 │ ├──────────────────────────────────────────┤ │ │ │ Conv Layer 4~6(中级特征:形状/部件) │ ← 可选冻结 │ ├──────────────────────────────────────────┤ │ │ │ Conv Layer 7~N(高级特征:语义信息) │ ← 微调 │ ├──────────────────────────────────────────┤ │ │ │ Classifier Head(分类头) │ ← 替换 & 训练 │ └──────────────────────────────────────────┘ │ └─────────────────────────────────────────────────┘
2. Cargar un modelo preentrenado
PyTorch ofrece una gran cantidad de modelos preentrenados oficiales a través de torchvision.models, y cargarlos es muy sencillo.
Ejemplo
import torchvision.models as models
# Cargar modelo preentrenado (descarga automática de pesos)
# PyTorch >= 0.13 recomienda la nueva sintaxis: usar el parámetro weights
from torchvision.models import ResNet50_Weights
model = models.resnet50(weights=ResNet50_Weights.IMAGENET1K_V2)
# Sintaxis antigua (sigue funcionando, pero mostrará una advertencia de deprecación)
model = models.resnet50(pretrained=True)
# No cargar pesos preentrenados (solo usar la estructura de la red)
model = models.resnet50(weights=None)
Ver la estructura del modelo
Ejemplo
print(model)
# Ver solo las últimas capas (clasificador)
print(model.fc)
# Linear(in_features=2048, out_features=1000, bias=True)
# Contar el número de parámetros
total_params = sum(p.numel() for p in model.parameters())
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Total de parámetros: {total_params:,}")
print(f"Parámetros entrenables: {trainable_params:,}")
Nombre del clasificador de cada modelo
Diferentes modelos tienen diferentes nombres de atributo para el clasificador; al transferir, es necesario reemplazar la capa correspondiente:
| Modelo | Atributo del clasificador |
|---|---|
| ResNet / RegNet | model.fc |
| VGG / AlexNet | model.classifier[-1] |
| DenseNet | model.classifier |
| EfficientNet | model.classifier[-1] |
| MobileNetV2/V3 | model.classifier[-1] |
| ViT (Vision Transformer) | model.heads.head |
| ConvNeXt | model.classifier[-1] |
| Inception V3 | model.fc |
| Swin Transformer | model.head |
3. Tres estrategias de transferencia
3.1 Estrategia 1: Extracción de características (congelar todo)
Congelar todos los parámetros del modelo preentrenado y entrenar solo el nuevo clasificador reemplazado.
Es adecuada cuando hay muy pocos datos (unos cientos de imágenes) o cuando la tarea es muy similar a la tarea original.
Ejemplo
import torch.nn as nn
import torchvision.models as models
from torchvision.models import ResNet18_Weights
NUM_CLASSES = 5 # Número de clases de la tarea objetivo
# Paso 1: Cargar el modelo preentrenado
model = models.resnet18(weights=ResNet18_Weights.IMAGENET1K_V1)
# Paso 2: Congelar todos los parámetros
for param in model.parameters():
param.requires_grad = False
# Paso 3: Reemplazar el clasificador (estos parámetros tienen requires_grad=True por defecto)
in_features = model.fc.in_features # 512
model.fc = nn.Linear(in_features, NUM_CLASSES)
# Verificar: solo el clasificador es entrenable
trainable = [(n, p.shape) for n, p in model.named_parameters() if p.requires_grad]
print(f"Capas entrenables: {len(trainable)}")
for name, shape in trainable:
print(f" {name}: {shape}")
# Salida:
# fc.weight: torch.Size([5, 512])
# fc.bias: torch.Size([5])
# Paso 4: Pasar solo los parámetros entrenables al optimizador (más eficiente)
optimizer = torch.optim.Adam(
filter(lambda p: p.requires_grad, model.parameters()),
lr=1e-3
)
# O una forma equivalente pero más clara:
optimizer = torch.optim.Adam(model.fc.parameters(), lr=1e-3)
La extracción de características es la estrategia de transferencia de aprendizaje más simple y más utilizada, especialmente adecuada para escenarios con pocos datos.
3.2 Estrategia 2: Ajuste fino (Fine-tuning)
Descongelar todas o parte de las capas preentrenadas y entrenar todo el modelo con una tasa de aprendizaje más pequeña.
Es adecuada cuando hay una cantidad media de datos o cuando la tarea es algo diferente a la tarea original.
Ejemplo
import torch.nn as nn
import torchvision.models as models
NUM_CLASSES = 10
model = models.resnet50(weights='IMAGENET1K_V2')
# Método A: Ajuste fino completo (descongelar todas las capas)
# Primero congelar
for param in model.parameters():
param.requires_grad = False
# Luego descongelar (equivalente al ajuste fino completo; esta sintaxis se usa a menudo en escenarios de descongelado progresivo)
for param in model.parameters():
param.requires_grad = True
# Reemplazar el clasificador
model.fc = nn.Linear(model.fc.in_features, NUM_CLASSES)
# Ajuste fino completo: backbone con tasa de aprendizaje pequeña, cabeza con tasa grande (ver estrategia 3)
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4, momentum=0.9)
# Método B: Descongelar las últimas N capas (ajuste fino parcial)
model = models.resnet50(weights='IMAGENET1K_V2')
# Primero congelar todo
for param in model.parameters():
param.requires_grad = False
# Descongelar solo layer4 y fc (el último bloque de ResNet y el clasificador)
for param in model.layer4.parameters():
param.requires_grad = True
model.fc = nn.Linear(model.fc.in_features, NUM_CLASSES) # fc es entrenable por defecto
print("Parámetros entrenables:")
for name, param in model.named_parameters():
if param.requires_grad:
print(f" {name}")
3.3 Estrategia 3: Tasa de aprendizaje diferenciada por capas
El backbone usa una tasa de aprendizaje pequeña (para conservar el conocimiento preentrenado), mientras que el clasificador usa una tasa de aprendizaje grande (para adaptarse rápidamente a la nueva tarea).
Esta es la estrategia de ajuste fino más utilizada en la industria y ofrece el mejor rendimiento general.
Ejemplo
import torch.nn as nn
import torchvision.models as models
NUM_CLASSES = 8
model = models.resnet50(weights='IMAGENET1K_V2')
model.fc = nn.Linear(model.fc.in_features, NUM_CLASSES)
# Opción 1: Dos tasas de aprendizaje (backbone vs cabeza)
optimizer = torch.optim.Adam([
{'params': model.fc.parameters(), 'lr': 1e-3}, # Clasificador: tasa de aprendizaje grande
{'params': [p for n, p in model.named_parameters() # Backbone: tasa de aprendizaje pequeña
if not n.startswith('fc')],
'lr': 1e-5},
])
# Opción 2: Tasa de aprendizaje decreciente por capas (la más precisa)
# Cuanto más cerca de la salida, mayor es la tasa de aprendizaje
layer_groups = [
(model.layer1, 1e-5), # Capa más superficial, tasa de aprendizaje más pequeña
(model.layer2, 3e-5),
(model.layer3, 1e-4),
(model.layer4, 3e-4), # Capa más profunda del backbone
(model.fc, 1e-3), # Clasificador, tasa de aprendizaje máxima
]
param_groups = [
{'params': layer.parameters(), 'lr': lr}
for layer, lr in layer_groups
]
optimizer = torch.optim.Adam(param_groups)
# Opción 3: Descongelado progresivo (Gradual Unfreezing)
# Al inicio del entrenamiento solo se entrena la cabeza, y progresivamente se descongelan más capas (recomendado por fastai)
model = models.resnet50(weights='IMAGENET1K_V2')
for param in model.parameters():
param.requires_grad = False
model.fc = nn.Linear(model.fc.in_features, NUM_CLASSES)
def unfreeze_layers(model, num_layers):
"""Descongela los últimos num_layers bloques de capas de ResNet"""
layers = [model.layer4, model.layer3, model.layer2, model.layer1]
for i in range(min(num_layers, len(layers))):
for param in layers[i].parameters():
param.requires_grad = True
# Época 1-5: solo entrenar el clasificador
# Época 6-10: descongelar layer4
unfreeze_layers(model, num_layers=1)
# Época 11+: descongelar más capas
unfreeze_layers(model, num_layers=3)
4. Modelos preentrenados comunes
4.1 Modelos de clasificación de imágenes
Ejemplo
# Serie ResNet (la más clásica, adecuada para la mayoría de tareas)
resnet18 = models.resnet18(weights='IMAGENET1K_V1') # Ligero, adecuado para dispositivos de borde
resnet50 = models.resnet50(weights='IMAGENET1K_V2') # Opción equilibrada
resnet101 = models.resnet101(weights='IMAGENET1K_V2') # Más potente, más lento
# Serie EfficientNet (relación precisión-eficiencia muy alta)
effnet_b0 = models.efficientnet_b0(weights='IMAGENET1K_V1') # El más ligero
effnet_b4 = models.efficientnet_b4(weights='IMAGENET1K_V1') # Equilibrado
effnet_b7 = models.efficientnet_b7(weights='IMAGENET1K_V1') # El más potente
# Vision Transformer (primera opción para tareas con grandes volúmenes de datos)
vit_b16 = models.vit_b_16(weights='IMAGENET1K_V1') # ViT-Base/16
vit_l16 = models.vit_l_16(weights='IMAGENET1K_V1') # ViT-Large/16
# MobileNet (despliegue en móvil/embebido)
mobilenet_v3 = models.mobilenet_v3_small(weights='IMAGENET1K_V1')
# ConvNeXt (versión moderna de CNN, rendimiento cercano a ViT)
convnext_t = models.convnext_tiny(weights='IMAGENET1K_V1')
convnext_b = models.convnext_base(weights='IMAGENET1K_V1')
Comparación de rendimiento de los modelos principales (ImageNet Top-1 Acc):
| Modelo | Top-1 Acc | Cantidad de parámetros | Velocidad de inferencia | Escenarios de aplicación |
|---|---|---|---|---|
| ResNet-18 | 69.8% | 11.7M | Muy rápido | Recursos limitados, prototipado rápido |
| ResNet-50 | 80.9% | 25.6M | Rápido | Elección general preferida |
| EfficientNet-B4 | 83.4% | 19.3M | Medio | Equilibrio entre precisión y eficiencia |
| ConvNeXt-Base | 84.1% | 88.6M | Medio | CNN de alta precisión |
| ViT-B/16 | 81.1% | 86.6M | Medio | Escenarios de grandes volúmenes de datos |
| ViT-L/16 | 85.1% | 307M | Lento | Mayor precisión |
4.2 Modelos de detección de objetos
Ejemplo
# Faster R-CNN (clásico detector de dos etapas)
faster_rcnn = detection.fasterrcnn_resnet50_fpn(weights='DEFAULT')
# SSD (detector de una etapa, rápido)
ssd = detection.ssd300_vgg16(weights='DEFAULT')
# RetinaNet
retinanet = detection.retinanet_resnet50_fpn(weights='DEFAULT')
# FCOS
fcos = detection.fcos_resnet50_fpn(weights='DEFAULT')
# Reemplazar la cabeza de clasificación de Faster R-CNN (adaptar al nuevo número de clases)
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
NUM_CLASSES = 5 + 1 # 5 clases de objetos + 1 clase de fondo
faster_rcnn = detection.fasterrcnn_resnet50_fpn(weights='DEFAULT')
in_features = faster_rcnn.roi_heads.box_predictor.cls_score.in_features
faster_rcnn.roi_heads.box_predictor = FastRCNNPredictor(in_features, NUM_CLASSES)
4.3 Modelos de texto (HuggingFace)
El aprendizaje por transferencia para tareas de NLP generalmente usa la biblioteca HuggingFace transformers:
Ejemplo
from transformers import (
BertForSequenceClassification,
RobertaForSequenceClassification,
AutoModelForSequenceClassification,
AutoTokenizer,
)
NUM_CLASSES = 3
# BERT (primera opción para clasificación de texto)
model = BertForSequenceClassification.from_pretrained(
'bert-base-chinese', # BERT chino
num_labels=NUM_CLASSES
)
tokenizer = AutoTokenizer.from_pretrained('bert-base-chinese')
# RoBERTa (versión mejorada de BERT, más potente)
model = RobertaForSequenceClassification.from_pretrained(
'roberta-base',
num_labels=NUM_CLASSES
)
# Método de carga genérico (identifica automáticamente el tipo de modelo)
model = AutoModelForSequenceClassification.from_pretrained(
'hfl/chinese-roberta-wwm-ext', # RoBERTa chino
num_labels=NUM_CLASSES
)
5. Preprocesamiento y aumento de datos
Al usar pesos preentrenados de ImageNet en aprendizaje por transferencia, es obligatorio usar los mismos parámetros de normalización que los del preentrenamiento; de lo contrario, la distribución de características no coincidirá y el rendimiento caerá drásticamente.
Ejemplo
# Parámetros estándar de normalización de ImageNet (comunes a todos los modelos preentrenados de torchvision)
IMAGENET_MEAN = [0.485, 0.456, 0.406]
IMAGENET_STD = [0.229, 0.224, 0.225]
# Conjunto de entrenamiento: con aumento de datos
train_transforms = transforms.Compose([
transforms.RandomResizedCrop(224), # Recorte aleatorio y redimensionado
transforms.RandomHorizontalFlip(p=0.5), # Volteo horizontal aleatorio
transforms.ColorJitter( # Jitter de color
brightness=0.2, contrast=0.2,
saturation=0.2, hue=0.1
),
transforms.RandomRotation(degrees=15), # Rotación aleatoria
transforms.ToTensor(),
transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD),
])
# Conjunto de validación/prueba: sin aumento aleatorio
val_transforms = transforms.Compose([
transforms.Resize(256), # Primero redimensionar a 256
transforms.CenterCrop(224), # Luego recorte central a 224
transforms.ToTensor(),
transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD),
])
# Carga del conjunto de datos (estructura de directorios: root/class_name/img.jpg)
from torchvision.datasets import ImageFolder
from torch.utils.data import DataLoader
train_dataset = ImageFolder(root='data/train', transform=train_transforms)
val_dataset = ImageFolder(root='data/val', transform=val_transforms)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True,
num_workers=4, pin_memory=True)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False,
num_workers=4, pin_memory=True)
print(f"Número de muestras de entrenamiento: {len(train_dataset)}")
print(f"Lista de clases: {train_dataset.classes}")
print(f"Mapa de clases: {train_dataset.class_to_idx}")
Usar el preprocesamiento recomendado oficial de torchvision
Ejemplo
from torchvision.models import ResNet50_Weights
weights = ResNet50_Weights.IMAGENET1K_V2
model = models.resnet50(weights=weights)
preprocess = weights.transforms() # Devuelve automáticamente el pipeline de preprocesamiento correspondiente
# preprocess ya incluye pasos como Resize(232), CenterCrop(224), Normalize, etc.
# Durante el entrenamiento, solo hay que añadir aumento de datos sobre esta base
Hay que usar los parámetros de normalización de ImageNet [0.485, 0.456, 0.406] y [0.229, 0.224, 0.225]; de lo contrario, las características preentrenadas no se alinearán correctamente.
6. Práctica completa: clasificación binaria de imágenes
Usando la clasificación de gatos y perros como ejemplo, se muestra el flujo completo de aprendizaje por transferencia desde la preparación de datos hasta la evaluación del entrenamiento:
Ejemplo
import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim.lr_scheduler import CosineAnnealingLR
from torchvision import models, transforms, datasets
from torch.utils.data import DataLoader, random_split
from torchvision.models import EfficientNet_B0_Weights
# Configuración
DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
NUM_CLASSES = 2
BATCH_SIZE = 32
EPOCHS = 20
BASE_LR = 1e-3
DATA_DIR = 'data/cats_and_dogs'
# Preparación de datos
train_tfm = transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ColorJitter(brightness=0.3, contrast=0.3),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225]),
])
val_tfm = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225]),
])
full_dataset = datasets.ImageFolder(DATA_DIR, transform=train_tfm)
n_val = int(len(full_dataset) * 0.2)
n_train = len(full_dataset) - n_val
train_set, val_set = random_split(full_dataset, [n_train, n_val])
val_set.dataset = datasets.ImageFolder(DATA_DIR, transform=val_tfm) # Reemplazar el transform de val
train_loader = DataLoader(train_set, BATCH_SIZE, shuffle=True,
num_workers=4, pin_memory=True)
val_loader = DataLoader(val_set, BATCH_SIZE, shuffle=False,
num_workers=4, pin_memory=True)
# Construir el modelo de transferencia
weights = EfficientNet_B0_Weights.IMAGENET1K_V1
model = models.efficientnet_b0(weights=weights)
# Congelar el backbone
for param in model.parameters():
param.requires_grad = False
# Reemplazar la cabeza de clasificación (estructura de la cabeza de EfficientNet-B0)
in_features = model.classifier[1].in_features # 1280
model.classifier = nn.Sequential(
nn.Dropout(p=0.2, inplace=True),
nn.Linear(in_features, NUM_CLASSES),
)
model = model.to(DEVICE)
# Optimizador: tasa de aprendizaje por capas
optimizer = optim.Adam([
{'params': model.classifier.parameters(), 'lr': BASE_LR},
{'params': model.features.parameters(), 'lr': BASE_LR * 0.1},
])
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
scheduler = CosineAnnealingLR(optimizer, T_max=EPOCHS, eta_min=1e-6)
# Funciones de entrenamiento y validación
def train_epoch(model, loader, optimizer, criterion):
model.train()
total_loss, correct = 0.0, 0
for imgs, labels in loader:
imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)
optimizer.zero_grad()
outputs = model(imgs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
total_loss += loss.item() * imgs.size(0)
correct += (outputs.argmax(1) == labels).sum().item()
n = len(loader.dataset)
return total_loss / n, correct / n
def eval_epoch(model, loader, criterion):
model.eval()
total_loss, correct = 0.0, 0
with torch.no_grad():
for imgs, labels in loader:
imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)
outputs = model(imgs)
loss = criterion(outputs, labels)
total_loss += loss.item() * imgs.size(0)
correct += (outputs.argmax(1) == labels).sum().item()
n = len(loader.dataset)
return total_loss / n, correct / n
# Entrenamiento por etapas
print("=== Etapa 1: solo entrenar la cabeza de clasificación (Epoch 1-5) ===")
best_acc = 0.0
for epoch in range(1, EPOCHS + 1):
# En el epoch 5 se descongela el backbone, entrando en la fase de ajuste fino completo
if epoch == 6:
print("\n=== Etapa 2: descongelar el backbone y ajuste fino completo (Epoch 6-20) ===")
for param in model.features.parameters():
param.requires_grad = True
train_loss, train_acc = train_epoch(model, train_loader, optimizer, criterion)
val_loss, val_acc = eval_epoch(model, val_loader, criterion)
scheduler.step()
print(f"Epoch {epoch:2d}/{EPOCHS} | "
f"Train Loss: {train_loss:.4f}, Acc: {train_acc:.4f} | "
f"Val Loss: {val_loss:.4f}, Acc: {val_acc:.4f} | "
f"LR: {scheduler.get_last_lr()[0]:.2e}")
if val_acc > best_acc:
best_acc = val_acc
torch.save(model.state_dict(), 'best_model.pth')
print(f" ✓ Guardar el mejor modelo Acc={best_acc:.4f}")
print(f"\nEntrenamiento completado, mejor precisión de validación: {best_acc:.4f}")
Inferencia y predicción
Ejemplo
# Cargar el mejor modelo
model.load_state_dict(torch.load('best_model.pth', map_location=DEVICE))
model.eval()
# Predecir una sola imagen
def predict(image_path, model, class_names):
img = Image.open(image_path).convert('RGB')
tensor = val_tfm(img).unsqueeze(0).to(DEVICE) # (1, 3, 224, 224)
with torch.inference_mode():
logits = model(tensor)
probs = torch.softmax(logits, dim=1)[0]
pred = probs.argmax().item()
for cls, prob in zip(class_names, probs.tolist()):
print(f" {cls}: {prob:.4f}")
print(f"Resultado de la predicción: {class_names[pred]}")
return class_names[pred]
class_names = train_set.dataset.classes # ['cat', 'dog']
predict('test_cat.jpg', model, class_names)
7. Práctica completa: clasificación de texto (BERT)
Ejemplo
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
from transformers import (
BertTokenizer,
BertForSequenceClassification,
AdamW,
get_linear_schedule_with_warmup,
)
DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
MODEL_NAME = 'bert-base-chinese'
NUM_CLASSES = 3 # Clasificación de sentimiento: positivo/neutral/negativo
MAX_LEN = 128
BATCH_SIZE = 16
EPOCHS = 5
LR = 2e-5 # Rango de tasa de aprendizaje recomendado para el ajuste fino de BERT: 1e-5 ~ 5e-5
# Dataset personalizado
class SentimentDataset(Dataset):
def __init__(self, texts, labels, tokenizer, max_len):
self.texts = texts
self.labels = labels
self.tokenizer = tokenizer
self.max_len = max_len
def __len__(self):
return len(self.texts)
def __getitem__(self, idx):
encoding = self.tokenizer(
self.texts[idx],
max_length=self.max_len,
padding='max_length',
truncation=True,
return_tensors='pt',
)
return {
'input_ids': encoding['input_ids'].squeeze(0),
'attention_mask': encoding['attention_mask'].squeeze(0),
'label': torch.tensor(self.labels[idx], dtype=torch.long),
}
# Cargar el modelo preentrenado BERT
tokenizer = BertTokenizer.from_pretrained(MODEL_NAME)
model = BertForSequenceClassification.from_pretrained(
MODEL_NAME,
num_labels=NUM_CLASSES,
hidden_dropout_prob=0.1,
)
model = model.to(DEVICE)
# Congelar las N capas inferiores (opcional)
# BERT-base tiene 12 capas Transformer; se pueden congelar las primeras capas para ahorrar cómputo
FREEZE_LAYERS = 6 # Congelar las primeras 6 capas
for i, layer in enumerate(model.bert.encoder.layer):
if i < FREEZE_LAYERS:
for param in layer.parameters():
param.requires_grad = False
print(f"Se han congelado las primeras {FREEZE_LAYERS} capas, reduciendo aproximadamente {FREEZE_LAYERS/12*100:.0f}% de las actualizaciones de parámetros")
# Carga de datos
# Datos de ejemplo (reemplazar por un conjunto de datos real)
train_texts = ["¡Esta película es fantástica!", "El servicio es muy malo, decepcionante", "Normal, nada especial"]
train_labels = [2, 0, 1] # 0: negativo 1: neutral 2: positivo
train_dataset = SentimentDataset(train_texts, train_labels, tokenizer, MAX_LEN)
train_loader = DataLoader(train_dataset, BATCH_SIZE, shuffle=True)
# Optimizador y scheduler
# Configuración estándar de BERT: AdamW + warmup lineal
no_decay = ['bias', 'LayerNorm.weight']
optimizer_grouped = [
{'params': [p for n, p in model.named_parameters()
if not any(nd in n for nd in no_decay)], 'weight_decay': 0.01},
{'params': [p for n, p in model.named_parameters()
if any(nd in n for nd in no_decay)], 'weight_decay': 0.0},
]
optimizer = AdamW(optimizer_grouped, lr=LR)
total_steps = len(train_loader) * EPOCHS
warmup_steps = int(total_steps * 0.1) # El 10% de los pasos se usa para warmup
scheduler = get_linear_schedule_with_warmup(
optimizer,
num_warmup_steps=warmup_steps,
num_training_steps=total_steps,
)
# Bucle de entrenamiento
for epoch in range(1, EPOCHS + 1):
model.train()
total_loss, correct = 0.0, 0
for batch in train_loader:
input_ids = batch['input_ids'].to(DEVICE)
attention_mask = batch['attention_mask'].to(DEVICE)
labels = batch['label'].to(DEVICE)
optimizer.zero_grad()
outputs = model(input_ids=input_ids,
attention_mask=attention_mask,
labels=labels)
loss = outputs.loss # BERT ya calcula la CE Loss internamente
logits = outputs.logits
loss.backward()
# Configuración estándar del ajuste fino de BERT: recorte de gradientes (gradient clipping) para evitar explosión de gradientes
nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
scheduler.step()
total_loss += loss.item()
correct += (logits.argmax(1) == labels).sum().item()
avg_loss = total_loss / len(train_loader)
acc = correct / len(train_dataset)
print(f"Epoch {epoch}/{EPOCHS} | Loss: {avg_loss:.4f} | Acc: {acc:.4f}")
# Inferencia
def predict_sentiment(text, model, tokenizer, id2label):
model.eval()
encoding = tokenizer(text, max_length=MAX_LEN, padding='max_length',
truncation=True, return_tensors='pt')
with torch.inference_mode():
outputs = model(
input_ids = encoding['input_ids'].to(DEVICE),
attention_mask = encoding['attention_mask'].to(DEVICE),
)
pred = outputs.logits.argmax(1).item()
return id2label[pred]
id2label = {0: 'negativo', 1: 'neutral', 2: 'positivo'}
print(predict_sentiment("¡La calidad de este producto es realmente buena!", model, tokenizer, id2label))
8. Técnicas para modificar la estructura del modelo
Método general para reemplazar el clasificador
Ejemplo
import torchvision.models as models
def build_transfer_model(arch, num_classes, pretrained=True, dropout=0.5):
"""
Función genérica de construcción de modelos de transferencia que identifica y reemplaza automáticamente la cabeza de clasificación
Soporta: resnet, efficientnet, densenet, mobilenet, vit, convnext
"""
weights = 'IMAGENET1K_V1' if pretrained else None
model = getattr(models, arch)(weights=weights)
name = arch.lower()
if 'resnet' in name or 'resnext' in name or 'inception' in name:
in_f = model.fc.in_features
model.fc = nn.Sequential(
nn.Dropout(dropout),
nn.Linear(in_f, num_classes)
)
elif 'efficientnet' in name or 'mobilenet' in name or 'convnext' in name:
in_f = model.classifier[-1].in_features
model.classifier[-1] = nn.Sequential(
nn.Dropout(dropout),
nn.Linear(in_f, num_classes)
)
elif 'densenet' in name:
in_f = model.classifier.in_features
model.classifier = nn.Linear(in_f, num_classes)
elif 'vit' in name or 'swin' in name:
in_f = model.heads.head.in_features
model.heads.head = nn.Linear(in_f, num_classes)
else:
raise ValueError(f"Arquitectura no soportada: {arch}")
return model
# Ejemplo de uso
model_resnet = build_transfer_model('resnet50', num_classes=10)
model_effnet = build_transfer_model('efficientnet_b3', num_classes=10)
model_vit = build_transfer_model('vit_b_16', num_classes=10)
Añadir capas intermedias de extracción de características
Ejemplo
import torch.nn as nn
import torchvision.models as models
class TransferWithAttention(nn.Module):
"""Conectar un módulo de atención personalizado y una cabeza de clasificación después del backbone preentrenado"""
def __init__(self, num_classes, dropout=0.5):
super().__init__()
backbone = models.resnet50(weights='IMAGENET1K_V2')
# Eliminar la cabeza de clasificación original, conservar el extractor de características
self.backbone = nn.Sequential(*list(backbone.children())[:-1])
self.feat_dim = 2048 # Dimensión de características de ResNet50
# Compuerta de atención personalizada (attention gating)
self.attention = nn.Sequential(
nn.Linear(self.feat_dim, 512),
nn.Tanh(),
nn.Linear(512, 1),
nn.Sigmoid()
)
# Cabeza de clasificación
self.classifier = nn.Sequential(
nn.Dropout(dropout),
nn.Linear(self.feat_dim, 256),
nn.ReLU(),
nn.Linear(256, num_classes),
)
def forward(self, x):
feat = self.backbone(x).flatten(1) # (N, 2048)
weight = self.attention(feat) # (N, 1)
feat = feat * weight # Características ponderadas
return self.classifier(feat)
9. Mejores prácticas en transferencia de aprendizaje
Selección de la tasa de aprendizaje
Ejemplo
# Normalmente se establece entre 1/10 y 1/100 de la tasa de aprendizaje de la cabeza
backbone_lr = 1e-5 # Estrategia conservadora, recomendada cuando hay pocos datos
head_lr = 1e-3 # Las capas nuevas comienzan con inicialización aleatoria y necesitan una tasa de aprendizaje mayor
# Tasa de aprendizaje para Transformers como BERT / ViT
# Estos modelos grandes son extremadamente sensibles a la tasa de aprendizaje; salirse del rango puede dañar el conocimiento preentrenado
bert_lr = 2e-5 # Rango recomendado: 1e-5 ~ 5e-5
Problemas comunes y soluciones
Ejemplo
# Solución 1: reducir el batch size
# Solución 2: usar gradient checkpointing (intercambiar tiempo por memoria)
from torch.utils.checkpoint import checkpoint_sequential
model.features = lambda x: checkpoint_sequential(model.features, 4, x)
# Solución 3: congelar más capas (reducir el cómputo de retropropagación)
# Problema 2: alta precisión en entrenamiento, baja en validación (sobreajuste)
# Solución 1: aumentar el aumento de datos
# Solución 2: aumentar el Dropout
model.classifier = nn.Sequential(nn.Dropout(0.5), nn.Linear(in_f, num_classes))
# Solución 3: usar Label Smoothing (suavizado de etiquetas)
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
# Solución 4: congelar más capas del backbone, reducir parámetros entrenables
# Problema 3: entrenamiento inestable, fluctuación de la pérdida (Loss)
# Solución 1: reducir la tasa de aprendizaje (probar reduciéndola a la mitad)
# Solución 2: añadir warmup de calentamiento
# Solución 3: recorte de gradientes (gradient clipping)
nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# Solución 4: cambiar de optimizador (Adam → AdamW)
# Problema 4: parámetros de normalización no coinciden
# Error: usar normalización personalizada no coincide con el preentrenamiento
transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
# Correcto: se deben usar la media y la desviación estándar de ImageNet
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
Referencia rápida de estrategias generales
| Problema → Problema | Práctica recomendada → Práctica recomendada |
|---|---|
| Cantidad de datos < 500 | Congelar todas las capas, entrenar solo la cabeza de clasificación |
| Cantidad de datos 500~2000 | Congelar los primeros 2/3, ajustar finamente el último 1/3 |
| Cantidad de datos 2000~10000 → Cantidad de datos 2000~10000 | Ajuste fino completo, columna vertebral con 1e-5, cabeza con 1e-3 |
| Cantidad de datos > 10000 | Ajuste fino completo, considerar una tasa de aprendizaje mayor o entrenar desde cero |
| Entrenamiento lento → Entrenamiento lento | Primero congelar y entrenar algunas épocas, luego descongelar; usar precisión mixta AMP |
| Cuello de botella de rendimiento | Cambiar a un backbone más grande/nuevo; probar ConvNeXt / ViT |
| El modelo necesita implementarse en producción | Priorizar modelos ligeros como EfficientNet-B0 / MobileNetV3 |
| Tareas de PLN en chino | BERT-base-chinese o chinese-roberta-wwm-ext |
Estrategia de entrenamiento completa recomendada
Primera fase (Epoch 1~N/4):
- Congelar la columna vertebral, entrenar solo la cabeza de clasificación
- Usar una tasa de aprendizaje mayor (1e-3) para converger rápidamente los parámetros de la cabeza
Segunda fase (Epoch N/4~N):
- Descongelar la columna vertebral y ajustar finamente todo el modelo
- Columna vertebral con tasa de aprendizaje pequeña (1e-5), cabeza mantiene (1e-3)
- Combinar con recocido de coseno o ReduceLROnPlateau
Estrategia de guardado:
- Guardar solo el checkpoint con mejor métrica en el conjunto de validación
- Guardar también el estado de model / optimizer / scheduler