這里所有代碼都是由Python實現!
一個協作性過濾算法通常的做法就是對一大群人進行搜索,從中找出來和我們品味興趣相近的一小群人來。
推薦算法,從字面上看就是向用戶推薦他所感興趣的內容,如果是購物網站,就推薦他感興趣的商品;如果是音樂網站,就推薦他感興趣的音樂等等。
說到推薦算法,我最先能想到的就是相似度計算,但是如何應用呢?而這里又談到計算,就要有數,那數從哪里來呢?
由剛才提到的協作性過濾算法,可以知道如果要提供推薦,那就要獲取大量的數據,包括人,商品以及人對商品的評價,通過分析評論來對用戶進行推薦,
將用戶評論量化就得到了數,有了數就可以進行計算了。
這里我要說兩種推薦算法:其一是基於用戶的推薦算法,其二是基於物品或商品的推薦算法。這兩種是類似的,只是將物品和人互換一下的感覺。
首先說一下兩種算法的思路:
對於基於用戶的推薦算法,由名字可以知道,它就由人來進行物品的推薦的,找到與被推薦用戶品味興趣相同的用戶,根據
這些用戶來找到推薦的商品。當數據量非常大的時候,基於用戶的推薦算法,效率就比較低了,因為每次為某個用戶進行推薦,都要將其與其余所有用戶
計算相似度。
因此出現了基於物品的推薦算法,顧名思義,它是找到每件物品最為相近的物品,也就是對物品之間計算相似度,當為某位用戶推薦時,查看他
評價過的物品或者購買過的物品來為他推薦相近的可能是他感興趣的物品。
基於物品的推薦算法和基於用戶的推薦算法相比,最顯著的區別在於,物品間的比較不會像用戶間的比較那么頻繁變化。這就表示不用不停的計算
與每樣物品最為相近的其他物品:,而基於用戶的推薦算法,計算某個用戶的相似用戶,就要拿該用戶去和其他所有用戶去計算相似度,這就顯得很麻煩了。
白話介紹完以上兩個推薦算法之后,該正式說一下推薦算法的主要思路了:
一、以下步驟以基於用戶的推薦算法為例講解思路。
1、首先要收集到一群人對相應的物品或商品評價值,並將其保存起來。這里我用字典的形式將這些數據保存起來,用一個二層字典的形式,
最外層字典的鍵為用戶的名稱,即人的名字,value值為一個第二層的字典,第二層字典包含的是商品和評價值的鍵值對。這里以一個影評者及其
對幾部影片的評價值為例,即:
# 一個涉及影評者及其幾步影片評分情況的字典 critics = {'Lisa Rose': {'Lady in the Water': 2.5, 'Snakes on a Plane': 3.5, 'Just My Luck': 3.0, 'Superman Returns': 3.5, 'You, Me and Dupree': 2.5, 'The Night Listener': 3.0}, 'Gene Seymour': {'Lady in the Water': 3.0, 'Snakes on a Plane': 3.5, 'Just My Luck': 1.5, 'Superman Returns': 5.0, 'The Night Listener': 3.0, 'You, Me and Dupree': 3.5}, 'Michael Phillips': {'Lady in the Water': 2.5, 'Snakes on a Plane': 3.0, 'Superman Returns': 3.5, 'The Night Listener': 4.0}, 'Claudia Puig': {'Snakes on a Plane': 3.5, 'Just My Luck': 3.0, 'The Night Listener': 4.5, 'Superman Returns': 4.0, 'You, Me and Dupree': 2.5}, 'Mick LaSalle': {'Lady in the Water': 3.0, 'Snakes on a Plane': 4.0, 'Just My Luck': 2.0, 'Superman Returns': 3.0, 'The Night Listener': 3.0, 'You, Me and Dupree': 2.0}, 'Jack Matthews': {'Lady in the Water': 3.0, 'Snakes on a Plane': 4.0, 'The Night Listener': 3.0, 'Superman Returns': 5.0, 'You, Me and Dupree': 3.5}, 'Toby': {'Snakes on a Plane': 4.5, 'You, Me and Dupree': 1.0, 'Superman Returns': 4.0}}
2、有了數據,就可以開始運算了,這里要提到相似度計算,通過相似度計算可以得到相近的人或者相近的物,然后再向用戶進行推薦。相似度計算有很多種,這里我主要
說其中兩種相似度計算的方法:(1)歐幾里德距離;(2)皮爾遜相關系統
(1)歐幾里德距離:它是要構建一個“偏好”空間,“偏好”空間以人們一致評價的物品或商品為坐標軸,將參與評價人繪制到“偏好”空間里。然后計算人與人之間的距離,距離越
近,則表示他們的興趣偏好越相近。這里面的距離即為坐標軸中兩點之間的距離計算公式是一樣的,即對兩點各個對應坐標的差值平方和進行開根號。具體代碼表示如下:
from math import sqrt # 返回一個person1和person2的基於距離的相似度評價,即:歐幾里德距離 # 其中prefs所要傳入的參數為第一步建立的字典 def sim_distance(prefs, person1, person2): # 由歐幾里德距離的定義可知,首先要找到兩個人一致評價的物品或商品 sim_items = [] for item in prefs[person1]: if item in prefs[person2]: sim_items.append(item) # 若沒有一致評價的物品,則相似度為0 if len(sim_items) == 0: retunr 0 # 如果有一致評價的物品,則使用歐幾里德距離計算相似度 sim = 0 # 計算差值平方和 for item in sim_items: sim += pow(prefs[person1][item] - prefs[person2][item], 2)
return 1 / (1 + sqrt(sim))
最后返回的結果為1 / (1 + sqrt(sim)),這是因為把相似度變為0——1之間的數,越大越相似,越小越不相似。其中計算差值平方和也可以直接用一個列表推導式來表達。
(2)皮爾遜相關系統(皮爾遜相關度評價):它是判斷兩組數據與某一直線擬合程度的一種度量。與歐幾里德距離相反,它是以人為坐標軸,將人們一致評價過的物品或商
品繪制到坐標系里的。對應的計算公式相比歐幾里德距離要復雜的多,而且難於理解。具體如何理解等我能很好的理解了再給出分析。皮爾遜相關系統主要是在數據不是很規范
的時候,會傾向於給出更好的結果。剛才第一句說,皮爾遜相關系數是判斷兩組數據與某一直線擬合程度的一種度量,這條直線也會繪制到坐標系中,它的繪制原則是盡可能地
靠近圖上的所有坐標點,故而成為最佳擬合線。
在這里,我要說一下,皮爾遜相關系數計算很復雜,但為什么會提出皮爾遜相關系數呢?其中最主要的原因是皮爾遜相關系數解決了“誇大分值”的情況。什么又是“誇大分值”
的情況呢,舉個例子來說,對於有一種情況,就是對於兩個人都評價的商品或物品,如果其中一個人的評價值始終高於另一個人,而且兩者對同一影片的評價值之差也非常的相近,
這樣兩個人的品味其實是相似的,他們的最終直線也仍然是擬合的。但是如果將這兩個人用歐幾里德距離去計算,他們的相似度會偏低,這就是所謂的“誇大分值”的情況。
接下來給出皮爾遜相關系統的代碼,代碼中包含它的計算方法:
# 對於皮爾遜相關系統,也是計算兩個人的相似度,同時也要找到兩個# 人共同評價過的商品或者物品,其中prefs也是之前第一步中提出的# 字典 def sim_pearson(prefs, person1, person2): # 首先獲取兩個人都評價過的物品列表 sim_items = [] for item in prefs[person1]: if item in prefs[person2]: sim_items.append(item) # 若兩者無共同評價過的商品,則相似度為0 n = len(sim_items) # 這個n在計算時要用到 if n == 0: return 0 # 若兩者有共同評價的商品,則通過皮爾遜相關度來計算相似度 # 首先對每個人的偏好求和 sum1 = 0 sum2 = 0 for item in sim_items: sum1 += prefs[person1][item] sum2 += prefs[person2][item] # 然后求每個人的偏好平方和 sum1Sq = 0 sum2Sq = 0 for item in sim_items: sum1Sq += pow(prefs[person1][item], 2) sum2Sq += pow(prefs[person2][item], 2) # 求兩個人偏好乘積之和 pSum = 0 for item in sim_items: pSum = prefs[person1][item] * prefs[person2][item] # 准備工作做完,最后計算皮爾遜相關系數 num = pSum - (sum1 * sum2 / n) den = sqrt((sum1Sq - pow(sum1, 2) / n) * (sum2Sq - pow(sum2, 2) / n)) if den == 0: # 皮爾遜相關系數是num/den,若den為0就無意義了,返回0 return 0 return num / den
由代碼可知,皮爾遜相關系數的計算比較難,但是相對於某些情況來說(如“誇大分值”的情況),皮爾遜相關系數的效果是很好的。皮爾遜相關系數計算的值為一個介於-1到1之間的數,
若大於0,則表示兩個人的評價值呈現正相關,若小於0,則說明兩個人的評價值呈現負相關。值為1則表明兩個人對每一樣物品均有着完全一致的評價。
3、有了相似度度量方法,則可以通過字典數據來計算人與人之間的相似度了(也可以計算物品與物品之間的相似度,但需要對字典數據進行相應的轉換,轉換為物品,人和評價值的字典),
這里寫一個函數來計算相似度並返回top-n最相近的人或物。
# 用來計算某人與其他人的相似度,並返回最相似的前幾個 # 其中prefs為字典數據,要找到與person相似的人 # n為返回的前n個最相似的人,similarity為相似度度量方法 # similarity可以用皮爾遜也可以用歐幾里德,也可用其他相似度方法 # similarity默認用皮爾遜度量方法 def topMathches(prefs, person, n, similarity=sim_pearson): sims = [] #將相似度和人構建元組存到列表里,便於排序 for otherperson in prefs: # 首先判斷otherperson是不是person,避免自己同自己比較 if otherperson != person: sim = similarity(person, otherperson) # 為了方便排序,將sim當做鍵,otherperson當做value sims.append((sim, otherperson)) sims.sort() sims.reverse() return sim[0: n]
通過該函數就可以得到與person最相似的前n個人了。該方法也可以將person替換為物品,prefs轉換為物品,人和評價值的字典形式,然后來計算某個物品最相近的幾個物品。
4、現在我們可以得到與被推薦用戶最為相似的幾個用戶了,但是這還沒有達到我們的目的,我們的目的是為了向被推薦用戶推薦其感興趣的商品或者物品,因此這一步是要為用戶推薦物品。
怎么推薦呢?其中有中方法是,可以為其推薦相似用戶所評價或購買的一件物品,而這件物品被推薦用戶沒有評價或購買這件物品,但是這樣顯得很隨意,因為可能會有問題:以電影評價為例,
評論者還未對某些影片做過評論,而這些影片也許就是被推薦用戶所喜歡的;還有一種可能,會找到一些熱衷某些影片的古怪評論者,但根據上一步topMatches返回的結果,可能其他的評論者
都不看好這部影片。為了解決以上可能出現的問題,要生成一個適合任何情況的推薦方法,這里通過一個經過加權的評價值來為影片打分,評論者的評分結果因此形成了先后的排名。如何加權:
我們得到與一些評論者的相似度之后,用相似度去乘以他們為每部影片所給的評價值,這樣一來,相比於與被推薦用戶不相近的人,那些與被推薦用戶相近的人將會對整體評價值擁有更多的貢獻,
對一部影片,將這些乘積相加的一個總計值,但是如果這部影片被更多的人評價的話(相比於其他電影這部電影評價的人更多),就會導致對總計值的結果造成很大的影響,因此我們拿這個總計值
去除以參與評價這部影片的評價者的相似度之和。這就是加權的方法。下面我們用圖表顯示,並做大概說明:
表:為Toby提供推薦
評價者\影片名稱 | 相似度 | Night | S.xNight | Lady | S.xLady | Luck | S.xLuck |
Rose | 0.99 | 3.0 | 2.97 | 2.5 | 2.48 | 3.0 | 2.97 |
Seymour | 0.38 | 3.0 | 1.14 | 3.0 | 1.14 | 1.5 | 0.57 |
Puig | 0.89 | 4.5 | 4.02 | 3.0 | 2.68 | ||
LaSalle | 0.92 | 3.0 | 2.77 | 3.0 | 2.77 | 2.0 | 1.82 |
Matthews | 0.66 | 3.0 | 1.99 | 3.0 | 1.99 | ||
總計 | 12.89 | 8.38 | 8.07 | ||||
Sim. Sum | 3.82 | 2.95 | 3.18 | ||||
總計/Sim. Sum | 3.35 | 2.83 | 2.53 |
其中最左邊一列表示評價者以及總計值和所有評價者相似度之和,最上面一行位影片名稱,已經經過加權的影片名稱(即S.x***)。上面的話用公式來解讀就是:
由表名可知,是為Toby推薦影片,那首先要計算其他評論者與Toby的相似度,然后為其推薦他沒有評價過的影片,由表知,需要推薦的影片是Night,Lady,Luck;這里以一部影片
Lady為例子,為要推薦的影片做排名,按照上述所說加權的方法,為:
評價Lady的評價者只有4個,因此首先計算總計值,總計值 = 2.5 * 0.99 + 3.0 * 0.38 + 3.0 * 0.92 + 3.0 * 0.66 = 8.38;然后計算Sim. Sum,即參與評價該影片的評價者的相似度之和,
即為0.99 + 0.38 + 0.92 + 0.66 = 2.95;最后為影片打分為:總計值/Sim. Sum = 8.38 / 2.95 = 2.83。通過計算示例則可以對該方法一目了然,對影片的排名即推薦寫為一個函數形式:
# 返回為person推薦的物品,prefs為第一步的字典數據 # n為推薦的個數(物品排名前n名), # similarity指的是相似度度量方法,這里默認為皮爾遜相關系數 def getRecommendations(prefs, person, n, similarity=sim_pearson): # 以要推薦的影片為准(這些推薦的影片都是person未評價的) totals = {} # 用來存放總計值,鍵值為 影片名:總計值 simSum = {} # 用來存放參與評價相似度之和,鍵值為 影片名:Sim. Sum # 計算相似度不能拿person自己去計算相似度,因此過濾person for otherperson in prefs: if otherperson != person: sim = similarity(prefs, person, otherperson) else: sim = 0 #為了過濾掉自己和自己計算相似度的情況 # 由於使用皮爾遜相關系數,因此要去掉小於等於0的相似度 if sim > 0: for item in prefs[otherperson]: # 過濾掉person評價過的影片 if item not in prefs[person]: totals.setdefault(item, 0) totals[item] += prefs[otherperson][item] * sim simSum.setdefault(item, 0) simSum[item] += sim # 經過上面的for循環,則得到了需要被推薦的影片的總計值以及相應的參與評價的相似度之和 rankings = [] # 用來保存推薦的影片及其排名,影片和排名構成元組存到列表中,便於排序 for item in totals: rankings.append((totals[item]/simSum[item], item))# 將影片排名值放在前面,方便排序 rankings.sort() rankings.reverse() return rankings[0: n]
這個函數里用了一個setdefault函數,setdefault(item, 0)是在字典里創建一個item:0的鍵值對,如果字典中包含item,則這條語句不起任何作用,如果不包含item,則創建item:0。
將上面的所有函數結合起來,就可以得到為某個人推薦的物品及其排名了。
以上思路主要是以基於用戶來推薦的,因此是基於用戶的推薦算法的整體流程,基於物品的推薦算法的流程又是什么呢,其實很簡單,只要把數據字典換成“{物品:{評價者:評價值}}”即可。通過一段代碼來將之前的數據轉換為物品的形式:
# prefs為{評價者:{物品:評價值}}的字典形式 # 返回{物品:{評價者:評價值}}的字典形式 def transformPrefs(prefs): itemPrefs = {} for person in prefs: for item in prefs[person]: itemPrefs.setdefault(item, {}) # 使用setdefault的特點,很容易進行字典的轉換 # 將物品和人員對調 itemPrefs[item][person] = prefs[person][item] return itemPrefs
利用這段代碼,將字典形式轉換之后,然后就可以處理基於物品的推薦算法了。
在這里要再大致說下兩個算法:基於用戶的推薦算法是找到相似的用戶,然后根據相似用戶對影片的評價數據來進行推薦的;而基於物品的推薦算法是
要找到相似的物品,然后根據被推薦者參加評價的物品或者購買過的物品的評價數據來進行推薦(根據評價記錄或者購買記錄)。
二、基於物品的推薦算法
以上思路的講解或多或小有涉及到兩者的不同之處,這里總結一下兩者的不同。
兩種算法的區別:
區別一:兩種算法的數據形式不一樣,基於用戶的推薦算法的數據形式為:{評價者:{物品:評價值}},而基於物品的推薦算法的數據形式為:{物品:{評價者:評價值}};
區別二:在以上所有的函數中的計算相似度只是將person和prefs替換為item(物品)和newprefs
區別三:基於用戶的推薦算法只要求出被推薦用戶的相似用戶即可,而基於物品的推薦算法則需要計算出每件物品的相似物品,構建一個物品相似表,則需要構建新的函數
來得到物品相似表。
區別四:基於用戶的推薦算法是利用其它相似用戶的評價數據來進行推薦;而基於物品的推薦算法是利用被推薦用戶的歷史評論數據來進行推薦的。因此在獲得推薦的函數 中,基於物品的推薦算法,表的最左邊一列是被推薦用戶的歷史評價物品名稱(而基於用戶推薦,表的最左邊一列是相似用戶名稱)。
區別五:兩種算法應用的情況不一樣,在針對大數據集生成推薦列表時,基於物品進行過濾的方式明顯要比基於用戶的過濾更快,不過它也有維護物品相似度表的額外開 銷。總體來說,對於稀疏數據集,基於物品的過濾方法通常要優於基於用戶的過濾方法;而對於密集數據集,兩者的效果幾乎是一樣的。但是,基於用戶的過濾方法更容易 實現,而且無需額外步驟,因此它通常適用於規模較小的變化非常頻繁的內存數據集。
• 由區別一以及轉換字典函數可以得出基於物品的推薦算法所需要的數據形式,將第一章第一步的字典critics代入轉換函數返回的結果即為所需的數據形式。
• 再由區別二以及相似度度量方法,將相似度度量函數中的參數person1和person2替換為item1和item2即可;
• 再根據區別三,寫一個獲得物品相似表的函數,通過該代碼則可以得到一個物品相似表,用字典形式保存。代碼如下:
# prefs指的是{評價者:{物品:評價值}}字典數據 # n指的是要求出每件物品的前n個相似物品 def calculateSimilarItems(prefs, n=10): # 建立輸出字典,以給出與這些物品最為相近的所有其他物品 simItems = {} # 使用轉換算法將數據進行轉換 itemPrefs = transformPrefs(prefs) c = 0 # 主要是針對大數據集的,用來計數(不影響物品相似表的獲取) for item in itemPrefs: # 針對大數據集更新狀態變量 c += 1 if c % 100 ==0: print("%d / %d" % (c, len(itemPrefs))) # 以上三條語句與物品相似表的獲取無關 scores = topMatches(itemPrefs, item, n) simItems[item] = scores return simItems
• 再根據區別四可知,為某個用戶推薦物品,需要根據他的歷史評價記錄數據或歷史購買記錄數據來獲得推薦的物品數據。相較於基於用戶的推薦算法的加權計算表,基於物
品的加權計算表格為如下所示:
表:為Toby推薦物品(基於物品的推薦)
影片名稱 | 評分 | Night | R.xNight | Lady | R.xLady | Luck | R.xLuck |
Snakes | 4.5 | 0.182 | 0.818 | 0.222 | 0.999 | 0.105 | 0.474 |
Superman | 4.0 | 0.103 | 0.412 | 0.091 | 0.363 | 0.065 | 0.258 |
Dupree | 1.0 | 0.148 | 0.148 | 0.4 | 0.4 | 0.182 | 0.182 |
總計 | 0.433 | 1.378 | 0.713 | 1.762 | 0.352 | 0.914 | |
歸一化結果 | 3.183 | 2.473 | 2.598 |
表中最左邊一列表示Toby曾經評價過的影片(也就是歷史評價記錄),而表最上面一行指的是Toby未曾評價過的影片以及這些影片的加權值(即R.x****)。以Night為例,Night
的排名為:總計值 = 0.182 * 4.5 + 0.103 * 4.0 + 0.148 * 1.0 = 1.378,相似度之和sim. Sum = 0.182 + 0.103 + 0.148 = 0.433,最后排名結果為:1.378 / 0.433 = 3.183。直觀
地從兩種算法的表可以看出,基於物品的推薦是固定被推薦者的參與評價的幾個影片的評價值,而未曾評價過的的影片(需要推薦的影片)則根據不同的影片相似度是不同的;
而基於用戶的推薦是固定被推薦者與幾個相似者的相似度,而未曾評價過的的影片(需要推薦的影片)則根據不同的評價者評價值是不同的。
隨后根據表格,寫出推薦物品的函數,代碼如下
# 其中prefs為{評價者:{物品:評價值}}的字典數據,目的是為了找到被推薦用戶user的評價記錄(即得到user所評價過的物品) # simItems指的是相似物品表,是calculateSimilarItems的返回結果 def getRecommendationsByItems(prefs, simItems, user): # 首先獲取user的評價表 userItems = prefs[user] totals = {} # 存儲 item:總計值 simSum = {} # 存儲 item:相似度之和 # 找到user評價過的每個物品的相似物品 # item為物品,rating為對應的評價值 # userItems.items()返回一個列表,列表元素為(物品, 評價值) for (item, rating) in userItems.items(): # 循環遍歷與當前物品item相近的物品 # simItems[item]就是一個元素為(物品,評價值)的列表 for (similarity, item2) in simItems[item]: # 要過濾掉user已經評價過的物品,保留未評價過的 if item2 not in userItems: totals.setdefault(item2, 0) simSum.setdefault(item2, 0) totals[item2] += rating * similarity simSum[item2] += similarity # 求出一個推薦的排名 rankings = [] for r_item in totals: rankings.append((totals[r_item] / simSum[r_item], r_item)) return rankings
以上所有內容即為兩個推薦算法:基於用戶的推薦和基於物品的推薦。它們兩者的使用原則之前也說過,現在重新說一下:在針對大數據集生成推薦列表時,基於物品進行過濾的方式明顯要比基於用戶的過濾更快,不過它也有維護物品相似表的額外開銷。總體來說,對於稀疏數據集,基於物品的過濾方法通常要優於基於用戶的過濾方法;而對於密集數據集,兩者的效果幾乎是一樣的。但是,基於用戶的過濾方法更容易實現,而且無需額外步驟,因此它通常適用於規模較小的變化非常頻繁的內存數據集。