Clasificación de imágenes con Transfer Learning y PyTorch

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

Introducción

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

El uso del aprendizaje de transferencia puede acelerar drásticamente la tasa de implementación de una aplicación que está diseñando, lo que hace 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?

Pytorch es una biblioteca desarrollada para Python, especializada en aprendizaje profundo y procesamiento natural del lenguaje. 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 visto una creciente popularidad entre los investigadores de aprendizaje profundo gracias a su velocidad y flexibilidad. PyTorch se vende a sí mismo 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 entrenar una red neuronal sea mucho más fácil cuando no tiene idea de cuánta memoria se necesitará para su problema).

PyTorch es compatible con NumPy y permite transformar matrices NumPy en tensores y viceversa.

Definición de términos necesarios

Antes de continuar, tomemos un momento para definir algunos términos relacionados con Transfer Learning. Aclarar 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 entender y replicar.

¿Qué es el aprendizaje profundo?

Aprendizaje profundo es una subsección del aprendizaje automático, y el aprendizaje automático se puede describir 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 nodo o neurona.

Estos nodos 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 se relacionan los puntos de datos 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](https://www.forbes.com/sites/bernardmarr/2018/10/01/what-is-deep -aprendizaje-ai-una-guia-sencilla-con-8-ejemplos-practicos/#61353db38d4b) tiene. Hay diferentes tipos de redes neuronales, y cada tipo tiene su propia especialidad.

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

¿Qué es una red neuronal convolucional?

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

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

Una CNN se divide en tres componentes diferentes: las capas convolucionales, las capas de agrupación y las capas totalmente 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 aprendibles, denominado núcleo. La otra matriz es una parte de la imagen que se analiza, que tendrá un alto, un ancho y canales de color. Las capas convolucionales son donde ocurre la mayor parte del cálculo en una CNN. El kernel se mueve a lo largo de todo el ancho y el 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 la 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 las CNN, tomando la salida de una capa convolucional y reduciéndola para simplificar la representación.

La capa de agrupación logra esto al observar diferentes puntos en las salidas de la red y "agrupar" los valores cercanos, obteniendo un único valor que representa todos los valores cercanos. En otras palabras, toma una estadística resumen 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 mientras conserva la información relevante que permitirá a la red reconocer esa información y dibujar patrones significativos a partir 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 Max Pooling, donde se toma el valor máximo de la región y se utiliza para representar la vecindad.

La capa totalmente conectada es donde todas las neuronas están unidas entre sí, con conexiones entre cada capa anterior y posterior en la red. Aquí es donde se analiza la información 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 a través de 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 una no linealidad.

Hay una variedad de diferentes funciones de activación no lineal que se pueden usar 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 los valores reales a solo valores positivos por encima de 0. Dicho de otra manera, la función ReLu toma cualquier valor por encima de cero y lo devuelve tal cual, mientras que si el valor está por debajo de cero es devuelto como cero.

La función ReLu es popular debido a su confiabilidad y velocidad, ya que 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 cuando maneja 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 en 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 reduce los valores entre -1 y 1.

Capacitación 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 recibe 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" acerca de los datos que se han alimentado.

La fase de prueba es donde se evalúa lo que la red ha aprendido. La red recibe un nuevo conjunto de datos, uno que no ha visto antes, y luego se le pide a la red que aplique sus conjeturas sobre los patrones que ha aprendido a los nuevos datos. Se evalúa la precisión del modelo y, por lo general, el modelo se ajusta 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 volver a entrenar toda la red desde cero. Esto significa que la red se puede usar inmediatamente para realizar pruebas, o solo ciertas capas de la red se pueden ajustar y luego volver a entrenar. Esto acelera enormemente el despliegue de la red neuronal profunda.

¿Qué es el aprendizaje por transferencia?

La idea detrás de Transfer Learning 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 tiempo necesarios 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 tarea. 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.

Transferencia de la teoría del aprendizaje

La utilización del aprendizaje por transferencia tiene varios conceptos importantes. Para comprender la implementación del aprendizaje por transferencia, debemos repasar cómo se ve un modelo preentrenado y cómo se puede ajustar ese modelo para 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 mientras 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).

Hay una gran variedad de modelos preentrenados que se pueden usar en PyTorch. Algunas de las CNN preentrenadas incluyen:

  • AlexNet
  • CaffeResNet
  • Inicio
  • 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 fue desarrollado y entrenado en un conjunto de datos ImageNet, así como el CIFAR-10 conjunto de datos. Como tal, está optimizado para tareas de reconocimiento visual y mostró una notable mejora con respecto a la serie VGG, razón por la cual lo utilizaremos.

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

Como documentación de PyTorch sobre transferencia de aprendizaje explica, hay dos formas principales de usar el aprendizaje por transferencia: afinando una CNN o usando la CNN como un extractor de funciones fijas.

Cuando ajusta una CNN, usa los pesos que tiene la red preentrenada en lugar de inicializarlos aleatoriamente, y luego entrena como de costumbre. Por el contrario, un enfoque de extractor de características significa que mantendrá todos los pesos de la CNN, excepto aquellos en las últimas capas, que se inicializarán aleatoriamente y se entrenarán normalmente.

El ajuste fino de un modelo es importante porque aunque el modelo ha sido entrenado previamente, ha sido 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 probablemente querrá 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 transferencia de aprendizaje 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 el entrenamiento, para que sus pesos se puedan entrenar.

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

Clasificación de imágenes con Transfer Learning en PyTorch

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

Preprocesamiento de datos {#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 el conjunto. Puedes descargar este aquí mismo.

Asegúrese de dividir el conjunto de datos en dos conjuntos de igual tamaño: "tren" y "val".

Puede hacer esto de la forma que desee, moviendo manualmente los archivos o escribiendo una función para manejarlo. También es posible que 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 la validación. Sin embargo, la cantidad de imágenes que desea usar para el entrenamiento depende de usted.

Aquí hay una forma de preparar los datos para su uso:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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("5\d\d\d", f)
    if validationCatsSearchObj:
        shutil.move(f'{cat_train}/{f}', cat_val)

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

Cargando los datos

Una vez que hayamos seleccionado y preparado los datos, podemos comenzar importando todas las bibliotecas necesarias. Necesitaremos muchos de los paquetes Torch como la red neuronal nn, los optimizadores y los DataLoaders. También querremos matplotlib para visualizar algunos de nuestros ejemplos de entrenamiento.

Necesitamos numpy para manejar la creación de matrices de datos, así como algunos otros módulos misceláneos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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 use nuestra red neuronal. Vamos a hacer uso de las transformas de Pytorch para ese propósito. Tendremos que asegurarnos de que las imágenes en el conjunto de entrenamiento y el conjunto de validación tengan el 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 obligá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 funciona 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.

Luego componemos todas nuestras transformaciones elegidas. Tenga en cuenta que las transformaciones de validación no tienen ningún cambio ni rotación, ya que no forman parte de nuestro conjunto de entrenamiento, por lo que la red no está aprendiendo sobre ellas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 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 función ImageFolder de PyTorch para crear conjuntos de datos:

1
2
3
4
5
6
7
# 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 utilizar los cargadores de datos para crear objetos iterables para que trabajemos con nosotros. Le decimos qué conjuntos de datos queremos usar, le damos un tamaño de lote y mezclamos los datos.

1
2
3
4
# 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']}

Vamos a necesitar conservar cierta 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 debemos especificar con qué tipo de dispositivo estamos trabajando, una CPU o una GPU. La siguiente configuración usará GPU si está disponible; de ​​lo contrario, se usará CPU:

1
2
3
4
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 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 DataLoader y almacenarlos para su uso posterior. Luego, crearemos una cuadrícula para mostrar las entradas y mostrarlas:

1
2
3
4
5
6
7
# 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])

Configuración de un modelo preentrenado

Ahora tenemos que configurar el modelo preentrenado que queremos usar para transferir el aprendizaje. En este caso, vamos a usar el modelo tal como está y simplemente restablecer la capa final completamente conectada, proporcionándole nuestra cantidad de características y clases.

Cuando se usan modelos previamente entrenados, PyTorch establece que el modelo se descongele (tendrá sus pesos ajustados) de manera predeterminada. Así que estaremos entrenando todo el modelo:

1
2
3
4
5
6
7
# 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.

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

Esto es lo que devuelve:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
conv1
bn1
relu
maxpool
layer1
layer2
layer3
layer4
avgpool
fc

Observe que la parte final es fc, o "Fully-Connected". Esta es la única capa de la que estamos modificando la forma, dándole nuestras dos clases para generar.

Esencialmente, vamos a cambiar las salidas de la parte final completamente conectada a solo dos clases y ajustar los pesos para todas las demás capas.

Ahora necesitamos 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. CrossEntropyLoss y el optimizador SGD son buenas opciones, aunque hay muchas otras.

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

1
2
3
4
5
6
7
8
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, el criterio y el programador que elegimos. También especificaremos un número predeterminado de épocas de entrenamiento.

Cada época tendrá una fase de entrenamiento y validación. Para empezar, establecemos 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 lo siguiente:

  1. Disminuir la tasa de aprendizaje
  2. Cero los gradientes
  3. Realizar el pase de entrenamiento adelantado
  4. Calcular la pérdida
  5. Haz la propagación hacia atrás y actualiza los pesos con el optimizador

También haremos 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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 impresiones de entrenamiento deberían verse así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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 ha hecho nuestro modelo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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:

1
2
3
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. Todavía tomará algún tiempo incluso si usa una GPU.

Extractor de características fijas

Debido al largo tiempo de entrenamiento, muchas personas eligen simplemente usar el modelo preentrenado como un extractor de características fijas, y solo entrenan 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 al anterior, excepto que especificamos que los gradientes no necesitan cálculo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 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)

¿Qué pasaría si quisiéramos descongelar capas selectivamente y calcular los gradientes para solo unas pocas capas elegidas? ¿Es eso posible? Sí, lo es.

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

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

Aquí están las capas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
conv1
bn1
relu
maxpool
layer1
layer2
layer3
layer4
avgpool
fc

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

1
2
3
4
5
6
7
8
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.

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

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

Conclusión

Felicitaciones, ahora ha implementado el aprendizaje de 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 congelando y descongelando 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 puedes probar:

  • Usar diferentes modelos preentrenados 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 obtener más información 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 ellas, así como casos de uso. aquí.

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