算法介绍

回归和分类

回归算法和分类算法通常会被联系在一起,因为两者都可以通过一个或者多个值来预测一个或者多个值 he
为了能够做出预测,两者需要从一组输入和输出中学习预测规则,在学习过程中需要告诉它们问题以及问题的答案
因此,回归和分类都属于监督学习类的算法

回归是预测一个数值型的结果,例如温度,成绩等
分类是预测一个标号或者类别,例如邮件是否为辣鸡邮件,一个人是属于哪个人种

这里将使用决策树随机森林算法来完成工作,这两种算法灵活且被广泛应用
既可以用于分类问题也可以用于回归问题

特征识别

假设我们要通过某饭店商家今天的销售额来预测明天的销售额,使用机器学习算法完全没有问题
但是在这之前我们要做一些操作,今天的销售额是一个非常广泛的概念,无法直接应用到算法中
那么我们就需要对今天的销售额进行特征提取和识别,这些特征有助于我们预测明天的销售额
例如:

  • 今天的最低人流量:20
  • 今天的最高人流量:200
  • 今天平均上菜速度:3
  • 忙时是早上、中午还是晚上:中午
  • 隔壁饭店的平均人流量:60

等等,特征有时候也被称为维度或者预测指标,以上的每个特征都能够被量化,例如
今天的最低人流量为20,单位人
今天平均上菜速度为3,单位分钟

因此,今天的销售额就可以简化为一个值列表:
20,200,3,中午,60

这几个特征顺序排列就组成了特征向量,一个特征向量可以用来描述该商家每天的销售额

关于如何提取特征,要根据实际的业务场景来操作,提取出的特征能够贴切的描述具体的业务场景,这个过程甚至需要业务专家的参与

相信你已经注意到了,示例中的特征向量中并不是每个值都是数值型,例如中午
在实际应用中,这种情况很常见,通常把特征分为两种:

  • 数值型特征:可以直接使用数字表示,数字的小大是有意义的
  • 类别型特征:在几个离散值中选取一个,离散值之间的大小是没有意义的

训练样本

为了进行预测,监督学习算法需要在大量的数据集上进行训练,这些数据集不仅要包含数据特征输入,还要包含正确的输入
如上例中的特征向量是不能够被作为训练样本的,因为其没有正确的输出–今天的销售额
如果将今天的销售额2000也作为一个特征加入该数据中,该数据就能够作为一个训练样本,为机器学习算法提供了一个结构化的输入:
20,200,3,中午,60,2000

注意,这里的输出是一个数值型
回归和分类的区别在于:回归的目标为数值型特征,分类的目标为类别型特征
并不是所有的回归、分类算法都能够处理类别型特征或者类别型目标,有些算法只能处理数值型

我们可以通过适当的转换规则将类别型特征转换为数值型特征,例如:

  • 早上:1
  • 中午:2
  • 晚上:3

用数字123分别代表早上、中午和晚上,那么该训练样本就可以表示为:
20,200,3,2,60,2000

注意:有些算法对对数值型特征的大小进行猜测,一旦将类别型特征转换为数值型特征可能会造成一些影响,因为类别型特征本身的大小是没有意义的

决策树

决策树算法家族能够自然的处理署执行和类别型特征
什么是决策树呢?
事实上,我们在生活中无意中就会用到决策树所体现的推理方法,例如:
看到一部满意的新手机,但是旧手机还能用,买还是不买呢?
于是我会经历一下的过程:

1.旧手机是否已经无法忍受?是,就买
2.上一步为否,那么新手机是否能够达到非买不可的地步?是,就买
3.上一步为否,新手机的价格是否达到可以随意购买的地步?是,就买
4.上一步为否,新手机的综合性价比能否和旧手机甩开一个距离?是,就买
5.上一步为否,那么就不买

可以看到,决策树推理的过程就是一系列的是/否,在程序中表达就是一堆if/else
但是决策树有个很严重的缺点过度拟合,如何理解?
即,用训练数据训练出来的模型在训练数据中表现优异,但是对新的数据无法做出合理的预测
决策树训练出来的模型可能会对训练数据有过度拟合的问题

随机森林

随机森林是由多个随机的决策树组成的,何为随机的决策树?
随机森林中的每颗决策树中,使用的训练数据是总训练数据中随机的一部分,每层使用的决策规则都是随机选定的
随机森林的做法是属于集体智慧的,集体的平均预测应该比任何一个个体的预测要准确
正是因为随机森林构建过程中的随机性,才有了这种独立性,这是随机森林的关键

因为每颗决策树所使用的决策都是随机的一部分,所以随机森林得以有足够的时间来构建多颗决策树
也正是因为如此,森林中的每棵树更不会产生过度拟合的问题,因为使用的决策是随机的一部分

随机森林的预测是所有决策树的加权平均

程序开发

数据集

示例中将使用Covtype的森林植被数据集,该数据集是公开的,可以在线下载
该数据集的每个样本描述每块土地的若干特征,包括海拔、坡度、到水源的距离、遮阳情况和土壤类型等54个特征,并给出了目标特征–每块土地的森林植被类型

Spark Mllib的决策树

这里将使用Spark中的Mllib机器学习库,Mllib将特征向量抽象为LabeledPoint,关于LabeledPoint的具体介绍请看:
Spark(十一) – Mllib API编程 线性回归、KMeans、协同过滤演示

LabeledPoint中的Vector本质上是对多个Double类型的抽象,所以LabeledPoint只能处理数值型特征,样本数据集中已经将类别型的特征转换为数值型特征了
Mllib中的DecisionTree的输入为LabeledPoint类型,所以读取数据之后,我们需要将数据转换为LabeledPoint

val conf = new SparkConf().setAppName("DecisionTree")
val sc = new SparkContext(conf)
//读取数据
val rawData = sc.textFile("/spark_data/covtype.data")
//转换为为LabeledPoint
val data = rawData.map { line =>
  val values = line.split(",").map(_.toDouble)
  //init返回除了最后一个元素的所有元素,作为特征向量
  val feature = Vectors.dense(values.init)
  //返回最后一个目标特征,由于决策树的目标特征规定从0开始,而数据是从1开始的,所以要-1
  val label = values.last - 1
  LabeledPoint(label, feature)
}

为了能够评估训练出来的模型的准确度,我们可以将数据集划分为三部分:训练集、交叉验证集和测试集,各占80%,10%和10%

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

接下来我们先训练出一个决策树模型来看看猪长什么样

/**
  * 获得评估指标
  *
  * @param model 决策树模型
  * @param data  用于交叉验证的数据集
  **/
def getMetrics(model: DecisionTreeModel, data: RDD[LabeledPoint]): MulticlassMetrics = {
  //将交叉验证数据集的每个样本的特征向量交给模型预测,并和原本正确的目标特征组成一个tuple
  val predictionsAndLables = data.map { d =>
    (model.predict(d.features), d.label)
  }
  //将结果交给MulticlassMetrics,其可以以不同的方式计算分配器预测的质量
  new MulticlassMetrics(predictionsAndLables)
}

val model = DecisionTree.trainClassifier(trainData, 7, Map[Int, Int](), "gini", 4, 100)
val metrics = getMetrics(model, cvData)

DecisionTree有两种训练模型的方法:trainClassifier和trainRegressor分别对应分类和回归问题
trainClassifier的参数:

1.训练数据集
2.目标类别个数,即结果有几种选择
3.Map中的键值分别对应Vector下标和该下标对应类别特征的取值情况,空表示所有特征都是数值型(为了方便,示例中直接取空,实际当中并不能这么使用)
4.不纯性(impurity)度量:gini或者entropy,不纯度用来衡量一个规则的好坏,好的规则可以将数据划分为等值的两部分,坏规则则相反
5.决策树的最大深度,越深的决策树越有可能产生过度拟合的问题
6.决策树的最大桶数,每层使用的决策规则的个数,越多就可能精确,花费的时候也就越多,最小的桶数应该不小于类别特征中最大的选择个数

现在来看看metrics指标中的混淆矩阵:

System.out.println(metrics.confusionMatrix.toString())

结果为:

14411.0  6564.0   17.0    1.0    0.0   0.0  317.0
5444.0   22158.0  449.0   22.0   4.0   0.0  43.0
0.0      415.0    3022.0  95.0   0.0   0.0  0.0
0.0      0.0      159.0   112.0  0.0   0.0  0.0
0.0      895.0    34.0    0.0    14.0  0.0  0.0
0.0      422.0    1228.0  108.0  0.0   0.0  0.0
1112.0   28.0     0.0     0.0    0.0   0.0  903.0

因为目标类别有7个,所以混淆矩阵是一个7*7的矩阵,每一行对应一个正确的目标特征值,每一列对应一个预测的目标特征
第i行第j列表示一个正确类别为i的样本被预测为j的次数,所以对角线上的元素代表预测正确的次数,其他表示预测错误的次数
另外,模型的精准度也可以用一个数字来表示:

System.out.println(metrics.precision)

结果为0.6996041965535718,准确率大概在69%左右

除了准确度,还有召回率等概念,还可以为每个目标特征单独查看其准确度等信息:

(0 until 7).map(target => (metrics.precision(target), metrics.recall(target))).foreach(println)

下面为输出结果:

(0.6845289541918755,0.6708390193402664)
(0.7237410535817912,0.7904577464788732)
(0.6385618166526493,0.8483240223463687)
(0.5800865800865801,0.44966442953020136)
(0.0,0.0)
(0.7283950617283951,0.03460410557184751)
(0.6814580031695721,0.4405737704918033)

可以看到,每个类别的准确度都不同,而且很诡异的是第5个类别的准确度为0,所以这个模型并不是我们想要的
我们想要的是一个符合实际的,准确度尽可能高的模型,那么该如何确定模型训练的参数呢?
可以在训练模型的时候选择不同的组合,并反馈出每个组合训练处的模型准确度结果,那么我们就可以从中选出最好的那个模型来使用:

/**
  * 在训练数据集上得到最好的参数组合
  * @param trainData 训练数据集
  * @param cvData 交叉验证数据集
  * */
def getBestParam(trainData: RDD[LabeledPoint], cvData: RDD[LabeledPoint]): Unit = {
  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 metrics = getMetrics(model, cvData)
    ((impurity, depth, bins), metrics.precision)
  }
  evaluations.sortBy(_._2).reverse.foreach(println)
}

执行的结果为:

((entropy,20,300),0.9123611325743918)
((gini,20,300),0.9062140490049623)
((entropy,20,10),0.8948814368378578)
((gini,20,10),0.8902625388485379)
((gini,1,300),0.6352272532151995)
((gini,1,10),0.6349525232232697)
((entropy,1,300),0.4855337488624461)
((entropy,1,10),0.4855337488624461)

可以看到最好的组合为第一个,准确度在91%左右,我们可以再查看一下使用最好参数训练出来的模型,各个类别的准确度为多少:

(0.899412455934195,0.9029350698376746)
(0.9193229901269393,0.9203289914928166)
(0.9222857142857143,0.9238694905552376)
(0.8263888888888888,0.8623188405797102)
(0.8294663573085846,0.7663451232583065)
(0.8596802841918295,0.8491228070175438)
(0.9454369869628199,0.9275225011842728)

现在看来比之前的要好上很多了吧

直到这里,我们一直没有使用到10%的测试数据集,如果说10%的交叉验证数据集的作用是确定在训练数据集上训练出来的模型的最好参数
那么测试数据集的作用就是评估CV数据集的最好参数,可以将训练集和CV集结合起来,然后对测试集进行以上的步骤验证来得到一个合适的参数组合

随机森林

在Spark Mllib中使用随机森林的方式和决策树差不多:

val forest = RandomForest.trainClassifier(trainData, 7, Map(10 -> 4, 11 -> 40), 20, "auto", "entropy", 30, 300)

和构架决策树的时候不同,这里不再使用空的Map
Map(10 -> 4, 11 -> 40)表示特征向量中下标为10的是类别型特征,类别取值有4个;下标为11的类别特征向量取值有40个
此时,随机森林不会再对10和11这两个特征向量做出“大小推测”等数值型特征才会有的操作
这样明显更加贴近实际,所得到的模型质量也会更好

此外随机森林还多了两个参数:

1.森林中有多少颗决策树,示例中取值为20
2.每层决策规则的选择方法,示例中为”auto”

按照决策树的评估流程对随机森林模型进行测试,可以得到关于准确度的一些指标进而衡量模型质量

至此,关于决策树和随机森林算法的模型已经构建完毕,可以使用这个模型对数据进行预测

作者:@小黑