机器学习实践—基于Scikit-Learn、Keras和TensorFlow2第二版—第6章 决策树(Decision Trees)

与SVM类似,决策树可以完成分类和回归任务,甚至可以完成多输出任务。

决策树也是随机森林的基础组成部分,随机森林就是通过组合不同的大小(深度)的决策树达到很好的效果。

0. 导入所需的库

import sklearn
import numpy as np
import matplotlib as mpl
%matplotlib inline
import matplotlib.pyplot as plt
import os

for i in (sklearn, np, mpl):
    print(i.__name__,": ",i.__version__,sep="")

输出:

sklearn: 0.21.3
numpy: 1.17.4
matplotlib: 3.1.2

1. 决策树模型的训练与可视化

导入数据集:

from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier

iris = load_iris()
X = iris.data[:,2:]
y = iris.target

X.shape, y.shape

输出:

((150, 2), (150,))
tree_clf = DecisionTreeClassifier(max_depth=2, random_state=42)
tree_clf.fit(X,y)

输出:

DecisionTreeClassifier(class_weight=None, criterion='gini', max_depth=2,
                       max_features=None, max_leaf_nodes=None,
                       min_impurity_decrease=0.0, min_impurity_split=None,
                       min_samples_leaf=1, min_samples_split=2,
                       min_weight_fraction_leaf=0.0, presort=False,
                       random_state=42, splitter='best')
from graphviz import Source
from sklearn.tree import export_graphviz

image_path = "./images/decision_trees"
os.makedirs(image_path, exist_ok=True)

export_graphviz(tree_clf,
               out_file=os.path.join(image_path,"iris_tree.dot"),
               feature_names=iris.feature_names[2:],
               class_names=iris.target_names,
               rounded=True,
               filled=True)
Source.from_file(os.path.join(image_path,"iris_tree.dot"))

输出:

如上所示为构建的决策树模型,通过以上构建决策树模型发现,决策树算法不要求训练集归一化和中心化。

2. 决策树模型预测

上述图中,节点的samples属性表示属于训练集中属于该节点的样本个数,value属性表示属于该节点的每个类别的样本个数,gini属性表示样本类别的混杂度,例如所有样本属于一个类别的,则gini等于1,如果属于两个类别并各占一半,则gini等于0.5。gini计算公式如下:

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'])
    plt.contourf(x1, x2, y_pred, alpha=0.3, cmap=custom_cmap)
    
    if not iris:
        custom_cmap2 = ListedColormap(['#7d7d58','#4c4c7f','#507d50'])
        plt.contourf(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(fontsize=14)
    
plt.figure(figsize=(12,6))
plot_decision_boundary(tree_clf, X, y)

plt.plot([2.45,2.45],[0,3],"k-",linewidth=5)
plt.text(1.70,1.0,"Depth=0",fontsize=15)
plt.plot([2.45,7.5],[1.75,1.75],"k--",linewidth=5)
plt.text(3.8,1.8,"Depth=1",fontsize=15)

plt.plot([4.85,4.85],[1.75,3],"g--",linewidth=3)
plt.plot([4.955,4.955],[0,1.75],"r--",linewidth=3)
plt.text(4.2,0.5,"(Depth=2)",fontsize=15)

plt.tight_layout()
plt.show()

输出:

如上图所示为决策树的决策边界。

黑色直线表示根节点的决策边界,即花瓣长度2.45cm。可以看到,黑色直线左边是一类,右边是两类,所以还需要在右边找到一条直线将蓝色点和绿色点分开。

黑色虚线表示第一层右边节点的决策边界,即花瓣宽度1.75cm。可以看到,决策边界将大部分样本分开了,但有少数蓝色点和绿色点分错了。

如果设置较大的深度,例如模型构建时设置超参数max_depth=3,则可能第二层节点会继续分下去,例如如图中红色虚线和绿色虚线所示,其中绿色虚线为第二层右边节点的决策边界,而红色虚线为第二层左边节点的决策边界。

3. 预测类别及类别概率

决策树模型也可以输出预测样本属于某个类的概率值,这个概率值就是根据节点中value值计算的:

tree_clf.predict_proba([[5,1.5]])

输出:

array([[0.        , 0.90740741, 0.09259259]])
tree_clf.predict([[5,1.5]])

输出:

array([1])

如上输出结果显示,当预测样本花瓣长度为5,宽度为1.5时,则根据上面训练的决策树模型,该样本会落到第二层左边的节点上,则概率分别为0/54=0,49/54=90.7,5/54=9.3%,观察发现与上面的输出一致。

4. CART算法

sklearn使用CART算法进行决策树的训练,CART主要思想就是找到两个子集,使得加权生的纯度最高。找到两个子集后再分别对这两个子集找子集,以次类推,直到达到超参数规定的最大深度,或者无法找到两个加权求和之后的纯度更高的子集。或者可以使用其它超参数控制CART算法的终止,例如min_sampes_split,max_leaf_nodes等。

注意CART算法属于贪婪算法。贪婪算法通常能生成比较好的解决方案,但不保证是最佳的。找到最佳的决策树属于NP-完问题范畴,计算复杂度为O(exp(m)),这对于很小的数据集都比较棘手,更不用对中大型数据集了。

5. 计算复杂度

用决策树预测较大数据集时,运算速度也比较快。预测样本只需要在每层的两个节点中选一个,并且根据所有特征中的一个特征判断选择下一层的哪个节点,因此预测计算复杂度为O(log2(m))。

训练复杂度为O(nmlog2(m)),在每一层需要比较所有样本的所有特征。

6. gini系数

sklearn中默认使用gini 纯度为判断标准,也可以手动指定gini熵(criterion=entropy)为判断标准。

熵的概念源于热力学,其用来表征分子无序的一种度量,当分子处理静止且有序时,熵为0。后来被广泛应用于其它领域,例如香农信息学领域,用来表征信息的一致程度。

在机器学习中,熵的含义是如果样本都属于一个类别,则熵为0,计算公式如下:

7. 正则化超参数

决策树算法几乎对训练数据没什么要求,决策树会尽可能地去拟合训练数据,因此过拟合是决策树经常发生的问题。

sklearn中可以通过max_depth超参数设置树的最大深度,min_samples_split节点中样本个数大于这个值时才会继续分隔,min_samples_leaf叶子节点最小样本个数,等等。

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=(12, 5), 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("")

plt.tight_layout()
plt.show()

输出:

如上输出,左图模型使用sklearn默认超参数,右图模型为指定叶节节点样本个数大于等于4。观察发现,左图有明显的过拟合,而右图模型是比较好的结果。

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=(12, 5))
plot_decision_boundary(tree_clf_r, Xr, y, axes=[0.5, 7.5, -1.0, 1], iris=False)

plt.show()

输出:

8. 决策树完成回归任务

决策树不仅能完成分类任务,也能完成回归任务。

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

X.shape, y.shape

输出:

((200, 1), (200, 1))
from sklearn.tree import DecisionTreeRegressor

tree_reg = DecisionTreeRegressor(max_depth=2, random_state=42)
tree_reg.fit(X,y)

输出:

DecisionTreeRegressor(criterion='mse', max_depth=2, max_features=None,
                      max_leaf_nodes=None, min_impurity_decrease=0.0,
                      min_impurity_split=None, min_samples_leaf=1,
                      min_samples_split=2, min_weight_fraction_leaf=0.0,
                      presort=False, random_state=42, splitter='best')
export_graphviz(tree_reg,
               out_file=os.path.join(image_path,"regression_tree.dot"),
               feature_names=["x1"],
               rounded=True,
               filled=True)
Source.from_file(os.path.join(image_path,"regression_tree.dot"))

输出:

生成的决策树如上图所示。从形状上看起来与决策树分类模型很类似,区别在于决策树分类模型返回的是预测的类别,而决策树回归模型返回的数值。

那么,决策树回归模型是如何预测的呢?假如有预测样本x=0.6,则根据上面决策树会落到第二层第三个节点上,那么预测的结果就是0.111。这个0.111是训练过程中落到这个节点上的110个样本的平均值,并且这110个样本的均方差是0.015。

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=(12,5),sharey=True)

plt.sca(axes[0])
plot_regression_predictions(tree_reg1, X, y)
plt.legend(fontsize=18)
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.title("max_depth=2", fontsize=14)

plt.sca(axes[1])
plot_regression_predictions(tree_reg2, X, y, ylabel=None)
plt.legend(fontsize=18)
plt.title("max_depth=3",fontsize=14)
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.tight_layout()
plt.show()

输出:

如上图所示,左图是最大深度为2的决策树回归模型,右图是最大深度为3的决策树回归模型。

CART与之前的类似,目标是不断地拆分训练集,使得圴方误差MSE最小。

与决策树分类算法类似,决策树回归模型也同样很容易过拟合:

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=(12, 5), 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)

plt.tight_layout()
plt.show()

输出:

如上输出所示,左图为使用sklearn默认的参数,右图为指定超参数min_samples_leaf=10。观察可以发现,使用默认参数的左图模型很明显处理过拟合状态,右图模型效果似乎稍微好一些。

9. 不稳定性

决策树易于理解,解释性强,容易使用并且功能比较强大。但是,也有一些缺点或不足:

一是决策树决策边界都是正交的,即决策边界都垂直于坐标轴。换句话说,决策树模型对训练集的方向(或旋转)比较敏感。请看下面这个例子:

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=(12, 5), 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("")

plt.tight_layout()
plt.show()

输出:

如上输出所示,左图为原始数据训练的决策树分类模型,分类效果很好。右图是将训练数据进行了45度的旋转,此时模型就变得很复杂,需要5条决策边界才能将数据分开。对于右图数据,如果使用线性回归模型,一条直接几乎很轻松就能把两个类别分开。

二是决策树模型对训练数据细微的变化很敏感。请看下面这个例子:

X[(X[:, 1]==X[:, 1][y==1].max()) & (y==1)] # 花瓣最宽的Iris versicolor

not_widest_versicolor = (X[:, 1]!=1.8) | (y==2)  # 从数据集中删除花瓣最宽的数据
X_tweaked = X[not_widest_versicolor]
y_tweaked = y[not_widest_versicolor]

tree_clf_tweaked = DecisionTreeClassifier(max_depth=2, random_state=40)
tree_clf_tweaked.fit(X_tweaked, y_tweaked)

输出:

DecisionTreeClassifier(class_weight=None, criterion='gini', max_depth=2,
                       max_features=None, max_leaf_nodes=None,
                       min_impurity_decrease=0.0, min_impurity_split=None,
                       min_samples_leaf=1, min_samples_split=2,
                       min_weight_fraction_leaf=0.0, presort=False,
                       random_state=40, splitter='best')
plt.figure(figsize=(12,5))

plt.subplot(122)
plot_decision_boundary(tree_clf_tweaked, X_tweaked, y_tweaked, 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)
plt.ylabel("")
plt.title("Non Widest versicolor")

plt.subplot(121)
plot_decision_boundary(tree_clf, X, y, legend=False)
plt.title("All datasets")
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.text(1.10, 1.0, "Depth=0", fontsize=15)
plt.text(3.2, 1.80, "Depth=1", fontsize=13)

plt.tight_layout()
plt.show()

输出:

如上输出所示,左图是原始数据上训练的决策树模型,而右图是删除最宽花瓣的数据后训练的决策模型,对比左右模型发现,两者是完全不同的两个模型。说明决策树模型对训练数据微小的变化很敏感。

同时,sklearn中决策树的训练算法是随机的,利用相同的数据训练决策树模型,每次得到的结果可能都不一样。可以通过设置超参数random_state,可保证每次的随机性一样,类似于随机种子的功能。

 

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值