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.
🧮 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.