背景
在使用决策树模型时,如果训练集中的样本数很多,则会使得生成的决策树过于庞大,即分化出了很多的枝节。这时会产生过拟合问题,也就是在模型在训练集上的表现效果良好,而在测试集的效果却很差。因此在生成一棵决策树之后,需要对它进行必要的剪枝,从而提高它的泛化能力。本文将讲述后剪枝算法——REP方法。
原理
剪枝是指将决策树的一些枝节去掉,将中间节点变成叶子节点,该叶子节点的预测值便是该分组训练样本 y y y值的均值。剪枝算法分为预剪枝和后剪枝,预剪枝是在决策树生成的过程中同步进行,而后剪枝是在决策树生成完之后再剪枝。
REP方法也称为错误率降低剪枝,它是一类最基础、最简单的后剪枝算法,是其他剪枝算法的基础。主要过程是将训练集分为两个集合 N 1 N_1 N1和 N 2 N_2 N2,可以称为训练集中的训练集和训练集中的验证集。 N 1 N_1 N1用来生成决策树, N 2 N_2 N2用来验证剪枝前后的模型效果。具体是先用 N 1 N_1 N1来生成决策树,然后自底向上遍历所有中间节点,对于每个中间节点,比较剪枝前后的两棵决策树在验证集 N 2 N_2 N2上的效果,这个效果体现在 N 2 N_2 N2通过两棵决策树得到的预测值与原始实际值的误差平方和,若剪枝后的误差平方和更小,则对决策树进行剪枝,反之则不进行剪枝。
例子
假设通过对数据集
N
1
N_1
N1进行训练,得到了如下的决策树
现在要自底向上进行剪枝,对象是中间节点,对应到图中依次是节点5、2、3。对于节点5,先将它的左右枝8和9剪掉,得到剪枝前和剪枝后的两棵树
将数据集
N
2
N_2
N2的特征
x
x
x分别代入这两棵树,得到两组预测值,然后通过比较两组数据的误差平方和来决策是否进行剪枝。之后再考虑节点2,最后考虑节点3。
程序实现
重新定义树结构
为了方便处理不同节点间的调用,CART回归树的树模型不再用字典进行存储,而改用自定义的类对象(参考leetcode中的树节点),每个节点可以通过成员变量来调用分裂出来的左右节点。
class TreeNode:
def __init__(self, val, fea_name=None, fea_c=None):
self.left = None
self.right = None
self.val = round(val,2)
self.fea_name = fea_name
self.fea_c = fea_c if fea_c is None else round(fea_c,2)
变量 v a l val val是指当前节点数据集的 y y y值的均值,若当前节点是叶子节点,则该变量代表这种分支的预测值,若是中间节点,则可以表示为对该节点进行剪枝后的节点预测值。
生成剪枝后的子树
def sub_tree(tree, num): # 返回后序剪枝得到的子树
stack = [(False, tree)]
while stack:
flag, t = stack.pop()
if not t:continue
if flag:
if t.left or t.right:
if num==0:
t.left = None
t.right = None
return tree
else:
num -= 1
else:
stack.append((True, t))
stack.append((False, t.right))
stack.append((False, t.left))
return tree
采用后序遍历的方式来搜索中间节点,参数 n u m num num是为了控制对应序号的中间节点,因为并不是剪去每个中间节点都能提高性能,通过 n u m num num可以避开不想剪去的中间节点。
计算中间节点的个数
def mid_leaf_num(tree):
if not tree or (not tree.left and not tree.right):
return 0
return 1 + mid_leaf_num(tree.left) + mid_leaf_num(tree.right)
效果比较函数
def ifmore(self, temp_tree, test_x, test_y):
orig_ = []
temp_ = []
for i in range(len(test_x)):
orig_.append(self.check(self.tree, test_x[i]))
temp_.append(self.check(temp_tree, test_x[i]))
orig_sum = sum(np.power(np.array(orig_)-test_y, 2))
temp_sum = sum(np.power(np.array(temp_)-test_y, 2))
if orig_sum>temp_sum: # and (orig_sum-temp_sum)/orig_sum>0.0001:
self.tree = temp_tree
return True
else:
return False
REP剪枝函数
def prune_tree(self, test_x, test_y):
mid_num = mid_leaf_num(self.tree)
i = 0
while i<mid_num:
temp_tree = sub_tree(self.tree, i)
if self.ifmore(temp_tree, test_x, test_y):
i = 0
mid_num -= 1
else:
i += 1
实例化演示
X,y = make_regression(n_samples=1000, n_features=4, noise=0.1)
X_name = np.array(list('abcd'))
clf = Tree_Regress()
train_x, test_x, train_y, test_y = train_test_split(X, y, test_size=0.30)
train_size = len(train_x)//4
train_test_x, train_test_y = train_x[:train_size], train_y[:train_size]
train_train_x, train_train_y = train_x[train_size:], train_y[train_size:]
print('不剪枝')
clf.fit(train_x, train_y, X_name)
predict_y = clf.predict(test_x)
pre_error = sum(np.power(test_y-predict_y,2))
print('误差为:', pre_error,' 节点数:', clf.node_num())
print('有剪枝')
clf.fit(train_train_x, train_train_y, X_name)
clf.prune_tree(train_test_x, train_test_y)
predict_y = clf.predict(test_x)
pre_error = sum(np.power(test_y-predict_y,2))
print('误差为:', pre_error,' 节点数:', clf.node_num())
# 不剪枝
# 误差为: 9860.41223121167 节点数: 249
# 有剪枝
# 误差为: 6892.04066950184 节点数: 33
在对模型进行后剪枝之后,模型的泛化能力有所提升。
不足
REP方法虽然在一定程度上简化了决策树,提高了模型的性能,但在有些情况下反而会造成相反的结果,使得模型表现更差。我试了几个不同的数据集,发现效果其实不是很好,可能是这个方法考虑到的东西比较少,它单单考虑到了模型拟合的误差平方和,却没考虑生成的节点个数,粗暴地将影响模型性能的枝节都剪去,使得模型太过简单。另外,该方法的计算开销是很大的,需要遍历搜索两次中间节点。