Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Chapter 3

Functions

Solution of least squares by the normal equations
lsnormal.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
"""
    lsnormal(A, b)

Solve a linear least-squares problem by the normal equations.
Returns the minimizer of ||b-Ax||.
"""
function lsnormal(A, b)
    N = A' * A
    z = A' * b
    R = cholesky(N).U
    w = forwardsub(R', z)                   # solve R'z=c
    x = backsub(R, w)                       # solve Rx=z
    return x
end
Solution of least squares by QR factorization
lsqrfact.jl
1
2
3
4
5
6
7
8
9
10
11
12
"""
    lsqrfact(A, b)

Solve a linear least-squares problem by QR factorization. Returns
the minimizer of ||b-Ax||.
"""
function lsqrfact(A, b)
    Q, R = qr(A)
    c = Matrix(Q)' * b
    x = backsub(R, c)
    return x
end
QR factorization by Householder reflections
qrfact.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
"""
    qrfact(A)

QR factorization by Householder reflections. Returns Q and R.
"""
function qrfact(A)
    m, n = size(A)
    Qt = diagm(ones(m))
    R = float(copy(A))
    for k in 1:n
        z = R[k:m, k]
        w = [-sign(z[1]) * norm(z) - z[1]; -z[2:end]]
        nrmw = norm(w)
        if nrmw < eps()
            continue    # already in place; skip this iteration
        end
        v = w / nrmw
        # Apply the reflection to each relevant column of R and Q
        for j in k:n
            R[k:m, j] -= v * (2 * (v' * R[k:m, j]))
        end
        for j in 1:m
            Qt[k:m, j] -= v * (2 * (v' * Qt[k:m, j]))
        end
    end
    return Qt', triu(R)
end

Examples

3.1 Fitting functions to data

Example 3.1.1

Here are 5-year averages of the worldwide temperature anomaly as compared to the 1951–1980 average (source: NASA).

using Plots
year = 1955:5:2000
temp = [ -0.0480, -0.0180, -0.0360, -0.0120, -0.0040,
       0.1180, 0.2100, 0.3320, 0.3340, 0.4560 ]
    
scatter(year, temp, label="data",
    xlabel="year", ylabel="anomaly (degrees C)", 
    legend=:bottomright)
Loading...

A polynomial interpolant can be used to fit the data. Here we build one using a Vandermonde matrix. First, though, we express time as decades since 1950, as it improves the condition number of the matrix.

t = @. (year - 1950) / 10
n = length(t)
V = [ t[i]^j for i in 1:n, j in 0:n-1 ]
c = V \ temp
10-element Vector{Float64}: -14.114000001832462 76.36173810552113 -165.45597224550528 191.96056669514388 -133.27347224319684 58.015577787494486 -15.962888891734785 2.6948063497166928 -0.2546666667177082 0.010311111113288083

The coefficients in vector c are used to create a polynomial. Then we create a function that evaluates the polynomial after changing the time variable as we did for the Vandermonde matrix.

using Polynomials
p = Polynomial(c)
f = yr -> p((yr - 1950) / 10)
plot!(f, 1955, 2000, label="interpolant")
Loading...

As you can see, the interpolant does represent the data, in a sense. However it’s a crazy-looking curve for the application. Trying too hard to reproduce all the data exactly is known as overfitting.

Example 3.1.2

Here are the 5-year temperature averages again.

year = 1955:5:2000
t = @. (year - 1950) / 10
temp = [ -0.0480, -0.0180, -0.0360, -0.0120, -0.0040,
          0.1180, 0.2100, 0.3320, 0.3340, 0.4560 ]
10-element Vector{Float64}: -0.048 -0.018 -0.036 -0.012 -0.004 0.118 0.21 0.332 0.334 0.456

The standard best-fit line results from using a linear polynomial that meets the least-squares criterion.

V = [ t.^0 t ]    # Vandermonde-ish matrix
@show size(V)
c = V \ temp
p = Polynomial(c)
size(V) = (10, 2)

Loading...
f = yr -> p((yr - 1950) / 10)
scatter(year, temp, label="data",
    xlabel="year", ylabel="anomaly (degrees C)", leg=:bottomright)
plot!(f, 1955, 2000, label="linear fit")
Loading...

If we use a global cubic polynomial, the points are fit more closely.

V = [ t[i]^j for i in 1:length(t), j in 0:3 ]   
@show size(V);
size(V) = (10, 4)

Now we solve the new least-squares problem to redefine the fitting polynomial.

p = Polynomial( V \ temp )
plot!(f, 1955, 2000, label="cubic fit")
Loading...

If we were to continue increasing the degree of the polynomial, the residual at the data points would get smaller, but overfitting would increase.

Example 3.1.3
a = [1/k^2 for k in 1:100] 
s = cumsum(a)        # cumulative summation
p = @. sqrt(6*s)

scatter(1:100, p;
    title="Sequence convergence", xlabel=L"k",  ylabel=L"p_k")
Loading...

This graph suggests that maybe pkπp_k\to \pi, but it’s far from clear how close the sequence gets. It’s more informative to plot the sequence of errors, ϵk=πpk\epsilon_k= |\pi-p_k|. By plotting the error sequence on a log-log scale, we can see a nearly linear relationship.

ϵ = @. abs(π - p)    # error sequence
scatter(1:100, ϵ;
    title="Convergence of errors", xaxis=(:log10, L"k"),  yaxis=(:log10, L"\epsilon_k"))
Loading...

The straight line on the log-log scale suggests a power-law relationship where ϵkakb\epsilon_k\approx a k^b, or logϵkb(logk)+loga\log \epsilon_k \approx b (\log k) + \log a.

k = 1:100
V = [ k.^0 log.(k) ]     # fitting matrix
c = V \ log.(ϵ)          # coefficients of linear fit
2-element Vector{Float64}: -0.1823752497282998 -0.9674103233127929

In terms of the parameters aa and bb used above, we have

a, b = exp(c[1]), c[2];
@show b;
b = -0.9674103233127929

It’s tempting to conjecture that the slope b1b\to -1 asymptotically. Here is how the numerical fit compares to the original convergence curve.

plot!(k, a * k.^b, l=:dash, label="power-law fit")
Loading...

3.2 The normal equations

Example 3.2.1

Because the functions sin2(t)\sin^2(t), cos2(t)\cos^2(t), and 1 are linearly dependent, we should find that the following matrix is somewhat ill-conditioned.

t = range(0, 3, 400)
f = [ x -> sin(x)^2, x -> cos((1 + 1e-7) * x)^2, x -> 1. ]
A = [ f(t) for t in t, f in f ]
@show κ = cond(A);
κ = cond(A) = 1.8253225428206295e7

Now we set up an artificial linear least-squares problem with a known exact solution that actually makes the residual zero.

x = [1., 2, 1]
b = A * x;

Using backslash to find the least-squares solution, we get a relative error that is well below κ\kappa times machine epsilon.

x_BS = A \ b
@show observed_error = norm(x_BS - x) / norm(x);
@show error_bound = κ * eps();
observed_error = norm(x_BS - x) / norm(x) = 1.7978982202463188e-10
error_bound = κ * eps() = 4.053030228813602e-9

If we formulate and solve via the normal equations, we get a much larger relative error. With κ21014\kappa^2\approx 10^{14}, we may not be left with more than about 2 accurate digits.

N = A' * A
x_NE = N \ (A'*b)
@show observed_err = norm(x_NE - x) / norm(x);
@show digits = -log10(observed_err);
observed_err = norm(x_NE - x) / norm(x) = 0.036205099396071284
digits = -(log10(observed_err)) = 1.441230255886605

3.3 The QR factorization

Example 3.3.1

Julia provides access to both the thin and full forms of the QR factorization.

A = rand(1.:9., 6, 4)
@show m, n = size(A);
(m, n) = size(A) = (6, 4)

Here is a standard call:

Q, R = qr(A)
Q
6×6 LinearAlgebra.QRCompactWYQ{Float64, Matrix{Float64}, Matrix{Float64}}
R
4×4 Matrix{Float64}: -5.91608 -11.156 -12.3393 -10.3109 0.0 -4.42073 -1.88721 1.1375 0.0 0.0 8.13519 2.07962 0.0 0.0 0.0 -5.10558

If you look carefully, you see that we seemingly got a full Q\mathbf{Q} but a thin R\mathbf{R}. However, the Q\mathbf{Q} above is not a standard matrix type. If you convert it to a true matrix, then it reverts to the thin form.

Q̂ = Matrix(Q)
6×4 Matrix{Float64}: -0.507093 0.14865 -0.120047 0.812445 -0.169031 -0.0258522 0.59808 0.187487 -0.338062 -0.277911 0.03738 -0.147422 -0.676123 -0.103409 -0.434902 -0.401645 -0.338062 0.626916 0.493129 -0.347785 -0.169031 -0.704473 0.440653 -0.0278298

We can test that Q\mathbf{Q} is an orthogonal matrix:

opnorm(Q' * Q - I)
7.780480177132984e-16

The thin Q^\hat{\mathbf{Q}} cannot be an orthogonal matrix, because it is not square, but it is still ONC:

Q̂' * Q̂ - I
4×4 Matrix{Float64}: -2.22045e-16 -3.51732e-17 -8.79344e-17 -6.14657e-17 -3.51732e-17 0.0 1.79093e-17 4.38173e-17 -8.79344e-17 1.79093e-17 -2.22045e-16 -9.58025e-17 -6.14657e-17 4.38173e-17 -9.58025e-17 0.0
Example 3.3.2

We’ll repeat the experiment of Example 3.2.1, which exposed instability in the normal equations.

The error in the solution by Function 3.3.2 is similar to the bound predicted by the condition number.

observed_error = norm(FNC.lsqrfact(A, b) - x) / norm(x);
@show observed_error;
@show error_bound = cond(A) * eps();
observed_error = 5.401298095140833e-10
error_bound = cond(A) * eps() = 4.053030228813602e-9

3.4 Computing QR factorizations

Example 3.4.1

We will use Householder reflections to produce a QR factorization of a random matrix.

A = rand(float(1:9), 6, 4)
m, n = size(A)
(6, 4)

Our first step is to introduce zeros below the diagonal in column 1 by using (3.4.4) and (3.4.1).

z = A[:, 1];
v = normalize(z - norm(z) * [1; zeros(m-1)])
P₁ = I - 2v * v'   # reflector
6×6 Matrix{Float64}: 0.450035 0.675053 0.112509 0.112509 0.337526 0.450035 0.675053 0.171408 -0.138099 -0.138099 -0.414296 -0.552394 0.112509 -0.138099 0.976984 -0.0230164 -0.0690493 -0.0920657 0.112509 -0.138099 -0.0230164 0.976984 -0.0690493 -0.0920657 0.337526 -0.414296 -0.0690493 -0.0690493 0.792852 -0.276197 0.450035 -0.552394 -0.0920657 -0.0920657 -0.276197 0.631737

We check that this reflector introduces zeros as it should:

P₁ * z
6-element Vector{Float64}: 8.888194417315587 0.0 5.551115123125783e-17 -5.551115123125783e-17 0.0 0.0

Now we replace A\mathbf{A} by PA\mathbf{P}\mathbf{A}.

A = P₁ * A
6×4 Matrix{Float64}: 8.88819 10.9134 7.87562 7.6506 0.0 1.65146 -7.43945 -1.70836 5.55112e-17 8.60858 6.59342 7.04861 -5.55112e-17 4.60858 -0.406576 2.04861 0.0 -0.17427 0.780273 0.145819 0.0 3.43431 3.3737 -0.805575

We are set to put zeros into column 2. We must not use row 1 in any way, lest it destroy the zeros we just introduced. So we leave it out of the next reflector.

z = A[2:m, 2]
v = normalize(z - norm(z) * [1; zeros(m-2)])
P₂ = I - 2v * v'
5×5 Matrix{Float64}: 0.157533 0.821174 0.439613 -0.0166236 0.327599 0.821174 0.199581 -0.428502 0.0162034 -0.319319 0.439613 -0.428502 0.770603 0.00867447 -0.170947 -0.0166236 0.0162034 0.00867447 0.999672 0.00646421 0.327599 -0.319319 -0.170947 0.00646421 0.872611

We now apply this reflector to rows 2 and below only.

A[2:m, :] = P₂ * A[2:m, :]
A
6×4 Matrix{Float64}: 8.88819 10.9134 7.87562 7.6506 2.11809e-17 10.4833 5.1559 6.15327 3.48656e-17 -1.11969e-15 -5.68358 -0.614325 -6.65637e-17 -6.10592e-16 -6.97904 -2.05372 4.17942e-19 2.57989e-17 1.02881 0.300945 -8.23633e-18 5.04637e-16 -1.52409 -3.86263

We need to iterate the process for the last two columns.

for j in 3:n
    z = A[j:m, j]
    v = normalize(z - norm(z) * [1; zeros(m-j)])
    P = I - 2v * v'
    A[j:m, :] = P * A[j:m, :]
end

We have now reduced the original to an upper triangular matrix using four orthogonal Householder reflections:

R = triu(A)
6×4 Matrix{Float64}: 8.88819 10.9134 7.87562 7.6506 0.0 10.4833 5.1559 6.15327 0.0 0.0 9.18648 2.61484 0.0 0.0 0.0 3.57326 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0