为了了解sklearn的API,以及决策树的工作原理,本文以经典的花分类问题为例,编写代码并讲解。最后深入源代码查看其实现
关键词:决策树、基尼系数、决策树可视化、特征重要性。
首先要准备数据集,并调用sklearn的API训练决策树。
from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier
from sklearn import tree
import matplotlib.pyplot as plt
iris = load_iris()
print("feature names", iris.feature_names)
print("target names", iris.target_names)
X = iris.data[:, 2:]
y = iris.target
print("data shape", iris.data.shape)
print("X shape", X.shape)
输出如下,每个样本有4个特征,且标签有3种取值。
feature names ['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']
target names ['setosa' 'versicolor' 'virginica']
取出特征集和标签集。本案例每个样本只选取后两个特征"petal length"和"petal width"(通过切片方式iris.data[:, 2:]
)。
X = iris.data[:, 2:]
y = iris.target
print("data shape", iris.data.shape)
print("X shape", X.shape)
尝试输出特征集如下:
data shape (150, 4)
的含义是,原本数据集有150个样本,每个样本原本有4个特征。X shape (150, 2)
的含义是,由于每个样本只取后两个特征,其列数只为2。data shape (150, 4)
X shape (150, 2)
之后,创建决策树并拟合,这里设置了最大深度为2,限制决策树的高度最多为2。
tree_clf = DecisionTreeClassifier(max_depth=2)
tree_clf.fit(X, y)
绘制决策树图片。sklearn提供了plot_tree
的接口。
fig = plt.figure(figsize=(25,20))
_ = tree.plot_tree(
tree_clf,
feature_names=iris.feature_names[2:],
class_names=iris.target_names,
filled=True
)
# Save picture
fig.savefig("decistion_tree.png")
调用tree_clf.feature_importances_
可以输出决策树的特征重要性:
print("feature_importances_", tree_clf.feature_importances_)
其输出如下,两个特征的重要性分别是0.0和1.0,为什么这样呢?
feature_importances_ [0. 1.]
回看决策树的图,可以发现两个树中节点的判断条件分别是"petal width <= 0.8"和"petal width <= 1.75",也就是说,只用到了"petal width"这个属性,而没用到"petal length"属性。
另外,可以检验图中节点的基尼系数。取绿色的树节点为例,样本数为54,判断成3种类别的样本数分别是0、49、5。
根据基尼系数公式,计算得到 1 − ( 49 / 54 ) 2 − ( 5 / 54 ) 2 = 0.168 1-(49/54)^2-(5/54)^2=0.168 1−(49/54)2−(5/54)2=0.168。
决策树的基尼系数公式此处不再赘述,不是本文重点
上一例生成的决策树节点只用到了pedal width属性,从而难以校验特征重要性的计算公式。本例把决策树的最大深度从2改为3,鼓励其使用到两种属性。
tree_clf = DecisionTreeClassifier(max_depth=3)
其生成的决策树可视化如下:
输出的两个特征的特征重要性分别是0.58和0.41
feature_importances_ [0.58561555 0.41438445]
本节将对这两个数据做验证。两种属性各个取值的基尼系数增益如下
所以,属性的基尼系数增益之和,为其各个划分点的基尼系数加权和。
两者作归一化后,得到0.36 / (0.36+0.25)=0.59,似乎与输出有一点偏差,这是由于舍去小数位末尾导致的。
合并公式计算并观察可知,加权系数之间的分子分母可以消除。
也就是说 ( 0.043 − 0.444 ∗ 3 46 − 0.0 ∗ 43 46 ) ∗ 46 / 150 = 0.043 ∗ 46 / 150 − 0.444 ∗ 3 / 150 − 0.0 ∗ 43 / 150 = 0.0043 (0.043-0.444 * \frac{3}{46} - 0.0 * \frac{43}{46})*46/150=0.043*46/150-0.444*3/150-0.0*43/150=0.0043 (0.043−0.444∗463−0.0∗4643)∗46/150=0.043∗46/150−0.444∗3/150−0.0∗43/150=0.0043
以此法引用于每个划分点,可以计算得到另外几项:
所以,两个属性的基尼系数增益之和为0.367、和0.259,归一化得到0.586和0.414,非常接近于程序输出。
由此,我们可以得出结论,计算某属性的特征重要性,首先要求各个特征值的基尼系数增益,再各自乘以全局加权系数,并求和,
建议先阅读参考文章:
在sklearn,特征重要性的计算核心函数是cpython文件_tree.pyx的
的compute_feature_importances
。
cpdef compute_feature_importances(self, normalize=True):
"""Computes the importance of each feature (aka variable)."""
cdef Node* left
cdef Node* right
cdef Node* nodes = self.nodes
cdef Node* node = nodes
cdef Node* end_node = node + self.node_count
cdef double normalizer = 0.
cdef np.ndarray[np.float64_t, ndim=1] importances
importances = np.zeros((self.n_features,))
cdef DOUBLE_t* importance_data = <DOUBLE_t*>importances.data
with nogil:
while node != end_node:
if node.left_child != _TREE_LEAF:
# ... and node.right_child != _TREE_LEAF:
left = &nodes[node.left_child]
right = &nodes[node.right_child]
importance_data[node.feature] += (
node.weighted_n_node_samples * node.impurity -
left.weighted_n_node_samples * left.impurity -
right.weighted_n_node_samples * right.impurity)
node += 1
importances /= nodes[0].weighted_n_node_samples
if normalize:
normalizer = np.sum(importances)
if normalizer > 0.0:
# Avoid dividing by zero (e.g., when root is pure)
importances /= normalizer
return importances
其中,以下代码所做行为就是在计算某特征值的加权基尼系数增益。
importance_data[node.feature] += (
node.weighted_n_node_samples * node.impurity -
left.weighted_n_node_samples * left.impurity -
right.weighted_n_node_samples * right.impurity)
importance_data[node.feature]
的+=
符号代表这个节点的增益值归属于它的所属特征,由于一个特征可能会有多个划分值(比如"petal length<=4.85"和"petal length<=4.95"都属于petal length),所以它们的增益要累加。
.impurity
里的其实就是基尼系数。
weighted_n_node_samples
的含义应该是该节点的全局加权系数,即该节点的样本数除以全局样本数 n n o d e / n t o t a l n_{node}/n_{total} nnode/ntotal。比如对于上一例里右下角三个节点的全局加权系数分别是46/150、3/150、43/150。
importances /= nodes[0].weighted_n_node_samples
的含义是,最后除以根节点的全局加权系数。但笔者认为通常这个值就是1。
如果设置要进行归一化,就最后除以总和,保证各特征值相加为1。
if normalize:
normalizer = np.sum(importances)
if normalizer > 0.0:
# Avoid dividing by zero (e.g., when root is pure)
importances /= normalizer