Python-无监督学习实用手册-全-

Python 无监督学习实用手册(全)

原文:Hands-on unsupervised learning with Python

协议:CC BY-NC-SA 4.0

零、前言

无监督学习是数据科学中一个越来越重要的分支,其目标是训练能够学习数据集结构的模型,并为用户提供关于新样本的有用信息。在许多不同的业务部门(如市场营销、商业智能、战略等),无监督学习在帮助经理做出最佳决策方面一直发挥着主要作用,无论是基于定性方法,还是最重要的是基于定量方法。在一个数据越来越普及、存储成本不断下降的世界里,分析真实、复杂数据集的可能性有助于将老式的商业模式转变为新的、更准确、更灵敏、更有效的商业模式。这就是为什么数据科学家可能对所有的可能性都没有一个清晰的概念,专注于所有方法的优缺点,并增加他们对每个特定领域的最佳潜在策略的知识。这本书并不打算成为一个详尽的资源(这实际上是不可能找到的),而是更多的是一个参考,让你开始探索这个世界,为你提供不同的方法,可以立即使用和评估。我希望不同背景的读者能学到有价值的东西来改善他们的业务,并希望你能寻求对这个迷人话题的更多研究!

这本书是给谁的

这本书的目标读者是数据科学家(有抱负的和专业的)、机器学习实践者和开发人员,他们希望学习如何实现最常见的无监督算法并调整其参数,以便为所有业务领域的不同利益相关者提供有价值的见解。

这本书涵盖了什么

第 1 章无监督学习入门,从非常实用的角度介绍了机器学习和数据科学。讨论了主要概念,并给出了几个简单的例子,特别关注无监督的问题结构。

第二章聚类基础,开始我们对聚类算法的探索。分析了最常见的方法和评估指标,并给出了具体的例子,说明如何从不同的角度调整超参数和评估性能。

第三章高级聚类讨论了一些比较复杂的算法。在第 2 章聚类基础中分析的许多问题,使用更强大和灵活的方法进行重新评估,只要基本算法的性能不符合要求,就可以轻松使用这些方法。

第 4 章行动中的层次聚类,完全致力于一系列能够根据特定标准计算完整聚类层次的算法。分析了最常见的策略,以及可以提高方法有效性的具体性能度量和算法变体。

第 5 章软聚类和高斯混合模型集中于几个著名的软聚类算法,特别强调高斯混合,这允许在相当合理的假设下定义生成模型。

第 6 章异常检测讨论了无监督学习的一个特殊应用:新颖性和离群点检测。目标是分析一些可以有效使用的常见方法,以了解新样本是有效的,还是需要特别注意的异常样本。

第 7 章降维和成分分析、涵盖了降维、成分分析和字典学习最常见和最强大的方法。这些例子展示了如何在不同的特定场景中有效地执行这样的操作。

第八章无监督神经网络模型,讨论了一些非常重要的无监督神经模型。特别地,焦点指向可以学习通用数据生成过程的结构的网络和执行降维。

第 9 章生成对抗网络和 SOMs 继续分析一些深度神经网络,它们可以学习数据生成过程的结构,并输出从这些过程中提取的新样本。此外,还讨论了一种特殊的网络,并给出了一些实例。

充分利用这本书

这本书需要机器学习和 Python 编码的基础知识。此外,需要大学水平的概率论、微积分和线性代数知识,以便全面理解所有理论讨论。然而,不熟悉这些概念的读者可以跳过数学讨论,只关注实际方面。无论何时需要,都提供具体论文和书籍的参考,以便更深入地理解最复杂的概念。

下载示例代码文件

你可以从你在www.packt.com的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packt.com/support并注册将文件直接通过电子邮件发送给您。

您可以按照以下步骤下载代码文件:

  1. 登录或注册www.packt.com
  2. 选择“支持”选项卡。
  3. 点击代码下载和勘误表。
  4. 在搜索框中输入图书的名称,并按照屏幕指示进行操作。

下载文件后,请确保使用最新版本的解压缩文件夹:

  • 视窗系统的 WinRAR/7-Zip
  • zipeg/izp/un ARX for MAC
  • 适用于 Linux 的 7-Zip/PeaZip

这本书的代码包也在 GitHub 上托管在https://GitHub . com/packt publishing/HandsOn-Unsupervised-Learning-with-Python。如果代码有更新,它将在现有的 GitHub 存储库中更新。

我们还有来自丰富的图书和视频目录的其他代码包,可在【https://github.com/PacktPublishing/】获得。看看他们!

下载彩色图像

我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。可以在这里下载:http://www . packtpub . com/sites/default/files/downloads/9781789348279 _ color images . pdf

使用的约定

本书通篇使用了许多文本约定。

CodeInText:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。下面是一个例子:“将下载的WebStorm-10*.dmg磁盘镜像文件作为另一个磁盘挂载到系统中。”

代码块设置如下:

X_train = faces['images']
X_train = (2.0 * X_train) - 1.0

width = X_train.shape[1]
height = X_train.shape[2]

当我们希望将您的注意力吸引到代码块的特定部分时,相关的行或项目以粗体显示:

import tensorflow as tf

session = tf.InteractiveSession(graph=graph)
tf.global_variables_initializer().run()

粗体:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。例如,菜单或对话框中的单词像这样出现在文本中。下面是一个示例:“从管理面板中选择系统信息。”

Warnings or important notes appear like this. Tips and tricks appear like this.

取得联系

我们随时欢迎读者的反馈。

一般反馈:如果你对这本书的任何方面有疑问,在你的信息主题中提到书名,发邮件给我们customercare@packtpub.com

勘误表:虽然我们已经尽了最大的努力来保证内容的准确性,但是错误还是会发生。如果你在这本书里发现了一个错误,如果你能向我们报告,我们将不胜感激。请访问www.packt.com/submit-errata,选择您的图书,点击勘误表提交链接,并输入详细信息。

盗版:如果您在互联网上遇到任何形式的我们作品的非法拷贝,如果您能提供我们的位置地址或网站名称,我们将不胜感激。请通过copyright@packt.com联系我们,并提供材料链接。

如果你有兴趣成为一名作者:如果有一个你有专长的话题,你有兴趣写或者投稿一本书,请访问authors.packtpub.com

复习

请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?然后,潜在的读者可以看到并使用您不带偏见的意见来做出购买决定,我们在 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们的书的反馈。谢谢大家!

更多关于 Packt 的信息,请访问packt.com

一、无监督学习入门

在本章中,我们将介绍基本的机器学习概念,假设您有一些统计学习和概率论的基本知识。您将了解到机器学习技术的使用以及逻辑过程,这提高了我们对数据集的性质和属性的了解。整个过程的目的是建立能够支持业务决策的描述性和预测性模型。

无监督学习旨在为数据探索、挖掘和生成提供工具。在这本书里,你将通过具体的例子和分析来探索不同的场景,你将学习如何应用基本的和更复杂的算法来解决特定的问题。

在这一介绍性章节中,我们将讨论:

  • 为什么我们需要机器学习?
  • 描述性、诊断性、预测性和规定性分析
  • 机器学习的类型
  • 我们为什么使用 Python?

技术要求

本章中的代码要求:

示例可在 GitHub 资源库中获得:https://GitHub . com/packktpublishing/HandsOn-Unsupervised-Learning-with-Python/tree/master/chapter 01

为什么我们需要机器学习?

数据无处不在。此时此刻,成千上万的系统正在收集构成特定服务历史的记录,以及日志、用户交互和许多其他依赖于上下文的元素。仅在十年前,大多数公司甚至无法有效管理 1%的数据。由于这个原因,数据库被定期删除,只有重要的数据被保留在永久存储服务器中。

相反,如今几乎每个公司都可以利用可扩展的云基础架构来应对不断增长的传入数据量。Apache Hadoop 或 Apache Spark 等工具允许数据科学家和工程师实施涉及海量数据的复杂管道。至此,所有的障碍都被拆除,民主化进程已经到位。然而,这些大型数据集的实际价值是什么?从业务角度来看,只有当信息能够帮助做出正确的决策、减少不确定性并提供更好的上下文洞察力时,信息才是有价值的。这意味着,如果没有正确的工具和知识,大量数据对公司来说只是一种成本,需要加以限制才能提高利润率。

机器学习是计算机科学(特别是人工智能)的一个大分支,旨在通过利用现有数据集来实现现实的描述性预测性模型。由于这本书致力于实用的无监督解决方案,我们将只关注通过寻找隐藏的原因和关系来描述上下文的算法。然而,即使只是从理论的角度来看,展示机器学习问题之间的主要差异也是有帮助的。只有对目标的完全认知(不仅限于技术层面)才能对最初的问题——为什么我们需要机器学习——给出合理的答案。

我们可以先说人类有非凡的认知能力,启发了很多系统,但当元素数量显著增加时,他们就缺乏分析能力。例如,如果你是一名老师,第一次和他/她的班级见面,你可以在浏览整个小组后,计算出女生比例的粗略估计。通常,即使估计是由两个或两个以上的个人进行的,估计也可能是准确的,并接近实际计数。然而,如果我们用聚集在一个院子里的一所学校的全部人口重复这个实验,性别的区别将不明显。这是因为所有的学生在课堂上都清晰可见;然而,在院子里区分性别受到某些因素的限制(例如,高个子可以隐藏矮个子)。去掉类比,我们可以说,大量的数据通常承载着大量的信息。为了提取和分类信息,有必要采取自动化的方法。

在进入下一部分之前,让我们讨论最初由 Gartner 定义的描述性、诊断性、预测性和规定性分析的概念。然而,在这种情况下,我们希望专注于我们正在分析的系统(例如,通用上下文),以便获得对其行为越来越多的控制。

下图显示了整个过程:

Descriptive, diagnostic, predictive, and prescriptive flow

描述性分析

在几乎所有数据科学场景中,首先要解决的问题是理解其本质。我们需要知道系统是如何工作的,或者数据集描述了什么。没有这种分析,我们的知识就太有限,无法做出任何假设或假设。例如,我们可以观察一个城市几年的平均温度图表。如果我们不能描述发现相关性、季节性和趋势的时间序列,任何其他问题仍然没有解决。在我们的特定环境中,如果我们没有发现对象组之间的相似性,我们就不能试图找到一种方法来总结它们的共同特征。数据科学家必须为每个特定的问题使用特定的工具,但是,在这个阶段结束时,所有可能的(和有用的)问题都必须得到回答。

此外,由于这个过程必须具有明确的业务价值,因此让不同的利益相关者参与进来以收集他们的知识并将其转化为一种通用语言是很重要的。例如,当处理医疗保健数据时,医生可能会谈论遗传因素,但出于我们的目的,最好说一些样本之间存在相关性,因此我们没有完全授权将它们视为统计上独立的元素。一般来说,描述性分析的结果是一个总结,包含了所有必要的度量评估和结论,以限定上下文,并减少不确定性。在温度图的例子中,数据科学家应该能够回答自相关、峰值的周期性、潜在异常值的数量以及趋势的存在。

诊断分析

到目前为止,我们一直在处理输出数据,这些数据是在特定的底层流程生成之后观察到的。描述了系统之后,自然的问题涉及到原因。温度取决于许多气象和地理因素,这些因素可以很容易观察到,也可以完全隐藏起来。时间序列中的季节性明显受到一年中的时间段的影响,但是异常值呢?

例如,我们在一个被称为冬季的地区发现了一个高峰。我们如何证明这一点?在一个简单的方法中,这可以被认为是一个噪声异常值,可以被过滤掉。但是,如果已经观察到,并且措施背后有一个基本事实(例如,各方都同意这不是错误),我们应该假设存在一个隐藏的(或潜在的)原因。

这可能令人惊讶,但大多数更复杂的场景都有大量难以分析的潜在原因(有时称为因素)。总的来说,这是一个不错的条件,但是,正如我们将要讨论的,将它们包含在模型中以通过数据集了解它们的影响是很重要的。

另一方面,决定放弃所有未知元素意味着降低模型的预测能力,同时准确性成比例下降。因此,诊断分析的首要目标不一定是找出所有原因,而是列出可观察和可测量的要素(称为因素),以及所有潜在的潜在要素(通常将其总结为单个全局要素)。

在某种程度上,诊断分析通常类似于逆向工程过程,因为我们可以轻松监控结果,但更难发现潜在原因和可观察结果之间的现有关系。因此,这种分析通常是概率性的,有助于发现某个确定的原因带来特定影响的概率。这样,也更容易排除非影响元素,并确定最初被排除的关系。然而,这个过程需要对统计学习方法有更深入的了解,除了几个例子,比如高斯混合,这本书不会讨论。

预测分析

一旦收集了总体描述性知识,并且对潜在原因的认识令人满意,就有可能创建预测模型。这些模型的目标是根据模型本身的历史和结构来推断未来的结果。在许多情况下,这个阶段与下一个阶段一起分析,因为我们很少对系统的自由演化感兴趣(例如,下个月温度将如何变化),而是对我们影响输出的方式感兴趣。

也就是说,让我们只关注预测,考虑应该考虑的最重要的因素。首先要考虑的是过程的性质。对于确定性过程,我们不需要机器学习,除非它们的复杂性如此之高,以至于我们不得不将它们视为黑盒。我们将要讨论的绝大多数例子都是关于随机过程的,其中不确定性是无法消除的。例如,我们知道一天中的温度可以被建模为依赖于先前观察的条件概率(例如,高斯)。因此,预测并不是要把系统变成一个确定性的系统,这是不可能的,而是要减少分布的方差,这样,只有在很短的温度范围内,概率才会很高。另一方面,由于我们知道许多潜在因素在幕后起作用,我们永远不能接受基于尖峰分布的模型(例如,概率为 1 的单一结果),因为这种选择会对最终精度产生可怕的负面影响。

如果我们的模型参数化了受学习过程影响的变量(例如高斯的均值和协方差矩阵),我们的目标是在所谓的偏差-方差权衡中找到最佳平衡。由于这一章是介绍性的,我们不是用数学公式来形式化概念,而是需要一个实用的定义(进一步的细节可以在博纳科尔索 g .**掌握机器学习算法,Packt,2018 中找到)。

定义统计预测模型的常用术语是估计量。因此,估计量的偏差是不正确假设和学习程序的可测量影响。换句话说,如果一个过程的平均值是 5.0,而我们的估计值的平均值是 3.0,我们可以说这个模型是有偏差的。考虑到前面的例子,如果观测值和预测值之间的误差的期望值不为零,我们就使用有偏估计器。重要的是要理解,我们并不是说每一个单独的估计都必须有零误差,但是在收集足够的样本并计算平均值的同时,它的值应该非常接近零(只有在无限个样本的情况下,它才能为零)。只要它大于零,就意味着我们的模型不能正确预测训练值。显而易见,我们正在寻找无偏估计量,平均而言,它能产生准确的预测。

另一方面,估计器的方差是在不属于训练集的样本存在的情况下鲁棒性的度量。在这一节的开始,我们说我们的过程通常是随机的。这意味着任何数据集都必须被视为来自特定的数据生成过程 p 数据T5。如果我们有足够多的代表性元素 x i ∈ X ,我们可以假设使用有限的数据集 X 训练一个分类器会得到一个能够对所有潜在样本进行分类的模型,这些样本可以从 p 数据 中提取。

例如,如果我们需要建模一个人脸分类器,它的上下文仅限于肖像(不允许进一步的人脸姿态),我们可以收集不同个人的大量肖像。我们唯一关心的是不排除现实生活中可能存在的类别。假设我们有 10,000 张不同年龄和性别的个人照片,但我们没有任何带帽子的肖像。当系统生产时,我们接到客户的电话,说系统对许多图片进行了错误分类。经过分析,我们发现它们总是代表戴帽子的人。很明显,我们的模型不对错误负责,因为它是用只代表数据生成过程的一个区域的样本训练的。因此,为了解决这个问题,我们收集其他样本并重复训练过程。然而,现在我们决定使用一个更复杂的模型,期望它会工作得更好。不幸的是,我们观察到更差的验证精度(例如,在训练阶段没有使用的子集上的精度),以及更高的训练精度。这里发生了什么?

当一个估计器学会了对训练集进行完美的分类,但是它对从未见过的样本的能力很差时,我们说它是过度训练,并且它的方差对于特定的任务来说太高(相反,欠训练模型有很大的偏差,所有的预测都非常不准确)。直觉上,模型对训练数据了解太多,已经失去了泛化能力。为了更好地理解这个概念,让我们看看高斯数据生成过程,如下图所示:

Original data generating process (solid line) and sampled data histogram

如果训练集没有以完全一致的方式进行采样,或者它是部分不平衡的(一些类比其他类具有更少的样本),或者如果模型易于过度拟合,则结果可以用不准确的分布来表示,如下所示:

Learned distribution

在这种情况下,模型被迫学习训练集的细节,直到它从分布中排除了许多潜在的样本。结果不再是高斯分布,而是双峰分布,其中一些概率错误地很低。当然,测试和验证集是从训练集没有覆盖的小区域中采样的(因为训练数据和验证数据之间没有重叠),因此模型在提供完全不正确的结果时会失败。

换句话说,我们可以说方差太高,因为模型已经学会处理太多的细节,在合理的阈值上增加了不同分类的可能性范围。例如,肖像分类器可以了解到,戴蓝色眼镜的人在 30-40 岁的年龄范围内总是男性(这是一种不现实的情况,因为细节水平通常很低,但是,这有助于理解问题的本质)。

我们可以总结说,一个好的预测模型必须具有非常低的偏差和成比例的低方差。不幸的是,通常不可能有效地最小化这两种措施,所以必须接受一种折衷。

一个具有良好泛化能力的系统可能会有更高的偏差,因为它无法捕捉所有的细节。相反,高方差允许非常小的偏差,但是模型的能力几乎局限于训练集。在这本书里,我们不打算谈论分类器,但是你应该完全理解这些概念,以便始终意识到你在项目工作中可能遇到的不同行为。

规定性分析

这样做的主要目的是回答我如何影响系统的输出?为了避免混淆,最好将这一概念翻译成纯机器学习语言,因此问题可能是,获得特定输出需要哪些输入值?

如前一节所述,这个阶段通常与预测分析合并在一起,因为模型通常用于这两项任务。然而,在某些特定情况下,预测仅限于零输入演化(例如在温度示例中),并且在规定阶段必须分析更复杂的模型。主要原因在于能够控制导致特定输出的所有原因。

有时,在没有必要的时候,它们只是被肤浅地分析。当原因不可控制时(例如,气象事件),或者当包含全局潜在参数集更简单时,都可能发生这种情况。后一种选择在机器学习中非常常见,并且已经开发了许多算法来有效地处理潜在因素的存在(例如,电磁或奇异值分解推荐系统)。出于这个原因,我们没有关注这个特定的方面(这在系统理论中是极其重要的),同时,我们隐含地假设我们的模型提供了研究由不同输入产生的许多可能输出的能力。

例如,在深度学习中,可以创建反向模型,生成输入空间的显著图,强制特定的输出类。考虑肖像分类器的例子,我们可能有兴趣发现哪些视觉元素影响类的输出。诊断分析通常是无效的,因为原因极其复杂,并且它们的水平太低(例如,轮廓的形状)。因此,逆模型可以通过显示不同几何区域的影响来帮助解决规定性问题。然而,完整的说明性分析超出了本书的范围,在许多情况下,这是不必要的,因此我们在接下来的章节中不考虑这样的步骤。现在我们来分析一下不同类型的机器学习算法。

机器学习算法的类型

在这一点上,我们可以简要介绍不同类型的机器学习,重点介绍它们的主要特点和差异。在接下来的章节中,我们将讨论非正式的定义,然后是更正式的定义。如果你不熟悉讨论中涉及的数学概念,可以跳过细节。然而,研究所有未知的理论元素是非常明智的,因为它们是理解下一章分析的概念的基础。

监督学习算法

在有监督的场景中,模型的任务是找到样本的正确标签,假设训练集的存在被正确标记,以及将估计值与正确值进行比较的可能性。术语监督的来源于外部教学代理的想法,该代理在每次预测后提供精确和即时的反馈。该模型可以使用这样的反馈作为误差的度量,并因此执行减少误差所需的校正。

更正式地说,如果我们假设一个数据生成过程,数据集获得如下:

如前所述,所有样本必须是从数据生成过程中均匀采样的独立且同分布的 ( IID )值。特别是所有的类都必须代表实际的分布(例如如果 p(y=0) = 0.4p(y=1) = 0.6 ,比例应该是 40%或者 60%)。然而,为了避免偏差,当类与类之间的差异不是很大时,一个合理的选择是完全均匀抽样,并且对于 y=1,2,...,M

通用分类器可以通过两种方式建模:

  • 输出预测类的参数化函数
  • 输出每个输入样本的类别概率的参数化概率分布

在第一种情况下,我们有:

考虑到整个数据集 X ,可以计算一个全局成本函数 L :

由于 L 仅依赖于参数向量(xIT5】和yIT9】是常数),所以一般算法必须找到最小化成本函数的最优参数向量。例如,在回归问题中(其中标签是连续的),误差度量可以是实际值和预测值之间的平方误差:**

这样的代价函数可以用不同的方式优化(特定算法特有的),但是一个非常常见的策略(尤其是在深度学习中)是使用 S 到 S(SGD)算法。它由以下两个步骤的迭代组成:

  • 用一小批样本 x i ∈ X 计算梯度 ∇L (相对于参数向量)
  • 更新权重并在梯度 -∇L 的相反方向上移动参数(记住梯度总是指向最大值)

相反,当分类器是概率性的时,它应该被表示为参数化的条件概率分布:

换句话说,分类器现在将输出给定输入向量的标签 y 的概率。现在的目标是找到最佳参数集,它将获得:

在前面的公式中,我们已经将 p 数据 表示为条件分布。可以使用概率距离度量来获得优化,例如库尔巴克-莱布勒散度 D KL (仅当两个分布相同时,该距离度量总是非负的DKL0DKL】T22 =0):

通过一些简单的操作,我们获得:

因此,得到的成本函数对应于 pp 数据 之间的交叉熵差,直到一个恒定值(数据生成过程的熵)。因此,现在的训练策略是基于使用单热编码来表示标签(例如,如果有两个标签 0 → (0,1)1 → (1,0) ,因此所有元素的总和必须始终等于 1 )并使用内在概率输出(例如在逻辑回归中)或 softmax 滤波器,这将 M 值转换为概率分布。

在这两种情况下,很明显隐藏教师的存在提供了一致的误差度量,允许模型相应地校正参数。特别是第二种方法对我们的目的非常有帮助,因此如果不太了解的话,我建议进一步研究(所有主要定义也可以在博纳科尔索 g .【机器学习算法】第二版,Packt,2018 中找到)。

我们现在可以讨论监督学习的一个非常基本的例子,一个可以用来预测简单时间序列演化的线性回归模型。

监督你好世界!

在这个例子中,我们想展示如何用二维数据进行简单的线性回归。特别是,让我们假设我们有一个包含 100 个样本的自定义数据集,如下所示:

import numpy as np
import pandas as pd

T = np.expand_dims(np.linspace(0.0, 10.0, num=100), axis=1)
X = (T * np.random.uniform(1.0, 1.5, size=(100, 1))) + np.random.normal(0.0, 3.5, size=(100, 1))
df = pd.DataFrame(np.concatenate([T, X], axis=1), columns=['t', 'x'])

We have also created a pandas DataFrame because it's easier to create plots using the seaborn library (https://seaborn.pydata.org). In the book, the code for the plots (using Matplotlib or seaborn) is normally omitted, but it's always present in the repository.

我们希望以合成的方式表示数据集,如下所示:

该任务可以使用线性回归算法来执行,如下所示:

from sklearn.linear_model import LinearRegression

lr = LinearRegression()
lr.fit(T, X)

print('x(t) = {0:.3f}t + {1:.3f}'.format(lr.coef_[0][0], lr.intercept_[0]))

最后一个命令的输出如下:

x(t) = 1.169t + 0.628

我们还可以通过绘制数据集和回归线来获得视觉确认,如下图所示:

Dataset and regression line

在这个例子中,回归算法最小化了平方误差成本函数,试图减少预测值和实际值之间的差异。由于对称分布,高斯(零均值)噪声的存在对斜率的影响最小。

无监督学习算法

在无监督的情况下,很容易想象,没有隐藏的老师,因此主要目标不能与最小化关于地面事实的预测误差相关。事实上,在这种情况下,相同的基本真理概念有着稍微不同的含义。事实上,当使用分类器时,我们希望训练样本有一个空错误(这意味着除了真正的类之外,其他类永远不会被认为是正确的)。

相反,在无监督的问题中,我们希望模型在没有任何正式指示的情况下学习一些信息。这个条件意味着,唯一可以学习的元素是包含在样本本身中的元素。因此,无监督算法通常旨在发现样本之间的相似性和模式,或者在给定一组向量的情况下再现输入分布。现在我们来分析一些最常见的无监督模型类别。

聚类分析

聚类分析(通常只称为聚类)是一个任务的例子,在这个任务中,我们想要找出大样本集中的共同特征。在这种情况下,我们总是假设存在数据生成过程,并且我们将数据集 X 定义为:

聚类算法基于隐含的假设,即样本可以根据它们的相似性进行分组。特别地,给定两个向量,相似性函数被定义为度量函数的倒数或倒数。例如,如果我们在欧几里得空间中工作,我们有:

在前面的公式中,引入了常数 ε 来避免被零除。很明显 d(a,c) < d(a,b)s(a,c) > s(a,b) 。因此,给定每个聚类的代表,我们可以考虑以下规则来创建分配的向量集:

换句话说,一个集群包含所有那些与代表的距离与所有其他代表相比最小的元素。这意味着,与所有代表相比,一个聚类包含的样本与代表的相似度最大。此外,在分配之后,样本获得权利以与同一聚类的其他成员共享其特征。

事实上,聚类分析最重要的应用之一是试图增加被认为相似的样本的同质性。例如,推荐引擎可以基于用户向量的聚类(包含关于他们的兴趣和购买的产品的信息)。一旦定义了组,属于同一集群的所有元素都被认为是相似的,因此我们被隐式授权共享差异。如果用户 A 购买了产品 P 并给予肯定评价,我们可以向没有购买的用户 B 建议该商品,反之亦然。这个过程看起来可以是任意的,但是当元素的数量很大,并且特征向量包含许多有区别的元素(例如,等级)时,它会非常有效。

生成模型

另一种无监督的方法是基于生成模型。这个概念与我们已经讨论过的监督算法没有太大区别,但是,在这种情况下,数据生成过程不包含任何标签。因此,目标是对参数化分布进行建模并优化参数,从而使候选分布和数据生成过程之间的距离最小化:

该过程通常基于库尔巴克-莱布勒散度或其他类似的度量:

训练阶段结束,我们假设 L → 0 ,那么 p ≈ p 数据T5。通过这种方式,我们没有将分析局限于可能样本的子集,而是局限于整个分布。使用生成模型允许您绘制新的样本,这些样本可能与为训练过程选择的样本非常不同,但它们总是属于相同的分布。因此,它们(可能)总是可以接受的。

例如,生成对抗网络 ( GAN )是一种特定的深度学习模型,它能够学习图像集的分布,产生与训练样本几乎无法区分的新样本(从视觉语义的角度来看)。由于无监督学习是本书的主要主题,因此我们在此介绍中不会进一步详述 GAN。所有这些概念都将在接下来的章节中进行广泛的讨论(用实际的例子)。

关联规则

我们考虑的最后一种无监督方法是基于关联规则的发现,它在数据挖掘领域极其重要。一个常见的场景是由产品子集组成的商业交易集合。目标是找出产品之间最重要的关联(例如购买PIT5】和PjT9】的概率为 70%)。特定的算法可以高效地挖掘整个数据库,突出显示出于战略和物流目的可以考虑的所有关系。例如,网上商店可以采用这种方法来促销那些经常与其他商品一起购买的商品。此外,预测方法允许通过建议所有那些由于其他项目的销售增加而很可能售罄的产品来简化供应过程。**

在这一点上,向读者介绍一个无监督学习的实际例子是有帮助的。不需要特别的先决条件,但最好有概率论的基础知识。

无人监管的 hello world!

由于这本书完全致力于无监督算法,我决定不把一个简单的聚类分析展示为 hello world!例子,而是一个相当基本的生成模型。让我们假设我们正在监控每小时到达地铁站的列车数量,因为我们需要确定车站所需的安全代理数量。特别是,我们被要求每列火车至少有一名代理人,每当人数较少时,我们将支付罚款。

此外,更容易在每个小时开始时发送一个组,而不是逐个控制代理。因为问题很简单,我们也知道好的分布是泊松分布,用 μ 参数化,也是均值。从理论上,我们知道,在独立性的主要假设下,这样的分布可以有效地模拟在固定时间框架内发生的事件的随机数。在一般情况下,生成模型基于参数化分布(例如,具有神经网络),并且没有对其族做出具体假设。只有在某些特定的情况下(例如,高斯混合),选择具有特定属性的分布才是合理的,并且在不损失严格性的情况下,我们可以将此示例视为这样的场景。

泊松分布的概率质量函数为:

该分布描述了在预定间隔内观察到 k 事件的概率。在我们的例子中,间隔总是一个小时,我们热衷于估计观察超过 10 列火车的概率。如何获得 μ 的正确数字?

最常见的策略叫做最大似然估计 ( MLE )。它收集一组观察值,并找到 μ 的值,该值最大化了所有点都由我们的分布生成的概率。

假设我们已经收集了 N 个观测值(每个观测值是一小时内到达的次数) μ 相对于所有样本的似然度是所有样本(为简单起见,假设为 IID)在使用 μ 计算的概率分布下的联合概率:

当我们处理乘积和指数时,计算对数似然是一个常见的规则:

一旦计算出对数似然性,就可以将相对于 μ 的导数设置为 0,以便找到最佳值。在这种情况下,我们省略了证明(这很容易获得),直接得出 μ 的最大似然估计:

我们很幸运!最大似然估计只是到达时间的平均值。这意味着,如果我们用平均值 μ 观察到 N 个值,那么最有可能产生它们的泊松分布将 μ 作为特征系数。因此,从这种分布中提取的任何其他样本将与观察到的数据集兼容

我们现在可以从第一次模拟开始。假设我们在一个工作日的下午早些时候收集了 25 个观察结果,如下所示:

import numpy as np

obs = np.array([7, 11, 9, 9, 8, 11, 9, 9, 8, 7, 11, 8, 9, 9, 11, 7, 10, 9, 10, 9, 7, 8, 9, 10, 13])
mu = np.mean(obs)

print('mu = {}'.format(mu))

最后一个命令的输出如下:

mu = 9.12

因此,我们平均每小时有 9 列火车到达。直方图如下图所示:

Histogram of the initial distribution

为了计算所请求的概率,我们需要使用累积分布函数 ( CDF ),该函数在 SciPy(在scipy.stats包中)中实现。特别是,由于我们感兴趣的是观察到比固定值多的列车的概率,所以有必要使用生存函数 ( SF ),对应于 1-CDF ,如下:

from scipy.stats import poisson

print('P(more than 8 trains) = {}'.format(poisson.sf(8, mu)))
print('P(more than 9 trains) = {}'.format(poisson.sf(9, mu)))
print('P(more than 10 trains) = {}'.format(poisson.sf(10, mu)))
print('P(more than 11 trains) = {}'.format(poisson.sf(11, mu)))

前面片段的输出如下:

P(more than 8 trains) = 0.5600494497386543
P(more than 9 trains) = 0.42839824517059516
P(more than 10 trains) = 0.30833234660452563
P(more than 11 trains) = 0.20878680161156604

不出所料,观察 10 列以上的概率很低(30%),派 10 个特工似乎不太合理。然而,由于我们的模型是自适应的,我们可以继续收集观察结果(例如,在清晨),如下所示:

new_obs = np.array([13, 14, 11, 10, 11, 13, 13, 9, 11, 14, 12, 11, 12, 14, 8, 13, 10, 14, 12, 13, 10, 9, 14, 13, 11, 14, 13, 14])

obs = np.concatenate([obs, new_obs])
mu = np.mean(obs)

print('mu = {}'.format(mu))

μ 的新值如下:

mu = 10.641509433962264

现在平均差不多每小时 11 趟火车。假设我们已经收集了足够的样本(考虑到所有潜在的事故),我们可以重新估计概率,如下所示:

print('P(more than 8 trains) = {}'.format(poisson.sf(8, mu)))
print('P(more than 9 trains) = {}'.format(poisson.sf(9, mu)))
print('P(more than 10 trains) = {}'.format(poisson.sf(10, mu)))
print('P(more than 11 trains) = {}'.format(poisson.sf(11, mu)))

输出如下:

P(more than 8 trains) = 0.7346243910180037
P(more than 9 trains) = 0.6193541369812121
P(more than 10 trains) = 0.49668918740243756
P(more than 11 trains) = 0.3780218948425254

有了新的数据集,观察到 9 列以上的概率约为 62%(这证实了我们最初的选择),但现在观察到 10 列以上的概率约为 50%。由于我们不想冒险支付罚款(这比代理的成本高),所以最好总是派出一组 10 名代理。为了得到进一步的确认,我们决定从分布中抽取 2,000 个值,如下所示:

syn = poisson.rvs(mu, size=2000)

相应的直方图如下图所示:

Histogram of 2000 points sampled from the final Poisson distribution

该图确认了 10 之后的一个峰值(非常接近 11)和从 k = 13 开始的快速衰减,这已经使用有限的数据集被发现(比较直方图的形状以进一步确认)。然而,在这种情况下,我们生成的潜在样本无法出现在我们的观察集中。最大似然估计保证概率分布与数据一致,并保证新样本得到相应的加权。这个例子显然非常简单,它的目标只是展示一个生成模型的动态。

我们将在本书的下几章讨论许多其他更复杂的模型和例子。许多算法共有的一项重要技术在于不选择预定义的分布(这意味着先验知识),而是使用灵活的参数模型(例如,神经网络)来找出最优分布。只有当潜在的随机过程具有很高的置信度时,预定义先验的选择才是合理的。在所有其他情况下,最好避免任何假设,只依靠数据来找到数据生成过程的最合适的近似值。

半监督学习算法

半监督场景可以被认为是标准的监督场景,其利用了属于无监督学习技术的一些特征。事实上,当很容易获得大的未标记数据集但标记成本很高时,就会出现一个非常常见的问题。因此,合理的做法是只标记一小部分样本,并将标记传播到所有与标记样本的距离低于预定阈值的未标记样本。如果数据集是从单个数据生成过程中提取的,并且标记样本是均匀分布的,则半监督算法可以获得与监督算法相当的精度。在这本书里,我们不讨论这些算法;然而,简要介绍两个非常重要的模型是有帮助的:

  • 标签传播
  • 半监督支持向量机

第一种称为标签传播,其目标是将少数样本的标签传播到更大的群体中。这个目标是通过考虑一个图来实现的,其中每个顶点代表一个样本,每个边都使用距离函数进行加权。通过一个迭代过程,所有标记的样本将把它们的标记值的一小部分发送给它们的所有邻居,并且重复这个过程,直到标记停止改变。这个系统有一个稳定点(也就是说,一个不能再进化的配置),算法可以通过有限的迭代次数很容易地达到它。

标签传播在所有那些可以根据相似性度量来标记一些样本的环境中非常有用。例如,一家在线商店可能有大量客户,但只有 10%的人透露了他们的性别。如果特征向量足够丰富,能够代表男性和女性用户的共同行为,那么就有可能采用标签传播算法来猜测没有透露的客户的性别。当然,重要的是要记住,所有的作业都是基于相似样本具有相同标签的假设。这在许多情况下可能是正确的,但是当特征向量的复杂性增加时,这也可能是误导。

另一个重要的半监督算法家族是基于标准 SVM (简称支持向量机)对包含未标记样本的数据集的扩展。在这种情况下,我们不想传播现有的标签,而是传播分类标准。换句话说,我们希望使用已标记的数据集训练分类器,并将判别规则扩展到未标记的样本。

与只能评估未标记样本的标准过程相反,半监督 SVM 使用它们来校正分离超平面。假设总是基于相似性:如果 A 有标签 1 ,而未标签样本 Bd(A,B) < ε(其中 ε 是预定义的阈值),那么合理的假设是 B 的标签也是 1 。这样,即使只有一个子集被手动标记,分类器也可以在整个数据集上获得高精度。类似于标签传播,只有当数据集的结构不是非常复杂时,特别是当相似性假设成立时,这些类型的模型才是可靠的(不幸的是,在某些情况下,找到合适的距离度量非常困难,因此许多相似的样本确实不相似,反之亦然)。

强化学习算法

强化学习场景可以被认为是一个有监督的场景,其中隐藏的老师在模型的每个决策之后只提供近似的反馈。更正式地说,强化学习的特征是代理和环境之间的持续交互。前者负责做出决策(行动),最终确定以增加回报,而后者为每一项行动提供反馈。反馈通常被认为是一种奖励,它的价值可以是积极的(行动已经成功),也可以是消极的(行动不应该重复)。当代理分析环境(状态)的不同配置时,每个奖励都必须被视为绑定到元组(动作、状态)。因此,最终目标是找到一种策略(一种在每个州都建议最佳行动的策略),使预期的总回报最大化。

强化学习的一个非常经典的例子是学习如何玩游戏的代理。在一集里,代理测试所有遇到的状态下的动作并收集奖励。一种算法修正了策略,以减少非积极行为(即那些奖励为积极的行为)的可能性,并增加在剧集结束时可获得的预期总奖励。

强化学习有很多有趣的应用,不仅限于游戏。例如,推荐系统可以根据用户提供的二元反馈(例如,拇指向上或向下)来校正建议。强化学习和监督学习的主要区别在于环境提供的信息。事实上,在有监督的情况下,校正通常与它成比例,而在强化学习中,必须考虑一系列动作和未来的奖励来分析它。因此,修正通常基于对预期报酬的估计,其效果受后续行动价值的影响。例如,一个监督模型没有记忆,因此它的校正是立即的,而一个强化学习代理必须考虑一集的部分展开,以便决定一个动作实际上是否是负面的。

强化学习是机器学习的一个迷人的分支。遗憾的是,本主题超出了本作品的范围,因此我们不再详细讨论(您可以在用 Python 进行强化学习的实践,Ravichandiran S,Packt Publishing, 2018 和掌握机器学习算法,Bonaccorso G,Packt Publishing, 2018 中找到进一步的细节)。

我们现在可以简要解释为什么选择 Python 作为探索无监督学习世界的主要语言。

为什么 Python 用于数据科学和机器学习?

在继续更多的技术讨论之前,我认为解释选择 Python 作为本书的编程语言是有帮助的。在过去的十年里,数据科学和机器学习领域的研究呈指数级增长,有数千篇有价值的论文和几十个完整的工具。特别是,由于其高效、优雅和紧凑,Python 被许多研究人员和程序员选择来创建一个完整的科学生态系统,该生态系统已经免费发布。

如今,像 scikit-learn、SciPy、NumPy、Matplotlib、pandas 和许多其他软件包代表了数百个生产就绪系统的主干,它们的使用量一直在增长。此外,复杂的深度学习应用程序,如 Anano、TensorFlow 和 PyTorch,允许每个 Python 用户创建和训练复杂的模型,而没有任何速度限制。事实上,需要注意的是 Python 不再是脚本语言了。它支持几十种特定的任务(例如,网络框架和图形),并且可以与用 C 或 C++编写的本机代码接口。

由于这些原因,Python 几乎是任何数据科学项目中的最佳选择,由于它的特性,所有不同背景的程序员都可以在短时间内轻松学会有效使用它。其他免费解决方案也是可用的(例如,R、Java 或 Scala),然而,在 R 的情况下,有统计和数学函数的完整覆盖,但它缺乏构建完整应用程序所必需的支持框架。相反,Java 和 Scala 有一个完整的生产就绪库生态系统,但是,特别是,Java 不像 Python 那样紧凑和易于使用。此外,对本机代码的支持要复杂得多,并且大多数库都完全依赖于 JVM(从而导致性能损失)。

Scala 在大数据全景图中获得了重要的地位,这得益于它的功能属性以及 Apache Spark(可用于利用大数据执行机器学习任务)等框架的存在。然而,考虑到所有的利弊,Python 仍然是最佳选择,这就是为什么它被选为这本书的原因。

摘要

在本章中,我们讨论了使用机器学习模型的主要原因,以及如何分析数据集以描述其特征、列举特定行为背后的原因、预测未来行为并影响它。

我们还探讨了有监督、无监督、半监督和强化学习之间的区别,重点是前两种模型。我们还使用了两个简单的例子来理解监督和非监督方法。

在下一章中,我们将介绍聚类分析的基本概念,重点讨论一些非常著名的算法,如 k-means 和K-近邻 ( KNN )以及最重要的评估指标。

问题

  1. 当监督学习不适用时,无监督学习是最常见的选择。正确吗?

  2. 你公司的首席执行官要求你找出决定消极销售趋势的因素。需要进行什么样的分析?

  3. 给定独立样本数据集和候选数据生成过程(例如,高斯分布),通过对所有样本的概率求和来获得似然性。是正确的吗?

  4. 在哪个假设下,可能性可以被计算为单一概率的乘积?

  5. 假设我们有一个包含一些未知数字特征(例如年龄、分数等)的学生数据集。你想把男生和女生分开,所以你决定把数据集分成两组。不幸的是,这两个集群都有大约 50%的男生和 50%的女生。你如何解释这个结果?

  6. 考虑前面的例子,但是重复实验并分成五组。你期望在他们每个人身上找到什么?(列举一些合理的可能性。)

  7. 你聚集了一家在线商店的顾客。给定一个新样本,你能做出什么样的预测?

进一步阅读

  • 机器学习算法第二版博纳科索格帕克特出版,2018
  • Python 强化学习实操ravichanduran s .Packt**出版,2018
  • 与 NumPy 和熊猫的实践数据分析米勒 C.帕克特出版,2018

二、聚类基础

在这一章中,我们将介绍聚类分析的基本概念,将注意力集中在我们的主要原则上,这些原则是许多算法共有的,也是可以用来评估方法性能的最重要的技术。

特别是,我们将讨论:

  • 聚类和距离函数介绍
  • K-均值和 K-均值++
  • 评估指标
  • K-最近邻居 ( KNN )
  • 矢量量化 ( VQ )

技术要求

本章中的代码要求:

数据集可以通过 UCI 获得。该 CSV 文件可从https://archive . ics . UCI . edu/ml/机器学习-数据库/乳腺癌-威斯康星/wdbc.data 下载,除了添加加载阶段会出现的列名外,不需要任何预处理。

GitHub 存储库中提供了以下示例:

https://github . com/PacktPublishing/HandsOn-Unsupervised-Learning-with-Python/chapter 02

聚类介绍

正如我们在第 1 章无监督学习入门中所解释的,聚类分析的主要目标是根据相似性度量或接近性标准对数据集的元素进行分组。在本章的第一部分,我们将重点关注前一种方法,而在第二部分和下一章,我们将分析利用数据集其他几何特征的更通用的方法。

我们来看一个数据生成过程 p 数据 (x) 并从中抽取 N 个样本:

可以假设 p 数据 (x) 的概率空间可划分为包含 K (对于 K=1,2,...)区域,以便 p 数据(x;k) 表示样本属于一个聚类的概率 k 。这样,我们就说明了当 p 数据 (x) 确定时,每个可能的聚类结构都已经存在。可以对更接近 p 数据 (x) 的聚类概率分布做进一步的假设(正如我们将在第 5 章软聚类和高斯混合模型中看到的)。然而,当我们试图将概率空间(和相应的样本)分成内聚组时,我们可以假设两种可能的策略:

  • 硬聚类:在这种情况下,每个样本 x p ∈ X 被分配到一个聚类kIT9】和kI∨kj=∅i ≠ j 。我们将要讨论的大多数算法都属于这一类。在这种情况下,问题可以表示为一个参数化函数,该函数为每个输入样本分配一个聚类:

  • 软聚类:通常细分为概率模糊聚类,这种方法确定了属于预定聚类的每个样本 x p ∈ X 的概率 p(x) 。因此,如果有 K 簇,我们有一个概率向量p(x)=【p1(x),p 2 (x)、...,pk(x)】,其中 p i (x) 代表被分配到集群 i 的概率。在这种情况下,聚类并不脱节,通常,样本将属于具有相当于概率的隶属度的所有聚类(这是模糊聚类特有的概念)。

出于我们的目的,在这一章中,我们简单地假设数据集 X 是从一个数据生成过程中绘制的,给定一个度量函数,该数据生成过程的空间可被分割成彼此分离的紧凑区域。事实上,我们的主要目标是找到满足最大内聚最大分离双重属性的 K 团簇。在讨论 K-means 算法时,这个概念会更加清晰。然而,可以将集群想象成密度远远高于在分隔两个或多个集群的空间中可观察到的密度的斑点,如下图所示:

Bidimensional clustering structure obeying the rule of maximum cohesion and maximum separation. Nk represents the number of samples belonging to the cluster k while Nout(r) is the number of samples that are outside the balls centered at each cluster center with a maximum radius r

在上图中,考虑到样本离中心的最大距离,我们假设大多数样本将被其中一个球捕获。然而,由于我们不想对球的生长施加任何限制(也就是说,它可以包含任何数量的样本),因此最好不要考虑半径,而是通过对小的子区域(整个空间的子区域)进行采样并收集它们的密度来评估分离区域。

在理想情况下,集群跨越密度为 D 的一些子区域,而分离区域的特征是密度为 d < < D 。关于几何性质的讨论可能变得极其复杂,而且在许多情况下,这是极其理论化的。此后,我们只考虑属于不同簇的最近点之间的距离。如果该值远小于所有聚类的样本与其聚类中心之间的最大距离,我们可以确定分离是有效的,并且很容易区分聚类和分离区域。相反,当使用距离度量时(例如,在 K-means 中),我们需要考虑的另一个重要要求是聚类的凸性。如果 ∀ x 1 ,x 2 ∈ C ,并且属于连接x1t17】和x2t21】的线段的所有点都属于 C ,则通用集合 C 是凸的。在下图中,对凸簇和非凸(凹)簇进行了比较:**

Example of a convex cluster (left) and a concave one (right)

遗憾的是,由于距离函数的对称性,像 K-means 这样的算法无法管理非凸聚类。在我们的探索中,我们将展示这个限制以及其他方法如何克服它。

距离函数

即使聚类的一般定义通常基于相似度的概念,也很容易使用其逆,该逆由距离函数(相异度度量)表示。最常见的选择是欧氏距离,但是在选择它之前,需要考虑它的性质以及它们在高维空间中的行为。让我们首先引入闵可夫斯基距离作为欧几里得距离的推广。如果样品为xI∈ℜnt13】,则定义为:

对于 p=1 ,我们得到曼哈顿(或城市街区)距离,而 p=2 对应标准欧氏距离。我们要理解 d pp → ∞ 时的行为。假设我们在二维空间中工作,有一个中心为 x c =(0,0) 的簇和一个采样点 x=(5,3) ,距离 d p (x c ,x) 相对于 p 的不同值为:

很明显(也很简单的证明)如果| x1j-x2T7】j|是最大的分量绝对差,p→∩d p (x c ,x)→| x1T19】j-x 这意味着,如果我们将相似性(或不相似性)视为所有组件差异的结果,我们需要为 p 选择一个小值(例如, p=12 )。另一方面,如果仅根据成分之间的最大绝对差异就必须认为两个样品不同,则 p 的较高值是合适的。一般来说,这种选择非常依赖于上下文,不能轻易概括。出于我们的目的,我们通常只考虑欧几里得距离,这在大多数情况下是合理的。另一方面,当 N → ∞ 时,选择较大的 p 值有重要的后果。让我们从一个例子开始。对于 pN 的不同值,我们要测量 1N 矢量(属于 N 的矢量,所有分量等于 1 )与原点之间的距离(使用对数标尺压缩 y 轴),这可以通过以下方式完成:

import numpy as np

from scipy.spatial.distance import cdist

distances = np.zeros(shape=(8, 100))

for i in range(1, distances.shape[0] + 1):
    for j in range(1, distances.shape[1] + 1):
        distances[i - 1, j - 1] = np.log(cdist(np.zeros(shape=(1, j)), np.ones(shape=(1, j)), 
                                               metric='minkowski', p=i)[0][0])

距离如下图所示:

Minkowski distances (log-scale) for different values of p and N

第一个结果是,如果我们为 N 选取一个值,当 p → ∞ 时,距离收缩并饱和。这是闵可夫斯基距离结构的正常结果,但有另一个因素,敏锐的读者可能已经注意到了。让我们想象一下将 1N 矢量的一个分量设置为 0.0 。这相当于从 N 维超立方体的一个顶点移动到另一个顶点。距离会怎样?嗯,用一个例子很容易证明,当 p → ∞ 时,两个距离收敛到同一个值。特别是,阿格沃尔、欣内堡和凯米(在关于高维空间中距离度量的惊人行为,阿格沃尔 C. C .,欣内堡 a .,凯米 D. A .,ICDT 2001)证明了一个重要的结果。

假设我们有一个分布 M 二元样本xI∑(0,1) d 。如果我们使用闵可夫斯基度量,我们可以计算从 p(x) 采样的两个点与原点之间的最大(Dmaxp)和最小(Dminp)距离(一般来说,这个距离可以通过分析计算,但也可以使用迭代程序继续采样,直到 D 作者证明了以下不等式成立:

在之前的公式中,CpT3】是一个依赖于 p 的常数。当 p → ∞ 时,期望值的极限E【Dmaxp-Dminp被捕获在边界k1CpD1/p-1/2(M-1)之间作为术语d1/p-1/2→0p > 2d → ∞ 时,最大和最小距离之差的期望值收敛到 0 。这意味着,独立于样本,当维度足够高并且 p > 2 时,使用闵可夫斯基距离几乎不可能区分两个样本。当我们在距离函数上寻找相似性时,这个定理警告我们当 d > > 1p 选择大值。当 d > > 1 (即使 p=1 将是最佳选择)时,欧几里德度量的常见选择也是相当可靠的,因为它对组件的重量影响最小(可以假设它们具有相同的重量),并保证在高维空间中的可区分性。相反,高维空间中的 p > > 2 对于最大分量保持不变而所有其他分量都被修改的所有样本产生不可区分的距离(例如,如果 x=(5,0) → (5,a) 其中 | a| < 5 ,如下例所示:**

import numpy as np

from scipy.spatial.distance import cdist

distances = []

for i in range(1, 2500, 10):
    d = cdist(np.array([[0, 0]]), np.array([[5, float(i/500)]]), metric='minkowski', p=15)[0][0]
    distances.append(d)

print('Avg(distances) = {}'.format(np.mean(distances)))
print('Std(distances) = {}'.format(np.std(distances)))

输出如下:

Avg(distances) = 5.0168687736484765
Std(distances) = 0.042885311128215066

因此,对于p = 15,所有样品 (5,x) (对于x≈0.002,5.0) 与原点的距离,其平均值约为5.0,标准偏差约为0.04。当p变大时,Avg(distances) = 5.0Std(distances) = 0.04

在这一点上,我们可以开始讨论最常见和广泛采用的聚类算法之一:K-means。

k 均值

K-means 是最大分离最大内聚力原则的最简单实现。让我们假设我们有一个数据集x∈ℜm×nt5】(即 M N 维样本),我们希望将其拆分为 K 个聚类和一组 K 质心对应于分配给每个聚类的样本的平均值 K j :

集合 M 和质心有一个指示迭代步骤的附加索引(上标)。从最初的猜测 M (0) 开始,K-means 试图最小化称为惯性的目标函数(即,分配给一个聚类的样本之间的总平均聚类内距离 K j 及其质心 μ j ):

很容易理解 S(t) 不能认为是绝对测度,因为它的值受样本方差的影响很大。然而, S(t+1) < S(t) 意味着质心正在向最佳位置移动,在该位置,分配给聚类的点与相应质心的距离尽可能最小。因此,迭代过程(也称为劳氏算法)从用随机值初始化 M (0) 开始。下一步是将每个样本 x i ∈ X 分配给质心距离 x i 最小的聚类:

一旦完成所有作业,新质心将作为算术平均值重新计算:

重复该过程,直到质心停止变化(这也意味着序列 S(0) > S(1) >...> S(t 结束 ) )。读者应该马上明白,计算时间受最初猜测的影响很大。如果 M (0) 非常接近 M (t ) ,几次迭代就能找到最优配置。反之,当 M (0) 纯随机时,低效初始选择的概率接近于 1 (也就是说,每一个初始均匀随机选择在计算复杂度上几乎是等价的)。

K-means++

找到最优初始构型相当于最小化惯性;然而,Arthur 和 Vassilvitskii(在 K-means++:小心播种的优势,Arthur D,Vassilvitskii S,第十八届年度 ACM-SIAM 离散算法研讨会论文集, 2007)提出了一种替代的初始化方法(称为 K-means++ ,该方法可以通过选择更高概率接近最终质心的初始质心来显著提高收敛速度。完整的证明相当复杂,可以在前面提到的论文中找到。在这种情况下,我们直接提供最终结果和一些重要的结果。

让我们考虑函数D()定义为:

D()代表样品 x ∈ X 和已经选择的质心之间的最短距离。一旦计算出函数,就可以确定概率分布 G(x) ,如下所示:

第一个质心 μ 1 是从均匀分布中采样的。此时,可以计算所有样品的D()**X∈X以及分布 G(x) 。很简单,如果我们从 G(x) 采样,在密集区域选择值的概率比均匀采样或在分离区域选取质心的概率大得多。因此,我们继续从 G(x) 中取样 μ 2 。重复该过程,直到所有 K 质心都已确定。当然,由于这是一种概率方法,我们不能保证最终的配置是最佳的。但是,K-means++的使用是O(log K)-竞争。事实上,如果 S optS 的理论最优值,那么作者证明了以下不等式成立:

由于 S 减少了一个更好的选择,前面的公式为期望值E【S】设定了一个上限,大致与 log K 成正比。例如对于 K=10E【S】19.88SoptE【S】12.87Sopt对于 K=3 。这一结果揭示了两个重要因素。第一个是 K 不是特别大的时候 K-means++表现更好,第二个,可能也是最重要的一个,是单个 K-means++初始化不能足以获得最优配置。因此,常见的实现(例如 scikit-learn)会执行可变数量的初始化,并选择初始惯性最小的一个。

威斯康星乳腺癌数据集分析

在本章中,我们使用众所周知的乳腺癌威斯康星数据集进行聚类分析。最初,提出数据集是为了训练分类器;然而,它对于非平凡的聚类分析非常有帮助。它包含 569 条记录,由 32 个属性组成(包括诊断和识别号)。所有属性都与肿瘤的生物学和形态学属性严格相关,但我们的目标是考虑到基本事实(良性或恶性)和数据集的统计属性来验证一般假设。在继续之前,澄清一些要点很重要。数据集是高维的,聚类是非凸的(因此我们不能期望完美的分割)。此外,我们的目标不是使用聚类算法来获得分类器的结果;因此,基本事实只能作为潜在群体的一般标志来考虑。这样一个例子的目的是展示如何执行一个简短的初步分析,选择最佳数量的集群,并验证最终结果。

下载后(如技术要求部分所述),CSV 文件必须放在我们一般称为<data_folder>的文件夹中。第一步是加载数据集,并通过熊猫DataFrame展示的函数describe()执行全局统计分析,如下所示:

import numpy as np
import pandas as pd

bc_dataset_path = '<data_path>\wdbc.data'

bc_dataset_columns = ['id','diagnosis', 'radius_mean', 'texture_mean', 'perimeter_mean', 
 'area_mean', 'smoothness_mean', 'compactness_mean', 'concavity_mean', 
 'concave points_mean', 'symmetry_mean', 'fractal_dimension_mean',
 'radius_se','texture_se', 'perimeter_se', 'area_se', 'smoothness_se', 
 'compactness_se', 'concavity_se', 'concave points_se', 'symmetry_se', 
 'fractal_dimension_se', 'radius_worst', 'texture_worst', 'perimeter_worst', 
 'area_worst', 'smoothness_worst', 'compactness_worst', 'concavity_worst',
 'concave points_worst', 'symmetry_worst', 'fractal_dimension_worst']

df = pd.read_csv(bc_dataset_path, index_col=0, names=bc_dataset_columns).fillna(0.0)
print(df.describe())

我强烈建议使用 Jupyter Notebook(在这种情况下,命令必须只有df.describe()),其中所有的命令都会产生内联输出。出于实际原因,在下面的截图中,显示了表格输出的第一部分(包含八个属性):

Statistical report of the first eight attributes of the dataset

当然,我邀请读者检查所有属性的值,即使我们只关注一个子集。特别是,我们需要观察前八个属性之间存在的不同尺度。标准偏差范围从 0.01 到 350,这意味着许多向量可能仅仅因为一个或两个属性而极其相似。另一方面,使用方差缩放对值进行归一化将赋予所有属性相同的责任(例如,area_mean143.52501之间有界,而smoothness_mean0.050.16之间有界)。迫使它们具有相同的方差会影响因子的生物学影响,并且由于我们没有任何具体的指示,我们没有授权做出这样的选择)。显然,一些属性在聚类过程中将具有更高的权重,我们接受它们的主要影响作为与上下文相关的条件。

现在我们从perimeter_meanarea_meansmoothness_meanconcavity_meansymmetry_mean的配对剧情开始初步分析。该图显示在下面的截图中:

Pair-plot of perimeter mean, area mean, smoothness mean, concavity mean, and symmetry mean

该图将每个非对角线属性绘制为所有其他属性的函数,而对角线图表示每个属性分成两个部分的分布(在这种情况下,这就是诊断)。因此,第二个非对角线图(左上角)是perimeter_mean作为area_mean的函数的图,以此类推。快速分析突出了一些有趣的元素:

  • area_meanperimeter_mean有明显的相关性,决定了明显的分离。当area_mean大于约 1000 时,显然周长也增加,诊断从良性突然切换到恶性。因此,这两个属性是最终结果的决定因素,其中一个可能是多余的。
  • 其他图(例如,perimeter_mean / area_meansmoothness_meanarea_meansymmetry_meanconcavity_meansmoothness_meanconcavity_meansymmetry_mean)具有水平间隔(将轴反转为垂直)。这意味着,对于自变量( x 轴)假设的几乎所有值,有一个阈值将另一个变量的值分为两组(良性和恶性)。
  • 一些图(例如,perimeter_mean / area_meanconcavity_mean / concavity_meansymmetry_mean)显示了稍微负的倾斜对角线间隔。这意味着,当自变量很小时,对于因变量的几乎所有值,诊断保持不变,而另一方面,当自变量变得越来越大时,诊断成比例地切换到相反的值。例如,对于小的perimeter_mean值,concavity_mean可以在不影响诊断的情况下达到其最大值,(这是良性的),而perimeter_mean > 150总是独立于concavity_mean产生恶性诊断。

当然,我们不能轻易地从分割分析中得出结论(因为我们需要考虑所有的交互),但是为了给每个聚类提供一个语义标签,这个活动将是有帮助的。在这一点上,通过t-分布式随机邻域嵌入 ( t-SNE )变换在二维平面上可视化数据集(没有非结构属性)是有帮助的(更多细节,请查看使用 t-SNE 可视化数据,范德马滕 l,辛顿 g,机器学习研究杂志 9, 2008)。这可以通过以下方式实现:

import pandas as pd

from sklearn.manifold import TSNE

cdf = df.drop(['diagnosis'], axis=1)

tsne = TSNE(n_components=2, perplexity=10, random_state=1000)
data_tsne = tsne.fit_transform(cdf)

df_tsne = pd.DataFrame(data_tsne, columns=['x', 'y'], index=cdf.index)
dff = pd.concat([df, df_tsne], axis=1)

下面的截图显示了结果图:

Bidimensional t-SNE plot of the Breast Cancer Wisconsin dataset

图是高度非线性的(别忘了这是从 302 的投影),但大部分恶性样本都在半平面 y < 0 内。不幸的是,也有中等百分比的良性样本在这个区域,因此我们不期望使用 K=2 的完美分离(在这种情况下,很难理解真实的几何形状,但是 t-SNE 保证二维分布与原始高维分布具有最小的库尔巴克-莱布勒散度)。现在让我们使用 K=2 执行初始聚类。我们将使用n_clusters=2max_iter=1000创建KMeans scikit-learn 类的实例(只要有可能,random_state将始终设置为等于1000)。

其余参数为默认参数(K-means++初始化,10 次尝试),如下所示:

import pandas as pd

from sklearn.cluster import KMeans

km = KMeans(n_clusters=2, max_iter=1000, random_state=1000)
Y_pred = km.fit_predict(cdf)

df_km = pd.DataFrame(Y_pred, columns=['prediction'], index=cdf.index)
kmdff = pd.concat([dff, df_km], axis=1)

下面的截图显示了结果图:

K-means clustering (with K=2) of the Breast Cancer Wisconsin dataset

不出意外的话,对于 y < -20 来说,结果还是比较准确的,但是算法并不能将边界点( y ≈ 0 )也纳入到主要恶性聚类中。这主要是由于原始集合的非凸性,用 K-means 很难解决这个问题。而且在投影中,大部分带有 y ≈ 0 的恶性样本与良性样本混在一起,因此基于接近度的其他方法误差概率也较高。正确分离这些样本的唯一机会来自原始分布。事实上,如果属于同一类别的点可以被 30 中不相交的球捕获,K-means 也可以成功。不幸的是,在这种情况下,混合集看起来非常有凝聚力,因此我们不能期望在没有转换的情况下提高性能。然而,出于我们的目的,该结果允许我们应用主要的评估指标,然后从 K=2 移动到更大的值。借助 K > 2 ,我们将分析一些集群,将它们的结构与配对图进行比较。

评估指标

在本节中,我们将分析一些常用的方法,这些方法可以用来评估聚类算法的性能,并帮助找到最佳的聚类数。

最小化惯性

K-means 和类似算法的最大缺点之一是对聚类数量的明确要求。有时,这部分信息是由外部约束强加的(例如,在乳腺癌的例子中,只有两种可能的诊断),但在许多情况下(当需要探索性分析时),数据科学家必须检查不同的配置并对其进行评估。评估 K-means 性能和选择适当数量簇的最简单方法是基于不同最终惯性的比较。

让我们从下面这个简单的例子开始,它基于 12 个非常紧凑的高斯斑点,这些斑点是用 scikit-learn 函数make_blobs()生成的:

from sklearn.datasets import make_blobs

X, Y = make_blobs(n_samples=2000, n_features=2, centers=12, 
                  cluster_std=0.05, center_box=[-5, 5], random_state=100)

这些斑点如下图所示:

Dataset made up of 12 disjoint bidimensional blobs

现在让我们计算 K ∈ [2,20],的惯性(在训练的KMeans模型中作为实例变量inertia_可用),如下所示:

from sklearn.cluster import KMeans

inertias = []

for i in range(2, 21):
    km = KMeans(n_clusters=i, max_iter=1000, random_state=1000)
    km.fit(X)
    inertias.append(km.inertia_)

结果图如下:

Inertia as a function of the number of clusters

前面的情节显示了一种常见的行为。当团簇的数量很少时,密度成比例地低,因此内聚力低,因此惯性高。集群数量的增加迫使模型创建更具凝聚力的群体,惯性开始突然减小。如果我们继续这个过程和 M > > K ,我们将观察到一个非常缓慢的接近值,该值对应于一个配置,其中 K=M (每个样本是一个聚类)。一般的启发式规则(当没有外部约束时)是选择对应于将高变化区域与几乎平坦的区域分开的点的聚类数。通过这种方式,我们确信所有集群都达到了最大的凝聚力,没有内部分裂。当然,在这种情况下,如果我们选择 K=15 ,九个斑点将被分配给不同的簇,而另外三个将被分成两部分。显然,当我们分裂一个高密度区域时,惯性仍然很低,但是最大分离的原则不再被遵循。

我们现在可以用威斯康星乳腺癌数据集重复实验,其 K ∈ [2,50] 、如下:

from sklearn.cluster import KMeans

inertias = []

for i in range(2, 51):
    km = KMeans(n_clusters=i, max_iter=1000, random_state=1000)
    km.fit(cdf)
    inertias.append(km.inertia_)

下面的截图显示了结果图:

Inertia as a function of the number of clusters for the Breast Cancer Wisconsin dataset

在这种情况下,基本事实表明,我们应该根据诊断结果分成两组。然而,该图显示了急剧下降,在 K=8 处结束,并以较低的斜率继续,直到约 K=40 。在初步分析期间,我们已经看到二维投影由许多共享同一诊断的孤立斑点组成。因此,我们可以决定采用例如 K=8 并分析对应于每个聚类的特征。由于这不是一个分类任务,基本事实可以作为主要参考,但是正确的探索性分析可以尝试理解子结构的组成,以便为技术人员(例如,医生)提供进一步的细节。

现在,让我们在乳腺癌威斯康星数据集上对八个聚类进行 K 均值聚类,以描述两个样本组的结构,如下所示:

import pandas as pd

from sklearn.cluster import KMeans

km = KMeans(n_clusters=8, max_iter=1000, random_state=1000)
Y_pred = km.fit_predict(cdf)

df_km = pd.DataFrame(Y_pred, columns=['prediction'], index=cdf.index)
kmdff = pd.concat([dff, df_km], axis=1)

下面的截图显示了结果图:

K-means clustering (with K=8) result for the Breast Cancer Wisconsin dataset

现在让我们考虑位于地块底部的子聚类( -25 < x < 30-60 < y < -40 ,如下所示:

sdff = dff[(dff.x > -25.0) & (dff.x < 30.0) & (dff.y > -60.0) & (dff.y < -40.0)]
print(sdff[['perimeter_mean', 'area_mean', 'smoothness_mean', 
            'concavity_mean', 'symmetry_mean']].describe())

下表显示了易于打印的统计表:

Statistical description of a malignant cluster

从基本事实来看,我们知道所有这些样本都是恶性的,但我们可以尝试确定一个规律。比率area_mean / perimeter_mean约为9.23,相对标准差与平均值相比非常小。这意味着这些样本代表了非常窄范围内的扩展肿瘤。而且concavity_meansymmetry_mean都大于整体数值。因此(没有科学合理分析的假设),我们可以得出结论,分配给这些簇的样本代表已经到达晚期的非常坏的肿瘤。

为了与良性样本进行比较,现在让我们考虑由 x > -1020 < y < 50 划定的区域,如下所示:

sdff = dff[(dff.x > -10.0) & (dff.y > 20.0) & (dff.y < 50.0)]
print(sdff[['perimeter_mean', 'area_mean', 'smoothness_mean',
            'concavity_mean', 'symmetry_mean']].describe())

结果如下图所示:

Statistical description of a benign cluster

这种情况下area_mean / perimeter_mean的比值约为4.89,但area_mean的标准差较大(确实其最大值约为410)。concavity_mean相对于前一个非常小(即使标准偏差大致相同),而symmetry_mean几乎相等。从这个简要的分析,我们可以推断出symmetry_mean不是判别特征,而concavity_mean小于或等于0.04的比area_mean / perimeter_mean小于5.42(考虑最大值)应该保证一个良性的结果。由于concavity_mean可以达到非常大的最大值(大于与恶性样本相关的最大值),因此有必要考虑其他特征,以决定其值是否应被视为警报。然而,我们可以得出结论,认为属于这些聚类的所有样本都是良性的,错误概率可以忽略不计。我想重复一遍,与其说这是真正的分析,不如说这是一种练习,在这种情况下,数据科学家的主要任务是收集能够支持结论的上下文信息。即使存在基本事实,这个验证过程也总是强制性的,因为潜在原因的复杂性会导致完全错误的陈述和规则。

轮廓分数

在不了解基本事实的情况下,评估聚类算法性能的最常见方法是轮廓得分。它提供了每个样本的索引和全局图形表示,显示了集群的内部一致性和分离程度。为了计算分数,我们需要引入两个辅助措施。第一个是样本的平均聚类内距离xI∈KjT7】假设基数为 |K j | = n(j) :

对于 K 均值,距离假设为欧氏距离,但没有具体的限制。当然,d()必须与聚类过程中使用的距离函数相同。

给定一个样本 x i ∈ K j ,让我们将最近的集群表示为 K c 。这样,我们还可以定义最小最近聚类距离(作为平均最近聚类距离):

通过这两个度量,我们可以定义 x i ∈ X 的轮廓分数:

分数s(∞(-1,1) 。当s(→-1时,表示b(<)T33】a(),因此样本 x i ∈ K j 比分配给 K j 的其他样本更接近最近的聚类 K c 。这种情况表明分配错误。反之,当s()→1b()>>a()时,那么样本 x i 与其邻居(属于同一个聚类)的距离比分配给最近聚类的任何其他点都要近得多。显然,这是一个最佳条件,也是微调算法时要使用的参考。然而,由于该指数不是全局的,引入侧影图是有帮助的,侧影图显示了每个样本获得的分数,按聚类分组并按降序排序。

让我们考虑 K={2,4,6,8} 乳腺癌威斯康星数据集的轮廓图(完整代码包含在存储库中):

Silhouette plots for the Breast Cancer Wisconsin dataset

第一个图显示了 K=2自然聚类。第一个轮廓非常清晰,表明平均簇间距离具有较大的方差。此外,一个集群的任务比另一个集群多得多(即使它不太清晰)。从数据集描述中,我们知道这两个类别是不平衡的(357 个良性对 212 个恶性),因此不对称是部分合理的。然而,一般来说,当数据集平衡时,良好的轮廓图的特征是具有接近 1.0 的圆形轮廓的同质聚类。事实上,当形状类似于长雪茄时,这意味着簇内距离非常接近它们的平均值(高凝聚力),并且相邻簇之间有明显的分离。对于 K=2 ,我们有合理的分数,因为第一个聚类达到了 0.6,而第二个聚类有一个对应于 0.8 左右的峰值。然而,在后一种情况下,大多数样品的特征是s()>0.75,而在前一种情况下,大约一半的样品低于 0.5。这种分析表明,越大的聚类越均匀,K-means 分配样本越容易(即在测度方面,xI∈K2的方差越小,在高维空间中,代表 K 2 的球比代表 K 1 的球更均匀)。

其他图显示了类似的情况,因为已经检测到一个非常紧密的集群和一些尖锐的集群。这意味着有一个非常一致的宽度差异。然而,增加 K ,我们获得稍微更均匀的聚类,因为分配的样本数量趋于相似。具有**()>0.75的非常圆的(几乎是矩形的)聚类的存在证实了数据集包含至少一组非常内聚的样本,其相对于分配给其他聚类的任何其他点的距离非常接近。我们知道恶性类(即使它的基数较大)更紧凑,而良性类则分布在一个宽得多的子空间上;因此,我们可以假设对于所有的 K ,最圆的聚类由恶性样本组成,并且所有其他的可以根据它们的锐度来区分。例如,对于 K=8 ,第三个聚类很可能对应于第一个图中第二个聚类的中心部分,而较小的聚类包含属于良性子集的孤立区域的样本。

*如果不知道地面真相,就要同时考虑 K=2K=8 (甚至更大)。事实上,在第一种情况下,我们可能会丢失许多细粒度的信息,但我们正在确定一个强大的细分(假设由于问题的性质,一个集群不是非常有凝聚力)。另一方面,在 K > 8 的情况下,集群明显更小,具有适度更高的凝聚力,并且它们代表具有一些共同特征的子群。正如我们在上一节中所讨论的,最终的选择取决于许多因素,这些工具只能提供一个大致的指示。此外,当聚类是非凸的或者它们的方差不是均匀分布在所有特征中时,K-means 将总是产生次优的性能,因为得到的聚类将包含大的空白空间。如果没有特定的方向,最佳聚类数与包含均匀(宽度大致相同)圆形图的图相关联。如果形状对于任何 K 值都保持尖锐,这意味着几何图形与对称度量不完全兼容(例如,簇非常拉伸),应该考虑其他方法。

完整性分数

这项措施(以及从现在开始讨论的所有其他措施)是基于对基本事实的了解。在介绍索引之前,定义一些常见的值是有帮助的。如果我们用 Y true 表示包含真实赋值的集合,用 Y pred 表示预测集合(都包含 M 值和 K 聚类),我们可以估计以下概率:

在前面的公式中, n 真/pred (k) 代表属于聚类 k ∈ K 的真/预测样本数。此时,我们可以计算出 Y trueY pred 的熵:

考虑到熵的定义,H()通过均匀分布最大化,而均匀分布又对应于每个分配的最大不确定性。出于我们的目的,也有必要引入条件熵(在已知另一个分布的情况下,表示该分布的不确定性)YtrueT5】给定YpredT9】反过来:**

函数 n(i,j) 在第一种情况下表示分配给KjT7】的真标签 i 的样本数,在第二种情况下表示分配给 K i 的真标签 j 的样本数。

完整性分数定义为:

很容易理解当H(Ypred| Ytrue)→0时,YtrueT9】的知识减少了预测的不确定性,因此, c → 1 。这相当于说所有具有相同真实标签的样本都被分配到同一个聚类。反之,当H(Ypred| Ytrue**)→H(Ypred)时,则意味着地面真实不提供任何降低预测不确定性的信息, c → 0

当然,一个好的集群的特征是 c → 1 。在乳腺癌威斯康星数据集的情况下,完整性分数,使用 scikit-learn 函数completenss_score()(也适用于文本标签)和 K=2 (与地面真实相关的唯一配置)计算如下:

import pandas as pd

from sklearn.cluster import KMeans
from sklearn.metrics import completeness_score

km = KMeans(n_clusters=2, max_iter=1000, random_state=1000)
Y_pred = km.fit_predict(cdf)

df_km = pd.DataFrame(Y_pred, columns=['prediction'], index=cdf.index)
kmdff = pd.concat([dff, df_km], axis=1)

print('Completeness: {}'.format(completeness_score(kmdff['diagnosis'], kmdff['prediction'])))

上一个片段的输出如下:

Completeness: 0.5168089972809706

这个结果证实,对于 K=2 ,K-means 不能完美地分离聚类,因为正如我们所看到的,有一些恶性样本被错误地分配给包含绝大多数良性样本的聚类。然而,由于 c 不是非常小,我们可以确定两个类别的大部分样本已经被分配到不同的聚类。请读者使用其他方法(在第 3 章高级聚类中讨论)检查该值,并提供不同结果的简要说明。

同质性得分

同质性分数与前一个是互补的,它基于一个聚类必须只包含具有相同真实标签的样本的假设。其定义为:

类似于完备性得分,当 H(Y |Y pred ) → H(Y ) 时,意味着赋值对条件熵没有影响,因此聚类后不确定性没有减少(例如,每个聚类包含属于所有类的样本) h → 0 。相反,当H(Ytrue| Ypred**)→0h → 1 时,因为预测的知识减少了关于真实赋值的不确定性,并且聚类几乎只包含具有相同标签的样本。重要的是要记住,仅仅这个分数是不够的,因为它不能保证一个聚类包含所有具有相同真实标签的样本 x i ∈ X 。这就是为什么同质性分数总是与完整性分数一起评估。

对于威斯康星乳腺癌数据集和 K=2 ,我们获得以下结果:

from sklearn.metrics import homogeneity_score

print('Homogeneity: {}'.format(homogeneity_score(kmdff['diagnosis'], kmdff['prediction'])))

相应的输出如下:

Homogeneity: 0.42229071246999117

这个值(特别是对于 K=2 )证实了我们最初的分析。至少有一个聚类(良性样本占大多数的聚类)不是完全同质的,因为它包含属于两个类别的样本。然而,由于该值不是很接近 0 ,我们可以确定作业是部分正确的。考虑到这两个值, hc ,我们可以推断 K-means 的表现不是非常好(可能是因为非凸性),但同时,它能够正确地分离所有最近聚类距离高于特定阈值的样本。不言而喻,有了基础事实的知识,我们不能轻易接受 K-means,我们应该寻找另一种能够同时产生 hc → 1 的算法。

使用 V-测度在同质性和完整性之间进行权衡

熟悉监督学习的读者应该知道 F-score(或 F-measure)的概念,它是精度和召回率的调和平均值。在给定基本事实的情况下,在评估聚类结果时也可以采用同样的权衡。

事实上,在许多情况下,有一个单一的衡量标准来考虑同质性和完整性是有帮助的。使用 V 测量(或 V 评分)可以很容易地获得这样的结果,其定义为:

对于威斯康星乳腺癌数据集,V 度量如下:

from sklearn.metrics import v_measure_score

print('V-Score: {}'.format(v_measure_score(kmdff['diagnosis'], kmdff['prediction'])))

上一个片段的输出如下:

V-Score: 0.46479332792160793

不出所料,V-Score 是一个平均指标,在这种情况下,会受到较低同质性的负面影响。当然,这个指数并没有提供任何不同的信息,因此它只对综合单一值的完整性和同质性有帮助。然而,通过一些简单但繁琐的数学运算,就有可能证明 V 测度也是对称的(即V(Ypred| Vtrue)= V(Ytrue| Ypred));因此,给定两个独立的赋值 Y 1Y 2V(Y1| Y2)就是它们之间一致的一种度量。这样的场景并不是非常常见,因为其他措施可以达到更好的效果。然而,这样的分数可以用于例如检查两种算法(可能基于不同的策略)是否倾向于产生相同的分配或者它们是否不一致。在后一种情况下,即使地面真相未知,数据科学家也可以理解一种策略肯定不如另一种策略有效,并开始探索过程以找出最佳聚类算法。

调整后的互信息得分

该评分的主要目标是评估 Y trueY pred 之间的一致程度,而不考虑排列。这样的目标可以采用互信息 ( MI )的信息论概念来衡量;在我们的例子中,它被定义为:

功能与之前定义的相同。当 MI → 0n(i,j)**→ntrue(I)npred(j),其项分别与 p(i,j)ptrue(I)ppred(j)成正比。因此,这个条件相当于说 Y Y pred 在统计学上是独立的,不存在一致。另一方面,通过一些简单的操作,我们可以将 MI 重写为:

因此,作为H(Ypred| Ytrue)≤H(Ypred),当对地面真相的认识减少了对YpredT11】的不确定性时,那么T14】H(Ypred| YtrueT20】)→0 和就我们的目的而言,最好考虑一个规范化的版本(在 01 之间),该版本也是根据机会进行调整的(也就是说,考虑到真正的赋值是由于机会的可能性)。 AMI 评分,其完整推导非小事,超出本书范围,定义为:

Y Y pred 完全一致(也存在排列)时,该值等于 0 ,等于 1 。对于威斯康星乳腺癌数据集和 K=2 ,我们获得以下结果:

from sklearn.metrics import adjusted_mutual_info_score

print('Adj. Mutual info: {}'.format(adjusted_mutual_info_score(kmdff['diagnosis'], kmdff['prediction'])))

输出如下:

Adj. Mutual info: 0.42151741598216214

该协议是温和的,与其他措施兼容。假设存在排列和机会分配的可能性, Y trueY pred 共享中等水平的信息,因为正如我们已经讨论过的,K-means 能够正确分配重叠概率可以忽略的所有样本,同时它倾向于考虑两个聚类之间边界上的良性许多恶性样本(相反,它不会对良性样本进行错误分配)。在没有任何进一步指示的情况下,该索引还建议检查可以管理非凸聚类的其他聚类算法,因为共享信息的缺乏主要是由于无法使用标准球捕捉复杂的几何形状(特别是在重叠更显著的子空间中)。

调整后的兰德分数

调整后的兰德分数是真实标签分布和预测分布之间差异的度量。为了计算它,有必要定义如下数量:

  • a :代表具有相同真实标签 (y i 、y j )的样本对数量( x i ,xjT7】):yI= yjT17】并分配到同一聚类KcT21】**
  • b :代表不同真标签 (y i 、y j )的样本对数量(x*T5】I、xj*):yI≠yjT21】并分配到不同的聚类 K

*如果有 M 个值,则二进制组合的总数是使用具有 k=2 的二项式系数获得的,因此,差异的初始度量是:

显然,该值可以由 ab 控制。在这两种情况下,较高的分数表明作业符合基本事实。然而, ab 都有可能被随机分配所偏向。这就是为什么引入了调整后的兰德分数。更新后的公式为:

该值在 -11 之间。当 R A → -1 时,A 和 b 都很小,绝大多数作业都是错的。另一方面,当 R A → 1 时,预测分布非常接近地面真相。对于威斯康星乳腺癌数据集和 K=2 ,我们获得以下信息:

from sklearn.metrics import adjusted_rand_score

print('Adj. Rand score: {}'.format(adjusted_rand_score(kmdff['diagnosis'], kmdff['prediction'])))

上一个片段的输出如下:

Adj. Rand index: 0.49142453622455523

该结果优于其他指标,因为该值大于 -1 (负极端)。它证实了分布之间的差异不是很明显,这主要是由于样本子集有限。这个分数非常可靠,也可以用作评估聚类算法性能的单一指标。接近 0.5 的值证实了 K-means 不太可能是最优解,但与此同时,数据集具有几乎可以被对称球完全捕获的几何形状,除了一些重叠概率很高的非凸区域。

应急矩阵

一个非常简单而强大的工具是权变矩阵 C m ,它可以在地面真相已知的情况下显示聚类算法的性能。如果有 m 类,cm∈ℜm×m并且每个元素 C m (i,j) 代表已经分配给聚类 j 的带有 Y true = i 的样本数量。因此,一个完美的列联矩阵是对角的,而所有其他单元中元素的存在表明了一个聚类错误。

在我们的案例中,我们获得了以下信息:

from sklearn.metrics.cluster import contingency_matrix

cm = contingency_matrix(kmdff['diagnosis'].apply(lambda x: 0 if x == 'B' else 1), kmdff['prediction'])

前面片段的输出可以被可视化为热图(变量cm是一个(2 × 2)矩阵):

Graphical representation of the contingency matrix

这个结果表明,几乎所有良性样本都被正确聚类,而中等百分比的恶性样本被错误地分配到第一个聚类。我们已经确认使用这一其他指标,但是,类似于分类任务中的混淆矩阵,应急矩阵允许立即可视化哪些类最难分离,帮助数据科学家寻找更有效的解决方案。

k 近邻

K-最近邻 ( K NN)是属于一个叫做基于实例的学习的类别的方法。在这种情况下,没有参数化模型,而是为了加快特定查询的速度而对样本进行了重新排列。在最简单的情况下(也称为蛮力搜索),假设我们有一个数据集 X 包含 M 个样本xI∈ℜnt15】。给定距离函数 d(x i ,x j ) ,可以将试样 x i 的半径邻域定义为:

集合 ν(x i ) 是一个以xIT7】为中心的球,包括所有距离小于或等于 R 的样品。或者,也可以只计算最上面的 k 最近邻,也就是距离xIT17】更近的 k 样本(一般来说,这个集合是ν(xI**)的子集,但是 k 很大的时候也会出现相反的情况)。这个过程很简单,但不幸的是,从计算的角度来看太昂贵了。事实上,对于每个查询,都需要计算M2**N维距离(即假设每距离 N 次运算,复杂度为 O(NM 2 ) ),这一条件让蛮力方法遭受了维度的诅咒。比如N = 2**M = 1,000 ,复杂度为 O(2 10 6 ) ,但是 N=1,000M=10,000 就变成了 O(10 11 ) 。例如,如果每个操作需要 1 纳秒,那么查询将需要 100 秒,这在许多实际情况下超出了可容忍的限度。此外,对于 64 位浮点值,成对距离矩阵每次计算大约需要 764 兆字节,考虑到任务的性质,这也是一个过高的要求。**

由于这些原因,KNN 的具体实现仅在 M 非常小时使用强力搜索,并且在所有其他情况下依赖稍微复杂的结构。第一种替代方法基于 kd 树,这是二叉树到多维数据集的自然扩展。

在下图中,部分 kd 树由 3 维向量组成:

Example of kd-tree with 3-dimensional vectors

kd 树的构造非常简单。给定一个根样本(一个 1 ,一个 2 ,...,a N ,考虑第一个特征进行第一次拆分操作,使左分支包含(b1T21】a1,...、等)和右边的一个(c1T22】a1,...,等等)。该过程继续第二个特征、第三个特征,以此类推,直到第一个特征,以此类推,直到到达叶节点(分配给叶的样本数是一个需要调整的超参数。在 scikit-learn 中,该参数称为leaf_size,默认值为 30 个样本)。

当维数 N 不是特别大的时候,计算复杂度就变成了 O(N log M) ,比蛮力搜索好了不少。比如N = 1000**M = 10000,计算复杂度就变成了O(4000)<<O(1011)。遗憾的是,当 N 大的时候,一个 kd-tree 查询就变成了 O(NM) ,所以,考虑到前面的例子, O(10 7 ) ,比蛮力搜索要好,但对于实时查询来说,有时候还是太贵了。

KNN 常用的第二种数据结构是球树。在这种情况下,根节点由一个 R 0 球表示,该球被精确定义为样本的邻域:

选择第一个球以捕获所有样本。此时,其他更小的球嵌套在βR0T3】中,确保每个样品始终属于一个球。在下图中,有一个简单球树的示意图:

Example of a simple ball-tree

由于每个球完全由其中心 c j 决定,所以使用测试样本xIT7】的查询需要计算距离 d(x i ,c j ) 。因此,从底部(最小的球所在的位置)开始,执行完整的扫描。如果没有球包含样本,级别会增加,直到到达根节点(请记住,样本可以属于单个球)。由于球的属性(也就是说,给定中心和半径,可以用一次距离计算来检查样本的成员资格),计算复杂度现在总是 O(N log M) 。一旦确定了正确的球,样本的邻居 x i 需要计算有限数量的成对距离(该值小于叶子大小,因此与数据集的维度相比,通常可以忽略不计)。

当然,这些结构是在培训阶段构建的,在生产阶段不会修改。这意味着仔细选择最小半径或分配给叶节点的样本数。事实上,由于查询通常需要多个邻居 k ,因此只有当 k < |ν(x i )| 时,才能达到最优性。换句话说,我们希望在包含xIT9】的同一子结构中找到所有邻居。每当k>|ν(xI**)|时,算法也必须检查相邻结构并合并结果。当然,当叶子尺寸太大时(与样本总数 M 相比),这些树的优势就消失了,因为需要计算太多的成对距离才能回答一个查询。考虑到软件的生产使用,必须做出正确的叶片尺寸选择。

例如,如果推荐系统需要一个具有 100 个邻居的初始查询和几个(例如,5 个)具有 10 个邻居的后续查询,那么等于 10 的叶子大小将优化细化阶段,但是它对第一个查询有负面影响。相反,选择等于 100 的叶大小将减慢所有 10 个邻居的查询。折衷可能是 25,这减少了第一个查询的负担,但对细化查询的成对距离的计算有适度的负面影响。

我们现在可以基于 Olivetti 人脸数据集(由 scikit-learn 直接提供)分析一个简短的示例。它由 400 幅 64 × 64 灰度图像组成,代表不同人的肖像。让我们从如下加载数据集开始:

from sklearn.datasets import fetch_olivetti_faces

faces = fetch_olivetti_faces()
X = faces['data']

变量X包含数据集的展平版本(400 个 4,096 维实例已经在 0 和 1 之间规范化)。此时,我们可以训练一个NearestNeighbor模型,假设默认查询有 10 个样本(参数n_neighbors)和等于 20 的半径(参数radius)。我们保留默认的leaf_size (30)并使用p=2(欧几里德距离)明确设置闵可夫斯基度量。该算法基于球树,但我邀请读者测试不同的度量和 kd 树。我们现在可以创建一个NearestNeighbors实例并继续训练模型:

from sklearn.neighbors import NearestNeighbors

knn = NearestNeighbors(n_neighbors=10, metric='minkowski', p=2, radius=20.0, algorithm='ball_tree')
knn.fit(X)

一旦模型被训练,使用一个有噪声的测试面来寻找 10 个最近的邻居,如下所示:

import numpy as np

i = 20
test_face = X[i] + np.random.normal(0.0, 0.1, size=(X[0].shape[0]))

测试面绘制在下面的截图中:

Noisy test face

可以使用仅提供测试样本的方法kneighbors()来执行具有默认邻居数量的查询(在邻居数量不同的情况下,必须调用函数来提供参数n_neighbors)。该函数,如果参数return_distance=True,返回一个包含distances, neighbors的元组,如下所示:

distances, neighbors = knn.kneighbors(test_face.reshape(1, -1))

查询结果如下图所示:

Nearest neighbors of the test sample with their relative distances

第一个样本总是测试样本(在这种情况下,它被去噪,因此它的距离不是零)。正如可以看到的,即使距离是累积函数,第二个和第四个样本指的是同一个人,而其他样本共享不同的解剖元素。当然,欧几里德距离不是衡量图像之间差异的最合适方式,但这个例子在一定程度上证实了,当图片相当相似时,全局距离也可以为我们提供一个有价值的工具,可以用来寻找相似的样本。

现在让我们使用设置radius=100的方法执行半径查询,如下所示:

import numpy as np

distances, neighbors = knn.radius_neighbors(test_face.reshape(1, -1), radius=100.0)
sd, sd_arg = np.sort(distances[0]), np.argsort(distances[0])

包含前 20 个邻居的结果如下图所示:

First 50 neighbors using a radius query

有趣的是,注意到距离不会很快发散(第二个样本有d=8.91和第五个d=10.26)。这主要是由于两个因素:第一个因素是样本之间的全局相似性(在几何元素和色调方面),第二个因素很可能与欧氏距离对 4,096 维向量的影响有关。正如在讨论聚类基础时所解释的,高维样本可能缺乏可区分性(特别是当 p > > 1 时)。在这种情况下,图片不同部分的平均效果会产生与分类系统完全不兼容的结果。特别是,深度学习模型倾向于通过使用卷积网络来避免这个陷阱,卷积网络可以学习检测不同级别的特定特征。我建议用不同的度量重复这个例子,并观察 p 对 radius 查询的样本所显示的实际差异的影响。

矢量量化

矢量量化 ( VQ )是一种利用无监督学习来对样本xI∈ℜnT9】(为简单起见,我们假设多维样本被展平)或整个数据集 X 执行有损压缩的方法。主要思想是找到一个带有多个条目的码本QCC<T23】N并将每个元素与一个条目 q i ∈ Q 相关联。在单个样本的情况下,每个条目将表示一组或多组特征(例如,它可以是平均值),因此,该过程可以描述为变换 T ,其一般表示为:

码本定义为 Q = (q 1 ,q 2 ,...,q C ) 。因此,给定由一组特征集合(例如,一组两个连续元素)组成的合成数据集,VQ 关联单个码本条目:

由于输入样本是使用固定值的组合来表示的,汇总整个组的,该过程被定义为量化。类似地,如果输入是数据集 X ,转换就像任何标准的聚类过程一样对样本组进行操作。主要区别在于目的:使用 VQ 以质心表示每个聚类,从而减少数据集的方差。这个过程是不可逆的。一旦执行转换,就不可能重建原始聚类(唯一可行的过程是基于从具有相同原始均值和协方差的分布中采样,但重建显然是近似的)。

让我们从展示一个非常简单的高斯数据集开始,如下所示:

import numpy as np

nb_samples = 1000
data = np.random.normal(0.0, 1.5, size=(nb_samples, 2))

n_vectors = 16
qv = np.random.normal(0.0, 1.5, size=(n_vectors, 2))

我们的目标是用 16 个向量来表示数据集。在下面的屏幕截图中,图表显示了初始配置的示例:

Initial configuration of the vectors for the VQ example

当我们使用随机数时,同一代码的后续执行会产生不同的初始配置。该过程迭代所有样本,选择最近的量化矢量,并将其距离减少固定量delta=0.05,如下所示:

import numpy as np

from scipy.spatial.distance import cdist

delta = 0.05
n_iterations = 1000

for i in range(n_iterations):
    for p in data:
        distances = cdist(qv, np.expand_dims(p, axis=0))
        qvi = np.argmin(distances)
        alpha = p - qv[qvi]
        qv[qvi] += (delta * alpha)

distances = cdist(data, qv)
Y_qv = np.argmin(distances, axis=1)

代替固定的 for 循环,也可以使用 while 循环来检查量化向量是否已经达到它们的稳定状态(比较在时间 tt+1 计算的向量的范数)。过程结束时的结果如下图所示:

Final configuration of the quantization vectors (left). Influence area of each quantization vector (right)

正如预期的那样,量化矢量已经达到最终配置,其中每个矢量代表数据集的一小部分(如右图所示)。此时,给定一个点,最近的向量将表示它。有趣的是,注意到全局方差没有受到影响,但是,选择任何子集,内部方差反而显著减少。向量的相对位置反映了数据集的密度,因为一个区域中更多的样本会吸引更多的向量。这样,通过构建距离矩阵,有可能获得粗略的密度估计(例如,当向量与其近邻的平均距离较高时,这意味着底层区域的密度较低)。我们将在第六章异常检测中更详细地讨论这个话题。

现在让我们考虑一个例子,其中一个样本代表一只浣熊的图片。由于过程可能非常长,第一步是加载示例 RGB 图像(由 SciPy 提供),并将其大小调整为 192 × 256,如下所示:

from scipy.misc import face
from skimage.transform import resize

picture = resize(face(gray=False), output_shape=(192, 256), mode='reflect')

原始图片(已经在[0,1]范围内归一化)如下图所示:

Sample RGB picture for VQ example

我们希望使用 2 × 2 正方形区域(由包含 2 × 2 × 3 特征的展平向量表示)计算的 24 个向量来执行 VQ。然而,我们将使用 K-means 算法来寻找质心,而不是从头开始执行这个过程。第一步是收集所有正方形区域,如下所示:

import numpy as np

square_fragment_size = 2
n_fragments = int(picture.shape[0] * picture.shape[1] / (square_fragment_size**2))

fragments = np.zeros(shape=(n_fragments, square_fragment_size**2 * picture.shape[2]))
idx = 0

for i in range(0, picture.shape[0], square_fragment_size):
    for j in range(0, picture.shape[1], square_fragment_size):
        fragments[idx] = picture[i:i + square_fragment_size, 
                                 j:j + square_fragment_size, :].flatten()
        idx += 1

此时,可以用 24 个量化向量执行 K 均值聚类,如下所示:

from sklearn.cluster import KMeans

n_qvectors = 24

km = KMeans(n_clusters=n_qvectors, random_state=1000)
km.fit(fragments)

qvs = km.predict(fragments)

在训练结束时,变量qvs将包含与每个正方形区域相关联的质心索引(可通过实例变量cluster_centers_获得)。

现在可以使用质心构建量化图像,如下所示:

import numpy as np

qv_picture = np.zeros(shape=(192, 256, 3))
idx = 0

for i in range(0, 192, square_fragment_size):
    for j in range(0, 256, square_fragment_size):
        qv_picture[i:i + square_fragment_size,
                   j:j + square_fragment_size, :] = \
            km.cluster_centers_[qvs[idx]].\
                reshape((square_fragment_size, square_fragment_size, 3))
        idx += 1

量化图像如下图所示:

Picture quantized with 24 vectors

结果显然是原始图像的有损压缩版本。每个组可以用指向码本中条目的索引(例如,在我们的例子中,它可以是 8 位整数)来表示(km.cluster_centers_)。因此,如果最初有 192 × 256 × 3 = 1,474,560 个 8 位值,量化后我们有 12,288 个 8 位索引(2 × 2 × 3 块的数量)加上 24 个 12 维量化矢量。为了了解 VQ 对图像的影响,为原始图像和处理后的图像绘制 RGB 直方图会很有帮助,如下图所示:

RGB histogram of the original image (top) and quantized version (bottom) For readers who are not familiar with histograms, we can briefly describe them as having a dataset X and a fixed number of bins. Each bin is assigned to a range (starting from min(X) and ending in max(X)) and each range (a, b) is associated with the number of samples such that a ≤ x < b. The resulting plot is proportional to an approximation of the actual probability distribution that generated X. In our case, on the x-axis, there are all possible values for each pixel per channel (8-bit), while the y-axis represents the estimated frequency (Nx / Total number of pixels).

可以看到,量化减少了信息量,但直方图倾向于再现原始直方图。增加量化向量的数量具有减少近似的效果,产生差异不太明显的直方图。对这个话题的完整分析超出了本书的范围;然而,我邀请读者用其他图像和不同数量的量化向量来测试这个过程。还可以将原始图像的(共)方差(或者,熵)与量化版本进行比较,并找到保留 80%方差的阈值。例如,仅考虑红色通道,用频率计数近似每个值(0 ÷ 255)的概率,我们得到以下结果:

import numpy as np

hist_original, _ = np.histogram(picture[:, :, 0].flatten() * 255.0, bins=256)
hist_q, _ = np.histogram(qv_picture[:, :, 0].flatten() * 255.0, bins=256)

p_original = hist_original / np.sum(hist_original)
H_original = -np.sum(p_original * np.log2(p_original + 1e-8))

p_q = hist_q / np.sum(hist_q)
H_q = -np.sum(p_q * np.log2(p_q + 1e-8))

print('Original entropy: {0:.3f} bits - Quantized entropy: {1:.3f} bits'.format(H_original, H_q))

上一个片段的输出如下:

Original entropy: 7.726 bits - Quantized entropy: 5.752 bits

由于信息量与熵成正比,我们现在已经确认,24 个量化向量(具有 2 × 2 个正方形块)能够解释大约 74%的红色通道的原始熵(即使三个通道不是独立的,也可以通过对三个熵求和来获得总熵的粗略近似值)。这种方法可以有效地用于寻找压缩强度和最终结果质量之间的折衷。

摘要

在这一章中,我们解释了聚类分析的基本概念,从相似性的概念和如何衡量它开始。我们讨论了 K-means 算法及其称为 K-means++的优化变体,并分析了乳腺癌威斯康星数据集。然后,我们讨论了最重要的评估指标(有或没有基本事实的知识),我们了解了哪些因素会影响绩效。接下来的两个主题是 KNN 和 VQ,前者是一种非常著名的算法,可用于在给定查询向量的情况下查找最相似的样本,后者是一种利用聚类算法来查找样本(例如,图像)或数据集的有损表示的技术。

在下一章中,我们将介绍一些最重要的高级聚类算法,展示它们如何轻松解决非凸问题。

问题

  1. 如果两个样本的 Minkowski 距离( p=5 )等于 10,你能说说它们的曼哈顿距离吗?

  2. 对 K 均值收敛速度产生负面影响的主要因素是数据集的维数。这是正确的吗?

  3. 能够对 K 均值的性能产生积极影响的最重要因素之一是聚类的凸性。这是正确的吗?

  4. 聚类应用程序的同质性得分等于 0.99。这是什么意思?

  5. 调整后的兰德分数等于-0.5 是什么意思?

  6. 考虑到前面的问题,不同数量的簇能产生更好的分数吗?

  7. 基于 KNN 的应用程序平均每分钟需要 100 个基于 5-NN 的查询。每分钟执行 2 个 50-NN 查询(每个查询需要 4 秒钟,叶大小=25),之后立即执行 2 秒钟的阻塞任务。假设没有其他延迟,一片叶子大小=50,每分钟可以执行多少次基本查询?

  8. 球树结构不适合管理高维数据,因为它受到维度的诅咒。这是正确的吗?

  9. 数据集是通过从 3 个二维高斯分布中采样 1000 个样本获得的: N([-1.0,0.0],diag[0.8,0.2])N([0.0,5.0],diag[0.1,0.1]) ,以及 N([-0.8,0.0]diag[0.6,0.3]) 。最有可能的集群数量是多少?

  10. 能否采用 VQ 压缩一个文本文件(例如,构建一个在【0.0,1.0】范围内统一映射的 10000 个单词的字典,将文本拆分为标记,并将其转换为一系列浮点数)?

进一步阅读

  • 关于高维空间中距离度量的 的惊人行为阿加尔瓦尔 c .欣内堡 a .凯米 d . a .ICDT,2001
  • K**-意指++: 小心播种的优势Arthur dVassilvitskii s .第十八届年度 ACM-SIAM 离散算法研讨会论文集,2007
    ** 使用 t-SNE范德马滕辛顿机器学习研究杂志 9 ,2008* 两个线性不可分集合的鲁棒线性规划判别贝内特·k·p .莽草酸优化方法和软件 1 ,1992* 1995 年 7 月至 8 月,通过线性规划进行乳腺癌诊断和预后、漫画有限公司西大街沃尔伯格西大街运筹学,43(4),第 570-577 页* V-Measure:一种基于条件熵的外部聚类评估方法罗森伯格 A.赫希伯格 J.2007 年自然语言处理和计算自然语言学习中的经验方法联合会议论文集,2007
    *

三、高级聚类

在这一章中,我们将继续探索更复杂的聚类算法,这些算法可以用于非凸任务(也就是说,例如,K-means 无法同时获得内聚性和分离性)。一个经典的例子由交错的几何图形表示)。我们还将展示如何将基于密度的算法应用于复杂数据集,以及如何根据所需结果正确选择超参数并评估性能。这样,数据科学家可以准备好面对不同类型的问题,排除价值较低的解决方案,只关注最有希望的解决方案。

特别是,我们将讨论以下主题:

  • 光谱聚类
  • 均值漂移
  • 带噪声应用的基于密度的空间聚类 ( 数据库扫描)
  • 其他评估指标:卡林斯基-哈拉巴斯指数和集群不稳定性
  • k-水母
  • 在线聚类(小批量 K-means 和使用层次结构的平衡迭代约简和聚类 ( BIRCH ))

技术要求

本章中的代码要求:

数据集可以通过 UCI 获得。CSV 文件可以从https://archive.ics.uci.edu/ml/datasets/Absenteeism+at+work下载,除了添加加载阶段会出现的列名外,不需要任何预处理。

Github 存储库中提供了一些示例:

https://github . com/PacktPublishing/HandsOn-Unsupervised-Learning-with-Python/chapter 03

光谱聚类

能够管理非凸聚类的最常见算法家族之一是谱聚类。其主要思想是将数据集 X 投影到一个空间上,在这个空间中,集群可以被超球体捕获(例如,使用 K-means)。这个结果可以通过不同的方式来实现,但是,由于算法的目标是去除一般形状区域的凹陷,所以第一步总是将 X 表示为图形 G={V,E},,其中顶点 V ≡ X 和加权边缘表示每对样本的接近度 x ix j ∈ X 生成的图可以是完整的(完全连通的),也可以只有一些样本对之间的边(也就是说,不存在的权重的权重设置为零)。在下图中,有一个局部图的示例:

Example of a graph: Point x0 is the only one that is connected to x1

有两种主要策略可以用来确定权重wijT3:【KNN】和径向基函数 ( 径向基函数)。第一个是基于上一章讨论的相同算法。考虑到邻居的数量 k ,数据集被表示为球树或 kd 树,并且对于每个样本 x i ,计算集合 kNN(x i ) 。此时,给定另一个样本 x j ,重量计算如下:

在这种情况下,该图不包含任何关于实际距离的信息,因此,考虑到在 KNN 使用的相同距离函数d(),最好将wT5表示为:

这种方法简单而且相当可靠,但是得到的图并不是完全连通的。这种情况可以通过使用如下定义的径向基函数来轻松实现:

这样,所有夫妇都会根据他们的距离自动加权。由于径向基函数是高斯曲线,当xI= xjT7】时,它等于 1 ,并与平方距离 d(x i ,x j ) 成比例减小(表示为差值的范数)。参数 γ 决定半钟曲线的振幅(一般默认值为 γ=1 )。当 γ < 1 时,振幅增大,反之亦然。因此, γ < 1 表示对距离的敏感度较低,而 γ > 1 表示径向基函数下降较快,如下图截图所示:

Bidimensional RBFs as functions of the distance between x and 0 computed for γ = 0.1, 1.0, and 5.0

γ = 0.1x = 1 (相对于 0.0)加权约 0.9。对于 γ = 1.0 该值约为 0.5,对于γ = 5.0 该值几乎为零。因此,在调整谱聚类模型时,考虑 γ 的不同值并选择产生最佳性能的值(例如,使用第 2 章聚类基础中讨论的标准进行评估)是极其重要的。一旦创建了图形,就可以使用对称的亲和矩阵 W = {w ij } 来表示它。对于 KNN 来说 W 一般比较稀疏,可以用专门的库进行高效的存储和操作。相反,对于 RBF,它总是密集的,如果 X ∈ ℜ N × M ,它需要存储 N 2 值。

不难证明,我们到目前为止所分析的过程相当于将 X 分割成多个内聚区域。事实上,让我们考虑一个例子,一个图 G 与 KNN 获得的亲和矩阵。连接的组件 C i 是一个子图,其中每对顶点 x axb∈CI通过属于CT21】I的顶点路径连接,并且不存在连接 C i 的任何顶点的边换句话说,连接的组件是内聚子集 C i G ,其代表聚类选择的最佳候选。在下图中,有一个从图中提取的连接组件的示例:

Example of a connected component extracted from a graph

在原空间中,点 x 0x 2x 3 连接到 x nx m ,以及x****T25】q这可以表示非常简单的非凸几何形状,例如半月形。事实上,在这种情况下,凸性假设对于最佳分离不再是必要的,因为正如我们将要看到的,这些分量被提取并投影到具有平坦几何形状的子空间上(通过诸如 K-means 的算法容易管理)。

当使用 KNN 时,这个过程更加明显,但是,一般来说,当区域间距离(例如,两个最近点之间的距离)与平均区域内距离相当时,我们可以说两个区域可以合并。解决这个问题最常见的方法之一是由石和马利克提出的(在规范化切割和图像分割中,石和马利克,IEEE 模式分析和机器智能学报,第 22 卷, 08/2000),它被称为规范化切割。整个证明超出了本书的范围,但我们可以讨论主要概念。给定一个图,可以构建规范化的拉普拉斯图,定义为:

对角矩阵 D 称为度矩阵,每个元素 d i i 是对应行的权重之和。有可能证明以下陈述:

  • 在特征分解 L (考虑非正规图拉普拉斯 L u = D - W 并求解方程 L u v = λDv 后,很容易计算出特征值和特征向量,零特征值总是以多重性 p 存在。
  • 如果 G 是无向图(所以 w ij ≥ 0 ∀ i,j ,则连接的分量数等于 p (零特征值的重数)。
  • 如果 A N θA 的可数子集(即 X 是可数子集,因为样本数总是有限的),则给定 θ i,向量v∈ℜnt14】被称为指示向量,用于θif 例如,如果我们有两个向量 a = (1,0)b = (0,0) (所以,θ= { a,b} )并且我们考虑 A = {(1,n)其中 n∈1,10】},则向量 v = (1,0) 是一个指示向量,因为 a ∈ A**
  • L 的第一 p 特征向量(对应于零特征值)是每个连通分量 C 1 ,C 2 所跨越的特征空间的指示向量,...,C p

因此,如果数据集由 M 样本 x i ∈ ℜ N 组成,并且图 G 与亲和矩阵wm×mt13】相关联,Shi 和 Malik 提议构建包含第一个 p 特征向量的矩阵b∈ℜm×pt17】事实上,每一行都代表了样本在 p 维子空间上的投影,其中非凸性由可以包含在规则球中的子区域表示。**

现在,让我们应用光谱聚类来分离由以下片段生成的二维正弦数据集:

import numpy as np

nb_samples = 2000

X0 = np.expand_dims(np.linspace(-2 * np.pi, 2 * np.pi, nb_samples), axis=1)
Y0 = -2.0 - np.cos(2.0 * X0) + np.random.uniform(0.0, 2.0, size=(nb_samples, 1))

X1 = np.expand_dims(np.linspace(-2 * np.pi, 2 * np.pi, nb_samples), axis=1)
Y1 = 2.0 - np.cos(2.0 * X0) + np.random.uniform(0.0, 2.0, size=(nb_samples, 1))

data_0 = np.concatenate([X0, Y0], axis=1)
data_1 = np.concatenate([X1, Y1], axis=1)
data = np.concatenate([data_0, data_1], axis=0)

数据集如下图所示:

A sinusoidal dataset for the spectral clustering example

我们没有具体说明任何基本事实;然而,目标是分离两个正弦曲线(它们是非凸的)。很容易检查捕捉正弦曲线的球是否还包括属于其他正弦子集的许多样本。为了显示纯 K-means 和谱聚类(scikit-learn 实现了 Shi-Malik 算法,然后是 K-means 聚类)之间的区别,我们将训练两个模型,为后者使用带有 γ = 2.0 ( gamma参数)的径向基函数(affinity参数)。当然,我邀请读者也来测试其他价值观和 KNN 亲和力。基于径向基函数的解决方案如下所示:

from sklearn.cluster import SpectralClustering, KMeans

km = KMeans(n_clusters=2, random_state=1000)
sc = SpectralClustering(n_clusters=2, affinity='rbf', gamma=2.0, random_state=1000)

Y_pred_km = km.fit_predict(data)
Y_pred_sc = sc.fit_predict(data)

结果显示在下面的截图中:

Original dataset (left). Spectral clustering result (center). K-means result (right)

如您所见,K-means 沿着 x 轴用两个球分割数据集,而谱聚类成功地正确分离了两个正弦曲线。每当聚类的数量和 X 的维数都不太大时(在这种情况下,拉普拉斯算子的特征分解会变得非常昂贵),该算法就非常强大。此外,由于该算法基于图形切割过程,因此它非常适合集群数量为偶数的情况。

均值漂移

让我们考虑让数据集x∈ℜm×n(m n)维样本从多元数据生成过程 p 数据 中提取。应用于聚类问题的均值漂移算法的目标是找到 p 数据 最大的区域,并将周围子区域中包含的样本关联到同一聚类。由于 p 数据 是一个 P 概率密度函数 ( PDF ),将其表示为以均值和方差等参数的小子集为特征的正则 PDF(例如高斯)的和是合理的。这样,样本就可以假设是由概率最高的 PDF 生成的。我们也将在第 5 章软聚类和高斯混合模型、第 6 章异常检测中讨论这个过程。出于我们的目的,将问题重构为更新平均向量(质心)位置直到它们达到最大值的迭代过程是有帮助的。当质心到达它们的最终位置时,使用标准邻域函数将样本分配给每个聚类。

这个算法的第一步是确定一个合适的方法来近似 p 数据。经典方法(将在第 6 章异常检测中讨论)基于 Parzen 窗口的使用。目前来说,帕尔岑窗是一个非负核函数f()的特征是一个名为带宽的参数(更多细节,请查看《数理统计年鉴》帕尔岑 e .关于概率密度函数和模式的估计的原始论文)。33、 1962)。顾名思义,此类参数的作用是加宽或限制 Parzen 窗口接近其最大值的区域。考虑与高斯分布的类比,带宽具有与方差相同的作用。因此,小带宽将产生在平均值附近非常峰值的函数,而较大的值与更平坦的函数相关。不难理解,在这种特殊情况下,集群的数量隐含地由带宽决定,反之亦然。因此,大多数实现(如 scikit-learn)只使用一个参数,并计算另一个参数。考虑到这种算法被设计用于概率分布,自然的选择是指定期望的带宽或者让实现检测最优的带宽。这个过程可能看起来比强加特定数量的集群更复杂,但是,在许多实际情况下,特别是当基本事实至少部分已知时,测试不同带宽的结果更容易。

均值漂移最常见的选择是用 n 个平核的和( n 为质心数)来近似数据生成过程:

因此,收敛后,每个样本由最近的质心表示。不幸的是,这种近似导致了不可能代表真实过程的分段函数。因此,最好使用基于相同底层内核的平滑 Parzen 窗口K():

K()是平方距离(例如标准球)和带宽 h 的函数。有许多可能的候选函数可以使用,但是,当然,最明显的是高斯核(RBF),其中 h 2 扮演方差的角色。得到的近似现在非常平滑,其中 n 个峰值对应于质心(即平均值)。一旦定义了函数,就可以计算质心 x 1 ,x 2 的最佳位置,...,x n

给定一个质心和一个邻域函数(为简单起见,我们假设使用半径为 hK(x) ≠ 0 ∀ x ∈ B r 的标准球bhT3】,相应的均值偏移向量定义为:

可以看到,m()是用K()加权的所有邻域样本的平均值。显然,由于K()是对称的,并且与距离有关,当 x i 达到实际平均值时,m()将趋于稳定。带宽的作用是限制 x i 周围的区域。现在应该更清楚了,小值迫使算法引入更多的质心,以便将所有样本分配给一个聚类。相反,大带宽可能导致单个集群的最终配置。迭代过程从初始质心猜测 x 1 (0) ,x 2 (0) 开始,...,x n (0) 并用以下规则修正向量:

前面的公式很简单;在每一步,质心都移动(移动)到更靠近m()的位置。这样,由于m**()与相对于 x i 计算的邻域密度成正比,当 x i 到达概率最大的位置时,m()→m最终 不再需要更新。当然,收敛速度受到样本数量的强烈影响。对于非常大的数据集,该过程会变得非常慢,因为每个均值偏移向量的计算需要邻域的预先计算。另一方面,当聚类标准由数据密度定义时,该算法非常有用。

例如,现在让我们考虑一个合成数据集,其二维样本由三个具有对角协方差矩阵的多元高斯生成,如下所示:

import numpy as np

nb_samples = 500

data_1 = np.random.multivariate_normal([-2.0, 0.0], np.diag([1.0, 0.5]), size=(nb_samples,))
data_2 = np.random.multivariate_normal([0.0, 2.0], np.diag([1.5, 1.5]), size=(nb_samples,))
data_3 = np.random.multivariate_normal([2.0, 0.0], np.diag([0.5, 1.0]), size=(nb_samples,))

data = np.concatenate([data_1, data_2, data_3], axis=0)

数据集如下图所示:

Sample dataset for the mean shift algorithm example

在这种情况下,我们知道基本事实,但我们想测试不同的带宽并比较结果。由于生成的高斯区域彼此非常接近,一些外部区域可以被识别为集群。为了集中研究最优参数,我们可以观察到平均方差(考虑不对称)为 1,因此,我们可以考虑值h = 0.91.01.21.5。此时,我们可以实例化 scikit-learn 类MeanShift,通过参数bandwidth传递h值,如下所示:

from sklearn.cluster import MeanShift

mss = []
Y_preds = []
bandwidths = [0.9, 1.0, 1.2, 1.5]

for b in bandwidths:
   ms = MeanShift(bandwidth=b)
    Y_preds.append(ms.fit_predict(data))
    mss.append(ms)

在密度分析之后,训练过程自动选择质心的数量和初始位置。不幸的是,这个数字通常比最后一个数字大(因为局部密度差异);因此,该算法将优化所有质心,但是在完成之前,执行合并过程以消除所有那些与其他质心过于接近的质心(即,重复的质心)。Scikit-learn 提供了参数bin_seeding,通过根据带宽对样本空间进行离散化(宁滨),可以加快研究速度。这样,就有可能在合理损失精度的情况下减少候选项的数量。

下图显示了四个培训过程结束时的结果:

Mean shift clustering results for different bandwidths

如您所见,带宽的微小差异会导致不同数量的集群。在我们的例子中,最佳值是h=1.2,这产生了一个结果,其中确定了三个不同的区域(加上一个包含潜在异常值的额外聚类)。最大簇的质心大致对应于实际平均值,但簇的形状不像任何高斯分布。这是一个缺点,可以通过使用其他方法解决(在第 5 章软聚类和高斯混合模型中讨论)。实际上,均值漂移适用于局部邻域,并且 p 数据 不被认为属于特定分布。因此,最终结果是将数据集非常精确地分割成高度密集的区域(请注意,最大分离不再是要求),这也可以从多个标准分布的叠加中得出。没有任何预先假设,我们不能期望结果非常规则,但是,将这个算法与 VQ 进行比较,很容易注意到分配是基于找到每个密集斑点的最佳代表的想法。为此,由概率较低的高斯 N(μ,σ)生成的一些点被分配给质心比 μ 更具代表性的不同聚类。

DBSCAN

DBSCAN 是另一种基于数据集密度估计的聚类算法。然而,与均值漂移相反,没有直接引用数据生成过程。在这种情况下,事实上,过程从一般假设 X 由高密度区域(斑点)和低密度区域(斑点)分开开始,通过自下而上的分析建立样本之间的关系。因此,DBSCAN 不仅需要最大分离约束,而且为了确定集群的边界,它还强制执行这样的条件。此外,该算法不允许指定期望的聚类数,这是 X 结构的结果,但是,类似于均值漂移,可以控制过程的粒度。

具体来说,DBSCAN 基于两个基本参数: ε ,其代表以样品 x i 为中心的球 B ε (x i )n min , 这是必须包含在Bε(xI**)中的最小样本数,以便将 x i 视为一个核心点(即一个可以作为一个聚类的实际成员的点)。 形式上,给定一个对一个集合中包含的样本数进行计数的函数N(),在以下情况下,样本 x i ∈ X 称为核心点:

所有点xj∈Bε(xI**)定义为x i 直接密度可达。这样的条件是点之间最强的关系,因为它们都属于以 x i 为中心的同一个球,并且Bε(xI)中包含的样本总数足够大,可以将邻域视为一个密集的子区域。而且,如果有顺序xI→xI+1→...→ x j 其中 x i+1 是从 x i 直接密度可达的(对于所有顺序配对),xj被定义为密度-从 x i 可达的。这个概念非常直观,可以通过考虑下图立即理解:

The point x2 is density-reachable from x0 if nmin = 4

如果我们将最小样本数设为 4,则x0T3、x1T7、x2T11】为核心点,x1T15】为从x0T19】直接密度可达,x2T23】为直接密度可达因此, x 2 是从 x 0 密度可达的。换句话说,这意味着可以定义一系列重叠的密集球(N(≥Nmin)从 x 0 开始,到x2T47】结束。这个概念可以通过增加一个进一步的定义扩展到属于球的所有其他点:给定一个点 x k ,点xIT55】和xjT59】是密度连接的,如果两者xIT65】和 x**************

理解这样的条件弱于密度可达性是很重要的,因为为了保证一个密集链,需要考虑第三个点,代表两个密集子区域之间的连接器。事实上,可以有两个密度连接点 ab ,其中 a 不是从 b 密度可达的(反之亦然)。只要满足最小数量的样本条件,只在一个方向上移动,就会出现这种情况(也就是说,属于一个球的样本不是均匀分布的,而是倾向于聚集在一个小的超体积中)。

因此,例如,如果 N(a) > > n min 和 N(a 1 ) < < N(a) ,则过渡 a → a 1 可以允许构建一个球 B ε (a) 也包含 a 1 (连同许多其他然而,在逆转换中 a 1 → a、Bε**(a1)不能足够密集以建立直接密度可达性条件。

因此,当在两个方向中的一个方向上移动时,更长的序列可能被打破,从而导致密度可达性的损失。现在应该更清楚了,两点xIT5】和 x j 之间的密度连接允许我们避免这个问题,前提是有另一个点可以同时到达xIT13】和xjT17】。**

所有密度连通点对( x i 、x j )与 x ix j ∈ X 将被分配到同一个集群 C t 。而且,如果Xk∈Ct,则从 x k 密度可达的所有点 x p ∈ X 也将属于同一簇。不能从任何其他点 x i ∈ X 密度可达的点 x n 被定义为噪声点。因此,与其他算法相反,DBSCAN 输出 n 聚类加上一个包含所有噪声点的附加集合(这些噪声点不一定被视为异常值,而是不属于任何密集子区域的点)。当然,由于噪音点没有标签,它们的数量应该相当低;因此,调整参数 εnminT49】很重要,有双重目标:最大化衔接和分离,避免太多的点被标记为嘈杂。没有标准规则来实现这样的目标,因此我建议在做出最终决定之前测试不同的值。

最后,重要的是要记住,DBSCAN 可以处理非凸几何图形,与均值漂移相反,它假设存在被低密度区域包围的高密度区域。此外,它的复杂性与所采用的 KNN 方法(蛮力、球树或 kd 树)密切相关。一般来说,在数据集不太大的情况下,平均性能在 O(N log N) 左右,但在 N 很大的情况下,可以倾向于 O(N 2 ) 。另一个需要记住的重要因素是样本的维度。正如我们已经讨论过的,高维度量可以减少两点的可区分性,从而对 KNN 方法的性能产生负面影响。因此,当维度非常高时,应该避免(或者至少仔细分析)数据库扫描,因为生成的聚类不能有效地表示实际的密集区域。

在展示一个具体的例子之前,有必要介绍一种在不明真相的情况下可以使用的评估方法。

卡林斯基-哈拉巴斯茨评分

让我们假设已经对包含 M 样本的数据集 X 应用了聚类算法,以便将其分割成ncT7】聚类cIt11 】,由质心 μ i ∀ i = 1 表示..n c 。我们可以将W****I-Cluster distribution(WCD)**定义如下:****

**

如果 x i 是一个 N 维列向量,xk∈ℜn×n。不难理解 WCD(k) 编码了关于聚类的伪方差的全局信息。如果满足最大内聚力条件,我们期望质心周围的分散有限。另一方面, WCD(k) 甚至可能受到包含异常值的单个聚类的负面影响。因此,我们的目标是在每种情况下最小化 WCD(k) 。以类似的方式,我们可以定义一个 B 团簇间分散 ( BCD )为:

在前面的公式中, N(C i ) 是分配给聚类CIT7】的元素个数, μ 是整个数据集的全局质心。考虑到最大分离的原则,我们希望密集的区域远离全局质心。 BCD(k) 精确表达了这一原理,因此我们需要将其最大化以获得更好的性能。

卡林斯基-哈拉巴斯兹评分定义为:

引入了对预测标签的显式依赖,因为质心的计算不被认为是聚类算法的一部分。分数没有绝对的意义,而是有必要比较不同的值,以了解哪种解决方案更好。显然,CHk()越高,聚类性能越好,因为这样的条件意味着更大的分离和更大的内部凝聚力。

使用数据库扫描分析工作缺勤数据集

“工作缺勤”数据集(按照本章开头的说明下载)由 740 条记录组成,这些记录包含休假员工的信息。共有 20 个属性,代表年龄、服务时间、教育程度、习惯、疾病、违纪行为、交通费用、从家到办公室的距离等(字段的完整描述可在https://archive.ics.uci.edu/ml/datasets/Absenteeism+at+work获得)。我们的目标是对数据进行预处理并应用数据库扫描,以便发现具有特定语义内容的密集区域。

第一步是如下加载 CSV 文件(必须更改占位符<data_path>以指向文件的实际位置):

import pandas as pd

data_path = '<data_path>\Absenteeism_at_work.csv'

df = pd.read_csv(data_path, sep=';', header=0, index_col=0).fillna(0.0)
print(df.count())

前一个命令的输出如下:

Reason for absence                 740
Month of absence                   740
Day of the week                    740
Seasons                            740
Transportation expense             740
Distance from Residence to Work    740
Service time                       740
Age                                740
Work load Average/day              740
Hit target                         740
Disciplinary failure               740
Education                          740
Son                                740
Social drinker                     740
Social smoker                      740
Pet                                740
Weight                             740
Height                             740
Body mass index                    740
Absenteeism time in hours          740
dtype: int64

其中一些特征是分类的,并用连续整数编码(例如,Reason for absenceMonth of absence,等等)。由于这些值可能会影响距离,但没有精确的语义原因(例如,Month=12Month=10大,但两个月在距离方面是相等的),我们需要在继续下一步之前对所有这些特征进行一次热编码(新特征将被附加在列表的末尾)。在下面的片段中,我们使用get_dummies()熊猫函数来执行编码;然后删除原始列:

import pandas as pd

cdf = pd.get_dummies(df, columns=['Reason for absence', 'Month of absence', 'Day of the week', 'Seasons', 'Disciplinary failure', 'Education', 'Social drinker', 'Social smoker'])

cdf = cdf.drop(labels=['Reason for absence', 'Month of absence', 'Day of the week', 'Seasons', 'Disciplinary failure', 'Education', 'Social drinker', 'Social smoker']).astype(np.float64)

一次热编码的结果通常会产生平均值之间的差异,因为许多特征将被限制为 0 或 1,而其他特征(例如年龄)可以具有更大的范围。因此,最好将平均值标准化(在不影响标准偏差的情况下,保持不变是有帮助的,因为它们与现有的信息内容成比例)。这一步可以通过StandardScaler类设置参数with_std=False来实现,如下所示:

from sklearn.preprocessing import StandardScaler

ss = StandardScaler(with_std=False)
sdf = ss.fit_transform(cdf)

此时,像往常一样,我们可以使用 t-SNE 算法来降低数据集的维度(使用n_components=2)并可视化结构。数据框dff将包含原始数据集和 t-SNE 坐标,如下所示:

from sklearn.manifold import TSNE

tsne = TSNE(n_components=2, perplexity=15, random_state=1000)
data_tsne = tsne.fit_transform(sdf)

df_tsne = pd.DataFrame(data_tsne, columns=['x', 'y'], index=cdf.index) 
dff = pd.concat([cdf, df_tsne], axis=1)

下面的截图显示了结果图:

t-SNE bidimensional representation of the Absenteeism at Work dataset

在任何考虑之前,重要的是要重复 t-SNE 产生一个最佳的低维表示,但是总是有必要在原始数据集上测试算法,以便检查由 t-SNE 识别的邻居是否对应于实际的团块。特别地,考虑到 DBSCAN 的结构,考虑到 t-SNE 表示,ε值可以是合理的,但是当移动到更高维空间时,球不能再捕获相同的样本。然而,前面的图显示了被空白空间包围的密集区域的存在。不幸的是,密度不太可能是均匀的(这是 DBSCAN 的建议要求之一,因为 εnminT5】的值都不能改变),但是,在这种情况下,我们假设所有斑点的密度都是恒定的。

为了找到最适合我们的配置,我们使用闵可夫斯基度量将簇的数量、噪声点的数量、轮廓分数和卡林斯基-哈拉巴斯分数绘制为ε的函数,其中 p=2p=4p=8p=12、如下图所示:

Evaluation metrics as functions of ε

剪影和 Calinski-Harabasz 都基于凸簇的假设(例如,色散显然是一种假设样本围绕质心径向分布的度量),因此它们在非凸情况下的期望值通常较小。但是,我们希望最大化这两个分数(剪影→ 1 和 Calinski-Harabasz → ∞),同时避免大量的聚类。考虑到我们的初始目标(寻找以一组特定特征为特征的内聚集群),我们选择了 ε=25 和一个具有 p=12 的闵可夫斯基度量,这产生了合理数量的集群(13)和 22 个噪声点。在第二章聚类基本面、中我们已经表明,当 p → ∞ (但对于 p > 2 效果已经可见)时,距离趋于最大特征差。

因此,这种选择应该总是通过上下文分析来证明。在这种情况下,我们可以假设每个(非)凸斑点代表一个由特定特征支配的类别(所有其他特征的次要贡献),因此 p=12 (导致 17 个聚类)对于中粗粒度分析(考虑到有 20 个属性)来说是一个很好的权衡。此外, ε=22.5 与最高的卡林斯基-哈拉巴斯分数之一 129.3 以及大约等于 0.2 的轮廓分数相关联。特别是,后一个值表示总体聚类是合理正确的,但是可能有重叠。由于底层几何很可能是非凸的,这样的结果是可以接受的(通常在凸的情况下是不可接受的),同时考虑到具有相应峰值的 Calinski-Harabasz 分数。ε的较大值产生略高的轮廓分数(小于 0.23),但所得的簇数和卡林斯基-哈巴斯兹分数都不受所得构型的影响。必须明确的是,这一选择没有得到任何外部证据的证实,必须通过对结果的语义分析进行验证。如果需要精细的分析,具有更多簇和更多噪声点的配置也是可以接受的(因此,读者可以使用值来玩,并且还可以提供结果的解释)。然而,这个例子的最终目标保持不变:分割数据集,以便每个聚类包含特定的(可能是唯一的)属性。

*我们现在可以实例化一个DBSCAN模型,并使用包含规范化特征的数组sdf对其进行训练。配置为 ε=25 (参数eps)和 n min =3 (参数min_samples),Minkowski 公制(metric='minkowski')带p=12

我们现在可以执行以下集群:

from sklearn.cluster import DBSCAN
from sklearn.metrics import silhouette_score, calinski_harabaz_score

ds = DBSCAN(eps=25, min_samples=3, metric='minkowski', p=12)
Y_pred = ds.fit_predict(sdf)

print('Number of clusters: {}'.format(np.max(Y_pred) + 1))
print('Number of noise points: {}'.format(np.sum(Y_pred==-1)))

print('Silhouette score: {:.3f}'.format(silhouette_score(dff, Y_pred, metric='minkowski', p=12)))
print('Calinski-Harabaz score: {:.3f}'.format(calinski_harabaz_score(dff, Y_pred)))

由于DBSCAN用标签-1标记了有噪声的点,所以上一个片段的输出如下:

Number of clusters: 13
Number of noise points: 22

Silhouette score: 0.2
Calinski-Harabaz score: 129.860

下面的截图显示了结果图:

Clustering result for the Absenteeism at Work dataset

如您所见(我建议运行代码,以便获得更好的视觉确认),大多数孤立的(即使在 t-SNE 图中没有内聚性)区域已被成功检测到,并且样本已被分配到同一聚类。我们还可以观察到两个基本结果:噪声点(用十字标记)在 t-SNE 表示中不是孤立的,一些簇是部分分裂的。这不是算法的失败,而是降维的直接后果。在原始空间中,所有噪声点实际上都没有与任何其他样本进行密度连接,但它们可能会在 t-SNE 图中出现重叠或靠近某些斑点。然而,我们对高密度和准内聚的非凸区域感兴趣,幸运的是,它们在二维图中看起来也是连接的。

现在让我们考虑两个不同的区域(为简单起见,将分析限制在一次热编码后的前 10 个属性)。第一个是二维区域x < -45,如下:

sdff = dff[(dff.x < -45.0)]
print(sdff[sdff.columns[0:10]].describe())

输出的漂亮打印版本如下图所示:

Statistical measures corresponding to the subdataset x < -45

有两个因素可以立即引起我们的注意:运输费用(似乎标准化为 179 的值)和儿子的数量(考虑到平均值和标准偏差,大多数样本对应于 0)。让我们也考虑服务时间和从住所到工作地点的距离,这可以帮助我们找到集群的语义标签。所有其他参数都不太具有歧视性,我们在这一简要分析中将它们排除在外。因此,我们可以假设这样一个亚集群包含 40 岁左右没有孩子的人,服务时间大,住的离办公室相当远(我请读者查看整体统计数据来证实这一点),交通费用标准化(例如,一次付清的车费)。

现在让我们将此结果与区域 -20 < x < 20y < 20 进行比较,如下所示:

sdff = dff[(dff.x > 20.0) & (dff.y > -20.0) & (dff.y < 20.0)]
print(sdff[sdff.columns[0:10]].describe())

相应的输出如下:

Statistical measures corresponding to the subdataset -20 < x < -20 and y < 20

在这种情况下,交通费用较大,而从住所到工作地点的距离大约是前面例子的一半(也考虑了标准差)。而且儿子平均数量为 1 个,有两个孩子的员工比例适中,服务时间约为 12 个,标准差为 3.6。我们可以推断,这个聚类包含了年龄范围内(28–58 岁)有家庭的(已婚)人的所有样本,他们住得离办公室相对较近,但旅行费用较高(例如,由于使用出租车服务)。这类员工倾向于避免加班,但他们的平均工作量几乎与前面例子中观察到的相同。即使没有正式确认,我们也可以假设这样的员工通常更有效率,而第一组包含有生产力的员工,然而,他们需要更多的时间来实现他们的目标(例如,因为旅行时间更长)。

这显然不是详尽的分析,也不是一套客观的陈述。目标是展示如何通过观察样本的统计特征来找到聚类的语义内容。在现实生活中,所有的观察都必须由专家(例如,人力资源经理)验证,以了解分析的最后部分(特别是语义上下文的定义)是否正确,或者是否有必要使用大量的集群、不同的指标或其他算法。作为练习,我邀请读者分析包含单个聚类的所有区域,以便完成一幅大图,并测试不同类别(例如,非常年轻的人、有三个孩子的员工等)对应的人工样本的预测。

作为性能指标的集群不稳定性

聚类不稳定性是冯·卢克斯堡(在聚类稳定性:概述,冯·卢克斯堡 u,arXiv 1007:1075v1,2010 中)提出的一种方法,可以衡量算法相对于特定数据集的优良性。它可以用于不同的目的(例如,调整超参数或找到最佳的集群数量),并且相对容易计算。该方法基于这样的思想,即满足最大内聚和分离要求的聚类结果也应该对数据集的噪声扰动具有鲁棒性。换句话说,如果数据集 X 已经被分割成聚类集 C ,则导出的数据集 X n (基于特征的小扰动)应该被映射到相同的聚类集。如果不满足这个条件,通常有两种可能:噪声扰动太强或者算法对小的变化太敏感,因此不是很稳定。因此,我们定义了原始数据集 X 的一组 k 扰动(或二次采样)版本:

如果我们应用一个算法 A 产生相同数量的簇ncT5】,我们可以定义一个距离度量d()A(X i )A(X j ) 之间,它测量不一致分配的数量(即 A(X i ) 因此,d()可以简单地计算不同标签的数量,假设算法(如有必要)以相同的方式播种,并且数据集显然没有被打乱。算法的不稳定性(相对于 Xk 噪声变化)定义为:

因此,不稳定性是噪声变化对的聚类结果之间的平均距离。当然,这个值不是绝对的,所以有可能导出的规则是:选择产生最小不稳定性的配置。同样重要的是,这种方法不能与前面讨论的其他方法相比,因为它基于其他超参数(噪声变化的数量、噪声均值和方差、子采样比等),因此当 AX 固定时,它也可以产生不同的结果。特别是噪声的大小会极大地改变不稳定性,因此在确定例如高斯噪声的 μσ之前,需要评估 X 的均值和协方差矩阵。在我们的示例中(基于工作缺勤数据集中的 DBSCAN 聚类),我们创建了 20 个扰动版本,从一个加性噪声项NI∞N(E[X]Cov(X)/4 )开始,并应用从均匀分布 U(0,1) 采样的乘法掩码。这样,一些噪声项会被随机抵消或减少,如以下代码所示:

import numpy as np

data = sdf.copy()

n_perturbed = 20
n_data = []

data_mean = np.mean(data, axis=0)
data_cov = np.cov(data.T) / 4.0

for i in range(n_perturbed):
    gaussian_noise = np.random.multivariate_normal(data_mean, data_cov, size=(data.shape[0], ))
    noise = gaussian_noise * np.random.uniform(0.0, 1.0, size=(data.shape[0], data.shape[1]))
    n_data.append(data.copy() + noise)

在这种情况下,我们希望将不稳定性计算为 ε 的函数,但是可以用任何其他算法和超参数重复该示例。此外,我们使用归一化汉明距离,它与两个聚类结果之间不一致分配的数量成比例,如下所示:

from sklearn.cluster import DBSCAN
from sklearn.metrics.pairwise import pairwise_distances

instabilities = []

for eps in np.arange(5.0, 31.0, 1.5):
    Yn = []

    for nd in n_data:
        ds = DBSCAN(eps=eps, min_samples=3, metric='minkowski', p=12)
        Yn.append(ds.fit_predict(nd))

    distances = []

    for i in range(len(Yn)-1):
        for j in range(i, len(Yn)):
            d = pairwise_distances(Yn[i].reshape(-1, 1), Yn[j].reshape(-1, 1), 'hamming')
            distances.append(d[0, 0])

    instability = (2.0 * np.sum(distances)) / float(n_perturbed ** 2)
    instabilities.append(instability)

结果如下图所示:

Cluster instability of DBSCAN applied to the Absenteeism at Work dataset as a function of ε

大约 ε < 7 的值为空。这样的结果是由于算法产生了大量的聚类和噪声样本。由于样本分布在不同的区域,小的扰动不能改变分配。对于 7 < ε < 17 ,我们观察到正斜率达到最大值对应约 ε = 12.5 ,随后负斜率达到最终值 0。在这种情况下,集群变得越来越大,包括越来越多的样本;然而,当 ε 仍然太小时,密度可达性链很容易被小扰动打破(也就是说,样本可以克服球的边界,因此它被排除在簇之外)。因此,在应用加性噪声之后,样本通常被分配给不同的聚类。这种现象在 ε = 12.5 时达到最大,然后开始变得不太显著。

事实上,当 ε 足够大时,球的结合能够包裹整个簇,为小扰动留下足够的自由空间。当然,在取决于数据集的阈值之后,将只产生单个聚类,并且如果噪声不是太强,任何扰动版本将产生相同的分配。在我们的具体情况下, ε = 25 保证了高稳定性,这也是 t-SNE 图所证实的。一般来说,这种方法可以用于所有算法和几何图形,但我建议在决定如何创建扰动版本之前,对 X 进行彻底分析。事实上,一个错误的决定可能会损害结果,产生大的/小的不稳定性,这并不表示表现不好/好。特别是,当聚类具有不同的方差时(例如,在高斯混合中),加性噪声项对某些样本的影响可以忽略不计,而它可以完全改变其余样本的结构。在这些情况下,这种方法比其他方法弱,应该使用例如二次采样和具有非常小方差(通常小于最小聚类(共)方差)的高斯噪声。另一方面,在基于密度的算法中,二次采样显然是非常危险的,在这种算法中,由于可达性的丧失,一个小的聚类可能变成一组孤立的噪声点。我邀请读者也用 K 均值来测试这个方法,以便找到最佳的聚类数(这通常与最小的不稳定性有关)。

k-水母

在前一章中,我们已经表明,当簇的几何形状是凸的时,K 均值通常是一个很好的选择。然而,这种算法有两个主要的局限性:度量总是欧几里德的,并且对异常值不太鲁棒。第一个要素是显而易见的,而第二个要素是质心性质的直接结果。事实上,K-means 选择形心作为不能成为数据集一部分的实际均值。因此,当一个聚类有一些异常值时,平均值会受到影响,并成比例地向它们移动。下图显示了一个示例,其中一些异常值的存在迫使质心到达密集区域之外的位置:

Example of centroid selection (left) and medoid selection (right)

K-medoids (在借助 medoids 进行聚类,Kaufman L .,rousseuw p . j .,在基于 L1-诺姆和相关方法的统计数据分析中,North-Holland, 1987)最初是为了缓解对异常值缺乏鲁棒性的问题(在原始论文中,该算法被设计为仅适用于 Manhattan 度量),但后来不同的版本被设计为允许使用任何度量(特别是任意 Minkowski 度量)。与 K 均值的主要区别在于质心的选择,在这种情况下,质心是始终属于数据集的示例性样本(称为水母)。该算法本身非常类似于标准的 K-means,并且通过将样本重新分配到具有最接近的 medoid 的聚类中,来替换 med oid 的定义μI= XI∈X(作为最小化与分配给聚类的所有其他样本的平均或总距离的元素 C i )。

很容易理解,离群值不再有很高的权重,因为与标准质心相反,它们被选择为水母的概率接近于零。另一方面,当一个聚类由不能被归类为异常值的样本包围的密集斑点组成时,K-水母的表现较差。在这种情况下,算法可能会错误地分配这些样本,因为它无法生成能够捕获它们的虚拟球(请记住,半径是由质心/水母的相互位置隐式定义的)。因此,虽然 K-means 可以将质心移动到非密集区域以便也捕获远点,但是当密集斑点包含许多点时,K-med oid 不太可能以这种方式表现。

此外,K-medoids 倾向于聚集密度具有两个峰值的非常重叠的斑点,而 K-means 通常根据平均值的位置将整个区域分成两部分。如果凸几何的假设成立,这种行为通常会被接受,但在其他情况下,这可能是一种限制(我们将在示例中展示这种效果)。

最后一个基本区别是公制距离。由于没有限制,K-甲状旁腺素或多或少可以具有攻击性。正如我们在第 2 章聚类基本原理的开头所讨论的,最长的距离是由 Manhattan 度量提供的(它以相同的方式评估每个不同的组件),而当 p 增加时(在通用的 Minkowski 度量中),组件之间的最大差异变得占主导地位。K-means 是基于最常见的折衷(欧几里德距离),但是当更大的 p 可以导致更好的性能时(当比较 p=1p > 1 时,效果更明显),也有一些特殊的情况。例如,如果 c 1 =(0,0)c 2 =(2,1) ,以及 x=(0.55,1.25) ,曼哈顿距离 d 1 (x,c 1 )d 1 因此,当 p=1 时,该点被分配给第二个簇,而当 p=2 时,该点被分配给第一个簇。

一般来说,预测正确的值 p 并不容易,但是总是有可能使用轮廓和调整后的兰德分数等方法测试几种配置,并选择产生更好分割的配置(即最大内聚和分离或更高的调整后兰德分数)。在我们的示例中,我们将生成一个包含基本事实的数据集,因此我们可以使用后一个选项轻松评估性能。因此,我们将使用函数make_blobs生成1000样本,这些样本在由[-5.0, 5.0]限定的框中被分割成8斑点,如下所示:

from sklearn.datasets import make_blobs

nb_samples = 1000
nb_clusters = 8

X, Y = make_blobs(n_samples=nb_samples, n_features=2, centers=nb_clusters, 
                  cluster_std=1.2, center_box=[-5.0, 5.0], random_state=1000)

生成的数据集呈现出一些强烈的重叠(如最终图所示),因此我们不期望使用对称方法获得高水平的结果,但我们对比较 K-means 和 K-med oid 所做的分配感兴趣。

让我们开始评估通过 K-means 达到的调整后的 Rand 分数,如下所示:

from sklearn.cluster import KMeans
from sklearn.metrics import adjusted_rand_score

km = KMeans(n_clusters=nb_clusters, random_state=1000)
C_km = km.fit_predict(X)

print('Adjusted Rand score K-Means: {}'.format(adjusted_rand_score(Y, C_km)))

上一个块的输出如下:

Adjusted Rand score K-Means: 0.4589907163792297

这个值足以理解 K-means 正在进行许多错误的赋值,尤其是在重叠区域。由于使用这种方法很难对数据集进行聚类,因此我们不将这一结果视为真正的指标,而仅将其视为可与 K-medoids 评分进行比较的衡量标准。现在让我们使用带有p = 7的闵可夫斯基度量来实现该算法(请读者更改该值并检查结果),如下所示:

import numpy as np

C = np.random.randint(0, nb_clusters, size=(X.shape[0], ), dtype=np.int32)
mu_idxs = np.zeros(shape=(nb_clusters, X.shape[1]))

metric = 'minkowski'
p = 7
tolerance = 0.001

mu_copy = np.ones_like(mu_idxs)

数组C包含赋值,mu_idxs将包含水母。由于存储整个 medoids 所需的空间通常很小,我们更喜欢这种方法,而不是只存储索引。优化算法如下:

import numpy as np

from scipy.spatial.distance import pdist, cdist, squareform
from sklearn.metrics import adjusted_rand_score

while np.linalg.norm(mu_idxs - mu_copy) > tolerance:
    for i in range(nb_clusters):
        Di = squareform(pdist(X[C==i], metric=metric, p=p))
        SDi = np.sum(Di, axis=1)

        mu_copy[i] = mu_idxs[i].copy()
        idx = np.argmin(SDi)
        mu_idxs[i] = X[C==i][idx].copy()

    C = np.argmin(cdist(X, mu_idxs, metric=metric, p=p), axis=1)

print('Adjusted Rand score K-Medoids: {}'.format(adjusted_rand_score(Y, C)))

行为很简单。在每次迭代中,我们计算属于一个聚类的所有元素之间的成对距离(这实际上是最昂贵的部分),然后我们选择最小化总和的 med oidSDi。一个周期后,我们通过最小化样本与水母的距离来分配样本。重复这些操作,直到水母的范数变化变得小于预定阈值。调整后的兰德分数如下:

Adjusted Rand score K-Medoids: 0.4761670824763849

The final adjusted Rand score is influenced by the random initialization of the algorithm (hence, the previous result can slightly change when running the code). In real applications, I suggest employing a double stopping criterion based on the maximum number of iterations together with a small tolerance.

因此,即使重叠没有被解决,性能也比 K-means 稍好。地面实况、K-均值和 K-med oid 结果显示在下面的截图中:

Ground truth (left), K-means (center), and K-medoids (right)

正如你所看到的,地面真相包含两个重叠的区域,极难聚集。在这个特殊的例子中,我们对解决这个问题不感兴趣,而是想展示这两种方法的不同行为。如果我们考虑前两个斑点(左上角),K-means 将整个区域分成两部分,而 K-medoids 将所有元素分配给同一个聚类。在不知道基本事实的情况下,后一个结果可能比第一个结果更连贯。事实上,观察第一个图,可能会注意到密度差并没有强到完全证明分裂是合理的(然而,在某些情况下,这可能是合理的)。由于该区域相当密集,并且与其邻居分隔开,因此单个集群可能是预期的结果。此外,几乎不可能根据相异度来区分样本(大多数靠近分隔线的样本被错误分配),因此 K-med oid 比 K-means 的攻击性更小,显示出更好的折衷。相反,第二个重叠区域(右下角)由两种算法以几乎相同的方式管理。这是因为 K-means 将质心放置在非常靠近一些实际样本的位置。在这两种情况下,算法需要在 0 和 4 之间创建几乎水平的间隔,因为否则不可能分割区域。这种行为在所有基于标准球的方法中都很常见,在这种特殊情况下,这是极其复杂的几何图形的正常结果(许多相邻的点有不同的标签)。因此,我们可以得出结论说,K-med oid 对异常值更稳健,有时通过避免不希望的分离比 K-means 表现得更好。另一方面,当在没有异常值的非常密集的区域中工作时,这两种算法(特别是当采用相同的度量时)是等效的。作为练习,我邀请读者使用其他指标(包括余弦距离)并比较结果。

*# 在线聚类

有时,数据集太大,无法放入内存,或者样本通过通道以不同的时间步长传输和接收。在这种情况下,前面讨论的算法都不能使用,因为它们假设从第一步开始就可以访问整个数据集。由于这个原因,已经提出了一些在线替代方案,它们目前正在许多现实生活的过程中实施。

小批量 K 均值

该算法是标准 K-means 的扩展,但是,由于质心不能用所有样本计算,当现有聚类不再有效时,有必要包括负责重新分配样本的附加步骤。特别是,小批量 K 均值不是计算全局均值,而是处理流均值。一旦接收到批次,算法计算部分平均值并确定质心的位置。然而,不是所有的聚类都有相同数量的分配,因此算法必须决定是等待还是重新分配样本。

这个概念可以通过考虑一个非常低效的流过程来立即理解,该流过程开始发送属于一个半空间的所有样本,并且只包括属于互补半空间的几个点。由于簇的数量是固定的,算法将开始优化质心,同时只考虑一个子区域。让我们假设质心已经被放置在球的中心,球围绕着属于互补子空间的几个样本。如果越来越多的批次继续向密集区域添加点,算法可以合理地决定丢弃孤立质心并重新分配样本。然而,如果该过程开始发送属于互补半空间的点,算法必须准备好将它们分配给最合适的簇(即,它必须将其他质心放置在空区域中)。

该方法通常基于名为再分配比 α 的参数。当 α 较小时,算法会等待较长的时间再重新分配样本,而较大的值会加速这一过程。当然,我们希望避免这两种极端情况。换句话说,我们需要避免一个过于静态的算法,它在做出决定之前需要很多样本,同时也需要避免一个过于快速变化的算法,它在每批之后重新分配样本。一般来说,第一种情况以较低的计算成本产生次优解,而后者可能变得非常类似于在每次批处理后重新应用于流式数据集的标准 K 均值。考虑到这种通常与实时过程相关的场景,我们通常对需要高计算成本的极其精确的解决方案不感兴趣,而是对在收集新数据时得到改进的良好近似感感兴趣。

然而,重新分配比率的选择必须考虑到每一个单独的上下文,包括流过程的合理预测(例如,它纯粹是随机的吗?样本是独立的吗?某些样本在某个时间段内更频繁吗?).此外,必须群集的数据量(即批处理大小,这是一个极其重要的因素),当然还有可以调配的硬件。一般来说,当批量不是太小(但这通常不是一个可控的超参数,因为它依赖于外部资源)并且相应地选择重新分配比率时,可以证明小批量 K-means 产生的结果与标准 K-means 相当,具有更低的内存需求和更高的计算复杂度。

相反,当从真实数据生成过程中对批次进行统一采样时,重新分配比率成为不太重要的参数,并且其影响较低。事实上,在这些情况下,批次大小通常是获得良好结果的主要影响因素。如果它足够大,算法能够立即确定质心的最可能位置,并且后续批次不能显著改变这种配置(因此减少了连续重新分配的需要)。当然,在在线场景中,很难确定数据生成过程的结构,因此通常只能假设一个批次(如果不是太小的话)包含足够的每个独特区域的代表。数据科学家的主要任务是通过收集足够的样本来执行完整的 K 均值,并将性能与小批量版本进行比较,从而验证这一假设。观察到较小批量产生更好的最终结果的场景(具有相同的重新分配比率)并不奇怪。这种现象可以通过考虑这种算法不会立即重新分配样本来理解;因此,有时,较大的批次可能会导致错误的配置,然而,具有更多的代表,因此重新分配的概率较低(也就是说,算法更快,但准确性较低)。相反,在相同的情况下,由于频繁的重新分配,较小的批次可以迫使算法执行更多的迭代,从而得到(更精确的)最终配置。由于定义一个通用的经验法则并不容易,一般的建议是在做出决定之前检查不同的值。

桦树

该算法(其名称代表使用层次结构的平衡迭代约简和聚类)比小批量 K-means 具有稍微复杂的动态性,并且最后一部分采用了我们将在第 4 章行动中的层次聚类中介绍的方法(层次聚类)。然而,就我们的目的而言,最重要的部分涉及数据准备阶段,该阶段基于称为聚类特征-特征树 ( CF-Tree )的特定树结构。给定一个数据集 X ,树的每个节点都由三个元素的元组组成:

特征元素分别是属于一个节点的样本数、所有样本的和以及平方范数的和。这一选择背后的原因将立即清楚,但现在让我们将注意力集中在树的结构上,以及如何在试图平衡高度的同时插入新元素。在下图中,有一个通用的 CF-Tree 表示,其中所有终端节点都是实际的子集群,为了获得所需数量的集群,必须对其进行合并:

Example of a simple CF-Tree with a binary repartition

在上图中,点代表指向子节点的指针。因此,每个非终端节点都与指向其所有子节点的指针存储在一起( CF i ,p i ),而终端节点是纯 CFs。为了讨论插入策略,必须考虑另外两个因素。第一个叫做分支因子 B ,第二个叫做阈值 T 。而且,每个非终端节点最多可以包含 B 元组。该策略旨在通过减少存储的数据量和计算次数,最大限度地提高仅依赖主内存的流处理的性能。

现在让我们考虑一个需要插入的新样本 x i 。很容易理解,a CF j = (n j 、a j 、b j ) 的质心简单来说就是μj= aj/nj;因此, x i 由于到达末端 CF(子集群)而沿着树传播,其中距离 d(x i ,μ j ) 最小。此时,CF 会逐步更新:

但是,如果没有控制,树很容易变得不平衡,从而导致性能损失。因此,该算法执行额外的步骤。一旦确定了 CF,就计算更新的半径rjT3 】,并且无论rjT30和 CFs 的数量是否大于分支因子,都分配新的块并且保持原始 CF 不变。由于这个新的区块几乎完全是空的(除了 x i ),BIRCH 执行一个额外的步骤来检查所有子集群之间的差异(这个概念将在第 4 章行动中的层次聚类中更清楚;然而,读者可以考虑属于两个不同子聚类的点之间的平均距离)。最不相似的一对被分成两部分,其中一个被移到新的区块。这样的选择保证了子集群的高度紧凑性,并加快了最后一步。实际上,实际聚类阶段涉及的算法需要合并子聚类,直到总数减少到期望值;因此,如果总相异度先前已经被最小化,则更容易执行该操作,因为可以被立即识别为连续的并被合并。本章不会详细讨论这个阶段,但不难想象。所有终端 CFs 被顺序合并到更大的块中,直到确定了单个集群(即使当数量与期望的集群数量匹配时有可能停止该过程)。因此,与小批量 K-means 相反,这种方法可以轻松管理大量集群 n c ,而当 n c 很小时,这种方法不是很有效。事实上,正如我们将在示例中看到的,它的精度通常低于小批量 k-means 所能达到的精度,并且它的最佳使用需要准确选择分支因子和阈值。由于该算法的主要目的是在在线场景下工作,因此 BT 在处理一些批次后可能会变得无效(而小批次 K 均值通常能够在几次迭代后校正聚类),从而产生次优结果。因此,BIRCH 的主要用例是一个在线过程,需要非常细粒度的分割,而在所有其他情况下,通常最好选择小批量 K 均值作为初始选项。

小批量 K 均值与 BIRCH 的比较

在本例中,我们希望将两种算法的性能与包含 2000 个样本的二维数据集进行比较,这些样本被分割成8个斑点(因为目的是分析性的,所以我们也使用基本事实),如下所示:

from sklearn.datasets import make_blobs

nb_clusters = 8
nb_samples = 2000

X, Y = make_blobs(n_samples=nb_samples, n_features=2, centers=nb_clusters,
                  cluster_std=0.25, center_box=[-1.5, 1.5], shuffle=True, random_state=100)

数据集(已经被混洗以消除流过程中的任何相互关联)显示在下面的截图中:

Bidimensional dataset for a comparison between mini-batch K-means and BIRCH

在执行在线聚类之前,评估标准 K 均值的调整后的 Rand 分数是有帮助的,如下所示:

from sklearn.cluster import KMeans

km = KMeans(n_clusters=nb_clusters, random_state=1000)
Y_pred_km = km.fit_predict(X)

print('Adjusted Rand score: {}'.format(adjusted_rand_score(Y, Y_pred_km)))

上一个块的输出如下:

Adjusted Rand score: 0.8232109771787882

考虑到数据集的结构(没有凹陷),我们可以合理地假设该值代表在线流程的基准。我们现在可以实例化类MiniBatchKMeansBirch,参数分别等于reassignment_ratio=0.001threshold=0.2branching_factor=350。这些值是在研究后选择的,但我邀请读者用不同的配置重复这个例子,比较结果。在这两种情况下,我们假设批次大小等于50样品,如下所示:

from sklearn.cluster import MiniBatchKMeans, Birch

batch_size = 50

mbkm = MiniBatchKMeans(n_clusters=nb_clusters, batch_size=batch_size, reassignment_ratio=0.001, random_state=1000)
birch = Birch(n_clusters=nb_clusters, threshold=0.2, branching_factor=350)

该示例的目标是现在使用方法partial_fit()递增地训练两个模型,并评估调整后的兰德分数,考虑到直到每一步处理的全部数据量,如下所示:

from sklearn.metrics import adjusted_rand_score

scores_mbkm = []
scores_birch = []

for i in range(0, nb_samples, batch_size):
    X_batch, Y_batch = X[i:i+batch_size], Y[i:i+batch_size]

    mbkm.partial_fit(X_batch)
    birch.partial_fit(X_batch)

    scores_mbkm.append(adjusted_rand_score(Y[:i+batch_size], mbkm.predict(X[:i+batch_size])))
    scores_birch.append(adjusted_rand_score(Y[:i+batch_size], birch.predict(X[:i+batch_size])))

print('Adjusted Rand score Mini-Batch K-Means: {}'.format(adjusted_rand_score(Y, Y_pred_mbkm)))
print('Adjusted Rand score BIRCH: {}'.format(adjusted_rand_score(Y, Y_pred_birch)))

前一个片段的输出包含整个数据集的调整后兰德分数:

Adjusted Rand score Mini-Batch K-Means: 0.814244790452388
Adjusted Rand score BIRCH: 0.767304858161472

不出所料,小批量 K-means 在所有样本处理完毕后几乎达到基准,而 BIRCH 的性能稍差。为了更好地理解这种行为,让我们考虑一个增量分数作为批次函数的图,如下图所示:

Incremental adjusted Rand scores as functions of the batches (number of samples)

如你所见,小批量 K 均值很快达到最大值,所有后续振荡都是由于重新分配。相反,随着负面趋势的出现,BIRCH 的表现总是更差。造成这种差异的主要原因是不同的策略。事实上,小批量 K-means 可以在几个批次后纠正质心的初始猜测,并且重新分配不会显著改变配置。另一方面,BIRCH 执行的合并数量受样本数量的影响。

在开始时,由于 CF-Tree 中的子集群数量不是很大(因此,聚合更连贯,但是在几个批次之后,BIRCH 不得不聚合越来越多的子集群,以便获得期望的最终集群数量,因此性能没有很大的不同。这种情况,加上流样本数量的增加,驱动算法重新排列树,通常会导致稳定性的损失。此外,数据集有一些重叠,可以通过对称方法更容易地管理(在这种情况下,质心实际上可以到达它们的最终位置,即使分配是错误的),而分层方法(例如 BIRCH 使用的方法)更能够找到所有子区域,但是在以最小的间隔合并子聚类时更容易出错,或者更糟糕的是,重叠。然而,这个例子证实了小批量 K-means 作为第一选项通常是优选的,并且只有当性能没有达到预期时才应该选择 BIRCH(仔细选择其参数)。我邀请读者用更大数量的期望簇(例如nb_clusters=20center_box=[-10.5, 10.5])重复该示例。有可能看到在这种情况下(保持所有其他参数不变),由小批量 K-means 执行的重新分配如何以更差的最终调整兰德分数减缓收敛,而 BIRCH 立即达到最佳值(几乎等于标准 K-means 实现的值),并且它不再受样本数量的影响。

摘要

在这一章中,我们介绍了一些最重要的聚类算法,可以用来解决非凸问题。光谱聚类是一种非常流行的技术,它将数据集投影到一个新的空间中,在这个空间中,凹的几何形状变成凸的,标准算法(如 K-means)可以轻松地分割数据。

相反,mean shift 和 DBSCAN 分析数据集的密度,并尝试将其拆分,以便将所有密集和连接的区域合并在一起,组成聚类。特别是,DBSCAN 在非常不规则的上下文中非常有效,因为它基于连接在一起的本地最近邻集,直到间隔超过预定义的阈值。以这种方式,该算法可以解决许多特定的聚类问题,唯一的缺点是它还产生一组噪声点,这些噪声点不能自动分配给现有的聚类。在基于工作缺勤数据集的示例中,我们展示了如何选择超参数,以便获得所需数量的聚类,同时具有最小数量的噪声点和可接受的轮廓或 Calinski-Harabasz 分数。

在最后一部分中,我们分析了 K-med oid 作为 K-means 的一种替代方法,它对异常值的鲁棒性也更强。该算法不能用于解决非凸问题,但它有时比 K 均值更有效,因为它不选择实际均值作为质心,而是仅依赖数据集,聚类中心(称为 medoids)是示例性样本。此外,该算法不受欧几里德度量的严格限制,因此可以充分挖掘替代距离函数的潜力。最后一个主题涉及两种在线聚类算法(小批量 K-means 和 BIRCH),当数据集太大而无法放入内存时,或者当数据在长时间帧内流动时,可以使用这两种算法。

在下一章中,我们将分析一个非常重要的聚类算法家族,它可以输出完整的层次结构,使我们能够观察完整的聚合过程,并选择最有帮助和最一致的最终配置。

问题

  1. 半月形数据集是凸簇吗?
  2. 二维数据集由两个半月组成。第二个完全包含在第一个的凹度中。哪种核可以很容易地允许两个聚类的分离(使用谱聚类)?
  3. 应用 ε=1.0 的 DBSCAN 算法后,我们发现噪声点太多。 ε=0.1 我们应该期待什么?
  4. k-med oid 基于欧几里得度量。这是正确的吗?
  5. DBSCAN 对数据集的几何非常敏感。这是正确的吗?
  6. 一个数据集包含 10,000,000 个样本,可以使用大型机器使用 K-means 轻松进行聚类。相反,我们可以使用更小的机器和小批量 K-means 吗?
  7. 聚类的标准偏差等于 1.0。施加一个噪声 N (0,0.005)后,80%的原始赋值被改变。我们能说这样的集群配置总体稳定吗?

进一步阅读

  • 归一化切割和图像分割史和马立克IEEE 模式分析和机器智能交易第 22 卷,08/2000
  • 光谱聚类教程冯卢克斯堡大学,2007
  • 函数与图形第 2 卷盖尔范德 I. M.格拉戈列娃 E. G.施诺尔 E. E.麻省理工学院出版社, 1969
  • 关于概率密度函数和模式的估计帕尔森 E.数理统计年鉴, 33,1962
  • 神经模糊网络在预测工作缺勤中的应用,马蒂亚诺,费雷拉,萨希,阿方索,信息系统与技术(CISTI)第七届伊比利亚会议(第 1-4 页)。IEEE ,2012 年
  • 一种基于密度的算法,用于在有噪声的大型空间数据库中发现聚类埃斯特 M.克里格尔 H. P.桑德 J.徐 X.第二届知识发现和数据挖掘国际会议论文集,美国俄勒冈州波特兰市,出版社,1996
  • Mac hine 学习算法第二版Bonaccorso g .**Packt 出版,2018
  • 集群稳定性:概述冯卢克斯堡大学arXiv 1007:1075v1 ,2010
  • 通过水母 ,考夫曼 l,罗塞夫 P.J .,在基于 L1-规范和相关方法的统计数据分析中,北荷兰,1987****

四、层次聚类实战

在本章中,我们将讨论层次聚类的概念,这是一种强大而广泛的技术,用于生成完整的聚类配置层次结构,从相当于数据集的单个聚类(除法方法)或等于样本数的多个聚类(凝聚方法)开始。当需要立刻分析整个分组过程以便理解例如如何将较小的簇合并成较大的簇时,这种方法特别有用。

特别是,我们将讨论以下主题:

  • 分层聚类策略(分裂和聚集)
  • 距离度量和链接方法
  • 树木图及其解释
  • 聚集聚类
  • 作为绩效衡量标准的共病相关性
  • 连通性限制

技术要求

本章中的代码要求如下:

数据集可以从 UCI 机器学习存储库中获得。CSV 文件可以从https://archive . ics . UCI . edu/ml/datasets/water+processing+plant下载,除了添加列名外,不需要任何预处理,这将在加载阶段发生。

示例可在 GitHub 资源库上获得:https://GitHub . com/packktpublishing/HandsOn-Unsupervised-Learning-with-Python/chapter 04

集群层次结构

在前面的章节中,我们分析了聚类算法,其中输出是基于预定义数量的聚类或参数集和精确基础几何的结果的单个分割。另一方面,分层聚类生成一系列聚类配置,这些配置可以排列在树的结构中。特别是,让我们假设我们有一个数据集, X ,包含 n 个样本:

一种凝聚方法开始于将每个样本分配给一个聚类,CIT5】,并通过在每一步合并两个聚类直到产生单个最终聚类(对应于 X ):

在上例中,集群CIT3】和CjT7】合并为CkT11】;因此,我们在第二步中获得 n-1 簇。该过程继续进行,直到剩余的两个聚类被合并成包含整个数据集的单个块。相反地,分裂方法(最初由考夫曼和鲁索提出,有一种称为 DIANA 的算法)的操作方向相反,从 X 开始,以每个聚类包含一个样本的分割结束:**

在这两种情况下,结果都是层次结构的形式,其中每一层都是通过在前一层上执行合并或拆分操作而获得的。复杂性是这两种方法的主要区别,因为它对于分裂聚类来说更高。事实上,合并/拆分决策是通过考虑所有可能的组合并选择最合适的组合(根据特定标准)做出的。例如,在比较第一步时,很明显,考虑到所有可能的组合(在分裂情况下),找到最合适的样本对(在凝聚情况下)比找到 X 的最佳分裂更容易,这需要指数复杂度。

由于最终的结果几乎相同,虽然除法算法的计算复杂度要高得多,但一般来说,没有特别的理由倾向于这种方法。因此,在本书中,我们将只讨论凝聚聚类(假设所有概念都立即适用于分裂算法)。我鼓励您始终考虑整个层次结构,即使大多数实现(例如 scikit-learn)都需要指定期望的集群数量。实际上,在实际应用程序中,一旦达到目标,最好停止该过程,而不是计算整个树。然而,这一步是分析阶段必不可少的一部分(特别是当集群的数量没有很好地定义时),我们将演示如何可视化树,并为每个特定的问题做出最合理的决策。

聚集聚类

正如在其他算法中看到的,为了执行聚合,我们需要首先定义一个距离度量,它表示样本之间的不同。我们已经分析了其中的许多,但是在这种情况下,开始考虑通用的闵可夫斯基距离(用 p 参数化)是有帮助的:

两种特殊情况对应于 p=2p=1 。在前一种情况下,当 p=2 时,我们得到标准的欧氏距离(相当于 L 2 范数):

p=1 时,我们获得曼哈顿城市街区距离(相当于L1T9】范数):

这些距离之间的主要差异在第 2 章聚类基础中讨论。在本章中,引入余弦距离是有用的,这不是一个合适的距离度量(从数学的角度来看),但是当样本之间的区分必须仅取决于它们形成的角度时,这是非常有用的:

余弦距离的应用非常特殊(例如自然语言处理 ( 自然语言处理),因此不是一个常见的选择。但是,我鼓励大家用一些样本向量来检查它的属性(例如,( 0,1 )、( 1,0 )和( 0.5,0.5 ),因为它可以解决很多现实生活中的问题(例如,在 word2vec 中,可以通过检查两个单词的余弦相似度来轻松评估它们的相似度)。一旦定义了距离度量,定义一个接近矩阵P 是有帮助的:

P 对称,所有对角元素都为空。由于这个原因,一些应用程序(如 SciPy 的pdist函数)产生一个压缩矩阵, P c ,它是一个只包含矩阵上三角部分的向量,因此 P cijT9】第元素对应于 d(x i ,x j )

下一步是定义合并策略,在这种情况下,称为链接。链接方法的目标是找出必须在层次结构的每一层合并成一个的集群。因此,它必须使用代表集群的通用样本集。在这种情况下,假设我们正在分析几个集群( C a ,CbT7),我们需要找到哪个索引 ab 对应于将要合并的集群。

单一和完整的联系

最简单的方法称为完全 联动,定义如下:

单一链接方法选择包含最接近的样本对的对(每个样本属于不同的聚类)。该流程如下图所示,选择 C1C2 进行合并:

Example of single linkage. C1 and C2 are selected to be merged

这种方法的主要缺点是可能有小簇和非常大的簇。正如我们将在下一节中看到的,单个链接可以将异常值隔离,直到存在非常高的相异度。为了避免或减轻这个问题,可以使用平均法和沃德法。

相反,完整链接定义为:

这种链接方法的目标是最小化属于合并聚类的最远样本之间的距离。下图中有一个完整联动的例子,选择了C1T3C3T7】😗***

Example of complete linkage. C1 and C3 are selected for merging

算法选择 C 1C 3 以增加内部凝聚力。事实上,考虑到所有可能的组合,很容易理解完全连锁导致集群密度最大化。在上图所示的示例中,如果期望的集群数量是两个,合并 C 1C 2C 2C 3 将产生凝聚力较小的最终配置,这通常是不期望的结果。

平均联系

另一种常用的方法叫做平均联动(或未加权对组法,算术平均 ( UPGMA ))。其定义如下:

这个想法与完全联动非常相似,但是,在这种情况下,考虑到每个集群的平均值,目标是最小化平均集群间距离,考虑所有可能的对( C a ,CbT5】)。下图显示了平均链接的示例:

Example of average linkage. C1 and C2 are selected for merging. The highlighted points are the averages.

平均连锁在生物信息学应用中特别有用(这是层次聚类被定义的主要背景)。对其性质的数学解释并不简单,我鼓励你查看原始论文(评估系统关系的统计方法,索卡尔 r,麦切纳 c,堪萨斯大学科学通报,1958 年第 38 期)以获得更多细节。

沃德的联系

我们要讨论的最后一种方法叫做沃德联系(以其作者命名,最初是在优化目标函数的层次分组中提出的,美国统计协会杂志。58(301),1963 。它基于欧几里得距离,形式定义如下:

在每一个级别,所有的集群都被考虑在内,其中两个集群的选择目标是最小化平方距离的总和。该过程本身与平均链接没有很大不同,并且有可能证明合并过程导致集群方差的减少(即增加它们的内部凝聚力)。此外,沃德的链接倾向于产生包含大约相同数量样本的聚类(也就是说,与单个链接相比,沃德的方法避免了小聚类和非常大的聚类的存在,这将在下一节中讨论)。沃德链接是一个流行的默认选择,但是,为了在每个特定的上下文中做出正确的选择,有必要引入树图的概念。

分析树木图

一个树形图是一个树形数据结构,允许我们表示由凝聚或分裂算法产生的整个聚类层次。想法是将样品放在 x 轴上,将相异度放在 y 轴上。每当两个聚类被合并时,树图显示对应于它出现时的不同级别的联系。因此,在聚集场景中,树图总是从所有被视为聚类的样本开始,并向上移动(方向纯粹是常规的),直到定义了单个聚类。

出于教学目的,最好显示与非常小的数据集 X 相对应的树形图,但是我们将要讨论的所有概念都可以应用于任何情况。但是,对于较大的数据集,通常需要应用一些截断,以便以更紧凑的形式可视化整个结构。

让我们考虑一个小数据集, X ,由4高斯分布生成的12二维样本组成,其平均向量在( -1,1 ) × ( -1,1 ):

from sklearn.datasets import make_blobs

nb_samples = 12
nb_centers = 4

X, Y = make_blobs(n_samples=nb_samples, n_features=2, center_box=[-1, 1], centers=nb_centers, random_state=1000)

数据集(带有标签)如下图所示:

Dataset employed for dendrogram analysis

为了生成树图(使用 SciPy),我们首先需要创建一个链接矩阵。在这种情况下,我们选择了带有沃德链接的欧几里德度量(但是,像往常一样,我鼓励您使用不同的配置来执行分析):

from scipy.spatial.distance import pdist
from scipy.cluster.hierarchy import linkage

dm = pdist(X, metric='euclidean')
Z = linkage(dm, method='ward')

dm数组是一个压缩的成对距离矩阵,而Z是由沃德方法产生的链接矩阵(linkage()函数需要method参数,该参数接受值singlecompleteaverageward)。此时,我们可以生成并绘制树形图(使用默认或提供的 Matplotlib axis对象,dendrogram()功能可以自动绘制图表):

import matplotlib.pyplot as plt

from scipy.cluster.hierarchy import dendrogram

fig, ax = plt.subplots(figsize=(12, 8))

d = dendrogram(Z, show_leaf_counts=True, leaf_font_size=14, ax=ax)

ax.set_xlabel('Samples', fontsize=14)
ax.set_yticks(np.arange(0, 6, 0.25))

plt.show()

下图显示了该图:

Dendrogram corresponding to Ward's linkage applied to the dataset

如前一张截图中所述, x 轴代表旨在最小化交叉连接风险的样本,而 y 轴则显示不同程度。现在让我们从底部分析图表。初始状态对应于被视为独立聚类的所有样本(因此相异度为空)。向上移动,我们开始观察第一次合并。特别地,当相异度约为 0.35 时,样本 1 和 3 被合并。

当样本 0 和 9 也合并时,第二步的相异度略低于 0.5。这个过程一直持续到创建单个集群时,相异度约为 5.25。现在让我们水平解剖树图,当相异度等于 1.25 时。查看底层连接,我们发现集群结构为:{6}、{7,5,8}、{0,9,4,10}、{11}、{2,1,3}。

因此,我们有五个集群,其中两个由单个样本组成。观察到样本 6 和样本 11 是最后合并的样本也就不足为奇了。事实上,它们之间的距离比其他所有的都远。在下面的屏幕截图中,显示了四个不同的级别(只有包含多个样本的集群用圆圈标记):

Clusters generated by cutting the dendrogram at different levels (Ward's linkage)

很容易理解,聚集从选择最相似的集群/样本开始,并通过添加最近的邻居进行,直到到达树根。在我们的例子中,在相异度等于 2.0 的情况下,已经检测到三个定义明确的聚类。左边的一个也保留在下一个切割中,而右边的两个(显然更接近)被选择合并,以便产生单个簇。这个过程本身很简单,不需要特别的解释;然而,有两个重要的考虑。

第一个是固有的树图结构本身。与其他方法相反,层次聚类允许观察整个聚类树,并且当需要通过增加相异度来显示过程如何发展时,这样的特性非常有用。例如,一个产品推荐应用程序不能提供任何关于代表用户的期望集群数量的信息,但是执行管理层可能有兴趣理解合并过程是如何构造和发展的。

事实上,观察集群是如何合并的,可以深入了解底层的几何结构,也有可能发现哪些集群可能被认为是更大集群的一部分。在我们的示例中,在 0.5 级,我们有一个小集群{1,3}。“通过增加相异度可以将什么样的样本添加到这个聚类中?”可以立即用{2}回答。当然,在这种情况下,这是一个微不足道的问题,可以通过查看数据图来解决,但是对于高维数据集,如果没有树图的支持,这可能会变得更加困难。

谱系图的第二个优点是可以比较不同连锁方法的行为。使用沃德的方法,第一次合并发生在相当低的相异度水平上,但是五个集群和三个集群之间有很大的差距。这是几何和合并策略的结果。例如,如果我们使用一个单一的链接(本质上非常不同),会发生什么?相应的树形图如下图所示:

Dendrogram corresponding to single linkage applied to the dataset

结论是树图是不对称的,簇通常与单个样品或小团块合并。从右侧开始,我们可以看到样本{11}和{6}很晚才合并。此外,样本{6}(可能是异常值)在最高相异度时被合并,此时必须产生最终的单个聚类。通过下面的截图可以更好地理解这个过程:

Clusters generated by cutting the dendrogram at different levels (single linkage)

从截图中可以看到,虽然沃德的方法生成了两个包含所有样本的聚类,但单个链接通过将潜在的异常值保持在外部来聚合 1.0 级的最大块。因此,谱系图也允许定义聚合语义,这在心理测量学和社会学的背景下非常有帮助。虽然沃德的链接方式与其他对称算法非常相似,但单个链接有一种逐步的方式,显示出对增量构建的集群的潜在偏好,从而避免了相异度的巨大差距。

最后,有趣的是,虽然沃德的链接通过在 3.0 级切割树形图产生了潜在的最佳聚类数(三个),但单个链接从未达到这样的配置(因为聚类{6}仅在最后一步被合并)。这种效果与最大分离和最大凝聚的双重原则严格相关。沃德的联系倾向于很快找到最有凝聚力和分离的集群。当相异度差距超过预定阈值时(当然,当达到期望的聚类数时),它允许切割树形图,而其他链接需要不同的方法,有时会产生不期望的最终配置。

考虑到问题的性质,我总是鼓励大家去检验所有联动方法的行为,找出最适合一些样本场景的方法(例如,根据教育水平、入住率和收入对一个国家的人口进行细分)。这是提高意识和提高提供过程语义解释能力的最佳方法(这是任何聚类过程的基本目标)。

作为性能度量的共亨相关性

可以使用前面章节中介绍的任何方法来评估分层聚类性能。然而,在这种特殊情况下,可以采用特定的措施(不需要基本事实)。给定一个邻近矩阵、 P 和一个链接、 L 、几个样本、 x ix j ∈ X 总是被分配给某个层次级别的同一聚类。当然,重要的是要记住,在聚集场景中,我们从 n 个不同的集群开始,最终得到一个相当于 X 的集群。此外,当两个合并的聚类成为单个聚类时,属于一个聚类的两个样本将总是继续属于同一扩大的聚类,直到该过程结束。

考虑到上一节中显示的第一个树形图,样本{1}和{3}会立即合并;然后添加样本{2},接着是{11}。此时,整个集群与另一个块(包含样本{0}、{9}、{4}和{10})合并。在最后一级,合并剩余的样本以形成单个最终聚类。因此,命名不同等级 DL 0DL 1 ,...,和 DL k ,样品{1}和{3}在 DL 1 开始属于同一个聚类,而例如在 DL 6 发现{2}和{1}在同一个聚类中。

此时,我们可以将 DL ij 定义为 x ix j 第一次属于同一个聚类的相异度,以下( n × n )矩阵中共亨 矩阵 表示为 CP :

换句话说, CP ij 元素是观察同一簇中的xIT7】和xjT11】所需的最小相异度。有可能证明 CP ijx ix j 之间的距离度量,;因此, CPP 类似,具有相似矩阵的相同性质(例如所有对角元素都为空)。特别是,我们对它们的相关性感兴趣(在 -11 范围内归一化)。这个值(共病相关系数 ( CPC )表示 PCP 之间的一致程度,可以很容易地计算出来,如下式所示。**

由于 PCP 都是( n × n )对角元素为零的对称矩阵,因此可以只考虑包含 n(n-1)/2 值的下三角部分(不包括对角线,表示为Tril())。因此,平均值如下:

标准化的平方和值如下:

因此,标准化的共病相关性简单地等于如下:

前面的等式是基于这样的假设,如果三个样本, x ix jx p 具有诸如 d(x i 、xj)<d(xI,x p ) , 有理由期待 x ix j 会在xIT33】和xT37】p之前合并到同一个集群中(即 x ix 合并对应的相异程度 因此, CPC → 1 表示联动生成了一个最优的层次,反映了底层的几何。另一方面, CPC → -1 表示完全不一致和与几何不一致的潜在聚类结果。不言而喻,给定一个问题,我们的目标是找到一个最大化 CPC 的指标和联系。

考虑到第三章高级聚类中描述的例子,我们可以使用 SciPy 函数cophenet计算共亨矩阵和对应于不同连杆(假设欧几里德距离)的共亨矩阵。该函数要求链接矩阵作为第一个参数,邻近矩阵作为第二个参数,并返回共亨矩阵和 CPC(变量dm是先前计算的浓缩邻近矩阵):

from scipy.cluster.hierarchy import linkage, cophenet

cpc, cp = cophenet(linkage(dm, method='ward'), dm)
print('CPC Ward\'s linkage: {:.3f}'.format(cpc))

cpc, cp = cophenet(linkage(dm, method='single'), dm)
print('CPC Single linkage: {:.3f}'.format(cpc))

cpc, cp = cophenet(linkage(dm, method='complete'), dm)
print('CPC Complete linkage: {:.3f}'.format(cpc))

cpc, cp = cophenet(linkage(dm, method='average'), dm)
print('CPC Average linkage: {:.3f}'.format(cpc))

这个片段的输出如下所示:

CPC Ward's linkage: 0.775
CPC Single linkage: 0.771
CPC Complete linkage: 0.779
CPC Average linkage: 0.794

这些值非常接近,表明所有的联系都产生了相当好的结果(即使它们不是最佳的,因为存在两个异常值)。然而,如果我们需要选择一种方法,平均联系是最准确的,如果没有跳过它的具体原因,应该优先于其他联系。

共亨相关性是层次聚类特有的评估指标,通常提供可靠的结果。然而,当几何形状更复杂时,CPC 值可能会产生误导,并导致次优配置。出于这个原因,我总是建议使用其他指标(例如,轮廓分数或调整后的兰德分数),以便再次检查性能并做出最合适的选择。

水处理厂数据集上的凝聚聚类

现在让我们在一个更大的数据集上考虑一个更详细的问题(下载说明在本章开头的技术要求部分提供),该数据集包含 527 个样本,38 个描述水处理厂状态的化学和物理变量。正如同一批作者(贝哈尔、科尔特斯和波奇)所言,该领域结构不佳,需要仔细分析。同时,我们的目标是用不可知的方法找到最优聚类;换句话说,我们不会考虑语义标注过程(需要领域专家),而只考虑数据集的几何结构和凝聚算法发现的关系。

一旦下载,CSV 文件(称为water-treatment.data)就可以使用熊猫加载(当然,术语<DATA_PATH>必须更改,以便指向文件的确切位置)。第一列是与特定植物相关的索引,而所有其他值都是数字,可以转换为float64。缺少的值用'?'字符表示,由于我们没有任何其他信息,它们用每个属性的平均值来设置:

import pandas as pd

data_path = '<DATA_PATH>/water-treatment.data'

df = pd.read_csv(data_path, header=None, index_col=0, na_values='?').astype(np.float64)
df.fillna(df.mean(), inplace=True)

由于单个变量具有非常不同的大小(我邀请读者使用数据框上的describe函数检查这一陈述),最好在范围( -1,1 )内对它们进行归一化,保留原始方差:

from sklearn.preprocessing import StandardScaler

ss = StandardScaler(with_std=False)
sdf = ss.fit_transform(df)

此时,像往常一样,我们可以使用 t-SNE 算法将数据集投影到二维空间:

from sklearn.manifold import TSNE

tsne = TSNE(n_components=2, perplexity=10, random_state=1000)
data_tsne = tsne.fit_transform(sdf)

df_tsne = pd.DataFrame(data_tsne, columns=['x', 'y'], index=df.index)
dff = pd.concat([df, df_tsne], axis=1)

下面的截图显示了结果图:

t-SNE plot of the Water Treatment Plant dataset

该图显示了一个潜在的非凸几何形状,其中许多小(密集区域)由空白空间分隔。然而,如果没有任何领域信息,就不太容易决定哪些 blobs 可以被认为是同一个集群的一部分。我们可以决定强加的唯一伪约束(考虑到所有工厂都以类似的方式运行)是拥有中等或较小的最终集群数量。因此,假设欧几里德距离并使用 scikit-learn AgglomerativeClustering类,我们可以计算所有连杆机构的共亨相关性和轮廓得分以及集群的46810数量:

import numpy as np

from sklearn.cluster import AgglomerativeClustering
from sklearn.metrics import silhouette_score

from scipy.spatial.distance import pdist
from scipy.cluster.hierarchy import linkage, cophenet

nb_clusters = [4, 6, 8, 10]
linkages = ['single', 'complete', 'ward', 'average']

cpcs = np.zeros(shape=(len(linkages), len(nb_clusters)))
silhouette_scores = np.zeros(shape=(len(linkages), len(nb_clusters)))

for i, l in enumerate(linkages):
    for j, nbc in enumerate(nb_clusters): 
        dm = pdist(sdf, metric='minkowski', p=2)
        Z = linkage(dm, method=l)
        cpc, _ = cophenet(Z, dm)
        cpcs[i, j] = cpc

        ag = AgglomerativeClustering(n_clusters=nbc, affinity='euclidean', linkage=l)
        Y_pred = ag.fit_predict(sdf)
        sls = silhouette_score(sdf, Y_pred, random_state=1000)
        silhouette_scores[i, j] = sls

相应的图如下图所示:

Cophenetic correlation (left) and silhouette score (right) for a different number of clusters and four linkage methods

需要考虑的第一个因素是,共基因相关性对于完全和平均连锁来说是合理可接受的,而对于单一连锁来说太低了。考虑到剪影分数,最大值(约 0.6)是通过单个链接和四个集群实现的。该结果表明,即使分层算法产生次优配置,四个区域也可以用中等或高水平的内部内聚性来分离。

如前一节所述,共基因关联有时可能会产生误导,在这种情况下,我们可以得出结论,如果潜在集群的理论数量为四个,则使用单链是最佳选择。然而,所有其他图表显示了对应于完整链接的最大值(以及单个图表的最小值)。因此,要回答的第一个问题是:我们需要偶数簇吗?在这个例子中,让我们假设许多工厂以非常标准的方式运行(许多样本共享差异),但是也可能有一些特殊的情况(不适当的异常值)会表现出非常不同的行为。

这种假设在许多情况下是现实的,可能是由于创新或实验过程、缺乏资源、测量过程中的内部问题等。领域专家可以确认或拒绝我们的假设,但是,由于这是一个通用的例子,我们可以决定保留八个具有完全链接的集群(轮廓分数约为 0.5)。该值表示存在重叠,但考虑到数据集的维度和非凸性,它在许多实际情况下是可以接受的。

此时,我们还可以分析截断到 80 片叶子的树形图(这可以通过设置trucate_mode='lastp'参数和p=80来实现),以避免间隔过小和难以区分(不过,可以去掉这个约束,提高分辨率):

Dendrogram of the Water Treatment Plant dataset with the Euclidean metric and complete linkage

我们可以看到,凝聚过程是不均匀的。在该过程的开始,相异度增加得相当慢,但是在大约对应于 10,000 的值之后,跳跃变得更大。看一下 t-SNE 图,可以理解非凸性的影响对非常大的星系团有更强的影响,因为密度降低了,隐含地,相异度增加了。很简单,极少数集群(例如 1、2 或 3)的特点是内部差异非常大,内聚性非常低。

此外,树图显示,在大约 17,000 的水平上有两个主要的不均匀聚集,因此我们可以推断粗粒度分析突出了一个主要行为(从顶部看图)和一个次要行为的存在,由较少数量的植物使用。特别是,较小的组非常稳定,因为它将以大约 50,000 的相异度合并到最终的单个集群中。因此,我们应该预期伪异常值的存在,这些伪异常值被分组到更孤立的区域中(这也由 t-SNE 图所证实)。

在 4,000 ÷ 6,000 的范围内切割(对应于大约八个簇),较大的块比较小的块更密集。换句话说,离群点聚类包含的样本将比其他聚类少得多。这并不奇怪,因为正如分析树状图一节所讨论的,最远的聚类通常在完全连锁中合并得很晚。

此时,我们最终可以执行聚类并检查结果。Scikit-learn 的实现并不计算整个树形图,而是在达到期望的聚类数时停止该过程(除非compute_full_tree参数不是True):

import pandas as pd

from sklearn.cluster import AgglomerativeClustering

ag = AgglomerativeClustering(n_clusters=8, affinity='euclidean', linkage='complete')
Y_pred = ag.fit_predict(sdf)

df_pred = pd.Series(Y_pred, name='Cluster', index=df.index)
pdff = pd.concat([dff, df_pred], axis=1)

最后的图显示在下面的截图中:

Clustering result of the Water Treatment Plant dataset (eight clusters)

不出所料,这些簇是不均匀的,但它们与几何形状相当一致。此外,孤立的聚类(例如,在区域x∑(-40,-20)y > 60 中)非常小,并且它们非常可能包含真正的异常值,这些异常值的行为与大多数其他样本非常不同。我们不打算分析语义,因为问题非常具体。然而,有理由认为x∞(-40,40)y∞(-40,-10) 区域的大星系团,尽管是非凸的,但代表了一个合适的基线。相反,其他大块(在这个集群的极端)对应于具有特定属性或行为的植物,这些属性或行为足够分散,可以被认为是标准的替代实践。当然,正如开头提到的,这是一个不可知论的分析,应该有助于理解如何使用分层聚类。

作为最后一步,我们希望在大约 35,000(对应于两个聚类)的相异度水平上切割树图。结果如下图所示:

Clustering result of the Water Treatment Plant dataset (two clusters)

在这个级别上,树图显示了属于一个集群和剩余较小区块的大部分样本。现在我们知道这样的二级区域对应x∑(-40,10)y > 20 。同样,结果并不令人惊讶,因为 t-SNE 图显示,这些样本是唯一具有 y > 20 ÷ 25 的样本(而更大的星系团,即使有巨大的空白区域,也几乎覆盖了所有的范围)。

因此,我们可以说这样的样本代表了具有极端行为的非常不同的植物,并且,如果一个新的样本被分配到那个集群,它可能是一个非标准植物(假设一个标准植物具有与其大多数同类相似的行为)。作为练习,我鼓励你测试其他数量的集群和不同的链接(特别是单个链接,这是非常特殊的),并尝试验证或拒绝一些样本,先前的假设(它们没有必要在物理上是可接受的)。

连通性限制

凝聚层次聚类的一个重要特征是可以包含连接性约束来强制合并特定的样本。这种先验知识在邻居之间存在强关系的上下文中或者当我们知道某些样本由于其内在属性而必须属于同一聚类时非常常见。为了实现这个目标,我们需要使用一个连接矩阵,A∑{ 0,1} n × n :

一般来说, A 是数据集的一个图诱导的邻接矩阵;然而,唯一重要的要求是没有孤立的样本(没有连接),因为它们不能以任何方式合并。连通性矩阵在初始合并阶段应用,并强制算法聚集指定的样本。由于以下聚集不会影响连通性(两个合并的样本或聚类将保持合并状态,直到过程结束),因此约束总是被强制执行。

为了理解这个过程,让我们考虑一个包含从8二元高斯分布提取的50二维点的样本数据集:

from sklearn.datasets import make_blobs

nb_samples = 50
nb_centers = 8

X, Y = make_blobs(n_samples=nb_samples, n_features=2, center_box=[-1, 1], centers=nb_centers, random_state=1000)

下图显示了标记的数据集:

Dataset for connectivity constraints example

看剧情,我们看到样本 18 和 31(x0∑(-2,-1)x1∑(1,2) )相当接近;然而,我们不希望它们被合并,因为样本 18 在大的中心斑点中有更多的邻居,而点 31 是部分隔离的,应该被认为是一个自治集群。我们还希望样本 33 形成单个集群。这些要求将迫使算法合并不再考虑底层几何的聚类(根据高斯分布),而是考虑先验知识。

为了检查聚类是如何工作的,现在让我们使用欧几里德距离和平均链接来计算树图(被截断为 20 片叶子):

from scipy.spatial.distance import pdist
from scipy.cluster.hierarchy import linkage, dendrogram

dm = pdist(X, metric='euclidean')
Z = linkage(dm, method='average')

fig, ax = plt.subplots(figsize=(20, 10))

d = dendrogram(Z, orientation='right', truncate_mode='lastp', p=20, ax=ax)

ax.set_xlabel('Dissimilarity', fontsize=18)
ax.set_ylabel('Samples', fontsize=18)

树形图(从右到左)如下图所示:

Dendrogram for the connectivity constraints example with the Euclidean distance and average linkage

如预期的那样,样本 18 和 31 被立即合并,然后与包含 2 个样本的另一个集群聚合(当数字在括号之间时,这意味着它是包含更多样本的复合块),这可能是 44 和 13。样本 33 也被合并,所以它不会留在一个孤立的集群中。作为确认,我们用n_clusters=8进行聚类:

from sklearn.cluster import AgglomerativeClustering

ag = AgglomerativeClustering(n_clusters=8, affinity='euclidean', linkage='average')
Y_pred = ag.fit_predict(X)

以下屏幕截图显示了群集数据集的图:

Dataset clustered using the Euclidean distance and average linkage

结果证实了前面的分析。在没有约束的情况下,平均关联产生了与基本事实(八个高斯分布)兼容的合理划分。为了分割大的中心斑点并保持所需数量的聚类,算法还必须合并孤立的样本,即使树形图确认它们在末端以最高的相异度被合并。

为了施加我们的约束,我们可以观察到,基于前两个最近邻居的连通性矩阵很可能会强制聚合属于更密集区域的所有样本(考虑到邻居更近),并最终将孤立点保留在自治集群中。这种假设行为的原因是基于平均链接的目标(最小化集群间的平均距离)。因此,在施加约束之后,该算法更倾向于聚集具有其他邻居的接近的聚类(记住 A 具有空值,但是在对应于两个最近邻居的位置)并且不聚集最远的点,直到相异度足够大(产生非常不均匀的聚类)。

为了检查我们的假设是否正确,让我们使用 scikit-learn、kneighbors_graph()函数和n_neighbors=2生成一个连通性矩阵,并重新聚集数据集,设置connectivity约束:

from sklearn.cluster import AgglomerativeClustering
from sklearn.neighbors import kneighbors_graph

cma = kneighbors_graph(X, n_neighbors=2)

ag = AgglomerativeClustering(n_clusters=8, affinity='euclidean', linkage='average', connectivity=cma)
Y_pred = ag.fit_predict(X)

上一个片段的图形输出显示在下面的屏幕截图中:

Dataset clustered using the Euclidean distance and average linkage using connectivity constraints

不出所料,样本 18 已被分配到大的中央集群,而点 31 和 33 现在被隔离。当然,由于过程是分层的,所以施加连接性约束比分离性约束更容易。事实上,虽然单个样本在初始阶段可以很容易地合并,但在最终合并之前,不能很容易地保证使用所有链接来排除它们。

当需要复杂的约束时(给定一个距离和一个链接),通常需要调整连接矩阵和期望的集群数量。当然,如果期望的结果是用特定数量的聚类实现的,那么它也将用更大的值实现,直到相异度下限(即,合并过程减少了聚类的数量;因此,如果相异度足够大,所有现有约束将保持有效)。例如,如果三个样本被约束为属于同一个聚类,我们通常不能期望在初始合并阶段之后出现这种结果。

但是,如果所有三个样本的合并发生在某个不同的级别(例如,2.0 对应于 30 个集群),它也将对 n < 30 集群和所有具有 DL > 2.0 的配置保持有效。因此,如果我们从 5 个集群开始,我们可以很容易地增加这个数量,同时注意具有比约束所施加的最后一次合并所对应的不同级别更大的不同级别。我鼓励您用其他数据集测试这种方法,并尝试定义可以在聚类过程后轻松验证的先验约束。

摘要

在这一章中,我们介绍了层次聚类方法,重点是可以使用的不同策略(分裂和聚集策略)。我们还讨论了用于发现哪些集群可以合并或拆分(链接)的方法。特别是,给定一个距离度量,我们分析了四种链接方法的行为:单一、完整、平均和沃德方法。

我们已经展示了如何构建一个树形图,以及如何分析它,以便使用不同的链接方法来理解整个分层过程。引入了一种称为共亨蒂奇相关的特定性能度量,以在不了解基本事实的情况下评估分层算法的性能。

我们分析了一个更大的数据集(水处理厂数据集),定义了一些假设,并使用前面讨论的所有工具验证了它们。在这一章的最后,我们讨论了连通性约束的概念,它允许使用连通性矩阵将先验知识引入到过程中。

在下一章中,我们将介绍软聚类的概念,重点介绍一种模糊算法和两种非常重要的高斯混合模型。

问题

  1. 凝聚方法和分裂方法有什么区别?
  2. 给定两个簇 a: [(-1,-1),(0,0)]b: [(1,1),(1,0)] ,考虑欧氏距离的单个完整的连杆机构有哪些?
  3. 树形图表示给定数据集的不同链接结果。这是正确的吗?
  4. 在聚集聚类中,树形图的底部(初始部分)包含单个聚类。这是正确的吗?
  5. 聚类分析中树形图的 y 轴是什么意思?
  6. 当合并更小的簇时,相异度减小。这是正确的吗?
  7. 共亨矩阵的元素 C(i,j) 报告了两个对应元素xIT5】和xjT9】首次出现在同一簇中的不同程度。这是正确的吗?**
  8. 连接限制的主要目的是什么?

进一步阅读

  • 评价系统关系的统计方法索卡尔 R.麦切纳 C.堪萨斯大学科学通报,38,1958
  • 优化目标函数的分层分组沃德 JrJ. H.美国统计协会杂志。58(301), 1963
  • LINNEO+:一种针对不良结构域的分类方法贝哈尔 J.科尔特斯 U.波奇 M.研究报告 RT-93-10-R .科学信息学部巴塞罗纳,1993
  • 机器学习算法,第二版博纳科尔索集团帕克特出版,2018

五、软聚类和高斯混合模型

在本章中,我们将讨论软聚类的概念,它允许我们获得数据集的每个样本相对于定义的聚类配置的隶属度。也就是说,考虑从 0%到 100%的范围,我们想知道xIT3 在多大程度上属于一个集群。极值为 0,表示 x i 完全在集群的域外,为 1 (100%),表示 x i 完全分配给单个集群。所有中间值都意味着两个或多个不同簇的部分区域。因此,与硬聚类相反,在这里,我们感兴趣的不是确定一个固定的赋值,而是一个具有概率分布(或概率本身)相同属性的向量。这种方法允许对边界样本进行更好的控制,并帮助我们找到生成过程的合适近似,数据集应该从该生成过程中提取。

特别是,我们将讨论以下主题:

  • 模糊 C 均值
  • 高斯混合
  • AIC 和 BIC 作为绩效指标
  • 贝叶斯高斯混合(简介)
  • 生成(半监督)高斯混合

技术要求

本章将介绍的代码需要以下内容:

  • Python 3.5+ ( Anaconda 发行版强烈推荐)
  • 以下库:
    • SciPy 0.19+
    • NumPy 1.10+
    • 学习 0.20+
    • Scikit-fuzzy 0.2
    • 熊猫 0.22+
    • Matplotlib 2.0+
    • seaborn 0.9+

这些例子可以在 GitHub 资源库中找到,网址为 https://GitHub . com/packktpublishing/HandsOn-Unsupervised-Learning-with-Python/tree/master/chapter 05

软聚类

第 4 章行动中的层次聚类中分析的所有算法都属于硬聚类方法家族。这意味着一个给定的样本总是被分配给一个单独的聚类。另一方面,软聚类的目的是将每个样本, x i 与一个向量相关联,一般表示 x i 属于每个聚类的概率:

或者,输出可以解释为隶属向量:

在形式上,这两个版本没有区别,但通常情况下,当算法不是明确基于概率分布时,采用后者。然而,出于我们的目的,我们总是将 c(x i ) 与概率联系起来。这样,读者就会被激励去思考用来获取数据集的数据生成过程。一个明显的例子是将这些向量解释为与构成数据生成过程近似值的特定贡献相关的概率, p data 。例如,采用概率混合,我们可以决定近似 p 数据 如下:

因此,该过程被分解成(独立的)分量的加权和,输出是每一个分量的 x i 的概率。当然,我们通常期望每个样本都有一个主导成分,但是通过这种方法,我们对所有边界点都有很大的了解,这些边界点会受到小的扰动,可以分配给不同的聚类。由于这个原因,当输出可以被馈送到另一个可以利用整个概率向量的模型(例如,神经网络)中时,软聚类非常有帮助。例如,推荐者可以首先使用软聚类算法来分割用户,然后处理向量,以便基于显式反馈找到更复杂的关系。一个常见的场景是通过回答诸如“这个结果与你相关吗?”或者“你想看到更多像这样的结果吗?”因为答案是由用户直接提供的,所以它们可以被用在监督或强化学习模型中,该模型的输入基于软自动分割(例如,基于购买历史或详细的页面视图)。通过这种方式,可以通过改变原始分配的效果来轻松管理边缘用户(由于不同集群提供的大量贡献,这可能完全不重要),同时可以稍微修改对其他具有强成员资格(例如,概率接近 1)的用户的推荐,以提高他们的回报。

我们现在可以开始讨论 Fuzzy c-means,这是一种非常灵活的算法,它将针对 k-means 讨论的概念扩展到软聚类场景。

模糊 c 均值

我们将提出的第一个算法是基于软分配的 k-means 的变体。 Fuzzy c-means 这个名字来源于一个模糊集的概念,它是经典二进制集(即在这种情况下,一个样本可以属于一个单一的聚类)基于代表整个集合不同区域的不同子集的叠加而扩展到集合。例如,基于一些用户年龄的集合可以具有与三个不同(并且部分重叠)年龄范围相关联的程度youngadultsenior:18-35 岁、28-60 岁和> 50 岁。因此,例如,一个 30 岁的用户在不同程度上既是young又是adult(考虑到界限,确实是一个边缘用户)。关于这类集合和所有相关运算的更多细节,我建议阅读《概念和模糊逻辑》一书,麻省理工学院出版社,2011 年。出于我们的目的,我们可以将包含 m 个样本的数据集 X 划分为 k 个重叠的聚类,这样每个样本总是与每个聚类相关联,根据隶属度,wijT18】(该值绑定在 0 和 1 之间)。如果 w ij = 0 ,则表示 x i 完全在集群CjT30】之外,反之,则wijT35】= 1表示对集群CjT40】的硬分配。所有中间值代表部分成员资格。当然,由于显而易见的原因,样本的所有隶属度之和必须归一化为 1(类似于概率分布)。这样,一个样本总是属于所有聚类的并集,而将一个聚类分成两个或更多的子聚类总是会产生一个连贯的结果,就成员而言。**

该算法基于广义惯性的优化,SfT3:

在之前的公式中,μjT3】是星团CjT7】的质心,而 m (m > 1) 是重加权指数系数。当 m ≈ 1 时,重量不受影响。数值越大,如wij∑(0,1) ,其重要性成比例降低。可以选择这样的系数来比较不同值的结果和期望的模糊程度。事实上,在每次迭代之后(完全等同于 k 均值),使用以下公式更新权重:**

如果 x i 接近质心, μ j ,则和变得接近 0,权重递增(当然,为了避免数值不稳定,在分母上加了一个小常数,所以永远不能等于 0)。当 m > > 1 时,指数变得接近 0,总和中的所有项都趋于 1。这意味着对特定集群的偏好减弱,并且 w ij ≈ 1/k 对应于均匀分布。因此,更大的 m 意味着更平坦的划分,在不同的分配之间没有明显的差异(除非样本非常接近质心),而当 m ≈ 1 时,单个主导权重将几乎等于 1,而其他权重将接近 0(即分配是困难的)。

质心以类似于 k 均值的方式更新(换句话说,目标是最大化分离和内部凝聚力):

重复该过程,直到质心和权重变得稳定。收敛后,可以用一个特定的度量来评估结果,称为归一化的邓恩分割系数,定义如下:

这样的系数被限制在 0 和 1 之间。当 P C ≈ 0 时,表示 w C ≈ 1/k ,暗示分布平坦,模糊程度高。另一方面,当 P C ≈ 1 时,则 w C ≈ 1 ,表示几乎是硬性作业。所有其他值都与模糊程度成正比。因此,给定一项任务,数据科学家可以根据期望的结果立即评估算法的执行情况。在某些情况下,硬分配更可取,因此, P C 可以被认为是在切换到例如标准 k 均值之前要执行的检查。其实当 P C ≈ 1 (而且这样的结果是预期的结果)的时候,再用 Fuzzy c-means 就没有意义了。相反,小于 1 的值(例如, P C = 0.5 )告知我们,由于存在许多边界样本,可能会有非常不稳定的硬性分配。

现在,让我们将模糊 c 均值算法应用于 scikit-learn 提供的简化 MNIST 数据集。该算法由 Scikit-Fuzzy 库提供,它实现了所有最重要的模糊逻辑模型。第一步是加载和标准化样本,如下所示:

from sklearn.datasets import load_digits

digits = load_digits()
X = digits['data'] / 255.0
Y = digits['target']

X数组包含 1,797 个展平样本, x ∈ ℜ 64 ,对应灰度 8 × 8 图像(其值在 0 和 1 之间归一化)。我们要分析不同 m 系数(1.05 到 1.5 之间的 5 个统一值)的行为,并检查样本的权重(在我们的例子中,我们将使用X[0])。因此,我们调用 Scikit-Fuzzy cmeans函数,设置c=10(簇的数量)和两个收敛参数,error=1e-6maxiter=20000。而且,出于重现性的原因,我们也会随机设置一个标准seed=1000。输入数组应包含列形式的样本;因此,我们需要转置它,如下所示:

from skfuzzy.cluster import cmeans

Ws = []
pcs = []

for m in np.linspace(1.05, 1.5, 5):
    fc, W, _, _, _, _, pc = cmeans(X.T, c=10, m=m, error=1e-6, maxiter=20000, seed=1000)
    Ws.append(W)
    pcs.append(pc)

前面的片段执行不同类型的聚类,并将相应的权重矩阵W和划分系数pc附加到两个列表中。在分析特定配置之前,显示测试样本(代表数字 0)的最终重量(对应于每个数字)会很有帮助:

Weights (in an inverse logarithmic scale) for the sample X[0], corresponding to different m values

由于极值往往差异很大,我们选择了使用对数反比标度(即 -log(w 0j ) 而不是 w 0 j )。当 m = 1.05P C 约为 0.96,所有重量(除了与 C 2 对应的重量)都很小时(记住如果 -log(w) = 30 ,那么 w = e -30 )。这样的配置清楚地显示了具有主导成分的非常硬的聚类( C 2 )。上图后续三个地块继续呈现优势,但是,当 m 增加(而PCT35】减少)时,优势成分和次优势成分的差异越来越小。这种效果证实了模糊度的增加,对于 m > 1.38 达到最大值。事实上,当 m = 1.5 时,即使 P C ≈ 0.1 ,所有的权重几乎是相同的,测试样本也不容易被分配到优势聚类。正如我们之前所讨论的,我们现在知道像 k-means 这样的算法可以很容易地找到硬划分,因为平均来说,对应于不同数字的样本彼此之间有很大的不同,并且欧几里德距离足以将它们分配到正确的质心。在这个例子中,我们希望保持适度的模糊性;因此,我们选择了 m = 1.2 (对应PC≈0.73):

fc, W, _, _, _, _, pc = cmeans(X.T, c=10, m=1.2, error=1e-6, maxiter=20000, seed=1000)
Mu = fc.reshape((10, 8, 8)) 

Mu数组包含质心,如下图所示:

Centroids corresponding to m = 1.2 and PC ≈ 0.73

可以看到,所有不同的数字都被选中,不出所料,第三组(由C2T5】表示)对应数字 0。现在我们来检查一下X[0]对应的权重(同样, W 是换位的,所以存储在W[:, 0]中):

print(W[:, 0])

输出如下:

[2.68474857e-05 9.14566391e-06 9.99579876e-01 7.56684450e-06
 1.52365944e-05 7.26653414e-06 3.66562441e-05 2.09198951e-05
 2.52320741e-04 4.41638611e-05]

即使作业不是特别辛苦,集群CT5【2】T6的统治力还是显而易见的。第二个潜在赋值是 C 8 ,对应数字 9(比例约为 4000)。这样的结果与数字的形状绝对一致,而且考虑到最大重量和第二个重量之间的差异,很明显大多数样本几乎不会被赋值(也就是说,就像在 k 均值中一样),即使有 P C ≈ 0.75 。为了检查硬分配(使用权重矩阵上的argmax函数获得)的性能,并考虑到我们知道基本事实,我们可以使用adjusted_rand_score,如下所示:

from sklearn.metrics import adjusted_rand_score

Y_pred = np.argmax(W.T, axis=1)

print(adjusted_rand_score(Y, Y_pred))

上一个片段的输出如下:

0.6574291419247339

这样的值证实了大多数样本已经被成功硬分配。作为补充练习,让我们找出权重标准差最小的样本:

im = np.argmin(np.std(W.T, axis=1))

print(im)
print(Y[im])
print(W[:, im])

输出如下:

414
8
[0.09956437 0.05777962 0.19350572 0.01874303 0.15952518 0.04650815
 0.05909216 0.12910096 0.17526108 0.06091973]

样本X【414】代表一个数字(8),如下图截图所示:

Plot of the sample, X[414], corresponding to the weight vector with the smallest standard deviation

在这种情况下,有三个优势集群: C 8C 4C 7 (按降序排列)。不幸的是,它们都不对应数字 8,数字 8 与C5T15 相关联。不难理解,这样的错误主要是由于数字下半部分的畸形,这产生了更类似于 9 的结果(这样的错误分类也可能发生在人类身上)。然而,低标准偏差和缺乏明显的主导成分应该告诉我们,决策不容易做出,样本具有属于三个主要类别的特征。一个更复杂的监督模型可以很容易地避免这个错误,但是考虑到我们正在执行一个无监督的分析,并且我们仅仅使用基础事实来进行评估,结果并不是那么负面。我建议你用其他 m 值来检验结果,并尝试找出一些潜在的组成规则(即 8 个数字的大部分被柔和地赋给 C iC j ,这样我们就可以假设对应的质心编码了部分共有的特征,例如被所有 8 个和 9 个数字共享)。

我们现在可以讨论高斯混合的概念,这是一种非常广泛使用的建模数据集分布的方法,其特征是由低密度区域包围的密集斑点。

高斯混合

高斯混合是最著名的软聚类方法之一,有几十种具体的应用。它可以被认为是 k-means 之父,因为它的工作方式非常相似;但是,与该算法相反,给定样本 x i ∈ Xk 聚类(其被表示为高斯分布),它提供概率向量【p(XI**∈C1),...,p(xI∈Ck)】

更一般地说,如果数据集 X 是从数据生成过程 p 数据T5】中采样的,则高斯混合模型基于以下假设:

换句话说,数据生成过程由多元高斯分布的加权和来近似。这种分布的概率密度函数如下:

每个多元高斯的每个分量的影响取决于协方差矩阵的结构。下图显示了二元高斯分布的三种主要可能性(结果可以很容易地扩展到 n 维空间):

Full covariance matrix (left); diagonal covariance (center); circular/spherical covariance (right)

从现在开始,我们将一直考虑全协方差矩阵的情况,这允许实现最大的表达能力。很容易理解,当这样的分布是完全对称的(即协方差矩阵是圆形/球形)时,伪聚类的形状与 k 均值相同(当然,在高斯混合中,聚类没有边界,但在固定数量的标准差后,总是有可能剪切高斯)。相反,当协方差矩阵不是对角的或具有不同的方差时,影响不再对称(例如,在二元的情况下,一个分量可能比另一个分量显示更大的方差)。在这两种情况下,高斯混合允许我们计算实际概率,而不是测量样本之间的距离, x i ,和均值向量, μ j (如 k 均值)。下图显示了一个单变量混合的示例:

Example of a univariate Gaussian mixture

在这种情况下,每个样本在每个高斯下总是有一个非空概率,其影响由其均值和协方差矩阵决定。例如,对应于 x 位置 2.5 的点既可以属于中心高斯,也可以属于右侧高斯(而左侧高斯的影响最小)。正如本章开头所解释的,任何软聚类算法都可以通过选择影响最大的组件(argmax)而转变为硬聚类算法。

您将立即理解,在这种特定情况下,通过对角协方差矩阵,argmax提供了一条附加信息(被 k 均值完全丢弃),可以在进一步的处理步骤中使用(也就是说,推荐器应用程序可以提取所有聚类的主要特征,并根据相对概率重新加权它们)。

高斯混合模型的电磁算法

完整的算法(在掌握机器学习算法中有充分描述,作者:Bonaccorso G .,Packt Publishing,2018)比 k-means 稍微复杂一点,需要更深层次的数学知识。由于这本书的范围更实用,我们只讨论主要步骤,没有提供正式证据。

让我们首先考虑一个数据集, X ,包含 n 个样本:

给定 k 分布,我们需要找到权重,wjT5】,以及每个高斯 j ,σj)的参数,条件如下:

最后一个条件对于保持与概率定律的一致性是必要的。如果我们将所有参数分组为一个集合, θ j = (w j 、μ j 、σj),我们可以定义样本xIT13】在jT16】th 高斯下的概率,如下:

类似的,我们可以引入一个伯努利分布,zIT3】j= p(j | xI,θj)∞B(p),这是jT12】th 高斯生成样本xIT17】的概率。换句话说,给定一个样本, x i ,z ij 将等于 1,概率 p(j|x i ,θ j ) ,否则为 0。

此时,我们可以计算整个数据集的联合对数似然,如下所示:

在前面的公式中,我们利用了指数指标表示法,它依赖于这样一个事实,即zijT3 只能是 0 或 1。因此,当 z ij = 0 时,意味着样品 x i 还没有由 j th 高斯生成,产品中对应的项变为 1(即 x 0 = 1 )。反之,当 z ij = 1 时,该项等于xIT27】和jT30】th 高斯的联合概率。因此,联合对数似然是模型生成整个数据集的联合概率,假设每个 x i ∈ X独立同分布 ( IID )。要解决的问题是最大似然估计 ( MLE ),或者换句话说,寻找最大化L(θ;x,Z) 。但是变量 z ij 并没有被观察到(或者潜伏),所以不可能直接最大化可能性,因为我们不知道它们的值。**

解决这一问题的最有效方法是采用 em 算法(由 Dempster A. P,Laird N. M .和 Rubin D. B .提出,通过 EM 算法从不完整数据中获得最大似然,皇家统计学会杂志,系列 B. 39 (1),1977)。完整的解释超出了本书的范围,但我们想提供主要步骤。首先要做的是使用概率链规则,以便将前面的表达式转换为条件概率的总和(这可以很容易地管理):

这两种可能性现在很简单。术语 p(x i |j,θ j )jT12】th 高斯下的xIT9】的概率,而 p(j|θ j ) 只是jT20】th 高斯下的概率,相当于权重,为了消除潜在的变量,EM 算法以迭代的方式进行,由两个步骤组成。第一个(称为期望步骤,或 E 步骤)是计算没有潜在变量的可能性的代理。如果我们将整个参数集表示为 θ ,并且在迭代 t 时计算的同一个参数集表示为θtT37】,我们可以计算出以下函数:

Q( θ|θ t ) 是关于变量 z ij 的联合对数似然的期望值,并被调节到数据集 X 和迭代时的参数集 t 。这种操作的效果是消除潜在的变量(这些变量被求和或积分出来),并产生实际对数似然的近似值。不难想象,第二步(称为最大化-步,或M-步)的目标是最大化 Q(θ|θ t ) ,生成一个新的参数集, θ t+1 。重复该过程,直到参数变得稳定,并且有可能证明最终参数集对应于最大似然估计。跳过所有中间步骤,假设最佳参数设置为 θ f ,最终结果如下:

为了清楚起见,概率 p(j|x i ,θ f ) 可以通过使用贝叶斯定理来计算:

比例性可以通过归一化所有项来消除,使得它们的和等于 1(满足概率分布的要求)。

现在,让我们考虑一个使用 scikit-learn 的实际例子。由于目标纯粹是说教,我们使用了一个易于可视化的二维数据集:

from sklearn.datasets import make_blobs

nb_samples = 300
nb_centers = 2

X, Y = make_blobs(n_samples=nb_samples, n_features=2, center_box=[-1, 1], centers=nb_centers, cluster_std=[1.0, 0.6], random_state=1000)

数据集是从两个具有不同标准差(1.0 和 0.6)的高斯分布中采样生成的,如下图所示:

Dataset for the Gaussian mixture example

我们的目标是使用高斯混合模型和 k 均值,并比较最终结果。正如我们预期的两个组成部分,数据生成过程的近似如下:

我们现在可以用n_components=2训练一个GaussianMixture实例。默认的协方差类型是完整的,但是可以通过设置covariance_type参数来更改该选项。允许的值是fulldiagsphericaltied(这迫使算法对所有高斯函数使用共享的单个协方差矩阵):

from sklearn.mixture import GaussianMixture

gm = GaussianMixture(n_components=2, random_state=1000)
gm.fit(X)
Y_pred = gm.fit_predict(X)

print('Means: \n{}'.format(gm.means_))
print('Covariance matrices: \n{}'.format(gm.covariances_))
print('Weights: \n{}'.format(gm.weights_))

上一个片段的输出如下:

Means: 
[[-0.02171304 -1.03295837]
 [ 0.97121896 -0.01679101]]

Covariance matrices: 
[[[ 0.86794212 -0.18290731]
  [-0.18290731  1.06858097]]

 [[ 0.44075382  0.02378036]
  [ 0.02378036  0.37802115]]]

Weights: 
[0.39683899 0.60316101]

因此,最大似然估计产生了两个分量,其中稍占优势的一个分量(即 w 2 = 0.6 )。为了知道高斯轴的方向,我们需要计算协方差矩阵的归一化特征向量(这个概念将在第 7 章降维和分量分析中充分解释):

import numpy as np

c1 = gm.covariances_[0]
c2 = gm.covariances_[1]

w1, v1 = np.linalg.eigh(c1)
w2, v2 = np.linalg.eigh(c2)

nv1 = v1 / np.linalg.norm(v1)
nv2 = v2 / np.linalg.norm(v2)

print('Eigenvalues 1: \n{}'.format(w1))
print('Eigenvectors 1: \n{}'.format(nv1))

print('Eigenvalues 2: \n{}'.format(w2))
print('Eigenvectors 2: \n{}'.format(nv2))

输出如下:

Eigenvalues 1: 
[0.75964929 1.17687379]
Eigenvectors 1: 
[[-0.608459   -0.36024664]
 [-0.36024664  0.608459  ]]

Eigenvalues 2: 
[0.37002567 0.4487493 ]
Eigenvectors 2: 
[[ 0.22534853 -0.6702373 ]
 [-0.6702373  -0.22534853]]

在两个二元高斯(一旦被截断并从顶部观察,可以想象成椭圆)中,主要成分是第二个(即第二列,对应于最大的特征值)。椭圆的偏心率由特征值之间的比值决定。如果这样的比例等于 1,形状就是圆,高斯是完全对称的;否则,它们会沿着一个轴拉伸。主要部件与 x 轴之间的角度(度)如下:

import numpy as np

a1 = np.arccos(np.dot(nv1[:, 1], [1.0, 0.0]) / np.linalg.norm(nv1[:, 1])) * 180.0 / np.pi
a2 = np.arccos(np.dot(nv2[:, 1], [1.0, 0.0]) / np.linalg.norm(nv2[:, 1])) * 180.0 / np.pi

之前的公式是基于主成分 v 1x -versor、e0T9】(即【1,0】)之间的点积:

在显示最终结果之前,使用 k 均值对数据集进行聚类将会很有帮助:

from sklearn.cluster import KMeans

km = KMeans(n_clusters=2, random_state=1000)
km.fit(X)
Y_pred_km = km.predict(X)

聚类结果如下图所示:

Gaussian mixture result (left) with the shapes of three horizontal sections; k-means result (right)

正如预期的那样,两种算法产生非常相似的结果,主要的差异是由于高斯分布的非对称性。特别地,对应于数据集左下部分的伪聚类在两个方向上具有较大的方差,并且对应的高斯是主导的。为了检查混合物的行为,让我们计算三个样本点的概率( (0,-2)(1,-1)—一个临界样本;和 (1,0) ,使用predict_proba()方法:

print('P([0, -2]=G1) = {:.3f} and P([0, -2]=G2) = {:.3f}'.format(*list(gm.predict_proba([[0.0, -2.0]]).squeeze())))
print('P([1, -1]=G1) = {:.3f} and P([1, -1]=G2) = {:.3f}'.format(*list(gm.predict_proba([[1.0, -1.0]]).squeeze())))
print('P([1, 0]=G1) = {:.3f} and P([1, 0]=G2) = {:.3f}'.format(*list(gm.predict_proba([[1.0, 0.0]]).squeeze())))

上一个块的输出如下:

P([0, -2]=G1) = 0.987 and P([0, -2]=G2) = 0.013
P([1, -1]=G1) = 0.354 and P([1, -1]=G2) = 0.646
P([1, 0]=G1) = 0.068 and P([1, 0]=G2) = 0.932

我邀请读者通过使用其他协方差类型来重复这个例子,然后用 k 均值来比较所有的硬赋值。

用 AIC 和 BIC 评估高斯混合模型的性能

由于高斯混合模型是一个概率模型,找到最佳的组件数量需要一种不同于前面几章分析的方法。最广泛使用的技术之一是阿卡克信息标准 ( AIC ),其基于信息论(首次在阿卡克首次提出,统计模型识别的新观点,IEEE 自动控制交易, 19 (6))。如果概率模型具有npT9】参数(即必须学习的单个值)并达到最大负对数似然, L opt ,则 AIC 定义如下:

这种方法有两个重要的含义。第一个是关于价值本身;AIC 越小,分数越高。事实上,考虑到奥卡姆剃刀原理,模型的目标是以最小的参数数量实现最优似然。第二个含义与信息论严格相关(我们不讨论细节,这些细节在数学上很重要),特别是数据生成过程和一般概率模型之间的信息损失。有可能证明 AIC 的渐近最小化(即当样本数趋于无穷大时)等价于信息损失的最小化。考虑基于不同数量的分量的几个高斯混合( n p 是所有权重、均值和协方差参数的总和),具有最小 AIC 的配置对应于以最高精度再现数据生成过程的模型。AIC 的主要局限在于小数据集。在这种情况下,AIC 倾向于在大量参数下达到最小值,这与奥卡姆剃刀原理形成对比。然而,在大多数现实生活中,AIC 提供了一个有用的相对度量,可以帮助数据科学家排除许多配置,只分析最有希望的配置。

当需要强制参数数量保持较低时,可以采用贝叶斯信息准则 ( BIC ,定义如下:

上式中, n 为样本数(例如 n = 1000 且采用自然对数,罚值约为 6.9);因此,BIC 几乎等同于 AIC,对参数的数量有更强的惩罚。然而,即使 BIC 倾向于选择较小的模型,结果通常也不如 AIC 可靠。BIC 的主要优点是当 n → ∞ 时,数据生成过程 p 数据T9】与模型pmT13】之间的 kulback-Leibler 散度(BIC 最小)趋于 0:**

由于当两个分布相同时,库尔巴克-莱布勒散度为零,所以前一个条件意味着 BIC 趋向于渐近地选择精确再现数据生成过程的模型。

现在,让我们考虑前面的例子,检查 AIC 和 BIC 的不同数量的组件。Scikit-learn 将这些措施作为GaussianMixture课程的方法(aic()bic())纳入其中。此外,我们还想计算每个模型所达到的最终对数似然。这可以通过将通过score()方法获得的值(每样本对数似然平均值乘以样本数)相乘来实现,如下所示:

from sklearn.mixture import GaussianMixture

n_max_components = 20

aics = []
bics = []
log_likelihoods = []

for n in range(1, n_max_components + 1):
 gm = GaussianMixture(n_components=n, random_state=1000)
 gm.fit(X)
 aics.append(gm.aic(X))
 bics.append(gm.bic(X))
 log_likelihoods.append(gm.score(X) * nb_samples)

结果图如下图所示:

AICs, BICs, and log-likelihoods for Gaussian mixtures with the number of components in the range (1, 20)

在这种情况下,我们知道数据集是由两个高斯分布生成的,但是让我们假设我们没有这条信息。AIC 和 BIC 的(当地)最低气温都是 n c = 2 。然而,当 BIC 不断变得越来越大时,AIC 的伪全球最小值为 n c = 18 。因此,如果我们信任 AIC,我们应该选择 18 个分量,这相当于用许多高斯子超分割数据集,具有小的方差。另一方面, n c = 2n c = 18 相比其他数值的差异并不是很大,所以我们也可以更倾向于前者的配置,考虑到要简单得多。BIC 证实了这一选择。事实上,即使也有对应于 n c = 18 的局部最小值,其值也比 n c = 2 的 BIC 值大得多。正如我们之前解释的,这种行为是由于 BIC 强加的样本量的额外惩罚。由于每个二元高斯需要一个权重变量,两个均值变量,四个协方差矩阵变量,对于 n c = 2 ,我们得到np= 2(1+2+4)= 14,对于 n c = 18 ,我们得到 n p = 18(1 由于有 300 个样本,BIC 被罚 log(300) ≈ 5.7 ,导致 BIC 增加约 350 个。当 n c 变大时,对数似然性增加(因为在极端情况下,每个点可以被认为是由具有零方差的单个高斯生成的,相当于狄拉克δ),参数的数量在模型选择过程中起着主要作用。

没有任何额外的惩罚,更大的模型很可能被选为最佳选择,但是在聚类过程中,我们也需要执行最大分离原则。这种情况部分与较少的组件有关,因此,BIC 应成为最佳方法。总的来说,我建议比较这两个标准,试图找到对应于 AIC 和 BIC 之间最大一致的ncT3。此外,还应该考虑基本的背景知识,因为许多数据生成过程都有明确定义的行为,并且可以通过排除所有不现实的值来限制潜在组件的范围。我请读者用 n c = 18 重复前面的例子,画出所有高斯人,比较某些特定点的概率。

基于贝叶斯高斯混合的组件选择

贝叶斯高斯混合模型是基于变分框架的标准高斯混合模型的扩展。这个题目相当高深,需要透彻的数学描述,超出了本书的范围(你可以在 Nasios N .和 Bors A. G .,高斯混合模型的变分学习,IEEE Transactions On Systems,Man,and 控制论, 36/ 4,08/2006 中找到)。然而,在我们讨论主要属性之前,理解主要概念和差异将是有帮助的。假设我们有一个数据集, X ,以及一个用向量 θ 参数化的概率模型。前面几节你看到了概率p(X |**【θ】)就是可能性 L(θ|X) ,它的最大化导致了一个生成概率最大的 X 的模型。但是,我们不对参数施加任何约束,它们的最终值完全取决于 X 。如果我们引入贝叶斯定理,我们会得到以下结果:

左边是给定数据集的参数后验概率,我们知道它与参数的似然性乘以先验概率成正比。在一个标准的最大似然估计中,我们只使用 p(X|θ) ,但是,当然,我们也可以包含一段关于 θ (根据概率分布)的先验知识,并最大化 p(θ|X) 或比例代理函数。然而,一般来说, p(θ|X) 是难以处理的,之前的 p(θ) 往往很难定义,因为没有足够的关于高度可能区域的知识。为此,最好将参数建模为用 η (所有特定参数的集合,如平均值、系数等)参数化的概率分布,并引入变分后验q(θ| X;η )近似真实分布。

这样的工具是一种称为变分贝叶斯推理的技术的关键要素(你可以在前面提到的论文中找到进一步的细节),它允许我们轻松找到最佳参数,而不需要与实际的 p(θ|X) 一起工作。特别是,在高斯混合模型中,有三组不同的参数,每组参数都用适当的分布建模。在这种情况下,我们倾向于不讨论这些选择的细节,但是理解其基本原理是有用的。

在贝叶斯框架中,给定一个似然性, p(X|θ) ,一个概率密度函数, p(θ) ,属于同一个后验族, p(θ|X) ,被称为共轭先验。在这种情况下,程序显然被简化了,因为可能性的影响仅限于修改先前的参数。为此,由于似然性是正态的,为了对均值建模,我们可以采用正态分布(它是相对于均值的共轭先验),对于协方差矩阵,我们可以采用威沙特分布(它是相对于协方差矩阵的逆的共轭先验)。在本讨论中,没有必要熟悉所有这些分布(除了正态分布),但记住它们是共轭先验是有帮助的,因此,给定参数的初始猜测,可能性的作用是调整它们,以便在给定数据集的情况下最大化它们的联合概率。

由于混合的权重被归一化,因此它们的和必须总是等于 1,并且我们希望只自动选择大量分量的子集,因此我们可以使用狄利克雷分布,它具有稀疏的有用特性。换句话说,给定一组权重 w 1 ,w2T5,...,以及 w n ,狄利克雷分布倾向于保持大部分权重的概率相当低,而非空权重的较小子组决定了主要贡献。狄利克雷过程提供了另一种选择,狄利克雷过程是一种产生概率分布的特殊随机过程。在这两种情况下,目标都是调整单个参数(称为重量浓度参数,该参数增加或减少具有稀疏分布(或简单地说,狄利克雷分布的稀疏性)的概率。

Scikit-learn 实现了贝叶斯高斯混合(通过BayesianGaussianMixture类),它可以基于狄利克雷过程和分布。在本例中,我们将保留默认值(process)并检查不同浓度值的行为(weight_concentration_prior参数)。也可以调整高斯的平均值作为均值,调整威沙特的自由度作为逆协方差。然而,在没有任何特定的先验知识的情况下,很难设置这些值(我们假设我们不知道均值可能位于何处或协方差矩阵的结构),因此,最好保留从问题结构中导出的值。因此,平均值(高斯)将等于 X 的平均值(可以用mean_precision_prior参数控制位移;值 < 1.0 倾向于将单个均值向 X 的均值移动,而较大的值会增加位移),自由度(Wishart)的数量被设置为等于特征的数量(维度为 X )。在许多情况下,这些参数是由学习过程自动调整的,没有必要改变它们的初始值。相反,weight_concentration_prior可以调整,以增加或减少活性成分的数量(即,其重量不接近零或比其他成分低得多)。

在这个例子中,我们将生成 500 个二维样本,使用 5 个部分重叠的高斯分布(特别是,其中 3 个共享一个非常大的重叠区域):

from sklearn.datasets import make_blobs

nb_samples = 500
nb_centers = 5

X, Y = make_blobs(n_samples=nb_samples, n_features=2, center_box=[-5, 5], 
                  centers=nb_centers, random_state=1000)

先从大重量浓度参数(1000)开始,最大成分数等于5。在这种情况下,我们期望找到大量(可能是5)的活性成分,因为狄利克雷过程不能实现高度稀疏:

from sklearn.mixture import BayesianGaussianMixture

gm = BayesianGaussianMixture(n_components=5, weight_concentration_prior=1000, 
                             max_iter=10000, random_state=1000)
gm.fit(X)

print('Weights: {}'.format(gm.weights_))

上一个片段的输出如下:

Weights: [0.19483693 0.20173229 0.19828598 0.19711226 0.20803253]

正如所料,所有组件的重量大致相同。为了得到进一步的确认,我们可以检查(通过argmax功能)有多少样本几乎没有分配给每个样本,如下所示:

Y_pred = gm.fit_predict(X)

print((Y_pred == 0).sum())
print((Y_pred == 1).sum())
print((Y_pred == 2).sum())
print((Y_pred == 3).sum())
print((Y_pred == 4).sum())

输出如下:

96
102
97
98
107

因此,平均来说,所有高斯人产生的点数是相同的。最终配置如下图所示:

Final configuration, with five active components

这种模式一般可以接受;然而,让我们假设我们知道潜在原因(即生成高斯分布)的数量可能是 4,而不是 5。我们可以尝试的第一件事是保持原有的最大组分数,降低重量浓度参数(即 0.1)。如果近似能够使用较少的高斯分布成功生成 X ,我们应该找到一个零权重:

gm = BayesianGaussianMixture(n_components=5, weight_concentration_prior=0.1, 
                             max_iter=10000, random_state=1000)

gm.fit(X)

print('Weights: {}'.format(gm.weights_))

现在输出如下:

Weights: [3.07496936e-01 2.02264778e-01 2.94642240e-01 1.95417680e-01 1.78366038e-04]

可以看到,第五个高斯的权重比其他高斯小得多,可以完全丢弃(我邀请你检查是否有几个样本几乎没有分配给它)。新配置包含四个活动组件,如下图所示:

Final configuration, with four active components

正如可以看到的,模型已经执行了组件数量的自动选择,并且它已经将较大的右斑点分成两部分,这两部分几乎是正交的。即使使用大量初始组件(例如,10 个)训练模型,该结果也保持不变。作为练习,我建议用其他值重复该示例,检查权重之间的差异。贝叶斯高斯混合模型非常强大,因为它们能够避免过度拟合。事实上,虽然标准高斯混合模型将通过减少它们的协方差来使用所有的分量,如果必要的话(以便覆盖密集的区域),但是这些模型利用了狄利克雷过程/分布的属性,以避免激活太多的分量。例如,通过检查模型可实现的最小组件数量,可以很好地洞察潜在的数据生成过程。在没有任何其他先验知识的情况下,这样的值是最终配置的一个很好的候选值,因为较少数量的组件也会产生较低的最终可能性。当然,也可以将 AIC/BIC 与这种方法结合使用,以获得另一种形式的确认。然而,与标准高斯混合模型的主要区别在于,它可能包含来自专家的先验信息(例如,均值和协方差方面的原因结构)。为此,我邀请您通过更改mean_precision_prior的值来重复该示例。例如,可以将mean_prior参数设置为不同于 X 平均值的值,并调整mean_precision_prior,从而迫使模型基于一些先验知识实现不同的分割(即一个区域中的所有样本都应该由特定的分量生成)。

生成高斯混合

高斯混合模型主要是生成模型。这意味着训练过程的目标是优化参数,以便最大化模型生成数据集的可能性。如果假设是正确的,并且 X 已经从特定的数据生成过程中采样,则最终的近似值必须能够生成所有其他潜在的样本。换句话说,我们假设 x i ∈ X 为 IDD,XI∨p数据;因此,当已经找到最优近似 p ≈ p 数据 时,所有在 p 下概率高的样本xjT19】也很可能是由 p 数据 生成的。

在这个例子中,我们希望在半监督场景中使用高斯混合模型。这意味着我们有一个包含标记和未标记样本的数据集,我们希望利用标记样本作为基础事实,并找到可以生成整个数据集的最佳混合。当标注非常大的数据集非常困难和昂贵时,这种情况非常常见。为了克服这个问题,可以标记一个均匀采样的子集,并训练一个生成模型,该模型能够生成具有最大可能可能性的剩余样本。

我们将使用更新后的权重、均值和协方差矩阵公式,这些公式在主要部分通过一个简单的过程进行了讨论,如下所示:

  • 所有被标记的样本都被认为是基本事实;所以,如果有 k 个类,我们还需要定义 k 个组件,并将每个类分配给其中一个。因此,如果xIT5】是标有 y i = {1,2,...,k} ,对应的概率向量将为 p(x i ) = (0,0,..., 1, 0, ...,0) ,其中 1 对应于与 y i 类相关联的高斯。换句话说,我们信任已标记的样本,并强制单个高斯生成具有相同标签的子集。
  • 所有未标记的样本都以标准方式处理,概率向量是通过将权重乘以每个高斯下的概率来确定的。

让我们首先生成一个包含 500 个二维样本的数据集(100已标记,其余的未标记),具有真实的标记,01,以及等于-1的未标记标记:

from sklearn.datasets import make_blobs

nb_samples = 500
nb_unlabeled = 400

X, Y = make_blobs(n_samples=nb_samples, n_features=2, centers=2, cluster_std=1.5, random_state=100)

unlabeled_idx = np.random.choice(np.arange(0, nb_samples, 1), replace=False, size=nb_unlabeled)
Y[unlabeled_idx] = -1

此时,我们可以初始化高斯参数(权重被选择为相等,并且协方差矩阵必须是半正定的。如果读者不熟悉这个概念,我们可以说对称方阵a∈ℜn×nT3】是正半定的,如果:

而且所有的特征值都是非负的,特征向量生成一个正交基(这个概念在第 7 章降维和成分分析讲 PCA 的时候会很有帮助)。

如果协方差矩阵是随机选择的,为了成为半正定的,需要将它们中的每一个乘以它的转置):

import numpy as np

m1 = np.array([-2.0, -2.5])
c1 = np.array([[1.0, 1.0],
               [1.0, 2.0]])
q1 = 0.5

m2 = np.array([1.0, 3.0])
c2 = np.array([[2.0, -1.0],
               [-1.0, 3.5]])
q2 = 0.5

数据集和初始高斯分布如下图所示:

Dataset (the unlabeled samples are marked with an x) and initial configuration

现在,我们可以按照之前定义的规则执行几次迭代(在我们的例子中是 10 次)(当然,也可以检查参数的稳定性,以便停止迭代)。使用 SciPy multivariate_normal类计算每个高斯分布下的概率:

from scipy.stats import multivariate_normal

nb_iterations = 10

for i in range(nb_iterations):
    Pij = np.zeros((nb_samples, 2))

    for i in range(nb_samples):

        if Y[i] == -1:
            p1 = multivariate_normal.pdf(X[i], m1, c1, allow_singular=True) * q1
            p2 = multivariate_normal.pdf(X[i], m2, c2, allow_singular=True) * q2
            Pij[i] = [p1, p2] / (p1 + p2)
        else:
            Pij[i, :] = [1.0, 0.0] if Y[i] == 0 else [0.0, 1.0]

    n = np.sum(Pij, axis=0)
    m = np.sum(np.dot(Pij.T, X), axis=0)

    m1 = np.dot(Pij[:, 0], X) / n[0]
    m2 = np.dot(Pij[:, 1], X) / n[1]

    q1 = n[0] / float(nb_samples)
    q2 = n[1] / float(nb_samples)

    c1 = np.zeros((2, 2))
    c2 = np.zeros((2, 2))

    for t in range(nb_samples):
        c1 += Pij[t, 0] * np.outer(X[t] - m1, X[t] - m1)
        c2 += Pij[t, 1] * np.outer(X[t] - m2, X[t] - m2)

    c1 /= n[0]
    c2 /= n[1]

过程结束时的高斯混合参数如下:

print('Gaussian 1:')
print(q1)
print(m1)
print(c1)

print('\nGaussian 2:')
print(q2)
print(m2)
print(c2)

上一个片段的输出如下:

Gaussian 1:
0.4995415573662937
[ 0.93814626 -4.4946583 ]
[[ 2.53042319 -0.10952365]
 [-0.10952365  2.26275963]]

Gaussian 2:
0.5004584426337063
[-1.52501526  6.7917029 ]
[[ 2.46061144 -0.08267972]
 [-0.08267972  2.54805208]]

不出所料,由于数据集的对称性,权重几乎保持不变,同时更新了均值和协方差矩阵,以最大化可能性。最后的图显示在下面的截图中:

Final configuration, after 10 iterations

可以看到,这两个 Gaussians 都已经成功优化,能够从几个扮演可信向导角色的标注样本开始生成整个数据集。这样的方法非常强大,因为它允许我们在模型中包含一些先验知识,而无需任何修改。然而,由于标记样本具有等于 1 的固定概率,该方法在异常值方面不是非常稳健。如果数据生成过程没有生成样本,或者样本受到噪声的影响,模型可能会导致高斯错误。然而,这种情况通常应该被忽略,因为任何先验知识,当包括在估计中时,总是必须被预先评估,以便检查它是否可靠。这样的步骤是必要的,以避免迫使模型只学习原始数据生成过程的一部分的风险。相反,当标记样本真正代表底层过程时,它们的包含减少了误差并加速了收敛。我邀请读者在介绍一些有噪声的点(例如,(-20,-10))后重复这个例子,并比较几个未标记测试样本的概率。

摘要

在本章中,我们介绍了一些最常见的软聚类方法,重点介绍了它们的属性和特点。模糊 c 均值是经典 k 均值算法的扩展,基于模糊集的概念。集群不被认为是互斥的分区,而是可以与其他一些集群重叠的灵活集合。所有的样本总是被分配给所有的聚类,但是权重向量决定了每个聚类的隶属度。连续的簇可以定义部分重叠的属性;因此,对于两个或多个聚类,给定样本可以具有非空权重。大小决定了它属于每个片段的多少。

高斯混合是一个生成过程,它基于一个假设,即可以用高斯分布的加权和来近似真实的数据生成过程。给定预定义数量的组件,为了最大化可能性,对模型进行训练。我们讨论了如何使用 AIC 和 BIC 作为性能度量,以便找到高斯分布的最佳数量。我们还简要介绍了贝叶斯高斯混合的概念,并研究了包含先验知识如何有助于自动选择一小部分活动组件。在最后一部分,我们讨论了半监督高斯混合的概念,展示了如何使用一些标记样本作为指导,以优化具有大量未标记点的训练过程。

在下一章中,我们将讨论核密度估计的概念及其在异常检测领域的应用。

问题

  1. 软聚类和硬聚类的主要区别是什么?
  2. 模糊 c 均值可以很容易地处理非凸聚类。这个说法对吗?
  3. 高斯混合模型的主要假设是什么?
  4. 假设两个模型达到相同的最优对数似然;然而,第一个的 AIC 是第二个的两倍。这是什么意思?
  5. 考虑到前面的问题,我们更喜欢哪种模式?
  6. 为什么我们要使用狄利克雷分布作为贝叶斯高斯混合模型权重的先验?
  7. 假设我们有一个包含 1000 个标记样本的数据集,这些样本的值已经过专家认证。我们从相同的来源收集了 5000 个样本,但我们不想为额外的标签付费。我们可以做些什么来将它们纳入我们的模型?

进一步阅读

  • 理论神经科学达扬·p .麻省理工学院出版社,2005
  • 通过 EM 算法不完全数据的最大似然皇家统计学会杂志登普斯特 A. P .,莱尔德 N. M .,鲁宾 D. B.系列 B. 39 (1),1977
  • 统计模型识别的新面貌阿凯克 H.IEEE 自动控制交易,19 (6)
  • 高斯混合模型的变分学习纳西欧和博思公司IEEE 系统、人和控制论学报,36/ 4,08/2006
  • Belohlavek R .,Klir G. J. (编著),概念与模糊逻辑麻省理工学院出版社,2011
  • 查佩尔·奥、斯科尔科夫·b、齐恩·a .(编著)半监督学习**麻省理工学院出版社,2010
  • 掌握机器学习算法博纳科索格帕克特出版,2018
  • 机器学习算法,第二版博纳科索格帕克特出版,2018

六、异常检测

在本章中,我们将讨论无监督学习的一个实际应用。我们的目标是训练模型,这些模型要么能够重现特定数据生成过程的概率密度函数,要么能够识别给定的新样本是内联样本还是外联样本。一般来说,我们可以说我们要追求的具体目标是发现异常,这些异常往往是模型下可能性极小的样本(即给定概率分布 p(x) < < λ 其中 λ 是预定义的阈值)或者离主分布质心相当远的样本。

特别是,本章将包括以下主题:

  • 概率密度函数及其基本性质简介
  • 直方图及其局限性
  • 籽粒密度估算 ( KDE )
  • 带宽选择标准
  • 异常检测的单变量示例
  • 使用 KDD 杯 99 数据集的 HTTP 攻击异常检测示例
  • 一类支持向量机
  • 利用隔离森林进行异常检测

技术要求

本章中的代码要求:

这些例子可以在 GitHub 资源库上获得,网址为 https://GitHub . com/packktpublishing/HandsOn-Unsupervised-Learning-with-Python/tree/master/chapter 06

概率密度函数

在前面的所有章节中,我们一直假设我们的数据集是从隐式数据生成过程 p data 中提取的,并且所有的算法都假设 x i ∈ X独立同分布 ( IID )并且均匀采样。我们假设 X 以足够的精度表示 p 数据 ,这样算法就可以在有限的初始知识下学会泛化。相反,在本章中,我们感兴趣的是在没有任何特定限制的情况下直接建模 p 数据 (例如,高斯混合模型通过对分布的结构施加约束来实现这一目标)。在讨论一些非常强大的方法之前,简要回顾一下在可测子集 X ℜn 上定义的一般连续概率密度函数 p(x) 的性质是有帮助的(为了避免混淆,我们将使用 p(x) 表示密度函数,使用 P(x) 表示实际概率):

例如,单变量高斯分布完全由均值 μ 和方差σ2T5 表征:

因此,x∑(a,b) 的概率如下:

即使一个事件在连续空间(例如,高斯)中的绝对概率为零(因为积分具有相同的极值),概率密度函数也提供了一个非常有用的度量,可以了解一个样本比另一个样本更有可能发生多少。例如:考虑高斯分布 N(0,1) ,密度 p(1) = 0.4 ,而对于 x = 2 ,密度降低到约 0.05 。意思是 12 大 0.4 / 0.05 = 8 倍。同样的,我们可以设置一个接受阈值 α ,将p(xI)<**α的所有样本xIT19】定义为异常(例如,在我们的例子中, α = 0.01 )。这个选择是异常检测过程中至关重要的一步,正如我们将要讨论的,它还必须包括潜在的异常值,然而,这些异常值仍然是常规样本。

在许多情况下,特征向量是用多维随机变量建模的。例如:数据集 X 3 可以用联合概率密度函数 p(x,y,z) 来表示。一般情况下,实际概率需要三重积分:

很容易理解,任何使用这种联合概率的算法都会受到复杂性的负面影响。通过假设单个分量的统计独立性,可以获得很强的简化:

不熟悉这个概念的读者可以在考试前想象一群学生。用随机变量建模的特征是学习小时数( x )和已完成课程数( y ),我们想找出给定这些因素的成功概率 p(成功|x,y) (这样的例子是基于条件概率的,但主要概念总是一样的)。我们可以假设,一个学完所有课程的学生,在家需要少学习;然而,这样的选择意味着这两个因素之间的依赖性(和相关性),这不能再单独评估了。相反,我们可以通过假设没有任何相关性来简化程序,并根据已完成课程的数量和作业的小时数来计算边际成功概率。重要的是要记住,特征之间的独立性不同于随后从分布中提取的样本的独立性。当我们说数据集由 IID 样本组成时,我们指的是概率p(xI| xI-1,x i -2 ,...,p 1 ) =每个样品的 p(x i ) 。换句话说,我们假设样本之间没有相关性。这样的条件更容易实现,因为它通常足以混洗数据集以消除任何残留的相关性。相反,特征之间的相关性是数据生成过程的一个特殊属性,无法消除。因此,在某些情况下,我们假设独立性,因为我们知道它的影响可以忽略不计,最终结果不会受到严重影响,而在其他情况下,我们将基于整个多维特征向量来训练模型。我们现在可以定义异常的概念,它将在剩下的部分中使用。

异常作为异常值或新奇值

本章的主题是在没有任何监督的情况下自动检测异常。由于模型不是基于标记样本提供的反馈,我们只能依靠整个数据集的属性来找出相似之处并突出不同之处。特别是,我们从一个非常简单但有效的假设出发:常见事件为正常,而不太可能发生的事件一般被视为异常。当然,这个定义意味着我们正在监控的过程正在正常运行,大多数结果被认为是有效的。例如:一个硅加工厂必须把一个晶片切成相等的块。我们知道它们每一个都是 0.2 × 0.2 英寸(约 0.5 × 0.5 厘米),每一侧的标准偏差为 0.001 英寸。经过 1,000,000 个处理步骤后,已经确定了该度量。我们是否有权将 0.25 × 0.25 英寸芯片视为异常?当然,我们是。实际上,我们假设每条边的长度都建模为高斯分布(非常合理的选择)μ= 0.2**σ= 0.001;三个标准差后,概率下降到几乎为零。因此,例如: P(边> 0.23) ≈ 0 这样的尺寸的芯片必须明确视为异常。

显然,这是一个极其简单的例子,不需要任何模型。然而,在现实生活中,密度的结构可能非常复杂,几个高概率区域被低概率区域包围。这就是为什么必须采用更通用的方法来建模整个样本空间。

当然,异常的语义不能标准化,它总是取决于正在分析的具体问题。因此,定义异常概念的一个常见方法是区分异常值新奇值。前者是包含在数据集中的样本,即使它们与其他样本之间的距离大于平均值。因此,异常值检测过程旨在找出这样的奇怪的样本(例如:考虑到前面的例子,0.25 × 0.25 英寸的芯片如果包含在数据集中显然是异常值)。相反,新颖性检测的目标略有不同,因为在这种情况下,我们假设使用只包含正常样本的数据集;因此,给定一个新的,我们有兴趣理解我们是否可以认为它是从原始数据生成过程中提取的或者是一个异常值(例如:一个新手技术人员问我们这个问题:0.25 × 0.25 英寸的芯片是一个异常值吗?如果我们已经收集了正常芯片的数据集,我们可以使用我们的模型来回答这个问题。

描述这种情况的另一种方式是将样本视为一系列可能受可变噪声影响的值: y(t) = x(t) + n(t) 。当| | n(t)| |<|<| | x(t)|时,样品可归类为洁净 : y(t) ≈ x(t) 。反之,当| | n(t)|∑| | x(t)| |(甚至更大)时,则为异常值,不能代表真实的底层过程 p 数据 。由于噪声的平均幅度通常比信号小得多,P(| | n(t)|∞| | x(t)| |)的概率接近于零。因此,我们可以将异常想象成受异常外部噪声影响的正常样本。管理异常和噪声样本之间的真正主要区别通常在于检测真正异常和相应标记样本的能力。事实上,虽然有噪声的信号肯定是被破坏的,并且目标是最小化噪声的影响,但是异常可以被人类非常频繁地识别并正确标记。然而,正如已经讨论过的,在本章中,我们有兴趣找出不依赖于现有标签的发现方法。此外,为了避免混淆,我们总是引用异常,每次都定义数据集的内容(只有内联或内联和外联)和我们分析的目标。在下一节中,我们将简要讨论数据集的预期结构。

数据集的结构

在标准的有监督(通常也是无监督)任务中,数据集应该是平衡的。换句话说,属于每个类的样本数量应该几乎相同。在本章我们将要讨论的任务中,我们假设有非常不平衡的数据集 X (包含 N 个样本):

  • N 异常值 < < N ,如果存在异常值检测(即数据集部分为污垢;(因此,我们需要找出一种方法来过滤掉所有异常值)
  • N 异常值 = 0 (或者更现实一点, P(N 异常值 > 0) → 0) ,如果有新颖性检测(也就是我们一般可以信任已有样本,把注意力集中在新样本上)

这些标准的原因很明显:让我们考虑前面讨论的例子。如果在 1000000 个加工步骤后观察到的异常率等于 0.2%,则有 2000 个异常,这对于一个工作过程来说可能是一个合理的值。如果这样的数字大得多,就意味着系统中应该有更严重的问题,这超出了数据科学家的角色。因此,在这种情况下,我们期望数据集包含大量正确的样本和非常少的异常(甚至为零)。在许多情况下,经验法则是反映潜在的数据生成过程,因此,如果专家可以确认,例如,有 0.2%的异常,比率应该是 1000÷2 ,以找出一个现实的概率密度函数。在这种情况下,事实上,找出决定异常值可分辨性的因素更为重要。另一方面,如果我们被要求仅执行新颖性检测(例如:区分有效和恶意网络请求),数据集必须经过验证,以便不包含异常,但同时反映负责所有可能的有效样本的真实数据生成过程。

事实上,如果正确样本的总体是详尽的,任何与高概率区域的大偏差都足以触发警报。相反,真实数据生成过程的有限区域可能导致假阳性结果(也就是说,没有包含在训练集中的有效样本被错误地识别为异常值)。在最坏的情况下,如果特征被改变(也就是说,异常值被错误地识别为有效样本),噪声很大的子集也可能确定假阴性。然而,在大多数现实生活中,最重要的因素是样本的数量和收集样本的背景。不言而喻,任何模型都必须用将要测试的相同类型的元素来训练。例如:如果化工厂内的测量是使用低精度仪器进行的,则用高精度仪器收集的测试可能不能代表人口(当然,它们比数据集可靠得多)。因此,在进行分析之前,我强烈建议仔细检查数据的性质,并询问是否所有的测试样本都来自同一个数据生成过程。

我们现在可以引入直方图的概念,这是估计包含观测值的数据集分布最简单的方法。

直方图

找出概率密度函数近似值的最简单方法是基于频率计数。如果我们有一个数据集 X 包含 m 个样本 x i ∈ ℜ (为简单起见,我们只考虑单变量分布,但该过程对于多维样本完全等效),我们可以定义 mM 如下:

间隔 (m,M) 可以分成固定数量的 b 箱(其可以具有相同或不同的宽度,表示为 w(b j ) ,使得np(bj)对应于包含在箱中的样品数量bjT17】。此时,给定一个测试样本 x t ,很容易理解,通过检测包含 x t 的仓,并使用以下公式,可以很容易地获得概率的近似值:

在分析这种方法的利弊之前,让我们考虑一个简单的例子,它基于细分为 10 个不同阶层的人的年龄分布:

import numpy as np

nb_samples = [1000, 800, 500, 380, 280, 150, 120, 100, 50, 30]

ages = []

for n in nb_samples:
    i = np.random.uniform(10, 80, size=2)
    a = np.random.uniform(i[0], i[1], size=n).astype(np.int32)
    ages.append(a)

ages = np.concatenate(ages)

The dataset can be reproduced only using the random seed 1000 (that is, setting np.random.seed(1000)).

ages数组包含了所有的样本,我们想创建一个直方图来初步了解分布。我们将使用 NumPy np.histrogram()功能,它提供了所有必要的工具。首先要解决的问题是找出最佳的箱数。对于标准分布来说,这可能很容易,但是当没有关于概率密度的先验知识时,这可能变得极其困难。原因很简单:因为我们需要用一个逐步函数来逼近一个连续函数,所以仓的宽度决定了最终的精度。例如:如果密度是平坦的(例如:均匀分布),几个面元就足以达到良好的效果。相反,当有峰值时,在函数的一阶导数较大的区域放置更多(更短)的面元,当导数接近零时放置更小的面元(表示平坦区域)会有所帮助。正如我们将要讨论的,使用更复杂的技术,这个过程变得更容易,而直方图通常基于对最佳箱数的更粗略的计算。具体来说,NumPy 允许设置bins='auto'参数,该参数强制算法根据定义明确的统计方法(基于弗里德曼双精度估计器和斯特奇公式)自动选择数字:

在上式中,四分位数区间 ( IQR )对应于第 75 和第 25 个百分点之间的差值。由于我们对分布没有一个清晰的概念,我们更喜欢依靠自动选择,如下面的代码片段所示:

import numpy as np

h, e = np.histogram(ages, bins='auto')

print('Histograms counts: {}'.format(h))
print('Bin edges: {}'.format(e))

上一个片段的输出如下:

Histograms counts: [177  86 122 165 236 266 262 173 269 258 241 116 458 257 311   1   1   5 6]
Bin edges: [16\.         18.73684211 21.47368421 24.21052632 26.94736842 29.68421053
 32.42105263 35.15789474 37.89473684 40.63157895 43.36842105 46.10526316
 48.84210526 51.57894737 54.31578947 57.05263158 59.78947368 62.52631579
 65.26315789 68\.        ]

因此,该算法定义了 19 个仓,并输出了频率计数和边缘(即最小值为16,最大值为68)。我们现在可以显示直方图:

Histogram of the test distribution

该图证实分布相当不规则,一些区域有被平坦区域包围的峰。如前所述,当查询基于样本属于特定箱的概率时,直方图是有帮助的。例如,在这种情况下,我们可能有兴趣确定一个人的年龄在 48.84 到 51.58 之间的概率(这对应于从 0 开始的第 12 个箱)。由于所有的箱柜宽度相同,我们可以简单地用np(b12**)(h[12])和 m ( ages.shape[0])之间的比值来近似这样一个值:

d = e[1] - e[0]
p50 = float(h[12]) / float(ages.shape[0])

print('P(48.84 < x < 51.58) = {:.2f} ({:.2f}%)'.format(p50, p50 * 100.0))

输出如下:

P(48.84 < x < 51.58) = 0.13 (13.43%)

因此,概率的近似值约为 13.5%,这也由直方图的结构所证实。然而,读者应该已经清楚地理解,这样的方法有明显的局限性。第一个也是最明显的是箱子的数量和宽度。事实上,一个小数字产生的粗略结果不能考虑快速振荡。另一方面,大量的数据会被驱动到一个带孔的直方图,因为大部分的箱子都没有样本。因此,考虑到现实生活中可能遇到的所有可能的动态,需要一种更可靠的方法。这是我们将在下一节讨论的内容。

核密度估计(KDE)

直方图不连续问题的解决可以用简单的方法有效地解决。给定一个样本 x i ∈ X ,可以考虑一个超体积(通常是超立方体或超球),假设我们正在处理多元分布,其中心是 x i 。这样一个区域的范围是通过一个称为带宽的常数 h 来定义的(选择这个名称是为了支持值为正的有限区域的含义)。然而,我们现在不是简单地计算属于超体积的样本数量,而是使用平滑核函数 K(x i 来近似这个值;h) 具有一些重要特征:

此外,出于统计和实际的原因,还需要强制执行以下积分约束(为简单起见,它们仅在单变量情况下显示,但扩展很简单):

在讨论名为核密度估计 ( KDE )的技术之前,展示一下K()的一些常见选择是有帮助的。

高斯核

这是最常用的内核之一,其结构如下:

下图显示了图形表示:

Gaussian kernel

鉴于其规律性,高斯核是许多密度估计任务的常见选择。但是,由于该方法不允许混合不同的内核,因此选择时必须考虑所有属性。从统计数据中,我们知道高斯分布可以被认为是峰度的平均参考(峰度与峰值和尾部的重量成正比)。为了最大化内核的选择性,我们需要减少带宽。这意味着即使是最小的振荡也会改变密度,结果是非常不规则的估计。另一方面,当 h 较大(即高斯的方差)时,近似变得非常平滑,并且可能失去捕获所有峰值的能力。因此,在选择最合适的带宽的同时,考虑其他可以自然简化过程的内核也是有帮助的。

epanechnikov 核

这个核被提出来最小化均方误差,并且它还具有非常规则的性质(实际上,它可以被想象成倒抛物线)。公式如下:

引入常数 ε 对内核进行归一化,满足所有要求(类似的方式,可以在范围上扩展内核( -hh )以便与其他函数更加一致)。下图显示了图形表示:

Epanechnikov kernel

h → 0 时,籽粒会变得非常尖。然而,鉴于其数学结构,它将始终保持非常规则;因此,在大多数情况下,没有必要将其用作高斯核的替代(即使后者具有稍大的均方误差)。此外,由于函数在x = h(K(x;h) = 0 对于 |x| > h ,它会导致密度估计快速下降,特别是在边界处,例如高斯函数非常平缓地下降。

指数核

一个非常峰值的内核是指数内核,其一般表达式如下:

与高斯核相反,这个核有非常重的尾部和一个尖锐的峰值。下图显示了一个图表:

Exponential kernel

可以看到,这样的函数适用于模拟密度高度集中在某些特定点周围的非常不规则的分布。另一方面,当数据生成过程非常规则且表面光滑时,误差会变得非常高。可以用来评估内核(和带宽)性能的一个很好的理论度量是平均积分平方误差 ( MISE ),定义如下:

在之前的公式中, p K (x) 为估算密度, p(x) 为实际密度。遗憾的是, p(x) 未知(否则,我们不需要任何估计);因此,这样的方法只能用于理论评估(例如:Epanechnikov 核的最优性)。然而,很容易理解,每当内核不能靠近实际表面时,MISE 就会变大。由于指数型的跳跃非常突然,它只能在特定的情况下适用。在所有其他情况下,它的行为导致更大的 MISEs,因此其他内核是更好的。

均匀核

这是最简单且不太平滑的内核函数,其用法类似于构建直方图的标准过程。它等于以下内容:

显然,在带宽限定的范围内,这是一个恒定的步长,只有在估计不需要平滑时才有帮助。

估计密度

一旦选择了一个核函数,就有可能使用 k 近邻方法来构建概率密度函数的完全近似。事实上,给定一个数据集 X (为简单起见, X ∈ ℜ m ,因此这些值是实数),很容易创建例如一个球树(如第 2 章聚类基础中所讨论的)来以有效的方式划分数据。当数据结构准备好时,可以获得带宽定义的半径内查询点 x j 的所有邻居。假设这样一套是 X j = {x 1 ,...,x t } 且点数为 N j 。概率密度的估计如下:

不难证明,如果带宽选择得当(作为邻域包含的样本数的函数), p K 在概率上收敛于实际的 p(x) 。换句话说,如果粒度足够大,近似和真实密度之间的绝对误差收敛到零。 p K (x j ) 的构建过程如下图所示:

Density estimation of xj. The Kernel functions are evaluated in each point belonging to the neighborhood of xj

在这一点上,很自然地会问为什么不对每个查询使用整个数据集,而不是 k-NN 方法?答案很简单,它基于这样的假设:在 x j 计算的密度函数值可以很容易地使用局部行为进行插值(也就是说,对于多元分布,以 x j 为中心的球)和远点对估计没有影响。因此,我们可以将计算限制在 X 的较小子集内,避免包含接近于零的贡献。

在讨论如何确定最佳带宽之前,让我们展示一下之前定义的数据集的密度估计(使用 scikit-learn)。由于我们没有任何特定的先验知识,我们将采用不同带宽(0.1、0.5 和 1.5)的高斯核。所有其他参数保持默认值;但是KernelDensity类允许设置度量(默认为metric='euclidean')、数据结构(默认为algorithm='auto',根据维度在球树和 kd 树之间执行自动选择)以及绝对和相对容差(分别为 0 和 10 -8 )。在许多情况下,没有必要更改默认值;但是,对于具有特定特征的非常大的数据集,例如,更改leaf_size参数以提高性能可能会有所帮助(如第 2 章聚类基础中所述)。此外,默认度量不能适用于所有任务(例如:标准文档显示了一个基于哈弗斯距离的示例,在处理纬度和经度时可以使用该示例)。其他情况下,最好使用超立方体,而不是球(曼哈顿距离就是这种情况)。

让我们从实例化类和拟合模型开始:

from sklearn.neighbors import KernelDensity

kd_01 = KernelDensity(kernel='gaussian', bandwidth=0.1)
kd_05 = KernelDensity(kernel='gaussian', bandwidth=0.5)
kd_15 = KernelDensity(kernel='gaussian', bandwidth=1.5)

kd_01.fit(ages.reshape(-1, 1))
kd_05.fit(ages.reshape(-1, 1))
kd_15.fit(ages.reshape(-1, 1))

此时,可以调用score_samples()方法来获得一组数据点的对数密度估计值(在我们的例子中,我们考虑的是 0.05 增量的范围(10,70))。由于数值为 log(p) ,需要计算 e log(p) 才能得到实际概率。

结果图如下图所示:

Gaussian density estimations with bandwidths: 0.1 (top), 0.5 (middle), and 1.5 (bottom)

可以注意到,当带宽很小时(0.1),由于缺少特定子范围的样本,密度会有强烈的振荡。当 h = 0.5 时,轮廓(由于数据集是单变量的)变得更加稳定,但仍有一些由邻域内部方差引起的残留快速变化。当 h 变大(在我们的例子中为 1.5)时,这种行为几乎完全消除。一个显而易见的问题是:如何确定最合适的带宽?当然,最自然的选择是最小化 MISE 的 h 的值,但是,正如所讨论的,这种方法只能在真实概率密度已知的情况下使用。然而,有几个经验标准已经被证实是非常可靠的。给定完整的数据集 X ∈ ℜ m ,第一个数据集基于以下公式:

在我们的案例中,我们获得了以下信息:

import numpy as np

N = float(ages.shape[0])
h = 1.06 * np.std(ages) * np.power(N, -0.2)

print('h = {:.3f}'.format(h))

输出如下:

h = 2.415

因此,建议增加的带宽甚至比我们上次实验中的还要多。因此,第二种方法基于四分位数范围( IQR = Q3 - Q1 或相当于第 75 百分位-第 25 百分位),它对非常强的内部变化更加稳健:

计算如下:

import numpy as np

IQR = np.percentile(ages, 75) - np.percentile(ages, 25)
h = 0.9 * np.min([np.std(ages), IQR / 1.34]) * np.power(N, -0.2)

print('h = {:.3f}'.format(h))

现在的输出是:

h = 2.051

这个值比前一个值要小得多,说明 p K (x) 可以用更小的超体积更精确。根据经验,我建议选择带宽最小的方法,即使第二种方法通常在不同的上下文中提供最佳结果。现在让我们使用 h = 2.0 和高斯核、Epanechnikov 核和指数核(我们排除了均匀核,因为最终结果相当于直方图)重新执行估算:

from sklearn.neighbors import KernelDensity

kd_gaussian = KernelDensity(kernel='gaussian', bandwidth=2.0)
kd_epanechnikov = KernelDensity(kernel='epanechnikov', bandwidth=2.0)
kd_exponential = KernelDensity(kernel='exponential', bandwidth=2.0)

kd_gaussian.fit(ages.reshape(-1, 1))
kd_epanechnikov.fit(ages.reshape(-1, 1))
kd_exponential.fit(ages.reshape(-1, 1))

图形输出如下图所示:

Density estimations with bandwidths equal to 2.0 and Gaussian kernel (top), Epanechnikov kernel (middle), and Exponential kernel (bottom)

不出所料,Epanechnikov 核和指数核都比高斯核更具振荡性(因为当 h 小时,它们往往更具峰值);然而,很明显,中心情节肯定是最准确的(就 MISE 而言)。类似的结果以前已经用高斯核和 h = 0.5 得到,但是,在那种情况下,振荡是极不规则的。如上所述,当值达到带宽边界时,Epanechnikov 内核具有非常强的不连续趋势。这种现象可以通过观察估计值的极值立即理解,估计值几乎垂直下降到零。相反, h = 2 的高斯估计似乎很平滑,没有捕捉到 50 年到 60 年之间的变化。同样的情况也发生在指数内核上,这也显示了它的特殊行为:非常尖锐的极端。在下面的例子中,我们将使用 Epanechnikov 内核;然而,我邀请读者也检查不同带宽的高斯结果。这种选择有一个精确的理由(没有充分的理由不能放弃):我们认为数据集是详尽的,我们想惩罚所有克服自然极端的样本。在所有其他情况下,非常小的剩余概率是优选的;然而,这种选择必须考虑到每一个具体目标。

异常检测

现在让我们应用 Epanechnikov 密度估计来执行异常检测的示例。根据概率密度的结构,我们决定在 p(x) < 0.005 处实施截止。这种情况显示在下面的屏幕截图中:

Epanechnikov density estimation with anomaly cut-off

红点表示样本被归类为异常的年龄限制。让我们计算一些测试点的概率密度:

import numpy as np

test_data = np.array([12, 15, 18, 20, 25, 30, 40, 50, 55, 60, 65, 70, 75, 80, 85, 90]).reshape(-1, 1)

test_densities_epanechnikov = np.exp(kd_epanechnikov.score_samples(test_data))
test_densities_gaussian = np.exp(kd_gaussian.score_samples(test_data))

for age, density in zip(np.squeeze(test_data), test_densities_epanechnikov):
    print('p(Age = {:d}) = {:.7f} ({})'.format(age, density, 'Anomaly' if density < 0.005 else 'Normal'))

上一个片段的输出如下:

p(Age = 12) = 0.0000000 (Anomaly)
p(Age = 15) = 0.0049487 (Anomaly)
p(Age = 18) = 0.0131965 (Normal)
p(Age = 20) = 0.0078079 (Normal)
p(Age = 25) = 0.0202346 (Normal)
p(Age = 30) = 0.0238636 (Normal)
p(Age = 40) = 0.0262830 (Normal)
p(Age = 50) = 0.0396169 (Normal)
p(Age = 55) = 0.0249084 (Normal)
p(Age = 60) = 0.0000825 (Anomaly)
p(Age = 65) = 0.0006598 (Anomaly)
p(Age = 70) = 0.0000000 (Anomaly)
p(Age = 75) = 0.0000000 (Anomaly)
p(Age = 80) = 0.0000000 (Anomaly)
p(Age = 85) = 0.0000000 (Anomaly)
p(Age = 90) = 0.0000000 (Anomaly)

可以看到,函数的突然下降产生了一种垂直分离。一个年龄15的人,几乎到了边界( p(15) ≈ 0.0049 ,而对于上限来说,行为更为激烈。截止年龄约为 58 岁,但年龄为60的样本比年龄为 57 岁的样本可能性小 10 倍左右(这也由初始直方图证实)。由于这只是一个说教的例子,很容易发现异常;然而,如果没有标准化的算法,即使是稍微复杂一点的分布也会产生一些问题。特别是在这种特殊情况下,这是一个简单的单变量分布,异常通常位于尾部。

因此,我们假设给定总体密度估计 p K (x) :

当考虑包含所有样本(正常样本和异常样本)的数据集时,这种行为通常是不正确的,数据科学家在决定阈值时必须小心。即使可以很明显,通过去除数据集中的所有异常来学习正态分布也是一个好主意,这样可以将异常所在的区域变平( p K (x) → 0 )。这样,前面的标准仍然有效,并且很容易比较不同的密度来进行区分。

在继续下一个例子之前,我建议通过创建人工洞和为检测设置不同的阈值来修改初始分布。此外,我邀请读者基于例如年龄和身高生成二元分布(例如:基于一些高斯分布的总和),并创建一个简单的模型,该模型能够检测其参数非常不可能的所有人。

利用 KDD 杯 99 数据集进行异常检测

此示例基于 KDD 杯 99 数据集,该数据集收集了一长串正常和恶意的互联网活动。特别是,我们将重点关注 HTTP 请求的子集,它有四个属性:持续时间、源字节、目标字节和行为(这更像是一个分类元素,但它有助于我们立即访问一些特定的攻击)。由于原始值是零附近的非常小的数字,所有版本(包括 scikit-learn one)都使用公式 log(x + 0.1) 对变量进行了重整(因此,在用新样本模拟异常检测时必须应用它)。当然,逆变换如下:

让我们从使用 scikit-learn 内置函数fetch_kddcup99()加载和准备数据集开始,选择percent10=True将数据限制在原始集合的 10%(非常大)。当然,我邀请读者也用整个数据集和完整的参数列表(包含 34 个数值)进行测试。

在这种情况下,我们还选择了subset='http',它已经准备好包含非常多的正常连接和一些特定的攻击(如在标准的定期日志中):

from sklearn.datasets import fetch_kddcup99

kddcup99 = fetch_kddcup99(subset='http', percent10=True, random_state=1000)

X = kddcup99['data'].astype(np.float64)
Y = kddcup99['target']

print('Statuses: {}'.format(np.unique(Y)))
print('Normal samples: {}'.format(X[Y == b'normal.'].shape[0]))
print('Anomalies: {}'.format(X[Y != b'normal.'].shape[0]))

输出如下:

Statuses: [b'back.' b'ipsweep.' b'normal.' b'phf.' b'satan.'] Normal samples: 56516 Anomalies: 2209

因此,有四种类型的攻击(其细节在本文中并不重要)具有2209恶意样本和56516正常连接。为了进行密度估计,我们将把这三个分量作为独立的随机变量(这并不完全正确,但它可以是一个合理的起点)进行一些初步考虑,但最终估计是基于完全联合分布的。当我们想要确定最佳带宽时,让我们执行一个基本的统计分析:

import numpy as np

means = np.mean(X, axis=0)
stds = np.std(X, axis=0)
IQRs = np.percentile(X, 75, axis=0) - np.percentile(X, 25, axis=0)

上一个片段的输出如下:

Means: [-2.26381954  5.73573107  7.53879208]
Standard devations: [0.49261436 1.06024947 1.32979463]
IQRs: [0\.         0.34871118 1.99673381]

持续时间(第一个分量)的 IQR 为空;因此,大多数价值观是平等的。让我们绘制一个直方图来证实这一点:

Histogram for the first component (duration)

不出所料,这样的成分并不十分显著,因为只有一小部分样本具有不同的值。因此,在这个例子中,我们将跳过它,只处理源和目标字节。现在让我们按照前面的解释计算带宽:

import numpy as np

N = float(X.shape[0])

h0 = 0.9 * np.min([stds[0], IQRs[0] / 1.34]) * np.power(N, -0.2)
h1 = 0.9 * np.min([stds[1], IQRs[1] / 1.34]) * np.power(N, -0.2)
h2 = 0.9 * np.min([stds[2], IQRs[2] / 1.34]) * np.power(N, -0.2)

print('h0 = {:.3f}, h1 = {:.3f}, h2 = {:.3f}'.format(h0, h1, h2))

输出如下:

h0 = 0.000, h1 = 0.026, h2 = 0.133

排除第一个值,我们需要在h1h2之间进行选择。由于这些值的幅度不大,我们希望有很大的选择性,我们将设置 h = 0.025 并采用高斯核,这提供了良好的平滑度。包含第一个组件的分割输出(使用 seaborn 可视化库获得,该库包括一个内部 KDE 模块)如下图所示:

Density estimations for normal connections (upper line) and malicious attacks (lower line)

第一行显示正常连接的密度,而较低的是恶意攻击。正如预期的那样,第一个组件(持续时间)在两种情况下几乎相同,可以丢弃。相反,源字节和目标字节表现出非常不同的行为。不考虑对数变换,正常连接平均发送 5 个字节,方差非常低,将潜在范围扩展到区间(4,6)。响应的方差较大,值在 4 到 10 之间,密度从 10 开始非常低。相反,恶意攻击的源字节和目标字节都有两个峰值:较短的字节对应于-2,较高的字节分别对应于约 11 和 9(与正常区域的重叠最小)。即使不考虑完整的联合概率密度,也不难理解大多数攻击发送更多的输入数据,接收更长的响应(而连接持续时间不受强烈影响)。

我们现在可以通过仅选择正常样本(即对应于Y == b'normal.')来训练估计器:

from sklearn.neighbors import KernelDensity

X = X[:, 1:]

kd = KernelDensity(kernel='gaussian', bandwidth=0.025)
kd.fit(X[Y == b'normal.'])

让我们计算正常和异常样本的密度:

Yn = np.exp(kd.score_samples(X[Y == b'normal.']))
Ya = np.exp(kd.score_samples(X[Y != b'normal.']))

print('Mean normal: {:.5f} - Std: {:.5f}'.format(np.mean(Yn), np.std(Yn)))
print('Mean anomalies: {:.5f} - Std: {:.5f}'.format(np.mean(Ya), np.std(Ya)))

输出如下:

Mean normal: 0.39588 - Std: 0.25755
Mean anomalies: 0.00008 - Std: 0.00374

很明显,当例如 p K (x) < 0.05 (考虑三个标准差)对于一个异常,我们得到pK(x)∞(0,0.01)) ,而Yn的中位数约为 0.35。这意味着至少一半的样品具有pK**(x)>0.35。然而,经过简单的计数检查后,我们得到了以下结果:

print(np.sum(Yn < 0.05))
print(np.sum(Yn < 0.03))
print(np.sum(Yn < 0.02))
print(np.sum(Yn < 0.015))

输出如下:

3147
1778
1037
702

由于有 56,516 个正常样本,我们可以决定选择两个阈值(同时考虑异常异常值):

  • 正常连接:pK(x)>0.03
  • 中度预警 : 0.03(涉及 3.1%的正常样本可识别为假阳性)
  • 高警戒 : 0.015(这种情况下,只有 1.2%的正常样本可以触发报警)

此外,在第二个警报中,我们发现:

print(np.sum(Ya < 0.015))

输出如下:

2208

因此,只有一个异常样本具有 p K (x) > 0.015 (有 2,209 个向量),这证实了这样的选择是合理的。密度直方图也证实了之前的结果:

Histogram of anomaly (left) and normal (right) densities

正态分布的右尾并不惊人,因为异常高度集中在左侧。在这个领域,也有大部分的异常,因此也是最关键的。原因与特定的域(对于不同类型的请求,输入和输出字节可能非常相似)密切相关,在更稳定的解决方案中,有必要考虑进一步的参数(例如:完整的 KDD 杯 99 数据集)。但是,出于教学目的,我们可以定义一个简单的函数(基于之前定义的阈值)来根据源和目标字节量(非对数)检查连接状态:

import numpy as np

def is_anomaly(kd, source, destination, medium_thr=0.03, high_thr=0.015):
    xs = np.log(source + 0.1)
    xd = np.log(destination + 0.1)
    data = np.array([[xs, xd]])

    density = np.exp(kd.score_samples(data))[0]

    if density >= medium_thr:
        return density, 'Normal connection'
    elif density >= high_thr:
        return density, 'Medium risk'
    else:
        return density, 'High risk'

我们现在可以用三个不同的例子来测试这个函数:

print('p = {:.2f} - {}'.format(*is_anomaly(kd, 200, 1100)))
print('p = {:.2f} - {}'.format(*is_anomaly(kd, 360, 200)))
print('p = {:.2f} - {}'.format(*is_anomaly(kd, 800, 1800)))

输出如下:

p = 0.30 - Normal connection
p = 0.02 - Medium risk
p = 0.00000 - High risk

一般来说,还可以考虑源和目标字节密度的二元图:

Bivariate plot of the source and destination bytes densities

前面的截图证实,虽然攻击通常涉及大量的输入字节,但响应与正常的非常相似,即使它们占据了该区域的极端部分。作为练习,我邀请读者用整个 KDD 杯 99 数据集训练一个模型,并找出检测非常危险和中等风险攻击的最佳阈值。

一类支持向量机

schlkopf B,Platt J C,Shawe-Taylor J C,Smola A J 和 Williamson R C 在文章估计高维分布的支持度,神经计算,2001 年 13 月 7 日中提出了单类支持向量机的概念,作为将新奇事物分类为从真实数据生成过程中抽取的样本或离群值的方法。让我们从我们想要实现的目标开始:找到一个无监督的模型,在给定样本 x i 的情况下,该模型可以产生二进制输出 y i (传统上,SVMs 结果是双极性的:-1 和+1),因此,如果 x i 是内联的 y i = +1 ,反之, y i = -1 如果 x i 是一个离群值(更正确的说法是,在前面提到的论文中,作者假设对于构成训练集的大多数内联者来说,结果是 1 )。 乍一看,这似乎是一个经典的监督问题;然而,这并不是因为它不需要有标签的数据集。事实上,给定包含 m 样本xI∈ℜnt41】的数据集 X ,将使用单个固定类训练模型,目标是找到一个分离超平面,使 X 与原点之间的距离最大化。首先,让我们考虑一个简单的线性情况,如下图所示:

Linear one-class SVM scenario: the training set is separated from the origin with the largest margin

该模型被训练以找出超平面的参数,这些参数使到原点的距离最大化。超平面一侧的所有样本预计都是内联的,输出标签为 +1 ,其余所有样本都被认为是外联的,输出标签为 -1 。这个标准看起来很有效,但它只适用于线性可分离的数据集。标准支持向量机通过将数据集(通过函数φ())投影到特征空间 D 来解决这个问题,在特征空间 D 中它获得这样的属性:

特别是,考虑到问题的数学本质,如果选择一个内核,投影在计算上就变得轻量级了。换句话说,我们希望使用具有以下属性的函数:

投影函数φ()的存在性保证在一个非常容易得到的条件(称为 Mercer 条件)下存在(即在一个实子空间中,核必须是正半定的)。这样选择的原因与问题解决所涉及的过程有严格的关系(更详细的解释可以在机器学习算法第二版博纳科索格Packt Publishing ,2018 中找到)。但是,不熟悉 SVMs 的读者不应该担心,因为我们不打算讨论太多的数学细节。要记住的最重要的事情是,不支持任何内核的通用投影会导致计算复杂性的急剧增加(特别是对于大型数据集)。

K(,)最常见的选择之一是径向基函数(已经在第三章高级聚类中进行了分析):

另一个有用的内核是多项式内核:

在这种情况下,指数 c 定义多项式函数的次数,该次数与特征空间的维数成比例。然而,内核及其超参数的选择都是依赖于上下文的,并且没有总是有效的通用规则。因此,对于每一个问题,都需要进行初步分析,通常还需要进行网格搜索,以做出最合适的选择。一旦选择了内核,问题可以用下面的方式表示:

没有完整的讨论(这超出了本书的范围),我们可以将注意力集中在几个重要的元素上。首先,决策功能如下:

解决方案中涉及的数学过程允许我们简化以下表达式,但是,出于我们的目的,最好保留原始表达式。如果读者具有监督学习的基本知识,他们可以容易地理解权重向量和样本的投影之间的点积 x i 允许确定 x i 相对于分离超平面的位置。事实上,如果两个向量之间的角度小于 90°(π/2),点积是非负的。当角度正好为 90°时,它等于零(也就是说,向量是正交的),当角度在 90°和 180°之间时,它是负的。该过程如下图所示:

Decision process in SVM

权重向量与分离超平面正交。样本 x i 被识别为内联,因为点积为正且大于阈值 ρ 。相反, x j 被标记为异常值,因为决策函数的符号为负。术语ξII≥0)被称为松弛变量,并被引入以允许在离群值和内联值之间有更灵活的边界。事实上,如果这些变量都等于零(为了简单起见,还有 ρ=1 ),那么施加在优化问题上的条件就变成了:

这意味着所有训练样本必须被视为内联样本,因此,必须选择分离超平面,使得所有 x i 都在同一侧。然而,通过定义软边界,松弛变量的使用允许更大的灵活性。每个训练样本都与一个变量 ξ i 相关联,当然,这个问题要求它们最小化。然而,通过这个技巧,一些边界样本也可以位于超平面的相对侧(足够靠近它),即使它们继续被识别为内联。最后一个要考虑的元素是本文中最重要的元素之一,涉及超参数ν∞(0,1) 。在前述论文中,作者证明了每当 ρ ≠ 0 时,ν 可以解释为训练样本分数的上界,实际上是离群值。在这一章的开头,我们已经说明了在一个新奇的检测问题中,数据集必须是干净的。不幸的是,这并不总是正确的;因此, ν 和松弛变量的联合使用也允许我们处理包含一小部分异常值的数据集。在概率方面,如果 X 是从被噪声部分破坏的数据生成过程中提取的, ν 是在 X 中找到异常值的概率。

现在,让我们分析一个基于用元组(年龄、身高)标识的学生数据集的二维示例。我们将从二元高斯分布和 200 个均匀采样的测试点生成 2000 个内联:

import numpy as np

nb_samples = 2000
nb_test_samples = 200

X = np.empty(shape=(nb_samples + nb_test_samples, 2))

X[:nb_samples] = np.random.multivariate_normal([15, 160], np.diag([1.5, 10]), size=nb_samples)
X[nb_samples:, 0] = np.random.uniform(11, 19, size=nb_test_samples)
X[nb_samples:, 1] = np.random.uniform(120, 210, size=nb_test_samples)

由于比例不同,最好在训练模型之前对数据集进行归一化:

from sklearn.preprocessing import StandardScaler

ss = StandardScaler()
Xs = ss.fit_transform(X)

下图显示了标准化数据集的图表:

Dataset for the one-class SVM example

主斑点主要由内联体组成,部分测试样本位于同一高密度区域;因此,我们可以合理地假设在包含所有样本的数据集中约有 20%的异常值(因此, ν=0.2 )。当然,这样的选择是基于我们的一个假设,在任何现实场景中, ν 的值必须始终反映数据集中预期异常值的实际百分比。当这条信息不可用时,最好从一个较大的值开始(例如, ν=0.5 ),并通过减少它来继续,直到找到最佳配置(即,误分类的概率最小)。

同样重要的是要记住,训练过程有时会找到次优解;因此,一些内联可能被标记为异常值。在这些情况下,最好的策略是测试不同核的效果,例如,当使用多项式核时,增加它们的复杂性,直到找到最优解(不一定排除所有错误)。

现在,让我们使用一个径向基函数内核(特别适合高斯数据生成过程)初始化 scikit-learn OneClassSVM类的一个实例,并训练模型:

from sklearn.svm import OneClassSVM

ocsvm = OneClassSVM(kernel='rbf', gamma='scale', nu=0.2)
Ys = ocsvm.fit_predict(Xs)

我们选择了推荐值gamma='scale',该值基于以下公式:

这种选择通常是最佳的起点,可以改变(根据结果是否不可接受而增加或减少)。在我们的例子中,由于数据集是二维的( n=2 )和归一化的 (std(X) = 1)γ = 0.5 ,这对应于单位方差高斯分布(因此,我们应该期望它是最合适的选择)。此时,我们可以通过突出显示异常值来绘制结果:

Classification result (left). Outliers from the test set (right)

从左图中可以看到,该模型已经成功识别了数据集的高密度部分,并且还将密集斑点外部区域的一些样本标记为异常值。它们对应于二元高斯下的低概率值,在我们的例子中,我们假设它们是应该被滤除的噪声样本。在右图中,只能看到异常区域,当然,这是高密度斑点的补充。我们可以总结说,一级 SVM,即使有点容易过度,也能够帮助我们以非常小的错误概率识别新奇事物。这也是由于数据集的结构(然而,这在许多情况下非常常见),可以使用径向基函数内核轻松管理。不幸的是,对于高维数据,这种简单性往往会丧失,需要更彻底的超参数搜索来最小化错误率。

利用隔离森林进行异常检测

刘 F T,Ting K M,周 z 在文章隔离森林,第八届 IEEE 国际数据挖掘会议, 2008)中提出了一种非常强大的异常检测方法,它基于集成学习的一般框架。由于这个主题非常广泛,并且主要涵盖在有监督的机器学习书籍中,我们邀请读者在必要时查看建议的资源之一。相反,在这种情况下,我们将描述模型,而不是非常强烈地引用所有潜在的理论。

先说一个森林是一组独立的模型叫做决策树。顾名思义,它们不仅仅是算法,而是划分数据集的一种非常实用的方法。从根开始,对于每个节点,选择一个特征和一个阈值,并将样本分成两个子集(对于非二叉树不是这样,但一般来说,所有涉及的树都是这些模型是二进制的),如下图所示:

Generic structure of a binary decision tree

在有监督的任务中,选择元组(特征、阈值)是根据特定的标准来选择的,这些标准最小化了子对象的杂质。这意味着目标通常是分割一个节点,使得结果子集包含属于单个类的大多数样本。当然,这很容易理解,当所有的叶子都是纯的或者达到最大深度时,这个过程就结束了。相反,在这个特定的上下文中,我们从一个非常特殊的(但经验证明的)假设开始:如果属于隔离森林的树是在每次选择随机特征和随机阈值时生长的,则从根到包含任何内联的叶子的路径的平均长度比隔离离群点所需的路径长。这一假设的原因可以通过考虑一个二维例子很容易理解,如作者所示:

Bidimensional random partitioning. On the left, an inlier is isolated. On the right, an outlier belonging to a low-density region is detected

可以观察到,内联体通常属于高密度区域,需要更多的分区来隔离样本。相反,位于低密度区域的异常值可以用较少的划分步骤来检测,因为所需的粒度与斑点的密度成比例。因此,构建隔离林的目标是测量所有内联器的平均路径长度,并将其与新样本所需的路径长度进行比较。当这样的长度较短时,成为异常值的概率增加。作者提出的异常分数基于指数函数:

在前面的公式中, m 是属于训练集 Xavg(h(XI)是考虑所有树的样本 x i 的平均路径长度, c(m) 是只依赖于 m 的归一化项。当 s(x i ,m) → 1 时,样品xIT19】被确定为异常。因此,由于s(**)被限制在 0 和 1 之间,如果我们考虑 0.5 的阈值,则正常样本与值 s(x i ,m) < < 0.5 相关联。

现在让我们考虑一下葡萄酒数据集,它包含 178 个样本 x i ∈ ℜ 13 ,其中每个特征都是特定的化学属性(例如,酒精、苹果酸、灰分等),并训练一个隔离森林来检测一种新的葡萄酒是可以被认为是内联的(例如,现有品牌的变体)还是离群的,因为它的化学属性与每个现有样本都不兼容。第一步包括加载和规范化数据集:

import numpy as np

from sklearn.datasets import load_wine
from sklearn.preprocessing import StandardScaler

wine = load_wine()
X = wine['data'].astype(np.float64)

ss = StandardScaler()
X = ss.fit_transform(X)

我们现在可以实例化一个IsolationForest类并设置最重要的超参数。第一个是n_estimators=150,通知模型训练 150 棵树。另一个基本参数(类似于一类支持向量机中的 ν )称为contamination,其值表示训练集中异常值的预期百分比。因为我们信任数据集,所以我们选择了一个等于 0.01 (1%)的值来处理可以忽略不计的奇怪样本。出于兼容性原因,插入了behaviour='new'参数(查看官方文档了解更多信息)random_state=1000保证实验的可重复性。一旦类被初始化,就可以训练模型:

from sklearn.ensemble import IsolationForest

isf = IsolationForest(n_estimators=150, behaviour='new', contamination=0.01, random_state=1000)
Y_pred = isf.fit_predict(X)

print('Outliers in the training set: {}'.format(np.sum(Y_pred == -1)))

上一个片段的输出是:

2

因此,隔离林已经成功识别了 178 个内联器中的 176 个。我们可以接受这个结果,但是,像往常一样,我建议调整参数,以便获得一个与每个特定情况兼容的模型。此时,我们可以生成几个噪声样本:

import numpy as np

X_test_1 = np.mean(X) + np.random.normal(0.0, 1.0, size=(50, 13))
X_test_2 = np.mean(X) + np.random.normal(0.0, 2.0, size=(50, 13))
X_test = np.concatenate([X_test_1, X_test_2], axis=0)

测试集被分成两个块。第一个数组X_test_1包含噪声水平相对较低的样本( σ=1 ),而第二个数组X_test_2包含更多噪声样本( σ=2 )。因此,我们预计第一组中的异常值数量较少,第二组中的异常值数量较多。数组X_test是两个测试集的有序连接。现在让我们预测一下状态。由于这些值是双极性的,我们希望将它们与训练结果区分开来,因此我们将乘以预测时间2(即,-1表示训练集中的异常值,1表示训练集中的内联值,-2表示测试集中的异常值,2表示测试集中的内联值):

Y_test = isf.predict(X_test) * 2

Xf = np.concatenate([X, X_test], axis=0)
Yf = np.concatenate([Y_pred, Y_test], axis=0)

print(Yf[::-1])

输出如下:

[ 2 2 -2 -2 -2 -2 -2 2 2 2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 2 -2 2 2 -2 -2 -2 2 -2 -2 -2 -2 2 2 -2 -2 -2 -2 -2 -2 2 2 -2 2 -2 2 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 -2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 -1 1 1 1 1 1 1 1 1 1 1 -1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1]

随着顺序的保留和反转,我们可以看到属于X_test_2(高方差)的大部分样本被归类为异常,而低方差的大部分样本被识别为内联。为了得到进一步的视觉确认,我们可以执行 t-SNE 降维,考虑到最终结果是二维分布,其与原始(13 维)分布的库尔巴克-莱布勒散度最低。这意味着所得维度的可解释性非常低,并且该图只能用于理解二维空间的哪些区域更有可能被内联体占据:

from sklearn.manifold import TSNE

tsne = TSNE(n_components=2, perplexity=5, n_iter=5000, random_state=1000)
X_tsne = tsne.fit_transform(Xf)

结果图如下图所示:

t-SNE plot for the novelty detection with the wine dataset

可以看到,许多接近训练内联器的样本本身就是内联器,一般来说,几乎所有远测试样本都是异常值。然而,由于强降维,很难得出更多的结论。然而,我们知道当噪声足够小时,找到内联的概率很大(这是一个合理的结果)。作为练习,我邀请读者检查单个化学属性(可在https://sci kit-learn . org/stable/datasets/index . html # wine-dataset上获得),并针对每个属性或针对组,找出将内联者转化为外联者的阈值(例如,回答这个问题:与训练集兼容的最大酒精量是多少?).

摘要

在这一章中,我们讨论了概率密度函数的性质,以及如何利用它们来计算实际概率和相对可能性。我们已经看到了如何创建直方图,这是将值分组到预定义的箱中后表示值的频率的最简单方法。由于直方图有一些重要的限制(它们非常不连续,很难找到最佳的面元大小),我们引入了核密度估计的概念,这是一种使用平滑函数估计密度的稍微复杂的方法。

我们分析了最常见的核(高斯核、Epanechnikov 核、指数核和均匀核)的特性,以及两种可用于找出每个数据集的最佳带宽的经验方法。使用这样的技术,我们试图解决一个基于合成数据集的非常简单的单变量问题。我们分析了 KDD 杯 99 数据集的 HTTP 子集,其中包含了几个正常和恶意网络连接的日志记录。我们使用 KDE 技术创建了一个基于两个阈值的简单异常检测系统,我们还解释了在处理这类问题时必须考虑哪些因素。

在最后一部分,我们分析了两种常见的方法,可以用来进行新颖性检测。单类支持向量机利用核的能力将复杂的数据集投影到特征空间,在那里它们可以线性分离。下一步是基于假设所有的训练集(除了一小部分)都是内联的,因此它们属于同一个类。该模型以最大化内联体和特征空间原点之间的分离为目标进行训练,结果基于样本相对于分离超平面的位置。相反,隔离森林是基于假设的集成模型,即在随机训练的决策树中,从根到样本的路径对于异常值来说平均较短。

因此,在训练森林之后,可以考虑给定新样本的平均路径长度来计算异常分数。当这样的分数接近 1 时,我们就可以断定出现异常的概率也是非常大的。相反,非常小的分值表明新奇事物反而是潜在的内联。

在下一章中,我们将讨论最常见的降维和字典学习技术,当需要管理具有大量特征的数据集时,这些技术非常有用。

问题

  1. 一个人身高 1.70 m 的概率是 p(高)= 0.75 ,而明天要下雨的概率是 P(雨)= 0.2P(高,雨)的概率是多少?(即一个人身高 1.70 m,明天要下雨的概率)。
  2. 给定一个数据集 X ,我们构建一个包含 1000 个面元的直方图,我们发现其中许多面元是空的。为什么会这样?
  3. 直方图包含三个面元,分别包含 20、30 和 25 个样本。第一个仓的范围为 0 < x < 2 ,第二个 2 < x < 4 ,第三个 4 < x < 6P(x) > 2 的大概概率是多少?
  4. 给定正态分布 N(0,1) ,样本 xp(x) = 0.35 是否可以认为是异常?
  5. 有 500 个样本的数据集 X标准值(X) = 2.5IQR 值(X) = 3.0 。最佳带宽是多少?
  6. 一位专家告诉我们,一个分布在两个值附近达到极大的峰值,密度突然从峰值的平均值下降 0.2 个标准差。哪种内核最合适?
  7. 给定一个样本 x (从 10,000 个样本的流动人群中收集),我们不确定这是异常还是新奇,因为 p(x) = 0.0005 。在另外 10,000 次观察之后,我们重新训练模型并且 x 保持 p(x) < 0.001 。我们能断定 x 是异常吗?

进一步阅读

  • Epanechnikov V . A,多元概率密度的非参数估计,概率论及其应用,14, 1969

  • 《数理统计年鉴》,1962 年

  • 谢瑟·S·J,六种流行的带宽选择方法在一些真实数据集上的性能(附讨论),计算统计学,7, 1992

  • 施科尔科夫,普拉特·J·C,肖-泰勒·J·C,斯摩拉·A·J,威廉姆森·R·C,估计高维分布的支持,神经计算,13/7, 2001

  • 刘 F T,丁 K M,周 z,隔离森林,ICDM 2008第八届 IEEE 数据挖掘国际会议,2008

  • 《理论神经科学》,麻省理工学院出版社,2005 年

  • 机器学习算法第二版博纳科索格帕克特出版,2018

七、降维和成分分析

在这一章,我们将介绍和讨论一些非常重要的技术,可以用来进行降维和组件提取。在前一种情况下,目标是将高维数据集转换为低维数据集,以尽量减少信息损失。后者是一个过程,需要找到一个可以混合的原子字典,以建立样本。

特别是,我们将讨论以下主题:

  • 主成分分析 ( 主成分分析
  • 奇异值分解 ( 奇异值分解)和白化
  • 核主成分分析
  • 稀疏主成分分析和字典学习
  • 要素分析
  • 独立成分分析 ( ICA
  • 非负矩阵分解 ( NNMF )
  • 潜在狄利克雷分配 ( LDA )

技术要求

本章将介绍的代码需要以下内容:

示例可在 GitHub 资源库中获得,网址为https://GitHub . com/PacktPublishing/HandsOn-Unsupervised-Learning-with-Python/tree/master/chapter 07

主成分分析

降低数据集维数的最常见方法之一是基于样本协方差矩阵的分析。一般来说,我们知道随机变量的信息含量与其方差成正比。例如,给定一个多变量高斯,熵(我们用来测量信息的数学表达式)如下:

上式中,σ为协方差矩阵。如果我们(不失一般性)假设σ是对角的,那么很容易理解熵(按比例)大于每个单个分量的方差, σ i 2 。这并不奇怪,因为一个低方差的随机变量集中在均值附近,惊喜的概率很低。另一方面,当 σ 2 变得越来越大时,潜在的结果随着不确定性而增加,这与信息量成正比。

当然,组件的影响一般是不同的;因此,主成分分析 ( PCA )的目标是找到样本的线性变换,能够将它们投影到更低维的子空间上,从而保留最大可能量的初始方差。实际上,让我们考虑一个数据集,x∈ℜm×nT7:

我们想要找到的线性变换是一个新的数据集,如下所示:

在应用这样的转换之后,我们期望得到以下结果:

让我们开始考虑样本协方差矩阵(出于我们的目的,我们也可以采用有偏估计);为简单起见,我们还假设 X 的平均值为零:

这样的矩阵是对称的,是正半定的(不熟悉这些概念也没关系,但是对于证明后面的步骤很重要),所以它的特征向量构成了正交基。简单回顾一下,如果 A 是正方形矩阵,向量 v i 被称为与特征值相关联的特征向量 λ i ,如果下列情况成立:

换句话说,特征向量被转换成其自身的扩展或收缩版本(不能发生旋转)。证明协方差矩阵的特征向量定义协方差分量的方向(即数据集具有特定协方差分量的方向)并不难(但所有数学细节将被省略)。然而,原因很简单;事实上,在变换之后,新的协方差矩阵(变换数据集的, Z )是不相关的(即它是对角的),因为新的轴与协方差分量对齐。这意味着一个 versor(例如, v 0 = (1,0,0,...,0) )转化为σI2vI,所以是一个关联特征值与 i th 分量方差成正比的特征向量。

因此,为了找出哪些元素可以被丢弃,我们可以对特征值进行排序,从而得出以下结论:

对应的特征向量( v 1 ,v 2 ,...,v n )分别确定最大方差对应的分量,以此类推,直到最后一个。形式上,我们定义这样的特征向量为主成分;因此,第一主成分是与最大方差相关的方向,第二主成分与第一主成分正交,并且它与第二大方差相关,以此类推。在二维数据集的情况下,这个概念显示在下面的屏幕截图中:

Principal components of a bidimensional dataset; the first principal component lies along the axis with the largest variance, while the second one is orthogonal, and it's proportional to the residual variance

至此,问题差不多解决了;事实上,如果我们只选择第一个 k 主成分( v i ∈ ℜ n × 1 ),我们就可以构建一个变换矩阵,ak∈ℜn×k,从而将与第一个 k 特征值关联的特征向量作为行:

因此,我们可以通过使用以下矩阵乘法来转换整个数据集:

新数据集 Z 的维度等于 k <(或< < ) n ,并且它包含与组件数量成比例的原始方差量。例如,考虑到上一个截图中显示的例子,如果我们选择单个分量,所有的向量都被转换成沿着第一个主分量的点。当然,有一些信息的丢失,这必须逐案考虑;在接下来的几节中,我们将讨论如何评估这种损失并做出合理的决定。现在,我们将简要说明如何以有效的方式提取主成分。

奇异值分解的主成分分析

即使我们将采用完整的主成分分析实现,了解如何有效地执行这样的过程也是有帮助的。当然,最明显的方法是基于样本协方差矩阵的计算,它的特征分解(将输出特征值和相应的特征向量),然后最终,有可能建立转换矩阵。这种方法很简单,但不幸的是,它的效率也很低。主要原因是我们需要计算样本协方差矩阵,这对于大型数据集来说可能是一项非常长的任务。

奇异值分解 ( SVD )提供了一种高效得多的方式,这是一个线性代数过程,具有一些重要的特性:它可以直接对数据集进行操作,当提取到所需数量的组件时可以停止,并且有可以小批量工作的增量版本,克服了内存不足的问题。特别是考虑到数据集 X ∈ ℜ m × n ,SVD 可以表示如下:

U 为酉矩阵(即UUT= UTU = I,故UT= U-1)包含作为行的左手奇异向量( XX T 的特征向量); V (也是酉)包含右奇异向量作为行(对应于 X T X 的特征向量),而λ是包含sT29】的奇异值的对角矩阵(它们是XXTT33】和 X T 的特征值的平方根特征值按降序排序,特征向量重新排列以匹配相应的位置。由于 1/m 因子是乘法常数,不影响特征值的相对大小;因此,排序顺序保持不变。因此,我们可以直接使用 VU 并从λ中选择第一个顶级 k 特征值。特别是我们可以观察到以下结果(作为变换矩阵, A ,等于 V ):**

因此,通过使用 U k (仅包含顶部 k 特征向量)和λkT9】(仅包含顶部 k 特征值)的截断版本,我们可以直接获得低维变换数据集(具有 k 分量),如下所示:

这种方法快速、有效,并且当数据集太大而无法放入内存时,可以很容易地进行缩放。即使我们在本书中没有处理这样的场景,提到 scikit-learn TruncatedSVD类(执行仅限于 k 顶特征值的 SVD)和IncrementalPCA类(对小批量执行 PCA)也是有帮助的。出于我们的目的,我们将使用标准的PCA类和一些重要的变体,它们要求整个数据集适合内存。

白粉

奇异值分解的一个重要应用是白化过程,该过程强制数据集 X 具有零均值(即 E[X] = 0 或零中心),以具有单位协方差矩阵 C (它是实对称的)。这种方法非常有助于提高许多监督算法的性能,这些算法可以受益于所有组件共享的统一单一方差。

将分解应用于 C ,我们得到以下结果:

矩阵 V 的列是 C 的特征向量,而λ是包含特征值的对角矩阵(记住 SVD 输出奇异值,奇异值是特征向量的平方根)。因此,我们需要找到一个线性变换, z = Ax ,这样E【ZTZ】= I。当使用前面的分解时,这很简单:

从前面的等式中,我们可以推导出变换矩阵的表达式 A :

现在,我们将使用一个小的测试数据集展示美白的效果,如下所示:

import numpy as np

from sklearn.datasets import make_blobs

X, _ = make_blobs(n_samples=300, centers=1, cluster_std=2.5, random_state=1000)

print(np.cov(X.T))

显示数据集协方差矩阵的上一个块的输出如下:

[[6.37258226 0.40799363]
 [0.40799363 6.32083501]]

用于在通用数据集上执行白化的whiten()函数(零中心化是过程的一部分)如下所示(白化后correct参数强制进行比例校正):

import numpy as np

def zero_center(X):
    return X - np.mean(X, axis=0)

def whiten(X, correct=True):
    Xc = zero_center(X)
    _, L, V = np.linalg.svd(Xc)
    W = np.dot(V.T, np.diag(1.0 / L))
    return np.dot(Xc, W) * np.sqrt(X.shape[0]) if correct else 1.0

应用于X数组的白化结果如下截图所示:

Original dataset (left); whitened dataset (right)

我们现在可以检查新的协方差矩阵,如下所示:

import numpy as np

Xw = whiten(X)
print(np.cov(Xw.T))

输出如下:

[[1.00334448e+00 1.78229783e-17]
 [1.78229783e-17 1.00334448e+00]]

可以看到,矩阵现在是一个恒等式(误差最小),数据集也有一个空平均值。

MNIST 数据集的主成分分析

现在,让我们应用主成分分析,以降低 MNIST 数据集的维度。我们将使用 scikit-learn 提供的压缩版本(1,797,8 × 8 图像),但我们的任何考虑都不会受到这一选择的影响。让我们从加载和规范化数据集开始:

from sklearn.datasets import load_digits

digits = load_digits()
X = digits['data'] / np.max(digits['data'])

从理论讨论中,我们知道协方差矩阵特征值的大小与相应主成分的相对重要性(即解释的方差,因此也是信息量)成正比。因此,如果按降序排序,可以计算以下差异:

当组件数量 k → n 时,重要性趋于降低,我们可以通过选取第一个最大差异来选择最佳的 k ,这表明以下所有组件解释的差异量大幅下降。为了更好地理解这种机制,让我们计算特征值及其差(作为协方差矩阵, C ,是正半定的,我们确定λI≥0∀I∑(1,n) ):

import numpy as np

C = np.cov(X.T)
l, v = np.linalg.eig(C)
l = np.sort(l)[::-1]
d = l[:l.shape[0]-1] - l[1:]

展平图像(64 维阵列)的差异如下图所示:

Eigenvalue differences for each principal component

可以看出,第一主成分的差异非常大,对应于第四主成分(λ43T5)达到最大值;但是,接下来的差别还是很大的,而对应λ6T9】则有一个急跌。在这一点上,趋势几乎是稳定的(除了一些残余振荡),直到 λ 11 ,然后开始非常迅速地下降,趋于零。由于我们还是要有正方形的图像,我们就要选择 k = 16 (相当于每边除以四)。在另一个任务中,你可以选择,例如 k = 15 ,甚至k = 8;然而,为了更好地理解降维引起的误差,分析解释的方差也将是有帮助的。因此,让我们从执行主成分分析开始:**

from sklearn.decomposition import PCA

pca = PCA(n_components=16, random_state=1000)
digits_pca = pca.fit_transform(X)

digits_pca阵列是在拟合模型并将所有样本投影到对应于前 16 个主成分的子空间之后获得的。如果我们想要将原始图像与其重建进行比较,我们需要调用inverse_transform()方法,该方法执行到原始空间的投影。所以,如果 PCA 在本例中是一个变换,f(x):ℜ64→ℜ16T7】,逆变换是g(z):ℜ16→ℜ64t13】。下面的截图显示了前 10 位数字及其重构之间的比较:**

Original samples (top row); reconstructions (bottom row)

重建很明显是有损耗的,但是数字仍然是可区分的。现在,让我们通过对explained_variance_ratio_数组的所有值求和来检查总的解释方差,该数组包含每个分量的解释方差的相对量(因此任何 k < n 分量的和总是小于 1):

print(np.sum(pca.explained_variance_ratio_))

上一个片段的输出如下:

0.8493974642542452

因此,在降维到 16 个分量的情况下,我们解释了大约 85%的原始方差,这是一个合理的值,考虑到我们为每个样本丢弃了 48 个分量。

显示所有单个贡献的图如下截图所示:

Explained variance ratio corresponding to each principal component

正如预期的那样,这种贡献往往会减少,因为在这种情况下,第一个主要组成部分是负责任的;例如,对于颜色,一条线(如黑色或白色),而其余的线形成灰色阴影。这种行为非常普遍,几乎在每种情况下都能观察到。通过这个图表,也很容易找到额外的损失,以便进一步减少。例如,我们可以立即发现,对 3 个分量的严格限制将解释大约 40%的原始方差;所以,剩下的 45%被分成了剩下的 13 个部分。我邀请你重复这个例子,试图找到人类区分所有数字所需的最小组件数。

核主成分分析

有时,数据集不是线性可分的,标准的主成分分析不能提取正确的主成分。这个过程与第 3 章高级聚类中讨论的过程没有什么不同,当时我们面对的是非凸聚类的问题。在这种情况下,由于几何形状的原因,一些算法无法成功分离。在这种情况下,目标是根据主成分的结构来区分不同的类(在纯无监督的场景中,我们考虑特定的分组)。因此,我们希望处理转换后的数据集, Z ,并检测可区分阈值的存在。例如,让我们考虑以下截图:

Original dataset (left); PCA projected version (right)

由于原始数据集是线性可分的,在 PCA 投影后,我们可以立即找到允许检测第一个分量的阈值(这是唯一真正需要的),以便区分两个斑点。但是,如果数据集不是线性可分的,我们会得到一个不可接受的结果,如下图所示:

Original dataset (left); PCA projected version (right)

当几何形状更复杂时,找到可区分的阈值可能是不可能的。然而,我们知道将数据投影到更高维的空间可以使它们线性分离。特别是如果 x ∈ ℜ n ,我们可以选择一个合适的函数, f(x) ,这样y = f(x)∈ℜpT9】,用p>t23】n。不幸的是,将这种转换应用于整个数据集可能非常昂贵;实际上,给定一个变换矩阵, A ,(带 n 个分量),单个主分量,A(t),投影后,可以写成如下形式(记住它们是协方差矩阵的特征向量):

因此,单个向量的变换如下:

可以看到,变换需要点积的计算,f(xI)Tf(xI)。在这些情况下,我们可以使用所谓的内核技巧,它表示有特定的函数 K(,),称为内核,具有有趣的属性,如下所示:

换句话说,我们可以通过简单地计算每两个点的核来计算在高维空间中主成分上的投影,而不是执行点积,这需要在计算f()之后进行 n 次乘法。

一些常见的内核如下:

  • 径向基函数 ( 径向基函数,或高斯核:
  • 多项式核,次数为 p :
  • 乙状结肠核:

对于非常大的数据集,这个过程仍然相当昂贵(但是为了避免额外的时间,可以预先计算和存储内核值),但是它比标准的投影要高效得多。此外,它的优点是允许在可能进行线性判别的空间中提取主成分。现在,让我们将径向基函数核主成分分析应用于上一张截图中显示的半月形数据集。gamma参数等于1/σ2T4。在这种特殊情况下,主要问题是存在双重重叠。考虑到原始标准差约为 1.0(也就是 σ 2 = 1 ,我们至少需要三个标准差才能恰当区分;因此,我们将设置 γ = 10 :

from sklearn.datasets import make_moons
from sklearn.decomposition import KernelPCA

X, Y = make_moons(n_samples=800, noise=0.05, random_state=1000)

kpca = KernelPCA(n_components=2, kernel='rbf', gamma=10.0, random_state=1000)
X_pca = kpca.fit_transform(X)

投影结果如下图所示:

Original dataset (left); kernel PCA projected version (right)

可以看到,即使在这种情况下,第一个组件也足以做出决定(由于噪声,具有最小容差),将阈值设置为零可以分离数据集。我邀请读者测试其他内核的效果并应用它们,以便区分包含全 0 和全 1 的 MNIST 子集。

通过因子分析增加异方差噪声的鲁棒性

标准主成分分析的主要问题之一是这种模型在异方差噪声方面的固有弱点。如果你不熟悉这个术语,引入两个定义会很有帮助。多元去相关噪声项的特征在于对角协方差矩阵 C ,其可以具有两种不同的配置,如下所示:

  • C = diag(σ 2 ,σ 2 ,...,σ 2 ) :这种情况下,噪声定义为同态(所有分量方差相同)。
  • C = diag(σ1T3】2,σ2T7】2,...,σn2**),同σ12≠σ22...≠ σ n 2 :这种情况下,噪声定义为异方差(每个分量都有自己的方差)。

有可能证明,当噪声是同态时,主成分分析可以很容易地管理它,因为单个成分的解释方差以同样的方式受到噪声项的影响(也就是说,这相当于没有噪声)。相反,当噪声为异方差时,主成分分析的性能下降,结果可能是绝对不可接受的。为此,Rubin 和 Thayer(在 ML 因子分析的 EM 算法中,Rubin D .和 Thayer D .,mentorimetrika,47,1982)提出了一种替代的降维方法,称为因子分析,可以解决这类问题。

假设我们有一个以零为中心的数据集, X ,包含 m 样本xI∈ℜnT9】。我们的目标是找到一组潜在变量,zI∈ℜp(带 p < n )和一个矩阵, A (称为因子加载矩阵),这样每个样本都可以重写,如下所示:

因此,我们现在假设一个样本, x i 是一组高斯潜变量加上一个额外的异方差噪声项的组合。由于潜在变量的维数较低,这个问题与标准主成分分析非常相似,主要区别在于,现在我们考虑了异方差噪声(当然,术语 n 也可以为空,或同方差)。因此,当确定分量(即潜在变量)时,不同噪声方差的影响被包括在模型中,最终效果是部分滤波(去噪)。在前面提到的论文中,作者提出了一种优化算法,它在形式上并不复杂,但需要许多数学运算(为此,我们省略了任何证明)。该方法基于期望最大化 ( EM )算法,便于找到最大化对数似然的参数集。在本书中,我们不讨论所有的数学细节(可以在原始论文中找到),而是检查这种方法的属性,并将结果与标准主成分分析进行比较。

让我们从加载 Olivetti faces 数据集开始,将其置零,并创建一个异方差噪声版本,如下所示:

import numpy as np

from sklearn.datasets import fetch_olivetti_faces

faces = fetch_olivetti_faces(shuffle=True, random_state=1000)
X = faces['data']
Xz = X - np.mean(X, axis=0)

C = np.diag(np.random.uniform(0.0, 0.1, size=Xz.shape[1]))
Xnz = Xz + np.random.multivariate_normal(np.zeros(shape=Xz.shape[1]), C, size=Xz.shape[0])

下面的截图显示了一些原始且有噪声的图像:

Original images (upper line); noisy versions (lower line)

现在,让我们评估平均对数似然性(通过score()方法,在PCAFactorAnalysis类中都可用)如下:

  • 主成分分析,带有原始数据集和128组件
  • 主成分分析,带噪声数据集和128组件
  • 因子分析,含噪声数据集和128成分(潜在变量)

在下面的代码片段中,所有 3 个模型都被实例化和训练:

from sklearn.decomposition import PCA, FactorAnalysis

pca = PCA(n_components=128, random_state=1000)
pca.fit(Xz)
print('PCA log-likelihood(Xz): {}'.format(pca.score(Xz)))

pcan = PCA(n_components=128, random_state=1000)
pcan.fit(Xnz)
print('PCA log-likelihood(Xnz): {}'.format(pcan.score(Xnz)))

fa = FactorAnalysis(n_components=128, random_state=1000)
fa.fit(Xnz)
print('Factor Analysis log-likelihood(Xnz): {}'.format(fa.score(Xnz)))

上一个片段的输出如下:

PCA log-likelihood(Xz): 4657.3828125
PCA log-likelihood(Xnz): -2426.302304948351
Factor Analysis log-likelihood(Xnz): 1459.2912218162423

这些结果表明了在异方差噪声存在时因子分析的有效性。主成分分析获得的最大平均对数似然约为4657,在有噪声的情况下降至-2426。相反,因子分析获得的平均对数似然值约为 1,460,比使用主成分分析获得的对数似然值大得多(即使噪声的影响尚未完全滤除)。因此,每当数据集包含(或数据科学家怀疑它包含)异方差噪声时(例如,样本是通过不同仪器捕获的源的叠加获得的),我强烈建议将因子分析作为主要的降维方法。当然,如果需要其他条件(例如,非线性、稀疏性等),可以在做出最终决定之前评估本章中讨论的其他方法。

稀疏主成分分析和字典学习

标准 PCA 一般是密集分解;也就是说,向量一旦被变换,就是所有具有非零系数的分量的线性组合:

在前面的表达式中,系数 α i 几乎总是不同于零,因此所有的组件都参与了重建过程。出于降维的目的,这不是一个问题,因为我们对每个组件解释的方差更感兴趣,以便限制它们。然而,对于一些任务,分析更大的一组建筑原子是有帮助的,假设每个向量可以表示为它们的稀疏组合。最经典的例子是文本语料库,其中词典包含的术语比每个文档中包含的术语多得多。这种模型通常被称为字典学习算法,因为原子集合定义了一种字典,包含了所有可以用来创建新样本的单词。当原子的数量 k 大于样本的维度 n 时,字典称为过完备,表示往往比较稀疏。反之,当 k < n 时,字典称为欠完备,向量需要更密集。

通过对解的 L 1 范数施加惩罚,这样的学习问题可以很容易地通过函数的最小化来解决。这种约束导致稀疏的原因超出了本书的范围,但感兴趣的人可以在掌握机器学习算法, 博纳科尔索 g .**Packt Publications2018 中找到更长的讨论。

字典学习(以及稀疏主成分分析)的问题可以正式表达如下:

这是一种算法的特殊情况,其中组件 U k 被强制具有单位长度(除非有normalize_components=False参数),系数 V 被惩罚,以便增加它们的稀疏度(与系数成比例, α )。

让我们考虑 MNIST 数据集,执行 30 个分量的稀疏主成分分析(产生不完全字典)和中高稀疏度水平(例如, α = 2.0 )。数组X应该包含归一化样本,如下面的主成分分析示例所示:

from sklearn.decomposition import SparsePCA

spca = SparsePCA(n_components=30, alpha=2.0, normalize_components=True, random_state=1000)
spca.fit(X)

在训练过程结束时,components_数组包含原子,如下图所示:

Components extracted by the sparse PCA algorithm

不难理解,每个数字都可以用这些原子组成;然而,考虑到原子的数量,稀疏的数量不可能非常大。我们来考虑一下,比如数字X[0]的变换:

y = spca.transform(X[0].reshape(1, -1)).squeeze()

系数的绝对值如下图所示:

Absolute coefficients for the sparse transformation of the digit X[0]

显然有一些主导成分(例如 2、7、13、17、21、24、26、27 和 30),一些次要成分(例如 5、8 等)和一些无效或可忽略的成分(例如 1、3、6 等)。如果以相同的码长(30 个分量)增加稀疏度,则空分量对应的系数将降至零,而如果同样增加码长(例如 k = 100 ,则字典将变得过完备,空系数的数量也将增加。

非负矩阵分解

当数据集 X 为非负时,有可能应用因子分解技术,当任务的目标是提取与样本的结构部分相对应的原子时,该技术已被证明(例如在通过非负矩阵因子分解学习物体的部分,Lee D. D .,和 Seung,S. H .,Nature, 401,10/1999)更可靠。例如,在图像的情况下,它们应该是几何元素或者甚至更复杂的部分。非负矩阵分解 ( NNMF )强加的主要条件是所有涉及的矩阵必须是非负的并且 X = UV 。因此,一旦定义了一个规范 N (例如,弗罗贝尼乌斯),简单的目标就变成了以下内容:

当还需要稀疏性时,这通常是不可接受的(此外,为了允许更大的灵活性来改变解决方案以满足特定的要求),该问题通常通过在 Frobenius(对 L 2 的矩阵扩展)和 L 1 规范(例如,在 ElasticNet 中)上增加惩罚来表达(例如在 scikit-learn 中):

双重正则化通过避免类似于监督模型的过拟合的效果(由于解是次优的,它也更灵活地适应从相同的数据生成过程中提取的新样本),允许您获得样本的稀疏性和部分之间的更好匹配;这增加了通常可实现的可能性)。

现在,让我们考虑 MNIST 数据集,让我们将其分解为 50 个原子,最初设置 α = 2.0β = 0.1 (在 scikit-learn 中称为l1_ratio)。这种配置将强制中等稀疏度和强L2/弗罗贝纽斯正则化。该过程简单明了,类似于稀疏主成分分析:

from sklearn.decomposition import NMF

nmf = NMF(n_components=50, alpha=2.0, l1_ratio=0.1, random_state=1000)
nmf.fit(X)

在培训过程结束时,组件(原子)如下图所示:

Atoms extracted by the NNMF algorithm

与我们在标准词典学习中观察到的相反,原子现在的结构更加结构化,它们再现了数字的特定部分(例如,垂直或水平笔画、圆圈、点等);因此,我们可以期待更多的稀疏表示,因为更少的组件足以构建一个数字。考虑到上一节所示的例子(数字X[0],所有组件的绝对贡献如下图所示:

Absolute coefficients for the NNMF of the digit X[0]

三个成分占优势(3、24 和 45);因此,我们可以尝试将样本表示为它们的组合。系数分别为 0.19、0.18 和 0.16。结果如下截图所示(数字X[0]表示零):

Deconstruction of the digit X[0], based on three main components

有趣的是该算法是如何选择原子的。即使这个过程受到 αβ 参数的强烈影响,通过范数,我们可以观察到,例如第三个原子(截图中的第一个)可以被许多零、三、八共享;最后一个原子对 0 和 9 都有帮助。每当原子的粒度太粗时,带有较弱的 L 1 惩罚的过完备字典可能会有所帮助。当然,每个问题都需要具体的解决方案;因此,我强烈建议用领域专家来检查原子的结构。作为练习,我邀请您将 NNMF 应用于另一个小图像数据集(例如,Olivetti、Cifar-10 或 STL-10),并尝试找到隔离固定数量的结构部分所需的正确参数(例如,对于面部,它们可以是眼睛、鼻子和嘴巴)。

独立成分分析

当使用标准主成分分析(或其他技术,如因子分析)时,这些成分是不相关的,但不能保证它们在统计上是独立的。换句话说,假设我们有一个数据集, X ,从联合概率分布中得出,p(X);如果有 n 组件,我们不能总是确定以下等式成立:

然而,有许多重要的任务,基于一个叫做鸡尾酒会的常见模式。在这种情况下,我们可以假设(或者我们知道)许多不同且独立的来源(例如,声音和音乐)重叠并产生单个信号。此时,我们的目标是通过对每个样本应用线性变换来尝试分离源。让我们考虑一个白化的数据集, X (因此所有的成分都具有相同的信息内容),我们可以假设它是从高斯分布 N(0,I) 中采样的(这不是限制性条件,因为许多不同来源的重叠很容易收敛到正态分布)。因此,目标可以表达如下:

换句话说,我们将每个样本表示为多个独立因素的乘积,具有基于指数函数的先验分布。唯一必须绝对执行的条件是非高斯性(否则,组件变得不可区分);因此,函数 f k (z) 不能是二次多项式。在实践中,我们也希望包括适度的稀疏性,因此我们期望峰值和重尾分布(也就是说,概率仅在非常短的范围内很高,然后突然下降到几乎为零)。这种情况可以通过检查归一化的第四矩来验证,称为峰度:

对于高斯分布,峰度为 3。由于这通常是一个参考值,所有具有峰度(X) > 3 的分布称为超高斯或细峰度,而具有峰度(X) < 3 的分布称为亚高斯或扁峰度。前一类分布的一个例子是拉普拉斯分布,如下图所示:

Probability density functions of Gaussian distribution (left) and Laplace distribution (right)

不幸的是,峰度的使用因其相对于异常值缺乏鲁棒性而受到阻碍(也就是说,由于它涉及四次幂,即使很小的值也可以被放大并改变最终结果;例如,尾部有异常值的噪声高斯可以表现为超高斯)。为此,作者 Hyvarinen 和 Oja(在独立成分分析:算法与应用,Hyvarinen A .和 Oja,e .神经网络 13,2000)基于负熵的概念,提出了一种称为快速独立成分分析(FT6】astICAT8】)的算法。我们不打算在这本书里描述整个模型;然而,理解基本思想是有帮助的。可以证明,在所有方差相同的分布中,高斯分布的熵最大。因此,如果数据集 X (零中心)是从具有协方差的分布σ中提取的,则可以将 X 的负熵定义为高斯N(0;σ)X的熵:

因此,我们的目标是通过减少 H(X) 来减少 H N (X) (总是大于等于零)。FastICA 算法基于通过特定函数的组合对 H N (X) 的近似。最常见的称为 logcosh (也是 scikit-learn 中的默认值),如下所示:

有了这个技巧,负熵可以更容易地优化,最终的分解肯定包含独立的成分。现在,让我们将 FastICA 算法应用于 MNIST 数据集(为了强制更好的精度,我们正在设置max_iter=10000tol=1e-5):

from sklearn.decomposition import FastICA

ica = FastICA(n_components=50, max_iter=10000, tol=1e-5, random_state=1000)
ica.fit(X)

算法找到的 50 个独立组件(始终可通过components_实例变量获得)如下截图所示:

Independent components extracted by FastICA

在这种情况下,组件可以立即识别为数字的一部分(考虑到数据集的维度,我邀请读者通过将组件的数量减少和增加到 64 个来重复该示例,这是最大数量)。分量趋向于达到相应分布的平均位置;因此,用更少的数量,可以区分更结构化的模式(可以被认为是不同的重叠信号),而更大数量的组件导致更多以特征为中心的元素。然而,与 NNMF 相反,FastICA 并不保证提取样本的实际部分,而是提取更完整的区域。换句话说,虽然 NNMF 可以很容易地检测到,例如,一些单笔画,但快速独立分量分析倾向于将样本视为不同信号的总和,在图像的情况下,这通常涉及样本的整个维度,除非分量的数量急剧增加。为了更好地理解这个概念,让我们考虑奥利维蒂人脸数据集,它包含 400 幅 64 × 64 灰度肖像:

from sklearn.datasets import fetch_olivetti_faces

faces = fetch_olivetti_faces(shuffle=True, random_state=1000)

下面的截图显示了前 10 张脸:

Sample faces extracted from the Olivetti faces dataset

现在,让我们提取 100 个独立的组件:

ica = FastICA(n_components=100, max_iter=10000, tol=1e-5, random_state=1000)
ica.fit(faces['data'])

前 50 个组件绘制在下面的截图中:

50 (out of 100) independent components extracted by FastICA

正如你所看到的,每个组件都类似于一个元面(有时称为特征面),由于所有剩余的特征(即使它们在精确的样本集中不能立即识别),它们具有一些特定的、主要的特征以及次要的贡献。当组件数量增加到 350 时,效果变得更加明显,如下所示:

50 (out of 350) independent components extracted by FastICA

在这种情况下,次要特征不太占优势,因为有更多的重叠分布,并且它们中的每一个都集中在更原子的特征面上。当然,没有完整的领域知识,无法定义组件的最佳数量。例如,在 Olivetti 人脸数据集的情况下,识别特定的子元素(例如,眼镜的位置)或更完整的面部表情可能会有所帮助。在前一种情况下,较大数量的组件产生更集中的解决方案(即使它们在全局上不太容易区分),而在后一种情况下,较小数量的组件(如前一个示例)产生更完整的结果,在这种情况下可以评估不同的影响因素。就信号而言,分量的数量应该等于预期重叠因子的数量(假设它们是独立的)。例如,音频信号可以包含一个人在机场说话的录音,背景声音宣布航班。在这种情况下,场景可以由三个部分组成:两个声音和噪音。由于噪声将被部分分解为主要成分,最终数量将等于 2。

基于潜在狄利克雷分配的主题建模

我们现在将考虑另一种在处理文本文档时非常有用的分解(也就是 NLP)。理论部分不是很容易,因为需要很深的概率论和统计学习知识(可以在原论文潜伏狄利克雷分配,机器学习研究杂志, Blei D.Ng A.和 Jordan M. ,3,(2003)993-1022);因此,我们只讨论主要元素,没有任何数学参考(更紧凑的描述也出现在机器学习算法第二版,Bonaccorso,g ., Packt Publications ,2018)。让我们考虑一组文本文档, d j (称为语料库),其原子(或成分)是单词, w i :

收集所有单词后,我们可以建立一个字典:

我们还可以陈述以下不等式(N()计算一个集合的元素数):

这意味着文档之间的单词分布是稀疏的,因为单个文档中只使用了几个单词,而前者的选择是对称狄利克雷分布(模型以其命名),这是极其稀疏的(而且,它是分类分布的共轭先验,是一个一次多项式分布,所以很容易纳入模型)。概率密度函数(由于分布是对称的,参数 α i = α ∀ i )如下:

现在,让我们考虑将文档语义分组为主题, t k ,并且假设每个主题都由少量特殊单词表征:

这意味着话题之间的词语分布也是稀疏的。所以我们有全联合概率(单词、话题),我们要确定条件概率 p(w i |t k )p(tk| wI)。换句话说,给定一个文档,它是术语的集合(每个术语都有一个边际概率 p(w i ) ,我们要计算这样的文档属于特定主题的概率。由于文档被柔和地分配给所有的主题(也就是说,它可以不同程度地属于多个主题),我们需要考虑一个稀疏的主题-文档分布,从中可以得出主题-混合( θ i ):

类似地,我们需要考虑话题词分布(因为一个词可以不同程度地被更多话题分享),从中我们可以得出话题词混合样本, β j :

潜在狄利克雷分配 ( LDA )是一个生成模型(训练目标,以一种简单的方式,包括找到最优参数 αγ ),该模型能够从语料库中提取固定数量的主题,并用一组词来表征它们。给定一个样本文档,它能够通过提供主题混合概率向量( θ i = (p(t 1 )、p(t 2 )将其分配给一个主题,...,p(tk));它还可以处理看不见的文档(使用相同的字典)。

现在,让我们将 LDA 应用于 20 个新闻组数据集的一个子集,该子集包含数千条公开发布用于自然语言处理研究的消息。特别是,我们想要对rec.autoscomp.sys.mac.hardware子组进行建模。我们可以使用内置的 scikit-learn fetch_20newsgroups()功能,要求去掉所有不必要的页眉、页脚和引号(其他帖子附在答案上):

from sklearn.datasets import fetch_20newsgroups

news = fetch_20newsgroups(subset='all', categories=('rec.autos', 'comp.sys.mac.hardware'), remove=('headers', 'footers', 'quotes'), random_state=1000)

corpus = news['data']
labels = news['target']

此时,我们需要对语料库进行矢量化。换句话说,我们需要将每个文档转换成包含词汇表中每个单词的频率(计数)的稀疏向量:

我们将使用CountVectorizer类来执行这一步,要求去掉重音并删除相对频率很高但不具有代表性的终止词(例如,英语中的 and、the 等)。此外,我们正在强制标记器排除所有非纯文本的标记(通过设置token_pattern='[a-z]+')。在其他情况下,这种模式可能会有所不同,但在这种情况下,我们不想依赖数字和符号:

from sklearn.feature_extraction.text import CountVectorizer

cv = CountVectorizer(strip_accents='unicode', stop_words='english', analyzer='word', token_pattern='[a-z]+')
Xc = cv.fit_transform(corpus)

print(len(cv.vocabulary_))

上一个片段的输出如下:

14182

因此,每个文档都是一个 14,182 维的稀疏向量(很明显,大多数值都是空的)。我们现在可以通过强加n_components=2来执行 LDA,因为我们期望提取两个主题:

from sklearn.decomposition import LatentDirichletAllocation

lda = LatentDirichletAllocation(n_components=2, learning_method='online', max_iter=100, random_state=1000)
Xl = lda.fit_transform(Xc)

训练过程结束后,components_实例变量包含每对(单词和话题)的相对频率(以计数表示)。因此,在我们的例子中,它的形状是(2,14,182),而components_[i, j]元素,用 i ∈ (0,1)j ∈ (0,14,181) ,可以解释为这个词的重要性, j ,为了定义这个话题, i 。因此,我们将有兴趣检查,例如,这两个主题的前 10 个单词:

import numpy as np

Mwts_lda = np.argsort(lda.components_, axis=1)[::-1]

for t in range(2):
    print('\nTopic ' + str(t))
    for i in range(10):
        print(cv.get_feature_names()[Mwts_lda[t, i]])

输出如下:

Topic 0
compresion
progress
deliberate
dependency
preemptive
wv
nmsu
bpp
coexist
logically

Topic 1
argues
compromising
overtorque
moly
forbid
cautioned
sauber
explosion
eventual
agressive

很容易(考虑到一些非常特殊的术语)理解Topic 0被分配给comp.sys.mac.hardware,另一个被分配给rec.autos(不幸的是,这个过程不能基于自动检测,因为语义必须由人类来解释)。为了评估模型,让我们考虑两个示例消息,如下所示:

print(corpus[100])
print(corpus[200])

输出限于几行,如下所示:

I'm trying to find some information on accelerator boards for the SE. Has
anyone used any in the past, especially those from Extreme Systems, Novy or
MacProducts? I'm looking for a board that will support extended video,
especially Radius's two-page monitor. Has anyone used Connectix Virtual in
conjunction with their board? Any software snafus? Are there any stats
anywhere on the speed difference between a board with an FPU and one
without? Please send mail directly to me. Thanks.

...

The new Cruisers DO NOT have independent suspension in the front.  They
still
run a straight axle, but with coils.  The 4Runner is the one with
independent
front.  The Cruisers have incredible wheel travel with this system. 

The 91-up Cruiser does have full time 4WD, but the center diff locks in
low range.  My brother has a 91 and is an incredibly sturdy vehicle which
has done all the 4+ trails in Moab without a tow.  The 93 and later is even
better with the bigger engine and locking diffs.

所以第一个帖子明显和图形有关,第二个是政治信息。让我们计算两者的主题混合,如下所示:

print(Xl[100])
print(Xl[200])

输出如下:

[0.98512538 0.01487462]
[0.01528335 0.98471665]

因此,第一条消息属于Topic 0的概率约为 98%,而第二条消息几乎不属于Topic 1。这证实了分解工作正常。为了更好地了解总体分布情况,将属于每个类别的消息的混合可视化将会很有帮助,如下图所示:

Topic-mixtures for comp.sys.mac.hardware (left) and rec.autos (right)

如你所见,主题几乎是正交的。属于rec.autos的大部分消息都有 p(t 0 ) < 0.5p(t 1 ) > 0.5 ,而comp.sys.mac.hardware有轻微的重叠,其中没有p(t0)>0.5p(t 1】的消息组这可能是因为存在可以将两个主题限定为同等重要性的词语(例如,术语讨论辩论可以同等地出现在两个新闻组中)。作为练习,我邀请您使用更多子集,并尝试证明主题的正交性,并检测可能导致不正确作业的单词。

摘要

在这一章中,我们介绍了可用于降维和字典学习的不同技术。主成分分析是一种非常众所周知的方法,它涉及找到与方差较大的方向相关联的数据集的最重要的组成部分。这种方法具有对角化协方差矩阵和提供每个特征重要性的直接度量的双重效果,从而简化选择并最大化残差解释方差(可以用较少数量的分量解释的方差量)。由于主成分分析本质上是一种线性方法,它不能经常用于非线性数据集。为此,开发了一个基于内核的变体。在我们的例子中,您看到了径向基函数核如何能够将非线性可分数据集投影到子空间上,其中主成分分析可以确定判别分量。

稀疏主成分分析和字典学习是广泛使用的技术,当需要提取可以混合(线性组合)的建筑原子以产生样本时使用。在许多情况下,目标是找到一个所谓的过完备字典,这相当于说我们期望比实际用于构建每个样本的原子多得多的原子(这就是为什么表示是稀疏的)。虽然主成分分析可以提取不相关的成分,但它很少找不到统计独立的成分。为此,我们引入了独立分量分析的概念,这是一种为了从样本中提取重叠源而开发的技术,这些样本可以被认为是独立原因(例如,声音或视觉元素)的总和。另一种具有特殊特征的方法是 NNMF,它可以产生稀疏表示和一组类似于样本特定部分的成分(例如,对于一张脸,它们可以代表眼睛、鼻子等)。最后一节介绍了 LDA 的概念,LDA 是一种主题建模技术,在给定一个文档语料库(即一个文档属于每个特定主题的概率)的情况下,可以用来找到主题混合。

在下一章中,我们将介绍一些基于无监督范式的神经模型。具体来说,将讨论深度信念网络、自动编码器和无需协方差矩阵的特征分解(nor SVD)就能提取数据集主成分的模型。

问题

  1. 数据集 X 有一个协方差矩阵 C=diag(2,1) 。你对 PCA 有什么期待?
  2. 考虑到前面的问题,如果 X 以零为中心,球 B 0.5 (0,0) 为空,我们能假设 x = 0 (第一主成分)的阈值允许水平判别吗?
  3. 主成分分析提取的成分在统计上是独立的。这是正确的吗?
  4. 一个 Kurt(X) = 5 的分布适合 ICA。这是正确的吗?
  5. 包含样本( 1,2 )和( 0,-3 )的数据集 X 的 NNMF 是什么?
  6. 一个包含 10 个文档的语料库与一个包含 10 个术语的词典相关联。我们知道每份文件的固定长度是 30 个字。字典是不是太全了?
  7. 核主成分分析采用二次核。如果原始维数为 2,那么进行 PCA 的新空间的维数是多少?

进一步阅读

  • 稀疏编码在线词典学习,J. MairalF .巴赫J. Ponce和 G. Sapiro ,2009
  • 通过非负矩阵分解学习物体的各个部分李德德承宪自然,401,10/1999
  • 用于 ML 因子分析的 EM 算法鲁宾 D.和泰尔 D.心理测量学卡,47,1982
  • 独立成分分析:算法和应用海瓦里宁和欧嘉神经网络 13,2000
  • 信息论的数学基础钦钦人工智能多佛出版物
  • 潜在狄利克雷分配,机器学习研究杂志布莱 D.Ng A.和乔丹 M. ,3,(2003) 993-1022
  • 机器学习算法第二版博纳科索格帕克特出版,2018
  • 掌握机器学习算法博纳科索格帕克特出版,2018

八、无监督神经网络模型

在本章中,我们将讨论一些可用于无监督任务的神经模型。神经网络(通常是深度神经网络)的选择允许您解决具有需要复杂处理单元(例如图像)的特定特征的高维数据集的复杂性。

特别是,我们将涵盖以下内容:

  • 自动编码器
  • 去噪自动编码器
  • 稀疏自动编码器
  • 可变自动编码器
  • 主成分分析神经网络;
  • 桑格网络
  • Rubner-Tavan 的网络
  • 无监督的深度信念网络 ( DBN )

技术要求

本章中的代码要求如下:

这些例子可以在 GitHub 资源库中找到,网址为https://GitHub . com/packktpublishing/HandsOn-Unsupervised-Learning-with-Python/chapter 08

自动编码器

第 7 章降维和分量分析中,我们讨论了一些常见的方法,这些方法可以用来降低数据集的维数,给定其特有的统计特性(例如,协方差矩阵)。然而,当复杂度增加时,即使核主成分分析 ( k ernel PCA )也可能无法找到合适的低维表示。换句话说,信息的丢失可以克服一个阈值,这个阈值保证了有效重建样本的可能性。自动编码器是利用神经网络的极端非线性的模型,以便找到给定数据集的低维表示。具体来说,我们假设 X 是从数据生成过程中抽取的一组样本, p data (x) 。为简单起见,我们将考虑xI∈ℜn,但对支架的结构没有限制(例如,对于 RGB 图像,xI∈ℜn×m×3)。自动编码器在形式上分为两个部分:一个是编码器,将高维输入转换为较短的代码,另一个是解码器,执行相反的操作(如下图所示):

Structural schema of a generic autoencoder

如果代码是一个 p 维向量,编码器可以定义为一个参数化函数,e():

以类似的方式,解码器是另一个参数化函数,d():

因此,一个完整的自动编码器是一个复合函数,给定一个输入样本, x i ,它提供一个最优重构作为输出:

由于自动编码器通常通过神经网络实现,因此使用反向传播算法训练自动编码器,通常基于均方误差成本函数:

或者,考虑到数据生成过程,我们可以考虑参数化条件分布q()来重新表达目标:

因此,成本函数现在可以变成例如 p 数据()q()之间的库尔巴克-莱布勒散度:

由于 p 数据 的熵为常数,可以通过优化过程排除;因此,散度的最小化相当于最小化 p 数据q 之间的交叉熵。如果假设 p 数据q 为高斯型,则库尔巴克-莱布勒成本函数相当于均方误差,因此两种方法可以互换。在某些情况下,当数据在(0,1)范围内归一化时,可以对 p 数据q 采用伯努利分布。形式上,这并不完全正确,因为伯努利分布是二元的并且xI∑{ 0,1 }d;然而,sigmoid 输出单元的使用也保证了连续样本的成功优化,xI∑(0,1) d 。在这种情况下,成本函数如下:

深度卷积自动编码器示例

让我们实现一个基于 TensorFlow 和 Olivetti faces 数据集的深度卷积自动编码器(该数据集相对较小,但提供了良好的表达能力)。让我们从加载图像和准备训练集开始:

from sklearn.datasets import fetch_olivetti_faces

faces = fetch_olivetti_faces(shuffle=True, random_state=1000)
X_train = faces['images']

这些样本是 400,64 × 64 灰度图像,我们将把它们的大小调整到 32 × 32,以加快计算速度并避免内存问题(此操作会导致视觉精度略有下降,如果您有足够的计算资源,可以将其删除)。我们现在可以定义主要常数(纪元数(nb_epochs)、batch_sizecode_length)和graph:

import tensorflow as tf

nb_epochs = 600
batch_size = 50
code_length = 256 
width = 32
height = 32

graph = tf.Graph()

因此,我们将为 600 个时代训练模型,每批 50 个样本。由于每幅图像为 64 × 64 = 4,096 ,压缩比为 4,096/256 = 16 倍。当然,这种选择并不是一个规则,我邀请您始终检查不同的配置,以便最大限度地提高收敛速度和最终精度。在我们的案例中,我们使用以下层对编码器进行建模:

  • 2D 卷积与 16 (3 × 3)滤波器,(2 × 2)步距,ReLU 激活和相同的填充
  • 2D 卷积与 32 (3 × 3)滤波器、(1 × 1)步距、ReLU 激活和相同的填充
  • 2D 卷积与 64 (3 × 3)滤波器、(1 × 1)步距、ReLU 激活和相同的填充
  • 2D 卷积与 128 (3 × 3)滤波器、(1 × 1)步距、ReLU 激活和相同的填充

解码器利用转置卷积序列(也称为去卷积):

  • 2D 转置卷积具有 128 (3 × 3)个滤波器、(2 × 2)个步长、ReLU 激活和相同的填充
  • 2D 转置卷积具有 64 (3 × 3)个滤波器,(1 × 1)步距,ReLU 激活和相同的填充
  • 2D 转置卷积具有 32 (3 × 3)个滤波器、(1 × 1)个步长、ReLU 激活和相同的填充
  • 具有 1 (3 × 3)滤波器、(1 × 1)步距、Sigmoid 激活和相同填充的 2D 转置卷积

损失函数基于重建图像和原始图像之间差异的 L 2 范数。优化器是亚当,学习率 η=0.001 。TensorFlow DAG 的编码器部分如下:

import tensorflow as tf

with graph.as_default():
    input_images_xl = tf.placeholder(tf.float32, 
                                     shape=(None, X_train.shape[1], X_train.shape[2], 1))
    input_images = tf.image.resize_images(input_images_xl, (width, height), 
                                          method=tf.image.ResizeMethod.BICUBIC)

    # Encoder
    conv_0 = tf.layers.conv2d(inputs=input_images,
                              filters=16,
                              kernel_size=(3, 3),
                              strides=(2, 2),
                              activation=tf.nn.relu,
                              padding='same')

    conv_1 = tf.layers.conv2d(inputs=conv_0,
                              filters=32,
                              kernel_size=(3, 3),
                              activation=tf.nn.relu,
                              padding='same')

    conv_2 = tf.layers.conv2d(inputs=conv_1,
                              filters=64,
                              kernel_size=(3, 3),
                              activation=tf.nn.relu,
                              padding='same')

    conv_3 = tf.layers.conv2d(inputs=conv_2,
                              filters=128,
                              kernel_size=(3, 3),
                              activation=tf.nn.relu,
                              padding='same')

DAG 的代码部分如下:

import tensorflow as tf

with graph.as_default():   
    # Code layer
    code_input = tf.layers.flatten(inputs=conv_3)

    code_layer = tf.layers.dense(inputs=code_input,
                                 units=code_length,
                                 activation=tf.nn.sigmoid)

    code_mean = tf.reduce_mean(code_layer, axis=1)

DAG 的解码器部分如下:

import tensorflow as tf

with graph.as_default(): 
    # Decoder
    decoder_input = tf.reshape(code_layer, (-1, int(width / 2), int(height / 2), 1))

    convt_0 = tf.layers.conv2d_transpose(inputs=decoder_input,
                                         filters=128,
                                         kernel_size=(3, 3),
                                         strides=(2, 2),
                                         activation=tf.nn.relu,
                                         padding='same')

    convt_1 = tf.layers.conv2d_transpose(inputs=convt_0,
                                         filters=64,
                                         kernel_size=(3, 3),
                                         activation=tf.nn.relu,
                                         padding='same')

    convt_2 = tf.layers.conv2d_transpose(inputs=convt_1,
                                         filters=32,
                                         kernel_size=(3, 3),
                                         activation=tf.nn.relu,
                                         padding='same')

    convt_3 = tf.layers.conv2d_transpose(inputs=convt_2,
                                         filters=1,
                                         kernel_size=(3, 3),
                                         activation=tf.sigmoid,
                                         padding='same')

    output_images = tf.image.resize_images(convt_3, (X_train.shape[1], X_train.shape[2]), 
                                           method=tf.image.ResizeMethod.BICUBIC)

loss函数和 Adam 优化器在以下代码片段中定义:

import tensorflow as tf

with graph.as_default():
    # Loss
    loss = tf.nn.l2_loss(convt_3 - input_images)

    # Training step
    training_step = tf.train.AdamOptimizer(0.001).minimize(loss)

一旦定义了完整的 DAG,我们就可以初始化会话和所有变量:

import tensorflow as tf

session = tf.InteractiveSession(graph=graph)
tf.global_variables_initializer().run()

一旦初始化了 TensorFlow,就可以开始训练过程,如下所示:

import numpy as np

for e in range(nb_epochs):
    np.random.shuffle(X_train)

    total_loss = 0.0
    code_means = []

    for i in range(0, X_train.shape[0] - batch_size, batch_size):
        X = np.expand_dims(X_train[i:i + batch_size, :, :], axis=3).astype(np.float32)

        _, n_loss, c_mean = session.run([training_step, loss, code_mean],
                                        feed_dict={
                                            input_images_xl: X
                                        })
        total_loss += n_loss
        code_means.append(c_mean)

    print('Epoch {}) Average loss per sample: {} (Code mean: {})'.
          format(e + 1, total_loss / float(X_train.shape[0]), np.mean(code_means)))

上一个片段的输出如下:

Epoch 1) Average loss per sample: 11.933397521972656 (Code mean: 0.5420681238174438)
Epoch 2) Average loss per sample: 10.294102325439454 (Code mean: 0.4132006764411926)
Epoch 3) Average loss per sample: 9.917563934326171 (Code mean: 0.38105469942092896)
...
Epoch 600) Average loss per sample: 0.4635812330245972 (Code mean: 0.42368677258491516)

在训练过程结束时,每个样本的平均损失约为 0.46(考虑 32 × 32 个图像),代码的平均值为 0.42。该值表示编码相当密集,因为单个值预期在范围(0,1)内均匀分布;因此,平均值为 0.5。在这种情况下,我们对这个数据不感兴趣,但是我们也将在寻找稀疏性时比较结果。

下图显示了一些示例图像的自动编码器输出:

Sample output of the deep convolutional autoencoder

放大到 64 × 64 部分影响重建质量;然而,通过降低压缩率和增加代码长度可以获得更好的结果。

去噪自动编码器

自动编码器的一个非常有用的应用并不严格与它们寻找低维表示的能力相关,而是依赖于从输入到输出的转换过程。特别是,让我们假设一个以零为中心的数据集, X ,以及一个噪声版本,其样本具有以下结构:

在这种情况下,自动编码器的目标是去除噪声项并恢复原始样本,xIT3。从数学角度来看,标准和去噪自动编码器没有特别的区别;但是,考虑此类型号的容量需求非常重要。由于它们必须恢复原始样本,给定一个损坏的输入(其特征占据更大的样本空间),图层的数量和尺寸可能比标准自动编码器更大。当然,考虑到复杂性,不经过几次测试是不可能有清晰的洞察力的;因此,我强烈建议从较小的模型开始,增加容量,直到最佳成本函数达到合适的值。对于噪声的添加,有几种可能的策略:

  • 腐蚀每批样品(在整个时代)。
  • 使用噪声层作为编码器的输入 1。
  • 使用缺失层作为编码器的输入 1(例如,带有椒盐噪声)。在这种情况下,丢失的概率可以是固定的,或者可以在预定义的间隔内随机采样(例如,(0.1,0.5))。

如果假设噪声是高斯噪声(这是最常见的选择),就有可能同时产生同异方差噪声和异方差噪声。在第一种情况下,所有分量的方差保持不变(即N(I)∞N(0,σ 2 I) ),而在后一种情况下,每个分量都有自己的方差。根据问题的性质,另一种解决方案可能更合适;然而,当没有限制时,为了提高系统的整体鲁棒性,最好使用异方差噪声。

向深度卷积自动编码器添加噪声

在本例中,我们将修改之前开发的深度卷积自动编码器,以便管理有噪声的输入样本。DAG 几乎是等效的,不同的是,现在,我们需要同时提供噪声图像和原始图像:

import tensorflow as tf

with graph.as_default():
    input_images_xl = tf.placeholder(tf.float32, 
                                     shape=(None, X_train.shape[1], X_train.shape[2], 1))
    input_noisy_images_xl = tf.placeholder(tf.float32, 
                                           shape=(None, X_train.shape[1], X_train.shape[2], 1))

    input_images = tf.image.resize_images(input_images_xl, (width, height), 
                                          method=tf.image.ResizeMethod.BICUBIC)
    input_noisy_images = tf.image.resize_images(input_noisy_images_xl, (width, height), 
                                                method=tf.image.ResizeMethod.BICUBIC)

    # Encoder
    conv_0 = tf.layers.conv2d(inputs=input_noisy_images,
                              filters=16,
                              kernel_size=(3, 3),
                              strides=(2, 2),
                              activation=tf.nn.relu,
                              padding='same')
...

loss函数当然是通过考虑原始图像来计算的:

...

# Loss
loss = tf.nn.l2_loss(convt_3 - input_images)

# Training step
training_step = tf.train.AdamOptimizer(0.001).minimize(loss)

变量标准初始化后,我们可以开始训练过程,考虑一个加性噪声,NI∞N(0,0.45) (即 σ ≈ 0.2 ):

import numpy as np

for e in range(nb_epochs):
    np.random.shuffle(X_train)

    total_loss = 0.0
    code_means = []

    for i in range(0, X_train.shape[0] - batch_size, batch_size):
        X = np.expand_dims(X_train[i:i + batch_size, :, :], axis=3).astype(np.float32)
        Xn = np.clip(X + np.random.normal(0.0, 0.2, size=(batch_size, X_train.shape[1], X_train.shape[2], 1)), 0.0, 1.0)

        _, n_loss, c_mean = session.run([training_step, loss, code_mean],
                                        feed_dict={
                                            input_images_xl: X,
                                            input_noisy_images_xl: Xn
                                        })
        total_loss += n_loss
        code_means.append(c_mean)

    print('Epoch {}) Average loss per sample: {} (Code mean: {})'.
          format(e + 1, total_loss / float(X_train.shape[0]), np.mean(code_means)))

一旦模型经过训练,就有可能用一些有噪声的样本来测试它。结果显示在下面的截图中:

Noisy samples (upper row); denoised images (lower row)

正如您所看到的,自动编码器已经成功地学会了如何对输入图像进行降噪,即使它们已经非常损坏。我邀请您用其他数据集测试模型,寻找允许合理良好重建的最大噪声方差。

稀疏自动编码器

标准自动编码器生成的代码通常很密集;然而,正如第 7 章降维和成分分析中所讨论的,有时,最好使用过完备的字典和稀疏编码。实现这一目标的主要策略是简单地在成本函数中添加一个 L 1 惩罚(在代码层上):

α 常数决定了将要达到的稀疏程度。当然,由于CsT5 的最优值与原值不对应,为了达到同样的精度,往往需要更多的纪元和更长的码层。吴恩达提出的另一种方法(在斯坦福大学的稀疏自动编码器、* CS294A 中)是基于一种稍微不同的方法。代码层被认为是一组独立的伯努利随机变量;因此,给定另一组具有小平均值的伯努利变量(例如,pr∞B(0.05)),就有可能尝试找到最佳代码,该代码也使 z i 和这样的参考分布之间的 kulback-Leibler 散度最小化:*

因此,新的成本函数如下:

最终的效果与使用L1T3】点球取得的效果没有太大区别。事实上,在这两种情况下,模型都被迫学习次优表示,还试图最小化一个目标,如果单独考虑,该目标将导致输出代码始终为空。因此,全代价函数将达到保证重构能力和稀疏性的最小值(稀疏性必须总是与代码长度相平衡)。因此,一般来说,代码越长,可能实现的稀疏性就越大。

向深度卷积自动编码器添加稀疏性约束

在这个例子中,我们希望通过使用L1T4】惩罚来增加代码的稀疏性。DAG 和训练过程与主例完全相同,唯一不同的是loss函数,现在变成如下:

...
sparsity_constraint = 0.01 * tf.reduce_sum(tf.norm(code_layer, ord=1, axis=1))
loss = tf.nn.l2_loss(convt_3 - input_images) + sparsity_constraint
...

我们添加了稀疏约束α= 0.01;因此,我们可以通过检查平均代码长度来重新训练模型。该过程的输出如下:

Epoch 1) Average loss per sample: 12.785746307373048 (Code mean: 0.30300647020339966)
Epoch 2) Average loss per sample: 10.576686706542969 (Code mean: 0.16661183536052704)
Epoch 3) Average loss per sample: 10.204148864746093 (Code mean: 0.15442773699760437)
...
Epoch 600) Average loss per sample: 0.8058895015716553 (Code mean: 0.028538944199681282)

如您所见,代码现在变得非常稀疏,最终平均值约为 0.03。这条信息表明,大多数代码值接近于零,在解码图像时只能考虑其中的少数几个。作为练习,我邀请您分析一组选定图像的代码,试图根据它们的激活/失活来理解它们的值的语义。

可变自动编码器

让我们考虑一个数据集, X ,来自一个数据生成过程, p 数据 。变分自动编码器是一种生成模型(基于标准自动编码器的主要概念),由 Kingma 和 Welling(在自动编码变分贝叶斯中,Kingma D. P .和 Welling m .**ArXiv:1312.6114【stat。ML] ),旨在再现数据生成过程。为了实现这个目标,我们需要从一个基于一组潜在变量 z 和一组可学习参数 θ 的通用模型开始。给定一个样本, x i ∈ X ,模型的概率为 p(x,z; θ) 。因此,训练过程的目标是找到最大化可能性的最佳参数,p(x;θ) ,可通过全联合概率边缘化得到:

前面的表达式很简单,但不幸的是,它很少能以封闭的形式处理。主要原因是我们没有关于先验的有效信息,p(z; θ) 。而且,即使假设例如z∨N(0,σ)(例如 N(0,I) ),找到有效样本的概率也极其稀疏。换句话说,给定一个值 z ,我们也不太可能生成一个实际上属于 p 数据 的样本。为了解决这个问题,作者提出了一种变分方法,我们将简要地看一下(在前面提到的论文中给出了完整的解释)。假设标准自动编码器的结构,我们可以通过将编码器建模为q(z | x;θ q ) 。此时,我们可以计算q()和实际条件概率p(z | x;θ) :

当期望值运算符在 z 上工作时,可以提取最后一个术语并将其移动到表达式的左侧,如下所示:

经过另一个简单的操作,前面的等式变成如下:

左边是模型下样本的对数似然,右边是一个非负项(KL-散度)和另一个名为证据下界 ( ELBO )的项之和:

正如我们将要讨论的,使用 ELBO 比处理公式的剩余部分更容易,并且,由于 KL-散度不能引入负贡献,如果我们最大化 ELBO,我们也最大化对数似然。

我们之前定义了p(z;θ) = N(0,I);因此,我们可以建模q(z | x;**【θ)作为多元高斯,其中两个参数集(均值向量和协方差矩阵)由分裂概率编码器表示。特别地,给定样本 x ,编码器现在必须输出均值向量μ(z | x;θ q ) ,协方差矩阵σ(z | x;θ q ) 。为简单起见,我们可以假设矩阵是对角的,因此两个组件具有完全相同的结构。得到的分布是q(z | x;θq)= N(μ(z | x;θ q ),σ(z | x;θq);因此,ELBO 的第一项是两个高斯分布之间的负 KL-散度:

在上式中, p 是码长,所以是均值和对角协方差向量的维数。右边的表达式非常容易计算,因为σ是对角的(即迹是元素的和,行列式是积)。然而,当采用随机梯度下降 ( SGD )算法时,该公式的最大化虽然正确,但不是可微的操作。为了克服这个问题,作者建议重新参数化分布。

当出现批次时,对正态分布进行采样,得到α∨N(0,I) 。使用该值,可以通过使用概率编码器的输出来构建所需的样本:μ(z | x;θq)+ασ(z | x;θq**)2。这个表达式是可微的,因为 α 在每批期间是恒定的(当然,作为μ(z | x;θ q )σ(z | x;θ q ) 用神经网络参数化,它们是可微的)。

ELBO 右侧的第二项是 log p(x|z)的期望值; θ) 。很容易看出,这样的表达式对应于原始分布和重构之间的交叉熵:

这是标准自动编码器的成本函数,假设使用伯努利分布,我们将把它最小化。所以,公式变成如下:

深度卷积变分自动编码器示例

在这个例子中,我们想要建立和训练一个基于 Olivetti 人脸数据集的深度卷积变分自动编码器。该结构与我们第一个示例中使用的结构非常相似。编码器有以下几层:

  • 2D 卷积与 16 (3 × 3)滤波器,(2 × 2)步距,ReLU 激活和相同的填充
  • 2D 卷积与 32 (3 × 3)滤波器、(1 × 1)步距、ReLU 激活和相同的填充
  • 2D 卷积与 64 (3 × 3)滤波器、(1 × 1)步距、ReLU 激活和相同的填充
  • 2D 卷积与 128 (3 × 3)滤波器、(1 × 1)步距、ReLU 激活和相同的填充

解码器具有以下转置卷积:

  • 2D 转置卷积具有 128 (3 × 3)个滤波器、(2 × 2)个步长、ReLU 激活和相同的填充
  • 2D 转置卷积具有 128 (3 × 3)个滤波器、(2 × 2)个步长、ReLU 激活和相同的填充
  • 2D 转置卷积具有 32 (3 × 3)个滤波器、(1 × 1)个步长、ReLU 激活和相同的填充
  • 具有 1 (3 × 3)滤波器、(1 × 1)步距、Sigmoid 激活和相同填充的 2D 转置卷积

噪音的产生完全由 TensorFlow 管理,它基于理论部分解释的技巧。包含图形定义和编码器的 DAG 的第一部分显示在下面的代码片段中:

import tensorflow as tf

nb_epochs = 800
batch_size = 100
code_length = 512
width = 32
height = 32

graph = tf.Graph()

with graph.as_default():
    input_images_xl = tf.placeholder(tf.float32, 
                                     shape=(batch_size, X_train.shape[1], X_train.shape[2], 1))
    input_images = tf.image.resize_images(input_images_xl, (width, height), 
                                          method=tf.image.ResizeMethod.BICUBIC)

    # Encoder
    conv_0 = tf.layers.conv2d(inputs=input_images,
                              filters=16,
                              kernel_size=(3, 3),
                              strides=(2, 2),
                              activation=tf.nn.relu,
                              padding='same')

    conv_1 = tf.layers.conv2d(inputs=conv_0,
                              filters=32,
                              kernel_size=(3, 3),
                              activation=tf.nn.relu,
                              padding='same')

    conv_2 = tf.layers.conv2d(inputs=conv_1,
                              filters=64,
                              kernel_size=(3, 3),
                              activation=tf.nn.relu,
                              padding='same')

    conv_3 = tf.layers.conv2d(inputs=conv_2,
                              filters=128,
                              kernel_size=(3, 3),
                              activation=tf.nn.relu,
                              padding='same')

DAG 中定义代码层的部分如下:

import tensorflow as tf

with graph.as_default():
    # Code layer
    code_input = tf.layers.flatten(inputs=conv_3)

    code_mean = tf.layers.dense(inputs=code_input,
                                units=width * height)

    code_log_variance = tf.layers.dense(inputs=code_input,
                                        units=width * height)

    code_std = tf.sqrt(tf.exp(code_log_variance))

DAG 的解码器部分如下:

import tensorflow as tf

with graph.as_default():  
    # Decoder
    decoder_input = tf.reshape(sampled_code, (-1, int(width / 4), int(height / 4), 16))

    convt_0 = tf.layers.conv2d_transpose(inputs=decoder_input,
                                         filters=128,
                                         kernel_size=(3, 3),
                                         strides=(2, 2),
                                         activation=tf.nn.relu,
                                         padding='same')

    convt_1 = tf.layers.conv2d_transpose(inputs=convt_0,
                                         filters=128,
                                         kernel_size=(3, 3),
                                         strides=(2, 2),
                                         activation=tf.nn.relu,
                                         padding='same')

    convt_2 = tf.layers.conv2d_transpose(inputs=convt_1,
                                         filters=32,
                                         kernel_size=(3, 3),
                                         activation=tf.nn.relu,
                                         padding='same')

    convt_3 = tf.layers.conv2d_transpose(inputs=convt_2,
                                         filters=1,
                                         kernel_size=(3, 3),
                                         padding='same')

    convt_output = tf.nn.sigmoid(convt_3)

    output_images = tf.image.resize_images(convt_output, (X_train.shape[1], X_train.shape[2]), 
                                           method=tf.image.ResizeMethod.BICUBIC)

DAG 的最后一部分包含损失函数和 Adam 优化器,如下所示:

import tensorflow as tf

with graph.as_default():
    # Loss
    reconstruction = tf.nn.sigmoid_cross_entropy_with_logits(logits=convt_3, labels=input_images)
    kl_divergence = 0.5 * tf.reduce_sum(
            tf.square(code_mean) + tf.square(code_std) - tf.log(1e-8 + tf.square(code_std)) - 1, axis=1)

    loss = tf.reduce_sum(tf.reduce_sum(reconstruction) + kl_divergence)

    # Training step
    training_step = tf.train.AdamOptimizer(0.001).minimize(loss)

损耗函数由两部分组成:

  1. 基于交叉熵的重构损失
  2. 码分布与参考正态分布之间的库尔巴克-莱布勒散度

此时,像往常一样,我们可以初始化会话和所有变量,并开始 800 个时期和每批 100 个样本的训练过程:

import tensorflow as tf
import numpy as np

session = tf.InteractiveSession(graph=graph)
tf.global_variables_initializer().run()

for e in range(nb_epochs):
    np.random.shuffle(X_train)

    total_loss = 0.0

    for i in range(0, X_train.shape[0] - batch_size, batch_size):
        X = np.zeros((batch_size, 64, 64, 1), dtype=np.float32)
        X[:, :, :, 0] = X_train[i:i + batch_size, :, :]

        _, n_loss = session.run([training_step, loss],
                                feed_dict={
                                    input_images_xl: X
                                })
        total_loss += n_loss

    print('Epoch {}) Average loss per sample: {}'.format(e + 1, total_loss / float(batch_size)))

在训练过程的最后,我们可以测试几个样本的重建。结果如下图所示:

Sample reconstructions produced by the variational autoencoder

作为练习,我邀请读者修改 DAG,以便接受通用输入代码并评估模型的生成属性。或者,可以获取训练样本的代码并应用一些噪声,以便观察对输出重建的影响。

基于 Hebbian 的主成分分析

在本节中,我们将分析两个神经模型(桑格和鲁布纳-塔万网络),它们可以执行主成分分析 ( 主成分分析)而不需要特征分解协方差矩阵或执行截断的奇异值分解。它们都是基于赫布边学习的概念(更多细节请参考达扬,p .和雅培,L. F .,理论神经科学,麻省理工学院出版社, 2005 或博纳科索,g .,掌握机器学习算法, Packt,2018),这是最早关于非常简单神经元动力学的数学理论之一。然而,这样的概念有非常有趣的含义,特别是在组件分析领域。为了更好地理解网络的动力学,快速概述神经元的基本模型将是有帮助的。让我们考虑一个输入, x ∈ ℜ n ,以及一个权重向量, w ∈ ℜ n 。神经元执行点积(无偏差),以产生标量输出, y :

现在,如果我们想象两个神经元,第一个叫做突触前单位,另一个叫做突触后单位。 Hebb 法则规定突触强度在突触前和突触后单位输出相同符号的值(特别是两者都为正)时必须增加,而在符号不同时必须减弱。这样一个概念的数学表达如下:

常数 η 是学习速率。完整的分析超出了本书的范围,但是有可能证明一个赫伯神经元(通过一些非常简单的修改,需要控制 w 的生长)修改了突触权重,使得在足够多的迭代之后,它沿着数据集的第一个主成分 X 对齐。从这个结果出发(我们就不证明了),我们可以引入桑格的网络。

桑格网络

桑格的网络模型是由桑格提出的(在桑格,T. D .,单层线性前馈神经网络中的最优无监督学习,神经网络, 2,1989),目的是通过在线过程(相反,标准 PCA 是需要整个数据集的批处理)以降序提取数据集的第一个 k 主成分, X 。即使有一个基于特定版本的奇异值分解的增量算法,这些神经模型的主要优势是它们处理单个样本而不损失性能的内在能力。在展示网络结构之前,有必要先介绍一下对 Hebb 规则的修改,称为 Oja 规则:

引入这一规则是为了解决标准 Hebbian 神经元无限增长的问题。其实很容易理解,如果点积 w T x 为正,δw 会越来越多地通过增加 w 的量级来更新权重。因此,在大量迭代之后,模型可能会遇到溢出。Oja 规则通过引入一个自动限制来克服这个问题,该限制迫使权重大小饱和,而不影响神经元找到第一个主成分方向的能力。实际上,用 w k 表示 k 迭代后的权重向量,可以证明如下:

桑格的网络是基于欧嘉规则的修改版本,定义为广义赫布边学习 ( GHL )。假设我们有一个数据集, X,包含 m 向量,xI∈ℜn。网络结构如下图所示:

Structure of a generic Sanger's network

权重组织成矩阵,W = { Wij}(WijT7】是连接突触前单位 i 与突触后单位 j 的权重);因此,输出的激活可以通过使用以下公式来计算:

然而,在这种网络中,我们更感兴趣的是最终的权重,因为它们必须等于第一个 n 个主成分。不幸的是,如果我们不加任何修改地应用 Oja 规则,所有的神经元都会找到相同的成分(第一个);因此,必须采用不同的策略。从理论上,我们知道主成分必须是正交的;因此,如果w1T5】是第一个分量方向的向量,我们可以强制w2T9】与w1T13】正交,依此类推。该方法基于克-施密特正交化程序。让我们考虑两个向量——已经收敛的 w 1w20,在没有任何干预的情况下,也将收敛到 w 1 。通过考虑这个向量在 w 1 上的投影,我们可以找到 w 20 的正交分量:**

此时,正交分量 w 2 等于以下值:

第三个分量必须正交于 w 1w 2 ,因此必须对所有 n 单元重复该过程,直到最终收敛。此外,我们现在使用的是已经融合的组件,而是一个并行更新的动态系统;因此,有必要将此过程纳入学习规则,如下所示:

之前的更新是指单个权重, w ij ,给定一个输入, x 。很容易理解,第一部分是标准的赫布规则,而剩下的部分是正交项,它扩展到 y i 之前的所有单位。

在矩阵形式中,更新如下:

Tril()函数计算正方形矩阵的下三角形部分。收敛性证明不是无足轻重的,但是在 η 单调递减的温和条件下,可以看到这个模型是如何按降序收敛到第一个 n 主成分的:

这样的约束不难实现;但是,一般来说,算法在 η < 1 时也能达到收敛,并且在迭代过程中保持不变。

桑格网络的一个例子

让我们考虑一个样本二维零中心数据集,它是用 scikit-learn make_blobs()效用函数获得的:

import numpy as np

def zero_center(Xd):
    return Xd - np.mean(Xd, axis=0)

X, _ = make_blobs(n_samples=500, centers=3, cluster_std=[5.0, 1.0, 2.5], random_state=1000)
Xs = zero_center(X)

Q = np.cov(Xs.T)
eigu, eigv = np.linalg.eig(Q)

print('Covariance matrix: {}'.format(Q))
print('Eigenvalues: {}'.format(eigu))
print('Eigenvectors: {}'.format(eigv.T))

上一个片段的输出如下:

Covariance matrix: [[18.14296606  8.15571356]
 [ 8.15571356 22.87011239]]
Eigenvalues: [12.01524122 28.99783723]
Eigenvectors: [[-0.79948496  0.60068611]
 [-0.60068611 -0.79948496]]

特征值分别约为 12 和 29,说明第一主成分(对应转置特征向量矩阵的第一行,所以 (-0.799,0.6) )比第二主成分短很多。当然,在这种情况下,我们已经通过特征分解协方差矩阵计算了主成分,但这只是为了教学目的。桑格网络将按降序提取组件;因此,我们期望找到第二列作为权重矩阵的第一列,第一列作为第二列。让我们从初始化权重和训练常数开始:

import numpy as np

n_components = 2
learning_rate = 0.01
nb_iterations = 5000
t = 0.0

W_sanger = np.random.normal(scale=0.5, size=(n_components, Xs.shape[1]))
W_sanger /= np.linalg.norm(W_sanger, axis=1).reshape((n_components, 1))

In order to reproduce the example, it's necessary to set the random seed equal to 1,000; that is, np.random.seed(1000).

在这种情况下,我们执行固定次数的迭代(5000 次);但是,我邀请您修改该示例,以便使用基于两个后续时间步骤计算的权重之差的范数(例如,Frobenius)的容差和停止标准(该方法可以通过避免无用的迭代来加快训练速度)。

下图显示了初始配置:

Initial configuration of the Sanger's network

此时,我们可以开始训练循环,如下所示:

import numpy as np

for i in range(nb_iterations):
    dw = np.zeros((n_components, Xs.shape[1]))
    t += 1.0

    for j in range(Xs.shape[0]):
        Ysj = np.dot(W_sanger, Xs[j]).reshape((n_components, 1))
        QYd = np.tril(np.dot(Ysj, Ysj.T))
        dw += np.dot(Ysj, Xs[j].reshape((1, X.shape[1]))) - np.dot(QYd, W_sanger)

    W_sanger += (learning_rate / t) * dw
    W_sanger /= np.linalg.norm(W_sanger, axis=1).reshape((n_components, 1))

print('Final weights: {}'.format(W_sanger))
print('Final covariance matrix: {}'.format(np.cov(np.dot(Xs, W_sanger.T).T)))

上一个片段的输出如下:

Final weights: [[-0.60068611 -0.79948496]
 [-0.79948496  0.60068611]]
Final covariance matrix: [[ 2.89978372e+01 -2.31873305e-13]  
 [-2.31873305e-13 1.20152412e+01]]

如您所见,最终协方差矩阵如预期的那样去相关,权重已经收敛到 C 的特征向量。重量(主要部件)的最终配置如下图所示:

Final configuration of the Sanger's network

第一主成分对应权重, w 0 ,最大, w 1 为第二主成分。我邀请您使用高维数据集测试网络,并基于协方差矩阵的奇异值分解或特征分解,将性能与标准算法进行比较。

Rubner-Tavan 的网络

另一个可以进行主成分分析的神经网络是由 Rubner 和 Tavan 提出的(在 Rubner,j .和 Tavan,p .,一个用于主成分分析的自组织网络,欧洲物理学快报, 10(7),1989)。然而,他们的方法是基于协方差矩阵的去相关,协方差矩阵是主成分分析的最终结果(也就是说,它就像用自下而上的策略操作,而标准过程是自上而下的)。让我们考虑一个以零为中心的数据集, X ,以及一个输出为 y ∈ ℜ m 向量的网络。因此,输出分布的协方差矩阵如下:

Structure of a generic Rubner-Tavan's network

如您所见,与桑格网络的主要区别在于每个输出单元之前都有求和节点(第一个除外)。这种方法被称为分层横向连接,因为每个节点 y i (i > 0) 都是由一个直接分量 n i 组成的,加起来就是所有先前的加权输出。因此,假设符号 v (i) ,表示向量的 i th 分量, i ,网络输出如下:

已经证明,这个模型,有一个特定的权重更新规则(我们将讨论),收敛到一个单一的,稳定的,不动点,输出被迫成为相互去相关。从模型的结构来看,操作顺序如下:

  • 第一个输出保持不变
  • 第二输出被迫与第一输出去相关
  • 第三输出被迫与第一和第二输出去相关,以此类推
  • 最后一个输出被迫与所有先前的输出去相关

经过多次迭代,每一次生产,yIyjT5【同】 i ≠ j 变为空, C 变为对角协方差矩阵。此外,在上述论文中,作者证明了特征值(对应于方差)是按降序排序的;因此,可以通过取包含第一个 p 行和列的子矩阵来选择顶部的 p 组件。

Rubner-Tavan 网络通过使用两个不同的规则进行更新,每个权重层一个规则。内部权重 w ij 通过使用 Oja 规则进行更新:

该规则保证了主成分的提取不会出现wijT3】的无限增长。相反,外部权重 v jk 通过使用反希伯来人规则来更新:

前一个公式的第一项-ηy(j)y(k)负责去相关,而第二项类似于 Oja 规则,充当防止权重溢出的自限制正则化器。特别是-ηy(I)y(k)项可以解释为更新规则的反馈信号,δwij,受δvJK项修正的实际输出影响。考虑到桑格网络的行为,不容易理解的是,一旦输出被去相关,内部权重 w ij 就变成正交的,代表 X 的第一主成分。

在矩阵形式中,权重 w ij 可以立即排列成 W = {w ij } ,这样在训练过程结束时,每一列都是 C 的特征向量(降序)。相反,对于外部砝码 v jk ,我们需要再次使用Tril()运算符:

因此,迭代 t+1 时的输出变为如下:

有趣的是,这样的网络有一个循环输出;因此,一旦应用了输入,需要几次迭代才能使 y 稳定下来(理想情况下,更新必须持续到| | y(t+1)-y(t)| |→0)。

鲁布纳-塔万网络的一个例子

在本例中,我们将使用桑格网络示例中定义的数据集,以便使用鲁布纳-塔万网络执行主成分提取。为了方便起见,让我们重新计算特征分解:

import numpy as np

Q = np.cov(Xs.T)
eigu, eigv = np.linalg.eig(Q)

print('Eigenvalues: {}'.format(eigu))
print('Eigenvectors: {}'.format(eigv.T))

上一个片段的输出如下:

Eigenvalues: [12.01524122 28.99783723]
Eigenvectors: [[-0.79948496 0.60068611]
 [-0.60068611 -0.79948496]]

我们现在可以初始化超参数,如下所示:

n_components = 2
learning_rate = 0.0001
max_iterations = 1000
stabilization_cycles = 5
threshold = 0.00001

W = np.random.normal(0.0, 0.5, size=(Xs.shape[1], n_components))
V = np.tril(np.random.normal(0.0, 0.01, size=(n_components, n_components)))
np.fill_diagonal(V, 0.0)

prev_W = np.zeros((Xs.shape[1], n_components))
t = 0

因此,我们选择使用等于 0.00001 的停止阈值(比较基于权重矩阵的两次连续计算的弗罗贝纽斯范数)和最大 1000 次迭代。我们还设置了五个稳定周期和一个固定的学习速率, η=0.0001 。我们可以开始学习过程,如下所示:

import numpy as np

while np.linalg.norm(W - prev_W, ord='fro') > threshold and t < max_iterations:
    prev_W = W.copy()
    t += 1

    for i in range(Xs.shape[0]):
        y_p = np.zeros((n_components, 1))
        xi = np.expand_dims(Xs[i], 1)
        y = None

        for _ in range(stabilization_cycles):
            y = np.dot(W.T, xi) + np.dot(V, y_p)
            y_p = y.copy()

        dW = np.zeros((Xs.shape[1], n_components))
        dV = np.zeros((n_components, n_components))

        for t in range(n_components):
            y2 = np.power(y[t], 2)
            dW[:, t] = np.squeeze((y[t] * xi) + (y2 * np.expand_dims(W[:, t], 1)))
            dV[t, :] = -np.squeeze((y[t] * y) + (y2 * np.expand_dims(V[t, :], 1)))

        W += (learning_rate * dW)
        V += (learning_rate * dV)

        V = np.tril(V)
        np.fill_diagonal(V, 0.0)

        W /= np.linalg.norm(W, axis=0).reshape((1, n_components))

print('Final weights: {}'.format(W))

上一个块的输出如下:

Final weights: [[-0.60814345 -0.80365858]
 [-0.79382715 0.59509065]]

正如预期的那样,权重收敛到协方差矩阵的特征向量。让我们也计算最终的协方差矩阵,以便检查它的值:

import numpy as np

Y_comp = np.zeros((Xs.shape[0], n_components))

for i in range(Xs.shape[0]):
    y_p = np.zeros((n_components, 1))
    xi = np.expand_dims(Xs[i], 1)

    for _ in range(stabilization_cycles):
        Y_comp[i] = np.squeeze(np.dot(W.T, xi) + np.dot(V.T, y_p))
        y_p = y.copy()

print('Final covariance matrix: {}'.format(np.cov(Y_comp.T)))

输出如下:

Final covariance matrix: [[28.9963492 0.31487817]
 [ 0.31487817 12.01606874]]

最后的协方差矩阵也是去相关的(误差可以忽略不计)。Rubner-Tavan 的网络通常比 Sanger 的网络更快,因为反 Hebbian 反馈加快了收敛速度;因此,当采用这种模型时,它们应该是首选。然而,为了避免振荡,调整学习速率非常重要。我建议从一个小值开始,稍微增加它,直到迭代次数达到最小值。或者,可以从更高的学习速率开始,允许更快的初始校正,并通过使用线性(如桑格网络)或指数衰减来逐步降低它。

无监督深度信念网络

在本节中,我们将讨论一个非常著名的生成模型,在无监督的情况下,可以使用该模型对从预定义的数据生成过程中提取的输入数据集 X 进行降维。由于这本书没有特别的先决条件,而且数学复杂性相当高,我们将简要介绍概念,没有证明,也没有对算法的结构进行深入分析。在讨论深度信念网络 ( DBN )之前,有必要介绍另一个模型,受限玻尔兹曼机 ( RBM ),它可以被认为是一个 DBN 的积木。

受限玻尔兹曼机器

这个网络,也被称为调和,在动力系统中的信息处理:调和理论的基础 ,并行分布式处理,第 1 卷,麻省理工学院出版社, 1986)中被提出作为一个概率生成模型。换句话说,RBM 的目标是学习未知的分布(即数据生成过程),以便生成所有可能的样本。通用结构如下图所示:

Structure of a generic Restricted Boltzmann Machine

神经元 x i 是可观察的(也就是说,它们代表 RBM 必须学习的过程产生的向量),而hjT7】是潜在的(也就是说,它们是隐藏的,对 x i 假设的值有贡献)。没有任何进一步的细节,我们需要说这个模型具有一个马尔可夫随机场 ( MRF )的结构,这得益于同一层的神经元之间没有连接(即描述网络的图是二分图)。MRFs 的一个重要性质是对全联合概率建模的可能性, p(x,h;θ) ,吉布斯分布:

指数 E(x,h,**【θ】起着物理系统能量的作用,在我们的例子中,它等于:

这个公式的主要假设是所有的神经元都是伯努利分布的(即 x i ,hj∞B(0,1) ),术语 b ic j 是对可观察和潜在单位的偏差。给定一个数据生成过程, p 数据 ,一个 RBM 必须被优化,使得可能性,p(x;**【θ),被最大化。跳过所有的中间步骤(可以在前面提到的论文中找到),可以证明以下几点:

在前面的公式中,σ()是 sigmoid 函数。给定这两个表达式,就有可能导出(操作被省略)对数似然相对于所有可学习变量的梯度:

很容易理解,所有梯度的第一项非常容易计算,而所有第二项需要所有可能的可观察值的总和。这显然是一个无法用封闭形式解决的棘手问题。为此,Hinton(在 Hinton,G. 《训练受限玻尔兹曼机器的实用指南》,多伦多大学计算机科学系,2010)提出了一种称为对比发散的算法,可用于寻找近似解。这种方法的解释需要马氏链的知识(这不是先决条件);然而,我们可以总结这种策略,说它通过有限(和少量)的采样步骤来计算梯度的近似值(通常,一个步骤就足以获得好的结果)。这种方法可以非常有效地训练 RBM,并使深度信念网络易于使用且极其有效。

深层信念网络

DBN 是一个基于成果管理制的堆叠模型。通用结构如下图所示:

Structure of a generic DBN

第一层包含可见单元,而其余所有单元都是潜在的。在无监督的情况下,目标是学习未知的分布,找出样本的内部表示。事实上,当潜在单元的数量小于输入单元的数量时,该模型学习如何利用低维子空间对分布进行编码。辛顿和奥森德罗(在辛顿和奥森德罗的《深度信念网的快速学习算法》,神经计算, 18/7,2005)提出了一个逐步贪婪训练过程(通常实现的)。每对层被认为是 RBM,并使用对比发散算法进行训练。一旦训练了一个 RBM,隐藏的层就变成了后续 RBM 的可观察层,这个过程一直持续到最后一层。因此,DBN 开发了一系列内部表示(这就是为什么它被定义为深度网络),其中每个层次都是在较低层次的特征上训练的。这个过程与变化的自动编码器没有什么不同;然而,在这种情况下,模型的结构更加严格(例如,不可能使用卷积单元)。此外,输出不是输入的重构,而是内部表示。因此,考虑到上一节中讨论的公式,如果有必要反转流程(即,给定内部表示,获取输入),则有必要从最顶层进行采样,应用以下公式:

当然,这个过程必须向后重复,直到到达实际的输入层。数据库网络非常强大(例如,在天体物理学领域有几个科学应用),即使它们的结构不像其他更近的模型那样灵活;然而,复杂性通常更高,因此,我总是建议从较小的模型开始,只有在最终精度不足以满足特定目的的情况下,才增加层和/或神经元的数量。

无人监管的 DBN 的例子

在本例中,我们希望使用 DBN 来寻找 MNIST 数据集的低维表示。由于这些模型的复杂性很容易增加,我们将把过程限制在 500 个随机样本。该实现基于深信念网络包(https://github.com/albertbup/deep-belief-network,支持 NumPy 和 TensorFlow。在前一种情况下,类(名称保持不变)必须从dbn包导入,而在后一种情况下,包是dbn.tensorflow。在本例中,我们将使用需求较少的 NumPy 版本,但也邀请读者查看 TensorFlow 版本。

让我们从加载和规范化数据集开始,如下所示:

import numpy as np

from sklearn.datasets import load_digits
from sklearn.utils import shuffle

nb_samples = 500

digits = load_digits()

X_train = digits['data'] / np.max(digits['data'])
Y_train = digits['target']

X_train, Y_train = shuffle(X_train, Y_train, random_state=1000)
X_train = X_train[0:nb_samples]
Y_train = Y_train[0:nb_samples]

我们现在可以实例化UnsupervisedDBN类,其结构如下:

  1. 64 个输入神经元(从数据集中隐含检测到)
  2. 32 个乙状结肠神经元
  3. 32 个乙状结肠神经元
  4. 16 个乙状结肠神经元

因此,最后一个表示由 16 个值组成(原始维度的四分之一)。我们正在设置 η=0.025 的学习率,每批 16 个样本(当然,为了尽量减少重构误差,请你查看其他配置)。以下代码片段初始化并训练了模型:

from dbn import UnsupervisedDBN

unsupervised_dbn = UnsupervisedDBN(hidden_layers_structure=[32, 32, 16],
                                   learning_rate_rbm=0.025,
                                   n_epochs_rbm=500,
                                   batch_size=16,
                                   activation_function='sigmoid')

X_dbn = unsupervised_dbn.fit_transform(X_train)

在训练过程的最后,我们可以在将分布投影到二维空间后,对其进行分析。像往常一样,我们将使用 t-SNE 算法,它保证找到最相似的低维分布:

from sklearn.manifold import TSNE

tsne = TSNE(n_components=2, perplexity=10, random_state=1000)
X_tsne = tsne.fit_transform(X_dbn)

下图显示了投影样本的图表:

t-SNE plot of the unsupervised DBN output representations

正如你所看到的,大部分的块是相当内聚的,这表明一个数字的特殊属性已经在低维空间中被成功地表现出来了。在某些情况下,同一个数字组被分成更多的簇,但一般来说,有噪声的(孤立的)点的数量极低。例如,包含数字2的组用符号x表示。大部分样品在0<x0T32】30,x1T33-40范围内;但是,一个子组也位于范围-10<x1<10内。如果我们检查这个小簇的邻居,他们是由代表数字8(由正方形代表)的样本组成的。很容易理解,一些格式错误的二进制与格式错误的八进制非常相似,这证明了原始簇的拆分是正确的。从统计学的角度来看,解释的差异可能会有不同的影响。在某些情况下,几个组件足以确定一个类的特殊特征,但这通常是不正确的。当属于不同类别的样本显示出相似性时,只能通过次要成分的差异来进行区分。当处理包含几乎(甚至部分)重叠样本的数据集时,这一考虑非常重要。当执行降维时,数据科学家的主要任务不是检查总体解释的方差,而是理解是否存在受降维负面影响的区域。在这种情况下,可以定义多个检测规则(例如,当一个样本,xI∈R1xI∈R4→xI具有 y k 标签)或尽量避免创建这种分割的模型(例如

摘要

在这一章中,我们讨论了一些非常常见的用于解决无监督任务的神经模型。自动编码器允许您找到数据集的低维表示,而不会对其复杂性有特定的限制。特别地,深度卷积网络的使用有助于检测和学习高级和低级几何特征,当内部代码也比原始维度短得多时,这可以导致非常精确的重构。我们还讨论了如何为自动编码器增加稀疏性,以及如何使用这些模型对样本进行降噪。标准自动编码器的一个稍有不同的变体是变分自动编码器,它是一个生成模型,可以提高学习数据生成过程的能力,数据集应该从该过程中绘制。

Sanger 和 Rubner-Tavan 的网络是神经模型,能够在没有任何统计预处理的情况下提取数据集的第一个 k 主成分。它们还具有以在线方式自然工作的优势(尽管标准主成分分析通常需要整个数据集,即使存在性能略差于离线算法的增量变体),以及以降序提取组件的优势。我们讨论的最后一个模型是无监督环境下的数据库网络。我们描述了它们的构造块 RBM 的生成属性,然后我们分析了这样的模型如何学习数据生成过程的内部(通常是低维的)表示。

在下一章中,我们将讨论其他神经模型— 生成性对抗网络 ( GANs )和自组织映射 ( SOMs )。前者可以学习输入分布,并从中产生新的样本,而后者基于大脑某些特定区域的功能,并训练它们的单位,以便接受特定的输入模式。

问题

  1. 在自动编码器中,编码器和解码器必须结构对称。这是正确的吗?
  2. 给定数据集 X 及其变换 Y ,基于自动编码器产生的代码, X 中包含的所有信息都可以在 Y 中找到。这是正确的吗?
  3. 一码,zI∑(0,1)128T5】有和(z i ) = 36* 。稀疏吗?*
  4. 如果 std(z i ) = 0.03 ,是不是代码稀疏?
  5. 桑格网络需要协方差矩阵列作为输入向量。这是正确的吗?
  6. 我们如何确定由 Rubner-Tavan 网络提取的每个组件的重要性?
  7. 给定一个随机向量,hI∈ℜmt5】(m是一个 DBN 的输出维数),有可能确定最可能对应的输入样本吗?

进一步阅读

  • 堆叠去噪自动编码器:利用局部去噪标准在深度网络中学习有用的表示文森特,P. ,拉罗彻尔,h,拉约伊,I,本吉奥,y,和曼扎戈尔,P.机器学习研究杂志 11 ,2010
  • 稀疏自动编码器,CS294A, Ng,A.斯坦福大学
  • 自动编码变分贝叶斯金马。D. P】和威林* *,m .ArXiv:1312.6114【stat。ML]
  • 理论神经科学达扬和阿博特,洛杉矶麻省理工学院出版社,2005 年
  • 单层线性前馈神经网络中的最优无监督学习神经网络桑格,T. D.2,1989
  • 用于主成分分析的自组织网络欧洲物理学快报鲁布纳,j .和塔万,P.10(7),1989
  • 动力系统中的信息处理:和声理论的基础并行分布式处理斯摩棱斯基,保罗第一卷,麻省理工学院出版社,1986 年
  • 训练受限玻尔兹曼机器实用指南辛顿,G. ,多伦多大学计算机科学系,2010
  • 深度信念网络的快速学习算法辛顿,奥辛德罗,和特怀威神经计算,18/7,2005
  • 机器学习算法,第二版,博纳科尔索,g,Packt,2018
  • 掌握机器学习算法,博纳科索,g,Packt,2018

九、生成性对抗网络与自监督系统

在这一章中,我们将结束无监督学习的旅程,讨论一些非常流行的神经模型,这些模型可以用来执行数据生成过程,并从中提取新的样本。此外,我们将分析自组织地图的功能,它可以调整它们的结构,以便特定的单元对不同的输入模式做出响应。

特别是,我们将讨论以下主题:

  • 生成性对抗网络 ( GANs )
  • 深度卷积 GANs ( DCGANs )
  • 水鹅 ( WGANs
  • 自组织地图 ( SOMs )

技术要求

本章将介绍的代码需要以下内容:

  • Python 3.5+(强烈推荐蟒蛇分布(https://www.anaconda.com/distribution/)
  • 库,如下所示:
    • SciPy 0.19+
    • NumPy 1.10+
    • 学习 0.20+
    • 熊猫 0.22+
    • Matplotlib 2.0+
    • seaborn 0.9+
    • 张量流 1.5+
    • Keras 2+(仅适用于数据集实用程序功能)

这些例子可以在 GitHub 资源库中找到,网址为https://GitHub . com/packktpublishing/HandsOn-Unsupervised-Learning-with-Python/chapter 09

生成性对抗网络

这些生成模型是由古德费勒和其他研究人员提出的(在生成对抗网络,古德费勒,保罗-阿巴迪,米尔扎,许,沃德-法利,奥扎尔,库维尔和本吉奥,arXiv:1406.2661 [stat。【M1】)为了利用对抗性训练的威力,伴随着深度神经网络的灵活性。不需要太多的技术细节,我们可以引入对抗训练的概念,作为一种基于博弈论的技术,其目标是优化两个相互对抗的代理。当一个代理试图欺骗对手时,另一个代理必须学会如何区分正确和虚假的输入。特别是,氮化镓是一个模型,分为两个明确定义的组成部分:

  • 一台发电机
  • 一个鉴别者(也称一个批评家)

让我们首先假设我们有一个数据生成过程, p data ,以及一个数据集, X ,从其中抽取的 m 个样本:

为简单起见,假设数据集只有一个维度;然而,这不是一种约束,也不是一种限制。生成器是一个参数化函数(通常使用神经网络),它被馈送有噪声的样本,并提供一个 n 维向量作为输出:

换句话说,发生器是在样本 x ∈ ℜ n 上均匀分布到另一个分布 p g (x) 的变换。GAN 的主要目标如下:

然而,与自动编码器相反,在自动编码器中,这种目标是通过整个模型的直接训练来实现的,在 GAN 中,目标是通过生成器和鉴别器之间的游戏来实现的,该游戏是另一个参数化函数,该函数获取样本,xI∈ℜnt5 】,并返回一个概率:

鉴别器的作用是区分从 p 数据 提取的样本(返回大概率)和g(z;θ g ) (返回低概率)。然而,由于生成器的目标是变得越来越能够再现 p 数据 ,其作用是学习如何利用从数据生成过程的几乎完美再现中提取的样本来欺骗鉴别器。因此,考虑到鉴别器,目标是最大化以下条件:

但是这是一个极小极大游戏,也就是说两个对手 AB 必须尽量最小化( A )和最大化( B ),目标一致。在这种情况下,生成器的目标是最小化先前双成本函数的第二项:

事实上,当两个代理成功优化目标时,鉴别器将能够区分从 p 数据 提取的样本和异常值,生成器将能够输出属于 p 数据 的合成样本。但必须明确的是,问题可以用单一目标来表达,训练过程的目标是找出最优的参数集, θ = {θ d ,θ g } ,使鉴别器最大化,而发生器最小化。两个代理必须同时优化,但实际上,过程是交替的(例如,生成器、鉴别器、生成器等)。在更简洁的形式中,目标可以表达如下:

因此,通过解决以下问题可以达到最优:

根据博弈论,这是一个承认一个纳什均衡点的非合作博弈。当满足这样的条件时,如果我们假设双方都知道对手的策略,他们就没有理由再改变自己的策略了。在 GAN 的上下文中,这个条件意味着一旦达到平衡(甚至只是理论上),生成器就可以继续输出样本,确保它们不会被鉴别器误分类。同时,鉴别器没有理由改变策略,因为它可以完美区分 p 数据T5】和任何其他分布。从动态的角度来看,两个组件的训练速度是不对称的。虽然生成器通常需要更多的迭代,但鉴别器可以非常快速地收敛。然而,这种过早的收敛对于整体性能来说是非常危险的。事实上,由于鉴频器提供的反馈,发生器也达到了最佳状态。不幸的是,当梯度非常小时,这种贡献变得可以忽略不计,其明显的后果是生成器错过了提高其输出更好样本的能力的机会(例如,当样本是图像时,它们的质量可以保持非常低,即使是复杂的架构)。这种情况不依赖于发生器固有的容量不足,而是依赖于一旦鉴别器收敛(或非常接近收敛)就开始应用的有限数量的校正。在实践中,由于没有具体的规则,唯一有效的建议是在训练过程中检查两个损失函数。如果鉴别器损耗下降得太快,而发生器损耗仍然很大,通常最好用单个鉴别器步骤交错更多的发生器训练步骤。

分析氮化镓

让我们假设我们有一个通过使用数据集 X 正确训练的 GAN,该数据集是从 p 数据 (x) 中提取的。Goodfellow 等人证明,给定发电机分布 p g (x) ,最优鉴别器如下:

全局目标可以通过使用最佳鉴别器来重写:

我们现在可以扩展前面的表达式:

现在,让我们考虑两个分布 ab 之间的库尔巴克-莱布勒散度:

考虑到前面的表达式,经过一些简单的操作,很容易证明以下等式:

因此,目标可以表示为数据生成过程和生成器分布之间的延森-香农散度的函数。与库尔巴克-莱布勒散度的主要区别是: 0 ≤ D JS (p 数据 ||p g ) ≤ log(2) ,且对称。这个重构并不奇怪,因为 GAN 的真正目标是成为一个能够成功复制 p 数据 的生成模型,如下图所示:

The goal of a GAN is to move the generative model distribution in the direction of pdata, trying to maximize the overlap

初始分布一般与目标分布完全不同;因此,一个 GAN 必须重塑并向 p 数据 移动。当重叠完成时,詹森-香农散度达到最小值,优化完成。然而,正如我们将在下一节中讨论的那样,由于詹森-香农发散的性质,这个过程并不总是运行得如此平稳,并且氮化镓可以在离期望的最终配置非常远的地方达到次优最小值。

模式崩溃

给定一个概率分布,最常出现的值(在离散情况下),或对应于概率密度函数最大值的值(在连续情况下),称为模式。如果我们考虑后一种情况,PDF 具有单一最大值的分布称为单峰。当有两个局部极大值时,称为双峰、等(一般来说,当有多种模式时,分布简称为多峰)。下面的截图显示了两个例子:

Examples of unimodal (left) and bimodal (right) distributions

在处理复杂数据集时,我们不能轻易估计模式的数量;然而,有理由假设数据生成过程是多模态的。有时,当样本基于一个共同的结构时,可以有一个主导模式和几个次要模式;但一般来说,如果样本在结构上不同,则出现单一模式的概率非常低(当然,如果同一基本元素有细微的修改,单一模式也是可能的,但这不是一个需要考虑的有效情况)。

现在,让我们假设我们正在处理人脸图片的多模态分布(比如我们将在下一节讨论的例子中的那些)。一个模式的内容是什么?这个问题很难准确回答,但是很容易理解,数据生成过程的一个最大值对应的人脸应该包含数据集中最常见的元素(例如,如果 80%的人有胡子,我们可以合理地假设模式会包含它)。

在使用 GANs 时,我们面临的最著名和最棘手的问题之一被称为模式崩溃,它涉及到一个次优的最终配置,其中生成器冻结在一个模式周围,并继续提供与输出相同种类的样本。出现这种情况的原因分析起来极其复杂(确实只有理论),但如果我们重新思考 minimax 游戏,就能理解为什么会出现这种情况。因为我们要训练两个不同的成分,即使纳什均衡得到保证,经过几次迭代后,鉴别器对最常见的模式变得非常有选择性。当然,由于生成器是为了欺骗鉴别器而训练的,因此实现这一目标的最简单方法是简单地避开远离模式的所有样本。这种行为增加了鉴别器的选择性,并产生了一个反馈过程,使氮化镓崩溃到只有一小部分数据产生过程存在的状态。

在梯度方面,鉴别器为生成器优化提供的信息很快变得非常稀缺,因为最常见的样本不需要任何调整。另一方面,当生成器开始避免所有那些 p(x) 不接近最大值的样本时,它们不会将鉴别器暴露给新的、潜在有效的样本,因此梯度将保持非常小,直到它消失为零。不幸的是,没有可以用来避免这个问题的全局策略,但是在本章中,我们将讨论为减轻模式崩溃(WGANs)的风险而提出的方法之一。特别是,我们将把我们的注意力集中在詹森-香农发散的局限性上,在某些情况下,这可能会导致氮化镓在没有大梯度的情况下达到次优配置。在这篇介绍中,重要的是不熟悉这些模型的读者意识到风险,并且能够在模式崩溃发生时识别它。

在这一点上,我们可以转向事物的实际方面,并使用张量流来模拟一个真正的 GAN。

深度卷积氮化镓的例子

我们现在可以实现一个 DCGAN,基于在深度卷积生成对抗网络的无监督表示学习中提出的模型,拉德福德 a,梅兹 l,和钦塔拉 s ., arXiv:1511.06434 [cs。LG] ,Olivetti 面向数据集,该数据集足够小,允许快速训练阶段。

让我们从加载数据集开始,对范围( -1,1 )中的值进行归一化,如下所示:

from sklearn.datasets import fetch_olivetti_faces

faces = fetch_olivetti_faces(shuffle=True, random_state=1000)

X_train = faces['images']
X_train = (2.0 * X_train) - 1.0

width = X_train.shape[1]
height = X_train.shape[2]

下面的截图显示了几个示例人脸:

Sample faces drawn from the Olivetti faces dataset

即使所有脸的结构都类似于相同的图案,眼睛(戴眼镜和不戴眼镜)、鼻子和嘴巴的形状也有细微的差异。而且,有些人留着胡子,表情也大不相同(微笑、严肃、盯着远离镜头的东西等等)。因此,我们需要期待一个多模态分布,可能有一个对应于平均面部结构的主模式,以及几个对应于具有特定共同特征的子集的其他模式。

此时,我们可以定义主要常数,如下所示:

nb_samples = 400
code_length = 512
nb_epochs = 500
batch_size = 50
nb_iterations = int(nb_samples / batch_size)

400 64 × 64 灰度样本(对应每个样本 4096 个分量)。在本例中,我们选择使用带有512分量的噪声码向量,并用50样本批次为500时期训练模型。这样的值不是基于黄金法则,因为(特别是对于 GANs)几乎不可能知道哪种设置会产生最佳结果。因此,像往常一样,我强烈建议在做出决定之前检查不同的超参数集。

当训练过程不太长时,可以用一组均匀采样的超参数(例如,批量∑{ 20,50,100,200} )来检查发生器和鉴别器的平均损失。例如,如果一个最优值似乎存在于范围( 50,100 )中,一个好的策略是提取一些随机值并重新训练模型。可以重复这样的过程,直到采样值之间的差异可以忽略不计。当然,考虑到这些模型的复杂性,彻底的搜索只能用专用硬件(即多个 GPU 或 TPU)进行;因此,另一个建议是从已测试的配置开始(即使上下文不同)并应用小的修改,以便为特定任务优化它们。在本例中,我们根据原始论文设置了许多值,但我邀请读者在自定义更改后重新运行代码,并观察差异。

我们现在可以为生成器定义 DAG,它基于以下结构:

  • 具有 1,024 (4 × 4)个滤波器的 2D 卷积,具有( 1,1 )步距、有效填充和线性输出
  • 批处理规范化和泄漏 ReLU 激活(当输入值为负时,这更有效;事实上,当 x < 0 时,标准的 ReLU 具有零梯度,而泄漏的 ReLU 具有恒定的小梯度,允许轻微的修改)
  • 2D 卷积与 512 (4 × 4)滤波器的( 2,2 )步距,相同的填充和线性输出
  • 批量标准化和泄漏 ReLU 激活
  • 具有 256 (4 × 4)个滤波器的 2D 卷积,具有( 2,2 )步距、相同的填充和线性输出
  • 批量标准化和泄漏 ReLU 激活
  • 2D 卷积与 128 (4 × 4)滤波器,具有( 2,2 )步距,相同的填充和线性输出
  • 批量标准化和泄漏 ReLU 激活
  • 2D 卷积与 1 (4 × 4)过滤器与( 2,2 )步距,相同的填充,和双曲正切输出

生成器的代码如以下代码片段所示:

import tensorflow as tf

def generator(z, is_training=True):
    with tf.variable_scope('generator'):
        conv_0 = tf.layers.conv2d_transpose(inputs=z,
                                            filters=1024,
                                            kernel_size=(4, 4),
                                            padding='valid')

        b_conv_0 = tf.layers.batch_normalization(inputs=conv_0, training=is_training)

        conv_1 = tf.layers.conv2d_transpose(inputs=tf.nn.leaky_relu(b_conv_0),
                                            filters=512,
                                            kernel_size=(4, 4),
                                            strides=(2, 2),
                                            padding='same')

        b_conv_1 = tf.layers.batch_normalization(inputs=conv_1, training=is_training)

        conv_2 = tf.layers.conv2d_transpose(inputs=tf.nn.leaky_relu(b_conv_1),
                                            filters=256,
                                            kernel_size=(4, 4),
                                            strides=(2, 2),
                                            padding='same')

        b_conv_2 = tf.layers.batch_normalization(inputs=conv_2, training=is_training)

        conv_3 = tf.layers.conv2d_transpose(inputs=tf.nn.leaky_relu(b_conv_2),
                                            filters=128,
                                            kernel_size=(4, 4),
                                            strides=(2, 2),
                                            padding='same')

        b_conv_3 = tf.layers.batch_normalization(inputs=conv_3, training=is_training)

        conv_4 = tf.layers.conv2d_transpose(inputs=tf.nn.leaky_relu(b_conv_3),
                                            filters=1,
                                            kernel_size=(4, 4),
                                            strides=(2, 2),
                                            padding='same')

        return tf.nn.tanh(conv_4)

代码很简单,但有助于阐明对可变范围上下文(通过命令tf.variable_scope('generator')定义)的需求。因为我们需要用另一种方式训练模型,所以当生成器被优化时,只有它的变量必须被更新。因此,我们定义了一个命名范围内的所有层,允许强制优化器只处理所有可训练变量的子集。

鉴别器的 DAG 基于以下对称结构:

  • 具有 128 (4 × 4)个滤波器的 2D 卷积,具有( 2,2 )步距、相同的填充和泄漏的 ReLU 输出
  • 具有 256 (4 × 4)个滤波器的 2D 卷积,具有( 2,2 )步距、相同的填充和线性输出
  • 批量标准化和泄漏 ReLU 激活
  • 2D 卷积与 512 (4 × 4)滤波器的( 2,2 )步距,相同的填充和线性输出
  • 批量标准化和泄漏 ReLU 激活
  • 具有 1,024 (4 × 4)个滤波器的 2D 卷积,具有( 2,2 )步距、相同的填充和线性输出
  • 批量标准化和泄漏 ReLU 激活
  • 具有 1 (4 × 4)个滤波器的 2D 卷积,这些滤波器具有( 2,2 )步距、有效填充和线性输出(输出预期为 sigmoid,可以表示概率,但我们将直接在损失函数内部执行此转换)

鉴别器的代码如下:

import tensorflow as tf

def discriminator(x, is_training=True, reuse_variables=True):
    with tf.variable_scope('discriminator', reuse=reuse_variables):
        conv_0 = tf.layers.conv2d(inputs=x,
                                  filters=128,
                                  kernel_size=(4, 4),
                                  strides=(2, 2),
                                  padding='same')

        conv_1 = tf.layers.conv2d(inputs=tf.nn.leaky_relu(conv_0),
                                  filters=256,
                                  kernel_size=(4, 4),
                                  strides=(2, 2),
                                  padding='same')

        b_conv_1 = tf.layers.batch_normalization(inputs=conv_1, training=is_training)

        conv_2 = tf.layers.conv2d(inputs=tf.nn.leaky_relu(b_conv_1),
                                  filters=512,
                                  kernel_size=(4, 4),
                                  strides=(2, 2),
                                  padding='same')

        b_conv_2 = tf.layers.batch_normalization(inputs=conv_2, training=is_training)

        conv_3 = tf.layers.conv2d(inputs=tf.nn.leaky_relu(b_conv_2),
                                  filters=1024,
                                  kernel_size=(4, 4),
                                  strides=(2, 2),
                                  padding='same')

        b_conv_3 = tf.layers.batch_normalization(inputs=conv_3, training=is_training)

        conv_4 = tf.layers.conv2d(inputs=tf.nn.leaky_relu(b_conv_3),
                                  filters=1,
                                  kernel_size=(4, 4),
                                  padding='valid')

        return conv_4

同样,在这种情况下,我们需要声明一个专用的变量范围。然而,由于鉴别器被用在两个不同的上下文中(即,真实样本的评估和生成样本的评估),我们需要请求在第二个声明中重用变量。如果没有设置这样的标志,对函数的每次调用都会产生新的变量集,对应于不同的鉴别器。

一旦声明了两个主要组件,我们就可以初始化图形并为 GAN 设置整个 DAG,如下所示:

import tensorflow as tf

graph = tf.Graph()

with graph.as_default():
    input_x = tf.placeholder(tf.float32, shape=(None, width, height, 1))
    input_z = tf.placeholder(tf.float32, shape=(None, code_length))
    is_training = tf.placeholder(tf.bool)

    gen = generator(z=tf.reshape(input_z, (-1, 1, 1, code_length)), is_training=is_training)

    discr_1_l = discriminator(x=input_x, is_training=is_training, reuse_variables=False)
    discr_2_l = discriminator(x=gen, is_training=is_training, reuse_variables=True)

    loss_d_1 = tf.reduce_mean(
            tf.nn.sigmoid_cross_entropy_with_logits(labels=tf.ones_like(discr_1_l), logits=discr_1_l))
    loss_d_2 = tf.reduce_mean(
            tf.nn.sigmoid_cross_entropy_with_logits(labels=tf.zeros_like(discr_2_l), logits=discr_2_l))
    loss_d = loss_d_1 + loss_d_2

    loss_g = tf.reduce_mean(
            tf.nn.sigmoid_cross_entropy_with_logits(labels=tf.ones_like(discr_2_l), logits=discr_2_l))

    variables_g = [variable for variable in tf.trainable_variables() if variable.name.startswith('generator')]
    variables_d = [variable for variable in tf.trainable_variables() if variable.name.startswith('discriminator')]

    with tf.control_dependencies(tf.get_collection(tf.GraphKeys.UPDATE_OPS)):
        training_step_d = tf.train.AdamOptimizer(0.0001, beta1=0.5).minimize(loss=loss_d, var_list=variables_d)
        training_step_g = tf.train.AdamOptimizer(0.0005, beta1=0.5).minimize(loss=loss_g, var_list=variables_g)

第一个块包含占位符的声明。为了清楚起见,虽然input_xinput_z的目的很容易理解,is_training可以不那么明显。这个布尔标志的目标是允许在生产阶段禁用批处理规范化(它必须只在训练阶段有效)。接下来的步骤包括声明生成器和两个鉴别器(它们在形式上是相同的,因为变量是共享的,但是一个被馈送真实样本,另一个必须评估生成器的输出)。然后,是时候定义损失函数了,这是基于一个加快计算速度和增加数值稳定性的技巧。

函数tf.nn.sigmoid_cross_entropy_with_logits()接受一个逻辑(这就是为什么我们没有直接对鉴别器输出应用 sigmoid 变换),并允许我们执行以下矢量计算:

因此,由于loss_d_1是真实样本的损失函数,我们使用运算符tf.ones_like()将所有标签设置为 1;因此,sigmoid 交叉熵的第二项变为零,结果如下:

相反,loss_d_2恰好需要乙状结肠交叉熵的第二项;因此,我们将所有标签设置为零,以获得损失函数:

同样的概念也适用于发电机损耗函数。接下来的步骤需要定义两个亚当优化器。正如我们之前解释的,我们需要隔离变量,以便执行交错训练;因此,minimize()函数现在既有损失,也有必须更新的变量集。在官方 TensorFlow 文档中建议使用上下文声明tf.control_dependencies(tf.get_collection(tf.GraphKeys.UPDATE_OPS)),无论何时使用批处理规范化,其目标都是仅在计算平均值和方差后才允许执行训练步骤(有关该技术的更多详细信息,请查看原始论文:批处理规范化:通过减少内部协变量偏移来加速深度网络训练Ioffe S .和 Szegedy C.arXiv:1502.03167[cs]。LG] )。

此时,我们可以创建一个会话并初始化所有变量,如下所示:

import tensorflow as tf

session = tf.InteractiveSession(graph=graph)
tf.global_variables_initializer().run()

一旦一切准备就绪,训练过程就可以开始了。下面的代码片段显示了执行鉴别器和生成器交替训练的代码:

import numpy as np

samples_range = np.arange(nb_samples)

for e in range(nb_epochs):
    d_losses = []
    g_losses = []

    for i in range(nb_iterations):
        Xi = np.random.choice(samples_range, size=batch_size)
        X = np.expand_dims(X_train[Xi], axis=3)
        Z = np.random.uniform(-1.0, 1.0, size=(batch_size, code_length)).astype(np.float32)

        _, d_loss = session.run([training_step_d, loss_d],
                                    feed_dict={
                                        input_x: X,
                                        input_z: Z,
                                        is_training: True
                                    })
        d_losses.append(d_loss)

        Z = np.random.uniform(-1.0, 1.0, size=(batch_size, code_length)).astype(np.float32)

        _, g_loss = session.run([training_step_g, loss_g],
                                    feed_dict={
                                        input_x: X,
                                        input_z: Z,
                                        is_training: True
                                        })

        g_losses.append(g_loss)

    print('Epoch {}) Avg. discriminator loss: {} - Avg. generator loss: {}'.format(e + 1, np.mean(d_losses), np.mean(g_losses)))

在这两个步骤中,我们向网络馈送一批真实图像(在生成器优化期间不会使用)和统一采样的代码 Z ,其中每个组件都是ZI∞U(-1,1) 。为了降低模式崩溃的风险,我们将在每次迭代开始时对集合进行洗牌。这不是一个稳健的方法,但它至少保证避免了可能导致 GAN 成为次优配置的相互关联。

在训练过程结束时,我们可以生成几个样本人脸,如下所示:

import numpy as np

Z = np.random.uniform(-1.0, 1.0, size=(20, code_length)).astype(np.float32)

Ys = session.run([gen],
                 feed_dict={
                     input_z: Z,
                     is_training: False
                 })

Ys = np.squeeze((Ys[0] + 1.0) * 0.5 * 255.0).astype(np.uint8)

结果如下图所示:

Sample faces generated by the DCGAN

可以看到,质量非常高,更长的训练阶段会有所帮助(伴随着更深的超参数搜索)。然而,GAN 已经成功地学会了如何通过使用相同的属性集来生成新面孔。表达式和视觉元素(例如,眼睛形状、眼镜的存在等)都被重新应用于不同的模型,以便产生从相同的原始数据生成过程中绘制的潜在面部。例如,第七个和第八个是基于一个人,具有修改的属性。原图如下:

Original pictures corresponding to one of the Olivetti people

嘴巴的结构对两个生成的样本来说都是通用的,但是观察第二个样本,我们可以确认已经从其他样本中提取了许多元素(鼻子、眼睛、前额和方向),产生了一张不存在的人的照片。即使模型工作正常,也会出现部分模式崩溃,因为某些面孔(具有相对属性,如眼镜)比其他面孔更常见。相反,一些女性面孔(数据集中的少数)已经与男性属性合并,产生了样本,例如包含生成样本的图像的第一行的第二个或第八个。作为练习,我邀请读者使用不同的参数和其他数据集(包含灰度和 RGB 图像,如 Cifar-10 或 STL-10)重新训练模型。

The screenshots that are shown in this and other examples in this chapter are often based on random generations; therefore, in order to increase the reproducibility, I suggest setting both the NumPy and TensorFlow random seed equal to 1000. The commands are: np.random.seed(1000) and tf.set_random_seed(1000).

水来了

给定概率分布 p(x) ,集合Dp= { x:p(x)>0 }称为支持。如果两个分布 p(x)q(x) 具有不连续的支撑(即,dp∩dq= {∅},则 Jensen-Shannon 散度等于 log(2) 。这意味着梯度为零,不能再进行任何修正。在涉及 GAN 的一般场景中, p g (x)p 数据 完全重叠的可能性极小(但是,您可以预期最小重叠);因此,梯度非常小,权重的更新也很小。这种问题可能会阻碍训练过程,并使氮化镓陷入无法逃脱的次优配置。为此,Arjovsky、Chintala 和 Bottou(在 Wasserstein GAN、T30】Arjovsky m .、 Chintala S.和 Bottou L.arXiv:1701.07875【stat。ML] )提出了一个略有不同的模型,基于一个更稳健的发散度量,称为瓦瑟斯坦距离(或地球移动者距离):

为了理解前面的公式,有必要说一下 ∏(p 数据、p g ) 是包含数据生成过程和生成器分布之间所有可能的联合分布的集合。因此,瓦瑟斯坦距离等于范数期望值集合的下确界 ||x - y|| ,假设夫妇( x,y )是来自一个分布μ∈∏(p数据,p g ) 。这样的定义并不是非常直观的,即使这个概念很简单,也可以通过思考两个二维斑点来总结,这两个斑点的距离是两个最近点之间的距离。很明显,不相交支撑的问题被完全克服了,而且,度量也与实际分布距离成正比。不幸的是,我们没有使用有限集合;因此,瓦瑟斯坦距离的计算可能非常低效,几乎不可能用于现实生活中的任务。然而,坎特罗维奇-鲁宾斯坦定理(因为超出了本书的范围,所以没有完全分析)允许我们通过使用特殊的支持函数 f(x) 来简化表达式:

该定理施加的主要约束是 f(x) 必须是一个 L-李普希茨函数,也就是说,给定一个非负常数, L ,以下公式适用:

考虑使用神经网络参数化的函数f(),全局目标如下:

在这个特定的语境中,鉴别者常常被称为批评家,所以f(x;θ c ) 起这个作用。由于这样的函数必须是 L-Lipschitz,作者建议在应用校正后裁剪所有变量 θ c :

这种方法不是非常有效,因为它减慢了学习过程;然而,由于函数执行有限组变量的操作,输出被假设总是由一个常数约束,并且可以应用 Kantorovich-Rubinstein 定理。当然,由于参数化经常需要很多变量(有时需要几百万或更多),所以裁剪常数应该保持非常小(例如 0.01)。此外,由于剪辑的存在影响了批评家的训练速度,因此在每次迭代期间增加批评家训练步骤的数量也是必要的(例如,批评家 5 次,生成器 1 次,等等)。

将 DCGAN 转变为 WGAN

在这个例子中,我们将使用时尚 MNIST 数据集(由 Keras 直接提供)实现一个基于 Wasserstein 距离的 DCGAN。这套数据集由 60,000 幅 28 × 28 的服装灰度图像组成,它是由 Zalando 引入的,作为标准 MNIST 数据集的替代,标准数据集的类太容易被许多分类器分离。考虑到这种网络需要的训练时间,我们决定将这个过程限制在 5000 个样本,但是有足够资源的读者可以选择增加或取消这个限制。

第一步包括加载、切片和归一化数据集(在范围( -1,1 )内),如下所示:

import numpy as np

from keras.datasets import fashion_mnist

nb_samples = 5000

(X_train, _), (_, _) = fashion_mnist.load_data()
X_train = X_train.astype(np.float32)[0:nb_samples] / 255.0
X_train = (2.0 * X_train) - 1.0

width = X_train.shape[1]
height = X_train.shape[2]

下面的截图显示了一些示例:

Samples extracted from the Fashion MNIST dataset

我们现在可以基于 DCGAN 的同一层定义生成器 DAG,如下所示:

  • 具有 1,024 (4 × 4)个滤波器的 2D 卷积,具有( 1,1 )步距、有效填充和线性输出
  • 批量标准化和泄漏 ReLU 激活
  • 2D 卷积与 512 (4 × 4)滤波器的( 2,2 )步距,相同的填充和线性输出
  • 批量标准化和泄漏 ReLU 激活
  • 具有 256 (4 × 4)个滤波器的 2D 卷积,具有( 2,2 )步距、相同的填充和线性输出
  • 批量标准化和泄漏 ReLU 激活
  • 2D 卷积与 128 (4 × 4)滤波器,具有( 2,2 )步距,相同的填充和线性输出
  • 批量标准化和泄漏 ReLU 激活
  • 2D 卷积与 1 (4 × 4)过滤器与( 2,2 )步距,相同的填充,和双曲正切输出

代码如下面的代码片段所示:

import tensorflow as tf

def generator(z, is_training=True):
    with tf.variable_scope('generator'):
        conv_0 = tf.layers.conv2d_transpose(inputs=z,
                                            filters=1024,
                                            kernel_size=(4, 4),
                                            padding='valid')

        b_conv_0 = tf.layers.batch_normalization(inputs=conv_0, training=is_training)

        conv_1 = tf.layers.conv2d_transpose(inputs=tf.nn.leaky_relu(b_conv_0),
                                            filters=512,
                                            kernel_size=(4, 4),
                                            strides=(2, 2),
                                            padding='same')

        b_conv_1 = tf.layers.batch_normalization(inputs=conv_1, training=is_training)

        conv_2 = tf.layers.conv2d_transpose(inputs=tf.nn.leaky_relu(b_conv_1),
                                            filters=256,
                                            kernel_size=(4, 4),
                                            strides=(2, 2),
                                            padding='same')

        b_conv_2 = tf.layers.batch_normalization(inputs=conv_2, training=is_training)

        conv_3 = tf.layers.conv2d_transpose(inputs=tf.nn.leaky_relu(b_conv_2),
                                            filters=128,
                                            kernel_size=(4, 4),
                                            strides=(2, 2),
                                            padding='same')

        b_conv_3 = tf.layers.batch_normalization(inputs=conv_3, training=is_training)

        conv_4 = tf.layers.conv2d_transpose(inputs=tf.nn.leaky_relu(b_conv_3),
                                            filters=1,
                                            kernel_size=(4, 4),
                                            strides=(2, 2),
                                            padding='same')

        return tf.nn.tanh(conv_4)

评论家的 DAG 基于以下几层:

  • 具有 128 (4 × 4)个滤波器的 2D 卷积,具有( 2,2 )步距、相同的填充和泄漏的 ReLU 输出
  • 具有 256 (4 × 4)个滤波器的 2D 卷积,具有( 2,2 )步距、相同的填充和线性输出
  • 批量标准化和泄漏 ReLU 激活
  • 2D 卷积与 512 (4 × 4)滤波器的( 2,2 )步距,相同的填充和线性输出
  • 批量标准化和泄漏 ReLU 激活
  • 具有 1,024 (4 × 4)个滤波器的 2D 卷积,具有( 2,2 )步距、相同的填充和线性输出
  • 批量标准化和泄漏 ReLU 激活
  • 具有 1 (4 × 4)个滤波器的 2D 卷积,具有( 2,2 )步距、有效填充和线性输出

对应的代码块如下:

import tensorflow as tf

def critic(x, is_training=True, reuse_variables=True):
    with tf.variable_scope('critic', reuse=reuse_variables):
        conv_0 = tf.layers.conv2d(inputs=x,
                                  filters=128,
                                  kernel_size=(4, 4),
                                  strides=(2, 2),
                                  padding='same')

        conv_1 = tf.layers.conv2d(inputs=tf.nn.leaky_relu(conv_0),
                                  filters=256,
                                  kernel_size=(4, 4),
                                  strides=(2, 2),
                                  padding='same')

        b_conv_1 = tf.layers.batch_normalization(inputs=conv_1, training=is_training)

        conv_2 = tf.layers.conv2d(inputs=tf.nn.leaky_relu(b_conv_1),
                                  filters=512,
                                  kernel_size=(4, 4),
                                  strides=(2, 2),
                                  padding='same')

        b_conv_2 = tf.layers.batch_normalization(inputs=conv_2, training=is_training)

        conv_3 = tf.layers.conv2d(inputs=tf.nn.leaky_relu(b_conv_2),
                                  filters=1024,
                                  kernel_size=(4, 4),
                                  strides=(2, 2),
                                  padding='same')

        b_conv_3 = tf.layers.batch_normalization(inputs=conv_3, training=is_training)

        conv_4 = tf.layers.conv2d(inputs=tf.nn.leaky_relu(b_conv_3),
                                  filters=1,
                                  kernel_size=(4, 4),
                                  padding='valid')

        return conv_4

由于与 DCGAN 没有特别的区别,因此没有必要添加更多的评论。因此,我们可以继续讨论图和整个 DAG 的定义,如下所示:

import tensorflow as tf

nb_epochs = 100
nb_critic = 5
batch_size = 64
nb_iterations = int(nb_samples / batch_size)
code_length = 100

graph = tf.Graph()

with graph.as_default():
    input_x = tf.placeholder(tf.float32, shape=(None, width, height, 1))
    input_z = tf.placeholder(tf.float32, shape=(None, code_length))
    is_training = tf.placeholder(tf.bool)

    gen = generator(z=tf.reshape(input_z, (-1, 1, 1, code_length)), is_training=is_training)

    r_input_x = tf.image.resize_images(images=input_x, size=(64,64),    
                                       method=tf.image.ResizeMethod.BICUBIC)

     crit_1_l = critic(x=r_input_x, is_training=is_training, reuse_variables=False)
     crit_2_l = critic(x=gen, is_training=is_training, reuse_variables=True)

     loss_c = tf.reduce_mean(crit_2_l - crit_1_l)
     loss_g = tf.reduce_mean(-crit_2_l)

     variables_g = [variable for variable in tf.trainable_variables() 
                     if variable.name.startswith('generator')]
     variables_c = [variable for variable in tf.trainable_variables() 
                     if variable.name.startswith('critic')]

     with tf.control_dependencies(tf.get_collection(tf.GraphKeys.UPDATE_OPS)):
         optimizer_c = tf.train.AdamOptimizer(0.00005, beta1=0.5, beta2=0.9).\
                             minimize(loss=loss_c, var_list=variables_c)

         with tf.control_dependencies([optimizer_c]):
             training_step_c = tf.tuple(tensors=[
                                     tf.assign(variable, tf.clip_by_value(variable, -0.01, 0.01))
                                                             for variable in variables_c])

         training_step_g = tf.train.AdamOptimizer(0.00005, beta1=0.5, beta2=0.9).\
                                  minimize(loss=loss_g, var_list=variables_g)

通常,第一步包括声明占位符,占位符与 DCGAN 相同。然而,由于模型(特别是卷积或转置卷积的序列)已经针对 64 × 64 图像进行了优化,我们将使用方法tf.image.resize_images()来调整原始样本的大小。该操作将导致有限的质量损失;因此,在生产应用程序中,我强烈建议您使用针对原始输入维度优化的模型。在声明了生成器和批评者之后(正如我们在前面的例子中所讨论的,我们需要两个实例共享相同的变量,因为损失函数是单独优化的),我们可以设置损失。在这种情况下,它们非常简单,计算速度也非常快,但是我们为这种简化付出了代价,网络将能够应用更小的校正。事实上,在这种情况下,我们不是直接最小化批评家损失函数;相反,我们首先用算子optimizer_c计算和应用梯度,然后用算子training_step_c裁剪所有的临界变量。因为我们只想调用这个操作符,所以我们已经在使用指令tf.control_dependencies([optimizer_c])定义的上下文中声明了它。这样,当一个会话被请求计算traning_step_c时,TensorFlow 将首先运行optimizer_c,但只有当结果准备好时才会执行主命令(简单地剪辑变量)。正如我们在理论中所解释的,这一步是必要的,以保证批评家仍然是一个 L-李普希茨函数,因此,允许使用从坎特罗维奇-鲁宾斯坦定理导出的简化的瓦瑟斯坦距离表达式。

当图形完全定义后,可以创建一个会话并初始化所有变量,如下所示:

import tensorflow as tf

session = tf.InteractiveSession(graph=graph)
tf.global_variables_initializer().run()

现在,所有的组件都已经设置好了,我们准备开始训练过程,训练过程分为nb_critic(在我们的例子中是五次)批评家训练步骤的迭代和生成器训练步骤的一次执行,如下所示:

import numpy as np

samples_range = np.arange(X_train.shape[0])

for e in range(nb_epochs):
    c_losses = []
    g_losses = []

    for i in range(nb_iterations):
        for j in range(nb_critic):
            Xi = np.random.choice(samples_range, size=batch_size)
            X = np.expand_dims(X_train[Xi], axis=3)
            Z = np.random.uniform(-1.0, 1.0, size=(batch_size, code_length)).astype(np.float32)

            _, c_loss = session.run([training_step_c, loss_c],
                                        feed_dict={
                                            input_x: X,
                                            input_z: Z,
                                            is_training: True
                                        })
            c_losses.append(c_loss)

        Z = np.random.uniform(-1.0, 1.0, size=(batch_size, code_length)).astype(np.float32)

        _, g_loss = session.run([training_step_g, loss_g],
                                    feed_dict={
                                        input_x: np.zeros(shape=(batch_size, width, height, 1)),
                                        input_z: Z,
                                        is_training: True
                                    })

        g_losses.append(g_loss)

    print('Epoch {}) Avg. critic loss: {} - Avg. generator loss: {}'.format(e + 1,
                                                                            np.mean(c_losses),
                                                                            np.mean(g_losses)))

在这个过程的最后(可能会很长,尤其是在没有任何 GPU 支持的情况下),为了获得视觉确认,我们可以再次生成几个样本,如下所示:

import numpy as np

Z = np.random.uniform(-1.0, 1.0, size=(30, code_length)).astype(np.float32)

Ys = session.run([gen],
                  feed_dict={
                      input_z: Z,
                      is_training: False
                  })

Ys = np.squeeze((Ys[0] + 1.0) * 0.5 * 255.0).astype(np.uint8)

结果显示在下面的截图中:

Samples generated by the WGAN

可以看到,WGAN 已经收敛到一个相当好的最终配置。图像的质量受到调整大小操作的强烈影响;然而,有趣的是,观察到生成的样本平均比原始样本更复杂。例如,衣服的质地和形状会受到其他因素(例如包和鞋)的影响,结果是不太规则的模型,新样本数量增加。然而,与 Olivetti faces 数据集相反,在这种情况下,更难理解样本是否由异构属性的混合组成,因为数据生成过程(如标准 MNIST)至少有 10 个对应于原始类的主导模式。

WGAN 不会陷入模式崩溃,但是不同区域的强烈分离阻止了模型轻松地合并元素,正如我们对人脸观察到的那样。作为练习,我邀请读者用 Olivetti faces 数据集重复这个示例,找到最佳超参数配置,并将结果与标准 DCGAN 实现的结果进行比较。

自组织地图

自组织图是由 Willshaw 和 Von Der Malsburg 首次提出的模型(在《自组织如何建立模式化的神经连接》中,Willshaw,D. J .和 Von Der Malsburg,c .,《伦敦皇家学会学报》,B/194,N. 1117,1976),目的是找到一种方法来描述许多动物大脑中发生的不同现象。事实上,他们观察到大脑的某些区域可以形成内部组织的结构,这些结构的子成分可以选择性地接受特定的输入模式(例如,某些视觉皮层区域对垂直或水平带非常敏感)。SOM 的中心思想可以通过考虑一个聚类过程来综合,该过程旨在发现样本的低级属性,这要归功于它被分配给一个聚类。主要的实际区别在于,在 SOM 中,单个单元通过一个名为赢者通吃的学习过程成为样本总体的一部分(即数据生成过程的一个区域)的代表。这样的训练过程从引发所有单元(我们称之为神经元)的响应开始,并通过减少最活跃单元周围的影响区域来强化所有权重,直到单个单元成为给定输入模式的唯一响应神经元。

该过程如下图所示:

Mexican hat selectivity developed by an SOM

在最初的步骤中,许多单位响应相同的输入模式,但是我们已经可以观察到在xIT3 周围的优势。然而,立即选择该单元可能会导致过早收敛,从而导致精度损失。这就是为什么获胜单位周围的半径逐渐减小的原因(观察一种叫做墨西哥帽的现象,因为它的特征形状)。当然,在这个过程中,最初的获胜单位不可能保持稳定;这就是为什么避免半径的快速减少很重要,因为这可能会阻止其他潜在单位被吸引。当一个神经元在特定模式出现时保持最活跃时,它会稍微转换成一个真正的赢家,因此,它会带走所有,因为没有其他单位会被加强。

一些非常著名和有用的 SOMs 是科霍宁地图(首次出现在拓扑正确特征地图的自组织形成,科霍宁,生物控制论, 43/1,1982)。它们的结构就像投射到二维流形上的平面(最经典的情况是平面二维区域),由 N 神经元组成。从现在开始,为了简单起见,我们将考虑映射到包含 k×p 单位的矩阵上的曲面,每个曲面都使用突触权重wij∈ℜn(维度与输入模式相同,xI∈ℜn)。因此,权重矩阵变为 W(i,j) ∈ ℜ k×p×n 。从实际的观点来看,在这个模型中,神经元通过相应的权重向量来表示,因为不执行内部变换。当一个模式 x i 被呈现时,获胜神经元 n w (作为元组)然后通过使用以下规则被确定:

训练过程通常分为两个截然不同的阶段:调整收敛。在调整阶段,更新被扩展到获胜单位的所有邻居,而在后者,只有权重 W(n w ) 将被加强。然而,平稳渐进的下降比快速下降更可取;因此,邻域大小的常见选择 n s (i,j) 是基于具有指数衰减方差的径向基函数 ( 径向基函数):

初始方差(与最大邻域成正比)为 σ 0 ,按时间常数 τ 呈指数衰减。根据经验,当 t > 4τ 时, σ(t) ≈ 0 ,因此 τ 应设置为等于调整阶段训练期数的 1/4th:τ= 0.25tadj。一旦定义了邻域,就可以根据所有成员与每个样本的不同程度来更新它们的权重,xIT21:

在前面的公式中,学习速率 η(t) 也是训练时期的函数,因为最好在早期阶段(特别是在调整阶段)施加更大的灵活性,而在收敛阶段最好设置更小的 η ,以便允许更小的修改。衰减学习率的一个非常常见的选择类似于邻域大小:

学习规则的效果是迫使获胜单元的权重更接近特定的模式,因此在训练过程结束时,每个模式都应该引发代表明确定义的特征集的单个单元的响应。形容词“自组织”来源于这样的模型必须优化单元的能力,以便将相似的模式彼此靠近(例如,如果一个竖条引起单元的响应,稍微旋转的竖条应该引起邻居的响应)。

科霍宁地图示例

在本例中,我们希望训练一个 8 × 8 的正方形科霍宁地图来接受奥利韦蒂人脸数据集。由于每个样本都是 64 × 64 的灰度图像,我们需要分配一个形状等于(8,8,4,096)的权重矩阵。训练过程可能很长;因此,我们将地图限制为 100 个随机样本(当然,读者可以自由移除这一限制,并使用整个数据集训练模型)。

像往常一样,让我们从加载和规范化数据集开始,如下所示:

import numpy as np

from sklearn.datasets import fetch_olivetti_faces

faces = fetch_olivetti_faces(shuffle=True)
Xcomplete = faces['data'].astype(np.float64) / np.max(faces['data'])
np.random.shuffle(Xcomplete)
X = Xcomplete[0:100]

现在,让我们定义距离函数方差的指数衰减函数 σ(t) ,以及学习率的指数衰减函数 η(t) ,如下所示:

import numpy as np

eta0 = 1.0
sigma0 = 3.0
tau = 100.0

def eta(t):
 return eta0 * np.exp(-float(t) / tau)

def sigma(t):
 return float(sigma0) * np.exp(-float(t) / tau)

在这个例子中,我们使用初始学习率η(0) = 1 和半径方差 σ(0) = 3 。时间常数选择为等于 100,因为我们计划执行 500 次调整迭代和500次收敛迭代(1000次总迭代)。相应的值在下面的代码片段中声明:

nb_iterations = 1000
nb_adj_iterations = 500

此时,我们可以定义权重矩阵(初始化为wij∞N(0,0.01 )和负责计算获胜单位的函数,基于L2T7】范数的差值 w - x ,如下:

import numpy as np

pattern_length = 64 * 64
pattern_width = pattern_height = 64
matrix_side = 8

W = np.random.normal(0, 0.1, size=(matrix_side, matrix_side, pattern_length))

def winning_unit(xt):
    distances = np.linalg.norm(W - xt, ord=2, axis=2)
    max_activation_unit = np.argmax(distances)
    return int(np.floor(max_activation_unit / matrix_side)), max_activation_unit % matrix_side

在开始训练循环之前,有必要预先计算距离矩阵, dm(x 0 、y 0 、x 1 、y 1 ) ,其中每个元素代表( x 0 、y 0 )与( x 1 、y 1)之间的平方欧几里得距离如下面的片段所示,当必须确定获胜单元的邻域时,这一步骤避免了计算开销:

import numpy as np

precomputed_distances = np.zeros((matrix_side, matrix_side, matrix_side, matrix_side))

for i in range(matrix_side):
    for j in range(matrix_side):
        for k in range(matrix_side):
            for t in range(matrix_side):
                precomputed_distances[i, j, k, t] = \
                    np.power(float(i) - float(k), 2) + np.power(float(j) - float(t), 2)

def distance_matrix(xt, yt, sigmat):
    dm = precomputed_distances[xt, yt, :, :]
    de = 2.0 * np.power(sigmat, 2)
    return np.exp(-dm / de)

函数distance_matrix()计算包含以(xt, yt)为中心的所有神经元的指数衰减影响的平方矩阵。现在,我们拥有了创建训练过程所需的所有构件,该过程基于我们之前描述的权重更新规则,如下所示:

import numpy as np

sequence = np.arange(0, X.shape[0])
t = 0

for e in range(nb_iterations):
    np.random.shuffle(sequence)
    t += 1

    if e < nb_adj_iterations:
        etat = eta(t)
        sigmat = sigma(t)
    else:
        etat = 0.2
        sigmat = 1.0

    for n in sequence:
        x_sample = X[n]

        xw, yw = winning_unit(x_sample)
        dm = distance_matrix(xw, yw, sigmat)

        dW = etat * np.expand_dims(dm, axis=2) * (x_sample - W)
        W += dW

    W /= np.linalg.norm(W, axis=2).reshape((matrix_side, matrix_side, 1))

    if e > 0 and e % 100 == 0:
        print('Training step: {}'.format(t-1))

在每个循环中,执行以下步骤:

  1. 输入样本的序列被打乱,以避免相互关联。
  2. 计算学习率和距离方差(收敛值为η= 0.2σ= 1)。
  3. 对于每个样本,以下内容适用:
    1. 计算获胜单位。
    2. 计算距离矩阵。
    3. 计算并应用权重更新。
  4. 权重被重整,以避免溢出。

现在,我们可以在训练过程结束时显示权重矩阵,如下所示:

Kohonen map weight matrix at the end of the training process

可以看到,每个权重都集中在一个人脸的通用结构上(因为数据集只包含这种模式);然而,不同的权重对特定的输入属性变得更加敏感。我建议你开始观察左上面部的一个元素(例如眼睛或嘴巴),然后沿着一个螺旋顺时针方向旋转,该螺旋在中心重物上结束。这样,很容易看到感受野的变化。作为练习,我邀请读者使用其他数据集(例如,MNIST 或时尚 MNIST)测试模型,并对最终权重矩阵执行手动标注(例如,考虑到此示例,特定权重可以表示戴眼镜和大鼻子的笑脸)。在每个元素被标记后,可以投射原始样本,并通过直接提供标记作为输出来检查哪些神经元更容易接受。

摘要

在这一章中,我们介绍了 GANs 的概念,讨论了一个 DCGAN 的例子。这种模型能够通过使用一个极小极大游戏中涉及的两个神经网络来学习数据生成过程。生成器必须学习如何返回与训练过程中使用的其他样本无法区分的样本。鉴别者或批评家必须变得越来越聪明,只给有效样本分配高概率。对抗性训练方法是基于这样一种想法,即通过学习如何使用与真实样本具有相同属性的合成样本来欺骗生成器,从而迫使生成器战胜鉴别器。与此同时,生成器被迫通过变得越来越有选择性来战胜鉴别器。在我们的例子中,我们还分析了一个重要的变体,称为 WGAN,它可以在标准模型无法再现有效样本时使用。

自组织神经网络是一种基于大脑特定区域功能的结构,它迫使它们的单位学习输入样本的特定特征。这种模型会自动组织自己,因此对相似模式做出响应的单元会更接近。一旦出现新的样本,计算获胜单位就足够了,即权重与样本距离最短的单位;并且,在标记过程之后,有可能立即理解哪些特征引起了反应(例如,垂直线或高级特征,如眼镜或胡子的存在,或面部的形状)。

问题

  1. 在 GAN 中,发生器和鉴别器的作用与自动编码器中的编码器和解码器相同。这是正确的吗?
  2. 鉴频器能输出 (-1,1) 范围内的值吗?
  3. GANs 的问题之一是鉴别器过早收敛。这样对吗?
  4. 在瓦瑟斯坦 GAN 中,在训练阶段,批评家(鉴别者)比生成器慢吗?
  5. 考虑到前面的问题,速度不同的原因是什么?
  6. U(-1,0)U(1,2) 之间的延森-香农散度值是多少?
  7. 赢家通吃战略的目标是什么?
  8. SOM 培训过程的调整阶段的目的是什么?

进一步阅读

  • 生成性对抗网络古德费勒·伊·j .普杰特-阿巴迪·j .米尔扎·m .徐·b .沃德-法利·d .奥萨尔·s .库维尔·a .和本吉奥·y .arXiv:144ML]
  • 深度卷积生成对抗网络的无监督表征学习拉德福德 A.梅兹 L.和钦塔拉 S.arXiv:1511.06434 [cs。LG]
  • WasT2【er stein GAN、 Arjovsky M.Chintala S.和 Bottou L.ArXiv:1701.07875【stat。ML]
  • 自组织如何建立模式化的神经连接威尔肖,D. J .和冯德·马尔斯堡,C.伦敦皇家学会学报B/194N. 1117 ,1976
  • 拓扑正确特征图的自组织形成科霍宁生物控制论,43/1,1982
  • 掌握机器学习算法博纳科索格帕克特出版,2018
posted @ 2025-09-03 10:21  绝不原创的飞龙  阅读(6)  评论(0)    收藏  举报