影片推荐系统思考
1、用户信息的补充和处理
背景:智能电视通过机顶盒向用户分发电视节目。事先采集的用户信息及其有限。且电视节目的用户大多以家庭为单位,用户画像也相应呈现出家庭的特征,各项属性,如年龄,性别,学历,等等特征都是不固定的。
如何扩展有限的用户特征?
方法1:利用用户在办理开通业务时候的登记号码,可以提取用户所在的区域。
方法2:另外,通过用户的历史观影(viewlog)数据,统计一天内,24小时不同时间段的用户数量,构建数据分布图,根据数据分布,将观影时段分类,建立用户观影时段属性.
2、影片之间的相似度矩阵构建
思路1:根据用户观看历史,建立用户与观看影片列表,如图:
利用word2vec生成每个电影的词向量,利用词向量生成m-m matrix。
词向量维度可以自己定义,但一般在100-400之间,因此该方法需要有比较大的开销。
思路2:将每一个影片的导演、演员、类型等属性组成 sentence,利用编辑距离,或自定义加权编辑距离计算影片之间的距离,构建m-m matrix。
3、用户之间的相似度矩阵构建
思路1: 利用用户与观看影片列表,计算两者共同观看的电影数量占总观影数量的比例来确定user与user之间的关系。用户的点播行为是随时间变化的,且在计算时,要考虑一天中的不同时段,其观看影片的类型会有所不同(比如孩子放学的时段,播放动画片的概率会比较高。)因此user-user matrix也应该随时间更新(比如一月一次)。
4、电视节目的基于流行度的推荐
由于用户特征有限,完全的个性化推荐不太实际。首先考虑采取基于流行度的粗粒度推荐。
思路:
1、用户分类。
可根据user-user matrix 将用户分类
根据属性,比如AREA,TIMEZOOM分类
根据属性分层如:先根据AREA粗分,再根据 TIMEZOOM细分
利用上述的方法,将用户分成不同的GROUP
2、选择topk影片
根据Day1 的数据,获得每一个GROUP下TOPk 电影,并结合m-m,计算最相关的多个电影进行推荐。
这中间要解决如,编辑距离加权。和电影重复推荐,以及冷片推送的问题。
5、冷片的推荐
**用spark.mllib.ALS实现最简单的推荐 **
采用协同过滤式方法推荐影片--该方法根据用户的历史评分,分析用户的喜好,从而向用户推荐合适的产品.
数据集下载
https://grouplens.org/datasets/movielens/
下载数据解压缩:
u.data中存放用户评分数据,分为四列,分别是use id,movieid, rating,time
u.item 有一些字段,记录着电影的信息,分别为:movieid,name(year), pubulish date…
导入数据
scala> val rawUserData = sc.textFile("u.data")
scala> rawUserData.take(5).foreach(println)
196 242 3 881250949
186 302 3 891717742
22 377 1 878887116
244 51 2 880606923
166 346 1 886397596
//检查数据,看看有没有缺失和异常
scala> rawUserData.map(_.split("\t")(0).toDouble).stats()
res1: org.apache.spark.util.StatCounter = (count: 100000, mean: 462.484750, stdev: 266.613087, max: 943.000000, min: 1.000000)
scala> rawUserData.map(_.split("\t")(1).toDouble).stats()
res2: org.apache.spark.util.StatCounter = (count: 100000, mean: 425.530130, stdev: 330.796702, max: 1682.000000, min: 1.000000)
scala> rawUserData.map(_.split("\t")(2).toDouble).stats()
res3: org.apache.spark.util.StatCounter = (count: 100000, mean: 3.529860, stdev: 1.125668, max: 5.000000, min: 1.000000)
scala> rawUserData.map(_.split("\t")(3).toDouble).stats()
res5: org.apache.spark.util.StatCounter = (count: 100000, mean: 883528851.488622, stdev: 5343829.470155, max: 893286638.000000, min: 874724710.000000)
训练model
scala> import org.apache.spark.mllib.recommendation.ALS
import org.apache.spark.mllib.recommendation.ALS
scala> import org.apache.spark.mllib.recommendation.Rating
import org.apache.spark.mllib.recommendation.Rating
//只需要前三个属性(userid,movieid,rating)
scala> val rawRatings = rawUserData.map(_.split("\t").take(3))
rawRatings: org.apache.spark.rdd.RDD[Array[String]] = MapPartitionsRDD[12] at map at <console>:27
scala> val ratingRDD = rawRatings.map{case Array(user,movie,rating) => Rating(user.toInt,movie.toInt,rating.toDouble)}
ratingRDD: org.apache.spark.rdd.RDD[org.apache.spark.mllib.recommendation.Rating] = MapPartitionsRDD[13] at map at <console>:27
//将构造好的Rating 丢进模型训练
scala> val model = ALS.train(ratingRDD,10,10,0.01)
/*
ALS.train(ratings,rank,numIterations,lambda)参数解释:
rating:具有特定格式的 RDD
rank:首先算法将原本的u_m_matrix矩阵(user * movie)(通过ratingRDD构建--仔细思考,这个矩阵是很稀疏的)
分解为用户矩阵X(user * rank),和产品矩阵Y(movie * rank)(矩阵分解之后,矩阵不再稀疏,且维数有所约减),
然后分别构建user-user矩阵,和movie-movie矩阵,进而通过矩阵乘法可以得到movie 与所有用户的关系,也可以得到user与所有movie的关系,
rank 可以理解为分解矩阵时特征的数量,是一个超参数,需要人工调.
numIterations:训练次数
lambda:正则化参数,用来控制模型泛化性能,也是超参数,需要人工调.
*/
2019-02-28 15:48:42 WARN BLAS:61 - Failed to load implementation from: com.github.fommil.netlib.NativeSystemBLAS
2019-02-28 15:48:42 WARN BLAS:61 - Failed to load implementation from: com.github.fommil.netlib.NativeRefBLAS
2019-02-28 15:48:42 WARN LAPACK:61 - Failed to load implementation from: com.github.fommil.netlib.NativeSystemLAPACK
2019-02-28 15:48:42 WARN LAPACK:61 - Failed to load implementation from: com.github.fommil.netlib.NativeRefLAPACK
model: org.apache.spark.mllib.recommendation.MatrixFactorizationModel = org.apache.spark.mllib.recommendation.MatrixFactorizationModel@23bc053c
预测
//推荐用户454喜欢的movie
scala> model.recommendProducts(454,5).mkString("\n")
res8: String =
Rating(454,1141,4.473476952668481)
Rating(454,34,4.465395838192516)
Rating(454,1192,4.129537334173054)
Rating(454,1410,4.125483999697048)
Rating(454,867,4.001905086743346)
//推荐用户相关的用户
scala> model.recommendUsers(464,5).mkString("\n")
res9: String =
Rating(219,464,13.07212541336996)
Rating(726,464,11.970157628357022)
Rating(418,464,11.162018183746392)
Rating(98,464,10.768677532641455)
Rating(820,464,9.919882868872023)
//预测用户对movie 的评分
scala> model.predict(196,442)
//这个人可能蛮不喜欢这个产品
res10: Double = -0.20212121637734182
将推荐的影片id跟名称对应起来
scala> val itemRDD = sc.textFile("u.item")
itemRDD: org.apache.spark.rdd.RDD[String] = u.item MapPartitionsRDD[226] at textFile at <console>:26
//读取对应关系,构建collectAsMap()
scala> val movieTitle = itemRDD.map(_.split("\\|").take(2)).map(array=>(array(0).toInt,array(1))).collectAsMap()
scala> movieTitle.take(5).foreach(println)
(146,Unhook the Stars (1996))
(1205,Secret Agent, The (1996))
(550,Die Hard: With a Vengeance (1995))
(891,Bent (1997))
(137,Big Night (1996))
//每一次推荐,返回的是Rating组成的Array
scala> model.recommendProducts(196,5)
res14: Array[org.apache.spark.mllib.recommendation.Rating] = Array(Rating(196,634,9.521015571592464), Rating(196,394,9.006035614225851), Rating(196,1242,8.888490353715557), Rating(196,1066,8.876096458148403), Rating(196,1172,8.239287750500973))
//Rating.product读出movie id,再用movieTitle对应表找出name
scala> model.recommendProducts(196,5).map(rating=>(rating.product,movieTitle(rating.product),rating.rating)).foreach(println)
(634,Microcosmos: Le peuple de l'herbe (1996),9.521015571592464)
(394,Radioland Murders (1994),9.006035614225851)
(1242,Old Lady Who Walked in the Sea, The (Vieille qui marchait dans la mer, La) (1991),8.888490353715557)
(1066,Balto (1995),8.876096458148403)
(1172,Women, The (1939),8.239287750500973)
电影推荐demo :https://www.jianshu.com/p/be5daf1fd1be