《推薦系統實踐》——基於物品的協同過濾演算法(程式碼實現)
一、基礎演算法
基於物品的協同過濾演算法(簡稱ItemCF)給使用者推薦那些和他們之前喜歡的物品相似的物品。不過ItemCF不是利用物品的內容計算物品之間相似度,而是利用使用者的行為記錄。
該演算法認為,物品A和物品B具有很大的相似度是因為喜歡物品A的使用者大都也喜歡物品B。這裡蘊含一個假設,就是每個使用者的興趣都侷限在某幾個方面,因此如果兩個物品屬於同一個使用者的興趣列表,那麼這兩個物品可能就屬於有限的幾個領域。而如果兩個物品同時出現在很多使用者的興趣列表,那麼它們可能就屬於同一領域,因而具有很大的相似度。
從上述概念出發,定義物品i和j的相似度為
其中,是喜歡物品i的使用者數,是同時喜歡物品i和物品j的使用者數。分母是懲罰物品i和j的權重,因此懲罰了熱門物品和很多物品相似的可能性。
在得到物品相似度之後,ItemCF通過以下公式計算使用者u對未產生行為的物品j的感興趣程度。
這裡的是使用者喜歡的物品集合,是和物品j最相似的K個物品的集合,是物品j和i的相似度,是使用者u對物品j的興趣評分(簡單的,如果使用者u對物品i有過行為,即可令=1)
下面舉一個例子說明(只考慮使用者有歷史購買行為的物品)。
使用者A購買物品a b d,使用者B購買物品b c e,使用者C購買物品c d,使用者D購買物品b c d,使用者E購買物品a d。
user | item |
---|---|
A | a b d |
B | b c e |
C | c d |
D | b c d |
E | a d |
資料集格式為(使用者,=1,物品),每行記錄都是唯一的,興趣評分由決定。
uid_score_bid = ['A,1,a','A,1,b','A,1,d','B,1,b','B,1,c','B,1,e','C,1,c','C,1,d','D,1,b','D,1,c','D,1,d','E,1,a','E,1,d']
import math
class ItemBasedCF:
def __init__(self,train_file):
self.train_file = train_file
self.readData()
def readData(self):
#讀取檔案,並生成資料集(使用者,興趣程度,物品)
self.train = dict()
for line in self.train_file:
user,score,item = line.strip().split(",")
self.train.setdefault(user,{})
self.train[user][item] = int(float(score))
print (self.train) #輸出資料集
def ItemSimilarity(self):
C = dict() #物品-物品的共現矩陣
N = dict() #物品被多少個不同使用者購買
for user,items in self.train.items():
for i in items.keys():
N.setdefault(i,0)
N[i] += 1 #物品i出現一次就計數加一
C.setdefault(i,{})
for j in items.keys():
if i == j : continue
C[i].setdefault(j,0)
C[i][j] += 1 #物品i和j共現一次就計數加一
print ('N:',N)
print ('C:',C)
#計算相似度矩陣
self.W = dict()
for i,related_items in C.items():
self.W.setdefault(i,{})
for j,cij in related_items.items():
self.W[i][j] = cij / (math.sqrt(N[i] * N[j])) #按上述物品相似度公式計算相似度
for k,v in self.W.items():
print (k+':'+str(v))
return self.W
#給使用者user推薦前N個最感興趣的物品
def Recommend(self,user,K=3,N=10):
rank = dict() #記錄user的推薦物品(沒有歷史行為的物品)和興趣程度
action_item = self.train[user] #使用者user購買的物品和興趣評分r_ui
for item,score in action_item.items():
for j,wj in sorted(self.W[item].items(),key=lambda x:x[1],reverse=True)[0:K]: #使用與物品item最相似的K個物品進行計算
if j in action_item.keys(): #如果物品j已經購買過,則不進行推薦
continue
rank.setdefault(j,0)
rank[j] += score * wj #如果物品j沒有購買過,則累計物品j與item的相似度*興趣評分,作為user對物品j的興趣程度
return dict(sorted(rank.items(),key=lambda x:x[1],reverse=True)[0:N])
#宣告一個ItemBased推薦的物件
Item = ItemBasedCF(uid_score_bid)
Item.ItemSimilarity()
recommedDic = Item.Recommend("A") #計算給使用者A的推薦列表
for k,v in recommedDic.items():
print (k,"\t",v )
輸出結果:
資料集self.train
{'A': {'a': 1, 'd': 1, 'b': 1}, 'E': {'a': 1, 'd': 1}, 'D': {'d': 1, 'b': 1, 'c': 1}, 'B': {'e': 1, 'b': 1, 'c': 1}, 'C': {'d': 1, 'c': 1}}
物品被多少個不同使用者購買
N: {'e': 1, 'a': 2, 'd': 4, 'b': 3, 'c': 3}
物品-物品的共現矩陣
C: {'e': {'b': 1, 'c': 1}, 'a': {'d': 2, 'b': 1}, 'd': {'a': 2, 'b': 2, 'c': 2}, 'b': {'a': 1, 'd': 2, 'e': 1, 'c': 2}, 'c': {'e': 1, 'd': 2, 'b': 2}}
物品相似矩陣
a:{'d': 0.7071067811865475, 'b': 0.4082482904638631}
d:{'a': 0.7071067811865475, 'b': 0.5773502691896258, 'c': 0.5773502691896258}
e:{'b': 0.5773502691896258, 'c': 0.5773502691896258}
c:{'d': 0.5773502691896258, 'e': 0.5773502691896258, 'b': 0.6666666666666666}
b:{'a': 0.4082482904638631, 'd': 0.5773502691896258, 'e': 0.5773502691896258, 'c': 0.6666666666666666}
使用者A的推薦列表
e 0.5773502691896258
c 1.2440169358562925
二、使用者活躍度對物品相似度的影響
在ItemCF中,兩個物品產生相似度是因為它們共同出現在很多使用者的興趣列表中。假設有這麼一個使用者,他是開書店的,並且買了噹噹網上 80% 的書準備用來自己賣。那麼,
他的購物車裡包含噹噹網 80% 的書。所以這個使用者對於他所購買書的兩兩相似度的貢獻應該遠遠小於一個只買了十幾本自己喜歡的書的文學青年。
提出一個稱為 IUF ( Inverse User Frequence ),即使用者活躍度對數的倒數的引數,來修正物品相似度的計算公式。認為活躍使用者對物品相似度的貢獻應該小於不活躍的使用者。
三、物品相似度的歸一化
對於已經得到的物品相似度矩陣w,按照以下公式對w進行按列歸一化,不僅可以增加推薦的準確度,它還可以提高推薦的覆蓋率和多樣性。
假設物品分為兩類—— A 和 B , A 類物品之間的相似度為 0.5 , B 類物品之間的相似度為 0.6 ,而 A 類物品和 B 類物品之間的相似度是 0.2 。在這種情況下,如果一個使用者喜歡了 5個 A 類物品和 5 個 B 類物品,用 ItemCF 給他進行推薦,推薦的就都是 B 類物品,因為 B 類物品之間的相似度大。但如果歸一化之後, A 類物品之間的相似度變成了 1 , B 類物品之間的相似度也是 1 ,那麼這種情況下,使用者如果喜歡 5 個 A 類物品和 5 個 B類物品,那麼他的推薦列表中 A 類物品和 B 類物品的數目也應該是大致相等的。從這個例子可以看出,相似度的歸一化可以提高推薦的多樣性。
一般來說,熱門的類其類內物品相似度一般比較大。如果不進行歸一化,就會推薦比較熱門的類裡面的物品,而這些物品也是比較熱門的。因此,推薦的覆蓋率就比較低。相反,如果進行相似度的歸一化,則可以提高推薦系統的覆蓋率。
結合二,三改進演算法
def ItemSimilarity(self):
C = dict()
N = dict()
for user,items in self.train.items():
for i in items.keys():
N.setdefault(i,0)
N[i] += 1
C.setdefault(i,{})
for j in items.keys():
if i == j : continue
C[i].setdefault(j,0)
#C[i][j] += 1 #基礎演算法
C[i][j] += 1/math.log(1+len(items)*1.0) #改進第一點
print ('N:',N)
print ('C:',C)
#計算相似度矩陣
self.W = dict()
self.W_max = dict() #記錄每一列的最大值
for i,related_items in C.items():
self.W.setdefault(i,{})
for j,cij in related_items.items():
self.W_max.setdefault(j,0.0)#
self.W[i][j] = cij / (math.sqrt(N[i] * N[j]))
if self.W[i][j]>self.W_max[j]:#
self.W_max[j]=self.W[i][j] #記錄第j列的最大值,按列歸一化
print('W:',self.W)
for i,related_items in C.items(): #
for j,cij in related_items.items(): #
self.W[i][j]=self.W[i][j] / self.W_max[j] #
print ('W_max:',self.W_max)
for k,v in self.W.items():
print (k+':'+str(v))
return self.W
輸出結果:
物品相似度矩陣W(歸一化之前)
W: {'a': {'b': 0.2944888920518062, 'd': 0.576853026474115}, 'c': {'b': 0.4808983469629878, 'd': 0.470998523813926, 'e': 0.4164701851078906}, 'b': {'a': 0.2944888920518062, 'c': 0.4808983469629878, 'd': 0.4164701851078906, 'e': 0.4164701851078906}, 'd': {'a': 0.576853026474115, 'c': 0.470998523813926, 'b': 0.4164701851078906}, 'e': {'c': 0.4164701851078906, 'b': 0.4164701851078906}}
矩陣W的每列最大值
W_max: {'a': 0.576853026474115, 'c': 0.4808983469629878, 'b': 0.4808983469629878, 'd': 0.576853026474115, 'e': 0.4164701851078906}
物品相似度矩陣W(歸一化之後)
a:{'b': 0.6123724356957947, 'd': 1.0}
c:{'b': 1.0, 'd': 0.8164965809277261, 'e': 1.0}
b:{'a': 0.5105093993383438, 'c': 1.0, 'd': 0.721969316263228, 'e': 1.0}
d:{'a': 1.0, 'c': 0.9794138964885573, 'b': 0.8660254037844387}
e:{'c': 0.8660254037844387, 'b': 0.8660254037844387}
使用者A的推薦列表
c 1.9794138964885573
e 1.0
四、評估指標
TopN推薦:是根據使用者在訓練集上的行為給使用者做出的推薦列表,是使用者在測試集上的行為列表。
推薦結果的召回率:
推薦結果的準確率:
首先,將使用者行為資料集按照均勻分佈隨機分成 M份(本文取 M =5 ),挑選一份作為測試集,將剩下的 M -1 份作為訓練集。然後在訓練集上建立使用者興趣模型,並在測試集上對使用者行為進行預測,統計出相應的評測指標。為了保證評測指標並不是過擬合的結果,需要進行 M 次實驗,並且每次都使用不同的測試集。然後將 M 次實驗測出的評測指標的平均值作為最終的評測指標。
每次實驗選取不同的 k ( 0 ≤ k ≤ M - 1 )和相同的隨機數種子 seed ,進行 M 次實驗就可以得到 M 個不同的訓練集和測試集。
如果資料集夠大,模型夠簡單,為了快速通過離線實驗初步地選擇演算法,也可以只進行一次實驗。
#-*-coding:utf-8 -*-
import math
import numpy as np
import random
#進行5折交叉驗證,計算平均準確率和召回率
class ItemBasedCF:
def __init__(self,data_file):
self.data_file = data_file
def splitData(self,k,M=5,seed=1):
self.train_file = []
self.test_file = []
random.seed(seed)
for line in open(self.data_file):
if random.randint(0,M)==k:
self.test_file.append(line)
else:
self.train_file.append(line)
def readData(self,file):
#讀取檔案,並生成使用者-物品的評分表
self.data_dict = dict() #使用者-物品的評分表
for line in file:
tmp = line.strip().split(" ")
if len(tmp)<3: continue
user,score,item = tmp[:3]
self.data_dict.setdefault(user,{})
self.data_dict[user][item] = int(float(score))
return self.data_dict
def ItemSimilarity(self):
#同上
#給使用者user推薦
def Recommend(self,user,K=10,N=100):
#同上
#宣告一個ItemBased推薦的物件
uid_score_bid='/home/lady/tmp/liushuang/1.9Item-basedCF/data/buy_user_spu.1210_0109'
Item = ItemBasedCF(uid_score_bid)#讀取資料集
M=5
pre_lst=[]
rec_lst=[]
for k in range(M): #進行5次交叉驗證
Item.splitData(k,M,seed=1)
Item.train=Item.readData(Item.train_file)
Item.test=Item.readData(Item.test_file)
Item.ItemSimilarity() #計算物品相似度矩陣
recommedDic = dict()
hit = 0
n_pre = 0
n_rec = 0
print '訓練集數量',len(Item.train)
print '測試集數量',len(Item.test)
for user in Item.train.keys():
recommedDic[user] = Item.Recommend(user) #對於訓練user生成推薦列表
n_pre+=len(recommedDic[user])
rec_item=set()
for item in recommedDic[user]:
rec_item.add(item[0])
#測試user真實購買的商品
buy_item=set()
if user not in Item.test: continue
for item in Item.test[user].keys():
buy_item.add(item)
hit += len(rec_item & buy_item)
for user in Item.test:
n_rec += len(Item.test[user])
pre = hit/(1.0*n_pre)
rec = hit/(1.0*n_rec)
pre_lst.append(pre)
rec_lst.append(rec)
print k,' hit:',hit,'n_pre:',n_pre,'n_rec;',n_rec
print pre_lst,'平均:',np.mean(pre_lst)
print rec_lst,'平均:',np.mean(rec_lst)