如何评估推荐系统的表现?

推荐系统是当前最有价值的机器学习应用之一,据说亚马逊有35%的利润来自于他们的推荐系统。推荐系统的主要功能是发现并推荐用户“可能”会喜欢的商品/物品给用户,要实现这个目的,个性化的推荐系统需要通过分析用户的历史行为数据(如购买、点击、评分等),并从中学习和掌握到了用户的个人偏好(喜好),那么当下次再遇到用户的时候,就可以有的放矢的做个性化推荐了。我之前写过几篇推荐系统的文章,其内容都是教大家如何来开发一个个性化的推荐系统.今天我想给大家介绍一下当推荐系统开发完成以后,我们如何来评估它的表现。

数据

你可以在这里下载豆瓣网的电影评价数据,我们将使用由Nicolas Hug创建的Surprise库,来评估我们的推荐系统。首先加载我们所需要的库,然后整理一下我们的数据。

import pandas as pd
import numpy as np 
from surprise import SVD
from surprise import KNNBaseline
from surprise.model_selection import train_test_split
from surprise.model_selection import LeaveOneOut
from surprise import Reader
from surprise import Dataset
from surprise import accuracy
from surprise.model_selection import train_test_split
from collections import defaultdict
movies = pd.read_csv( './data/douban/movies.csv')
ratings = pd.read_csv('./data/douban/ratings.csv')

combine_movie_rating= pd.merge(ratings,movies,on='movieId',how='inner')
combine_movie_rating=combine_movie_rating.drop(['timestamp'],axis = 1)
combine_movie_rating = combine_movie_rating.dropna(axis = 0 ,subset=['title'])
print('评分数量:%d' % len(combine_movie_rating))
combine_movie_rating.sample(10)

我们将movies和ratings这两个表进行了合并,并且删除了那些没有电影名称(title)的数据,最后剩下260万左右的评价数据,由于数据量过于庞大,会使计算非常缓慢,因此我们要缩小数据规模,我们要过滤掉那些冷门的电影评分数据,只保留热门电影的评分数据,那如何定义热门电影呢?我们可以认为评分次数越高的电影就应该越热门,所以可以利用分位数统计方法查看热门电影的评分次数的分布,我们只需要取分位数统计中位于顶层的那1%至10%以内的数据就可以了,下面我们来演示一下怎么过滤出热门电影:

movie_rating_count=pd.DataFrame(combine_movie_rating.
                    groupby(['movieId'])['rating'].
                    count().
                    reset_index().
                    rename(columns={'rating':'totalRatingCount'})                   
                   )
rating_with_totalRatingCount = combine_movie_rating.merge(movie_rating_count,left_on='movieId',right_on='movieId')
rating_with_totalRatingCount.head()

 

看了上述代码大家应该知道,我们首先要分组统计出每部电影的评价次数的总和,然后将它与combine_movie_rating合并,就得到了上诉结果。然后我们进行顶层的10%以内的分位数统计:

从以上统计结果中,我们可以看到有10%的数据评价次数大于2351次,有9%的数据评价次数大于2441,有8%的数据评价次数大于2654,因为评价数据的总量为260多万,10%的化应该也有26万左右,那么我们可以暂定评价次数大于2351次的电影作为热门电影,数据量应该在26万左右,这样我们就扔掉了90%的数据,得到了最热门的电影评价数据集。

#取10%的最热门的电影
popular_threshold=2351 
rating_with_totalRatingCount = combine_movie_rating.merge(movie_rating_count,left_on='movieId',right_on='movieId')
popular_movies_rating= rating_with_totalRatingCount.query('totalRatingCount>=@popular_threshold')
print('热门电影数据量:%d' % len(popular_movies_rating))

 

接下来我们使用Surprise的SVD算法对用户的评分进行预测:

扫描二维码关注公众号,回复: 11312659 查看本文章

评分预测

正如我前几篇推荐系统的文章中所提到的,有时候我们在开发推荐系统的时候需要创建一个“用户-电影-评价”的矩阵,用户不可能对没有看过的电影进行评价,也有可能对看过的电影不做评价。因此我们所创建的“用户-电影-评价”矩阵将是一个非常稀疏的矩阵,里面充斥着大量的0元素,如下图所示:

目前大多数的推荐系统,都会去预测用户对那些他们没有看过的电影的可能评分, 即要将上图中的0元素替换成系统预测的分数。 如果存在太多的“0”元素,那么推荐系统将没有足够的数据来猜测用户到底喜欢看什么电影。相反如果我们有足够多的评分数据,那么对剩下的那些用户没有评分过的电影就可以非常容易的来预测它们的评分。下面我们就用Surprise库的SVD算法,来预测用户对电影的评分。并且我们使用RMSE(均方根误差)和MAE(平均绝对误差)这两个指标来评估我们的预测结果。

reader = Reader(rating_scale=(0.5, 5))
data = Dataset.load_from_df(combine_movie_rating[['userId', 'movieId', 'rating']], reader)
train, test = train_test_split(data, test_size=.25, random_state=0)
svd_model = SVD(random_state=0)
svd_model.fit(trainSet)
predict = svd_model.test(test)   
print("RMSE: ",accuracy.mae(predict, verbose=False))
print("MAE: ",accuracy.rmse(predict, verbose=False))
predict[:10]

可以看到我们的RMSE为0.5769,MAE为0.7324,在predict的结果中uid表示用户id,iid表示电影id,r_ui表示用户对电影的实际评分,而est表示SVD算法预测的评分。

TOP-N

从在线购物网站到视频门户网站,TOP-N的推荐系统无处不在。 TOP-N推荐为用户提供他们可能感兴趣的N个商品的排名列表,以鼓励观看和购买。

“Top-N”推荐是亚马逊的推荐系统最重要的功能之一,它可以为个人用户提供最佳结果列表,如下图所示。

亚马逊的“Top-N”推荐包括8页,第一页有7个物品。 一个好的推荐系统应该能够识别某个用户感兴趣的一组N个物品。 因为我很少在亚马逊购买书籍,所以亚马逊网站似乎没有我的评价信息,他们给我推荐的仅仅只是当前的热销商品的TOP-N列表,可想而知,如果你没有在购物网站上留下任何信息,他们也无法给你做个性化的推荐。

下面我们每位用户创建一个推荐10部电影的TOP-10列表。我们首先定义一个get_top_n的函数,以便从预测的结果集中抽取10个评分最高的电影

def get_top_n(predictions, n=10):
    top_n = defaultdict(list)
    for uid, iid, true_r, est, _ in predictions:
        top_n[uid].append((iid, est))

    for uid, user_ratings in top_n.items():
        user_ratings.sort(key=lambda x: x[1], reverse=True)
        top_n[uid] = user_ratings[:n]

    return top_n

 下面我们要使用"留一法"(LeaveOneOut)交叉验证的方法来生成训练集和测试集,在测试集中我们为每个用户只保留一条评分数据(请记住训练集中只保留每个用户的一条评分记录),将剩下的评分数据创建为训练集。

#定义留一法交叉验证,在测试集中每个用户只保留一条评分记录
LOOCV = LeaveOneOut(n_splits=1, random_state=1)

for trainSet, testSet in LOOCV.split(data):
    
    #在训练集上训练模型
    svd_model.fit(trainSet)
    #在测试集上预测
    leftOutPredictions = svd_model.test(testSet)
    
    #从训练集中创建一个测试集,该测试集中包含了所有用户没有看过的电影,即该测试集中的数据不在训练集中
    bigTestSet = trainSet.build_anti_testset()
    
    #得到所有用户没有看过的所有电影的预测评分
    allPredictions = svd_model.test(bigTestSet)
    
    #从每个用户的未看过的电影的预测评分中抽取前10个得分最高的电影
    topNPredicted = get_top_n(allPredictions, n=10)

#打印为每个用户推荐的10部电影和对它们的评分
for uid, user_ratings in topNPredicted.items():
      print(uid, [(iid,round(rating,1)) for (iid, rating) in user_ratings])

 返回的结果中第一列为用户ID,后面的list中包含了推荐给用户的他们没有看过的10部电影的id和对该电影id的预测评分值。(从高到低排序输出),现在虽然有了TOP-N的推荐结果,但是TOP-N的推荐效果好不好,我们有没有办法来评估TOP-N的推荐效果?不用担心,我们可以使用“命中率”来评估我们的TOP-N的推荐效果。

 命中率

让我们看看我们的TOP-N推荐的效果怎么样。 为了评估TOP-10,我们使用命中率,也就是说,如果TOP-10推荐的结果中包含了我们之前在测试集上movieId的话,那么我们认为它被“命中”。

单个用户的计算命中率的步骤

  • 在评分数据中查找此用户的所有评分记录
  • 删除此用户的一条评分记录(将这条记录放到测试集中,LeaveOneOut 交叉验证)
  • 将此用户的剩余评分记录“喂”给模型进行训练。最后经过预测,我们得到TOP-10的推荐结果
  • 如果前面删除的那条评分记录出现在TOP-10的推荐结果里,那么我们就认为它被“命中”,反之则没有被“命中”。

整体命中率

def HitRate(topNPredicted, leftOutPredictions):
    hits = 0
    total = 0
 
    for leftOut in leftOutPredictions:
        userID = leftOut[0]
        leftOutMovieID = leftOut[1]
        
        hit = False
        for movieID, predictedRating in topNPredicted[int(userID)]:
            if (int(leftOutMovieID) == int(movieID)):
                hit = True
                break
        if (hit) :
            hits += 1

        total += 1

    return hits/total
print("整体命中率: ", HitRate(topNPredicted, leftOutPredictions))

系统整体的命中率就是所有用户的命中次数的总和除以总的用户数,它可以衡量那些被我们删除的用户评价过的电影(保留在测试集中)它们被推荐的可能性。当然这个指标越高越好。

命中率非常低意味着我们没有足够的数据可供使用。 就像亚马逊对我的命中率非常低,因为它没有足够的我的图书购买数据。

评分的命中率

所谓评分的命中率是指我们可以统计出每种评分值的命中率,比如1分的命中率,2分的命中率等等, 理想情况下,我们希望预测用户喜欢的电影,因此我们更关注高评分值的命中率(如4分,5分的命中率),而忽略低评分值的命中率。

def RatingHitRate(topNPredicted, leftOutPredictions):
    hits = defaultdict(float)
    total = defaultdict(float)    
    for userID, leftOutMovieID, actualRating, estimatedRating, _ in leftOutPredictions:
        
        hit = False
        for movieID, predictedRating in topNPredicted[int(userID)]:
            if (int(leftOutMovieID) == movieID):
                hit = True
                break
        if (hit) :
            hits[actualRating] += 1
        total[actualRating] += 1

    for rating in sorted(hits.keys()):
        print(rating, hits[rating] / total[rating])
print("评分的命中率: ")
RatingHitRate(topNPredicted, leftOutPredictions)

从上述结果来看,4分,5分的命中率要高于1分,2分,3分的命中率. 这正是我们所需要的。

累积命中率

因为我们关心更高的评分(4分,5分),所以我们可以忽略低于4分的预测评分,只计算评分> = 4的命中率。

def CumulativeHitRate(topNPredicted, leftOutPredictions, ratingCutoff=0):
    hits = 0
    total = 0

    for userID, leftOutMovieID, actualRating, estimatedRating, _ in leftOutPredictions:

        if (actualRating >= ratingCutoff):            
            hit = False
            for movieID, predictedRating in topNPredicted[int(userID)]:
                if (int(leftOutMovieID) == movieID):
                    hit = True
                    break
            if (hit) :
                hits += 1
            total += 1
    return hits/total
print("累积命中率 (rating >= 4): ", CumulativeHitRate(topNPredicted, leftOutPredictions, 4.0))

 我们的rating>=4的累积命中率是0.219,这比之前的4分的命中率0.163要高,但却低于之前5分的命中率0.253,这是为什么?大家可以想一想。

平均互惠命中排名(ARHR)

常用的用于衡量被命中的物品在TOP-N推荐结果中的位置排名,如被命中的电影在TOP-N推荐结果中排在前几位(如第1,第2位等),那就要比排最后几位(如第9,第10位)效果要好,被命中的电影在TOP-N推荐的结果中排名越靠前,TOP-N推荐的表现就越好。

def AverageReciprocalHitRank(topNPredicted, leftOutPredictions):
    summation = 0
    total = 0
        
    for userID, leftOutMovieID, actualRating, estimatedRating, _ in leftOutPredictions:
        
        hitRank = 0
        rank = 0
        for movieID, predictedRating in topNPredicted[int(userID)]:
            rank = rank + 1
            if (int(leftOutMovieID) == movieID):
                hitRank = rank
                break
        if (hitRank > 0) :
                summation += 1.0 / hitRank

        total += 1

    return summation / total

print("平均互惠命中排名: ", AverageReciprocalHitRank(topNPredicted, leftOutPredictions))

您开发的第一个推荐系统它的表现可能并不会非常的好,其中原因有很多,比如新的系统没有足够的用户数据,或者根本没有用户数据,或者面对一个新用户,我们无法对他做出个性化的推荐。但是不要气馁,随着时间的推移和用户数据的不断累计,您会发现我们的推荐系统会变得越来越聪明,越来越智能,那一天一定会到了的,亚马逊的今天也许就是你的明天。

完整代码在此下载

猜你喜欢

转载自blog.csdn.net/weixin_42608414/article/details/88294156
今日推荐