User协同过滤(基于Python实现)

项目地址: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='\t',
                     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]

 

posted @ 2019-09-05 15:35  KamShing  阅读(912)  评论(0编辑  收藏  举报