• 推荐系统入门案例


    一)数据文件说明:

    user_artist_data.txt
    3 columns: userid artistid playcount

    artist_data.txt
    2 columns: artistid artist_name

    artist_alias.txt
    2 columns: badid, goodid
    known incorrectly spelt artists and the correct artist id.
    you can correct errors in user_artist_data as you read it in using this file
    (we're not yet finished merging this data)


    二)数据准备:
    将文件:artist_alias.txt、artist_data.txt、user_artist_data.txt导入到hdfs:///tmp/data/profiledata目录下面

    利用 SparkContext 的 textFile 方法,将数据文
    件转换成 String 类型的 RDD:
    spark-shell --driver-memory 10g

    val rawUserArtistData = sc.textFile("hdfs:///tmp/data/profiledata/user_artist_data.txt")

    //计算数据量:24296858
    rawUserArtistData.count()

    方法 stats()
    返回统计信息对象,包括最大值和最小值,数据量

    rawUserArtistData.map(_.split(' ')(0).toDouble).stats()
    rawUserArtistData.map(_.split(' ')(1).toDouble).stats()

    //返回结果如下:
    org.apache.spark.util.StatCounter = (count: 24296858, mean: 1947573.265353, stdev: 496000.544975, max: 2443548.000000, min: 90.000000)


    现在 artist_data.txt 包含艺术家 ID 和名字,它们用制表符分隔。
    这里 span() 用第一个制表符将一行拆分成两部分,接着将第一部分解析为艺术家 ID,剩
    余部分作为艺术家的名字(去掉了空白的制表符)。文件里有少量行看起来是非法的:有
    些行没有制表符,有些行不小心加入了换行符。这些行会导致 NumberFormatException ,它
    们不应该有输出结果

    val rawArtistData = sc.textFile("hdfs:///tmp/data/profiledata/artist_data.txt")

    val artistByID = rawArtistData.map { line =>
    val (id, name) = line.span(_ != ' ')
    (id.toInt, name.trim)
    }

    然而, map() 函数要求对每个输入必须严格返回一个值,因此这里不能用这个函数。另一
    种可行的方法是用 filter() 方法删除那些无法解析的行,但这会重复解析逻辑。当需要将
    每个元素映射为零个、一个或更多结果时,我们应该使用 flatMap() 函数,因为它将每个
    输入对应的零个或多个结果组成的集合简单展开,然后放入到一个更大的 RDD 中。它可
    以和 Scala 集合一起使用,也可以和 Scala 的 Option 类一起使用。 Option 代表一个值可以
    不存在,有点儿像只有 1 或 0 的一个简单集合,1 对应子类 Some ,0 对应子类 None 。因此
    在以下代码中,虽然 flatMap 中的函数本可以简单返回一个空 List ,或一个只有一个元素
    的 List ,但使用 Some 和 None 更合理,这种方法简单明了。


    val artistByID = rawArtistData.flatMap { line =>
    val (id, name) = line.span(_ != ' ')
    if (name.isEmpty) {
    None
    } else {
    try {
    Some((id.toInt, name.trim))
    } catch {
    case e: NumberFormatException => None
    }
    }
    }

    artist_alias.txt 将拼写错误的艺术家 ID 或非标准的艺术家 ID 映射为艺术家的正规名字。其
    中每行有两个 ID,用制表符分隔。这个文件相对较小,有 200 000 个记录。有必要把它转
    成 Map 集合的形式,将“不良的”艺术家 ID 映射到“良好的”ID,而不是简单地把它作
    为包含艺术家 ID 二元组的 RDD。这里又有一点小问题:由于某种原因有些行没有艺术家
    的第一个 ID。这些行将被过滤掉

    val rawArtistAlias = sc.textFile("hdfs:///tmp/data/profiledata/artist_alias.txt")
    val artistAlias = rawArtistAlias.flatMap { line =>
    val tokens = line.split(' ')
    if (tokens(0).isEmpty) {
    None
    } else {
    Some((tokens(0).toInt, tokens(1).toInt))
    }
    }.collectAsMap()


    比如,第一条将 ID 6803336 映射为 1000010。接下来我们可以从包含艺术家名字的 RDD
    中进行查找:
    artistByID.lookup(6803336).head
    artistByID.lookup(1000010).head
    显然,这条记录将“Aerosmith (unplugged)” 映射为“Aerosmith”。


    三)构建第一个模型

    如果艺术家存在别名,取得艺术家别名,否则取得原始名字。

    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()

    最后,我们构建模型:
    val model = ALS.trainImplicit(trainData, 10, 5, 0.01, 1.0)

    特征向量是一个包含 10 个数值的数组,数
    组的打印形式原本是不可读的。代码用 mkString() 把向量翻译成可读的形式,在 Scala 中,
    mkString() 方法常用于把集合元素表示成以某种形式分隔的字符串。

    model.userFeatures.mapValues(_.mkString(", ")).first()


    四)逐个检查推荐结果

    应该看看模型给出的艺术家推荐直观上是否合理,我们检查一下用户播放过的艺术家,然
    后看看模型向用户推荐的艺术家。具体来看看用户 2093760 的例子。现在我们要提取该用
    户收听过的艺术家 ID 并打印他们的名字,这意味着先在输入数据中搜索该用户收听过的
    艺术家的 ID,然后用这些 ID 对艺术家集合进行过滤,这样我们就可以获取并按序打印这
    些艺术家的名字:

    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)
    ...
    David Gray
    Blackalicious
    Jurassic 5
    The Saw Doctors
    Xzibit

    我们可以对该用户作出 5 个推荐:
    val recommendations = model.recommendProducts(2093760, 5)
    recommendations.foreach(println)

    ...
    Rating(2093760,1300642,0.02833118412903932)
    Rating(2093760,2814,0.027832682960168387)
    Rating(2093760,1037970,0.02726611004625264)
    Rating(2093760,1001819,0.02716011293509426)
    Rating(2093760,4605,0.027118271894797333)


    结果由 Rating 对象组成,包括用户 ID(重复的)、艺术家 ID 和一个数值。虽然字段名称叫 rating ,但其实不是估计的得分。对这类 ALS 算法,它是一个在 0 到 1 之间的模糊值,
    值越大,推荐质量越好。它不是概率,但可以把它理解成对 0/1 值的一个估计,0 表示用
    户不喜欢播放艺术家的歌曲,1 表示喜欢播放艺术家的歌曲。
    得到所推荐艺术家的 ID 之后,就可以用类似的方法查到艺术家的名字:

    val recommendedProductIDs = recommendations.map(_.product).toSet
    artistByID.filter { case (id, name) =>
    recommendedProductIDs.contains(id)
    }.values.collect().foreach(println)
    ...
    Green Day
    Linkin Park
    Metallica
    My Chemical Romance
    System of a Down


    五)选择超参数
    ALS.trainImplicit() 的参数包括以下几个。
    • rank = 10
    模型的潜在因素的个数,即“用户 - 特征”和“产品 - 特征”矩阵的列数;一般来说,
    它也是矩阵的阶。
    • iterations = 5
    矩阵分解迭代的次数;迭代的次数越多,花费的时间越长,但分解的结果可能会更好。
    • lambda = 0.01
    标准的过拟合参数;值越大越不容易产生过拟合,但值太大会降低分解的准确度。
    • alpha = 1.0
    控制矩阵分解时,被观察到的“用户 - 产品”交互相对没被观察到的交互的权重。
    可以把 rank 、 lambda 和 alpha 看作为模型的超参数。( iterations 更像是对分解过程使用
    的资源的一种约束。)这些值不会体现在 MatrixFactorizationModel 的内部矩阵中,这些矩
    阵只是参数,其值由算法选定。而 rank 、 lambda 和 alpha 这几个超参数是构建过程本身的
    参数。
    刚才列表中给出的超参数值不一定是最优的。如何选择好的超参数值在机器学习中是个普
    遍性问题。最基本的方法是尝试不同值的组合并对每个组合评估某个指标,然后挑选指标
    值最好的组合。
    在下面的示例中,我们尝试了 8 中可能的组合: rank = 10 或 50、 lambda = 1.0 或 0.0001,
    以及 alpha = 1.0 或 40.0。这些值当然也是猜的,但它们能够覆盖很大范围的参数值。各
    种组合的结果按 AUC 得分从高到底排序:

    val evaluations =
    for (rank <- Array(10, 50);
    lambda <- Array(1.0, 0.0001);
    alpha <- Array(1.0, 40.0))
    yield {
    val model = ALS.trainImplicit(trainData, rank, 10, lambda, alpha)
    val auc = areaUnderCurve(cvData, bAllItemIDs, model.predict)
    ((rank, lambda, alpha), auc)
    }
    evaluations.sortBy(_._2).reverse.foreach(println)
    ...
    ((50,1.0,40.0),0.9776687571356233)
    ((50,1.0E-4,40.0),0.9767551668703566)
    ((10,1.0E-4,40.0),0.9761931539712336)
    ((10,1.0,40.0),0.976154587705189)
    ((10,1.0,1.0),0.9683921981896727)
    ((50,1.0,1.0),0.9670901331816745)
    ((10,1.0E-4,1.0),0.9637196892517722)
    ((50,1.0E-4,1.0),0.9543377999707536)

    有意思的是,参数 alpha 取 40 的时候看起来总是比取 1 表现好(为了满足读者的好奇,顺
    便提一下,40 是前面提到的最初 ALS 论文的默认值之一)。这说明了模型在强调用户听过
    什么时的表现要比强调用户没听过什么时要好。
    lambda 取较大的值看起来结果要稍微好一些。这表明模型有些受过拟合的影响,因此需要
    一个较大的 lambda 值以防止过度精确拟合每个用户的稀疏输入数据。
    lambda 取较大的值看起来结果要稍微好一些。这表明模型有些受过拟合的影响,因此需要
    一个较大的 lambda 值以防止过度精确拟合每个用户的稀疏输入数据
    特征值的个数影响不是很明显;在分数最高的组合和分数最低的组合中均出现特征值个
    数取 50 的情况,虽然分数绝对值变化也不大。这可能表示正确的特征值个数实际上比 50
    大,而特征值个数太小时无论特征值个数是多少区别都不大。
    当然我们可以重复上述过程,试试不同的取值范围或试试更多值。这是超参数选择的一种
    暴力方式。但是在当今这个世界,这种简单粗暴的方式变得相对可行:集群常常有几 TB
    内存,成百上千个核,Spark 之类的框架可以利用并行计算和内存来提高速度。
    严格来说,理解超参数的含义其实不是必需的,但知道这些值的典型范围有助于找到一个
    合适的参数空间开始搜索,这个空间不宜太大,也不能太小。

    六)小结:
    ALS 不是唯一的推荐引擎算法。目前它是 Spark MLlib 唯一支持的算法。但是,对于非
    隐含数据,MLlib 也支持一种 ALS 的变体,它的用法和 ALS 是一样的,不同之处在于模
    型用方法 ALS.train() 构建。它适用于给出评分数据而不是次数数据。比如,如果数据集
    是用户对艺术家的打分,值从 1 到 5,那么用这种变体就很合适。不同推荐方法返回的
    Rating 对象结果,其中 rating 字段是估计的打分。

    源于《Spark高级数据分析》

  • 相关阅读:
    数据库一直显示恢复中。。记录一则处理数据库异常的解决方法
    MSSQl分布式查询
    ASP.NET MVC中实现数据库填充的下拉列表 .
    理解浮点数的储存规则
    获取 "斐波那契数列" 的函数
    Int64 与 Currency
    学 Win32 汇编[33] 探讨 Win32 汇编的模块化编程
    学 Win32 汇编[34] 宏汇编(1)
    Delphi 中 "位" 的使用(2) 集合
    如何用弹出窗口显示进度 回复 "嘿嘿嘿" 的问题
  • 原文地址:https://www.cnblogs.com/fishjar/p/9389325.html
Copyright © 2020-2023  润新知