一、介绍

决策树(Decision Tree)是有监督学习中的一种算法,并且是一种基本的分类与回归的方法。有分类树和回归树两种。

决策树的算法本质是树形结构,我们可以把决策树看成是一个if-then规则的集合。将决策树转换成if-then规则的过程是这样的:

  • 由决策树的根节点到叶节点的每一条路径构建一条规则
  • 路径上中间节点的特征对应着规则的条件,叶节点的类标签对应着规则的结论

决策树的路径或者其对应的if-then规则集合有一个重要的性质:互斥且完备。即每一个实例都被有且仅有一条路径或者规则所覆盖。这里的覆盖指实例的特征与路径上的特征一致,或实例满足规则的条件。

二、香农熵和信息增益

香农熵及计算函数:

Python利用决策树方法实现分类任务代码及相关文件_python = Python利用决策树方法实现分类任务代码及相关文件_机器学习_02

Python利用决策树方法实现分类任务代码及相关文件_python_03 = Python利用决策树方法实现分类任务代码及相关文件_信息增益_04

Python利用决策树方法实现分类任务代码及相关文件_python_03的值越小,则D的不纯度就越低。

信息增益

Python利用决策树方法实现分类任务代码及相关文件_决策树_06 = Python利用决策树方法实现分类任务代码及相关文件_python_03Python利用决策树方法实现分类任务代码及相关文件_python_08

Python实现:

#创建数据集,书中海洋生物为例
import numpy as np
import pandas as pd

def createDataSet():
    row_data = {'no surfacing':[1,1,1,0,0],
                'flippers':[1,1,0,1,1],
                'fish':['yes','yes','no','no','no']}
    dataSet = pd.DataFrame(row_data)
    return dataSet

# 计算香农熵
def calEnt(dataSet):
    n = dataSet.shape[0] 
    iset = dataSet.iloc[:,-1].value_counts() 
    p = iset/n 
    ent = (-p*np.log2(p)).sum()
    return ent

# 根据信息增益选择出最佳数据集切分的列
def bestSplit(dataSet):
    baseEnt = calEnt(dataSet) #计算原始熵
    bestGain = 0 #初始化信息增益
    axis = -1 #初始化最佳切分列,标签列
    for i in range(dataSet.shape[1]-1): #对特征的每一列进行循环,-1是不需要对标签列循环
        levels= dataSet.iloc[:,i].value_counts().index #提取出当前列的所有取值
        ents = 0 #初始化子节点的信息熵
        for j in levels: #对当前列的每一个取值进行循环
            childSet = dataSet[dataSet.iloc[:,i]==j] #某一个子节点的dataframe
            ent = calEnt(childSet) #计算某一个子节点的信息熵
            ents += (childSet.shape[0]/dataSet.shape[0])*ent #计算当前列的信息熵
            #print(f'第{i}列的信息熵为{ents}')
        infoGain = baseEnt-ents #计算当前列的信息增益
        #print(f'第{i}列的信息增益为{infoGain}')
        if (infoGain > bestGain):
            bestGain = infoGain #选择最大信息增益
            axis = i #最大信息增益所在列的索引
    return axis

# 按照给定的列划分数据集
def mySplit(dataSet,axis,value):
    col = dataSet.columns[axis]
    redataSet = dataSet.loc[dataSet[col]==value,:].drop(col,axis=1)
    return redataSet

三、递归构建决策树

构建决策树的算法有很多,例如ID3、C4.5、CART。在此处选择ID3。

ID3算法的核心是在决策树各个节点上对应信息增益准则选择特征,递归地构建决策树。
具体做法:从根节点开始,对节点计算所有可能的特征的信息增益,选择信息增益最大的特征作为节点的特征,由该特征的不同取值建立子节点;再对子节点递归地调用以上方法,构建决策树;直到所有特征信息增益均很小或没有特征可以选择为止。最后得到一个决策树。

递归结束的条件:程序遍历完所有的特征列,或者每个分支下的所有实例都具有相同的分类。如果所有实例均具有相同分类,则得到一个叶节点。任何到达叶节点的数据必然属于叶节点的分类,即叶节点里面必须是标签。

def createTree(dataSet):
    featlist = list(dataSet.columns) 
    classlist = dataSet.iloc[:,-1].value_counts() 
    #判断最多标签数目是否等于数据集行数,或者数据集是否只有一列
    if classlist[0]==dataSet.shape[0] or dataSet.shape[1] == 1:
        return classlist.index[0] 
    axis = bestSplit(dataSet) 
    bestfeat = featlist[axis] 
    myTree = {bestfeat:{}} 
    del featlist[axis] 
    valuelist = set(dataSet.iloc[:,axis]) 
    for value in valuelist: 
        myTree[bestfeat][value] = createTree(mySplit(dataSet,axis,value))
    return myTree

myTree = createTree(dataSet)
myTree

#树的存储
np.save('myTree.npy',myTree)

#树的读取
read_myTree = np.load('myTree.npy').item()
read_myTree

# 对一个测试实例进行分类
def classify(inputTree,labels, testVec):
    firstStr = next(iter(inputTree)) 
    secondDict = inputTree[firstStr] 
    featIndex = labels.index(firstStr)
    for key in secondDict.keys():
        if testVec[featIndex] == key:
            if type(secondDict[key]) == dict :
                classLabel = classify(secondDict[key], labels, testVec)
            else:
                classLabel = secondDict[key]
    return classLabel


# 对测试集进行预测,并返回预测后的结果
def acc_classify(train,test):
    inputTree = createTree(train)
    labels = list(train.columns)
    result = []
    for i in range(test.shape[0]): 
        testVec = test.iloc[i,:-1] 
        classLabel = classify(inputTree,labels,testVec) 
        result.append(classLabel)
    test['predict']=result 
    acc = (test.iloc[:,-1]==test.iloc[:,-2]).mean() 
    print(f'模型预测准确率为{acc}')
    return test

四、使用SKlearn中graphviz绘制决策树

#导入相应的包
from sklearn import tree
from sklearn.tree import DecisionTreeClassifier
import graphviz

#特征
Xtrain = dataSet.iloc[:,:-1]
#标签
Ytrain = dataSet.iloc[:,-1]
labels = Ytrain.unique().tolist()
Ytrain = Ytrain.apply(lambda x: labels.index(x))

#绘制树模型
clf = DecisionTreeClassifier()
clf = clf.fit(Xtrain, Ytrain)
tree.export_graphviz(clf)
dot_data = tree.export_graphviz(clf, out_file=None)
graphviz.Source(dot_data)

#给图形增加标签和颜色
dot_data = tree.export_graphviz(clf, out_file=None,
                                feature_names=['no surfacing', 'flippers'],
                                class_names=['fish', 'not fish'],
                                filled=True, rounded=True,
                                special_characters=True)
graphviz.Source(dot_data)

#利用render方法生成图形
graph = graphviz.Source(dot_data)
graph.render("fish")

五、决策树可视化

# 递归计算叶子节点的数目
def getNumLeafs(myTree):
    numLeafs = 0 
    firstStr = next(iter(myTree))
    secondDict = myTree[firstStr] 
    for key in secondDict.keys():
        if type(secondDict[key]) == dict: 
            numLeafs += getNumLeafs(secondDict[key]) 
        else:
            numLeafs +=1 #不是字典,代表此结点为叶子结点
    return numLeafs


# 递归计算树的深度
def getTreeDepth(myTree):
    maxDepth = 0
    firstStr = next(iter(myTree))
    secondDict = myTree[firstStr]
    for key in secondDict.keys():
        if type(secondDict[key]) == dict:
            thisDepth = 1+getTreeDepth(secondDict[key])
        else:
            thisDepth = 1
        if thisDepth>maxDepth:
            maxDepth = thisDepth
    return maxDepth


# 绘制节点
def plotNode(nodeTxt, cntrPt, parentPt, nodeType):
    arrow_args = dict(arrowstyle="<-") 
    createPlot.ax1.annotate(nodeTxt,
                            xy=parentPt,xycoords='axes fraction',  
                            # axes fraction
                            xytext=cntrPt, 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=0)


# 绘制决策树
def plotTree(myTree, parentPt, nodeTxt):
    decisionNode = dict(boxstyle="sawtooth", fc="0.8") 
    leafNode = dict(boxstyle="round4", fc="0.8")
    numLeafs = getNumLeafs(myTree) 
    depth = getTreeDepth(myTree) 
    firstStr = next(iter(myTree))
    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 
    for key in secondDict.keys():
        if type(secondDict[key])== dict: 
            plotTree(secondDict[key],cntrPt,str(key)) 
        else:
            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


# 创建绘制面板
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()

六、使用决策树预测隐形眼镜

# 导入数据集
lenses = pd.read_table('lenses.txt',header = None)
lenses.columns =['age','prescript','astigmatic','tearRate','class']


# 划分训练集和测试集
import random
def randSplit(dataSet, rate):
    l = list(dataSet.index) #提取出索引
    random.shuffle(l) #随机打乱索引
    dataSet.index = l #将打乱后的索引重新赋值给原数据集
    n = dataSet.shape[0] #总行数
    m = int(n * rate) #训练集的数量
    train = dataSet.loc[range(m), :] #提取前m个记录作为训练集
    test = dataSet.loc[range(m, n), :] #剩下的作为测试集
    dataSet.index = range(dataSet.shape[0]) #更新原数据集的索引
    test.index = range(test.shape[0]) #更新测试集的索引
    return train, test


#利用训练集生成决策树
lensesTree = createTree(train1)
lensesTree

#构造注解树
createPlot(lensesTree)

#用决策树进行分类并计算有预测准确率
acc_classify(train1,test1)

# 使用SKlearn中graphviz绘制决策树
#特征列
Xtrain1 = train1.iloc[:,:-1]
for i in Xtrain1.columns:
    labels = Xtrain1[i].unique().tolist()
    Xtrain1[i]= Xtrain1[i].apply(lambda x: labels.index(x))

#标签列
Ytrain1 = train1.iloc[:,-1]
labels = Ytrain1.unique().tolist()
Ytrain1= Ytrain1.apply(lambda x: labels.index(x))

#绘制树形图
clf = DecisionTreeClassifier()
clf = clf.fit(Xtrain1, Ytrain1)
tree.export_graphviz(clf)
dot_data = tree.export_graphviz(clf, out_file=None)
graphviz.Source(dot_data)

#添加标签和颜色
dot_data = tree.export_graphviz(clf, out_file=None,
                                feature_names=['age', 'prescript', 'astigmatic','tearRate'],
                                class_names=['soft','hard','no lenses'],
                                filled=True, rounded=True,special_characters=True)
graphviz.Source(dot_data)

#使用render存储树形图
graph = graphviz.Source(dot_data)
graph.render("lense")

七、算法优缺点

优点:

(1)决策树可以可视化,易于理解和解释;

(2)数据准备工作很少。其他很多算法通常都需要数据规范化,需要创建虚拟变量并删除空值等;

(3)能够同时处理数值和分类数据,既可以做回归又可以做分类。其他技术通常专门用于分析仅具有一种变类型的数据集;

(4)效率高,决策树只需要一次构建,反复使用,每一次预测的最大计算次数不超过决策树的深度;

(5)能够处理多输出问题,即含有多个标签的问题,注意与一个标签中含有 多种标签分类的问题区别开;

(6)是一个白盒模型,结果很容易能够被解释。如果在模型中可以观察到给定的情况,则可以通过布尔逻辑轻松解释条件。相反,在黑盒模型中(例如,在人工神经网络中),结果可能更难以解释。

缺点:

(1)递归生成树的方法很容易出现过拟合。

(2)决策树可能是不稳定的,因为即使非常小的变异,可能会产生一颗完全不同的树

(3)如果某些分类占优势,决策树将会创建一棵有偏差的树。因此,建议在拟合决策树之前平衡数据集。