Entrenar un modelo no basta. Después de entrenarlo, necesitamos responder una pregunta fundamental: ¿qué tan bueno es realmente? Esa pregunta nos lleva al tema de la evaluación.
En visión por computadora, evaluar un modelo significa medir su desempeño de una manera que sea útil, honesta y alineada con el problema real. No alcanza con mirar si la pérdida baja durante el entrenamiento. Tampoco alcanza, muchas veces, con una sola métrica global.
En este tema veremos cómo evaluar clasificadores de imágenes, qué métricas son más comunes, cuándo pueden engañarnos y cómo interpretar los resultados de manera más seria.
Porque un modelo puede parecer bueno durante entrenamiento y sin embargo comportarse mal en datos no vistos. También puede tener una accuracy aceptable pero fallar justo en la clase más importante del problema.
Evaluar bien sirve para:
Para evaluar correctamente, conviene distinguir tres subconjuntos:
Esta separación evita que terminemos optimizando el modelo sobre los mismos datos con los que luego pretendemos evaluarlo.
Una métrica es una forma numérica de resumir el desempeño del modelo. Pero cada métrica destaca un aspecto distinto del comportamiento.
Por eso no conviene pensar que existe una única cifra mágica que diga toda la verdad. Una métrica puede ser útil para un problema y poco informativa para otro.
La métrica más simple y conocida en clasificación es la accuracy. Mide qué fracción de ejemplos fue clasificada correctamente.
Su fórmula conceptual es:
accuracy = aciertos / total
Si de 100 imágenes el modelo acierta 87, la accuracy es 0.87 o 87%.
La accuracy es útil porque:
En problemas balanceados y relativamente simples, puede ser una métrica bastante razonable.
El problema es que la accuracy puede resultar engañosa cuando las clases están desbalanceadas. Imaginemos un dataset donde el 95% de las imágenes pertenecen a la misma clase. Un modelo que predice siempre esa clase tendrá 95% de accuracy y, sin embargo, será prácticamente inútil para detectar las clases minoritarias.
Por eso, especialmente en problemas reales, conviene complementar accuracy con otras métricas.
Una herramienta muy valiosa es la matriz de confusión. Esta matriz muestra cuántos ejemplos de cada clase real fueron predichos como cada clase posible.
En vez de resumir todo en una sola cifra, la matriz permite ver dónde se equivoca el modelo.
Esto es especialmente útil cuando queremos responder preguntas como:
En una matriz de confusión, normalmente:
La diagonal principal contiene los aciertos. Los valores fuera de la diagonal muestran confusiones entre clases.
Cuanto más concentrada esté la matriz en la diagonal, mejor estará funcionando el modelo.
Cuando una clase es especialmente importante, suelen aparecer dos métricas clave: precision y recall.
En un contexto binario:
Estas dos métricas capturan errores distintos y por eso conviene distinguirlas.
La precision responde a la pregunta: “cuando el modelo dice que sí, ¿cuánto puedo confiar en eso?”.
Si la precision es baja, significa que el modelo está produciendo muchos falsos positivos.
Esto es importante en problemas donde una alarma incorrecta tiene costo alto.
El recall responde a la pregunta: “de todo lo que realmente debía detectar, ¿cuánto logró encontrar?”.
Si el recall es bajo, significa que el modelo deja escapar muchos positivos reales.
Esto es crucial en problemas donde omitir un caso importante tiene consecuencias graves.
El F1-score combina precision y recall en una sola medida. No reemplaza su interpretación individual, pero puede servir como resumen cuando queremos equilibrar ambos aspectos.
Un F1 alto sugiere que el modelo no solo detecta bastante, sino que además comete relativamente pocos falsos positivos.
En clasificación multiclase, accuracy sigue siendo útil, pero precision, recall y F1 también pueden calcularse por clase. Esto permite estudiar el comportamiento del modelo de forma más detallada.
Por ejemplo, puede ocurrir que el modelo funcione muy bien en tres clases y muy mal en una cuarta. Una única accuracy global podría ocultar ese problema.
Cuando se calculan métricas multiclase, suelen aparecer distintas formas de agregación:
La elección depende del problema y del nivel de importancia que queramos dar a las clases minoritarias.
Aunque solemos mirar accuracy y otras métricas, la pérdida sigue siendo una señal importante. Dos modelos pueden tener accuracy parecida pero pérdidas diferentes. Eso puede reflejar diferencias en la confianza de sus predicciones.
Por eso conviene observar tanto la pérdida como las métricas finales de clasificación.
Durante el desarrollo del modelo solemos mirar la validación al final de cada época. Eso sirve para tomar decisiones: cambiar hiperparámetros, aplicar early stopping, guardar el mejor modelo, etc.
Pero la evaluación final ideal se hace sobre el conjunto de test, que debe permanecer lo más “intacto” posible durante el proceso de diseño.
Evaluar de forma honesta significa medir el modelo sobre datos que no hayan influido en las decisiones de desarrollo. Si usamos una y otra vez el conjunto de test para ajustar el sistema, ya no estamos midiendo generalización real, sino adaptación indirecta a ese test.
Además de métricas finales, es útil mirar la evolución de entrenamiento y validación a lo largo de las épocas. Eso ayuda a detectar:
La evaluación, entonces, no es solo una foto final: también es una observación del proceso.
En lugar de usar datos sintéticos, en este tema conviene trabajar con un problema clásico y real: MNIST, el dataset de dígitos manuscritos.
La idea será entrenar una CNN pequeña y luego calcular:
Así podremos ver cómo se implementa una evaluación más seria que simplemente imprimir un porcentaje de aciertos, pero ahora sobre imágenes reales de los números del 0 al 9.
En Python, una combinación muy práctica es:
scikit-learn para matriz de confusión y reporte de clasificación.Por ejemplo, funciones como confusion_matrix y classification_report facilitan mucho el análisis.
Este ejemplo descarga MNIST, entrena brevemente una CNN pequeña y luego la evalúa sobre el conjunto de test:
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
from sklearn.metrics import confusion_matrix, classification_report
class CNNPequena(nn.Module):
def __init__(self, num_clases=10):
super().__init__()
self.conv1 = nn.Conv2d(1, 16, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1)
self.pool = nn.MaxPool2d(2, 2)
self.fc1 = nn.Linear(32 * 7 * 7, 128)
self.fc2 = nn.Linear(128, num_clases)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = torch.flatten(x, start_dim=1)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return x
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
transform = transforms.ToTensor()
train_dataset = datasets.MNIST(
root="./data",
train=True,
download=True,
transform=transform
)
test_dataset = datasets.MNIST(
root="./data",
train=False,
download=True,
transform=transform
)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)
model = CNNPequena(num_clases=10).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# Entrenamiento breve
for epoch in range(2):
model.train()
for images, labels in train_loader:
images = images.to(device)
labels = labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# Evaluación
model.eval()
test_loss = 0.0
correct = 0
total = 0
y_true = []
y_pred = []
with torch.no_grad():
for images, labels in test_loader:
images = images.to(device)
labels = labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
test_loss += loss.item() * images.size(0)
preds = outputs.argmax(dim=1)
correct += (preds == labels).sum().item()
total += labels.size(0)
y_true.extend(labels.cpu().numpy())
y_pred.extend(preds.cpu().numpy())
avg_test_loss = test_loss / total
test_acc = correct / total
print(f"Test loss: {avg_test_loss:.4f}")
print(f"Test accuracy: {test_acc:.4f}")
print("\nMatriz de confusion:")
print(confusion_matrix(y_true, y_pred))
print("\nReporte de clasificacion:")
print(classification_report(y_true, y_pred, digits=4, zero_division=0))
Este ejemplo permite observar no solo una accuracy final, sino también cómo se distribuyen los errores entre los dígitos del 0 al 9.
Si al ejecutar el programa aparece una salida como esta:
Test loss: 1.3795
Test accuracy: 0.9720
Matriz de confusion:
[[ 970 0 1 0 0 1 3 1 4 0]
[ 0 1128 2 1 0 1 1 0 2 0]
[ 6 2 1001 4 2 0 3 8 6 0]
[ 1 0 8 972 0 9 0 6 9 5]
[ 1 0 2 0 947 0 6 1 2 23]
[ 4 1 0 12 1 857 7 1 7 2]
[ 7 2 0 0 4 7 935 0 3 0]
[ 1 5 12 2 1 0 0 992 2 13]
[ 6 0 3 7 3 5 4 3 938 5]
[ 5 4 1 5 12 5 1 7 4 965]]
En un caso como este, la accuracy de 0.9720 nos dice que el modelo acertó el 97.20% de las imágenes del conjunto de test. Es una métrica global útil, pero no alcanza por sí sola para entender dónde se equivoca.
La matriz de confusión agrega justamente esa información. Cada fila representa la clase real y cada columna la clase predicha. Lo ideal es que casi todos los valores queden sobre la diagonal principal, porque esa diagonal contiene los aciertos.
Por ejemplo:
970 en la primera fila y primera columna indica que 970 imágenes del dígito 0 fueron reconocidas correctamente como 0.23 en la última columna, lo que indica que varios cuatros fueron confundidos con nueves.Eso nos permite identificar qué pares de clases se parecen más para el modelo. En MNIST es normal que aparezcan confusiones entre dígitos con formas visuales parecidas, como 4 y 9, 3 y 5, o 7 y 9.
El classification_report profundiza todavía más el análisis. Allí veremos, para cada dígito, si el modelo fue preciso al predecirlo, si logró recuperarlo cuando realmente estaba presente y qué equilibrio hubo entre ambos aspectos.
El classification_report suele mostrar, para cada clase:
Esto ayuda mucho a detectar clases problemáticas que una accuracy global podría ocultar.
Aunque MNIST es un dataset real y muy útil para aprender, sigue siendo un problema relativamente simple comparado con tareas modernas de visión por computadora.
Las imágenes son pequeñas, en escala de grises y con una sola cifra centrada. En problemas reales suele haber más ruido, fondos complejos, variaciones de iluminación, múltiples objetos y clases mucho más difíciles de separar.
Por eso, una buena evaluación en MNIST no garantiza por sí sola que el mismo enfoque funcione igual de bien en un problema visual más exigente.
Algunos errores frecuentes son:
model.eval().torch.no_grad() durante inferencia.La evaluación es la etapa que transforma un entrenamiento en una conclusión confiable. Sin una buena evaluación, no sabemos si el modelo realmente aprendió algo útil o si solo se adaptó a las particularidades del conjunto de desarrollo.
Aprender a mirar accuracy, matriz de confusión, precision, recall y F1 no es un detalle accesorio: es parte esencial del trabajo en visión por computadora. Estas herramientas permiten pasar de una impresión superficial del rendimiento a una comprensión más profunda del comportamiento del sistema.
En el próximo tema avanzaremos hacia una estrategia muy importante en problemas reales: el transfer learning en visión por computadora, donde aprovecharemos modelos ya entrenados para acelerar y mejorar nuevos proyectos.