1. 协同过滤算法概述
协同过滤(Collaborative Filtering)推荐算法是最经典、最常用的推荐算法。
所谓协同过滤, 基本思想是根据用户之前的喜好以及其他兴趣相近的用户的选择来给用户推荐物品(基于对用户历史行为数据的挖掘发现用户的喜好偏向, 并预测用户可能喜好的产品进行推荐),一般是仅仅基于用户的行为数据(评价、购买、下载等),而不依赖于项的任何附加信息(物品自身特征)或者用户的任何附加信息(年龄,性别等)
协同过滤算法
- 基于用户的协同过滤算法(UserCF): 给用户推荐和他兴趣相似的其他用户喜欢的产品
- 基于物品的协同过滤算法(ItemCF): 给用户推荐和他之前喜欢的物品相似的物品
不管是UserCF还是ItemCF算法, 非常重要的步骤之一就是计算用户和用户或者物品和物品之间的相似度。
2. 相似度指标与计算
- 杰卡德(Jaccard)相似系数
这个是衡量两个集合的相似度一种指标。两个用户和交互商品交集的数量占这两个用户交互商品并集的数量的比例,称为两个集合的杰卡德相似系数
- 分子是两个布尔向量做点积计算, 得到的就是交集元素的个数
- 分母是两个布尔向量做或运算, 再求元素和
sim u v = ∣ N ( u ) ∩ N ( v ) ∣ ∣ N ( u ) ∣ ∪ ∣ N ( v ) ∣ \operatorname{sim}_{u v}=\frac{|N(u) \cap N(v)|}{\sqrt{|N(u)| \cup \mid N(v)} \mid} simuv=∣N(u)∣∪∣N(v)∣∣N(u)∩N(v)∣
- 余弦相似度
相比于Jaccard公式来说就是分母有差异,不是两个用户交互商品的并集的数量,而是两个用户分别交互的商品数量的乘积
余弦相似度的特点, 与向量长度无关,余弦相似度计算要对向量长度归一化, 两个向量只要方向一致,无论程度强弱, 都可以视为’相似’
sim u v = ∣ N ( u ) ∣ ∩ ∣ N ( v ) ∣ ∣ N ( u ) ∣ ⋅ ∣ N ( v ) ∣ \operatorname{sim}_{u v}=\frac{|N(u)| \cap|N(v)|}{\sqrt{|N(u)| \cdot \mid N(v)} \mid} simuv=∣N(u)∣⋅∣N(v)∣∣N(u)∣∩∣N(v)∣
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
import pandas as pd
# 避免稀疏矩阵 采用集合方式计算 生成的矩阵为[[simii, simij], [simji, simjj]]
i = [1, 0, 0, 0]
j = [1, 0.5, 0.5, 0]
cosine_similarity([i, j])
# 生成方式二
# 余弦相似度
i = np.array(i)
j = np.array(j)
i.dot(j)/(np.linalg.norm(i) * np.linalg.norm(j))
- 皮尔逊系数
相比余弦相似度,皮尔逊相关系数通过使用用户的平均分对各独立评分进行修正,减小了用户评分偏置的影响
实际上也是一种余弦相似度, 不过先对向量做了中心化, 向量a b 各自减去向量的均值后, 再计算余弦相似度。
皮尔逊相似度计算结果在-1,1之间 -1表示负相关, 1表示正相关
sim ( u , v ) = ∑ i ∈ I ( r u i − r ˉ u ) ( r v i − r ˉ v ) ∑ i ∈ I ( r u i − r ˉ u ) 2 ∑ i ∈ I ( r v i − r ˉ v ) 2 \operatorname{sim}(u, v)=\frac{\sum_{i \in I}\left(r_{u i}-\bar{r}_{u}\right)\left(r_{v i}-\bar{r}_{v}\right)}{\sqrt{\sum_{i \in I}\left(r_{u i}-\bar{r}_{u}\right)^{2}} \sqrt{\sum_{i \in I}\left(r_{v i}-\bar{r}_{v}\right)^{2}}} sim(u,v)=∑i∈I(rui−rˉu)2∑i∈I(rvi−rˉv)2∑i∈I(rui−rˉu)(rvi−rˉv)
i = np.array([1, 0, 0, 0])
j = np.array([1, 0.5, 0.5, 0])
# 皮尔逊系数
i_avg = np.mean(i)
j_avg = np.mean(j)
i_temp = i - i_avg
j_temp = j - j_avg
sim = i_temp.dot(j_temp) / (np.sqrt(np.sum(i_temp**2) * np.sum(j_temp**2)))
sim
# 皮尔逊系数
pearsonr(i, j)
# 皮尔逊系数
np.corrcoef(i, j)
3. 基于用户的协同过滤实现(UserCF)
思路
- 第一步: 找出和目标用户兴趣类似的用户集合
- 第二步: 找到集合中用户喜欢的,且目标用户没有听说过的物品推荐给目标用户
示例:
- 计算用户相似度
- 根据相似度用户计算Alice对物品5的最终得分
- 根据用户评分对用户进行推荐
此次采用皮尔逊系数
sim ( u , v ) = ∑ i ∈ I ( r u i − r ˉ u ) ( r v i − r ˉ v ) ∑ i ∈ I ( r u i − r ˉ u ) 2 ∑ i ∈ I ( r v i − r ˉ v ) 2 \operatorname{sim}(u, v)=\frac{\sum_{i \in I}\left(r_{u i}-\bar{r}_{u}\right)\left(r_{v i}-\bar{r}_{v}\right)}{\sqrt{\sum_{i \in I}\left(r_{u i}-\bar{r}_{u}\right)^{2}} \sqrt{\sum_{i \in I}\left(r_{v i}-\bar{r}_{v}\right)^{2}}} sim(u,v)=∑i∈I(rui−rˉu)2∑i∈I(rvi−rˉv)2∑i∈I(rui−rˉu)(rvi−rˉv)
最终打分预测公式
P i , j = R ˉ i + ∑ k = 1 n ( S i , k ( R k , j − R ˉ k ) ) ∑ k = 1 n S j , k P_{i, j}=\bar{R}_{i}+\frac{\sum_{k=1}^{n}\left(S_{i, k}\left(R_{k, j}-\bar{R}_{k}\right)\right)}{\sum_{k=1}^{n} S_{j, k}} Pi,j=Rˉi+∑k=1nSj,k∑k=1n(Si,k(Rk,j−Rˉk))
import pandas as pd
import numpy as np
# 定义数据集, 也就是那个表格, 注意这里我们采用字典存放数据, 因为实际情况中数据是非常稀疏的, 很少有情况是现在这样
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
"""计算用户相似性矩阵"""
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])
# 这里可以获得相似性矩阵(共现矩阵) 其中[0][1]与[1][0]相同为相关性系数
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]
for i in range(1, 6):
similarity_matrix[i][i] = 1.0
"""计算前n个相似的用户"""
n = 2
similarity_users = similarity_matrix[1].sort_values(ascending=False)[1:n+1].index.tolist() # [2, 3]也就是用户1和用户2
"""计算最终得分"""
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
至此, 我们就用代码完成了上面的小例子, 有了这个评分, 我们其实就可以对该用户做推荐了。
4. 基于物品的协同过滤实现(ItemCF)
思路
- Step 1:计算物品之间的相似度
- Step 2:根据物品的相似度和用户的历史行为,为用户生成推荐列表
需要关注的是相关性系数和得分公式的改变
仍然采用皮尔逊系数, 需要注意对象为商品
sim ( u , v ) = ∑ i ∈ I ( r u i − r ˉ u ) ( r v i − r ˉ v ) ∑ i ∈ I ( r u i − r ˉ u ) 2 ∑ i ∈ I ( r v i − r ˉ v ) 2 \operatorname{sim}(u, v)=\frac{\sum_{i \in I}\left(r_{u i}-\bar{r}_{u}\right)\left(r_{v i}-\bar{r}_{v}\right)}{\sqrt{\sum_{i \in I}\left(r_{u i}-\bar{r}_{u}\right)^{2}} \sqrt{\sum_{i \in I}\left(r_{v i}-\bar{r}_{v}\right)^{2}}} sim(u,v)=∑i∈I(rui−rˉu)2∑i∈I(rvi−rˉv)2∑i∈I(rui−rˉu)(rvi−rˉv)
最终的打分预测
P Alice , 物品 5 = R ˉ 物品 5 + ∑ k = 1 2 ( S 物品 5 , 物品 k ( R Alice,物品 k − R ˉ 物品 k ) ) ∑ k = 1 2 S 物品 k , 物品 5 P_{\text {Alice }, \text { 物品 } 5}=\bar{R}_{\text {物品 } 5}+\frac{\sum_{k=1}^{2}\left(S_{\text {物品 } 5, \text { 物品 } k}\left(R_{\text {Alice,物品 } k}-\bar{R}_{\text {物品 } k}\right)\right)}{\sum_{k=1}^{2} S_{\text {物品 } k, \text { 物品 } 5}} PAlice , 物品 5=Rˉ物品 5+∑k=12S物品 k, 物品 5∑k=12(S物品 5, 物品 k(RAlice,物品 k−Rˉ物品 k))
这里仍然采用上面的例子:
- 计算商品相似度
- 找出与物品5最相近的n个物品
- 根据Alice对最相近的n个物品的打分去计算对物品5的打分情况
import pandas as pd
import numpy as np
# 定义数据集, 也就是那个表格, 注意这里我们采用字典存放数据, 因为实际情况中数据是非常稀疏的, 很少有情况是现在这样
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
"""计算物品的相似矩阵"""
similarity_matrix = pd.DataFrame(np.ones((len(items), len(items))), index=['A', 'B', 'C', 'D',
'E'], columns=['A', 'B', 'C', 'D', 'E'])
# 遍历每条物品-用户评分数据
for itemId in items:
for otheritemId in items:
vec_item = [] # 定义列表, 保存当前两个物品的向量值
vec_otheritem = []
#userRagingPairCount = 0 # 两件物品均评过分的用户数
if itemId != otheritemId: # 物品不同
for userId in users: # 遍历用户-物品评分数据
userRatings = users[userId] # 每条数据为该用户对所有物品的评分, 这也是个字典
if itemId in userRatings and otheritemId in userRatings: # 用户对这两个物品都评过分
#userRagingPairCount += 1
vec_item.append(userRatings[itemId])
vec_otheritem.append(userRatings[otheritemId])
# 这里可以获得相似性矩阵(共现矩阵)
similarity_matrix[itemId][otheritemId] = np.corrcoef(np.array(vec_item),
np.array(vec_otheritem))[0][1]
#similarity_matrix[itemId][otheritemId] = cosine_similarity(np.array(vec_item),np.array(vec_otheritem))[0][1]
"""得到与物品5相似的前n个物品"""
n = 2
similarity_items = similarity_matrix['E'].sort_values(ascending=False)[1:n+1].index.tolist() #['A', 'D'] 这里使降序排列要排除最初1.0
"""计算最终得分"""
base_score = np.mean(np.array([value for value in items['E'].values()]))
weighted_scores = 0.
corr_values_sum = 0.
for item in similarity_items: # ['A', 'D']
corr_value = similarity_matrix['E'][item] # 两个物品之间的相似性
mean_item_score = np.mean(np.array([value for value in items[item].values()])) # 每个物品的打分平均值
weighted_scores += corr_value * (users[1][item]-mean_item_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
至此, 我们就用代码完成了上面的小例子
5. 算法评估
- 召回率
这个意思就是说, 在用户真实购买或者看过的影片里面, 我模型真正预测出了多少, 这个考察的是模型推荐的一个全面
性。 - 准确率
这个意思再说, 在我推荐的所有物品中, 用户真正看的有多少, 这个考察的是我模型推荐的一个准确性。 - 覆盖率
覆盖率反映了推荐算法发掘长尾的能力, 覆盖率越高, 说明推荐算法越能将长尾中的物品推荐给用户 - 新颖度
用推荐列表中物品的平均流行度度量推荐结果的新颖度。 如果推荐出的物品都很热门, 说明推荐的新颖度较低。
6. 课后思考
- 什么时候使用UserCF,什么时候使用ItemCF?为什么?
- UserCF
由于是基于用户相似度进行推荐, 所以具备更强的社交特性, 这样的特点非常适于用户少,物品多,时效性较强的场合, 比如新闻推荐场景, 因为新闻本身兴趣点分散, 相比用户对不同新闻的兴趣偏好, 新闻的及时性,热点性往往更加重要, 所以正好适用于发现热点,跟踪热点的趋势。 另外还具有推荐新信息的能力, 更有可能发现惊喜, 因为看的是人与人的相似性, 推出来的结果可能更有惊喜,可以发现用户潜在但自己尚未察觉的兴趣爱好。对于用户较少, 要求时效性较强的场合, 就可以考虑UserCF。
- ItemCF
这个更适用于兴趣变化较为稳定的应用, 更接近于个性化的推荐, 适合物品少,用户多,用户兴趣固定持久,物品更新速度不是太快的场合
- 协同过滤在计算上有什么缺点?有什么比较好的思路可以解决(缓解)?
第一个问题就是泛化能力弱, 即协同过滤无法将两个物品相似的信息推广到其他物品的相似性上。 导致的问题是热门物品具有很强的头部效应,容易跟大量物品产生相似,而尾部物品由于特征向量稀疏,导致很少被推荐。 比如下面这个例子:
A, B, C, D是物品, 看右边的物品共现矩阵, 可以发现物品D与A、B、C的相似度比较大, 所以很有可能将D推荐给用过A、B、C的用户。 但是物品D与其他物品相似的原因是因为D是一件热门商品, 系统无法找出A、B、C之间相似性的原因是其特征太稀疏, 缺乏相似性计算的直接数据。 所以这就是协同过滤的天然缺陷:推荐系统头部效应明显,处理稀疏向量的能力弱。
- 上面介绍的相似度计算方法有什么优劣之处?
cosine相似度还是比较常用的, 一般效果也不会太差, 但是对于评分数据不规范的时候, 也就是说, 存在有的用户喜欢打高分, 有的用户喜欢打低分情况的时候,有的用户喜欢乱打分的情况, 这时候consine相似度算出来的结果可能就不是那么准确了, 比如下面这种情况
这时候, 如果用余弦相似度进行计算, 会发现用户d和用户f比较相似, 而实际上, 如果看这个商品喜好的一个趋势的话, 其实d和e比较相近, 只不过e比较喜欢打低分, d比较喜欢打高分。 所以对于这种用户评分偏置的情况, 余弦相似度就不是那么好了, 可以考虑使用下面的皮尔逊相关系数。
- 协同过滤还存在其他什么缺陷?有什么比较好的思路可以解决(缓解)?
协同过滤的特点就是完全没有利用到物品本身或者是用户自身的属性, 仅仅利用了用户与物品的交互信息就可以实现推荐,比较简单高效, 但这也是它的一个短板所在, 由于无法有效的引入用户年龄, 性别,商品描述,商品分类,当前时间,地点等一系列用户特征、物品特征和上下文特征, 这就造成了有效信息的遗漏,不能充分利用其它特征数据。为了解决这个问题, 在推荐模型中引用更多的特征,推荐系统慢慢的从以协同过滤为核心到了以逻辑回归模型为核心, 提出了能够综合不同类型特征的机器学习模型。