Linear Models
Notebook Cell
# we will need matplotlib for plotting
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
Linear algebra¶
Read Chapter 2 of MML Book by Deisenroth et al https://
mml -book .github .io/
Linear algebra is the study of vectors and certain rules to manipulate vectors.
Equation of a 2D line¶
There are two equations of lines
What are the advantages or disadvantages of both of those?
A 2D line is the set of all points (x, y) that satisfy the an equation for given . For the same line there can be multiple valid parameters . Either or can be zero. But both and cannot be zero at the same time.
Ideally the equation of line must be written in the set notation:
This equation is read as: the line defined by the paramters is the set of all points such that it the satisfy the equation and and are in the set of all real numbers.
Implicit form¶
Line is a type of curve. Other curves can be parabola, circle etc. Every curve can be represented in implicit form like we did for the line above. In implicit form, the points are constrained by one or more equations to lie on the curve. The equations define a test whether the point lies on the curve or not.
This equation is read as the curve defined by the paramters is the set of all points such that it the satisfy the equation and and are in the set of all real numbers.
Take circle with center and radius , as an example. The implicit form is:
Paramteric form of 2D Line¶
A parameteric form defines how the points on the curve are generated from a free paramters. For a line:
This equation is read as: the line defined by the paramters is the set of all points such that is any real number.
Take circle with center and radius , as an example. The parameteric form is:
In general, the paramteric form of a curve depend on some free paramters and the points are defined as functions of the free parameters.

Vectors¶
We will denote vectors with bold font notations instead of the usualy notation. The arrow notation vectors are sometimes called geometric vectors. We will make no such distinction. The set of all real numbers will be denoted as . The set of all real 2D vectors is written as . When we write , it means that is in the set of real 2D vectors, hence a 2D real vector. We will write for the magnitude of the vector, and for dot product between two vector and .
n-D vector¶
A n-D vector is also written as and the vector has real components. Every vector has a magnitude and a direction . The magnitude and direction are given by:
The direction vector is a unit vector because its magnitide is one i.e. .
x = np.array([2, 3, 4, 5, 9, 10])
xmag = np.linalg.norm(x)
xunit = x / xmag
# For many many x vectors you can do this:
xs = np.array([[2, 3, 4, 5, 9, 10],
[-3, 2, -5, 4, -10, 9]]) # 2 x 6
xmags = np.linalg.norm(xs, axis=-1, keepdims=True) # 2 x 1
xunits = xs / xmags # 2 x 6
print(xunit, xunits)[0.13046561 0.19569842 0.26093123 0.32616404 0.58709527 0.65232807] [[ 0.13046561 0.19569842 0.26093123 0.32616404 0.58709527 0.65232807]
[-0.19569842 0.13046561 -0.32616404 0.26093123 -0.65232807 0.58709527]]
Vector addition¶
Vector addition is element-wise addition
Geometrically the resulting vector can be obtained by triangle law or the parallelogram law.

Reference: [1]
v = np.arange(0, 10)
w = np.arange(10, 20)
v+warray([10, 12, 14, 16, 18, 20, 22, 24, 26, 28])Dot product of vectors¶
Dot product of two vectors is a scalar given by sum of element-wise product.
Geometrically, dot product is closely related to the projection. Projection of vector on is the dot product of with the direction of

Dot product of vector with itself gives the square of the magnitude .
Reference: [2]
v = np.arange(10)
u = np.arange(10, 20)
v @ u # v . unp.int64(735)v @ u / np.linalg.norm(u)np.float64(15.723948706640511)np.allclose(v @ v , np.linalg.norm(v)**2)TrueMatrices¶
Matrices are a group of vectors. A matrix can be obtained by vertical stacking of row (horizontal) vectors or horizontal stacking of column (vertical) vectors. It is common to represent all vectors as column vectors unless specified otherwise, so we consider a matrix as a horizontal concatenation of column vectors . Let each vector be m-dimensional .
Such a matrix is said to be a matrix. It is also written as .
v1 = np.arange(5)
v2 = np.arange(5, 10)
v3 = np.arange(10, 15)
V = np.hstack((v1[:, None], v2[:, None], v3[:, None]))
Varray([[ 0, 5, 10],
[ 1, 6, 11],
[ 2, 7, 12],
[ 3, 8, 13],
[ 4, 9, 14]])Linear functions¶
What are linear functions?
Definition of Linear combination (2.11 in MML book)¶
Given vectors , a linear combination of vectors is defined as, for every any scalar
Definition of Linear function¶
A function is linear in when
for all and
for all and
The above two properties can be combined in a single test
Examples of linear functions¶
Homework (Linear Models). Problem 1¶
(10 marks) Show that the above functions are linear.
Examples of non-linear functions¶
Homework (Linear Models). Problem 2¶
(10 marks) Show that the above functions are non-linear.
def f(x):
return 4*(3*x[0] + 2*x[1]) + x[2]
x = np.random.rand(3)
y = np.random.rand(3)
alpha = np.random.rand()
beta = np.random.rand()
np.allclose(f(alpha * x + beta * y) ,
alpha * f(x) + beta * f(y))TrueProperties of Matrices¶
Transpose of a Matrix¶
Transpose of a matrix (denoted as ) is an operation that swaps rows with columns and columns with rows. For example, the transpose of the above matrix will make it a vertical concatenation of row vectors.
If , then . The two dimensions get swapped.
V = np.arange(12).reshape(3, 4)
V, V.T(array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]]),
array([[ 0, 4, 8],
[ 1, 5, 9],
[ 2, 6, 10],
[ 3, 7, 11]]))Tranpose of a column vector¶
All vectors are also matrices. By convention, all vectors are considered column matrices and hence called column vectors. A n-D vector is by convention considered a column matrix i.e. .
The transpose of a column vector is a row vector
Row vectors are always denoted with a tranpose of their corresponding column vector.
v = np.arange(5).reshape(5, 1)
v.shape, v.T.shape((5, 1), (1, 5))A = np.random.rand(3, 4)
B = np.random.rand(3, 4)
np.allclose(A + B, B+ A)TrueMatrix-scalar product¶
You can multiply matrices with a scalar (a real number)
Matrix scalar product is commutative
alpha = np.random.rand()
A = np.random.rand(3, 4)
np.allclose(alpha * A, A * alpha)TrueMatrix-vector product¶
For those who know dot product, matrix-vector product is best defined as a collection of dot products. Define a matrix as a vertical concatenation of n-dimensional row-vectors
The matrix can be multiplied by a n-dimensional column vector with the product defined as the vector-wise dot product vertically concatenated to result in another column vector:
When , then and the matrix product is . Dot product between two vectors and is also written as . Going forward we will prefer notation for dot product instead of .
V = np.random.rand(3, 4)
u = np.random.rand(4)
V @ uarray([0.61997133, 0.38670963, 1.42804002])Matrix-matrix product¶
Matrix-matrix product between two matrices and can be defined in terms of matrix-vector product by writing as a horizontal concatenation of -dimensional column vectors .
The result is a horizontal concatenation of matrix-vector products, where the left matrix gets multiplied with each column vector of right matrix .
Another interpretation is that the matrix-matrix product are all possible dot products between left matrices’ row vectors with right matrices’ column vectors.
Matrix-matrix product or short matrix products do not commute i.e in general.
Matrix-matrix product does follow distributive property .
V = np.random.rand(3, 4)
U = np.random.rand(4, 5)
V @ Uarray([[0.9058884 , 0.22409012, 0.81158039, 0.65014481, 0.94369832],
[0.86286196, 0.50831017, 0.76892233, 0.68919024, 1.10617922],
[1.10100106, 0.41266198, 0.87124987, 0.88688025, 1.42231462]])Identity matrix¶
Square matrix¶
A square matrix is a matrix with number of rows equal to the number of columns.
Inverse of a square matrix¶
A matrix is called the inverse of a square matrix if . The inverse of a square matrix exists only when it is singular i.e the determinant of the matrix is non-zero .
I = np.eye(10)
Iarray([[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
[0., 1., 0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 1., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 1., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 1., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 1., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 1., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 1., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]])Linearity of matrix multiplication¶
Consider a function defined as
.
Show that the function is linear.
Proof¶
The function is linear if for any and ,
Theorem¶
All linear functions of the form can be written as matrix multplications of the form .
Proof:¶
Consider any given linear function which is linear. Because of linearity it satisifies .
Consider special vectors, called the standard basis vectors, that have only one element as 1 and the rest are 0.
Any vector can be written as a linear combination of standard basis vectors,
Hence the function evaluation at , can also be written as the linear combination of function evaluations at standard basis vectors,
Note that , , are constants with respect to . So you can arrange them into a constant matrix, independent of ,
Theorem¶
Show that composition of two linear functions, and is also a linear function.
Homework (Linear Models) Problem 3¶
(10 marks) Proof is left as a homework for the above theorem.
Using vectors for 2D line notation¶
def stylizeax(ax, limits):
"""Set ax style"""
minx, maxx, miny, maxy = limits
# x-axis, y=0
ax.annotate("",
xy=(minx, 0),
xytext=(maxx, 0),
arrowprops=dict(arrowstyle="<->"),
color='k')
ax.text(maxx, 0, "x")
# y-axis, x=0
ax.annotate("",
xy=(0, miny),
xytext=(0, maxy),
arrowprops=dict(arrowstyle="<->"),
color='k')
ax.text(0, maxy, "y")
ax.grid(True, which='both') # show the grid
ax.set_aspect('equal') # set aspect ratio of the grid to 1:1
def points_on_line(a, b, c, Npts=6, scale=10):
"""Generate points on the line ax + by + c = 0"""
# ax + by + c = 0
# In parameteric form with free parameter r
# (x, y) = (-b*r + x0, a*r + y0)
# where
# x0 = - a*c / (a*a + b*b)
# y0 = - b*c / (a*a + b*b)
# (x0, y0) is the point on the line closest to the origin
uniformgrid = [i/Npts for i in range(-scale*Npts//2, scale*Npts//2, scale)]
x0 = -a*c/(a*a + b*b)
y0 = -b*c/(a*a + b*b)
x = [-b*r + x0 for r in uniformgrid]
y = [ a*r + y0 for r in uniformgrid]
return x, y
Matplotlib¶
# Plot a line ax + by + c = 0
# a, b, c = 2.5, -1, -5 # pick numbers by hand
# pick a, b, c at random
import random
scale = 10
a, b, c = [scale*(random.random()-0.5) for _ in range(3)] # random number from -10 to 10
# Generate some sample points on a line
x, y = points_on_line(a, b, c, scale=scale)
# Plot the points
fig, ax = plt.subplots()
stylizeax(ax, (min(x), max(x), min(y), max(y)))
ax.plot(x, y, '*-') # the line
ax.set_title(f'{a:.1f}x{b:+.1f}y{c:+.1f} = 0') # print the equation
We started from 2D linear models, but we want to work with N-D models where N can be even in thousands or millions. It makes sense to simplify the notation by using vector notation.
Recall that the implicit equation for a line is
We will represent a 2D point (x, y) by a 2D vector and the parameters with weight vector . Let’s compute the dot product between the two newly defined vectors :
The equation of the line under new notation in full its full glory is
Unique line notation¶
Same line can be represented by multiple equations for the same form. This representation of line is not unique. For example, equations and represent the same line. In general, for any real number all equations represent the same line. Once can choose an arbitrary non-zero for making the equation unique.
In vector notation, all non-zero represent the same line. One good candidate for is , because this changes to a unit vector.
where .
Geometric interpretaion¶
The new equation of the line has a convinient geometric interpretation. Recall that is the projection of on . In other words, the equation constrains all vectors on the line to have a constant projection .
This means that vector is perpendicular to the line and cuts the line a distance from the origin.
import numpy as np # a vector algebra library
a = np.array([0, 1, 2, 3]) # a vector
print("a=", a)
b = np.array([4, 5, 6, 7]) # another vector
print("b=", b)
C = np.array([[0, 1, 2, 3],
[4, 5, 6, 7]]) # A matrix
print("C=", C)
D = np.zeros((2, 4)) # a 2x4 matrix of zeros
print("D=", D)
E = np.random.rand(2,5) # Random 2x5 matrix of numbers between 0 and 1
print("E=", E)a= [0 1 2 3]
b= [4 5 6 7]
C= [[0 1 2 3]
[4 5 6 7]]
D= [[0. 0. 0. 0.]
[0. 0. 0. 0.]]
E= [[0.6018821 0.02634302 0.13422675 0.44627918 0.65200175]
[0.62550933 0.93727334 0.29797968 0.16805498 0.53928057]]
print("a*0.1 = ", a * 0.1) # element-wise multiplication
print("C*0.2 = ", C * 0.2) # element-wise multiplication
print("a*b = ", a * b) # element-wise multiplication (Note: different from Matlab)
print("a*b*0.2 = ", a * b * 0.2) # element-wise multiplication
print("C @ a = ", C @ a) # matrix-vector product
print("C.T = ", C.T) # matrix transpose
print("C.T @ D = ", C.T @ D) # matrix-matrix product
print("a * C = ", a * C) # so called broadcasting; numpy specifica*0.1 = [0. 0.1 0.2 0.3]
C*0.2 = [[0. 0.2 0.4 0.6]
[0.8 1. 1.2 1.4]]
a*b = [ 0 5 12 21]
a*b*0.2 = [0. 1. 2.4 4.2]
C @ a = [14 38]
C.T = [[0 4]
[1 5]
[2 6]
[3 7]]
C.T @ D = [[0. 0. 0. 0.]
[0. 0. 0. 0.]
[0. 0. 0. 0.]
[0. 0. 0. 0.]]
a * C = [[ 0 1 4 9]
[ 0 5 12 21]]
Numpy: General Broadcasting Rules¶
When operating on two arrays, NumPy compares their shapes element-wise. It starts with the trailing (i.e. rightmost) dimension and works its way left. Two dimensions are compatible when
they are equal, or
one of them is 1.
Otherwise a ValueError is raised
Ref: https://
In the following example, both the A and B arrays have axes with length one that are expanded to a larger size during the broadcast operation:
A (4d array): 8 x 1 x 6 x 1
B (3d array): 7 x 1 x 5
Result (4d array): 8 x 7 x 6 x 5A = np.random.rand(8, 1, 6, 1)
B = np.random.rand(7, 1, 5)
(A * B).shape # Returns the shape of the multi dimensional array(8, 7, 6, 5)Here are some more examples:
A (2d array): 5 x 4
B (1d array): 1
Result (2d array): ?
A (2d array): 5 x 4
B (1d array): 4
Result (2d array): ?
A (3d array): 15 x 3 x 5
B (3d array): 15 x 1 x 5
Result (3d array):
A (3d array): 15 x 3 x 5
B (2d array): 3 x 5
Result (3d array): ?
A (3d array): 15 x 3 x 5
B (2d array): 3 x 1
Result (3d array): ?
def points_on_line(hatw, w0, Npts=6, scale=10):
""" Generate some sample points on a line """
assert hatw.shape == (2,) # only works for 2D
perp_hatw = np.array([-hatw[1], hatw[0]])# vector perpendicular to hatw
uniformgrid = np.linspace(-scale//2, scale//2, Npts)
return perp_hatw * uniformgrid[:, None] - w0*hatw# Plot a line ax + by + c = 0
scale = 10
# a, b, c = [scale*(random.random()-0.5) for _ in range(3)] # random number from -10 to 10
# abc = scale*(np.random.rand(3)-0.5) # random number from -10 to 10
abc = [3, 2, -6] # pick your favorite line
w = abc[:2]
hatw = w / np.linalg.norm(w) # What does np.linalg.norm do?
w0 = abc[2] / np.linalg.norm(w)
# Generate some sample points on a line
x = points_on_line(hatw, w0, Npts=6, scale=scale) # Npts x 2 array
# Plot the points
fig, ax = plt.subplots()
ax.axis('equal')
stylizeax(ax, (x[:, 0].min(), x[:, 0].max(), x[:, 1].min(), x[:, 1].max())) # numpy allows for multi-dimensional slicing
ax.plot(x[:, 0], x[:, 1], '*-') # the line
pt0 = -w0*hatw
ax.annotate("", xytext=(0, 0), xy=(pt0[0], pt0[1]),
arrowprops=dict(arrowstyle="->", color='r'))
ax.text(pt0[0], pt0[1], r"$-w_0\hat{\mathbf{w}}$", color='r')

Linear regression: review¶
Let’s take the simple linear regression example from STS332 textbook (uploaded on brightspace;page 300; Table 6-1).
“As an illustration, consider the data in Table 6-1. In this table, y is the salt concentration (milligrams/liter) found in surface streams in a particular watershed and x is the percentage of the watershed area consisting of paved roads.”
%%writefile saltconcentration.tsv
#Observation SaltConcentration RoadwayArea
1 3.8 0.19
2 5.9 0.15
3 14.1 0.57
4 10.4 0.4
5 14.6 0.7
6 14.5 0.67
7 15.1 0.63
8 11.9 0.47
9 15.5 0.75
10 9.3 0.6
11 15.6 0.78
12 20.8 0.81
13 14.6 0.78
14 16.6 0.69
15 25.6 1.3
16 20.9 1.05
17 29.9 1.52
18 19.6 1.06
19 31.3 1.74
20 32.7 1.62Overwriting saltconcentration.tsv
# numpy can import text files separated by seprator like tab or comma
salt_concentration_data = np.loadtxt("saltconcentration.tsv")
salt_concentration_dataarray([[ 1. , 3.8 , 0.19],
[ 2. , 5.9 , 0.15],
[ 3. , 14.1 , 0.57],
[ 4. , 10.4 , 0.4 ],
[ 5. , 14.6 , 0.7 ],
[ 6. , 14.5 , 0.67],
[ 7. , 15.1 , 0.63],
[ 8. , 11.9 , 0.47],
[ 9. , 15.5 , 0.75],
[10. , 9.3 , 0.6 ],
[11. , 15.6 , 0.78],
[12. , 20.8 , 0.81],
[13. , 14.6 , 0.78],
[14. , 16.6 , 0.69],
[15. , 25.6 , 1.3 ],
[16. , 20.9 , 1.05],
[17. , 29.9 , 1.52],
[18. , 19.6 , 1.06],
[19. , 31.3 , 1.74],
[20. , 32.7 , 1.62]])# Plot the points
fig, ax = plt.subplots()
# Scatter plot using matplotlib
ax.scatter(salt_concentration_data[:, 2], salt_concentration_data[:, 1])
ax.set_xlabel(r"Roadway area %")
ax.set_ylabel(r"Salt concentration (mg/L)")
Least squares regression¶

The problem of linear regression is to find a line that “best fits” the given data. That is we want all the points to satisfy the equation of the line . Since we know that there exists no such line, so we will try to make , by minimizing some error/distance/cost/loss function between and for every point in the dataset. The simplest error function that results in nice answers is squared distance:
Then we can minimize the total error to find the line:
Geometrically, this error minimization corresponds to minimizing the stubs in the following figure:
Vectorization of Least square regression¶
Recall that the magnitude of a vector has a similar form to the error function. This suggests that we can define an error vector with the signed error for each data point as it’s elements
Minimizing the total error is same as minimizing the square of error vector magnitude
While we are at at it let us define to denote the vector of all x coordinates of the dataset and to denote y coordinates. Then the error vector is:
where is a n-D vector of all ones. Finally, we vectorize parameters of the line . We will also need to horizontally concatenate and . Let’s call the result . Now, the error vector looks like this:
Expanding the error magnitude:
Our minimization problem in vectorized form is:
This is a quadratic equation in that can be minimized by equating the derivate to zero.
Two rules of vector derivatives¶
There are two conventions in vector derivatives:
Gradient convention
Jacobian convention
Gradient convention¶
Under gradient convention the derivative of scalar-valued vector function function is defined as vertical stacking of element-wise derivatives
Jacobian convention¶
Under gradient convention the derivative of scalar-valued vector function function is defined as horizontal stacking of element-wise derivatives
For a vector-value vector function , Jacobian of is the vertical concatentation of gradients transposed, resulting in matrix
We will use Jacobian convention in this course, because it works nicely with chain rule.
Derivative of a linear function¶
All scalar-valued linear functions of can be written in the form .
Derivative of a quadratic function¶
All scalar-valued homogeneous quadratic functions of can be written in the form .
Homework (Linear Models): Problem 5¶
(10 marks)
Proof of above two derivatives is left as an exercises.
Back to Least square regression¶
This gives us the solution
The symbol is called inverse of matrix .
The term is also called the pseudo-inverse of a matrix , denoted as .
n = salt_concentration_data.shape[0]
bfx = salt_concentration_data[:, 2:3]
bfy = salt_concentration_data[:, 1]
bfX = np.hstack((bfx, np.ones((bfx.shape[0], 1))))
bfXarray([[0.19, 1. ],
[0.15, 1. ],
[0.57, 1. ],
[0.4 , 1. ],
[0.7 , 1. ],
[0.67, 1. ],
[0.63, 1. ],
[0.47, 1. ],
[0.75, 1. ],
[0.6 , 1. ],
[0.78, 1. ],
[0.81, 1. ],
[0.78, 1. ],
[0.69, 1. ],
[1.3 , 1. ],
[1.05, 1. ],
[1.52, 1. ],
[1.06, 1. ],
[1.74, 1. ],
[1.62, 1. ]])bfm = np.linalg.inv(bfX.T @ bfX) @ bfX.T @ bfy
print("Method 1:", bfm)
bfm = np.linalg.solve(bfX.T @ bfX, bfX.T @ bfy)
print("Method 2:", bfm)
bfm, *_ = np.linalg.lstsq(bfX, bfy, rcond=None)
print("Method 3:", bfm)Method 1: [17.5466671 2.67654631]
Method 2: [17.5466671 2.67654631]
Method 3: [17.5466671 2.67654631]
m = bfm.flatten()[0]
c = bfm.flatten()[1]
# Plot the points
fig, ax = plt.subplots()
ax.scatter(salt_concentration_data[:, 2], salt_concentration_data[:, 1])
ax.set_xlabel(r"Roadway area $\%$")
ax.set_ylabel(r"Salt concentration (mg/L)")
x = salt_concentration_data[:, 2]
y = m * x + c
# Plot the points
ax.plot(x, y, 'r-') # the line