基於鄰域的協同過濾
此篇使用樸素的程式碼介紹基於鄰域的協同過濾演算法機制。
為了使說明過程更清楚,這裡使用自已編造的資料。每一行記錄著某使用者對某本書的評分,評分割槽間為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_idbook_idrating 0user_001book_0014 1user_001book_0023 2user_001book_0055 3user_002book_0015 4user_002book_0034 ----- user_count:6 book_count:6 複製程式碼
生成使用者物品關係矩陣
現在根據載入進來的資料生成推薦系統中至關重要的使用者物品關係矩陣。可以理解為資料庫中的一張表,一本書為一列,一行對應一個使用者,當用戶看過某本書並進行評分後,在對應的位置填入分數,其他位置均置為0,表示尚未看過。
需要注意的是,矩陣取值要用下標表示,比如 matrix[2][2]
對應的是第三個使用者對第三本書的評分情況,所以這裡要做一個 user_id
, book_id
到該矩陣座標的對應關係,使用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 ('使用者關係矩陣長這樣:') print ('-----') print (user_book_matrix) 複製程式碼
使用者關係矩陣長這樣: ----- [[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]] 複製程式碼
計算相似度矩陣
所謂相似度,我們這裡使用餘弦相似度,其他的還有皮爾遜相關度、歐式距離、傑卡德相似度等,箇中差別暫不細表。 計算公式為:

現在已經拿到了 user_book_matrix
,每個使用者、每個物品都可以對應一個向量,比如 user_book_matrix[2]
為代表 user_003
的向量等於 [4, 0, 5, 3, 4, 0]
,而 user_book_matrix[:,2]
則代表了 book_003
: [0, 4, 5, 0, 0, 2]
。
這樣基於使用者和基於物品便分別可以計算出使用者相似度矩陣與物品相似度矩陣。
以使用者相似度矩陣為例,計算後會得到一個形狀為(user_count, user_count)的矩陣,比如 user_similarity_matrix[2][3]
的值為0.5,則表示 user_002
與 user_003
的餘弦相似度為0.5。 此矩陣為對稱矩陣,相應地, 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.30.] [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.30.0.0.97 1.0.53] [0.0.16 0.40.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.]] 複製程式碼
推薦
有了相似度矩陣,可以開始進行推薦。
首先可以為使用者推薦與其品味相同的使用者列表,這在知乎、豆瓣、網易雲音樂這樣具有社交屬性的產品中很有意義。
做法很簡單,要為使用者A推薦K位品味相似的使用者(此處K取3),則將 user_similarity_matrix
中關於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 複製程式碼
同時在物品維度,類似的推薦也是很有用的,比如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個使用者,然後找出這K個使用者評過分的書籍的集合,再去掉該使用者已經評過分的部分。 在剩下的書籍中,根據下面的公式,計算出該使用者為某書籍的預計評分,將評分從高到低排序輸出即可。

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 # 相似使用者未看過該書則不計入統計. 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') 複製程式碼
34.0 22.0 dtype: float64 複製程式碼
以上是利用使用者相似度矩陣來為使用者推薦物品,同樣也可以反過來為利用物品相似度矩陣來為使用者推薦書籍。
做法是,找出該使用者讀過的所有書,為每本書找出兩本與該書最相似的書籍,將找出來的所有書去掉使用者已讀過的,然後為書籍預測被使用者評分的分值。
這裡的確有些繞,容易與上文纏在一起搞亂掉,遂舉例如下:
比如 user_001
讀過書 book_001
, book_002
, book_005
,找到的書籍集合再去掉使用者已讀過的結果為 {'book_003', 'book_006'}
,要為 book_003
預測分數,需要注意到它同時被 book_001
與 book_005
找出,要根據它們、使用者對 book_001
與 book_005
的評分以及相似度套用至上文公式,來得出對 book_003
的分數為: (4*0.79+5*0.71)/(0.79+0.71)=4.47
。
則基於物品為使用者推薦物品的函式為:
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') 複製程式碼
24.47 53.00 dtype: float64 複製程式碼
總結
以上是基於領域的協同過濾的運作機制介紹,只用了兩個簡單的數學公式,加上各種程式碼處理,便可以為使用者做出一些推薦。
就給使用者推薦物品而言,基於使用者與基於物品各有特點。
基於使用者給出的推薦結果,更依賴於當前使用者相近的使用者群體的社會化行為,考慮到計算代價,它適合於使用者數較少的情況,同時,對於新加入的物品的冷啟動問題比較友好,然而相對於物品的相似性,根據使用者之間的相似性做出的推薦的解釋性是比較弱的,實時性方面,使用者新的行為不一定會導致結果的變化。 基於物品給出的推薦結果,更側重於使用者自身的個體行為,適用於物品數較少的情況,對長尾物品的發掘好於基於使用者,同時,新加入的使用者可以很快得到推薦,並且物品之間的關聯性更易懂,是更易於解釋的,而且使用者新的行為一定能導致結果的變化。
顯然,基於物品總體上要優於基於使用者,歷史上,也的確是基於使用者先被髮明出來,之後Amazon發明了基於物品的演算法,現在基於使用者的產品已經比較少了。