ALS
基于 Audioscrobbler 数据集的音乐推荐 (pyspark)
根据用户播放次数数据使用协同过滤算法完成音乐推荐。
数据集
Audioscrobbler 数据集
下载 Audioscrobbler 数据集user_artist_data.txt
它包含 141000 个用户和 160 万个艺术家,记录了约 2420 万条用户播放艺术家歌曲的信息,其中包括播放次数信息。播放次数较多意味着该用户更喜欢对应艺术家的作品。
userid | artistid | playcount |
---|---|---|
用户 ID | 艺术家 ID | 播放次数 |
1000002 | 1 | 55 |
artist_data.txt
该文件包含两列: artistid artist_name 艺术家 ID 艺术家名字。文件中给出了每个艺术家的 ID 和对应的名字。此文件用于 ID 与名字的转换。
artistid | artist_name |
---|---|
艺术家 ID | 艺术家名 |
1134999 | 06Crazy Life |
artist_alias.txt
该文件包含两列: badid, goodid 坏 ID 好 ID 。该文件包含已知错误拼写的艺术家 ID 及其对应艺术家的正规的,用于将拼写错误的艺术家 ID 或 ID 变体对应到该艺术家正确的 ID。
badid | goodid |
---|---|
坏 ID | 好 ID |
1092764 | 1000311 |
算法
交替最小二乘推荐算法 (Alternating Least Squares,ALS)
人们虽然经常听音乐,但很少给音乐评分。因此 Audioscrobbler 数据集覆盖了更多的用户和艺术家,也包含了更多的总体信息,虽然单条记录的信息比较少。这种类型的数据通常被称为隐式反馈数据,因为用户和艺术家的关系是通过其他行动隐含体现出来的,而不是通过显式的评分或点赞得到的。
根据两个用户的相似行为判断他们有相同的偏好,学习算法不需要用户和艺术家的属性信息。这类算法通常称为协同过滤算法。
潜在因素模型:试图通过数据相对少的未被观察到的底层原因,来解释大量用户和产品之间可观察到的交互。因子分析方法背后的理论是,有关观测变量之间的相互依赖性的信息可以稍后用于减少数据集中的变量集。
矩阵分解模型:数学上,算法把用户和产品数据当成一个大矩阵 R,矩阵第 i 行和第 j 列上的元素有值,代表用户 i 播放过艺术家 j 的音乐。矩阵 R 是稀疏的:R 中大多数元素都是 0,因为相对于所有可能的用户 - 艺术家组合,只有很少一部分组合会出现在数据中。算法将 R 分解为两个小矩阵 U 和 P 的乘积。矩阵 U 和矩阵 P 非常 “瘦”。因为 A 有很多行和列,但 U 和 P 的行很多而列很少(列数用 k 表示)。这 k 个列就是潜在因素,用于解释数据中的交互关系。由于 k 的值小,矩阵分解算法只能是某种近似。
为了使低秩矩阵 P 和 U 尽可能的逼近 R,可以通过最小化如下损失函数 L 来完成。
损失函数公式与上图对应:
于是就简化为一个最小化损失函数 L 的优化问题。用户 - 特征矩阵
如果 P 已知,求 U 的最优解是非常容易的,反之亦然。但 P 和 U 事先都是未知的。
虽然 P 是未知的,但可以把 P 初始化为随机行向量矩阵。接着运用简单的线性代数,就能在给定 R 和 P 的条件下求出 U 的最优解。实际上,U 的第 i 行是 R 的第 i 行和 P 的函数。
因此可以很容易分开计算 U 的每一行。因为 U 的每一行可以分开计算,所以我们可以将其并行化,而并行化是大规模计算的一大优点。
ALS 是求解
随机对 P、Q 初始化,随后交替进行优化直到收敛。收敛标准是均方误差小于预定义阈值,或者到达最大迭代次数。
推荐质量评价指标 AUC
AUC 指标是一个 [0,1] 之间的实数,代表如果随机挑选一个正样本和一个负样本,分类算法将这个正样本排在负样本前面的概率。值越大,表示分类算法更有可能将正样本排在前面,也即算法准确性越好。
随机抽出一对样本(一个正样本,一个负样本),然后用训练得到的分类器来对这两个样本进行预测,预测得到正样本的概率大于负样本概率的概率。
在有 M 个正样本,N 个负样本的数据集里。一共有 M×N 对样本(一对样本,一个正样本与一个负样本)。统计这 M×N 对样本里,正样本的预测概率大于负样本的预测概率的个数。
其中,
实验过程
数据预处理
artist_data.txt 文件
数据最终处理成以逗号分割
artist_data.txt 文件
两列之间的间隔有的是空格有的是 Tab,第二列数据中包含空格
因第二列数据中含有逗号和空格,数据最终处理成以 Tab 分割
去除第一列不是数字的行
artist_alias.txt 文件
将拼写错误的艺术家 ID 或 ID 变体对应到该艺术家的规范 ID
两列之间的间隔有的是空格有的是 Tab
包含数据缺失的列
在数据处理时对拼写错误 ID 进行映射,用别名数据集将所有的艺术家 ID 转换成正规 ID。
aa={} |
预处理后得到的数据集
artist_data
user_artist_data
获取数据文件,并上传至 HDFS
读入数据,转换成 DataFrame 备用
from pyspark.sql.types import Row |
展示数据格式基本统计信息
数据格式
uaDF.show() |
基本统计信息
用户数
a=uaDF.select(uaDF.user).distinct().count() |
艺术家数目
b=uaDF.select(uaDF.item).distinct().count() |
每用户平均播放次数
uaDF.drop("item").groupBy("user").agg({"rating":"mean"}).show() |
每艺术家平均播放次数
uaDF.drop("user").groupBy("item").agg({"rating":"mean"}).show() |
构建 ALS 模型
构建 ALS 模型,并记录所耗时间。初始参数:Rank 10, maxiter 15, RegParm 0.01 Alpha 1.0。
from pyspark.ml.recommendation import ALS,ALSModel |
输出结果:
时间:785.1817960739136 |
这样我们就构建了一个 ALSModel 模型。
模型用两个不同的 DataFrame,它们分别表示 “用户 - 特征” 和 “产品 - 特征” 这两个大型矩阵。
检查推荐结果
依据构建的模型,选择部分 ID 检查推荐结果。
看看模型给出的艺术家推荐直观上是否合理,检查一下用户播放过的艺术家,然后看看模型向用户推荐的艺术家。具体来看看用户 2093760 的例子。
userID = 2093760 |
查看用户输出结果:
[ |
获取艺术家 ID:
artistid=[] |
输出结果:
[1180, 1255340, 378, 813, 942] |
要提取该用户收听过的艺术家 ID 并打印他们的名字,这意味着先在输入数据中搜索该用户收听过的艺术家的 ID,然后用这些 ID 对艺术家集合进行过滤,这样我们就可以获取并按序打印这些艺术家的名字:
b=aDF.rdd.filter(**lambda** x: x[0] **in** artistid).collect() |
输出结果:
[Row(id=1180, name='David Gray'), |
用户播放过的艺术家既有大众流行音乐风格的也有嘻哈风格的。
使用 Spark2.4.6 自带的 recommendForUserSubset 方法,对所有艺术家评分,并返回向用户 2093760 推荐其中分值最高的前 5 位。
d=sc.parallelize([(2093760,1)]).toDF(['user']) |
输出结果:
+-------+--------------------+ |
遍历打印一下:
t.select("recommendations").rdd.foreach(**lambda** x:**print**(x)) |
输出:
Row(recommendations=[ |
结果全部是嘻哈风格。能看出,这些推荐都不怎么样。虽然推荐的艺术家都受人欢迎,但好像并没有针对用户的收听习惯进行个性化。
训练 - 验证切分
训练 - 验证切分,采用初始参数,重新训练模型。
为了利用输入数据,需要把它分成训练集和验证集。训练集只用于训练 ALS 模型,验证集用于评估模型。这里将 90% 的数据用于训练,剩余的 10% 用于交叉验证:
train,test=uaDF.randomSplit([0.9,0.1]) |
计算 AUC
接受一个交叉验证集和一个预测函数,交叉验证集代表每个用户对应的 “正面的” 或 “好的” 艺术家。预测函数把每个包含 “用户 - 艺术家” 对的 DataFrame 转换为一个同时包含 “用户 - 艺术家” 和 “预测” 的 DataFrame,“预测” 表示 “用户” 与 “艺术家” 之间关联的强度值,这个值越高,代表推荐的排名越高。
allArtistIDs = uaDF.select("item").distinct().collect() |
输出结果:
Row(avg(auc)=0.9098560946043145) |
有必要把上述方法和一个更简单方法做一个基准比对。举个例子,考虑下面的推荐方法:向每个用户推荐播放最多的艺术家。这个策略一点儿都不个性化,但它很简单,也可能有效。定义这个简单预测函数并评估它的 AUC 得分:
def predictMostListened(data): |
输出结果:
Row(avg(auc)=0.9578054887285846) |
结果得分大约是 0.96。这意味着,对 AUC 这个指标,非个性化的推荐表现已经不错了。然而,我们想要的是得分更高,也就是更为 “个性化” 的推荐。显然这个模型还有待改进。调整超参数,使推荐结果更合理。
选择超参数
Rank 可选(5,30)RegParam 可选(4.0,0.0001),alpha 可选(1.0,40.0)。合计 8 种参数组合。
可以把 rank、regParam 和 alpha 看作模型的超参数。(maxIter 更像是对分解过程使用的资源的一种约束。)这些值不会体现在 ALSModel 的内部矩阵中,这些矩阵只是参数,其值由算法选定。超参数则是构建过程本身的参数。
def TrainALS(rank,regParam,alpha,dir): |
构建模型
dir=0 |
加载模型计算 AUC 得分:
try: |
输出结果:
(Row(avg(auc)=0.9122637924972641), (5, 4.0, 1.0)) |
可以看出 rank=30,regParam=4.0,alpha=40.0 时取得了最优的结果 avg (auc)=0.9275148741094371.
虽然这些值的绝对差很小,但对于 AUC 值来说,仍然具有一定的意义。有意思的是,参数 alpha 取 40 的时候看起来总是比取 1 表现好。这说明了模型在强调用户听过什么时的表现要比强调用户没听过什么时要好。
产生推荐
选取 10 个用户展示推荐结果
model=ALSModel.load("/model/ALS/Try2/6") |
输出推荐结果:
Row(recommendations=[Row(item=1010991, rating=1.1815389394760132), Row(item=1245226, rating=1.139704942703247), Row(item=4629, rating=1.1092422008514404), Row(item=1113701, rating=1.1066040992736816), Row(item=1019715, rating=1.10056471824646)]) |
按照用户顺序将最喜欢的推荐结果输出到文件
def getp(x): |
推荐输出详见 result 文件,以下为部分推荐输出:
Row(user=1000092, recommendations=[Row(item=1002400, rating=1.2386927604675293)]) |