Tarea 1: Clasificación de dígitos manuscritos con MNIST en PyTorch

Tarea 1: Clasificación de dígitos manuscritos con MNIST en PyTorch

Información de la Tarea

Estudiante: Andrés Cruz Chipol
Curso: Aprendizaje Profundo
Fecha de entrega: 19 de Mayo, 2026

Descripción de la Tarea

Realizar la red del tutorial de Pytorch para clasificar los imágenes de dígitos de MNIST


Clasificación de dígitos manuscritos

Introducción

El dataset MNIST es fundamental en el Aprendizaje Profundo. Contiene imágenes de dígitos manuscritos del 0 al 9 en formato de escala de grises de 28x28 píxeles. El objetivo es entrenar una red neuronal multicapa (MLP) para predecir correctamente la clase a partir de los píxeles de entrada.

Importaciones e Hiperparámetros

Comenzamos configurando el entorno en PyTorch, estableciendo la semilla y definiendo los hiperparámetros de entrenamiento.

import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import v2
import matplotlib.pyplot as plt
torch.manual_seed(2)
device = "mps"
print(f"Using {device} device")
learning_rate = 0.001
batch_size = 64
epochs = 20

Preparación de Datos

Utilizamos las transformaciones v2 de torchvision para convertir las imágenes a tensores de tipo float32 y preparamos los iteradores DataLoader.

#Preparación de datos
transform = v2.Compose([v2.ToImage(), v2.ToDtype(torch.float32, scale=True)])
training_data = datasets.MNIST(root="data", train=True, download=True, transform=transform)
test_data = datasets.MNIST(root="data", train=False, download=True, transform=transform)
train_dataloader = DataLoader(training_data, batch_size=batch_size)
test_dataloader = DataLoader(test_data, batch_size=batch_size)

Visualización

Para explorar el conjunto de datos, seleccionamos y mostramos 9 dígitos aleatorios utilizando Matplotlib.

# Visualización
labels_map = {i: str(i) for i in range(10)}
figure = plt.figure(figsize=(8, 8))
for i in range(1, 10):
sample_idx = torch.randint(len(training_data), size=(1,)).item()
img, label = training_data[sample_idx]
figure.add_subplot(3, 3, i)
plt.title(labels_map[label])
plt.axis("off")
plt.imshow(img.squeeze(), cmap="gray")
figure.suptitle("Imágenes de MNIST", fontsize=16)
plt.show()
Muestras aleatorias de dígitos de MNIST

Definición del Modelo

Nuestra arquitectura MLP aplana la imagen a un vector de 784 elementos y utiliza dos capas ocultas de 512 neuronas con activación ReLU, finalizando en 10 clases de salida.

# Modelo - Red Neuronal
class NeuralNetwork(nn.Module):
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()
self.linear_relu_stack = nn.Sequential(
nn.Linear(28*28, 512),
nn.ReLU(),
nn.Linear(512, 512),
nn.ReLU(),
nn.Linear(512, 10),
)
def forward(self, x):
x = self.flatten(x)
return self.linear_relu_stack(x)
model = NeuralNetwork().to(device)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, nesterov=True, momentum=0.99)

Funciones de Entrenamiento y Prueba

Definimos los bucles para propagar los datos, calcular el error y actualizar los pesos del modelo, así como para evaluar el rendimiento sin calcular gradientes.

# Funciones de train & test
def train_loop(dataloader, model, loss_fn, optimizer):
size = len(dataloader.dataset)
model.train()
for batch, (X, y) in enumerate(dataloader):
X, y = X.to(device), y.to(device)
pred = model(X)
loss = loss_fn(pred, y)
loss.backward()
optimizer.step()
optimizer.zero_grad()
if batch % 100 == 0:
loss_val, current = loss.item(), batch * batch_size + len(X)
print(f"loss: {loss_val:>7f} [{current:>5d}/{size:>5d}]")
def test_loop(dataloader, model, loss_fn):
model.eval()
size = len(dataloader.dataset)
num_batches = len(dataloader)
test_loss, correct = 0, 0
with torch.no_grad():
for X, y in dataloader:
X, y = X.to(device), y.to(device)
pred = model(X)
test_loss += loss_fn(pred, y).item()
correct += (pred.argmax(1) == y).type(torch.float).sum().item()
test_loss /= num_batches
correct /= size
print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")

Ejecución del Entrenamiento

Ejecutamos el entrenamiento por 20 épocas y guardamos los pesos finales.

# Entrenamiento
for t in range(epochs):
print(f"Epoch {t+1}\n-------------------------------")
train_loop(train_dataloader, model, loss_fn, optimizer)
test_loop(test_dataloader, model, loss_fn)
torch.save(model, 'model.pth')

Resultados de la Consola

Al iniciar la ejecución, se reporta el dispositivo utilizado y la arquitectura de la red:

Using mps device
NeuralNetwork(
(flatten): Flatten(start_dim=1, end_dim=-1)
(linear_relu_stack): Sequential(
(0): Linear(in_features=784, out_features=512, bias=True)
(1): ReLU()
(2): Linear(in_features=512, out_features=512, bias=True)
(3): ReLU()
(4): Linear(in_features=512, out_features=10, bias=True)
)
)

Tras procesar las 20 épocas de entrenamiento, se alcanzan los siguientes resultados finales:

Epoch 20
-------------------------------
loss: 0.003598 [ 64/60000]
loss: 0.002625 [ 6464/60000]
loss: 0.003522 [12864/60000]
loss: 0.002404 [19264/60000]
loss: 0.012981 [25664/60000]
loss: 0.005628 [32064/60000]
loss: 0.002907 [38464/60000]
loss: 0.008000 [44864/60000]
loss: 0.007265 [51264/60000]
loss: 0.012696 [57664/60000]
Test Error:
Accuracy: 98.2%, Avg loss: 0.063219

Conclusión

La implementación de la red neuronal feedforward en PyTorch permitió clasificar los dígitos manuscritos del dataset MNIST con una precisión final del 98.2% tras 20 épocas, demostrando la eficacia del optimizador SGD con momento Nesterov y el procesamiento acelerado para el entrenamiento de modelos multicapa. Este es el mejor resultado al que se llegó utilizando SGD, momentum y Nesterov; se hicieron varios cambios de semilla, learning rate y épocas, siendo este el mejor modelo obtenido después de varias iteraciones.