# Least Squares Approximation¶

In practice, when trying to solve a linear system of equations $Ax=b$, there may not be an exact solution. One way to find an 'almost solution' is to find the vector $x'$ such that $\lVert Ax'-b \rVert_2$ is minimized, where $A$ is a full rank $m \times n$ matrix such that $m\geq n$. This least squares solution which works by minimizing the sum of the squares of the differences between the entries of $Ax'$ and $b$ and thus solving the solution $Ax=b$ as closely as possible.

If the system has an exact solution, $x'$ is precisely the value of $x$ that solves the system. The reasoning is as follows:

The system is solvable, so there is a vector $x$, such that $Ax=b$. Equivalently, there is a vector $x$ such that $Ax-b = 0$. Then, we have $\lVert Ax-b \rVert_2 = 0$. The L2 norm is nonnegative for all its inputs by definition, so the smallest value it can take is $0$. Thus, $x$ is the vector that minimizes the expression.

In this notebook, we will generate random systems, find least-squares approximations using deterministic and randomized methods, and then compare the results.

In [1]:
import numpy as np
from time import perf_counter
from matplotlib import pyplot as plt
from tqdm import tqdm

In [2]:
dims = np.arange(100, 2000, step=50) # dimensions of the random systems


### Deterministic QR-based Least Squares Solver¶

In [3]:
def ls(dims):
"""Calculates exact least-squares solution to a linear system using QR-based method."""
times = []
errors = []
for n in tqdm(dims):
m = 2 * n

A = np.random.randn(m,n)
b = np.random.randn(m,1)

start = perf_counter()

q,r = np.linalg.qr(A)
qt = np.transpose(q)
c = qt @ b

rinv = np.linalg.inv(r)
xls = rinv @ c

end = perf_counter()
times.append(end - start)
errors.append(np.linalg.norm(A@xls - b))

return times, errors


#### Deterministic Results¶

In [4]:
times, errors = ls(dims)
fig, ax = plt.subplots(figsize=(10,5))

ax.set_xlabel('input size')

lns1 = ax.plot(dims, errors, c= 'b', label='Error')
ax.set_ylabel('Absolute Error')
ax1=ax.twinx()
ax1.set_ylabel('time elapsed (seconds)')
lns2 = ax1.plot(dims,times, c = 'm', label = 'Time')
lns = lns1+lns2
labs = [l.get_label() for l in lns]
ax.legend(lns, labs, loc=0)

plt.show()

100%|██████████████████████████████████████████████████████████████████████████████████| 38/38 [00:26<00:00,  1.46it/s]


### Naive Randomized Least Squares Solver¶

In [5]:
def rls(dims):
"""Calculates approximate least-squares solution to a linear system using randomized method."""
times = []
errors = []
iterations = 100
for n in tqdm(dims):
m = 2 * n

A = np.random.randn(m,n)
b = np.random.randn(m,1)
def calc_norm(x):
return np.linalg.norm(A@x - b)
x_sol = np.random.randn(n,1)
start = perf_counter()
for i in range(iterations):
x = np.random.randn(n,1)
x_sol = min(x,x_sol,key = calc_norm)
end = perf_counter()
times.append(end - start)
errors.append(np.linalg.norm(A@x_sol - b))
return times, errors


#### Randomized Results¶

In [6]:
times2, errors2 = rls(dims)
fig, ax = plt.subplots(figsize=(10,5))

ax.set_xlabel('input size')

lns1 = ax.plot(dims, errors2, c= 'b', label='Error')
ax.set_ylabel('Absolute Error')
ax1=ax.twinx()
ax1.set_ylabel('time elapsed (seconds)')
lns2 = ax1.plot(dims,times2, c = 'm', label = 'Time')
lns = lns1+lns2
labs = [l.get_label() for l in lns]
ax.legend(lns, labs, loc=0)

plt.show()

100%|██████████████████████████████████████████████████████████████████████████████████| 38/38 [00:20<00:00,  1.87it/s]


### Conclusion¶

The naive randomized method we designed fails to compete with the optimal deterministic algorithm. This example shows us that randomness isn't a tool that can be used blindly -- there are cases where the naive randomized algorithm will work perfectly, but in general, the use of randomness will have to be more structured and measured.