Tensores con PyTorch
Tutorial completo de tensores en PyTorch: creación, operaciones, autograd, torch.nn, clasificación MNIST y uso de GPU.
🔥 Tensores con PyTorch
Submódulo: Tensores en Deep Learning · Fundamentos de Deep Learning
PyTorch es el framework de deep learning desarrollado por Meta (Facebook AI Research). Se distingue por su paradigma de grafos dinámicos (define-by-run), que permite construir y modificar modelos de forma intuitiva, como si fuera código Python normal.
En este notebook cubrimos:
- Creación de tensores
- Operaciones básicas
- Auto-diferenciación con
autograd - Regresión lineal desde cero
- Regresión con
torch.nn - Clasificación MNIST con una red neuronal
- Uso de GPU
1. Creación de Tensores
Los tensores en PyTorch (torch.Tensor) son muy similares a los ndarray de NumPy, pero con dos superpoderes adicionales: autograd y ejecución en GPU.
import torch
import numpy as np
# ── Escalares, vectores, matrices, tensores ─────────────
escalar = torch.tensor(7)
vector = torch.tensor([1.0, 2.0, 3.0])
matriz = torch.tensor([[1, 2], [3, 4]])
tensor = torch.tensor([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
for name, t in [('Escalar', escalar), ('Vector', vector), ('Matriz', matriz), ('Tensor 3D', tensor)]:
print(f"{name:10s} | shape: {str(t.shape):15s} | dtype: {str(t.dtype):14s} | rango: {t.dim()}")
# ── Tensores especiales ─────────────────────────────────
print(f"\ntorch.zeros(2, 3):\n{torch.zeros(2, 3)}")
print(f"\ntorch.ones(2, 3):\n{torch.ones(2, 3)}")
print(f"\ntorch.randn(2, 3) (distribución normal):\n{torch.randn(2, 3)}")
# ── Conversión NumPy ↔ PyTorch ──────────────────────────
np_array = np.array([1, 2, 3])
torch_tensor = torch.from_numpy(np_array)
back_to_np = torch_tensor.numpy()
print(f"\nNumPy → Torch: {np_array} → {torch_tensor}")
print(f"Torch → NumPy: {torch_tensor} → {back_to_np}")
Escalar: tensor(7)
Vector: tensor([1., 2., 3.])
Matriz: tensor([[1, 2],
[3, 4]])
Tensor: tensor([[[1, 2],
[3, 4]],
[[5, 6],
[7, 8]]])
2. Operaciones Básicas
PyTorch soporta todas las operaciones tensoriales estándar, tanto con funciones (torch.add, torch.matmul) como con operadores (+, *, @).
# ── Operaciones con tensores ──────────────────────────────
a = torch.tensor([1, 2, 3])
b = torch.tensor([4, 5, 6])
print(f"a = {a}")
print(f"b = {b}")
# Suma
print(f"\nSuma (a + b): {torch.add(a, b)}")
# Producto Hadamard (elemento a elemento)
print(f"Producto Hadamard (a*b): {torch.mul(a, b)}")
# Producto escalar
a_f = torch.tensor([1., 2., 3., 4.])
b_f = torch.tensor([5., 6., 7., 8.])
print(f"\nProducto escalar: {torch.dot(a_f, b_f).item()}")
# Producto matricial
M1 = torch.tensor([[1, 2], [3, 4]])
M2 = torch.tensor([[5, 6], [7, 8]])
print(f"\nProducto matricial M1 @ M2:")
print(torch.matmul(M1, M2))
Suma: tensor([5, 7, 9])
Multiplicación: tensor([ 4, 10, 18])
Producto Matricial: tensor([[19, 22],
[43, 50]])
3. Auto-Diferenciación: autograd
PyTorch rastrea todas las operaciones sobre tensores con requires_grad=True, construyendo un grafo computacional dinámico. Al llamar .backward() sobre el resultado, calcula automáticamente todos los gradientes.
$$y = x^2 \quad \Rightarrow \quad \frac{dy}{dx} = 2x$$
# ── Autograd ──────────────────────────────────────────────
x = torch.tensor(3.0, requires_grad=True)
# Ejemplo 1: y = x²
y = x ** 2
y.backward()
print(f"y = x² en x=3.0")
print(f"dy/dx = 2x = {x.grad.item()}")
# Ejemplo 2: función más compleja
x = torch.tensor(3.0, requires_grad=True)
y = x**3 + 2*x**2 - 5*x + 3
y.backward()
print(f"\ny = x³ + 2x² - 5x + 3 en x=3.0")
print(f"dy/dx = 3x² + 4x - 5 = {x.grad.item()} (esperado: 34.0)")
El gradiente de y respecto a x en x=3.0 es: tensor(6.)
4. Regresión Lineal desde Cero
Entrenamos $y = Wx + b$ usando autograd para calcular gradientes y actualizamos los parámetros manualmente.
# ── Regresión lineal con autograd ────────────────────────
np.random.seed(42)
# Datos: y = 2x + 1 + ruido
X = torch.tensor(np.linspace(0, 10, 100), dtype=torch.float32).view(-1, 1)
Y = torch.tensor(2 * np.linspace(0, 10, 100) + 1 + np.random.randn(100) * 0.5,
dtype=torch.float32).view(-1, 1)
# Parámetros
W = torch.randn(1, requires_grad=True)
b = torch.randn(1, requires_grad=True)
lr = 0.01
for epoch in range(100):
Y_pred = W * X + b
loss = torch.mean((Y - Y_pred) ** 2)
loss.backward()
with torch.no_grad():
W -= lr * W.grad
b -= lr * b.grad
W.grad.zero_()
b.grad.zero_()
if (epoch + 1) % 20 == 0:
print(f"Epoch {epoch+1:>3}: loss={loss.item():.4f}, W={W.item():.3f}, b={b.item():.3f}")
print(f"\n✅ Modelo final: y = {W.item():.3f}·x + {b.item():.3f} (real: y = 2x + 1)")
Epoch 10: pérdida = 0.6366785168647766, W = 2.201432466506958, b = -0.21856509149074554 Epoch 20: pérdida = 0.6005821824073792, W = 2.192554235458374, b = -0.1593771129846573 Epoch 30: pérdida = 0.5679053664207458, W = 2.1840872764587402, b = -0.10306552052497864 Epoch 40: pérdida = 0.5383244752883911, W = 2.1760313510894775, b = -0.04948756471276283 Epoch 50: pérdida = 0.5115457773208618, W = 2.1683664321899414, b = 0.0014894385822117329 Epoch 60: pérdida = 0.4873040020465851, W = 2.1610734462738037, b = 0.04999168589711189 Epoch 70: pérdida = 0.4653586447238922, W = 2.154134750366211, b = 0.09613936394453049 Epoch 80: pérdida = 0.44549232721328735, W = 2.1475329399108887, b = 0.14004677534103394 Epoch 90: pérdida = 0.42750808596611023, W = 2.1412513256073, b = 0.18182267248630524 Epoch 100: pérdida = 0.4112274944782257, W = 2.135274887084961, b = 0.22157053649425507 Modelo entrenado: Y = 2.135274887084961 * X + 0.22157053649425507
5. Lo Mismo con torch.nn
El módulo torch.nn proporciona capas predefinidas, funciones de pérdida y optimizadores. Es la forma estándar de definir modelos en PyTorch.
import torch.nn as nn
# ── Regresión con torch.nn ────────────────────────────────
X = torch.tensor(np.linspace(0, 10, 100), dtype=torch.float32).view(-1, 1)
Y = torch.tensor(2 * np.linspace(0, 10, 100) + 1 + np.random.randn(100) * 0.5,
dtype=torch.float32).view(-1, 1)
model = nn.Sequential(nn.Linear(1, 1))
criterion = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
for epoch in range(100):
Y_pred = model(X)
loss = criterion(Y_pred, Y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
W_nn, b_nn = list(model.parameters())
print(f"Modelo nn: y = {W_nn.item():.3f}·x + {b_nn.item():.3f} (real: y = 2x + 1)")
Modelo entrenado: [Parameter containing: tensor([[2.1044]], requires_grad=True), Parameter containing: tensor([0.3352], requires_grad=True)]
6. Red Neuronal para Clasificación (MNIST)
Un ejemplo completo de clasificación de dígitos manuscritos con un MLP. PyTorch requiere definir el modelo como una clase que hereda de nn.Module.
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
# ── Cargar MNIST ──────────────────────────────────────────
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,)) # [0,1] → [-1,1]
])
trainset = torchvision.datasets.MNIST(root='./data', train=True, download=True, transform=transform)
testset = torchvision.datasets.MNIST(root='./data', train=False, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)
testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=False)
print(f"Train: {len(trainset)} | Test: {len(testset)}")
# Visualizar ejemplos
images, labels = next(iter(trainloader))
fig, axes = plt.subplots(2, 5, figsize=(12, 5))
for i, ax in enumerate(axes.flat):
ax.imshow(images[i].squeeze(), cmap='gray')
ax.set_title(f"Label: {labels[i].item()}")
ax.axis('off')
plt.suptitle('Ejemplos de MNIST', fontsize=14)
plt.tight_layout()
plt.show()
Tamaño del conjunto de entrenamiento: 60000 Tamaño del conjunto de prueba: 10000
# ── Definir el modelo ────────────────────────────────────
class MNISTNet(nn.Module):
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()
self.fc1 = nn.Linear(28 * 28, 128)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = self.flatten(x)
x = torch.relu(self.fc1(x))
return self.fc2(x)
model = MNISTNet()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# ── Entrenar ──────────────────────────────────────────────
for epoch in range(5):
total_loss = 0
for images, labels in trainloader:
outputs = model(images)
loss = criterion(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
print(f"Epoch {epoch+1}/5 — Loss: {total_loss/len(trainloader):.4f}")
# ── Evaluar ───────────────────────────────────────────────
correct, total = 0, 0
with torch.no_grad():
for images, labels in testloader:
outputs = model(images)
_, predicted = torch.max(outputs, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f"\n✅ Test accuracy: {100*correct/total:.1f}%")
Precisión en test: 97.02%
7. Uso de GPU
En PyTorch, mover tensores y modelos a GPU es explícito con .to(device) o .cuda(). Esto da control total sobre dónde se ejecuta cada operación.
# ── Verificar GPU ────────────────────────────────────────
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Dispositivo: {device}")
if torch.cuda.is_available():
print(f"GPU: {torch.cuda.get_device_name(0)}")
print(f"Memoria: {torch.cuda.get_device_properties(0).total_mem/1024**3:.1f} GB")
# Operación en GPU (o CPU si no hay GPU)
A = torch.rand(1000, 1000, device=device)
B = torch.rand(1000, 1000, device=device)
C = torch.matmul(A, B)
print(f"\nMultiplicación 1000×1000 en {device}")
print(f"Resultado shape: {C.shape}")
8. Resumen
| Concepto | PyTorch | Equivalente NumPy |
|---|---|---|
| Crear tensor | torch.tensor() |
np.array() |
| Suma | torch.add(a, b) o a + b |
a + b |
| Matmul | torch.matmul(A, B) o A @ B |
A @ B |
| Gradientes | autograd + .backward() |
❌ (manual) |
| GPU | .to('cuda') |
❌ |
| Modelos | nn.Module + nn.Sequential |
❌ |
¿TensorFlow o PyTorch? Ambos son excelentes. PyTorch destaca por su simplicidad y flexibilidad (grafos dinámicos), mientras que TensorFlow tiene un ecosistema más amplio para producción (TF Serving, TF Lite, TF.js).
💡 En investigación, PyTorch es el framework más utilizado. En producción/industria, TensorFlow sigue siendo muy popular.