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.

Dense Linear Algebra in NumPy and SciPy

Open in Colab

Linear Algebra

Linear Algebra is the fundamental building block in scientific computing. Why? Even if we are dealing with complicated functions, we can always deal with approximations. If we want to understand a function near a point (sample), the simplest approximation is the constant function, which says the function is the same everywhere. This isn’t very interesting. The next level of approximation is a linear approximation, where we model the rate of change near that point. This is much more useful, even if it isn’t exact, and opens up the possibility of saying all kinds of things about the system that we’re studying.

In other words, you can do a lot with degree-1 Taylor series.

Let’s first recall some definitions from your mathematics classes:

A vector space VV over a field F\mathbb{F} consists of a set of vectors which are related by addition and scalar multiplication

  1. v,w∈Vv, w\in V implies v+w∈Vv + w \in V (addition)

  2. v∈V,α∈Fv \in V, \alpha \in \mathbb{F} implies αv∈V\alpha v \in V

These operations are associative and commutative, etc. Usually what makes things interesting is the existence of a linear map.

A linear map M:V→WM: V\to W is a map from one vector space (VV) to another (WW, over the same field F\mathbb{F}) which satisfies M(αv+βw)=αMv+βMwM(\alpha v + \beta w) = \alpha M v + \beta M w

Matrices encode a linear map when we have chosen bases for both VV and WW.

In scientific computing the field F\mathbb{F} is typically either the real numbers R\mathbb{R} or complex numbers C\mathbb{C} (but remember we use floating point arithmetic)

We will also deal with finite dimensional vector spaces. Sometimes these are explicitly vectors in Euclidean space (e.g. in computational geometry), but more often are some sort of function space.

Householder Notation

Householder notations is standard in numerical linear algebra:

  • scalars (elements of F\mathbb{F}) are denoted with lower-case Greek letters α,β,…\alpha, \beta, \dots

  • vectors are denoted with lower-case Latin letters a,b,…,w,v,…a, b, \dots, w, v, \dots

  • matrices are denoted with capital letters (Latin or Greek), such as A,B,…A, B, \dots

This was introduced by Householder (see his book “Theory of Matrices in Numerical Analysis”, or the modern text “Matrix Computations” by Golub and Van Loan).

What Do we Compute?

  • basic operations - e.g. compute yy where y=Axy = Ax

  • solving linear systems - find xx where Ax=bAx = b.

  • matrix analysis - understand the action of the matrix AA via eigenvalues, SVD, etc.

Some Places where Linear Algebra Appears

  1. Linear regression (example of linear system)

  2. Principal components analysis (example of matrix analysis)

  3. Solving ODE/PDE (depending on details, involves basic operations and solving linear systems)

  4. Data Visualization (often use matrix analysis)

  5. Optimization (gradient descent uses basic operations, Newton’s method solves a linear system)

  6. Imaging - (basic operations, solving systems, some matrix analysis)

And the list goes on.

Example: Diffusion

One example where linear algebra appears is in measuring the strength of a signal at several sources from receivers. Consider a simplified problem where we have several points that emit some pollutant. We aren’t able to measure this pollutant directly at the sources. This can happen for a variety of reasons, such as:

  1. The pollutant is completely inaccessible (e.g. far underwater)

  2. We’re not able to get close for political reasons (people responsible won’t cooperate)

  3. It is much cheaper to measure from other locations where we already have equipment

And so on. This problem is related to a variety of other problems in modeling heat or electromagnetic or acoustic signals, and is useful in imaging/remote sensing. We’ll denote the source locations with xx, and the receiver locations with yy. We’ll put these in a 2-dimensional plane

<Figure size 640x480 with 1 Axes>

Each source emits the pollutant at rate ff. We represent this with a vector, where f[i] is the pollutant emitted by source i.

array([0.56804456, 0.92559664, 0.07103606])

We’ll assume the pollutant spreads via diffusion. What follows next might look familiar if you have taken some Physics classes before. If not, don’t worry - the basic idea is that pollution spreads out evenly from each source. As we get further away from the source, the pollution becomes less concentrated because it is spread out over a larger area. The function that tells us the concentration of a unit of pollution emitted by the source some distance away is called the Greens function.

Let p(x)p(x) denote the strength of the pollutant as a function in the 2-dimensional plane. This means that diffusion obeys the PDE

Δp=f,Δ:=∂2∂x12+∂2∂x22,f=∑ifiδxi,\Delta p = f,\quad \Delta := \frac{\partial^2}{\partial x_1^2} + \frac{\partial^2}{\partial x_2^2}, \quad f = \sum_i f_i \delta_{x_i},

where Δ\Delta is the laplacian, and δ\delta is a dirac delta. We’re just going to measure the pollutant at the points yy, where the receivers are located. The amount of pollutant at receiver jj from source ii is

−fi2πlog⁡∥xi−yj∥2- \frac{f_i}{2\pi} \log \| x_i - y_j\|_2

(this is the Green’s function for Δ\Delta). Because the total amount of pollution at each reciever is the sum of the pollution from each source measured at that receiver, we can write

p=Gfp = G f

where p[j] is the pollution at y[j], f[i] is the rate of pollution at source x[i], and G[j,i] is −12πlog⁡∥xi−yj∥2-\frac{1}{2\pi} \log \|x_i - y_j\|_2

array([[0.2493779 , 0.15224308, 0.22302595], [0.10068884, 0.14768804, 0.08119219], [0.18840228, 0.26461807, 0.15141249]])

Now, we can calculate the concentration of pollution at the receivers using matrix-vector multipication

array([0.29841632, 0.19966288, 0.36270623])

However, the point is that we want to solve the inverse problem, which is to determine how much each source is polluting from our measurements at the receivers. In words, we want to find ff so that Gf=pGf = p for some known pp.

If you were to write down the solution, you would find f=G−1pf = G^{-1} p. However, you should never explicilty invert a matrix on a computer, because you often run into inaccuracy due to floating point representation error. For now, we’ll use np.linalg.solve

np.float64(3.416972895534106e-14)
array([0., 0., 0.])

We see that the vector we solved for (f2) is very close to the true solution of f, and is close to machine precision.

We are able to accurately obtain the rates at which the sources release pollution based on measurements from some randomly placed sensors.

Linear Algebra In NumPy

Basic Operations

Vectors and matrices in NumPy are represented using np.ndarray, which we have seen already. Here is a review:

array([1.68700745, 1.77035189, 3.20616645])
array([ 0.98960871, -0.02812151, -3.75157541, 3.47067757])
array([ 0.98960871, -0.02812151, -3.75157541, 3.47067757])
array([ 0.00000000e+00, -1.70002901e-16, 0.00000000e+00, -8.88178420e-16])
array([-1.24900090e-16, 0.00000000e+00, 2.22044605e-16])

Solving Linear Systems

Recall from our example that for numerical reasons, you should never invert a matrix to solve a linear system. Let’s see an example where we solve A @ x = b for x

0.022307872772216797 sec.
np.float64(4.1651748070961266e-11)
0.011479854583740234 sec.
np.float64(4.264671197622496e-12)

We see that you will get a more precise answer, and faster when using solve instead of a matrix inverse.

If we want to do a least squares solve: min⁡x∥Ax−b∥\min_x \|Ax - b\|

We can use lstsq. For full-rank (square) matrices, this is equivalent to solve.

np.float64(6.964575844777663e-12)

lstsq can also be used with over-and under determined linear systems

np.float64(5.229269775919361e-14)

in the above case, we are able to solve the system exactly because b in in the image of A

np.float64(12.784556981877792)

in the above case, there are many possiblities for x where A*x = b, so we find the smallest-norm solution

SciPy Linear Algebra

We’re now going to switch gears and start using scipy.linalg instead of numpy.linalg. From the user’s point of view, there isn’t really any difference, except scipy.linalg has all the same functions as numpy.linalg as well as additional functions. The call signatures are essentially the same, but there are sometimes different implementations under the hood.

SciPy is built on top of the NumPy ndarray data type, so it is fully interoperable. For example:

np.float64(0.0)

We’ll just import scipy.linalg into the la namespace, replacing numpy.linalg

Matrix Factorizations

A matrix factorization or matrix decomposition writes a matrix AA as the product of matrices A=BCD…A = BCD\dots, where the matrices in the product typically have some special structure.

Here are some examples:

  • Diagonal matrices - easy to apply and solve linear systems

  • Triangular matrices (upper or lower) - fast to solve linear systems

  • Orthonormal matrices: QQ orthogonal means QT=Q†Q^T = Q^{\dagger} (pseudoinverse)

  • Permutation matrices: sparse orthonormal matrices

Most of the matrix factorizations we will see run in O(n3)O(n^3) time for a n×nn\times n matrix AA, or O(min⁡(m,n)2max⁡(m,n))O(\min(m,n)^2 \max(m,n)) for a m×nm\times n matrix AA.

If you want to get really in-depth into how to compute matrix factorizations, take a numerical linear algebra course. We’ll get them treating scipy as a black box (meaning we don’t look inside).

LU decomposition

The first type of factorization we’ll look at is a LULU decomposition, where LL is lower-triangular and UU is upper triangular. For numerical stability, this is often computed with a pivoting strategy, which means there is also row or column permutation matrix PP.

A=PLUA = PLU
np.float64(2.7375882280132696e-12)

The nice thing about triangular matrices is that they can solve linear systems in O(n2)O(n^2) time, instead of O(n3)O(n^3) time for general matrices, using the forward or backward substitution algorithms. There is a special function solve_triangular for this reason:

4.610892733204046e-13

We can replicate the functionality of solve using an LU factorization, using the expression

A−1=U−1L−1PTA^{-1} = U^{-1} L^{-1} P^T
5.545337890205843e-11

Exercise

  1. Use the time module to perform an experiment to verify that solve_triangular takes O(n2)O(n^2) time. Hint: take LU decompositions of random matrices to get triangular factors of different sizes.

Notebook Cell
<Figure size 640x480 with 1 Axes>
  1. Suppose AA is n×nn\times n, so the LULU factorization above takes O(n3)O(n^3) time. What is a big-O expression for the time to run my_solve on AA?

Notebook Cell

The big-O expression for the time to run my_solve on A is O(n^3) + O(n^2). LU factorization takes O(n^3) and each inverse of a triangular matrix takes O(n^2), but two triangular matrices are still O(n^2), and then we sum them up since there is an order performing the algorithm not composed.

QR decomposition

The QRQR decomposition, A=QRA = QR, contains a matrix QQ with orthonormal columns, and an upper triangular matrix RR. For stability reasons, column pivoting is often used which means there is often a permutation matrix PP and A=QRPA = QRP.

np.float64(8.243087056689126e-13)
((1000, 500), (500, 500))
np.float64(8.002355261400986e-13)

The QR factorization is used for least-squares solutions, because Q @ Q.T projects a vector onto the subspace spanned by the matrix A

5.244247957288521e-14

Excercise

Suppose AA is a n×nn\times n matrix, and that the QRQR factorization takes O(n3)O(n^3) time. What is the time to run my_lstsq using AA in big-O notation?

The computational complexity of QR factorization is O(n3)O(n^3), and the matirx-vector multiplication "QTb Q^{T} b" costs O(n2) O (n^2). Then solving Rx=QTb Rx = Q^{T}b has the cost of solving a triangular system O(n2) O (n^2) . Thus, the overall computational complexity of my_lstsq is (O(n3)+O(n2)O(n^3) + O (n^2)) or just O(n3) O(n^3) (because we usually omit the lower orders).

Eigenvalue Decompositions

A vector xx is an eigenvector of AA with eigenvalue λ\lambda if Ax=xλAx = x \lambda. An eigenvalue decomposition is a decomposition A=XΛX−1A = X \Lambda X^{-1} where Λ\Lambda is a diagonal matrix. We can compute such a decomposition using eig:

columns of X are eigenvectors, and eigenvalues are diagonal entries of Lam

3.8400462682364843e-13

When A is symmetric (or hermitian), there exists and orthonormal basis where every basis element is an eigenvector. In this case, we can write A=UΛUHA = U\Lambda U^H. There is a special function eigh for such a situation.

3.2993189238553773e-13
np.float64(2.857495078064133e-12)

Computing eigenvector decompositions takes O(n3)O(n^3) time for a n×nn\times n matrix.

Exercises

  1. Both eig and eigh take O(n3)O(n^3) time. Which is faster in practice on a symmetric matrix? Do they produce the same answer on the same symmetric matrix?

As shown in the figure, eigh is much faster than eig in practice. We checked the eigenvalues produced by both methods (eigenvalues of eigh are sorted), they produced the same answer.

  1. Write a function that solves a linear system with a symmetric matrix using the Eigenvalue decomposition. Is this function faster or slower than using LU in my_solve?

As shown in the figure, my_solve is faster than eigh in practice.

Notebook Cell
Notebook Cell
<Figure size 640x480 with 1 Axes>
np.float64(1.5774048733874224e-12)
Notebook Cell
Notebook Cell
<Figure size 640x480 with 1 Axes>

SVD

The singular value decomposition is an extremely useful practical and theoretical tool. We can decompose a m×nm\times n matrix AA as A=UΣVTA = U \Sigma V^T, where UU is a m×mm \times m matrix with orthonormal columns (called left singular vectors), VV is a n×nn\times n matrix with orthonormal columns (called right singular vectors), and Σ\Sigma is a diagonal matrix with positive entries decreasing in magnitude (called singular values).

The top singular value solves the variational problem σ0=max⁡uTAv\sigma_0 = \max u^T A v subject to ∥u∥2=1,∥v∥2=1\|u\|_2 = 1, \|v\|_2=1, and describes the direction in which AA induces the largest change in maginitude in a vector. The next singular value is defined similarly on the subspaces orthogonal to uu and vv, and so on.

One way to visualize the action of a matrix is seeing how it maps the unit sphere. The image is an ellipsoid, and the right singular vectors give the directions of the axes, and the singular values give the lengths of these axes.

<Figure size 1000x1000 with 1 Axes>

Exercises

  1. Computing the SVD takes O(n3)O(n^3) time for a n×nn\times n matrix, just like all the other matrix factorizations we’ve seen. Make plots of the runtimes of svd, eigh, eig, qr and lu as we increase nn. Which is fastest in practice?

From the plot, we see that from slowest to fastest is: eig, svd, eigh, qr, lu. lu is fastest in practice.

  1. Write a function that solves a linear system using the Singular Value Decomposition.

  2. What is the SVD of a Hermitian (symmetric matrix)? Is svd or eigh faster?

From the last figure, we can see that eigh is faster on a Hermitian.

Notebook Cell
Notebook Cell
<Figure size 640x480 with 1 Axes>
Notebook Cell
Notebook Cell
Notebook Cell
<Figure size 640x480 with 1 Axes>