🏭 Caso de Uso

Introducción a los Tensores con Python y NumPy

Conceptos matemáticos de tensores y sus operaciones fundamentales (dot product, matmul, contracción, Hadamard, broadcasting) implementadas con Python y NumPy.

🐍 Python 📓 Jupyter Notebook

🧮 Introducción a los Tensores con Python y NumPy

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

Los tensores son la estructura de datos fundamental del deep learning. Son una generalización de escalares, vectores y matrices a dimensiones arbitrarias:

Rango Nombre Ejemplo
0 Escalar Un número: 7
1 Vector Una lista: [1, 2, 3]
2 Matriz Una tabla 2D: shape (3, 4)
3 Tensor 3D Un cubo de datos: shape (3, 4, 5)
N Tensor ND Estructura N-dimensional

En este notebook exploramos los conceptos matemáticos detrás de los tensores y sus operaciones fundamentales, implementadas con Python puro y NumPy.

Notación Matemática

Un tensor de orden $n$ en un espacio $\mathbb{R}^{I_1 \times I_2 \times \dots \times I_n}$ se denota:

$$\mathcal{T} \in \mathbb{R}^{I_1 \times I_2 \times \dots \times I_n}$$

donde $I_k$ es la dimensión en el eje $k$.

1. Producto Escalar (Dot Product)

El producto escalar de dos vectores $\mathbf{a}, \mathbf{b} \in \mathbb{R}^n$ es:

$$\mathbf{a} \cdot \mathbf{b} = \sum_{i=1}^n a_i b_i$$

Ejemplo: Sean $\mathbf{a} = [1, 2, 3, 4]$ y $\mathbf{b} = [5, 6, 7, 8]$:

$$\mathbf{a} \cdot \mathbf{b} = (1)(5) + (2)(6) + (3)(7) + (4)(8) = 5 + 12 + 21 + 32 = 70$$

[ ]
import numpy as np

# ── Producto escalar ──────────────────────────────────────
a = np.array([1, 2, 3, 4])
b = np.array([5, 6, 7, 8])

# Tres formas equivalentes de calcularlo:
dot1 = np.dot(a, b)          # Función np.dot
dot2 = a @ b                  # Operador @
dot3 = np.sum(a * b)          # Definición manual

print(f"a = {a}")
print(f"b = {b}")
print(f"\na · b = {dot1}")
print(f"Verificación (a @ b):       {dot2}")
print(f"Verificación (sum(a * b)):  {dot3}")
El producto escalar de a y b es: 70

2. Multiplicación de Matrices

La multiplicación de $A \in \mathbb{R}^{m \times n}$ y $B \in \mathbb{R}^{n \times p}$ produce $C \in \mathbb{R}^{m \times p}$:

$$C_{ij} = \sum_{k=1}^n A_{ik} B_{kj}$$

Ejemplo: $A_{2\times3} \cdot B_{3\times2}$:

$$A = \begin{pmatrix} 1 & 2 & 3 \ 4 & 5 & 6 \end{pmatrix}, \quad B = \begin{pmatrix} 7 & 8 \ 9 & 10 \ 11 & 12 \end{pmatrix}$$

$$AB = \begin{pmatrix} 1{\cdot}7+2{\cdot}9+3{\cdot}11 & 1{\cdot}8+2{\cdot}10+3{\cdot}12 \ 4{\cdot}7+5{\cdot}9+6{\cdot}11 & 4{\cdot}8+5{\cdot}10+6{\cdot}12 \end{pmatrix} = \begin{pmatrix} 58 & 64 \ 139 & 154 \end{pmatrix}$$

[ ]
# ── Multiplicación de matrices ────────────────────────────
A = np.array([[1, 2, 3],
              [4, 5, 6]])

B = np.array([[7, 8],
              [9, 10],
              [11, 12]])

# Dos formas equivalentes:
AB_dot = np.dot(A, B)    # np.dot
AB_at  = A @ B           # operador @

print(f"A shape: {A.shape}")
print(f"B shape: {B.shape}")
print(f"\nAB = A @ B (shape {AB_at.shape}):")
print(AB_at)
El resultado de la multiplicación AB es:
[[ 58  64]
 [139 154]]

3. Contracción de Tensores (Dimensiones Superiores)

Para tensores de rango > 2, la multiplicación se generaliza mediante la contracción de índices: se suma sobre ejes compartidos.

Dados $\mathcal{A} \in \mathbb{R}^{I \times J \times K}$ y $\mathcal{B} \in \mathbb{R}^{K \times L \times M}$, la contracción sobre $K$ produce:

$$\mathcal{C}{ijlm} = \sum{k=1}^K \mathcal{A}{ijk} \cdot \mathcal{B}{klm}$$

En NumPy usamos np.tensordot(A, B, axes=([eje_A], [eje_B])).

[ ]
# ── Contracción de tensores 3D ───────────────────────────
np.random.seed(42)
A = np.random.randint(1, 5, size=(2, 2, 3))  # shape (2, 2, 3)
B = np.random.randint(1, 5, size=(3, 2, 2))  # shape (3, 2, 2)

# Contraer sobre el eje 2 de A y eje 0 de B (ambos de tamaño 3)
C = np.tensordot(A, B, axes=([2], [0]))

print(f"A shape: {A.shape}")
print(f"B shape: {B.shape}")
print(f"C shape: {C.shape}  (esperado: 2×2×2×2)")
print(f"\nTensor C:")
print(C)
Forma del tensor resultante C: (2, 2, 2, 2)
Tensor resultante C:
[[[[13 12]
   [ 5 10]]

  [[23 27]
   [ 9 17]]]


 [[[30 32]
   [12 22]]

  [[31 30]
   [13 22]]]]

4. Operaciones Elemento a Elemento

4.1 Suma (Hadamard)

$(\mathcal{A} + \mathcal{B}){i_1, \dots, i_n} = \mathcal{A}{i_1, \dots, i_n} + \mathcal{B}_{i_1, \dots, i_n}$

4.2 Producto de Hadamard

$(\mathcal{A} \circ \mathcal{B}){i_1, \dots, i_n} = \mathcal{A}{i_1, \dots, i_n} \cdot \mathcal{B}_{i_1, \dots, i_n}$

Ambas requieren que los tensores tengan la misma forma.

[ ]
# ── Operaciones elemento a elemento ──────────────────────
A = np.array([[1, 2],
              [3, 4]])

B = np.array([[5, 6],
              [7, 8]])

print("A:")
print(A)
print("\nB:")
print(B)

print("\n─── Suma elemento a elemento (A + B) ───")
print(A + B)

print("\n─── Producto de Hadamard (A * B) ───")
print(A * B)
El producto elemento a elemento de A y B es:
[[ 5 12]
 [21 32]]

5. Manipulación de Forma: Reshape, Transpose y Broadcasting

Tres operaciones esenciales para trabajar con tensores en deep learning:

  • Reshape: cambiar la forma sin alterar los datos (ej: imagen 28×28 → vector 784)
  • Transpose: intercambiar ejes (ej: filas ↔ columnas)
  • Broadcasting: operar tensores de formas distintas expandiendo automáticamente los ejes de tamaño 1
[ ]
# ── Reshape ───────────────────────────────────────────────
v = np.arange(12)  # vector de 12 elementos
print(f"Vector original: {v}  (shape {v.shape})")

# Convertir a matriz 3x4
m = v.reshape(3, 4)
print(f"\nReshape (3, 4):")
print(m)

# Convertir a tensor 3D
t = v.reshape(2, 2, 3)
print(f"\nReshape (2, 2, 3):")
print(t)

# ── Transpose ─────────────────────────────────────────────
print(f"\n─── Transpose ───")
A = np.array([[1, 2, 3],
              [4, 5, 6]])  # shape (2, 3)
print(f"A (shape {A.shape}):")
print(A)
print(f"\nA.T (shape {A.T.shape}):")
print(A.T)

# ── Broadcasting ──────────────────────────────────────────
print(f"\n─── Broadcasting ───")
M = np.array([[1, 2, 3],
              [4, 5, 6]])  # shape (2, 3)
v = np.array([10, 20, 30])  # shape (3,)

# NumPy expande v automáticamente a (2, 3)
resultado = M + v
print(f"M (shape {M.shape}):")
print(M)
print(f"\nv (shape {v.shape}): {v}")
print(f"\nM + v (broadcasting):")
print(resultado)

6. Ejemplo Práctico: Forward Pass de una Neurona

Un perceptrón calcula la suma ponderada de las entradas más un bias, y luego aplica una función de activación:

$$y = f(\mathbf{w} \cdot \mathbf{x} + b)$$

Todo esto se reduce a operaciones tensoriales:

[ ]
# ── Forward pass de una neurona ──────────────────────────
np.random.seed(0)

# 3 features de entrada
x = np.array([0.5, 0.3, 0.2])
w = np.random.randn(3)     # pesos
b = np.random.randn()       # bias

# Forward pass
z = np.dot(w, x) + b        # suma ponderada (producto escalar + bias)
y = max(0, z)                # ReLU como activación

print(f"Entradas (x): {x}")
print(f"Pesos (w):    {w.round(3)}")
print(f"Bias (b):     {b:.3f}")
print(f"\nSuma ponderada: z = w·x + b = {z:.4f}")
print(f"Salida (ReLU):  y = max(0, z) = {y:.4f}")

# ── Forward pass de una capa completa (4 neuronas) ───────
print("\n" + "═" * 50)
print("Capa completa: 3 entradas → 4 neuronas")
print("═" * 50)

W = np.random.randn(3, 4)   # Matriz de pesos (3 inputs, 4 neuronas)
b = np.random.randn(4)       # Vector de bias (4 neuronas)

z = x @ W + b               # Multiplicación matricial + broadcasting del bias
y = np.maximum(0, z)         # ReLU elemento a elemento

print(f"\nEntrada x: {x} (shape {x.shape})")
print(f"Pesos W shape: {W.shape}")
print(f"Bias b shape:  {b.shape}")
print(f"\nSalida z = x @ W + b: {z.round(3)} (shape {z.shape})")
print(f"Salida y = ReLU(z):    {y.round(3)}")

7. Resumen

Operación NumPy Notación Matemática
Producto escalar np.dot(a, b) o a @ b $\mathbf{a} \cdot \mathbf{b}$
Multiplicación matricial A @ B o np.matmul(A, B) $C_{ij} = \sum_k A_{ik} B_{kj}$
Contracción tensorial np.tensordot(A, B, axes) $\sum_k \mathcal{A}{\dots k} \mathcal{B}{k \dots}$
Suma elemento a elemento A + B $\mathcal{A} + \mathcal{B}$
Producto de Hadamard A * B $\mathcal{A} \circ \mathcal{B}$
Reshape a.reshape(shape)
Transpose A.T $A^T$
Broadcasting automático

💡 Estas operaciones son la base de todo lo que hacen frameworks como PyTorch y TensorFlow. La diferencia es que ellos añaden autograd (gradientes automáticos) y aceleración GPU.