在众多召回策略里面,基于Item与基于User(可参考:https://www.cnblogs.com/SysoCjs/p/11466424.html)在实现上非常相似。所以这里使用了跟基于User协同过滤的数据u.data。
u.data数据格式(user_id, item_id, rating, timestamp)
实现原理:
区别于User,先根据User已经购买过,或者评价过的Items,基于算法,对其他Items做一个相似度计算,来获取基于该User的Items的相似Items,这样每个User_Item都可以得到一堆相似Items,根据相似度进行排序,每个User_Item取出topN个Items,然后将所有的User_item的各自topN个Items,共同组成一个大的Item_List,Item_list中的每个item的相似度乘上对应的User_item的权重,得到新的权重值,根据新的权重值排序,取topK个Items,作为最终的Items候选集。
具体实现:
因为是使用Python来实现,所以还是选择最简单的算法:杰卡尔德算法。在User协同过滤中:通过两个用户共同拥有的物品集合数量,除以两个用户的物品的平均数量。这里,需要倒过来:通过两个item共同用户数量,除以两个item各自的用户的平均数量。所以要做的是,想方设法获取这三个参数值:共同用户数量、两个item各自的用户数量。
一、创建源数据的结构
timestamp列数据在寻找相似用户里面,意义不大,可以不使用;对于pyspark程序,u.data数据是没有结构的,所以第一时间是读取u.data,并定义数据的结构,可以将数据的结构定义为:
dict{user_id:{item_id:rating}}
import pandas as pd
def generate_train_data(nrows=10):
# 处理训练数据 -> dict{user_id:{item_id:rating}}
df = pd.read_csv('../row_data/u.data',
sep=' ',
nrows=nrows,
names=['user_id', 'item_id', 'rating', 'timestamp'])
# d为每个用户的商品及对应打分的列表
d = dict()
for _, row in df.iterrows():
# 类型转换
user_id = str(row['user_id'])
item_id = str(row['item_id'])
rating = row['rating']
if user_id not in d.keys():
d[user_id] = {item_id: rating}
else:
d[user_id][item_id] = rating
return d
二、统计杰卡尔德的参数
基于Item的协同过滤,在遍历train_data的时候,需要同时统计每个item的User数量,以及item与item之间共同的User数量,因为,相对于基于User的协同过滤,item的User数量不可以直接通过len(train_data[i])来获取,需要另外定义一个类型为dict的N来存储,N的数据结构为{item: userNum}。
N = dict() # N{item:userNum},统计item的user数量
在遍历train_data的同时,可以将N和C都收集得到:
N = dict() # N{item:userNum},统计item的user数量
def item_sim(train_data):
C = dict() # C{item:{iten:simUserNum}}
for u,items in train_data.items():
for i in items.keys():
# 对于每一个u的items,N[i]只有一个
if N.get(i,-1)==-1:
N[i] = 0
N[i] += 1
if C.get(i,-1)==-1:
C[i] = dict()
for j in items.keys():
if i==j:
continue
if C[i].get(j,-1)==-1:
C[i][j] = 0
C[i][j] += 1
return C
这个时候,共同用户数量存储在C里面,而每个item对应的用户数量存储在N里面,那么接下来可以借用杰卡尔德公式计算Item-Item相似度矩阵。
# 计算相似度矩阵,杰卡尔德算法
def item_item(C):
for i, related_items in C.items():
for j, cij in related_items.items():
C[i][j] = 2 * cij/(N[i]+N[j]*1.0)
return C
最终结果还是保存在C里面,此时C的数据结构变成:
C = dict() # C{item:{iten:sim_score}}
二、过滤,统计推荐候选集
最终是要得到Item候选集,所以先定义一个字典,用于存储item,及其对应的打分:
rank = dict() # rank{item:score}
Item候选集仍然是指代推荐给指定User的Items,一般会根据业务需求,将该User购买过或评价过的Item进行过滤,这时,需要取出该User对应的r购买过或评价过的Item列表:
Ru = train_data[user_id]
过滤掉User购买过或评价过的Item后,需要计算候选集中每个Item的打分,通过Item在该User的rating,乘上其相似Item的sim_score,得到最终打分score:
def recommendation(train_data, user_id, C, k):
rank = dict() # rank{item:score}
# 用户user_id有很多个已经购买的item,每个item都有好多个相似item,
# 每个item取k个相似item
Ru = train_data[user_id]
for i, rating in Ru.items(): # 相对于user-based,item-based的一个用户对应于多个item,所以大循环,要遍历item
for j, sim_score in sorted(C[i].items(), key=lambda x:x[1],reverse=True)[0:k]:
# 相对于user-based,每个user还要遍历各自的item,item-based则不需要,需要它的矩阵就是item-iten
# 过滤这个user已经打分过的item
if j in Ru:
continue
if rank.get(j,-1)==-1:
rank[j] = 0
rank[j] += sim_score*rating
return rank
三、根据指定User,得出其推荐Item候选集
if __name__ == '__main__':
train_data = dict()
with open(train_data_path,'r') as ft:
train_data = eval(ft.read())
C = dict()
with open(sim_item_path, 'r') as fs:
C = eval(fs.read())
user_id = '196'
k = 5
rank = recommendation(user_id, C, train_data, k)
print(sorted(rank.items(), key=lambda x: x[1], reverse=True)[0:k])