現在我們就開始構建一個推薦引擎,該推薦引擎關注的是餐館食物的推薦。假設一個人在家決定外出吃飯,但是他並不知道該到哪兒去吃飯,該點什麼菜。我們這個推薦系統可以幫他做到這兩點。
首先我們構建一個基本的推薦引擎,它能夠尋找用戶沒有嘗過的菜餚。然後,通過SVD來減少特徵空間並提高推薦的效果。這之後,將程序打包並通過用戶可讀的人機界面提供給人們使用。最後,我們介紹在構建推薦系統時面臨的一些問題。
14.5.1 推薦未嘗過的菜餚
推薦系統的工作過程是:給定一個用戶,系統會為此用戶返回N個最好的推薦菜。為了實現這一點,則需要我們做到:
- 尋找用戶沒有評級的菜餚,即在用戶-物品矩陣中的0值;
- 在用戶沒有評級的所有物品中,對每個物品預計一個可能的評級分數。這就是說,我們認為用戶可能會對物品的打分(這就是相似度計算的初衷);
- 對這些物品的評分從高到低進行排序,返回前N個物品。
好了,接下來我們嘗試這樣做。打開svdRec.py
文件並加入下列程序清單中的代碼。
程序清單14-2 基於物品相似度的推薦引擎
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
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))
#❸ 尋找前N個未評級物品
return sorted(itemScores, key=lambda jj: jj[1], reverse=True)[:N]
上述程序包含了兩個函數。第一個函數是standEst
,用來計算在給定相似度計算方法的條件下,用戶對物品的估計評分值。第二個函數是recommend
,也就是推薦引擎,它會調用standEst
函數。我們先討論standEst
函數,然後討論recommend
函數。
函數standEst
的參數包括數據矩陣、用戶編號、物品編號和相似度計算方法。假設這裡的數據矩陣為圖14-1和圖14-2的形式,即行對應用戶、列對應物品。那麼,我們首先會得到數據集中的物品數目,然後對兩個後面用於計算估計評分值的變量進行初始化。接著,我們遍歷行中的每個物品。如果某個物品評分值為0,就意味著用戶沒有對該物品評分,跳過了這個物品。該循環大體上是對用戶評過分的每個物品進行遍歷,並將它和其他物品進行比較。變量overLap
給出的是兩個物品當中已經被評分的那個元素❶。如果兩者沒有任何重合元素,則相似度為0且中止本次循環。但是如果存在重合的物品,則基於這些重合物品計算相似度。隨後,相似度會不斷累加,每次計算時還考慮相似度和當前用戶評分的乘積。最後,通過除以所有的評分總和,對上述相似度評分的乘積進行歸一化。這就可以使得最後的評分值在0到5之間,而這些評分值則用於對預測值進行排序。
函數recommend
產生了最高的N個推薦結果。如果不指定N的大小,則默認值為3。該函數另外的參數還包括相似度計算方法和估計方法。我們可以使用程序清單14-1中的任意一種相似度計算方法。此時我們能採用的估計方法只有一種選擇,但是在下一小節中會增加另外一種選擇。該函數的第一件事就是對給定的用戶建立一個未評分的物品列表❷。如果不存在未評分物品,那麼就退出函數;否則,在所有的未評分物品上進行循環。對每個未評分物品,則通過調用standEst
來產生該物品的預測得分。該物品的編號和估計得分值會放在一個元素列表itemScores
中。最後按照估計得分,對該列表進行排序並返回❸。該列表是從大到小逆序排列的,因此其第一個值就是最大值。
接下來看看它的實際運行效果。在保存 svdRec.py
文件之後,在Python提示符下輸入命令:
>>> reload(svdRec)
<module \'svdRec\' from \'svdRec.py\'>
下面,我們調入了一個矩陣實例,可以對本章前面給出的矩陣稍加修改後加以使用。首先,調入原始矩陣:
>>> myMat=mat(svdRec.loadExData)
該矩陣對於展示SVD的作用非常好,但是它本身不是十分有趣,因此我們要對其中的一些值進行更改:
>>> 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],
[1, 1, 1, 0, 0],
[5, 5, 5, 0, 0]])
好了,現在我們已經可以做些推薦了。我們先嘗試一下默認的推薦:
>>> svdRec.recommend(myMat, 2)
[(2, 2.5000000000000004), (1, 2.0498713655614456)]
這表明了用戶2(由於我們從0開始計數,因此這對應了矩陣的第3行)對物品2的預測評分值為2.5,對物品1的預測評分值為2.05。下面我們就利用其他的相似度計算方法來進行推薦:
>>> svdRec.recommend(myMat, 2, simMeas=svdRec.ecludSim)
[(2, 3.0), (1, 2.8266504712098603)]
>>> svdRec.recommend(myMat, 2, simMeas=svdRec.pearsSim)
[(2, 2.5), (1, 2.0)]
我們可以對多個用戶進行嘗試,或者對數據集做些修改來瞭解其給預測結果帶來的變化。
這個例子給出了如何利用基於物品相似度和多個相似度計算方法來進行推薦的過程,下面我們介紹如何將SVD應用於推薦。
14.5.2 利用SVD提高推薦的效果
實際的數據集會比我們用於展示recommend
函數功能的myMat
矩陣稀疏得多。圖14-4就給出了一個更真實的矩陣的例子。
圖14-4 一個更大的用戶-菜餚矩陣,其中有很多物品都沒有評分,這比一個全填充的矩陣更接近真實情況
我們可以將該矩陣輸入到程序中去,或者從下載代碼中複製函數loadExData2
。下面我們計算該矩陣的SVD來瞭解其到底需要多少維特徵。
>>>from numpy import linalg as la
>>> U,Sigma,VT=la.svd(mat(svdRec.loadExData2))
>>> Sigma
array([ 1.38487021e+01, 1.15944583e+01, 1.10219767e+01,
5.31737732e+00, 4.55477815e+00, 2.69935136e+00,
1.53799905e+00, 6.46087828e-01, 4.45444850e-01,
9.86019201e-02, 9.96558169e-17])
接下來我們看看到底有多少個奇異值能達到總能量的90%。首先,對Sigma
中的值求平方:
>>> Sig2=Sigma**2
再計算一下總能量:
>>> sum(Sig2)
541.99999999999932
再計算總能量的90%:
>>> sum(Sig2)*0.9
487.79999999999939
然後,計算前兩個元素所包含的能量:
>>> sum(Sig2[:2])
378.8295595113579
該值低於總能量的90%,於是計算前三個元素所包含的能量:
>>> sum(Sig2[:3])
500.50028912757909
該值高於總能量的90%,這就可以了。於是,我們可以將一個11維的矩陣轉換成一個3維的矩陣。下面對轉換後的三維空間構造出一個相似度計算函數。我們利用SVD將所有的菜餚映射到一個低維空間中去。在低維空間下,可以利用前面相同的相似度計算方法來進行推薦。我們會構造出一個類似於程序清單14-2中的standEst
函數。打開svdRec.py
文件並加入如下程序清單中的代碼。
程序清單14-3 基於SVD的評分估計
def svdEst(dataMat, user, simMeas, item):
n = shape(dataMat)[1]
simTotal = 0.0; ratSimTotal = 0.0
U,Sigma,VT = la.svd(dataMat)
#❶ 建立對角矩陣
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
上述程序中包含有一個函數svdEst
。在recommend
中,這個函數用於替換對standEst
的調用,該函數對給定用戶給定物品構建了一個評分估計值。如果將該函數與程序清單14-2中的 standEst
函數進行比較,就會發現很多行代碼都很相似。該函數的不同之處就在於它在第3行對數據集進行了SVD分解。在SVD分解之後,我們只利用包含了90%能量值的奇異值,這些奇異值會以NumPy數組的形式得以保存。因此如果要進行矩陣運算,那麼就必須要用這些奇異值構建出一個對角矩陣❶。然後,利用U
矩陣將物品轉換到低維空間中❷。
對於給定的用戶,for
循環在用戶對應行的所有元素上進行遍歷。這和standEst
函數中的for
循環的目的一樣,只不過這裡的相似度計算是在低維空間下進行的。相似度的計算方法也會作為一個參數傳遞給該函數。然後,我們對相似度求和,同時對相似度及對應評分值的乘積求和。這些值返回之後則用於估計評分的計算。for
循環中加入了一條print
語句,以便能夠瞭解相似度計算的進展情況。如果覺得這些輸出很累贅,也可以將該語句註釋掉。
接下來看看程序的執行效果。將程序清單14-3中的代碼輸入到文件svdRec.py
中並保存之後,在Python提示符下運行如下命令:
>>> reload(svdRec)
<module \'svdRec\' from \'svdRec.pyc\'>
>>> svdRec.recommend(myMat, 1, estMethod=svdRec.svdEst)
The 0 and 3 similarity is 0.362287.
.
.
.
The 9 and 10 similarity is 0.497753.
[(6, 3.387858021353602), (8, 3.3611246496054976), (7, 3.3587350221130028)]
下面再嘗試另外一種相似度計算方法:
>>> svdRec.recommend(myMat, 1, estMethod=svdRec.svdEst,
simMeas=svdRec.pearsSim)
The 0 and 3 similarity is 0.116304.
.
.
.
The 9 and 10 similarity is 0.566796.
[(6, 3.3772856083690845), (9, 3.3701740601550196), (4, 3.3675118739831169)]
我們還可以再用其他多種相似度計算方法嘗試一下。感興趣的讀者可以將這裡的結果和前面的方法(不做SVD分解)進行比較,看看到底哪個性能更好。
14.5.3 構建推薦引擎面臨的挑戰
本節的代碼很好地展示出了推薦引擎的工作流程以及SVD將數據映射為重要特徵的過程。在撰寫這些代碼時,我盡量保證它們的可讀性,但是並不保證代碼的執行效率。一個原因是,我們不必在每次估計評分時都做SVD分解。對於上述數據集,是否包含SVD分解在效率上沒有太大的區別。但是在更大規模的數據集上,SVD分解會降低程序的速度。SVD分解可以在程序調入時運行一次。在大型系統中,SVD每天運行一次或者頻率更低,並且還要離線運行。
推薦引擎中還存在其他很多規模擴展性的挑戰性問題,比如矩陣的表示方法。在上面給出的例子中有很多0,實際系統中0的數目更多。也許,我們可以通過只存儲非零元素來節省內存和計算開銷?另一個潛在的計算資源浪費則來自於相似度得分。在我們的程序中,每次需要一個推薦得分時,都要計算多個物品的相似度得分,這些得分記錄的是物品之間的相似度。因此在需要時,這些記錄可以被另一個用戶重複使用。在實際中,另一個普遍的做法就是離線計算並保存相似度得分。
推薦引擎面臨的另一個問題就是如何在缺乏數據時給出好的推薦。這稱之為冷啟動(cold-start)問題,處理起來十分困難。這個問題的另一個說法是,用戶不會喜歡一個無效的物品,而用戶不喜歡的物品又無效。1如果推薦只是一個可有可無的功能,那麼上述問題倒也不大。但是如果應用的成功與否和推薦的成功與否密切相關,那麼問題就變得相當嚴重了。
1 也就是說,在協同過濾場景下,由於新物品到來時由於缺乏所有用戶對其的喜好信息,因此無法判斷每個用戶對其的喜好。而無法判斷某個用戶對其的喜好,也就無法利用該商品。——譯者注
冷啟動問題的解決方案,就是將推薦看成是搜索問題。在內部表現上,不同的解決辦法雖然有所不同,但是對用戶而言卻都是透明的。為了將推薦看成是搜索問題,我們可能要使用所需要推薦物品的屬性。在餐館菜餚的例子中,我們可以通過各種標籤來標記菜餚,比如素食、美式BBQ、價格很貴等等。同時,我們也可以將這些屬性作為相似度計算所需要的數據,這被稱為基於內容(content-based)的推薦。可能,基於內容的推薦並不如我們前面介紹的基於協同過濾的推薦效果好,但我們擁有它,這就是個良好的開始。