基于鄰域的協(xié)同過濾

此篇使用樸素的代碼介紹基于鄰域的協(xié)同過濾算法機(jī)制。

為了使說明過程更清楚,這里使用自已編造的數(shù)據(jù)。每一行記錄著某用戶對某本書的評分,評分區(qū)間為1至5。

import pandas as pd

data_url = 'https://gist.githubusercontent.com/guerbai/3f4964350678c84d359e3536a08f6d3a/raw/f62f26d9ac24d434b1a0be3b5aec57c8a08e7741/user_book_ratings.txt'
df = pd.read_csv(data_url, 
            sep = ',',
            header = None,                   
            names = ['user_id', 'book_id', 'rating'])
print (df.head())
print ('-----')
user_count = df['user_id'].unique().shape[0]
book_count = df['book_id'].unique().shape[0]
print ('user_count: ', user_count)
print ('book_count: ', book_count)
    user_id   book_id  rating
0  user_001  book_001       4
1  user_001  book_002       3
2  user_001  book_005       5
3  user_002  book_001       5
4  user_002  book_003       4
-----
user_count:  6
book_count:  6

生成用戶物品關(guān)系矩陣

現(xiàn)在根據(jù)加載進(jìn)來的數(shù)據(jù)生成推薦系統(tǒng)中至關(guān)重要的用戶物品關(guān)系矩陣。可以理解為數(shù)據(jù)庫中的一張表,一本書為一列,一行對應(yīng)一個(gè)用戶,當(dāng)用戶看過某本書并進(jìn)行評分后,在對應(yīng)的位置填入分?jǐn)?shù),其他位置均置為0,表示尚未看過。

需要注意的是,矩陣取值要用下標(biāo)表示,比如matrix[2][2]對應(yīng)的是第三個(gè)用戶對第三本書的評分情況,所以這里要做一個(gè)user_id, book_id到該矩陣坐標(biāo)的對應(yīng)關(guān)系,使用pandas的Series表示。

user_id_index_series = pd.Series(range(user_count), index=['user_001', 'user_002', 'user_003', 'user_004', 'user_005', 'user_006'])
book_id_index_series = pd.Series(range(book_count), index=['book_001', 'book_002', 'book_003', 'book_004', 'book_005', 'book_006'])
import numpy as np

def construct_user_item_matrix(df):
    user_item_matrix = np.zeros((user_count, user_count), dtype=np.int8)
    for row in df.itertuples():
        user_id = row[1]
        book_id = row[2]
        rating = row[3]
        user_item_matrix[user_id_index_series[user_id], book_id_index_series[book_id]] = rating
    return user_item_matrix

user_book_matrix = construct_user_item_matrix(df)
print ('用戶關(guān)系矩陣長這樣:')
print ('-----')
print (user_book_matrix)
用戶關(guān)系矩陣長這樣:
-----
[[4 3 0 0 5 0]
 [5 0 4 0 4 0]
 [4 0 5 3 4 0]
 [0 3 0 0 0 5]
 [0 4 0 0 0 4]
 [0 0 2 4 0 5]]

計(jì)算相似度矩陣

所謂相似度,我們這里使用余弦相似度,其他的還有皮爾遜相關(guān)度、歐式距離、杰卡德相似度等,個(gè)中差別暫不細(xì)表。
計(jì)算公式為:

現(xiàn)在已經(jīng)拿到了user_book_matrix,每個(gè)用戶、每個(gè)物品都可以對應(yīng)一個(gè)向量,比如user_book_matrix[2]為代表user_003的向量等于[4, 0, 5, 3, 4, 0],而user_book_matrix[:,2]則代表了book_003[0, 4, 5, 0, 0, 2]

這樣基于用戶和基于物品便分別可以計(jì)算出用戶相似度矩陣與物品相似度矩陣。

以用戶相似度矩陣為例,計(jì)算后會(huì)得到一個(gè)形狀為(user_count, user_count)的矩陣,比如user_similarity_matrix[2][3]的值為0.5,則表示user_002user_003的余弦相似度為0.5。
此矩陣為對稱矩陣,相應(yīng)地,user_similarity_matrix[3][2]亦為0.5,而用戶與自己自然是最相似的,遂有user_similarity_matrix[n][n]總是等于1。

def cosine_similarity(vec1, vec2):
    return round(vec1.dot(vec2)/(np.linalg.norm(vec1)*np.linalg.norm(vec2)), 2)


def construct_similarity_matrix(user_item_matrix, dim='user'):
    if dim == 'user':
        similarity_matrix = np.zeros((user_count, user_count))
        count = user_count
    else:
        similarity_matrix = np.zeros((book_count, book_count))
        count = book_count
    get_vector = lambda i: user_item_matrix[i] if dim == 'user' else user_item_matrix[:,i]
    for i in range(user_count):
        i_vector = get_vector(i)
        similarity_matrix[i][i] = cosine_similarity(i_vector, i_vector)
        for j in range(i, count):
            j_vector = get_vector(j)
            similarity = cosine_similarity(i_vector, j_vector)
            similarity_matrix[i][j] = similarity
            similarity_matrix[j][i] = similarity
    return similarity_matrix

user_similarity_matrix = construct_similarity_matrix(user_book_matrix)
book_similarity_matrix = construct_similarity_matrix(user_book_matrix, dim='book')
print ('user_similarity_matrix:')
print (user_similarity_matrix)
print ('book_similarity_matrix:')
print (book_similarity_matrix)
user_similarity_matrix:
[[1.   0.75 0.63 0.22 0.3  0.  ]
 [0.75 1.   0.91 0.   0.   0.16]
 [0.63 0.91 1.   0.   0.   0.4 ]
 [0.22 0.   0.   1.   0.97 0.64]
 [0.3  0.   0.   0.97 1.   0.53]
 [0.   0.16 0.4  0.64 0.53 1.  ]]
book_similarity_matrix:
[[1.   0.27 0.79 0.32 0.98 0.  ]
 [0.27 1.   0.   0.   0.34 0.65]
 [0.79 0.   1.   0.69 0.71 0.18]
 [0.32 0.   0.69 1.   0.32 0.49]
 [0.98 0.34 0.71 0.32 1.   0.  ]
 [0.   0.65 0.18 0.49 0.   1.  ]]

推薦

有了相似度矩陣,可以開始進(jìn)行推薦。
首先可以為用戶推薦與其品味相同的用戶列表,這在知乎、豆瓣、網(wǎng)易云音樂這樣具有社交屬性的產(chǎn)品中很有意義。

做法很簡單,要為用戶A推薦K位品味相似的用戶(此處K取3),則將user_similarity_matrix中關(guān)于A的那一行的值排序從最高往下取出K位即可。

def recommend_similar_users(user_id, n=3):
    user_index = user_id_index_series[user_id]
    similar_users_index = pd.Series(user_similarity_matrix[user_index]).drop(index=user_index).sort_values(ascending=False).index[:n]
    return np.array(similar_users_index)

print ('recommend user_indexes %s to user_001' % recommend_similar_users('user_001'))
recommend user_indexes [1 2 4] to user_001

同時(shí)在物品維度,類似的推薦也是很有用的,比如QQ音樂給用戶正在聽的音樂推薦相似的歌曲,還有亞馬遜中對用戶剛購買的物品推薦相似的物品。
代碼與推薦相似用戶相同,無需做其他處理。

def recommend_similar_items(item_id, n=3):
    item_index = book_id_index_series[item_id]
    similar_item_index = pd.Series(book_similarity_matrix[item_index]).drop(index=item_index).sort_values(ascending=False).index[:n]
    return np.array(similar_item_index)
    
print ('recommend item_indexes %s to book_001' % recommend_similar_items('book_001'))
recommend item_indexes [4 2 3] to book_001

接下來是為用戶推薦書籍,首先選出與該用戶最相似的K個(gè)用戶,然后找出這K個(gè)用戶評過分的書籍的集合,再去掉該用戶已經(jīng)評過分的部分。
在剩下的書籍中,根據(jù)下面的公式,計(jì)算出該用戶為某書籍的預(yù)計(jì)評分,將評分從高到低排序輸出即可。

def recommend_item_to_user(user_id):
    user_index = user_id_index_series[user_id]
    similar_users = recommend_similar_users(user_id, 2)
    recommend_set = set()
    for similar_user in similar_users:
        recommend_set = recommend_set.union(np.nonzero(user_book_matrix[similar_user])[0])
    recommend_set = recommend_set.difference(np.nonzero(user_book_matrix[user_index])[0])
    predict = pd.Series([0.0]*len(recommend_set), index=list(recommend_set))
    for book_index in recommend_set:
        fenzi = 0
        fenmu = 0
        for j in similar_users:
            if user_book_matrix[j][book_index] == 0:
                continue # 相似用戶未看過該書則不計(jì)入統(tǒng)計(jì).
            fenzi += user_book_matrix[j][book_index] * user_similarity_matrix[j][user_index]
            fenmu += user_similarity_matrix[j][user_index]
        if fenmu == 0:
            continue
        predict[book_index] = round(fenzi/fenmu, 2)
    return predict.sort_values(ascending=False)
            

recommend_item_to_user('user_005')
3    4.0
2    2.0
dtype: float64

以上是利用用戶相似度矩陣來為用戶推薦物品,同樣也可以反過來為利用物品相似度矩陣來為用戶推薦書籍。
做法是,找出該用戶讀過的所有書,為每本書找出兩本與該書最相似的書籍,將找出來的所有書去掉用戶已讀過的,然后為書籍預(yù)測被用戶評分的分值。

這里的確有些繞,容易與上文纏在一起搞亂掉,遂舉例如下:
比如user_001讀過書book_001, book_002,book_005,找到的書籍集合再去掉用戶已讀過的結(jié)果為{'book_003', 'book_006'},要為book_003預(yù)測分?jǐn)?shù),需要注意到它同時(shí)被book_001book_005找出,要根據(jù)它們、用戶對book_001book_005的評分以及相似度套用至上文公式,來得出對book_003的分?jǐn)?shù)為:(4*0.79+5*0.71)/(0.79+0.71)=4.47

則基于物品為用戶推薦物品的函數(shù)為:

def recommend_item_to_user_ib(user_id):
    user_index = user_id_index_series[user_id]
    user_read_books = np.nonzero(user_book_matrix[user_index])[0]
    book_set = set()
    book_relation = dict()
    for book in user_read_books:
        relative_books = recommend_similar_items(book, 2)
        book_set = book_set.union(relative_books)
        book_relation[book] = relative_books
    book_set = book_set.difference(user_read_books)
    predict = pd.Series([0.0]*len(book_set), index=list(book_set))
    for book in book_set:
        fenzi = 0
        fenmu = 0
        for similar_book, relative_books in book_relation.items():
            if book in relative_books:
                fenzi += book_similarity_matrix[book][similar_book] * user_book_matrix[user_index][similar_book]
                fenmu += book_similarity_matrix[book][similar_book]
        predict[book] = round(fenzi/fenmu, 2)
    return predict.sort_values(ascending=False)

recommend_item_to_user_ib('user_001')
2    4.47
5    3.00
dtype: float64

總結(jié)

以上是基于領(lǐng)域的協(xié)同過濾的運(yùn)作機(jī)制介紹,只用了兩個(gè)簡單的數(shù)學(xué)公式,加上各種代碼處理,便可以為用戶做出一些推薦。

就給用戶推薦物品而言,基于用戶與基于物品各有特點(diǎn)。
基于用戶給出的推薦結(jié)果,更依賴于當(dāng)前用戶相近的用戶群體的社會(huì)化行為,考慮到計(jì)算代價(jià),它適合于用戶數(shù)較少的情況,同時(shí),對于新加入的物品的冷啟動(dòng)問題比較友好,然而相對于物品的相似性,根據(jù)用戶之間的相似性做出的推薦的解釋性是比較弱的,實(shí)時(shí)性方面,用戶新的行為不一定會(huì)導(dǎo)致結(jié)果的變化。
基于物品給出的推薦結(jié)果,更側(cè)重于用戶自身的個(gè)體行為,適用于物品數(shù)較少的情況,對長尾物品的發(fā)掘好于基于用戶,同時(shí),新加入的用戶可以很快得到推薦,并且物品之間的關(guān)聯(lián)性更易懂,是更易于解釋的,而且用戶新的行為一定能導(dǎo)致結(jié)果的變化。

顯然,基于物品總體上要優(yōu)于基于用戶,歷史上,也的確是基于用戶先被發(fā)明出來,之后Amazon發(fā)明了基于物品的算法,現(xiàn)在基于用戶的產(chǎn)品已經(jīng)比較少了。

參考

Intro to Recommender Systems: Collaborative Filtering
【近鄰?fù)扑]】人以群分,你是什么人就看到什么世界
架構(gòu)師特刊:推薦系統(tǒng)(理論篇)
美團(tuán)推薦算法實(shí)踐

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容