CART是决策树分类器的一种,它的独特之处在于可以处理分类问题也可以处理回归问题,具体处理哪类问题是根据关注的变量是类别型还是连续型而定的。
选择分裂属性的标准:
CART和ID3和C4.5在评价标准的选取上存在着差别。ID3使用的纯度衡量指标是信息增益,C4.5使用的是信息增益比,而CART使用的是Gini(分类)和均方差(回归)。对于回归问题,传统的信息熵标度确实不是一个很好的评价标准(相比于最小均方差),但是在分类问题中,为什么要用Gini系数代替信息增益比呢?这样做实际上是为了减少运算的复杂度,熵模型是对数运算而Gini系数是二次运算,同时基尼系数和熵之半的曲线是非常近似的,所以可以作为一个运算代价更小的替代指标。
离散属性的分裂
因为CART生成的决策树是一个二叉树,所以它会把该属性下的所有离散值分为两组,在所有可能的分组方案中找到Gini系数最小的那一个。
连续属性的分裂
CART对于连续型属性的处理方式和C4.5差不多,它是通过将所有出现在数据集中的值都作为一个类别,然后根据大小进行分割。最后,根据均方差最小原则来选取最优的分裂位置。
剪枝的标准
CART采用CCP(代价复杂度)剪枝方法。代价复杂度选择节点表面误差率增益值最小的非叶子节点,删除该非叶子节点的左右子节点,若有多个非叶子节点的表面误差率增益值相同小,则选择非叶子节点中子节点数最多的非叶子节点进行剪枝。
算法流程图
def pre_splite(index, value, dataset):
"""
将一个数据集根据某一个属性的指定值进行划分
@index: 指定属性
@value: 指定划分的阈值
@dataset: 数据集
"""
left, right = [], []
for row in dataset:
if row[index] < value:
left.append(row)
else:
right.append(row)
return left, right
def gini_index(groups, classes):
"""
计算被划分的子集的基尼系数
@groups: 划分后的数据集
@classes: 类别集合
"""
# 计算全集的大小
n_instances = float(sum([len(group) for group in groups]))
# 求每一个子集的加权基尼系数
gini = 0.0
for group in groups:
size = float(len(group))
# 注意:分母不能为0
if size == 0:
continue
score = 0.0
for val in classes:
p = [row[-1] for row in group].count(val) / size
score += p * p
gini += (1.0 - score) * (size / n_instances)
return gini
def get_split(dataset):
"""
找到最好的分割点
@dataset: 数据集
"""
class_values = list(set(row[-1] for row in dataset))
b_index, b_value, b_score, b_groups = float('inf'), float('inf'), float('inf'), None
for index in range(len(dataset[0])-1):
for row in dataset:
groups = pre_splite(index, row[index], dataset)
gini = gini_index(groups, class_values)
if gini < b_score:
b_index, b_value, b_score, b_groups = index, row[index], gini, groups
return {'index': b_index, 'value': b_value, 'groups': b_groups} # 以dict形式返回结果
def to_terminal(group):
"""
创造一个叶子节点输出值
@groups: 组
"""
# print(group)
outcomes = [row[-1] for row in group]
return max(set(outcomes), key=outcomes.count)
def split(node, max_depth, min_size, depth):
"""
树的分叉
@node: 当前所在的节点
@max_depth: 最大深度
@min_size: 停止拆分的数目(终止条件)
@depth: 深度
"""
left, right = node['groups']
# print(left, right)
del(node['groups']) # 删掉
if not left or not right:
# 已经到达叶子
node['left'] = node['right'] = to_terminal(left+right)
return
if depth >= max_depth:
node['left'], node['right'] = to_terminal(left), to_terminal(right)
return
if len(left) <= min_size:
node['left'] = to_terminal(left)
else:
node['left'] = get_split(left)
split(node['left'], max_depth, min_size, depth+1)
if len(right) <= min_size:
node['right'] = to_terminal(right)
else:
node['right'] = get_split(right)
split(node['right'], max_depth, min_size, depth+1) # 递归调用,深度优先
def build_tree(train, max_depth, min_size):
"""
创建决策树
@train: 训练集
@max_depth: 最大深度
@min_size: 停止拆分的数目(终止条件)
"""
train = list(train.reshape((-1,5)))
root = get_split(train)
split(root, max_depth, min_size, 1) # 激活递归
return root
def tree_prediction(node, row):
"""
使用决策树来进行预测
@node: 一个新的数据点
@row: 当前所在层
"""
if row[node['index']] < node['value']:
if isinstance(node['left'], dict):
# 如果左子树尚未到达叶子则继续递归
return tree_prediction(node['left'], row)
else:
return node['left']
else:
if isinstance(node['right'], dict):
return tree_prediction(node['right'], row)
else:
return node['right']
def decision_tree(train, test, max_depth, min_size):
"""
决策树算法
@train: 训练集
@test: 测试集
@max_depth: 最大深度
@min_size: 终止条件
"""
tree = build_tree(train, max_depth, min_size)
predictions = []
for row in test:
prediction = tree_prediction(tree, row)
predictions.append(prediction)
return(predictions)
def print_tree(node, depth=0):
"""
暴力打印结果
@node: 节点
@depth: 当前深度
"""
if isinstance(node, dict):
print('%s[X%d < %.3f]' % ((depth*' ', (node['index']+1), node['value'])))
print_tree(node['left'], depth+1)
print_tree(node['right'], depth+1)
else:
print('%s[%s]' % ((depth*' ', node)))
seed(1)
filename = 'iris.csv'
dataset = pd.read_csv(filename).values
str_column_to_int(dataset, len(dataset[0])-1)
max_depth = 10
min_size = 1
n_folds = 3
scores = evaluate_algorithm(dataset, decision_tree, n_folds, max_depth, min_size)
print('某个折上的准确率: %s' % scores)
print('算法的平均准确率: %.3f%%' % (sum(scores)/float(len(scores))))
结果:
{'Iris-versicolor': 0, 'Iris-virginica': 1, 'Iris-setosa': 2}
某个折上的准确率: [92.0, 98.0, 96.0]
算法的平均准确率: 95.333%
因为交叉检验了三次,所以有三棵决策树生成。
使用sklearn进行可视化(自己造图实在是太困难了):
from sklearn import tree
from sklearn.datasets import load_iris
import pydotplus
from IPython.display import Image, display
# 载入数据
iris = load_iris()
# 构建决策树
clf = tree.DecisionTreeClassifier(max_depth=5)# 深度为4层时有1个结果仍然模糊,5层就足够
clf = clf.fit(iris.data, iris.target)
# 数据可视化
dot_data = tree.export_graphviz(clf,
out_file = None,
feature_names = iris.feature_names,
class_names = iris.target_names,
filled=True,
rounded=True
)
graph = pydotplus.graph_from_dot_data(dot_data)
display(Image(graph.create_png()))
fig = make_subplots(rows=1, cols=2, subplot_titles=("Change folds", "Change minimum size"))
scores, index, acc = [], [], []
max_depth = 10
min_size = 1
for i in range(2, 22):
score = evaluate_algorithm(dataset, decision_tree, i, max_depth, min_size)
scores.append(list(score))
acc.append(sum(score)/float(len(score)))
index.append([i for j in range(i)])
fig.append_trace(go.Scatter(x=[i + 2 for i in range(20)], y=acc,
mode='lines+markers',
name='mean'), row=1, col=1)
fig.append_trace(go.Scatter(x=sum(index, []), y=sum(scores, []),
mode='markers',
name='each'), row=1, col=1)
scores, index, acc = [], [], []
for j in range(1, 11):
score = evaluate_algorithm(dataset, decision_tree, 5, max_depth, j)
scores.append(list(score))
acc.append(sum(score)/float(len(score)))
index.append(j)
fig.append_trace(go.Scatter(x=[i + 1 for i in range(10)], y=acc,
mode='lines+markers',
name='mean-acc'), row=1, col=2)
fig.update_layout(height=600, width=1200, template='none')
fig.update_yaxes(title_text="Accuracy", row=1, col=1)
fig.update_yaxes(title_text="Accuracy", row=1, col=2)
fig.update_xaxes(title_text="Folds Num", row=1, col=1)
fig.update_xaxes(title_text="Minimum Size", row=1, col=2)
fig.show()