电影推荐系统Sparrow Recsys源码解读——FeatureEngineering部分

小广告

(欢迎大家关注我的公众号一懂算法”,之后将在公众号上持续记录本人从非科班转到算法路上的学习心得、笔经面经、心得体会。未来的重点也会主要放在机器学习面试上!)

(慢慢补充中…)
来源:王喆老师的github https://github.com/wzhe06/SparrowRecSys

特征工程部分FeatureEngineering的代码。对电影数据进行特征处理。
代码内容包含:

  1. 对类别标签进行one-hot编码,
  2. 对类别标签进行multi-hot编码,
  3. 对电影评分数据中的数值特征的处理:
    • 获取每部电影的评分总数、平均分、评分方差
    • 对每部电影的评分总数进行分箱
    • 对每部电影的平均评分进行归一化
1. 先来看主函数
  def main(args: Array[String]): Unit = {
    Logger.getLogger("org").setLevel(Level.ERROR)
    //设置Logger日志级别,ERROR level指出虽然发生错误事件,但仍然不影响系统的继续运行。如果没有设置这玩意,每次运行都会报出一大堆信息
    val conf = new SparkConf()
      .setMaster("local")
      .setAppName("featureEngineering")
      .set("spark.submit.deployMode", "client")
//一般来说,如果提交任务的节点(即Master)和Worker集群在同一个网络内,此时client mode比较合适。
//如果提交任务的节点和Worker集群相隔比较远,就会采用cluster mode来最小化Driver和Executor之间的网络延迟。
    val spark = SparkSession.builder.config(conf).getOrCreate()
    val movieResourcesPath = this.getClass.getResource("/webroot/sampledata/movies.csv")
    val movieSamples = spark.read.format("csv").option("header", "true").load(movieResourcesPath.getPath)
    println("Raw Movie Samples:")
    movieSamples.printSchema()
    movieSamples.show(10)

    println("OneHotEncoder Example:")
    oneHotEncoderExample(movieSamples)

    println("MultiHotEncoder Example:")
    multiHotEncoderExample(movieSamples)

    println("Numerical features Example:")
    val ratingsResourcesPath = this.getClass.getResource("/webroot/sampledata/ratings.csv")
    val ratingSamples = spark.read.format("csv").option("header", "true").load(ratingsResourcesPath.getPath)
    ratingFeatures(ratingSamples)
  /**
   * One-hot encoding example function
   * @param samples movie samples dataframe
   */
  def oneHotEncoderExample(samples:DataFrame): Unit ={
    val samplesWithIdNumber = samples.withColumn("movieIdNumber", col("movieId").cast(sql.types.IntegerType))
//新增一列movieIdNumber,用cast将string类型改为数值类型。onehotencoder必须输入数值型。经过自己的尝试,改为.cast("Int")也行

    val oneHotEncoder = new OneHotEncoderEstimator()
      .setInputCols(Array("movieIdNumber"))
      .setOutputCols(Array("movieIdVector"))
      .setDropLast(false)
//setInputCols要求的输入类型为Array[String],这个表示每个array里的元素都是一个字符串,而不是一个字符,也就是并没有拆分String。个人猜测可能是为了方便输入多个列。
//setDropLast表示丢弃最后一个类别,默认为True,也就是比如有a,b,c个ID,那么onehot后的长度为2,前两个有向量,最后一个被丢弃了。c对应的值为[]。
    val oneHotEncoderSamples = oneHotEncoder.fit(samplesWithIdNumber).transform(samplesWithIdNumber)
    oneHotEncoderSamples.printSchema()
    oneHotEncoderSamples.show(10)
  }
  val array2vec: UserDefinedFunction = udf { (a: Seq[Int], length: Int) => org.apache.spark.ml.linalg.Vectors.sparse(length, a.sortWith(_ < _).toArray, Array.fill[Double](a.length)(1.0)) }

  /**
   * Multi-hot encoding example function
   * @param samples movie samples dataframe
   */
  def multiHotEncoderExample(samples:DataFrame): Unit ={
    val samplesWithGenre = samples.select(col("movieId"), col("title"),explode(split(col("genres"), "\\|").cast("array<string>")).as("genre"))
    val genreIndexer = new StringIndexer().setInputCol("genre").setOutputCol("genreIndex")
// StringIndexer类似于labelEncoder,比如a,b,c三类会被映射为0,1,2
    val stringIndexerModel : StringIndexerModel = genreIndexer.fit(samplesWithGenre)

    val genreIndexSamples = stringIndexerModel.transform(samplesWithGenre)
      .withColumn("genreIndexInt", col("genreIndex").cast(sql.types.IntegerType))

    val indexSize = genreIndexSamples.agg(max(col("genreIndexInt"))).head().getAs[Int](0) + 1

    val processedSamples =  genreIndexSamples
      .groupBy(col("movieId")).agg(collect_list("genreIndexInt").as("genreIndexes"))
        .withColumn("indexSize", typedLit(indexSize))

    val finalSample = processedSamples.withColumn("vector", array2vec(col("genreIndexes"),col("indexSize")))
    finalSample.printSchema()
    finalSample.show(10)
  }
  val double2vec: UserDefinedFunction = udf { (value: Double) => org.apache.spark.ml.linalg.Vectors.dense(value) }
//自定义算子:1、功能:将double值转为dense向量,相对的有稀疏向量
//2、目的:MinMaxScaler需要传入数组类型
//一个dense类型的向量背后其实就是一个数组,而sparse向量背后则是两个并行
//数组——索引数组和值数组。比如向量(1.0, 0.0, 3.0)既可以用密集型向量表示
//为[1.0, 0.0, 3.0],也可以用稀疏型向量表示为(3, [0,2],[1.0,3.0]),其
//中3是数组的大小。

  /**
   * Process rating samples
   * @param samples rating samples
   */
  def ratingFeatures(samples:DataFrame): Unit ={
    samples.printSchema()
    samples.show(10)

    //calculate average movie rating score and rating count
    val movieFeatures = samples.groupBy(col("movieId"))
      .agg(count(lit(1)).as("ratingCount"),
        avg(col("rating")).as("avgRating"),
        variance(col("rating")).as("ratingVar"))
        .withColumn("avgRatingVec", double2vec(col("avgRating")))
// groupby和agg是对应的,分组聚合。agg里可以用逗号分开进行多组操作
//不知道为什么count里的lit(1)是什么意思。
    movieFeatures.show(10)
    
    //bucketing
    val ratingCountDiscretizer = new QuantileDiscretizer()
      .setInputCol("ratingCount")
      .setOutputCol("ratingCountBucket")
      .setNumBuckets(100)

    //Normalization
    val ratingScaler = new MinMaxScaler()
      .setInputCol("avgRatingVec")
      .setOutputCol("scaleAvgRating")

    val pipelineStage: Array[PipelineStage] = Array(ratingCountDiscretizer, ratingScaler)
    val featurePipeline = new Pipeline().setStages(pipelineStage)

    val movieProcessedFeatures = featurePipeline.fit(movieFeatures).transform(movieFeatures)
    movieProcessedFeatures.show(10)
  }
  }
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值