Householder QR factorization, cont'd

20250925_085244.jpg 20250925_092230.jpg 20250925_085304.jpg

Implementation

import numpy as np

def QRstep(A):
    # modify A in place and return H
    m = A.shape[0]  # number of rows of A
    w = np.zeros(m)
    z = A[:,0] # first column of A
    w[0] = np.linalg.norm(z)
    if  z[0] >= 0: w[0] *=-1  # choose - to avoid possible bad r.o. error
    v = w - z
    Pv = np.outer(v,v)/np.dot(v,v)
    H = np.eye(m) - 2*Pv
    #A = H@A  # incorrect because creates a new matrix in memory
              # and points the name A at it, so the memory location referred to by A
              # in the calling routine is not modified.
    A[:,:] = H@A  # overwrite HA on A
    return H

def QR(A):
    # modify A in place and return Q
    m,n = A.shape  # number of rows and columns of A
    H = np.eye(m)
    for i in range(n):
        # construct the "Hi"
        Htildei = QRstep(A[i:,i:])
        Hi = np.eye(m)
        Hi[i:,i:] = Htildei
        H = Hi@H
    return H.T   # return Q

Note: an efficient implementation would not actually form the H matrices, but would work only with the v vectors.

# Let's test it!

A = np.random.randn(5,3)
Asaved = A.copy()

Q = QR(A)  # will modify A in place

display(Asaved)
display(Q)
display(A) # this is the transformed A, called R
           #                                   0

# first check that Q is orthogonal

import matplotlib.pyplot as plt
plt.imshow((Q.T)@Q,interpolation='nearest',cmap='gray')
plt.colorbar()
array([[ 0.16291236,  1.24942785, -3.15583016],
       [-1.37319029,  0.26587118,  0.57615065],
       [ 0.41994405,  0.50289645, -2.06341379],
       [-0.83800519,  0.029154  ,  1.0927124 ],
       [-3.4505053 , -0.88767803, -0.85167823]])

array([[-0.04249549, -0.85403932, -0.25619827, -0.01392667,  0.45053242],
       [ 0.35819503, -0.38964036,  0.56830581, -0.48935686, -0.3967809 ],
       [-0.1095419 , -0.29142011, -0.42393359,  0.30367714, -0.7944403 ],
       [ 0.21859265, -0.14424203,  0.51033369,  0.81672953,  0.06264092],
       [ 0.90006015,  0.11430564, -0.41380042,  0.03269523,  0.06727684]])

array([[-3.83363855e+00, -8.05540320e-01,  3.88103802e-02],
       [ 9.90766715e-17, -1.42288064e+00,  2.81706510e+00],
       [ 3.81724700e-18, -4.16484782e-18,  2.92077116e+00],
       [-2.52188336e-18, -5.10047312e-17, -1.27786966e-17],
       [ 2.81742418e-16,  7.91706409e-17, -3.63185212e-16]])

Observe that the last matrix is indeed upper-triangular to machine precision, and QTQ is the identity matrix to machine precision:

QTQ.png

Now check that QR is really a factorization of A:

# check that QR = A
Q@A - Asaved
array([[-2.22044605e-16,  2.22044605e-16, -4.44089210e-16],
       [-4.44089210e-16, -1.66533454e-16, -5.55111512e-16],
       [-2.77555756e-16, -1.11022302e-16,  4.44089210e-16],
       [ 1.11022302e-16,  3.81639165e-17,  0.00000000e+00],
       [ 0.00000000e+00, -2.22044605e-16,  3.33066907e-16]])

Yes, the difference is zero to machine precision. 🙂

20250925_092103.jpg linear_model.png quadratic_model.png