第 2 章 监督学习(下)

2.3.5 决策树

决策树是广泛用于分类和回归任务的模型。本质上,它从一层层的 if/else 问题中进行学习,并得出结论。
这些问题类似于你在“20 Questions”游戏 9 中可能会问的问题。想象一下,你想要区分下面这四种动物:熊、鹰、企鹅和海豚。你的目标是通过提出尽可能少的 if/else 问题来得到正确答案。你可能首先会问:这种动物有没有羽毛,这个问题会将可能的动物减少到只有两种。如果答案是“有”,你可以问下一个问题,帮你区分鹰和企鹅。例如,你可以问这种动物会不会飞。如果这种动物没有羽毛,那么可能是海豚或熊,所以你需要问一个问题
来区分这两种动物——比如问这种动物有没有鳍。
这一系列问题可以表示为一棵决策树,如图 2-22 所示。

In[56]:
mglearn.plots.plot_animal_tree()
图 2-22:区分几种动物的决策树

在这张图中,树的每个结点代表一个问题或一个包含答案的终结点(也叫叶结点)。树的边将问题的答案与将问的下一个问题连接起来。
用机器学习的语言来说就是,为了区分四类动物(鹰、企鹅、海豚和熊),我们利用三个特征(“有没有羽毛”“会不会飞”和“有没有鳍”)来构建一个模型。我们可以利用监督学习从数据中学习模型,而无需人为构建模型。

1. 构造决策树

我们在图 2-23 所示的二维分类数据集上构造决策树。这个数据集由 2 个半月形组成,每个类别都包含 50 个数据点。我们将这个数据集称为 two_moons。
学习决策树,就是学习一系列 if/else 问题,使我们能够以最快的速度得到正确答案。在机器学习中,这些问题叫作测试(不要与测试集弄混,测试集是用来测试模型泛化性能的数据)。
数据通常并不是像动物的例子那样具有二元特征(是 / 否)的形式,而是表示为连续特征,比如图 2-23 所示的二维数据集。用于连续数据的测试形式是:“特征 i 的值是否大于 a ?”


图 2-23:用于构造决策树的 two_moons 数据集

为了构造决策树,算法搜遍所有可能的测试,找出对目标变量来说信息量最大的那一个。
图 2-24 展示了选出的第一个测试。将数据集在 x[1]=0.0596 处垂直划分可以得到最多信息,它在最大程度上将类别 0 中的点与类别 1 中的点进行区分。顶结点(也叫根结点)表示整个数据集,包含属于类别 0 的 50 个点和属于类别 1 的 50 个点。通过测试 x[1] <=0.0596 的真假来对数据集进行划分,在图中表示为一条黑线。如果测试结果为真,那么将这个点分配给左结点,左结点里包含属于类别 0 的 2 个点和属于类别 1 的 32 个点。否则将这个点分配给右结点,右结点里包含属于类别 0 的 48 个点和属于类别 1 的 18 个点。这两个结点对应于图 2-24 中的顶部区域和底部区域。尽管第一次划分已经对两个类别做了很好的区分,但底部区域仍包含属于类别 0 的点,顶部区域也仍包含属于类别 1 的点。我们可以在两个区域中重复寻找最佳测试的过程,从而构建出更准确的模型。图 2-25 展示了信息量最大的下一次划分,这次划分是基于 x[0] 做出的,分为左右两个区域。


图 2-24:深度为 1 的树的决策边界(左)与相应的树(右)
图 2-25:深度为 2 的树的决策边界(左)与相应的树(右)

这一递归过程生成一棵二元决策树,其中每个结点都包含一个测试。或者你可以将每个测试看成沿着一条轴对当前数据进行划分。这是一种将算法看作分层划分的观点。由于每个测试仅关注一个特征,所以划分后的区域边界始终与坐标轴平行。对数据反复进行递归划分,直到划分后的每个区域(决策树的每个叶结点)只包含单一目标值(单一类别或单一回归值)。如果树中某个叶结点所包含数据点的目标值都相同,那么这个叶结点就是纯的(pure)。这个数据集的最终划分结果见图 2-26。


图 2-26:深度为 9 的树的决策边界(左)与相应的树的一部分(右);完整的决策树非常大,很难可视化

想要对新数据点进行预测,首先要查看这个点位于特征空间划分的哪个区域,然后将该区域的多数目标值(如果是纯的叶结点,就是单一目标值)作为预测结果。从根结点开始对树进行遍历就可以找到这一区域,每一步向左还是向右取决于是否满足相应的测试。决策树也可以用于回归任务,使用的方法完全相同。预测的方法是,基于每个结点的测试对树进行遍历,最终找到新数据点所属的叶结点。这一数据点的输出即为此叶结点中所有训练点的平均目标值。

2. 控制决策树的复杂度

通常来说,构造决策树直到所有叶结点都是纯的叶结点,这会导致模型非常复杂,并且对训练数据高度过拟合。纯叶结点的存在说明这棵树在训练集上的精度是 100%。训练集中的每个数据点都位于分类正确的叶结点中。在图 2-26 的左图中可以看出过拟合。你可以看到,在所有属于类别 0 的点中间有一块属于类别 1 的区域。另一方面,有一小条属于类别 0 的区域,包围着最右侧属于类别 0 的那个点。这并不是人们想象中决策边界的样子,这个决策边界过于关注远离同类别其他点的单个异常点。
防止过拟合有两种常见的策略:一种是及早停止树的生长,也叫预剪枝(pre-pruning);另一种是先构造树,但随后删除或折叠信息量很少的结点,也叫后剪枝(post-pruning)或剪枝(pruning)。预剪枝的限制条件可能包括限制树的最大深度、限制叶结点的最大数目,或者规定一个结点中数据点的最小数目来防止继续划分。scikit-learn 的决策树在 DecisionTreeRegressor 类和 DecisionTreeClassifier 类中实现。
scikit-learn 只实现了预剪枝,没有实现后剪枝。
我们在乳腺癌数据集上更详细地看一下预剪枝的效果。和前面一样,我们导入数据集并将其分为训练集和测试集。然后利用默认设置来构建模型,默认将树完全展开(树不断分支,直到所有叶结点都是纯的)。我们固定树的 random_state,用于在内部解决平局问题:

In[58]:
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)))
Out[58]:
Accuracy on training set: 1.000
Accuracy on test set: 0.937

不出所料,训练集上的精度是 100%,这是因为叶结点都是纯的,树的深度很大,足以完美地记住训练数据的所有标签。测试集精度比之前讲过的线性模型略低,线性模型的精度约为 95%。
如果我们不限制决策树的深度,它的深度和复杂度都可以变得特别大。因此,未剪枝的树容易过拟合,对新数据的泛化性能不佳。现在我们将预剪枝应用在决策树上,这可以在完美拟合训练数据之前阻止树的展开。一种选择是在到达一定深度后停止树的展开。这里我们设置 max_depth=4,这意味着只可以连续问 4 个问题(参见图 2-24 和图 2-26)。限制树的深度可以减少过拟合。这会降低训练集的精度,但可以提高测试集的精度:

In[59]:
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)))
Out[59]:
Accuracy on training set: 0.988
Accuracy on test set: 0.951

3. 分析决策树

我们可以利用 tree 模块的 export_graphviz 函数来将树可视化。这个函数会生成一个 .dot 格式的文件,这是一种用于保存图形的文本文件格式。我们设置为结点添加颜色的选项,颜色表示每个结点中的多数类别,同时传入类别名称和特征名称,这样可以对树正确标记:

In[60]:
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)

4. 树的特征重要性

查看整个树可能非常费劲,除此之外,我还可以利用一些有用的属性来总结树的工作原理。其中最常用的是特征重要性(feature importance),它为每个特征对树的决策的重要性进行排序。对于每个特征来说,它都是一个介于 0 和 1 之间的数字,其中 0 表示“根本没用到”, 1 表示“完美预测目标值”。特征重要性的求和始终为 1:

In[62]:
print("Feature importances:\n{}".format(tree.feature_importances_))
Out[62]:
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.   ]

我们可以将特征重要性可视化,与我们将线性模型的系数可视化的方法类似(图 2-28):


图 2-28:在乳腺癌数据集上学到的决策树的特征重要性
In[63]:
def plot_feature_importances_cancer(model):
    n_features = cancer.data.shape[1]
    plt.barh(range(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”)是最重要的特征。这也证实了我们在分析树时的观察结论,即第一层划分已经将两个类别区分得很好。
但是,如果某个特征的 feature_importance_ 很小,并不能说明这个特征没有提供任何信息。这只能说明该特征没有被树选中,可能是因为另一个特征也包含了同样的信息。
与线性模型的系数不同,特征重要性始终为正数,也不能说明该特征对应哪个类别。特征重要性告诉我们“worst radius”(最大半径)特征很重要,但并没有告诉我们半径大表示样本是良性还是恶性。事实上,在特征和类别之间可能没有这样简单的关系,你可以在下面的例子中看出这一点:

In[64]:
tree = mglearn.plots.plot_tree_not_monotone()
display(tree)
Out[64]:
Feature importances: [ 0. 1.]
图 2-29:一个二维数据集(y 轴上的特征与类别标签是非单调的关系)与决策树给出的决策边界

虽然我们主要讨论的是用于分类的决策树,但对用于回归的决策树来说,所有内容都是类似的,在 DecisionTreeRegressor 中实现。回归树的用法和分析与分类树非常类似。但在将基于树的模型用于回归时,我们想要指出它的一个特殊性质。 DecisionTreeRegressor(以及其他所有基于树的回归模型)不能外推(extrapolate),也不能在训练数据范围之外进行预测。
我们利用计算机内存(RAM)历史价格的数据集来更详细地研究这一点。图 2-31 给出了这个数据集的图像, x 轴为日期, y 轴为那一年 1 兆字节(MB) RAM 的价格:


图 2-31:用对数坐标绘制 RAM 价格的历史发展
In[65]:
import pandas as pd
ram_prices = pd.read_csv("data/ram_price.csv")
plt.semilogy(ram_prices.date, ram_prices.price)
plt.xlabel("Year")
plt.ylabel("Price in $/Mbyte")

注意 y 轴的对数刻度。在用对数坐标绘图时,二者的线性关系看起来非常好,所以预测应该相对比较容易,除了一些不平滑之处之外。
我们将利用 2000 年前的历史数据来预测 2000 年后的价格,只用日期作为特征。我们将对比两个简单的模型: DecisionTreeRegressor 和 LinearRegression。我们对价格取对数,使得二者关系的线性相对更好。这对 DecisionTreeRegressor 不会产生什么影响,但对LinearRegression 的影响却很大(我们将在第 4 章中进一步讨论)。训练模型并做出预测之后,我们应用指数映射来做对数变换的逆运算。为了便于可视化,我们这里对整个数据集进行预测,但如果是为了定量评估,我们将只考虑测试数据集:

In[66]:
from sklearn.tree import DecisionTreeRegressor
# 利用历史数据预测2000年后的价格
data_train = ram_prices[ram_prices.date < 2000]
data_test = ram_prices[ram_prices.date >= 2000]
# 基于日期来预测价格
X_train = data_train.date[:, np.newaxis]
# 我们利用对数变换得到数据和目标之间更简单的关系
y_train = np.log(data_train.price)
tree = DecisionTreeRegressor().fit(X_train, y_train)
linear_reg = LinearRegression().fit(X_train, y_train)
# 对所有数据进行预测
X_all = ram_prices.date[:, np.newaxis]
pred_tree = tree.predict(X_all)
pred_lr = linear_reg.predict(X_all)
# 对数变换逆运算
price_tree = np.exp(pred_tree)
price_lr = np.exp(pred_lr)

这里创建的图 2-32 将决策树和线性回归模型的预测结果与真实值进行对比:

In[67]:
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()
图 2-32:线性模型和回归树对 RAM 价格数据的预测结果对比

两个模型之间的差异非常明显。线性模型用一条直线对数据做近似,这是我们所知道的。这条线对测试数据(2000 年后的价格)给出了相当好的预测,不过忽略了训练数据和测试数据中一些更细微的变化。与之相反,树模型完美预测了训练数据。由于我们没有限制树的复杂度,因此它记住了整个数据集。但是,一旦输入超出了模型训练数据的范围,模型就只能持续预测最后一个已知数据点。树不能在训练数据的范围之外生成“新的”响应。
所有基于树的模型都有这个缺点。

5. 优点、 缺点和参数

如前所述,控制决策树模型复杂度的参数是预剪枝参数,它在树完全展开之前停止树的构造。通常来说,选择一种预剪枝策略(设置 max_depth、 max_leaf_nodes 或 min_samples_leaf)足以防止过拟合。
与前面讨论过的许多算法相比,决策树有两个优点:一是得到的模型很容易可视化,非专家也很容易理解(至少对于较小的树而言);二是算法完全不受数据缩放的影响。由于每个特征被单独处理,而且数据的划分也不依赖于缩放,因此决策树算法不需要特征预处理,比如归一化或标准化。特别是特征的尺度完全不一样时或者二元特征和连续特征同时存在时,决策树的效果很好。
决策树的主要缺点在于,即使做了预剪枝,它也经常会过拟合,泛化性能很差。因此,在大多数应用中,往往使用下面介绍的集成方法来替代单棵决策树。

2.3.6 决策树集成

集成(ensemble)是合并多个机器学习模型来构建更强大模型的方法。在机器学习文献中有许多模型都属于这一类,但已证明有两种集成模型对大量分类和回归的数据集都是有效的,二者都以决策树为基础,分别是随机森林(random forest)和梯度提升决策树(gradient boosted decision tree)。

1. 随机森林

我们刚刚说过,决策树的一个主要缺点在于经常对训练数据过拟合。随机森林是解决这个问题的一种方法。随机森林本质上是许多决策树的集合,其中每棵树都和其他树略有不同。随机森林背后的思想是,每棵树的预测可能都相对较好,但可能对部分数据过拟合。如果构造很多树,并且每棵树的预测都很好,但都以不同的方式过拟合,那么我们可以对这些树的结果取平均值来降低过拟合。既能减少过拟合又能保持树的预测能力,这可以在数学上严格证明。
为了实现这一策略,我们需要构造许多决策树。每棵树都应该对目标值做出可以接受的预测,还应该与其他树不同。随机森林的名字来自于将随机性添加到树的构造过程中,以确保每棵树都各不相同。随机森林中树的随机化方法有两种:一种是通过选择用于构造树的数据点,另一种是通过选择每次划分测试的特征。我们来更深入地研究这一过程。
构造随机森林。
想要构造一个随机森林模型,你需要确定用于构造的树的个数(RandomForestRegressor 或 RandomForestClassifier 的 n_estimators 参数)。比如我们想要构造 10 棵树。这些树在构造时彼此完全独立,算法对每棵树进行不同的随机选择,以确保树和树之间是有区别的。想要构造一棵树,首先要对数据进行自助采样(bootstrap sample)。也就是说,从 n_samples 个数据点中有放回地(即同一样本可以被多次抽取)重
复随机抽取一个样本,共抽取 n_samples 次。这样会创建一个与原数据集大小相同的数据集,但有些数据点会缺失(大约三分之一),有些会重复。
举例说明,比如我们想要创建列表 ['a', 'b', 'c', 'd'] 的自助采样。一种可能的自主采样是 ['b', 'd', 'd', 'c'],另一种可能的采样为 ['d', 'a', 'd', 'a']。
接下来,基于这个新创建的数据集来构造决策树。但是,要对我们在介绍决策树时描述的算法稍作修改。在每个结点处,算法随机选择特征的一个子集,并对其中一个特征寻找最佳测试,而不是对每个结点都寻找最佳测试。选择的特征个数由 max_features 参数来控制。每个结点中特征子集的选择是相互独立的,这样树的每个结点可以使用特征的不同子集来做出决策。
由于使用了自助采样,随机森林中构造每棵决策树的数据集都是略有不同的。由于每个结点的特征选择,每棵树中的每次划分都是基于特征的不同子集。这两种方法共同保证随机森林中所有树都不相同。在这个过程中的一个关键参数是 max_features。如果我们设置 max_features 等于n_features,那么每次划分都要考虑数据集的所有特征,在特征选择的过程中没有添加随机性(不过自助采样依然存在随机性)。如果设置 max_features 等于 1,那么在划分时将无法选择对哪个特征进行测试,只能对随机选择的某个特征搜索不同的阈值。因此,如果 max_features 较大,那么随机森林中的树将会十分相似,利用最独特的特征可以轻松拟合数据。如果 max_features 较小,那么随机森林中的树将会差异很大,为了很好地拟合数据,每棵树的深度都要很大。
想要利用随机森林进行预测,算法首先对森林中的每棵树进行预测。对于回归问题,我们可以对这些结果取平均值作为最终预测。对于分类问题,则用到了“软投票”(soft voting)策略。也就是说,每个算法做出“软”预测,给出每个可能的输出标签的概率。对所有树的预测概率取平均值,然后将概率最大的类别作为预测结果。
分析随机森林。
下面将由 5 棵树组成的随机森林应用到前面研究过的 two_moons 数据集上:

In[68]:
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_moons
X, y = make_moons(n_samples=100, noise=0.25, random_state=3)
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y,
random_state=42)
forest = RandomForestClassifier(n_estimators=5, random_state=2)
forest.fit(X_train, y_train)

作为随机森林的一部分,树被保存在 estimator_ 属性中。我们将每棵树学到的决策边界可视化,也将它们的总预测(即整个森林做出的预测)可视化(图 2-33):

In[69]:
fig, axes = plt.subplots(2, 3, figsize=(20, 10))
for i, (ax, tree) in enumerate(zip(axes.ravel(), forest.estimators_)):
    ax.set_title("Tree {}".format(i))
    mglearn.plots.plot_tree_partition(X_train, y_train, tree, ax=ax)
    
mglearn.plots.plot_2d_separator(forest, X_train, fill=True, ax=axes[-1, -1],
                                alpha=.4)
axes[-1, -1].set_title("Random Forest")
mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train)
图 2-33: 5 棵随机化的决策树找到的决策边界,以及将它们的预测概率取平均后得到的决策边界

你可以清楚地看到,这 5 棵树学到的决策边界大不相同。每棵树都犯了一些错误,因为这里画出的一些训练点实际上并没有包含在这些树的训练集中,原因在于自助采样。
随机森林比单独每一棵树的过拟合都要小,给出的决策边界也更符合直觉。在任何实际应用中,我们会用到更多棵树(通常是几百或上千),从而得到更平滑的边界。
再举一个例子,我们将包含 100 棵树的随机森林应用在乳腺癌数据集上:

In[70]:
X_train, X_test, y_train, y_test = train_test_split(
cancer.data, cancer.target, random_state=0)
forest = RandomForestClassifier(n_estimators=100, random_state=0)
forest.fit(X_train, y_train)
print("Accuracy on training set: {:.3f}".format(forest.score(X_train, y_train)))
print("Accuracy on test set: {:.3f}".format(forest.score(X_test, y_test)))
Out[70]:
Accuracy on training set: 1.000
Accuracy on test set: 0.972

在没有调节任何参数的情况下,随机森林的精度为 97%,比线性模型或单棵决策树都要好。我们可以调节 max_features 参数,或者像单棵决策树那样进行预剪枝。但是,随机森林的默认参数通常就已经可以给出很好的结果。
与决策树类似,随机森林也可以给出特征重要性,计算方法是将森林中所有树的特征重要性求和并取平均。一般来说,随机森林给出的特征重要性要比单棵树给出的更为可靠。参见图 2-34。

plot_feature_importances_cancer(forest)

图 2-34:拟合乳腺癌数据集得到的随机森林的特征重要性

如你所见,与单棵树相比,随机森林中有更多特征的重要性不为零。与单棵决策树类似,随机森林也给了“worst radius”(最大半径)特征很大的重要性,但从总体来看,它实际上却选择“worst perimeter”(最大周长)作为信息量最大的特征。由于构造随机森林过程中的随机性,算法需要考虑多种可能的解释,结果就是随机森林比单棵树更能从总体把握数据的特征。
优点、 缺点和参数。
用于回归和分类的随机森林是目前应用最广泛的机器学习方法之一。
这种方法非常强大,通常不需要反复调节参数就可以给出很好的结果,也不需要对数据进行缩放。从本质上看,随机森林拥有决策树的所有优点,同时弥补了决策树的一些缺陷。仍然使用决策树的一个原因是需要决策过程的紧凑表示。基本上不可能对几十棵甚至上百棵树做出详细解释,随机森林中树的深度往往比决策树还要大(因为用到了特征子集)。因此,如果你需要以可视化的方式向非专家总结预测过程,那么选择单棵决策树可能更好。虽然在大型数据集上构建随机森林可能比较费时间,但在一台计算机的多个 CPU 内核上并行计算也很容易。如果你用的是多核处理器(几乎所有的现代化计算机都是),你可以用 n_jobs 参数来调节使用的内核个数。使用更多的 CPU 内核,可以让速度线性增加(使用 2 个内核,随机森林的训练速度会加倍),但设置 n_jobs 大于内核个数是没有用
的。你可以设置 n_jobs=-1 来使用计算机的所有内核。
你应该记住,随机森林本质上是随机的,设置不同的随机状态(或者不设置 random_state参数)可以彻底改变构建的模型。森林中的树越多,它对随机状态选择的鲁棒性就越好。如果你希望结果可以重现,固定 random_state 是很重要的。
对于维度非常高的稀疏数据(比如文本数据),随机森林的表现往往不是很好。对于这种数据,使用线性模型可能更合适。即使是非常大的数据集,随机森林的表现通常也很好,训练过程很容易并行在功能强大的计算机的多个 CPU 内核上。不过,随机森林需要更大的内存,训练和预测的速度也比线性模型要慢。对一个应用来说,如果时间和内存很重要的话,那么换用线性模型可能更为明智。
需要调节的重要参数有 n_estimators 和 max_features,可能还包括预剪枝选项(如 max_depth)。 n_estimators 总是越大越好。对更多的树取平均可以降低过拟合,从而得到鲁棒性更好的集成。不过收益是递减的,而且树越多需要的内存也越多,训练时间也越长。常用的经验法则就是“在你的时间 / 内存允许的情况下尽量多”。
前面说过, max_features 决定每棵树的随机性大小,较小的 max_features 可以降低过拟合。一般来说,好的经验就是使用默认值:对于分类,默认值是 max_features=sqrt(n_features);对于回归,默认值是 max_features=n_features。增大 max_features 或 max_leaf_nodes 有时也可以提高性能。它还可以大大降低用于训练和预测的时间和空间要求。

2. 梯度提升回归树(梯度提升机)

梯度提升回归树是另一种集成方法,通过合并多个决策树来构建一个更为强大的模型。虽然名字中含有“回归”,但这个模型既可以用于回归也可以用于分类。与随机森林方法不同,梯度提升采用连续的方式构造树,每棵树都试图纠正前一棵树的错误。默认情况下,梯度提升回归树中没有随机化,而是用到了强预剪枝。梯度提升树通常使用深度很小(1到 5 之间)的树,这样模型占用的内存更少,预测速度也更快。
梯度提升背后的主要思想是合并许多简单的模型(在这个语境中叫作弱学习器),比如深度较小的树。每棵树只能对部分数据做出好的预测,因此,添加的树越来越多,可以不断迭代提高性能。
梯度提升树经常是机器学习竞赛的优胜者,并且广泛应用于业界。与随机森林相比,它通常对参数设置更为敏感,但如果参数设置正确的话,模型精度更高。
除了预剪枝与集成中树的数量之外,梯度提升的另一个重要参数是 learning_rate(学习率),用于控制每棵树纠正前一棵树的错误的强度。较高的学习率意味着每棵树都可以做出较强的修正,这样模型更为复杂。通过增大 n_estimators 来向集成中添加更多树,也可以增加模型复杂度,因为模型有更多机会纠正训练集上的错误。
下面是在乳腺癌数据集上应用 GradientBoostingClassifier 的示例。默认使用 100 棵树,最大深度是 3,学习率为 0.1:

In[72]:
from sklearn.ensemble import GradientBoostingClassifier
X_train, X_test, y_train, y_test = train_test_split(
cancer.data, cancer.target, random_state=0)
gbrt = GradientBoostingClassifier(random_state=0)
gbrt.fit(X_train, y_train)
print("Accuracy on training set: {:.3f}".format(gbrt.score(X_train, y_train)))
print("Accuracy on test set: {:.3f}".format(gbrt.score(X_test, y_test)))
Out[72]:
Accuracy on training set: 1.000
Accuracy on test set: 0.958

由于训练集精度达到 100%,所以很可能存在过拟合。为了降低过拟合,我们可以限制最大深度来加强预剪枝,也可以降低学习率:

In[73]:
gbrt = GradientBoostingClassifier(random_state=0, max_depth=1)
gbrt.fit(X_train, y_train)
print("Accuracy on training set: {:.3f}".format(gbrt.score(X_train, y_train)))
print("Accuracy on test set: {:.3f}".format(gbrt.score(X_test, y_test)))
Out[73]:
Accuracy on training set: 0.991
Accuracy on test set: 0.972
In[74]:
gbrt = GradientBoostingClassifier(random_state=0, learning_rate=0.01)
gbrt.fit(X_train, y_train)
print("Accuracy on training set: {:.3f}".format(gbrt.score(X_train, y_train)))
print("Accuracy on test set: {:.3f}".format(gbrt.score(X_test, y_test)))
Out[74]:
Accuracy on training set: 0.988
Accuracy on test set: 0.965

降低模型复杂度的两种方法都降低了训练集精度,这和预期相同。在这个例子中,减小树的最大深度显著提升了模型性能,而降低学习率仅稍稍提高了泛化性能。对于其他基于决策树的模型,我们也可以将特征重要性可视化,以便更好地理解模型(图 2-35)。由于我们用到了 100 棵树,所以即使所有树的深度都是 1,查看所有树也是不现实的:

In[75]:
gbrt = GradientBoostingClassifier(random_state=0, max_depth=1)
gbrt.fit(X_train, y_train)
plot_feature_importances_cancer(gbrt)

图 2-35:用于拟合乳腺癌数据集的梯度提升分类器给出的特征重要性

可以看到,梯度提升树的特征重要性与随机森林的特征重要性有些类似,不过梯度提升完全忽略了某些特征。由于梯度提升和随机森林两种方法在类似的数据上表现得都很好,因此一种常用的方法就是先尝试随机森林,它的鲁棒性很好。如果随机森林效果很好,但预测时间太长,或者机器学习模型精度小数点后第二位的提高也很重要,那么切换成梯度提升通常会有用。
如果你想要将梯度提升应用在大规模问题上,可以研究一下 xgboost 包及其 Python 接口,在写作本书时,这个库在许多数据集上的速度都比 scikit-learn 对梯度提升的实现要快(有时调参也更简单)。
优点、 缺点和参数。
梯度提升决策树是监督学习中最强大也最常用的模型之一。其主要缺点是需要仔细调参,而且训练时间可能会比较长。与其他基于树的模型类似,这一算法不需要对数据进行缩放就可以表现得很好,而且也适用于二元特征与连续特征同时存在的数据集。与其他基于树的模型相同,它也通常不适用于高维稀疏数据。
梯度提升树模型的主要参数包括树的数量 n_estimators 和学习率 learning_rate,后者用于控制每棵树对前一棵树的错误的纠正强度。这两个参数高度相关,因为 learning_rate 越低,就需要更多的树来构建具有相似复杂度的模型。随机森林的 n_estimators 值总是越大越好,但梯度提升不同,增大 n_estimators 会导致模型更加复杂,进而可能导致过拟合。通常的做法是根据时间和内存的预算选择合适的 n_estimators,然后对不同learning_rate 进行遍历。
另一个重要参数是 max_depth(或 max_leaf_nodes),用于降低每棵树的复杂度。梯度提升模型的 max_depth 通常都设置得很小,一般不超过 5。

2.3.7 核支持向量机

我们要讨论的下一种监督学习模型是核支持向量机(kernelized support vector machine)。在 2.3.3 节中,我们研究了将线性支持向量机用于分类任务。核支持向量机(通常简称为SVM)是可以推广到更复杂模型的扩展,这些模型无法被输入空间的超平面定义。虽然支持向量机可以同时用于分类和回归,但我们只会介绍用于分类的情况,它在 SVC 中实现。
类似的概念也适用于支持向量回归,后者在 SVR 中实现。
核支持向量机背后的数学有点复杂,已经超出了本书的范围。你可以阅读 Hastie、Tibshirani 和 Friedman 合著的《统计学习基础》一书(http://statweb.stanford.edu/~tibs/ElemStatLearn/)的第 12 章了解更多细节。不过,我们会努力向读者传达这一方法背后的理念。

1. 线性模型与非线性特征

如图 2-15 所示,线性模型在低维空间中可能非常受限,因为线和平面的灵活性有限。有一种方法可以让线性模型更加灵活,就是添加更多的特征——举个例子,添加输入特征的交互项或多项式。
我们来看一下 2.3.5 节中用到的模拟数据集(见图 2-29):

In[76]:
X, y = make_blobs(centers=4, random_state=8)
y = y % 2
mglearn.discrete_scatter(X[:, 0], X[:, 1], y)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
图 2-36:二分类数据集,其类别并不是线性可分的

用于分类的线性模型只能用一条直线来划分数据点,对这个数据集无法给出较好的结果(见图 2-37):

In[77]:
from sklearn.svm import LinearSVC
linear_svm = LinearSVC().fit(X, y)
mglearn.plots.plot_2d_separator(linear_svm, X)
mglearn.discrete_scatter(X[:, 0], X[:, 1], y)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
图 2-37:线性 SVM 给出的决策边界

现在我们对输入特征进行扩展,比如说添加第二个特征的平方(feature1 ** 2)作为一个新特征。现在我们将每个数据点表示为三维点 (feature0, feature1, feature1 ** 2),而不是二维点 (feature0, feature1)11。这个新的表示可以画成图 2-38 中的三维散点图:

In[78]:
# 添加第二个特征的平方,作为一个新特征
X_new = np.hstack([X, X[:, 1:] ** 2])
from mpl_toolkits.mplot3d import Axes3D, axes3d
figure = plt.figure()
# 3D可视化
ax = Axes3D(figure, elev=-152, azim=-26)
# 首先画出所有y == 0的点,然后画出所有y == 1的点
mask = y == 0
ax.scatter(X_new[mask, 0], X_new[mask, 1], X_new[mask, 2], c='b',
cmap=mglearn.cm2, s=60)
ax.scatter(X_new[~mask, 0], X_new[~mask, 1], X_new[~mask, 2], c='r', marker='^',
cmap=mglearn.cm2, s=60)
ax.set_xlabel("feature0")
ax.set_ylabel("feature1")
ax.set_zlabel("feature1 ** 2")
图 2-38:对图 2-37 中的数据集进行扩展,新增由 feature1 导出的的第三个特征

在数据的新表示中,现在可以用线性模型(三维空间中的平面)将这两个类别分开。我们可以用线性模型拟合扩展后的数据来验证这一点(见图 2-39):


In[79]:
linear_svm_3d = LinearSVC().fit(X_new, y)
coef, intercept = linear_svm_3d.coef_.ravel(), linear_svm_3d.intercept_
# 显示线性决策边界
figure = plt.figure()
ax = Axes3D(figure, elev=-152, azim=-26)
xx = np.linspace(X_new[:, 0].min() - 2, X_new[:, 0].max() + 2, 50)
yy = np.linspace(X_new[:, 1].min() - 2, X_new[:, 1].max() + 2, 50)
XX, YY = np.meshgrid(xx, yy)
ZZ = (coef[0] * XX + coef[1] * YY + intercept) / -coef[2]
ax.plot_surface(XX, YY, ZZ, rstride=8, cstride=8, alpha=0.3)
ax.scatter(X_new[mask, 0], X_new[mask, 1], X_new[mask, 2], c='b',
cmap=mglearn.cm2, s=60)
ax.scatter(X_new[~mask, 0], X_new[~mask, 1], X_new[~mask, 2], c='r', marker='^',
cmap=mglearn.cm2, s=60)
ax.set_xlabel("feature0")
ax.set_ylabel("feature1")
ax.set_zlabel("feature1 ** 2")
图 2-39:线性 SVM 对扩展后的三维数据集给出的决策边界

如果将线性 SVM 模型看作原始特征的函数,那么它实际上已经不是线性的了。它不是一条直线,而是一个椭圆,你可以在下图中看出(图 2-40):

In[80]:
ZZ = YY ** 2
dec = linear_svm_3d.decision_function(np.c_[XX.ravel(), YY.ravel(), ZZ.ravel()])
plt.contourf(XX, YY, dec.reshape(XX.shape), levels=[dec.min(), 0, dec.max()],
cmap=mglearn.cm2, alpha=0.5)
mglearn.discrete_scatter(X[:, 0], X[:, 1], y)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
图 2-40:将图 2-39 给出的决策边界作为两个原始特征的函数

2. 核技巧

这里需要记住的是,向数据表示中添加非线性特征,可以让线性模型变得更强大。但是,通常来说我们并不知道要添加哪些特征,而且添加许多特征(比如 100 维特征空间所有可能的交互项)的计算开销可能会很大。幸运的是,有一种巧妙的数学技巧,让我们可以在更高维空间中学习分类器,而不用实际计算可能非常大的新的数据表示。这种技巧叫作核技巧(kernel trick),它的原理是直接计算扩展特征表示中数据点之间的距离(更准确地说是内积),而不用实际对扩展进行计算。
对于支持向量机,将数据映射到更高维空间中有两种常用的方法:一种是多项式核,在一定阶数内计算原始特征所有可能的多项式(比如 feature1 ** 2 * feature2 ** 5);另一种是径向基函数(radial basis function, RBF)核,也叫高斯核。高斯核有点难以解释,因为它对应无限维的特征空间。一种对高斯核的解释是它考虑所有阶数的所有可能的多项式,但阶数越高,特征的重要性越小。不过在实践中,核 SVM 背后的数学细节并不是很重要,可以简单地总结出使用 RBF 核SVM 进行预测的方法——我们将在下一节介绍这方面的内容。

3. 理解SVM

在训练过程中, SVM 学习每个训练数据点对于表示两个类别之间的决策边界的重要性。通常只有一部分训练数据点对于定义决策边界来说很重要:位于类别之间边界上的那些点。这些点叫作支持向量(support vector),支持向量机正是由此得名。想要对新样本点进行预测,需要测量它与每个支持向量之间的距离。分类决策是基于它与支持向量之间的距离以及在训练过程中学到的支持向量重要性(保存在 SVC 的 dual_coef_属性中)来做出的。

数据点之间的距离由高斯核给出:
krbf (x1, x2) = exp (-γ‖ x1 - x2‖ 2)
这里 x1 和 x2 是数据点, ‖ x1 - x2‖ 表示欧氏距离, γ(gamma)是控制高斯核宽度的参数。

图 2-41 是支持向量机对一个二维二分类数据集的训练结果。决策边界用黑色表示,支持向量是尺寸较大的点。下列代码将在 forge 数据集上训练 SVM 并创建此图:

In[81]:
from sklearn.svm import SVC
X, y = mglearn.tools.make_handcrafted_dataset()
svm = SVC(kernel='rbf', C=10, gamma=0.1).fit(X, y)
mglearn.plots.plot_2d_separator(svm, X, eps=.5)
mglearn.discrete_scatter(X[:, 0], X[:, 1], y)
# 画出支持向量
sv = svm.support_vectors_
# 支持向量的类别标签由dual_coef_的正负号给出
sv_labels = svm.dual_coef_.ravel() > 0
mglearn.discrete_scatter(sv[:, 0], sv[:, 1], sv_labels, s=15, markeredgewidth=3)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
图 2-41: RBF 核 SVM 给出的决策边界和支持向量

在这个例子中, SVM 给出了非常平滑且非线性(不是直线)的边界。这里我们调节了两个参数: C 参数和 gamma 参数,下面我们将详细讨论。

4. SVM调参

gamma 参数是上一节给出的公式中的参数,用于控制高斯核的宽度。它决定了点与点之间“靠近”是指多大的距离。 C 参数是正则化参数,与线性模型中用到的类似。它限制每个点的重要性(或者更确切地说,每个点的 dual_coef_)。
我们来看一下,改变这些参数时会发生什么(图 2-42):

In[82]:
fig, axes = plt.subplots(3, 3, figsize=(15, 10))

for ax, C in zip(axes, [-1, 0, 3]):
    for a, gamma in zip(ax, range(-1, 2)):
        mglearn.plots.plot_svm(log_C=C, log_gamma=gamma, ax=a)
        
axes[0, 0].legend(["class 0", "class 1", "sv class 0", "sv class 1"],
                  ncol=4, loc=(.9, 1.2))
图 2-42:设置不同的 C 和 gamma 参数对应的决策边界和支持向量

从左到右,我们将参数 gamma 的值从 0.1 增加到 10。 gamma 较小,说明高斯核的半径较大,许多点都被看作比较靠近。这一点可以在图中看出:左侧的图决策边界非常平滑,越向右的图决策边界更关注单个点。小的 gamma 值表示决策边界变化很慢,生成的是复杂度较低的模型,而大的 gamma 值则会生成更为复杂的模型。
从上到下,我们将参数 C 的值从 0.1 增加到 1000。与线性模型相同, C 值很小,说明模型非常受限,每个数据点的影响范围都有限。你可以看到,左上角的图中,决策边界看起来几乎是线性的,误分类的点对边界几乎没有任何影响。再看左下角的图,增大 C 之后这些点对模型的影响变大,使得决策边界发生弯曲来将这些点正确分类。我们将 RBF 核 SVM 应用到乳腺癌数据集上。默认情况下, C=1, gamma=1/n_features:

In[83]:
X_train, X_test, y_train, y_test = train_test_split(
cancer.data, cancer.target, random_state=0)
svc = SVC()
svc.fit(X_train, y_train)
print("Accuracy on training set: {:.2f}".format(svc.score(X_train, y_train)))
print("Accuracy on test set: {:.2f}".format(svc.score(X_test, y_test)))
Out[83]:
Accuracy on training set: 1.00
Accuracy on test set: 0.63

这个模型在训练集上的分数十分完美,但在测试集上的精度只有 63%,存在相当严重的过拟合。虽然 SVM 的表现通常都很好,但它对参数的设定和数据的缩放非常敏感。特别地,它要求所有特征有相似的变化范围。我们来研究处理这个问题的几种方法。

5. 为SVM预处理数据

解决这个问题的一种方法就是对每个特征进行缩放,使其大致都位于同一范围。核 SVM常用的缩放方法就是将所有特征缩放到 0 和 1 之间。我们将在第 3 章学习如何使用MinMaxScaler 预处理方法来做到这一点,到时会给出更多细节。现在我们来“人工”做到这一点:

In[85]:
# 计算训练集中每个特征的最小值
min_on_training = X_train.min(axis=0)
# 计算训练集中每个特征的范围(最大值-最小值)
range_on_training = (X_train - min_on_training).max(axis=0)
# 减去最小值,然后除以范围
# 这样每个特征都是min=0和max=1
X_train_scaled = (X_train - min_on_training) / range_on_training
print("Minimum for each feature\n{}".format(X_train_scaled.min(axis=0)))
print("Maximum for each feature\n {}".format(X_train_scaled.max(axis=0)))
Out[85]:
Minimum for each feature
[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
Maximum for each feature
[ 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.
1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
In[86]:
# 利用训练集的最小值和范围对测试集做相同的变换(详见第3章)
X_test_scaled = (X_test - min_on_training) / range_on_training
In[87]:
svc = SVC()
svc.fit(X_train_scaled, y_train)
print("Accuracy on training set: {:.3f}".format(
svc.score(X_train_scaled, y_train)))
print("Accuracy on test set: {:.3f}".format(svc.score(X_test_scaled, y_test)))
Out[87]:
Accuracy on training set: 0.948
Accuracy on test set: 0.951

数据缩放的作用很大!实际上模型现在处于欠拟合的状态,因为训练集和测试集的性能非常接近,但还没有接近 100% 的精度。从这里开始,我们可以尝试增大 C 或 gamma 来拟合更为复杂的模型。例如:

In[88]:
svc = SVC(C=1000)
svc.fit(X_train_scaled, y_train)
print("Accuracy on training set: {:.3f}".format(
svc.score(X_train_scaled, y_train)))
print("Accuracy on test set: {:.3f}".format(svc.score(X_test_scaled, y_test)))
Out[88]:
Accuracy on training set: 0.988
Accuracy on test set: 0.972

在这个例子中,增大 C 可以显著改进模型,得到 97.2% 的精度。

6. 优点、 缺点和参数

核支持向量机是非常强大的模型,在各种数据集上的表现都很好。 SVM 允许决策边界很复杂,即使数据只有几个特征。它在低维数据和高维数据(即很少特征和很多特征)上的表现都很好,但对样本个数的缩放表现不好。在有多达 10 000 个样本的数据上运行 SVM可能表现良好,但如果数据量达到 100 000 甚至更大,在运行时间和内存使用方面可能会面临挑战。
SVM 的另一个缺点是,预处理数据和调参都需要非常小心。这也是为什么如今很多应用中用的都是基于树的模型,比如随机森林或梯度提升(需要很少的预处理,甚至不需要预处理)。此外, SVM 模型很难检查,可能很难理解为什么会这么预测,而且也难以将模型向非专家进行解释。
不过 SVM 仍然是值得尝试的,特别是所有特征的测量单位相似(比如都是像素密度)而且范围也差不多时。核 SVM 的重要参数是正则化参数 C、核的选择以及与核相关的参数。虽然我们主要讲的是RBF 核,但 scikit-learn 中还有其他选择。 RBF 核只有一个参数 gamma,它是高斯核宽度的倒数。 gamma 和 C 控制的都是模型复杂度,较大的值都对应更为复杂的模型。因此,这两个参数的设定通常是强烈相关的,应该同时调节。

2.3.8 神经网络( 深度学习)

一类被称为神经网络的算法最近以“深度学习”的名字再度流行。虽然深度学习在许多机器学习应用中都有巨大的潜力,但深度学习算法往往经过精确调整,只适用于特定的使用场景。这里只讨论一些相对简单的方法,即用于分类和回归的多层感知机(multilayer perceptron, MLP), 它可以作为研究更复杂的深度学习方法的起点。 MLP 也被称为(普通)前馈神经网络,有时也简称为神经网络。

1. 神经网络模型

MLP 可以被视为广义的线性模型,执行多层处理后得到结论。
还记得线性回归的预测公式为:
ŷ = w[0] * x[0] + w[1] * x[1] + … + w[p] * x[p] + b
简单来说, ŷ 是输入特征 x[0] 到 x[p] 的加权求和,权重为学到的系数 w[0] 到 w[p]。
这个模型需要学习更多的系数(也叫作权重):在每个输入与每个隐单元(隐单元组成了隐层)之间有一个系数,在每个隐单元与输出之间也有一个系数。
从数学的角度看,计算一系列加权求和与只计算一个加权求和是完全相同的,因此,为了让这个模型真正比线性模型更为强大,我们还需要一个技巧。在计算完每个隐单元的加权求和之后,对结果再应用一个非线性函数——通常是校正非线性(rectifying nonlinearity,也叫校正线性单元或 relu)或正切双曲线(tangens hyperbolicus, tanh)。然后将这个函数的结果用于加权求和, 计算得到输出 ŷ。这两个函数的可视化效果见图 2-46。 relu 截断小于0 的值,而 tanh 在输入值较小时接近 -1,在输入值较大时接近 +1。有了这两种非线性函数,神经网络可以学习比线性模型复杂得多的函数。

In[91]:
line = np.linspace(-3, 3, 100)
plt.plot(line, np.tanh(line), label="tanh")
plt.plot(line, np.maximum(line, 0), label="relu")
plt.legend(loc="best")
plt.xlabel("x")
plt.ylabel("relu(x), tanh(x)")
图 2-46:双曲正切激活函数与校正线性激活函数

对于图 2-45 所示的小型神经网络,计算回归问题的 ŷ 的完整公式如下(使用 tanh 非线性):

h[0] = tanh(w[0, 0] * x[0] + w[1, 0] * x[1] + w[2, 0] * x[2] + w[3, 0] * x[3] + b[0])
h[1] = tanh(w[0, 0] * x[0] + w[1, 0] * x[1] + w[2, 0] * x[2] + w[3, 0] * x[3] + b[1])
h[2] = tanh(w[0, 0] * x[0] + w[1, 0] * x[1] + w[2, 0] * x[2] + w[3, 0] * x[3] + b[2])
ŷ = v[0] * h[0] + v[1] * h[1] + v[2] * h[2] + b

其中, w 是输入 x 与隐层 h 之间的权重, v 是隐层 h 与输出 ŷ 之间的权重。权重 w 和 v 要从数据中学习得到, x 是输入特征, ŷ 是计算得到的输出, h 是计算的中间结果。需要用户设置的一个重要参数是隐层中的结点个数。对于非常小或非常简单的数据集,这个值可以小到 10;对于非常复杂的数据,这个值可以大到 10 000。

2. 神经网络调参

我们将 MLPClassifier 应用到本章前面用过的 two_moons 数据集上,以此研究 MLP 的工作原理。结果如图 2-48 所示。

In[93]:
from sklearn.neural_network import MLPClassifier
from sklearn.datasets import make_moons
X, y = make_moons(n_samples=100, noise=0.25, random_state=3)
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y,
random_state=42)
mlp = MLPClassifier(solver='lbfgs', random_state=0).fit(X_train, y_train)
mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3)
mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
图 2-48:包含 100 个隐单元的神经网络在 two_moons 数据集上学到的决策边界

如你所见,神经网络学到的决策边界完全是非线性的,但相对平滑。我们用到了solver='lbfgs',这一点稍后会讲到。
默认情况下, MLP 使用 100 个隐结点,这对于这个小型数据集来说已经相当多了。我们可以减少其数量(从而降低了模型复杂度),但仍然得到很好的结果(图 2-49):

In[94]:
mlp = MLPClassifier(solver='lbfgs', random_state=0, hidden_layer_sizes=[10])
mlp.fit(X_train, y_train)
mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3)
mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
图 2-49:包含 10 个隐单元的神经网络在 two_moons 数据集上学到的决策边界

只有 10 个隐单元时,决策边界看起来更加参差不齐。默认的非线性是 relu,如图 2-46 所示。如果使用单隐层,那么决策函数将由 10 个直线段组成。如果想得到更加平滑的决策边界,可以添加更多的隐单元(见图 2-48)、添加第二个隐层(见图 2-50)或者使用 tanh非线性(见图 2-51)。

In[95]:
# 使用2个隐层,每个包含10个单元
mlp = MLPClassifier(solver='lbfgs', random_state=0,
hidden_layer_sizes=[10, 10])
mlp.fit(X_train, y_train)
mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3)
mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
In[96]:
# 使用2个隐层,每个包含10个单元,这次使用tanh非线性
mlp = MLPClassifier(solver='lbfgs', activation='tanh',
random_state=0, hidden_layer_sizes=[10, 10])
mlp.fit(X_train, y_train)
mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3)
mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
图 2-50:包含 2 个隐层、每个隐层包含 10 个隐单元的神经网络学到的决策边界(激活函数为 relu)

图 2-51:包含 2 个隐层、每个隐层包含 10 个隐单元的神经网络学到的决策边界(激活函数为 tanh)

最后,我们还可以利用 L2 惩罚使权重趋向于 0,从而控制神经网络的复杂度,正如我们在岭回归和线性分类器中所做的那样。 MLPClassifier 中调节 L2 惩罚的参数是 alpha(与线性回归模型中的相同),它的默认值很小(弱正则化)。图 2-52 显示了不同 alpha 值对 two_moons 数据集的影响,用的是 2 个隐层的神经网络,每层包含 10 个或 100 个单元:

In[97]:
fig, axes = plt.subplots(2, 4, figsize=(20, 8))
for axx, n_hidden_nodes in zip(axes, [10, 100]):
    for ax, alpha in zip(axx, [0.0001, 0.01, 0.1, 1]):
        mlp = MLPClassifier(solver='lbfgs', random_state=0,
                            hidden_layer_sizes=[n_hidden_nodes, n_hidden_nodes],
                            alpha=alpha)
        mlp.fit(X_train, y_train)
        mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3, ax=ax)
        mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train, ax=ax)
        ax.set_title("n_hidden=[{}, {}]\nalpha={:.4f}".format(
                      n_hidden_nodes, n_hidden_nodes, alpha))
图 2-52:不同隐单元个数与 alpha 参数的不同设定下的决策函数

现在你可能已经认识到了,控制神经网络复杂度的方法有很多种:隐层的个数、每个隐层中的单元个数与正则化(alpha)。实际上还有更多,但这里不再过多介绍。神经网络的一个重要性质是,在开始学习之前其权重是随机设置的,这种随机初始化会影响学到的模型。也就是说,即使使用完全相同的参数,如果随机种子不同的话,我们也可能得到非常不一样的模型。如果网络很大,并且复杂度选择合理的话,那么这应该不会对精度有太大影响,但应该记住这一点(特别是对于较小的网络)。图 2-53 显示了几个模型的图像,所有模型都使用相同的参数设置进行学习:

In[98]:
fig, axes = plt.subplots(2, 4, figsize=(20, 8))
for i, ax in enumerate(axes.ravel()):
    mlp = MLPClassifier(solver='lbfgs', random_state=i,
                        hidden_layer_sizes=[100, 100])
    mlp.fit(X_train, y_train)
    mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3, ax=ax)
    mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train, ax=ax)
图 2-53:相同参数但不同随机初始化的情况下学到的决策函数

为了在现实世界的数据上进一步理解神经网络,我们将 MLPClassifier 应用在乳腺癌数据集上。首先使用默认参数:

In[99]:
print("Cancer data per-feature maxima:\n{}".format(cancer.data.max(axis=0)))
Out[99]:
Cancer data per-feature maxima:
[ 28.110 39.280 188.500 2501.000 0.163 0.345 0.427
0.201 0.304 0.097 2.873 4.885 21.980 542.200
0.031 0.135 0.396 0.053 0.079 0.030 36.040
49.540 251.200 4254.000 0.223 1.058 1.252 0.291
0.664 0.207]
In[100]:
X_train, X_test, y_train, y_test = train_test_split(
cancer.data, cancer.target, random_state=0)
mlp = MLPClassifier(random_state=42)
mlp.fit(X_train, y_train)
print("Accuracy on training set: {:.2f}".format(mlp.score(X_train, y_train)))
print("Accuracy on test set: {:.2f}".format(mlp.score(X_test, y_test)))
Out[100]:
Accuracy on training set: 0.92
Accuracy on test set: 0.90

MLP 的精度相当好,但没有其他模型好。与较早的 SVC 例子相同,原因可能在于数据的缩放。神经网络也要求所有输入特征的变化范围相似,最理想的情况是均值为 0、方差为1。我们必须对数据进行缩放以满足这些要求。同样,我们这里将人工完成,但在第 3 章将会介绍用 StandardScaler 自动完成:

In[101]:
# 计算训练集中每个特征的平均值
mean_on_train = X_train.mean(axis=0)
# 计算训练集中每个特征的标准差
std_on_train = X_train.std(axis=0)
# 减去平均值,然后乘以标准差的倒数
# 如此运算之后, mean=0, std=1
X_train_scaled = (X_train - mean_on_train) / std_on_train
# 对测试集做相同的变换(使用训练集的平均值和标准差)
X_test_scaled = (X_test - mean_on_train) / std_on_train
mlp = MLPClassifier(random_state=0)
mlp.fit(X_train_scaled, y_train)
print("Accuracy on training set: {:.3f}".format(
mlp.score(X_train_scaled, y_train)))
print("Accuracy on test set: {:.3f}".format(mlp.score(X_test_scaled, y_test)))
Out[101]:
Accuracy on training set: 0.991
Accuracy on test set: 0.965
ConvergenceWarning:
Stochastic Optimizer: Maximum iterations reached and the optimization
hasn't converged yet.

缩放之后的结果要好得多,而且也相当有竞争力。不过模型给出了一个警告,告诉我们已经达到最大迭代次数。这是用于学习模型的 adam 算法的一部分,告诉我们应该增加迭代次数:

In[102]:
mlp = MLPClassifier(max_iter=1000, random_state=0)
mlp.fit(X_train_scaled, y_train)
print("Accuracy on training set: {:.3f}".format(
mlp.score(X_train_scaled, y_train)))
print("Accuracy on test set: {:.3f}".format(mlp.score(X_test_scaled, y_test)))
Out[102]:
Accuracy on training set: 0.995
Accuracy on test set: 0.965

增加迭代次数仅提高了训练集性能,但没有提高泛化性能。不过模型的表现相当不错。由于训练性能和测试性能之间仍有一些差距,所以我们可以尝试降低模型复杂度来得到更好的泛化性能。这里我们选择增大 alpha 参数(变化范围相当大,从 0.0001 到 1),以此向权重添加更强的正则化:

In[103]:
mlp = MLPClassifier(max_iter=1000, alpha=1, random_state=0)
mlp.fit(X_train_scaled, y_train)
print("Accuracy on training set: {:.3f}".format(
mlp.score(X_train_scaled, y_train)))
print("Accuracy on test set: {:.3f}".format(mlp.score(X_test_scaled, y_test)))
Out[103]:
Accuracy on training set: 0.988
Accuracy on test set: 0.972

这得到了与我们目前最好的模型相同的性能。
虽然可以分析神经网络学到了什么,但这通常比分析线性模型或基于树的模型更为复杂。
要想观察模型学到了什么,一种方法是查看模型的权重。你可以在 scikit-learn 示例库中查看这样的一个示例(http://scikit-learn.org/stable/auto_examples/neural_networks/plot_mnist_filters.html)。对于乳腺癌数据集,这可能有点难以理解。下面这张图(图 2-54)显示了连接输入和第一个隐层之间的权重。图中的行对应 30 个输入特征,列对应 100 个隐单元。浅色代表较大的正值,而深色代表负值。

In[104]:
plt.figure(figsize=(20, 5))
plt.imshow(mlp.coefs_[0], interpolation='none', cmap='viridis')
plt.yticks(range(30), cancer.feature_names)
plt.xlabel("Columns in weight matrix")
plt.ylabel("Input feature")
plt.colorbar()
图 2-54:神经网络在乳腺癌数据集上学到的第一个隐层权重的热图

我们可以推断,如果某个特征对所有隐单元的权重都很小,那么这个特征对模型来说就“不太重要”。可以看到,与其他特征相比,“mean smoothness”“mean compactness”以及“smoothness error” 和“fractal dimension error”之间的特征的权重都相对较小。这可能说明这些特征不太重要,也可能是我们没有用神经网络可以使用的方式来表示这些特征。我们还可以将连接隐层和输出层的权重可视化,但它们更加难以解释。
虽然 MLPClassifier 和 MLPRegressor 为最常见的神经网络架构提供了易于使用的接口,但它们只包含神经网络潜在应用的一部分。如果你有兴趣使用更灵活或更大的模型,我们建议你看一下除了 scikit-learn 之外的很棒的深度学习库。对于 Python 用户来说,最为完善的是 keras、 lasagna 和 tensor-flow。 lasagna 是基于 theano 库构建的,而 keras 既可以用 tensor-flow 也可以用 theano。这些库提供了更为灵活的接口,可以用来构建神经网络并跟踪深度学习研究的快速发展。所有流行的深度学习库也都允许使用高性能的图形处理单元(GPU),而 scikit-learn 不支持 GPU。使用 GPU 可以将计算速度加快 10 到 100倍, GPU 对于将深度学习方法应用到大型数据集上至关重要。

3. 优点、 缺点和参数

在机器学习的许多应用中,神经网络再次成为最先进的模型。它的主要优点之一是能够获取大量数据中包含的信息,并构建无比复杂的模型。给定足够的计算时间和数据,并且仔细调节参数,神经网络通常可以打败其他机器学习算法(无论是分类任务还是回归任务)。这就引出了下面要说的缺点。神经网络——特别是功能强大的大型神经网络——通常需要很长的训练时间。它还需要仔细地预处理数据,正如我们这里所看到的。与 SVM 类似,神经网络在“均匀”数据上的性能最好,其中“均匀”是指所有特征都具有相似的含义。如果数据包含不同种类的特征,那么基于树的模型可能表现得更好。神经网络调参本身也是一门艺术。调节神经网络模型和训练模型的方法有很多种,我们只是蜻蜓点水地尝试了几种而已。
估计神经网络的复杂度。 最重要的参数是层数和每层的隐单元个数。你应该首先设置 1 个或 2 个隐层,然后可以逐步增加。每个隐层的结点个数通常与输入特征个数接近,但在几千个结点时很少会多于特征个数。
在考虑神经网络的模型复杂度时,一个有用的度量是学到的权重(或系数)的个数。如果你有一个包含 100 个特征的二分类数据集,模型有 100 个隐单元,那么输入层和第一个隐层之间就有 100 * 100 = 10 000 个权重。在隐层和输出层之间还有 100 * 1 = 100 个权重,总共约 10 100 个权重。如果添加含有 100 个隐单元的第二个隐层,那么在第一个隐层和第二个隐层之间又有 100 * 100 = 10 000 个权重,总数变为约 20 100 个权重。如果你使用包含1000 个隐单元的单隐层,那么在输入层和隐层之间需要学习 100 * 1000 = 100 000 个权重,隐层到输出层之间需要学习 1000 * 1 = 1000 个权重,总共 101 000 个权重。如果再添加第二个隐层,就会增加 1000 * 1000 = 1 000 000 个权重,总数变为巨大的 1 101 000 个权重,这比含有 2 个隐层、每层 100 个单元的模型要大 50 倍。
神经网络调参的常用方法是,首先创建一个大到足以过拟合的网络,确保这个网络可以对任务进行学习。知道训练数据可以被学习之后,要么缩小网络,要么增大 alpha 来增强正则化,这可以提高泛化性能。
在我们的实验中,主要关注模型的定义:层数、每层的结点个数、正则化和非线性。这些内容定义了我们想要学习的模型。还有一个问题是, 如何学习模型或用来学习参数的算法,这一点由 solver 参数设定。 solver 有两个好用的选项。默认选项是 'adam',在大多数情况下效果都很好,但对数据的缩放相当敏感(因此,始终将数据缩放为均值为 0、方差为 1 是很重要的)。另一个选项是 'lbfgs',其鲁棒性相当好,但在大型模型或大型数据集上的时间会比较长。还有更高级的 'sgd' 选项,许多深度学习研究人员都会用到。 'sgd'选项还有许多其他参数需要调节,以便获得最佳结果。你可以在用户指南中找到所有这些参数及其定义。当你开始使用 MLP 时,我们建议使用 'adam' 和 'lbfgs'。

2.4 分类器的不确定度估计

我们还没有谈到 scikit-learn 接口的另一个有用之处,就是分类器能够给出预测的不确定度估计。一般来说,你感兴趣的不仅是分类器会预测一个测试点属于哪个类别,还包括它对这个预测的置信程度。在实践中,不同类型的错误会在现实应用中导致非常不同的结果。想象一个用于测试癌症的医疗应用。假阳性预测可能只会让患者接受额外的测试,但假阴性预测却可能导致重病没有得到治疗。第 6 章会进一步探讨这一主题。scikit-learn 中有两个函数可用于获取分类器的不确定度估计: decision_function 和predict_proba。大多数分类器(但不是全部)都至少有其中一个函数,很多分类器两个都有。我们来构建一个 GradientBoostingClassifier 分类器(同时拥有 decision_function和 predict_proba 两个方法),看一下这两个函数对一个模拟的二维数据集的作用:

In[105]:
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.datasets import make_circles
X, y = make_circles(noise=0.25, factor=0.5, random_state=1)
# 为了便于说明,我们将两个类别重命名为"blue"和"red"
y_named = np.array(["blue", "red"])[y]
# 我们可以对任意个数组调用train_test_split
# 所有数组的划分方式都是一致的
X_train, X_test, y_train_named, y_test_named, y_train, y_test = \
train_test_split(X, y_named, y, random_state=0)
# 构建梯度提升模型
gbrt = GradientBoostingClassifier(random_state=0)
gbrt.fit(X_train, y_train_named)

2.4.1 决策函数

对于二分类的情况, decision_function 返回值的形状是 (n_samples,),为每个样本都返回一个浮点数:

In[106]:
print("X_test.shape: {}".format(X_test.shape))
print("Decision function shape: {}".format(
gbrt.decision_function(X_test).shape))
Out[106]:
X_test.shape: (25, 2)
Decision function shape: (25,)

对于类别 1 来说,这个值表示模型对该数据点属于“正”类的置信程度。正值表示对正类的偏好,负值表示对“反类”(其他类)的偏好:

In[107]:
# 显示decision_function的前几个元素
print("Decision function:\n{}".format(gbrt.decision_function(X_test)[:6]))
Out[107]:
Decision function:
[ 4.136 -1.683 -3.951 -3.626 4.29 3.662]

我们可以通过仅查看决策函数的正负号来再现预测值:

In[108]:
print("Thresholded decision function:\n{}".format(
gbrt.decision_function(X_test) > 0))
print("Predictions:\n{}".format(gbrt.predict(X_test)))
Out[108]:
Thresholded decision function:
[ True False False False True True False True True True False True
True False True False False False True True True True True False
False]
Predictions:
['red' 'blue' 'blue' 'blue' 'red' 'red' 'blue' 'red' 'red' 'red' 'blue'
'red' 'red' 'blue' 'red' 'blue' 'blue' 'blue' 'red' 'red' 'red' 'red'
'red' 'blue' 'blue']

对于二分类问题,“反”类始终是 classes_ 属性的第一个元素,“正”类是 classes_ 的第二个元素。因此,如果你想要完全再现 predict 的输出,需要利用 classes_ 属性:

In[109]:
# 将布尔值True/False转换成0和1
greater_zero = (gbrt.decision_function(X_test) > 0).astype(int)
# 利用0和1作为classes_的索引
pred = gbrt.classes_[greater_zero]
# pred与gbrt.predict的输出完全相同
print("pred is equal to predictions: {}".format(
np.all(pred == gbrt.predict(X_test))))
Out[109]:
pred is equal to predictions: True
decision_function 可以在任意范围取值,这取决于数据与模型参数:
In[110]:
decision_function = gbrt.decision_function(X_test)
print("Decision function minimum: {:.2f} maximum: {:.2f}".format(
np.min(decision_function), np.max(decision_function)))
Out[110]:
Decision function minimum: -7.69 maximum: 4.29

由于可以任意缩放,因此 decision_function 的输出往往很难解释。
在下面的例子中,我们利用颜色编码在二维平面中画出所有点的 decision_function,还有决策边界,后者我们之间见过。我们将训练点画成圆,将测试数据画成三角(图 2-55):

In[111]:
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
mglearn.tools.plot_2d_separator(gbrt, X, ax=axes[0], alpha=.4,
fill=True, cm=mglearn.cm2)
scores_image = mglearn.tools.plot_2d_scores(gbrt, X, ax=axes[1],
alpha=.4, cm=mglearn.ReBl)
for ax in axes:
# 画出训练点和测试点
mglearn.discrete_scatter(X_test[:, 0], X_test[:, 1], y_test,
markers='^', ax=ax)
mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train,
markers='o', ax=ax)
ax.set_xlabel("Feature 0")
ax.set_ylabel("Feature 1")
cbar = plt.colorbar(scores_image, ax=axes.tolist())
axes[0].legend(["Test class 0", "Test class 1", "Train class 0",
"Train class 1"], ncol=4, loc=(.1, 1.1))
图 2-55:梯度提升模型在一个二维玩具数据集上的决策边界(左)和决策函数(右)

既给出预测结果,又给出分类器的置信程度,这样给出的信息量更大。但在上面的图像中,很难分辨出两个类别之间的边界。

2.4.2 预测概率

predict_proba 的输出是每个类别的概率,通常比 decision_function 的输出更容易理解。对于二分类问题,它的形状始终是 (n_samples, 2):

In[112]:
print("Shape of probabilities: {}".format(gbrt.predict_proba(X_test).shape))
Out[112]:
Shape of probabilities: (25, 2)

每行的第一个元素是第一个类别的估计概率,第二个元素是第二个类别的估计概率。由于predict_proba 的输出是一个概率,因此总是在 0 和 1 之间,两个类别的元素之和始终为 1:

In[113]:
# 显示predict_proba的前几个元素
print("Predicted probabilities:\n{}".format(
gbrt.predict_proba(X_test[:6])))
Out[113]:
Predicted probabilities:
[[ 0.016 0.984]
[ 0.843 0.157]
[ 0.981 0.019]
[ 0.974 0.026]
[ 0.014 0.986]
[ 0.025 0.975]]

由于两个类别的概率之和为 1,因此只有一个类别的概率超过 50%。这个类别就是模型的预测结果。
在上一个输出中可以看到,分类器对大部分点的置信程度都是相对较高的。不确定度大小实际上反映了数据依赖于模型和参数的不确定度。过拟合更强的模型可能会做出置信程度更高的预测,即使可能是错的。复杂度越低的模型通常对预测的不确定度越大。如果模型给出的不确定度符合实际情况,那么这个模型被称为校正(calibrated)模型。在校正模型中,如果预测有 70% 的确定度,那么它在 70% 的情况下正确。
在下面的例子中(图 2-56),我们再次给出该数据集的决策边界,以及类别 1 的类别概率:

In[114]:
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
    
mglearn.tools.plot_2d_separator(
    gbrt, X, ax=axes[0], alpha=.4, fill=True, cm=mglearn.cm2)
scores_image = mglearn.tools.plot_2d_scores(
    gbrt, X, ax=axes[1], alpha=.5, cm=mglearn.ReBl, function='predict_proba')

for ax in axes:
    # plot training and test points
    mglearn.discrete_scatter(X_test[:, 0], X_test[:, 1], y_test,
                             markers='^', ax=ax)
    mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train,
                             markers='o', ax=ax)
    ax.set_xlabel("Feature 0")
    ax.set_ylabel("Feature 1")
# don't want a transparent colorbar
cbar = plt.colorbar(scores_image, ax=axes.tolist())
cbar.set_alpha(1)
cbar.draw_all()
axes[0].legend(["Test class 0", "Test class 1", "Train class 0",
                "Train class 1"], ncol=4, loc=(.1, 1.1))

图 2-56:图 2-55 中梯度提升模型的决策边界(左)和预测概率

这张图中的边界更加明确,不确定的小块区域清晰可见。scikit-learn 网 站(http://scikit-learn.org/stable/auto_examples/classification/plot_classifier_comparison.html)给出了许多模型的对比,以及不确定度估计的形状。

2.4.3 多分类问题的不确定度

到目前为止,我们只讨论了二分类问题中的不确定度估计。但 decision_function 和predict_proba 也适用于多分类问题。我们将这两个函数应用于鸢尾花(Iris)数据集,这是一个三分类数据集:

In[115]:
from sklearn.datasets import load_iris
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
iris.data, iris.target, random_state=42)
gbrt = GradientBoostingClassifier(learning_rate=0.01, random_state=0)
gbrt.fit(X_train, y_train)
In[116]:
print("Decision function shape: {}".format(gbrt.decision_function(X_test).shape))
# 显示决策函数的前几个元素
print("Decision function:\n{}".format(gbrt.decision_function(X_test)[:6, :]))
Out[116]:
Decision function shape: (38, 3)
Decision function:
[[-0.529 1.466 -0.504]
[ 1.512 -0.496 -0.503]
[-0.524 -0.468 1.52 ]
[-0.529 1.466 -0.504]
[-0.531 1.282 0.215]
[ 1.512 -0.496 -0.503]]

对于多分类的情况, decision_function 的形状为 (n_samples, n_classes),每一列对应每个类别的“确定度分数”,分数较高的类别可能性更大,得分较低的类别可能性较小。你可以找出每个数据点的最大元素,从而利用这些分数再现预测结果:

In[117]:
print("Argmax of decision function:\n{}".format(
np.argmax(gbrt.decision_function(X_test), axis=1)))
print("Predictions:\n{}".format(gbrt.predict(X_test)))
Out[117]:
Argmax of decision function:
[1 0 2 1 1 0 1 2 1 1 2 0 0 0 0 1 2 1 1 2 0 2 0 2 2 2 2 2 0 0 0 0 1 0 0 2 1 0]
Predictions:
[1 0 2 1 1 0 1 2 1 1 2 0 0 0 0 1 2 1 1 2 0 2 0 2 2 2 2 2 0 0 0 0 1 0 0 2 1 0]

predict_proba 输出的形状相同,也是 (n_samples, n_classes)。同样,每个数据点所有可能类别的概率之和为 1:
In[118]:

# 显示predict_proba的前几个元素
print("Predicted probabilities:\n{}".format(gbrt.predict_proba(X_test)[:6]))
# 显示每行的和都是1
print("Sums: {}".format(gbrt.predict_proba(X_test)[:6].sum(axis=1)))
Out[118]:
Predicted probabilities:
[[ 0.107 0.784 0.109]
[ 0.789 0.106 0.105]
[ 0.102 0.108 0.789]
[ 0.107 0.784 0.109]
[ 0.108 0.663 0.228]
[ 0.789 0.106 0.105]]
Sums: [ 1. 1. 1. 1. 1. 1.]

同样,我们可以通过计算 predict_proba 的 argmax 来再现预测结果:

In[119]:
print("Argmax of predicted probabilities:\n{}".format(
np.argmax(gbrt.predict_proba(X_test), axis=1)))
print("Predictions:\n{}".format(gbrt.predict(X_test)))
Out[119]:
Argmax of predicted probabilities:
[1 0 2 1 1 0 1 2 1 1 2 0 0 0 0 1 2 1 1 2 0 2 0 2 2 2 2 2 0 0 0 0 1 0 0 2 1 0]
Predictions:
[1 0 2 1 1 0 1 2 1 1 2 0 0 0 0 1 2 1 1 2 0 2 0 2 2 2 2 2 0 0 0 0 1 0 0 2 1 0]

总 之, predict_proba 和 decision_function 的 形 状 始 终 相 同, 都 是 (n_samples, n_classes)——除了二分类特殊情况下的 decision_function。对于二分类的情况, decision_function 只有一列,对应“正”类 classes_[1]。这主要是由于历史原因。如果有 n_classes 列,你可以通过计算每一列的 argmax 来再现预测结果。但如果类别是字符串,或者是整数,但不是从 0 开始的连续整数的话,一定要小心。如果你想要对比 predict 的结果与 decision_function 或 predict_proba 的结果,一定要用分类器的classes_ 属性来获取真实的属性名称:

In[120]:
logreg = LogisticRegression()
# 用Iris数据集的类别名称来表示每一个目标值
named_target = iris.target_names[y_train]
logreg.fit(X_train, named_target)
print("unique classes in training data: {}".format(logreg.classes_))
print("predictions: {}".format(logreg.predict(X_test)[:10]))
argmax_dec_func = np.argmax(logreg.decision_function(X_test), axis=1)
print("argmax of decision function: {}".format(argmax_dec_func[:10]))
print("argmax combined with classes_: {}".format(
logreg.classes_[argmax_dec_func][:10]))
Out[120]:
unique classes in training data: ['setosa' 'versicolor' 'virginica']
predictions: ['versicolor' 'setosa' 'virginica' 'versicolor' 'versicolor'
'setosa' 'versicolor' 'virginica' 'versicolor' 'versicolor']
argmax of decision function: [1 0 2 1 1 0 1 2 1 1]
argmax combined with classes_: ['versicolor' 'setosa' 'virginica' 'versicolor'
'versicolor' 'setosa' 'versicolor' 'virginica' 'versicolor' 'versicolor']

2.5 小结与展望

本章首先讨论了模型复杂度,然后讨论了泛化,或者说学习一个能够在前所未见的新数据上表现良好的模型。这就引出了欠拟合和过拟合的概念,前者是指一个模型无法获取训练数据中的所有变化,后者是指模型过分关注训练数据,但对新数据的泛化性能不好。然后本章讨论了一系列用于分类和回归的机器学习模型,各个模型的优点和缺点,以及如何控制它们的模型复杂度。我们发现,对于许多算法而言,设置正确的参数对模型性能至关重要。有些算法还对输入数据的表示方式很敏感,特别是特征的缩放。因此,如果盲目地将一个算法应用于数据集,而不去理解模型所做的假设以及参数设定的含义,不太可能会得到精度高的模型。
本章包含大量有关算法的信息,在继续阅读后续章节之前你不必记住所有这些细节。但是,这里提到的有关模型的某些知识(以及在特定情况下使用哪种模型)对于在实践中成功应用机器学习模型是很重要的。关于何时使用哪种模型,下面是一份快速总结。
最近邻
适用于小型数据集,是很好的基准模型,很容易解释。
线性模型
非常可靠的首选算法,适用于非常大的数据集,也适用于高维数据。
朴素贝叶斯
只适用于分类问题。比线性模型速度还快,适用于非常大的数据集和高维数据。精度通常要低于线性模型。
决策树
速度很快,不需要数据缩放,可以可视化,很容易解释。
随机森林
几乎总是比单棵决策树的表现要好,鲁棒性很好,非常强大。不需要数据缩放。不适用于高维稀疏数据。
梯度提升决策树
精度通常比随机森林略高。与随机森林相比,训练速度更慢,但预测速度更快,需要的内存也更少。比随机森林需要更多的参数调节。
支持向量机
对于特征含义相似的中等大小的数据集很强大。需要数据缩放,对参数敏感。
神经网络
可以构建非常复杂的模型,特别是对于大型数据集而言。对数据缩放敏感,对参数选取敏感。大型网络需要很长的训练时间。
面对新数据集,通常最好先从简单模型开始,比如线性模型、朴素贝叶斯或最近邻分类器,看能得到什么样的结果。对数据有了进一步了解之后,你可以考虑用于构建更复杂模型的算法,比如随机森林、梯度提升决策树、 SVM 或神经网络。现在你应该对如何应用、调节和分析我们介绍过的模型有了一定的了解。本章主要介绍了二分类问题,因为这通常是最容易理解的。不过本章大多数算法都可以同时用于分类和回归,而且所有分类算法都可以同时用于二分类和多分类。你可以尝试将这些算法应用于scikit-learn 的内置数据集,比如用于回归的 boston_housing 或 diabetes 数据集,或者用于多分类的 digits 数据集。在不同的数据集上实验这些算法,可以让你更好地感受它们所需的训练时间、分析模型的难易程度以及它们对数据表示的敏感程度。
虽然我们分析了不同的参数设定对算法的影响,但在生产环境中实际构建一个对新数据泛化性能很好的模型要更复杂一些。我们将在第 6 章介绍正确调参的方法和自动寻找最佳参数的方法。
不过首先,我们将在下一章深入讨论无监督学习和预处理。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349