Tarea 2: Clasificador de calidad de vinos en PyTorch

Tarea 2: Clasificador de calidad de vinos en PyTorch

Información de la Tarea

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

Descripción de la Tarea

Con estos datos realizar un clasificador con redes neuronales. Se tienen 11 características y se deben clasificar los vinos por su calidad. Comparar la gráfica de pérdida contra época con tres optimizadores: Adam, SGD y SGD con momento y el atributo ‘nesterov’ habilitado.


Clasificación de calidad de vinos

Introducción

Construí una red neuronal basándonos en 11 características del conjunto de datos winequality-white.csv. Comparamos el desempeño y la convergencia de la red utilizando tres optimizadores diferentes: Adam, SGD tradicional, y SGD con momento Nesterov.

Se enfatiza que al inicio se usaron redes con pocas neuronas para encontrar un buen % de accuracy, sin embargo, ninguno pasaba del 55% aproximadamente. Fue hasta que incrementamos el número de capas ocultas que pudimos obtener un mayor porcentaje de aciertos.

Recalco que el tamano de la prueba es del 15% del conjunto de datos, ya que observe que los datos estaban muy desbalanceados, por lo que quise experimentar con el tamaño de prueba para obtener un mayor porcentaje de aciertos y al parecer funcionó. aun que seguramente no es lo mas adecuado para una red robusta.

Preparación de Datos

Use StandardScaler de Scikit-Learn para normalizar las características de entrada.

from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
import numpy as np
import torch
from torch import nn
torch.manual_seed(1)
scaler = StandardScaler()
data = np.loadtxt("winequality-white.csv", delimiter=';', skiprows=1)
X = data[:,0:11]
y = data[:,11]
X_train, X_test, y_train, y_test = train_test_split(X,y, random_state=99, stratify=y, test_size=0.15)
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
X_train = torch.tensor(X_train, dtype=torch.float32)
X_test = torch.tensor(X_test, dtype=torch.float32)
y_train = torch.tensor(y_train, dtype=torch.long)
y_test = torch.tensor(y_test, dtype=torch.long)

Definición del Modelo y Optimizadores

Red secuencial con tres capas que reducen gradualmente la dimensionalidad (de 11 a 512, de 512 a 256 y finalmente a 11 posibles salidas), utilizando ReLU como función de activación.

class WineNeuronalNetwork(nn.Module):
def __init__(self):
super().__init__()
self.architecture = nn.Sequential(
nn.Linear(11, 512),
nn.ReLU(),
nn.Linear(512, 256),
nn.ReLU(),
nn.Linear(256, 11)
)
def forward(self, x):
r = self.architecture(x)
return r
loss_fn = nn.CrossEntropyLoss()
learning_rate = 0.1
batch_size = 64

Ciclo de Entrenamiento y Evaluación

def train_loop(X,y,model,loss_fn,optimizer):
size = X.shape[0]
model.train()
i = 0
total_loss = 0
num_batches = 0
while i < size:
Xe = X[i:i+batch_size,:]
ye = y[i:i+batch_size]
pred = model(Xe)
loss_val = loss_fn(pred,ye)
loss_val.backward()
optimizer.step()
optimizer.zero_grad()
total_loss += loss_val.item()
num_batches += 1
i += batch_size
return total_loss / num_batches
def test_loop(t,X,y,model):
model.eval()
with torch.no_grad():
pred = model(X)
size = pred.shape[0]
a = torch.argmax(pred,axis=1)
b = y
correct = (a==b).type(torch.float).sum().item()
accuracy = correct / size
print (f"Epoch:{t} - Acc:{accuracy}")

Ejecución del Comparativo

Entrenamos cada variante de la red neuronal por 200 épocas. Durante cada iteración por optimizador, guardamos el historial de la pérdida para ser posteriormente visualizado y comparado, y finalmente evaluamos y almacenamos el modelo pre-entrenado para su reutilización.

Recalco que el codigo aqui esta automatizado, pero se hizo una busqueda manual para encontrar el mejor learning rate para cada optimizador.

epochs = 200
optimizadores_a_probar = [
('Adam', 'loss_adam.txt', 0.01),
('SGD', 'loss_sgd.txt', 0.1),
('SGD Nesterov', 'loss_nesterov.txt', 0.1)
]
for nombre_opt, archivo_salida, lr_opt in optimizadores_a_probar:
model_actual = WineNeuronalNetwork()
if nombre_opt == 'Adam':
opt_actual = torch.optim.Adam(model_actual.parameters(), lr=lr_opt)
elif nombre_opt == 'SGD':
opt_actual = torch.optim.SGD(model_actual.parameters(), lr=lr_opt)
else:
opt_actual = torch.optim.SGD(model_actual.parameters(), lr=lr_opt, momentum=0.9, nesterov=True)
historial_loss = []
for t in range(epochs):
avg_loss = train_loop(X_train, y_train, model_actual, loss_fn, opt_actual)
historial_loss.append(avg_loss)
print(f"Resultados de {nombre_opt}:")
test_loop(epochs, X_test, y_test, model_actual)
np.savetxt(archivo_salida, historial_loss)
torch.save(model_actual, f'model_{nombre_opt.replace(" ", "_").lower()}.pth')

Gráfica de Perdida vs Epoca

import matplotlib.pyplot as plt, numpy as np, os
pruebas = [('Adam', 'loss_adam.txt', '#E67E22'),
('SGD', 'loss_sgd.txt', '#8E44AD'),
('SGD Nesterov', 'loss_nesterov.txt', '#1ABC9C')]
for nombre, archivo, color in pruebas:
if os.path.exists(archivo):
plt.plot(np.loadtxt(archivo), label=nombre, color=color, linewidth=2)
plt.title('Pérdida vs Época (15% de datos)')
plt.xlabel('Época')
plt.ylabel('Pérdida')
plt.legend()
plt.grid(True)
plt.savefig('grafica_loss_final.png')
Pérdida contra épocas usando Adam, SGD y SGD Nesterov

Resultados y Conclusiones

Los mejores resultados de precisión en la validación por optimizador fueron:

A pesar de que usualmente se considera que Nesterov acelera la convergencia de manera más agresiva, con los parámetros actuales (momentum de 0.9 y lr de 0.1), el SGD sin momentum produjo una métrica ligeramente superior.

Analizando la gráfica de pérdida, se puede observar que Nesterov disminuye la función objetivo más rápida y suavemente en las etapas iniciales, mientras que Adam tiene oscilaciones.

Estoy seguro que la inestabilidad que muestran los optimizadores se debe al tamaño de la prueba, ya que usé el 15% de los datos para prueba y, aunque está desbalanceado, si hubiera mas datos probablemente convergerian mejor.