奇異值分解
只有方陣(行數等於列數)才能做特徵值分解,非方陣可不可以分解爲 個矩陣的乘積呢?
這種方式是【奇異值分解】,這種方法大學裏並不學。
因爲本科的線性代數主要研究方陣(除了線性系統),所以大學裏並沒有介紹非方陣的奇異值分解(),奇異值分解在數據降維、語義分析、圖像等領域都有十分廣泛的應用,比如 算法裏如果用數據矩陣的奇異值分解代替協方差矩陣的特徵值分解,速度更快。
舉個荔枝,演示一下非方陣的分解步驟。
-
-
求出矩陣 和 :
發現 和 都是實對稱矩陣,必然可以做特徵值分解。
-
因此,分別求出 和 的特徵值和特徵向量:
有倆組,
有三組,
-
將 的特徵向量橫向拼成矩陣 :
是正交矩陣,因爲TA的列向量倆倆正交,且都是單位向量。
-
將 和 的相同特徵值開方,拼成矩陣:
拼成矩陣:
P.S. 的擺放順序與特徵向量一致。
-
將 的特徵向量橫向拼成一個矩陣:
P.S. 也是正交矩陣,因爲TA的列向量倆倆正交,且是單位向量。
-
最後,將 發現結果等於原矩陣,說明 矩陣 可以分解爲。
-
雖然這只是一個 的矩陣的分解過程,但推廣到 的矩陣,按照這樣的步驟同樣可以分解爲三個矩陣的乘積,這種分解方式就是【奇異值分解】。
-
奇異值分解:
, 矩陣 的尺寸是
,矩陣 的尺寸是 ,其列向量稱爲 矩陣 的左奇異向量;
,矩陣 的尺寸是 ,其對角線上的值稱爲 矩陣 的奇異值;
, 矩陣 的尺寸是 ,其列向量稱爲 矩陣 的右奇異向量。
低秩近似
我們可以把矩陣看成一種變換,把矩陣乘法當成線性變換時,找出變換矩陣的特徵值和特徵向量,實際上就是找出變換矩陣的主要變換方向。
也可以說是,特徵值和特徵向量代表了一個方陣的【固有信息】。
特徵值分解是奇異值分解的特例,特徵值分解只能分解方陣,奇異值分解可以分解任意形狀的矩陣。
因此,奇異值及奇異向量可以說是代表了一個 矩陣的【固有信息】。
奇異值越大,代表的信息就越多。
另外,如果我們在奇異值矩陣 中,將奇異值從大到小排列,就會發現奇異值下降特別快。
很多情況下,前 個奇異值的和就佔了全部奇異值之和的 %。
也就是說,前 個奇異值就足以代表整個矩陣的【固有信息】!!
所以,可以用 最大的 個奇異值及對應的左右奇異向量來近似矩陣。
這種理論,就被稱爲【低秩近似】。
來看一個 的矩陣,使用【低秩近似】的實例!!
-
奇異值分解:,看中間的矩陣: 。
發現奇異值有 個,分別是 ,我們只用前面倆個奇異值來算一下,佔總奇異值的比例:
- %
這個比例很大了,所以我們可以認爲前面倆個奇異值,及其對應的左右奇異向量足以代表原來的矩陣。把這些部分截取下來:
將截取下來的矩陣相乘,就低秩近似原來的數據矩陣,對比倆個矩陣可以發現,倆者數值非常接近。
- 原始矩陣: 近似矩陣:
工程應用:圖像壓縮
基於奇異值分解的【低秩近似】理論在工程中有廣泛的應用,比如圖像壓縮。
圖像本來就是一個矩陣,比如下面的圖片:
假設這張圖片的尺寸是 ,就需要 個字節來存儲。
就這樣一張不大的灰度圖,都要將近 萬字節()存儲,要是某個應用是圖片爲主的,那您可以想象應用會有多大。
圖像壓縮主要有倆個好處:
- 存儲空間會小很多
- 方便網絡傳輸
我們可以用 奇異值分解 來壓縮圖像,算法就是【低秩近似】理論。
圖像壓縮有倆部分:
-
壓縮圖像
對 圖像矩陣做奇異值分解,得到
選取前 大的奇異值(),按照【低秩近似】理論,對 做截取,得到存儲或者傳輸新的 ,新的矩陣不一定比原來的小,一定要選取一個恰當的
-
圖像重構
將 按順序乘起來,圖像的主要信息就可以表示出來啦。但
完整代碼:
import cv2
# cv 庫用來讀取圖片
import numpy as np
import matplotlib.pyplot as plt
# plt 庫顯示圖片
''' @para: c 是保留奇異值(奇異向量)個數佔總個數比例 '''
def imgCompress(c, img):
# 1. 圖像壓縮(SVD), 返回的是分解後的 3 個矩陣
U, sigma, VT = np.linalg.svd(img)
k = int(c * img.shape[1]) # .shape[1] 是列數,也是奇異值的個數
sig = np.eye(k) * sigma[ :k] # sigma矩陣截取,構造新的奇異值矩陣,也是一個對角矩陣
# 2. 圖像重構
res_img = (U[:, :k] * sig) * VT[:k, :] # U、VT矩陣截取,並相乘
size = U.shape[0] * k + sig.shape[0] * sig.shape[1] + k * VT.shape[1]
# 壓縮後的數據量 = 截取後的(U的大小 + sigma的大小 + VT的大小)
return res_img, size;
if __name__ == '__main__':
# 1.讀取待壓縮的圖像
img_path = input("圖片路徑:> ")
ori_img = np.mat(cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)) # cv2.IMREAD_GRAYSCALE:以灰度圖方式讀取
# 2.圖像壓縮(含重構)
res_img, size = imgCompress(0.1, ori_img) # 0.1 只保留前 10% 的奇異值,比例越小,壓縮的越厲害,重構的圖片就越模糊
print("壓縮前圖像大小:> ", str(ori_img.shape[0] * ori_img.shape[1]))
print("壓縮後圖像大小:> ", str(size))
# 顯示圖像(對比)
fig, ax = plt.subplots(1, 2)
ax[0].imshow(ori_img, cmap='gray')
ax[0].set_title("before compress")
ax[1].imshow(res_img, cmap='gray')
ax[1].set_title("after compress")
plt.show()
運行結果:
-
壓縮前圖像大小:> 50292
-
壓縮後圖像大小:> 8949
哈哈,整整壓縮了一個量級()。
顯示圖片:
工程應用:推薦系統
您看,我正在看電影,右邊會有一個推薦列表。
TA這個是根據什麼推薦呢?
可能是我根據的觀看記錄,這裏我們以評分爲判斷依據吧,簡單起見。
上圖一共 位大佬,一共有 部電影,電影評分是 , 表示未評分或者未看過。
現在【馮八】大佬又來看電影了,我們應該推薦什麼給【馮八】呢?
因爲每一部電影都有分類的,我們可以在一個類別裏面給【馮八】挑:
- 科幻:變形金剛、鋼鐵俠、流浪地球
- 喜劇:喜劇之王、功夫、少林足球
- 賭博:賭俠、賭神、賭聖
【馮八】給賭博片的打分普遍很高(賭聖5分、賭神4分),所以應該推薦賭博片裏沒看過的賭俠。
不過計算機理解不了這個影片分類,所以我們可以使用降維,使得數據投影變成 維,就有了科幻、喜劇、賭博的分類。
介紹一下,推薦系統的流程:
- 前置準備,把所有用戶的評分數據放入到一個矩陣裏。
- 現在爲【馮八】推送服務,尋找【馮八】未打分的電影 — 在這個矩陣的第九行裏尋找等於 的元素。
-
預測【馮八】會給那些未打分的電影,打多少分。
科幻類(黃色)打 分,喜劇類(藍色)打 分,賭博片(紅色)打 分。這裏的分類,其實是降維操作 — 使用 算法將高維投影低維。 算法可參考《特徵值分解實驗:人臉識別與PageRank網頁排序》。不過,這裏面的 算法採用的是特徵值分解(EVD),這篇文章是奇異值分解(SVD),所以我們還是用奇異值分解包裝的 算法。
奇異向量也可以構造 降維矩陣,因爲我們現在是對行降維,協方差矩陣維是 ,做奇異值分解時,左奇異矩陣 就是矩陣 的特徵向量拼接而來的 — 所以說,奇異值分解的過程中,本身就包含了特徵值分解。用特徵值來構造降維矩陣,和用奇異向量構造降維矩陣其實是一回事,只是書寫方式不同。
所以,我們使用 來構造降維矩陣,那降維矩陣就是從 左奇異矩陣 中截取:。
因爲 左奇異矩陣 是列向量橫向堆疊而成的,所以要轉置一下。而後將 數據矩陣 投影到 所代表的低維空間裏,得到矩陣。
如果是對行降維,協方差矩陣維是 ,降維矩陣從左奇異矩陣 中截取:。
如果是對列降維,協方差矩陣維是 ,降維矩陣從右奇異矩陣 中截取:。
在低維空間中,計算出待預測電影與其他電影的相似度。計算相似度,採用相似度算法接口,可參考《向量實驗:相似度算法》。
而後,逐一將已評分電影的分數 相似度,而後求和 — 把相似度當成權重,得到預測分數
-
根據預測評分的大小排序,就前 個電影給用戶。
完整代碼:
import numpy as np
def load_dataSet(): # “用戶-電影”矩陣 ,行表示用戶的評分 ,列表示電影
return np.mat([ [ 5, 4, 5, 4, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 5, 4, 1, 4, 0],
[0, 0, 0, 0, 0, 0, 0, 5, 4],
[3, 3, 5, 0, 5, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 0, 0, 0],
[1, 2, 3, 0, 0, 0, 0, 0, 0],
[2, 0, 0, 5, 4, 5, 0, 0, 0],
[0, 0, 5, 0, 0, 0, 0, 1, 0],
[1, 0, 2, 0, 1, 0, 0, 4, 5],
[0, 5, 0, 0, 0, 0, 0, 1, 0],
[4, 4, 4, 0, 0, 0, 0, 1, 2]])
def cosSim(inA, inB):
return 0.5 + 0.5 * (float(inA.T * inB) / (np.linalg.norm(inA) * np.linalg.norm(inB))) # 歸一化到[0,1],配合分數的歸一化
def scorePredict(dataMat, xformedItems, user_id, unrated_idx):
rateTotal = 0.0 # 預測分數
simTotal = 0.0 # 總相似度(權重)
n = dataMat.shape[1] # 獲取電影個數
for i in range(n): # 遍歷所有電影
userRating = dataMat[user_id, i] # 針對該用戶,拿到一個電影得分 [1, 0, 2, 0, 1, 0, 0, 4, 5],
if userRating == 0 : # 跳過未評分項
continue
similarity = cosSim(xformedItems[:, unrated_idx], xformedItems[:, i]) # 求餘弦相似度
print( 'the movie_%d and movie_%d similarity is: %f' % (unrated_idx, i, similarity))
rateTotal += similarity * userRating # 預測分數 = 相似度 * 已評分數
simTotal += similarity # 相似度求和
return rateTotal / simTotal # 評分歸一化:使得評分值在0-5之間
def recommed(dataMat,user_id,N=3):
# 1. 找出該用戶未評分電影
unratedItems = np.nonzero(dataMat[user_id, :]==0)[1] # “==0”操作將0置爲1,將非0置爲0 [0, 1, 0, 1, 0, 1, 1, 0, 0]
print("-------- The user -------\n",np.around(dataMat[user_id, :], decimals=3))
print("-------- unratedItems -------\n",np.around(unratedItems , decimals=3))
# 2.預測評分
# 2.1. 降維(提取電影主題)
U, Sigma, VT = np.linalg.svd(dataMat)
# U*U.T = E ?若爲E證明U爲正交矩陣,其列向量已經單位正交化,就不用像EVD降維那樣,還要自己單位化
print("----- U*U.T = E ? -----\n",np.around(U*U.T, decimals=0))
# 2.2 自動收縮最適合的k
k = 0
for i in range(len(Sigma)):
if (np.linalg.norm(Sigma[:i + 1]) / np.linalg.norm(Sigma)) > 0.9:
k = i + 1
break #剛好找到滿足條件的k,退出循環
# 2.3 截取U,得到降維矩陣
red_U = U[:, :k]
# 2.4 降維
xformedItems = red_U.T * dataMat
print("xformedItems shape:",xformedItems.shape) # (3, 9)
print("----- xformedItems -----\n",np.around(xformedItems, decimals=2))
# 2.5 對未評分電影逐一進行分數預測
movScores = [] # 存儲預測到的分數
for unrated_idx in unratedItems: # 遍歷所有未評分項的索引,逐項預測
print ("-------- predict movie_%d -------" % (unrated_idx))
score = scorePredict(dataMat, xformedItems, user_id, unrated_idx) # 預測當前未評分項的分數
movScores.append((unrated_idx, score)) # 以元組方式堆疊到movScores
print("-------- movScores -------\n",np.around(movScores, decimals=3))
# 3.按照預測分數從大到小排序,並返回前N大分數對應的電影
return sorted(movScores, key=lambda tmp: tmp[1], reverse=True)[:N]
if __name__ == "__main__":
# 1.加載數據集
dataMat = load_dataSet()
print("dataMat shape:",dataMat.shape)
print("-------- dataMat -------\n",np.around(dataMat, decimals=3))
# 2.輸入一個用戶編號,給他推薦N部電影
user_id = 8
N = 4
recommed_items = recommed(dataMat,user_id,N)
print("---the recommendation of our system for user_%d are as follows---"%(user_id))
print(np.around(recommed_items, decimals=3))
print("done!!!!")
就我們設計的推薦系統,可能面臨的一些問題:
-
可能會有上億用戶,數據矩陣規模很大,矩陣分解會很耗時間;
解決:因爲這個矩陣在一段時間之內變換不大,所以一般一天計算一次就好。
-
電影有成千上萬部,需要多次計算相似度,也很耗時間;
解決:提前計算各個電影直接的相似度,需要的時候調用即可,不用計算。
-
很多用戶都沒有給電影打分的習慣,所以矩陣爆,會影響推薦效果。
解決:胡歌,請您來打分~