2.1 回归简介
回归与分类
回归是 预测一个数值型数量 分类是 预测标号或者类别 监督学习:两者都需要从一组输入和输出中学习预测规则(即需要告诉其问题与答案)
2.2 向量与特征
特征:也叫维度
数值型特征:可以用数值进行量化的特征,并且对这些特征排序是有意义的 类别型特征:不能用数值表示,没有大小顺序,一般都是在几个离散值中取一个 特征向量:特征值按一定顺序排列 预测指标:也叫变量
2.3 样本训练
特征向量为学习算法的 输入 提供了一种结构化的方式(例如输入:12.5,15.0,0.10,晴朗,0),预测的输出(即目标:17.2)也会被称为一个特征(通常把它当做特征向量的一个附加特征),因此可以把整个训练样本示例表示为:12.5,15.0,0.10,晴朗,0,17.2;而这些样本的集合就称之为 训练集。
2.4 决策树和决策森林
决策树算法家族 能够很自然地处理类别型和数值型特征。它们对数据中的离群点(outlier)具有稳健性(robust),这意味着一些极端或可能错误的数据点根本不会对预测产生影响 算法可以接受不同类型和不同取值范围的数据,不需要将数据转化为同一类型,或是将数据规范化到特定的值域中。 决策树例子
2.5 Covtype数据集
该数据集记录了 美国科罗拉多州不同地块的森林植被类型,每个样本包含描述每块土地的若干特征,包括海拔,坡度、到水源的距离、遮阳情况和土壤类型,并且给出地块对应的已知森林植被类型。
2.6 准备数据
val dataWithoutHeader = spark.read.
option("inferSchema", true).
option("header", false).
csv("hdfs:///user/ds/covtype.data")
编码方式:
one-hot/1-of-n编码:一个有N个不同取值的类别型特征可以变成N个数值型特征,变换后的每个数值型特征的取值为0或1.在这N个特征中,有且只有一个取值为1,其他特征取值都为0. 分配数值 编码方式:为类别型特征的每个可能取值分配一个不同数值,目标“Cover_Type”本身也是类别型值,用1~7编码 在处理DataFrame之前,将列名添加到其中,让其使用更方便
val colNames = Seq(
"Elevation", "Aspect", "Slope",
"Horizontal_Distance_To_Hydrology", "Vertical_Distance_To_Hydrology",
"Horizontal_Distance_To_Roadways",
"Hillshade_9am", "Hillshade_Noon", "Hillshade_3pm",
"Horizontal_Distance_To_Fire_Points"
) ++ (
(0 until 4).map(i => s"Wilderness_Area_$i")
) ++ (
(0 until 40).map(i => s"Soil_Type_$i")
) ++ Seq("Cover_Type")
val data = dataWithoutHeader.toDF(colNames:_*).
withColumn("Cover_Type", $"Cover_Type".cast("double"))
data.show()
data.head
2.7 第一棵决策树
将数据集 90%当成训练集(再分一个子集用作验证集(交叉验证集)),10%当成测试集
val Array(trainData, testData) = data.randomSplit(Array(0.9, 0.1))
trainData.cache()
testData.cache()
2.7.1 数据预处理
输入DataFrame包含许多列,每列对应一个特征,可以用来预测目标列。Spark MLlib要求将所有输入合并成一列,该列的值是一个向量。 通过VectorAssmbler类来完成这个工作
val inputCols = trainData.columns.filter(_ != "Cover_Type") //除了目标列
val assembler = new VectorAssembler().
setInputCols(inputCols).//组合成特征向量的列
setOutputCol("featureVector")//包含特征向量的新列的名称
val assembledTrainData = assembler.transform(trainData)
assembledTrainData.select("featureVector").show(truncate = false)
[特征值数目,[非零值索引],[非零值]] 输出看起来不是很像一串数字,因为它显示的是向量的原始表示,也就是SparseVector的实例,它仅存储非零值及其索引,可以节省存储空间。 VectorAssembler可以将一个DataFrame转换成另外一个DataFrame,并且可以和其他Transformer组合成一个管道。
2.7.2 构建第一个决策树分类模型
该模型的一些树结构,它由一系列针对特征的嵌套决策组成,这些决策将特征值与阈值相比较
val classifier = new DecisionTreeClassifier().
setSeed(Random.nextLong()).
setLabelCol("Cover_Type").//被预测特征值的列
setFeaturesCol("featureVector").//包含输入特征向量的列
setPredictionCol("prediction")//存储预测值的列的名称
val model = classifier.fit(assembledTrainData)
println(model.toDebugString)
在构建决策树的过程中,决策树能够评估输入特征的重要性。在下面,打印出评估每个输入特征对做出正确预测的贡献值。 Elevation 是该模型预测绝对重要的特征!其他大多数特征时没有作用的。
model.featureImportances.toArray.zip(inputCols).
sorted.reverse.foreach(println)
返回的DecisionTreeClassificationModel 本身即为一个转换器,它可以将一个包含特征向量的DataFrame转换成一个包含特征向量及其预测结果的DataFrame. 下面 是观测模型在训练数据上的预测结果,可以比较模型预测值与正确的覆盖类型
val predictions = model.transform(assembledTrainData)
predictions.select("Cover_Type", "prediction", "probability").
show(truncate = false)
[正确覆盖类型,模型预测值,模型对每个可能的输出的准确率的估计] 尽管我们只有七中结果,分别为1~7,但概率向量上有8个值,向量索引1 ~ 7的值分别表示结果为1 ~ 7的概率,索引0是一个值(无效的结果,视为向量的一个小问题) 根据结果可以看出,预测结果的准确率是不高的,因此需要调整决策树分类器中的几个超参数。 测试集可用于对默认超参数训练出的模型的期望准确率做无偏估计; SparkMLlib评估器的作用即以某种方式评估输出DataFrame的质量,MultclassClassificationEvaluator能够计算准确率和其他评估模型预测质量的指标。
val evaluator = new MulticlassClassificationEvaluator().
setLabelCol("Cover_Type").
setPredictionCol("prediction")
val accuracy = evaluator.setMetricName("accuracy").evaluate(predictions)
val f1 = evaluator.setMetricName("f1").evaluate(predictions)
println(accuracy)
println(f1)
得到两个结果:一个为分类器的准确率(在这里用于评价分类器) 另一个为F1值。 单个准确率可以概括分类器输出的好坏,但混淆矩阵 是更有效的手段 混淆矩阵
混淆矩阵 是一个N*N的表,N代表可能的目标值的个数。每一行代表数据的真实归属类别,每一列按顺序依次代表预测值,第i行和第j列的条目表示数据中真正归属第i个类别却被预测为第j个类别的数据总量。因此,其正确的预测时沿着对角线计算的,而非对角线元素代表错误预测 在我们这个案例中的目标值有7个分类,所以是一个7*7的矩阵。 Spark提供用于计算混淆矩阵的库(MulticlassMetrics),但是这个库是基于操作RDD的旧版MLlib API实现的,因此需要将DataFrame和Dataset转换为RDD来使用这些旧版API.
val predictionRDD = predictions.
select("prediction", "Cover_Type").
as[(Double,Double)]. // 转换成Dataset
rdd // 转换成RDD
val multiclassMetrics = new MulticlassMetrics(predictionRDD)
println(multiclassMetrics.confusionMatrix)
对角线上次数越多越好,但也出现分类错误的情况,分类器甚至没有将任何一个样本的类别预测为5和6 计算混淆矩阵的第二个方法:直接使用DataFrame API中一些通用操作来计算,对两个维度上的值进行分组,这些值会变成输出的行和列,然后对这些分组进行计数之类的聚合计算。
val confusionMatrix = predictions.
groupBy("Cover_Type").
pivot("prediction", (1 to 7)).
count().
na.fill(0.0). //使用0来替换null
orderBy("Cover_Type")
confusionMatrix.show()
准确率比较
为了验证我们的 70%准确率 是否已经比较好,我们可以通过一个随机猜测的准确率来对我们上面模型得到的准确率做一个对比评估 如果构建得到我们的随机猜测的准确率呢?
按照类别在训练集中出现的比例来预测类别,我们构建一个 “分类器”;举例来说,即时如果类型1在训练集中占30%,那么分类器就有33%的概率猜测类型为“1”,每次分类的准确率将和一个类型在交叉验证集中出现的次数成正比,如果测试集的40%是植被类型1,那么全猜“1”的就有40%的准确率,而植被类型1有30% * 40%= 12%的概率被猜中,并为整体准确率贡献12%,将所有类别在训练集和交叉验证集出现的概率相乘,然后把结果相加,我们就得到一个对准确率的评估 def classProbabilities(data: DataFrame): Array[Double] = {
val total = data.count()
data.groupBy("Cover_Type").count(). //计算数据中每个类别的样本数
orderBy("Cover_Type").// 对类别的样本数进行排序
select("count").as[Double].//转换成Dataset
map(_ / total).
collect()
}
val trainPriorProbabilities = classProbabilities(trainData)
val testPriorProbabilities = classProbabilities(testData)
val accuracy = trainPriorProbabilities.zip(testPriorProbabilities).map { // 对训练集和测试集中键值对的乘积求和
case (trainProb, cvProb) => trainProb * cvProb
}.sum
println(accuracy)
随机猜测的准确率只有37%,而我们的决策树模型的准确率有70%,因此还是决策树模型的准确率比较好。
2.8 决策树的超参数
决策树的超参数:最大深度、最大桶数、不纯性度量和最小信息增益
最大深度:是分类器为了对样本进行分类所做的一连串判断的最大次数,对决策树的层数做出限制,限制判断次数有利于避免对训练数据产生过拟合。 最大桶数:Spark MLlib的实现把决策规则集合称为 “桶”;决策树算法负责为每层生成可能的决策规则:对数值型特征,决策采用特征>=的形式,对类别特征,决策采用特征在(值1,值2,…)中的形式;因此要尝试的决策规则集合实际上是可以嵌入决策规则中的一系列值。桶越大,找到的决策规则越优,但需要处理的时间就越长 不纯性度量:好的决策规则应该通过目标类别值对样本做出有意义的画风,划分前后每个集合各类型的不纯性应该是降低的即好规则把训练集数据的目标值分为相对是同类或“纯”子集,最小化规则对应的两个子集的不纯性。
Gini不纯度:直接和随机猜测分类器的准确率相关,在每个子集中,它就是对一个随机挑选的样本进行随机分类时分类错误的概率(随机挑选样本和随机分类时要参照子数据集的类别分布),即用1 减去每个类别的比例与自身的乘积之和
假设子数据集中包含N个类别的样本,
P
i
P_{i}
P i 是类别i的样本所占比例,其Gini不纯度公式:
I
G
(
p
)
=
1
−
∑
i
=
1
N
P
i
2
I_{G}(p)=1-\sum_{i=1}^{N}P_{i}^{2}
I G ( p ) = 1 − ∑ i = 1 N P i 2 如果子数据集中所有样本都属于同一个类别,则Gini不纯度的值为0,因为这个子数据集完全是“纯的”;当子数据集中的样本来自N个不同的类别时,Gini不纯度值大于0,并且在每个类别的样本数据都相同时达到最大,也就是最不纯的情况 熵:代表了子集中目标取值集合对子集中的数据所做的预测的不确定程度,如果子集只包含一个类别,则是完全确定的,熵为0;…这个跟Gini系数一样,值越小则代表分类越好 公式:
I
E
(
p
)
=
∑
i
=
1
N
p
i
l
o
g
(
1
p
)
=
−
∑
i
=
1
N
p
i
l
o
g
(
p
i
)
I_{E}(p) = \sum_{i=1}^{N}p_{i}log(\frac{1}{p})=-\sum_{i=1}^{N}p_{i}log(p_{i})
I E ( p ) = ∑ i = 1 N p i l o g ( p 1 ) = − ∑ i = 1 N p i l o g ( p i ) 不确定性是有单位的,取自然对数(以e为底),熵的单位是纳特(nat),以2为底取对数是 比特。 最小信息增益:会导致最小信息增益或最小不纯度降低,有利于避免过拟合。
2.9 决策树调优
构建一个管道,用于封装与上面相同的两个步骤:创建VectorAssembler 和DecisionTreeClassifier,然后将这两个Transformer串起来,就能得到一个单独的Pipeline对象(可以将前面的两个操作表示为一个)
val inputCols = unencTrainData.columns.filter(_ != "Cover_Type")
val assembler = new VectorAssembler().
setInputCols(inputCols).
setOutputCol("featureVector")
val indexer = new VectorIndexer().
setMaxCategories(40).
setInputCol("featureVector").
setOutputCol("indexedVector")
val classifier = new DecisionTreeClassifier().
setSeed(Random.nextLong()).
setLabelCol("Cover_Type").
setFeaturesCol("indexedVector").
setPredictionCol("prediction")
val pipeline = new Pipeline().setStages(Array(assembler, indexer, classifier))
定义使用Spark ML API内建支持的ParamGridBuilder来测试超参数的组合
val paramGrid = new ParamGridBuilder().
addGrid(classifier.impurity, Seq("gini", "entropy")).
addGrid(classifier.maxDepth, Seq(1, 20)).
addGrid(classifier.maxBins, Seq(40, 300)).
addGrid(classifier.minInfoGain, Seq(0.0, 0.05)).
build()
val multiclassEval = new MulticlassClassificationEvaluator().
setLabelCol("Cover_Type").
setPredictionCol("prediction").
setMetricName("accuracy")
对四个超参数而言,每个超参数的两个值都要构建和评估一个模型,共16种超参数组合,会训练出16个模型,并使用多分类准确率对这些模型进行评估 TrainValidationSplit将这些组件拼在一起,形成一个管道,这个管道可以构建模型、模型评估指标并尝试不同的超参数,然后在训练数据上使用模型评价指标对每个模型进行评估。
val validator = new TrainValidationSplit().
setSeed(Random.nextLong()).
setEstimator(pipeline).
setEvaluator(multiclassEval).
setEstimatorParamMaps(paramGrid).
setTrainRatio(0.9) //这里表示训练数据实际上被TrainValidationSplit划分为90%和10%的两个子集,前面90%的数据将用于训练每个模型,剩下的10%的数据将作为交叉验证集对模型进行评估
val validatorModel = validator.fit(unencTrainData)
交叉验证集的目的是为了评估适合训练集的参数,而测试集的目的是评估适合交叉验证集的超参数,保证了对最终选定的超参数及模型准确率的无偏估计。 validator的结果包含它找到的最优模型,因此可以从结果PipelineModel中提取DecisionTreeClassificationModel的实例找到最优模型
val bestModel = validatorModel.bestModel
println(bestModel.asInstanceOf[PipelineModel].stages.last.extractParamMap)
获取每一种超参数组合训练出来的模型的准确率,超参数和评估结果分别使用getEstimatorParamMaps和validationMetrics获得,按指标排序并显示所有参数组合
val validatorModel = validator.fit(trainData)
val paramsAndMetrics = validatorModel.validationMetrics.
zip(validatorModel.getEstimatorParamMaps).sortBy(-+._1)
paramsAndMetrics .foreach { case (metric, params) =>
println(metric)
println(params)
println()
}
这个模型在交叉验证集中达到的准确率 和在测试集中达到的准确率
val bestModel = validatorModel.bestModel
println(bestModel.asInstanceOf[PipelineModel].stages.last.extractParamMap)
println(validatorModel.validationMetrics.max)
val testAccuracy = multiclassEval.evaluate(bestModel.transform(testData))
println(testAccuracy)
val trainAccuracy = multiclassEval.evaluate(bestModel.transform(trainData))
println(trainAccuracy)
2.10 随机决策森林
决策树使用一些启发式策略,来决定哪些是需要实际考虑的少数规则。在选择规则的过程中也涉及一些随机性,每次只考虑随机选择少数特征,而且只考虑训练数据中的一个随机子集。 随机决策森林:每一棵都能对正确目标值给出合理、独立且互不相同的估计,这些树的集体平均预测比任何一个体预测更接近答案,(随机性、独立性),其预测只是所有决策树预测的加权平均。 构建多棵局册数的方式注入了随机因素,每课数使用了数据的一个不同的随机子集,甚至使用随机的特征子集,这样的随机森林会大幅度避免产生过拟合。 将RandomForestClassifier替换DecisionTreeClassifier就可以使用随机森林
val classifier = new RandomForestClassifier().
setSeed(Random.nextLong()).
setLabelCol("Cover_Type").
setFeaturesCol("indexedVector").
setPredictionCol("prediction").
setImpurity("entropy").
setMaxDepth(20).
setMaxBins(300)
...
val forestModel = bestModel.asInstanceOf[PipelineModel].
stages.last.asInstanceOf[RandomForestClassificationModel]
println(forestModel.extractParamMap)
println(forestModel.getNumTrees)
forestModel.featureImportances.toArray.zip(inputCols).
sorted.reverse.foreach(println)
随机决策森林中的决策树往往都是独立构造的,总体答案的每个部分都可以通过在部分数据上独立计算完成,随机决策森林中的决策树可以并且应该只在特征子集或输入数据子集上进行训练。
2.11 进行预测
删除测试集中输入的“Cover_Type”列,即可进行预测
bestModel.transform(unencTestData.drop("Cover_Type")).select("prediction").show()
2.12 总结
分类和回归算法不只是包括决策树和决策森林,对于分类问题,Spark MLlib还包括:
朴素贝叶斯 Gradient boosting logistic 回归 多层感知机
关注「一个热爱学习的计算机小白」公众号 ,发送「森林植被」获取文章中音乐推荐源码+数据集(本小白全中文注释)。