[关闭]
@xtccc 2016-04-28T01:13:01.000000Z 字数 14294 阅读 3725

Decision Tree

给我写信
GitHub

此处输入图片的描述

数据挖掘&机器学习


目录


信息论基础


对于包含以下4个符号的系统,A, B, C, D,我们可以有多种方法对其进行编码。例如:

方法1:

符号 编码 出现概率
A 00 0.1
B 01 0.2
C 10 0.2
D 11 0.5

方法2:

符号 编码 出现概率
A 0 0.2
B 10 0.2
C 110 0.3
D 111 0.3

在方法1中,平均每个符号的编码占用2个bits;在方法2中,平均每个符号的编码占用2.4个编码。其他的编码方式也许需要更多或更少的信息量来进行编码。

Entropy(熵)

那么,平均对每个符号进行编码的信息量的最小值是多少呢?这与每个字符在系统中的概率分布有关。

假设系统中有N个符号,符号出现的概率为,那么对该系统中每个符号进行编码平均所需的信息量(bits的数量)为:

这里的H(X)就是变量X的熵(entropy)。

Conditional Entropy(条件熵)

假设数据集有2个attribute,分别是X和Y,例如:

X Y
Math Yes
History No
CS Yes
Math No
Math No
CS Yes
History No
Math Yes

那么,可以算出,
H(X) = 1.5, H(Y)=1。
H(Y|X=Math) = 1,H(Y|X=History) = 0,H(Y|X=CS) = 0。

可见,随机变量分布得越均匀,它的熵就越小

条件熵H(Y|X)的算法为:

Infogamtion Gain

信息增益的值总是非负的。




决策树算法


基本

决策树算法是一个贪心算法Greedy Algorithm,常常用于分类与回归。Tree ensemble algorithms(例如 random forestsboosting)常常是用于解决分类与回归问题的最好算法。

重点

决策树算法是一个supervised learning,因为训练集会提供每一个tuple的class tag。

决策树很容易理解,不多赘述了。其中,最关键的步骤是怎样选择一个最重要的attribute,并根据它的值来对数据集进行切分。

所谓最重要的attribute,是指用该attribute来切分数据集时,能够使得由数据集切分出的子集尽可能的纯净。

例如,如果按照某个attribute能将数据集切分成n个子集,且每个子集中的tuples都属于各自的class,那么,这个attribue就可以认为是最重要的attribute。因为这些切分出的子集,都是纯净的(**pure**)子集。

或者可以这样表述:最重要的attribute,是指用该attribute来切分数据集的前后,information gain最大

那么,怎样来选择最重要的attribute呢?有三种方法:
1. Information Gain (ID3算法使用)
2. Gain Ration (C4.5算法使用)
3. Gini Index (CART算法使用)




Attribute Selection Measures


构造决策树时,在每一个节点处,需要决定该节点选择哪一个attribute来对相应的数据集进行划分。理论上最好的attribute,将使得划分出的每一个子集中的数据都属于同一个class(这样的子集被称为pure partition)。当然实际上不大可能每一个节点都做到如此。

将训练数据集记为,含有个class,其中第个class的子集记为

数据集的Entropy

Entropy被用于描述一个集合中的内容的不纯净程度(impurity),它在数学上的定义为:


这里,D为全集,共包含m个不同的class,为D中第i个class所占的比例,即

D的entropy越高,它所包含的信息量越大。

如果D中的数据全部属于同一个class,那么D的entropy为0,显然这对于数据挖掘没什么用;如果D中有50%数据属于class A,50%数据属于class B,那么D的entropy为1,对于数据挖掘而言这是一个非常好的数据集。

在构造决策树时,我们希望知道:决定D中数据点属于哪一个class时,哪一个attribute是最重要的? 而Information Gain就可以告诉我们哪一个attribute是最重要的。

Information Gain (信息增益)

关于Information Gain的概念, 这篇文档 讲的不错。

ID3算法采用了信息增益来选择最重要的attribute。

对于决策树中的dataset而言,它的信息增益为:


这里的entropyAfterSplit是决策树中某节点的所有branch中数据的entropy的带权之和,一个branch的权重就是该branch中的tuples数量与该节点中tuples数量之比。

如果将数据集按照attribute A进行划分,同时A在数据集中有v个值,并且按照A的属性值可以将D划分为对应的v个partition,即 , , ..., ,其中中数据点的属性A的值为。那么,数据集D在属性A处的entropy为:


数据集D在属性A处的信息增益为:

对数据集的每一个attribute都计算其信息增益,选择信息增益最大的attribute来切分数据集

举个例子:

QQ20151128-0@2x.png-219.1kB




Building the Decision Tree


构造决策树的过程是一个递归的过程。

  1. 首先,选择数据集内信息增益最大的attribute作为根节点;
  2. 为根节点的attribute的每种value分别创建一个branch,将对应的tuple归入该branch内。对每个branch内的数据子集,继续构造决策树。
  3. 重复以上步骤, 直到某一个branche满足以下任意条件时停止:
    a). 所有的attribue都被用过了,如果此时该branch内的tuples不属于同一个class,则采用多数人投票的方法,为该branch添加一个leaf node,并为其标记一个class tag;
    b). 该branch内的所有数据点都属于同一个class,此时为该节点创建一个leaf node,并将其标记一个class tag;




Python代码实现


下面用Python代码来实现决策树的构造过程。为了简单起见,这里只支持nominal类型的数据。
假如我们有以下数据,来判断一个tuple是不是一条鱼:

Can survive without
coming to surface?
Hash flippers? Is it a fish?
1 1 yes
1 1 yes
1 0 no
0 1 no
0 1 no

这里,前两列是数据集的两种feature,最后一列是对应的class。
在下面的代码中,我们将假设数据集都是这种形式的。

创建数据集

  1. def createDataset():
  2. dataset = [
  3. [1, 1, 'yes'],
  4. [1, 1, 'yes'],
  5. [1, 0, 'no'],
  6. [0, 1, 'no'],
  7. [0, 1, 'no']
  8. ]
  9. labels = ["Can Survive", "Has Flipper"]
  10. return dataset, labels


计算entropy

首先回忆一下怎样计算一个数据集D的entropy。

Entropy被用于描述一个集合中的内容的不纯净程度(impurity),它在数学上的定义为:


这里,D为全集,共包含m个不同的class,为D中第i个class所占的比例,即

下面是实现代码。

  1. def calcShannonEntropy(dataset):
  2. classCount = {} # 每一种class的数量
  3. datasetSize = len(dataset)
  4. # 计算每一种class的在数据集中出现的次数
  5. for t in dataset:
  6. classTag = t[-1]
  7. if classTag not in classCount.keys():
  8. classCount[classTag] = 0
  9. classCount[classTag] += 1
  10. # 计算entropy
  11. entropy = 0.0
  12. for classTag in classCount.keys():
  13. prob = classCount[classTag]/float(datasetSize)
  14. entropy -= prob*log(prob, 2)
  15. return entropy


按照feature对数据集进行划分

  1. ############################################################
  2. # 对数据集按照某个feature进行切分
  3. # 其中,dataset的每个元素是一个一维数组(长度为N),前N-1个元素每个
  4. # 都是1个feature,最后一个元素是该数据的class tag。
  5. # 本方法要按照第`splitIndex`个feature对数据集进行切分,
  6. # 返回该feature的值等于`value`的所有记录
  7. # splitIndex的范围在[0, N-1]之间:
  8. # 返回的子集中,已经删除了splitIndex对应的feature,即返回的子集
  9. # 中的每条记录含有N-2个feature
  10. ############################################################
  11. def splitDataset(dataset, splitIndex, value):
  12. subset = []
  13. for t in dataset:
  14. if t[splitIndex] == value:
  15. reducedVec = t[:splitIndex]
  16. reducedVec.extend(t[splitIndex+1:])
  17. subset.append(reducedVec)
  18. return subset

请注意Python中List的两个方法(extendappend)的区别。


计算最重要的feature

回顾一下什么才算是最重要的feature。

如果将数据集按照attribute A进行划分,同时A在数据集中有v个不同的值,并且按照A的属性值可以将D划分为对应的v个partition,即 , , ..., ,其中中数据点的属性A的值为。那么,数据集D在属性A处的entropy为:


数据集D在属性A处的信息增益为:

对数据集的每一个attribute都计算其信息增益,信息增益最大的attribute就是最重要的feature(最能帮助划分数据)。



代码实现如下。

  1. ############################################################
  2. # 对于包含N-1个feature的数据集,从中找出信息增益最大的feature,来
  3. # 作为该数据集的splitting criteria
  4. # 返回的是最重要的feature在数据集的每条tuple中的索引
  5. ############################################################
  6. def chooseBestSplitCriteria(dataset):
  7. datasetSize = len(dataset)
  8. baseEntropy = calcShannonEntropy(dataset) # 数据集D的熵
  9. featNum = len(dataset[0])-1 # feature的数量(维度)
  10. bestCriteria = -1 # 最佳split属性的index
  11. maxInfoGain = 0.0 # 这里有个前提:info gain总是不小于0的!!
  12. for i in range(featNum): # 针对每一维的feature来计算
  13. featureValues = [t[i] for t in dataset]
  14. distinctFeatValues = set(featureValues)
  15. subEntropy = 0.0 # D在每一个属性处的entropy
  16. for v in distinctFeatValues: # 针对该feature的每一种值来计算
  17. subset = splitDataset(dataset, i, v)
  18. prob = len(subset)/float(datasetSize)
  19. subEntropy += prob * calcShannonEntropy(subset)
  20. # 调试用:
  21. print "attribute index -> ", i, "\t subEntropy -> ", subEntropy,
  22. "\t info gain -> ", baseEntropy - subEntropy
  23. if baseEntropy - subEntropy > maxInfoGain: # 寻找最大的信息增益
  24. maxInfoGain = baseEntropy - subEntropy
  25. bestCriteria = i
  26. return bestCriteria


构建Decision Tree

这里,我们用Josn的格式来表征一棵树,即:

  1. { root : {
  2. child1: { ... },
  3. child2: { ... },
  4. ...
  5. childN : { ... }
  6. }
  7. }



回顾构造决策树的过程。

构造决策树的过程是一个递归的过程。

  1. 首先,选择数据集内信息增益最大的attribute作为根节点;
  2. 为根节点的attribute的每种value分别创建一个branch,将对应的tuple归入该branch内。对每个branch内的数据子集,继续构造决策树。
  3. 重复以上步骤, 直到某一个branche满足以下任意条件时停止:
    a). 所有的attribue都被用过了,如果此时该branch内的tuples不属于同一个class,则采用多数人投票的方法,为该branch添加一个leaf node,并为其标记一个class tag;
    b). 该branch内的所有数据点都属于同一个class,此时为该节点创建一个leaf node,并将其标记一个class tag;



实现代码如下:

  1. ############################################################
  2. # 为一个数据集(nominal类型)构建一颗decision tree
  3. # attrLabels是所有attribues label的列表,其中的label的顺序
  4. # 与dataeset中每个tuple中的attribute value的顺序必须是一致的
  5. ############################################################
  6. def createDecisionTree(dataset, featLabels):
  7. classValueList = [t[-1] for t in dataset] # 取出全部的class values
  8. # 如果dataset中全部的数据都属于同一个class,则返回该class
  9. if classValueList.count(classValueList[0]) == len(classValueList):
  10. return classValueList[0]
  11. # 如果dataset中已经没有attribute了,则返回所有class中的majority
  12. if (len(dataset[0]) == 1):
  13. return majorClass(classValueList)
  14. # 找出目前dataset中最重要的feature的index
  15. bestFeatIdx = chooseBestSplitCriteria(dataset)
  16. bestFeatLabel = featLabels[bestFeatIdx]
  17. subFeatLabels = featLabels[:]
  18. del(subFeatLabels[bestFeatIdx]) # 去掉已经用过的feature label
  19. tree = { bestFeatLabel:{ } } # 构造根节点
  20. # 下面处理根节点的每一个branch
  21. featValueSet = set([t[bestFeatIdx] for t in dataset])
  22. for featValue in featValueSet:
  23. tree[bestFeatLabel][featValue] = createDecisionTree(
  24. splitDataset(dataset, bestFeatIdx, featValue),
  25. subFeatLabels)
  26. return tree
  27. ############################################################
  28. # 找出最主流的class(按照频率计算)
  29. ############################################################
  30. def majorClass(classList):
  31. classCountMap = {}
  32. for clz in classList:
  33. if clz not in classCountMap.keys():
  34. classCountMap[clz] = 1
  35. else:
  36. classCountMap[clz] += 1
  37. # 按照value进行排序,并返回频次最高的class的tag
  38. return sorted(classCountMap.iteritems(), key=operator.itemgetter(1), reverse=True)[0][3]




测试数据验证


例1

我们要本文上面提到的数据集来验证。

数据集如下:

Can survive without
coming to surface?
Hash flippers? Is it a fish?
1 1 yes
1 1 yes
1 0 no
0 1 no
0 1 no



创建数据集的代码为

  1. def createDataset():
  2. dataset = [
  3. [1, 1, 'yes'],
  4. [1, 1, 'yes'],
  5. [1, 0, 'no'],
  6. [0, 1, 'no'],
  7. [0, 1, 'no']
  8. ]
  9. labels = ["Can Survive", "Has Flipper"]
  10. return dataset, labels



调用createDecisionTree方法

  1. dataset, labels = createDataset()
  2. print json.dumps(createDecisionTree(dataset, labels), indent=4)



输出结果如下:

  1. {
  2. "Survive": {
  3. "0": "no",
  4. "1": {
  5. "Fish": {
  6. "0": "no",
  7. "1": "yes"
  8. }
  9. }
  10. }
  11. }


例2

我们再用《Data Mining: Concepts and Techniques》中的例子来测试。

数据集如下:

QQ20151127-0@2x.png-200kB



创建数据集:

  1. ############################################################
  2. # 再用一组新数据来测试
  3. # 数据来源:《Data Mining: Concepts and Techniques》
  4. ############################################################
  5. def generateDataSet():
  6. dataset = [
  7. ['youth', 'high', 'no', 'fair', 'no'],
  8. ['youth', 'high', 'no', 'excellent', 'no'],
  9. ['middle_aged', 'high', 'no', 'fair', 'yes'],
  10. ['senior', 'medium', 'no', 'fair', 'yes'],
  11. ['senior', 'low', 'yes', 'fair', 'yes'],
  12. ['senior', 'low', 'yes', 'excellent', 'no'],
  13. ['middle_aged', 'low', 'yes', 'excellent', 'yes'],
  14. ['youth', 'medium', 'no', 'fair', 'no'],
  15. ['youth', 'low', 'yes', 'fair', 'yes'],
  16. ['senior', 'medium', 'yes', 'fair', 'yes'],
  17. ['youth', 'medium', 'yes', 'excellent', 'yes'],
  18. ['middle_aged', 'medium', 'no', 'excellent', 'yes'],
  19. ['middle_aged', 'high', 'yes', 'fair', 'yes'],
  20. ['senior', 'medium', 'no', 'excellent', 'no']
  21. ]
  22. labels = ['age', 'income', 'student', 'credit_rating', 'buys_computer']
  23. return dataset, labels



调用createDecisionTree方法

  1. dataset, labels = generateDataSet()
  2. print json.dumps(createDecisionTree(dataset, labels), indent=4)



输出结果为:

  1. {
  2. "age": {
  3. "youth": {
  4. "student": {
  5. "yes": "yes",
  6. "no": "no"
  7. }
  8. },
  9. "senior": {
  10. "credit_rating": {
  11. "fair": "yes",
  12. "excellent": "no"
  13. }
  14. },
  15. "middle_aged": "yes"
  16. }
  17. }



这与书上构造出的决策树是一样的。
QQ20151127-1@2x.png-189.6kB




使用决策树来预测分类


上面已经构建出了决策树,下面将用构建出的决策树预测输入数据的分类情况。

  1. ###################################################################
  2. # tree : 构建出的决策树
  3. # featLabels : feature的名字的列表,例如 ["Survive", "Fish"]
  4. # testVec:一条测试数据,但是不知道其class
  5. ###################################################################
  6. def classify(tree, featLabels, testVec):
  7. firstStr = tree.keys()[0]
  8. innerDict = tree[firstStr]
  9. featIndex = featLabels.index(firstStr)
  10. for key in innerDict.keys() :
  11. if key == testVec[featIndex]:
  12. if type(innerDict[key]).__name__ == 'dict':
  13. return classify(innerDict[key], featLabels, testVec)
  14. else:
  15. return innerDict[key]
  16. # 测试新的数据的预测分类
  17. dataset, labels = createDataset()
  18. decisionTree = createDecisionTree(dataset, labels)
  19. # print decisionTree
  20. print classify(decisionTree, ["Survive", "Fish"], [1, 0])
  21. dataset, labels = trees.generateDataSet()
  22. decisionTree = trees.createDecisionTree(dataset, labels)
  23. print classify(decisionTree, ['age', 'income', 'student', 'credit_rating'], ['senior', 'medium', 'no', 'excellent'])




使用MLLib中的决策树工具


参考:
+

Spark版本:CDH5.4.7 - Spark 1.3

我们对上面例1和例2中的数据,应用MLLib来构建决策树。

  1. package cn.gridx.spark.examples.MLlib.algo
  2. import org.apache.log4j.{Level, Logger}
  3. import org.apache.spark.{SparkConf, SparkContext}
  4. import org.apache.spark.mllib.linalg.Vectors
  5. import org.apache.spark.mllib.regression.LabeledPoint
  6. import org.apache.spark.mllib.tree.DecisionTree
  7. import org.apache.spark.mllib.tree.model.DecisionTreeModel
  8. /**
  9. * Created by tao on 11/30/15.
  10. *
  11. * 对MLlib中的决策树工具的使用例子(Spark 1.3)
  12. *
  13. * 参考: http://web.cs.ucla.edu/~linmanna/cs239/
  14. * https://spark.apache.org/docs/1.3.0/mllib-decision-tree.html
  15. * https://databricks.com/blog/2014/09/29/scalable-decision-trees-in-mllib.html
  16. *
  17. */
  18. object DecisionTreesExample {
  19. def main(args: Array[String]): Unit = {
  20. Logger.getRootLogger.setLevel(Level.ERROR)
  21. val sc = new SparkContext(new SparkConf())
  22. Test_SmallCategoricalDataset_1(sc)
  23. println("\n\n\n")
  24. Test_SmallCategoricalDataset_2(sc)
  25. sc.stop
  26. }
  27. /** 用一个小数据集来测试决策树
  28. * 数据集中的tuple的全部feature都是categorical
  29. * dataset = [
  30. [1, 1, 'yes'],
  31. [1, 1, 'yes'],
  32. [1, 0, 'no'],
  33. [0, 1, 'no'],
  34. [0, 1, 'no']
  35. ]
  36. 'yes'记为1, 'no'记为1
  37. */
  38. def Test_SmallCategoricalDataset_1(sc: SparkContext): Unit = {
  39. val trainingData = sc.parallelize(Array(
  40. LabeledPoint(1, Vectors.dense(1, 1)),
  41. LabeledPoint(1, Vectors.dense(1, 1)),
  42. LabeledPoint(0, Vectors.dense(1, 0)),
  43. LabeledPoint(0, Vectors.dense(0, 1)),
  44. LabeledPoint(0, Vectors.dense(0, 1))))
  45. val numClasses = 2
  46. // categorical feature info,为空则认为所有的feature都是连续性变量
  47. val categoricalFeatureInfo = Map(0 -> 2, 1 -> 2)
  48. val impurity = "entropy" // 或者是gini
  49. val maxDepth = 5
  50. val maxBins = 32
  51. val model: DecisionTreeModel =
  52. DecisionTree.trainClassifier(trainingData, numClasses,
  53. categoricalFeatureInfo, impurity, maxDepth, maxBins)
  54. // 将训练出的模型持久化到HDFS文件中,然后再将其读出来使用
  55. def modelPath = "/user/tao/mllib/model/tree.model"
  56. model.save(sc, modelPath)
  57. val sameModel = DecisionTreeModel.load(sc, modelPath)
  58. /**
  59. * 输出
  60. * DecisionTreeModel classifier of depth 2 with 5 nodes
  61. If (feature 0 in {0.0})
  62. Predict: 0.0
  63. Else (feature 0 not in {0.0})
  64. If (feature 1 in {0.0})
  65. Predict: 0.0
  66. Else (feature 1 not in {0.0})
  67. Predict: 1.0
  68. 这与例1中的结果是一样的!
  69. */
  70. println("="*30 + " full model in the form of a string " + "="*30)
  71. println(sameModel.toDebugString)
  72. println("="*30 + " testing prediction " + "="*30)
  73. println("(1, 1) -> " + sameModel.predict(Vectors.dense(1,1)))
  74. println("(0, 1) -> " + sameModel.predict(Vectors.dense(0,1)))
  75. }
  76. /**
  77. * 另一个数据集,来自 Data Mining: Concepts and Techniques, 3rd edition
  78. 是上面例2中的数据集
  79. * @param sc
  80. */
  81. def Test_SmallCategoricalDataset_2(sc: SparkContext): Unit = {
  82. val originalDataset = Array(
  83. ("youth", "high", "no", "fair", "no"),
  84. ("youth", "high", "no", "excellent", "no"),
  85. ("middle_aged", "high", "no", "fair", "yes"),
  86. ("senior", "medium", "no", "fair", "yes"),
  87. ("senior", "low", "yes", "fair", "yes"),
  88. ("senior", "low", "yes", "excellent", "no"),
  89. ("middle_aged", "low", "yes", "excellent", "yes"),
  90. ("youth", "medium", "no", "fair", "no"),
  91. ("youth", "low", "yes", "fair", "yes"),
  92. ("senior", "medium", "yes", "fair", "yes"),
  93. ("youth", "medium", "yes", "excellent", "yes"),
  94. ("middle_aged", "medium", "no", "excellent", "yes"),
  95. ("middle_aged", "high", "yes", "fair", "yes"),
  96. ("senior", "medium", "no", "excellent", "no"))
  97. // 对数据集进行转换
  98. val dataSet: Array[LabeledPoint]
  99. = originalDataset.map(t => LabeledPoint(
  100. t._5 match { // class tag
  101. case "no" => 0
  102. case "yes" => 1
  103. },
  104. Vectors.dense( // features
  105. t._1 match {
  106. case "youth" => 0
  107. case "middle_aged" => 1
  108. case "senior" => 2
  109. },
  110. t._2 match {
  111. case "high" => 0
  112. case "medium" => 1
  113. case "low" => 2
  114. },
  115. t._3 match {
  116. case "no" => 0
  117. case "yes" => 1
  118. },
  119. t._4 match {
  120. case "excellent" => 0
  121. case "fair" => 1
  122. })))
  123. println("="*30 + " dataset " + "="*30)
  124. dataSet.foreach(println)
  125. val trainingData = sc.parallelize(dataSet)
  126. val numClasses = 2
  127. val categoricalFeatureInfo: Map[Int, Int] = Map(0 -> 3, 1 -> 3, 2 -> 2, 3 -> 2)
  128. val impurity = "entropy"
  129. val maxDepth = 20
  130. val maxBin = 32
  131. val model =
  132. DecisionTree.trainClassifier(trainingData, numClasses,
  133. categoricalFeatureInfo, impurity, maxDepth, maxBin)
  134. /**
  135. * DecisionTreeModel classifier of depth 4 with 13 nodes
  136. If (feature 2 in {0.0})
  137. If (feature 0 in {0.0})
  138. Predict: 0.0
  139. Else (feature 0 not in {0.0})
  140. If (feature 0 in {2.0})
  141. If (feature 3 in {0.0})
  142. Predict: 0.0
  143. Else (feature 3 not in {0.0})
  144. Predict: 1.0
  145. Else (feature 0 not in {2.0})
  146. Predict: 1.0
  147. Else (feature 2 not in {0.0})
  148. If (feature 0 in {0.0,1.0})
  149. Predict: 1.0
  150. Else (feature 0 not in {0.0,1.0})
  151. If (feature 3 in {0.0})
  152. Predict: 0.0
  153. Else (feature 3 not in {0.0})
  154. Predict: 1.0
  155. 但是,这与书上的例子计算出的Model的结构不同!!!
  156. */
  157. println("="*30 + " full model in the form of a string " + "="*30)
  158. println(model.toDebugString)
  159. println("="*30 + " testing prediction " + "="*30)
  160. println("(2, 1, 0, 0) -> " + model.predict(Vectors.dense(2, 1, 0, 0)))
  161. }
  162. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注