决策树
是一种基本的分类与回归模型,呈树形结构。可以看作if-then的处理结构,也可以看作条件概率分布,对特征空间进行划分,在子特征空间进行类别判断,大于阈值则属于子空间的类别。
https://blog.csdn.net/weixin_35479108/article/details/85284613
sklearn
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris, load_boston
from sklearn import tree
from sklearn.model_selection import train_test_split
# 分类树
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
clf = tree.DecisionTreeClassifier()
clf = clf.fit(X_train, y_train)
print ("Classifier Score:", clf.score(X_test, y_test))
tree.plot_tree(clf.fit(X, y))
plt.show()
# 回归树
X, y = load_boston(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
clf = tree.DecisionTreeRegressor()
clf = clf.fit(X_train, y_train)
print ("Regression Score:", clf.score(X_test, y_test))
tree.plot_tree(clf.fit(X, y))
plt.show()
信息论基础
首先先来几个概念,我们后面介绍决策树原理的时候会提到,这里可以先扫一眼,用到的时候再回来看.
- 熵和信息熵
熵,热力学中表征物质状态的参量之一,用符号S表示,其物理意义是体系混乱程度的度量. 可以看出,熵表示的是体系的不确定性大小. 熵越大, 物理的不确定性越大. 1948年,香农提出了“信息熵”的概念,才解决了对信息的量化度量问题. 同理, 信息熵越小,数据的稳定性越好,我们更加相信此时数据得到的结论. 换言之, 我们现在目的肯定熵越小,机器学习得到的结果越准确.
信息熵表示随机变量不确定性的度量,设随机标量X是一个离散随机变量,其概率分布为:
P
(
X
=
x
i
)
=
p
i
,
i
=
1
,
2
,
.
.
.
,
n
P(X=x_i)=p_i, i=1,2,...,n
P(X=xi)=pi,i=1,2,...,n
则随机变量X的熵定义为:
H
(
X
)
=
−
∑
i
=
1
n
p
i
l
o
g
p
i
H(X)=-\sum_{i=1}^{n}p_ilog{p_i}
H(X)=−i=1∑npilogpi
熵越大,随机变量的不确定性就越大,当
p
i
=
1
n
p_i=\frac{1}{n}
pi=n1时,
随机变量的熵最大等于logn,故
0
≤
H
(
P
)
≤
l
o
g
n
0 \leq H(P) \leq logn
0≤H(P)≤logn.
- 条件熵
条件熵就是在给定X的条件的情况下,随机标量Y的条件,记作
H
(
Y
∣
X
)
H(Y|X)
H(Y∣X),可以结合贝叶斯公式进行理解,定义如下
H
(
Y
∣
X
)
=
∑
i
=
1
n
p
i
H
(
Y
∣
X
=
x
i
)
H(Y|X)=\sum_{i=1}^{n}p_iH(Y|X=x_i)
H(Y∣X)=i=1∑npiH(Y∣X=xi)
这里
p
i
=
P
(
X
=
x
i
)
,
i
=
1
,
2
,
.
.
.
,
n
p_i=P(X=x_i),i=1,2,...,n
pi=P(X=xi),i=1,2,...,n.
一般在基于数据的估计中,我们使用的基于极大似然估计出来的经验熵和经验条件熵.
- 联合熵
联合熵是相对两个及其以上的变量而言的, 两个变量X和Y的联合信息熵为:
H ( X , Y ) = − ∑ x ∑ y P ( x , y ) l o g 2 [ P ( x , y ) ] H(X,Y)=-\sum_x \sum_y P(x,y)log_2[P(x,y)] H(X,Y)=−x∑y∑P(x,y)log2[P(x,y)]
其中: x和y是X和Y的特定值, 相应地, 是这些值一起出现的联合概率, 若为0, 则定义为0.
对于两个以上的变量
X
1
,
.
.
.
,
X
n
X_1,...,X_n
X1,...,Xn,一般形式位:
H
(
X
1
,
.
.
.
,
X
n
)
=
−
∑
x
1
⋅
⋅
⋅
∑
x
n
P
(
x
1
,
.
.
.
,
x
n
)
l
o
g
2
[
P
(
x
1
,
.
.
.
,
x
n
)
]
H(X_1,...,X_n)=-\sum_{x_1}\cdot \cdot \cdot\sum_{x_n}P(x1,...,x_n)log_2[P(x_1,...,x_n)]
H(X1,...,Xn)=−x1∑⋅⋅⋅xn∑P(x1,...,xn)log2[P(x1,...,xn)]
性质:
- 大于每个独立的熵
H ( X , Y ) ≥ m a x ( H ( X ) , H ( Y ) ) H(X,Y) \geq max(H(X),H(Y)) H(X,Y)≥max(H(X),H(Y))
H ( X 1 , . . . , X n ) ≥ m a x ( H ( X 1 ) , . . . , H ( X n ) ) H(X_1,...,X_n) \geq max(H(X_1),...,H(X_n)) H(X1,...,Xn)≥max(H(X1),...,H(Xn)) - 小于独立熵的和
H ( X 1 , . . . , X n ) ≤ s u m ( H ( X 1 ) , . . . , H ( X n ) ) H(X_1,...,X_n) \leq sum(H(X_1),...,H(X_n)) H(X1,...,Xn)≤sum(H(X1),...,H(Xn)) - 和条件熵的关系
H ( Y ∣ X ) = H ( X , Y ) − H ( X ) H(Y|X)=H(X,Y)-H(X) H(Y∣X)=H(X,Y)−H(X) - 和互信息的关系
I ( Y ; X ) = H ( X ) + H ( Y ) − H ( X , Y ) = H ( Y ) − ( H ( X , Y ) − H ( X ) ) I(Y;X)=H(X)+H(Y)-H(X,Y)=H(Y)-(H(X,Y)-H(X)) I(Y;X)=H(X)+H(Y)−H(X,Y)=H(Y)−(H(X,Y)−H(X))
- 信息增益
信息增益又叫互信息,它表示的是在的得知特征X的信息后,使得类Y的信息的不确定性(熵)减少的程度.
g
(
Y
,
X
)
=
H
(
Y
)
−
H
(
Y
∣
X
)
g(Y,X)=H(Y)-H(Y|X)
g(Y,X)=H(Y)−H(Y∣X)
- 基尼指数
基尼指数又称基尼系数或者基尼不纯度,基尼系数是指国际上通用的、用以衡量一个国家或地区居民收入差距的常用指标. 在信息学中,例如分类问题, 假设有K个类,样本点属于第k类的概率是
p
k
p_k
pk,则该概率分布的基尼指数定义为:
G
i
n
i
(
p
)
=
∑
k
K
p
k
(
1
−
p
k
)
=
1
−
∑
k
K
p
k
2
Gini(p)=\sum_k^Kp_k(1-p_k)=1-\sum_k^Kp_k^2
Gini(p)=k∑Kpk(1−pk)=1−k∑Kpk2
model | feature select | 树的类型 | 计算公式 |
---|---|---|---|
ID3 | {分类:信息增益} | 多叉树 | g ( D , A ) = H ( D ) − H ( D ∥ A ) g(D,A)=H(D)-H(D\|A) g(D,A)=H(D)−H(D∥A) |
C4.5 | {分类:信息增益比} | 多叉树 | g R ( D , A ) = g ( D , A ) H A ( D ) g_R(D,A)=\frac{g(D,A)}{H_A(D)} gR(D,A)=HA(D)g(D,A) |
CART | {回归:平方误差;分类:基尼指数} | 二叉树 | G i n i ( p ) = ∑ k = 1 K p k ( 1 − p k ) = 1 − ∑ k = 1 K p k 2 Gini(p)=\sum_{k=1}^{K}p_k(1-p_k)=1-\sum_{k=1}^{K}p_k^2 Gini(p)=∑k=1Kpk(1−pk)=1−∑k=1Kpk2 |
其中, H A ( D ) = H ( D ∣ A ) H_A(D)=H(D|A) HA(D)=H(D∣A).
过拟合和剪枝
决策树建立的过程中,只考虑经验损失最小化,没有考虑结构损失. 因此可能在训练集上表现良好,但是会出现过拟合问题.(我们构造决策树的时候,是完全的在训练数据上得到最优的模型. 这就是过拟合问题,训练误差很小,但是验证集上就不怎么好用.) 为了解决过拟合,我们从模型损失进行考虑:
模 型 损 失 = 经 验 风 险 最 小 化 + 正 则 项 = 结 构 风 险 最 小 化 模型损失=经验风险最小化+正则项=结构风险最小化 模型损失=经验风险最小化+正则项=结构风险最小化
思路很简单,给损失函数加上正则项再进行优化. 正则项表示树节点的个数,因此有如下公式:
C α ( T ) = C ( T ) + α ∣ T ∣ C_{\alpha}(T)=C(T)+\alpha|T| Cα(T)=C(T)+α∣T∣
进一步详细定义,解决问题:
重新定义损失函数,树的叶子节点个数|T|,t是树T的叶节点,该叶节点有
N
t
N_t
Nt个样本,其中k类的样本点有
N
t
k
N_{tk}
Ntk个,k=1,2,…,K,
H
t
(
T
)
H_t(T)
Ht(T)是叶子节点t经验熵,
α
≤
0
\alpha \leq 0
α≤0是参数,平衡经验损失和正则项,得到计算公式如下:
C
α
(
T
)
=
∑
t
=
1
∣
T
∣
N
t
H
t
(
T
)
+
α
∣
T
∣
C_{\alpha}(T)=\sum_{t=1}^{|T|}N_tH_t(T)+\alpha|T|
Cα(T)=t=1∑∣T∣NtHt(T)+α∣T∣
其中,经验熵为:
H
t
(
T
)
=
−
∑
k
N
t
k
H
t
l
o
g
N
t
k
H
t
H_t(T)=-\sum_{k}\frac{N_{tk}}{H_t}log\frac{N_{tk}}{H_t}
Ht(T)=−k∑HtNtklogHtNtk
这是有:
C
α
=
C
(
T
)
+
α
∣
T
∣
C_{\alpha}=C(T)+\alpha|T|
Cα=C(T)+α∣T∣
决策树剪枝优化过程考虑了在训练数据上的经验风险最小化和减小模型复杂度两个方向. 因为加了正则项,所有我们基于贪心的思想进行剪枝,因为当剪掉一个树节点,虽然经验风险增加了,但是模型复杂度降低了,我们基于两个方面的平衡进行剪枝,如果剪掉之后,总的风险变小,就进行剪枝.
算法:
输入: 算法产生的整个决策树,参数
α
\alpha
α
修剪之后的树
T
α
T_{\alpha}
Tα
- 计算每个节点的经验熵
- 递归从树的叶节点向上回溯,假设将某个叶节点回缩到其父节点前后的整体树对应的
T
B
T_B
TB和
T
A
T_A
TA,对应的损失分别是
C
α
(
T
B
)
C_{\alpha}(T_B)
Cα(TB)和
C
α
(
T
A
)
C_{\alpha}(T_A)
Cα(TA),如果:
C α ( T A ) ≤ C α ( T B ) C_{\alpha}(T_A) \leq C_{\alpha}(T_B) Cα(TA)≤Cα(TB)
表示,剪掉之后,损失减小,就进行剪枝. - 重复2,直到不能继续为止,得到损失函数最小的子树 T α T_{\alpha} Tα.
4. 动态规划剪枝.
可以看出来上述算法是一个递归问题,存在很多重复项计算,这里我们使用dfs+备忘录进行加速计算,这种方法和动态规划类似.
算法:
输入: 算法产生的整个决策树,参数
α
\alpha
α
修剪之后的树
T
α
T_{\alpha}
Tα
- dp[所有树的节点] = {0}; 保留所有几点的信息熵
- 计算每个cur_node节点的经验熵, {if dp[cur_node] 直接返回, 否则, 执行2}
- 递归从树的叶节点向上回溯,假设将某个叶节点回缩到其父节点前后的整体树对应的
T
B
T_B
TB和
T
A
T_A
TA,对应的损失分别是
C
α
(
T
B
)
C_{\alpha}(T_B)
Cα(TB)和
C
α
(
T
A
)
C_{\alpha}(T_A)
Cα(TA),如果:
C α ( T A ) ≤ C α ( T B ) C_{\alpha}(T_A) \leq C_{\alpha}(T_B) Cα(TA)≤Cα(TB)
表示,剪掉之后,损失减小,就进行剪枝.
d
p
[
c
u
r
n
o
d
e
]
=
C
α
(
T
A
)
dp[cur_node] = C_{\alpha}(T_A)
dp[curnode]=Cα(TA)
4. 重复2,直到不能继续为止,得到损失函数最小的子树
T
α
T_{\alpha}
Tα.
# -*- coding: UTF-8 -*-
import copy
import numpy as np
import pandas as pd
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
class DecisionTree(object):
"""自定的树结构,用来保存决策树.
Paramters:
----------
col: int, default(-1)
当前使用的第几列数据
val: int or float or str, 分割节点
分割节点的值,
int or float : 使用大于进行比较
str : 使用等于模式
LeftChild: DecisionTree
左子树, <= val
RightChild: DecisionTree
右子树, > val
results:
"""
def __init__(self, col=-1, val=None, LeftChild=None, RightChild=None, result=None):
self.col = col
self.val = val
self.LeftChild = LeftChild
self.RightChild = RightChild
self.result = result
class DecisionTreeClassifier(object):
"""使用基尼指数的分类决策树接口.
Paramters:
---------
max_depth : int or None, optional(dafault=None)
表示决策树的最大深度. None: 表示不设置深度,可以任意扩展,
直到叶子节点的个数小于min_samples_split个数.
min_samples_split : int, optional(default=2)
表示最小分割样例数.
if int, 表示最小分割样例树,如果小于这个数字,不在进行分割.
min_samples_leaf : int, optional (default=1)
表示叶节点最少有min_samples_leaf个节点树,如果小于等于这个数,直接返回.
if int, min_samples_leaf就是最小样例数.
min_impurity_decrease : float, optional (default=0.)
分割之后基尼指数大于这个数,则进行分割.
N_t / N * (impurity - N_t_R / N_t * right_impurity
- N_t_L / N_t * left_impurity)
min_impurity_split : float, default=1e-7
停止增长的阈值,小于这个值直接返回.
Attributes
----------
classes_ : array of shape (n_classes,) or a list of such arrays
表示所有的类
feature_importances_ : ndarray of shape (n_features,)
特征重要性, 被选择最优特征的次数,进行降序.
tree_ : Tree object
The underlying Tree object.
"""
def __init__(self,
max_depth=None,
min_samples_split=2,
min_samples_leaf=1,
min_impurity_decrease=0.,
min_impurity_split=1e-7):
self.max_depth = max_depth
self.min_samples_split = min_samples_split
self.min_samples_leaf = min_samples_leaf
self.min_impurity_decrease = min_impurity_decrease
self.min_impurity_split = min_impurity_split
self.classes_ = None
self.max_features_ = None
self.decision_tree = None
self.all_feats = None
def fit(self, X, y, check_input=True):
"""使用X和y训练决策树的分类模型.
Parameters
----------
X : {array-like} of shape (n_samples, n_features)
The training input samples. Internally, it will be converted to
``dtype=np.float32``
y : array-like of shape (n_samples,) or (n_samples, n_outputs)
The target values (class labels) as integers or strings.
check_input : bool, (default=True)
Allow to bypass several input checking.
Returns
-------
self : object
Fitted estimator.
"""
if isinstance(X, list):
X = self.__check_array(X)
if isinstance(y, list):
y = self.__check_array(y)
if X.shape[0] != y.shape[0]:
raise ValueError("输入的数据X和y长度不匹配")
self.classes_ = list(set(y))
if isinstance(X, pd.DataFrame):
X = X.values
if isinstance(y, pd.DataFrame):
y = y.values
data_origin = np.c_[X, y]
print(data_origin)
self.all_feats = [i for i in range(X.shape[1])]
self.max_features_ = X.shape[0]
data = copy.deepcopy(data_origin)
self.decision_tree = self.__build_tree(data, 0)
def __predict_one(self, input_x):
"""预测一个样例的返回结果.
Paramters:
---------
input_x : list or np.ndarray
需要预测输入数据
Returns:
-------
class : 对应的类
"""
tree = self.decision_tree
# ============================= show me your code =======================
# here
def run(node, tree):
if tree.result != None:
return tree.result
val = node[tree.col] # col: int, default(-1) 当前使用的第几列数据 val 分割节点的值
tree = tree.LeftChild if val <= tree.val else tree.RightChild
return run(node, tree)
pre_y = run(input_x, tree)
# ============================= show me your code =======================
return pre_y
def predict(self, test):
"""预测函数,
Paramters:
---------
test: {array-like} of shape (n_samples, n_features)
Returns:
result : np.array(list)
"""
result = []
for i in range(len(test)):
result.append(self.__predict_one(test[i]))
return np.array(result)
def score(self, vali_X, vali_y):
"""验证模型的特征,这里使用准确率.
Parameters
----------
vali_X : {array-like} of shape (n_samples, n_features)
The training input samples. Internally, it will be converted to
``dtype=np.float32``
vali_y : array-like of shape (n_samples,) or (n_samples, n_outputs)
The target values (class labels) as integers or strings.
Returns:
-------
score : float, 预测的准确率
"""
vali_y = np.array(vali_y)
pre_y = self.predict(vali_X)
pre_score = 1.0 * sum(vali_y == pre_y) / len(vali_y)
return pre_score
def __build_tree(self, data, depth):
"""创建决策树的主要代码
Paramters:
---------
data : {array-like} of shape (n_samples, n_features) + {label}
The training input samples. Internally, it will be converted to
``dtype=np.float32``
depth: int, 树的深度
Returns:
-------
DecisionTree
"""
labels = np.unique(data[:, -1])
# 只剩下唯一的类别时,停止,返回对应类别
if len(labels) == 1:
return DecisionTree(result=list(labels)[0])
# 遍历完所有特征时,只剩下label标签,就返回出现字数最多的类标签
if not self.all_feats:
return DecisionTree(result=np.argmax(np.bincount(data[:, -1].astype(int))))
# 超过最大深度,则停止,使用出现最多的参数作为该叶子节点的类
if self.max_depth and depth > self.max_depth:
return DecisionTree(result=np.argmax(np.bincount(data[:, -1].astype(int))))
# 如果剩余的样本数大于等于给定的参数 min_samples_split,
# 则不在进行分割, 直接返回类别中最多的类,该节点作为叶子节点
if self.min_samples_split >= data.shape[0]:
return DecisionTree(result=np.argmax(np.bincount(data[:, -1].astype(int))))
# 叶子节点个数小于指定参数就进行返回,叶子节点中的出现最多的类
if self.min_samples_leaf >= data.shape[0]:
return DecisionTree(result=np.argmax(np.bincount(data[:, -1].astype(int))))
# 根据基尼指数选择每个分割的最优特征
best_idx, best_val, min_gini = self.__getBestFeature(data)
print ("Current best Feature:", best_idx, best_val, min_gini)
# 如果当前的gini指数小于指定阈值,直接返回
if min_gini < self.min_impurity_split:
return DecisionTree(result=np.argmax(np.bincount(data[:, -1].astype(int))))
leftData, rightData = self.__splitData(data, best_idx, best_val)
# ============================= show me your code =======================
# here
leftDecisionTree = self.__build_tree(leftData, depth + 1)
rightDecisionTree = self.__build_tree(rightData, depth + 1)
# ============================= show me your code =======================
return DecisionTree(col=best_idx, val=best_val, LeftChild=leftDecisionTree, RightChild=rightDecisionTree)
def __getBestFeature(self, data):
"""得到最优特征对应的列
Paramters:
---------
data: np.ndarray
从data中选择最优特征
Returns:
-------
bestInx, val, 最优特征的列的索引和使用的值.
"""
best_idx = -1
best_val = None
min_gini = 1.0
# 遍历现在可以使用的特征列
# ============================= show me your code =======================
# here
for f in self.all_feats:
x = data[:,f]
for v in x:
l, r = self.__splitData(data, f, v)
l_gini = 1.0 * len(l) / len(data) * self.gini(l[:,-1])
r_gini = 1.0 * len(r) / len(data) * self.gini(r[:,-1])
c_gini = l_gini + r_gini
if c_gini < min_gini:
best_idx = f
best_val = v
min_gini = c_gini
# ============================= show me your code =======================
# 删除使用过的特征
self.all_feats.remove(best_idx)
return best_idx, best_val, min_gini
def gini(self, labels):
"""计算基尼指数.
Paramters:
----------
labels: list or np.ndarray, 数据对应的类目集合.
Returns:
-------
gini : float ```Gini(p) = \sum_{k=1}^{K}p_k(1-p_k)=1-\sum_{k=1}^{K}p_k^2 ```
"""
# ============================= show me your code =======================
# here
label_set = np.array(labels)
gini = 1
for l in np.unique(label_set):
gini -= 1.0 * (np.sum(label_set == l) / len(label_set)) ** 2
# ============================= show me your code =======================
return gini
def __splitData(self, data, featColumn, val):
'''根据特征划分数据集分成左右两部分.
Paramters:
---------
data: np.ndarray, 分割的数据
featColumn : int, 使用第几列的数据进行分割
val : int or float or str, 分割的值
int or float : 使用比较方式
str : 使用相等方式
Returns:
-------
leftData, RightData
int or left: leftData <= val < rightData
str : leftData = val and rightData != val
'''
if isinstance(val, str):
leftData = data[data[:, featColumn] == val]
rightData = data[data[:, featColumn] != val]
elif isinstance(val, int) or isinstance(val, float):
leftData = data[data[:, featColumn] <= val]
rightData = data[data[:, featColumn] > val]
return leftData, rightData
def __check_array(self, X):
"""检查数据类型
Parameters:
----------
X : {array-like} of shape (n_samples, n_features)
The training input samples.
Retures
-------
X: {array-like} of shape (n_samples, n_features)
"""
if isinstance(X, list):
X = np.array(X)
if not isinstance(X, np.ndarray) and not isinstance(X, pd.DataFrame):
raise ValueError("输出数据不合法,目前只支持np.ndarray or pd.DataFrame")
return X
if __name__ == "__main__":
# 分类树
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
clf = DecisionTreeClassifier()
clf.fit(X_train, y_train)
print ("Classifier Score:", clf.score(X_test, y_test))