机器学习(二):决策树之ID3

时间:2023-02-12 23:40:08

文中的代码和数据集下载地址:
https://github.com/TimePickerWang/MachineLearningInAction

介绍决策树之前先介绍两个信息论里的概念:熵和信息增益。
1.熵:代表了信息的混乱程度。也就是说熵越高,混合的数据越多,越无序。熵的计算方式如下(其中 p ( x i ) 是样本为某一类别的概率。):

H = i = 1 n p ( x i ) l o g 2 p ( x i )

2.信息增益:信息增益可以理解为熵的减少或者说是数据无序度的减少。在划分数据时。为了使划分后数据的熵减少,我们需要选择信息增益最高的特征来划分数据。

决策树的创建过程描述如下,记为creatBranch()方法:

 检测数据中的每个样本是否属于同一类别
    如果是,返回类标签
    否则执行以下步骤:
        选择信息增益最高的特征
        划分数据集
        创建分支节点
            对于划分后的每个子集调用creatBranch()进行递归操作
        返回分支节点

下面直接上计算熵的代码:

from math import log

''' 计算香农熵 Input: data_set: 需要计算熵的m个样本,每个样本向量的最后一列是类别 Output:香农熵的值 '''
def calc_shannonEnt(data_set):
    m = len(data_set)
    lable_counts = {}
    for featVec in data_set:
        current_lable = featVec[-1]
        lable_counts[current_lable] = lable_counts.get(current_lable, 0) + 1
    shannon_ent = 0
    for key in lable_counts:
        prob = float(lable_counts[key])/m
        shannon_ent -= prob * log(prob, 2)
    return shannon_ent

为了方便计算熵,以上代码用一个字典记录了样本中数据的类别及其出现的次数。传入的数据是一个样本集,每个样本看成一个向量,样本向量的最后一列是该样本所属的类别。

ID3在每次划分数据时,会根据获得信息增益最大的特征来划分数据。并且在每次划分数据后,会删除该特征。然后把剩下的数据作为创建子树的数据重新进行划分,直到所有特征划分完毕。以下代码是通过某一特征来划分数据集,返回的是划分后的数据集:

''' 划分数据集 Input: data_set: 待划分的数据集 axis:划分数据集的特征 value:需要返回的特征的值 Output:划分后的数据集 '''
def split_dataSet(data_set, axis, value):
    ret_data_set = []
    for feat_vec in data_set:
        if feat_vec[axis] == value:
            reduced_feat_vec = feat_vec[:axis]
            reduced_feat_vec += feat_vec[axis+1:]
            ret_data_set.append(reduced_feat_vec)
    return ret_data_set

为了直观感受以下,假设有如下数据集调用了split_dataSet()方法:

data_set = [[0, 1, 0, 0, 'yes'],
            [1, 1, 0, 0, 'yes'],
            [0, 1, 1, 0, 'yes'],
            [0, 0, 1, 0, 'yes'],
            [1, 0, 0, 0, 'yes'],
            [0, 0, 1, 0, 'yes'],
            [0, 0, 0, 0, 'no'],
            [1, 1, 1, 1, 'no'],
            [0, 0, 0, 1, 'no'],
            [0, 1, 1, 1, 'no'],
            [0, 1, 0, 1, 'no']]

split_dataSet(data_set, 1, 0)

此时的返回如下:

机器学习(二):决策树之ID3

可以看出返回的是原数据集中第2个特征为0,并把第二个特征删除后的数据。

知道了熵的计算方式和划分数据的方式,接下来需要通过获得信息增益最高的特征来划分数据了。代码如下:

''' Input: data_set: 待划分的数据集 Output:划分结果最好(划分后信息增益最高)的特征索引 '''
def choose_best_feature_to_split(data_set):
    feature_num = len(data_set[0]) - 1  # 特征的个数
    m = len(data_set)  # 样本数
    base_entropy = calc_shannonEnt(data_set)  # 经验熵
    best_info_gain = 0.0  # 最好的信息增益值
    best_feature = -1  # 划分后信息增益最高的特征索引值
    for i in range(feature_num):
        # 数据集中所有第i个特征的值存到feat_list中
        feat_list = [example[i] for example in data_set]
        unique_feat = set(feat_list)
        new_entropy = 0.0  # 条件熵
        for feature in unique_feat:
            # 根据第i个特征划分数据
            sub_data_set = split_dataSet(data_set, i, feature)
            prob = len(sub_data_set)/m
            new_entropy += prob * calc_shannonEnt(sub_data_set)
        info_gain = base_entropy - new_entropy
        if info_gain > best_info_gain:
            best_info_gain = info_gain
            best_feature = i
    return best_feature

以上代码仅仅进行了一次数据的划分。首先计算了一下数据划分前的熵,然后遍历数据集的每一个特征,通过某一特征的所有可能取值调用split_dataSet()方法来划分数据,并计算划分数据后的熵之和。将划分数据前后的两个熵香减即是信息增益,用变量best_feature保存获得信息增益最大时划分特征的索引,在遍历完所有特征后返回,此时best_feature的值即是划分数据最好的方式。

要创建一颗决策树,仅仅进行一次划分是不够的,我们需要用完所有的特征。但如果我们用完所有的特征后,数据的类标签依然存在多个,这时我们需要结合kNN中多数表决的方式确定某一样本的类别。多数表决的方式如下:

''' Input: class_list: 分类名称的列表 Output:通过投票机制,返回出现次数最多的分类名称 '''
def majority_label(class_list):
    class_count = {}
    for vote in class_list:
        class_count[vote] = class_count.get(vote, 0) + 1
    sorted_class_count = sorted(class_count.items(), key=lambda item: item[1], reverse=True)
    return sorted_class_count[0][0]

上述方法传入的是一个类别向量(也就是数据集中最后一列值组成的向量),是一个行向量。对sorted()方法不了解的可以看我另一篇博客一:细说python3中sort和sorted。最后,我们就可以用之前的代码来创建决策树了,创建树的代码如下:

''' Input: data_set: 待划分的数据集 labels:标签列表 Output:表示决策树结构的字典 '''
def creat_tree(data_set, labels):
    class_list = [example[-1] for example in data_set]
    # 若所有的类标签完全相同,直接返回该类标签
    if class_list.count(class_list[0]) == len(class_list):
        return class_list[0]
    # 若使用完所有特征仍不能将数据划分成包含唯一类别的分组,则通过投票机制选出现次数最多的类别作为返回
    if len(data_set[0]) == 1:
        return majority_label(class_list)
    best_feature = choose_best_feature_to_split(data_set)
    best_feature_label = labels[best_feature]
    my_tree = {best_feature_label: {}}
    del labels[best_feature]
    feature_values = [example[best_feature] for example in data_set]
    unique_feature = set(feature_values)
    for value in unique_feature:
        sub_lables = labels[:]
        my_tree[best_feature_label][value] = creat_tree(
            split_dataSet(data_set, best_feature, value), sub_lables)
    return my_tree

以上代码中,用字典来保存树的所有信息。划分数据最好方式的特征索引用变量best_feature来保存,然后通过该特征的所有来划分数据,创建子树。该方法的放回有两种情况:1.最后的类标签。2.保存子树信息的字典。

好了,接下来通过一些简单的测试数据来试试该算法,测试数据如下:

def test_fun():
    data_set = [[0, 1, 0, 0, 'yes'],
                [1, 1, 0, 0, 'yes'],
                [0, 1, 1, 0, 'yes'],
                [0, 0, 1, 0, 'yes'],
                [1, 0, 0, 0, 'yes'],
                [0, 0, 1, 0, 'yes'],
                [0, 0, 0, 0, 'no'],
                [1, 1, 1, 1, 'no'],
                [0, 0, 0, 1, 'no'],
                [0, 1, 1, 1, 'no'],
                [0, 1, 0, 1, 'no']]
    labels = ["have care", "have house", "have money", "have wife"]
    tree = creat_tree(data_set, labels[:])
    print(tree)

输出的结果如下:
机器学习(二):决策树之ID3

该结果是一颗如下图所示的树:
机器学习(二):决策树之ID3

决策树的一大优点就是我们可以把树结构序列化之后保存起来,这样在分类数据时就不用每次都计算来建造树了,序列化和反序列化的代码如下:

import pickle

''' Input: input_tree: 表示决策树结构的字典 filename: 需要存储的文件名 '''
# 将字典类型的树结构序列化后存储在文件中
def store_tree(input_tree, filename = "./testTree.txt"):
    fw = open(filename, 'wb')  # 以二进制格式打开一个文件只用于写入。
    pickle.dump(input_tree, fw)
    fw.close()


''' Input: filename: 需要读取的文件名 Output: 决策树字典 '''
def grab_tree(filename = "./testTree.txt"):
    fr = open(filename, "rb")
    return pickle.load(fr)

需要注意的是,这里序列化和反序列化都需要用二进制格式打开文件,这里书上的代码有问题。这里文件的路径默认为当前路径下的testTree.txt文件。

当跟新数据进行分时的代码如下:

''' Input: input_tree: 表示决策树结构的字典 feat_labels: 标签列表 test_vec:待测试的向量 Output:分类结果 '''
def classify(input_tree, feat_labels, test_vec):
    first_fect = list(input_tree.keys())[0]
    second_dict = input_tree[first_fect]
    feat_index = feat_labels.index(first_fect)
    for key in second_dict.keys():
        if test_vec[feat_index] == key:
            if type(second_dict[key]).__name__ == 'dict':
                class_lable = classify(second_dict[key], feat_labels, test_vec)
            else:
                class_lable = second_dict[key]
    return class_lable

这也是一个递归的函数,需要判断某一结点是叶子结点函数子树。如果是子树,则该结点的类型为字典类型。

好了, 现在我们将以上测试数据先保存起来,然后通过反序列化,直接通过保存好的决策树树来根新样本分类。代码如下:

# 分类测试数据
def classify_test_data():
    tree = grab_tree()
    test_vec1 = [1, 0, 1, 0]
    test_vec2 = [0, 1, 0, 1]
    labels = ["has care", "has house", "has money", "has wife"]
    print("分类结果:" + classify(tree, labels, test_vec1))
    print("分类结果:" + classify(tree, labels, test_vec2))

classify_test_data()

最后结果如下:

机器学习(二):决策树之ID3

在书上还提到了将决策树的结构画出来,这里不做议论,书中还有一个预测隐形眼镜类型的例子,其数据集和代码我在开始已经放出来了,可以去下载,顺便点个星星支持一下,^_^。