欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-02-03 15:53:40
...

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

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

H=i=1np(xi)log2p(xi)

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

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