算法思想
刚接触决策树的概念时,让我想到了有段时间比较火的一些性格测试,参与者需要认真回答10道左右给出的所谓“性格测试题”,并在完成后得出最终测得的参与者“性格”。从我身边的例子来看,大部分情况下这些测试结果还是令参与者满意的,虽说其中不乏有一定的娱乐成分,但是我想这些试题也并非是随随便便给出来的。这些试题正如一颗高度为10的决策树一般,随着参与者每道题的选择而走向不同的路径,并最终到达叶节点,以一定的条件概率值获得最终测得的结果。决策树更像是一系列if-then(互斥完备)规则,逐层选择,最终得到预测结果。
构建决策模型本质上是从训练集中归纳出一组分类规则(递归地选择最优特征),并根据该特征对训练数据进行分割,使得对各个子数据集有一个最好的分类过程,这一过程对应着对特征空间的划分,也对应着对决策树的构建,这样的模型不仅需要对训练数据有很好的拟合,而且对未知数据有很好的预测。
算法步骤
特征选择、构建决策树、决策树剪枝
1.特征选择
该步骤就要决定选择哪个特征来划分特征空间,即选择的特征要能更好的对训练数据进行分类,那么如何进行特征选择,以及如何判断选择的特征是否达到了预期的分类效果,就要引入一个非常重要的准则:熵(entropy)和基尼(Gini)指数
说明:CART决策树算法利用Gini指数划分数据集,ID3决策树算法利用信息增益划分数据集,C4.5决策树算法利用增益率划分数据集,CART既能处理分类问题也能处理回归问题,更多公式详见附录
1)熵定义:
在信息论与概率统计中,熵是随机变量的不确定度量,即熵越大,随机变量的不确定性就越大,拿二分类问题来说,若P(X=1)=p,P(X=0)=1-p,则熵为:H§=-plogp-(1-p)log(1-p),我们用matplotlib画一下,可以看到,越偏向两边,事件发生的概率就越偏向于某一事件,它的不确定性就越小,熵也就越小:
2)Gini指数定义:
与熵类似,基尼指数也是样本集合的不确定性度量,GIni指数越大,样本不确定性越大,当二分类时,Gini指数和熵之半图像接近。熵和Gini指数从效果上并无优劣之分,但从计算复杂度来说,熵的计算要更慢些,因为涉及到log的计算!
光看数学公式,可能并不好理解熵和Gini指数是如何进行特征选择的,这个时候就需要通过一些实例来加深一下印象,读者可以参考------李航《统计学习方法》一书中介绍的“贷款申请样本数据表”的例子,周志华《机器学习》一书中介绍的“判断是否是好瓜”的例子,《机器学习实战》一书中的“海洋生物”的例子。由于篇幅有限,博客里就不再举例详述公式,默认读者已了解过至少其中之一的例子。
2.构建决策树
有了特征选择准则,接下来就要按照这一特征将训练数据集划分成子集,使得各个子集有一个在当前条件下最好的分类,如此递归下去,直至所有训练数据子集被基本正确分类为止,此时便生成了一棵决策树。
3.决策树剪枝
对于一些分类决策面不是很明晰的数据,决策树可以在终止之前一直延伸其高度,以iris数据集为例,图中圈红处为了将边缘的两个橘点准确分类,决策边界延伸到绿点内部,这就造成了过拟合现象。而决策树剪枝的目的就是为了防止过拟合,将一些容易造成过拟合的分支剪掉,使得剪枝后的决策树(损失函数最小)具有更好的泛化能力:
关键代码
以iris数据集作为训练数据,实现CART决策树,解决分类问题,为实现可视化,我们只取iris数据的两个特征。
1.实现熵或Gini指数的计算:
我们用python自带的Counter类,生成一个字典,Key是标签名,value是标签对应的数量,格式如下:
# 计算信息熵
def entropy(y):
counter = Counter(y) #生成一个字典(key:value),统计数据集中不同标签的个数
entropy = 0.0
for num in counter.values():
p = num / len(y)
entropy += -p * np.log(p)
return entropy
# 计算基尼指数
def gini(y):
counter = Counter(y) # 生成一个字典(key:value),统计数据集中不同标签的个数
gini_p = 0.0
for num in counter.values():
p = num / len(y)
gini_p += p ** 2
gini = 1 - gini_p
return gini
2.构建决策树 (使用信息熵作为特征选择准则,Gini指数同理,两者效果一致)
我们按照数据的第d个维度的value值,将data和label划分成左右子树,比value大的划分到左子树,比value小的计划分到右子树,并返回划分后的data和label,data_l, data_r, label_l, label_r,计算划分后树的熵,直至熵达到最小,并最终返回最小熵,划分的value值和,value所在的维度:
# 划分数据集
def split_data(data, label, d, value):
data_l = []
data_r = []
label_l = []
label_r = []
for x in range(data.shape[0]):
if data[x, d] >= value:
data_l.append(data[x])
label_l.append(label[x])
if data[x, d] < value:
data_r.append(data[x])
label_r.append(label[x])
return data_l, data_r, label_l, label_r
# 构建决策树
def build_tree(data, label):
best_entropy = float('inf')
best_d, best_v = -1, -1
for d in range(data.shape[1]):
#在每一维度中选阈值value
feature_vlues = {}
for sample in data:
feature_vlues[sample[d]] = 1
# print(len(feature_vlues))
for value in feature_vlues.keys():
data_l, data_r, label_l, label_r = split_data(data, label, d, value)
p_l, p_r = len(data_l) / len(X), len(data_r) / len(X)
e = p_l * entropy(label_l) + p_r * entropy(label_r)
if e < best_entropy:
best_entropy, best_d, best_v = e, d, value
return best_entropy, best_d, best_v
将build_tree函数返回的best_d, best_v带回到split_data函数中,将原始数据正确分割开:
X1_l, X1_r, y1_l, y1_r = split_data(X, y, best_d, best_v)
print("entropy_y1_l =", entropy(y1_l))
print("entropy_y1_r =", entropy(y1_r))
结果显示,右子树信息熵为0,左子树信息熵为0.69314718056,表明右子树已达到最优分类,此时基于第0维度的3.0划分数据集达到最小信息熵。接着对左子树进行划分,此时基于第1维度的1.8划分左子树达到最小信息熵:
最后我们将实验结果用matplotlib可视化出来,便是这样,其中绿线是第0维的划分,蓝线是第1维的划分:
附:经过翻阅资料了解到,还有一种阈值选择方法,即选某一维度上两个连续值的中间值作为value进行划分,代码如下:
# 构建决策树(二分法)
def dec_tree(data, label):
best_entropy = float('inf')
best_d, best_v = -1, -1
for d in range(data.shape[1]):
sorted_index = np.argsort(data[:, d])
for i in range(1, len(data)):
if data[sorted_index[i], d] != data[sorted_index[i - 1], d]:
#注意此处和上述build_tree函数阈值选取的区别
v = (data[sorted_index[i], d] + data[sorted_index[i - 1], d]) / 2
data_l, data_r, label_l, label_r = split_data(data, label, d, v)
p_l, p_r = len(data_l) / len(X), len(data_r) / len(X)
e = p_l * entropy(label_l) + p_r * entropy(label_r)
if e < best_entropy:
best_entropy, best_d, best_v = e, d, v
return best_entropy, best_d, best_v
从最终的可视化分类效果来看,似乎这样的阈值选取发更为合理些!
到此便实现ID3决策树的构建,当然我们只是实现了高度为2的决策树,并无过拟合现象,所以无需剪枝。
总结
1.为了实现可视化,我把iris数据简化为只有两个维度,而且经过这一时间段的学习发现
v = (data[sorted_index[i], d] + data[sorted_index[i - 1], d]) / 2
# for sample in data:
# feature_vlues[sample[d]] = 1
这样选取每一维度的阈值,要比直接取各维度上的值要更加合理些,今后我也将直接采用这样的方法
2.本文实现了一棵高度为2的ID3决策树,由于直接限制了树的高度,所以不会出现一直向下延伸的想象,即不会发生过拟合现象,下篇博客将使用scikit-learn封装好的DecisionTreeClassifier来构建CART决策树,解决分类问题和回归问题,过拟合现象及解决方案也将在下篇博客详述!
附录
研一为了准备模式识别考试,写了些总结,丢掉可惜,放在这里供参考!