Manipulaciones de Bases Ortonormales en Python
Aprende a generar, validar y transformar bases ortonormales usando algoritmos clásicos y librerías modernas. Incluimos ejemplos, comparativas de rendimiento y recomendaciones de buenas prácticas.
Introducción
Una base ortonormal es un conjunto de vectores que son unitarios y mutuamente ortogonales. Estas bases son esenciales en áreas como procesamiento de señales, machine learning, optimización y métodos numéricos porque simplifican cálculos de proyección, descomposición y reducción dimensional.
En este artículo veremos:
- Fundamentos teóricos del algoritmo de Gram‑Schmidt y sus variantes.
- Implementaciones en
NumPyySciPy. - Comparativa con QR y SVD.
- Casos de uso reales y buenas prácticas de rendimiento y precisión.
Fundamentos teóricos
Dados n vectores linealmente independientes
v₁, v₂, …, vₙ ∈ ℝᵐ, el proceso de Gram‑Schmidt genera una base ortonormal u₁, u₂, …, uₙ mediante:
u₁ = v₁ / ‖v₁‖
for k = 2 … n:
wₖ = vₖ - Σ_{j=1}^{k-1} (⟨vₖ, uⱼ⟩) uⱼ
uₖ = wₖ / ‖wₖ‖
Donde ⟨·,·⟩ es el producto interno y ‖·‖ la norma euclídea.
La versión modificada (MGS) mejora la estabilidad numérica al actualizar vₖ en cada iteración, reduciendo la pérdida de ortogonalidad en precisión finita.
Implementación en Python
1️⃣ Gram‑Schmidt puro (NumPy)
import numpy as np
def gram_schmidt(A):
"""Devuelve una base ortonormal a partir de la matriz A (m x n)."""
m, n = A.shape
Q = np.zeros((m, n))
for k in range(n):
v = A[:, k]
for j in range(k):
proj = np.dot(Q[:, j], v) * Q[:, j]
v = v - proj
norm = np.linalg.norm(v)
if norm < 1e-12:
raise ValueError('Los vectores son linealmente dependientes')
Q[:, k] = v / norm
return Q
# Ejemplo de uso
A = np.random.rand(5, 3)
Q = gram_schmidt(A)
print('Q.T @ Q =\n', np.round(Q.T @ Q, 12)) # debe ser ~I
Este código es didáctico, pero su precisión se degrada para matrices mal condicionadas.
2️⃣ Versión Modificada (MGS) con NumPy
def modified_gram_schmidt(A):
m, n = A.shape
Q = A.astype(float).copy()
for i in range(n):
norm = np.linalg.norm(Q[:, i])
if norm < 1e-12:
raise ValueError('Dependencia lineal detectada')
Q[:, i] = Q[:, i] / norm
for j in range(i+1, n):
proj = np.dot(Q[:, i], Q[:, j])
Q[:, j] = Q[:, j] - proj * Q[:, i]
return Q
Qm = modified_gram_schmidt(A)
print('Ortogonalidad MGS:', np.max(np.abs(Qm.T @ Qm - np.eye(n))))
La MGS mantiene la ortogonalidad mucho mejor en precisión float64.
3️⃣ Usando scipy.linalg.qr (QR de Householder)
from scipy.linalg import qr
Q_qr, R = qr(A, mode='economic')
print('Ortogonalidad QR:', np.max(np.abs(Q_qr.T @ Q_qr - np.eye(n))))
Este método es el estándar de facto: rápido, estable y con soporte para float32, float64 e incluso tipos complejos.
Comparativa de rendimiento y precisión
| Método | Complejidad | Ortogonalidad (error máximo) | Tiempo (ms) – 1000×500 | Ventajas | Desventajas |
|---|---|---|---|---|---|
| Gram‑Schmidt (clásico) | O(m·n²) | ≈1e‑10 (float64) | ≈45 | Fácil de entender, sin dependencias externas. | Pérdida de ortogonalidad para matrices mal condicionadas. |
| Modified Gram‑Schmidt | O(m·n²) | ≈1e‑14 (float64) | ≈48 | Mejor estabilidad numérica. | Ligera sobrecarga de cálculo. |
QR (Householder) – scipy.linalg.qr |
O(m·n²) | ≈1e‑16 (float64) | ≈12 | Máxima estabilidad, soporta pivoteo. | Requiere SciPy, mayor consumo de memoria en modo full. |
SVD – numpy.linalg.svd |
O(m·n²) (más costoso) | ≈1e‑16 | ≈30 | Provee rangos y valores singulares útiles. | Sobre‑costo si solo se necesita ortogonalidad. |
Casos de uso en el mundo real
🔹 Reducción dimensional con PCA
El algoritmo de PCA comienza calculando la SVD de la matriz de datos y, a partir de los vectores singulares, se obtienen bases ortonormales que maximizan la varianza. En entornos con recursos limitados, una QR seguida de Gram‑Schmidt puede servir como alternativa ligera.
🔹 Sistemas de control y filtros Kalman
Los filtros Kalman utilizan matrices de covarianza que deben mantenerse simétricas y positivas definidas. Re‑ortogonalizar la matriz de transición mediante QR evita la degradación numérica en simulaciones a largo plazo.
🔹 Compresión de imágenes (JPEG, WebP)
Los bloques DCT se basan en bases ortonormales discretas. Implementar la DCT mediante una matriz ortonormal pre‑calculada (usando QR) acelera la codificación.
🔹 Machine Learning – Inicialización de pesos
En redes neuronales profundas, inicializar capas lineales con matrices ortonormales (por ejemplo, usando torch.nn.init.orthogonal_) mejora la propagación del gradiente.
Buenas prácticas y consideraciones de seguridad numérica
- Escalado previo: Normaliza los datos a rango
[0,1]o[-1,1]antes de aplicar Gram‑Schmidt para evitar overflow/underflow. - Tipo de dato: Usa
float64en cálculos críticos;float32puede ser suficiente en inferencia de IA cuando se combina contorch.nn.utils.prune. - Re‑ortogonalización periódica: En algoritmos iterativos (p.ej., método de potencias) reaplica QR cada k iteraciones para controlar la pérdida de ortogonalidad.
- Detección de degeneración: Verifica que la norma de los vectores intermedios no caiga bajo un umbral (
1e‑12) y, en caso contrario, aplica pivotado o elimina columnas dependientes. - Seguridad de la memoria: Cuando se trabaja con datos sensibles (ej. biométricos), sobrescribe buffers temporales con
numpy.fill(0)después de su uso para evitar filtraciones.
Optimización avanzada
Para matrices de gran escala (m,n > 10⁴) se recomiendan los siguientes enfoques:
- BLAS/LAPACK multihilo: Usa
numpy.linalg.qrcon OpenBLAS o Intel MKL compilados para aprovechar todos los núcleos. - Algoritmos aleatorios: Randomized QR (disponible en
scikit‑learn.utils.extmath.randomized_range_finder) reduce la complejidad aO(m·n·log(k))para obtener una base de rangok. - GPU: Bibliotecas como
cupy.linalg.qrotorch.linalg.qrtrasladan la carga a la GPU, logrando mejoras de 5‑10× en tiempo. - Batch processing: Cuando se procesan cientos de matrices pequeñas, agrúpalas en un tensor 3‑D y usa
torch.linalg.qrconbatch_first=Truepara paralelismo.
Troubleshooting frecuente
| Síntoma | Causa | Solución |
|---|---|---|
| Ortogonalidad ≈ 1e‑3 en matrices bien condicionadas | Implementación clásica sin re‑escalado | Cambiar a MGS o usar scipy.linalg.qr. |
Excepción LinAlgError: singular matrix | Columnas linealmente dependientes | Aplicar numpy.linalg.matrix_rank y eliminar columnas redundantes. |
| Desbordamiento (inf) al calcular normas | Valores extremadamente grandes | Normalizar los datos o usar dtype=np.float128 si la plataforma lo permite. |
| Rendimiento muy bajo en GPU | Transferencia de datos frecuente entre CPU y GPU | Mantener los tensores en GPU y evitar copias innecesarias. |
Conclusiones
Las bases ortonormales siguen siendo una herramienta esencial en el arsenal de cualquier ingeniero de datos o científico computacional. Si bien el algoritmo de Gram‑Schmidt es didáctico, en producción se prefiere QR (Householder) o SVD por su robustez y velocidad. La elección depende del caso de uso, recursos disponibles y requerimientos de precisión.
Implementa siempre una capa de validación (Q.T @ Q ≈ I) y considera re‑ortogonalizar periódicamente en procesos iterativos para garantizar resultados fiables.
Manipulaciones de Bases Ortonormales en Python: Algoritmo, Implementación y Casos de Uso