决策树概念:
分类决策树模型是一种描述对实例进行分类的树形结构。决策树由结点和有向边组成。结点有两种类型:内部节点和叶节点,内部节点表示一个特征或属性,叶节点表示一个类。
分类的时候,从根节点开始,对实例的某一个特征进行测试,根据测试结果,将实例分配到其子结点;此时,每一个子结点对应着该特征的一个取值。如此递归向下移动,直至达到叶结点,最后将实例分配到叶结点的类中。
知识点:
例如判断某款物品的潜在买家:
决策树可以看成一个if-then规则的集合:由决策树的根结点到叶结点的每一条路径构建一条规则;路径上的内部结点的特征对应着规则的条件,而叶结点对应着分类的结论。决策树的路径和其对应的if-then规则集合是等效的,它们具有一个重要的性质:互斥并且完备。这里的意思是说:每一个实例都被一条路径或一条规则所覆盖,而且只被一条规则所覆盖。
几个基本概念
特征选择问题希望选取对训练数据具有良好分类能力的特征,这样可以提高决策树学习的效率。如果利用一个特征进行分类的结果与随机分类的结果没有很大差别,则称这个特征是没有分类能力的(对象是否喜欢打游戏应该不会成为关键特征吧,也许也会……)。为了解决特征选择问题,找出最优特征,先要介绍一些信息论里面的概念。
1.信息熵(香农熵)
熵是表示随机变量不确定性的度量。设X是一个取有限个值的离散随机变量,其概率分布为:
随机变量的熵为:
熵越大,随机变量的不确定性就越大。
数据集熵值计算函数:
from math import log
def calcShannonEnt(dataSet): """dataSet 为前n-1列为特征,最后一列为类别的数据集 """ numEntries = len(dataSet) labelCounts = {} for featVec in dataSet: # 遍历每个实例,统计标签的频数 currentLabel = featVec[-1] if currentLabel not in labelCounts.keys(): labelCounts[currentLabel] = 0 labelCounts[currentLabel] += 1 shannonEnt = 0.0 for key in labelCounts: prob = float(labelCounts[key]) / numEntries shannonEnt -= prob * log(prob,2) # 以2为底的对数 return shannonEnt
2.条件熵
设有随机变量(X,Y),其联合概率分布为
条件熵H(Y|X)表示在已知随机变量XX的条件下随机变量Y的不确定性。随机变量X给定的条件下随机变量Y的条件熵H(Y|X),定义为X给定条件下Y的条件概率分布的熵对X的数学期望:
这里,pi=P(X=xi),i=1,2,⋯,n.pi=P(X=xi),i=1,2,⋯,n.
3.信息熵
信息增益表示得知特征XX的信息而使得类Y的信息的不确定性减少的程度。特征A对训练数据集D的信息增益g(D,A),定义为集合D的经验熵H(D)与特征A给定条件下D的经验条件熵H(D|A)之差,即
这个差又称为互信息。信息增益大的特征具有更强的分类能力。
根据信息增益准则的特征选择方法是:对训练数据集(或子集)计算其每个特征的信息增益,选择信息增益最大的特征。
计算信息增益的算法如下:
通过划分数据集,并计算信息增益
def splitDataSet(dataSet, axis, value): ''' 按照给定特征划分数据集 :param dataSet:待划分的数据集 :param axis:划分数据集的特征 :param value: 需要返回的特征的值 :return: 划分结果列表 ''' retDataSet = [] for featVec in dataSet: if featVec[axis] == value: reducedFeatVec = featVec[:axis] #chop out axis used for splitting reducedFeatVec.extend(featVec[axis+1:]) retDataSet.append(reducedFeatVec) return retDataSet def calcConditionalEntropy(dataSet, i, featList, uniqueVals): ''' 计算X_i给定的条件下,Y的条件熵 :param dataSet:数据集 :param i:维度i :param featList: 数据集特征列表 :param uniqueVals: 数据集特征集合 :return: 条件熵 ''' conditionEnt = 0.0 for value in uniqueVals: subDataSet = splitDataSet(dataSet, i, value) prob = len(subDataSet) / float(len(dataSet)) # 极大似然估计概率 conditionEnt += prob * calcShannonEnt(subDataSet) # 条件熵的计算 return conditionEnt def calcInformationGain(dataSet, baseEntropy, i): ''' 计算信息增益 :param dataSet:数据集 :param baseEntropy:数据集的信息熵 :param i: 特征维度i :return: 特征i对数据集的信息增益g(D|X_i) ''' featList = [example[i] for example in dataSet] # 第i维特征列表 uniqueVals = set(featList) # 转换成集合 newEntropy = calcConditionalEntropy(dataSet, i, featList, uniqueVals) infoGain = baseEntropy - newEntropy # 信息增益,就yes熵的减少,也就yes不确定性的减少 return infoGain
两种算法
1,ID3算法
ID3算法由Ross Quinlan发明,建立在“奥卡姆剃刀”的基础上:越是小型的决策树越优于大的决策树(be simple简单理论)。ID3算法中根据信息增益评估和选择特征,每次选择信息增益最大的特征作为判断模块建立子结点。ID3算法可用于划分标称型数据集,没有剪枝的过程,为了去除过度数据匹配的问题,可通过裁剪合并相邻的无法产生大量信息增益的叶子节点(例如设置信息增益阀值)。使用信息增益的话其实是有一个缺点,那就是它偏向于具有大量值的属性。就是说在训练集中,某个属性所取的不同值的个数越多,那么越有可能拿它来作为分裂属性,而这样做有时候是没有意义的,另外ID3不能处理连续分布的数据特征,于是就有了C4.5算法。CART算法也支持连续分布的数据特征。
步骤:
ID3算法思想描述:
a.对当前例子集合,计算属性的信息增益;
b.选择信息增益最大的属性Ai(关于信息增益后面会有详细叙述)
c.把在Ai处取值相同的例子归于同于子集,Ai取几个值就得几个子集
d.对依次对每种取值情况下的子集,递归调用建树算法,即返回a,
e.若子集只含有单个属性,则分支为叶子节点,判断其属性值并标上相应的符号,然后返回调用处。
寻找最优划分方式代码
def chooseBestFeatureToSplitByID3(dataSet): ''' 选择最好的数据集划分方式 :param dataSet:数据集 :return: 划分结果 ''' numFeatures = len(dataSet[0]) - 1 # 最后一列yes分类标签,不属于特征向量 baseEntropy = calcShannonEnt(dataSet) bestInfoGain = 0.0 bestFeature = -1 for i in range(numFeatures): # 遍历所有特征 infoGain = calcInformationGain(dataSet, baseEntropy, i) # 计算信息增益 if (infoGain > bestInfoGain): # 选择最大的信息增益 bestInfoGain = infoGain bestFeature = i return bestFeature # 返回最优特征对应的维度
2.C4.5算法
C4.5算法是用于生成决策树的一种经典算法,是ID3算法的一种延伸和优化。C4.5算法对ID3算法主要做了一下几点改进:
(1)通过信息增益率选择分裂属性,克服了ID3算法中通过信息增益倾向于选择拥有多个属性值的属性作为分裂属性的不足;
(2)能够处理离散型和连续型的属性类型,即将连续型的属性进行离散化处理;
(3)构造决策树之后进行剪枝操作;
(4)能够处理具有缺失属性值的训练数据。
-
- 优点:
(1)通过信息增益率选择分裂属性,克服了ID3算法中通过信息增益倾向于选择拥有多个属性值的属性作为分裂属性的不足;
(2)能够处理离散型和连续型的属性类型,即将连续型的属性进行离散化处理;
(3)构造决策树之后进行剪枝操作;
(4)能够处理具有缺失属性值的训练数据。 - 缺点:
(1)算法的计算效率较低,特别是针对含有连续属性值的训练样本时表现的尤为突出。
(2)算法在选择分裂属性时没有考虑到条件属性间的相关性,只计算数据集中每一个条件属性与决策属性之间的期望信息,有可能影响到属性选择的正确性.
- 优点:
增益率
特征A对训练数据集D的信息增益比gR(D,A)定义为其信息增益g(D,A)与训练数据集D关于特征A的值的熵HA(D)之比即
步骤:
代码:
def calcInformationGainRatio(dataSet, baseEntropy, i): ''' 计算信息增益比 :param dataSet:数据集 :param baseEntropy:数据集的信息熵 :param i: 特征维度i :return: 特征i对数据集的信息增益比gR(D|X_i) ''' H=calcShannonEnt([example[:i+1] for example in dataSet] ) if H==0.0: return float('inf') return calcInformationGain(dataSet, baseEntropy, i) / H def chooseBestFeatureToSplitByC45(dataSet): ''' 选择最好的数据集划分方式 :param dataSet: :return: 划分结果 ''' numFeatures = len(dataSet[0]) - 1 # 最后一列yes分类标签,不属于特征变量 baseEntropy = calcShannonEnt(dataSet) bestInfoGainRate = 0.0 bestFeature = -1 for i in range(numFeatures): # 遍历所有维度特征 infoGainRate = calcInformationGainRatio(dataSet, baseEntropy, i) # 计算信息增益比 if (infoGainRate > bestInfoGainRate): # 选择最大的信息增益比 bestInfoGainRate = infoGainRate bestFeature = i return bestFeature # 返回最佳特征对应的维
创建树
import operator
def majorityCnt(classList): ''' 采用多数表决的方法决定叶结点的分类 :param: 所有的类标签列表 :return: 出现次数最多的类 ''' classCount={} for vote in classList: # 统计所有类标签的频数 if vote not in classCount.keys(): classCount[vote] = 0 classCount[vote] += 1 sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True) # 排序 return sortedClassCount[0][0] def createTree(dataSet,labels): ''' 创建决策树 :param: dataSet:训练数据集 :return: labels:所有的类标签 ''' classList = [example[-1] for example in dataSet] if classList.count(classList[0]) == len(classList): return classList[0] # 第一个递归结束条件:所有的类标签完全相同 if len(dataSet[0]) == 1: return majorityCnt(classList) # 第二个递归结束条件:用完了所有特征 bestFeat = chooseBestFeatureToSplitByID3(dataSet) # 最优划分特征 #bestFeat = chooseBestFeatureToSplitByC45(dataSet) bestFeatLabel = labels[bestFeat] myTree = {bestFeatLabel:{}} # 使用字典类型储存树的信息 label_tmp=labels[:bestFeat]+labels[bestFeat+1:] featValues = [example[bestFeat] for example in dataSet] uniqueVals = set(featValues) for value in uniqueVals: subLabels = label_tmp[:] # 复制所有类标签,保证每次递归调用时不改变原始列表的内容 myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels) return myTree
测试分类
构造了决策树之后,我们就可以将它用于实际数据的分类,在执行分类时,需要输入决策树和用于构造树的所有类标签向量。然后,程序比较测试数据与决策树上的数值,递归执行该过程直到进入叶结点;最后将测试数据定义为叶结点所属的类型。
def classify(inputTree,featLabels,testVec): ''' 利用决策树进行分类 :param: inputTree:构造好的决策树模型 :param: featLabels:所有的类标签 :param: testVec:测试数据 :return: 分类决策结果 ''' firstStr = inputTree.keys()[0] secondDict = inputTree[firstStr] featIndex = featLabels.index(firstStr) key = testVec[featIndex] valueOfFeat = secondDict[key] if isinstance(valueOfFeat, dict): classLabel = classify(valueOfFeat, featLabels, testVec) else: classLabel = valueOfFeat return classLabel
可视化树
import matplotlib.pyplot as plt import tree # 定义文本框和箭头格式 decisionNode = dict(boxstyle="round4", color='#3366FF') # 定义判断结点形态 leafNode = dict(boxstyle="circle", color='#FF6633') # 定义叶结点形态 arrow_args = dict(arrowstyle="<-", color='g') # 定义箭头 #计算叶结点数 def getNumLeafs(myTree): numLeafs = 0 firstStr = list(myTree.keys())[0] secondDict = myTree[firstStr] for key in secondDict.keys(): if type(secondDict[key]).__name__=='dict':# 测试结点的数据类型是否为字典 numLeafs += getNumLeafs(secondDict[key]) else: numLeafs +=1 return numLeafs # 计算树的深度 def getTreeDepth(myTree): maxDepth = 0 firstStr = list(myTree.keys())[0] secondDict = myTree[firstStr] for key in secondDict.keys(): if type(secondDict[key]).__name__=='dict':# 测试结点的数据类型是否为字典 thisDepth = 1 + getTreeDepth(secondDict[key]) else: thisDepth = 1 if thisDepth > maxDepth: maxDepth = thisDepth return maxDepth # 绘制带箭头的注释 def plotNode(nodeTxt, centerPt, parentPt, nodeType): createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction', xytext=centerPt, textcoords='axes fraction', va="center", ha="center", bbox=nodeType, arrowprops=arrow_args ) # 在父子结点间填充文本信息 def plotMidText(cntrPt, parentPt, txtString): xMid = (parentPt[0]-cntrPt[0])/2.0 + cntrPt[0] yMid = (parentPt[1]-cntrPt[1])/2.0 + cntrPt[1] createPlot.ax1.text(xMid, yMid, txtString, va="center", ha="center", rotation=30) def plotTree(myTree, parentPt, nodeTxt): numLeafs = getNumLeafs(myTree) # 计算宽与高 depth = getTreeDepth(myTree) firstStr = list(myTree.keys())[0] cntrPt = (plotTree.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW, plotTree.yOff) plotMidText(cntrPt, parentPt, nodeTxt) plotNode(firstStr, cntrPt, parentPt, decisionNode) # 标记子结点属性值 secondDict = myTree[firstStr] plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD # 减少y偏移 for key in secondDict.keys(): if type(secondDict[key]).__name__=='dict': plotTree(secondDict[key],cntrPt,str(key)) #recursion else: #it's a leaf node print the leaf node plotTree.xOff = plotTree.xOff + 1.0/plotTree.totalW plotNode(secondDict[key], (plotTree.xOff, plotTree.yOff), cntrPt, leafNode) plotMidText((plotTree.xOff, plotTree.yOff), cntrPt, str(key)) plotTree.yOff = plotTree.yOff + 1.0/plotTree.totalD #if you do get a dictonary you know it's a tree, and the first element will be another dict def createPlot(inTree): fig = plt.figure(1, facecolor='white') fig.clf() axprops = dict(xticks=[], yticks=[]) createPlot.ax1 = plt.subplot(111, frameon=False, **axprops) plotTree.totalW = float(getNumLeafs(inTree)) plotTree.totalD = float(getTreeDepth(inTree)) plotTree.xOff = -0.5/plotTree.totalW; plotTree.yOff = 1.0; plotTree(inTree, (0.5,1.0), '') plt.show()
两个测试用例
1.
myDat = [['youth', 'no', 'no', '1', 'refuse'], ['youth', 'no', 'no', '2', 'refuse'], ['youth', 'yes', 'no', '2', 'agree'], ['youth', 'yes', 'yes', '1', 'agree'], ['youth', 'no', 'no', '1', 'refuse'], ['mid', 'no', 'no', '1', 'refuse'], ['mid', 'no', 'no', '2', 'refuse'], ['mid', 'yes', 'yes', '2', 'agree'], ['mid', 'no', 'yes', '3', 'agree'], ['mid', 'no', 'yes', '3', 'agree'], ['elder', 'no', 'yes', '3', 'agree'], ['elder', 'no', 'yes', '2', 'agree'], ['elder', 'yes', 'no', '2', 'agree'], ['elder', 'yes', 'no', '3', 'agree'], ['elder', 'no', 'no', '1', 'refuse'], ] labels = ['age', 'working?', 'house?', 'credit_situation'] myTree = createTree(myDat, labels) createPlot(myTree) labels = ['age', 'working?', 'house?', 'credit_situation'] print(classify(myTree,labels ,['youth','no','no',1]))
###结果为refuse
2.网上数据
import pandas as pd import numpy as np name_c=['sample code number','clump thickness', 'uniformity of cell size', 'uniformity of cell shape','marginal adhesion','single epithelial cell', 'bare nuclei','bland chromatin','normal nucleoli','mitoses','class' ] data=pd.read_csv('http://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/breast-cancer-wisconsin.data',names=name_c) data=data.replace(to_replace='?',value=np.nan) data=data.dropna(how='any') myDat=data[name_c[1:]].values[:].tolist() myTree = createTree(myDat, name_c[1:]) createPlot(myTree) print( classify(myTree,name_c[1:],[5,3,2,1,3,'1',1,1,1]))
###结果为2
存储决策树结构
def storeTree(Tree,filename): import pickle with open(filename,'wb') as f: pickle.dump(Tree,f) def grabTree(Tree,filename): import pickle with open(filename,'rb') as f: return pickle.load(f)