Abstract: In this session we combine the objective function perspective and the probabilistic perspective on linear regression. We motivate the importance of linear algebra by showing how much faster we can complete a linear regression using linear algebra.
::: {.cell .markdown}
import matplotlib.pyplot as plt
plt.rcParams.update({'font.size': 22})
%pip install notutils
from the command prompt where you can access your python installation.
The code is also available on GitHub: https://github.com/lawrennd/notutils
Once notutils
is installed, it can be imported in the usual manner.
import notutils
%pip install mlai
from the command prompt where you can access your python installation.
The code is also available on GitHub: https://github.com/lawrennd/mlai
Once mlai
is installed, it can be imported in the usual manner.
import mlai
[edit]
Regression involves predicting a real value, $y_i$, given an input vector, $\mathbf{ x}_i$. For example, the Tecator data involves predicting the quality of meat given spectral measurements. Or in radiocarbon dating, the C14 calibration curve maps from radiocarbon age to age measured through a back-trace of tree rings. Regression has also been used to predict the quality of board game moves given expert rated training data.
[edit]
Figure: Start of the 2012 London 100m race. Image from Wikimedia Commons http://bit.ly/191adDC |
The first thing we will do is load a standard data set for regression modelling. The data consists of the pace of Olympic Gold Medal 100m winners for the Olympics from 1896 to present. First we load in the data and plot.
%pip install pods
import numpy as np
import pods
data = pods.datasets.olympic_100m_men()
x = data['X']
y = data['Y']
offset = y.mean()
scale = np.sqrt(y.var())
import matplotlib.pyplot as plt
import teaching_plots as plot
import mlai
import pods
from matplotlib import pyplot as plt
%matplotlib inline
xlim = (1875,2030)
ylim = (9, 12)
yhat = (y-offset)/scale
fig, ax = plt.subplots(figsize=plot.big_wide_figsize)
_ = ax.plot(x, y, 'r.',markersize=10)
ax.set_xlabel('year', fontsize=20)
ax.set_ylabel('pace min/km', fontsize=20)
ax.set_xlim(xlim)
ax.set_ylim(ylim)
mlai.write_figure(filename='olympic-100m.svg',
directory='./datasets')
Figure: Olympic 100m wining times since 1896.
[edit]
|
Image from Wikimedia Commons |
The first thing we will do is load a standard data set for regression modelling. The data consists of the pace of Olympic Gold Medal Marathon winners for the Olympics from 1896 to present. First we load in the data and plot.
import numpy as np
import pods
data = pods.datasets.olympic_marathon_men()
x = data['X']
y = data['Y']
offset = y.mean()
scale = np.sqrt(y.var())
yhat = (y - offset)/scale
import matplotlib.pyplot as plt
import mlai.plot as plot
import mlai
xlim = (1875,2030)
ylim = (2.5, 6.5)
fig, ax = plt.subplots(figsize=plot.big_wide_figsize)
_ = ax.plot(x, y, 'r.',markersize=10)
ax.set_xlabel('year', fontsize=20)
ax.set_ylabel('pace min/km', fontsize=20)
ax.set_xlim(xlim)
ax.set_ylim(ylim)
mlai.write_figure(filename='olympic-marathon.svg',
directory='./datasets')
Figure: Olympic marathon pace times since 1896.
Things to notice about the data include the outlier in 1904, in this year, the olympics was in St Louis, USA. Organizational problems and challenges with dust kicked up by the cars following the race meant that participants got lost, and only very few participants completed.
More recent years see more consistently quick marathons.
[edit]
What is machine learning? At its most basic level machine learning is a combination of
$$\text{data} + \text{model} \stackrel{\text{compute}}{\rightarrow} \text{prediction}$$where data is our observations. They can be actively or passively acquired (meta-data). The model contains our assumptions, based on previous experience. That experience can be other data, it can come from transfer learning, or it can merely be our beliefs about the regularities of the universe. In humans our models include our inductive biases. The prediction is an action to be taken or a categorization or a quality score. The reason that machine learning has become a mainstay of artificial intelligence is the importance of predictions in artificial intelligence. The data and the model are combined through computation.
In practice we normally perform machine learning using two functions. To combine data with a model we typically make use of:
a prediction function a function which is used to make the predictions. It includes our beliefs about the regularities of the universe, our assumptions about how the world works, e.g. smoothness, spatial similarities, temporal similarities.
an objective function a function which defines the cost of misprediction. Typically it includes knowledge about the world’s generating processes (probabilistic objectives) or the costs we pay for mispredictions (empiricial risk minimization).
The combination of data and model through the prediction function and the objective function leads to a learning algorithm. The class of prediction functions and objective functions we can make use of is restricted by the algorithms they lead to. If the prediction function or the objective function are too complex, then it can be difficult to find an appropriate learning algorithm. Much of the acdemic field of machine learning is the quest for new learning algorithms that allow us to bring different types of models and data together.
A useful reference for state of the art in machine learning is the UK Royal Society Report, Machine Learning: Power and Promise of Computers that Learn by Example.
You can also check my post blog post on What is Machine Learning?.
[edit]
Laplace had the idea to augment the observations by noise, that is equivalent to considering a probability density whose mean is given by the prediction function $$p\left(y_i|x_i\right)=\frac{1}{\sqrt{2\pi\sigma^2}}\exp\left(-\frac{\left(y_i-f\left(x_i\right)\right)^{2}}{2\sigma^2}\right).$$
This is known as stochastic process. It is a function that is corrupted by noise. Laplace didn’t suggest the Gaussian density for that purpose, that was an innovation from Carl Friederich Gauss, which is what gives the Gaussian density its name.
In the standard Gaussian, parametized by mean and variance.
Make the mean a linear function of an input.
This leads to a regression model. $$ \begin{align*} y_i=&f\left(x_i\right)+\epsilon_i,\\ \epsilon_i \sim & \mathcal{N}\left(0,\sigma^2\right). \end{align*} $$
Assume $y_i$ is height and $x_i$ is weight.
import notutils as nu
nu.display_google_book(id='spcAAAAAMAAJ', page='PA72')
Figure: Legendre’s book was on the determination of orbits of comets. This page describes the formulation of least squares
Of course, the main text is in French, but the key part we are interested in can be roughly translated as
In most matters where we take measures data through observation, the most accurate results they can offer, it is almost always leads to a system of equations of the form $$E = a + bx + cy + fz + etc .$$ where $a$, $b$, $c$, $f$ etc are the known coefficients and $x$, $y$, $z$ etc are unknown and must be determined by the condition that the value of E is reduced, for each equation, to an amount or zero or very small.
He continues
Of all the principles that we can offer for this item, I think it is not broader, more accurate, nor easier than the one we have used in previous research application, and that is to make the minimum sum of the squares of the errors. By this means, it is between the errors a kind of balance that prevents extreme to prevail, is very specific to make known the state of the closest to the truth system. The sum of the squares of the errors $E^2 + \left.E^\prime\right.^2 + \left.E^{\prime\prime}\right.^2 + etc$ being if we wanted a minimum, by varying x alone, we will have the equation …
This is the earliest know printed version of the problem of least squares. The notation, however, is a little awkward for mordern eyes. In particular Legendre doesn’t make use of the sum sign, $$ \sum_{i=1}^3 z_i = z_1 + z_2 + z_3 $$ nor does he make use of the inner product.
In our notation, if we were to do linear regression, we would need to subsititue: $$\begin{align*} a &\leftarrow y_1-c, \\ a^\prime &\leftarrow y_2-c,\\ a^{\prime\prime} &\leftarrow y_3 -c,\\ \text{etc.} \end{align*}$$ to introduce the data observations $\{y_i\}_{i=1}^{n}$ alongside $c$, the offset. We would then introduce the input locations $$\begin{align*} b & \leftarrow x_1,\\ b^\prime & \leftarrow x_2,\\ b^{\prime\prime} & \leftarrow x_3\\ \text{etc.} \end{align*}$$ and finally the gradient of the function $$x \leftarrow -m.$$ The remaining coefficients ($c$ and $f$) would then be zero. That would give us $$\begin{align*} &(y_1 - (mx_1+c))^2 \\ + &(y_2 -(mx_2 + c))^2\\ + &(y_3 -(mx_3 + c))^2 \\ + & \text{etc.} \end{align*}$$ which we would write in the modern notation for sums as $$ \sum_{i=1}^n(y_i-(mx_i + c))^2 $$ which is recognised as the sum of squares error for a linear regression.
This shows the advantage of modern summation operator, $\sum$, in keeping our mathematical notation compact. Whilst it may look more complicated the first time you see it, understanding the mathematical rules that go around it, allows us to go much further with the notation.
Inner products (or dot products) are similar. They allow us to write $$ \sum_{i=1}^q u_i v_i $$ in a more compact notation, $\mathbf{u}\cdot\mathbf{v}.$
Here we are using bold face to represent vectors, and we assume that the individual elements of a vector $\mathbf{z}$ are given as a series of scalars $$ \mathbf{z} = \begin{bmatrix} z_1\\ z_2\\ \vdots\\ z_n \end{bmatrix} $$ which are each indexed by their position in the vector.
[edit]
Note that x
and y
are not pandas
data frames for this example,
they are just arrays of dimensionality $n\times 1$, where $n$ is the
number of data.
The aim of this lab is to have you coding linear regression in python. We will do it in two ways, once using iterative updates (coordinate ascent) and then using linear algebra. The linear algebra approach will not only work much better, it is easy to extend to multiple input linear regression and non-linear regression using basis functions.
Now we will take the maximum likelihood approach we derived in the lecture to fit a line, $y_i=mx_i + c$, to the data you’ve plotted. We are trying to minimize the error function: $$ E(m, c) = \sum_{i=1}^n(y_i-mx_i-c)^2 $$ with respect to $m$, $c$ and $\sigma^2$. We can start with an initial guess for $m$,
m = -0.4
c = 80
Then we use the maximum likelihood update to find an estimate for the offset, $c$.
import numpy as np
import matplotlib.pyplot as plt
import mlai
x = np.random.normal(size=(4, 1))
m_true = 1.4
c_true = -3.1
y = m_true*x+c_true
import matplotlib.pyplot as plt
plt.plot(x, y, 'r.', markersize=10) # plot data as red dots
plt.xlim([-3, 3])
mlai.write_figure(filename='regression.svg', directory='./ml', transparent=True)
Figure: A simple linear regression.
noise = np.random.normal(scale=0.5, size=(4, 1)) # standard deviation of the noise is 0.5
y = m_true*x + c_true + noise
plt.plot(x, y, 'r.', markersize=10)
plt.xlim([-3, 3])
mlai.write_figure(filename='regression_noise.svg', directory='./ml', transparent=True)
Figure: A simple linear regression with noise.
# create an array of linearly separated values around m_true
m_vals = np.linspace(m_true-3, m_true+3, 100)
# create an array of linearly separated values ae
c_vals = np.linspace(c_true-3, c_true+3, 100)
m_grid, c_grid = np.meshgrid(m_vals, c_vals)
E_grid = np.zeros((100, 100))
for i in range(100):
for j in range(100):
E_grid[i, j] = ((y - m_grid[i, j]*x - c_grid[i, j])**2).sum()
import teaching_plots as plot
import mlai
f, ax = plt.subplots(figsize=(5,5))
plot.regression_contour(f, ax, m_vals, c_vals, E_grid)
mlai.write_figure(filename='regression_contour.svg', directory='./ml')
Figure: Contours of the objective function for linear regression by minimizing least squares.
m_star = 0.0
c_star = -5.0
Now we need to compute the gradient of the error function, firstly with respect to $c$, $$ \frac{\text{d}E(m, c)}{\text{d} c} = -2\sum_{i=1}^n(y_i - mx_i - c) $$
This is computed in python as follows
c_grad = -2*(y-m_star*x - c_star).sum()
print("Gradient with respect to c is ", c_grad)
To see how the gradient was derived, first note that the $c$ appears in every term in the sum. So we are just differentiating $(y_i - mx_i - c)^2$ for each term in the sum. The gradient of this term with respect to $c$ is simply the gradient of the outer quadratic, multiplied by the gradient with respect to $c$ of the part inside the quadratic. The gradient of a quadratic is two times the argument of the quadratic, and the gradient of the inside linear term is just minus one. This is true for all terms in the sum, so we are left with the sum in the gradient.
The gradient with respect tom $m$ is similar, but now the gradient of the quadratic’s argument is $-x_i$ so the gradient with respect to $m$ is
$$\frac{\text{d}E(m, c)}{\text{d} m} = -2\sum_{i=1}^nx_i(y_i - mx_i - c)$$which can be implemented in python (numpy) as
m_grad = -2*(x*(y-m_star*x - c_star)).sum()
print("Gradient with respect to m is ", m_grad)
import teaching_plots as plot
f, ax = plt.subplots(figsize=plot.big_figsize)
plot.regression_contour(f, ax, m_vals, c_vals, E_grid)
ax.plot(m_star, c_star, 'g*', markersize=20)
ax.arrow(m_star, c_star, -m_grad*0.1, -c_grad*0.1, head_width=0.2)
mlai.write_figure(filename='regression_contour_step001.svg', directory='./ml/', transparent=True)
Figure: Single update descending the contours of the error surface for regression.
The step size has already been introduced, it’s again known as the learning rate and is denoted by $\eta$. $$ c_\text{new}\leftarrow c_{\text{old}} - \eta\frac{\text{d}E(m, c)}{\text{d}c} $$
gives us an update for our estimate of $c$ (which in the code we’ve
been calling c_star
to represent a common way of writing a
parameter estimate, $c^*$) and $$
m_\text{new} \leftarrow m_{\text{old}} - \eta\frac{\text{d}E(m, c)}{\text{d}m}
$$
Giving us an update for $m$.
print("Original m was", m_star, "and original c was", c_star)
learn_rate = 0.01
c_star = c_star - learn_rate*c_grad
m_star = m_star - learn_rate*m_grad
print("New m is", m_star, "and new c is", c_star)
num_plots = plot.regression_contour_fit(x, y, diagrams='./ml')
import pods
from ipywidgets import IntSlider
pods.notebook.display_plots('regression_contour_fit{num:0>3}.svg', directory='./ml', num=IntSlider(0, 0, num_plots, 1))
Figure: Batch gradient descent for linear regression
The real gradient with respect to $m$ is given by
$$\frac{\text{d}E(m, c)}{\text{d} m} = -2\sum_{i=1}^nx_i(y_i - mx_i - c)$$but it has $n$ terms in the sum. Substituting in the gradient we can see that the full update is of the form
$$m_\text{new} \leftarrow m_\text{old} + 2\eta\left[x_1 (y_1 - m_\text{old}x_1 - c_\text{old}) + (x_2 (y_2 - m_\text{old}x_2 - c_\text{old}) + \dots + (x_n (y_n - m_\text{old}x_n - c_\text{old})\right]$$This could be split up into lots of individual updates $$m_1 \leftarrow m_\text{old} + 2\eta\left[x_1 (y_1 - m_\text{old}x_1 - c_\text{old})\right]$$ $$m_2 \leftarrow m_1 + 2\eta\left[x_2 (y_2 - m_\text{old}x_2 - c_\text{old})\right]$$ $$m_3 \leftarrow m_2 + 2\eta \left[\dots\right]$$ $$m_n \leftarrow m_{n-1} + 2\eta\left[x_n (y_n - m_\text{old}x_n - c_\text{old})\right]$$
which would lead to the same final update.
# choose a random point for the update
i = np.random.randint(x.shape[0]-1)
# update m
m_star = m_star + 2*learn_rate*(x[i]*(y[i]-m_star*x[i] - c_star))
# update c
c_star = c_star + 2*learn_rate*(y[i]-m_star*x[i] - c_star)
Putting it all together in an algorithm, we can do stochastic gradient descent for our regression data.
num_plots = plot.regression_contour_sgd(x, y, diagrams='./ml')
import pods
from ipywidgets import IntSlider
pods.notebook.display_plots('regression_sgd_contour_fit{num:0>3}.svg',
directory='./ml', num=IntSlider(0, 0, num_plots, 1))
Figure: Stochastic gradient descent for linear regression.
Think about:
What effect does the learning rate have in the optimization? What’s the effect of making it too small, what’s the effect of making it too big? Do you get the same result for both stochastic and steepest gradient descent?
The stochastic gradient descent doesn’t help very much for such a small data set. It’s real advantage comes when there are many, you’ll see this in the lab.
[edit]
Now we’ve identified the empirical risk with the loss, we’ll use $E(\mathbf{ w})$ to represent our objective function. $$ E(\mathbf{ w}) = \sum_{i=1}^n\left(y_i - f(\mathbf{ x}_i, \mathbf{ w})\right)^2 $$ gives us our objective.
In the case of the linear prediction function we can substitute $f(\mathbf{ x}_i, \mathbf{ w}) = \mathbf{ w}^\top \mathbf{ x}_i$. $$ E(\mathbf{ w}) = \sum_{i=1}^n\left(y_i - \mathbf{ w}^\top \mathbf{ x}_i\right)^2 $$ To compute the gradient of the objective, we first of all expand the brackets.
In this section we’re going compute the minimum of the quadratic loss with respect to the parameters. When we do this we’ll also review linear algebra. We will represent all our errors and functions in the form of matrices and vectors.
Linear algebra is just a shorthand for performing lots of multiplications and additions simultaneously. What does it have to do with our system then? Well the first thing to note is that the classic linear function we fit for a one dimensional regression has the form: $$ f(x) = mx + c $$ the classical form for a straight line. From a linear algebraic perspective we are looking for multiplications and additions. We are also looking to separate our parameters from our data. The data is the givens. In French the word is données literally translated means givens that’s great, because we don’t need to change the data, what we need to change are the parameters (or variables) of the model. In this function the data comes in through $x$, and the parameters are $m$ and $c$.
What we’d like to create is a vector of parameters and a vector of data. Then we could represent the system with vectors that represent the data, and vectors that represent the parameters.
We look to turn the multiplications and additions into a linear algebraic form, we have one multiplication ($m\times c$) and one addition ($mx + c$). But we can turn this into a inner product by writing it in the following way, $$ f(x) = m \times x + c \times 1, $$ in other words we’ve extracted the unit value, from the offset, $c$. We can think of this unit value like an extra item of data, because it is always given to us, and it is always set to 1 (unlike regular data, which is likely to vary!). We can therefore write each input data location, $\mathbf{ x}$, as a vector $$ \mathbf{ x}= \begin{bmatrix} 1\\ x\end{bmatrix}. $$
Now we choose to also turn our parameters into a vector. The parameter
vector will be defined to contain $$
\mathbf{ w}= \begin{bmatrix} c \\ m\end{bmatrix}
$$ because if we now take the inner product between these to vectors we
recover $$
\mathbf{ x}\cdot\mathbf{ w}= 1 \times c + x \times m = mx + c
$$ In numpy
we can define this vector as follows
import numpy as np
# define the vector w
w = np.zeros(shape=(2, 1))
w[0] = m
w[1] = c
This gives us the equivalence between original operation and an operation in vector space. Whilst the notation here isn’t a lot shorter, the beauty is that we will be able to add as many features as we like and still keep the seame representation. In general, we are now moving to a system where each of our predictions is given by an inner product. When we want to represent a linear product in linear algebra, we tend to do it with the transpose operation, so since we have $\mathbf{a}\cdot\mathbf{b} = \mathbf{a}^\top\mathbf{b}$ we can write $$ f(\mathbf{ x}_i) = \mathbf{ x}_i^\top\mathbf{ w}. $$ Where we’ve assumed that each data point, $\mathbf{ x}_i$, is now written by appending a 1 onto the original vector $$ \mathbf{ x}_i = \begin{bmatrix} 1 \\ x_i \end{bmatrix} $$
We can do this for the entire data set to form a design
matrix $\mathbf{X}$, $$
\mathbf{X}
= \begin{bmatrix}
\mathbf{ x}_1^\top \\\
\mathbf{ x}_2^\top \\\
\vdots \\\
\mathbf{ x}_n^\top
\end{bmatrix} = \begin{bmatrix}
1 & x_1 \\\
1 & x_2 \\\
\vdots
& \vdots \\\
1 & x_n
\end{bmatrix},
$$ which in numpy
can be done with the following commands:
import numpy as np
X = np.hstack((np.ones_like(x), x))
print(X)
When we think of the objective function, we can think of it as the errors where the error is defined in a similar way to what it was in Legendre’s day $y_i - f(\mathbf{ x}_i)$, in statistics these errors are also sometimes called residuals. So we can think as the objective and the prediction function as two separate parts, first we have, $$ E(\mathbf{ w}) = \sum_{i=1}^n(y_i - f(\mathbf{ x}_i; \mathbf{ w}))^2, $$ where we’ve made the function $f(\cdot)$’s dependence on the parameters $\mathbf{ w}$ explicit in this equation. Then we have the definition of the function itself, $$ f(\mathbf{ x}_i; \mathbf{ w}) = \mathbf{ x}_i^\top \mathbf{ w}. $$ Let’s look again at these two equations and see if we can identify any inner products. The first equation is a sum of squares, which is promising. Any sum of squares can be represented by an inner product, $$ a = \sum_{i=1}^{k} b^2_i = \mathbf{b}^\top\mathbf{b}, $$ so if we wish to represent $E(\mathbf{ w})$ in this way, all we need to do is convert the sum operator to an inner product. We can get a vector from that sum operator by placing both $y_i$ and $f(\mathbf{ x}_i; \mathbf{ w})$ into vectors, which we do by defining $$ \mathbf{ y}= \begin{bmatrix}y_1\\ y_2\\ \vdots \\ y_n\end{bmatrix} $$ and defining $$ \mathbf{ f}(\mathbf{ x}_1; \mathbf{ w}) = \begin{bmatrix}f(\mathbf{ x}_1; \mathbf{ w})\\ f(\mathbf{ x}_2; \mathbf{ w})\\ \vdots \\ f(\mathbf{ x}_n; \mathbf{ w})\end{bmatrix}. $$ The second of these is actually a vector-valued function. This term may appear intimidating, but the idea is straightforward. A vector valued function is simply a vector whose elements are themselves defined as functions, i.e. it is a vector of functions, rather than a vector of scalars. The idea is so straightforward, that we are going to ignore it for the moment, and barely use it in the derivation. But it will reappear later when we introduce basis functions. So we will, for the moment, ignore the dependence of $\mathbf{ f}$ on $\mathbf{ w}$ and $\mathbf{X}$ and simply summarise it by a vector of numbers $$ \mathbf{ f}= \begin{bmatrix}f_1\\f_2\\ \vdots \\ f_n\end{bmatrix}. $$ This allows us to write our objective in the folowing, linear algebraic form, $$ E(\mathbf{ w}) = (\mathbf{ y}- \mathbf{ f})^\top(\mathbf{ y}- \mathbf{ f}) $$ from the rules of inner products. But what of our matrix $\mathbf{X}$ of input data? At this point, we need to dust off matrix-vector multiplication. Matrix multiplication is simply a convenient way of performing many inner products together, and it’s exactly what we need to summarise the operation $$ f_i = \mathbf{ x}_i^\top\mathbf{ w}. $$ This operation tells us that each element of the vector $\mathbf{ f}$ (our vector valued function) is given by an inner product between $\mathbf{ x}_i$ and $\mathbf{ w}$. In other words it is a series of inner products. Let’s look at the definition of matrix multiplication, it takes the form $$ \mathbf{c} = \mathbf{B}\mathbf{a}, $$ where $\mathbf{c}$ might be a $k$ dimensional vector (which we can intepret as a $k\times 1$ dimensional matrix), and $\mathbf{B}$ is a $k\times k$ dimensional matrix and $\mathbf{a}$ is a $k$ dimensional vector ($k\times 1$ dimensional matrix).
The result of this multiplication is of the form $$ \begin{bmatrix}c_1\\c_2 \\ \vdots \\ a_k\end{bmatrix} = \begin{bmatrix} b_{1,1} & b_{1, 2} & \dots & b_{1, k} \\ b_{2, 1} & b_{2, 2} & \dots & b_{2, k} \\ \vdots & \vdots & \ddots & \vdots \\ b_{k, 1} & b_{k, 2} & \dots & b_{k, k} \end{bmatrix} \begin{bmatrix}a_1\\a_2 \\ \vdots\\ c_k\end{bmatrix} = \begin{bmatrix} b_{1, 1}a_1 + b_{1, 2}a_2 + \dots + b_{1, k}a_k\\ b_{2, 1}a_1 + b_{2, 2}a_2 + \dots + b_{2, k}a_k \\ \vdots\\ b_{k, 1}a_1 + b_{k, 2}a_2 + \dots + b_{k, k}a_k\end{bmatrix} $$ so we see that each element of the result, $\mathbf{a}$ is simply the inner product between each row of $\mathbf{B}$ and the vector $\mathbf{c}$. Because we have defined each element of $\mathbf{ f}$ to be given by the inner product between each row of the design matrix and the vector $\mathbf{ w}$ we now can write the full operation in one matrix multiplication,
$$ \mathbf{ f}= \mathbf{X}\mathbf{ w}. $$import numpy as np
f = X@w # The @ sign performs matrix multiplication
Combining this result with our objective function, $$ E(\mathbf{ w}) = (\mathbf{ y}- \mathbf{ f})^\top(\mathbf{ y}- \mathbf{ f}) $$ we find we have defined the model with two equations. One equation tells us the form of our predictive function and how it depends on its parameters, the other tells us the form of our objective function.
resid = (y-f)
E = np.dot(resid.T, resid) # matrix multiplication on a single vector is equivalent to a dot product.
print("Error function is:", E)
[edit]
Performing a solve instead of a matrix inverse is the more numerically stable approach, but we can do even better. A QR-decomposition of a matrix factorises it into a matrix which is an orthogonal matrix $\mathbf{Q}$, so that $\mathbf{Q}^\top \mathbf{Q} = \mathbf{I}$. And a matrix which is upper triangular, $\mathbf{R}$. $$ \mathbf{X}^\top \mathbf{X}\boldsymbol{\beta} = \mathbf{X}^\top \mathbf{ y} $$ $$ (\mathbf{Q}\mathbf{R})^\top (\mathbf{Q}\mathbf{R})\boldsymbol{\beta} = (\mathbf{Q}\mathbf{R})^\top \mathbf{ y} $$ $$ \mathbf{R}^\top (\mathbf{Q}^\top \mathbf{Q}) \mathbf{R} \boldsymbol{\beta} = \mathbf{R}^\top \mathbf{Q}^\top \mathbf{ y} $$
$$ \mathbf{R}^\top \mathbf{R} \boldsymbol{\beta} = \mathbf{R}^\top \mathbf{Q}^\top \mathbf{ y} $$$$ \mathbf{R} \boldsymbol{\beta} = \mathbf{Q}^\top \mathbf{ y} $$
This is a more numerically stable solution because it removes the need to compute $\mathbf{X}^\top\mathbf{X}$ as an intermediate. Computing $\mathbf{X}^\top\mathbf{X}$ is a bad idea because it involves squaring all the elements of $\mathbf{X}$ and thereby potentially reducing the numerical precision with which we can represent the solution. Operating on $\mathbf{X}$ directly preserves the numerical precision of the model.
This can be more particularly seen when we begin to work with basis functions in the next session. Some systems that can be resolved with the QR decomposition can not be resolved by using solve directly.
import scipy as sp
Q, R = np.linalg.qr(X)
w = sp.linalg.solve_triangular(R, Q.T@y)
w = pd.DataFrame(w, index=X.columns)
w
For fitting linear models: Section 1.1-1.2 of Rogers and Girolami (2011)
Section 1.2.5 up to equation 1.65 of Bishop (2006)
Section 1.3 for Matrix & Vector Review of Rogers and Girolami (2011)
For more information on these subjects and more you might want to check the following resources.
Bishop, C.M., 2006. Pattern recognition and machine learning. springer.
Legendre, A.-M., 1805. Nouvelles méthodes pour la détermination des orbites des comètes. F. Didot.
Rogers, S., Girolami, M., 2011. A first course in machine learning. CRC Press.