Clasificación de imágenes con aprendizaje de transferencia y PyTorch

C

Introducción

El aprendizaje por transferencia es una técnica poderosa para entrenar redes neuronales profundas que le permite a uno tomar el conocimiento aprendido sobre un problema de aprendizaje profundo y aplicarlo a un problema de aprendizaje diferente pero similar.

El uso del aprendizaje por transferencia puede acelerar drásticamente la tasa de implementación de una aplicación que está diseñando, haciendo que tanto el entrenamiento como la implementación de su red neuronal profunda sean más simples y fáciles.

En este artículo repasaremos la teoría detrás del aprendizaje por transferencia y veremos cómo llevar a cabo un ejemplo de aprendizaje por transferencia en redes neuronales convolucionales (CNN) en PyTorch.

  • ¿Qué es PyTorch?
  • Definición de términos necesarios
    • ¿Qué es el aprendizaje profundo?
    • ¿Qué es una red neuronal convolucional?
    • Entrenamiento y pruebas
    • ¿Qué es el aprendizaje por transferencia?
  • Teoría del aprendizaje por transferencia
  • Clasificación de imágenes con aprendizaje de transferencia en PyTorch
    • Preprocesamiento de datos
    • Cargando los datos
    • Configurar un modelo previamente entrenado
    • Visualización
    • Extractor de funciones fijas
  • Conclusión

¿Qué es PyTorch?

Pytorch es una biblioteca desarrollada para Python, especializada en aprendizaje profundo y procesamiento de lenguaje natural. PyTorch aprovecha el poder de las unidades de procesamiento gráfico (GPU) para hacer que la implementación de una red neuronal profunda sea más rápida que entrenar una red en una CPU.

PyTorch ha tenido una creciente popularidad entre los investigadores de aprendizaje profundo gracias a su velocidad y flexibilidad. PyTorch se vende solo en tres características diferentes:

  • Una interfaz simple y fácil de usar
  • Integración completa con la pila de ciencia de datos de Python
  • Gráficos computacionales flexibles / dinámicos que se pueden cambiar durante el tiempo de ejecución (lo que hace que el entrenamiento de una red neuronal sea significativamente más fácil cuando no tiene idea de cuánta memoria se requerirá para su problema).

PyTorch es compatible con NumPy y permite que las matrices NumPy se transformen en tensores y viceversa.

Definición de términos necesarios

Antes de continuar, tomemos un momento para definir algunos términos relacionados con el aprendizaje transferido. Tener claras nuestras definiciones hará que la comprensión de la teoría detrás del aprendizaje por transferencia y la implementación de una instancia de aprendizaje por transferencia sea más fácil de comprender y replicar.

¿Qué es el aprendizaje profundo?

El aprendizaje profundo es una subsección del Machine Learning, y el Machine Learning puede describirse simplemente como el acto de permitir que las computadoras realicen tareas sin estar programadas explícitamente para hacerlo.

Los sistemas de aprendizaje profundo utilizan redes neuronales, que son marcos computacionales modelados a partir del cerebro humano.

Las redes neuronales tienen tres componentes diferentes: una capa de entrada , una capa oculta o capa intermedia y una capa de salida .

La capa de entrada es simplemente donde se procesan los datos que se envían a la red neuronal, mientras que las capas intermedias / capas ocultas se componen de una estructura denominada node o neurona.

Estos nodes son funciones matemáticas que alteran la información de entrada de alguna manera y pasan los datos alterados a la capa final, o capa de salida. Las redes neuronales simples pueden distinguir patrones simples en los datos de entrada ajustando las suposiciones, o ponderaciones, sobre cómo los puntos de datos se relacionan entre sí.

Una red neuronal profunda recibe su nombre del hecho de que está formada por muchas redes neuronales regulares unidas. Cuantas más redes neuronales estén vinculadas, más patrones complejos podrá distinguir la red neuronal profunda y más usos tendrá. Existen diferentes tipos de redes neuronales, y cada tipo tiene su propia especialidad.

Por ejemplo, las redes neuronales profundas de memoria a corto plazo son redes que funcionan muy bien cuando se manejan tareas urgentes, donde el orden cronológico de los datos es importante, como los datos de texto o voz.

¿Qué es una red neuronal convolucional?

Este artículo se ocupará de las redes neuronales convolucionales , un tipo de red neuronal que se destaca en la manipulación de datos de imágenes.

Las redes neuronales convolucionales (CNN) son tipos especiales de redes neuronales, expertas en crear representaciones de datos visuales. Los datos en una CNN se representan como una cuadrícula que contiene valores que representan el brillo y el color de cada píxel de la imagen.

Una CNN se divide en tres componentes diferentes : las capas convolucionales , las capas agrupadas y las capas completamente conectadas .

La responsabilidad de la capa convolucional es crear una representación de la imagen tomando el producto escalar de dos matrices.

La primera matriz es un conjunto de parámetros que se pueden aprender, denominados kernel. La otra matriz es una parte de la imagen que se analiza, que tendrá canales de altura, ancho y color. Las capas convolucionales son donde ocurre la mayor parte de los cálculos en una CNN. El núcleo se mueve a lo largo de todo el ancho y alto de la imagen, produciendo finalmente una representación de la imagen completa que es bidimensional, una representación conocida como mapa de activación.

Debido a la gran cantidad de información contenida en las capas convolucionales de CNN, puede llevar mucho tiempo entrenar la red. La función de las capas de agrupación es reducir la cantidad de información contenida en las capas convolucionales de CNN, tomando la salida de una capa convolucional y escalando para hacer la representación más simple.

La capa de agrupación logra esto al observar diferentes puntos en las salidas de la red y “agrupar” los valores cercanos, generando un valor único que representa todos los valores cercanos. En otras palabras, se necesita una estadística resumida de los valores en una región elegida.

Resumir los valores en una región significa que la red puede reducir en gran medida el tamaño y la complejidad de su representación y, al mismo tiempo, conservar la información relevante que permitirá a la red reconocer esa información y dibujar patrones significativos de la imagen.

Hay varias funciones que se pueden usar para resumir los valores de una región, como tomar el promedio de un vecindario o la agrupación promedio. También se puede tomar un promedio ponderado del vecindario, al igual que la norma L2 de la región. La técnica de agrupación más común es la agrupación máxima, donde se toma el valor máximo de la región y se utiliza para representar el vecindario.

La capa completamente conectada es donde todas las neuronas están unidas entre sí, con conexiones entre cada capa anterior y siguiente en la red. Aquí es donde se analiza la información que ha sido extraída por las capas convolucionales y agrupada por las capas de agrupación, y donde se aprenden los patrones en los datos. Los cálculos aquí se llevan a cabo mediante la multiplicación de matrices combinada con un efecto de sesgo.

También hay varias no linealidades presentes en la CNN. Al considerar que las imágenes en sí mismas son cosas no lineales, la red debe tener componentes no lineales para poder interpretar los datos de la imagen. Las capas no lineales generalmente se insertan en la red directamente después de las capas convolucionales, ya que esto le da al mapa de activación no linealidad.

Existe una variedad de diferentes funciones de activación no lineal que se pueden utilizar con el fin de permitir que la red interprete correctamente los datos de la imagen. La función de activación no lineal más popular es ReLu, o la unidad lineal rectificada . La función ReLu convierte las entradas no lineales en una representación lineal al comprimir valores reales solo a valores positivos por encima de 0. Para decirlo de otra manera, la función ReLu toma cualquier valor por encima de cero y lo devuelve como está, mientras que si el valor es menor devuelto como cero.

La función ReLu es popular debido a su confiabilidad y velocidad, y funciona alrededor de seis veces más rápido que otras funciones de activación. La desventaja de ReLu es que puede atascarse fácilmente al manejar grandes gradientes, sin actualizar nunca las neuronas. Este problema se puede abordar estableciendo una tasa de aprendizaje para la función.

Otras dos funciones no lineales populares son la función sigmoidea y la función Tanh .

La función sigmoidea funciona tomando valores reales y aplastándolos a un rango entre 0 y 1, aunque tiene problemas para manejar activaciones que están cerca de los extremos del gradiente, ya que los valores se vuelven casi cero.

Mientras tanto, la función Tanh opera de manera similar a Sigmoid, excepto que su salida está centrada cerca de cero y aplasta los valores entre -1 y 1.

Entrenamiento y pruebas

Hay dos fases diferentes para crear e implementar una red neuronal profunda: entrenamiento y prueba .

La fase de entrenamiento es donde la red se alimenta con los datos y comienza a aprender los patrones que contienen los datos, ajustando los pesos de la red, que son suposiciones sobre cómo los puntos de datos se relacionan entre sí. Para decirlo de otra manera, la fase de entrenamiento es donde la red “aprende” sobre los datos que se han alimentado.

La fase de prueba es donde se evalúa lo que la red ha aprendido. A la red se le proporciona un nuevo conjunto de datos, uno que no ha visto antes, y luego se le pide a la red que aplique sus suposiciones sobre los patrones que ha aprendido a los nuevos datos. La precisión del modelo se evalúa y, por lo general, el modelo se modifica y se vuelve a entrenar, luego se vuelve a probar, hasta que el arquitecto está satisfecho con el rendimiento del modelo.

En el caso del aprendizaje por transferencia, la red que se utiliza ha sido preentrenada. Los pesos de la red ya se han ajustado y guardado, por lo que no hay razón para entrenar a toda la red nuevamente desde cero. Esto significa que la red se puede usar inmediatamente para realizar pruebas, o simplemente se pueden modificar y volver a capacitar determinadas capas de la red. Esto acelera enormemente el despliegue de la red neuronal profunda.

¿Qué es el aprendizaje por transferencia?

La idea detrás del aprendizaje por transferencia es tomar un modelo entrenado en una tarea y aplicarlo a una segunda tarea similar. El hecho de que un modelo ya haya entrenado algunos o todos los pesos para la segunda tarea significa que el modelo se puede implementar mucho más rápido. Esto permite una evaluación rápida del rendimiento y el ajuste del modelo, lo que permite una implementación más rápida en general. El aprendizaje por transferencia se está volviendo cada vez más popular en el campo del aprendizaje profundo, gracias a la gran cantidad de recursos computacionales y el tiempo necesario para entrenar modelos de aprendizaje profundo, además de conjuntos de datos grandes y complejos.

La principal limitación del aprendizaje por transferencia es que las características del modelo aprendidas durante la primera tarea son generales y no específicas de la primera. En la práctica, esto significa que los modelos entrenados para reconocer ciertos tipos de imágenes pueden reutilizarse para reconocer otras imágenes, siempre que las características generales de las imágenes sean similares.

Teoría del aprendizaje por transferencia

La utilización del aprendizaje por transferencia tiene varios conceptos importantes. Para comprender la implementación del aprendizaje por transferencia, necesitamos repasar cómo se ve un modelo previamente entrenado y cómo ese modelo se puede ajustar a sus necesidades.

Hay dos formas de elegir un modelo para el aprendizaje por transferencia. Es posible crear un modelo desde cero para sus propias necesidades, guardar los parámetros y la estructura del modelo y luego reutilizar el modelo más tarde.

La segunda forma de implementar el aprendizaje por transferencia es simplemente tomar un modelo ya existente y reutilizarlo, ajustando sus parámetros e hiperparámetros a medida que lo hace. En este caso, usaremos un modelo previamente entrenado y lo modificaremos. Una vez que haya decidido qué enfoque desea utilizar, elija un modelo (si está utilizando un modelo previamente entrenado).

Existe una gran variedad de modelos previamente entrenados que se pueden usar en PyTorch. Algunas de las CNN previamente capacitadas incluyen:

  • AlexNet
  • CaffeResNet
  • Comienzo
  • La serie ResNet
  • La serie VGG

Se puede acceder a estos modelos previamente entrenados a través de la API de PyTorch y, cuando se le indique, PyTorch descargará sus especificaciones en su máquina. El modelo específico que vamos a utilizar es ResNet34, parte de la serie Resnet.

El modelo Resnet se desarrolló y entrenó en un conjunto de datos ImageNet , así como en el conjunto de datos CIFAR-10 . Como tal, está optimizado para tareas de reconocimiento visual y mostró una mejora notable con respecto a la serie VGG, razón por la cual lo usaremos.

Sin embargo, existen otros modelos previamente entrenados y es posible que desee experimentar con ellos para ver cómo se comparan.

Como explica la documentación de PyTorch sobre el aprendizaje por transferencia , hay dos formas principales en que se utiliza el aprendizaje por transferencia: ajustando una CNN o utilizando la CNN como un extractor de funciones fijas.

Al ajustar una CNN, usa los pesos que tiene la red preentrenada en lugar de inicializarlos al azar, y luego entrena como de costumbre. Por el contrario, un enfoque de extracción de características significa que mantendrá todos los pesos de la CNN excepto los de las últimas capas, que se inicializarán aleatoriamente y se entrenarán de forma normal.

El ajuste fino de un modelo es importante porque, aunque el modelo se ha entrenado previamente, se ha entrenado en una tarea diferente (aunque con suerte similar). Los pesos densamente conectados con los que viene el modelo preentrenado probablemente serán algo insuficientes para sus necesidades, por lo que es probable que desee volver a entrenar las últimas capas de la red.

Por el contrario, debido a que las primeras capas de la red son solo capas de extracción de características y funcionarán de manera similar en imágenes similares, se pueden dejar como están. Por lo tanto, si el conjunto de datos es pequeño y similar, el único entrenamiento que debe realizarse es el entrenamiento de las últimas capas. Cuanto más grande y complejo sea el conjunto de datos, más será necesario volver a entrenar el modelo. Recuerde que el aprendizaje por transferencia funciona mejor cuando el conjunto de datos que está utilizando es más pequeño que el modelo preentrenado original y similar a las imágenes alimentadas al modelo preentrenado.

Trabajar con modelos de aprendizaje por transferencia en Pytorch significa elegir qué capas congelar y cuáles descongelar. Congelar un modelo significa decirle a PyTorch que conserve los parámetros (pesos) en las capas que ha especificado. Descongelar un modelo significa decirle a PyTorch que desea que las capas que ha especificado estén disponibles para entrenamiento, para que se puedan entrenar sus pesos.

Una vez que haya terminado de entrenar las capas elegidas del modelo preentrenado, probablemente querrá guardar los pesos recién entrenados para usarlos en el futuro. Aunque usar un modelo previamente entrenado es más rápido que entrenar un modelo desde cero, todavía lleva tiempo entrenar, por lo que querrá copiar los mejores pesos de modelo.

Clasificación de imágenes con aprendizaje de transferencia en PyTorch

Estamos listos para comenzar a implementar el aprendizaje por transferencia en un conjunto de datos. Cubriremos tanto el ajuste fino de ConvNet como el uso de la red como extractor de funciones fijas.

Preprocesamiento de datos

En primer lugar, tendremos que decidir qué conjunto de datos usar. Elijamos algo que tenga muchas imágenes realmente claras para entrenar. El conjunto de datos de Stanford Cats and Dogs es un conjunto de datos de uso muy común, elegido por lo simple pero ilustrativo que es. Puede descargarlo aquí mismo .

Asegúrese de dividir el conjunto de datos en dos conjuntos de igual tamaño: “train” y “val”.

Puede hacer esto de la forma que desee, moviendo manualmente los archivos o escribiendo una función para manejarlo. Es posible que también desee limitar el conjunto de datos a un tamaño más pequeño, ya que viene con casi 12,000 imágenes en cada categoría, y esto llevará mucho tiempo para entrenar. Es posible que desee reducir ese número a alrededor de 5000 en cada categoría, con 1000 reservados para validación. Sin embargo, la cantidad de imágenes que desea utilizar para el entrenamiento depende de usted.

Esta es una forma de preparar los datos para su uso:

import os
import shutil
import re

base_dir = "PetImages/"

# Create training folder
files = os.listdir(base_dir)

# Moves all training cat images to cats folder, training dog images to dogs folder
def train_maker(name):
  train_dir = f"{base_dir}/train/{name}"
  for f in files:
        search_object = re.search(name, f)
        if search_object:
          shutil.move(f'{base_dir}/{name}', train_dir)

train_maker("Cat")
train_maker("Dog")

# Make the validation directories
try:
    os.makedirs("val/Cat")
    os.makedirs("val/Dog")
except OSError:
    print ("Creation of the directory %s failed")
else:
    print ("Successfully created the directory %s ")

# Create validation folder

cat_train = base_dir + "train/Cat/"
cat_val = base_dir + "val/Cat/"
dog_train = base_dir + "train/Dog/"
dog_val = base_dir + "val/Dog/"

cat_files = os.listdir(cat_train)
dog_files = os.listdir(dog_train)

# This will put 1000 images from the two training folders
# into their respective validation folders

for f in cat_files:
    validationCatsSearchObj = re.search("5ddd", f)
    if validationCatsSearchObj:
        shutil.move(f'{cat_train}/{f}', cat_val)

for f in dog_files:
    validationCatsSearchObj = re.search("5ddd", f)
    if validationCatsSearchObj:
        shutil.move(f'{dog_train}/{f}', dog_val)

Cargando los datos

Después de haber seleccionado y preparado los datos, podemos comenzar importando todas las bibliotecas necesarias. Necesitaremos muchos de los paquetes de Torch como nnla red neuronal, los optimizadores y DataLoaders. También queremos matplotlibvisualizar algunos de nuestros ejemplos de formación.

Necesitamos numpymanejar la creación de matrices de datos, así como algunos otros módulos diversos:

from __future__ import print_function, division

import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim import lr_scheduler
import torchvision
from torchvision import datasets, models, transforms
import matplotlib.pyplot as plt
import numpy as np
import time
import os
import copy

Para empezar, necesitamos cargar nuestros datos de entrenamiento y prepararlos para que los utilice nuestra red neuronal. Vamos a utilizar Pytorch transformspara ese propósito. Tendremos que asegurarnos de que las imágenes en el conjunto de entrenamiento y el conjunto de validación sean del mismo tamaño, por lo que usaremos transforms.Resize.

También haremos un pequeño aumento de datos, tratando de mejorar el rendimiento de nuestro modelo forzándolo a aprender sobre imágenes en diferentes ángulos y recortes, por lo que recortaremos y rotaremos las imágenes al azar.

A continuación, crearemos tensores a partir de las imágenes, ya que PyTorch trabaja con tensores. Finalmente, normalizaremos las imágenes, lo que ayuda a que la red funcione con valores que pueden tener una amplia gama de valores diferentes.

A continuación, composetodas nuestras transformaciones elegidas. Tenga en cuenta que las transformaciones de validación no tienen ningún cambio o rotación, ya que no forman parte de nuestro conjunto de capacitación, por lo que la red no está aprendiendo sobre ellas:

# Make transforms and use data loaders

# We'll use these a lot, so make them variables
mean_nums = [0.485, 0.456, 0.406]
std_nums = [0.229, 0.224, 0.225]

chosen_transforms = {'train': transforms.Compose([
        transforms.RandomResizedCrop(size=256),
        transforms.RandomRotation(degrees=15),
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        transforms.Normalize(mean_nums, std_nums)
]), 'val': transforms.Compose([
        transforms.Resize(256),
        transforms.CenterCrop(224),
        transforms.ToTensor(),
        transforms.Normalize(mean_nums, std_nums)
]),
}

Ahora configuraremos el directorio para nuestros datos y usaremos la ImageFolderfunción de PyTorch para crear conjuntos de datos:

# Set the directory for the data
data_dir="/data/"

# Use the image folder function to create datasets
chosen_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),
  chosen_transforms[x])
                  for x in ['train', 'val']}

Ahora que hemos elegido las carpetas de imágenes que queremos, necesitamos usar DataLoaders para crear objetos iterables con los que trabajar. Le decimos qué conjuntos de datos queremos usar, le damos un tamaño de lote y mezclamos los datos.

# Make iterables with the dataloaders
dataloaders = {x: torch.utils.data.DataLoader(chosen_datasets[x], batch_size=4,
  shuffle=True, num_workers=4)
              for x in ['train', 'val']}

Necesitaremos preservar alguna información sobre nuestro conjunto de datos, específicamente el tamaño del conjunto de datos y los nombres de las clases en nuestro conjunto de datos. También necesitamos especificar con qué tipo de dispositivo estamos trabajando, una CPU o GPU. La siguiente configuración usará GPU si está disponible; de ​​lo contrario, se usará CPU:

dataset_sizes = {x: len(chosen_datasets[x]) for x in ['train', 'val']}
class_names = chosen_datasets['train'].classes

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

Ahora intentemos visualizar algunas de nuestras imágenes con una función. Tomaremos una entrada, crearemos una matriz Numpy a partir de ella y la transpondremos. Luego, normalizaremos la entrada usando la media y la desviación estándar. Finalmente, recortaremos los valores entre 0 y 1 para que no haya un rango masivo en los valores posibles de la matriz, y luego mostraremos la imagen:

# Visualize some images
def imshow(inp, title=None):
    inp = inp.numpy().transpose((1, 2, 0))
    mean = np.array([mean_nums])
    std = np.array([std_nums])
    inp = std * inp + mean
    inp = np.clip(inp, 0, 1)
    plt.imshow(inp)
    if title is not None:
        plt.title(title)
    plt.pause(0.001)  # Pause a bit so that plots are updated

Ahora usemos esa función y visualicemos algunos de los datos. Vamos a obtener las entradas y el nombre de las clases del DataLoadery las almacenaremos para su uso posterior. Luego crearemos una cuadrícula para mostrar las entradas y mostrarlas:

# Grab some of the training data to visualize
inputs, classes = next(iter(dataloaders['train']))

# Now we construct a grid from batch
out = torchvision.utils.make_grid(inputs)

imshow(out, title=[class_names[x] for x in classes])

Configurar un modelo previamente entrenado

Ahora tenemos que configurar el modelo preentrenado que queremos usar para el aprendizaje por transferencia. En este caso, usaremos el modelo como está y simplemente restableceremos la capa final completamente conectada, proporcionándole nuestra cantidad de características y clases.

Cuando se utilizan modelos previamente entrenados, PyTorch configura el modelo para que se descongele (tendrá sus pesos ajustados) de forma predeterminada. Así que entrenaremos todo el modelo:

# Setting up the model
# load in pretrained and reset final fully connected

res_mod = models.resnet34(pretrained=True)

num_ftrs = res_mod.fc.in_features
res_mod.fc = nn.Linear(num_ftrs, 2)

Si esto todavía parece poco claro, visualizar la composición del modelo puede ayudar.

for name, child in res_mod.named_children():
    print(name)

Esto es lo que devuelve:

conv1
bn1
relu
maxpool
layer1
layer2
layer3
layer4
avgpool
fc

Observe que la parte final está fc“Completamente conectada”. Esta es la única capa de la que estamos modificando la forma, dándole nuestras dos clases para la salida.

Básicamente, cambiaremos las salidas de la parte final completamente conectada a solo dos clases y ajustaremos los pesos para todas las demás capas.

Ahora tenemos que enviar nuestro modelo a nuestro dispositivo de entrenamiento. También debemos elegir el criterio de pérdida y el optimizador que queremos usar con el modelo. CrossEntropyLossy el SGDoptimizador son buenas opciones, aunque hay muchas otras.

También elegiremos un programador de tasas de aprendizaje, que disminuye la tasa de aprendizaje del optimizador con el tiempo y ayuda a prevenir la no convergencia debido a las grandes tasas de aprendizaje. Puede obtener más información sobre los programadores de tasas de aprendizaje aquí si tiene curiosidad:

res_mod = res_mod.to(device)
criterion = nn.CrossEntropyLoss()

# Observe that all parameters are being optimized
optimizer_ft = optim.SGD(res_mod.parameters(), lr=0.001, momentum=0.9)

# Decay LR by a factor of 0.1 every 7 epochs
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)

Ahora solo necesitamos definir las funciones que entrenarán el modelo y visualizarán las predicciones.

Comencemos con la función de entrenamiento. Tomará nuestro modelo elegido, así como el optimizador, criterio y programador que elegimos. También especificaremos un número predeterminado de épocas de entrenamiento.

Cada época tendrá una fase de formación y validación. Para empezar, configuramos los mejores pesos iniciales del modelo a los del modo preentrenado, usando state_dict.

Ahora, para cada época en el número de épocas elegido, si estamos en la fase de entrenamiento, haremos:

  • Disminuir la tasa de aprendizaje
  • Cero los gradientes
  • Realizar el pase de entrenamiento de avance
  • Calcule la perdida
  • Haga propagación hacia atrás y actualice los pesos con el optimizador

También realizaremos un seguimiento de la precisión del modelo durante la fase de entrenamiento, y si pasamos a la fase de validación y la precisión ha mejorado, guardaremos los pesos actuales como los mejores pesos del modelo:

def train_model(model, criterion, optimizer, scheduler, num_epochs=10):
    since = time.time()

    best_model_wts = copy.deepcopy(model.state_dict())
    best_acc = 0.0

    for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch, num_epochs - 1))
        print('-' * 10)

        # Each epoch has a training and validation phase
        for phase in ['train', 'val']:
            if phase == 'train':
                scheduler.step()
                model.train()  # Set model to training mode
            else:
                model.eval()   # Set model to evaluate mode

            current_loss = 0.0
            current_corrects = 0

            # Here's where the training happens
            print('Iterating through data...')

            for inputs, labels in dataloaders[phase]:
                inputs = inputs.to(device)
                labels = labels.to(device)

                # We need to zero the gradients, don't forget it
                optimizer.zero_grad()

                # Time to carry out the forward training poss
                # We only need to log the loss stats if we are in training phase
                with torch.set_grad_enabled(phase == 'train'):
                    outputs = model(inputs)
                    _, preds = torch.max(outputs, 1)
                    loss = criterion(outputs, labels)

                    # backward + optimize only if in training phase
                    if phase == 'train':
                        loss.backward()
                        optimizer.step()

                # We want variables to hold the loss statistics
                current_loss += loss.item() * inputs.size(0)
                current_corrects += torch.sum(preds == labels.data)

            epoch_loss = current_loss / dataset_sizes[phase]
            epoch_acc = current_corrects.double() / dataset_sizes[phase]

            print('{} Loss: {:.4f} Acc: {:.4f}'.format(
                phase, epoch_loss, epoch_acc))

            # Make a copy of the model if the accuracy on the validation set has improved
            if phase == 'val' and epoch_acc > best_acc:
                best_acc = epoch_acc
                best_model_wts = copy.deepcopy(model.state_dict())

        print()

    time_since = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(
        time_since // 60, time_since % 60))
    print('Best val Acc: {:4f}'.format(best_acc))

    # Now we'll load in the best model weights and return it
    model.load_state_dict(best_model_wts)
    return model

Nuestras copias impresas de formación deberían verse así:

Epoch 0/25
----------
Iterating through data...
train Loss: 0.5654 Acc: 0.7090
Iterating through data...
val Loss: 0.2726 Acc: 0.8889

Epoch 1/25
----------
Iterating through data...
train Loss: 0.5975 Acc: 0.7090
Iterating through data...
val Loss: 0.2793 Acc: 0.8889

Epoch 2/25
----------
Iterating through data...
train Loss: 0.5919 Acc: 0.7664
Iterating through data...
val Loss: 0.3992 Acc: 0.8627

Visualización

Ahora crearemos una función que nos permitirá ver las predicciones que hizo nuestro modelo.

def visualize_model(model, num_images=6):
    was_training = model.training
    model.eval()
    images_handeled = 0
    fig = plt.figure()

    with torch.no_grad():
        for i, (inputs, labels) in enumerate(dataloaders['val']):
            inputs = inputs.to(device)
            labels = labels.to(device)

            outputs = model(inputs)
            _, preds = torch.max(outputs, 1)

            for j in range(inputs.size()[0]):
                images_handeled += 1
                ax = plt.subplot(num_images//2, 2, images_handeled)
                ax.axis('off')
                ax.set_title('predicted: {}'.format(class_names[preds[j]]))
                imshow(inputs.cpu().data[j])

                if images_handeled == num_images:
                    model.train(mode=was_training)
                    return
        model.train(mode=was_training)

Ahora podemos unir todo. Entrenaremos el modelo en nuestras imágenes y mostraremos las predicciones:

base_model = train_model(res_mod, criterion, optimizer_ft, exp_lr_scheduler, num_epochs=3)
visualize_model(base_model)
plt.show()

Ese entrenamiento probablemente te llevará mucho tiempo si estás usando una CPU y no una GPU. Aún tomará algo de tiempo incluso si usa una GPU.

Extractor de funciones fijas

Debido al largo tiempo de entrenamiento, muchas personas eligen simplemente usar el modelo previamente entrenado como un extractor de características fijas y solo entrenar la última capa más o menos. Esto acelera significativamente el tiempo de entrenamiento. Para hacer eso, deberá reemplazar el modelo que hemos construido. Habrá un enlace a un repositorio de GitHub para ambas versiones de la implementación de ResNet.

Reemplace la sección donde se define el modelo preentrenado con una versión que congela los pesos y no lleva nuestros cálculos de gradiente o backprop.

Se ve bastante similar a antes, excepto que especificamos que los gradientes no necesitan cálculo:

# Setting up the model
# Note that the parameters of imported models are set to requires_grad=True by default

res_mod = models.resnet34(pretrained=True)
for param in res_mod.parameters():
    param.requires_grad = False

num_ftrs = res_mod.fc.in_features
res_mod.fc = nn.Linear(num_ftrs, 2)

res_mod = res_mod.to(device)
criterion = nn.CrossEntropyLoss()

# Here's another change: instead of all parameters being optimized
# only the params of the final layers are being optimized

optimizer_ft = optim.SGD(res_mod.fc.parameters(), lr=0.001, momentum=0.9)

exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)

¿Y si quisiéramos descongelar capas de forma selectiva y calcular los degradados de unas pocas capas elegidas? ¿Es eso posible? Sí lo es.

Imprimamos los hijos del modelo nuevamente para recordar qué capas / componentes tiene:

for name, child in res_mod.named_children():
    print(name)

Aquí están las capas:

conv1
bn1
relu
maxpool
layer1
layer2
layer3
layer4
avgpool
fc

Ahora que sabemos cuáles son las capas, podemos descongelar las que queramos, como solo las capas 3 y 4:

for name, child in res_mod.named_children():
    if name in ['layer3', 'layer4']:
        print(name + 'has been unfrozen.')
        for param in child.parameters():
            param.requires_grad = True
    else:
        for param in child.parameters():
            param.requires_grad = False

Por supuesto, también necesitaremos actualizar el optimizador para reflejar el hecho de que solo queremos optimizar ciertas capas.

optimizer_conv = torch.optim.SGD(filter(lambda x: x.requires_grad, res_mod.parameters()), lr=0.001, momentum=0.9)

Así que ahora sabe que puede sintonizar toda la red, solo la última capa o algo intermedio.

Conclusión

Felicitaciones, ahora ha implementado el aprendizaje por transferencia en PyTorch. Sería una buena idea comparar la implementación de una red sintonizada con el uso de un extractor de funciones fijas para ver cómo difiere el rendimiento. También se recomienda experimentar con la congelación y descongelación de ciertas capas, ya que le permite tener una mejor idea de cómo puede personalizar el modelo para que se ajuste a sus necesidades.

Aquí hay algunas otras cosas que puede probar:

  • Usar diferentes modelos previamente entrenados para ver cuáles funcionan mejor en diferentes circunstancias
  • Cambiar algunos de los argumentos del modelo, como ajustar la tasa de aprendizaje y el impulso
  • Pruebe la clasificación en un conjunto de datos con más de dos clases

Si tiene curiosidad por aprender más sobre las diferentes aplicaciones de aprendizaje por transferencia y la teoría detrás de ellas, hay un excelente desglose de algunas de las matemáticas detrás de esto, así como los casos de uso aquí .

El código de este artículo se puede encontrar en este repositorio de GitHub .

 

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias para su correcto funcionamiento. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad