小广告
(欢迎大家关注我的公众号“一懂算法”,之后将在公众号上持续记录本人从非科班转到算法路上的学习心得、笔经面经、心得体会。未来的重点也会主要放在机器学习面试上!)
(慢慢补充中…)
来源:王喆老师的github https://github.com/wzhe06/SparrowRecSys
特征工程部分FeatureEngineering的代码。对电影数据进行特征处理。
代码内容包含:
- 对类别标签进行one-hot编码,
- 对类别标签进行multi-hot编码,
- 对电影评分数据中的数值特征的处理:
- 获取每部电影的评分总数、平均分、评分方差
- 对每部电影的评分总数进行分箱
- 对每部电影的平均评分进行归一化
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)
}
}