Bootstrap

我的基于Spark的电影推荐(机器学习实训)

基于Spark的电影推荐

一、实训背景

近几年互联网信息量呈几何级增长,用户很容易迷失在海量信息中。虽然可使用层次分类(分类目录)或搜索引擎等方法解决这类信息过载问题,但是层次分类需要手工对信息进行分类,并且随着数据量增加层级逐渐增多,不便于用户浏览;而搜索引擎需要用户自己输入关键词,自行选择结果,这要求用户已明确知道要查找的内容,并且具有核心词的抽象能力,如果对结果不满意需要调整关键词重新搜索。推荐系统根据用户的浏览记录、社交网络等信息进行个性化的计算,发现用户的兴趣,并应用推荐算法最终达到“千人千面”“个性化”推荐的效果。

二、实训内容

使用Spark的机器学习算法实现库MLlib,以及将其应用在MovieLens数据集上实现个性化电影推荐。

三、关键技术

  • 基于协同过滤的推荐算法原理
  • 最小二乘法(ALS)原理
  • pyspark推荐算法模型训练

四、实训目的

  • 了解推荐系统的应用场景
  • 熟悉协同过滤推荐算法原理
  • 掌握pyspark中使用最小二乘法(ALS)来实现协同过滤推荐模型。

五、实训环境

  • 操作系统:ubuntu16
  • 工具软件:jupyter notebook、Python 3.6.13
  • 硬件环境:无特殊要求
  • 核心库:
    • pyspark 3.0.1

六、实训原理

1 推荐系统的应用场景

  • 电商平台

    目前推荐系统已经基本成为电商平台标配。主流的电商平台具有多种推荐形式,例如“猜你喜欢”“购买此商品的用户也购买了……”等;除 此之外,还有隐式商品推荐,例如在搜索结果中将推荐商品排名提前。

  • 个性化视频网站

    每年国内外都有大量电影上映,由用户自制的视频节目也越来越多,用户很难在海量的视频节目中进行选择。视频网站基于用户的历史观看记录以及视频内容之间的内在联系,分析用户潜在兴趣,向用户推荐其感兴趣的内容。

  • 音乐歌单

    目前音频类个性化推荐主要是向用户推荐歌曲或播单,好的推荐会让用户既熟悉又有新鲜和惊喜的感觉。主流音乐平台音乐推荐的实现方法与电影推荐类似,主要基于音乐的风格、用户收听历史、用户收听行为等进行协同过滤。

  • 社交网络

    推荐系统在社交网络中的应用主要是好友推荐和内容推荐。好友推荐是指在社交网站中向用户推荐具有共同兴趣的用户成为好友。用户之间可通过关系网络建立联系,还可以通过阅读、点赞、评论了相同的博文产生关系。如果两个用户有多个共同的标签,曾经评论或者转发相同的信息,说明他们对这条信息有着共同的兴趣。对这些用户行为应用基于用户的协同过滤算法,就可以向用户进行个性化内容推荐。在社交网站中用户之间形成一个社交网络图,可以分析用户之间兴趣的相似性,例如,用于学术社区中同行的发现,对那些研究领域相同,但在网站中并非好友的用户,推荐他们互加好友。

  • 新闻网站

    新闻网站中应用推荐算法可以方便用户及时获取个性化信息,减少用户浏览、检索新闻的时间,并提供更好的阅读体验,从而增加用户黏性。一般采用基于内容的协同过滤推荐算法来实现,其中数据包括用户属性特征、浏览历史和新闻内容等,从而解决新闻量过大时给用户带来的信息过载和迷航问题。在新闻网站中常有“冷启动”的问题。它是指网站刚刚建立,用户和 新闻内容较少,用户的行为数据更少,所以协同过滤算法往往无效。为缓解此问题,可以使用热门内容作为推荐结果,逐渐收集用户行为数据,不断完善推荐结果,吸引更多用户注册,从而形成良性循环。

  • 个性化阅读

    个性化阅读是为每一位用户定制其感兴趣的个性化内容,例如新闻、论坛帖子、小说等。推荐系统通过推荐算法获得用户兴趣,并向其推送个性化的阅读内容,从而提供更优的阅读方式和更好的阅读体验。

  • 个性化广告

    个性化广告是指有针对性地向特定用户展示特定广告内容。首先对广告受众进行用户画像,对受众的个人状况、商业兴趣、社交图谱等方面进行刻画,这是广告推荐引擎的基础。然后推荐系统基于用户的行为进行协同过滤,并对推荐的广告结果进行粗选、精选。上述过程可以通过定时运行的方式离线生成推荐结果;也可以实时生成推荐结果,这对推荐算法和硬件计算均要求较高。在用户浏览广告过程中可看出用户对广告的态度或反应,这可作为推荐结果的评价依据,并用于改进推荐算法,减少用户对广告的负面体验。

2 Spark介绍

Spark是一个开源的并行计算与分布式计算框架,最大特点是基于 内存计算,适合迭代计算,兼容Hadoop生态系统中的组件,同时包括相 关的测试和数据生成器。其设计目的是全栈式解决批处理、结构化数据查询、流计算、图计算和机器学习等应用,适用于需要多次操作特定数据集的应用场合。需要反复操作的次数越多,所需读取的数据量越大,效率提升越大,在这方面比Hadoop快很多倍。Spark集成了以下模块, 为不同应用领域的从业者提供了快速的大数据处理方式。

  • Spark SQL:分布式SQL查询引擎,提供了一个DataFrame编程抽 象。

  • Spark Streaming:把流式计算分解成一系列短小的批处理计算,并 提供高可靠和吞吐量服务。

  • MLlib:Spark对常用的机器学习算法的实现库,支持4种常见的机 器学习问题:分类、回归、聚类和协同过滤。

  • GraphX:提供图计算服务。

  • SparkR:支持R语言的库。

  • 扩展库pyspark:提供了SparkContext作为主要入口,还有弹性分布式数据集(Resilient Distributed Dataset,RDD)、文件访问类(SparkFiles)等可以公开访问的类,并且提供了pyspark.sql、pyspark.streaming与pyspark.mllib等模块与包。

3 基于协同过滤的推荐算法介绍

基于用户人口属性和行为数据设计的推荐算法,称为协同过滤算法。此方法主要根据用户的历史行为,寻找用户或物品的近邻集合,以此计算用户对物品的偏好,包括基于领域、图、关联规则、知识的推荐算法,其中最广泛应用的是基于领域的方法,在实践中往往是上述几种方法的混合应用。

基于领域的推荐算法分类:
基于领域的推荐算法主要包含两种:基于用户的协同过滤算法
(UserCF)和基于物品的协同过滤算法(ItemCF)。基于用户的协同过滤计算用户兴趣相似度,基于物品的协同过滤算法计算与用户偏好的物品相似的物品。本实验电影推荐使用的是基于用户的协同过滤算法。

基于用户的协同过滤算法:
基于用户的协同过滤算法为用户推荐兴趣相似的其他用户喜欢的物品。算法的关键是计算两个用户的兴趣相似度。计算用户相似度的常用方法有余弦相似性、皮尔森系数相关和修正的余弦相似性。

算法步骤如下:

① 找到与目标用户兴趣相似的用户集合;

② 找到这个集合中的用户喜欢的,且目标用户没有用过的物品, 推荐给目标用户。

4 基于用户的协同过滤推荐示例

用户/物品物品A物品B物品C物品D
用户A推荐
用户B
用户C

该表是基于用户的协同过滤推荐示例,可以看到用户A与用户C 所喜欢的物品具有较多的交集,即两个用户具有相似性,那么用户C喜 欢的物品很有可能用户A也会喜欢,而用户C喜欢物品D,则可以向用户 A推荐物品D。

计算用户兴趣相似度时,要避免热门物品自带马太效应的影响,即大部分用户可能都对热门的物品表现出喜欢的状况,但是这些用户之间并非一类人,因为所谓的热门物品区分度较弱。

基于用户的协同过滤算法的缺点是随着用户数目增大,计算用户兴趣相似度越来越复杂,时间和空间复杂度与用户数接近于平方关系。所以一般采用离线方式进行推荐,即当用户产生新的行为时,不会立即进行计算,所以推荐结果并不会马上发生变化。此外,这一算法是基于隐式群体的兴趣进行推荐,可解释性不强。这一算法适用于用户兴趣比较稳定的场景,即通过群体的兴趣来代表用户个体的兴趣,一旦群体的兴趣确立,就可以认为个体用户服从此兴趣,由此向其进行推荐,结果一般较准确。

5 模型训练(ALS算法)

在pyspark中使用交替最小二乘法(Alternating Least Squares, ALS)来实现协同过滤推荐,主要原因是它支持稀疏的输入数据(用户 对物品的评分是稀疏矩阵),并且可用简单的线性代数运算求解最优解,此外,输入数据本身可以并行化,这就使ALS在大规模数据上速度 非常快。

ALS中文名作交替最小二乘法,在机器学习中,ALS特指使用最小二乘法求解的一个协同过滤算法,是协同过滤中的一种。ALS算法是2008年以来,用的比较多的协同过滤算法。它已经集成到Spark的Mllib库中,使用起来比较方便。从协同过滤的分类来说,ALS算法属于User-Item CF,也叫做混合CF,因为它同时考虑了User和Item两个方面,即可基于用户进行推荐又可基于物品进行推荐。

一般而言用户只会购买物品集中的极少数部分产品,并对其进行打分。考虑下面这样一个包含用户的打分矩阵(列为用户u1-u6,行为物品I1-I8),我们可以看到这个用户的评分矩阵是十分稀疏的,有很多用户的购买的记录是空的,而且在现实业务中,用户的评分矩阵会更加的稀疏。如何通过这样一个稀疏矩阵,对用户进行协同推荐用户可能很喜欢的物品对于推荐系统而言是一种很大的考验。

用户评分矩阵

l1l2l3l4l5l6l7l8
u152
u2431
u315
u472
u5711
u6542

解决稀疏矩阵问题,需要采用矩阵分解,如下面两个图:
将原本矩阵A(m * n) 分解成X(m * rank) 矩阵与Y(rank * n) 矩阵,而且A大约等于 X * Y

在这里插入图片描述

在spark MLlib 机器学习库中目前推荐模型只包含基于矩阵分解(matrix factorization)的实现。具体的分解思路,找出两个低维的矩阵,使得它们的乘积是原始矩阵。因此这也是一种降维技术。假设我们的用户和物品分别是m和n,那对应的“用户-物品”矩阵A,类似图所示:

在这里插入图片描述
找到和“用户-物品“矩阵近似的k维(低阶)矩阵,最终还是要求出如下两个矩阵:一个用于表示用户X维矩阵,以及一个表征物品的Y维矩阵。这两个矩阵也称为因子矩阵,他们的矩阵乘积便是原始评级数据的一个近似值。值得注意的是,原始评级矩阵通常很稀疏,但因子矩阵却是稠密的,如图所示:

在这里插入图片描述
ALS是求解矩阵分解问题的一种最优化方法,它功能强大,效果理想而且被证明相对容易实现。这使得它很适合如Spark这样的平台。

ALS的实现原理是迭代式求解一系列最小二乘回归问题。在每次迭代时,固定用户因子矩阵或者是物品因子矩阵中的一个,然后用固定的这个矩阵以及评级数据来更新另一个矩阵。之后,被更新的矩阵被固定住,再更新另外一个矩阵。如此迭代,知道模型收敛(或者是迭代了预设好的次数)。

6 电影数据集

实验采用MovieLens数据集作为数据源,它是一个关于电影评分的 数据集。包括links.csv、movies.csv、ratings.csv、tags.csv几个文件。
link.csv文件的内容是电影编号,通过编号可以在网站上找到对应的电影链接;

  • movies.csv文件中包含电影编号、标题、电影题材;
  • ratings.csv文件是用户对电影的评分和评分时间戳,其中评分是5分制,按半星的 规模递增;
  • tags.csv文件包含用户对电影的标签化评价和打标签时间戳;上述数据的编号、时间戳等均为数字型(long型),评分为float型。
  • MovieLens数据集按照数据量的大小分为10万、2000万yue 、2600万等 几种压缩包,方便不同用途的应用,其中10万的数据量较少,包括10万 条评分记录、1300个标签,对应9000部电影,用户数是700人,更新时间是2016年10月;2000万的压缩包有2000万条评分记录,565000个标 签,对应2.7万部电影,用户数是13.8万人,并且包括标签genome数据; 2600万的压缩包是全量数据,包含2600万条评分数据、75万个标签、 4.5万部电影、27万用户数,并且还有1200万条标签genome数据,最后 更新时间是2017年8月份。本次实验过程中采用10万的数据集中的rating.csv进行验ratings数据。

文件里面的内容包含了每一个用户对于每一部电影的评分。数据格式如下:userId, movieId, rating, timestamp;

  • userId: 数字类型的每个用户的编号
  • wget http://files.grouplens.org/datasets/movielens/ml-100k.zip: 数字类型每部电影的编号
  • rating: 浮点类型的用户评分值,是5星制,按半颗星的规模递增(0.5 stars - 5 stars)
  • timestamp: 评分时间戳,本实验中不使用。

数据排序的顺序按照userId,movieId排列的。

我们使用pandas观察数据集的描述及前五行:

import os
os.environ["PYSPARK_PYTHON"]="/usr/bin/python3"
os.environ["PYSPARK_DRIVER_PYTHON"]="/usr/bin/python3"
# 数据简介
import pandas as pd
ratings = pd.read_csv('../dataset/ratings.csv',header=None,names=['userid','movieid','rating','timestamp'])
ratings.describe()
useridmovieidratingtimestamp
count100004.000000100004.000000100004.0000001.000040e+05
mean347.01131012548.6643633.5436081.129639e+09
std195.16383826369.1989691.0580641.916858e+08
min1.0000001.0000000.5000007.896520e+08
25%182.0000001028.0000003.0000009.658478e+08
50%367.0000002406.5000004.0000001.110422e+09
75%520.0000005418.0000004.0000001.296192e+09
max671.000000163949.0000005.0000001.476641e+09
# 前5行
ratings.head(5)
useridmovieidratingtimestamp
01312.51260759144
1110293.01260759179
2110613.01260759182
3111292.01260759185
4111724.01260759205

七、实训步骤

1 导入库

import os
import math
import time
from pyspark import SparkContext
from pyspark.sql import SQLContext, Row, SparkSession
from pyspark.mllib.recommendation import ALS

2 加载数据库

首先对数据进行预处理,加载评分文件,并将文件中的记录按照6:2:2分为训练集、验证集、测试集,随机数种子固定为10。

sc = SparkContext()

#文件访问
small_raw_data = sc.textFile('../dataset/ratings.csv')
small_data = small_raw_data.map(lambda line: line.split(",")).map(lambda col: (col[0], col[1], col[2])).cache()


#按照6:2:2分为训练集、验证集、测试集
training_RDD, validation_RDD, test_RDD = small_data.randomSplit([6, 2, 2], seed=10)
validation_predict_RDD = validation_RDD.map(lambda x: (x[0], x[1]))
test_predict_RDD = test_RDD.map(lambda x: (x[0], x[1]))

3 模型训练

基于ALS的原理,需要确认最佳秩(rank)值,首先循环计算多个秩的值,并记录最小误差,误差评价标准是RMSE,以最佳秩值作为输入重新进行训练,生成模型,代码如下:

#ALS参数配置
seed = 5
iterations = 10
regularization_param = 0.1
ranks = [4, 8, 12]
errors = [0, 0, 0]
err = 0
tolerance = 0.02

#模型训练确认rank值(最小误差)
min_error = float('inf')
best_rank = -1
best_iteration = -1
for rank in ranks:
    model = ALS.train(training_RDD, rank, seed=seed, iterations=iterations, lambda_=regularization_param)
    predict = model.predictAll(validation_predict_RDD).map(lambda r: ((r[0], r[1]), r[2]))
    rates_predictions = validation_RDD.map(lambda r: ((int(r[0]), int(r[1])), float(r[2]))).join(predict)
    error = math.sqrt(rates_predictions.map(lambda r: (r[1][0] - r[1][1]) ** 2).mean())
    errors[err] = error
    err += 1
    if error < min_error:
        min_error = error
        best_rank = rank

#以最佳rank值新重训练模型
model = ALS.train(training_RDD, best_rank, seed=seed, iterations=iterations, lambda_=regularization_param)

训练好的模型可以保存到文件中,这样在使用模型时,通过矩阵分解模型(MatrixFactorization Model)从文件中加载即可使用,代码如下:

model.save(sc, "spark_movie.model")
sameModel = MatrixFactorizationModel.load(sc, "spark_movie.model")

4 模型结果评估

模型的评估是将测试集(只含有用户编号和电影编号)提交给模型,由其进行打分,并将打分结果与测试集中实际评分值进行比较,对所有预测结果计算RMSE值,作为模型评价指标,代码如下,其输出结果为94.15%,说明模型预测的准确性较好。

#模型测试
predictions = model.predictAll(test_predict_RDD).map(lambda r: ((r[0], r[1]), r[2]))
rates_and_predictions = test_RDD.map(lambda r: ((int(r[0]), int(r[1])), float(r[2]))).join(predictions)
#计算RMSE指标
error = math.sqrt(rates_predictions.map(lambda r: (r[1][0] - r[1][1]) ** 2).mean())
print('Model RMSE = %s' % error)

5 模型使用

假设用户编号为16,如果要预测其对电影编号为48的电影的评分值,可通过以下代码,调用模型的predict方法即可返回评分值。此外,
还可以基于用户的历史行为向其推荐n部电影,具体调用模型的recommendProducts方法,输入参数为用户编号和预测的电影数量,本实验中推荐电影的数量为10。

#预测某一用户对某一电影的评分
user_id = 16
movie_id = 48
predictedRating = model.predict(user_id, movie_id)
print("用户编号:"+str(user_id)+" 对电影:"+str(movie_id)+" 的评分为:"+str(predictedRating))

#向某一用户推荐10部电影
topKRecs = model.recommendProducts(user_id, 10)
print("向用户编号:"+str(user_id)+"的用户推荐10部电影:")
for rec in topKRecs:
    print(rec)

运行程序后的输出结果如下,可以看到模型预测编号为16的用户对电影48评分为3.10。也可以看到向其推荐的10部电影编号和对应的评分 值。结果如下:

	Model RMSE = 0.9403545117603078
	用户编号:16 对电影:48 的评分为:3.216689261395394
	向用户编号:16的用户推荐10部电影:
	Rating(user=16, product=4630, rating=6.05529107837512)
	Rating(user=16, product=5765, rating=6.05529107837512)
	Rating(user=16, product=3437, rating=6.05529107837512)
	Rating(user=16, product=9010, rating=6.05529107837512)
	Rating(user=16, product=7371, rating=5.807446390235324)
	Rating(user=16, product=8607, rating=5.802511774802554)
	Rating(user=16, product=91653, rating=5.751695420024291)
	Rating(user=16, product=2810, rating=5.648375121054762)
	Rating(user=16, product=6063, rating=5.630159072112605)
	Rating(user=16, product=7247, rating=5.582917190407091)

显示电影标题

  • header: csv文件的header。默认值是false;
  • delimiter: 分隔符。默认值是’ , ';
  • inferSchema: 根据你的数据预测你的数据类型,加了的话读取的次数是2次。这么说吧,比如学生的成绩,你不加的话,读出来的类型是string,加了就是int。
sqlsc = SQLContext(sc)
movieTitle = sqlsc.read.options(header='true', inferSchema='True', delimiter=',').csv("movies.csv")
print("向用户编号:"+str(user_id)+"的用户推荐10部电影:")

for p in topKRecs:
    print(movieTitle.filter("movieId=="+str(p[1])).show())  # <class 'pyspark.sql.dataframe.DataFrame

打印结果:

	向用户编号:16的用户推荐10部电影:
	+-------+--------------------+------+
	|movieId|               title|genres|
	+-------+--------------------+------+
	|   4630|No Holds Barred (...|Action|
	+-------+--------------------+------+
	
	None
	+-------+-----+------+
	|movieId|title|genres|
	+-------+-----+------+
	+-------+-----+------+
	
	None
	+-------+-----+------+
	|movieId|title|genres|
	+-------+-----+------+
	+-------+-----+------+
	
	None
	+-------+--------------------+-------------+
	|movieId|               title|       genres|
	+-------+--------------------+-------------+
	|   9010|Love Me If You Da...|Drama|Romance|
	+-------+--------------------+-------------+
	
	None
	+-------+---------------+--------------------+
	|movieId|          title|              genres|
	+-------+---------------+--------------------+
	|   7371|Dogville (2003)|Drama|Mystery|Thr...|
	+-------+---------------+--------------------+
	
	None
	+-------+--------------------+--------------------+
	|movieId|               title|              genres|
	+-------+--------------------+--------------------+
	|   8607|Tokyo Godfathers ...|Adventure|Animati...|
	+-------+--------------------+--------------------+
	
	None
	+-------+--------------------+------------+
	|movieId|               title|      genres|
	+-------+--------------------+------------+
	|  91653|We Bought a Zoo (...|Comedy|Drama|
	+-------+--------------------+------------+
	
	None
	+-------+-------------------+--------------------+
	|movieId|              title|              genres|
	+-------+-------------------+--------------------+
	|   2810|Perfect Blue (1997)|Animation|Horror|...|
	+-------+-------------------+--------------------+
	
	None
	+-------+----------+------------+
	|movieId|     title|      genres|
	+-------+----------+------------+
	|   6063|May (2002)|Drama|Horror|
	+-------+----------+------------+
	
	None
	+-------+--------------------+--------------------+
	|movieId|               title|              genres|
	+-------+--------------------+--------------------+
	|   7247|Chitty Chitty Ban...|Adventure|Childre...|
	+-------+--------------------+--------------------+
	
	None

针对电影4754推荐最有可能喜欢的前5个用户:

model.recommendUsers(4754, 5)

输出结果:

[Rating(user=156, product=4754, rating=7.392522617118351),
 Rating(user=304, product=4754, rating=6.951528110514651),
 Rating(user=498, product=4754, rating=6.902421770828882),
 Rating(user=348, product=4754, rating=6.877413192449223),
 Rating(user=29, product=4754, rating=6.870045800604159)]

八、实训总结

通过本实验的结果可以看到,基于Spark的协同过滤推荐算法并没有依赖具体的业务数据,如电影的内容分析和用户属性特征分析等,说明它为通用算法框架,可以用于其他行业的个性化推荐,例如餐饮推荐、音乐推荐和新闻推荐等,只要将各行业中的评分数据转化为本例中的ratings.csv对应的格式即可直接应用。

九、附资源

开头处无需积分即可下载 😀

;