- PyTorch destaca por su grafo dinámico, API “pythónica” y gran soporte en GPU.
- Instalación sencilla con pip/conda; CPU para empezar y CUDA/ROCm para acelerar.
- Tensores, nn.Module y DataLoader son la base para MLPs y CNNs escalables.
- PyTorch 2 aporta torch.compile y mejor gestión del device para más rendimiento.
Si te preguntas qué pinta tiene hoy el desarrollo de redes neuronales en Python, la respuesta corta es: PyTorch manda. Su popularidad no viene de la nada; combina una sintaxis muy cercana a Python con la potencia de las GPU, lo que permite prototipar y depurar con rapidez, y escalar a producción cuando toca.
Además, PyTorch se distingue por su uso de gráficos de cómputo dinámicos, una idea clave para entender por qué es tan flexible frente a alternativas de grafo estático. A lo largo de este artículo verás qué es PyTorch, cómo instalarlo correctamente, cómo funcionan sus tensores y cómo levantar tus primeras redes (densas y convolucionales), con prácticas de entrenamiento en CPU/GPU, trucos de PyTorch 2 y ejemplos reales de regresión y clasificación.
¿Qué es PyTorch y qué lo hace diferente?
PyTorch es un framework de código abierto para deep learning, creado por Meta AI y mantenido por una comunidad enorme, que ofrece cálculo con tensores acelerado por GPU (similar a NumPy, pero con CUDA/ROCm), un sistema de autograd para derivadas automáticas y una API de alto nivel con módulos listos para crear redes neuronales. Gracias a TorchScript y a su compatibilidad con ONNX, está preparado para investigación y despliegue en producción, incluyendo ejecución gráfica, cuantización, entrenamiento distribuido y móvil.
Su gran baza es la experiencia de desarrollo: con una API muy “pythónica” y un grafo que se construye en tiempo de ejecución, resulta muy fácil de depurar con herramientas estándar de Python (pdb/ipdb). Esto lo convierte en la opción predilecta para investigadores y equipos que necesitan iterar rápido, y también para perfiles de data science que quieren pasar de idea a prototipo en muy pocas líneas.
Gráficos de cómputo dinámicos frente a estáticos
Un grafo de cómputo modela el flujo de datos entre operaciones matemáticas; en redes neuronales, describe cómo se transforman los tensores capa a capa. En PyTorch, ese grafo se construye al vuelo durante la ejecución; es decir, no tienes que “compilar” toda la arquitectura antes de correrla, lo que te permite cambiar el modelo sobre la marcha y ejecutar fragmentos aislados del pipeline sin rehacerlo todo.
Este enfoque dinámico facilita la depuración y el prototipado, especialmente en problemas complejos (visión, NLP) donde quieres iterar rápido en trozos concretos del modelo. Además, el grafo inverso para la retropropagación se genera automáticamente en cada pasada, adaptándose a la lógica ejecutada; con grafos estáticos, en cambio, hay que fijar la arquitectura por adelantado, lo cual puede limitar la flexibilidad (por ejemplo, modelos con número variable de capas o entradas de tamaños irregulares).
Alternativas a PyTorch
Existen varias opciones populares en el ecosistema: TensorFlow (Google) con su enfoque por grafos y soporte industrial muy maduro; Caffe, orientado a visión por computador y famoso por sus modelos preentrenados; Microsoft CNTK, con buenos resultados en reconocimiento del habla; Theano, pionera para definir y optimizar expresiones matemáticas; y Keras, una API de alto nivel que hoy suele usarse encima de TensorFlow para prototipar de forma rápida y amigable.
¿Por qué tantos eligen PyTorch? Porque la API es más directa, se parece a escribir Python puro, y el grafo dinámico simplifica muchísimo la vida a la hora de depurar. A esto súmale el empuje de la comunidad y la adopción en papers y repos como Hugging Face, donde la mayoría de modelos open source ya se entrenan y publican con PyTorch.
Instalación: CPU vs GPU y opciones prácticas
Instalar PyTorch es sencillo con pip o conda. Si vas a usar GPU NVIDIA, elige la build compatible con tu versión de CUDA; puedes comprobarla con las herramientas del sistema y luego consultar la guía de instalación de CUDA en Windows. Si no tienes GPU o quieres empezar sin complicarte, instala la versión para CPU y listo; en Google Colab viene preinstalado con aceleración disponible.
En equipos domésticos verás con frecuencia CUDA 11.7 u 11.8; selecciona el instalador que coincida. Aunque PyTorch también soporta GPUs de AMD (ROCm), hoy sigue siendo menos habitual; si vas a tirar de portátil o no te quieres liar, arranca en CPU y migra a GPU cuando el modelo lo pida.
# Instalación básica con pip (CPU)
pip install torch torchvision torchaudio
# Ejemplo (orientativo) para CUDA 11.x con pip
# Consulta el selector oficial para el comando exacto según versión de CUDA
Para validar que todo funciona, en el intérprete de Python prueba a importar los módulos; si no aparece error, lo tienes listo. También puedes verificar si hay GPU con torch.cuda.is_available() para asegurarte de que PyTorch ve la aceleradora.
Trabajar con tensores
Todo en PyTorch gira en torno a los tensores, estructuras similares a arrays de NumPy que pueden vivir en CPU o en GPU. Puedes crearlos a mano, convertirlos desde NumPy o generarlos de forma aleatoria con dimensiones concretas. La API te ofrece métodos para consultar tamaño, reindexar y aplicar operaciones matemáticas element-wise o reducciones como la media.
import torch
# Desde listas
x = torch.tensor(, ])
# Aleatorios en )
sub = y # acceso por cortes
Cuando necesitas reordenar dimensiones, PyTorch ofrece view y reshape. La primera devuelve una vista que comparte memoria con el tensor original y requiere que los datos sean contiguos; la segunda intenta devolver una vista, pero puede crear una copia si hace falta. Esta diferencia es útil para evitar costes de copia inesperados.
t = torch.arange(12)
A = t.view(3, 4) # vista si es contiguous
B = t.reshape(2, 6) # puede crear copia si no hay layout compatible
Fundamentos de redes neuronales en PyTorch
Las redes en PyTorch se montan con módulos del paquete torch.nn. Subclasificando nn.Module defines tu arquitectura, declaras capas en __init__ y describes el paso hacia delante en forward. Con bloques como nn.Linear (densas), nn.Conv2d (convolución), funciones de activación como nn.ReLU y utilidades como nn.Flatten o pooling, puedes ensamblar desde MLPs a CNNs y más.
Una red “fully-connected” típica aplica transformaciones lineales con activaciones entre medias: la ecuación y = W x + b sigue siendo el corazón, mientras que activaciones como ReLU, Sigmoid o LogSoftmax aportan la no linealidad necesaria para representar funciones complejas.
import torch.nn as nn
class MLP(nn.Module):
def __init__(self, in_features, hidden=16, out_features=3):
super().__init__()
self.fc1 = nn.Linear(in_features, hidden)
self.act = nn.ReLU()
self.fc2 = nn.Linear(hidden, out_features)
def forward(self, x):
x = self.act(self.fc1(x))
return self.fc2(x)
Primeros pasos: datos tabulares y clasificación (Iris)
Para problemas tabulares, cargar datos es directo: con scikit-learn divides en train/test, conviertes a tensores y listo. Si el dataset cabe en memoria puedes entrenar tal cual; si no, conviene crear TensorDataset y DataLoader para iterar en batches y barajar.
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from torch.utils.data import TensorDataset, DataLoader
import torch
X, y = load_iris(return_X_y=True)
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.long)
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.2, stratify=y)
train_ds = TensorDataset(Xtr, ytr)
train_dl = DataLoader(train_ds, batch_size=32, shuffle=True)
El ciclo de entrenamiento en PyTorch sigue un patrón claro: pones gradientes a cero, haces forward, calculas la pérdida, llamas a backward y actualizas con el optimizador. Para clasificación multi-clase, nn.CrossEntropyLoss es el estándar; como optimizadores, Adam o SGD suelen ser suficientes para empezar.
import torch.nn.functional as F
import torch.optim as optim
model = MLP(in_features=X.shape)
criterion = nn.CrossEntropyLoss()
opt = optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(500):
for xb, yb in train_dl:
opt.zero_grad()
logits = model(xb)
loss = criterion(logits, yb)
loss.backward()
opt.step()
Para evaluar, cambia el modelo a eval() y desactiva gradientes con torch.no_grad(); si vas a reutilizar el modelo, guarda los pesos con torch.save(model.state_dict(), ruta) y recárgalos con load_state_dict sobre una instancia idéntica. Recuerda que torch.save guarda parámetros, no la clase de la red.
Imágenes a gran escala: CNN y carga en lotes
Con imágenes no es viable meter todo en memoria. Aquí entran Dataset y DataLoader para cargar por lotes, aplicar transformaciones y aumentar datos. Si tus imágenes están organizadas por carpetas por clase, torchvision.datasets.ImageFolder simplifica la creación del dataset con rutas tipo raiz/clase/imagen.png.
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
transform = transforms.Compose()
train_set = datasets.ImageFolder('data/train', transform=transform)
train_loader = DataLoader(train_set, batch_size=64, shuffle=True)
Para una CNN básica en CIFAR-10, combinas nn.Conv2d, nn.MaxPool2d, activaciones y capas densas al final. El entrenamiento se parece al del MLP, solo que iteras sobre los batches del DataLoader. En este tipo de tareas, entrenar en GPU marca la diferencia en tiempo.
class SmallCNN(nn.Module):
def __init__(self, n_classes=10):
super().__init__()
self.conv1 = nn.Conv2d(3, 16, 3, padding=1)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
self.flat = nn.Flatten()
self.fc = nn.Linear(32*8*8, n_classes)
def forward(self, x):
x = F.relu(self.conv1(x))
x = self.pool(x)
x = F.relu(self.conv2(x))
x = self.pool(x)
x = self.flat(x)
return self.fc(x)
Entrenar en GPU y gestión del dispositivo
La forma básica de enviar tu modelo y datos a GPU es detectar si está disponible y usar to(‘cuda’) en el modelo y en cada batch. Esto asegura que los tensores y las operaciones residan en el mismo device, lo cual es imprescindible para evitar errores y aprovechar la aceleración.
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = SmallCNN().to(device)
for imgs, labels in train_loader:
imgs = imgs.to(device)
labels = labels.to(device)
# ... forward, loss, backward, step ...
Con PyTorch 2, además, puedes fijar un dispositivo por defecto para no tener que llamar a .to() en cada parte. Hay una API global y también un context manager para crear capas directamente en el device deseado, reduciendo boilerplate y errores sutiles.
import torch
# Global
torch.set_default_device('cuda')
# Contextual
with torch.device('cpu'):
tmp = torch.randn(2, 2) # se crea en CPU por contexto
Novedades de PyTorch 2: velocidad y ergonomía
La función torch.compile() es la estrella: envuelve tu modelo para compilarlo y aplicar optimizaciones como fusión de operadores o planificación mejorada. En validaciones internas, ha aportado mejoras de tiempo de ejecución notables en multitud de arquitecturas (con especial brillo en transformers). Cuando funciona, las aceleraciones pueden ser muy sustanciales, con tasas de éxito muy altas sobre modelos comunes.
model = SmallCNN().to(device)
compiled_model = torch.compile(model)
# Entrenas e infieres con compiled_model en lugar de model
Ten en cuenta una salvedad práctica: en algunos entornos Windows, esta función puede no estar soportada todavía debido a dependencias con Triton. En Linux con GPU datacenter (p. ej., A100) es donde más brilla; en GPUs domésticas verás mejoras menores, aunque sigue siendo una palanca interesante para exprimir el hardware.
Regresión vs clasificación: bases y librerías de apoyo
La diferencia es sencilla: en clasificación predices categorías; en regresión, valores continuos. PyTorch cubre ambos con pérdidas y capas adecuadas, y se integra de lujo con librerías que te facilitan la vida: scikit-learn para splits y métricas, NumPy para manipulación numérica, el hub Datasets de Hugging Face para descargar conjuntos, y herramientas de tracking como neptune.ai para gestionar experimentos.
En modelos densos, combinarás capas lineales con activaciones; en CNN, trabajarás con convoluciones y pooling; en secuencias o NLP entrarás en terreno de RNNs/transformers. La clave es dominar los bloques básicos de nn y ajustar la función de pérdida y el optimizador según la tarea.
Ejemplo de regresión: diabetes
Para regresión, una configuración típica incluye MSELoss y un optimizador como AdamW, con métricas de evaluación tipo MSE y R². Un pipeline sencillo: DataLoader para batches, red con varias densas activadas con ReLU y una salida de dimensión 1.
from sklearn.datasets import load_diabetes
from sklearn.model_selection import train_test_split
from torch.utils.data import TensorDataset, DataLoader
import torch.nn as nn
X, y = load_diabetes(return_X_y=True)
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y.reshape(-1, 1), dtype=torch.float32)
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.2)
train_dl = DataLoader(TensorDataset(Xtr, ytr), batch_size=32, shuffle=True)
torch.set_default_device('cpu') # por ejemplo
class NeuralRegression(nn.Module):
def __init__(self, in_features):
super().__init__()
self.seq = nn.Sequential(
nn.Linear(in_features, 20), nn.ReLU(),
nn.Linear(20, 10), nn.ReLU(),
nn.Linear(10, 5), nn.ReLU(),
nn.Linear(5, 1)
)
def forward(self, x):
return self.seq(x)
model = NeuralRegression(X.shape)
criterion = nn.MSELoss()
opt = torch.optim.AdamW(model.parameters(), lr=1e-3)
for epoch in range(5000):
for xb, yb in train_dl:
opt.zero_grad()
pred = model(xb)
loss = criterion(pred, yb)
loss.backward()
opt.step()
Tras el entrenamiento, evalúa en test con torch.no_grad() y calcula MSE/R² con scikit-learn. Verás que, en este dataset, el modelo capta bien la tendencia: primeras predicciones suelen aproximarse a los valores reales con errores moderados y un R² razonable.
Ejemplo de clasificación: dígitos (8×8)
Para clasificación con imágenes pequeñas (como el dataset de dígitos 8×8 de scikit-learn), puedes normalizar con StandardScaler, re-formar a 1×8×8 y mezclar Conv2d + MaxPool2d con densas, añadiendo Dropout para combatir el overfitting y LogSoftmax en la salida si vas a usar NLLLoss.
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from torch.utils.data import TensorDataset, DataLoader
X, y = load_digits(return_X_y=True)
X = StandardScaler().fit_transform(X)
X = torch.tensor(X, dtype=torch.float32).view(-1, 1, 8, 8)
y = torch.tensor(y, dtype=torch.long)
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.2, stratify=y)
train_dl = DataLoader(TensorDataset(Xtr, ytr), batch_size=32, shuffle=True)
test_dl = DataLoader(TensorDataset(Xte, yte), batch_size=32)
class DigitCNN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 10, kernel_size=2)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(10, 20, kernel_size=2)
self.drop = nn.Dropout(0.25)
self.flat = nn.Flatten()
self.fc1 = nn.Linear(20, 50)
self.fc2 = nn.Linear(50, 10)
self.logsoft = nn.LogSoftmax(dim=1)
def forward(self, x):
x = F.relu(self.conv1(x))
x = self.pool(x)
x = F.relu(self.conv2(x))
x = self.pool(x)
x = self.drop(x)
x = self.flat(x)
x = F.relu(self.fc1(x))
return self.logsoft(self.fc2(x))
model = DigitCNN()
criterion = nn.NLLLoss()
opt = torch.optim.AdamW(model.parameters(), lr=1e-3)
for epoch in range(10):
for xb, yb in train_dl:
opt.zero_grad()
out = model(xb)
loss = criterion(out, yb)
loss.backward()
opt.step()
En evaluación, reporta accuracy, precision, recall y F1. Es común que ciertas clases (como el 8) confundan más al modelo; se puede mitigar con más datos de esa clase o con preprocesado/aumento específico que potencie sus rasgos distintivos.
Entrenamiento con validación, guardado y pruebas por clase
En proyectos reales conviene separar entrenamiento, validación y prueba; guardarás el mejor modelo según accuracy (u otra métrica) en validación y luego lo cargarás para evaluar en test. Es útil implementar funciones de test por clase para ver dónde acierta y falla el sistema.
# Patrón de entrenamiento con validación (esqueleto)
best_acc = 0.0
for epoch in range(10):
model.train()
# ... loop de entrenamiento ...
model.eval()
with torch.no_grad():
# ... loop de validación ...
# si accuracy > best_acc: guardar pesos
Una vez termines, puedes exportar a ONNX para integrar con runtimes compatibles, manteniendo interoperabilidad con otros entornos. Este paso encaja bien tras cerrar la fase de validación final.
Técnicas para mejorar modelos
Cuida siempre los datos: la normalización (media 0, desviación 1) ayuda a estabilizar el entrenamiento; Batch Normalization, inserta normalización entre capas y acelera la convergencia. Para evitar sobreajuste, aplica Dropout y regularización L2 (weight decay) en el optimizador.
Si quieres atajar, usa transfer learning: empieza desde modelos preentrenados (por ejemplo, ResNet o VGG en visión, o backbones modernos) y ajusta las últimas capas a tu tarea. PyTorch y torchvision proporcionan pesos listos y APIs para reemplazar la cabeza de clasificación en muy pocas líneas.
Por último, cuando el modelo o el dataset crezcan, aprovecha Data Parallelism para repartir el trabajo en múltiples GPUs o núcleos; PyTorch cuenta con utilidades nativas para paralelizar y entrenar a escala con una configuración razonable.
Mirando todo el conjunto, PyTorch combina una curva de aprendizaje amable con una potencia notable: desde tensores y grafos dinámicos hasta CNNs y compilación con torch.compile, pasando por entrenamiento en GPU y utilidades de producción como ONNX. Con buenas prácticas de datos, validación sólida y uso de las novedades de PyTorch 2, es fácil pasar de prototipos rápidos a soluciones que rinden de verdad en el mundo real.