zoukankan      html  css  js  c++  java
  • 推荐系统入门案例

    一)数据文件说明:

    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高级数据分析》

  • 相关阅读:
    leetcode 131. Palindrome Partitioning
    leetcode 526. Beautiful Arrangement
    poj 1852 Ants
    leetcode 1219. Path with Maximum Gold
    leetcode 66. Plus One
    leetcode 43. Multiply Strings
    pytorch中torch.narrow()函数
    pytorch中的torch.repeat()函数与numpy.tile()
    leetcode 1051. Height Checker
    leetcode 561. Array Partition I
  • 原文地址:https://www.cnblogs.com/fishjar/p/9389325.html
Copyright © 2011-2022 走看看