《网络电视节目推荐系统----基于用户协同过滤与基于内容的推荐算法的后融合》

一、什么是推荐系统

  1. 维基定义:推荐系统是一种信息过滤系统,用于预测用户对物品的“评分”或“偏好”。(即实质上是一种预测系统)
  2. 具体定义:推荐系统是一个可以根据用户(users)的历史行为、社交关系、兴趣点、所处上下文环境等去判断用户的当前需求或感兴趣的物品(items)的系统。
  3. 别名:个性化推荐系统(见名知义,即“为你服务”)



二、产生原因

你可以假想一下,在一个悠闲的下午,你手中怀揣着一袋零食,眼前摆着一部电脑,突发奇想的你想要让一部喜剧电影来陪伴你度过这个美好的下午。这时,你轻点了几下鼠标,跳转到了一个视频门户网站。你又轻轻敲了键盘,在右上角的搜索框内键入"喜剧"关键词,结果你发现,整个页面是一列表的喜剧电影,琳琅满目,但也让你手足失措,不知道要看哪一部,或者其中列出的一些电影你之前已经看过了。你感慨道:哎,有没有一个专门为我服务的工具呢,让它帮我选一下电影,我可不想把我宝贵的时间花在找电影的功夫上。。。


此时,我们可以看出,我们在现实生活中会经常遇到信息过载的问题,特别是随着最近互联网的迅速发展,世界的数据量呈螺旋式增长,所以,我们需要一个工具,或者说,是一个系统,来帮我们做一些决策,给出一些建议供你选择。而且这个工具是自动化的,换句话说,它可以分析你的历史兴趣,从庞大的数据集当中寻找出符合你兴趣的物品让你去选择,这个工具就是个性化推荐系统

推荐算法其实在1992年就提出来了,但是实际上是最近这些年才火起来,因为互联网的爆发,导致了我们所谓的“大数据”,我们就可以很好地利用这些数据,来实行我们的推荐算法,进行各种精准营销业务和个性化服务。

由上我们可以得知,推荐系统产生的两大原因
  • 信息过载
  • 用户需求的多样性(也就是用户在大部分时候没有明确的需求)

所以,一般只有同时满足这两个原因时,我们才会使用推荐系统,不然可能适得其反。




三、应用场景

  1. 电子商务

    比如著名的电子商务网站亚马逊的个性化商品推荐列表和相关商品的推荐列表。

  2. 电影和视频网站

    比如著名的Netflix的在线视频推荐系统以及YouTube的基于Deep Learning的推荐系统。

  3. 个性化音乐网络平台

    比如博主使用的网易云音乐中的“每日推荐”。

  4. 社交网络

    比如我们熟悉的Facebook和Twitter的社交网络应用中信息流的会话推荐。

  5. 个性化阅读

    比如国外著名的Google Reader,它允许用户关注自己感兴趣的人,然后看到所关注用户分享的文章。

  6. 基于位置的服务

    比如我们经常使用的美团App,它会获取我们实时的地理位置信息,然后基于这些位置信息给我们推荐附近的且我们可能感兴趣的店铺。

  7. 个性化邮件

    上面已经说了,在定义里面提到了“过滤”,这里就是指个性化的邮件过滤推荐系统,它会帮我们过滤掉垃圾邮件,然后留下我们所感兴趣的邮件并依据重要性进行排序。最早的一个个性化邮件推荐系统是Tapestry。

  8. 个性化广告

    这个很容易理解,现在个性化广告投放甚至都成为了一门独立的学科----计算广告学。




四、推荐系统的评测方法

一个推荐系统产生之后,我们还不可以贸然实行,还需评测一下它的具体性能,才可以上线实行,要不然会出现意想不到的后果,甚至与我们想要的效果想违背。

  • 完整的推荐系统中的三个参与方
    • 用户
    • 物品提供者
    • 提供推荐系统的网站

在介绍推荐系统的指标之前,先说一下可以获得这些指标的实验方法~~

1. 推荐系统的三大实验方法

a. 离线实验:
  1. 通过日志系统获得用户行为数据,按照一定的格式生成一个标准的数据集
  2. 将数据集按照一定的规则分成训练集和测试集
  3. 在训练集上训练用户兴趣模型,在测试集上进行预测
  4. 通过事先定义的离线指标评测算法在测试集上的预测结果

优点:

  • 不需要真实用户的参与
  • 不需要有对实际系统的控制权
  • 速度快,可以测试大量算法

缺点:

  • 无法计算商业关心的指标,例如点击率、转化率等


b. 用户调查

由于离线试验的指标与商业指标存在差异,高准确率不等于高用户满意度。所以,想要准确的评测一个算法,需要相对比较真实的环境。最好的方法就是直接上线测试,而由于无法确定算法的影响,上线测试会有比较大的风险,这时,我们的用户调查就派上用场了。很多离线试验无法获得的主观感受指标都可以通过用户调查获得。

PS:过程大致和产品的用户调查差不多,但应尽量保证双盲实验,尽量使用多样用户群,并保证用户属性的平衡。

优点:

  • 可以获得很多体现用户主观感受的指标
  • 相对在线实验风险很低,出现错误后很容易弥补

缺点

  • 招募测试用户代价较大,很难组织大规模的测试用户
  • 设计双盲实验较困难,并且用户在测试环境下的行为和真实环境下的行为可能有所不同,导致测试结果在真实环境下无法重现。


c. 在线实验

在线实验就是大杀器了,它可以统计到最真实的用户反馈和商业指标。在完成必要的离线试验和用户调查后,可以采用AB测试的方式比较新旧算法

AB测试(一种在线评测算法的实验方法):

根据一定规则将用户随机分成几组,对不同组的用户采用不同的算法,然后统计不同组用户的各种不同的评测指标来比较不同算法。比如可以统计不同组用户的点击率,通过该指标来比较不同算法的性能。



《Summary》

一个新的推荐系统的最终上线,需要完成上面的三个实验:

  • Firstly:通过离线实验证明它在很多离线指标上优于现有的算法。
  • Secondly:通过用户调查确定它的用户满意度不低于现有的算法。
  • Finally: 最后通过在线的AB测试确定它在我们关心的指标上优于现有的算法。


2. 推荐系统的常用评测指标

由于网上的资料很多,加上这个不是本文所要阐述的重点,这里仅罗列一下,具体公式可以自行Google~~

  • 用户满意度

    用户满意度是推荐系统最重要的指标,但是,用户满意度无法离线计算,只能通过用户调查或在线实验得到。

  • 预测准确度

    是度量一个推荐系统预测用户行为的能力,是最重要的离线评测方法。

  • TopN推荐

    TopN 推荐是指,用户会不会对物品感兴趣。TopN 推荐有两个重要指标:准确率和召回率。

    • 准确率:为用户推荐且用户感兴趣的物品,在推荐结果列表中所占的比例。
    • 召回率:为用户推荐且用户感兴趣的物品,在用户感兴趣的所有物品列表中所占的比例。
  • 评分预测

    评分预测是指用户会对一个物品产生怎样的评分,由此可以习得用户的兴趣模型或用户画像。

  • 覆盖率

    覆盖率描述一个推荐系统对长尾的发掘能力。覆盖率没有唯一的定义方法,一个简单的定义是,推荐列表中的物品占总物品数的比例。

  • 实时性

  • 健壮性

  • 商业目标

    。。。

    。。。

    。。。

此外,还有一些偏向主观方面的指标, 比如:多样性、新颖性、惊喜度、信任度等。。。



五、目前常用的推荐算法

  • 基于物品的协同过滤算法(Item-based Collaborative Filtering),可简称为ItemCF。

  • 基于用户的协同过滤算法(User-based Collaborative Filtering),可简称为UserCF。

  • 隐语义与矩阵分解模型(Latent Factor Model)

  • 基于内容的推荐算法(Content-based Recommendation),可简称为CB。

  • 基于深度学习的推荐算法

    。。。

    。。。



六、十大开源推荐系统

有兴趣的可以了解一下~~



七、例子实践(网络电视节目的精准营销推荐)

简化说明: 本例子来自今年(2018)的泰迪杯的B题,但这里已经过大量简化, 辛苦了我辛勤耕耘的队友。这里仅通过三个用户的收视数据来实现我们的混合推荐算法。

1. 目标

基于每位用户的观看记录以及节目信息,对每位用户实行节目的个性化推荐。


2. 算法思路

将UserCF 与 CB 进行“后融合”


3. 输入数据

  1. 用户的观看记录: “用户A对于其三个月来所看过节目的评分.xls”,“用户B对于其三个月来所看过节目的评分.xls”,“用户C对于其三个月来所看过节目的评分.xls”。具体格式如下:
    (对于其中的A、B列,是一些辅助数据,这里不会使用,可以忽略)
    1

  2. 备选推荐节目集: “备选推荐节目集及所属类型.xlsx”。具体格式如下:
    2


4. 具体实现步骤:


#### a. 基于内容的推荐算法CB的实现: ##### 1. 刻画节目画像(Item Profiles) > 将“备选推荐节目集及所属类型.xlsx” 转换为01矩阵表示的节目表“备选推荐节目集及所属类型01矩阵.xlsx”。

Source Code:
items_labels_to_01matrix.py

# 代码说明:
# 根据"D:\Recommender Systems\备选推荐节目集及所属类型.xlsx"生成"D:\Recommender Systems\备选推荐节目集及所属类型01矩阵表.xlsx"

import pandas as pd
import numpy as np

if __name__ == '__main__':

    df = pd.read_excel("D:\Recommender Systems\备选推荐节目集及所属类型.xlsx")
    (m, n) = df.shape

    data_array = np.array(df.iloc[1:m+1,:])

    # 按指定顺序排列的所有标签
    all_labels = ['教育', '戏曲', '悬疑', '科幻', '惊悚', '动作', '资讯', '武侠', '剧情', '警匪', '生活', '军事', '言情', '体育', '冒险', '纪实', '少儿教育', '少儿', '综艺', '古装', '搞笑', '广告']
    labels_num = len(all_labels)

    # 按顺序提取所有节目的名称
    all_items_names = np.array(df.iloc[:m+1, 0])[1:]

    # 创建一个01矩阵,0表示该节目不属于该类型,1表示该节目属于该类型
    data_to_be_written = []

    for i in range(len(all_items_names)):

        # 每个节目的01行向量
        vector = [0] * labels_num
        labels_names = str(data_array[i][1]).split(" ")

        for j in range(len(labels_names)):
            location = all_labels.index(labels_names[j])
            vector[location] = 1

        data_to_be_written.append(vector)

    # 将01矩阵写入“备选推荐节目集及所属类型01矩阵表”
    df = pd.DataFrame(data_to_be_written, index=all_items_names, columns=all_labels)
    df.to_excel("D:\Recommender Systems\备选推荐节目集及所属类型01矩阵表.xlsx")
    
    # PS: 记得在生成的“备选推荐节目集及所属类型01矩阵表”中节目名那一列的首个空白的单元格中打上“节目名”

Result:
2

2. 刻画用户画像(User Profiles)& 得出推荐集

根据“用户A\B\C对于其三个月来所看过节目的评分.xls”三个表,得出“所有用户对其看过的节目的评分矩阵.xlsx”。

这个要考虑到权重以及其他因素,具体方法取决于实际应用,我就直接把代码的输出结果的样式给出来。
Result:
4

根据“用户A\B\C对于其三个月来所看过节目的评分.xls”三个表,得出“所有用户看过的节目及所属类型的01矩阵.xlsx”。

Source Code:
items_saw_labels_to_01matrix.py

# 代码说明:
# 根据"D:\Recommender Systems\用户A/B/C对于其三个月来所看过节目的评分.xls"
# 生成"D:\Recommender Systems\所有用户看过的节目及所属类型的01矩阵.xlsx"
import pandas as pd
import numpy as np

if __name__ == '__main__':

    all_users_names = ['A', 'B', 'C']

    # 所有用户看过的节目名 all_items_users_saw = [item2, item3, item4]
    # 所有用户看过的节目名对应的类型 all_items_users_saw_labels = ["label2 label3", "label3", ...]
    all_items_users_saw = []
    all_items_users_saw_labels = []

    for j in range(len(all_users_names)):

        fileToBeRead = "D:\Recommender Systems\用户" + all_users_names[j] + "对于其三个月来所看过节目的评分.xls"
        df = pd.read_excel(fileToBeRead)
        (m, n) = df.shape
        data_array = np.array(df)

        for i in range(m):
            # 不重复记录相同的节目
           if data_array[i][2] not in all_items_users_saw:
               all_items_users_saw.append(data_array[i][2])
               all_items_users_saw_labels.append(data_array[i][3])

    # 生成"所有用户看过的节目及所属类型的01矩阵"
    all_labels = ['教育', '戏曲', '悬疑', '科幻', '惊悚', '动作', '资讯', '武侠', '剧情', '警匪', '生活', '军事', '言情', '体育', '冒险', '纪实', '少儿教育', '少儿', '综艺', '古装', '搞笑', '广告']
    labels_num = len(all_labels)

    all_items_labels_01_vectors = []

    for i in range(len(all_items_users_saw)):
        vector = [0] * labels_num
        labels_names = all_items_users_saw_labels[i].split(" ")

        for j in range(len(labels_names)):
            location = all_labels.index(labels_names[j])
            vector[location] = 1

        all_items_labels_01_vectors.append(vector)

    df = pd.DataFrame(all_items_labels_01_vectors, index=all_items_users_saw, columns=all_labels)
    df.to_excel("D:\Recommender Systems\所有用户看过的节目及所属类型的01矩阵.xlsx")

    # PS: 记得在生成的“所有用户看过的节目及所属类型的01矩阵表”中节目名那一列的首个空白的单元格中打上“节目名”

Result:
5


> 根据“所有用户对其看过的节目的评分矩阵.xlsx”以及“所有用户看过的节目及所属类型的01矩阵.xlsx”得到用户画像(一个关于评分数据的行向量,表示某个用户对于各个类型的评分),然后将每个用户画像与所有**备选推荐节目**的画像(一个关于是否含有/具备该类型的01行向量,表示某个节目含有/具备哪些类型)进行相似度的计算,然后将推荐节目集按相似度进行降序排序,最后取出topN个节目。CB的具体实现如下:

Source Code:
CB.py

# 代码说明:
# 基于内容的推荐算法的具体实现

import math
import numpy as np
import pandas as pd

# 创建节目画像
# 参数说明:
# items_profiles = {item1:{'label1':1, 'label2': 0, 'label3': 0, ...}, item2:{...}...}
def createItemsProfiles(data_array, labels_names, items_names):

    items_profiles = {}

    for i in range(len(items_names)):

        items_profiles[items_names[i]] = {}

        for j in range(len(labels_names)):
            items_profiles[items_names[i]][labels_names[j]] = data_array[i][j]

    return items_profiles

# 创建用户画像
# 参数说明:
# data_array: 所有用户对于其所看过的节目的评分矩阵 data_array = [[2, 0, 0, 1.1, ...], [0, 0, 1.1, ...], ...]
# users_profiles = {user1:{'label1':1.1, 'label2': 0.5, 'label3': 0.0, ...}, user2:{...}...}
def createUsersProfiles(data_array, users_names, items_names, labels_names, items_profiles):

    users_profiles = {}

    # 计算每个用户对所看过的所有节目的平均隐性评分
    # users_average_scores_list = [1.2, 2.2, 4.3,...]
    users_average_scores_list = []

    # 统计每个用户所看过的节目(不加入隐性评分信息)
    # items_users_saw = {user1:[item1, item3, item5], user2:[...],...}
    items_users_saw = {}

    # 统计每个用户所看过的节目及评分
    # items_users_saw_scores = {user1:[[item1, 1.1], [item2, 4.1]], user2:...}
    items_users_saw_scores = {}

    for i in range(len(users_names)):

        items_users_saw_scores[users_names[i]] = []
        items_users_saw[users_names[i]] = []
        count = 0
        sum = 0.0

        for j in range(len(items_names)):

            # 用户对该节目隐性评分为正,表示真正看过该节目
            if data_array[i][j] > 0:
                items_users_saw[users_names[i]].append(items_names[j])
                items_users_saw_scores[users_names[i]].append([items_names[j], data_array[i][j]])
                count += 1
                sum += data_array[i][j]

        if count == 0:
            users_average_scores_list.append(0)
        else:
            users_average_scores_list.append(sum / count)

    for i in range(len(users_names)):

        users_profiles[users_names[i]] = {}

        for j in range(len(labels_names)):
            count = 0
            score = 0.0

            for item in items_users_saw_scores[users_names[i]]:

                # 参数:
                # 用户user1对于类型label1的隐性评分: user1_score_to_label1
                # 用户user1对于其看过的含有类型label1的节目item i 的评分: score_to_item i
                # 用户user1对其所看过的所有节目的平均评分: user1_average_score
                # 用户user1看过的节目总数: items_count

                # 公式: user1_score_to_label1 = Sigma(score_to_item i - user1_average_score)/items_count

                # 该节目含有特定标签labels_names[j]
                if items_profiles[item[0]][labels_names[j]] > 0:
                    score += (item[1] - users_average_scores_list[i])
                    count += 1

            # 如果求出的值太小,直接置0
            if abs(score) < 1e-6:
                score = 0.0
            if count == 0:
                result = 0.0
            else:
                result = score / count

            users_profiles[users_names[i]][labels_names[j]] = result

    return (users_profiles, items_users_saw)


# 计算用户画像向量与节目画像向量的距离(相似度)
# 向量相似度计算公式:
# cos(user, item) = sigma_ui/sqrt(sigma_u * sigma_i)

# 参数说明:
# user_profile: 某一用户user的画像 user = {'label1':1.1, 'label2': 0.5, 'label3': 0.0, ...}
# item: 某一节目item的画像 item = {'label1':1, 'label2': 0, 'label3': 0, ...}
# labels_names: 所有类型名
def calCosDistance(user, item, labels_names):

    sigma_ui = 0.0
    sigma_u = 0.0
    sigma_i = 0.0

    for label in labels_names:
        sigma_ui += user[label] * item[label]
        sigma_u += (user[label] * user[label])
        sigma_i += (item[label] * item[label])

    if sigma_u == 0.0 or sigma_i == 0.0:  # 若分母为0,相似度为0
        return 0

    return sigma_ui/math.sqrt(sigma_u * sigma_i)


# 基于内容的推荐算法:
# 借助特定某个用户user的画像user_profile和备选推荐节目集的画像items_profiles,通过计算向量之间的相似度得出推荐节目集

# 参数说明:
# user_profile: 某一用户user的画像 user_profile = {'label1':1.1, 'label2': 0.5, 'label3': 0.0, ...}
# items_profiles: 备选推荐节目集的节目画像: items_profiles = {item1:{'label1':1, 'label2': 0, 'label3': 0}, item2:{...}...}
# items_names: 备选推荐节目集中的所有节目名
# labels_names: 所有类型名
# items_user_saw: 用户user看过的节目

def contentBased(user_profile, items_profiles, items_names, labels_names, items_user_saw):

    # 对于用户user的推荐节目集为 recommend_items = [[节目名, 该节目画像与该用户画像的相似度], ...]
    recommend_items = []

    for i in range(len(items_names)):
        # 从备选推荐节目集中的选择用户user没有看过的节目
        if items_names[i] not in items_user_saw:
            recommend_items.append([items_names[i], calCosDistance(user_profile, items_profiles[items_names[i]], labels_names)])

    # 将推荐节目集按相似度降序排列
    recommend_items.sort(key=lambda item: item[1], reverse=True)

    return recommend_items

# 输出推荐给该用户的节目列表
# max_num:最多输出的推荐节目数
def printRecommendedItems(recommend_items_sorted, max_num):
    count = 0
    for item, degree in recommend_items_sorted:
        print("节目名:%s, 推荐指数:%f" % (item, degree))
        count += 1
        if count == max_num:
            break


# 主程序
if __name__ == '__main__':

    all_users_names = ['A', 'B', 'C']
    all_labels = ['教育', '戏曲', '悬疑', '科幻', '惊悚', '动作', '资讯', '武侠', '剧情', '警匪', '生活', '军事', '言情', '体育', '冒险', '纪实',
                  '少儿教育', '少儿', '综艺', '古装', '搞笑', '广告']
    labels_num = len(all_labels)

    df1 = pd.read_excel("D:\Recommender Systems\所有用户对其看过的节目的评分矩阵.xlsx")
    (m1, n1) = df1.shape
    # 所有用户对其看过的节目的评分矩阵
    # data_array1 = [[0.1804 0.042 0.11  0.07  0.19  0.56  0.14  0.3  0.32 0, ...], [...]]
    data_array1 = np.array(df1.iloc[:m1 + 1, 1:])
    # 按照"所有用户对其看过的节目的评分矩阵"的列序排列的所有用户观看过的节目名称
    items_users_saw_names1 = df1.columns[1:].tolist()


    df2 = pd.read_excel("D:\Recommender Systems\所有用户看过的节目及所属类型的01矩阵.xlsx")
    (m2, n2) = df2.shape
    data_array2 = np.array(df2.iloc[:m2 + 1, 1:])
    # 按照"所有用户看过的节目及所属类型的01矩阵"的列序排列的所有用户观看过的节目名称
    items_users_saw_names2 = np.array(df2.iloc[:m2 + 1, 0]).tolist()

    # 为用户看过的节目建立节目画像
    items_users_saw_profiles = createItemsProfiles(data_array2, all_labels, items_users_saw_names2)

    # 建立用户画像users_profiles和用户看过的节目集items_users_saw
    (users_profiles, items_users_saw) = createUsersProfiles(data_array1, all_users_names, items_users_saw_names1, all_labels, items_users_saw_profiles)

    df3 = pd.read_excel("D:\Recommender Systems\备选推荐节目集及所属类型01矩阵.xlsx")
    (m3, n3) = df3.shape
    data_array3 = np.array(df3.iloc[:m3 + 1, 1:])
    # 按照"备选推荐节目集及所属类型01矩阵"的列序排列的所有用户观看过的节目名称
    items_to_be_recommended_names = np.array(df3.iloc[:m3 + 1, 0]).tolist()

    # 为备选推荐节目集建立节目画像
    items_to_be_recommended_profiles = createItemsProfiles(data_array3, all_labels, items_to_be_recommended_names)

    for user in all_users_names:
         print("对于用户 %s 的推荐节目如下:" % user)
         recommend_items = contentBased(users_profiles[user], items_to_be_recommended_profiles, items_to_be_recommended_names, all_labels, items_users_saw[user])
         printRecommendedItems(recommend_items, 3)
         print()

Result:
6


b. 基于用户的协同过滤算法UserCF的实现:
1. 准备好输入数据

从上面的CB已经得到“备选推荐节目集及所属类型01矩阵.xlsx"和”所有用户对其看过的节目的评分矩阵.xlsx“


2. 挖掘用户的邻居,得出推荐集

找出每个用户的k个邻居,结合所有k个邻居看过的节目对该用户进行节目的推荐。注意被推荐的节目应该不包含该用户看过的节目,也不包含不在备选推荐节目集中的节目。(因为备选推荐节目集是商家盈利的节目集,也就是点播收费之类的。而用户观看的节目集包括免费的卫视直播节目以及付费点播的节目,也就是与备选推荐节目集有交集)

Source Code:
UserCF.py

# 代码说明:
# 基于用户的协同过滤算法的具体实现

import math
import numpy as np
import pandas as pd

# 借助pearson相关系数进行修正后的余弦相似度计算公式,计算两个用户之间的相似度
# 记  sim(user1, user2) = sigma_xy /sqrt(sigma_x * sigma_y)
# user1和user2都表示为[[节目名称,隐性评分], [节目名称,隐性评分]],如user1 = [['节目一', 3.2], ['节目四', 0.2], ['节目八', 6.5], ...]

def calCosDistByPearson(user1, user2):
    x = 0.0
    y = 0.0

    sigma_xy = 0.0
    sigma_x = 0.0
    sigma_y = 0.0

    for item in user1:
        x += item[1]

    # user1对其看过的所有节目的平均评分
    average_x = x / len(user1)

    for item in user2:
        y += item[1]

    # user2对其看过的所有节目的平均评分
    average_y = y / len(user2)

    for item1 in user1:
        for item2 in user2:
            if item1[0] == item2[0]:  # 对user1和user2都共同看过的节目才考虑进去
                sigma_xy += (item1[1] - average_x) * (item2[1] - average_y)
                sigma_x += (item1[1] - average_x) * (item1[1] - average_x)
                sigma_y += (item2[1] - average_y) * (item2[1] - average_y)

    if sigma_x == 0.0 or sigma_y == 0.0:  # 若分母为0,相似度为0
        return 0

    return sigma_xy/math.sqrt(sigma_x * sigma_y)


# 创建所有用户的观看信息(包含隐性评分信息),“从用户到节目”
# 格式例子:users_to_items = {用户一:[['节目一', 3.2], ['节目四', 0.2], ['节目八', 6.5]], 用户二: ... }
def createUsersDict(df):
    
    (m, n) = df.shape
    data_array = np.array(df.iloc[:m + 1, 1:])
    users_names = np.array(df.iloc[:m + 1, 0]).tolist()
    items_names = np.array(df.columns)[1:]

    users_to_items = {}

    for i in range(len(users_names)):
        user_and_scores_list = []
        for j in range(len(items_names)):
            if data_array[i][j] > 0:
                user_and_scores_list.append([items_names[j], data_array[i][j]])
        users_to_items[users_names[i]] = user_and_scores_list

    return users_to_items

# 创建所有节目被哪些用户观看的字典,也就是创建“从节目到用户”的倒排表items_and_users
# items_to_users = {节目一: [用户一, 用户三], 节目二: ... }
def createItemsDict(df):
    
    (m, n) = df.shape
    data_array = np.array(df.iloc[:m + 1, 1:])
    users_names = np.array(df.iloc[:m + 1, 0]).tolist()
    items_names = np.array(df.columns)[1:]
    items_to_users = {}

    for i in range(len(items_names)):
        users_list = []
        for j in range(len(users_names)):
            if data_array[j][i] > 0:
                users_list.append(users_names[j])
        items_to_users[items_names[i]] = users_list

    return items_to_users


# 找出与用户user_name相关的所有用户(即邻居)并依照相似度排序
# neighbors_distance = [[用户名, 相似度大小], [...], ...] = [['用户四', 0.78],[...], ...]
def findSimilarUsers(users_dict, items_dict, user_name):

    neighbors = []   # neighbors表示与该用户看过相同节目的所有用户

    for items in users_dict[user_name]:
        for neighbor in items_dict[items[0]]:
            if neighbor != user_name and neighbor not in neighbors:
                neighbors.append(neighbor)

    # 计算该用户与其所有邻居的相似度并降序排序
    neighbors_distance = []
    for neighbor in neighbors:
        distance = calCosDistByPearson(users_dict[user_name], users_dict[neighbor])
        neighbors_distance.append([neighbor, distance])

    neighbors_distance.sort(key=lambda item: item[1], reverse=True)

    return neighbors_distance


# 基于用户的协同过滤算法
# K为邻居个数,是一个重要参数,参数调优时使用
def userCF(user_name, users_dict, items_dict, K, all_items_names_to_be_recommend):

    # recommend_items = {节目名:某个看过该节目的该用户user_name的邻居与该用户的相似度, ...}
    recommend_items = {}
    # 将上面的recommend_items转换成列表形式并排序为recommend_items_sorted = [[节目一, 该用户对节目一的感兴趣程度],[...], ...]
    recommend_items_sorted = []

    # 用户user_name看过的节目
    items_user_saw = []
    for item in users_dict[user_name]:
        items_user_saw.append(item[0])

    # 找出与该用户相似度最大的K个用户(邻居)
    similar_users = findSimilarUsers(users_dict, items_dict, user_name)
    if len(similar_users) < K:
        k_similar_user = similar_users
    else:
        k_similar_user = similar_users[:K]

    # 得出对该用户的推荐节目集
    for user in k_similar_user:
        for item in users_dict[user[0]]:
            # 该用户user_name没有看过的节目才添加进来,才可以推荐给该用户
            if item[0] not in items_user_saw:
                # 而且该节目必须是在备选推荐节目集中
                if item[0] in all_items_names_to_be_recommend:
                    if item[0] not in recommend_items:
                        # recommend_items是一个字典。第一次迭代中,表示将第一个邻居用户与该用户的相似度加到节目名上,后续迭代如果有其他邻居用户也看过该节目,
                        # 也将其与该用户的相似度加到节目名上,迭代的结果就是该用户对该节目的感兴趣程度
                        recommend_items[item[0]] = user[1]

                    else:
                        # 如果某个节目有k个邻居用户看过,则将这k个邻居用户与该用户的相似度相加,得到该用户对某个节目的感兴趣程度
                        recommend_items[item[0]] += user[1]

    for key in recommend_items:
        recommend_items_sorted.append([key, recommend_items[key]])

    # 对推荐节目集按用户感兴趣程度降序排序
    recommend_items_sorted.sort(key=lambda item: item[1], reverse=True)

    return recommend_items_sorted

# 输出推荐给该用户的节目列表
# max_num:最多输出的推荐节目数
def printRecommendItems(recommend_items_sorted, max_num):
    count = 0
    for item, degree in recommend_items_sorted:
        print("节目名:%s, 推荐指数:%f" % (item, degree))
        count += 1
        if count == max_num:
            break

# 主程序
if __name__ == '__main__':

    all_users_names = ['A', 'B', 'C']

    df1 = pd.read_excel("D:\Recommender Systems\备选推荐节目集及所属类型01矩阵.xlsx")
    (m1, n1) = df1.shape
    # 按照"备选推荐节目集及所属类型01矩阵"的列序排列的所有用户观看过的节目名称
    items_to_be_recommended_names = np.array(df1.iloc[:m1 + 1, 0]).tolist()

    df2 = pd.read_excel("D:\Recommender Systems\所有用户对其看过的节目的评分矩阵.xlsx")

    # users_dict = {用户一:[['节目一', 3.2], ['节目四', 0.2], ['节目八', 6.5]], 用户二: ... }
    users_dict = createUsersDict(df2)
    # items_dict = {节目一: [用户一, 用户三], 节目二: [...], ... }
    items_dict = createItemsDict(df2)

    for user in all_users_names:
        print("对于用户 %s 的推荐节目如下:" % user)
        recommend_items = userCF(user, users_dict, items_dict, 2, items_to_be_recommended_names)
        printRecommendItems(recommend_items, 3)
        print()

Result:
7

PS: 用户相似度计算产生负值为正常现象


####c. 实现混合推荐
将CB和userCF的两个推荐集按一定比例混合

Source Code:
CB_Mixture_userCF.py

from recommender_system.CB import *
from recommender_system.UserCF import *


# 输出推荐给该用户的节目列表
# max_num:最多输出的推荐节目数
def printRecommendItems(recommend_items_sorted, max_num):
    count = 0
    for item, degree in recommend_items_sorted:
        print("节目名:%s, 推荐指数:%f" % (item, degree))
        count += 1
        if count == max_num:
            break

if __name__ == '__main__':

    all_users_names = ['A', 'B', 'C']
    all_labels = ['教育', '戏曲', '悬疑', '科幻', '惊悚', '动作', '资讯', '武侠', '剧情', '警匪', '生活', '军事', '言情', '体育', '冒险', '纪实',
                  '少儿教育', '少儿', '综艺', '古装', '搞笑', '广告']
    labels_num = len(all_labels)

    df1 = pd.read_excel("D:\Recommender Systems\所有用户对其看过的节目的评分矩阵.xlsx")
    (m1, n1) = df1.shape
    # 所有用户对其看过的节目的评分矩阵
    # data_array1 = [[0.1804 0.042 0.11  0.07  0.19  0.56  0.14  0.3  0.32 0, ...], [...]]
    data_array1 = np.array(df1.iloc[:m1 + 1, 1:])
    # 按照"所有用户对其看过的节目的评分矩阵"的列序排列的所有用户观看过的节目名称
    items_users_saw_names1 = df1.columns[1:].tolist()

    # users_dict = {用户一:[['节目一', 3.2], ['节目四', 0.2], ['节目八', 6.5]], 用户二: ... }
    users_dict = createUsersDict(df1)
    # items_dict = {节目一: [用户一, 用户三], 节目二: [...], ... }
    items_dict = createItemsDict(df1)

    df2 = pd.read_excel("D:\Recommender Systems\所有用户看过的节目及所属类型的01矩阵.xlsx")
    (m2, n2) = df2.shape
    data_array2 = np.array(df2.iloc[:m2 + 1, 1:])
    # 按照"所有用户看过的节目及所属类型的01矩阵"的列序排列的所有用户观看过的节目名称
    items_users_saw_names2 = np.array(df2.iloc[:m2 + 1, 0]).tolist()

    # 为用户看过的节目建立节目画像
    items_users_saw_profiles = createItemsProfiles(data_array2, all_labels, items_users_saw_names2)

    # 建立用户画像users_profiles和用户看过的节目集items_users_saw
    (users_profiles, items_users_saw) = createUsersProfiles(data_array1, all_users_names, items_users_saw_names1,
                                                            all_labels, items_users_saw_profiles)

    df3 = pd.read_excel("D:\Recommender Systems\备选推荐节目集及所属类型01矩阵.xlsx")
    (m3, n3) = df3.shape
    data_array3 = np.array(df3.iloc[:m3 + 1, 1:])
    # 按照"备选推荐节目集及所属类型01矩阵"的列序排列的所有用户观看过的节目名称
    items_to_be_recommended_names = np.array(df3.iloc[:m3 + 1, 0]).tolist()

    # 为备选推荐节目集建立节目画像
    items_to_be_recommended_profiles = createItemsProfiles(data_array3, all_labels, items_to_be_recommended_names)


    # 两种推荐算法后融合,也就是将两种推荐算法对某个用户分别产生的两个推荐节目集按不同比例混合,得出最后的对该用户的推荐结果

    # 对于每个用户推荐topN个节目,在两种推荐算法产生的推荐集中分别选取比例为w1和w2的推荐结果,CB占w1, userCF占w2
    # w1 + w2 = 1 且 w1 * topN + w2 * topN = topN

    topN = 5

    w1 = 0.7
    w2 = 0.3

    # 从CB的推荐集中选出前topW1项
    topW1 = int(w1 * topN)

    # 从userCF的推荐集中选出前topW2项
    topW2 = topN-topW1

    for user in all_users_names:

        # 对于用户user的最终混合推荐节目集
        recommend_items = []

        # CB
        # recommend_items1 =  [[节目名, 该节目与该用户user画像的相似度], ...]
        recommend_items1 = contentBased(users_profiles[user], items_to_be_recommended_profiles, items_to_be_recommended_names, all_labels, items_users_saw[user])
        len1 = len(recommend_items1)

        if len1 <= topW1:
            recommend_items = recommend_items + recommend_items1
        else:
            recommend_items = recommend_items + recommend_items1[:topW1]


        # userCF
        # recommend_item2 = [[节目名, 该用户user对该节目的感兴趣程度],...]
        recommend_items2 = userCF(user, users_dict, items_dict, 2, items_to_be_recommended_names)
        len2 = len(recommend_items2)

        if len2 <= topW2:
            recommend_items = recommend_items + recommend_items2
        else:
            recommend_items = recommend_items + recommend_items2[:topW2]

        # 将推荐结果按推荐指数降序排序
        recommend_items.sort(key=lambda item: item[1], reverse=True)

        print("对于用户 %s 的推荐节目如下" % user)
        printRecommendItems(recommend_items, 5)
        print()

Result:
8


5. 相关说明

a. 利用改进的皮尔逊相关系数计算用户之间的相似度

9

Pearson相关系数用来衡量两个数据集合之间的相似性。Pearson相关系数的一个优点是可以避免评分等级膨胀(grade inflation)的问题,也就是说有的用户可能倾向于对所有的电影都给比较高的分数,而有的用户则会比较苛刻,给分都比较低。对于这种情况,Pearson相关系数可以处理。计算用户x与用户y之间的相似度的公式如上面所示。


其中x,y表示具体的用户,Sxy表示用户x与用户y看过的节目的交集,rxs为用户x对于节目s的评分,-rx为用户x对于其所看过的所有节目的平均评分。


PS: 计算用户x和y之间的相似度时并不是去拿原始的评分向量去计算,而是只关注他们所看过的节目的交集Sxy。这是因为一个用户只对很少的物品有过评分,这样用户评分向量是个高度稀疏的向量,采用Pearson相关系数计算两个用户的相似度时很不准。


b. 基于用户之间的相似度得到用户对于节目的感兴趣程度

2

得到用户之间的兴趣相似度后,userCF算法会给用户推荐和他兴趣最相似的K个用户喜欢的物品。上面的公式度量了userCF算法中用户u对物品i的感兴趣程度。其中,S(u,K)包含和用户u兴趣最接近的K个用户,N(i)是对物品i有过行为的用户集合,w是用户u和用户v的兴趣相似度,r是用户v对物品i的兴趣,这里令所有的r=1.



Reference Materials :

1. [推荐算法]基于用户的协同过滤算法
2. 基于用户的协同过滤算法原理分析及代码实现
3. python 实现协同过滤推荐算法
4. 一个简单的基于内容的推荐算法]
5. 10 Minutes to pandas
6. 喻玲. 面向家庭用户的互联网电视资源推荐模型研究[D]
7. 付小飞. 基于用户画像的移动广告推荐技术的研究与应用[Z]
8. 孙亮. 一种基于云计算平台的网络电视混合推荐方法的研究[D]
9. 杨武、唐瑞、卢玲. 基于内容的推荐与协同过滤融合的新闻推荐方法[Z]
10. 项亮.推荐系统实践




相关数据&代码获取:

其中代码包名为”recommender_system“,表格数据包名为”Recommender Systems“,将表格数据包解压到电脑D盘即可运行上述代码.

该实例中所用到的表格
代码包

没有积分的小伙伴,我已将项目开源(代码和数据包),可以直接获取,欢迎star、fork:
项目地址

  • 39
    点赞
  • 181
    收藏
    觉得还不错? 一键收藏
  • 31
    评论
评论 31
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值