Visión por Computadora con PyTorch en 2026: CNN, Transfer Learning y Preguntas de Entrevista

Guía completa sobre visión por computadora con PyTorch: redes neuronales convolucionales, transfer learning y preparación para entrevistas técnicas en deep learning.

Visión por Computadora con PyTorch en 2026: CNN, Transfer Learning y Preguntas de Entrevista

La visión por computadora se ha consolidado como uno de los campos más relevantes dentro de la inteligencia artificial. Mediante PyTorch, los desarrolladores y científicos de datos pueden construir modelos avanzados para clasificación de imágenes, detección de objetos y segmentación semántica. Esta guía aborda los fundamentos de las CNN, técnicas de transfer learning y las preguntas más frecuentes en entrevistas técnicas.

El dominio de las CNN y el transfer learning representa una ventaja competitiva significativa para posiciones en machine learning. Los reclutadores frecuentemente evalúan la comprensión práctica de estos conceptos durante las entrevistas técnicas.

Fundamentos de las Redes Neuronales Convolucionales

Las redes neuronales convolucionales (CNN) constituyen la arquitectura fundamental para el procesamiento de imágenes. A diferencia de las redes fully-connected, las CNN aprovechan la estructura espacial de los datos visuales mediante operaciones de convolución.

La arquitectura típica de una CNN incluye varios tipos de capas: las capas de convolución extraen características locales, las capas de pooling reducen la dimensionalidad, y las capas fully-connected realizan la clasificación final.

python
import torch
import torch.nn as nn
import torch.nn.functional as F

class SimpleCNN(nn.Module):
    def __init__(self, num_classes: int = 10):
        super(SimpleCNN, self).__init__()
        # Convolutional layers
        self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
        self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
        
        # Batch normalization
        self.bn1 = nn.BatchNorm2d(32)
        self.bn2 = nn.BatchNorm2d(64)
        self.bn3 = nn.BatchNorm2d(128)
        
        # Pooling and dropout
        self.pool = nn.MaxPool2d(2, 2)
        self.dropout = nn.Dropout(0.5)
        
        # Fully connected layers
        self.fc1 = nn.Linear(128 * 4 * 4, 512)
        self.fc2 = nn.Linear(512, num_classes)
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        # Block 1: 32x32 -> 16x16
        x = self.pool(F.relu(self.bn1(self.conv1(x))))
        
        # Block 2: 16x16 -> 8x8
        x = self.pool(F.relu(self.bn2(self.conv2(x))))
        
        # Block 3: 8x8 -> 4x4
        x = self.pool(F.relu(self.bn3(self.conv3(x))))
        
        # Flatten and classify
        x = x.view(-1, 128 * 4 * 4)
        x = self.dropout(F.relu(self.fc1(x)))
        x = self.fc2(x)
        
        return x

Este modelo ilustra los componentes esenciales: batch normalization estabiliza el entrenamiento, dropout previene el sobreajuste, y max pooling reduce progresivamente las dimensiones espaciales.

Configuración del Entorno y Preparación de Datos

Antes de entrenar un modelo, la preparación de datos constituye una etapa crítica. PyTorch proporciona herramientas robustas para la carga y aumentación de datos.

python
import torch
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
from typing import Tuple

def create_data_loaders(
    data_dir: str,
    batch_size: int = 32,
    num_workers: int = 4
) -> Tuple[DataLoader, DataLoader]:
    """Create training and validation data loaders with augmentation."""
    
    # Training transforms with augmentation
    train_transform = transforms.Compose([
        transforms.RandomResizedCrop(224),
        transforms.RandomHorizontalFlip(),
        transforms.RandomRotation(15),
        transforms.ColorJitter(
            brightness=0.2, 
            contrast=0.2, 
            saturation=0.2
        ),
        transforms.ToTensor(),
        transforms.Normalize(
            mean=[0.485, 0.456, 0.406],
            std=[0.229, 0.224, 0.225]
        )
    ])
    
    # Validation transforms (no augmentation)
    val_transform = transforms.Compose([
        transforms.Resize(256),
        transforms.CenterCrop(224),
        transforms.ToTensor(),
        transforms.Normalize(
            mean=[0.485, 0.456, 0.406],
            std=[0.229, 0.224, 0.225]
        )
    ])
    
    # Create datasets
    train_dataset = datasets.ImageFolder(
        root=f"{data_dir}/train",
        transform=train_transform
    )
    
    val_dataset = datasets.ImageFolder(
        root=f"{data_dir}/val",
        transform=val_transform
    )
    
    # Create data loaders
    train_loader = DataLoader(
        train_dataset,
        batch_size=batch_size,
        shuffle=True,
        num_workers=num_workers,
        pin_memory=True
    )
    
    val_loader = DataLoader(
        val_dataset,
        batch_size=batch_size,
        shuffle=False,
        num_workers=num_workers,
        pin_memory=True
    )
    
    return train_loader, val_loader

La aumentación de datos mejora la robustez del modelo al crear variaciones artificiales de las imágenes de entrenamiento. Las transformaciones geométricas y colorimétricas incrementan la diversidad del dataset sin requerir datos adicionales.

Transfer Learning con Modelos Preentrenados

El transfer learning permite aprovechar modelos entrenados en datasets extensos como ImageNet. Este enfoque reduce significativamente el tiempo de entrenamiento y mejora el rendimiento, especialmente con datasets limitados.

python
import torch
import torch.nn as nn
from torchvision import models
from torchvision.models import ResNet50_Weights, EfficientNet_B0_Weights

def create_transfer_model(
    model_name: str,
    num_classes: int,
    freeze_backbone: bool = True
) -> nn.Module:
    """Create a transfer learning model with a custom classifier."""
    
    if model_name == "resnet50":
        # Load pre-trained ResNet50
        weights = ResNet50_Weights.IMAGENET1K_V2
        model = models.resnet50(weights=weights)
        
        # Freeze backbone if specified
        if freeze_backbone:
            for param in model.parameters():
                param.requires_grad = False
        
        # Replace classifier
        num_features = model.fc.in_features
        model.fc = nn.Sequential(
            nn.Dropout(0.5),
            nn.Linear(num_features, 512),
            nn.ReLU(),
            nn.Dropout(0.3),
            nn.Linear(512, num_classes)
        )
        
    elif model_name == "efficientnet_b0":
        # Load pre-trained EfficientNet
        weights = EfficientNet_B0_Weights.IMAGENET1K_V1
        model = models.efficientnet_b0(weights=weights)
        
        if freeze_backbone:
            for param in model.parameters():
                param.requires_grad = False
        
        # Replace classifier
        num_features = model.classifier[1].in_features
        model.classifier = nn.Sequential(
            nn.Dropout(0.5),
            nn.Linear(num_features, num_classes)
        )
    
    else:
        raise ValueError(f"Unknown model: {model_name}")
    
    return model


def unfreeze_layers(model: nn.Module, num_layers: int) -> None:
    """Gradually unfreeze layers for fine-tuning."""
    # Get all parameters as a list
    params = list(model.parameters())
    
    # Unfreeze the last n layers
    for param in params[-num_layers:]:
        param.requires_grad = True

La estrategia de congelamiento progresivo de capas optimiza el proceso de entrenamiento. Las primeras capas, que detectan características genéricas como bordes y texturas, permanecen congeladas. Las capas superiores, específicas de la tarea, se entrenan con el nuevo dataset.

Bucle de Entrenamiento Completo

Un bucle de entrenamiento robusto integra validación, seguimiento de métricas y guardado de los mejores modelos.

python
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from typing import Dict, List
import copy

class Trainer:
    def __init__(
        self,
        model: nn.Module,
        train_loader: DataLoader,
        val_loader: DataLoader,
        learning_rate: float = 1e-4,
        device: str = "cuda"
    ):
        self.model = model.to(device)
        self.train_loader = train_loader
        self.val_loader = val_loader
        self.device = device
        
        self.criterion = nn.CrossEntropyLoss()
        self.optimizer = optim.AdamW(
            filter(lambda p: p.requires_grad, model.parameters()),
            lr=learning_rate,
            weight_decay=0.01
        )
        self.scheduler = optim.lr_scheduler.CosineAnnealingLR(
            self.optimizer, 
            T_max=50
        )
        
        self.best_model_weights = None
        self.best_accuracy = 0.0
        self.history: Dict[str, List[float]] = {
            "train_loss": [],
            "val_loss": [],
            "val_accuracy": []
        }
    
    def train_epoch(self) -> float:
        """Train for one epoch."""
        self.model.train()
        running_loss = 0.0
        
        for images, labels in self.train_loader:
            images = images.to(self.device)
            labels = labels.to(self.device)
            
            self.optimizer.zero_grad()
            outputs = self.model(images)
            loss = self.criterion(outputs, labels)
            loss.backward()
            self.optimizer.step()
            
            running_loss += loss.item()
        
        return running_loss / len(self.train_loader)
    
    @torch.no_grad()
    def validate(self) -> tuple[float, float]:
        """Validate the model."""
        self.model.eval()
        running_loss = 0.0
        correct = 0
        total = 0
        
        for images, labels in self.val_loader:
            images = images.to(self.device)
            labels = labels.to(self.device)
            
            outputs = self.model(images)
            loss = self.criterion(outputs, labels)
            
            running_loss += loss.item()
            _, predicted = outputs.max(1)
            total += labels.size(0)
            correct += predicted.eq(labels).sum().item()
        
        accuracy = correct / total
        avg_loss = running_loss / len(self.val_loader)
        
        return avg_loss, accuracy
    
    def fit(self, epochs: int) -> Dict[str, List[float]]:
        """Full training loop."""
        for epoch in range(epochs):
            train_loss = self.train_epoch()
            val_loss, val_accuracy = self.validate()
            self.scheduler.step()
            
            # Save history
            self.history["train_loss"].append(train_loss)
            self.history["val_loss"].append(val_loss)
            self.history["val_accuracy"].append(val_accuracy)
            
            # Save best model
            if val_accuracy > self.best_accuracy:
                self.best_accuracy = val_accuracy
                self.best_model_weights = copy.deepcopy(
                    self.model.state_dict()
                )
            
            print(
                f"Epoch {epoch+1}/{epochs} - "
                f"Train Loss: {train_loss:.4f} - "
                f"Val Loss: {val_loss:.4f} - "
                f"Val Acc: {val_accuracy:.4f}"
            )
        
        # Load best weights
        self.model.load_state_dict(self.best_model_weights)
        return self.history

Esta implementación utiliza el optimizador AdamW con weight decay para regularización y un scheduler cosine annealing para ajustar dinámicamente el learning rate.

Técnicas Avanzadas para Visión por Computadora

Las arquitecturas modernas incorporan mecanismos de atención que mejoran la capacidad del modelo para enfocarse en regiones relevantes de la imagen.

python
import torch
import torch.nn as nn
import torch.nn.functional as F

class ChannelAttention(nn.Module):
    """Squeeze-and-Excitation channel attention."""
    
    def __init__(self, channels: int, reduction: int = 16):
        super().__init__()
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        self.fc = nn.Sequential(
            nn.Linear(channels, channels // reduction, bias=False),
            nn.ReLU(inplace=True),
            nn.Linear(channels // reduction, channels, bias=False),
            nn.Sigmoid()
        )
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        b, c, _, _ = x.size()
        y = self.avg_pool(x).view(b, c)
        y = self.fc(y).view(b, c, 1, 1)
        return x * y.expand_as(x)


class SpatialAttention(nn.Module):
    """CBAM spatial attention module."""
    
    def __init__(self, kernel_size: int = 7):
        super().__init__()
        self.conv = nn.Conv2d(
            2, 1, 
            kernel_size=kernel_size, 
            padding=kernel_size // 2,
            bias=False
        )
        self.sigmoid = nn.Sigmoid()
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        avg_out = torch.mean(x, dim=1, keepdim=True)
        max_out, _ = torch.max(x, dim=1, keepdim=True)
        attention = torch.cat([avg_out, max_out], dim=1)
        attention = self.sigmoid(self.conv(attention))
        return x * attention

Los mecanismos de atención de canal y espacial permiten que la red aprenda qué características y qué regiones son más importantes para la tarea de clasificación.

¿Listo para aprobar tus entrevistas de Data Science & ML?

Practica con nuestros simuladores interactivos, flashcards y tests técnicos.

Preguntas Frecuentes en Entrevistas de Visión por Computadora

Las entrevistas técnicas en deep learning generalmente cubren conceptos teóricos y prácticos. A continuación se presentan las preguntas más comunes con sus respuestas.

Pregunta: Explique la diferencia entre valid y same padding en las convoluciones.

El padding "valid" no agrega píxeles alrededor de la imagen, lo que reduce las dimensiones de salida. El padding "same" agrega píxeles para mantener las dimensiones de entrada idénticas a las de salida. La elección depende de la arquitectura deseada y la preservación de información espacial en los bordes.

Pregunta: ¿Cómo mejora batch normalization el entrenamiento de las CNN?

Batch normalization normaliza las activaciones en cada mini-batch, reduciendo el covariate shift interno. Esta técnica acelera la convergencia, permite utilizar learning rates más altos y actúa como regularizador. Durante la inferencia, las estadísticas de media y varianza se calculan sobre todo el dataset de entrenamiento.

Pregunta: ¿Cuándo usar transfer learning versus entrenar desde cero?

El transfer learning es preferible con datasets limitados, tareas similares al dataset fuente, o restricciones de recursos computacionales. El entrenamiento desde cero conviene para datasets masivos, dominios muy diferentes de ImageNet (imágenes médicas, satelitales), o cuando las características de bajo nivel difieren significativamente.

Pregunta: Explique el concepto de receptive field en las CNN.

El receptive field representa la región de la imagen de entrada que influye en un neurona particular. Aumenta con la profundidad de la red mediante convoluciones y poolings sucesivos. Un receptive field más amplio permite al modelo capturar dependencias espaciales a mayor escala, esencial para la comprensión contextual.

Pregunta: ¿Cómo manejar el desbalance de clases en clasificación de imágenes?

Existen varias estrategias: ponderación de clases en la función de pérdida, sobremuestreo de clases minoritarias, submuestreo de clases mayoritarias, o generación de datos sintéticos mediante aumentación dirigida. La elección depende de la severidad del desbalance y el tamaño del dataset.

Métricas de Evaluación para Visión por Computadora

La evaluación rigurosa de un modelo requiere métricas adaptadas a la tarea y al contexto empresarial.

python
import torch
import numpy as np
from sklearn.metrics import (
    confusion_matrix, 
    classification_report,
    precision_recall_fscore_support
)
from typing import Dict, List

@torch.no_grad()
def evaluate_model(
    model: torch.nn.Module,
    test_loader: torch.utils.data.DataLoader,
    class_names: List[str],
    device: str = "cuda"
) -> Dict:
    """Comprehensive model evaluation."""
    model.eval()
    all_predictions = []
    all_labels = []
    all_probabilities = []
    
    for images, labels in test_loader:
        images = images.to(device)
        outputs = model(images)
        probabilities = torch.softmax(outputs, dim=1)
        _, predicted = outputs.max(1)
        
        all_predictions.extend(predicted.cpu().numpy())
        all_labels.extend(labels.numpy())
        all_probabilities.extend(probabilities.cpu().numpy())
    
    # Calculate metrics
    precision, recall, f1, support = precision_recall_fscore_support(
        all_labels, 
        all_predictions, 
        average=None
    )
    
    # Per-class metrics
    per_class_metrics = {
        class_names[i]: {
            "precision": float(precision[i]),
            "recall": float(recall[i]),
            "f1_score": float(f1[i]),
            "support": int(support[i])
        }
        for i in range(len(class_names))
    }
    
    # Overall metrics
    accuracy = np.mean(np.array(all_predictions) == np.array(all_labels))
    macro_f1 = np.mean(f1)
    
    return {
        "accuracy": float(accuracy),
        "macro_f1": float(macro_f1),
        "per_class": per_class_metrics,
        "confusion_matrix": confusion_matrix(
            all_labels, 
            all_predictions
        ).tolist()
    }

La accuracy por sí sola puede ser engañosa con clases desbalanceadas. El F1-score macro proporciona una vista más equilibrada del rendimiento en todas las clases.

Conclusión

La visión por computadora con PyTorch ofrece posibilidades considerables para resolver problemas complejos de procesamiento de imágenes. El dominio de las CNN, transfer learning y técnicas de atención constituye una base sólida para los profesionales del deep learning. La preparación para entrevistas técnicas requiere una comprensión profunda de estos conceptos, combinada con experiencia práctica en su implementación. El ecosistema de PyTorch continúa evolucionando con nuevas arquitecturas y optimizaciones, haciendo esencial mantenerse actualizado en este campo en rápida evolución.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Desarrollador fullstack, fundador de SharpSkill

Desarrollador fullstack desde hace más de 10 años. Dirige SharpSkill y responde por todo lo que se publica aquí.

Actualizado el 14 de agosto de 2026

Compartir

Artículos relacionados