决策树超参数调优的视觉指南
决策树超参数调优的视觉指南
引言
基于这两个因素,我决定探索不同的决策树超参数如何影响树的表现(通过如 MAE、RMSE 和 R²等因素衡量)以及其视觉外观(查看深度、节点/叶计数和整体结构等因素)。
对于模型,我将使用 scikit-learn 的DecisionTreeRegressor。分类决策树需要与回归决策树相似的超参数调整,因此我将不会单独讨论它们。我将查看的超参数包括max_depth、ccp_alpha、min_samples_split、min_samples_leaf和max_leaf_nodes。我将使用加州住房数据集,可通过 scikit-learn 获得(更多信息在这里) (CC-BY)。下面所有的图像都是我制作的。如果你想要亲自尝试这个小型项目,代码在我的 GitHub 上可用:github.com/jamesdeluk/data-projects/tree/main/visualising-trees
数据
这是数据(为了视觉效果进行了转置):
| Feature | Row 1 | Row 2 | Row 3 |
|---|---|---|---|
| MedInc | 8.3252 | 8.3014 | 7.2574 |
| HouseAge | 41 | 21 | 52 |
| AveRooms | 6.98412698 | 6.23813708 | 8.28813559 |
| AveBedrms | 1.02380952 | 0.97188049 | 1.07344633 |
| Population | 322 | 2401 | 496 |
| AveOccup | 2.55555556 | 2.10984183 | 2.80225989 |
| Latitude | 37.88 | 37.86 | 37.85 |
| Longitude | -122.23 | -122.22 | -122.24 |
| MedHouseVal | 4.526 | 3.585 | 3.521 |
每一行代表一个“区块群”,一个地理区域。列的顺序是:中位数收入、中位数房屋年龄、平均房间数、平均卧室数、人口、平均居住人数、纬度、经度和中位数房屋价值(目标)。目标值范围从 0.15 到 5.00,平均值为 2.1。
我留出了最后的项目作为我自己的个人测试器:
| Feature | Value |
|---|---|
| MedInc | 2.3886 |
| HouseAge | 16 |
| AveRooms | 5.25471698 |
| AveBedrms | 1.16226415 |
| Population | 1387 |
| AveOccup | 2.61698113 |
| Latitude | 39.37 |
| Longitude | -121.24 |
| MedHouseVal | 0.894 |
我将使用train_test_split创建训练和测试数据,我将使用这些数据来比较树。
树深度
浅度
我将从一个小树开始,max_depth为 3。我将使用timeit来记录拟合和预测所需的时间。当然,这是基于我的机器;目的是给出相对时间,而不是绝对时间。为了获得更准确的计时,我取了 10 次拟合和预测的平均值。
拟合需要 0.024 秒,预测需要 0.0002 秒,结果得到平均绝对误差(MAE)为 0.6,平均绝对百分比误差(MAPE)为 0.38(即 38%),平均平方误差(MSE)为 0.65,均方根误差(RMSE)为 0.80,R²为 0.50。请注意,对于 R²,与之前的误差统计不同,越高越好。对于我选择的块,它预测了 1.183,与实际值 0.894 相比,总体来说并不理想。
这是树本身,使用plot_tree:

您可以看到它只使用了 MedInc、AveRooms 和 AveOccup 特征——换句话说,从数据集中删除 HouseAge、AveBedrms、Population、Latitude 和 Longitude 将给出相同的预测。
深度
让我们设置max_depth为None,即无限。
拟合需要 0.09 秒(约 4 倍更长),预测需要 0.0007 秒(约 4 倍更长),结果得到 MAE 为 0.47,MAPE 为 0.26,MSE 为 0.53,RMSE 为 0.73,R²为 0.60。对于我选择的块,它预测了 0.819,与实际值 0.894 相比,要好得多。
树:

哇。它有 34 层(.get_depth()),29,749 个节点(.tree_.node_count),和 14,875 个单独的分支(.get_n_leaves())——换句话说,对于 MedHouseVal 有高达 14,875 个不同的最终值。
使用一些自定义代码,我可以绘制其中一个分支:

这个分支单独使用了八个特征中的六个,所以很可能是,在所有约 15,000 个分支中,所有特征都被表示了。
然而,如此复杂的树可能导致过拟合,因为它可以分裂成非常小的组并捕捉噪声。
剪枝
ccp_alpha参数(ccp = cost-complexity pruning)可以在树构建后进行剪枝。将 0.005 的值添加到无限深度的树中,结果得到 MAE 为 0.53,MAPE 为 0.33,MSE 为 0.52,RMSE 为 0.72,R²为 0.60——因此它在深度和浅度树之间表现。对于我选择的块,它预测了 1.279,所以在这种情况下,比浅度树差。拟合需要 0.64 秒(比深度树长 6 倍以上),预测需要 0.0002 秒(与浅度树相同)——因此,拟合速度慢,但预测速度快。
这棵树看起来像:

交叉验证
如果我们混合数据会怎样?在循环中,我使用了 train_test_split 不带随机状态 (每次都获取新的数据),并根据新数据拟合和预测每棵树。每次循环我都会记录 MAE/MAPE/MSE/RMSE/R²,然后为每个找到平均值和标准差。我进行了 1000 次循环。这有助于(正如其名所示)验证我们的结果——单个高或低误差结果可能只是偶然,所以取平均值可以更好地了解新数据上的典型误差,而标准差有助于了解模型有多稳定/可靠。
值得注意的是,sklearn 有一些内置工具可以进行这种形式的验证,即cross_validation,使用ShuffleSplit或RepeatedKFold,它们通常要快得多;我只是手动做了,以便更清楚地了解发生了什么,并强调时间差异。
max_depth=3 (时间:22.1 秒)
| 指标 | 平均值 | 标准差 |
|---|---|---|
| MAE | 0.597 | 0.007 |
| MAPE | 0.378 | 0.008 |
| MSE | 0.633 | 0.015 |
| RMSE | 0.795 | 0.009 |
| R² | 0.524 | 0.011 |
max_depth=None (时间:100.0 秒)
| 指标 | 平均值 | 标准差 |
|---|---|---|
| MAE | 0.463 | 0.010 |
| MAPE | 0.253 | 0.008 |
| MSE | 0.524 | 0.023 |
| RMSE | 0.724 | 0.016 |
| R² | 0.606 | 0.018 |
max_depth=None, ccp_alpha=0.005 (时间:650.2 秒)
| 指标 | 平均值 | 标准差 |
|---|---|---|
| MAE | 0.531 | 0.012 |
| MAPE | 0.325 | 0.012 |
| MSE | 0.521 | 0.021 |
| RMSE | 0.722 | 0.015 |
| R² | 0.609 | 0.016 |
与深树相比,在所有误差统计中,浅树具有更高的误差(也称为偏差),但标准差较低(也称为方差)。用更通俗的话来说,在精度(所有预测都接近)和准确性(所有预测都接近真实值)之间有一个权衡。剪枝的深树通常介于两者之间,但拟合所需时间更长。
我们可以用箱线图可视化所有这些统计数据:

我们可以看到深树(绿色框)通常具有更低的误差(y 轴值更小),但比浅树(蓝色框)具有更大的变化(线条之间的差距更大)。通过归一化平均值(使它们都是 0),我们可以更清楚地看到变化;例如,对于 MAE:

直方图也可能很有趣。再次以 MAE 为例:

绿色(深)的误差较低,但蓝色(浅)的误差范围较窄。有趣的是,剪枝树的结果比其他两种结果更不正常——尽管这不是典型行为。
其他超参数
我们可以调整哪些其他超参数?完整的列表可以在文档中找到:scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html
最小分割样本数
这是单个节点可以包含的最小样本数,以允许分裂。它可以是数字或百分比(实现为介于 0 和 1 之间的浮点数)。它通过确保每个分支包含合理数量的结果,而不是仅基于少量样本不断分裂成更小的分支,从而有助于避免过拟合。
例如,max_depth=10,我将将其作为参考,看起来是这样的:

| 指标 | 平均值 | 标准差 |
|---|---|---|
| MAE | 0.426 | 0.010 |
| MAPE | 0.240 | 0.008 |
| MSE | 0.413 | 0.018 |
| RMSE | 0.643 | 0.014 |
| R² | 0.690 | 0.014 |
这意味着有 1563 个节点和 782 个叶子。
而max_depth=10, min_samples_split=0.2看起来是这样的:

| 指标 | 平均值 | 标准差 |
|---|---|---|
| MAE | 0.605 | 0.013 |
| MAPE | 0.367 | 0.007 |
| MSE | 0.652 | 0.027 |
| RMSE | 0.807 | 0.016 |
| R² | 0.510 | 0.019 |
因为它不能分裂少于总样本数 20%(0.2)的任何节点(如叶子样本百分比所示),它被限制在 4 个深度,只有 15 个节点和 8 个叶子。
对于深度为 10 的树,许多叶子节点只包含单个样本。这么多叶子节点却只有这么少的样本可能是一个过拟合的迹象。对于约束树,最小的叶子节点包含超过 1000 个样本。
在这个案例中,约束树在所有指标上都比非约束树差;然而,将min_samples_split设置为 10(即 10 个样本,而不是 10%)改善了结果:
| 指标 | 平均值 | 标准差 |
|---|---|---|
| MAE | 0.425 | 0.009 |
| MAPE | 0.240 | 0.008 |
| MSE | 0.407 | 0.017 |
| RMSE | 0.638 | 0.013 |
| R² | 0.695 | 0.013 |
这个又回到了 10 个深度,有 1133 个节点和 567 个叶子(所以比非约束树少约 1/3)。其中许多叶子也只包含一个样本。
每个叶子节点的最小样本数
另一种约束树的方法是通过设置叶子节点可以拥有的最小样本数。同样,它可以是数字或百分比。
对于max_depth=10, min_samples_leaf=0.1:

与第一个min_samples_split类似,它有 4 个深度,15 个节点和 8 个叶子。然而,请注意节点和叶子是不同的;例如,在min_samples_split树的最右侧叶子中,有 5.8%的样本,而在这个中,"相同"的叶子有 10%(即 0.1)。
这些统计数据与那个也相似:
| 指标 | 平均值 | 标准差 |
|---|---|---|
| MAE | 0.609 | 0.010 |
| MAPE | 0.367 | 0.007 |
| MSE | 0.659 | 0.023 |
| RMSE | 0.811 | 0.014 |
| R² | 0.505 | 0.016 |
允许“更大的”叶子可以提高结果。min_samples_leaf=10有 10 个深度,961 个节点和 481 个叶子——所以与min_samples_split=10相似。它给出了迄今为止的最佳结果,表明限制 1 样本叶子的数量确实减少了过拟合。
| 指标 | 平均值 | 标准差 |
|---|---|---|
| MAE | 0.417 | 0.010 |
| MAPE | 0.235 | 0.008 |
| MSE | 0.380 | 0.017 |
| RMSE | 0.616 | 0.014 |
| R² | 0.714 | 0.013 |
最大叶子节点数
另一种减少叶子节点过多且样本过少的方法是直接通过max_leaf_nodes限制叶子节点的数量(技术上它仍然可能导致单个样本的叶子节点,但可能性较小)。上面的树从 8 个叶子节点到几乎 800 个叶子节点不等。使用max_depth=10, max_leaf_nodes=100:

这再次具有 10 的深度,有 199 个节点和 100 个叶子节点。在这种情况下,只有一个叶子节点包含单个样本,只有 9 个叶子节点包含少于十个样本。结果也不错:
| 指标 | 平均值 | 标准差 |
|---|---|---|
| MAE | 0.450 | 0.010 |
| MAPE | 0.264 | 0.010 |
| MSE | 0.414 | 0.018 |
| RMSE | 0.644 | 0.014 |
| R² | 0.689 | 0.013 |
贝叶斯搜索
最后,对于这些数据,“完美”的树是什么样子?当然,可以使用试错法与上述超参数,但使用类似BayesSearchCV(假设你有时间让它运行)要容易得多。在 20 分钟内,它执行了 200 次迭代(即超参数组合),每次迭代有五个交叉验证(类似于五个train_test_split)。
它找到的超参数:{'ccp_alpha': 0.0, 'criterion': 'squared_error', 'max_depth': 100, 'max_features': 0.9193546958301854, 'min_samples_leaf': 15, 'min_samples_split': 24}。
该树深度为 20,有 798 个叶子节点和 1595 个节点,所以显著少于完全深度的树。这清楚地表明增加min_samples_可以帮助;虽然叶子节点和节点的数量与深度为 10 的树相似,但拥有“更大”的叶子节点和更深层次的树提高了结果。我之前还没有提到max_features,但正如其名——在每个分割时要考虑多少个特征。鉴于这个数据有 8 个特征,并且~0.9 ✕ 8 = ~7.2,在每个分割时,将考虑 8 个特征中的 7 个来找到最佳得分。
对于我的单个块,它预测了 0.81632,所以非常接近真实值。
在经过 1000 次循环(这花了刚好超过 60 秒——表明在拟合树时最长的因素是剪枝)后,最终的得分:
| 指标 | 平均值 | 标准差 |
|---|---|---|
| MAE | 0.393 | 0.007 |
| MAPE | 0.216 | 0.006 |
| MSE | 0.351 | 0.013 |
| RMSE | 0.592 | 0.011 |
| R² | 0.736 | 0.010 |
将这些添加到箱线图中:

错误更低,方差更低,R²更高。非常好。
结论
将树可视化可以使看到其功能变得清晰——你可以手动选择一行,跟随流程,并得到你的结果。当然,对于叶子较少的浅树来说,这要容易得多。然而,正如我们所看到的,它的表现并不好——毕竟,16,000 个训练行被回归到只有 8 个值,然后这些值被用来预测 4,000 个测试行。
深树中的数万个节点表现更佳,尽管手动追踪其流程会困难得多,但仍然可行。然而,这导致了过拟合——这并不一定令人惊讶,因为叶子的数量几乎与数据行的数量相当,而且值与训练行的比例约为 1:4(与浅树的 1:2000 相比)。
修剪可以帮助减少过拟合并提高性能,同时减少预测时间(但调整其他因素,如要分割的样本数量、每个叶子的样本数量和最大叶子数量,通常能做得更好)。现实生活中的树类比非常强烈——在树生长过程中照顾它更为有效和高效,确保其以最佳方式分支,而不是让它野生生长数年,然后再尝试修剪它。
手动平衡所有这些超参数是一项挑战,但幸运的是,计算机擅长快速运行大量计算,因此使用如 BayesSearchCV 之类的搜索算法来获取最佳超参数是明智的。那么,为什么不直接忘记所有上述内容,而进行网格搜索,测试所有可能的组合呢?嗯,运行数百万次计算仍然需要时间,特别是对于大型数据集来说,因此能够缩小超参数窗口可以显著加快速度。
接下来,

浙公网安备 33010602011771号