【機器學習實戰-python3】利用SVD簡化數據

本篇的數據和代碼參見: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。公式如下:

Datam×n=Um×mΣm×nVTn×n

上述分解中會構建出一個矩陣 Σ ,該矩陣只有對角元素,其他元素均爲0。另一個慣例就是,Σ 的對角元素是從大到小排列的。這些對角元素稱爲奇異值(Singular Value),它們對應了原始數據集矩陣 Data 的奇異值。奇異值和特徵值是有關係的。這裏的奇異值就是矩陣DataDataT 特徵值的平方根。

科學和工程中,一直存在這樣一個普遍事實:在某個奇異值的數目( 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 個數值比其他的值大了很多(如果你的最後兩個值的結果與這裏的結果稍有不同,也不必擔心。它們太小了,所以在不同機器上產生的結果就可能會稍有不同,但是數量級應該和這裏的結果差不多)。於是,我們就可以將最後兩個值去掉了。數據表示爲:

Datam×n=Um×3Σ3×3VT3×n

這裏寫圖片描述

我們是如何知道僅需保留前 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 。

cosΘ=ABAB

其中AB 爲A、B的2範數。你可以定義向量的任一範數,但是如果不指定範數階數,則都假設爲 2 範數。
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 分解和相似度計算,是一種減少冗餘計算和推薦所需時間的辦法。
在下一章中,我們將介紹在大數據集上進行機器學習的一些工具。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章