课后题4.3:编程实现基于信息熵进行划分选择的决策树算法,并为表4.3中的西瓜数据集生成一棵决策树

这道题主要参考了这篇博客,课后题4.3编程实现。我对其中给出的代码进行了一些注释,下面贴出代码全文:

点击展开
import numpy as np
import pandas as pd
import math
import copy
import matplotlib.pyplot as plt
import matplotlib as mpl
mpl.rcParams[u'font.sans-serif'] = ['simhei']
mpl.rcParams['axes.unicode_minus'] = False
 
 
dataset = pd.read_excel('./WaterMelon_3.0.xlsx',encoding = 'gbk')  # 读取数据
Attributes = dataset.columns        #所有属性的名称
#print(Attributes)
m,n = np.shape(dataset)   # 得到数据集大小
dataset = np.matrix(dataset)
for i in range(m):      # 将标签替换成 好瓜 和 坏瓜
    if dataset[i,n-1]=='是': dataset[i,n-1] = '好瓜'
    else : dataset[i,n-1] = '坏瓜'
attributeList = []       # 属性列表,每一个属性的取值,列表中元素是集合
for i in range(n):
    curSet = set()      # 使用集合是利用了集合里面元素不可重复的特性,从而提取出了每个属性的取值
    for j in range(m):
        curSet.add(dataset[j,i])
    attributeList.append(curSet)
#print(attributeList)
D = np.arange(0,m,1)     # 表示每一个样本编号
A = list(np.ones(n))    # 表示每一个属性是否被使用,使用过了标为 -1
A[-1] = -1              # 将数据里面的标签和编号列标记为 -1
A[0] = -1
#print(A)
#print(D)
EPS = 0.000001
 
 
class Node(object):            # 创建一个类,用来表示节点的信息
    def __init__(self,title):
        self.title = title     # 上一级指向该节点的线上的标记文字
        self.v = 1              # 节点的信息标记
        self.children = []     # 节点的孩子列表
        self.deep = 0         # 节点深度
        self.ID = -1         # 节点编号
 
def isSameY(D):                  # 判断所有样本是否属于同一类
    curY = dataset[D[0],n-1]
    for i in range(1,len(D)):
        if dataset[D[i],n-1] != curY:
            return  False
    return True
 
def isBlankA(A):              # 判断 A 是否是空,是空则返回true
    for i in range(n):
        if A[i]>0: return False
    return True
 
def isSameAinD(D,A):       # 判断在D中,是否所有的未使用过的样本属性均相同
    for i in range(n):
        if A[i]>0:
            for j in range(1,len(D)):
                if not isSameValue(dataset[D[0],i],dataset[D[j],i],EPS):
                    return False
    return True
 
def isSameValue(v1,v2,EPS):            # 判断v1、v2 是否相等
    if type(v1)==type(dataset[0,8]):
        return abs(v1-v2)<EPS
    else: return  v1==v2
 
def mostCommonY(D):             # 寻找D中样本数最多的类别
    res = dataset[D[0],n-1]     # D中第一个样本标签
    maxC = 1
    count = {}
    count[res] = 1              # 该标签数量记为1
    for i in range(1,len(D)):
        curV = dataset[D[i],n-1]   # 得到D中第i+1个样本的标签
        if curV not in count:      # 若之前不存在这个标签
            count[curV] = 1        # 则该标签数量记为1
        else:count[curV] += 1      # 否则 ,该标签对应的数量加一
        if count[curV]>maxC:       # maxC始终存贮最多标签对应的样本数量
            maxC = count[curV]     # res 存贮当前样本数最多的标签类型
            res = curV
    return res             # 返回的是样本数最多的标签的类型
 
def entropyD(D):       # 参数D中所存的样本的交叉熵
    types = []         # 存贮类别标签
    count = {}         # 存贮每个类别对应的样本数量
    for i in range(len(D)):           # 统计D中存在的每个类型的样本数量
        curY = dataset[D[i],n-1]
        if curY not in count:
            count[curY] = 1
            types.append(curY)
        else:
            count[curY] += 1
    ans = 0
    total = len(D)                # D中样本总数量
    for i in range(len(types)):   # 计算交叉熵
        ans -= count[types[i]]/total*math.log2(count[types[i]]/total)
    return ans
 
def gain(D,p):        # 属性 p 上的信息增益
    if type(dataset[0,p])== type(dataset[0,8]):   # 判断若是连续属性,则调用另一个函数
        res,divideV = gainFloat(D,p)
    else:
        types = []
        count = {}
        for i in range(len(D)):  # 得到每一个属性取值上的样本编号
            a = dataset[D[i],p]
            if a not in count:
                count[a] = [D[i]]
                types.append(a)
            else:
                count[a].append(D[i])
        res = entropyD(D)              # D的交叉熵
        total = len(D)
        for i in range(len(types)):    # 计算出每一个属性取值分支上的交叉熵,再计算出信息增益
            res -= len(count[types[i]])/total*entropyD(count[types[i]])
        divideV = -1000              # 这个只是随便给的一个值,没有实际意义
    return res,divideV
 
def gainFloat(D,p):            # 获得在连续属性上的最大信息增益及对应的划分点
    a = []
    for i in range(len(D)):    # 得到在该属性上的所有取值
        a.append(dataset[D[i],p])
    a.sort()     # 排序
    T = []
    for i in range(len(a)-1):       # 计算每一个划分点
        T.append((a[i]+a[i+1])/2)
    res = entropyD(D)               # D的交叉熵
    ans = 0
    divideV = T[0]
    for i in range(len(T)):         # 循环根据每一个分割点进行划分
        left = []
        right = []
        for j in range(len(D)):     # 根据特定分割点将样本分成两部分
            if (dataset[D[j],p]<=T[i]):
                left.append(D[j])
            else:right.append(D[j])
        temp = res-entropyD(left)-entropyD(right)    # 计算特定分割点下的信息增益
        if temp>ans:
            divideV = T[i]     # 始终存贮产生最大信息增益的分割点
            ans = temp         # 存贮最大的信息增益
    return ans,divideV
 
def treeGenerate(D,A,title):
    node = Node(title)
    if isSameY(D):             # D中所有样本是否属于同一类
        node.v = dataset[D[0],n-1]
        return node
 
    # 是否所有属性全部使用过  或者  D中所有样本的未使用的属性均相同
    if isBlankA(A) or isSameAinD(D,A):
        node.v = mostCommonY(D)  # 此时类别标记为样本数最多的类别(暗含可以处理存在异常样本的情况)
        return node              # 否则所有样本的类别应该一致
 
    entropy = 0
    floatV = 0
    p = 0
    for i in range(len(A)):      # 循环遍历A,找可以获得最大信息增益的属性
        if(A[i]>0):
            curEntropy,divideV = gain(D,i)
            if curEntropy > entropy:
                p = i                     # 存贮属性编号
                entropy = curEntropy
                floatV = divideV
 
    if isSameValue(-1000,floatV,EPS):   # 说明是离散属性
        node.v = Attributes[p]+"=?"     # 节点信息
        curSet = attributeList[p]       # 该属性的所有取值
        for i in curSet:
            Dv = []
            for j in range(len(D)):     # 获得该属性取某一个值时对应的样本标号
                if dataset[D[j],p]==i:
                    Dv.append(D[j])
 
            # 若该属性取值对应没有符合的样本,则将该分支作为叶子,类别是D中样本数最多的类别
            # 其实就是处理在没有对应的样本情况下的问题。那就取最大可能性的一类。
            if Dv==[]:
                nextNode = Node(i)
                nextNode.v = mostCommonY(D)
                node.children.append(nextNode)
            else:     # 若存在对应的样本,则递归继续生成该节点下的子树
                newA = copy.deepcopy(A)    # 注意是深度复制,否则会改变A中的值
                newA[p]=-1
                node.children.append(treeGenerate(Dv,newA,i))
    else:   # 若对应的是连续的属性
        Dleft = []
        Dright = []
        node.v = Attributes[p]+"<="+str(floatV)+"?"     # 节点信息
        for i in range(len(D)):       # 根据划分点将样本分成左右两部分
            if dataset[D[i],p]<=floatV: Dleft.append(D[i])
            else: Dright.append(D[i])
        node.children.append(treeGenerate(Dleft,A[:],"是"))    # 左边递归生成子树,是 yes 分支
        node.children.append(treeGenerate(Dright,A[:],"否"))    # 同上。 注意,在此时没有将对应的A中值变成 -1
    return node                                                # 因为连续属性可以使用多次进行划分
 
def countLeaf(root,deep):
    root.deep = deep
    res = 0
    if root.v=='好瓜' or root.v=='坏瓜':   # 说明此时已经是叶子节点了,所以直接返回
        res += 1
        return res,deep
    curdeep = deep             # 记录当前深度
    for i in root.children:    # 得到子树中的深度和叶子节点的个数
        a,b = countLeaf(i,deep+1)
        res += a
        if b>curdeep: curdeep = b
    return res,curdeep
 
def giveLeafID(root,ID):         # 给叶子节点编号
    if root.v=='好瓜' or root.v=='坏瓜':
        root.ID = ID
        ID += 1
        return ID
    for i in root.children:
        ID = giveLeafID(i,ID)
    return ID
 
def plotNode(nodeTxt,centerPt,parentPt,nodeType):     # 绘制节点
    plt.annotate(nodeTxt,xy = parentPt,xycoords='axes fraction',xytext=centerPt,
                 textcoords='axes fraction',va="center",ha="center",bbox=nodeType,
                 arrowprops=arrow_args)
 
def dfsPlot(root):
    if root.ID==-1:          # 说明根节点不是叶子节点
        childrenPx = []
        meanPx = 0
        for i in root.children:
            cur = dfsPlot(i)
            meanPx += cur
            childrenPx.append(cur)
        meanPx = meanPx/len(root.children)
        c = 0
        for i in root.children:
            nodetype = leafNode
            if i.ID<0: nodetype=decisionNode
            plotNode(i.v,(childrenPx[c],0.9-i.deep*0.8/deep),(meanPx,0.9-root.deep*0.8/deep),nodetype)
            plt.text((childrenPx[c]+meanPx)/2,(0.9-i.deep*0.8/deep+0.9-root.deep*0.8/deep)/2,i.title)
            c += 1
        return meanPx
    else:
        return 0.1+root.ID*0.8/(cnt-1)
 
 
 
myDecisionTreeRoot = treeGenerate(D,A,"root")        # 生成决策树
cnt,deep = countLeaf(myDecisionTreeRoot,0)     # 得到树的深度和叶子节点的个数
giveLeafID(myDecisionTreeRoot,0)
# 绘制决策树
decisionNode = dict(boxstyle = "sawtooth",fc = "0.9",color='blue')
leafNode = dict(boxstyle = "round4",fc="0.9",color='red')
arrow_args = dict(arrowstyle = "<-",color='green')
fig = plt.figure(1,facecolor='white')
rootX = dfsPlot(myDecisionTreeRoot)
plotNode(myDecisionTreeRoot.v,(rootX,0.9),(rootX,0.9),decisionNode)
plt.show()

其中,所需要的西瓜数据集下载地址,西瓜数据集
在运行时, 会出现那片参考博客中所说的中文乱码问题,所以需要下载 simhei.tff 字体文件,然后按照作者的方法进行修改配置即可。simhei.tff 字体下载地址,simhei.tff字体文件
最终,得到的决策树如下图所示:

课后题4.4:编程实现基于基尼指数进行选择划分的决策树算法,并在数据集4.2上生成预剪枝、后剪枝决策树,并与未剪枝决策树进行比较。
首先,参考课后题4.3中的思路,先基于表4.2的数据集生成未剪枝的决策树。 其实相较于上面的代码改动很少,主要就是将信息增益的计算改成基尼指数的计算。下面给出基于基尼指数在数据集表4.2(使用了训练集和测试集所有的数据)上生成的未剪枝决策树的代码:

点击展开
import numpy as np
import pandas as pd
import copy
import matplotlib.pyplot as plt
import matplotlib as mpl
mpl.rcParams[u'font.sans-serif'] = ['simhei']
mpl.rcParams['axes.unicode_minus'] = False
 
dataset = pd.read_excel('./WaterMelon_2.0.xlsx',encoding = 'gbk')  # 读取数据
Attributes = dataset.columns[:-1]        #所有属性的名称
#print(Attributes)
dataset = np.matrix(dataset)
dataset = dataset[:,:-1]
m,n = np.shape(dataset)   # 得到数据集大小
for i in range(m):      # 将标签替换成 好瓜 和 坏瓜
    if dataset[i,n-1]=='是': dataset[i,n-1] = '好瓜'
    else : dataset[i,n-1] = '坏瓜'
attributeList = []       # 属性列表,每一个属性的取值,列表中元素是集合
for i in range(n):
    curSet = set()      # 使用集合是利用了集合里面元素不可重复的特性,从而提取出了每个属性的取值
    for j in range(m):
        curSet.add(dataset[j,i])
    attributeList.append(curSet)
#print(attributeList)
D = np.arange(0,m,1)     # 表示每一个样本编号
A = list(np.ones(n))    # 表示每一个属性是否被使用,使用过了标为 -1
A[-1] = -1              # 将数据里面的标签和编号列标记为 -1
A[0] = -1
#print(A)
#print(D)
 
 
class Node(object):             # 创建一个类,用来表示节点的信息
    def __init__(self,title):
        self.title = title      # 上一级指向该节点的线上的标记文字
        self.v = 1              # 节点的信息标记
        self.children = []      # 节点的孩子列表
        self.deep = 0           # 节点深度
        self.ID = -1            # 节点编号
 
def isSameY(D):                 # 判断所有样本是否属于同一类
    curY = dataset[D[0],n-1]
    for i in range(1,len(D)):
        if dataset[D[i],n-1] != curY:
            return  False
    return True
 
def isBlankA(A):               # 判断 A 是否是空,是空则返回true
    for i in range(n):
        if A[i]>0: return False
    return True
 
def isSameAinD(D,A):           # 判断在D中,是否所有的未使用过的样本属性均相同
    for i in range(n):
        if A[i]>0:
            for j in range(1,len(D)):
                if not isSameValue(dataset[D[0],i],dataset[D[j],i]):
                    return False
    return True
 
def isSameValue(v1,v2):        # 判断v1、v2 是否相等
    return  v1==v2
 
def mostCommonY(D):            # 寻找D中样本数最多的类别
    res = dataset[D[0],n-1]    # D中第一个样本标签
    maxC = 1
    count = {}
    count[res] = 1             # 该标签数量记为1
    for i in range(1,len(D)):
        curV = dataset[D[i],n-1]      # 得到D中第i+1个样本的标签
        if curV not in count:         # 若之前不存在这个标签
            count[curV] = 1           # 则该标签数量记为1
        else:count[curV] += 1         # 否则 ,该标签对应的数量加一
        if count[curV]>maxC:          # maxC始终存贮最多标签对应的样本数量
            maxC = count[curV]        # res 存贮当前样本数最多的标签类型
            res = curV
    return res                        # 返回的是样本数最多的标签的类型
 
def gini(D):        # 参数D中所存的样本的基尼值
    types = []      # 存贮类别标签
    count = {}      # 存贮每个类别对应的样本数量
    for i in range(len(D)):           # 统计D中存在的每个类型的样本数量
        curY = dataset[D[i],n-1]
        if curY not in count:
            count[curY] = 1
            types.append(curY)
        else:
            count[curY] += 1
    ans = 1
    total = len(D)                # D中样本总数量
    for i in range(len(types)):   # 计算基尼值
        ans -= (count[types[i]]/total)**2
    return ans
 
def gini_indexD(D,p):        # 属性 p 上的基尼指数
    types = []
    count = {}
    for i in range(len(D)):  # 得到每一个属性取值上的样本编号
        a = dataset[D[i],p]
        if a not in count:
            count[a] = [D[i]]
            types.append(a)
        else:
            count[a].append(D[i])
    res = 0
    total = len(D)
    for i in range(len(types)):     # 计算出每一个属性取值分支上的基尼值,再计算出基尼指数
        res += len(count[types[i]])/total*gini(count[types[i]])
    return res
 
def treeGenerate(D,A,title):
    node = Node(title)
    if isSameY(D):       # D中所有样本是否属于同一类
        node.v = dataset[D[0],n-1]
        return node
 
    # 是否所有属性全部使用过  或者  D中所有样本的未使用的属性均相同
    if isBlankA(A) or isSameAinD(D,A):
        node.v = mostCommonY(D)   # 此时类别标记为样本数最多的类别(暗含可以处理存在异常样本的情况)
        return node               # 否则所有样本的类别应该一致
 
    gini_index = float('inf')
    p = 0
    for i in range(len(A)):      # 循环遍历A,找可以获得最小基尼指数的属性
        if(A[i]>0):
            curGini_index = gini_indexD(D,i)
            if curGini_index < gini_index:
                p = i                     # 存贮属性编号
                gini_index = curGini_index
 
    node.v = Attributes[p]+"=?"    # 节点信息
    curSet = attributeList[p]      # 该属性的所有取值
    for i in curSet:
        Dv = []
        for j in range(len(D)):     # 获得该属性取某一个值时对应的样本标号
            if dataset[D[j],p]==i:
                Dv.append(D[j])
 
            # 若该属性取值对应没有符合的样本,则将该分支作为叶子,类别是D中样本数最多的类别
            # 其实就是处理在没有对应的样本情况下的问题。那就取最大可能性的一类。
        if Dv==[]:
            nextNode = Node(i)
            nextNode.v = mostCommonY(D)
            node.children.append(nextNode)
        else:     # 若存在对应的样本,则递归继续生成该节点下的子树
            newA = copy.deepcopy(A)    # 注意是深度复制,否则会改变A中的值
            newA[p]=-1
            node.children.append(treeGenerate(Dv,newA,i))
    return node
 
def countLeaf(root,deep):
    root.deep = deep
    res = 0
    if root.v=='好瓜' or root.v=='坏瓜':   # 说明此时已经是叶子节点了,所以直接返回
        res += 1
        return res,deep
    curdeep = deep             # 记录当前深度
    for i in root.children:    # 得到子树中的深度和叶子节点的个数
        a,b = countLeaf(i,deep+1)
        res += a
        if b>curdeep: curdeep = b
    return res,curdeep
 
def giveLeafID(root,ID):         # 给叶子节点编号
    if root.v=='好瓜' or root.v=='坏瓜':
        root.ID = ID
        ID += 1
        return ID
    for i in root.children:
        ID = giveLeafID(i,ID)
    return ID
 
def plotNode(nodeTxt,centerPt,parentPt,nodeType):     # 绘制节点
    plt.annotate(nodeTxt,xy = parentPt,xycoords='axes fraction',xytext=centerPt,
                 textcoords='axes fraction',va="center",ha="center",bbox=nodeType,
                 arrowprops=arrow_args)
 
def dfsPlot(root):
    if root.ID==-1:          # 说明根节点不是叶子节点
        childrenPx = []
        meanPx = 0
        for i in root.children:
            cur = dfsPlot(i)
            meanPx += cur
            childrenPx.append(cur)
        meanPx = meanPx/len(root.children)
        c = 0
        for i in root.children:
            nodetype = leafNode
            if i.ID<0: nodetype=decisionNode
            plotNode(i.v,(childrenPx[c],0.9-i.deep*0.8/deep),(meanPx,0.9-root.deep*0.8/deep),nodetype)
            plt.text((1.5*childrenPx[c]+0.5*meanPx)/2,(0.9-i.deep*0.8/deep+0.9-root.deep*0.8/deep)/2,i.title)
            c += 1
        return meanPx
    else:
        return 0.1+root.ID*0.8/(cnt-1)
 
 
myDecisionTreeRoot = treeGenerate(D,A,"root")        # 生成决策树
cnt,deep = countLeaf(myDecisionTreeRoot,0)           # 得到树的深度和叶子节点的个数
giveLeafID(myDecisionTreeRoot,0)
# 绘制决策树
decisionNode = dict(boxstyle = "sawtooth",fc = "0.9",color='blue')
leafNode = dict(boxstyle = "round4",fc="0.9",color='red')
arrow_args = dict(arrowstyle = "<-",color='green')
fig = plt.figure(1,facecolor='white')
rootX = dfsPlot(myDecisionTreeRoot)
plotNode(myDecisionTreeRoot.v,(rootX,0.9),(rootX,0.9),decisionNode)
plt.show()

最终得到的决策树如下所示:

接着,我们来考虑预剪枝情况下的决策树。相较于之前的代码,核心部分就是将样本分成了训练集和验证集,并添加了计算在验证集上相应节点划分前后分类精确度的函数,从而来确定是否进行此次划分。  下面直接给出原代码

点击展开
import numpy as np
import pandas as pd
import copy
import matplotlib.pyplot as plt
import matplotlib as mpl
mpl.rcParams[u'font.sans-serif'] = ['simhei']
mpl.rcParams['axes.unicode_minus'] = False
 
dataset = pd.read_excel('./WaterMelon_2.0.xlsx',encoding = 'gbk')  # 读取数据
Attributes = dataset.columns[:-1]        #所有属性的名称
#print(Attributes)
dataset = np.matrix(dataset)
m,n = np.shape(dataset)
D_train = []                 # 得到所有的训练样本编号和验证样本编号
D_test = []
for i in range(m):
    if dataset[i,n-1]=='train':D_train.append(i)
    else:D_test.append(i)
#print(D_test)
#print(D_train)
dataset = dataset[:,:-1]
m,n = np.shape(dataset)   # 得到数据集大小
for i in range(m):      # 将标签替换成 好瓜 和 坏瓜
    if dataset[i,n-1]=='是': dataset[i,n-1] = '好瓜'
    else : dataset[i,n-1] = '坏瓜'
attributeList = []       # 属性列表,每一个属性的取值,列表中元素是集合
for i in range(n):
    curSet = set()      # 使用集合是利用了集合里面元素不可重复的特性,从而提取出了每个属性的取值
    for j in range(m):
        curSet.add(dataset[j,i])
    attributeList.append(curSet)
#print(attributeList)
A = list(np.ones(n))    # 表示每一个属性是否被使用,使用过了标为 -1
A[-1] = -1              # 将数据里面的标签和编号列标记为 -1
A[0] = -1
#print(A)
#print(D)
 
 
class Node(object):            # 创建一个类,用来表示节点的信息
    def __init__(self,title):
        self.title = title     # 上一级指向该节点的线上的标记文字
        self.v = 1             # 节点的信息标记
        self.children = []     # 节点的孩子列表
        self.deep = 0          # 节点深度
        self.ID = -1           # 节点编号
 
def isSameY(D):                  # 判断所有样本是否属于同一类
    curY = dataset[D[0],n-1]
    for i in range(1,len(D)):
        if dataset[D[i],n-1] != curY:
            return  False
    return True
 
def isBlankA(A):              # 判断 A 是否是空,是空则返回true
    for i in range(n):
        if A[i]>0: return False
    return True
 
def isSameAinD(D,A):       # 判断在D中,是否所有的未使用过的样本属性均相同
    for i in range(n):
        if A[i]>0:
            for j in range(1,len(D)):
                if not isSameValue(dataset[D[0],i],dataset[D[j],i]):
                    return False
    return True
 
def isSameValue(v1,v2):            # 判断v1、v2 是否相等
    return  v1==v2
 
def mostCommonY(D):             # 寻找D中样本数最多的类别
    res = dataset[D[0],n-1]     # D中第一个样本标签
    maxC = 1
    count = {}
    count[res] = 1              # 该标签数量记为1
    for i in range(1,len(D)):
        curV = dataset[D[i],n-1]      # 得到D中第i+1个样本的标签
        if curV not in count:         # 若之前不存在这个标签
            count[curV] = 1           # 则该标签数量记为1
        else:count[curV] += 1         # 否则 ,该标签对应的数量加一
        if count[curV]>maxC:          # maxC始终存贮最多标签对应的样本数量
            maxC = count[curV]        # res 存贮当前样本数最多的标签类型
            res = curV
    return res             # 返回的是样本数最多的标签的类型
 
def gini(D):        # 参数D中所存的样本的基尼值
    types = []      # 存贮类别标签
    count = {}      # 存贮每个类别对应的样本数量
    for i in range(len(D)):           # 统计D中存在的每个类型的样本数量
        curY = dataset[D[i],n-1]
        if curY not in count:
            count[curY] = 1
            types.append(curY)
        else:
            count[curY] += 1
    ans = 1
    total = len(D)          # D中样本总数量
    for i in range(len(types)):   # 计算基尼值
        ans -= (count[types[i]]/total)**2
    return ans
 
def gini_indexD(D,p):        # 属性 p 上的基尼指数
    types = []
    count = {}
    for i in range(len(D)):  # 得到每一个属性取值上的样本编号
        a = dataset[D[i],p]
        if a not in count:
            count[a] = [D[i]]
            types.append(a)
        else:
            count[a].append(D[i])
    res = 0
    total = len(D)
    for i in range(len(types)):     # 计算出每一个属性取值分支上的基尼值,再计算出基尼指数
        res += len(count[types[i]])/total*gini(count[types[i]])
    return res
 
def beforePrecision(D):        # 计算出在划分之前的精确度
    v = mostCommonY(D)         # 划分之前节点的分类标签
    count = 0
    for i in range(len(D)):    # 计算在D上分类正确的样本个数
        if dataset[D[i],n-1] == v:
            count += 1
    return count/len(D)        # 返回精确度
 
def afterPrecision(D,D1,p):        # 计算在划分后的精确度
    curSet = attributeList[p]      # 该属性的所有取值
    count = 0
    for i in curSet:
        Dv = []
        Dv1 = []
        for j in range(len(D)):     # 计算出训练集在该属性特定取值i上的样本编号
            if dataset[D[j],p] == i:
                Dv.append(D[j])
        for j in range(len(D1)):    # 计算出验证集在该属性特定取值i上的样本编号
            if dataset[D1[j],p] == i:
                Dv1.append(D1[j])
        if Dv == []:                # 若训练集在属性取值i上为空
            v = mostCommonY(D)      # 则该分支节点标签为其父节点中训练集样本数的最多一类的标签
        else:
            v = mostCommonY(Dv)     # 否则,该节点标签是符合条件的训练集样本中数量最多的一类的标签
        for k in range(len(Dv1)):
            if dataset[Dv1[k],n-1] == v:   # 计算验证集中分类正确的样本个数
                count += 1
    return count/len(D1)           # 返回准确率
 
 
 
def treeGenerate(D,D1,A,title):
    node = Node(title)
    if isSameY(D):       # D中所有样本是否属于同一类
        node.v = dataset[D[0],n-1]
        return node
 
    # 是否所有属性全部使用过  或者  D中所有样本的未使用的属性均相同
    if isBlankA(A) or isSameAinD(D,A):
        node.v = mostCommonY(D)   # 此时类别标记为样本数最多的类别(暗含可以处理存在异常样本的情况)
        return node              # 否则所有样本的类别应该一致
 
    gini_index = float('inf')
    p = 0
    for i in range(len(A)):      # 循环遍历A,找可以获得最小基尼指数的属性
        if(A[i]>0):
            curGini_index = gini_indexD(D,i)
            if curGini_index < gini_index:
                p = i                     # 存贮属性编号
                gini_index = curGini_index
    befPrecision = beforePrecision(D1)         # 划分前精确度
    aftPrecision = afterPrecision(D,D1,p)      # 划分后精确度
 
    '''
    此处之所以用大于等于进行判断,而不是严格的大于,仅仅是为了能绘制出一个树形结构。
    因为当使用严格大于时,可以发现,根本没办法进行划分,根节点直接就是叶子节点,这样显然是不合实际的
    个人认为,由于预剪枝本身存在着欠拟合的风险,所以用大于等于条件进行判断一定程度上可以降低欠拟合的风险
    但是,也可能出现划分后,所有分支的标签都一样的问题。这时可能需要考虑多个因素进行优化,比如结合后剪枝进行。
    '''
    if aftPrecision >= befPrecision:
        node.v = Attributes[p]+"=?"     # 节点信息
        curSet = attributeList[p]       # 该属性的所有取值
        for i in curSet:
            Dv = []
            Dv1 = []
            for j in range(len(D)):     # 获得该属性取某一个值时对应的训练集样本标号
                if dataset[D[j],p]==i:
                    Dv.append(D[j])
            for j in range(len(D1)):    # 获得该属性取某一个值时的验证集样本标号
                if dataset[D1[j],p]==i:
                    Dv1.append(D1[j])
 
            # 若该属性取值对应没有符合的样本,则将该分支作为叶子,类别是D中样本数最多的类别
            # 其实就是处理在没有对应的样本情况下的问题。那就取最大可能性的一类。
            if Dv==[]:
                nextNode = Node(i)
                nextNode.v = mostCommonY(D)
                node.children.append(nextNode)
            else:     # 若存在对应的样本,则递归继续生成该节点下的子树
                newA = copy.deepcopy(A)    # 注意是深度复制,否则会改变A中的值
                newA[p]=-1
                node.children.append(treeGenerate(Dv,Dv1,newA,i))
    else:
        node.v = mostCommonY(D)
    return node
 
def countLeaf(root,deep):
    root.deep = deep
    res = 0
    if root.v=='好瓜' or root.v=='坏瓜':   # 说明此时已经是叶子节点了,所以直接返回
        res += 1
        return res,deep
    curdeep = deep             # 记录当前深度
    for i in root.children:    # 得到子树中的深度和叶子节点的个数
        a,b = countLeaf(i,deep+1)
        res += a
        if b>curdeep: curdeep = b
    return res,curdeep
 
def giveLeafID(root,ID):         # 给叶子节点编号
    if root.v=='好瓜' or root.v=='坏瓜':
        root.ID = ID
        ID += 1
        return ID
    for i in root.children:
        ID = giveLeafID(i,ID)
    return ID
 
def plotNode(nodeTxt,centerPt,parentPt,nodeType):     # 绘制节点
    plt.annotate(nodeTxt,xy = parentPt,xycoords='axes fraction',xytext=centerPt,
                 textcoords='axes fraction',va="center",ha="center",bbox=nodeType,
                 arrowprops=arrow_args)
 
def dfsPlot(root):
    if root.ID==-1:          # 说明根节点不是叶子节点
        childrenPx = []
        meanPx = 0
        for i in root.children:
            cur = dfsPlot(i)
            meanPx += cur
            childrenPx.append(cur)
        meanPx = meanPx/len(root.children)
        c = 0
        for i in root.children:
            nodetype = leafNode
            if i.ID<0: nodetype=decisionNode
            plotNode(i.v,(childrenPx[c],0.9-i.deep*0.8/deep),(meanPx,0.9-root.deep*0.8/deep),nodetype)
            plt.text((childrenPx[c]+meanPx)/2,(0.9-i.deep*0.8/deep+0.9-root.deep*0.8/deep)/2,i.title)
            c += 1
        return meanPx
    else:
        return 0.1+root.ID*0.8/(cnt-1)
 
 
myDecisionTreeRoot = treeGenerate(D_train,D_test,A,"root")        # 生成决策树
cnt,deep = countLeaf(myDecisionTreeRoot,0)     # 得到树的深度和叶子节点的个数
giveLeafID(myDecisionTreeRoot,0)
# 绘制决策树
decisionNode = dict(boxstyle = "sawtooth",fc = "0.9",color='blue')
leafNode = dict(boxstyle = "round4",fc="0.9",color='red')
arrow_args = dict(arrowstyle = "<-",color='green')
fig = plt.figure(1,facecolor='white')
rootX = dfsPlot(myDecisionTreeRoot)
plotNode(myDecisionTreeRoot.v,(rootX,0.9),(rootX,0.9),decisionNode)
plt.show()

 

最终得到的决策树如下所示:
20181224231424973.png

可能会发现,根据属性 根蒂 进行的划分没有任何实际意义。   但是,再重申一下,这个情况的出现就是因为在判断是否进行属性划分时使用了等号,而不是严格的大于号。这样做仅仅为了可以得到一个树形结构。因为根节点属性 色泽 在划分时,其实他前后的精确度也是一样的。所以,如果严格大于进行决策,则我们只能得到一个标记为好瓜的节点,没有任何实际的意义。若想得到比较理想的决策树,还需要考虑多个因素进行优化。

 

最后,让我们来看一下后剪枝的问题。相较于之前的代码,后剪枝主要是加入了一个剪枝函数,具体的思想步骤就是根据决策树的深度,从最低处对每一个非叶结点进行判断,决定是否进行剪枝。具体的实现步骤在代码中已经进行了详细的注释,代码全文如下:

点击展开
import numpy as np
import numpy as np
import pandas as pd
import copy
import matplotlib.pyplot as plt
import matplotlib as mpl
mpl.rcParams[u'font.sans-serif'] = ['simhei']
mpl.rcParams['axes.unicode_minus'] = False
 
dataset = pd.read_excel('./WaterMelon_2.0.xlsx',encoding = 'gbk')  # 读取数据
Attributes = dataset.columns[:-1]        #所有属性的名称
#print(Attributes)
dataset = np.matrix(dataset)
m,n = np.shape(dataset)
D_train = []                 # 得到所有的训练样本编号和验证样本编号
D_test = []
for i in range(m):
    if dataset[i,n-1]=='train':D_train.append(i)
    else:D_test.append(i)
#print(D_test)
#print(D_train)
dataset = dataset[:,:-1]
m,n = np.shape(dataset)   # 得到数据集大小
for i in range(m):      # 将标签替换成 好瓜 和 坏瓜
    if dataset[i,n-1]=='是': dataset[i,n-1] = '好瓜'
    else : dataset[i,n-1] = '坏瓜'
attributeList = []       # 属性列表,每一个属性的取值,列表中元素是集合
for i in range(n):
    curSet = set()      # 使用集合是利用了集合里面元素不可重复的特性,从而提取出了每个属性的取值
    for j in range(m):
        curSet.add(dataset[j,i])
    attributeList.append(curSet)
#print(attributeList)
A = list(np.ones(n))    # 表示每一个属性是否被使用,使用过了标为 -1
A[-1] = -1              # 将数据里面的标签和编号列标记为 -1
A[0] = -1
#print(A)
#print(D)
 
class Node(object):             # 创建一个类,用来表示节点的信息
    def __init__(self,title):
        self.title = title      # 上一级指向该节点的线上的标记文字
        self.v = 1              # 节点的信息标记
        self.children = []      # 节点的孩子列表
        self.train = []         # 节点上所含的训练样本编号,主要用于在剪枝时确定节点的标签类别
        self.deep = 0           # 节点深度
        self.ID = -1            # 节点编号
 
def isSameY(D):                  # 判断所有样本是否属于同一类
    curY = dataset[D[0],n-1]
    for i in range(1,len(D)):
        if dataset[D[i],n-1] != curY:
            return  False
    return True
 
def isBlankA(A):              # 判断 A 是否是空,是空则返回true
    for i in range(n):
        if A[i]>0: return False
    return True
 
def isSameAinD(D,A):       # 判断在D中,是否所有的未使用过的样本属性均相同
    for i in range(n):
        if A[i]>0:
            for j in range(1,len(D)):
                if not isSameValue(dataset[D[0],i],dataset[D[j],i]):
                    return False
    return True
 
def isSameValue(v1,v2):            # 判断v1、v2 是否相等
    return  v1==v2
 
def mostCommonY(D):             # 寻找D中样本数最多的类别
    res = dataset[D[0],n-1]     # D中第一个样本标签
    maxC = 1
    count = {}
    count[res] = 1              # 该标签数量记为1
    for i in range(1,len(D)):
        curV = dataset[D[i],n-1]      # 得到D中第i+1个样本的标签
        if curV not in count:         # 若之前不存在这个标签
            count[curV] = 1           # 则该标签数量记为1
        else:count[curV] += 1         # 否则 ,该标签对应的数量加一
        if count[curV]>maxC:          # maxC始终存贮最多标签对应的样本数量
            maxC = count[curV]        # res 存贮当前样本数最多的标签类型
            res = curV
    return res             # 返回的是样本数最多的标签的类型
 
def gini(D):        # 参数D中所存的样本的基尼值
    types = []      # 存贮类别标签
    count = {}      # 存贮每个类别对应的样本数量
    for i in range(len(D)):           # 统计D中存在的每个类型的样本数量
        curY = dataset[D[i],n-1]
        if curY not in count:
            count[curY] = 1
            types.append(curY)
        else:
            count[curY] += 1
    ans = 1
    total = len(D)          # D中样本总数量
    for i in range(len(types)):   # 计算基尼值
        ans -= (count[types[i]]/total)**2
    return ans
 
def gini_indexD(D,p):        # 属性 p 上的基尼指数
    types = []
    count = {}
    for i in range(len(D)):  # 得到每一个属性取值上的样本编号
        a = dataset[D[i],p]
        if a not in count:
            count[a] = [D[i]]
            types.append(a)
        else:
            count[a].append(D[i])
    res = 0
    total = len(D)
    for i in range(len(types)):     # 计算出每一个属性取值分支上的基尼值,再计算出基尼指数
        res += len(count[types[i]])/total*gini(count[types[i]])
    return res
 
def treeGenerate(D,A,title):
    node = Node(title)
    node.train = D
    if isSameY(D):       # D中所有样本是否属于同一类
        node.v = dataset[D[0],n-1]
        return node
 
    # 是否所有属性全部使用过  或者  D中所有样本的未使用的属性均相同
    if isBlankA(A) or isSameAinD(D,A):
        node.v = mostCommonY(D)   # 此时类别标记为样本数最多的类别(暗含可以处理存在异常样本的情况)
        return node               # 否则所有样本的类别应该一致
 
    gini_index = float('inf')
    p = 0
    for i in range(len(A)):       # 循环遍历A,找可以获得最小基尼指数的属性
        if(A[i]>0):
            curGini_index = gini_indexD(D,i)
            if curGini_index < gini_index:
                p = i                     # 存贮属性编号
                gini_index = curGini_index
 
    node.v = Attributes[p]+"=?"    # 节点信息
    curSet = attributeList[p]      # 该属性的所有取值
    for i in curSet:
        Dv = []
        for j in range(len(D)):     # 获得该属性取某一个值时对应的训练集样本标号
            if dataset[D[j],p]==i:
                Dv.append(D[j])
 
            # 若该属性取值对应没有符合的样本,则将该分支作为叶子,类别是D中样本数最多的类别
            # 其实就是处理在没有对应的样本情况下的问题。那就取最大可能性的一类。
        if Dv==[]:
            nextNode = Node(i)
            nextNode.v = mostCommonY(D)
            node.children.append(nextNode)
        else:     # 若存在对应的样本,则递归继续生成该节点下的子树
            newA = copy.deepcopy(A)    # 注意是深度复制,否则会改变A中的值
            newA[p]=-1
            node.children.append(treeGenerate(Dv,newA,i))
    return node
 
def postPruning(root):                       # 后剪枝操作
    maxDeep = getMaxDeep(root,0)                        # 得到树的最大深度
    for de in range(maxDeep-1,0,-1):                   # 循环依次从最低层进行遍历操作
        notLeafnode = getNotLeafnode(root,de)          # 得到指定深度上的非叶子节点列表
        notLeafnode = np.array(notLeafnode).flatten()  # 主要是进行形状变换,拉平成一维数组
        for i in range(len(notLeafnode)):             # 循环遍历每一个非叶节点
            befpruning = getRightNum(root,D_test)/len(D_test)     # 剪枝之前的精确度
            node = notLeafnode[i]                                 # 得到一个节点
            curv = node.v                                         # 当前节点的信息
            v = mostCommonY(node.train)                           # 根据该节点包含的训练集样本得到剪枝后的类别
            node.v = v                           # 进行剪枝,注意此时仅仅是改了信息,与子节点的连接依然存在
            aftpruning = getRightNum(root,D_test)/len(D_test)     # 剪枝后的精确度
            if aftpruning>befpruning:      # 此处用严格大于,是为了可以画出一个好看的树,实际情况下应该用大于等于,参见西瓜书P82页的解释
                node.children = []         # 彻底进行剪枝,去除和子节点的连接信息
                print("去掉划分属性 ",curv[0:2])
                print("剪之前精确度:",befpruning)
                print("剪之后精确度:",aftpruning)
            else:
                node.v = curv              # 若不需要剪枝,则直接更改节点的信息即可恢复到原树
                print("恢复划分属性 ",curv[0:2])
                print("剪之前精确度:",befpruning)
                print("剪之后精确度:",aftpruning)
 
def getMaxDeep(root,deep):        # 得到决策树的最大深度
    root.deep = deep
    if root.v == '好瓜' or root.v == '坏瓜':
        return deep
    curdeep = deep
    for i in root.children:
        b = getMaxDeep(i,deep+1)
        if b>curdeep:
            curdeep = b
    return  curdeep
 
def getNotLeafnode(root,deep):     # 迭代得到指定深度处的非叶子节点
    if root.v != '好瓜' and root.v != '坏瓜' and root.deep == deep:
        return root
    else:
        node = []                # 注意,这个语句只能放在else内,切不可放在函数开头!!!
        if root.children != []:
            for i in root.children:
                curnode = getNotLeafnode(i,deep)
                if curnode != []:
                    node.append(curnode)
    return node
 
def getRightNum(root,D):     # 得到在样本集合D上正确分类的样本数目
    if root.v == '好瓜':
        good = getGoodNum(D)
        return good
    if root.v == '坏瓜':
        bad = getBadNum(D)
        return bad
    children = root.children
    child = children[0]
    num = 0
    v = root.v[0:2]
    p = getIndex(Attributes,v)
    curSet = attributeList[p]
    for i in curSet:
        for k in children:
            if k.title == i:
                child = k
                break
        Dv = []
        for j in range(len(D)):
            if dataset[D[j],p] == i:
                Dv.append(D[j])
        if Dv != []:
            num += getRightNum(child,Dv)
    return num
 
def getGoodNum(D):      # 若标签是好瓜,得到样本中好瓜的数目
    num = 0
    for i in range(len(D)):
        if dataset[D[i],n-1] == '好瓜':
            num += 1
    return num
 
def getBadNum(D):       # 同上,得到坏瓜的数目
    num = 0
    for i in range(len(D)):
        if dataset[D[i],n-1] == '坏瓜':
            num += 1
    return num
 
def getIndex(LL,aa):                 # 得到一个列表里面指定元素的索引
    for i in range(len(LL)):
        if LL[i] == aa:
            return i
 
def countLeaf(root,deep):
    root.deep = deep
    res = 0
    if root.v=='好瓜' or root.v=='坏瓜':   # 说明此时已经是叶子节点了,所以直接返回
        res += 1
        return res,deep
    curdeep = deep             # 记录当前深度
    for i in root.children:    # 得到子树中的深度和叶子节点的个数
        a,b = countLeaf(i,deep+1)
        res += a
        if b>curdeep: curdeep = b
    return res,curdeep
 
def giveLeafID(root,ID):         # 给叶子节点编号
    if root.v=='好瓜' or root.v=='坏瓜':
        root.ID = ID
        ID += 1
        return ID
    for i in root.children:
        ID = giveLeafID(i,ID)
    return ID
 
def plotNode(nodeTxt,centerPt,parentPt,nodeType,arrow_args):     # 绘制节点
    plt.annotate(nodeTxt,xy = parentPt,xycoords='axes fraction',xytext=centerPt,
                 textcoords='axes fraction',va="center",ha="center",bbox=nodeType,
                 arrowprops=arrow_args)
 
def dfsPlot(root,decisionNode,leafNode,arrow_args,cnt,deep):
    if root.ID==-1:          # 说明根节点不是叶子节点
        childrenPx = []
        meanPx = 0
        for i in root.children:
            cur = dfsPlot(i,decisionNode,leafNode,arrow_args,cnt,deep)
            meanPx += cur
            childrenPx.append(cur)
        meanPx = meanPx/len(root.children)
        c = 0
        for i in root.children:
            nodetype = leafNode
            if i.ID<0: nodetype=decisionNode
            plotNode(i.v,(childrenPx[c],0.9-i.deep*0.8/deep),(meanPx,0.9-root.deep*0.8/deep),nodetype,arrow_args)
            plt.text((childrenPx[c]+meanPx)/2,(0.9-i.deep*0.8/deep+0.9-root.deep*0.8/deep)/2,i.title)
            c += 1
        return meanPx
    else:
        return 0.1+root.ID*0.8/(cnt-1)
 
def plotTree(root):                  # 绘制决策树
    cnt,deep = countLeaf(root,0)     # 得到树的深度和叶子节点的个数
    giveLeafID(root,0)
    decisionNode = dict(boxstyle = "sawtooth",fc = "0.9",color='blue')
    leafNode = dict(boxstyle = "round4",fc="0.9",color='red')
    arrow_args = dict(arrowstyle = "<-",color='green')
    fig = plt.figure(1,facecolor='white')
    rootX = dfsPlot(root,decisionNode,leafNode,arrow_args,cnt,deep)
    plotNode(root.v,(rootX,0.9),(rootX,0.9),decisionNode,arrow_args)
    plt.show()
 
 
myDecisionTreeRoot = treeGenerate(D_train,A,"root")        # 生成未剪枝决策树
plotTree(myDecisionTreeRoot)                               # 未剪枝的决策树
postPruning(myDecisionTreeRoot)                            # 进行后剪枝
plotTree(myDecisionTreeRoot)                               # 后剪枝的决策树

在训练集上基于基尼指数划分选择得到的决策树如下图所示:


20181225183036437.jpg

后剪枝后得到的决策树如下图所示:
20181225183243435.jpg
同样出现了根蒂属性划分无意义的问题。再次声明只是为了画出的树好看一点,在精确度没有变化时,选择不进行剪枝。在实际应用中,根据奥卡姆剃刀准则,应该进行剪枝处理。

 

 课后题4.7  不使用递归方法进行决策树的生成。

如果用     树的最大深度    进行控制,使用栈数据结构可以实现非递归情况下的深度优先搜索算法。而使用队列数据结构,则可以实现广度优先搜索算法。

参考了这篇博客中给出的算法(非递归决策树算法),结合之前的经验,进行了一部分的修改。下面给出算法的文字描述:

点击展开
输入:训练集 D={(x1,y1),(x2,y2),...,(xm,ym)};
 
      属性集 A={a1,a2,...,ad}
 
      最大深度 MaxDepth
 
构建一个结点类:Node     
   存贮的信息:节点文字信息、节点深度、节点所含样本集、节点所含的可用属性、节点的孩子节点列表、节点名称、该节点用来划分的最佳属性的编号(仅针对非叶节点,其余为默认值0)
 
过程:函数TreeGenerate(D,A,MaxDepth)
    1: 生成结点  node;
    2: if D中样本属于同一类别C  then
    3:     将node标记为C类叶节点;return
    4: end if
    5: if A是空集或者D中样本在A中取值相同  then
    6:     将node标记为叶节点,类别为D中样本数最多的类; return
    7: end if
    8: 从A中选择最优划分属性a*;
    9: 将node标记为分支节点,给node相应的变量赋值,包括深度、样本集、属性集、最佳属性编号、文字信息等
   10: 将node加入到结点列表nodeQueue中
   11: while nodeQueue != []:
   12:     取出一个节点,记为curnode        (此处,只需改动取元素的方式就可以分别实现队列和栈)
   13:     if curnode的深度已经大于等于最大深度: then
   14:          将curnode标记为叶节点,类型为其自身包含的样本集中样本数目最多的类别数
   15:          continue
   16:     for a*包含的每一个值 a*v   do:
   17:          生成curnode的一个分支节点,记为nextnode
   18:          给nextnode初始化深度、属性集等信息
   19:          找到curnode包含的样本集中属性值是a*v的所有样本,记为Dv
   20:          给nextnode初始化样本集,就等于Dv
   21:          if nextnode的样本集为空     then
   22:               将nextnode记为叶子节点,类别为curnode样本集中样本数目最多的类别
   23:          elif nextnode的样本集中样本均属于同一类C   then
   24:               将nextnode记为叶子节点,类别为C
   25:          elif nextnode的可划分属性集为空 或者 它的样本集中所有的可划分属性取值均相同
   26:               将nextnode记为叶子节点,类别为nextnode样本集中样本数目最多的类别
   27:          else 
   28:               从nextnode所包含的可划分属性集中选择出最佳划分属性a*'
   29:               将nextnode的节点文字信息记为a*'
   30:               将节点nextnode加入到结点列表nodeQueue中
   31:          end if
   32:     end for
   33: end while
   34: 返回结点 node
   35: 输出:得到的是以node为根节点的一棵决策树

下面给出使用 栈 数据结构进行深度优先搜索,并基于基尼指数进行划分选择,采用非递归方式生成决策树的代码实现

点击展开
                     
import numpy as np
import pandas as pd
import copy
import matplotlib.pyplot as plt
import matplotlib as mpl
mpl.rcParams[u'font.sans-serif'] = ['simhei']
mpl.rcParams['axes.unicode_minus'] = False
 
dataset = pd.read_excel('./WaterMelon_2.0.xlsx',encoding = 'gbk')  # 读取数据
Attributes = dataset.columns[:-1]        #所有属性的名称
#print(Attributes)
dataset = np.matrix(dataset)
dataset = dataset[:,:-1]
m,n = np.shape(dataset)    # 得到数据集大小
for i in range(m):        # 将标签替换成 好瓜 和 坏瓜
    if dataset[i,n-1]=='是': dataset[i,n-1] = '好瓜'
    else : dataset[i,n-1] = '坏瓜'
attributeList = []       # 属性列表,每一个属性的取值,列表中元素是集合
for i in range(n):
    curSet = set()      # 使用集合是利用了集合里面元素不可重复的特性,从而提取出了每个属性的取值
    for j in range(m):
        curSet.add(dataset[j,i])
    attributeList.append(curSet)
#print(attributeList)
D = np.arange(0,m,1)
A = list(np.ones(n))    # 表示每一个属性是否被使用,使用过了标为 -1
A[-1] = -1              # 将数据里面的标签和编号列标记为 -1
A[0] = -1
#print(A)
#print(D)
 
class Node(object):             # 创建一个类,用来表示节点的信息
    def __init__(self,title):
        self.title = title      # 上一级指向该节点的线上的标记文字
        self.v = 1              # 节点的信息标记
        self.children = []      # 节点的孩子列表
        self.p = 0              # 最佳属性索引号
        self.train = []         # 节点上所含的训练样本编号,主要用于在剪枝时确定节点的标签类别
        self.attribute = []     # 该结点含有的可用来进行划分的属性
        self.deep = 0           # 节点深度
        self.ID = -1            # 节点编号
 
def isSameY(D):                  # 判断所有样本是否属于同一类
    curY = dataset[D[0],n-1]
    for i in range(1,len(D)):
        if dataset[D[i],n-1] != curY:
            return  False
    return True
 
def isBlankA(A):              # 判断 A 是否是空,是空则返回true
    for i in range(n):
        if A[i]>0: return False
    return True
 
def isSameAinD(D,A):       # 判断在D中,是否所有的未使用过的样本属性均相同
    for i in range(n):
        if A[i]>0:
            for j in range(1,len(D)):
                if not isSameValue(dataset[D[0],i],dataset[D[j],i]):
                    return False
    return True
 
def isSameValue(v1,v2):            # 判断v1、v2 是否相等
    return  v1==v2
 
def mostCommonY(D):             # 寻找D中样本数最多的类别
    res = dataset[D[0],n-1]     # D中第一个样本标签
    maxC = 1
    count = {}
    count[res] = 1              # 该标签数量记为1
    for i in range(1,len(D)):
        curV = dataset[D[i],n-1]      # 得到D中第i+1个样本的标签
        if curV not in count:         # 若之前不存在这个标签
            count[curV] = 1           # 则该标签数量记为1
        else:count[curV] += 1         # 否则 ,该标签对应的数量加一
        if count[curV]>maxC:          # maxC始终存贮最多标签对应的样本数量
            maxC = count[curV]        # res 存贮当前样本数最多的标签类型
            res = curV
    return res             # 返回的是样本数最多的标签的类型
 
def gini(D):        # 参数D中所存的样本的基尼值
    types = []      # 存贮类别标签
    count = {}      # 存贮每个类别对应的样本数量
    for i in range(len(D)):           # 统计D中存在的每个类型的样本数量
        curY = dataset[D[i],n-1]
        if curY not in count:
            count[curY] = 1
            types.append(curY)
        else:
            count[curY] += 1
    ans = 1
    total = len(D)          # D中样本总数量
    for i in range(len(types)):   # 计算基尼值
        ans -= (count[types[i]]/total)**2
    return ans
 
def gini_indexD(D,p):        # 属性 p 上的基尼指数
    types = []
    count = {}
    for i in range(len(D)):  # 得到每一个属性取值上的样本编号
        a = dataset[D[i],p]
        if a not in count:
            count[a] = [D[i]]
            types.append(a)
        else:
            count[a].append(D[i])
    res = 0
    total = len(D)
    for i in range(len(types)):     # 计算出每一个属性取值分支上的基尼值,再计算出基尼指数
        res += len(count[types[i]])/total*gini(count[types[i]])
    return res
 
def treeGenerate(D,A,title,MaxDeepth):
    nodeQueue = []
    node = Node(title)
    node.train = D
    node.attribute = A
    node.deep = 0
    if isSameY(D):       # D中所有样本是否属于同一类
        node.v = dataset[D[0],n-1]
        return node
 
    # 是否所有属性全部使用过  或者  D中所有样本的未使用的属性均相同
    if isBlankA(A) or isSameAinD(D,A):
        node.v = mostCommonY(D)   # 此时类别标记为样本数最多的类别(暗含可以处理存在异常样本的情况)
        return node               # 否则所有样本的类别应该一致
 
    p = getBestAttribute(D,A)
    node.p = p
    node.v = Attributes[p]+"=?"       # 节点信息
    print('加入:',node.v)
    nodeQueue.append(node)            # 加入队列
 
    while nodeQueue != []:
        curNode = nodeQueue.pop()
        print('取出:',curNode.v)
        pp = curNode.p
        if curNode.deep == MaxDeepth:     # 达到最大深度
            curv = mostCommonY(curNode.train)
            curNode.v = curv
            continue
        curAttribute = curNode.attribute
        nextAttibute = setAunuse(curAttribute,pp)   # 将该节点下的A中的属性p处 置为-1,得到子节点中可以使用的属性
        curSet = attributeList[pp]      # 该属性的所有取值
        for i in curSet:
            nextNode = Node(i)         # 根据该最佳属性的某一个取值,生成一个分支节点
            nextNode.attribute = nextAttibute
            nextNode.deep = curNode.deep + 1
            Dv = []       # 获取该分支所含有的样本集合编号
            for j in range(len(curNode.train)):     # 获得该属性取某一个值时对应的训练集样本标号
                if dataset[curNode.train[j],pp]==i:
                    Dv.append(curNode.train[j])
            nextNode.train = Dv
 
            # 若该属性取值对应没有符合的样本,则将该分支作为叶子,类别是D中样本数最多的类别
            # 其实就是处理在没有对应的样本情况下的问题。那就取最大可能性的一类。
            if nextNode.train==[]:
                nextNode.v = mostCommonY(curNode.train)
                curNode.children.append(nextNode)
            elif isSameY(nextNode.train):         # 若该分支所有样本类别都一致
                nextNode.v = dataset[nextNode.train[0],n-1]
                curNode.children.append(nextNode)
            elif isBlankA(nextNode.attribute) or isSameAinD(nextNode.train,nextNode.attribute):
                nextNode.v = mostCommonY(nextNode.train)
                curNode.children.append(nextNode)
            else:
                ppp = getBestAttribute(nextNode.train,nextNode.attribute)
                nextNode.v = Attributes[ppp]+"=?"
                nextNode.p = ppp
                curNode.children.append(nextNode)
                nodeQueue.append(nextNode)
                print('加入:',nextNode.v)
    return node
 
def setAunuse(A,p):
    newA = copy.deepcopy(A)
    newA[p] = -1
    return newA
 
 
def getBestAttribute(D,A):
    gini_index = float('inf')
    p = 0
    for i in range(len(A)):      # 循环遍历A,找可以获得最小基尼指数的属性
        if(A[i]>0):
            curGini_index = gini_indexD(D,i)
            if curGini_index < gini_index:
                p = i                     # 存贮属性编号
                gini_index = curGini_index
    return p
 
def countLeaf(root):
    deep = root.deep
    res = 0
    if root.v=='好瓜' or root.v=='坏瓜':   # 说明此时已经是叶子节点了,所以直接返回
        res += 1
        return res,deep
    curdeep = deep             # 记录当前深度
    for i in root.children:    # 得到子树中的深度和叶子节点的个数
        a,b = countLeaf(i)
        res += a
        if b>curdeep: curdeep = b
    return res,curdeep
 
def giveLeafID(root,ID):         # 给叶子节点编号
    if root.v=='好瓜' or root.v=='坏瓜':
        root.ID = ID
        ID += 1
        return ID
    for i in root.children:
        ID = giveLeafID(i,ID)
    return ID
 
def plotNode(nodeTxt,centerPt,parentPt,nodeType,arrow_args):     # 绘制节点
    plt.annotate(nodeTxt,xy = parentPt,xycoords='axes fraction',xytext=centerPt,
                 textcoords='axes fraction',va="center",ha="center",bbox=nodeType,
                 arrowprops=arrow_args)
 
def dfsPlot(root,decisionNode,leafNode,arrow_args,cnt,deep):
    if root.ID==-1:          # 说明根节点不是叶子节点
        childrenPx = []
        meanPx = 0
        for i in root.children:
            cur = dfsPlot(i,decisionNode,leafNode,arrow_args,cnt,deep)
            meanPx += cur
            childrenPx.append(cur)
        meanPx = meanPx/len(root.children)
        c = 0
        for i in root.children:
            nodetype = leafNode
            if i.ID<0: nodetype=decisionNode
            plotNode(i.v,(childrenPx[c],0.9-i.deep*0.8/deep),(meanPx,0.9-root.deep*0.8/deep),nodetype,arrow_args)
            plt.text((1.5*childrenPx[c]+0.5*meanPx)/2,(0.9-i.deep*0.8/deep+0.9-root.deep*0.8/deep)/2,i.title)
            c += 1
        return meanPx
    else:
        return 0.1+root.ID*0.8/(cnt-1)
 
def plotTree(root):                  # 绘制决策树
    cnt,deep = countLeaf(root)       # 得到树的深度和叶子节点的个数
    giveLeafID(root,0)
    decisionNode = dict(boxstyle = "sawtooth",fc = "0.9",color='blue')
    leafNode = dict(boxstyle = "round4",fc="0.9",color='red')
    arrow_args = dict(arrowstyle = "<-",color='green')
    fig = plt.figure(1,facecolor='white')
    rootX = dfsPlot(root,decisionNode,leafNode,arrow_args,cnt,deep)
    plotNode(root.v,(rootX,0.9),(rootX,0.9),decisionNode,arrow_args)
    plt.show()
 
myDecisionTreeRoot = treeGenerate(D,A,"root",5)            # 生成未剪枝决策树
plotTree(myDecisionTreeRoot)                               # 未剪枝的决策树

最终得到的决策树和之前递归方式得到的一致,如下图所示(使用的数据集市西瓜数据集2.0的所有样本)
20181226165702806.jpg

另外,在程序输出中,我们得到的节点处理顺序如下所示,结合决策树可以看出,确实是进行了深度优先的搜索:

点击展开
加入: 纹理=?
取出: 纹理=?
加入: 触感=?
加入: 根蒂=?
取出: 根蒂=?
加入: 色泽=?
取出: 色泽=?
加入: 触感=?
取出: 触感=?
取出: 触感=?

当采用队列数据结构时,我们就可以实现广度优先搜索。代码基本和上面一致,只是提取元素的方式变了一下。最终得到的决策树也完全相同。下面仅给出程序输出的处理节点的前后顺序信息,可以看出,确实是进行了广度优先搜索。

点击展开
加入: 纹理=?
取出: 纹理=?
加入: 根蒂=?
加入: 触感=?
取出: 根蒂=?
加入: 色泽=?
取出: 触感=?
取出: 色泽=?
加入: 触感=?
取出: 触感=?

课后题4.8: 采用最大节点数进行控制,实现广度优先搜索。

当采用  最大节点数  进行控制时,此时我们只适合采用队列数据结构进行广度优先的搜索算法,而不能再进行深度优先的搜索。

因为,若进行深度优先的搜索,可能出现一种情况就是当某一个分支一直搜索到最深处时,达到了最大节点的限制要求,此时,其他分支可能还没来的及划分就已经结束,这样就会生成一棵畸形树。也就是决策树看起来某一个分支深度很深,而有些分支可能只有一层,这样的决策树肯定不是我们希望得到的。   所以题目也明确指出使用广度优先。

而采用广度优先搜索时,就可以很好地解决这个问题。因为广度优先就要求每一层每一层进行处理,所以即使在处理某一个节点时达到了最大节点数的限制,此时,这个决策树也不会出现畸形(理论上只会差一层)。

 

关于在最大节点数的限制下进行广度优先搜索的算法,其实只需要将上面代码中的限制条件改成最大节点数限制即可,应该不是很困难,具体的代码有空再补上。

 

对于  使用最大节点数控制的广度优先搜索   和   使用最大深度控制的深度优先搜索或者广度优先搜索    哪一种更易于控制决策树所需存贮不超过内存的问题,我个人见解如下(我个人认为题目中所述的比较两种方式,主要是让比较两种不同的控制方式的差异,而不是比较深度搜索和广度搜索的差异):

认为最大节点数控制的广度优先搜索更容易控制内存。

假设,当所有属性的取值有很多种时,标志着决策树每一个非叶节点的直接子节点数量会很大,形象点说,就是树看起来很胖。此时,如果使用最大深度控制的深度优先搜索,当搜索到最深处的一个节点处时,可能他的深度还没有达到最大深度的限制,但是可以想象,由于先进后出的原则,这个最深处节点的所有兄弟节点、它的父节点的所有兄弟节点、它的祖父节点的所有兄弟节点……依次类推,直到根节点,这些所有节点必然还存留在栈中等待处理。而此时该节点的深度可能并不是很深,但是内存必然已经占用了很多。            同样,广度优先搜索时也存在这样的问题,可能深度很小,但是队列中等待处理的节点数目已经很大,占用了很大内存。           因为内存占用的多少是和等待处理的结点数目直接相关,而和树的深度并没有很直接的联系。

再者,让我们来看最大节点数目控制的广度优先搜索的情况。在使用最大节点数目进行控制的广度优先搜索时,不论每一个非叶节点的子节点的数目有多大,我们总可以通过控制最大节点数目来保证所占用的内存肯定低于某一个确定的值。这样就可以控制内存不会溢出。

参考:【决策树python源码实现(含预剪枝和后剪枝)】http://t.csdnimg.cn/Fs5gq
【Python手撸机器学习系列(六):决策树(附Python实现西瓜书决策树构建及剪枝代码) 】http://t.csdnimg.cn/iD6CI
https://blog.csdn.net/qq_37691909/article/details/85235472
http://www.baidu.com/link?url=UeVaXJQxSNJl-0EKfTzVJr0K9ZddMwWn7yp8sdqwpuAnxDG3zfQ7ul9Q6UnEZjqbFXp_9HKm9dAHsr43SbmZdK&wd=&eqid=e8448af20034942500000006658edad3
【西瓜书学习笔记—第四章 决策树】http://t.csdnimg.cn/vJjIe
https://zhuanlan.zhihu.com/p/44666694
https://blog.csdn.net/wzmsltw/article/details/51057311
https://zhuanlan.zhihu.com/p/44666694
https://link.zhihu.com/?target=https%3A//github.com/han1057578619/MachineLearning_Zhouzhihua_ProblemSets/tree/master/ch4–%25E5%2586%25B3%25E7%25AD%2596%25E6%25A0%2591/4.3-4.4
https://github.com/hanmq/MachineLearning_Zhouzhihua_ProblemSets/tree/master/ch4–%E5%86%B3%E7%AD%96%E6%A0%91/4.3-4.4