第二章 利用用户行为数据
基于用户行为分析的推荐算法是个性化推荐系统的重要算法,学术界一般将这种类型的算法称为协同过滤算法。
2.1 用户行为数据简介
用户行为数据在网站上最简单的存在形式就是日志。网站在运行过程中都产生大量原始日志(raw log),并将其存储在文件系统中。
会话日志:其中每个 会话表示一次用户行为和对应的服务。
点击日志:其中记录了查询和返回结果。
会话日志通常存储在分布式数据仓库中,如支持离线分析的 Hadoop Hive和支持在线分析的Google Dremel。这些日志记录了用户的各种行为,如在电子商务网站中这些行为主要包括网页浏览、购买、点击、评分和评论等。
用户行为可以分为显性反馈行为和隐性反馈行为:
显性反馈行为:包括用户明确表示对物品喜好的行为。
隐性反馈行为:指的是那些不能明确反应用户喜好的行为。最具代表性的隐性反馈行为就是页面浏览行为。
下表为一些例子:
下表为用户行为的同意表示:
2.2 用户行为分析
2.2.1 用户活跃度和物品流行度的分布
互联网上的很多数据分布都满足一种称为Power Law的分布,这个分布在互联网领域也称长尾分布。
f
(
x
)
=
α
x
k
f(x) =\alpha{x^k}
f(x)=αxk
这个现象表明,在英文中大部分词的词频其实很低,只有很少的词被经常使用。
2.2.2 用户活跃度和物品流行度的关系
一般认为,新用户倾向于浏览热门的物品,因为他们对网站还不熟悉,只能点击首页的热门物品,而老用户会逐渐开始浏览冷门的物品。仅仅基于用户行为数据设计的推荐算法一般称为协同过滤算法。
学术界对协同过滤算法进行了深入研究,提出了很多方法:
比如基于邻域的方法(neighborhood-based)、隐语义模型(latent factor model)、基于图的随机游走算法(random walk on graph)等。在这些方法中,最著名的、在业界得到最广泛应用的算法是基于邻域的方法,而基于邻域的方法主要包含下面两种算法:
基于用户的协同过滤算法 这种算法给用户推荐和他兴趣相似的其他用户喜欢的物品。
基于物品的协同过滤算法 这种算法给用户推荐和他之前喜欢的物品相似的物品。
2.3 实验设计和算法评测
评测推荐系统有3种方法——离线实验、用户调查和在线实验。
协同过滤算法的离线实验一般如下设计:
将用户行为数据集按照均匀分布随机分成M份(本章取M=8),挑选一份作为测试集,将剩下的M-1份作为训练集。
然后在训练集上建立用户兴趣模型。并且在测试集上对用户行为预测。
2.3.3 评测指标
召回率:
准确率:
**覆盖率:**覆盖率反应了推荐算法发掘长尾的能力,覆盖率越高,说明推荐算法越能够将长尾中的物品推荐给用户。
其
中
∣
I
∣
表
示
所
有
物
品
信
息
的
总
数
。
而
分
子
是
所
有
用
户
被
推
荐
的
物
品
的
并
集
。
也
就
是
如
果
所
有
的
物
品
都
被
推
荐
给
至
少
一
个
用
户
,
那
么
覆
盖
率
就
是
100
%
。
其中|I|表示所有物品信息的总数。而分子是所有用户被推荐的物品的并集。也就是如果所有的物品\\都被推荐给至少一个用户,那么覆盖率就是100\%。
其中∣I∣表示所有物品信息的总数。而分子是所有用户被推荐的物品的并集。也就是如果所有的物品都被推荐给至少一个用户,那么覆盖率就是100%。
新颖度:
这里可以用推荐列表中物品的平均流行度度量推荐结果的新颖度。如果推荐出的物品都很热门,说明推荐的新颖度较低,否则说明推荐结果比较新颖。【注意:新颖度就是那些冷门的不容易被关注到的物品推荐给用户会让用户觉得新颖的物品】
在计算平均流行度的时候,对每个物品的流行度取对数,这是因为物品的流行度分布满足长尾分布,在取对数之后,流行度的平均值更加稳定。
2.4 基于领域的算法
2.4.1 基于用户的协同过滤算法
基于用户的协同过滤算法主要包括两个步骤。
(1) 找到和目标用户兴趣相似的用户集合。
(2) 找到这个集合中的用户喜欢的,且目标用户没有听说过的物品推荐给目标用户。
步骤一主要是计算两个用户之间的兴趣相似度。协同过滤主要利用行为的相似度计算兴趣的相似度。
其
中
N
(
u
)
表
示
用
户
u
曾
经
有
过
的
正
反
馈
物
品
列
表
,
N
(
v
)
表
示
用
户
v
曾
经
有
过
的
峥
反
馈
物
品
列
表
。
w
u
v
表
示
兴
趣
相
似
度
。
其中N(u)表示用户u曾经有过的正反馈物品列表,N(v)表示用户v曾经有过的峥反馈物品列表。\\w_{uv}表示兴趣相似度。
其中N(u)表示用户u曾经有过的正反馈物品列表,N(v)表示用户v曾经有过的峥反馈物品列表。wuv表示兴趣相似度。
也可以用余弦相似度:
一个例子:
其中的图表示A用户对a,b,d物品产生过行为,etc.
其
中
{
a
,
b
,
d
}
∩
{
a
,
c
}
=
1
很
好
理
解
其中\{a,b,d\}∩\{a,c\}=1\ \ 很好理解
其中{a,b,d}∩{a,c}=1 很好理解
而 { a , b , d } ∣ ∣ { a , c } 是 表 示 将 { a , b , d } 中 的 元 素 与 { a , c } 进 行 配 对 , 共 有 6 种 配 对 情 况 。 而\{a,b,d\}||\{a,c\} 是表示将\{a,b,d\}中的元素与\{a,c\}进行配对,共有6种配对情况。 而{a,b,d}∣∣{a,c}是表示将{a,b,d}中的元素与{a,c}进行配对,共有6种配对情况。
这种方法的时间复杂度是O(|U|*|U|),这在用户数很大时非常耗时。事实上,很多用户相互之间并没有对同样的物品产生过行为,即很多时候
∣
N
(
u
)
∩
N
(
v
)
∣
=
0
也
就
是
很
多
情
况
下
不
同
的
用
户
产
生
过
行
为
的
商
品
之
间
没
有
交
集
|N(u)∩N(v)|=0\\也就是很多情况下不同的用户产生过行为的商品之间没有交集
∣N(u)∩N(v)∣=0也就是很多情况下不同的用户产生过行为的商品之间没有交集
优化后的结构:
其 中 , 左 上 角 的 图 表 示 每 个 用 户 访 问 过 的 商 品 , 右 上 角 的 图 表 示 每 个 商 品 被 哪 些 用 户 访 问 过 。 下 面 的 图 表 示 这 个 用 户 之 间 有 多 少 个 都 访 问 过 的 物 品 , 这 一 定 是 个 是 对 成 矩 阵 。 其中,左上角的图表示每个用户访问过的商品,右上角的图表示每个商品被哪些用户访问过。下面的\\图表示这个用户之间有多少个都访问过的物品,这一定是个是对成矩阵。 其中,左上角的图表示每个用户访问过的商品,右上角的图表示每个商品被哪些用户访问过。下面的图表示这个用户之间有多少个都访问过的物品,这一定是个是对成矩阵。
得到用户的兴趣相似度表之后,UserCF**(基于用户的协同过滤)**就会给用户推荐和它他兴趣相似的K个用户喜欢的物品。
用 此 公 式 衡 量 用 户 u 对 物 品 i 的 感 兴 趣 程 度 。 其 中 , S ( u , K ) 包 含 和 用 户 u 兴 趣 最 接 近 的 K 个 用 户 , N ( i ) 是 对 物 品 i 有 过 行 为 的 用 户 集 合 , w u v 是 用 户 u 和 用 户 v 的 兴 趣 相 似 度 , r v i 代 表 用 户 v 对 物 品 i 的 兴 趣 , 因 为 使 用 的 是 单 一 行 为 的 隐 反 馈 数 据 , 所 以 所 有 的 r v i = 1 。 用此公式衡量用户u对物品i的感兴趣程度。其中,S(u, K)包含和用户u兴趣最接近的K个用户,\\N(i)是对物品i有过行为的用户集合,w_{uv}是用户u和用户v的兴趣相似度,r_{vi}代表用户v对\\物品i的兴趣,因为使用的是单一行为的隐反馈数 据,所以所有的r_{vi}=1。 用此公式衡量用户u对物品i的感兴趣程度。其中,S(u,K)包含和用户u兴趣最接近的K个用户,N(i)是对物品i有过行为的用户集合,wuv是用户u和用户v的兴趣相似度,rvi代表用户v对物品i的兴趣,因为使用的是单一行为的隐反馈数据,所以所有的rvi=1。
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
UserCF只有一个重要的参数K,即为每个用户选出K个和他兴趣最相似的用户,然后推荐那K个用户感兴趣的物品。
为了作对比,将两种极端情况下的推荐效果列举出来:
**分别是:**Random算法每次都随机挑选10个用户没有产生过行为的物品推荐给当前用户,也就是为当前用户推荐和他相似的用户从来没接触过的商品。MostPopular算法则按照物品的流行度给用户推荐他没有产生过行为的物品中最热门的10个物品,也就是完全按照流行度推荐。很显然第一种情形的准确率和召回率都很低,而第二种情况的覆盖率特别低,仅仅覆盖了热门前10的物品,虽然用户还是有一定概率感兴趣。
还可以发现参数K是UserCF算法的一个重要的参数,它的调整对推荐算法的各种指标都会产生一定的影响。
用户相似度计算的改进:
对于《新华词典》这样热门的商品,购买并不能说明用户对他感兴趣。而对于冷门书籍,同时购买的用户便能够说明用户对其感兴趣。于是便有了下面的公式:
其
中
1
l
o
g
1
+
∣
N
(
i
)
∣
惩
罚
了
用
户
u
与
用
户
v
中
共
同
兴
趣
列
表
中
热
门
物
品
对
他
们
相
似
度
的
影
响
。
N
(
i
)
是
对
物
品
i
有
过
行
为
的
用
户
集
合
,
此
值
越
大
,
那
么
此
公
式
的
结
果
就
会
越
小
。
其中\frac{1}{log1+|N(i)|}惩罚了用户u与用户v中共同兴趣列表中热门物品对他们\\相似度的影响。N(i)是对物品i有过行为的用户集合,此值越大,那么此公式的结果就会越小。
其中log1+∣N(i)∣1惩罚了用户u与用户v中共同兴趣列表中热门物品对他们相似度的影响。N(i)是对物品i有过行为的用户集合,此值越大,那么此公式的结果就会越小。
本书将此UserCF算法称呼为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
2.4.2 基于物品的协同过滤算法
此算法为业界应用的最多的算法。简称ItemCF算法,也就是给用户推荐那些和他们之前喜欢的物品相似的物品。不过,ItemCF算法并不利用物品的内容属性计算物品之间的相似度,它主要通过分析用户的行为记录计算物品之间的相似度。
- 基础算法
基于物品的协同过滤算法主要分为两步:
(1) 计算物品之间的相似度。
(2) 根据物品的相似度和用户的历史行为给用户生成推荐列表。
其
中
N
(
i
)
表
示
喜
欢
物
品
i
的
用
户
数
。
N
(
i
)
∩
N
(
j
)
表
示
同
时
喜
欢
物
品
i
和
物
品
j
的
用
户
数
。
那
么
此
公
式
就
表
示
喜
欢
物
品
i
的
用
户
中
有
多
少
用
户
喜
欢
物
品
j
。
其中N(i)表示喜欢物品i的用户数。N(i)∩N(j)表示同时喜欢物品i和物品j的用户数。\\那么此公式就表示喜欢物品i的用户中有多少用户喜欢物品j。
其中N(i)表示喜欢物品i的用户数。N(i)∩N(j)表示同时喜欢物品i和物品j的用户数。那么此公式就表示喜欢物品i的用户中有多少用户喜欢物品j。
其中红色面积越大,便说明物品i和物品j的相似度越大。
但是也存在问题:比如热门商品,很多用户都喜欢。这对于致力于挖掘长尾信息的推荐系统来说显然不是一个好的特性。
可以通过上面的公式对热门商品进行惩罚。减轻了热门商品和很多物品的相似的可能性。
其
中
N
(
i
)
∣
∣
N
(
j
)
的
含
义
如
下
图
所
示
,
结
果
为
n
u
m
b
e
r
(
N
(
i
)
)
∗
N
(
j
)
其中N(i)||N(j)的含义如下图所示,结果为number(N(i))*N(j)
其中N(i)∣∣N(j)的含义如下图所示,结果为number(N(i))∗N(j)
和UserCF一样,Item算法也首先建立用户-物品倒排表(即对每个用户建立一个包含他喜欢的物品的列表)
此图中左边的图是输出的用户行为记录,每一行代表一个用户感兴趣的物品集合。然后对于每个物品集合,两两配对,并且将配对结果所对应的在中间的图中的相应的位置加1。于是得到中间的矩阵,然后将所有的矩阵相加便得到了上图最右边的矩阵,该矩阵中对应的C[i] [j]值表示同时喜欢物品i和物品j的用户数,此矩阵称为共现矩阵。最后,将C矩阵归一化可以得到物品之间的余弦相似度矩阵W。
代码如下:
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的兴趣:
其
中
N
(
u
)
表
示
用
户
感
兴
趣
的
物
品
集
合
,
S
(
j
,
K
)
表
示
和
物
品
j
最
相
似
的
K
个
物
品
集
合
。
所
以
i
∈
N
(
u
)
∩
S
(
j
,
K
)
的
整
体
意
思
就
是
用
户
感
兴
趣
中
的
商
品
中
在
和
商
品
j
最
相
似
的
K
个
商
品
中
占
据
了
几
个
。
w
j
i
是
物
品
j
和
i
的
相
似
度
,
r
u
i
表
示
用
户
u
对
物
品
i
的
兴
趣
度
。
(
物
品
i
是
我
们
已
经
知
道
的
商
品
,
知
道
用
户
对
其
的
兴
趣
度
,
以
及
和
物
品
j
的
相
似
度
)
其中N(u)表示用户感兴趣的物品集合,S(j,K)表示和物品j最相似的K个物品集合。所以\\ i∈N(u)∩S(j,K)的整体意思就是用户感兴趣中的商品中在和商品j最相似的K个商品\\中 占据了几个。w_{ji}是物品j和i的相似度,r_{ui}表示用户u对物品i的兴趣度。(物品i是我们已经\\知道的商品,知道用户对其的兴趣度,以及和物品j的相似度)
其中N(u)表示用户感兴趣的物品集合,S(j,K)表示和物品j最相似的K个物品集合。所以i∈N(u)∩S(j,K)的整体意思就是用户感兴趣中的商品中在和商品j最相似的K个商品中占据了几个。wji是物品j和i的相似度,rui表示用户u对物品i的兴趣度。(物品i是我们已经知道的商品,知道用户对其的兴趣度,以及和物品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
one example
ItemCF优势:可以提供推荐解释。利用用户历史上喜欢的物品为现在的推荐结果进行解释
代码如下:
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].weight += pi * wj
rank[j].reason[i] = pi * wj
return rank
-
用户活跃度对物品相似度的影响
活跃用户对物品相似度的贡献应该小于不活跃的用户,John S. Breese在论文①中提出了一个称为IUF(Inverse User Frequence)。**可以理解为不活跃的用户一旦来买书那么就是针对其兴趣有目的地买书的。**而一次性买几十万本书这种(进货商)完全没有参考性。
①Empirical Analysis of Predictive Algorithms for CollaborativeFiltering”(Morgan Kaufmann Publishers ,1998)。
于是提出了修正的物品相似度的计算公式:
也就是对活跃用户进行惩罚,比如:一次性买了当当网80%书的人(过于活跃度人)。很显然,不应当将其纳入相似度计算贡献的人员中。
上面的公式怎么理解呢?
一
旦
太
活
跃
,
那
么
此
用
户
购
买
过
的
N
(
u
)
就
会
非
常
大
,
那
么
进
而
1
l
o
g
1
+
∣
N
(
u
)
∣
就
会
非
常
小
,
因
此
w
i
j
就
会
非
常
小
,
近
似
于
0
。
一旦太活跃,那么此用户购买过的N(u)就会非常大,那么进而\frac{1}{log1+|N(u)|}就会非常小,\\因此w_{ij}就会非常小,近似于0。
一旦太活跃,那么此用户购买过的N(u)就会非常大,那么进而log1+∣N(u)∣1就会非常小,因此wij就会非常小,近似于0。
代码如下:
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
此更正后的算法记作:ItemCF-IUF
-
物品相似度的归一化。
为啥归一化?
因为Karypis发现可以提高推荐的准确率。
在物品相似度矩阵w上执行归一化。卡哇伊用下面的方式归一化得到新的相似度矩阵。
归一化的好处不仅仅在于增加推荐的准确度,它还可以提高推荐的覆盖率和多样性。
一个例子:
如果A类物品的相似度为0.5,B类物品的相似度为0.6。A类和B类的物品之间的相似度为0.2,如果一个用户喜欢了5个A类物品和5个B类物品,用ItemCF给他进行推荐,那么推荐系统就更会推荐B类物品。但是归一化之后,A, B类物品的相似度均为1,最后推荐的物品数A,B更倾向于各占据一半。相似度的归一化可以提高推荐的多样性。
归一化后和归一化之前的对比:
2.4.3 UserCF和ItemCF的综合比较
UserCF的推荐结果着重于反映和用户兴趣相似的小群体的热点,而ItemCF的推荐结果着重于维系用户的历史兴趣。换句话说,UserCF的推荐更社会化,反映了用户所在的小型兴趣群体中物品的热门程度,而ItemCF的推荐更加个性化,反映了用户自己的兴趣传承。
哈利波特问题
研究发现,《哈利波特》太热门了,似乎购买任何书的人都会购买《哈利波特》。
首先看看计算物品相似度的经典公式:
很显然,如果j非常的热门,那么他将会覆盖几乎所有的用户,那么分子的结果就会接近|N(i)|。尽管分母已经从N(i)变成了N(i)∩N(j),在一定程序上减轻了流行度的影响。但是实际应用中,热门的j仍然会获得比较大的相似度。
那么怎么解决这个问题呢?
第一种方法:
加大对热门商品的惩罚。
其
中
α
∈
[
0.5
,
1
]
,
通
过
提
高
α
,
就
可
以
惩
罚
热
门
的
j
其中\alpha∈[0.5, 1],通过提高\alpha,就可以惩罚热门的j
其中α∈[0.5,1],通过提高α,就可以惩罚热门的j
两个不同领域的最热门物品之间往往具有比较高的相似度,这个时候,仅仅靠用户行为数据是不能够解决问题的。这时候就要引入物品的内容数据解决这个问题了。比如对不同领域的数据降低权重等,这就不是协同过滤了。
2.5 隐语义模型(latent factor model)
本节主要讨论隐语义模型在Top-N中的应用。并用实际的数据进行评测。
2.5.1 基础算法
核心思想是通过隐含特征(latent factor)联系用户兴趣和物品。
基于兴趣分类的方法需要解决的3个问题:
① 如何对物品进行分类
② 如何确定用户对哪类物品感兴趣,以及感兴趣的程度?
③ 确定好类别之后,在此类中选取哪些物品推荐给用户,以及如何确定这些物品在一个类中的权重。
隐含语义分析技术因为采取基于用户行为统计的自动聚类,较好地解决了下面提出的5个问题。
① 物品属于哪一类?不同角度分类不同
② 物品属于哪一类?划分细致程度分类不同,如:计算机技术范围>>机器学习范畴
③ 属于一类??多类??
④ 为什么这个读者选择这本书?因为作者?研究领域?还是出版社?
⑤ 一个物品给予他多大的权重合适?
隐语义模型(LFM)主要通过如下的公式计算用户u对物品i的兴趣:
这
个
公
式
中
p
u
,
k
和
q
i
,
k
是
模
型
的
参
数
,
其
中
p
u
,
k
度
量
了
用
户
u
的
兴
趣
和
第
k
个
隐
类
的
关
系
,
而
q
i
,
k
度
量
了
第
k
个
隐
类
和
物
品
i
之
间
的
关
系
。
那
么
,
下
面
的
问
题
就
是
如
何
计
算
这
两
个
参
数
。
这个公式中p_{u,k}和 q_{i,k} 是模型的参数,其中p_{u,k}度量了用户u的兴趣和第k个隐类的关系,\\而 q_{i,k}度量了第k个隐类和物品i之间的关系。那么,下面的问题就是如何计算这两个参数。
这个公式中pu,k和qi,k是模型的参数,其中pu,k度量了用户u的兴趣和第k个隐类的关系,而qi,k度量了第k个隐类和物品i之间的关系。那么,下面的问题就是如何计算这两个参数。
推荐系统的用户行为分为显性反馈和隐性反馈。
显性反馈:评分数据
这里主要讨论隐性数据集,这种数据集的特点就是只有正样本。
那么在隐性反馈数据集上应用LFM解决TopN的第一个关键问题题就是如何给每个用户生成负样本。
① 将用户没有过行为的物品视作负样本
② 从没有过行为的样本中均匀采样一些物品作为负样本
③ 在②的基础上,保证每个用户的正负样本数相当
④ 在没有过行为的物品上采样一些物品作为负样本,但是采样的时候偏重采样不热门的物品。
下面的代码是进行负样本采样
def RandomSelectNegativeSample(self, items): # items是一个dict,维护了用户有过行为的物品的集合
ret = dict() # 创建一个字典
for i in items.keys(): # 如果用户对一个物品有过行为,那么就把此物品对应的ret值置为1
ret[i] = 1
n = 0
for i in range(0, len(items) * 3):
item = items_pool[random.randint(0, len(items_pool) - 1)] # items_pool是一个候选物品集的列表,在这个列表中物品出现的次数和物品i的流行度成正比,这句代码的目的是随机中此候选物品集中挑选出一个物品,那么挑选的过程中,由于热门商品出现次数多,因此热门商品挑选上的几率就大。
if item in ret: # 如果此物品,用户有过行为,那么就跳出此轮循环
continue
ret[item] = 0 # 否则就将此没有过行为的物品对应的ret值置为0
n + = 1 # 用户没有产生过行为的物品数+1
if n > len(items): # 表示最多统计len(items)个没有过行为的物品
break
return ret # 最后返回一个ret,ret中值为1的就是用户有过行为的正样本,ret值为0的就是哪些虽然热门,但是用户没有过行为的负样本。
现在正负样本都有了,就要来求我们提及的 q i , k q_{i,k} qi,k以及 p u , k p_{u,k} pu,k。
通过优化如下损失函数来求:
其中 λ ∣ ∣ p u ∣ ∣ 2 + λ ∣ ∣ q i ∣ ∣ 2 \lambda||p_{u}||^2+\lambda||q_{i}||^2 λ∣∣pu∣∣2+λ∣∣qi∣∣2是防止过拟合的正则化项。可以采用随机梯度下降算法求参数的偏安倒数找到最快下降方向,然后通过迭代法不断优化参数。
其中 r u i r_{ui} rui是真实值, ∑ k = 1 K p u , k q i , k \sum_{k=1}^{K}p_{u,k}q_{i,k} ∑k=1Kpu,kqi,k是预测值。
求导结果为:
然后采用递推公式更新参数:
其中 α \alpha α为学习率。
对应代码如下:
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) # 用负样本真实的rui 减去预测的值
for f in range(0, F):
P[user][f] += alpha * (eui * Q[item][f] - lambda * P[user][f]) # 用梯度计算puk
Q[item][f] += alpha * (eui * P[user][f] - lambda * Q[item][f]) # 用梯度计算quk
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在TopN推荐中的性能,有4个重要的参数:
隐特征的个数F;
学习速率alpha;
正则化参数lambda;
负样本/正样本比例 ratio。通过实验发现,此参数的影响最大
可以看到随着ratio的增加,推荐结果的流行度不断增加,ratio参数控制了推荐算法发掘长尾的能力。和之前的ItemCF和UserCF对比,可以发现LFM在所有指标上都优于UserCF和ItemCF。但是当数据集非常稀疏时,LFM的性能会明显下降。
2.5.2 基于LFM的实际系统的例子
下面讨论雅虎利用LFM进行个性化设计的方案:
优化目标:CTR(点击率)
利用LFM来预测用户是否会单击一个链接。
**训练集:**用户历史上对首页上链接的行为记录。
**正样本:**用户u单击过链接,即 r u i r_{ui} rui。
**负样本:**链接i展示给用户u,用户u从来没有单击过链接。即 r u i = − 1 r_{ui}=-1 rui=−1。
利用前面提及的LFM预测用户是否单击链接:
r
^
u
i
=
p
u
T
˙
q
i
\hat r_{ui}=p_{u}^{T}\dot\ q_{i}
r^ui=puT ˙qi
将其与前面定义的用户u对一个物品i的偏好度(兴趣度)计算公式:
这个公式中
p
u
,
k
p_{u,k}
pu,k和 $q_{i,k}
是
模
型
的
参
数
,
其
中
是模型的参数,其中
是模型的参数,其中p_{u,k}$度量了用户u的兴趣和第k个隐类的关系,而
q
i
,
k
q_{i,k}
qi,k度量了第k个隐类和物品i之间的关系。那么,下面的问题就是如何计算这两个参数。
**缺点:**难以实现实时的推荐。训练耗时(因为要遍历用户的所有行为记录)
经典LFM模型每次训练需要扫描所有用户行为记录。这样才能计算出用户隐类向量 p u p_{u} pu,以及物品隐类向量 q i q_{i} qi。注:用户隐类向量可以理解为用户之间的隐藏关系,物品隐类向量可以理解为物品之间的隐藏关系。
实时性在雅虎的首页个性化推荐系统中非常重要。为了解决传统LFM不能实时化,而产品需要实时性的矛盾,雅虎的研究人员提出了一个解决方案。
他们会利用如下的公式预测用户u是否会单击链接:
r
u
i
=
x
u
T
⋅
y
i
+
p
u
T
⋅
q
i
r_{ui}=x_{u}^T·y_{i}+p_{u}^T·q_{i}
rui=xuT⋅yi+puT⋅qi
y i y_{i} yi | 根据物品的内容属性直接生成 |
---|---|
x u k x_{uk} xuk | 是用户u对于内容特征k的兴趣程度 |
p u , q i p_{u},q_{i} pu,qi | 根据实时拿到的用户最近几小时的行为训练LFM得到的 |
对于新加入的物品,可以通过 x u T ⋅ y i x_{u}^T·y_{i} xuT⋅yi估计用户u对物品 i i i的兴趣。经过几小时之后,就可以通过 p u T ⋅ q i p_{u}^T·q_{i} puT⋅qi得到更加准确的预测值了。
2.5.3 LFM和基于领域的方法比较
离线计算的空间复杂度
基于领域的方法,需要维护一张离弦的相关表,如果用户/物品数很多,将会占据很大的内存。
假设有M个用户,和N个物品。那么假设使用户相关表,则需要O(M ∗ * ∗M)的空间,而对于物品相关表,则需要O(N ∗ * ∗N)的空间。而LFM在建模过程中,如果是F个隐类,那么它需要的存储空间是O(F(M+N))*。这在M和N很大时可以很好地节省离线计算的内存。
O(F(M+N))表示M个用户和N个商品均会建立一个隐向量。*
离线计算的时间复杂度
在一般情况下,LFM的时间复杂度要稍微高于UserCF和ItemCF,这主要是因为该算法需要多次迭代。但总体上,这两种算法在时间复杂度上没有质的差别。
在线实时推荐
UserCF和ItemCF需要将相关表缓存在内存中,一旦用户喜欢了新的物品,就可以通过查询内存中的相关表将和该物品相似的其他物品推荐给用户。
而LFM的预测公式可以看到。LFM在给用户生成推荐列表时。需要计算用户对所有物品的兴趣权重,然后排名,返回权重最大的N个物品。当物品数量很多的时候,这个时间复杂度就会非常高,可达
O
(
M
∗
N
∗
F
)
O(M*N*F)
O(M∗N∗F)。因此LFM不太适合用于物品数非常庞大的系统,如
果要用,我们也需要一个比较快的算法给用户先计算一个比较小的候选列表,然后再用
LFM重新排名。同时LFM在生成用户推荐列表时速度太慢,因此不能在线实时计算。需要通过离线将所有用户的推荐结果事先计算存储在数据库中。也就是说,当用户有了新的行为后,他的推荐列表并不会立刻发生变化。
推荐解释
ItemCF算法支持很好的推荐解释。可以利用用户的历史行为解释推荐结果。但是LFM无法提供这样的解释。虽然其计算出来的隐类在语义上代表了一类兴趣和物品,但是很难用自然语言描述并且给用户展示。
2.6 基于图的模型
本节将重点讲解如何将用户行为用图表示,并且利用好图的算法给用户进行个性化推荐。
2.6.1 用户行为数据的二分图表示
本章讨论的用户行为数据是由一系列二元组组成的,其中每个二元组(u, i)表示用户u对物品i产生过行为。这种数据集很容易用一个二分图表示。
在图中,圆形节点代表用户,方形节点代表物品。圆形节点和方形节点之间的边代表用户对物品的行为。比如图中用户节点A和物品节点a、b、d相连,说明用户A对物品a、b、d产生过行为。
2.6.2 基于图的推荐算法
那么如何将个性化推荐算法放到二分图模型上。那么给用户u推荐物品的任务就可以转换为度量用户顶点 v u v_{u} vu和与 v u v_{u} vu没有边直接相连的物品节点在图中的相关性。相关性越高,在推荐结果中的权重越大。
可以用两个顶点之间的路径数,路径长度,路径上经过的顶点来衡量相关度。
相关性高一般有这几个特点:两点间有很多路径相连,顶点之间路径短,连接的两个顶点之间不会经过出度比较大的顶点。
针对上图这个例子,用户A和物品c有两条长度为3的路径相连,分别是:(A,a,B,c)以及(A,d,D,c) 。用户A和物品e之间也有两条长度为3的路径相连。但是顶点A与e之间的相关度要高于顶点A与c之间。
因为顶点A与e之间有两条路径——(A, b, C, e)和(A, d, D, e)。其中,(A, b, C, e)路径经过的顶点的出度为(3, 2, 2,2),而(A, d, D, e)路径经过的顶点的出度为(3, 2, 3, 2)。因此,(A, d, D, e)经过了一个出度比较大的顶点D,所以(A, d, D, e)对顶点A与e之间相关性的贡献要小于(A, b, C, e)。(前面说了相关性比较高的其中一个特点就是:两个顶点之间不会经过出度比较大的顶点)
那么如何计算图中顶点之间的相关度呢??
首先介绍基于随机游走的PersonalRank算法
要对用户u进行个性化推荐,可以从用户u对应的节点 v u v_{u} vu开始在用户物品二分图上进行随机游走。当游走到任何一个节点时,首先按照概率 α \alpha α决定继续往前走,还是停止此次游走回到 v u v_{u} vu重新游走。如果继续游走,就从相连节点中按照均匀分布随机选择一个节点作为下次游走的节点。最终经过很多次之后,每个物品节点被访问的概率会收敛到一个数,最终推荐列表中物品的权重就是物品节点的访问概率。
为什么上面的这种思想可行呢?
回想一下我们之间定义的评价用户和物品之间的相关性的强弱的标准:
其中,不经过出度较大的视为相关性强。既然不经历出度大节点,那么每个节点可走的路较少,每一条路被走到的概率就大了,因此定义的相关度就大了。
最后每个物品被访问到的概率是:分支1被选择的概率 ∗ * ∗分支2被选择到的概率 ∗ * ∗…
度小,分支就少,分支被选择到的概率就越大。
那么上面的思想的公式描述如下:
表示顶点为 v u v_{u} vu时就以 α \alpha α的概率选择下一个顶点。 P R ( v ) PR(v) PR(v)表示访问v节点的概率。 o u t ( v ′ ) out(v^{'}) out(v′)表示 v ′ v^{'} v′点指向的顶点的结合。
代码如下:
# 代码还要细读
def PersonalRank(G, alpha, root): # 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
网上相关的代码:来自博客:https://blog.csdn.net/gamer_gyt/article/details/51694250/
#-*-coding:utf-8-*-
'''
Created on 2016年6月16日
@author: Gamer Think
'''
'''
G:二分图 alpha:随机游走的概率 root:游走的初始节点 max_step;最大走动步数
'''
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是顶点。ri是与其相连的顶点极其边的权重
#取节点i的出边的尾节点j以及边E(i,j)的权重wij, 边的权重都为1,在这不起实际作用
for j, wij in ri.items(): #j是i的连接顶点,wij是权重
#i是j的其中一条入边的首节点,因此需要遍历图找到j的入边的首节点,
#这个遍历过程就是此处的2层for循环,一次遍历就是一次游走
tmp[j] += alpha * rank[i] / (1.0 * len(ri))
#我们每次游走都是从root节点出发,因此root节点的权重需要加上(1 - alpha)
#在《推荐系统实践》上,作者把这一句放在for j, wij in ri.items()这个循环下,我认为是有问题。
tmp[root] += (1 - alpha)
rank = tmp
#输出每次迭代后各个节点的权重
print 'iter: ' + str(k) + "\t",
for key, value in rank.items():
print "%s:%.3f, \t"%(key, value),
print
return rank
'''
主函数,G表示二分图,‘A’表示节点,后边对应的字典的key是连接的顶点,value表示边的权重
'''
if __name__ == '__main__':
G = {'A' : {'a' : 1, 'c' : 1},
'B' : {'a' : 1, 'b' : 1, 'c':1, 'd':1},
'C' : {'c' : 1, 'd' : 1},
'a' : {'A' : 1, 'B' : 1},
'b' : {'B' : 1},
'c' : {'A' : 1, 'B' : 1, 'C':1},
'd' : {'B' : 1, 'C' : 1}}
PersonalRank(G, 0.85, 'A', 100)
虽然随机游走用理论比较好解释,但是该算法在时间复杂度上有明显的缺点。因为对于每个用户进行推荐时,都需要在整个用户物品二分图上进行迭代。直到图中每个顶点的PR值收敛。这些过程时间复杂度高,不仅无法在线提供实时推荐,甚至离线推荐也很耗时。
**关于基于图的模型的博客:**https://blog.csdn.net/gamer_gyt/article/details/51694250/
书上还有进一步的改善 见书P76-P77