实现最基本的决策树——只有代码

决策树

决策树

决策树学习的目的是为了产生一颗泛化能力强,即处理未见示列能力强的决策树

计算信息熵

def calcShannonEnt(dataSet):
    """
    :Author: WangBlue
    :Create: 2022/8/14 17:08
    :func:计算香农熵
    :param dataSet: 数据集
    :return: 香农熵
    """
    numEntires = len(dataSet)  # 返回数据集的行数
    labelCounts = {}  # 保存每个标签(Label)出现次数的字典
    for featVec in dataSet:  # 对每组特征向量进行统计
        currentLabel = featVec[-1]  # 提取标签(Label)信息,就相当与取最后一列数据
        if currentLabel not in labelCounts.keys():  # 如果标签(Label)没有放入统计次数的字典,添加进去
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1  # Label计数,统计最后一列中,每个类别的占比(yes:9)
    shannonEnt = 0.0  # 经验熵(香农熵),初始化香农熵
    for key in labelCounts:  # 计算香农熵
        prob = float(labelCounts[key]) / numEntires  # 选择该标签(Label)的概率
        shannonEnt -= prob * log(prob, 2)  # 利用公式计算
    return shannonEnt

划分数据集:

def splitDataSet(dataSet, axis, value):
    """
    :Author: WangBlue
    :Create: 2022/8/14 17:10
    :func:划分数据集
    :param dataSet:待划分的数据集
    :param axis: 划分数据集的特征的索引值
    :param value: 返回特征的值
    :return: 划分后的数据集
    """
    retDataSet = []  # 创建返回的数据集列表
    for featVec in dataSet:  # 遍历数据集
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis]  # 去掉axis特征
            reducedFeatVec.extend(featVec[axis + 1:])  # 将符合条件的添加到返回的数据集
            retDataSet.append(reducedFeatVec)
​
    return retDataSet

选择最佳特征划分

def chooseBestFeatureToSplit(dataSet):
    """
    :Author: WangBlue
    :Create: 2022/8/14 17:50
    :func:选择最好的特征
    :param dataSet:
    :return:信息增益最大的特征的索引值
    """
    numFeatures = len(dataSet[0]) - 1                    #特征数量,去除最后一列
    baseEntropy = calcShannonEnt(dataSet)                 #计算数据集的香农熵
    bestInfoGain = 0.0                                  #信息增益
    bestFeature = -1                                    #最优特征的索引值
    for i in range(numFeatures):                         #遍历所有特征
        #获取dataSet的第i个所有特征
        featList = [example[i] for example in dataSet]
        uniqueVals = set(featList)                         #创建set集合{},元素不可重复
        newEntropy = 0.0                                  #经验条件熵
        for value in uniqueVals:                         #计算信息增益
            subDataSet = splitDataSet(dataSet, i, value)         #subDataSet划分后的子集
            prob = len(subDataSet) / float(len(dataSet))           #计算子集的概率
            newEntropy += prob * calcShannonEnt(subDataSet)     #根据公式计算经验条件熵
        infoGain = baseEntropy - newEntropy                     #信息增益
        # print("第%d个特征的增益为%.3f" % (i, infoGain))            #打印每个特征的信息增益
        if (infoGain > bestInfoGain):                             #计算信息增益
            bestInfoGain = infoGain                             #更新信息增益,找到最大的信息增益
            bestFeature = i                                     #记录信息增益最大的特征的索引值
    return bestFeature

投票表决:

原因:如果数据集已经处理了所有属性,但是类标签依然不是唯一的,此时我们需要 决定如何定义该叶子节点,在这种情况下,我们通常会采用多数表决的方式决定该叶子节点的分类

实现:

def majorityCnt(classList):
    """
    :Author: WangBlue
    :Create: 2022/8/14 18:00
    :func:如果数据集已经处理了所有属性,但是类标签依然不是唯一的,此时我们需要
           决定如何定义该叶子节点,在这种情况下,我们通常会采用多数表决的方式决定该叶子节点的分类
    :param classList:
    :return: 返回classList中出现次数最多的元素
    """
    classCount = {}
    for vote in classList:                                        #统计classList中每个元素出现的次数
        if vote not in classCount.keys():classCount[vote] = 0
        classCount[vote] += 1
    sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)        #根据字典的值降序排序
    print(sortedClassCount)
    return sortedClassCount[0][0]                                #返回classList中出现次数最多的元素

创建决策树:

def createTree(dataSet, labels, featLabels):
    """
    :Author: WangBlue
    :Create: 2022/8/14 18:10
    :func:创建树
    :param dataSet: 数据集
    :param labels: 标签列表
    :param featLabels:存储选择的最优特征标签,为了后面画图设置
    :return: 创建好的树
    """
    classList = [example[-1] for example in dataSet]            #取分类标签(是否放贷:yes or no)
    if classList.count(classList[0]) == len(classList):            #如果类别完全相同则停止继续划分
        return classList[0]
    if len(dataSet[0]) == 1:                                    #遍历完所有特征时返回出现次数最多的类标签
        return majorityCnt(classList)
    bestFeat = chooseBestFeatureToSplit(dataSet)                #选择最优特征
    bestFeatLabel = labels[bestFeat]                            #最优特征的标签,为了后面的画图,
    featLabels.append(bestFeatLabel)
    myTree = {bestFeatLabel:{}}                                    #根据最优特征的标签生成树
    del(labels[bestFeat])                                        #删除已经使用特征标签
    featValues = [example[bestFeat] for example in dataSet]        #得到训练集中所有最优特征的属性值
    uniqueVals = set(featValues)                                #去掉重复的属性值
    for value in uniqueVals:                                    #遍历特征,创建决策树。
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), labels, featLabels)
    return myTree

测试函数

def classify(inputTree, featLabels, testVec):
        """
        :Author: WangBlue
        :Create: 2022/8/14 18:20
        :func:测试创建的决策树
        :param inputTree: 输入已经创建的决策树
        :param featLabels: 存储选择的最优特征标签
        :param testVec:测试数据列表,顺序对应最优特征标签
        :return:
        """
        global classLabel
        firstStr = next(iter(inputTree))  # 获取决策树结点
        secondDict = inputTree[firstStr]  # 下一个字典
        featIndex = featLabels.index(firstStr)
​
        for key in secondDict.keys():
            if testVec[featIndex] == key:
                if type(secondDict[key]).__name__ == 'dict':
                    classLabel = classify(secondDict[key], featLabels, testVec)
                else:
                    classLabel = secondDict[key]
        return classLabel

运行:

if __name__ == '__main__':
    dataSet = [[0, 0, 0, 0, 'no'],  # 数据集
               [0, 0, 0, 1, 'no'],
               [0, 1, 0, 1, 'yes'],
               [0, 1, 1, 0, 'yes'],
               [0, 0, 0, 0, 'no'],
               [1, 0, 0, 0, 'no'],
               [1, 0, 0, 1, 'no'],
               [1, 1, 1, 1, 'yes'],
               [1, 0, 1, 2, 'yes'],
               [1, 0, 1, 2, 'yes'],
               [2, 0, 1, 2, 'yes'],
               [2, 0, 1, 1, 'yes'],
               [2, 1, 0, 1, 'yes'],
               [2, 1, 0, 2, 'yes'],
               [2, 0, 0, 0, 'no']]
    labels = ['年龄', '有工作', '有自己的房子', '信贷情况']
    featLabels = []
    mytree = createTree(dataSet, labels, featLabels)
    testVec = [1, 0]  # 测试数据 选择的是有房子,但是没有工作,结果:房贷
    result = classify(mytree, featLabels, testVec)
    if result == 'yes':
        print('放贷')
    if result == 'no':
        print('不放贷')

完整代码:其中里画树、存储树

import operator
​
from math import log
​
from matplotlib.font_manager import FontProperties
​
​
def calcShannonEnt(dataSet):
    """
    :Author: WangBlue
    :Create: 2022/8/14 17:08
    :func:计算香农熵
    :param dataSet: 数据集
    :return: 香农熵
    """
    numEntires = len(dataSet)  # 返回数据集的行数
    labelCounts = {}  # 保存每个标签(Label)出现次数的字典
    for featVec in dataSet:  # 对每组特征向量进行统计
        currentLabel = featVec[-1]  # 提取标签(Label)信息,就相当与取最后一列数据
        if currentLabel not in labelCounts.keys():  # 如果标签(Label)没有放入统计次数的字典,添加进去
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1  # Label计数,统计最后一列中,每个类别的占比(yes:9)
    shannonEnt = 0.0  # 经验熵(香农熵),初始化香农熵
    for key in labelCounts:  # 计算香农熵
        prob = float(labelCounts[key]) / numEntires  # 选择该标签(Label)的概率
        shannonEnt -= prob * log(prob, 2)  # 利用公式计算
    return shannonEnt
​
​
def splitDataSet(dataSet, axis, value):
    """
    :Author: WangBlue
    :Create: 2022/8/14 17:10
    :func:划分数据集
    :param dataSet:待划分的数据集
    :param axis: 划分数据集的特征的索引值
    :param value: 返回特征的值
    :return: 划分后的数据集
    """
    retDataSet = []  # 创建返回的数据集列表
    for featVec in dataSet:  # 遍历数据集
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis]  # 去掉axis特征
            reducedFeatVec.extend(featVec[axis + 1:])  # 将符合条件的添加到返回的数据集
            retDataSet.append(reducedFeatVec)
​
    return retDataSet
​
​
def chooseBestFeatureToSplit(dataSet):
    """
    :Author: WangBlue
    :Create: 2022/8/14 17:50
    :func:选择最好的特征
    :param dataSet:
    :return:信息增益最大的特征的索引值
    """
    numFeatures = len(dataSet[0]) - 1                    #特征数量,去除最后一列
    baseEntropy = calcShannonEnt(dataSet)                 #计算数据集的香农熵
    bestInfoGain = 0.0                                  #信息增益
    bestFeature = -1                                    #最优特征的索引值
    for i in range(numFeatures):                         #遍历所有特征
        #获取dataSet的第i个所有特征
        featList = [example[i] for example in dataSet]
        uniqueVals = set(featList)                         #创建set集合{},元素不可重复
        newEntropy = 0.0                                  #经验条件熵
        for value in uniqueVals:                         #计算信息增益
            subDataSet = splitDataSet(dataSet, i, value)         #subDataSet划分后的子集
            prob = len(subDataSet) / float(len(dataSet))           #计算子集的概率
            newEntropy += prob * calcShannonEnt(subDataSet)     #根据公式计算经验条件熵
        infoGain = baseEntropy - newEntropy                     #信息增益
        # print("第%d个特征的增益为%.3f" % (i, infoGain))            #打印每个特征的信息增益
        if (infoGain > bestInfoGain):                             #计算信息增益
            bestInfoGain = infoGain                             #更新信息增益,找到最大的信息增益
            bestFeature = i                                     #记录信息增益最大的特征的索引值
    return bestFeature
​
​
def majorityCnt(classList):
    """
    :Author: WangBlue
    :Create: 2022/8/14 18:00
    :func:如果数据集已经处理了所有属性,但是类标签依然不是唯一的,此时我们需要
           决定如何定义该叶子节点,在这种情况下,我们通常会采用多数表决的方式决定该叶子节点的分类
    :param classList:
    :return: 返回classList中出现次数最多的元素
    """
    classCount = {}
    for vote in classList:                                        #统计classList中每个元素出现的次数
        if vote not in classCount.keys():classCount[vote] = 0
        classCount[vote] += 1
    sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)        #根据字典的值降序排序
    print(sortedClassCount)
    return sortedClassCount[0][0]                                #返回classList中出现次数最多的元素
​
​
def createTree(dataSet, labels, featLabels):
    """
    :Author: WangBlue
    :Create: 2022/8/14 18:10
    :func:创建树
    :param dataSet: 数据集
    :param labels: 标签列表
    :param featLabels:存储选择的最优特征标签,为了后面画图设置
    :return: 创建好的树
    """
    classList = [example[-1] for example in dataSet]            #取分类标签(是否放贷:yes or no)
    if classList.count(classList[0]) == len(classList):            #如果类别完全相同则停止继续划分
        return classList[0]
    if len(dataSet[0]) == 1:                                    #遍历完所有特征时返回出现次数最多的类标签
        return majorityCnt(classList)
    bestFeat = chooseBestFeatureToSplit(dataSet)                #选择最优特征
    bestFeatLabel = labels[bestFeat]                            #最优特征的标签,为了后面的画图,
    featLabels.append(bestFeatLabel)
    myTree = {bestFeatLabel:{}}                                    #根据最优特征的标签生成树
    del(labels[bestFeat])                                        #删除已经使用特征标签
    featValues = [example[bestFeat] for example in dataSet]        #得到训练集中所有最优特征的属性值
    uniqueVals = set(featValues)                                #去掉重复的属性值
    for value in uniqueVals:                                    #遍历特征,创建决策树。
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), labels, featLabels)
    return myTree
​
# 测试
def classify(inputTree, featLabels, testVec):
        """
        :Author: WangBlue
        :Create: 2022/8/14 18:20
        :func:测试创建的决策树
        :param inputTree: 输入已经创建的决策树
        :param featLabels: 存储选择的最优特征标签
        :param testVec:测试数据列表,顺序对应最优特征标签
        :return:
        """
        global classLabel
        firstStr = next(iter(inputTree))  # 获取决策树结点
        secondDict = inputTree[firstStr]  # 下一个字典
        featIndex = featLabels.index(firstStr)
​
        for key in secondDict.keys():
            if testVec[featIndex] == key:
                if type(secondDict[key]).__name__ == 'dict':
                    classLabel = classify(secondDict[key], featLabels, testVec)
                else:
                    classLabel = secondDict[key]
        return classLabel
​
​
​
​
def getNumLeafs(myTree):
    """
    :Author: WangBlue
    :Create: 2022/8/14 20:12
    :func:获取叶子节点数目
    :param myTree:
    :return: 叶子节点的数目
    """
    numLeafs = 0                                                #初始化叶子
    firstStr = next(iter(myTree))                                #python3中myTree.keys()返回的是dict_keys,不在是list,所以不能使用myTree.keys()[0]的方法获取结点属性,可以使用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):
    """
    :Author: WangBlue
    :Create: 2022/8/14 20:15
    :func:获取决策树的层数
    :param myTree: 决策树
    :return:
    """
    maxDepth = 0                                                #初始化决策树深度
    firstStr = next(iter(myTree))                                #python3中myTree.keys()返回的是dict_keys,不在是list,所以不能使用myTree.keys()[0]的方法获取结点属性,可以使用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):
    """
    :Author: WangBlue
    :Create: 2022/8/14 20:20
    :func:绘制结点
    :param nodeTxt: 结点名
    :param centerPt: 文本位置
    :param parentPt: 标注的箭头位置
    :param nodeType:结点的格式
    :return:无
    """
    arrow_args = dict(arrowstyle="<-")                                            #定义箭头格式
    font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14)        #设置中文字体
    createPlot.ax1.annotate(nodeTxt, xy=parentPt,  xycoords='axes fraction',    #绘制结点
        xytext=centerPt, textcoords='axes fraction',
        va="center", ha="center", bbox=nodeType, arrowprops=arrow_args, FontProperties=font)
​
​
def plotMidText(cntrPt, parentPt, txtString):
    """
​
    :param cntrPt: 用于计算标注位置
    :param parentPt:用于计算标注位置
    :param txtString:标注的内容
    :return:无
    """
    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):
    """
    :Author: WangBlue
    :Create: 2022/8/14 20:31
    :func:绘制决策树
    :param myTree:
    :param parentPt:
    :param nodeTxt:
    :return:无
    """
    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          #y偏移
    for key in secondDict.keys():
        if type(secondDict[key]).__name__=='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):
    """
    :Author: WangBlue
    :Create: 2022/8/14 20:33
    :func:创建绘制面板
    :param inTree: 决策树
    :return: 无
    """
    import matplotlib.pyplot as plt
    fig = plt.figure(1, facecolor='white')                                                    #创建fig
    fig.clf()                                                                                #清空fig
    axprops = dict(xticks=[], yticks=[])
    createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)                                #去掉x、y轴
    plotTree.totalW = float(getNumLeafs(inTree))                                            #获取决策树叶结点数目
    plotTree.totalD = float(getTreeDepth(inTree))                                            #获取决策树层数
    plotTree.xOff = -0.5/plotTree.totalW; plotTree.yOff = 1.0                              #x偏移
    plotTree(inTree, (0.5,1.0), '')                                                            #绘制决策树
    plt.show()
​
​
# 决策树的存储
def storeTree(inputTree, filename):
    """
    :Author: WangBlue
    :Create: 2022/8/14 20:41
    :func:决策树的存储
    :param inputTree: 输入的决策树
    :param filename: 文件名称
    :return: 无
    """
    import pickle
    with open(filename, 'wb') as fw:
        pickle.dump(inputTree, fw)
​
​
if __name__ == '__main__':
    dataSet = [[0, 0, 0, 0, 'no'],  # 数据集
               [0, 0, 0, 1, 'no'],
               [0, 1, 0, 1, 'yes'],
               [0, 1, 1, 0, 'yes'],
               [0, 0, 0, 0, 'no'],
               [1, 0, 0, 0, 'no'],
               [1, 0, 0, 1, 'no'],
               [1, 1, 1, 1, 'yes'],
               [1, 0, 1, 2, 'yes'],
               [1, 0, 1, 2, 'yes'],
               [2, 0, 1, 2, 'yes'],
               [2, 0, 1, 1, 'yes'],
               [2, 1, 0, 1, 'yes'],
               [2, 1, 0, 2, 'yes'],
               [2, 0, 0, 0, 'no']]
    labels = ['年龄', '有工作', '有自己的房子', '信贷情况']
    featLabels = []
    mytree = createTree(dataSet, labels, featLabels)
    # print(mytree)
    # createPlot(mytree)
​
    testVec = [1, 0]  # 测试数据
    result = classify(mytree, featLabels, testVec)
    if result == 'yes':
        print('放贷')
    if result == 'no':
        print('不放贷')
​
    # myTree = {'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}}
    storeTree(mytree, 'classifierStorage.txt')

选择划分:

信息熵:

"信息熵" (information entropy) 是度量样本集合纯度最常用的一种指标. 假定当前样本集合中 第类样本所占的比例为 Pk (k = 1, 2,. . . , IYI) ,则 的信息熵定义为

信息熵公式:

$$
\operatorname{Ent}(D)=-\sum_{k=1}^{|\mathcal{Y}|} p_{k} \log _{2} p_{k}
$$

信息熵的代码实现:

# 数据中某个特征的信息熵
def entr(data):
    """
    	data:数据集
    	func:计算数据集中某个特征的信息熵
    """
    prob1 = pd.value_counts(data)/len(data) # 统计分类后的类别总数
    entropy = sum( (-1)*prob1*np.log2(prob1) ) # prob代表公式中的pk
    return entropy

信息增益:

信息增益:以某特征划分数据集前后的熵的差值。熵可以表示样本集合的不确定性,熵越大,样本的不确定性就越大。因此可以使用划分前后集合熵的差值来衡量使用当前特征对于样本集合D划分效果的好坏。信息增益 = entroy(前) - entroy(后) 注:信息增益表示得知特征X的信息而使得类Y的信息熵减少的程度

信息增益公式:

$$
\operatorname{Gain}(D, a)=\operatorname{Ent}(D)-\sum_{v=1}^{V} \frac{\left|D^{v}\right|}{|D|} \operatorname{Ent}\left(D^{v}\right)
$$

信息增益代码实现:

def conditional_entropy(data, str1, str2, entr):
    """
        data:数据集
        str1:数据集中的特征一
        str2:数据集中的特征二
        entr:信息熵函数
        func:计算信息增益
    """
    entr1 = data.groupby(str1).apply(lambda x:entr(x[str2]))
    prob2 = pd.value_counts(data[str1])/len(data[str1])  # 某个样本占总样本的比列
    con_ent = sum(entr1*prob2)

    #信息增益
    Information_gain_rate = entr(data[str2])-con_ent
    return Information_gain_rate

信息增益率:

信息增益率:增益率:增益率是用前面的信息增益Gain(D, a)和属性a对应的"固有值"(intrinsic value) [Quinlan , 1993J的比值来共同定义的。

信息增益率公式:

$$
\operatorname{Gain}_{-} \operatorname{ratio}(D, a)=\frac{\operatorname{Gain}(D, a)}{I V(a)}
$$

$$
I V(a)=-\sum_{v=1}^{V} \frac{D^{v}}{D} \log \frac{D^{v}}{D}
$$

信息增益的实现:

# 可以合并,但是为了看起来和公式对应,写成如下模式
def Gain_ratio(ce, str1, str2, data):
    """
    conditional_entropy:信息增益
    feature:数据集中的某个特征
    data:数据集    
    """
    import numpy as np
    prob2 = pd.value_counts(data[str1])/len(data[str1])
    IV = -1 * sum(prob2*np.log2(prob2))
    
    GR = ce / IV
    return GR
ce = conditional_entropy(data,'天气','打球')
gr = Gain_ratio(ce,'天气','打球', data)
gr

参考文献:(136条消息) Python3《机器学习实战》学习笔记(三):决策树实战篇之为自己配个隐形眼镜_Jack-Cui的博客-CSDN博客

基尼指数

定义:基尼指数(基尼不纯度):

表示在样本集合中一个随机选中的样本被分错的概率。Gini指数越小表示集合中被选中的样本被分错的概率越小,也就是说集合的纯度越高,反之,集合越不纯。 G 越大,数据的不确定性越高; G 越小,数据的不确定性越低; G = 0,数据集中的所有样本都是同一类别;

基尼指数公式:

数据集D的纯度可用基尼值来度量,两个都行

$$
\begin{aligned} \operatorname{Gini}(D) &=\sum_{k=1}^{|\mathcal{Y}|} \sum_{k^{\prime} \neq k} p_{k} p_{k^{\prime}} \\ &=1-\sum_{k=1}^{|\mathcal{Y}|} p_{k}^{2} \end{aligned}
$$

$$
G(p)=\sum_{k=1}^{K} p_{k}\left(1-p_{k}\right)=1-\sum_{k=1}^{K} p_{k}^{2}
$$

基尼指数的实现:

def cal_gini_index(data, col_num):
    """
    :Author: WangBlue
    :Create: 2022/8/15 10:02
    :func:计算基尼指数
    :param data: 数据集
    :param col_num: 数据集列数
    :return: 基尼指数
    """

    if len(data) == 0:
        return 0

    gini = []  # 存储所有列的基尼指数
    for i in range(col_num):
        label_counts = label_uniq_cnt(data, i)
        # 计算数据集的Gini指数
        GI = 0.0
        for label in label_counts:
            GI = GI + pow(label_counts[label], 2)
        GI = 1 - float(GI) / pow(len(data), 2)
        gini.append(GI)

    return gini


def label_uniq_cnt(data, colunms_index):
    """
    :Author: WangBlue
    :Create: 2022/8/15 10:04
    :func:样本中的标签的个数,统计的是每一种类各个取值的数量,
            把每一个种类的各种取值的数量放在字典中存储返回
    :param data: 数据集
    :param colunms_index:列的下标索引值
    :return:每一个种类的各种取值的数量放在字典中存储返回
    """

    label_uniq_cnts = {}
    # 遍历每一列
    for columns in data:
        label = columns[colunms_index]
        if label not in label_uniq_cnts:
            label_uniq_cnts[label] = 0
        label_uniq_cnts[label] += 1
    return label_uniq_cnts


if __name__ == '__main__':
    data = [('用', '有', '是'),
            ('用', '有', '是'),
            ('用', '无', '否'),
            ('不用', '有', '否'),
            ('不用', '有', '否')]
    gini = cal_gini_index(data, len(data[0]))
    print(gini)

参考文献:(136条消息) 决策树的预剪枝与后剪枝zfan520的博客-CSDN博客预剪枝和后剪枝

Sklearn决策树的实现及其案例:

from sklearn import tree 
from sklearn.model_selection import train_test_split

# 加载红酒数据集
from sklearn.datasets import load_wine
wine = load_wine()

# 探索数据集
wine.data.shape
wine.target  # 获取最后一列的数据

# 获取特征名称
wine.feature_names

# 目前分为了多数个类别
wine.target_names

# 划分数据集与训练集
Xtrain, Xtest, Ytrain, Ytest = train_test_split(wine.data, wine.target, test_size =0.3)

# 这里使用了决策树来建立模型
clf = tree.DecisionTreeClassifier(criterion="entropy"
                                 ,random_state=30
                                 ,splitter="random"
                                 ,max_depth=3     # 设置的最大层数为3
                                 ,min_samples_leaf=10 # 确保每个叶子节点的样本树不会少于10个样本
                                 # ,min_samples_split=10 # 保证分支的过程中,不会分支少于10的样本,
                                 )

clf = clf.fit(Xtrain, Ytrain)
score = clf.score(Xtest, Ytest) # 返回预测的准确度
score

Sklearn中DecisionTreeClassifier()参数的解释

参数说明
criterion{“gini”, “entropy”}, default=”gini” 这个参数是用来选择使用何种方法度量树的切分质量的。当criterion取值为“gini”时采用 基尼不纯度(Gini impurity)算法构造决策树,当criterion取值为 “entropy” 时采用信息增益( information gain)算法构造决策树.
splitter{“best”, “random”}, default=”best” 此参数决定了在每个节点上拆分策略的选择。支持的策略是“best” 选择“最佳拆分策略”, “random” 选择“最佳随机拆分策略”。
max_depthint, default=None 树的最大深度。如果取值为None,则将所有节点展开,直到所有的叶子都是纯净的或者直到所有叶子都包含少于min_samples_split个样本。
min_samples_splitint or float, default=2 拆分内部节点所需的最少样本数: · 如果取值 int , 则将min_samples_split视为最小值。 · 如果为float,则min_samples_split是一个分数,而ceil(min_samples_split * n_samples)是每个拆分的最小样本数。 -注释 在版本0.18中更改:增加了分数形式的浮点值。
min_samples_leafint or float, default=1 在叶节点处所需的最小样本数。 仅在任何深度的分裂点在左分支和右分支中的每个分支上至少留有min_samples_leaf个训练样本时,才考虑。 这可能具有平滑模型的效果,尤其是在回归中。 · 如果为int,则将min_samples_leaf视为最小值 · 如果为float,则min_samples_leaf是一个分数,而ceil(min_samples_leaf * n_samples)是每个节点的最小样本数。 - 注释: 在版本0.18中发生了更改:添加了分数形式的浮点值。
min_weight_fraction_leaffloat, default=0.0 在所有叶节点处(所有输入样本)的权重总和中的最小加权分数。 如果未提供sample_weight,则样本的权重相等。
max_featuresint, float or {“auto”, “sqrt”, “log2”}, default=None 寻找最佳分割时要考虑的特征数量: - 如果为int,则在每次拆分时考虑max_features功能。 - 如果为float,则max_features是一个分数,而int(max_features * n_features)是每个分割处的特征数量。 - 如果为“auto”,则max_features = sqrt(n_features)- 如果为“sqrt”,则max_features = sqrt(n_features)- 如果为“log2”,则max_features = log2(n_features)- 如果为None,则max_features = n_features。 注意:直到找到至少一个有效的节点样本分区,分割的搜索才会停止,即使它需要有效检查的特征数量多于max_features也是如此。
random_stateint, RandomState instance, default=None 此参数用来控制估计器的随机性。即使分割器设置为“最佳”,这些特征也总是在每个分割中随机排列。当max_features <n_features时,该算法将在每个拆分中随机选择max_features,然后再在其中找到最佳拆分。但是,即使max_features = n_features,找到的最佳分割也可能因不同的运行而有所不同。 就是这种情况,如果标准的改进对于几个拆分而言是相同的,并且必须随机选择一个拆分。 为了在拟合过程中获得确定性的行为,random_state必须固定为整数。 有关详细信息,请参见词汇表。
max_leaf_nodesint, default=None 优先以最佳方式生成带有max_leaf_nodes的树。 最佳节点定义为不纯度的相对减少。 如果为None,则叶节点数不受限制。
min_impurity_decreasefloat, default=0.0 如果节点分裂会导致不纯度的减少大于或等于该值,则该节点将被分裂。 加权不纯度减少方程如下: N_t / N * (impurity - N_t_R / N_t * right_impurity - N_t_L / N_t * left_impurity) 其中N是样本总数,N_t是当前节点上的样本数,N_t_L是左子节点中的样本数,N_t_R是右子节点中的样本数。 如果给sample_weight传了值,则N , N_t , N_t_RN_t_L均指加权总和。 在 0.19 版新增 。
min_impurity_splitfloat, default=0 树模型停止生长的阈值。如果节点的不纯度高于阈值,则该节点将分裂,否则为叶节点。 警告: 从版本0.19开始被弃用:min_impurity_split在0.19中被弃用,转而支持min_impurity_decreasemin_impurity_split的默认值在0.23中从1e-7更改为0,在0.25中将被删除。使用min_impurity_decrease代替。
class_weightdict, list of dict or “balanced”, default=None{class_label: weight}的形式表示与类别关联的权重。如果取值None,所有分类的权重为1。对于多输出问题,可以按照y的列的顺序提供一个字典列表。 注意多输出(包括多标签) ,应在其自己的字典中为每一列的每个类别定义权重。例如:对于四分类多标签问题, 权重应为[{0:1、1:1:1],{0:1、1:5},{0:1、1:1:1},{0:1、1: 1}],而不是[{1:1},{2:5},{3:1},{4:1}]。 “平衡”模式使用y的值自动将权重与输入数据中的类频率成反比地调整为n_samples /(n_classes * np.bincount(y))。 对于多输出,y的每一列的权重将相乘。 请注意,如果指定了sample_weight,则这些权重将与sample_weight(通过fit方法传递)相乘。
presortdeprecated, default=’deprecated’ 此参数已弃用,并将在v0.24中删除。 注意:从0.22版开始已弃用。
ccp_alphanon-negative float, default=0.0 用于最小化成本复杂性修剪的复杂性参数。 将选择成本复杂度最大且小于ccp_alpha的子树。 默认情况下,不执行修剪。 有关详细信息,请参见最小成本复杂性修剪。
属性说明
classes_ndarray of shape (n_classes,) or list of ndarray 类标签(单输出问题)或类标签数组的列表(多输出问题)。
feature_importances_ndarray of shape (n_features,) 返回特征重要程度数据。
max_features_int max_features 的推断值。
n_classes_int or list of int 整数的类别数(单输出问题),或者一个包含所有类别数量的列表(多输出问题)。
n_features_int 执行模型拟合训练时的特征数量。
n_outputs_int 执行模型拟合训练时的输出数量。
tree_Tree 基础的Tree对象。请通过 help(sklearn.tree._tree.Tree)查看Tree对象的属性,并了解决策树的结构以了解这些属性的基本用法。

# 将特征名称重命名并画树,这里重命名是为了方便我们观察
feature_name = ['酒精','苹果酸','灰','灰的碱性','镁','总酚','类黄酮','非黄烷类酚类','花青素',
                '颜色强度','色调','od280/od315稀释葡萄酒','脯氨酸']
import graphviz
dot_data = tree.export_graphviz(clf
                               ,out_file = None
                               ,feature_names= feature_name
                               ,class_names=["琴酒","雪莉","贝尔摩德"] # 酒的名称
                               ,filled=True  # 填充颜色
                               ,rounded=True
                               ,impurity=True # 显示节点上的不纯度
                               ,node_ids=True
                               )
graph = graphviz.Source(dot_data)
graph

export_graphviz参数的详解:

参数说明
decision_treedecision tree classifier 用来做可视化的决策树模型
out_filefile object or string, optional (default=None) 输出文件的句柄或名称。 如果为None,则结果以字符串形式返回 在0.20版本中更新:out_file的默认值从"tree.dot"更改为None
max_depthint, optional (default=None) 描绘的最大深度。如果为None,则这树完全生长。
feature_nameslist of strings, optional (default=None) 每个特征的名字
class_nameslist of strings, bool or None, optional (default=None) 每个目标类别的名称、按升序排列。 仅与分类相关,不支持多输出。 如果为True,则显示类名称的符号表示。
label{‘all’, ‘root’, ‘none’}, optional (default=’all’) 是否显示不纯度的信息性标签等。选项包括“ all”显示在每个节点上,“ root”显示在顶部根节点上,“ none”显示在任何节点上。
filledbool, optional (default=False) 设置为True时,绘制节点以表示多数类用于分类问题,值的极值用于回归问题,或表示节点的纯度用于多输出问题。
leaves_parallelbool, optional (default=False) 设置为True时,在树的底部绘制所有叶节点。
impuritybool, optional (default=True) 设置为True时,显示每个节点上的不纯度。
node_idsbool, optional (default=False) 设置为True时,显示每个节点上的ID号。
proportionbool, optional (default=False) 设置为True时,将“值”和/或“样本”的显示分别更改为比例和百分比。
rotatebool, optional (default=False) 设置为True时,将树从左到右而不是自上而下定向。
roundedbool, optional (default=False) 设置为True时,绘制带有圆角的节点框,并使用Helvetica字体代替Times-Roman
special_charactersbool, optional (default=False) 设置为False时,请忽略特殊字符以实现PostScript兼容性。
precisionint, optional (default=3) 每个节点的杂质值,阈值和值属性中浮点数的精度位数。
返回值说明
dot_datastring 树模型 GraphViz dot 格式的字符串表现形式。仅仅在out_file为None时返回

参考文献:

决策树python源码实现(含预剪枝和后剪枝)_王路ylu的博客-CSDN博客_决策树预剪枝代码

理论:(137条消息) 决策树的预剪枝与后剪枝zfan520的博客-CSDN博客预剪枝和后剪枝

西瓜书:周志华

机器学习实战

菜菜的机器学习

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值