机器学习决策树DecisionTree以及python代码实现
考虑到以后面试会遇到算法原理的知识,所以先学习记录下。
本文参考西瓜书以及mooc上面的视频…,数据集使用西瓜书上的数据。
1.基本算法原理
决策树的基本形状如下:其内部节点为属性(特征),叶节点则为输出类别。
算法的伪代码如下:
def createDecisionTree(dataSet,A):
'''
dataSet为训练集,A是属性(特征)集
'''
生成节点node
if dataSet中的样本都是一个类别C:
将node标记为叶节点,类别为C,return
if 当前A中特征已经遍历完:
将node标记为叶节点,类别为dataSet所有样本中类别最多的类别,return
从A中选出最优划分属性(特征)a
for 遍历属性a能取的所有值a_v:
为node生成一个分支
从dataSet中划分出其属性a取值为a_v的所有样本子集sub_dataSet
if sub_dataSet为空:
将分支节点设置为叶节点,类别为dataSet中类别最多的类别,return
else
继续递归createDecisionTree(dataSet,A),并将其作为分支节点
返回node
其基本思路就是从当前数据集的特征集中选择一个最优的特征,然后根据该特征不同取值进行对数据集进行分支,再将分支的子集重复上面的工作。什么时候结束分支呢?有以下几种情况:(1)特征被使用完;(2)当前分支的数据集都是同一个类别,没必要在进行划分;(3)当前分支对应的数据集为空,不能划分(这种情况主要发生在经过不断的分支之后,分支节点处的数据集不断变小,而当前进行划分所使用的特征的所有取值是根据全集确定的,而变小后的数据集在该特征的部分取值上可能并没有样本能取得该值)。
此外决策树中最核心的部分就是如何选择最优的特征进行划分?
2.选择最优特征进行划分
这里需要先引入一些前导知识:
2.1信息增益
信息熵(information entropy)是度量样本集合不纯度的一项指标,因为熵本来就是体系混乱程度,不确定程度的度量,这里用来表示集合的不纯度。假定集合D中第k类样本所占比例为
p
k
p_k
pk(k=1,2,3…,n),则D的信息熵定义如下:
E
n
t
(
D
)
=
−
∑
k
=
1
n
p
k
l
o
g
2
p
k
Ent(D)=-\sum^{n}_{k=1}p_klog_{2}p_k
Ent(D)=−k=1∑npklog2pk
信息熵越小,集合D的不纯度越小,也就是集合纯度越高。例如假设D中共有两类样本,
当
p
1
=
0.5
,
p
2
=
0.5
时
,
E
n
t
(
D
)
=
1
当p_1=0.5, p_2=0.5时, Ent(D)=1
当p1=0.5,p2=0.5时,Ent(D)=1,而当
p
1
=
0
,
p
2
=
1
时
,
E
n
t
(
D
)
=
0
p_1=0, p_2=1时, Ent(D)=0
p1=0,p2=1时,Ent(D)=0,显然第二种情况只有第二类样本,此时纯度最高,信息熵也是更小。
条件熵是表示在一个条件下,集合D的不纯度。假设集合D使用属性
a
a
a(
a
=
a
1
,
a
2
,
.
.
.
,
a
V
a=a_1, a_2,...,a_V
a=a1,a2,...,aV)进行划分这个条件,则其条件熵定义如下:
E
n
t
(
D
∣
a
)
=
∑
v
=
1
V
p
v
E
n
t
(
D
∣
a
=
a
v
)
=
∑
v
=
1
V
∣
D
v
∣
∣
D
∣
E
n
t
(
D
v
)
Ent(D|a)=\sum^{V}_{v=1}p_vEnt(D|a=a_v)\\ =\sum^{V}_{v=1}\frac{|D^v|}{|D|}Ent(D^v)
Ent(D∣a)=v=1∑VpvEnt(D∣a=av)=v=1∑V∣D∣∣Dv∣Ent(Dv)
条件熵Ent(D|a)就是集合D对属性a的不同取值划分后的各个子集的信息熵的加权平均,权值就是各个子集在原集合中所占比例
∣
D
v
∣
∣
D
∣
\frac{|D^v|}{|D|}
∣D∣∣Dv∣。
信息增益(information gain)表示在一个条件下,集合D的不纯度减少的程度,其定义如下:
G
a
i
n
(
D
,
a
)
=
E
n
t
(
D
)
−
E
n
t
(
D
∣
a
)
Gain(D,a)=Ent(D)-Ent(D|a)
Gain(D,a)=Ent(D)−Ent(D∣a)
E
n
t
(
D
)
Ent(D)
Ent(D)表示集合D的不存度,
E
n
t
(
D
∣
a
)
Ent(D|a)
Ent(D∣a)表示集合D在使用属性a划分条件下的不纯度,因此
E
n
t
(
D
)
−
E
n
t
(
D
∣
a
)
Ent(D)-Ent(D|a)
Ent(D)−Ent(D∣a)就是集合D在使用属性a划分条件下的不纯度减少的程度。信息增益
G
a
i
n
(
D
,
a
)
Gain(D,a)
Gain(D,a)越大,不纯度减少的越多,那么就可以使用属性a对D进行划分,因此信息增益也是最优划分属性选择的重要指标。
但是信息增益有一个问题,就是对可取值较多的属性有偏好,例如假设训练集D中有一个属性为“编号”,每一个样本“编号”都不同,那么使用该属性划分的信息增益会更大,但是这也有一个严重的问题,就是会产生过拟合,导致模型泛化能力差。
2.2信息增益率
信息增益率的定义如下:
G
a
i
n
_
r
a
t
i
o
(
D
,
a
)
=
G
a
i
n
(
D
,
a
)
I
V
(
a
)
Gain\_ratio(D,a)=\frac{Gain(D,a)}{IV(a)}
Gain_ratio(D,a)=IV(a)Gain(D,a)
其中,
I
V
(
a
)
=
−
∑
v
V
∣
D
v
∣
∣
D
∣
l
o
g
2
∣
D
v
∣
∣
D
∣
IV(a)=-\sum^{V}_v\frac{|D^v|}{|D|}log_2\frac{|D^v|}{|D|}
IV(a)=−v∑V∣D∣∣Dv∣log2∣D∣∣Dv∣
相比信息增益,信息增益率则是对可取值数目较少的属性有所偏好,因为属性a取值越多的时候,IV(a)就越大,信息增益率就越小。
因此通常是先从候选划分属性中找出信息增益高于平均水平的属性,在从中找出信息增益率最高的的属性作为最优化为属性。
2.3基尼系数
基尼系数的定义如下:
G
i
n
i
(
D
)
=
1
−
∑
k
=
1
n
p
k
2
Gini(D)=1-\sum^{n}_{k=1}p_k^2
Gini(D)=1−k=1∑npk2
其中n是集合D中样本的类别数目,
p
k
p_k
pk是第k类样本所占比例。直观上基尼系数反映了从数据集D中随机抽取两个样本,其类别标记不一致的概率。因为
p
k
∗
p
k
p_k*p_k
pk∗pk表示样本类别一致的概率,1减去所有种样本一致的概率之和就是不一致的概率。基尼系数越小,不一致的概率就越小,不纯度就越小。
4.连续值以及缺失值的处理
4.1连续值的处理
因为对与连续值而言取值较多,不可能像离散值那样对于每一个取值就进行划分数据集,而是选取一个最优的划分点将数据集D划分为两部分。其具体做法就是将连续属性a的所有取值从小到大排序得到{ a 1 , a 2 , . . . , a n a^1,a^2,...,a^n a1,a2,...,an},然后基于划分点t将数据集D划分为 D t − D_t^- Dt−(包含那些在属性a上取值不大于t的样本)和 D t + D_t^+ Dt+(那些在属性a上取值大于t的样本)。而t在区间 [ a i , a i + 1 ) [a^i, a^{i+1}) [ai,ai+1)中取任何值划分结果都相同,因此可以取中间点进行划分,即t的所有取值集合为 T a = { a i + a i + 1 2 ∣ 1 ≤ i ≤ n } T_a=\{\frac{a^i+a^{i+1}}{2}|1\leq i\leq n\} Ta={2ai+ai+1∣1≤i≤n},然后从所有集合中选取信息增益最大的t作为最优化分点,并将这个信息增益作为数据集D使用属性a进行划分的信息增益。
4.2缺失值的处理
在实际应用中以及一些比赛当中,缺失值都是不可避免,对于缺失值的处理方式也有很多,而对于决策树而言,因为在树的内部节点都是对数据集在某个属性a的不同取值进行划分,而对于那些在属性a上取值为空的样本则不能被正常处理。(1)首先需要解决的问题是“如何在属性值缺失的情况下进行划分属性选择?”,具体做法如下:
对于给定数据集D和属性a(取值为
a
1
,
a
2
,
.
.
,
a
n
a^1,a^2,..,a^n
a1,a2,..,an),令
D
^
\hat{D}
D^表示数据集D在属性a上面没有缺失值的样本子集,
D
^
v
\hat{D}^v
D^v表示
D
^
\hat{D}
D^中在属性a上取值为
a
v
a^v
av的样本子集,
D
^
k
\hat{D}_k
D^k表示
D
^
\hat{D}
D^中属于第k类的样本子集。此外对于每个样本赋予一个权重
w
x
w_x
wx,其初始值为1。并定义如下:
p
=
∑
x
∈
D
^
w
x
∑
x
∈
D
w
x
p
^
k
=
∑
x
∈
D
^
k
w
x
∑
x
∈
D
^
w
x
r
^
v
=
∑
x
∈
D
^
v
w
x
∑
x
∈
D
^
w
x
p=\frac{\sum_{x\in\hat{D}}w_x}{\sum_{x\in D}w_x}\\\hat{p}_k=\frac{\sum_{x\in\hat{D}^k}w_x}{\sum_{x\in \hat{D}}w_x}\\\hat{r}_v=\frac{\sum_{x\in\hat{D}_v}w_x}{\sum_{x\in \hat{D}}w_x}
p=∑x∈Dwx∑x∈D^wxp^k=∑x∈D^wx∑x∈D^kwxr^v=∑x∈D^wx∑x∈D^vwx
其中,
p
p
p表示数据集D在属性a上无缺失值的样本的比例,
p
^
k
\hat{p}_k
p^k表示无缺失值样本中第k类样本的比例,
r
^
v
\hat{r}_v
r^v表示无缺失值样本子集中在属性a上取值为
a
v
a^v
av的样本比例。相应的前面信息增益的公式变成如下形式:
G
a
i
n
(
D
,
a
)
=
p
∗
G
a
i
n
(
D
^
,
a
)
=
p
∗
(
E
n
t
(
D
^
)
−
E
n
t
(
D
^
∣
a
)
)
=
p
∗
(
E
n
t
(
D
^
)
−
∑
v
=
1
V
r
^
v
∗
E
n
t
(
D
^
v
)
)
Gain(D,a)=p*Gain(\hat{D},a)\\=p*(Ent(\hat{D})-Ent(\hat{D}|a))\\=p*(Ent(\hat{D})-\sum_{v=1}^V\hat{r}_v*Ent(\hat{D}_v))
Gain(D,a)=p∗Gain(D^,a)=p∗(Ent(D^)−Ent(D^∣a))=p∗(Ent(D^)−v=1∑Vr^v∗Ent(D^v))
其中信息熵的表达式为:
E
n
t
(
D
^
)
=
−
∑
k
=
1
y
p
^
k
l
o
g
2
p
^
k
Ent(\hat{D})=-\sum_{k=1}^{y}\hat{p}_klog_2\hat{p}_k
Ent(D^)=−∑k=1yp^klog2p^k,y为样本类别数目。
(2)第二个需要解决的问题是,“给定划分属性,若样本在该属性上有缺失,如何对样本进行划分?”。其解决方法为,将在划分属性上有缺失的样本划分到所有节点中,并且更新其权重为
r
^
v
∗
w
x
\hat{r}_v*w_x
r^v∗wx。
5.python代码实现
import math
import pandas as pd
import numpy as np
#带有连续值的数据集
def createDataset():
df = pd.DataFrame()
df["色泽"] = ["青绿","乌黑","乌黑","青绿","浅白","青绿","乌黑","乌黑","乌黑","青绿","浅白","浅白","青绿","浅白","乌黑","浅白","青绿"]
df["根蒂"] = ["蜷缩", "蜷缩", "蜷缩", "蜷缩", "蜷缩", "稍蜷", "稍蜷", "稍蜷", "稍蜷", "硬挺", "硬挺", "蜷缩", "稍蜷", "稍蜷", "稍蜷", "蜷缩", "蜷缩"]
df["敲声"] = ["浊响", "沉闷", "浊响", "沉闷", "浊响", "浊响", "浊响", "浊响", "沉闷", "清脆", "清脆", "浊响", "浊响", "沉闷", "浊响", "浊响", "沉闷"]
df["纹理"] = ["清晰", "清晰", "清晰", "清晰", "清晰", "清晰", "稍糊", "清晰", "稍糊", "清晰", "模糊", "模糊", "稍糊", "稍糊", "清晰", "模糊", "稍糊"]
df["脐部"] = ["凹陷", "凹陷", "凹陷", "凹陷", "凹陷", "稍凹", "稍凹", "稍凹", "稍凹", "平坦", "平坦", "平坦", "凹陷", "凹陷", "稍凹", "平坦", "稍凹"]
df["触感"] = ["硬滑", "硬滑", "硬滑", "硬滑", "硬滑", "软粘", "软粘", "硬滑", "硬滑", "软粘", "硬滑", "软粘", "硬滑", "硬滑", "软粘", "硬滑", "硬滑"]
df['密度'] = [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]
df['含糖量'] = [0.460, 0.376, 0.264, 0.318, 0.215, 0.237, 0.149, 0.211, 0.091, 0.267, 0.057, 0.099, 0.161, 0.198, 0.370, 0.042, 0.103]
df["好瓜"] = ["是", "是", "是", "是", "是", "是", "是", "是", "否", "否", "否", "否", "否", "否", "否", "否", "否"]
print(df)
df.to_csv('data/watermelon.csv', index=0)
#包含缺失值的数据集
def createDatasetNULL():
df = pd.DataFrame()
df["色泽"] = [np.nan,"乌黑","乌黑","青绿",np.nan,"青绿","乌黑","乌黑","乌黑","青绿","浅白","浅白",np.nan,"浅白","乌黑","浅白","青绿"]
df["根蒂"] = ["蜷缩", "蜷缩", "蜷缩", "蜷缩", "蜷缩", "稍蜷", "稍蜷", "稍蜷", np.nan, "硬挺", "硬挺", "蜷缩", "稍蜷", "稍蜷", "稍蜷", "蜷缩", np.nan]
df["敲声"] = ["浊响", "沉闷", np.nan, "沉闷", "浊响", "浊响", "浊响", "浊响", "沉闷", "清脆", "清脆", np.nan, "浊响", "沉闷", "浊响", "浊响", "沉闷"]
df["纹理"] = ["清晰", "清晰", "清晰", "清晰", "清晰", "清晰", "稍糊", np.nan, "稍糊", np.nan, "模糊", "模糊", "稍糊", "稍糊", "清晰", "模糊", "稍糊"]
df["脐部"] = ["凹陷", "凹陷", "凹陷", "凹陷", "凹陷", np.nan, "稍凹", "稍凹", "稍凹", "平坦", "平坦", "平坦", "凹陷", "凹陷", "稍凹", "平坦", "稍凹"]
df["触感"] = ["硬滑", np.nan, "硬滑", "硬滑", "硬滑", "软粘", "软粘", "硬滑", "硬滑", "软粘", np.nan, "软粘", "硬滑", "硬滑", "软粘", "硬滑", "硬滑"]
df["好瓜"] = ["是", "是", "是", "是", "是", "是", "是", "是", "否", "否", "否", "否", "否", "否", "否", "否", "否"]
print(df)
df.to_csv('data/watermelon_null.csv', index=0)
#计算信息熵
def calEntro(dataset):
dataset = np.array(dataset)
data_len = len(dataset)
#labelCount记录各类样本数据的数量
labelCount = {}
for row in dataset:
cur_label = row[-1]
if cur_label not in labelCount.keys():
labelCount[cur_label] = 0
labelCount[cur_label] += 1
result = 0
for key in labelCount.keys():
prob = labelCount[key]/data_len
result -= prob*math.log2(prob)
return result
#计算含有缺失值的信息熵, 不同之处是每个样本带有权重
def calEntroWithNull(weight,d_v:pd.DataFrame,label='好瓜'):
classlist = d_v[label].unique()
ret_entro = 0
for k in classlist:
prob_k = sum(weight[(d_v[label]==k)]) / sum(weight)
# print('p_k : ',sum(weight[(d_v[label]==k)]),'/',sum(weight),'=',prob_k)
ret_entro -= prob_k*np.log2(prob_k)
return ret_entro
#根据信息增益从剩余特征选择信息增益最大的特征,使得根据该特征划分的数据子集纯度最大
def chooseBestFeatureToSplit(dataSet:pd.DataFrame, weight):
'''
:param dataSet: 当前节点需要进行划分的数据集
:param weight: 当前节点需要划分数据集其对应的权重
'''
# print(dataSet)
# print('weight is ',weight)
features = dataSet.columns.values[0:-1]
ret_feat = ''
ret_val = -999
max_Gain = 0
for feat in features:
#计算在特征feat上取值非空集合的熵
entro = calEntroWithNull(weight[dataSet[feat].notnull()], dataSet[dataSet[feat].notnull()])
# print(feat,':')
# print('entro: ',entro)
# p表示数据集在属性feat上,无缺失值样本所占的比例
p = sum(weight[dataSet[feat].notnull()]) / sum(weight)
# print('p: ',sum(weight[dataSet[feat].notnull()]),'/',sum(weight),'=',p)
#对于连续属性和离散属性分开处理
if ('object' in str(dataSet[feat].dtype)) or ('int' in str(dataSet[feat].dtype)):
#数据集dataSet在属性feat上没有缺失值的集合
feat_values = dataSet[feat][dataSet[feat].notnull()].unique()
#当前属性的信息增益
feat_max_Gain = entro
for val in feat_values:
# print('val is ',val)
#d_v是数据集在属性feat上无缺失值的样本集合
d_v = dataSet[dataSet[feat]==val][dataSet[feat].notnull()]
#r_v表示d_v中属性feat取值为val的样本所占比例
r_v = sum(weight[(dataSet[feat]==val)&(dataSet[feat].notnull())])/sum(weight[dataSet[feat].notnull()])
# print('r_v is ',r_v)
tmp_entro = calEntroWithNull(weight[(dataSet[feat]==val)&(dataSet[feat].notnull())], d_v)
# print('entro is ',calEntroWithNull(weight[(dataSet[feat]==val)&(dataSet[feat].notnull())], d_v))
feat_max_Gain -= r_v*tmp_entro
feat_max_Gain *= p
# print('gain: ',feat_max_Gain,'\n')
if feat_max_Gain>max_Gain:
max_Gain = feat_max_Gain
ret_feat = feat
ret_val = -999
elif 'float' in str(dataSet[feat].dtype):
#将连续属性的所有取值从小到大排序,然后取相邻元素的中间值作为分割点
tmp_feat_values = sorted(dataSet[feat].unique())
feat_values = []
for i in range(len(tmp_feat_values)-1):
feat_values.append((tmp_feat_values[i]+tmp_feat_values[i+1])/2)
#遍历上面所有的分割点,将信息增益最大的分割点的信息增益作为该特征的信息增益
feat_max_Gain = 0
feat_val = -999 #分割点
for val in feat_values:
tmp_Gain = entro
df_left = dataSet[dataSet[feat]<=val][dataSet[feat].notnull()]
df_right = dataSet[dataSet[feat]>val][dataSet[feat].notnull()]
r_v_l = sum(weight[(dataSet[feat]<=val)&(dataSet[feat].notnull())])/sum(weight[dataSet[feat].notnull()])
r_v_r = sum(weight[(dataSet[feat]>val)&(dataSet[feat].notnull())])/sum(weight[dataSet[feat].notnull()])
tmp_entro_left = r_v_l*calEntroWithNull(weight[(dataSet[feat]<=val)&(dataSet[feat].notnull())],df_left)
tmp_entro_right = r_v_r*calEntroWithNull(weight[(dataSet[feat]>val)&(dataSet[feat].notnull())],df_right)
tmp_Gain -= (tmp_entro_left + tmp_entro_right)
tmp_Gain *= p
if tmp_Gain>feat_max_Gain:
feat_max_Gain = tmp_Gain
feat_val = val
# print(feat,' : ',feat_max_Gain)
#找出当前数据集中所有属性信息增益最大的作为最优化分特征
if feat_max_Gain>max_Gain:
max_Gain = feat_max_Gain
ret_feat = feat
ret_val = feat_val
return max_Gain, ret_feat, ret_val
#根据特征的取值划分数据集
def splitData(dataSet,null_dataSet, feat, value, weight):
feat_values = dataSet.columns.values
ret_feats = [f for f in feat_values if f != feat]
ret_dataSet = dataSet.loc[dataSet[feat]==value,ret_feats]
ret_nulldataSet = null_dataSet[ret_feats]
ret_weight = weight[dataSet[feat]==value]
return ret_dataSet, ret_weight, ret_nulldataSet
#当特征遍历完后从剩余样本中选出类别数最多的作为类别
def majorityCnt(classList):
data = pd.Series(classList)
return data.value_counts().index[0]
#递归构建决策树
def createDecisionTree(dataSet, weight):
classList = list(dataSet['好瓜'])
#当子集中全部样本的类别相同时,停止递归
if classList.count(classList[0]) == len(classList):
return classList[0]
#遍历完所有特征时
if len(dataSet.columns.values) == 1:
return majorityCnt(classList)
#获取最优划分特征,以及划分点(针对连续变量)
_, bestFeat,bestValSplit = chooseBestFeatureToSplit(dataSet, weight)
# print('best feature is ',bestFeat,'\n')
#这里要取到特征bestFeat的所有可能的取值,所以是在原始数据集df上进行取值
feat_values = df[bestFeat].dropna().unique()
#在bestFeat上取值为null的数据需要加入到每个子结点中,同时更新加入每个子结点的不同的权重
null_dataSet = dataSet[dataSet[bestFeat].isnull()]
decisionTree = {bestFeat:{}}
#如果是离散变量,对所有取值进行划分,并且该特征后续不能再使用
if ('object' in str(dataSet[bestFeat].dtype)) or ('int' in str(dataSet[bestFeat].dtype)):
for val in feat_values:
# print('val is ',val)
sub_dataSet, sub_weight, sub_nulldataSet = splitData(dataSet,null_dataSet, bestFeat, val, weight)
#将在bestFeat上取值为空的数据加入进来
sub_dataSet = sub_dataSet.append(sub_nulldataSet)
#更新加入的数据的权重
r_v = sum(weight[(dataSet[bestFeat]==val)&(dataSet[bestFeat].notnull())])/sum(weight[dataSet[bestFeat].notnull()])
# print('r_v is ',sum(weight[(dataSet[bestFeat]==val)&(dataSet[bestFeat].notnull())]),'/',sum(weight[dataSet[bestFeat].notnull()]),'=',r_v)
add_weight = np.ones(len(null_dataSet)) * r_v
sub_weight = np.append(sub_weight, add_weight)
# print('sub weight is ',sub_weight)
if len(sub_dataSet) == 0:
decisionTree[bestFeat][val] = majorityCnt(classList)
else:
decisionTree[bestFeat][val] = createDecisionTree(sub_dataSet, sub_weight)
#如果是连续属性,则使用前面的得到最优划分点进行划分,并且后续还可继续使用这个属性
elif 'float' in str(dataSet[bestFeat].dtype):
tmp_dataSet = [dataSet[dataSet[bestFeat]<=bestValSplit], dataSet[dataSet[bestFeat]>bestValSplit]]
r_v_l = sum(weight[(dataSet[bestFeat] <= bestValSplit) & (dataSet[bestFeat].notnull())]) / sum(weight[dataSet[bestFeat].notnull()])
r_v_r = sum(weight[(dataSet[bestFeat] > bestValSplit) & (dataSet[bestFeat].notnull())]) / sum(weight[dataSet[bestFeat].notnull()])
# print('r_v is ',sum(weight[(dataSet[bestFeat]==val)&(dataSet[bestFeat].notnull())]),'/',sum(weight[dataSet[bestFeat].notnull()]),'=',r_v)
r_v_list = [r_v_l, r_v_r]
sub_weight_list = [weight[dataSet[bestFeat]<=bestValSplit], weight[dataSet[bestFeat]>bestValSplit]]
symbols = ['-', '+']
for i, sub_dataSet in enumerate(tmp_dataSet):
# 将在bestFeat上取值为空的数据加入进来
sub_nulldataSet = null_dataSet[[f for f in dataSet.columns.values if f != bestFeat]]
sub_dataSet = sub_dataSet.append(sub_nulldataSet)
#更新每个样本的权重
r_v = r_v_list[i]
add_weight = np.ones(len(null_dataSet)) * r_v
sub_weight = np.append(sub_weight_list[i], add_weight)
if len(sub_dataSet) == 0:
decisionTree[bestFeat][str(bestValSplit)+symbols[i]] = majorityCnt(classList)
else:
decisionTree[bestFeat][str(bestValSplit)+symbols[i]] = createDecisionTree(sub_dataSet, sub_weight)
return decisionTree
# createDataset()
# createDatasetNULL()
df = pd.read_csv('data/watermelon_null.csv')
myTree = createDecisionTree(df, np.ones(len(df)))
print(myTree)
输出结果:
{'纹理': {'清晰': {'脐部': {'凹陷': '是', '稍凹': {'触感': {'硬滑': '是', '软粘': {'色泽': {'乌黑': '否', '青绿': '是', '浅白': '否'}}}}, '平坦': {'根蒂': {'蜷缩': '否', '稍蜷': '是', '硬挺': '否'}}}}, '稍糊': {'敲声': {'浊响': {'脐部': {'凹陷': '否', '稍凹': '是', '平坦': '是'}}, '沉闷': '否', '清脆': '否'}}, '模糊': {'色泽': {'乌黑': '是', '青绿': '否', '浅白': '否'}}}}
6.决策树的可视化实现
这里使用了Graphviz对上面的得到决策树进行可视化的实现。
import pygraphviz as pgv
labels = {'否':'坏瓜', '是':'好瓜'}
def drawTree(tree:{}, root_node,root_num, features:[]):
global node_num
for val in tree[root_node].keys():
if isinstance(tree[root_node][val], str): #如果是叶节点则停止递归
leaf_node = labels[tree[root_node][val]]+'_'+str(node_num)
A.add_node(leaf_node,fontname="Microsoft YaHei")
A.add_edge(root_node+'_'+str(root_num), leaf_node, label=val,fontname="Microsoft YaHei")
node_num += 1
continue
inner_node = list(tree[root_node][val].keys())[0]
A.add_node(inner_node+'_'+str(node_num), fontname="Microsoft YaHei")
A.add_edge(root_node+'_'+str(root_num), inner_node+'_'+str(node_num), label=val,fontname="Microsoft YaHei")
node_num += 1
drawTree(tree[root_node][val], inner_node,node_num-1, features)
node_num = 0
root_node = list(myTree.keys())[0]
A=pgv.AGraph(directed=True,strict=False)
A.add_node(root_node + '_' + str(node_num),fontname="Microsoft YaHei")
node_num += 1
drawTree(myTree, root_node,node_num-1, df.columns.values)
A.layout('dot')
A.draw('tree2.png')
输出结果:
7.总结
1.决策树和其它部分模型不同,它可以处理包含缺失值的数据。
2.决策树主要分为ID3(使用信息增益选择特征),C4.5(使用信息增益率),CART决策树(使用信息增益,可用于分类和回归)。并且CART决策树对于离散值的处理和连续值处理一样,并且离散特征还可用于后续划分。
3.补充:sklearn中实现的是CART决策树,并且除了采用基尼系数之外还是实现了信息增益选择最优特征划分。
8.回归树及其python实现(2020.11.30更新)
上文都是关于决策树用于分类时相关理论及其实现,但是决策树除了分类还可以用于回归问题,其思想和分类决策树类似。
8.1回归树的原理
一颗回归树对应着输入空间(即特征空间)的一个划分以及在划分的单元上的输出值。假设已将输入空间划分为M个单元 R 1 , R 2 , . . . , R M R_1,R_2,...,R_M R1,R2,...,RM,并在每个单元 R m R_m Rm上有一个固定的输出值 c m c_m cm,于是回归树模型可表示为:
f ( x ) = ∑ m = 1 M c m I ( x ∈ R m ) f(x)=\sum_{m=1}^{M}c_mI(x\in R_m) f(x)=m=1∑McmI(x∈Rm)
其中I为符号函数,当括号内的表达式为True时返回1,否则返回0。
整个过程我们只需要知道树是如何划分的(即最优划分特征的选择)以及划分后每个单元得输出值怎么确定?
(1)输出值 c m c_m cm的确定:对于回归问题而言,可以使用均方损失函数来表示回归树训练的误差,训练的目标就是使得均方误差最小,即最小化 ∑ x i ∈ R m ( y i − f ( x i ) ) 2 \sum_{x_i\in R_m}(y_i-f(x_i))^2 ∑xi∈Rm(yi−f(xi))2,将式子对 f ( x i ) f(x_i) f(xi)求导,可得 c m = 1 l e n ( R m ) ∑ x i ∈ R m y i c_m=\frac{1}{len(R_m)}\sum_{x_i\in R_m}y_i cm=len(Rm)1∑xi∈Rmyi,即最优的 c m c_m cm是 R m R_m Rm上所有实例对应的真实值得均值。
(2)最优划分特征的选择:分类问题选择特征主要根据信息增益、信息增益率和基尼系数来进行选择,而对于连续值的预测显然不能靠集合不纯度或熵来衡量,而对于回归问题而言,我们选择的特征j及其划分点s要能够使得划分后的误差最小。这里的划分点就是上面分类中对于连续值的处理,此外这里对于离散值属性的处理也是进行选择划分点而不是考虑其所有离散取值。
8.2 python实现
import numpy as np
import pandas as pd
import pygraphviz as pgv
'''构建回归树'''
#计算均方误差
def cal_mse(feat_val_list,split_val,y_true):
feat_val_list = np.array(feat_val_list)
y_pred = np.zeros(len(y_true))
c1 = np.mean(np.array(y_true)[feat_val_list<split_val])
c2 = np.mean(np.array(y_true)[feat_val_list>split_val])
y_pred[feat_val_list<split_val] = c1
y_pred[feat_val_list>split_val] = c2
return np.sum((y_pred-y_true)**2)/len(y_true),c1,c2
#选择最优划分特征以及划分点
def select_best_feature(data:pd.DataFrame, y_true):
'''
:param data: 待划分数据集
:param y_true: 样本对应的真实值
:return: 最优的划分特征以及划分点
'''
min_mse = np.inf #最小均方误差
best_feat = '' #最优划分特征
best_split_val = 0 #最优划分点
best_c1 = 0 #最优划分后每个区域的取值
best_c2 = 0
for feat in data.columns.values:
feat_val_list = list(data[feat]) #该属性下的所有取值,含重复取值
sorted_feat_val_list = sorted(list(data[feat].unique()))
#所有划分点,不含重复值
split_val_list = [(sorted_feat_val_list[i]+sorted_feat_val_list[i+1])/2 for i in np.arange(len(sorted_feat_val_list)-1)]
for split_val in split_val_list:
#计算最小的划分空间,以及其均方误差
cur_mse,c1,c2 = cal_mse(feat_val_list, split_val, y_true)
if cur_mse<min_mse:
min_mse = cur_mse
best_feat = feat
best_split_val = split_val
best_c1 = c1
best_c2 = c2
return best_feat, best_split_val, min_mse, best_c1, best_c2
def build_regressionTree(data:pd.DataFrame,y_true,cur_depth,min_samples_leaf=1,min_samples_split=2,max_depth=3):
#当待划分数据集大小小于最小划分大小时,或者达到树最大深度时,停止划分
if (len(data) < min_samples_split) or cur_depth>max_depth:
return {'isLeaf':True,'val':np.mean(y_true)}
tree = {} #构建树
best_feat, best_split_val, min_mse, best_c1, best_c2 = select_best_feature(data, y_true)
l_tree_index = data[best_feat]<best_split_val
r_tree_index = data[best_feat]>best_split_val
# 当划分后的叶节点数据量大小小于最小叶节点数据量时,停止划分
if(len(data[l_tree_index]) < min_samples_leaf) or (len(data[r_tree_index]) < min_samples_leaf):
return {'isLeaf':True,'val':np.mean(y_true)}
# print(best_feat, best_split_val)
#继续递归划分
tree['isLeaf'] = False
tree['best_feat'] = best_feat
tree['split_val'] = best_split_val
tree['l_tree'] = build_regressionTree(data[l_tree_index],np.array(y_true)[l_tree_index],cur_depth+1, min_samples_leaf, min_samples_split, max_depth)
tree['r_tree'] = build_regressionTree(data[r_tree_index],np.array(y_true)[r_tree_index],cur_depth+1, min_samples_leaf, min_samples_split, max_depth)
return tree
def predict(tree:{}, data:pd.DataFrame):
y_pred = np.zeros(len(data))
for i in np.arange(len(data)):
tmp_tree = tree
while(tmp_tree['isLeaf']==False):
cur_feat = tmp_tree['best_feat']
split_val = tmp_tree['split_val']
if data.loc[i,cur_feat] <= split_val:
tmp_tree = tmp_tree['l_tree']
else:
tmp_tree = tmp_tree['r_tree']
y_pred[i] = tmp_tree['val']
return y_pred
def plotTree(tree:{}, father_node,depth,label):
#如果当前是根节点
if depth == 1:
A.add_node(father_node)
#如果既是根节点又是叶节点,即树桩
if tree['isLeaf'] == True:
A.add_edge(father_node,tree['val'],label=label)
return
else:
plotTree(tree['l_tree'], father_node,depth+1,'<=')
plotTree(tree['r_tree'], father_node,depth+1,'>')
return
if tree['isLeaf'] == True:
A.add_edge(father_node, tree['val'], label=label)
return
A.add_edge(father_node, tree['best_feat']+':'+str(tree['split_val']), label=label)
plotTree(tree['l_tree'], tree['best_feat']+':'+str(tree['split_val']), depth+1,'<=')
plotTree(tree['r_tree'], tree['best_feat']+':'+str(tree['split_val']), depth+1,'>')
if __name__ == '__main__':
df = pd.DataFrame(columns=['x1'])
df['x1'] = [1,2,3,4,5,6,7,8,9,10]
y_true = [4.50,4.75,4.91,5.34,5.80,7.05,7.90,8.23,8.70,9.00]
y_true = [5.56,5.70,5.91,6.40,6.80,7.05,8.90,8.70,9.00,9.05]
tree = build_regressionTree(df, y_true, cur_depth=1,max_depth=3)
print(tree)
# print(select_best_feature(df, y_true))
print(predict(tree,df))
A = pgv.AGraph(directed=True, strict=False)
plotTree(tree, tree['best_feat']+':'+str(tree['split_val']),depth=1,label='')
A.layout('dot') # layout with dot
A.draw('tree2.png') # write to file
后续有时间再补充剪枝部分的内容…