十八、深入:决策树与森林

优质
小牛编辑
115浏览
2023-12-01

在这里,我们将探索一类基于决策树的算法。 最基本决策树非常直观。 它们编码一系列ifelse选项,类似于一个人如何做出决定。 但是,从数据中完全可以了解要问的问题以及如何处理每个答案。

例如,如果你想创建一个识别自然界中发现的动物的指南,你可能会问以下一系列问题:

  • 动物是大于还是小于一米?
    • 较大:动物有角吗?
      • 是的:角长是否超过十厘米?
      • 不是:动物有项圈吗?
  • 较小:动物有两条腿还是四条腿?
    • 二:动物有翅膀吗?
    • 四:动物有浓密的尾巴吗?

等等。 这种问题的二元分裂是决策树的本质。

基于树的模型的主要好处之一是它们几乎不需要数据预处理。 它们可以处理不同类型的变量(连续和离散),并且对特征的缩放不变。

另一个好处是基于树的模型被称为“非参数”,这意味着他们没有一套固定的参数需要学习。 相反,如果给出更多数据,树模型可以变得越来越灵活。 换句话说,自由参数的数量随着样本量而增长并且不是固定的,例如在线性模型中。

决策树回归

决策树是一种简单的二元分类树,类似于最近邻分类。 它可以这样使用:

from figures import make_dataset
x, y = make_dataset()
X = x.reshape(-1, 1)

plt.figure()
plt.xlabel('Feature X')
plt.ylabel('Target y')
plt.scatter(X, y);

from sklearn.tree import DecisionTreeRegressor

reg = DecisionTreeRegressor(max_depth=5)
reg.fit(X, y)

X_fit = np.linspace(-3, 3, 1000).reshape((-1, 1))
y_fit_1 = reg.predict(X_fit)

plt.figure()
plt.plot(X_fit.ravel(), y_fit_1, color='tab:blue', label="prediction")
plt.plot(X.ravel(), y, 'C7.', label="training data")
plt.legend(loc="best");

单个决策树允许我们以非参数方式估计标签,但显然存在一些问题。 在某些地区,该模型表现出高偏差并且对数据欠拟合。 (请见不遵循数据轮廓的长扁形线条),而在其他区域,模型表现高方差并且过拟合数据(反映为受单点噪声影响的窄峰形)。

决策树分类

决策树分类原理非常相似,通过将叶子中的多数类分配给叶子中的所有点:

from sklearn.datasets import make_blobs
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from figures import plot_2d_separator
from figures import cm2


X, y = make_blobs(centers=[[0, 0], [1, 1]], random_state=61526, n_samples=100)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

clf = DecisionTreeClassifier(max_depth=5)
clf.fit(X_train, y_train)

plt.figure()
plot_2d_separator(clf, X, fill=True)
plt.scatter(X_train[:, 0], X_train[:, 1], c=y_train, cmap=cm2, s=60, alpha=.7, edgecolor='k')
plt.scatter(X_test[:, 0], X_test[:, 1], c=y_test, cmap=cm2, s=60, edgecolor='k');

有许多参数可以控制树的复杂性,但最容易理解的是最大深度。这限制了树可以对输入空间进行划分的精确度,或者在决定样本所在的类之前,可以询问多少if-else问题。

此参数对于调整树和基于树的模型非常重要。下面的交互式图表显示了该模型的欠拟合和过拟合。 max_depth为 1 显然是一个欠拟合的模型,而 7 或 8 的深度明显过拟合。对于该数据集,树可以生长的最大深度是 8,此时每个叶仅包含来自单个类的样本。这被称为所有叶子都是“纯的”。

在下面的交互式图中,区域被指定为蓝色和红色,来表明该区域的预测类。颜色的阴影表示该类的预测概率(较暗为较高概率),而黄色区域表示任一类的预测概率相等。

from figures import plot_tree
max_depth = 3
plot_tree(max_depth=max_depth)

决策树训练快,易于理解,并且经常产生可解释的模型。 但是,单个树通常倾向于过拟合训练数据。 使用上面的滑块,你可能会注意到,即使在类之间有良好的分隔之前,模型也会开始过拟合。

因此,在实践中,更常见的是组合多个树来产生更好泛化的模型。 组合树的最常用方法是随机森林和梯度提升树。

随机森林

随机森林只是许多树,建立在数据的不同随机子集(带放回抽样)上,并对于每个分裂,使用特征的不同随机子集(无放回抽样)。 这使得树彼此不同,并使它们过拟合不同的方面。 然后,他们的预测被平均,产生更平稳的估计,更少过拟合。

from figures import plot_forest
max_depth = 3
plot_forest(max_depth=max_depth)

通过交叉验证选择最优估计

from sklearn.model_selection import GridSearchCV
from sklearn.datasets import load_digits
from sklearn.ensemble import RandomForestClassifier

digits = load_digits()
X, y = digits.data, digits.target

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

rf = RandomForestClassifier(n_estimators=200)
parameters = {'max_features':['sqrt', 'log2', 10],
              'max_depth':[5, 7, 9]}

clf_grid = GridSearchCV(rf, parameters, n_jobs=-1)
clf_grid.fit(X_train, y_train)

clf_grid.score(X_train, y_train)

clf_grid.score(X_test, y_test)

另一个选项:梯度提升

可能有用的另一种集合方法是提升:在这里,我们构建了一个由 200 个估计器组成的链,它迭代地改进了先前估计器的结果,而不是查看(比方说)200 个并行估计器。 我们的想法是,通过顺序应用非常快速,简单的模型,我们可以获得比任何单个部分更好的总模型误差。

from sklearn.ensemble import GradientBoostingRegressor
clf = GradientBoostingRegressor(n_estimators=100, max_depth=5, learning_rate=.2)
clf.fit(X_train, y_train)

print(clf.score(X_train, y_train))
print(clf.score(X_test, y_test))

练习:梯度提升的交叉验证

使用网格搜索在数字数据集上优化梯度提升树learning_ratemax_depth

from sklearn.datasets import load_digits
from sklearn.ensemble import GradientBoostingClassifier

digits = load_digits()
X_digits, y_digits = digits.data, digits.target

# split the dataset, apply grid-search

# %load solutions/18_gbc_grid.py

特征的重要性

RandomForestGradientBoosting对象在拟合之后都会提供feature_importances_属性。 此属性是这些模型最强大的功能之一。 它们基本上量化了在不同树的节点中,每个特征对表现的贡献程度。

X, y = X_digits[y_digits < 2], y_digits[y_digits < 2]

rf = RandomForestClassifier(n_estimators=300, n_jobs=1)
rf.fit(X, y)
print(rf.feature_importances_)  # one value per feature

plt.figure()
plt.imshow(rf.feature_importances_.reshape(8, 8), cmap=plt.cm.viridis, interpolation='nearest')