MIT-6-0001-Python-计算和编程入门第三版-四-

MIT 6.0001:Python 计算和编程入门第三版(四)

原文:zh.z-lib.gs/md5/b81f9400901fb07c6e4e456605c4cd1f

译者:飞龙

协议:CC BY-NC-SA 4.0

第二十四章:机器学习速览

世界上的数字数据量以难以人类理解的速度增长。自 1980 年代以来,世界的数据存储能力每三年大约翻一番。在你阅读本章所需的时间里,世界上的数据存储将增加约10¹⁸位。这么大的数字不容易理解。可以这样想:10¹⁸个加元便士的表面积大约是地球的两倍。

当然,更多的数据并不总是意味着更有用的信息。进化是一个缓慢的过程,人类心智同化数据的能力并不会每三年翻倍。世界正在使用的一种方法,以期从“大数据”中提取更多有用信息,就是统计机器学习

机器学习很难定义。从某种意义上说,每个有用的程序都会学习一些东西。例如,牛顿法的实现学习一个多项式的根。美国电气工程师和计算机科学家阿瑟·萨缪尔提出的最早定义之一是,它是一个“使计算机能够在没有明确编程的情况下学习的研究领域”。

人类通过两种方式学习——记忆和概括。我们利用记忆来积累个别事实。例如,在英国,小学生可能会学习一份英王列表。人类使用概括从旧事实推导出新事实。例如,政治学学生可能会观察大量政治家的行为,并从这些观察中推导出所有政治家在竞选时都撒谎。

当计算机科学家谈论机器学习时,他们最常指的是编写程序的学科,这些程序自动学习从数据中的隐含模式中得出有用的推论。例如,线性回归(见第二十章)学习一条曲线,该曲线是一组示例的模型。然后可以使用该模型对以前未见过的示例进行预测。基本范式是

  1. 1. 观察一组示例,通常称为训练数据,它表示有关某些统计现象的不完整信息。

  2. 2. 使用推理技术创建一个可能生成所观察示例的过程模型。

  3. 3. 使用该模型对以前未见过的示例进行预测。

假设,例如,你获得了图 24-1 中的两个名称集合和图 24-2 中的特征向量

c24-fig-0001.jpg

图 24-1 两个名称集合

c24-fig-0002.jpg

图 24-2 将特征向量与每个名称关联

向量的每个元素对应于某个方面(即特征)。基于对这些历史人物的有限信息,你可能推断出将标签A或标签B分配给每个示例的过程是为了将高个子总统与矮个子总统区分开来。

机器学习有许多方法,但所有方法都试图学习一个提供示例的概括模型。所有方法都有三个组成部分:

  • 模型的一个表示

  • 用于评估模型优劣的目标函数

  • 一种优化方法,用于学习最小化或最大化目标函数值的模型

广义来说,机器学习算法可以分为有监督和无监督两种。

有监督学习中,我们从一组特征向量/值对开始。目标是从这些对中推导出一个规则,以预测与先前未见的特征向量相关联的值。回归模型将一个实数与每个特征向量关联。分类模型将有限数量的标签中的一个与每个特征向量关联。¹⁸²

在第二十章中,我们看了一种回归模型,即线性回归。每个特征向量是一个 x 坐标,与之相关的值是对应的 y 坐标。通过特征向量/值对的集合,我们学习了一个模型,可以用来预测与任何 x 坐标相关联的 y 坐标。

现在,让我们来看一个简单的分类模型。给定我们在图 24-1 中标记的总统集合AB以及图 24-2 中的特征向量,我们可以生成图 24-3 中的特征向量/标签对。

c24-fig-0003.jpg

图 24-3 总统的特征向量/标签对

从这些标记的示例中,学习算法可能推断出所有高个子总统应该标记为A,所有矮个子总统标记为B。当被要求为

[American, President, 189 cm.]¹⁸³

它会使用它所学到的规则来选择标签A

有监督机器学习广泛应用于诸如检测信用卡欺诈使用和向人们推荐电影等任务。

无监督学习中,我们获得一组特征向量,但没有标签。无监督学习的目标是揭示特征向量集合中的潜在结构。例如,给定总统特征向量的集合,无监督学习算法可能会将总统分为高个子和矮个子,或者可能分为美国人和法国人。无监督机器学习的方法可以分为聚类方法或学习潜变量模型的方法。

潜变量是一个其值不是直接观察到的变量,但可以从观察到的变量值中推断出来。例如,大学的招生官员试图根据一组可观察的值(如中学成绩和标准化测试成绩)推断申请者成为成功学生的概率(即潜变量)。有许多丰富的方法用于学习潜变量模型,但我们在本书中不讨论这些。

聚类将一组实例划分为多个组(称为簇),使得同一组中的实例彼此之间更相似,而与其他组中的实例则相对不相似。例如,遗传学家使用聚类来寻找相关基因的群体。许多流行的聚类方法出人意料地简单。

我们在第二十五章介绍了一种广泛使用的聚类算法,并在第二十六章讨论了几种监督学习的方法。在本章的其余部分,我们讨论了构建特征向量的过程以及计算两个特征向量之间相似性的不同方法。

24.1 特征向量

信噪比SNR)的概念在许多工程和科学领域中使用。确切的定义在不同应用中有所不同,但基本思想很简单。将其视为有用输入与无关输入的比率。在餐厅里,信号可能是你约会对象的声音,而噪声则是其他 diners 的声音。如果我们试图预测哪些学生在编程课程中表现良好,之前的编程经验和数学能力将是信号的一部分,而发色则仅仅是噪声。将信号与噪声分离并不总是容易。当做得不够好时,噪声可能会干扰,掩盖信号中的真相。

特征工程的目的是将可用数据中对信号有贡献的特征与仅仅是噪声的特征分开。如果未能做好这项工作,可能导致模型表现不佳。当数据的维度(即不同特征的数量)相对于样本数量较大时,风险特别高。

成功的特征工程能够减少可用信息的庞大数量,使其成为有助于推广的有用信息。例如,想象一下,你的目标是学习一个模型,以预测一个人是否可能会发生心脏病发作。一些特征,比如他们的年龄,可能非常相关。其他特征,比如他们是否是左撇子,可能不太相关。

特征选择技术可以自动识别在给定特征集合中哪些特征最有可能是有帮助的。例如,在监督学习的背景下,我们可以选择与样本标签最强相关的特征。¹⁸⁵然而,如果没有相关特征,这些特征选择技术帮助不大。假设我们心脏病发作示例的原始特征集合包括身高和体重。尽管身高和体重都不是心脏病发作的强预测因素,但身体质量指数(BMI)可能是。虽然 BMI 可以通过身高和体重计算得出,但其关系(体重以千克为单位除以身高以米为单位的平方)过于复杂,无法通过典型的机器学习技术自动发现。成功的机器学习通常涉及领域专家设计特征。

在无监督学习中,问题甚至更困难。通常,我们根据对哪些特征可能与我们希望发现的结构相关的直觉来选择特征。然而,依赖于对特征潜在相关性的直觉是有问题的。你对某人的牙齿历史是否是未来心脏病发作的有用预测因素的直觉有多好?

考虑图 24-4,其中包含特征向量的表格和每个向量相关的标签(爬行动物或非爬行动物)。

c24-fig-0004.jpg

图 24-4 各种动物的名称、特征和标签

仅仅通过有关眼镜蛇的信息(即,仅表格的第一行),一个监督机器学习算法(或人类)几乎只能记住眼镜蛇是爬行动物这一事实。现在,让我们添加有关响尾蛇的信息。我们可以开始进行概括,并可能推断出一个规则:如果动物下蛋、有鳞、是毒性、是冷血动物,并且没有腿,则它是爬行动物。

现在,假设我们被要求决定一条巨蟒是否是爬行动物。我们可能会回答“不是”,因为巨蟒既不是毒性动物,也不是下蛋的。然而,这将是错误的答案。当然,试图从两个示例中进行概括而导致错误也并不令人惊讶。一旦我们将巨蟒纳入我们的训练数据,我们可能会形成新的规则:如果动物有鳞、是冷血动物,并且没有腿,则它是爬行动物。在这样做的过程中,我们将特征下蛋毒性视为与分类问题无关而丢弃。

如果我们用新规则来分类鳄鱼,我们错误地得出结论,认为因为它有腿,所以不是爬行动物。一旦我们将鳄鱼纳入训练数据,我们就重新制定规则,允许爬行动物没有腿或有四条腿。当我们观察飞镖蛙时,我们正确得出结论,它不是爬行动物,因为它不是冷血的。然而,当我们用目前的规则来分类鲑鱼时,我们错误地得出结论,认为鲑鱼是爬行动物。我们可以为我们的规则增加更多复杂性,以将鲑鱼与鳄鱼区分开来,但这是徒劳的。没有办法修改我们的规则,使其能够正确分类鲑鱼和蟒蛇,因为这两种物种的特征向量是相同的。

这种问题在机器学习中比比皆是。特征向量中包含足够信息以完美分类的情况非常罕见。在这种情况下,问题在于我们没有足够的特征。

如果我们考虑到爬行动物的卵有羊膜,我们可以制定出一个将爬行动物与鱼类分开的规则。不幸的是,在大多数机器学习的实际应用中,构造允许完美区分的特征向量是不可能的。

这是否意味着我们应该放弃,因为所有可用的特征只是噪声?不。在这种情况下,特征鳞片冷血是成为爬行动物的必要条件,但不是充分条件。如果动物有鳞片且是冷血的,就不可能产生假阴性,即,任何被分类为非爬行动物的动物确实不会是爬行动物。然而,这条规则会产生一些假阳性,即,一些被分类为爬行动物的动物实际上并不是爬行动物。

24.2 距离度量

在图 24-4 中,我们使用四个二元特征和一个整数特征描述了动物。假设我们想用这些特征来评估两种动物的相似性,例如,询问响尾蛇与蚺蛇或飞镖蛙更相似。

进行这种比较的第一步是将每种动物的特征转换为数字序列。如果我们说True = 1False = 0,我们会得到以下特征向量:

Rattlesnake: [1,1,1,1,0]
Boa constrictor: [0,1,0,1,0]
Dart frog: [1,0,1,0,4]

比较数字向量相似性的方法有很多。比较相同长度向量时最常用的度量基于闵可夫斯基距离

c24-fig-5001.jpg

其中len是向量的长度。

参数p,至少为 1,定义了在遍历向量VW之间的距离时可以遵循的路径类型。这在向量长度为二时可以很容易地可视化,因此可以使用笛卡尔坐标表示。请考虑图 24-5 中的图像。

c24-fig-0005.jpg

图 24-5 可视化距离度量

左下角的圆形更接近十字还是更接近星形?这要看情况。如果我们可以直线行走,十字更近。毕达哥拉斯定理告诉我们,十字距离圆形8单位的平方根,大约是2.8单位,而星形距离圆形3单位。这些距离被称为欧几里得距离,对应于使用闵可夫斯基距离时p = 2。但想象一下,图中的线条对应于街道,我们必须沿街道从一个地方到另一个地方。星形仍然距离圆形3单位,但十字现在距离4单位。这些距离被称为曼哈顿 距离,¹⁹⁰,它们对应于使用闵可夫斯基距离时p = 1。图 24-6 包含一个实现闵可夫斯基距离的函数。

c24-fig-0006.jpg

图 24-6 闵可夫斯基距离

图 24-7 包含类Animal。它将两只动物之间的距离定义为与动物相关的特征向量之间的欧几里得距离。

c24-fig-0007.jpg

图 24-7 类Animal

图 24-8 包含一个比较动物列表的函数,并生成一张显示成对距离的表格。代码使用了我们之前未使用的 Matplotlib 绘图工具:table

table函数生成一个(惊喜!)看起来像表格的图。关键字参数rowLabelscolLabels用于提供行和列的标签(在本例中是动物的名称)。关键字参数cellText用于提供表格单元格中出现的值。在该示例中,cellText绑定到table_vals,这是一个字符串列表的列表。table_vals中的每个元素都是表格一行中单元格值的列表。关键字参数cellLoc用于指定文本在每个单元格中的位置,关键字参数loc用于指定表格本身在图中的位置。示例中使用的最后一个关键字参数是colWidths。它绑定到一个浮点数列表,给出表格中每一列的宽度(以英寸为单位)。代码table.scale(1, 2.5)指示 Matplotlib 保持单元格的水平宽度不变,但将单元格的高度增加2.5倍(使表格看起来更美观)。

c24-fig-0008.jpg

图 24-8 建立动物对之间的距离表

如果我们运行代码

rattlesnake = Animal('rattlesnake', [1,1,1,1,0])
boa = Animal('boa', [0,1,0,1,0])
dart_frog = Animal('dart frog', [1,0,1,0,4])
animals = [rattlesnake, boa, dart_frog]
compare_animals(animals, 3)

它生成了图 24-9 中的表格。

正如你可能预期的,响尾蛇与蟒蛇之间的距离小于任何一条蛇与飞蛙之间的距离。顺便提一下,飞蛙离响尾蛇稍微近一点,而不是蟒蛇。

c24-fig-0009.jpg

图 24-9 三种动物之间的距离

现在,让我们在上述代码的最后一行之前插入以下行

alligator = Animal('alligator', [1,1,0,1,4])
animals.append(alligator)

它生成了图 24-10 中的表格。

c24-fig-0010.jpg

图 24-10 四种动物之间的距离

也许你会惊讶于鳄鱼与飞蛙之间的距离明显小于与响尾蛇或蟒蛇之间的距离。花点时间思考一下为什么。

鳄鱼的特征向量与响尾蛇的特征向量在两个地方不同:是否有毒和腿的数量。鳄鱼的特征向量与飞蛙的特征向量在三个地方不同:是否有毒、是否有鳞片,以及是否是冷血动物。然而,根据我们的欧几里得距离度量,鳄鱼与飞蛙更相似,而非响尾蛇。这是怎么回事?

问题的根源在于不同特征具有不同的值范围。除了一个特征,其他特征的范围均在01之间,但腿的数量则在04之间。这意味着当我们计算欧几里得距离时,腿的数量权重不成比例。让我们看看如果将特征变为二元特征,若动物无腿则值为0,否则为1会发生什么。

c24-fig-0011.jpg

图 24-11 使用不同特征表示的距离

这看起来更合理了。

当然,仅使用二元特征并不总是方便。在第 25.4 节中,我们将介绍一种更通用的方法来处理特征之间的规模差异。

24.3 本章介绍的术语

  • 统计机器学习

  • 泛化

  • 训练数据

  • 特征向量

  • 监督学习

  • 回归模型

  • 分类模型

  • 标签

  • 无监督学习

  • 潜在变量

  • 聚类

  • 信噪比(SNR)

  • 特征工程

  • 数据的维度

  • 特征选择

  • 闵可夫斯基距离

  • 三角不等式

  • 欧几里得距离

  • 曼哈顿距离

第二十五章:聚类

无监督学习涉及在无标签数据中寻找隐藏结构。最常用的无监督机器学习技术是聚类。

聚类可以定义为将对象组织成某种方式相似的组的过程。一个关键问题是定义“相似”的含义。考虑图 25-1 中的图示,显示了 13 个人的身高、体重和衬衫颜色。

c25-fig-0001.jpg

图 25-1 身高、体重和衬衫颜色

如果我们按身高对人进行聚类,会出现两个明显的簇——由虚线水平线划分。如果我们按体重对人进行聚类,会出现两个不同的明显簇——由实线垂直线划分。如果我们根据他们的衬衫进行聚类,还有第三种聚类——由倾斜的虚线划分。顺便提一下,最后这种划分不是线性的,因为我们无法用一条直线根据衬衫颜色将人们分开。

聚类是一个优化问题。目标是找到一组簇,以优化目标函数,同时遵循一组约束条件。给定一个可以用来决定两个示例之间接近程度的距离度量,我们需要定义一个目标函数,以最小化簇内示例之间的不相似度。

我们称之为变异性(在文献中通常称为惯性)的一个度量,表示单个簇内的示例c之间的差异性是

c25-fig-5001.jpg

其中mean(c)是簇内所有示例特征向量的均值。一个向量集的均值是逐组件计算的。对应元素相加,然后结果除以向量的数量。如果v1v2是数字的arrays,表达式(v1+v2)/2的值是它们的欧几里得均值

我们所称的变异性类似于第十七章中提出的方差的概念。不同之处在于,变异性没有通过簇的大小进行归一化,因此根据该度量,具有更多点的簇看起来可能不那么凝聚。如果我们想比较两个不同大小簇的凝聚性,就需要将每个簇的变异性除以簇的大小。

单个簇c内的变异性的定义可以扩展为一组簇C的不相似度度量:

c25-fig-5002.jpg

请注意,由于我们不将变异性除以簇的大小,因此一个大的不凝聚簇会比一个小的不凝聚簇更大地增加dissimilarity(C)的值。这是有意设计的。

那么,优化问题是否是找到一组簇 C,使得 dissimilarity(C) 被最小化?不完全是。通过将每个示例放在其自己的簇中,可以很容易地将其最小化。我们需要添加一些约束。例如,我们可以对簇之间的最小距离施加约束,或者要求最大簇数为某个常数 k

一般来说,解决这个优化问题在大多数有趣的问题上都是计算上不可行的。因此,人们依赖于提供近似解的贪心算法。在第 25.2 节中,我们介绍一种这样的算法,即 k 均值聚类。但首先我们将介绍一些实现该算法(以及其他聚类算法)时有用的抽象。

25.1 类 Cluster

Example (图 25-2) 将用于构建要聚类的样本。与每个示例相关联的是一个名称、一个特征向量和一个可选标签。distance 方法返回两个示例之间的欧几里得距离。

c25-fig-0002.jpg

图 25-2 类 Example

Cluster (图 25-3) 稍微复杂一些。一个簇是一组示例。Cluster 中两个有趣的方法是 compute_centroidvariability。将簇的 质心 视为其质心。方法 compute_centroid 返回一个示例,其特征向量等于簇中示例特征向量的欧几里得均值。方法 variability 提供了簇的连贯性度量。

c25-fig-0003.jpg

图 25-3 类 Cluster

动手练习:一个簇的质心是否总是该簇中的一个示例?

25.2 K 均值聚类

K 均值聚类可能是最广泛使用的聚类方法。¹⁹¹ 它的目标是将一组示例划分为 k 个簇,使得

  • 每个示例位于其质心最近的簇中。

  • 簇集的相异性被最小化。

不幸的是,在大数据集上找到该问题的最优解在计算上是不可行的。幸运的是,有一种高效的贪心算法¹⁹² 可用于找到有用的近似解。它由伪代码描述。

randomly choose k examples as initial centroids of clusters
while true:
1\. Create k clusters by assigning each example to closest centroid
2\. Compute k new centroids by averaging the examples in each cluster
3\. If none of the centroids differ from the previous iteration:
           return the current set of clusters

步骤 1 的复杂度为 θ(k*n*d),其中 k 是聚类的数量,n 是样本的数量,d 是计算一对样本之间距离所需的时间。步骤 2 的复杂度为 θ(n),步骤 3 的复杂度为 θ(k)。因此,单次迭代的复杂度为 θ(k*n*d)。如果使用闵可夫斯基距离比较样本,d 与特征向量的长度呈线性关系。¹⁹³ 当然,整个算法的复杂度取决于迭代的次数。这一点不容易表征,但可以说通常是较小的。

图 25-4 包含描述 k-means 的伪代码的 Python 翻译。唯一的特殊之处在于,如果任何迭代创建了一个没有成员的聚类,则会抛出异常。生成空聚类是很少见的。它不会出现在第一次迭代中,但可能在后续迭代中出现。这通常是由于选择的 k 过大或初始质心选择不幸。将空聚类视为错误是 MATLAB 使用的选项之一。另一个选项是创建一个只包含一个点的新聚类——该点是其他聚类中距离质心最远的点。我们选择将其视为错误,以简化实现。

k-means 算法的一个问题是返回的值依赖于初始随机选择的质心。如果选择了一组特别不幸的初始质心,算法可能会陷入一个远离全局最优解的局部最优解。在实践中,通常通过多次运行 k-means 来解决这个问题,初始质心随机选择。然后,我们选择具有最小聚类相异度的解决方案。

图 25-5 包含一个函数 try_k_means,它多次调用 k_means (图 25-4) 并选择相异度最低的结果。如果试验失败,因为 k_means 生成了一个空聚类并因此抛出了异常,try_k_means 仅仅是重新尝试——假设最终 k_means 会选择一个成功收敛的初始质心集合。

c25-fig-0004.jpg

图 25-4 K-means 聚类

c25-fig-0005.jpg

图 25-5 寻找最佳的 k-means 聚类

25.3 一个人为的例子

图 25-6 包含生成、绘制和聚类来自两个分布的样本的代码。

函数 gen_distributions 生成一个包含 n 个样本的列表,这些样本具有二维特征向量。这些特征向量元素的值来自正态分布。

函数plot_samples绘制一组示例的特征向量。它使用plt.annotate在绘图中的点旁边放置文本。第一个参数是文本,第二个参数是与文本相关联的点,第三个参数是文本相对于与其相关联的点的位置。

函数contrived_test使用gen_distributions创建两个包含 10 个示例的分布(每个分布具有相同的标准差但不同的均值),使用plot_samples绘制示例,然后使用try_k_means对其进行聚类。

c25-fig-0006.jpg

图 25-6 k 均值的测试

调用contrived_test(1, 2, True)生成了图 25-7 中的绘图,并打印了图 25-8 中的线条。注意,初始(随机选择的)质心导致了高度偏斜的聚类,其中一个聚类包含了除一个点以外的所有点。然而,到第四次迭代时,质心移动到使得两个分布的点合理分开为两个聚类的位置。唯一的“错误”发生在A0A8上。

c25-fig-0007.jpg

图 25-7 来自两个分布的示例

c25-fig-0008.jpg

图 25-8 调用contrived_test(1, 2, True)打印的线条

当我们尝试50次实验而不是1次时,通过调用contrived_test(50, 2, False),它打印了

Final result
 Cluster with centroid [2.74674403 4.97411447] contains:
  A1, A2, A3, A4, A5, A6, A7, A8, A9
 Cluster with centroid [6.0698851  6.20948902] contains:
  A0, B0, B1, B2, B3, B4, B5, B6, B7, B8, B9

A0仍然与B混在一起,但A8则没有。如果我们尝试1000次实验,结果也一样。这可能会让你感到惊讶,因为图 25-7 显示,如果A0B0被选为初始质心(这在1000次实验中可能发生),第一次迭代将产生完美分离AB的聚类。然而,在第二次迭代中将计算新的质心,A0将被分配到与B的一个聚类中。这不好吗?请记住,聚类是一种无监督学习形式,它在未标记数据中寻找结构。将A0B分组并不不合理。

使用 k 均值聚类的一个关键问题是选择k。图 25-9 中的函数contrived_test_2生成、绘制并聚类来自三个重叠高斯分布的点。我们将使用它来查看在不同k值下对该数据的聚类结果。数据点在图 25-10 中显示。

c25-fig-0009.jpg

图 25-9 从三个分布生成点

c25-fig-0010.jpg

图 25-10 来自三个重叠高斯的点

调用contrived_test2(40, 2)打印

Final result has dissimilarity 90.128
 Cluster with centroid [5.5884966  4.43260236] contains:
  A0, A3, A5, B0, B1, B2, B3, B4, B5, B6, B7
 Cluster with centroid [2.80949911 7.11735738] contains:
  A1, A2, A4, A6, A7, C0, C1, C2, C3, C4, C5, C6, C7

调用contrived_test2(40, 3)打印

Final result has dissimilarity 42.757
 Cluster with centroid [7.66239972 3.55222681] contains:
  B0, B1, B3, B6
 Cluster with centroid [3.56907939 4.95707576] contains:
  A0, A1, A2, A3, A4, A5, A7, B2, B4, B5, B7
 Cluster with centroid [3.12083099 8.06083681] contains:
  A6, C0, C1, C2, C3, C4, C5, C6, C7

调用contrived_test2(40, 6)打印

Final result has dissimilarity 11.441
 Cluster with centroid [2.10900238 4.99452866] contains:
  A1, A2, A4, A7
 Cluster with centroid [4.92742554 5.60609442] contains:
  B2, B4, B5, B7
 Cluster with centroid [2.80974427 9.60386549] contains:
  C0, C6, C7
 Cluster with centroid [3.27637435 7.28932247] contains:
  A6, C1, C2, C3, C4, C5
 Cluster with centroid [3.70472053 4.04178035] contains:
  A0, A3, A5
 Cluster with centroid [7.66239972 3.55222681] contains:
  B0, B1, B3, B6

最后的聚类是最紧密的拟合,即聚类的不相似度最低(11.441)。这是否意味着这是“最佳”聚类?不一定。回想一下我们在 20.1.1 节中观察到的线性回归,通过增加多项式的次数,我们得到了一个更复杂的模型,从而更紧密地拟合了数据。我们还观察到,当我们增加多项式的次数时,我们有可能找到一个预测值较差的模型——因为它过拟合了数据。

选择合适的k值与为线性回归选择合适的多项式次数完全类似。通过增加k,我们可以减少不相似度,但有过拟合的风险。(当k等于待聚类样本数量时,不相似度为 0!)如果我们知道待聚类样本的生成方式,例如从m个分布中选择,我们可以利用这些信息来选择k。在缺乏此类信息的情况下,有多种启发式方法可以选择k。深入讨论这些超出了本书的范围。

25.4 一个不那么复杂的例子

不同物种的哺乳动物有不同的饮食习惯。一些物种(例如,大象和海狸)只吃植物,其他物种(例如,狮子和老虎)只吃肉,还有一些物种(例如,猪和人类)则吃任何能放进嘴里的东西。素食物种称为草食动物,肉食动物称为肉食动物,而那些既吃植物又吃动物的物种称为杂食动物。

在千百年的演化过程中(或者,如果你愿意,可以认为是某种神秘的过程),物种的牙齿被赋予了适合其偏好食物的形态。¹⁹⁴ 这引出了一个问题,即基于哺乳动物的牙齿结构进行聚类是否会产生与其饮食相关的聚类。

图 25-11 显示了一个文件的内容,列出了某些哺乳动物的物种、其牙齿公式(前8个数字)以及其平均成年体重(磅)。¹⁹⁵ 文件顶部的注释描述了与每种哺乳动物相关的项目,例如,名称后第一个项目是顶端门牙的数量。

c25-fig-0011.jpg

图 25-11 哺乳动物的牙齿结构在dentalFormulas.csv

图 25-12 包含三个函数。read_mammal_data函数首先读取一个 CSV 文件,格式如图 25-11 所示,以创建一个数据框。关键字参数comment用于指示read_csv忽略以#开头的行。如果参数scale_method不等于None,则使用scale_method缩放数据框中的每一列。最后,它创建并返回一个将物种名称映射到特征向量的字典。build_mammal_examples函数使用read_mammal_data返回的字典生成并返回一组示例。test_teeth函数生成并打印聚类。

c25-fig-0012.jpg

图 25-12 读取和处理 CSV 文件

调用test_teeth('dentalFormulas.csv', 3, 40)打印

Bear, Cow, Deer, Elk, Fur seal, Grey seal, Lion, Sea lion

Badger, Cougar, Dog, Fox, Guinea pig, Human, Jaguar, Kangaroo, Mink, Mole, Mouse, Pig, Porcupine, Rabbit, Raccoon, Rat, Red bat, Skunk, Squirrel, Wolf, Woodchuck

Moose

粗略检查表明,我们的聚类完全被动物的体重主导。问题在于,体重的范围远大于其他任何特征的范围。因此,当计算样本之间的欧几里得距离时,唯一真正重要的特征是体重。

我们在第 24.2 节遇到过类似的问题,当时发现动物之间的距离主要由腿的数量主导。我们通过将腿的数量转换为二元特征(有腿或无腿)来解决了那个问题。那对于该数据集是可行的,因为所有动物的腿数恰好是零或四条。然而,在这里,毫无疑问地将体重转换为单一二元特征而不损失大量信息是不现实的。

这是一个常见的问题,通常通过对特征进行缩放来解决,使每个特征的均值为0,标准差为1,¹⁹⁶,就像在图 25-13 中z_scale函数所做的那样。很容易看出,语句result = result - mean确保返回数组的均值总是接近0。¹⁹⁷ 标准差总是为1并不明显,可以通过一系列繁琐的代数变换证明,但我们不想让你感到乏味。这种缩放通常被称为z-缩放,因为标准正态分布有时被称为 Z 分布。

另一种常见的缩放方法是将最小特征值映射到0,将最大特征值映射到1,并在其间使用线性缩放,就像在图 25-13 中linear_scale函数所做的那样。这通常被称为最小-最大缩放

c25-fig-0013.jpg

图 25-13 缩放属性

调用test_teeth('dentalFormulas.csv', 3, 40, z_scale)打印

Badger, Bear, Cougar, Dog, Fox, Fur seal, Grey seal, Human, Jaguar, Lion, Mink, Mole, Pig, Raccoon, Red bat, Sea lion, Skunk, Wolf

Guinea pig, Kangaroo, Mouse, Porcupine, Rabbit, Rat, Squirrel, Woodchuck

Cow, Deer, Elk, Moose

这种聚类如何与这些哺乳动物相关的特征并不明显,但至少它并不是仅仅通过体重对哺乳动物进行分组。

回想一下,我们在本节开始时假设哺乳动物的牙齿特征与其饮食之间存在关系。 图 25-14 包含了一个 CSV 文件diet.csv的摘录,关联了哺乳动物及其饮食偏好。

c25-fig-0014.jpg

图 25-14 CSV 文件的开始,用于按饮食分类哺乳动物

我们可以使用diet.csv中的信息来看我们生成的聚类与饮食之间的关系。 图 25-15 中的代码正是这样做的。

c25-fig-0015.jpg

图 25-15 将聚类与标签相关联

当运行test_teeth_diet('dentalFormulas.csv', ‘diet.csv', 3, 40, z_scale)时,它打印了

Badger, Bear, Cougar, Dog, Fox, Fur seal, Grey seal, Human, Jaguar, Lion, Mink, Mole, Pig, Raccoon, Red bat, Sea lion, Skunk, Wolf
   0 herbivores, 13 carnivores, 5 omnivores

Guinea pig, Kangaroo, Mouse, Porcupine, Rabbit, Rat, Squirrel, Woodchuck
   3 herbivores, 0 carnivores, 5 omnivores

Cow, Deer, Elk, Moose
   4 herbivores, 0 carnivores, 0 omnivores

使用 z 缩放的聚类(线性缩放产生相同的聚类)并没有完美地根据动物的饮食习惯进行划分,但它与它们的饮食确实相关。它很好地将食肉动物与草食动物分开,但杂食动物出现的地方没有明显的模式。这表明,除了牙齿和体重外,可能还需要其他特征来区分杂食动物与草食动物和食肉动物。

25.5 本章引入的术语

  • 欧几里得均值

  • 不相似性

  • 重心

  • k 均值聚类

  • 标准正态分布

  • z 缩放

  • 线性缩放

  • 最小-最大缩放

  • 线性插值

第二十六章:分类方法

监督机器学习最常见的应用是构建分类模型。分类模型或分类器用于将示例标记为属于有限类别集中的一个。例如,判断一封电子邮件是否为垃圾邮件就是一个分类问题。在文献中,这些类别通常称为(因此得名分类)。我们也可以将一个示例描述为属于某个类或具有标签

单类学习中,训练集仅包含来自一个类的示例。目标是学习一个模型,预测示例是否属于该类。当很难找到不在该类之外的训练示例时,单类学习非常有用。单类学习常用于构建异常检测器,例如,检测计算机网络上以前未见过的攻击类型。

二分类学习(通常称为二元分类)中,训练集包含来自正负两个类的示例,目标是找到一个边界将这两个类分开。多类学习涉及找到将多个类别彼此分开的边界。

在本章中,我们研究两种广泛使用的监督学习方法来解决分类问题:k-近邻和回归。在此之前,我们先讨论如何评估这些方法产生的分类器的问题。

本章中的代码假设了导入语句

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import random
import sklearn.linear_model as sklm
import sklearn.metrics as skm

26.1 评估分类器

阅读过第二十章的朋友可能会记得,该章的一部分讨论了选择线性回归的次数的问题,1) 提供对可用数据的合理拟合,2) 有合理的机会对尚未见过的数据做出良好的预测。使用监督机器学习训练分类器时同样会出现这些问题。

我们首先将数据分为两个集合,一个训练集和一个测试集。训练集用于学习模型,测试集用于评估该模型。当我们训练分类器时,我们试图最小化训练误差,即在训练集中对示例分类时的错误,前提是满足某些约束。这些约束旨在提高模型在尚未见过的数据上表现良好的概率。让我们用图示来看一下。

图 26-1 左侧的图表显示了 60 位(模拟)美国公民的投票模式。x 轴表示选民家距离马萨诸塞州波士顿的距离,y 轴表示选民的年龄。星形标记表示通常投票给民主党的选民,三角形标记表示通常投票给共和党的选民。右侧的图表在图 26-1 中展示了包含随机选择的 30 位选民的训练集。实线和虚线表示两个群体之间的两个可能边界。基于实线的模型中,线下的点被分类为民主党选民;基于虚线的模型中,线左侧的点被分类为民主党选民。

c26-fig-0001.jpg

图 26-1 选民偏好的图

这两个边界都不能完美分隔训练数据。两个模型的训练错误显示在图 26-2 中的混淆矩阵里。每个矩阵的左上角显示被分类为民主党的例子的数量,这些例子实际上也是民主党,即真正的正例。左下角显示被分类为民主党的例子的数量,但这些例子实际上是共和党,即假阳性。右侧列显示了顶部的假阴性数量和底部的真正负例数量。

c26-fig-0002.jpg

图 26-2 混淆矩阵

每个分类器在训练数据上的准确性可以计算为

c26-fig-5001.jpg

在这种情况下,每个分类器的准确率为0.7。哪个更好地拟合了训练数据?这取决于我们是否更关注将共和党选民错误分类为民主党选民,或反之亦然。

如果我们愿意画出更复杂的边界,就可以得到一个更准确分类训练数据的分类器。例如,图 26-3 中所示的分类器在训练数据上的准确率约为 0.83,如图的左侧图所示。然而,正如我们在第二十章的线性回归讨论中看到的,模型越复杂,就越有可能出现对训练数据的过拟合。图 26-3 右侧的图展示了如果将复杂模型应用于保留集,会发生什么——准确率下降至0.6

c26-fig-0003.jpg

图 26-3 更复杂的模型

当两个类别的大小大致相等时,准确度是一种合理的评估分类器的方法。但在类别严重不平衡的情况下,准确度是一种糟糕的评估方式。想象一下,你被指派评估一个分类器,该分类器预测某种潜在致命疾病,该疾病在大约 0.1% 的待测人群中出现。准确度并不是一个特别有用的统计数据,因为仅仅通过宣称所有患者无病,就可以达到 99.9% 的准确率。对那些负责支付治疗费用的人来说,该分类器可能看起来很好(没人会接受治疗!),但对于那些担心自己可能患有该疾病的人来说,这个分类器可能看起来就没那么好了。

幸运的是,有关分类器的统计数据能够在类别不平衡时提供洞见:

c26-fig-5002.jpg

c26-fig-5003.jpg

c26-fig-5004.jpg

c26-fig-5005.jpg

灵敏度(在某些领域称为 召回率)是真阳性率,即正确识别为阳性的比例。特异性是真阴性率,即正确识别为阴性的比例。正预测值是被分类为阳性的示例真正为阳性的概率。负预测值是被分类为阴性的示例真正为阴性的概率。

这些统计测量的实现以及一个使用它们生成一些统计数据的函数在 图 26-4 中。我们将在本章稍后使用这些函数。

c26-fig-0004.jpg

图 26-4 评估分类器的函数

26.2 预测跑者的性别

在本书早些时候,我们使用波士顿马拉松的数据来说明许多统计概念。现在我们将使用相同的数据来说明各种分类方法的应用。任务是根据跑者的年龄和完成时间预测其性别。

函数 build_marathon_examples 在 图 26-6 中从如 图 26-5 所示格式的 CSV 文件中读取数据,然后构建一组示例。每个示例是类 Runner 的一个实例。每个跑者都有一个标签(性别)和一个特征向量(年龄和完成时间)。在 Runner 中唯一有趣的方法是 feature_dist。它返回两个跑者特征向量之间的欧几里得距离。

c26-fig-0005.jpg

图 26-5 bm_results2012.csv 的前几行

下一步是将示例分成训练集和保留的测试集。像往常一样,我们使用80%的数据进行训练,使用剩下的20%进行测试。这是通过图 26-6 底部的函数divide_80_20完成的。请注意,我们随机选择训练数据。简单地选择数据的前80%会减少代码量,但这样做有可能无法代表整个集合。例如,如果文件按完成时间排序,我们将得到一个偏向于更优秀跑者的训练集。

我们现在准备看看使用训练集构建一个可以预测跑者性别的分类器的不同方法。检查发现,训练集中58%的跑者是男性。因此,如果我们总是猜测男性,我们应该期待58%的准确率。在查看更复杂分类算法的性能时,请牢记这一基线。

c26-fig-0006.jpg

图 26-6 构建示例并将数据分为训练集和测试集

26.3 K-最近邻

K-最近邻(KNN)可能是所有分类算法中最简单的一种。“学习”模型只是训练示例本身。新的示例会根据它们与训练数据中示例的相似度被分配标签。

想象一下,你和朋友在公园散步,发现了一只鸟。你认为它是一只黄喉啄木鸟,但你的朋友则很确定它是一只金绿色啄木鸟。你急忙回家,翻找出你的鸟类书籍(或者,如果你在35 岁以下,去你喜欢的搜索引擎),开始查看标记好的鸟类图片。把这些标记好的图片看作训练集。没有一张图片完全匹配你看到的那只鸟,因此你选择了看起来最像你看到的鸟的五张(五个“最近邻”)。其中大多数是黄喉啄木鸟的照片——你宣布胜利。

KNN(和其他)分类器的一个弱点是,当训练数据中的示例分布与测试数据中的分布不同时,它们往往会给出较差的结果。如果书中鸟类照片的频率与您所在社区中该物种的频率相同,KNN 可能会表现良好。然而,假设尽管这种物种在你所在的社区同样常见,但你的书中有 30 张黄喉啄木鸟的照片,仅有一张金绿色啄木鸟的照片。如果使用简单的多数投票来确定分类,即使这些照片看起来与您看到的鸟不太相似,黄喉啄木鸟仍然会被选中。通过使用更复杂的投票方案,可以部分缓解这个问题,在这种方案中,k 个最近邻会根据它们与待分类示例的相似度加权。

图 26-7 中的函数实现了一个 k 最近邻分类器,根据跑步者的年龄和完成时间来预测跑步者的性别。该实现是暴力破解。函数 find_k_nearestexample_set 的示例数量上是线性的,因为它计算了 exampleexample_set 中每个元素之间的特征距离。函数 k_nearest_classify 使用简单的多数投票机制进行分类。k_nearest_classify 的复杂度为 O(len(training)*len(test_set)),因为它总共调用 find_k_nearest 函数 len(test_set) 次。

c26-fig-0007.jpg

图 26-7 查找 k 最近邻

当代码

examples = build_marathon_examples('bm_results2012.csv')
training, test_set = divide_80_20(examples)   
true_pos, false_pos, true_neg, false_neg =\
        k_nearest_classify(training, test_set, 'M', 9)
get_stats(true_pos, false_pos, true_neg, false_neg)

运行后,它打印了

Accuracy = 0.65
Sensitivity = 0.715
Specificity = 0.563
Pos. Pred. Val. = 0.684

我们是否应该感到高兴,因为在给定年龄和完成时间的情况下,我们可以以 65% 的准确率预测性别?评估分类器的一种方法是将其与一个甚至不考虑年龄和完成时间的分类器进行比较。图 26-8 中的分类器首先使用 training 中的示例来估计在 test_set 中随机选择的示例属于 label 类的概率。利用这个先验概率,它然后随机分配一个标签给 test_set 中的每个示例。

c26-fig-0008.jpg

图 26-8 基于普遍性的分类器

当我们在同样的波士顿马拉松数据上测试 prevalence_classify 时,它打印了

 Accuracy = 0.514
 Sensitivity = 0.593
 Specificity = 0.41
 Pos. Pred. Val. = 0.57

表明我们在考虑年龄和完成时间时获得了相当大的优势。

这种优势是有代价的。如果你运行 图 26-7 中的代码,你会注意到它需要相当长的时间才能完成。训练示例有 17,233 个,测试示例有 4,308 个,因此计算了近 75 百万的距离。这引发了一个问题:我们是否真的需要使用所有的训练示例。让我们看看如果我们简单地将训练数据按 10 的比例 下采样 会发生什么。

如果我们运行

reduced_training = random.sample(training, len(training)//10)
true_pos, false_pos, true_neg, false_neg =\
        k_nearest_classify(reduced_training, test_set, 'M', 9)
get_stats(true_pos, false_pos, true_neg, false_neg)

它完成所需时间的一半,分类性能几乎没有变化:

Accuracy = 0.638
Sensitivity = 0.667
Specificity = 0.599
Pos. Pred. Val. = 0.687

在实际操作中,当人们将 KNN 应用于大型数据集时,他们常常会对训练数据进行下采样。一个更常见的替代方法是使用某种快速近似 KNN 算法。

在上述实验中,我们将 k 设置为 9。我们并不是因为科学上的角色(我们太阳系中的行星数量),¹⁹⁸它的宗教意义(印度女神杜尔迦的形式数量),或它的社会学重要性(棒球阵容中的击球手数量)而选择这个数字。相反,我们通过使用 图 26-9 中的代码从训练数据中学习了 k,以搜索一个合适的 k

外层循环测试一系列 k 的值。我们只测试奇数值,以确保在 k_nearest_classify 中投票时,总会有一个性别占多数。

内部循环使用n 折交叉验证测试每个 k 值。在循环的每次num_folds迭代中,原始训练集被分成新的训练集/测试集对。然后,我们计算使用 k 近邻和新训练集对新测试集分类的准确度。当我们退出内部循环时,计算num_folds折的平均准确度。

当我们运行代码时,生成了图 26-10 中的图。正如我们所见,17是导致 5 折交叉验证中最佳准确度的k值。当然,没有保证某个大于21的值会更好。然而,一旦k达到9,准确度就在合理的狭窄范围内波动,因此我们选择使用9

c26-fig-0009.jpg

图 26-9 寻找合适的 k 值

c26-fig-0010.jpg

图 26-10 选择 k 值

26.4 基于回归的分类器

在第二十章中,我们使用线性回归构建了数据模型。我们可以在这里尝试同样的做法,使用训练数据为男性和女性分别构建模型。图 25-11 中的图是通过图 26-12 中的代码生成的。

c26-fig-0011.jpg

图 26-11 男性和女性的线性回归模型

c26-fig-0012.jpg

图 26-12 生成并绘制线性回归模型

看一眼图 26-11 就足以看到,线性回归模型仅解释了数据中很小一部分的方差。¹⁹⁹ 尽管如此,可以利用这些模型构建分类器。每个模型试图捕捉年龄与完成时间之间的关系。这种关系对男性和女性是不同的,我们可以利用这一点来构建分类器。给定一个示例,我们会问年龄与完成时间之间的关系更接近男性跑者的模型(实线)还是女性跑者的模型(虚线)所预测的关系。这个想法在图 26-13 中得到了实现。

当代码运行时,它打印

Accuracy = 0.614
Sensitivity = 0.684
Specificity = 0.523
Pos. Pred. Val. = 0.654 

结果比随机更好,但不如 KNN。

c26-fig-0013.jpg

图 26-13 使用线性回归构建分类器

你可能会想知道为什么我们采用这种间接的方法来使用线性回归,而不是明确使用年龄和时间的某种函数作为因变量,使用实际数字(比如0代表女性,1代表男性)作为自变量。

我们可以轻松地使用polyfit构建这样的模型,将年龄和时间的函数映射到一个实数。然而,预测某个跑步者处于男性和女性之间的中间位置意味着什么呢?比赛中有双性人吗?也许我们可以将 y 轴解释为一个跑步者是男性的概率。其实并不是。甚至没有保证将polyval应用于模型会返回一个介于01之间的值。

幸运的是,有一种回归形式,逻辑回归,²⁰⁰专门设计用于预测事件的概率。Python 库sklearn²⁰¹提供了良好的逻辑回归实现,以及与机器学习相关的许多其他有用函数和类。

模块sklearn.linear_model包含类LogisticRegression。该类的__init__方法有大量参数,用于控制解决回归方程所用的优化算法等。它们都有默认值,在大多数情况下,使用这些默认值是可以的。

LogisticRegression类的核心方法是fit。该方法接受两个相同长度的序列(元组、列表或数组)作为参数。第一个是特征向量的序列,第二个是相应标签的序列。在文献中,这些标签通常称为结果

fit方法返回一个类型为LogisticRegression的对象,该对象已为特征向量中的每个特征学习了系数。这些系数通常称为特征权重,捕捉了特征与结果之间的关系。正特征权重表明特征与结果之间存在正相关,而负特征权重表明负相关。权重的绝对值与相关性的强度有关。²⁰² 这些权重的值可以通过LogisticRegressioncoef_属性访问。由于可以在多个结果(在包的文档中称为类)上训练LogisticRegression对象,因此coef_的值是一个序列,其中每个元素包含与单个结果相关的权重序列。例如,表达式model.coef_[1][0]表示第二个结果的第一个特征的系数值。

一旦学习了系数,就可以使用LogisticRegression类的predict_proba方法来预测与特征向量相关的结果。predict_proba方法接受一个参数(除了self),即特征向量的序列。它返回一个数组,其中每个特征向量对应一个数组。返回数组中的每个元素包含对应特征向量的预测。预测为数组的原因是它包含构建model时所用每个标签的概率。

图 26-14 中的代码简单地展示了这一切是如何运作的。它首先创建了一个包含100,000个示例的列表,每个示例都有一个长度为3的特征向量,并标记为'A''B''C'‘D'。每个示例的前两个特征值来自标准差为0.5的高斯分布,但均值根据标签不同而变化。第三个特征的值是随机选择的,因此不应在预测标签时有用。在创建示例后,代码生成一个逻辑回归模型,打印特征权重,最后打印与四个示例相关的概率。

c26-fig-0014.jpg

图 26-14 使用sklearn进行多类逻辑回归

当我们运行图 26-14 中的代码时,它打印了

model.classes_ = ['A' 'B' 'C' ‘D']
For label A feature weights = [-4.7229 -4.3618  0.0595]
For label B feature weights = [-3.3346  4.7875  0.0149]
For label C feature weights = [ 3.7026 -4.4966 -0.0176]
For label D feature weights = [ 4.3548  4.0709 -0.0568]
[0, 0] probs = [9.998e-01 0.000e+00 2.000e-04 0.000e+00]
[0, 2] probs = [2.60e-03 9.97e-01 0.00e+00 4.00e-04]
[2, 0] probs = [3.000e-04 0.000e+00 9.996e-01 2.000e-04]
[2, 2] probs = [0.000e+00 5.000e-04 2.000e-04 9.992e-01]

首先让我们看看特征权重。第一行告诉我们前两个特征的权重大致相同,并且与示例标签为'A'的概率呈负相关。²⁰³也就是说,前两个特征值越大,示例为'A'的可能性就越小。我们预计在预测标签时价值较小的第三个特征,其相对于其他两个值的值较小,表明它相对不重要。第二行告诉我们,示例标签为'B'的概率与第一个特征的值呈负相关,而与第二个特征呈正相关。同样,第三个特征的值相对较小。第三行和第四行是前两行的镜像。

现在,让我们来看与四个示例相关的概率。概率的顺序对应于属性model.classes_中结果的顺序。正如你所希望的,当我们预测与特征向量[0, 0]相关的标签时,'A'的概率非常高,而'D'的概率非常低。类似地,[2, 2]'D'的概率非常高,而对'A'的概率非常低。与中间两个示例相关的概率也符合预期。

图 26-15 中的示例与图 26-14 中的示例相似,只是我们只创建了两个类'A''D'的示例,并且不包括不相关的第三个特征。

c26-fig-0015.jpg

图 26-15 二分类逻辑回归示例

当我们运行图 26-15 中的代码时,它打印了

model.coef = [[6.7081 6.5737]]
[0, 0] probs = [1\. 0.]
[0, 2] probs = [0.5354 0.4646]
[2, 0] probs = [0.4683 0.5317]
[2, 2] probs = [0\. 1.]

注意,coef_中只有一组权重。当使用fit为二元分类器生成模型时,它只为一个标签生成权重。这是足够的,因为一旦proba计算出某个示例属于任一类的概率,就可以确定它属于另一类的概率——因为这两者的概率之和必须为1coef_中的权重对应于哪一个标签?由于权重是正的,它们必须对应于'D',因为我们知道特征向量中的值越大,示例越可能属于'D'类。传统上,二元分类使用标签01,分类器使用1的权重。在这种情况下,coef_包含与最大标签相关联的权重,正如str类型的>运算符所定义的。

让我们回到波士顿马拉松的例子。图 26-16 中的代码使用LogisticRegression类为我们的波士顿马拉松数据构建和测试模型。函数apply_model接受四个参数:

  • model:一个LogisticRegression类型的对象,它已经构建了一个拟合模型。

  • test_set:一系列示例。这些示例具有与构建model拟合模型所使用的特征和标签相同的类型。

  • label:正类的标签。apply_model返回的混淆矩阵信息是相对于这个标签的。

  • prob:用于决定在test_set中将哪个标签分配给示例的概率阈值。默认值为0.5。由于它不是常量,apply_model可以用来研究假阳性和假阴性之间的权衡。

apply_model的实现首先使用列表推导(第 5.3.2 节)构建一个列表,其元素是test_set中示例的特征向量。然后它调用model.predict_proba获取与每个特征向量预测相对应的对的数组。最后,它将预测与与该特征向量相关的示例的标签进行比较,并跟踪并返回真正例、假正例、真负例和假负例的数量。

当我们运行代码时,它打印出:

Feature weights for label M: age = 0.055, time = -0.011
Accuracy = 0.636
Sensitivity = 0.831
Specificity = 0.377
Pos. Pred. Val. = 0.638

让我们将这些结果与我们使用 KNN 时获得的结果进行比较:

Accuracy = 0.65
Sensitivity = 0.715
Specificity = 0.563
Pos. Pred. Val. = 0.684

准确率和正预测值相似,但逻辑回归具有更高的敏感性和更低的特异性。这使得这两种方法难以比较。我们可以通过调整apply_model使用的概率阈值,使其具有与 KNN 大致相同的敏感性,从而解决这个问题。我们可以通过迭代prob的值,直到获得接近 KNN 的敏感性的概率。

如果我们用prob = 0.578来调用apply_model而不是0.5,我们会得到以下结果。

Accuracy = 0.659
Sensitivity = 0.715
Specificity = 0.586
Pos. Pred. Val. = 0.695

换句话说,这些模型的性能相似。

c26-fig-0016.jpg

图 26-16 使用逻辑回归预测性别

由于探索改变逻辑回归模型的决策阈值的影响可能会很复杂,人们常常使用称为接收器操作特征曲线,²⁰⁴或ROC 曲线,来可视化敏感性和特异性之间的权衡。该曲线绘制了多个决策阈值下的真实正例率(敏感性)与假正例率(1 – 特异性)的关系。

ROC 曲线通常通过计算曲线下的面积(AUROC,常缩写为AUC)来彼此比较。该面积等于模型将随机选择的正例分配更高的正概率的概率,相对于随机选择的负例。这被称为模型的区分能力。请记住,区分能力并不反映概率的准确性,通常称为校准。例如,我们可以将所有估计的概率除以2,而不会改变区分能力,但肯定会改变估计的准确性。

图 26-17 中的代码将逻辑回归分类器的 ROC 曲线绘制为实线,图 26-18。虚线是随机分类器的 ROC——一个随机选择标签的分类器。我们本可以先插值(因为我们只有离散的点)然后积分 ROC 曲线来计算 AUROC,但我们懒惰地直接调用了函数sklearn.metrics.auc

c26-fig-0017.jpg

图 26-17 构建 ROC 曲线并找到 AUROC

c26-fig-0018.jpg

图 26-18 ROC 曲线和 AUROC

指尖练习:编写代码以绘制 ROC 曲线并计算在 200 名随机选择的竞争者上测试时所构建模型的 AUROC。使用该代码调查训练样本数量对 AUROC 的影响(尝试从10变化到1010,增量为50)。

26.5 生存于泰坦尼克号

在 1912 年 4 月 15 日的早晨,RMS 泰坦尼克号撞上冰山并在北大西洋沉没。大约有1,300名乘客在船上,832人在这场灾难中遇难。许多因素导致了这场灾难,包括导航错误、救生艇不足以及附近船只反应缓慢。个别乘客的生存与否有随机因素,但远非完全随机。有一个有趣的问题是,是否可以仅通过船上乘客名单的信息建立一个合理的生存预测模型。

在本节中,我们从一个包含1046名乘客信息的 CSV 文件构建分类模型。²⁰⁵ 文件的每一行包含关于单个乘客的信息:舱位等级(1 等、2 等或 3 等)、年龄、性别、乘客是否在灾难中幸存以及乘客的姓名。CSV 文件的前几行是

Class,Age,Gender,Survived,Last Name,Other Names
1,29.0,F,1,Allen, Miss. Elisabeth Walton
1,0.92,M,1,Allison, Master. Hudson Trevor
1,2.0,F,0,Allison, Miss. Helen Loraine

在构建模型之前,快速查看一下数据,可能是个好主意。这样做通常能提供有关各种特征在模型中可能发挥的作用的有用见解。执行代码

manifest = pd.read_csv('TitanicPassengers.csv')
print(manifest.corr().round(2))

生成相关性表

          Class   Age  Survived
Class      1.00 -0.41     -0.32
Age       -0.41  1.00     -0.06
Survived  -0.32 -0.06      1.00

为什么Gender没有出现在这个表中?因为它在 CSV 文件中没有编码为数字。我们来处理一下,看看相关性是什么样的。

manifest['Gender'] = (manifest['Gender'].
                      apply(lambda g: 1 if g == 'M' else 0))
print(manifest.corr().round(2))

生成

          Class   Age  Gender  Survived
Class      1.00 -0.41    0.14     -0.32
Age       -0.41  1.00    0.06     -0.06
Gender     0.14  0.06    1.00     -0.54
Survived  -0.32 -0.06   -0.54      1.00

classGenderSurvived之间的负相关性表明,确实有可能利用清单中的信息建立预测模型。(因为我们将男性编码为 1,女性编码为 0,SurvivedGender的负相关性告诉我们,女性比男性更可能生存。同样,Class的负相关性表明,头等舱的乘客更安全。)

现在,让我们使用逻辑回归构建一个模型。我们选择使用逻辑回归是因为

  • 这是最常用的分类方法。

  • 通过检查逻辑回归生成的权重,我们可以获得一些关于为什么某些乘客比其他乘客更可能生存的见解。

图 26-19 定义了Passenger类。该代码中唯一感兴趣的地方是舱位等级的编码。虽然 CSV 文件将舱位等级编码为整数,但它实际上是类别的简写。舱位等级不像数字那样运作,例如,一个头等舱加一个二等舱并不等于一个三等舱。我们使用三个二进制特征(每种可能的舱位等级一个)对舱位等级进行编码。对于每位乘客,这三个变量中的一个被设置为1,其他两个被设置为0

这是机器学习中经常出现的问题的一个例子。类别特征(有时称为名义特征)是描述许多事物的自然方式,例如,跑步者的国家。用整数替换这些特征是很简单的,例如,我们可以根据国家的 ISO 3166-1 数字代码来选择表示,例如,巴西为 076,英国为 826,委内瑞拉为 862。这样做的问题在于,回归会将这些视为数值变量,从而对国家施加无意义的排序,导致委内瑞拉距离英国比距离巴西更近。

通过将分类变量转换为二元变量可以避免这个问题,就像我们处理舱位类时所做的那样。这样做的一个潜在问题是,它可能导致非常长且稀疏的特征向量。例如,如果一家医院配发 2000 种不同的药物,我们将把一个分类变量转换为 2000 个二元变量,每种药物一个。

图 26-20 包含使用 Pandas 从文件中读取数据并根据 泰坦尼克号 数据构建示例集的代码。

现在我们有了数据,可以使用构建波士顿马拉松数据模型时使用的相同代码构建逻辑回归模型。然而,由于数据集样本相对较少,我们需要关注使用之前采用的评估方法。完全有可能得到一个不具代表性的 80-20 数据划分,然后生成误导性结果。

为了降低风险,我们创建了许多 80-20 划分(每个划分使用在 图 26-6 中定义的 divide_80_20 函数创建),为每个划分构建和评估一个分类器,然后报告均值和 95% 置信区间,使用 图 26-21 和 图 26-22 中的代码。

c26-fig-0019.jpg

图 26-19 类 Passenger

c26-fig-0020.jpg

图 26-20 读取 泰坦尼克号 数据并构建示例列表²⁰⁷

c26-fig-0021.jpg

图 26-21 泰坦尼克号 生存测试模型

c26-fig-0022.jpg

图 26-22 打印有关分类器的统计信息

调用 test_models(build_Titanic_examples(), 100, True, False) 打印了

Averages for 100 trials
 Mean accuracy = 0.783, 95% conf. int. = 0.736 to 0.83
 Mean sensitivity = 0.702, 95% conf. int. = 0.603 to 0.801
 Mean specificity = 0.783, 95% conf. int. = 0.736 to 0.83
 Mean pos. pred. val. = 0.702, 95% conf. int. = 0.603 to 0.801
 Mean AUROC = 0.839, 95% conf. int. = 0.789 to 0.889

看起来这小组特征足以很好地预测生存情况。为了了解原因,让我们看看各种特征的权重。我们可以通过调用 test_models(build_Titanic_examples(), 100, False, True) 来做到这一点,它打印了

Averages for 100 trials
 Mean weight 1st Class = 1.145, 95% conf. int. = 1.02 to 1.27
 Mean weight 2nd Class = -0.083, 95% conf. int. = -0.185 to 0.019
 Mean weight 3rd Class = -1.062, 95% conf. int. = -1.179 to -0.945
 Mean weight age = -0.034, 95% conf. int. = -0.04 to -0.028
 Mean weight male = -2.404, 95% conf. int. = -2.542 to -2.266

当谈到船难生存时,似乎拥有财富是有用的(泰坦尼克号的一等舱舱位在今天的美国美元中相当于超过$70,000),年轻和女性也是优势。

26.6 总结

在最后三章中,我们几乎只是触及了机器学习的表面。

这同样适用于本书第二部分中介绍的许多其他主题。我试图让你感受到利用计算更好理解世界所涉及的思维方式——希望你能找到独立研究该主题的方法。

26.7 章节中引入的术语

  • 分类模型

  • 类别

  • 标签

  • 单类学习

  • 两类学习

  • 二元分类

  • 多类学习

  • 测试集

  • 训练误差

  • 混淆矩阵

  • 准确率

  • 类别不平衡

  • 敏感性(召回率)

  • 特异性(精准度)

  • 正预测值(PPV)

  • k 最近邻(KNN)

  • 下采样

  • 阴性预测值

  • n 倍交叉验证

  • 逻辑回归

  • 结果

  • 特征权重

  • ROC 曲线

  • AUROC

  • 模型区分度

  • 校准

  • 类别特征

第二十七章:PYTHON 3.8 快速参考

对数值类型的常见操作

**i+j**ij 的和。

**i–j**i 减去 j

**i*j**ij 的乘积。

**i//j** 是向下取整除法。

**i/j** 是浮点除法。

**i%j** 是整型 i 除以整型 j 的余数。

**i**j**ij 次幂。

**x += y** 等同于 x = x + y***=****-=** 也以相同方式工作。

比较运算符有 == (等于)、 != (不等于)、 > (大于)、 >= (至少)、 < (小于)和 <= (最多)。

布尔运算符

**x == y** 如果 xy 相等,则返回 True

**x != y** 如果 xy 不相等,则返回 True

**<, >, <=, >=** 具有其通常的含义。

**a and b** 如果 ab 都为 True,则为 True,否则为 False

**a or b** 如果 ab 至少有一个为 True,则为 True,否则为 False

**not a** 如果 aFalse,则为 True;如果 aTrue,则为 False

对序列类型的常见操作

**seq[i]** 返回序列中的第 i 个元素。

**len(seq)** 返回序列的长度。

**seq1 + seq2** 连接两个序列。(范围不适用。)

**n*seq** 返回一个重复 seq n 次的序列。(范围不适用。)

**seq[start:end]** 返回一个新的序列,它是 seq 的切片。

**e in seq** 测试 e 是否包含在序列中。

**e not in seq** 测试 e 是否不包含在序列中。

**for e in seq** 遍历序列中的元素。

常见字符串方法

**s.count(s1)** 计算字符串 s1s 中出现的次数。

**s.find(s1)** 返回子字符串 s1s 中第一次出现的索引;如果 s1 不在 s 中,则返回 -1

**s.rfind(s1)**find 相同,但从 s 的末尾开始。

**s.index(s1)**find 相同,但如果 s1 不在 s 中,则引发异常。

**s.rindex(s1)**index 相同,但从 s 的末尾开始。

**s.lower()** 将所有大写字母转换为小写。

**s.replace(old, new)** 将字符串 old 的所有出现替换为字符串 new

**s.rstrip()** 移除末尾的空白字符。

**s.split(d)** 使用 d 作为分隔符分割 s。返回 s 的子字符串列表。

常见列表方法

**L.append(e)** 将对象 e 添加到列表 L 的末尾。

**L.count(e)** 返回元素 e 在列表 L 中出现的次数。

**L.insert(i, e)** 在列表 L 的索引 i 处插入对象 e

**L.extend(L1)** 将列表 L1 中的项追加到列表 L 的末尾。

**L.remove(e)** 从列表 L 中删除 e 的第一次出现。

**L.index(e)** 返回 e 在列表 L 中第一次出现的索引。如果 e 不在 L 中,则引发 ValueError

**L.pop(i)** 移除并返回索引 i 处的项;i 默认为 -1。如果 L 为空,则引发 IndexError

**L.sort()** 具有对 L 中元素进行排序的副作用。

**L.reverse()** 具有反转 L 中元素顺序的副作用。

**L.copy()** 返回 L 的浅拷贝。

**L.deepcopy()** 返回 L 的深拷贝。

字典的常见操作

**len(d)** 返回 d 中项目的数量。

**d.keys()** 返回 d 中键的视图。

**d.values()** 返回 d 中值的视图。

**d.items()** 返回 d 中的 (键, 值) 对的视图。

**k in d** 如果键 kd 中,则返回 True

**d[k]** 返回 d 中键为 k 的项目。如果 k 不在 d 中,则引发 KeyError

**d.get(k, v)** 如果 kd 中,则返回 d[k],否则返回 v

**d[k] = v** 将值 v 关联到键 k。如果 k 已经关联了一个值,则该值会被替换。

**del d[k]**d 中删除键为 k 的元素。如果 k 不在 d 中,则引发 KeyError

**for k in d** 遍历 d 中的键。

常见的输入/输出机制

**input(msg)** 打印 msg,然后返回输入的值作为字符串。

**print(s1, …, sn)** 打印字符串 s1, …, sn,并用空格分隔。

**open('file_name', 'w')** 创建一个用于写入的文件。

**open('file_name', 'r')** 打开现有文件以进行读取。

**open('file_name', 'a')** 打开现有文件以进行追加。

**file_handle.read()** 返回包含文件内容的字符串。

**file_handle.readline()** 返回文件中的下一行。

**file_handle.readlines()** 返回包含文件行的列表。

**file_handle.write(s)** 将字符串 s 写入文件末尾。

**file_handle.writelines(L)**L 的每个元素写入文件作为单独的行。

**file_handle.close()** 关闭文件。

posted @   绝不原创的飞龙  阅读(9)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示