机器学习案例——决策树

一、实验目的

了解决策树算法的基本思想、实现步骤,并用代码实现。

二、实验内容

根据“决策树训练数据集.xlsx”中的数据,完成以下实验。
(1) 利用信息增益作为选择分裂属性的标准,构建决策树。
(2) 以增益比率作为选择分裂属性的标准,构建决策树。
(3) 根据(1)中构建的决策树为35岁收入10万元的已婚男性预测所购车型。

三、实验代码

本次代码包含MyDecisionTree.py和treePlotter.py两个文件,其中treePlotter.py是使用matplotlib.pylot模块来完成绘制决策树的功能,而MyDecisionTree.py主要是完成以最大信息增益和最大信息增益比率两种方式分割数据集时构建决策树的过程,并调用treePlotter.py中的函数来绘制两棵决策树。
(1)treePlotter.py源代码

import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签SimHei
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号

# 使用文本注解绘制树节点
# 定义文本框和箭头格式
decisionNode = dict(boxstyle="sawtooth",fc="1.0")
leafNode = dict(boxstyle="round4",fc="0.8")
arrow_args = dict(arrowstyle="<-")

def plotNode(nodeTxt,conterPt,parentPt,nodeType):
    createPlot.ax1.annotate(nodeTxt,xy=parentPt,xycoords='axes fraction',\
                            xytext=conterPt,textcoords='axes fraction',\
                            va="center",ha="center",bbox=nodeType,arrowprops=arrow_args)
# 在父子节点间填充文本信息
def plotMidText(cntrPt,parentPt,txtString):
    xMid = (parentPt[0]-cntrPt[0]) / 2.0 + cntrPt[0]
    yMid = (parentPt[1]-cntrPt[1]) / 2.0 + cntrPt[1]
    createPlot.ax1.text(xMid, yMid, txtString)

def plotTree(myTree,parentPt,nodeTxt):
    numLeafs = getNumLeafs(myTree)
    depth = getTreeDepth(myTree)    # 计算宽高
    firstStr = list(myTree.keys())[0]
    cntrPt = (plotTree.xOff + (1.0 + float(numLeafs)) /2.0 /plotTree.totalW,plotTree.yOff)
    plotMidText(cntrPt,parentPt,nodeTxt)    # 标记子节点属性
    plotNode(firstStr,cntrPt,parentPt,decisionNode)
    secondDict = myTree[firstStr]
    plotTree.yOff = plotTree.yOff - 1.0 / plotTree.totalD   # 减少y偏移
    for key in secondDict.keys():
        if type(secondDict[key]).__name__ == 'dict':
            plotTree(secondDict[key],cntrPt,str(key))
        else:
            plotTree.xOff = plotTree.xOff + 1.0/plotTree.totalW
            plotNode(secondDict[key], (plotTree.xOff,plotTree.yOff),cntrPt,leafNode)
            plotMidText((plotTree.xOff,plotTree.yOff) ,cntrPt, str(key))
    plotTree.yOff = plotTree.yOff + 1.0 /plotTree.totalD

def createPlot(inTree):
    fig = plt.figure(1, facecolor="white")
    fig.clf()
    axprops = dict(xticks=[],yticks=[])
    createPlot.ax1 = plt.subplot(111,frameon=False, **axprops)
    plotTree.totalW = float(getNumLeafs(inTree))
    plotTree.totalD = float(getTreeDepth(inTree))
    plotTree.xOff = -0.5/plotTree.totalW
    plotTree.yOff = 1.0
    plotTree(inTree,(0.5,1.0),'')
    plt.show()

# 获取叶节点的数目和树的层数
def getNumLeafs(myTree):
    numLeafs = 0
    firstStr = list(myTree.keys())[0]
    secondDict = myTree[firstStr]
    for key in secondDict.keys():
        if type(secondDict[key]).__name__ == 'dict':
            numLeafs += getNumLeafs(secondDict[key])
        else:
            numLeafs += 1
    return numLeafs
def getTreeDepth(myTree):
    maxDepth = 0
    firstStr = list(myTree.keys())[0]
    secondDict = myTree[firstStr]
    for key in secondDict.keys():
        if type(secondDict[key]).__name__ == 'dict':
            thisDepth = 1 + getTreeDepth(secondDict[key])
        else:
            thisDepth = 1
        if thisDepth > maxDepth:
            maxDepth = thisDepth
    return maxDepth
# 输出预先存储的树的信息
def retrieveTree(i):
    listOfTrees = [{'no surfacing':{0:'no',1:{'flippers': \
                                                 {0:'no',1:'yes'}}}},
                   {'no surfacing':{0:'no',1:{'flippers': \
                                                  {0:{'head':{0:'no',1:'yes'}},1:'no'}}}}
                   ]
    return listOfTrees[i]

(2)MyDecisionTree.py源代码

import operator
from math import log

import numpy as np
from openpyxl import load_workbook

import treePlotter  # 导入自定义的模块,用于绘制决策树

"""
函数功能: 划分测试集和训练集
输入: 
dataset: 待划分的数据集
test_size: 测试集所占比例
输出:
train: 训练集
test: 测试集 
"""
def train_test_split(dataset,test_size):
    totalSize,columns = dataset.shape
    testSize = int(totalSize*test_size)
    trainSize = totalSize - testSize
    testIndex = random.sample(range(totalSize),testSize)
    train = np.zeros((trainSize,columns))
    test = np.zeros((testSize,columns))
    i = 0
    j = 0
    for index in range(totalSize):
        if index in testIndex:
            test[i,:] = dataset[index,:]
            i += 1
        else:
            train[j,:] = dataset[index,:]
            j += 1
    return train,test


"""
函数功能:读取fn中的数据
输入:
fn: 文件路径
输出:
dataSet 训练数据集
labels 属性名
"""


def createDataSet(fn):
    # 打开文件目录
    wb = load_workbook(fn)
    ws = wb.active
    rownum = ws.max_row  # 行数
    colnum = ws.max_column  # 列数
    dataSet = np.zeros((rownum - 1, colnum))  # 训练数据
    labels = []
    for index, row in enumerate(ws.rows):
        if index == 0:
            labels = np.array(['年龄', '性别', '年收入', '婚姻', '车型'])
        else:
            # 需要将数据进行编码,
            # 男:0  女:1
            # 单身:0 已婚:1 离异:2
            # 普通: 0 中档:1 高级:2
            row_data = []
            for idx, cell in enumerate(row):
                if idx == 0:
                    row_data.append(cell.value)
                elif idx == 1:
                    if cell.value == '男':
                        row_data.append(0)
                    else:
                        row_data.append(1)
                elif idx == 2:
                    row_data.append(cell.value)
                elif idx == 3:
                    if cell.value == '单身':
                        row_data.append(0)
                    elif cell.value == '已婚':
                        row_data.append(1)
                    else:
                        row_data.append(2)
                else:
                    if cell.value == '普通':
                        row_data.append(0)
                    elif cell.value == '中档':
                        row_data.append(1)
                    else:
                        row_data.append(2)
            dataSet[index - 1, :] = row_data
    labelFeature = [0, 1, 0, 1, 1]  # 属性类型 0表示连续型, 1表示离散型
    return dataSet, labels, labelFeature


"""
函数功能: 计算信息熵
输入: dataSet 数据集 最后一列为训练属性
"""


def calEnt(dataSet):
    numEntries = len(dataSet)
    labelCounts = {}
    for featVec in dataSet:
        currentLabel = featVec[-1]
        # 计算训练属性每一项的频率,保存至labelCounts中
        if currentLabel not in labelCounts.keys():
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1
    entropy = 0.0
    for key in labelCounts:
        prob = float(labelCounts[key]) / numEntries
        entropy -= prob * log(prob, 2)  # 计算每一项的信息熵后累计相减
    return entropy


"""
函数功能:按照给定的离散属性特征划分数据集
输入:
dataSet : 待划分的数据集
axis: 划分数据集的特征的下标
value: 需要返回的特征的值
labels: 特征属性标签向量
labelFeatures: 特征属性类型的向量
输出:
retDataSet: 根据划分特征筛选后的数据集    
"""


def splitDataSetForDiscrete(dataSet, axis, value):
    retDataSet = []
    for featVec in dataSet:
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis]
            reducedFeatVec = np.append(reducedFeatVec, featVec[axis + 1:])  # 将划分数据集的特征列抽取出来
            retDataSet.append(reducedFeatVec)
    return retDataSet


"""
函数功能:按照给定的离散属性特征划分数据集
输入:
dataSet : 待划分的数据集
axis: 划分数据集的特征的下标
value: 需要返回的特征的值
输出:
retDataSet: 根据划分特征筛选后的数据集    
"""


def splitDataSetForContinuous(dataSet, axis, value):
    retDataSet = []
    for featVec in dataSet:
        if featVec[axis] < value:
            reducedFeatVec = featVec[:axis]
            reducedFeatVec = np.append(reducedFeatVec, featVec[axis + 1:])  # 将划分数据集的特征列抽取出来
            retDataSet.append(reducedFeatVec)
    return retDataSet


"""
函数功能:按照信息增益最大的原则,选择出最优的数据集划分方式
输入: 
dataSet : 待划分的数据集
labelFeatures: 各个属性的类型向量 1表示离散型 0表示连续型
输出:
bestFeature: 最优划分的特征下标
"""


def chooseBestFeatureToSplit(dataSet, labelFeatures):
    numFeatures = len(dataSet[0]) - 1  # 用于分类的特征总数
    baseEntropy = calEnt(dataSet)  # 计算初始的信息熵
    bestInfoGain = 0.0  # 最大信息增益
    bestFeature = -1  # 获得最大信息增益的特征
    bestSep = -1  # 若最大信息增益为连续型属性,则bestSep记录的分割点的值
    print("baseEntropy:", baseEntropy)
    for i in range(numFeatures):
        # 当前属性为离散型时
        if labelFeatures[i] == 1:
            print(i, "当前属性为离散型")
            featList = [row[i] for row in dataSet]  # 取出当前特征对应的列值
            uniqueVals = set(featList)  # 取出当前特征所有可能取值
            newEntropy = 0.0  # 当前特征值来划分时得到的条件熵
            for value in uniqueVals:  # 根据该特征的取值来划分数据集,并计算条件熵
                subDataSet = splitDataSetForDiscrete(dataSet, i, value)
                prob = float(len(subDataSet)) / float(len(dataSet))  # 该特征取当前value的概率值
                newEntropy += prob * calEnt(subDataSet)  # 累加得到根据当前特征划分时的条件熵
            print("newEntropy:", newEntropy)
            infoGain = baseEntropy - newEntropy  # 计算当前特征划分时的信息增益
            if infoGain > bestInfoGain:  # 当前特征划分时的信息增益大于最大信息增益
                bestFeature = i
                bestInfoGain = infoGain
                print("bestFeature:", bestFeature, "bestInfoGain:", bestInfoGain)
        # 当前属性为连续型
        if labelFeatures[i] != 1:
            dataSet = np.array(dataSet)
            dataSet = dataSet[dataSet[:, i].argsort()]  # 将数据集按照当前连续属性进行排序
            featList = [row[-1] for row in dataSet]  # 取出当前数据集 分类属性的所有取值
            newEntropy = 0.0
            # 在类型发生变化的两个实体之间,划分数据集,计算条件熵
            for j in range(len(featList)):
                if j > 0:
                    if featList[j] != featList[j - 1]:
                        newEntropy = 0.0
                        print(j, j - 1, sep="###")
                        # 将数据集划分为两个部分,分别计算各自的信息熵后,相加得出条件熵
                        subDataSet1 = dataSet[:j, :]
                        subDataSet2 = dataSet[j:, :]
                        prob1 = float(len(subDataSet1)) / float(len(dataSet))
                        prob2 = float(len(subDataSet2)) / float(len(dataSet))
                        newEntropy += prob1 * calEnt(subDataSet1)
                        newEntropy += prob2 * calEnt(subDataSet2)
                        print("newEntropy:", newEntropy)
                        infoGain = baseEntropy - newEntropy  # 计算当前特征划分时的信息增益
                        if infoGain > bestInfoGain:  # 当前特征划分时的信息增益大于最大信息增益
                            bestFeature = i
                            bestInfoGain = infoGain
                            bestSep = dataSet[j, i]
                            print("bestFeature:", bestFeature, "bestInfoGain:", bestInfoGain, "bestSep", bestSep)
    # 判断最大信息增益的属性是否为连续型,返回最大信息增益的属性
    if labelFeatures[bestFeature] == 1:
        print("最大信息增益属性为离散型:", bestFeature)
        return bestFeature
    else:
        print("最大信息增益属性为连续型:", bestFeature, "   ", bestSep)
        return bestFeature, bestSep


"""
函数功能:统计出现次数最多的类别
输入:
classList : 属性列的向量
输出:
出现次数最多的属性
"""


def majorityCnt(classList):
    classCount = {}
    for item in classList:
        if item not in classCount.keys():
            classCount[item] = 0
        classCount[item] += 1
    sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
    return sortedClassCount[0][0]


"""
函数功能:根据最大信息增益来创建树
输入:
dataSet : 训练数据集
labels : 数据集各个属性标签
"""


def createTreeByGainMax(dataSet, labels, labelFeatures):
    print("labels:", labels)
    print("labelFeatures:", labelFeatures)
    classList = [row[-1] for row in dataSet]  # 将每一行数据的分类属性取出
    if classList.count(classList[0]) == len(classList):  # 当类别完全相同则停止继续划分
        return classList[0]
    if len(dataSet[0]) == 1:  # 每选择一个最优分裂属性递归一次,数据集都会减少一列数据
        # 因此遍历完所有特征时,数据集只剩下分类特征的一列,这时返回出现此时最多的类型
        return majorityCnt(classList)

    bestFeat = chooseBestFeatureToSplit(dataSet, labelFeatures)  # 选择最优分裂属性
    bestFeatLabel = ""
    if isinstance(bestFeat, tuple) == False:  # 当返回结果不是元组类型时,说明最优分裂属性是离散型
        bestFeatLabel = labels[bestFeat]
        myTree = {bestFeatLabel: {}}
        labels = np.delete(labels, bestFeat)  # 将最优分裂属性从标签中删除
        labelFeatures = np.delete(labelFeatures, bestFeat)
        featValues = [row[bestFeat] for row in dataSet]  # 最优分裂属性的所有取值
        uniqueVals = set(featValues)
        for value in uniqueVals:  # 根据最优分裂属性划分数据集后,在此节点上递归调用创建树
            subLabels = labels[:]
            subLabelFeatures = labelFeatures[:]
            myTree[bestFeatLabel][value] = createTreeByGainMax(splitDataSetForDiscrete(dataSet, bestFeat, value),
                                                               subLabels,
                                                               subLabelFeatures)
        return myTree
    else:  # 最优分裂属性为连续型时
        print("最优分裂属性:", labels[bestFeat[0]], "  最优分裂点取值:", bestFeat[1], sep=" ")
        bestFeatLabel = "{0}<{1}?".format(labels[bestFeat[0]], bestFeat[1])  # 最优分裂属性标签,拼接上最佳分裂点的取值
        bestSepValue = bestFeat[1]  # 最优分裂点的取值
        # 分为两部分 <bestSepValue  or >=bestSepValue
        dataSet = np.array(dataSet)
        dataSet = dataSet[dataSet[:, bestFeat[0]].argsort()]  # 将数据集按照最优分裂属性进行排序
        print("将数据集按照最优分裂属性进行排序:", dataSet, sep="\n")
        labels = np.array([list(labels)[i] if i != bestFeat[0] else bestFeatLabel for i in range(len(labels))])
        labelFeatures[bestFeat[0]] = 1  # 将此列的属性类型修改为离散型
        featRealValues = [row[bestFeat[0]] for row in dataSet]  # 最佳分裂属性这一列的真实取值
        # 将这一列的数据离散化,小于等于最佳分裂点的取为0,大于取1
        dataSet[:featRealValues.index(bestFeat[1]), bestFeat[0]] = [0 for i in range(featRealValues.index(bestFeat[1]))]
        dataSet[featRealValues.index(bestFeat[1]):, bestFeat[0]] = [1 for i in range(featRealValues.index(bestFeat[1]),
                                                                                     len(dataSet))]
        print("将连续型离散化后的dataSet:", dataSet, sep="\n")
        myTree = {bestFeatLabel: {}}
        # 递归调用
        featValues = [row[bestFeat[0]] for row in dataSet]  # 最优分裂属性的所有取值
        uniqueVals = set(featValues)
        for value in uniqueVals:  # 根据最优分裂属性划分数据集后,在此节点上递归调用创建树
            subLabels = labels[:]
            subLabelFeatures = labelFeatures[:]
            subLabels = np.append(subLabels[:bestFeat[0]], subLabels[bestFeat[0] + 1:])
            subLabelFeatures = np.append(subLabelFeatures[:bestFeat[0]], subLabelFeatures[bestFeat[0] + 1:])
            myTree[bestFeatLabel][value] = createTreeByGainMax(splitDataSetForDiscrete(dataSet, bestFeat[0], value),
                                                               subLabels,
                                                               subLabelFeatures)
        return myTree


"""
函数功能: 根据增益比率最大的原则,选出最优的数据集划分属性
"""


def chooseBestFeatureToSplitByGainRatio(dataSet, labelFeatures):
    numFeatures = len(dataSet[0]) - 1  # 用于分类的特征总数
    baseEntropy = calEnt(dataSet)  # 计算初始的信息熵
    bestInfoGainRatio = 0.0  # 最大信息增益
    bestFeature = -1  # 获得最大信息增益的特征
    bestSep = -1  # 若最大信息增益为连续型属性,则bestSep记录的分割点的值
    print("baseEntropy:", baseEntropy)
    for i in range(numFeatures):
        # 当前属性为离散型时
        if labelFeatures[i] == 1:
            print(i, "当前属性为离散型")
            featList = [row[i] for row in dataSet]  # 取出当前特征对应的列值
            uniqueVals = set(featList)  # 取出当前特征所有可能取值
            newEntropy = 0.0  # 当前特征值来划分时得到的条件熵
            for value in uniqueVals:  # 根据该特征的取值来划分数据集,并计算条件熵
                subDataSet = splitDataSetForDiscrete(dataSet, i, value)
                prob = float(len(subDataSet)) / float(len(dataSet))  # 该特征取当前value的概率值
                newEntropy += prob * calEnt(subDataSet)  # 累加得到根据当前特征划分时的条件熵
            print("newEntropy:", newEntropy)
            infoGain = baseEntropy - newEntropy  # 计算当前特征划分时的信息增益
            # 根据当前的特征属性计算信息熵,splitInfo(D,A)

            splitInfo = calEnt(np.reshape(np.array(featList), (len(featList), 1)))

            # 计算出信息增益比率
            infoGainRatio = infoGain / splitInfo
            if infoGainRatio > bestInfoGainRatio:  # 当前特征划分时的信息增益大于最大信息增益
                bestFeature = i
                bestInfoGainRatio = infoGainRatio
                print("bestFeature:", bestFeature, "bestInfoGainRatio:", bestInfoGainRatio)
        # 当前属性为连续型
        if labelFeatures[i] != 1:
            dataSet = np.array(dataSet)
            dataSet = dataSet[dataSet[:, i].argsort()]  # 将数据集按照当前连续属性进行排序
            featList = [row[-1] for row in dataSet]  # 取出当前数据集 分类属性的所有取值
            newEntropy = 0.0
            # 在类型发生变化的两个实体之间,划分数据集,计算条件熵
            for j in range(len(featList)):
                if j > 0:
                    if featList[j] != featList[j - 1]:
                        newEntropy = 0.0
                        print(j, j - 1, sep="###")
                        # 将数据集划分为两个部分,分别计算各自的信息熵后,相加得出条件熵
                        subDataSet1 = dataSet[:j, :]
                        subDataSet2 = dataSet[j:, :]
                        prob1 = float(len(subDataSet1)) / float(len(dataSet))
                        prob2 = float(len(subDataSet2)) / float(len(dataSet))
                        newEntropy += prob1 * calEnt(subDataSet1)
                        newEntropy += prob2 * calEnt(subDataSet2)
                        print("newEntropy:", newEntropy)
                        infoGain = baseEntropy - newEntropy  # 计算当前特征划分时的信息增益
                        splitInfo = -1 * prob1 * log(2, prob1) - prob2 * log(2, prob2)
                        # 计算信息增益比率
                        infoGainRatio = infoGain / splitInfo
                        if infoGainRatio > bestInfoGainRatio:  # 当前特征划分时的信息增益大于最大信息增益
                            bestFeature = i
                            bestInfoGainRatio = infoGainRatio
                            bestSep = dataSet[j, i]
                            print("bestFeature:", bestFeature, "bestInfoGainRatio:", bestInfoGainRatio, "bestSep",
                                  bestSep)
    # 判断最大信息增益的属性是否为连续型,返回最大信息增益的属性
    if labelFeatures[bestFeature] == 1:
        print("最大信息增益属性为离散型:", bestFeature)
        return bestFeature
    else:
        print("最大信息增益属性为连续型:", bestFeature, "   ", bestSep)
        return bestFeature, bestSep


"""
函数功能:根据最大信息增益比率来创建树
输入:
dataSet : 训练数据集
labels : 数据集各个属性标签
"""


def createTreeByGainRatioMax(dataSet, labels, labelFeatures):
    print("labels:", labels)
    print("labelFeatures:", labelFeatures)
    classList = [row[-1] for row in dataSet]  # 将每一行数据的分类属性取出
    if classList.count(classList[0]) == len(classList):  # 当类别完全相同则停止继续划分
        return classList[0]
    if len(dataSet[0]) == 1:  # 每选择一个最优分裂属性递归一次,数据集都会减少一列数据
        # 因此遍历完所有特征时,数据集只剩下分类特征的一列,这时返回出现此时最多的类型
        return majorityCnt(classList)

    bestFeat = chooseBestFeatureToSplitByGainRatio(dataSet, labelFeatures)  # 选择最优分裂属性
    bestFeatLabel = ""
    if isinstance(bestFeat, tuple) == False:  # 当返回结果不是元组类型时,说明最优分裂属性是离散型
        bestFeatLabel = labels[bestFeat]
        myTree = {bestFeatLabel: {}}
        labels = np.delete(labels, bestFeat)  # 将最优分裂属性从标签中删除
        labelFeatures = np.delete(labelFeatures, bestFeat)
        featValues = [row[bestFeat] for row in dataSet]  # 最优分裂属性的所有取值
        uniqueVals = set(featValues)
        for value in uniqueVals:  # 根据最优分裂属性划分数据集后,在此节点上递归调用创建树
            subLabels = labels[:]
            subLabelFeatures = labelFeatures[:]
            myTree[bestFeatLabel][value] = createTreeByGainMax(splitDataSetForDiscrete(dataSet, bestFeat, value),
                                                               subLabels,
                                                               subLabelFeatures)
        return myTree
    else:  # 最优分裂属性为连续型时
        print("最优分裂属性:", labels[bestFeat[0]], "  最优分裂点取值:", bestFeat[1], sep=" ")
        bestFeatLabel = "{0}<{1}?".format(labels[bestFeat[0]], bestFeat[1])  # 最优分裂属性标签,拼接上最佳分裂点的取值
        bestSepValue = bestFeat[1]  # 最优分裂点的取值
        # 分为两部分 <bestSepValue  or >=bestSepValue
        dataSet = np.array(dataSet)
        dataSet = dataSet[dataSet[:, bestFeat[0]].argsort()]  # 将数据集按照最优分裂属性进行排序
        print("将数据集按照最优分裂属性进行排序:", dataSet, sep="\n")
        labels = np.array([list(labels)[i] if i != bestFeat[0] else bestFeatLabel for i in range(len(labels))])
        labelFeatures[bestFeat[0]] = 1  # 将此列的属性类型修改为离散型
        featRealValues = [row[bestFeat[0]] for row in dataSet]  # 最佳分裂属性这一列的真实取值
        # 将这一列的数据离散化,小于等于最佳分裂点的取为0,大于取1
        dataSet[:featRealValues.index(bestFeat[1]), bestFeat[0]] = [0 for i in range(featRealValues.index(bestFeat[1]))]
        dataSet[featRealValues.index(bestFeat[1]):, bestFeat[0]] = [1 for i in range(featRealValues.index(bestFeat[1]),
                                                                                     len(dataSet))]
        print("将连续型离散化后的dataSet:", dataSet, sep="\n")
        myTree = {bestFeatLabel: {}}
        # 递归调用
        featValues = [row[bestFeat[0]] for row in dataSet]  # 最优分裂属性的所有取值
        uniqueVals = set(featValues)
        for value in uniqueVals:  # 根据最优分裂属性划分数据集后,在此节点上递归调用创建树
            subLabels = labels[:]
            subLabelFeatures = labelFeatures[:]
            subLabels = np.append(subLabels[:bestFeat[0]], subLabels[bestFeat[0] + 1:])
            subLabelFeatures = np.append(subLabelFeatures[:bestFeat[0]], subLabelFeatures[bestFeat[0] + 1:])
            myTree[bestFeatLabel][value] = createTreeByGainRatioMax(
                splitDataSetForDiscrete(dataSet, bestFeat[0], value), subLabels,
                subLabelFeatures)
        return myTree


if __name__ == '__main__':
    fn = "D:\\For_Study\\BusinessIntelligent\\实验2决策树分类\\决策树训练数据集.xlsx"
dataSet, labels, labelFeatures = createDataSet(fn)
train,test = train_test_split(dataSet,0.2)      # 划分训练集和测试集
print("测试集:",test,sep='\n')
print("训练集:",train,sep='\n')
mytree = createTreeByGainMax(train, labels, labelFeatures)  # 最大信息增益来训练模型
treePlotter.createPlot(mytree)
mytree2 = createTreeByGainRatioMax(train, labels, labelFeatures)  # 根据最大信息增益比率来创建树
treePlotter.createPlot(mytree2)

四、实验结果及相关说明:

(1)连续型数值的处理
决策树的构建中,我将年龄、年收入两列属性视为连续型属性,在车型分类发生变化的临界点处计算信息增益或信息增益率。
为了方便存储数据,若数据集的最优分裂属性为连续型,则在最佳分割点处将数据集二分,值为0的分支表示小于最佳分割点处的临界值,1表示大于或等于最佳分割处的临界值。
(2)数据编码
为了方便存储数据,处理数据之前对数据进行了编码
婚姻: 0 单身, 1 已婚, 2 离异
性别: 0 男, 1 女
车型: 0 普通, 1 中档 ,2 高级
(3)训练集和测试集的划分
一开始做实验时,由于看到数据量非常小,我并没有想到要划分测试集和训练集,而是将所有的数据带入模型训练,训练出来的决策树如下图所示,它对新的数据的预测能力非常差,无法预测“35岁年收入为10万的已婚男性”的所购车型:
在这里插入图片描述
后面划分了测试集和训练集后,明显改善了模型的预测能力,无论测试集比例为0.1或0.2时,它得出来的模型都至少能够预测“35岁年收入为10万的已婚男性”的所购车型,并且无论如何划分测试集与训练集,利用信息增益最大的原则得出的决策树对“35岁年收入为10万的已婚男性”的所购车型都预测为普通。
(4)实验结果
由于每次的抽样不同,训练集和测试集不同,所以得到的决策树也不同。
①测试集为0.2时,根据信息增益得到的预测能力比较好的几棵决策树如下:
在这里插入图片描述
上面这棵树在测试集上的准确率为0.5
重复训练了很多次,似乎在测试样本比例为0.2的条件下,最大的准确率只能到达50%
②测试集为0.2时,根据信息增益率得到的预测能力比较好的几棵决策树如下:
在这里插入图片描述
上面这棵决策树在测试集上的准确率达到了100%

五、利用Sklearn库来训练决策树

因为想知道自己编程实现的决策树,与调用Sklearn库中的方法构造的决策树,有什么不同,于是利用下面的代码:

import pandas as pd
import pydotplus
from sklearn import tree
from sklearn.model_selection import train_test_split



"""
函数功能:读取fn中的数据
输入:
fn: 文件路径
输出:
dataSet 训练数据集
labels 属性名
"""


def createDataSet(fn):
    # 打开文件目录
    dataset = pd.read_excel(fn)
    dataset['性别'], _ = pd.factorize(dataset['性别'])
    dataset['婚姻'], _ = pd.factorize(dataset['婚姻'])
    dataset['车型'], _ = pd.factorize(dataset['车型'])
    return dataset


if __name__ == '__main__':
    # 读取数据
    fn = "D:\\For_Study\\BusinessIntelligent\\实验2决策树分类\\决策树训练数据集.xlsx"
    dataset = createDataSet(fn)
    print(dataset)

    # 划分数据为训练集和测试集
    Xtrain, Xtest, Ytrain, Ytest = train_test_split(dataset.loc[:, :'婚姻'], dataset.loc[:, '车型'], test_size=0.2)

    # 建立模型
    clf = tree.DecisionTreeClassifier(criterion="entropy")
    clf = clf.fit(Xtrain, Ytrain)
    score = clf.score(Xtest, Ytest)
    print("准确度:",score)

    # 对数据进行可视化
    feature_name = ['age', 'gender', 'income', 'marriage']
    with open("car.dot",'w') as f:
        f = tree.export_graphviz(clf,
                                 out_file=f,
                                 feature_names=feature_name,
                                 class_names=['normal', 'high', 'medium'],
                                 filled=True,
                                 rounded=True)

然后安装graphviz工具,使用命令行将决策树的图形输出为pdf,其中一次运行结果如下:
在这里插入图片描述

六、实验小结

本次实验遇到的问题非常非常多,从各种库的安装开始,到决策树的构建、使用matplotlib绘制决策树,在自己一步一步debug的过程中,不仅熟悉了决策树生成的算法,也熟悉了python这门语言的引用机制、numpy、pandas这些库的使用方式。
本次实验的部分代码参考了《机器学习实战》这本书,在参考代码上也是做出了比较大的改动,但这本书上的代码仅仅只涉及到了离散型属性的选择,而没有考虑连续型属性,于是我在选择最优分裂属性、最优分裂点的时候将在排序后的数据集的分类变化的点上计算信息熵,选择好分裂点后,再将这列连续型数值离散化(二分),这样这一列属性也适用于离散型数值的处理。我认为这是我本次实验时最让我有成就感的地方吧。
然后是测试集和训练集的划分,一开始看着数据量非常小,觉得没有必要划分,但是当决策树构建好后,发现对一个新的样本却完全不能预测时,我才发现自己错了。于是又写了一个用来划分测试集和训练集的函数。
最后是对训练结果的观察,每一次不同的测试集、训练集划分方式得到的决策树都不太一样,预测能力也有很大差别。这也让我意识到了训练样本选择的重要性。哪怕是同一类模型,由于训练样本的不同,得出的模型预测能力也有着很大区别。要避免出现过拟合的现象。
在参考《机器学习实战》这本书的代码来手动写决策树的算法时,用的递归方法也让我印象深刻,如何去构造一棵树。这本书上的代码非常的巧妙,它在根据最优属性来划分数据集时,将最优属性这一列的值删掉,将属性标签向量这一列的值删除。最后递归结束的条件设置也非常巧妙——判断数据集是否只剩下属性值这一列or数据集的所有行的属性都为同一种。我觉得这是我在构思决策树代码时完全想不到的。而为了学习这一思想,我也尝试了numpy库中各种对array对象的删除、切片等操作。
最后尝试了sklearn库来构造决策树,这中间也非常曲折,首先遇到的报错就是文本字符串类型的数据没有编码处理,于是又去学习了pandas库中的数据编码操作。最后想利用graphviz库来可视化决策树,又遇到了一些错误,但也同时了解了graphviz这个小插件的使用。
总而言之,这次实验比较难、花费了很多时间,但是确实收获也很大。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值