算法原理
决策树是一类经典的机器学习方法,既可以用于分类任务,也可以用于回归。分类和回归对应的分别是分类树和回归树,本文将以最常见的一类决策树——ID3分类树为例,讲解模型的原理以及程序实现。
模型
已知一组有n个样本的训练集
{
x
i
,
y
i
}
i
=
1
n
,
x
i
∈
∏
k
=
1
t
D
k
,
y
i
∈
{
0
,
1
}
\{x_i,y_i\}_ {i=1}^n,x_i\in\prod_{k=1}^tD^k,y_i\in\{0,1\}
{xi,yi}i=1n,xi∈k=1∏tDk,yi∈{0,1}
其中,每个样本
x
i
x_i
xi都有
t
t
t个特征(为了简便起见,假设样本的特征都是离散值)。现在的目的是希望通过对训练集进行训练。之后往模型中代入新的样本
x
j
x_j
xj,得到预测的分类类别。
原理
在讲原理之前先通过一个例子引入,假设现在是晚上,你在考虑是否要玩游戏,这时可能要先判断现在是几点,是否已经很晚了,如果很晚则不玩游戏,若是现在还早,那可能还要判断明天需要上交的作业写好了没有,还没写好则要抓紧时间写,如果写好了,就可以去玩游戏了。这个例子的图示可以表示为
决策树依次通过样本的特征自上而下地对样本进行分类,根据某一个特征的不同,可以将样本分成若干组,对于样本类别全都相同的组则停止分类,对于样本类别不全相同的组,则需要根据剩余的特征再进行分类,直至最后得到的每个组里的样本类别全相同。
为了理解决策树的构建过程,需要先弄清楚几个问题
树的组成
决策树包含节点和分支。
图上的方框即代表节点,它是根据特征进行分类得到的数据组。其中,节点包括根节点,中间节点和叶子节点。根节点是指最初始的节点,即还没有进行任何分类的节点。叶子节点是指不用再进行分类的节点。不是根节点和叶子节点的节点便是中间节点。
分支是指由节点引出的若干条细支,每条分支都代表一个分类过程。
哪些特征需要优先选择
对应到决策树中的节点,由于决策树是自上而下构建的,并且每次只通过一个特征进行分类。而样本集中的特征有很多,现在需要考虑的是要优先选择哪一个特征进行分类?按正常情况来讲当然是分类后得到的各组数据的纯度越高越好,这里的纯度体现在每组数据中各类别的分布情况上。当每组数据中只有一种类别,则纯度是最高的,当不止有一种类别,则纯度会较低一些。衡量这种纯度的方法有很多,本文介绍的是ID3算法,它是通过计算信息增益来选择特征的。
信息增益=分类前信息熵-分类后的加权信息熵
信
息
增
益
=
E
−
∑
i
=
1
k
∣
D
i
∣
∣
D
∣
E
i
信息增益=E-\sum_{i=1}^k\frac{|D_i|}{|D|}E_i
信息增益=E−i=1∑k∣D∣∣Di∣Ei
其中,
E
E
E是分类前的信息熵,
∣
D
∣
|D|
∣D∣指分类前的样本总数,
∣
D
i
∣
|D_i|
∣Di∣是分类后的第i组数据的样本总数,
E
i
E_i
Ei是指分类后的第i组的信息熵。
为了使得纯度更高,我们希望分类后的信息熵更小,因此要选取信息增益最大的那个特征。按照上述方法对类别数超过一的节点即数据集进行分类,直至得到的分组数据集中类别都只有一种。
分类树与回归树的区别
最明显的不同就是分类树输出的是样本的类别,属于离散值,而回归树输出的是实数,是连续值。另外,样本的特征也有离散值和连续值的差别,如果特征是离散值,则在选取特征时可以用普通的ID3、C4.5和Gini方法。如果特征是连续值,则需要将该特征的数值按大小进行排序,然后分别取这些数值作为阈值将数据分成两类,结合ID3、C4.5或Gini方法选出最优的特征。
程序实现
各函数
计算信息熵
def cal_entropy(y):
"""信息熵计算
参数
-------
y:类别编号 类型:narray, shape:{n_samples}
返回
----
e:信息熵 类型:float
"""
count = np.array(pd.Series(y).value_counts())
p = count/count.sum()
return -np.sum(np.log2(p)*p)
选取最优的特征
def choose_features_ID3(X, y):
"""选择特征(单个)
参数
------
X:特征,类型:ndarray,shape:{n_samples, n_features}
y: 类别编号 类型:narray, shape:{n_samples}
返回
-----
min_fea_index:选出的特征,类型:integer
entropy:信息增益,类型:float
"""
n_samples, n_features = X.shape
fea_index = 0
max_entropy = 0
pre_y_entropy = cal_entropy(y)
for i in range(n_features):
entropy_sum = 0
row_value = X[:,i]
for value in set(row_value):
bools = row_value==value
entropy_sum += np.sum(bools)/n_samples * cal_entropy(y[bools])
entropy = pre_y_entropy-entropy_sum
if entropy>max_entropy:
max_entropy = entropy
fea_index = i
return fea_index,entropy
构建ID3决策树
def tree_ID3(X, y, X_name):
"""构建决策树,采用ID3,无剪枝操作
参数
------
X:特征,类型:ndarray,shape:{n_samples, n_features}
y: 类别编号,类型:ndarray,shape:{n_samples}
X_name: 特征名,类型:ndarray,shape:{n_samples}
"""
if not len(X):return
if cal_entropy(y)==0:return y[0]
n_samples, n_features = X.shape
index = choose_features_ID3(X, y)[0]
dic = {X_name[index]:{}}
remove_fea = X[:, index]
for fea in set(remove_fea):
row_bool = remove_fea==fea # 留下的行索引
col_bool = np.arange(n_features)!=index # 留下的列索引
dic[X_name[index]][fea] = tree_ID3(X[row_bool][:,col_bool], y[row_bool], X_name[col_bool])
return dic
实例化演示
以西瓜数据集为例
dataSet = np.array([
# 1
['青绿', '蜷缩', '浊响', '清晰', '凹陷', '硬滑', '好瓜'],
# 2
['乌黑', '蜷缩', '沉闷', '清晰', '凹陷', '硬滑', '好瓜'],
# 3
['乌黑', '蜷缩', '浊响', '清晰', '凹陷', '硬滑', '好瓜'],
# 4
['青绿', '蜷缩', '沉闷', '清晰', '凹陷', '硬滑', '好瓜'],
# 5
['浅白', '蜷缩', '浊响', '清晰', '凹陷', '硬滑', '好瓜'],
# 6
['青绿', '稍蜷', '浊响', '清晰', '稍凹', '软粘', '好瓜'],
# 7
['乌黑', '稍蜷', '浊响', '稍糊', '稍凹', '软粘', '好瓜'],
# 8
['乌黑', '稍蜷', '浊响', '清晰', '稍凹', '硬滑', '好瓜'],
# ----------------------------------------------------
# 9
['乌黑', '稍蜷', '沉闷', '稍糊', '稍凹', '硬滑', '坏瓜'],
# 10
['青绿', '硬挺', '清脆', '清晰', '平坦', '软粘', '坏瓜'],
# 11
['浅白', '硬挺', '清脆', '模糊', '平坦', '硬滑', '坏瓜'],
# 12
['浅白', '蜷缩', '浊响', '模糊', '平坦', '软粘', '坏瓜'],
# 13
['青绿', '稍蜷', '浊响', '稍糊', '凹陷', '硬滑', '坏瓜'],
# 14
['浅白', '稍蜷', '沉闷', '稍糊', '凹陷', '硬滑', '坏瓜'],
# 15
['乌黑', '稍蜷', '浊响', '清晰', '稍凹', '软粘', '坏瓜'],
# 16
['浅白', '蜷缩', '浊响', '模糊', '平坦', '硬滑', '坏瓜'],
# 17
['青绿', '蜷缩', '沉闷', '稍糊', '稍凹', '硬滑', '坏瓜']
])
X = dataSet[:,:-1]
y = dataSet[:,-1]
X_name = np.array(['色泽','根蒂','敲声','纹理','脐部','触感'])
tree_ID3(X,y,X_name)
out
{'纹理': {'稍糊': {'触感': {'硬滑': '坏瓜', '软粘': '好瓜'}},
'清晰': {'根蒂': {'稍蜷': {'色泽': {'青绿': '好瓜',
'乌黑': {'触感': {'硬滑': '好瓜', '软粘': '坏瓜'}}}},
'硬挺': '坏瓜',
'蜷缩': '好瓜'}},
'模糊': '坏瓜'}}
封装成一个类
增加训练函数fit和预测函数predict、check,其中,check函数是对一个样本进行预测,predict函数整合了多个样本。
class Tree_ID3:
def __init__(self):
pass
def cal_entropy(self, y):
"""信息熵计算
参数
-------
y:类别, 类型:narray, shape:{n_samples}
返回
----
e:信息熵, 类型:float
"""
count = np.array(pd.Series(y).value_counts())
# 每个类别的概率
p = count/count.sum()
# 信息熵
return -np.sum(np.log2(p)*p)
def choose_features_ID3(self, X, y):
"""选择特征(单个)
参数
------
X:特征, 类型:ndarray, shape:{n_samples, n_features}
y:类别, 类型:ndarray, shape:{n_samples}
返回
-----
fea_index:选出的特征, 类型:integer
entropy:信息增益, 类型:float
"""
n_samples, n_features = X.shape
# 最优特征的索引
fea_index = 0
# 最大的信息增益
max_entropy = 0
# 分类前标签y的信息熵
pre_y_entropy = self.cal_entropy(y)
for i in range(n_features):
# 初始化分类后的加权信息熵
entropy_sum = 0
row_value = X[:,i]
for value in set(row_value):
# 选中的样本索引
bools = row_value==value
entropy_sum += np.sum(bools)/n_samples * self.cal_entropy(y[bools])
# 当前信息增益
entropy = pre_y_entropy-entropy_sum
if entropy>max_entropy:
max_entropy = entropy
fea_index = i
return fea_index,entropy
def tree_ID3(self, X, y, X_name):
"""构建决策树
参数
------
X:特征, 类型:ndarray, shape:{n_samples, n_features}
y:类别编号, 类型:ndarray, shape:{n_samples}
X_name:特征名, 类型:ndarray, shape:{n_samples}
返回
-----
dic:决策树, 类型:dict
"""
if not len(X):return
# 只剩一类,返回
if self.cal_entropy(y)==0:return y[0]
n_samples, n_features = X.shape
index = self.choose_features_ID3(X, y)[0]
# 决策树构建
dic = {X_name[index]:{}}
remove_fea = X[:, index]
for fea in set(remove_fea):
# 剩下的行索引
row_bool = remove_fea==fea
# 剩下的列索引
col_bool = np.arange(n_features)!=index
# 递归
dic[X_name[index]][fea] = self.tree_ID3(X[row_bool][:,col_bool], y[row_bool], X_name[col_bool])
return dic
def check(self, tree, X, X_name):
"""预测
"""
if not len(tree) or not len(X):return
cur_fea_name = list(tree.keys())[0]
cur_fea_index = np.where(X_name==cur_fea_name)[0][0]
if X[cur_fea_index] not in tree[cur_fea_name].keys():return
if tree[cur_fea_name][X[cur_fea_index]] in self.y_name:
return tree[cur_fea_name][X[cur_fea_index]]
else:
bools = np.arange(len(X))!=cur_fea_index
return self.check(tree[cur_fea_name][X[cur_fea_index]], X[bools], X_name[bools])
def fit(self, X, y, X_name):
self.X_name = X_name
self.y_name = list(set(y))
self.tree = self.tree_ID3(X, y, X_name)
def predict(self, X):
res = []
for i in range(len(X)):
res.append(self.check(self.tree, X[i], self.X_name))
return np.array(res)
演示
clf = Tree_ID3()
clf.fit(X, y, X_name)
predict_y = clf.predict(X)
sum(predict_y==y)==len(y)
以上便是ID3分类树的代码实现,如有不对不妥之处,欢迎交流批评改正。
参考资料:
周志华《机器学习》