# 5.18 深入：决策树与森林

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

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

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

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

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

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

## 决策树回归

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

```py
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");
```

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

## 决策树分类

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

```py
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，此时每个叶仅包含来自单个类的样本。这被称为所有叶子都是“纯的”。

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

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

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

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

## 随机森林

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

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

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

```py
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 个并行估计器。 我们的想法是，通过顺序应用非常快速，简单的模型，我们可以获得比任何单个部分更好的总模型误差。

```py
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_rate`和`max_depth`。

```py
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
```

## 特征的重要性

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

```py
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')
```
