决策树是一种机器学习的方法。决策树的生成算法有ID3, C4.5和CART等。决策树是一种树形结构,其中每个内部节点表示一个属性上的判断,每个分支代表一个判断结果的输出,最后每个叶节点代表一种分类结果。
由于本次实验仅采用ID3算法构建决策树,因此仅介绍该算法。
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=1∑K∣D∣∣Ck∣log2∣D∣∣Ck∣
其中 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(D∣A)=i=1∑n∣D∣∣Di∣H(Di)=−i=1∑n∣D∣∣Di∣(k=1∑K∣Di∣∣Dik∣log2∣Di∣∣Dik∣)
其中 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(D∣A)
信息增益越大表示使用特征 A 来划分所获得的“纯度提升越大”
在集大食堂吃饭时,经常会点一碗免费的例汤配饭,当我们站在窗口前面对若干碗清汤,大脑就生成了一颗决策树。我将使用(清汤清晰程度、食材量、个人口渴程度、温度)这些离散属性对清汤的需求程度进行分类。
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')
可视化结果为:
虽然部分属性组合结果可以预测,但由于数据量较少导致树的深度较浅,尚存在其他属性组合无法进行预测。