Clasificación de imágenes utilizando redes neuronales convolucionales en PyTorch¶
Parte 5 de "Aprendizaje profundo con Pytorch: de cero a GAN"¶
Esta serie de tutoriales es una introducción práctica y sencilla para principiantes al aprendizaje profundo utilizando PyTorch, una biblioteca de redes neuronales de código abierto. Estos tutoriales adoptan un enfoque práctico y centrado en la codificación. La mejor manera de aprender el material es ejecutar el código y experimentar con él usted mismo. Mira la serie completa aquí:
- [Conceptos básicos de PyTorch: tensores y degradados] (https://jovian.ai/aakashns/01-pytorch-basics)
- Descenso de gradiente y regresión lineal
- Trabajar con imágenes y regresión logística
- Entrenamiento de redes neuronales profundas en una GPU
- [Clasificación de imágenes mediante redes neuronales convolucionales] (https://jovian.ai/aakashns/05-cifar10-cnn)
- Aumento de datos, regularización y ResNets
- Generación de imágenes mediante redes generativas adversarias
Este tutorial cubre los siguientes temas:
- Descarga de un conjunto de datos de imágenes desde la URL web
- Comprensión de las capas de convolución y agrupación.
- Creación de una red neuronal convolucional (CNN) usando PyTorch
- Entrenamiento de una CNN desde cero y seguimiento del rendimiento.
- Underfitting, overfitting y cómo superarlos
Cómo ejecutar el código¶
Este tutorial es un ejecutable Jupyter notebook alojado en Jovian. Puede ejecutar este tutorial y experimentar con los ejemplos de código de dos maneras: usando recursos gratuitos en línea (recomendado) o en su computadora.
Opción 1: Ejecutar usando recursos en línea gratuitos (1 clic, recomendado)¶
La forma más sencilla de comenzar a ejecutar el código es hacer clic en el botón Ejecutar en la parte superior de esta página y seleccionar Ejecutar en Colab. Google Colab es una plataforma en línea gratuita para ejecutar portátiles Jupyter utilizando la infraestructura en la nube de Google. También puede seleccionar "Ejecutar en Binder" o "Ejecutar en Kaggle" si tiene problemas al ejecutar el cuaderno en Google Colab.
Opción 2: ejecutar en su computadora localmente¶
Para ejecutar el código en su computadora localmente, deberá configurar Python, descargar el cuaderno e instalar las bibliotecas necesarias. Recomendamos utilizar la distribución Conda de Python. Haga clic en el botón Ejecutar en la parte superior de esta página, seleccione la opción Ejecutar localmente y siga las instrucciones.
Usando una GPU para un entrenamiento más rápido¶
Puede utilizar una Unidad de procesamiento de gráficos (GPU) para entrenar sus modelos más rápido si su plataforma de ejecución está conectada a una GPU fabricada por NVIDIA. Siga estas instrucciones para usar una GPU en la plataforma de su elección:
- Google Colab: utilice la opción de menú "Tiempo de ejecución > Cambiar tipo de tiempo de ejecución" y seleccione "GPU" en el menú desplegable "Acelerador de hardware".
- Kaggle: En la sección "Configuración" de la barra lateral, seleccione "GPU" en el menú desplegable "Acelerador". Utilice el botón en la parte superior derecha para abrir la barra lateral.
- Binder: Las computadoras portátiles que ejecutan Binder no pueden usar una GPU, ya que las máquinas que alimentan Binder no están conectadas a ninguna GPU.
- Linux: Si su computadora portátil/escritorio tiene una GPU (tarjeta gráfica) NVIDIA, asegúrese de haber instalado los [controladores NVIDIA CUDA] (https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index .html).
- Windows: si su computadora portátil/escritorio tiene una GPU (tarjeta gráfica) NVIDIA, asegúrese de haber instalado los [controladores NVIDIA CUDA] (https://docs.nvidia.com/cuda/cuda-installation-guide-microsoft-windows /index.html).
- macOS: macOS no es compatible con las GPU NVIDIA
Si no tiene acceso a una GPU o no está seguro de cuál es, no se preocupe, puede ejecutar todo el código de este tutorial sin una GPU.
Explorando el conjunto de datos CIFAR10¶
En el tutorial anterior, entrenamos redes neuronales feedfoward con una única capa oculta para clasificar dígitos escritos a mano del [conjunto de datos MNIST](http:// yann.lecun.com/exdb/mnist) con más del 97% de precisión. Para este tutorial, usaremos el conjunto de datos CIFAR10, que consta de 60000 imágenes en color de 32x32 px en 10 clases. Aquí hay algunas imágenes de muestra del conjunto de datos:
# Descomente y ejecute el comando apropiado para su sistema operativo, si es necesario
# Linux/Binder/Windows (sin GPU)
# !pip install numpy matplotlib torch==1.7.0+cpu torchvision==0.8.1+cpu torchaudio==0.7.0 -f https://download.pytorch.org/whl/torch_stable.html
# Linux/Windows (GPU)
# pip install torch==1.7.1+cu110 torchvision==0.8.2+cu110 torchaudio==0.7.2 -f https://download.pytorch.org/whl/torch_stable.html
# MacOS (NO GPU)
# !pip instalar numpy matplotlib antorcha torchvision torchaudio
import os
import torch
import torchvision
import tarfile
from torchvision.datasets.utils import download_url
from torch.utils.data import random_split
project_name='05-cifar10-cnn'
Descargaremos las imágenes en formato PNG desde esta página, usando algunas funciones auxiliares de los paquetes torchvision y tarfile.
# Descargar el conjunto de datos
dataset_url = "https://s3.amazonaws.com/fast-ai-imageclas/cifar10.tgz"
download_url(dataset_url, '.')
# Extraer del archivo
with tarfile.open('./cifar10.tgz', 'r:gz') as tar:
tar.extractall(path='./data')
El conjunto de datos se extrae al directorio data/cifar10. Contiene 2 carpetas "train" y "test", que contienen el conjunto de entrenamiento (50000 imágenes) y el conjunto de prueba (10000 imágenes) respectivamente. Cada uno de ellos contiene 10 carpetas, una para cada clase de imágenes. Verifiquemos esto usando os.listdir.
data_dir = './data/cifar10'
print(os.listdir(data_dir))
classes = os.listdir(data_dir + "/train")
print(classes)
Miremos dentro de un par de carpetas, una del conjunto de entrenamiento y otra del conjunto de prueba. Como ejercicio, puedes comprobar que hay el mismo número de imágenes para cada clase, 5000 en el conjunto de entrenamiento y 1000 en el conjunto de prueba.
airplane_files = os.listdir(data_dir + "/train/airplane")
print('No. of training examples for airplanes:', len(airplane_files))
print(airplane_files[:5])
ship_test_files = os.listdir(data_dir + "/test/ship")
print("No. of test examples for ship:", len(ship_test_files))
print(ship_test_files[:5])
Muchos conjuntos de datos de visión por computadora utilizan la estructura de directorios anterior (una carpeta por clase), y la mayoría de las bibliotecas de aprendizaje profundo proporcionan utilidades para trabajar con dichos conjuntos de datos. Podemos usar la clase ImageFolder de torchvision para cargar los datos como tensores de PyTorch.
from torchvision.datasets import ImageFolder
from torchvision.transforms import ToTensor
dataset = ImageFolder(data_dir+'/train', transform=ToTensor())
Veamos un elemento de muestra del conjunto de datos de entrenamiento. Cada elemento es una tupla que contiene un tensor de imagen y una etiqueta. Dado que los datos constan de imágenes en color de 32x32 px con 3 canales (RGB), cada tensor de imagen tiene la forma "(3, 32, 32)".
img, label = dataset[0]
print(img.shape, label)
img
La lista de clases se almacena en la propiedad .classes del conjunto de datos. La etiqueta numérica de cada elemento corresponde al índice de la etiqueta del elemento en la lista de clases.
print(dataset.classes)
Podemos ver la imagen usando matplotlib, pero necesitamos cambiar las dimensiones del tensor a (32,32,3). Creemos una función auxiliar para mostrar una imagen y su etiqueta.
import matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
matplotlib.rcParams['figure.facecolor'] = '#ffffff'
def show_example(img, label):
print('Label: ', dataset.classes[label], "("+str(label)+")")
plt.imshow(img.permute(1, 2, 0))
Veamos un par de imágenes del conjunto de datos. Como puedes ver, las imágenes de 32x32px son bastante difíciles de identificar, incluso para el ojo humano. Intente cambiar los índices a continuación para ver imágenes diferentes.
show_example(*dataset[0])
show_example(*dataset[1099])
Guarda y sube tu libreta¶
Ya sea que esté ejecutando este cuaderno Jupyter en línea o en su computadora, es esencial guardar su trabajo de vez en cuando. Puede continuar trabajando en un cuaderno guardado más tarde o compartirlo con amigos y colegas para permitirles ejecutar su código. Jovian ofrece una forma sencilla de guardar y compartir sus cuadernos de Jupyter en línea.
!pip install jovian --upgrade -q
import jovian
jovian.commit(project=project_name)
jovian.commit carga el cuaderno en su cuenta Jovian, captura el entorno Python y crea un enlace para compartir para su cuaderno, como se muestra arriba. Puede utilizar este enlace para compartir su trabajo y permitir que cualquiera (incluido usted) ejecute sus cuadernos y reproduzca su trabajo.
Conjuntos de datos de capacitación y validación¶
Al crear modelos de aprendizaje automático del mundo real, es bastante común dividir el conjunto de datos en 3 partes:
- Conjunto de entrenamiento: se utiliza para entrenar el modelo, es decir, calcular la pérdida y ajustar los pesos del modelo mediante el descenso de gradiente.
- Conjunto de validación: se utiliza para evaluar el modelo durante el entrenamiento, ajustar los hiperparámetros (tasa de aprendizaje, etc.) y elegir la mejor versión del modelo.
- Conjunto de pruebas: se utiliza para comparar diferentes modelos o diferentes tipos de enfoques de modelado e informar la precisión final del modelo.
Dado que no hay un conjunto de validación predefinido, podemos reservar una pequeña porción (5000 imágenes) del conjunto de entrenamiento para usarla como conjunto de validación. Usaremos el método auxiliar random_split de PyTorch para hacer esto. Para garantizar que siempre creemos el mismo conjunto de validación, también estableceremos una semilla para el generador de números aleatorios.
random_seed = 42
torch.manual_seed(random_seed);
val_size = 5000
train_size = len(dataset) - val_size
train_ds, val_ds = random_split(dataset, [train_size, val_size])
len(train_ds), len(val_ds)
La biblioteca "jovian" también proporciona una API sencilla para registrar parámetros importantes relacionados con el conjunto de datos, el entrenamiento del modelo, los resultados, etc. para facilitar la referencia y comparación entre múltiples experimentos. Registremos dataset_url, val_pct y rand_seed usando jovian.log_dataset.
jovian.log_dataset(dataset_url=dataset_url, val_size=val_size, random_seed=random_seed)
Ahora podemos crear cargadores de datos para entrenamiento y validación, para cargar los datos en lotes.
from torch.utils.data.dataloader import DataLoader
batch_size=128
train_dl = DataLoader(train_ds, batch_size, shuffle=True, num_workers=4, pin_memory=True)
val_dl = DataLoader(val_ds, batch_size*2, num_workers=4, pin_memory=True)
Podemos ver lotes de imágenes del conjunto de datos usando el método make_grid de torchvision. Cada vez que se ejecuta el siguiente código, obtenemos un bach diferente, ya que el muestreador mezcla los índices antes de crear lotes.
from torchvision.utils import make_grid
def show_batch(dl):
for images, labels in dl:
fig, ax = plt.subplots(figsize=(12, 6))
ax.set_xticks([]); ax.set_yticks([])
ax.imshow(make_grid(images, nrow=16).permute(1, 2, 0))
break
show_batch(train_dl)
Una vez más, guardemos y confirmemos nuestro trabajo usando "jovian" antes de continuar.
jovian.commit(project=project_name, environment=None)
Después de la primera confirmación, todas las confirmaciones posteriores registran una nueva versión del cuaderno dentro del mismo proyecto joviano. Puede usar jovian.commit para versionar los cuadernos de Jupyter (en lugar de hacer Archivo > Guardar como) y mantener organizados sus proyectos de ciencia de datos. Consulte también la pestaña Records en la página del proyecto para ver cómo se registra la información usando jovian.log_dataset aparece en la interfaz de usuario.
Definición del modelo (red neuronal convolucional)¶
En nuestro [tutorial anterior] (https://jovian.ml/aakashns/04-feedforward-nn), definimos una red neuronal profunda con capas completamente conectadas usando nn.Linear. Sin embargo, para este tutorial usaremos una red neuronal convolucional, utilizando la clase nn.Conv2d de PyTorch.
La convolución 2D es una operación bastante simple en el fondo: se comienza con un núcleo, que es simplemente una pequeña matriz de pesos. Este núcleo se "desliza" sobre los datos de entrada 2D, realiza una multiplicación por elementos con la parte de la entrada en la que se encuentra actualmente y luego resume los resultados en un solo píxel de salida. - Fuente
Implementemos una operación de convolución en una imagen de 1 canal con un núcleo de 3x3.
def apply_kernel(image, kernel):
ri, ci = image.shape # image dimensions
rk, ck = kernel.shape # kernel dimensions
ro, co = ri-rk+1, ci-ck+1 # output dimensions
output = torch.zeros([ro, co])
for i in range(ro):
for j in range(co):
output[i,j] = torch.sum(image[i:i+rk,j:j+ck] * kernel)
return output
sample_image = torch.tensor([
[3, 3, 2, 1, 0],
[0, 0, 1, 3, 1],
[3, 1, 2, 2, 3],
[2, 0, 0, 2, 2],
[2, 0, 0, 0, 1]
], dtype=torch.float32)
sample_kernel = torch.tensor([
[0, 1, 2],
[2, 2, 0],
[0, 1, 2]
], dtype=torch.float32)
apply_kernel(sample_image, sample_kernel)
Para imágenes multicanal, se aplica un núcleo diferente a cada canal y las salidas se suman por píxeles.
Consulte los siguientes artículos para comprender mejor las convoluciones:
- [Comprensión intuitiva de las convoluciones para el aprendizaje profundo] (https://towardsdatascience.com/intuitively-understanding-convolutions-for-deep-learning-1f6f42faee1) por Irhum Shafkat
- [Convoluciones en profundidad] (https://sgugger.github.io/convolution-in- Depth.html) de Sylvian Gugger (este artículo implementa convoluciones desde cero)
Existen ciertas ventajas que ofrecen las capas convolucionales cuando se trabaja con datos de imágenes:
- Menos parámetros: se utiliza un pequeño conjunto de parámetros (el núcleo) para calcular los resultados de toda la imagen, por lo que el modelo tiene muchos menos parámetros en comparación con una capa completamente conectada.
- Escasez de conexiones: en cada capa, cada elemento de salida solo depende de una pequeña cantidad de elementos de entrada, lo que hace que los pases hacia adelante y hacia atrás sean más eficientes.
- Compartición de parámetros e invariancia espacial: las características aprendidas por un núcleo en una parte de la imagen se pueden usar para detectar patrones similares en una parte diferente de otra imagen.
También usaremos capas de max-pooling para disminuir progresivamente la altura y el ancho de los tensores de salida de cada capa convolucional.
Antes de definir el modelo completo, veamos cómo opera en los datos una única capa convolucional seguida de una capa de agrupación máxima.
import torch.nn as nn
import torch.nn.functional as F
simple_model = nn.Sequential(
nn.Conv2d(3, 8, kernel_size=3, stride=1, padding=1),
nn.MaxPool2d(2, 2)
)
Consulte la [publicación de Sylvian] (https://sgugger.github.io/convolution-in- Depth.html) para obtener una explicación de kernel_size, stride y padding.
for images, labels in train_dl:
print('images.shape:', images.shape)
out = simple_model(images)
print('out.shape:', out.shape)
break
La capa Conv2d transforma una imagen de 3 canales en un mapa de características de 16 canales, y la capa MaxPool2d reduce a la mitad la altura y el ancho. El mapa de características se hace más pequeño a medida que agregamos más capas, hasta que finalmente nos queda un mapa de características pequeño, que se puede aplanar en un vector. Luego podemos agregar algunas capas completamente conectadas al final para obtener un vector de tamaño 10 para cada imagen.
Definamos el modelo extendiendo una clase ImageClassificationBase que contiene métodos auxiliares para entrenamiento y validación.
class ImageClassificationBase(nn.Module):
def training_step(self, batch):
images, labels = batch
out = self(images) # Generate predictions
loss = F.cross_entropy(out, labels) # Calculate loss
return loss
def validation_step(self, batch):
images, labels = batch
out = self(images) # Generate predictions
loss = F.cross_entropy(out, labels) # Calculate loss
acc = accuracy(out, labels) # Calculate accuracy
return {'val_loss': loss.detach(), 'val_acc': acc}
def validation_epoch_end(self, outputs):
batch_losses = [x['val_loss'] for x in outputs]
epoch_loss = torch.stack(batch_losses).mean() # Combine losses
batch_accs = [x['val_acc'] for x in outputs]
epoch_acc = torch.stack(batch_accs).mean() # Combine accuracies
return {'val_loss': epoch_loss.item(), 'val_acc': epoch_acc.item()}
def epoch_end(self, epoch, result):
print("Epoch [{}], train_loss: {:.4f}, val_loss: {:.4f}, val_acc: {:.4f}".format(
epoch, result['train_loss'], result['val_loss'], result['val_acc']))
def accuracy(outputs, labels):
_, preds = torch.max(outputs, dim=1)
return torch.tensor(torch.sum(preds == labels).item() / len(preds))
Usaremos nn.Sequential para encadenar las capas y funciones de activación en una única arquitectura de red.
class Cifar10CnnModel(ImageClassificationBase):
def __init__(self):
super().__init__()
self.network = nn.Sequential(
nn.Conv2d(3, 32, kernel_size=3, padding=1),
nn.ReLU(),
nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.MaxPool2d(2, 2), # output: 64 x 16 x 16
nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.MaxPool2d(2, 2), # output: 128 x 8 x 8
nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.MaxPool2d(2, 2), # output: 256 x 4 x 4
nn.Flatten(),
nn.Linear(256*4*4, 1024),
nn.ReLU(),
nn.Linear(1024, 512),
nn.ReLU(),
nn.Linear(512, 10))
def forward(self, xb):
return self.network(xb)
model = Cifar10CnnModel()
model
Verifiquemos que el modelo produzca el resultado esperado en un lote de datos de entrenamiento. Las 10 salidas para cada imagen se pueden interpretar como probabilidades para las 10 clases objetivo (después de aplicar softmax), y la clase con la probabilidad más alta se elige como la etiqueta predicha por el modelo para la imagen de entrada. Consulte la Parte 3 (regresión logística) para obtener una discusión más detallada sobre cómo interpretar los resultados, aplicar softmax e identificar las etiquetas predichas.
for images, labels in train_dl:
print('images.shape:', images.shape)
out = model(images)
print('out.shape:', out.shape)
print('out[0]:', out[0])
break
Para usar sin problemas una GPU, si hay una disponible, definimos un par de funciones auxiliares (get_default_device y to_device) y una clase auxiliar DeviceDataLoader para mover nuestro modelo y datos a la GPU según sea necesario. Estos se describen con más detalle en el tutorial anterior.
def get_default_device():
"""Pick GPU if available, else CPU"""
if torch.cuda.is_available():
return torch.device('cuda')
else:
return torch.device('cpu')
def to_device(data, device):
"""Move tensor(s) to chosen device"""
if isinstance(data, (list,tuple)):
return [to_device(x, device) for x in data]
return data.to(device, non_blocking=True)
class DeviceDataLoader():
"""Wrap a dataloader to move data to a device"""
def __init__(self, dl, device):
self.dl = dl
self.device = device
def __iter__(self):
"""Yield a batch of data after moving it to device"""
for b in self.dl:
yield to_device(b, self.device)
def __len__(self):
"""Number of batches"""
return len(self.dl)
Según dónde esté ejecutando esta computadora portátil, su dispositivo predeterminado podría ser una CPU (torch.device('cpu')) o una GPU (torch.device('cuda'))
device = get_default_device()
device
Ahora podemos empaquetar nuestros cargadores de datos de entrenamiento y validación usando DeviceDataLoader para transferir automáticamente lotes de datos a la GPU (si está disponible) y usar to_device para mover nuestro modelo a la GPU (si está disponible).
train_dl = DeviceDataLoader(train_dl, device)
val_dl = DeviceDataLoader(val_dl, device)
to_device(model, device);
Una vez más, guardemos y confirmemos el cuaderno antes de continuar.
jovian.commit(project=project_name)
Entrenando el modelo¶
Definiremos dos funciones: "ajustar" y "evaluar" para entrenar el modelo usando el descenso de gradiente y evaluar su desempeño en el conjunto de validación. Para obtener un tutorial detallado de estas funciones, consulte el tutorial anterior.
@torch.no_grad()
def evaluate(model, val_loader):
model.eval()
outputs = [model.validation_step(batch) for batch in val_loader]
return model.validation_epoch_end(outputs)
def fit(epochs, lr, model, train_loader, val_loader, opt_func=torch.optim.SGD):
history = []
optimizer = opt_func(model.parameters(), lr)
for epoch in range(epochs):
# Training Phase
model.train()
train_losses = []
for batch in train_loader:
loss = model.training_step(batch)
train_losses.append(loss)
loss.backward()
optimizer.step()
optimizer.zero_grad()
# Validation phase
result = evaluate(model, val_loader)
result['train_loss'] = torch.stack(train_losses).mean().item()
model.epoch_end(epoch, result)
history.append(result)
return history
Antes de comenzar a entrenar, creemos una instancia del modelo una vez más y veamos cómo se desempeña en el conjunto de validación con el conjunto inicial de parámetros.
model = to_device(Cifar10CnnModel(), device)
evaluate(model, val_dl)
La precisión inicial es de alrededor del 10%, que es lo que uno podría esperar de un modelo inicializado aleatoriamente (ya que tiene una probabilidad de 1 entre 10 de obtener una etiqueta correcta al adivinar al azar).
Usaremos los siguientes hiperparámetros (tasa de aprendizaje, número de épocas, tamaño de lote, etc.) para entrenar nuestro modelo. Como ejercicio, puedes intentar cambiarlos para ver si logras una mayor precisión en menos tiempo.
num_epochs = 10
opt_func = torch.optim.Adam
lr = 0.001
Es importante registrar los hiperparámetros de cada experimento que realice, para replicarlo más tarde y compararlo con otros experimentos. Podemos grabarlos usando jovian.log_hyperparams.
jovian.reset()
jovian.log_hyperparams({
'num_epochs': num_epochs,
'opt_func': opt_func.__name__,
'batch_size': batch_size,
'lr': lr,
})
history = fit(num_epochs, lr, model, train_dl, val_dl, opt_func)
Así como hemos registrado los hiperparámetros, también podemos registrar las métricas finales logradas por el modelo usando jovian.log_metrics como referencia, análisis y comparación.
jovian.log_metrics(train_loss=history[-1]['train_loss'],
val_loss=history[-1]['val_loss'],
val_acc=history[-1]['val_acc'])
También podemos trazar las precisiones del conjunto de validación para estudiar cómo mejora el modelo con el tiempo.
def plot_accuracies(history):
accuracies = [x['val_acc'] for x in history]
plt.plot(accuracies, '-x')
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.title('Accuracy vs. No. of epochs');
plot_accuracies(history)
Nuestro modelo alcanza una precisión de alrededor del 75% y, al observar el gráfico, parece poco probable que el modelo alcance una precisión superior al 80% incluso después de un entrenamiento prolongado. Esto sugiere que es posible que necesitemos utilizar un modelo más potente para capturar la relación entre las imágenes y las etiquetas con mayor precisión. Esto se puede hacer agregando más capas convolucionales a nuestro modelo o aumentando el número. de canales en cada capa convolucional, o mediante el uso de técnicas de regularización.
También podemos trazar las pérdidas de entrenamiento y validación para estudiar la tendencia.
def plot_losses(history):
train_losses = [x.get('train_loss') for x in history]
val_losses = [x['val_loss'] for x in history]
plt.plot(train_losses, '-bx')
plt.plot(val_losses, '-rx')
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(['Training', 'Validation'])
plt.title('Loss vs. No. of epochs');
plot_losses(history)
Inicialmente, tanto las pérdidas de formación como las de validación parecen disminuir con el tiempo. Sin embargo, si entrena el modelo durante el tiempo suficiente, notará que la pérdida de entrenamiento continúa disminuyendo, mientras que la pérdida de validación deja de disminuir e incluso comienza a aumentar después de cierto punto.
Este fenómeno se llama sobreajuste y es el no. 1 por qué muchos modelos de aprendizaje automático dan resultados bastante terribles con datos del mundo real. Esto sucede porque el modelo, en un intento por minimizar la pérdida, comienza a aprender patrones que son exclusivos de los datos de entrenamiento, a veces incluso memorizando ejemplos de entrenamiento específicos. Debido a esto, el modelo no se generaliza bien a datos nunca antes vistos.
A continuación se presentan algunas estrategias comunes para evitar el sobreajuste:
- Recopilar y generar más datos de entrenamiento o agregarles ruido.
- Uso de técnicas de regularización como normalización y abandono por lotes.
- Detención anticipada del entrenamiento del modelo, cuando la pérdida de validación comienza a aumentar
Cubriremos estos temas con más detalle en el próximo tutorial de esta serie y aprenderemos cómo podemos alcanzar una precisión de más del 90 % realizando cambios menores pero importantes en nuestro modelo.
Antes de continuar, guardemos nuestro trabajo en la nube usando jovian.commit.
jovian.commit(project=project_name)
Cuando prueba diferentes experimentos (cambiando la tasa de aprendizaje, el tamaño del lote, el optimizador, etc.) y registra hiperparámetros y métricas con cada versión de su computadora portátil, puede usar [Comparar](https://jovian.ml /aakshns/05-cifar10-cnn/compare) en la página del proyecto para analizar qué enfoques están funcionando bien y cuáles no. Usted ordena/filtra por precisión, pérdida, etc., agrega notas para cada versión e incluso invita a colaboradores a contribuir a su proyecto con sus propios experimentos.
Pruebas con imágenes individuales¶
Si bien hasta ahora hemos estado rastreando la precisión general de un modelo, también es una buena idea observar los resultados del modelo en algunas imágenes de muestra. Probemos nuestro modelo con algunas imágenes del conjunto de datos de prueba predefinido de 10000 imágenes. Comenzamos creando un conjunto de datos de prueba usando la clase ImageFolder.
test_dataset = ImageFolder(data_dir+'/test', transform=ToTensor())
Definamos una función auxiliar predict_image, que devuelve la etiqueta predicha para un tensor de imagen único.
def predict_image(img, model):
# Convert to a batch of 1
xb = to_device(img.unsqueeze(0), device)
# Get predictions from model
yb = model(xb)
# Pick index with highest probability
_, preds = torch.max(yb, dim=1)
# Retrieve the class label
return dataset.classes[preds[0].item()]
img, label = test_dataset[0]
plt.imshow(img.permute(1, 2, 0))
print('Label:', dataset.classes[label], ', Predicted:', predict_image(img, model))
img, label = test_dataset[1002]
plt.imshow(img.permute(1, 2, 0))
print('Label:', dataset.classes[label], ', Predicted:', predict_image(img, model))
img, label = test_dataset[6153]
plt.imshow(img.permute(1, 2, 0))
print('Label:', dataset.classes[label], ', Predicted:', predict_image(img, model))
Identificar dónde nuestro modelo funciona mal puede ayudarnos a mejorarlo, recopilando más datos de entrenamiento, aumentando/disminuyendo la complejidad del modelo y cambiando los hiperparámetros.
Como paso final, observemos también la pérdida y precisión general del modelo en el conjunto de prueba y registremos usando "joviano". Esperamos que estos valores sean similares a los del conjunto de validación. De lo contrario, es posible que necesitemos un mejor conjunto de validación que tenga datos y distribución similares a los del conjunto de prueba (que a menudo proviene de datos del mundo real).
test_loader = DeviceDataLoader(DataLoader(test_dataset, batch_size*2), device)
result = evaluate(model, test_loader)
result
jovian.log_metrics(test_loss=result['val_loss'], test_acc=result['val_acc'])
Guardando y cargando el modelo¶
Dado que hemos entrenado nuestro modelo durante mucho tiempo y logramos una precisión razonable, sería una buena idea guardar los pesos del modelo en el disco, para que podamos reutilizar el modelo más adelante y evitar volver a entrenar desde cero. Así es como puedes guardar el modelo.
torch.save(model.state_dict(), 'cifar10-cnn.pth')
El método .state_dict devuelve un OrderedDict que contiene todos los pesos y matrices de sesgo asignados a los atributos correctos del modelo. Para cargar los pesos del modelo, podemos redefinir el modelo con la misma estructura y usar el método .load_state_dict.
model2 = to_device(Cifar10CnnModel(), device)
model2.load_state_dict(torch.load('cifar10-cnn.pth'))
Solo como control de cordura, verifiquemos que este modelo tenga la misma pérdida y precisión en el conjunto de prueba que antes.
evaluate(model2, test_loader)
Hagamos una confirmación final usando "jovian".
jovian.commit(project=project_name)
Consulte la pestaña Archivos en la página del proyecto para ver o descargar los pesos del modelo entrenado. También puedes descargar todos los archivos juntos usando la opción Descargar Zip en el menú desplegable Clonar.
El trabajo de ciencia de datos a menudo está fragmentado en muchas plataformas diferentes (Git para código, Dropbox/S3 para conjuntos de datos y artefactos, hojas de cálculo para hiperparámetros, métricas, etc.), lo que puede dificultar compartir y reproducir experimentos. Jovian.ml resuelve esto capturando todo lo relacionado con un proyecto de ciencia de datos en una única plataforma, al tiempo que proporciona un flujo de trabajo perfecto para capturar, compartir y reproducir su trabajo. Para saber qué puede hacer con Jovian.ml, consulte los documentos: https://docs.jovian.ml.
Resumen y lecturas adicionales/ejercicios¶
Hemos cubierto mucho terreno en este tutorial. Aquí hay un resumen rápido de los temas:
- Introducción al conjunto de datos CIFAR10 para clasificación de imágenes.
- Descargar, extraer y cargar un conjunto de datos de imágenes usando
torchvision - Mostrar lotes aleatorios de imágenes en una cuadrícula usando
torchvision.utils.make_grid - Creación de una red neuronal convolucional usando las capas
nn.Conv2dynn.MaxPool2d - Captura de información del conjunto de datos, métricas e hiperparámetros utilizando la biblioteca "joviana".
- Entrenar una red neuronal convolucional y visualizar las pérdidas y errores.
- Comprender el sobreajuste y las estrategias para evitarlo (más sobre esto más adelante)
- Generar predicciones sobre imágenes individuales del conjunto de prueba.
- Guardar y cargar los pesos del modelo y adjuntarlos a la instantánea del experimento usando
jovian
Hay muchas posibilidades para experimentar aquí y le recomiendo que utilice la naturaleza interactiva de Jupyter para jugar con los distintos parámetros. Aqui hay algunas ideas:
- Intente cambiar los hiperparámetros para lograr una mayor precisión en menos épocas. Utilice la tabla de comparación en la página del proyecto Jovian.ml para comparar sus experimentos.
- Intente agregar más capas convolucionales o aumentar la cantidad de canales en cada capa convolucional
- Intente utilizar una red neuronal de avance y vea cuál es la máxima precisión que puede lograr
- Lea acerca de algunas de las estrategias mencionadas anteriormente para reducir el sobreajuste y lograr mejores resultados, e intente implementarlas consultando los documentos de PyTorch.
- Modifique este cuaderno para entrenar un modelo para un conjunto de datos diferente (por ejemplo, CIFAR100 o ImageNet)
En el próximo tutorial, continuaremos mejorando la precisión de nuestro modelo utilizando técnicas como aumento de datos, normalización por lotes y abandono. También aprenderemos sobre las redes residuales (o ResNets), un cambio pequeño pero crítico en la arquitectura del modelo que aumentará significativamente el rendimiento de nuestro modelo. ¡Manténganse al tanto!
>a>