数据分析案例之电影推荐

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/a_step_further/article/details/79875363

前言

通过案例来学习数据分析的思路和练习相应分析工具,往往最有效的。本文用julia来进行全流程的探索和分析,以期达到既掌握分析思路,又练习了这一新兴的数据科学利器。同时,个性化推荐是个太大的topic,涉及的理论方法和实践非常多,本文有些地方会详细展开,有些则一笔带过。

如无特殊说明,本文中所使用的code均为julia代码,IDE环境为JuliaPro.

问题阐述

个性化推荐是当今网络世界上普遍存在的一种大数据服务,视频、音乐、读书、新闻、购物等领域均流行此类服务。本文面对的问题是基于海量用户在MovieLens网站上对不同电影进行的评分数据,来为用户进行电影推荐。



数据准备

本文使用的数据为明尼苏达大学的 [MovieLens],我们下载其中的1M大小版本的数据集,这个数据集存储的是:

100,000 ratings and 1,300 tag applications applied to 9,000 movies by 700 users. Last updated 10/2016. Users were selected at random for inclusion. All selected users had rated at least 20 movies. No demographic information is included. Each user is represented by an id, and no other information is provided.

看起来是把用户在 MovieLens网站(感兴趣的话可点击 [movielens.org]]了解)上对电影的评价记录做了抽样采集,通过readme文件可以了解数据集的详细信息。然后即可导入数据,并查看一些简单的信息:

using DataFrames
 
 #为了更方便地对不同数据集进行数据探索,定义一个函数
 function get_info(dataset)
     println("数据集大小为: ", size(dataset))
     println("数据预览: ", head(dataset))
     println("字段信息:")
     showcols(dataset)
 end
 
 #数据导入
 movies = readtable("~/ml-latest-small/movies.csv",header=true)
 ratings = readtable("~/ml-latest-small/ratings.csv", header=true)
 tags = readtable("~/ml-latest-small/tags.csv",header=true)
 
 #获取数据信息
 get_info(movies)
 get_info(ratings)
 get_info(tags)

分析思路

电影推荐是一种典型的个性化推荐场景,相关的方法有很多。
1. 基于用户的协同过滤,潜在假设是相似用户对于同类电影可能会有相似的偏好程度,人以类聚嘛,兴趣的相似在现实社会中确实是普遍存在的;
2. 基于物品的协同过滤,利用物品属性的相似性进行推荐;
3. 基于社交网络的推荐,即我们往往倾向于会去看一些朋友们都在看的电影,目的之一就是与朋友们保持有可以聊天的材料,即使我们自身并非特别喜欢这些电影。像微信“看一看”功能中给新闻打上的标签“朋友们都在看”,我理解这种既有基于用户协同过滤又有社交网络推荐的味道,因为微信好友并不一定是基于兴趣建立的好友关系。但是除了自带社交属性的应用,一般的网络服务不存在用户社交关系的数据;

4. 隐语义模型,主要采用SVD矩阵分解为手段。

本文选择基于用户的协同过滤来进行练习。为了进行电影推荐,我们本质上是要计算一个用户对电影的偏好程度,即得到一个 (user, movie, preference),这里的movie需要是用户未评价过的,那么preference就是一种预测值。用数学一点的表达就是我们其实是需要做下面这个矩阵的空白值填充(下面数据探索部分会提到,对于本文所使用的MovieLens数据集而言,该矩阵的空值率为98.4%,所以是一个非常稀疏的矩阵)。对于每个用户,如果某个电影的评分预测值较高,那么就是值得推荐的。

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


无论是基于用户还是基于物品,我们本质上都需要计算不同物体之间的一个相似度,而计算相似度的指标就太多了。例如比较常用的Jaccard距离、余弦相似度、欧式距离、pearson相关系数等。在计算相似度时,往往最终的结果是形成(item1, item2, similarity), 那么需要对物品进行两两组合计算,如果暴力轮询,在item数量为N的情况下,计算复杂度就是O(|N|^2),  所以计算距离时,是需要一点小trick的。

数据探索

我们先来看一下本文涉及到的数据集中,用户对电影的评价数据是怎样记录的,
ratings = readtable("~/ratings.csv", header=true) 
head(ratings) 

数据的每一行代表一个用户对某部电影进行的一次评分记录,字段分别是 用户ID,电影ID, 评分,时间。这里的评分是从0-5星的数值,以0.5为步长 。


#了解数据
 println("用户数共计: ", length(unique(ratings[:userId])))
 println("电影数共计: ", length(unique(ratings[:movieId])))
 stat = @by(ratings, :userId, movieNum = length(unique(:movieId)), movieAvgRating = mean(:rating))
 using Gadfly
 sort!(stat,cols=order(:movieNum),rev=true)
 plot(stat, x=:userId, y=:movieNum, Geom.bar, Guide.XLabel("用户id"),Guide.YLabel("评价电影数"))
 describe(stat[:movieNum])

这里我们看到有671个用户,如果计算两两的相似度,关系对总数为45万个。电影数量为9066,如果每个用户对每个电影都有过评分的话,那么用户-电影这个二维矩阵的大小就应该是 671\*9066 = 6083286, 而现实中ratings数据集的大小仅为100004,那么空值率为98.4%! 可见这是一个多么稀疏的矩阵啊。

基于用户的协同过滤

对于给定的user, movie, 我们的目标是求得 f(user, movie, score) ,设movie被user1, user2 … userN 有评价过,评分分别为score1, score2 … scoreN,  那么我们基于用户之间的相似度可以得到如下这样一个加权的预测评分:
f(user, movie, score) = (similarity(user, user1)*score1 + similarity(user, user1)*score2 + … +  similarity(user, userN)*scoreN)/(similarity(user, user1)+ similarity(user, user2)+…+ similarity(user, userN))

我们先来做一些基础的工作。首先将每个用户评分过的电影保存到字典的数据结构中:

#定义一个函数来获得每个用户对所有电影的评分,保存到字典结构
 function getUserRatedMovie(userId)
     userRatedMovie = Dict()
     curUserMovieRatings = ratings[ratings[:userId] .== userId,:]
     for i in 1:nrow(curUserMovieRatings)
         m = curUserMovieRatings[i,:movieId]
         r = curUserMovieRatings[i,:rating]
         userRatedMovie[m] = r
     end
     return userRatedMovie
 end
 
 #遍历所有用户id, 将所有用户的电影评分数据也保存到字典结构中
 userRatedMovieDict = Dict()
 for u in unique(ratings[:userId])
     userRatedMovieDict[u] = getUserRatedMovie(u)
 end

接下来计算两个用户之间的相似度,本文采用基于欧式距离来计算,计算方法为:

 #计算两个用户之间的相似度,基于欧式距离
 function getSimilarity(user1, user2)
     if user1 == user2
         return 1.0
     end
     similarity = 0.0
     #找到user1, user2共同评价过的电影集
     movieSet1 = Set([u for u in keys(userRatedMovieDict[user1])])
     movieSet2 = Set([u for u in keys(userRatedMovieDict[user2])])
     commonMovie = intersect(movieSet1, movieSet2)
     # println("共同评价的电影集合大小:", length(commonMovie))
     if length(commonMovie) == 0
         return 0.0
     end
 
     #遍历每一个共同的评价电影,计算欧式距离
     eculidDistance = 0.0
     for m in commonMovie
         score1 = userRatedMovieDict[user1][m]
         score2 = userRatedMovieDict[user2][m]
         eculidDistance += (score1 - score2)^2
         # println(score1,"\t", score2,"\t",eculidDistance)
     end
     eculidDistance = sqrt(eculidDistance)
     similarity = 1/(1+eculidDistance)
     return similarity
 end

有了相似度计算的函数,我们可以预先计算好任意两个用户之间的相似度,并保存到变量中,方便后续进行推荐评分时使用。本文数据集中共用671个用户,自己与自己的相似度不需要计算,同时任意两个用户之间仅需要计算一次,这样计算下来需要执行的距离计算次数为:(671\*671 - 671 )/2 = 224785 次。代码如下:

#暴力寻找每两个用户对之间的相似度
 tic()
 
 userSet = Set(ratings[:userId])
 similarityBetween = Dict()
 count = 0
 for u1 in userSet
     for u2 in userSet
         if u1 == u2 
             similarityBetween[(u1,u2)] = 0.0
         elseif u1 < u2 
             similarityBetween[(u1,u2)] = getSimilarity(u1,u2)
             count += 1
         else 
             continue
         end
     end
 end
 
 println("计算距离的次数:", count)
 toc()

代码耗时共计16.1秒钟。可以采取的一个优化措施就是构造类似信息检索领域的“倒排索引”,针对每个电影,找到对其有过评分的用户,然后构造关系对。如果用户之间相似度比较稀疏,这种思路可以大规模降低计算量。结合本文的数据,代码可以组织如下:

#构造倒排索引
 #先得到所有电影的评分人
 function getMovieRatedUser(movieId)
     curMovieRatings = ratings[ratings[:movieId] .== movieId,:]
     userList = []
     for i in 1:nrow(curMovieRatings)
         u = curMovieRatings[i,:userId]
         push!(userList,u)
     end
     return userList
 end
 
 movieRatedUser = Dict()
 for m in unique(ratings[:movieId])
     movieRatedUser[m] = getMovieRatedUser(m)
 end
 
 #基于电影的评分人列表,构造关系对
 function getPair(inputList)
     result = Dict()
     for i in 1:length(inputList)
         for j in (i+1):length(inputList)
             result[(inputList[i],inputList[j])] = 1
         end
     end
     return keys(result)
 end
 
 #对所有的电影应用上述函数
 allUserPairArray = map(getPair,values(movieRatedUser))
 allUserPairDict = Dict()
 for pairArray in allUserPairArray
     for pair in pairArray
         if pair[1] < pair[2]
             allUserPairDict[(pair[1],pair[2])] = 0
         else
             allUserPairDict[(pair[2],pair[1])] = 0
         end
     end
 end
 allUserPair = unique(keys(allUserPairDict))
 
 size(allUserPair)
 
 #针对上面得到的关系对,计算相似度
 tic()
 allUserPairSimilartiy = Dict()
 for pair in allUserPair
     allUserPairSimilartiy[pair] = getSimilarity(pair[1],pair[2])
 end
 
 toc()

接下来构造一个函数开求用户inputUserId对inputMovieId的预测评分:

 #计算用户对电影的预测评分
 function getRecMovie(inputUserId, inputMovieId)
     moviePrefer = Dict()
     #找到对该电影有过评分的用户及评分
     userList = ratings[ratings[:movieId] .== inputMovieId,[:userId,:rating]]
     #计算每个评分用户与当前用户的相似度,并进行加权评分
     predScore = 0.0
     similarity = 0.0
     sumSimilarity = 0.0 
     for i in 1:nrow(userList)
         userId = userList[i,:userId]
         score = userList[i,:rating]
         similarity = userId < inputUserId? getSimilarity(userId,inputUserId): getSimilarity(inputUserId,userId)
         sumSimilarity += similarity
         predScore += similarity*score
     end
     moviePrefer[inputMovieId] = predScore/sumSimilarity
     return moviePrefer
 end


参考资料

【1】《数据科学实战手册》第9章, 但其实个人认为该书这一章节的语言和思路组织地并不清晰,使用的数据集也过旧。
【2】《个性化推荐系统》项亮著, 第2章

猜你喜欢

转载自blog.csdn.net/a_step_further/article/details/79875363