基于用户行为分析的推荐算法是个性化推荐系统的重要算法,一般将这种类型的算法称为协同过滤算法。协同过滤就是指用户可以齐心协力,通过不断地和网站互动,使自己的推荐列表能够不断过滤掉自己不感兴趣的物品,从而越来越满足自己的需求。
用户行为数据简介
用户行为数据在网站上最简单的存在形式就是日志。网站在运行过程中都产生大量原始日志raw log,并将其存储在文件系统中。很多互联网业务会把多种原始日志按照用户行为汇总成会话日志session log,其中每个会话表示一次用户行为和对应的服务。会话日志通常存储在分布式数据仓库中,如支持离线分析的Hadoop Hive和支持在线分析的Google Dremel。
用户行为在个性化推荐系统中一般分两种---显性反馈行为(explicit feedback)和隐性反馈行为(implicit feedback):
- 显性反馈行为: 包括用户明确表示对物品喜好的行为。不同网站收集显性反馈行为的主要方式是评分和喜欢/不喜欢。
- 隐性反馈行为: 指那些不能明确反应用户喜好的行为。最具代表性的隐性反馈行为就是页面浏览行为。用户浏览一个物品的页面并不代表用户一定喜欢这个页面展示的物品,比如可能因为这个页面链接显示在首页,用户更容易点击它而已。相比显性反馈,隐性反馈虽然不明确,但数据量更大。
显性反馈数据和隐性反馈数据的比较.
显性反馈数据 | 隐性反馈行为 | |
---|---|---|
用户兴趣 | 明确 | 不明确 |
数量 | 较少 | 庞大 |
存储 | 数据库 | 分布式文件系统 |
实时读取 | 实时 | 有延迟 |
正负反馈 | 都有 | 只有正反馈 |
按照反馈的方向分,可以分为正反馈和负反馈。正反馈指用户的行为倾向于指用户喜欢该物品,负反馈指用户的行为倾向于指用户不喜欢该物品。在显性反馈中,很容易区分一个用户行为是正反馈还是负反馈,而在隐性反馈行为中,就相对比较难以确定。
一般来说,不同的数据集包含不同的行为,目前比较有代表性的数据集有下面几个:
- 无上下文信息的隐性反馈数据集: 每一条行为记录仅仅包含用户ID和物品ID。
- 无上下文信息的显性反馈数据集: 每一条记录包含用户ID、物品ID和用户对物品的评分;
- 有上下文信息的隐性反馈数据集: 每一条记录包含用户ID、物品ID和用户对物品产生行为的时间戳;
- 有上下文信息的显性反馈数据集: 每一条记录包含用户ID、物品ID和用户对物品的评分和评分行为发生的时间戳。
用户行为分析
在利用用户行为数据设计推荐算法之前,研究人员首先需要对用户行为数据进行分析,了解数据中蕴含的一般规律,才能对算法的设计起到指导作用[相当与ML中数据探索]。
用户活跃度和物品流行度的分布
[物品的流行度指对物品产生过行为的用户总数]
很多互联网数据的研究发现,互联网上的很多数据分布都满足一种称为Power Law的分布,这个分布在互联网领域也称为长尾分布。
(f(x) = alpha x^k)
很多研究人员发现,用户行为数据也蕴含这种长尾分布的规律。
物品流行度和用户活跃度都近似于长尾分布。
用户活跃度和物品流行度的关系
仅仅基于用户行为数据设计的推荐算法一般称为协同过滤算法。协同过滤算法分为基于邻域的方法(neighborhood-based)、隐语义模型(latent factor model)、基于图的随机游走算法(random walk on graph)等。在这些方法中,最著名的、在业界得到最广泛应用的算法是基于邻域的方法,而基于邻域的方法主要包含下面两种算法:
- 基于用户的协同过滤算法: 给用户推荐和他兴趣相似的其他用户喜欢的物品;
- 基于物品的协同过滤算法: 给用户推荐和他之前喜欢的物品相似的物品。
基于邻域的算法
基于邻域的算法分为两类:一类是基于用户的协同过滤算法,另一类是基于物品的协同过滤算法。
基于用户的协同过滤算法
基于用户的协同过滤算法是推荐系统中最古老的算法。
1.基础算法
基于用户的协同过滤算法主要包括两个步骤:
- 找到和目标用户兴趣相似的用户集合;
- 找到这个集合中的用户喜欢的,且目标用户没有听说过的物品推荐给目标用户。
步骤一的关键在于计算两个用户的兴趣相似度。协同过滤算法主要利用行为的相似度计算兴趣的相似度。给定用户u和用户v,令N(u)表示用户u曾经有过正反馈的物品集合,令N(v)为用户v曾经有过正反馈的物品集合。可以通过Jaccard公式简单计算u和v的兴趣相似度:
余弦相似度计算:
2.基于相似度计算的改进
原来的相似度计算公式,如余弦相似度计算方法太过于粗糙。如果两个用户对冷门物品采取过同样的行为更能说明他们兴趣的相似度。John S. Breese在论文中提出如下公式,根据用户行为计算用户的兴趣相似度:
其中,N(i)表示物品i的流行度。公式惩罚了用户u和v共同兴趣列表中热门物品对他们相似度的影响。
基于用户的协同过滤算法的缺点:首先,随着网站的用户数目越来越大,计算用户兴趣相似度矩阵越来越困难,其运算时间复杂度和空间复杂度的增长和用户的增长近似于平方关系;其次,基于用户的协同过滤很难对推荐结果做出解释。
基于物品的系统过滤算法
基于物品的协同过滤item-based collaborative filtering算法是目前业界应用最多的算法。
1.基础算法
基于物品的协同过滤算法(简称ItemCF)给用户与推荐那些和他们之前喜欢的物品相似的物品。ItemCF算法并不利用物品的内容属性计算物品之间的相似度,主要通过分析用户的行为记录就是那物品之间的相似度。该算法认为,物品A和物品B具有很大的相似性是因为喜欢物品A的用户大都也喜欢物品B。
基于物品的协同过滤算法主要分为两步:
- 计算物品之间的相似度;
- 根据物品的相似度和用户的历史行为给用户生成推荐列表。
购买了该商品的用户也经常经常购买的其他商品,从这句话的定义出发,给出定义物品相似度的计算公式:
其中,分母|N(i)|是喜欢物品i的用户数,分子是同事喜欢物品i和物品j的用户数。
但是,上述公式存在一个问题,如果物品j很热门,很多人都喜欢,那么Wij就会很大,接近于1.因此,该公式会造成任务物品都会和热门的物品有很大的相似度,对致力于挖掘长尾信息的推荐系统来说不是一个好的特性。为了避免推荐出热门的物品,使用下面的公式:
这个公式惩罚了物品j的权重,因此减轻了热门物品会和很多物品相似的可能性。
从上面的定义可以看到,在协同过滤中两个物品产生相似度是因为它们共同被很多用户喜欢,也就是说每个用户都可以通过他们的历史兴趣列表给物品”贡献“相似度。
ItemCF算法计算物品相似度时,先建立一个用户-物品倒排表,然后对于每个用户,将他物品列表中的物品量量在共现矩阵C中加1.详细代码:
def ItemSimilarity(train):#train是用户-物品倒排表
C = dict()#物品i,j共现矩阵,用字典表示;
N = dict()#物品i的流行度--喜欢物品i的用户数目
for user, items in train.items():
for i in items:
N[i] += 1
for j in items:
if i != j:
C[i][j] = C[i].get(j, 0) + 1
W = dict()#物品相似度矩阵 字典
for i, related_items in C.items():
for j, cij in related_items.items():
W[i][j] = cij / math.sqrt(N[i]*N[j])
return W
在得到物品之间的相似度后,ItemCF通过如下公式计算用户u对一个物品j的兴趣:
这里N(u)是用户喜欢的物品集合,S(j,K)是和物品j最相似的K个物品的集合,wji是物品j和i的相似度,rui是用户u对物品i的兴趣。(对于隐反馈数据集,如果用户u对物品i有过行为,即可令rui=1.) 该公式的含义是:和用户历史上感兴趣的物品[N(u)里]越相似的物品,越有可能在用户的推荐列表中获得比较高的排名。 [在S集合中筛选掉已经喜欢的物品]实现代码:
def Recommendation(train, user_id, W, K):
rank = dict()
ru = train[user_id]#用户喜欢物品字典,物品:rui(用户u对物品i的兴趣,默认为1)
for i, rui in ru.items():
# 选择物品i的相似度矩阵,并由大到小排序;然后选择前K个物品
si = sorted(W[i].items(),key=lambda a:a[1],reverse=True)
for j, wij in si[:K]:
if j in ru:#排除已经喜欢的物品
continue
rank[j] += wij * rui
return rank
ItemCF算法的一个优势是可以提供推荐解释,即利用用户历史上喜欢的物品为现在的推荐结果进行解释。带解释的ItemCF算法:
def Recommendation(train, user_id, W, K):
rank = dict()
reason = dict()
ru = train[user_id]
for i, rui in ru.items():
si = sorted(W[i].items(),key=lambda a:a[1], reverse=True)
for j, wij in si[:K]:
if j not in ru:
rank[j] += wij * rui
reason[j][i] = wij * rui
return rank, reason
对不同 K 值的测量可以看到:
- 准确率和召回率和 K 也不成线性关系;选择合适的K对获得最高精度是非常重要的;
- K 和流行度不完全正相关: 随着K的增加,结果流行度会逐渐提高,但当K增加大到一定程度,流行度就不会再有明显变化;
- K 增大会降低系统的覆盖率。
2.用户活跃度对物品相似度的影响
在协同过滤中两个物品苍生相似度是因为它们共同出现在很多用户的兴趣列表中。换句话说,每个用户的兴趣列表都对物品的相似度产生贡献。但每个用户的贡献不应该都相同。
John S.Breese在论文中提出一个IUF(Inversr User Frequence)用户活跃度对数的倒数的参数,他认为活跃用户对物品相似度的贡献应该小于不活跃的用户,提出应该增加IUF参数来修正物品相似度的计算公式:
N(i)、N(j)物品i,j的流行度;u是同时购买物品i和物品j的用户,N(u)是用户喜欢的物品数[用来表示用户u的活跃度]。上面公式只是对活跃用户做了一种软性的惩罚。
但对于很多过于活跃的用户,为了避免相似度矩阵过于稠密,在实际运算中一般直接忽略他的兴趣列表,不将其纳入到相似度计算的数据集中。ItemCF-IUF实现:
def ItemSimilarity(train):
C = dict()#分子
N = dict()#物品i的流行度
for u, items in train.items():
for i in items:
N[i] += 1
for j in items:
if i != j:
C[i][j] += 1 / math.log(1 + len(items)*1.0)
W = dict()#相似度矩阵
for i, related_items in C.items():
for j, cij in related_items.items():
W[i][j] = cij / math.sqrt(N[i] * N[j])
return W
3.物品相似度的归一化
在研究汇中发现如果将ItemCF的相似度矩阵按最大值归一化,可以提高推荐的准确率。其研究表明,如果已经得到了物品相似度矩阵w,可以用如下公式得到归一化之后的相似度矩阵w':
按照行进行归一化。归一化的好处不仅仅在于增加推荐的准确度,还可以提高推荐的覆盖率和多样性。
UserCF和ItemCF的综合比较
UserCF的推荐结果着重于反应和用户兴趣相似的小群体的热点,ItemCF的推荐结果着重于维系用户的历史兴趣。换句话说,UserCF的推荐更社会化,反映了用户所在的小型群体中物品的热门程度,而ItemCF的推荐更加个性化,反映了用户自己的兴趣传承。
从技术上考虑, UserCF 需要维护一个用户相似度的矩阵,而 ItemCF 需要维护一个物品相似度矩阵。从存储的角度说,如果用户很多,那么维护用户兴趣相似度矩阵需要很大的空间,同理,如果物品很多,那么维护物品相似度矩阵代价较大。
UserCF和ItemCF优缺点对比.
UserCF | ItemCF | |
---|---|---|
性能 | 适用于用户较少的场合,如果用户很多,计算用户相似度矩阵代价很大 | 适用于物品数明显小鱼用户数的场合,如果物品很多(网页),计算物品相似度矩阵代价很大 |
领域 | 时效性强,用户个性化兴趣不太明显的领域 | 长尾物品丰富,用户个性化需求强烈的领域 |
实时性 | 用户有新行为,不一定造成推荐结果的立即变化 | 用户有新行为,一定会导致推荐结果的实时变化 |
冷启动 | 在新用户对很少的物品产生行为的情况下,不能立即对他进行个性化推荐,因为用户性相似度表是每隔一段时间离线计算的 新物品上线后一段时间,一旦有用户对物品产生行为,就可以将新物品推荐给和对它产生行为的用户兴趣相似的其他用户 | 新用户只要对一个物品产生行为,就可以给他推荐和该物品相关的其他物品 但没有办法在不离线更新物品相似度表的情况下将新物品推荐给用户 |
推荐利用 | 很难提供令用户信服的推荐解释 | 利用用户的历史行为给用户做推荐解释,可以令用户比较信服 |
离线实验的性能在选择推荐算法时病不起决定作用。首先应该满足产品的需求,然后需要看实现代价。
隐语义模型
LFM(latent factor model)隐语义模型,该算法最早在文本挖掘领域被提出,用于找到文本的隐含语义。相关的名词有LSI、pLSI、LDA和Topic Model.
- 基础算法
隐语义模型的核心思想是通过隐含特征(latent factor)联系用户兴趣和物品。简单说就是对物品的兴趣分类,对于用户,首先确定他的兴趣分类,然后从分类中选择他可能喜欢的物品。基于兴趣分类的方法大概解决3个问题:
- 如何给物品分类?
- 如何确定用户对哪些类的物品感兴趣,以及感兴趣的程度?
- 对于一个给定的类,选择哪些属于这个类的物品推荐给用户,以及如何确定这些物品在一个类中的权重?
这里的对物品分类的问题,可以用隐含语义分析技术较好地解决。它基于用户行为统计做分类,和专家标记相比:
- 分类来源于对用户行为的统计,能代表各种用户的看法;
- 通过指定最终分类的个数,能控制分类的粒度;数目越大,分类粒度越细;
- 能给一个物品多个分类:隐语义模型计算物品属于每个类的权重,不是硬性地被分到某个类中;
- 带维度属性,属于多维度或同维度;
- 可以确定物品在某个分类中的权重:通过统计用户行为决定物品在每个类中的权重;
这些都是专家标记不能或者很难做到的。
LFM通过如下公式计算用户u对物品i的兴趣:
公式中puk和qik是模型的参数,其中puk度量了用户u的兴趣和第k个隐类的关系,而qik度量了第k个隐类和物品i之间的关系。
这两个参数的计算方式需要一个训练集,对于每个用户u,训练集里包含了用户u喜欢的物品和不感兴趣的物品,通过学习这个数据集,就可以获得上面的模型参数。
推荐系统的用户行为分为显性反馈和隐性反馈。LFM在显性反馈数据(评分数据)上解决评分预测问题并达到了很好的精度。如果是隐性数据集,这种数据集的特点是只有正样本(用户喜欢什么物品),没有负样本(用户对什么物品不感兴趣)。
在隐性反馈数据集上应用LFM解决TopN推荐的第一个关键问题就是如何给每个用户生成负样本。
对负样本采样时应该遵循以下原则:
- 对每个用户,要保证正负样本的平衡(数目相似);
- 对每个用户采样负样本时,要选取那些很热门,而用户却没有行为的物品。
一般认为,很热门而用户却没有行为更加代表用户对这个物品不感兴趣。因为对于冷门的物品,用户可能是压根没在网站中发现这个物品,所以谈不上是否感兴趣。
需要优化的损失函数如下:
其中,后两项是用来防止过拟合的正则化项,lambda可以通过实验获得。通过梯度下降算法对损失函数进行优化求解,得到两个参数指。
在LFM中,重要的参数有4个:
- 隐特征的个数F;
- 学习速率alpha;
- 正则化参数lambda;
- 负样本/正样本比例radio。
通过实验发现,radio对LFM的性能影响最大。
2.LFM和基于邻域的方法的比较
LFM是一种基于机器学习的方法,具有比较好的理论基础。和基于邻域的方法相比,各有优缺点。
- 理论基础 LFM具有比较好的理论基础,是一种学习方法,通过优化一个设定的指标建立最优的模型。基于邻域的方法更多的是一种基于统计的方法,并没有学习过程。
- 离线计算的空间复杂度 基于邻域的方法需要维护一张离线的相关表。
基于图的模型
用户行为很容易用二分图表示,因此很多图的算法都可以应用到推荐系统中。
1.用户行为数据的二分图表示
在研究基于图的模型之前,首先需要将用户行为数据表示成图的形式。这里讨论用户行为数据是由一系列二元组组成的,其中每个二元组(u,i)表示用户u对物品i产生过行为;这种数据集很容易用一个二分图表示。
2.基于图的推荐算法
在二分图上给用户进行个性化推荐。如果将个性化推荐算法放到二分图模型上,那么给用户u推荐物品的任务就可以转换为度量用户顶点vu和与vu没有边直接相连的物品节点在图上的相关性,相关性越高的物品在推荐列表中的权重就越高。
度量图中两个顶点之间相关性的方法有很多,但一般来说图中顶点的相关性主要取决于下面3个因素:
- 两个顶点之间的路径数;
- 两个顶点之间路径的长度;
- 两个顶点之间的路径经过的顶点。
相关性高的一堆顶点一般具有如下特征:
- 两个顶点之间有很多路径相连;
- 连接两个顶点之间的路径长度都比较短;
- 连接两个顶点之间的路径不会经过出度比较大的顶点。
基于上面3个主要因素,设计了很多计算图中顶点之间相关性的方法。比如随机游走PersonalRank算法。
假设要给用户u进行个性化推荐,可以从用户u对应的节点vu开始在用户物品二分图上进行随机游走。游走到任何一个节点时,首先按照概率alpha决定是继续游走,还是停止这次游走并从vu节点开始重新游走。如果决定继续游走,那么就从当前节点指向的节点中按照均匀分布随机选择一个节点作为游走下次经过的节点。这样,经过很多次随机游走后,每个物品节点被访问到的概率后收敛到一个数。最终的推荐列表中物品的权重就是物品节点的访问概率。
表示公式如:
alpha游走概率,1-alpha停留概率;
代码实现:
def PersonalRank(G, alpha, root, max_step):
rank = dict()#推荐结果
rank = {x : 0 for x in G.keys()}
rank[root] = 1
for k in range(max_step):
tmp = {x : 0 for x in G.keys()}
#取节点i和它的出边尾节点集合ri
for i, ri in G.items():
#取i->j边的尾节点j以及边E(i,j)的权重wij, 边的权重都为1,
for j, wij in ri.items():
if j not in tmp:
tmp[j] = 0
tmp[j] += alpha * rank[i] / (1.0 * len(ri))
if j == root:
tmp[j] += 1 - alpha
rank = tmp
return rank
虽然PersonalRank算法可以通过随机游走进行比较好的理论解释,但该算法在时间复杂度上有明显的缺点。因为在为每个用户进行推荐时,都需要在整个用户物品二分图上进行迭代,知道整个图上的每个顶点的PR值收敛。这一过程时间复杂度非常高,不仅无法在线提供实时推荐,甚至离线生成推荐结果也很耗时。
为了解决PersonalRank每次都需要在全图迭代并造成时间复杂度高的问题,给出两种解决方法。第一种,减少迭代次数,在收敛之前就停止。会影响最终的精度,但一般来说影响不会特别大;另一种就是从矩阵论出发,重新设计算法。