Menu iconMenu icon
Aprendizaje Profundo y Superhéroe de IA

Capítulo 4: Aprendizaje profundo con PyTorch

4.3 Aprendizaje por Transferencia y Ajuste Fino de Modelos Preentrenados en PyTorch

En muchas aplicaciones del mundo real, entrenar un modelo de aprendizaje profundo desde cero presenta desafíos significativos, como la escasez de grandes conjuntos de datos etiquetados y los considerables recursos computacionales necesarios para entrenar modelos complejos con millones de parámetros. El aprendizaje por transferencia ofrece una solución elegante a estos desafíos al aprovechar el conocimiento de modelos preexistentes.

Este enfoque consiste en tomar un modelo que ha sido preentrenado en un gran conjunto de datos general (como ImageNet, que contiene millones de imágenes etiquetadas en miles de categorías) y adaptarlo a una nueva tarea, a menudo más específica. La idea clave es que las características aprendidas por el modelo en la tarea original suelen ser lo suficientemente generales como para ser útiles en otras tareas relacionadas.

El aprendizaje por transferencia es particularmente poderoso en dominios como visión por computadora, procesamiento del lenguaje natural y reconocimiento de voz. Por ejemplo, un modelo entrenado en ImageNet puede adaptarse para tareas específicas como identificar especies de plantas o detectar condiciones médicas en radiografías, a menudo con menos datos específicos de la tarea de los que serían necesarios para entrenar desde cero.

Al implementar el aprendizaje por transferencia en PyTorch, los investigadores y profesionales generalmente emplean una de dos estrategias principales:

  1. Extracción de características: En este enfoque, el modelo preentrenado se usa como un extractor de características fijo. Los pesos de la mayoría de la red (generalmente todas las capas excepto la última) se congelan, lo que significa que no se actualizarán durante el entrenamiento. Solo la capa final, a menudo llamada capa de clasificación, se reemplaza con una nueva capa apropiada para la nueva tarea y se entrena con el nuevo conjunto de datos. Este método es particularmente útil cuando la nueva tarea es similar a la tarea original y cuando los recursos computacionales o los datos específicos de la tarea son limitados.
  2. Ajuste fino: Este enfoque más flexible implica descongelar algunas o todas las capas del modelo preentrenado y continuar entrenándolas con el nuevo conjunto de datos. El ajuste fino permite que el modelo adapte sus características aprendidas a las especificidades de la nueva tarea. Este método puede conducir a un mejor rendimiento, especialmente cuando la nueva tarea es significativamente diferente de la original o cuando hay una cantidad sustancial de datos específicos de la tarea disponibles. Sin embargo, requiere una gestión cuidadosa de las tasas de aprendizaje y la regularización para evitar el sobreajuste o el olvido catastrófico de las características originalmente aprendidas.

La elección entre la extracción de características y el ajuste fino a menudo depende de factores como el tamaño y la similitud del nuevo conjunto de datos con el conjunto de datos original, la complejidad de la nueva tarea y los recursos computacionales disponibles. En la práctica, es común comenzar con la extracción de características y avanzar gradualmente hacia el ajuste fino según sea necesario para optimizar el rendimiento.

4.3.1 Modelos Preentrenados en PyTorch

PyTorch ofrece una extensa colección de modelos preentrenados a través del módulo torchvision.models, lo que simplifica significativamente el proceso de aprendizaje por transferencia. Estos modelos, que incluyen arquitecturas populares como ResNet, VGG e Inception, han sido entrenados en el vasto conjunto de datos ImageNet. Este conjunto de datos comprende más de 1.2 millones de imágenes en 1,000 categorías de objetos diversas, lo que permite que estos modelos aprendan características ricas y generalizables.

La disponibilidad de estos modelos preentrenados presenta varias ventajas:

1. Prototipado rápido

Los modelos preentrenados en PyTorch permiten una experimentación rápida con arquitecturas de vanguardia, reduciendo significativamente el tiempo y los recursos típicamente necesarios para el desarrollo de modelos. Esta ventaja permite a los investigadores y desarrolladores:

  • Probar rápidamente hipótesis e ideas utilizando arquitecturas de modelos establecidas.
  • Iterar rápidamente sobre diferentes configuraciones de modelos sin la necesidad de ciclos de entrenamiento extensos.
  • Explorar la efectividad de varias arquitecturas en tareas o conjuntos de datos específicos.
  • Acelerar el proceso de desarrollo aprovechando características preaprendidas.
  • Enfocarse más en la resolución de problemas y menos en las complejidades de la implementación del modelo.

Esta capacidad es particularmente valiosa en campos donde el tiempo de comercialización o los plazos de investigación son críticos, permitiendo una innovación y descubrimiento más rápidos en las aplicaciones de aprendizaje automático.

2. Eficiencia en el aprendizaje por transferencia

Estos modelos preentrenados sirven como puntos de partida excelentes para tareas de aprendizaje por transferencia, reduciendo significativamente el tiempo y los recursos necesarios para el entrenamiento. Al aprovechar las ricas características aprendidas de conjuntos de datos a gran escala como ImageNet, estos modelos pueden ajustarse con eficacia en conjuntos de datos más pequeños y específicos de dominio. Este enfoque es particularmente valioso en escenarios donde los datos etiquetados son escasos o costosos de obtener, como en imágenes médicas o aplicaciones industriales especializadas.

La eficiencia del aprendizaje por transferencia con estos modelos preentrenados se debe a varios factores:

  • Reutilización de características: Las capas inferiores de estos modelos a menudo capturan características genéricas (como bordes, texturas y formas) que son aplicables en una amplia gama de tareas visuales.
  • Reducción del tiempo de entrenamiento: El ajuste fino de un modelo preentrenado generalmente requiere menos épocas para converger en comparación con entrenar desde cero, lo que lleva a ahorros significativos de tiempo.
  • Mejora en la generalización: El conocimiento diverso codificado en los modelos preentrenados a menudo ayuda a lograr una mejor generalización en nuevas tareas, incluso con datos específicos de dominio limitados.
  • Requisitos computacionales más bajos: El ajuste fino generalmente requiere menos poder computacional que entrenar un modelo complejo desde cero, lo que lo hace más accesible para investigadores y desarrolladores con recursos limitados.

Esta eficiencia en el aprendizaje por transferencia ha democratizado el acceso a técnicas de aprendizaje automático de vanguardia, permitiendo el prototipado rápido y el despliegue de modelos sofisticados en diversos dominios y aplicaciones.

3. Comparaciones de referencia

Los modelos preentrenados sirven como puntos de referencia invaluables para evaluar arquitecturas personalizadas. Ofrecen varias ventajas en este sentido:

  • Métricas de rendimiento estandarizadas: Los investigadores pueden comparar sus enfoques novedosos contra puntos de referencia reconocidos, asegurando una evaluación justa y consistente.
  • Información a través de arquitecturas cruzadas: Al comparar con varios modelos preentrenados, los desarrolladores pueden obtener una comprensión más profunda de las fortalezas y debilidades de su modelo personalizado en diferentes diseños arquitectónicos.
  • Eficiencia en tiempo y recursos: Usar modelos preentrenados como referencia elimina la necesidad de entrenar múltiples modelos complejos desde cero, reduciendo significativamente los recursos computacionales y el tiempo necesarios para comparaciones exhaustivas.
  • Rendimiento estándar de la industria: Los modelos preentrenados a menudo representan el rendimiento de vanguardia en conjuntos de datos a gran escala, proporcionando un alto estándar para que los modelos personalizados se esfuercen por igualar o superar.

Esta capacidad de referencia es crucial para avanzar en el campo del aprendizaje automático, ya que permite a los investigadores y profesionales cuantificar mejoras e identificar áreas para más innovaciones en el diseño y las técnicas de entrenamiento del modelo.

Para utilizar estos modelos preentrenados, simplemente puedes importarlos desde torchvision.models y especificar el parámetro pretrained=True. Esto carga la arquitectura del modelo junto con sus pesos preentrenados, listos para ser usados de inmediato o ajustados para tu tarea específica.

Ejemplo: Cargar un Modelo Preentrenado

import torch
import torchvision.models as models
from torchvision import transforms
from PIL import Image
import matplotlib.pyplot as plt

# Load a pretrained ResNet-18 model (compatible with latest torchvision versions)
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

# Print the model architecture
print(model)

# Set the model to evaluation mode
model.eval()

# Define image transformations
transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Load and preprocess an image
img_path = 'path_to_your_image.jpg'  # Ensure this path is correct
img = Image.open(img_path)
img_tensor = transform(img).unsqueeze(0)  # Add batch dimension

# Make a prediction
with torch.no_grad():
    output = model(img_tensor)

# Get the predicted class
_, predicted_idx = torch.max(output, 1)

# Load ImageNet class labels from Torchvision
labels = models.ResNet18_Weights.DEFAULT.meta["categories"]

# Print the predicted class
print(f"Predicted class: {labels[predicted_idx]}")

# Visualize the image
plt.imshow(img)
plt.axis('off')
plt.title(f"Predicted: {labels[predicted_idx]}")
plt.show()

Este ejemplo muestra cómo utilizar un modelo ResNet-18 preentrenado para la clasificación de imágenes en PyTorch.

  1. Importaciones: Las bibliotecas necesarias son torch para PyTorch, torchvision.models para modelos preentrenados, torchvision.transforms para preprocesamiento de imágenes, PIL para el manejo de imágenes y matplotlib.pyplot para visualización.
  2. Cargar el Modelo: El modelo se carga usando models.resnet18(weights=models.ResNet18_Weights.DEFAULT), asegurando la compatibilidad con las últimas versiones de PyTorch. El modelo se establece en modo evaluación usando model.eval().
  3. Preprocesamiento de Imagen: La imagen se redimensiona a 256x256, se recorta del centro a 224x224, se convierte a tensor y se normaliza usando la media y desviación estándar de ImageNet.
  4. Cargar y Procesar Imagen: La imagen se carga usando Image.open(), se transforma y se redimensiona con .unsqueeze(0) para cumplir con los requisitos de entrada del modelo.
  5. Realizar una Predicción: La imagen procesada se pasa a través del modelo dentro de torch.no_grad() para desactivar el seguimiento de gradientes. El índice de clase con la mayor probabilidad se obtiene usando torch.max().
  6. Interpretar los Resultados: El índice de clase predicho se mapea a su etiqueta usando models.ResNet18_Weights.DEFAULT.meta["categories"].
  7. Visualización: La imagen se muestra con matplotlib.pyplot, y la clase predicha se muestra en el título.

Este proceso simple carga un modelo preentrenado, procesa una imagen, realiza una predicción y visualiza el resultado.

4.3.2 Extracción de Características con Modelos Preentrenados

En el enfoque de extracción de características, aprovechamos el poder de los modelos preentrenados tratándolos como sofisticados extractores de características. Este método implica congelar los pesos de las capas convolucionales del modelo preentrenado, que ya han aprendido a reconocer una amplia gama de características visuales a partir de grandes conjuntos de datos como ImageNet. Al mantener estas capas fijas, preservamos su capacidad para extraer características significativas de las imágenes, independientemente de la tarea específica.

La modificación clave en este enfoque es reemplazar la capa final completamente conectada (FC) del modelo preentrenado con una nueva capa adaptada a nuestra tarea específica. Esta nueva capa FC se convierte en la única parte entrenable de la red, actuando como un clasificador que aprende a mapear las características extraídas a las clases de salida deseadas para nuestra nueva tarea. Esta estrategia es particularmente efectiva cuando:

  • La nueva tarea es similar a la tarea original para la que fue entrenado el modelo.
  • El conjunto de datos disponible para la nueva tarea es relativamente pequeño.
  • Los recursos computacionales son limitados.
  • Se necesita un prototipado rápido o experimentación.

Al utilizar la extracción de características, podemos reducir significativamente el tiempo de entrenamiento y los requisitos de recursos, al tiempo que aprovechamos las ricas representaciones de características aprendidas por modelos de vanguardia. Este enfoque permite una rápida adaptación a nuevas tareas y dominios, lo que lo convierte en una técnica valiosa en el aprendizaje por transferencia.

Ejemplo: Usar un ResNet Preentrenado para la Extracción de Características

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

# Load a pretrained ResNet-18 model (compatible with latest torchvision versions)
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

# Freeze all layers in the model (i.e., prevent backpropagation through these layers)
for param in model.parameters():
    param.requires_grad = False

# Replace the final fully connected layer to match the number of classes in the new dataset
# ResNet's final layer (fc) originally outputs 1000 classes, we change it to 10 for CIFAR-10
model.fc = nn.Linear(in_features=model.fc.in_features, out_features=10)

# Print the modified model
print(model)

# Define transformations for the CIFAR-10 dataset
transform = transforms.Compose([
    transforms.Resize(224),  # ResNet expects 224x224 input
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Load CIFAR-10 dataset
train_dataset = CIFAR10(root='./data', train=True, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

# Define loss function and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.fc.parameters(), lr=0.001)

# Training loop
num_epochs = 5
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)

for epoch in range(num_epochs):
    model.train()
    running_loss = 0.0
    for i, (images, labels) in enumerate(train_loader):
        images, labels = images.to(device), labels.to(device)
        
        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        
        running_loss += loss.item()
        
        if (i + 1) % 100 == 0:
            print(f'Epoch [{epoch+1}/{num_epochs}], Step [{i+1}/{len(train_loader)}], Loss: {running_loss/100:.4f}')
            running_loss = 0.0

print("Training completed!")

# Save the fine-tuned model
torch.save(model.state_dict(), 'resnet18_cifar10.pth')

Este ejemplo ajusta finamente un modelo ResNet-18 preentrenado en el conjunto de datos CIFAR-10 usando PyTorch.

  1. Importaciones: Las bibliotecas necesarias incluyen torch para PyTorch, torch.nn para redes neuronales, torchvision.models para modelos preentrenados, torchvision.transforms para preprocesamiento, y torch.utils.data.DataLoader para el manejo de conjuntos de datos.
  2. Cargar el Modelo Preentrenado: El modelo se carga usando models.resnet18(weights=models.ResNet18_Weights.DEFAULT), asegurando la compatibilidad con las versiones más recientes de PyTorch.
  3. Congelar Capas Preentrenadas: Todas las capas excepto la capa totalmente conectada final se congelan usando param.requires_grad = False, evitando actualizaciones innecesarias durante el entrenamiento.
  4. Modificar la Capa Final: La última capa totalmente conectada (fc) se reemplaza para generar 10 clases en lugar de 1000, haciéndola adecuada para CIFAR-10.
  5. Preprocesamiento de Imagen: El conjunto de datos se redimensiona a 224x224, se convierte a tensor y se normaliza usando la media y desviación estándar de ImageNet.
  6. Cargar el Conjunto de Datos CIFAR-10: El conjunto de datos se descarga y se carga en un DataLoader con un tamaño de lote de 32.
  7. Definir Pérdida y Optimizador: La función de pérdida es CrossEntropyLoss, y el optimizador es Adam, actualizando solo la nueva capa fc.
  8. Bucle de Entrenamiento: El modelo se entrena durante 5 épocas, iterando a través de mini-lotes, calculando la pérdida y actualizando los pesos.
  9. Guardar el Modelo: El modelo ajustado se guarda usando torch.save(model.state_dict(), 'resnet18_cifar10.pth') para uso futuro.

Este ejemplo completo muestra todo el proceso de aprendizaje por transferencia, desde la carga de un modelo preentrenado hasta su ajuste fino en un nuevo conjunto de datos y el guardado de los resultados. Es una demostración práctica de cómo aprovechar los modelos preentrenados para nuevas tareas con un entrenamiento mínimo.

4.3.3 Ajuste Fino de un Modelo Preentrenado

En el ajuste fino, permitimos que algunas o todas las capas del modelo preentrenado se actualicen durante el entrenamiento. Este enfoque ofrece un equilibrio entre aprovechar las características previamente aprendidas y adaptar el modelo a una nueva tarea. Generalmente, se congelan las capas iniciales (que capturan características genéricas como bordes y texturas) y se ajustan las capas más profundas (que capturan características más específicas de la tarea).

La lógica detrás de esta estrategia se basa en la naturaleza jerárquica de las redes neuronales. Las capas iniciales tienden a aprender características generales de bajo nivel que son aplicables a una amplia gama de tareas, mientras que las capas más profundas aprenden características más especializadas y de alto nivel que son más específicas de la tarea. Al congelar las capas iniciales, preservamos las valiosas características genéricas aprendidas del gran conjunto de datos en el que el modelo fue entrenado originalmente. Esto es especialmente útil cuando nuestra nueva tarea tiene datos de entrenamiento limitados.

Ajustar finamente las capas más profundas permite que el modelo adapte estas características de alto nivel a las particularidades de la nueva tarea. Este proceso puede mejorar significativamente el rendimiento en comparación con usar el modelo preentrenado tal como está o entrenar un modelo nuevo desde cero, especialmente cuando se trabaja con conjuntos de datos limitados o cuando la nueva tarea es similar a la tarea original para la que fue entrenado el modelo.

El número exacto de capas a congelar o ajustar finamente suele determinarse de manera empírica y puede variar dependiendo de factores como la similitud entre las tareas original y nueva, el tamaño del nuevo conjunto de datos y los recursos computacionales disponibles. En la práctica, es común experimentar con diferentes configuraciones para encontrar el equilibrio óptimo para una tarea determinada.

Ejemplo: Ajuste Fino de las Últimas Capas de un ResNet Preentrenado

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

# Load a pretrained ResNet-18 model
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

# Freeze the first few layers
for name, param in model.named_parameters():
    if 'layer4' not in name and 'fc' not in name:  # Only allow parameters in 'layer4' and 'fc' to be updated
        param.requires_grad = False

# Replace the final fully connected layer
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, 10)  # 10 is the number of classes in CIFAR-10

# Print the modified model with some layers frozen
print(model)

# Define transformations for the CIFAR-10 dataset
transform = transforms.Compose([
    transforms.Resize(224),  # ResNet expects 224x224 input
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Load CIFAR-10 dataset
train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

# Define loss function and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(filter(lambda p: p.requires_grad, model.parameters()), lr=0.001, momentum=0.9)

# Training loop
num_epochs = 5
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)

for epoch in range(num_epochs):
    model.train()
    running_loss = 0.0
    for i, (images, labels) in enumerate(train_loader):
        images, labels = images.to(device), labels.to(device)
        
        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        
        running_loss += loss.item()
        
        if (i + 1) % 100 == 0:
            print(f'Epoch [{epoch+1}/{num_epochs}], Step [{i+1}/{len(train_loader)}], Loss: {running_loss/100:.4f}')
            running_loss = 0.0

print("Fine-tuning completed!")

# Save the fine-tuned model
torch.save(model.state_dict(), 'resnet18_cifar10_finetuned.pth')

Este ejemplo demuestra un enfoque integral para el ajuste fino de un modelo ResNet-18 preentrenado en el conjunto de datos CIFAR-10. Analicémoslo:

  1. Importaciones y Carga del Modelo:
    • Importamos los módulos necesarios de PyTorch y torchvision.
    • Se carga un modelo ResNet-18 preentrenado usando models.resnet18(weights=models.ResNet18_Weights.DEFAULT).
  2. Congelación de Capas:
    • Iteramos a través de los parámetros nombrados del modelo y congelamos todas las capas excepto 'layer4' y 'fc'.
    • Esto se logra estableciendo param.requires_grad = False para las capas que queremos congelar.
  3. Modificación de la Capa Final:
    • La capa totalmente conectada final (fc) se reemplaza por una nueva que genera 10 clases (para CIFAR-10) en lugar de las 1000 originales (para ImageNet).
    • Utilizamos model.fc.in_features para mantener el tamaño de entrada correcto para la nueva capa.
  4. Preparación de Datos:
    • Definimos transformaciones para preprocesar las imágenes CIFAR-10, incluyendo el redimensionamiento a 224x224 (requerido por ResNet), la conversión a tensor y la normalización.
    • Se carga el conjunto de datos CIFAR-10 y se crea un DataLoader para el procesamiento por lotes.
  5. Configuración del Entrenamiento:
    • Se utiliza Cross Entropy Loss como función de pérdida.
    • Se utiliza el optimizador SGD para actualizar solo los parámetros de las capas no congeladas (layer4 y fc).
    • El modelo se traslada a GPU si está disponible.
  6. Bucle de Entrenamiento:
    • El modelo se ajusta finamente durante un número específico de épocas.
    • En cada época, iteramos a través de los datos de entrenamiento, calculamos la pérdida, realizamos la retropropagación y actualizamos las capas no congeladas del modelo.
    • El progreso del entrenamiento se imprime cada 100 pasos.
  7. Guardado del Modelo:
    • Después del ajuste fino, el diccionario de estado del modelo se guarda en un archivo.

Este ejemplo completo muestra todo el proceso de ajuste fino de un modelo preentrenado, desde la carga y modificación del modelo hasta su entrenamiento en un nuevo conjunto de datos y el guardado de los resultados. Demuestra cómo aprovechar el aprendizaje por transferencia manteniendo el conocimiento en las capas iniciales mientras se adaptan las capas posteriores a una nueva tarea.

4.3.4 Entrenando el Modelo con Aprendizaje por Transferencia

Una vez que el modelo se modifica para el aprendizaje por transferencia (ya sea extracción de características o ajuste fino), el proceso de entrenamiento sigue una estructura similar al entrenamiento de un modelo desde cero. Sin embargo, hay algunas diferencias clave a tener en cuenta:

1. Actualizaciones Selectivas de Parámetros

En el aprendizaje por transferencia, solo las capas no congeladas tendrán sus parámetros actualizados durante el entrenamiento. Este enfoque dirigido permite que el modelo conserve características valiosas preaprendidas mientras se adapta a la nueva tarea. Al actualizar selectivamente los parámetros, podemos:

  • Preservar características generales: Las capas iniciales de las redes neuronales a menudo capturan características universales como bordes o texturas. Al congelar estas capas, mantenemos este conocimiento general.
  • Enfocarse en el aprendizaje específico de la tarea: Las capas no congeladas, típicamente las posteriores, se ajustan para aprender características específicas de la nueva tarea.
  • Mitigar el sobreajuste: Cuando se trabaja con conjuntos de datos más pequeños, las actualizaciones selectivas pueden ayudar a prevenir que el modelo se ajuste demasiado a los nuevos datos al mantener algunas de las características robustas aprendidas del conjunto de datos original más grande.

Esta estrategia es particularmente efectiva cuando la nueva tarea es similar a la tarea original, ya que aprovecha el conocimiento existente del modelo mientras permite su adaptación. El número de capas que se deben congelar o ajustar finamente suele requerir experimentación para encontrar el equilibrio óptimo para una tarea dada.

2. Consideraciones sobre la Tasa de Aprendizaje

Al ajustar finamente modelos preentrenados, es crucial elegir cuidadosamente la tasa de aprendizaje. A menudo se recomienda una tasa de aprendizaje más baja por varias razones:

  • Preservación del conocimiento preentrenado: Una tasa de aprendizaje más baja ayuda a mantener las valiosas características aprendidas durante el preentrenamiento, permitiendo que el modelo se adapte gradualmente a la nueva tarea sin perder su conocimiento inicial.
  • Estabilidad en el entrenamiento: Las actualizaciones más pequeñas evitan cambios drásticos en los pesos del modelo, lo que conduce a un entrenamiento más estable y consistente.
  • Evitar óptimos locales: Las actualizaciones suaves permiten que el modelo explore el paisaje de la pérdida más a fondo, potencialmente encontrando mejores óptimos locales o incluso alcanzando el óptimo global.

Además, se pueden emplear técnicas como la programación de la tasa de aprendizaje para optimizar aún más el proceso de ajuste fino. Por ejemplo, se podría comenzar con una tasa de aprendizaje aún más baja y aumentarla gradualmente (warm-up), o utilizar tasas de aprendizaje cíclicas para explorar periódicamente diferentes regiones del espacio de parámetros.

Vale la pena señalar que la tasa de aprendizaje óptima puede variar según factores como la similitud entre las tareas de origen y destino, el tamaño del nuevo conjunto de datos y las capas específicas que se están ajustando finamente. Por lo tanto, a menudo es beneficioso experimentar con diferentes tasas de aprendizaje o utilizar técnicas como los "buscadores de tasa de aprendizaje" para determinar el valor más adecuado para su escenario de aprendizaje por transferencia particular.

3. Flujo de Gradientes y Aprendizaje Específico de Capa

Durante la retropropagación, los gradientes solo fluyen a través de las capas no congeladas, creando una dinámica de aprendizaje única. Este flujo selectivo de gradientes tiene varias implicaciones importantes:

  • Extracción de características fijas: Las capas congeladas, típicamente las iniciales, actúan como extractores de características estáticos. Estas capas, preentrenadas en grandes conjuntos de datos, ya han aprendido a reconocer características generales de bajo nivel como bordes, texturas y formas básicas. Al mantener estas capas congeladas, aprovechamos este conocimiento preexistente sin modificarlo.
  • Aprendizaje adaptable en capas no congeladas: Las capas no congeladas, generalmente las más profundas en la red, reciben y procesan los gradientes. Estas capas aprenden a interpretar y adaptar las características fijas extraídas por las capas congeladas, adaptándolas a los requisitos específicos de la nueva tarea.
  • Aprendizaje eficiente por transferencia: Este enfoque permite que el modelo transfiera eficientemente el conocimiento de la tarea original a la nueva. Preserva las características generalizadas valiosas aprendidas del conjunto de datos original grande, mientras que enfoca el proceso de aprendizaje en adaptaciones específicas de la tarea.
  • Reducción del riesgo de sobreajuste: Al limitar las actualizaciones de parámetros a solo un subconjunto de capas, reducimos el riesgo de sobreajuste, especialmente cuando se trabaja con conjuntos de datos más pequeños para la nueva tarea. Esto es particularmente beneficioso cuando la nueva tarea es similar a la original pero tiene datos de entrenamiento limitados.

Esta estrategia de flujo selectivo de gradientes permite un equilibrio fino entre preservar el conocimiento general y adaptarse a nuevas tareas específicas, lo que hace que el aprendizaje por transferencia sea una técnica poderosa en escenarios con datos o recursos computacionales limitados.

4. Preprocesamiento de Datos y Aumento

Cuando se trabaja con modelos preentrenados, es crucial preprocesar los datos de entrada de una manera coherente con los datos de entrenamiento originales del modelo. Esto garantiza que los nuevos datos estén en un formato que el modelo pueda interpretar eficazmente. El preprocesamiento típicamente incluye:

  • Redimensionamiento de imágenes: La mayoría de los modelos preentrenados esperan imágenes de entrada de un tamaño específico (por ejemplo, 224x224 píxeles para muchas arquitecturas populares). Cambiar el tamaño garantiza que todas las imágenes coincidan con esta dimensión de entrada esperada.
  • Normalización: Esto implica ajustar los valores de los píxeles a una escala estándar, a menudo utilizando la media y la desviación estándar del conjunto de datos de entrenamiento original (por ejemplo, estadísticas de ImageNet para muchos modelos).
  • Aumento de datos: Esta técnica expande artificialmente el conjunto de datos de entrenamiento aplicando varias transformaciones a las imágenes existentes. Aumentaciones comunes incluyen:
    • Recorte y volteo aleatorio: Ayuda al modelo a aprender invarianza a la posición y orientación.
    • Variación de color: Ajusta el brillo, el contraste y la saturación para mejorar la robustez ante condiciones de iluminación.
    • Rotación y escalado: Mejora la capacidad del modelo para reconocer objetos en diferentes ángulos y tamaños.

El preprocesamiento y el aumento adecuados no solo garantizan la compatibilidad con el modelo preentrenado, sino que también pueden mejorar significativamente la capacidad de generalización del modelo y su rendimiento en la nueva tarea.

5. Monitoreo del Rendimiento y Parada Temprana

El monitoreo vigilante del rendimiento del modelo en los conjuntos de datos de entrenamiento y validación es esencial en el aprendizaje por transferencia. A diferencia de los modelos entrenados desde cero, los modelos de aprendizaje por transferencia a menudo muestran una rápida convergencia debido a su conocimiento preexistente. Este proceso de aprendizaje acelerado requiere una observación cuidadosa para evitar el sobreajuste. Implementar técnicas de parada temprana se vuelve crucial en este contexto.

La parada temprana implica detener el proceso de entrenamiento cuando el rendimiento del modelo en el conjunto de validación comienza a deteriorarse, incluso cuando sigue mejorando en el conjunto de entrenamiento. Esta divergencia en el rendimiento es un claro indicador de sobreajuste, donde el modelo comienza a memorizar los datos de entrenamiento en lugar de aprender patrones generalizables.

Para implementar un monitoreo de rendimiento efectivo y parada temprana:

  • Evalúe regularmente el modelo en un conjunto de validación reservado durante el entrenamiento.
  • Rastree métricas clave como precisión, pérdida y, potencialmente, medidas específicas de la tarea (por ejemplo, F1-score para tareas de clasificación).
  • Implemente mecanismos de paciencia, donde el entrenamiento continúa durante un número determinado de épocas incluso después de detectar un posible punto de sobreajuste, para asegurarse de que no sea una fluctuación temporal.
  • Considere usar técnicas como el guardado de puntos de control del modelo para guardar el estado del modelo que mejor funcione, lo que le permite volver a este punto óptimo después del entrenamiento.

Al emplear estas estrategias, puede aprovechar las capacidades de aprendizaje acelerado del aprendizaje por transferencia, mientras se protege contra el sobreajuste, lo que finalmente produce un modelo que generaliza bien en datos no vistos.

Al tener en cuenta estos factores, puede aprovechar eficazmente el aprendizaje por transferencia para lograr un rendimiento superior en nuevas tareas, especialmente cuando se trabaja con conjuntos de datos o recursos computacionales limitados.

Ejemplo: Entrenando un ResNet-18 Preentrenado en un Nuevo Conjunto de Datos

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

# Check if CUDA is available
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

# Define transformations for the new dataset
transform = transforms.Compose([
    transforms.Resize(224),  # ResNet requires 224x224 images
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Load the new dataset (CIFAR-10)
train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
test_dataset = datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)

# Load pre-trained ResNet18 model
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

# Modify the final layer for CIFAR-10 (10 classes)
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, 10)

# Move model to the appropriate device
model = model.to(device)

# Define the loss function and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)

# Training loop
epochs = 10
for epoch in range(epochs):
    model.train()
    running_loss = 0.0
    for i, (images, labels) in enumerate(train_loader):
        images, labels = images.to(device), labels.to(device)
        
        optimizer.zero_grad()   # Zero the parameter gradients
        outputs = model(images)  # Forward pass
        loss = criterion(outputs, labels)  # Compute the loss
        loss.backward()  # Backward pass (compute gradients)
        optimizer.step()  # Optimization step (update parameters)

        running_loss += loss.item()
        
        if i % 100 == 99:    # Print every 100 mini-batches
            print(f'[{epoch + 1}, {i + 1:5d}] loss: {running_loss / 100:.3f}')
            running_loss = 0.0

    # Validation
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for images, labels in test_loader:
            images, labels = images.to(device), labels.to(device)
            outputs = model(images)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()

    print(f'Accuracy on test set: {100 * correct / total:.2f}%')

print('Finished Training')

# Save the model
torch.save(model.state_dict(), 'cifar10_resnet18.pth')

Este ejemplo de código muestra un método para el ajuste fino de un modelo ResNet18 preentrenado en el conjunto de datos CIFAR-10 utilizando PyTorch.

Analicemos los componentes clave y expliquemos sus propósitos:

  1. Importaciones y Configuración del Dispositivo:
    • Importamos los módulos necesarios de PyTorch y torchvision.
    • Verificamos la disponibilidad de CUDA para utilizar la aceleración por GPU si es posible.
  2. Preprocesamiento de Datos:
    • Definimos un proceso de transformación que redimensiona las imágenes a 224x224 (requerido por ResNet), las convierte en tensores y las normaliza utilizando las estadísticas de ImageNet.
    • Los conjuntos de datos de entrenamiento y prueba se cargan utilizando el conjunto de datos CIFAR-10 de torchvision.
  3. Cargadores de Datos:
    • Creamos objetos DataLoader tanto para los conjuntos de entrenamiento como de prueba, que manejan el procesamiento por lotes y la aleatorización de datos.
  4. Preparación del Modelo:
    • Cargamos un modelo ResNet18 preentrenado usando models.resnet18(weights=models.ResNet18_Weights.DEFAULT).
    • La capa totalmente conectada final se modifica para producir 10 clases (para CIFAR-10) en lugar de las 1000 originales (para ImageNet).
    • El modelo se transfiere al dispositivo apropiado (GPU si está disponible).
  5. Función de Pérdida y Optimizador:
    • Se utiliza la Pérdida de Entropía Cruzada como función de pérdida, que es adecuada para clasificación multiclase.
    • Se utiliza el optimizador SGD con una tasa de aprendizaje de 0.001 y momento de 0.9.
  6. Bucle de Entrenamiento:
    • El modelo se entrena durante 10 épocas.
    • En cada época, iteramos a través de los datos de entrenamiento, calculamos la pérdida, realizamos la retropropagación y actualizamos los parámetros del modelo.
    • El progreso del entrenamiento se imprime cada 100 lotes.
  7. Validación:
    • Después de cada época, el modelo se evalúa en el conjunto de prueba para medir su precisión.
    • Esto ayuda a monitorear el rendimiento del modelo y detectar el sobreajuste.
  8. Guardado del Modelo:
    • Después del entrenamiento, el diccionario de estado del modelo se guarda en un archivo para uso posterior.

Este ejemplo muestra el proceso completo de ajuste fino de un modelo preentrenado, desde la preparación de datos hasta la evaluación y guardado del modelo. Demuestra las mejores prácticas como el uso de aceleración por GPU, preprocesamiento adecuado de datos y evaluación regular del rendimiento durante el entrenamiento.

4.3.5 Evaluación del Modelo Ajustado Finamente

Después de la fase de entrenamiento, es crucial evaluar el rendimiento del modelo en un conjunto de datos de prueba separado. Este proceso de evaluación cumple varios propósitos:

  • Proporciona una estimación imparcial de la capacidad del modelo para generalizar a datos no vistos.
  • Ayuda a detectar posibles problemas de sobreajuste que podrían haber ocurrido durante el entrenamiento.
  • Permite la comparación con otros modelos o versiones anteriores del mismo modelo.

Al evaluar el modelo en un conjunto de prueba, podemos medir qué tan bien se desempeña nuestro modelo ajustado finamente en datos que no ha encontrado durante el proceso de entrenamiento, brindándonos información valiosa sobre su aplicabilidad en el mundo real.

Ejemplo: Evaluación del Modelo Ajustado Finamente

import torch
import torchvision
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
import numpy as np

# Define the device
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

# Define transformations for the test dataset
transform = transforms.Compose([
    transforms.Resize(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Load the test dataset (CIFAR-10 test set)
test_dataset = datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)

# Load the model (assuming it's already trained and saved)
model = torchvision.models.resnet18(weights=None)
num_ftrs = model.fc.in_features
model.fc = torch.nn.Linear(num_ftrs, 10)  # 10 classes for CIFAR-10
model.load_state_dict(torch.load('cifar10_resnet18.pth'))
model = model.to(device)

# Switch model to evaluation mode
model.eval()

# Disable gradient computation for evaluation
correct = 0
total = 0
class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))

with torch.no_grad():
    for images, labels in test_loader:
        images, labels = images.to(device), labels.to(device)
        outputs = model(images)
        _, predicted = torch.max(outputs, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()
        
        c = (predicted == labels).squeeze()
        for i in range(len(labels)):
            label = labels[i]
            class_correct[label] += c[i].item()
            class_total[label] += 1

# Calculate overall accuracy
accuracy = 100 * correct / total
print(f'Overall Accuracy on test set: {accuracy:.2f}%')

# Calculate and print per-class accuracy
classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
for i in range(10):
    print(f'Accuracy of {classes[i]}: {100 * class_correct[i] / class_total[i]:.2f}%')

# Visualize some predictions
def imshow(img):
    img = img / 2 + 0.5     # unnormalize
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))
    plt.axis('off')

# Get some random test images
dataiter = iter(test_loader)
images, labels = next(dataiter)

# Make predictions
outputs = model(images.to(device))
_, predicted = torch.max(outputs, 1)

# Show images and their predicted labels
fig = plt.figure(figsize=(12, 48))
for i in range(4):
    ax = fig.add_subplot(1, 4, i+1)
    imshow(images[i])
    ax.set_title(f'Predicted: {classes[predicted[i]]}\nActual: {classes[labels[i]]}')

plt.tight_layout()
plt.show()

Este ejemplo de código proporciona una evaluación integral del modelo ajustado finamente. Analicemos sus componentes:

  1. Importaciones y Configuración del Dispositivo:
    • Importamos los módulos necesarios de PyTorch y torchvision.
    • Configuramos el dispositivo (CPU o GPU) para el cómputo.
  2. Preprocesamiento de Datos:
    • Definimos el mismo proceso de transformación utilizado durante el entrenamiento.
    • Cargamos el conjunto de prueba CIFAR-10 y creamos un DataLoader.
  3. Carga del Modelo:
    • Recreamos la arquitectura del modelo (ResNet18 con la capa final modificada).
    • Cargamos los pesos guardados del modelo desde 'cifar10_resnet18.pth'.
    • Trasladamos el modelo al dispositivo apropiado (CPU o GPU).
  4. Bucle de Evaluación:
    • Cambiamos el modelo a modo evaluación usando model.eval().
    • Desactivamos el cálculo de gradientes usando torch.no_grad() para ahorrar memoria y acelerar el cómputo.
    • Iteramos a través de los datos de prueba, realizando predicciones y comparándolas con las etiquetas verdaderas.
    • Llevamos un registro de las predicciones correctas totales y por clase.
  5. Cálculo y Reporte de Resultados:
    • Calculamos e imprimimos la precisión general en el conjunto de prueba.
    • Calculamos e imprimimos las precisiones por clase, lo que nos da una idea de en qué clases el modelo funciona bien y en cuáles tiene dificultades.
  6. Visualización:
    • Definimos una función imshow() para mostrar imágenes.
    • Obtenemos un lote de imágenes de prueba y realizamos predicciones sobre ellas.
    • Visualizamos 4 imágenes de prueba aleatorias junto con sus etiquetas predichas y reales.

Esta evaluación integral proporciona varios beneficios:

  • Nos da la precisión general, que es una medida global del rendimiento del modelo.
  • Proporciona precisiones por clase, permitiéndonos identificar si el modelo tiene sesgos a favor o en contra de ciertas clases.
  • La visualización de las predicciones nos ayuda a evaluar cualitativamente el rendimiento del modelo y potencialmente identificar patrones en sus errores.

Este enfoque de evaluación del modelo nos proporciona una comprensión mucho más detallada de las fortalezas y debilidades de nuestro modelo, lo cual es crucial para mejoras posteriores y para evaluar su idoneidad para su implementación en aplicaciones del mundo real.

4.3 Aprendizaje por Transferencia y Ajuste Fino de Modelos Preentrenados en PyTorch

En muchas aplicaciones del mundo real, entrenar un modelo de aprendizaje profundo desde cero presenta desafíos significativos, como la escasez de grandes conjuntos de datos etiquetados y los considerables recursos computacionales necesarios para entrenar modelos complejos con millones de parámetros. El aprendizaje por transferencia ofrece una solución elegante a estos desafíos al aprovechar el conocimiento de modelos preexistentes.

Este enfoque consiste en tomar un modelo que ha sido preentrenado en un gran conjunto de datos general (como ImageNet, que contiene millones de imágenes etiquetadas en miles de categorías) y adaptarlo a una nueva tarea, a menudo más específica. La idea clave es que las características aprendidas por el modelo en la tarea original suelen ser lo suficientemente generales como para ser útiles en otras tareas relacionadas.

El aprendizaje por transferencia es particularmente poderoso en dominios como visión por computadora, procesamiento del lenguaje natural y reconocimiento de voz. Por ejemplo, un modelo entrenado en ImageNet puede adaptarse para tareas específicas como identificar especies de plantas o detectar condiciones médicas en radiografías, a menudo con menos datos específicos de la tarea de los que serían necesarios para entrenar desde cero.

Al implementar el aprendizaje por transferencia en PyTorch, los investigadores y profesionales generalmente emplean una de dos estrategias principales:

  1. Extracción de características: En este enfoque, el modelo preentrenado se usa como un extractor de características fijo. Los pesos de la mayoría de la red (generalmente todas las capas excepto la última) se congelan, lo que significa que no se actualizarán durante el entrenamiento. Solo la capa final, a menudo llamada capa de clasificación, se reemplaza con una nueva capa apropiada para la nueva tarea y se entrena con el nuevo conjunto de datos. Este método es particularmente útil cuando la nueva tarea es similar a la tarea original y cuando los recursos computacionales o los datos específicos de la tarea son limitados.
  2. Ajuste fino: Este enfoque más flexible implica descongelar algunas o todas las capas del modelo preentrenado y continuar entrenándolas con el nuevo conjunto de datos. El ajuste fino permite que el modelo adapte sus características aprendidas a las especificidades de la nueva tarea. Este método puede conducir a un mejor rendimiento, especialmente cuando la nueva tarea es significativamente diferente de la original o cuando hay una cantidad sustancial de datos específicos de la tarea disponibles. Sin embargo, requiere una gestión cuidadosa de las tasas de aprendizaje y la regularización para evitar el sobreajuste o el olvido catastrófico de las características originalmente aprendidas.

La elección entre la extracción de características y el ajuste fino a menudo depende de factores como el tamaño y la similitud del nuevo conjunto de datos con el conjunto de datos original, la complejidad de la nueva tarea y los recursos computacionales disponibles. En la práctica, es común comenzar con la extracción de características y avanzar gradualmente hacia el ajuste fino según sea necesario para optimizar el rendimiento.

4.3.1 Modelos Preentrenados en PyTorch

PyTorch ofrece una extensa colección de modelos preentrenados a través del módulo torchvision.models, lo que simplifica significativamente el proceso de aprendizaje por transferencia. Estos modelos, que incluyen arquitecturas populares como ResNet, VGG e Inception, han sido entrenados en el vasto conjunto de datos ImageNet. Este conjunto de datos comprende más de 1.2 millones de imágenes en 1,000 categorías de objetos diversas, lo que permite que estos modelos aprendan características ricas y generalizables.

La disponibilidad de estos modelos preentrenados presenta varias ventajas:

1. Prototipado rápido

Los modelos preentrenados en PyTorch permiten una experimentación rápida con arquitecturas de vanguardia, reduciendo significativamente el tiempo y los recursos típicamente necesarios para el desarrollo de modelos. Esta ventaja permite a los investigadores y desarrolladores:

  • Probar rápidamente hipótesis e ideas utilizando arquitecturas de modelos establecidas.
  • Iterar rápidamente sobre diferentes configuraciones de modelos sin la necesidad de ciclos de entrenamiento extensos.
  • Explorar la efectividad de varias arquitecturas en tareas o conjuntos de datos específicos.
  • Acelerar el proceso de desarrollo aprovechando características preaprendidas.
  • Enfocarse más en la resolución de problemas y menos en las complejidades de la implementación del modelo.

Esta capacidad es particularmente valiosa en campos donde el tiempo de comercialización o los plazos de investigación son críticos, permitiendo una innovación y descubrimiento más rápidos en las aplicaciones de aprendizaje automático.

2. Eficiencia en el aprendizaje por transferencia

Estos modelos preentrenados sirven como puntos de partida excelentes para tareas de aprendizaje por transferencia, reduciendo significativamente el tiempo y los recursos necesarios para el entrenamiento. Al aprovechar las ricas características aprendidas de conjuntos de datos a gran escala como ImageNet, estos modelos pueden ajustarse con eficacia en conjuntos de datos más pequeños y específicos de dominio. Este enfoque es particularmente valioso en escenarios donde los datos etiquetados son escasos o costosos de obtener, como en imágenes médicas o aplicaciones industriales especializadas.

La eficiencia del aprendizaje por transferencia con estos modelos preentrenados se debe a varios factores:

  • Reutilización de características: Las capas inferiores de estos modelos a menudo capturan características genéricas (como bordes, texturas y formas) que son aplicables en una amplia gama de tareas visuales.
  • Reducción del tiempo de entrenamiento: El ajuste fino de un modelo preentrenado generalmente requiere menos épocas para converger en comparación con entrenar desde cero, lo que lleva a ahorros significativos de tiempo.
  • Mejora en la generalización: El conocimiento diverso codificado en los modelos preentrenados a menudo ayuda a lograr una mejor generalización en nuevas tareas, incluso con datos específicos de dominio limitados.
  • Requisitos computacionales más bajos: El ajuste fino generalmente requiere menos poder computacional que entrenar un modelo complejo desde cero, lo que lo hace más accesible para investigadores y desarrolladores con recursos limitados.

Esta eficiencia en el aprendizaje por transferencia ha democratizado el acceso a técnicas de aprendizaje automático de vanguardia, permitiendo el prototipado rápido y el despliegue de modelos sofisticados en diversos dominios y aplicaciones.

3. Comparaciones de referencia

Los modelos preentrenados sirven como puntos de referencia invaluables para evaluar arquitecturas personalizadas. Ofrecen varias ventajas en este sentido:

  • Métricas de rendimiento estandarizadas: Los investigadores pueden comparar sus enfoques novedosos contra puntos de referencia reconocidos, asegurando una evaluación justa y consistente.
  • Información a través de arquitecturas cruzadas: Al comparar con varios modelos preentrenados, los desarrolladores pueden obtener una comprensión más profunda de las fortalezas y debilidades de su modelo personalizado en diferentes diseños arquitectónicos.
  • Eficiencia en tiempo y recursos: Usar modelos preentrenados como referencia elimina la necesidad de entrenar múltiples modelos complejos desde cero, reduciendo significativamente los recursos computacionales y el tiempo necesarios para comparaciones exhaustivas.
  • Rendimiento estándar de la industria: Los modelos preentrenados a menudo representan el rendimiento de vanguardia en conjuntos de datos a gran escala, proporcionando un alto estándar para que los modelos personalizados se esfuercen por igualar o superar.

Esta capacidad de referencia es crucial para avanzar en el campo del aprendizaje automático, ya que permite a los investigadores y profesionales cuantificar mejoras e identificar áreas para más innovaciones en el diseño y las técnicas de entrenamiento del modelo.

Para utilizar estos modelos preentrenados, simplemente puedes importarlos desde torchvision.models y especificar el parámetro pretrained=True. Esto carga la arquitectura del modelo junto con sus pesos preentrenados, listos para ser usados de inmediato o ajustados para tu tarea específica.

Ejemplo: Cargar un Modelo Preentrenado

import torch
import torchvision.models as models
from torchvision import transforms
from PIL import Image
import matplotlib.pyplot as plt

# Load a pretrained ResNet-18 model (compatible with latest torchvision versions)
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

# Print the model architecture
print(model)

# Set the model to evaluation mode
model.eval()

# Define image transformations
transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Load and preprocess an image
img_path = 'path_to_your_image.jpg'  # Ensure this path is correct
img = Image.open(img_path)
img_tensor = transform(img).unsqueeze(0)  # Add batch dimension

# Make a prediction
with torch.no_grad():
    output = model(img_tensor)

# Get the predicted class
_, predicted_idx = torch.max(output, 1)

# Load ImageNet class labels from Torchvision
labels = models.ResNet18_Weights.DEFAULT.meta["categories"]

# Print the predicted class
print(f"Predicted class: {labels[predicted_idx]}")

# Visualize the image
plt.imshow(img)
plt.axis('off')
plt.title(f"Predicted: {labels[predicted_idx]}")
plt.show()

Este ejemplo muestra cómo utilizar un modelo ResNet-18 preentrenado para la clasificación de imágenes en PyTorch.

  1. Importaciones: Las bibliotecas necesarias son torch para PyTorch, torchvision.models para modelos preentrenados, torchvision.transforms para preprocesamiento de imágenes, PIL para el manejo de imágenes y matplotlib.pyplot para visualización.
  2. Cargar el Modelo: El modelo se carga usando models.resnet18(weights=models.ResNet18_Weights.DEFAULT), asegurando la compatibilidad con las últimas versiones de PyTorch. El modelo se establece en modo evaluación usando model.eval().
  3. Preprocesamiento de Imagen: La imagen se redimensiona a 256x256, se recorta del centro a 224x224, se convierte a tensor y se normaliza usando la media y desviación estándar de ImageNet.
  4. Cargar y Procesar Imagen: La imagen se carga usando Image.open(), se transforma y se redimensiona con .unsqueeze(0) para cumplir con los requisitos de entrada del modelo.
  5. Realizar una Predicción: La imagen procesada se pasa a través del modelo dentro de torch.no_grad() para desactivar el seguimiento de gradientes. El índice de clase con la mayor probabilidad se obtiene usando torch.max().
  6. Interpretar los Resultados: El índice de clase predicho se mapea a su etiqueta usando models.ResNet18_Weights.DEFAULT.meta["categories"].
  7. Visualización: La imagen se muestra con matplotlib.pyplot, y la clase predicha se muestra en el título.

Este proceso simple carga un modelo preentrenado, procesa una imagen, realiza una predicción y visualiza el resultado.

4.3.2 Extracción de Características con Modelos Preentrenados

En el enfoque de extracción de características, aprovechamos el poder de los modelos preentrenados tratándolos como sofisticados extractores de características. Este método implica congelar los pesos de las capas convolucionales del modelo preentrenado, que ya han aprendido a reconocer una amplia gama de características visuales a partir de grandes conjuntos de datos como ImageNet. Al mantener estas capas fijas, preservamos su capacidad para extraer características significativas de las imágenes, independientemente de la tarea específica.

La modificación clave en este enfoque es reemplazar la capa final completamente conectada (FC) del modelo preentrenado con una nueva capa adaptada a nuestra tarea específica. Esta nueva capa FC se convierte en la única parte entrenable de la red, actuando como un clasificador que aprende a mapear las características extraídas a las clases de salida deseadas para nuestra nueva tarea. Esta estrategia es particularmente efectiva cuando:

  • La nueva tarea es similar a la tarea original para la que fue entrenado el modelo.
  • El conjunto de datos disponible para la nueva tarea es relativamente pequeño.
  • Los recursos computacionales son limitados.
  • Se necesita un prototipado rápido o experimentación.

Al utilizar la extracción de características, podemos reducir significativamente el tiempo de entrenamiento y los requisitos de recursos, al tiempo que aprovechamos las ricas representaciones de características aprendidas por modelos de vanguardia. Este enfoque permite una rápida adaptación a nuevas tareas y dominios, lo que lo convierte en una técnica valiosa en el aprendizaje por transferencia.

Ejemplo: Usar un ResNet Preentrenado para la Extracción de Características

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

# Load a pretrained ResNet-18 model (compatible with latest torchvision versions)
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

# Freeze all layers in the model (i.e., prevent backpropagation through these layers)
for param in model.parameters():
    param.requires_grad = False

# Replace the final fully connected layer to match the number of classes in the new dataset
# ResNet's final layer (fc) originally outputs 1000 classes, we change it to 10 for CIFAR-10
model.fc = nn.Linear(in_features=model.fc.in_features, out_features=10)

# Print the modified model
print(model)

# Define transformations for the CIFAR-10 dataset
transform = transforms.Compose([
    transforms.Resize(224),  # ResNet expects 224x224 input
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Load CIFAR-10 dataset
train_dataset = CIFAR10(root='./data', train=True, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

# Define loss function and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.fc.parameters(), lr=0.001)

# Training loop
num_epochs = 5
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)

for epoch in range(num_epochs):
    model.train()
    running_loss = 0.0
    for i, (images, labels) in enumerate(train_loader):
        images, labels = images.to(device), labels.to(device)
        
        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        
        running_loss += loss.item()
        
        if (i + 1) % 100 == 0:
            print(f'Epoch [{epoch+1}/{num_epochs}], Step [{i+1}/{len(train_loader)}], Loss: {running_loss/100:.4f}')
            running_loss = 0.0

print("Training completed!")

# Save the fine-tuned model
torch.save(model.state_dict(), 'resnet18_cifar10.pth')

Este ejemplo ajusta finamente un modelo ResNet-18 preentrenado en el conjunto de datos CIFAR-10 usando PyTorch.

  1. Importaciones: Las bibliotecas necesarias incluyen torch para PyTorch, torch.nn para redes neuronales, torchvision.models para modelos preentrenados, torchvision.transforms para preprocesamiento, y torch.utils.data.DataLoader para el manejo de conjuntos de datos.
  2. Cargar el Modelo Preentrenado: El modelo se carga usando models.resnet18(weights=models.ResNet18_Weights.DEFAULT), asegurando la compatibilidad con las versiones más recientes de PyTorch.
  3. Congelar Capas Preentrenadas: Todas las capas excepto la capa totalmente conectada final se congelan usando param.requires_grad = False, evitando actualizaciones innecesarias durante el entrenamiento.
  4. Modificar la Capa Final: La última capa totalmente conectada (fc) se reemplaza para generar 10 clases en lugar de 1000, haciéndola adecuada para CIFAR-10.
  5. Preprocesamiento de Imagen: El conjunto de datos se redimensiona a 224x224, se convierte a tensor y se normaliza usando la media y desviación estándar de ImageNet.
  6. Cargar el Conjunto de Datos CIFAR-10: El conjunto de datos se descarga y se carga en un DataLoader con un tamaño de lote de 32.
  7. Definir Pérdida y Optimizador: La función de pérdida es CrossEntropyLoss, y el optimizador es Adam, actualizando solo la nueva capa fc.
  8. Bucle de Entrenamiento: El modelo se entrena durante 5 épocas, iterando a través de mini-lotes, calculando la pérdida y actualizando los pesos.
  9. Guardar el Modelo: El modelo ajustado se guarda usando torch.save(model.state_dict(), 'resnet18_cifar10.pth') para uso futuro.

Este ejemplo completo muestra todo el proceso de aprendizaje por transferencia, desde la carga de un modelo preentrenado hasta su ajuste fino en un nuevo conjunto de datos y el guardado de los resultados. Es una demostración práctica de cómo aprovechar los modelos preentrenados para nuevas tareas con un entrenamiento mínimo.

4.3.3 Ajuste Fino de un Modelo Preentrenado

En el ajuste fino, permitimos que algunas o todas las capas del modelo preentrenado se actualicen durante el entrenamiento. Este enfoque ofrece un equilibrio entre aprovechar las características previamente aprendidas y adaptar el modelo a una nueva tarea. Generalmente, se congelan las capas iniciales (que capturan características genéricas como bordes y texturas) y se ajustan las capas más profundas (que capturan características más específicas de la tarea).

La lógica detrás de esta estrategia se basa en la naturaleza jerárquica de las redes neuronales. Las capas iniciales tienden a aprender características generales de bajo nivel que son aplicables a una amplia gama de tareas, mientras que las capas más profundas aprenden características más especializadas y de alto nivel que son más específicas de la tarea. Al congelar las capas iniciales, preservamos las valiosas características genéricas aprendidas del gran conjunto de datos en el que el modelo fue entrenado originalmente. Esto es especialmente útil cuando nuestra nueva tarea tiene datos de entrenamiento limitados.

Ajustar finamente las capas más profundas permite que el modelo adapte estas características de alto nivel a las particularidades de la nueva tarea. Este proceso puede mejorar significativamente el rendimiento en comparación con usar el modelo preentrenado tal como está o entrenar un modelo nuevo desde cero, especialmente cuando se trabaja con conjuntos de datos limitados o cuando la nueva tarea es similar a la tarea original para la que fue entrenado el modelo.

El número exacto de capas a congelar o ajustar finamente suele determinarse de manera empírica y puede variar dependiendo de factores como la similitud entre las tareas original y nueva, el tamaño del nuevo conjunto de datos y los recursos computacionales disponibles. En la práctica, es común experimentar con diferentes configuraciones para encontrar el equilibrio óptimo para una tarea determinada.

Ejemplo: Ajuste Fino de las Últimas Capas de un ResNet Preentrenado

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

# Load a pretrained ResNet-18 model
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

# Freeze the first few layers
for name, param in model.named_parameters():
    if 'layer4' not in name and 'fc' not in name:  # Only allow parameters in 'layer4' and 'fc' to be updated
        param.requires_grad = False

# Replace the final fully connected layer
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, 10)  # 10 is the number of classes in CIFAR-10

# Print the modified model with some layers frozen
print(model)

# Define transformations for the CIFAR-10 dataset
transform = transforms.Compose([
    transforms.Resize(224),  # ResNet expects 224x224 input
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Load CIFAR-10 dataset
train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

# Define loss function and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(filter(lambda p: p.requires_grad, model.parameters()), lr=0.001, momentum=0.9)

# Training loop
num_epochs = 5
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)

for epoch in range(num_epochs):
    model.train()
    running_loss = 0.0
    for i, (images, labels) in enumerate(train_loader):
        images, labels = images.to(device), labels.to(device)
        
        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        
        running_loss += loss.item()
        
        if (i + 1) % 100 == 0:
            print(f'Epoch [{epoch+1}/{num_epochs}], Step [{i+1}/{len(train_loader)}], Loss: {running_loss/100:.4f}')
            running_loss = 0.0

print("Fine-tuning completed!")

# Save the fine-tuned model
torch.save(model.state_dict(), 'resnet18_cifar10_finetuned.pth')

Este ejemplo demuestra un enfoque integral para el ajuste fino de un modelo ResNet-18 preentrenado en el conjunto de datos CIFAR-10. Analicémoslo:

  1. Importaciones y Carga del Modelo:
    • Importamos los módulos necesarios de PyTorch y torchvision.
    • Se carga un modelo ResNet-18 preentrenado usando models.resnet18(weights=models.ResNet18_Weights.DEFAULT).
  2. Congelación de Capas:
    • Iteramos a través de los parámetros nombrados del modelo y congelamos todas las capas excepto 'layer4' y 'fc'.
    • Esto se logra estableciendo param.requires_grad = False para las capas que queremos congelar.
  3. Modificación de la Capa Final:
    • La capa totalmente conectada final (fc) se reemplaza por una nueva que genera 10 clases (para CIFAR-10) en lugar de las 1000 originales (para ImageNet).
    • Utilizamos model.fc.in_features para mantener el tamaño de entrada correcto para la nueva capa.
  4. Preparación de Datos:
    • Definimos transformaciones para preprocesar las imágenes CIFAR-10, incluyendo el redimensionamiento a 224x224 (requerido por ResNet), la conversión a tensor y la normalización.
    • Se carga el conjunto de datos CIFAR-10 y se crea un DataLoader para el procesamiento por lotes.
  5. Configuración del Entrenamiento:
    • Se utiliza Cross Entropy Loss como función de pérdida.
    • Se utiliza el optimizador SGD para actualizar solo los parámetros de las capas no congeladas (layer4 y fc).
    • El modelo se traslada a GPU si está disponible.
  6. Bucle de Entrenamiento:
    • El modelo se ajusta finamente durante un número específico de épocas.
    • En cada época, iteramos a través de los datos de entrenamiento, calculamos la pérdida, realizamos la retropropagación y actualizamos las capas no congeladas del modelo.
    • El progreso del entrenamiento se imprime cada 100 pasos.
  7. Guardado del Modelo:
    • Después del ajuste fino, el diccionario de estado del modelo se guarda en un archivo.

Este ejemplo completo muestra todo el proceso de ajuste fino de un modelo preentrenado, desde la carga y modificación del modelo hasta su entrenamiento en un nuevo conjunto de datos y el guardado de los resultados. Demuestra cómo aprovechar el aprendizaje por transferencia manteniendo el conocimiento en las capas iniciales mientras se adaptan las capas posteriores a una nueva tarea.

4.3.4 Entrenando el Modelo con Aprendizaje por Transferencia

Una vez que el modelo se modifica para el aprendizaje por transferencia (ya sea extracción de características o ajuste fino), el proceso de entrenamiento sigue una estructura similar al entrenamiento de un modelo desde cero. Sin embargo, hay algunas diferencias clave a tener en cuenta:

1. Actualizaciones Selectivas de Parámetros

En el aprendizaje por transferencia, solo las capas no congeladas tendrán sus parámetros actualizados durante el entrenamiento. Este enfoque dirigido permite que el modelo conserve características valiosas preaprendidas mientras se adapta a la nueva tarea. Al actualizar selectivamente los parámetros, podemos:

  • Preservar características generales: Las capas iniciales de las redes neuronales a menudo capturan características universales como bordes o texturas. Al congelar estas capas, mantenemos este conocimiento general.
  • Enfocarse en el aprendizaje específico de la tarea: Las capas no congeladas, típicamente las posteriores, se ajustan para aprender características específicas de la nueva tarea.
  • Mitigar el sobreajuste: Cuando se trabaja con conjuntos de datos más pequeños, las actualizaciones selectivas pueden ayudar a prevenir que el modelo se ajuste demasiado a los nuevos datos al mantener algunas de las características robustas aprendidas del conjunto de datos original más grande.

Esta estrategia es particularmente efectiva cuando la nueva tarea es similar a la tarea original, ya que aprovecha el conocimiento existente del modelo mientras permite su adaptación. El número de capas que se deben congelar o ajustar finamente suele requerir experimentación para encontrar el equilibrio óptimo para una tarea dada.

2. Consideraciones sobre la Tasa de Aprendizaje

Al ajustar finamente modelos preentrenados, es crucial elegir cuidadosamente la tasa de aprendizaje. A menudo se recomienda una tasa de aprendizaje más baja por varias razones:

  • Preservación del conocimiento preentrenado: Una tasa de aprendizaje más baja ayuda a mantener las valiosas características aprendidas durante el preentrenamiento, permitiendo que el modelo se adapte gradualmente a la nueva tarea sin perder su conocimiento inicial.
  • Estabilidad en el entrenamiento: Las actualizaciones más pequeñas evitan cambios drásticos en los pesos del modelo, lo que conduce a un entrenamiento más estable y consistente.
  • Evitar óptimos locales: Las actualizaciones suaves permiten que el modelo explore el paisaje de la pérdida más a fondo, potencialmente encontrando mejores óptimos locales o incluso alcanzando el óptimo global.

Además, se pueden emplear técnicas como la programación de la tasa de aprendizaje para optimizar aún más el proceso de ajuste fino. Por ejemplo, se podría comenzar con una tasa de aprendizaje aún más baja y aumentarla gradualmente (warm-up), o utilizar tasas de aprendizaje cíclicas para explorar periódicamente diferentes regiones del espacio de parámetros.

Vale la pena señalar que la tasa de aprendizaje óptima puede variar según factores como la similitud entre las tareas de origen y destino, el tamaño del nuevo conjunto de datos y las capas específicas que se están ajustando finamente. Por lo tanto, a menudo es beneficioso experimentar con diferentes tasas de aprendizaje o utilizar técnicas como los "buscadores de tasa de aprendizaje" para determinar el valor más adecuado para su escenario de aprendizaje por transferencia particular.

3. Flujo de Gradientes y Aprendizaje Específico de Capa

Durante la retropropagación, los gradientes solo fluyen a través de las capas no congeladas, creando una dinámica de aprendizaje única. Este flujo selectivo de gradientes tiene varias implicaciones importantes:

  • Extracción de características fijas: Las capas congeladas, típicamente las iniciales, actúan como extractores de características estáticos. Estas capas, preentrenadas en grandes conjuntos de datos, ya han aprendido a reconocer características generales de bajo nivel como bordes, texturas y formas básicas. Al mantener estas capas congeladas, aprovechamos este conocimiento preexistente sin modificarlo.
  • Aprendizaje adaptable en capas no congeladas: Las capas no congeladas, generalmente las más profundas en la red, reciben y procesan los gradientes. Estas capas aprenden a interpretar y adaptar las características fijas extraídas por las capas congeladas, adaptándolas a los requisitos específicos de la nueva tarea.
  • Aprendizaje eficiente por transferencia: Este enfoque permite que el modelo transfiera eficientemente el conocimiento de la tarea original a la nueva. Preserva las características generalizadas valiosas aprendidas del conjunto de datos original grande, mientras que enfoca el proceso de aprendizaje en adaptaciones específicas de la tarea.
  • Reducción del riesgo de sobreajuste: Al limitar las actualizaciones de parámetros a solo un subconjunto de capas, reducimos el riesgo de sobreajuste, especialmente cuando se trabaja con conjuntos de datos más pequeños para la nueva tarea. Esto es particularmente beneficioso cuando la nueva tarea es similar a la original pero tiene datos de entrenamiento limitados.

Esta estrategia de flujo selectivo de gradientes permite un equilibrio fino entre preservar el conocimiento general y adaptarse a nuevas tareas específicas, lo que hace que el aprendizaje por transferencia sea una técnica poderosa en escenarios con datos o recursos computacionales limitados.

4. Preprocesamiento de Datos y Aumento

Cuando se trabaja con modelos preentrenados, es crucial preprocesar los datos de entrada de una manera coherente con los datos de entrenamiento originales del modelo. Esto garantiza que los nuevos datos estén en un formato que el modelo pueda interpretar eficazmente. El preprocesamiento típicamente incluye:

  • Redimensionamiento de imágenes: La mayoría de los modelos preentrenados esperan imágenes de entrada de un tamaño específico (por ejemplo, 224x224 píxeles para muchas arquitecturas populares). Cambiar el tamaño garantiza que todas las imágenes coincidan con esta dimensión de entrada esperada.
  • Normalización: Esto implica ajustar los valores de los píxeles a una escala estándar, a menudo utilizando la media y la desviación estándar del conjunto de datos de entrenamiento original (por ejemplo, estadísticas de ImageNet para muchos modelos).
  • Aumento de datos: Esta técnica expande artificialmente el conjunto de datos de entrenamiento aplicando varias transformaciones a las imágenes existentes. Aumentaciones comunes incluyen:
    • Recorte y volteo aleatorio: Ayuda al modelo a aprender invarianza a la posición y orientación.
    • Variación de color: Ajusta el brillo, el contraste y la saturación para mejorar la robustez ante condiciones de iluminación.
    • Rotación y escalado: Mejora la capacidad del modelo para reconocer objetos en diferentes ángulos y tamaños.

El preprocesamiento y el aumento adecuados no solo garantizan la compatibilidad con el modelo preentrenado, sino que también pueden mejorar significativamente la capacidad de generalización del modelo y su rendimiento en la nueva tarea.

5. Monitoreo del Rendimiento y Parada Temprana

El monitoreo vigilante del rendimiento del modelo en los conjuntos de datos de entrenamiento y validación es esencial en el aprendizaje por transferencia. A diferencia de los modelos entrenados desde cero, los modelos de aprendizaje por transferencia a menudo muestran una rápida convergencia debido a su conocimiento preexistente. Este proceso de aprendizaje acelerado requiere una observación cuidadosa para evitar el sobreajuste. Implementar técnicas de parada temprana se vuelve crucial en este contexto.

La parada temprana implica detener el proceso de entrenamiento cuando el rendimiento del modelo en el conjunto de validación comienza a deteriorarse, incluso cuando sigue mejorando en el conjunto de entrenamiento. Esta divergencia en el rendimiento es un claro indicador de sobreajuste, donde el modelo comienza a memorizar los datos de entrenamiento en lugar de aprender patrones generalizables.

Para implementar un monitoreo de rendimiento efectivo y parada temprana:

  • Evalúe regularmente el modelo en un conjunto de validación reservado durante el entrenamiento.
  • Rastree métricas clave como precisión, pérdida y, potencialmente, medidas específicas de la tarea (por ejemplo, F1-score para tareas de clasificación).
  • Implemente mecanismos de paciencia, donde el entrenamiento continúa durante un número determinado de épocas incluso después de detectar un posible punto de sobreajuste, para asegurarse de que no sea una fluctuación temporal.
  • Considere usar técnicas como el guardado de puntos de control del modelo para guardar el estado del modelo que mejor funcione, lo que le permite volver a este punto óptimo después del entrenamiento.

Al emplear estas estrategias, puede aprovechar las capacidades de aprendizaje acelerado del aprendizaje por transferencia, mientras se protege contra el sobreajuste, lo que finalmente produce un modelo que generaliza bien en datos no vistos.

Al tener en cuenta estos factores, puede aprovechar eficazmente el aprendizaje por transferencia para lograr un rendimiento superior en nuevas tareas, especialmente cuando se trabaja con conjuntos de datos o recursos computacionales limitados.

Ejemplo: Entrenando un ResNet-18 Preentrenado en un Nuevo Conjunto de Datos

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

# Check if CUDA is available
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

# Define transformations for the new dataset
transform = transforms.Compose([
    transforms.Resize(224),  # ResNet requires 224x224 images
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Load the new dataset (CIFAR-10)
train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
test_dataset = datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)

# Load pre-trained ResNet18 model
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

# Modify the final layer for CIFAR-10 (10 classes)
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, 10)

# Move model to the appropriate device
model = model.to(device)

# Define the loss function and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)

# Training loop
epochs = 10
for epoch in range(epochs):
    model.train()
    running_loss = 0.0
    for i, (images, labels) in enumerate(train_loader):
        images, labels = images.to(device), labels.to(device)
        
        optimizer.zero_grad()   # Zero the parameter gradients
        outputs = model(images)  # Forward pass
        loss = criterion(outputs, labels)  # Compute the loss
        loss.backward()  # Backward pass (compute gradients)
        optimizer.step()  # Optimization step (update parameters)

        running_loss += loss.item()
        
        if i % 100 == 99:    # Print every 100 mini-batches
            print(f'[{epoch + 1}, {i + 1:5d}] loss: {running_loss / 100:.3f}')
            running_loss = 0.0

    # Validation
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for images, labels in test_loader:
            images, labels = images.to(device), labels.to(device)
            outputs = model(images)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()

    print(f'Accuracy on test set: {100 * correct / total:.2f}%')

print('Finished Training')

# Save the model
torch.save(model.state_dict(), 'cifar10_resnet18.pth')

Este ejemplo de código muestra un método para el ajuste fino de un modelo ResNet18 preentrenado en el conjunto de datos CIFAR-10 utilizando PyTorch.

Analicemos los componentes clave y expliquemos sus propósitos:

  1. Importaciones y Configuración del Dispositivo:
    • Importamos los módulos necesarios de PyTorch y torchvision.
    • Verificamos la disponibilidad de CUDA para utilizar la aceleración por GPU si es posible.
  2. Preprocesamiento de Datos:
    • Definimos un proceso de transformación que redimensiona las imágenes a 224x224 (requerido por ResNet), las convierte en tensores y las normaliza utilizando las estadísticas de ImageNet.
    • Los conjuntos de datos de entrenamiento y prueba se cargan utilizando el conjunto de datos CIFAR-10 de torchvision.
  3. Cargadores de Datos:
    • Creamos objetos DataLoader tanto para los conjuntos de entrenamiento como de prueba, que manejan el procesamiento por lotes y la aleatorización de datos.
  4. Preparación del Modelo:
    • Cargamos un modelo ResNet18 preentrenado usando models.resnet18(weights=models.ResNet18_Weights.DEFAULT).
    • La capa totalmente conectada final se modifica para producir 10 clases (para CIFAR-10) en lugar de las 1000 originales (para ImageNet).
    • El modelo se transfiere al dispositivo apropiado (GPU si está disponible).
  5. Función de Pérdida y Optimizador:
    • Se utiliza la Pérdida de Entropía Cruzada como función de pérdida, que es adecuada para clasificación multiclase.
    • Se utiliza el optimizador SGD con una tasa de aprendizaje de 0.001 y momento de 0.9.
  6. Bucle de Entrenamiento:
    • El modelo se entrena durante 10 épocas.
    • En cada época, iteramos a través de los datos de entrenamiento, calculamos la pérdida, realizamos la retropropagación y actualizamos los parámetros del modelo.
    • El progreso del entrenamiento se imprime cada 100 lotes.
  7. Validación:
    • Después de cada época, el modelo se evalúa en el conjunto de prueba para medir su precisión.
    • Esto ayuda a monitorear el rendimiento del modelo y detectar el sobreajuste.
  8. Guardado del Modelo:
    • Después del entrenamiento, el diccionario de estado del modelo se guarda en un archivo para uso posterior.

Este ejemplo muestra el proceso completo de ajuste fino de un modelo preentrenado, desde la preparación de datos hasta la evaluación y guardado del modelo. Demuestra las mejores prácticas como el uso de aceleración por GPU, preprocesamiento adecuado de datos y evaluación regular del rendimiento durante el entrenamiento.

4.3.5 Evaluación del Modelo Ajustado Finamente

Después de la fase de entrenamiento, es crucial evaluar el rendimiento del modelo en un conjunto de datos de prueba separado. Este proceso de evaluación cumple varios propósitos:

  • Proporciona una estimación imparcial de la capacidad del modelo para generalizar a datos no vistos.
  • Ayuda a detectar posibles problemas de sobreajuste que podrían haber ocurrido durante el entrenamiento.
  • Permite la comparación con otros modelos o versiones anteriores del mismo modelo.

Al evaluar el modelo en un conjunto de prueba, podemos medir qué tan bien se desempeña nuestro modelo ajustado finamente en datos que no ha encontrado durante el proceso de entrenamiento, brindándonos información valiosa sobre su aplicabilidad en el mundo real.

Ejemplo: Evaluación del Modelo Ajustado Finamente

import torch
import torchvision
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
import numpy as np

# Define the device
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

# Define transformations for the test dataset
transform = transforms.Compose([
    transforms.Resize(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Load the test dataset (CIFAR-10 test set)
test_dataset = datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)

# Load the model (assuming it's already trained and saved)
model = torchvision.models.resnet18(weights=None)
num_ftrs = model.fc.in_features
model.fc = torch.nn.Linear(num_ftrs, 10)  # 10 classes for CIFAR-10
model.load_state_dict(torch.load('cifar10_resnet18.pth'))
model = model.to(device)

# Switch model to evaluation mode
model.eval()

# Disable gradient computation for evaluation
correct = 0
total = 0
class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))

with torch.no_grad():
    for images, labels in test_loader:
        images, labels = images.to(device), labels.to(device)
        outputs = model(images)
        _, predicted = torch.max(outputs, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()
        
        c = (predicted == labels).squeeze()
        for i in range(len(labels)):
            label = labels[i]
            class_correct[label] += c[i].item()
            class_total[label] += 1

# Calculate overall accuracy
accuracy = 100 * correct / total
print(f'Overall Accuracy on test set: {accuracy:.2f}%')

# Calculate and print per-class accuracy
classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
for i in range(10):
    print(f'Accuracy of {classes[i]}: {100 * class_correct[i] / class_total[i]:.2f}%')

# Visualize some predictions
def imshow(img):
    img = img / 2 + 0.5     # unnormalize
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))
    plt.axis('off')

# Get some random test images
dataiter = iter(test_loader)
images, labels = next(dataiter)

# Make predictions
outputs = model(images.to(device))
_, predicted = torch.max(outputs, 1)

# Show images and their predicted labels
fig = plt.figure(figsize=(12, 48))
for i in range(4):
    ax = fig.add_subplot(1, 4, i+1)
    imshow(images[i])
    ax.set_title(f'Predicted: {classes[predicted[i]]}\nActual: {classes[labels[i]]}')

plt.tight_layout()
plt.show()

Este ejemplo de código proporciona una evaluación integral del modelo ajustado finamente. Analicemos sus componentes:

  1. Importaciones y Configuración del Dispositivo:
    • Importamos los módulos necesarios de PyTorch y torchvision.
    • Configuramos el dispositivo (CPU o GPU) para el cómputo.
  2. Preprocesamiento de Datos:
    • Definimos el mismo proceso de transformación utilizado durante el entrenamiento.
    • Cargamos el conjunto de prueba CIFAR-10 y creamos un DataLoader.
  3. Carga del Modelo:
    • Recreamos la arquitectura del modelo (ResNet18 con la capa final modificada).
    • Cargamos los pesos guardados del modelo desde 'cifar10_resnet18.pth'.
    • Trasladamos el modelo al dispositivo apropiado (CPU o GPU).
  4. Bucle de Evaluación:
    • Cambiamos el modelo a modo evaluación usando model.eval().
    • Desactivamos el cálculo de gradientes usando torch.no_grad() para ahorrar memoria y acelerar el cómputo.
    • Iteramos a través de los datos de prueba, realizando predicciones y comparándolas con las etiquetas verdaderas.
    • Llevamos un registro de las predicciones correctas totales y por clase.
  5. Cálculo y Reporte de Resultados:
    • Calculamos e imprimimos la precisión general en el conjunto de prueba.
    • Calculamos e imprimimos las precisiones por clase, lo que nos da una idea de en qué clases el modelo funciona bien y en cuáles tiene dificultades.
  6. Visualización:
    • Definimos una función imshow() para mostrar imágenes.
    • Obtenemos un lote de imágenes de prueba y realizamos predicciones sobre ellas.
    • Visualizamos 4 imágenes de prueba aleatorias junto con sus etiquetas predichas y reales.

Esta evaluación integral proporciona varios beneficios:

  • Nos da la precisión general, que es una medida global del rendimiento del modelo.
  • Proporciona precisiones por clase, permitiéndonos identificar si el modelo tiene sesgos a favor o en contra de ciertas clases.
  • La visualización de las predicciones nos ayuda a evaluar cualitativamente el rendimiento del modelo y potencialmente identificar patrones en sus errores.

Este enfoque de evaluación del modelo nos proporciona una comprensión mucho más detallada de las fortalezas y debilidades de nuestro modelo, lo cual es crucial para mejoras posteriores y para evaluar su idoneidad para su implementación en aplicaciones del mundo real.

4.3 Aprendizaje por Transferencia y Ajuste Fino de Modelos Preentrenados en PyTorch

En muchas aplicaciones del mundo real, entrenar un modelo de aprendizaje profundo desde cero presenta desafíos significativos, como la escasez de grandes conjuntos de datos etiquetados y los considerables recursos computacionales necesarios para entrenar modelos complejos con millones de parámetros. El aprendizaje por transferencia ofrece una solución elegante a estos desafíos al aprovechar el conocimiento de modelos preexistentes.

Este enfoque consiste en tomar un modelo que ha sido preentrenado en un gran conjunto de datos general (como ImageNet, que contiene millones de imágenes etiquetadas en miles de categorías) y adaptarlo a una nueva tarea, a menudo más específica. La idea clave es que las características aprendidas por el modelo en la tarea original suelen ser lo suficientemente generales como para ser útiles en otras tareas relacionadas.

El aprendizaje por transferencia es particularmente poderoso en dominios como visión por computadora, procesamiento del lenguaje natural y reconocimiento de voz. Por ejemplo, un modelo entrenado en ImageNet puede adaptarse para tareas específicas como identificar especies de plantas o detectar condiciones médicas en radiografías, a menudo con menos datos específicos de la tarea de los que serían necesarios para entrenar desde cero.

Al implementar el aprendizaje por transferencia en PyTorch, los investigadores y profesionales generalmente emplean una de dos estrategias principales:

  1. Extracción de características: En este enfoque, el modelo preentrenado se usa como un extractor de características fijo. Los pesos de la mayoría de la red (generalmente todas las capas excepto la última) se congelan, lo que significa que no se actualizarán durante el entrenamiento. Solo la capa final, a menudo llamada capa de clasificación, se reemplaza con una nueva capa apropiada para la nueva tarea y se entrena con el nuevo conjunto de datos. Este método es particularmente útil cuando la nueva tarea es similar a la tarea original y cuando los recursos computacionales o los datos específicos de la tarea son limitados.
  2. Ajuste fino: Este enfoque más flexible implica descongelar algunas o todas las capas del modelo preentrenado y continuar entrenándolas con el nuevo conjunto de datos. El ajuste fino permite que el modelo adapte sus características aprendidas a las especificidades de la nueva tarea. Este método puede conducir a un mejor rendimiento, especialmente cuando la nueva tarea es significativamente diferente de la original o cuando hay una cantidad sustancial de datos específicos de la tarea disponibles. Sin embargo, requiere una gestión cuidadosa de las tasas de aprendizaje y la regularización para evitar el sobreajuste o el olvido catastrófico de las características originalmente aprendidas.

La elección entre la extracción de características y el ajuste fino a menudo depende de factores como el tamaño y la similitud del nuevo conjunto de datos con el conjunto de datos original, la complejidad de la nueva tarea y los recursos computacionales disponibles. En la práctica, es común comenzar con la extracción de características y avanzar gradualmente hacia el ajuste fino según sea necesario para optimizar el rendimiento.

4.3.1 Modelos Preentrenados en PyTorch

PyTorch ofrece una extensa colección de modelos preentrenados a través del módulo torchvision.models, lo que simplifica significativamente el proceso de aprendizaje por transferencia. Estos modelos, que incluyen arquitecturas populares como ResNet, VGG e Inception, han sido entrenados en el vasto conjunto de datos ImageNet. Este conjunto de datos comprende más de 1.2 millones de imágenes en 1,000 categorías de objetos diversas, lo que permite que estos modelos aprendan características ricas y generalizables.

La disponibilidad de estos modelos preentrenados presenta varias ventajas:

1. Prototipado rápido

Los modelos preentrenados en PyTorch permiten una experimentación rápida con arquitecturas de vanguardia, reduciendo significativamente el tiempo y los recursos típicamente necesarios para el desarrollo de modelos. Esta ventaja permite a los investigadores y desarrolladores:

  • Probar rápidamente hipótesis e ideas utilizando arquitecturas de modelos establecidas.
  • Iterar rápidamente sobre diferentes configuraciones de modelos sin la necesidad de ciclos de entrenamiento extensos.
  • Explorar la efectividad de varias arquitecturas en tareas o conjuntos de datos específicos.
  • Acelerar el proceso de desarrollo aprovechando características preaprendidas.
  • Enfocarse más en la resolución de problemas y menos en las complejidades de la implementación del modelo.

Esta capacidad es particularmente valiosa en campos donde el tiempo de comercialización o los plazos de investigación son críticos, permitiendo una innovación y descubrimiento más rápidos en las aplicaciones de aprendizaje automático.

2. Eficiencia en el aprendizaje por transferencia

Estos modelos preentrenados sirven como puntos de partida excelentes para tareas de aprendizaje por transferencia, reduciendo significativamente el tiempo y los recursos necesarios para el entrenamiento. Al aprovechar las ricas características aprendidas de conjuntos de datos a gran escala como ImageNet, estos modelos pueden ajustarse con eficacia en conjuntos de datos más pequeños y específicos de dominio. Este enfoque es particularmente valioso en escenarios donde los datos etiquetados son escasos o costosos de obtener, como en imágenes médicas o aplicaciones industriales especializadas.

La eficiencia del aprendizaje por transferencia con estos modelos preentrenados se debe a varios factores:

  • Reutilización de características: Las capas inferiores de estos modelos a menudo capturan características genéricas (como bordes, texturas y formas) que son aplicables en una amplia gama de tareas visuales.
  • Reducción del tiempo de entrenamiento: El ajuste fino de un modelo preentrenado generalmente requiere menos épocas para converger en comparación con entrenar desde cero, lo que lleva a ahorros significativos de tiempo.
  • Mejora en la generalización: El conocimiento diverso codificado en los modelos preentrenados a menudo ayuda a lograr una mejor generalización en nuevas tareas, incluso con datos específicos de dominio limitados.
  • Requisitos computacionales más bajos: El ajuste fino generalmente requiere menos poder computacional que entrenar un modelo complejo desde cero, lo que lo hace más accesible para investigadores y desarrolladores con recursos limitados.

Esta eficiencia en el aprendizaje por transferencia ha democratizado el acceso a técnicas de aprendizaje automático de vanguardia, permitiendo el prototipado rápido y el despliegue de modelos sofisticados en diversos dominios y aplicaciones.

3. Comparaciones de referencia

Los modelos preentrenados sirven como puntos de referencia invaluables para evaluar arquitecturas personalizadas. Ofrecen varias ventajas en este sentido:

  • Métricas de rendimiento estandarizadas: Los investigadores pueden comparar sus enfoques novedosos contra puntos de referencia reconocidos, asegurando una evaluación justa y consistente.
  • Información a través de arquitecturas cruzadas: Al comparar con varios modelos preentrenados, los desarrolladores pueden obtener una comprensión más profunda de las fortalezas y debilidades de su modelo personalizado en diferentes diseños arquitectónicos.
  • Eficiencia en tiempo y recursos: Usar modelos preentrenados como referencia elimina la necesidad de entrenar múltiples modelos complejos desde cero, reduciendo significativamente los recursos computacionales y el tiempo necesarios para comparaciones exhaustivas.
  • Rendimiento estándar de la industria: Los modelos preentrenados a menudo representan el rendimiento de vanguardia en conjuntos de datos a gran escala, proporcionando un alto estándar para que los modelos personalizados se esfuercen por igualar o superar.

Esta capacidad de referencia es crucial para avanzar en el campo del aprendizaje automático, ya que permite a los investigadores y profesionales cuantificar mejoras e identificar áreas para más innovaciones en el diseño y las técnicas de entrenamiento del modelo.

Para utilizar estos modelos preentrenados, simplemente puedes importarlos desde torchvision.models y especificar el parámetro pretrained=True. Esto carga la arquitectura del modelo junto con sus pesos preentrenados, listos para ser usados de inmediato o ajustados para tu tarea específica.

Ejemplo: Cargar un Modelo Preentrenado

import torch
import torchvision.models as models
from torchvision import transforms
from PIL import Image
import matplotlib.pyplot as plt

# Load a pretrained ResNet-18 model (compatible with latest torchvision versions)
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

# Print the model architecture
print(model)

# Set the model to evaluation mode
model.eval()

# Define image transformations
transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Load and preprocess an image
img_path = 'path_to_your_image.jpg'  # Ensure this path is correct
img = Image.open(img_path)
img_tensor = transform(img).unsqueeze(0)  # Add batch dimension

# Make a prediction
with torch.no_grad():
    output = model(img_tensor)

# Get the predicted class
_, predicted_idx = torch.max(output, 1)

# Load ImageNet class labels from Torchvision
labels = models.ResNet18_Weights.DEFAULT.meta["categories"]

# Print the predicted class
print(f"Predicted class: {labels[predicted_idx]}")

# Visualize the image
plt.imshow(img)
plt.axis('off')
plt.title(f"Predicted: {labels[predicted_idx]}")
plt.show()

Este ejemplo muestra cómo utilizar un modelo ResNet-18 preentrenado para la clasificación de imágenes en PyTorch.

  1. Importaciones: Las bibliotecas necesarias son torch para PyTorch, torchvision.models para modelos preentrenados, torchvision.transforms para preprocesamiento de imágenes, PIL para el manejo de imágenes y matplotlib.pyplot para visualización.
  2. Cargar el Modelo: El modelo se carga usando models.resnet18(weights=models.ResNet18_Weights.DEFAULT), asegurando la compatibilidad con las últimas versiones de PyTorch. El modelo se establece en modo evaluación usando model.eval().
  3. Preprocesamiento de Imagen: La imagen se redimensiona a 256x256, se recorta del centro a 224x224, se convierte a tensor y se normaliza usando la media y desviación estándar de ImageNet.
  4. Cargar y Procesar Imagen: La imagen se carga usando Image.open(), se transforma y se redimensiona con .unsqueeze(0) para cumplir con los requisitos de entrada del modelo.
  5. Realizar una Predicción: La imagen procesada se pasa a través del modelo dentro de torch.no_grad() para desactivar el seguimiento de gradientes. El índice de clase con la mayor probabilidad se obtiene usando torch.max().
  6. Interpretar los Resultados: El índice de clase predicho se mapea a su etiqueta usando models.ResNet18_Weights.DEFAULT.meta["categories"].
  7. Visualización: La imagen se muestra con matplotlib.pyplot, y la clase predicha se muestra en el título.

Este proceso simple carga un modelo preentrenado, procesa una imagen, realiza una predicción y visualiza el resultado.

4.3.2 Extracción de Características con Modelos Preentrenados

En el enfoque de extracción de características, aprovechamos el poder de los modelos preentrenados tratándolos como sofisticados extractores de características. Este método implica congelar los pesos de las capas convolucionales del modelo preentrenado, que ya han aprendido a reconocer una amplia gama de características visuales a partir de grandes conjuntos de datos como ImageNet. Al mantener estas capas fijas, preservamos su capacidad para extraer características significativas de las imágenes, independientemente de la tarea específica.

La modificación clave en este enfoque es reemplazar la capa final completamente conectada (FC) del modelo preentrenado con una nueva capa adaptada a nuestra tarea específica. Esta nueva capa FC se convierte en la única parte entrenable de la red, actuando como un clasificador que aprende a mapear las características extraídas a las clases de salida deseadas para nuestra nueva tarea. Esta estrategia es particularmente efectiva cuando:

  • La nueva tarea es similar a la tarea original para la que fue entrenado el modelo.
  • El conjunto de datos disponible para la nueva tarea es relativamente pequeño.
  • Los recursos computacionales son limitados.
  • Se necesita un prototipado rápido o experimentación.

Al utilizar la extracción de características, podemos reducir significativamente el tiempo de entrenamiento y los requisitos de recursos, al tiempo que aprovechamos las ricas representaciones de características aprendidas por modelos de vanguardia. Este enfoque permite una rápida adaptación a nuevas tareas y dominios, lo que lo convierte en una técnica valiosa en el aprendizaje por transferencia.

Ejemplo: Usar un ResNet Preentrenado para la Extracción de Características

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

# Load a pretrained ResNet-18 model (compatible with latest torchvision versions)
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

# Freeze all layers in the model (i.e., prevent backpropagation through these layers)
for param in model.parameters():
    param.requires_grad = False

# Replace the final fully connected layer to match the number of classes in the new dataset
# ResNet's final layer (fc) originally outputs 1000 classes, we change it to 10 for CIFAR-10
model.fc = nn.Linear(in_features=model.fc.in_features, out_features=10)

# Print the modified model
print(model)

# Define transformations for the CIFAR-10 dataset
transform = transforms.Compose([
    transforms.Resize(224),  # ResNet expects 224x224 input
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Load CIFAR-10 dataset
train_dataset = CIFAR10(root='./data', train=True, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

# Define loss function and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.fc.parameters(), lr=0.001)

# Training loop
num_epochs = 5
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)

for epoch in range(num_epochs):
    model.train()
    running_loss = 0.0
    for i, (images, labels) in enumerate(train_loader):
        images, labels = images.to(device), labels.to(device)
        
        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        
        running_loss += loss.item()
        
        if (i + 1) % 100 == 0:
            print(f'Epoch [{epoch+1}/{num_epochs}], Step [{i+1}/{len(train_loader)}], Loss: {running_loss/100:.4f}')
            running_loss = 0.0

print("Training completed!")

# Save the fine-tuned model
torch.save(model.state_dict(), 'resnet18_cifar10.pth')

Este ejemplo ajusta finamente un modelo ResNet-18 preentrenado en el conjunto de datos CIFAR-10 usando PyTorch.

  1. Importaciones: Las bibliotecas necesarias incluyen torch para PyTorch, torch.nn para redes neuronales, torchvision.models para modelos preentrenados, torchvision.transforms para preprocesamiento, y torch.utils.data.DataLoader para el manejo de conjuntos de datos.
  2. Cargar el Modelo Preentrenado: El modelo se carga usando models.resnet18(weights=models.ResNet18_Weights.DEFAULT), asegurando la compatibilidad con las versiones más recientes de PyTorch.
  3. Congelar Capas Preentrenadas: Todas las capas excepto la capa totalmente conectada final se congelan usando param.requires_grad = False, evitando actualizaciones innecesarias durante el entrenamiento.
  4. Modificar la Capa Final: La última capa totalmente conectada (fc) se reemplaza para generar 10 clases en lugar de 1000, haciéndola adecuada para CIFAR-10.
  5. Preprocesamiento de Imagen: El conjunto de datos se redimensiona a 224x224, se convierte a tensor y se normaliza usando la media y desviación estándar de ImageNet.
  6. Cargar el Conjunto de Datos CIFAR-10: El conjunto de datos se descarga y se carga en un DataLoader con un tamaño de lote de 32.
  7. Definir Pérdida y Optimizador: La función de pérdida es CrossEntropyLoss, y el optimizador es Adam, actualizando solo la nueva capa fc.
  8. Bucle de Entrenamiento: El modelo se entrena durante 5 épocas, iterando a través de mini-lotes, calculando la pérdida y actualizando los pesos.
  9. Guardar el Modelo: El modelo ajustado se guarda usando torch.save(model.state_dict(), 'resnet18_cifar10.pth') para uso futuro.

Este ejemplo completo muestra todo el proceso de aprendizaje por transferencia, desde la carga de un modelo preentrenado hasta su ajuste fino en un nuevo conjunto de datos y el guardado de los resultados. Es una demostración práctica de cómo aprovechar los modelos preentrenados para nuevas tareas con un entrenamiento mínimo.

4.3.3 Ajuste Fino de un Modelo Preentrenado

En el ajuste fino, permitimos que algunas o todas las capas del modelo preentrenado se actualicen durante el entrenamiento. Este enfoque ofrece un equilibrio entre aprovechar las características previamente aprendidas y adaptar el modelo a una nueva tarea. Generalmente, se congelan las capas iniciales (que capturan características genéricas como bordes y texturas) y se ajustan las capas más profundas (que capturan características más específicas de la tarea).

La lógica detrás de esta estrategia se basa en la naturaleza jerárquica de las redes neuronales. Las capas iniciales tienden a aprender características generales de bajo nivel que son aplicables a una amplia gama de tareas, mientras que las capas más profundas aprenden características más especializadas y de alto nivel que son más específicas de la tarea. Al congelar las capas iniciales, preservamos las valiosas características genéricas aprendidas del gran conjunto de datos en el que el modelo fue entrenado originalmente. Esto es especialmente útil cuando nuestra nueva tarea tiene datos de entrenamiento limitados.

Ajustar finamente las capas más profundas permite que el modelo adapte estas características de alto nivel a las particularidades de la nueva tarea. Este proceso puede mejorar significativamente el rendimiento en comparación con usar el modelo preentrenado tal como está o entrenar un modelo nuevo desde cero, especialmente cuando se trabaja con conjuntos de datos limitados o cuando la nueva tarea es similar a la tarea original para la que fue entrenado el modelo.

El número exacto de capas a congelar o ajustar finamente suele determinarse de manera empírica y puede variar dependiendo de factores como la similitud entre las tareas original y nueva, el tamaño del nuevo conjunto de datos y los recursos computacionales disponibles. En la práctica, es común experimentar con diferentes configuraciones para encontrar el equilibrio óptimo para una tarea determinada.

Ejemplo: Ajuste Fino de las Últimas Capas de un ResNet Preentrenado

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

# Load a pretrained ResNet-18 model
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

# Freeze the first few layers
for name, param in model.named_parameters():
    if 'layer4' not in name and 'fc' not in name:  # Only allow parameters in 'layer4' and 'fc' to be updated
        param.requires_grad = False

# Replace the final fully connected layer
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, 10)  # 10 is the number of classes in CIFAR-10

# Print the modified model with some layers frozen
print(model)

# Define transformations for the CIFAR-10 dataset
transform = transforms.Compose([
    transforms.Resize(224),  # ResNet expects 224x224 input
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Load CIFAR-10 dataset
train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

# Define loss function and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(filter(lambda p: p.requires_grad, model.parameters()), lr=0.001, momentum=0.9)

# Training loop
num_epochs = 5
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)

for epoch in range(num_epochs):
    model.train()
    running_loss = 0.0
    for i, (images, labels) in enumerate(train_loader):
        images, labels = images.to(device), labels.to(device)
        
        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        
        running_loss += loss.item()
        
        if (i + 1) % 100 == 0:
            print(f'Epoch [{epoch+1}/{num_epochs}], Step [{i+1}/{len(train_loader)}], Loss: {running_loss/100:.4f}')
            running_loss = 0.0

print("Fine-tuning completed!")

# Save the fine-tuned model
torch.save(model.state_dict(), 'resnet18_cifar10_finetuned.pth')

Este ejemplo demuestra un enfoque integral para el ajuste fino de un modelo ResNet-18 preentrenado en el conjunto de datos CIFAR-10. Analicémoslo:

  1. Importaciones y Carga del Modelo:
    • Importamos los módulos necesarios de PyTorch y torchvision.
    • Se carga un modelo ResNet-18 preentrenado usando models.resnet18(weights=models.ResNet18_Weights.DEFAULT).
  2. Congelación de Capas:
    • Iteramos a través de los parámetros nombrados del modelo y congelamos todas las capas excepto 'layer4' y 'fc'.
    • Esto se logra estableciendo param.requires_grad = False para las capas que queremos congelar.
  3. Modificación de la Capa Final:
    • La capa totalmente conectada final (fc) se reemplaza por una nueva que genera 10 clases (para CIFAR-10) en lugar de las 1000 originales (para ImageNet).
    • Utilizamos model.fc.in_features para mantener el tamaño de entrada correcto para la nueva capa.
  4. Preparación de Datos:
    • Definimos transformaciones para preprocesar las imágenes CIFAR-10, incluyendo el redimensionamiento a 224x224 (requerido por ResNet), la conversión a tensor y la normalización.
    • Se carga el conjunto de datos CIFAR-10 y se crea un DataLoader para el procesamiento por lotes.
  5. Configuración del Entrenamiento:
    • Se utiliza Cross Entropy Loss como función de pérdida.
    • Se utiliza el optimizador SGD para actualizar solo los parámetros de las capas no congeladas (layer4 y fc).
    • El modelo se traslada a GPU si está disponible.
  6. Bucle de Entrenamiento:
    • El modelo se ajusta finamente durante un número específico de épocas.
    • En cada época, iteramos a través de los datos de entrenamiento, calculamos la pérdida, realizamos la retropropagación y actualizamos las capas no congeladas del modelo.
    • El progreso del entrenamiento se imprime cada 100 pasos.
  7. Guardado del Modelo:
    • Después del ajuste fino, el diccionario de estado del modelo se guarda en un archivo.

Este ejemplo completo muestra todo el proceso de ajuste fino de un modelo preentrenado, desde la carga y modificación del modelo hasta su entrenamiento en un nuevo conjunto de datos y el guardado de los resultados. Demuestra cómo aprovechar el aprendizaje por transferencia manteniendo el conocimiento en las capas iniciales mientras se adaptan las capas posteriores a una nueva tarea.

4.3.4 Entrenando el Modelo con Aprendizaje por Transferencia

Una vez que el modelo se modifica para el aprendizaje por transferencia (ya sea extracción de características o ajuste fino), el proceso de entrenamiento sigue una estructura similar al entrenamiento de un modelo desde cero. Sin embargo, hay algunas diferencias clave a tener en cuenta:

1. Actualizaciones Selectivas de Parámetros

En el aprendizaje por transferencia, solo las capas no congeladas tendrán sus parámetros actualizados durante el entrenamiento. Este enfoque dirigido permite que el modelo conserve características valiosas preaprendidas mientras se adapta a la nueva tarea. Al actualizar selectivamente los parámetros, podemos:

  • Preservar características generales: Las capas iniciales de las redes neuronales a menudo capturan características universales como bordes o texturas. Al congelar estas capas, mantenemos este conocimiento general.
  • Enfocarse en el aprendizaje específico de la tarea: Las capas no congeladas, típicamente las posteriores, se ajustan para aprender características específicas de la nueva tarea.
  • Mitigar el sobreajuste: Cuando se trabaja con conjuntos de datos más pequeños, las actualizaciones selectivas pueden ayudar a prevenir que el modelo se ajuste demasiado a los nuevos datos al mantener algunas de las características robustas aprendidas del conjunto de datos original más grande.

Esta estrategia es particularmente efectiva cuando la nueva tarea es similar a la tarea original, ya que aprovecha el conocimiento existente del modelo mientras permite su adaptación. El número de capas que se deben congelar o ajustar finamente suele requerir experimentación para encontrar el equilibrio óptimo para una tarea dada.

2. Consideraciones sobre la Tasa de Aprendizaje

Al ajustar finamente modelos preentrenados, es crucial elegir cuidadosamente la tasa de aprendizaje. A menudo se recomienda una tasa de aprendizaje más baja por varias razones:

  • Preservación del conocimiento preentrenado: Una tasa de aprendizaje más baja ayuda a mantener las valiosas características aprendidas durante el preentrenamiento, permitiendo que el modelo se adapte gradualmente a la nueva tarea sin perder su conocimiento inicial.
  • Estabilidad en el entrenamiento: Las actualizaciones más pequeñas evitan cambios drásticos en los pesos del modelo, lo que conduce a un entrenamiento más estable y consistente.
  • Evitar óptimos locales: Las actualizaciones suaves permiten que el modelo explore el paisaje de la pérdida más a fondo, potencialmente encontrando mejores óptimos locales o incluso alcanzando el óptimo global.

Además, se pueden emplear técnicas como la programación de la tasa de aprendizaje para optimizar aún más el proceso de ajuste fino. Por ejemplo, se podría comenzar con una tasa de aprendizaje aún más baja y aumentarla gradualmente (warm-up), o utilizar tasas de aprendizaje cíclicas para explorar periódicamente diferentes regiones del espacio de parámetros.

Vale la pena señalar que la tasa de aprendizaje óptima puede variar según factores como la similitud entre las tareas de origen y destino, el tamaño del nuevo conjunto de datos y las capas específicas que se están ajustando finamente. Por lo tanto, a menudo es beneficioso experimentar con diferentes tasas de aprendizaje o utilizar técnicas como los "buscadores de tasa de aprendizaje" para determinar el valor más adecuado para su escenario de aprendizaje por transferencia particular.

3. Flujo de Gradientes y Aprendizaje Específico de Capa

Durante la retropropagación, los gradientes solo fluyen a través de las capas no congeladas, creando una dinámica de aprendizaje única. Este flujo selectivo de gradientes tiene varias implicaciones importantes:

  • Extracción de características fijas: Las capas congeladas, típicamente las iniciales, actúan como extractores de características estáticos. Estas capas, preentrenadas en grandes conjuntos de datos, ya han aprendido a reconocer características generales de bajo nivel como bordes, texturas y formas básicas. Al mantener estas capas congeladas, aprovechamos este conocimiento preexistente sin modificarlo.
  • Aprendizaje adaptable en capas no congeladas: Las capas no congeladas, generalmente las más profundas en la red, reciben y procesan los gradientes. Estas capas aprenden a interpretar y adaptar las características fijas extraídas por las capas congeladas, adaptándolas a los requisitos específicos de la nueva tarea.
  • Aprendizaje eficiente por transferencia: Este enfoque permite que el modelo transfiera eficientemente el conocimiento de la tarea original a la nueva. Preserva las características generalizadas valiosas aprendidas del conjunto de datos original grande, mientras que enfoca el proceso de aprendizaje en adaptaciones específicas de la tarea.
  • Reducción del riesgo de sobreajuste: Al limitar las actualizaciones de parámetros a solo un subconjunto de capas, reducimos el riesgo de sobreajuste, especialmente cuando se trabaja con conjuntos de datos más pequeños para la nueva tarea. Esto es particularmente beneficioso cuando la nueva tarea es similar a la original pero tiene datos de entrenamiento limitados.

Esta estrategia de flujo selectivo de gradientes permite un equilibrio fino entre preservar el conocimiento general y adaptarse a nuevas tareas específicas, lo que hace que el aprendizaje por transferencia sea una técnica poderosa en escenarios con datos o recursos computacionales limitados.

4. Preprocesamiento de Datos y Aumento

Cuando se trabaja con modelos preentrenados, es crucial preprocesar los datos de entrada de una manera coherente con los datos de entrenamiento originales del modelo. Esto garantiza que los nuevos datos estén en un formato que el modelo pueda interpretar eficazmente. El preprocesamiento típicamente incluye:

  • Redimensionamiento de imágenes: La mayoría de los modelos preentrenados esperan imágenes de entrada de un tamaño específico (por ejemplo, 224x224 píxeles para muchas arquitecturas populares). Cambiar el tamaño garantiza que todas las imágenes coincidan con esta dimensión de entrada esperada.
  • Normalización: Esto implica ajustar los valores de los píxeles a una escala estándar, a menudo utilizando la media y la desviación estándar del conjunto de datos de entrenamiento original (por ejemplo, estadísticas de ImageNet para muchos modelos).
  • Aumento de datos: Esta técnica expande artificialmente el conjunto de datos de entrenamiento aplicando varias transformaciones a las imágenes existentes. Aumentaciones comunes incluyen:
    • Recorte y volteo aleatorio: Ayuda al modelo a aprender invarianza a la posición y orientación.
    • Variación de color: Ajusta el brillo, el contraste y la saturación para mejorar la robustez ante condiciones de iluminación.
    • Rotación y escalado: Mejora la capacidad del modelo para reconocer objetos en diferentes ángulos y tamaños.

El preprocesamiento y el aumento adecuados no solo garantizan la compatibilidad con el modelo preentrenado, sino que también pueden mejorar significativamente la capacidad de generalización del modelo y su rendimiento en la nueva tarea.

5. Monitoreo del Rendimiento y Parada Temprana

El monitoreo vigilante del rendimiento del modelo en los conjuntos de datos de entrenamiento y validación es esencial en el aprendizaje por transferencia. A diferencia de los modelos entrenados desde cero, los modelos de aprendizaje por transferencia a menudo muestran una rápida convergencia debido a su conocimiento preexistente. Este proceso de aprendizaje acelerado requiere una observación cuidadosa para evitar el sobreajuste. Implementar técnicas de parada temprana se vuelve crucial en este contexto.

La parada temprana implica detener el proceso de entrenamiento cuando el rendimiento del modelo en el conjunto de validación comienza a deteriorarse, incluso cuando sigue mejorando en el conjunto de entrenamiento. Esta divergencia en el rendimiento es un claro indicador de sobreajuste, donde el modelo comienza a memorizar los datos de entrenamiento en lugar de aprender patrones generalizables.

Para implementar un monitoreo de rendimiento efectivo y parada temprana:

  • Evalúe regularmente el modelo en un conjunto de validación reservado durante el entrenamiento.
  • Rastree métricas clave como precisión, pérdida y, potencialmente, medidas específicas de la tarea (por ejemplo, F1-score para tareas de clasificación).
  • Implemente mecanismos de paciencia, donde el entrenamiento continúa durante un número determinado de épocas incluso después de detectar un posible punto de sobreajuste, para asegurarse de que no sea una fluctuación temporal.
  • Considere usar técnicas como el guardado de puntos de control del modelo para guardar el estado del modelo que mejor funcione, lo que le permite volver a este punto óptimo después del entrenamiento.

Al emplear estas estrategias, puede aprovechar las capacidades de aprendizaje acelerado del aprendizaje por transferencia, mientras se protege contra el sobreajuste, lo que finalmente produce un modelo que generaliza bien en datos no vistos.

Al tener en cuenta estos factores, puede aprovechar eficazmente el aprendizaje por transferencia para lograr un rendimiento superior en nuevas tareas, especialmente cuando se trabaja con conjuntos de datos o recursos computacionales limitados.

Ejemplo: Entrenando un ResNet-18 Preentrenado en un Nuevo Conjunto de Datos

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

# Check if CUDA is available
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

# Define transformations for the new dataset
transform = transforms.Compose([
    transforms.Resize(224),  # ResNet requires 224x224 images
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Load the new dataset (CIFAR-10)
train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
test_dataset = datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)

# Load pre-trained ResNet18 model
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

# Modify the final layer for CIFAR-10 (10 classes)
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, 10)

# Move model to the appropriate device
model = model.to(device)

# Define the loss function and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)

# Training loop
epochs = 10
for epoch in range(epochs):
    model.train()
    running_loss = 0.0
    for i, (images, labels) in enumerate(train_loader):
        images, labels = images.to(device), labels.to(device)
        
        optimizer.zero_grad()   # Zero the parameter gradients
        outputs = model(images)  # Forward pass
        loss = criterion(outputs, labels)  # Compute the loss
        loss.backward()  # Backward pass (compute gradients)
        optimizer.step()  # Optimization step (update parameters)

        running_loss += loss.item()
        
        if i % 100 == 99:    # Print every 100 mini-batches
            print(f'[{epoch + 1}, {i + 1:5d}] loss: {running_loss / 100:.3f}')
            running_loss = 0.0

    # Validation
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for images, labels in test_loader:
            images, labels = images.to(device), labels.to(device)
            outputs = model(images)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()

    print(f'Accuracy on test set: {100 * correct / total:.2f}%')

print('Finished Training')

# Save the model
torch.save(model.state_dict(), 'cifar10_resnet18.pth')

Este ejemplo de código muestra un método para el ajuste fino de un modelo ResNet18 preentrenado en el conjunto de datos CIFAR-10 utilizando PyTorch.

Analicemos los componentes clave y expliquemos sus propósitos:

  1. Importaciones y Configuración del Dispositivo:
    • Importamos los módulos necesarios de PyTorch y torchvision.
    • Verificamos la disponibilidad de CUDA para utilizar la aceleración por GPU si es posible.
  2. Preprocesamiento de Datos:
    • Definimos un proceso de transformación que redimensiona las imágenes a 224x224 (requerido por ResNet), las convierte en tensores y las normaliza utilizando las estadísticas de ImageNet.
    • Los conjuntos de datos de entrenamiento y prueba se cargan utilizando el conjunto de datos CIFAR-10 de torchvision.
  3. Cargadores de Datos:
    • Creamos objetos DataLoader tanto para los conjuntos de entrenamiento como de prueba, que manejan el procesamiento por lotes y la aleatorización de datos.
  4. Preparación del Modelo:
    • Cargamos un modelo ResNet18 preentrenado usando models.resnet18(weights=models.ResNet18_Weights.DEFAULT).
    • La capa totalmente conectada final se modifica para producir 10 clases (para CIFAR-10) en lugar de las 1000 originales (para ImageNet).
    • El modelo se transfiere al dispositivo apropiado (GPU si está disponible).
  5. Función de Pérdida y Optimizador:
    • Se utiliza la Pérdida de Entropía Cruzada como función de pérdida, que es adecuada para clasificación multiclase.
    • Se utiliza el optimizador SGD con una tasa de aprendizaje de 0.001 y momento de 0.9.
  6. Bucle de Entrenamiento:
    • El modelo se entrena durante 10 épocas.
    • En cada época, iteramos a través de los datos de entrenamiento, calculamos la pérdida, realizamos la retropropagación y actualizamos los parámetros del modelo.
    • El progreso del entrenamiento se imprime cada 100 lotes.
  7. Validación:
    • Después de cada época, el modelo se evalúa en el conjunto de prueba para medir su precisión.
    • Esto ayuda a monitorear el rendimiento del modelo y detectar el sobreajuste.
  8. Guardado del Modelo:
    • Después del entrenamiento, el diccionario de estado del modelo se guarda en un archivo para uso posterior.

Este ejemplo muestra el proceso completo de ajuste fino de un modelo preentrenado, desde la preparación de datos hasta la evaluación y guardado del modelo. Demuestra las mejores prácticas como el uso de aceleración por GPU, preprocesamiento adecuado de datos y evaluación regular del rendimiento durante el entrenamiento.

4.3.5 Evaluación del Modelo Ajustado Finamente

Después de la fase de entrenamiento, es crucial evaluar el rendimiento del modelo en un conjunto de datos de prueba separado. Este proceso de evaluación cumple varios propósitos:

  • Proporciona una estimación imparcial de la capacidad del modelo para generalizar a datos no vistos.
  • Ayuda a detectar posibles problemas de sobreajuste que podrían haber ocurrido durante el entrenamiento.
  • Permite la comparación con otros modelos o versiones anteriores del mismo modelo.

Al evaluar el modelo en un conjunto de prueba, podemos medir qué tan bien se desempeña nuestro modelo ajustado finamente en datos que no ha encontrado durante el proceso de entrenamiento, brindándonos información valiosa sobre su aplicabilidad en el mundo real.

Ejemplo: Evaluación del Modelo Ajustado Finamente

import torch
import torchvision
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
import numpy as np

# Define the device
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

# Define transformations for the test dataset
transform = transforms.Compose([
    transforms.Resize(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Load the test dataset (CIFAR-10 test set)
test_dataset = datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)

# Load the model (assuming it's already trained and saved)
model = torchvision.models.resnet18(weights=None)
num_ftrs = model.fc.in_features
model.fc = torch.nn.Linear(num_ftrs, 10)  # 10 classes for CIFAR-10
model.load_state_dict(torch.load('cifar10_resnet18.pth'))
model = model.to(device)

# Switch model to evaluation mode
model.eval()

# Disable gradient computation for evaluation
correct = 0
total = 0
class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))

with torch.no_grad():
    for images, labels in test_loader:
        images, labels = images.to(device), labels.to(device)
        outputs = model(images)
        _, predicted = torch.max(outputs, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()
        
        c = (predicted == labels).squeeze()
        for i in range(len(labels)):
            label = labels[i]
            class_correct[label] += c[i].item()
            class_total[label] += 1

# Calculate overall accuracy
accuracy = 100 * correct / total
print(f'Overall Accuracy on test set: {accuracy:.2f}%')

# Calculate and print per-class accuracy
classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
for i in range(10):
    print(f'Accuracy of {classes[i]}: {100 * class_correct[i] / class_total[i]:.2f}%')

# Visualize some predictions
def imshow(img):
    img = img / 2 + 0.5     # unnormalize
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))
    plt.axis('off')

# Get some random test images
dataiter = iter(test_loader)
images, labels = next(dataiter)

# Make predictions
outputs = model(images.to(device))
_, predicted = torch.max(outputs, 1)

# Show images and their predicted labels
fig = plt.figure(figsize=(12, 48))
for i in range(4):
    ax = fig.add_subplot(1, 4, i+1)
    imshow(images[i])
    ax.set_title(f'Predicted: {classes[predicted[i]]}\nActual: {classes[labels[i]]}')

plt.tight_layout()
plt.show()

Este ejemplo de código proporciona una evaluación integral del modelo ajustado finamente. Analicemos sus componentes:

  1. Importaciones y Configuración del Dispositivo:
    • Importamos los módulos necesarios de PyTorch y torchvision.
    • Configuramos el dispositivo (CPU o GPU) para el cómputo.
  2. Preprocesamiento de Datos:
    • Definimos el mismo proceso de transformación utilizado durante el entrenamiento.
    • Cargamos el conjunto de prueba CIFAR-10 y creamos un DataLoader.
  3. Carga del Modelo:
    • Recreamos la arquitectura del modelo (ResNet18 con la capa final modificada).
    • Cargamos los pesos guardados del modelo desde 'cifar10_resnet18.pth'.
    • Trasladamos el modelo al dispositivo apropiado (CPU o GPU).
  4. Bucle de Evaluación:
    • Cambiamos el modelo a modo evaluación usando model.eval().
    • Desactivamos el cálculo de gradientes usando torch.no_grad() para ahorrar memoria y acelerar el cómputo.
    • Iteramos a través de los datos de prueba, realizando predicciones y comparándolas con las etiquetas verdaderas.
    • Llevamos un registro de las predicciones correctas totales y por clase.
  5. Cálculo y Reporte de Resultados:
    • Calculamos e imprimimos la precisión general en el conjunto de prueba.
    • Calculamos e imprimimos las precisiones por clase, lo que nos da una idea de en qué clases el modelo funciona bien y en cuáles tiene dificultades.
  6. Visualización:
    • Definimos una función imshow() para mostrar imágenes.
    • Obtenemos un lote de imágenes de prueba y realizamos predicciones sobre ellas.
    • Visualizamos 4 imágenes de prueba aleatorias junto con sus etiquetas predichas y reales.

Esta evaluación integral proporciona varios beneficios:

  • Nos da la precisión general, que es una medida global del rendimiento del modelo.
  • Proporciona precisiones por clase, permitiéndonos identificar si el modelo tiene sesgos a favor o en contra de ciertas clases.
  • La visualización de las predicciones nos ayuda a evaluar cualitativamente el rendimiento del modelo y potencialmente identificar patrones en sus errores.

Este enfoque de evaluación del modelo nos proporciona una comprensión mucho más detallada de las fortalezas y debilidades de nuestro modelo, lo cual es crucial para mejoras posteriores y para evaluar su idoneidad para su implementación en aplicaciones del mundo real.

4.3 Aprendizaje por Transferencia y Ajuste Fino de Modelos Preentrenados en PyTorch

En muchas aplicaciones del mundo real, entrenar un modelo de aprendizaje profundo desde cero presenta desafíos significativos, como la escasez de grandes conjuntos de datos etiquetados y los considerables recursos computacionales necesarios para entrenar modelos complejos con millones de parámetros. El aprendizaje por transferencia ofrece una solución elegante a estos desafíos al aprovechar el conocimiento de modelos preexistentes.

Este enfoque consiste en tomar un modelo que ha sido preentrenado en un gran conjunto de datos general (como ImageNet, que contiene millones de imágenes etiquetadas en miles de categorías) y adaptarlo a una nueva tarea, a menudo más específica. La idea clave es que las características aprendidas por el modelo en la tarea original suelen ser lo suficientemente generales como para ser útiles en otras tareas relacionadas.

El aprendizaje por transferencia es particularmente poderoso en dominios como visión por computadora, procesamiento del lenguaje natural y reconocimiento de voz. Por ejemplo, un modelo entrenado en ImageNet puede adaptarse para tareas específicas como identificar especies de plantas o detectar condiciones médicas en radiografías, a menudo con menos datos específicos de la tarea de los que serían necesarios para entrenar desde cero.

Al implementar el aprendizaje por transferencia en PyTorch, los investigadores y profesionales generalmente emplean una de dos estrategias principales:

  1. Extracción de características: En este enfoque, el modelo preentrenado se usa como un extractor de características fijo. Los pesos de la mayoría de la red (generalmente todas las capas excepto la última) se congelan, lo que significa que no se actualizarán durante el entrenamiento. Solo la capa final, a menudo llamada capa de clasificación, se reemplaza con una nueva capa apropiada para la nueva tarea y se entrena con el nuevo conjunto de datos. Este método es particularmente útil cuando la nueva tarea es similar a la tarea original y cuando los recursos computacionales o los datos específicos de la tarea son limitados.
  2. Ajuste fino: Este enfoque más flexible implica descongelar algunas o todas las capas del modelo preentrenado y continuar entrenándolas con el nuevo conjunto de datos. El ajuste fino permite que el modelo adapte sus características aprendidas a las especificidades de la nueva tarea. Este método puede conducir a un mejor rendimiento, especialmente cuando la nueva tarea es significativamente diferente de la original o cuando hay una cantidad sustancial de datos específicos de la tarea disponibles. Sin embargo, requiere una gestión cuidadosa de las tasas de aprendizaje y la regularización para evitar el sobreajuste o el olvido catastrófico de las características originalmente aprendidas.

La elección entre la extracción de características y el ajuste fino a menudo depende de factores como el tamaño y la similitud del nuevo conjunto de datos con el conjunto de datos original, la complejidad de la nueva tarea y los recursos computacionales disponibles. En la práctica, es común comenzar con la extracción de características y avanzar gradualmente hacia el ajuste fino según sea necesario para optimizar el rendimiento.

4.3.1 Modelos Preentrenados en PyTorch

PyTorch ofrece una extensa colección de modelos preentrenados a través del módulo torchvision.models, lo que simplifica significativamente el proceso de aprendizaje por transferencia. Estos modelos, que incluyen arquitecturas populares como ResNet, VGG e Inception, han sido entrenados en el vasto conjunto de datos ImageNet. Este conjunto de datos comprende más de 1.2 millones de imágenes en 1,000 categorías de objetos diversas, lo que permite que estos modelos aprendan características ricas y generalizables.

La disponibilidad de estos modelos preentrenados presenta varias ventajas:

1. Prototipado rápido

Los modelos preentrenados en PyTorch permiten una experimentación rápida con arquitecturas de vanguardia, reduciendo significativamente el tiempo y los recursos típicamente necesarios para el desarrollo de modelos. Esta ventaja permite a los investigadores y desarrolladores:

  • Probar rápidamente hipótesis e ideas utilizando arquitecturas de modelos establecidas.
  • Iterar rápidamente sobre diferentes configuraciones de modelos sin la necesidad de ciclos de entrenamiento extensos.
  • Explorar la efectividad de varias arquitecturas en tareas o conjuntos de datos específicos.
  • Acelerar el proceso de desarrollo aprovechando características preaprendidas.
  • Enfocarse más en la resolución de problemas y menos en las complejidades de la implementación del modelo.

Esta capacidad es particularmente valiosa en campos donde el tiempo de comercialización o los plazos de investigación son críticos, permitiendo una innovación y descubrimiento más rápidos en las aplicaciones de aprendizaje automático.

2. Eficiencia en el aprendizaje por transferencia

Estos modelos preentrenados sirven como puntos de partida excelentes para tareas de aprendizaje por transferencia, reduciendo significativamente el tiempo y los recursos necesarios para el entrenamiento. Al aprovechar las ricas características aprendidas de conjuntos de datos a gran escala como ImageNet, estos modelos pueden ajustarse con eficacia en conjuntos de datos más pequeños y específicos de dominio. Este enfoque es particularmente valioso en escenarios donde los datos etiquetados son escasos o costosos de obtener, como en imágenes médicas o aplicaciones industriales especializadas.

La eficiencia del aprendizaje por transferencia con estos modelos preentrenados se debe a varios factores:

  • Reutilización de características: Las capas inferiores de estos modelos a menudo capturan características genéricas (como bordes, texturas y formas) que son aplicables en una amplia gama de tareas visuales.
  • Reducción del tiempo de entrenamiento: El ajuste fino de un modelo preentrenado generalmente requiere menos épocas para converger en comparación con entrenar desde cero, lo que lleva a ahorros significativos de tiempo.
  • Mejora en la generalización: El conocimiento diverso codificado en los modelos preentrenados a menudo ayuda a lograr una mejor generalización en nuevas tareas, incluso con datos específicos de dominio limitados.
  • Requisitos computacionales más bajos: El ajuste fino generalmente requiere menos poder computacional que entrenar un modelo complejo desde cero, lo que lo hace más accesible para investigadores y desarrolladores con recursos limitados.

Esta eficiencia en el aprendizaje por transferencia ha democratizado el acceso a técnicas de aprendizaje automático de vanguardia, permitiendo el prototipado rápido y el despliegue de modelos sofisticados en diversos dominios y aplicaciones.

3. Comparaciones de referencia

Los modelos preentrenados sirven como puntos de referencia invaluables para evaluar arquitecturas personalizadas. Ofrecen varias ventajas en este sentido:

  • Métricas de rendimiento estandarizadas: Los investigadores pueden comparar sus enfoques novedosos contra puntos de referencia reconocidos, asegurando una evaluación justa y consistente.
  • Información a través de arquitecturas cruzadas: Al comparar con varios modelos preentrenados, los desarrolladores pueden obtener una comprensión más profunda de las fortalezas y debilidades de su modelo personalizado en diferentes diseños arquitectónicos.
  • Eficiencia en tiempo y recursos: Usar modelos preentrenados como referencia elimina la necesidad de entrenar múltiples modelos complejos desde cero, reduciendo significativamente los recursos computacionales y el tiempo necesarios para comparaciones exhaustivas.
  • Rendimiento estándar de la industria: Los modelos preentrenados a menudo representan el rendimiento de vanguardia en conjuntos de datos a gran escala, proporcionando un alto estándar para que los modelos personalizados se esfuercen por igualar o superar.

Esta capacidad de referencia es crucial para avanzar en el campo del aprendizaje automático, ya que permite a los investigadores y profesionales cuantificar mejoras e identificar áreas para más innovaciones en el diseño y las técnicas de entrenamiento del modelo.

Para utilizar estos modelos preentrenados, simplemente puedes importarlos desde torchvision.models y especificar el parámetro pretrained=True. Esto carga la arquitectura del modelo junto con sus pesos preentrenados, listos para ser usados de inmediato o ajustados para tu tarea específica.

Ejemplo: Cargar un Modelo Preentrenado

import torch
import torchvision.models as models
from torchvision import transforms
from PIL import Image
import matplotlib.pyplot as plt

# Load a pretrained ResNet-18 model (compatible with latest torchvision versions)
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

# Print the model architecture
print(model)

# Set the model to evaluation mode
model.eval()

# Define image transformations
transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Load and preprocess an image
img_path = 'path_to_your_image.jpg'  # Ensure this path is correct
img = Image.open(img_path)
img_tensor = transform(img).unsqueeze(0)  # Add batch dimension

# Make a prediction
with torch.no_grad():
    output = model(img_tensor)

# Get the predicted class
_, predicted_idx = torch.max(output, 1)

# Load ImageNet class labels from Torchvision
labels = models.ResNet18_Weights.DEFAULT.meta["categories"]

# Print the predicted class
print(f"Predicted class: {labels[predicted_idx]}")

# Visualize the image
plt.imshow(img)
plt.axis('off')
plt.title(f"Predicted: {labels[predicted_idx]}")
plt.show()

Este ejemplo muestra cómo utilizar un modelo ResNet-18 preentrenado para la clasificación de imágenes en PyTorch.

  1. Importaciones: Las bibliotecas necesarias son torch para PyTorch, torchvision.models para modelos preentrenados, torchvision.transforms para preprocesamiento de imágenes, PIL para el manejo de imágenes y matplotlib.pyplot para visualización.
  2. Cargar el Modelo: El modelo se carga usando models.resnet18(weights=models.ResNet18_Weights.DEFAULT), asegurando la compatibilidad con las últimas versiones de PyTorch. El modelo se establece en modo evaluación usando model.eval().
  3. Preprocesamiento de Imagen: La imagen se redimensiona a 256x256, se recorta del centro a 224x224, se convierte a tensor y se normaliza usando la media y desviación estándar de ImageNet.
  4. Cargar y Procesar Imagen: La imagen se carga usando Image.open(), se transforma y se redimensiona con .unsqueeze(0) para cumplir con los requisitos de entrada del modelo.
  5. Realizar una Predicción: La imagen procesada se pasa a través del modelo dentro de torch.no_grad() para desactivar el seguimiento de gradientes. El índice de clase con la mayor probabilidad se obtiene usando torch.max().
  6. Interpretar los Resultados: El índice de clase predicho se mapea a su etiqueta usando models.ResNet18_Weights.DEFAULT.meta["categories"].
  7. Visualización: La imagen se muestra con matplotlib.pyplot, y la clase predicha se muestra en el título.

Este proceso simple carga un modelo preentrenado, procesa una imagen, realiza una predicción y visualiza el resultado.

4.3.2 Extracción de Características con Modelos Preentrenados

En el enfoque de extracción de características, aprovechamos el poder de los modelos preentrenados tratándolos como sofisticados extractores de características. Este método implica congelar los pesos de las capas convolucionales del modelo preentrenado, que ya han aprendido a reconocer una amplia gama de características visuales a partir de grandes conjuntos de datos como ImageNet. Al mantener estas capas fijas, preservamos su capacidad para extraer características significativas de las imágenes, independientemente de la tarea específica.

La modificación clave en este enfoque es reemplazar la capa final completamente conectada (FC) del modelo preentrenado con una nueva capa adaptada a nuestra tarea específica. Esta nueva capa FC se convierte en la única parte entrenable de la red, actuando como un clasificador que aprende a mapear las características extraídas a las clases de salida deseadas para nuestra nueva tarea. Esta estrategia es particularmente efectiva cuando:

  • La nueva tarea es similar a la tarea original para la que fue entrenado el modelo.
  • El conjunto de datos disponible para la nueva tarea es relativamente pequeño.
  • Los recursos computacionales son limitados.
  • Se necesita un prototipado rápido o experimentación.

Al utilizar la extracción de características, podemos reducir significativamente el tiempo de entrenamiento y los requisitos de recursos, al tiempo que aprovechamos las ricas representaciones de características aprendidas por modelos de vanguardia. Este enfoque permite una rápida adaptación a nuevas tareas y dominios, lo que lo convierte en una técnica valiosa en el aprendizaje por transferencia.

Ejemplo: Usar un ResNet Preentrenado para la Extracción de Características

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

# Load a pretrained ResNet-18 model (compatible with latest torchvision versions)
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

# Freeze all layers in the model (i.e., prevent backpropagation through these layers)
for param in model.parameters():
    param.requires_grad = False

# Replace the final fully connected layer to match the number of classes in the new dataset
# ResNet's final layer (fc) originally outputs 1000 classes, we change it to 10 for CIFAR-10
model.fc = nn.Linear(in_features=model.fc.in_features, out_features=10)

# Print the modified model
print(model)

# Define transformations for the CIFAR-10 dataset
transform = transforms.Compose([
    transforms.Resize(224),  # ResNet expects 224x224 input
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Load CIFAR-10 dataset
train_dataset = CIFAR10(root='./data', train=True, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

# Define loss function and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.fc.parameters(), lr=0.001)

# Training loop
num_epochs = 5
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)

for epoch in range(num_epochs):
    model.train()
    running_loss = 0.0
    for i, (images, labels) in enumerate(train_loader):
        images, labels = images.to(device), labels.to(device)
        
        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        
        running_loss += loss.item()
        
        if (i + 1) % 100 == 0:
            print(f'Epoch [{epoch+1}/{num_epochs}], Step [{i+1}/{len(train_loader)}], Loss: {running_loss/100:.4f}')
            running_loss = 0.0

print("Training completed!")

# Save the fine-tuned model
torch.save(model.state_dict(), 'resnet18_cifar10.pth')

Este ejemplo ajusta finamente un modelo ResNet-18 preentrenado en el conjunto de datos CIFAR-10 usando PyTorch.

  1. Importaciones: Las bibliotecas necesarias incluyen torch para PyTorch, torch.nn para redes neuronales, torchvision.models para modelos preentrenados, torchvision.transforms para preprocesamiento, y torch.utils.data.DataLoader para el manejo de conjuntos de datos.
  2. Cargar el Modelo Preentrenado: El modelo se carga usando models.resnet18(weights=models.ResNet18_Weights.DEFAULT), asegurando la compatibilidad con las versiones más recientes de PyTorch.
  3. Congelar Capas Preentrenadas: Todas las capas excepto la capa totalmente conectada final se congelan usando param.requires_grad = False, evitando actualizaciones innecesarias durante el entrenamiento.
  4. Modificar la Capa Final: La última capa totalmente conectada (fc) se reemplaza para generar 10 clases en lugar de 1000, haciéndola adecuada para CIFAR-10.
  5. Preprocesamiento de Imagen: El conjunto de datos se redimensiona a 224x224, se convierte a tensor y se normaliza usando la media y desviación estándar de ImageNet.
  6. Cargar el Conjunto de Datos CIFAR-10: El conjunto de datos se descarga y se carga en un DataLoader con un tamaño de lote de 32.
  7. Definir Pérdida y Optimizador: La función de pérdida es CrossEntropyLoss, y el optimizador es Adam, actualizando solo la nueva capa fc.
  8. Bucle de Entrenamiento: El modelo se entrena durante 5 épocas, iterando a través de mini-lotes, calculando la pérdida y actualizando los pesos.
  9. Guardar el Modelo: El modelo ajustado se guarda usando torch.save(model.state_dict(), 'resnet18_cifar10.pth') para uso futuro.

Este ejemplo completo muestra todo el proceso de aprendizaje por transferencia, desde la carga de un modelo preentrenado hasta su ajuste fino en un nuevo conjunto de datos y el guardado de los resultados. Es una demostración práctica de cómo aprovechar los modelos preentrenados para nuevas tareas con un entrenamiento mínimo.

4.3.3 Ajuste Fino de un Modelo Preentrenado

En el ajuste fino, permitimos que algunas o todas las capas del modelo preentrenado se actualicen durante el entrenamiento. Este enfoque ofrece un equilibrio entre aprovechar las características previamente aprendidas y adaptar el modelo a una nueva tarea. Generalmente, se congelan las capas iniciales (que capturan características genéricas como bordes y texturas) y se ajustan las capas más profundas (que capturan características más específicas de la tarea).

La lógica detrás de esta estrategia se basa en la naturaleza jerárquica de las redes neuronales. Las capas iniciales tienden a aprender características generales de bajo nivel que son aplicables a una amplia gama de tareas, mientras que las capas más profundas aprenden características más especializadas y de alto nivel que son más específicas de la tarea. Al congelar las capas iniciales, preservamos las valiosas características genéricas aprendidas del gran conjunto de datos en el que el modelo fue entrenado originalmente. Esto es especialmente útil cuando nuestra nueva tarea tiene datos de entrenamiento limitados.

Ajustar finamente las capas más profundas permite que el modelo adapte estas características de alto nivel a las particularidades de la nueva tarea. Este proceso puede mejorar significativamente el rendimiento en comparación con usar el modelo preentrenado tal como está o entrenar un modelo nuevo desde cero, especialmente cuando se trabaja con conjuntos de datos limitados o cuando la nueva tarea es similar a la tarea original para la que fue entrenado el modelo.

El número exacto de capas a congelar o ajustar finamente suele determinarse de manera empírica y puede variar dependiendo de factores como la similitud entre las tareas original y nueva, el tamaño del nuevo conjunto de datos y los recursos computacionales disponibles. En la práctica, es común experimentar con diferentes configuraciones para encontrar el equilibrio óptimo para una tarea determinada.

Ejemplo: Ajuste Fino de las Últimas Capas de un ResNet Preentrenado

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

# Load a pretrained ResNet-18 model
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

# Freeze the first few layers
for name, param in model.named_parameters():
    if 'layer4' not in name and 'fc' not in name:  # Only allow parameters in 'layer4' and 'fc' to be updated
        param.requires_grad = False

# Replace the final fully connected layer
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, 10)  # 10 is the number of classes in CIFAR-10

# Print the modified model with some layers frozen
print(model)

# Define transformations for the CIFAR-10 dataset
transform = transforms.Compose([
    transforms.Resize(224),  # ResNet expects 224x224 input
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Load CIFAR-10 dataset
train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

# Define loss function and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(filter(lambda p: p.requires_grad, model.parameters()), lr=0.001, momentum=0.9)

# Training loop
num_epochs = 5
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)

for epoch in range(num_epochs):
    model.train()
    running_loss = 0.0
    for i, (images, labels) in enumerate(train_loader):
        images, labels = images.to(device), labels.to(device)
        
        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        
        running_loss += loss.item()
        
        if (i + 1) % 100 == 0:
            print(f'Epoch [{epoch+1}/{num_epochs}], Step [{i+1}/{len(train_loader)}], Loss: {running_loss/100:.4f}')
            running_loss = 0.0

print("Fine-tuning completed!")

# Save the fine-tuned model
torch.save(model.state_dict(), 'resnet18_cifar10_finetuned.pth')

Este ejemplo demuestra un enfoque integral para el ajuste fino de un modelo ResNet-18 preentrenado en el conjunto de datos CIFAR-10. Analicémoslo:

  1. Importaciones y Carga del Modelo:
    • Importamos los módulos necesarios de PyTorch y torchvision.
    • Se carga un modelo ResNet-18 preentrenado usando models.resnet18(weights=models.ResNet18_Weights.DEFAULT).
  2. Congelación de Capas:
    • Iteramos a través de los parámetros nombrados del modelo y congelamos todas las capas excepto 'layer4' y 'fc'.
    • Esto se logra estableciendo param.requires_grad = False para las capas que queremos congelar.
  3. Modificación de la Capa Final:
    • La capa totalmente conectada final (fc) se reemplaza por una nueva que genera 10 clases (para CIFAR-10) en lugar de las 1000 originales (para ImageNet).
    • Utilizamos model.fc.in_features para mantener el tamaño de entrada correcto para la nueva capa.
  4. Preparación de Datos:
    • Definimos transformaciones para preprocesar las imágenes CIFAR-10, incluyendo el redimensionamiento a 224x224 (requerido por ResNet), la conversión a tensor y la normalización.
    • Se carga el conjunto de datos CIFAR-10 y se crea un DataLoader para el procesamiento por lotes.
  5. Configuración del Entrenamiento:
    • Se utiliza Cross Entropy Loss como función de pérdida.
    • Se utiliza el optimizador SGD para actualizar solo los parámetros de las capas no congeladas (layer4 y fc).
    • El modelo se traslada a GPU si está disponible.
  6. Bucle de Entrenamiento:
    • El modelo se ajusta finamente durante un número específico de épocas.
    • En cada época, iteramos a través de los datos de entrenamiento, calculamos la pérdida, realizamos la retropropagación y actualizamos las capas no congeladas del modelo.
    • El progreso del entrenamiento se imprime cada 100 pasos.
  7. Guardado del Modelo:
    • Después del ajuste fino, el diccionario de estado del modelo se guarda en un archivo.

Este ejemplo completo muestra todo el proceso de ajuste fino de un modelo preentrenado, desde la carga y modificación del modelo hasta su entrenamiento en un nuevo conjunto de datos y el guardado de los resultados. Demuestra cómo aprovechar el aprendizaje por transferencia manteniendo el conocimiento en las capas iniciales mientras se adaptan las capas posteriores a una nueva tarea.

4.3.4 Entrenando el Modelo con Aprendizaje por Transferencia

Una vez que el modelo se modifica para el aprendizaje por transferencia (ya sea extracción de características o ajuste fino), el proceso de entrenamiento sigue una estructura similar al entrenamiento de un modelo desde cero. Sin embargo, hay algunas diferencias clave a tener en cuenta:

1. Actualizaciones Selectivas de Parámetros

En el aprendizaje por transferencia, solo las capas no congeladas tendrán sus parámetros actualizados durante el entrenamiento. Este enfoque dirigido permite que el modelo conserve características valiosas preaprendidas mientras se adapta a la nueva tarea. Al actualizar selectivamente los parámetros, podemos:

  • Preservar características generales: Las capas iniciales de las redes neuronales a menudo capturan características universales como bordes o texturas. Al congelar estas capas, mantenemos este conocimiento general.
  • Enfocarse en el aprendizaje específico de la tarea: Las capas no congeladas, típicamente las posteriores, se ajustan para aprender características específicas de la nueva tarea.
  • Mitigar el sobreajuste: Cuando se trabaja con conjuntos de datos más pequeños, las actualizaciones selectivas pueden ayudar a prevenir que el modelo se ajuste demasiado a los nuevos datos al mantener algunas de las características robustas aprendidas del conjunto de datos original más grande.

Esta estrategia es particularmente efectiva cuando la nueva tarea es similar a la tarea original, ya que aprovecha el conocimiento existente del modelo mientras permite su adaptación. El número de capas que se deben congelar o ajustar finamente suele requerir experimentación para encontrar el equilibrio óptimo para una tarea dada.

2. Consideraciones sobre la Tasa de Aprendizaje

Al ajustar finamente modelos preentrenados, es crucial elegir cuidadosamente la tasa de aprendizaje. A menudo se recomienda una tasa de aprendizaje más baja por varias razones:

  • Preservación del conocimiento preentrenado: Una tasa de aprendizaje más baja ayuda a mantener las valiosas características aprendidas durante el preentrenamiento, permitiendo que el modelo se adapte gradualmente a la nueva tarea sin perder su conocimiento inicial.
  • Estabilidad en el entrenamiento: Las actualizaciones más pequeñas evitan cambios drásticos en los pesos del modelo, lo que conduce a un entrenamiento más estable y consistente.
  • Evitar óptimos locales: Las actualizaciones suaves permiten que el modelo explore el paisaje de la pérdida más a fondo, potencialmente encontrando mejores óptimos locales o incluso alcanzando el óptimo global.

Además, se pueden emplear técnicas como la programación de la tasa de aprendizaje para optimizar aún más el proceso de ajuste fino. Por ejemplo, se podría comenzar con una tasa de aprendizaje aún más baja y aumentarla gradualmente (warm-up), o utilizar tasas de aprendizaje cíclicas para explorar periódicamente diferentes regiones del espacio de parámetros.

Vale la pena señalar que la tasa de aprendizaje óptima puede variar según factores como la similitud entre las tareas de origen y destino, el tamaño del nuevo conjunto de datos y las capas específicas que se están ajustando finamente. Por lo tanto, a menudo es beneficioso experimentar con diferentes tasas de aprendizaje o utilizar técnicas como los "buscadores de tasa de aprendizaje" para determinar el valor más adecuado para su escenario de aprendizaje por transferencia particular.

3. Flujo de Gradientes y Aprendizaje Específico de Capa

Durante la retropropagación, los gradientes solo fluyen a través de las capas no congeladas, creando una dinámica de aprendizaje única. Este flujo selectivo de gradientes tiene varias implicaciones importantes:

  • Extracción de características fijas: Las capas congeladas, típicamente las iniciales, actúan como extractores de características estáticos. Estas capas, preentrenadas en grandes conjuntos de datos, ya han aprendido a reconocer características generales de bajo nivel como bordes, texturas y formas básicas. Al mantener estas capas congeladas, aprovechamos este conocimiento preexistente sin modificarlo.
  • Aprendizaje adaptable en capas no congeladas: Las capas no congeladas, generalmente las más profundas en la red, reciben y procesan los gradientes. Estas capas aprenden a interpretar y adaptar las características fijas extraídas por las capas congeladas, adaptándolas a los requisitos específicos de la nueva tarea.
  • Aprendizaje eficiente por transferencia: Este enfoque permite que el modelo transfiera eficientemente el conocimiento de la tarea original a la nueva. Preserva las características generalizadas valiosas aprendidas del conjunto de datos original grande, mientras que enfoca el proceso de aprendizaje en adaptaciones específicas de la tarea.
  • Reducción del riesgo de sobreajuste: Al limitar las actualizaciones de parámetros a solo un subconjunto de capas, reducimos el riesgo de sobreajuste, especialmente cuando se trabaja con conjuntos de datos más pequeños para la nueva tarea. Esto es particularmente beneficioso cuando la nueva tarea es similar a la original pero tiene datos de entrenamiento limitados.

Esta estrategia de flujo selectivo de gradientes permite un equilibrio fino entre preservar el conocimiento general y adaptarse a nuevas tareas específicas, lo que hace que el aprendizaje por transferencia sea una técnica poderosa en escenarios con datos o recursos computacionales limitados.

4. Preprocesamiento de Datos y Aumento

Cuando se trabaja con modelos preentrenados, es crucial preprocesar los datos de entrada de una manera coherente con los datos de entrenamiento originales del modelo. Esto garantiza que los nuevos datos estén en un formato que el modelo pueda interpretar eficazmente. El preprocesamiento típicamente incluye:

  • Redimensionamiento de imágenes: La mayoría de los modelos preentrenados esperan imágenes de entrada de un tamaño específico (por ejemplo, 224x224 píxeles para muchas arquitecturas populares). Cambiar el tamaño garantiza que todas las imágenes coincidan con esta dimensión de entrada esperada.
  • Normalización: Esto implica ajustar los valores de los píxeles a una escala estándar, a menudo utilizando la media y la desviación estándar del conjunto de datos de entrenamiento original (por ejemplo, estadísticas de ImageNet para muchos modelos).
  • Aumento de datos: Esta técnica expande artificialmente el conjunto de datos de entrenamiento aplicando varias transformaciones a las imágenes existentes. Aumentaciones comunes incluyen:
    • Recorte y volteo aleatorio: Ayuda al modelo a aprender invarianza a la posición y orientación.
    • Variación de color: Ajusta el brillo, el contraste y la saturación para mejorar la robustez ante condiciones de iluminación.
    • Rotación y escalado: Mejora la capacidad del modelo para reconocer objetos en diferentes ángulos y tamaños.

El preprocesamiento y el aumento adecuados no solo garantizan la compatibilidad con el modelo preentrenado, sino que también pueden mejorar significativamente la capacidad de generalización del modelo y su rendimiento en la nueva tarea.

5. Monitoreo del Rendimiento y Parada Temprana

El monitoreo vigilante del rendimiento del modelo en los conjuntos de datos de entrenamiento y validación es esencial en el aprendizaje por transferencia. A diferencia de los modelos entrenados desde cero, los modelos de aprendizaje por transferencia a menudo muestran una rápida convergencia debido a su conocimiento preexistente. Este proceso de aprendizaje acelerado requiere una observación cuidadosa para evitar el sobreajuste. Implementar técnicas de parada temprana se vuelve crucial en este contexto.

La parada temprana implica detener el proceso de entrenamiento cuando el rendimiento del modelo en el conjunto de validación comienza a deteriorarse, incluso cuando sigue mejorando en el conjunto de entrenamiento. Esta divergencia en el rendimiento es un claro indicador de sobreajuste, donde el modelo comienza a memorizar los datos de entrenamiento en lugar de aprender patrones generalizables.

Para implementar un monitoreo de rendimiento efectivo y parada temprana:

  • Evalúe regularmente el modelo en un conjunto de validación reservado durante el entrenamiento.
  • Rastree métricas clave como precisión, pérdida y, potencialmente, medidas específicas de la tarea (por ejemplo, F1-score para tareas de clasificación).
  • Implemente mecanismos de paciencia, donde el entrenamiento continúa durante un número determinado de épocas incluso después de detectar un posible punto de sobreajuste, para asegurarse de que no sea una fluctuación temporal.
  • Considere usar técnicas como el guardado de puntos de control del modelo para guardar el estado del modelo que mejor funcione, lo que le permite volver a este punto óptimo después del entrenamiento.

Al emplear estas estrategias, puede aprovechar las capacidades de aprendizaje acelerado del aprendizaje por transferencia, mientras se protege contra el sobreajuste, lo que finalmente produce un modelo que generaliza bien en datos no vistos.

Al tener en cuenta estos factores, puede aprovechar eficazmente el aprendizaje por transferencia para lograr un rendimiento superior en nuevas tareas, especialmente cuando se trabaja con conjuntos de datos o recursos computacionales limitados.

Ejemplo: Entrenando un ResNet-18 Preentrenado en un Nuevo Conjunto de Datos

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

# Check if CUDA is available
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

# Define transformations for the new dataset
transform = transforms.Compose([
    transforms.Resize(224),  # ResNet requires 224x224 images
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Load the new dataset (CIFAR-10)
train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
test_dataset = datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)

# Load pre-trained ResNet18 model
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

# Modify the final layer for CIFAR-10 (10 classes)
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, 10)

# Move model to the appropriate device
model = model.to(device)

# Define the loss function and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)

# Training loop
epochs = 10
for epoch in range(epochs):
    model.train()
    running_loss = 0.0
    for i, (images, labels) in enumerate(train_loader):
        images, labels = images.to(device), labels.to(device)
        
        optimizer.zero_grad()   # Zero the parameter gradients
        outputs = model(images)  # Forward pass
        loss = criterion(outputs, labels)  # Compute the loss
        loss.backward()  # Backward pass (compute gradients)
        optimizer.step()  # Optimization step (update parameters)

        running_loss += loss.item()
        
        if i % 100 == 99:    # Print every 100 mini-batches
            print(f'[{epoch + 1}, {i + 1:5d}] loss: {running_loss / 100:.3f}')
            running_loss = 0.0

    # Validation
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for images, labels in test_loader:
            images, labels = images.to(device), labels.to(device)
            outputs = model(images)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()

    print(f'Accuracy on test set: {100 * correct / total:.2f}%')

print('Finished Training')

# Save the model
torch.save(model.state_dict(), 'cifar10_resnet18.pth')

Este ejemplo de código muestra un método para el ajuste fino de un modelo ResNet18 preentrenado en el conjunto de datos CIFAR-10 utilizando PyTorch.

Analicemos los componentes clave y expliquemos sus propósitos:

  1. Importaciones y Configuración del Dispositivo:
    • Importamos los módulos necesarios de PyTorch y torchvision.
    • Verificamos la disponibilidad de CUDA para utilizar la aceleración por GPU si es posible.
  2. Preprocesamiento de Datos:
    • Definimos un proceso de transformación que redimensiona las imágenes a 224x224 (requerido por ResNet), las convierte en tensores y las normaliza utilizando las estadísticas de ImageNet.
    • Los conjuntos de datos de entrenamiento y prueba se cargan utilizando el conjunto de datos CIFAR-10 de torchvision.
  3. Cargadores de Datos:
    • Creamos objetos DataLoader tanto para los conjuntos de entrenamiento como de prueba, que manejan el procesamiento por lotes y la aleatorización de datos.
  4. Preparación del Modelo:
    • Cargamos un modelo ResNet18 preentrenado usando models.resnet18(weights=models.ResNet18_Weights.DEFAULT).
    • La capa totalmente conectada final se modifica para producir 10 clases (para CIFAR-10) en lugar de las 1000 originales (para ImageNet).
    • El modelo se transfiere al dispositivo apropiado (GPU si está disponible).
  5. Función de Pérdida y Optimizador:
    • Se utiliza la Pérdida de Entropía Cruzada como función de pérdida, que es adecuada para clasificación multiclase.
    • Se utiliza el optimizador SGD con una tasa de aprendizaje de 0.001 y momento de 0.9.
  6. Bucle de Entrenamiento:
    • El modelo se entrena durante 10 épocas.
    • En cada época, iteramos a través de los datos de entrenamiento, calculamos la pérdida, realizamos la retropropagación y actualizamos los parámetros del modelo.
    • El progreso del entrenamiento se imprime cada 100 lotes.
  7. Validación:
    • Después de cada época, el modelo se evalúa en el conjunto de prueba para medir su precisión.
    • Esto ayuda a monitorear el rendimiento del modelo y detectar el sobreajuste.
  8. Guardado del Modelo:
    • Después del entrenamiento, el diccionario de estado del modelo se guarda en un archivo para uso posterior.

Este ejemplo muestra el proceso completo de ajuste fino de un modelo preentrenado, desde la preparación de datos hasta la evaluación y guardado del modelo. Demuestra las mejores prácticas como el uso de aceleración por GPU, preprocesamiento adecuado de datos y evaluación regular del rendimiento durante el entrenamiento.

4.3.5 Evaluación del Modelo Ajustado Finamente

Después de la fase de entrenamiento, es crucial evaluar el rendimiento del modelo en un conjunto de datos de prueba separado. Este proceso de evaluación cumple varios propósitos:

  • Proporciona una estimación imparcial de la capacidad del modelo para generalizar a datos no vistos.
  • Ayuda a detectar posibles problemas de sobreajuste que podrían haber ocurrido durante el entrenamiento.
  • Permite la comparación con otros modelos o versiones anteriores del mismo modelo.

Al evaluar el modelo en un conjunto de prueba, podemos medir qué tan bien se desempeña nuestro modelo ajustado finamente en datos que no ha encontrado durante el proceso de entrenamiento, brindándonos información valiosa sobre su aplicabilidad en el mundo real.

Ejemplo: Evaluación del Modelo Ajustado Finamente

import torch
import torchvision
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
import numpy as np

# Define the device
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

# Define transformations for the test dataset
transform = transforms.Compose([
    transforms.Resize(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Load the test dataset (CIFAR-10 test set)
test_dataset = datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)

# Load the model (assuming it's already trained and saved)
model = torchvision.models.resnet18(weights=None)
num_ftrs = model.fc.in_features
model.fc = torch.nn.Linear(num_ftrs, 10)  # 10 classes for CIFAR-10
model.load_state_dict(torch.load('cifar10_resnet18.pth'))
model = model.to(device)

# Switch model to evaluation mode
model.eval()

# Disable gradient computation for evaluation
correct = 0
total = 0
class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))

with torch.no_grad():
    for images, labels in test_loader:
        images, labels = images.to(device), labels.to(device)
        outputs = model(images)
        _, predicted = torch.max(outputs, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()
        
        c = (predicted == labels).squeeze()
        for i in range(len(labels)):
            label = labels[i]
            class_correct[label] += c[i].item()
            class_total[label] += 1

# Calculate overall accuracy
accuracy = 100 * correct / total
print(f'Overall Accuracy on test set: {accuracy:.2f}%')

# Calculate and print per-class accuracy
classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
for i in range(10):
    print(f'Accuracy of {classes[i]}: {100 * class_correct[i] / class_total[i]:.2f}%')

# Visualize some predictions
def imshow(img):
    img = img / 2 + 0.5     # unnormalize
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))
    plt.axis('off')

# Get some random test images
dataiter = iter(test_loader)
images, labels = next(dataiter)

# Make predictions
outputs = model(images.to(device))
_, predicted = torch.max(outputs, 1)

# Show images and their predicted labels
fig = plt.figure(figsize=(12, 48))
for i in range(4):
    ax = fig.add_subplot(1, 4, i+1)
    imshow(images[i])
    ax.set_title(f'Predicted: {classes[predicted[i]]}\nActual: {classes[labels[i]]}')

plt.tight_layout()
plt.show()

Este ejemplo de código proporciona una evaluación integral del modelo ajustado finamente. Analicemos sus componentes:

  1. Importaciones y Configuración del Dispositivo:
    • Importamos los módulos necesarios de PyTorch y torchvision.
    • Configuramos el dispositivo (CPU o GPU) para el cómputo.
  2. Preprocesamiento de Datos:
    • Definimos el mismo proceso de transformación utilizado durante el entrenamiento.
    • Cargamos el conjunto de prueba CIFAR-10 y creamos un DataLoader.
  3. Carga del Modelo:
    • Recreamos la arquitectura del modelo (ResNet18 con la capa final modificada).
    • Cargamos los pesos guardados del modelo desde 'cifar10_resnet18.pth'.
    • Trasladamos el modelo al dispositivo apropiado (CPU o GPU).
  4. Bucle de Evaluación:
    • Cambiamos el modelo a modo evaluación usando model.eval().
    • Desactivamos el cálculo de gradientes usando torch.no_grad() para ahorrar memoria y acelerar el cómputo.
    • Iteramos a través de los datos de prueba, realizando predicciones y comparándolas con las etiquetas verdaderas.
    • Llevamos un registro de las predicciones correctas totales y por clase.
  5. Cálculo y Reporte de Resultados:
    • Calculamos e imprimimos la precisión general en el conjunto de prueba.
    • Calculamos e imprimimos las precisiones por clase, lo que nos da una idea de en qué clases el modelo funciona bien y en cuáles tiene dificultades.
  6. Visualización:
    • Definimos una función imshow() para mostrar imágenes.
    • Obtenemos un lote de imágenes de prueba y realizamos predicciones sobre ellas.
    • Visualizamos 4 imágenes de prueba aleatorias junto con sus etiquetas predichas y reales.

Esta evaluación integral proporciona varios beneficios:

  • Nos da la precisión general, que es una medida global del rendimiento del modelo.
  • Proporciona precisiones por clase, permitiéndonos identificar si el modelo tiene sesgos a favor o en contra de ciertas clases.
  • La visualización de las predicciones nos ayuda a evaluar cualitativamente el rendimiento del modelo y potencialmente identificar patrones en sus errores.

Este enfoque de evaluación del modelo nos proporciona una comprensión mucho más detallada de las fortalezas y debilidades de nuestro modelo, lo cual es crucial para mejoras posteriores y para evaluar su idoneidad para su implementación en aplicaciones del mundo real.