本篇承接上篇文章监督学习算法——线性模型
决策树
import sys
sys.path
mglearn.plots.plot_animal_tree()
1.构建决策树
我们在下图所示的二位分类数据集上构造决策树。这个数据集由2个半月形组成,每个类别都包含50个数据点。我们将这个数据集称为two_moons。
学习决策树,就是学习一系列if/else问题,使我们能够以最快的速度得到正确答案。在机器学习中国,这些问题叫做测试(切记,不要和测试集弄混,测试集是用来测试模型泛化性能的数据)。数据通常并不是像动物的例子那样具有二元特征的形式,而是表示为连续特征,比如下图所示的二维数据集。用于连续数据的测试形式是:“特征i的值是否大于a?”
mglearn.plots.plot_tree_progressive()
为了构造决策树,算法搜遍所有的可能的测试,找出对目标变量来说信息量最大的那一个。下图展示了选出第一个测试。将数据集在x[1]=0.0596处垂直划分可以得到最多信息,它在最大程度上将类别0中的50个点和属于类别1的50个点。通过测试x[1]<=0.0596的真假来对数据集进行划分,在图中表示为一条黑线。如果测试结果为真,那么将这个点分配给左结点,左结点里包含属于类别0的2个点和属于类别1的21个点。否则将这个点分配给右结点,右结点里包含属于类别0的48个点和属于类别1的18个点。
这一递归过程生成一棵二元决策树,其中每个结点都包含一个测试。或者可以将每个测试看成沿着一条轴对当前数据进行的划分。这是一种将算法看作分层划分的观点。由于每个测试仅关注一个特征,所以划分后的区域边界始终与坐标轴平行。
对数据反复进行递归划分,知道划分后的每一个区域只包含单一目标值。如果树中某个叶结点所包含数据点的目标值都相同,那么这个叶结点就是纯的。
想要对新数据点进行预测,首先要查看这个点位于特征空间划分的哪个区域,然后将该区域的多目标值作为预测结果。从分界点开始对树进行遍历就可以找到这一区域,每一步向左还是向右取决于是否满足相应的测试。
决策树也可以用于回归任务,使用的方法完全相同。预测是方法是,基于每个结点的测试对树遍历,最终找到新数据点所属的叶结点。这一数据点的输出即为叶结点中所有训练点的平均目标值。
2.控制决策树的复杂度
通常来说,构造决策树直到所有叶结点都是纯的叶结点,这会导致模型非常复杂,开且对
训练数据高度过拟合。纯叶结点的存在说明这棵树在训练集上的精度是100%。训练集中的每个数据点都位于分类正确的叶结点中。在上图的左图中可以看出过拟合。可以看到,在所有属于类别0的点中间有一块属于类别1的区域。另一方面,有一小条属于类别0的区域,包围着最右侧属于类别0的那个点。这并不是人们想象中决策边界的样子,这个决策边界过于关注远离同类别其他点的单个异常点。
防止过拟合有两种常见的策略:一种是及早停止树的生长,也叫预剪枝(pre-pruning) ;另一种是先构造树,但随后删除或折叠信息量很少的结点,也叫后剪枝(post-pruning) 或剪枝(pruning)。预剪枝的限制条件可能包括限制树的最大深度、限制叶结点的最大数目,或者规定一一个结点中数据点的最小数目来防止继续划分。
scikit-learn的决策树在DecisionTreeRegressor类和DecisionTreeClassifier类中实现。scikit- learn只实现了预剪枝,没有实现后剪枝。
我们在乳腺癌数据集上更详细地看一下预剪枝的效果。和前面一样, 我们导入数据集并将
其分为训练集和测试集。然后利用默认设置来构建模型,默认将树完全展开(树不断分
支,直到所有叶结点都是纯的)。我们固定树的rano_tate, 用于在内部解决平局问题:
from sklearn.tree import DecisionTreeClassifier
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
cancer.data, cancer.target, stratify=cancer.target, random_state=42)
tree = DecisionTreeClassifier(random_state=0)
tree.fit(X_train, y_train)
print("Accuracy on training set: {:.3f}".format(tree.score(X_train, y_train)))
print("Accuracy on test set: {:.3f}".format(tree.score(X_test, y_test)))
Accuracy on training set: 1.000
Accuracy on test set: 0.937
不出所料,训练集的精度是100%,这是因为叶结点都是纯的,树的深度很大,足以完美地记住训练数据地所有标签。测试集精度比之前讲过地线性模型略低,线性模型地精度约为95%。
如果我们不限制决策树地深度,它地深度和复杂度都可以变得很大。因此,未剪枝地树容易过拟合,对新数据的泛化性能不佳。现在我们将预剪枝应用在决策树上,这可以在完美拟合训练数据之前阻止树的展开。一种选择是在到达一定深度后停止对树的展开。这是max_depth=4,这意味着可以连续问4个问题。限制深度可以减少过拟合。这会降低训练集的精度,但可以提高测试集的精度:
tree = DecisionTreeClassifier(max_depth=4, random_state=0)
tree.fit(X_train, y_train)
print("Accuracy on training set: {:.3f}".format(tree.score(X_train, y_train)))
print("Accuracy on test set: {:.3f}".format(tree.score(X_test, y_test)))
Accuracy on training set: 0.988
Accuracy on test set: 0.951
3.分析决策树
可以利用tree模块的export_graphviz函数来将树可视化。这个函数会生成一个.dot格式的文件。这是一种用于保存图形的文本文件格式,我们设置为结点添加颜色的选项,颜色表示每个结点的多数类别,同时传入类别名称和特征名称,这样可以对树正确标记:
from sklearn.tree import export_graphviz
export_graphviz(tree, out_file="tree.dot", class_names=["malignant", "benign"],
feature_names=cancer.feature_names, impurity=False, filled=True)
可以利用graphviz模块读取这个文件并将其可视化
import graphviz
with open("tree.dot") as f:
dot_graph = f.read()
display(graphviz.Source(dot_graph))
树的可视化有助于深入理解算法是如何进行预测的,也是易于向非专家解释机器学习算法的优秀事例。不过,及时这里的树只有四层,也有点太大了。深度更大的树,更加难以理解。一种观察树的方法可能有用,就是找出大部分数据的实际路径。上图中每个结点的samples给出了该结点中的样本个数,values给出的是每个类别的样本个数。观察wort radius<=16.795分支右侧的子结点,我们发现它只是包含8个良性样本,但有134个恶性样本。树的这一侧的其余分支只是利用一些更精细的区别将这8个良性样本分离出来。在第一次划分右侧的142个样本中,几乎所有样本最后都进入最右侧的叶结点中。
再来看一下根结点的左侧子结点,对于worst radius >16.795,我们得到25个恶性样本和259个良性样本。几乎所有良性样本最终都进入左数第二个叶结点中,大部分其他叶结点都只包含很少的样本。
4.树的特征重要性
查看整棵树可能非常费劲,除此之外,我还可以利用一些有用的属性来总结树的工作原理。其中最常用的特征重要性,它为每个特征对树的决策的重要性进行排序。对于每个特征来说,都是介于0和1之间的数字,其中0表示“根本没用到”,1表示“完美预测目标值”。特征重要性的求和始终为1:
print("Feature importances:")
print(tree.feature_importances_)
Feature importances:
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.01 0.048
0. 0. 0.002 0. 0. 0. 0. 0. 0.727 0.046 0. 0.
0.014 0. 0.018 0.122 0.012 0. ]
def plot_feature_importances_cancer(model):
n_features = cancer.data.shape[1]
plt.barh(np.arange(n_features), model.feature_importances_, align='center')
plt.yticks(np.arange(n_features), cancer.feature_names)
plt.xlabel("Feature importance")
plt.ylabel("Feature")
plt.ylim(-1, n_features)
plot_feature_importances_cancer(tree)
这里可以看到,顶部划分用到的特征(“worst radius”)是最重要的特征。这也证实了在分析树时的观察言论,即第一层划分已经将两个类别区分的=得很好。
但是,如果某个特征得fearyre_importance_很小,并不能说明这个特征没有提供任何信息。这只能说明该特征没有被选中,可能时因为另一个特征也包含了同样得信息。
与线性模型得系数不同,特征重要性始终为正数,也不能说明该特征对应哪个类别。
tree = mglearn.plots.plot_tree_not_monotone()
display(tree)
Feature importances: [0. 1.]
该图显示的是有两个特征和两个类别的数据集。这里所包含信息都包含在x[1]中,没有用到x[0]。但x[1]和输出类别之间并不是单调关系。较大的x[1]对应类别0,较小的x[1]对应类别1.
虽然我们主要讨论的是用于分类的决策树,但对用于回归的决策树来说,所有内容都是类似的,在DecisionTreeRegressor中实现。回归树的用法和分析与分类树非常相似。但在将其基于树的模型用于回归时,我们想要指出它的一个特殊性质。DecisionTreeRegressor不能外推,也不能在训练数据范围之外进行预测。
import os
ram_prices = pd.read_csv(os.path.join(mglearn.datasets.DATA_PATH, "ram_price.csv"))
plt.semilogy(ram_prices.date, ram_prices.price)
plt.xlabel("Year")
plt.ylabel("Price in $/Mbyte")
Text(0, 0.5, 'Price in $/Mbyte')
注意y轴的对数刻度,在用对数坐标绘图时,二者的线性关系看起来非常好,所以预测应该相对比较容易,除了一些不平滑之处之外。
我们利用2000年前的历史数据来预测2000年后的价格,只用日期作为特征。我们将对比两个简单的模型:DecisionTreeRegressor和LinearRegression。我们对价格取对数,使得二者的关系的线性相对更好。这对于前者不会产生什么影响,但对后者的影响很大。
训练模型并作出预测之后,我们应用指数映射来做出对数变换的逆运算。为了便于可视化,我们这里对整个数据集进行预测,但如果是为了定量评估,我们只考虑测试数据集:
from sklearn.tree import DecisionTreeRegressor
# use historical data to forecast prices after the year 2000
data_train = ram_prices[ram_prices.date < 2000]
data_test = ram_prices[ram_prices.date >= 2000]
# predict prices based on date
X_train = data_train.date[:, np.newaxis]
# we use a log-transform to get a simpler relationship of data to target
y_train = np.log(data_train.price)
tree = DecisionTreeRegressor(max_depth=3).fit(X_train, y_train)
linear_reg = LinearRegression().fit(X_train, y_train)
# predict on all data
X_all = ram_prices.date[:, np.newaxis]
pred_tree = tree.predict(X_all)
pred_lr = linear_reg.predict(X_all)
# undo log-transform
price_tree = np.exp(pred_tree)
price_lr = np.exp(pred_lr)
plt.semilogy(data_train.date, data_train.price, label="Training data")
plt.semilogy(data_test.date, data_test.price, label="Test data")
plt.semilogy(ram_prices.date, price_tree, label="Tree prediction")
plt.semilogy(ram_prices.date, price_lr, label="Linear prediction")
plt.legend()
<matplotlib.legend.Legend at 0x1624b51f088>
两个模型之间的差异非常明显。线性模型用一条直线对数据做近似,这是我们所知道的。这条线对测试数据(2000年后的价格)给出了相当好的预测,不过忽略了训练数据和测试数据的复杂度,因此它记住了整个数据集。但是,一旦输入超出了模型训练数据的范围,模型就只能持续预测最后一个已知数据点。树不能在训练数据的范围之外生成“新的”响应,所有基于树的模型都有了这个缺点。
5.优点、缺点和参数
如前所述,控制决策树模型复杂度的参数是预剪枝参数,它在树完全展开之前停止树的构
造。通常来说,选择一种预剪枝策略(设置max_depth、max_leaf_nodes或min_samples_
leaf)足以防止过拟合。
与前面讨论过的许多算法相比,决策树有两个优点:一是得到的模型很容易可视化,非
专家也很容易理解(至少对于较小的树而言) ;二是算法完全不受数据缩放的影响。由于
每个特征被单独处理,而且数据的划分也不依赖于缩放,因此决策树算法不需要特征预处
理,比如归一化或标准化。特别是特征的尺度完全不一样时或者二元特征和连续特征同时
存在时,决策树的效果很好。
决策树的主要缺点在于,即使做了预剪枝,它也经常会过拟合,泛化性能很差。因此,在
大多数应用中,往往使用集成方法来替代单棵决策树。