#!/usr/bin/env python # coding: utf-8 # # Singular Value Decomposition # # ## (SVD) # # [SVD](https://en.wikipedia.org/wiki/Singular_value_decomposition) is a generalization of eigenvalue decomposition of # non-square matrices. SVD decomposes (*n*⁢ x *m*) transformation matrix $\mathbf{X}$ into three simple transforms: # # * rotation ($\mathbf{V}^T$) # # * scaling ($\mathbf{\Lambda}^{1/2}$) # # * rotation ($\mathbf{U}^T$) # # SVD decomposition of X: # # $$\mathbf{X}= \mathbf{U}\mathbf{\Lambda}^{1/2}\mathbf{V}^T$$ # # visualization: # # # # ## Relation of SVD with eigendecomposition # # Any symmetric matrix $\mathbf{A}$ can be decomposed with an orthogonal matrix $\mathbf{U}$ ($\mathbf{U}^T\mathbf{U}=\mathbf{E}$, $\mathbf{E}$ identity matrix) and a diagonal matrix $\mathbf{\Lambda}$ of eigenvalues: # # $$\mathbf{A}= \mathbf{U}\mathbf{\Lambda}\mathbf{U}^T$$ # # There are two ways to cretate a symmetric matrix from an *arbitrary* (*n*⁢ x *m*) matrix $\mathbf{X}$ (one is (*m*⁢ x *m*), the other is (*n*⁢ x *n*)): $\mathbf{X}^T \mathbf{X}$ or $\mathbf{X} \mathbf{X}^T$. These *symmetric* matrices can be decomposed with *identical* eigenvalues: # # $$\mathbf{X}^T \mathbf{X}=\mathbf{V}\mathbf{\Lambda}\mathbf{V}^T=(\mathbf{V}\mathbf{\Lambda}^{1/2}\mathbf{U}^T)(\mathbf{U}\mathbf{\Lambda}^{1/2}\mathbf{V}^T)$$ # # and # # $$\mathbf{X} \mathbf{X}^T=\mathbf{U}\mathbf{\Lambda}\mathbf{U}^T=(\mathbf{U}\mathbf{\Lambda}^{1/2}\mathbf{V}^T)(\mathbf{V}\mathbf{\Lambda}^{1/2}\mathbf{U}^T)$$ # # therefore any rank *r* matrix $\mathbf{X}$ has decomposition with orthogonal matrices $\mathbf{U}$ and $\mathbf{V}$ and with a diagonal matrix $\mathbf{\Lambda}^{1/2}$ of singular values: # # $$\mathbf{X}= \mathbf{U}\mathbf{\Lambda}^{1/2}\mathbf{V}^T$$ # # where # # $$\mathbf{\Lambda}^{1/2}=\begin{bmatrix}\sqrt{\lambda_1} & \cdots & 0\\ \vdots & \ddots & \vdots \\ 0 & \cdots & \sqrt{\lambda_r}\end{bmatrix}$$ # # and *r* is *rank* of matrix $\mathbf{X}$. # # ### Example # # Let # # $$\mathbf{X} = \begin{bmatrix} 1 & 2\\ 2 & 1\\ 1 & 3 \end{bmatrix}$$ # # Find eigenvalues of matrix $\mathbf{X} \mathbf{X}^T$, then calculate $\mathbf{\Lambda}^{1/2}$, SVD decomposition of matrix $\mathbf{X}$ and eigenvectors $\mathbf{V}_1$, $\mathbf{V}_2$. Then find $\mathbf{U}$ from formula and best rank-1 approximation $\mathbf{X}_1$ of matrix $\mathbf{X}$ (further discussion of this topic follows later). # # Solution (Python, NumPy [`eig`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.linalg.eig.html) ) # In[1]: import numpy as np from numpy import linalg as LA X = np.array([[1,2],[2,1],[1,3]]) # eigendecomposition of X'.X e,V = LA.eig(np.dot(X.T,X)) Lam = np.sqrt(np.diag(e)) # singular values np.diag(Lam) U = np.dot(X,V)/np.sqrt(e) # this is the original matrix X: np.dot(U,np.dot(Lam,V.T)) # In[2]: # best rank-1 approximation of X X1 = np.sqrt(e[1])*np.outer(U[:,1],V[:,1].T) # residuals: X1 - X # SVD decomposition with the dedicated NumPy function [numpy.linalg.svd](https://docs.scipy.org/doc/numpy/reference/generated/numpy.linalg.svd.html). # In[3]: # SVD decomposition of X with dedicated function U,S,V = LA.svd(X) # singular values ordered by magnitude S # ## Two SVD variants # # # # ## Properties of SVD # # The two systems of eigenvectors are not independent: # # $$\mathbf{U}_m=\frac{1}{\sqrt{\lambda_m}}\mathbf{X}\mathbf{V}_m \;\;\;\;\;m=1,...,r.$$ # # SVD produces best rank *k* approximation of $\mathbf{X}$: # # $$\mathbf{X}_k=\sum_{m=1}^{k}\sqrt{\lambda_m}\mathbf{U}_m\mathbf{V}_m^T \;\;\;\;\;k\le r,$$ # # approximation error: # # $$\varepsilon_k^2=\sum_{m,n}\left | x(m,n)-x_k(m,n)\right |^2$$ sum of eigenvalues not considered: $$\varepsilon_k^2=\sum_{m=k+1}^r \lambda_m$$ # # ## Matrix expansion and spectrum # # *k*-term expansion of matrix $\mathbf{X}$ # # $$\mathbf{X}_k=\sum_{m=1}^{k}\sqrt{\lambda_m}\mathbf{U}_m\mathbf{V}_m^T \;\;\;\;\;k\le r,$$ # # *spectrum* of matrix is defined as the set of the singular values $\sqrt{\lambda_m}$ (*m*=1, … , *r* ). # # ## Pseudoinverse # # For any non-square matrix $\mathbf{X}$ there is a pseudoinverse $\mathbf{X}^+$: # # $$\mathbf{X}^+= \mathbf{V}\mathbf{\Lambda}^{1/2+}\mathbf{U}^T$$ # # where # # $${\mathbf{\Lambda}^{1/2+}\atop{(m,m)}} = \begin{bmatrix}\lambda_1^{-1/2} & \cdots & 0 & \cdots & 0\\ \vdots & \ddots & \vdots & & \vdots\\ 0 & \cdots & \lambda_r^{-1/2} & \cdots & 0\\ \vdots & & \vdots & \ddots & \vdots \\ 0 & \cdots & 0 & \cdots & 0\end{bmatrix}$$ . # # Along the main diagonal of matrix $\mathbf{\Lambda}^{1/2+}$ all terms with index greater than *r* are zero. # ## Solution of least squares problem with SVD # # Minimize the following error norm: # # $$\left \| \mathbf{A}\mathbf{x}-\mathbf{y} \right \|$$ # # Least squares (LSQ) solution $\mathbf{x}$ is calculated from the pseudoinverse obtained by SVD of matrix $\mathbf{A}$: # # $$\mathbf{x}= \mathbf{V}\mathbf{\Lambda}^{1/2+}\mathbf{U}^T \mathbf{y}$$ . # # ## Solution of weighted least squares problem with SVD # # Let us minimize the following error norm: # # $$ (\mathbf{A}\mathbf{x}-\mathbf{y})^T \mathbf{P} (\mathbf{A}\mathbf{x}-\mathbf{y})$$ # # Least squares (LSQ) solution $\mathbf{x}$ can be obtained with SVD decomposition of matrix $\mathbf{P}_0\mathbf{A}$ ($\mathbf{P}_0\mathbf{A}= \mathbf{U}\mathbf{\Lambda}^{1/2}\mathbf{V}^T$), from which we calculate pseudoinverse, where $\mathbf{P}_0$ is the square root of matrix $\mathbf{P}$, $\mathbf{P}=\mathbf{P}_0^T\mathbf{P}_0$: # # $$\mathbf{x}= \mathbf{V}\mathbf{\Lambda}^{1/2+}\mathbf{U}^T \mathbf{P}_0\mathbf{y}$$ . # ## Matrix approximation with SVD # # Example: SVD approximation of the geoid in Hungary # # Read geoid heights: # In[4]: geoid = np.loadtxt("geoid.txt") # grid of geoid heights, 35 rows, 70 columns # In[5]: N = np.flipud(np.reshape(geoid[:,2],(35,70))) import matplotlib.pyplot as plt cmap = plt.get_cmap('PiYG') levels=np.linspace(37,47) plt.contourf(N,levels=levels,cmap=cmap) plt.colorbar() plt.show() # calculate SVD # In[8]: U,S,V = LA.svd(N) # decreasing order of singular values print(S[0:10]) print(U.shape, V.shape) # Matrix spectrum # In[9]: # rank of matrix r = LA.matrix_rank(N) plt.semilogy(range(r),S[:r]) plt.xlabel("index") plt.ylabel("singular value") plt.show() # Best SVD approximation of degree *n*, plotted with step size `step` # In[10]: def svdapprox(U,S,V,k,n,step): dm = np.zeros((len(U),len(V))) for i in range(k,n): dm = dm + S[i]*np.outer(U.T[i],V[i]) if np.mod(i,step) == 0: cmin = np.amin(dm) cmax = np.amax(dm) # print(cmin, cmax) levels=np.arange(cmin,cmax,step=0.1) plt.contourf(dm,levels=levels,cmap=cmap) plt.title("SVD approximation rank=%d" % (i+1)) plt.colorbar() plt.show() # raw_input() return dm Na = svdapprox(U,S,V,0,11,5) # error of last approximation # In[11]: dN = N - Na cmin = np.amin(dN) cmax = np.amax(dN) print("min: %.3f max: %.3f" % (cmin, cmax)) levels=np.arange(cmin,cmax,step=0.01) plt.contourf(dN,levels=levels,cmap=cmap) plt.title("Error of SVD approximation (m)") plt.colorbar() plt.show() # ### Exercise # Calculate approximation error from the neglected singular values! # In[ ]: np.sqrt(np.sum(S[11:]**2)/(35*70-1))