🏭 Caso de Uso

Tensores con TensorFlow

Tutorial completo de tensores en TensorFlow: creación, operaciones, GradientTape, Keras, clasificación MNIST y uso de GPU.

🐍 Python 📓 Jupyter Notebook

🔶 Tensores con TensorFlow

Submódulo: Tensores en Deep Learning · Fundamentos de Deep Learning

TensorFlow es la biblioteca de deep learning desarrollada por Google. Su nombre viene literalmente de tensor + flow: datos tensoriales fluyendo por un grafo de operaciones.

En este notebook recorremos las operaciones fundamentales con tensores en TensorFlow:

  1. Creación de tensores (escalares, vectores, matrices, tensores ND)
  2. Operaciones básicas (suma, multiplicación, matmul)
  3. Auto-diferenciación con GradientTape
  4. Regresión lineal desde cero
  5. Regresión con la API Keras
  6. Clasificación MNIST con una red neuronal
  7. Uso de GPU

1. Creación de Tensores

En TensorFlow, los tensores son objetos tf.Tensor inmutables. Se crean con tf.constant() o se generan con funciones como tf.zeros(), tf.ones(), tf.random.uniform().

[ ]
import tensorflow as tf
import numpy as np

# ── Escalares, vectores, matrices, tensores ─────────────
escalar = tf.constant(7)
vector  = tf.constant([1.0, 2.0, 3.0])
matriz  = tf.constant([[1, 2], [3, 4]])
tensor  = tf.constant([[[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):12s} | dtype: {t.dtype.name:8s} | rango: {len(t.shape)}")

# ── Tensores especiales ─────────────────────────────────
print(f"\ntf.zeros((2,3)):\n{tf.zeros((2,3)).numpy()}")
print(f"\ntf.ones((2,3)):\n{tf.ones((2,3)).numpy()}")
print(f"\ntf.random.uniform((2,3)):\n{tf.random.uniform((2,3)).numpy().round(3)}")
Escalar: tf.Tensor(7, shape=(), dtype=int32)
Vector: tf.Tensor([1. 2. 3.], shape=(3,), dtype=float32)
Matriz: tf.Tensor(
[[1 2]
 [3 4]], shape=(2, 2), dtype=int32)
Tensor: tf.Tensor(
[[[1 2]
  [3 4]]

 [[5 6]
  [7 8]]], shape=(2, 2, 2), dtype=int32)

2. Operaciones Básicas

TensorFlow soporta las mismas operaciones que NumPy, con funciones como tf.add(), tf.multiply(), tf.matmul(), o directamente con operadores +, *, @.

[ ]
# ── Operaciones con tensores ──────────────────────────────
a = tf.constant([1, 2, 3])
b = tf.constant([4, 5, 6])

print(f"a = {a.numpy()}")
print(f"b = {b.numpy()}")

# Suma elemento a elemento
print(f"\nSuma (a + b):           {tf.add(a, b).numpy()}")

# Multiplicación elemento a elemento
print(f"Producto Hadamard (a*b): {tf.multiply(a, b).numpy()}")

# Producto matricial
M1 = tf.constant([[1, 2], [3, 4]])
M2 = tf.constant([[5, 6], [7, 8]])
print(f"\nProducto matricial M1 @ M2:")
print(tf.matmul(M1, M2).numpy())

# Producto escalar
dot = tf.tensordot(tf.constant([1., 2., 3., 4.]), tf.constant([5., 6., 7., 8.]), axes=1)
print(f"\nProducto escalar: {dot.numpy()}")
Suma: tf.Tensor([5 7 9], shape=(3,), dtype=int32)
Multiplicación: tf.Tensor([ 4 10 18], shape=(3,), dtype=int32)
Producto Matricial: tf.Tensor(
[[19 22]
 [43 50]], shape=(2, 2), dtype=int32)

3. Auto-Diferenciación: GradientTape

TensorFlow calcula gradientes automáticamente con tf.GradientTape. Esto es la base del backpropagation en redes neuronales.

El paradigma es:

  1. Crear una tf.Variable (parámetro a optimizar)
  2. Dentro de with tf.GradientTape() as tape: computar el forward pass
  3. Llamar tape.gradient(loss, variable) para obtener $\frac{\partial \text{loss}}{\partial \text{variable}}$
[ ]
# ── GradientTape ─────────────────────────────────────────
x = tf.Variable(3.0)

# Ejemplo 1: y = x²  →  dy/dx = 2x = 6.0
with tf.GradientTape() as tape:
    y = x ** 2
grad = tape.gradient(y, x)
print(f"y = x² en x=3.0")
print(f"dy/dx = 2x = {grad.numpy()}")

# Ejemplo 2: función más compleja  y = x³ + 2x² - 5x + 3
with tf.GradientTape() as tape:
    y = x**3 + 2*x**2 - 5*x + 3
grad = tape.gradient(y, x)
print(f"\ny = x³ + 2x² - 5x + 3 en x=3.0")
print(f"dy/dx = 3x² + 4x - 5 = {grad.numpy()}  (esperado: 34.0)")
El gradiente de y respecto a x en x=3.0 es: 6.0

4. Regresión Lineal desde Cero

Implementamos el modelo $y = Wx + b$ y lo entrenamos con descenso del gradiente manual, usando GradientTape para calcular los gradientes automáticamente.

[ ]
# ── Regresión lineal con GradientTape ────────────────────
np.random.seed(42)

# Datos: y = 2x + 1 + ruido
X = np.linspace(0, 10, 100).astype(np.float32)
Y = (2 * X + 1 + np.random.randn(100).astype(np.float32) * 0.5)

X = tf.constant(X)
Y = tf.constant(Y)

# Parámetros a aprender
W = tf.Variable(tf.random.normal([1]))
b = tf.Variable(tf.random.normal([1]))

lr = 0.01
for epoch in range(100):
    with tf.GradientTape() as tape:
        Y_pred = W * X + b
        loss = tf.reduce_mean(tf.square(Y - Y_pred))
    
    grads = tape.gradient(loss, [W, b])
    W.assign_sub(lr * grads[0])
    b.assign_sub(lr * grads[1])
    
    if (epoch + 1) % 20 == 0:
        print(f"Epoch {epoch+1:>3}: loss={loss.numpy():.4f}, W={W.numpy()[0]:.3f}, b={b.numpy()[0]:.3f}")

print(f"\n✅ Modelo final: y = {W.numpy()[0]:.3f}·x + {b.numpy()[0]:.3f}  (real: y = 2x + 1)")
Epoch 10: pérdida = 0.2812432050704956, W = [2.0178018], b = [0.9052763]
Epoch 20: pérdida = 0.28030240535736084, W = [2.0163846], b = [0.9148339]
Epoch 30: pérdida = 0.27945074439048767, W = [2.0150177], b = [0.9239248]
Epoch 40: pérdida = 0.278679758310318, W = [2.0137172], b = [0.93257433]
Epoch 50: pérdida = 0.27798184752464294, W = [2.0124798], b = [0.9408039]
Epoch 60: pérdida = 0.2773500382900238, W = [2.0113022], b = [0.948634]
Epoch 70: pérdida = 0.2767781913280487, W = [2.0101821], b = [0.956084]
Epoch 80: pérdida = 0.27626028656959534, W = [2.0091164], b = [0.96317244]
Epoch 90: pérdida = 0.27579158544540405, W = [2.0081022], b = [0.9699167]
Epoch 100: pérdida = 0.27536728978157043, W = [2.0071375], b = [0.9763336]

Modelo entrenado: Y = 2.0071375370025635 * X + 0.9763336181640625

5. Lo Mismo con la API Keras

tf.keras abstrae todo el bucle anterior en unas pocas líneas. Es la forma estándar de usar TensorFlow en proyectos reales.

[ ]
# ── Regresión con Keras ───────────────────────────────────
X_np = np.linspace(0, 10, 100)
Y_np = 2 * X_np + 1 + np.random.randn(100) * 0.5

model = tf.keras.Sequential([
    tf.keras.layers.Dense(1, input_shape=(1,))
])
model.compile(optimizer='sgd', loss='mse')

# Entrenar (verbose=0 para no llenar la salida)
model.fit(X_np, Y_np, epochs=100, verbose=0)

W_k, b_k = model.layers[0].get_weights()
print(f"Modelo Keras: y = {W_k[0][0]:.3f}·x + {b_k[0]:.3f}  (real: y = 2x + 1)")
C:\Users\juano\AppData\Roaming\Python\Python310\site-packages\keras\src\layers\core\dense.py:87: UserWarning: Do not pass an `input_shape`/`input_dim` argument to a layer. When using Sequential models, prefer using an `Input(shape)` object as the first layer in the model instead.
  super().__init__(activity_regularizer=activity_regularizer, **kwargs)
Modelo entrenado: Y = 2.034008264541626 * X + 0.8488515019416809

6. Red Neuronal para Clasificación (MNIST)

Un ejemplo completo de clasificación de dígitos manuscritos con un MLP de una capa oculta (128 neuronas, ReLU).

[ ]
import matplotlib.pyplot as plt

# ── Cargar MNIST ──────────────────────────────────────────
(X_train, y_train), (X_test, y_test) = tf.keras.datasets.mnist.load_data()
X_train, X_test = X_train / 255.0, X_test / 255.0  # Normalizar [0, 1]

print(f"Train: {X_train.shape} | Test: {X_test.shape}")

# Visualizar ejemplos
fig, axes = plt.subplots(2, 5, figsize=(12, 5))
for i, ax in enumerate(axes.flat):
    ax.imshow(X_train[i], cmap='gray')
    ax.set_title(f"Label: {y_train[i]}")
    ax.axis('off')
plt.suptitle('Ejemplos de MNIST', fontsize=14)
plt.tight_layout()
plt.show()
Tamaño del conjunto de entrenamiento: (60000, 28, 28)
Tamaño del conjunto de prueba: (10000, 28, 28)
Output
[ ]
# ── Modelo MLP ────────────────────────────────────────────
model = tf.keras.Sequential([
    tf.keras.layers.Flatten(input_shape=(28, 28)),
    tf.keras.layers.Dense(128, activation='relu'),
    tf.keras.layers.Dense(10, activation='softmax')
])

model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

# Entrenar
model.fit(X_train, y_train, epochs=5, batch_size=64)

# Evaluar
loss, acc = model.evaluate(X_test, y_test, verbose=0)
print(f"\n✅ Test accuracy: {acc*100:.1f}%")
Epoch 1/5
C:\Users\juano\AppData\Roaming\Python\Python310\site-packages\keras\src\layers\reshaping\flatten.py:37: UserWarning: Do not pass an `input_shape`/`input_dim` argument to a layer. When using Sequential models, prefer using an `Input(shape)` object as the first layer in the model instead.
  super().__init__(**kwargs)
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 2s 879us/step - accuracy: 0.8800 - loss: 0.4273
Epoch 2/5
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 2s 849us/step - accuracy: 0.9629 - loss: 0.1223
Epoch 3/5
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 2s 844us/step - accuracy: 0.9769 - loss: 0.0765
Epoch 4/5
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 2s 852us/step - accuracy: 0.9836 - loss: 0.0526
Epoch 5/5
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 2s 859us/step - accuracy: 0.9877 - loss: 0.0417
313/313 ━━━━━━━━━━━━━━━━━━━━ 0s 595us/step - accuracy: 0.9722 - loss: 0.0889

Pérdida en test: 0.07669886201620102
Precisión en test: 0.9761999845504761

7. Uso de GPU

TensorFlow detecta automáticamente las GPUs disponibles y mueve los tensores al dispositivo óptimo. Con tf.device('/GPU:0') se fuerza la ejecución en GPU.

[ ]
# ── Verificar GPU ────────────────────────────────────────
gpus = tf.config.list_physical_devices('GPU')
print(f"GPUs disponibles: {len(gpus)}")
for gpu in gpus:
    print(f"  {gpu.name}")

# Operación en GPU (si disponible)
device = '/GPU:0' if gpus else '/CPU:0'
with tf.device(device):
    A = tf.random.uniform([1000, 1000])
    B = tf.random.uniform([1000, 1000])
    C = tf.matmul(A, B)

print(f"\nMultiplicación 1000×1000 ejecutada en {device}")
print(f"Resultado shape: {C.shape}")

8. Resumen

Concepto TensorFlow Equivalente NumPy
Crear tensor tf.constant() np.array()
Suma tf.add(a, b) o a + b a + b
Matmul tf.matmul(A, B) o A @ B A @ B
Gradientes tf.GradientTape ❌ (manual)
GPU tf.device('/GPU:0')
Modelos tf.keras.Sequential

💡 TensorFlow añade sobre NumPy dos capacidades clave: auto-diferenciación (GradientTape) y aceleración GPU — imprescindibles para entrenar redes neuronales de forma eficiente.