决策树概念
决策树是机器学习中一种特别常见的算法。决策树是一个预测模型,它在已知各种情况发生的概率下,训练而成的一个学习器,代表着对象属性和对象值的一种映射关系。
预测满意度样例:
划分标准
1.信息熵: 度量一个属性的信息量
S 代 表 训 练 样 本 集 合 , p 代 表 预 测 结 果 在 总 样 本 的 占 比 的 集 合 公 式 :
Entropy
(
S
)
=
−
∑
i
=
1
n
p
i
log
2
p
i
\operatorname{Entropy}(\mathrm{S})=-\sum_{\mathrm{i}=1}^{\mathrm{n}} \mathrm{p}_{\mathrm{i}} \log _{2} \mathrm{p}_{\mathrm{i}}
Entropy(S)=−∑i=1npilog2pi
p i 总 样 本 数 : 5
正 样 本 数 ( 满 意 ) : 3
负 样 本 数 ( 不 满 意 ) :2
实 例 计 算 :
Entropy
(
S
)
=
−
∑
i
=
1
n
p
i
log
2
p
i
Entropy
(
S
)
=
−
3
5
⋅
log
2
(
3
5
)
−
2
5
⋅
log
2
(
2
5
)
\operatorname{Entropy}(\mathrm{S})=-\sum_{\mathrm{i}=1}^{\mathrm{n}} \mathrm{p}_{\mathrm{i}} \log _{2} \mathrm{p}_{\mathrm{i}}\operatorname{Entropy}(\mathrm{S})=-\frac{3}{5} \cdot \log _{2}\left(\frac{3}{5}\right)-\frac{2}{5} \cdot \log _{2}\left(\frac{2}{5}\right)
Entropy(S)=−∑i=1npilog2piEntropy(S)=−53⋅log2(53)−52⋅log2(52)
2.条件熵: 表示在已知属性X的情况下它的属性类别Y的不确定性(需要求所有属性的条件熵)
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∣)
3.信息增益 = 信息熵 - 条件熵:
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的取值范围为0-100,我们划分为0-25, 25-50, 50-75, 75-100四个区间,作为属性a的四种取值
那么,划分边界的选择就是我们需要解决的问题。我们以二分为例(化为两个子区间):
设一个属性a={a1, a2, …, an}的所有取值:
(0.697, 0.774, 0.634, 0.608, 0.556, 0.403, 0.481, 0.437, 0.666, 0.243, 0.245, 0.343, 0.639, 0.657, 0.360, 0.593, 0.719)
将属性a的所有的取值进行排序:
(0.774, 0.719, 0.697, 0.666, 0.657, 0.639, 0.634, 0.608, 0.593, 0.556, 0.481, 0.437, 0.403, 0.36, 0.343, 0.245, 0.243)
我们可以取任意相邻取值的中位点,作为划分点,例如:我们可以使用0.634和0.608的中位点进行划分,我们希望划分之后的数据集更加纯净,所以我们依然用信息熵来度量划分的效果,我们选取划分效果最好的点作为划分点。
剪枝处理
预剪枝:在决策树生成过程中,对每个结点在划分前先进行估计,若当前结点的划分不能带来决策树泛化性能提升,则停止划分并将当前结点标记为叶结点
后剪枝:先从训练集生成一棵完整的决策树,然后自底向上地对非叶结点进行考察,若将该结点对应的子树替换为叶结点能带来决策树泛化性能提升,则将该子树替换为叶结点。
决策树优缺点
优点:
可解释性强:决策树模型的生成过程类似人类决策的过程,易于理解和解释,可帮助决策者了解数据特征、属性间的关系和决策规则,有助于对数据的深入分析和挖掘。
适用性广:决策树算法不需要对数据做过多的前置处理,如特征缩放、归一化等,可以直接处理离散或连续型的数据,且不受数据分布的影响,适用于各种类型的数据和问题。
可处理缺失值和异常值:决策树算法可以处理缺失值和异常值,因为在分裂节点时只需考虑当前样本的特征值,而不需要考虑其他样本的特征值。
速度快:决策树的训练和预测速度都很快,因为它的判定过程非常简单,只需对每个特征进行一次比较,所以时间复杂度为 O(n),其中 n 表示样本数量。
缺点:
容易过拟合:决策树的划分过程是基于训练数据的,因此容易出现过拟合现象,导致模型泛化能力差。可以通过剪枝、限制树的深度、增加样本数等方法来解决过拟合问题。
不稳定性高:由于数据的微小变化可能导致树结构的大幅变化,所以决策树的稳定性较差,需要采用集成学习等方法来提高稳定性。
对连续值处理不好:决策树算法对连续型变量的处理不如对离散型变量的处理好,需要对连续型变量进行离散化处理。
高度依赖数据质量:决策树算法需要有足够的样本数据和较好的数据质量,否则容易出现欠拟合和过拟合等问题,影响模型的性能。
数据集分类指标
对于茶百道奶茶好喝与否,我将用以下属性(甜度,茶底,小料,温度)
代码实现
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import math
from math import log
from sklearn.model_selection import train_test_split
def create_data():
datasets = [['多糖', '红', '否', 34.2, '否'],
['多糖', '绿', '否', 35.4, '否'],
['多糖', '乌龙', '正常', 32.9, '否'],
['多糖', '乌龙', '是', 33.8, '是'],
['多糖', '红', '否', 33.7, '否'],
['少糖', '红', '否', 34.1, '否'],
['少糖', '绿', '正常', 33.0, '否'],
['少糖', '乌龙', '是', 33.2, '是'],
['少糖', '绿', '是', 34.0, '是'],
['少糖', '红', '是', 35.6, '是'],
['标准糖', '红', '是', 35.7, '否'],
['标准糖', '绿', '是', 34.5, '是'],
['标准糖', '乌龙', '否', 33.0, '是'],
['标准糖', '乌龙', '正常', 35.5, '是'],
['标准糖', '红', '否', 34.2, '否'],
]
labels = [u'甜度', u'茶底', u'小料', u'温度', u'好喝与否']
# 返回数据集和每个维度的名称
return datasets, labels
# 定义节点类 二叉树
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 = {}
self.best = 0
# 熵
@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 trainsform(self, datasets):
pre_change = -1
for i in range(len(datasets[0])):
if type(datasets[0][i])==float:
pre_change = i
if pre_change == -1:
return datasets, -1
ent = self.calc_ent(datasets)
pre_feature = datasets[:, pre_change:pre_change + 1].flatten()
pre_feature_ls = np.array(sorted(list(pre_feature)))
eta_ls = []
#确定划分标准
for i in range(len(pre_feature_ls) - 1):
eta_ls.append((pre_feature_ls[i + 1] + pre_feature_ls[i]) / 2)
tmp_pre_feature_ls = np.copy(pre_feature_ls)
ent_count = []
for i in range(len(eta_ls)):
eta = eta_ls[i]
tmp_pre_feature_ls[pre_feature_ls <= eta] = int(0)
tmp_pre_feature_ls[pre_feature_ls > eta] = int(1)
tmp_datasets = np.copy(datasets)
tmp_datasets[:, pre_change:pre_change + 1] = np.array(tmp_pre_feature_ls).reshape(-1, 1)
gain = self.info_gain(ent, self.cond_ent(tmp_datasets, axis=pre_change))
ent_count.append(gain)
# 确定最佳标准
ent_count = np.array(ent_count)
best = np.argmax(ent_count)
tmp_pre_feature_ls[pre_feature_ls <= eta_ls[best]] = int(0)
tmp_pre_feature_ls[pre_feature_ls > eta_ls[best]] = int(1)
datasets[:, pre_change:pre_change + 1] = np.array(tmp_pre_feature_ls).reshape(-1, 1)
return datasets, eta_ls[best]
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为信息增益最大的特征
# 计算最大信息增益时,先将连续值转为离散值
datasets, self.best = dt.trainsform(np.array(train_data))
max_feature, max_info_gain = self.info_gain_train(datasets)
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):
if X_test[3] <= self.best:
X_test[3] = int(0)
else:
X_test[3] = int(1)
return self._tree.predict(X_test)
datasets, labels = create_data()
data_df = pd.DataFrame(datasets, columns=labels)
dt = DTree()
tree = dt.fit(data_df)
input = ['多糖', '红', '否', 12.0]
print(dt.predict(input))
print(tree)
树结构
可视化树
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')
可视化结果为: