Módulo / 9. Visión artificial
0%
Módulo 9 · Avanzado

Visión artificial

Cómo una máquina pasa de una matriz de píxeles a una interpretación del mundo: filtros, convoluciones, arquitecturas CNN y las tareas fundamentales de la visión moderna.

8 lecciones 14 h estimadas Demo de convolución
0 de 8 lecciones completadas
Lección 9.1

Cómo ve una máquina: imágenes como tensores

16 minRepresentaciónCanales y color

Para una persona, una foto es un gato. Para un modelo, es un tensor de números. Entender qué representa cada dimensión —alto, ancho, canales— y qué se pierde al redimensionar es el primer paso de la visión artificial.

Al terminar sabrás

Interpretar una imagen como tensor $H\times W\times C$.
Explicar qué hacen la normalización y el redimensionado.
Reconocer el coste computacional de una imagen de alta resolución.
Evitar errores clásicos de canales y rango de valores.

El tensor de imagen

Dimensiones y rango $$X\in\mathbb{R}^{H\times W\times C}, \qquad X_{ijc}\in[0,1]\ \text{(float)} \quad\text{o}\quad \{0,\dots,255\}\ \text{(uint8)}$$ $H$ alto, $W$ ancho, $C$ canales. RGB tiene 3 canales; escala de grises, 1. En PyTorch el orden es $C\times H\times W$; en TensorFlow/Keras suele ser $H\times W\times C$.
1

Cuántos números hay en una foto

Una imagen de 224×224 en color tiene $224\cdot224\cdot3=150\,528$ valores. Una capa densa con 1 000 neuronas sobre esa entrada tendría $150\,528\,000$ pesos: demasiado para un problema pequeño y sin estructura espacial.

Una imagen de 4K (3840×2160×3) tiene casi 25 millones de valores. Una CNN con stride reduce rápidamente la resolución espacial; por eso las arquitecturas modernas trabajan a 224–512 píxeles y no a resolución completa.

Normalización y aumento de tamaño

Los modelos preentrenados esperan la misma normalización con la que fueron entrenados. ImageNet usa media y desviación por canal:

Normalización por canal $$\hat X_c=\frac{X_c-\mu_c}{\sigma_c}, \qquad \mu=(0.485,0.456,0.406), \quad \sigma=(0.229,0.224,0.225)$$ Estos valores son promedios sobre ImageNet en RGB con valores en [0,1]. Si usas otra normalización, el modelo preentrenado rinde peor aunque no dé error.
Rango de valores Pasar una imagen con valores 0–255 a un modelo que espera 0–1 no lanza una excepción: produce predicciones basura. Verifica siempre X.min(), X.max() y el tipo antes de inferir.

Lectura y preprocesado en Python

Pythonimagen.py
from PIL import Image
import numpy as np
import torch
from torchvision import transforms

img = Image.open("foto.jpg").convert("RGB")
print(img.size)                      # (ancho, alto)
arr = np.asarray(img)
print(arr.shape, arr.dtype, arr.min(), arr.max())   # (H, W, 3) uint8

# Pipeline estándar para un modelo preentrenado
preprocesar = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),                       # HWC uint8 -> CHW float [0,1]
    transforms.Normalize(
        mean=[0.485, 0.456, 0.406],
        std=[0.229, 0.224, 0.225]
    )
])

tensor = preprocesar(img)
print(tensor.shape, tensor.dtype, tensor.min().item(), tensor.max().item())

# Añadir dimensión de batch: (3,224,224) -> (1,3,224,224)
batch = tensor.unsqueeze(0)
print(batch.shape)

Resolución, detalle y coste

ResoluciónPíxeles por canalUso típicoCompromiso
28×28784Dígitos, Fashion-MNISTMuy rápido, poco detalle
224×22450 176Clasificación con modelos preentrenadosEstándar de facto
512×512262 144Detección, segmentaciónMás detalle, más memoria
1024×10241 048 576Imágenes médicas, satéliteRequiere recortes o arquitecturas eficientes
Recortes (tiles) Para imágenes grandes se divide en parches con solapamiento, se predice cada uno y se combinan. Es habitual en patología, teledetección y microscopía. El solapamiento evita artefactos en los bordes de cada parche.
Ejercicio 9.1 · Inspeccionar una imagen

Carga una imagen, imprime forma, tipo y rango. Convierte a grises, redimensiona a 224×224 y normaliza. Comprueba que la media y desviación por canal tras normalizar son aproximadamente 0 y 1. ¿Qué ocurre si olvidas dividir entre 255?

Orientación: sin dividir entre 255, los valores quedan en 0–255 y la normalización produce números grandes; las activaciones saturan y el modelo preentrenado falla. Verifica siempre el contrato de entrada del modelo.

Lección 9.2

Convolución, padding, stride y pooling

24 minOperación centralSimulador

La convolución es la operación que da a las CNN su eficiencia. Un filtro pequeño se comparte en todas las posiciones de la imagen, detectando el mismo patrón en cualquier lugar. Esa restricción —pesos compartidos y conectividad local— es una suposición sobre el mundo, no una limitación.

La operación paso a paso

El kernel se coloca sobre una ventana de la imagen, se multiplica elemento a elemento y se suma. El resultado es un valor del mapa de activación. El kernel se desplaza y se repite.

Convolución 2D discreta $$(I*K)(i,j)=\sum_{m=0}^{k-1}\sum_{n=0}^{k-1} I(i+m,\,j+n)\,K(m,n)$$ En deep learning, los valores de $K$ se aprenden. En visión clásica se diseñaban a mano: Sobel para bordes, Laplaciano para cambios de intensidad, media para suavizar.
⊞

Simulador · Convolución 3×3

Cambia el kernel y observa el mapa de activación que produce sobre una imagen sintética.

—
Los bordes aparecen donde el kernel detecta cambios bruscos. Un kernel de desenfoque produce valores suaves y pierde detalle.

Padding y stride

  • Padding: añade un borde de ceros (u otro valor) para controlar el tamaño de salida. same conserva el tamaño; valid lo reduce.
  • Stride: paso del kernel. Stride 2 reduce a la mitad la resolución espacial.
  • Dilatación: separa los elementos del kernel para ampliar el campo receptivo sin más parámetros.
Tamaño de salida $$H_{\text{out}}=\left\lfloor\frac{H_{\text{in}}+2P-K}{S}\right\rfloor+1, \qquad W_{\text{out}}=\left\lfloor\frac{W_{\text{in}}+2P-K}{S}\right\rfloor+1$$ Con $H_{\text{in}}=32$, $K=3$, $P=1$, $S=1$: salida 32. Con $S=2$: salida 16.
1

Detectar un borde vertical

Kernel de Sobel vertical: $\begin{pmatrix}-1&0&1\\-2&0&2\\-1&0&1\end{pmatrix}$. Sobre una región uniforme, la suma de productos es cero. Sobre un borde donde la intensidad salta de 0.2 a 0.9, la suma es aproximadamente $(0.9-0.2)\cdot4=2.8$: un valor alto que marca el borde.

Si el borde va en la dirección contraria, el resultado es negativo. Por eso a veces se toma el valor absoluto o se aplica ReLU después de la convolución.

Pooling

El max pooling toma el máximo de cada ventana; el average pooling, la media. Reducen la dimensión espacial, disminuyen el coste y aportan cierta invariancia a pequeños desplazamientos. No tienen parámetros.

Max pooling 2×2 $$y_{ij}=\max\{x_{2i,2j},\ x_{2i,2j+1},\ x_{2i+1,2j},\ x_{2i+1,2j+1}\}$$ Con stride 2, la salida tiene la mitad de alto y ancho. El máximo conserva la activación más fuerte de cada región.
¿Sigue usándose el pooling? Muchas arquitecturas modernas reemplazan el max pooling por convoluciones con stride o average pooling global. El pooling no desaparece, pero ya no es obligatorio. Su función principal —reducir resolución— se puede lograr de varias formas.

Implementación en PyTorch

Pythonconv_pytorch.py
import torch
import torch.nn as nn

# batch=1, canales=3, alto=32, ancho=32
x = torch.randn(1, 3, 32, 32)

conv = nn.Conv2d(
    in_channels=3,
    out_channels=16,
    kernel_size=3,
    stride=1,
    padding=1,          # "same"
    bias=True
)
print(conv(x).shape)     # torch.Size([1, 16, 32, 32])

conv_stride = nn.Conv2d(3, 16, kernel_size=3, stride=2, padding=1)
print(conv_stride(x).shape)  # torch.Size([1, 16, 16, 16])

pool = nn.MaxPool2d(kernel_size=2, stride=2)
print(pool(x).shape)     # torch.Size([1, 3, 16, 16])

# Número de parámetros de una convolución
pesos = 3 * 16 * 3 * 3
sesgos = 16
print("parámetros:", pesos + sesgos)   # 448

# Campo receptivo: dos conv 3x3 equivalen a una 5x5
def campo_receptivo(k, s=1, n=1):
    return 1 + sum((k - 1) * (s ** i) for i in range(n))
print(campo_receptivo(3, 1, 2))   # 5
print(campo_receptivo(3, 1, 3))   # 7
Padding y bordes El padding de ceros crea un borde artificial. En imágenes médicas o satelitales, los bordes pueden ser importantes: considera reflect o replicate, o recorta los bordes del mapa de salida. No es un detalle cosmético.
Ejercicio 9.2 · Calcular formas

Una entrada de 128×128×3 pasa por Conv(3×3, 32 filtros, padding 1, stride 1), MaxPool(2), Conv(3×3, 64, padding 1), MaxPool(2), Conv(3×3, 128, padding 1). Calcula la forma final y el número de parámetros de cada convolución.

Orientación: formas: 128→128→64→64→32→32. Parámetros: primera $3\cdot3\cdot3\cdot32+32=896$; segunda $3\cdot3\cdot32\cdot64+64=18\,496$; tercera $3\cdot3\cdot64\cdot128+128=73\,856$. Observa cómo el número de canales multiplica el coste.

Lección 9.3

Arquitecturas CNN: de LeNet a ResNet

20 minEvoluciónDiseño

Las arquitecturas CNN no son una lista de nombres para memorizar: cada una resolvió un problema concreto. Entender qué aportó cada idea te permite leer arquitecturas nuevas y elegir una adecuada para tu restricción de datos y cómputo.

La evolución en cinco ideas

ModeloAñoAporte clavePor qué importó
LeNet-51998Conv + pooling + densasDemostró que la convolución funciona en dígitos
AlexNet2012ReLU, dropout, GPU, augmentationGanó ImageNet y abrió la era deep learning
VGG2014Bloques de 3×3 apiladosMostró que la profundidad importa y simplificó el diseño
ResNet2015Conexiones residualesPermitió entrenar 100+ capas sin degradación
EfficientNet2019Escalado compuestoMejor equilibrio precisión/coste

El bloque residual

Residual connection $$\mathbf{y}=\mathcal{F}(\mathbf{x},\{W_i\})+\mathbf{x}$$ La red aprende una corrección $\mathcal{F}$ sobre la identidad. Si una capa no aporta, puede aprender $\mathcal{F}\approx0$ sin destruir la señal. El gradiente fluye directamente por la conexión.

Sin conexiones residuales, una red muy profunda sufre degradación: el error de entrenamiento aumenta al añadir capas, aunque no haya overfitting. ResNet demostró que la profundidad podía ser una ventaja si se facilitaba la optimización.

1

Por qué 3×3 apilados son eficientes

Dos convoluciones 3×3 con $C$ canales tienen $2\cdot9C^2=18C^2$ parámetros y un campo receptivo de 5×5. Una sola convolución 5×5 tiene $25C^2$ parámetros. Además, entre las dos capas hay una no linealidad, lo que aumenta la capacidad expresiva.

Tres convoluciones 3×3 cubren 7×7 con $27C^2$ parámetros frente a $49C^2$ de una 7×7. Esa es la lógica de VGG y de muchas arquitecturas posteriores.

Diseñar una CNN razonable

  1. Empieza con un modelo preentrenado si tu dataset no es enorme.
  2. Si entrenas desde cero, usa bloques Conv–BN–ReLU y reduce la resolución progresivamente.
  3. Duplica canales al reducir resolución para mantener la capacidad.
  4. Termina con average pooling global y una capa densa (o directamente una convolución 1×1).
  5. Ajusta la profundidad a los datos: demasiada capacidad con pocos ejemplos garantiza overfitting.
Pythonresnet_mini.py
import torch
import torch.nn as nn

class BloqueResidual(nn.Module):
    def __init__(self, canales):
        super().__init__()
        self.conv1 = nn.Conv2d(canales, canales, 3, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(canales)
        self.conv2 = nn.Conv2d(canales, canales, 3, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(canales)
        self.relu = nn.ReLU(inplace=True)

    def forward(self, x):
        identidad = x
        out = self.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        return self.relu(out + identidad)

class MiniResNet(nn.Module):
    def __init__(self, n_classes=10):
        super().__init__()
        self.stem = nn.Sequential(
            nn.Conv2d(3, 32, 3, padding=1, bias=False),
            nn.BatchNorm2d(32), nn.ReLU(inplace=True)
        )
        self.bloques = nn.Sequential(
            BloqueResidual(32),
            nn.MaxPool2d(2),
            BloqueResidual(32),
            nn.MaxPool2d(2),
            BloqueResidual(32)
        )
        self.pool = nn.AdaptiveAvgPool2d(1)
        self.fc = nn.Linear(32, n_classes)

    def forward(self, x):
        x = self.bloques(self.stem(x))
        x = self.pool(x).flatten(1)
        return self.fc(x)

modelo = MiniResNet()
print(sum(p.numel() for p in modelo.parameters()), "parámetros")
Vision Transformers (ViT) Los ViT dividen la imagen en parches, los tratan como tokens y aplican self-attention. Con datasets pequeños rinden peor que las CNN; con datasets enormes o preentrenamiento a gran escala, compiten o superan. La elección depende de datos y recursos, no de moda.
Ejercicio 9.3 · Leer una arquitectura

Elige una arquitectura del torchvision.models (por ejemplo, ResNet-18) e imprime su estructura. Identifica el stem, los bloques, el pooling global y la cabeza. Calcula cuántos parámetros tiene la cabeza y cuántos el extractor.

Orientación: en ResNet-18, el extractor concentra casi todos los parámetros y la cabeza es una única capa lineal de 512 a 1 000 clases. Al hacer transfer learning, reemplazas la cabeza y congelas el extractor; por eso el entrenamiento es rápido y requiere pocos datos.

Lección 9.4

Entrenamiento de CNN y data augmentation

22 minRegularizaciónPipeline

Entrenar una CNN es un problema de ingeniería además de uno de modelado. El augmentation, la normalización, el balanceo y el control del overfitting importan tanto como la arquitectura. Y no todas las transformaciones son válidas para todos los dominios.

Data augmentation: qué es válido

TransformaciónÚtil enPeligrosa en
Volteo horizontalObjetos naturales, escenasTexto, dígitos, señales de tráfico asimétricas
Rotación pequeñaFotos, microscopíaImágenes con orientación semántica (radar, documentos)
Recorte aleatorioObjetos grandesObjetos pequeños o lesiones localizadas
Cambio de brillo/contrasteCasi todas las fotosImágenes calibradas cuantitativamente
Mezcla (Mixup/CutMix)Clasificación naturalDatos médicos con etiquetas por región
El augmentation debe preservar la etiqueta Si volteas un dígito «6», puede parecer un «9». Si recortas una lesión pequeña, la imagen queda etiquetada como positiva sin evidencia. Antes de aumentar, pregunta: ¿un especialista seguiría asignando la misma etiqueta a esta transformación?

Contrato de entrenamiento

Pythontrain_cnn.py
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms, models

train_tf = transforms.Compose([
    transforms.RandomResizedCrop(224, scale=(0.7, 1.0)),
    transforms.RandomHorizontalFlip(),
    transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.1),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])

val_tf = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])

train_ds = datasets.ImageFolder("datos/train", transform=train_tf)
val_ds = datasets.ImageFolder("datos/val", transform=val_tf)

train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4)
val_loader = DataLoader(val_ds, batch_size=64, shuffle=False, num_workers=4)

# Transfer learning
modelo = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
for p in modelo.parameters():
    p.requires_grad = False
modelo.fc = nn.Linear(modelo.fc.in_features, len(train_ds.classes))

device = "cuda" if torch.cuda.is_available() else "cpu"
modelo = modelo.to(device)

criterio = nn.CrossEntropyLoss(label_smoothing=0.1)
optimizador = torch.optim.AdamW(modelo.fc.parameters(), lr=1e-3)

mejor_f1 = 0.0
for epoch in range(10):
    modelo.train()
    for xb, yb in train_loader:
        xb, yb = xb.to(device), yb.to(device)
        optimizador.zero_grad()
        loss = criterio(modelo(xb), yb)
        loss.backward()
        optimizador.step()

    # Validación (omitida para brevedad: calcula F1 y guarda el mejor estado)
    print(f"epoch {epoch} terminada")

Diagnóstico con curvas

Underfitting

Train y val altas

Más capacidad, mejor augmentation o menos regularización. Revisa que las etiquetas y el preprocesado sean correctos.

Overfitting

Train baja, val alta

Más augmentation, weight decay, dropout, early stopping o transfer learning. También revisa el tamaño del dataset.

Inestable

Val oscila mucho

Learning rate alta, batch pequeño o clase desbalanceada. Prueba warmup, gradient clipping o un batch mayor.

No aprende

Pérdida plana

Preprocesado incorrecto, etiquetas desalineadas, learning rate absurda o capas congeladas por error.

Congelar y descongelar Con pocos datos, entrena solo la cabeza unas épocas y luego descongela las últimas capas con una tasa de aprendizaje menor (por ejemplo, 10× menor). El fine-tuning completo desde el inicio puede destruir las representaciones preentrenadas.
Ejercicio 9.4 · Augmentation con criterio

Diseña el augmentation para (a) clasificar especies de plantas desde fotos de campo, (b) detectar neumonía en radiografías, (c) leer dígitos manuscritos. Justifica qué transformaciones son válidas y cuáles no.

Orientación: (a) recortes, giros, cambios de luz y color son válidos; (b) giros ligeros y cambios de contraste moderados, pero no voltear si cambia la lateralidad anatómica ni recortar lesiones; (c) pequeñas rotaciones y traslaciones, sin voltear horizontalmente porque «6» y «9» se confunden.

Lección 9.5

Clasificación de imágenes de principio a fin

20 minPipelineEvaluación

Un clasificador de imágenes no termina en el modelo: empieza en la definición del problema y termina en una matriz de confusión interpretada. Este es el esqueleto que puedes reutilizar en cualquier proyecto.

Las decisiones antes del código

  1. ¿Qué es una clase? Definir fronteras ambiguas (por ejemplo, «vehículo») produce etiquetas inconsistentes.
  2. ¿Qué se ve en producción? Si el modelo se usará con fotos de móvil, entrena con fotos de móvil, no solo con imágenes de estudio.
  3. ¿Cuál es el coste de cada error? Decide la métrica antes de entrenar.
  4. ¿Cómo se agrupan los datos? Si hay varias fotos del mismo objeto o paciente, agrupa para evitar fuga.
1

Accuracy alta, utilidad nula

Un dataset de inspección industrial tiene 97 % de piezas correctas y 3 % defectuosas. Un modelo que predice siempre «correcta» obtiene 97 % de accuracy y no detecta ningún defecto. El recall de la clase defectuosa es 0.

En este caso, la métrica útil es el recall de defectos con una precisión mínima aceptable, o el coste esperado de los errores. La accuracy solo sirve para descartar modelos que ni siquiera superan la clase mayoritaria.

Pipeline con transfer learning

Pythonclasificacion.py
import torch
import torch.nn as nn
from torchvision import models

def crear_modelo(n_clases, congelar=True):
    modelo = models.efficientnet_b0(weights=models.EfficientNet_B0_Weights.DEFAULT)
    if congelar:
        for p in modelo.features.parameters():
            p.requires_grad = False
    en_features = modelo.classifier[1].in_features
    modelo.classifier[1] = nn.Linear(en_features, n_clases)
    return modelo

modelo = crear_modelo(n_clases=5)

# Dos fases: cabeza primero, fine-tuning después
fase1 = torch.optim.AdamW(modelo.classifier.parameters(), lr=1e-3)

# Tras 3-5 épocas, descongelar el último bloque
for p in modelo.features[-2:].parameters():
    p.requires_grad = True
fase2 = torch.optim.AdamW([
    {"params": modelo.features[-2:].parameters(), "lr": 1e-5},
    {"params": modelo.classifier.parameters(), "lr": 1e-4}
])

# Evaluación: matriz de confusión y métricas por clase
from sklearn.metrics import confusion_matrix, classification_report
modelo.eval()
with torch.no_grad():
    logits = modelo(X_test_tensor)
    preds = logits.argmax(dim=1).cpu().numpy()

print(classification_report(y_test, preds, target_names=clases, digits=3))
print(confusion_matrix(y_test, preds))

Qué revisar antes de dar por bueno el modelo

  • Matriz de confusión: ¿qué clases se confunden y tiene sentido?
  • Ejemplos de alta confianza incorrectos: suelen revelar etiquetas erróneas o atajos del dataset.
  • Rendimiento por subgrupo: iluminación, dispositivo, hospital, demografía si está disponible.
  • Sensibilidad al preprocesado: recortes, compresión JPEG, cambios de resolución.
  • Calibración: si usas la probabilidad para decidir, comprueba que esté calibrada.
Grad-CAM para inspección Grad-CAM resalta las regiones que más contribuyen a la predicción. No prueba causalidad, pero ayuda a detectar atajos: un modelo que mira el fondo o una marca de agua en lugar del objeto suele delatarse. Úsalo como herramienta de auditoría, no como explicación definitiva.
Ejercicio 9.5 · Del problema a la métrica

Para cada caso elige la métrica principal y justifica: (a) clasificar 10 especies de aves con clases balanceadas; (b) detectar grietas en hormigón con 2 % de positivos; (c) clasificar variedades de vino con 5 clases y coste similar de error.

Orientación: (a) accuracy o F1 macro, con matriz de confusión; (b) recall de grietas a precisión fija, o coste esperado; (c) accuracy estratificada y F1 macro; en todos, reportar por clase y revisar errores de alta confianza.

Lección 9.6

Detección de objetos

22 minBounding boxesIoU y mAP

Clasificar una imagen entera responde «¿qué hay?». Detectar responde «¿qué hay y dónde?». Ese «dónde» convierte el problema en uno de regresión estructurada: hay que predecir coordenadas además de clases.

Salidas de un detector

Un detector devuelve una lista de detecciones. Cada una tiene una clase, una caja delimitadora $(x_1,y_1,x_2,y_2)$ o $(x_c,y_c,w,h)$, y una puntuación de confianza. El número de objetos no se conoce de antemano.

Intersección sobre unión (IoU) $$\text{IoU}(A,B)=\frac{|A\cap B|}{|A\cup B|}$$ Mide el solapamiento entre la caja predicha y la real. IoU = 0 no se tocan; IoU = 1 son idénticas. Un umbral típico para considerar una detección correcta es 0.5.

Dos familias de detectores

FamiliaIdeaEjemplosCompromiso
Dos etapasPropone regiones y luego clasifica/refinaFaster R-CNN, Mask R-CNNMás precisas, más lentas
Una etapaPredice cajas y clases directamente en una pasadaYOLO, SSD, RetinaNetMás rápidas, precisión competitiva
TransformerConsulta objetos con attentionDETR y variantesDiseño más simple, requiere más datos/épocas
1

Calcular IoU

Caja real: $(0,0,10,10)$ con área 100. Caja predicha: $(5,5,15,15)$ con área 100. La intersección es $(5,5,10,10)$ con área 25. La unión es $100+100-25=175$. IoU $=25/175\approx0.143$.

Con umbral 0.5, esta detección no se considera correcta aunque la clase sea la correcta. El detector debe localizar mejor. En objetos pequeños, pequeños desplazamientos en píxeles producen caídas grandes de IoU.

NMS: eliminar duplicados

Un detector genera muchas cajas alrededor del mismo objeto. Non-Maximum Suppression ordena por confianza, conserva la mejor y elimina las que se solapan con ella por encima de un umbral. Sin NMS, la salida sería una lista de cajas casi idénticas.

mAP: la métrica estándar

Para cada clase se calcula la curva precisión–recall variando el umbral de confianza, y se obtiene el average precision (AP). La mean Average Precision promedia AP sobre clases. COCO reporta mAP con varios umbrales de IoU (0.5:0.95) y para objetos de distintos tamaños.

No compares mAP entre datasets La mAP depende del conjunto, de las clases, del umbral de IoU y de la definición de «detección correcta». Un 0.45 en COCO y un 0.45 en tu dataset no son comparables. Reporta siempre la configuración.

Código: inferencia con un detector preentrenado

Pythondeteccion.py
import torch
from torchvision import models, transforms
from PIL import Image, ImageDraw

pesos = models.detection.FasterRCNN_ResNet50_FPN_V2_Weights.DEFAULT
modelo = models.detection.fasterrcnn_resnet50_fpn_v2(weights=pesos).eval()

preprocesar = transforms.Compose([transforms.ToTensor()])
img = Image.open("calle.jpg").convert("RGB")
tensor = preprocesar(img)

with torch.no_grad():
    salida = modelo([tensor])[0]

# Filtrar por confianza
umbral = 0.6
dibujo = ImageDraw.Draw(img)
for caja, etiqueta, score in zip(
    salida["boxes"], salida["labels"], salida["scores"]
):
    if score < umbral:
        continue
    x1, y1, x2, y2 = caja.tolist()
    dibujo.rectangle([x1, y1, x2, y2], outline="red", width=3)
    dibujo.text((x1 + 4, y1 + 4), f"{pesos.meta['categories'][etiqueta]} {score:.2f}")

img.save("detecciones.jpg")
Objetos pequeños La detección de objetos pequeños es un problema abierto. Aumentar la resolución de entrada, usar feature pyramids o dividir la imagen en tiles suele ayudar. Mide el rendimiento por tamaño de objeto: un mAP global puede ocultar que los objetos pequeños no se detectan.
Ejercicio 9.6 · Evaluar un detector

Un detector obtiene 40 detecciones correctas, 10 falsos positivos y 15 objetos reales no detectados para una clase. Calcula precisión y recall a ese umbral. ¿Qué cambiarías si el coste de no detectar es muy alto?

Orientación: precisión $=40/50=0.8$; recall $=40/55\approx0.727$. Si el coste de no detectar es alto, baja el umbral de confianza (sube recall, baja precisión) y revisa la curva PR. Reporta también el rendimiento por tamaño de objeto y por condición de iluminación.

Lección 9.7

Segmentación semántica e instancias

20 minPíxelesU-Net y Mask R-CNN

La segmentación asigna una etiqueta a cada píxel. Es la tarea de visión con mayor resolución de salida y la base de aplicaciones médicas, conducción autónoma y análisis satelital.

Tres variantes

TipoSalidaDistingue instanciasEjemplo
SemánticaUna clase por píxelNoTodos los coches son «coche»
De instanciasUna máscara por objetoSíCoche 1, coche 2, persona 1
PanópticaClases + instancias + fondoSíEscena completa etiquetada

U-Net: encoder, decoder y conexiones de salto

U-Net fue diseñada para imágenes médicas. El encoder reduce la resolución y extrae contexto; el decoder recupera la resolución espacial. Las conexiones de salto copian características del encoder al decoder para recuperar detalles finos que se pierden al reducir.

Pérdida por píxel $$L=-\frac{1}{N}\sum_{i=1}^{N}\sum_{c=1}^{C}y_{ic}\log\hat y_{ic}$$ Entropía cruzada por píxel. Con clases muy desbalanceadas (lesiones pequeñas), se usa Dice loss o una combinación para evitar que el fondo domine.
1

Por qué la accuracy por píxel engaña

Una imagen de 512×512 tiene 262 144 píxeles. Si solo el 1 % corresponde a una lesión, predecir «fondo» en todos los píxeles da 99 % de accuracy y una segmentación inútil. El IoU de la clase lesión sería 0.

Métricas habituales: Dice $=2|A\cap B|/(|A|+|B|)$ e IoU por clase. Ambas se calculan sobre la clase de interés y se promedian. Reporta siempre la clase minoritaria.

Código: U-Net mínima

Pythonunet.py
import torch
import torch.nn as nn

class Bloque(nn.Module):
    def __init__(self, entrada, salida):
        super().__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(entrada, salida, 3, padding=1, bias=False),
            nn.BatchNorm2d(salida), nn.ReLU(inplace=True),
            nn.Conv2d(salida, salida, 3, padding=1, bias=False),
            nn.BatchNorm2d(salida), nn.ReLU(inplace=True)
        )

    def forward(self, x):
        return self.conv(x)

class UNet(nn.Module):
    def __init__(self, n_clases=2):
        super().__init__()
        self.enc1 = Bloque(3, 64)
        self.enc2 = Bloque(64, 128)
        self.enc3 = Bloque(128, 256)
        self.pool = nn.MaxPool2d(2)
        self.up2 = nn.ConvTranspose2d(256, 128, 2, stride=2)
        self.dec2 = Bloque(256, 128)
        self.up1 = nn.ConvTranspose2d(128, 64, 2, stride=2)
        self.dec1 = Bloque(128, 64)
        self.salida = nn.Conv2d(64, n_clases, 1)

    def forward(self, x):
        e1 = self.enc1(x)              # 64
        e2 = self.enc2(self.pool(e1))  # 128, 1/2
        e3 = self.enc3(self.pool(e2))  # 256, 1/4
        d2 = self.dec2(torch.cat([self.up2(e3), e2], dim=1))
        d1 = self.dec1(torch.cat([self.up1(d2), e1], dim=1))
        return self.salida(d1)

modelo = UNet(n_clases=2)
print(modelo(torch.randn(1, 3, 256, 256)).shape)   # (1,2,256,256)
Segmentación con modelos preentrenados torchvision y segmentation_models_pytorch ofrecen encoders preentrenados (ResNet, EfficientNet) para U-Net y FPN. En datasets pequeños, un encoder preentrenado marca una diferencia enorme frente a entrenar desde cero.

Errores frecuentes

  • Redimensionar máscaras con interpolación bilineal y crear clases inexistentes: usa nearest neighbor.
  • No alinear máscara e imagen tras augmentation: aplica las mismas transformaciones geométricas a ambas.
  • Evaluar solo en imágenes fáciles: estratifica por dificultad, tamaño de lesión y centro.
  • Ignorar el desbalance: usa Dice/Focal loss y reporta IoU por clase.
Ejercicio 9.7 · Métricas de segmentación

Una máscara real tiene 200 píxeles de lesión; la predicción tiene 150, de los cuales 120 coinciden con la real. Calcula precisión, recall, Dice e IoU de la clase lesión.

Orientación: TP=120, FP=30, FN=80. Precisión $=120/150=0.8$; recall $=120/200=0.6$; Dice $=2\cdot120/(150+200)\approx0.686$; IoU $=120/(120+30+80)=0.522$. Reporta los cuatro: cada uno cuenta una historia distinta.

Lección 9.8

Transfer learning y despliegue de visión

20 minProducciónCierre del módulo

Un modelo de visión que solo funciona en tu portátil no es un producto. El despliegue impone restricciones de latencia, memoria, tamaño y robustez que a menudo obligan a cambiar de arquitectura o a optimizar el modelo.

Transfer learning: la receta práctica

  1. Elige un modelo preentrenado en un dominio razonablemente cercano.
  2. Reemplaza la cabeza y entrena solo ella con el extractor congelado.
  3. Si el rendimiento se estanca y tienes datos, descongela las últimas capas con lr baja.
  4. Ajusta la resolución de entrada a la del preentrenamiento o reentrena la cabeza con la nueva resolución.
  5. Evalúa con el mismo preprocesado que usarás en producción.
1

Congelar no siempre es suficiente

Si tu dominio es muy distinto (por ejemplo, imágenes térmicas o microscopía de fluorescencia), las primeras capas del modelo preentrenado detectan bordes y texturas genéricas que aún sirven, pero las capas profundas pueden ser irrelevantes. Congelar todo y entrenar solo la cabeza puede quedarse corto. Descongela progresivamente y mide.

Optimización para inferencia

TécnicaQué haceCoste
CuantizaciónPasa pesos de float32 a int8Pequeña pérdida de precisión
PruningElimina pesos o canales poco importantesRequiere reentrenamiento
DestilaciónEntrena un modelo pequeño para imitar uno grandeNecesita datos y tiempo
ONNX / TensorRTOptimiza el grafo para el hardwareConversión y validación
Batch inferenceProcesa varias imágenes a la vezMás latencia por petición, más throughput
Pythonexportar_onnx.py
import torch

modelo.eval()
ejemplo = torch.randn(1, 3, 224, 224)

torch.onnx.export(
    modelo,
    ejemplo,
    "modelo.onnx",
    input_names=["imagen"],
    output_names=["logits"],
    dynamic_axes={"imagen": {0: "batch"}, "logits": {0: "batch"}},
    opset_version=17
)

# Verificar que ONNX produce la misma salida
import onnxruntime as ort
import numpy as np

sesion = ort.InferenceSession("modelo.onnx", providers=["CPUExecutionProvider"])
salida_onnx = sesion.run(None, {"imagen": ejemplo.numpy()})[0]
with torch.no_grad():
    salida_torch = modelo(ejemplo).numpy()

print("diferencia máxima:", np.abs(salida_onnx - salida_torch).max())

# Cuantización dinámica (reduce tamaño ~4x)
modelo_cuantizado = torch.quantization.quantize_dynamic(
    modelo, {torch.nn.Linear, torch.nn.Conv2d}, dtype=torch.qint8
)
torch.save(modelo_cuantizado.state_dict(), "modelo_int8.pt")

Robustez en producción

  • Distribución de entrada: cámaras, iluminación y compresión cambian. Monitorea estadísticas de entrada y rendimiento en muestras etiquetadas.
  • Casos límite: imágenes borrosas, oscuras, rotadas o de otro dominio deben tener un comportamiento definido (abstención o alerta).
  • Latencia: mide en el hardware real, no en el portátil. Incluye preprocesado y postprocesado.
  • Privacidad: las imágenes pueden contener datos personales. Define retención, anonimización y acceso.
El preprocesado es parte del modelo Si entrenaste con Resize(256) + CenterCrop(224) y en producción redimensionas directamente a 224, cambias el encuadre y la escala. El rendimiento cae sin errores visibles. Versiona el pipeline de preprocesado junto con los pesos.
Ejercicio 9.8 · Plan de despliegue

Debes desplegar un clasificador de defectos en una línea de producción con cámara industrial y latencia máxima de 50 ms por pieza. Diseña el plan: modelo, resolución, optimización, umbral y monitoreo. ¿Qué harías si el modelo tarda 120 ms?

Orientación: empezar con un modelo pequeño preentrenado, medir latencia real de extremo a extremo, probar resolución menor y cuantización int8, y considerar destilación. Definir umbral por coste de falso negativo y monitorear la tasa de abstención y la distribución de imágenes.

Curso de Machine Learning · teoría, matemáticas y práctica en Python Progreso local · 0% completado · Hecho para estudiar en cualquier lugar