5-决策树
理论部分
训练和可视化决策树
- 决策树是通用的机器学习算法,可以执行分类和回归任务,甚至多输出任务
做出预测
- 从根节点开始遍历,直到它所在的叶子节点(即它没有任何子节点),它不会提出任何问题,只需查看该节点的预测类,然后决策树就可以做出预测
- 决策树的许多特质之一就是它们几乎不需要数据准备,实际上,它们根本不需要特征缩放或居中
- 属性说明
- 节点的samples属性统计它应用的训练实例数量
- 节点的value属性说明了该节点上每个类别的训练实例数量
- 节点的gini属性衡量其不纯度(impurity):如果应用的所有训练实例都属于同一个类别,那么节点就是“纯”的(gini=0)
- 基尼系数(基尼不纯度)
\[G_i = 1 - \sum^n_{k=1}p_{i,k}^2 \\
p_{i,k}是第i个节点中训练实例之间的k类实例的比率
\]
- CART算法仅生成二叉树:非叶节点永远只有两个子节点(即问题答案仅有是或否)
CART训练算法
- CART树算法,即分类回归树算法,也称增长树。概算法的工作原理是:首先使用单个特征k和阈值tk将训练集分为两个子集。它搜索产生最纯子集(按其大小加权)的一对(k,tk)
- CART分类成本函数
\[J(k,t_k)=\frac{m_{left}}{m}G_{left}+\frac{m_{right}}{m}G_{right} \\
其中,\left\{
\begin{matrix}
G_{left/right}测量左右子集的不纯度 \\
m_{left/right}测量左右子集的实例数
\end{matrix}
\right .
\]
- 一旦CART算法成功地将训练集分为两部分,它就会使用相同的逻辑将子集进行分割,然后再分割子集,以此类推。一旦达到最大深度,或者找不到可减少不纯度的分割,它将停止递归
- CART是一种贪婪算法:从顶层开始搜索最优分裂,然后每层重复这个过程。基层分裂之后,它并不会检视这个分裂的不纯度是否为可能的最低值。贪婪算法通常会产生一个相当不错的解,但是不能保证是最优解。而不幸的是,寻找最优树是一个已知的NP完全问题
- P是可以在多项式时间内解决的一组问题。NP是可以在多项式时间内验证其解的一组问题。NP难问题是可以在多项式时间内将任何NP问题减少的问题。一个NP完全问题是NP和NP难。一个主要的开放数学问题是P=NP是否成立。如果P!=NP(这似乎是可能的),那么不会找到针对任何NP完全问题的多项式算法(也许在量子计算机上除外)
训练复杂度
- 进行预测需要从根到叶遍历决策树。决策树通常是近似平衡的,因此遍历决策树需要经过大约O(log2(m))个节点。由于每个节点仅需要检查一个特征值,因此总体预测复杂度为O(log2(m)),与特征数量无关,因此即使处理大训练集,预测也非常快
- 训练算法比每个节点上所有样本上的所有特征。比较每个节点上所有样本的所有特征会导致训练复杂度为O(n*m log2(m))。对于小训练集可以通过对数据进行预排序加快训练速度,对于大训练集,这样做则会大大降低训练集的训练速度
基尼不纯度或熵
- 熵的概念源于热力学,是一种分子混乱程度的度量:如果分子保持静止和良序,则熵接近于0。在机器学习中,如果数据集中仅包含一个类别的实例,其熵为0
\[H_i = -{\underset {p_{i,k}≠0} \sum^n_{k=1}}p_{i,k}log_2(p_{i,k})
\]
- 大多数情况下,基尼不纯度和熵没有什么大的不同,产生的树都很近似。基尼不纯度的计算速度略快一些(没有对数运算),所以它是个不错的默认选择。它们的不同在于,基尼不纯度倾向于从树枝中分裂出最常见的类别,而熵则倾向于生产更平衡的树
正则化超参数
- 决策树极少对训练数据做出假设。如果不加以限制,树的结构将跟随训练集变化,严密拟合,并且很可能过拟合。这种模型通常被称为非参数模型,这不是说它不包含任何参数,而是指在训练之前没有确定参数的数量,导致模型结构自由而紧密的贴近数据。相反,参数模型(比如线性模型)则有预先设定好的一部分参数,因此其自由度受限,从而降低了过拟合的风险(但是增加了欠拟合的风险)
- 为避免过拟合,需要在训练过程中降低决策树的自由度,这个过程被称为正则化
- 还可以先不加约束地训练模型,然后再对不必要的节点进行剪枝(删除)。如果一个节点的子节点全部为叶节点,则该节点可被认为不必要,除非它所表示的不纯度有重要的统计意义。标准统计测试(比如卡方测试)用来估算提升纯粹是出于偶然(被称为零假设)的概率。如果这个概率(p值)高于一个给定阈值(通常是5%,由超参数控制),那么这个节点看被认为不必要,其子节点可被删除。直到所有不必要的节点都被删除,剪枝过程结束
回归
- 回归树每个节点不再预测一个类别,而是预测一个值,每个区域的预测值永远等于该区域内实例的目标平均值。算法分裂每个区域的方法就是使最多的训练实例尽可能接近这个预测值。
- CART回归树不再尝试以最小化不纯度的方式来拆分训练集,而是以最小化MSE的方式来拆分训练集
\[CART回归树成本函数:\\
J(k,t_k)=\frac{m_{left}}{m}MSE_{left}+\frac{m_{right}}{m}MSE_{right} \\
其中,\left\{\begin{matrix}
MSE_{node}=\sum_{i∈node}(\hat y_{node}-y^{(i)})^2 \\
\hat y_{node}=\frac{\sum_{i∈node}y^{(i)}}{m_{node}}
\end{matrix}\right .
\]
不稳定性
- 决策树喜欢正交的决策边界(所有分割都垂直于轴),这使它们对训练集旋转敏感
- 限制此问题的一种方法是使用主成分分析
代码部分
引入
import sys
assert sys.version_info >= (3, 5)
import sklearn
assert sklearn.__version__ >= '0.20'
import numpy as np
import os
np.random.seed(42)
%matplotlib inline
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.rc('axes', labelsize=14)
mpl.rc('xtick', labelsize=12)
mpl.rc('ytick', labelsize=12)
PROJECT_ROOT_DIR = '.'
CHAPTER_ID = 'decision_trees'
IMAGES_PATH = os.path.join(PROJECT_ROOT_DIR, 'images', CHAPTER_ID)
os.makedirs(IMAGES_PATH, exist_ok=True)
def save_fig(fig_id, tight_layout=True, fig_extension='png', resolution=300):
path = os.path.join(IMAGES_PATH, fig_id + '.' + fig_extension)
print('Saving figure', fig_id)
if tight_layout:
plt.tight_layout()
plt.savefig(path, format=fig_extension, dpi=resolution)
训练和可视化
from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier
iris = load_iris()
X = iris.data[:, 2:]
y = iris.target
tree_clf = DecisionTreeClassifier(max_depth=2, random_state=42)
tree_clf.fit(X, y)
import os
os.environ['PATH'] += os.pathsep + 'C:/Program Files/Graphviz/bin/'
import pydotplus
from IPython.display import Image
from six import StringIO
from sklearn.tree import export_graphviz
with open(os.path.join(IMAGES_PATH, 'iris_tree.dot'), 'w') as fp:
export_graphviz(tree_clf, out_file=fp)
dot_data = StringIO()
export_graphviz(
tree_clf,
out_file=dot_data,
feature_names=iris.feature_names[2:],
class_names=iris.target_names,
rounded=True,
filled=True,
special_characters=True
)
graph = pydotplus.graph_from_dot_data(dot_data.getvalue())
Image(graph.create_png())
from matplotlib.colors import ListedColormap
def plot_decision_boundary(clf, X, y, axes=[0, 7.5, 0, 3], iris=True, legend=False, plot_training=True):
x1s = np.linspace(axes[0], axes[1], 100)
x2s = np.linspace(axes[2], axes[3], 100)
x1, x2 = np.meshgrid(x1s, x2s)
X_new = np.c_[x1.ravel(), x2.ravel()]
y_pred = clf.predict(X_new).reshape(x1.shape)
custom_cmap = ListedColormap(['#fafab0', '#9898ff', '#a0faa0'])
# f:filled,也即对等高线间的填充区域进行填充(使用不同的颜色)
# contourf:将不会再绘制等高线(显然不同的颜色分界就表示等高线本身)
plt.contourf(x1, x2, y_pred, alpha=0.3, cmap=custom_cmap)
if not iris:
custom_cmap2 = ListedColormap(['#7d7d58', '#4c4c7f', '#507d50'])
plt.contour(x1, x2, y_pred, cmap=custom_cmap2, alpha=0.8)
if plot_training:
plt.plot(X[:, 0][y==0], X[:, 1][y==0], 'yo', label='Iris setosa')
plt.plot(X[:, 0][y==1], X[:, 1][y==1], 'bs', label='Iris versicolor')
plt.plot(X[:, 0][y==2], X[:, 1][y==2], 'g^', label='Iris virginica')
plt.axis(axes)
if iris:
plt.xlabel('Petal length', fontsize=14)
plt.ylabel('Petal width', fontsize=14)
else:
plt.xlabel(r'$x_1$', fontsize=18)
plt.ylabel(r'$x_2$', fontsize=18, rotation=0)
if legend:
plt.legend(loc='lower right', fontsize=14)
plt.figure(figsize=(8, 4))
plot_decision_boundary(tree_clf, X, y)
plt.plot([2.45, 2.45], [0, 3], 'k-', linewidth=2)
plt.plot([2.45, 7.5], [1.75, 1.75], 'k--', linewidth=2)
plt.plot([4.95, 4.95], [0, 1.75], 'k:', linewidth=2)
plt.plot([4.85, 4.85], [1.75, 3], 'k:', linewidth=2)
plt.text(1.40, 1.0, 'Depth=0', fontsize=15)
plt.text(3.2, 1.80, 'Depth=1', fontsize=13)
plt.text(4.05, 0.5, '(Depth=2)', fontsize=11)
save_fig('decision_tree_decision_boundaries_plot')
plt.show()
预测类别和概率
tree_clf.predict_proba([[5, 1.5]]) # array([[0. , 0.90740741, 0.09259259]])
tree_clf.predict([[5, 1.5]]) # array([1])
高波动性
tree_clf_tweaked = DecisionTreeClassifier(max_depth=2, random_state=40)
tree_clf_tweaked.fit(X, y)
plt.figure(figsize=(8, 4))
plot_decision_boundary(tree_clf_tweaked, X, y, legend=False)
plt.plot([0, 7.5], [0.8, 0.8], 'k-', linewidth=2)
plt.plot([0, 7.5], [1.75, 1.75], 'k--', linewidth=2)
plt.text(1.0, 0.9, 'Depth=0', fontsize=15)
plt.text(1.0, 1.80, 'Depth=1', fontsize=13)
save_fig('decision_tree_instability_plot')
plt.show()
正则化
from sklearn.datasets import make_moons
Xm, ym = make_moons(n_samples=100, noise=0.25, random_state=53)
deep_tree_clf1 = DecisionTreeClassifier(random_state=42)
deep_tree_clf2 = DecisionTreeClassifier(min_samples_leaf=4, random_state=42)
deep_tree_clf1.fit(Xm, ym)
deep_tree_clf2.fit(Xm, ym)
fig, axes = plt.subplots(ncols=2, figsize=(10, 4), sharey=True)
plt.sca(axes[0])
plot_decision_boundary(deep_tree_clf1, Xm, ym, axes=[-1.5, 2.4, -1, 1.5], iris=False)
plt.title('No restrictions', fontsize=16)
plt.sca(axes[1])
plot_decision_boundary(deep_tree_clf2, Xm, ym, axes=[-1.5, 2.4, -1, 1.5], iris=False)
plt.title('min_samples_leaf = {}'.format(deep_tree_clf2.min_samples_leaf), fontsize=14)
plt.ylabel('')
save_fig('min_samples_leaf_plot')
plt.show()
对旋转敏感
angle = np.pi / 180 * 20
rotation_matrix = np.array([[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]])
Xr = X.dot(rotation_matrix)
tree_clf_r = DecisionTreeClassifier(random_state=42)
tree_clf_r.fit(Xr, y)
plt.figure(figsize=(8, 3))
plot_decision_boundary(tree_clf_r, Xr, y, axes=[0.5, 7.5, -1.0, 1], iris=False)
plt.show()
np.random.seed(6)
Xs = np.random.rand(100, 2) - 0.5
ys = (Xs[:, 0] > 0).astype(np.float32) * 2
angle = np.pi / 4
rotation_matrix = np.array([[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]])
Xsr = Xs.dot(rotation_matrix)
tree_clf_s = DecisionTreeClassifier(random_state=42)
tree_clf_s.fit(Xs, ys)
tree_clf_sr = DecisionTreeClassifier(random_state=42)
tree_clf_sr.fit(Xsr, ys)
fig, axes = plt.subplots(ncols=2, figsize=(10, 4), sharey=True)
plt.sca(axes[0])
plot_decision_boundary(tree_clf_s, Xs, ys, axes=[-0.7, 0.7, -0.7, 0.7], iris=False)
plt.sca(axes[1])
plot_decision_boundary(tree_clf_sr, Xsr, ys, axes=[-0.7, 0.7, -0.7, 0.7], iris=False)
plt.ylabel('')
save_fig('sensitivity_to_rotation_plot')
plt.show()
回归树
np.random.seed(42)
m = 200
X = np.random.rand(m, 1)
y = 4 * (X - 0.5) ** 2
y = y + np.random.randn(m, 1) / 10
from sklearn.tree import DecisionTreeRegressor
tree_reg = DecisionTreeRegressor(max_depth=2, random_state=42)
tree_reg.fit(X, y)
from sklearn.tree import DecisionTreeRegressor
tree_reg1 = DecisionTreeRegressor(random_state=42, max_depth=2)
tree_reg2 = DecisionTreeRegressor(random_state=42, max_depth=3)
tree_reg1.fit(X, y)
tree_reg2.fit(X, y)
def plot_regression_predictions(tree_reg, X, y, axes=[0, 1, -0.2, 1], ylabel='$y$'):
x1 = np.linspace(axes[0], axes[1], 500).reshape(-1, 1)
y_pred = tree_reg.predict(x1)
plt.axis(axes)
plt.xlabel('$x_1$', fontsize=18)
if ylabel:
plt.ylabel(ylabel, fontsize=18, rotation=0)
plt.plot(X, y, 'b.')
plt.plot(x1, y_pred, 'r.-', linewidth=2, label=r'$\hat {y}$')
fig, axes = plt.subplots(ncols=2, figsize=(10, 4), sharey=True)
plt.sca(axes[0])
plot_regression_predictions(tree_reg1, X, y)
for split, style in ((0.1973, 'k-'), (0.0917, 'k--'), (0.7718, 'k--')):
plt.plot([split, split], [-0.2, 1], style, linewidth=2)
plt.text(0.21, 0.65, 'Depth=0', fontsize=15)
plt.text(0.01, 0.2, 'Depth=1', fontsize=13)
plt.text(0.65, 0.8, 'Depth=1', fontsize=13)
plt.legend(loc='upper center', fontsize=18)
plt.title('max_depth=2', fontsize=14)
plt.sca(axes[1])
plot_regression_predictions(tree_reg2, X, y, ylabel=None)
for split, style in ((0.1973, 'k-'), (0.0917, 'k--'), (0.7718, 'k--')):
plt.plot([split, split], [-0.2, 1], style, linewidth=2)
for split in (0.0458, 0.1298, 0.2873, 0.9040):
plt.plot([split, split], [-0.2, 1], 'k:', linewidth=1)
plt.text(0.3, 0.5, 'Depth=2', fontsize=13)
plt.title('max_depth=3', fontsize=14)
save_fig('tree_regression_plot')
plt.show()
import os
os.environ['PATH'] += os.pathsep + 'C:/Program Files/Graphviz/bin/'
import pydotplus
from IPython.display import Image
from six import StringIO
from sklearn.tree import export_graphviz
with open(os.path.join(IMAGES_PATH, 'regression_tree.dot'), 'w') as fp:
export_graphviz(tree_clf, out_file=fp)
dot_data = StringIO()
export_graphviz(
tree_reg1,
out_file=dot_data,
feature_names=['x1'],
class_names=iris.target_names,
rounded=True,
filled=True,
special_characters=True
)
graph = pydotplus.graph_from_dot_data(dot_data.getvalue())
Image(graph.create_png())
回归树正则化
tree_reg1 = DecisionTreeRegressor(random_state=42)
tree_reg2 = DecisionTreeRegressor(random_state=42, min_samples_leaf=10)
tree_reg1.fit(X, y)
tree_reg2.fit(X, y)
x1 = np.linspace(0, 1, 500).reshape(-1, 1)
y_pred1 = tree_reg1.predict(x1)
y_pred2 = tree_reg2.predict(x1)
fig, axes = plt.subplots(ncols=2, figsize=(10, 4), sharey=True)
plt.sca(axes[0])
plt.plot(X, y, 'b.')
plt.plot(x1, y_pred1, r'.-', linewidth=2, label=r'$\hat {y}$')
plt.axis([0, 1, -0.2, 1.1])
plt.xlabel('$x_1$', fontsize=18)
plt.ylabel('$y$', fontsize=18, rotation=0)
plt.legend(loc='upper center', fontsize=18)
plt.title('No restrictions', fontsize=14)
plt.sca(axes[1])
plt.plot(X, y, 'b.')
plt.plot(x1, y_pred2, 'r.-', linewidth=2, label=r'$\hat {y}$')
plt.axis([0, 1, -0.2, 1.1])
plt.xlabel('$x_1$', fontsize=18)
plt.title('min_samples_leaf={}'.format(tree_reg2.min_samples_leaf), fontsize=14)
save_fig('tree_regression_regularization_plot')
plt.show()