Phase 01 - Lesson 19

Números Complejos para IA

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

La raíz cuadrada de -1 no es imaginaria. Es la clave para las rotaciones, las frecuencias y la mitad del procesamiento de señales.

Tipo: Aprender Lenguaje: Python Requisitos previos: Fase 1, Lecciones 01-04 (álgebra lineal, cálculo) Tiempo: ~60 minutos

Objetivos de Aprendizaje

  • Realizar aritmética compleja (sumar, multiplicar, dividir, conjugar) tanto en forma rectangular como polar
  • Aplicar la fórmula de Euler para convertir entre exponenciales complejas y funciones trigonométricas
  • Implementar la Transformada Discreta de Fourier usando las raíces complejas de la unidad
  • Explicar cómo las rotaciones complejas están detrás de RoPE y de las codificaciones posicionales sinusoidales en los transformers

El Problema

Abres un artículo sobre transformadas de Fourier y hay i por todas partes. Miras las codificaciones posicionales de los transformers y ves sin y cos a distintas frecuencias -- las partes real e imaginaria de exponenciales complejas. Lees sobre computación cuántica y encuentras todo expresado en espacios vectoriales complejos.

Los números complejos parecen abstractos. Un sistema numérico construido sobre la raíz cuadrada de -1 parece un truco matemático. Pero no es un truco. Es el lenguaje natural de las rotaciones y las oscilaciones. Cada vez que algo gira, vibra u oscila, los números complejos son la herramienta correcta.

Sin entender los números complejos, no puedes entender la Transformada Discreta de Fourier. No puedes entender la FFT. No puedes entender cómo funciona RoPE (Rotary Position Embedding) en los modelos de lenguaje modernos. No puedes entender por qué las codificaciones posicionales sinusoidales del artículo original del Transformer usan las frecuencias que usan.

Esta lección construye la aritmética compleja desde cero, la conecta con la geometría y te muestra exactamente dónde aparecen los números complejos en machine learning.

El Concepto

¿Qué es un número complejo?

Un número complejo tiene dos partes: una parte real y una parte imaginaria.

z = a + bi

where:
  a is the real part
  b is the imaginary part
  i is the imaginary unit, defined by i^2 = -1

Eso es todo. Extiendes la recta numérica a un plano. Los números reales se ubican en un eje. Los números imaginarios se ubican en el otro. Todo número complejo es un punto en ese plano.

Aritmética compleja

Suma. Suma las partes reales entre sí, suma las partes imaginarias entre sí.

(a + bi) + (c + di) = (a + c) + (b + d)i

Example: (3 + 2i) + (1 + 4i) = 4 + 6i

Multiplicación. Usa la propiedad distributiva y recuerda que i^2 = -1.

(a + bi)(c + di) = ac + adi + bci + bdi^2
                 = ac + adi + bci - bd
                 = (ac - bd) + (ad + bc)i

Example: (3 + 2i)(1 + 4i) = 3 + 12i + 2i + 8i^2
                            = 3 + 14i - 8
                            = -5 + 14i

Conjugado. Invierte el signo de la parte imaginaria.

conjugate of (a + bi) = a - bi

El producto de un número complejo y su conjugado siempre es real:

(a + bi)(a - bi) = a^2 + b^2

División. Multiplica el numerador y el denominador por el conjugado del denominador.

(a + bi) / (c + di) = (a + bi)(c - di) / (c^2 + d^2)

Esto elimina la parte imaginaria del denominador, dándote un número complejo limpio.

El plano complejo

El plano complejo mapea todo número complejo a un punto 2D. El eje horizontal es el eje real, el eje vertical es el eje imaginario.

z = 3 + 2i  corresponds to the point (3, 2)
z = -1 + 0i corresponds to the point (-1, 0) on the real axis
z = 0 + 4i  corresponds to the point (0, 4) on the imaginary axis

Un número complejo es simultáneamente un punto y un vector desde el origen. Esta interpretación doble es lo que hace útiles a los números complejos para la geometría.

Forma polar

Cualquier punto del plano puede describirse por su distancia al origen y su ángulo respecto al eje real positivo.

z = r * (cos(theta) + i*sin(theta))

where:
  r = |z| = sqrt(a^2 + b^2)     (magnitude, or modulus)
  theta = atan2(b, a)             (phase, or argument)

La forma rectangular (a + bi) es buena para la suma. La forma polar (r, theta) es buena para la multiplicación.

Multiplicación en forma polar. Multiplica las magnitudes, suma los ángulos.

z1 = r1 * e^(i*theta1)
z2 = r2 * e^(i*theta2)

z1 * z2 = (r1 * r2) * e^(i*(theta1 + theta2))

Por eso los números complejos son perfectos para las rotaciones. Multiplicar por un número complejo de magnitud 1 es una rotación pura.

Fórmula de Euler

El puente entre las exponenciales complejas y la trigonometría:

e^(i*theta) = cos(theta) + i*sin(theta)

Esta es la fórmula más importante de esta lección. Cuando theta = pi:

e^(i*pi) = cos(pi) + i*sin(pi) = -1 + 0i = -1

Therefore: e^(i*pi) + 1 = 0

Cinco constantes fundamentales (e, i, pi, 1, 0) ligadas en una sola ecuación.

Por qué la fórmula de Euler importa para ML

La fórmula de Euler dice que e^(i*theta) traza el círculo unitario a medida que theta varía. En theta = 0, estás en (1, 0). En theta = pi/2, estás en (0, 1). En theta = pi, estás en (-1, 0). En theta = 3pi/2, estás en (0, -1). Una rotación completa es theta = 2pi.

Esto significa que las exponenciales complejas SON rotaciones. Y las rotaciones están en todas partes en el procesamiento de señales y en ML.

Conexión con las rotaciones 2D

Multiplicar el número complejo (x + yi) por e^(i*theta) rota el punto (x, y) un ángulo theta alrededor del origen.

Rotation via complex multiplication:
  (x + yi) * (cos(theta) + i*sin(theta))
  = (x*cos(theta) - y*sin(theta)) + (x*sin(theta) + y*cos(theta))i

Rotation via matrix multiplication:
  [cos(theta)  -sin(theta)] [x]   [x*cos(theta) - y*sin(theta)]
  [sin(theta)   cos(theta)] [y] = [x*sin(theta) + y*cos(theta)]

Producen resultados idénticos. La multiplicación compleja ES la rotación 2D. La matriz de rotación es solo la multiplicación compleja escrita en notación matricial.

graph TD
    subgraph "Complex Multiplication = 2D Rotation"
        A["z = x + yi<br/>Point (x, y)"] -->|"multiply by e^(i*theta)"| B["z' = z * e^(i*theta)<br/>Point rotated by theta"]
    end
    subgraph "Equivalent Matrix Form"
        C["vector [x, y]"] -->|"multiply by rotation matrix"| D["[x cos theta - y sin theta,<br/> x sin theta + y cos theta]"]
    end
    B -.->|"same result"| D

Fasores y señales rotativas

Una exponencial compleja e^(iomegat) es un punto que gira alrededor del círculo unitario con frecuencia angular omega. A medida que t aumenta, el punto traza el círculo.

La parte real de ese punto rotativo es cos(omegat). La parte imaginaria es sin(omegat). Una señal sinusoidal es la sombra de un número complejo en rotación.

e^(i*omega*t) = cos(omega*t) + i*sin(omega*t)

Real part:      cos(omega*t)    -- a cosine wave
Imaginary part: sin(omega*t)    -- a sine wave

Esta es la representación por fasores. En lugar de seguir una onda sinusoidal ondulante, sigues una flecha que gira suavemente. Los desplazamientos de fase se vuelven desplazamientos de ángulo. Los cambios de amplitud se vuelven cambios de magnitud. La suma de señales se vuelve suma vectorial.

Raíces de la unidad

Las N raíces N-ésimas de la unidad son N puntos igualmente espaciados en el círculo unitario:

w_k = e^(2*pi*i*k/N)    for k = 0, 1, 2, ..., N-1

Para N = 4, las raíces son: 1, i, -1, -i (los cuatro puntos cardinales). Para N = 8, obtienes los cuatro puntos cardinales más las cuatro diagonales.

Las raíces de la unidad son la base de la Transformada Discreta de Fourier. La DFT descompone una señal en componentes a esas N frecuencias igualmente espaciadas.

Conexión con la DFT

La Transformada Discreta de Fourier de una señal x[0], x[1], ..., x[N-1] es:

X[k] = sum_{n=0}^{N-1} x[n] * e^(-2*pi*i*k*n/N)

Cada X[k] mide cuánto se correlaciona la señal con la k-ésima raíz de la unidad -- una sinusoide compleja a la frecuencia k. La DFT descompone una señal en N fasores rotativos e informa la amplitud y la fase de cada uno.

Por qué i no es imaginario

La palabra "imaginario" es un accidente histórico. Descartes la usó de forma despectiva. Pero i no es más imaginario de lo que eran los números negativos cuando la gente los rechazó por primera vez. Los números negativos responden "¿de cuánto restas 5 a 3 para obtener?". La unidad imaginaria responde "¿qué elevas al cuadrado para obtener -1?".

De forma más útil: i es un operador de rotación de 90 grados. Multiplica un número real por i una vez y rotas 90 grados hacia el eje imaginario. Multiplica por i de nuevo (i^2) y rotas otros 90 grados -- ahora apuntas en la dirección real negativa. Por eso i^2 = -1. No es misterioso. Es una media vuelta construida a partir de dos cuartos de vuelta.

Por eso los números complejos están en todas partes en la ingeniería. Cualquier cosa que gire -- ondas electromagnéticas, estados cuánticos, oscilaciones de señales, codificaciones posicionales -- se describe naturalmente con números complejos.

Exponenciales complejas vs funciones trigonométricas

Antes de la fórmula de Euler, los ingenieros escribían las señales como Acos(omegat + phi) -- amplitud A, frecuencia omega, fase phi. Esto funciona, pero hace dolorosa la aritmética. Sumar dos cosenos con distintas fases exige identidades trigonométricas.

Con exponenciales complejas, la misma señal es Ae^(i(omega*t + phi)). Sumar dos señales es solo sumar dos números complejos. Multiplicar (modular) es solo multiplicar magnitudes y sumar ángulos. Los desplazamientos de fase se vuelven sumas de ángulo. Los desplazamientos de frecuencia se vuelven multiplicaciones por fasores.

Todo el campo del procesamiento de señales migró a la notación de exponenciales complejas porque las matemáticas son más limpias. La "señal real" siempre es solo la parte real de la representación compleja. La parte imaginaria se lleva como contabilidad, haciendo que toda el álgebra funcione de forma natural.

Conexión con los transformers

Codificaciones posicionales sinusoidales (artículo original del Transformer):

PE(pos, 2i) = sin(pos / 10000^(2i/d))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d))

Los pares sin y cos son las partes real e imaginaria de exponenciales complejas a distintas frecuencias. Cada frecuencia proporciona una "resolución" diferente para codificar la posición. Las frecuencias bajas cambian lentamente (posición gruesa). Las frecuencias altas cambian rápidamente (posición fina). Juntas, le dan a cada posición una huella de frecuencia única.

RoPE (Rotary Position Embedding) lleva esto más lejos. Multiplica explícitamente los vectores de query y key por matrices de rotación compleja. La posición relativa entre dos tokens se vuelve un ángulo de rotación. La atención se calcula usando esos vectores rotados, haciendo al modelo sensible a la posición relativa mediante la multiplicación compleja.

Operación Forma Algebraica Significado Geométrico
Suma (a+c) + (b+d)i Suma vectorial en el plano
Multiplicación (ac-bd) + (ad+bc)i Rotar y escalar
Conjugado a - bi Reflejar sobre el eje real
Magnitud sqrt(a^2 + b^2) Distancia al origen
Fase atan2(b, a) Ángulo desde el eje real positivo
División multiplicar por el conjugado Revertir la rotación y reescalar
Potencia r^n * e^(intheta) Rotar n veces, escalar por r^n
graph LR
    subgraph "Unit Circle"
        direction TB
        U1["e^(i*0) = 1"] -.-> U2["e^(i*pi/2) = i"]
        U2 -.-> U3["e^(i*pi) = -1"]
        U3 -.-> U4["e^(i*3pi/2) = -i"]
        U4 -.-> U1
    end
    subgraph "Applications"
        A1["Euler's formula:<br/>e^(i*theta) = cos + i*sin"]
        A2["DFT uses roots of unity:<br/>e^(2*pi*i*k/N)"]
        A3["RoPE uses rotation:<br/>q * e^(i*m*theta)"]
    end
    U1 --> A1
    U1 --> A2
    U1 --> A3

Constrúyelo

Paso 1: Clase Complex

Construye una clase de número complejo que soporte aritmética, magnitud, fase y conversión entre las formas rectangular y polar.

import math

class Complex:
    def __init__(self, real, imag=0.0):
        self.real = real
        self.imag = imag

    def __add__(self, other):
        return Complex(self.real + other.real, self.imag + other.imag)

    def __mul__(self, other):
        r = self.real * other.real - self.imag * other.imag
        i = self.real * other.imag + self.imag * other.real
        return Complex(r, i)

    def __truediv__(self, other):
        denom = other.real ** 2 + other.imag ** 2
        r = (self.real * other.real + self.imag * other.imag) / denom
        i = (self.imag * other.real - self.real * other.imag) / denom
        return Complex(r, i)

    def magnitude(self):
        return math.sqrt(self.real ** 2 + self.imag ** 2)

    def phase(self):
        return math.atan2(self.imag, self.real)

    def conjugate(self):
        return Complex(self.real, -self.imag)

Paso 2: Conversión polar y fórmula de Euler

def to_polar(z):
    return z.magnitude(), z.phase()

def from_polar(r, theta):
    return Complex(r * math.cos(theta), r * math.sin(theta))

def euler(theta):
    return Complex(math.cos(theta), math.sin(theta))

Verifica: euler(theta).magnitude() siempre debe ser 1.0. euler(0) debe dar (1, 0). euler(pi) debe dar (-1, 0).

Paso 3: Rotación

Rotar un punto (x, y) un ángulo theta es una multiplicación compleja:

point = Complex(3, 4)
rotated = point * euler(math.pi / 4)

La magnitud permanece igual. Solo cambia el ángulo.

Paso 4: DFT a partir de la aritmética compleja

def dft(signal):
    N = len(signal)
    result = []
    for k in range(N):
        total = Complex(0, 0)
        for n in range(N):
            angle = -2 * math.pi * k * n / N
            total = total + Complex(signal[n], 0) * euler(angle)
        result.append(total)
    return result

Esta es la DFT O(N^2). Cada salida X[k] es la suma de las muestras de la señal multiplicadas por las raíces de la unidad.

Paso 5: DFT inversa

La DFT inversa reconstruye la señal original a partir de su espectro. Los únicos cambios respecto a la DFT directa: invertir el signo del exponente y dividir por N.

def idft(spectrum):
    N = len(spectrum)
    result = []
    for n in range(N):
        total = Complex(0, 0)
        for k in range(N):
            angle = 2 * math.pi * k * n / N
            total = total + spectrum[k] * euler(angle)
        result.append(Complex(total.real / N, total.imag / N))
    return result

Esto te da una reconstrucción perfecta. Aplica la DFT, luego la IDFT, y recuperas la señal original con precisión de máquina. No se pierde información.

Paso 6: Raíces de la unidad

def roots_of_unity(N):
    return [euler(2 * math.pi * k / N) for k in range(N)]

Verifica dos propiedades:

  • Toda raíz tiene magnitud exactamente 1.
  • La suma de todas las N raíces es cero (se cancelan por simetría).

Estas propiedades son lo que hace invertible a la DFT. Las raíces de la unidad forman una base ortogonal para el dominio de la frecuencia.

Úsalo

Python tiene soporte nativo para números complejos. El literal j representa la unidad imaginaria.

z = 3 + 2j
w = 1 + 4j

print(z + w)
print(z * w)
print(abs(z))

import cmath
print(cmath.phase(z))
print(cmath.exp(1j * cmath.pi))

Para arreglos, numpy maneja los números complejos de forma nativa:

import numpy as np

z = np.array([1+2j, 3+4j, 5+6j])
print(np.abs(z))
print(np.angle(z))
print(np.conj(z))
print(np.real(z))
print(np.imag(z))

signal = np.sin(2 * np.pi * 5 * np.linspace(0, 1, 128))
spectrum = np.fft.fft(signal)
freqs = np.fft.fftfreq(128, d=1/128)

Entrégalo

Ejecuta code/complex_numbers.py para generar outputs/skill-complex-arithmetic.md.

Ejercicios

  1. Aritmética compleja a mano. Calcula (2 + 3i) * (4 - i) y verifica con el código. Luego calcula (5 + 2i) / (1 - 3i). Dibuja ambos resultados en el plano complejo y comprueba que la multiplicación rotó y escaló el primer número.

  2. Secuencia de rotación. Comienza con el punto (1, 0). Multiplica por e^(i*pi/6) doce veces. Verifica que regresas a (1, 0) tras 12 multiplicaciones. Imprime las coordenadas en cada paso y confirma que trazan un polígono regular de 12 lados.

  3. DFT de una señal conocida. Crea una señal que sea la suma de sin(2pi3t) y 0.5sin(2pi7*t) muestreada en 32 puntos. Ejecuta tu DFT. Verifica que el espectro de magnitud tiene picos en las frecuencias 3 y 7, con el pico en 7 siendo la mitad de la altura del pico en 3.

  4. Visualización de las raíces de la unidad. Calcula las 8 raíces de la unidad. Verifica que suman cero. Verifica que multiplicar cualquier raíz por la raíz primitiva e^(2pii/8) da la siguiente raíz.

  5. Equivalencia con la matriz de rotación. Para 10 ángulos aleatorios y 10 puntos aleatorios, verifica que la multiplicación compleja da el mismo resultado que la multiplicación matriz-vector con la matriz de rotación 2x2. Imprime la diferencia numérica máxima.

Términos Clave

Término Lo que significa
Número complejo Un número a + bi, donde a es la parte real, b es la parte imaginaria e i^2 = -1
Unidad imaginaria El número i, definido por i^2 = -1. No imaginario en el sentido filosófico -- es un operador de rotación
Plano complejo El plano 2D donde el eje x es real y el eje y es imaginario. También llamado plano de Argand
Magnitud (módulo) La distancia al origen: sqrt(a^2 + b^2). Escrita como |z|
Fase (argumento) El ángulo desde el eje real positivo: atan2(b, a). Escrita como arg(z)
Conjugado La imagen especular a través del eje real: el conjugado de a + bi es a - bi
Forma polar Expresar z como r * e^(i*theta) en lugar de a + bi. Hace fácil la multiplicación
Fórmula de Euler e^(itheta) = cos(theta) + isin(theta). Conecta las exponenciales con la trigonometría
Fasor Un número complejo rotativo e^(iomegat) que representa una señal sinusoidal
Raíces de la unidad Los N números complejos e^(2pii*k/N) para k = 0 a N-1. N puntos igualmente espaciados en el círculo unitario
DFT Transformada Discreta de Fourier. Descompone una señal en componentes sinusoidales complejos usando las raíces de la unidad
RoPE Rotary Position Embedding. Usa la multiplicación compleja para codificar la posición relativa en la atención de los transformers

Lecturas Adicionales

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).