两个月前我开始学习机器学习,这两个月期间学习了一些基础的机器学习算法及其Python实现。这周我刚开始学习《推荐系统实践》这本书,并打算以后定期将自己的学习情况做个简单的总结。这份总结是我这个博客上的第一篇文章。这周的学习的主要内容是基于邻域的协同过滤算法。基于用户行为分析的推荐算法是个性化推荐系统的重要算法,学术界一般将这种类型的算法称为协同过滤算法。
协同过滤,从名字就可以看出这种方法的本质是先有协同(用户齐心协力)再有过滤(每个用户的推荐列表能够过滤掉他不喜欢的物品)。对协同过滤算法,学术届提出了了很多种方法,比如基于邻域的方法(neighborhood-based)、隐语义模型(laten factor model)、基于图的随机游走算法(random walk on graph)等。
基于邻域的方法主要包括两种算法:基于用户的协同过滤算法(UserCF)和基于物品的协同过滤算法(ItemCF)。
UserCF的本质就是我要给你推荐物品,我先找到与你兴趣最相似的K个用户,然后把他们喜欢的而你却没产生过行为的物品推荐给你。当然不是把所有的他们喜欢的且你没产生过行为的都推荐给你,而是推荐你最感兴趣的 N个产品。根据上面这些描述,我们看到使用 UserCF需要两步:
在第一步中,我们需要去计算你与每个用户的兴趣相似度,然后按相似度降序排列,所对应的前K个用户就是我们要找的。
对于用户u和v我们可以通过如下的Jaccard公式来计算:
wuv=∣N(u)∩N(v)∣∣N(u)∪N(v)∣ ,
或者通过余弦相似度计算:
wuv=∣N(u)∩N(v)∣∣N(u)∪N(v)∣√ ,
计算相似度的时候,为了减少时间复杂度,需要建立一个物品-用户倒排表。因为当用户数很大时,直接从数据集很费时间,很多时间浪费在了计算 ∣N(u)∪N(v)∣=0 上。另外,两个用户对冷门物品采取过同样的行为更能说明他们兴趣的相似度。所以书中指出 John S. Breese 在论文中提出如下计算相似度的公式:
train={'user1':['a', 'b', 'c'], 'user2':['a','b'],....,'userN':[...]}]
W={('user1', 'user2'): w12, ('user1', 'user3'): w13,.....}
下面是计算相似度的代码
def UserSimilarity2(train, flag=1):
item_users = dict()
for u, item in train.items():
for i in item:
if i not in item_users:
item_users[i] = set() # 生成一个集合
item_users[i].add(u)
C = dict() # C为分子
N = dict() # N[u]表示用户u喜欢的物品个数
""
#用户数很大时计算C耗时
for u in train.keys():
for v in train.keys():
if u==v:
continue
else:
if len(set(train[u] ) & set(train[v]))!=0:
C[(u, v)]=len(set(train[u] ) & set(train[v]))
for u in train.keys():
N[u]=len(train[u])
print C
print N
""
for item, users in item_users.items():
for u in users:
if u not in N:
N[u] = 1 # 如果用户u不在字典N里面,先创建
else:
N[u] += 1
for v in users:
if u != v:
if flag == 0: # 修正前
if (u, v) not in C:
C[(u, v)] = 1
else:
C[(u, v)] += 1
elif flag == 1: # 修正后
if (u, v) not in C:
C[(u, v)] = 1 / log(1 + len(users))
else:
C[(u, v)] += 1 / log(1 + len(users))
W = dict()
for uv in C.keys():
# pdb.set_trace()
u = uv[0]
v = uv[1]
if u not in W:
W[u] = set()
# 添加与用户u相关的用户v,第二个意思是他们的权重Wuv
W[u].add((v, C[uv] / sqrt(N[u] * N[v])))
return W
这个代码中 flag=0对应的是第一个没有修正的相似度计算公式,flag=1对应的就是修正后的相似度计算公式。
计算出相似度后,下来要做的就是找出与你相似度高的前K个用户,然后去把他们喜欢的你没发生过行为的物品(记为 Itemj )却很感兴趣的物品推荐给你。
第二步和第一步的思路差不多,定性问题定量化。计算你对 Itemj 的感兴趣程度,然后降序排列,选出感兴趣程度最高的前N个推荐给你。
这里,感兴趣程度是用下面的公式来度量的:
下面是实现推荐的代码:
def Recommend(user, train, W, N, K=10):
rank = dict()
interacted_items = train[user]
for v, wuv in sorted(W[user], key=lambda x: x[1], reverse=True)[0:K]: # 相似度最大的K个用户
for i in train[v]: # v产生过行为的物品
if i not in interacted_items:
if i not in rank:
rank[i] = wuv * 1
else:
rank[i] += wuv * 1
rank = sorted(rank.items(), key=lambda x: x[1], reverse=True)
rank = rank[:N]
return rank
这里的K是一个很关键的参数,K的选取会影响最后的推荐结果。这本书中K对TopN推荐的好坏,是用这几个指标来衡量的:准确率,召回率,覆盖率 和 流行度。
记U为系统的用户集合,R(u)为训练集上得出的推荐列表, T(u)为测试集上的行为列表。
推荐结果的准确率可定义为:
def PrecisionRecall(train, test, N):
hit = 0 #两者的分子
n_precision=0 # 准确率的分母
n_recall = 0 #召回率的分母
W = UserSimilarity2(train)
for user in train.keys():
try: # 计算分子分母
te_user_items = test[user]
recomRank = Recommend(user, train, W, N)
for recom_item, w in recomRank:
if recom_item in te_user_items:
hit += 1
n_precision += N
n_recall+=len(te_user_items)
except:
pass
return [hit / (1.0*n_precision), hit/(1.0*n_recall)]
覆盖率的计算公式为:
def Coverage(train, N):
recommend_items = set() #分子
all_items = set() #分母
W = UserSimilarity2(train)
for user in train.keys():
for item in train[user]:
all_items.add(item)
rank = Recommend(user, train, W, N)
for item in rank[0]:
recommend_items.add(item)
return len(recommend_items) / (len(all_items) * 1.0)
这本书上,目前看的这一章,我没有看到流行度的具体定义,但从给的代码看,似乎平均流行度是这么定义的:
def Popularity(train,N):
item_popularity=dict()
for user, items in train.items():
for item in items:
if item not in item_popularity:
item_popularity[item]=0
item_popularity[item]+=1
ret=0
n=0
W = UserSimilarity2(train, flag=1)
for user in train.keys():
rank=Recommend(user, train, W, N, K=10)
for item, pui in rank:
ret +=log(1+ item_popularity[item])
n +=1
ret /=n*1.0
return ret