《推荐系统实践》 程序实现 —— 2.4.1 基于用户的协同过滤算法

本文为《推荐系统实践-项亮》中,关于2.4.1 基于用户的协同过滤算法的程序实现python

  • 在查询本文中,关于原书某页的算法实现时,能够直接Crtl+F,按格式"PXX"输入原书页码(例:P45)查询。未添加的程序实现会在以后陆续补上。
  • 在概览程序思想时,能够将程序中全部的**print()**注释掉。在详细了解程序内容或练习的时候,建议将其中的解释性print()执行一遍 ,查看运行过程,经过中间过程,能够很好的帮助理解。
  • 程序中“”字的注释,表示此处与原书中不一样,可是思想一致,只是在python语言的表述上不一样。

源码下载:《推荐系统实践》 程序实现 —— 2.4.1 基于用户的协同过滤算法web

1 基础算法

公式介绍:
书中选择了以下两种公式,每一种均可以用于计算两个用户的兴趣类似度。算法

P45Jaccard公式

在这里插入图片描述
用一个图片,介绍这个公式:
在这里插入图片描述
因此,O越大,两我的共同喜欢的物品越多,两我的的兴趣爱好越类似。svg

P45 余弦类似度公式

在这里插入图片描述

p45 实现余弦类似度,计算两两用户的类似度

import math
W = dict()
train = {'A':{'a','b','d'},'B':{'a','c'},'C':{'b','e'},'D':{'c','d','e'}} #{'a','b','c'}是一个集合
for u in train.keys():
	for v in train.keys():
		if u == v:
			continue
			
		W.update({u:{v:len(train[u]&train[v])}})#计算分子,'&'是 Python中集合的并运算
		W[u][v]/=math.sqrt(len(train[u])*len(train[v])*1.0)#分子/分母

for u,v_similarity in W.items():
	print(u,'corresponds to',v_similarity)

执行结果函数

('A', 'corresponds to', {'D': 0.3333333333333333})
('C', 'corresponds to', {'D': 0.4082482904638631})
('B', 'corresponds to', {'D': 0.4082482904638631})
('D', 'corresponds to', {'B': 0.4082482904638631})

p46 创建物品-用户的倒排表(build inverse table for item_users)

train = dict();

train = {'A':{'a':1,'b':1,'d':1},
		 'B':{'a':1,'c':1},
		 'C':{'b':1,'e':1},
		 'D':{'c':1,'d':1,'e':1}
		}#此处物品a、b、c、d、e后面对应的值1,是用户对物品感兴趣的程度。好比'A':{'a':1},是用户A对物品a感兴趣的程度。
		 #按理说感兴趣的程度应该是不一样的数值,但此处,由于使用的是单一行为的隐反馈数据,因此全部的都定为1了。这里的感兴趣程度即书中p47页公式(用于计算用户u对物品i的感兴趣程度)中的rvi,可参考其介绍。

#一、build inverse table for item_users:经过用户-物品列表,创建物品-用户倒排表
item_users = dict()#存储物品-用户倒排表
for u,items in train.items():
	#print(u,'corresponds to',items)
	for i in items.keys():#遍历每个用户的物品列表。
		#print(i)
		if i not in item_users:
			item_users[i] = set()
		item_users[i].add(u)#物品列表中的物品i,有用户u访问过

print('输出item_users[]')
for u,items in item_users.items():
	print(u,'corresponds to',items)
print('')

执行结果测试

A corresponds to {'a': 1, 'b': 1, 'd': 1}
a
b
d
B corresponds to {'a': 1, 'c': 1}
a
c
C corresponds to {'b': 1, 'e': 1}
b
e
D corresponds to {'c': 1, 'd': 1, 'e': 1}
c
d
e
输出item_users[]
a corresponds to {'A', 'B'}
b corresponds to {'A', 'C'}
d corresponds to {'A', 'D'}
c corresponds to {'B', 'D'}
e corresponds to {'D', 'C'}

p46 创建用户类似度矩阵W(calculate co-rated items between users)

#二、calculate co-rated items between users :计算两个用户共同访问过的物品数,创建用户类似度矩阵C,C[u][v]=x,即表示u,v共同访问过的物品有x个。 
C = dict()#C[][]是一个嵌套的二维字典,eg:C = {'A':{'C':1,'B':1,'D':1}}
N = dict()#N[]统计用户有过行为的物品数,最终N[A]=3,N[B]=2,N[C]=2,N[D]=3
for i , users in item_users.items():
	print(i, 'corresponds to ', users) #示例:('a', 'corresponds to ', set(['A', 'B']))
	
	for u in users:#u遍历一遍物品i的users列表
		print(u ,'u in users')
		if u not in N.keys():
			N[u] = 0
		N[u] += 1 #统计用户u有过行为的物品数
        
		for v in users:#v遍历一遍物品i的users列表
			print(v ,'v in uers')
			if u == v:
				continue
			
			#注意:二维“字典”新添一个key-value对时,须要判断key是否已经存在了
			if u not in C.keys():
				C.update({u:{v:0}})
			
			if v not in C[u].keys():
				C[u].update({v:0})
				
			C[u][v] += 1 #用户u、v对同一个物品有过行为。通过u,v两层遍历,C[][]会成为一个对称矩阵。
			print(C)

print('\n输出N[]:')
for user,value in N.items():
	print(user,'corresponds to',value)
	
print('\n输出C[][]:')
for u,related_users in C.items():
	print(u,'corresponds to',related_users)

执行结果ui

a corresponds to  {'B', 'A'}
B u in users
B v in uers
A v in uers
{'B': {'A': 1}}
A u in users
B v in uers
{'B': {'A': 1}, 'A': {'B': 1}}
A v in uers
b corresponds to  {'C', 'A'}
C u in users
C v in uers
A v in uers
{'B': {'A': 1}, 'A': {'B': 1}, 'C': {'A': 1}}
A u in users
C v in uers
{'B': {'A': 1}, 'A': {'B': 1, 'C': 1}, 'C': {'A': 1}}
A v in uers
d corresponds to  {'D', 'A'}
D u in users
D v in uers
A v in uers
{'B': {'A': 1}, 'A': {'B': 1, 'C': 1}, 'C': {'A': 1}, 'D': {'A': 1}}
A u in users
D v in uers
{'B': {'A': 1}, 'A': {'B': 1, 'C': 1, 'D': 1}, 'C': {'A': 1}, 'D': {'A': 1}}
A v in uers
c corresponds to  {'B', 'D'}
B u in users
B v in uers
D v in uers
{'B': {'A': 1, 'D': 1}, 'A': {'B': 1, 'C': 1, 'D': 1}, 'C': {'A': 1}, 'D': {'A': 1}}
D u in users
B v in uers
{'B': {'A': 1, 'D': 1}, 'A': {'B': 1, 'C': 1, 'D': 1}, 'C': {'A': 1}, 'D': {'A': 1, 'B': 1}}
D v in uers
e corresponds to  {'C', 'D'}
C u in users
C v in uers
D v in uers
{'B': {'A': 1, 'D': 1}, 'A': {'B': 1, 'C': 1, 'D': 1}, 'C': {'A': 1, 'D': 1}, 'D': {'A': 1, 'B': 1}}
D u in users
C v in uers
{'B': {'A': 1, 'D': 1}, 'A': {'B': 1, 'C': 1, 'D': 1}, 'C': {'A': 1, 'D': 1}, 'D': {'A': 1, 'B': 1, 'C': 1}}
D v in uers

输出N[]:
B corresponds to 2
A corresponds to 3
C corresponds to 2
D corresponds to 3

输出C[][]:
B corresponds to {'A': 1, 'D': 1}
A corresponds to {'B': 1, 'C': 1, 'D': 1}
C corresponds to {'A': 1, 'D': 1}
D corresponds to {'A': 1, 'B': 1, 'C': 1}

p46 计算最终类似度矩阵W(cal)

#三、calculate final similarity matrix W :计算最终类似度矩阵W
W = dict()
#下面的两层迭代其实即C[u][v]=cuv。
for u,related_users in C.items():#u为一级坐标
	for v,cuv in related_users.items():#v为二级坐标,cuv即C[u][v]的值,即|N(u)∩N(v)|
		if u not in W.keys():
			W.update({u:{v:cuv/math.sqrt(N[u]*N[v])}})
		else:
			W[u].update({v:cuv/math.sqrt(N[u]*N[v])})

		print(W)
		
for u,related_users in W.items():
		print(u,'corresponds to ',related_users)

执行结果.net

{'B': {'A': 0.4082482904638631}}
{'B': {'A': 0.4082482904638631, 'D': 0.4082482904638631}}
{'B': {'A': 0.4082482904638631, 'D': 0.4082482904638631}, 'A': {'B': 0.4082482904638631}}
{'B': {'A': 0.4082482904638631, 'D': 0.4082482904638631}, 'A': {'B': 0.4082482904638631, 'C': 0.4082482904638631}}
{'B': {'A': 0.4082482904638631, 'D': 0.4082482904638631}, 'A': {'B': 0.4082482904638631, 'C': 0.4082482904638631, 'D': 0.3333333333333333}}
{'B': {'A': 0.4082482904638631, 'D': 0.4082482904638631}, 'A': {'B': 0.4082482904638631, 'C': 0.4082482904638631, 'D': 0.3333333333333333}, 'C': {'A': 0.4082482904638631}}
{'B': {'A': 0.4082482904638631, 'D': 0.4082482904638631}, 'A': {'B': 0.4082482904638631, 'C': 0.4082482904638631, 'D': 0.3333333333333333}, 'C': {'A': 0.4082482904638631, 'D': 0.4082482904638631}}
{'B': {'A': 0.4082482904638631, 'D': 0.4082482904638631}, 'A': {'B': 0.4082482904638631, 'C': 0.4082482904638631, 'D': 0.3333333333333333}, 'C': {'A': 0.4082482904638631, 'D': 0.4082482904638631}, 'D': {'A': 0.3333333333333333}}
{'B': {'A': 0.4082482904638631, 'D': 0.4082482904638631}, 'A': {'B': 0.4082482904638631, 'C': 0.4082482904638631, 'D': 0.3333333333333333}, 'C': {'A': 0.4082482904638631, 'D': 0.4082482904638631}, 'D': {'A': 0.3333333333333333, 'B': 0.4082482904638631}}
{'B': {'A': 0.4082482904638631, 'D': 0.4082482904638631}, 'A': {'B': 0.4082482904638631, 'C': 0.4082482904638631, 'D': 0.3333333333333333}, 'C': {'A': 0.4082482904638631, 'D': 0.4082482904638631}, 'D': {'A': 0.3333333333333333, 'B': 0.4082482904638631, 'C': 0.4082482904638631}}
输出W[][]:
B corresponds to  {'A': 0.4082482904638631, 'D': 0.4082482904638631}
A corresponds to  {'B': 0.4082482904638631, 'C': 0.4082482904638631, 'D': 0.3333333333333333}
C corresponds to  {'A': 0.4082482904638631, 'D': 0.4082482904638631}
D corresponds to  {'A': 0.3333333333333333, 'B': 0.4082482904638631, 'C': 0.4082482904638631}

P47 计算用户user,对,本身没有过行为的物品,的感兴趣的程度

#4 计算用户A对,本身没有过行为的物品,的感兴趣程度
def itemgetter(elem):
    return elem[1]

# 给用户A进行推荐,user = 'A'
user = 'A'
K = 3
rank = dict()
interacted_items = train[user]
for v, wuv in sorted(W[user].items(), key=itemgetter, reverse=True)[0:K]:
    for i, rvi in train[v].items():
        if i in interacted_items:
        	#即若是物品i,在用户user(此处为A)的,有过行为的列表中,(换句话说,用户user对物品i有过行为),将物品i过滤掉,不计算感兴趣程度。
        	continue;
        else:
        	if i not in rank.keys():
        		rank.update({i:0})
        	rank[i] += wuv*rvi

# 获得结果rank
print('输出rank[]:')
print(rank)

执行结果:code

输出rank[]:
{'c': 0.7415816237971964, 'e': 0.7415816237971964}

2 用户类似度计算的改进

Python中的math.log()函数默认以math.e为底数。[5]
经测试xml

>>> import math
>>> math.log(math.e)
1.0

参考文献

[1] Python的二维字典(two-dimension dictionary)定义与实现方法
[2]《推荐系统实践》项亮
[3]《Python基础教程 (第三版)》
[4]《流畅的Python》Luciano Ramalho
[5] math不写底数时,底数究竟是多少?-知乎