🏭 Caso de Uso

Tensores con PyTorch

Tutorial completo de tensores en PyTorch: creación, operaciones, autograd, torch.nn, clasificación MNIST y uso de GPU.

🐍 Python 📓 Jupyter Notebook

🔥 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:

  1. Creación de tensores
  2. Operaciones básicas
  3. Auto-diferenciación con autograd
  4. Regresión lineal desde cero
  5. Regresión con torch.nn
  6. Clasificación MNIST con una red neuronal
  7. 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
Output
[ ]
# ── 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.