Clasificación de Flores Iris con PyTorch
Clasificación multiclase del dataset Iris usando un MLP de una capa oculta implementado en PyTorch con CrossEntropyLoss y Adam.
🌸 Clasificación de Flores Iris con un MLP en PyTorch
Submódulo: Perceptrón Multicapa (MLP) · Fundamentos de Deep Learning
En este ejemplo implementamos un MLP (Perceptrón Multicapa) con PyTorch para clasificar las tres especies de flores Iris:
- Setosa (clase 0)
- Versicolor (clase 1)
- Virginica (clase 2)
El dataset Iris es un clásico de machine learning: 150 muestras, 4 características (largo/ancho de sépalos y pétalos) y 3 clases perfectamente balanceadas (50 muestras cada una).
Veremos el flujo completo de un proyecto de clasificación con deep learning:
- Exploración de datos (EDA)
- Preprocesamiento y split train/test
- Definición del MLP en PyTorch
- Entrenamiento con CrossEntropyLoss + Adam
- Evaluación del modelo
1. Carga y exploración de los datos
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
# ── Cargar dataset ──────────────────────────────────────────
iris = load_iris()
X = iris.data # (150, 4)
y = iris.target # (150,) — clases 0, 1, 2
df = pd.DataFrame(X, columns=iris.feature_names)
df["species"] = pd.Categorical.from_codes(y, iris.target_names)
print(f"Dataset: {X.shape[0]} muestras, {X.shape[1]} características, {len(iris.target_names)} clases")
print(f"Clases: {list(iris.target_names)}")
print(f"\nMuestras por clase:")
print(df["species"].value_counts().to_string())
1.1 Distribución de las características
Visualizamos la distribución de cada variable para entender la separabilidad entre clases.
# ── Histogramas por característica ─────────────────────────
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
for i, (col, ax) in enumerate(zip(iris.feature_names, axes.flat)):
for cls_idx, cls_name in enumerate(iris.target_names):
mask = y == cls_idx
ax.hist(X[mask, i], bins=15, alpha=0.6, label=cls_name)
ax.set_title(col)
ax.legend(fontsize=8)
ax.grid(alpha=0.3)
plt.suptitle("Distribución por clase", fontsize=14)
plt.tight_layout()
plt.show()
1.2 Box plots por clase
Los box plots nos ayudan a detectar valores atípicos y ver la separabilidad entre clases. Observa que petal length y petal width separan muy bien a Setosa del resto.
# ── Box plots ──────────────────────────────────────────────
fig, axes = plt.subplots(1, 4, figsize=(16, 4))
for i, (col, ax) in enumerate(zip(iris.feature_names, axes)):
df_plot = pd.DataFrame({"value": X[:, i], "species": [iris.target_names[c] for c in y]})
sns.boxplot(data=df_plot, x="species", y="value", ax=ax, palette="Set2")
ax.set_title(col, fontsize=10)
ax.set_xlabel("")
plt.suptitle("Distribución por especie", fontsize=14)
plt.tight_layout()
plt.show()
2. Preprocesamiento
Estandarizamos las características (media=0, std=1) y dividimos en train/test. Convertimos los datos a tensores PyTorch con los tipos correctos:
- Features →
FloatTensor - Labels →
LongTensor(índices de clase para CrossEntropyLoss)
# ── Preprocesamiento ───────────────────────────────────────
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
# Convertir a tensores PyTorch
X_train_t = torch.FloatTensor(X_train)
X_test_t = torch.FloatTensor(X_test)
y_train_t = torch.LongTensor(y_train)
y_test_t = torch.LongTensor(y_test)
print(f"Train: {X_train_t.shape} | Test: {X_test_t.shape}")
print(f"Labels train: {y_train_t.shape} | Labels test: {y_test_t.shape}")
3. Arquitectura del MLP
Definimos un MLP con una capa oculta de 16 neuronas:
Entrada (4) → Linear(16) → ReLU → Linear(3) → Salida (logits)
Nota importante: no aplicamos Softmax en la última capa porque nn.CrossEntropyLoss de PyTorch ya incluye un LogSoftmax internamente. Esto es más estable numéricamente.
# ── Definición del MLP ──────────────────────────────────────
class IrisNet(nn.Module):
"""
MLP con 1 capa oculta para clasificación de 3 clases.
"""
def __init__(self):
super(IrisNet, self).__init__()
self.fc1 = nn.Linear(4, 16) # 4 features → 16 neuronas
self.relu = nn.ReLU() # Activación no lineal
self.fc2 = nn.Linear(16, 3) # 16 neuronas → 3 clases
def forward(self, x):
x = self.relu(self.fc1(x)) # Capa oculta + ReLU
x = self.fc2(x) # Salida (logits, sin softmax)
return x
model = IrisNet()
print(model)
print(f"\nParámetros totales: {sum(p.numel() for p in model.parameters()):,}")
4. Entrenamiento
Configuramos:
- Función de pérdida:
CrossEntropyLoss— la pérdida estándar para clasificación multiclase - Optimizador:
Adamcon learning rate = 0.01 - Epochs: 100 iteraciones sobre todo el dataset de entrenamiento
# ── Configuración del entrenamiento ────────────────────────
criterion = nn.CrossEntropyLoss() # Para clasificación multiclase
optimizer = optim.Adam(model.parameters(), lr=0.01)
# ── Bucle de entrenamiento ─────────────────────────────────
num_epochs = 100
losses = []
for epoch in range(num_epochs):
model.train()
# Forward pass
outputs = model(X_train_t) # Predicciones (logits)
loss = criterion(outputs, y_train_t) # Calcular pérdida
# Backward pass + actualización
optimizer.zero_grad() # Limpiar gradientes
loss.backward() # Backpropagation
optimizer.step() # Actualizar pesos
losses.append(loss.item())
if (epoch + 1) % 10 == 0:
print(f"Epoch [{epoch+1}/{num_epochs}] — Loss: {loss.item():.4f}")
# ── Curva de pérdida ───────────────────────────────────────
plt.figure(figsize=(10, 4))
plt.plot(losses, color="#6C5CE7", lw=2)
plt.xlabel("Epoch")
plt.ylabel("CrossEntropy Loss")
plt.title("Evolución de la pérdida durante el entrenamiento")
plt.grid(alpha=0.3)
plt.show()
Epoch 10, Loss: 0.9493270516395569 Epoch 20, Loss: 0.8226385712623596 Epoch 30, Loss: 0.7527078986167908 Epoch 40, Loss: 0.716499924659729 Epoch 50, Loss: 0.6858377456665039 Epoch 60, Loss: 0.6511966586112976 Epoch 70, Loss: 0.6232404112815857 Epoch 80, Loss: 0.6068210601806641 Epoch 90, Loss: 0.597316563129425 Epoch 100, Loss: 0.5918430685997009 Entrenamiento finalizado
5. Evaluación del modelo
Evaluamos el modelo en el conjunto de test usando torch.no_grad() para desactivar el cálculo de gradientes (ahorra memoria y es más rápido). Usamos torch.max() para obtener la clase predicha a partir de los logits.
# ── Evaluación en test ─────────────────────────────────────
model.eval()
with torch.no_grad():
outputs = model(X_test_t)
_, predicted = torch.max(outputs, dim=1)
correct = (predicted == y_test_t).sum().item()
total = y_test_t.size(0)
accuracy = 100 * correct / total
print(f"Resultados en test ({total} muestras):")
print(f" Correctas: {correct}")
print(f" Accuracy: {accuracy:.1f}%")
# ── Matriz de confusión ────────────────────────────────────
from sklearn.metrics import confusion_matrix, classification_report
cm = confusion_matrix(y_test_t.numpy(), predicted.numpy())
plt.figure(figsize=(6, 5))
sns.heatmap(cm, annot=True, fmt="d", cmap="Blues",
xticklabels=iris.target_names, yticklabels=iris.target_names)
plt.xlabel("Predicción")
plt.ylabel("Real")
plt.title(f"Matriz de confusión — Accuracy: {accuracy:.1f}%")
plt.tight_layout()
plt.show()
print("\nReporte de clasificación:")
print(classification_report(y_test_t.numpy(), predicted.numpy(), target_names=iris.target_names))
Precisión en el conjunto de prueba: 96.67%
6. Conclusiones
Con un MLP de una sola capa oculta (16 neuronas) y solo 100 epochs de entrenamiento, conseguimos una precisión muy alta en la clasificación de flores Iris.
Puntos clave del flujo PyTorch:
- Definir el modelo como subclase de
nn.Module - Elegir pérdida (
CrossEntropyLosspara clasificación) y optimizador (Adam) - El bucle:
zero_grad()→forward()→loss.backward()→optimizer.step() - Evaluar con
model.eval()ytorch.no_grad()
💡 Para explorar: prueba a cambiar el número de neuronas, añadir más capas, o cambiar la tasa de aprendizaje. ¿Cómo afecta a la accuracy y a la velocidad de convergencia?