协同过虑概念
协同过滤是什么
协同过滤这个算法,目的就是找相似。
其中:找相似,可以是找相似的人,也可以找相似的东西
比如,我们找到相似的一群人,我们就能用其中一些人喜欢的东西,推荐给另一个人
找相似的东西的,如果一个人喜欢一样东西,那么我再推荐她另一样东西,因为这两样东西很相似
协同过滤相似怎么找
我们都知道,坐标里面的两个点,如果他们的夹角越小,那么这两个点越相近
是不是发现,角度越小,cos𝜽就越接近1,角度越大,cos𝜽就越接近-1。
那找相似我们就能直接用cos𝜽的大小来描述啦
但是现实生活的例子是很复杂的呀,比如突然从两维变成三维或者多维的话,算cos𝜽怎么办?
下面引出相似度的计算公式,我们举个例子,具体讲下这个公式含义
根据我们之前说的越相似,cos𝜽越接近于1来看。你说,A点和B点,算是接近还是不接近呢?
协同过滤举例
找相似的人user-based
小美一直喜欢在网上买化妆品,今天晚上,她又打开了常去的网站,逛逛。
这时候,网站的主页上,正好有一个定制化推荐广告位,需要给小美推荐一个美妆产品
小美呢,以前在这个网站上买过口红,眼影和香水,还给她们评价給过分。除了小美的购买的记录,我们还知道,其他人的购买记录和评分,比如小丽,小红,小花。我们是不是能够利用其他人的信息,来找到小美最可能买的东西,推荐给她呢?这样问题变得很简单了,只要用我们之前说的,找小美和其他所有人之间的cos𝜽,然后挑几个最接近她的人,看她们买过什么,给的评价怎么样,然后推荐给小美,就行了
首先第一步,我们先产生一个表格,竖着的是人,横着的是化妆品,里面内容是她们给的化妆品评分
第二步,没有评分的化妆品,我们假设这些人都打0分。这样4个小女孩,就变成了之前公式里的4个点。
小美(4,0,0,5,1,0,0)
小丽(5,5,4,0,0,0,0)
小红(0,0,0,2,4,5,0)
小花(0,3,0,0,0,0,3)
根据前面的公式,我们算出来
相似度(小美,小丽) = 0.38
相似度(小美,小红) = 0.32
相似度(小美,小花) = 0
这样看下来,小美和小丽是相对比较相似的,然后是小红
通常情况下,我们一般会挑和小美1~3(N)个的相似的人,看他们的评价,综合算出最后推荐给目标人群的产品
这个例子里面,我们挑2个人,小丽和小红,来综合算出推荐给小美的东西。
由于7样产品里面,小美买过3样,有4样还没有买过和评价过,所以,我们只要知道,
另外4样小美可能会评价的分数,那么我们就可以挑最高的推荐给她。
利用小丽,和小红的评价分数和她们与小美相似度,我们就能推算出小美的评价分数.
小美会给粉饼的打分是 = (5 *0.38 + 0 * 0.32)/(0.38+0) = 5
小美会给眼线笔的打分是 = (4 * 0.38 + 0 * 0.32)/(0.38+0) = 4.8
小美会给润肤露的打分是 = (0 * 0.38+5 * 0.32)/(0+0.32) = 5
这样粉饼和润肤露都是5,但由于小丽和小美的相似度最高,她给粉饼是5分,所以我们推荐粉饼给小红
打0分合理吗?
谁说,她们一定都会给那些没有买过、评价过的化妆品,打0分呀。
如果一个人以前给2个产品分别打过4分,2分。
然后,我们取一个平均值:3分。把这个3分,作为没买过东西的可能打分,是不是更合理呢?
对的,完全正确。这个方法在统计里面我们叫做标准化。标准化,不仅解决之前说的不合理性,还能解决每个人的评价标准差异。比如,有人自认为5分就是她的最高分,而有些人比较严格觉得3分就是她认为的最高分。但是,其实3分,5分,都是这些人心目中的最高分。标准化,就解决了这个问题
我们回到之前的表格,在没有评分的地方,我们算出平均值填进去。比如小美的平均值就是(4+5+1)/3 = 10/3; 小丽是(5+5+4)/3 =14/3
为了达到标准化每个人的评价要求差异,我们将每个人的平均值就变成以0为中心,打分低于平均分的是负的数,高于平均分为正数
这样我们有了标准化的打分表,重新算一遍小美和其他人的相似度,取和她最相似的人,综合平均她们平均打过的分数,选出小美可能会打分最高的推荐给她。
找相似的物item-based
上面那个例子就是找相似的人,现在我们说下找相似的东西
有一天,小美又上网闲逛了,她买过也评分过润肤露,眼影,香水,睫毛膏,这次只剩下口红和粉饼没有买,那下一个该推荐给她口红呢,还是粉饼呢?和人人相似一样,物物相似的表格变成了,竖过来的是化妆品,横过来的是人
根据之前的顺序,我们算出化妆品之间的相似性,步骤也是先填平均值->标准化->算cos相似度值->挑出几个和目标物体相似的物体-> 结合物品打分和物品相似值,综合打分->挑出分数高的推荐给小美。
这样,我们算出,和口红最接近的是润肤露和睫毛膏;和粉饼最接近的是眼影和香水。根据小美打过其他产品的分数,以及这些产品和口红、粉饼相似度,
我们算出:
小美给口红打分:(0.41 * 2 + 0.59 * 3)/(0.41+0.59) = 2.6
小美给粉饼打分:(0.47 * 5 + 0.40 * 4)/(0.47+0.40) = 4.5
粉饼大于口红,我们推荐粉饼给小美
人人相似和物物相似的推荐哪个更好
现实生活中,其实两个都有在用。但是物物相似更常用一点。因为每个人都有自己的喜好,用的不当心,容易产生反感。
比如一个男孩子买了啤酒,薯片。一个女孩子买了口红,啤酒,薯片。根据计算,男孩和女孩很相似,然后推荐口红给男孩。看起来,是不是很奇怪呀?但是如果你规则定的好,人人相似,也会有很好的效果。
实验
电影推荐的协同过滤算法。
数据集采用 http://files.grouplens.org/datasets/movielens/ml-100k.zip。
数据量大概为 100000 rows × 4 columns
```python
#%%
import numpy as np
import pandas as pd
#%% md
# 读取数据:u.data每行数据分为userid,itemid,rating,时间戳四部分#
#%%
names = ['user_id', 'item_id', 'rating', 'timestamp']
df = pd.read_csv('./ml-100k/u.data', sep='\t', names=names)
df.head()
#%% md
# 统计文件中用户总数与电影总数
#%%
n_users = df.user_id.unique().shape[0]
n_items = df.item_id.unique().shape[0]
print (str(n_users) + ' users')
print (str(n_items) + ' items')
#%% md
# 构造 用户-电影评分矩阵
#%%
ratings = np.zeros((n_users, n_items))
for row in df.itertuples():
ratings[row[1]-1, row[2]-1] = row[3]
ratings
#%% md
# 计算数据稀疏度
#%%
sparsity = float(len(ratings.nonzero()[0]))
sparsity /= (ratings.shape[0] * ratings.shape[1])
sparsity *= 100
print ('Sparsity: {:4.2f}%'.format(sparsity))
#%% md
# 数据稀疏度为6.3%,943个user,1682个item,
# 每个用户平均需要做出100条评论,随机抽取10%数据,
# 将数据分为训练集与测试机两部分
#%%
def train_test_split(ratings):
test = np.zeros(ratings.shape)
train = ratings.copy()
for user in range(ratings.shape[0]):
test_ratings = np.random.choice(ratings[user, :].nonzero()[0],
size=10,
replace=False)
train[user, test_ratings] = 0.
test[user, test_ratings] = ratings[user, test_ratings]
# Test and training are truly disjoint
assert(np.all((train * test) == 0))
return train, test
#%%
train, test = train_test_split(ratings)
#%% md
# 计算user或item的余弦相似性可以用代码通过for循环实现,但是这样Python代码会运行非常慢,这里可以使用NumPy的科学计算函数来表达方程式,提高计算速度
#%%
def slow_similarity(ratings, kind='user'):
if kind == 'user':
axmax = 0
axmin = 1
elif kind == 'item':
axmax = 1
axmin = 0
sim = np.zeros((ratings.shape[axmax], ratings.shape[axmax]))
for u in range(ratings.shape[axmax]):
for uprime in range(ratings.shape[axmax]):
rui_sqrd = 0.
ruprimei_sqrd = 0.
for i in range(ratings.shape[axmin]):
sim[u, uprime] = ratings[u, i] * ratings[uprime, i]
rui_sqrd += ratings[u, i] ** 2
ruprimei_sqrd += ratings[uprime, i] ** 2
sim[u, uprime] /= rui_sqrd * ruprimei_sqrd
return sim
def fast_similarity(ratings, kind='user', epsilon=1e-9):
# epsilon -> small number for handling dived-by-zero errors
if kind == 'user':
sim = ratings.dot(ratings.T) + epsilon
elif kind == 'item':
sim = ratings.T.dot(ratings) + epsilon
norms = np.array([np.sqrt(np.diagonal(sim))])
return (sim / norms / norms.T)
#%%
%timeit fast_similarity(train, kind='user')
#%% md
# 分别计算user相似性和item相似性,并输出item相似性矩阵的前4行
#%%
user_similarity = fast_similarity(train, kind='user')
item_similarity = fast_similarity(train, kind='item')
item_similarity
#%% md
# 预测评分,predict_fast_simple使用NumPy数学函数,计算更块
#%%
def predict_slow_simple(ratings, similarity, kind='user'):
pred = np.zeros(ratings.shape)
if kind == 'user':
for i in range(ratings.shape[0]):
for j in range(ratings.shape[1]):
pred[i, j] = similarity[i, :].dot(ratings[:, j])\
/np.sum(np.abs(similarity[i, :]))
return pred
elif kind == 'item':
for i in range(ratings.shape[0]):
for j in range(ratings.shape[1]):
pred[i, j] = similarity[j, :].dot(ratings[i, :].T)\
/np.sum(np.abs(similarity[j, :]))
return pred
def predict_fast_simple(ratings, similarity, kind='user'):
if kind == 'user':
return similarity.dot(ratings) / np.array([np.abs(similarity).sum(axis=1)]).T
elif kind == 'item':
return ratings.dot(similarity) / np.array([np.abs(similarity).sum(axis=1)])
#%%
%timeit predict_slow_simple(train, user_similarity, kind='user')
#%%
%timeit predict_fast_simple(train, user_similarity, kind='user')
#%% md
# 使用sklearn计算MSE,首先去除数据矩阵中的无效0值,然后直接调用sklearn里面的mean_squared_error函数计算MSE
#%%
from sklearn.metrics import mean_squared_error
def get_mse(pred, actual):
# Ignore nonzero terms.
pred = pred[actual.nonzero()].flatten()
actual = actual[actual.nonzero()].flatten()
return mean_squared_error(pred, actual)
#%%
item_prediction = predict_fast_simple(train, item_similarity, kind='item')
user_prediction = predict_fast_simple(train, user_similarity, kind='user')
print ('User-based CF MSE: ' + str(get_mse(user_prediction, test)))
print ('Item-based CF MSE: ' + str(get_mse(item_prediction, test)))
#%% md
# 为提高预测的MSE,可以只考虑使用与目标用户最相似的k个用户的数据,进行Top-k预测并进行MSE计算
#%%
def predict_topk(ratings, similarity, kind='user', k=40):
pred = np.zeros(ratings.shape)
if kind == 'user':
for i in range(ratings.shape[0]):
top_k_users = [np.argsort(similarity[:,i])[:-k-1:-1]]
for j in range(ratings.shape[1]):
pred[i, j] = similarity[i, :][top_k_users].dot(ratings[:, j][top_k_users])
pred[i, j] /= np.sum(np.abs(similarity[i, :][top_k_users]))
if kind == 'item':
for j in range(ratings.shape[1]):
top_k_items = [np.argsort(similarity[:,j])[:-k-1:-1]]
for i in range(ratings.shape[0]):
pred[i, j] = similarity[j, :][top_k_items].dot(ratings[i, :][top_k_items].T)
pred[i, j] /= np.sum(np.abs(similarity[j, :][top_k_items]))
return pred
#%%
pred = predict_topk(train, user_similarity, kind='user', k=40)
print ('Top-k User-based CF MSE: ' + str(get_mse(pred, test)))
pred = predict_topk(train, item_similarity, kind='item', k=40)
print ('Top-k Item-based CF MSE: ' + str(get_mse(pred, test)))
#%% md
# 为进一步降低MSE,这里尝试使用不同的k值寻找最小的MSE,使用matplotlib 可视化输出结果
#%%
k_array = [5, 15, 30, 50, 100, 200]
user_train_mse = []
user_test_mse = []
item_test_mse = []
item_train_mse = []
def get_mse(pred, actual):
pred = pred[actual.nonzero()].flatten()
actual = actual[actual.nonzero()].flatten()
return mean_squared_error(pred, actual)
for k in k_array:
user_pred = predict_topk(train, user_similarity, kind='user', k=k)
item_pred = predict_topk(train, item_similarity, kind='item', k=k)
user_train_mse += [get_mse(user_pred, train)]
user_test_mse += [get_mse(user_pred, test)]
item_train_mse += [get_mse(item_pred, train)]
item_test_mse += [get_mse(item_pred, test)]
#%%
%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns
sns.set()
pal = sns.color_palette("Set2", 2)
plt.figure(figsize=(8, 8))
plt.plot(k_array, user_train_mse, c=pal[0], label='User-based train', alpha=0.5, linewidth=5)
plt.plot(k_array, user_test_mse, c=pal[0], label='User-based test', linewidth=5)
plt.plot(k_array, item_train_mse, c=pal[1], label='Item-based train', alpha=0.5, linewidth=5)
plt.plot(k_array, item_test_mse, c=pal[1], label='Item-based test', linewidth=5)
plt.legend(loc='best', fontsize=20)
plt.xticks(fontsize=16);
plt.yticks(fontsize=16);
plt.xlabel('k', fontsize=30);
plt.ylabel('MSE', fontsize=30);
#%% md
# 从图中可以看出,在测试数据集中,k为15和50时分别产生一个最小值对基于用户和基于项目的协同过滤
#%%
def predict_nobias(ratings, similarity, kind='user'):
if kind == 'user':
user_bias = ratings.mean(axis=1)
ratings = (ratings - user_bias[:, np.newaxis]).copy()
pred = similarity.dot(ratings) / np.array([np.abs(similarity).sum(axis=1)]).T
pred += user_bias[:, np.newaxis]
elif kind == 'item':
item_bias = ratings.mean(axis=0)
ratings = (ratings - item_bias[np.newaxis, :]).copy()
pred = ratings.dot(similarity) / np.array([np.abs(similarity).sum(axis=1)])
pred += item_bias[np.newaxis, :]
return pred
#%%
user_pred = predict_nobias(train, user_similarity, kind='user')
print ('Bias-subtracted User-based CF MSE: ' + str(get_mse(user_pred, test)))
item_pred = predict_nobias(train, item_similarity, kind='item')
print ('Bias-subtracted Item-based CF MSE: ' + str(get_mse(item_pred, test)))
#%% md
# 将Top-k和偏置消除算法结合起来,计算基于User的和基于Item的MSE,并分别取k=5,15,30,50,100,200,将计算的MSE结果运用matplotlib 可视化输出
#%%
def predict_topk_nobias(ratings, similarity, kind='user', k=40):
pred = np.zeros(ratings.shape)
if kind == 'user':
user_bias = ratings.mean(axis=1)
ratings = (ratings - user_bias[:, np.newaxis]).copy()
for i in range(ratings.shape[0]):
top_k_users = [np.argsort(similarity[:,i])[:-k-1:-1]]
for j in range(ratings.shape[1]):
pred[i, j] = similarity[i, :][top_k_users].dot(ratings[:, j][top_k_users])
pred[i, j] /= np.sum(np.abs(similarity[i, :][top_k_users]))
pred += user_bias[:, np.newaxis]
if kind == 'item':
item_bias = ratings.mean(axis=0)
ratings = (ratings - item_bias[np.newaxis, :]).copy()
for j in range(ratings.shape[1]):
top_k_items = [np.argsort(similarity[:,j])[:-k-1:-1]]
for i in range(ratings.shape[0]):
pred[i, j] = similarity[j, :][top_k_items].dot(ratings[i, :][top_k_items].T)
pred[i, j] /= np.sum(np.abs(similarity[j, :][top_k_items]))
pred += item_bias[np.newaxis, :]
return pred
#%%
k_array = [5, 15, 30, 50, 100, 200]
user_train_mse = []
user_test_mse = []
item_test_mse = []
item_train_mse = []
for k in k_array:
user_pred = predict_topk_nobias(train, user_similarity, kind='user', k=k)
item_pred = predict_topk_nobias(train, item_similarity, kind='item', k=k)
user_train_mse += [get_mse(user_pred, train)]
user_test_mse += [get_mse(user_pred, test)]
item_train_mse += [get_mse(item_pred, train)]
item_test_mse += [get_mse(item_pred, test)]
#%%
pal = sns.color_palette("Set2", 2)
plt.figure(figsize=(8, 8))
plt.plot(k_array, user_train_mse, c=pal[0], label='User-based train', alpha=0.5, linewidth=5)
plt.plot(k_array, user_test_mse, c=pal[0], label='User-based test', linewidth=5)
plt.plot(k_array, item_train_mse, c=pal[1], label='Item-based train', alpha=0.5, linewidth=5)
plt.plot(k_array, item_test_mse, c=pal[1], label='Item-based test', linewidth=5)
plt.legend(loc='best', fontsize=20)
plt.xticks(fontsize=16);
plt.yticks(fontsize=16);
plt.xlabel('k', fontsize=30);
plt.ylabel('MSE', fontsize=30);
user_id item_id rating timestamp
0 196 242 3 881250949
1 186 302 3 891717742
2 22 377 1 878887116
3 244 51 2 880606923
4 166 346 1 886397596
将Top-k和偏置消除算法结合起来,计算基于User的和基于Item的MSE,并分别取k=5,15,30,50,100,200,
从图中可以看出,在测试数据集中,k为15和50时分别产生一个最小值对基于用户和基于项目的协同过滤
参考文章
[1]: https://www.jianshu.com/p/3961e3c51047