Entrenamiento de redes neuronales profundas en una GPU con PyTorch¶
Parte 4 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:
- Creando una red neuronal profunda con capas ocultas.
- Usando una función de activación no lineal
- Usar una GPU (cuando esté disponible) para acelerar el entrenamiento
- Experimentar con hiperparámetros para mejorar el modelo.
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.
Jupyter Notebooks: este tutorial es un Jupyter notebook: un documento compuesto de celdas. Cada celda puede contener código escrito en Python o explicaciones en inglés sencillo. Puede ejecutar celdas de código y ver los resultados, por ejemplo, números, mensajes, gráficos, tablas, archivos, etc., instantáneamente dentro del cuaderno. Jupyter es una poderosa plataforma para la experimentación y el análisis. No tengas miedo de trastear con el código y romper cosas: aprenderás mucho encontrando y corrigiendo errores. Puede utilizar la opción de menú "Kernel > Reiniciar y borrar salida" o "Editar > Borrar salidas" para borrar todas las salidas y comenzar de nuevo desde arriba.
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.
Preparando los datos¶
En [el tutorial anterior] (https://jovian.ai/aakashns/03-logistic-regression), entrenamos un modelo de regresión logística para identificar dígitos escritos a mano del conjunto de datos MNIST con una precisión de alrededor del 86%. El conjunto de datos consta de imágenes en escala de grises de 28 px por 28 px de dígitos escritos a mano (0 a 9) y etiquetas para cada imagen que indican qué dígito representa. Aquí hay algunas imágenes de muestra del conjunto de datos:

Notamos que es bastante difícil mejorar la precisión de un modelo de regresión logística más allá del 87%, ya que el modelo supone una relación lineal entre las intensidades de los píxeles y las etiquetas de las imágenes. En esta publicación, intentaremos mejorarlo utilizando una red neuronal de retroalimentación que puede capturar relaciones no lineales entre entradas y objetivos.
Comencemos instalando e importando los módulos y clases necesarios de torch, torchvision, numpy y matplotlib.
# Descomente y ejecute el comando apropiado para su sistema operativo, si es necesario
# Linux / Carpeta
# !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
# ventanas
# !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
# Mac OS
# !pip instalar numpy matplotlib antorcha torchvision torchaudio
import torch
import torchvision
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import torch.nn as nn
import torch.nn.functional as F
from torchvision.datasets import MNIST
from torchvision.transforms import ToTensor
from torchvision.utils import make_grid
from torch.utils.data.dataloader import DataLoader
from torch.utils.data import random_split
%matplotlib inline
# Utilice un fondo blanco para las figuras matplotlib.
matplotlib.rcParams['figure.facecolor'] = '#ffffff'
Podemos descargar los datos y crear un conjunto de datos de PyTorch usando la clase MNIST de torchvision.datasets.
dataset = MNIST(root='data/', download=True, transform=ToTensor())
Veamos un par de imágenes del conjunto de datos. Las imágenes se convierten a tensores de PyTorch con la forma "1x28x28" (las dimensiones representan canales de color, ancho y alto). Podemos usar plt.imshow para mostrar las imágenes. Sin embargo, plt.imshow espera que los canales sean la última dimensión en un tensor de imagen, por lo que usamos el método permute para reordenar las dimensiones de la imagen.
image, label = dataset[0]
print('image.shape:', image.shape)
plt.imshow(image.permute(1, 2, 0), cmap='gray')
print('Label:', label)
image, label = dataset[0]
print('image.shape:', image.shape)
plt.imshow(image.permute(1, 2, 0), cmap='gray')
print('Label:', label)
A continuación, usemos la función auxiliar random_split para reservar 10000 imágenes para nuestro conjunto de validación.
val_size = 10000
train_size = len(dataset) - val_size
train_ds, val_ds = random_split(dataset, [train_size, val_size])
len(train_ds), len(val_ds)
Ahora podemos crear cargadores de datos PyTorch para entrenamiento y validación.
batch_size=128
train_loader = DataLoader(train_ds, batch_size, shuffle=True, num_workers=4, pin_memory=True)
val_loader = DataLoader(val_ds, batch_size*2, num_workers=4, pin_memory=True)
¿Puedes descubrir el propósito de los argumentos num_workers y pin_memory? Intente consultar la documentación: https://pytorch.org/docs/stable/data.html.
Visualicemos un lote de datos en una cuadrícula usando la función make_grid de torchvision. También usaremos el método .permute en el tensor para mover los canales a la última dimensión, como lo esperaba matplotlib.
for images, _ in train_loader:
print('images.shape:', images.shape)
plt.figure(figsize=(16,8))
plt.axis('off')
plt.imshow(make_grid(images, nrow=16).permute((1, 2, 0)))
break
Capas ocultas, funciones de activación y no linealidad¶
Crearemos una red neuronal con dos capas: una capa oculta y una capa de salida. Además, usaremos una función de activación entre las dos capas. Veamos un ejemplo paso a paso para aprender cómo las capas ocultas y las funciones de activación pueden ayudar a capturar relaciones no lineales entre entradas y salidas.
Primero, creemos un lote de tensores de entrada. Aplanaremos las imágenes 1x28x28 en vectores de tamaño 784, para que puedan pasarse a un objeto nn.Linear.
for images, labels in train_loader:
print('images.shape:', images.shape)
inputs = images.reshape(-1, 784)
print('inputs.shape:', inputs.shape)
break
A continuación, creemos un objeto nn.Linear, que servirá como nuestra capa oculta. Estableceremos el tamaño de la salida de la capa oculta en 32. Este número se puede aumentar o disminuir para cambiar la capacidad de aprendizaje del modelo.
input_size = inputs.shape[-1]
hidden_size = 32
layer1 = nn.Linear(input_size, hidden_size)
Ahora podemos calcular salidas intermedias para el lote de imágenes pasando "entradas" a través de "capa1".
inputs.shape
layer1_outputs = layer1(inputs)
print('layer1_outputs.shape:', layer1_outputs.shape)
Los vectores de imagen de tamaño 784 se transforman en vectores de salida intermedios de longitud 32 realizando una multiplicación matricial de la matriz de inputs con la matriz de pesos transpuesta de layer1 y agregando el sesgo. Podemos verificar esto usando torch.allclose. Para obtener una explicación más detallada, revise el tutorial sobre [regresión lineal] (https://jovian.ai/aakshns/02-linear-regression).
layer1_outputs_direct = inputs @ layer1.weight.t() + layer1.bias
layer1_outputs_direct.shape
torch.allclose(layer1_outputs, layer1_outputs_direct, 1e-3)
Por lo tanto, "layer1_outputs" y "inputs" tienen una relación lineal, es decir, cada elemento de "layer_outputs" es una suma ponderada de elementos de "inputs". Por lo tanto, incluso cuando entrenamos el modelo y modificamos los pesos, la "capa1" solo puede capturar relaciones lineales entre las "entradas" y las "salidas".
A continuación, usaremos la función Unidad lineal rectificada (ReLU) como función de activación para las salidas. Tiene la fórmula relu(x) = max(0,x), es decir, simplemente reemplaza los valores negativos en un tensor dado con el valor 0. ReLU es una función no lineal, como se ve aquí visualmente:
Podemos usar el método F.relu para aplicar ReLU a los elementos de un tensor.
F.relu(torch.tensor([[1, -1, 0],
[-0.1, .2, 3]]))
Apliquemos la función de activación a layer1_outputs y verifiquemos que los valores negativos fueron reemplazados por 0.
relu_outputs = F.relu(layer1_outputs)
print('min(layer1_outputs):', torch.min(layer1_outputs).item())
print('min(relu_outputs):', torch.min(relu_outputs).item())
Ahora que hemos aplicado una función de activación no lineal, relu_outputs y inputs no tienen una relación lineal. Nos referimos a ReLU como la función de activación, porque para cada entrada se activan ciertas salidas (aquellas con valores distintos de cero) mientras que otras se apagan (aquellas con valores distintos de cero)
A continuación, creemos una capa de salida para convertir vectores de longitud hidden_size en relu_outputs en vectores de longitud 10, que es la salida deseada de nuestro modelo (ya que hay 10 etiquetas de destino).
output_size = 10
layer2 = nn.Linear(hidden_size, output_size)
layer2_outputs = layer2(relu_outputs)
print(layer2_outputs.shape)
inputs.shape
Como era de esperar, layer2_outputs contiene un lote de vectores de tamaño 10. Ahora podemos usar esta salida para calcular la pérdida usando F.cross_entropy y ajustar los pesos de layer1 y layer2 usando el descenso de gradiente.
F.cross_entropy(layer2_outputs, labels)
Por lo tanto, nuestro modelo transforma entradas en capa2_salidas aplicando una transformación lineal (usando capa1), seguida de una activación no lineal (usando F.relu), seguida de otra transformación lineal (usando capa2 ). Verifiquemos esto volviendo a calcular la salida usando operaciones matriciales básicas.
# Versión ampliada de Layer2(F.relu(layer1(inputs)))
outputs = (F.relu(inputs @ layer1.weight.t() + layer1.bias)) @ layer2.weight.t() + layer2.bias
torch.allclose(outputs, layer2_outputs, 1e-3)
Tenga en cuenta que las "salidas" y las "entradas" no tienen una relación lineal debido a la función de activación no lineal "F.relu". A medida que entrenamos el modelo y ajustamos los pesos de "capa1" y "capa2", ahora podemos capturar relaciones no lineales entre las imágenes y sus etiquetas. En otras palabras, la introducción de la no linealidad hace que el modelo sea más potente y versátil. Además, dado que hidden_size no depende de las dimensiones de las entradas o salidas, lo variamos para aumentar la cantidad de parámetros dentro del modelo. También podemos introducir nuevas capas ocultas y aplicar la misma activación no lineal después de cada capa oculta.
El modelo que acabamos de crear se llama red neuronal. Una red neuronal profunda es simplemente una red neuronal con una o más capas ocultas. De hecho, el [Teorema de aproximación universal] (http://neuralnetworksanddeeplearning.com/chap4.html) establece que una red neuronal suficientemente grande y profunda puede calcular cualquier función arbitraria, es decir, puede aprender relaciones no lineales ricas y complejas entre entradas y objetivos. Aquí hay unos ejemplos:
- Identificar si una imagen contiene un gato o un perro (o [algo más] (https://machinelearningmastery.com/introduction-to-the-imagenet-large-scale-visual-recognition-challenge-ilsvrc/))
- Identificar el género de una canción usando una muestra de 10 segundos.
- Clasificar las reseñas de películas como positivas o negativas según su contenido.
- Navegar con vehículos autónomos utilizando una transmisión de video de la carretera.
- Traducir oraciones del inglés al francés (y cientos de otros idiomas)
- Convertir una grabación de voz a texto y viceversa
- Y muchos más...
Es difícil imaginar cómo el simple proceso de multiplicar entradas con matrices inicializadas aleatoriamente, aplicar activaciones no lineales y ajustar pesos repetidamente mediante el descenso de gradiente puede producir resultados tan sorprendentes. Los modelos de aprendizaje profundo a menudo contienen millones de parámetros, que en conjunto pueden capturar relaciones mucho más complejas de las que el cerebro humano puede comprender.
Si no hubiéramos incluido una activación no lineal entre las dos capas lineales, la relación final entre entradas y salidas seguiría siendo lineal. Una simple refactorización de los cálculos ilustra esto.
# Igual que capa2 (capa1 (entradas))
outputs2 = (inputs @ layer1.weight.t() + layer1.bias) @ layer2.weight.t() + layer2.bias
# Cree una sola capa para reemplazar las dos capas lineales
combined_layer = nn.Linear(input_size, output_size)
combined_layer.weight.data = layer2.weight @ layer1.weight
combined_layer.bias.data = layer1.bias @ layer2.weight.t() + layer2.bias
# Igual que combine_layer(entradas)
outputs3 = inputs @ combined_layer.weight.t() + combined_layer.bias
torch.allclose(outputs2, outputs3, 1e-3)
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.
# Instalar la biblioteca
!pip install jovian --upgrade --quiet
import jovian
jovian.commit(project='04-feedforward-nn')
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.
Modelo¶
Ahora estamos listos para definir nuestro modelo. Como se mencionó anteriormente, crearemos una red neuronal con una capa oculta. Esto es lo que eso significa:
En lugar de usar un solo objeto
nn.Linearpara transformar un lote de entradas (intensidades de píxeles) en salidas (probabilidades de clase), usaremos dos objetosnn.Linear. Cada uno de estos se denomina capa en la red.La primera capa (también conocida como capa oculta) transformará la matriz de entrada de la forma
batch_size x 784en una matriz de salida intermedia de la formabatch_size x hide_size. El parámetrohidden_sizese puede configurar manualmente (por ejemplo, 32 o 64).Luego aplicaremos una función de activación no lineal a las salidas intermedias. La función de activación transforma elementos individuales de la matriz.
El resultado de la función de activación, que también es de tamaño
batch_size x hide_size, se pasa a la segunda capa (también conocida como capa de salida). La segunda capa lo transforma en una matriz de tamañobatch_size x 10. Podemos usar este resultado para calcular la pérdida y ajustar los pesos mediante el descenso de gradiente.
Como se mencionó anteriormente, nuestro modelo contendrá una capa oculta. Así es como se ve visualmente:
Definamos el modelo extendiendo la clase nn.Module de PyTorch.
class MnistModel(nn.Module):
"""Feedfoward neural network with 1 hidden layer"""
def __init__(self, in_size, hidden_size, out_size):
super().__init__()
# hidden layer
self.linear1 = nn.Linear(in_size, hidden_size)
# output layer
self.linear2 = nn.Linear(hidden_size, out_size)
def forward(self, xb):
# Flatten the image tensors
xb = xb.view(xb.size(0), -1)
# Get intermediate outputs using hidden layer
out = self.linear1(xb)
# Apply activation function
out = F.relu(out)
# Get predictions using output layer
out = self.linear2(out)
return out
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, '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 [{}], val_loss: {:.4f}, val_acc: {:.4f}".format(epoch, result['val_loss'], result['val_acc']))
También necesitamos definir una función de "precisión" que calcule la precisión de la predicción del modelo en un lote de entradas. Se usa en validation_step arriba.
def accuracy(outputs, labels):
_, preds = torch.max(outputs, dim=1)
return torch.tensor(torch.sum(preds == labels).item() / len(preds))
Crearemos un modelo que contiene una capa oculta con 32 activaciones.
input_size = 784
hidden_size = 32 # you can change this
num_classes = 10
model = MnistModel(input_size, hidden_size=32, out_size=num_classes)
Echemos un vistazo a los parámetros del modelo. Esperamos ver una matriz de peso y sesgo para cada una de las capas.
for t in model.parameters():
print(t.shape)
Intentemos generar algunos resultados usando nuestro modelo. Tomaremos el primer lote de 128 imágenes de nuestro conjunto de datos y las pasaremos a nuestro modelo.
for images, labels in train_loader:
outputs = model(images)
loss = F.cross_entropy(outputs, labels)
print('Loss:', loss.item())
break
print('outputs.shape : ', outputs.shape)
print('Sample outputs :\n', outputs[:2].data)
Usando una GPU¶
A medida que aumentan los tamaños de nuestros modelos y conjuntos de datos, necesitamos usar GPU para entrenar nuestros modelos en un período de tiempo razonable. Las GPU contienen cientos de núcleos optimizados para realizar costosas operaciones matriciales en números de punto flotante rápidamente, lo que las hace ideales para entrenar redes neuronales profundas. Puede utilizar GPU de forma gratuita en Google Colab y Kaggle o alquilar máquinas con GPU en servicios como Google Cloud Platform, Amazon Web Services, y Paperspace.
Podemos comprobar si hay una GPU disponible y si los controladores NVIDIA CUDA necesarios están instalados usando torch.cuda.is_available.
torch.cuda.is_available()
Definamos una función auxiliar para garantizar que nuestro código use la GPU si está disponible y use de manera predeterminada la CPU si no lo está.
def get_default_device():
"""Pick GPU if available, else CPU"""
if torch.cuda.is_available():
return torch.device('cuda')
else:
return torch.device('cpu')
device = get_default_device()
device
A continuación, definamos una función que pueda mover datos y modelos a un dispositivo elegido.
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)
for images, labels in train_loader:
print(images.shape)
images = to_device(images, device)
print(images.device)
break
Finalmente, definimos una clase DeviceDataLoader para empaquetar nuestros cargadores de datos existentes y mover lotes de datos al dispositivo seleccionado. Curiosamente, no necesitamos ampliar una clase existente para crear un cargador de datos de PyTorch. Todo lo que necesitamos es un método __iter__ para recuperar lotes de datos y un método __len__ para obtener el número de lotes.
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)
La palabra clave yield en Python se usa para crear una función generadora que se puede usar dentro de un bucle for, como se ilustra a continuación.
def some_numbers():
yield 10
yield 20
yield 30
for value in some_numbers():
print(value)
Ahora podemos empaquetar nuestros cargadores de datos usando DeviceDataLoader.
train_loader = DeviceDataLoader(train_loader, device)
val_loader = DeviceDataLoader(val_loader, device)
Los tensores movidos a la GPU tienen una propiedad de "dispositivo" que incluye la palabra "cuda". Verifiquemos esto mirando un lote de datos de valid_dl.
for xb, yb in val_loader:
print('xb.device:', xb.device)
print('yb:', yb)
break
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.
def evaluate(model, val_loader):
"""Evaluate the model's performance on the validation set"""
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):
"""Train the model using gradient descent"""
history = []
optimizer = opt_func(model.parameters(), lr)
for epoch in range(epochs):
# Training Phase
for batch in train_loader:
loss = model.training_step(batch)
loss.backward()
optimizer.step()
optimizer.zero_grad()
# Validation phase
result = evaluate(model, val_loader)
model.epoch_end(epoch, result)
history.append(result)
return history
Antes de entrenar el modelo, debemos asegurarnos de que los datos y los parámetros del modelo (pesos y sesgos) estén en el mismo dispositivo (CPU o GPU). Podemos reutilizar la función to_device para mover los parámetros del modelo al dispositivo correcto.
# Modelo (en GPU)
model = MnistModel(input_size, hidden_size=hidden_size, out_size=num_classes)
to_device(model, device)
Veamos cómo se desempeña el modelo en el conjunto de validación con el conjunto inicial de ponderaciones y sesgos.
history = [evaluate(model, val_loader)]
history
La precisión inicial es de alrededor del 10%, como se 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).
Entrenemos el modelo durante cinco épocas y observemos los resultados. Podemos utilizar una tasa de aprendizaje relativamente alta de 0,5.
history += fit(5, 0.5, model, train_loader, val_loader)
¡96% es bastante bueno! Entrenemos el modelo para cinco épocas más a una tasa de aprendizaje más baja de 0,1 para mejorar aún más la precisión.
history += fit(5, 0.1, model, train_loader, val_loader)
Ahora podemos trazar las pérdidas y las precisiones para estudiar cómo mejora el modelo con el tiempo.
losses = [x['val_loss'] for x in history]
plt.plot(losses, '-x')
plt.xlabel('epoch')
plt.ylabel('loss')
plt.title('Loss vs. No. of epochs');
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');
¡Nuestro modelo actual supera al modelo de regresión logística (que solo pudo lograr alrededor del 86% de precisión) por un margen considerable! Alcanza rápidamente una precisión del 97%, pero no mejora mucho más allá de esto. Para mejorar aún más la precisión, necesitamos hacer que el modelo sea más potente aumentando el tamaño de la capa oculta o agregando más capas ocultas con activaciones. Le animo a que pruebe ambos enfoques y vea cuál funciona mejor.
Como paso final, podemos guardar y confirmar nuestro trabajo usando la biblioteca "joviana".
!pip install jovian --upgrade -q
import jovian
jovian.commit(project='04-feedforward-nn', environment=None)
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 recreando el conjunto de datos de prueba con la transformación "ToTensor".
# Definir conjunto de datos de prueba
test_dataset = MNIST(root='data/',
train=False,
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):
xb = to_device(img.unsqueeze(0), device)
yb = model(xb)
_, preds = torch.max(yb, dim=1)
return preds[0].item()
Probémoslo con algunas imágenes.
img, label = test_dataset[0]
plt.imshow(img[0], cmap='gray')
print('Label:', label, ', Predicted:', predict_image(img, model))
img, label = test_dataset[1839]
plt.imshow(img[0], cmap='gray')
print('Label:', label, ', Predicted:', predict_image(img, model))
img, label = test_dataset[193]
plt.imshow(img[0], cmap='gray')
print('Label:', 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, veamos también la pérdida general y la precisión del modelo en el conjunto de prueba.
test_loader = DeviceDataLoader(DataLoader(test_dataset, batch_size=256), device)
result = evaluate(model, test_loader)
result
Esperamos que esto sea similar a la precisión/pérdida en el 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).
Guardemos los pesos del modelo y adjuntémoslo al cuaderno usando jovian.commit. También registraremos el rendimiento del modelo en el conjunto de datos de prueba usando jovian.log_metrics.
jovian.log_metrics(test_loss=result['val_loss'], test_acc=result['val_loss'])
torch.save(model.state_dict(), 'mnist-feedforward.pth')
jovian.commit(project='04-feedforward-nn',
environment=None,
outputs=['mnist-feedforward.pth'])
Ejercicios¶
Pruebe los siguientes ejercicios para aplicar los conceptos y técnicas que ha aprendido hasta ahora:
- Ejercicios de codificación sobre entrenamiento de modelos de un extremo a otro: https://jovian.ai/aakashns/03-cifar10-feedforward
- Cuaderno de inicio para modelos de aprendizaje profundo: https://jovian.ai/aakashns/fashion-feedforward-minimal
Entrenar excelentes modelos de aprendizaje automático de manera confiable requiere práctica y experiencia. Intente experimentar con diferentes conjuntos de datos, modelos e hiperparámetros, es la mejor manera de adquirir esta habilidad.
Resumen y lecturas adicionales¶
Aquí hay un resumen de los temas cubiertos en este tutorial:
Creamos una red neuronal con una capa oculta para mejorar el modelo de regresión logística del tutorial anterior. También utilizamos la función de activación ReLU para introducir no linealidad en el modelo, permitiéndole aprender relaciones más complejas entre las entradas (densidades de píxeles) y las salidas (probabilidades de clase).
Definimos algunas utilidades como
get_default_device,to_deviceyDeviceDataLoaderpara aprovechar una GPU si está disponible, moviendo los datos de entrada y los parámetros del modelo al dispositivo apropiado.Pudimos usar exactamente el mismo ciclo de entrenamiento: la función "ajuste" que habíamos definido anteriormente para entrenar el modelo y evaluarlo usando el conjunto de datos de validación.
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 el tamaño de la capa oculta o agregue más capas ocultas y vea si puede lograr una mayor precisión.
Intente cambiar el tamaño del lote y la tasa de aprendizaje para ver si puede lograr la misma precisión en menos épocas.
Compare los tiempos de entrenamiento en una CPU frente a una GPU. ¿Ves una diferencia significativa? ¿Cómo varía con el tamaño del conjunto de datos y el tamaño del modelo (número de pesos y parámetros)?
Intente crear un modelo para un conjunto de datos diferente, como los conjuntos de datos CIFAR10 o CIFAR100.
Aquí hay algunas referencias para lectura adicional:
[Una prueba visual de que las redes neuronales pueden calcular cualquier función] (http://neuralnetworksanddeeplearning.com/chap4.html), también conocido como teorema de aproximación universal.
Pero ¿qué es una red neuronal? - Una introducción visual e intuitiva a qué son las redes neuronales y qué representan las capas intermedias
Notas de la conferencia Stanford CS229 sobre retropropagación - para un tratamiento más matemático de cómo se calculan los gradientes y se actualizan los pesos para Redes neuronales con múltiples capas.
Ahora está listo para pasar al siguiente tutorial: [Clasificación de imágenes mediante redes neuronales convolucionales] (https://jovian.ai/aakashns/05-cifar10-cnn).