自己动手写决策树(一)——初步搭建决策树框架

数挖实验课的时候,老师让我们自己动手写决策树,还不能调用 scikit-learn 包,顿时感觉有点难(可能是我比较菜),想到网上找一找看能不能学一下,可是许多大佬都是调包做的,剩下的大佬们写的代码也无法短时间去理解,于是我只好照着书本以及参考网上大佬们的思想来自己动手写一写。

实验题目是这样的,要求使用 ID3 算法在鸢尾花卉 Iris 数据集上训练出一个基本的决策树模型,拿到题目时我是一点思路都没有的,以前从来没有自己动手实现过机器学习的那些经典算法(倒是拿着西瓜书匆匆地翻过一遍,现在真的是后悔了),所以只能一步一步来了,接下来我讲一讲我的思路,或许可以给朋友们提供一些参照。

1 数据集的下载与处理

首先,在网上找到鸢尾花卉数据集,一般像这样经典的数据集网上都是公开的可以直接下载,这里给出数据集网站地址:http://archive.ics.uci.edu/ml/datasets/Iris,根据以下标示下载数据集。
在这里插入图片描述
在这里插入图片描述
下载好数据后,我们发现它是一个文本格式的文件,我们来观察一下这个数据集,这个文件里存储了鸢尾花卉的四个属性——花萼长度(sepal length)、花萼宽度(sepal width)、花瓣长度(petal length)和花瓣宽度(petal width),以及它们的三个类别(class)——山鸢尾(Iris Setosa),变色鸢尾(Iris Versicolor)和维吉尼亚鸢尾(Iris Virginica)。以我们看到的第一个样本为例,“5.1,3.5,1.4,0.2”分别代表样本的四个属性的取值,“Iris-setosa”代表当前样本的类别,该数据集共有 150 个这样的样本,且每类样本分别有 50 个数据:
在这里插入图片描述
一般文本格式的不太好处理,所以最好转成 CSV 格式的文件,然后用 pandas 库处理,较为直接的办法就是重命名为 CSV 格式的文件,然后使用 Excel 打开文件,给每一列添加一个列名,也可以通过代码折腾一下来实现(对了,Windows 系统上面跑的话路径需要注意改一下):

import numpy as np
import pandas as pd


def dataProcess(source_fpath, target_fpath):
    with open(source_fpath) as source_f:
        sample_list = []
        for line in source_f:
            content = line.strip().split(",")
            sample_list.append(np.array(content))
        csvdf = pd.DataFrame(sample_list)
        csvdf.columns = ["SepalLength", "SepalWidth", "PetalLength", "PetalWidth", "Class"]
        csvdf.to_csv(target_fpath, index=0) # 保存为 CSV 文件时不保留行索引


if __name__ == "__main__":
    source_fpath = "iris.data"
    target_fpath = source_fpath.replace("data", "csv")
    dataProcess(source_fpath, target_fpath)

使用 pandas 读取刚刚生成的 CSV 文件,以 DataFrame 形式打印出来:

dataset = pd.read_csv("iris.csv")
print(datset)

我这里是把五个列名分别命名为 SepalLength、SepalWidth、PetalLength、PetalWidth 以及 Class 的,大家可以根据自己的命名喜好修改一下:
在这里插入图片描述

2 随机划分数据集

现在,数据集准备好了,我们要开始划分数据集来为模型训练、预测以及评估做准备,一般是以 8:2 的比例随机划分出训练集和测试集(其实也可以使用交叉验证,不过我觉得交叉验证框架主要是用来选取超参数的,单纯地被用来评估模型我个人感觉有点浪费了,而且加上我也有点懒,所以就没有整上去了,感兴趣的朋友们可以试一下)。

一开始,我是像下面这样直接划分数据集的:

def datasetPartitioning(dataset):

    # 打乱索引
    index = [i for i in range(len(dataset))]
    np.random.seed(1) # 设置随机种子为 1
    np.random.shuffle(index)

    # 以 8:2 划分训练集和测试集
    traindatasetlen = int(len(dataset) * 0.8)
    traindataset = dataset.loc[index[:traindatasetlen]]
    testdataset = dataset.loc[index[traindatasetlen:]]
    traindataset = traindataset.reset_index() # 重置索引
    testdataset = testdataset.reset_index() # 重置索引

    return traindataset, testdataset

但是,在后面进行模型评估的时候发现模型精确度不是太高,比如说设置随机数种子为 1 时,精确度都不到 0.9,不过我把随机数种子修改为其他值时,比如说,随机数种子设置为 2 时,精确度会在 0.95 以上,我就在想为什么设置不同随机数种子的模型之间的差别会像这样子比较大,讲道理,差别肯定是有的,但一般不会相差太大的,出现我这样的情况肯定有一些原因,后来在调试当中我发现,有的随机数种子会导致数据集划分的不均匀:
在这里插入图片描述
我们可以看到随机数种子为 1 时,训练数据中不同类别的数量不如随机数种子为 2 时均匀,这也导致模型精确度的较大差别:
在这里插入图片描述
因此,不能单纯地直接划分数据集,需要做一些改变,在这里,我选择了类似分层抽样的方法,因为我是按 8:2 划分数据集,因此每个类别随机划分 40 个样本给训练集,10 个样本给测试集,这样子既达到了随机划分数据集的目的,又避免了数据集划分的不均匀:

# 将数据集划分为训练集和测试集
def subdatasetPartitioning(dataset):

    # 打乱索引
    index = [i for i in range(len(dataset))]
    np.random.seed(1)
    np.random.shuffle(index)

    # 以 8:2 划分训练集和测试集
    traindatasetlen = int(len(dataset) * 0.8)
    traindataset = dataset.loc[index[:traindatasetlen]]
    testdataset = dataset.loc[index[traindatasetlen:]]

    return traindataset, testdataset


# 类似于分层抽样,每个类别随机划分同样个数的样本给训练集
def datasetPartitioning(dataset):

    traindataset_list = []
    testdataset_list = []
    for i in range(3):
        subdataset = dataset.loc[i * 50 : (i + 1) * 50 - 1]
        subdataset = subdataset.reset_index() # 重置索引
        subtraindataset, subtestdataset = subdatasetPartitioning(subdataset)
        traindataset_list.append(subtraindataset)
        testdataset_list.append(subtestdataset)

    traindataset = pd.concat(traindataset_list, ignore_index=True) # 合并 DataFrame,ignore_index=True 表示忽略原索引,类似重置索引的效果
    testdataset = pd.concat(testdataset_list, ignore_index=True)

    return traindataset, testdataset

给大家看一看这样更改后的模型精确度,可以看到效果非常好,我这个展示的是随机数种子设为 1 的情况,其他的大家也可以试一下,我觉得结果不会像刚才那样差别巨大了:
在这里插入图片描述

3 计算信息熵

之前都是关于数据集处理的,到这里就要正式接触 ID3 算法了,关于 ID3 算法以及信息熵和信息增益的概念,我相信看到这篇博文的同学都基本学过,没学过网上也都有大量的教程可以去看看(这句话写的我自己都恶心了,相信朋友们看到也恶心,什么叫基本都学过,什么叫过网上都有大量的教程可以去看看,所以如果时间充裕的话,我之后会写一个关于决策树的博客,然后把链接放在这里,让大家看的舒心一点,不过有一点是真的,网上写的都是别人的,不管看书还是看教程只有自己学到的才是自己的)。

关于信息熵,大家主要记得一点就差不多可以了,就是如果一个数据集的信息熵越小,说明这个数据集的纯度越高,数据集中样本越有可能属于同一类别,在这里,先给出信息熵的公式:

E n t ( D ) = − ∑ k = 1 ∣ Y ∣ p k l o g 2 p k 其 中 , D   表 示 当 前 数 据 集 , p k   表 示 数 据 集   D   中 第   k   类 样 本 所 占 的 比 例 ( k = 1 , 2 , … , ∣ Y ∣ ) , ∣ Y ∣   是 类 别 数 Ent(D) = - \sum_{k=1}^{|Y|} p_k log_2 p_k \\ 其中,D ~ 表示当前数据集,p_k ~ 表示数据集 ~ D ~ 中第 ~ k ~ 类样本所占的比例(k = 1, 2, \ldots , |Y|),|Y| ~ 是类别数 Ent(D)=k=1Ypklog2pkD pk  D  k k=1,2,,YY 

然后,我们根据上面的公式写出计算信息熵的代码,给定数据集,返回其信息熵:

# 给定数据集,计算并返回其信息熵
def informationEntropy(dataset):
    entropysum = 0
    category_list = list(dataset["Class"])
    for category in set(dataset["Class"]):
        pk = category_list.count(category) / len(dataset)
        entropysum += pk * math.log(pk, 2)
    return (-1) * entropysum

4 计算划分属性的信息增益

我们已经知道,数据集的纯度和信息熵息息相关,而 ID3 算法就是基于信息熵,该算法想要达到一个目的,就是将数据集划分的越来越纯,让同一类别的样本尽可能在一起,以达到分类的目的,在决策树当中就是让子结点的信息熵加起来要小于父节点的信息熵,信息熵的减少量就是信息增益,ID3 算法就是要让信息增益越大越好,这里给出属性离散情况下的信息增益公式:

G a i n ( D , a ) = E n t ( D ) − ∑ v = 1 ∣ V ∣ ∣ D v ∣ ∣ D ∣ E n t ( D v ) 其 中 , D   为 当 前 数 据 集 , a   为 离 散 属 性 , 有   V   个 可 能 的 取 值   { a 1 , a 2 , … , a V } , 而   D v   即 是 数 据 集   D   中 在 属 性   a   上 取 值 为   a v   的 样 本 , 对 应 划 分 出 的 第   V   个 分 支 节 点 Gain(D , a) = Ent(D) - \sum_{v=1}^{|V|} \frac {|D^v|} {|D|} Ent(D^v) \\ 其中,D~为当前数据集,a ~为离散属性,有~V~个可能的取值~\{a^1,a^2,\ldots,a^V\},\\ 而~D^v ~ 即是数据集~D~中在属性~a~上取值为~a^v~的样本,对应划分出的第~V~个分支节点 Gain(D,a)=Ent(D)v=1VDDvEnt(Dv)D a  V  {a1,a2,,aV} Dv  D  a  av  V 

同一个数据集 D,划分属性 a 不同,对应的信息增益也不一样,我们要做的就是找出信息增益最大的划分属性,然后根据它划分数据集,生成分支节点,这将在下一步进行分析,现在根据上面的公式编写代码,给定数据集和离散类型属性,返回根据该属性划分数据集得到的信息增益:

# 给定数据集和离散类型属性,计算并返回根据该属性划分数据集得到的信息增益
def informationDiscreteGain(dataset, attribute):
    entropy = informationEntropy(dataset)
    entropysum = 0
    attribute_value_list = list(dataset[attribute])
    for attribute_value in set(dataset[attribute]):
        weight = attribute_value_list.count(attribute_value) / len(dataset)
        entropysum += weight * informationEntropy(dataset[dataset[attribute] == attribute_value])
    return entropy - entropysum

如果数据集中的属性是离散的,那么这一步基本就做完了,但是我们现在的数据集是鸢尾花卉数据集,它的数据的四个属性均是连续属性,所以我们要做不同的处理。

由于连续属性的可取值数目不再有限,因此,不能直接根据连续属性的可取值来对结点进行划分,此时,我们需要使用连续属性离散化技术,最简单的策略是采用二分法(bi-partition)对连续属性进行处理。

那么二分法是如何操作的呢?二分法采用的是基于划分点的方法,通过选取划分点 t 将数据集 D 分为子集 Dt 和 Dt+,其中 Dt 包含那些在属性 a 上取值不大于 t 的样本,而 Dt+ 则包含那些在属性 a 上取值大于 t 的样本。

那么如何选取划分点 t 呢?给点数据集 D 和连续属性 a,假定 a 在 D 上出现了 n 个不同的取值,将这些值从小到大进行排序,记为 { a1, a2, …, an },我们准备一个包含 n - 1 个元素的候选划分点集合:

T a = { a i + a i + 1 2 ∣ 1 ⩽ i ⩽ n − 1 } T_a = \{ \frac {a^i + a^{i+1}} 2 | 1 \leqslant i \leqslant n - 1 \} Ta={2ai+ai+11in1}

也就是说,把每对相邻属性取值 ai 与 ai+1 的均值即区间 [ai, ai+1) 的中位点作为候选划分点,然后,我们就可以像考察离散值一样来考察这些划分点,选取最优的划分点来进行数据集的划分,接着,我们需要对信息增益公式改造一下,让其能处理连续属性:

G a i n ( D , a ) = max ⁡ t ∈ T a G a i n ( D , a , t ) = max ⁡ t ∈ T a { E n t ( D ) − ∑ λ ∈ { − , + } ∣ D t λ ∣ ∣ D ∣ E n t ( D t λ ) } 其 中 , G a i n ( D , a , t ) 是 数 据 集   D   基 于 划 分 点   t   二 分 后 的 信 息 增 益 Gain(D, a) = \max_{t \in T_a} Gain(D, a, t) = \max_{t \in T_a} \{ Ent(D) - \sum_{\lambda \in \{ -, + \} } \frac {| D_t^{\lambda} |} {| D |} Ent(D_t^{\lambda}) \} \\ 其中,Gain(D, a, t) 是数据集 ~D~ 基于划分点 ~t~ 二分后的信息增益 Gain(D,a)=tTamaxGain(D,a,t)=tTamax{Ent(D)λ{,+}DDtλEnt(Dtλ)}Gain(D,a,t) D  t 

我们认为使 Gain(D, a, t) 达到最大的划分点即为最优划分点,好了,既然思路比较清晰了,那就继续开始编写代码,给定数据集和连续类型属性,返回根据该属性划分数据集得到的信息增益以及该属性上的最优划分点:

# 给定数据集和连续类型属性,计算根据该属性划分数据集得到的信息增益,并返回在该属性上的划分点以及信息增益
def informationContinuousGain(dataset, attribute):
    entropy = informationEntropy(dataset)
    attribute_value_list = sorted(set(dataset[attribute]))
    if len(attribute_value_list) == 1:
        thresholds = [attribute_value_list[0]]
    else:
        thresholds = [(attribute_value_list[i] + attribute_value_list[i + 1]) / 2 for i in range(len(attribute_value_list) - 1)] # 候选划分点集合
    
    threshold_entropysum_dict = {}
    for threshold in thresholds:
        lessthreshold = dataset[dataset[attribute] <= threshold]
        morethreshold = dataset[dataset[attribute] > threshold]
        lessweight = len(lessthreshold) / len(dataset)
        moreweight = len(morethreshold) / len(dataset)
        entropysum = lessweight * informationEntropy(lessthreshold) + moreweight * informationEntropy(morethreshold)
        threshold_entropysum_dict[threshold] = entropysum
        
    threshold_entropysum_sorted = sorted(threshold_entropysum_dict.items(), key=lambda item: item[1])
    minentropysum_threshold = threshold_entropysum_sorted[0][0]
    minentropysum = threshold_entropysum_sorted[0][1]
    return minentropysum_threshold, entropy - minentropysum

5 递归生成决策树

信息熵和信息增益模块都已经分别处理好了,现在到了最重要的一环了,我们要开始根据训练集学习并构造决策树,决策树学习的目的是为了产生一棵泛化能力强,即处理未见示例能力强的决策树,其基本流程遵循简单且直观的分而治之(divide-and-conquer)策略,在这里给出西瓜书上的一张图:
在这里插入图片描述
这张图我觉得脉络非常清晰,我当时学习的时候还根据这张图做了一张流程图,这里也给大家展示一下(我的这张图曾经被吐槽画得太 low 了,还是用 WPS 画得,我的同学们都觉得太捞了这张图,大家看到不要笑蛤):
在这里插入图片描述
现在,决策树构造流程有了,那应该构造成什么样呢,如果真的用数据结构弄出树形结构,我觉得有点麻烦,所以我采取了一个大佬的博客上的方法,构建一个决策树字典,这样即方便又直观,这里给出大佬博客地址:https://www.cnblogs.com/further-further-further/p/9429257.html,大家可以看一下,我觉得写得非常好。

准备工作已经做好了,现在开始编写代码,根据数据集和属性列表递归生成决策树结点,返回决策树模型字典:

# 计算数据集中类数量最多的类
def maxNumOutcome(dataset):
    category_list = list(dataset["Class"])
    category_dict = {}
    for category in set(dataset["Class"]):
        category_dict[category] = category_list.count(category)
    category_sorted = sorted(category_dict.items(), key=lambda item: item[1], reverse=True)
    return category_sorted[0][0]


# 递归生成决策树结点,返回决策树模型字典
def treeNodeGenerate(dataset, attribute_list):
    if len(set(dataset["Class"])) == 1:
        node = list(set(dataset["Class"]))[0]
    elif len(attribute_list) == 0 or sum([len(set(dataset[attribute])) - 1 for attribute in attribute_list]) == 0:
        node = maxNumOutcome(dataset)
    else:
        attribute_gain_dict = {}
        for attribute in attribute_list:
            threshold, attribute_gain = informationContinuousGain(dataset, attribute)
            attribute_gain_dict[attribute] = threshold, attribute_gain
        attribute_gain_sorted = sorted(attribute_gain_dict.items(), key=lambda item: item[1][1], reverse=True)
        maxgain_attribute = attribute_gain_sorted[0][0]
        maxgain_threshold = attribute_gain_sorted[0][1][0]

        son_node_attribute_list = attribute_list.copy()
        son_node_attribute_list.remove(maxgain_attribute)

        left_node_dataset = dataset[dataset[maxgain_attribute] <= maxgain_threshold]
        if len(left_node_dataset) == 0:
            leftnode = maxNumOutcome(dataset)
        else:
            leftnode = treeNodeGenerate(left_node_dataset, son_node_attribute_list)
        
        right_node_dataset = dataset[dataset[maxgain_attribute] > maxgain_threshold]
        if len(right_node_dataset) == 0:
            rightnode = maxNumOutcome(dataset)
        else:
            rightnode = treeNodeGenerate(right_node_dataset, son_node_attribute_list)
        
        if leftnode == rightnode:
            node = leftnode
        else:
            node = {}
            node[(maxgain_attribute, maxgain_threshold)] = {"<=":leftnode, ">":rightnode}

    return node

这是我随机数种子设为 1 时在训练集上训练出来的决策树模型,大家要注意,不同随机数种子划分出的训练集上构造的决策树会有一定程度不同:
在这里插入图片描述

6 模型的预测与评估

决策树模型训练好了,到了最后一个版块了,我们要使用训练好的决策树模型去预测测试集上的样本,并对预测结果进行评估,这两个模块代码都比较好写,前者是通过递归得出样本预测结果,后者我只写了一个简单的精确度评估指标:

# 预测一条数据的结果
def predictOne(tree_train_model, testdata):
    if type(tree_train_model) == str:
        predict_value = tree_train_model
    elif type(tree_train_model) == dict:
        key = list(tree_train_model)[0]
        if testdata[key[0]] <= key[1]:
            son_tree_train_model = tree_train_model[key]["<="]
        else:
            son_tree_train_model = tree_train_model[key][">"]
        predict_value = predictOne(son_tree_train_model, testdata)
    return predict_value


# 进行模型预测,返回预测结果列表
def predict(tree_train_model, testdataset):
    predict_list = []
    for i in range(len(testdataset)):
        predict_value = predictOne(tree_train_model, testdataset.loc[i])
        predict_list.append((testdataset.loc[i]["Class"], predict_value))
    return predict_list

这是我的预测结果列表,大家参照着看一下就好:
在这里插入图片描述
然后对预测模型进行精确度评估:

# 对预测模型进行精确度评估
def predictAccuracy(predict_list):
    predict_true_num = 0
    for bigram in predict_list:
        if bigram[0] == bigram[1]:
            predict_true_num += 1
    accuracy = predict_true_num / len(predict_list)
    return accuracy

得出模型评估结果,这个大家刚才也已经看到了,不可思议的 1.0:
在这里插入图片描述

7 组合模块形成完整代码

最后,将前面的所有模块进行组合,并添加 main 函数,得到完整代码:

import pandas as pd
import math, sys, os
import numpy as np


# 给定数据集,计算并返回其信息熵
def informationEntropy(dataset):
    entropysum = 0
    category_list = list(dataset["Class"])
    for category in set(dataset["Class"]):
        pk = category_list.count(category) / len(dataset)
        entropysum += pk * math.log(pk, 2)
    return (-1) * entropysum


# 给定数据集和离散类型属性,计算并返回根据该属性划分数据集得到的信息增益
def informationDiscreteGain(dataset, attribute):
    entropy = informationEntropy(dataset)
    entropysum = 0
    attribute_value_list = list(dataset[attribute])
    for attribute_value in set(dataset[attribute]):
        weight = attribute_value_list.count(attribute_value) / len(dataset)
        entropysum += weight * informationEntropy(dataset[dataset[attribute] == attribute_value])
    return entropy - entropysum


# 给定数据集和连续类型属性,计算根据该属性划分数据集得到的信息增益,并返回在该属性上的划分点以及信息增益
def informationContinuousGain(dataset, attribute):
    entropy = informationEntropy(dataset)
    attribute_value_list = sorted(set(dataset[attribute]))
    if len(attribute_value_list) == 1:
        thresholds = [attribute_value_list[0]]
    else:
        thresholds = [(attribute_value_list[i] + attribute_value_list[i + 1]) / 2 for i in range(len(attribute_value_list) - 1)] # 候选划分点集合
    
    threshold_entropysum_dict = {}
    for threshold in thresholds:
        lessthreshold = dataset[dataset[attribute] <= threshold]
        morethreshold = dataset[dataset[attribute] > threshold]
        lessweight = len(lessthreshold) / len(dataset)
        moreweight = len(morethreshold) / len(dataset)
        entropysum = lessweight * informationEntropy(lessthreshold) + moreweight * informationEntropy(morethreshold)
        threshold_entropysum_dict[threshold] = entropysum
        
    threshold_entropysum_sorted = sorted(threshold_entropysum_dict.items(), key=lambda item: item[1])
    minentropysum_threshold = threshold_entropysum_sorted[0][0]
    minentropysum = threshold_entropysum_sorted[0][1]
    return minentropysum_threshold, entropy - minentropysum


# 计算数据集中类数量最多的类
def maxNumOutcome(dataset):
    category_list = list(dataset["Class"])
    category_dict = {}
    for category in set(dataset["Class"]):
        category_dict[category] = category_list.count(category)
    category_sorted = sorted(category_dict.items(), key=lambda item: item[1], reverse=True)
    return category_sorted[0][0]


# 递归生成决策树结点,返回决策树模型字典
def treeNodeGenerate(dataset, attribute_list):
    if len(set(dataset["Class"])) == 1:
        node = list(set(dataset["Class"]))[0]
    elif len(attribute_list) == 0 or sum([len(set(dataset[attribute])) - 1 for attribute in attribute_list]) == 0:
        node = maxNumOutcome(dataset)
    else:
        attribute_gain_dict = {}
        for attribute in attribute_list:
            threshold, attribute_gain = informationContinuousGain(dataset, attribute)
            attribute_gain_dict[attribute] = threshold, attribute_gain
        attribute_gain_sorted = sorted(attribute_gain_dict.items(), key=lambda item: item[1][1], reverse=True)
        maxgain_attribute = attribute_gain_sorted[0][0]
        maxgain_threshold = attribute_gain_sorted[0][1][0]

        son_node_attribute_list = attribute_list.copy()
        son_node_attribute_list.remove(maxgain_attribute)

        left_node_dataset = dataset[dataset[maxgain_attribute] <= maxgain_threshold]
        if len(left_node_dataset) == 0:
            leftnode = maxNumOutcome(dataset)
        else:
            leftnode = treeNodeGenerate(left_node_dataset, son_node_attribute_list)
        
        right_node_dataset = dataset[dataset[maxgain_attribute] > maxgain_threshold]
        if len(right_node_dataset) == 0:
            rightnode = maxNumOutcome(dataset)
        else:
            rightnode = treeNodeGenerate(right_node_dataset, son_node_attribute_list)
        
        if leftnode == rightnode:
            node = leftnode
        else:
            node = {}
            node[(maxgain_attribute, maxgain_threshold)] = {"<=":leftnode, ">":rightnode}

    return node


# 预测一条数据的结果
def predictOne(tree_train_model, testdata):
    if type(tree_train_model) == str:
        predict_value = tree_train_model
    elif type(tree_train_model) == dict:
        key = list(tree_train_model)[0]
        if testdata[key[0]] <= key[1]:
            son_tree_train_model = tree_train_model[key]["<="]
        else:
            son_tree_train_model = tree_train_model[key][">"]
        predict_value = predictOne(son_tree_train_model, testdata)
    return predict_value


# 进行模型预测,返回预测结果列表
def predict(tree_train_model, testdataset):
    predict_list = []
    for i in range(len(testdataset)):
        predict_value = predictOne(tree_train_model, testdataset.loc[i])
        predict_list.append((testdataset.loc[i]["Class"], predict_value))
    return predict_list


# 对预测模型进行精确度评估
def predictAccuracy(predict_list):
    predict_true_num = 0
    for bigram in predict_list:
        if bigram[0] == bigram[1]:
            predict_true_num += 1
    accuracy = predict_true_num / len(predict_list)
    return accuracy


# 将数据集划分为训练集和测试集
def subdatasetPartitioning(dataset):

    # 打乱索引
    index = [i for i in range(len(dataset))]
    np.random.seed(1)
    np.random.shuffle(index)

    # 以 8:2 划分训练集和测试集
    traindatasetlen = int(len(dataset) * 0.8)
    traindataset = dataset.loc[index[:traindatasetlen]]
    testdataset = dataset.loc[index[traindatasetlen:]]

    return traindataset, testdataset


# 类似于分层抽样,每个类别划分同样个数的样本给训练集
def datasetPartitioning(dataset):

    traindataset_list = []
    testdataset_list = []
    for i in range(3):
        subdataset = dataset.loc[i * 50 : (i + 1) * 50 - 1]
        subdataset = subdataset.reset_index() # 重置索引
        subtraindataset, subtestdataset = subdatasetPartitioning(subdataset)
        traindataset_list.append(subtraindataset)
        testdataset_list.append(subtestdataset)

    traindataset = pd.concat(traindataset_list, ignore_index=True) # ignore_index=True 表示忽略原索引,类似重置索引的效果
    testdataset = pd.concat(testdataset_list, ignore_index=True)

    return traindataset, testdataset


if __name__ == "__main__":

    # 读取数据
    dataset = pd.read_csv("iris.csv")

    # 将数据集以 8:2 划分为训练集和测试集(每一类别抽 40 个作训练集)
    traindataset, testdataset =  datasetPartitioning(dataset)

    # 使用训练集进行模型训练
    attribute_list = ["SepalLength", "SepalWidth", "PetalLength", "PetalWidth"]
    tree_train_model = treeNodeGenerate(traindataset, attribute_list)
    print("The Dict of Trained Model:")
    print(tree_train_model, "\n")

    # 使用训练好的模型在测试集上进行测试得到预测结果
    predict_list = predict(tree_train_model, testdataset)
    print("The List of Predicting Outcomes (Actual Label, Predicted Value) :")
    print(predict_list, "\n")

    # 对预测结果进行评估
    print("The Accuracy of Model Prediction: ", predictAccuracy(predict_list))

编写完成后,运行得到最终结果:
在这里插入图片描述
好了本文到此就快结束了,以上就是我自己动手写决策树的想法和思路,大家参照一下即可,重要的还是经过自己的思考来编写代码,文章中还有很多不足和不正确的地方,欢迎大家指正(也请大家体谅,写一篇博客真的挺累的,花的时间比我写出代码的时间还要长),我会尽快修改,之后我也会尽量完善本文,尽量写得通俗易懂,毕竟如果我写得大家看不懂那我还不如不写,回家种田算了。

博文创作不易,转载请注明本文地址:https://blog.csdn.net/qq_44009891/article/details/105787894

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值