You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

16 KiB

27 | 模型训练(中):回归、分类和聚类算法详解

你好,我是吴磊。

在上一讲我们学习了决策树系列算法包括决策树、GBDT和随机森林。今天这一讲我们来看看在Spark MLlib框架下如何将这些算法应用到实际的场景中。

你还记得我们给出的Spark MLlib模型算法“全景图”么对于这张“全景图”我们会时常回顾它。一方面它能为我们提供“全局视角”再者有了它我们就能够轻松地把学习过的内容对号入座从而对于学习的进展做到心中有数。

图片

今天这一讲我们会结合房屋预测场景一起学习回归、分类与聚类中的典型算法在Spark MLlib框架下的具体用法。掌握这些用法之后针对同一类机器学习问题回归、分类或是聚类你就可以在其算法集合中灵活、高效地做算法选型。

房屋预测场景

在这个场景中我们有3个实例分别是房价预测、房屋分类和房屋聚类。房价预测我们并不陌生在前面的学习中我们一直在尝试把房价预测得更准。

房屋分类它指的是给定离散标签Label如“OverallQual”房屋质量结合房屋属性特征将所有房屋分类到相应的标签取值如房屋质量的“好、中、差”三类。

而房屋聚类,它指的是,在不存在标签的情况下,根据房屋特征向量,结合“物以类聚”的思想,将相似的房屋聚集到一起,形成聚类。

房价预测

在特征工程的两讲中我们一直尝试使用线性模型来拟合房价但线性模型的拟合能力相当有限。决策树系列模型属于非线性模型在拟合能力上更胜一筹。经过之前的讲解想必你对Spark MLlib框架下模型训练的“套路”已经了然于胸模型训练基本上可以分为3个环节

  • 准备训练样本
  • 定义模型,并拟合训练数据
  • 验证模型效果

除了模型定义第一个与第三个环节实际上是通用的。不论我们采用哪种模型训练样本其实都大同小异度量指标不论是用于回归的RMSE还是用于分类的AUC本身也与模型无关。因此今天这一讲我们把重心放在第二个环节,对于代码实现,我们在文稿中也只粘贴这一环节的代码,其他环节的代码,你可以参考特征工程的两讲的内容。

上一讲我们学过了决策树系列模型及其衍生算法也就是随机森林与GBDT算法。这两种算法既可以解决分类问题也可以用来解决回归问题。既然GBDT擅长拟合残差那么我们不妨用它来解决房价预测的回归问题而把随机森林留给后面的房屋分类。

要用GBDT来拟合房价我们首先还是先来准备训练样本。

// numericFields代表数值字段indexFields为采用StringIndexer处理后的非数值字段
val assembler = new VectorAssembler()
.setInputCols(numericFields ++ indexFields)
.setOutputCol("features")
 
// 创建特征向量“features”
engineeringDF = assembler.transform(engineeringDF)
 
import org.apache.spark.ml.feature.VectorIndexer
 
// 区分离散特征与连续特征
val vectorIndexer = new VectorIndexer()
.setInputCol("features")
.setOutputCol("indexedFeatures")
// 设定区分阈值
.setMaxCategories(30)
 
// 完成数据转换
engineeringDF = vectorIndexer.fit(engineeringDF).transform(engineeringDF)

我们之前已经学过了VectorAssembler的用法它用来把多个字段拼接为特征向量。你可能已经发现在VectorAssembler之后我们使用了一个新的特征处理函数对engineeringDF进一步做了转换这个函数叫作VectorIndexer。它是用来干什么的呢

简单地说它用来帮助决策树系列算法如GBDT、随机森林区分离散特征与连续特征。连续特征也即数值型特征数值之间本身是存在大小关系的。而离散特征如街道类型在经过StringIndexer转换为数字之后数字与数字之间会引入原本并不存在的大小关系(具体你可以回看第25讲)。

这个问题要怎么解决呢首先对于经过StringIndexer处理过的离散特征VectorIndexer会进一步对它们编码抹去数字之间的比较关系从而明确告知GBDT等算法该特征为离散特征数字与数字之间相互独立不存在任何关系。

VectorIndexer对象的setMaxCategories方法用于设定阈值该阈值用于区分离散特征与连续特征我们这里设定的阈值为30。这个阈值有什么用呢凡是多样性Cardinality大于30的特征后续的GBDT模型会把它们看作是连续特征而多样性小于30的特征GBDT会把它们当作是离散特征来进行处理。

说到这里,你可能会问:“对于一个特征,区分它是连续的、还是离散的,有这么重要吗?至于这么麻烦吗?”

还记得在决策树基本原理中,特征的“提纯”能力这个概念吗?对于同样一份数据样本,同样一个特征,连续值与离散值的“提纯”能力可能有着天壤之别。还原特征原本的“提纯”能力,将为决策树的合理构建,打下良好的基础。

好啦样本准备好之后接下来我们就要定义并拟合GBDT模型了。

import org.apache.spark.ml.regression.GBTRegressor
 
// 定义GBDT模型
val gbt = new GBTRegressor()
.setLabelCol("SalePriceInt")
.setFeaturesCol("indexedFeatures")
// 限定每棵树的最大深度
.setMaxDepth(5)
// 限定决策树的最大棵树
.setMaxIter(30)
 
// 区分训练集、验证集
val Array(trainingData, testData) = engineeringDF.randomSplit(Array(0.7, 0.3))
 
// 拟合训练数据
val gbtModel = gbt.fit(trainingData)

可以看到我们通过定义GBTRegressor来定义GBDT模型其中setLabelCol、setFeaturesCol都是老生常谈的方法了不再赘述。值得注意的是setMaxDepth和setMaxIter这两个方法用于避免GBDT模型出现过拟合的情况前者限定每棵树的深度而后者直接限制了GBDT模型中决策树的总体数目。后面的训练过程依然是调用模型的fit方法

到此为止我们介绍了如何通过定义GBDT模型来拟合房价。后面的效果评估环节鼓励你结合第23讲的模型验证部分,去自行尝试,加油!

房屋分类

接下来,我们再来说说房屋分类。我们知道,在“House Prices - Advanced Regression Techniques”竞赛项目中数据集总共有79个字段。在之前我们一直把售价SalePrice当作是预测标的也就是Label而用其他字段构建特征向量。

现在我们来换个视角把房屋质量OverallQual看作是Label让售价SalePrice作为普通字段去参与构建特征向量。在房价预测的数据集中房屋质量是离散特征它的取值总共有10个如下图所示。

图片

如此一来我们就把先前的回归问题预测连续值转换成了分类问题预测离散值。不过不管是什么机器学习问题模型训练都离不开那3个环节

  • 准备训练样本
  • 定义模型,并拟合训练数据
  • 验证模型效果

在训练样本的准备上除了把预测标的从SalePrice替换为OverallQual我们完全可以复用刚刚使用GBDT来预测房价的代码实现。

// Label字段"OverallQual"
val labelField: String = "OverallQual"
 
import org.apache.spark.sql.types.IntegerType
engineeringDF = engineeringDF
.withColumn("indexedOverallQual", col(labelField).cast(IntegerType))
.drop(labelField)

接下来我们就可以定义随机森林模型、并拟合训练数据。实际上除了类名不同RandomForestClassifier在用法上与GBDT的GBTRegressor几乎一模一样如下面的代码片段所示。

import org.apache.spark.ml.regression.RandomForestClassifier
 
// 定义随机森林模型
val rf= new RandomForestClassifier ()
// Label不再是房价而是房屋质量
.setLabelCol("indexedOverallQual")
.setFeaturesCol("indexedFeatures")
// 限定每棵树的最大深度
.setMaxDepth(5)
// 限定决策树的最大棵树
.setMaxIter(30)
 
// 区分训练集、验证集
val Array(trainingData, testData) = engineeringDF.randomSplit(Array(0.7, 0.3))
 
// 拟合训练数据
val rfModel = rf.fit(trainingData)

模型训练好之后,在第三个环节,我们来初步验证模型效果。

需要注意的是衡量模型效果时回归与分类问题各自有一套不同的度量指标。毕竟回归问题预测的是连续值我们往往用不同形式的误差如RMSE、MAE、MAPE等等来评价回归模型的好坏。而分类问题预测的是离散值因此我们通常采用那些能够评估分类“纯度”的指标比如说准确度、精准率、召回率等等。

图片

这里我们以Accuracy准确度为例来评估随机森林模型的拟合效果代码如下所示。

import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator
 
// 在训练集上做推理
val trainPredictions = rfModel.transform(trainingData)
 
// 定义分类问题的评估对象
val evaluator = new MulticlassClassificationEvaluator()
.setLabelCol("indexedOverallQual")
.setPredictionCol("prediction")
.setMetricName("accuracy")
 
// 在训练集的推理结果上计算Accuracy度量值
val accuracy = evaluator.evaluate(trainPredictions)

好啦到此为止我们以房价预测和房屋分类为例分别介绍了如何在Spark MLlib框架下去应对回归问题与分类问题。分类与回归是监督学习中最典型的两类模型算法是我们必须要熟悉并掌握的。接下来让我们以房屋聚类为例说一说非监督学习。

房屋聚类

与监督学习相对非监督学习泛指那些数据样本中没有Label的机器学习问题。

以房屋数据为例整个数据集包含79个字段。如果我们把“SalePrice”和“OverallQual”这两个字段抹掉那么原始数据集就变成了不带Label的数据样本。你可能会好奇“对于这些没有Label的样本我们能拿他们做些什么呢

其实能做的事情还真不少基于房屋数据我们可以结合“物以类聚”的思想使用K-means算法把他们进行分门别类的处理。再者在下一讲电影推荐的例子中我们还可以基于频繁项集算法挖掘出不同电影之间共现的频次与关联规则从而实现推荐。

今天我们先来讲K-mean结合数据样本的特征向量根据向量之间的相对距离K-means算法可以把所有样本划分为K个类别这也是算法命名中“K”的由来。举例来说图中的每个点都代表一个向量给定不同的K值K-means划分的结果会随着K的变化而变化。

图片

在Spark MLlib的开发框架下我们可以轻而易举地对任意向量做聚类。

首先在模型训练的第一个环节我们先把训练样本准备好。注意这一次我们去掉了“SalePrice”和“OverallQual”这两个字段。

import org.apache.spark.ml.feature.VectorAssembler
 
val assembler = new VectorAssembler()
// numericFields包含连续特征oheFields为离散特征的One hot编码
.setInputCols(numericFields ++ oheFields)
.setOutputCol("features")

接下来在第二个环节我们来定义K-means模型并使用刚刚准备好的样本去做模型训练。可以看到模型定义非常简单只需实例化KMeans对象并通过setK指定K值即可。

import org.apache.spark.ml.clustering.KMeans
 
val kmeans = new KMeans().setK(20)
 
val Array(trainingSet, testSet) = engineeringDF
.select("features")
.randomSplit(Array(0.7, 0.3))
 
val model = kmeans.fit(trainingSet)

这里我们准备把不同的房屋划分为20个不同的类别。完成训练之后我们同样需要对模型效果进行评估。由于数据样本没有Label因此先前回归与分类的评估指标不适合像K-means这样的非监督学习算法。

K-means的设计思想是“物以类聚”既然如此那么同一个类别中的向量应该足够地接近而不同类别中向量之间的距离应该越远越好。因此我们可以用距离类的度量指标如欧氏距离来量化K-means的模型效果。

import org.apache.spark.ml.evaluation.ClusteringEvaluator
 
val predictions = model.transform(trainingSet)
 
// 定义聚类评估器
val evaluator = new ClusteringEvaluator()
 
// 计算所有向量到分类中心点的欧氏距离
val euclidean = evaluator.evaluate(predictions)

好啦到此为止我们使用非监督学习算法K-means根据房屋向量对房屋类型进行了划分。不过你要注意使用这种方法划分出的类型是没有真实含义的比如它不能代表房屋质量也不能代表房屋评级。既然如此我们用K-means忙活了半天图啥呢

尽管K-means的结果没有真实含义但是它以量化的形式刻画了房屋之间的相似性与差异性。你可以这样来理解我们用K-means为房屋生成了新的特征相比现有的房屋属性这个生成的新特征Generated Features往往与预测标的如房价、房屋类型有着更强的关联性所以让这个新特性参与到监督学习的训练就有希望优化/提升监督学习的模型效果。

图片

好啦到此为止结合房价预测、房屋分类和房屋聚类三个实例我们成功打卡了回归、分类和聚类这三类模型算法。恭喜你离Spark MLlib模型算法通关咱们还有一步之遥。在下一讲我们会结合电影推荐的场景继续学习两个有趣的模型算法协同过滤与频繁项集。

重点回顾

今天这一讲你首先需要掌握K-means算法的基本原理。聚类的设计思想是“物以类聚、人以群分”给定任意向量集合K-means都可以把它划分为K个子集合从而完成聚类。

K-means的计算主要依赖向量之间的相对距离它的计算结果一方面可以直接用于划分“人群”、“种群”另一方面可以拿来当做生成特征去参与到监督学习的训练中去。

此外你需要掌握GBTRegressor和RandomForestClassifier的一般用法。其中setLabelCol与setFeaturesCol分别用于指定模型的预测标的与特征向量。而setMaxDepth与setMaxIter分别用于设置模型的超参数也即最大树深与最大迭代次数决策树的数量从而避免模型出现过拟合的情况。

每课一练

对于房价预测与房屋分类这两个场景,你觉得在它们之间,有代码(尤其是特征工程部分的代码)复用的必要和可能性吗?

欢迎你在留言区跟我交流互动,也推荐你把这一讲的内容分享给更多的同事、朋友。