一、协同过滤算法的原理及实现

协同过滤推荐算法是诞生最早,并且较为著名的推荐算法。主要的功能是预测和推荐。算法通过对用户历史行为数据的挖掘发现用户的偏好,基于不同的偏好对用户进行群组划分并推荐品味相似的商品。协同过滤推荐算法分为两类,分别是基于用户的协同过滤算法(user-based collaboratIve filtering),和基于物品的协同过滤算法(item-based collaborative filtering)。简单的说就是:人以类聚,物以群分。下面我们将分别说明这两类推荐算法的原理和实现方法。

  1.基于用户的协同过滤算法(user-based collaboratIve filtering)

  基于用户的协同过滤算法是通过用户的历史行为数据发现用户对商品或内容的喜欢(如商品购买,收藏,内容评论或分享),并对这些喜好进行度量和打分。根据不同用户对相同商品或内容的态度和偏好程度计算用户之间的关系。在有相同喜好的用户间进行商品推荐。简单的说就是如果A,B两个用户都购买了x,y,z三本图书,并且给出了5星的好评。那么A和B就属于同一类用户。可以将A看过的图书w也推荐给用户B。

  1.1寻找偏好相似的用户

  我们模拟了5个用户对两件商品的评分,来说明如何通过用户对不同商品的态度和偏好寻找相似的用户。在示例中,5个用户分别对两件商品进行了评分。这里的分值可能表示真实的购买,也可以是用户对商品不同行为的量化指标。例如,浏览商品的次数,向朋友推荐商品,收藏,分享,或评论等等。这些行为都可以表示用户对商品的态度和偏好程度。

  从表格中很难直观发现5个用户间的联系,我们将5个用户对两件商品的评分用散点图表示出来后,用户间的关系就很容易发现了。在散点图中,Y轴是商品1的评分,X轴是商品2的评分,通过用户的分布情况可以发现,A,C,D三个用户距离较近。用户A(3.3 6.5)和用户C(3.6 6.3),用户D(3.4 5.8)对两件商品的评分较为接近。而用户E和用户B则形成了另一个群体。

  散点图虽然直观,但无法投入实际的应用,也不能准确的度量用户间的关系。因此我们需要通过数字对用户的关系进行准确的度量,并依据这些关系完成商品的推荐。

  1.2欧几里德距离评价

  欧几里德距离评价是一个较为简单的用户关系评价方法。原理是通过计算两个用户在散点图中的距离来判断不同的用户是否有相同的偏好。以下是欧几里德距离评价的计算公式。

  通过公式我们获得了5个用户相互间的欧几里德系数,也就是用户间的距离。系数越小表示两个用户间的距离越近,偏好也越是接近。不过这里有个问题,太小的数值可能无法准确的表现出不同用户间距离的差异,因此我们对求得的系数取倒数,使用户间的距离约接近,数值越大。在下面的表格中,可以发现,用户A&C用户A&D和用户C&D距离较近。同时用户B&E的距离也较为接近。与我们前面在散点图中看到的情况一致。

  

  1.3皮尔逊相关度评价

  皮尔逊相关度评价是另一种计算用户间关系的方法。他比欧几里德距离评价的计算要复杂一些,但对于评分数据不规范时皮尔逊相关度评价能够给出更好的结果。以下是一个多用户对多个商品进行评分的示例。这个示例比之前的两个商品的情况要复杂一些,但也更接近真实的情况。我们通过皮尔逊相关度评价对用户进行分组,并推荐商品。

  

  1.4皮尔逊相关系数

  皮尔逊相关系数的计算公式如下,结果是一个在-1与1之间的系数。该系数用来说明两个用户间联系的强弱程度。

  相关系数的分类

0.8-1.0 极强相关
0.6-0.8 强相关
0.4-0.6 中等程度相关
0.2-0.4 弱相关
0.0-0.2 极弱相关或无相关

  通过计算5个用户对5件商品的评分我们获得了用户间的相似度数据。这里可以看到用户A&B,C&D,C&E和D&E之间相似度较高。下一步,我们可以依照相似度对用户进行商品推荐。

 

 

  

  2,为相似的用户提供推荐物品

  为用户C推荐商品

  当我们需要对用户C推荐商品时,首先我们检查之前的相似度列表,发现用户C和用户D和E的相似度较高。换句话说这三个用户是一个群体,拥有相同的偏好。因此,我们可以对用户C推荐D和E的商品。但这里有一个问题。我们不能直接推荐前面商品1-商品5的商品。因为这这些商品用户C以及浏览或者购买过了。不能重复推荐。因此我们要推荐用户C还没有浏览或购买过的商品。

  加权排序推荐

  我们提取了用户D和用户E评价过的另外5件商品A—商品F的商品。并对不同商品的评分进行相似度加权。按加权后的结果对5件商品进行排序,然后推荐给用户C。这样,用户C就获得了与他偏好相似的用户D和E评价的商品。而在具体的推荐顺序和展示上我们依照用户D和用户E与用户C的相似度进行排序。

  以上是基于用户的协同过滤算法。这个算法依靠用户的历史行为数据来计算相关度。也就是说必须要有一定的数据积累(冷启动问题)。对于新网站或数据量较少的网站,还有一种方法是基于物品的协同过滤算法。

  基于物品的协同过滤算法(item-based collaborative filtering)

  基于物品的协同过滤算法与基于用户的协同过滤算法很像,将商品和用户互换。通过计算不同用户对不同物品的评分获得物品间的关系。基于物品间的关系对用户进行相似物品的推荐。这里的评分代表用户对商品的态度和偏好。简单来说就是如果用户A同时购买了商品1和商品2,那么说明商品1和商品2的相关度较高。当用户B也购买了商品1时,可以推断他也有购买商品2的需求。

  1.寻找相似的物品

  表格中是两个用户对5件商品的评分。在这个表格中我们用户和商品的位置进行了互换,通过两个用户的评分来获得5件商品之间的相似度情况。单从表格中我们依然很难发现其中的联系,因此我们选择通过散点图进行展示。

  在散点图中,X轴和Y轴分别是两个用户的评分。5件商品按照所获的评分值分布在散点图中。我们可以发现,商品1,3,4在用户A和B中有着近似的评分,说明这三件商品的相关度较高。而商品5和2则在另一个群体中。

  欧几里德距离评价

  在基于物品的协同过滤算法中,我们依然可以使用欧几里德距离评价来计算不同商品间的距离和关系。以下是计算公式。

  通过欧几里德系数可以发现,商品间的距离和关系与前面散点图中的表现一致,商品1,3,4距离较近关系密切。商品2和商品5距离较近。

  皮尔逊相关度评价

  我们选择使用皮尔逊相关度评价来计算多用户与多商品的关系计算。下面是5个用户对5件商品的评分表。我们通过这些评分计算出商品间的相关度。

  皮尔逊相关度计算公式

  通过计算可以发现,商品1&2,商品3&4,商品3&5和商品4&5相似度较高。下一步我们可以依据这些商品间的相关度对用户进行商品推荐。

  2,为用户提供基于相似物品的推荐

  这里我们遇到了和基于用户进行商品推荐相同的问题,当需要对用户C基于商品3推荐商品时,需要一张新的商品与已有商品间的相似度列表。在前面的相似度计算中,商品3与商品4和商品5相似度较高,因此我们计算并获得了商品4,5与其他商品的相似度列表。

  以下是通过计算获得的新商品与已有商品间的相似度数据。

  加权排序推荐

  这里是用户C已经购买过的商品4,5与新商品A,B,C直接的相似程度。我们将用户C对商品4,5的评分作为权重。对商品A,B,C进行加权排序。用户C评分较高并且与之相似度较高的商品被优先推荐。

 

 

二、基于物品的协同过滤算法详解


最近参加KDD Cup 2012比赛,选了track1,做微博推荐的,找了推荐相关的论文学习。“Item-Based Collaborative Filtering Recommendation Algorithms”这篇是推荐领域比较经典的论文,现在很多流行的推荐算法都是在这篇论文提出的算法的基础上进行改进的。

        一、协同过滤算法描述

        推荐系统应用数据分析技术,找出用户最可能喜欢的东西推荐给用户,现在很多电子商务网站都有这个应用。目前用的比较多、比较成熟的推荐算法是协同过滤(Collaborative Filtering,简称CF)推荐算法,CF的基本思想是根据用户之前的喜好以及其他兴趣相近的用户的选择来给用户推荐物品。

        如图1所示,在CF中,用m×n的矩阵表示用户对物品的喜好情况,一般用打分表示用户对物品的喜好程度,分数越高表示越喜欢这个物品,0表示没有买过该物品。图中行表示一个用户,列表示一个物品,Uij表示用户i对物品j的打分情况。CF分为两个过程,一个为预测过程,另一个为推荐过程。预测过程是预测用户对没有购买过的物品的可能打分值,推荐是根据预测阶段的结果推荐用户最可能喜欢的一个或Top-N个物品。

        二、User-based算法与Item-based算法对比

        CF算法分为两大类,一类为基于memory的(Memory-based),另一类为基于Model的(Model-based),User-based和Item-based算法均属于Memory-based类型,具体细分类可以参考wikipedia的说明。

        User-based的基本思想是如果用户A喜欢物品a,用户B喜欢物品a、b、c,用户C喜欢a和c,那么认为用户A与用户B和C相似,因为他们都喜欢a,而喜欢a的用户同时也喜欢c,所以把c推荐给用户A。该算法用最近邻居(nearest-neighbor)算法找出一个用户的邻居集合,该集合的用户和该用户有相似的喜好,算法根据邻居的偏好对该用户进行预测。

        User-based算法存在两个重大问题:

        1. 数据稀疏性。一个大型的电子商务推荐系统一般有非常多的物品,用户可能买的其中不到1%的物品,不同用户之间买的物品重叠性较低,导致算法无法找到一个用户的邻居,即偏好相似的用户。

        2. 算法扩展性。最近邻居算法的计算量随着用户和物品数量的增加而增加,不适合数据量大的情况使用。

        Iterm-based的基本思想是预先根据所有用户的历史偏好数据计算物品之间的相似性,然后把与用户喜欢的物品相类似的物品推荐给用户。还是以之前的例子为例,可以知道物品a和c非常相似,因为喜欢a的用户同时也喜欢c,而用户A喜欢a,所以把c推荐给用户A。

        因为物品直接的相似性相对比较固定,所以可以预先在线下计算好不同物品之间的相似度,把结果存在表中,当推荐时进行查表,计算用户可能的打分值,可以同时解决上面两个问题。

        三、Item-based算法详细过程

        (1)相似度计算

        Item-based算法首选计算物品之间的相似度,计算相似度的方法有以下几种:

        1. 基于余弦(Cosine-based)的相似度计算,通过计算两个向量之间的夹角余弦值来计算物品之间的相似性,公式如下:

        其中分子为两个向量的内积,即两个向量相同位置的数字相乘。

        2. 基于关联(Correlation-based)的相似度计算,计算两个向量之间的Pearson-r关联度,公式如下:

        其中表示用户u对物品i的打分,表示第i个物品打分的平均值。

        3. 调整的余弦(Adjusted Cosine)相似度计算,由于基于余弦的相似度计算没有考虑不同用户的打分情况,可能有的用户偏向于给高分,而有的用户偏向于给低分,该方法通过减去用户打分的平均值消除不同用户打分习惯的影响,公式如下:

        其中表示用户u打分的平均值。

        (2)预测值计算

        根据之前算好的物品之间的相似度,接下来对用户未打分的物品进行预测,有两种预测方法:

        1. 加权求和。

        用过对用户u已打分的物品的分数进行加权求和,权值为各个物品与物品i的相似度,然后对所有物品相似度的和求平均,计算得到用户u对物品i打分,公式如下:

 

 

 

        其中为物品i与物品N的相似度,为用户u对物品N的打分。

        2. 回归。

        和上面加权求和的方法类似,但回归的方法不直接使用相似物品N的打分值,因为用余弦法或Pearson关联法计算相似度时存在一个误区,即两个打分向量可能相距比较远(欧氏距离),但有可能有很高的相似度。因为不同用户的打分习惯不同,有的偏向打高分,有的偏向打低分。如果两个用户都喜欢一样的物品,因为打分习惯不同,他们的欧式距离可能比较远,但他们应该有较高的相似度。在这种情况下用户原始的相似物品的打分值进行计算会造成糟糕的预测结果。通过用线性回归的方式重新估算一个新的值,运用上面同样的方法进行预测。重新计算的方法如下:

        其中物品N是物品i的相似物品,和通过对物品N和i的打分向量进行线性回归计算得到,为回归模型的误差。具体怎么进行线性回归文章里面没有说明,需要查阅另外的相关文献。

 

三、基于协同过滤算法实现

prepare_data.py   数据来源https://grouplens.org/datasets/movielens

import os


def get_user_click(rating_file):
    '''获取用户的点击'''
    if not os.path.exists(rating_file):
        return {},{}
    fp = open(rating_file, 'r', encoding='UTF-8')
    num = 0
    user_click = {}
    user_click_time = {}
    for line in fp:
        if num == 0:
            num += 1
            continue
        item = str(line).strip().split(',')
        if len(item) < 4:
            continue
        [userid, itemid, rating, tiemstamp] = item
        if userid + '_' + itemid not in user_click_time:
            user_click_time[userid + '_' + itemid] = int(tiemstamp)
        if float(rating) < 3.0:
            continue
        if userid not in user_click:
            user_click[userid] = []
        user_click[userid].append(itemid)

    fp.close()
    return user_click, user_click_time


def get_item_info(item_file):
    '''获取电影详情'''
    if not os.path.exists(item_file):
        return {}
    num = 0
    item_info = {}
    fp = open(item_file, 'r', encoding='UTF-8')
    for line in fp:
        if num == 0:
            num += 1
            continue
        item = line.strip().split(',')
        if len(item) < 3:
            continue
        if len(item) == 3:
            [itemid, title, genres] = item
        elif len(item) > 3:
            itemid = item[0]
            title = ','.join(item[1:-1])
            genres = item[-1]
        if itemid not in item_info:
            item_info[itemid] = [title, genres]
    fp.close()
    return item_info


if __name__ == '__main__':
    user_click = get_user_click('..\\data\\ratings_max.txt')
    print(len(user_click))
    print(user_click['1'])
    item_info = get_item_info('..\\data\\movies.txt')
    print(len(item_info))
    print(item_info['1'])

item_cf.py  基于物品推荐

import sys, json, math, operator, time

sys.path.append('..\\util')
from prepare_data import get_item_info, get_user_click


def base_contribute_score(click_time_one, click_time_tow):
    '''根据时间衰减'''
    delata_time = abs(click_time_one - click_time_tow)
    total_sec = 60 * 60 * 24
    delata_time = delata_time / total_sec
    return 1 / (1 + delata_time)

def base_contribute_score(user_total_click_num):
    '''根据点击数量衰减'''
    return 1 / math.log10(1 + user_total_click_num)


def cal_item_sim(user_click, user_click_time):
    '''根据用户的点击系列等到用户的相似度'''
    co_appear = {}
    item_user_click_time = {}
    for user, itemlist in user_click.items():
        for index_i in range(0, len(itemlist)):
            itemid_i = itemlist[index_i]
            item_user_click_time.setdefault(itemid_i, 0)
            item_user_click_time[itemid_i] += 1
            for index_j in range(index_i + 1, len(itemlist)):
                itemid_j = itemlist[index_j]
                if user + '_' + itemid_i not in user_click_time:
                    click_time_one = 0
                else:
                    click_time_one = user_click_time[user + '_' + itemid_i]

                if user + '_' + itemid_j not in user_click_time:
                    click_time_tow = 0
                else:
                    click_time_tow = user_click_time[user + '_' + itemid_j]

                co_appear.setdefault(itemid_i, {})
                co_appear[itemid_i].setdefault(itemid_j, 0)
                co_appear[itemid_i][itemid_j] += base_contribute_score(click_time_one, click_time_tow)

                co_appear.setdefault(itemid_j, {})
                co_appear[itemid_j].setdefault(itemid_i, 0)
                co_appear[itemid_j][itemid_i] += base_contribute_score(click_time_one, click_time_tow)

    item_sim_scor = {}
    item_sim_scor_sorted = {}
    for itemid_i, relate_item in co_appear.items():
        for itemid_j, co_time in relate_item.items():
            sim_score = co_time / math.sqrt(item_user_click_time[itemid_i] * item_user_click_time[itemid_j])  # 计算相似度得分
            item_sim_scor.setdefault(itemid_i, {})
            item_sim_scor[itemid_i].setdefault(itemid_j, 0)
            item_sim_scor[itemid_i][itemid_j] = sim_score
    for itemid in item_sim_scor:
        item_sim_scor_sorted[itemid] = sorted(item_sim_scor[itemid].items(), key=operator.itemgetter(1), reverse=True)
    return item_sim_scor_sorted


def cal_recom_result(sim_info, user_click):
    recet_click_num = 10
    topk = 20
    recom_info = {}
    for user in user_click:
        click_list = user_click[user]
        recom_info.setdefault(user, {})
        for itemid in click_list[:recet_click_num]:
            if itemid not in sim_info:
                continue
            for itemsimzube in sim_info[itemid][:topk]:
                itemsimid = itemsimzube[0]
                itemsimcore = itemsimzube[1]
                recom_info[user][itemsimid] = itemsimcore

    return recom_info


def debug_itemsim(sim_info, item_info):
    '''打印推荐的物品'''
    fixed_itemid = '1'
    if fixed_itemid not in item_info:
        return {}
    [item_fix, genres_fix] = item_info[fixed_itemid]
    for zube in sim_info[fixed_itemid]:
        itemid_sim = zube[0]
        sin_scroe = zube[1]
        if itemid_sim not in item_info:
            continue
        [item, genres] = item_info[itemid_sim]
        print('{}-{}-{}-{}-{}'.format(item_fix, genres_fix, item, genres, sin_scroe))


def debug_recomresult(recom_result, item_info):
    '''打印推荐结果'''
    user_id = '1'
    if user_id not in recom_result:
        return {}
    for zube in sorted(recom_result[user_id].items(), key=operator.itemgetter(1), reverse=True):
        itemid, sore = zube
        if itemid not in item_info:
            continue
        print(','.join(item_info[itemid]) + str(sore))


def main_flow():
    start_time = time.time()
    # 获取用户的点击系列
    user_click, user_click_time = get_user_click('..\\data\\ratings_max.txt')

    # 获取item_info
    item_info = get_item_info('..\\data\\movies.txt')

    # 根据用户的点击系列等到用户的相似度
    sim_info = cal_item_sim(user_click, user_click_time)

    # # 打印相似度
    # debug_itemsim(sim_info, item_info)

    # 根据用户的点击系列和item_info计算推荐结构
    recom_result = cal_recom_result(sim_info, user_click)
    # print(recom_result['1'])
    debug_recomresult(recom_result, item_info)


if __name__ == '__main__':
    main_flow()

user_cf.py   基于用户推荐

import sys, json, math, operator, time

sys.path.append('..\\util')
from perpaer_data import get_item_info, get_user_click


def base_contribute_score(click_time_one, click_time_tow):
    '''根据时间衰减'''
    delata_time = abs(click_time_one - click_time_tow)
    total_sec = 60 * 60 * 24
    delata_time = delata_time / total_sec
    return 1 / (1 + delata_time)


def base_contribute_score(user_total_click_num):
    '''根据点击数量衰减'''
    return 1 / math.log10(1 + user_total_click_num)

def cal_user_sim(item_click_by_user, user_click_time):
    '''根据用户的点击系列等到用户的相似度'''
    co_appear = {}
    item_user_click_count = {}
    for itemid, user_list in item_click_by_user.items():
        for index_i in range(0, len(user_list)):
            user_i = user_list[index_i]
            item_user_click_count.setdefault(user_i, 0)
            item_user_click_count[user_i] += 1
            for index_j in range(index_i + 1, len(user_list)):
                user_j = user_list[index_j]
                if itemid + '_' + user_i not in user_click_time:
                    click_time_one = 0
                else:
                    click_time_one = user_click_time[itemid + '_' + user_i]

                if itemid + '_' + user_j not in user_click_time:
                    click_time_tow = 0
                else:
                    click_time_tow = user_click_time[itemid + '_' + user_j]

                co_appear.setdefault(user_i, {})
                co_appear[user_i].setdefault(user_j, 0)
                co_appear[user_i][user_j] += base_contribute_score(click_time_one, click_time_tow)

                co_appear.setdefault(user_j, {})
                co_appear[user_j].setdefault(user_i, 0)
                co_appear[user_j][user_i] += base_contribute_score(click_time_one, click_time_tow)

    user_sim_scor = {}
    user_sim_scor_sorted = {}
    for user_i, relate_user in co_appear.items():
        for user_j, co_time in relate_user.items():
            sim_score = co_time / math.sqrt(item_user_click_count[user_i] * item_user_click_count[user_j])  # 计算相似度得分
            user_sim_scor.setdefault(user_i, {})
            user_sim_scor[user_i].setdefault(user_j, 0)
            user_sim_scor[user_i][user_j] = sim_score
    for itemid in user_sim_scor:
        user_sim_scor_sorted[itemid] = sorted(user_sim_scor[itemid].items(), key=operator.itemgetter(1), reverse=True)
    return user_sim_scor_sorted


def cal_recom_result(sim_info, user_click):
    recet_click_num = 10
    topk = 20
    recom_result = {}
    for user, item_list in user_click.items():
        tmp_dict = {}
        for itemid in item_list:
            tmp_dict.setdefault(itemid, 1)
        recom_result.setdefault(user, {})
        for zube in sim_info[user][:topk]:
            userid_j, sim_score = zube
            if userid_j not in user_click:
                continue
            for itemid_j in user_click[userid_j][:recet_click_num]:
                recom_result[user].setdefault(itemid_j, sim_score)

    return recom_result


def debug_itemsim(user_sim, item_info):
    '''打印推荐的物品'''
    topk = 5
    fix_user = '1'
    if fix_user not in user_sim:
        return {}
    for zuhe in user_sim[fix_user][:topk]:
        userid, score = zuhe
        print('{}-{}'.format(userid, score))


def debug_recomresult(recom_result, item_info):
    '''打印推荐结果'''
    user_id = '1'
    if user_id not in recom_result:
        return {}
    for itemid in recom_result[user_id]:
        if itemid not in item_info:
            continue
        recom_score = recom_result[user_id][itemid]
        print('{}-{}'.format(','.join(item_info[itemid]), recom_score))


def tramsfer_user_click(user_click):
    '''用户点击转化为item被用户点击'''
    item_click_by_user = {}
    for user in user_click:
        item_list = user_click[user]
        for itemid in item_list:
            item_click_by_user.setdefault(itemid, [])
            item_click_by_user[itemid].append(user)

    return item_click_by_user


def main_flow():
    start_time = time.time()
    # 获取用户的点击系列
    user_click, user_click_time = get_user_click('..\\data\\ratings_max.txt')

    # 获取item_info
    item_info = get_item_info('..\\data\\movies.txt')

    # 把用户的点击转化成item点击
    item_click_by_user = tramsfer_user_click(user_click)

    # 根据用户的点击系列等到用户的相似度
    user_sim = cal_user_sim(item_click_by_user, user_click_time)

    # # 打印相似度
    debug_itemsim(user_sim, item_info)

    # 根据用户的点击系列和item_info计算推荐结构
    recom_result = cal_recom_result(user_sim, user_click)
    # print(recom_result['1'])
    debug_recomresult(recom_result, item_info)


if __name__ == '__main__':
    main_flow()