De cero a héroe Crea tu primer modelo de ML con PyTorch
Crea tu primer modelo de ML con PyTorch
Motivación
PyTorch es el framework de Deep Learning basado en Python más utilizado. Proporciona un gran soporte para todas las arquitecturas y pipelines de aprendizaje automático. En este artículo, repasamos todos los conceptos básicos del framework para ayudarte a comenzar a implementar tus algoritmos.
Todas las implementaciones de aprendizaje automático tienen 4 pasos principales:
- Conozca Verba una herramienta de código abierto para construir su p...
- RLHF para la toma de decisiones de alto rendimiento estrategias y o...
- Guíame a través del tiempo SceNeRFlow es un método de IA que genera...
- Manipulación de datos
- Arquitectura del modelo
- Bucle de entrenamiento
- Evaluación
Recorremos todos estos pasos mientras implementamos nuestro propio modelo de clasificación de imágenes MNIST en PyTorch. Esto te familiarizará con el flujo general de un proyecto de aprendizaje automático.
Importaciones
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
# Usando el conjunto de datos MNIST proporcionado por PyTorch
from torchvision.datasets.mnist import MNIST
import torchvision.transforms as transforms
# Importar el modelo implementado en un archivo diferente
from model import Classifier
import matplotlib.pyplot as plt
El módulo torch.nn proporciona soporte para arquitecturas de redes neuronales y tiene implementaciones incorporadas de capas populares como capas densas, redes neuronales convolucionales y muchas más.
torch.optim proporciona implementaciones de optimizadores como el Descenso de Gradiente Estocástico y Adam.
Otros módulos de utilidad están disponibles para el manejo de datos y transformaciones. Repasaremos cada uno en más detalle más adelante.
Declarar Hiperparámetros
Cada hiperparámetro se explicará más adelante cuando sea apropiado. Sin embargo, es una buena práctica declararlos al principio de nuestro archivo para facilitar los cambios y la comprensión.
INPUT_SIZE = 784 # Imágenes 28x28 aplanadas
NUM_CLASSES = 10 # Dígitos escritos a mano del 0 al 9.
BATCH_SIZE = 128 # Usando Mini-Batches para el entrenamiento
LEARNING_RATE = 0.01 # Paso del optimizador
NUM_EPOCHS = 5 # Total de épocas de entrenamiento
Carga de Datos y Transformaciones
data_transforms = transforms.Compose([
transforms.ToTensor(),
transforms.Lambda(lambda x: torch.flatten(x))
])
train_dataset = MNIST(root=".data/", train=True, download=True, transform=data_transforms)
test_dataset = MNIST(root=".data/", train=False, download=True, transform=data_transforms)
MNIST es un conjunto de datos popular de clasificación de imágenes, proporcionado por defecto en PyTorch. Consiste en imágenes en escala de grises de 10 dígitos escritos a mano del 0 al 9. Cada imagen tiene un tamaño de 28 píxeles por 28 píxeles, y el conjunto de datos contiene 60000 imágenes de entrenamiento y 10000 imágenes de prueba.
Cargamos el conjunto de datos de entrenamiento y prueba por separado, indicado por el argumento train en la función de inicialización de MNIST. El argumento root declara el directorio en el que se descargará el conjunto de datos.
Sin embargo, también pasamos un argumento adicional transform. Para PyTorch, todas las entradas y salidas deben estar en formato Torch.Tensor. Esto es equivalente a un numpy.ndarray en numpy. Este formato tensor proporciona soporte adicional para la manipulación de datos. Sin embargo, los datos MNIST que cargamos están en formato PIL.Image. Necesitamos transformar las imágenes en tensores compatibles con PyTorch. En consecuencia, pasamos las siguientes transformaciones:
data_transforms = transforms.Compose([
transforms.ToTensor(),
transforms.Lambda(lambda x: torch.flatten(x))
])
La transformación ToTensor() convierte las imágenes al formato tensor. A continuación, pasamos una transformación Lambda adicional. La función Lambda nos permite implementar transformaciones personalizadas. Aquí declaramos una función para aplanar la entrada. Las imágenes tienen un tamaño de 28×28, pero las aplanamos, es decir, las convertimos en una matriz unidimensional de tamaño 28×28 o 784. Esto será importante más adelante cuando implementemos nuestro modelo.
La función Compose combina secuencialmente todas las transformaciones. Primero, los datos se convierten al formato tensor y luego se aplanan a una matriz unidimensional.
Dividiendo nuestros datos en lotes
Por motivos computacionales y de entrenamiento, no podemos pasar todo el conjunto de datos completo al modelo de una vez. Necesitamos dividir nuestro conjunto de datos en mini lotes que se alimentarán al modelo en orden secuencial. Esto permite un entrenamiento más rápido y agrega aleatoriedad a nuestro conjunto de datos, lo que puede ayudar en el entrenamiento estable.
PyTorch proporciona soporte incorporado para dividir nuestros datos en lotes. La clase DataLoader del módulo torch.utils puede crear lotes de datos, dados un módulo de conjunto de datos de antorcha. Como se mencionó anteriormente, ya tenemos el conjunto de datos cargado.
train_dataloader = DataLoader(train_dataset, batch_size=TAMAÑO_LOTE, shuffle=True)
test_dataloader = DataLoader(test_dataset, batch_size=TAMAÑO_LOTE, shuffle=False)
Pasamos el conjunto de datos a nuestro DataLoader y nuestro hiperparámetro de tamaño de lote como argumentos de inicialización. Esto crea un DataLoader iterable, por lo que podemos iterar fácilmente sobre cada lote usando un simple bucle for.
Nuestra imagen inicial tenía un tamaño de (784, ) con una única etiqueta asociada. La división en lotes combina diferentes imágenes y etiquetas en un lote. Por ejemplo, si tenemos un tamaño de lote de 64, el tamaño de entrada en un lote será (64, 784) y tendremos 64 etiquetas asociadas para cada lote.
También mezclamos el lote de entrenamiento, lo que cambia las imágenes dentro de un lote para cada época. Esto permite un entrenamiento estable y una convergencia más rápida de los parámetros de nuestro modelo.
Definiendo nuestro modelo de clasificación
Utilizamos una implementación simple que consta de 3 capas ocultas. Aunque es simple, esto puede brindarte una comprensión general de cómo combinar diferentes capas para implementaciones más complejas.
Como se describió anteriormente, tenemos un tensor de entrada de tamaño (784, ) y 10 clases de salida diferentes, una para cada dígito del 0 al 9.
** Para la implementación del modelo, podemos ignorar la dimensión del lote.
import torch
import torch.nn as nn
class Clasificador(nn.Module):
def __init__(
self,
tamaño_entrada:int,
num_clases:int
) -> None:
super().__init__()
self.capa_entrada = nn.Linear(tamaño_entrada, 512)
self.oculta_1 = nn.Linear(512, 256)
self.oculta_2 = nn.Linear(256, 128)
self.capa_salida = nn.Linear(128, num_clases)
self.activacion = nn.ReLU()
def forward(self, x):
# Pasar la entrada secuencialmente a través de cada capa densa y activación
x = self.activacion(self.capa_entrada(x))
x = self.activacion(self.oculta_1(x))
x = self.activacion(self.oculta_2(x))
return self.capa_salida(x)
En primer lugar, el modelo debe heredar de la clase torch.nn.Module. Esto proporciona funcionalidad básica para las arquitecturas de redes neuronales. Luego debemos implementar dos métodos, __init__ y forward.
En el método __init__, declaramos todas las capas que el modelo utilizará. Utilizamos capas Lineales (también llamadas Densas) proporcionadas por PyTorch. La primera capa mapea la entrada a 512 neuronas. Podemos pasar el tamaño de entrada como un parámetro del modelo, para poder usarlo más tarde para entradas de diferentes tamaños. La segunda capa mapea las 512 neuronas a 256. La tercera capa oculta mapea las 256 neuronas de la capa anterior a 128. Finalmente, la capa de salida se reduce al tamaño de salida. Nuestro tamaño de salida será un tensor de tamaño (10, ) porque estamos prediciendo diez números diferentes.
Además, inicializamos una capa de activación ReLU para la no linealidad en nuestro modelo.
La función forward recibe imágenes y proporcionamos código para procesar la entrada. Utilizamos las capas declaradas y pasamos nuestra entrada secuencialmente a través de cada capa, con una capa de activación ReLU intermedia.
En nuestro código principal, podemos inicializar el modelo proporcionándole el tamaño de entrada y salida para nuestro conjunto de datos.
modelo = Clasificador(tamaño_entrada=784, num_clases=10)
modelo.to(DISPOSITIVO)
Una vez inicializado, cambiamos el dispositivo del modelo (que puede ser GPU CUDA o CPU). Verificamos nuestro dispositivo cuando inicializamos los hiperparámetros. Ahora, debemos cambiar manualmente el dispositivo para nuestros tensores y capas del modelo.
Bucle de entrenamiento
En primer lugar, debemos declarar nuestra función de pérdida y el optimizador que se utilizará para optimizar los parámetros de nuestro modelo.
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)
En primer lugar, debemos declarar nuestra función de pérdida y el optimizador que se utilizará para optimizar los parámetros de nuestro modelo.
Utilizamos la función de pérdida de entropía cruzada que se utiliza principalmente para modelos de clasificación con múltiples etiquetas. Primero aplica softmax a las predicciones y calcula las etiquetas objetivo y los valores predichos dados.
El optimizador Adam es la función de optimización más utilizada que permite un descenso de gradiente estable hacia la convergencia. Es la elección de optimizador por defecto en la actualidad y proporciona resultados satisfactorios. Pasamos los parámetros de nuestro modelo como argumento que denota los pesos que se optimizarán.
Para nuestro bucle de entrenamiento, lo construimos paso a paso y completamos las partes que faltan a medida que adquirimos comprensión.
Como punto de partida, iteramos sobre el conjunto de datos completo varias veces (llamadas épocas) y optimizamos nuestro modelo cada vez. Sin embargo, hemos dividido nuestros datos en lotes. Entonces, para cada época, también debemos iterar sobre cada lote. El código para esto se verá así:
for epoch in range(NUM_EPOCHS):
for batch in iter(train_dataloader):
# Entrenar el modelo para cada lote.
Ahora, podemos entrenar el modelo dado un solo lote de entrada. Nuestro lote consta de imágenes y etiquetas. En primer lugar, debemos separar cada uno de ellos. Nuestro modelo solo requiere imágenes como entrada para hacer predicciones. Luego comparamos las predicciones con las etiquetas reales para estimar el rendimiento de nuestro modelo.
for epoch in range(NUM_EPOCHS):
for batch in iter(train_dataloader):
images, labels = batch # Separar entradas y etiquetas
# Convertir los dispositivos de hardware de Tensor a GPU o CPU
images = images.to(DEVICE)
labels = labels.to(DEVICE)
# Llama a la función model.forward() para generar predicciones
predictions = model(images)
Pasamos el lote de imágenes directamente al modelo que será procesado por la función forward definida dentro del modelo. Una vez que tenemos nuestras predicciones, podemos optimizar los pesos de nuestro modelo.
El código de optimización se ve así:
# Calcular la pérdida de entropía cruzada
loss = criterion(predictions, labels)
# Limpia los valores de gradiente del lote anterior
optimizer.zero_grad()
# Calcula el gradiente de retropropagación basado en la pérdida
loss.backward()
# Optimiza los pesos del modelo
optimizer.step()
Usando el código anterior, podemos calcular todos los gradientes de retropropagación y optimizar los pesos del modelo utilizando el optimizador Adam. Todos los códigos anteriores combinados pueden entrenar nuestro modelo hacia la convergencia.
El bucle de entrenamiento completo se ve así:
for epoch in range(NUM_EPOCHS):
total_epoch_loss = 0
steps = 0
for batch in iter(train_dataloader):
images, labels = batch # Separar entradas y etiquetas
# Convertir los dispositivos de hardware de Tensor a GPU o CPU
images = images.to(DEVICE)
labels = labels.to(DEVICE)
# Llama a la función model.forward() para generar predicciones
predictions = model(images)
# Calcular la pérdida de entropía cruzada
loss = criterion(predictions, labels)
# Limpia los valores de gradiente del lote anterior
optimizer.zero_grad()
# Calcula el gradiente de retropropagación basado en la pérdida
loss.backward()
# Optimiza los pesos del modelo
optimizer.step()
steps += 1
total_epoch_loss += loss.item()
print(f'Epoch: {epoch + 1} / {NUM_EPOCHS}: Pérdida promedio: {total_epoch_loss / steps}')
La pérdida disminuye gradualmente y llega cerca de 0. Luego, podemos evaluar el modelo en el conjunto de datos de prueba que declaramos inicialmente.
Evaluando el rendimiento de nuestro modelo
for batch in iter(test_dataloader):
images, labels = batch
images = images.to(DEVICE)
labels = labels.to(DEVICE)
predictions = model(images)
# Tomando la etiqueta predicha con mayor probabilidad
predictions = torch.argmax(predictions, dim=1)
correct_predictions += (predictions == labels).sum().item()
total_predictions += labels.shape[0]
print(f"\nPRECISIÓN DE LA PRUEBA: {((correct_predictions / total_predictions) * 100):.2f}")
Similar al bucle de entrenamiento, iteramos sobre cada lote en el conjunto de datos de prueba para su evaluación. Generamos predicciones para las entradas. Sin embargo, para la evaluación, solo necesitamos la etiqueta con la probabilidad más alta. La función argmax proporciona esta funcionalidad para obtener el índice del valor con el valor más alto en nuestro conjunto de predicciones.
Para la puntuación de precisión, podemos comparar si la etiqueta predicha coincide con la etiqueta objetivo verdadera. Luego calculamos la precisión del número de etiquetas correctas dividido por el total de etiquetas predichas.
Resultados
Solo entrené el modelo durante cinco épocas y logré una precisión en la prueba de más del 96 por ciento, en comparación con una precisión del 10 por ciento antes del entrenamiento. La imagen a continuación muestra las predicciones del modelo después de entrenar cinco épocas.
Y eso es todo. Ahora has implementado un modelo desde cero que puede diferenciar dígitos escritos a mano solo a partir de los valores de píxeles de la imagen.
Esto de ninguna manera es una guía completa para PyTorch, pero te brinda una comprensión general de la estructura y el flujo de datos en un proyecto de aprendizaje automático. Esto es, no obstante, conocimiento suficiente para comenzar a implementar arquitecturas de vanguardia en el aprendizaje profundo.
Código completo
El código completo es el siguiente:
model.py:
import torch
import torch.nn as nn
class Classifier(nn.Module):
def __init__(
self,
input_size:int,
num_classes:int
) -> None:
super().__init__()
self.input_layer = nn.Linear(input_size, 512)
self.hidden_1 = nn.Linear(512, 256)
self.hidden_2 = nn.Linear(256, 128)
self.output_layer = nn.Linear(128, num_classes)
self.activation = nn.ReLU()
def forward(self, x):
# Pasar la entrada secuencialmente a través de cada capa densa y la activación
x = self.activation(self.input_layer(x))
x = self.activation(self.hidden_1(x))
x = self.activation(self.hidden_2(x))
return self.output_layer(x)
main.py
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
# Usando el conjunto de datos MNIST proporcionado por PyTorch
from torchvision.datasets.mnist import MNIST
import torchvision.transforms as transforms
# Importar el modelo implementado en un archivo diferente
from model import Classifier
import matplotlib.pyplot as plt
if __name__ == "__main__":
INPUT_SIZE = 784 # Imágenes aplanadas de 28x28
NUM_CLASSES = 10 # Dígitos escritos a mano del 0 al 9.
BATCH_SIZE = 128 # Usando mini-lotes para el entrenamiento
LEARNING_RATE = 0.01 # Paso del optimizador
NUM_EPOCHS = 5 # Total de épocas de entrenamiento
DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
# Se utilizará para convertir imágenes en tensores de PyTorch
data_transforms = transforms.Compose([
transforms.ToTensor(),
transforms.Lambda(lambda x: torch.flatten(x))
])
train_dataset = MNIST(root=".data/", train=True, download=True, transform=data_transforms)
test_dataset = MNIST(root=".data/", train=False, download=True, transform=data_transforms)
train_dataloader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
test_dataloader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)
model = Classifier(input_size=784, num_classes=10)
model.to(DEVICE)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)
for epoch in range(NUM_EPOCHS):
total_epoch_loss = 0
steps = 0
for batch in iter(train_dataloader):
images, labels = batch # Separar entradas y etiquetas
# Convertir los dispositivos de hardware de tensor a GPU o CPU
images = images.to(DEVICE)
labels = labels.to(DEVICE)
# Llama a la función model.forward() para generar predicciones
predictions = model(images)
# Calcular la pérdida de entropía cruzada
loss = criterion(predictions, labels)
# Borra los valores de gradiente del lote anterior
optimizer.zero_grad()
# Calcula el gradiente de retropropagación basado en la pérdida
loss.backward()
# Optimiza los pesos del modelo
optimizer.step()
steps += 1
total_epoch_loss += loss.item()
print(f'Época: {epoch + 1} / {NUM_EPOCHS}: Pérdida promedio: {total_epoch_loss / steps}')
# Guardar el modelo entrenado
torch.save(model.state_dict(), 'trained_model.pth')
model.eval()
correct_predictions = 0
total_predictions = 0
for batch in iter(test_dataloader):
images, labels = batch
images = images.to(DEVICE)
labels = labels.to(DEVICE)
predictions = model(images)
# Tomar la etiqueta predicha con mayor probabilidad
predictions = torch.argmax(predictions, dim=1)
correct_predictions += (predictions == labels).sum().item()
total_predictions += labels.shape[0]
print(f"\nPRECISIÓN DE PRUEBA: {((correct_predictions / total_predictions) * 100):.2f}")
# -- Código para graficar los resultados -- #
batch = next(iter(test_dataloader))
images, labels = batch
fig, ax = plt.subplots(nrows=1, ncols=4, figsize=(16,8))
for i in range(4):
image = images[i]
prediction = torch.softmax(model(image), dim=0)
prediction = torch.argmax(prediction, dim=0)
# print(type(prediction), type(prediction.item()))
ax[i].imshow(image.view(28,28))
ax[i].set_title(f'Predicción: {prediction.item()}')
plt.show()
Muhammad Arham es un Ingeniero de Aprendizaje Profundo que trabaja en Visión por Computadora y Procesamiento del Lenguaje Natural. Ha trabajado en la implementación y optimización de varias aplicaciones de IA generativas que alcanzaron los primeros puestos a nivel mundial en Vyro.AI. Está interesado en construir y optimizar modelos de aprendizaje automático para sistemas inteligentes y cree en la mejora continua.