3-Spark高级数据分析-第三章 音乐推荐和Audioscrobbler数据集
偏好是无法度量的。
相比其他的机器学习算法,推荐引擎的输出更直观,更容易理解。
接下来三章主要讲述Spark中主要的机器学习算法。其中一章围绕推荐引擎展开,主要介绍音乐推荐。在随后的章节中我们先介绍Spark和MLib的实际应用,接着介绍一些机器学习的基本思想。
3.1 数据集
用户和艺术家的关系是通过其他行动隐含提现出来的,例如播放歌曲或专辑,而不是通过显式的评分或者点赞得到的。这被称为隐式反馈数据。现在的家用电视点播也是这样,用户一般不会主动评分。
数据集在http://www-etud.iro.umontreal.ca/~bergstrj/audioscrobbler_data.html,需要自带梯子,下载地址是http://www.iro.umontreal.ca/~lisa/datasets/profiledata_06-May-2005.tar.gz,这个好像不用梯子。在百度网盘共享地址是http://pan.baidu.com/s/1bQ4Ilg。
3.2 交替最小二乘推荐算法
我们要找的学习算法不需要用户和艺术家的属性信息。这类算法通常称为协同过滤算法。根据两个用户的年龄相同来判断谈么可能有相似的偏好,这不叫协同过滤。相反,根据两个用户包房过许多相同歌曲来判断他们可能都喜欢某首歌,这才叫协同过滤。
潜在因素模型通过数量相对少的未被观察到的底层原因,来解释大量用户和产品之间可观察到的交互。
本实例使用的是一种矩阵分解模型。问题简化为“用户-特征矩阵”和“特征-艺术家矩阵”的乘积,该乘积的结果是对整个稠密的“用户-艺术家互相关心矩阵”的完整估计。
在求解矩阵分解时,使用交替最小二乘(Alternating Least Squares,ALS)算法。需要借助QR分解的方法。
3.3 准备数据
如果数据不是运行在集群上,而是运行在本地,为了保证内存充足,在启动spark-shell时需要指定参数--driver-memory 6g。
构建模型的第一步是了解数据,对数据进行解析或转换,以便在Spark中做分析。
Spark MLib的ALS算法实现有一个小缺点:它要求用户和产品的ID必须是数值型,并且是32位非负整数,这意味着大于Integer.MAX_VALUE(2147483647)的ID是非法的。我们首先看看数据集是否满足要求:
Scala:
scala> val rawUserArtistData = sc.textFile("D:/Workspace/AnalysisWithSpark/src/main/java/advanced/chapter3/profiledata_06-May-2005/user_artist_data.txt")
rawUserArtistData: org.apache.spark.rdd.RDD[String] = D:/Workspace/AnalysisWithSpark/src/main/java/advanced/chapter3/profiledata_06-May-/user_artist_data.txt MapPartitionsRDD[] at textFile at <console>: scala> rawUserArtistData.map(_.split(' ')().toDouble).stats()
res0: org.apache.spark.util.StatCounter = (count: , mean: 1947573.265353, stdev: 496000.544975, max: 2443548.000000, min: 90.000000) scala> rawUserArtistData.map(_.split(' ')().toDouble).stats()
res1: org.apache.spark.util.StatCounter = (count: , mean: 1718704.093757, stdev: 2539389.040171, max: 10794401.000000, min: 1.000000)
Java:
//初始化SparkConf
SparkConf sc = new SparkConf().setMaster("local").setAppName("RecommendingMusic");
System.setProperty("hadoop.home.dir", "D:/Tools/hadoop-2.6.4");
JavaSparkContext jsc = new JavaSparkContext(sc); //读入用户-艺术家播放数据
JavaRDD<String> rawUserArtistData =jsc.textFile("src/main/java/advanced/chapter3/profiledata_06-May-2005/user_artist_data.txt"); //显示数据统计信息
System.out.println(rawUserArtistData.mapToDouble(line -> Double.parseDouble(line.split(" ")[0])).stats());
System.out.println(rawUserArtistData.mapToDouble(line -> Double.parseDouble(line.split(" ")[1])).stats());
最大用户和艺术家ID为2443548和10794401,没必要处理这些ID。
接着解析艺术家ID与与艺术家名对应关系。由于文件中少量的行不规范,有些行没有制表符、有些不小心加入了换行符,所以不能直接使用map处理。这是需要使用flatMap,它将输入对应的两个或多个结果组成的集合简单展开,然后放到一个更大的RDD中。后面的Scala程序没有运行过,只是粘贴在这里。读入艺术家ID-艺术家名数据并剔除错误数据:
Scala:
val rawArtistData = sc.textFile("hdfs:///user/ds/artist_data.txt")
val artistByID = rawArtistData.flatMap { line =>
val (id, name) = line.span(_ != '\t')
if (name.isEmpty) {
None
} else {
try {
Some((id.toInt, name.trim))
} catch {
case e: NumberFormatException => None
}
}
}
Java:
//读入艺术家ID-艺术家名数据
JavaRDD<String> rawArtistData =jsc.textFile("src/main/java/advanced/chapter3/profiledata_06-May-2005/artist_data.txt");
JavaPairRDD<Integer, String> artistByID = rawArtistData.flatMapToPair(line -> {
List<Tuple2<Integer, String>> results = new ArrayList<>();
String[] lineSplit = line.split("\\t", 2);
if (lineSplit.length == 2) {
Integer id;
try {
id = Integer.parseInt(lineSplit[0]);
} catch (NumberFormatException e) {
id = null;
}
if(!lineSplit[1].isEmpty() && id != null){
results.add(new Tuple2<Integer, String>(id, lineSplit[1]));
}
}
return results;
});
将拼写错误的艺术家ID或非标准的艺术家ID映射为艺术家的正规名:
Scala:
val rawArtistAlias = sc.textFile("hdfs:///user/ds/artist_alias.txt")
val artistAlias = rawArtistAlias.flatMap { line =>
val tokens = line.split('\t')
if (tokens(0).isEmpty) {
None
} else {
Some((tokens(0).toInt, tokens(1).toInt))
}
}.collectAsMap()
Java:
//将拼写错误的艺术家ID或非标准的艺术家ID映射为艺术家的正规名
JavaRDD<String> rawArtistAlias =jsc.textFile("src/main/java/advanced/chapter3/profiledata_06-May-2005/artist_alias.txt");
Map<Integer, Integer> artistAlias = rawArtistAlias.flatMapToPair(line -> {
List<Tuple2<Integer, Integer>> results = new ArrayList<>();
String[] lineSplit = line.split("\\t", 2);
if((lineSplit.length == 2 && !lineSplit[0].isEmpty())){
results.add(new Tuple2<Integer, Integer>(Integer.parseInt(lineSplit[0]), Integer.parseInt(lineSplit[1])));
}
return results;
}).collectAsMap();
artist_alias.txt中第一条为:"1092764 1000311",获取ID为1092764和1000311的艺术家名:
Java:
artistByID.lookup(1092764).forEach(System.out::println);
artistByID.lookup(1000311).forEach(System.out::println);
输出为:
Winwood, Steve
Steve Winwood
书中的例子为:
Scala:
artistByID.lookup().head
artistByID.lookup().head
Java:
artistByID.lookup(1000010).forEach(System.out::println);
artistByID.lookup(6803336).forEach(System.out::println);
输出为:
Aerosmith (unplugged)
Aerosmith
3.4 构建第一个模型
我们需要做两个转换:第一,将艺术家ID转为正规ID;第二,把数据转换成rating对象,它是ALS算法对“用户-产品-值”的抽象。其中产品指“向人们推荐的物品”。现在完成这两个工作:
Scala:
import org.apache.spark.mllib.recommendation._
val bArtistAlias = sc.broadcast(artistAlias)
val trainData = rawUserArtistData.map { line =>
val Array(userID, artistID, count) = line.split(' ').map(_.toInt)
val finalArtistID =
bArtistAlias.value.getOrElse(artistID, artistID)
Rating(userID, finalArtistID, count)
}.cache()
Java:
//数据集转换
Broadcast<Map<Integer, Integer>> bArtistAlias = jsc.broadcast(artistAlias); JavaRDD<Rating> trainData = rawUserArtistData.map( line -> {
List<Integer> list = Arrays.asList(line.split(" ")).stream().map(x -> Integer.parseInt(x)).collect(Collectors.toList());
bArtistAlias.getValue().getOrDefault(list.get(1), list.get(1));
return new Rating(list.get(0), list.get(1), list.get(2));
}).cache();
这里使用了广播变量,它能够在每个executor上将数据缓存为原始的Java对象,这样就不用为每个任务执行反序列化,可以在多个作业和阶段之间缓存数据。
构建模型:
Scala:
val model = ALS.trainImplicit(trainData, 10, 5, 0.01, 1.0)
Java:
MatrixFactorizationModel model = org.apache.spark.mllib.recommendation.ALS.train(JavaRDD.toRDD(trainData), 10, 5 ,0.01, 1);
构建要花费很长的时间。我使用的是i5的笔记本,估计得三四天才能算完。所以实际计算时,我只使用了前98个用户的数据,一共是14903行。所以在3.5中打印用户播放过艺术家作品时,ID使用数据集中有的ID。
查看特征变量:
Scala:
model.userFeatures.mapValues(_.mkString(", ")).first()
Java:
model.userFeatures().toJavaRDD().foreach(f -> System.out.println(f._1.toString() + f._2[0] + f._2.toString()));
3.5 逐个检查推荐结果
获取用户ID对应的艺术家:
Scala:
val rawArtistsForUser = rawUserArtistData.map(_.split(' ')).
filter { case Array(user,_,_) => user.toInt == 2093760 }
val existingProducts =
rawArtistsForUser.map { case Array(_,artist,_) => artist.toInt }.
collect().toSet
artistByID.filter { case (id, name) =>
existingProducts.contains(id)
}.values.collect().foreach(println)
Java:
JavaRDD<String[]> rawArtistsForUser = rawUserArtistData.map(x -> x.split(" ")).filter(f -> Integer.parseInt(f[0]) == 1000029 );
List<Integer> existingProducts = rawArtistsForUser.map(f -> Integer.parseInt(f[1])).collect();
artistByID.filter(f -> existingProducts.contains(f._1)).values().collect().forEach(System.out::println);
我们可以对此用户做出5个推荐:
Scala:
val recommendations = model.recommendProducts(2093760, 5)
recommendations.foreach(println)
Java:
Rating[] recommendations = model.recommendProducts(1000029, 5);
Arrays.asList(recommendations).stream().forEach(System.out::println);
结果如下:
Rating(,,506.30319635520425)
Rating(,,453.6082026572616)
Rating(,,137.14313260781685)
Rating(,,130.16330043654924)
Rating(,,128.75804355555215)
书上说每行最后的数值是0到1之间的模糊值,值越大,推荐质量越好。但是我运行返回的结果不是这样的。
Spark 1.6.2的Java API是这么说的:
Rating objects, each of which contains the given user ID, a product ID, and a "score" in the rating field. Each represents one recommended product, and they are sorted by score, decreasing. The first returned is the one predicted to be most strongly recommended to the user. The score is an opaque value that indicates how strongly recommended the product is.
应该最后一个数组是评分吧。
得到所推荐艺术家的ID后,就可以用类似的方法查到艺术家的名字:
Scala:
val recommendedProductIDs = recommendations.map(_.product).toSet
artistByID.filter { case (id, name) =>
recommendedProductIDs.contains(id)
}.values.collect().foreach(println)
Java:
List<Integer> recommendedProductIDs = Arrays.asList(recommendations).stream().map(y -> y.product()).collect(Collectors.toList());
artistByID.filter(f -> recommendedProductIDs.contains(f._1)).values().collect().forEach(System.out::println);
输出结果:
Barenaked Ladies
Da Vinci's Notebook
Rage
They Might Be Giants
"Weird Al" Yankovic
书上说好像推荐的结果不怎么样。
3.8 选择超参数
计算AUC这部分代码没有试。AUC(Area Under ROC Curve)是ROC(Receiver Operating Characteristic,受试者工作特征)线,它源于二战中用于敌机检测的雷达信号分析技术。在非均等代价下,ROC曲线不能直接反映出学习器的期望总体代价,而“代价曲线”则可达到该目的。
机器学习常涉及两类参数:一类是算法的参数,亦称“超参数”,数目常在10以内;另一类是模型的参数,数目可能很多。前者通常是由人工设定多个参数候选值后产生模型,后者则是通过学习来产生多个候选模型。
ALS.trainImplicit()的参数包括以下几个:
rank
模型的潜在因素的个数,即“用户-特征”和“产品-特征”矩阵的列数;一般来说,它也是矩阵的阶。
iterations
矩阵分解迭代的次数;迭代的次数越多,花费的时间越长,但分解的结果可能会更好。
lambda
标准的过拟合参数;值越大越不容易产生过拟合,但值太大会降低分解的准确度。lambda取较大的值看起来结果要稍微好一些。
alpha
控制矩阵分解时,被观察到的“用户-产品”交互相对没被观察到的交互的权重。40是最初ALS论文的默认值,这说明了模型在强调用户听过什么时的表现要比强调用户没听过什么时要好。
3.9 产生推荐
这个模型可以对所有用户产生推荐。它可以用于批处理,批处理每隔一个小时或更短的时间为所有用户重算模型和推荐结果,具体时间间隔取决于数据规模和集群速度。
但是目前Spark Mlib的ALS实现并不支持向所有用户给出推荐。该实现可以每次对一个用户进行推荐,这样每次都会启动一个短的几秒钟的分布式作业。这适合对小用户群体快速重算推荐。下面对数据中的多个用户进行推荐并打印结果:
Scala:
val someUsers = allData.map(_.user).distinct().take(100)
val someRecommendations =
someUsers.map(userID => model.recommendProducts(userID, 5))
someRecommendations.map(
recs => recs.head.user + " -> " + recs.map(_.product).mkString(", ")
).foreach(println)
Java:
//对id为1000029的用户做5个推荐
Rating[] recommendations = model.recommendProducts(1000029, 5);
Arrays.asList(recommendations).stream().forEach(System.out::println);
整个流程也可用于向艺术家推荐用户:
Scala:
rawUserArtistData.map { line =>
...
val userID = tokens(1).toInt
val artistID = tokens(0).toInt
...
}
Java:
在数据集转换时,"return new Rating(list.get(0), list.get(1), list.get(2));"变为"return new Rating(list.get(1), list.get(0), list.get(2));"
3.10 小结
对于非隐含数据,MLib也支持一种ALS的变体,它的用法和ALS是一样的,不同之处在于模型使用方法ALS.train()构建。它适用于给出评分数据而不是次数数据。比如,如果数据集是用户对艺术家的打分,值从1到5,那么用这种变体就很合适。不同推荐方法返回的Rating对象结果,其中rating字段是估计的打分。
如果能需要按需计算出推荐结果,可以使用Oryx 2(https://github.com/OryxProject/oryx),其底层使用MLib之类的库,但用高效的方式访问内存中的模型数据。
3-Spark高级数据分析-第三章 音乐推荐和Audioscrobbler数据集的更多相关文章
- Spark 实践——音乐推荐和 Audioscrobbler 数据集
本文基于<Spark 高级数据分析>第3章 用音乐推荐和Audioscrobbler数据 完整代码见 https://github.com/libaoquan95/aasPractice/ ...
- Spark高级数据分析-第2章 用Scala和Spark进行数据分析
2.4 小试牛刀:Spark shell和SparkContext 本章使用的资料来自加州大学欧文分校机器学习资料库(UC Irvine Machine Learning Repository),这个 ...
- 音乐推荐与Audioscrobbler数据集
1. Audioscrobbler数据集 数据下载地址: http://www.iro.umontreal.ca/~lisa/datasets/profiledata_06-May-2005.tar. ...
- Spark高级数据分析——纽约出租车轨迹的空间和时间数据分析
Spark高级数据分析--纽约出租车轨迹的空间和时间数据分析 一.地理空间分析: 二.pom.xml 原文地址:https://www.jianshu.com/p/eb6f3e0c09b5 作者:II ...
- Spark高级数据分析中文版-读者交流
第二章: 备注:1.本书第二章样例数据由于才有的是短链接,国内的用户可能无法下载.我把数据集拷贝到百度网盘上.大家可以从这个地方下载:http://pan.baidu.com/s/1pJvjHA7 谢 ...
- 5-Spark高级数据分析-第五章 基于K均值聚类的网络流量异常检测
据我们所知,有‘已知的已知’,有些事,我们知道我们知道:我们也知道,有 ‘已知的未知’,也就是说,有些事,我们现在知道我们不知道.但是,同样存在‘不知的不知’——有些事,我们不知道我们不知道. 上一章 ...
- 4-Spark高级数据分析-第四章 用决策树算法预测森林植被
预测是非常困难的,更别提预测未来. 4.1 回归简介 随着现代机器学习和数据科学的出现,我们依旧把从“某些值”预测“另外某个值”的思想称为回归.回归是预测一个数值型数量,比如大小.收入和温度,而分类则 ...
- UNIX环境高级编程-第三章习题
1,当读写磁盘文件时,read,write等函数确实是不带缓冲机制的吗?请说明原因. 答:所有磁盘I/O都要经过内核的块缓存区(即内核的缓冲区高速缓存).唯一例外的是对原始磁盘设备的I/O,但是我们不 ...
- JavaScript高级程序设计 第三章 基本概念
ch3 基本概念 标签(空格分隔): JavaScript 语法 标识符 - 第一个字符必须是字母.下划线或美元 - 驼峰大小写格式 严格模式 ECMAScript5引入,定义了一种解析和执行模型.此 ...
随机推荐
- My Game --文件读取数据
My Game --线段数据 中说到背景的绘制由贝赛尔曲线生成线段,用 DrawNode 画多边形,同时一张背景有两座山,一座山有两条以上贝赛尔曲线保存,用了嵌套的数据类:Bezier,LineLay ...
- jquery.qrcode 生成二维码带logo
<div id="container">这里是二维码显示位置</div> <script language="JavaScript" ...
- JSP内置对象---用户登录页面(get和post)
Login.jsp 页面: <%@ page language="java" import="java.util.*" contentType=" ...
- svn出现版本冲突之后的 无效路径
.csproj.FileListAbsolute.txt 找到之后删掉错误的代码
- Titanium studio安装
在Win7 Titanium Studio的安装过程. 1.准备工作 Titanium存储空间的要求,Titanium Studio 需要1 GB.Android SDK需要1.5 GB.Blackb ...
- unicode编码与utf-8 区别
unicode编码与utf-8 区别 如果是为了跨平台兼容性,只需要知道,在 Windows 记事本的语境中: 所谓的「ANSI」指的是对应当前系统 locale 的遗留(legacy)编码.[1] ...
- CentOS 7 Hadoop安装配置
前言:我使用了两台计算机进行集群的配置,如果是单机的话可能会出现部分问题.首先设置两台计算机的主机名 root 权限打开/etc/host文件 再设置hostname,root权限打开/etc/hos ...
- js对象的深度克隆
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- iOS---XMPP环境搭建过程
什么是即时通信? 即时通信是目前Internet上最为流行的通讯方式, 各种各样的即时通讯软件也层出不穷, 服务提供商也提供了越来越枫木的通讯服务功能. 即时通讯有多重实现方式, XMPP就是其中一种 ...
- .NET/android/java/iOS AES通用加密解密(修正安卓)
移动端越来越火了,我们在开发过程中,总会碰到要和移动端打交道的场景,比如.NET和android或者iOS的打交道.为了让数据交互更安全,我们需要对数据进行加密传输.今天研究了一下,把几种语言的加密都 ...