Números como Puntos en una Línea... y Más Allá

Todos los números que has usado hasta ahora — enteros, fracciones, decimales, incluso números irracionales como $\pi$ — viven en una sola línea: la recta numérica real . Puedes deslizarte a la izquierda (negativo) o a la derecha (positivo), pero siempre estás atrapado en una dimensión. Para la mayor parte de la aritmética, eso es suficiente. Pero cuando necesitamos describir rotaciones — y la rotación resulta ser central en cómo los transformers modernos codifican la posición — una dimensión no es suficiente. Necesitamos un sistema numérico que viva en un plano.

La idea clave es engañosamente simple: definir un nuevo número $i$ cuyo cuadrado sea $-1$.

$$i^2 = -1$$

Ningún número real tiene esta propiedad (cualquier número real al cuadrado es no negativo), así que $i$ se llama la unidad imaginaria . Un número complejo es entonces cualquier expresión de la forma:

$$z = a + bi$$

donde $a$ y $b$ son números reales. Llamamos a $a$ la parte real y a $b$ la parte imaginaria . Cuando $b = 0$ recuperamos los números reales ordinarios. Cuando $a = 0$ obtenemos un número puramente imaginario como $3i$ o $-2i$.

Aquí está la ganancia geométrica: como cada número complejo tiene dos componentes ($a$ y $b$), podemos graficarlo como un punto en un plano 2D. El eje horizontal es la parte real, el eje vertical es la parte imaginaria. Este es el plano complejo (también llamado diagrama de Argand). El número $z = 3 + 2i$ se ubica en el punto $(3, 2)$. El número $z = -1 - i$ se ubica en $(-1, -1)$. Los números reales viven sobre el eje horizontal, y los números puramente imaginarios viven sobre el eje vertical.

El gráfico a continuación muestra varios números complejos en el plano. Observa cómo cada uno se identifica de manera única por su posición — un número complejo es un punto 2D.

import math, json, js

# Complex numbers to plot: (real, imag, label)
points = [
    (3, 2, "3 + 2i"),
    (-1, -1, "-1 - i"),
    (0, 1, "i"),
    (-2, 0, "-2"),
    (1, -1.5, "1 - 1.5i"),
    (0, 0, "0"),
]

# Build scatter-style plot by placing each point as a separate line with a single data point
# We'll plot the real axis and imaginary axis as reference lines, then overlay points
x_axis = [i * 0.5 for i in range(-8, 9)]  # -4 to 4
y_zero = [0 for _ in x_axis]

lines = [
    {"label": "Eje real", "data": y_zero, "color": "#d1d5db"}
]

# Plot each point as a small cluster around its location for visibility
for real, imag, label in points:
    # Create a tiny visible dot by using a single-element line at the right x position
    # We'll use a trick: create x_data with just the point's x, data with just the point's y
    pass

# Better approach: use a single series per point, with matching x_data
# Since the plot system uses shared x_data, we'll mark points by creating spike data
# that is null everywhere except at the point's x-coordinate

# Actually, let's plot the points as separate charts won't work well.
# Instead, build a scatter-like visualisation using lines that converge to points.

# Simplest approach: plot thin vertical lines from x-axis to each point
all_x = []
all_lines_data = {}

for real, imag, label in points:
    line_x = [real, real]
    line_y = [0, imag]

# Let's use a different strategy: create a combined x_data that includes all point x-coords
# and for each point, only show a value at its x-coordinate

# Best approach for this plot system: draw the unit circle and overlay key points
theta_vals = [i * 0.02 for i in range(0, 315)]  # 0 to 2pi
circle_x = [math.cos(t) for t in theta_vals]
circle_y = [math.sin(t) for t in theta_vals]

# Create a grid of x values that includes our point locations
x_vals = [i * 0.1 for i in range(-40, 41)]  # -4.0 to 4.0

# For each labelled point, create a "spike" series
spike_lines = []
colors = ["#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6", "#6b7280"]
for idx, (real, imag, label) in enumerate(points):
    # Create data that shows the point's y-value at the nearest x-grid position
    data = []
    for x in x_vals:
        if abs(x - real) < 0.05:
            data.append(imag)
        else:
            data.append(None)
    spike_lines.append({"label": label, "data": data, "color": colors[idx % len(colors)]})

plot_data = [
    {
        "title": "El Plano Complejo",
        "x_label": "Parte real",
        "y_label": "Parte imaginaria",
        "x_data": x_vals,
        "lines": spike_lines
    }
]
js.window.py_plot_data = json.dumps(plot_data)

print("Cada número complejo z = a + bi es un punto (a, b) en el plano.")
print()
for real, imag, label in points:
    print(f"  {label:>10}  =>  punto ({real}, {imag})")
💡 ¿Por qué inventar un número cuyo cuadrado es negativo? Porque desbloquea una hermosa conexión entre el álgebra y la geometría. Sumar números complejos es suma de vectores. Multiplicarlos — como estamos a punto de ver — es rotación y escalado. Este significado geométrico de la multiplicación es la razón por la que los números complejos aparecen en el aprendizaje profundo.

Multiplicar por i Rota 90°

Ahora viene la intuición geométrica clave que hace poderosos a los números complejos. Comienza con el número $z = 1$, que se ubica en el punto $(1, 0)$ sobre el eje real positivo. ¿Qué sucede cuando multiplicamos por $i$?

$$1 \cdot i = i$$

El resultado es $i$, que se ubica en $(0, 1)$. Nos movimos del eje real positivo al eje imaginario positivo — una rotación de 90° en sentido antihorario. Multiplica por $i$ de nuevo:

$$i \cdot i = i^2 = -1$$

Ahora estamos en $(-1, 0)$. Otra rotación de 90°. De nuevo:

$$-1 \cdot i = -i$$

Estamos en $(0, -1)$. Y una vez más:

$$-i \cdot i = -i^2 = -(-1) = 1$$

De vuelta donde empezamos. Cuatro multiplicaciones por $i$, cuatro rotaciones de 90°, un círculo completo de 360°. Esto no es una coincidencia ni un truco — es el significado geométrico de la multiplicación compleja. Multiplicar cualquier número complejo por $i$ lo rota 90° en sentido antihorario alrededor del origen. Las potencias de $i$ trazan los cuatro puntos cardinales del círculo unitario:

  • $i^0 = 1$ — ángulo 0° — el punto $(1, 0)$
  • $i^1 = i$ — ángulo 90° — el punto $(0, 1)$
  • $i^2 = -1$ — ángulo 180° — el punto $(-1, 0)$
  • $i^3 = -i$ — ángulo 270° — el punto $(0, -1)$
  • $i^4 = 1$ — ángulo 360° — de vuelta a $(1, 0)$

El gráfico a continuación muestra estos cuatro puntos en el círculo unitario, con flechas indicando la dirección de rotación. Cada multiplicación por $i$ avanza el ángulo 90°.

import math, json, js

# Unit circle
theta_vals = [i * 0.02 for i in range(0, 316)]
circle_x = [math.cos(t) for t in theta_vals]
circle_y = [math.sin(t) for t in theta_vals]

# The four powers of i
powers = [
    (1, 0, "i⁰ = 1"),
    (0, 1, "i¹ = i"),
    (-1, 0, "i² = -1"),
    (0, -1, "i³ = -i"),
]

# Use shared x_data as the circle x-values, plot circle y as one line
# Then mark the four points

# Plot the unit circle and the four key points
# We'll use the circle as one continuous line
# For the four points, create spike markers

# Build x_data from circle_x (already 316 points around the circle)
# For each power-of-i point, find the nearest index in circle_x and mark it

x_data = circle_x
lines = [
    {"label": "Círculo unitario", "data": circle_y, "color": "#d1d5db"}
]

colors = ["#3b82f6", "#10b981", "#ef4444", "#f59e0b"]
for idx, (px, py, label) in enumerate(powers):
    marker_data = []
    for j in range(len(circle_x)):
        dist = math.sqrt((circle_x[j] - px)**2 + (circle_y[j] - py)**2)
        if dist < 0.05:
            marker_data.append(py)
        else:
            marker_data.append(None)
    lines.append({"label": label, "data": marker_data, "color": colors[idx]})

plot_data = [
    {
        "title": "Potencias de i en el Círculo Unitario: Cada Multiplicación Rota 90°",
        "x_label": "Parte real",
        "y_label": "Parte imaginaria",
        "x_data": x_data,
        "lines": lines
    }
]
js.window.py_plot_data = json.dumps(plot_data)

print("Multiplicar por i rota 90° en sentido antihorario:")
print("  1  ->  i  ->  -1  ->  -i  ->  1")
print("  0°    90°    180°    270°    360°")

Esto plantea una pregunta natural: $i$ nos da rotaciones de 90°, pero ¿qué pasa si queremos rotar un ángulo arbitrario — digamos 30° o 45° o $\theta$ radianes? Para eso, necesitamos la fórmula de Euler.

La Fórmula de Euler: El Puente Entre Exponenciales y Rotaciones

La fórmula de Euler es una de las ecuaciones más bellas de todas las matemáticas. Conecta la función exponencial — que gobierna el crecimiento y la decadencia — con la trigonometría — que gobierna los círculos y las rotaciones:

$$e^{i\theta} = \cos\theta + i\sin\theta$$

Desempaquemos lo que esto dice geométricamente. El lado derecho, $\cos\theta + i\sin\theta$, es un número complejo con parte real $\cos\theta$ y parte imaginaria $\sin\theta$. ¿Dónde se ubica este punto en el plano complejo? La componente real es la coordenada $x$ en el círculo unitario en el ángulo $\theta$, y la componente imaginaria es la coordenada $y$. Entonces $e^{i\theta}$ es el punto en el círculo unitario en el ángulo $\theta$ desde el eje real positivo.

Esto es extraordinario: elevar $e$ a una potencia imaginaria no produce crecimiento exponencial — produce rotación . El exponente $i\theta$ te indica el ángulo. La base $e$ es simplemente la constante matemática $2.718...$, pero con un exponente imaginario traza un círculo en lugar de dispararse al infinito.

Verifiquemos esto con las rotaciones que ya conocemos de las potencias de $i$:

  • $e^{i \cdot 0} = \cos(0) + i\sin(0) = 1 + 0 = 1$ — sin rotación. Coincide con $i^0 = 1$. Correcto.
  • $e^{i\pi/2} = \cos(\pi/2) + i\sin(\pi/2) = 0 + i \cdot 1 = i$ — rotación de 90°. Coincide con $i^1 = i$. Correcto.
  • $e^{i\pi} = \cos(\pi) + i\sin(\pi) = -1 + 0 = -1$ — rotación de 180°. Esta es la identidad de Euler , frecuentemente escrita como $e^{i\pi} + 1 = 0$, conectando cinco constantes fundamentales ($e$, $i$, $\pi$, $1$, $0$) en una sola ecuación.
  • $e^{i \cdot 2\pi} = \cos(2\pi) + i\sin(2\pi) = 1 + 0 = 1$ — rotación completa de 360°, de vuelta al inicio.

A medida que $\theta$ barre de $0$ a $2\pi$, el punto $e^{i\theta}$ traza todo el círculo unitario. El gráfico a continuación muestra esto: las partes real e imaginaria oscilan como coseno y seno respectivamente, y juntas trazan un círculo perfecto.

import math, json, js

# Trace e^{i*theta} as theta goes from 0 to 2*pi
n_points = 200
theta_vals = [2 * math.pi * i / n_points for i in range(n_points + 1)]

real_parts = [math.cos(t) for t in theta_vals]  # cos(theta)
imag_parts = [math.sin(t) for t in theta_vals]  # sin(theta)

# Mark key angles: 0, pi/2, pi, 3pi/2, 2pi
key_angles = [0, math.pi/2, math.pi, 3*math.pi/2, 2*math.pi]
key_labels = ["0", "\u03c0/2", "\u03c0", "3\u03c0/2", "2\u03c0"]
key_real = [math.cos(a) for a in key_angles]
key_imag = [math.sin(a) for a in key_angles]

# Plot 1: The unit circle traced by e^{i*theta}
# Use real_parts as x_data and imag_parts as y_data
plot_data = [
    {
        "title": "e^(i\u03b8) traza el c\u00edrculo unitario mientras \u03b8 va de 0 a 2\u03c0",
        "x_label": "Real: cos(\u03b8)",
        "y_label": "Imaginario: sin(\u03b8)",
        "x_data": real_parts,
        "lines": [
            {"label": "e^(i\u03b8)", "data": imag_parts, "color": "#3b82f6"}
        ]
    },
    {
        "title": "Partes real e imaginaria de e^(i\u03b8) vs \u03b8",
        "x_label": "\u03b8 (radianes)",
        "y_label": "Valor",
        "x_data": [round(t, 4) for t in theta_vals],
        "lines": [
            {"label": "cos(\u03b8) [parte real]", "data": real_parts, "color": "#3b82f6"},
            {"label": "sin(\u03b8) [parte imag.]", "data": imag_parts, "color": "#ef4444"}
        ]
    }
]
js.window.py_plot_data = json.dumps(plot_data)

print("Valores clave de e^(i\u03b8):")
for angle, label, r, im in zip(key_angles, key_labels, key_real, key_imag):
    sign = "+" if im >= 0 else "-"
    print(f"  \u03b8 = {label:>5}  =>  e^(i\u03b8) = {r:+.3f} {sign} {abs(im):.3f}i")
📌 ¿De dónde viene la fórmula de Euler? Una derivación elegante usa series de Taylor. La exponencial $e^x = 1 + x + \frac{x^2}{2!} + \frac{x^3}{3!} + \cdots$ y cuando sustituyes $x = i\theta$, las potencias de $i$ ciclan ($i^2 = -1$, $i^3 = -i$, $i^4 = 1$, ...) y ordenan los términos reales e imaginarios exactamente en las series de Taylor de $\cos\theta$ y $\sin\theta$ respectivamente. No es una definición ni un truco — se deduce inevitablemente de cómo se definen $e^x$, $\cos x$ y $\sin x$.

La Rotación como Multiplicación Compleja

Ahora podemos responder la pregunta que motiva todo este artículo: ¿cómo se rota un vector 2D por un ángulo arbitrario? Toma cualquier punto 2D $(x_1, x_2)$ y represéntalo como un número complejo $z = x_1 + ix_2$. Para rotar este punto un ángulo $\theta$ en sentido antihorario alrededor del origen, multiplica por $e^{i\theta}$:

$$z' = z \cdot e^{i\theta} = (x_1 + ix_2)(\cos\theta + i\sin\theta)$$

Expandiendo este producto usando la propiedad distributiva (y recordando que $i^2 = -1$):

$$z' = (x_1\cos\theta - x_2\sin\theta) + i(x_1\sin\theta + x_2\cos\theta)$$

Leyendo las partes real e imaginaria del resultado:

  • Nueva parte real: $x_1' = x_1\cos\theta - x_2\sin\theta$
  • Nueva parte imaginaria: $x_2' = x_1\sin\theta + x_2\cos\theta$

Ahora escribe esto en forma matricial:

$$\begin{pmatrix} x_1' \\ x_2' \end{pmatrix} = \begin{pmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{pmatrix} \begin{pmatrix} x_1 \\ x_2 \end{pmatrix}$$

Esta es la matriz de rotación 2D estándar. Acabamos de demostrar que la multiplicación compleja por $e^{i\theta}$ es exactamente equivalente a la rotación matricial. Dos operaciones que lucen completamente diferentes — multiplicar números complejos y multiplicar matrices — producen la misma transformación geométrica. Esta equivalencia es el fundamento matemático de RoPE (Rotary Position Embedding) , que codifica las posiciones de los tokens en transformers rotando los vectores de query y key.

Verifiquemos esto numéricamente. El código a continuación toma un vector 2D y lo rota por varios ángulos usando multiplicación compleja, luego confirma que la matriz de rotación da el mismo resultado.

import math, json, js

# Original vector
x1, x2 = 3.0, 1.0

angles_deg = [0, 30, 45, 90, 180, 270]
rows = []

for deg in angles_deg:
    theta = math.radians(deg)
    cos_t = math.cos(theta)
    sin_t = math.sin(theta)

    # Method 1: Complex multiplication
    # z = x1 + i*x2,  rotator = cos(theta) + i*sin(theta)
    # z' = z * rotator = (x1*cos - x2*sin) + i*(x1*sin + x2*cos)
    new_real = x1 * cos_t - x2 * sin_t
    new_imag = x1 * sin_t + x2 * cos_t

    # Method 2: Rotation matrix (same formula, just to confirm)
    mat_x1 = cos_t * x1 + (-sin_t) * x2
    mat_x2 = sin_t * x1 + cos_t * x2

    match = "Sí" if abs(new_real - mat_x1) < 1e-10 and abs(new_imag - mat_x2) < 1e-10 else "No"

    rows.append([
        f"{deg}\u00b0",
        f"({new_real:.3f}, {new_imag:.3f})",
        f"({mat_x1:.3f}, {mat_x2:.3f})",
        match
    ])

js.window.py_table_data = json.dumps({
    "headers": ["Ángulo", "Mult. compleja", "Mult. matricial", "¿Coinciden?"],
    "rows": rows
})

print(f"Vector original: ({x1}, {x2})")
print(f"A 90\u00b0: ({x1}, {x2}) -> ({-x2}, {x1}) [intercambiado y negado, como se esperaba]")
print(f"A 180\u00b0: ({x1}, {x2}) -> ({-x1}, {-x2}) [ambos negados]")

La tabla confirma lo que el álgebra prometía: la multiplicación compleja y la rotación matricial son la misma operación, para todos los ángulos. La conexión con el aprendizaje profundo es directa: RoPE toma un vector de query o key, agrupa sus dimensiones en pares consecutivos $(x_1, x_2)$, trata cada par como un número complejo, y multiplica por $e^{im\theta}$ donde $m$ es la posición del token en la secuencia. Como el producto punto de dos vectores rotados depende solo de la diferencia en sus ángulos de rotación (es decir, la diferencia de posiciones), la puntuación de atención captura naturalmente la posición relativa — exactamente el sesgo inductivo que queremos.

💡 ¿Por qué el producto punto solo depende de la posición relativa? Si el vector $\mathbf{q}$ se rota por un ángulo $m\theta$ y el vector $\mathbf{k}$ se rota por un ángulo $n\theta$, su producto punto es igual al producto punto de los vectores sin rotar rotados por $(m - n)\theta$. Esto es porque la rotación preserva las longitudes y el ángulo entre dos vectores cambia solo por la diferencia de sus rotaciones individuales. Así, la posición $m$ atendiendo a la posición $n$ da la misma geometría que la posición $m+5$ atendiendo a la posición $n+5$ — solo importa la diferencia $m - n$.

Por Qué Esto Importa para el Aprendizaje Profundo

Los números complejos y la fórmula de Euler no son solo matemáticas abstractas — aparecen a lo largo del aprendizaje profundo moderno, a veces explícitamente y a veces entre bastidores.

RoPE (Rotary Position Embedding). Esta es la aplicación más directa y la razón por la que este artículo existe en la pista de fundamentos matemáticos. Dado un vector de query o key de dimensión $d$, RoPE lo divide en $d/2$ pares consecutivos. Cada par $(x_1, x_2)$ se trata como un número complejo $x_1 + ix_2$, luego se multiplica por $e^{im\theta_k}$ donde $m$ es la posición del token y $\theta_k$ es una frecuencia que difiere para cada par (típicamente $\theta_k = 10000^{-2k/d}$). El resultado: cada par se rota por un ángulo dependiente de la posición. Cuando calculamos el producto punto entre un query en la posición $m$ y un key en la posición $n$, los ángulos de rotación se cancelan parcialmente, dejando un producto punto que depende de la distancia relativa $m - n$ en lugar de las posiciones absolutas. Esta elegante codificación de la posición relativa a través de la rotación es por qué los números complejos importan para los transformers. Construimos la imagen completa en el artículo de codificaciones posicionales .

La Transformada de Fourier. La transformada discreta de Fourier descompone una señal en sus frecuencias constituyentes usando la fórmula $X_k = \sum_{n=0}^{N-1} x_n \cdot e^{-i2\pi kn/N}$. Cada término $e^{-i2\pi kn/N}$ es una rotación en el plano complejo, y la suma mide cuánto "resuena" la señal en cada frecuencia. Las transformadas de Fourier sustentan el procesamiento de audio basado en espectrogramas, las componentes de frecuencia usadas en algunas codificaciones posicionales (como las codificaciones sinusoidales en el Transformer original), y la convolución eficiente mediante el algoritmo FFT.

Procesamiento de señales y modelos de difusión. Aunque los modelos de difusión usan principalmente ruido gaussiano de valores reales, los esquemas de ruido subyacentes y la teoría que conecta diferentes niveles de ruido se basan en la decadencia exponencial ($e^{-\lambda t}$), que es el primo del eje real de la fórmula de Euler. Más ampliamente, cualquier técnica que involucre funciones periódicas, cambios de fase o descomposición en frecuencias se construye sobre $e^{i\theta} = \cos\theta + i\sin\theta$.

La conclusión central: cada vez que veas rotaciones, frecuencias o patrones periódicos en el aprendizaje profundo, los números complejos y la fórmula de Euler son el lenguaje matemático detrás de ellos. La cadena de ideas que hemos construido — $i$ rota 90°, $e^{i\theta}$ rota por un $\theta$ arbitrario, la multiplicación compleja es igual a la matriz de rotación — es el fundamento que hace funcionar a RoPE y a los métodos basados en Fourier.

Quiz

Pon a prueba tu comprensión de los números complejos, la fórmula de Euler y su conexión con las rotaciones.

¿Cuál es el efecto geométrico de multiplicar un número complejo por $i$?

¿A qué es igual $e^{i\pi}$?

Para rotar un vector 2D $(x_1, x_2)$ por un ángulo $\theta$, lo representamos como un número complejo $z = x_1 + ix_2$ y multiplicamos por:

¿Por qué RoPE usa multiplicación compleja (rotación) para codificar la posición?