本篇的數據和代碼參見:https://github.com/stonycat/ML-in-Action
一、開篇:簡述SVD應用
利用SVD實現,我們能夠用小得多的數據集來表示原始數據集。這樣做,實際上是去除了噪聲和冗餘信息。簡而言之,SVD是一種從大量數據中提取主要關鍵數據的方法。
下面介紹幾種應用場景:
1、隱性語義索引
最早的SVD應用之一就是信息檢索。我們稱利用SVD的方法爲隱性語義索引(LatentSemantic Indexing,LSI)或隱性語義分析(Latent Semantic Analysis,LSA)。在LSI中,一個矩陣是由文檔和詞語組成的。應用SVD時,構建的SVD奇異值代表了文章的主題或者主要概念。
當我們查找一個詞時,其同義詞所在的文檔可能並不會匹配上。如果我們從上千篇相似的文檔中抽取出概念,那麼同義詞就會映射爲同一概念。
2、推薦系統
簡單版本的推薦系統能夠計算項或者人之間的相似度。更先進的方法則先利用SVD從數據中構建一個主題空間,然後再在該空間下計算其相似度。
SVD是矩陣分解的一種類型,而矩陣分解是將數據矩陣分解爲多個獨立部分的過程。
二、矩陣分解
很多情況下,數據中的一小段攜帶了數據集中的大部分信息,其他信息則要麼是噪聲,要麼就是毫不相關的信息。
在線性代數中還有很多矩陣分解技術。矩陣分解可以將原始矩陣表示成新的易於處理的形式,這種新形式是兩個或多個矩陣的乘積。
不同的矩陣分解技術具有不同的性質,其中有些更適合於某個應用,有些則更適合於其他應用。最常見的一種矩陣分解技術就是SVD。公式如下:
上述分解中會構建出一個矩陣 Σ ,該矩陣只有對角元素,其他元素均爲0。另一個慣例就是,Σ 的對角元素是從大到小排列的。這些對角元素稱爲奇異值(Singular Value),它們對應了原始數據集矩陣 Data 的奇異值。奇異值和特徵值是有關係的。這裏的奇異值就是矩陣
科學和工程中,一直存在這樣一個普遍事實:在某個奇異值的數目( r 個)之後,其他的奇異值都置爲0。這就意味着數據集中僅有 r個重要特徵,而其餘特徵則都是噪聲或冗餘特徵。
三、利用 Python 實現 SVD
NumPy有一個稱爲linalg的線性代數工具箱。接下來,我們瞭解一下如何利用該工具箱實現如下矩陣的SVD處理:
Sigma爲了方便僅返回對角元素。
建立一個新文件 svdRec.py 並加入如下代碼:
def loadExData():
return [[0, 0, 0, 2, 2],
[0, 0, 0, 3, 3],
[0, 0, 0, 1, 1],
[1, 1, 1, 0, 0],
[2, 2, 2, 0, 0],
[5, 5, 5, 0, 0],
[1, 1, 1, 0, 0]]
測試:
>>> import svdRec
>>> Data=svdRec.loadExData()
>>> U,Sigma,VT=linalg.svd(Data)
>>> Sigma
array([ 9.64365076e+00, 5.29150262e+00, 8.36478329e-16,6.91811207e-17, 3.76946717e-34])
前 3 個數值比其他的值大了很多(如果你的最後兩個值的結果與這裏的結果稍有不同,也不必擔心。它們太小了,所以在不同機器上產生的結果就可能會稍有不同,但是數量級應該和這裏的結果差不多)。於是,我們就可以將最後兩個值去掉了。數據表示爲:
我們是如何知道僅需保留前 3 個奇異值的呢?確定要保留的奇異值的數目有很多啓發式的策略,其中一個典型的做法就是保留矩陣中 90% 的能量信息。我們將所有的奇異值求其平方和,將平方和累加到總值的 90% 爲止。
另一個啓發式策略就是,當矩陣上有上萬的奇異值時,那麼就保留前面的 2000 或 3000 個。儘管後一種方法不太優雅,但是在實際中更容易實施。
現在我們已經通過三個矩陣對原始矩陣進行了近似。我們可以用一個小很多的矩陣來表示一個大矩陣。有很多應用可以通過 SVD 來提升性能。下面我們將討論一個比較流行的 SVD 應用的例子 —— 推薦引擎。
四、基於協同過濾的推薦引擎
協同過濾( collaborative filtering )是通過將用戶和其他用戶的數據進行對比來實現推薦的,唯一所需要的數學方法就是相似度的計算。
1、相似度計算
利用用戶對它們的意見來計算相似度:這就是協同過濾中所使用的方法。它並不關心物品的描述屬性,而是嚴格地按照許多用戶的觀點來計算相似度。
我們希望,相似度值在 0 到 1 之間變化,並且物品對越相似,它們的相似度值也就越大。我們可以用“相似度 =1/(1+ 距離 ) ”這樣的算式來計算相似度。當距離爲 0 時,相似度爲 1.0 。如果距離真的非常大時,相似度也就趨近於 0 。
1-距離採用歐式距離來計算(計算平方和)。
2-第二種計算距離的方法是皮爾遜相關係數( Pearson correlation )。
該方法相對於歐氏距離的一個優勢在於,它對用戶評級的量級並不敏感。比如某個狂躁者對所有物品的評分都是 5 分,而另一個憂鬱者對所有物品的評分都是 1 分,皮爾遜相關係數會認爲這兩個向量是相等的。在 NumPy 中,皮爾遜相關係數的計算是由函數 corrcoef() 進行的,後面我們很快就會用到它了。皮爾遜相關係數的取值範圍從 1 到 +1 ,我們通過 0.5 + 0.5*corrcoef() 這個函數計算,並且把其取值範圍歸一化到 0 到 1 之間。
3-餘弦相似度 ( cosine similarity )
其計算的是兩個向量夾角的餘弦值。如果夾角爲 90 度,則相似度爲 0 ;如果兩個向量的方向相同,則相似度爲 1.0 。
其中
from numpy import *
from numpy import linalg as la
#相似度1:歐式距離
def ecludSim(inA,inB):
return 1.0/(1.0 + la.norm(inA - inB))
#相似度2:威爾遜距離
def pearsSim(inA,inB):
if len(inA) < 3 : return 1.0
return 0.5+0.5*corrcoef(inA, inB, rowvar = 0)[0][1]
#相似度3:餘弦
def cosSim(inA,inB):
num = float(inA.T*inB)
denom = la.norm(inA)*la.norm(inB)
#歐式距離
>>> myMat=mat(svdRec.loadExData())
>>> svdRec.ecludSim(myMat[:,0],myMat[:,4])
0.12973190755680383
>>> svdRec.ecludSim(myMat[:,0],myMat[:,0])
1.0
#威爾遜相關係數
>>> svdRec.pearsSim(myMat[:,0],myMat[:,4])
0.20596538173840329
>>> svdRec.pearsSim(myMat[:,0],myMat[:,0])
1.0
#餘弦相似度
>>> svdRec.cosSim(myMat[:,0],myMat[:,4])
0.5
>>> svdRec.cosSim(myMat[:,0],myMat[:,0])
1.0
2、基於物品的相似度還是基於用戶的相似度?
上圖:行與行之間比較的是基於用戶的相似度,列與列之間比較的則是基於物品的相似度。到底使用哪一種相似度呢?
這取決於用戶或物品的數目。
基於物品相似度計算的時間會隨物品數量的增加而增加,基於用戶的相似度計算的時間則會隨用戶數量的增加而增加。
如果用戶的數目很多,那麼我們可能傾向於使用基於物品相似度的計算方法。對於大部分產品導向的推薦引擎而言,用戶的數量往往大於物品的數量,即購買商品的用戶數會多於出售的商品種類。
3、推薦引擎的評價
如何對推薦引擎進行評價呢?此時,我們既沒有預測的目標值,也沒有用戶來調查他們對預測的滿意程度。這裏我們就可以採用前面多次使用的交叉測試的方法。具體的做法就是,我們將某些已知的評分值去掉,然後對它們進行預測,最後計算預測值和真實值之間的差異。
通常用於推薦引擎評價的指標是稱爲最小均方根誤差( Root Mean Squared Error , RMSE )的指標,它首先計算均方誤差的平均值然後取其平方根。
五、示例:餐館菜餚推薦引擎
構建一個基本的推薦引擎,它能夠尋找用戶沒有嘗過的菜餚。然後,通過 SVD 來減少特徵空間並提高推薦的效果。這之後,將程序打包並通過用戶可讀的人機界面提供給人們使用。
(1) 尋找用戶沒有評級的菜餚,即在用戶-物品矩陣中的 0 值;
(2) 在用戶沒有評級的所有物品中,對每個物品預計一個可能的評級分數。這就是說,我們
認爲用戶可能會對物品的打分(這就是相似度計算的初衷);
(3) 對這些物品的評分從高到低進行排序,返回前N個物品。
下述代碼:遍歷數據行中的每個物品。如果某個物品評分值爲 0 ,就意味着用戶沒有對該物品評分,跳過了這個物品。該循環大體上是對用戶評過分的每個物品進行遍歷,並將它和其他物品進行比較。
但是如果存在重合的物品,則基於這些重合物品計算相似度。隨後,相似度會不斷累加,每次計算時還考慮相似度和當前用戶評分的乘積。最後,通過除以所有的評分總和,對上述相似度評分的乘積進行歸一化。這就可以使得最後的評分值在 0 到 5 之間,而這些評分值則用於對預測值進行排序。
#遍歷 計算相似度
def standEst(dataMat, user, simMeas, item):#數據矩陣、用戶編號、相似度計算方法和物品編號
n = shape(dataMat)[1]
simTotal = 0.0;ratSimTotal = 0.0
for j in range(n):
userRating = dataMat[user, j]
if userRating == 0: continue
#尋找兩個用戶都做了評價的產品
overLap = nonzero(logical_and(dataMat[:, item].A > 0, dataMat[:, j].A > 0))[0]
if len(overLap) == 0:
similarity = 0
else:#存在兩個用戶都評價的產品 計算相似度
similarity = simMeas(dataMat[overLap, item], dataMat[overLap, j])
print ('the %d and %d similarity is: %f' % (item, j, similarity))
simTotal += similarity #計算每個用戶對所有評價產品累計相似度
ratSimTotal += similarity * userRating #根據評分計算比率
if simTotal == 0:
return 0
else:
return ratSimTotal / simTotal
#推薦實現:recommend() 產生了最高的 N 個推薦結果
def recommend(dataMat, user, N=3, simMeas=cosSim, estMethod=standEst):
unratedItems = nonzero(dataMat[user, :].A == 0)[1] #尋找用戶未評價的產品
if len(unratedItems) == 0: return ('you rated everything')
itemScores = []
for item in unratedItems:
estimatedScore = estMethod(dataMat, user, simMeas, item)#基於相似度的評分
itemScores.append((item, estimatedScore))
return sorted(itemScores, key=lambda jj: jj[1], reverse=True)[:N]
recommend()函數在所有的未評分物品上進行循環。對每個未評分物品,則通過調用standEst() 來產生該物品的預測得分。該物品的編號和估計得分值會放在一個元素列表itemScores 中。最後按照估計得分,對該列表進行排序並返回。
測試:
>>> reload(svdRec)
<module 'svdRec' from '/home/zq/Git_zq/ML-in-Action-Code-and-Note/ch14/svdRec.py'>
>>> myMat=mat(svdRec.loadExData())
>>> myMat[0,1]=myMat[0,0]=myMat[1,0]=myMat[2,0]=4
>>> myMat[3,3]=2
>>> myMat
matrix([[4, 4, 0, 2, 2],
[4, 0, 0, 3, 3],
[4, 0, 0, 1, 1],
[1, 1, 1, 2, 0],
[2, 2, 2, 0, 0],
[5, 5, 5, 0, 0],
[1, 1, 1, 0, 0]])
>>> svdRec.recommend(myMat,2)
the 1 and 0 similarity is: 1.000000
the 1 and 3 similarity is: 0.928746
the 1 and 4 similarity is: 1.000000
the 2 and 0 similarity is: 1.000000
the 2 and 3 similarity is: 1.000000
the 2 and 4 similarity is: 0.000000
[(2, 2.5), (1, 2.0243290220056256)]
這表明了用戶 2 (由於我們從 0 開始計數,因此這對應了矩陣的第 3 行)對物品 2 的預測評分值爲 2.5 ,對物品 1 的預測評分值爲 2.05 。
下面利用 SVD 提高推薦的效果
>>> from numpy import linalg as la
>>> U,Sigma,VT=la.svd(mat(svdRec.loadExData2()))
>>> Sigma
array([ 15.77075346, 11.40670395, 11.03044558, 4.84639758,
3.09292055, 2.58097379, 1.00413543, 0.72817072,
0.43800353, 0.22082113, 0.07367823])
>>> Sig2=Sigma**2 #計算平方和
>>> sum(Sig2)
541.99999999999955
>>> sum(Sig2)*0.9 #取前90%
487.79999999999961
>>> sum(Sig2[:3]) #>90% SVD取前三個特徵值
500.50028912757932
下述程序中包含有一個函數 svdEst() 。在 recommend() 中,這個函數用於替換對 standEst() 的調用,該函數對給定用戶給定物品構建了一個評分估計值。
#利用SVD
def svdEst(dataMat, user, simMeas, item):
n = shape(dataMat)[1]
simTotal = 0.0;ratSimTotal = 0.0
U, Sigma, VT = la.svd(dataMat) #不同於stanEst函數,加入了SVD分解
Sig4 = mat(eye(4) * Sigma[:4]) # 建立對角矩陣
xformedItems = dataMat.T * U[:, :4] * Sig4.I #降維:變換到低維空間
#下面依然是計算相似度,給出歸一化評分
for j in range(n):
userRating = dataMat[user, j]
if userRating == 0 or j == item: continue
similarity = simMeas(xformedItems[item, :].T, xformedItems[j, :].T)
print ('the %d and %d similarity is: %f' % (item, j, similarity))
simTotal += similarity
ratSimTotal += similarity * userRating
if simTotal == 0:
return 0
else:
return ratSimTotal / simTotal
>>> svdRec.recommend(myMat,1,estMethod=svdRec.svdEst)
the 1 and 0 similarity is: 0.498142
the 1 and 3 similarity is: 0.498131
the 1 and 4 similarity is: 0.509974
the 2 and 0 similarity is: 0.552670
the 2 and 3 similarity is: 0.552976
the 2 and 4 similarity is: 0.217301
[(2, 3.4177569186592378), (1, 3.3307171545585641)]
構建推薦引擎面臨的挑戰
SVD 分解可以在程序調入時運行一次。在大型系統中, SVD 每天運行一次或者其頻率更低,並且還要離線運行。
1-推薦引擎中還存在其他很多規模擴展性的挑戰性問題,比如矩陣的表示方法。在上面給出的例子中有很多 0 ,實際系統中 0 的數目更多。也許,我們可以通過只存儲非零元素來節省內存和計算開銷?
2-一個潛在的計算資源浪費則來自於相似度得分。在我們的程序中,每次需要一個推薦得分時,都要計算多個物品的相似度得分,這些得分記錄的是物品之間的相似度。因此在需要時,這些記錄可以被另一個用戶重複使用。在實際中,另一個普遍的做法就是離線計算並保存相似度得分。
3-推薦引擎面臨的另一個問題就是如何在缺乏數據時給出好的推薦。這稱爲冷啓動 ( cold-start )問題,冷啓動問題的解決方案,就是將推薦看成是搜索問題。
爲了將推薦看成是搜索問題,我們可能要使用所需要推薦物品的屬性。在餐館菜餚的例子中,我們可以通過各種標籤來標記菜餚,比如素食、美式 BBQ 、價格很貴等。同時,我們也可以將這些屬性作爲相似度計算所需要的數據,這被稱爲基於內容( content-based )的推薦。可能,基於內容的推薦並不如我們前面介紹的基於協同過濾的推薦效果好,但我們擁有它,這就是個良好的開始。
六、示例:基於 SVD 的圖像壓縮
在代碼庫中,我們包含了一張手寫的數字圖像,該圖像在第 2 章使用過。原始的圖像大小是 32×32=1024 像素,我們能否使用更少的像素來表示這張圖呢?如果能對圖像進行壓縮,那麼就可以節省空間或帶寬開銷了。我們可以使用 SVD 來對數據降維,從而實現圖像的壓縮。
#實例:SVD實現圖像壓縮
#打印矩陣。由於矩陣包含了浮點數,因此必須定義淺色和深色。
def printMat(inMat, thresh=0.8):
for i in range(32):
for k in range(32):
if float(inMat[i,k]) > thresh:
print 1,
else: print 0,
print ('')
#壓縮
def imgCompress(numSV=3, thresh=0.8):
myl = []
for line in open('0_5.txt').readlines():
newRow = []
for i in range(32):
newRow.append(int(line[i]))
myl.append(newRow)
myMat = mat(myl)
print ("****original matrix******")
printMat(myMat, thresh)
U,Sigma,VT = la.svd(myMat) #SVD分解得到特徵矩陣
SigRecon = mat(zeros((numSV, numSV))) #初始化新對角矩陣
for k in range(numSV):#構造對角矩陣,將特徵值填充到對角線
SigRecon[k,k] = Sigma[k]
reconMat = U[:,:numSV]*SigRecon*VT[:numSV,:]#降維
print ("****reconstructed matrix using %d singular values******" % numSV)
printMat(reconMat, thresh)
imgCompress()函數基於任意給定的奇異值數目來重構圖像。該函數構建了一個列表,然後打開文本文件,讀入字符。
接下來就開始對原始圖像進行SVD分解並重構圖像。在程序中,通過將 Sigma 重新構成 SigRecon 來實現這一點。 Sigma 是一個對角矩陣,因此需要建立一個全0矩陣,然後將前面的那些奇異值填充到對角線上。最後,通過截斷的 U 和 V T 矩陣,用 SigRecon 得到重構後的矩陣,該矩陣通過 printMat() 函數輸出。
小結
VD 是一種強大的降維工具,我們可以利用 SVD 來逼近矩陣並從中提取重要特徵。通過保留矩陣 80% ~ 90% 的能量,就可以得到重要的特徵並去掉噪聲。
在大規模數據集上, SVD 的計算和推薦可能是一個很困難的工程問題。通過離線方式來進行SVD 分解和相似度計算,是一種減少冗餘計算和推薦所需時間的辦法。
在下一章中,我們將介紹在大數據集上進行機器學習的一些工具。