二元决策树就是基于属性做一系列的二元(是/否)决策。每次决策对应于从两种可能性中选择一个。每次决策后,要么引出另外一个决策,要么生成最终的结果。一个实际训练决策树的例子有助于加强对这个概念的理解。了解了训练后的决策树是什么样的,就学会了决策树的训练过程。
代码清单6-1为使用Scikitlearn的DecisionTreeRegressor工具包针对红酒口感数据构建二元决策树的代码。图6-1为代码清单6-1生成的决策树。
代码清单6-1 构建一个决策树预测红酒口感-winTree.py
__author__ = 'mike-bowles' import urllib2 import numpy from sklearn import tree from sklearn.tree import DecisionTreeRegressor from sklearn.externals.six import StringIO from math import sqrt import matplotlib.pyplot as plot #read data into iterable target_url = ("http://archive.ics.uci.edu/ml/machine-learning-" "databases/wine-quality/winequality-red.csv") data = urllib2.urlopen(target_url) xList = [] labels = [] names = [] firstLine = True for line in data: if firstLine: names = line.strip().split(";") firstLine = False else: #split on semi-colon row = line.strip().split(";") #put labels in separate array labels.append(float(row[-1])) #remove label from row row.pop() #convert row to floats floatRow = [float(num) for num in row] xList.append(floatRow) nrows = len(xList) ncols = len(xList[0]) wineTree = DecisionTreeRegressor(max_depth=3) wineTree.fit(xList, labels) with open("wineTree.dot", 'w') as f: f = tree.export_graphviz(wineTree, out_file=f) #Note: The code above exports the trained tree info to a #Graphviz "dot" file. #Drawing the graph requires installing GraphViz and the running the #following on the command line #dot -Tpng wineTree.dot -o wineTree.png # In windows, you can also open the .dot file in the GraphViz #gui (GVedit.exe)]图6-1为针对红酒数据的训练结果,即一系列的决策。决策树框图显示了一系列的方框,这些方框称作节点(nodes)。有两类节点,一种针对问题输出“是”或者“否”,另外一种是终止节点,输出针对样本的预测结果,并终止整个决策的过程。终止节点也叫作叶子节点(leaf)。在图6-1中,终止节点处在框图底部,它们下面没有分支或者进一步的决策节点。

图6-1确定红酒口感的决策树
1.1 如何利用二元决策树进行预测当一个观察(或一行数据)被传送到一个非终止节点时,此行数据要回答此节点的问题。如果回答“是”,则该行数据进入节点下面的左侧节点。如果回答”否“,则此行数据进入节点下面的右侧节点。该过程持续进行,直到到达一个终止节点(即叶子节点),叶子节点给该行数据分配预测值。叶子节点分配的预测值是所有到达此节点的训练数据结果的均值。
尽管此决策树的第二个决策层在两个分支中都考虑了变量X[9],这两个决策也可以是针对不同属性所做的判断(可以参看第三个决策层的例子)。 最上面的节点又叫根节点(root node)。这个节点提出的问题是“X[10]<=10.525”。在二元决策树中,越是重要的变量越早用来分割数据(越接近决策树的顶端),因此决策树认为变量X[10],也就是酒精含量属性很重要。这点决策树与第5章的惩罚线性回归是一致的。第5章“用惩罚线性方法构建预测模型”也认为酒精含量是决定红酒口感最重要的属性。图6-1所示决策树的深度为3。决策树的深度定义为从上到下遍历树的最长路径(所经过的决策的数目)。在“决策树的训练等价于分割点的选择”小节的关于训练的讨论中,可以看到没有理由要求到达终止节点的所有路径具有相同的长度(见图6-1)。
现在已经知道一个训练好的决策树是什么样的,也看到了如何使用一个决策树来进行预测。下面介绍如何训练决策树。
1.2 如何训练一个二元决策树了解如何训练决策树最简单的方法就是通过一个具体的例子。代码清单6-2为给定一个实数属性如何预测一个实数标签的例子。数据集在代码中产生(也叫作合成数据)。生成过程是把0.5~+0.5等分成100份,单一实数属性x就是这些等分数。标签y等于x加上随机噪声。
代码清单6-2 简单回归问题的决策树训练-simpleTree.py
__author__ = 'mike-bowles' import numpy import matplotlib.pyplot as plot from sklearn import tree from sklearn.tree import DecisionTreeRegressor from sklearn.externals.six import StringIO #Build a simple data set with y = x + random nPoints = 100 #x values for plotting xPlot = [(float(i)/float(nPoints) - 0.5) for i in range(nPoints + 1)] #x needs to be list of lists. x = [[s] for s in xPlot] #y (labels) has random noise added to x-value #set seed numpy.random.seed(1) y = [s + numpy.random.normal(scale=0.1) for s in xPlot] plot.plot(xPlot,y) plot.axis('tight') plot.xlabel('x') plot.ylabel('y') plot.show() simpleTree = DecisionTreeRegressor(max_depth=1) simpleTree.fit(x, y) #draw the tree with open("simpleTree.dot", 'w') as f: f = tree.export_graphviz(simpleTree, out_file=f) #compare prediction from tree with true values yHat = simpleTree.predict(x) plot.figure() plot.plot(xPlot, y, label='True y') plot.plot(xPlot, yHat, label='Tree Prediction ', linestyle='--') plot.legend(bbox_to_anchor=(1,0.2)) plot.axis('tight') plot.xlabel('x') plot.ylabel('y') plot.show() simpleTree2 = DecisionTreeRegressor(max_depth=2) simpleTree2.fit(x, y) #draw the tree with open("simpleTree2.dot", 'w') as f: f = tree.export_graphviz(simpleTree2, out_file=f) #compare prediction from tree with true values yHat = simpleTree2.predict(x) plot.figure() plot.plot(xPlot, y, label='True y') plot.plot(xPlot, yHat, label='Tree Prediction ', linestyle='--') plot.legend(bbox_to_anchor=(1,0.2)) plot.axis('tight') plot.xlabel('x') plot.ylabel('y') plot.show() #split point calculations - try every possible split point to #find the best one sse = [] xMin = [] for i in range(1, len(xPlot)): #divide list into points on left and right of split point lhList = list(xPlot[0:i]) rhList = list(xPlot[i:len(xPlot)]) #calculate averages on each side lhAvg = sum(lhList) / len(lhList) rhAvg = sum(rhList) / len(rhList) #calculate sum square error on left, right and total lhSse = sum([(s - lhAvg) * (s - lhAvg) for s in lhList]) rhSse = sum([(s - rhAvg) * (s - rhAvg) for s in rhList]) #add sum of left and right to list of errors sse.append(lhSse + rhSse) xMin.append(max(lhList)) plot.plot(range(1, len(xPlot)), sse) plot.xlabel('Split Point Index') plot.ylabel('Sum Squared Error') plot.show() minSse = min(sse) idxMin = sse.index(minSse) print(xMin[idxMin]) #what happens if the depth is really high? simpleTree6 = DecisionTreeRegressor(max_depth=6) simpleTree6.fit(x, y) #too many nodes to draw the tree #with open("simpleTree2.dot", 'w') as f: # f = tree.export_graphviz(simpleTree6, out_file=f) #compare prediction from tree with true values yHat = simpleTree6.predict(x) plot.figure() plot.plot(xPlot, y, label='True y') plot.plot(xPlot, yHat, label='Tree Prediction ', linestyle=' ') plot.legend(bbox_to_anchor=(1,0.2)) plot.axis('tight') plot.xlabel('x') plot.ylabel('y') plot.show()图6-2为属性 x 和标签 y 的关系图。正如预期, y 值大致上一直跟随 x 值变化,但是有些随机的小扰动。

图6-2标签与属性的关系图
1.3 决策树的训练等同于分割点的选择代码清单6-2的第一步是运行scikitlearn的regression tree包,并指定决策树的深度为1。此处理过程的结果如图6-3所示。图6-3为深度为1的决策树的框图。深度为1的树又叫作桩(stumps)。在根节点的决策就是将属性值与0.075比较。这个值叫作分割点(split point),因为它把数据分割成两部分。由根节点发散出去的两个方框可知,101个实例中有43个到了根节点的左分支,剩下的58个实例到了根节点的右分支。如果属性值小于分割点,则此决策树的预测值就是方框里指明的值,大约就是0.302。

图6-3一个简单问题的解:深度为1的决策树的框图
分割点的选择如何影响预测效果审视决策树的另一个方法就是将预测值与真实的标签值进行对比。这个简单的合成数据只有一个属性,由决策树产生的预测值一直跟随着实际的标签值,从中也能看出这个简单的决策树的训练是如何完成的。如图6-4所示,预测的值是基于一个简单的判断方法。预测值实际上是属性值的阶梯函数。这个“阶梯”就发生在分割点。

图6-4预测值与实际值的比较
分割点选择算法这个简单的决策树需要确定3个变量:分割点的值、分割后生成的两组数据的预测值。决策树的训练过程就是要完成这个任务。下面介绍上述目标如何达到。训练此决策树的目标是使预测值的误差平方最小。首先假设分割点已经确认。一旦给定分割点,分配给两个组的预测值就可以确定下来。分配的值就是使均方误差最小的那个值。那么剩下的问题就是如何确定分割点的值。代码清单6-2有一小段代码用来确定分割点。这个过程是尝试每一个可能的分割点,然后把数据分成2组,取每组数值的均值作为分配的预测值,然后计算相应的误差平方和。
图6-5展示了误差平方和作为分割点的函数是如何变化的。大概在数据的中心,可以明确地取到最小的误差平方。训练一个决策树需要穷尽地搜索所有可能的分割点来确定哪个值可以使误差平方和最小化。这是这个简单的例子需要注意的地方。

图6-5每个可能的分割点对应的误差平方和
多变量决策树的训练-选择哪个属性进行分割如果问题含有多个属性该怎么办?算法会对所有的属性检查所有可能的分割点,对每个属性找到误差平方和最小的分割点,然后找到哪个属性对应的误差平方和最小。
在训练决策树的过程中,每个计算周期都要对分割点进行计算。同样地,训练基于决策树的集成算法时,每个周期也要对分割点进行计算。如果属性没有重复值,每个数据点对应的属性值都要作为分割点进行测试(则分割点的测试次数等于数据点数目减1)。
随着数据规模的增大,分割点的计算量也成比例增加。测试的分割点彼此可能非常近。因此设计针对大规模数据的算法时,分割点的检测通常要比原始数据的粒度粗糙得多。论文“PLANET:Massively Parallel Learning of Tree Ensembles with MapReduce” 提出一种方法,是谷歌工程师针对大规模数据集构建决策树时采用的方法,他们使用决策树来实现梯度提升(gradient boosting) 算法(本章将会学到该集成方法)。
通过递归分割获得更深的决策树代码清单6-2展示了当决策树深度从1增加到2时,预测曲线会发生什么变化。预测曲线如图6-6所示。决策树的框图如图6-7所示。深度为1的决策树只有一步,这个预测曲线有3步。第2决策层分割点的确定与第1个分割点的方法完全一样。决策树的每个节点处理基于上个分割点生成的数据子集。每个节点中分割点的选择是使下面2个节点的误差平方和最小。图6-6的曲线非常接近一个实际的阶梯函数曲线。决策树深度的增加意味着更细小的步长、更高的保真度(准确性)。但是如果这个过程无限地继续下去会怎样?

图6-6深度为2的决策树的预测曲线