最近参加了Datawhale新闻推荐的学习,之前自己没有接触过推荐算法相关的内容,所以完全算是个小白,要从基础抓起,所以第一步会跟着组队第一次的学习任务跑一边baseline,然后了解清楚协同过滤算法基本情况吧~
贴一个新闻推荐比赛数据的链接 添加链接描述
协同过滤(Collaborative Filtering)推荐算法是最经典、最常用的推荐算法。所谓协同过滤, 基本思想是根据用户之前的喜好以及其他兴趣相近的用户的选择来给用户推荐物品(基于对用户历史行为数据的挖掘发现用户的喜好偏向, 并预测用户可能喜好的产品进行推荐),一般是仅仅基于用户的行为数据(评价、购买、下载等), 而不依赖于项的任何附加信息(物品自身特征)或者用户的任何附加信息(年龄, 性别等)。目前应用比较广泛的协同过滤算法是基于邻域的方法, 而这种方法主要有下面两种算法:
基于用户的协同过滤算法(UserCF): 给用户推荐和他兴趣相似的其他用户喜欢的产品
基于物品的协同过滤算法(ItemCF): 给用户推荐和他之前喜欢的物品相似的物品
不管是UserCF还是ItemCF算法, 非常重要的步骤之一就是计算用户和用户或者物品和物品之间的相似度,所以下面先整理一下常用的相似度的计算方法。
这个是衡量两个集合的相似度一种指标。两个用户 u和 v交互商品交集的数量占这两个用户交互商品并集的数量的比例,称为两个集合的杰卡德相似系数,用符号 s i m u v sim_{uv} simuv 表示,其中 N(u),N(v) 分别表示用户 u和用户 v 交互商品的集合。
s i m u v = ∣ N ( u ) ⋂ N ( v ) ∣ ∣ N ( u ) ⋃ N ( v ) ∣ sim_{uv} = \frac{|N(u) \bigcap N(v)|}{|N(u) \bigcup N(v)|} simuv=∣N(u)⋃N(v)∣∣N(u)⋂N(v)∣
由于杰卡德相似系数一般无法反映具体用户的评分喜好信息, 所以常用来评估用户是否会对某商品进行打分, 而不是预估用户会对某商品打多少分。
余弦相似度衡量了两个向量的夹角,夹角越小越相似。首先从集合的角度描述余弦相似度,相比于Jaccard公式来说就是分母有差异,不是两个用户交互商品的并集的数量,而是两个用户分别交互的商品数量的乘积,公式如下:
s i m u v = ∣ N ( u ) ∣ ⋂ ∣ N ( v ) ∣ ∣ N ( u ) ∣ ∗ ∣ N ( v ) ∣ sim_{uv} = \frac{|N(u) | \bigcap | N(v)|}{\sqrt{|N(u)| *|N(v)|}} simuv=∣N(u)∣∗∣N(v)∣∣N(u)∣⋂∣N(v)∣
从向量的角度进行描述,令矩阵 A为用户-商品交互矩阵(因为是TopN推荐并不需要用户对物品的评分,只需要知道用户对商品是否有交互就行),即矩阵的每一行表示一个用户对所有商品的交互情况,有交互的商品值为1没有交互的商品值为0,矩阵的列表示所有商品。若用户和商品数量分别为 m,n的话,交互矩阵 A 就是一个 m 行 n 列的矩阵。此时用户的相似度可以表示为(其中 u⋅v 指的是向量点积):
s i m u v = c o s ( u , v ) = u . v ∣ u ∣ . ∣ v ∣ sim_{uv}=cos(u,v)=\frac{u.v}{|u|.|v|} simuv=cos(u,v)=∣u∣.∣v∣u.v
上述用户-商品交互矩阵在现实情况下是非常的稀疏了,为了避免存储这么大的稀疏矩阵,在计算用户相似度的时候一般会采用集合的方式进行计算。理论上向量之间的相似度计算公式都可以用来计算用户之间的相似度,但是会根据实际的情况选择不同的用户相似度度量方法。
这个在具体实现的时候, 可以使用sklearn中的cosine_similarity进行实现,代码及结果如下:
from sklearn.metrics.pairwise import cosine_similarity
i = [1, 0, 0, 0]
j = [1, 0.5, 0.5, 0]
cosine_similarity([i, j])
皮尔逊相关系数的公式与余弦相似度的计算公式非常的类似,首先对于上述的余弦相似度的计算公式写成求和的形式,其中 r_{ui},r_{vi}分别表示用户 u 和用户 v对商品 i是否有交互(或者具体的评分值):
所以相比余弦相似度,皮尔逊相关系数通过使用用户的平均分对各独立评分进行修正,减小了用户评分偏置的影响。具体实现, 我们也是可以调包, 这个计算方式很多, 下面是其中的一种代码介绍
from scipy.stats import pearsonr
i = [1, 0, 0, 0]
j = [1, 0.5, 0.5, 0]
pearsonr(i, j)
基于用户的协同过滤(以下用UserCF表示),思想其实比较简单,当一个用户A需要个性化推荐的时候, 我们可以先找到和他有相似兴趣的其他用户, 然后把那些用户喜欢的, 而用户A没有听说过的物品推荐给A。
上面的两个步骤中, 第一个步骤里面, 我们会基于前面给出的相似性度量的方法找出与目标用户兴趣相似的用户, 而第二个步骤里面, 如何基于相似用户喜欢的物品来对目标用户进行推荐呢? 这个要依赖于目标用户对相似用户喜欢的物品的一个喜好程度, 那么如何衡量这个程度大小呢? 为了更好理解上面的两个步骤, 下面拿一个具体的例子把两个步骤具体化。
以下图为例,此例将会用于本文各种算法中
给用户推荐物品的过程可以形象化为一个猜测用户对商品进行打分的任务,上面表格里面是5个用户对于5件物品的一个打分情况,就可以理解为用户对物品的喜欢程度,我们最终要通过算法推测出Alice用户对物品5的打分。
应用UserCF算法的两个步骤:
关于第一个步骤, 上面已经给出了计算两个用户相似性的方法, 这里不再过多赘述, 这里主要解决第二个问题, 如何产生最终结果的预测。
方法一:
根据上面的几种方法, 我们可以计算出向量之间的相似程度, 也就是可以计算出Alice和其他用户的相近程度, 这时候我们就可以选出与Alice最相近的前n个用户, 基于他们对物品5的评价猜测出Alice的打分值, 那么是怎么计算的呢?
这里常用的方式之一是利用用户相似度和相似用户的评价加权平均获得用户的评价预测, 用下面式子表示:
方法二:
还有一种方式如下, 这种方式考虑的更加全面, 依然是用户相似度作为权值, 但后面不单纯的是其他用户对物品的评分, 而是相似用户对该物品的评分与此用户的所有评分平均值的差值进行加权平均, 这时候考虑到了有的用户内心的评分标准不一的情况, 即有的用户喜欢打高分, 有的用户喜欢打低分的情况。
在获得用户 u对不同物品的评价预测后, 最终的推荐列表根据预测评分进行排序得到。 至此,基于用户的协同过滤算法的推荐过程完成。
根据上面的问题, 下面手算一下:
目标: 猜测Alice对物品5的得分:
1、计算Alice与其他用户的相似度(这里使用皮尔逊相关系数)
这里我们使用皮尔逊相关系数, 也就是Alice与用户1的相似度是0.85。同样的方式, 我们就可以计算与其他用户的相似度, 这里可以使用numpy的相似度函数得到用户的相似性矩阵:
从这里看出, Alice用户和用户1,用户2,用户3,用户4的相似度是0.85,0.7,0, -0.79。 所以如果n=2, 找到与Alice最相近的两个用户是用户1, 和Alice的相似度是0.85, 用户2, 和Alice相似度是0.7。
2、根据相似度用户计算Alice对物品5的最终得分
用户1对物品5的评分是3, 用户2对物品5的打分是5, 那么根据上面的计算公式, 可以计算出Alice对物品5的最终得分是
3、根据用户评分对用户进行推荐
这时候, 我们就得到了Alice对物品5的得分是4.87, 根据Alice的打分对物品排个序从大到小:
这时候,如果要向Alice推荐2款产品的话, 我们就可以推荐物品1和物品5给Alice。至此, 基于用户的协同过滤算法原理介绍完毕。
这里简单的通过编程实现上面的案例,为后面的新闻推荐算法做一个热身, 梳理一下上面的过程其实就是三步: 计算用户相似性矩阵、得到前n个相似用户、计算最终得分。
所以我们下面的程序也是分为这三步:
1、首先, 先把数据表给建立起来
这里采用了字典的方式, 之所以没有用pandas, 是因为上面举得这个例子其实是个个例, 在真实情况中, 我们知道, 用户对物品的打分情况并不会这么完整, 会存在大量的空值, 所以矩阵会很稀疏, 这时候用DataFrame, 会有大量的NaN。故这里用字典的形式存储。 用两个字典, 第一个字典是物品-用户的评分映射, 键是物品1-5, 用A-E来表示, 每一个值又是一个字典, 表示的是每个用户对该物品的打分。 第二个字典是用户-物品的评分映射, 键是上面的五个用户, 用1-5表示, 值是该用户对每个物品的打分。
import numpy as np
import pandas as pd
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity='all'
def loadData():
items={
'A': {
1: 5, 2: 3, 3: 4, 4: 3, 5: 1},
'B': {
1: 3, 2: 1, 3: 3, 4: 3, 5: 5},
'C': {
1: 4, 2: 2, 3: 4, 4: 1, 5: 5},
'D': {
1: 4, 2: 3, 3: 3, 4: 5, 5: 2},
'E': {
2: 3, 3: 5, 4: 4, 5: 1}
}
users={
1: {
'A': 5, 'B': 3, 'C': 4, 'D': 4},
2: {
'A': 3, 'B': 1, 'C': 2, 'D': 3, 'E': 3},
3: {
'A': 4, 'B': 3, 'C': 4, 'D': 3, 'E': 5},
4: {
'A': 3, 'B': 3, 'C': 1, 'D': 5, 'E': 4},
5: {
'A': 1, 'B': 5, 'C': 5, 'D': 2, 'E': 1}
}
return items,users
items, users = loadData()
item_df = pd.DataFrame(items).T
user_df = pd.DataFrame(users).T
item_df
user_df
2、计算用户相似性矩阵
这个是一个共现矩阵, 5*5,行代表每个用户, 列代表每个用户, 值代表用户和用户的相关性,这里的思路是这样, 因为要求用户和用户两两的相关性, 所以需要用双层循环遍历用户-物品评分数据, 当不是同一个用户的时候, 我们要去遍历物品-用户评分数据, 在里面去找这两个用户同时对该物品评过分的数据放入到这两个用户向量中。 因为正常情况下会存在很多的NAN, 即可能用户并没有对某个物品进行评分过, 这样的不能当做用户向量的一部分, 没法计算相似性。 还是看代码吧, 感觉不太好描述:
“”“计算用户相似性矩阵”""
similarity_matrix = pd.DataFrame(np.zeros((len(users), len(users))), index=[1, 2, 3, 4, 5], columns=[1, 2, 3, 4, 5])
for userID in users:
for otheruserId in users:
vec_user = []
vec_otheruser = []
if userID != otheruserId:
for itemId in items: # 遍历物品-用户评分数据
itemRatings = items[itemId] # 这也是个字典 每条数据为所有用户对当前物品的评分
if userID in itemRatings and otheruserId in itemRatings: # 说明两个用户都对该物品评过分
vec_user.append(itemRatings[userID])
vec_otheruser.append(itemRatings[otheruserId])
# 这里可以获得相似性矩阵(共现矩阵)
similarity_matrix[userID][otheruserId] = np.corrcoef(np.array(vec_user), np.array(vec_otheruser))[0][1]
#similarity_matrix[userID][otheruserId] = cosine_similarity(np.array(vec_user), np.array(vec_otheruser))[0][1]
3、计算前n个相似的用户
有了相似性矩阵, 我们就可以得到与Alice最相关的前n个用户。
"""计算前n个相似的用户"""
n = 2
similarity_users = similarity_matrix[1].sort_values(ascending=False)[:n].index.tolist() # [2, 3] 也就是用户1和用户2
4、计算最终得分
这里就是上面的那个公式了。
"""计算最终得分"""
base_score = np.mean(np.array([value for value in users[1].values()]))
weighted_scores = 0.
corr_values_sum = 0.
for user in similarity_users: # [2, 3]
corr_value = similarity_matrix[1][user] # 两个用户之间的相似性
mean_user_score = np.mean(np.array([value for value in users[user].values()])) # 每个用户的打分平均值
weighted_scores += corr_value * (users[user]['E']-mean_user_score) # 加权分数
corr_values_sum += corr_value
final_scores = base_score + weighted_scores / corr_values_sum
print('用户Alice对物品5的打分: ', final_scores)
user_df.loc[1]['E'] = final_scores
user_df
添加链接描述
Datawhale详细解答。有时间再补,最近工作还有考试比较多,有点忙不过来。空了补上!
Baseline在 本地运行了一遍,想在天池实验室运行的,奈何数据就是读不进去,第一次使用天池实验室,可能还不太用的缘故,有空再细细探究!
本地运行的情况,代码是从头到尾运行了一遍,但是有些代码的细节还没能够完全消化,不是能完全理解,只是对推荐算法了解了一些,算入门吧,知道了相似度的计算,协同过滤算法,以及最终结果计算的几种方法!
未知的事情还有很多,要保持热情继续一直好奇并探索下去呀~