4-Spark高级数据分析-第四章 用决策树算法预测森林植被

预测是非常困难的,更别提预测未来。

 

4.1 回归简介

 

        随着现代机器学习和数据科学的出现,我们依旧把从“某些值”预测“另外某个值”的思想称为回归。回归是预测一个数值型数量,比如大小、收入和温度,而分类则指预测标号或类别,比如判断邮件是否为“垃圾邮件”,拼图游戏的图案是否为“猫”。

        将回归和分类联系在一起是因为两者都可以通过一个(或更多)值预测另一个(或多个)值。为了能够做出预测,两者都需要从一组输入和输出中学习预测规则。在学习的过程中,需要告诉它们问题及问题的答案。因此,它们都属于所谓的监督学习。

       分类和回归是分析预测中最古老的话题。支持向量机、逻辑回归、朴素贝叶斯算法、神经网络和深度学习都属于分类和回归技术。

        本章将重点关注决策树算法和它的扩展随机决策森林算法,这两个算法灵活且应用广泛,即可用于分类问题,也可用于回归问题。更令人兴奋的是,它们可以帮助我们预测未来,至少是预测我们尚不肯定的事情。比如,根据线上行为来预测购买汽车的概率,根据用词预测邮件是否是垃圾邮件,根据地理位置和土壤的化学成分预测哪块耕地的产量可能更高。

 

4.2 向量和特征

 

      天气的特征有:

         最低气温

         最高气温

         平均气温

         多云、有雨、晴朗

       特征有时也被称为维度、预测指标或简单地称为变量。本书将特征只分为两大类:类别型特征和数值型特征。

       特征值按顺序排列,就是所谓的特征向量。

 

4.3 样本训练

       

        某一天,气温12~16摄氏度,湿度10%,晴朗,没有寒流预报,第二天最高气温为17.2摄氏度。

        特征向量为学习算法的输入提供一种结构化的方式(比如输入:12.5,15.5,0.10,晴朗,0)。预测的输出(即目标)也被称为一个特征,它是一个数值特征:17.2。

        通常我们把目标作为特征向量的一个附加特征。因此可以把整个训练样本列示为:12.5,15.5,0.10,晴朗,0,17.2。所有这些样本的集合称为训练集。

        回归和分析的区别在于:回归问题的目标为数值型特征,而分类问题的目标为类别型特征。并不是所有的回归或分类算法都能够处理类别型特征或类别型目标,有些算法只能处理数值型特征。

 

4.4 决策树和决策森林

 

        决策树算法家族能自然地处理类别型和数值型特征。决策树算法容易并行化。它们对数据中的离群点具有鲁棒性,这意味着一些极端或可能错误的数据点根本不会对预测产生影响。算法可以接受不同类型和量纲的数据,对数据类型和尺度不同的情况不需要做预处理和规范化。

        决策树可以推广为更强大的决策森林算法。本章将会把Spark MLib的DecisionTree和RandomForest算法实现应用到一个数据集上。

        基于决策树的算法还有一个优点,那就是理解和推理起来相对直观。

 

4.5 Covtype数据集

 

       下载地址是https://archive.ics.uci.edu/ml/machine-learning-databases/covtype/

       或者http://pan.baidu.com/s/1gfkDKlH

       covtype.data.gz为压缩数据文件,附带一个描述数据文件的信息文件covtype.txt。记录了美国科罗拉多州不同地块森林植被特征:海拔、坡度、到水源的距离、遮阳情况和土壤类型,并且随同给出了地块的已知森林植被类型。有581012个样本。

 

4.6 准备数据

 

        Spark MLib将特征向量抽象为LabeledPoint,它由一个包含对个特征值的Spark MLib Vector和一个称为标号(label)的目标值组成。该目标为Double类型,而Vector本质上是对多个Double类型值的抽象。这说明LabeledPoint只适用于数值型特征。但只要经过适当编码,LabeledPoint也可用于类别型特征。

 

        其中一种编码是one-hot或1-of-n编码。在这种编码中,一个有N个不同取值的类别型特征可以变成N个数值型特征,变换后的每个数值型特征的取值为0或1.在这N个特征中,有且只有一个取值为1,其他特征取值都为0。

 

4.7 第一棵决策树

 

开始时我们原样使用数据。

 

Scala:

 

import org.apache.spark.mllib.linalg._

import org.apache.spark.mllib.regression._

 

val rawData = sc.textFile("D:/Workspace/AnalysisWithSpark/src/main/java/advanced/chapter4/covtype/covtype.data")

val data = rawData.map { line =>

         val values = line.split(',').map(_.toDouble)

         val featureVector = Vectors.dense(values.init)

         val label = values.last - 1

         LabeledPoint(label, featureVector)

}

  

Java:

 1 public static JavaRDD<LabeledPoint> readData(JavaSparkContext jsc) {
 2 
 3          JavaRDD<String> rawData =jsc.textFile("src/main/java/advanced/chapter4/covtype/covtype.data");
 4 
 5         
 6 
 7          JavaRDD<LabeledPoint> data = rawData.map(line -> {
 8 
 9                    String[] values = line.split(",");
10 
11                    double[] features = new double[values.length-1];
12 
13                    for (int i = 0; i < values.length-1; i++) {
14 
15                             features[i] = Double.parseDouble(values[i]);
16 
17                    }
18 
19                    Vector featureVector = Vectors.dense(features);
20 
21                    Double label = (double) (Double.parseDouble(values[values.length-1]) - 1);
22 
23                    return new LabeledPoint(label, featureVector);
24 
25          });
26 
27          return data;
28 
29 }
30 
31  

 

本章将数据分成完整的三部分:训练集、交叉检验集(CV)和测试集。在下面的代码中你会看到,训练集占80%,交叉检验集和测试集各占10%。

 

Scala:

val Array(trainData, cvData, testData) =data.randomSplit(Array(0.8, 0.1, 0.1))

trainData.cache()

cvData.cache()

testData.cache()

  

 

Java:

 1 //将数据分割为训练集、交叉检验集(CV)和测试集
 2 
 3 JavaRDD<LabeledPoint>[] splitArray = data.randomSplit(new double[]{0.8, 0.1, 0.1});
 4 
 5 JavaRDD<LabeledPoint> trainData = splitArray[0];
 6 
 7 trainData.cache();
 8 
 9 JavaRDD<LabeledPoint> cvData = splitArray[1];
10 
11 cvData.cache();
12 
13 JavaRDD<LabeledPoint> testData = splitArray[2];
14 
15 testData.cache();
16 
17  

 

和ALS实现一样,DecisionTree实现也有几个超参数,我们需要为它们选择值。和之前一样,训练集和CV集用于给这些超参数选择一个合适值。这里第三个数据集,也就是测试集,用于对基于选定超参数的模型期望准确度做无偏估计。模型在交叉检验集上的准确度往往有点过于乐观,不是无偏差的。本章我们还将更进一步,以此在测试集上评估最终模型。

 

我们先来试试在训练集上构造一个DecisionTreeModel模型,参数采用默认值,并用CV集来计算结果模型的指标:

 

Scala:

import org.apache.spark.mllib.evaluation._

import org.apache.spark.mllib.tree._

import org.apache.spark.mllib.tree.model._

import org.apache.spark.rdd._

 

def getMetrics(model: DecisionTreeModel, data: RDD[LabeledPoint]):

                   MulticlassMetrics = {

         val predictionsAndLabels = data.map(example =>

                   (model.predict(example.features), example.label)

         )

         new MulticlassMetrics(predictionsAndLabels)

}

val model = DecisionTree.trainClassifier(

         trainData, 7, Map[Int,Int](), "gini", 4, 100)

val metrics = getMetrics(model, cvData)

  

 

Java:

 

 1 public static MulticlassMetrics getMetrics(DecisionTreeModel model, JavaRDD<LabeledPoint> data){
 2 
 3          JavaPairRDD<Object, Object> predictionsAndLabels = data.mapToPair(example -> {
 4 
 5                    return new Tuple2<Object, Object>(model.predict(example.features()), example.label());
 6 
 7          });
 8 
 9         
10 
11          return new MulticlassMetrics(JavaPairRDD.toRDD(predictionsAndLabels));
12 
13 }
14 
15  
16 
17 //构建DecisionTreeModel
18 
19 DecisionTreeModel model = DecisionTree.trainClassifier(trainData, 7, new HashMap<Integer, Integer>(), "gini", 4, 100);
20 
21  
22 
23 //用CV集来计算结果模型的指标
24 
25 MulticlassMetrics metrics = getMetrics(model, cvData);
26 
27  

 

这里我们使用trainClassifier,而不是trainRegressor,trainClassifier指示每个LabeledPoint里的目标应该当做不同的类别指标,而不是数值型特征。

 

和MulticlassMetrics一起,Spark还提供了BinaryClassificationMetrics。它提供类似MulticlassMetrics的评价指标实现,不过仅适用常见的类别型目标只有两个可能取值的情况。

 

查看混淆矩阵:

Scala:

metrics.confusionMatrix

 

Java:

 

1 //查看混淆矩阵
2 
3 System.out.println(metrics.confusionMatrix());

 

结果:

14395.0  6552.0   13.0    1.0    0.0   0.0  359.0 

5510.0   22020.0  447.0   29.0   6.0   0.0  36.0  

0.0      413.0    3033.0  82.0   0.0   0.0  0.0   

0.0      0.0      175.0   103.0  0.0   0.0  0.0   

0.0      897.0    33.0    0.0    15.0  0.0  0.0   

0.0      437.0    1210.0  99.0   0.0   0.0  0.0    

1148.0   41.0     0.0     0.0    0.0   0.0  822.0 

 

你得到的值可能稍有不同。构造决策树过程中的一些随机选项会导致分类结果稍有不同。

矩阵每一行对应一个实际的正确类别值,矩阵每一列按序对应预测值。第i行第j列的元素代表一个正确类别为i的样本被预测为类别为j的次数。因此,对角线上的元素代表预测正确的次数,而其他元素则代表预测错误的次数。对角线上的次数多是好的。但也确实出现了一些分类错误的情况,比如上面结果中没将任何一个样本类别预测为6。

 

查看准确度(或者说精确度):

Scala:

metrics.precision

 

Java:

1 //查看准确度
2 
3 System.out.println(metrics.precision());

 

结果:

0.7022196201332805

 

每个类别相对其他类别的精确度

Scala:

 

(0 until 7).map(

cat => (metrics.precision(cat), metrics.recall(cat))

).foreach(println)

  

Java:

 

1 //每个类别相对其他类别的精确度
2 
3 Arrays.asList(new Integer[]{0,1,2,3,4,5,6}).stream()
4 
5 .forEach(cat -> System.out.println(metrics.precision(cat) + "," + metrics.recall(cat)));

 

结果:

0.6738106679481018,0.6707807118254879

0.7300201914935192,0.7838858581619806

0.6303782269361617,0.8536585365853658

0.4672489082969432,0.3835125448028674

0.0,0.0

0.67,0.03877314814814815

0.6796116504854369,0.4454233969652472

 

有此可以看到每个类型准确度各有不同。

 

增加一个准确度评估:

 

Scala:

 

import org.apache.spark.rdd._

def classProbabilities(data: RDD[LabeledPoint]): Array[Double] = {

         val countsByCategory = data.map(_.label).countByValue()

         val counts = countsByCategory.toArray.sortBy(_._1).map(_._2)

         counts.map(_.toDouble / counts.sum)

}

 

val trainPriorProbabilities = classProbabilities(trainData)

val cvPriorProbabilities = classProbabilities(cvData)

trainPriorProbabilities.zip(cvPriorProbabilities).map {

         case (trainProb, cvProb) => trainProb * cvProb

}.sum

  

Java:

 1 public static List<Double> classProbabilities(JavaRDD<LabeledPoint> data) {
 2 
 3          //计算数据中每个类的样本数:(类别,样本数)
 4          Map<Double, Long> countsByCategory = data.map( x -> x.label()).countByValue();
 5 
 6          //排序
 7          List<Map.Entry<Double, Long>> categoryList = new ArrayList<>(countsByCategory.entrySet());
 8          Collections.sort(categoryList, (m1, m2) -> m1.getKey().intValue()-m2.getKey().intValue());
 9 
10          //取出样本数
11          List<Long> counts = categoryList.stream().map(x -> x.getValue()).collect(Collectors.toList());
12          Double sum = counts.stream().reduce((r, e) -> r = r + e ).get().doubleValue();
13          return counts.stream().map(x -> x.doubleValue()/sum).collect(Collectors.toList());
14 }
15 
16  
17 
18 List<Double> trainPriorProbabilities = classProbabilities(trainData);
19 List<Double> cvPriorProbabilities = classProbabilities(cvData);
20 
21 //把训练集和CV集中的某个类别的概率结成对,相乘然后相加
22 List<Tuple2<Double, Double>> mergePriorProbabilities = new ArrayList<>();
23 Arrays.asList(new Integer[]{0,1,2,3,4,5,6}).stream().forEach(i -> mergePriorProbabilities.add(new Tuple2<Double, Double>(trainPriorProbabilities.get(i), cvPriorProbabilities.get(i))));
24 System.out.println(mergePriorProbabilities.stream().map(x -> x._1*x._2).reduce((r,e) -> r=r+e).get());

 

结果:

0.3762360562429304

 

随机猜猜的准确度为37%,所以我们前面得到的70%的准确度看起来还不错。如果在决策树构建过程中试试超参数的其他值,准确度还可以提高。

 

4.8 决策树的超参数

 

控制决策树选择过程的参数为最大深度、最大桶数和不纯性度量。

最大深度只是对决策树的层数做出限制。限制判断次数有利于避免对训练数据产生过拟合。

好规则把训练集数据的目标值分为相对是同类或“纯”(pure)的子集。选择最好的规则也就意味着最小化规则对应的两个子集的不纯性。不纯性有两种常用度量方式:Gini不纯度或熵。

Gini不纯度直接和随机猜测分类器的准确度相关。熵是另一种度量不纯性的方式,它来源于信息论。Spark的实现默认采用Gini不纯度。

 

4.9 决策树调优

 

我们取不同的参数进行测验:

 

Scala:

 

val evaluations =

         for (impurity <- Array("gini", "entropy");

                   depth <- Array(1, 20);

                   bins <- Array(10, 300))

         yield {

                   val model = DecisionTree.trainClassifier(

                            trainData, 7, Map[Int,Int](), impurity, depth, bins)

                   val predictionsAndLabels = cvData.map(example =>

                            (model.predict(example.features), example.label)

                   )

                   val accuracy =

                            new MulticlassMetrics(predictionsAndLabels).precision

                   ((impurity, depth, bins), accuracy)

         }

evaluations.sortBy(_._2).reverse.foreach(println)

  

Java新建一个类:Hyperparameters进行测试

Java:

 

 1 //决策树调优
 2 
 3 List<Tuple2<Tuple3<String, Integer, Integer>, Double>> evaluations = new ArrayList<>();
 4 
 5  
 6 
 7 String[] impuritySet = new String[]{"gini", "entropy"};
 8 Integer[] depthSet = new Integer[]{1, 20};
 9 Integer[] binsSet = new Integer[]{10, 300};
10 for (String impurity : impuritySet) {
11          for (Integer depth : depthSet) {
12                    for (Integer bins : binsSet) {
13                             //构建DecisionTreeModel
14                             DecisionTreeModel model = DecisionTree.trainClassifier(trainData, 7, new HashMap<Integer, Integer>(), impurity, depth, bins);
15                             //用CV集来计算结果模型的指标
16                             MulticlassMetrics metrics = getMetrics(model, cvData); 
17 
18                             evaluations.add(new Tuple2<Tuple3<String, Integer, Integer>, Double>(new Tuple3<String, Integer, Integer>(impurity, depth, bins), metrics.precision()));
19                    }
20          }
21 }
22 
23 Collections.sort(evaluations, (m1, m2) -> (int)((m2._2-m1._2)*1000));
24 evaluations.forEach(x -> System.out.println(x._1._1() + "," + x._1._2() + "," + x._1._3() + "," + x._2));

 

结果:

entropy,20,300,0.909348831610984

gini,20,300,0.9034338084839314

entropy,20,10,0.8934436095396943

gini,20,10,0.8906752411575563

gini,1,10,0.6348504909125299

gini,1,300,0.634283061368365

entropy,1,10,0.4892962154168888

entropy,1,300,0.4892962154168888

 

前面的测试表明,目前最佳选择是:不纯性度量采用熵,最大深度为20,桶数为300,这时准确度为90.9%。

 

想要真正评估这个最佳模型在将来的样本上的表现,当然需要在没有用于训练的样本上进行评估。也就是使用第三个子集进行测试:

 

Scala:

val model = DecisionTree.trainClassifier(

trainData.union(cvData), 7, Map[Int,Int](), "entropy", 20, 300)

 

Java:

 

1 DecisionTreeModel modelR = DecisionTree.trainClassifier(trainData.union(cvData), 7, new HashMap<Integer, Integer>(), "entropy", 20, 300);
2 MulticlassMetrics metricsR = getMetrics(modelR, testData);
3 System.out.println(metricsR.precision());

 

结果:

0.91562526784949

 

结果准确度为91.6%。估计还算可靠吧。决策树在一定程度上存在对训练数据的过拟合。减小最大深度可能会使过拟合问题有所改善。

 

4.10 重谈类别型特征

 

数据中类别型特征使用one-hot编码,这种编码迫使决策树算法在底层要单独考虑类别型特征的每一个值,增加内存使用量并且减慢决策速度。我们取消one-hot编码:

 

Scala:

 

val data = rawData.map { line =>

         val values = line.split(',').map(_.toDouble)

         val wilderness = values.slice(10, 14).indexOf(1.0).toDouble

         val soil = values.slice(14, 54).indexOf(1.0).toDouble

         val featureVector =

                   Vectors.dense(values.slice(0, 10) :+ wilderness :+ soil)

         val label = values.last - 1

         LabeledPoint(label, featureVector)

}

 

val evaluations =

         for (impurity <- Array("gini", "entropy");

                   depth <- Array(10, 20, 30);

                   bins <- Array(40, 300))

         yield {

                   val model = DecisionTree.trainClassifier(

                            trainData, 7, Map(10 -> 4, 11 -> 40),

                            impurity, depth, bins)

                   val trainAccuracy = getMetrics(model, trainData).precision

                   val cvAccuracy = getMetrics(model, cvData).precision

                   ((impurity, depth, bins), (trainAccuracy, cvAccuracy))

         }

  

复制Hyperparameters命名为CategoricalFeatures,修改readData和决策树参数。

Java:

 1 public static JavaRDD<LabeledPoint> readData(JavaSparkContext jsc) {
 2          JavaRDD<String> rawData =jsc.textFile("src/main/java/advanced/chapter4/covtype/covtype.data");
 3 
 4          JavaRDD<LabeledPoint> data = rawData.map(line -> {
 5                    String[] values = line.split(",");
 6                    double[] features = new double[12];
 7                    for (int i = 0; i < 10; i++) {
 8                             features[i] = Double.parseDouble(values[i]);
 9                    }
10                    for (int i = 10; i < 14; i++) {
11                             if(Double.parseDouble(values[i]) == 1.0){
12                                      features[10] = i-10;
13                             }
14                    }
15 
16                    for (int i = 14; i < 54; i++) {
17                             if(Double.parseDouble(values[i]) == 1.0){
18                                      features[11] = i-14;
19                             }
20                    }
21 
22                    Vector featureVector = Vectors.dense(features);
23                    Double label = (double) (Double.parseDouble(values[values.length-1]) - 1);
24                    return new LabeledPoint(label, featureVector);
25          });
26          return data;
27 }
28 
29  
30 
31 List<Tuple2<Tuple3<String, Integer, Integer>, Double>> evaluations = new ArrayList<>();
32 Map<Integer, Integer> map = new HashMap<Integer, Integer>();
33 map.put(10, 4);
34 map.put(11, 40);
35 String[] impuritySet = new String[]{"gini", "entropy"};
36 Integer[] depthSet = new Integer[]{10, 20, 30};
37 Integer[] binsSet = new Integer[]{40, 300};
38 for (String impurity : impuritySet) {
39          for (Integer depth : depthSet) {
40                    for (Integer bins : binsSet) {
41                             //构建DecisionTreeModel
42                             DecisionTreeModel model = DecisionTree.trainClassifier(trainData, 7, map, impurity, depth, bins);
43                             //用CV集来计算结果模型的指标
44                             MulticlassMetrics metrics = getMetrics(model, cvData);
45 
46                             evaluations.add(new Tuple2<Tuple3<String, Integer, Integer>, Double>(new Tuple3<String, Integer, Integer>(impurity, depth, bins), metrics.precision()));
47                    }
48          }
49 }
50 
51 Collections.sort(evaluations, (m1, m2) -> (int)((m2._2-m1._2)*1000));
52 evaluations.forEach(x -> System.out.println(x._1._1() + "," + x._1._2() + "," + x._1._3() + "," + x._2));
53 
54  

 

结果:

entropy,30,300,0.9420165175498968

entropy,30,40,0.9400378527185134

gini,30,40,0.9340674466620784

gini,30,300,0.9330006882312457

gini,20,40,0.9227288368891947

entropy,20,40,0.923554714384033

entropy,20,300,0.9233138334480385

gini,20,300,0.9211286992429456

gini,10,40,0.7867859600825877

gini,10,300,0.7862869924294563

entropy,10,40,0.7863214039917412

entropy,10,300,0.779525120440468

 

准确度为94.2%。比以前更好。

 

4.11 随机决策森林

 

随机决策森林是由多个决策树独立构造而成。

 

Scala:

 

val forest = RandomForest.trainClassifier(

trainData, 7, Map(10 -> 4, 11 -> 40), 20,

"auto", "entropy", 30, 300)

  

Java:

 

 1 Map<Integer, Integer> map = new HashMap<Integer, Integer>();
 2 map.put(10, 4);
 3 map.put(11, 40);
 4 
 5 //构建RandomForestModel
 6 RandomForestModel model = RandomForest.trainClassifier(trainData, 7, map, 20, "auto", "entropy", 30, 300, Utils.random().nextInt());
 7 
 8 //用CV集来计算结果模型的指标
 9 MulticlassMetrics metrics = getMetrics(model, cvData);
10 System.out.println(metrics.precision());

结果:

0.964827502890772

 

准确率96.5%。

在大数据背景下,随机决策森林非常有吸引力,因为决策树往往是独立构造的,诸如Spark和MapReduce这样的大数据技术本质上适合数据并行问题。

4.12 进行预测

Scala:

val input = "2709,125,28,67,23,3224,253,207,61,6094,0,29"

val vector = Vectors.dense(input.split(',').map(_.toDouble))

forest.predict(vector)

 

Java:

 

1 double[] input = new double[] {(double) 2709,(double) 125,(double) 28,(double) 67,(double) 23,(double) 3224,(double) 253,(double) 207,(double) 61,(double) 6094,(double) 0,(double) 29};
2 Vector vector = Vectors.dense(input);
3 System.out.println(model.predict(vector));

 

结果:

4.0

 

4.0对应原始Covtype数据集中的类别5(元素特征从1开始)。(在本地算了半个小时 ㄒoㄒ )。

 

4.13 小结

 

本章介绍分类和回归。介绍概念:特征、向量、训练和交叉检验。

一般情况,准确度超过95%是很难达到的。通常,通过包括更多特征,或将已有特征转换成预测性更好的形式,我们可以进一步提高准确度。

分类和回归算法不只包括决策树和决策森林,Spark Mlib 实现的算法也不限于决策树和决策森林。对分类问题,Spark MLib提供的实现包括:

         朴素贝叶斯

         支持向量机

         逻辑回归

他们接受一个LabeledPoint类型的RDD作为输入,需要通过将输入数据划分为训练集、交叉检验集和测试集来选择超参数。

 

posted on 2016-08-19 11:49  龙猫先生  阅读(6410)  评论(0编辑  收藏  举报

导航