在一些大型購物網(wǎng)站我碟,我們常會(huì)看到一個(gè)功能叫“猜你喜歡”(或其它類似的名字)霹陡,里面列出一些跟你買過商品相關(guān)的其它商品憋槐。網(wǎng)站的用戶越多双藕,或你在網(wǎng)站上購買的東西越多,它往往就猜的越準(zhǔn)秦陋。在一些音樂網(wǎng)站蔓彩、書評(píng)網(wǎng)站治笨、電影網(wǎng)站也有類似的推薦系統(tǒng)驳概,比如豆瓣上的“豆瓣猜”、百度音樂的“為你推薦”等旷赖,推薦結(jié)果都不錯(cuò)顺又。
這些推薦系統(tǒng)的具體實(shí)現(xiàn)我們無法知曉,但原理是類似的等孵,都是采用基于協(xié)同過濾的推薦機(jī)制稚照。這里我們探討一下這個(gè)推薦機(jī)制的原理。
舉例
下圖是一個(gè)用戶對(duì)課程評(píng)分表。評(píng)分從1星到5星果录,灰色表示該用戶沒有對(duì)該課程評(píng)分上枕。由圖可知,用戶3沒有學(xué)過《面向?qū)ο蠡A(chǔ)》和《Struts開發(fā)框架》弱恒。問辨萍,如果要給用戶3推薦其中一門課程,應(yīng)該推薦哪一門返弹?
基本概念
相似度
如果一個(gè)用戶喜歡一種物品锈玉,那么他很可能也喜歡類似的物品。如果我們找到了測量物品之間相似程度的方法义起,也就解決了推薦系統(tǒng)的核心問題拉背。
那如何找出這些方法呢?比如默终,啤酒與芝麻醬更相似還是與紙尿褲更相似椅棺?怎么知道啤酒和紙尿布的相似度是多少?
解決這個(gè)問題之前齐蔽,不妨先考慮一個(gè)簡單的問題土陪。假設(shè)平面上有3個(gè)點(diǎn),坐標(biāo)分別是A(1,2)肴熏、B(1,3)和C(4,7)鬼雀,如圖:
AB的距離=
BC的距離=
很顯然B與A的距離小于B與C的距離,換句話說B與A更接近(相似)蛙吏。
這種用根方差計(jì)算出來的距離叫歐式距離源哩,歐式距離可以擴(kuò)展到多維空間。大于3維的空間我們想象不出來鸦做,但是算法是一樣的励烦。
如果我們有下面的數(shù)據(jù)
那么通過用歐式距離公式可知:
《機(jī)器學(xué)習(xí)》與《python編程》的距離=
為了便于理解和比較,一般將相似度的值設(shè)在0到1之間泼诱,用歐式距離d得出的相似度可以表示為:
除了用歐式距離計(jì)算相似度外坛掠,常用的方法還有皮爾遜相關(guān)系數(shù)(Pearson correlation)和余玄相似度(cosine similarity).
下面是一段python代碼,實(shí)現(xiàn)了基于歐式距離的相似度計(jì)算
``` python
from numpy import *
from numpy import linalg as la
def eSim(A,B):
return 1.0/(1.0+la.norm(A-B))
```
再添加一個(gè)加載數(shù)據(jù)的方法治筒。該方法返回一個(gè)二維數(shù)組屉栓,表示用戶對(duì)課程的評(píng)價(jià)值。
``` python
def loadData():
return[[5, 3, 0, 2, 2],
[4, 0, 0, 3, 3],
[5, 0, 0, 1, 1],
[1, 1, 1, 2, 0],
[2, 2, 2, 0, 0],
[1, 1, 1, 0, 0],
[5, 5, 5, 0, 0]]
```
推薦引擎 - 給用戶推薦最喜歡的課程
目的:給定一個(gè)用戶耸袜,程序返回N個(gè)該用戶最喜歡的課程
步驟
* 查詢用戶沒有評(píng)級(jí)的課程, 即矩陣中的0元素
* 在用戶沒有評(píng)級(jí)的所有課程中友多,對(duì)每個(gè)課程預(yù)測一個(gè)評(píng)級(jí)分?jǐn)?shù)
* 評(píng)分從高到底排序, 返回前N個(gè)課程
推薦引擎需要一個(gè)對(duì)課程評(píng)估分值的函數(shù)
``` python
'''
函數(shù)功能:在給定相似度計(jì)算方法的條件下,估計(jì)該用戶對(duì)課程的評(píng)分值
input
ds: 評(píng)價(jià)矩陣
userIdx: 用戶編號(hào)
simFunc: 相似度計(jì)算方法
courseIdx: 課程編號(hào)
output
編號(hào)為courseIdx的課程對(duì)應(yīng)的估計(jì)分值
'''
def standEst(ds, userIdx, simFunc, courseIdx):
n = shape(ds)[1] #課程數(shù)量
simTotal = 0.0; ratSimTotal = 0.0
#遍歷所有課程
for j in range(n):
userRating = ds[userIdx,j]? #用戶對(duì)第j個(gè)課程的評(píng)價(jià)
if userRating == 0: continue? #用戶沒有對(duì)該課程評(píng)分堤框,跳過
#尋找兩個(gè)用戶都評(píng)級(jí)的課程
overLap = nonzero(logical_and(ds[:,courseIdx].A>0, ds[:,j].A>0))[0]
#如果兩個(gè)課程(courseIdx和j)沒有共同評(píng)價(jià)人域滥,則相似度=0
if len(overLap) == 0: similarity = 0
#否則纵柿,計(jì)算相似度
else: similarity = simFunc(ds[overLap,courseIdx], ds[overLap,j])
#總相似度(相似度可以理解為權(quán)重)
simTotal += similarity
#相似度*評(píng)分的合計(jì)
ratSimTotal += similarity * userRating
if simTotal == 0: return 0
else: return ratSimTotal/simTotal #預(yù)計(jì)得分
```
推薦引擎代碼
``` python
'''
推薦引擎: 給用戶推薦N個(gè)最喜歡的課程
input
ds: 評(píng)價(jià)矩陣
userIdx:
N: 最高推薦N個(gè)結(jié)果
simFunc
estFunc
'''
def recommendCourses(ds, userIdx, N=3, simFunc=eSim, estFunc=standEst):
unratedCourses = nonzero(ds[userIdx,:].A==0)[1] #當(dāng)前用戶沒有打分的課程
if len(unratedCourses) == 0: return '你已經(jīng)學(xué)過所有課程'
courseScores = [] #課程分?jǐn)?shù)列表
for courseIdx in unratedCourses:
estimatedScore = estFunc(ds, userIdx, simFunc, courseIdx)
courseScores.append((courseIdx, estimatedScore))
return sorted(courseScores, key=lambda jj: jj[1], reverse=True)[:N]
```
測試函數(shù),給用戶3推薦課程
``` python
def test():
dataMat = mat(loadData())
print recommendCourses(dataMat,2)
```
執(zhí)行代碼
``` bash
>>> import recommend
>>> recommend.test()
[(2, 3.6666666666666665), (1, 2.068764098505754)]
```
推薦結(jié)果:下標(biāo)為2的課程(《Struts開發(fā)框架》)得分3.67星启绰,下標(biāo)為1的課程(《面向?qū)ο笏枷搿罚┑梅譃?星昂儒。因此,判斷用戶更喜歡《Struts開發(fā)框架》委可。
從直觀上也可以這樣理解:用戶4荆忍,5,6撤缴,7都對(duì)《Java編程》和《Struts開發(fā)框架》做了評(píng)價(jià)刹枉,而且評(píng)價(jià)相同。因此屈呕,《Struts開發(fā)框架》與《Java編程》屬于非常相似的物品微宝。 而用戶3對(duì)《Java編程》評(píng)價(jià)極高(5星),故判斷《Struts開發(fā)框架》也應(yīng)該得高分(對(duì)于用戶3而言)虎眨。
局限
* 計(jì)算量問題:這個(gè)算法需要對(duì)整個(gè)數(shù)據(jù)集進(jìn)行多次復(fù)雜的計(jì)算蟋软,如果數(shù)據(jù)量很大,則性能可能無法接受嗽桩。一種解決辦法是對(duì)矩陣進(jìn)行SVD分解岳守,把高維度的矩陣轉(zhuǎn)換成低維度度的矩陣。此外碌冶,采用離線計(jì)算湿痢,將相似度這個(gè)中間結(jié)果保存起來重復(fù)利用也可以提高性能。
* 冷啟動(dòng)問題:新課程加進(jìn)來時(shí)扑庞,由于缺乏數(shù)據(jù)無法進(jìn)行推薦譬重。這個(gè)可以通過給課程打標(biāo)簽的方式進(jìn)行。