1. 程式人生 > >[完]機器學習實戰 第十四章 利用SVD簡化資料

[完]機器學習實戰 第十四章 利用SVD簡化資料

本章內容:

  • SVD矩陣分解
  • 推薦引擎
  • 利用SVD提升推薦引擎的效能

餐館可分為很多類別,不同的專家對其分類可能有不同依據。實際中,我們可以忘掉專家,從資料著手,可對記錄使用者關於餐館觀點的資料進行處理,並從中提取出其背後的因素。這些因素可能會與餐館的類別、烹飪時採用的某個特定配料,或其他任意物件一致。然後,可利用這些因素來估計人們對沒有去過的餐館的看法。

提取這些資訊的方法稱為奇異值分解(Singular Value Decomposition,SVD)。從生物資訊學到金融學等在內的很多應用中,SVD都是提取資訊的強大工具。

本章會介紹SVD的概念及其能進行資料約簡的原因,然後,介紹基於Python的SVD實現以及將資料對映到低維空間的過程。還將學習推薦引擎的概念和它們實際執行過程。為提高SVD的精度,我們將會把其應用到推薦系統中,該推薦系統將會幫助人們尋找到合適的餐館,最後,會講述一個SVD在影象壓縮中的例子。

一、SVD的應用

奇異值分解的優缺點:

  • 優點:簡化資料,去除噪聲,提高演算法的結果。
  • 缺點:資料的轉換可能難以理解。
  • 使用資料型別:數值型資料。

利用SVD,可使用小得多的資料集來表示原始資料集,這樣會去除噪聲資料和冗餘資訊。在此,我們主要是為了從資料中抽取資訊。基於此,可把SVD看成是從有噪聲資料中抽取相關特徵。

1-1 隱形語義索引

最早的SVD應用之一是資訊檢索。將利用SVD的方法稱為隱性語義索引(Latent Semantic Indexing,LSI)隱性語義分析(Latent Semantic Analysis,LSA)

在LSI中,一個矩陣是由文件和詞語組成的。當我們在該矩陣上應用SVD時,就會構建出多個奇異值。這些奇異值代表了文件中的概念或主題,這一特點可以用於更高效的文件搜尋。在詞語拼寫錯誤時,只基於詞語存在與否的簡單搜尋方法會遇到問題。簡單搜尋的另一個問題就是同義詞的使用。即,當查詢一個詞時,其同義詞所在的文件可能並不會匹配上。如果從上千篇相似的文件中抽取出概念,那麼同義詞就會對映為統一概念。

1-2 推薦系統

SVD的另一個應用是推薦系統。簡單版本的推薦系統能計算項或人之間的相似度。更先進的方法則先利用SVD從資料中構建一個主題空間,然後再在該空間下計算其相似度。圖1給出的矩陣,由餐館的菜和品菜師對這些菜的意見構成。品菜師可採用1-5之間的任意整數來對菜評級。若沒品嚐過某道菜,則評0級。


餐館的菜及其評級的資料。對此矩陣進行SVD處理則可以將資料壓縮到若干概念中。在右邊的矩陣當中,標出了一個概念。
圖1 餐館的菜及其評級的資料。對此矩陣進行SVD處理則可以將資料壓縮到若干概念中。在右邊的矩陣當中,標出了一個概念。

對上述矩陣進行SVD處理,會得到兩個奇異值。因此,就會彷彿有兩個概念或主題與此資料集相關聯。看看能否通過觀察圖中的0來找到這個矩陣的具體概念。觀察有圖的陰影部分,看起來Ed、Peter和Tracy對“烤牛肉”和“手撕豬肉”進行了評級,同時這三人未對其他菜評級。烤牛肉和手撕豬肉都是美式燒烤餐館才有的菜,其他菜則在日式餐館才有。

可以將奇異值想象成一個新空間。與圖1中的矩陣給出的五維或者七維不同,我們最終的矩陣只有二維。這二維分別是什麼呢?能告訴我們資料的什麼資訊?這二維分別對應圖中給出的兩個組,右圖中已經標示出了其中的一個組。可基於每個組的共同特徵來命名這二維,比如得到的美式BBQ和日式食品這二維。

如何將原始資料變換到上述新空間中呢?下一節會進一步詳細地介紹SVD,將會了解到SVD是如何得到UVT兩個矩陣。VT矩陣會將使用者對映到BBQ/日式食品空間去。類似地,U矩陣會將餐館的菜對映到BBQ/日式食品空間去。真實的資料通常不會像圖1中的矩陣那樣稠密或整齊,這裡如此只是為了便於說明問題。

推薦引擎中可能會有噪聲資料,比如,某人對某些菜的評級就可能存在噪聲,並且推薦系統也可將資料抽取為這些基本主題。基於這些主題,推薦系統就能取得比原始資料更好的推薦效果。

二、矩陣分解

在很多情況下,資料中的一小段攜帶了資料集中的大部分資訊,而其他資訊要麼是噪聲,要麼就是毫不相關的資訊。矩陣分解可將原始矩陣表示成新的易於處理的形式,新形式是兩個或多個矩陣的乘積。

不同的矩陣分解技術具有不同的性質,其中有些更適合於某個應用,有些則更適合於其他應用。最常見的一種矩陣分解技術就是SVD。SVD將原始的資料集矩陣Data分解成三個矩陣UΣVT。如果原始矩陣Data是m行n列,則有如下等式:

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

上述分解中會構建出一個矩陣Σ,該矩陣只有對角元素,其他元素均為0。另一個慣例就是,Σ的對角元素是從大到小排列的。這些對角元素稱為奇異值(Singular Value),它們對應了原始資料集矩陣Data的奇異值。回想PCA章節,得到的是矩陣的特徵值,它們告訴我們資料集中的重要特徵。Σ中的奇異值也是如此。奇異值和特徵值時有關係的。這裡的奇異值就是矩陣DataDataT特徵值的平方根。

矩陣Σ只有從大到小排列的對角元素。在科學和工程中,一致存在這樣一個普遍事實:在某個奇異值的數目(r個)之後,其他的奇異值都置為0。這就意味著資料集中僅有r個重要特徵,而其餘特徵則都是噪聲或冗餘特徵。

三、利用Python實現SVD

NumPy由一個稱為linalg的線性代數工具箱,利用此工具箱可實現如下矩陣的SVD處理:

[1717]

要在Python上實現該矩陣的SVD處理,執行如下命令:

>>> from numpy import *
>>> U,Sigma,VT=linalg.svd([[1,1],[7,7]])
>>> U
array([[-0.14142136, -0.98994949],
       [-0.98994949,  0.14142136]])
>>> Sigma
array([ 10.,   0.])
>>> VT
array([[-0.70710678, -0.70710678],
       [-0.70710678,  0.70710678]])

注意,矩陣Sigma以行向量array([ 10., 0.])返回,而非如下矩陣:

array([[ 10.,   0.],
       [  0.,   0.]])

由於矩陣除了對角元素其他均為0,因此這種僅返回對角元素的方式能夠節省空間,這是由NumPy的內部機制產生的。接下來在一個更大的資料集上進行更多的分解。建立新檔案svdRec.py並加入如下程式碼:

def loadExData() :
    return [[1, 1, 1, 0, 0],
            [2, 2, 2, 0, 0],
            [1, 1, 1, 0, 0],
            [5, 5, 5, 0, 0],
            [1, 1, 0, 2, 2],
            [0, 0, 0, 3, 3],
            [0, 0, 0, 1, 1]]

接下來對該矩陣進行分解。

>>> import ml.svdRec as svdRec
>>> Data=svdRec.loadExData()
>>> U,Sigma,VT=linalg.svd(Data)
>>> Sigma
array([  9.72140007e+00,   5.29397912e+00,   6.84226362e-01,
         1.67441533e-15,   3.39639411e-16])

前三個資料比其他的值大很多,後兩個值在不同機器上結果可能會稍有差異,但數量級差不多。於是,我們可將後兩個值去掉。原始資料集可用如下結果來近似:

Datam×nUm×3Σ3×3VT3×n

圖2是上述近似計算的一個示意圖。


SVD的示意圖。矩陣Data被分解。淺灰色區域時原始資料,深灰色區域是矩陣近似計算僅需要的資料
圖2 SVD的示意圖。矩陣Data被分解。淺灰色區域時原始資料,深灰色區域是矩陣近似計算僅需要的資料

接著可重構原始矩陣,首先構建一個3x3的矩陣Sig3:

>>> Sig3=mat([[Sigma[0], 0, 0],[0, Sigma[1], 0],[0, 0, Sigma[2]]])

接下來重構原始矩陣的近似矩陣。由於Sig3僅為3x3的矩陣,因而只需使用矩陣U的前3列和VT的前三行。為了在Python中實現這一點,輸入如下命令:

>>> U[:,:3]*Sig3*VT[:3,:]
matrix([[  1.00000000e+00,   1.00000000e+00,   1.00000000e+00,
          -6.81963166e-17,  -7.18826040e-17],
        [  2.00000000e+00,   2.00000000e+00,   2.00000000e+00,
           7.52436308e-17,   6.76542156e-17],
        [  1.00000000e+00,   1.00000000e+00,   1.00000000e+00,
           7.91142325e-16,   7.87456038e-16],
        [  5.00000000e+00,   5.00000000e+00,   5.00000000e+00,
           8.50014503e-17,   6.63531730e-17],
        [  1.00000000e+00,   1.00000000e+00,  -8.88178420e-16,
           2.00000000e+00,   2.00000000e+00],
        [  1.66533454e-16,   1.52655666e-15,  -1.44328993e-15,
           3.00000000e+00,   3.00000000e+00],
        [  2.77555756e-17,   4.78783679e-16,  -4.02455846e-16,
           1.00000000e+00,   1.00000000e+00]])

如何知道僅需保留前3個奇異值呢?確定要保留的奇異值的數目有很多啟發式的策略,其中一個典型的做法是保留矩陣中90%的能量資訊。為計算總能量資訊,將所有的奇異值求其平方和。於是可將奇異值的平方和累加到總值的90%為止。另一個啟發式策略是,當矩陣上有上萬的奇異值時,那麼就保留前面的2000或3000個。在任何資料集上,都不能保證前3000個奇異值能夠包含90%的能量資訊,但在實際中更容易實施。

通過三個矩陣對原始矩陣進行了近似,可用一個小很多的矩陣來表示一個大矩陣。很多應用可通過SVD來提升效能。接下來將討論一個比較流行的SVD應用的例子——推薦引擎。

四、基於協同過濾的推薦引擎

有很多方法可實現推薦功能,這裡使用一種稱為協同過濾(collaborative filtering)的方法。協同過濾是通過將使用者和其他使用者的資料進行對比來實現推薦的。

這裡的資料是從概念上組織成了類似圖2所給出的矩陣形式。當資料採用這種方式進行組織時,我們就可比較使用者或物品之間的相似度了。當知道兩個使用者或兩個物品之間的相似度,就可利用已有的資料來預測未知的使用者喜好。如,我們試圖對某個使用者喜歡的電影進行預測,推薦引擎會發現有一部電影該使用者還沒看過。然後,它就會計算該電影和使用者看過的電影之間的相似度,如果相似度高,推薦演算法就會認為使用者喜歡這部電影。

在上述場景下,唯一所需要的數學方法就是相似度計算。我們首先討論物品之間的相似度計算,然後討論在基於物品和基於使用者的相似度計算之間的折中。最後,介紹推薦引擎成功的度量方法。

4-1 相似度計算

希望擁有一些物品之間相似度的定量方法。我們不利用專家所給出重要屬性來描述物品從而計算它們之間的相似度,而是利用使用者對它們的意見來計算相似度。這就是協同過濾中所使用的的方法。它並不關心物品的描述屬性,而是嚴格地按照許多使用者的觀點來計算相似度。圖3給出了有一些使用者及其對前面給出的部分菜餚的評級資訊所組成的矩陣。


用於展示相似度計算的簡單矩陣
圖3 用於展示相似度計算的簡單矩陣

計算一下手撕豬肉和烤牛肉之間的相似度。一開始使用歐氏距離來計算。

(44)2+(33)2+(21)2=1

而手撕豬肉和鰻魚飯的歐式距離為:

(42)2+(35)2+(22)2=2.83

在該資料中,由於手撕豬肉和烤牛肉的距離小於手撕豬肉和鰻魚飯的距離。因此手撕豬肉與烤牛肉比鰻魚飯更為相似。我們希望,相似度值在0到1之間變化,並且物品對越相似,它們的相似度值也就越大。可用“相似度=1/(1+距離)”這樣的算式來計算相似度。當距離為0時,相似度為1.0。如果距離真的非常大時,相似度也就趨近於0。

第二種計算距離的方法是皮爾遜相關係數(Pearson correlation)。在度量回歸方程的精度時曾經用到過這個量,它度量的是兩個向量之間的相似度。該方法相對於歐式距離的一個優勢在於,它對使用者評級的量級並不敏感。比如,某個狂躁者對所有物品的評分都是5分,而另一個憂鬱者對所有物品的評分都是1分,皮爾遜相關係數會認為這兩個向量時相等的。在NumPy中,皮爾遜相關係數的計算是由函式corrcoef()進行的,後面很快就會用到它了。皮爾遜相關係數取值範圍從-1到+1,可通過0.5+0.5*corrcoef()這個函式計算,並且把其取值範圍歸一化到0到1之間。

另一個常用的距離計算方法是餘弦相似度(cosine similarity),其計算的是兩個夾角的餘弦值。如果夾角為90度,則相似度為0;如果兩個向量的方向相同,則相似度為1.0。同皮爾遜相關係數一樣,餘弦相似度的取值範圍也在-1到+1之間,因此也需將它歸一化到0到1之間。計算餘弦相似度,採用的兩個向量AB夾角的餘弦相似度的定義如下:

cosθ=ABAB

其中,表示向量AB的2範數,你還可以定義向量的任一範數,但是如果不指定範數階數,則都假設為2範數。向量[4, 2, 2]的2範數為:

42+22+22

NumPy的線性代數工具箱中提供了範數的計算方法linalg.norm()。

將上述各種相似度的計算方法寫成Python中的函式。

from numpy import *
from numpy import linalg as la
# inA和inB都是列向量
def ecludSim(inA, inB) :
    return 1.0/(1.0 + la.norm(inA - inB))

def pearsSim(inA, inB) :
    # 檢查是否存在三個或更多的點,若不存在,則返回1.0,這是因為此時兩個向量完全相關
    if len(inA) < 3 : return 1.0
    return 0.5+0.5*corrcoef(inA, inB, rowvar = 0)[0][1]

def cosSim(inA, inB) :
    num = float(inA.T*inB)
    denom = la.norm(inA)*la.norm(inB)
    return 0.5+0.5*(num/denom)

下面我們嘗試執行上述函式:

>>> import ml.svdRec as svdRec
>>> from numpy import *
>>> myMat = mat(svdRec.loadExData())
# 歐氏距離
>>> svdRec.ecludSim(myMat[:,0], myMat[:,4])
0.13367660240019172
>>> svdRec.ecludSim(myMat[:,0], myMat[:,0])
1.0
# 餘弦相似度
>>> svdRec.cosSim(myMat[:,0], myMat[:,4])
0.54724555912615336
>>> svdRec.cosSim(myMat[:,0], myMat[:,0])
0.99999999999999989
# 皮爾遜相關係數
>>> svdRec.pearsSim(myMat[:,0], myMat[:,4])
0.23768619407595826
>>> svdRec.pearsSim(myMat[:,0], myMat[:,0])
1.0

上面的相似度計算都是假設資料採用了列向量的方式進行表示。如果利用上述函式來計算兩個行向量的相似度就會遇到問題(我們很容易對上述函式進行修改以計算行向量之間的相似度)。這裡採用列向量的表示方法,暗示著我們將