美文网首页
第 2 章 监督学习(上)

第 2 章 监督学习(上)

作者: python基础机器学习 | 来源:发表于2020-08-30 17:48 被阅读0次

    前面说过,监督学习是最常用也是最成功的机器学习类型之一。本章将会详细介绍监督学习,并解释几种常用的监督学习算法。我们在第 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 美元并没有实质差别,即使两者金额不同。如果我们的算法在本应预测 40000 美元时的预测结果是 39999 美元或
    40001 美元,不必过分在意。
    与此相反,对于识别网站语言的任务(这是一个分类问题)来说,并不存在程度问题。网站使用的要么是这种语言,要么是那种语言。在语言之间不存在连续性,在英语和法语之
    间不存在其他语言。

    2.2 泛化、 过拟合与欠拟合

    在监督学习中,我们想要在训练数据上构建模型,然后能够对没见过的新数据(这些新数据与训练集具有相同的特性)做出准确预测。如果一个模型能够对没见过的数据做出准确
    预测,我们就说它能够从训练集泛化(generalize)到测试集。我们想要构建一个泛化精度尽可能高的模型。
    通常来说,我们构建模型,使其在训练集上能够做出准确预测。如果训练集和测试集足够相似,我们预计模型在测试集上也能做出准确预测。不过在某些情况下这一点并不成立。
    例如,如果我们可以构建非常复杂的模型,那么在训练集上的精度可以想多高就多高。
    为了说明这一点,我们来看一个虚构的例子。比如有一个新手数据科学家,已知之前船的买家记录和对买船不感兴趣的顾客记录,想要预测某个顾客是否会买船。目标是向可能购
    买的人发送促销电子邮件,而不去打扰那些不感兴趣的顾客。
    假设我们有顾客记录,如表 2-1 所示。

    表2-1:顾客数据示例

    对数据观察一段时间之后,我们的新手数据科学家发现了以下规律:“如果顾客年龄大于45 岁,并且子女少于 3 个或没有离婚,那么他就想要买船。”如果你问他这个规律的效果
    如何,我们的数据科学家会回答:“100% 准确!”的确,对于表中的数据,这条规律完全正确。我们还可以发现好多规律,都可以完美解释这个数据集中的某人是否想要买船。数据中的年龄都没有重复,因此我们可以这样说: 66、 52、 53 和 58 岁的人想要买船,而其他年龄的人都不想买。虽然我们可以编出许多条适用于这个数据集的规律,但要记住,我们感兴趣的并不是对这个数据集进行预测,我们已经知道这些顾客的答案。我们想知道新顾客是否可能会买船。因此,我们想要找到一条适用于新顾客的规律,而在训练集上实现100% 的精度对此并没有帮助。我们可能认为数据科学家发现的规律无法适用于新顾客。它看起来过于复杂,而且只有很少的数据支持。例如,规律里“或没有离婚”这一条对应的只有一名顾客。
    判断一个算法在新数据上表现好坏的唯一度量,就是在测试集上的评估。然而从直觉上看,我们认为简单的模型对新数据的泛化能力更好。如果规律是“年龄大于 50 岁的人想要买船”,并且这可以解释所有顾客的行为,那么我们将更相信这条规律,而不是与年龄、子女和婚姻状况都有关系的那条规律。因此,我们总想找到最简单的模型。构建一个对现有信息量来说过于复杂的模型,正如我们的新手数据科学家做的那样,这被称为过拟合(overfitting)。如果你在拟合模型时过分关注训练集的细节,得到了一个在训练集上表现很好、但不能泛化到新数据上的模型,那么就存在过拟合。与之相反,如果你的模型过于简单——比如说,“有房子的人都买船”——那么你可能无法抓住数据的全部内容以及数据中的变化,你的模型甚至在训练集上的表现就很差。选择过于简单的模型被称为欠拟合(underfitting)。
    我们的模型越复杂,在训练数据上的预测结果就越好。但是,如果我们的模型过于复杂,我们开始过多关注训练集中每个单独的数据点,模型就不能很好地泛化到新数据上。
    二者之间存在一个最佳位置,可以得到最好的泛化性能。这就是我们想要的模型。
    图 2-1 给出了过拟合与欠拟合之间的权衡。


    图 2-1:模型复杂度与训练精度和测试精度之间的权衡

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

    需要注意,模型复杂度与训练数据集中输入的变化密切相关:数据集中包含的数据点的变化范围越大,在不发生过拟合的前提下你可以使用的模型就越复杂。通常来说,收集更多的数据点可以有更大的变化范围,所以更大的数据集可以用来构建更复杂的模型。但是,仅复制相同的数据点或收集非常相似的数据是无济于事的。
    回到前面卖船的例子,如果我们查看了 10 000 多行的顾客数据,并且所有数据都符合这条规律:“如果顾客年龄大于 45 岁,并且子女少于 3 个或没有离婚,那么他就想要买船”,那么我们就更有可能相信这是一条有效的规律,比从表 2-1 中仅 12 行数据得出来的更为可信。
    收集更多数据,适当构建更复杂的模型,对监督学习任务往往特别有用。本书主要关注固定大小的数据集。在现实世界中,你往往能够决定收集多少数据,这可能比模型调参更为有效。永远不要低估更多数据的力量!

    2.3 监督学习算法

    现在开始介绍最常用的机器学习算法,并解释这些算法如何从数据中学习以及如何预测。我们还会讨论每个模型的复杂度如何变化,并概述每个算法如何构建模型。我们将说明每
    个算法的优点和缺点,以及它们最适应用于哪类数据。此外还会解释最重要的参数和选项的含义。许多算法都有分类和回归两种形式,两者我们都会讲到。
    没有必要通读每个算法的详细描述,但理解模型可以让你更好地理解机器学习算法的各种工作原理。本章还可以用作参考指南,当你不确定某个算法的工作原理时,就可以回来查
    看本章内容。

    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% 的样本预测的类别都是正确的。

    2. 分析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:

    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 为自变量,对比训练集精度和测试集精度

    3. 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,表示模型的拟合相对较好。

    4. 分析KNeighborsRegressor

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

    In[24]:
    fig, axes = plt.subplots(1, 3, figsize=(15, 4))
    # create 1,000 data points, evenly spaced between -3 and 3
    line = np.linspace(-3, 3, 1000).reshape(-1, 1)
    for n_neighbors, ax in zip([1, 3, 9], axes):
        # make predictions using 1, 3, or 9 neighbors
        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 近邻回归的预测结果对比

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

    5. 优点、 缺点和参数

    一般来说, 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,以及如何控制模型复杂度。下面介绍最常见的线性回归模型。

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

    线性回归,或者普通最小二乘法(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),下面来看一下。

    3. 岭回归

    岭回归也是一种用于回归的线性模型,因此它的预测公式与普通最小二乘法相同。但在岭回归中,对系数(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 set score: 0.93
    Test set score: 0.77
    

    这里 alpha=0.1 似乎效果不错。我们可以尝试进一步减小 alpha 以提高泛化性能。现在,注意参数 alpha 与图 2-1 中的模型复杂度的对应关系。第 5 章将会讨论选择参数的正确方法。我们还可以查看 alpha 取不同值时模型的 coef_ 属性,从而更加定性地理解 alpha 参数是如何改变模型的。更大的 alpha 表示约束更强的模型,所以我们预计大 alpha 对应的 coef_元素比小 alpha 对应的 coef_ 元素要小。这一点可以在图 2-12 中得到证实:

    In[34]:
    plt.plot(ridge.coef_, 's', label="Ridge alpha=1")
    plt.plot(ridge10.coef_, '^', label="Ridge alpha=10")
    plt.plot(ridge01.coef_, 'v', label="Ridge alpha=0.1")
    plt.plot(lr.coef_, 'o', label="LinearRegression")
    plt.xlabel("Coefficient index")
    plt.ylabel("Coefficient magnitude")
    plt.hlines(0, 0, len(lr.coef_))
    plt.ylim(-25, 25)
    plt.legend()
    
    图 2-12:不同 alpha 值的岭回归与线性回归的系数比较

    这里 x 轴对应 coef_ 的元素: x=0 对应第一个特征的系数, x=1 对应第二个特征的系数,以此类推,一直到 x=100。 y 轴表示该系数的具体数值。这里需要记住的是,对于 alpha=10,系数大多在 -3 和 3 之间。对于 alpha=1 的 Ridge 模型,系数要稍大一点。对于 alpha=0.1,点的范围更大。对于没有做正则化的线性回归(即 alpha=0),点的范围很大,许多点都超出了图像的范围。
    还有一种方法可以用来理解正则化的影响,就是固定 alpha 值,但改变训练数据量。对于图 2-13 来说,我们对波士顿房价数据集做二次抽样,并在数据量逐渐增加的子数据集上分别对 LinearRegression 和 Ridge(alpha=1) 两个模型进行评估(将模型性能作为数据集大小的函数进行绘图,这样的图像叫作学习曲线):

    In[35]:
    mglearn.plots.plot_ridge_n_samples()
    
    图 2-13:岭回归和线性回归在波士顿房价数据集上的学习曲线

    正如所预计的那样,无论是岭回归还是线性回归,所有数据集大小对应的训练分数都要高于测试分数。由于岭回归是正则化的,因此它的训练分数要整体低于线性回归的训练分数。但岭回归的测试分数要更高,特别是对较小的子数据集。如果少于 400 个数据点,线性回归学不到任何内容。随着模型可用的数据越来越多,两个模型的性能都在提升,最终线性回归的性能追上了岭回归。这里要记住的是,如果有足够多的训练数据,正则化变得不那么重要,并且岭回归和线性回归将具有相同的性能(在这个例子中,二者相同恰好发生在整个数据集的情况下,这只是一个巧合)。图 2-13 中还有一个有趣之处,就是线性回归的训练性能在下降。如果添加更多数据,模型将更加难以过拟合或记住所有的数据。

    4. lasso

    除了 Ridge,还有一种正则化的线性回归是 Lasso。与岭回归相同,使用 lasso 也是约束系数使其接近于 0,但用到的方法不同,叫作 L1 正则化。 8 L1 正则化的结果是,使用 lasso 时某些系数刚好为 0。这说明某些特征被模型完全忽略。这可以看作是一种自动化的特征选择。某些系数刚好为 0,这样模型更容易解释,也可以呈现模型最重要的特征。

    我们将 lasso 应用在扩展的波士顿房价数据集上:

    In[36]:
    from sklearn.linear_model import Lasso
    lasso = Lasso().fit(X_train, y_train)
    print("Training set score: {:.2f}".format(lasso.score(X_train, y_train)))
    print("Test set score: {:.2f}".format(lasso.score(X_test, y_test)))
    print("Number of features used: {}".format(np.sum(lasso.coef_ != 0)))
    Out[36]:
    Training set score: 0.29
    Test set score: 0.21
    Number of features used: 4
    

    如你所见, Lasso 在训练集与测试集上的表现都很差。这表示存在欠拟合,我们发现模型只用到了 105 个特征中的 4 个。与 Ridge 类似, Lasso 也有一个正则化参数 alpha,可以控制系数趋向于 0 的强度。在上一个例子中,我们用的是默认值 alpha=1.0。为了降低欠拟合,我们尝试减小 alpha。这么做的同时,我们还需要增加 max_iter 的值(运行迭代的最大次数):

    In[37]:
    # 我们增大max_iter的值,否则模型会警告我们,说应该增大max_iter
    lasso001 = Lasso(alpha=0.01, max_iter=100000).fit(X_train, y_train)
    print("Training set score: {:.2f}".format(lasso001.score(X_train, y_train)))
    print("Test set score: {:.2f}".format(lasso001.score(X_test, y_test)))
    print("Number of features used: {}".format(np.sum(lasso001.coef_ != 0)))
    Out[37]:
    Training set score: 0.90
    Test set score: 0.77
    Number of features used: 33
    

    alpha 值变小,我们可以拟合一个更复杂的模型,在训练集和测试集上的表现也更好。模型性能比使用 Ridge 时略好一点,而且我们只用到了 105 个特征中的 33 个。这样模型可能更容易理解。但如果把 alpha 设得太小,那么就会消除正则化的效果,并出现过拟合,得到与LinearRegression 类似的结果:

    In[38]:
    lasso00001 = Lasso(alpha=0.0001, max_iter=100000).fit(X_train, y_train)
    print("Training set score: {:.2f}".format(lasso00001.score(X_train, y_train)))
    print("Test set score: {:.2f}".format(lasso00001.score(X_test, y_test)))
    print("Number of features used: {}".format(np.sum(lasso00001.coef_ != 0)))
    Out[38]:
    Training set score: 0.95
    Test set score: 0.64
    Number of features used: 94
    

    再次像图 2-12 那样对不同模型的系数进行作图,见图 2-14:

    In[39]:
    plt.plot(lasso.coef_, 's', label="Lasso alpha=1")
    plt.plot(lasso001.coef_, '^', label="Lasso alpha=0.01")
    plt.plot(lasso00001.coef_, 'v', label="Lasso alpha=0.0001")
    plt.plot(ridge01.coef_, 'o', label="Ridge alpha=0.1")
    plt.legend(ncol=2, loc=(0, 1.05))
    plt.ylim(-25, 25)
    plt.xlabel("Coefficient index")
    plt.ylabel("Coefficient magnitude")
    

    图 2-14:不同 alpha 值的 lasso 回归与岭回归的系数比较
    在 alpha=1 时,我们发现不仅大部分系数都是 0(我们已经知道这一点),而且其他系数也都很小。将 alpha 减小至 0.01,我们得到图中向上的三角形,大部分特征等于 0。alpha=0.0001 时,我们得到正则化很弱的模型,大部分系数都不为 0,并且还很大。为了便于比较,图中用圆形表示 Ridge 的最佳结果。 alpha=0.1 的 Ridge 模型的预测性能与
    alpha=0.01 的 Lasso 模型类似,但 Ridge 模型的所有系数都不为 0。在实践中,在两个模型中一般首选岭回归。但如果特征很多,你认为只有其中几个是重要的,那么选择 Lasso 可能更好。同样,如果你想要一个容易解释的模型, Lasso 可以给出更容易理解的模型,因为它只选择了一部分输入特征。 scikit-learn 还提供了 ElasticNet类,结合了 Lasso 和 Ridge 的惩罚项。在实践中,这种结合的效果最好,不过代价是要调节两个参数:一个用于 L1 正则化,一个用于 L2 正则化。

    5. 用于分类的线性模型

    线性模型也广泛应用于分类问题。我们首先来看二分类。这时可以利用下面的公式进行预测:
    ŷ = w[0] * x[0] + w[1] * x[1] + …+ w[p] * x[p] + b > 0
    这个公式看起来与线性回归的公式非常相似,但我们没有返回特征的加权求和,而是为预测设置了阈值(0)。如果函数值小于 0,我们就预测类别 -1;如果函数值大于 0,我们就预测类别 +1。对于所有用于分类的线性模型,这个预测规则都是通用的。同样,有很多种不同的方法来找出系数(w)和截距(b)。
    对于用于回归的线性模型,输出 ŷ 是特征的线性函数,是直线、平面或超平面(对于更高维的数据集)。对于用于分类的线性模型, 决策边界是输入的线性函数。换句话说,(二元)线性分类器是利用直线、平面或超平面来分开两个类别的分类器。本节我们将看到这方面的例子。
    学习线性模型有很多种算法。这些算法的区别在于以下两点:
    • 系数和截距的特定组合对训练数据拟合好坏的度量方法;
    • 是否使用正则化,以及使用哪种正则化方法。
    不同的算法使用不同的方法来度量“对训练集拟合好坏”。由于数学上的技术原因,不可能调节 w 和 b 使得算法产生的误分类数量最少。对于我们的目的,以及对于许多应用而言,上面第一点(称为损失函数)的选择并不重要。
    最常见的两种线性分类算法是 Logistic 回归(logistic regression)和线性支持向量机(linear support vector machine,线性 SVM),前者在 linear_model.LogisticRegression 中实现,后者在 svm.LinearSVC(SVC 代表支持向量分类器)中实现。虽然 LogisticRegression的名字中含有回归(regression),但它是一种分类算法,并不是回归算法,不应与LinearRegression 混淆。
    我们可以将 LogisticRegression 和 LinearSVC 模型应用到 forge 数据集上,并将线性模型找到的决策边界可视化(图 2-15):

    In[40]:
    from sklearn.linear_model import LogisticRegression
    from sklearn.svm import LinearSVC
    
    X, y = mglearn.datasets.make_forge()
    
    fig, axes = plt.subplots(1, 2, figsize=(10, 3))
    
    for model, ax in zip([LinearSVC(), LogisticRegression()], axes):
        clf = model.fit(X, y)
        mglearn.plots.plot_2d_separator(clf, X, fill=False, eps=0.5,
                                        ax=ax, alpha=.7)
        mglearn.discrete_scatter(X[:, 0], X[:, 1], y, ax=ax)
        ax.set_title("{}".format(clf.__class__.__name__))
        ax.set_xlabel("Feature 0")
        ax.set_ylabel("Feature 1")
    axes[0].legend()
    
    图 2-15:线性 SVM 和 Logistic 回归在 forge 数据集上的决策边界(均为默认参数)

    在这张图中, forge 数据集的第一个特征位于 x 轴,第二个特征位于 y 轴,与前面相同。图中分别展示了 LinearSVC 和 LogisticRegression 得到的决策边界,都是直线,将顶部归为类别 1 的区域和底部归为类别 0 的区域分开了。换句话说,对于每个分类器而言,位于黑线上方的新数据点都会被划为类别 1,而在黑线下方的点都会被划为类别 0。两个模型得到了相似的决策边界。注意,两个模型中都有两个点的分类是错误的。两个模型都默认使用 L2 正则化,就像 Ridge 对回归所做的那样。对于 LogisticRegression 和 LinearSVC,决定正则化强度的权衡参数叫作 C。 C 值越大,对应的正则化越弱。换句话说,如果参数 C 值较大,那么 LogisticRegression 和LinearSVC 将尽可能将训练集拟合到最好,而如果 C 值较小,那么模型更强调使系数向量(w)接近于 0。
    参数 C 的作用还有另一个有趣之处。较小的 C 值可以让算法尽量适应“大多数”数据点,而较大的 C 值更强调每个数据点都分类正确的重要性。下面是使用 LinearSVC 的图示
    (图 2-16):

    In[41]:
    mglearn.plots.plot_linear_svc_regularization()
    
    图 2-16:不同 C 值的线性 SVM 在 forge 数据集上的决策边界

    在左侧的图中, C 值很小,对应强正则化。大部分属于类别 0 的点都位于底部,大部分属于类别 1 的点都位于顶部。强正则化的模型会选择一条相对水平的线,有两个点分类错误。在中间的图中, C 值稍大,模型更关注两个分类错误的样本,使决策边界的斜率变大。最后,在右侧的图中,模型的 C 值非常大,使得决策边界的斜率也很大,现在模型对类别 0 中所有点的分类都是正确的。类别 1 中仍有一个点分类错误,这是因为对这个数据集来说,不可能用一条直线将所有点都分类正确。右侧图中的模型尽量使所有点的分类都正
    确,但可能无法掌握类别的整体分布。换句话说,这个模型很可能过拟合。
    与回归的情况类似,用于分类的线性模型在低维空间中看起来可能非常受限,决策边界只能是直线或平面。同样,在高维空间中,用于分类的线性模型变得非常强大,当考虑更多特征时,避免过拟合变得越来越重要。
    我们在乳腺癌数据集上详细分析 LogisticRegression:

    In[42]:
    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=42)
    logreg = LogisticRegression().fit(X_train, y_train)
    print("Training set score: {:.3f}".format(logreg.score(X_train, y_train)))
    print("Test set score: {:.3f}".format(logreg.score(X_test, y_test)))
    Out[42]:
    Training set score: 0.953
    Test set score: 0.958
    

    C=1 的默认值给出了相当好的性能,在训练集和测试集上都达到 95% 的精度。但由于训练集和测试集的性能非常接近,所以模型很可能是欠拟合的。我们尝试增大 C 来拟合一个更灵活的模型:

    In[43]:
    logreg100 = LogisticRegression(C=100).fit(X_train, y_train)
    print("Training set score: {:.3f}".format(logreg100.score(X_train, y_train)))
    print("Test set score: {:.3f}".format(logreg100.score(X_test, y_test)))
    Out[43]:
    Training set score: 0.972
    Test set score: 0.965
    

    使用 C=100 可以得到更高的训练集精度,也得到了稍高的测试集精度,这也证实了我们的直觉,即更复杂的模型应该性能更好。
    我们还可以研究使用正则化更强的模型时会发生什么。设置 C=0.01:

    
    In[44]:
    logreg001 = LogisticRegression(C=0.01).fit(X_train, y_train)
    print("Training set score: {:.3f}".format(logreg001.score(X_train, y_train)))
    print("Test set score: {:.3f}".format(logreg001.score(X_test, y_test)))
    Out[44]:
    Training set score: 0.934
    Test set score: 0.930
    

    正如我们所料,在图 2-1 中将已经欠拟合的模型继续向左移动,训练集和测试集的精度都比采用默认参数时更小。
    最后,来看一下正则化参数 C 取三个不同的值时模型学到的系数(图 2-17):

    In[45]:
    plt.plot(logreg.coef_.T, 'o', label="C=1")
    plt.plot(logreg100.coef_.T, '^', label="C=100")
    plt.plot(logreg001.coef_.T, 'v', label="C=0.001")
    plt.xticks(range(cancer.data.shape[1]), cancer.feature_names, rotation=90)
    plt.hlines(0, 0, cancer.data.shape[1])
    plt.ylim(-5, 5)
    plt.xlabel("Coefficient index")
    plt.ylabel("Coefficient magnitude")
    plt.legend()
    
    图 2-17:不同 C 值的 Logistic 回归在乳腺癌数据集上学到的系数

    由于 LogisticRegression 默认应用 L2 正则化,所以其结果与图 2-12 中Ridge 的结果类似。更强的正则化使得系数更趋向于 0,但系数永远不会正好等于 0。进一步观察图像,还可以在第 3 个系数那里发现有趣之处,这个系数是“平均周长”(mean perimeter)。 C=100 和 C=1 时,这个系数为负,而C=0.001 时这个系数为正,其绝对值比 C=1 时还要大。在解释这样的模型时,人们可能会认为,系数可以告诉我们某个特征与哪个类别有关。例如,人们可能会认为高“纹理错误”(texture error)特征与“恶性”样本有关。但“平均周长”系数的正负号发生变化,说明较大的“平均周长”可以被当作“良性”的指标或“恶性”的指标,具体取决于我们考虑的是哪个模型。这也说明,对线性模型系数的解释应该始终持保留态度。
    如果想要一个可解释性更强的模型,使用 L1 正则化可能更好,因为它约束模型只使用少数几个特征。下面是使用 L1 正则化的系数图像和分类精度(图 2-18)。


    图 2-18:对于不同的 C 值, L1 惩罚的 Logistic 回归在乳腺癌数据集上学到的系数
    In[46]:
    for C, marker in zip([0.001, 1, 100], ['o', '^', 'v']):
        lr_l1 = LogisticRegression(C=C, penalty="l1").fit(X_train, y_train)
        print("Training accuracy of l1 logreg with C={:.3f}: {:.2f}".format(
              C, lr_l1.score(X_train, y_train)))
        print("Test accuracy of l1 logreg with C={:.3f}: {:.2f}".format(
              C, lr_l1.score(X_test, y_test)))
        plt.plot(lr_l1.coef_.T, marker, label="C={:.3f}".format(C))
    
    plt.xticks(range(cancer.data.shape[1]), cancer.feature_names, rotation=90)
    xlims = plt.xlim()
    plt.hlines(0, xlims[0], xlims[1])
    plt.xlim(xlims)
    plt.xlabel("Feature")
    plt.ylabel("Coefficient magnitude")
    
    plt.ylim(-5, 5)
    plt.legend(loc=3)
    Out[46]:
    Training accuracy of l1 logreg with C=0.001: 0.91
    Test accuracy of l1 logreg with C=0.001: 0.92
    Training accuracy of l1 logreg with C=1.000: 0.96
    Test accuracy of l1 logreg with C=1.000: 0.96
    Training accuracy of l1 logreg with C=100.000: 0.99
    Test accuracy of l1 logreg with C=100.000: 0.98
    

    如你所见,用于二分类的线性模型与用于回归的线性模型有许多相似之处。与用于回归的线性模型一样,模型的主要差别在于 penalty 参数,这个参数会影响正则化,也会影响模型是使用所有可用特征还是只选择特征的一个子集。

    6. 用于多分类的线性模型

    许多线性分类模型只适用于二分类问题,不能轻易推广到多类别问题(除了 Logistic 回归)。将二分类算法推广到多分类算法的一种常见方法是“一对其余”(one-vs.-rest)方法。在“一对其余”方法中,对每个类别都学习一个二分类模型,将这个类别与所有其他类别尽量分开,这样就生成了与类别个数一样多的二分类模型。在测试点上运行所有二类分类器来进行预测。在对应类别上分数最高的分类器“胜出”,将这个类别标签返回作为预测结果。
    每个类别都对应一个二类分类器,这样每个类别也都有一个系数(w)向量和一个截距(b)。下面给出的是分类置信方程,其结果中最大值对应的类别即为预测的类别标签:
    w[0] * x[0] + w[1] * x[1] + … + w[p] * x[p] + b
    多分类 Logistic 回归背后的数学与“一对其余”方法稍有不同,但它也是对每个类别都有一个系数向量和一个截距,也使用了相同的预测方法。
    我们将“一对其余”方法应用在一个简单的三分类数据集上。我们用到了一个二维数据集,每个类别的数据都是从一个高斯分布中采样得出的(见图 2-19):

    In[47]:
    from sklearn.datasets import make_blobs
    X, y = make_blobs(random_state=42)
    mglearn.discrete_scatter(X[:, 0], X[:, 1], y)
    plt.xlabel("Feature 0")
    plt.ylabel("Feature 1")
    plt.legend(["Class 0", "Class 1", "Class 2"])
    
    图 2-19:包含 3 个类别的二维玩具数据集

    现在,在这个数据集上训练一个 LinearSVC 分类器:

    In[48]:
    linear_svm = LinearSVC().fit(X, y)
    print("Coefficient shape: ", linear_svm.coef_.shape)
    print("Intercept shape: ", linear_svm.intercept_.shape)
    Out[48]:
    Coefficient shape: (3, 2)
    Intercept shape: (3,)
    

    我们看到, coef_ 的形状是 (3, 2),说明 coef_ 每行包含三个类别之一的系数向量,每列包含某个特征(这个数据集有 2 个特征)对应的系数值。现在 intercept_ 是一维数组,保存每个类别的截距。
    我们将这 3 个二类分类器给出的直线可视化(图 2-20):

    In[49]:
    mglearn.discrete_scatter(X[:, 0], X[:, 1], y)
    line = np.linspace(-15, 15)
    for coef, intercept, color in zip(linear_svm.coef_, linear_svm.intercept_,
                                      mglearn.cm3.colors):
        plt.plot(line, -(line * coef[0] + intercept) / coef[1], c=color)
    plt.ylim(-10, 15)
    plt.xlim(-10, 8)
    plt.xlabel("Feature 0")
    plt.ylabel("Feature 1")
    plt.legend(['Class 0', 'Class 1', 'Class 2', 'Line class 0', 'Line class 1',
                'Line class 2'], loc=(1.01, 0.3))
    
    图 2-20:三个“一对其余”分类器学到的决策边界

    你可以看到,训练集中所有属于类别 0 的点都在与类别 0 对应的直线上方,这说明它们位于这个二类分类器属于“类别 0”的那一侧。属于类别 0 的点位于与类别 2 对应的直线上方,这说明它们被类别 2 的二类分类器划为“其余”。属于类别 0 的点位于与类别 1 对应的直线左侧,这说明类别 1 的二元分类器将它们划为“其余”。因此,这一区域的所有点都会被最终分类器划为类别 0(类别 0 的分类器的分类置信方程的结果大于 0,其他两个类别对应的结果都小于 0)。
    但图像中间的三角形区域属于哪一个类别呢, 3 个二类分类器都将这一区域内的点划为“其余”。这里的点应该划归到哪一个类别呢?答案是分类方程结果最大的那个类别,即最接近的那条线对应的类别。
    下面的例子(图 2-21)给出了二维空间中所有区域的预测结果:

    In[50]:
    mglearn.plots.plot_2d_classification(linear_svm, X, fill=True, alpha=.7)
    mglearn.discrete_scatter(X[:, 0], X[:, 1], y)
    line = np.linspace(-15, 15)
    for coef, intercept, color in zip(linear_svm.coef_, linear_svm.intercept_,
                                      mglearn.cm3.colors):
        plt.plot(line, -(line * coef[0] + intercept) / coef[1], c=color)
    plt.legend(['Class 0', 'Class 1', 'Class 2', 'Line class 0', 'Line class 1',
                'Line class 2'], loc=(1.01, 0.3))
    plt.xlabel("Feature 0")
    plt.ylabel("Feature 1")
    
    图 2-21:三个“一对其余”分类器得到的多分类决策边界

    7. 优点、 缺点和参数

    线性模型的主要参数是正则化参数,在回归模型中叫作 alpha,在 LinearSVC 和 LogisticRegression 中叫作 C。 alpha 值较大或 C 值较小,说明模型比较简单。特别是对于回归模型而言,调节这些参数非常重要。通常在对数尺度上对 C 和 alpha 进行搜索。你还需要确定的是用 L1 正则化还是 L2 正则化。如果你假定只有几个特征是真正重要的,那么你应该用L1 正则化,否则应默认使用 L2 正则化。如果模型的可解释性很重要的话,使用 L1 也会有帮助。由于 L1 只用到几个特征,所以更容易解释哪些特征对模型是重要的,以及这些特征的作用。
    线性模型的训练速度非常快,预测速度也很快。这种模型可以推广到非常大的数据集,对稀疏数据也很有效。如果你的数据包含数十万甚至上百万个样本,你可能需要研究如何使用 LogisticRegression 和 Ridge 模型的 solver='sag' 选项,在处理大型数据时,这一选项比默认值要更快。其他选项还有 SGDClassifier 类和 SGDRegressor 类,它们对本节介绍的线性模型实现了可扩展性更强的版本。
    线性模型的另一个优点在于,利用我们之间见过的用于回归和分类的公式,理解如何进行预测是相对比较容易的。不幸的是,往往并不完全清楚系数为什么是这样的。如果你的数据集中包含高度相关的特征,这一问题尤为突出。在这种情况下,可能很难对系数做出解释。
    如果特征数量大于样本数量,线性模型的表现通常都很好。它也常用于非常大的数据集,只是因为训练其他模型并不可行。但在更低维的空间中,其他模型的泛化性能可能更好。2.3.7 节会介绍几个线性模型不适用的例子。

    2.3.4 朴素贝叶斯分类器

    朴素贝叶斯分类器是与上一节介绍的线性模型非常相似的一种分类器,但它的训练速度往往更快。这种高效率所付出的代价是,朴素贝叶斯模型的泛化能力要比线性分类器(如LogisticRegression 和 LinearSVC)稍差。
    朴素贝叶斯模型如此高效的原因在于,它通过单独查看每个特征来学习参数,并从每个特征中收集简单的类别统计数据。 scikit-learn 中实现了三种朴素贝叶斯分类器:GaussianNB、 BernoulliNB 和 MultinomialNB。 GaussianNB 可 应 用 于 任 意 连 续 数 据, 而BernoulliNB 假定输入数据为二分类数据, MultinomialNB 假定输入数据为计数数据(即每个特征代表某个对象的整数计数,比如一个单词在句子里出现的次数)。 BernoulliNB 和MultinomialNB 主要用于文本数据分类。
    BernoulliNB 分类器计算每个类别中每个特征不为 0 的元素个数。用一个例子来说明会很容易理解:

    In[54]:
    X = np.array([[0, 1, 0, 1],
                  [1, 0, 1, 1],
                  [0, 0, 0, 1],
                  [1, 0, 1, 0]])
    y = np.array([0, 1, 0, 1])
    

    这里我们有 4 个数据点,每个点有 4 个二分类特征。一共有两个类别: 0 和 1。对于类别 0(第 1、 3 个数据点),第一个特征有 2 个为零、 0 个不为零,第二个特征有 1 个为零、 1 个不为零,以此类推。然后对类别 1 中的数据点计算相同的计数。计算每个类别中的非零元素个数,大体上看起来像这样:

    In[55]:
    counts = {}
    for label in np.unique(y):
        # 对每个类别进行遍历
        # 计算(求和)每个特征中1的个数
        counts[label] = X[y == label].sum(axis=0)
    print("Feature counts:\n{}".format(counts))
    Out[55]:
    Feature counts:
    {0: array([0, 1, 0, 2]), 1: array([2, 0, 2, 1])}
    

    另外两种朴素贝叶斯模型(MultinomialNB 和 GaussianNB)计算的统计数据类型略有不同。MultinomialNB 计算每个类别中每个特征的平均值,而 GaussianNB 会保存每个类别中每个特征的平均值和标准差。
    要想做出预测,需要将数据点与每个类别的统计数据进行比较,并将最匹配的类别作为预测结果。有趣的是, MultinomialNB 和 BernoulliNB 预测公式的形式都与线性模型完全相同(见 2.3.3 节)。不幸的是,朴素贝叶斯模型 coef_ 的含义与线性模型稍有不同,因为 coef_不同于 w。
    优点、 缺点和参数
    MultinomialNB 和 BernoulliNB 都只有一个参数 alpha,用于控制模型复杂度。 alpha 的工作原理是,算法向数据中添加 alpha 这么多的虚拟数据点,这些点对所有特征都取正值。这可以将统计数据“平滑化”(smoothing)。 alpha 越大,平滑化越强,模型复杂度就越低。
    算法性能对 alpha 值的鲁棒性相对较好,也就是说, alpha 值对模型性能并不重要。但调整这个参数通常都会使精度略有提高。
    GaussianNB 主要用于高维数据,而另外两种朴素贝叶斯模型则广泛用于稀疏计数数据,比如文本。 MultinomialNB 的性能通常要优于 BernoulliNB,特别是在包含很多非零特征的数据集(即大型文档)上。
    朴素贝叶斯模型的许多优点和缺点都与线性模型相同。它的训练和预测速度都很快,训练过程也很容易理解。该模型对高维稀疏数据的效果很好,对参数的鲁棒性也相对较好。朴素贝叶斯模型是很好的基准模型,常用于非常大的数据集,在这些数据集上即使训练线性模型可能也要花费大量时间。

    相关文章

      网友评论

          本文标题:第 2 章 监督学习(上)

          本文链接:https://www.haomeiwen.com/subject/tnnssktx.html