决策树类似于20问题
,可以用一个流程图来表示。
二十个问题的游戏,游戏的规则很简单:参与游戏的一方在脑海里想某个事物,
其他参与者向他提问题,只允许提20个问题,问题的答案也只能用对或错回答。问问题的人通过
推断分解,逐步缩小待猜测事物的范围。
文末的决策树:根据已有的数据来预测一个患者适合哪种类型的隐形眼镜。
决策树(Decison Tree)树中每个节点的分类依据都是特征(feature
) ,节点下都是对象。特征即有无脚蹼,是否有羽毛,有脚蹼和无脚蹼称为这个feature
下的不同value
。当一个特征被确定之后,分支(branch
)即依赖value
来构造。
ID3 (Iterative Dichotomiser 3)含义为迭代二叉树3代。
分类算法中,k-近邻算法可以完成很多分类任务,但是它最大的缺点就是无法给出数据的内在含义。从原始数据中创建规则构造决策树的过程就是学习过程。
优点:
缺点:
思考构建一个决策树可以由三部分组成。
递归建树在递归划分时实现。
伪代码:
def create_tree(dataset):
这个特征下,每个子项下是否都为同一类:
if so (到叶子)
return 标签
else (到节点)
除去该特征,构造子集 sub_dataset
寻找子集中的最佳特征
创建分支节点
for 每个子集:
create_tree(sub_dataset)
return 分支节点
信息熵:
如果一个待分类的事务在多个分类中,那么对于一种分类方案。如 x x x的分类方式 x i , i = 1 , 2 , 3... {x_i,i=1,2,3...} xi,i=1,2,3...,那么 x i x_i xi的熵
为
l ( x i ) = − l o g 2 p ( x i ) l(x_i)=-log_2{p(x_i)} l(xi)=−log2p(xi)
p ( x i ) p(x_i) p(xi)为该分类的概率。
对于这个分类,我们计算它的期望
H = − ∑ i = 1 n p ( x i ) l o g 2 p ( x i ) H =-\sum_{i=1}^np(x_i)log_2{p(x_i)} H=−i=1∑np(xi)log2p(xi)
分类的结果让数据越混乱,熵
越大,因此最优的分类方案有最小的熵,它和原数据集的熵的差值即为信息增益。
'''
一个判断是否是海洋生物的dataset。
1,0表示有无
no surfacing 不浮出水面能否生存
flippers 表示有脚蹼
yes/no 是否鱼类
'''
dataset = [[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
labels = ['no surfacing', 'flippers']
# shannon entropy
def calc_shannon_ent(dataset):
num_entries = len(dataset) # dataset 的条目数
labels_counts = {}
for feat_vec in dataset:
cur_label = feat_vec[-1]
if cur_label not in labels_counts.keys():
labels_counts[cur_label] = 1
else:
labels_counts[cur_label] += 1
shannon_ent = 0.0
for key in labels_counts:
prob = float(labels_counts[key]) / num_entries
shannon_ent -= prob * log(prob, 2)
return shannon_ent
>>> calc_shannon_ent(data)
0.9709505944546686
>>> -0.4*log(0.4,2)-0.6*log(0.6,2)
0.9709505944546686
在分类中,可能一种分类下的分支产生的’yes/no’结果为(1,3),另一种为(2,2),那么计算shannon_ent
这两种分类就有优劣了。
已知分类的特征,划分数据集。
# 去除dataset中的一列。
def split_dataset(dataset, axis, value):
ret_dataset = []
for feat_vec in dataset:
if feat_vec[axis] == value:
reduce_dataset = feat_vec[:axis]
reduce_dataset.extend(feat_vec[axis + 1:])
ret_dataset.append(reduce_dataset)
return ret_dataset
选取该dataset
下最佳的分类方式。
def choose_best_feature_to_split(dataset):
# 去除最后一个'yes/no'标签
num_feat = len(dataset[0]) - 1
# 原始熵,信息增益,特征序号
base_shannon_ent = calc_shannon_ent(dataset)
best_gain = 0.0
best_feature = -1
# 遍历每个特征,抽取一个列,计算熵的总和
# vec 为行,list为列
for i in range(num_feat):
featlist = [feat_vec[i] for feat_vec in dataset]
# set去重
unique_feat = set(featlist)
new_entropy = 0.0
# 这个特征下不同的值划分产生的子集,计算子集的期望
for value in unique_feat:
sub_dataset = split_dataset(dataset, i, value)
# 计算期望
prob = len(sub_dataset) / float(len(dataset))
new_entropy += prob * calc_shannon_ent(sub_dataset)
# 比较
gain = base_shannon_ent - new_entropy
if (gain > best_gain):
best_gain = gain
best_feature = i
return best_feature
工作原理:
对原始数据集,基于最好的属性值划分数据集,由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。第一次划分之后,数据将被向下传递到树分支的下一个节点,在这个节点上,我们可以再次划分数据。因此我们可以采用递归的原则处理数据集。
递归终点:
1.这个子集下特征全部一致
2.用完全部特征(最后一个特征)
如果用到最后一个特征,但子集下特征还不一致,那么需要额外处理,处理这个(唯一的)混乱子集。显然挑一个结果次数最多的,返回标签。
def majority_cnt(classlist):
class_count = {}
for vote in classlist:
if vote not in class_count:
class_count[vote] = 0
else:
class_count[vote] += 1
sorted_class_count = sorted(
class_count.items(), key=operator.itemgetter(1), reverse=True)
return sorted_class_count[0][0]
import operator
from math import log
import pathlib
dataset = [[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
labels = ['no surfacing', 'flippers']
# shannon entropy
def calc_shannon_ent(dataset):
num_entries = len(dataset) # dataset 的条目数
labels_counts = {}
for feat_vec in dataset:
cur_label = feat_vec[-1]
if cur_label not in labels_counts.keys():
labels_counts[cur_label] = 1
else:
labels_counts[cur_label] += 1
shannon_ent = 0.0
for key in labels_counts:
prob = float(labels_counts[key]) / num_entries
shannon_ent -= prob * log(prob, 2)
return shannon_ent
# 去除dataset中的一列。
def split_dataset(dataset, axis, value):
ret_dataset = []
for feat_vec in dataset:
if feat_vec[axis] == value:
reduce_dataset = feat_vec[:axis]
reduce_dataset.extend(feat_vec[axis + 1:])
ret_dataset.append(reduce_dataset)
return ret_dataset
def choose_best_feature_to_split(dataset):
# 去除最后一个'yes/no'标签
num_feat = len(dataset[0]) - 1
# 原始熵,信息增益,特征序号
base_shannon_ent = calc_shannon_ent(dataset)
best_gain = 0.0
best_feature = -1
# 遍历每个特征,抽取一个列,计算熵的总和
# vec 为行,list为列
for i in range(num_feat):
featlist = [feat_vec[i] for feat_vec in dataset]
# set去重
unique_feat = set(featlist)
new_entropy = 0.0
# 这个特征下不同的值划分产生的子集,计算子集的期望
for value in unique_feat:
sub_dataset = split_dataset(dataset, i, value)
# 计算期望
prob = len(sub_dataset) / float(len(dataset))
new_entropy += prob * calc_shannon_ent(sub_dataset)
# 比较
gain = base_shannon_ent - new_entropy
if (gain > best_gain):
best_gain = gain
best_feature = i
return best_feature
def majority_cnt(classlist):
class_count = {}
for vote in classlist:
if vote not in class_count:
class_count[vote] = 0
else:
class_count[vote] += 1
sorted_class_count = sorted(
class_count.items(), key=operator.itemgetter(1), reverse=True)
return sorted_class_count[0][0]
# 递归建树
def create_tree(dataset, labels):
classlist = [i[-1] for i in dataset]
# 是否该(sub)dataset下分类全部一致
if classlist.count(classlist[0]) == len(classlist):
return classlist[0]
# 分类完只剩最后一个label,剩下的dataset仍然没有完全被分类,调用majority_cnt 返回剩下中的最多的一个分类
if len(classlist) == 1:
return majority_cnt(classlist)
best_feature = choose_best_feature_to_split(dataset)
best_feature_label = labels[best_feature]
myTree = {best_feature_label: {}}
del(labels[best_feature])
feat_list = [i[best_feature]for i in dataset]
unique_list = set(feat_list)
for feat in unique_list:
sub_labels = labels[:]
myTree[best_feature_label][feat] = create_tree(
split_dataset(dataset, best_feature, feat), sub_labels)
return myTree
if __name__ == '__main__':
print(create_tree(dataset,labels))
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
增加原始数据处理的过程
if __name__ == '__main__':
fp = open('lenses.txt','r')
# lenses return as a 2 decison array
lenses = [eachline.strip().split('\t') for eachline in fp.readlines()]
lenses_label = ['age', 'prescript', 'astigmatic', 'tearRate']
lenses_tree = create_tree(lenses, lenses_label)
print(lenses_tree)
{'tearRate': {'reduced': 'no lenses', 'normal': {'astigmatic': {'yes': {'prescript': {'hyper': {'age': {'young': 'hard', 'presbyopic': 'no lenses', 'pre': 'no lenses'}}, 'myope': 'hard'}}, 'no': {'age': {'young': 'soft', 'presbyopic': {'prescript': {'hyper': 'soft', 'myope': 'no lenses'}}, 'pre': 'soft'}}}}}}
构造决策树是很耗时的任务。一般分类时调用已有的决策树。使用Python模块pickle
序列化对象。
def storeTree(inputTree, filename):
import pickle
fw = open(filename, 'w')
pickle.dump(inputTree, fw)
fw.close()
def grabTree(filename):
import pickle
fr = open(filename)
return pickle.load(fr)