机器学习之决策树

目录

  • 决策树概念
  • ID3算法
    • 思想
    • 划分标准
    • 缺点
  • 确定分类指标
  • 代码实现
    • 导入必要库
    • 创建数据集
    • 熵计算
    • 计算经验条件熵
    • 计算信息增益
    • 确定根节点特征
    • 定义节点类
    • 定义决策树类
    • 决策树的生成与预测
      • 打印出树结构
  • 总结

决策树概念

决策树是一种机器学习的方法。决策树的生成算法有ID3, C4.5和CART等。决策树是一种树形结构,其中每个内部节点表示一个属性上的判断,每个分支代表一个判断结果的输出,最后每个叶节点代表一种分类结果。

ID3算法

由于本次实验仅采用ID3算法构建决策树,因此仅介绍该算法。

思想

  • 从信息论的知识中我们知道:信息熵越大,从而样本纯度越低,。ID3 算法的核心思想就是以信息增益来度量特征选择,选择信息增益最大的特征进行分裂。算法采用自顶向下的贪婪搜索遍历可能的决策树空间(C4.5 也是贪婪搜索)。 其大致步骤为:
  • (1)初始化特征集合和数据集合;
  • (2)计算数据集合信息熵和所有特征的条件熵,选择信息增益最大的特征作为当前决策节点;
  • (3)更新数据集合和特征集合(删除上一步使用的特征,并按照特征值来划分不同分支的数据集合);
  • (4)重复 2,3 两步,若子集值包含单一特征,则为分支叶子节点。

划分标准

  • ID3 使用的分类标准是信息增益,它表示得知特征 A 的信息而使得样本集合不确定性减少的程度。数据集的信息熵公式如下:
    H ( D ) = − ∑ k = 1 K ∣ C k ∣ ∣ D ∣ l o g 2 ∣ C k ∣ ∣ D ∣ H(D)=-\sum_{k=1}^{K}\frac{\left | C_k \right | }{\left | D \right | } log_2\frac{\left | C_k \right | }{\left | D \right | } H(D)=k=1KDCklog2DCk

  • 其中 C k C_k Ck表示集合 D 中属于第 k 类样本的样本子集。

  • 针对某个特征 A,对于数据集 D 的条件熵 H(D|A) 为:
    H ( D ∣ A ) = ∑ i = 1 n ∣ D i ∣ ∣ D ∣ H ( D i ) = − ∑ i = 1 n ∣ D i ∣ ∣ D ∣ ( ∑ k = 1 K ∣ D i k ∣ ∣ D i ∣ l o g 2 ∣ D i k ∣ ∣ D i ∣ ) H(D|A)=\sum_{i=1}^{n}\frac{\left | D_i \right | }{\left | D \right | }H(D_i) =- \sum_{i=1}^{n}\frac{\left | D_i \right | }{\left | D \right | }(\sum_{k=1}^{K}\frac{\left | D_{ik} \right | }{\left | D_i \right | }log_2\frac{\left | D_{ik} \right | }{\left | D_i \right | }) H(DA)=i=1nDDiH(Di)=i=1nDDi(k=1KDiDiklog2DiDik)

  • 其中 D i D_i Di表示 D 中特征 A 取第 i 个值的样本子集, D i k D_{ik} Dik 表示 D i D_i Di中属于第 k 类的样本子集。

  • 信息增益 = 信息熵 - 条件熵:
    G a i n ( D , A ) = H ( D ) − H ( D ∣ A ) Gain(D,A)=H(D)-H(D|A) Gain(D,A)=H(D)H(DA)

  • 信息增益越大表示使用特征 A 来划分所获得的“纯度提升越大”

缺点

  • ID3 没有剪枝策略,容易过拟合;
  • 信息增益准则对可取值数目较多的特征有所偏好,类似“编号”的特征其信息增益接近于 1;
  • 只能用于处理离散分布的特征;
  • 没有考虑缺失值。

确定分类指标

在集大食堂吃饭时,经常会点一碗免费的例汤配饭,当我们站在窗口前面对若干碗清汤,大脑就生成了一颗决策树。我将使用(清汤清晰程度、食材量、个人口渴程度、温度)这些离散属性对清汤的需求程度进行分类。

代码实现

导入必要库

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

import math
from math import log

创建数据集

数据集基于近些天堂食的清汤决策记录。

def create_data():
    datasets = [['清澈', '少', '否', '一般', '否'],
               ['清澈', '一般', '否', '烫', '否'],
               ['清澈', '多', '一般', '凉', '否'],
               ['清澈', '多', '是', '一般', '是'],
               ['清澈', '少', '否', '一般', '否'],
               ['较模糊', '少', '否', '一般', '否'],
               ['较模糊', '一般', '一般', '凉', '否'],
               ['较模糊', '多', '是', '凉', '是'],
               ['较模糊', '一般', '是', '一般', '是'],
               ['较模糊', '少', '是', '烫', '是'],
               ['浑浊', '少', '是', '烫', '否'],
               ['浑浊', '一般', '是', '一般', '是'],
               ['浑浊', '多', '否', '凉', '是'],
               ['浑浊', '多', '一般', '烫', '是'],
               ['浑浊', '少', '否', '一般', '否'],
               ]
    labels = [u'清汤清晰程度', u'食材量', u'个人口渴程度', u'温度', u'是否需求']
    # 返回数据集和每个维度的名称
    return datasets, labels

熵计算

采用上述公式计算信息熵,用于后续信息增益计算。

# 熵
def calc_ent(datasets):
    data_length = len(datasets)
    label_count = {}
    for i in range(data_length):
        label = datasets[i][-1]
        if label not in label_count:
            label_count[label] = 0
        label_count[label] += 1
    ent = -sum([(p/data_length)*log(p/data_length, 2) for p in label_count.values()])
    return ent

计算经验条件熵

# 经验条件熵
def cond_ent(datasets, axis=0):
    data_length = len(datasets)
    feature_sets = {}
    for i in range(data_length):
        feature = datasets[i][axis]
        if feature not in feature_sets:
            feature_sets[feature] = []
        feature_sets[feature].append(datasets[i])
    cond_ent = sum([(len(p)/data_length)*calc_ent(p) for p in feature_sets.values()])
    return cond_ent

计算信息增益

信息增益=总体信息熵-该属性的经验条件熵

# 信息增益
def info_gain(ent, cond_ent):
    return ent - cond_ent

经过一次测试得到对应特征的信息增益,这里个人口渴程度信息增益最高故以此为跟节点
在这里插入图片描述

确定根节点特征

根据基尼指数计算信息增益,选择增益最大的属性作为根节点。

def info_gain_train(datasets):
    count = len(datasets[0]) - 1
    ent = calc_ent(datasets)
    best_feature = []
    for c in range(count):
        c_info_gain = info_gain(ent, cond_ent(datasets, axis=c))
        best_feature.append((c, c_info_gain))
        print('特征({}) - info_gain - {:.3f}'.format(labels[c], c_info_gain))
    # 比较大小
    best_ = max(best_feature, key=lambda x: x[-1])
    return '特征({})的信息增益最大,选择为根节点特征'.format(labels[best_[0]])

定义节点类

每个节点需要保存父节点和该节点的子树,方便预测时的遍历和查找

# 定义节点类 二叉树
class Node:
    def __init__(self, root=True, label=None, feature_name=None, feature=None):
        self.root = root
        self.label = label
        self.feature_name = feature_name
        self.feature = feature
        self.tree = {}
        self.result = {'label:': self.label, 'feature': self.feature, 'tree': self.tree}

    def __repr__(self):
        return '{}'.format(self.result)

    def add_node(self, val, node):
        self.tree[val] = node

    def predict(self, features):
        if self.root is True:
            return self.label
        return self.tree[features[self.feature]].predict(features)

定义决策树类

定义决策树时设定阈值,当该节点数据量小于阈值时将其剪枝,防止决策树过拟合。

class DTree:
    def __init__(self, epsilon=0.1):
        self.epsilon = epsilon
        self._tree = {}

    # 熵
    @staticmethod
    def calc_ent(datasets):
        data_length = len(datasets)
        label_count = {}
        for i in range(data_length):
            label = datasets[i][-1]
            if label not in label_count:
                label_count[label] = 0
            label_count[label] += 1
        ent = -sum([(p/data_length)*log(p/data_length, 2) for p in label_count.values()])
        return ent

    # 经验条件熵
    def cond_ent(self, datasets, axis=0):
        data_length = len(datasets)
        feature_sets = {}
        for i in range(data_length):
            feature = datasets[i][axis]
            if feature not in feature_sets:
                feature_sets[feature] = []
            feature_sets[feature].append(datasets[i])
        cond_ent = sum([(len(p)/data_length)*self.calc_ent(p) for p in feature_sets.values()])
        return cond_ent

    # 信息增益
    @staticmethod
    def info_gain(ent, cond_ent):
        return ent - cond_ent

    def info_gain_train(self, datasets):
        count = len(datasets[0]) - 1
        ent = self.calc_ent(datasets)
        best_feature = []
        for c in range(count):
            c_info_gain = self.info_gain(ent, self.cond_ent(datasets, axis=c))
            best_feature.append((c, c_info_gain))
        # 比较大小
        best_ = max(best_feature, key=lambda x: x[-1])
        return best_

    def train(self, train_data):
        """
        input:数据集D(DataFrame格式),特征集A,阈值eta
        output:决策树T
        """
        _, y_train, features = train_data.iloc[:, :-1], train_data.iloc[:, -1], train_data.columns[:-1]
        # 1,若D中实例属于同一类Ck,则T为单节点树,并将类Ck作为结点的类标记,返回T
        if len(y_train.value_counts()) == 1:
            return Node(root=True,
                        label=y_train.iloc[0])

        # 2, 若A为空,则T为单节点树,将D中实例树最大的类Ck作为该节点的类标记,返回T
        if len(features) == 0:
            return Node(root=True, label=y_train.value_counts().sort_values(ascending=False).index[0])

        # 3,计算最大信息增益 同5.1,Ag为信息增益最大的特征
        max_feature, max_info_gain = self.info_gain_train(np.array(train_data))
        max_feature_name = features[max_feature]

        # 4,Ag的信息增益小于阈值eta,则置T为单节点树,并将D中是实例数最大的类Ck作为该节点的类标记,返回T
        if max_info_gain < self.epsilon:
            return Node(root=True, label=y_train.value_counts().sort_values(ascending=False).index[0])

        # 5,构建Ag子集
        node_tree = Node(root=False, feature_name=max_feature_name, feature=max_feature)

        feature_list = train_data[max_feature_name].value_counts().index
        for f in feature_list:
            sub_train_df = train_data.loc[train_data[max_feature_name] == f].drop([max_feature_name], axis=1)

            # 6, 递归生成树
            sub_tree = self.train(sub_train_df)
            node_tree.add_node(f, sub_tree)

        # pprint.pprint(node_tree.tree)
        return node_tree

    def fit(self, train_data):
        self._tree = self.train(train_data)
        return self._tree

    def predict(self, X_test):
        return self._tree.predict(X_test)

决策树的生成与预测

调用上述函数生成决策树,并输入未曾拥有的数据进行预测得到相应结果。

datasets, labels = create_data()
data_df = pd.DataFrame(datasets, columns=labels)
dt = DTree()
tree = dt.fit(data_df)

print(dt.predict(['浑浊', '多', '否', '一般']))

当属性值时['浑浊', '多', '否', '一般']时,对应清汤的需求为是。
在这里插入图片描述

打印出树结构

print(tree)

在这里插入图片描述
比较复杂难懂,于是将其可视化。
为了简化代码量,此处利用 sklearn.tree.DecisionTreeClassifier函数构建决策树,默认使用CART算法。数据集采用sklearn的iris数据集。最后采用graphviz进行可视化。

import numpy as np
import pandas as pd
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
# data
def create_data():
    iris = load_iris()
    df = pd.DataFrame(iris.data, columns=iris.feature_names)
    df['label'] = iris.target
    df.columns = ['sepal length', 'sepal width', 'petal length', 'petal width', 'label']
    data = np.array(df.iloc[:100, [0, 1, -1]])
    # print(data)
    return data[:,:2], data[:,-1]

X, y = create_data()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)

from sklearn.tree import DecisionTreeClassifier
from sklearn.tree import export_graphviz
import graphviz


clf = DecisionTreeClassifier()
clf.fit(X_train, y_train,)

clf.score(X_test, y_test)

tree_pic = export_graphviz(clf, out_file="mytree.pdf")
with open('mytree.pdf') as f:
    dot_graph = f.read()
    # 决策树可视化
    graph = graphviz.Source(dot_graph)
    graph.render('mytree')

可视化结果为:
在这里插入图片描述

总结

虽然部分属性组合结果可以预测,但由于数据量较少导致树的深度较浅,尚存在其他属性组合无法进行预测。

你可能感兴趣的:(机器学习,决策树,算法)