基於協同過濾的推薦引擎(理論部分)

記得原來和朋友猜測過網易雲的推薦是怎麼實現的,大概的猜測有兩種:一種是看你聽過的和收藏過的音樂,再看和你一樣聽過這些音樂的人他們喜歡聽什麼音樂,把他喜歡的你沒聽過的音樂推薦給你;另一種是看他聽過的音樂或者收藏的音樂中大部分是什麼類型,然後把那個類型的音樂推薦給他。當然這些都只是隨便猜測。但是能發現一個問題,第二種想法很依賴於推薦的東西本身的屬性,比如一個音樂要打幾個類型的標籤,屬性的粒度會對推薦的準確性產生較大影響。今天看了協同過濾後發現其實整個算法大概和第一種的思想差不多,它最大的特點就是忽略了推薦的東西本身的屬性,而是根據其他用戶對它的喜好程度進行推薦的。

我在網上隨便找了個電影評分數據集——Movielens電影評分數據,選擇recommended for education and development裏面的ml-latest-small.zip,打算邊學邊用這個數據集做測試並看看效果。寫在一篇太長了,下一篇發實戰。也可以用其他數據集,這個博客有幾個推薦引擎測試數據彙總。

整個算法學習是通過圖靈程序設計叢書的《Machine Learning in Action》學習的。


什麼是協同過濾

協同過濾(collaborative filtering)是通過將用戶和其他用戶的數據進行對比來實現推薦的算法。


協同過濾流程圖

協同過濾流程圖.jpg

圖用Grafio 3畫的,看着好有成就感哈哈哈哈哈哈哈:-):-):-),但是上傳了以後這個分辨率好像低了T.T。


相似度

相似度計算就是看兩個物品(或用戶)有多相似,拿電影來說,可能會比較類型、導演、地區等等,但是在協同過濾裏,不關心這些屬性,嚴格地按照許多用戶的觀點來計算相似度。比如下面的電影和用戶評分矩陣:

電影_用戶矩陣.png

相似度計算

歐氏距離(euclidean metric)

歐氏距離指在m維空間中兩個點之間的真實距離,或者向量的自然長度(即該點到原點的距離)。在二維和三維空間中的歐氏距離就是兩點之間的實際距離,就是那個“根號下橫座標差的平方加縱座標差的平方”。
假設我們看《海洋奇緣》和《一條狗的使命》的相似度,《海洋奇緣》列向量是(4, 3,1),《一條狗的使命》列向量是(4,3,2),做差再平方最後開根號。
一條狗與海洋奇緣的歐氏距離.png
Python實現代碼:

import numpy as np
def eulid_sim(colA, colB):
    return 1.0/(1.0 + np.linalg.norm(colA - colB))

代碼解析:
numpy的線性代數(Linear algebra)庫linalg裏有一個norm函數,用於求範數(normal form),如果不指定範數的階數,就默認指2範數,就是向量各個元素平方和開根號,比如向量A=(4,2,2),向量A的2範數||A||=根號下(4^2+2^2+2^2)(簡書不支持LaTeX真是不方便……),所以np.linalg.norm(colA - colB))就是向量A和B的歐式距離了。
1.0/(1.0 + 歐式距離)的作用是使相似度的值在0到1之間變化,越相似,相似度的值越大,距離爲0時,相似度爲1。

皮爾遜相關係數

皮爾遜相關係數.jpg
資料參考這裏
- 皮爾遜相關係數
皮爾遜相關係數可以用來度量兩個向量之間的相似度,比歐氏距離好的一點是它對用戶評級不敏感,比如某個狂躁者對所有電影評分都是5,一個憂鬱者對所有電影評分都是1,皮爾遜相關係數會認爲這兩個向量相等。看最後一個公式,對比兩個向量的餘弦公式,長得挺像,據說皮爾遜係數是兩組向量的餘弦
- Z分數
z分數(z-score),也叫標準分數(standard score)是一個數與平均數的差再除以標準差的過程。
z分數可以回答這樣一個問題:一個給定分數(指評分)距離平均數多少個標準差?在平均數之上的分數會得到一個正的標準分數,在平均數之下的分數會得到一個負的標準分數。
z分數是一種可以看出某分數在分佈中相對位置的方法。z分數能夠真實的反應一個分數距離平均數的相對標準距離。如果我們把每一個分數都轉換成z分數,那麼每一個z分數會以標準差爲單位表示一個具體分數到平均數的距離或離差。
- Python代碼實現

def pearson_sim(colA, colB):
     if len(colA) < 3:
        return 1.0
     return 0.5 + 0.5 * np.corrcoef(colA, colB, rowvar=0)[0][1]
  • 代碼解析
    len(colA) < 3是檢查是否有3個或更多的點,如果不存在,則返回1,兩向量完全相關。
    corrcoef(colA, colB, rowvar=0)返回的是變量的相關係數矩陣,第[0][1]個元素是相關係數,rowvar=0代表列是variables。API在這裏
    0.5 + 0.5 *皮爾遜相關係數目的也是將取值範圍歸一化到0~1之間,皮爾遜相關係數的取值範圍是-1~1,所以用0.5+0.5*係數的方式歸一化。
餘弦相似度


餘弦相似度就是計算兩個向量夾角的餘弦值,如果夾角爲90度,則相似度爲0;如果方向相同,相似度爲1。因爲餘弦值的範圍也是-1~1,所以需要用同樣的方法進行歸一化。
前面說過,||A||代表向量的2範數。
Python代碼

def cos_sim(colA, colB):
    num = float(colA.T * colB)
    denom = np.linalg.norm(colA) * np.linalg.norm(colB)
    return 0.5 + 0.5 * (num / denom)

相似度選擇

計算兩個電影之間的距離,是基於物品(item-based)的相似度,計算用戶的距離,是基於用戶(user-based)的相似度。到底使用哪種相似度,取決於用戶和物品的數量。基於物品的相似度會隨着物品增加而增加,基於用戶的相似度會隨着用戶的增加而增加。如果用戶很多,則傾向於物品相似度計算方法。對於大部分推薦引擎而言,用戶數目往往大於物品數目,所以一般用物品相似度。


構建評分估計函數(就是預估用戶會給要推薦的電影打多少分)

對於用戶沒看過的電影,我們要預測他會爲這些電影打多少分,然後把排名前N個的電影推薦給他。

一般評分估計

流程圖:
一般評分預估算法流程圖.PNG

# 計算某個物品和所有其他物品的相似度,進行累加,連評分也累加,最後用累加的總評分/總相似度得到預測該用戶對新物品的評分
# data_mat:物品-用戶矩陣
# user:用戶編號
# item:要預測評分的物品編號
# sim_meas:相似度計算方法 
def stand_est(data_mat, user, item, sim_meas):
    n = np.shape(data_mat)[1] # 取第1軸的元素個數,在這裏也就是列數
    sim_total = 0.0
    rat_sim_total = 0.0
    # 遍歷整行的每個元素
    for j in range(n): 
        user_rating = data_mat[user, j] # 取一個評分
        if user_rating == 0: # 如果用戶沒有評分,就跳過這個物品
            continue 
        # 找出要預測評分的物品列和當前取的物品j列裏評分都不爲0的下標(也就是所有評過這兩個物品的用戶對這兩個物品的評分)
        overlap = np.nonzero(np.logical_and(data_mat[:, item].A > 0, data_mat[:, j].A >0))[0]
        # 如果預測評分的物品列(假設叫列向量A)和當前取的物品j列(假設叫列向量B)沒有都非零的項(也就是說兩組向量裏要麼A評分B沒評分,要麼B評分A沒評分),則認爲相似度爲0,否則,計算相似度
        if len(overlap) == 0:
            similarity = 0
        else:
            similarity = sim_meas(data_mat[overlap, item], data_mat[overlap, j]) # 注意overlap是一個array,所以這裏還是傳的兩個列向量,兩個元素中都沒有0的列向量
        print('the %d and %d similarity is %f' % (item, j, similarity))
        sim_total += similarity # 累加相似度
        rat_sim_total += similarity * user_rating # 累加相似度*評分,
    if sim_total == 0:
        return 0
    else:
        return rat_sim_total / sim_total # 總評分/總相似度,除以總相似度是爲了歸一化,將評分歸到相似度的範圍(比如0~5)

代碼解析:
這裏比較難理解的就是overlap一句,data_ma[:,item]代表取矩陣中編號爲item的那一列,.A操作是將返回值變爲ndarray,data_ma[:,item].A>0會產生一個shape相同的布爾型矩陣,根據是否大於零置True或False,logical_and方法對兩個布爾矩陣求邏輯與,nonzero方法找出邏輯與後非零值的下標。
整個過程的作用就是從兩個物品列中曬出兩物品都被評分的行的下標,用於相似度計算。
爲了搞清楚overlap一句的作用,我做了如下測試:

data_mat = np.mat([[1, 2, 0, 0, 0],[6, 7, 8, 1, 10]])
a = data_mat[:,0]
b = data_mat[:,3]
print(a) # [[1] [6]]
print(a.A) # [[1] [6]] a.A和a長得一樣,有什麼差別呢?打印類型看看
print(type(a)) # <class 'numpy.matrixlib.defmatrix.matrix'>
print(type(a.A)) # <type 'numpy.ndarray'> 查看API,是這樣寫的:return 'self' as an'ndarray' object
print(type(a.A1)) # <type 'numpy.ndarray'> 查看API,是這樣寫的:return 'self' as a flattened 'ndarray'
print(a.A > 0) # [[ True][ True]]
print(b.A > 0) # [[ False][ True]]
print(np.logical_and(a.A > 0, b.A > 0)) # [[False][ True]] 每個值做邏輯與運算
print(np.nonzero(np.logical_and(a.A > 0, b.A > 0))) # (array([1]), array([0])) np.nonzero,return the indices of the elements that are non-zero.第0個元素是第0軸的下標,第1個元素是第1軸的下標
print(np.nonzero(np.logical_and(a.A > 0, b.A > 0))[0]) # [1] 因爲是二維單列向量,取第0軸下標就行 
SVD評分估計

流程圖:

svd評分預測流程圖.png

# 用svd將矩陣變換到低維空間,再給出預估評分
# data_mat:物品-用戶矩陣
# user:用戶編號
# item:物品編號
# sim_meas:相似度計算方法
def svd_est(data_mat, user, item, sim_meas):
    n = np.shape(data_mat)[1]
    sim_total = 0.0
    rat_sim_total = 0.0
    u, sigma, vt = np.linalg.svd(data_mat) # 對原數據矩陣做svd操作
    sig4 = np.mat(np.eye(4) * sigma[:4]) # sigma[:4]是取sigma矩陣的前四個,python爲了節省空接,sigma矩陣存成了行向量,所以通過eye(4)將其變回對角矩陣
    x_formed_items = data_mat.T * u[:,:4] * sig4.I # 利用u矩陣將其轉換到低維空間,I操作是矩陣的逆.
    for j in range(n):
        user_rating = data_mat[user, j]
        if user_rating == 0 or j == item:
            continue
        similarity = sim_meas(x_formed_items[item, :].T, x_formed_items[j,:].T) #行向量轉成列向量
        print('the %d and %d similarity is %f' % (item, j, similarity))
        sim_total += similarity
        rat_sim_total += similarity * user_rating
    if sim_total == 0:
        return 0
    else:
        return rat_sim_total / sim_total

代碼解析:
sig4取4個奇異值不是必須的,需要根據原數據矩陣看,要找的是包含原矩陣90%能量的個數的奇異值。x_formed_items一句,假設原數據矩陣的shape是(m,n),則u[:,4]的shape是(m,4),sig4的shape是(4,4),逆也是(4,4),想成後得到的x_formed_items的shape是(n,4),原來的n變成了行,我們求相似度傳的是一個個的列向量,所以轉置。


推薦

流程圖:
推薦流程圖.png

# data_mat:數據矩陣
# user:用戶編號
# N: 要返回的前N個要推薦的items
# sim_meas: 相似度方法
# est_method:評分預估法
def recommend(data_mat, user, N, sim_meas, est_method):
    un_rated_items = np.nonzero(data_mat[user, :].A == 0)[1] # 上面說過,nonzero的第1個元素是第1軸的下標,這裏也就是列下標
    if len(un_rated_items) == 0:
        return 'you rated everything'
    item_scores = []
    for item in un_rated_items:
        estimate_score = est_method(data_mat, user, item, sim_meas)
        item_scores.append((item, estimate_score))
    return sorted(item_scores, key=lambda jj:jj[1], reverse=True)[:N] # 排序,key=lambda jj:jj[1]表示按每個元素的下標爲1的參數從大到小排序,取前N個

評價

由於推薦引擎建好後既沒有預測的目標值,也沒有用戶來調查他們對推薦的滿意程度,所以常常將某些已知的評分值去掉,然後對它們進行預測,計算預測值和真實值之間的差異。
通常用於推薦引擎評價的指標是最小均方根誤差(Root Mean Squared Error,RMSE),先計算均方誤差的平均值,在取平方根。如果評級在1~5星級,我們得到的RMSE爲1,則和真實評價差了1個星級。

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