Movie recommendations and more with Spark - Crouching Data, Hidden Markov

标签: | 发表时间:2018-08-04 20:25 | 作者:
出处:http://mlnick.github.io

文章的灵感来自 Edwin Chen关于Scalding的帖子。我鼓励你先阅读那篇文章!Spark代码改编自他的Scalding代码,可在 此处获得

正如Ed的帖子所述,Scalding是一个用于Hadoop MapReduce的Scala DSL,它使编写MapReduce工作流程变得更容易,更自然,更简洁。Scala代码最终通过 Cascading编译为MapReduce作业。

Scalding

Scalding 项目是群集计算框架,强调低延迟作业执行和内存中缓存,以提供速度。因此,它可以比Hadoop MapReduce(当所有数据都缓存在内存中)快100倍。它是用Scala编写的,但也有Java和Python API。它与HDFS和任何Hadoop完全兼容 InputFormat/OutputFormat,但独立于Hadoop MapReduce。

Spark API与Scalding有许多相似之处,提供了一种编写自然Scala代码的方法,而不是 MappersReducers。以Ed为例:

1 
2
        // Create a histogram of tweet lengths.
tweets.map('tweet -> 'length) { tweet : String => tweet.size }.groupBy('length) { _.size }
火花
1 
2
        // Create a histogram of tweet lengths.
tweets.groupBy(tweet : String => tweet.size).map(pair => (pair._1, pair._2.size))

电影相似度

我最近一直在用Spark进行很多实验,并认为将Ed的计算方法与Scalding和Spark中的电影相似性进行比较会很有意思。所以我把他的Scalding代码移植到了Spark上,我们将比较这两个代码。有关Spark API的基本介绍,请参阅 Spark Quickstart

首先,我们从文件中读取评级。由于我无法访问不错的Twitter推文数据源,因此我使用了 MovieLens 100k评级数据集。训练集评级在一个名为的文件中 ua.base,而电影项目数据在 u.item

1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23
        /**
 * The input is a TSV file with three columns: (user, movie, rating).
 */
val INPUT_FILENAME = "data/ratings.tsv"

/**
 * Read in the input and give each field a type and name.
 */
val ratings = Tsv(INPUT_FILENAME, ('user, 'movie, 'rating))

/**
 * Let's also keep track of the total number of people who rated each movie.
 */
val numRaters =
  ratings
    // Put the number of people who rated each movie into a field called "numRaters".    
    .groupBy('movie) { _.size }.rename('size -> 'numRaters)

// Merge `ratings` with `numRaters`, by joining on their movie fields.
val ratingsWithSize =
  ratings.joinWithSmaller('movie -> 'movie, numRaters)

// ratingsWithSize now contains the following fields: (user, movie, rating, numRaters).
火花
1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27
        val TRAIN_FILENAME = "ua.base"
val MOVIES_FILENAME = "u.item"

// Spark programs require a SparkContext to be initialized
val sc = new SparkContext(master, "MovieSimilarities")

// extract (userid, movieid, rating) from ratings data
val ratings = sc.textFile(TRAIN_FILENAME)
  .map(line => {
    val fields = line.split("\t")
    (fields(0).toInt, fields(1).toInt, fields(2).toInt)
})

// get num raters per movie, keyed on movie id
val numRatersPerMovie = ratings
  .groupBy(tup => tup._2)
  .map(grouped => (grouped._1, grouped._2.size))

// join ratings with num raters on movie id
val ratingsWithSize = ratings
  .groupBy(tup => tup._2)
  .join(numRatersPerMovie)
  .flatMap(joined => {
    joined._2._1.map(f => (f._1, f._2, f._3, joined._2._2))
})

// ratingsWithSize now contains the following fields: (user, movie, rating, numRaters).

与Scalding的 Tsv方法(从HDFS读取TSV文件)类似,Spark的 sc.textFile方法从HDFS读取文本文件。但是,由我们来指定如何拆分字段。

此外,Spark的连接API比Scalding的更低级别,因此我们必须 groupBy先进行转换,然后 join进行 flatMap操作以获取我们想要的字段。烫伤实际上做了类似的事情 joinWithSmaller

计算相似度

为了确定两部电影彼此之间的相似程度,我们必须(按照Ed的帖子):

  • 对于每对电影A和B,找到所有同时评价A和B的人。
  • 使用这些评级来形成电影A矢量和电影B矢量。
  • 计算这两个向量之间的相关性。
  • 每当有人观看电影时,您都可以推荐与其最相关的电影。

这是基于项目的协作过滤。那么让我们计算上面的前两个步骤:

1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20
        /**
 * To get all pairs of co-rated movies, we'll join `ratings` against itself.
 * So first make a dummy copy of the ratings that we can join against.
 */
val ratings2 =
  ratingsWithSize
    .rename(('user, 'movie, 'rating, 'numRaters) -> ('user2, 'movie2, 'rating2, 'numRaters2))

/**
 * Now find all pairs of co-rated movies (pairs of movies that a user has rated) by
 * joining the duplicate rating streams on their user fields, 
 */
val ratingPairs =
  ratingsWithSize
    .joinWithSmaller('user -> 'user2, ratings2)
    // De-dupe so that we don't calculate similarity of both (A, B) and (B, A).
    .filter('movie, 'movie2) { movies : (String, String) => movies._1 < movies._2 }
    .project('movie, 'rating, 'numRaters, 'movie2, 'rating2, 'numRaters2)

// By grouping on ('movie, 'movie2), we can now get all the people who rated any pair of movies.
火花
1 
2 
3 
4 
5 
6 
7 
8 
9
        // dummy copy of ratings for self join
val ratings2 = ratingsWithSize.keyBy(tup => tup._1)

// join on userid and filter movie pairs such that we don't double-count and exclude self-pairs
val ratingPairs =
  ratingsWithSize
  .keyBy(tup => tup._1)
  .join(ratings2)
  .filter(f => f._2._1._2 < f._2._2._2)

请注意API与功能操作的相似之处 filter- 它们各自只需要一个Scala闭包。然后,我们计算每个评级向量的各种向量度量(大小,点积,范数等)。我们将使用这些来计算电影对之间的各种相似度量。

1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21
        /**
 * Compute dot products, norms, sums, and sizes of the rating vectors.
 */
val vectorCalcs =
  ratingPairs
    // Compute (x*y, x^2, y^2), which we need for dot products and norms.
    .map(('rating, 'rating2) -> ('ratingProd, 'ratingSq, 'rating2Sq)) {
      ratings : (Double, Double) =>
      (ratings._1 * ratings._2, math.pow(ratings._1, 2), math.pow(ratings._2, 2))
    }
    .groupBy('movie, 'movie2) { group =>
        group.size // length of each vector
        .sum('ratingProd -> 'dotProduct)
        .sum('rating -> 'ratingSum)
        .sum('rating2 -> 'rating2Sum)
        .sum('ratingSq -> 'ratingNormSq)
        .sum('rating2Sq -> 'rating2NormSq)
        .max('numRaters) // Just an easy way to make sure the numRaters field stays.
        .max('numRaters2)
        // All of these operations chain together like in a builder object.
    }
火花
1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29
        // compute raw inputs to similarity metrics for each movie pair
val vectorCalcs =
  ratingPairs
  .map(data => {
    val key = (data._2._1._2, data._2._2._2)
    val stats =
      (data._2._1._3 * data._2._2._3, // rating 1 * rating 2
        data._2._1._3,                // rating movie 1
        data._2._2._3,                // rating movie 2
        math.pow(data._2._1._3, 2),   // square of rating movie 1
        math.pow(data._2._2._3, 2),   // square of rating movie 2
        data._2._1._4,                // number of raters movie 1
        data._2._2._4)                // number of raters movie 2
    (key, stats)
  })
  .groupByKey()
  .map(data => {
    val key = data._1
    val vals = data._2
    val size = vals.size
    val dotProduct = vals.map(f => f._1).sum
    val ratingSum = vals.map(f => f._2).sum
    val rating2Sum = vals.map(f => f._3).sum
    val ratingSq = vals.map(f => f._4).sum
    val rating2Sq = vals.map(f => f._5).sum
    val numRaters = vals.map(f => f._6).max
    val numRaters2 = vals.map(f => f._7).max
    (key, (size, dotProduct, ratingSum, rating2Sum, ratingSq, rating2Sq, numRaters, numRaters2))
  })

相似度量

对于每个电影对,我们计算 相关性正则化相关性余弦相似度Jaccard相似度(参见Ed的帖子和完整细节的代码)。

1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19
        val PRIOR_COUNT = 10
val PRIOR_CORRELATION = 0

val similarities =
  vectorCalcs
    .map(('size, 'dotProduct, 'ratingSum, 'rating2Sum, 'ratingNormSq, 'rating2NormSq, 'numRaters, 'numRaters2) ->
      ('correlation, 'regularizedCorrelation, 'cosineSimilarity, 'jaccardSimilarity)) {

      fields : (Double, Double, Double, Double, Double, Double, Double, Double) =>

      val (size, dotProduct, ratingSum, rating2Sum, ratingNormSq, rating2NormSq, numRaters, numRaters2) = fields

      val corr = correlation(size, dotProduct, ratingSum, rating2Sum, ratingNormSq, rating2NormSq)
      val regCorr = regularizedCorrelation(size, dotProduct, ratingSum, rating2Sum, ratingNormSq, rating2NormSq, PRIOR_COUNT, PRIOR_CORRELATION)
      val cosSim = cosineSimilarity(dotProduct, math.sqrt(ratingNormSq), math.sqrt(rating2NormSq))
      val jaccard = jaccardSimilarity(size, numRaters, numRaters2)

      (corr, regCorr, cosSim, jaccard)
    }
火花
1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19
        val PRIOR_COUNT = 10
val PRIOR_CORRELATION = 0

// compute similarity metrics for each movie pair
val similarities =
  vectorCalcs
  .map(fields => {

    val key = fields._1
    val (size, dotProduct, ratingSum, rating2Sum, ratingNormSq, rating2NormSq, numRaters, numRaters2) = fields._2

    val corr = correlation(size, dotProduct, ratingSum, rating2Sum, ratingNormSq, rating2NormSq)
    val regCorr = regularizedCorrelation(size, dotProduct, ratingSum, rating2Sum,
      ratingNormSq, rating2NormSq, PRIOR_COUNT, PRIOR_CORRELATION)
    val cosSim = cosineSimilarity(dotProduct, scala.math.sqrt(ratingNormSq), scala.math.sqrt(rating2NormSq))
    val jaccard = jaccardSimilarity(size, numRaters, numRaters2)

    (key, (corr, regCorr, cosSim, jaccard))
  })

这里的好处是,一旦原料输入度量自己计算,我们就可以使用完全相同的功能,从例子来计算的相似性指标如从上面可以看出-我简单地复制和粘贴埃德的 correlationregularizedCorrelationcosineSimilarityjaccardSimilarity功能!

一些结果

那么,将所有这些结合在一起后,结果会是什么样的?由于我使用了不同的输入数据源,我们不会得到相同的结果,但我们希望它们中的大多数都有意义。与Ed的结果类似,我发现使用raw会 correlation导致次优相似性(至少从眼球和“感觉检查”),因为一些电影对很少有共同的评价者(许多只有一个共同的评价者)。

我也发现 cosine similarity在“感觉检查”的基础上做得不好,这有点令人惊讶,因为这通常是协同过滤的标准相似性度量。这似乎是由于很多电影的余弦相似度为1.0,所以也许我搞砸了某个地方的计算(如果你发现错误请告诉我)。

无论如何,这里是与Die Hard(1998)最相似的前10部电影,排名依据 regularized correlation

电影1 电影2 关联 Reg Correlation 余弦相似度 Jaccard相似度
死硬(1988) 死硬:复仇(1995) 0.5413 0.4946 0.9692 0.4015
死硬(1988) 死硬2(1990) 0.4868 0.4469 0.9687 0.4088
死硬(1988) 香蕉(1971) 0.5516 0.4390 0.9745 0.1618
死硬(1988) 好,坏和丑,(1966) 0.4608 0.4032 0.9743 0.2518
死硬(1988) 寻找红十月,(1990) 0.4260 0.3944 0.9721 0.4098
死硬(1988) 城市切片机II:卷曲金的传说(1994) 0.5349 0.3903 0.9506 0.1116
死硬(1988) 油脂2(1982) 0.6502 0.3901 0.9449 0.0647
死硬(1988) 星际迷航:可汗之怒(1982) 0.4160 0.3881 0.9675 0.4441
死硬(1988) 球体(1998) 0.7722 0.3861 0.9893 0.0403
死硬(1988) 梦之场(1989) 0.4126 0.3774 0.9630 0.3375


看起来很合理!以下是与Il Postino最相似的10个:

电影1 电影2 关联 Reg Correlation 余弦相似度 Jaccard相似度
Postino,Il(1994) 瓶火箭(1996) 0.8789 0.4967 0.9855 0.0699
Postino,Il(1994) 寻找理查德(1996) 0.7112 0.4818 0.9820 0.1123
Postino,Il(1994) Ridicule(1996) 0.6550 0.4780 0.9759 0.1561
Postino,Il(1994) 当我们是国王(1996) 0.7581 0.4773 0.9888 0.0929
Postino,Il(1994) 母亲之夜(1996) 0.8802 0.4611 0.9848 0.0643
Postino,Il(1994) Kiss Me,Guido(1997) 0.9759 0.4337 0.9974 0.0452
Postino,Il(1994) 脸上的蓝色(1995) 0.6372 0.4317 0.9585 0.1148
Postino,Il(1994) 奥赛罗(1995) 0.5875 0.4287 0.9774 0.1330
Postino,Il(1994) 英国病人,(1996) 0.4586 0.4210 0.9603 0.2494
Postino,Il(1994) Mediterraneo(1991) 0.6200 0.4200 0.9879 0.1235


星球大战怎么样?

电影1 电影2 关联 Reg Correlation 余弦相似度 Jaccard相似度
星球大战(1977) 帝国反击战,(1980年) 0.7419 0.7168 0.9888 0.5306
星球大战(1977) 绝地归来(1983年) 0.6714 0.6539 0.9851 0.6708
星球大战(1977) 迷失方舟的攻略(1981) 0.5074 0.4917 0.9816 0.5607
星球大战(1977) 认识John Doe(1941) 0.6396 0.4397 0.9840 0.0442
星球大战(1977) 爱在下午(1957) 0.9234 0.4374 0.9912 0.0181
星球大战(1977) 年度人物(1995年) 1.0000 0.4118 0.9995 0.0141
星球大战(1977) 当我们是国王(1996) 0.5278 0.4021 0.9737 0.0637
星球大战(1977) 哭泣,心爱的国家(1995) 0.7001 0.3957 0.9763 0.0257
星球大战(1977) 成为或不成为(1942) 0.6999 0.3956 0.9847 0.0261
星球大战(1977) 爱德华·D·伍德的幽灵世界,(1995) 0.6891 0.3895 0.9758 0.0262


最后, 星球大战最 不相似的10个怎么样?

电影1 电影2 关联 Reg Correlation 余弦相似度 Jaccard相似度
星球大战(1977) 父亲节(1997年) -0.6625 -0.4417 0.9074 0.0397
星球大战(1977) 杰森的抒情诗(1994) -0.9661 -0.3978 0.8110 0.0141
星球大战(1977) 闪电杰克(1994) -0.7906 -0.3953 0.9361 0.0202
星球大战(1977) 标记为死亡(1990) -0.5922 -0.3807 0.8729 0.0361
星球大战(1977) 混合坚果(1994) -0.6219 -0.3731 0.8806 0.0303
星球大战(1977) Poison Ivy II(1995) -0.7443 -0.3722 0.7169 0.0201
星球大战(1977) 在感官境界(Ai no corrida)(1976) -0.8090 -0.3596 0.8108 0.0162
星球大战(1977) 发生了什么......(1994) -0.9045 -0.3392 0.8781 0.0121
星球大战(1977) 女性变态(1996) -0.8039 -0.3310 0.8670 0.0141
星球大战(1977) Celtic Pride(1996) -0.6062 -0.3175 0.8998 0.0220


我会留给你决定准确性。

结论和后续步骤

希望这能够体现Spark以及它如何以与Scalding和MapReduce非常相似的方式使用 - 具有HDFS兼容性,内存缓存功能,低延迟执行和其他分布式内存原语(如广播变量和累加器); 更不用说通过Scala / Spark控制台以及Java和Python API进行交互式分析了! 在这里查看文档,教程和示例。

从上面的代码片段中可以明显看出,Scalding的API在进行复杂的字段操作和连接时更加清晰,因为能够将命名字段设置为Scala  Symbols,例如  tweets.map('tweet -> 'length) { tweet : String => tweet.size }

Spark的API中缺少命名字段会导致一些混乱的元组解包并使跟踪哪些字段更复杂。这可能是Spark的一个有趣的潜在补充。

相关 [movie recommendations and] 推荐:

Movie recommendations and more with Spark - Crouching Data, Hidden Markov

- -
Edwin Chen关于Scalding的帖子. Spark代码改编自他的Scalding代码,可在. 正如Ed的帖子所述,Scalding是一个用于Hadoop MapReduce的Scala DSL,它使编写MapReduce工作流程变得更容易,更自然,更简洁. Scala代码最终通过 Cascading编译为MapReduce作业.

Movie Monkey – 本地电影管理工具 | 小众软件 > 媒体工具

- aki - 小众软件
Movie Monkey 是一款管理本地电影的软件,只要你设置好电影所在的目录, Movie Monkey 会帮你自动分类. 比如:科幻、剧情和动作等,并且还会自动下载电影封面和影片剧照(但都只有一张),方便你管理. 遗憾的是, Movie Monkey 对中文支持不太好,如果你硬盘里面的电影都是中文命名的话, Movie Monkey 会提示说无法辨认.

如何使用ALS计算获得item相似度 How to get similar item recommendations using ALS - Quora

- -
不幸的是,Spark ML不支持使用Matrix Factorization模型的item 相似性推荐. Spark不使用Matrix Factorization模型计算item相似度的原因只是该技术不计算item相似性,也不计算用户相似性矩阵. (MF会计算出结果用户因素和项目因素,但不会在这里详细介绍它.

Canon 将推出「EOS Movie」的全新单反相机:4K 录像、35mm 全幅传感器

- PS - Engadget 中国版
想不到 Canon 除了 C300 外,今天竟然还有更多 EOS Movie 产品线的摄影机即将推出的消息. 主站在发表会中看到了一部原型的数码单反相机,宣称将可适用电影影像及其他「高解析」数字制作的应用方向. 这台 DSLR 目前已知将配备 35mm 的全幅影像传感器并可录制 24fps 4K 画质的 Motion-JPEG.