这篇博客介绍一下本人对协同过滤算法的一点粗浅的理解
协同过滤是一种经典的推荐算法,其主要功能是进行预测和推荐。协同过滤算法主要是通过对用户的历史数据进行挖掘,从而获取用户的偏好,进而对用户进行推荐。协同过滤算法主要分为两类:第一类是基于用户的协同过滤算法(user-based collaboratIve filtering),第二种是基于物品的协同过滤算法(item-based collaboratIve filtering)。接下来分别介绍这两种算法:
- 基于用户的协同过滤算法(user-based collaboratIve filtering)
基于用户的协同过滤算法的核心思想就是根据用户的购买的历史数据找出和被推荐用户偏好相同的用户,根据这些被找出的用户的偏好来对被推荐用户进行商品的推荐。
- 寻找偏好相同的用户
假设存在如下的购买历史数据,表中为用户对商品的打分,例如我们现在要对用户2进行商品的推荐
商品0 | 商品1 | 商品2 | 商品3 | 商品4 | |
用户0 | 6.3 | 7.9 | 9.4 | 7.5 | 1.4 |
用户1 | 2.8 | 8.6 | 3.2 | 8.4 | 3.9 |
用户2 | 8.3 | 2.1 | 1.9 | 8.8 | 6.2 |
用户3 | 10.1 | 5. | 3.5 | 10.3 | 1.8 |
用户4 | 5.3 | 8.5 | 3.5 | 5.7 | 3. |
计算不同用户之间的余弦相似度(当然也可以采取其他指标来衡量用户之间的相似度,如欧氏距离,皮尔逊相关系数等),每一行作为描述用户的一个向量,为了避免余弦相似度对于数据大小的不敏感,我们采用修正余弦相似度,修正余弦相似度就是原始数据中每一列的值减去每一列的均值之后再求余弦相似度(如果是基于商品的话就是每一行减去每一行的均值之后再求余弦相似度),求得的余弦相似度矩阵如下所示:
用户0 | 用户1 | 用户2 | 用户3 | 用户4 | |
用户0 | 1 | -0.10743661 | -0.72897511 | -0.21702824 | 0.06820863 |
用户1 | -0.10743661 | 1 | -0.41094847 | -0.74749105 | 0.581322 |
用户2 | -0.72897511 | -0.41094847 | 1 | 0.40040862 | -0.54275862 |
用户3 | -0.21702824 | -0.74749105 | 0.40040862 | 1 | -0.70154355 |
用户4 | 0.06820863 | 0.581322 | -0.54275862 | -0.70154355 | 1 |
从上表中可以看出和用户2最相似的前两个用户分别为用户3以及用户1
假设对于新一批商品,用户商品矩阵如下:
商品0 | 商品1 | 商品2 | 商品3 | 商品4 | |
用户0 | 7.9 | 2.5 | 5.8 | 4.6 | 2.8 |
用户1 | 2.7 | 5. | 2.3 | 6.1 | 0. |
用户2 | 2.3 | 0. | 0. | 0.8 | 1.8 |
用户3 | 5.3 | 4.2 | 0.6 | 9.1 | 1.4 |
用户4 | 3.6 | 2.5 | 1. | 4.6 | 2.1 |
我们只需要看用户3以及用户1购买了的,但是用户2未购买的商品,从表中可以看出就是商品1和商品3了,然而,商品1和商品3哪一个更应该优先推荐给用户2呢?在这里我们这样计算:使用用户3和用户1与用户2的相似度作为权值,对评分进行加权求和再进行排序便得出了推荐顺序,计算过程如下:
因此,我们对于用户2的推荐顺序为,优先推荐商品3,其次推荐商品1
到这里基于用户的协同过滤算法原理就说完了,接下来我们讨论基于商品的协同过滤算法
- 基于物品的协同过滤算法(item-based collaboratIve filtering)
基于物品的协同过滤算法的核心思想就是,从新的商品中找出与用户已经购买的商品较为相似的商品,然后在推荐给用户。
先假设用户商品矩阵同上,如下所示:
商品0 | 商品1 | 商品2 | 商品3 | 商品4 | |
用户0 | 6.3 | 7.9 | 9.4 | 7.5 | 1.4 |
用户1 | 2.8 | 8.6 | 3.2 | 8.4 | 3.9 |
用户2 | 8.3 | 2.1 | 1.9 | 8.8 | 6.2 |
用户3 | 10.1 | 5. | 3.5 | 10.3 | 1.8 |
用户4 | 5.3 | 8.5 | 3.5 | 5.7 | 3. |
现在我们需要对用户2基于商品1来进行推荐
我们可以求得商品的修正余弦相似度矩阵,如下所示:
商品0 | 商品1 | 商品2 | 商品3 | 商品4 | |
商品0 | 1 | -0.67573792 | -0.47589085 | 0.52394413 | -0.2619477 |
商品1 | -0.67573792 | 1 | 0.17905794 | -0.08555884 | -0.38554924 |
商品2 | -0.47589085 | 0.17905794 | 1 | -0.73612574 | 0.02310763 |
商品3 | 0.52394413 | -0.08555884 | -0.73612574 | 1 | -0.58185256 |
商品4 | -0.2619477 | -0.38554924 | 0.02310763 | -0.58185256 | 1 |
可以明显的看出,和商品1最相似的前2个(这里我们去前2个)商品为商品2和商品3,相似度依次为0.17905794和-0.08555884
现假设有新用户(用户A-C)对一批新商品(商品A-C)以及老商品进行了购买,对于老商品的购买,我们只关注对商品2和商品3的评分,则新用户对新商品(商品A-C)以及对老商品(商品2和商品3)的评分如下矩阵:
商品A | 商品B | 商品C | 商品2 | 商品3 | |
用户A | 1.57319796 | -1.92680204 | 3.47319796 | -0.98441243 | -2.13518146 |
用户B | -0.04562112 | -0.84562112 | -1.34562112 | -0.4090029 | 2.64586626 |
用户C | -2.92988811 | -3.92988811 | -0.62988811 | 3.82356068 | 3.66610367 |
可以计算得到修正余弦相似度矩阵如下所示:
商品A | 商品B | 商品C | 商品2 | 商品3 | |
商品A | 1 | 0.57477089 | 0.58667619 | -0.96447871 | -0.85518385 |
商品B | 0.57477089 | 1 | -0.18283381 | -0.72245596 | -0.56219773 |
商品C | 0.58667619 | -0.18283381 | 1 | -0.35192754 | -0.70337559 |
商品2 | -0.96447871 | -0.72245596 | -0.35192754 | 1 | 0.75766642 |
商品3 | -0.85518385 | -0.56219773 | -0.70337559 | 0.75766642 | 1 |
由此我们可以得到商品A-C与商品2、商品3的相似度:
商品A | 商品B | 商品C | |
商品2 | -0.96447871 | -0.72245596 | -0.35192754 |
商品3 | -0.85518385 | -0.56219773 | -0.70337559 |
我们只需要将用户2对商品2和商品3的评分作为权值对商品A-C的相似度分别进行加权求和,如下所示:
用户2对商品2、3的评分 | 商品A | 商品B | 商品C | |
商品2 | 1.9 | -0.96447871 | -0.72245596 | -0.35192754 |
商品3 | 8.8 | -0.85518385 | -0.56219773 | -0.70337559 |
加权求和 | -9.358127429 | -6.320006348 | -6.8583675180000006 |
可以看出加权求和得到的推荐指数由高到低对应的商品分别为:商品B,商品C,商品A
由此我们便得出了对于用户2,基于商品1的推荐策略为,优先推荐商品B,其次为商品C,最后为商品A
至此,协同过滤算法以基本介绍完毕,下面是本人使用python编写的相关程序:
import numpy as np
from numpy import random as rd
import os
from copy import deepcopy
np.set_printoptions(linewidth=1000, suppress=True)
rd.seed(222)
# '用户-商品'矩阵为用户对商品的评分,0表示用户未购买此商品
class CF_Algorithm(object):
def __init__(self, type, data, u_i_index, k):
"""
:param data:用户商品矩阵,用于计算有用户间相似度或者是物品间相似度,0轴表示用户,1轴表示商品
:param type:指定为"u"表示基于用户的协同过滤算法,指定为"i"表示基于物品的协同过滤算法
:param u_i_index:指定被找最近邻的用户或商品的索引,如果u_i_index为物品索引,则判断是否为用户推荐u_i_index号商品
:param k:相似度前k大的商品或用户
"""
print("总共有%s个用户(用户编号为%s~%s)和%s种商品(商品编号为%s~%s)" % (data.shape[0], 0, data.shape[0] - 1, data.shape[1], 0, data.shape[1] - 1))
print("'用户-商品'矩阵为:\n", data)
self.k = k
self.type = type.lower()
self.u_i_index = u_i_index
assert self.type in ["i", "u"], "协同过滤算法指定类别:u:基于用户,i:基于商品"
self.data = data.astype(np.float64)
self.data_copy = deepcopy(self.data)
if self.type == "u":
self.data = self.data - np.mean(self.data, axis=0, keepdims=True)
else:
self.user_index_recomended = int(input("请输入被推荐商品的用户编号[%s-%s]:" % (0, self.data.shape[0] - 1)))
assert 0 <= self.user_index_recomended <= self.data.shape[0] - 1, "用户编号超出范围"
assert self.data[self.user_index_recomended][self.u_i_index] != 0, "用户%s未购买商品%s,因此不能基于商品%s做推荐" % (self.user_index_recomended, self.u_i_index, self.user_index_recomended)
self.data = self.data - np.mean(self.data, axis=1, keepdims=True)
if self.type == "u":
assert 0 <= self.u_i_index <= (self.data.shape[0] - 1), "指定被找最近邻的用户索引越界, 范围为[%d,%d]" % (0, self.data.shape[0] - 1)
else:
assert 0 <= self.u_i_index <= (self.data.shape[1] - 1), "指定被找最近邻的商品索引越界, 范围为[%d,%d]" % (0, self.data.shape[1] - 1)
def calc_similarity(self):
# 用来计算相似度矩阵
mid_mat = None
if self.type == "u":
mid_mat = np.dot(self.data, self.data.T)
else:
mid_mat = np.dot(self.data.T, self.data)
self.similarity_mat = np.zeros(mid_mat.shape, np.float64)
for i in range(mid_mat.shape[0]):
norm_i = None
if self.type == "u":
norm_i = np.linalg.norm(self.data[i, :])
else:
norm_i = np.linalg.norm(self.data[:, i])
for j in range(i + 1):
norm_j = None
if self.type == "u":
norm_j = np.linalg.norm(self.data[j, :], ord=2)
else:
norm_j = np.linalg.norm(self.data[:, j], ord=2)
self.similarity_mat[i, j] = mid_mat[i, j] / (norm_i * norm_j)
self.similarity_mat = (self.similarity_mat + self.similarity_mat.T) / (np.ones(mid_mat.shape) + np.eye(mid_mat.shape[0]))
if self.type == "i":
print("商品修正余弦相似度矩阵为:\n", self.similarity_mat)
else:
print("用户修正余弦相似度矩阵为:\n", self.similarity_mat)
return self.similarity_mat
def get_k_nearest(self):
sort_index = np.argsort(self.similarity_mat[self.u_i_index, :])[::-1]
self.k_nearest_index = sort_index[:self.k + 1][1:]
if self.type == "u":
print("和%s号用户最相似的前%s个用户的索引为:\n" % (self.u_i_index, self.k), self.k_nearest_index)
else:
print("和%s号商品最相似的前%s个商品的索引为:\n" % (self.u_i_index, self.k), self.k_nearest_index)
# self.k_neareest_score记录了self.user_index_recomended用户对离self.u_i_index索引的物品最近的self.k个物品的评分
self.k_nearest_score = self.data_copy[self.user_index_recomended, self.k_nearest_index]
self.simi = self.similarity_mat[self.u_i_index, self.k_nearest_index]
print("相似度依次为:", self.simi)
def recommend(self, data_new, new_user_score_nearest):
"""
:param data_new: 当self.type=u时为新用户对新物品的评分矩阵,即‘用户-商品’矩阵,当self.type=i时为新商品的‘用户-商品’矩阵
:param new_user_score_nearest: 为新用户对最近的self.k个商品的评分矩阵
:return:
"""
print("新商品的'用户-商品'矩阵(%s个新商品,商品编号为%s~%s):\n" % (data_new.shape[1], 0, data_new.shape[1] - 1), data_new)
if self.type == "u":
good_bool_not_buy = data_new[self.u_i_index] == 0
if not np.any(good_bool_not_buy):
print("无需推荐,所有的新商品%s号用户均已购买评价" % self.u_i_index)
return
good_index_not_buy = np.array(list(range(data_new.shape[1])))[good_bool_not_buy]
nearest_new_good_score = data_new[self.k_nearest_index][:, good_bool_not_buy]
recommend_sort = good_index_not_buy[np.argsort(np.sum(self.simi.reshape(-1, 1) * nearest_new_good_score, axis=0))[::-1]]
print("针对用户%s,推荐指数由高到低的商品编号为:\n" % self.u_i_index, recommend_sort)
else:
data_new_add = np.concatenate([data_new, new_user_score_nearest], axis=1)
data_new_add = data_new_add - np.mean(data_new_add, axis=1, keepdims=True)
print("将与%s号商品最相似的%s个商品的评分追加到新的'用户-商品'矩阵最后得到(商品编号为[%s-%s]):\n" % (self.u_i_index, self.k, 0, data_new.shape[1] + 1), data_new_add)
med_data = np.dot(data_new_add.T, data_new_add)
new_simi_mat = np.zeros(med_data.shape, dtype=np.float)
for i in range(med_data.shape[0]):
for k in range(i + 1):
new_simi_mat[i, k] = med_data[i, k] / (np.linalg.norm(data_new_add[:, i]) * np.linalg.norm(data_new_add[:, k]))
new_simi_mat = (new_simi_mat + new_simi_mat.T) / (np.eye(new_simi_mat.shape[0]) + np.ones(new_simi_mat.shape, dtype=np.float))
print("新商品追加最近的%s个商品的修正余弦相似度矩阵:\n" % self.k, new_simi_mat)
# nearest_simi_with_new_good记录了新商品和最近的k个商品的相似度
nearest_simi_with_new_good = new_simi_mat[-2:, :-2]
print("新商品和最近的%s个商品的相似度:\n" % self.k, nearest_simi_with_new_good)
print("用户%s对最近的%s个商品的评分:\n" % (self.user_index_recomended, self.k), self.k_nearest_score)
recomend_order_big_to_small = np.arange(nearest_simi_with_new_good.shape[1])[np.argsort(np.sum(self.k_nearest_score.reshape(-1, 1) * nearest_simi_with_new_good, axis=0))[::-1]]
print("用户%s基于商品%s对于新商品的推荐指数从高到低依次为:\n" % (self.user_index_recomended, self.u_i_index), recomend_order_big_to_small)
def main():
cf = CF_Algorithm("i", np.around(rd.uniform(1, 11, (5, 5)), 1), 1, 2)
cf.calc_similarity()
cf.get_k_nearest()
cf.recommend(np.around(np.array(rd.randint(0, 11, (3, 3)), dtype=np.float) / rd.uniform(1, 2, (3, 3)), 1), rd.uniform(0, 11, (3, cf.k)))
if __name__ == "__main__":
main()