在之前我也看了很多人写的推荐系统的博客,理论的、算法的都有,多是个人的理解和感悟,虽然很深刻,但是对于自己而言还是不成系统,于是我参考大牛项亮编著的《推荐系统实践》将该领域知识系统整理一遍,与大家一起学习。
本系列对应的代码请查看https://github.com/wangyuyunmu/Recommended-system-practice
上一篇总结了基于用户行为数据的推荐方法——协同过滤,本篇继续总结基于用户行为数据的相关推荐方法——隐语义。
如何给用户userA推荐item?
1)UserCF通过寻找userA相关的user,找到topN的tems
2)ItemCF找到userA喜欢的item对应的topN的item
3)找到userA对item的兴趣关系(隐语义)以及兴趣与item的关系,来确定user与item的关系。
隐语义分析的核心思想:通过隐含特征(latent factor)联系用户和数据。
1,隐语义分析
对基于兴趣分类的方法,需要解决三个问题:
1)如何对物品进行分类?
2)如何确定用户对哪些类的物品感兴趣,以及感兴趣的程度?
3)对于给定的一个类,选择哪些属于这个类的物品给用户,以及他们的权重?
第一个分类问题可以人工进行处理,但是人分类的主观性、有限性,容易带来偏差和不全面。因此需要从数据出发,自动的进行物品分类,然后进行个性化推荐。
2,LFM(latent factor model)算法
LFM算法的兴趣指标公式如下:
p
r
e
f
e
r
e
n
c
e
(
u
,
i
)
=
r
u
i
=
p
u
t
q
i
=
∑
k
=
1
K
p
u
,
k
q
i
,
k
preference(u,i)=r_{ui}=p_u^tq_i=\sum_{k=1}^Kp_{u,k}q_{i,k}
preference(u,i)=rui=putqi=k=1∑Kpu,kqi,k
r表示用户u对物品i的兴趣,是用户u与隐含类k的关系和物品i与隐含类k的关系决定的。所以需要求得用户对每个类别的关系参数p、每个item与类别的关系参数q,这两个参数。测试的时候只要给定user,计算所有item对应的兴趣指标r,根据r的大小可以判定用户u对item的喜好程度。
C = ∑ ( u , i ) ( r u i − r ^ u i ) 2 = ∑ u , i ( r u i − ∑ k = 1 K p u , k q i , k ) 2 + λ ∥ p u ∥ 2 + λ ∥ q i ∥ 2 C=\sum_{(u,i)}(r_{ui}-\hat r_{ui})^2=\sum_{u,i}(r_{ui}-\sum_{k=1}^Kp_{u,k}q_{i,k})^2+\lambda\Vert p_{u} \Vert^2+\lambda\Vert q_{i} \Vert^2 C=(u,i)∑(rui−r^ui)2=u,i∑(rui−k=1∑Kpu,kqi,k)2+λ∥pu∥2+λ∥qi∥2
C表示loss,是目标喜好程度与预测喜好程度的方差。
2.1 负样本
1)那么目标r是多少呢?
对于显性反馈数据集,r表示用户的评分。
2)对于隐语义数据集,没有负样本怎么办?
对于隐反馈数据集,数据没有评分,没有正负样本的区别,这里就需要自定义训练集的正负样本,正样本为1,负样本为0。负样本可以采样获得。对于user没有标记为喜好的item中采样,得到与正样本相似大小的数据集,保证样本平衡性(这一点很重要);作为负样本集,其中尽量选取热门的item。
这里书中是分步随机选取的负样本,其速度要比下面一种方法快一点(大概24s一次),但是并没有按照流行度采样,如果按照流行度逐个采样,速度太慢(3.5m一次)。所以还是选择一次性多个随机采样的方法,虽然采集的样本数目不是特别准确,但是误伤大雅,速度也可以(30s一次)。
# # 分步按照流行度采集负样本
# for user in new_data:
# seen = set(new_data[user])
# pos_num = len(seen)
# n = 0
# for i in range(pos_num*3):
# # temp = items[np.random.randint(0,len(items)-1)]
# temp = np.random.choice(items, 1, pops)[0]
# if temp in new_data[user]:
# continue
# new_data[user][temp] = 0
# n += 1
# if n > pos_num*ratio:
# break
# 负样本
for user in new_data:
seen = set(new_data[user])
pos_num = len(seen)
item = np.random.choice(items, int(pos_num * ratio * 3), pops)
item = [x for x in item if x not in seen][:int(pos_num * ratio)]
new_data[user].update({x: 0 for x in item})
2.2 算法优化
那么通过梯度下降算法,对loss两个参数求导,就可以获得梯度,优化即可。
∂
C
∂
p
u
k
=
−
2
q
i
k
(
r
u
i
−
∑
k
=
1
K
p
u
,
k
q
i
,
k
)
+
2
λ
p
u
k
\frac {\partial C}{\partial p_{uk}}=-2q_{ik}(r_{ui}-\sum_{k=1}^Kp_{u,k}q_{i,k})+2\lambda p_{uk}
∂puk∂C=−2qik(rui−k=1∑Kpu,kqi,k)+2λpuk
∂
C
∂
q
i
k
=
−
2
p
u
k
(
r
u
i
−
∑
k
=
1
K
p
u
,
k
q
i
,
k
)
+
2
λ
q
i
k
\frac {\partial C}{\partial q_{ik}}=-2p_{uk}(r_{ui}-\sum_{k=1}^Kp_{u,k}q_{i,k})+2\lambda q_{ik}
∂qik∂C=−2puk(rui−k=1∑Kpu,kqi,k)+2λqik
# 训练
P, Q = {}, {}
for user in train:
P[user] = np.random.random(K)
for item in items:
Q[item] = np.random.random(K)
for s in trange(step):
data = nSample(train, ratio)
for user in data:
for item in data[user]:
eui = data[user][item] - (P[user] * Q[item]).sum()
P[user] += lr * (Q[item] * eui - lmbda * P[user])
Q[item] += lr * (P[user] * eui - lmbda * Q[item])
lr *= 0.9 # 调整学习率
由此看来,LFM算法需要维护两个表p、q,参数共有k*(u+i)个,其受类别k的影响,要比CF算法保存的相关性表格小的多。
推荐过程:训练好p、q之后,给定一个user,遍历所有item,计算r,排序得到topN个items。
# 获取接口函数
def GetRecommendation(user):
seen_items = set(train[user])
recs = {}
for item in items:
if item not in seen_items:
recs[item] = (P[user] * Q[item]).sum()
recs = list(sorted(recs.items(), key=lambda x: x[1], reverse=True))[:N]
return recs
训练结果如下:
#M=8取平均,top-n=10,ratio=5,负样本5倍
Metric: {'Precision': 25.9, 'Recall': 12.44, 'Coverage': 26.5, 'Popularity': 7.10661}
2.3 LFM与CF对比
LFM方法与基于邻域的方法(CF)比较:
1,LFM是一种学习方法,CF是统计方法
2,LFM空间复杂度O(F*(M+N)),CF的空间复杂度O(MM)或者O(NN)
3,离线时间复杂度差不多
4,CF可以在内存中保存一个表,进行实时推荐。LFM在推荐时需要遍历每一个物品然后进行排名,当item很多时,时间复杂度很高,不太适用于物品非常多的系统;LFM在生成用户推荐表时太慢,不能实时计算。
5,解释性不强,无法用语言描述解释推荐原因。
2.4 其他模型
隐语义模型(LFM,latent factor model),相关的有LSI、pLSA、LDA和topic model。隐含类别模型(latent class model)、隐含主题模型(latent topic model)、矩阵分解(MF)。
以上模型将会在其他专栏中总结。