【机器学习】机器学习之决策树(基于ID3、CART挑选西瓜)

一、决策树介绍

  决策树(decision tree)是一类常见的机器学习方法。基于树结构来进行决策。决策树学习的目的是为了产生一颗泛化能力强,即处理未见实例能力强的决策树。
  一般的,一颗决策树包含一个根结点、若干个内部结点和若干个叶结点;叶结点对应于决策结果,其他每个结点则对应于一个属性测试;每个结点包含的样本集合根据属性测试的结果被划分到子结点中;根结点包含样本全集。从根结点到每个叶结点的路径对应一个判定测试序列。

1. 基本流程

  决策树的基本流程遵循简单且直观的“分而治之”(divide-and-conquer)策略。
在这里插入图片描述
  决策树的生成是一个递归过程,再决策树的基本算法中,以下三种情况会导致递归返回:

  • 当前结点包含的样本全属于同一类别,无需划分。
  • 当前属性集为空,或是所有样本在所有属性上取值相同,无法划分。
  • 当前结点包含的样本集合为空,不能划分。

2. 选择划分因素

  生成决策树的关键在于如何选择最优划分属性。一般而言,随着划分过程不断进行,我们希望决策树的分支节点所包含的样本尽可能属于同一类别,即结点的“纯度”(purity)越来越高。

(1)信息熵(information entropy)

  信息熵(information entropy)是度量样本集合纯度的最常用的一种指标。
  假定样本集合 D D D中第 k k k类样本所占的比例为 p k ( k = 1 , 2 , . . . , ∣ y ∣ ) p_k(k = 1,2,...,|y|) pk(k=1,2,...,y),则 D D D的信息熵定义为:
E n t ( D ) = − ∑ k = 1 ∣ y ∣ p k l o g 2 p k Ent(D) = - \sum_{k=1}^{|y|} p_k log_2p_k Ent(D)=k=1ypklog2pk

  显然, E n t ( D ) Ent(D) Ent(D)的值越小,集合 D D D的纯度越高。

因为 p k ∈ [ 0 , 1 ] p_k \in[0,1] pk[0,1] ,故 l o g 2 p k ≤ 0 log_2p_k\leq0 log2pk0 E n t ( D ) ≥ 0 Ent(D)\geq0 Ent(D)0. 极限情况下,考虑 D D D 中样本同属于同一类,则此时的 E n t ( D ) Ent(D) Ent(D) 值为 0 0 0(取到最小值)。当 D D D 中样本都分别属于不同类别时, E n t ( D ) Ent(D) Ent(D)取到最大值 l o g 2 ∣ y ∣ log_2 |y| log2y

(2)信息增益 (information gain)

  假定离散属性 a a a V V V 个可能的取值 { a 1 , a 2 , . . . a V a^1,a^2,...a^V a1,a2,...aV},若使用 a a a 来对样本集 D D D 进行划分,则会产 V V V 个分支结点,其中第 v v v 个分支结点包含了 D D D 中所有在属性 a a a 上的取值为 a v a^v av 的样本,记为 D v D^v Dv 。由于不同分支结点包含的样本数不同,给分支结点赋予权重 ∣ D v ∣ ∣ D ∣ \frac{|D^v|}{|D|} DDv ,即样本数量越多的分支结点影响越大。由此可计算出用属性 a a a 对样本集 D D D 进行划分所获得的信息增益(information gain)
G a i n ( D , a ) = E n t ( D ) − ∑ v = 1 V ∣ D v ∣ ∣ D ∣ E n t ( D v ) Gain(D,a) = Ent(D) - \sum_{v=1}^{V}\frac{|D^v|}{|D|}Ent(D^v) Gain(D,a)=Ent(D)v=1VDDvEnt(Dv)

  信息增益越大,则意味着使用属性 a a a 来进行划分所获得的“纯度提升”越大,此次划分的效果越好。因此,我们可用信息增益来进行决策树的划分属性选择。
   I D 3 ID3 ID3决策树学习算法就是以信息增益为准则来选择划分属性

(3)增益率(gain ratio)

  基于信息增益的最优属性划分原则——信息增益准则
,对可取值数据较多的属性有所偏好。 C 4.5 C4.5 C4.5算法使用增益率代替信息增益来选择最优划分属性,增益率定义为:
G a i n _ r a t i o ( D , a ) = G a i n ( D , a ) I V ( a ) Gain\_ratio(D,a) = \frac{Gain(D,a)}{IV(a)} Gain_ratio(D,a)=IV(a)Gain(D,a)
其中
I V ( a ) = − ∑ v = 1 V ∣ D v ∣ ∣ D ∣ l o g 2 ∣ D v ∣ ∣ D ∣ IV(a) = - \sum_{v=1}^{V}\frac{|D^v|}{|D|}log_2\frac{|D^v|}{|D|} IV(a)=v=1VDDvlog2DDv

  称为属性 a a a 的固有值。属性 a a a 的可能取值数目越多(即 V V V越大),则 I V ( a ) IV(a) IV(a) 的值通常会越大。这在一定程度上消除了对可取值数据较多的属性的偏好。
  事实上,增益率准则对可取值数目较少的属性有所偏好, C 4.5 C4.5 C4.5 算法并不是直接使用增益率准则,而是先从候选划分属性中找出信息增益高于平均水平的属性,再从中选择增益率最高的。

(4)基尼指数(Gini index)

   C A R T CART CART 决策树算法使用基尼指数(Gini index)来选择划分属性。数据集 D D D纯度可用基尼值来度量:
G i n i ( D ) = ∑ k = 1 ∣ y ∣ ∑ k ′ ≠ k p k p k ′ = 1 − ∑ k = 1 ∣ y ∣ p k 2 \begin{aligned} Gini(D)&= \sum_{k=1}^{|y|}\sum_{k' \neq k}p_kp_{k'} \\ &=1 - \sum_{k=1}^{|y|}p_k^2 \\ \end{aligned} Gini(D)=k=1yk=kpkpk=1k=1ypk2

   G i n i ( D ) Gini(D) Gini(D) 反应了从数据集 D D D 中随机抽取两个样本,其类别标记不一致的概率。由此, G i n i ( D ) Gini(D) Gini(D) 越小,纯度越高。
  属性 a a a 的基尼指数定义为:
G i n i _ i n d e x ( D , a ) = ∑ v = 1 V ∣ D v ∣ ∣ D ∣ G i n i ( D v ) Gini\_index(D,a) = \sum_{v=1}^{V}\frac{|D^v|}{|D|}Gini(D^v) Gini_index(D,a)=v=1VDDvGini(Dv)
  在候选属性集合 A A A 中,选择那个使得划分后基尼指数最小的属性作为最优化分属性。即 a ∗ = arg ⁡ a ∈ A min ⁡ G i n i _ i n d e x ( D , a ) a_* = \arg_{a\in A} \min {Gini\_index(D,a)} a=argaAminGini_index(D,a)

二、实现基于信息增益准则(ID3)的决策树

1. 数据样本处理

  本文案例基于教材《机器学习》 P 76 P76 P76 表4.1西瓜数据集2.0,使用Python构建一棵基于信息增益准则的决策树。
在这里插入图片描述

2. 代码实现

(1)建立决策树

  • 计算信息熵函数
#计算信息熵
def calcInformationEntropy(dataSet):
    #dataSet最后一列是类别,前面是特征
    dict = {}
    m = len(dataSet)
    for i in range(m):
        #.get()函数:如果没有这个key,就返回默认值;如果有这个key,就返回这个key的value
        dict[dataSet[i][-1]] = dict.get(dataSet[i][-1], 0) + 1;
    ent = 0
    for key in dict.keys():
        p = float(dict[key]) / m
        ent = ent - (p * np.math.log(p, 2))
    return ent
  • 划分数据集函数
#划分数据集
#dataSet:数据集
#axis: 要划分的列下标
#value: 要划分的列的值
def splitDataSet(dataSet, axis, value):
    splitedDataSet = []
    for data in dataSet:
        if(data[axis] == value):
            reduceFeatureVec = data[: axis]
            reduceFeatureVec.extend(data[axis + 1 :])
            splitedDataSet.append(reduceFeatureVec)
    return splitedDataSet
  • 计算信息增益函数
#计算信息增益,然后选择最优的特征进行划分数据集
#信息增益的计算公式:西瓜书P75
def chooseBestFeatureToSplit(dataSet):
    #计算整个集合的熵
    EntD = calcInformationEntropy(dataSet)
    mD = len(dataSet)  #行
    featureNumber = len(dataSet[0][:]) - 1 #列
    maxGain = -1000
    bestFeatureIndex = -1
    for i in range(featureNumber):
        #featureSet = set(dataSet[:][i])  #错误写法:dataSet[:][i]仍然是获取行
        featureCol = [x[i] for x in dataSet]   #取列表某列的方法!!
        featureSet = set(featureCol)
        splitedDataSet = []
        for av in featureSet:
            retDataSet = splitDataSet(dataSet, i, av)
            splitedDataSet.append(retDataSet)
        gain = EntD
        for ds in splitedDataSet:
            mDv = len(ds)
            gain = gain - (float(mDv) / mD) * calcInformationEntropy(ds)
        if(bestFeatureIndex == -1):
            maxGain = gain
            bestFeatureIndex = i
        elif(maxGain < gain):
            maxGain = gain
            bestFeatureIndex = i

    return bestFeatureIndex
#当所有的特征划分完了之后,如果仍然有叶子节点中的数据不是同一个类别,
# 则把类别最多的作为这个叶子节点的标签
def majorityCnt(classList):
    dict = {}
    for label in classList:
        dict[label] = dict.get(label, 0) + 1
    sortedDict = sorted(dict, dict.items(), key = operator.itemgetter(1), reversed = True)
    return sortedDict[0][0]
  • 递归构建决策树
#递归构建决策树
def createTree(dataSet, labels):
    classList = [x[-1] for x in dataSet]
#    if(len(set(classList)) == 1):
#        return classList[0]
    if(classList.count(classList[0]) == len(classList)):
        return classList[0]
    elif(len(dataSet[0]) == 1):  #所有的属性全部划分完毕
        return majorityCnt(classList)
    else:
        bestFeatureIndex = chooseBestFeatureToSplit(dataSet)
        bestFeatureLabel = labels[bestFeatureIndex]
        myTree = {bestFeatureLabel: {}}
        del(labels[bestFeatureIndex])  #使用完该属性之后,要删除
        featureList = [x[bestFeatureIndex] for x in dataSet]
        featureSet = set(featureList)
        for feature in featureSet:
            subLabels = labels[:]  #拷贝一份,防止label在递归的时候被修改  (list是传引用调用)
            tmpDataSet = splitDataSet(dataSet, bestFeatureIndex, feature)  #划分数据集
            myTree[bestFeatureLabel][feature] = createTree(tmpDataSet, subLabels)
        return myTree
  • 读取数据
#读取西瓜数据集2.0
def readWatermelonDataSet():
    ifile = open("周志华_西瓜数据集2.txt")
    featureName = ifile.readline()  #表头
    labels = (featureName.split(' ')[0]).split(',')
    lines = ifile.readlines()
    dataSet = []
    for line in lines:
        tmp = line.split('\n')[0]
        tmp = tmp.split(',')
        dataSet.append(tmp)

    return dataSet, labels
  • 输出
melonDataSet, melonLabels = readWatermelonDataSet()
print(melonLabels)
melonBestFeature = chooseBestFeatureToSplit(melonDataSet)
tree = createTree(melonDataSet, melonLabels)
print(tree)

输出结果:

[‘色泽’, ‘根蒂’, ‘敲声’, ‘纹理’, ‘脐部’, ‘触感’, ‘好瓜’]
{‘纹理’: {‘稍糊’: {‘触感’: {‘软粘’: ‘是’, ‘硬滑’: ‘否’}}, ‘模糊’: ‘否’, ‘清晰’: {‘根蒂’: {‘蜷缩’: ‘是’, ‘稍蜷’: {‘色泽’: {‘乌黑’: {‘触感’: {‘软粘’: ‘否’, ‘硬滑’: ‘是’}}, ‘青绿’: ‘是’}}, ‘硬挺’: ‘否’}}}}

(2)绘制决策树

#使用Matlotlib绘制决策树
import matplotlib.pyplot as plt

#设置文本框和箭头格式
decisionNode = dict(boxstyle = "sawtooth", fc = "0.8")
leafNode = dict(boxstyle = "round4", fc = "0.8")
arrow_args = dict(arrowstyle = "<-")
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['font.family'] = 'sans-serif'

#画节点
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 getNumLeafs(myTree):
    leafNumber = 0
    firstStr = list(myTree.keys())[0]
    secondDict = myTree[firstStr]
    for key in secondDict.keys():
        if(type(secondDict[key]).__name__ == 'dict'):
            leafNumber = leafNumber + getNumLeafs(secondDict[key])
        else:
            leafNumber += 1
    return leafNumber

#获取决策树的高度(递归)
def getTreeDepth(myTree):
    maxDepth = 0
    firstStr = list(myTree.keys())[0]
    secondDict = myTree[firstStr]
    for key in secondDict.keys():
        #test to see if the nodes are dictonaires, if not they are leaf nodes
        if type(secondDict[key]).__name__=='dict':
            thisDepth = 1 + getTreeDepth(secondDict[key])
        else:   thisDepth = 1
        if thisDepth > maxDepth: maxDepth = thisDepth
    return maxDepth

#在父子节点添加信息
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):#if the first key tells you what feat was split on
    numLeafs = getNumLeafs(myTree)  #this determines the x width of this tree
    depth = getTreeDepth(myTree)
    firstStr = list(myTree.keys())[0]     #the text label for this node should be this
    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]).__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


#画布初始化
def createPlot(inTree):
    fig = plt.figure(1, facecolor='white')
    fig.clf()
    axprops = dict(xticks=[], yticks=[])
    createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)    #no ticks
    #createPlot.ax1 = plt.subplot(111, frameon=False) #ticks for demo puropses
    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()
  • 定义主函数
def main():
    #createPlot()
    print(getTreeDepth(tree)) #输出数的深度
    print(getNumLeafs(tree))  #输出数的叶子个数
    createPlot(tree)

输出结果

main()

4
8

请添加图片描述

3. 结果分析

先看《机器学习》教材给出的标准决策树:
在这里插入图片描述
  将书上的结果与我们获得的结果进行对比,基本是一致的,但还是有略微的区别,我们缺少一个色泽为浅白的叶。通过对样本数据与代码的检查最终找到缺失原因:原始数据中不存在纹理为清晰、根蒂为稍蜷且色泽为浅白的瓜,导致决策树缺失这种情况的子叶。

三、使用Sklearn库实现决策树

1. 基于信息增益准则( I D 3 ID3 ID3 C 4.5 C4.5 C4.5)方法建立决策树

  1. 导入相关库
#导入相关库
import pandas as pd
import graphviz 
from sklearn.model_selection import train_test_split
from sklearn import tree
  1. 导入数据
f = open('周志华_西瓜数据集2.csv','r')
data = pd.read_csv(f)

x = data[["色泽","根蒂","敲声","纹理","脐部","触感"]].copy()
y = data['好瓜'].copy()
print(data)

在这里插入图片描述

  1. 数据预处理
  • 将特征值数值化
#将特征值数值化
x = x.copy()
for i in ["色泽","根蒂","敲声","纹理","脐部","触感"]:
    for j in range(len(x)):
        if(x[i][j] == "青绿" or x[i][j] == "蜷缩" or data[i][j] == "浊响" \
           or x[i][j] == "清晰" or x[i][j] == "凹陷" or x[i][j] == "硬滑"):
            x[i][j] = 1
        elif(x[i][j] == "乌黑" or x[i][j] == "稍蜷" or data[i][j] == "沉闷" \
           or x[i][j] == "稍糊" or x[i][j] == "稍凹" or x[i][j] == "软粘"):
            x[i][j] = 2
        else:
            x[i][j] = 3
            
y = y.copy()
for i in range(len(y)):
    if(y[i] == "是"):
        y[i] = int(1)
    else:
        y[i] = int(-1) 
  • 将数据转换为DataFrame数据类型
#需要将数据x,y转化好格式,数据框dataframe,否则格式报错
x = pd.DataFrame(x).astype(int)
y = pd.DataFrame(y).astype(int)
print(x)
print(y)

在这里插入图片描述

  1. 划分训练集与测试集
  • 80%数据用于训练,20%数据用于测试
x_train, x_test, y_train, y_test = train_test_split(x,y,test_size=0.2)
print(x_train)

在这里插入图片描述

  1. 建立模型并训练
#决策树学习
clf = tree.DecisionTreeClassifier(criterion="entropy")                    #实例化 
clf = clf.fit(x_train, y_train) 
score = clf.score(x_test, y_test)
print(score)

在这里插入图片描述

  • DecisionTreeClassifier函数参数解释:
sklearn.tree.DecisionTreeClassifier(criterion=’gini’,\
									 splitter=’best’, \
 									 max_depth=None, \
 									 min_samples_split=2, \
									 min_samples_leaf=1,\
								  	 min_weight_fraction_leaf=0.0, \
 									 max_features=None, \
 									 max_leaf_nodes=None, \
 									 min_impurity_decrease=0.0,\
 									 min_impurity_split=None, \
 									 class_weight=None, \
 									 presort=False)
参数含义
criterion选择结点划分质量的度量标准,默认使用"gini",即基尼系数,基尼系数是CART算法中采用的度量标准,该参数还可以设置为"entropy",表示基于信息增益 I D 3 ID3 ID3 C 4.5 C4.5 C4.5建立决策树
splitter结点划分时的策略,默认使用"best""best"表示依据选用的criterion标准,还可以设置为"random",表示最优的随机划分属性
max_depth设置决策树的最大深度,默认为None
min_samples_split当对一个内部结点划分时,要求该内部结点上的最小样本数,默认为2
min_samples_leaf设置叶子结点上的最小样本数,默认为1
min_weight_fraction_leaf设置每一个叶子节点上样本的权重和的最小值,该参数默认为0
max_features划分结点、寻找最优划分属性时,设置允许搜索的最大属性个数,默认为None
max_leaf_nodes设置决策树的最大叶子节点个数,该参数与max_depth等参数一起限制决策树的复杂度,默认为None,表示不加限制
min_impurity_decrease默认值为0,可以通过设置该参数来提前结束树的生长
class_weight设置样本数据中每个类的权重,这里权重是针对整个类的数据设定的,默认为None,即不施加权重
presort设置对训练数据进行预排序,以提升结点最优划分属性的搜索,默认为False
  1. 可视化决策树
feature_name = ["色泽","根蒂","敲声","纹理","脐部","触感"]
dot_data = tree.export_graphviz(clf                          
                                ,feature_names= feature_name                                
                                ,class_names=["好瓜","坏瓜"]                                
                                ,filled=True                                
                                ,rounded=True
                                ,out_file =None  
                                ) 
graph = graphviz.Source(dot_data) 
graph

在这里插入图片描述

2. 基于基尼指数( C A R T CART CART)建立决策树

  根据DecisionTreeClassifier函数参数解释可知,只需将criterion值改为"gini",即可基于基尼指数( C A R T CART CART)建立决策树。

#决策树学习
clf = tree.DecisionTreeClassifier(criterion="gini")                    #实例化 
clf = clf.fit(x_train, y_train) 
score = clf.score(x_test, y_test)
print(score)

得到输出:
在这里插入图片描述

  • 可视化( C A R T CART CART)决策树
    在这里插入图片描述

四、总结

  本文西瓜数据集2.0,使用Python自定义构建一棵基于信息增益准则ID3的决策树,并通过Matlotlib绘制该决策树。
  本文通过学习使用sklearn库中DecisionTreeClassifier函数分别根据 I D 3 ID3 ID3 C 4.5 C4.5 C4.5 C A R T CART CART方法实现了决策树,并使用export_graphviz函数绘制生成的决策树。但由于样本数据量过小,导致每次划分的测试集与训练集变化较大,以至于模型精准度浮动较大。并且使用sklearn库时,必须将特征量以及标签转化为数值型才能生成模型。

五、参考

sklearn实战-----1.sklearn入门与决策树

机器学习六(决策树ID3算法原理和实现——西瓜书学习笔记)

sklearn.tree.DecisionTreeClassifier 详细说明

  • 6
    点赞
  • 59
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值