Python机器学习基础篇二《监督学习》

前言

前期回顾: Python机器学习基础篇一《为什么用Python进行机器学习》

前面说过,监督学习是最常用也是最成功的机器学习类型之一。本章将会详细介绍监督学 习,并解释几种常用的监督学习算法。我们在第 1 章已经见过一个监督学习的应用:利用 物理测量数据将鸢尾花分成几个品种。

记住,每当想要根据给定输入预测某个结果,并且还有输入 / 输出对的示例时,都应该使 用监督学习。这些输入 / 输出对构成了训练集,我们利用它来构建机器学习模型。我们的 目标是对从未见过的新数据做出准确预测。监督学习通常需要人力来构建训练集,但之后 的任务本来非常费力甚至无法完成,现在却可以自动完成,通常速度也更快。

2.1 分类与回归

监督机器学习问题主要有两种,分别叫作分类(classification)与回归(regression)。

分类问题的目标是预测类别标签(class label),这些标签来自预定义的可选列表。第 1 章讲过一个例子,即将鸢尾花分到三个可能的品种之一。分类问题有时可分为二分类 (binary classification,在两个类别之间进行区分的一种特殊情况)和多分类(multiclass classification,在两个以上的类别之间进行区分)。你可以将二分类看作是尝试回答一道是 / 否问题。将电子邮件分为垃圾邮件和非垃圾邮件就是二分类问题的实例。在这个二分类任 务中,要问的是 / 否问题为:“这封电子邮件是垃圾邮件吗?”

在二分类问题中,我们通常将其中一个类别称为正类(positive class),另一个类别称为反 类(negative class)。这里的“正”并不代表好的方面或正数,而是代表研究对象。因此在 寻找垃圾邮件时,“正”可能指的是垃圾邮件这一类别。将两个类别中的哪一个作为“正 类”,往往是主观判断,与具体的领域有关。

另一方面,鸢尾花的例子则属于多分类问题。另一个多分类的例子是根据网站上的文本预测网站所用的语言。这里的类别就是预定义的语言列表。

回归任务的目标是预测一个连续值,编程术语叫作浮点数(floating-point number),数学术 语叫作实数(real number)。根据教育水平、年龄和居住地来预测一个人的年收入,这就是 回归的一个例子。在预测收入时,预测值是一个金额(amount),可以在给定范围内任意 取值。回归任务的另一个例子是,根据上一年的产量、天气和农场员工数等属性来预测玉 米农场的产量。同样,产量也可以取任意数值。

区分分类任务和回归任务有一个简单方法,就是问一个问题:输出是否具有某种连续性。 如果在可能的结果之间具有连续性,那么它就是一个回归问题。想想预测年收入的例子。 输出具有非常明显的连续性。一年赚 40 000 美元还是 40 001 美元并没有实质差别,即使 两者金额不同。如果我们的算法在本应预测 40 000 美元时的预测结果是 39 999 美元或 40 001 美元,不必过分在意。

与此相反,对于识别网站语言的任务(这是一个分类问题)来说,并不存在程度问题。网 站使用的要么是这种语言,要么是那种语言。在语言之间不存在连续性,在英语和法语之 间不存在其他语言。

2.2 泛化、过拟合与欠拟合

在监督学习中,我们想要在训练数据上构建模型,然后能够对没见过的新数据(这些新数据与训练集具有相同的特性)做出准确预测。如果一个模型能够对没见过的数据做出准确 预测,我们就说它能够从训练集泛化(generalize)到测试集。我们想要构建一个泛化精度 尽可能高的模型。

通常来说,我们构建模型,使其在训练集上能够做出准确预测。如果训练集和测试集足够 相似,我们预计模型在测试集上也能做出准确预测。不过在某些情况下这一点并不成立。 例如,如果我们可以构建非常复杂的模型,那么在训练集上的精度可以想多高就多高。

为了说明这一点,我们来看一个虚构的例子。比如有一个新手数据科学家,已知之前船的 买家记录和对买船不感兴趣的顾客记录,想要预测某个顾客是否会买船。目标是向可能购 买的人发送促销电子邮件,而不去打扰那些不感兴趣的顾客。

假设我们有顾客记录,如表 2-1 所示。

表2-1:顾客数据示例

在这里插入图片描述

在这里插入图片描述

对数据观察一段时间之后,我们的新手数据科学家发现了以下规律:“如果顾客年龄大于 45 岁,并且子女少于 3 个或没有离婚,那么他就想要买船。”如果你问他这个规律的效果 如何,我们的数据科学家会回答:“100% 准确!”的确,对于表中的数据,这条规律完全 正确。我们还可以发现好多规律,都可以完美解释这个数据集中的某人是否想要买船。数 据中的年龄都没有重复,因此我们可以这样说:66、52、53 和 58 岁的人想要买船,而其 他年龄的人都不想买。虽然我们可以编出许多条适用于这个数据集的规律,但要记住,我 们感兴趣的并不是对这个数据集进行预测,我们已经知道这些顾客的答案。我们想知道新 顾客是否可能会买船。因此,我们想要找到一条适用于新顾客的规律,而在训练集上实现 100% 的精度对此并没有帮助。我们可能认为数据科学家发现的规律无法适用于新顾客。 它看起来过于复杂,而且只有很少的数据支持。例如,规律里“或没有离婚”这一条对应 的只有一名顾客。

判断一个算法在新数据上表现好坏的唯一度量,就是在测试集上的评估。然而从直觉上 看 3 ,我们认为简单的模型对新数据的泛化能力更好。如果规律是“年龄大于 50 岁的人想 要买船”,并且这可以解释所有顾客的行为,那么我们将更相信这条规律,而不是与年 龄、子女和婚姻状况都有关系的那条规律。因此,我们总想找到最简单的模型。构建一 个对现有信息量来说过于复杂的模型,正如我们的新手数据科学家做的那样,这被称为 过拟合(overfitting)。如果你在拟合模型时过分关注训练集的细节,得到了一个在训练 集上表现很好、但不能泛化到新数据上的模型,那么就存在过拟合。与之相反,如果你 的模型过于简单——比如说,“有房子的人都买船”——那么你可能无法抓住数据的全部 内容以及数据中的变化,你的模型甚至在训练集上的表现就很差。选择过于简单的模型 被称为欠拟合(underfitting)。

我们的模型越复杂,在训练数据上的预测结果就越好。但是,如果我们的模型过于复杂, 我们开始过多关注训练集中每个单独的数据点,模型就不能很好地泛化到新数据上。

二者之间存在一个最佳位置,可以得到最好的泛化性能。这就是我们想要的模型。

图 2-1 给出了过拟合与欠拟合之间的权衡。

在这里插入图片描述

模型复杂度与数据集大小的关系

需要注意,模型复杂度与训练数据集中输入的变化密切相关:数据集中包含的数据点的变 化范围越大,在不发生过拟合的前提下你可以使用的模型就越复杂。通常来说,收集更多 的数据点可以有更大的变化范围,所以更大的数据集可以用来构建更复杂的模型。但是, 仅复制相同的数据点或收集非常相似的数据是无济于事的。

回到前面卖船的例子,如果我们查看了 10 000 多行的顾客数据,并且所有数据都符合 这条规律:“如果顾客年龄大于 45 岁,并且子女少于 3 个或没有离婚,那么他就想要买 船”,那么我们就更有可能相信这是一条有效的规律,比从表 2-1 中仅 12 行数据得出来 的更为可信。

收集更多数据,适当构建更复杂的模型,对监督学习任务往往特别有用。本书主要关注固 定大小的数据集。在现实世界中,你往往能够决定收集多少数据,这可能比模型调参更为 有效。永远不要低估更多数据的力量!

2.3 监督学习算法

现在开始介绍最常用的机器学习算法,并解释这些算法如何从数据中学习以及如何预测。 我们还会讨论每个模型的复杂度如何变化,并概述每个算法如何构建模型。我们将说明每 个算法的优点和缺点,以及它们最适应用于哪类数据。此外还会解释最重要的参数和选项 的含义。4 许多算法都有分类和回归两种形式,两者我们都会讲到。

没有必要通读每个算法的详细描述,但理解模型可以让你更好地理解机器学习算法的各种 工作原理。本章还可以用作参考指南,当你不确定某个算法的工作原理时,就可以回来查 看本章内容。

2.3.1 一些样本数据集

我们将使用一些数据集来说明不同的算法。其中一些数据集很小,而且是模拟的,其目的 是强调算法的某个特定方面。其他数据集都是现实世界的大型数据集。

一个模拟的二分类数据集示例是 forge 数据集,它有两个特征。下列代码将绘制一个散点 图(图 2-2),将此数据集的所有数据点可视化。图像以第一个特征为 x 轴,第二个特征为 y 轴。正如其他散点图那样,每个数据点对应图像中的一点。每个点的颜色和形状对应其 类别:

In[2]:
# 生成数据集
X, y = mglearn.datasets.make_forge()
# 数据集绘图
mglearn.discrete_scatter(X[:, 0], X[:, 1], y)
plt.legend(["Class 0", "Class 1"], loc=4)
plt.xlabel("First feature")
plt.ylabel("Second feature")
print("X.shape: {}".format(X.shape))
Out[2]:
X.shape: (26, 2)

在这里插入图片描述

图 2-2:forge 数据集的散点图

从 X.shape 可以看出,这个数据集包含 26 个数据点和 2 个特征。

我们用模拟的 wave 数据集来说明回归算法。wave 数据集只有一个输入特征和一个连续的 目标变量(或响应),后者是模型想要预测的对象。下面绘制的图像(图 2-3)中单一特征 位于 x 轴,回归目标(输出)位于 y 轴:

In[3]:
X, y = mglearn.datasets.make_wave(n_samples=40)
plt.plot(X, y, 'o')
plt.ylim(-3, 3)
plt.xlabel("Feature")
plt.ylabel("Target")

在这里插入图片描述

图 2-3:wave 数据集的图像,x 轴表示特征,y 轴表示回归目标

我们之所以使用这些非常简单的低维数据集,是因为它们的可视化非常简单——书页只有 两个维度,所以很难展示特征数超过两个的数据。从特征较少的数据集(也叫低维数据 集)中得出的结论可能并不适用于特征较多的数据集(也叫高维数据集)。只要你记住这 一点,那么在低维数据集上研究算法也是很有启发的。

除了上面这些小型的模拟的数据集,我们还将补充两个现实世界中的数据集,它们都包含 在 scikit-learn 中。其中一个是威斯康星州乳腺癌数据集(简称 cancer),里面记录了乳 腺癌肿瘤的临床测量数据。每个肿瘤都被标记为“良性”(benign,表示无害肿瘤)或“恶 性”(malignant,表示癌性肿瘤),其任务是基于人体组织的测量数据来学习预测肿瘤是否 为恶性。

可以用 scikit-learn 模块的 load_breast_cancer 函数来加载数据:

In[4]:
from sklearn.datasets import load_breast_cancer
cancer = load_breast_cancer()
print("cancer.keys(): \n{}".format(cancer.keys()))
Out[4]:
cancer.keys():
dict_keys(['feature_names', 'data', 'DESCR', 'target', 'target_names'])

包含在 scikit-learn 中的数据集通常被保存为 Bunch 对象,里面包含真实 数据以及一些数据集信息。关于 Bunch 对象,你只需要知道它与字典很相 似,而且还有一个额外的好处,就是你可以用点操作符来访问对象的值(比 如用 bunch.key 来代替 bunch[‘key’])。

这个数据集共包含 569 个数据点,每个数据点有 30 个特征:

In[5]:
print("Shape of cancer data: {}".format(cancer.data.shape))
Out[5]:
Shape of cancer data: (569, 30)

在 569 个数据点中,212 个被标记为恶性,357 个被标记为良性:

In[6]:
print("Sample counts per class:\n{}".format(
 {
   n: v for n, v in zip(cancer.target_names, np.bincount(cancer.target))}))
Out[6]:
Sample counts per class:
{
   'benign': 357, 'malignant': 212}

为了得到每个特征的语义说明,我们可以看一下 feature_names 属性:

In[7]:
print("Feature names:\n{}".format(cancer.feature_names))
Out[7]:
Feature names:
['mean radius' 'mean texture' 'mean perimeter' 'mean area'
 'mean smoothness' 'mean compactness' 'mean concavity'
 'mean concave points' 'mean symmetry' 'mean fractal dimension'
 'radius error' 'texture error' 'perimeter error' 'area error'
 'smoothness error' 'compactness error' 'concavity error'
 'concave points error' 'symmetry error' 'fractal dimension error'
 'worst radius' 'worst texture' 'worst perimeter' 'worst area'
 'worst smoothness' 'worst compactness' 'worst concavity'
 'worst concave points' 'worst symmetry' 'worst fractal dimension']

感兴趣的话,你可以阅读 cancer.DESCR 来了解数据的更多信息。

我们还会用到一个现实世界中的回归数据集,即波士顿房价数据集。与这个数据集相关的 任务是,利用犯罪率、是否邻近查尔斯河、公路可达性等信息,来预测 20 世纪 70 年代波 士顿地区房屋价格的中位数。这个数据集包含 506 个数据点和 13 个特征:

In[8]:
from sklearn.datasets import load_boston
boston = load_boston()
print("Data shape: {}".format(boston.data.shape))
Out[8]:
Data shape: (506, 13)

同样,你可以阅读 boston 对象的 DESCR 属性来了解数据集的更多信息。对于我们的目的而言,我们需要扩展这个数据集,输入特征不仅包括这 13 个测量结果,还包括这些特征之间的乘积(也叫交互项)。换句话说,我们不仅将犯罪率和公路可达性作为特征,还将 犯罪率和公路可达性的乘积作为特征。像这样包含导出特征的方法叫作特征工程(feature engineering),将在第 4 章中详细讲述。这个导出的数据集可以用 load_extended_boston 函数加载:

In[9]:
X, y = mglearn.datasets.load_extended_boston()
print("X.shape: {}".format(X.shape))
Out[9]:
X.shape: (506, 104)

最初的 13 个特征加上这 13 个特征两两组合(有放回)得到的 91 个特征,一共有 104 个 特征。

我们将利用这些数据集对不同机器学习算法的性质进行解释说明。但目前来说,先来看算 法本身。首先重新学习上一章见过的 k 近邻(k-NN)算法。

2.3.2 k近邻

k-NN 算法可以说是最简单的机器学习算法。构建模型只需要保存训练数据集即可。想要 对新数据点做出预测,算法会在训练数据集中找到最近的数据点,也就是它的“最近邻”。

  1. k近邻分类

k-NN 算法最简单的版本只考虑一个最近邻,也就是与我们想要预测的数据点最近的训练 数据点。预测结果就是这个训练数据点的已知输出。图 2-4 给出了这种分类方法在 forge 数据集上的应用:

In[10]:
mglearn.plots.plot_knn_classification(n_neighbors=1)

在这里插入图片描述

图 2-4:单一最近邻模型对 forge 数据集的预测结果

这里我们添加了 3 个新数据点(用五角星表示)。对于每个新数据点,我们标记了训练集 中与它最近的点。单一最近邻算法的预测结果就是那个点的标签(对应五角星的颜色)。

除了仅考虑最近邻,我还可以考虑任意个(k 个)邻居。这也是 k 近邻算法名字的来历。 在考虑多于一个邻居的情况时,我们用“投票法”(voting)来指定标签。也就是说,对 于每个测试点,我们数一数多少个邻居属于类别 0,多少个邻居属于类别 1。然后将出现 次数更多的类别(也就是 k 个近邻中占多数的类别)作为预测结果。下面的例子(图 2-5) 用到了 3 个近邻:

In[11]:
mglearn.plots.plot_knn_classification(n_neighbors=3)

在这里插入图片描述

图 2-5:3 近邻模型对 forge 数据集的预测结果

和上面一样,预测结果可以从五角星的颜色看出。你可以发现,左上角新数据点的预测结 果与只用一个邻居时的预测结果不同。

虽然这张图对应的是一个二分类问题,但方法同样适用于多分类的数据集。对于多分类问 题,我们数一数每个类别分别有多少个邻居,然后将最常见的类别作为预测结果。

现在看一下如何通过 scikit-learn 来应用 k 近邻算法。首先,正如第 1 章所述,将数据分 为训练集和测试集,以便评估泛化性能:

In[12]:
from sklearn.model_selection import train_test_split
X, y = mglearn.datasets.make_forge()
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)

然后,导入类并将其实例化。这时可以设定参数,比如邻居的个数。这里我们将其设为 3:

In[13]:
from sklearn.neighbors import KNeighborsClassifier
clf = KNeighborsClassifier(n_neighbors=3)

现在,利用训练集对这个分类器进行拟合。对于 KNeighborsClassifier 来说就是保存数据 集,以便在预测时计算与邻居之间的距离:

In[14]:
clf.fit(X_train, y_train)

调用 predict 方法来对测试数据进行预测。对于测试集中的每个数据点,都要计算它在训 练集的最近邻,然后找出其中出现次数最多的类别:

In[15]:
print("Test set predictions: {}".format(clf.predict(X_test)))
Out[15]:
Test set predictions: [1 0 1 0 1 0 0]

为了评估模型的泛化能力好坏,我们可以对测试数据和测试标签调用 score 方法:

In[16]:
print("Test set accuracy: {:.2f}".format(clf.score(X_test, y_test)))
Out[16]:
Test set accuracy: 0.86

可以看到,我们的模型精度约为 86%,也就是说,在测试数据集中,模型对其中 86% 的 样本预测的类别都是正确的。

  1. 分析KNeighborsClassifier

对于二维数据集,我们还可以在 xy 平面上画出所有可能的测试点的预测结果。我们根据 平面中每个点所属的类别对平面进行着色。这样可以查看决策边界(decision boundary), 即算法对类别 0 和类别 1 的分界线。

下列代码分别将 1 个、3 个和 9 个邻居三种情况的决策边界可视化,见图 2-6:

In[17]:
fig, axes = plt.subplots(1, 3, figsize=(10, 3))
for n_neighbors, ax in zip([1, 3, 9], axes):
 # fit方法返回对象本身,所以我们可以将实例化和拟合放在一行代码中
 clf = KNeighborsClassifier(n_neighbors=n_neighbors).fit(X, y)
 mglearn.plots.plot_2d_separator(clf, X, fill=True, eps=0.5, ax=ax, alpha=.4)
 mglearn.discrete_scatter(X[:, 0], X[:, 1], y, ax=ax)
 ax.set_title("{} neighbor(s)".format(n_neighbors))
 ax.set_xlabel("feature 0")
 ax.set_ylabel("feature 1")
axes[0].legend(loc=3)

在这里插入图片描述

图 2-6:不同 n_neighbors 值的 k 近邻模型的决策边界

从左图可以看出,使用单一邻居绘制的决策边界紧跟着训练数据。随着邻居个数越来越 多,决策边界也越来越平滑。更平滑的边界对应更简单的模型。换句话说,使用更少的邻 居对应更高的模型复杂度(如图 2-1 右侧所示),而使用更多的邻居对应更低的模型复杂度 (如图 2-1 左侧所示)。假如考虑极端情况,即邻居个数等于训练集中所有数据点的个数, 那么每个测试点的邻居都完全相同(即所有训练点),所有预测结果也完全相同(即训练 集中出现次数最多的类别)。

我们来研究一下能否证实之前讨论过的模型复杂度和泛化能力之间的关系。我们将在现实 世界的乳腺癌数据集上进行研究。先将数据集分成训练集和测试集,然后用不同的邻居个 数对训练集和测试集的性能进行评估。输出结果见图 2-7:

In[18]:
from sklearn.datasets import load_breast_cancer
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
 cancer.data, cancer.target, stratify=cancer.target, random_state=66)
training_accuracy = []
test_accuracy = []
# n_neighbors取值从1到10
neighbors_settings = range(1, 11)
for n_neighbors in neighbors_settings:
# 构建模型
 clf = KNeighborsClassifier(n_neighbors=n_neighbors)
 clf.fit(X_train, y_train)
 # 记录训练集精度
 training_accuracy.append(clf.score(X_train, y_train))
 # 记录泛化精度
 test_accuracy.append(clf.score(X_test, y_test))
plt.plot(neighbors_settings, training_accuracy, label="training accuracy")
plt.plot(neighbors_settings, test_accuracy, label="test accuracy")
plt.ylabel("Accuracy")
plt.xlabel("n_neighbors")
plt.legend()

图像的 x 轴是 n_neighbors,y 轴是训练集精度和测试集精度。虽然现实世界的图像很少有 非常平滑的,但我们仍可以看出过拟合与欠拟合的一些特征(注意,由于更少的邻居对应 更复杂的模型,所以此图相对于图 2-1 做了水平翻转)。仅考虑单一近邻时,训练集上的预 测结果十分完美。但随着邻居个数的增多,模型变得更简单,训练集精度也随之下降。单 一邻居时的测试集精度比使用更多邻居时要低,这表示单一近邻的模型过于复杂。与之相 反,当考虑 10 个邻居时,模型又过于简单,性能甚至变得更差。最佳性能在中间的某处, 邻居个数大约为 6。不过最好记住这张图的坐标轴刻度。最差的性能约为 88% 的精度,这 个结果仍然可以接受。

在这里插入图片描述

图 2-7:以 n_neighbors 为自变量,对比训练集精度和测试集精度

  1. k近邻回归

k 近邻算法还可以用于回归。我们还是先从单一近邻开始,这次使用 wave 数据集。我们添 加了 3 个测试数据点,在 x 轴上用绿色五角星表示。利用单一邻居的预测结果就是最近邻 的目标值。在图 2-8 中用蓝色五角星表示:

In[19]:
mglearn.plots.plot_knn_regression(n_neighbors=1)

在这里插入图片描述

图 2-8:单一近邻回归对 wave 数据集的预测结果

同样,也可以用多个近邻进行回归。在使用多个近邻时,预测结果为这些邻居的平均值 (图 2-9):

In[20]:
mglearn.plots.plot_knn_regression(n_neighbors=3)

在这里插入图片描述

图 2-9:3 个近邻回归对 wave 数据集的预测结果

用于回归的 k 近邻算法在 scikit-learn 的 KNeighborsRegressor 类中实现。其用法与 KNeighborsClassifier 类似:

In[21]:
from sklearn.neighbors import KNeighborsRegressor
X, y = mglearn.datasets.make_wave(n_samples=40)
# 将wave数据集分为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
# 模型实例化,并将邻居个数设为3
reg = KNeighborsRegressor(n_neighbors=3)
# 利用训练数据和训练目标值来拟合模型
reg.fit(X_train, y_train)

现在可以对测试集进行预测:

In[22]:
print("Test set predictions:\n{}".format(reg.predict(X_test)))
Out[22]:
Test set predictions:
[-0.054 0.357 1.137 -1.894 -1.139 -1.631 0.357 0.912 -0.447 -1.139]

我们还可以用 score 方法来评估模型,对于回归问题,这一方法返回的是 R2 分数。R2 分 数也叫作决定系数,是回归模型预测的优度度量,位于 0 到 1 之间。R2 等于 1 对应完美预 测,R2 等于 0 对应常数模型,即总是预测训练集响应(y_train)的平均值:

In[23]:
print("Test set R^2: {:.2f}".format(reg.score(X_test, y_test)))
Out[23]:
Test set R^2: 0.83

这里的分数是 0.83,表示模型的拟合相对较好。

  1. 分析KNeighborsRegressor

对于我们的一维数据集,可以查看所有特征取值对应的预测结果(图 2-10)。为了便于绘 图,我们创建一个由许多点组成的测试数据集:

In[24]:
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# 创建1000个数据点,在-3和3之间均匀分布
line = np.linspace(-3, 3, 1000).reshape(-1, 1)
for n_neighbors, ax in zip([1, 3, 9], axes):
 # 利用1个、3个或9个邻居分别进行预测
 reg = KNeighborsRegressor(n_neighbors=n_neighbors)
 reg.fit(X_train, y_train)
 ax.plot(line, reg.predict(line))
 ax.plot(X_train, y_train, '^', c=mglearn.cm2(0), markersize=8)
 ax.plot(X_test, y_test, 'v', c=mglearn.cm2(1), markersize=8)
  ax.set_title(
 "{} neighbor(s)\n train score: {:.2f} test score: {:.2f}".format(
 n_neighbors, reg.score(X_train, y_train),
 reg.score(X_test, y_test)))
 ax.set_xlabel("Feature")
 ax.set_ylabel("Target")
axes[0].legend(["Model predictions", "Training data/target",
 "Test data/target"], loc="best")

在这里插入图片描述

图 2-10:不同 n_neighbors 值的 k 近邻回归的预测结果对比

从图中可以看出,仅使用单一邻居,训练集中的每个点都对预测结果有显著影响,预测结 果的图像经过所有数据点。这导致预测结果非常不稳定。考虑更多的邻居之后,预测结果 变得更加平滑,但对训练数据的拟合也不好。

  1. 优点、缺点和参数

一般来说,KNeighbors 分类器有 2 个重要参数:邻居个数与数据点之间距离的度量方法。 在实践中,使用较小的邻居个数(比如 3 个或 5 个)往往可以得到比较好的结果,但你应 该调节这个参数。选择合适的距离度量方法超出了本书的范围。默认使用欧式距离,它在 许多情况下的效果都很好。

k-NN 的优点之一就是模型很容易理解,通常不需要过多调节就可以得到不错的性能。在 考虑使用更高级的技术之前,尝试此算法是一种很好的基准方法。构建最近邻模型的速度 通常很快,但如果训练集很大(特征数很多或者样本数很大),预测速度可能会比较慢。 使用 k-NN 算法时,对数据进行预处理是很重要的(见第 3 章)。这一算法对于有很多特 征(几百或更多)的数据集往往效果不好,对于大多数特征的大多数取值都为 0 的数据集 (所谓的稀疏数据集)来说,这一算法的效果尤其不好。

虽然 k 近邻算法很容易理解,但由于预测速度慢且不能处理具有很多特征的数据集,所以 在实践中往往不会用到。下面介绍的这种方法就没有这两个缺点。

2.3.3 线性模型

线性模型是在实践中广泛使用的一类模型,几十年来被广泛研究,它可以追溯到一百多年 前。线性模型利用输入特征的线性函数(linear function)进行预测,稍后会对此进行解释。

  1. 用于回归的线性模型

对于回归问题,线性模型预测的一般公式如下:

ŷ = w[0] * x[0] + w[1] * x[1] ++ w[p] * x[p] + b

这里 x[0] 到 x[p] 表示单个数据点的特征(本例中特征个数为 p+1),w 和 b 是学习模型的 参数,ŷ 是模型的预测结果。对于单一特征的数据集,公式如下:

ŷ = w[0] * x[0] + b

你可能还记得,这就是高中数学里的直线方程。这里 w[0] 是斜率,b 是 y 轴偏移。对于有 更多特征的数据集,w 包含沿每个特征坐标轴的斜率。或者,你也可以将预测的响应值看 作输入特征的加权求和,权重由 w 的元素给出(可以取负值)。

下列代码可以在一维 wave 数据集上学习参数 w[0] 和 b:

In[25]:
mglearn.plots.plot_linear_regression_wave()
Out[25]:
w[0]: 0.393906 b: -0.031804

在这里插入图片描述

图 2-11:线性模型对 wave 数据集的预测结果

我们在图中添加了坐标网格,便于理解直线的含义。从 w[0] 可以看出,斜率应该在 0.4 左 右,在图像中也可以直观地确认这一点。截距是指预测直线与 y 轴的交点:比 0 略小,也 可以在图像中确认。

用于回归的线性模型可以表示为这样的回归模型:对单一特征的预测结果是一条直线,两 个特征时是一个平面,或者在更高维度(即更多特征)时是一个超平面。

如果将直线的预测结果与图 2-10 中 KNeighborsRegressor 的预测结果进行比较,你会发现 直线的预测能力非常受限。似乎数据的所有细节都丢失了。从某种意义上来说,这种说法 是正确的。假设目标 y 是特征的线性组合,这是一个非常强的(也有点不现实的)假设。 但观察一维数据得出的观点有些片面。对于有多个特征的数据集而言,线性模型可以非常 强大。特别地,如果特征数量大于训练数据点的数量,任何目标 y 都可以(在训练集上) 用线性函数完美拟合 。

有许多不同的线性回归模型。这些模型之间的区别在于如何从训练数据中学习参数 w 和 b,以及如何控制模型复杂度。下面介绍最常见的线性回归模型。

  1. 线性回归(又名普通最小二乘法)

线性回归,或者普通最小二乘法(ordinary least squares,OLS),是回归问题最简单也最经 典的线性方法。线性回归寻找参数 w 和 b,使得对训练集的预测值与真实的回归目标值 y 之间的均方误差最小。均方误差(mean squared error)是预测值与真实值之差的平方和除 以样本数。线性回归没有参数,这是一个优点,但也因此无法控制模型的复杂度。

下列代码可以生成图 2-11 中的模型:

In[26]:
from sklearn.linear_model import LinearRegression
X, y = mglearn.datasets.make_wave(n_samples=60)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
lr = LinearRegression().fit(X_train, y_train)

“斜率”参数(w,也叫作权重或系数)被保存在 coef_ 属性中,而偏移或截距(b)被保 存在 intercept_ 属性中:

In[27]:
print("lr.coef_: {}".format(lr.coef_))
print("lr.intercept_: {}".format(lr.intercept_))
Out[27]:
lr.coef_: [ 0.394]
lr.intercept_: -0.031804343026759746

你可能注意到了 coef_ 和 intercept_ 结尾处奇怪的下划线。scikit-learn 总是将从训练数据中得出的值保存在以下划线结尾的属性中。这是为了将其 与用户设置的参数区分开。

intercept_ 属性是一个浮点数,而 coef_ 属性是一个 NumPy 数组,每个元素对应一个输 入特征。由于 wave 数据集中只有一个输入特征,所以 lr.coef_ 中只有一个元素。

我们来看一下训练集和测试集的性能:

In[28]:
print("Training set score: {:.2f}".format(lr.score(X_train, y_train)))
print("Test set score: {:.2f}".format(lr.score(X_test, y_test)))
Out[28]:
Training set score: 0.67
Test set score: 0.66

R2 约为 0.66,这个结果不是很好,但我们可以看到,训练集和测试集上的分数非常接近。 这说明可能存在欠拟合,而不是过拟合。对于这个一维数据集来说,过拟合的风险很小, 因为模型非常简单(或受限)。然而,对于更高维的数据集(即有大量特征的数据集),线 性模型将变得更加强大,过拟合的可能性也会变大。我们来看一下 LinearRegression 在更 复杂的数据集上的表现,比如波士顿房价数据集。记住,这个数据集有 506 个样本和 105 个导出特征。首先,加载数据集并将其分为训练集和测试集。然后像前面一样构建线性回 归模型:

In[29]:
X, y = mglearn.datasets.load_extended_boston()
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
lr = LinearRegression().fit(X_train, y_train)

比较一下训练集和测试集的分数就可以发现,我们在训练集上的预测非常准确,但测试集 上的 R2 要低很多:

In[30]:
print("Training set score: {:.2f}".format(lr.score(X_train, y_train)))
print("Test set score: {:.2f}".format(lr.score(X_test, y_test)))
Out[30]:
Training set score: 0.95
Test set score: 0.61

训练集和测试集之间的性能差异是过拟合的明显标志,因此我们应该试图找到一个可以控 制复杂度的模型。标准线性回归最常用的替代方法之一就是岭回归(ridge regression),下 面来看一下。

  1. 岭回归

岭回归也是一种用于回归的线性模型,因此它的预测公式与普通最小二乘法相同。但在岭 回归中,对系数(w)的选择不仅要在训练数据上得到好的预测结果,而且还要拟合附加 约束。我们还希望系数尽量小。换句话说,w 的所有元素都应接近于 0。直观上来看,这 意味着每个特征对输出的影响应尽可能小(即斜率很小),同时仍给出很好的预测结果。 这种约束是所谓正则化(regularization)的一个例子。正则化是指对模型做显式约束,以避免过拟合。岭回归用到的这种被称为 L2 正则化。

岭回归在 linear_model.Ridge 中实现。来看一下它对扩展的波士顿房价数据集的效 果如何:

In[31]:
from sklearn.linear_model import Ridge
ridge = Ridge().fit(X_train, y_train)
print("Training set score: {:.2f}".format(ridge.score(X_train, y_train)))
print("Test set score: {:.2f}".format(ridge.score(X_test, y_test)))
Out[31]:
Training set score: 0.89
Test set score: 0.75

可以看出,Ridge 在训练集上的分数要低于 LinearRegression,但在测试集上的分数更高。 这和我们的预期一致。线性回归对数据存在过拟合。Ridge 是一种约束更强的模型,所以 更不容易过拟合。复杂度更小的模型意味着在训练集上的性能更差,但泛化性能更好。由 于我们只对泛化性能感兴趣,所以应该选择 Ridge 模型而不是 LinearRegression 模型。

Ridge 模型在模型的简单性(系数都接近于 0)与训练集性能之间做出权衡。简单性和训练 集性能二者对于模型的重要程度可以由用户通过设置 alpha 参数来指定。在前面的例子中, 我们用的是默认参数 alpha=1.0。但没有理由认为这会给出最佳权衡。alpha 的最佳设定 值取决于用到的具体数据集。增大 alpha 会使得系数更加趋向于 0,从而降低训练集性能, 但可能会提高泛化性能。例如:

In[32]:
ridge10 = Ridge(alpha=10).fit(X_train, y_train)
print("Training set score: {:.2f}".format(ridge10.score(X_train, y_train)))
print("Test set score: {:.2f}".format(ridge10.score(X_test, y_test)))
Out[32]:
Training set score: 0.79
Test set score: 0.64

减小 alpha 可以让系数受到的限制更小,即在图 2-1 中向右移动。对于非常小的 alpha 值, 系数几乎没有受到限制,我们得到一个与 LinearRegression 类似的模型:

In[33]:
ridge01 = Ridge(alpha=0.1).fit(X_train, y_train)
print("Training set score: {:.2f}".format(ridge01.score(X_train, y_train)))
print("Test set score: {:.2f}".format(ridge01.score(X_test, y_test)))
Out[33]:
Training 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值