决策树之CART算法分类树原理及python实现
- 决策树
- 决策树的特点
- 优点
- 缺点
- 决策树构造
- 决策树生成算法
- CART算法
- 构造分类决策树
- python代码实现
- 决策树可视化
- sklearn构造决策树
决策树
决策树模型是一种传统的算法,与人类思维十分相似
- 基本思想:模拟人类进行级联选择或决策的过程,按照属性的某个优先级依次对数据的全部属性进行判别,从而得到输入数据对应的预测输出。
- 树形模型是一个一个特征值进行处理,而线性模型是所有特征赋权相加得到新值。
- 监督学习模型:通过对训练集的学习,挖掘其中的规则,预测新数据
- 基本结构:根节点为训练全集,中间节点为属性测试,叶子节点为决策结果
决策树的特点
优点
- 计算简单,可解释性强。
- 适合处理有缺失属性的样本。
- 可自动忽略目标变量中没有贡献的属性。
缺点
- 容易造成过拟合,需要采取剪枝操作。
- 忽略数据之间的相关性。
- 信息增益偏向于变量取值更多的离散特征。
决策树构造
基本思路:
决策树的构造本质上是一种贪心算法,每次都根据分类规则找到最优的划分特征,根据该特征将节点样本划分若干部个样本集,不断重复,构造新的节点,重复递归直至满足终止条件。
终止条件:
- 当前节点包含样本全属于同一类别,无需划分
- 当前节点包含样本集为空,不能划分
- 当前属性集为空,或所有样本在所有属性上取值相同,无法划分
决策树生成算法
三种经典的决策树生成算法
- 基于信息增益的ID3算法
-基于信息增益率的C4.5算法
-基于基尼指数的CART算法
CART算法
CART算法构造一棵分类回归树,既可以作分类用,也可作回归用。构造的决策树为二叉树,对于特征取值大于2的需要进行二元划分。
如婚姻状况有三种取值:{单身,已婚,离异},在进行划分时,首先将其划分为{已婚,非已婚}两类,其次再将非已婚划分为{单身,离异}两类。
构造分类决策树
python代码实现
'''
导入训练集数据,注意这里的特征值score为连续型变量
'''
def train_dataset():
dataset = [[10, 'low'],
[26, 'low'],
[37, 'low'],
[27, 'mid'],
[48, 'mid'],
[52, 'low'],
[60, 'mid'],
[72, 'mid'],
[68, 'high'],
[80, 'high'],
[89, 'high'],
[95, 'high']]
features = ['score']
return dataset, features
'''
计算当前集合的Gini系数
'''
def calGini(dataset):
# 得到样本总数
data_count = len(dataset)
labels = {}
for data in dataset:
# 统计标签出现的次数
if data[-1] not in labels:
labels[data[-1]] = 1
else:
labels[data[-1]] += 1
# 计算当前集合中各标签样本数出现的概率
for label in labels:
labels[label] /= data_count
labels[label] = labels[label] * labels[label]
# 计算按照当前方式分类的Gini系数
Gini = 1 - sum(labels.values())
return Gini
'''
分割样本集,按照特征值index可能的取值value,将样本集分成取值为value和取值不为value的两部分
二分法构造二叉树
'''
def split_dataset(dataset, index, value):
sub_dataset1 = []
sub_dataset2 = []
for data in dataset:
# 离散值划分:等于value / 不等于value
# 连续值划分:<= value / > value
if data[index] <= value:
# 注意特征值要再用来划分(cart算法可以重复利用特征值)
# current_list = data[: index]
# current_list.extend(data[index + 1:])
sub_dataset1.append(data)
else:
# current_list = data[: index]
# current_list.extend(data[index + 1:])
sub_dataset2.append(data)
return sub_dataset1, sub_dataset2
'''
选择最佳的划分特征属性和对应属性值
'''
def choose_best_feature(dataset):
# 特征值个数
feature_count = len(dataset[0]) - 1
# 定义最佳基尼系数、最优特征的索引、最优划分点
bestGini = 1
index_of_best_feature = -1
best_split_point = 0.0
for i in range(feature_count):
# 当取值为离散型变量时:得到当前特征属性的所有取值(去重,每个取值只出现一次),如婚姻状况的三种取值:单身、已婚、离异
# colValue = set(data[i] for data in dataset)
# 当取值为连续型变量时:得到当前特征属性的所有取值,从小到大进行排序
colValueRow = list(data[i] for data in dataset)
colValueRow.sort()
colValue = []
for j in range(len(colValueRow) - 1):
# 对排序后的取值,取相邻两个值的平均数,
# 如对特征值序列{10,20,30},划分点为<= 15, >15, <=25, >25
colValue.append((colValueRow[j] + colValueRow[j + 1]) / 2)
# 构建Gini字典, key为特征属性的取值,value为对应的gini系数
Gini = {}
for v in colValue:
sub_dataset1, sub_dataset2 = split_dataset(dataset, i, v)
prob1 = len(sub_dataset1) / len(dataset)
prob2 = len(sub_dataset2) / len(dataset)
sub_gini1 = calGini(sub_dataset1)
sub_gini2 = calGini(sub_dataset2)
# 由当前切分点划分后的基尼指数
Gini[v] = prob1 * sub_gini1 + prob2 * sub_gini2
# 更新最优划分点和最优特征
if Gini[v] < bestGini:
bestGini = Gini[v]
index_of_best_feature = i
best_split_point = v
print(Gini)
return index_of_best_feature, best_split_point
'''
构造决策树
1. 递归停止的条件:
> 对于当前节点的数据集,样本个数小于阈值或者没有特征,则返回决策子树,停止递归
>
3. 算计当前节点各个特征属性的取值的gini系数,选择最优特征和最优切分点,划分样本为两部分,生成两个子节点
4. 对子节点递归调用决策树构造函数,生成CART分类树
'''
def create_decision_tree(dataset, features, i, labelValues):
labels = [data[-1] for data in dataset]
# 若当前集合中所有样本标签相等,返回标签值
if labels.count(labels[0]) == len(labels):
labelValues.remove(labels[0])
return labels[0]
# 若当前集合重样本大部分被分纯(阈值),返回标签值
for label in labels:
if labels.count(label) >= 0.8 * len(labels):
labelValues.remove(label)
return label
# 这里i代表了决策树的高度,当决策树分类到指定高度时,递归停止
if i == 0:
return labelValues[0]
# 开始构建决策树
index_of_best_feature, best_split_point = choose_best_feature(dataset)
best_feature = features[index_of_best_feature]
# 初始化决策树
decision_tree = {best_feature: {}}
# CART可以重复使用特征值,只要能利用该特征继续进行划分就可以使用
sub_dataset1, sub_dataset2 = split_dataset(dataset, index_of_best_feature, best_split_point)
# 递归调用,构建子树,左子树是等于切分点值的,右子树是不等于切分点的
decision_tree[best_feature][best_split_point] = create_decision_tree(sub_dataset1, features, i - 1, labelValues)
decision_tree[best_feature]['others'] = create_decision_tree(sub_dataset2, features, i - 1, labelValues)
return decision_tree
'''
用训练好的决策树对新样本进行分类
'''
def classify(decision_tree, features, test_example):
# 根节点代表的属性
root_feature = list(decision_tree.keys())[0]
# 第一个分类属性的值
second_dict = decision_tree[root_feature]
classLabel = ''
# 判断该属性值是属性中的第几个
index_of_first_feature = features.index(root_feature)
for key in second_dict.keys():
if key != 'others':
if test_example[index_of_first_feature] <= key:
# 若当前属性值是字典,则递归查询
if type(second_dict[key]).__name__ == 'dict':
classLabel = classify(second_dict[key], features, test_example)
else:
classLabel = second_dict[key]
else:
if isinstance(second_dict['others'], str):
classLabel = second_dict['others']
else:
classLabel = classify(second_dict['others'], features, test_example)
return classLabel
'''
对测试集进行分类,统计各标签下的数据量
'''
def getClassifyResult(test_dataset):
dataset, features = train_dataset()
labelValues = ['low', 'mid', 'high']
decision_tree = create_decision_tree(dataset, features, 2, labelValues)
labels = {}
for example in test_dataset:
label = classify(decision_tree, features, example)
if label not in labels:
labels[label] = 0
labels[label] += 1
return labels
if __name__ == '__main__':
test_example = [[63]]
print(getClassifyResult(test_example))
测试集为score=63,得到结果为:分类为mid
决策树可视化
上面只能看到分类的一个结果,更加直观的方式为画出决策树。
import matplotlib.pyplot as plt
import CART
# 设置显示中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
decisionNode = dict(boxstyle="sawtooth", fc="0.8")
leafNode = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-")
def plotNode(nodeTxt, centerPt, parentPt, nodeType):
# matplotlib.pyplot.annotate(text, xy, *args, **kwargs
# text:文本注释
# xy: 被指向的数据点(x,y)的位置坐标
# xytext: 注释文本的坐标点
# textcoords / xtcoords: 注释文本的坐标系属性
createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction',
xytext=centerPt, textcoords='axes fraction',
va="center", ha="center", bbox=nodeType, arrowprops=arrow_args)
def getNumLeafs(myTree):
numLeafs = 0
firstStr = list(myTree.keys())[0]
secondDict = myTree[firstStr]
for key in secondDict.keys():
if type(secondDict[key]).__name__ == 'dict':
numLeafs += getNumLeafs(secondDict[key])
else:
numLeafs += 1
return numLeafs
def getTreeDepth(myTree):
maxDepth = 0
firstStr = list(myTree.keys())[0]
secondDict = myTree[firstStr]
for key in secondDict.keys():
if type(secondDict[key]).__name__ == 'dict':
thisDepth = 1 + getTreeDepth(secondDict[key])
else:
thisDepth = 1
if thisDepth > maxDepth: maxDepth = thisDepth
return maxDepth
# 在父子节点之间填充文本信息
def plotMidText(cntrPt, parentPt, txtString):
xMid = (parentPt[0] - cntrPt[0]) / 2.0 + cntrPt[0]
yMid = (parentPt[1] - cntrPt[1]) / 2.0 + cntrPt[1]
createPlot.ax1.text(xMid, yMid, txtString)
def plotTree(myTree, parentPt, nodeTxt):
numLeafs = getNumLeafs(myTree)
depth = getTreeDepth(myTree)
firstStr = list(myTree.keys())[0]
cntrPt = (plotTree.xOff + (1.0 + float(numLeafs)) / 2.0 / plotTree.totalW, plotTree.yOff)
# 标记子节点的属性值
plotMidText(cntrPt, parentPt, nodeTxt)
plotNode(firstStr, cntrPt, parentPt, decisionNode)
secondDict = myTree[firstStr]
# 计算子节点的y偏移
plotTree.yOff = plotTree.yOff - 1.0 / plotTree.totalD
for key in secondDict.keys():
if type(secondDict[key]).__name__ == 'dict':
plotTree(secondDict[key], cntrPt, str(key))
else:
# 计算子节点的x偏移
plotTree.xOff = plotTree.xOff + 1.0 / plotTree.totalW
plotNode(secondDict[key], (plotTree.xOff, plotTree.yOff), cntrPt, leafNode)
plotMidText((plotTree.xOff, plotTree.yOff), cntrPt, str(key))
plotTree.yOff = plotTree.yOff + 1. / plotTree.totalD
def createPlot(inTree):
# 创建新图形
fig = plt.figure(1, facecolor='white')
# 清空绘图区
fig.clf()
axprops = dict(xticks=[], yticks=[])
# 定义绘图区
createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)
plotTree.totalW = float(getNumLeafs(inTree))
plotTree.totalD = float(getTreeDepth(inTree))
plotTree.xOff = -0.5 / plotTree.totalW
plotTree.yOff = 1.0
# 绘制树
plotTree(inTree, (0.5, 1.0), '')
plt.show()
得到绘图的结果为:
上图的划分结果为:
score <= 26.5 : low
26.5 < score <= 64: mid
score > 64: high
从而印证了上述代码分类结果的正确性。
sklearn构造决策树
除手动构建决策树外,sklearn提供了构造决策树的函数,能够快速构造出CART决策树。
def train(x, y):
# 训练集和测试集划分
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2)
# 建立决策树模型
model = tree.DecisionTreeClassifier(
criterion='gini', # 采用gini还是entropy进行特征选择
max_depth=2, # 树的最大深度
min_samples_split=2, # 内部节点分裂所需要的最小样本数量
min_samples_leaf=1, # 叶子节点所需要的最小样本数量
max_features=None # 寻找最优分割点时的最大特征数
)
# 对模型进行训练
model.fit(x_train, y_train)
# 预测,比较结果
print("预测结果准确性:")
print(model.score(x_test, y_test))
return model
'''
以dot形式导出决策树
可采用以下命令生成图形渲染:
dot -Tpng tree.dot -o tree.png
或者直接调用drawTree函数,得到Sourve.gv.pdf
'''
def drawTree(model, features, labels):
with open("tree.dot", 'w') as f:
f = tree.export_graphviz(model,
out_file=f,
feature_names=features,
class_names=labels)
with(open("tree.dot")) as f:
dot_graph = f.read()
graph = graphviz.Source(dot_graph)
graph.view()
画出的决策树如下图(PS:这里画的树数据集改变了,所以与上面分类结果不同)
参数解读:
- 内部分支上第一行为分类点
- gini / entropy为该节点的对应gini系数/信息熵
- samples为该分类下样本的容量
- value表示该分类下各个label中包含的样本数量
example: value=[1,3,0]表示该分类中包含1个标签为low,3个标签为mid的样本 - class主要显示出容量多的样本
参考内容:
【1】https://zhuanlan.zhihu.com/p/32164933
【2】
【3】Peter Harrington.Machine Learning in Action(《机器学习实战》)[M].北京:人民邮电出版社,2013