天天看點

如何讓奇異值分解(SVD)變得不“奇異”?

在之前的一篇文章: 劃重點!通俗解釋協方差與相關系數

,紅色石頭為大家通俗化地講解了協方差是如何定義的,以及如何直覺了解協方差,并且比較了協方差與相關系數的關系。

本文紅色石頭将繼續使用白話語言,介紹機器學習中應用十分廣泛的矩陣分解方法:奇異值分解(SVD)。本文不注重詳細的數學推導,隻注重感性的了解以及如何在實際應用中使用它們。

1 普通方陣的矩陣分解(EVD)

我們知道如果一個矩陣 A 是方陣,即行列次元相同(mxm),一般來說可以對 A 進行特征分解:

如何讓奇異值分解(SVD)變得不“奇異”?

其中,U 的列向量是 A 的特征向量,Λ 是對角矩陣,Λ 對角元素是對應特征向量的特征值。

舉個簡單的例子,例如方陣 A 為:

如何讓奇異值分解(SVD)變得不“奇異”?
那麼對其進行特征分解,相應的 Python 代碼為:

import numpy as np
A = np.array([[2,2],[1,2]])
lamda,U=np.linalg.eig(A)
print('方陣 A: ',A)
print('特征值 lamda: ',lamda)
print('特征向量 U: ',U)      

運作輸出:

方陣 A: [[2 2]

   [1 2]]

特征值 lamda: [ 3.41421356 0.58578644]

特征向量 U: [[ 0.81649658 -0.81649658]

   [ 0.57735027 0.57735027]]

特征分解就是把 A 拆分,如下所示:

如何讓奇異值分解(SVD)變得不“奇異”?

其中,特征值 λ1=3.41421356,對應的特征向量 u1=[0.81649658 0.57735027];特征值 λ2=0.58578644,對應的特征向量 u2=[-0.81649658 0.57735027],特征向量均為列向量。

值得注意的是,特征向量都是機關矩陣,互相之間是線性無關的,但是并不正交。得出的結論是對于任意方陣,不同特征值對應的特征向量必然線性無關,但是不一定正交。

02 對稱矩陣的矩陣分解(EVD)

如何讓奇異值分解(SVD)變得不“奇異”?
A = np.array([[2,1],[1,1]])
lamda,U=np.linalg.eig(A)
print('方陣 A: ',A)
print('特征值 lamda: ',lamda)
print('特征向量 U: ',U)      

方陣 A:  [[2 1]

 [1 1]]

特征值 lamda:  [ 2.61803399  0.38196601]

特征向量 U:  [[ 0.85065081 -0.52573111]

 [ 0.52573111  0.85065081]]

如何讓奇異值分解(SVD)變得不“奇異”?

其中,特征值 λ1=2.61803399,對應的特征向量 u1=[0.85065081 0.52573111];特征值 λ2=0.38196601,對應的特征向量 u2=[-0.52573111 0.85065081],特征向量均為列向量。

注意,我們發現對陣矩陣的分解和非對稱矩陣的分解除了公式不同之外,特征向量也有不同的特性。對稱矩陣的不同特征值對應的特征向量不僅線性無關,而且是互相正交的。什麼是正交呢?就是特征向量内積為零。驗證如下:

0.85065081 * -0.52573111 + 0.52573111 * 0.85065081 = 0\

如何讓奇異值分解(SVD)變得不“奇異”?

3 奇異值分解(SVD)

我們發現,在矩陣分解裡的 A 是方陣或者是對稱矩陣,行列次元都是相同的。但是實際應用中,很多矩陣都是非方陣、非對稱的。那麼如何對這類矩陣進行分解呢?是以,我們就引入了針對次元為 mxn 矩陣的分解方法,稱之為奇異值分解(Singular Value Decomposition)。

假設矩陣 A 的次元為 mxn,雖然 A 不是方陣,但是下面的矩陣卻是方陣,且次元分别為 mxm、nxn。

如何讓奇異值分解(SVD)變得不“奇異”?

是以,我們就可以分别對上面的方陣進行分解:

如何讓奇異值分解(SVD)變得不“奇異”?

其中,Λ1 和 Λ2 是對焦矩陣,且對角線上非零元素均相同,即兩個方陣具有相同的非零特征值,特征值令為 σ1, σ2, ... , σk。值得注意的是,k<=m 且 k<=n。

根據 σ1, σ2, ... , σk 就可以得到矩陣 A 的特征值為:

如何讓奇異值分解(SVD)變得不“奇異”?

其中,P 稱為左奇異矩陣,次元是 mxm,Q 稱為右奇異矩陣,次元是 nxn。Λ 并不是方陣,其次元為 mxn,Λ 對角線上的非零元素就是 A 的特征值 λ1, λ2, ... , λk。圖形化表示奇異值分解如下圖所示:

如何讓奇異值分解(SVD)變得不“奇異”?
如何讓奇異值分解(SVD)變得不“奇異”?
如何讓奇異值分解(SVD)變得不“奇異”?

4 如何形象化了解 SVD

奇異值分解到底有什麼用呢?如何形象化地了解奇異值?我們一起來看下面的例子。

首先放上男神的照片:

如何讓奇異值分解(SVD)變得不“奇異”?
如何讓奇異值分解(SVD)變得不“奇異”?
如何讓奇異值分解(SVD)變得不“奇異”?
如何讓奇異值分解(SVD)變得不“奇異”?
如何讓奇異值分解(SVD)變得不“奇異”?
如何讓奇異值分解(SVD)變得不“奇異”?

可見,取前 50 個最大奇異值來重構圖像時,已經非常清晰了。我們得到和原圖差别不大的圖像。也就是說,随着選擇的奇異值的增加,重構的圖像越來越接近原圖像。

基于這個原理,奇異值分解可以用來進行圖檔壓縮。例如在本例中,原始圖檔的次元是 870x870,總共需要儲存的像素值是:870x870=756900。若使用 SVD,取前 50 個最大的奇異值即可,則總共需要存儲的元素個數為:

(870+1+870)x50=87050

顯然,所需存儲量大大減小了。在需要存儲許多高清圖檔,而存儲空間有限的情況下,就可以利用 SVD,保留奇異值最大的若幹項,舍去奇異值較小的項即可。

值得一提的是,奇異值從大到小衰減得特别快,在很多情況下,前 10% 甚至 1% 的奇異值的和就占了全部的奇異值之和的 99% 以上了。這對于資料壓縮來說是個好事。

SVD 資料壓縮的算法圖示如下:

如何讓奇異值分解(SVD)變得不“奇異”?

SVD 資料壓縮的示例代碼為:

from skimage import io
import matplotlib.pyplot as plt
from PIL import Image
img=io.imread('./ng.jpg')
m,n = img.shape
io.imshow(img)
plt.show()
P, L, Q = np.linalg.svd(img)
tmp = np.diag(L)
if m < n:
   d = np.hstack((tmp,np.zeros((m,n-m))))
else:
   d = np.vstack((tmp,np.zeros((m-n,n))))
# k = 50
img2 = P[:,:50].dot(d[:50,:50]).dot(Q[:50,:])
io.imshow(np.uint8(img2))
plt.show()
tmp = np.uint8(img2)
im = Image.fromarray(tmp)
im.save("out.jpg")      

現在,你已經完全了解了奇異值分解了吧。是不是挺簡單也挺有意思呢?