Last time¶
Linear Algebra
Polynomial evaluation
Orthogonality
Today¶
Revisit orthogonality
Constructing orthogonal bases
2.1 Orthogonal matrices
2.2 Gram-Schmidt orthogonalizationQR factorization
using LinearAlgebra
using Plots
using Polynomials
default(linewidth=4, legendfontsize=12)
using Pkg
Pkg.add("LaTeXStrings")
using LaTeXStrings
function vander(x, k=nothing)
if isnothing(k)
k = length(x)
end
m = length(x)
V = ones(m, k)
for j in 2:k
V[:, j] = V[:, j-1] .* x
end
V
end Updating registry at `~/.julia/registries/General.toml`
Resolving package versions...
Updating `~/.julia/environments/v1.10/Project.toml`
[b964fa9f] + LaTeXStrings v1.4.1
Updating `~/.julia/environments/v1.10/Manifest.toml`
[b964fa9f] ↑ LaTeXStrings v1.4.0 ⇒ v1.4.1
Precompiling packages...
770.3 ms ✓ LaTeXStrings
2517.9 ms ✓ Latexify
692.6 ms ✓ Latexify → SparseArraysExt
3 dependencies successfully precompiled in 6 seconds. 195 already precompiled.
vander (generic function with 2 methods)x = LinRange(-1, 1, 50)
A = vander(x, 4)
M = A * [.5 0 0 0; # 0.5
0 1 0 0; # x
0 0 1 0]' # x^2
# that is, M = [0.5 | x | x^2]
scatter(x, M, label = ["M_1" "M_2" "M_3"])
plot!(x, 0*x, label=:none, color=:black)Which inner product will be zero?
Which functions are even and odd?
Pairwise inner product¶
The pairwise inner products between two sets of vectors can be expressed by collecting the sets as columns in matrices and writing where . It follows from this definition that
M' * M3×3 Matrix{Float64}:
12.5 -1.11022e-15 8.67347
-1.11022e-15 17.3469 -2.22045e-16
8.67347 -2.22045e-16 10.8272Normalization and orthogonalization¶
q1 = M[:,1]
q1 /= norm(q1) # normalize q1
Q = [q1 M[:, 2:end]]
scatter(x, Q, label = ["M_1/|M_1|" "M_2" "M_3"])
plot!(x, 0*x, label=:none, color=:black)Q' * Q # now the first entry of the Q' * Q matrix, after normalizing the first column vector of M, is 13×3 Matrix{Float64}:
1.0 -1.94289e-16 2.45323
-1.94289e-16 17.3469 -2.22045e-16
2.45323 -2.22045e-16 10.82722. Orthogonality¶
2.1 Orthogonal matrices¶
If two vectors and are such that then we say and are orthogonal (or “ is orthogonal to ”).
A vector is said to be normalized if . If is orthogonal to and then we say and are orthonormal.
A square matrix with orthonormal columns is said to be an orthogonal matrix (or orthonormal matrix).
We typically use or and for matrices that are known/constructed to be orthogonal.
Orthogonal matrices are always full rank -- the columns are linearly independent.
The inverse of an orthogonal matrix is its transpose:
Orthogonal matrices are a powerful building block for robust numerical algorithms.
2.2 Gram-Schmidt orthogonalization¶
For many applications, we find ourselves interested in the column spaces of a matrix :
The idea of QR factorization is the construction of a sequence of orthonormal vectors, that span these successive spaces.
Thus, suppose we want to find an orthogonal basis for the span of the columns of :
Given , we can construct vectors and entries , by an iterative process of successive orthogonalization.
A naive algorithm¶
function gram_schmidt_naive(A)
m, n = size(A)
Q = zeros(m, n)
R = zeros(n, n)
for j in 1:n
v = A[:,j]
for k in 1:j-1
r = Q[:,k]' * v
v -= Q[:,k] * r
R[k,j] = r
end
R[j,j] = norm(v)
Q[:,j] = v / R[j,j]
end
Q, R
endgram_schmidt_naive (generic function with 1 method)A = vander(x, 4)
Q, R = gram_schmidt_naive(A)
@show norm(Q' * Q - I) # Q is indeed orthogonal
@show norm(Q * R - A); # A is factored as the product of Q and Rnorm(Q' * Q - I) = 6.609184312080184e-16
norm(Q * R - A) = 2.500206551829073e-16
2.500206551829073e-16What do orthogonal polynomials look like?¶
Computations with orthogonal polynomials form the basis of spectral methods, one of the most powerful techniques for the numerical solution of partial differential equations (PDE).
x = LinRange(-1, 1, 50)
A = vander(x, 6)
Q, R = gram_schmidt_naive(A)
plot(x, Q, marker=:auto, legend=:none)What happens if we use more than 50 values of ?
Theorem:¶
Every full-rank matrix () has a unique reduced factorization with ¶
The algorithm we’re using generates this positive matrix due to the line:
R[j,j] = norm(v)Solving equations using ¶
If then (because Q is orthogonal, and it is easy to solve with since is triangular).
x1 = [-0.9, 0.1, 0.5, 0.8] # points where we know values
y1 = [1, 2.4, -0.2, 1.3]
scatter(x1, y1)
A = vander(x1, 3)
Q, R = gram_schmidt_naive(A)
p1 = R \ (Q' * y1) # our computed solution: R^{-1} Q^T y1
p2 = A \ y1 # Julia A^{-1} y1
plot!(x, vander(x, 3) * p1, label = "\$ V(x) p1\$")
plot!(x, vander(x, 3) * p2, label = "\$ V(x) p2\$", linestyle = :dash)How accurate is it?¶
m = 20
x = LinRange(-1, 1, m) # let's include more columns in the Vandermonde matrix
A = vander(x, m)
Q, R = gram_schmidt_naive(A)
@show norm(Q' * Q - I) # we are losing orthogonality; unstable algorithm
@show norm(Q * R - A)norm(Q' * Q - I) = 1.073721107832196e-8
norm(Q * R - A) = 8.268821431611631e-16
8.268821431611631e-16A variant with more parallelism¶
We are performing the following (from right to left):
Here removes the component of a vector in the direction , assuming is a unit vector. From right to left, we are first applying , which removes the component. Then we apply , which removes the component. In the above, the cross term vanishe as .
This operation can be factored in blocks to expoloit a bit of parallelism. Let’s call this classical Gram-Schmidt.
function gram_schmidt_classical(A)
m, n = size(A)
Q = zeros(m, n)
R = zeros(n, n)
for j in 1:n
v = A[:,j]
R[1:j-1,j] = Q[:,1:j-1]' * v
v -= Q[:,1:j-1] * R[1:j-1,j]
R[j,j] = norm(v)
Q[:,j] = v / norm(v)
end
Q, R
endgram_schmidt_classical (generic function with 1 method)m = 20
x = LinRange(-1, 1, m)
A = vander(x, m)
Q, R = gram_schmidt_classical(A)
@show norm(Q' * Q - I) # really not orthogonal; unstable algorithm
@show norm(Q * R - A)norm(Q' * Q - I) = 1.4985231287367549
norm(Q * R - A) = 7.350692433565389e-16
7.350692433565389e-16Classical Vs Modified Gram-Schmidt¶
Why does the order of operations matter?¶
We are performing the following (from right to left):
which can be factored in blocks and is not exact in finite arithmetic.
Let’s look at this a bit more closely. In the classical Gram-Schmidt (CGS), we take each vector, one at a time, and make it orthogonal to all previous vectors. If an error is made in computing in CGS, so that is small, but non-zero in finite arithmetic, this will not be corrected for in any of the computations that follow, and it will actually be propagated. In this case, will not be orthogonal to or .
Because of rounding errors, in classical Gram-Schmidt, does not have truly orthogonal columns. In modified Gram–Schmidt (MGS) we compute the length of the projection of onto and subtract that projection (and the rounding errors) from . Next, we compute the length of the projection of the computed onto and subtract that projection (and the rounding errors) from , and so on, but always orthogonalizing against the computed version of .
We can look at the size of what’s left over¶
We project out the components of our vectors in the directions of each .
x = LinRange(-1, 1, 23)
A = vander(x)
Q, R = gram_schmidt_classical(A)
scatter(diag(R), yscale=:log10)The next vector is almost linearly dependent¶
x = LinRange(-1, 1, 20)
A = vander(x)
Q, _ = gram_schmidt_classical(A)
#Q, _ = qr(A) # try it with Julia's built-in
v = A[:,end]
@show norm(v)
# let's check the coordinates of the projection of v onto the subspace spanned by the columns of Q, to measure how strongly v aligns with each orthonormal direction
scatter(abs.(Q[:,1:end-1]' * v), yscale=:log10, xlabel = L"$q_i$ column index") norm(v) = 1.4245900685395503
We can see that has substantial components in some -directions, while it is nearly orthogonal to the other directions.
Right-looking modified Gram-Schmidt¶
Each outer step of the modified Gram-Schmidt algorithm can be interpreted as a right-multiplication by a square upper-triangular matrix.
function gram_schmidt_modified(A)
m, n = size(A)
Q = copy(A)
R = zeros(n, n)
for j in 1:n
R[j,j] = norm(Q[:,j])
Q[:,j] /= R[j,j]
R[j,j+1:end] = Q[:,j]'*Q[:,j+1:end]
Q[:,j+1:end] -= Q[:,j]*R[j,j+1:end]'
end
Q, R
endgram_schmidt_modified (generic function with 1 method)m = 20
x = LinRange(-1, 1, m)
A = vander(x, m)
Q, R = gram_schmidt_modified(A)
@show norm(Q' * Q - I) # better, in terms of orthogonality error
@show norm(Q * R - A)norm(Q' * Q - I) = 8.486718528276085e-9
norm(Q * R - A) = 8.709998074379606e-16
8.709998074379606e-16The order of operations matter!
Classical versus modified?¶
Classical
Really unstable, orthogonality error of size
Don’t need to know all the vectors in advance
Modified
Needs to be right-looking for efficiency
Less unstable, but orthogonality error
m = 10
x = LinRange(-1, 1, m)
A = vander(x, m)
Q, R = qr(A) # the Julia built-in
@show norm(Q' * Q - I) # not so bad at all..norm(Q' * Q - I) = 2.2760345477368898e-15
2.2760345477368898e-15