项目地址:https://github.com/ChanKamShing/UserCF_python.git
推荐系统的作业流程:
召回/match(推荐引擎)-> 物品候选集 -> 过滤 -> 排序 -> 策略(保证结果多样性) -> 推荐list
协同过滤CF属于第一阶段,我们常常称之为“推荐引擎”。“推荐引擎”可以有多个基准,包括:基于相似用户、基于相似物品、基于特征搜索,以及基于热门等方式。通过不同的方式可以解决不同的问题,譬如冷启动问题,这里介绍的是基于相似用户的方式。
在本文中,不会详细介绍代码,主要从逻辑上讲述。
基本步骤:
1、找出当前用户的若干个相似用户,取出每个相似用户购买过的商品(或打分过的电影)集合;
2、基于当前用户的购买过的商品(或打分过的电影)集合,对其相似用户购买过的商品(或打分过的电影)集合进行过滤,得出存在相似用户,同时不存在当前用户的商品(或电影)集合;
3、基于当前用户与相似用户之间的相似度,以及用户对商品(或电影)的打分,进行排序取topN,得到物品候选集
具体实现:
u.data数据格式(user_id, item_id, rating, timestamp):
一、创建源数据的结构
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
函数返回结果的结构是(196为dict()的key,下面为196的value,它内部也是一个字典类型):
二、基于杰卡尔德,计算用户相似度
最终数据的结构为:
dict{u:{v:sim_rate}}
首先,明白杰卡尔德实现相似度计算的原理:通过两个用户共同拥有的物品集合数量,除以两个用户的物品的平均数量。
根据定义原理,需要计算获取的有3个值:u和v的共同物品数量、u的商品数量、v的商品数量。
(普通版计算)
1、计算u、v的商品数量很容易得到,根据第一步得到的数据结构,直接:
len(d[u]) # u的商品数量
len(d[v]) # v的商品数量
2、如何得到u和v的共同物品数量?可以通过:分别将u、v的物品放进set集合,再将两者的set进行&运算(交集操作),运算后的set集合的元素数量就是u和v的共同物品数量。
len(set(d[u]) & set(d[v]))
3、最后进行相似度运算:
u和v的共同物品数量/[(u的商品数量+v的商品数量)/2] = 2 * u和v的共同物品数量/(u的商品数量+v的商品数量)
详细代码如下:
# 输入数据形式:train_data{u:{item:score}}
def user_normal_sim(train_data):
w = dict()
for u in train_data.keys():
if u not in w:
w[u] = dict() # 存放u用户相似的用户
for v in train_data.keys():
if u == v:
continue
# 相似度计算,杰卡尔德相似度:通过两个用户共同拥有的物品集合数量,除以两个用户的物品的平均数量
# 分别将u,v的集合转换成set形式,再互相&运算(各自去重,找两者的交集)
w[u][v] = len(set(train_data[u]) & set(train_data[v])) # 此时的w的结构为w{u:{v:sim_itemNum}}
w[u][v] = 2 * w[u][v] / (len(train_data[u])+len(train_data[v]))*1.0 # 此时的w的结构为w{u:{v:sim_rate}}
return w
经过上述步骤,可以得到每个用户,与其相似用户,以及它们之间的相似度。仔细观察,上面求相似度时,在遍历用户集合(train_data.keys())基础上,再遍历一遍用户集合,那么其运算的时间复杂度,就是O(n^2),对于实际场景,用户量是非常庞大的,相似用户则相对少数的,即每个用户购买过的商品在总的商品集中属于稀疏的,换句话说,做了很多无用的遍历,因为很多情况,遍历的u和v是完全没有共同商品。因此,可以进行一层优化。
(优化版计算,采用倒排方式:user_id->items =》 items->user_id)
1、获取倒排字典
数据形式为:item_user{item:(u)}
item_users = dict()
for u,items in train_data.items():
# 倒排操作{item:{u,v,...}}
for item in items.keys():
if item not in item_users:
# 如果item_users集合里面没有当前item,则将该item作为key加入到集合,并创建一个set集合的value
item_users[item] = set()
# 如果item_users集合里面存在当前item,则在该item所对应的set集合中添加当前遍历的user
item_users[item].add(u)
2、统计u、v之间的共同商品数量
数据的结构为:C{u:{v:simNum}}
C=dict()
for item,users in item_users.items():
for u in users:
# 获取用户与用户之间的共同item数量
if C.get(u,-1) == -1:
C[u] = dict()
for v in users: # 遍历同一个item下的users集合
if u == v:
continue
# 其实对于每一个users集合(每一行item_users),C[u][v]最多只会累加1
if C[u].get(v,-1) == -1:
C[u][v] = 1
C[u][v] += 1
C[u][v] += 1 ,加的这个1,其实就是一个共同item; 此时的C的结构为C{u:{v:sim_itemNum}}
值得注意的是,这里还有一个优化,实际场景当中,热门商品往往很多用户都会购买,不然怎么会被称为“热门”呢?!所以热门商品对单独用户来说,所带来的参考价值并不高,因此可以对其降低权重(简称,降权),降权的思路就是:越多用户购买的商品,理论上,其权重越低,这就很容易联想到使用倒数,即1/len(item_users[item]),当单纯倒数的形式会给数据带来强烈的反差,可以使用log对item_users[item]进行数据的平滑,同时log函数的值不为0,需要加1,即1/log(1+len(item_users[item]))。
上面代码在统计u、v共同商品数量时,将C[u][v] += 1改成:
# 优化:对热门商品降权 1/log(n+1) , n为购买该商品的用户量。
# 实际开发中,是对每一个商品都进行降权操作,计算相似商品数量时,累加的不是1,而是降权之后的值。
# 相当于,降权之前,物品的权重都是1,打分都一样,降权之后,每个商品都自己对应的打分。
C[u][v] += 1/math.log(1+len(item_users[item]))
3、相似度运算
数据的结构为:C{u:{v:sim_rate}}
# 计算最终相似度:u,v共同物品数量,除以(u的物品数量与v的物品数量的和的平均数)
for u, v_itemNum in C.items():
for v, itemNum in v_itemNum.items():
C[u][v] = 2*C[u][v]/float(len(train_data[u])+len(train_data[v])) # 此时的C的结构为C{u:{v:sim_rate}}
三、过滤商品,并对商品进行打分
(物品分数=用户相似度*相似用户对电影(物品)的打分)
这里是对指定用户进行操作,如果想同时对所有用户操作,只需要遍历所有即可。
数据的结构:rank{v_item:cuv*v_rating}
先取出u的购买过的商品,用于遍历v的商品时,v的商品进行过滤;然后取出topN的相似用户v,计算物品打分,相似用户如果都有同一个商品,那么u对应的商品候选集的这个商品的打分就是累加。
def recommend(user_id, C, train_data, k=5):
rank = dict() # rank={v_item:cuv*v_rating}
# 获取user_id=196的用户对应的items
interacted_items = train_data[user_id].keys()
# 取相似的top k个用户。sorted第一个参数是待排序对象,key是基于排序的基础
# C[user_id].items():196用户对应的相似用户v,以及相似度cuv的字典
# key=lambda x:x[1]:根据第二个(即cuv)排序
for v,cuv in sorted(C[user_id].items(), key=lambda x:x[1],reverse=True)[0:k]:
# 取出相似用户对应的item和rating
for v_i, v_rating in train_data[v].items():
# 过滤掉196用户已经评价过的电影(或已经购买过的物品)
if v_i in interacted_items:
continue
elif rank.get(v_i,-1) == -1:
rank[v_i] = 0
# 计算物品打分:用户相似度*相似用户对电影(物品)的打分
# 各个相似用户都评价了同一个item,那么user_id对应的这个item的可能评分是累加的
rank[v_i] += cuv*v_rating
四、取出topN的商品列表
sorted(rank.items(),key=lambda x:x[1],reverse=True)[0:N]