2.1 用户行为数据简介
在电子商务网站中行为主要包括网页浏览、购买、点击、评分和评论等。
用户行为在个性化推荐系统中一般分两种——显性反馈行为(explicit feedback)和隐性反馈行为(implicit feedback)。显性反馈行为包括用户明确表示对物品喜好的行为。隐性反馈行为指的是那些不能明确反应用户喜好的行为。最具代表性的隐性反馈行为就是页面浏览行为。
按照反馈的明确性分,用户行为数据可以分为显性反馈和隐性反馈,但按照反馈的方向分,又可以分为正反馈和负反馈。正反馈指用户的行为倾向于指用户喜欢该物品,而负反馈指用户的行为倾向于指用户不喜欢该物品。在显性反馈中,很容易区分一个用户行为是正反馈还是负反馈,而在隐性反馈行为中,就相对比较难以确定。
一个用户行为可表示为6部分,即产生行为的用户和行为的对象、行为的种类、产生行为的上下文、行为的内容和权重。
目前比较有代表性的数据集有下面几个。
无上下文信息的隐性反馈数据集 每一条行为记录仅仅包含用户ID和物品ID。
Book-Crossing(http://www2.informatik.uni-freiburg.de/~cziegler/BX/)就是这种类型的数据集。
无上下文信息的显性反馈数据集 每一条记录包含用户ID、物品ID和用户对物品的评分。
有上下文信息的隐性反馈数据集 每一条记录包含用户ID、物品ID和用户对物品产生行为的时间戳。Lastfm数据集(https://www.dtic.upf.edu/~ocelma/MusicRecommendationDataset/lastfm-1K.html)就是这种类型的数据集。
有上下文信息的显性反馈数据集 每一条记录包含用户ID、物品ID、用户对物品的评分和评分行为发生的时间戳。Netflix Prize(https://netflixprize.com/index.html)提供的就是这种类型的数据集。
2.2 用户行为分析
2.2.1 用户活跃度和物品流行度的分布
令fu(k)为对k个物品产生过行为的用户数,令fi(k)为被k个用户产生过行为的物品数。那么,fu(k)和fi(k)都满足长尾分布。
物品的流行度指对物品产生过行为的用户总数。
用户的活跃度为用户产生过行为的物品总数。
不管是物品的流行度还是用户的活跃度,都近似于长尾分布。
2.2.2 用户活跃度和物品流行度的关系
一般认为,新用户倾向于浏览热门的物品,因为他们对网站还不熟悉,只能点击首页的热门物品,而老用户会逐渐开始浏览冷门的物品。
用户越活跃,越倾向于浏览冷门的物品。
仅仅基于用户行为数据设计的推荐算法一般称为协同过滤算法。学术界对协同过滤算法进行了深入研究,提出了很多方法,比如基于邻域的方法(neighborhood-based)、隐语义模型(latent factor model)、基于图的随机游走算法(random walk on graph)等。在这些方法中,最著名的、在业界得到最广泛应用的算法是基于邻域的方法,而基于邻域的方法主要包含下面两种算法。
基于用户的协同过滤算法:这种算法给用户推荐和他兴趣相似的其他用户喜欢的物品。
基于物品的协同过滤算法:这种算法给用户推荐和他之前喜欢的物品相似的物品。
2.3 实验设计和算法评测
2.3.1 数据集
MovieLens数据集,https://grouplens.org/datasets/movielens/
2.3.2 实验设计
协同过滤算法的离线实验一般如下设计。首先,将用户行为数据集按照均匀分布随机分成M份(本章取M=8),挑选一份作为测试集,将剩下的M-1份作为训练集。然后在训练集上建立用户兴趣模型,并在测试集上对用户行为进行预测,统计出相应的评测指标。为了保证评测指标并不是过拟合的结果,需要进行M次实验,并且每次都使用不同的测试集。然后将M次实验测出的评测指标的平均值作为最终的评测指标。
2.3.3 评测指标
对用户u推荐N个物品(记为R(u)),令用户u在测试集上喜欢的物品集合为T(u),然后可以通过准确率/召回率评测推荐算法的精度:
召回率描述有多少比例的用户—物品评分记录包含在最终的推荐列表中,而准确率描述最终的推荐列表中有多少比例是发生过的用户—物品评分记录。
覆盖率反映了推荐算法发掘长尾的能力,覆盖率越高,说明推荐算法越能够将长尾中的物品推荐给用户。
用推荐列表中物品的平均流行度度量推荐结果的新颖度。如果推荐出的物品都很热门,说明推荐的新颖度较低,否则说明推荐结果比较新颖。
def Popularity(train, test, N):
item_popularity = dict()
for user, items in train.items():
for item in items.keys()
if item not in item_popularity:
item_popularity[item] = 0
item_popularity[item] += 1
ret = 0
n = 0
for user in train.keys():
rank = GetRecommendation(user, N)
for item, pui in rank:
ret += math.log(1 + item_popularity[item])
n += 1
ret /= n * 1.0
return ret
2.4 基于邻域的算法
基于邻域的算法分为两大类,一类是基于用户的协同过滤算法,另一类是基于物品的协同过滤算法。
2.4.1 基于用户的协同过滤算法
1. 基础算法
在一个在线个性化推荐系统中,当一个用户A需要个性化推荐时,可以先找到和他有相似兴趣的其他用户,然后把那些用户喜欢的、而用户A没有听说过的物品推荐给A。这种方法称为基于用户的协同过滤算法。
基于用户的协同过滤算法主要包括两个步骤。
(1) 找到和目标用户兴趣相似的用户集合。
(2) 找到这个集合中的用户喜欢的,且目标用户没有听说过的物品推荐给目标用户。
步骤(1)的关键就是计算两个用户的兴趣相似度。这里,协同过滤算法主要利用行为的相似度计算兴趣的相似度。
给定用户u和用户v,令N(u)表示用户u曾经有过正反馈的物品集合,令N(v)为用户v曾经有过正反馈的物品集合。那么,我们可以通过如下的Jaccard公式简单地计算u和v的兴趣相似度:
或者通过余弦相似度计算:
以余弦相似度为例,实现该相似度可以利用如下的伪码:
def UserSimilarity(train):
W = dict()
for u in train.keys():
for v in train.keys():
if u == v:
continue
W[u][v] = len(train[u] & train[v])
W[u][v] /= math.sqrt(len(train[u]) * len(train[v]) * 1.0)
return W
这种方法的时间复杂度是O(|U|*|U|),这在用户数很大时非常耗时。我们可以首先计算出的用户对(u,v),然后再对这种情况除以分母
。
为此,可以首先建立物品到用户的倒排表,对于每个物品都保存对该物品产生过行为的用户列表。从而,可以扫描倒排表中每个物品对应的用户列表,将用户列表中的两两用户对应的C[u][v]加1,最终就可以得到所有用户之间不为0的C[u][v]。
def UserSimilarity(train):
# build inverse table for item_users
item_users = dict()
for u, items in train.items():
for i in items.keys():
if i not in item_users:
item_users[i] = set()
item_users[i].add(u)
#calculate co-rated items between users
C = dict()
N = dict()
for i, users in item_users.items():
for u in users:
N[u] += 1
for v in users:
if u == v:
continue
C[u][v] += 1
#calculate finial similarity matrix W
W = dict()
for u, related_users in C.items():
for v, cuv in related_users.items():
W[u][v] = cuv / math.sqrt(N[u] * N[v])
return W
得到用户之间的兴趣相似度后,UserCF算法会给用户推荐和他兴趣最相似的K个用户喜欢的物品。如下的公式度量了UserCF算法中用户u对物品i的感兴趣程度:
def Recommend(user, train, W):
rank = dict()
interacted_items = train[user]
for v, wuv in sorted(W[u].items, key=itemgetter(1), reverse=True)[0:K]:
for i, rvi in train[v].items:
if i in interacted_items:
#we should filter items user interacted before
continue
rank[i] += wuv * rvi
return rank
参数K是UserCF的一个重要参数,它的调整对推荐算法的各种指标都会产生一定的影响。
(1)准确率和召回率, 推荐系统的精度指标(准确率和召回率)并不和参数K成线性关系。
(2)流行度,K越大则UserCF推荐结果就越热门。
(3)覆盖率,K越大则UserCF推荐结果的覆盖率越低。
2. 用户相似度计算的改进
两个用户对冷门物品采取过同样的行为更能说明他们兴趣的相似度。根据用户行为计算用户的兴趣相似度:
惩罚了用户u和用户v共同兴趣列表中热门物品对他们相似度的影响。称为User-IIF算法。
def UserSimilarity(train):
# build inverse table for item_users
item_users = dict()
for u, items in train.items():
for i in items.keys():
if i not in item_users:
item_users[i] = set()
item_users[i].add(u)
#calculate co-rated items between users
C = dict()
N = dict()
for i, users in item_users.items():
for u in users:
N[u] += 1
for v in users:
if u == v:
continue
C[u][v] += 1 / math.log(1 + len(users))
#calculate finial similarity matrix W
W = dict()
for u, related_users in C.items():
for v, cuv in related_users.items():
W[u][v] = cuv / math.sqrt(N[u] * N[v])
return W
UserCF-IIF在各项性能上略优于UserCF。这说明在计算用户兴趣相似度时考虑物品的流行度对提升推荐结果的质量确实有帮助。
3. 实际在线系统使用UserCF的例子
UserCF在目前的实际应用中使用并不多。其中最著名的使用者是Digg。
2.4.2 基于物品的协同过滤算法
1. 基础算法
基于用户的协同过滤算法的缺点:
(1)随着网站的用户数目越来越大,计算用户兴趣相似度矩阵将越来越困难,其运算时间复杂度和空间复杂度的增长和用户数的增长近似于平方关系。
(2)基于用户的协同过滤很难对推荐结果作出解释。
基于物品的协同过滤算法(简称ItemCF)给用户推荐那些和他们之前喜欢的物品相似的物品。ItemCF算法并不利用物品的内容属性计算物品之间的相似度,它主要通过分析用户的行为记录计算物品之间的相似度。该算法认为,物品A和物品B具有很大的相似度是因为喜欢物品A的用户大都也喜欢物品B。
基于物品的协同过滤算法可以利用用户的历史行为给推荐结果提供推荐解释。
基于物品的协同过滤算法主要分为两步。
(1) 计算物品之间的相似度。
(2) 根据物品的相似度和用户的历史行为给用户生成推荐列表。
为了避免推荐出热门的物品,可以用下面的公式计算物品的相似度:
在协同过滤中两个物品产生相似度是因为它们共同被很多用户喜欢,也就是说每个用户都可以通过他们的历史兴趣列表给物品“贡献”相似度。
和UserCF算法类似,用ItemCF算法计算物品相似度时也可以首先建立用户—物品倒排表(即对每个用户建立一个包含他喜欢的物品的列表),然后对于每个用户,将他物品列表中的物品两两在共现矩阵C中加1。
def ItemSimilarity(train):
#calculate co-rated users between items
C = dict()
N = dict()
for u, items in train.items():
for i in users:
N[i] += 1
for j in users:
if i == j:
continue
C[i][j] += 1
#calculate finial similarity matrix W
W = dict()
for i,related_items in C.items():
for j, cij in related_items.items():
W[u][v] = cij / math.sqrt(N[i] * N[j])
return W
在得到物品之间的相似度后,ItemCF通过如下公式计算用户u对一个物品j的兴趣:
和用户历史上感兴趣的物品越相似的物品,越有可能在用户的推荐列表中获得比较高的排名。
def Recommendation(train, user_id, W, K):
rank = dict()
ru = train[user_id]
for i,pi in ru.items():
for j, wj in sorted(W[i].items(), key=itemgetter(1), reverse=True)[0:K]:
if j in ru:
continue
rank[j] += pi * wj
return rank
ItemCF的一个优势就是可以提供推荐解释,即利用用户历史上喜欢的物品为现在的推荐结果进行解释。
ItemCF算法在不同K值下的性能:
(1)精度(准确率和召回率):ItemCF推荐结果的精度也是不和K成正相关或者负相关的,因此选择合适的K对获得最高精度是非常重要的。
(2)流行度:和UserCF不同,参数K对ItemCF推荐结果流行度的影响也不是完全正相关的。随着K的增加,结果流行度会逐渐提高,但当K增加到一定程度,流行度就不会再有明显变化。
(3)覆盖率:K增加会降低系统的覆盖率。
2. 用户活跃度对物品相似度的影响
活跃用户对物品相似度的贡献应该小于不活跃的用户,他提出应该增加IUF参数(Inverse User Frequence,用户活跃度对数的倒数)来修正物品相似度的计算公式:
算法记为ItemCF-IUF。
def ItemSimilarity(train):
#calculate co-rated users between items
C = dict()
N = dict()
for u, items in train.items():
for i in users:
N[i] += 1
for j in users:
if i == j:
continue
C[i][j] += 1 / math.log(1 + len(items) * 1.0)
#calculate finial similarity matrix W
W = dict()
for i,related_items in C.items():
for j, cij in related_items.items():
W[u][v] = cij / math.sqrt(N[i] * N[j])
return W
3. 物品相似度的归一化
在研究中发现如果将ItemCF的相似度矩阵按最大值归一化,可以提高推荐的准确率。
其实,归一化的好处不仅仅在于增加推荐的准确度,它还可以提高推荐的覆盖率和多样性。
一般来说,热门的类其类内物品相似度一般比较大。如果不进行归一化,就会推荐比较热门的类里面的物品,而这些物品也是比较热门的。因此,推荐的覆盖率就比较低。相反,如果进行相似度的归一化,则可以提高推荐系统的覆盖率。
2.4.3 UserCF和ItemCF的综合比较
UserCF的推荐更社会化,反映了用户所在的小型兴趣群体中物品的热门程度,而ItemCF的推荐更加个性化,反映了用户自己的兴趣传承。
UserCF适合用于新闻推荐,在图书、电子商务和电影网站,比如亚马逊、豆瓣、Netflix中,ItemCF则能极大地发挥优势。
从技术上考虑,UserCF需要维护一个用户相似度的矩阵,而ItemCF需要维护一个物品相似度矩阵。从存储的角度说,如果用户很多,那么维护用户兴趣相似度矩阵需要很大的空间,同理,如果物品很多,那么维护物品相似度矩阵代价较大。
哈利波特问题
很多书都和《哈利波特》相关,因为《哈利波特》太热门了。
哈利波特问题有几种解决方案。
(1)在分母上加大对热门物品的惩罚
其中
[0.5 ,1]。通过提高α,就可以惩罚热门的j。
通过这种方法可以在适当牺牲准确率和召回率的情况下显著提升结果的覆盖率和新颖性(降低流行度即提高了新颖性)。
(2)引入物品的内容数据
2.5 隐语义模型
2.5.1 基础算法
隐语义模型(LFM,latent factor model)的核心思想是通过隐含特征(latent factor)联系用户兴趣和物品。
对于某个用户,首先得到他的兴趣分类,然后从分类中挑选他可能喜欢的物品。
总结一下,这个基于兴趣分类的方法大概需要解决3个问题。
(1)如何给物品进行分类?
(2)如何确定用户对哪些类的物品感兴趣,以及感兴趣的程度?
(3)对于一个给定的类,选择哪些属于这个类的物品推荐给用户,以及如何确定这些物品在一个类中的权重?
对于第一个问题:如何给物品进行分类?
简单解决方案是找编辑给物品分类,编辑给出的分类仍然具有以下缺点:
a、编辑的意见不能代表各种用户的意见。
b、编辑很难控制分类的粒度。
c、编辑很难给一个物品多个分类。
d、编辑很难给出多维度的分类。
e、编辑很难决定一个物品在某一个分类中的权重。
研究人员提出:为什么我们不从数据出发,自动地找到那些类,然后进行个性化推荐?于是,隐含语义分析技术(latent variable analysis)出现了。隐含语义分析技术因为采取基于用户行为统计的自动聚类,较好地解决了上面提出的5个问题。
a、编辑的意见不能代表各种用户的意见,但隐含语义分析技术的分类来自对用户行为的统计,代表了用户对物品分类的看法。隐含语义分析技术和ItemCF在物品分类方面的思想类似,如果两个物品被很多用户同时喜欢,那么这两个物品就很有可能属于同一个类。
b、编辑很难控制分类的粒度,但隐含语义分析技术允许我们指定最终有多少个分类,这个数字越大,分类的粒度就会越细,反正分类粒度就越粗。
c、编辑很难给一个物品多个分类,但隐含语义分析技术会计算出物品属于每个类的权重,因此每个物品都不是硬性地被分到某一个类中。
d、编辑很难给出多维度的分类,但隐含语义分析技术给出的每个分类都不是同一个维度的,它是基于用户的共同兴趣计算出来的,如果用户的共同兴趣是某一个维度,那么LFM给出的类也是相同的维度。
e、编辑很难决定一个物品在某一个分类中的权重,但隐含语义分析技术可以通过统计用户行为决定物品在每个类中的权重,如果喜欢某个类的用户都会喜欢某个物品,那么这个物品在这个类中的权重就可能比较高。
相关方法:pLSA、LDA、隐含类别模型(latent class model)、隐含主题模型(latent topic model)、矩阵分解(matrix factorization
LFM通过如下公式计算用户u对物品i的兴趣:
这个公式中pu,k 和qi,k 是模型的参数,其中pu,k 度量了用户u的兴趣和第k个隐类的关系,而qi,k 度量了第k个隐类和物品i之间的关系。
在隐性反馈数据集上应用LFM解决TopN推荐的第一个关键问题就是如何给每个用户生成负样本。
对负样本采样时应该遵循以下原则。
(1)对每个用户,要保证正负样本的平衡(数目相似)。
(2)对每个用户采样负样本时,要选取那些很热门,而用户却没有行为的物品。
很热门而用户却没有行为更加代表用户对这个物品不感兴趣。因为对于冷门的物品,用户可能是压根没在网站中发现这个物品,所以谈不上是否感兴趣。
def RandomSelectNegativeSample(self, items):
ret = dict()
for i in items.keys():
ret[i] = 1
n = 0
for i in range(0, len(items) * 3):
item = items_pool[random.randint(0, len(items_pool) - 1)]
if item in ret:
continue
ret[item] = 0
n + = 1
if n > len(items):
break
return ret
采样后,需要优化如下的损失函数来找到最合适的参数p和q:
要最小化上面的损失函数,可以利用一种称为随机梯度下降法①的算法。该算法是最优化理论里最基础的优化算法,它首先通过求参数的偏导数找到最速下降方向,然后通过迭代法不断地优化参数。
其中,
根据随机梯度下降法,需要将参数沿着最速下降方向向前推进,因此可以得到如下递推公式:
其中,α是学习速率(learning rate),它的选取需要通过反复实验获得。
def LatentFactorModel(user_items, F, N, alpha, lambda):
[P, Q] = InitModel(user_items, F)
for step in range(0,N):
for user, items in user_items.items():
samples = RandSelectNegativeSamples(items)
for item, rui in samples.items():
eui = rui - Predict(user, item)
for f in range(0, F):
P[user][f] += alpha * (eui * Q[item][f] - lambda * P[user][f])
Q[item][f] += alpha * (eui * P[user][f] - lambda * Q[item][f])
alpha *= 0.9
def Recommend(user, P, Q):
rank = dict()
for f, puf in P[user].items():
for i, qfi in Q[f].items():
if i not in rank:
rank[i] += puf * qfi
return rank
在LFM中,重要的参数有4个:
隐特征的个数F;
学习速率alpha;
正则化参数lambda;
负样本/正样本比例 ratio。
通过实验发现,ratio参数对LFM的性能影响最大。随着负样本数目的增加,LFM的准确率和召回率有明显提高。不过当ratio>10以后,准确率和召回率基本就比较稳定了。同时,随着负样本数目的增加,覆盖率不断降低,而推荐结果的流行度不断增加,说明ratio参数控制了推荐算法发掘长尾的能力。
2.5.2 基于LFM的实际系统的例子
雅虎的研究人员利用前文提到的LFM预测用户是否会单击链接:
LFM模型在实际使用中有一个困难,那就是它很难实现实时的推荐。经典的LFM模型每次训练时都需要扫描所有的用户行为记录,这样才能计算出用户隐类向量(pu)和物品隐类向量(qi)。而且LFM的训练需要在用户行为记录上反复迭代才能获得比较好的性能。因此,LFM的每次训练都很耗时,一般在实际应用中只能每天训练一次,并且计算出所有用户的推荐结果。从而LFM模型不能因为用户行为的变化实时地调整推荐结果来满足用户最近的行为。
为了解决传统LFM不能实时化,而产品需要实时性的矛盾,雅虎的研究人员提出了一个解决方案。他们的解决方案分为两个部分。首先,他们利用新闻链接的内容属性(关键词、类别等)得到链接i的内容特征向量yi。其次,他们会实时地收集用户对链接的行为,并且用这些数据得到链接i的隐特征向量qi。然后,他们会利用如下公式预测用户u是否会单击链接i:
其中,yi是根据物品的内容属性直接生成的,xuk是用户u对内容特征k的兴趣程度,用户向量xu可以根据历史行为记录获得,而且每天只需要计算一次。而pu、qi是根据实时拿到的用户最近几小时的行为训练LFM获得的。
2.5.3 LFM和基于邻域的方法的比较
LFM | 基于邻域的方法 | |
理论基础 | 具有比较好的理论基础,它是一种学习方法,通过优化一个设定的指标建立最优的模型。 | 更多的是一种基于统计的方法,并没有学习过程 |
离线计算的空间复杂度 | 如果是F个隐类,那么它需要的存储空间是O(F*(M+N)),这在M和N很大时可以很好地节省离线计算的内存。 | 基于邻域的方法需要维护一张离线的相关表。在离线计算相关表的过程中,如果用户/物品数很多,将会占据很大的内存。假设有M个用户和N个物品,那么假设是用户相关表,则需要O(M*M)的空间,而对于物品相关表,则需要O(N*N)的空间。 |
离线计算的时间复杂度 | 如果用F个隐类,迭代S次,那么它的计算复杂度是O(K * F * S) | 假设有M个用户、N个物品、K条用户对物品的行为记录。那么,UserCF计算用户相关表的时间复杂度是O(N * (K/N)^2),而ItemCF计算物品相关表的时间复杂度是O(M*(K/M)^2) |
在线实时推荐 | 可以在线进行实时的预测 | 不能进行在线实时推荐 |
推荐解释 | 支持很好的推荐解释 | 无法提供解释 |
2.6 基于图的模型
2.6.1 用户行为数据的二分图表示
用户行为数据是由一系列二元组组成的,其中每个二元组(u, i)表示用户u对物品i产生过行为。这种数据集很容易用一个二分图①表示。
2.6.2 基于图的推荐算法
如果将个性化推荐算法放到二分图模型上,那么给用户u推荐物品的任务就可以转化为度量用户顶点vu和与vu没有边直接相连的物品节点在图上的相关性,相关性越高的物品在推荐列表中的权重就越高。
图中顶点的相关性主要取决于下面3个因素:
(1)两个顶点之间的路径数;
(2)两个顶点之间路径的长度;
(3)两个顶点之间的路径经过的顶点。
相关性高的一对顶点一般具有如下特征:
(1)两个顶点之间有很多路径相连;
(2)连接两个顶点之间的路径长度都比较短;
(3)连接两个顶点之间的路径不会经过出度比较大的顶点。
基于随机游走的PersonalRank算法
假设要给用户u进行个性化推荐,可以从用户u对应的节点vu开始在用户物品二分图上进行随机游走。游走到任何一个节点时,首先按照概率α决定是继续游走,还是停止这次游走并从vu节点开始重新游走。如果决定继续游走,那么就从当前节点指向的节点中按照均匀分布随机选择一个节点作为游走下次经过的节点。这样,经过很多次随机游走后,每个物品节点被访问到的概率会收敛到一个数。最终的推荐列表中物品的权重就是物品节点的访问概率。
def PersonalRank(G, alpha, root):
rank = dict()
rank = {x:0 for x in G.keys()}
rank[root] = 1
for k in range(20):
tmp = {x:0 for x in G.keys()}
for i, ri in G.items():
for j, wij in ri.items():
if j not in tmp:
tmp[j] = 0
tmp[j] += 0.6 * rank[i] / (1.0 * len(ri))
if j == root:
tmp[j] += 1 - alpha
rank = tmp
return rank
虽然PersonalRank算法可以通过随机游走进行比较好的理论解释,但该算法在时间复杂度上有明显的缺点。因为在为每个用户进行推荐时,都需要在整个用户物品二分图上进行迭代,直到整个图上的每个顶点的PR值收敛。这一过程的时间复杂度非常高,不仅无法在线提供实时推荐,甚至离线生成推荐结果也很耗时。
解决PersonalRank时间复杂度很高的方案:(1)减少迭代次数,在收敛之前就停止。这样会影响最终的精度,但一般来说影响不会特别大。(2)从矩阵论出发,重新设计算法。