📢 转载信息
原文链接:https://machinelearningmastery.com/why-decision-trees-fail-and-how-to-fix-them/
原文作者:Iván Palomares Carrascosa
在本文中,您将了解到决策树在实践中为何有时会失败,以及如何使用简单有效的技术来纠正最常见的问题。
我们将涵盖的主题包括:
- 如何发现并减少决策树中的过拟合。
- 如何通过调整模型容量来识别并修复欠拟合。
- 噪声或冗余特征如何误导决策树,以及特征选择如何提供帮助。
让我们不要再浪费时间了。
决策树为何会失败(以及如何修复它们)
图片作者:Editor
用于分类和回归等预测性机器学习任务的决策树模型无疑具有丰富的优势——例如它们捕获特征之间非线性关系的能力,以及直观的可解释性,使得跟踪决策变得容易。然而,它们并非完美,尤其是在训练中等或高度复杂的数据集时可能会失败,在这种情况下,过拟合、欠拟合或对噪声特征的敏感性等问题通常会出现。
在本文中,我们将探讨训练好的决策树模型可能失败的三个常见原因,并概述应对这些问题的简单而有效的策略。讨论中附带了可供您自己尝试的 Python 示例。
1. 过拟合:记住数据而不是从中学习
Scikit-learn 的简洁性和直观性在构建机器学习模型时可能很诱人,人们可能会认为简单地“默认”构建模型就应该产生令人满意的结果。然而,许多机器学习模型的常见问题是过拟合,即模型从数据中学得太多,以至于几乎记住了它接触到的每一个数据样本。结果是,一旦训练好的模型遇到新的、未见过的数据样本,它在确定正确的输出预测时就会遇到困难。
这个例子在流行的、公开可用的 California Housing 数据集上训练了一个决策树:这是一个常用于回归任务的中等复杂度和大小的数据集,用于根据一个地区的人口统计特征和平均房屋特征来预测该地区的中位数房价。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeRegressor
from sklearn.metrics import mean_squared_error
import numpy as np
# Loading the dataset and splitting it into training and test sets
X, y = fetch_california_housing(return_X_y=True, as_frame=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
# Building a tree without specifying maximum depth
overfit_tree = DecisionTreeRegressor(random_state=42)
overfit_tree.fit(X_train, y_train)
print("Train RMSE:", np.sqrt(mean_squared_error(y_train, overfit_tree.predict(X_train))))
print("Test RMSE:", np.sqrt(mean_squared_error(y_test, overfit_tree.predict(X_test))))
|
请注意,我们训练了一个决策树回归器,但没有指定任何超参数,包括对树的形状和大小的限制。是的,这将带来后果,即训练样本上的误差几乎为零(注意下面科学计数法 e-16)与测试集上高得多的误差之间存在巨大鸿沟。这是过拟合的明确迹象。
输出:
|
1
2
|
Train RMSE: 3.013481908235909e-16
Test RMSE: 0.7269954649985176
|
为了解决过拟合问题,一个常见的策略是正则化,即简化模型的复杂度。虽然对于其他模型来说,这需要一种相当复杂的数学方法,但对于 scikit-learn 中的决策树来说,它就像限制树可以生长的最大深度,或叶节点应包含的最小样本数等方面一样简单:这两个超参数都是为了控制和防止可能过度生长的树而设计的。
|
1
2
3
4
5
|
pruned_tree = DecisionTreeRegressor(max_depth=6, min_samples_leaf=20, random_state=42)
pruned_tree.fit(X_train, y_train)
print("Train RMSE:", np.sqrt(mean_squared_error(y_train, pruned_tree.predict(X_train))))
print("Test RMSE:", np.sqrt(mean_squared_error(y_test, pruned_tree.predict(X_test))))
|
|
1
2
|
Train RMSE: 0.6617348643931361
Test RMSE: 0.6940789988854102
|
总的来说,第二个树比第一个树更受青睐,尽管训练集的误差有所增加。关键在于测试数据上的误差,这通常是模型在现实世界中表现的更好指标,而该误差确实相对于第一个树有所下降。
2. 欠拟合:树过于简单,无法良好工作
与过拟合恰恰相反的是欠拟合问题,这本质上是指模型从训练数据中学得不好,以至于即使在评估这些数据时,性能也低于预期。
虽然过拟合的树通常过度生长且很深,但欠拟合通常与浅层树结构相关。
解决欠拟合的一种方法是仔细地增加模型复杂度,同时注意不要使其过于复杂而陷入前面解释的过拟合问题。下面是一个示例(请在 Colab 笔记本或类似环境中尝试以查看结果):
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
from sklearn.datasets import fetch_openml
from sklearn.tree import DecisionTreeRegressor
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
import numpy as np
wine = fetch_openml(name="wine-quality-red", version=1, as_frame=True)
X, y = wine.data, wine.target.astype(float)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
# A tree that is too shallow (depth of 2) is likely prone to underfitting
shallow_tree = DecisionTreeRegressor(max_depth=2, random_state=42)
shallow_tree.fit(X_train, y_train)
print("Train RMSE:", np.sqrt(mean_squared_error(y_train, shallow_tree.predict(X_train))))
print("Test RMSE:", np.sqrt(mean_squared_error(y_test, shallow_tree.predict(X_test))))
|
以及一个能降低误差并缓解欠拟合的版本:
|
1
2
3
4
5
|
better_tree = DecisionTreeRegressor(max_depth=5, random_state=42)
better_tree.fit(X_train, y_train)
print("Train RMSE:", np.sqrt(mean_squared_error(y_train, better_tree.predict(X_train))))
print("Test RMSE:", np.sqrt(mean_squared_error(y_test, better_tree.predict(X_test))))
|
3. 误导性的训练特征:引发干扰
决策树也可能对与现有特征一起使用时不相关或冗余的特征非常敏感。这与“信噪比”有关;换句话说,数据中包含的信号(对预测有价值的信息)越多,噪声越少,模型的性能就越好。想象一个游客迷失在京都站区域中央,并询问去清水的方向——清水寺位于几公里之外。如果她收到指示说“乘坐 EX101 路公交车,在五条坂下车,然后沿着上坡的街道走”,她很可能会轻松到达目的地,但如果她被告知要一直走到那里,路上有几十个转弯和街道名称,她可能会再次迷路。这是决策树等模型中“信噪比”的一个比喻。
仔细且有策略的特征选择通常是解决此问题的最佳方法。这个稍微复杂一点的例子说明了基线树模型、为模拟低质量训练数据而有意向数据集中添加人工噪声,以及随后的特征选择以提高模型性能之间的比较。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
from sklearn.datasets import fetch_openml
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.preprocessing import OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.feature_selection import SelectKBest, mutual_info_classif
from sklearn.metrics import accuracy_score
import numpy as np, pandas as pd, matplotlib.pyplot as plt
adult = fetch_openml("adult", version=2, as_frame=True)
X, y = adult.data, (adult.target == ">50K").astype(int)
cat, num = X.select_dtypes("category").columns, X.select_dtypes(exclude="category").columns
Xtr, Xte, ytr, yte = train_test_split(X, y, stratify=y, random_state=42)
def make_preprocessor(df):
return ColumnTransformer([
("num", "passthrough", df.select_dtypes(exclude="category").columns),
("cat", OneHotEncoder(handle_unknown="ignore"), df.select_dtypes("category").columns)
])
# Baseline model
base = Pipeline([
("prep", make_preprocessor(X)),
("clf", DecisionTreeClassifier(max_depth=None, random_state=42))
]).fit(Xtr, ytr)
print("Baseline acc:", round(accuracy_score(yte, base.predict(Xte)), 3))
# Adding 300 noisy features to emulate a poorly performing model due to being trained on noise
rng = np.random.RandomState(42)
noise = pd.DataFrame(rng.normal(size=(len(X), 300)), index=X.index, columns=[f"noise_{i}" for i in range(300)])
X_noisy = pd.concat([X, noise], axis=1)
Xtr, Xte, ytr, yte = train_test_split(X_noisy, y, stratify=y, random_state=42)
noisy = Pipeline([
("prep", make_preprocessor(X_noisy)),
("clf", DecisionTreeClassifier(max_depth=None, random_state=42))
]).fit(Xtr, ytr)
print("With noise acc:", round(accuracy_score(yte, noisy.predict(Xte)), 3))
# Our fix: applying feature selection with SelectKBest() function in a pipeline
sel = Pipeline([
("prep", make_preprocessor(X_noisy)),
("select", SelectKBest(mutual_info_classif, k=20)),
("clf", DecisionTreeClassifier(max_depth=None, random_state=42))
]).fit(Xtr, ytr)
print("After selection acc:", round(accuracy_score(yte, sel.predict(Xte)), 3))
# Plotting feature importance
importances = noisy.named_steps["clf"].feature_importances_
names = noisy.named_steps["prep"].get_feature_names_out()
pd.Series(importances, index=names).nlargest(20).plot(kind="barh")
plt.title("Top 20 Feature Importances (Noisy Model)")
plt.gca().invert_yaxis()
plt.show()
|
🚀 想要体验更好更全面的AI调用?
欢迎使用青云聚合API,约为官网价格的十分之一,支持300+全球最新模型,以及全球各种生图生视频模型,无需翻墙高速稳定,文档丰富,小白也可以简单操作。
评论区