SparkMLlib—協同過濾推薦算法,電影推薦系統,物品喜好推薦



相關內容原文地址:
博客園:Lemon_Qin:MLlib-協同過濾
博客園:大數據和AI躺過的坑:Spark MLlib協同過濾算法
CSDN:Running_Tiger:Spark MLlib協同過濾推薦算法實現
CSDN:小俊同學:推薦算法+Spark MLlib代碼Demo



一、協同過濾

協同過濾(Collaborative Filtering,簡稱CF,WIKI上的定義是:簡單來說是利用某個興趣相投、擁有共同經驗之羣體的喜好來推薦感興趣的資訊給使用者,個人透過合作的機制給予資訊相當程度的迴應(如評分)並記錄下來以達到過濾的目的,進而幫助別人篩選資訊,迴應不一定侷限於特別感興趣的,特別不感興趣資訊的紀錄也相當重要。

協同過濾常被應用於推薦系統。這些技術旨在補充用戶—商品關聯矩陣中所缺失的部分。

MLlib 當前支持基於模型的協同過濾,其中用戶和商品通過一小組隱性因子進行表達,並且這些因子也用於預測缺失的元素。MLLib 使用交替最小二乘法(ALS) 來學習這些隱性因子。

協同過濾是推薦系統的常用方法。可以填充user-item相關矩陣中的缺失值。MLlib支持基於模型的協同過濾,即使用能夠預測缺失值的一個隱藏因素集合來表示用戶和產品。MLlib使用交替做小二乘法(alternating least squares, ALS)學習隱藏因子。MLlib算法中的參數如下:

  • numBlocks 並行計算的block數(-1爲自動配置)
  • rank 模型中隱藏因子數
  • iterations 算法迭代次數
  • lambda ALS中的正則化參數
  • implicitPrefs 使用顯示反饋ALS變量或隱式反饋
  • alpha ALS隱式反饋變化率用於控制 the baseline confidence in preference observations

在這裏插入圖片描述

1.1 顯示vs隱式反饋

基於矩陣分解的協同過濾的標準方法一般將用戶商品矩陣中的元素作爲用戶對商品的顯性偏好。
用戶對物品或者信息的偏好,根據應用本身的不同,可能包括用戶對物品的評分、用戶查看物品的記錄、用戶的購買記錄等。其實這些用戶的偏好信息可以分爲兩類:

  • 顯式的用戶反饋:類是用戶在網站上自然瀏覽或者使用網站以外,顯式地提供反饋信息,例如用戶對物品的評分或者對物品的評論。
  • 這類是用戶在使用網站是產生的數據,隱式地反映了用戶對物品的喜好,例如用戶購買了某物品,用戶查看了某物品的信息,等等。

真實的案例中通常只有隱式反饋(例如,查看,點擊,購買,喜歡,分享等)。

顯式的用戶反饋能準確地反映用戶對物品的真實喜好,但需要用戶付出額外的代價;而隱式的用戶行爲,通過一些分析和處理,也能反映用戶的喜好,只是數據不是很精確,有些行爲的分析存在較大的噪音。但只要選擇正確的行爲特徵,隱式的用戶反饋也能得到很好的效果,只是行爲特徵的選擇可能在不同的應用中有很大的不同,例如在電子商務的網站上,購買行爲其實就是一個能很好表現用戶喜好的隱式反饋。

推薦引擎根據不同的推薦機制可能用到數據源中的一部分,然後根據這些數據,分析出一定的規則或者直接對用戶對其他物品的喜好進行預測計算。這樣推薦引擎可以在用戶進入時給他推薦他可能感興趣的物品。

1.2 實例介紹

將使用協同過濾算法對GroupLens Research(http://grouplens.org/datasets/movielens/)提供的數據進行分析,該數據爲一組從20世紀90年末到21世紀初由MovieLens用戶提供的電影評分數據,這些數據中包括電影評分、電影元數據(風格類型和年代)以及關於用戶的人口統計學數據(年齡、郵編、性別和職業等)。根據不同需求該組織提供了不同大小的樣本數據,不同樣本信息中包含三種數據:評分、用戶信息和電影信息。

對這些數據分析進行如下步驟:

  1. 裝載如下兩種數據:
    • a)裝載樣本評分數據,其中最後一列時間戳除10的餘數作爲key,Rating爲值;
    • b)裝載電影目錄對照表(電影ID->電影標題)
  2. 將樣本評分表以key值切分成3個部分,分別用於訓練 (60%,並加入用戶評分), 校驗 (20%), and 測試 (20%)
  3. 訓練不同參數下的模型,並再校驗集中驗證,獲取最佳參數下的模型
  4. 用最佳模型預測測試集的評分,計算和實際評分之間的均方根誤差
  5. 根據用戶評分的數據,推薦前十部最感興趣的電影(注意要剔除用戶已經評分的電影)

1.2.1 數據說明

在MovieLens提供的電影評分數據分爲三個表:評分、用戶信息和電影信息,在該系列提供的附屬數據提供大概6000位讀者和100萬個評分數據,具體位置爲/data/class8/movielens/data目錄下,對三個表數據說明可以參考該目錄下README文檔。

評分數據說明(ratings.data)

該評分數據總共四個字段,格式爲UserID::MovieID::Rating::Timestamp,分爲爲用戶編號::電影編號::評分::評分時間戳,其中各個字段說明如下:

  • 用戶編號範圍1~6040
  • 電影編號1 -3952
  • 電影評分爲五星評分,範圍0~5
  • 評分時間戳單位秒
  • 每個用戶至少有20個電影評分

ratings.dat的數據樣本如下所示:

1::1193::5::978300760
1::661::3::978302109
1::914::3::978301968
1::3408::4::978300275
1::2355::5::978824291
1::1197::3::978302268
1::1287::5::978302039
1::2804::5::978300719

用戶信息(users.dat)

用戶信息五個字段,格式爲UserID::Gender::Age::Occupation::Zip-code,分爲爲用戶編號::性別::年齡::職業::郵編,其中各個字段說明如下:

  • 用戶編號範圍1~6040
  • 性別,其中M爲男性,F爲女性
  • 不同的數字代表不同的年齡範圍,如:25代表25~34歲範圍
  • 職業信息,在測試數據中提供了21中職業分類
  • 地區郵編

users.dat的數據樣本如下所示:

1::F::1::10::48067
2::M::56::16::70072
3::M::25::15::55117
4::M::45::7::02460
5::M::25::20::55455
6::F::50::9::55117
7::M::35::1::06810
8::M::25::12::11413

電影信息(movies.dat)

電影數據分爲三個字段,格式爲MovieID::Title::Genres,分爲爲電影編號::電影名::電影類別,其中各個字段說明如下:

  • 電影編號1~3952
  • 由IMDB提供電影名稱,其中包括電影上映年份
  • 電影分類,這裏使用實際分類名非編號,如:Action、Crime等

movies.dat的數據樣本如下所示:

1::Toy Story (1995)::Animation|Children’s|Comedy
2::Jumanji (1995)::Adventure|Children’s|Fantasy
3::Grumpier Old Men (1995)::Comedy|Romance
4::Waiting to Exhale (1995)::Comedy|Drama
5::Father of the Bride Part II (1995)::Comedy
6::Heat (1995)::Action|Crime|Thriller
7::Sabrina (1995)::Comedy|Romance
8::Tom and Huck (1995)::Adventure|Children’s

程序代碼

import java.io.File
import scala.io.Source
import org.apache.log4j.{Level, Logger}
import org.apache.spark.SparkConf
import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._
import org.apache.spark.rdd._
import org.apache.spark.mllib.recommendation.{ALS, Rating, MatrixFactorizationModel}

object MovieLensALS {
  def main(args: Array[String]) {

    // 屏蔽不必要的日誌顯示在終端上
    Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
    Logger.getLogger("org.eclipse.jetty.server").setLevel(Level.OFF)

    if (args.length != 2) {
      println("Usage: /path/to/spark/bin/spark-submit --driver-memory 2g --class week7.MovieLensALS " +
        "week7.jar movieLensHomeDir personalRatingsFile")
      sys.exit(1)
    }

    // 設置運行環境
    val conf = new SparkConf().setAppName("MovieLensALS").setMaster("local[4]")
    val sc = new SparkContext(conf)

    // 裝載用戶評分,該評分由評分器生成
    val myRatings = loadRatings(args(1))
    val myRatingsRDD = sc.parallelize(myRatings, 1)

     // 樣本數據目錄
    val movieLensHomeDir = args(0)

     // 裝載樣本評分數據,其中最後一列Timestamp取除10的餘數作爲key,Rating爲值,即(Int,Rating)
    val ratings = sc.textFile(new File(movieLensHomeDir, "ratings.dat").toString).map { line =>
      val fields = line.split("::")
      (fields(3).toLong % 10, Rating(fields(0).toInt, fields(1).toInt, fields(2).toDouble))
    }

     // 裝載電影目錄對照表(電影ID->電影標題)
    val movies = sc.textFile(new File(movieLensHomeDir, "movies.dat").toString).map { line =>
      val fields = line.split("::")
      (fields(0).toInt, fields(1))
    }.collect().toMap

     val numRatings = ratings.count()
    val numUsers = ratings.map(_._2.user).distinct().count()
    val numMovies = ratings.map(_._2.product).distinct().count()

    println("Got " + numRatings + " ratings from " + numUsers + " users on " + numMovies + " movies.")

     // 將樣本評分表以key值切分成3個部分,分別用於訓練 (60%,並加入用戶評分), 校驗 (20%), and 測試 (20%)
    // 該數據在計算過程中要多次應用到,所以cache到內存
    val numPartitions = 4
    val training = ratings.filter(x => x._1 < 6)
      .values
      .union(myRatingsRDD) //注意ratings是(Int,Rating),取value即可
      .repartition(numPartitions)
      .cache()

    val validation = ratings.filter(x => x._1 >= 6 && x._1 < 8)
      .values
      .repartition(numPartitions)
      .cache()

    val test = ratings.filter(x => x._1 >= 8).values.cache()

    val numTraining = training.count()
    val numValidation = validation.count()
    val numTest = test.count()

    println("Training: " + numTraining + ", validation: " + numValidation + ", test: " + numTest)

    // 訓練不同參數下的模型,並在校驗集中驗證,獲取最佳參數下的模型
    val ranks = List(8, 12)
    val lambdas = List(0.1, 10.0)
    val numIters = List(10, 20)

    var bestModel: Option[MatrixFactorizationModel] = None
    var bestValidationRmse = Double.MaxValue
    var bestRank = 0
    var bestLambda = -1.0
    var bestNumIter = -1
    for (rank <- ranks; lambda <- lambdas; numIter <- numIters) {
      val model = ALS.train(training, rank, numIter, lambda)
      val validationRmse = computeRmse(model, validation, numValidation)
      println("RMSE (validation) = " + validationRmse + " for the model trained with rank = "
        + rank + ", lambda = " + lambda + ", and numIter = " + numIter + ".")
      if (validationRmse < bestValidationRmse) {
        bestModel = Some(model)
        bestValidationRmse = validationRmse
        bestRank = rank
        bestLambda = lambda
        bestNumIter = numIter
      }
    }

    // 用最佳模型預測測試集的評分,並計算和實際評分之間的均方根誤差
    val testRmse = computeRmse(bestModel.get, test, numTest)

     println("The best model was trained with rank = " + bestRank + " and lambda = " + bestLambda  + ", and numIter = " + bestNumIter + ", and its RMSE on the test set is " + testRmse + ".")

     // create a naive baseline and compare it with the best model
    val meanRating = training.union(validation).map(_.rating).mean

    val baselineRmse =

      math.sqrt(test.map(x => (meanRating - x.rating) * (meanRating - x.rating)).mean)

    val improvement = (baselineRmse - testRmse) / baselineRmse * 100

    println("The best model improves the baseline by " + "%1.2f".format(improvement) + "%.")

     // 推薦前十部最感興趣的電影,注意要剔除用戶已經評分的電影
    val myRatedMovieIds = myRatings.map(_.product).toSet
    val candidates = sc.parallelize(movies.keys.filter(!myRatedMovieIds.contains(_)).toSeq)
    val recommendations = bestModel.get
      .predict(candidates.map((0, _)))
      .collect()
      .sortBy(-_.rating)
      .take(10)

    var i = 1
    println("Movies recommended for you:")
    recommendations.foreach { r =>
      println("%2d".format(i) + ": " + movies(r.product))
      i += 1
    }

  sc.stop()
  }

  /** 校驗集預測數據和實際數據之間的均方根誤差 **/
  def computeRmse(model: MatrixFactorizationModel, data: RDD[Rating], n: Long): Double = {
    val predictions: RDD[Rating] = model.predict(data.map(x => (x.user, x.product)))
    val predictionsAndRatings = predictions.map(x => ((x.user, x.product), x.rating))
      .join(data.map(x => ((x.user, x.product), x.rating)))
      .values
    math.sqrt(predictionsAndRatings.map(x => (x._1 - x._2) * (x._1 - x._2)).reduce(_ + _) / n)
  }

  /** 裝載用戶評分文件 **/
  def loadRatings(path: String): Seq[Rating] = {
    val lines = Source.fromFile(path).getLines()
    val ratings = lines.map { line =>
      val fields = line.split("::")
      Rating(fields(0).toInt, fields(1).toInt, fields(2).toDouble)
    }.filter(_.rating > 0.0)
    if (ratings.isEmpty) {
      sys.error("No ratings provided.")
    } else {
      ratings.toSeq
    }
  }
}

二、協同過濾推薦算法——推薦系統代碼

2.1 訓練數據

1,1,1
1,2,1
2,1,1
2,3,1
3,3,1
3,4,1
4,2,1
4,4,1
5,1,1
5,2,1
5,3,1
6,4,1

2.2 實戰代碼

相似度計算類ItemSimilarity,支持同現相似度、餘弦相似度、歐氏距離相似度。

import scala.math._
import org.apache.spark.rdd.RDD

/**
  * 物品相似度計算類
  * 通過設置模型參數後執行Similarity方法,進行相似度計算,返回物品與物品相似度RDD
  * 相似度計算支持:同現相似度、餘弦相似度、歐氏距離相似度
  *
  */

/**
  * 用戶評分
  *
  * @param userid 用戶
  * @param itemid 評分物品
  * @param pref   評分
  */
case class ItemPref(
                     val userid: String,
                     val itemid: String,
                     val pref: Double
                   ) extends Serializable

/**
  * 用戶推薦
  *
  * @param userid 用戶
  * @param itemid 推薦物品
  * @param pref   評分
  */
case class UserRecomm(
                       val userid: String,
                       val itemid: String,
                       val pref: Double
                     ) extends Serializable

/**
  * 相似度
  *
  * @param itemid1 物品
  * @param itemid2 物品
  * @param similar 相似度
  */
case class ItemSimi(
                     val itemid1: String,
                     val itemid2: String,
                     val similar: Double
                   ) extends Serializable

/**
  * 相似度計算
  * 支持同現相似度、餘弦相似度、歐氏距離相似度
  */
class ItemSimilarity extends Serializable {

  /**
    * 相似度計算
    *
    * @param user_rdd 用戶評分
    * @param stype    計算相似度方式
    * @return 返回物品相似度
    */
  def Similarity(user_rdd: RDD[ItemPref], stype: String): (RDD[ItemSimi]) = {
    val simil_rdd = stype match {
      case "cooccurrence" => ItemSimilarity.CooccurrenceSimilarity(user_rdd)
      case "cosine" => ItemSimilarity.CosineeSimilarity(user_rdd)
      case "euclidean" => ItemSimilarity.EuclideanDistanceSimilarity(user_rdd)
      case _ => ItemSimilarity.CooccurrenceSimilarity(user_rdd)
    }
    simil_rdd
  }

}

object ItemSimilarity {
  /**
    * 同現相似度矩陣計算
    * w(i,j)=N(i)∩N(j)/sqrt(N(i)*N(j))
    *
    * @param user_rdd 用戶評分
    * @return 返回物品相似度
    */
  def CooccurrenceSimilarity(user_rdd: RDD[ItemPref]): (RDD[ItemSimi]) = {
    //1.數據準備
    val user_rdd1: RDD[(String, String, Double)] = user_rdd.map(f => (f.userid, f.itemid, f.pref))
    val user_rdd2: RDD[(String, String)] = user_rdd1.map(f => (f._1, f._2))
    //2.(用戶,物品)笛卡爾積操作=>物品與物品組合
    val user_rdd3: RDD[(String, (String, String))] = user_rdd2.join(user_rdd2)
    val user_rdd4: RDD[((String, String), Int)] = user_rdd3.map(f => (f._2, 1))
    //3.(物品,物品,頻次)
    val user_rdd5: RDD[((String, String), Int)] = user_rdd4.reduceByKey((x, y) => x + y)
    //4.對角矩陣
    val user_rdd6: RDD[((String, String), Int)] = user_rdd5.filter(f => f._1._1 == f._1._2)
    //5.非對角矩陣
    val user_rdd7: RDD[((String, String), Int)] = user_rdd5.filter(f => f._1._1 != f._1._2)
    //6.計算同現相似度(物品1,物品2,同現頻次)
    val user_rdd8: RDD[(String, ((String, String, Int), Int))] = user_rdd7.
      map(f => (f._1._1, (f._1._1, f._1._2, f._2))).join(user_rdd6.map(f => (f._1._1, f._2)))
    val user_rdd9: RDD[(String, (String, String, Int, Int))] = user_rdd8.map(f => (f._2._1._2, (f._2._1._1, f._2._1._2, f._2._1._3, f._2._2)))
    val user_rdd10: RDD[(String, ((String, String, Int, Int), Int))] = user_rdd9.join(user_rdd6.map(f => (f._1._1, f._2)))
    val user_rdd11: RDD[(String, String, Int, Int, Int)] = user_rdd10.map(f => (f._2._1._1, f._2._1._2, f._2._1._3, f._2._1._4, f._2._2))
    val user_rdd12: RDD[(String, String, Double)] = user_rdd11.map(f => (f._1, f._2, (f._3 / sqrt(f._4 * f._5))))
    //7.結果返回
    user_rdd12.map(f => ItemSimi(f._1, f._2, f._3))
  }

  /**
    * 餘弦相似度矩陣計算
    * T(x,y)=∑x(i)y(i)/sqrt(∑(x(i)*y(i))*∑(y(i)*y(i)))
    *
    * @param user_rdd 用戶評分
    * @return 返回物品相似度
    */
  def CosineeSimilarity(user_rdd: RDD[ItemPref]): (RDD[ItemSimi]) = {
    //1.數據準備
    val user_rdd1: RDD[(String, String, Double)] = user_rdd.map(f => (f.userid, f.itemid, f.pref))
    val user_rdd2: RDD[(String, (String, Double))] = user_rdd1.map(f => (f._1, (f._2, f._3)))
    //2.(用戶,物品,評分)笛卡爾積操作=>(物品1,物品2,評分1,評分2)組合
    val user_rdd3: RDD[(String, ((String, Double), (String, Double)))] = user_rdd2.join(user_rdd2)
    val user_rdd4: RDD[((String, String), (Double, Double))] = user_rdd3.map(f => ((f._2._1._1, f._2._2._1), (f._2._1._2, f._2._2._2)))
    //3.(物品1,物品2,評分1,評分2,)組合=>(物品1,物品2,評分1*評分2)組合並累加
    val user_rdd5: RDD[((String, String), Double)] = user_rdd4.map(f => (f._1, f._2._1 * f._2._2)).reduceByKey(_ + _)
    //4.對角矩陣
    val user_rdd6: RDD[((String, String), Double)] = user_rdd5.filter(f => f._1._1 == f._1._2)
    //5.非對角矩陣
    val user_rdd7: RDD[((String, String), Double)] = user_rdd5.filter(f => f._1._1 != f._1._2)
    //6.計算相似度
    val user_rdd8: RDD[(String, ((String, String, Double), Double))] = user_rdd7.map(f => (f._1._1, (f._1._1, f._1._2, f._2))).join(user_rdd6.map(f => (f._1._1, f._2)))
    val user_rdd9: RDD[(String, (String, String, Double, Double))] = user_rdd8.map(f => (f._2._1._2, (f._2._1._1, f._2._1._2, f._2._1._3, f._2._2)))
    val user_rdd10: RDD[(String, ((String, String, Double, Double), Double))] = user_rdd9.join(user_rdd6.map(f => (f._1._1, f._2)))
    val user_rdd11: RDD[(String, String, Double, Double, Double)] = user_rdd10.map(f => (f._2._1._1, f._2._1._2, f._2._1._3, f._2._1._4, f._2._2))
    val user_rdd12: RDD[(String, String, Double)] = user_rdd11.map(f => (f._1, f._2, (f._3 / sqrt(f._4 * f._5))))
    //7.結果返回
    user_rdd12.map(f => ItemSimi(f._1, f._2, f._3))
  }

  /**
    * 歐氏距離相似度矩陣計算
    * d(x,y)=sqrt(∑((x(i)-y(i))*(x(i)-y(i))))
    * sim(x,y)=n/(1+d(x,y))
    *
    * @param user_rdd 用戶評分
    * @return 返回物品相似度
    */
  def EuclideanDistanceSimilarity(user_rdd: RDD[ItemPref]): (RDD[ItemSimi]) = {
    //1.數據準備
    val user_rdd1: RDD[(String, String, Double)] = user_rdd.map(f => (f.userid, f.itemid, f.pref))
    val user_rdd2: RDD[(String, (String, Double))] = user_rdd1.map(f => (f._1, (f._2, f._3)))
    //2.(用戶,物品,評分)笛卡爾積操作=>(物品1,物品2,評分1,評分2)組合
    val user_rdd3: RDD[(String, ((String, Double), (String, Double)))] = user_rdd2 join user_rdd2
    val user_rdd4: RDD[((String, String), (Double, Double))] = user_rdd3.map(f => ((f._2._1._1, f._2._2._1), (f._2._1._2, f._2._2._2)))
    //3.(物品1,物品2,評分1,評分2)組合=>(物品1,物品2,評分1-評分2)組合並累加
    val user_rdd5: RDD[((String, String), Double)] = user_rdd4.map(f => (f._1, (f._2._1 - f._2._2) * (f._2._1 - f._2._2))).reduceByKey(_ + _)
    //4.(物品1,物品2,評分1,評分2)組合=>(物品1,物品2,1)組合計算物品1和物品2的重疊度
    val user_rdd6: RDD[((String, String), Int)] = user_rdd4.map(f => (f._1, 1)).reduceByKey(_ + _)
    //5.非對角矩陣
    val user_rdd7: RDD[((String, String), Double)] = user_rdd5.filter(f => f._1._1 != f._1._2)
    //6.相似度計算
    val user_rdd8: RDD[((String, String), (Double, Int))] = user_rdd7.join(user_rdd6)
    val user_rdd9: RDD[(String, String, Double)] = user_rdd8.map(f => (f._1._1, f._1._2, f._2._2 / (1 + sqrt(f._2._1))))
    //7.結果返回
    user_rdd9.map(f => ItemSimi(f._1, f._2, f._3))
  }

}

推薦計算類RecommendedItem,推薦計算根據物品相似度和用戶評分進行推薦物品計算,並過濾用戶已有物品及過濾最大過濾推薦數量。

import org.apache.spark.rdd.RDD
import scala.collection.mutable

/**
  * 物品推薦計算類
  * 通過設置模型參數,執行Recommend方法進行推薦計算,返回用戶的推薦物品RDD
  * 推薦計算根據物品相似度和用戶評分進行推薦物品計算,並過濾用戶已有物品及過濾最大過濾推薦數量
  */
class RecommendedItem {

  /**
    * 用戶推薦計算
    *
    * @param items_similar 物品相似度
    * @param user_prefer   用戶評分
    * @param r_number      推薦數量
    * @return 返回用戶推薦物品
    */
  def Recommend(items_similar: RDD[ItemSimi], user_prefer: RDD[ItemPref], r_number: Int): (RDD[UserRecomm]) = {
    //1.數據準備
    val rdd_app1_R1: RDD[(String, String, Double)] = items_similar.map(f => (f.itemid1, f.itemid2, f.similar))
    val user_prefer1: RDD[(String, String, Double)] = user_prefer.map(f => (f.userid, f.itemid, f.pref))
    //2.矩陣計算(i行j列join)
    val rdd_app1_R2: RDD[(String, ((String, Double), (String, Double)))] = rdd_app1_R1.map(f => (f._1, (f._2, f._3))).join(user_prefer1.map(f => (f._2, (f._1, f._3))))
    //3.矩陣計算(i行j列相乘)
    val rdd_app1_R3: RDD[((String, String), Double)] = rdd_app1_R2.map(f => ((f._2._2._1, f._2._1._1), f._2._2._2 * f._2._1._2))
    //4.矩陣計算(用戶:元素累加求和)
    val rdd_app1_R4: RDD[((String, String), Double)] = rdd_app1_R3.reduceByKey((x, y) => x + y)
    //5.矩陣計算(用戶:對結果過濾已有物品)
    val rdd_app1_R5: RDD[(String, (String, Double))] = rdd_app1_R4.leftOuterJoin(user_prefer1.map(f => ((f._1, f._2), 1))).filter(f => f._2._2.isEmpty).map(f => (f._1._1, (f._1._2, f._2._1)))
    //6.矩陣計算(用戶:用戶對結果排序,過濾)
    val rdd_app1_R6: RDD[(String, Iterable[(String, Double)])] = rdd_app1_R5.groupByKey()
    val rdd_app1_R7: RDD[(String, Iterable[(String, Double)])] = rdd_app1_R6.map(f => {
      val i2: mutable.Buffer[(String, Double)] = f._2.toBuffer
      val i2_2: mutable.Buffer[(String, Double)] = i2.sortBy(_._2)
      if (i2_2.length > r_number) i2_2.remove(0, (i2_2.length - r_number))
      (f._1, i2_2.toIterable)
    })
    val rdd_app1_R8: RDD[(String, String, Double)] = rdd_app1_R7.flatMap(f => {
      val id2: Iterable[(String, Double)] = f._2
      for (w <- id2) yield (f._1, w._1, w._2)
    })
    rdd_app1_R8.map(f => UserRecomm(f._1, f._2, f._3))
  }

  /**
    * 用戶推薦計算
    *
    * @param items_similar 物品相似度
    * @param user_prefer   用戶評分
    * @return 返回用戶推薦物品
    */
  def Recommend(items_similar: RDD[ItemSimi], user_prefer: RDD[ItemPref]): (RDD[UserRecomm]) = {
    //1.數據準備
    val rdd_app1_R1: RDD[(String, String, Double)] = items_similar.map(f => (f.itemid1, f.itemid2, f.similar))
    val user_prefer1: RDD[(String, String, Double)] = user_prefer.map(f => (f.userid, f.itemid, f.pref))
    //2.矩陣計算(i行和j列join)
    val rdd_app1_R2: RDD[(String, ((String, Double), (String, Double)))] = rdd_app1_R1.map(f => (f._1, (f._2, f._3))).join(user_prefer1.map(f => (f._2, (f._1, f._3))))
    //3.矩陣計算(i行j列元素相乘)
    val rdd_app1_R3: RDD[((String, String), Double)] = rdd_app1_R2.map(f => ((f._2._2._1, f._2._1._1), f._2._2._2 * f._2._1._2))
    //4.矩陣計算(用戶:元素累加求和)
    val rdd_app1_R4: RDD[((String, String), Double)] = rdd_app1_R3.reduceByKey((x, y) => x + y)
    //5.矩陣計算(用戶:對結果過濾已有物品)
    val rdd_app1_R5: RDD[(String, (String, Double))] = rdd_app1_R4.leftOuterJoin(user_prefer1.map(f => ((f._1, f._2), 1))).filter(f => f._2._2.isEmpty).map(f => (f._1._1, (f._1._2, f._2._1)))
    //6.矩陣計算(用戶:用戶對結果排序,過濾)
    val rdd_app1_R6: RDD[(String, String, Double)] = rdd_app1_R5.map(f => (f._1, f._2._1, f._2._2)).sortBy(f => (f._1, f._3))
    rdd_app1_R6.map(f => UserRecomm(f._1, f._2, f._3))
  }

}

基於物品推薦類ItemCF。

package itemCF

import org.apache.log4j.{Level, Logger}
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

//基於物品推薦
object ItemCF {
  def main(args: Array[String]): Unit = {
    //1.構建Spark對象
    val conf: SparkConf = new SparkConf().setAppName("ItemCF").setMaster("local[2]")
    val sc = new SparkContext(conf)
    Logger.getRootLogger.setLevel(Level.WARN)

    //2.讀取數據
    val data_path = "hdfs://node-1:9000/spark_data/sample_itemCF.txt"
    val data: RDD[String] = sc.textFile(data_path)
    val user_data: RDD[ItemPref] = data.map(_.split(",")).map(f => (ItemPref(f(0), f(1), f(2).toDouble))).cache()

    //3.建立模型
    val mysimil = new ItemSimilarity()
    val simil_rdd1: RDD[ItemSimi] = mysimil.Similarity(user_data,"cooccurrence")
    val recommd = new RecommendedItem
    val recommd_rdd1: RDD[UserRecomm] = recommd.Recommend(simil_rdd1,user_data,30)

    //4.打印結果
    println(s"物品相似度矩陣(物品i,物品j,相似度):${simil_rdd1.count()}")
    simil_rdd1.collect().foreach{ ItemSimi =>
      println(ItemSimi.itemid1+","+ItemSimi.itemid2+","+ItemSimi.similar)
    }
    println(s"用戶推薦列表(用戶,物品,推薦值):${recommd_rdd1.count()}")
    recommd_rdd1.collect().foreach{
      UserRecomm=> println(UserRecomm.userid+","+UserRecomm.itemid+","+UserRecomm.pref)
    }

    sc.stop()
  }
}

2.3 運行結果(親測可行)

物品相似度矩陣(物品i,物品j,相似度)10
2,4,0.3333333333333333
3,4,0.3333333333333333
4,2,0.3333333333333333
3,2,0.3333333333333333
1,2,0.6666666666666666
4,3,0.3333333333333333
2,3,0.3333333333333333
1,3,0.6666666666666666
2,1,0.6666666666666666
3,1,0.6666666666666666
用戶推薦列表(用戶,物品,推薦值)11
4,3,0.6666666666666666
4,1,0.6666666666666666
6,2,0.3333333333333333
6,3,0.3333333333333333
2,4,0.3333333333333333
2,2,1.0
5,4,0.6666666666666666
3,2,0.6666666666666666
3,1,0.6666666666666666
1,4,0.3333333333333333
1,3,1.0

Process finished with exit code 0

三、Spark MLlib推薦算法

訓練數據:

196    242    3    881250949
186    302    3    891717742
22    377    1    878887116
244    51    2    880606923
166    346    1    886397596
298    474    4    884182806
115    265    2    881171488
253    465    5    891628467
import org.apache.spark.mllib.recommendation.{ALS, Rating}
import org.apache.spark.{SparkConf, SparkContext}
/**
  * 協同過濾最小二乘法demo,基於用戶推薦:根據用戶的相似度來爲某個用戶推薦物品
  */
object UserCFDemo {
 
  /**
    * 解析String,獲取Rating
    * @param str
    * @return
    */
  def parseRating(str:String):Rating={
    val fields = str.split("\t")
    Rating(fields(0).toInt,fields(1).toInt,fields(2).toDouble)
  }
 
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("UserCFDemo").setMaster("local[*]")
    val sc = new SparkContext(conf)
    //用戶評價數據:用戶ID  影片ID  星級  時間戳
    val ratingData = sc.textFile("E:\\test\\ml-100k\\u.data")
    //讀取數據,生成RDD並轉換成Rating對象
    val ratingsRDD = ratingData.map(parseRating(_))
    //隱藏因子數(也即隱藏特徵,理論講因子數越多效果越好,但太多也會造成訓練模型和保存時所需的內存開銷,所以一般可取50~200之間)
    val rank=50
    //最大迭代次數(每次迭代都能降低評價矩陣的重建誤差,但一般經少數次迭代後ALS模型已能收斂成一個比較合理的好模型)
    val maxIter=10
    //正則化因子(該參數控制模型的正則化過程,從而控制模型的過擬合情況)
    val lambda=0.01
    //訓練模型
    val model = ALS.train(ratingsRDD,rank,maxIter,lambda)
 
    //從電影ID到標題的映射
    val movies = sc.textFile("E:\\test\\ml-100k\\u.item")
    val titles = movies.map(line=>line.split("\\|")).map(array=>(array(0).toInt,array(1))).collectAsMap()
 
    //推薦物品(電影)數量
    val K=10
    //用戶1
    val user1=66
    //推薦結果
    val topKRecs = model.recommendProducts(user1,K)
    println("用戶"+user1)
    topKRecs.foreach(rec=>{
      val movie = titles(rec.product)
      val rating = rec.rating
      println(s"推薦電影:$movie ,預測評分:$rating")
    })
  }
}

上面代碼假設爲用戶ID=66的用戶推薦10部電影,推薦結果以及該用戶對推薦電影的預測評分,並按預測評分從高到低排序如下所示:
在這裏插入圖片描述

四、基於物品的Spark MLlib代碼

訓練數據:

196    242    3    881250949
186    302    3    891717742
22    377    1    878887116
244    51    2    880606923
166    346    1    886397596
298    474    4    884182806
115    265    2    881171488
253    465    5    891628467
import org.apache.spark.mllib.recommendation.{ALS, Rating}
import org.apache.spark.{SparkConf, SparkContext}
import org.jblas.DoubleMatrix
/**
  * 協同過濾demo2,基於物品的推薦:根據物品的相似度給某個用戶推薦物品
  */
object ItemCFDemo {
 
  def parseRating(str:String):Rating={
    val fields = str.split("\t")
    Rating(fields(0).toInt,fields(1).toInt,fields(2).toDouble)
  }
 
  /**
    * 計算兩個向量的餘弦相似度,1爲最相似,0爲不相似,-1爲相反
    * 餘弦相似度=向量的點積/各向量範數的乘積     值域爲[-1,1]
    * @param vec1 向量1
    * @param vec2 向量2
    * @return
    */
  def cosineSimilarity(vec1:DoubleMatrix,vec2:DoubleMatrix)={
    vec1.dot(vec2)/(vec1.norm2()*vec2.norm2())
  }
 
 
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[*]").setAppName("ItemCFDemo")
    val sc = new SparkContext(conf)
    //構建ALS模型
    val ratingData = sc.textFile("E:\\test\\ml-100k\\u.data")
    val ratingsRDD = ratingData.map(parseRating(_))
    //四個參數:評級RDD、
    val model = ALS.train(ratingsRDD,50,10,0.01)
 
    //從電影ID到標題的映射
    val movies = sc.textFile("E:\\test\\ml-100k\\u.item")
    val titles = movies.map(line=>line.split("\\|")).map(array=>(array(0).toInt,array(1))).collectAsMap()
 
    //獲取給定物品在模型中對應的因子,並構建成向量
    val itemId=567
    val itemFactor: Array[Double] = model.productFeatures.lookup(itemId).head
    val itemVector = new DoubleMatrix(itemFactor)
 
    //求各個物品的餘弦相似度
    val sims = model.productFeatures.map {
      case (id, factor) =>
        val factorVector = new DoubleMatrix(factor)
        val sim = cosineSimilarity(factorVector, itemVector)
        (id, sim)
    }
    //取出餘弦相似度最高的10個,即爲跟給定物品最相似的10種物品
    val sortedSims: Array[(Int, Double)] = sims.top(10)(Ordering.by[(Int,Double),Double]{case (id,similarity)=>similarity})
 
    println("與"+titles(itemId)+"最爲相似的10部電影:")
    sortedSims.map{case (id,sim)=>(titles(id),sim)}.foreach(tuple=>println("電影:"+tuple._1+",相似度:"+tuple._2))
  }
}

基於物品的推薦,就要找出一樣相似的那些物品,如上代碼爲物品ID=567(代碼也實現了電影ID到電影名稱的映射,該電影名稱爲《Wes Craven’s New Nightmare》)的電影找出相似的10部電影,並按照相似度從高到低排序,輸出結果:

在這裏插入圖片描述

推薦模型效果的評估

如何知道訓練出來的模型是一個好模型?這就需要某種方法來評估它的預測結果。

評估指標(evaluation metric)指那些衡量模型預測能力或準確度的方法,提供了同一模型在不同參數下,又或是不同模型之間進行比較的標準方法。通過這些指標,人們可以從待選的模型中找出表現最好的那個模型。

均方差(Mean Squared Error,MSE)直接衡量“用戶-物品”評級矩陣的重建誤差。它也是一些模型裏所採用的的最小化目標函數,特別是許多矩陣分解類方法,比如ALS。因此,它常用於顯式評級的情形。它的定義爲各平方誤差的和與總數目的商。其中平方誤差是指預測到的評級與真實評級的差值的平方。公式:
在這裏插入圖片描述
均方根誤差(Root Mean Squared Error,RMSE)的使用也很普遍,其計算只需在MSE上取平方根即可,它等同於求預計評級和實際評級的差值的標準差,即:
在這裏插入圖片描述
代碼如下:

import learn.recommend.ItemCFDemo.parseRating
import org.apache.spark.mllib.evaluation.RegressionMetrics
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.mllib.recommendation.{ALS, Rating}
 
/**
  * 均方誤差測試:均方誤差越小,模型越好
  */
object MSEDemo {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[*]").setAppName("ItemCFDemo")
    val sc = new SparkContext(conf)
    val ratingData = sc.textFile("E:\\test\\ml-100k\\u.data")
    val ratingsRDD = ratingData.map(parseRating(_))
    //訓練ALS模型
    val model = ALS.train(ratingsRDD,50,10,0.01)
 
 
    val usersProducts = ratingsRDD.map{
      case Rating(user, product, rating)=>(user,product)
    }
    //獲取所有預測評級
    val predictions = model.predict(usersProducts).map {
      case Rating(user, product, rating) => ((user, product), rating)
    }
 
    //所有真實評級
    val ratings = ratingsRDD.map{case Rating(user,product,rating)=>((user,product),rating)}
 
    //關聯兩個RDD,得到((user,product),(真實評級,預測評級)) RDD
    val ratingsAndPredictions = ratings.join(predictions)
 
//    val MSE = ratingsAndPredictions.map{
//      case ((user,product),(actual,predicted))=>
//        math.pow((actual-predicted),2)}.reduce(_+_)/ratingsAndPredictions.count
 
    val predictedAndTrue = ratingsAndPredictions.map {
      case ((user, product), (predicted, real)) => (predicted, real)
    }
    //求解MSE和RMSE
    val regressionMetrics = new RegressionMetrics(predictedAndTrue)
    val MSE = regressionMetrics.meanSquaredError
    val RMSE = regressionMetrics.rootMeanSquaredError
    println("Mean Squared Error="+MSE)
    println("Root Mean Squared Error="+RMSE)
  }
}

輸出結果:

Mean Squared Error=0.0838857007108934
Root Mean Squared Error=0.2896302827932421

從定義可知,MSE(或RMSE)越小,則說明模型越好,越貼合實際。

發佈了399 篇原創文章 · 獲贊 225 · 訪問量 33萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章