🏭 Caso de Uso

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.

🐍 Python 📓 Jupyter Notebook

🌸 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:

  1. Exploración de datos (EDA)
  2. Preprocesamiento y split train/test
  3. Definición del MLP en PyTorch
  4. Entrenamiento con CrossEntropyLoss + Adam
  5. 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()
Output

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()
Output

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: Adam con 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:

  1. Definir el modelo como subclase de nn.Module
  2. Elegir pérdida (CrossEntropyLoss para clasificación) y optimizador (Adam)
  3. El bucle: zero_grad()forward()loss.backward()optimizer.step()
  4. Evaluar con model.eval() y torch.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?