Sistemas y matrices

Eliminación gaussiana con Python

Los factores L, U y P que deja la eliminación, la razón para conservarlos, y las factorizaciones de Cholesky y QR con sus dominios de aplicación.

La lección sobre sistemas lineales presentó la eliminación gaussiana como el procedimiento que np.linalg.solve ejecuta internamente. Esta se ocupa de lo que ese procedimiento deja tras de sí. La eliminación no solo produce una solución: produce una factorización de la matriz, y esa factorización es reutilizable.

Lo que la eliminación produce

Aplicar eliminación gaussiana con pivoteo parcial a AA equivale a construir tres matrices:

PA=LUPA = LU

donde PP es una permutación que registra los intercambios de fila, LL es triangular inferior con unos en la diagonal y contiene los multiplicadores empleados, y UU es triangular superior y es el resultado visible de la eliminación.

En la figura, UU se va formando por eliminación mientras LL almacena cada multiplicador rk\ell_{rk} en la posición que acaba de anularse:

P
100010001
L
100010001
U
21-1-3-12-212

P A = L U

Paso 1 / 7

Nada se descarta. Cada operación de fila queda registrada: los intercambios en PP, los multiplicadores en LL, y el resultado en UU. Esa es la diferencia entre ejecutar la eliminación y factorizar.

from scipy.linalg import lu P, L, U = lu(A) np.allclose(P @ L @ U, A) # True

La convención de SciPy difiere de la habitual. Devuelve PP tal que A=PLUA = PLU, mientras que los textos, y el enunciado de más arriba, escriben PA=LUPA = LU. Ambas describen la misma factorización: como PP es una permutación, P1=PP^{-1} = P^\top, de modo que A=PLUA = PLU equivale a PA=LUP^\top A = LU. La PP de SciPy es la traspuesta de la PP del enunciado.

Un subproducto inmediato: el determinante se lee en la diagonal de UU.

detA=(1)si=1nuii\det A = (-1)^{s} \prod_{i=1}^{n} u_{ii}

donde ss es el número de intercambios de fila. Para la matriz de la figura, dos intercambios y una diagonal (3,  5/3,  1/5)(-3,\; 5/3,\; 1/5) dan detA=1\det A = -1. Es así como np.linalg.det obtiene su resultado: no desarrolla por menores, que costaría O(n!)O(n!), sino que factoriza.

Factorizar una vez, resolver muchas

La factorización cuesta aproximadamente 23n3\tfrac{2}{3}n^3 operaciones. Resolver con ella, en cambio, son dos sustituciones triangulares —hacia adelante con LL, hacia atrás con UU— que cuestan 2n22n^2 en conjunto.

Esa asimetría es el motivo de conservar los factores. Cuando el mismo AA se resuelve contra varios términos independientes, situación frecuente en la práctica, repetir la eliminación desperdicia todo el trabajo caro:

from scipy.linalg import lu_factor, lu_solve lu_piv = lu_factor(A) # una vez: O(n³) x1 = lu_solve(lu_piv, b1) # cada uno: O(n²) x2 = lu_solve(lu_piv, b2)
resolver k veces
1.7·10⁹
factorizar una vez
93.3·10⁶

factor de mejora ×18.0

Recuento de operaciones: 2n³/3 para factorizar, 2n² por cada sustitución triangular.

matriz 500×500 · términos independientes k = 20

La ventaja crece con nn y con el número de sistemas. Para nn grande el coste se aproxima al de una sola factorización, con independencia de cuántos términos independientes haya que procesar.

Si todos los términos independientes se conocen de antemano, np.linalg.solve acepta una matriz como segundo argumento y factoriza una sola vez:

B = np.column_stack([b1, b2, b3]) X = np.linalg.solve(A, B) # una factorización, tres soluciones

Cuando la matriz es simétrica: Cholesky

La lección sobre la inversa y la transpuesta estableció que AAA^\top A es simétrica y semidefinida positiva. Para matrices con esa estructura existe una factorización más barata:

A=LLA = LL^\top

con LL triangular inferior de diagonal positiva. Es la factorización de Cholesky, y requiere que AA sea simétrica y definida positiva. Cuesta 13n3\tfrac{1}{3}n^3 operaciones, la mitad que LU, porque explota la simetría en lugar de ignorarla.

L = np.linalg.cholesky(K) # falla si K no es definida positiva alpha = np.linalg.solve(L.T, np.linalg.solve(L, y))

El fallo es informativo, no un inconveniente: si Cholesky no converge, la matriz no es definida positiva, lo que en un proceso gaussiano suele indicar una matriz de covarianza mal condicionada. La práctica habitual consiste en añadir un término de regularización a la diagonal, K+σ2IK + \sigma^2 I, que desplaza todos los valores propios y restaura la definición positiva.

Cuando el sistema es rectangular: QR

Para mínimos cuadrados, la lección anterior señaló que formar AAA^\top A eleva al cuadrado el número de condición. La alternativa es factorizar AA directamente:

A=QRA = QR

con QQ de columnas ortonormales y RR triangular superior. Sustituyendo en las ecuaciones normales, el problema se reduce a Rw=QbR\vec{w} = Q^\top\vec{b}, un sistema triangular, sin llegar a construir AAA^\top A en ningún momento.

Q, R = np.linalg.qr(A) w = np.linalg.solve(R, Q.T @ b)

Es, en esencia, lo que hace np.linalg.lstsq internamente.

FactorizaciónRequiereCosteSe usa para
LUcuadrada, no singular23n3\tfrac{2}{3}n^3sistemas generales, determinante
Choleskysimétrica definida positiva13n3\tfrac{1}{3}n^3covarianzas, procesos gaussianos
QRrango columna completo2mn22mn^2mínimos cuadrados
SVDninguna20n3\sim 20n^3rango, núcleo, pseudoinversa

La forma escalonada reducida

La eliminación puede llevarse más allá de UU hasta la forma escalonada reducida por filas, con pivotes iguales a uno y ceros también por encima de ellos. NumPy no la proporciona, y la ausencia es deliberada: para resolver un sistema no aporta nada que LULU no dé ya, y en coma flotante la decisión de qué entrada es un cero exacto resulta ambigua.

Donde sí tiene lugar es en el álgebra exacta, con sympy:

import sympy as sp M = sp.Matrix([[2, 1, -1], [-3, -1, 2], [-2, 1, 2]]) M.rref() # aritmética racional exacta, sin redondeo

La distinción es de dominio, no de calidad. sympy opera sobre números racionales exactos y resulta apropiado para determinar estructura —rango, base del núcleo, dependencias entre filas— en matrices pequeñas. numpy y scipy operan en coma flotante sobre implementaciones optimizadas y son la herramienta para el cálculo numérico. Emplear el segundo para razonar sobre estructura exacta, o el primero para resolver un sistema de tamaño considerable, invierte ambos usos.

Por qué no se programa a mano

Las rutinas mostradas delegan en LAPACK, una biblioteca con décadas de depuración cuyas implementaciones tienen en cuenta la jerarquía de memoria: operan por bloques para aprovechar la caché, cosa que una traducción directa del pseudocódigo no hace.

Una implementación propia de la eliminación es un ejercicio instructivo y una mala decisión en producción. Será entre uno y dos órdenes de magnitud más lenta y, con mayor probabilidad, menos estable: el pivoteo, el criterio de detección de singularidad y el tratamiento de los casos límite concentran la dificultad, y son precisamente las partes que un primer intento omite.


Ejercicio. Factorizar AA con scipy.linalg.lu y comprobar que el producto de la diagonal de U, con el signo correspondiente al número de intercambios, reproduce np.linalg.det(A). Determinar después cuántos términos independientes hacen falta para que factorizar una vez resulte más barato que llamar a solve repetidamente, con n=1000n = 1000.