R-机器学习第四版-三-

92 阅读1小时+

R 机器学习第四版(三)

原文:annas-archive.org/md5/e01de45f59df828d0f9928d7771beee5

译者:飞龙

协议:CC BY-NC-SA 4.0

第七章:黑盒方法 – 神经网络和支持向量机

已故科幻小说作家亚瑟·C·克拉克写道:“任何足够先进的技术都与魔法无法区分。”本章涵盖了两种机器学习方法,它们乍一看可能像是魔法。虽然它们非常强大,但它们的内部工作原理可能难以理解。

在工程学中,这些被称为黑盒过程,因为将输入转换为输出的机制被一个想象中的盒子所掩盖。例如,封闭源代码软件的黑盒有意隐藏了专有算法,政治立法的黑盒根植于官僚程序,而香肠制作的黑盒则涉及一点有意的(但美味的)无知。在机器学习的情况下,黑盒是由于复杂的数学使得它们能够运作。

虽然它们可能不容易理解,但盲目地应用黑盒模型是危险的。因此,在本章中,我们将窥视盒子内部,调查在拟合此类模型中涉及的统计香肠制作过程。你会发现如何:

  • 神经网络模仿生物大脑来模拟数学函数

  • 支持向量机使用多维表面来定义特征与结果之间的关系

  • 尽管它们很复杂,但它们可以轻松应用于现实世界的问题

希望你能意识到,你不需要在统计学上拥有黑带级别的能力来应对黑盒机器学习方法——无需感到害怕!

理解神经网络

人工神经网络ANN)通过从我们对生物大脑如何对感官输入的刺激做出反应的理解中得出的模型,模拟了一组输入信号与输出信号之间的关系。就像大脑使用称为神经元的相互连接的细胞网络来提供强大的学习能力一样,ANN 使用人工神经元或节点来解决具有挑战性的学习问题。

人类大脑由大约 850 亿个神经元组成,形成一个能够表示大量知识网络的网络。正如你所预期的那样,这使其他生物的大脑相形见绌。例如,一只猫大约有 10 亿个神经元,一只老鼠大约有 7500 万个神经元,而一只蟑螂只有大约 100 万个神经元。相比之下,许多 ANN 包含的神经元要少得多,通常只有数百或数千个,所以我们不会在近期内创造出人工大脑——即使是一只拥有 10 万个神经元的果蝇也远远超过在标准计算硬件上运行的最新 ANN——而且这些 ANN 仍然被小型动物的大脑所 dwarf,更不用说人类的大脑了——而生物大脑可以装入一个更小的包装中!

尽管可能无法完全模拟蟑螂的大脑,但神经网络仍然可以提供某些行为的足够启发式模型。假设我们开发了一个算法,可以模仿蟑螂被发现时逃跑的方式。如果机器人蟑螂的行为令人信服,那么它的脑部是否像活体生物一样复杂就无关紧要了。同样,如果基于神经网络工具(如 ChatGPT openai.com/blog/ChatGPT/)产生的文本大多数时候可以冒充人类文本,那么神经网络是否是完美的人脑模型就无关紧要了。这个问题是 1950 年由计算机科学先驱艾伦·图灵提出的有争议的图灵测试的核心,该测试将机器视为智能,如果人类无法区分其行为与生物体的行为。

更多关于围绕图灵测试的神秘和争议的信息,请参阅斯坦福哲学百科全书plato.stanford.edu/entries/turing-test/

简单的 ANNs 已经超过 60 年用于模拟大脑的解决问题的方法。最初,这涉及到学习简单的函数,如逻辑与函数或逻辑或函数。这些早期的练习主要用于帮助科学家了解生物大脑可能如何运作。然而,随着近年来计算机变得越来越强大,ANNs 的复杂性也相应增加,以至于它们现在经常应用于更实际的问题,包括:

  • 语音、手写和图像识别程序,如智能手机应用、邮件分拣机和搜索引擎所使用的程序

  • 智能设备的自动化,例如办公大楼的环境控制,或自动驾驶汽车和自主飞行的无人机控制

  • 复杂的天气和气候模式、抗拉强度、流体动力学以及许多其他科学、社会或经济现象的模型

从广义上讲,ANNs 是多才多艺的学习者,可以应用于几乎任何学习任务:分类、数值预测,甚至无监督模式识别。

无论是否值得,ANN 学习者经常在媒体上被大肆报道。例如,谷歌开发的一个“人工大脑”因其能够识别 YouTube 上的猫视频而备受赞誉。这种炒作可能更多与 ANNs 的独特性无关,而更多与 ANNs 因其与生物大脑的相似性而具有吸引力的事实有关。

ANNs 通常应用于输入数据和输出数据定义良好,但将输入与输出联系起来的过程极其复杂且难以定义的问题。作为一种黑盒方法,ANNs 对于这些类型的黑盒问题非常有效。

从生物神经元到人工神经元

由于人工神经网络(ANNs)被有意设计成人类大脑活动的概念模型,因此首先了解生物神经元是如何工作的是有帮助的。如图所示,细胞通过生化过程接收传入的信号,这些信号通过细胞树突。这个过程允许根据其相对重要性或频率对冲动进行加权。当细胞体开始积累传入的信号时,会达到一个阈值,此时细胞会放电,输出信号通过电化学过程沿着轴突传递。在轴突的末端,电信号再次被处理成化学信号,通过一个称为突触的微小间隙传递给相邻的神经元。

图描述自动生成

图 7.1:生物神经元的艺术描绘

单个人工神经元的模型可以用与生物模型非常相似的方式来理解。如图所示,一个有向网络图定义了通过树突接收的输入信号(x变量)和输出信号(y变量)之间的关系。就像生物神经元一样,每个树突的信号都会根据其重要性进行加权(现在忽略这些权重是如何确定的)。输入信号由细胞体相加,然后根据一个表示为f激活函数传递信号。

图描述自动生成

图 7.2:人工神经元的设计旨在模仿生物神经元的结构和功能

典型的人工神经元可以用以下公式表示,其中n表示输入树突的数量。w权重允许每个n个输入(用x[i]表示)对输入信号的总和贡献更多或更少的量。净总信号由激活函数*f(x)使用,产生的信号y(x)*是输出轴突:

神经网络使用这种方式定义的神经元作为构建复杂数据模型的基石。尽管神经网络有无数种变体,但每种都可以用以下特征来定义:

  • 激活函数,它将神经元的净输入信号转换成单个输出信号,以便在网络中进一步广播

  • 网络拓扑(或架构),它描述了模型中的神经元数量,以及层数和它们之间的连接方式

  • 训练算法,它指定了如何根据输入信号的比例来设置连接权重以抑制或兴奋神经元

让我们来看看每个类别中的一些变体,看看它们如何被用来构建典型的神经网络模型。

激活函数

激活函数是人工神经元处理传入信息并确定是否将信号传递给网络中其他神经元的机制。正如人工神经元是模仿生物版本一样,激活函数也是模仿自然的设计。

在生物案例中,激活函数可以想象为一个涉及求和总输入信号并确定它是否达到触发阈值的进程。如果是这样,神经元就会传递信号;否则,它就什么都不做。在 ANN 术语中,这被称为阈值激活函数,因为它只在达到指定的输入阈值时产生输出信号。

以下图描绘了一个典型的阈值函数;在这种情况下,当输入信号的求和至少为零时,神经元就会触发。由于其形状类似于楼梯,有时也被称为单位步激活函数

图表,直方图  自动生成的描述

图 7.3:阈值激活函数仅在输入信号达到阈值后才“开启”

尽管阈值激活函数由于其与生物学的相似性而有趣,但在 ANN 中很少使用。摆脱生物化学的限制,ANN 激活函数可以根据其展示的期望数学特征和准确模拟数据之间关系的能力来选择。

可能最常用的替代方案是以下图中所示的S 型激活函数(更具体地说,是逻辑 S 型)。请注意,在所示公式中,e是自然对数的底(大约为 2.72)。尽管它与阈值激活函数具有相似的步骤或“S”形状,但输出信号不再是二进制的;输出值可以落在零到一之间的任何地方。此外,S 型函数是可微分的,这意味着可以计算整个输入范围内的导数(曲线上的某一点的切线斜率)。正如你稍后将会学到的那样,这一特性对于创建高效的 ANN 优化算法至关重要。

图表  自动生成的描述

图 7.4:S 型激活函数使用平滑曲线来模拟自然界中发现的单位步激活函数

尽管 S 型函数可能是最常用的激活函数,并且通常默认使用,但一些神经网络算法允许选择替代方案。以下图 7.5 中展示了这类激活函数的选择:

图表,折线图  自动生成的描述

图 7.5:几种常见的神经网络激活函数

区分这些激活函数的主要细节是输出信号的范围。通常,这可能是以下之一:(0, 1),(-1, +1),或 (-∞, +∞)。激活函数的选择会影响神经网络的偏差,使其可能更适合某些类型的数据,从而允许构建专门的神经网络。例如,线性激活函数会导致神经网络非常类似于线性回归模型,而高斯激活函数是径向基函数(RBF)网络的基础。这些中的每一个都有适合某些学习任务的优点,而不适合其他任务。

神经网络几乎只使用非线性激活函数,因为这是网络随着节点数量的增加而变得更智能的原因。仅限于线性激活函数的网络将限于线性解决方案,并且其表现不会优于许多更简单的回归方法。

重要的是要认识到,对于许多激活函数,影响输出信号的输入值范围相对较窄。例如,在 sigmoid 函数的情况下,当输入信号低于-5 时,输出信号非常接近 0,而当输入信号高于 5 时,输出信号非常接近 1。以这种方式压缩信号会导致动态输入的高低端饱和,就像将吉他放大器音量调得过高,由于声音波峰的截断而导致失真声音一样。因为这种做法本质上是将输入值挤压到一个更小的输出值范围内,所以像 sigmoid 这样的激活函数有时被称为挤压函数

解决挤压问题的方法之一是将所有神经网络输入转换,使得特征值落在围绕零的小范围内。这可能涉及标准化或归一化特征。通过限制输入值的范围,激活函数将能够在整个范围内工作。一个附带的好处是,模型也可能训练得更快,因为算法可以更快地遍历可操作的输入值范围。

虽然从理论上讲,神经网络可以通过多次迭代调整其权重来适应非常动态的特征,但在极端情况下,许多算法会在这种情况发生之前就停止迭代。如果你的模型未能收敛,请务必检查你是否已正确标准化了输入数据。选择不同的激活函数也可能是合适的。

网络拓扑

神经网络学习的能力根植于其拓扑结构,即相互连接的神经元的模式和结构。尽管有无数种网络架构形式,但它们可以通过三个关键特征来区分:

  • 层数的数量

  • 网络中信息是否允许向后传播

  • 网络每一层中的节点数量

拓扑决定了网络可以学习的任务复杂性。一般来说,更大、更复杂的网络可以识别更微妙的模式和更复杂的决策边界。然而,网络的力量不仅取决于网络的大小,还取决于单元的排列方式。

层数的数量

要定义拓扑,我们需要术语来区分网络中位置不同的人工神经元。图 7.6展示了一个非常简单的网络的拓扑结构。一组称为输入节点的神经元直接从输入数据接收未经处理的信号。每个输入节点负责处理数据集中单个特征;该特征值将通过相应节点的激活函数进行转换。输入节点发送的信号被输出节点接收,该节点使用自己的激活函数生成最终的预测(在此处表示为p)。

输入和输出节点被安排在称为的组中。因为输入节点以接收到的数据完全相同的方式处理传入的数据,所以网络只有一个连接权重集(在此处标记为w[1],w[2],和w[3])。因此,它被称为单层网络。单层网络可用于基本的模式分类,尤其是对于线性可分模式的分类,但对于大多数学习任务,需要更复杂的网络。

自动生成的描述

图 7.6:具有三个输入节点的简单单层 ANN

如您所预期的那样,创建更复杂网络的一个明显方法是通过添加额外的层。如图图 7.7所示,一个多层网络添加一个或多个隐藏层,这些隐藏层在信号到达输出节点之前处理来自输入节点的信号。隐藏节点之所以得名,是因为它们在网络中心被遮挡,并且它们与数据和输出的关系更难以理解。隐藏层使得人工神经网络成为一个黑盒模型;了解这些层内部发生的事情实际上是不可能的,尤其是当拓扑变得更加复杂时。

自动生成的描述

图 7.7:具有单个两个节点隐藏层的多层网络

更复杂拓扑的示例在图 7.8中展示。可以使用多个输出节点来表示具有多个类别的结果。可以使用多个隐藏层来允许黑盒内部有更多的复杂性,从而模拟更复杂的问题。

包含文本、剪刀、工具的图片,自动生成的描述

图 7.8:复杂的 ANN 可以具有多个输出节点或多个隐藏层

具有多个隐藏层的神经网络被称为深度神经网络DNN),训练此类网络的实践被称为深度学习。在大数据集上训练的 DNN 在复杂任务如图像识别和文本处理上能够达到类似人类的性能。因此,深度学习被炒作成为机器学习中的下一个重大飞跃,但深度学习更适合某些任务而不是其他任务。

尽管深度学习在传统模型难以应对的复杂学习任务上表现相当出色,但它需要比大多数项目中找到的更大的数据集和更丰富的特征集。典型的学习任务包括对图像、音频或文本等非结构化数据进行建模,以及随时间重复测量的结果,如股票市场价格或能源消耗。在这些类型的数据上构建深度神经网络(DNN)需要专门的计算软件(有时还需要硬件),其使用难度比简单的 R 包要大。第十五章“利用大数据”提供了如何使用这些工具在 R 中执行深度学习和图像识别的详细信息。

信息传递的方向

简单的多层网络通常是全连接的,这意味着一个层中的每个节点都与下一层的每个节点相连,但这并非必需。更大的深度学习模型,例如将在第十五章“利用大数据”中介绍的用于图像识别的卷积神经网络CNN),只是部分连接。移除一些连接有助于限制在众多隐藏层中可能发生的过度拟合的数量。然而,这并非我们操纵拓扑的唯一方式。除了节点是否连接之外,我们还可以指定信息流在整个连接中的方向,并产生适合不同类型学习任务的神经网络。

你可能已经注意到,在前面的例子中,箭头被用来指示只在一个方向上传递的信号。从输入层到输出层连续单向输入输入信号的神经网络被称为前馈网络。尽管对信息流有限制,但前馈网络提供了令人惊讶的灵活性。

例如,可以改变每个级别的级别和节点数量,可以同时建模多个结果,或者应用多个隐藏层。

与前馈网络不同,循环神经网络RNN)(或反馈网络)允许信号通过循环向后传递。这种特性更接近于生物神经网络的工作方式,使得学习极其复杂的模式成为可能。加入短期记忆,或延迟,极大地增强了循环网络的能力。值得注意的是,这包括理解随时间推移的事件序列的能力。这可以用于股市预测、语音理解或天气预报。一个简单的循环网络如下所示:

图示描述自动生成

图 7.9:允许信息在网络中向后传递可以模拟时间延迟

由于 RNN 的短期记忆在定义上显然是短的,因此一种名为长短期记忆LSTM)的 RNN 形式调整了模型,使其具有显著更长的回忆能力——就像既有短期又有长期记忆的活物一样。虽然这似乎是一种明显的改进,但计算机具有完美的记忆,因此需要明确告知何时忘记和何时记住。挑战在于在过早忘记和过长记住之间找到平衡,这比说起来要难得多,因为用于训练模型的数学函数自然地被拉向这两个极端,原因将在本章继续阅读时变得更加清晰。

更多关于 LSTM 神经网络的信息,请参阅理解 LSTM——关于长短期记忆循环神经网络教程,Staudemeyer RC 和 Morris ER,2019arxiv.org/abs/1909.09586

LSTM 神经网络的发展导致了人工智能的进步,例如使机器人能够模仿控制机械、驾驶和玩电子游戏所需的人类行为序列。LSTM 模型也显示出在语音和文本识别、理解语言语义以及在不同语言之间翻译和学习复杂策略方面的能力。DNN 和循环网络越来越多地被用于各种高调应用,因此自本书第一版出版以来,它们变得更加流行。然而,构建这样的网络需要超出本书范围的技术和软件,并且通常需要访问专门的计算硬件或云服务器。

另一方面,简单的前馈网络也非常擅长模拟许多现实世界的任务,尽管这些任务可能不如自动驾驶汽车和玩电子游戏的计算机那样令人兴奋。虽然深度学习正在迅速成为主流,但多层前馈网络,也称为多层感知器MLP),可能仍然是传统学习任务的默认标准人工神经网络拓扑。此外,理解 MLP 拓扑为以后构建更复杂的深度学习模型提供了强大的理论基础。

每层的节点数量

除了层数和信息传递方向的变化之外,神经网络还可以通过每层的节点数量来变化其复杂性。输入节点的数量由输入数据中的特征数量预先确定。同样,输出节点的数量由要模拟的结果数量或结果中的类别级别数量预先确定。然而,隐藏节点的数量留给用户在训练模型之前决定。

不幸的是,没有可靠的规则来确定隐藏层中神经元的数量。合适的数量取决于输入节点的数量、训练数据量、噪声数据量以及学习任务的复杂性等因素。

通常,具有更多网络连接的更复杂网络拓扑允许学习更复杂的问题。更多的神经元将导致一个模型更接近训练数据,但这也存在过拟合的风险;它可能对未来数据的泛化能力较差。大型神经网络也可能计算成本高昂且训练缓慢。最佳实践是使用最少节点,这些节点在验证数据集上能够实现足够的性能。在大多数情况下,即使只有少量隐藏节点——通常只有几个——神经网络也能展现出惊人的学习能力。

已被证明,具有至少一个具有足够多神经元和非线性激活函数的隐藏层的神经网络是一个通用函数逼近器。这意味着神经网络可以用来逼近任何连续函数,在有限区间内达到任意精度的近似。这就是神经网络获得在章节引言中描述的那种“魔法”能力的地方;将一组输入放入神经网络的黑盒中,它可以学会产生任何一组输出,无论输入和输出之间的关系有多么复杂。当然,这假设“足够多的神经元”以及足够的数据来合理训练网络——同时避免对噪声过拟合,以便近似可以泛化到训练示例之外。我们将在下一节进一步探讨允许这种魔法发生的黑盒。

要查看网络拓扑变化如何使神经网络成为通用函数逼近器的实时可视化,请访问playground.tensorflow.org/的深度学习游乐场。游乐场允许你实验预测模型,其中特征与目标之间的关系复杂且非线性。虽然回归和决策树等方法在解决这些问题上会遇到困难,但你将发现,添加更多的隐藏节点和层可以使网络在足够的训练时间内为每个示例提供一个合理的近似。请注意,特别是选择线性激活函数而不是 Sigmoid 或双曲正切(tanh)函数,可以防止网络无论网络拓扑的复杂性如何都能学习到一个合理的解决方案。

通过调整连接权重来训练神经网络

网络拓扑是一个白板,它本身并没有学习到任何东西。就像一个新生儿一样,它必须通过经验来训练。随着神经网络处理输入数据,神经元之间的连接会加强或减弱,这类似于婴儿的大脑在经历环境时的发展。网络的连接权重会调整以反映随时间观察到的模式。

通过调整连接权重来训练神经网络是非常计算密集的。因此,尽管它们在几十年前就已经被研究,但直到 20 世纪 80 年代中后期,当发现了一种有效的训练 ANN 的方法之前,ANNs 很少应用于实际的学习任务。这个使用反向传播错误策略的算法现在简单地被称为反向传播

意外的是,几个研究团队几乎在同一时间独立发现了并发表了反向传播算法。其中,最常被引用的工作可能是Rumelhart, DE, Hinton, GE, Williams, RJ, Nature, 1986, Vol. 323, pp. 533-566,通过反向传播错误学习表示

尽管与许多其他机器学习算法相比,反向传播方法在计算上仍然有些昂贵,但它导致了人工神经网络(ANNs)兴趣的复苏。因此,使用反向传播算法的多层前馈网络现在在数据挖掘领域很常见。这些模型具有以下优点和缺点:

优点缺点

|

  • 可以适应分类或数值预测问题

  • 一种“通用逼近器”,能够模拟比几乎所有算法更复杂的模式

  • 对数据的潜在关系假设很少

|

  • 极其计算密集且训练缓慢,尤其是如果网络拓扑复杂

  • 极易过拟合训练数据,导致泛化能力差

  • 导致了一个复杂的黑盒模型,难以解释,如果不是不可能的话

|

在其最一般的形式中,反向传播算法通过许多两个过程的循环迭代。每个循环被称为一个时代。因为网络不包含先验(现有)知识,所以起始权重通常设置为随机。然后,算法通过这些过程迭代,直到达到停止标准。反向传播算法中的每个时代包括:

  • 一个前向阶段,在这个阶段,神经元按顺序从输入层到输出层被激活,沿途应用每个神经元的权重和激活函数。当达到最后一层时,产生一个输出信号。

  • 一个反向阶段,在这个阶段,网络的前向阶段产生的输出信号与训练数据中的真实目标值进行比较。网络输出信号与真实值之间的差异导致一个错误,该错误在网络中向后传播以修改神经元之间的连接权重并减少未来的错误。

随着时间的推移,算法使用向后发送的信息来减少网络犯下的总错误。然而,一个问题仍然存在:由于每个神经元的输入和输出之间的关系复杂,算法如何确定权重应该改变多少?

这个问题的答案涉及一种称为梯度下降的技术。从概念上讲,这类似于一个被困在丛林中的探险者如何找到通往水源的道路。通过检查地形并持续向最大下坡方向行走,探险者最终会到达最低的谷地,这很可能是河床。

在一个类似的过程中,反向传播算法使用每个神经元的激活函数的导数来识别每个输入权重的方向梯度——因此具有可微分的激活函数的重要性。梯度表明误差将如何随着权重的变化而减少或增加。算法将尝试通过称为学习率的量来改变权重,以实现误差的最大减少。学习率越大,算法尝试沿着梯度下降的速度就越快,这可能会减少训练时间,但也可能超过谷地。

图解 描述自动生成

图 7.10:梯度下降算法寻求最小误差,但也可能找到一个局部最小值

尽管使用梯度下降法找到最小误差率所需的数学知识复杂,因此超出了本书的范围,但在实践中通过其在 R 中神经网络算法的实现应用起来却很容易。让我们将我们对多层前馈网络的理解应用于一个实际问题。

示例 - 使用人工神经网络建模混凝土强度

在工程领域,拥有建筑材料的准确性能估计至关重要。这些估计是制定用于建筑、桥梁和道路建设中所用材料的安全生产指南所必需的。

估计混凝土的强度是一个特别有趣的问题。尽管它在几乎每个建设项目中都被使用,但由于各种成分以复杂的方式相互作用,混凝土的性能差异很大。因此,准确预测最终产品的强度是困难的。一个能够根据输入材料的成分列表可靠地预测混凝土强度的模型,可能导致更安全的施工实践。

第 1 步 – 收集数据

对于这次分析,我们将利用 I-Cheng Yeh 捐赠给 UCI 机器学习仓库(archive.ics.uci.edu/ml)的混凝土抗压强度数据。由于他发现使用神经网络来模拟这些数据是成功的,我们将尝试使用 R 中的简单神经网络模型来复制 Yeh 的工作。

关于 Yeh 对这一学习任务的方法的更多信息,请参阅使用人工神经网络建模高性能混凝土强度,Yeh, IC,水泥与混凝土研究,1998,第 28 卷,第 1797-1808 页

根据网站信息,该数据集包含 1,030 个混凝土示例,其中包含 8 个特征,描述了混合物中使用的成分。这些特征被认为与最终的抗压强度相关,包括产品中使用的水泥、矿渣、灰、水、超塑化剂、粗骨料和细骨料的量(以每立方米千克计),以及老化时间(以天为单位)。

要跟随这个示例,请从 Packt Publishing 网站下载concrete.csv文件,并将其保存到您的 R 工作目录中。

第 2 步 – 探索和准备数据

如同往常,我们将通过使用read.csv()函数将数据加载到 R 对象中,并确认它符合预期的结构来开始我们的分析:

> concrete <- read.csv("concrete.csv")
> str(concrete) 
'data.frame': 1030 obs. of  9 variables:
 $ cement      : num  141 169 250 266 155 ...
 $ slag        : num  212 42.2 0 114 183.4 ...
 $ ash         : num  0 124.3 95.7 0 0 ...
 $ water       : num  204 158 187 228 193 ...
 $ superplastic: num  0 10.8 5.5 0 9.1 0 0 6.4 0 9 ...
 $ coarseagg   : num  972 1081 957 932 1047 ...
 $ fineagg     : num  748 796 861 670 697 ...
 $ age         : int  28 14 28 28 28 90 7 56 28 28 ...
 $ strength    : num  29.9 23.5 29.2 45.9 18.3 ... 

数据框中的九个变量对应于八个特征和一个预期的结果,尽管已经出现了一个问题。神经网络在输入数据缩放到零附近的狭窄范围内表现最佳,而在这里我们看到值从零到超过一千。

通常,解决这个问题的方法是使用归一化或标准化函数对数据进行缩放。如果数据遵循钟形曲线(如第二章中描述的管理和理解数据的正常分布),那么使用 R 内置的scale()函数进行标准化可能是有意义的。另一方面,如果数据遵循均匀分布或严重非正态分布,那么将数据归一化到零到一的范围可能更合适。在这种情况下,我们将使用后者。

第三章懒惰学习 – 使用最近邻进行分类 中,我们定义了自己的 normalize() 函数如下:

> normalize <- function(x) {
    return((x - min(x)) / (max(x) - min(x)))
} 

执行此代码后,我们可以使用 lapply() 函数将 normalize() 函数应用于混凝土数据框中的每一列,如下所示:

> concrete_norm <- as.data.frame(lapply(concrete, normalize)) 

为了确认归一化工作正常,我们可以看到现在最小和最大的强度分别为零和一:

> summary(concrete_norm$strength) 
 Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
 0.0000  0.2664  0.4001  0.4172  0.5457  1.0000 

与此相比,原始的最小值和最大值分别为 2.33 和 82.60:

> summary(concrete$strength) 
 Min.  1st Qu.  Median    Mean   3rd Qu.    Max. 
   2.33   23.71    34.45   35.82    46.13     82.60 

在训练模型之前应用于数据的任何转换都必须反向应用,以便将其转换回原始的单位。为了便于缩放,保存原始数据或至少原始数据的摘要统计信息是明智的。

沿用原始出版物中 Yeh 的先例,我们将数据分为包含 75% 示例的训练集和包含 25% 的测试集。我们使用的 CSV 文件已经随机排序,所以我们只需将其分为两部分:

> concrete_train <- concrete_norm[1:773, ]
> concrete_test <- concrete_norm[774:1030, ] 

我们将使用训练数据集来构建神经网络,并使用测试数据集来评估模型对未来结果的泛化能力。由于神经网络很容易过拟合,这一步非常重要。

第 3 步 – 在数据上训练模型

为了模拟混凝土中使用的成分与最终产品强度之间的关系,我们将使用多层前馈神经网络。Stefan Fritsch 和 Frauke Guenther 的 neuralnet 包提供了一个标准且易于使用的此类网络实现。它还提供了一个用于绘制网络拓扑的功能。因此,neuralnet 实现是学习更多关于神经网络的一个很好的选择,尽管这并不意味着它不能用于完成实际工作——它是一个非常强大的工具,你很快就会看到。

在 R 中训练简单的 ANN 模型有几个其他常用的包,每个包都有其独特的优点和缺点。由于它作为标准 R 安装的一部分提供,nnet 包可能是引用最多的 ANN 实现。它使用比标准反向传播稍微复杂一点的算法。另一个选择是 RSNNS 包,它提供了一个完整的神经网络功能套件,缺点是它更难学习。构建或使用深度学习神经网络的专用软件在第十五章 利用大数据 中介绍。

由于 neuralnet 不包含在基础 R 中,您需要通过输入 install.packages("neuralnet") 来安装它,并使用 library(neuralnet) 命令加载它。包含的 neuralnet() 函数可以用于使用以下语法训练用于数值预测的神经网络:

文本,字母  描述自动生成

图 7.11:神经网络语法

我们将开始使用默认设置训练最简单的多层前馈网络,仅使用单个隐藏节点。由于训练人工神经网络的过程涉及随机化,这里使用的 set.seed() 函数将确保在运行 neuralnet() 函数时产生相同的结果:

> set.seed(12345)
> concrete_model <- neuralnet(strength ~ cement + slag +
  ash + water + superplastic + coarseagg + fineagg + age,
  data = concrete_train) 

然后,我们可以使用结果模型对象的 plot() 函数来可视化网络拓扑:

> plot(concrete_model) 

图描述自动生成

图 7.12:简单多层前馈网络的拓扑可视化

在这个简单模型中,每个特征都有一个输入节点,接着是一个单一的隐藏节点和一个单一的输出节点,该输出节点预测混凝土强度。每个连接的权重也被描绘出来,以及由标记为数字 1 的节点表示的偏差项。偏差项是数值常数,允许指示节点的值向上或向下移动,就像线性方程中的截距一样。在形式为 y = ax + b 的线性方程中,截距 b 允许当 x = 0 时,y 有一个非零的值。同样,神经网络中的偏差项允许当输入为零时,节点传递一个非零的值。这为学习数据中找到的真实模式提供了更多的灵活性,从而有助于模型更好地拟合。在具体到我们的混凝土模型的情况下,尽管在现实世界中不可能所有输入到混凝土中的因素(如水泥、年龄和水)都为零,但我们不一定会期望强度在接近零的这些因素值时,强度会精确地穿过原点。我们可能期望体重与年龄的关系模型具有一个大于零的偏差项,因为出生时的体重(年龄 = 0)是大于零的。

一个具有单个隐藏节点的神经网络可以被视为我们在 第六章 中研究的线性回归模型的远亲,即 预测数值数据 – 回归方法。每个输入节点与隐藏节点之间的权重类似于 beta 系数,而偏差项的权重类似于截距。如果使用线性激活函数,神经网络几乎就是线性回归。然而,一个关键的区别是,人工神经网络使用梯度下降进行训练,而线性回归通常使用最小二乘法。

在图的下部,R 报告了训练步骤的数量和一个称为平方误差和SSE)的错误度量,正如你可能预期的,它是预测值和实际值之间平方差的和。SSE 越低,模型与训练数据的符合度越高,这告诉我们关于训练数据的表现,但很少告诉我们它在未见数据上的表现。

第 4 步 – 评估模型性能

网络拓扑图让我们窥视了人工神经网络(ANN)的黑箱,但它并没有提供太多关于模型如何适应未来数据的信息。为了在测试数据集上生成预测,我们可以使用compute()函数如下:

> model_results <- compute(concrete_model, concrete_test[1:8]) 

compute() 函数与我们迄今为止使用的 predict() 函数工作方式略有不同。它返回一个包含两个组件的列表:$neurons,它存储网络中每一层的神经元,以及$net.result,它存储预测值。我们想要后者:

> predicted_strength <- model_results$net.result 

由于这是一个数值预测问题而不是分类问题,我们不能使用混淆矩阵来检查模型精度。相反,我们将测量我们预测的混凝土强度与真实值之间的相关性。如果预测值和实际值高度相关,则该模型很可能是混凝土强度的有用衡量标准。

记住,cor() 函数用于获取两个数值向量之间的相关性:

> cor(predicted_strength, concrete_test$strength) 
 [,1]
[1,] 0.8064656 

接近 1 的相关性表明两个变量之间存在强烈的线性关系。因此,这里大约 0.806 的相关性表明存在相当强的关系。这表明我们的模型做得相当不错,即使只有一个隐藏节点。然而,鉴于我们只使用了一个隐藏节点,我们很可能会提高我们模型的表现。让我们尝试做得更好。

第 5 步 - 提高模型性能

由于具有更复杂拓扑结构的网络能够学习更复杂的概念,让我们看看当我们把隐藏节点的数量增加到五个时会发生什么。我们像以前一样使用neuralnet()函数,但添加参数hidden = 5。请注意,由于这个神经网络复杂性的增加,根据您计算机的能力,新的模型可能需要 30 到 60 秒来训练:

> set.seed(12345)
> concrete_model2 <- neuralnet(strength ~ cement + slag +
                               ash + water + superplastic +
                               coarseagg + fineagg + age,
                               data = concrete_train, hidden = 5) 

再次绘制网络图,我们发现连接数量急剧增加。这对性能有何影响?

> plot(concrete_model2) 

图描述自动生成

图 7.13:具有额外隐藏节点的神经网络的拓扑可视化

注意,报告的误差(再次通过 SSE 测量)已从先前模型的 5.08 降低到这里的 1.63。此外,训练步骤的数量从 4,882 增加到 86,849,考虑到模型变得更加复杂,这一点并不令人惊讶。更复杂的网络需要更多的迭代来找到最优权重。

将相同的步骤应用于将预测值与真实值进行比较,我们现在获得大约 0.92 的相关性,这比之前单个隐藏节点时的 0.80 结果有相当大的改进:

> model_results2 <- compute(concrete_model2, concrete_test[1:8])
> predicted_strength2 <- model_results2$net.result
> cor(predicted_strength2, concrete_test$strength) 
 [,1]
[1,] 0.9244533426 

尽管有这些显著的改进,我们仍然可以做一些更多的事情来尝试提高模型的表现。特别是,我们有能力添加额外的隐藏层并更改网络的激活函数。在做出这些更改时,我们为一个非常简单的深度神经网络(DNN)奠定了基础。

激活函数的选择通常对深度学习非常重要。对于特定的学习任务,最佳函数通常是通过实验确定的,然后在机器学习研究社区中更广泛地共享。随着深度学习研究的深入,一种称为rectifier的激活函数因其成功应用于图像识别等复杂任务而变得极为流行。使用 rectifier 激活函数的神经网络中的节点被称为rectified linear unitReLU)。如图所示,ReLU 激活函数的定义是,当x至少为零时返回x,否则返回零。这种函数在深度学习中的流行是因为它非线性,但具有简单的数学性质,这使得它在计算上既便宜又高效,非常适合梯度下降。不幸的是,其导数在x = 0时未定义,因此不能与neuralnet()函数一起使用。

相反,我们可以使用 ReLU 的平滑近似,称为softplusSmoothReLU,这是一个定义为log(1 + e^x)的激活函数。如图所示,当x小于零时,softplus 函数几乎为零,而当x大于零时,大约等于x

图表,折线图  自动生成的描述

图 7.14:softplus 激活函数提供了 ReLU 的平滑、可微近似的描述

要在 R 中定义softplus()函数,请使用以下代码:

> softplus <- function(x) { log(1 + exp(x)) } 

可以通过act.fct参数将此激活函数提供给neuralnet()。此外,我们还将通过将hidden参数设置为整数向量c(5, 5)来添加一个包含五个节点的第二隐藏层。这创建了一个两层网络,每层都有五个节点,都使用 softplus 激活函数。和之前一样,这可能需要一分钟或更长时间来运行:

> set.seed(12345)
> concrete_model3 <- neuralnet(strength ~ cement + slag +
                               ash + water + superplastic +
                               coarseagg + fineagg + age,
                               data = concrete_train,
                               hidden = c(5, 5),
                               act.fct = softplus) 

网络可视化现在显示了一个由每个包含五个节点的两层隐藏层组成的拓扑结构:

> plot(concrete_model3) 

图表  自动生成的描述

图 7.15:使用软 plus 激活函数,用两层隐藏节点可视化我们的网络

再次,让我们计算预测值和实际混凝土强度之间的相关性:

> model_results3 <- compute(concrete_model3, concrete_test[1:8])
> predicted_strength3 <- model_results3$net.result
> cor(predicted_strength3, concrete_test$strength) 
 [,1]
[1,] 0.9348395359 

预测值和实际强度之间的相关性为 0.935,这是我们迄今为止的最佳性能。有趣的是,在原始出版物中,Yeh 报告的相关性为 0.885。这意味着我们只需付出相对较小的努力,就能匹配甚至超过领域专家的性能。当然,Yeh 的结果是在 1998 年发表的,这使我们能够受益于 25 年额外的神经网络研究!

有一个重要的事情需要注意,因为我们训练模型之前已经对数据进行归一化处理,所以预测也是在从零到一的归一化尺度上。例如,以下代码显示了一个数据框,它将原始数据集的混凝土强度值与其对应的预测值并排比较:

> strengths <- data.frame(
    actual = concrete$strength[774:1030],
    pred = predicted_strength3
  ) 
> head(strengths, n = 3)
    actual         pred
774  30.14 0.2860639091
775  44.40 0.4777304648
776  24.50 0.2840964250 

使用相关性作为性能指标,归一化或未归一化数据的选择不会影响结果。例如,无论预测强度与原始、未归一化的混凝土强度值(strengths$actual)还是与归一化值(concrete_test$strength)进行比较,相关性为 0.935 都是相同的:

> cor(strengths$pred, strengths$actual) 
[1] 0.9348395 
> cor(strengths$pred, concrete_test$strength) 
[1] 0.9348395 

然而,如果我们计算不同的性能指标,例如预测值和实际值之间的百分比差异,那么选择的比例就会相当重要。

在这个前提下,我们可以创建一个unnormalize()函数,它反转了 min-max 归一化过程,并允许我们将归一化的预测值转换为原始尺度:

> unnormalize <- function(x) {
    return(x * (max(concrete$strength) -
            min(concrete$strength)) + min(concrete$strength))
} 

在将自定义的unnormalize()函数应用于预测之后,我们可以看到新的预测值(pred_new)与原始混凝土强度值处于相似的尺度上。这使我们能够计算一个有意义的百分比误差值。得到的error_pct是真实值和预测值之间的百分比差异:

> strengths$pred_new <- unnormalize(strengths$pred)
> strengths$error_pct <- (strengths$pred_new - strengths$actual) / 
                            strengths$actual 
> head(strengths, n = 3)
    actual      pred pred_new  error_pct
774  30.14 0.2860639 25.29235 -0.16083776
775  44.40 0.4777305 40.67742 -0.08384179
776  24.50 0.2840964 25.13442 -0.02589470 

出乎意料的是,尽管反转了归一化,相关性仍然保持不变:

> cor(strengths$pred_new, strengths$actual) 
[1] 0.9348395 

当将神经网络应用于自己的项目时,你需要执行一系列类似的步骤,以将数据返回到其原始尺度。

你可能会发现,随着神经网络应用于更具挑战性的学习任务,它们会迅速变得更加复杂。例如,你可能会遇到所谓的消失梯度问题和与之密切相关的爆炸梯度问题,在这些问题中,由于无法在合理的时间内收敛,反向传播算法无法找到有用的解决方案。

作为这些问题的补救措施,人们可能会尝试改变隐藏节点的数量,应用不同的激活函数,如 ReLU,调整学习率等等。?neuralnet帮助页面提供了更多关于可以调整的各种参数的信息。然而,这又导致另一个问题,即测试许多参数成为构建高性能模型的一个瓶颈。这是人工神经网络(ANNs)和,尤其是深度神经网络(DNNs)的权衡:利用它们的巨大潜力需要巨大的时间和计算资源投入。

正如生活中更普遍的情况一样,在机器学习中,我们可以权衡时间和金钱。使用付费的云计算资源,如亚马逊网络服务AWS)和微软 Azure,可以更快地构建更复杂的模型或测试许多模型。

理解支持向量机

可以将支持向量机SVM)想象为在多维空间中绘制数据点所形成的表面,该空间代表示例及其特征值。SVM 的目标是创建一个称为超平面的平坦边界,将空间分割成两侧相对均匀的分区。通过这种方式,SVM 学习结合了第三章中介绍的基于实例的最近邻学习(Lazy Learning – Classification Using Nearest Neighbors)和第六章中描述的线性回归建模(Forecasting Numeric Data – Regression Methods)的方面。这种组合非常强大,使得 SVM 能够模拟高度复杂的关系。

尽管驱动 SVM 的基本数学原理已经存在了几十年,但自从机器学习社区采用它们之后,对它们的兴趣大大增加。在关于困难学习问题的知名成功故事以及获奖的 SVM 算法在许多编程语言(包括 R)中得到实现之后,它们的受欢迎程度爆炸式增长。因此,SVM 被广泛采用,这可能会让那些否则无法应用实现 SVM 所需的相对复杂数学的受众。好消息是,尽管数学可能很难,但基本概念是可理解的。

支持向量机(SVMs)可以适应用于几乎任何类型的机器学习任务,包括分类和数值预测。该算法的关键成功之一在于模式识别。值得注意的应用包括:

  • 在生物信息学领域对微阵列基因表达数据进行分类,以识别癌症或其他遗传疾病

  • 文本分类,例如识别文档中使用的语言或根据主题内容对文档进行分类

  • 检测罕见但重要的事件,如发动机故障、安全漏洞或地震

当用于二元分类时,SVM 最容易理解,这也是该方法传统上应用的方式。因此,在接下来的部分中,我们将仅关注 SVM 分类器。这里提出的原则也适用于将 SVM 适应于数值预测。

使用超平面进行分类

如前所述,SVM 使用称为超平面的边界将数据分组为具有相似类值的组。例如,以下图展示了在二维和三维空间中分离圆形和正方形组的超平面。因为圆形和正方形可以通过一条直线或平坦的表面完美地分开,所以它们被称为线性可分。最初,我们将考虑这种情况是简单的情况,但 SVM 也可以扩展到点不是线性可分的问题。

图片

图 7.16:在二维和三维空间中,正方形和圆形都是线性可分的

为了方便起见,超平面在二维空间中被传统地描绘为一条线,但这仅仅是因为在超过 2 维的空间中很难描绘空间。实际上,超平面是高维空间中的一个平面——这是一个可能难以理解的概念。

在二维空间中,SVM 算法的任务是识别一条将两个类别分开的线。如图所示,在圆和正方形的组之间有不止一条分割线。三种这样的可能性被标记为abc。算法是如何选择的?

图描述自动生成,置信度低

图 7.17:许多可能的分割正方形和圆的线中的三条

那个问题的答案涉及到寻找最大间隔超平面MMH)以在两个类别之间创建最大的分离。尽管任何三条分割圆和正方形的线都可以正确分类所有数据点,但预期导致最大分离的线将最好地推广到未来的数据。最大间隔将提高即使添加随机噪声,每个类别仍然保持在边界一侧的机会。

支持向量(如图中箭头所示)是每个类别中最接近 MMH 的点。每个类别至少必须有一个支持向量,但可能有多个。支持向量本身定义了 MMH。这是 SVM 的一个关键特性;支持向量提供了一种非常紧凑的方式来存储分类模型,即使特征数量非常大。

图描述自动生成

图 7.18:MMH 由支持向量定义

识别支持向量的算法依赖于向量几何,并涉及到一些超出本书范围的复杂数学。然而,这个过程的基本原理是直接的。

更多关于 SVM 数学的信息可以在经典论文《支持向量机,Cortes, C 和 Vapnik, V,机器学习,1995,第 20 卷,第 273-297 页》中找到。入门级别的讨论可以在《支持向量机:炒作还是赞美?,Bennett, KP 和 Campbell, C,SIGKDD Explorations,2000,第 2 卷,第 1-13 页》中找到。更深入的探讨可以在《支持向量机,Steinwart, I 和 Christmann, A,纽约:Springer,2008》中找到。

线性可分数据的案例

在假设类别是线性可分的情况下,找到最大间隔是最容易的。在这种情况下,最大间隔距离两组数据点的外部边界尽可能远。这些外部边界被称为凸包。最大间隔是两个凸包之间最短路径的垂直平分线。使用称为二次优化的技术的高级计算机算法可以通过这种方式找到最大间隔。

图描述自动生成

图 7.19:最大间隔超平面是凸包之间最短路径的垂直平分线

另一种(但等效)的方法是通过搜索每个可能超平面的空间来找到一组平行平面,这些平面将点分成同质组,同时它们彼此尽可能远。用比喻来说,可以想象这个过程就像尝试找到可以塞进你卧室楼梯井的最厚的床垫。

要理解这个搜索过程,我们需要精确地定义我们所说的超平面。在 n-维空间中,以下方程被使用:

如果你对这个符号不熟悉,字母上方的箭头表示它们是向量而不是单个数字。特别是,w 是一个包含 n 个权重的向量,即 {w[1], w[2], ..., w[n]},而 b 是一个称为偏置的单个数字。偏置在概念上等同于在第六章“预测数值数据 - 回归方法”中讨论的斜截式中的截距项。

如果你难以想象多维空间中的平面,不必担心细节。只需将方程视为指定一个表面的方式,就像斜截式(y = mx + b)在二维空间中用于指定直线一样。

使用这个公式,该过程的目的是找到一组权重,以指定两个超平面,如下所示:

我们还要求这些超平面被指定得使得一个类别的所有点都位于第一个超平面上方,而另一个类别的所有点都位于第二个超平面下方。这两个平面应该产生一个间隙,使得两个平面之间的空间中没有来自任一类的点。只要数据是线性可分的,这是可能的。向量几何定义了这两个平面之间的距离——我们希望尽可能大的距离——如下:

这里,||w|| 表示 欧几里得范数(从原点到向量 w 的距离)。因为 ||w|| 是分母,为了最大化距离,我们需要最小化 ||w||。这个任务通常被重新表述为以下一组约束条件:

虽然这看起来很混乱,但从概念上理解其实并不复杂。基本上,第一行意味着我们需要最小化欧几里得范数(平方并除以二以简化计算)。第二行指出,这受制于(s.t.)每个 y[i] 数据点被正确分类的条件。请注意,y 表示类别值(转换为+1 或-1),倒置的“A”是“对于所有”的简称。

与寻找最大边缘的其他方法一样,找到这个问题的解决方案最好是留给二次优化软件的任务。尽管它可能需要大量的处理器资源,但专门的算法甚至可以在大型数据集上快速解决这个问题。

非线性可分数据的情况

随着我们研究 SVM 背后的理论,你可能想知道房间里的大象:当数据不是线性可分时会发生什么?这个问题的解决方案是使用松弛变量,它创建了一个软边缘,允许一些点落在边缘的错误一侧。下面的图示说明了两个点落在错误一侧的线,以及相应的松弛项(用希腊字母 Xi 表示):

包含文本的图片,时钟  描述自动生成

图 7.20:落在边界错误一侧的点会带来成本惩罚

对所有违反约束的点应用一个成本值(表示为 C),而不是寻找最大边缘,算法试图最小化总成本。因此,我们可以将优化问题修改为:

如果你现在感到困惑,不要担心,你不是一个人。幸运的是,SVM 软件包会乐意为你优化,而无需你理解技术细节。重要的是要理解成本参数 C 的添加。修改这个值将调整落在超平面错误一侧的点的惩罚。成本参数越大,优化尝试达到 100%分离的努力就越大。另一方面,较低的成本参数将强调更宽的整体边缘。为了创建一个对未来数据泛化良好的模型,重要的是在这两者之间取得平衡。

用于非线性空间的核函数

在许多现实世界的数据集中,变量之间的关系是非线性的。正如我们刚刚发现的,通过添加松弛变量,SVM 仍然可以在这种数据上训练,这允许一些示例被错误分类。然而,这并不是解决非线性问题的唯一方法。SVM 的一个关键特性是它们能够使用称为核技巧的过程将问题映射到更高维的空间。在这个过程中,非线性关系可能突然显得非常线性。

虽然这听起来可能有些荒谬,但实际上用例子来说明非常简单。在下面的图中,左边的散点图描绘了天气类别(晴朗或雪)与两个特征:纬度和经度之间的非线性关系。图中中心的点是雪类别成员,而边缘的点都是晴朗的。这样的数据可能来自一组天气预报,其中一些是从山顶附近的站点获得的,而另一些是从山脉底部附近的站点获得的。

图描述自动生成

图 7.21:核技巧可以帮助将非线性问题转化为线性问题

在图右侧,在应用核技巧之后,我们通过新维度来观察数据:高度。通过添加这个特征,类别现在可以完美地线性分离。这是可能的,因为我们获得了对数据的新视角。在左侧的图中,我们是从鸟瞰的角度观察山脉,而在右侧,我们是从地面水平距离观察山脉。在这里,趋势很明显:在较高的海拔处发现了雪天气候。

以这种方式,具有非线性核的 SVM 通过向数据添加额外维度来创建分离。本质上,核技巧涉及构建新特征的过程,这些特征表达了测量特征之间的数学关系。例如,高度特征可以数学上表示为纬度和经度之间的相互作用——点越接近每个尺度中心,高度就越大。这使得 SVM 能够学习原始数据中未明确测量的概念。

具有非线性核的 SVM 是极其强大的分类器,尽管它们也有一些缺点,如下表所示:

优点缺点

|

  • 可用于分类或数值预测问题

  • 不太受噪声数据的影响,并且不太容易过拟合

  • 可能比神经网络更容易使用,尤其是由于存在几个得到良好支持的 SVM 算法

  • 由于其在数据挖掘竞赛中的高准确率和显眼的胜利而受到欢迎

|

  • 寻找最佳模型需要测试各种核和模型参数的组合

  • 训练可能较慢,尤其是如果输入数据集具有大量特征或示例

  • 导致产生复杂的黑盒模型,难以解释,甚至可能无法解释

|

核函数通常具有以下形式。由希腊字母 phi 表示的函数,即图片,是将数据映射到另一个空间的映射。因此,一般的核函数会对特征向量 x[i] 和 x[j] 进行一些变换,并使用点积将它们结合起来,点积将两个向量作为输入并返回一个单一数值:

图片

使用这种形式,已经为许多不同的领域开发了核函数。以下列出了最常用的几个核函数。几乎所有的 SVM 软件包都将包括这些核函数以及其他许多核函数。

线性核根本不转换数据。因此,它可以简单地表示为特征的点积:

图片

多项式核将数据添加一个简单的非线性变换:

图片

Sigmoid 核导致 SVM 模型类似于使用 sigmoid 激活函数的神经网络。希腊字母 kappa 和 delta 用作核参数:

图片

高斯径向基函数核类似于径向基神经网络。径向基函数核在许多类型的数据上表现良好,并被认为是为许多学习任务提供一个合理的起点:

图片

没有可靠的规则来匹配核函数与特定的学习任务。拟合程度很大程度上取决于要学习的概念、训练数据量以及特征之间的关系。通常,需要在验证数据集上训练和评估几个 SVM 来尝试和错误地找到合适的核函数。尽管如此,在许多情况下,核函数的选择是任意的,因为性能可能只有细微的差异。为了了解这在实践中是如何工作的,让我们将我们对 SVM 分类的理解应用到现实世界的问题中。

示例 - 使用 SVM 进行 OCR

对于许多机器学习算法来说,图像处理是一个困难的任务。将像素模式与更高层次概念联系起来的关系极其复杂且难以定义。例如,人类很容易识别人脸、猫或字母“A”,但在严格的规则中定义这些模式是困难的。此外,图像数据通常很嘈杂。根据光线、方向和主题的位置,图像的捕获可能会有许多细微的变化。

SVM 非常适合解决图像数据的挑战。它们能够学习复杂的模式,同时对噪声不太敏感,可以以高精度识别视觉模式。此外,SVM 的关键弱点——黑盒模型表示——对于图像处理来说不那么关键。如果一个 SVM 能够区分猫和狗,那么它如何做到这一点并不重要。

在本节中,我们将开发一个模型,类似于通常与桌面文档扫描仪或智能手机应用程序捆绑的光学字符识别(OCR)软件的核心。此类软件的目的是通过将打印或手写文本转换为电子形式以保存到数据库中来处理基于纸张的文档。

当然,这是一个困难的问题,因为手写风格和印刷字体有很多变体。尽管如此,软件用户期望完美无缺,因为错误或打字错误可能导致在商业环境中尴尬或昂贵的错误。让我们看看我们的 SVM 是否能够胜任这项任务。

第一步 – 收集数据

当 OCR 软件首次处理文档时,它会将纸张划分为一个矩阵,使得网格中的每个单元格都包含一个单个符号,这是一个指代字母、符号或数字的术语。接下来,对于每个单元格,软件将尝试将符号与它所识别的所有字符集合进行匹配。最后,单个字符可以组合成单词,这些单词可以选择性地与文档语言的词典进行拼写检查。

在这个练习中,我们假设我们已经开发出了将文档划分为矩形区域的算法,每个区域只包含一个符号。我们还将假设文档只包含英语字母。因此,我们将模拟一个将符号与 26 个字母之一(从 A 到 Z)匹配的过程。

为了达到这个目的,我们将使用 W. Frey 和 D. J. Slate 捐赠给 UCI 机器学习仓库(archive.ics.uci.edu/ml)的数据集。该数据集包含 20,000 个示例,展示了使用 20 种不同随机变形和扭曲的黑白字体打印的 26 个英文字母大写字母。

关于这些数据的更多信息,请参阅使用荷兰风格自适应分类器的字母识别,Slate, DJ 和 Frey, PW,机器学习,1991,第 6 卷,第 161-182 页

下图由 Frey 和 Slate 发布,提供了一些打印符号的示例。以这种方式扭曲后,字母对计算机来说具有挑战性,但对人类来说很容易识别:

文本描述自动生成

图 7.22:SVM 算法将尝试识别的符号示例

第二步 – 探索和准备数据

根据 Frey 和 Slate 提供的文档,当符号被扫描到计算机中时,它们被转换为像素,并记录了 16 个统计属性。

属性衡量诸如符号的水平尺寸和垂直尺寸、黑色(相对于白色)像素的比例以及像素的平均水平和垂直位置等特征。据推测,盒子不同区域黑色像素浓度的差异应该提供一种区分 26 个字母的方法。

要跟随这个示例,请从 Packt Publishing 网站下载letterdata.csv文件,并将其保存到您的 R 工作目录中。

将数据读入 R 中,我们确认我们已经收到了定义字母类每个示例的 16 个特征的完整数据。正如预期的那样,它有 26 个级别:

> letters <- read.csv("letterdata.csv", stringsAsFactors = TRUE)
> str(letters) 
'data.frame':	20000 obs. of  17 variables:
 $ letter: Factor w/ 26 levels "A","B","C","D",..: 20 9 4 14 ...
 $ xbox  : int  2 5 4 7 2 4 4 1 2 11 ...
 $ ybox  : int  8 12 11 11 1 11 2 1 2 15 ...
 $ width : int  3 3 6 6 3 5 5 3 4 13 ...
 $ height: int  5 7 8 6 1 8 4 2 4 9 ...
 $ onpix : int  1 2 6 3 1 3 4 1 2 7 ...
 $ xbar  : int  8 10 10 5 8 8 8 8 10 13 ...
 $ ybar  : int  13 5 6 9 6 8 7 2 6 2 ...
 $ x2bar : int  0 5 2 4 6 6 6 2 2 6 ...
 $ y2bar : int  6 4 6 6 6 9 6 2 6 2 ...
 $ xybar : int  6 13 10 4 6 5 7 8 12 12 ...
 $ x2ybar: int  10 3 3 4 5 6 6 2 4 1 ...
 $ xy2bar: int  8 9 7 10 9 6 6 8 8 9 ...
 $ xedge : int  0 2 3 6 1 0 2 1 1 8 ...
 $ xedgey: int  8 8 7 10 7 8 8 6 6 1 ...
 $ yedge : int  0 4 3 2 5 9 7 2 1 1 ...
 $ yedgex: int  8 10 9 8 10 7 10 7 7 8 ... 

SVM 学习器需要所有特征都是数值的,并且每个特征都缩放到一个相当小的区间。在这种情况下,每个特征都是整数,因此我们不需要将任何因素转换为数字。另一方面,这些整数变量的范围似乎很宽。这表明我们需要对数据进行归一化或标准化。然而,我们现在可以跳过这一步,因为我们将要使用的 R 包将自动执行缩放。

由于没有剩余的数据准备要执行,我们可以直接进入机器学习过程的训练和测试阶段。在先前的分析中,我们随机地将数据分配到训练集和测试集中。虽然我们也可以这样做,但 Frey 和 Slate 已经随机化了数据,因此建议使用前 16,000 条记录(80%)来构建模型,以及接下来的 4,000 条记录(20%)用于测试。遵循他们的建议,我们可以创建以下训练和测试数据框:

> letters_train <- letters[1:16000, ]
> letters_test  <- letters[16001:20000, ] 

我们的准备工作已经就绪,现在让我们开始构建我们的分类器。

第 3 步 – 在数据上训练模型

当涉及到在 R 中拟合 SVM 模型时,有多个出色的包可供选择。维也纳科技大学(TU Wien)统计学系的e1071包提供了一个 R 接口到获奖的 LIBSVM 库,这是一个广泛使用的开源 SVM 程序,用 C++编写。如果您已经熟悉 LIBSVM,您可能想从这里开始。

关于 LIBSVM 的更多信息,请参考作者网站www.csie.ntu.edu.tw/~cjlin/libsvm/

同样,如果您已经投资于 SVMlight 算法,杜伊斯堡-埃森科技大学(TU Dortmund)统计学系的klaR包提供了直接从 R 使用此 SVM 实现的功能。

关于 SVMlight 的信息,请参阅www.cs.cornell.edu/people/tj/svm_light/

最后,如果您是从零开始,那么最好从kernlab包中的 SVM 函数开始。这个包的一个有趣的优势是它是在 R 中本地开发的,而不是在 C 或 C++中,这使得它可以很容易地进行定制;内部没有任何东西隐藏在幕后。也许更重要的是,与其他选项不同,kernlab可以与caret包一起使用,这使得可以使用各种自动化方法(在第十四章构建更好的学习者中介绍)来训练和评估 SVM 模型。

要更全面地了解kernlab,请参阅作者在www.jstatsoft.org/v11/i09/上的论文。

使用kernlab训练 SVM 分类器的语法如下。如果您确实在使用其他包,命令在很大程度上是相似的。默认情况下,ksvm()函数使用高斯 RBF 核,但还提供了许多其他选项:

文本,字母,描述自动生成

图 7.23:SVM 语法

为了提供一个 SVM 性能的基线度量,让我们首先训练一个简单的线性 SVM 分类器。如果您还没有安装,请使用install.packages("kernlab")命令将kernlab包安装到您的库中。然后,我们可以在训练数据上调用ksvm()函数,并使用vanilladot选项指定线性(即“纯”)核,如下所示:

> library(kernlab)
> letter_classifier <- ksvm(letter ~ ., data = letters_train,
                            kernel = "vanilladot") 

根据您计算机的性能,此操作可能需要一些时间才能完成。完成后,输入存储模型的名称,以查看有关训练参数和模型拟合的一些基本信息:

> letter_classifier 
Support Vector Machine object of class "ksvm"
SV type: C-svc  (classification)
 parameter : cost C = 1
Linear (vanilla) kernel function.
Number of Support Vectors : 7037
Objective Function Value : -14.1746 -20.0072 -23.5628 -6.2009 -7.5524
-32.7694 -49.9786 -18.1824 -62.1111 -32.7284 -16.2209...
Training error : 0.130062 

这些信息对我们了解模型在现实世界中的表现帮助甚微。我们需要检查它在测试数据集中的表现,才能知道它是否很好地泛化到未见过的数据。

第 4 步 – 评估模型性能

predict()函数允许我们使用字母分类模型对测试数据集进行预测:

> letter_predictions <- predict(letter_classifier, letters_test) 

由于我们没有指定type参数,因此使用了默认的type = "response"。这返回一个包含测试数据中每行值预测字母的向量。使用head()函数,我们可以看到前六个预测字母分别是UNVXNH

> head(letter_predictions) 
[1] U N V X N H
Levels: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 

为了检查我们的分类器表现如何,我们需要将预测字母与测试数据集中的真实字母进行比较。我们将使用table()函数来完成此操作(此处仅显示部分完整表格):

> table(letter_predictions, letters_test$letter) 
letter_predictions   A   B   C   D   E
                 A 144   0   0   0   0
                 B   0 121   0   5   2
                 C   0   0 120   0   4
                 D   2   2   0 156   0
                 E   0   0   5   0 127 

对角线上的144121120156127值表示预测字母与真实值匹配的记录总数。同样,错误数量也被列出。例如,行B和列D中的5表示有 5 个案例,其中字母D被错误地识别为B

单独查看每种类型的错误可能会揭示模型在特定字母类型上遇到困难的一些有趣模式,但这很耗时。我们可以通过计算整体准确率来简化评估。这仅考虑预测是否正确或错误,而忽略错误类型。

以下命令返回一个包含TRUEFALSE值的向量,指示模型预测的字母是否与测试数据集中的实际字母相符(即匹配):

> agreement <- letter_predictions == letters_test$letter 

使用table()函数,我们看到分类器在 4000 个测试记录中的 3357 个中正确识别了字母:

> table(agreement) 
agreement
FALSE  TRUE
643 3357 

从百分比的角度来看,准确率大约是 84%:

> prop.table(table(agreement)) 
agreement
  FALSE    TRUE
0.16075 0.83925 

注意,当 Frey 和 Slate 在 1991 年发布数据集时,他们报告的识别准确率约为 80%。仅用几行 R 代码,我们就能够超越他们的结果,尽管我们也受益于数十年的额外机器学习研究。考虑到这一点,我们很可能会做得更好。

第 5 步 - 提高模型性能

让我们花点时间来了解一下我们训练的 SVM 模型在从图像数据中识别字母时的性能。通过一行 R 代码,该模型能够达到近 84%的准确率,略高于 1991 年学术研究人员发布的基准百分比。尽管 84%的准确率远远不足以用于 OCR 软件,但相对简单的模型能够达到这个水平本身就是一项了不起的成就。请记住,模型预测与实际值仅靠运气匹配的概率相当小,不到四%。这表明我们的模型的表现比随机机会高出 20 倍以上。尽管如此出色,也许通过调整 SVM 函数参数来训练一个稍微复杂一些的模型,我们还可以发现该模型在现实世界中是有用的。

为了计算 SVM 模型预测与实际值偶然匹配的概率,应用第四章中涵盖的独立事件的联合概率规则,即朴素贝叶斯分类的概率学习。因为测试集中有 26 个字母,每个字母出现的频率大约相同,所以任何单个字母被正确预测的概率是*(1 / 26) * (1 / 26)。由于有 26 个不同的字母,总的一致性概率是26 * (1 / 26) * (1 / 26) = 0.0384*,或 3.84%。

改变 SVM 核函数

我们之前的 SVM 模型使用了简单的线性核函数。通过使用更复杂的核函数,我们可以将数据映射到更高维的空间,并可能获得更好的模型拟合。

然而,从众多不同的核函数中选择可能具有挑战性。一个流行的惯例是从高斯 RBF 核开始,它已被证明对许多类型的数据表现良好。

我们可以使用ksvm()函数训练基于 RBF 的 SVM,如下所示。请注意,与之前使用的方法类似,我们需要设置随机种子以确保结果可重复:

> set.seed(12345)
> letter_classifier_rbf <- ksvm(letter ~ ., data = letters_train,
                                kernel = "rbfdot") 

接下来,我们像之前一样进行预测:

> letter_predictions_rbf <- predict(letter_classifier_rbf,
                                    letters_test) 

最后,我们将比较其准确性与我们的线性 SVM:

> agreement_rbf <- letter_predictions_rbf == letters_test$letter
> table(agreement_rbf) 
agreement_rbf
FALSE  TRUE
  278  3722 
> prop.table(table(agreement_rbf)) 
agreement_rbf
  FALSE    TRUE
0.0695 0.9305 

仅通过改变核函数,我们就能够将我们的字符识别模型的准确率从 84%提高到 93%。

确定最佳的 SVM 成本参数

如果这个性能水平对于 OCR 程序来说仍然不满意,当然可以测试额外的核函数。然而,另一种富有成效的方法是改变成本参数,这会修改 SVM 决策边界的宽度。这决定了模型在过拟合和欠拟合训练数据之间的平衡——成本值越大,学习器将越努力地尝试完美地分类每个训练实例,因为每个错误都有更高的惩罚。一方面,高成本可能导致学习器过拟合训练数据。另一方面,设置得太小的成本参数可能导致学习器错过训练数据中的重要、微妙的模式,并欠拟合真实模式。

没有先验的规则可以知道理想值,因此,我们将检查模型在C(成本参数)的各种值下的表现。而不是反复重复训练和评估过程,我们可以使用sapply()函数将自定义函数应用于潜在成本值的向量。我们首先使用seq()函数生成这个向量,它是一个从 5 开始计数到 40 的序列,每次增加 5。然后,如以下代码所示,自定义函数像以前一样训练模型,每次使用成本值并在测试数据集上进行预测。每个模型的准确率是匹配实际值的预测数量除以总预测数量。结果使用plot()函数进行可视化。请注意,根据您计算机的能力,这可能需要几分钟才能完成:

> cost_values <- c(1, seq(from = 5, to = 40, by = 5))
> accuracy_values <- sapply(cost_values, function(x) {
    set.seed(12345)
    m <- ksvm(letter ~ ., data = letters_train,
              kernel = "rbfdot", C = x)
    pred <- predict(m, letters_test)
    agree <- ifelse(pred == letters_test$letter, 1, 0)
    accuracy <- sum(agree) / nrow(letters_test)
    return (accuracy)
  })
> plot(cost_values, accuracy_values, type = "b") 

形状描述自动生成

图 7.24:映射准确率与 RBF 核的 SVM 成本

如可视化所示,在 93%的准确率下,默认的 SVM 成本参数C = 1在评估的 9 个模型中产生了最不准确的模式。相反,将C设置为 10 或更高的值,准确率可达到约 97%,这在性能上是一个相当大的提升!也许这已经足够接近完美,以至于可以在实际环境中部署该模型,尽管仍然值得进一步实验,以查看是否有可能达到 100%的准确率。每次准确率的提升都将减少 OCR 软件的错误,并为最终用户提供更好的整体体验。

摘要

在本章中,我们探讨了两种具有巨大潜力但常因复杂性而被忽视的机器学习方法。希望你现在能看出这种声誉至少在一定程度上是不应得的。驱动人工神经网络(ANNs)和支持向量机(SVMs)的基本概念并不难理解。

另一方面,由于人工神经网络(ANNs)和支持向量机(SVMs)已经存在了几十年,它们各自都有许多变体。本章只是对这些方法可能实现的内容进行了初步探讨。通过利用在这里学到的术语,你应该能够捕捉到每天正在开发的许多进步的细微差别,包括不断发展的深度学习领域。我们将在第十五章“利用大数据”中重新探讨深度学习,看看它如何解决机器学习中最具挑战性的问题。

在过去的几章中,我们学习了多种不同类型的预测模型,从基于简单启发式算法如最近邻算法的模型到复杂的黑盒模型以及其他许多模型。在下一章中,我们将开始考虑另一种学习任务的方法。这些无监督学习技术将在帮助我们找到“大海捞针”的过程中,揭示数据中的迷人模式。

加入我们书籍的 Discord 空间

加入我们的 Discord 社区,与志同道合的人相聚,并在以下地点与超过 4000 人一起学习:

packt.link/r

图片

第八章:寻找模式 - 使用关联规则进行篮子分析

想想你上一次的冲动购买。也许在杂货店的结账通道,你买了一包口香糖或一块巧克力棒。也许在深夜为尿布和配方奶粉的购物之旅中,你买了一瓶含咖啡因的饮料或一箱啤酒。你甚至可能是在书店推荐下买了这本书。这些冲动购买并非巧合,因为零售商使用复杂的数据分析技术来识别用于营销促销和通过产品定位推动交叉销售的有用模式。

在过去,这样的推荐基于市场营销专业人士和库存管理人员的直观直觉。现在,条形码扫描仪、库存数据库和在线购物车都生成交易数据,机器学习可以使用这些数据来学习购买模式。这种做法通常被称为篮子分析,因为它已经被频繁应用于超市数据。

尽管这项技术起源于购物数据,但它也适用于其他环境。在你完成这一章的时候,你将知道如何将篮子分析技术应用于你自己的任务,无论它们是什么。通常,这项工作涉及:

  • 理解交易数据的特殊性

  • 使用简单的性能指标在大数据库中寻找关联

  • 知道如何识别有用和可操作的规律

由于篮子分析能够发现许多类型的大型数据集中的洞察力,当我们应用这项技术时,你可能会发现你的工作中也有应用,即使你没有与零售行业有联系。

理解关联规则

篮子分析的基础是可能出现在任何给定交易中的项目。一组或多个项目被括号包围,以表示它们形成一个集合,或者更具体地说,是一个在数据中出现频率较高的项目集。交易是以项目集来指定的,如下面在典型杂货店可能找到的交易:

{面包, 花生酱, 果酱}

篮子分析的结果是一系列关联规则,这些规则指定了在项目集项目之间的关系中发现的模式。关联规则总是由项目集的子集组成,并通过将规则的一侧的左侧(LHS)与另一侧的右侧(RHS)关联来表示。LHS 是触发规则需要满足的条件,而 RHS 是满足该条件后的预期结果。从前面的示例交易中识别出的规则可能以以下形式表示:

{花生酱, 果酱} → {面包}

用简单的话来说,这个关联规则表明,如果花生酱和果酱一起购买,那么面包也很可能被购买。换句话说,“花生酱和果酱意味着面包。”

在零售交易数据库的背景下开发,关联规则不用于预测,而是用于在大数据库中进行无监督的知识发现。这与前几章中介绍的分类和数值预测算法不同。即便如此,你会发现关联规则学习的结果与第五章中介绍的分类规则学习的结果密切相关,并共享许多特征,即*“分而治之 – 使用决策树和规则进行分类”*。

由于关联规则学习器是无监督的,因此不需要对算法进行训练,数据也不需要在事先进行标记。程序只是简单地应用于数据集,希望找到有趣的相关性。当然,缺点是没有简单的方法可以客观地衡量规则学习器的性能,除了评估其定性有用性——通常,是一种某种形式的目视检查。

尽管关联规则最常用于篮子分析,但它们对于在许多不同类型的数据中寻找模式很有帮助。其他潜在的应用包括:

  • 在癌症数据中寻找有趣的 DNA 和蛋白质序列模式

  • 寻找与欺诈性信用卡或保险使用相结合的购买或医疗索赔模式

  • 识别出导致客户放弃手机服务或升级有线电视套餐的行为组合

关联规则分析用于在大量元素中寻找有趣的相关性。人类能够相当直观地做到这一点,但通常需要专家级知识或大量的经验才能在几分钟或几秒钟内完成规则学习算法所能做到的事情。此外,有些数据集太大、太复杂,以至于人类难以在“大海捞针”中找到所需的信息。

关联规则学习的 Apriori 算法

正如大型交易数据集给人类带来挑战一样,这些数据集也给机器带来了挑战。交易数据集在交易数量以及记录的项目或特征数量上都可以很大。搜索有趣的项目集的基本问题在于,潜在的项目集数量随着项目数量的指数增长。给定可以出现在集合中或不出现在集合中的k个项目,有2^k 种可能的项目集,这些项目集可能是潜在的规则。一个只销售 100 种不同商品的零售商可能需要评估大约2¹⁰⁰ = 1.27e+30个项目集——这看起来是一项看似不可能的任务。

而不是逐个评估这些项集,一个更智能的规则学习算法利用了这样一个事实:在实际情况中,许多潜在的项目组合很少甚至从未被发现。例如,即使一家商店同时销售汽车用品和食品产品,集合*{机油,香蕉}*可能非常罕见。通过忽略这些罕见(也许不那么重要)的组合,可以将搜索规则的范围限制在更易于管理的规模。

已做了大量工作来识别用于减少搜索项集数量的启发式算法。也许最广为人知的用于高效搜索大型数据库规则的算法被称为Apriori。该算法由 Rakesh Agrawal 和 Ramakrishnan Srikant 于 1994 年提出,尽管后来发明了更新、更快的算法,但 Apriori 算法仍然与关联规则学习有些同义。该名称来源于算法利用关于频繁项集属性的一种简单先验(即先验)信念。

在我们更深入地讨论这一点之前,值得注意的是,这个算法,像所有学习算法一样,并非没有其优点和缺点。以下列出了一些:

优点缺点

|

  • 能够处理大量交易数据

  • 生成易于理解的规则

  • 适用于数据挖掘和发现数据库中的意外知识

|

  • 对于相对较小的数据集不太有帮助

  • 需要努力区分真正的洞察力和常识

  • 容易从随机模式中得出错误的结论

|

如前所述,Apriori 算法采用一种简单的先验信念作为减少关联规则搜索空间的指导原则:频繁项集的所有子集也必须是频繁的。这种启发式方法被称为Apriori 属性。利用这一精明的观察,可以显著减少需要搜索的规则数量。例如,集合*{机油,香蕉}只有在{机油}{香蕉}都频繁出现的情况下才能是频繁的。因此,如果{机油}{香蕉}*不频繁,那么包含这些项目的任何集合都可以从搜索中排除。

关于 Apriori 算法的更多详细信息,请参阅Agrawal, R., Srikant, R.,《快速挖掘关联规则算法》,第 20 届国际非常大型数据库会议论文集,1994 年,第 487-499 页

为了看到这一原则如何在更现实的设置中应用,让我们考虑一个简单的交易数据库。以下表格显示了在一个虚构医院礼品店中完成的五个交易:

图片

图 8.1:表示假设医院礼品店中五个交易的项集

通过观察购买集合,我们可以推断出存在几种典型的购买模式。一个人去看望生病的亲朋好友时,往往会购买一张祝福卡和鲜花,而看望新妈妈的访客则倾向于购买毛绒玩具熊和气球。这些模式值得关注,因为它们出现的频率足够高,足以引起我们的兴趣;我们只需运用一点逻辑和专业知识来解释这些规则。

类似地,Apriori 算法使用项集“有趣性”的统计指标在更大的交易数据库中定位关联规则。在接下来的章节中,我们将发现 Apriori 如何计算这些兴趣度量,以及它们如何与 Apriori 属性结合以减少要学习的规则数量。

测量规则兴趣——支持度和置信度

一个关联规则是否被认为是有趣的,由两个统计指标决定:支持度和置信度。通过为这些指标提供最小阈值并应用 Apriori 原则,可以很容易地大幅减少报告的规则数量。如果这个限制过于严格,可能会导致只有最明显或常识性的规则被识别。因此,了解在这些标准下被排除的规则类型非常重要,以便获得正确的平衡。

项集或规则的支持度衡量它在数据中出现的频率。例如,项集*{祝福卡,鲜花}在医院的礼品店数据中的支持度为3/5 = 0.6*。同样,项集*{祝福卡} {鲜花}的支持度也是0.6*。支持度可以计算任何项集或单个项;例如,项集*{巧克力棒}的支持度为2/5 = 0.4*,因为巧克力棒出现在 40%的购买中。可以定义一个函数来定义项集X的支持度:

这里,N是数据库中的交易数量,而count(X)是包含项集X的交易数量。

规则的置信度是对其预测能力或准确性的衡量。它定义为包含XY的项集的支持度除以只包含X的项集的支持度:

实质上,置信度告诉我们交易中存在项或项集X导致存在项或项集Y的比例。记住,X导致Y的置信度与Y导致X的置信度不同。

例如,{flowers} {get-well card}的置信度为0.6 / 0.8 = 0.75。相比之下,{get-well card} {flowers}的置信度为0.6 / 0.6 = 1.0。这意味着购买鲜花 75%的时间也包括购买祝福卡,而购买祝福卡 100%的时间也包括鲜花。这些信息对礼品店的管理可能非常有用。

您可能已经注意到了支持度、置信度和在第四章“概率学习 - 使用朴素贝叶斯进行分类”中介绍的贝叶斯概率规则之间的相似性。事实上,support(A, B)P(A B)相同,而confidence(A *B)P(B | A)*相同。只是上下文不同。

类似于*{get-well card} {flowers}*的规则被称为强规则,因为它们既有高的支持度又有高的置信度。找到更多强规则的一种方法就是检查礼品店中所有可能的商品组合,测量支持度和置信度,并仅报告那些达到一定兴趣水平的规则。然而,正如之前所述,这种策略通常只适用于数据集非常小的情况。

在下一节中,您将看到 Apriori 算法如何使用 Apriori 原则和最小支持度、置信度水平,通过减少规则数量到更易管理的水平来快速找到强规则。

使用 Apriori 原则构建一组规则

记住,Apriori 原则指出,频繁项集的所有子集也必须是频繁的。换句话说,如果*{A, B}是频繁的,那么{A}{B}都必须是频繁的。还要记住,根据定义,支持度指标表示项集在数据中出现的频率。因此,如果我们知道{A}没有达到期望的支持度阈值,就没有理由考虑{A, B}或任何包含{A}*的其他项集;这些不可能频繁出现。

Apriori 算法使用这种逻辑在评估之前排除潜在的关联规则。创建规则的过程分为两个阶段:

  1. 识别所有满足最小支持度阈值的项集

  2. 使用满足最小置信度阈值的项集创建规则

第一阶段发生在多次迭代中。每一次连续的迭代都涉及评估一组越来越大项集的支持度。例如,第一次迭代涉及评估 1 项项集(1 项集),第二次迭代评估 2 项集,依此类推。每一次迭代i的结果是所有满足最小支持度阈值的i项集的集合。

所有来自迭代 i 的项集都被组合起来,以生成在迭代 i + 1 中评估的候选项集。但 Apriori 原则可以在下一轮开始之前消除其中的一些。如果在一轮迭代中 {A}, {B}, 和 {C} 是频繁的,而 {D} 不是频繁的,那么第二轮迭代将只考虑 {A, B}, {A, C}, 和 {B, C}。因此,算法只需要评估三个项集,而不是如果包含 D 的集合没有被提前消除,将需要评估的六个 2 项项集。

继续这个想法,假设在第二次迭代中发现 {A, B}{B, C} 是频繁的,但 {A, C} 不是。尽管第三次迭代通常从评估 3 项项集 {A, B, C} 的支持度开始,但这一步不是必要的。为什么不是?Apriori 原则指出,由于子集 {A, C} 不是频繁的,因此 {A, B, C} 不可能是频繁的。因此,在第三次迭代中没有生成新的项集,算法可能停止。

图 8.2:在这个例子中,Apriori 算法只评估了 15 个潜在项集(对于四个项目,0 项项集未显示)中的 7 个,这些项集可能出现在事务数据中

在这一点上,Apriori 算法的第二阶段可能开始。给定频繁项集的集合,从所有可能的子集中生成关联规则。例如,{A, B} 将导致为 {A} {B}{B} {A} 生成候选规则。这些规则将根据最小置信度阈值进行评估,任何不符合所需置信度水平的规则都将被淘汰。

示例 - 使用关联规则识别经常购买的杂货

如本章引言所述,市场篮子分析在许多实体店和在线零售商使用的推荐系统背后被使用。学习到的关联规则表明了经常一起购买的商品。了解这些模式可以为杂货连锁店优化库存、宣传促销或组织商店的物理布局提供新的见解。例如,如果购物者经常在早餐糕点时购买咖啡或橙汁,那么通过将糕点移至咖啡和果汁附近,可能有可能增加利润。

同样,在线零售商可以使用这些信息为动态推荐引擎提供支持,这些引擎建议与您已经查看的商品相关的商品,或者在网站访问或在线购买后通过电子邮件建议附加商品,这种做法被称为主动营销

在本教程中,我们将对一家杂货店的交易数据进行市场篮子分析。这样做,我们将看到 Apriori 算法如何高效地评估潜在的庞大关联规则集。同样的技术也可以应用于许多其他商业任务,从电影推荐到交友网站,再到寻找药物之间的危险相互作用。

第 1 步 – 收集数据

我们的市场篮子分析将利用一家真实杂货店一个月运营的购买数据。数据包含 9,835 笔交易,或大约每天 327 笔交易(在 12 小时营业日中,每小时大约 30 笔交易),这表明零售商既不是特别大,也不是特别小。

这里使用的数据集是从arules R 包中的Groceries数据集改编的。更多信息,请参阅概率数据建模对挖掘关联规则的影响,Hahsler, M.,Hornik, K.,Reutterer, T.,2005。在从数据和信息分析到知识工程,Gaul, W.,Vichi, M.,Weihs, C.,分类、数据分析和知识组织研究,2006,第 598-605 页

一个典型的杂货店提供大量商品。可能有五种牛奶品牌,一打洗衣剂类型,以及三种咖啡品牌。鉴于本例中零售商的适度规模,我们假设它并不特别关注仅适用于特定品牌牛奶或洗衣剂的规则,因此所有品牌名称都从购买中去除。这减少了杂货商品的种类,使其更易于管理,共有 169 种类型,使用广泛的类别,如鸡肉、冷冻食品、人造黄油和汽水。

如果你希望识别高度具体的关联规则——例如,客户是否更喜欢葡萄或草莓果酱与花生酱搭配——你需要大量的交易数据。大型连锁零售商使用数百万笔交易的数据库,以找到特定品牌、颜色或口味之间的关联。

你对哪些类型的商品可能一起购买有什么猜测吗?葡萄酒和奶酪会是常见的搭配吗?面包和黄油?茶和蜂蜜?让我们深入挖掘这些数据,看看这些猜测是否可以得到证实。

第 2 步 – 探索和准备数据

事务数据存储的格式与我们之前使用的略有不同。我们之前的大多数分析都使用了矩阵格式,其中行表示示例实例,列表示特征。如第一章介绍机器学习中所述,在矩阵格式中,所有示例必须具有完全相同的特征集。

相比之下,交易数据更为自由形式。通常,数据中的每一行指定一个单独的例子——在本例中,是一个交易。然而,与具有固定数量的特征不同,每条记录包含一个由逗号分隔的项目列表,数量从一到多个。本质上,特征可能因例子而异。

要跟随这个分析,请从本章的 Packt Publishing GitHub 仓库下载 groceries.csv 文件,并将其保存在您的 R 工作目录中。

原始 groceries.csv 文件的头五行如下:

 citrus fruit,semi-finished bread,margarine,ready soups
   tropical fruit,yogurt,coffee
   whole milk
   pip fruit,yogurt,cream cheese,meat spreads
   other vegetables,whole milk,condensed milk,long life bakery product 

这些行表示五个不同的杂货店交易。第一个交易包含四个项目:柑橘水果半成品面包黄油即食汤品。相比之下,第三个交易只包含一个项目:全脂牛奶

假设我们尝试使用与先前分析中相同的 read.csv() 函数加载数据。R 会欣然同意,并将数据以矩阵格式读入数据框,如下所示:

表格描述自动生成

图 8.3:将交易数据作为数据框读入 R 将会在以后造成问题

您会注意到 R 创建了四个列来存储交易数据中的项目:V1V2V3V4。虽然这看起来似乎是合理的,但如果我们以这种形式使用数据,我们以后会遇到问题。R 选择创建四个变量,因为第一行恰好有四个由逗号分隔的值。然而,我们知道杂货购买可能包含超过四个项目;在四列设计中,这样的交易将在矩阵的多个行中拆分。我们可以尝试通过将项目数量最多的交易放在文件顶部来纠正这个问题,但这忽略了另一个更成问题的问题。

通过这种方式结构化数据,R 构建了一套特征,不仅记录了交易中的项目,还记录了它们出现的顺序。如果我们把我们的学习算法想象成试图在 V1V2V3V4 之间找到关系,那么 V1 中的 whole milk 项目可能与 V2 中出现的 whole milk 项目被不同对待。相反,我们需要一个数据集,它不将交易视为需要(或不需)用特定项目填充(或不填充)的位置集合,而将其视为一个包含或不包含每个项目的市场篮子。

数据准备 – 为交易数据创建稀疏矩阵

这个问题的解决方案使用了一种称为稀疏矩阵的数据结构。你可能记得我们在第四章概率学习 - 使用朴素贝叶斯进行分类中使用了稀疏矩阵来处理文本数据。就像先前的数据集一样,稀疏矩阵中的每一行都表示一个事务。然而,稀疏矩阵为可能出现在某人购物袋中的每个商品都有一个列(即特征)。由于我们的杂货店数据中有 169 种不同的商品,我们的稀疏矩阵将包含 169 列。

为什么不就像我们之前的大多数分析那样将其存储为数据框呢?原因是随着更多的事务和商品的添加,传统的数据结构很快就会变得太大,无法适应可用的内存。即使使用这里相对较小的交易数据集,矩阵也包含近 170 万个单元格,其中大多数包含零(因此得名“稀疏”矩阵——非零值非常少)。

由于存储所有这些零没有好处,稀疏矩阵实际上并不在内存中存储完整的矩阵;它只存储被商品占用的单元格。这使得结构比同等大小的矩阵或数据框更节省内存。

为了从事务数据中创建稀疏矩阵数据结构,我们可以使用arules(关联规则)包提供的功能。使用install.packages("arules")library(arules)命令安装并加载该包。

关于arules包的更多信息,请参阅arules - A Computational Environment for Mining Association Rules and Frequent Item Sets, Hahsler, M., Gruen, B., Hornik, K., Journal of Statistical Software, 2005, Vol. 14

由于我们正在加载事务数据,我们不能简单地使用之前使用的read.csv()函数。相反,arules提供了一个read.transactions()函数,它类似于read.csv(),但区别在于它生成一个适合事务数据的稀疏矩阵。参数sep = ","指定输入文件中的项由逗号分隔。要将groceries.csv数据读取到名为groceries的稀疏矩阵中,请输入以下行:

> groceries <- read.transactions("groceries.csv", sep = ",") 

要查看我们刚刚创建的groceries矩阵的一些基本信息,请在对象上使用summary()函数:

> summary(groceries) 
transactions as itemMatrix in sparse format with
 9835 rows (elements/itemsets/transactions) and
 169 columns (items) and a density of 0.02609146 

输出的第一块信息提供了我们创建的稀疏矩阵的摘要。输出9835 rows指的是事务数量,169 columns表示可能出现在某人购物篮中的 169 种不同商品。矩阵中的每个单元格如果是为相应事务购买的商品,则为1,否则为0

密度值 0.02609146(2.6%)指的是非零矩阵单元格的比例。由于矩阵中有 9,835 * 169 = 1,662,115 个位置,我们可以计算出在商店运营的 30 天内总共购买了 1,662,115 * 0.02609146 = 43,367 件商品(不考虑可能购买相同商品的重复)。通过额外的步骤,我们可以确定平均交易包含 43,367 / 9,835 = 4.409 种不同的杂货商品。当然,如果我们进一步查看输出,我们会看到每笔交易的商品数量的平均值已经提供。

下一个 summary() 输出块列出了在交易数据中最常出现的商品。由于 2,513 / 9,835 = 0.2555,我们可以确定全脂牛奶出现在 25.6%的交易中。

其他蔬菜面包卷/面包汽水酸奶构成了其他常见商品的列表,如下所示:

most frequent items:
      whole milk   other vegetables       rolls/buns
            2513               1903             1809
            soda             yogurt          (Other)
            1715               1372            34055 

我们还提供了一组关于交易大小的统计数据。共有 2,159 笔交易只包含一个商品,而有一笔交易包含 32 个商品。第一四分位数和中位数购买量分别是两个和三个商品,这意味着 25%的交易包含两个或更少的商品,大约一半的交易包含三个或更少的商品。每笔交易 4.409 个商品的均值与我们手工计算出的值相匹配:

element (itemset/transaction) length distribution:
sizes
   1    2    3    4    5    6    7    8    9   10   11   12 
2159 1643 1299 1005  855  645  545  438  350  246  182  117 
  13   14   15   16   17   18   19   20   21   22   23   24 
  78   77   55   46   29   14   14    9   11    4    6    1 
  26   27   28   29   32 
   1    1    1    3    1
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  1.000   2.000   3.000   4.409   6.000  32.000 

最后,输出底部包括与商品矩阵可能关联的任何元数据的附加信息,例如商品层次结构或标签。我们没有使用这些高级功能,但输出仍然表明数据有标签。read.transactions() 函数在加载时自动使用原始 CSV 文件中的商品名称添加了这些标签,前三个标签(按字母顺序排列)如下所示:

includes extended item information – examples:
            labels
1 abrasive cleaner
2 artif. Sweetener
3   baby cosmetics 

注意,arules 包内部使用没有与真实世界中的商品关联的数字项 ID 来表示商品。默认情况下,大多数 arules 函数将使用商品标签解码这些数字。然而,为了说明数字 ID,我们可以使用所谓的“长”格式来检查前两个交易,而不进行解码。在长格式交易数据中,每一行代表单个交易中的一个单独商品,而不是每一行代表一个包含多个商品的单独交易。例如,因为第一个和第二个交易分别有四个和三个商品,所以长格式用七行来表示这些交易:

> head(toLongFormat(groceries, decode = FALSE), n = 7) 
 TID item
1   1   30
2   1   89
3   1  119
4   1  133
5   2   34
6   2  158
7   2  168 

在这种交易数据的表示中,名为 TID 的列指的是交易 ID——即第一个或第二个购物篮,而名为 item 的列指的是分配给商品的内部 ID 号。由于第一个交易包含 {柑橘类水果、黄油、即食汤和半成品面包},我们可以假设商品 ID 30 指的是 柑橘类水果,而 89 指的是 黄油

当然,arules包包括用于以更直观的格式检查交易数据的特性。要查看稀疏矩阵的内容,请使用inspect()函数与 R 的向量运算符结合。以下是如何查看前五笔交易的示例:

> inspect(groceries[1:5]) 
 items                      
[1] {citrus fruit,             
     margarine,                
     ready soups,              
     semi-finished bread}      
[2] {coffee,                   
     tropical fruit,           
     yogurt}                   
[3] {whole milk}               
[4] {cream cheese,             
     meat spreads,             
     pip fruit,                
     yogurt}                   
[5] {condensed milk,           
     long life bakery product, 
     other vegetables,         
     whole milk} 

当使用inspect()函数格式化时,数据看起来与我们之前在原始 CSV 文件中看到的数据没有太大区别。

由于groceries对象存储为稀疏项矩阵,可以使用[行,列]表示法来检查所需的项以及所需的交易。使用此功能与itemFrequency()函数结合,我们可以看到包含指定项的所有交易的比例。例如,要查看所有行中前三个项的支持水平,请使用以下命令:

> itemFrequency(groceries[, 1:3]) 
abrasive cleaner artif. sweetener   baby cosmetics
    0.0035587189     0.0032536858     0.0006100661 

注意到稀疏矩阵中的项按字母顺序排列在列中。磨料清洁剂和人造甜味剂在约 0.3%的交易中找到,而婴儿化妆品在约 0.06%的交易中找到。

可视化项支持 - 项频率图

要将这些统计数据可视化,请使用itemFrequencyPlot()函数。这会创建一个条形图,表示包含指定项的交易比例。由于交易数据包含大量项,你通常需要限制图表中出现的项,以便生成可读的图表。

如果你希望显示出现在交易中比例最小的项,请使用带有support参数的itemFrequencyPlot()函数:

> itemFrequencyPlot(groceries, support = 0.1) 

如以下图表所示,这会产生一个直方图,显示了groceries数据中至少有 10%支持的八个项:

图表,条形图  自动生成的描述

图 8.4:至少在 10%的交易中所有杂货项的支持水平

如果你希望将图表限制在特定数量的项上,请使用带有topN参数的函数:

> itemFrequencyPlot(groceries, topN = 20) 

然后直方图按支持度递减排序,如下图中groceries数据的前 20 项所示:

包含图表的图片  自动生成的描述

图 8.5:前 20 个杂货项的支持水平

可视化交易数据 - 绘制稀疏矩阵

除了查看特定项之外,还可以使用image()函数从整体上获得稀疏矩阵的鸟瞰图。当然,由于矩阵本身非常大,通常最好请求整个矩阵的一个子集。显示前五笔交易的稀疏矩阵的命令如下:

> image(groceries[1:5]) 

生成的图表显示了一个 5 行 169 列的矩阵,表示我们请求的 5 笔交易和 169 个可能的项。矩阵中的单元格在交易(行)中购买项(列)时填充为黑色。

包含图表的图片  自动生成的描述

图 8.6:前五笔交易的稀疏矩阵可视化

虽然图 8.6 很小,可能稍微难以阅读,但您可以看到第一、第四和第五笔交易各包含四个项目,因为它们的行有四个单元格被填充。在图表的右侧,您还可以看到第三行和第五行,以及第二行和第四行共享一个共同的项目。

这种可视化可以是一个有用的工具来探索交易数据。首先,它可能有助于识别潜在的数据问题。完全填满的列可能表明在每次交易中都购买的项目——这可能是由于零售商的名称或识别号码意外包含在交易数据集中而引起的问题。

此外,图中的模式可能有助于揭示有趣的交易和项目段,尤其是如果数据以有趣的方式排序。例如,如果交易按日期排序,黑色点的模式可能揭示购买数量或类型的季节性影响。也许在圣诞节或光明节期间,玩具更为常见;在万圣节期间,糖果可能变得流行。如果项目也被分类排序,这种类型的可视化可能特别强大。然而,在大多数情况下,图表看起来相当随机,就像电视屏幕上的静电一样。

请记住,这种可视化对于非常大的交易数据库可能不太有用,因为单元格太小,无法辨别。不过,通过结合sample()函数,您可以查看随机采样交易集的稀疏矩阵。创建 100 笔随机交易的选择命令如下:

> image(sample(groceries, 100)) 

这创建了一个 100 行 169 列的矩阵图:

图表,散点图  自动生成的描述

图 8.7:100 笔随机选择交易的稀疏矩阵可视化

几列似乎相当密集,表明商店中一些非常受欢迎的商品。然而,点的分布总体上似乎相当随机。如果没有其他值得注意的事项,让我们继续我们的分析。

第 3 步 – 在数据上训练模型

数据准备完成后,我们现在可以着手寻找购物车项目之间的关联。我们将使用我们一直在使用的arules包中的 Apriori 算法实现来探索和准备groceries数据。如果您还没有安装和加载此包,您需要这样做。

下表显示了使用apriori()函数创建规则集的语法:

文本  自动生成的描述

图 8.8:Apriori 关联规则学习语法

虽然运行apriori()函数很简单,但有时为了找到产生合理数量关联规则的支持置信度参数,可能需要进行相当多的试错。如果你将这些级别设置得太高,那么你可能会找不到任何规则,或者可能会找到过于通用的规则,不太有用。另一方面,如果阈值设置得太低,可能会导致规则数量过多。更糟糕的是,操作可能会花费很长时间,或者在学习阶段耗尽内存。

groceries数据上,使用默认的support = 0.1confidence = 0.8设置导致令人失望的结果。虽然为了简洁省略了完整的输出,但最终结果是零规则集:

> apriori(groceries) 
...
set of 0 rules 

显然,我们需要稍微扩大搜索范围。

如果你仔细想想,这个结果不应该令人特别惊讶。因为默认的support = 0.1,为了生成一个规则,一个商品必须至少出现在0.1 * 9,385 = 938.5次交易中。由于在我们的数据中只有八个商品这么频繁地出现,难怪我们没有找到任何规则。

解决设置最小支持度问题的一种方法是想一下在利益相关者认为一个模式有趣之前需要的最小交易数量。例如,可以争论如果一项商品每天被购买两次(在一个月的数据中大约是 60 次),那么它可能很重要。从那里,可以计算出找到至少那么多交易的规则所需的支持水平。由于 60 除以 9,835 大约等于 0.006,我们将首先尝试将支持度设置为那里。

设置最低置信度需要微妙的平衡。一方面,如果置信度太低,我们可能会被许多不可靠的规则淹没——例如,几十条规则表明面包和电池等商品偶然经常一起购买。那么我们如何知道在哪里投放我们的广告预算呢?另一方面,如果我们设置置信度太高,那么我们将局限于明显或不可避免的规则——比如烟雾探测器总是与电池一起购买的事实。在这种情况下,将烟雾探测器移近电池不太可能产生额外的收入,因为这两个商品几乎总是一起购买。

适当的最低置信度水平在很大程度上取决于你分析的目标。如果你从一个保守的值开始,如果你没有找到可操作的信息,你可以总是将其降低以扩大搜索范围。

我们将从一个置信度阈值为 0.25 开始,这意味着为了被包含在结果中,规则至少必须有 25%的时间是正确的。这将消除最不可靠的规则,同时为我们留出一些空间,通过有针对性的促销来修改行为。

现在我们准备生成一些规则。除了最小支持置信度参数外,设置minlen = 2以消除包含少于两个项目的规则是有帮助的。这可以防止仅因为项目经常被购买而创建无趣的规则,例如,{} => whole milk。此规则满足最小支持和置信度,因为全脂牛奶在超过 25%的交易中被购买,但这并不是一个非常有用的洞察。

使用 Apriori 算法查找关联规则的全命令如下:

> groceryrules <- apriori(groceries, parameter = list(support =
                            0.006, confidence = 0.25, minlen = 2)) 

输出的前几行描述了我们指定的参数设置,以及一些保持默认设置的参数;有关这些参数的定义,请使用?APparameter帮助命令。第二组行显示了幕后算法控制参数,这些参数对于处理更大的数据集可能很有帮助,因为它们控制着计算权衡,如优化速度或内存使用。有关这些参数的信息,请使用?APcontrol帮助命令:

Apriori
Parameter specification:
 confidence minval smax arem  aval originalSupport maxtime support
       0.25    0.1    1 none FALSE            TRUE       5   0.006
 minlen maxlen target  ext
      2     10  rules TRUE
Algorithmic control:
 filter tree heap memopt load sort verbose
    0.1 TRUE TRUE  FALSE TRUE    2    TRUE 

接下来,输出包括关于 Apriori 算法执行步骤的信息:

Absolute minimum support count: 59 
set item appearances ...[0 item(s)] done [0.00s].
set transactions ...[169 item(s), 9835 transaction(s)] done [0.00s].
sorting and recoding items ... [109 item(s)] done [0.00s].
creating transaction tree ... done [0.00s].
checking subsets of size 1 2 3 4 done [0.00s].
writing ... [463 rule(s)] done [0.00s].
creating S4 object  ... done [0.00s]. 

由于事务数据集规模较小,大多数行显示的步骤几乎不需要时间运行——在此输出中用[0.00s]表示,但你的输出可能因计算机性能而略有不同。

绝对最小支持计数指的是满足我们指定的支持阈值 0.006 所需的最小交易计数。由于0.006 * 9,835 = 59.01,算法要求项目至少出现在 59 个交易中。检查大小为 1 2 3 4 的子集输出表明算法在停止迭代过程并写入最终的 463 条规则之前测试了 1、2、3 和 4 个项目的-i 项集。

apriori()函数的最终结果是规则对象,我们可以通过输入其名称来查看:

> groceryrules 
set of 463 rules 

我们的groceryrules对象包含大量关联规则!为了确定其中是否有任何有用的规则,我们还需要深入研究。

步骤 4 – 评估模型性能

为了获得关联规则的高级概述,我们可以使用summary()如下。规则长度分布告诉我们有多少规则具有每个项目计数。在我们的规则集中,150 条规则只有两个项目,而 297 条规则有三个,16 条规则有四个。与此分布相关的摘要统计信息也提供在输出的前几行:

> summary(groceryrules) 
set of 463 rules
rule length distribution (lhs + rhs):sizes
  2   3   4 
150 297  16 
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  2.000   2.000   3.000   2.711   3.000   4.000 

如前一个输出所示,规则的尺寸是规则左侧(lhs)和右侧(rhs)的总和。这意味着像{bread} => {butter}这样的规则包含两个项目,而{peanut butter, jelly} => {bread}则包含三个。

接下来,我们看到规则质量度量指标的摘要统计信息,包括支持置信度,以及覆盖率提升度计数

summary of quality measures:
    support           confidence        coverage       
 Min.   :0.006101   Min.   :0.2500   Min.   :0.009964  
 1st Qu.:0.007117   1st Qu.:0.2971   1st Qu.:0.018709  
 Median :0.008744   Median :0.3554   Median :0.024809  
 Mean   :0.011539   Mean   :0.3786   Mean   :0.032608  
 3rd Qu.:0.012303   3rd Qu.:0.4495   3rd Qu.:0.035892  
 Max.   :0.074835   Max.   :0.6600   Max.   :0.255516  
      lift            count      
 Min.   :0.9932   Min.   : 60.0  
 1st Qu.:1.6229   1st Qu.: 70.0  
 Median :1.9332   Median : 86.0  
 Mean   :2.0351   Mean   :113.5  
 3rd Qu.:2.3565   3rd Qu.:121.0  
 Max.   :3.9565   Max.   :736.0 

支持置信度度量不应该非常令人惊讶,因为我们已经将这些作为规则选择标准。如果我们发现大多数或所有规则的支持和置信度都非常接近最小阈值,我们可能会感到惊讶,因为这意味着我们可能把门槛设得太高。但这里并非如此,因为有许多规则的支持值和置信度都远高于这个值。

计数覆盖度度量与支持度置信度密切相关。在这里定义的计数是支持度度量的分子,或者是包含该商品的交易数量(而不是比例)。由于绝对最小支持度计数为 59,因此观察到的最小计数为 60,这与参数设置非常接近并不令人惊讶。最大计数为 736 表明,该商品出现在 9,835 笔交易中的 736 笔;这与观察到的最大支持度相关,即736 / 9,835 = 0.074835

关联规则的覆盖度简单地是规则左侧的支持度,但它有一个有用的现实世界解释:它可以理解为规则在数据集中随机选择的任何给定交易中应用的概率。因此,最小的覆盖度为 0.009964 表明,最不适用规则的覆盖范围仅占交易的大约 1%;最大的覆盖度为 0.255516 表明,至少有一个规则覆盖了超过 25%的交易。当然,这个规则涉及到全脂牛奶,因为它是唯一出现在如此多交易中的商品。

最后一列是我们尚未考虑的度量。规则的提升度衡量的是,在已知另一个商品或商品集已被购买的情况下,一个商品或商品集相对于其典型购买率的购买可能性。这由以下方程定义:

与置信度不同,其中项目顺序很重要,提升(X Y)提升(Y *X)*相同。

例如,假设在一家杂货店,大多数人会购买牛奶和面包。仅凭运气,我们预计会发现许多同时购买牛奶和面包的交易。然而,如果提升(milk *bread)*大于 1,这表明这两个商品比仅凭运气更频繁地一起出现。换句话说,购买其中一个商品的人更有可能购买另一个商品。因此,一个大的提升值是规则重要的强烈指标,反映了商品之间的真实联系,并且该规则对商业用途是有用的。然而,请注意,这仅适用于足够大的交易数据集;对于支持度低的商品,提升值可能会被夸大。

apriori包的一对作者提出了新的度量标准,称为超提升超置信度,以解决这些度量标准在稀疏数据中的不足。更多信息,请参阅M. Hahsler 和 K. Hornik,关联规则的新概率兴趣度量(2018). arxiv.org/pdf/0803.0966.pdf

summary()输出的最后部分,我们收到挖掘信息,告诉我们规则是如何被选择的。在这里,我们看到groceries数据,其中包含 9,835 笔交易,被用来构建最小支持度为 0.006 和最小置信度为 0.25 的规则:

mining info:
      data  transactions support confidence
 groceries          9835   0.006       0.25 

我们可以使用inspect()函数查看具体的规则。例如,groceryrules对象中的前三条规则可以如下查看:

> inspect(groceryrules[1:3]) 
 lhs                rhs               support    
[1] {potted plants} => {whole milk}      0.006914082
[2] {pasta}         => {whole milk}      0.006100661
[3] {herbs}         => {root vegetables} 0.007015760
    confidence coverage   lift     count
[1] 0.4000000  0.01728521 1.565460 68   
[2] 0.4054054  0.01504830 1.586614 60   
[3] 0.4312500  0.01626843 3.956477 69 

第一条规则可以用普通语言读作:“如果一个顾客购买了盆栽植物,他们也会购买全脂牛奶。”支持度约为 0.007,置信度为 0.400,我们可以确定这条规则覆盖了大约 0.7%的交易,并且在涉及盆栽植物的 40%的购买中是正确的。提升值告诉我们,在顾客购买了盆栽植物的情况下,他们购买全脂牛奶的可能性相对于平均顾客要高多少。由于我们知道大约 25.6%的顾客购买了全脂牛奶(support),而 40%购买盆栽植物的顾客购买了全脂牛奶(confidence),我们可以计算出提升值为0.40 / 0.256 = 1.56,这与显示的值相匹配。

注意,标有support的列表示规则的支撑度,而不是lhsrhs单独的支撑度。标有coverage的列是左侧的支撑度。

尽管置信度和提升度都很高,但*{盆栽植物} {全脂牛奶}*看起来像一条非常有用的规则吗?可能不是,因为没有明显的逻辑原因说明为什么有人会更有可能和盆栽植物一起购买牛奶。然而,我们的数据表明情况并非如此。我们如何理解这一事实?

一种常见的方法是将关联规则分为以下三个类别:

  • 可执行

  • 琐碎

  • 无法解释

显然,市场篮子分析的目标是找到可执行的规则,这些规则提供了清晰且有趣的见解。有些规则是清晰的,有些是有趣的;同时具备这两个因素的规则较为罕见。

所说的琐碎规则包括任何如此明显以至于不值得提及的规则——它们是清晰的,但并不有趣。假设你是一名营销顾问,被支付大笔金钱来识别跨推广商品的新机会。如果你报告的发现是*{纸尿布} {配方}*,你可能不会被邀请回来进行另一项咨询工作。

简单的规则也可能伪装成更有趣的结果。例如,如果你发现某种儿童谷物品牌与一部流行的动画片之间存在关联。如果这部电影的主要角色出现在谷物盒的正面,这个发现就不是很具有洞察力。

如果项目之间的联系如此不清楚,以至于无法或几乎无法弄清楚如何使用这些信息,则规则是无法解释的。该规则可能仅仅是数据中的随机模式,例如,一条声称*{腌黄瓜}{巧克力冰淇淋}*之间有关系的规则可能仅是由于一位孕妇妻子对奇怪食物组合有定期渴望的单一客户。

最好的规则是隐藏的宝石——一旦被发现,似乎就显而易见的未发现见解。如果时间足够,可以评估每一条规则以找到这些宝石。然而,从事分析的数据科学家可能不是判断规则是否具有可操作性、平凡或无法解释的最佳评判者。因此,更好的规则很可能是通过与负责管理零售连锁店的领域专家合作而产生的,他们可以帮助解释这些发现。在下一节中,我们将通过采用排序和导出学习规则的方法来促进这种共享,以便最有趣的结果浮出水面。

第 5 步 – 提高模型性能

主题专家可能能够非常快速地识别出有用的规则,但要求他们评估数百或数千条规则则是对他们时间的低效利用。因此,能够根据不同的标准对规则进行排序,并以可以与营销团队共享并深入审查的形式从 R 中提取它们,是非常有用的。这样,我们可以通过使结果更具可操作性来提高我们规则的表现力。

如果你遇到内存限制问题,或者 Apriori 运行时间过长,也可以通过使用更近期的算法来提高关联规则挖掘过程的计算性能。

对关联规则集进行排序

根据市场篮子分析的目标,最有用的规则可能是那些具有最高支持度、置信度或提升度的规则。arules包与 R 的sort()函数一起工作,允许重新排序规则列表,使得具有最高或最低质量度量值的规则排在最前面。

为了重新排序groceryrules对象,我们可以使用sort()函数,同时指定by参数的值为"support""confidence""lift"。通过将排序与向量运算符结合使用,我们可以获得特定数量的有趣规则。例如,可以使用以下命令检查根据lift统计量得出的最佳五条规则:

> inspect(sort(groceryrules, by = "lift")[1:5]) 

输出如下:

 lhs                    rhs                      support
[1] {herbs}             => {root vegetables}    0.007015760
[2] {berries}           => {whipped/sour cream} 0.009049314
[3] {other vegetables,                                     
     tropical fruit,                                       
     whole milk}        => {root vegetables}    0.007015760
[4] {beef,                                                 
     other vegetables}  => {root vegetables}    0.007930859
[5] {other vegetables,                                     
     tropical fruit}    => {pip fruit}          0.009456024
    confidence coverage   lift     count
[1] 0.4312500  0.01626843 3.956477 69
[2] 0.2721713  0.03324860 3.796886 89
[3] 0.4107143  0.01708185 3.768074 69
[4] 0.4020619  0.01972547 3.688692 78
[5] 0.2634561  0.03589222 3.482649 93 

这些规则似乎比我们之前看到的规则更有趣。第一条规则,其 lift 值约为 3.96,意味着购买香草的人比典型客户更有可能购买根类蔬菜,可能是为了某种炖菜。第二条规则也很有趣。与其他购物车相比,奶油在装有浆果的购物车中出现的可能性超过三倍,这可能表明是一种甜点搭配。

默认情况下,排序顺序是降序,这意味着最大的值排在前面。要反转此顺序,添加一个额外的参数,decreasing = FALSE

考虑关联规则的子集

假设,根据前面的规则,营销团队对创建广告推广浆果的可能性感到兴奋,因为浆果现在正值季节。然而,在最终确定活动之前,他们要求你调查浆果是否经常与其他商品一起购买。为了回答这个问题,我们需要找到所有包含浆果的规则。

subset() 函数提供了一种搜索事务、项目或规则子集的方法。要使用它来查找规则中包含浆果的任何规则,请使用以下命令。这将把规则存储在一个名为 berryrules 的新对象中:

> berryrules <- subset(groceryrules, items %in% "berries") 

我们可以像之前处理较大集合那样检查规则:

> inspect(berryrules) 

结果是以下规则集:

 lhs          rhs                  support    
[1] {berries} => {whipped/sour cream} 0.009049314
[2] {berries} => {yogurt}             0.010574479
[3] {berries} => {other vegetables}   0.010269446
[4] {berries} => {whole milk}         0.011794611
    confidence coverage  lift     count
[1] 0.2721713  0.0332486 3.796886  89  
[2] 0.3180428  0.0332486 2.279848 104  
[3] 0.3088685  0.0332486 1.596280 101  
[4] 0.3547401  0.0332486 1.388328 116 

有四个涉及浆果的规则,其中两个似乎足够有趣,可以被称为可执行的。除了奶油,浆果也经常与酸奶一起购买——这种搭配可以作为早餐或午餐,以及甜点。

subset() 函数非常强大。选择子集的标准可以用几个关键词和操作符来定义:

  • 关键词 items,如前所述,匹配规则中出现的任何项目。要限制子集只匹配左侧或右侧,请使用 lhsrhs

  • 操作符 %in% 表示至少有一个项目必须出现在你定义的列表中。如果你想找到匹配“浆果”或“酸奶”的任何规则,你可以写 items %in% c("berries", "yogurt")

  • 可用其他操作符进行部分匹配 (%pin%) 和完全匹配 (%ain%)。部分匹配允许你使用一个搜索找到柑橘类水果和热带水果:items %pin% "fruit"。完全匹配要求所有列出的项目都必须存在。例如,items %ain% c("berries", "yogurt") 仅找到包含 berriesyogurt 的规则。

  • 子集也可以通过 supportconfidencelift 来限制。例如,confidence > 0.50 将规则限制在置信度大于 50%的规则。

  • 匹配标准可以与标准的 R 逻辑操作符(如 AND (&), OR (|), 和 NOT (!)) 结合使用。

使用这些选项,你可以将规则的选取限制得尽可能具体或一般。

将关联规则保存到文件或数据框

要分享您的购物篮分析结果,您可以使用write()函数将规则保存到 CSV 文件。这将生成一个 CSV 文件,可以在大多数电子表格程序中使用,包括 Microsoft Excel:

> write(groceryrules, file = "groceryrules.csv",
          sep = ",", quote = TRUE, row.names = FALSE) 

有时,将规则转换为 R 数据框也很方便。这可以通过使用as()函数实现,如下所示:

> groceryrules_df <- as(groceryrules, "data.frame") 

这将创建一个包含规则字符格式和数据框的规则,以及支持度、置信度、覆盖度、提升度和计数等数值向量:

> str(groceryrules_df) 
'data.frame':	463 obs. of  6 variables:
 $ rules     : chr  "{potted plants} => {whole milk}"   "{pasta} => {whole milk}" "{herbs} => {root vegetables}"   "{herbs} => {other vegetables}" ...
 $ support   : num  0.00691 0.0061 0.00702 0.00773 0.00773 ...
 $ confidence: num  0.4 0.405 0.431 0.475 0.475 ...
 $ coverage  : num  0.0173 0.015 0.0163 0.0163 0.0163 ...
 $ lift      : num  1.57 1.59 3.96 2.45 1.86 ...
 $ count     : int  68 60 69 76 76 69 70 67 63 88 ... 

将规则保存到数据框中可能很有用,如果您想对规则进行额外的处理或需要将它们导出到另一个数据库。

使用 Eclat 算法提高效率

Eclat 算法,该算法以“等价类项集聚类和自底向上的格遍历”方法命名,是一种稍微现代且实质上更快的关联规则学习算法。虽然实现细节超出了本书的范围,但它可以理解为 Apriori 的近亲;它也假设所有频繁项集的子集也是频繁的。然而,Eclat 通过利用提供识别潜在最大频繁项集的快捷方式的巧妙技巧,能够搜索更少的子集。由于 Apriori 在深入搜索之前先进行广泛搜索,因此它是一种广度优先算法,而 Eclat 被认为是一种深度优先算法,因为它深入到底端并只搜索所需的宽度。对于某些用例,这可能导致性能提升一个数量级并减少内存消耗。

关于 Eclat 的更多信息,请参阅快速发现关联规则的新算法,Zaki, M. J., Parthasarathy, S., Ogihara, M., Li, W., KDD-97 会议论文集,1997

与 Eclat 的快速搜索相比,一个关键的权衡是它跳过了 Apriori 中计算置信度的阶段。它假设一旦获得了具有高支持度的项集,就可以在以后识别最有用的关联——无论是通过主观的目测,还是通过另一轮处理来计算置信度和提升度等指标。尽管如此,arules包使得应用 Eclat 与 Apriori 一样简单,尽管在处理过程中增加了额外的一步。

我们从eclat()函数开始,并将support参数设置为之前相同的 0.006;然而,请注意,在这个阶段并未设置置信度:

> groceryitemsets_eclat <- eclat(groceries, support = 0.006) 

这里省略了一些输出,但最后几行与我们从apriori()函数获得的输出类似,关键区别在于写入了 747 个项集而不是 463 条规则:

Absolute minimum support count: 59 
create itemset … 
set transactions …[169 item(s), 9835 transaction(s)] done [0.00s].
sorting and recoding items … [109 item(s)] done [0.00s].
creating sparse bit matrix … [109 row(s), 9835 column(s)] done [0.00s].
writing  … [747 set(s)] done [0.02s].
Creating S4 object  … done [0.00s]. 

生成的 Eclat 项集对象可以使用inspect()函数,就像我们使用 Apriori 规则对象一样。以下命令显示了前五个项集:

> inspect(groceryitemsets_eclat[1:5]) 
 items                       support     count
[1] {potted plants, whole milk} 0.006914082 68   
[2] {pasta, whole milk}         0.006100661 60   
[3] {herbs, whole milk}         0.007727504 76   
[4] {herbs, other vegetables}   0.007727504 76   
[5] {herbs, root vegetables}    0.007015760 69 

要从项集中生成规则,请使用 ruleInduction() 函数,并设置所需的 confidence 参数值,如下所示:

> groceryrules_eclat <- ruleInduction(groceryitemsets_eclat,
    confidence = 0.25) 

supportconfidence 设置为之前的值 0.006 和 0.25,Eclat 算法产生了与 Apriori 相同的 463 条规则,这并不令人惊讶:

> groceryrules_eclat 
set of 463 rules 

结果的规则对象可以像之前一样进行检查:

> inspect(groceryrules_eclat[1:5]) 
 lhs                rhs                support    
[1] {potted plants} => {whole milk}       0.006914082
[2] {pasta}         => {whole milk}       0.006100661
[3] {herbs}         => {whole milk}       0.007727504
[4] {herbs}         => {other vegetables} 0.007727504
[5] {herbs}         => {root vegetables}  0.007015760
    confidence lift    
[1] 0.4000000  1.565460
[2] 0.4054054  1.586614
[3] 0.4750000  1.858983
[4] 0.4750000  2.454874
[5] 0.4312500  3.956477 

由于两种方法都易于使用,如果你有一个非常大的交易数据集,那么在较小的随机交易样本上测试 Eclat 和 Apriori 算法可能值得,以查看哪一个表现更好。

摘要

关联规则用于发现大型零售商的大量交易数据库中的洞察。作为一个无监督学习过程,关联规则学习器能够从大型数据库中提取知识,而无需任何关于要寻找的模式的先验知识。难点在于需要付出一些努力,将丰富的信息减少到更小、更易于管理的结果集。我们在本章研究的 Apriori 算法通过设置最小有趣性阈值,并仅报告满足这些标准的关联来实现这一点。

我们在为一家规模适中的超市进行一个月的交易市场篮子分析时使用了 Apriori 算法。即使在这样一个小例子中,也发现了大量的关联。在这些关联中,我们注意到了一些可能对未来的营销活动有用的模式。我们应用的方法在规模大得多的零售商处使用,其数据库规模是这个大小的多倍,也可以应用于零售环境之外的项目。

在下一章中,我们将检查另一个无监督学习算法。就像关联规则一样,它的目的是在数据中找到模式。但与寻求相关项目或特征的关联规则不同,下一章中的方法关注于在示例之间找到联系和关系。

加入我们书籍的 Discord 空间

加入我们的 Discord 社区,与志同道合的人见面,并在以下地方与超过 4000 人一起学习:

packt.link/r

第九章:寻找数据组——使用 k-means 进行聚类

你是否曾经花时间观察过人群?如果是的话,你可能已经看到了一些反复出现的个性特征。也许某种类型的人,通过一套新熨烫的西装和公文包,可以代表“肥猫”商业高管。一个二十多岁穿着紧身牛仔裤、法兰绒衬衫和太阳镜的人可能被称为“嬉皮士”,而一个从微型货车卸下孩子的女人可能被贴上“足球妈妈”的标签。

当然,将这些类型的刻板印象应用于个人是危险的,因为没有人是完全相同的。然而,如果将其理解为描述集体的一种方式,这些标签就能捕捉到群体中个体之间共享的一些潜在相似性。

如你很快就会了解到的那样,聚类或发现数据中的模式这一行为,与发现人群中的模式并没有太大的区别。本章将描述:

  • 聚类任务与我们之前考察的分类任务的不同之处

  • 聚类如何定义一个组以及这些组如何通过 k-means,一种经典且易于理解的聚类算法来识别

  • 将聚类应用于识别青少年社交媒体用户中营销细分市场的实际任务所需的步骤

在采取行动之前,我们将首先深入探讨聚类究竟意味着什么。

理解聚类

聚类是一种无监督的机器学习任务,它自动将数据划分为,或相似项的组。它这样做,而无需事先被告知这些组应该如何看起来。因为我们没有告诉机器我们具体在寻找什么,所以聚类用于知识发现而不是预测。它提供了对数据中自然分组洞察。

没有关于构成簇的先进知识,计算机怎么可能知道一个组在哪里结束,另一个组在哪里开始呢?答案是简单的:聚类是由这样一个原则指导的,即簇内的项目应该彼此非常相似,但与簇外的项目非常不同。相似性的定义可能因应用而异,但基本思想始终相同:将数据分组,使得相关元素放在一起。

然后可以使用这些簇进行行动。例如,你可能会发现聚类方法被用于以下应用:

  • 将客户分成具有相似人口统计或购买模式的小组,以进行定向营销活动

  • 通过识别使用模式落在已知簇之外的模式来检测异常行为,例如未经授权的网络入侵

  • 通过创建少量类别来简化极其“宽”的数据集——那些具有大量特征的数据集——以描述具有相对同质特征值的行

总体而言,当可以用更少的组来代表多样化和多变的数据时,聚类是有用的。它产生了有意义的可操作数据结构,减少了复杂性,并提供了对关系模式的洞察。

聚类作为机器学习任务

聚类与我们迄今为止考察的分类、数值预测和模式检测任务有所不同。在这些任务中,目标是建立一个将特征与结果相关联的模型,或者将一些特征与另一些特征相关联。这些任务中的每一个都描述了数据中的现有模式。相比之下,聚类的目标是创建新的数据。在聚类中,未标记的示例被赋予一个新的聚类标签,这个标签完全是从数据中的关系推断出来的。因此,有时你会看到聚类任务被称为无监督分类,因为在某种程度上,它对未标记的示例进行了分类。

但问题是,从无监督分类器获得的类别标签没有内在的意义。聚类会告诉你哪些示例组紧密相关——例如,它可能会返回组 A、B 和 C——但具体应用一个有意义的可操作标签,以及讲述“为什么 A 与 B 不同”的故事,取决于你。为了了解这如何影响聚类任务,让我们考虑一个简单的假设例子。

假设你正在组织一个关于数据科学的会议。为了促进专业网络和协作,你计划根据他们的研究专长将人们安排在三个桌子中的一张。不幸的是,在发出会议邀请后,你意识到你忘记包括一个调查,询问与会者希望坐在哪个学科组内。

在一次灵光一闪中,你意识到你可能能够通过检查每位学者的出版物历史来推断他们的研究专长。为此,你开始收集每位与会者在计算机科学相关期刊上发表的文章数量以及发表在数学或统计学相关期刊上的文章数量。使用为学者收集的数据,你创建了一个散点图:

图表,散点图  描述自动生成

图 9.1:通过数学和计算机科学出版物数据可视化学者

如预期的那样,似乎存在一种模式。我们可能会猜测左上角,代表那些有众多计算机科学出版物但数学文章很少的人,是一群计算机科学家。按照这个逻辑,右下角可能是一群数学家或统计学家。同样,右上角,那些既有数学又有计算机科学经验的人,可能是机器学习专家。

应用这些标签会产生以下可视化效果:

图示  描述自动生成

图 9.2:可以根据对每组学者的假设来识别集群

我们的分组是通过视觉形成的;我们只是将紧密聚集的数据点识别为集群。然而,尽管看似明显的分组,如果没有亲自询问每位学者的学术专长,我们就无法知道这些群体是否真正同质。标签是关于每个群体中人的类型的定性、假设性判断,基于有限的一组定量数据。

与主观定义群体边界相比,使用机器学习来客观地定义它们会更好。鉴于前一个图中的轴平行分割,我们的问题似乎是一个明显的决策树应用,如第五章分而治之 – 使用决策树和规则进行分类中所述。这将为我们提供一个干净的规则,例如:“如果一个学者数学出版物很少,那么他们是计算机科学专家。”不幸的是,这个计划有问题。没有每个点的真实类别值数据,监督学习算法就无法学习这样的模式,因为它无法知道哪些分割会产生同质群体。

与监督学习相反,聚类算法使用的过程与我们通过视觉检查散点图所做的过程非常相似。使用示例之间关系的度量,可以识别出同质群体。在下一节中,我们将开始探讨聚类算法是如何实现的。

这个例子突出了聚类的一个有趣应用。如果您从未标记的数据开始,可以使用聚类来创建类标签。从那里,您可以应用像决策树这样的监督学习器来找到这些类别的最重要的预测因子。这是第一章介绍机器学习中描述的半监督学习的一个例子。

聚类算法的集群

正如构建预测模型有许多方法一样,执行聚类描述性任务的方法也有很多。许多此类方法列在以下网站上,CRAN 聚类任务视图:cran.r-project.org/view=Cluster。在这里,您可以找到用于在数据中发现自然分组的众多 R 包。不同的算法主要根据两个特征来区分:

  • 相似度度量,它提供了两个示例之间关系的定量度量

  • 聚合函数,它控制着根据示例之间的相似性将示例分配到集群的过程

尽管这些方法之间可能存在细微的差异,但它们当然可以以各种方式聚类。存在多种这样的类型,但一个简单的三部分框架有助于理解主要区别。使用这种方法,从最简单到最复杂,以下是聚类算法的三个主要类别:

  • 层次方法,这些方法创建了一种家族树式的层次结构,将最相似的示例在图结构中放置得更近。

  • 基于划分的方法,这些方法将示例视为多维空间中的点,并试图找到这个空间中的边界,以形成相对同质的小组。

  • 基于模型或密度的方法,这些方法依赖于统计原理和/或点的密度来发现簇之间的模糊边界;在某些情况下,示例可能部分分配到多个簇中,甚至没有任何簇。

尽管层次聚类是这些方法中最简单的,但它并非没有两个有趣的优点。首先,它产生了一种称为树状图的层次图可视化,它描绘了示例之间的关联,使得最相似的示例在层次结构中位置更近。

这可以是一个有用的工具,用来理解哪些示例和示例的子集是最紧密地分组的。其次,层次聚类不需要预先定义数据集中存在多少簇的期望。相反,它实施了一个过程,在这个过程中,在一种极端情况下,每个示例都包含在一个包含所有其他示例的单个大簇中;在另一种极端情况下,每个示例都发现自己在一个只包含它自己的小簇中;在两者之间,示例可能包含在其他大小不一的簇中。

图 9.3 展示了一个包含八个示例的简单数据集的假设树状图,这些示例分别标记为 A 到 H。请注意,最相关的示例(通过x轴上的邻近性表示)在图中被更紧密地连接,作为兄弟姐妹。例如,示例 D 和 E 是最相似的,因此它们是最先被分组的。然而,所有八个示例最终都会连接到一个大的簇中,或者可能包含在任何数量的簇之间。在树状图的横向不同位置切割,会创建不同数量的簇,如图所示为三个和五个簇:

图表、示意图、箱线图 描述自动生成

图 9.3:层次聚类产生一个树状图,描绘了所需簇数的自然分组。

层次聚类的树状图可以使用“自下而上”或“自上而下”的方法生成。前者称为聚合聚类,它从每个示例自己的簇开始,然后首先连接最相似的示例,直到所有示例都连接到一个单独的簇中。后者称为分裂聚类,它从一个大型簇开始,以所有示例都在自己的单独簇中结束。

在将示例连接到示例组时,可以使用不同的度量标准,例如示例与组中最相似、最不相似或平均成员的相似度。一种更复杂的度量标准称为沃德方法,它不使用示例之间的相似度,而是考虑簇同质性的度量来构建链接。无论如何,结果是旨在将最相似的示例分组到任意数量的子组中的层次结构。

层次聚类技术的灵活性是以计算复杂性为代价的,这是由于需要计算每个示例与其他每个示例之间的相似性。随着示例数量(N)的增长,计算数量会增长到 N*N = N²,存储结果的相似性矩阵所需的内存也会增加。因此,层次聚类仅用于非常小的数据集,本章没有演示。然而,R 的stats包中包含的hclust()函数提供了一个简单的实现,该实现默认与 R 一起安装。

分裂聚类的巧妙实现可能比聚合聚类在计算上稍微高效一些,因为算法可能会在不需要创建更多簇的情况下提前停止。尽管如此,聚合聚类和分裂聚类都是“贪婪”算法的例子,正如在第五章“分而治之——使用决策树和规则进行分类”中定义的那样,因为它们基于“先来先服务”的原则使用数据,因此不能保证为给定的数据集产生整体最优的簇集。

基于划分的聚类方法在效率上具有比层次聚类明显的优势,因为它们通过应用启发式方法将数据划分为集群,而不需要评估每对示例之间的相似性。我们将在稍后更详细地探讨一种广泛使用的基于划分的方法,但就目前而言,只需了解这种方法关注的是寻找集群之间的边界,而不是将示例相互连接——这种方法需要远少于示例之间的比较。这种启发式方法在计算上可能非常高效,但有一个缺点是,在分组分配方面可能有些僵硬甚至任意。例如,如果请求五个集群,它将示例划分为所有五个集群;如果某些示例位于两个集群之间的边界上,这些示例将被随意但坚定地放入一个集群或另一个集群。同样,如果四个或六个集群可能更好地分割数据,这不会像层次聚类树状图那样明显。

更复杂的基于模型和密度聚类方法通过估计示例属于每个集群的概率来解决了一些这些不灵活的问题,而不是仅仅将其分配到单个集群。其中一些可能允许集群边界遵循在数据中识别出的自然模式,而不是强制在组之间进行严格的划分。基于模型的方法通常假设一个统计分布,认为示例是从该分布中抽取的。

其中一种方法,称为混合建模,试图解开由从统计分布混合中抽取的示例组成的集合数据——通常是高斯分布(正态钟形曲线)。例如,想象你有一个由混合男性和女性音域的语音数据组成的集合数据,如图图 9.4所示(请注意,分布是假设的,并非基于现实世界的数据)。尽管两者之间有一些重叠,但平均而言,男性的音域通常低于女性的音域。

图描述自动生成

图 9.4:混合建模为每个示例分配属于潜在分布之一的概率

考虑到未标记的整体分布(图的下部),混合模型能够为任何给定示例属于男性集群或女性集群的概率分配一个概率,令人难以置信的是,它从未在图的上部单独对男声或女声进行过训练!这是通过发现最有可能生成观察到的整体分布的统计参数,如均值和标准差,在假设涉及特定数量的不同分布的情况下实现的——在这种情况下,是两个高斯分布。

作为一种无监督方法,混合模型将无法知道左边的分布是男性,而右边的分布是女性,但一个人类观察者比较记录时,如果左簇中男性出现的可能性高于右簇,这将是显而易见的。这种技术的缺点是,它不仅需要了解涉及多少分布,还需要假设分布的类型。这可能对许多实际聚类任务来说过于僵化。

另一种名为DBSCAN的强大聚类技术,其命名来源于它所使用的“基于密度的空间聚类应用噪声”方法,该方法用于在数据中识别自然簇。这项获奖技术极其灵活,在处理许多聚类挑战方面表现良好,例如适应数据集的自然簇数量、对簇之间的边界灵活处理,以及不对数据进行特定的统计分布假设。

虽然实现细节超出了本书的范围,但 DBSCAN 算法可以直观地理解为创建一个过程,该过程为簇中的示例创建邻域,这些示例都在给定半径内。在指定半径内预定义的核心点形成初始簇核,然后位于任何核心点指定半径内的点被添加到簇中,并构成簇的最外层边界。与许多其他聚类算法不同,一些示例可能根本不会被分配到任何簇中,因为任何距离核心点不够近的点将被视为噪声。

尽管 DBSCAN 强大且灵活,但可能需要实验来优化参数以适应数据,例如构成核心点的数量或点之间的允许半径,这增加了机器学习项目的时间复杂度。当然,仅仅因为基于模型的方法更复杂,并不意味着它们适合每个聚类项目。正如我们将在本章剩余部分看到的那样,一个简单的基于分区的方法在具有挑战性的实际聚类任务上可以表现得非常出色。

尽管混合模型和 DBSCAN 在本章中没有演示,但有一些 R 包可以用来将这些方法应用于您自己的数据。mclust包可以将模型拟合到高斯分布的混合,而dbscan包提供了 DBSCAN 算法的快速实现。

k-means 聚类算法

k-means 算法可能是最常使用的聚类方法,并且是分区聚类方法的一个例子。经过几十年的研究,它成为了许多更复杂聚类技术的基础。如果你理解它使用的简单原则,你将拥有理解今天使用的几乎所有聚类算法所需的知识。

随着 k-means 算法随着时间的推移而发展,出现了许多算法的实现。一种早期的方法在*《k-means 聚类算法,Hartigan, J.A., Wong, M.A., 应用统计学,1979,第 28 卷,第 100-108 页》*中进行了描述。

尽管自 k-means 算法诞生以来聚类方法已经发展,但这并不意味着 k-means 已经过时。事实上,这个方法可能比以往任何时候都更受欢迎。以下表格列出了 k-means 仍然被广泛使用的一些原因:

优点缺点

|

  • 使用可以非统计术语解释的简单原则

  • 非常灵活,可以通过简单的调整来应对许多其不足之处

  • 在许多实际应用场景下表现良好

|

  • 不像更现代的聚类算法那样复杂

  • 由于它使用随机性的元素,不能保证找到最优的聚类集

  • 需要对数据中自然存在的聚类数量进行合理的猜测

  • 不适合非球形聚类或密度差异很大的聚类

|

如果你熟悉 k-means 这个名字,你可能是在回忆第三章中提出的k 近邻算法k-NN)。正如你很快就会看到的,k-means 与 k-NN 的共同之处不仅仅在于字母 k。

k-means 算法将每个n个示例分配给k个聚类中的一个,其中k是一个事先确定的数字。目标是使每个聚类内示例的特征值差异最小化,并使聚类之间的差异最大化。

除非kn非常小,否则无法计算所有可能的示例组合的最优聚类。相反,算法使用一种启发式过程来找到局部最优解。简单来说,这意味着它从一个初始的聚类分配猜测开始,然后稍微修改分配以查看这些变化是否改善了聚类内的同质性。

我们将在稍后深入探讨这个过程,但算法本质上涉及两个阶段。首先,它将示例分配给一组初始的k个聚类。然后,根据当前属于聚类的示例调整聚类边界来更新分配。这个过程会多次更新和分配,直到不再通过改变来改善聚类拟合。此时,过程停止,聚类被最终确定。

由于 k-means 的启发式性质,您可能只需对起始条件进行轻微的改变就会得到不同的结果。如果结果差异很大,这可能表明存在问题。例如,数据可能没有自然的分组,或者k的值选择不当。考虑到这一点,尝试多次进行聚类分析以测试您发现结果的稳健性是个好主意。

为了了解分配和更新过程在实际中的工作方式,让我们回顾一下假设的数据科学会议案例。虽然这是一个简单的例子,但它将说明 k-means 在底层是如何工作的。

使用距离分配和更新聚类

与 k-NN 一样,k-means 将特征值视为多维特征空间中的坐标。对于会议数据,只有两个特征,因此我们可以将特征空间表示为之前描述的两个维度的散点图。

k-means 算法首先在特征空间中选择k个点作为聚类中心。这些中心是推动剩余示例归位的催化剂。通常,这些点是通过从训练数据集中选择k个随机示例来选择的。因为我们希望识别三个聚类,所以使用这种方法,k = 3个点将被随机选择。

这些点在图 9.5中由星号、三角形和菱形表示:

图表,散点图  自动生成的描述

图 9.5:k-means 聚类算法首先通过选择 k 个随机聚类中心开始

值得注意的是,尽管前面图中三个聚类中心恰好分布得很远,但这并不总是必然的情况。因为起始点是随机选择的,三个中心也可能只是三个相邻的点。结合 k-means 算法对聚类中心起始位置高度敏感的事实,一组好的或坏的初始聚类中心可能会对最终的聚类集产生重大影响。

为了解决这个问题,k-means 可以被修改为使用不同的方法来选择初始中心。例如,一个变体选择在特征空间中任何地方出现的随机值,而不是仅从数据中观察到的值中选择。另一个选项是完全跳过这一步;通过随机将每个示例分配给一个聚类,算法可以立即跳到更新阶段。这些方法中的每一种都会给最终的聚类集添加特定的偏差,您可能可以利用这些偏差来改进您的结果。

在 2007 年,引入了一种名为**k-means++的算法,它提出了一种选择初始簇中心的不同方法。它声称这是一种更有效的方法,可以在减少随机机会影响的同时,更接近最优聚类解决方案。更多信息,请参阅《k-means++:谨慎播种的优势,Arthur, D, Vassilvitskii, S, 第十八届 ACM-SIAM 离散算法年度会议论文集,2007 年,第 1,027–1,035 页》

在选择初始簇中心之后,其他示例根据距离函数分配到最近的簇中心,该距离函数用作相似性度量。你可能还记得,我们在学习 k-NN 监督学习算法时使用了距离函数作为相似性度量。像 k-NN 一样,k-means 传统上使用欧几里得距离,但如果需要,也可以使用其他距离函数。

有趣的是,任何返回相似性数值度量的函数都可以用来代替传统的距离函数。事实上,k-means 甚至可以通过使用测量图像或文本对相似性的函数来适应聚类图像或文本文档。

要应用距离函数,请记住,如果n表示特征的数量,那么示例x和示例y之间的欧几里得距离的公式如下:

图片

例如,为了比较一个有五个计算机科学出版物和一个数学出版物的访客与一个没有计算机科学论文但有两位数学论文的访客,我们可以在 R 中这样计算:

> sqrt((5 - 0)² + (1 - 2)²) 
[1] 5.09902 

使用这种方式的距离函数,我们可以找到每个示例与每个簇中心的距离。然后,每个示例被分配到最近的簇中心。

请记住,因为我们使用距离计算,所以所有特征都需要是数值的,并且应该在事先将值归一化到标准范围内。第三章中提出的*《懒惰学习 – 使用最近邻进行分类》*方法将有助于这项任务。

如以下图所示,三个簇中心将示例划分为三个分区,分别标记为Cluster ACluster BCluster C。虚线表示由簇中心创建的Voronoi 图的边界。Voronoi 图表示比其他簇中心更接近一个簇中心的区域;所有三个边界相交的顶点是离所有三个簇中心的最大距离。

使用这些边界,我们可以轻松地看到每个初始 k-means 种子的所声称的区域:

图示 描述自动生成

图 9.6:初始簇中心创建了三个“最近”点的三组

现在初始分配阶段已经完成,k-means 算法进入更新阶段。更新簇的第一步是将初始中心移动到新的位置,称为质心,它是当前分配给该簇的点的平均位置。以下图示说明了随着簇中心移动到新的质心,Voronoi 图中的边界也移动,一个曾经位于簇 B(由箭头指示)的点被添加到簇 A

图解描述自动生成

图 9.7:更新阶段移动簇中心,导致一个点的重新分配

由于这次重新分配,k-means 算法将继续通过另一个更新阶段。在移动簇质心、更新簇边界并将点重新分配到新的簇(如箭头所示)之后,图看起来是这样的:

图解描述自动生成

图 9.8:在另一次更新后,另外两个点被重新分配到最近的簇中心

由于另外两个点被重新分配,必须进行另一次更新,这将移动质心并更新簇边界。然而,因为这些变化没有导致重新分配,k-means 算法停止。簇分配现在是最终的:

图表描述自动生成,置信度中等

图 9.9:更新阶段没有导致新的簇分配后,聚类停止

最终的簇可以通过两种方式之一进行报告。首先,你可能只是简单地报告每个示例的 A、B 或 C 簇的分配。或者,你可以在最终更新后报告簇质心的坐标。

无论是采用哪种报告方法,你都可以计算另一种方法;你可以使用每个簇示例的坐标来计算质心,或者你可以使用质心坐标来将每个示例分配到其最近的簇中心。

选择合适的簇数量

在 k-means 的介绍中,我们了解到该算法对随机选择的簇中心很敏感。确实,如果我们之前在示例中选择了不同的三个起始点组合,我们可能会找到与我们预期不同的数据分割的簇。同样,k-means 对簇的数量也很敏感;选择需要微妙的平衡。将k设置得非常大可以提高簇的同质性,同时它也冒着过度拟合数据的风险。

理想情况下,你将拥有关于真实分组的前置知识(即先验信念),并可以应用这些信息来选择簇的数量。例如,如果你对电影进行聚类,你可能首先将k设置为考虑的奥斯卡奖项类别数量。在我们之前解决的数据科学会议座位问题中,k可能反映了受邀者所属的学术研究领域数量。

有时,簇的数量是由业务需求或分析的动机决定的。例如,会议室中的桌子数量可能决定了从数据科学参会者名单中应该创建多少组人。将这个想法扩展到另一个业务案例,如果营销部门只有资源创建三个不同的广告活动,那么将k = 3分配所有潜在客户到三个吸引之一可能是有意义的。

如果没有任何先验知识,一个经验法则建议将k设置为*(n / 2)的平方根,其中n*是数据集中示例的数量。然而,这个经验法则很可能会导致大型数据集簇的数量过多。幸运的是,还有其他定量方法可以帮助找到合适的 k-means 簇集。

一种称为肘部方法的技术试图评估簇内同质性和异质性与k的不同值如何变化。如图中所示,随着额外簇的增加,簇内的同质性预计会增加;同样,簇内的异质性应该随着簇的增加而减少。因为你可能会继续看到改进,直到每个示例都在自己的簇中,所以目标不是无限期地最大化同质性或最小化异质性,而是找到k,这样在该值之后就没有递减的回报。这个k值被称为肘点,因为它像人手臂的肘关节一样弯曲。

图描述:自动使用中等置信度生成

图 9.10:肘部是增加k导致相对较小改进的点

有许多用于测量簇内同质性和异质性的统计数据,可以与肘部方法(以下信息框提供了更多细节的引用)一起使用。然而,在实践中,并不总是可行地迭代测试大量k值。这部分是因为聚类大型数据集可能相当耗时;重复聚类数据甚至更糟。此外,需要精确最优簇集集的应用很少。在大多数聚类应用中,基于便利性选择k值就足够了,而不是创建最同质簇的值。

对于大量聚类性能指标的全面回顾,请参阅 《聚类验证技术》,Halkidi, M, Batistakis, Y, Vazirgiannis, M, 智能信息系统杂志,2001 年,第 17 卷,第 107-145 页

设置 k 的过程本身有时可以带来有趣的洞察。通过观察随着 k 的变化聚类特征如何变化,人们可以推断数据自然定义的边界在哪里。更加紧密聚类的群体变化很小,而不够同质的群体则会在一段时间内形成和解散。

通常来说,花费少量时间去担心如何精确地确定 k 可能是明智的。接下来的例子将展示即使是来自好莱坞电影的一点点主题知识,也可以用来设定 k,从而找到可操作且有趣的聚类。由于聚类是无监督的,这项任务实际上取决于你如何利用它;价值在于你从算法的发现中获得的洞察。

使用 k-means 聚类寻找青少年市场细分

与 Facebook、TikTok 和 Instagram 等社交网络服务SNS)上的朋友互动,已成为全球青少年的一种仪式。这些青少年拥有相当可观的零花钱,因此他们成为了希望销售零食、饮料、电子产品、娱乐和卫生用品的企业所渴望的目标群体。

使用这些网站的数百万青少年消费者吸引了那些在日益竞争激烈的市场中寻找优势的营销人员的注意。获得这种优势的一种方式是识别具有相似口味的青少年群体,这样客户就可以避免向对所售产品不感兴趣的青少年投放广告。如果向 1,000 名网站访客展示一次广告的成本是 10 美元——这是一种每印象成本的衡量标准——如果我们对目标受众进行选择,广告预算将会更加充裕。例如,运动服装的广告应该针对更有可能对运动感兴趣的个体群体。

通过分析青少年的 SNS 帖子文本,我们可以识别出具有共同兴趣的群体,如体育、宗教或音乐。聚类可以自动化发现该人群自然段落的流程。然而,我们将决定这些聚类是否有趣以及如何用于广告。让我们从头到尾尝试这个过程。

第 1 步 – 收集数据

在这次分析中,我们将使用一个数据集,它代表 2006 年在一家知名 SNS 上有资料的 30,000 名美国高中学生的随机样本。为了保护用户的匿名性,SNS 将不会被命名。然而,在数据收集时,该 SNS 是美国青少年非常受欢迎的网站。因此,可以合理地假设这些资料代表了 2006 年美国青少年的广泛横截面。

我在圣母大学进行自己的青少年身份社会学研究时编制了这个数据集。如果你用于研究目的,请引用这本书的章节。完整的数据库可以在本书的 Packt Publishing GitHub 存储库中找到,文件名为snsdata.csv。为了互动式地跟随,本章假设你已经将此文件保存到你的 R 工作目录中。

数据在四个高中毕业年份(2006 年至 2009 年)之间均匀采样,代表当时的数据收集时的大学一年级、二年级、三年级和四年级学生。使用自动网络爬虫下载了 SNS 个人资料的全文,并记录了每个青少年的性别、年龄和 SNS 朋友数量。

使用文本挖掘工具将剩余的 SNS 页面内容划分为单词。从所有页面中出现的最顶部的 500 个单词中,选择了 36 个单词来代表五个兴趣类别:课外活动、时尚、宗教、浪漫和反社会行为。这 36 个单词包括诸如足球性感亲吻圣经购物死亡毒品等术语。最终的数据库表明,对于每个人,每个单词在个人的 SNS 资料中出现的次数。

第 2 步 – 探索和准备数据

我们将使用read.csv()来加载数据集并将字符数据转换为因子类型:

> teens <- read.csv("snsdata.csv", stringsAsFactors = TRUE) 

让我们也快速看一下数据的详细信息。str()输出的前几行如下所示:

> str(teens) 
'data.frame':	30000 obs. of  40 variables:
 $ gradyear    : int  2006 2006 2006 2006 2006 2006 2006 ...
 $ gender      : Factor w/ 2 levels "F","M": 2 1 2 1 NA 1 1 2 ...
 $ age         : num  19 18.8 18.3 18.9 19 ...
 $ friends     : int  7 0 69 0 10 142 72 17 52 39 ...
 $ basketball  : int  0 0 0 0 0 0 0 0 0 0 ... 

正如我们所预期的,数据包括 30,000 名青少年,其中四个变量表示个人特征,36 个单词表示兴趣。

你注意到gender行周围有什么奇怪的地方吗?如果你仔细观察,你可能已经注意到那个NA值,它与12值相比显得格格不入。NA是 R 告诉我们记录有一个缺失值的方式——我们不知道这个人的性别。到目前为止,我们还没有处理缺失数据,但它可能对许多类型的分析是一个重大问题。

让我们看看这个问题有多严重。一个选项是使用table()命令,如下所示:

> table(teens$gender) 
 F     M 
22054  5222 

虽然这告诉我们有多少个FM值存在,但table()函数排除了NA值,而不是将其作为一个单独的分类处理。要包括NA值(如果有的话),我们只需要添加一个额外的参数:

> table(teens$gender, useNA = "ifany") 
 F     M  <NA> 
22054  5222  2724 

在这里,我们看到有 2,724 条记录(九%)有缺失的性别数据。有趣的是,SNS 数据中女性的数量是男性的四倍多,这表明男性不像女性那样倾向于使用这个社交媒体网站。

如果你检查数据框中的其他变量,你会发现除了gender之外,只有age有缺失值。对于数值特征,summary()函数的默认输出包括NA值的计数:

> summary(teens$age) 
 Min. 1st Qu.  Median    Mean 3rd Qu.    Max.    NA's
  3.086  16.310  17.290  17.990  18.260 106.900    5086 

总共有 5,086 条记录(17%)有缺失的年龄。另外,最小值和最大值似乎不合理;一个三岁的孩子或一个 106 岁的孩子上高中是不太可能的。为了确保这些极端值不会对分析造成问题,我们将在继续之前清理它们。

高中生可能更合理的年龄范围包括那些至少 13 岁且未满 20 岁的人。任何超出这个范围的年龄值应被视为缺失数据——我们无法相信提供的年龄。为了重新编码age变量,我们可以使用ifelse()函数,如果年龄至少为 13 岁且小于 20 岁,则将teen$age赋予原始值;否则,它将接收值NA

> teens$age <- ifelse(teens$age >= 13 & teens$age < 20,
                        teens$age, NA) 

通过重新检查summary()输出,我们看到范围现在遵循的分布看起来更像是实际的高中:

> summary(teens$age) 
 Min. 1st Qu.  Median    Mean 3rd Qu.    Max.    NA's
  13.03   16.30   17.27   17.25   18.22   20.00    5523 

不幸的是,现在我们创建了一个更大的缺失数据问题。在我们继续分析之前,我们需要找到处理这些值的方法。

数据准备 - 虚拟编码缺失值

处理缺失值的一个简单方法是不包含任何缺失值的记录。然而,如果你考虑这种做法的后果,你可能在做之前会三思而后行——仅仅因为它容易并不意味着这是一个好主意!这种方法的问题在于,即使缺失并不广泛,你也很容易排除大量数据。

例如,假设在我们的数据中,性别为NA的人与缺失年龄数据的人完全不同。这表明,通过排除缺失性别或年龄的人,你会排除*9% + 17% = 26%*的数据,或者超过 7,500 条记录。而且这仅仅是两个变量的缺失数据!数据集中缺失值的数量越多,任何给定记录被排除的可能性就越大。很快,你将只剩下一个非常小的数据子集,或者更糟糕的是,剩余的记录将系统性地不同或不能代表整个总体。

对于性别等分类数据,一个替代方案是将缺失值视为一个单独的分类。例如,我们不仅可以限制为女性和男性,还可以添加一个未知性别的额外分类。这使我们能够利用虚拟编码,这在第三章懒惰学习 - 使用最近邻进行分类中已有介绍。

如果你记得,虚拟编码涉及为每个名义特征的每个级别创建一个单独的二进制(1 或 0)值虚拟变量,除了一个作为参考组保留的外。一个类别可以排除的原因是,其状态可以从其他类别中推断出来。例如,如果某人既不是女性也不是未知性别,那么他们一定是男性。因此,在这种情况下,我们只需要为女性和未知性别创建虚拟变量:

> teens$female <- ifelse(teens$gender == "F" &
                           !is.na(teens$gender), 1, 0)
> teens$no_gender <- ifelse(is.na(teens$gender), 1, 0) 

如你所预期,is.na() 函数测试性别是否等于 NA。因此,第一个语句在性别等于 F 且性别不等于 NA 时将 teens$female 赋值为 1;否则,它赋值为 0。在第二个语句中,如果 is.na() 返回 TRUE,意味着性别缺失,那么 teens$no_gender 变量被赋值为 1;否则,它被赋值为 0

为了确认我们工作正确,让我们将我们构建的虚拟变量与原始的 gender 变量进行比较:

> table(teens$gender, useNA = "ifany") 
 F     M  <NA> 
22054  5222  2724 
> table(teens$female, useNA = "ifany") 
 0     1 
 7946 22054 
> table(teens$no_gender, useNA = "ifany") 
 0     1 
27276  2724 

对于 teens$femaleteens$no_gender1 值数量与 FNA 值的数量相匹配,所以编码已经被正确执行。

数据准备 – 填充缺失值

接下来,让我们消除 5,523 个缺失的年龄。由于 age 是一个数值特征,为未知值创建一个额外的类别是没有意义的——你将如何将“未知”与其他年龄进行比较?相反,我们将使用一种称为插补的不同策略,它涉及用对真实值的猜测来填充缺失数据。

你能想到一种方法,我们可以利用 SNS 数据来对青少年的年龄做出有根据的猜测吗?如果你想到了使用毕业年份,你的想法是对的。在一个毕业班级中,大多数人都是在同一年出生的。如果我们能确定每个班级的典型年龄,那么我们就能对那个毕业年份的学生年龄有一个合理的估计。

找到一个典型值的一种方法是通过计算平均值或均值。如果我们尝试像之前分析那样应用 mean() 函数,会出现问题:

> mean(teens$age) 
[1] NA 

问题在于,对于包含缺失数据的向量,平均值是未定义的。由于我们的年龄数据包含缺失值,mean(teens$age) 返回一个缺失值。我们可以通过在计算平均值之前添加一个额外的 na.rm 参数来删除缺失值来纠正这个问题:

> mean(teens$age, na.rm = TRUE) 
[1] 17.25243 

这表明我们数据中的平均学生年龄大约是 17 岁。这只能让我们走了一半的路;我们实际上需要每个毕业年份的平均年龄。你可能会首先尝试计算四次平均值,但 R 的一个好处通常是有一个避免重复的方法。在这种情况下,aggregate() 函数就是这项工作的工具。它为数据的子组计算统计数据。在这里,它通过删除 NA 值来计算按毕业年份的平均年龄:

> aggregate(data = teens, age ~ gradyear, mean, na.rm = TRUE) 
 gradyear      age
1     2006 18.65586
2     2007 17.70617
3     2008 16.76770
4     2009 15.81957 

aggregate()的输出是一个数据框。这需要额外的工作才能将其合并回我们的原始数据。作为替代方案,我们可以使用ave()函数,该函数返回一个向量,其中每个组的平均值被重复,使得结果向量与原始向量长度相同。当aggregate()为每个毕业年份返回一个平均年龄(总共四个值)时,ave()函数为所有 30,000 名青少年返回一个值,反映该学生毕业年份的学生平均年龄(相同的四个值被重复以达到总共 30,000 个值)。

当使用ave()函数时,第一个参数是要计算组平均值的数值向量,第二个参数是提供组分配的类别向量,而FUN参数是要应用于数值向量的函数。在我们的情况下,我们需要定义一个新的函数,该函数在计算平均值时移除NA值。完整的命令如下:

> ave_age <- ave(teens$age, teens$gradyear, FUN =
                  function(x) mean(x, na.rm = TRUE)) 

为了将这些平均值填充到缺失值中,我们需要再调用一次ifelse()函数,仅当原始年龄值为NA时才使用ave_age值:

> teens$age <- ifelse(is.na(teens$age), ave_age, teens$age) 

summary()的结果显示,缺失值现在已经消除:

> summary(teens$age) 
 Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
  13.03   16.28   17.24   17.24   18.21   20.00 

数据分析准备就绪后,我们就准备好深入这个项目的有趣部分了。让我们看看我们的努力是否得到了回报。

第 3 步 – 在数据上训练模型

为了将青少年聚类到营销细分市场,我们将使用stats包中的 k-means 实现,这应该默认包含在您的 R 安装中。尽管其他 R 包中提供了许多更复杂的 k-means 函数,但默认stats包中的kmeans()函数被广泛使用,并提供了简单而强大的算法实现。

文本描述自动生成

图 9.11:K-means 聚类语法

kmeans()函数需要一个只包含数值数据的数据框或矩阵,以及指定k,即所需聚类数量的参数。如果您准备好了这两件事,构建模型的实际过程就很简单了。麻烦的是,选择正确的数据和聚类组合可能有点像艺术;有时需要大量的尝试和错误。

我们将开始我们的聚类分析,仅考虑 36 个特征,这些特征衡量了各种基于兴趣的关键词在青少年社交媒体个人资料文本中出现的次数。换句话说,我们不会基于年龄、毕业年份、性别或朋友数量进行聚类。当然,如果我们愿意,我们可以使用这四个特征,但我们选择不这样做,因为基于这些特征建立的任何聚类都比基于兴趣的聚类缺乏洞察力。这主要是因为年龄和性别已经是事实上的聚类,而基于兴趣的聚类在我们的数据中尚未被发现。其次,稍后更感兴趣的是看看兴趣聚类是否与聚类过程中保留的性别和受欢迎程度特征相关。如果基于兴趣的聚类可以预测这些个体特征,这提供了证据表明聚类可能是有用的。

为了避免意外包含其他特征,让我们创建一个名为 interests 的数据框,通过子集化数据框仅包含 36 个关键词列:

> interests <- teens[5:40] 

如果你还记得 第三章懒惰学习 – 使用最近邻进行分类,在执行任何使用距离计算的分析的任何分析之前,一个常见的做法是对特征进行归一化或 z 分数标准化,以便每个特征都利用相同的范围。通过这样做,你可以避免一个问题,即某些特征仅仅因为它们的值范围比其他特征大而主导。

z 分数标准化的过程重新调整特征,使它们具有均值为零和标准差为一。这种转换以可能在这里有用的方式改变了数据的解释。具体来说,如果有人在他们的个人资料中提到篮球三次,没有其他信息,我们无法知道这是否意味着他们比同龄人更喜欢篮球或更不喜欢篮球。另一方面,如果 z 分数是三,我们知道他们比平均青少年提到了篮球许多次。

要将 z 分数标准化应用于 interests 数据框,我们可以使用 scale() 函数结合 lapply()。由于 lapply() 返回一个列表对象,必须使用 as.data.frame() 函数将其转换回数据框形式,如下所示:

> interests_z <- as.data.frame(lapply(interests, scale)) 

为了确认转换是否正确,我们可以比较旧 interests 数据中 basketball 列的摘要统计:

> summary(interests$basketball) 
 Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
 0.0000  0.0000  0.0000  0.2673  0.0000 24.0000 
> summary(interests_z$basketball) 
 Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
-0.3322 -0.3322 -0.3322  0.0000 -0.3322 29.4923 

如预期的那样,interests_z 数据集将篮球特征转换成了均值为零,范围跨越零上和零下的值。现在,一个小于零的值可以解释为一个人在其个人资料中篮球提及次数少于平均水平。一个大于零的值则意味着这个人比平均水平更频繁地提及篮球。

我们最后的决定是决定使用多少个簇来分割数据。如果我们使用太多的簇,我们可能会发现它们过于具体而无法使用;相反,选择太少的簇可能会导致异质分组。你应该对实验k的值感到舒适。如果你不喜欢结果,你可以轻松尝试另一个值并重新开始。

如果你熟悉分析人群,选择簇的数量会更容易。对真实自然分组数量的直觉猜测可以节省一些尝试和错误。

为了帮助选择数据中的簇数量,我将参考我最喜欢的电影之一,《早餐俱乐部》,这是一部 1985 年上映的青春喜剧,由约翰·休斯执导。这部电影中的青少年角色根据以下五个身份进行自我描述:

  • 一个大脑 - 也常被称为“书呆子”或“极客”

  • 一个运动员 - 有时也被称为“运动健将”或“预备役”

  • 一个篮子案 - 指的是焦虑或神经质的人的俚语术语,在电影中被描绘为一个反社会的局外人

  • 一个公主 - 描绘为受欢迎、富有且具有刻板女性形象的女孩

  • 一个罪犯 - 代表社会学研究中描述的传统“燃尽”身份,参与反学校和反权威的叛逆行为

尽管这部电影描绘了五个具体的身份群体,但它们已经在多年的流行青少年小说中被描述,尽管随着时间的推移,这些刻板印象已经发生了变化,但美国青少年可能仍然会本能地理解它们。因此,五个似乎是一个合理的起始点来选择k,尽管诚然,它不太可能捕捉到高中身份的全貌。

要使用 k-means 算法将青少年的兴趣数据划分为五个簇,我们使用kmeans()函数对interests数据框进行操作。请注意,由于 k-means 使用随机起始点,因此使用set.seed()函数以确保结果与以下示例中的输出相匹配。如果你还记得前面的章节,此命令初始化 R 的随机数生成器到一个特定的序列。如果没有这个语句,每次运行 k-means 算法时结果可能会变化。按照以下方式运行 k-means 聚类过程将创建一个名为teen_clusters的列表,该列表存储了五个簇的属性:

> set.seed(2345)
> teen_clusters <- kmeans(interests_z, 5) 

让我们深入探讨一下,看看算法如何将青少年的兴趣数据进行了划分。

如果你发现你的结果与以下章节中显示的结果不同,请确保在运行kmeans()函数之前立即运行set.seed(2345)命令。此外,由于 R 的随机数生成器的行为随着 R 版本 3.6 而改变,如果你使用的是较旧的 R 版本,你的结果也可能与这里显示的结果略有不同。

第 4 步 – 评估模型性能

评估聚类结果可能具有一定的主观性。最终,模型的成功或失败取决于聚类是否适用于其预期目的。由于本分析的目标是识别具有相似兴趣的青少年聚类以用于营销目的,我们将主要从定性角度衡量我们的成功。对于其他聚类应用,可能需要更多定量成功的衡量标准。

评估一组聚类的有用性的最基本方法之一是检查每个组中落下的示例数量。如果某些组太大或太小,那么它们不太可能非常有用。

要获取kmeans()聚类的尺寸,只需检查teen_clusters$size组件,如下所示:

> teen_clusters$size 
[1]  1038   601  4066  2696 21599 

在这里,我们看到我们请求的五个聚类。最小的聚类有 601 名青少年(2%),而最大的有 21,599 名(72%)。尽管最大和最小聚类人数之间的差距略令人担忧,但如果不仔细检查这些组,我们不会知道这是否表明了问题。可能的情况是,聚类的尺寸差异表明了某些真实情况,例如一个拥有相似兴趣的大青少年群体,或者这可能是由初始 k-means 聚类中心引起的随机巧合。随着我们开始查看每个聚类的特征,我们将了解更多信息。

有时,k-means 可能会找到极小的聚类——有时小到只有一个点。这可能发生在初始聚类中心之一恰好落在远离其他数据的异常值上。是否将此类小型聚类视为代表极端案例的真正发现,还是随机机会造成的问题,并不总是很清楚。如果你遇到这个问题,可能值得用不同的随机种子重新运行 k-means 算法,看看小型聚类是否对不同的起始点具有鲁棒性。

要更深入地了解聚类,我们可以检查聚类质心的坐标,使用teen_clusters$centers组件,以下是对前四个兴趣的说明:

> teen_clusters$centers 
 basketball    football      soccer   softball
1  0.362160730  0.37985213  0.13734997  0.1272107
2 -0.094426312  0.06691768 -0.09956009 -0.0379725
3  0.003980104  0.09524062  0.05342109 -0.0496864
4  1.372334818  1.19570343  0.55621097  1.1304527
5 -0.186822093 -0.18729427 -0.08331351 -0.1368072 

输出的行(标记为15)指的是五个聚类,而每行的数字表示该聚类对列顶部列出的兴趣的平均值。由于这些值是 z 分数标准化,正值表示所有青少年整体平均水平的上方,而负值表示整体平均水平的下方。

例如,第四行在“篮球”列中具有最高值,这意味着在所有聚类中,聚类4对篮球的平均兴趣最高。

通过检查集群是否在每个兴趣类别的平均水平之上或之下,我们可以发现区分集群之间的模式。在实践中,这涉及到打印集群中心,并搜索它们以寻找任何模式或极端值,就像一个数字搜索谜题,但使用的是数字。以下标注的屏幕截图显示了五个集群中的每个集群的突出模式,针对 36 个青少年兴趣中的 18 个:

表格 描述自动生成

图 9.12:为了区分集群,突出其质心的坐标中的模式可能很有帮助

给定这个兴趣数据的快照,我们已能推断出一些集群的特征。集群四在几乎所有运动项目上的兴趣水平都显著高于平均水平,这表明这可能是一个运动员群体,按照*《早餐俱乐部》*的刻板印象。集群三包括最多的拉拉队、舞蹈和“热”这个词的提及。这些是所谓的公主吗?

通过继续以这种方式检查这些集群,可以构建一个表格,列出每个群体的主导兴趣。在下面的表格中,每个集群都展示了使其与其他集群最不同的特征,以及似乎最能准确捕捉群体特征的*《早餐俱乐部》*身份。

有趣的是,集群五之所以与众不同,是因为它并不出众:其成员在所有测量的活动中兴趣水平都低于平均水平。它也是成员数量最多的单个最大群体。我们如何调和这些明显的矛盾?一个可能的解释是,这些用户在网站上创建了一个个人资料,但从未发布过任何兴趣。

图表,表格 描述自动生成

图 9.13:可以使用表格列出每个集群的重要维度

当与利益相关者分享分割分析的结果时,应用易于记忆且信息丰富的标签,即所谓的角色,通常很有帮助,这些标签简化并捕捉了群体的本质,例如在此处应用的*《早餐俱乐部》*类型。添加此类标签的风险是,它们可能会掩盖群体的细微差别,甚至如果使用负面刻板印象,可能会冒犯群体成员。为了更广泛的传播,像“罪犯”和“公主”这样的挑衅性标签可能被更中性的术语如“叛逆青少年”和“时尚青少年”所取代。此外,因为即使是相对无害的标签也可能导致我们的思维产生偏见,如果标签被理解为整个真相而不是复杂性的简化,我们可能会错过重要的模式。

给出如图 9.13 所示的难忘标签和表格,营销主管会对社交网站上的五种青少年访问者类型有一个清晰的思维图像。基于这些人物角色,主管可以向与一个或多个聚类相关的产品相关的企业销售定向广告印象。在下一节中,我们将看到如何将聚类标签应用于原始人口以实现此类用途。

可以使用将多维特征数据展平为二维的技术来可视化聚类分析的结果,然后根据聚类分配给点着色。factoextra 包中的 fviz_cluster() 函数允许轻松构建此类可视化。如果您对此感兴趣,请加载该包并尝试以下命令以查看青少年 SNS 聚类的可视化:fviz_cluster(teen_clusters, interests_z, geom = "point")。尽管由于重叠点数量众多,这种视觉在 SNS 示例中用途有限,但有时它可以是演示目的的有用工具。为了更好地理解如何创建和理解这些图表,请参阅第十五章利用大数据

第 5 步 – 提高模型性能

由于聚类创建了新的信息,聚类算法的性能至少在一定程度上取决于聚类本身的质量以及如何使用这些信息。在前一节中,我们展示了五个聚类为青少年的兴趣提供了有用且新颖的见解。从这个角度来看,算法似乎表现相当好。因此,我们现在可以将精力集中在将这些见解转化为行动上。

我们将首先将聚类应用于完整的数据集。由 kmeans() 函数创建的 teen_clusters 对象包含一个名为 cluster 的组件,其中包含样本中所有 30,000 个个体的聚类分配。我们可以使用以下命令将其添加到 teens 数据框中:

> teens$cluster <- teen_clusters$cluster 

给定这些新数据,我们可以开始检查聚类分配与个人特征之间的关系。例如,以下是 SNS 数据中前五个青少年的个人信息:

> teens[1:5, c("cluster", "gender", "age", "friends")] 
 cluster gender    age friends
1       5      M 18.982       7
2       3      F 18.801       0
3       5      M 18.335      69
4       5      F 18.875       0
5       1   <NA> 18.995      10 

使用 aggregate() 函数,我们还可以查看聚类的人口统计特征。平均年龄在各个聚类之间变化不大,这并不太令人惊讶,因为青少年的身份通常在高中之前就已经确立。这如下所示:

> aggregate(data = teens, age ~ cluster, mean) 
 cluster      age
1       1 17.09319
2       2 17.38488
3       3 17.03773
4       4 17.03759
5       5 17.30265 

另一方面,各个聚类中女性比例的差异相当显著。这是一个非常有趣的发现,因为我们没有使用性别数据来创建聚类,但聚类仍然可以预测性别:

> aggregate(data = teens, female ~ cluster, mean) 
 cluster    female
1       1 0.8025048
2       2 0.7237937
3       3 0.8866208
4       4 0.6984421
5       5 0.7082735 

回想一下,总体而言,大约 74%的社交网络用户是女性。第三组,所谓的“公主”,女性比例高达 89%,而第四组和第五组女性比例仅为大约 70%。这些差异表明,青少年男孩和女孩在社交网络页面讨论的兴趣存在差异。

由于我们在预测性别方面的成功,你可能会怀疑聚类也可以预测用户拥有的朋友数量。这个假设似乎得到了以下数据的支持:

> aggregate(data = teens, friends ~ cluster, mean) 
 cluster  friends
1       1 30.66570
2       2 32.79368
3       3 38.54575
4       4 35.91728
5       5 27.79221 

平均而言,“公主”拥有最多的朋友(38.5 人),其次是“运动员”(35.9 人)和“大脑”(32.8 人)。在低端是“罪犯”(30.7 人)和“疯子”(27.8 人)。与性别一样,考虑到我们没有将友谊数据作为聚类算法的输入,一个青少年的朋友数量与其预测的聚类之间的联系是显著的。同样有趣的是,朋友数量似乎与每个聚类的刻板印象中的高中受欢迎程度有关:那些刻板印象中受欢迎的群体在现实中往往有更多的朋友。

团体成员资格、性别和朋友的数量之间的关联表明,聚类可以作为行为的有用预测指标。以这种方式验证其预测能力可能会使得在向营销团队推销时,聚类分析结果更容易被接受,从而最终提高算法的性能。

正如《早餐俱乐部》中的角色最终意识到的那样,“我们每个人都是一个大脑、一个运动员、一个疯子、一个公主和一个罪犯”,数据科学家意识到我们分配给每个聚类的标签或角色是刻板印象,并且个体可能以不同程度体现这些刻板印象,这一点很重要。在采取聚类分析结果时,请记住这个警告;一个群体可能相对同质,但每个成员仍然是独一无二的。

摘要

我们的研究支持了那句流行的谚语:“物以类聚,人以群分。”通过使用机器学习方法将具有相似兴趣的青少年进行聚类,我们能够开发出青少年身份类型的分类,这些分类可以预测个人特征,如性别和朋友的数量。这些相同的方法可以应用于其他具有相似结果的环境中。

本章仅涵盖了聚类的基础知识。k-means 算法有许多变体,以及其他许多聚类算法,它们为任务带来了独特的偏见和启发式方法。基于本章的基础,你将能够理解这些聚类方法并将它们应用于新的问题。

在下一章中,我们将开始探讨适用于许多机器学习任务的测量学习算法成功的方法。虽然我们的过程一直致力于评估学习的成功,但为了获得最高程度的性能,能够以最严格的标准定义和衡量它是至关重要的。

加入我们书籍的 Discord 空间

加入我们的 Discord 社区,与志同道合的人相聚,并与超过 4000 人一起学习:

packt.link/r

图片

第十章:评估模型性能

当只有富人才能负担得起教育时,考试和测试并不是用来评估学生的。相反,测试评估的是教师,以便父母了解他们的孩子是否学到了足够的知识来证明教师工资的合理性。显然,这与今天的情况不同。现在,这样的评估被用来区分表现优异和表现不佳的学生,将他们筛选到不同的职业和其他机会中。

由于这个过程的重要性,大量的努力被投入到开发准确的学业评估中。公平的评估包含大量的问题,覆盖广泛的主题,并奖励真正的知识而非幸运猜测。一个好的评估还要求学生思考他们以前从未面临过的问题。因此,正确的回答反映了更广泛地概括知识的能力。

评估机器学习算法的过程与评估学生的过程非常相似。由于算法具有不同的优势和劣势,测试应该区分学习者。了解学习者如何在未来的数据上表现也很重要。

本章提供了评估机器学习者的所需信息,例如:

  • 为什么预测准确性不足以衡量性能,以及你可能使用的其他性能衡量指标

  • 确保性能衡量指标合理反映模型预测或预测未见案例的能力的方法

  • 如何使用 R 将这些更有用的衡量指标和方法应用于前面章节中涵盖的预测模型

正如学习一个主题的最佳方式是尝试向其他人教授它一样,教授和评估机器学习者的过程将使你对迄今为止学到的方法有更深入的了解。

测量分类性能

在前面的章节中,我们通过将正确预测的数量除以总预测数量来衡量分类器的准确性。这找到了学习者正确案例的比例,错误案例的比例直接得出。例如,假设一个分类器在 10,000 个案例中有 99,990 个正确预测了新生婴儿是否携带可治疗但可能致命的遗传缺陷。这将意味着准确率为 99.99%,错误率仅为 0.01%。

初看起来,这似乎是一个非常宝贵的分类器。然而,在将孩子的生命托付给测试之前收集更多信息是明智的。如果仅在每 10 万个婴儿中有 10 个发现遗传缺陷呢?一个总是预测无缺陷的测试在所有案例中都是正确的,但在最重要的案例中却是错误的。换句话说,尽管分类器非常准确,但它对预防可治疗的出生缺陷并不很有用。

这是类别不平衡问题的一个后果,它指的是数据中大多数记录属于单个类别时所带来的麻烦。

虽然有许多方法可以衡量分类器的性能,但最好的衡量标准始终是捕捉分类器是否在其预期目的上成功的标准。在定义性能指标时,以效用而不是原始准确率为准至关重要。为此,我们将探讨从混淆矩阵中衍生出的各种替代性能指标。然而,在我们开始之前,我们需要考虑如何为评估准备分类器。

理解分类器的预测

评估分类模型的目标是更好地理解其性能如何外推到未来的案例。由于通常在真实环境中测试未经证实的模型是不切实际的,我们通常通过要求模型对由类似未来将要求其执行的任务的案例组成的测试数据集中的案例进行分类来模拟未来条件。通过观察学习者的响应,我们可以了解其优势和劣势。

虽然我们在前面的章节中已经评估了分类器,但值得反思我们可用的数据类型:

  • 实际的类别值

  • 预测的类别值

  • 预测的估计概率

实际和预测的类别值可能显而易见,但它们是评估的关键。就像老师使用答案键——一个正确答案的列表——来评估学生的答案一样,我们需要知道机器学习者的预测的正确答案。目标是维护两个数据向量:一个包含正确或实际类别值,另一个包含预测类别值。这两个向量必须存储相同数量的值,并且顺序相同。预测和实际值可以存储为单独的 R 向量,或者作为单个 R 数据框中的列。

获取这些数据很容易。实际的类别值直接来自测试数据集中的目标。预测的类别值是从基于训练数据构建的分类器中获得的,然后将它应用于测试数据。对于大多数机器学习包来说,这涉及到对一个模型对象和一个测试数据框应用predict()函数,例如predictions <- predict(model, test_data)

到目前为止,我们只使用这两个数据向量来检查分类预测,但大多数模型可以提供另一条有用的信息。尽管分类器对每个示例只做出一个预测,但它可能对某些决策比其他决策更有信心。

例如,一个分类器可能对包含“免费”和“铃声”这两个词的短信有 99%的确定性认为是垃圾邮件,但对包含“tonight”这个词的短信只有 51%的确定性认为是垃圾邮件。在这两种情况下,分类器都将消息分类为垃圾邮件,但它对其中一个决策的确定性远高于另一个。

包含图表的图片 自动生成描述

图 10.1:即使训练数据相同,学习者的预测信心也可能不同

研究这些内部预测概率为评估模型性能提供了有用的数据。如果两个模型犯同样的错误次数,但其中一个更能准确评估其不确定性,那么它就是一个更智能的模型。理想的情况是找到一个在做出正确预测时非常自信,但在面对怀疑时又很谨慎的学习者。信心与谨慎之间的平衡是模型评估的关键部分。

获取内部预测概率的函数调用在 R 包之间有所不同。对于大多数分类器,predict()函数允许一个额外的参数来指定所需的预测类型。要获取单个预测类别,例如垃圾邮件或正常邮件,通常设置type = "class"参数。要获取预测概率,type参数应根据所使用的分类器设置为"prob""posterior""raw""probability"之一。

本书介绍的所有分类器都可以提供预测概率。每个模型的type参数的正确设置都包含在每个模型的语法框中。

例如,要输出在第五章中构建的 C5.0 分类器的预测概率,请使用predict()函数并设置type = "prob",如下所示:

> predicted_prob <- predict(credit_model, credit_test, type = "prob") 

要输出在第四章中开发的短信垃圾邮件分类模型的朴素贝叶斯预测概率,请使用predict()函数并设置type = "raw",如下所示:

> sms_test_prob <- predict(sms_classifier, sms_test, type = "raw") 

在大多数情况下,predict()函数为每个结果类别返回一个概率。例如,在像短信分类器这样的双结局模型中,预测概率可能存储在一个矩阵或数据框中,如下所示:

> head(sms_test_prob) 
 ham         spam
[1,] 9.999995e-01 4.565938e-07
[2,] 9.999995e-01 4.540489e-07
[3,] 9.998418e-01 1.582360e-04
[4,] 9.999578e-01 4.223125e-05
[5,] 4.816137e-10 1.000000e+00
[6,] 9.997970e-01 2.030033e-04 

输出的每一行显示了分类器对垃圾邮件和正常邮件的预测概率。根据概率规则,每行的概率之和为 1,因为这些是相互排斥且穷尽的结局。为了方便起见,在评估过程中,构建一个收集预测类别、实际类别以及感兴趣类别级别的预测概率的数据框可能会有所帮助。

本章 GitHub 仓库中可用的sms_results.csv文件是一个符合这种格式的数据框的示例,它是由第四章中构建的短信分类器的预测构建的。为了简洁起见,省略了构建此评估数据集所需的步骤,因此要跟随这里的示例,只需下载文件并将其使用以下命令加载到数据框中:

> sms_results <- read.csv("sms_results.csv", stringsAsFactors = TRUE) 

生成的sms_results数据框很简单。它包含四个包含 1,390 个值的向量。一列包含表示实际短信消息类型(垃圾邮件或正常邮件)的值,另一列表示朴素贝叶斯模型预测的消息类型,第三和第四列分别表示消息是垃圾邮件或正常邮件的概率:

> head(sms_results) 
 actual_type predict_type prob_spam prob_ham
1         ham          ham   0.00000  1.00000
2         ham          ham   0.00000  1.00000
3         ham          ham   0.00016  0.99984
4         ham          ham   0.00004  0.99996
5        spam         spam   1.00000  0.00000
6         ham          ham   0.00020  0.99980 

对于这六个测试案例,预测值和实际短信消息类型一致;模型正确预测了它们的状态。此外,预测概率表明模型对这些预测非常有信心,因为它们都接近或正好是 0 或 1。

当预测值和实际值与 0 和 1 的距离更远时会发生什么?使用subset()函数,我们可以识别出这些记录中的一小部分。以下输出显示了模型估计垃圾邮件概率在 40%到 60%之间的测试案例:

> head(subset(sms_results, prob_spam > 0.40 & prob_spam < 0.60)) 
 actual_type predict_type prob_spam prob_ham
377         spam          ham   0.47536  0.52464
717          ham         spam   0.56188  0.43812
1311         ham         spam   0.57917  0.42083 

根据模型自己的估计,这些是正确预测几乎等同于抛硬币的情况。然而,所有三个预测都是错误的——一个不幸的结果。让我们看看更多模型预测错误的情况:

> head(subset(sms_results, actual_type != predict_type)) 
 actual_type predict_type prob_spam prob_ham
53         spam          ham   0.00071  0.99929
59         spam          ham   0.00156  0.99844
73         spam          ham   0.01708  0.98292
76         spam          ham   0.00851  0.99149
184        spam          ham   0.01243  0.98757
332        spam          ham   0.00003  0.99997 

这些案例说明了重要的事实,即一个模型可以非常自信,但仍然可能非常错误。所有六个测试案例都是垃圾邮件,分类器认为它们至少有 98%的概率是正常邮件。

尽管存在这样的错误,模型仍然有用吗?我们可以通过将各种错误度量应用于评估数据来回答这个问题。实际上,许多这样的度量都是基于我们在前几章中广泛使用的工具。

混淆矩阵的更详细分析

混淆矩阵是一个表格,根据预测值是否与实际值匹配来分类预测。表格的一个维度表示预测值的可能类别,而另一个维度表示实际值的相同类别。尽管我们到目前为止主要使用的是 2x2 的混淆矩阵,但可以为预测任何数量类别值的模型创建矩阵。以下图显示了熟悉的二类二元模型的混淆矩阵,以及三类的 3x3 混淆矩阵。

当预测值与实际值相同时,这是一种正确的分类。正确的预测位于混淆矩阵的对角线上(用O表示)。对角线外的矩阵单元格(用X表示)表示预测值与实际值不一致的情况。这些都是错误的预测。分类模型的性能度量基于这些表中位于对角线和偏离对角线上的预测数量:

图描述自动生成

图 10.2:混淆矩阵统计预测类别与实际值一致或不一致的情况

最常见的性能度量考虑模型区分一个类别与其他所有类别的能力。目标类别被称为正类,而所有其他类别被称为负类

使用正负术语并不旨在暗示任何价值判断(即,好与坏),也不一定意味着结果的存在或不存在(例如,存在出生缺陷或不存在)。正结果的选择甚至可以是任意的,例如在模型预测晴朗与雨天、狗与猫等类别的情况下。

正类和负类预测之间的关系可以用一个 2x2 的混淆矩阵来表示,该矩阵列出了预测是否属于以下四个类别之一:

  • 真阳性TP):正确地被分类为目标类

  • 真阴性TN):正确地被分类为非目标类

  • 假阳性FP):错误地被分类为目标类

  • 假阴性FN):错误地被分类为非目标类

对于垃圾邮件分类器,正类是垃圾邮件,因为这是我们希望检测的结果。然后我们可以想象混淆矩阵如图图 10.3所示:

图示 描述自动生成

图 10.3:区分正类和负类使混淆矩阵更加详细

以这种方式展示的混淆矩阵是许多最重要的模型性能度量指标的基础。在下一节中,我们将使用这个矩阵来更好地理解准确率的确切含义。

使用混淆矩阵来衡量性能

使用 2x2 混淆矩阵,我们可以将预测准确率(有时称为成功率)的定义形式化如下:

在这个公式中,术语TPTNFPFN分别指模型预测落在这些类别中的次数。因此,准确率是一个比例,表示真实正例和真实负例的数量除以预测总数。

错误率,即错误分类的样本比例,被定义为:

注意,错误率可以计算为 1 减去准确率。直观上,这是有道理的;一个正确率 95%的模型在 5%的时间内是错误的。

将分类器的预测整理成混淆矩阵的一个简单方法是使用 R 的table()函数。创建 SMS 数据混淆矩阵的命令如下所示。该表中的计数可以用来计算准确率和其他统计量:

> table(sms_results$actual_type, sms_results$predict_type) 
 ham spam
  ham  1203    4
  spam   31  152 

如果你想要创建一个具有更多信息的混淆矩阵,gmodels包中的CrossTable()函数提供了一个可定制的解决方案。如果你还记得,我们第一次使用这个函数是在第二章管理和理解数据。如果你当时没有安装这个包,你需要使用install.packages("gmodels")命令来安装。

默认情况下,CrossTable()的输出包括每个单元格中的比例,这些比例表示单元格计数占表格行、列和总计数百分比。输出还包括行和列总计。如下面的代码所示,语法与table()函数类似:

> library(gmodels)
> CrossTable(sms_results$actual_type, sms_results$predict_type) 

结果是一个包含大量额外详细信息的混淆矩阵:

 Cell Contents
|-------------------------|
|                       N |
| Chi-square contribution |
|           N / Row Total |
|           N / Col Total |
|         N / Table Total |
|-------------------------|

Total Observations in Table:  1390 

                        | sms_results$predict_type 
sms_results$actual_type |       ham |      spam | Row Total | 
------------------------|-----------|-----------|-----------|
                    ham |      1203 |         4 |      1207 | 
                        |    16.128 |   127.580 |           | 
                        |     0.997 |     0.003 |     0.868 | 
                        |     0.975 |     0.026 |           | 
                        |     0.865 |     0.003 |           | 
------------------------|-----------|-----------|-----------|
                   spam |        31 |       152 |       183 | 
                        |   106.377 |   841.470 |           | 
                        |     0.169 |     0.831 |     0.132 | 
                        |     0.025 |     0.974 |           | 
                        |     0.022 |     0.109 |           | 
------------------------|-----------|-----------|-----------|
           Column Total |      1234 |       156 |      1390 | 
                        |     0.888 |     0.112 |           | 
------------------------|-----------|-----------|-----------| 

我们在几个前面的章节中使用了CrossTable(),所以到现在你应该熟悉它的输出了。如果你忘记了如何解释输出,只需参考键(标记为“单元格内容”),它提供了表格单元格中每个数字的定义。

我们可以使用混淆矩阵来获取准确率和错误率。由于准确率是(TP + TN)/(TP + TN + FP + FN),我们可以按以下方式计算:

> (152 + 1203) / (152 + 1203 + 4 + 31) 
[1] 0.9748201 

我们还可以计算错误率(FP + FN)/(TP + TN + FP + FN)如下:

> (4 + 31) / (152 + 1203 + 4 + 31) 
[1] 0.02517986 

这与 1 减去准确率相同:

> 10.9748201 
[1] 0.0251799 

虽然这些计算可能看起来很简单,但重要的是要练习思考混淆矩阵的各个组成部分是如何相互关联的。在下一节中,你将看到这些相同的部分可以以不同的方式组合,以创建各种额外的性能指标。

不仅仅是准确性 – 其他性能指标

无数性能指标已经被开发并用于各种学科中,如医学、信息检索、营销和信号检测理论等特定目的。要涵盖所有这些指标可能需要数百页,这使得在这里进行全面的描述变得不可行。相反,我们将仅考虑机器学习文献中最有用和最常引用的一些指标。

Max Kuhn 的caret包包括计算许多此类性能指标的功能。这个包提供了准备、训练、评估和可视化机器学习模型和数据工具;"caret"这个名字是“分类和回归训练”的缩写。由于它对调整模型也很有价值,除了在这里的使用外,我们还将广泛使用caret包在第十四章构建更好的学习者。在继续之前,你需要使用install.packages("caret")命令来安装这个包。

关于caret的更多信息,请参阅Kuhn, M, 使用 caret 包在 R 中构建预测模型,统计软件杂志,2008,第 28 卷或包的非常详尽的文档页面topepo.github.io/caret/index.html

caret 包添加了创建混淆矩阵的另一个函数。如下所示,语法与 table() 类似,但略有不同。因为 caret 计算反映分类正类能力的模型性能度量,所以应指定 positive 参数。在这种情况下,由于 SMS 分类器旨在检测垃圾邮件,我们将设置 positive = "spam" 如下:

> library(caret)
> confusionMatrix(sms_results$predict_type,
    sms_results$actual_type, positive = "spam") 

这会导致以下输出:

Confusion Matrix and Statistics
          Reference
Prediction  ham spam
      ham  1203   31
      spam    4  152

               Accuracy : 0.9748          
                 95% CI : (0.9652, 0.9824)
    No Information Rate : 0.8683          
    P-Value [Acc > NIR] : < 2.2e-16       

                  Kappa : 0.8825          

 Mcnemar’s Test P-Value : 1.109e-05       

            Sensitivity : 0.8306          
            Specificity : 0.9967          
         Pos Pred Value : 0.9744          
         Neg Pred Value : 0.9749          
             Prevalence : 0.1317          
         Detection Rate : 0.1094          
   Detection Prevalence : 0.1122          
      Balanced Accuracy : 0.9136          

       ‘Positive’ Class : spam 

输出顶部是一个类似于 table() 函数生成的混淆矩阵,但已转置。输出还包括一组性能度量。其中一些,如准确度,是熟悉的,而许多其他则是新的。让我们看看一些最重要的指标。

卡方统计量

卡方统计量(在之前的输出中标记为 Kappa)通过考虑仅凭偶然正确预测的可能性来调整准确性。这对于具有严重类别不平衡的数据集尤为重要,因为分类器可以通过始终猜测最频繁的类别来获得高准确率。卡方统计量只会奖励那些比这种简单策略更频繁正确分类的分类器。

定义卡方统计量的方法不止一种。这里描述的最常见的方法使用 Cohen 的卡方系数,如论文 《名义量度的协议系数,Cohen, J, 教育与心理测量,1960,第 20 卷,第 37-46 页》 所述。

卡方值通常在 0 到最大值 1 之间,更高的值反映了模型预测与真实值之间更强的协议。如果预测始终错误,则可能观察到小于 0 的值——也就是说,预测与实际值不一致或错误率高于随机猜测的预期。这种情况在机器学习模型中很少发生,通常反映编码问题,可以通过简单地反转预测来修复。

根据模型的使用方式,卡方统计量的解释可能会有所不同。以下是一个常见的解释示例:

  • 差一致性 = 小于 0.2

  • 公平一致性 = 0.2 至 0.4

  • 中等一致性 = 0.4 至 0.6

  • 良好一致性 = 0.6 至 0.8

  • 非常好一致性 = 0.8 至 1.0

重要的是要注意,这些类别是主观的。虽然“良好一致性”可能足以预测某人的最爱冰淇淋口味,但如果目标是识别出生缺陷,“非常好一致性”可能就不够了。

关于前述量表更详细的信息,请参阅 《分类数据的观察者一致性测量,Landis, JR, Koch, GG. 生物统计学,1997,第 33 卷,第 159-174 页》

以下是为计算 kappa 统计量提供的公式。在这个公式中,Pr(a) 指的是实际协议的比例,而 Pr(e) 指的是在假设它们是随机选择的情况下,分类器和真实值之间预期协议的比例:

这些比例一旦你知道在哪里寻找,就可以从混淆矩阵中获得。让我们考虑使用 CrossTable() 函数创建的 SMS 分类模型的混淆矩阵,这里为了方便起见重复列出:

 | sms_results$predict_type 
sms_results$actual_type |       ham |      spam | Row Total | 
------------------------|-----------|-----------|-----------|
                    ham |      1203 |         4 |      1207 | 
                        |    16.128 |   127.580 |           | 
                        |     0.997 |     0.003 |     0.868 | 
                        |     0.975 |     0.026 |           | 
                        |     0.865 |     0.003 |           | 
------------------------|-----------|-----------|-----------|
                   spam |        31 |       152 |       183 | 
                        |   106.377 |   841.470 |           | 
                        |     0.169 |     0.831 |     0.132 | 
                        |     0.025 |     0.974 |           | 
                        |     0.022 |     0.109 |           | 
------------------------|-----------|-----------|-----------|
           Column Total |      1234 |       156 |      1390 | 
                        |     0.888 |     0.112 |           | 
------------------------|-----------|-----------|-----------| 

记住,每个单元格的底部值表示所有实例落入该单元格的比例。因此,为了计算观察到的协议比例 Pr(a),我们只需将预测类型和实际短信类型达成一致的实例比例相加。

因此,我们可以计算 Pr(a) 如下:

> pr_a <- 0.865 + 0.109
> pr_a 
[1] 0.974 

对于这个分类器,观察值和实际值有 97.4% 的时间达成一致——你会注意到这与准确率相同。kappa 统计量调整了相对于预期一致性 Pr(e) 的准确率,即仅凭偶然,在假设两者都是根据观察比例随机选择的情况下,预测值和实际值匹配的概率。

为了找到这些观察到的比例,我们可以使用我们在 第四章 中学到的概率规则。假设两个事件是独立的(意味着一个不会影响另一个),概率规则指出,两个事件同时发生的概率等于各自发生的概率的乘积。例如,我们知道选择非垃圾邮件的概率是:

Pr(实际类型是非垃圾邮件) * Pr(预测类型是非垃圾邮件)

选择垃圾邮件的概率是:

Pr(实际类型是垃圾邮件) * Pr(预测类型是垃圾邮件)

预测或实际类型是垃圾邮件或非垃圾邮件的概率可以从行或列总数中获得。例如,Pr(实际类型是非垃圾邮件) = 0.868 和 Pr(预测类型是非垃圾邮件) = 0.888。

Pr(e) 可以通过预测值和实际值都认为消息是垃圾邮件或非垃圾邮件的概率之和来计算。回想一下,对于互斥事件(不能同时发生的事件),任一事件发生的概率等于其概率之和。因此,为了获得最终的 Pr(e),我们只需将两个乘积相加,如下所示:

> pr_e <- 0.868 * 0.888 + 0.132 * 0.112
> pr_e 
[1] 0.785568 

由于 Pr(e) 是 0.786,仅凭偶然,我们预计观察值和实际值将有大约 78.6% 的时间达成一致。

这意味着我们现在拥有了完成 kappa 公式的所有信息。将 Pr(a) 和 Pr(e) 值代入 kappa 公式,我们得到:

> k <- (pr_a - pr_e) / (1 - pr_e) 
> k 
[1] 0.8787494 

kappa 大约是 0.88,这与之前 caretconfusionMatrix() 输出相符(小的差异是由于四舍五入)。使用建议的解释,我们注意到分类器的预测值和实际值之间有非常好的协议。

有几个 R 函数可以自动计算 kappa。可视化分类数据VCD)包中的Kappa()函数(请注意大写的“K”),使用预测值和实际值的混淆矩阵。通过输入install.packages("vcd")安装包后,可以使用以下命令获取 kappa:

> library(vcd)
> Kappa(table(sms_results$actual_type, sms_results$predict_type)) 
 value     ASE     z Pr(>|z|)
Unweighted 0.8825 0.01949 45.27        0
Weighted   0.8825 0.01949 45.27        0 

我们对无权重的 kappa 值感兴趣。0.88 的值与我们手动计算的结果相符。

当存在不同程度的协议时,使用加权 kappa。例如,使用冷、凉爽、温暖和热的刻度,温暖与热的值比与冷的值更一致。在两个结果事件的情况下,如垃圾邮件和正常邮件,加权 kappa 和未加权 kappa 统计量将是相同的。

Interrater Reliabilityirr)包中的kappa2()函数可以用来从数据框中存储的预测值和实际值的向量中计算 kappa。在通过install.packages("irr")安装包之后,可以使用以下命令获取 kappa:

> library(irr)
> kappa2(sms_results[1:2]) 
Cohen's Kappa for 2 Raters (Weights: unweighted)
 Subjects = 1390 
   Raters = 2 
    Kappa = 0.883 
        z = 33 
  p-value = 0 

Kappa()kappa2()函数报告相同的 kappa 统计量,因此使用您更舒适的选项。

请注意不要使用内置的kappa()函数。它与之前报告的 kappa 统计量完全无关!

矩阵相关系数

尽管准确性和 kappa 多年来一直是性能的流行指标,但第三个选项迅速成为机器学习领域的实际标准。与先前的指标一样,矩阵相关系数MCC)是一个单一统计量,旨在反映分类模型的总体性能。此外,MCC 与 kappa 类似,即使在数据集严重不平衡的情况下(在这种情况下,传统的准确度度量可能会非常误导),它也是有用的。

由于其易于解释,以及越来越多的证据表明它在比 kappa 更广泛的情境下表现更好,MCC 越来越受欢迎。最近的经验研究表明,MCC 可能是描述二元分类模型现实世界性能的最佳单一指标。其他研究已经确定了可能导致 kappa 统计量提供误导或不正确模型性能描述的潜在情境。在这些情况下,当 MCC 和 kappa 不一致时,MCC 指标往往能更合理地评估模型的真正能力。

关于马修斯相关系数与 k 值相对优势的更多信息,请参阅 The Matthews correlation coefficient (MCC) is more informative than Cohen’s kappa and brier score in binary classification assessment, Chicco D, Warrens MJ, Jurman G, IEEE Access, 2021, Vol. 9, pp. 78368-78381。或者,参考 Why Cohen’s Kappa should be avoided as performance measure in classification, Delgado R, Tibau XA, PLoS One, 2019, Vol. 14(9):e0222916

MCC 的值解释与皮尔逊相关系数相同,该系数在 第六章预测数值数据 – 回归方法 中介绍。这个范围从 -1 到 +1,分别表示完全不准确和完全准确的预测。值为 0 表示模型的表现不优于随机猜测。由于大多数 MCC 分数都位于 0 和 1 之间的某个值域内,因此“良好”分数的判断具有一定的主观性。与皮尔逊相关系数使用的刻度类似,一种可能的解释如下:

  • 完全错误 = -1.0

  • 强度错误 = -0.5 到 -1.0

  • 中度错误 = -0.3 到 -0.5

  • 弱度错误 = -0.1 到 0.3

  • 随机正确 = -0.1 到 0.1

  • 轻度正确 = 0.1 到 0.3

  • 中度正确 = 0.3 到 0.5

  • 强度正确 = 0.5 到 1.0

  • 完全正确 = 1.0

注意,表现最差的模型位于刻度中间。换句话说,位于刻度负侧(从完全错误到轻度错误)的模型仍然比随机预测的模型表现更好。例如,即使强度错误的模型的准确度很差,预测结果也可以简单地反转以获得正确的结果。

与所有此类刻度一样,这些刻度只能作为粗略的指南。此外,像 MCC 这样的度量标准的关键好处不是理解模型在孤立状态下的性能,而是促进跨多个模型的性能比较。

对于二分类器的混淆矩阵,MCC 可以通过以下公式计算:

使用 SMS 垃圾邮件分类模型的混淆矩阵,我们得到以下值:

  • TN = 1203

  • FP = 4

  • FN = 31

  • TP = 152

然后,可以在 R 中手动计算 MCC,如下所示:

> (152 * 1203 - 4 * 31) /
    sqrt((152 + 4) * (152 + 31) * (1203 + 4) * (1203 + 31)) 
[1] 0.8861669 

Ben Gorman 开发的 mltools 包提供了一个 mcc() 函数,该函数可以使用预测值和实际值的向量执行 MCC 计算。安装该包后,以下 R 代码产生的结果与手动计算的结果相同:

> library(mltools)
> mcc(sms_results$actual_type, sms_results$predict_type) 
[1] 0.8861669 

或者,对于将正类编码为 1 且将负类编码为 0 的二分类器,MCC 与预测值和实际值之间的皮尔逊相关系数相同。我们可以使用 R 中的 cor() 函数来演示这一点,在将分类值("spam""ham")转换为二进制值(10)后,如下所示:

> cor(ifelse(sms_results$actual_type == "spam", 1, 0),
      ifelse(sms_results$predict_type == "spam", 1, 0)) 
[1] 0.8861669 

这样一个显然的分类性能指标竟然隐藏在显而易见的地方,作为一个简单的对 19 世纪末引入的皮尔逊相关性的改编,这使得 MCC(Matthews Correlation Coefficient)仅在最近几十年才变得流行!生物化学家布莱恩·W·马修斯在 1975 年负责推广这个指标用于双分类问题,因此他因这一特定应用而获得命名荣誉。然而,似乎很可能是这个指标已经被广泛使用,即使它直到很久以后才引起了很多关注。如今,它在工业界、学术研究和甚至作为机器学习竞赛的基准中得到应用。可能没有单一的指标能更好地捕捉二元分类模型的总体性能。然而,正如你很快就会看到的,通过组合多个指标可以获得对模型性能的更深入理解。

虽然 MCC 在这里是为二元分类定义的,但它是否是多类结果的最佳指标尚不清楚。关于这一点和其他替代方案的讨论,请参阅*“多类预测中 MCC 和 CEN 误差测量的比较,Jurman G,Riccadonna S,Furlanello C,2012,PLOS One 7(8): e41882”*。

灵敏度和特异性

寻找一个有用的分类器通常需要在过于保守的预测和过于激进的预测之间取得平衡。例如,一个电子邮件过滤器可以通过激进地过滤几乎所有的正常邮件来保证消除每一封垃圾邮件。另一方面,为了保证没有正常邮件被意外过滤,可能需要我们允许通过过滤器的不合理数量的垃圾邮件。一对性能指标捕捉了这种权衡:灵敏度和特异性。

模型的灵敏度(也称为真正率)衡量的是正确分类的正面样本占所有正面样本的比例。因此,正如以下公式所示,它是通过将真正例的数量除以所有正面样本的总数来计算的,包括那些正确分类的(真正例)和那些错误分类的(假阴性):

图片

模型的特异性(也称为真正率)衡量的是正确分类的负面样本占所有负面样本的比例。与灵敏度一样,这是通过将真正例的数量除以所有负面样本的总数来计算的——包括真正例和假阳性。

图片

对于短信分类器的混淆矩阵,我们可以很容易地手动计算这些指标。假设垃圾邮件是一个正面类别,我们可以确认confusionMatrix()输出中的数字是正确的。例如,灵敏度的计算如下:

> sens <- 152 / (152 + 31)
> sens 
[1] 0.8306011 

同样,对于特异性,我们可以计算:

> spec <- 1203 / (1203 + 4)
> spec 
[1] 0.996686 

caret包提供了从预测值和实际值向量直接计算敏感性和特异性的函数。请确保适当地指定positivenegative参数,如下所示:

> library(caret)
> sensitivity(sms_results$predict_type, sms_results$actual_type,
              positive = "spam") 
[1] 0.8306011 
> specificity(sms_results$predict_type, sms_results$actual_type,
              negative = "ham") 
[1] 0.996686 

敏感性和特异性范围从 0 到 1,接近 1 的值更受欢迎。当然,找到两者之间的适当平衡是很重要的——这是一个通常非常具体于上下文的任务。

例如,在这种情况下,0.831 的敏感性意味着 83.1%的垃圾邮件被正确分类。同样,0.997 的特异性意味着 99.7%的非垃圾邮件被正确分类,或者换句话说,0.3%的有效消息被错误地标记为垃圾邮件。拒绝 0.3%的有效短信消息可能是不被接受的,或者考虑到垃圾邮件数量的减少,这可能是合理的权衡。

敏感性和特异性提供了思考此类权衡的工具。通常,会调整模型并测试不同的模型,直到找到一个满足所需敏感性和特异性阈值的模型。本章后面讨论的可视化也可以帮助理解敏感性和特异性之间的平衡。

精确度和召回率

与敏感性和特异性密切相关的是另外两个与分类中做出的妥协相关的性能指标:精确度和召回率。主要在信息检索的背景下使用,这些统计数据旨在表明模型的结果有多有趣和相关性,或者预测是否被无意义的噪声稀释。

精确度(也称为阳性预测值)定义为真正阳性的预测比例;换句话说,当模型预测阳性类别时,它有多正确?一个精确的模型只会预测那些非常可能是阳性的情况。它将非常可靠。

图片

考虑一下,如果模型非常不精确会发生什么。随着时间的推移,结果不太可能被信任。在信息检索的背景下,这类似于像 Google 这样的搜索引擎返回不相关结果。最终,用户可能会转向像 Bing 这样的竞争对手。在短信垃圾邮件过滤器的例子中,高精确度意味着模型能够仔细地只针对垃圾邮件,同时避免在正常邮件中出现误报。

另一方面,召回率是衡量结果完整性的一个指标。如下公式所示,这定义为真正阳性数除以总阳性数。你可能已经认识到这与敏感性相同;然而,解释略有不同。

图片

具有高召回率的模型能够捕获大量正例,这意味着它具有广泛的覆盖面。例如,具有高召回率的搜索引擎会返回大量与搜索查询相关的文档。同样,如果大多数垃圾邮件消息被正确识别,短信垃圾邮件过滤器也具有高召回率。

我们可以从混淆矩阵中计算出精确度和召回率。再次假设垃圾邮件是一个正类,精确度是:

> prec <- 152 / (152 + 4)
> prec 
[1] 0.974359 

召回率是:

> rec <- 152 / (152 + 31)
> rec 
[1] 0.8306011 

可以使用caret包从预测和实际类别的向量中计算这些度量之一。精确度使用posPredValue()函数:

> library(caret)
> posPredValue(sms_results$predict_type, sms_results$actual_type,
               positive = "spam") 
[1] 0.974359 

召回率使用我们之前使用的sensitivity()函数:

> sensitivity(sms_results$predict_type, sms_results$actual_type,
              positive = "spam") 
[1] 0.8306011 

就像敏感性和特异性之间的权衡一样,对于大多数现实世界的问题,很难构建一个既具有高精确度又具有高召回率的模型。如果你只针对容易分类的例子(即低垂的果实),那么很容易做到精确。同样,一个模型通过撒一个非常宽的网,意味着模型在识别正例时过于激进,也容易具有高召回率。相比之下,同时具有高精确度和召回率是非常具有挑战性的。因此,为了找到满足你项目需求的精确度和召回率的组合,测试各种模型是非常重要的。

F 度量

将精确度和召回率结合成一个单一数字的模型性能度量称为F 度量(有时也称为F[1] 分数F 分数)。F 度量通过调和平均数结合精确度和召回率,这是一种用于变化率的平均数类型。由于精确度和召回率都表示为 0 到 1 之间的比例,可以解释为变化率,因此使用调和平均数而不是更常见的算术平均数。以下为 F 度量的公式:

要计算 F 度量,使用之前计算出的精确度和召回率值:

> f <- (2 * prec * rec) / (prec + rec) 
> f 
[1] 0.8967552 

这与使用混淆矩阵中的计数完全相同:

> f <- (2 * 152) / (2 * 152 + 4 + 31)
> f 
[1] 0.8967552 

由于 F 度量将模型性能描述为一个单一数字,它提供了一个方便的、定量的指标,可以直接比较多个模型。确实,F 度量曾经几乎成为衡量模型性能的黄金标准,但今天,它似乎比以前使用得少得多。一个可能的解释是,它假设精确度和召回度应该被赋予相同的权重,这个假设并不总是有效的,这取决于现实世界中假阳性和假阴性的实际成本。当然,可以使用不同的精确度和召回度权重来计算 F 分数,但选择权重可能最坏的情况是随意的。尽管如此,也许这个指标不再受欢迎的更重要原因是采用了方法,这些方法可以直观地描绘模型在不同数据子集上的性能,如下一节所述。

使用 ROC 曲线可视化性能权衡

可视化有助于更详细地理解机器学习算法的性能。当诸如敏感度、特异性、精确度和召回率等统计量试图将模型性能简化为一个单一数字时,可视化则描绘了学习者在广泛条件下的表现。

由于学习算法有不同的偏差,两个具有相似准确率的模型可能在达到准确率的方式上存在巨大差异。一些模型可能在某些预测上挣扎,而其他模型则轻松完成,同时轻松处理其他模型难以正确处理的案例。可视化提供了一种方法,通过在单个图表中并排比较学习器来理解这些权衡。

接收者操作特征ROC)曲线通常用于检查在避免假阳性同时检测真阳性的权衡。正如你可能从其名称中猜测到的,ROC 曲线是由通信领域的工程师开发的。在第二次世界大战期间,雷达和无线电操作员使用 ROC 曲线来衡量接收器区分真实信号和虚假警报的能力。同样的技术今天对于可视化机器学习模型的功效也很有用。

关于 ROC 曲线的更多阅读,请参阅*《ROC 分析简介》,Fawcett T,Pattern Recognition Letters,2006 年,第 27 卷,第 861-874 页*。

典型 ROC 图的特征在图 10.4中展示。ROC 曲线使用垂直轴上的真阳性比例和水平轴上的假阳性比例来绘制。因为这些值分别等同于敏感度和(1 – 特异性),所以该图也被称为敏感度/特异性图

图表描述自动生成

图 10.4:ROC 曲线描绘了分类器形状相对于完美和无用分类器

组成 ROC 曲线的点表示在变化的假阳性阈值下的真正例率。为了说明这个概念,前一个图表中对比了三个假设的分类器。首先,完美分类器的曲线通过 100%真正例率和 0%假阳性率的点。它能够在错误地分类任何负例之前正确地识别所有真正例。接下来,从图的下左角到上右角的斜线代表一个无预测价值的分类器。这种分类器以相同的速率检测真正例和假阳性,这意味着分类器无法区分两者。这是其他分类器可以评判的基准。接近这条线的 ROC 曲线表示模型不太有用。最后,大多数现实世界的分类器都像测试分类器一样,它们位于完美和无用之间的区域。

理解 ROC 曲线构建的最佳方式是亲手绘制一个。图 10.5中表格中的数值表示了一个假设的垃圾邮件模型在包含 20 个示例的测试集上的预测结果,其中 6 个是正类(垃圾邮件),14 个是负类(正常邮件)。

表格描述自动生成

图 10.5:为了构建 ROC 曲线,将正类的估计概率值按降序排序,然后与实际类别值进行比较

要创建曲线,需要按照模型对正类估计概率的降序对分类器的预测进行排序,最大的值排在前面,如表中所示。然后,从图表的原点开始,每个预测对真正例率和假阳性率的影响导致曲线垂直于每个正例进行追踪,水平于每个负例进行追踪。这个过程可以在一张坐标纸上手工完成,如图图 10.6所示:

图表描述自动生成

图 10.6:可以在坐标纸上通过绘制正例数量与负例数量的对比来手工绘制 ROC 曲线

注意,此时 ROC 曲线并不完整,因为测试集中负例的数量是正例的两倍以上,导致坐标轴倾斜。一个简单的解决方案是将图表按比例缩放,使得两个坐标轴的大小相等,如图图 10.7所示:

图表,散点图描述自动生成

图 10.7:调整图表的坐标轴比例,可以创建一个无论初始正负例平衡如何都成比例的比较

如果我们想象现在 x 轴和 y 轴的范围都是从 0 到 1,我们可以将每个轴解释为百分比。y 轴表示正例的数量,最初的范围是从 0 到 6;将其缩小到 0 到 1 的比例后,每个增量变为 1/6。在这个比例上,我们可以将 ROC 曲线的垂直坐标视为真阳性数除以总正例数,即真阳性率,或灵敏度。同样,x 轴衡量的是负例的数量;通过除以总负例数(本例中为 14),我们得到真阴性率,或特异性。

图 10.8 中的表格描述了假设测试集中所有 20 个示例的计算:

表格描述自动生成

图 10.8:ROC 曲线追踪模型真阳性率与假阳性率随示例集规模逐渐增大而发生的变化

ROC 曲线的一个重要特性是它们不受类别不平衡问题的影响,其中一个结果(通常是正类)比另一个结果要罕见得多。许多性能指标,如准确率,对于不平衡数据可能会产生误导。ROC 曲线并非如此,因为图表的两个维度完全基于正负值内的比率,因此正负之间的比率不会影响结果。由于许多最重要的机器学习任务都涉及严重不平衡的结果,ROC 曲线是理解模型整体质量的一个非常有用的工具。

比较 ROC 曲线

如果 ROC 曲线有助于评估单个模型,那么它们也用于跨模型比较也就不足为奇了。直观上,我们知道靠近图表区域右上角的曲线更好。在实践中,这种比较往往比这更具有挑战性,因为曲线之间的差异通常是微妙的而不是明显的,而且解释是细微的、具体的,并且与模型的使用方式有关。

要理解细微差别,让我们首先考虑是什么原因导致两个模型在 ROC 图上绘制出不同的曲线。从原点开始,曲线长度随着预测为正的测试集示例数量的增加而延长。因为 y 轴代表真阳性率,而 x 轴代表假阳性率,更陡峭的上升轨迹是一个隐含的比率,意味着模型在识别正例时犯的错误更少。这如图 10.9 所示,它描绘了两个虚构模型的 ROC 曲线的起点。对于相同数量的预测——由从原点发出的向量的长度相等表示——第一个模型具有更高的真阳性率和更低的假阳性率,这意味着它是两个模型中表现更好的一个:

图 10.9:对于相同数量的预测,模型 1 优于模型 2,因为它具有更高的真正阳性率

假设我们继续追踪这两个模型的 ROC 曲线,评估模型在整个数据集上的预测。在这种情况下,也许第一个模型在曲线的所有点上继续优于第二个模型,如图 10.10所示。

在曲线的所有点上,第一个模型具有更高的真正阳性率和更低的假阳性率,这意味着它在整个数据集上是更好的表现者:

图表描述自动生成

图 10.10:模型 1 在所有曲线点上始终优于模型 2,具有更高的真正阳性和更低的假阳性率

尽管在先前的例子中第二个模型明显劣于第一个模型,但选择更好的表现者并不总是那么容易。图 10.11展示了相交的 ROC 曲线,这表明没有哪个模型是所有应用的最好表现者:

图表描述自动生成

图 10.11:对于数据的不同子集,模型 1 和模型 2 都是更好的表现者

两个 ROC 曲线的交点将图表分为两个区域:一个区域中第一个模型具有更高的真正阳性率,另一个区域中则相反。那么,我们如何知道哪个模型对于任何特定的用例是“最佳”的呢?

为了回答这个问题,当比较两条曲线时,了解两个模型都在尝试按照每个示例属于正类概率从高到低的顺序对数据集进行排序是有帮助的。那些能够更好地以这种方式排序数据集的模型将具有更靠近图表左上角的 ROC 曲线。

图 10.11中的第一个模型之所以能迅速领先,是因为它能够将更多的正例排序到数据集的前端,但在此初始激增之后,第二个模型能够逐渐赶上,并在数据集剩余部分中缓慢而稳定地将正例排序在负例之前,从而超越了其他模型。尽管第二个模型可能在整个数据集上具有更好的整体性能,但我们更倾向于选择早期表现更好的模型——那些在数据集中“低垂的果实”上表现更好的模型。选择这些模型的理由是,许多现实世界的模型仅用于对数据子集采取行动。

例如,考虑一个用于识别最有可能对直接邮件广告活动做出反应的客户的模型。如果我们能够向所有潜在客户发送邮件,那么模型就是不必要的。但由于我们没有足够的预算向每个地址发送广告,因此模型被用来估计收件人在查看广告后购买产品的概率。一个能够更好地将真正最有可能购买的产品放在列表前面的模型将会有一个更陡峭的 ROC 曲线早期斜率,并将缩小获取购买者所需的营销预算。在图 10.11中,第一个模型更适合这项任务。

与这种方法相反,另一个考虑因素是各种类型错误的相对成本;在现实世界中,假阳性和假阴性通常有不同的影响。如果我们知道垃圾邮件过滤器或癌症筛查需要针对特定的真正阳性率,例如 90%或 99%,我们将倾向于选择在期望水平上具有较低假阳性率的模型。尽管由于高假阳性率,这两个模型都不会很好,但图 10.11表明,第二个模型对于这些应用来说稍微更可取。

如这些示例所示,ROC 曲线允许比较模型性能,同时也考虑了模型的使用方式。这种灵活性比简单的数值指标如准确度或 kappa 更受欢迎,但可能也希望通过一个单一的指标来量化 ROC 曲线,以便可以进行定量比较,就像这些统计数据一样。下一节将介绍这种类型的度量。

ROC 曲线下的面积

比较 ROC 曲线可能具有一定的主观性和情境特异性,因此将性能简化为单一数值的指标总是有需求的,以便简化并使比较具有客观性。虽然可能难以说清楚什么是一个“好的”ROC 曲线,但一般来说,我们知道 ROC 曲线越接近图表的右上角,它在识别正值方面的能力就越好。这可以通过一个称为ROC 曲线下面积AUC)的统计量来衡量。AUC 将 ROC 图视为一个二维正方形,并测量 ROC 曲线下的总面积。AUC 的范围从 0.5(对于没有预测价值的分类器)到 1.0(对于完美的分类器)。解释 AUC 分数的惯例使用了一个类似于学术成绩等级的系统:

  • A:杰出 = 0.9 到 1.0

  • B:优秀/良好 = 0.8 到 0.9

  • C:可接受/公平 = 0.7 到 0.8

  • D:差 = 0.6 到 0.7

  • E:无区分度 = 0.5 到 0.6

与大多数此类量表一样,某些任务可能比其他任务更适合这些级别;类别之间的边界自然是有些模糊的。

ROC 曲线低于对角线的情况虽然罕见但可能发生,这会导致 AUC 小于 0.50。这意味着分类器的性能不如随机。通常,这是由于编码错误造成的,因为一个始终做出错误预测的模型显然已经从数据中学习到了一些有用的信息——它只是错误地应用了预测。要解决这个问题,请确认正例的编码是否正确,或者简单地反转预测,使得当模型预测负类时,选择正类代替。

当 AUC 的使用开始变得普遍时,有些人将其视为模型性能的最终衡量标准,尽管不幸的是,在所有情况下这并不成立。一般来说,更高的 AUC 值反映了分类器在将随机正例排序高于随机负例方面表现更好。然而,图 10.12说明了重要的事实:两条 ROC 曲线可能形状非常不同,但 AUC 却相同:

图 10.12:尽管 AUC 相同,ROC 曲线可能具有不同的性能

由于 AUC 是 ROC 曲线的简化,仅凭 AUC 本身不足以识别适用于所有用例的“最佳”模型。最安全的做法是将 AUC 与对 ROC 曲线的定性检查结合起来,正如本章前面所述。如果两个模型的 AUC 相同或相似,通常更倾向于选择早期表现更好的模型。此外,即使一个模型的整体 AUC 更好,对于仅将使用最自信预测子集的应用,具有更高初始真正阳性率的模型可能更受欢迎。

在 R 中创建 ROC 曲线和计算 AUC

pROC包提供了一套易于使用的函数,用于创建 ROC 曲线和计算 AUC。pROC网站([web.expasy.org/pROC/](web.expasy.org/pROC/))列出了完…

关于pROC包的更多信息,请参阅pROC:用于 R 和 S+的开源包,用于分析和比较 ROC 曲线,Robin, X, Turck, N, Hainard, A, Tiberti, N, Lisacek, F, Sanchez, JC, 和 Mueller M, BMC Bioinformatics, 2011, 第 12-77 页

要使用pROC创建可视化,需要两个数据向量。第一个必须包含正类估计概率,第二个必须包含预测类别值。

对于 SMS 分类器,我们将按照以下方式将估计的垃圾邮件概率和实际类别标签提供给roc()函数:

> library(pROC)
> sms_roc <- roc(sms_results$prob_spam, sms_results$actual_type) 

使用sms_roc对象,我们可以通过 R 的plot()函数来可视化 ROC 曲线。如下所示,许多用于调整图形的标准参数都可以使用,例如main(用于添加标题)、col(用于更改线条颜色)和lwd(用于调整线条宽度)。grid参数在图形上添加了浅色的网格线,有助于提高可读性,而legacy.axes参数指示pROCx轴标记为 1 – 特异性,这是一个流行的约定,因为它等同于假阳性率:

> plot(sms_roc, main = "ROC curve for SMS spam filter",
         Col = "blue", lwd = 2, grid = TRUE, legacy.axes = TRUE) 

结果是一个 Naive Bayes 分类器的 ROC 曲线和一个表示无预测价值的基线分类器的对角参考线:

图表,折线图  自动生成的描述

图 10.13:Naive Bayes SMS 分类器的 ROC 曲线

定性来看,我们可以看到这条 ROC 曲线似乎占据了图表的右上角空间,这表明它比代表无用分类器的虚线更接近完美分类器。

为了将此模型的性能与其他在同一数据集上预测的其他模型进行比较,我们可以在同一图表上添加额外的 ROC 曲线。假设我们已经在 SMS 数据上使用第三章中描述的knn()函数训练了一个 k-NN 模型。使用此模型,我们计算了测试集中每个记录的垃圾邮件预测概率,并将其保存到 CSV 文件中,我们可以在这里加载它。加载文件后,我们将像之前一样应用roc()函数来计算 ROC 曲线,然后使用plot()函数并设置参数add = TRUE将曲线添加到之前的图表中:

> sms_results_knn <- read.csv("sms_results_knn.csv")
> sms_roc_knn <- roc(sms_results$actual_type,
                       sms_results_knn$p_spam)
> plot(sms_roc_knn, col = "red", lwd = 2, add = TRUE) 

结果可视化中还有一个第二曲线,描述了 k-NN 模型在相同的测试集上对 Naive Bayes 模型进行预测的性能。k-NN 的曲线始终较低,表明它比 Naive Bayes 方法是一个持续较差的模型:

图表,折线图  自动生成的描述

图 10.14:比较 Naive Bayes(最上面的曲线)和 k-NN(底部曲线)在 SMS 测试集上的性能的 ROC 曲线

为了定量地确认这一点,我们可以使用pROC包来计算 AUC。为此,我们只需将包的auc()函数应用于每个模型的sms_roc对象,如下所示:

> auc(sms_roc) 
Area under the curve: 0.9836 
> auc(sms_roc_knn) 
Area under the curve: 0.8942 

Naive Bayes SMS 分类器的 AUC 为 0.98,这非常高,并且比 k-NN 分类器的 AUC 0.89 要好得多。但我们是怎样知道模型在另一个数据集上表现同样好的可能性,或者这种差异是否大于仅由偶然性预期的?为了回答这些问题,我们需要更好地理解我们可以将模型的预测外推多远超出测试数据。这些方法将在接下来的章节中描述。

这一点之前已经提到过,但值得再次强调:仅凭 AUC 值往往不足以确定一个“最佳”模型。在这个例子中,AUC 值确实能够识别出更好的模型,因为 ROC 曲线没有交叉——朴素贝叶斯模型在 ROC 曲线的所有点上都具有更好的真正阳性率。当 ROC 曲线确实交叉时,“最佳”模型将取决于模型的使用方式。此外,还可以使用第十四章中介绍的构建更好的学习器技术,将具有交叉 ROC 曲线的学习者组合成更强大的模型。

估计未来性能

一些 R 机器学习包在模型构建过程中会展示混淆矩阵和性能指标。这些统计数据的目的是为了提供对模型重新替换误差的洞察,这种误差发生在尽管模型是在这些数据上训练的,但训练样本的目标值被错误预测的情况下。这可以用作粗略的诊断工具,以识别明显表现不佳的模型。一个在训练数据上表现不佳的模型不太可能在未来的数据上表现良好。

反过来则不成立。换句话说,一个在训练数据上表现良好的模型不能假设它在未来的数据集上也会表现良好。例如,一个使用死记硬背来完美分类每个训练实例且零重新替换误差的模型将无法将其预测推广到它以前从未见过的数据。因此,训练数据上的错误率可以假设是对模型未来性能的乐观估计。

与依赖于重新替换误差相比,更好的做法是评估模型在尚未见过的数据上的性能。我们在前面的章节中已经使用过这种方法,当时我们将可用的数据分成训练集和测试集。然而,在某些情况下,创建训练集和测试集并不总是理想的。例如,在你只有一小部分数据的情况下,你可能不想进一步减少样本量。

幸运的是,你很快就会了解到,还有其他方法可以估计模型在未见数据上的性能。我们用来计算性能指标的caret包也提供了估计未来性能的函数。如果你正在跟随 R 代码示例,并且尚未安装caret包,请先安装。你还需要使用library(caret)命令将包加载到 R 会话中。

保留法

我们在前面章节中使用的数据划分为训练集和测试集的过程被称为保留法。如图 10.15 所示,训练集用于生成模型,然后该模型应用于测试集以生成用于评估的预测。通常,大约三分之一的用于测试,三分之二用于训练,但这个比例可能会根据可用数据的数量或学习任务的复杂性而变化。为了确保训练集和测试集没有系统性差异,它们的示例被随机分为两组。

图示  自动生成的描述

图 10.15:最简单的保留法将数据分为训练集和测试集

为了使保留法得到对未来性能的真正准确估计,在任何时候都不应允许测试集上的性能影响建模过程。正如斯坦福大学教授、著名机器学习专家 Trevor Hastie 所说:“理想情况下,测试集应该被保存在一个‘保险库’中,只有在数据分析结束时才取出。”换句话说,测试数据除了其唯一目的之外,不应被触及,即评估一个单一、最终的模型。

更多信息,请参阅*《统计学习元素》(第 2 版),Hastie,Tibshirani 和 Friedman(2009),第 222 页*。

很容易在不经意间违反这个规则,在选择多个模型之一或根据重复测试的结果更改单个模型时窥视这个比喻性的“保险库”。例如,假设我们在训练数据上构建了几个模型,并在测试数据上选择了准确率最高的模型。在这种情况下,因为我们已经使用了测试集来挑选最佳结果,所以测试性能并不是对未来未见数据性能的无偏度量

留心观察的读者会发现,在前几章中使用了保留测试数据来评估模型并提高模型性能。这样做是为了说明目的,但实际上违反了之前陈述的规则。因此,所显示的模型性能统计数据并不是对未来未见数据的真正无偏估计。

为了避免这个问题,最好将原始数据划分为除了训练集和测试集之外,还有一个验证集。验证集可以用于迭代和细化选定的模型或模型,而将测试集仅用于最终步骤,以报告对未来预测的估计错误率。典型的划分比例是训练集 50%,测试集 25%,验证集 25%。

图示  自动生成的描述

图 10.16:验证集可以从训练集中保留出来,以选择多个候选模型

使用随机数生成器将记录分配到分区是一种创建保留样本的简单方法。这种技术首次在第五章分而治之 – 使用决策树和规则进行分类中使用,用于创建训练和测试数据集。

如果你想要跟随以下示例,请从 Packt Publishing 的网站上下载credit.csv数据集,并使用credit <- read.csv("credit.csv", stringsAsFactors = TRUE)命令将其加载到数据框中。

假设我们有一个名为credit的数据框,包含 1,000 行数据。我们可以将其划分为三个分区,如下所示。首先,我们使用runif()函数创建一个从 1 到 1,000 的随机排序行 ID 向量,该函数默认在 0 和 1 之间生成指定数量的随机值。runif()函数的名字来源于随机均匀分布,这在第二章管理和理解数据中讨论过。

然后,order()函数返回一个指示 1,000 个随机数排名顺序的向量。例如,order(c(0.5, 0.25, 0.75, 0.1))返回序列4 2 1 3,因为最小的数字(0.1)出现在第四位,第二小的(0.25)出现在第二位,以此类推:

> random_ids <- order(runif(1000)) 

接下来,使用随机 ID 将信用数据框划分为包含训练、验证和测试数据集的 500、250 和 250 条记录:

> credit_train <- credit[random_ids[1:500], ]
> credit_validate <- credit[random_ids[501:750], ]
> credit_test <- credit[random_ids[751:1000], ] 

保留样本的一个问题是,每个分区可能包含某些类别的较大或较小的比例。在某个(或多个)类别在数据集中占非常小比例的情况下,这可能导致该类别被排除在训练数据集之外——这是一个重大问题,因为模型无法学习这个类别。

为了减少这种情况发生的可能性,可以使用一种称为分层随机抽样的技术。尽管随机样本通常应该包含与完整数据集大致相同的每个类别值的比例,但分层随机抽样保证随机分区几乎与完整数据集具有相同的每个类别的比例,即使某些类别很小。

caret包提供了一个createDataPartition()函数,它根据分层保留样本创建分区。以下命令显示了为credit数据集创建训练和测试数据集分层样本的步骤。要使用此函数,必须指定一个类别值向量(在这里,default表示一笔贷款是否违约),以及一个参数p,它指定要包含在分区中的实例比例。list = FALSE参数防止结果被存储为列表对象——这是更复杂采样技术所需的,但在这里是不必要的:

> in_train <- createDataPartition(credit$default, p = 0.75, list = FALSE)
> credit_train <- credit[in_train, ]
> credit_test <- credit[-in_train, ] 

in_train向量指示包含在训练样本中的行号。我们可以使用这些行号来选择credit_train数据框中的示例。同样,通过使用负号,我们可以使用in_train向量中未找到的行号来为credit_test数据集。

虽然分层抽样将类别均匀分布,但它并不能保证其他类型的代表性。一些样本可能包含过多或过少的困难案例、易于预测的案例或异常值。这对于较小的数据集尤其如此,因为可能没有足够多的此类案例来分配到训练集和测试集中。

除了可能存在偏差的样本外,保留法还存在另一个问题,即必须保留大量数据用于测试和验证模型。由于在测量其性能之前,这些数据不能用于训练模型,因此性能估计可能过于保守。

由于在较大数据集上训练的模型通常表现更好,一个常见的做法是在选择并评估最终模型后,在全部数据集(即训练、测试和验证)上重新训练模型。

一种称为重复保留法的技术有时被用来减轻随机组成训练数据集的问题。重复保留法是保留法的一个特例,它使用几个随机保留样本的平均结果来评估模型性能。由于使用了多个保留样本,因此模型训练或测试在非代表性数据上的可能性较小。我们将在下一节中进一步阐述这一想法。

交叉验证

重复保留法是称为k 折交叉验证k-fold CV)的技术基础,它已成为估计模型性能的行业标准。k 折交叉验证不是采取重复的随机样本,这些样本可能会多次使用相同的记录,而是将数据随机分为k个独立的随机分区,称为

虽然k可以设置为任何数字,但到目前为止最常用的惯例是使用 10 折交叉验证。为什么是 10 折?原因是经验证据表明,使用更多折数的好处很小。对于每个 10 折(每个包含总数据的 10%),在剩余的 90%数据上构建一个机器学习模型。然后使用该折的 10%样本进行模型评估。经过 10 次训练和评估模型的过程(使用 10 种不同的训练/测试组合)后,报告所有折的平均性能。

k 折交叉验证的一个极端情况是留一法,它使用数据的一个示例作为每个折进行 k 折交叉验证。这确保了用于训练模型的数据量最大。尽管这可能看起来很有用,但由于计算成本极高,因此在实践中很少使用。

可以使用caret包中的createFolds()函数创建 CV 数据集。类似于分层随机留出采样,此函数将尝试在每个折叠中保持与原始数据集相同的类别平衡。以下命令用于创建 10 个折叠,使用set.seed(123)确保结果可重复:

> set.seed(123)
> folds <- createFolds(credit$default, k = 10) 

createFolds()函数的结果是一个包含每个请求的k = 10个折叠的行号的向量列表。我们可以使用str()来查看其内容:

> str(folds) 
List of 10
 $ Fold01: int [1:100] 14 23 32 42 51 56 65 66 77 95 ...
 $ Fold02: int [1:100] 21 36 52 55 96 115 123 129 162 169 ...
 $ Fold03: int [1:100] 3 22 30 34 37 39 43 58 70 85 ...
 $ Fold04: int [1:100] 12 15 17 18 19 31 40 45 47 57 ...
 $ Fold05: int [1:100] 1 5 7 20 26 35 46 54 106 109 ...
 $ Fold06: int [1:100] 6 27 29 48 68 69 72 73 74 75 ...
 $ Fold07: int [1:100] 10 38 49 60 61 63 88 94 104 108 ...
 $ Fold08: int [1:100] 8 11 24 53 71 76 89 90 91 101 ...
 $ Fold09: int [1:100] 2 4 9 13 16 25 28 44 62 64 ...
 $ Fold10: int [1:100] 33 41 50 67 81 82 100 105 107 118 ... 

在这里,我们看到第一个折叠被命名为Fold01,并存储了 100 个整数,表示第一个折叠中credit数据框的 100 行。为了创建用于构建和评估模型的训练和测试数据集,需要额外的步骤。以下命令显示了如何为第一个折叠创建数据。我们将选定的 10%分配给测试数据集,并使用负号将剩余的 90%分配给训练数据集:

> credit01_test <- credit[folds$Fold01, ]
> credit01_train <- credit[-folds$Fold01, ] 

要执行完整的 10 折交叉验证,这个步骤需要重复 10 次,每次都构建一个模型并计算模型性能。最后,将性能度量平均以获得整体性能。幸运的是,我们可以通过应用我们之前学到的几种技术来自动化这项任务。

为了演示这个过程,我们将使用 10 折交叉验证来估计信用数据 C5.0 决策树模型的 kappa 统计量。首先,我们需要加载一些 R 包:caret(用于创建折叠)、C50(用于构建决策树)和irr(用于计算 kappa)。后两个包是为了演示目的选择的;如果你愿意,你可以使用不同的模型或不同的性能度量,但步骤序列保持不变:

> library(caret)
> library(C50)
> library(irr) 

接下来,我们将创建一个包含 10 个折叠的列表,就像之前做的那样。同样,这里使用set.seed()函数是为了确保如果再次运行相同的代码,结果是一致的:

> set.seed(123)
> folds <- createFolds(credit$default, k = 10) 

最后,我们将使用lapply()函数对折叠列表应用一系列相同的步骤。如以下代码所示,因为没有现成的函数能完全满足我们的需求,我们必须定义自己的函数并将其传递给lapply()。我们的自定义函数将credit数据框分为训练数据和测试数据,使用训练数据上的C5.0()函数构建决策树,从测试数据生成一组预测,并使用kappa2()函数比较预测值和实际值:

> cv_results <- lapply(folds, function(x) {
    credit_train <- credit[-x, ]
    credit_test <- credit[x, ]
    credit_model <- C5.0(default ~ ., data = credit_train)
    credit_pred <- predict(credit_model, credit_test)
    credit_actual <- credit_test$default
    kappa <- kappa2(data.frame(credit_actual, credit_pred))$value
    return(kappa)
  }) 

最终得到的 kappa 统计量被编译成一个列表,存储在cv_results对象中,我们可以使用str()来检查它:

> str(cv_results) 
List of 10
 $ Fold01: num 0.381
 $ Fold02: num 0.525
 $ Fold03: num 0.247
 $ Fold04: num 0.316
 $ Fold05: num 0.387
 $ Fold06: num 0.368
 $ Fold07: num 0.122
 $ Fold08: num 0.141
 $ Fold09: num 0.0691
 $ Fold10: num 0.381 

10 折交叉验证过程只剩下一步:我们必须计算这 10 个值的平均值。虽然你可能会想输入mean(cv_results),因为cv_results不是一个数值向量,所以结果会出错。相反,使用unlist()函数,它可以消除列表结构并将cv_results简化为一个数值向量。从那里,我们可以计算出预期的平均 kappa 值:

> mean(unlist(cv_results)) 
[1] 0.2939567 

这个 kappa 统计量相对较低,对应于解释尺度上的“公平”,这表明信用评分模型仅略优于随机机会。在第十四章,“构建更好的学习者”中,我们将检查基于 10 折交叉验证的自动化方法,这些方法可以帮助我们提高该模型的表现。

由于 CV 从多个测试集中提供性能估计,我们还可以计算估计的变异性。例如,10 次迭代的方差可以计算如下:

> sd(unlist(cv_results)) 
[1] 0.1448565 

在找到性能指标的平均值和标准差后,可以计算置信区间或确定两个模型在性能上是否有统计显著的差异,这意味着差异很可能是真实的,而不是由于随机变化。

不幸的是,最近的研究表明 CV 违反了此类统计测试的假设,尤其是数据需要来自独立随机样本的需要,而 CV 的折叠由于定义上的原因相互关联,这显然是不成立的。

关于从 10 折交叉验证中获得的性能估计局限性的讨论,请参阅*Bates S, Hastie T, and Tibshirani R, 2022, arxiv.org/abs/2104.00…

CV 的更复杂变体已被开发出来,以提高模型性能估计的鲁棒性。其中一种技术是重复 k 折交叉验证,它涉及反复应用 k 折交叉验证并平均结果。一种常见的策略是进行 10 折交叉验证 10 次。尽管计算量较大,但这种方法提供的性能估计比标准的 10 折交叉验证更加鲁棒,因为性能是在许多更多次试验中平均得出的。然而,它也违反了统计假设,因此对结果进行的统计测试可能略有偏差。

目前估计模型性能的黄金标准可能是嵌套交叉验证,它实际上是在另一个 k 折交叉验证过程中执行 k 折交叉验证。这项技术在本章 11 的《用机器学习取得成功》中有所描述,它不仅计算成本极高,而且实施和解释起来也更具挑战性。嵌套 k 折交叉验证的优点是,它产生了与标准 k 折交叉验证相比真正有效的模型性能比较,因为标准 k 折交叉验证由于违反了统计假设而存在偏差。另一方面,这个问题引起的偏差对于非常大的数据集似乎不太重要,因此使用从更简单的 CV 方法中得出的置信区间或显著性测试来帮助识别“最佳”模型仍然是合理且常见的做法。

自举采样

相比于 k 折交叉验证(k-fold CV)来说,一个稍微不那么流行但非常重要的替代方法是称为自举采样自举或简称为bootstrapping。一般来说,这些指的是使用数据的随机样本来估计更大集属性的统计方法。当这个原理应用于机器学习模型性能时,它意味着创建几个随机选择的训练和测试数据集,然后使用这些数据集来估计性能统计量。然后,从各种随机数据集中得出的结果被平均,以获得对未来性能的最终估计。

那么,是什么使得这个程序与 k 折交叉验证(k-fold CV)不同呢?CV 将数据分成单独的分区,其中每个示例只能出现一次,而自举允许通过有放回抽样的过程多次选择示例。这意味着从原始的 n 个示例数据集中,自举过程将创建一个或多个新的训练数据集,这些数据集也包含 n 个示例,其中一些是重复的。

然后从未被选为相应训练数据集的示例集中构建相应的测试数据集。

在自举数据集中,任何给定实例被排除在训练数据集之外的几率是 36.8%。我们可以通过认识到每个示例在每次向训练数据集添加 n 行时都有 1/n 的机会被采样来数学上证明这一点。因此,要进入测试集,一个示例必须没有被选择 n 次。由于被选择的机会是 1/n,因此未被选择的机会是 1 - 1/n,未被选择 n 次的概率如下:

图片

使用这个公式,如果自举的数据集包含 1,000 行,随机记录未被选中的概率是:

> (1 - (1/1000))¹⁰⁰⁰ 
[1] 0.3676954 

类似地,对于一个有 100,000 行的数据集:

> (1 - (1/100000))¹⁰⁰⁰⁰⁰ 
[1] 0.3678776 

n 趋近于无穷大时,公式简化为 1/e,如下所示:

> 1 / exp(1) 
[1] 0.3678794 

由于未被选中的概率为 36.8%,任何实例被选入训练数据集的概率为 100% - 36.8% = 63.2%。换句话说,训练数据仅代表可用示例的 63.2%,其中一些是重复的。与使用 90%示例进行训练的 10 折交叉验证相比,自举样本对整个数据集的代表性较低。

由于仅用 63.2%的训练数据进行训练的模型可能比在更大的训练集上训练的模型表现更差,因此自举的性能估计可能比模型稍后训练在完整数据集上获得的估计要低得多。

一种称为0.632 自举的特殊自举情况,通过将最终性能指标视为训练数据(过于乐观)和测试数据(过于悲观)性能的函数来解决这个问题。然后,最终错误率估计如下:

图片

与交叉验证相比,自举采样的一项优势是它通常在非常小的数据集上表现更好。此外,自举采样在性能测量之外还有应用。特别是,在第十四章 构建更好的学习者中,你将了解如何使用自举采样的原则来提高模型性能。

摘要

本章介绍了评估机器学习分类模型性能的几种最常见指标和技术。尽管准确率提供了一种简单的方法来检查模型正确性的频率,但在罕见事件的情况下,这可能会产生误导,因为这类事件在现实生活中的重要性可能与它们在数据中出现的频率成反比。

一些基于混淆矩阵的指标更好地捕捉了模型性能以及各种类型错误成本的平衡。Kappa 统计量和 Matthews 相关系数是两种更复杂的性能指标,即使在严重不平衡的数据集上也能很好地工作。此外,仔细检查敏感性和特异性,或精确率和召回率之间的权衡,可以成为思考现实世界中错误影响的有用工具。ROC 曲线等可视化也有助于此目的。

值得注意的是,有时衡量模型性能的最佳方法就是考虑它如何满足,或未能满足,其他目标。例如,你可能需要用简单语言解释模型的逻辑,这将排除一些模型。此外,即使模型表现非常好,但如果模型运行速度过慢或难以扩展到生产环境,那么它将完全无用。

展望接下来的章节,对性能进行测量的明显扩展是找到提高性能的方法。随着你继续阅读本书,你将应用本章中的许多原则,同时加强你的机器学习能力并增加更多高级技能。在接下来的页面中,CV 技术、ROC 曲线、自助法和 caret 包将定期出现,因为我们将在已有的工作基础上,通过系统地迭代、精炼和组合学习算法来研究如何制作更智能的模型。

加入我们书籍的 Discord 空间

加入我们的 Discord 社区,与志同道合的人相聚,并和超过 4000 人在以下地点一起学习:

packt.link/r