Computational Complexity: O(n3) for LU decomposition, O(n2) for forward and backward substitution.
LU Decomposition with Partial Pivoting
Pivoting (row swapping) ensures numerical stability when a pivot element is zero or very small.
Permutation matrix P such that PA=LU.
Permutation Matrix
A permutation matrix P is a square binary matrix that has exactly one entry of 1 in each row and each column and 0s elsewhere. It represents a reordering of the standard basis vectors.
Properties of Permutation Matrix:
P is obtained by permuting the rows of an identity matrix.
Orthogonal: P−1=PT
PA permutes the rows of A.
AP permutes the columns of A.
p = np.array([2,0,1])# permutation vector
P = np.eye(3)[p]# permutation matrix
b = np.random.rand(3)
A = np.random.rand(3,3)# permuted b
b[p]
P @ b
# permuted A rows
A[p,:]
P @ A
# permuted A columns
A[:, p]
A @ P
Since an overdetermined system may not have an exact solution, we can find the best approximate solution using least squares (least squares solution).
Solve Ax≈b is equivalent to solving ∣Ax−b∣≈0, which is equivalent to minimizing the residual r=b−Ax. The least squares solution minimizes the 2-norm of the residual:
minx∥r∥2=minx∥b−Ax∥2
Orthogonal Matrix Properties
QTQ=I=QQT
∣Qv∣=∣v∣ for any vector v (rotation, norm preservation).
k(Q)=1 (well-conditioned).
∣Q∣=1 (determinant).
The columns are orthonormal vectors. (perpendicular and unit length)
Algorithm
AxQRxQTQRxRx=b=b=QTb=QTb
Find Q,R such that A=QR.
Compute QTb.
Solve the upper triangular system Rx=QTb using back substitution. (U-solve, economy size)
Computational Complexity: O(mn2) for QR decomposition, O(mn) for computing QTb, and O(n2) for back substitution.
Two versions of QR factorization:
Full-size QR factorization: Q is square (and big), R is the same shape as A.
Economic QR factorization: Q is the same shape as A, R is square (and small).
defQRsolve(A, b):
Q, R = scipy.linalg.qr(A, mode='economic')# A = QR
b_hat = Q.T @ b # Q^T b
x = cs111.Usolve(R, b_hat)# Rx = Q^T breturn x
Methods to Compute QR Decomposition
Gram-Schmidt Process
Householder Reflections
Givens Rotations
Applications
Data Fitting
y=kx+b (linear regression).
Given data points (xi,yi), we can set up the system Ax=b where:
A=11⋮1x1x2⋮xn,x=[bk],b=y1y2⋮yn
We can then use QR decomposition to find the least squares solution for x.
Iterative Methods for Sparse Matrix
When A is large and sparse, use iterative methods to save memory and computational resources.
Error, Residual, and Condition Number
Vector 2-Norm (Euclidean): ∥x∥2=i=1∑nxi2
Matrix 2-Norm (Spectral): ∥A∥2=x=0max∣x∣∣Ax∣=∥x∥=1max∥Ax∥ (How much A stretches a vector)
Frobenius Norm: ∥A∥F=i=1∑mj=1∑naij2
Error: e=∣x−x^∣ (x is true solution, x^ is computed solution)
Relative Error: ∣x∣∣x−x^∣
Residual: r=b−Ax^
Relative Residual: ∣b∣∣b−Ax^∣
Condition Number: κ(A)=∥A∥∥A−1∥. Explain why it matters.
If κ(A) is large, the matrix is ill-conditioned, meaning small changes in b can lead to large changes in the solution x.
If κ(A) is small, the matrix is well-conditioned, and the solution x is more stable with respect to changes in b.
If κ(A) = 1, then A is perfectly conditioned.
For Ax=b, x^=x+e where Ae=r. Thus, ∥x∥∥e∥≤κ(A)∥b∥∥r∥.
where α is a step size and p is the search direction.
Algorithm
Initialize x0 to an arbitrary vector, often the zero vector.
Compute initial residual: r0=b−Ax0
Set initial search direction: p0=r0
For k=0,1,2,... until convergence
defconjugate_gradient(A, b, x0, tol=1e-10, max_iterations=1000):
x = x0.copy()
r = b - A @ x
p = r.copy()for k inrange(max_iterations):
r_dot_r = r.T @ r
Ap = A @ p
alpha = r_dot_r /(p.T @ Ap)
x_new = x + alpha * p
r_new = r - alpha * Ap
if np.linalg.norm(r_new)< tol:return x_new
beta =(r_new.T @ r_new)/ r_dot_r
p = r_new + beta * p
x = x_new
r = r_new
return x
Comparison of Iterative Methods
Method
Precondition
Convergence Rate
Memory Usage
Jacobi
Strictly diagonally dominant or SPD
Slow
Low
Gauss-Seidel
Strictly diagonally dominant or SPD
Faster than Jacobi
Low
Conjugate Gradient
Symmetric positive definite
Fast (depends on condition number)
Moderate (stores vectors)
Gradient Descent
Precondition: A is symmetric positive definite.
symmetric: A=AT
positive definite: xTAx>0 for all non-zero vectors x.
Solving Ax=b is equivalent to minimizing the quadratic function f(x)=21xTAx−bTx f is strictly convex, has a unique minimum.
Essentials: A transpose of a scalar is itself: cT=c for c∈R.
To derive the gradient ∇f(x):
f(x+v)=21(x+v)TA(x+v)−bT(x+v)=21(xTAx+xTAv+vTAx+vTAv)−bTx−bTv Since (xTAv=(xTAv)T=vTATx=vTAx,bTv=vTb)=21(xTAx+2vTAx+vTAv)−bTx−vTb=f(x)+vT(Ax−b)+21vTAv=f(x)+vT∇f(x)+21vTAvf′(x)=v→0limvf(x+v)−f(x)=v→0limvvT(Ax−b)+21vTAv=v→0limv(Ax−b)Tv+v→0limv21vTAv=(Ax−b)T
Thus, ∇f(x)=(f′(x))T=Ax−b. −∇f(x(k)) is orthogonal to −∇f(x(k+1)).
The step size αk is chosen to minimize f(x(k+1)), which means dαkdf(x(k+1))=0:
A=UΣVT where U∈Rm×m and V∈Rn×n are orthogonal matrices, and Σ∈Rm×n is a diagonal matrix with non-negative real numbers on the diagonal (singular values).
U=⋮u1⋮⋮u2⋮⋮⋮⋮⋮um⋮,
V=⋯⋯⋯⋯v1Tv2T⋯vnT⋯⋯⋯⋯,
Σ=σ10⋮00⋮00σ2⋮00⋮0⋯⋯⋱⋯⋯⋱⋯00⋮σr0⋮00⋮⋮⋮0⋮0
where σ1≥σ2≥...≥σr>0 are the singular values of A, and r=rank(A), the number of non-zero singular values.
A=i=1∑rσiuiviT (outer product expansion).
If we keep all singular values, we get the exact reconstruction of A.
If we keep only the top k(k<r) singular values, we get a rank-k approximation of A, denoted as Ak=i=1∑kσiuiviT.
The rank-k approximation Ak minimizes the Frobenius norm of the error: ∥A−Ak∥F.
U, sigma, Vt = spla.svd(A)
Asum = np.zeros( A.shape )for i inrange(len( sigma )):
Asum += sigma[i]* np.outer( U[:, i], Vt[i,:])
Theorems
AV=UΣ.
V responds a basis with n columns. AV takes the orthonormal basis vectors vi from the row space of A and maps them to the orthonormal basis vectors ui from the column space of A, scaled by the singular values σi.
Avi=σiui
Suppose an image is m×n pixels, the storage is m×n.
With first k terms in SVD, storage is mk+nk+k=k(m+n+1).
Compression ratio: k(m+n+1)mn.
img = plt.imread('image.png')# Load image
U, sigma, Vt = spla.svd(img, full_matrices=False)
k =50# Number of singular values to keep
Sigma_k = np.diag(sigma[:k])# Create diagonal matrix with top k singular values
img_approx = U[:,:k] @ Sigma_k @ Vt[:k,:]# Reconstruct the image
plt.imsave('compressed_image.png', img_approx.astype(np.uint8))# Save compressed image
Principal Component Analysis (PCA)
PCA is SVD on the covariance matrix of the data.
PCA is a dimensionality reduction technique that transforms data into a new coordinate system that helps us to reduce the number of features in a dataset while keeping the most important information.
For vectors x,y∈Rn, the Meanμ=n1i=1∑nxi. Define rx=⋯xi−μ⋯ as the deviation from the mean. Then there is the Varianceσ2=n−11i=1∑n(xi−μ)2=rxTrx, and the Covarianceσ=n−11i=1∑n(xi−μx)(yi−μy)=rxTry.
σxy>0: x and y are positively correlated, covary.
σxy<0: x and y are negatively correlated, anticovary.
σxy=0: x and y are uncorrelated.
The Data matrixD∈Rm×n, where m is the number of samples and n is the number of features.
The Mean matrixM∈Rm×n with μj=m1i=1∑mDij is the mean of feature j:
M=μ1μ1⋮μ1μ2μ2⋮μ2⋯⋯⋱⋯μnμn⋮μn
The Centered data matrix is R=D−M:
R=D11−μ1D21−μ1⋮Dm1−μ1D12−μ2D22−μ2⋮Dm2−μ2⋯⋯⋱⋯D1n−μnD2n−μn⋮Dmn−μn
The Covariance matrixC∈Rn×n with σij=Cov(i,j)=m−11k=1∑m(Dki−μi)(Dkj−μj) is the covariance between features i and j, C=m−11RTR is symmetric:
C=σ11σ12⋮σ1nσ12σ22⋮σ2n⋯⋯⋱⋯σ1nσ2n⋮σnn
SVD: C=UΣUT where U contains the eigenvectors (principal components) and Σ contains the eigenvalues (each singular value). U=V since C is symmetric. Each column vector ui represents a axis of the data.
Formula: D=M+[RU]UT. Approximation: D≈M+[RUk]UkT where Uk contains the first k columns of U. (dimension reduction)
Proof of the formula:
D−M=R=RI=R(UUT)=(RU)UT
Or by element-wise:
Theorem: v=(v⋅i)i+(v⋅j)j+(v⋅k)k where {i,j,k} form an orthonormal basis of R3.
Let rij=Dij−μj. Since {uj} of U form an orthonormal basis, ri=j=1∑n(ri⋅ujT)ujT=j=1∑n(riuj)ujT. [(riuj) is the scalar coefficient].
Therefore, R=(RU)UT.
d=m+[(d−m)u1]u1T+[(d−m)u2]u2T+...+[(d−m)un]unT for each data point d (row of D).
Matrices and Tensors
A matrix is a 2-dimensional array of numbers, while a tensor is a multi-dimensional array of numbers. Tensors generalize matrices to higher dimensions.
Application of SVD Eigenfaces
Eigenfaces are a set of eigenvectors used in the computer vision problem of human face recognition. They are derived from the covariance matrix of a set of facial images using Singular Value Decomposition (SVD).
Data matrix D∈Rm×n, where m is the number of images and n is the number of pixels in each image (flattened). For example, there are 400 images of size 64x64 pixels, then m=400 and n=4096.
For each pixel, we have the mean μj=m1i=1∑mDij and variance σj2=m−11i=1∑m(Dij−μj)2. Then we have the normal distribution of pixel values across all images: N(μj,σj2). We can sample randomly from this distribution to create new pixel values for new face images.
New faces: f=m+a1u1T+a2u2T+...+akukT where ai are random coefficients sampled from N(0,σi2), and ui are the top k eigenfaces.
what is eigenfaces?
Compute the mean face: μ=m1i=1∑mDi where Di is the i-th image vector.
Center the data: X=D−μ.
Compute the covariance matrix: C=m−11XTX.
Perform SVD on the covariance matrix: C=UΣUT.
The columns of U are the eigenfaces.
defcompute_eigenfaces(D, k):# D: data matrix of shape (m, n)# k: number of principal components to keep
m, n = D.shape
mu = np.mean(D, axis=0)# mean face
X = D - mu # center the data
C = np.cov(X, rowvar=False)# covariance matrix
U, S, Vt = np.linalg.svd(C)# SVD
eigenfaces = U[:,:k]# top k eigenfacesreturn eigenfaces, mu
defgenerate_new_face(eigenfaces, mu):
k = eigenfaces.shape[1]
coefficients = np.random.randn(k)# random coefficients
new_face = mu + eigenfaces @ coefficients # generate new facereturn new_face
Billiard Ball Simulation with Collision Detection
Conditions:
Undisturbed motion between collisions (constant velocity).
Perfectly elastic collisions (no energy loss).
No external forces (e.g., friction, gravity).
detect collisions between balls and walls, and between balls themselves.
Representation:
position vector p=[x,y], velocity vector v=[vx,vy]=[dtdx,dtdy], radius r, and mass m.
the distance between two balls is given by d=∣p1−p2∣=(x1−x2)2+(y1−y2)2.
The walls of the billiard table are represented as lines with defined boundaries.
Undisturbed Motion:
x′=x+vxdt
y′=y+vydt
Collision with Walls:
Detect collision with walls:
Left Wall: x′<xmin+r
Right Wall: x′>xmax−r
Bottom Wall: y′<ymin+r
Top Wall: y′>ymax−r
Find the time of collision:
Left Wall: dtl=vx(xmin+r)−x if vx<0
Right Wall: dtr=vx(xmax−r)−x if vx>0
Bottom Wall: dtb=vy(ymin+r)−y if vy<0
Top Wall: dtt=vy(ymax−r)−y if vy>0
Choose the smallest positive dt among these to determine the next collision time.
Adjust motion velocity:
Left or Right Wall: v′=[−vx,vy]
Bottom or Top Wall: v′=[vx,−vy]
Collision between Balls:
Detect collision between balls:
For two balls with positions p1′ and p2′, and radii r1 and r2, a collision occurs if:
∣p1′−p2′∣=(x1′−x2′)2+(y1′−y2′)2≤r1+r2