协同过滤算法

协同过虑概念

协同过滤是什么

协同过滤这个算法,目的就是找相似。
其中:找相似,可以是找相似的人,也可以找相似的东西
比如,我们找到相似的一群人,我们就能用其中一些人喜欢的东西,推荐给另一个人
在这里插入图片描述
找相似的东西的,如果一个人喜欢一样东西,那么我再推荐她另一样东西,因为这两样东西很相似
在这里插入图片描述

协同过滤相似怎么找

我们都知道,坐标里面的两个点,如果他们的夹角越小,那么这两个点越相近
在这里插入图片描述
是不是发现,角度越小,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

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值