基于用户的协同过滤算法的理解与简单实现

概述

最近在做一个有关图书推荐系统的项目,因此就涉及到了协同过滤算法。自己主要负责基于用户的协同过滤算法(User-CF)的实现,因此阅读了部分书籍与文章,也基于组员爬取的豆瓣图书评论数据,参考了一个User-CF的源码做了一个简单的实现。

本文将首先简单介绍一下最基础的基于用户的协同过滤算法,再介绍基于皮尔逊相关度以及豆瓣图书评论数据的User-CF的实现。

简单介绍

基于用户的协同过滤算法(User-CF)属于基于领域的算法。

基于用户的协同过滤算法主要包括两个步骤。

  1. 找到和目标用户兴趣相似的用户集合。
  2. 找到这个集合中的用户喜欢的,且目标用户没有听说过的物品推荐给目标用户

相似度算法

步骤一的关键就是计算两个用户之间的相似度,以下列举两种比较常见的相似度计算方法。

给定用户u和用户v,令N(u)表示用户u曾经有过正反馈的物品集合,令N(v)为用户v曾经有过正反馈的物品集合。

  • Jaccard公式
Jaccard公式
  • 余弦相似度
余弦相似度

另外还有一种基于向量的相似度算法,也是在代码实现中将使用的相似度计算方法。具体的关于Pearson相关系数的介绍会在下文中阐述。

  • Pearson相关系数
Pearson相关系数

关于这三种相似度之间的差别和选择标准,自己也还没有仔细去研究。之后有时间的话会补充。

计算举例

计算单独的两个用户之间的相似度

下面将简单介绍利用余弦相似度计算方法得到两个用户之间的相似度。

计算举例

例如,用户A对物品{a,b,d}有过行为,用户B对物品{a,c}有过行为,利用余弦相似度公式计算用户A和用户B的兴趣相似度为:

A和B之间的相似度

同理,我们可以算出用户A和用户C、D的相似度:

A和C、D之间的相似度

计算用户两两之间的相似度矩阵

对两两用户都利用余弦相似度计算相似度。这种方法的时间复杂度是O(N^2)。耗时很大。

因此我们需要建立一张物品到用户之间的倒排表,这样就能排除对根本没有任何联系的用户之间的相似度计算。

再根据倒排表计算共同评分过的物品的矩阵。

如下图所示

计算共同评分过的物品的矩阵

矩阵中的每一个数值都是余弦相似度中的分子部分,然后将分子除以分母可以得到最终的用户兴趣相似度。

即可以将上图中的共同评分过的物品的矩阵转换为用户之间的相似度矩阵,且只用计算非0的部分。

例如,计算A与B的用户相似度。用户A和B的矩阵中的值为1,即他们共同交集的物品为1。A总共评分过的物品为3,B总共评分过的物品为2。根据余弦相似度算法,得出相似度为

计算结果

筛选出K个与目标用户最相似的用户

得到了用户之间的相似度后,算法会给用户推荐和他兴趣最相似的K个用户喜欢的物品

以下公式度量了算法中用户u对物品i的感兴趣程度

用户u对物品i的感兴趣程度

其中,S(u,K)包含和用户u兴趣最相近的K个用户,N(i)是对物品i有过行为的用户集合,Wuv是用户u和用户v的兴趣相似度,rvi为1

举例

对目标用户A进行推荐。选取K=3,用户A对物品c,e没有过行为,因此可以把这两个物品推荐给用户A。根据算法,用户A对物品c,e的兴趣是

用户A对物品c,e的兴趣

基于豆瓣图书评论数据的简单实现

评论数据格式介绍

自己对评论数据作了一个简单的清洗,数据格式如下:

图书名+用户名+评分信息+评分时间

中间以"\t"作为分隔符

部分图书数据如下图所示

部分图书数据

使用Pearson相关系数计算用户相似度

之前介绍的协同过滤算法并没有引进评分的概念,引进评分之后,就不能简单地利用集合去计算了,而是应该利用向量去计算。

Pearson相关系数将两个用户共同评分的n个项目看做一组向量,计算两个用户在这n个项目上评分的相关性,减去用户平均评分是基于用户评分尺度的考量,公式如下:

Pearson相关系数

其中Iuv是用户u和v都评过分的项目的集合,ru是用户u所有评分的平均分

计算用户u对物品i评分的预测

得到用户相似度后,接下来就是根据目标用户u的近邻用户得到u对于物品i的评分预测,公式如下:

用户u对物品i的评分预测

评分预测越高,目标用户u喜欢该物品的概率也就越高。

最终按照评分预测的高低可得到最适合推荐给用户u的物品列表,在这个实例里也就是图书列表。

Python实现

导入数据

导入的原始数据为训练集(trainSet),利用该训练集生成图书-用户倒排表(bookUser),再利用倒排表生成用户-用户共同评分过的图书矩阵(u2u)

# encoding=utf-8
from math import sqrt
import codecs

def loadData():
    # 训练集
    trainSet = {}
    # 看过该本书的所有用户集合
    bookUser = {}
    # 用户-用户共有书籍矩阵
    u2u = {}

    TrainFile = '/Users/cjl/Desktop/comments.txt'

    # 加载训练集
    # trainset {"userName", {"bookName, rating"}}
    # bookUser {"bookName", ["username1", "username2", ...]} 即为物品-用户倒排表
    for line in codecs.open(TrainFile, "r", "utf-8"):
        (bookName, userName, rating, timestamp) = line.strip().split("\t")
        trainSet.setdefault(userName, {})
        trainSet[userName].setdefault(bookName, float(rating))

        bookUser.setdefault(bookName, [])
        bookUser[bookName].append(userName.strip())

    # 生成用户-用户共有书籍矩阵
    for b in bookUser.keys():
        # 遍历特定的书籍中所有的用户
        for u in bookUser[b]:
            u2u.setdefault(u, {})
            for n in bookUser[b]:
                if u != n:
                    u2u[u].setdefault(n,[])
                    u2u[u][n].append(b)

    return trainSet, u2u

简单测试导入的数据以及生成的u2u是否正确

测试导入数据

计算一个用户的平均评分

def getAverageRating(user, trainSet):
    average = (sum(trainSet[user].values())*1.0) / len(trainSet[user].keys())
    return average

简单测试

简单测试

计算用户相似度

遍历u2u,利用皮尔逊相关系数的计算,将共同评分过的书籍矩阵转换为两个用户之间的相似度矩阵

# 计算用户相似度
def getUserSim(u2u, trainSet):
    userSim = {}
    # 计算用户相似度
    # 对每个用户u
    for u in u2u.keys():
        if u != '':
            # 将用户u加入userSim中设为key,该用户对应一个字典
            userSim.setdefault(u, {})
            # 获取用户u对电影的平均评分
            average_u_rate = getAverageRating(u, trainSet)
            # 对与用户u相关的每个用户n
            for n in u2u[u].keys():
                if n != '':
                    # 将用户n加到用户u的字典中
                    userSim[u].setdefault(n,0)
                    # 获取用户n对电影的平均评分
                    average_n_rate = getAverageRating(n, trainSet)
                    # 皮尔逊相关系数的分子部分
                    part1 = 0
                    # 皮尔逊相关系数的分母的一部分
                    part2 = 0
                    # 皮尔逊相关系数的分母的一部分
                    part3 = 0
                    # 对用户u和用户n的共有的每个电影
                    for m in u2u[u][n]:
                        part1 += (trainSet[u][m] - average_u_rate) * (trainSet[n][m] - average_n_rate) * 1.0
                        part2 += pow(trainSet[u][m] - average_u_rate, 2) * 1.0
                        part3 += pow(trainSet[n][m] - average_n_rate, 2) * 1.0

                    part2 = sqrt(part2)
                    part3 = sqrt(part3)
                    # 若分母为0,相似度为0
                    if part2 == 0 or part3 == 0:
                        userSim[u][n] = 0
                    else:
                        userSim[u][n] = part1 / (part2 * part3)
    return userSim

简单测试

输出相似用户

寻找最近邻用户并生成推荐结果

遍历trainSet中的所有用户,生成一个评分预测pred

# 寻找用户最近邻并生成推荐结果
def getRecommendations(N, trainSet, userSim):
    pred = {}
    # 对每个用户
    for user in trainSet.keys():
        # 生成空预测表
        pred.setdefault(user, {})
        # 获取该用户评过分的书籍
        interacted_items = trainSet[user].keys()
        # 获取该用户的平均评分
        average_u_rate = getAverageRating(user, trainSet)
        userSimSum = 0
        # 选取N个最相似的用户
        # lambda x:x[1] 根据用户相似度进行比较
        if user.strip() != '':
            simUser = sorted(userSim[user.strip()].items(), key=lambda x: x[1], reverse=True)[0:N]
            # 遍历相似用户的用户名和相似度
            for n, sim in simUser:
                # 得到该近邻用户的平均评分
                average_n_rate = getAverageRating(n, trainSet)
                # 对该用户所有近邻用户相似度求和
                userSimSum += sim 
                # 遍历该近邻用户的所有评分书籍和具体评分
                for m, nrating in trainSet[n].items():
                    # 如果这本书该用户已经看过,则跳过
                    if m in interacted_items:
                        continue
                    # 否则将这本书以及这本书的推荐指数加到这名用户的推荐列表中
                    else:
                        pred[user].setdefault(m, 0)
                        pred[user][m] += (sim * (nrating - average_n_rate))
            # 遍历这名用户推荐列表中的所有书籍,更新评分预测
            for m in pred[user].keys():
                if userSimSum == 0:
                    pred[user][m] = average_u_rate
                else:
                    pred[user][m] = average_u_rate + (pred[user][m] * 1.0) / userSimSum
    return pred

简单测试

输出推荐书籍

参考内容

书籍:《推荐系统实践》

博客:http://blog.csdn.net/anryyang/article/details/23563237

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容