在推薦系統衆多方法中,基於用戶的協同過濾推薦算法是最早誕生的,原理也較爲簡單。該算法1992年提出並用於郵件過濾系統,兩年後1994年被 GroupLens 用於新聞過濾。一直到2000年,該算法都是推薦系統領域最著名的算法。
本文簡單介紹基於用戶的協同過濾算法思想以及原理,最後基於該算法實現園友的推薦,即根據你關注的人,爲你推薦博客園中其他你有可能感興趣的人。基本思想
俗話說“物以類聚、人以羣分”,拿看電影這個例子來說,如果你喜歡《蝙蝠俠》、《碟中諜》、《星際穿越》、《源代碼》等電影,另外有個人也都喜歡這些電影,而且他還喜歡《鋼鐵俠》,則很有可能你也喜歡《鋼鐵俠》這部電影。
所以說,當一個用戶 A 需要個性化推薦時,可以先找到和他興趣相似的用戶羣體 G,然後把 G 喜歡的、並且 A 沒有聽說過的物品推薦給 A,這就是基於用戶的系統過濾算法。原理
根據上述基本原理,我們可以將基於用戶的協同過濾推薦算法拆分爲兩個步驟:
1. 找到與目標用戶興趣相似的用戶集合
2. 找到這個集合中用戶喜歡的、並且目標用戶沒有聽說過的物品推薦給目標用戶
1. 發現興趣相似的用戶
通常用 Jaccard 公式或者餘弦相似度計算兩個用戶之間的相似度。設 N(u) 爲用戶 u 喜歡的物品集合,N(v) 爲用戶 v 喜歡的物品集合,那麼 u 和 v 的相似度是多少呢:
Jaccard 公式:
餘弦相似度:
假設目前共有4個用戶: A、B、C、D;共有5個物品:a、b、c、d、e。用戶與物品的關係(用戶喜歡物品)如下圖所示:
如何一下子計算所有用戶之間的相似度呢?爲計算方便,通常首先需要建立“物品—用戶”的倒排表,如下圖所示:
然後對於每個物品,喜歡他的用戶,兩兩之間相同物品加1。例如喜歡物品 a 的用戶有 A 和 B,那麼在矩陣中他們兩兩加1。如下圖所示:
計算用戶兩兩之間的相似度,上面的矩陣僅僅代表的是公式的分子部分。以餘弦相似度爲例,對上圖進行進一步計算:
到此,計算用戶相似度就大功告成,可以很直觀的找到與目標用戶興趣較相似的用戶。
2. 推薦物品
首先需要從矩陣中找出與目標用戶 u 最相似的 K 個用戶,用集合 S(u, K) 表示,將 S 中用戶喜歡的物品全部提取出來,並去除 u 已經喜歡的物品。對於每個候選物品 i ,用戶 u 對它感興趣的程度用如下公式計算:
其中 rvi 表示用戶 v 對 i 的喜歡程度,在本例中都是爲 1,在一些需要用戶給予評分的推薦系統中,則要代入用戶評分。
舉個例子,假設我們要給 A 推薦物品,選取 K = 3 個相似用戶,相似用戶則是:B、C、D,那麼他們喜歡過並且 A 沒有喜歡過的物品有:c、e,那麼分別計算 p(A, c) 和 p(A, e):
看樣子用戶 A 對 c 和 e 的喜歡程度可能是一樣的,在真實的推薦系統中,只要按得分排序,取前幾個物品就可以了。
在社交網絡的推薦中,“物品”其實就是“人”,“喜歡一件物品”變爲“關注的人”,這一節用上面的算法實現給我推薦 10 個園友。
1. 計算 10 名與我興趣最相似的園友
由於只是爲我一個人做用戶推薦,所以沒必要建立一個龐大的用戶兩兩之間相似度的矩陣了,與我興趣相似的園友只會在這個羣體產生:我關注的人的粉絲。除我自己之外,目前我一共關注了23名園友,這23名園友一共有22936個唯一粉絲,我對這22936個用戶逐一計算了相似度,相似度排名前10的用戶及相似度如下:
暱稱 |
關注數量 |
共同數量 |
相似度 |
藍楓葉1938 |
5 |
4 |
0.373001923296126 |
FBI080703 |
3 |
3 |
0.361157559257308 |
魚非魚 |
3 |
3 |
0.361157559257308 |
Lauce |
3 |
3 |
0.361157559257308 |
藍色蝸牛 |
3 |
3 |
0.361157559257308 |
shanyujin |
3 |
3 |
0.361157559257308 |
Mr.Huang |
6 |
4 |
0.340502612303499 |
對世界說你好 |
6 |
4 |
0.340502612303499 |
strucoder |
28 |
8 |
0.31524416249564 |
Mr.Vangogh |
4 |
3 |
0.312771621085612 |
2. 計算對推薦園友的興趣度
這10名相似用戶一共推薦了25名園友,計算得到興趣度並排序:
排序 |
暱稱 |
興趣度 |
1 |
wolfy |
0.373001923296126 |
2 |
Artech |
0.340502612303499 |
3 |
Cat Chen |
0.340502612303499 |
4 |
WXWinter(冬) |
0.340502612303499 |
5 |
DanielWise |
0.340502612303499 |
6 |
一路前行 |
0.31524416249564 |
7 |
Liam Wang |
0.31524416249564 |
8 |
usharei |
0.31524416249564 |
9 |
CoderZh |
0.31524416249564 |
10 |
博客園團隊 |
0.31524416249564 |
11 |
深藍色右手 |
0.31524416249564 |
12 |
Kinglee |
0.31524416249564 |
13 |
Gnie |
0.31524416249564 |
14 |
riccc |
0.31524416249564 |
15 |
Braincol |
0.31524416249564 |
16 |
滴答的雨 |
0.31524416249564 |
17 |
Dennis Gao |
0.31524416249564 |
18 |
劉冬.NET |
0.31524416249564 |
19 |
李永京 |
0.31524416249564 |
20 |
浪端之渡鳥 |
0.31524416249564 |
21 |
李濤 |
0.31524416249564 |
22 |
阿不 |
0.31524416249564 |
23 |
JK_Rush |
0.31524416249564 |
24 |
xiaotie |
0.31524416249564 |
25 |
Leepy |
0.312771621085612 |
只需要按需要取相似度排名前10名就可以了,不過看起來整個列表的推薦質量都還不錯!
具體代碼實現:
#-*- coding: utf-8 -*-
'''''
Created on 2015-06-22
@author: Lockvictor
'''
import sys
import random
import math
import os
from operator import itemgetter
from collections import defaultdict
random.seed(0)
'''''
users.dat 數據集
用戶id 用戶性別 用戶年齡 用戶職業 用戶所在地郵編
1::F::1::10::48067
2::M::56::16::70072
3::M::25::15::55117
movies.dat 數據集
電影id 電影名稱 電影類型
250::Heavyweights (1994)::Children's|Comedy
251::Hunted, The (1995)::Action
252::I.Q. (1994)::Comedy|Romance
ratings.dat 數據集
用戶id 電影id 用戶評分 時間戳
157::3519::4::1034355415
157::2571::5::977247494
157::300::3::977248224
'''
class UserBasedCF(object):
''''' TopN recommendation - User Based Collaborative Filtering '''
# 構造函數,用來初始化
def __init__(self):
# 定義 訓練集 測試集 爲字典類型
self.trainset = {}
self.testset = {}
# 訓練集用的相似用戶數
self.n_sim_user = 20
# 推薦電影數量
self.n_rec_movie = 10
self.user_sim_mat = {}
# 表示電影的流行度,有一個看過該電影,流行度+1,沒有人看過,流行度的值默認爲0
self.movie_popular = {}
# 記錄電影數量
self.movie_count = 0
# sys.stderr 是用來重定向標準錯誤信息的
print ('相似用戶數目爲 = %d' % self.n_sim_user, file=sys.stderr)
print ('推薦電影數目爲 = %d' %
self.n_rec_movie, file=sys.stderr)
# 加載文件
@staticmethod
def loadfile(filename):
''''' load a file, return a generator. '''
# 以只讀的方式打開傳入的文件
fp = open(filename, 'r')
# enumerate()爲枚舉,i爲行號從0開始,line爲值
for i, line in enumerate(fp):
# yield 迭代去下一個值,類似next()
# line.strip()用於去除字符串頭尾指定的字符。
yield line.strip('\r\n')
# 計數
if i % 100000 == 0:
print ('loading %s(%s)' % (filename, i), file=sys.stderr)
fp.close()
# 打印加載文件成功
print ('load %s succ' % filename, file=sys.stderr)
# 劃分訓練集和測試集 pivot用來定義訓練集和測試集的比例
def generate_dataset(self, filename, pivot=0.7):
''''' load rating data and split it to training set and test set '''
trainset_len = 0
testset_len = 0
for line in self.loadfile(filename):
# 根據 分隔符 :: 來切分每行數據
user, movie, rating, _ = line.split('::')
# 隨機數字 如果小於0.7 則數據劃分爲訓練集
if random.random() < pivot:
# 設置訓練集字典,key爲user,value 爲字典 且初始爲空
self.trainset.setdefault(user, {})
# 以下省略格式如下,集同一個用戶id 會產生一個字典,且值爲他評分過的所有電影
#{'1': {'914': 3, '3408': 4, '150': 5, '1': 5}, '2': {'1357': 5}}
self.trainset[user][movie] = int(rating)
trainset_len += 1
else:
self.testset.setdefault(user, {})
self.testset[user][movie] = int(rating)
testset_len += 1
# 輸出切分訓練集成功
print ('劃分數據爲訓練集和測試集成功!', file=sys.stderr)
# 輸出訓練集比例
print ('訓練集數目 = %s' % trainset_len, file=sys.stderr)
# 輸出測試集比例
print ('測試集數目 = %s' % testset_len, file=sys.stderr)
# 建立物品-用戶 倒排表
def calc_user_sim(self):
''''' calculate user similarity matrix '''
# build inverse table for item-users
# key=movieID, value=list of userIDs who have seen this movie
print ('構建物品-用戶倒排表中,請等待......', file=sys.stderr)
movie2users = dict()
# Python 字典(Dictionary) items() 函數以列表返回可遍歷的(鍵, 值) 元組數組
for user, movies in self.trainset.items():
for movie in movies:
# inverse table for item-users
if movie not in movie2users:
# 根據電影id 構造set() 函數創建一個無序不重複元素集
movie2users[movie] = set()
# 集合中值爲用戶id
# 數值形如
# {'914': {'1','6','10'}, '3408': {'1'} ......}
movie2users[movie].add(user)
# 記錄電影的流行度
if movie not in self.movie_popular:
self.movie_popular[movie] = 0
self.movie_popular[movie] += 1
print ('構建物品-用戶倒排表成功', file=sys.stderr)
# save the total movie number, which will be used in evaluation
self.movie_count = len(movie2users)
print ('總共被操作過的電影數目爲 = %d' % self.movie_count, file=sys.stderr)
# count co-rated items between users
usersim_mat = self.user_sim_mat
print ('building user co-rated movies matrix...', file=sys.stderr)
# 令係數矩陣 C[u][v]表示N(u)∩N(v) ,假如用戶u和用戶v同時屬於K個物品對應的用戶列表,就有C[u][v]=K
for
, users in movie2users.items():
for u in users:
usersim_mat.setdefault(u, defaultdict(int))
for v in users:
if u == v:
continue
usersim_mat[u][v] += 1
print ('build user co-rated movies matrix succ', file=sys.stderr)
# calculate similarity matrix
print ('calculating user similarity matrix...', file=sys.stderr)
# 記錄計算用戶興趣相似度的次數
simfactor_count = 0
# 計算用戶興趣相似度複雜度上限值
PRINT_STEP = 2000000
# 循環遍歷usersim_mat 根據餘弦相似度公式計算出用戶興趣相似度
for u, related_users in usersim_mat.items():
for v, count in related_users.items():
# 以下是公式計算過程
usersim_mat[u][v] = count / math.sqrt(
len(self.trainset[u]) * len(self.trainset[v]))
#計數 並沒有什麼卵用
simfactor_count += 1
if simfactor_count % PRINT_STEP == 0:
print ('calculating user similarity factor(%d)' %
simfactor_count, file=sys.stderr)
print ('calculate user similarity matrix(similarity factor) succ',
file=sys.stderr)
print ('Total similarity factor number = %d' %
simfactor_count, file=sys.stderr)
# 根據用戶給予推薦結果
def recommend(self, user):
'''''定義給定K個相似用戶和推薦N個電影'''
K = self.n_sim_user
N = self.n_rec_movie
# 定義一個字典來存儲爲用戶推薦的電影
rank = dict()
# 使用watched_movies來表示用戶看過的電影列表,後續在做推薦電影需要排除掉用戶看過的電影
watched_movies = self.trainset[user]
# sorted() 函數對所有可迭代的對象進行排序操作。 key 指定比較的對象 ,reverse=True 降序,這裏user_sim_mat好像應該換成usersim_mat
for similar_user, similarity_factor in sorted(self.user_sim_mat[user].items(),
key=itemgetter(1), reverse=True)[0:K]:
for movie in self.trainset[similar_user]:
# 判斷 如果這個電影 該用戶已經看過 則跳出循環
if movie in watched_movies:
continue
# 記錄用戶對推薦的電影的興趣度,這裏的興趣度也是根據該用戶與推薦用戶的相似度來定的
rank.setdefault(movie, 0)
rank[movie] += similarity_factor
# return the N best movies
return sorted(rank.items(), key=itemgetter(1), reverse=True)[0:N]
# 計算 準確率,召回率,覆蓋率,流行度
def evaluate(self):
''''' print evaluation result: precision, recall, coverage and popularity '''
print ('Evaluation start...', file=sys.stderr)
N = self.n_rec_movie
# varables for precision and recall
#記錄推薦正確的電影數
hit = 0
#記錄推薦電影的總數
rec_count = 0
#記錄測試數據中總數
test_count = 0
# varables for coverage
all_rec_movies = set()
# varables for popularity
popular_sum = 0
for i, user in enumerate(self.trainset):
if i % 500 == 0:
print ('recommended for %d users' % i, file=sys.stderr)
test_movies = self.testset.get(user, {})
rec_movies = self.recommend(user)
for movie, _ in rec_movies:
if movie in test_movies:
hit += 1
all_rec_movies.add(movie)
popular_sum += math.log(1 + self.movie_popular[movie])
rec_count += N
test_count += len(test_movies)
# 計算準確度
precision = hit / (1.0 * rec_count)
# 計算召回率
recall = hit / (1.0 * test_count)
# 計算覆蓋率
coverage = len(all_rec_movies) / (1.0 * self.movie_count)
#計算流行度
popularity = popular_sum / (1.0 * rec_count)
print ('precision=%.4f\trecall=%.4f\tcoverage=%.4f\tpopularity=%.4f' %
(precision, recall, coverage, popularity), file=sys.stderr)
if __name__ == '__main__':
ratingfile = os.path.join('ml-1m', 'ratings.dat')
usercf = UserBasedCF()
usercf.generate_dataset(ratingfile)
usercf.calc_user_sim()
'''''
以下爲用戶id 爲 1688的用戶推薦的電影
a = usercf.recommend("1688")
[('1210', 3.1260082382168055), ('2355', 3.0990860017403934), ('1198', 2.692208437663706), ('1527', 2.643102457311887), ('3578', 2.61895974438311), ('1376', 2.469905776632142), ('110', 2.4324588006133383), ('1372', 2.4307454264036528), ('1240', 2.424265305355254), ('32', 2.3926144836965966)]
'''
usercf.evaluate()