Spark MLlib 从房价预测”开始
Spark MLlib 机器学习。在数据科学、机器学习与人工智能火热的当下,积累一些机器学习的知识储备,有利于我们拓展视野,甚至为职业发展提供新的支点。
这个模块中,我们首先从一个“房价预测”的小项目入手,来初步了解机器学习以及 Spark MLlib 的基本用法。接下来,我们会着重讲解机器学习的两个关键环节:特征工程与模型调优,在深入学习 Spark MLlib 的同时,进一步优化“房价预测”的模型效果,从而让房价的预测越来越准。
机器学习
每个人在成长的过程中,或是通过书本,或是结合过往的经历,都在不断地吸取经验教训,从而总结出为人处世、待人接物的一般原则,然后再将这些原则应用到余下的人生中去。人类学习与成长的过程,大抵如此。
实际上,机器学习的过程也是类似的。基于历史数据,机器会根据一定的算法,尝试从历史数据中挖掘并捕捉出一般规律。然后,再把找到的规律应用到新产生的数据中,从而实现在新数据上的预测与判断。
所谓机器学习(Machine Learning),它指的是这样一种计算过程:对于给定的训练数据(Training samples),选择一种先验的数据分布模型(Models),然后借助优化算法(Learning Algorithms)自动地持续调整模型参数(Model Weights / Parameters),从而让模型不断逼近训练数据的原始分布。
这个持续调整模型参数的过程称为“模型训练”(Model Training)。模型的训练依赖于优化算法,基于过往的计算误差(Loss),优化算法以不断迭代的方式,自动地对模型参数进行调整。由于模型训练是一个持续不断的过程,那么自然就需要一个收敛条件(Convergence Conditions),来终结模型的训练过程。一旦收敛条件触发,即宣告模型训练完毕。
模型训练完成之后,我们往往会用一份新的数据集(Testing samples),去测试模型的预测能力,从而验证模型的训练效果,这个过程,我们把它叫作“模型测试”(Model Testing)。
回顾房价预测项目的 4 个数据文件,其中的 train.csv 就是我们说的训练数据(Training samples),它用于训练机器学习模型。相应地,test.csv 是测试数据(Testing samples),它用于验证我们模型的训练效果。
更严谨地说,测试数据用于考察模型的泛化能力(Generalization),也就是说,对于一份模型从来没有“看见过”的数据,我们需要知道,模型的预测能力与它在训练数据上的表现是否一致。
train.csv 和 test.csv 这两个文件的 Schema 完全一致,都包含 81 个字段,除了其中的 79 个房屋属性与 1 个交易价格外,还包含一个 ID 字段。在房价预测这个项目中,我们的任务是事先选定一个数据分布模型(Models),然后在训练数据上对它进行训练(Model Training),模型参数收敛之后,再用训练好的模型,去测试集上查看它的训练效果。
关于更多机器学习相关的内容,可以看我们的机器学习专栏
数据准备
为兼顾项目的权威性与代表性,这里我选择了 Kaggle(数据科学竞赛平台)的“House Prices - Advanced Regression Techniques”竞赛项目。这个项目的要求是,给定房屋的 79 个属性特征以及历史房价,训练房价预测模型,并在测试集上验证模型的预测效果。
房屋数据记录着美国爱荷华州 2006 年到 2010 年的房屋交易数据,其中包含着 79 个房屋属性以及当时的成交价格,你可以通过竞赛项目的 data 页面进行下载。
数据下载、解压之后,我们会得到 4 个文件,分别是 data_description.txt、train.csv、test.csv 和 sample_submission.csv。这 4 个文件的体量很小,总大小不超过 5MB,它们的内容与含义如下表所示。
其中,train.csv 与 test.csv 的 Schema 完全一致,都包含 79 个房屋属性字段以及一个交易价格字段,描述文件则详细地记录着 79 个字段的含义与取值范围。二者的唯一区别在于用途,train.csv 用于训练模型,而 test.csv 用于验证模型效果。
sample_submission.csv 文件则用于提交比赛结果。
房价预测的流程
理论总是没有实战来的更直接,接下来,我们就来借助 Spark MLlib 机器学习框架,去完成“房价预测”这个机器学习项目的实现。与此同时,随着项目的推进,我们再结合具体实现来深入理解刚刚提到的基本概念与常用术语。
定义问题收集数据
这里的问题就是根据已有的历史数据,建立模型,来预测房价,因为这个例子中我们的问题其实已经很明显了,但是在一些大型项目中,我们一开始要做的事情其实是理清边界,从而确定问题,接下来根据问题来收集数据
模型选择
那么都有哪些模型可供我们选择呢?对于房价预测的项目,我们又该选择其中哪一个呢?像这种如何挑选合适模型的问题,我们统一把它称作“模型选择”。
在机器学习领域,模型的种类非常多,不仅如此,模型的分类方法也各有不同。
- 按照拟合能力来分类,有线性模型与非线性模型之分;
- 按照预测标的来划分,有回归、分类、聚类、挖掘之分;
- 按照模型复杂度来区分,模型可以分为经典算法与深度学习;
- 按照模型结构来说,又可以分为广义线性模型、树模型、神经网络,等等。
在“房价预测”这个项目中,我们的预测标的(Label)是房价,而房价是连续的数值型字段,因此我们需要回归模型(Regression Model)来拟合数据。
数据探索
要想准确地预测房价,我们得先确定,在与房屋相关的属性中,哪些因素对于房价的影响最大。在模型训练的过程中,我们需要选择那些影响较大的因素,而剔除那些影响较小的干扰项。
在机器学习领域中,与预测标的相关的属性,统称为“数据特征”(Features),而选择有效特征的过程,我们称之为“特征选择”(Features Selection)。在做特性选择之前,我们自然免不了先对数据做一番初步的探索,才有可能得出结论。
具体的探索过程是这样的。首先,我们使用 SparkSession 的 read API,从 train.csv 文件创建 DataFrame,然后调用 show 与 printSchema 函数,来观察数据的样本构成与 Schema。
import org.apache.spark.sql.DataFrame
val rootPath: String = _
val filePath: String = s"${rootPath}/train.csv"
// 从CSV文件创建DataFrame
val trainDF: DataFrame = spark.read.format("csv").option("header", true).load(filePath)
trainDF.show
trainDF.printSchema
通过观察数据,我们会发现房屋的属性非常丰富,包括诸如房屋建筑面积、居室数量、街道路面情况、房屋类型(公寓还是别墅)、基础设施(水、电、燃气)、生活周边(超市、医院、学校)、地基类型(砖混还是钢混)、地下室面积、地上面积、厨房类型(开放还是封闭)、车库面积与位置、最近一次交易时间,等等。
特征工程
道理来说,要遴选那些对房价影响较大的特征,我们需要计算每一个特征与房价之间的相关性。不过,在第一版的实现中,咱们重点关注 Spark MLlib 的基本用法,暂时不看重模型效果。
所以,咱们不妨一切从简,只选取那些数值型特征(这类特征简单直接,适合上手),如建筑面积、地上面积、地下室面积和车库面积,即"LotArea",“GrLivArea”,“TotalBsmtSF"和"GarageArea”,其实这个过程就是选择特征,后续为了模型效果可能还会有特征转换,数据清洗,所以这个过程也叫做特征工程,这些事情都是基于之前的数据探索,如果后面模型效果不佳,可能还需要重新选择特征,持续迭代。
import org.apache.spark.sql.types.IntegerType
import org.apache.spark.sql.functions._
// 提取用于训练的特征字段与预测标的(房价SalePrice)
val selectedFields: DataFrame = trainDF.select("LotArea", "GrLivArea", "TotalBsmtSF", "GarageArea", "SalePrice")
// 将所有字段都转换为整型Int
val typedFields = selectedFields
.withColumn("LotAreaInt",col("LotArea").cast(IntegerType)).drop("LotArea")
.withColumn("GrLivAreaInt",col("GrLivArea").cast(IntegerType)).drop("GrLivArea")
.withColumn("TotalBsmtSFInt",col("TotalBsmtSF").cast(IntegerType)).drop("TotalBsmtSF")
.withColumn("GarageAreaInt",col("GarageArea").cast(IntegerType)).drop("GarageArea")
.withColumn("SalePriceInt",col("SalePrice").cast(IntegerType)).drop("SalePrice")
typedFields.printSchema
/** 结果打印
root
|-- LotAreaInt: integer (nullable = true)
|-- GrLivAreaInt: integer (nullable = true)
|-- TotalBsmtSFInt: integer (nullable = true)
|-- GarageAreaInt: integer (nullable = true)
|-- SalePriceInt: integer (nullable = true)
*/
从 CSV 创建 DataFrame,所有字段的类型默认都是 String,而模型在训练的过程中,只能消费数值型数据。因此,我们这里还要做一下类型转换,把所有字段都转换为整型。
这里小小提示一下,就是如果你是在命令行里面输入,也就是在spark-shell 里面,那些多行输入的代码可能不能正确执行,可以先输入:paste
然后输入代码,最后ctrl-D 结束输入
数据准备就绪,接下来,我们就可以借助 Spark MLlib 框架,开启机器学习的开发之旅。首先,第一步,我们把准备用于训练的多个特征字段,捏合成一个特征向量(Feature Vectors),如下所示。
import org.apache.spark.ml.feature.VectorAssembler
// 待捏合的特征字段集合
val features: Array[String] = Array("LotAreaInt", "GrLivAreaInt", "TotalBsmtSFInt", "GarageAreaInt")
// 准备“捏合器”,指定输入特征字段集合,与捏合后的特征向量字段名
val assembler = new VectorAssembler().setInputCols(features).setOutputCol("features")
// 调用捏合器的transform函数,完成特征向量的捏合
val featuresAdded: DataFrame = assembler.transform(typedFields)
.drop("LotAreaInt")
.drop("GrLivAreaInt")
.drop("TotalBsmtSFInt")
.drop("GarageAreaInt")
featuresAdded.printSchema
/** 结果打印
root
|-- SalePriceInt: integer (nullable = true)
|-- features: vector (nullable = true) // 注意,features的字段类型是Vector
*/
捏合完特征向量之后,我们就有了用于模型训练的训练样本(Training Samples),它包含两类数据,一类正是特征向量 features,另一类是预测标的 SalePriceInt。
这里有个问题就是这个捏合的过程是spark 特有的,也不能说是特有的,因为这个数据将来作为模型的输入,所以输入的结构是什么样子的取决于我们所采用的技术框架,所这个过程不是通用的。
划分数据集
接下来,我们把训练样本成比例地分成两份,一份用于模型训练,剩下的部分用于初步验证模型效果。
val Array(trainSet, testSet) = featuresAdded.randomSplit(Array(0.7, 0.3))
将训练样本拆分为训练集和验证集
模型训练
训练样本准备就绪,接下来,我们就可以借助 Spark MLlib 来构建线性回归模型了。实际上,使用 Spark MLlib 构建并训练模型,非常简单直接,只需 3 个步骤即可搞定。
第一步是导入相关的模型库,在 Spark MLlib 中,线性回归模型由 LinearRegression 类实现。第二步是创建模型实例,并指定模型训练所需的必要信息。第三步是调用模型的 fit 函数,同时提供训练数据集,开始训练。
import org.apache.spark.ml.regression.LinearRegression
// 构建线性回归模型,指定特征向量、预测标的与迭代次数
val lr = new LinearRegression()
.setLabelCol("SalePriceInt")
.setFeaturesCol("features")
.setMaxIter(10)
// 使用训练集trainSet训练线性回归模型
val lrModel = lr.fit(trainSet)
以看到,在第二步,我们先是创建 LinearRegression 实例,然后通过 setLabelCol 函数和 setFeaturesCol 函数,来分别指定预测标的字段与特征向量字段,也即“SalePriceInt”和“features”。紧接着,我们调用 setMaxIter 函数来指定模型训练的迭代次数。
这里,我有必要给你解释一下迭代次数这个概念。在前面介绍机器学习时,我们提到,模型训练是一个持续不断的过程,训练过程会反复扫描同一份数据,从而以迭代的方式,一次又一次地更新模型中的参数(Parameters,也叫作权重,Weights),直到模型的预测效果达到一定的标准,才能结束训练。
关于这个标准的制定,来自于两个方面。
- 一方面是对于预测误差的要求,当模型的预测误差小于预先设定的阈值时,模型迭代即可收敛、结束训练。
- 另一个方面就是对于迭代次数的要求,也就是说,不论预测误差是多少,只要达到了预先设定的迭代次数,模型训练即宣告结束。
模型训练也是类似的,我们一次次地把训练数据,“喂给”模型算法,一次次地调整模型参数,直到把预测误差降低到一定的范围、或是模型迭代达到一定的次数,即宣告训练结束。当有新的数据需要预测时,我们就把它喂给训练好的模型,模型就能生成预测结果。
模型权重的调整,依赖的往往是一种叫作“梯度下降”(Gradient Descend)的优化算法。在模型的每一次迭代中,梯度下降算法会自动地调整模型权重,而不需要人为的干预。
模型效果评估
模型训练好之后,我们需要对模型的效果进行验证、评估,才能判定模型的“好”、“坏”。
首先,我们先来看看,模型在训练集上的表现怎么样。在线性回归模型的评估中,我们有很多的指标,用来量化模型的预测误差。其中最具代表性的要数 RMSE(Root Mean Squared Error),也就是均方根误差。我们可以通过在模型上调用 summary 函数,来获取模型在训练集上的评估指标,如下所示。
val trainingSummary = lrModel.summary
println(s"RMSE: ${trainingSummary.rootMeanSquaredError}")
/** 结果打印
RMSE: 45798.86
*/
在训练集的数据分布中,房价的值域在(34900,755000)之间,因此,45798.86 的预测误差还是相当大的。这说明我们得到的模型,甚至没有很好地拟合训练数据。换句话说,训练得到的模型,处在一个“欠拟合”的状态。
这其实很好理解,一方面,咱们的模型过于简单,线性回归的拟合能力本身就非常有限。再者,在数据方面,我们目前仅仅使用了 4 个字段(LotAreaInt,GrLivAreaInt,TotalBsmtSFInt,GarageAreaInt)。房价影响因素众多,仅用 4 个房屋属性,是很难准确地预测房价的。所以在后面的几讲中,我们还会继续深入研究特征工程与模型选型对于模型拟合能力的影响。
总结
你需要掌握机器学习是怎样的一个计算过程。所谓机器学习(Machine Learning),它指的是这样一种计算过程。对于给定的训练数据(Training samples),选择一种先验的数据分布模型(Models),然后借助优化算法(Learning Algorithms)自动地持续调整模型参数(Model Weights / Parameters),从而让模型不断逼近训练数据的原始分布。