Skip to content

🔄 Unidad 7. Transfer Learning y Fine-Tuning

Transfer Learning es una técnica que aprovecha el conocimiento aprendido en un problema para aplicarlo a otro diferente. Es una de las técnicas más importantes en Deep Learning moderno.


7.1. ¿Qué es Transfer Learning?

Concepto

En lugar de entrenar una red desde cero, usamos una red preentrenada en un dataset grande (ImageNet, Wikipedia, etc.) y la adaptamos a nuestro problema específico.

Transfer Learning Strategy

Modelo Preentrenado          Tu Problema
(ImageNet: 14M imágenes)     (Tu dataset: 1000 imágenes)
        │                            │
        │ Conocimiento               │ Datos específicos
        │ general                    │
        ▼                            ▼
    ┌─────────────────────────────────────┐
    │         Transfer Learning           │
    │    Combina ambos para obtener      │
    │    un modelo de alta calidad       │
    └─────────────────────────────────────┘

¿Por qué Funciona?

Las capas iniciales de una red aprenden características genéricas:

  • Capas bajas: Bordes, texturas, colores.
  • Capas medias: Patrones más complejos (ojos, ruedas).
  • Capas altas: Conceptos específicos del problema.

Las características genéricas son transferibles a otros problemas.


7.2. Estrategias de Transfer Learning

Estrategia 1: Feature Extraction

Usar el modelo preentrenado como extractor de características fijas.

from tensorflow.keras.applications import VGG16
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense, Flatten, Input

# Cargar modelo preentrenado SIN las capas de clasificación
base_model = VGG16(
    weights='imagenet',      # Pesos preentrenados
    include_top=False,       # Excluir capas de clasificación
    input_shape=(224, 224, 3)
)

# CONGELAR todas las capas del modelo base
base_model.trainable = False

# Añadir nuestras capas de clasificación
model = Sequential([
    base_model,
    Flatten(),
    Dense(256, activation='relu'),
    Dense(10, activation='softmax')  # 10 clases
])

model.compile(
    optimizer='adam',
    loss='categorical_crossentropy',
    metrics=['accuracy']
)

model.summary()

Estrategia 2: Fine-Tuning

Descongelar algunas capas superiores del modelo base y reentrenarlas.

# Primero: entrenar solo las capas nuevas (feature extraction)
model.fit(X_train, y_train, epochs=10, validation_data=(X_val, y_val))

# Segundo: descongelar algunas capas del modelo base
base_model.trainable = True

# Congelar todas las capas excepto las últimas N
for layer in base_model.layers[:-4]:  # Descongelar últimas 4 capas
    layer.trainable = False

# Recompilar con learning rate muy bajo
model.compile(
    optimizer=tf.keras.optimizers.Adam(1e-5),  # LR muy bajo
    loss='categorical_crossentropy',
    metrics=['accuracy']
)

# Entrenar con fine-tuning
model.fit(X_train, y_train, epochs=10, validation_data=(X_val, y_val))

Comparación de Estrategias

Estrategia Cuándo Usar Dataset
Feature Extraction Dataset pequeño, similar al original < 10,000 imágenes
Fine-Tuning Parcial Dataset mediano, similar al original 10,000 - 100,000
Fine-Tuning Completo Dataset grande, diferente al original > 100,000

7.3. Modelos Preentrenados Populares

Para Imágenes (Computer Vision)

from tensorflow.keras.applications import (
    VGG16, VGG19,
    ResNet50, ResNet101, ResNet152,
    InceptionV3, InceptionResNetV2,
    MobileNetV2, MobileNetV3Small, MobileNetV3Large,
    EfficientNetB0, EfficientNetB7,
    DenseNet121, DenseNet201
)

# Ejemplo: cargar diferentes modelos
vgg = VGG16(weights='imagenet', include_top=False, input_shape=(224, 224, 3))
resnet = ResNet50(weights='imagenet', include_top=False, input_shape=(224, 224, 3))
efficientnet = EfficientNetB0(weights='imagenet', include_top=False, input_shape=(224, 224, 3))

Comparación de Modelos

Modelo Parámetros Top-1 Acc ImageNet Uso
MobileNetV2 3.4M 71.8% Dispositivos móviles
ResNet50 25.6M 76.0% Balance precisión/velocidad
EfficientNetB0 5.3M 77.1% Eficiente
EfficientNetB7 66M 84.3% Máxima precisión
VGG16 138M 71.3% Clásico, fácil de entender

7.4. Ejemplo Completo: Clasificación de Imágenes

import tensorflow as tf
from tensorflow.keras.applications import EfficientNetB0
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense, GlobalAveragePooling2D, Dropout
from tensorflow.keras.preprocessing.image import ImageDataGenerator

# =====================
# PREPARACIÓN DE DATOS
# =====================

# Data augmentation para entrenamiento
train_datagen = ImageDataGenerator(
    rescale=1./255,
    rotation_range=20,
    width_shift_range=0.2,
    height_shift_range=0.2,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True,
    fill_mode='nearest',
    validation_split=0.2
)

# Solo rescale para validación
val_datagen = ImageDataGenerator(
    rescale=1./255,
    validation_split=0.2
)

# Cargar datos desde directorio
train_generator = train_datagen.flow_from_directory(
    'data/train',
    target_size=(224, 224),
    batch_size=32,
    class_mode='categorical',
    subset='training'
)

val_generator = val_datagen.flow_from_directory(
    'data/train',
    target_size=(224, 224),
    batch_size=32,
    class_mode='categorical',
    subset='validation'
)

num_classes = train_generator.num_classes

# =====================
# CREAR MODELO
# =====================

# Modelo base preentrenado
base_model = EfficientNetB0(
    weights='imagenet',
    include_top=False,
    input_shape=(224, 224, 3)
)
base_model.trainable = False  # Congelar

# Modelo completo
model = Sequential([
    base_model,
    GlobalAveragePooling2D(),
    Dropout(0.2),
    Dense(256, activation='relu'),
    Dropout(0.2),
    Dense(num_classes, activation='softmax')
])

model.compile(
    optimizer='adam',
    loss='categorical_crossentropy',
    metrics=['accuracy']
)

# =====================
# FASE 1: FEATURE EXTRACTION
# =====================

print("Fase 1: Feature Extraction")
history1 = model.fit(
    train_generator,
    epochs=10,
    validation_data=val_generator,
    callbacks=[
        tf.keras.callbacks.EarlyStopping(patience=3, restore_best_weights=True)
    ]
)

# =====================
# FASE 2: FINE-TUNING
# =====================

print("\nFase 2: Fine-Tuning")

# Descongelar las últimas capas
base_model.trainable = True
for layer in base_model.layers[:-20]:  # Mantener congeladas las primeras
    layer.trainable = False

# Recompilar con learning rate bajo
model.compile(
    optimizer=tf.keras.optimizers.Adam(1e-5),
    loss='categorical_crossentropy',
    metrics=['accuracy']
)

history2 = model.fit(
    train_generator,
    epochs=10,
    validation_data=val_generator,
    callbacks=[
        tf.keras.callbacks.EarlyStopping(patience=3, restore_best_weights=True)
    ]
)

7.5. Transfer Learning para NLP

Modelos Preentrenados de Texto

  • BERT: Bidirectional Encoder Representations from Transformers.
  • GPT: Generative Pre-trained Transformer.
  • RoBERTa: Robustly optimized BERT.
  • DistilBERT: Versión ligera de BERT.
  • XLNet, ALBERT, T5, etc.

Ejemplo con Hugging Face

from transformers import (
    TFBertForSequenceClassification,
    BertTokenizer,
    create_optimizer
)
import tensorflow as tf

# Cargar modelo y tokenizer preentrenados
model_name = 'bert-base-uncased'
tokenizer = BertTokenizer.from_pretrained(model_name)
model = TFBertForSequenceClassification.from_pretrained(
    model_name,
    num_labels=2  # Clasificación binaria
)

# Tokenizar datos
def encode_texts(texts, tokenizer, max_length=128):
    return tokenizer(
        texts,
        truncation=True,
        padding='max_length',
        max_length=max_length,
        return_tensors='tf'
    )

# Datos de ejemplo
train_texts = ["I love this movie!", "This film was terrible."]
train_labels = [1, 0]

train_encodings = encode_texts(train_texts, tokenizer)

# Crear dataset
train_dataset = tf.data.Dataset.from_tensor_slices((
    dict(train_encodings),
    train_labels
)).batch(8)

# Configurar optimizador con warmup
num_train_steps = len(train_dataset) * 3  # 3 épocas
optimizer, schedule = create_optimizer(
    init_lr=2e-5,
    num_train_steps=num_train_steps,
    num_warmup_steps=num_train_steps // 10
)

# Compilar
model.compile(
    optimizer=optimizer,
    loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=['accuracy']
)

# Entrenar (fine-tuning)
model.fit(train_dataset, epochs=3)

Estrategias de Fine-Tuning para BERT

# Estrategia 1: Congelar BERT, solo entrenar clasificador
for layer in model.bert.layers:
    layer.trainable = False

# Estrategia 2: Descongelar últimas N capas
for layer in model.bert.encoder.layer[:-2]:  # Congelar excepto últimas 2
    layer.trainable = False

# Estrategia 3: Learning rate diferente por capa (discriminative fine-tuning)
# Las capas más profundas tienen LR más bajo

7.6. Transfer Learning con PyTorch

import torch
import torch.nn as nn
import torchvision.models as models
from torchvision import transforms
from torch.utils.data import DataLoader

# Cargar modelo preentrenado
model = models.resnet50(pretrained=True)

# Congelar todas las capas
for param in model.parameters():
    param.requires_grad = False

# Reemplazar la última capa
num_ftrs = model.fc.in_features
model.fc = nn.Sequential(
    nn.Linear(num_ftrs, 256),
    nn.ReLU(),
    nn.Dropout(0.2),
    nn.Linear(256, 10)  # 10 clases
)

# Solo los parámetros de la nueva capa se entrenan
optimizer = torch.optim.Adam(model.fc.parameters(), lr=0.001)

# Para fine-tuning posterior
def unfreeze_layers(model, n_layers):
    """Descongela las últimas n_layers."""
    layers = list(model.children())
    for layer in layers[-n_layers:]:
        for param in layer.parameters():
            param.requires_grad = True

# Después de entrenar las capas nuevas
unfreeze_layers(model, 3)
optimizer = torch.optim.Adam(
    filter(lambda p: p.requires_grad, model.parameters()),
    lr=1e-5  # LR bajo para fine-tuning
)

7.7. Domain Adaptation

Cuando el dominio fuente y objetivo son diferentes.

Técnicas

  • Feature Alignment: Alinear distribuciones de características.
  • Adversarial Training: Discriminador de dominios.
  • Self-Training: Pseudo-etiquetas en dominio objetivo.
# Domain Adversarial Neural Network (DANN)
class DomainAdversarialModel(tf.keras.Model):
    def __init__(self, feature_extractor, classifier, domain_discriminator):
        super().__init__()
        self.feature_extractor = feature_extractor
        self.classifier = classifier
        self.domain_discriminator = domain_discriminator

    def call(self, inputs, training=False):
        features = self.feature_extractor(inputs)
        class_output = self.classifier(features)

        # Gradient reversal para el discriminador de dominio
        reversed_features = gradient_reversal(features)
        domain_output = self.domain_discriminator(reversed_features)

        return class_output, domain_output

7.8. Knowledge Distillation

Transferir conocimiento de un modelo grande (teacher) a uno pequeño (student).

import tensorflow as tf

class DistillationLoss(tf.keras.losses.Loss):
    def __init__(self, temperature=3.0, alpha=0.5):
        super().__init__()
        self.temperature = temperature
        self.alpha = alpha

    def call(self, y_true, y_pred, teacher_logits):
        # Pérdida estándar
        hard_loss = tf.keras.losses.categorical_crossentropy(y_true, y_pred)

        # Pérdida de destilación (soft targets)
        soft_teacher = tf.nn.softmax(teacher_logits / self.temperature)
        soft_student = tf.nn.softmax(y_pred / self.temperature)
        soft_loss = tf.keras.losses.categorical_crossentropy(soft_teacher, soft_student)

        # Combinar
        return self.alpha * hard_loss + (1 - self.alpha) * self.temperature**2 * soft_loss

# Entrenamiento con destilación
def train_with_distillation(student, teacher, train_data, epochs):
    teacher.trainable = False  # Teacher congelado

    for epoch in range(epochs):
        for x_batch, y_batch in train_data:
            with tf.GradientTape() as tape:
                # Predicciones del teacher
                teacher_logits = teacher(x_batch, training=False)

                # Predicciones del student
                student_logits = student(x_batch, training=True)

                # Pérdida combinada
                loss = distillation_loss(y_batch, student_logits, teacher_logits)

            gradients = tape.gradient(loss, student.trainable_variables)
            optimizer.apply_gradients(zip(gradients, student.trainable_variables))

7.9. Mejores Prácticas

Preprocesamiento

Usar el mismo preprocesamiento que el modelo preentrenado:

from tensorflow.keras.applications.efficientnet import preprocess_input

# EfficientNet espera valores en [0, 255]
x_train_processed = preprocess_input(x_train)

Learning Rate

  • Feature extraction: LR normal (1e-3 a 1e-4).
  • Fine-tuning: LR muy bajo (1e-5 a 1e-6).

Batch Size

  • Con datasets pequeños, usar batch sizes pequeños.
  • Considerar acumulación de gradientes si la GPU no tiene suficiente memoria.

Regularización

# Añadir regularización para evitar overfitting
from tensorflow.keras.layers import Dropout, BatchNormalization

model = Sequential([
    base_model,
    GlobalAveragePooling2D(),
    BatchNormalization(),
    Dropout(0.5),
    Dense(256, activation='relu', kernel_regularizer='l2'),
    Dropout(0.3),
    Dense(num_classes, activation='softmax')
])

Early Stopping

callbacks = [
    tf.keras.callbacks.EarlyStopping(
        monitor='val_loss',
        patience=5,
        restore_best_weights=True
    ),
    tf.keras.callbacks.ReduceLROnPlateau(
        monitor='val_loss',
        factor=0.2,
        patience=3,
        min_lr=1e-7
    )
]

7.10. Cuándo NO Usar Transfer Learning

  • Dominio muy diferente: Imágenes médicas → pocas similitudes con ImageNet.
  • Mucho datos propios: Si tienes millones de imágenes etiquetadas.
  • Requerimientos específicos: Arquitectura o restricciones especiales.

📅 Fecha de creación: Enero 2026
✍️ Autor: Fran García