zoukankan      html  css  js  c++  java
  • 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='	',
                         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]
  • 相关阅读:
    PostgreSQL数据库中的常见错误
    postgresql相关命令
    Linux系统查看公网IP地址
    TCP/IP TIME_WAIT状态原理
    TCP连接状态详解及TIME_WAIT过多的解决方法
    让你提升命令行效率的 Bash 快捷键 [完整版]
    linux 如何显示一个文件的某几行(中间几行)
    linux中内核的一个不错的参数somaxconn
    Linux crontab 实现每秒执行
    Linux tar This does not look like a tar archive
  • 原文地址:https://www.cnblogs.com/SysoCjs/p/11466424.html
Copyright © 2011-2022 走看看