1.10. 决策树#
决策树 (DTs) 是一种非参数监督学习方法,用于分类和回归。其目标是通过学习从数据特征推断出的简单决策规则,创建一个模型来预测目标变量的值。树可以被视为分段常数逼近。
例如,在下面的例子中,决策树通过学习数据来近似一条正弦曲线,使用一组 if-then-else 决策规则。树越深,决策规则越复杂,模型拟合得越好。
决策树的一些优点包括:
易于理解和解释。树可以被可视化。
需要较少的数据准备。其他技术通常需要数据归一化,创建虚拟变量并删除空白值。某些树和算法组合支持缺失值。
使用树(即预测数据)的成本是用于训练树的数据点数量的对数。
能够处理数值和分类数据。然而,scikit-learn 目前的实现不支持分类变量。其他技术通常专门用于分析只有一种类型变量的数据集。有关更多信息,请参阅算法。
能够处理多输出问题。
使用白盒模型。如果在模型中观察到给定情况,则其原因很容易通过布尔逻辑来解释。相比之下,在黑盒模型(例如,人工神经网络)中,结果可能更难解释。
可以使用统计检验来验证模型。这使得评估模型的可靠性成为可能。
即使数据的生成模型在一定程度上违反了其假设,它也能表现良好。
决策树的缺点包括:
决策树学习器可能创建过于复杂的树,导致数据泛化能力差。这称为过拟合。需要剪枝、设置叶节点所需的最小样本数或设置树的最大深度等机制来避免此问题。
决策树可能不稳定,因为数据中的微小变化可能导致生成完全不同的树。使用集成方法中的决策树可以缓解这个问题。
决策树的预测既不平滑也不连续,而是如上图所示的分段常数逼近。因此,它们不擅长外推。
在几个优化方面,学习最优决策树的问题是 NP-完全的,即使对于简单的概念也是如此。因此,实用的决策树学习算法基于启发式算法,例如贪心算法,其中在每个节点处做出局部最优决策。此类算法不能保证返回全局最优决策树。这可以通过在集成学习器中训练多棵树来缓解,其中特征和样本是随机有替换采样的。
有些概念很难学习,因为决策树不容易表达它们,例如 XOR、奇偶校验或多路复用器问题。
如果某些类别占主导地位,决策树学习器会创建有偏的树。因此,建议在拟合决策树之前平衡数据集。
1.10.1. 分类#
DecisionTreeClassifier 是一个能够对数据集执行多类分类的类。
与其他分类器一样,DecisionTreeClassifier 接受两个数组作为输入:一个形状为 (n_samples, n_features) 的稀疏或密集数组 X,包含训练样本;以及一个形状为 (n_samples,) 的整数值数组 Y,包含训练样本的类别标签。
>>> from sklearn import tree
>>> X = [[0, 0], [1, 1]]
>>> Y = [0, 1]
>>> clf = tree.DecisionTreeClassifier()
>>> clf = clf.fit(X, Y)
拟合后,模型可用于预测样本的类别。
>>> clf.predict([[2., 2.]])
array([1])
如果多个类别具有相同且最高的概率,则分类器将预测在这些类别中索引最低的类别。
作为输出特定类别的替代方案,可以预测每个类别的概率,即叶节点中该类别的训练样本所占的比例。
>>> clf.predict_proba([[2., 2.]])
array([[0., 1.]])
DecisionTreeClassifier 能够进行二元分类(标签为 [-1, 1])和多类分类(标签为 [0, …, K-1])。
使用 Iris 数据集,我们可以按如下方式构建一棵树:
>>> from sklearn.datasets import load_iris
>>> from sklearn import tree
>>> iris = load_iris()
>>> X, y = iris.data, iris.target
>>> clf = tree.DecisionTreeClassifier()
>>> clf = clf.fit(X, y)
一旦训练完成,您可以使用 plot_tree 函数绘制该树。
>>> tree.plot_tree(clf)
[...]
导出树的其他方式#
我们还可以使用 export_graphviz 导出器以 Graphviz 格式导出树。如果您使用 conda 包管理器,可以通过 conda install python-graphviz 安装 graphviz 二进制文件和 python 包。
或者,可以从 graphviz 项目主页下载 graphviz 二进制文件,并从 pypi 使用 pip install graphviz 安装 Python 包装器。
下面是一个使用整个 iris 数据集训练的上述树的 graphviz 导出示例;结果保存在输出文件 iris.pdf 中。
>>> import graphviz
>>> dot_data = tree.export_graphviz(clf, out_file=None)
>>> graph = graphviz.Source(dot_data)
>>> graph.render("iris")
export_graphviz 导出器还支持各种美学选项,包括根据节点的类别(或回归值)着色,以及在需要时使用明确的变量和类别名称。Jupyter notebooks 也会自动内联渲染这些图表。
>>> dot_data = tree.export_graphviz(clf, out_file=None,
... feature_names=iris.feature_names,
... class_names=iris.target_names,
... filled=True, rounded=True,
... special_characters=True)
>>> graph = graphviz.Source(dot_data)
>>> graph
或者,也可以使用函数 export_text 将树导出为文本格式。此方法不需要安装外部库,并且更紧凑。
>>> from sklearn.datasets import load_iris
>>> from sklearn.tree import DecisionTreeClassifier
>>> from sklearn.tree import export_text
>>> iris = load_iris()
>>> decision_tree = DecisionTreeClassifier(random_state=0, max_depth=2)
>>> decision_tree = decision_tree.fit(iris.data, iris.target)
>>> r = export_text(decision_tree, feature_names=iris['feature_names'])
>>> print(r)
|--- petal width (cm) <= 0.80
| |--- class: 0
|--- petal width (cm) > 0.80
| |--- petal width (cm) <= 1.75
| | |--- class: 1
| |--- petal width (cm) > 1.75
| | |--- class: 2
示例
1.10.2. 回归#
决策树也可以应用于回归问题,使用 DecisionTreeRegressor 类。
与分类设置一样,fit 方法将接受数组 X 和 y 作为参数,只是在这种情况下,y 预期具有浮点值而不是整数值。
>>> from sklearn import tree
>>> X = [[0, 0], [2, 2]]
>>> y = [0.5, 2.5]
>>> clf = tree.DecisionTreeRegressor()
>>> clf = clf.fit(X, y)
>>> clf.predict([[1, 1]])
array([0.5])
示例
1.10.3. 多输出问题#
多输出问题是一个监督学习问题,有多个输出需要预测,即当 Y 是形状为 (n_samples, n_outputs) 的二维数组时。
当输出之间没有相关性时,解决此类问题的一种非常简单的方法是构建 n 个独立的模型,即每个输出一个模型,然后使用这些模型独立预测 n 个输出中的每一个。然而,由于与相同输入相关的输出值本身很可能相关,因此通常更好的方法是构建一个能够同时预测所有 n 个输出的单个模型。首先,这需要更低的训练时间,因为只构建了一个估计器。其次,所得估计器的泛化准确性通常可以提高。
对于决策树,这种策略可以很容易地用于支持多输出问题。这需要进行以下更改:
在叶节点中存储 n 个输出值,而不是 1 个;
使用计算所有 n 个输出平均减少量的拆分标准。
该模块通过在 DecisionTreeClassifier 和 DecisionTreeRegressor 中实现此策略来支持多输出问题。如果决策树在形状为 (n_samples, n_outputs) 的输出数组 Y 上拟合,则所得估计器将:
在
predict时输出 n_output 个值;在
predict_proba时输出 n_output 个类别概率数组的列表。
在 决策树回归 中演示了用于回归的多输出树的使用。在此示例中,输入 X 是单个实数值,输出 Y 是 X 的正弦和余弦。
在 使用多输出估计器完成人脸 中演示了用于分类的多输出树的使用。在此示例中,输入 X 是人脸上半部分的像素,输出 Y 是这些人脸下半部分的像素。
示例
References
M. Dumont et al, Fast multi-class image annotation with random subwindows and multiple output randomized trees, International Conference on Computer Vision Theory and Applications 2009
1.10.4. 复杂度#
下表显示了平衡二叉树的最坏情况复杂度估计值:
拆分器 |
总训练成本 |
总推断成本 |
|---|---|---|
“best” |
\(\mathcal{O}(n_{features} \, n^2_{samples} \log(n_{samples}))\) |
\(\mathcal{O}(\log(n_{samples}))\) |
“random” |
\(\mathcal{O}(n_{features} \, n^2_{samples})\) |
\(\mathcal{O}(\log(n_{samples}))\) |
一般来说,在每个节点上构建平衡二叉树的训练成本为:
第一项是 \(n_{samples}\) 排序重复 \(n_{features}\) 次的成本。第二项是线性扫描候选拆分点以找到提供最大杂质减少量的特征。对于贪心拆分器策略“best”来说,后者是次要的,因此通常被忽略。
无论拆分策略如何,在对所有内部节点的成本求和后,总复杂度与 \(n_{nodes}=n_{leaves}-1\) 成线性关系,在最坏情况下,即当树生长到每个样本都位于自己的叶节点时,复杂度为 \(\mathcal{O}(n_{samples})\)。
许多实现(例如 scikit-learn)使用高效的缓存技巧来跟踪每个节点索引的总体顺序,这样就不需要在每个节点重新排序特征;因此,这些实现的时间复杂度仅为 \(\mathcal{O}(n_{features}n_{samples}\log(n_{samples}))\) [1]。
推断成本与拆分器策略无关。它仅取决于树的深度 \(\mathcal{O}(\text{depth})\)。在近似平衡的二叉树中,每次拆分将数据减半,然后这种减半的次数随着深度呈指数增长。如果这个过程持续到每个样本都孤立在自己的叶节点中,则得到的深度为 \(\mathcal{O}(\log(n_{samples}))\)。
References
1.10.5. 实际使用技巧#
决策树倾向于对具有大量特征的数据集过拟合。正确处理样本数与特征数的比例非常重要,因为在高维空间中样本较少的树非常容易过拟合。
了解决策树结构将有助于深入了解决策树如何进行预测,这对于理解数据中的重要特征非常重要。
在训练时使用
export函数可视化您的树。使用max_depth=3作为初始树深度来感受树如何拟合您的数据,然后增加深度。请记住,树每增加一层,填充树所需的样本数就会增加一倍。使用
max_depth来控制树的大小以防止过拟合。使用
min_samples_split或min_samples_leaf来确保树中的每个决策都由多个样本提供信息,通过控制哪些拆分将被考虑。一个非常小的数字通常意味着树会过拟合,而一个大的数字会阻止树学习数据。尝试min_samples_leaf=5作为初始值。如果样本大小变化很大,可以在这两个参数中使用浮点数作为百分比。虽然min_samples_split可以创建任意小的叶节点,但min_samples_leaf保证每个叶节点都具有最小大小,避免在回归问题中出现低方差、过拟合的叶节点。对于只有少量类别的分类,min_samples_leaf=1通常是最佳选择。请注意,
min_samples_split直接考虑样本,并且独立于sample_weight(如果提供)(例如,一个具有 m 个加权样本的节点仍被视为恰好有 m 个样本)。如果需要在拆分时考虑样本权重,请考虑min_weight_fraction_leaf或min_impurity_decrease。在训练之前平衡数据集,以防止树偏向于占主导地位的类别。类别平衡可以通过从每个类别中抽取相同数量的样本来完成,或者优选通过将每个类别的样本权重 (
sample_weight) 总和归一化到相同的值。另请注意,基于权重的预剪枝标准(例如min_weight_fraction_leaf)将比不了解样本权重的标准(例如min_samples_leaf)对占主导地位的类别的偏见更小。如果样本被加权,使用基于权重的预剪枝标准(例如
min_weight_fraction_leaf)来优化树结构会更容易,这确保叶节点包含至少一小部分总样本权重。所有决策树内部都使用
np.float32数组。如果训练数据不是此格式,将创建数据集的副本。如果输入矩阵 X 非常稀疏,建议在调用 fit 之前将其转换为稀疏
csc_matrix,并在调用 predict 之前将其转换为稀疏csr_matrix。当特征在大多数样本中具有零值时,稀疏矩阵输入的训练时间可以比密集矩阵快几个数量级。
1.10.6. 树算法:ID3、C4.5、C5.0 和 CART#
所有各种决策树算法是什么,它们之间有何不同?scikit-learn 中实现了哪一个?
各种决策树算法#
ID3 (Iterative Dichotomiser 3) 由 Ross Quinlan 于 1986 年开发。该算法创建一个多叉树,对于每个节点(即以贪心方式),找到将为分类目标产生最大信息增益的分类特征。树被生长到最大大小,然后通常应用剪枝步骤来提高树对未见数据的泛化能力。
C4.5 是 ID3 的继任者,它通过动态定义一个离散属性(基于数值变量)来将连续属性值划分为离散区间集,从而消除了特征必须是分类的限制。C4.5 将训练好的树(即 ID3 算法的输出)转换为一组 if-then 规则。然后评估每个规则的准确性以确定它们的应用顺序。剪枝通过删除规则的先决条件来完成,如果删除后规则的准确性提高。
C5.0 是 Quinlan 的最新版本,以专有许可证发布。它使用更少的内存,构建的规则集比 C4.5 小,同时更准确。
CART (Classification and Regression Trees) 与 C4.5 非常相似,但不同之处在于它支持数值目标变量(回归)并且不计算规则集。CART 使用在每个节点产生最大信息增益的特征和阈值来构建二叉树。
scikit-learn 使用 CART 算法的优化版本;然而,scikit-learn 实现目前不支持分类变量。
1.10.7. 数学公式#
给定训练向量 \(x_i \in R^n\), i=1,…, l 和一个标签向量 \(y \in R^l\),决策树递归地划分特征空间,使得具有相同标签或相似目标值的样本被分组在一起。
设节点 \(m\) 处的数据由 \(Q_m\) 表示,其中有 \(n_m\) 个样本。对于每个候选拆分 \(\theta = (j, t_m)\),由特征 \(j\) 和阈值 \(t_m\) 组成,将数据划分为 \(Q_m^{left}(\theta)\) 和 \(Q_m^{right}(\theta)\) 子集:
然后使用杂质函数或损失函数 \(H()\) 计算节点 \(m\) 处候选拆分的质量,选择哪个函数取决于要解决的任务(分类或回归):
选择使杂质最小化的参数:
选择每个节点拆分的策略由 splitter 参数控制:
使用 最佳拆分器 (默认,
splitter='best'),通过对所有可用特征和所有可能的阈值 \(t_m\)(即排序后的不同特征值之间的中点)执行贪婪穷举搜索来找到 \(\theta^*\),选择恰好最小化 \(G(Q_m, \theta)\) 的对。使用 随机拆分器 (
splitter='random'),通过为每个可用特征采样单个随机候选阈值来找到 \(\theta^*\)。这执行贪婪搜索的随机近似,有效地减少了计算时间(参见复杂度)。
在节点 \(m\) 选择了最优拆分 \(\theta^*\) 之后,将相同的拆分过程递归地应用于每个分区 \(Q_m^{left}(\theta^*)\) 和 \(Q_m^{right}(\theta^*)\),直到达到停止条件,例如:
达到允许的最大深度 (
max_depth);\(n_m\) 小于
min_samples_split;此拆分的杂质减少量小于
min_impurity_decrease。
有关其他停止条件,请参阅相应的估计器文档字符串。
1.10.7.1. 分类标准#
如果目标是取值 0,1,…,K-1 的分类结果,对于节点 \(m\),设:
为节点 \(m\) 中类别 k 观察值的比例。如果 \(m\) 是一个终端节点,该区域的 predict_proba 设置为 \(p_{mk}\)。常见的杂质度量如下。
Gini
Log Loss 或 Entropy
香农熵#
熵标准计算可能类别的香农熵。它将到达给定叶节点 \(m\) 的训练数据点的类别频率作为其概率。使用 香农熵作为树节点拆分标准等同于最小化 真实标签 \(y_i\) 和树模型 \(T\) 对类别 \(k\) 的概率预测 \(T_k(x_i)\) 之间的 log loss(也称为交叉熵和多项式偏差)。
为了理解这一点,首先回想一下,在数据集 \(D\) 上计算的树模型 \(T\) 的 log loss 定义如下:
其中 \(D\) 是包含 \(n\) 对 \((x_i, y_i)\) 的训练数据集。
在分类树中,叶节点内的预测类别概率是恒定的,即:对于所有 \((x_i, y_i) \in Q_m\),对于每个类别 \(k\),都有:\(T_k(x_i) = p_{mk}\)。
此属性使得可以将 \(\mathrm{LL}(D, T)\) 重写为为 \(T\) 的每个叶节点计算的香农熵的总和,并按到达每个叶节点的训练数据点数量加权:
1.10.7.2. 回归标准#
如果目标是连续值,那么对于节点 \(m\),用于确定未来拆分位置的常见最小化标准是均方误差 (MSE 或 L2 误差)、泊松偏差以及平均绝对误差 (MAE 或 L1 误差)。MSE 和泊松偏差都将终端节点的预测值设置为节点的学习平均值 \(\bar{y}_m\),而 MAE 将终端节点的预测值设置为中位数 \(median(y)_m\)。
均方误差
平均泊松偏差
如果您的目标是计数或频率(每单位计数),设置 criterion="poisson" 可能是一个不错的选择。无论如何,\(y >= 0\) 是使用此标准的必要条件。出于性能原因,实际实现最小化的是半平均泊松偏差,即平均泊松偏差除以 2。
平均绝对误差
请注意,截至版本 1.8,拟合速度比 MSE 标准慢 3-6 倍。
1.10.8. 缺失值支持#
DecisionTreeClassifier, DecisionTreeRegressor 使用 splitter='best' 对缺失值具有内置支持,其中拆分以贪婪方式确定。ExtraTreeClassifier 和 ExtraTreeRegressor 使用 splitter='random' 对缺失值具有内置支持,其中拆分是随机确定的。有关拆分器在非缺失值上的差异的更多详细信息,请参阅森林部分。
当存在缺失值时,支持的分类标准是 'gini'、'entropy' 或 'log_loss',回归标准是 'squared_error'、'friedman_mse' 或 'poisson'。
首先,我们将描述 DecisionTreeClassifier 和 DecisionTreeRegressor 如何处理数据中的缺失值。
对于非缺失数据上的每个潜在阈值,拆分器将评估所有缺失值进入左节点或右节点的拆分。
决策制定如下:
默认情况下,在预测时,具有缺失值的样本将使用训练期间找到的拆分中使用的类别进行分类。
>>> from sklearn.tree import DecisionTreeClassifier >>> import numpy as np >>> X = np.array([0, 1, 6, np.nan]).reshape(-1, 1) >>> y = [0, 0, 1, 1] >>> tree = DecisionTreeClassifier(random_state=0).fit(X, y) >>> tree.predict(X) array([0, 0, 1, 1])
如果两个节点的标准评估相同,则预测时缺失值的平局通过进入右节点来打破。拆分器还检查所有缺失值进入一个子节点而非缺失值进入另一个子节点的拆分。
>>> from sklearn.tree import DecisionTreeClassifier >>> import numpy as np >>> X = np.array([np.nan, -1, np.nan, 1]).reshape(-1, 1) >>> y = [0, 0, 1, 1] >>> tree = DecisionTreeClassifier(random_state=0, max_depth=1).fit(X, y) >>> X_test = np.array([np.nan]).reshape(-1, 1) >>> tree.predict(X_test) array([1])
如果在训练期间没有看到给定特征的缺失值,那么在预测期间,缺失值将被映射到具有最多样本的子节点。
>>> from sklearn.tree import DecisionTreeClassifier >>> import numpy as np >>> X = np.array([0, 1, 2, 3]).reshape(-1, 1) >>> y = [0, 1, 1, 1] >>> tree = DecisionTreeClassifier(random_state=0).fit(X, y) >>> X_test = np.array([np.nan]).reshape(-1, 1) >>> tree.predict(X_test) array([1])
ExtraTreeClassifier 和 ExtraTreeRegressor 以稍微不同的方式处理缺失值。当拆分节点时,将选择一个随机阈值来拆分非缺失值。然后,非缺失值将根据随机选择的阈值发送到左子节点和右子节点,而缺失值也将随机发送到左子节点或右子节点。这在每个拆分中考虑的每个特征上重复。选择其中最佳的拆分。
在预测期间,缺失值的处理与决策树相同。
默认情况下,在预测时,具有缺失值的样本将使用训练期间找到的拆分中使用的类别进行分类。
如果在训练期间没有看到给定特征的缺失值,那么在预测期间,缺失值将被映射到具有最多样本的子节点。
1.10.9. 最小成本-复杂度剪枝#
最小成本-复杂度剪枝是一种用于修剪树以避免过拟合的算法,在 [BRE] 的第 3 章中有所描述。该算法由 \(\alpha\ge0\)(称为复杂度参数)参数化。复杂度参数用于定义给定树 \(T\) 的成本-复杂度度量 \(R_\alpha(T)\):
其中 \(|\widetilde{T}|\) 是 \(T\) 中的终端节点数,\(R(T)\) 传统上定义为终端节点的总错误分类率。或者,scikit-learn 使用终端节点的总样本加权杂质作为 \(R(T)\)。如上所示,节点的杂质取决于标准。最小成本-复杂度剪枝找到使 \(R_\alpha(T)\) 最小化的 \(T\) 的子树。
单个节点的成本复杂度度量为 \(R_\alpha(t)=R(t)+\alpha\)。分支 \(T_t\) 被定义为以节点 \(t\) 为根的树。通常,节点的杂质大于其终端节点杂质之和,即 \(R(T_t)ccp_alpha 参数时,此过程停止。
示例
References
L. Breiman, J. Friedman, R. Olshen, and C. Stone. Classification and Regression Trees. Wadsworth, Belmont, CA, 1984.
J.R. Quinlan. C4. 5: programs for machine learning. Morgan Kaufmann, 1993.
T. Hastie, R. Tibshirani and J. Friedman. Elements of Statistical Learning, Springer, 2009.