Python-应用无监督学习-全-
Python 应用无监督学习(全)
原文:
annas-archive.org/md5/6b15c463e64a9f03f0d968a77b424918译者:飞龙
前言
关于
本节简要介绍了作者、本书的内容覆盖范围、开始时你需要的技术技能,以及完成所有活动和练习所需的硬件和软件要求。
本书简介
无监督学习是一种在没有标签数据的情况下非常有用且实用的解决方案。
Python 应用无监督学习 引导你使用无监督学习技术与 Python 库配合,提取非结构化数据中的有意义信息的最佳实践。本书首先解释了基本的聚类如何工作,以在数据集中找到相似的数据点。一旦你熟悉了 k-means 算法及其操作方式,你将学习什么是降维以及如何应用它。随着学习的深入,你将掌握各种神经网络技术,以及它们如何提升你的模型。研究无监督学习的应用时,你还将学习如何挖掘 Twitter 上的热门话题。你将通过完成各种有趣的活动来挑战自己,例如进行市场购物篮分析,并识别不同产品之间的关系。
到本书的最后,你将掌握使用 Python 自信地构建自己模型所需的技能。
作者简介
Benjamin Johnston 是世界领先的数据驱动医疗科技公司之一的高级数据科学家,参与了整个产品开发过程中的创新数字解决方案的开发,从问题定义到解决方案的研发,再到最终部署。他目前正在完成机器学习博士学位,专攻图像处理和深度卷积神经网络。他在医疗设备设计和开发领域有超过 10 年的经验,担任过多种技术角色,拥有澳大利亚悉尼大学工程学和医学科学两项一等荣誉学士学位。
Aaron Jones 是美国一家大型零售商的全职高级数据科学家,同时也是一名统计顾问。他在零售、媒体和环境科学领域工作时,建立了预测性和推断性模型以及多个数据产品。Aaron 居住在华盛顿州的西雅图,特别关注因果建模、聚类算法、自然语言处理和贝叶斯统计。
Christopher Kruger 曾在广告领域担任高级数据科学家。他为不同行业的客户设计了可扩展的聚类解决方案。Chris 最近获得了康奈尔大学计算机科学硕士学位,目前在计算机视觉领域工作。
学习目标
-
理解聚类的基础知识和重要性
-
从零开始构建 k-means、层次聚类和 DBSCAN 聚类算法,并使用内置包实现
-
探索降维及其应用
-
使用 scikit-learn(sklearn)实现并分析鸢尾花数据集上的主成分分析(PCA)
-
使用 Keras 构建 CIFAR-10 数据集的自编码器模型
-
使用机器学习扩展(Mlxtend)应用 Apriori 算法研究交易数据
受众
Python 应用无监督学习是为开发人员、数据科学家和机器学习爱好者设计的,旨在帮助他们了解无监督学习。具有一定的 Python 编程基础,以及包括指数、平方根、均值和中位数等数学概念的基本知识将会非常有帮助。
方法
Python 应用无监督学习采用实践操作的方式,使用 Python 揭示您非结构化数据中的隐藏模式。它包含多个活动,利用现实生活中的商业场景,帮助您在高度相关的环境中练习并应用您的新技能。
硬件要求
为了获得最佳的学生体验,我们推荐以下硬件配置:
-
处理器:Intel Core i5 或同等配置
-
内存:4 GB RAM
-
存储:5 GB 可用空间
软件要求
我们还建议您提前安装以下软件:
-
操作系统:Windows 7 SP1 64 位,Windows 8.1 64 位,或 Windows 10 64 位;Linux(Ubuntu,Debian,Red Hat 或 Suse);或最新版本的 OS X
-
Python(3.6.5 或更高版本,最好是 3.7;通过
www.python.org/downloads/release/python-371/可获得) -
Anaconda(这是用于
mlp_toolkits中的basemap模块的;请访问www.anaconda.com/distribution/,下载 3.7 版本并按照说明进行安装。)
约定
文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名以如下方式显示:“使用math包没有先决条件,并且它已包含在所有标准 Python 安装中。”
一段代码的写法如下:
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
import numpy as np
import math
%matplotlib inline
新术语和重要词汇以粗体显示。您在屏幕上看到的词汇,例如在菜单或对话框中显示的内容,会以这种形式出现在文本中:“接下来,点击metro-jul18-dec18。”
安装与设置
每一段伟大的旅程都始于一个谦逊的步伐。我们即将展开的无监督学习之旅也不例外。在我们能够利用数据做出惊人的成就之前,我们需要准备好最有效的工作环境。接下来,我们将了解如何做到这一点。
在 Windows 上安装 Anaconda
Anaconda 是一个 Python 包管理器,能够轻松地安装并使用本书所需的库。要在 Windows 上安装它,请按照以下步骤进行:
-
Windows 上的 Anaconda 安装非常用户友好。请访问下载页面以获取安装可执行文件:
www.anaconda.com/distribution/#download-section。 -
双击计算机上的安装程序。
-
按照屏幕上的提示完成 Anaconda 的安装。
-
安装完成后,你可以访问 Anaconda Navigator,它将像其他应用程序一样出现在你的应用列表中。
在 Linux 上安装 Anaconda
Anaconda 是一个 Python 包管理器,可以轻松地安装并使用本书所需的库。在 Linux 上安装它,请按照以下步骤操作:
-
请访问 Anaconda 下载页面以获取安装 shell 脚本:
www.anaconda.com/distribution/#download-section。 -
要直接将 shell 脚本下载到你的 Linux 实例中,可以使用
curl或wget下载库。以下示例演示了如何使用curl从 Anaconda 下载页面找到的 URL 获取文件:curl -O https://repo.anaconda.com/archive/Anaconda3-2019.03-Linux-x86_64.sh -
下载 shell 脚本后,可以使用以下命令运行它:
bash Anaconda3-2019.03-Linux-x86_64.sh -
运行上述命令将引导你进入一个非常用户友好的安装过程。系统会提示你选择安装位置以及你希望 Anaconda 如何工作。在这种情况下,你只需保留所有标准设置即可。
-
安装 Anaconda 后,你必须创建环境,在这些环境中你可以安装你希望使用的包。Anaconda 环境的一个巨大优点是,你可以为你正在进行的特定项目创建独立的环境!要创建一个新环境,使用以下命令:
conda create --name my_packt_env python=3.7 -
一旦环境创建完成,你可以使用命名明确的
activate命令激活它:conda activate my_env就这样!你现在已经进入了自己的自定义环境,这将允许你根据需要为你的项目安装所需的包。要退出环境,你只需使用
conda deactivate命令。
在 macOS 上安装 Anaconda
Anaconda 是一个 Python 包管理器,允许你轻松安装并使用本书所需的库。在 macOS 上安装它,请按照以下步骤操作:
-
Windows 上的 Anaconda 安装非常用户友好。请访问下载页面以获取安装可执行文件:
www.anaconda.com/distribution/#download-section。 -
确保选择 macOS,并双击 Download 按钮以下载 Python 3 安装程序。
-
按照屏幕上的提示完成 Anaconda 的安装。
-
安装完成后,你可以访问 Anaconda Navigator,它将像其他应用程序一样出现在你的应用列表中。
在 Windows 上安装 Python
-
在此处查找你所需的 Python 版本:
www.python.org/downloads/windows/。 -
确保根据您的计算机系统安装正确的“位”版本,可以是 32 位或 64 位。您可以在操作系统的系统属性窗口中找到该信息。
下载安装程序后,只需双击文件并按照屏幕上的友好提示进行操作。
在 Linux 上安装 Python
在 Linux 上安装 Python,请执行以下操作:
-
打开命令提示符并通过运行
python3 --version验证 Python 3 是否已经安装。 -
要安装 Python 3,请运行以下命令:
sudo apt-get update sudo apt-get install python3.6 -
如果遇到问题,网络上有大量资源可以帮助您排除故障。
在 macOS X 上安装 Python
在 macOS X 上安装 Python,请执行以下操作:
-
按住 CMD + Space,在打开的搜索框中输入
terminal,然后按 Enter 打开终端。 -
通过运行
xcode-select --install在命令行中安装 Xcode。 -
安装 Python 3 最简单的方法是使用 homebrew,可以通过运行
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"来安装 homebrew。 -
将 homebrew 添加到您的
PATH环境变量中。通过运行sudo nano ~/.profile打开命令行中的配置文件,并在文件底部插入export PATH="/usr/local/opt/python/libexec/bin:$PATH"。 -
最后一步是安装 Python。在命令行中运行
brew install python。 -
请注意,如果安装 Anaconda,最新版本的 Python 将自动安装。
附加资源
本书的代码包也托管在 GitHub 上,网址为:https://github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python。我们还提供了来自我们丰富书籍和视频目录的其他代码包,您可以在 https://github.com/PacktPublishing/ 上查看它们!
我们还提供了一个 PDF 文件,里面包含本书中使用的带色彩的截图/图表。您可以在此处下载: https://www.packtpub.com/sites/default/files/downloads/9781789952292_ColorImages.pdf。
第一章:聚类简介
学习目标
到本章结束时,你将能够:
-
区分监督学习与无监督学习
-
解释聚类的概念
-
使用内建的 Python 包实现 k-means 聚类算法
-
计算数据的轮廓系数(Silhouette Score)
在本章中,我们将讨论聚类的概念。
引言
你是否曾经被要求查看一些数据,但最终一无所获?也许你不熟悉该数据集,或者甚至不知道从哪里开始。这可能非常令人沮丧,甚至会让你感到尴尬,尤其是当是别人让你处理这个任务时。
你不是孤单的,实际上,数据本身很多时候也过于混乱,难以理解。你可能正在模仿很多无监督算法的做法,试图从数据中找出意义。当你试图理解电子表格中的那些数字时,可能正是这些无监督算法在做的事情。现实中,很多真实世界中的数据集并没有任何规律或合理性,你将被要求在几乎没有背景准备的情况下分析它们。不过不用担心——本书将为你提供所需的知识,以便你在处理数据探索任务时不再感到沮丧。
在本书中,我们为你开发了一些最佳内容,帮助你理解无监督算法如何工作以及如何使用它们。我们将涵盖如何在数据中寻找聚类的基础知识,如何减少数据的规模以便更容易理解,以及无监督学习的各个方面如何应用于实际世界。希望你能够通过本书,深入理解无监督学习,了解它能解决的问题以及不能解决的问题。
感谢你的加入,祝你旅途愉快!
无监督学习与监督学习的区别
无监督学习是当前机器学习领域最令人兴奋的研究方向之一。如果你之前有研究过机器学习教材,可能已经熟悉过常见的监督学习和无监督学习问题的区分。监督学习包括使用带标签的数据集来进行分类(例如,在研究肺部健康数据集时预测吸烟者和非吸烟者)或拟合回归线(例如,根据房间数量预测房屋销售价格)。这种模型最接近人类直观的学习方法。
如果你想通过基本的烹饪知识来学习如何不把食物烧焦,你可以通过将食物放在炉子上并观察它烧焦所需的时间(输入),来构建一个数据集。最终,随着你不断地烧焦食物,你会建立一个关于何时会烧焦的心理模型,并避免将来再次发生。监督学习的发展曾经是快速而有价值的,但近年来已经逐渐平缓——许多了解数据的障碍已经被克服:

图 1.1:无监督学习与监督学习的区别
相反,无监督学习涵盖了大量无标签数据的问题。在这种情况下,有标签的数据是指提供了“目标”结果的数据,你试图找出与提供的数据的相关性(例如,在之前的例子中你知道你在找的是食物是否被烧焦)。无标签数据是指你不知道“目标”结果是什么,只有提供的输入数据。
基于之前的例子,假设你被丢到了地球上,对烹饪一无所知。你得到了 100 天的时间、一台炉子和一冰箱,里面满是食物,但没有任何关于该做什么的指示。你对厨房的初步探索可能会有无数种方向——在第 10 天,你可能终于学会如何打开冰箱;在第 30 天,你可能会学到食物可以放在炉子上;而在许多天之后,你可能会不经意间做出一道可食用的餐点。正如你所看到的,试图在没有足够信息结构的厨房中找到意义,会导致产生非常嘈杂且与实际做饭完全无关的数据。
无监督学习可以成为解决这个问题的答案。通过回顾你 100 天的数据,聚类可以用来寻找在某些天生产了餐点的相似模式,这样你就可以轻松回顾那些日子里你做了什么。然而,无监督学习并不是一种神奇的答案——仅仅发现聚类同样可能帮助你找到一些相似但最终无用的数据。
这个挑战正是让无监督学习如此令人兴奋的原因。我们如何才能找到更聪明的技术,加速发现对最终目标有益的信息聚类的过程?
聚类
如果你想找出数据集中的潜在含义,能够找到其中相似数据的分组是非常有价值的。如果你是商店老板,并且想要了解哪些客户更有价值,但并没有一个明确的“有价值”标准,那么聚类将是一个很好的起点,帮助你在数据中找到模式。你可能有一些关于“有价值客户”高层次的想法,但在面对大量数据时,你并不完全确定。通过聚类,你可以找到数据中相似群体之间的共性。如果你更深入地研究一个相似的群体,可能会发现这个群体中的每个人在你的网站上停留的时间比其他人都长。这能帮助你识别出“价值”的标准,并为未来的监督学习实验提供清晰的样本数据。
识别聚类
下图展示了两个散点图:

图 1.2:两个不同的散点图
下图将散点图分为两个不同的聚类:

图 1.3:散点图清晰展示了在提供的数据集中存在的聚类
两个图都显示了从高斯分布中随机生成的数字对(x,y 坐标)。仅仅通过瞥一眼图 1.2,你就应该能清楚地看到数据中聚类的位置——但在现实生活中,永远不会这么简单。现在你知道数据可以清晰地分成两个聚类,你可以开始理解这两个群体之间的差异。
从无监督学习在更大范围的机器学习环境中的位置稍作回顾,让我们从理解聚类的基本构成开始。最基本的定义将聚类视为大型数据集的子集中的相似数据组。举个例子,假设你有一个房间,里面有 10 个人,每个人的职业要么是金融行业,要么是科学家。如果你让所有金融行业的人员站在一起,所有科学家也站到一起,那么你就实际上形成了基于职业类型的两个聚类。找到聚类在识别更相似的项时极具价值,而在规模的另一端,它也能帮助识别相互之间有很大差异的项。
二维数据
为了理解这一点,假设你的雇主给了你一个简单的包含 1,000 行数据的数据集,其中有两列数字数据,如下所示:

图 1.4:NumPy 数组中的二维原始数据
初看之下,这个数据集没有提供任何实际的结构或理解——至少可以说是令人困惑的!
数据集中的维度是另一种简单的计数特征数量的方法。在大多数组织良好的数据表中,你可以通过查看列的数量来知道特征的数量。因此,使用一个大小为(1,000 x 2)的 1,000 行数据集,你将有 1,000 个观测值,涵盖两个维度:
你可以通过将第一列与第二列绘制出来,更好地了解数据的结构。会有很多时候,组间差异的原因看起来微不足道,但那些你能够采取行动的差异案例往往是非常有意义的!
练习 1:识别数据中的聚类
你将看到二维图形。请查看提供的二维图形并识别聚类,以强调机器学习的重要性。在不使用任何算法方法的情况下,识别数据中的聚类位置。
本练习将帮助你开始培养通过自身的眼睛和思维过程识别聚类的直觉。在完成这些练习时,思考一下为什么一组数据点应该被视为一个聚类,而另一组数据点不应该被视为聚类:
-
识别以下散点图中的聚类:
![图 1.5 二维散点图]()
图 1.5 二维散点图
聚类如下:
![图 1.6:散点图中的聚类]()
图 1.6:散点图中的聚类
-
识别散点图中的聚类:
![图 1.7:二维散点图]()
图 1.7:二维散点图
聚类如下:
![图 1.8:散点图中的聚类]()
图 1.8:散点图中的聚类
-
识别散点图中的聚类:

图 1.9:二维散点图
聚类如下:

图 1.10:散点图中的聚类
这些例子大多数你可能很容易理解——这就是重点!人类的大脑和眼睛在发现现实世界中的模式方面非常出色。仅仅通过几毫秒的查看,你就能分辨出哪些数据是组合在一起的,哪些不是。虽然对你来说很容易,但计算机无法像我们一样查看和处理图表。然而,这并不总是坏事——回顾图 1.10,你能仅凭观察图表就找到数据中的六个离散聚类吗?你可能只找到了三个到四个聚类,而计算机可以看到所有六个。人类的大脑非常强大,但它也缺乏基于严格逻辑方法所能处理的细微差别。通过算法聚类,你将学习如何建立一个比人类在这些任务中表现更好的模型!
让我们在下一节中看看这个算法。
k-means 聚类简介
希望到现在为止,你已经可以看到,在机器学习的工作流中,寻找聚类是非常有价值的。那么,如何实际找到这些聚类呢?其中一个最基础但最流行的方法是使用一种叫做 k-means 聚类 的聚类分析方法。k-means 通过在你的数据中寻找 K 个聚类,整个工作流程实际上非常直观——我们将从 k-means 的无数学介绍开始,随后进行 Python 实现。
无数学 k-means 解析
下面是 k-means 聚类的无数学算法:
-
选择 K 个质心(K = 期望的不同聚类数量)。
-
随机地将 K 个质心放置在你的现有训练数据中。
-
计算每个质心到你训练数据中所有点的欧几里得距离。
-
训练数据点会根据它们与质心的距离进行分组。
-
在每个质心分组中的数据点中,计算均值数据点,并将质心移动到该位置。
-
重复这个过程,直到收敛,或者每个组内的成员不再变化。
就这样!下面是一步步的过程,带有一个简单的聚类示例:

图 1.11: 原始数据图,标注在 x,y 坐标上
在图 1.11 中给出的原始数据的基础上,我们可以通过展示每一步的预测聚类来显示 k-means 的迭代过程:

图 1.12: 从左到右读取——红色点是随机初始化的质心,最接近的数据点被分配到各个质心的分组中
k-means 聚类深度解析
为了更深入地理解 k-means,让我们再次走过介绍部分给出的示例,并加入一些支持 k-means 的数学内容。这里的关键组件是欧几里得距离公式:

图 1.13: 欧几里得距离公式
质心在开始时随机设置为你 n 维空间中的点。每个质心作为 (a,b) 输入到前面的公式中,而你空间中的点作为 (x,y) 输入。计算每个点与每个质心坐标之间的距离,选择距离最短的质心作为该点所属的组。
该过程如下:
-
随机质心:[ (2,5) , (8,3) , (4, 5) ]
-
任意点 x: (0, 8)
-
从点到每个质心的距离:[ 3.61, 9.43, 5.00 ]
-
点 x 被分配给质心 1。
曼哈顿距离(替代距离度量)
欧几里得距离是许多机器学习应用中最常用的距离度量,通常被称为距离度量;然而,它并不是唯一的,也不是在每种情况下最好的距离度量。另一个在聚类中常用的距离度量是曼哈顿距离。
曼哈顿距离之所以如此命名,是因为该度量的直觉就像是你在一个大城市(比如纽约市)里开车,城市有许多方形街区。欧几里得距离依赖于对角线,因为它基于勾股定理,而曼哈顿距离则将距离限制为只有直角。曼哈顿距离的公式如下:

图 1.14:曼哈顿距离公式
这里,
是像欧几里得距离一样的向量。在我们之前关于欧几里得距离的例子中,我们希望找到两个点之间的距离,如果
和
,那么曼哈顿距离将等于
。这个功能适用于任何维度。在实践中,曼哈顿距离可能在处理高维数据时表现得比欧几里得距离更好。
更深的维度
当数据只有二维时,前面的例子很容易可视化。这是为了方便,帮助说明 k-means 是如何工作的,但也可能会让你产生聚类很简单的误解。在许多应用中,你的数据可能会大得多,甚至大到无法通过可视化感知(超过三维的数据对于人类来说是无法感知的)。在前面的例子中,你可以通过心算一些二维线条来将数据分成不同的组。而在更高维度时,你将需要计算机的帮助,找到一个适合分隔数据集的 n 维超平面。在实践中,这就是像 k-means 这样的聚类方法能够提供巨大价值的地方。

图 1.15:二维、三维和 n 维图
在接下来的练习中,我们将计算欧几里得距离。我们将使用NumPy和Math包。NumPy是一个用于 Python 的科学计算包,它将常见的数学函数以高度优化的格式进行预打包。通过使用像NumPy或Math这样的包,我们可以减少从头编写自定义数学函数所花费的时间,从而专注于开发我们的解决方案。
练习 2:在 Python 中计算欧几里得距离
在这个练习中,我们将创建一个示例点以及三个样本中心点,以帮助说明欧几里得距离是如何工作的。理解这个距离公式是我们进行聚类工作的基础。
在本次练习结束时,我们将能够从零开始实现欧几里得距离,并完全理解它在特征空间中对点的作用。
在本次练习中,我们将使用标准的 Python 内置 math 包。使用 math 包没有任何前提要求,并且它包含在所有 Python 的标准安装中。顾名思义,这个包非常有用,允许我们直接使用各种基本的数学构件,如指数、平方根等:
-
打开 Jupyter Notebook,并创建一个天真的公式来计算欧几里得距离,具体如下:
import math import numpy as np def dist(a, b): return math.sqrt(math.pow(a[0]-b[0],2) + math.pow(a[1]-b[1],2))这种方法被认为是天真的,因为它对数据点执行逐元素计算(慢),相比之下,使用向量和矩阵运算的更实际实现能够显著提高性能。
-
按如下方式在 Python 中创建数据点:
centroids = [ (2, 5), (8, 3), (4,5) ] x = (0, 8) -
使用你创建的公式,计算示例点与所提供的三个质心之间的欧几里得距离:
centroid_distances =[] for centroid in centroids: centroid_distances.append(dist(x,centroid)) print(centroid_distances) print(np.argmin(centroid_distances))输出如下:
[3.605551275463989, 9.433981132056603, 5.0] 0由于 Python 是零索引的,列表中质心距离的零位置向我们表明,示例点 x 将被分配给三个质心中的第一个。
这个过程会对数据集中的每个点重复,直到每个点都被分配到一个聚类。每分配一个点后,会计算每个聚类中所有点的平均点。计算这些点的平均值与计算单个整数的平均值相同。
现在,既然你已经通过欧几里得距离作为主要度量方法在数据中找到了聚类,回想一下你是如何在 练习 2 中轻松完成这一任务的,在 Python 中计算欧几里得距离。对于我们的人类思维来说,看到图中的点群并确定哪些点属于不同的聚类是非常直观的。然而,我们如何让一个天真的计算机重复这一任务呢?通过理解这个练习,你帮助计算机学习一种通过距离来形成聚类的方法。我们将在下一个练习中继续使用这些距离度量。
练习 3:通过距离的概念形成聚类
通过理解这个练习,你将帮助计算机学习通过距离来形成聚类的方法。我们将在本次练习中继续使用这些距离度量:
-
存储分配给聚类一的点 [ (0,8), (3,8), (3,4) ]:
cluster_1_points =[ (0,8), (3,8), (3,4) ] -
计算所有点的平均点以找到新的质心:
mean =[ (0+3+3)/3, (8+8+4)/3 ] print(mean)输出如下:
[2.0, 6.666666666666667] -
在计算出新的质心后,您将重复在练习 2中看到的聚类成员计算,即在 Python 中计算欧几里得距离,然后再进行前两步来找到新的聚类质心。最终,新的聚类质心将与进入问题时的质心相同,练习也将完成。重复的次数取决于您正在聚类的数据。
一旦您将质心位置移动到新的均值点(2, 6.67),可以将其与您进入问题时输入的初始质心列表进行比较。如果新的均值点与当前列表中的质心不同,这意味着您需要再执行前两个练习的迭代。直到您计算出的新的均值点与您开始时的质心相同,您就完成了 k-means 的一次运行,并达到了称为收敛的点。
在下一个练习中,我们将从头实现 k-means。
练习 4:从头实现 k-means
在这个练习中,我们将研究从头实现 k-means。这个练习依赖于 scikit-learn,一个开源的 Python 包,它使得快速原型化流行的机器学习模型成为可能。在 scikit-learn 中,我们将使用 datasets 功能来创建一个合成的簇数据集。除了利用 scikit-learn 的强大功能外,我们还将依赖 Matplotlib,这是一个流行的 Python 绘图库,它使得我们可以轻松地可视化数据。为此,请执行以下步骤:
-
导入必要的库:
from sklearn.datasets import make_blobs import matplotlib.pyplot as plt import numpy as np import math %matplotlib inline -
生成一个随机的聚类数据集进行实验,X = 坐标点,y = 聚类标签,并定义随机质心:
X, y = make_blobs(n_samples=1500, centers=3, n_features=2, random_state=800) centroids = [[-6,2],[3,-4],[-5,10]] -
打印数据:
X输出如下:
array([[-3.83458347, 6.09210705], [-4.62571831, 5.54296865], [-2.87807159, -7.48754592], ..., [-3.709726 , -7.77993633], [-8.44553266, -1.83519866], [-4.68308431, 6.91780744]]) -
按如下方式绘制坐标点:
plt.scatter(X[:, 0], X[:, 1], s=50, cmap='tab20b') plt.show()绘图如下所示:
![图 1.16:坐标点的绘图]()
图 1.16:坐标点的绘图
-
打印
y数组:y输出如下:
array([2, 2, 1, ..., 1, 0, 2]) -
按照正确的聚类标签绘制坐标点:
plt.scatter(X[:, 0], X[:, 1], c=y,s=50, cmap='tab20b') plt.show()绘图如下所示:

图 1.17:带有正确聚类标签的坐标点绘图
练习 5:实现带优化的 k-means
让我们自己重新创建这些结果!我们将通过一个例子来实现这个过程,并进行一些优化。这个练习是在前一个练习的基础上构建的,应在同一个 Jupyter notebook 中进行。对于这个练习,我们将依赖 SciPy,一个 Python 包,它提供了对高效版本科学计算的便捷访问。特别是,我们将使用 cdist 实现欧几里得距离,该函数的功能以更高效的方式复制了我们距离度量的基本实现:
-
欧几里得距离的非向量化实现如下:
def dist(a, b): return math.sqrt(math.pow(a[0]-b[0],2) + math.pow(a[1]-b[1],2)) -
现在,实现优化的欧几里得距离:
from scipy.spatial.distance import cdist -
存储 X 的值:
X[105:110]输出如下:
array([[-3.09897933, 4.79407445], [-3.37295914, -7.36901393], [-3.372895 , 5.10433846], [-5.90267987, -3.28352194], [-3.52067739, 7.7841276 ]]) -
计算距离并选择最短距离的索引作为一个聚类:
for x in X[105:110]: calcs = [] for c in centroids: calcs.append(dist(x, c)) print(calcs, "Cluster Membership: ", np.argmin(calcs, axis=0)) -
定义
k_means函数如下,并随机初始化 k 个质心。使用while循环重复该过程,直到新旧centroids之间的差值为0:def k_means(X, K): # Keep track of history so you can see k-means in action centroids_history = [] labels_history = [] rand_index = np.random.choice(X.shape[0], K) centroids = X[rand_index] centroids_history.append(centroids) while True: # Euclidean distances are calculated for each point relative to # centroids, and then np.argmin returns the index location of the # minimal distance - which cluster a point is assigned to labels = np.argmin(cdist(X, centroids), axis=1) labels_history.append(labels) # Take mean of points within clusters to find new centroids new_centroids = np.array([X[labels == i].mean(axis=0) for i in range(K)]) centroids_history.append(new_centroids) # If old centroids and new centroids no longer change, k-means is # complete and end. Otherwise continue if np.all(centroids == new_centroids): break centroids = new_centroids return centroids, labels, centroids_history, labels_history centers, labels, centers_hist, labels_hist = k_means(X, 3)注意
请不要破坏这段代码,因为这样可能会导致错误。
-
将中心的历史步骤及其标签压缩在一起:
for x, y in history: plt.figure(figsize=(4,3)) plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='tab20b'); plt.scatter(x[:, 0], x[:, 1], c='red') plt.show()第一张图如下所示:

图 1.18:第一次散点图
第二张图如下所示:

图 1.19:第二次散点图
第三张图如下所示:

图 1.20:第三次散点图
正如你在上面的图中看到的,K-means 采用迭代方法,通过距离不断精细化最佳聚类。该算法从随机初始化开始,根据数据的复杂性,迅速找到最合理的分隔。
聚类性能:轮廓分数
理解无监督学习方法的性能本质上比监督学习方法要困难得多,因为通常没有明确的“最佳”解决方案。对于监督学习,有许多可靠的性能指标——其中最直接的指标就是通过将模型预测标签与实际标签进行比较,并查看模型预测正确的数量来衡量准确度。不幸的是,对于聚类,我们没有标签可以依赖,需要建立对聚类“差异”的理解。我们通过轮廓分数(Silhouette Score)指标来实现这一点。这个方法的固有特性是,我们还可以使用轮廓分数来寻找无监督学习方法的最佳“K”聚类数量。
轮廓分数指标通过分析一个点在其聚类中的适配程度来工作。该指标的范围是从-1 到 1——如果你在聚类中计算的平均轮廓分数为 1,那么你将达成完美的聚类,且不会有任何混淆,知道哪个点属于哪个聚类。如果你想象我们上一个练习中的散点图,轮廓分数会接近 1,因为聚类之间的“球”非常紧凑,并且每个“球”之间有明显的距离。不过这种情况非常罕见——轮廓分数应视为尽力而为的结果,因为获得 1 的可能性极低。
从数学上讲,轮廓分数的计算通过简化的轮廓指数(SSI)非常简单,如下所示:
,其中
是点 i 到其所在聚类质心的距离,
是点 i 到最近的聚类质心的距离。
这里捕捉到的直觉是,
表示点i的簇作为一个明确簇的凝聚度,
表示簇之间的距离。我们将使用 scikit-learn 中silhouette_score的优化实现来完成活动 1,实现 k-means 聚类。使用它非常简单,只需传入特征数组和从 k-means 聚类方法中预测的簇标签。
在下一个练习中,我们将使用 pandas 库来读取 CSV。Pandas 是一个 Python 库,通过使用 DataFrame 使得数据处理变得更容易。要在 Python 中读取数据,你可以使用variable_name = pd.read_csv('file_name.csv', header=None)。
练习 6:计算轮廓系数
在这个练习中,我们将学习如何计算一个数据集的轮廓系数(Silhouette Score),并且该数据集有一个固定数量的簇。为此,我们将使用 Iris 数据集,数据集可以在github.com/TrainingByPackt/Unsupervised-Learning-with-Python/tree/master/Lesson01/Exercise06找到。
注意
这个数据集是从archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data下载的,可以通过github.com/TrainingByPackt/Unsupervised-Learning-with-Python/tree/master/Lesson01/Exercise06访问。
-
使用 pandas 加载 Iris 数据文件,pandas 是一个通过使用 DataFrame 使数据处理变得更加容易的库:
import pandas as pd import numpy as np import matplotlib.pyplot as plt from sklearn.metrics import silhouette_score from scipy.spatial.distance import cdist iris = pd.read_csv('iris_data.csv', header=None) iris.columns = ['SepalLengthCm', 'SepalWidthCm', 'PetalLengthCm', 'PetalWidthCm', 'species'] -
分离
X特征,因为我们希望将其视为无监督学习问题:X = iris[['SepalLengthCm', 'SepalWidthCm', 'PetalLengthCm', 'PetalWidthCm']] -
引入我们之前做的
k_means函数作为参考:def k_means(X, K): #Keep track of history so you can see k-means in action centroids_history = [] labels_history = [] rand_index = np.random.choice(X.shape[0], K) centroids = X[rand_index] centroids_history.append(centroids) while True: # Euclidean distances are calculated for each point relative to # centroids, #and then np.argmin returns # the index location of the minimal distance - which cluster a point # is #assigned to labels = np.argmin(cdist(X, centroids), axis=1) labels_history.append(labels) #Take mean of points within clusters to find new centroids: new_centroids = np.array([X[labels == i].mean(axis=0) for i in range(K)]) centroids_history.append(new_centroids) # If old centroids and new centroids no longer change, k-means is # complete and end. Otherwise continue if np.all(centroids == new_centroids): break centroids = new_centroids return centroids, labels, centroids_history, labels_history -
将我们的 Iris
X特征 DataFrame 转换为NumPy矩阵:X_mat = X.values -
在 Iris 矩阵上运行我们的
k_means函数:centroids, labels, centroids_history, labels_history = k_means(X_mat, 3) -
计算
PetalLengthCm和PetalWidthCm列的轮廓系数(Silhouette Score):silhouette_score(X[['PetalLengthCm','PetalWidthCm']], labels)输出结果类似于:
0.6214938502379446
在这个练习中,我们计算了 Iris 数据集的PetalLengthCm和PetalWidthCm列的轮廓系数。
活动 1:实现 k-means 聚类
情景:在面试中,你被要求从零实现一个 k-means 聚类算法,以证明你理解其工作原理。我们将使用 UCI ML 库提供的 Iris 数据集。Iris 数据集是数据科学界的经典,具有用于预测鸢尾花物种的特征。下载链接将在后面的活动中提供。
对于这个活动,你可以使用 Matplotlib、NumPy、scikit-learn 指标和 pandas。
通过轻松加载和重塑数据,你可以更多地专注于学习 k-means,而不是编写数据加载器功能。
Iris 数据列如下所示,供参考:
['SepalLengthCm', 'SepalWidthCm', 'PetalLengthCm', 'PetalWidthCm', 'species']
目标:要真正理解某个事物如何运作,您需要从头开始构建它。将您在前面的章节中学到的知识付诸实践,并在 Python 中从零开始实现 k-means。
请打开您最喜欢的编辑平台并尝试以下内容:
-
使用
NumPy或math包及欧几里得距离公式,编写一个计算两个坐标之间距离的函数。 -
编写一个函数,计算数据集中每个点到质心的距离,并返回簇的成员身份。
-
编写一个 k-means 函数,接受一个数据集和簇的数量(K),并返回最终的聚类质心以及构成该聚类的成员数据点。在从零开始实现 k-means 后,将您的自定义算法应用到鸢尾花数据集,数据集位置如下:
github.com/TrainingByPackt/Unsupervised-Learning-with-Python/tree/master/Lesson01/Activity01。注意
这个数据集是从
archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data下载的。可以在github.com/TrainingByPackt/Unsupervised-Learning-with-Python/tree/master/Lesson01/Activity01访问。UCI 机器学习库 [
archive.ics.uci.edu/ml]。加利福尼亚州欧文市:加利福尼亚大学信息与计算机科学学院。 -
移除此数据集中提供的类别,看看您的 k-means 算法能否仅根据植物特征将不同的鸢尾花物种分到正确的组别!
-
使用 scikit-learn 实现计算轮廓系数。
结果:通过完成这个练习,您将获得调优 k-means 聚类算法以适应真实世界数据集的实践经验。鸢尾花数据集被视为数据科学领域经典的“hello world”问题,适合用于测试基础技术。您的最终聚类算法应该能够较好地找到数据中存在的三类鸢尾花物种,具体如下:

图 1.21:鸢尾花物种的三类聚类期望图
注意
此活动的解决方案可以在第 306 页找到。
总结
在本章中,我们探讨了聚类的定义以及它在各种数据挑战中的重要性。基于这一聚类知识的基础,您实现了 k 均值算法,这是一种最简单但也最流行的无监督学习方法之一。如果您能够在这里总结并能够逐步向您的同学解释 k 均值算法的操作步骤,那么干得漂亮!如果不能,请返回并复习之前的材料——从这里开始,内容将变得更加复杂。接下来,我们将转向层次聚类,其中的一种配置重复使用了我们在 k 均值中使用的质心学习方法。在下一章中,我们将进一步阐述其他聚类方法和方法。
第二章:层次聚类
学习目标
到本章结束时,你将能够:
-
从头开始实现层次聚类算法,使用软件包
-
执行聚合聚类
-
比较 k-means 和层次聚类
在这一章中,我们将使用层次聚类来构建更强的分组,这些分组在逻辑上更合理。
介绍
在这一章中,我们将在第一章《聚类介绍》中构建的基本思想基础上扩展,通过将聚类与相似性的概念结合起来。我们将再次实现欧几里得距离的各种形式来捕捉相似性这一概念。需要记住的是,欧几里得距离只是最流行的距离度量之一,而不是唯一的度量!通过这些距离度量,我们将通过引入层次结构的概念,扩展我们在上一章中探索的简单邻居计算。通过使用层次结构传递聚类信息,我们可以构建出更强的、在逻辑上更合理的分组。
聚类复习
第一章《聚类介绍》涵盖了最基础聚类算法之一:k-means 的高层次直觉和深入细节。虽然这确实是一种简单的方法,但不要小看它;它将是你继续探索无监督学习世界时,工具箱中的一个宝贵补充。在许多实际应用场景中,公司通过最简单的方法,比如 k-means 或线性回归(监督学习)取得了突破性的发现。作为复习,我们快速回顾一下什么是聚类以及 k-means 如何找到它们:

图 2.1:区分监督学习和无监督学习问题的属性
如果你被给了一组没有任何指导的随机数据,你可能会开始使用基本统计方法进行探索——例如,求出每个特征的均值、中位数和众数。记住,从一个简单存在的高层次数据模型来看,是否是监督学习或无监督学习是由你为自己设定的目标或由经理设定的目标决定的。如果你确定其中一个特征实际上是标签,并且你想查看数据集中其余特征对它的影响,那么这将成为一个监督学习问题。然而,如果在初步探索后你意识到你所拥有的数据实际上只是没有目标的特征集合(例如一组健康指标、网店的购买发票等),那么你可以通过无监督方法来分析它。
无监督学习的一个经典例子是在来自网店发票的集合中找到相似客户的簇。您的假设是通过了解哪些人最相似,您可以创建更细粒度的营销活动,以迎合每个簇的兴趣。实现这些相似用户簇的一种方式是通过 k-means。
k-means 刷新
k-means 聚类是通过在您的数据中找到“k”个簇来工作,通过成对的欧氏距离计算。“K”点(也称为质心)在您的数据中随机初始化,并且计算每个数据点到每个质心的距离。这些距离的最小值指定了数据点属于哪个簇。一旦每个点都被分配到一个簇中,就会计算出簇内数据点的平均值作为新的质心。这个过程重复进行,直到新计算出的簇质心不再改变位置。
层次结构的组织
自然界和人造世界都包含许多将系统组织成层次结构的例子,大多数情况下这是很有道理的。从这些层次结构中开发出的常见表示可以在基于树的数据结构中看到。想象一下,你有一个父节点和任意数量的子节点,这些子节点随后可以成为自己的父节点。通过将概念组织成树形结构,您可以构建一个信息密集的图表,清晰地显示事物与其同行及其更大抽象概念的关系。
一个帮助说明这一概念的自然界的例子可以在如何看待动物的层次结构中看到,这些层次结构从父类到个体物种:

图 2.2: 在层次树结构中导航动物物种的关系
在图 2.2 中,您可以看到动物品种之间关系信息的一个示例,以一种既节省空间又传递大量信息的方式进行映射。这个例子可以看作是其自身的一棵树(显示猫和狗如何不同但都是驯养动物),也可以看作是一个更大树的一部分,显示驯养与非驯养动物的分解。
假设大多数人不是生物学家,让我们回到一个销售产品的网店的概念。如果你销售多种产品,那么你可能希望为客户创建一个层次化的导航系统。通过仅展示产品目录中的所有信息,客户将只暴露于与其兴趣相匹配的树路径。层次化导航的好处的一个示例可以在图 2.3 中看到:

图 2.3:在层次树结构中导航产品类别
显然,层级导航系统的优势在改善客户体验方面是无法过分强调的。通过将信息组织成层级结构,你可以从数据中构建一个直观的结构,展示明确的嵌套关系。如果这听起来像是另一种在数据中寻找聚类的方法,那么你绝对走在正确的轨道上!通过使用类似欧氏距离这样的距离度量方法(如 k-means 中的欧氏距离),我们可以开发出一棵树,展示数据的许多切割点,允许用户根据需要主观地创建聚类。
分层聚类简介
直到这一点为止,我们已经展示了层级结构可以作为一个很好的组织信息的方式,清晰地展示了数据点之间的嵌套关系。虽然这在理解项目之间的父子关系时非常有帮助,但在形成聚类时也非常有用。以前一节的动物示例为扩展,假设你仅仅获得了两种动物的特征:它们的身高(从鼻尖到尾端的长度)和体重。利用这些信息,你需要重新构建相同的结构,以便识别数据集中哪些记录对应于狗或猫,并区分它们的亚种。
由于你仅仅获得了动物的身高和体重信息,你无法推断出每个物种的具体名称。然而,通过分析你所得到的特征,你可以在数据中构建一个结构,用以近似表示数据中存在的动物物种。这为无监督学习问题提供了一个理想的背景,而分层聚类则是一个非常适合解决该问题的方法。在下面的图表中,你将看到我们在左侧创建的两个特征:左栏是动物的身高,右栏是动物的体重。接着,这些数据被绘制在一个二维坐标图上,身高为 x 轴,体重为 y 轴:

图 2.4:一个包含动物身高和体重的二维特征数据集示例
一种进行分层聚类的方法是从每个数据点作为其自身的聚类开始,并递归地将相似的点结合在一起形成聚类——这被称为凝聚型分层聚类。我们将在后面的章节中详细介绍不同的分层聚类方法。
在凝聚型层次聚类方法中,数据点相似性的概念可以在我们在 k-means 中看到的范式下进行思考。在 k-means 中,我们使用欧几里得距离来计算个体点到预期“k”簇的质心之间的距离。对于这种层次聚类方法,我们将重新使用相同的距离度量来确定数据集中记录之间的相似性。
最终,通过递归地将数据中每个记录与其最相似的记录进行组合,您会从底部构建一个层次结构。所有单成员簇将汇聚成一个顶层簇,形成层次结构的最上层。
执行层次聚类的步骤
为了理解凝聚型层次聚类的工作原理,我们可以通过一个简单的示例程序追踪它如何合并并形成一个层次结构:
-
给定 n 个样本数据点,将每个点视为一个单独的“簇”,其成员仅为该点。
-
计算所有簇的质心之间的成对欧几里得距离。
-
将最接近的点对聚集在一起。
-
重复步骤 2 和 步骤 3,直到您得到一个包含所有数据的单一簇。
-
绘制一个树状图,显示您的数据如何以层次结构的形式汇聚在一起。树状图只是用来表示树形结构的图表,展示簇从上到下的排列。
-
决定要在哪个层级创建簇。
分层聚类的示例演示
尽管比 k-means 稍微复杂一些,层次聚类在逻辑上并没有太大变化。以下是一个简单的示例,通过稍微详细的步骤演示前述过程:
-
给定四个样本数据点的列表,将每个点视为一个质心,该质心也是它自身的簇,点的索引为 0 到 3:
簇(4):[ (1,7) ], [ (-5,9) ], [ (-9,4) ] , [ (4, -2) ]
质心(4):[ (1,7) ], [ (-5,9) ], [ (-9,4) ] , [ (4, -2) ]
-
计算所有簇的质心之间的成对欧几里得距离。在下图显示的矩阵中,点的索引在水平方向和垂直方向上都介于 0 和 3 之间,表示各自点之间的距离。沿对角线的值极高,以确保我们不会将一个点选为其自身的邻居(因为它在技术上是“最接近”的点)。请注意,这些值在对角线两侧是对称的:
![图 2.5:距离数组]()
图 2.5:距离数组
-
将最接近的点对聚集在一起。
在这种情况下,点[1,7]和[-5,9]由于最接近而合并成一个簇,其余两个点则保持为单成员簇:
![图 2.6:距离数组]()
图 2.6:距离数组
这里是最终得到的三个簇:
[ [1,7], [-5,9] ] [-9,4] [4,-2] -
计算两个成员簇的质心,如下所示:
mean([ [1,7], [-5,9] ]) = [-2,8] -
将聚类中心添加到两个单一成员的聚类中心,并重新计算距离。
聚类 (3):
[ [1,7], [-5,9] ] [-9,4] [4,-2]聚类中心 (3):
[-2,8] [-9,4] [4,-2]输出将类似于以下图示,最短距离通过红色箭头标示:
![图 2.7:距离数组]()
图 2.7:距离数组
-
由于它具有最短的距离,点[-9,4]被添加到聚类 1:
聚类 (2):
[ [1,7], [-5,9], [-9,4] ] [4,-2] -
由于只有点(4,-2)与其邻近点的距离最远,你可以将它添加到聚类 1,从而统一所有聚类:
聚类 (1):
[ [ [1,7], [-5,9], [-9,4], [4,-2] ] ] -
绘制树状图以展示点与聚类之间的关系:

图 2.8:展示点与聚类之间关系的树状图
在这个过程的最后,你可以通过树状图可视化你创建的层次结构。这个图展示了数据点的相似性,看起来与我们之前讨论的层次树状结构相似。一旦你拥有了这个树状图结构,你可以解释数据点之间的关系,并主观决定聚类应存在于哪个“层级”。
回顾之前涉及狗和猫物种的动物分类示例,假设你面临以下树状图:

图 2.9:一个动物分类的树状图
层次聚类和树状图的优点在于,你可以看到所有可能的聚类拆分。如果你只对将物种数据集分为狗和猫感兴趣,你可以在分组的第一级停止聚类。然而,如果你想将所有物种分为家养动物和非家养动物,你可以在第二级停止聚类。
练习 7:构建层次结构
让我们尝试在 Python 中实现前述的层次聚类方法。通过为直觉打下框架,现在我们可以探索如何使用SciPy提供的辅助函数构建层次聚类。这个练习使用了SciPy,这是一个开源库,提供了许多有助于科学和技术计算的函数;例如,它简化了线性代数和微积分相关方法的实现。除了SciPy,我们还将使用 Matplotlib 来完成这个练习:
-
生成如下虚拟数据:
from scipy.cluster.hierarchy import linkage, dendrogram, fcluster from sklearn.datasets import make_blobs import matplotlib.pyplot as plt %matplotlib inline # Generate a random cluster dataset to experiment on. X = coordinate points, y = cluster labels (not needed) X, y = make_blobs(n_samples=1000, centers=8, n_features=2, random_state=800) -
可视化数据如下:
plt.scatter(X[:,0], X[:,1]) plt.show()输出结果如下:
![图 2.10:虚拟数据的绘图]()
图 2.10:虚拟数据的绘图
在绘制这个简单的示例数据之后,应该很清楚我们的虚拟数据包含了八个聚类。
-
我们可以通过内置的
SciPy包轻松生成距离矩阵,使用'linkage':# Generate distance matrix with 'linkage' function distances = linkage(X, method="centroid", metric="euclidean") print(distances)输出结果如下:
![图 2.11:距离矩阵]()
图 2.11:距离的矩阵
在第一种情况中,你可以看到,定制超参数确实会影响找到理想链接矩阵的性能。如果你回顾我们之前的步骤,链接的工作原理是通过计算每个数据点之间的距离来完成的。在
linkage函数中,我们可以选择度量标准和方法(稍后我们会详细讲解)。 -
在我们确定了链接矩阵后,可以轻松地将其传递给
SciPy提供的树状图函数:dn = dendrogram(distances) plt.show()输出如下:
![图 2.12:距离的树状图]()
图 2.12:距离的树状图
这个图将帮助我们更好地理解数据的潜在分组情况。
-
使用这些信息,我们可以通过使用
SciPy的fcluster函数来完成我们的层次聚类练习。公式中的数字3在这里表示 -
以下示例表示你将设置的最大簇间距离阈值超参数。这个超参数可以根据你所使用的数据集进行调整;然而,对于本次练习,它的默认值为
3:scipy_clusters = fcluster(distances, 3, criterion="distance") plt.scatter(X[:,0], X[:,1], c=scipy_clusters) plt.show()输出如下:

图 2.13:距离的散点图
只需调用 SciPy 提供的几个辅助函数,你就可以轻松地在几行代码中实现聚合聚类。尽管 SciPy 在许多中间步骤中提供了帮助,但这个例子仍然稍显冗长,可能不是你在日常工作中会遇到的情况。我们将在稍后的部分介绍更加简洁的实现方式。
链接
在练习 7,构建层次结构中,你使用了被称为质心链接的层次聚类方法。链接是指确定如何计算簇间距离的概念,这取决于你所面临的问题类型。选择质心链接是因为它本质上与我们在 k-means 中使用的新的质心搜索方法相似。然而,在将数据点聚类时,这并不是唯一的选择。另两种常见的用于计算簇间距离的方法是单链接和完全链接。
单链接通过找到两个簇之间一对点的最小距离来确定链接标准。简单来说,它通过基于两簇之间最接近的点来合并簇。数学表达式如下:
dist(a,b) = min( dist( a[i]), b[j] ) )
完全链接与单链接相反,它通过找到两个簇之间一对点的最大距离来确定链接标准。简单来说,它通过基于两簇之间最远的点来合并簇。数学表达式如下:
dist(a,b) = max( dist( a[i]), b[j] ) )
确定哪种连接标准最适合你的问题既是艺术也是科学,并且极大依赖于你特定的数据集。选择单一连接的一种原因是你的数据在邻近点上非常相似,因此,当存在差异时,这些数据就会表现出极大的不同。由于单一连接通过找到最接近的点来工作,因此它不会受到这些远离点的影响。相反,如果你的数据在类间较远,但类内相对密集,那么完全连接可能是一个更好的选择。质心连接有类似的优点,但如果数据非常嘈杂且聚类的“中心”不明确,它可能会失效。通常,最好的方法是尝试几种不同的连接标准选项,看看哪种最符合你的数据,并与目标最相关。
活动 2:应用连接标准
回想一下我们在上一练习中生成的八个聚类的虚拟数据。在现实世界中,你可能会获得类似的实际数据,这些数据表现得像离散的高斯“团块”。假设这些虚拟数据代表了某个特定商店中不同的顾客群体。商店经理要求你分析顾客数据,将顾客分类成不同的群体,以便根据每个群体的特点定制营销材料。
使用在上一练习中已经生成的数据,或生成新数据,你将分析哪些连接方式最适合将顾客分为不同的群体。
一旦你生成了数据,使用 SciPy 提供的文档来查看linkage函数中可用的连接类型。然后,通过将它们应用到你的数据中来评估这些连接类型。你应该测试的连接类型在以下列表中:
['centroid', 'single', 'complete', 'average', 'weighted']
完成这个活动后,你将理解连接标准——了解它对于你的层次聚类效果至关重要。目标是理解连接标准在不同数据集中的作用,以及它如何将一个无效的聚类转变为一个有效的聚类。
你可能会发现我们没有涵盖所有之前提到的连接类型——这项活动的关键部分是学习如何解析包提供的文档字符串,以探索它们的所有功能。
以下是完成此活动所需的步骤:
-
可视化我们在练习 7中创建的数据集,构建层次结构。
-
创建一个包含所有可能连接方法超参数的列表。
-
遍历你刚刚创建的列表中的每个方法,并展示它们对相同数据集的影响。
你应该为每种连接方式生成一个图表,并利用这些图表评论哪种连接方式最适合该数据。
你将生成的图表应该类似于下图所示:

图 2.14:所有方法的预期散点图
注意
这个活动的解决方案在第 310 页。
聚合式与分裂式聚类
迄今为止,我们的层次聚类实例都是聚合式的——也就是说,它们是从下往上构建的。虽然这种方法通常是这种类型聚类中最常见的做法,但重要的是要知道,它并不是创建层次结构的唯一方式。相反的层次方法,即从上往下构建,也可以用于创建分类法。这个方法叫做分裂式层次聚类,它的工作原理是将数据集中所有的数据点放在一个大的聚类中。分裂式方法的许多内部机制与聚合式方法非常相似:

图 2.15:聚合式与分裂式层次聚类
与大多数无监督学习中的问题一样,选择最佳方法通常高度依赖于你所面临的具体问题。
想象一下,你是一位刚刚购买了一家新杂货店的企业家,需要为商店进货。你收到了一个装满食物和饮料的大货柜,但你已经丢失了所有的货物信息!为了最有效地销售商品,你必须将类似的商品分组(如果你把所有东西随机摆放到货架上,商店将会是一团糟)。为了解决这个组织问题,你可以采取自下而上或自上而下的方法。从自下而上的角度,你会将整个运输容器视为杂乱无章——然后,你会拿起一个随机的物品,并找到它最相似的商品。例如,你可能会拿起苹果汁,意识到将它与橙汁放在一起是合理的。采用自上而下的方法,你会将所有物品视为一个大组。然后,你会遍历库存,并根据最大差异将这些组拆分开来。例如,你最初可能会认为苹果汁和豆腐是搭配的,但仔细想想,它们实际上非常不同。因此,你会把它们分成更小、不相似的组。
一般来说,帮助理解聚合方法是自下而上的方式,而分裂方法是自上而下的方式——但它们在性能上如何权衡呢?由于聚合方法的贪婪性质,它可能会被局部邻居所迷惑,无法看到任何时候形成的聚类的更大意义。另一方面,分裂方法的好处在于,它从一开始就能看到整个数据分布,并选择最佳的方式来拆分聚类。了解整个数据集的分布有助于潜在地创建更准确的聚类,这一点不容忽视。不幸的是,通常情况下,顶层的分裂方法会以更深的复杂度来交换更高的准确度。在实际应用中,聚合方法通常能正常工作,并且在层次聚类中应该是优先的起始点。如果在检查层次结构后,你对结果不满意,可以考虑使用分裂方法。
练习 8:使用 scikit-learn 实现聚合层次聚类
在大多数实际应用中,你可能会发现自己在使用一个将所有内容抽象化的包来实现层次聚类,比如 scikit-learn。Scikit-learn 是一个免费且不可或缺的 Python 机器学习包。它便捷地提供了许多流行算法的高度优化版本,如回归、分类和聚类等。通过使用像 scikit-learn 这样的优化包,你的工作变得更加轻松。然而,只有在你完全理解了前面章节中层次聚类的工作原理后,才应使用它。以下练习将比较两种形成聚类的潜在方法——使用 SciPy 和 scikit-learn。通过完成这个练习,你将了解它们各自的优缺点,以及从用户角度看,哪种方法最适合你:
-
Scikit-learn 使得实现变得非常简单,只需几行代码:
from sklearn.cluster import AgglomerativeClustering from sklearn.datasets import make_blobs import matplotlib.pyplot as plt from scipy.cluster.hierarchy import linkage, dendrogram, fcluster ac = AgglomerativeClustering(n_clusters = 8, affinity="euclidean", linkage="average") X, y = make_blobs(n_samples=1000, centers=8, n_features=2, random_state=800) distances = linkage(X, method="centroid", metric="euclidean") sklearn_clusters = ac.fit_predict(X) scipy_clusters = fcluster(distances, 3, criterion="distance")首先,我们通过传入我们熟悉的参数,如
affinity(距离函数)和linkage(如同我们在活动 2中所做的那样,探索你的选项,实现连接准则),将模型分配给ac变量。 -
在实例化模型到变量后,我们可以简单地传入我们感兴趣的数据集,并使用
.fit_predict()来确定聚类的归属,并将结果分配给另一个变量。 -
然后,我们可以通过绘图比较每种方法的最终聚类结果,看看 scikit-learn 方法的聚类:
plt.figure(figsize=(6,4)) plt.title("Clusters from Sci-Kit Learn Approach") plt.scatter(X[:, 0], X[:, 1], c = sklearn_clusters ,s=50, cmap='tab20b') plt.show()以下是 scikit-learn 方法聚类的输出:
![图 2.16:Scikit-Learn 方法的图示]()
图 2.16:Scikit-Learn 方法的图示
看看 SciPy 学习方法的聚类:
plt.figure(figsize=(6,4))
plt.title("Clusters from SciPy Approach")
plt.scatter(X[:, 0], X[:, 1], c = scipy_clusters ,s=50, cmap='tab20b')
plt.show()
输出如下:

图 2.17:SciPy 方法的图示
正如你在我们的示例问题中看到的,两个方法最终收敛到了基本相同的聚类。虽然从玩具问题的角度来看这很好,但你很快会在下一个活动中学到,输入参数的微小变化可能会导致完全不同的结果!
活动 3:比较 k-means 和层次聚类
你正在管理一家商店的库存,并收到一大批葡萄酒,但在运输过程中瓶上的品牌标签掉了。幸运的是,供应商提供了每瓶酒的化学成分数据及其对应的序列号。不幸的是,你不能打开每一瓶酒进行品尝测试,你必须找到一种方法,根据化学成分将未标记的瓶子重新分组!你知道从订单列表中,自己订购了三种不同类型的葡萄酒,并且只给出了两种葡萄酒属性来将它们重新分组。在这个活动中,我们将使用葡萄酒数据集。
注意
葡萄酒数据集可以从 archive.ics.uci.edu/ml/machine-learning-databases/wine/ 下载。也可以通过 github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson02/Activity03 访问。
UCI 机器学习库 [archive.ics.uci.edu/ml]。加利福尼亚州欧文市:加利福尼亚大学信息与计算机科学学院
本活动的目的是在葡萄酒数据集上实现 k-means 和层次聚类,并探索哪种方法最终更准确或更易于使用。你可以尝试不同的 scikit-learn 实现组合,并使用 SciPy 和 NumPy 中的辅助函数。你可以使用轮廓系数(silhouette score)比较不同的聚类方法,并在图表上可视化聚类结果。
预期结果:
完成此活动后,你将理解 k-means 和层次聚类在相似数据集上的工作原理。你可能会注意到,根据数据的形状,一个方法比另一个方法表现得更好。此活动的另一个关键收获是理解在任何给定用例中超参数的重要性。
以下是完成此活动的步骤:
-
从 scikit-learn 导入必要的包(
KMeans、AgglomerativeClustering和silhouette_score)。 -
将葡萄酒数据集读入 pandas DataFrame 并打印一个小样本。
-
可视化葡萄酒数据集,以理解其数据结构。
-
使用 sklearn 实现的 k-means 算法处理葡萄酒数据集,已知有三种葡萄酒类型。
-
在葡萄酒数据集上使用 sklearn 实现的层次聚类。
-
绘制 k-means 聚类的预测结果。
-
绘制来自层次聚类的预测聚类。
-
比较每种聚类方法的轮廓系数。
如下所示,绘制来自 k-means 聚类方法的预测聚类:

图 2.18:k-means 方法的预期聚类
如下所示,绘制来自凝聚层次聚类方法的预测聚类:

图 2.19:来自凝聚方法的预期聚类
注意
本活动的解决方案见第 312 页。
k-means 与层次聚类
现在我们已经扩展了对 k-means 聚类如何工作的理解,接下来要探讨的是层次聚类在整个过程中所扮演的角色。如在连接准则部分所提到的,当使用质心进行数据点分组时,确实存在一些直接重叠的可能性。到目前为止,所有提到的方法都通用地使用了距离函数来确定相似度。由于我们在上一章的深入探讨,我们一直使用欧几里得距离,但我们理解,任何距离函数都可以用来确定相似度。
实际操作中,选择某种聚类方法而非另一种方法时,有一些快速的要点:
-
层次聚类的优势在于不需要预先传入明确的“k”聚类数。这意味着你可以在算法完成后找到所有潜在的聚类,并决定哪些聚类最为合理。
-
从简化的角度来看,k-means 聚类有其优势——在商业应用中,往往需要找到既能向非技术观众解释,又能生成高质量结果的方法。k-means 可以轻松地填补这一空白。
-
层次聚类在处理形状异常的数据时,比 k-means 聚类有更多的参数可供调整。虽然 k-means 非常擅长发现离散聚类,但当涉及到混合聚类时,它可能会出现问题。通过调整层次聚类中的参数,你可能会得到更好的结果。
-
Vanilla k-means 聚类通过初始化随机质心并找到与这些质心最接近的点来工作。如果它们在特征空间中被随机初始化在离数据很远的地方,那么可能需要相当长的时间才能收敛,或者甚至可能永远无法到达那个点。层次聚类则不太容易受到这种弱点的影响。
总结
本章中,我们讨论了层次聚类的工作原理以及它最适合应用的场景。特别是,我们讨论了如何通过评估树状图(dendrogram)图来主观选择聚类的各个方面。如果你完全不知道数据中要寻找什么,这相较于 k-means 聚类来说是一个巨大的优势。我们还讨论了驱动层次聚类成功的两个关键参数:聚合法和分裂法,以及连接准则。聚合法聚类采用自下而上的方法,通过递归地将相邻的数据组合在一起,直到形成一个大簇。而分裂法聚类则采用自上而下的方法,从一个大簇开始,递归地将其拆分,直到每个数据点都属于自己的簇。分裂法聚类由于从一开始就能完整地看到数据,具有更高的准确性;然而,它增加了一层复杂性,可能会降低稳定性并增加运行时间。
连接准则处理的是如何计算候选簇之间距离的概念。我们已经探讨了质心如何在超越 k-means 聚类后再次出现,以及单链和完全链连接准则。单链连接通过比较每个簇中最接近的点来寻找簇间的距离,而完全链连接则通过比较每个簇中更远的点来寻找簇间的距离。从你在本章中获得的理解来看,你现在能够评估 k-means 聚类和层次聚类如何最好地解决你所面临的挑战。在下一章中,我们将介绍一种在高度复杂数据中最适合我们的聚类方法:DBSCAN(基于密度的空间聚类噪声应用)。
第三章:邻域方法与 DBSCAN
学习目标
在本章结束时,你将能够:
-
理解邻域方法在聚类中的工作原理,从头到尾
-
从头实现 DBSCAN 算法,使用相关软件包
-
从 k-means、层次聚类和 DBSCAN 中选择最适合的算法来解决你的问题
本章我们将介绍 DBSCAN 聚类方法,这对于处理高度复杂的数据最为适用。
介绍
到目前为止,我们已经介绍了两种流行的聚类方法:k-means 和层次聚类。每种聚类技术在实施时都有优缺点。让我们再次回顾前两章的内容,以便为本章的内容提供更好的背景。
在无监督学习的挑战领域中,你将会面临一组特征数据,但没有补充标签告诉你这些特征变量的具体含义。尽管你无法直接得知目标标签是什么,但你可以通过将相似的群体聚集在一起,查看组内的相似性,来从数据中挖掘出一定的结构。我们之前讲解的聚类相似数据点的第一个方法是 k-means。
k-means 最适用于简单的数据挑战,其中速度至关重要。通过简单地查看最接近的数据点,计算开销不大,但当面对高维数据集时,挑战也会增大。如果你不知道可能需要寻找多少个聚类,k-means 也不适合使用。在第二章,层次聚类中,我们曾经探讨过通过化学分析数据来判断哪些葡萄酒属于同一类别。这一练习之所以有效,是因为你知道有三种类型的葡萄酒已经被订购。然而,如果你对最初的排序没有任何直觉,k-means 的效果会大打折扣。
我们探讨的第二种聚类方法是层次聚类。这种方法可以通过两种方式工作——合并式或分裂式。合并式聚类采用自下而上的方式,将每个数据点视为一个独立的簇,并通过连接准则递归地将它们聚集在一起。分裂式聚类则朝相反方向工作,它将所有数据点视为一个大的类别,并递归地将其拆分为更小的簇。这种方法的优势在于能够全面理解整个数据分布,因为它计算了分割的潜力;然而,由于其更高的复杂性,通常在实际操作中并不常用。在不知道数据任何信息的情况下,层次聚类是聚类需求的有力竞争者。通过使用树状图,你可以可视化数据中的所有分割,并在事后考虑哪个簇的数量更合理。在你的特定使用案例中,这非常有帮助;然而,它也带来了与 k 均值聚类相似的较高计算成本。
在本章中,我们将介绍一种在高度复杂数据中最适合的聚类方法:DBSCAN(基于密度的空间聚类与噪声)。经典地,这种方法一直被认为在数据集密集分布的情况下表现优秀。让我们一起探讨它为什么在这些用例中表现如此出色。
聚类作为邻域
在前两章中,我们探讨了相似性的概念,这一概念通过欧几里得距离来描述——与某一点较近的数据点可以视为相似,而在欧几里得空间中距离较远的数据点则可以视为不相似。这一概念在 DBSCAN 算法中再次出现。正如其冗长的名称所暗示的,DBSCAN 方法通过引入密度的概念,扩展了基本的距离度量评估。如果一群数据点都位于彼此相同的区域中,它们可以被视为同一簇的成员:

图 3.1:邻居与聚类有直接的联系
在前面的示例中,我们可以看到四个邻域。
与我们之前讨论过的仅关注距离的传统方法相比,基于密度的方法有许多优势。如果你仅将距离作为聚类的阈值,那么在面对稀疏特征空间和离群点时,可能会发现你的聚类结果毫无意义。无论是 k 均值聚类还是层次聚类,都会自动将空间中的所有数据点分组,直到没有剩余点为止。
虽然层次聚类确实在某种程度上绕过了这个问题,因为你可以在聚类后使用树状图来指定聚类的形成位置,但 k-means 仍然是最容易失败的,因为它是最简单的聚类方法。当我们开始评估基于邻域的聚类方法时,这些问题就不那么明显了:

图 3.2:示例树状图
通过在 DBSCAN 中引入邻居密度的概念,我们可以根据运行时选择的超参数,选择是否将异常值排除在聚类之外。只有具有密切邻居的数据点才会被视为同一聚类的成员,而那些距离较远的数据点则可以被视为未聚类的异常值。
DBSCAN 简介
正如前一节所提到的,当我们分析基于密度的聚类方法的优势时,DBSCAN 的强大之处就显现出来了。DBSCAN 将密度评估为邻域半径和在邻域中找到的最小点数的组合,这些点数被视为一个聚类。
如果我们重新考虑你被要求为商店整理一批未标记的葡萄酒货物的场景,可能更容易理解这个概念。在之前的示例中,已经明确说明,我们可以根据葡萄酒的特征(如科学化学特性)找到相似的葡萄酒。了解这些信息后,我们可以更轻松地将相似的葡萄酒分组,并且快速将产品整理好以供销售。希望到现在为止这一点已经清楚了——但可能不太清楚的是,你为商店订购的商品通常反映了现实世界的购买模式。为了在库存中促进品种多样性,但又能保证最受欢迎的葡萄酒有足够的库存,你的商品种类往往会呈现出高度不均衡的分布。大多数人喜欢经典的葡萄酒,如白葡萄酒和红葡萄酒,但你可能仍会为那些喜欢昂贵葡萄酒的顾客提供更多异国情调的葡萄酒。这使得聚类更加困难,因为存在不均衡的类别分布(例如,你不会订购每种葡萄酒各 10 瓶)。
DBSCAN 与 k-means 和层次聚类的不同之处在于,你可以将直觉融入到如何评估我们感兴趣的顾客聚类的过程中。它可以以更简单的方式去除噪声,并且仅指出那些在营销活动中具有最高潜力的顾客。
通过基于邻域的聚类方法,我们可以区分出那些可以视为随机噪声的偶尔顾客,以及那些一次次光顾我们商店的更有价值的顾客。这种方法自然会引发关于如何确定邻域半径和每个邻域最小点数的最佳数值的问题。
作为一种高级启发式方法,我们希望将邻域半径设置得较小,但又不能太小。在极端的一端,你可以将邻域半径设置得非常大——这可能导致将所有点视为一个庞大的聚类。而在另一端,你可以将邻域半径设置得非常小。过小的邻域半径可能导致没有任何点被聚集在一起,并且出现大量单一成员的聚类。
类似的逻辑适用于构成聚类的最小点数。最小点数可以看作是一个次要阈值,它根据你数据空间中可用的数据来调整邻域半径。如果你在特征空间中的所有数据非常稀疏,最小点数会变得尤为重要,它与邻域半径配合使用,以确保不会只是大量无关的数据点。当数据非常密集时,最小点数阈值就不像邻域半径那样成为主导因素。
如你所见,这两个超参数规则的最佳选择通常依赖于数据集的具体情况。很多时候,你需要找到一个“恰到好处”的区间,即超参数既不过小,也不过大。
DBSCAN 深入解析
为了观察 DBSCAN 如何工作,我们可以通过一个简单的示例程序,跟踪其如何合并形成不同的聚类和噪声标记数据点:
-
给定 n 个未访问的样本数据点,在循环中依次遍历每个点并标记为已访问。
-
从每个点出发,查看与数据集中其他所有点的距离。
-
对于所有位于邻域半径超参数内的点,将它们连接为邻居。
-
检查邻居的数量是否至少达到所需的最小点数。
-
如果达到最小点数阈值,将点归为一个聚类。如果没有,将该点标记为噪声。
-
重复此过程,直到所有数据点被分类到聚类中或标记为噪声。
在某些方面,DBSCAN 算法相对简单——虽然引入了通过邻域半径和最小点数来衡量密度的新概念,但它的核心仍然是使用距离度量进行评估。
DBSCAN 算法演示
这是一个简单的示例,稍微详细地演示了前述步骤:
-
给定四个样本数据点,将每个点视为一个独立的聚类 [ (1,7) ]、[ (-8,6) ]、[ (-9,4) ] 、[ (4, -2) ]:
![图 3.3:样本数据点的绘制]()
图 3.3:样本数据点的绘制
-
计算每一对点之间的欧几里得距离:
![图 3.4:点之间的距离]()
图 3.4:点之间的距离
-
从每个点出发,扩展一个邻域大小并形成簇。为了这个示例,假设我们通过了半径为三的邻域。这意味着任何两个点,如果它们之间的距离小于三,就会被视为邻居。点(-8,6)和(-9,4)现在是聚类的候选点。
-
没有邻居的点被标记为噪声,并且保持未聚类状态。点(1,7)和(4,-2)由于在聚类中无用,超出了我们的兴趣范围。
-
有邻居的点随后会被评估,看它们是否符合最小点数阈值。在这个示例中,如果我们设置最小点数阈值为二,那么点(-8,6)和(-9,4)就可以正式组合成一个簇。如果最小点数阈值为三,那么这个集合中的所有四个数据点将被视为多余的噪声。
-
在剩余的未访问数据点上重复此过程。
在这个过程结束时,你将获得整个数据集,所有数据要么被归类为簇的一部分,要么被视为无关的噪声。通过走完整个玩具示例,你可以发现,DBSCAN 的性能高度依赖于你事先选择的阈值超参数。这意味着你可能需要多次运行 DBSCAN,并尝试不同的超参数选项,以了解它们如何影响整体性能。
DBSCAN 的一个优点是,它摒弃了我们在 k 均值和以质心为中心的层次聚类实现中看到的质心概念。这个特性使得 DBSCAN 更适合处理复杂的数据集,因为大多数现实世界中的数据并不像干净的簇那样分布。
练习 9:评估邻域半径大小的影响
对于这个练习,我们将采用与之前示例中常见的方式相反的顺序,先查看 scikit-learn 中 DBSCAN 的封装实现,然后再自己实现。这是故意为之,以充分探讨不同邻域半径大小如何极大地影响 DBSCAN 的性能。
完成这个练习后,你将熟悉调整邻域半径大小如何影响 DBSCAN 的性能。理解这些 DBSCAN 的特点很重要,因为它们可以帮助你在未来通过高效地排查聚类算法问题来节省时间。
-
生成一些虚拟数据:
from sklearn.cluster import DBSCAN from sklearn.datasets import make_blobs import matplotlib.pyplot as plt %matplotlib inline # Generate a random cluster dataset to experiment on. X = coordinate points, #y = cluster labels (not needed) X, y = make_blobs(n_samples=1000, centers=8, n_features=2, random_state=800) # Visualize the data plt.scatter(X[:,0], X[:,1]) plt.show()输出结果如下:
![图 3.5:可视化的玩具数据示例]()
图 3.5:可视化的玩具数据示例
-
在为这个玩具问题绘制虚拟数据后,你将看到数据集有两个特征,并大约有七到八个簇。要使用 scikit-learn 实现 DBSCAN,你需要实例化一个新的 scikit-learn 类:
db = DBSCAN(eps=0.5, min_samples=10, metric='euclidean')我们的 DBSCAN 实例存储在
db变量中,超参数在创建时传入。为了这个例子,你可以看到邻域半径(eps)设置为 0.5,而最小点数设置为 10。为了与前几章一致,我们将继续使用欧几里得距离作为度量标准。 -
让我们设置一个循环,允许我们交互式地探索潜在的邻域半径大小选项:
eps = [0.2,0.7] for ep in eps: db = DBSCAN(eps=ep, min_samples=10, metric='euclidean') plt.scatter(X[:,0], X[:,1], c=db.fit_predict(X)) plt.title('Toy Problem with eps: ' + str(ep)) plt.show()上述代码产生以下两张图:

图:3.6: 结果图
从图中可以看到,设置邻域半径过小会导致所有数据被视为随机噪声(紫色点)。稍微增加邻域半径,可以让我们形成更有意义的簇。试着重新创建前面的图形并尝试不同的eps值。
DBSCAN 属性 - 邻域半径
在练习 9,评估邻域半径大小的影响中,你看到了设置合适的邻域半径对你的 DBSCAN 实现性能的影响。如果邻域太小,你会遇到所有数据都未被聚类的问题。如果你设置的邻域过大,那么所有数据会被聚集到一个簇中,且无法提供任何价值。如果你用自己的eps值进一步探索前面的练习,你可能会注意到,仅凭邻域大小很难得到理想的聚类效果。这时,最小点数阈值就显得非常重要。我们稍后会讨论这一主题。
为了更深入了解 DBSCAN 的邻域概念,我们来看一下你在实例化时传入的eps超参数。eps代表 epsilon,是算法在寻找邻居时查看的距离。这个 epsilon 值会被转换为一个半径,围绕任意给定数据点以圆形方式进行扫描,以形成邻域:

图 3.7: 邻域半径可视化,红色圆圈为邻域
在这个例子中,中心点将有四个邻居。
这里需要注意的一个关键点是,你的邻域搜索形成的形状在二维空间中是圆形,在三维空间中是球形。根据数据的结构,这可能会影响你模型的表现。再次强调,簇可能看起来像是一个直观的结构,但这并不总是如此。幸运的是,DBSCAN 在处理这种你感兴趣的簇,但又不符合明确的簇状结构时非常有效。

图 3.8: 不同邻域半径大小的影响
在左侧,数据点会被分类为随机噪声。右侧,数据点有多个邻居,可能会成为一个独立的簇。
活动 4:从头实现 DBSCAN
在面试中使用生成的二维数据集时,你被要求从头开始实现 DBSCAN 算法。为此,你需要编码邻域搜索的直觉,并进行递归调用以添加邻居。
根据你在前几章中学到的 DBSCAN 和距离度量,使用 Python 从头实现 DBSCAN。你可以自由使用 NumPy 和 SciPy 来评估距离。
完成此活动的步骤如下:
-
生成一个随机簇数据集
-
可视化数据
-
从头创建函数,允许你在数据集上调用 DBSCAN
-
使用你创建的 DBSCAN 实现来寻找生成数据集中的簇。可以根据需要调整超参数,并根据其表现进行调优。
-
可视化从头开始实现的 DBSCAN 聚类效果
本练习的预期结果是让你理解 DBSCAN 的工作原理,从而在使用 scikit-learn 的完整实现之前,能够从头开始实现 DBSCAN。采取这种方法来学习任何机器学习算法是很重要的,它有助于你“获得”使用更简单实现的能力,同时在未来仍能深入讨论 DBSCAN:

图 3.9:预期结果
注意
此活动的解决方案可以在第 316 页找到。
DBSCAN 属性 – 最小点数
除了邻域半径之外,DBSCAN 成功实现的另一个核心组件是需要的最小点数,以证明某个数据点属于某个簇。如前所述,当数据集较为稀疏时,这个下限能明显有利于你的算法。不过,当数据非常密集时,这个参数并非无用——虽然将单个数据点随机散布在特征空间中可以很容易地被归类为噪声,但当数据随机形成两到三个点的“斑块”时,问题就变得模糊了。例如,这些数据点是应该作为一个独立的簇,还是也应该归类为噪声?最小点数阈值帮助解决了这个问题。
在 scikit-learn 实现的 DBSCAN 中,此超参数在min_samples字段中设置,该字段会在创建 DBSCAN 实例时传递。此字段与邻域半径大小超参数配合使用时非常有价值,可以帮助完善基于密度的聚类方法:

图 3.10:最小点数阈值决定数据点是否为噪声或簇
在右侧,如果最小点数阈值为 10 个点,它会将该邻域中的数据分类为噪声。
在现实场景中,当你拥有大量数据时,最小点数将产生显著影响。以葡萄酒聚类为例,如果你的商店实际上是一个大型酒类仓库,你可能会有成千上万种葡萄酒,每种酒只有一两瓶,这些酒可能会被轻松视为自己的独立聚类。根据你的使用场景,这可能会有帮助;然而,重要的是要记住数据的主观大小。如果你的数据有数百万个数据点,那么随机噪声可能会被视为数百甚至数千个随机的单次销售。然而,如果你的数据量在几百或几千个数据点的规模上,单个数据点可能会被视为随机噪声。
练习 10:评估最小点数阈值的影响
类似于我们的练习 9,评估邻域半径大小的影响,我们探索了设置适当邻域半径大小的值,我们将重复这一练习,但这次会在多种数据集上更改最小点数阈值。
使用我们当前实现的 DBSCAN,我们可以轻松地调整最小点数阈值。调整这个超参数,并观察它在生成的数据上的表现。
通过调整 DBSCAN 的最小点数阈值,你将理解它如何影响聚类预测的质量。
再次,从随机生成的数据开始:
-
按如下方式生成随机聚类数据集:
from sklearn.cluster import DBSCAN from sklearn.datasets import make_blobs import matplotlib.pyplot as plt %matplotlib inline X, y = make_blobs(n_samples=1000, centers=8, n_features=2, random_state=800) -
按如下方式可视化数据:
# Visualize the data plt.scatter(X[:,0], X[:,1]) plt.show()![图 3.11:生成数据的绘图]()
图 3.11:生成数据的绘图
-
使用与之前相同的绘图数据,让我们从练习 1,评估邻域半径大小的影响中选择一个表现较好的邻域半径大小——
eps= 0.7:db = DBSCAN(eps=0.7, min_samples=10, metric='euclidean')注意
eps是一个可调的超参数。然而,正如前文所述,0.7 是来自之前实验的结果,因此我们选择eps = 0.7作为最优值。 -
在实例化 DBSCAN 聚类算法后,让我们将
min_samples超参数视为我们希望调整的变量。我们可以通过循环来查找最适合我们使用场景的最小点数:num_samples = [10,19,20] for min_num in num_samples: db = DBSCAN(eps=0.7, min_samples=min_num, metric='euclidean') plt.scatter(X[:,0], X[:,1], c=db.fit_predict(X)) plt.title('Toy Problem with Minimum Points: ' + str(min_num)) plt.show()观察生成的第一个图,我们可以看到,如果你按照练习 1,评估邻域半径大小的影响,使用 10 个最小点作为聚类成员资格的阈值,你将达到的结果:

图 3.12:具有 10 个最小点的玩具问题绘图
剩下的两个超参数选项对 DBSCAN 聚类算法的性能有很大影响,并展示了单一数值的变化如何显著影响性能:

图 3.13:玩具问题的绘图
正如你所看到的,仅仅将最小点数从 19 改为 20,就为我们的特征空间添加了一个额外的(错误的!)聚类。通过这次练习,你已了解最小点数的概念,现在你可以调整 scikit-learn 实现中的 epsilon 值和最小点数阈值,以获得最优的聚类数。
注意
在我们最初生成数据时,我们创建了八个聚类。这表明,最小点数的小变化可以添加整个新的聚类,而这些聚类显然不应出现在数据中。
活动 5:将 DBSCAN 与 k-means 和层次聚类进行比较
你正在管理商店库存,收到了一大批葡萄酒货物,但在运输过程中瓶子的品牌标签掉落了。幸运的是,供应商提供了每瓶葡萄酒的化学成分数据以及各自的序列号。不幸的是,你不能打开每瓶酒并品尝其差异——你必须找到一种方法,根据化学成分将未标记的酒瓶重新分组!你从订单列表中得知,订购了三种不同类型的葡萄酒,并且只提供了两个酒的属性来将这些葡萄酒分组。
在第二章、层次聚类中,我们已经看到 k-means 和层次聚类如何在葡萄酒数据集上表现。在我们的最佳情况下,得到了 0.59 的轮廓系数。现在,使用 scikit-learn 实现的 DBSCAN,让我们看看能否获得更好的聚类结果。
以下步骤将帮助你完成此活动:
-
导入必要的包
-
加载葡萄酒数据集并检查数据的结构
-
可视化数据
-
使用 k-means、凝聚层次聚类和 DBSCAN 生成聚类
-
评估不同的 DBSCAN 超参数选项及其对轮廓系数的影响
-
基于最高的轮廓系数生成最终的聚类
-
可视化使用三种方法生成的聚类
注意
我们已从
archive.ics.uci.edu/ml/datasets/wine下载了此数据集。你可以通过github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson03/Activity05访问该数据集。UCI 机器学习库[
archive.ics.uci.edu/ml]。加利福尼亚州尔湾:加利福尼亚大学信息与计算机科学学院。
完成此活动后,你将重新创建一个聚类问题的完整工作流。你已经在第二章、层次聚类中熟悉了数据,并且在完成此活动后,你将执行模型选择,找到最适合你数据集的最佳模型和超参数。你将得到葡萄酒数据集在每种聚类方法下的轮廓系数。
注意
该活动的解决方案可以在第 319 页找到。
DBSCAN 与 k-means 和层次聚类的比较
现在你已经了解了 DBSCAN 是如何实现的,以及可以调整哪些不同的超参数来优化性能,我们来看看它与我们在第一章,聚类简介和第二章,层次聚类中介绍的聚类方法的比较。
你可能在活动 5,将 DBSCAN 与 k-means 和层次聚类进行比较中注意到,DBSCAN 在通过轮廓分数寻找最优簇时可能有些挑剔。这是邻域方法的一个缺点——当你对数据中的簇的数量有一些了解时,k-means 和层次聚类表现得更为出色。在大多数情况下,这个数量足够小,你可以迭代地尝试几个不同的数量,看看效果如何。相反,DBSCAN 采用更自下而上的方法,通过调整超参数来发现它认为重要的簇。在实践中,当前两种方法失败时,考虑使用 DBSCAN 是很有帮助的,因为它需要大量的调整才能正常工作。话虽如此,当 DBSCAN 实现正常工作时,它通常会远远超越 k-means 和层次聚类。(实际上,这种情况通常发生在高度交织但仍然离散的数据上,比如包含两个半月形状的特征空间)。
与 k-means 和层次聚类相比,DBSCAN 可能更加高效,因为它只需要对每个数据点进行一次检查。与需要多次迭代寻找新质心并评估其最近邻的位置不同,一旦一个点被分配到 DBSCAN 中的一个簇,它的簇成员就不会再改变。DBSCAN 与层次聚类相比,与 k-means 的另一个关键区别是,它不需要在创建时明确指定预期的簇的数量。这在没有外部指导如何将数据集拆分时非常有用。
总结
DBSCAN 在聚类方法上采取了与 k-means 和层次聚类不同的有趣方式。虽然层次聚类在某些方面可以看作是 k-means 中最近邻方法的扩展,但 DBSCAN 通过应用密度的概念来处理查找邻居的问题。当数据非常复杂且交织在一起时,这种方法尤为有用。虽然 DBSCAN 非常强大,但并不是万无一失的,并且根据原始数据的情况,它有时可能会显得过于复杂。
然而,将 DBSCAN 与 k-means 和层次聚类结合使用时,DBSCAN 为无监督学习任务中的数据聚类提供了强大的工具箱。在遇到这类问题时,比较每种方法的性能并找出最合适的方案是值得的。
在探索完聚类之后,我们将进入无监督学习中另一个关键的技能:降维。通过智能地减少维度,我们可以让聚类变得更易理解,并能够向利益相关者传达。降维对于以最有效的方式创建各种机器学习模型也是至关重要的。
第四章:降维和 PCA
学习目标
到本章结束时,您将能够:
-
应用降维技术。
-
描述主成分和降维背后的概念。
-
在使用 scikit-learn 解决问题时应用主成分分析(PCA)。
-
比较手动 PCA 与 scikit-learn 的 PCA。
本章将探讨降维及其不同的降维技术。
介绍
本章是三章系列中的第一章,探讨我们在无监督学习算法中使用不同特征集(或空间)的应用,我们将从降维的讨论开始,特别是主成分分析(PCA)。接下来,我们将通过探索两种独立且强大的机器学习架构——基于神经网络的自编码器,扩展我们对不同特征空间好处的理解。神经网络在监督学习问题中无疑有着应得的声誉,而通过使用自编码器阶段,它们已被证明在无监督学习问题的应用上足够灵活。最后,在本微系列的最后一章中,我们将基于神经网络实现和降维的基础,讨论 t 分布最近邻算法。
什么是降维?
降维是数据科学家工具包中的一个重要工具,并且由于其广泛的应用场景,它在该领域几乎被视为基本知识。因此,在我们考虑降维及其必要性之前,我们首先需要理解什么是维度。简单来说,维度是与数据样本相关的维度、特征或变量的数量。通常,可以将其视为电子表格中的列数,其中每个样本占据一行,每一列描述样本的某个属性。以下表格就是一个例子:

图 4.1:具有三个不同特征的两个数据样本
在图 4.1中,我们有两个数据样本,每个样本有三个独立的特征或维度。根据所解决的问题或数据集的来源,我们可能希望在不丢失已提供信息的情况下,减少每个样本的维度数量。这就是降维发挥作用的地方。
但是,降维究竟如何帮助我们解决问题呢?我们将在接下来的部分更详细地介绍应用;但假设我们有一个非常大的时间序列数据集,例如回声心动图或心电图(在一些国家也称为 EKG)信号,如下图所示:

图 4.2:心电图(ECG 或 EKG)
这些信号是从您公司新型号的手表中捕获的,我们需要寻找心脏病或中风的迹象。在查看数据集时,我们可以做出以下几项观察:
-
大多数单独的心跳信号非常相似。
-
数据中存在来自录音系统或患者在录音过程中移动的噪音。
-
尽管有噪音,心跳信号仍然可见。
-
数据量非常大——超出了手表可用硬件的处理能力。
正是在这种情况下,降维技术真正显示其优势!通过使用降维技术,我们能够从信号中去除大量噪音,这反过来将有助于提高应用于数据的算法性能,并减少数据集的大小,从而降低硬件要求。本章中我们将讨论的技术,特别是 PCA 和自编码器,在研究和行业中已被广泛应用于有效地处理、聚类和分类这类数据集。在本章结束时,您将能够将这些技术应用于您自己的数据,并希望看到您自己机器学习系统性能的提升。
降维的应用
在开始详细研究降维和 PCA 之前,我们将讨论这些技术的一些常见应用:
-
预处理/特征工程:这种方法最常见的应用是在机器学习解决方案开发的预处理或特征工程阶段。在算法开发过程中提供的信息质量,以及输入数据与期望结果之间的相关性,对于设计高性能的解决方案至关重要。在这种情况下,PCA 可以提供帮助,因为我们能够从数据中提取出最重要的信息成分,并将其提供给模型,从而确保只提供最相关的信息。这还可以带来第二个好处,即我们减少了提供给模型的特征数量,因此计算量也能相应减少,这可以减少系统的整体训练时间。
-
噪音减少:降维也可以作为一种有效的噪音减少/滤波技术。预期信号或数据集中的噪音并不占数据变异的主要成分。因此,我们可以通过去除变异较小的成分来去除信号中的一部分噪音,然后将数据恢复到原始数据空间。在以下示例中,左侧的图像已经过滤掉了最重要的 20 个数据源,生成了右侧的图像。我们可以看到图像质量有所降低,但关键信息仍然保留:

图 4.3:经过维度减少滤波的图像。左:原始图像(照片由来自 Pexels 的 Arthur Brognoli 拍摄),右:滤波后的图像
注意
这张照片是由来自 Pexels 的 Arthur Brognoli 拍摄,并可在www.pexels.com/photo-license/上免费使用。
-
生成可信的人工数据集:由于 PCA 将数据集分解为信息(或变化)的组件,我们可以通过调整特征值之间的比率来研究每个组件的效果或生成新的数据集样本。我们可以缩放这些组件,从而增加或减少特定组件的重要性。这也被称为统计形状建模,因为其中一种常见方法是使用它来创建形状的合理变体。它还被用来在图像中检测面部特征点,这是主动形状建模过程中的一个步骤。
-
金融建模/风险分析:降维为金融行业提供了一个有用的工具箱,因为能够将大量单独的市场指标或信号整合为较少的组件,可以加快和更高效地进行计算。同样,这些组件可以用来突出那些高风险的产品/公司。
维数灾难
在我们理解使用降维技术的好处之前,我们必须先了解为什么需要减少特征集的维度。维数灾难是一个常用的术语,用来描述在处理具有高维度特征空间的数据时出现的问题;例如,为每个样本收集的属性数量。考虑一个《吃豆人》游戏中点位置的数据集。你的角色吃豆人在虚拟世界中的位置由两个维度或坐标(x,y)定义。假设我们正在创建一个新的电脑敌人:一个由 AI 驱动的幽灵来对抗玩家,并且它需要一些关于我们角色的信息来做出自己的游戏逻辑决策。为了使这个机器人有效,我们需要玩家的位置(x,y)以及每个方向上的速度(vx,vy),此外还需要玩家最后五个(x,y)位置,剩余的心数以及迷宫中剩余的能量豆数(能量豆暂时允许吃豆人吃掉幽灵)。现在,对于每个时间点,我们的机器人需要 16 个单独的特征(或维度)来做出决策。显然,这比只提供位置的两个维度要多得多。

图 4.4:《吃豆人》游戏中的维度
为了解释降维的概念,我们将考虑一个虚构的数据集(见图 4.5),其中 x 和 y 坐标作为特征,形成了特征空间中的两个维度。需要注意的是,这个例子绝不是数学证明,而是旨在提供一种可视化增加维度后影响的方式。在这个数据集中,我们有六个独立的样本(或点),我们可以可视化当前在特征空间内占据的体积,约为 (3 - 1) x (4 - 2) = 2 x 2 = 4 平方单位。

图 4.5:二维特征空间中的数据
假设数据集包含相同数量的点,但每个样本都有一个额外的特征(z 坐标)。此时,所占数据体积大约为 2 x 2 x 2 = 8 立方单位。因此,我们现在有相同数量的样本,但包围数据集的空间变得更大。这样,数据在可用空间中占据的相对体积变小,数据变得更加稀疏。这就是维度灾难;随着可用特征数量的增加,数据的稀疏性增加,从而使得统计上有效的相关性变得更加困难。回到我们创建视频游戏机器人来与人类玩家对战的例子,我们有 12 个特征,包含不同类型的特征:速度、速度变化、加速度、技能水平、选择的武器和可用弹药。根据这些特征的可能值范围以及每个特征对数据集方差的贡献,数据可能非常稀疏。即使在受限的吃豆人世界中,每个特征的潜在方差也可能非常大,有些特征的方差远大于其他特征。
因此,在不处理数据集稀疏性的情况下,我们通过额外的特征获得了更多信息,但可能无法提高机器学习模型的性能,因为统计相关性变得更加困难。我们希望做的是保留额外特征提供的有用信息,同时最小化稀疏性的负面影响。这正是降维技术的设计目的,而这些技术在提高机器学习模型性能方面可能非常强大。
本章将讨论多种降维技术,并将在更详细的工作示例中介绍其中一种最重要和有用的方法——主成分分析(PCA)。
降维技术概述
正如在引言部分所讨论的,任何降维技术的目标都是在保持提供的有用信息的同时管理数据集的稀疏性,因此降维通常是分类阶段前的一个重要预处理步骤。大多数降维技术旨在通过特征投影的过程来完成这一任务,将数据从高维空间调整到较低维度的空间,以去除数据的稀疏性。再次通过可视化投影过程来理解这一点,可以考虑在三维空间中的一个球体。我们可以将球体投影到二维空间,变成一个圆形,虽然会有一些信息丢失(z 坐标的值),但仍保留了描述其原始形状的大部分信息。我们仍然知道原点、半径和流形(轮廓),而且仍然非常清楚它是一个圆。因此,如果我们仅仅得到了二维投影,凭借这些信息也能够重新构建原始的三维形状。所以,根据我们尝试解决的问题,我们可能已经在保留重要信息的同时减少了维度:

图 4.6:将一个三维球体投影到二维空间
通过在降维阶段对数据集进行预处理,能够获得的附加好处是提高的计算性能。由于数据已经被投影到较低维度的空间,它将包含更少但可能更强大的特征。特征较少意味着在后续的分类或回归阶段,处理的数据集的大小显著较小。这将可能减少分类/回归所需的系统资源和处理时间,在某些情况下,降维技术还可以直接用于完成分析。
这个类比还引入了降维的一个重要考虑因素。我们总是试图在将数据投影到低维空间时,平衡信息丢失和减少数据稀疏性之间的关系。根据问题的性质和使用的数据集,正确的平衡可能会自然出现,并且相对直接。在某些应用中,这个决策可能依赖于额外验证方法的结果,比如交叉验证(特别是在监督学习问题中)或领域专家的评估。
我们喜欢将降维中的这一权衡方式比作在计算机上传输文件或图像时的压缩过程。降维技术,如 PCA,实质上是将信息压缩成较小的大小以便传输,而在许多压缩方法中,压缩过程中会发生一些信息丢失。有时,这些丢失是可以接受的;例如,如果我们要传输一张 50MB 的图像并需要将其压缩到 5MB,我们可以预期仍然能够看到图像的主要内容,但一些较小的背景细节可能会变得模糊不清。我们也不会期望从压缩后的图像恢复出完全无损的原始图像,但可以期望在恢复时会出现一些附加的伪影,比如模糊。
降维与无监督学习
降维技术在机器学习中有许多用途,因为能够提取数据集中的有用信息可以在许多机器学习问题中提高性能。与监督学习方法不同,降维技术在无监督学习中尤其有用,因为数据集不包含任何实际标签或目标。无监督学习中,训练环境用于以适合问题解决的方式组织数据(例如,分类问题中的聚类),这种组织方式通常基于数据集中的最重要信息。降维提供了提取重要信息的有效手段,且由于我们可以使用多种方法,因此回顾一些可用选项是有益的:
-
线性判别分析(LDA):这是一种非常实用的技术,既可以用于分类,也可以用于降维。LDA 将在第七章中更详细地讲解:主题建模。
-
非负矩阵分解(NNMF):与许多降维技术一样,这种方法依赖于线性代数的性质来减少数据集中的特征数量。NNMF 也将在第七章,主题建模中进行更详细的讨论。
-
奇异值分解(SVD):这与 PCA(本章中将详细讨论)有些相关,也是一个矩阵分解过程,与 NNMF 并无太大不同。
-
独立成分分析(ICA):这与 SVD 和 PCA 有一些相似之处,但通过放宽数据为高斯分布的假设,可以实现非高斯数据的分离。
到目前为止描述的每种方法都使用线性分离来减少数据在其原始实现中的稀疏性。一些方法还有使用非线性核函数的变体,能够以非线性的方式减少稀疏性。根据所使用的数据集,非线性核可能在从信号中提取最有用的信息方面更为有效。
主成分分析(PCA)
如前所述,PCA 是一种常用且非常有效的降维技术,通常作为许多机器学习模型和技术的预处理阶段。因此,我们将在本书中专门花一章更详细地探讨 PCA,超越其他方法。PCA 通过将数据分解为一系列组件来减少数据的稀疏性,每个组件代表数据中的一个信息源。顾名思义,PCA 中产生的第一个组件,主成分,包含了数据中大部分的信息或方差。主成分通常可以被认为是除了均值之外,贡献最多有趣信息的部分。随着每个后续组件的加入,数据中传递的信息减少,但更加微妙。如果我们将所有这些组件都考虑在内,使用 PCA 将没有任何好处,因为它将恢复原始数据集。为了澄清这个过程以及 PCA 返回的信息,我们将使用一个实际的例子,通过手动完成 PCA 计算。但首先,我们需要回顾一些基础的统计学概念,这些概念是进行 PCA 计算所必需的。
均值
均值,或称平均值,简单来说就是将所有值相加后,除以数据集中值的数量。
标准差
协方差矩阵通常被称为数据的分布,与方差相关,标准差是衡量数据与均值的接近程度的指标。在正态分布的数据集中,大约 68%的数据位于均值的一个标准差范围内。
方差与标准差之间的关系相当简单——方差是标准差的平方。
协方差
当标准差或方差是计算单一维度数据的分布时,协方差是一个维度(或特征)与另一个维度的方差。当一个维度的协方差与其自身计算时,结果与仅计算该维度的方差相同。
协方差矩阵
协方差矩阵是可以计算数据集协方差值的矩阵表示。除了在数据探索中非常有用外,协方差矩阵在执行 PCA(主成分分析)时也是必需的。为了确定一个特征相对于另一个特征的方差,我们只需查找协方差矩阵中对应的值。参见图 4.7,我们可以看到,在第 1 列、第 2 行,值是特征或数据集Y相对于X的方差(cov(Y, X))。我们还可以看到,有一列协方差值是针对同一特征或数据集计算的;例如,cov(X, X)。在这种情况下,值就是X的方差。

图 4.7:协方差矩阵
通常,每个协方差的具体数值并不像观察矩阵中每个协方差的大小和相对大小那样有趣。某个特征与另一个特征的协方差较大,意味着一个特征与另一个特征有显著的变化,而接近零的值则表示变化极小。另一个值得关注的协方差特性是其符号;正值表示当一个特征增加或减少时,另一个特征也随之增加或减少,而负协方差则表示两个特征相互背离,一个增加时另一个减少,反之亦然。
值得庆幸的是,numpy和scipy提供了高效的函数来为你完成这些计算。在下一个练习中,我们将使用 Python 来计算这些值。
练习 11:理解统计学基础概念
在本练习中,我们将简要回顾如何使用numpy和pandas这两个 Python 包来计算一些基础的统计概念。在本练习中,我们将使用一个包含不同鸢尾花物种测量数据集,该数据集由英国生物学家和统计学家罗纳德·费舍尔爵士于 1936 年创建。该数据集可以在随附的源代码中找到,包含了三种不同鸢尾花品种(鸢尾花 Setosa、鸢尾花 Versicolor 和鸢尾花 Virginica)的四个独立测量值(花萼宽度和长度,花瓣宽度和长度)。
注意
该数据集来自 archive.ics.uci.edu/ml/machine-learning-databases/iris/。它可以从 github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson04/Exercise11 下载。
UCI 机器学习库 [archive.ics.uci.edu/ml]。加利福尼亚州欧文市:加利福尼亚大学信息与计算机科学学院。
执行的步骤如下:
-
导入
pandas、numpy和matplotlib包以供使用:import pandas as pd import numpy as np import matplotlib.pyplot as plt -
加载数据集并预览前五行数据:
df = pd.read_csv('iris-data.csv') df.head()输出如下:
![图 4.8:数据的头部]()
图 4.8:数据的头部
-
我们只需要
Sepal Length和Sepal Width特征,因此删除其他列:df = df[['Sepal Length', 'Sepal Width']] df.head()输出如下:
![图 4.9:清洗后的数据头部]()
图 4.9:清洗后的数据头部
-
通过绘制
Sepal Length与Sepal Width的值来可视化数据集:plt.figure(figsize=(10, 7)) plt.scatter(df['Sepal Length'], df['Sepal Width']); plt.xlabel('Sepal Length (mm)'); plt.ylabel('Sepal Width (mm)'); plt.title('Sepal Length versus Width');输出如下:
![图 4.10:数据的图示]()
图 4.10:数据的图示
-
使用
pandas方法计算均值:df.mean()输出如下:
Sepal Length 5.843333 Sepal Width 3.054000 dtype: float64 -
使用
numpy方法计算均值:np.mean(df.values, axis=0)输出如下:
array([5.84333333, 3.054 ]) -
使用
pandas方法计算标准差值:df.std()输出如下:
Sepal Length 0.828066 Sepal Width 0.433594 dtype: float64 -
使用
numpy方法计算标准差值:np.std(df.values, axis=0)输出如下:
array([0.82530129, 0.43214658]) -
使用
pandas方法计算方差值:df.var()输出如下:
Sepal Length 0.685694 Sepal Width 0.188004 dtype: float64 -
使用
numpy方法计算方差值:np.var(df.values, axis=0)输出如下:
array([0.68112222, 0.18675067]) -
使用
pandas方法计算协方差矩阵:df.cov()输出如下:
![图 4.11:使用 Pandas 方法的协方差矩阵]()
图 4.11:使用 Pandas 方法的协方差矩阵
-
使用
numpy方法计算协方差矩阵:np.cov(df.values.T)输出如下:

图 4.12:使用 NumPy 方法的协方差矩阵
现在我们知道如何计算基础的统计值,接下来我们将重点讨论 PCA 的其他组成部分。
特征值和特征向量
特征值和特征向量的数学概念在物理学和工程学领域中非常重要,它们也是计算数据集主成分的最后步骤。特征值和特征向量的精确定义超出了本书的范围,因为它涉及较为复杂的内容,并且需要有一定的线性代数基础。将数据集 (a) 分解为特征值 (S) 和特征向量 (U) 的线性代数方程如下:

图 4.13:特征向量/特征值分解
在 图 4.13 中,U 和 V 作为数据集 a 的左右值相关。如果 a 的形状为 m x n,则 U 将包含形状为 m x m 的值,V 的形状为 n x n。
简而言之,在 PCA 的上下文中:
-
特征向量 (U) 是对数据集做出信息贡献的成分,如本节第一段所述的主成分。每个特征向量描述了数据集中的某种变异性。
-
特征值 (S) 是描述每个特征向量对数据集贡献多少的单独数值。如我们之前所述,描述最大贡献的信号特征向量称为主成分,因此它将具有最大的特征值。因此,具有最小特征值的特征向量对数据的方差或信息贡献最少。
练习 12:计算特征值和特征向量
如我们之前所讨论的,手动推导和计算特征值及特征向量稍显复杂,并且超出了本书的范围。幸运的是,numpy 为我们提供了计算这些值的所有功能。再次说明,我们将使用 Iris 数据集作为示例:
注意
该数据集来自 archive.ics.uci.edu/ml/machine-learning-databases/iris/。
可以从 github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson04/Exercise12 下载。
UCI 机器学习库 [archive.ics.uci.edu/ml]。加利福尼亚州尔湾:加利福尼亚大学信息与计算机科学学院。
-
导入
pandas和numpy包:import pandas as pd import numpy as np -
加载数据集:
df = pd.read_csv('iris-data.csv') df.head()输出如下所示:
![图 4.14: 数据集的前五行]()
图 4.14: 数据集的前五行
-
同样,我们只需要
花萼长度和花萼宽度特征,因此删除其他列:df = df[['Sepal Length', 'Sepal Width']] df.head()输出如下所示:
![图 4.15: 花萼长度和花萼宽度特征]()
图 4.15: 花萼长度和花萼宽度特征
-
从 NumPy 的线性代数模块中,使用单值分解函数来计算
特征值和特征向量:eigenvectors, eigenvalues, _ = np.linalg.svd(df.values, full_matrices=False)注意
使用
full_matrices=False函数参数是一个标志,表示函数返回我们需要形状的特征向量;即:# 样本 x # 特征。 -
观察特征值,我们可以看到第一个值是最大的,因此第一个特征向量贡献了最多的信息:
eigenvalues输出如下所示:
array([81.25483015, 6.96796793]) -
观察特征值作为数据集总方差的百分比非常方便。我们将使用累积和函数来实现这一点:
eigenvalues = np.cumsum(eigenvalues) eigenvalues输出如下所示:
array([81.25483015, 88.22279808]) -
除以最后一个或最大值来转换为百分比:
eigenvalues /= eigenvalues.max() eigenvalues输出如下所示:
array([0.92101851, 1\. ])我们可以看到,这里第一个(或主)成分包含了数据中 92%的变化量,因此包含了大部分信息。
-
现在,让我们看看
特征向量:eigenvectors输出的一部分如下所示:
![]()
图 4.16: 特征向量
-
确认特征向量矩阵的形状为
# 样本 x # 特征;即,150x2:eigenvectors.shape输出如下所示:
(150, 2) -
因此,从特征值中我们可以看出,第一个特征向量是主成分。看看第一个特征向量的值:
P = eigenvectors[0] P输出如下:
array([-0.07553027, -0.11068158])
我们已经将数据集分解为主成分,并且利用特征向量,我们可以进一步减少可用数据的维度。在后续的示例中,我们将考虑 PCA 并将该技术应用于示例数据集。
PCA 的过程
现在,我们已经准备好所有步骤来完成 PCA,减少数据集的维度。
完成 PCA 的总体算法如下:
-
导入所需的 Python 包(
numpy和pandas)。 -
加载整个数据集。
-
从可用数据中选择你希望用于降维的特征。
注意
如果数据集的特征之间存在显著的尺度差异,例如,一个特征的值范围在 0 到 1 之间,而另一个在 100 到 1,000 之间,你可能需要对其中一个特征进行归一化,因为这种量级差异会消除较小特征的影响。在这种情况下,你可能需要将较大特征除以其最大值。
举个例子,看看这个:
x1 = [0.1, 0.23, 0.54, 0.76, 0.78]x2 = [121, 125, 167, 104, 192]x2 = x2 / np.max(x2) # 将 x2 归一化到 0 和 1 之间 -
计算所选(并可能已归一化)数据的
协方差矩阵。 -
计算
协方差矩阵的特征值和特征向量。 -
按从高到低的顺序对特征值(及其对应的特征向量)进行排序。
-
计算特征值在数据集总方差中的百分比。
-
选择所需的特征值(及其对应的特征向量)数量,以组成一个预定的最小组成方差值。
注意
在这一阶段,排序后的特征值表示数据集总方差的百分比。因此,我们可以利用这些值来选择所需的特征向量的数量,无论是为了解决问题,还是为了充分减少应用于模型的数据集的大小。例如,假设我们要求 PCA 输出中至少包含 90%的方差。那么,我们将选择那些包含至少 90%方差的特征值(及其对应的特征向量)的数量。
-
将数据集与选定的特征向量相乘,你就完成了 PCA,减少了表示数据的特征数量。
-
绘制结果。
在进行下一个练习之前,请注意,转置是线性代数中的一个术语,意思是将行和列互换。假设我们有一个矩阵
,那么 X 的转置将是
。
练习 13:手动执行 PCA
在这个练习中,我们将手动完成主成分分析(PCA),再次使用鸢尾花数据集。在这个例子中,我们希望将数据集中的维度数减少到足以包含至少 75% 的可用方差:
注意
此数据集取自 archive.ics.uci.edu/ml/machine-learning-databases/iris/。可以从 github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson04/Exercise13 下载。
UCI 机器学习库 [archive.ics.uci.edu/ml]。加利福尼亚州欧文:加利福尼亚大学信息与计算机科学学院。
-
导入
pandas和numpy包:import pandas as pd import numpy as np import matplotlib.pyplot as plt -
加载数据集:
df = pd.read_csv('iris-data.csv') df.head()输出如下:
![图 4.17:数据集的前五行]()
图 4.17:数据集的前五行
-
再次,我们只需要
花萼长度和花萼宽度特征,因此去除其他列。在这个例子中,我们没有对所选数据集进行归一化:df = df[['Sepal Length', 'Sepal Width']] df.head()输出如下:
![图 4.18:花萼长度和花萼宽度特征]()
图 4.18:花萼长度和花萼宽度特征
-
计算所选数据的
协方差矩阵。请注意,我们需要对协方差矩阵进行转置,以确保它基于特征数(2)而不是样本数(150):data = np.cov(df.values.T) # The transpose is required to ensure the covariance matrix is #based on features, not samples data data输出如下:
![]()
图 4.19:所选数据的协方差矩阵
-
计算协方差矩阵的特征向量和特征值。再次使用
full_matrices函数参数:eigenvectors, eigenvalues, _ = np.linalg.svd(data, full_matrices=False) -
特征值是什么?这些特征值按从高到低的顺序返回:
eigenvalues输出如下:
array([0.6887728 , 0.18492474]) -
对应的特征向量是什么?
eigenvectors输出如下:
![]()
图 4.20:特征向量
-
计算特征值作为数据集中方差的百分比:
eigenvalues = np.cumsum(eigenvalues) eigenvalues /= eigenvalues.max() eigenvalues输出如下:
array([0.78834238, 1\. ]) -
根据练习介绍,我们需要描述至少包含 75% 可用方差的数据。根据步骤 7,主成分包含 78% 的可用方差。因此,我们只需要数据集中的主成分。主成分是什么?
P = eigenvectors[0] P输出如下:
array([-0.99693955, 0.07817635])现在,我们可以应用降维过程。执行主成分与数据集转置矩阵的矩阵乘法。
注意
降维过程是所选特征向量与待转换数据的矩阵乘法。
-
如果不对
df.values矩阵进行转置,就无法进行矩阵乘法:x_t_p = P.dot(df.values.T) x_t_p输出的一部分如下:
![]()
图 4.21:矩阵乘法结果
注意
为了执行矩阵乘法,数据集的转置是必需的,因为矩阵的内维必须相同才能进行矩阵乘法。为了使 A.dot(B) 有效,A 必须具有 m x n 的形状,B 必须具有 n x p 的形状。在本例中,A 和 B 的内维都是 n。
在以下示例中,PCA 的输出是一个单列、150 个样本的数据集。因此,我们只是将初始数据集的大小减少了一半,包含了数据中约 79% 的方差:
![图 4.22: PCA 输出结果]()
图 4.22: PCA 输出结果
-
绘制主成分的值:
plt.figure(figsize=(10, 7)) plt.plot(x_t_p); plt.title('Principal Component of Selected Iris Dataset'); plt.xlabel('Sample'); plt.ylabel('Component Value');输出结果如下:

图 4.23: 使用手动 PCA 转换后的 Iris 数据集
在本次练习中,我们简单地计算了数据集的协方差矩阵,而没有对数据集进行任何预处理。如果两个特征的均值和标准差大致相同,这是完全可以接受的。然而,如果一个特征的值远大于另一个特征(并且均值也有所不同),那么在分解为主成分时,这个特征可能会主导另一个特征,从而可能会完全丧失较小特征所提供的信息。在计算协方差矩阵之前,一种简单的归一化方法是从特征中减去各自的均值,从而使数据集围绕零进行中心化。我们将在练习 15,通过手动 PCA 可视化方差减少中演示这一过程。
练习 14: Scikit-Learn PCA
通常情况下,我们不会手动完成 PCA,尤其是当 scikit-learn 提供了一个优化的 API,并且它的便捷方法能让我们轻松地将数据转换到低维空间并返回时。在本次练习中,我们将更详细地研究如何在 Iris 数据集上使用 scikit-learn 的 PCA:
注意
该数据集来自 archive.ics.uci.edu/ml/machine-learning-databases/iris/。
数据集可以从 github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson04/Exercise14 下载。
UCI 机器学习库 [archive.ics.uci.edu/ml]。加利福尼亚州尔湾市:加利福尼亚大学信息与计算机科学学院。
-
从
sklearn包中导入pandas、numpy和PCA模块:import pandas as pd import numpy as np import matplotlib.pyplot as plt from sklearn.decomposition import PCA -
加载数据集:
df = pd.read_csv('iris-data.csv') df.head()输出结果如下:
![图 4.24: 数据集的前五行]()
图 4.24: 数据集的前五行
-
同样,我们只需要
花萼长度和花萼宽度两个特征,因此需要删除其他列。在这个示例中,我们没有对选定的数据集进行归一化处理:df = df[['Sepal Length', 'Sepal Width']] df.head()输出如下:
![图 4.25:花萼长度和花萼宽度特征]()
图 4.25:花萼长度和花萼宽度特征
-
将数据拟合到 scikit-learn 的 PCA 模型上,使用协方差数据。使用默认值,就像我们这里所做的那样,会生成该数据集可能的最大特征值和特征向量数量:
model = PCA() model.fit(df.values)输出如下:
![图 4.26:将数据拟合到 PCA 模型]()
图 4.26:将数据拟合到 PCA 模型
在这里,
copy表示数据在模型内的拟合会在应用任何计算之前进行复制。iterated_power显示花萼长度和花萼宽度特征是保留的主成分数量。默认值为None,它会选择组件数量为样本数或特征数中的最小值减一。random_state允许用户为 SVD 求解器使用的随机数生成器指定种子。svd_solver指定在 PCA 过程中使用的 SVD 求解器。tol是 SVD 求解器使用的容差值。通过whiten,组件向量会乘以样本数的平方根。这将删除一些信息,但可以改善某些后续估计器的性能。 -
成分(特征值)所描述的方差百分比包含在
explained_variance_ratio_属性中。显示explained_variance_ratio_的值:model.explained_variance_ratio_输出如下:
array([0.78834238, 0.21165762]) -
通过
components_属性显示特征向量:model.components_输出如下:
![图 4.27:特征向量]()
图 4.27:特征向量
-
在这个练习中,我们将再次只使用主要成分,因此我们将创建一个新的
PCA模型,这次指定成分(特征向量/特征值)的数量为1:model = PCA(n_components=1) -
使用
fit方法将covariance矩阵拟合到PCA模型,并生成相应的特征值/特征向量:model.fit(df.values)![图 4.28:特征值和特征向量的最大数量]()
图 4.28:特征值和特征向量的最大数量
使用多个默认参数拟合模型,具体参数如前面的输出所示。
copy = True是提供给fit方法的数据,该数据在应用 PCA 之前会被复制。iterated_power='auto'用于定义内部 SVD 求解器的迭代次数。n_components=1指定 PCA 模型只返回主成分。random_state=None指定需要时内部 SVD 求解器使用的随机数生成器。svd_solver='auto'是使用的 SVD 求解器类型。tol=0.0是 SVD 求解器认为已收敛的容差值。whiten=False指定不修改特征向量。如果设置为True,白化会进一步通过乘以样本数量的平方根并除以奇异值来修改成分。这可以帮助改善后续算法步骤的性能。通常,除了组件数量(
n_components)之外,你无需担心调整任何这些参数。例如,你可以将n_components传递给fit方法,如model.fit(data, n_components=2)。 -
使用
components_属性显示特征向量:model.components_输出如下:
array([[ 0.99693955, -0.07817635]]) -
使用模型的
fit_transform方法将鸢尾花数据集转换到低维空间。将转换后的值赋给data_t变量。data_t = model.fit_transform(df.values) -
绘制转换后的值以可视化结果:
plt.figure(figsize=(10, 7)) plt.plot(data_t); plt.xlabel('Sample'); plt.ylabel('Transformed Data'); plt.title('The dataset transformed by the principal component');输出如下:

图 4.29:使用 scikit-learn PCA 转换的鸢尾花数据集
恭喜你!你刚刚使用手动 PCA 以及 scikit-learn API 减少了鸢尾花数据集的维度。但在我们过早庆祝之前,比较图 4.23 和图 4.29;这两张图应该是相同的,对吧?我们使用了两种不同的方法在同一数据集上完成 PCA,并且都选择了主成分。在下一个活动中,我们将探讨为什么两者之间存在差异。
活动 6:手动 PCA 与 scikit-learn
假设你被要求将一个旧应用程序中手动执行 PCA 的遗留代码移植到一个新的使用 scikit-learn 的应用程序。在移植过程中,你注意到手动 PCA 的输出与移植后的输出之间存在一些差异。为什么手动 PCA 和 scikit-learn 之间会有输出差异?比较两种方法在鸢尾花数据集上的结果。它们之间有什么区别?
注意
该数据集来源于archive.ics.uci.edu/ml/machine-learning-databases/iris/。可以从github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson04/Activity06下载。
UCI 机器学习库 [archive.ics.uci.edu/ml]。加利福尼亚州尔湾:加利福尼亚大学信息与计算机科学学院。
-
导入
pandas、numpy和matplotlib绘图库以及 scikit-learn 的PCA模型。 -
加载数据集并按照之前的练习仅选择萼片特征。显示数据的前五行。
-
计算数据的
协方差矩阵。 -
使用 scikit-learn API 并仅使用第一个主成分转换数据。将转换后的数据存储在
sklearn_pca变量中。 -
使用手动 PCA 和仅第一个主成分转换数据。将转换后的数据存储在
manual_pca变量中。 -
在同一图表上绘制
sklearn_pca和manual_pca的值,以可视化它们的差异。 -
请注意,两个图表看起来几乎相同,但有一些关键的差异。这些差异是什么?
-
看看是否能够修改手动 PCA 过程的输出,使其与 scikit-learn 版本一致。
注意
提示:scikit-learn API 在转换前会减去数据的均值。
预期输出:在本活动结束时,你将使用手动 PCA 和 scikit-learn PCA 方法对数据集进行转化。你将生成一张图表,展示两个降维数据集实际上是相同的,并且你应该理解为什么它们最初看起来有很大的不同。最终图表应类似于以下内容:

图 4.30:预期的最终图表
该图将展示通过两种方法完成的降维实际上是相同的。
注意
本活动的解决方案可以在第 324 页找到。
恢复压缩的数据集
现在我们已经覆盖了一些不同的将数据集转化为低维空间的例子,我们应当考虑这种转化对数据产生了什么实际效果。将 PCA 作为预处理步骤来压缩数据中的特征数量,会导致部分方差被丢弃。以下练习将引导我们完成这一过程,帮助我们了解通过转化丢弃了多少信息。
练习 15:通过手动 PCA 可视化方差减少
降维的一个最重要的方面是理解由于降维过程,从数据集中移除了多少信息。移除过多的信息会给后续处理带来额外挑战,而移除的信息不足则会破坏 PCA 或其他技术的目的。在本练习中,我们将可视化 PCA 将 Iris 数据集移除的多少信息:
注意
该数据集来自于archive.ics.uci.edu/ml/machine-learning-databases/iris/。
它可以从github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson04/Exercise15下载。
UCI 机器学习库 [archive.ics.uci.edu/ml]。加利福尼亚大学欧文分校,信息与计算机科学学院。
-
导入
pandas、numpy和matplotlib绘图库:import pandas as pd import numpy as np import matplotlib.pyplot as plt -
从鸢尾花数据集读取
花萼特征:df = pd.read_csv('iris-data.csv')[['Sepal Length', 'Sepal Width']] df.head()输出如下:
![图 4.31:花萼特征]()
图 4.31:花萼特征
-
通过减去相应的均值,使数据集围绕零居中:
注意
means = np.mean(df.values, axis=0) means输出如下:
array([5.84333333, 3.054 ])为了计算数据并打印结果,请使用以下代码:
data = df.values - means data输出的部分如下:
![图 4.32:输出的部分]()
图 4.32:输出的部分
-
使用手动 PCA 基于第一个主成分来变换数据:
eigenvectors, eigenvalues, _ = np.linalg.svd(np.cov(data.T), full_matrices=False) P = eigenvectors[0] P输出如下:
array([-0.99693955, 0.07817635]) -
将数据转换为低维空间:
data_transformed = P.dot(data.T) -
重塑主成分以便后续使用:
P = P.reshape((-1, 1)) -
为了计算减少数据集的逆变换,我们需要将选定的特征向量恢复到更高维空间。为此,我们将对矩阵进行求逆。矩阵求逆是另一种线性代数技术,这里我们只会简单介绍。一个方阵,A,如果存在另一个方阵B,且满足AB=BA=I,其中I是一个特殊矩阵,称为单位矩阵,只有主对角线上的值为
1,则该方阵被称为可逆矩阵:P_transformed = np.linalg.pinv(P) P_transformed输出如下:
array([[-0.99693955, 0.07817635]]) -
为矩阵乘法准备变换后的数据:
data_transformed = data_transformed.reshape((-1, 1)) -
计算减少数据的逆变换,并绘制结果以可视化去除数据方差的效果:
data_restored = data_transformed.dot(P_transformed) data_restored输出的部分如下:
![图 4.33:减少数据的逆变换]()
图 4.33:减少数据的逆变换
-
将
means添加回变换后的数据:data_restored += means -
通过绘制原始数据集和变换后的数据集来可视化结果:
plt.figure(figsize=(10, 7)) plt.plot(data_restored[:,0], data_restored[:,1], linestyle=':', label='PCA restoration'); plt.scatter(df['Sepal Length'], df['Sepal Width'], marker='*', label='Original'); plt.legend(); plt.xlabel('Sepal Length'); plt.ylabel('Sepal Width'); plt.title('Inverse transform after removing variance');输出如下:
![图 4.34:去除方差后的逆变换]()
图 4.34:去除方差后的逆变换
-
该数据集只有两个变化成分。如果我们不去除任何成分,那么逆变换的结果会是什么?再次将数据转换为低维空间,但这次使用所有的特征向量:
P = eigenvectors data_transformed = P.dot(data.T) -
转置
data_transformed,使其具有正确的形状以进行矩阵乘法:data_transformed = data_transformed.T -
现在,将数据恢复到更高维空间:
data_restored = data_transformed.dot(P) data_restored输出的部分如下:
![图 4.35:恢复的数据]()
图 4.35:恢复的数据
-
将均值添加回恢复的数据:
data_restored += means -
在原始数据集的背景下可视化恢复的数据:
plt.figure(figsize=(10, 7)) plt.scatter(data_restored[:,0], data_restored[:,1], marker='d', label='PCA restoration', c='k'); plt.scatter(df['Sepal Length'], df['Sepal Width'], marker='o', label='Original', c='k'); plt.legend(); plt.xlabel('Sepal Length'); plt.ylabel('Sepal Width'); plt.title('Inverse transform after removing variance');输出如下:

图 4.36:去除方差后的逆变换
如果我们比较本练习中生成的两个图,我们可以看到,PCA 降维后的数据集与恢复的数据集基本上是两个特征集之间的负线性趋势线。我们可以将其与从所有可用成分恢复的数据集进行比较,在该数据集中我们已经完整地重建了原始数据集。
练习 16:通过可视化降方差
在本练习中,我们将再次可视化降维对数据集的影响;不过这次,我们将使用 scikit-learn API。由于 scikit-learn 模型的强大功能和简便性,这也是在实际应用中常用的方法:
注意
该数据集来自 archive.ics.uci.edu/ml/machine-learning-databases/iris/。可以从 github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson04/Exercise16 下载。
UCI 机器学习库 [archive.ics.uci.edu/ml]。加利福尼亚州欧文市:加利福尼亚大学信息与计算机科学学院。
-
导入
pandas、numpy和matplotlib绘图库,以及从 scikit-learn 中导入PCA模型:import pandas as pd import numpy as np import matplotlib.pyplot as plt from sklearn.decomposition import PCA -
从鸢尾花数据集中读取
Sepal特征:df = pd.read_csv('iris-data.csv')[['Sepal Length', 'Sepal Width']] df.head()输出如下:
![图 4.37:来自鸢尾花数据集的 Sepal 特征]()
图 4.37:来自鸢尾花数据集的 Sepal 特征
-
使用 scikit-learn API 基于第一个主成分对数据进行变换:
model = PCA(n_components=1) data_p = model.fit_transform(df.values)输出如下:
-
计算降维数据的逆变换,并绘制结果以可视化去除数据中方差的效果:
data = model.inverse_transform(data_p); plt.figure(figsize=(10, 7)) plt.plot(data[:,0], data[:,1], linestyle=':', label='PCA restoration'); plt.scatter(df['Sepal Length'], df['Sepal Width'], marker='*', label='Original'); plt.legend(); plt.xlabel('Sepal Length'); plt.ylabel('Sepal Width'); plt.title('Inverse transform after removing variance');输出如下:
![图 4.38:去除方差后的逆变换]()
图 4.38:去除方差后的逆变换
-
该数据集中只有两个变化成分。如果我们不去除任何成分,逆变换的结果会是什么?
model = PCA() data_p = model.fit_transform(df.values) data = model.inverse_transform(data_p); plt.figure(figsize=(10, 7)) plt.scatter(data[:,0], data[:,1], marker='d', label='PCA restoration', c='k'); plt.scatter(df['Sepal Length'], df['Sepal Width'], marker='o', label='Original', c='k'); plt.legend(); plt.xlabel('Sepal Length'); plt.ylabel('Sepal Width'); plt.title('Inverse transform after removing variance');输出如下:

图 4.39:去除方差后的逆变换
再次,我们展示了从数据集中去除信息的效果,并且展示了如何使用所有可用的特征向量重新创建原始数据。
之前的练习指定了使用 PCA 将数据维度减少到二维,部分原因是为了方便结果的可视化。然而,我们也可以使用 PCA 将数据维度减少到任何小于原始数据集的值。以下示例演示了如何使用 PCA 将数据集减少到三维,从而实现可视化。
练习 17:在 Matplotlib 中绘制 3D 图形
在 matplotlib 中创建 3D 散点图并不像简单地将一系列 (x, y, z) 坐标提供给散点图那么简单。在本练习中,我们将通过一个简单的 3D 绘图示例,使用鸢尾花数据集进行操作:
注意
本数据集来自 archive.ics.uci.edu/ml/machine-learning-databases/iris/。
可以从 github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson04/Exercise17 下载。
UCI 机器学习数据库 [archive.ics.uci.edu/ml]。加利福尼亚州欧文市:加利福尼亚大学信息与计算机科学学院。
-
导入
pandas和matplotlib。为了启用 3D 绘图,你还需要导入Axes3D:from mpl_toolkits.mplot3d import Axes3D import pandas as pd import matplotlib.pyplot as plt -
读取数据集并选择
Sepal Length、Sepal Width和Petal Width列df = pd.read_csv('iris-data.csv')[['Sepal Length', 'Sepal Width', 'Petal Width']] df.head()输出结果如下:
![图 4.40:数据的前五行]()
图 4.40:数据的前五行
-
在三维空间中绘制数据,并使用
projection='3d'参数与add_subplot方法来创建 3D 图:fig = plt.figure(figsize=(10, 7)) ax = fig.add_subplot(111, projection='3d') # Where Axes3D is required ax.scatter(df['Sepal Length'], df['Sepal Width'], df['Petal Width']); ax.set_xlabel('Sepal Length (mm)'); ax.set_ylabel('Sepal Width (mm)'); ax.set_zlabel('Petal Width (mm)'); ax.set_title('Expanded Iris Dataset');绘图结果如下:

图 4.41:扩展版鸢尾花数据集
注意
尽管导入了 Axes3D,但没有直接使用,它对于配置三维绘图窗口是必需的。如果省略了 Axes3D 的导入,projection='3d' 参数将返回一个 AttributeError。
活动 7:使用扩展版鸢尾花数据集进行 PCA
在本活动中,我们将使用完整的鸢尾花数据集,观察选择不同数量组件进行 PCA 分解的效果。本活动旨在模拟一个真实世界问题中的过程,我们试图确定选择最佳组件数,同时平衡降维程度和信息丢失。因此,我们将使用 scikit-learn 的 PCA 模型:
注意
本数据集来自 archive.ics.uci.edu/ml/machine-learning-databases/iris/。
可以从 github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson04/Activity07 下载。
UCI 机器学习库 [archive.ics.uci.edu/ml]。加利福尼亚州欧文市:加利福尼亚大学信息与计算机科学学院。
-
导入
pandas和matplotlib。为了启用三维绘图,您还需要导入Axes3D。 -
读取数据集,并选择
花萼长度、花萼宽度和花瓣宽度列。 -
在三维空间中绘制数据。
-
创建一个
PCA模型,未指定组件数量。 -
将模型拟合到数据集。
-
显示特征值或
explained_variance_ratio_。 -
我们希望减少数据集的维度,但仍保持至少 90%的方差。为保持 90%的方差,所需的最小组件数是多少?
-
创建一个新的
PCA模型,这次指定所需的组件数量,以保持至少 90%的方差。 -
使用新模型变换数据。
-
绘制变换后的数据。
-
将变换后的数据恢复到原始数据空间。
-
在一个子图中绘制恢复后的三维数据,在第二个子图中绘制原始数据,以可视化去除部分方差的效果:
fig = plt.figure(figsize=(10, 14)) # Original Data ax = fig.add_subplot(211, projection='3d') # Transformed Data ax = fig.add_subplot(212, projection='3d')
预期输出:最终图形将如下所示:

图 4.42:预期图
注意
本活动的解决方案可以在第 328 页找到。
总结
本章介绍了降维和 PCA 的过程。我们完成了一些练习,并发展了提取数据中最重要方差成分的技能,以减少数据集的大小,既使用手动 PCA 过程,也使用 scikit-learn 提供的模型。在本章中,我们还将降维后的数据集恢复到原始数据空间,并观察去除方差对原始数据的影响。最后,我们讨论了 PCA 和其他降维过程的多种潜在应用。在下一章中,我们将介绍基于神经网络的自编码器,并使用 Keras 包实现它们。
第五章:自编码器
学习目标
本章结束时,您将能够完成以下内容:
-
解释自编码器的应用领域及其使用案例
-
理解人工神经网络的实现与应用
-
使用 Keras 框架实现一个人工神经网络
-
解释自编码器在降维和去噪中的应用
-
使用 Keras 框架实现自编码器
-
解释并实现一个基于卷积神经网络的自编码器模型
本章将介绍自编码器及其应用。
引言
本章继续讨论降维技术,我们将焦点转向自编码器。自编码器是一个特别有趣的研究领域,因为它们提供了一种基于人工神经网络的有监督学习方法,但又是在无监督的环境下进行的。基于人工神经网络的自编码器是一种极为有效的降维手段,并且提供了额外的好处。随着数据、处理能力和网络连接的可用性不断提升,自编码器自 1980 年代末期以来,正在经历一场复兴,重新被广泛使用和研究。这也与人工神经网络的研究相一致,后者最早在 1960 年代被提出和实现。如今,只需进行简单的互联网搜索,就能发现神经网络的流行性和强大能力。
自编码器可以用于图像去噪和生成人工数据样本,结合其他方法,如递归神经网络或长短期记忆(LSTM)架构,用于预测数据序列。人工神经网络的灵活性和强大功能使得自编码器能够形成数据的高效表示,之后这些表示可以直接用于极其高效的搜索方法,或作为特征向量进行后续处理。
考虑在图像去噪应用中使用自编码器,我们展示的是左侧的图像(见图 5.1)。可以看到,图像受到一些随机噪声的影响。我们可以使用经过特殊训练的自编码器去除这些噪声,右侧的图像就是去噪后的结果(见图 5.1)。在学习如何去除噪声的过程中,自编码器也学习到了如何编码构成图像的重要信息,并将这些信息解码(或重构)为原始图像的更清晰版本。

图 5.1:自编码器去噪
注意
此图像修改自 www.freenzphotos.com/free-photos-of-bay-of-plenty/stormy-fishermen/ 并遵循 CC0 许可协议。
本例演示了自编码器在无监督学习中有用的一个方面(编码阶段),以及在生成新图像时有用的另一个方面(解码阶段)。在本章中,我们将进一步探讨自编码器的这两个有用阶段,并将自编码器的输出应用于 CIFAR-10 数据集的聚类。
这里是编码器和解码器的表示:

图 5.2:编码器/解码器表示
人工神经网络基础
鉴于自编码器是基于人工神经网络的,因此理解神经网络的原理对于理解自编码器至关重要。本章的这一部分将简要回顾人工神经网络的基础知识。需要注意的是,神经网络的许多方面超出了本书的范围。神经网络的主题本身就能填满许多本书,这一部分并不是对该主题的详尽讨论。
如前所述,人工神经网络主要用于监督学习问题,其中我们有一组输入信息,例如一系列图像,我们正在训练一个算法将这些信息映射到所需的输出,例如类别或标签。以 CIFAR-10 数据集(图 5.3)为例,它包含了 10 个不同类别的图像(飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车),每个类别有 6,000 张图像。当神经网络用于监督学习时,图像被输入到网络中,而对应的类别标签则是网络的期望输出。
然后,网络经过训练,以最大化推断或预测给定图像正确标签的能力。

图 5.3:CIFAR-10 数据集
注意
此图来自www.cs.toronto.edu/~kriz/cifar.html,出自《从微小图像中学习多层特征》,Alex Krizhevsky,2009 年。
神经元
人工神经网络得名于生物神经网络,这些神经网络通常存在于大脑中。虽然这一类比的准确性是值得质疑的,但它是帮助理解人工神经网络概念的有用隐喻。与生物神经元类似,神经元是构建所有神经网络的基本单元,通过不同的配置将多个神经元连接起来,从而形成更强大的结构。每个神经元(图 5.4)由四个部分组成:输入值、可调权重(θ)、作用于输入值的激活函数以及最终的输出值:

图 5.4:神经元的解剖结构
激活函数的选择是根据神经网络的目标来特定选择的,有许多常用的函数,包括tanh、sigmoid、linear、sigmoid和ReLU(修正线性单元)。在本章中,我们将同时使用sigmoid和ReLU激活函数,接下来我们将更详细地讨论它们。
Sigmoid 函数
Sigmoid 激活函数因其能将输入值转化为接近二进制的输出,因此在神经网络分类任务中非常常见。Sigmoid 函数的输出如下:

图 5.5:Sigmoid 函数的输出
我们可以在图 5.5中看到,sigmoid 函数的输出随着x的增大渐近于 1,而当x在负方向远离 0 时,输出渐近于 0。这个函数常用于分类任务,因为它提供接近二进制的输出,表示是否属于类(0)或类(1)。
修正线性单元(ReLU)
修正线性单元是一个非常有用的激活函数,通常在神经网络的中间阶段使用。简单来说,输入小于 0 时输出为 0,大于 0 时输出为输入值本身。

图 5.6:ReLU 的输出
练习 18:模拟人工神经网络的神经元
在这个练习中,我们将通过使用 sigmoid 函数,实际介绍神经元在 NumPy 中的编程表示。我们将固定输入并调整可调权重,以研究其对神经元的影响。有趣的是,这个模型也非常接近于逻辑回归的监督学习方法。请执行以下步骤:
-
导入
numpy和 matplotlib 包:import numpy as np import matplotlib.pyplot as plt -
配置 matplotlib 以启用使用 Latex 渲染图像中的数学符号:
plt.rc('text', usetex=True) -
将
sigmoid函数定义为 Python 函数:def sigmoid(z): return np.exp(z) / (np.exp(z) + 1)注意
在这里,我们使用的是 sigmoid 函数。你也可以使用 ReLU 函数。尽管 ReLU 激活函数在人工神经网络中非常强大,但它的定义非常简单。它只需要在输入大于 0 时返回输入值,否则返回 0:
def relu(x):return np.max(0, x) -
定义神经元的输入(
x)和可调权重(theta)。在这个示例中,输入(x)将是 100 个在-5和5之间线性分布的数字。设置theta = 1:theta = 1 x = np.linspace(-5, 5, 100) x输出的一部分如下:
![图 5.7:打印输入]()
图 5.7:打印输入
-
计算神经元的输出(
y):y = sigmoid(x * theta) -
绘制神经元的输出与输入的关系图:
fig = plt.figure(figsize=(10, 7)) ax = fig.add_subplot(111) ax.plot(x, y) ax.set_xlabel('$x$', fontsize=22); ax.set_ylabel('$h(x\Theta)$', fontsize=22); ax.spines['left'].set_position(('data', 0)); ax.spines['top'].set_visible(False); ax.spines['right'].set_visible(False); ax.tick_params(axis='both', which='major', labelsize=22)输出如下:
![图 5.8:神经元与输入的关系图]()
图 5.8:神经元与输入的关系图
-
将可调参数
theta设置为 5,重新计算并存储神经元的输出:theta = 5 y_2 = sigmoid(x * theta) -
将可调参数
theta更改为0.2,然后重新计算并存储神经元的输出:theta = 0.2 y_3 = sigmoid(x * theta) -
在同一图表上绘制神经元的三条不同输出曲线(
theta = 1,theta = 5,theta = 0.2):fig = plt.figure(figsize=(10, 7)) ax = fig.add_subplot(111) ax.plot(x, y, label='$\Theta=1$'); ax.plot(x, y_2, label='$\Theta=5$', linestyle=':'); ax.plot(x, y_3, label='$\Theta=0.2$', linestyle='--'); ax.set_xlabel('$x\Theta$', fontsize=22); ax.set_ylabel('$h(x\Theta)$', fontsize=22); ax.spines['left'].set_position(('data', 0)); ax.spines['top'].set_visible(False); ax.spines['right'].set_visible(False); ax.tick_params(axis='both', which='major', labelsize=22); ax.legend(fontsize=22);输出如下所示:

图 5.9: 神经元的输出曲线
在本次练习中,我们使用了一个具有 Sigmoid 激活函数的人工神经网络基本构建模块。我们可以看到,使用 Sigmoid 函数会增加梯度的陡峭度,这意味着只有较小的 x 值才能将输出推向接近 1 或 0。同样,减小 theta 会降低神经元对非零值的敏感度,导致需要更极端的输入值才能将输出推向 0 或 1,从而调节神经元的输出。
活动 8:使用 ReLU 激活函数建模神经元
在本次活动中,我们将研究 ReLU 激活函数以及可调权重对修改 ReLU 单元输出的影响:
-
导入
numpy和 matplotlib。 -
将 ReLU 激活函数定义为一个 Python 函数。
-
定义神经元的输入(
x)和可调权重(theta)。在这个示例中,输入(x)将是 100 个在线性间隔内从-5到5的数字。设置theta = 1。 -
计算输出(
y)。 -
绘制神经元的输出与输入的关系图。
-
现在,将
theta设置为 5,重新计算并存储神经元的输出。 -
现在,将
theta设置为0.2,然后重新计算并存储神经元的输出。 -
在同一图表上绘制神经元的三条不同输出曲线(
theta = 1,theta = 5,和theta = 0.2)。
在本活动结束时,您将为 ReLU 激活神经元开发一系列响应曲线。您还将能够描述改变 theta 值对神经元输出的影响。输出将如下所示:

图 5.10: 预期的输出曲线
注意
这个活动的解决方案可以在第 333 页找到。
神经网络:架构定义
单个神经元在孤立状态下并不是特别有用;它提供一个激活函数和调节输出的手段,但单个神经元的学习能力有限。当多个神经元被组合并连接成网络结构时,它们的功能会更强大。通过使用多个不同的神经元并结合各个神经元的输出,可以建立更复杂的关系并构建更强大的学习算法。在本节中,我们将简要讨论神经网络的结构,并使用 Keras 机器学习框架(keras.io/) 实现一个简单的神经网络。

图 5.11:简化表示的神经网络
图 5.11展示了一个两层全连接神经网络的结构。我们可以做出的第一个观察是,这个结构包含了大量的信息,并且具有高度的连接性,这通过指向每个节点的箭头表示。从图像的左侧开始,我们可以看到神经网络的输入值,表示为(x)值。在这个例子中,每个样本有m个输入值,而且只有第一个样本被输入到网络中,因此,值来自于!凳子的特写
描述自动生成 到
。这些值随后与神经网络第一层的相应权重相乘(
),然后传递到相应神经元的激活函数中。这被称为前馈神经网络。图 5.11中用于标识权重的符号是!标志的特写
描述自动生成,其中i是权重所属的层,j是输入节点的编号(从顶部开始为 1),k是权重传递到的后续层节点。
观察第一层(也称为隐藏层)输出与输出层输入之间的互联性,我们可以看到,有大量的可训练参数(权重)可以用于将输入映射到期望的输出。图 5.11中的网络表示一个n类神经网络分类器,其中每个n节点的输出表示输入属于相应类别的概率。
每一层都可以使用不同的激活函数,如
和
所示,从而允许不同的激活函数混合使用,例如,第一层可以使用 ReLU,第二层可以使用 tanh,第三层可以使用 sigmoid。最终输出通过将前一层输出与相应的权重相乘并求和来计算。
如果我们考虑第一层节点的输出,可以通过将输入值与相应的权重相乘,求和结果,并通过激活函数来计算:

图 5.12:计算最后一个节点的输出
随着输入和输出之间的层数增加,我们增加了网络的深度。深度的增加也意味着可训练参数的数量增多,同时数据中描述关系的复杂性增加。通常,随着深度的增加,训练网络会变得更加困难,因为选择用于输入的特征变得更加关键。此外,随着我们向每一层添加更多的神经元,我们也增加了神经网络的高度。通过增加更多的神经元,网络描述数据集的能力增强,同时可训练的参数也增多。如果添加了过多的神经元,网络可能会记住数据集中的样本,但无法对新样本进行泛化。构建神经网络的关键在于找到一个平衡点,使得模型既有足够的复杂性来描述数据中的关系,又不会复杂到只会记忆训练样本。
练习 19:定义 Keras 模型
在这个练习中,我们将使用 Keras 机器学习框架定义一个神经网络架构(类似于图 5.11),用于对 CIFAR-10 数据集中的图像进行分类。由于每个输入图像的大小为 32 x 32 像素,输入向量将包含 32*32 = 1,024 个值。CIFAR-10 有 10 个不同的类别,因此神经网络的输出将由 10 个独立的值组成,每个值表示输入数据属于相应类别的概率。
-
对于这个练习,我们将需要 Keras 机器学习框架。Keras 是一个高层神经网络 API,通常用于现有库之上,如 TensorFlow 或 Theano。Keras 使得在底层框架之间切换变得更加容易,因为它提供的高层接口在不同的底层库中保持一致。在本书中,我们将使用 TensorFlow 作为底层库。如果你还没有安装 Keras 和 TensorFlow,请使用
conda安装:!conda install tensforflow keras或者,你也可以使用
pip安装它:!pip install tensorflow keras -
我们将需要从
keras.models和keras.layers中导入Sequential和Dense类。导入这些类:from keras.models import Sequential from keras.layers import Dense -
如前所述,输入层将接收 1,024 个值。第二层(层 1)将包含 500 个单元,并且因为网络需要分类 10 个不同的类别,所以输出层将包含 10 个单元。在 Keras 中,模型是通过将有序的层列表传递给
Sequential模型类来定义的。此示例使用了Dense层类,它是一个全连接神经网络层。第一层将使用 ReLU 激活函数,而输出层将使用softmax函数来确定每个类别的概率。定义模型:model = Sequential([ Dense(500, input_shape=(1024,), activation='relu'), Dense(10, activation='softmax') ]) -
在定义了模型之后,我们可以使用
summary方法来确认模型的结构以及其中可训练的参数(或权重)的数量:model.summary()输出如下:

图 5.13:模型中可训练参数的结构与数量
该表总结了神经网络的结构。我们可以看到,模型中有我们指定的两层,第一层包含 500 个单元,第二层包含 10 个输出单元。Param # 列显示了每一层中可训练权重的数量。表格还告诉我们,网络中总共有 517,510 个可训练权重。
在这个练习中,我们在 Keras 中创建了一个神经网络模型,该模型包含超过 500,000 个权重,可以用来对 CIFAR-10 的图像进行分类。在接下来的章节中,我们将训练该模型。
神经网络:训练
定义好神经网络模型后,我们可以开始训练过程;在这个阶段,我们将以监督学习的方式训练模型,以便在继续训练自编码器之前,对 Keras 框架有一定的熟悉度。监督学习模型通过提供输入信息和已知输出信息来训练模型;训练的目标是构建一个网络,使其能够仅通过模型的参数,将输入信息映射到已知输出。
在像 CIFAR-10 这样的监督分类示例中,输入信息是一张图像,而已知输出是该图像所属的类别。在训练过程中,对于每一个样本的预测,使用指定的误差函数计算前馈网络的预测误差。然后,模型中的每个权重都会进行调整,试图减少误差。这一调整过程被称为反向传播,因为误差是从输出开始通过网络向后传播,直到网络的起始位置。
在反向传播过程中,每个可训练的权重都会根据它对整体误差的贡献进行调整,调整幅度与一个被称为学习率的值成正比,该值控制可训练权重变化的速率。看一下图 5.14,我们可以看到,增加学习率的值可以加快误差减少的速度,但也有可能因为步长过大而无法收敛到最小误差。学习率过小可能会导致我们失去耐心,或者根本没有足够的时间找到全局最小值。因此,找到正确的学习率是一个试错过程,尽管从较大的学习率开始并逐步减少通常是一个有效的方法。以下图示展示了学习率的选择:

图 5.14:选择正确的学习率(一个 epoch 是一次学习步骤)
训练会重复进行,直到预测中的误差不再减少,或者开发者在等待结果时失去耐心。为了完成训练过程,我们首先需要做出一些设计决策,第一个是选择最合适的误差函数。可用的误差函数有很多种,从简单的均方差到更复杂的选项。类别交叉熵(在接下来的练习中使用)是一个非常有用的误差函数,适用于分类多个类别。
在定义了误差函数之后,我们需要选择一种使用误差函数更新可训练参数的方法。最节省内存且有效的更新方法之一是随机梯度下降法(SGD);SGD 有许多变种,所有变种都涉及根据每个权重对计算误差的贡献来调整权重。最后的训练设计决策是模型评估的性能指标,以及选择最佳架构;在分类问题中,这可能是模型的分类准确度,或者在回归问题中,可能是产生最低误差分数的模型。这些比较通常是通过交叉验证方法进行的。
练习 20:训练 Keras 神经网络模型
幸运的是,我们不需要担心手动编写神经网络的各个组件,如反向传播,因为 Keras 框架会为我们管理这些。在本次练习中,我们将使用 Keras 训练一个神经网络,使用前面练习中定义的模型架构对 CIFAR-10 数据集的一个小子集进行分类。与所有机器学习问题一样,第一步也是最重要的一步是尽可能多地了解数据集,这将是本次练习的初步重点:
注意
你可以从github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson05/Exercise20下载data_batch_1和batches.meta文件。
-
导入
pickle、numpy、matplotlib以及从keras.models导入Sequential类,并从keras.layers导入Dense:import pickle import numpy as np import matplotlib.pyplot as plt from keras.models import Sequential from keras.layers import Dense -
加载与随附源代码一起提供的 CIFAR-10 数据集样本,该样本位于
data_batch_1文件中:with open('data_batch_1', 'rb') as f: dat = pickle.load(f, encoding='bytes') -
数据作为字典加载。显示字典的键:
dat.keys()输出结果如下:
dict_keys([b'batch_label', b'labels', b'data', b'filenames']) -
注意,键是作为二进制字符串存储的,表示为
b'。我们关心的是数据和标签的内容。我们先来看标签:labels = dat[b'labels'] labels输出结果如下:
![]()
图 5.15:显示标签
-
我们可以看到标签是一个 0 到 9 的值列表,表示每个样本所属的类别。现在,来看一下
data键的内容:dat[b'data']输出结果如下:
![图 5.16:data 键的内容]()
图 5.16:数据键的内容
-
数据键提供了一个包含所有图像数据的 NumPy 数组。图像数据的形状是什么?
dat[b'data'].shape输出结果如下:
(1000, 3072) -
我们可以看到我们有 1000 个样本,但每个样本是一个长度为 3,072 的一维数组。难道图像不应该是 32 x 32 像素吗?是的,它们是,但由于图像是彩色的或 RGB 图像,它们包含三个通道(红、绿、蓝),这意味着图像的尺寸是 32 x 32 x 3。它们还被展开,提供 3,072 长度的向量。所以,我们可以重新调整数组形状,然后可视化一个样本图像。根据 CIFAR-10 文档,前 1,024 个样本是红色,第二个 1,024 个是绿色,第三个 1,024 个是蓝色:
images = np.zeros((10000, 32, 32, 3), dtype='uint8') for idx, img in enumerate(dat[b'data']): images[idx, :, :, 0] = img[:1024].reshape((32, 32)) # Red images[idx, :, :, 1] = img[1024:2048].reshape((32, 32)) # Green images[idx, :, :, 2] = img[2048:].reshape((32, 32)) # Blue -
显示前 12 张图片及其标签:
plt.figure(figsize=(10, 7)) for i in range(12): plt.subplot(3, 4, i + 1) plt.imshow(images[i]) plt.title(labels[i]) plt.axis('off')输出结果如下:
![图 5.17:前 12 张图片]()
图 5.17:前 12 张图片
-
标签的实际意义是什么?要了解这一点,加载
batches.meta文件:with open('batches.meta', 'rb') as f: label_strings = pickle.load(f, encoding='bytes') label_strings输出结果如下:
![]()
图 5.18:标签的含义
-
解码二进制字符串以获取实际标签:
actual_labels = [label.decode() for label in label_strings[b'label_names']] actual_labels输出结果如下:
![]()
图 5.19:打印实际标签
-
打印前 12 张图片的标签:
for lab in labels[:12]: print(actual_labels[lab], end=', ')输出结果如下:
![图 5.20:前 12 张图片的标签]()
图 5.20:前 12 张图片的标签
-
现在我们需要准备数据来训练模型。第一步是准备输出。目前,输出是一个 0 到 9 的数字列表,但我们需要每个样本都用一个包含 10 个单位的向量表示,正如前面模型所要求的那样。编码后的输出将是一个形状为 10000 x 10 的 NumPy 数组:
注意
one_hot_labels = np.zeros((images.shape[0], 10)) for idx, lab in enumerate(labels): one_hot_labels[idx, lab] = 1 -
显示前 12 个样本的独热编码值:
one_hot_labels[:12]输出结果如下:
![图 5.21:前 12 个样本的独热编码值]()
图 5.21:前 12 个样本的独热编码值
-
模型有 1,024 个输入,因为它期望输入的是 32 x 32 的灰度图像。将每张图像的三个通道的平均值取出,将其转换为 RGB:
images = images.mean(axis=-1) -
再次显示前 12 张图片:
plt.figure(figsize=(10, 7)) for i in range(12): plt.subplot(3, 4, i + 1) plt.imshow(images[i], cmap='gray') plt.title(labels[i]) plt.axis('off')输出结果如下:
![图 5.22:再次显示前 12 张图片。]()
图 5.22:再次显示前 12 张图片。
-
最后,将图片缩放到 0 和 1 之间,这是神经网络所有输入所需要的。由于图像中的最大值为 255,我们将其直接除以 255:
images /= 255. -
我们还需要将图像的形状调整为 10,000 x 1,024:
images = images.reshape((-1, 32 ** 2)) -
重新定义模型,使用与练习 19,定义 Keras 模型相同的架构:
model = Sequential([ Dense(500, input_shape=(1024,), activation='relu'), Dense(10, activation='softmax') ]) -
现在我们可以在 Keras 中训练模型。我们首先需要编译方法,以指定训练参数。我们将使用分类交叉熵,随机梯度下降,并使用分类准确率作为性能指标:
model.compile(loss='categorical_crossentropy', optimizer='sgd', metrics=['accuracy']) -
使用反向传播方法训练模型,训练 100 个周期,并使用模型的
fit方法:model.fit(images, one_hot_labels, epochs=100)输出结果如下:
![图 5.23:训练模型]()
图 5.23:训练模型
-
我们使用这个网络对 1,000 个样本进行了分类,并取得了大约 90% 的分类准确率。请再次检查对前 12 个样本的预测结果:
predictions = model.predict(images[:12]) predictions输出结果如下:
![图 5.24:打印预测结果]()
图 5.24:打印预测结果
-
我们可以使用
argmax方法来确定每个样本最可能的类别:np.argmax(predictions, axis=1)输出结果如下:
array([6, 9, 9, 4, 1, 1, 2, 7, 8, 3, 2, 7]) -
与标签进行比较:
labels[:12]输出结果如下:
[6, 9, 9, 4, 1, 1, 2, 7, 8, 3, 4, 7]
网络在这些样本中犯了一个错误,即它将倒数第二个样本分类为 2(鸟)而不是 4(鹿)。恭喜你!你刚刚成功训练了一个 Keras 神经网络模型。完成下一个活动,进一步巩固你在训练神经网络方面的技能。
活动 9:MNIST 神经网络
在本次活动中,你将训练一个神经网络来识别 MNIST 数据集中的图像,并进一步巩固你在训练神经网络方面的技能。这个活动为许多不同分类问题中的神经网络架构奠定了基础,尤其是在计算机视觉领域。从目标检测和识别到分类,这种通用结构被应用于多种不同的场景。
这些步骤将帮助你完成活动:
-
导入
pickle、numpy、matplotlib,以及来自 Keras 的Sequential和Dense类。 -
加载包含前 10,000 张图像及其相应标签的
mnist.pkl文件,这些数据来自附带源代码中的 MNIST 数据集。MNIST 数据集是一系列 28 x 28 像素的手写数字灰度图像,数字范围从 0 到 9。提取图像和标签。注意
你可以在
github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson05/Activity09找到mnist.pkl文件。 -
绘制前 10 个样本及其对应标签。
-
使用独热编码对标签进行编码。
-
准备图像以便输入神经网络。提示:这个过程有 两个 独立的步骤。
-
在 Keras 中构建一个神经网络模型,接受已准备好的图像,并具有 600 单元的隐藏层,激活函数为 ReLU,输出层的单元数与类别数相同。输出层使用
softmax激活函数。 -
使用多类交叉熵、随机梯度下降和准确性性能指标编译模型。
-
训练模型。要达到训练数据至少 95%的分类准确率,需要多少个训练周期(epoch)?
通过完成此活动,你已经训练了一个简单的神经网络来识别手写数字 0 到 9。你还开发了一个用于构建分类问题神经网络的通用框架。通过这个框架,你可以扩展和修改网络以适应其他任务。
注意
本活动的解答可以在第 335 页找到。
自编码器
既然我们已经习惯在 Keras 中开发监督学习神经网络模型,我们可以将注意力转回到无监督学习及本章的主要主题——自编码器。自编码器是一种专门设计的神经网络架构,旨在以高效且具有描述性的方式将输入信息压缩到较低维度空间。自编码器网络可以分解为两个独立的子网络或阶段:编码阶段和解码阶段。首先,编码阶段将输入信息通过一个后续的层进行压缩,该层的单元数少于输入样本的大小。随后,解码阶段扩展压缩后的图像,并试图将压缩数据恢复为其原始形式。因此,网络的输入和期望输出是相同的;网络输入的是 CIFAR-10 数据集中的一张图像,并试图返回相同的图像。该网络架构如图 5.25所示;在此图中,我们可以看到自编码器的编码阶段将表示信息的神经元数量减少,而解码阶段则将压缩格式的图像恢复到其原始状态。解码阶段的使用有助于确保编码器正确地表示了信息,因为恢复图像到原始状态所需的所有信息都来自于压缩后的表示。接下来,我们将使用 CIFAR-10 数据集来实现一个简化的自编码器模型:

图 5.25:简单自编码器网络架构
练习 21:简单自编码器
在本练习中,我们将为 CIFAR-10 数据集的样本构建一个简单的自编码器,将图像中存储的信息压缩以供后续使用。
注意
你可以从github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson05/Exercise21下载data_batch_1文件。
-
导入
pickle、numpy和matplotlib,以及从keras.models中导入Model类,从keras.layers中导入Input和Dense:import pickle import numpy as np import matplotlib.pyplot as plt from keras.models import Model from keras.layers import Input, Dense -
加载数据:
with open('data_batch_1', 'rb') as f: dat = pickle.load(f, encoding='bytes') -
由于这是无监督学习方法,我们只关注图像数据。按照前一个练习加载图像数据:
images = np.zeros((10000, 32, 32, 3), dtype='uint8') for idx, img in enumerate(dat[b'data']): images[idx, :, :, 0] = img[:1024].reshape((32, 32)) # Red images[idx, :, :, 1] = img[1024:2048].reshape((32, 32)) # Green images[idx, :, :, 2] = img[2048:].reshape((32, 32)) # Blue -
将图像转换为灰度图像,缩放至 0 到 1 之间,并将每个图像展平为一个长度为 1,024 的向量:
images = images.mean(axis=-1) images = images / 255.0 images = images.reshape((-1, 32 ** 2)) images -
定义自编码器模型。由于我们需要访问编码器阶段的输出,我们将使用一种稍微不同于之前的方法来定义模型。定义一个包含
1024个单元的输入层:input_layer = Input(shape=(1024,)) -
定义后续的
Dense层,包含256个单元(压缩比为 1024/256 = 4),并使用 ReLU 激活函数作为编码阶段。请注意,我们已将该层分配给一个变量,并将前一层传递给类的调用方法:encoding_stage = Dense(256, activation='relu')(input_layer) -
定义一个后续解码器层,使用 sigmoid 函数作为激活函数,并与输入层具有相同的形状。选择 sigmoid 函数是因为网络的输入值仅介于 0 和 1 之间:
decoding_stage = Dense(1024, activation='sigmoid')(encoding_stage) -
通过将网络的第一层和最后一层传递给
Model类来构建模型:autoencoder = Model(input_layer, decoding_stage) -
使用二元交叉熵损失函数和 adadelta 梯度下降法编译自编码器:
autoencoder.compile(loss='binary_crossentropy', optimizer='adadelta')注意
adadelta是随机梯度下降法的一个更复杂版本,其中学习率根据最近一段时间内的梯度更新窗口进行调整。与其他学习率调整方法相比,这避免了非常旧的周期的梯度影响学习率。 -
现在,让我们拟合模型;同样,我们将图像作为训练数据并作为期望的输出。训练 100 个周期:
autoencoder.fit(images, images, epochs=100)输出如下:
![图 5.26:训练模型]()
图 5.26:训练模型
-
计算并存储编码阶段前五个样本的输出:
encoder_output = Model(input_layer, encoding_stage).predict(images[:5]) -
将编码器输出重塑为 16 x 16(16 x 16 = 256)像素,并乘以 255:
encoder_output = encoder_output.reshape((-1, 16, 16)) * 255 -
计算并存储解码阶段前五个样本的输出:
decoder_output = autoencoder.predict(images[:5]) -
将解码器的输出重塑为 32 x 32 并乘以 255:
decoder_output = decoder_output.reshape((-1, 32,32)) * 255 -
重塑原始图像:
images = images.reshape((-1, 32, 32)) plt.figure(figsize=(10, 7)) for i in range(5): plt.subplot(3, 5, i + 1) plt.imshow(images[i], cmap='gray') plt.axis('off') plt.subplot(3, 5, i + 6) plt.imshow(encoder_output[i], cmap='gray') plt.axis('off') plt.subplot(3, 5, i + 11) plt.imshow(decoder_output[i], cmap='gray') plt.axis('off')输出如下:

图 5.27:简单自编码器的输出
在图 5.27中,我们可以看到三行图像。第一行是原始的灰度图像,第二行是对应于原始图像的自编码器输出,最后,第三行是从编码输入中重建的原始图像。我们可以看到第三行中的解码图像包含了图像的基本形状信息;我们可以看到青蛙和鹿的主体,以及卡车和汽车的轮廓。鉴于我们只训练了 100 个样本,这个练习也可以通过增加训练周期的数量来进一步提高编码器和解码器的性能。现在我们已经获得了训练好的自编码器阶段的输出,我们可以将其作为其他无监督算法的特征向量,如 K-means 或 K 近邻。
活动 10:简单 MNIST 自编码器
在此活动中,您将为包含在附带源代码中的 MNIST 数据集创建一个自编码器网络。像本活动中构建的自编码器网络,可以在无监督学习的预处理阶段非常有用。网络产生的编码信息可以用于聚类或分割分析,例如基于图像的网络搜索:
-
导入
pickle、numpy和matplotlib,以及来自 Keras 的Model、Input和Dense类。 -
从随附源代码提供的 MNIST 数据集样本加载图像(
mnist.pkl)。注意
你可以从
github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson05/Activity10下载mnist.pklP-code文件。 -
准备输入神经网络的图像。作为提示,这个过程分为两个独立的步骤。
-
构建一个简单的自编码器网络,在编码阶段后将图像大小缩小到 10 x 10。
-
使用二进制交叉熵损失函数和
adadelta梯度下降法编译自编码器。 -
拟合编码器模型。
-
计算并存储前五个样本的编码阶段输出。
-
将编码器输出重塑为 10 x 10(10 x 10 = 100)像素并乘以 255。
-
计算并存储前五个样本的解码阶段输出。
-
将解码器的输出重塑为 28 x 28 并乘以 255。
-
绘制原始图像、编码器输出和解码器输出。
完成此活动后,你将成功训练一个自编码器网络,该网络能够提取数据集中关键信息,并为后续处理做好准备。输出将类似于以下内容:

图 5.28:原始图像、编码器输出和解码器的预期图示
注意
此活动的解决方案可以在第 338 页找到。
练习 22:多层自编码器
在本练习中,我们将为 CIFAR-10 数据集的样本构建一个多层自编码器,将图像中存储的信息压缩以备后续使用:
注意
你可以从github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson05/Exercise22下载data_batch_1文件。
-
导入
pickle、numpy和matplotlib,以及来自keras.models的Model类,并导入来自keras.layers的Input和Dense类:import pickle import numpy as np import matplotlib.pyplot as plt from keras.models import Model from keras.layers import Input, Dense -
加载数据:
with open('data_batch_1', 'rb') as f: dat = pickle.load(f, encoding='bytes') -
由于这是无监督学习方法,我们只关心图像数据。按照之前的练习加载图像数据:
images = np.zeros((10000, 32, 32, 3), dtype='uint8') for idx, img in enumerate(dat[b'data']): images[idx, :, :, 0] = img[:1024].reshape((32, 32)) # Red images[idx, :, :, 1] = img[1024:2048].reshape((32, 32)) # Green images[idx, :, :, 2] = img[2048:].reshape((32, 32)) # Blue -
将图像转换为灰度,缩放到 0 到 1 之间,并将每个图像展平为一个长度为 1,024 的向量:
images = images.mean(axis=-1) images = images / 255.0 images = images.reshape((-1, 32 ** 2)) images -
定义多层自编码器模型。我们将使用与简单自编码器模型相同形状的输入:
input_layer = Input(shape=(1024,)) -
我们将在 256 自编码器阶段之前添加另一个层,这次使用 512 个神经元:
hidden_encoding = Dense(512, activation='relu')(input_layer) -
使用与之前练习相同大小的自编码器,但这次输入层是
hidden_encoding层:encoding_stage = Dense(256, activation='relu')(hidden_encoding) -
添加解码隐藏层:
hidden_decoding = Dense(512, activation='relu')(encoding_stage) -
使用与之前练习相同的输出阶段,这次连接到隐藏的解码阶段:
decoding_stage = Dense(1024, activation='sigmoid')(hidden_decoding) -
通过将网络的第一层和最后一层传递给
Model类来构建模型:autoencoder = Model(input_layer, decoding_stage) -
使用二元交叉熵损失函数和
adadelta梯度下降法编译自编码器:autoencoder.compile(loss='binary_crossentropy', optimizer='adadelta') -
现在,让我们拟合模型;再次,我们将图像作为训练数据和期望的输出。训练 100 个 epoch:
autoencoder.fit(images, images, epochs=100)输出如下:
![图 5.29: 训练模型]()
图 5.29: 训练模型
-
计算并存储前五个样本的编码阶段输出:
encoder_output = Model(input_stage, encoding_stage).predict(images[:5]) -
将编码器输出调整为 10 x 10(10 x 10 = 100)像素,并乘以 255:
encoder_output = encoder_output.reshape((-1, 10, 10)) * 255 -
计算并存储前五个样本的解码阶段输出:
decoder_output = autoencoder.predict(images[:5]) -
将解码器的输出调整为 28 x 28,并乘以 255:
decoder_output = decoder_output.reshape((-1, 28, 28)) * 255 -
绘制原始图像、编码器输出和解码器:
images = images.reshape((-1, 28, 28)) plt.figure(figsize=(10, 7)) for i in range(5): plt.subplot(3, 5, i + 1) plt.imshow(images[i], cmap='gray') plt.axis('off') plt.subplot(3, 5, i + 6) plt.imshow(encoder_output[i], cmap='gray') plt.axis('off') plt.subplot(3, 5, i + 11) plt.imshow(decoder_output[i], cmap='gray') plt.axis('off')输出如下:

图 5.30: 多层自编码器的输出
通过查看简单和多层自编码器产生的误差得分,并比较 图 5.27 和 图 5.30,我们可以看到这两种编码器结构的输出几乎没有区别。两幅图的中间行显示,两个模型学习到的特征实际上是不同的。我们可以使用许多选项来改进这两个模型,例如,训练更多的 epochs,使用不同数量的单元或神经元,或使用不同数量的层。这个练习的构建旨在展示如何构建和使用自编码器,但优化通常是一个系统性的试错过程。我们鼓励您调整模型的一些参数,并自己研究不同的结果。
卷积神经网络
在构建我们之前的所有神经网络模型时,您可能会注意到,我们在将图像转换为灰度图像时移除了所有颜色信息,并将每张图像展平为一个长度为 1,024 的单一向量。通过这种方式,我们本质上丢失了可能对我们有用的许多信息。图像中的颜色可能是特定于图像中的类别或物体的;此外,我们还丢失了许多关于图像的空间信息,例如,卡车图像中拖车相对于驾驶室的位置,或鹿的腿部相对于头部的位置。卷积神经网络不会遭遇这种信息丢失的问题。这是因为,卷积神经网络不是使用平坦的可训练参数结构,而是将权重存储在网格或矩阵中,这意味着每组参数可以在其结构中具有多个层。通过将权重组织成网格,我们防止了空间信息的丢失,因为这些权重以滑动的方式应用于图像。此外,通过拥有多个层,我们可以保留与图像相关的颜色通道。
在开发基于卷积神经网络的自编码器时,MaxPooling2D 和 Upsampling2D 层非常重要。MaxPooling2D 层通过在输入的窗口中选择最大值,来对输入矩阵在两个维度上进行降采样或减小尺寸。假设我们有一个 2 x 2 的矩阵,其中三个单元的值为 1,一个单元的值为 2:

图 5.31:示例矩阵演示
如果提供给 MaxPooling2D 层,这个矩阵会返回一个单一的 2 值,从而在两个方向上都将输入的尺寸减半。
UpSampling2D 层的作用与 MaxPooling2D 层相反,它增加了输入的尺寸,而不是减少它。上采样过程会重复数据的行和列,从而将输入矩阵的大小加倍。
练习 23:卷积自编码器
在本练习中,我们将开发一个基于卷积神经网络的自编码器,并将其性能与之前的全连接神经网络自编码器进行比较:
注意
你可以从github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson05/Exercise23下载data_batch_1文件。
-
导入
pickle、numpy、matplotlib,以及来自keras.models的Model类,和来自keras.layers的Input、Conv2D、MaxPooling2D、UpSampling2D:import pickle import numpy as np import matplotlib.pyplot as plt from keras.models import Model from keras.layers import Input, Conv2D, MaxPooling2D, UpSampling2D -
加载数据:
with open('data_batch_1', 'rb') as f: dat = pickle.load(f, encoding='bytes') -
由于这是一个无监督学习方法,我们只对图像数据感兴趣。根据之前的练习加载图像数据:
images = np.zeros((10000, 32, 32, 3), dtype='uint8') for idx, img in enumerate(dat[b'data']): images[idx, :, :, 0] = img[:1024].reshape((32, 32)) # Red images[idx, :, :, 1] = img[1024:2048].reshape((32, 32)) # Green images[idx, :, :, 2] = img[2048:].reshape((32, 32)) # Blue -
由于我们使用的是卷积网络,因此可以仅通过重新缩放图像来使用它们:
images = images / 255. -
定义卷积自编码器模型。我们将使用与图像相同形状的输入:
input_layer = Input(shape=(32, 32, 3,)) -
添加一个卷积层,包含 32 个层或滤波器,一个 3 x 3 的权重矩阵,ReLU 激活函数,并使用相同的填充方式,这意味着输出的尺寸与输入图像相同:
注意
hidden_encoding = Conv2D( 32, # Number of layers or filters in the weight matrix (3, 3), # Shape of the weight matrix activation='relu', padding='same', # How to apply the weights to the images )(input_layer) -
向编码器添加一个最大池化层,使用 2 x 2 的卷积核。
MaxPooling查看图像中的所有值,使用 2 x 2 的矩阵进行扫描。在每个 2 x 2 区域中,返回最大值,从而将编码层的尺寸减小一半:encoded = MaxPooling2D((2, 2))(hidden_encoding) -
添加一个解码卷积层(此层应与之前的卷积层相同):
hidden_decoding = Conv2D( 32, # Number of layers or filters in the weight matrix (3, 3), # Shape of the weight matrix activation='relu', padding='same', # How to apply the weights to the images )(encoded) -
现在我们需要将图像恢复到原始尺寸,因此我们将进行与
MaxPooling2D相同大小的上采样:upsample_decoding = UpSampling2D((2, 2))(hidden_decoding) -
添加最后的卷积层,为图像的 RGB 通道使用三个层:
decoded = Conv2D( 3, # Number of layers or filters in the weight matrix (3, 3), # Shape of the weight matrix activation='sigmoid', padding='same', # How to apply the weights to the images )(upsample_decoding) -
通过将网络的第一层和最后一层传递给
Model类来构建模型:autoencoder = Model(input_layer, decoded) -
显示模型的结构:
autoencoder.summary()请注意,与之前的自动编码器示例相比,我们的可训练参数要少得多。这是一个特定的设计决策,目的是确保该示例可以在各种硬件上运行。卷积网络通常需要更多的处理能力,并且经常需要特殊硬件,如图形处理单元(GPU)。
-
使用二元交叉熵损失函数和
adadelta梯度下降法编译自动编码器:autoencoder.compile(loss='binary_crossentropy', optimizer='adadelta') -
现在,让我们训练模型;再次地,我们将图像作为训练数据,并将其作为期望的输出。训练 20 个 epoch,因为卷积神经网络的计算需要更多时间:
autoencoder.fit(images, images, epochs=20)输出结果如下:
![图 5.32:训练模型]()
图 5.32:训练模型
请注意,在第二个 epoch 之后,误差已经比之前的自动编码器练习要小,这表明编码/解码模型有所改进。这种误差的减少主要归因于卷积神经网络没有丢弃太多数据,并且编码后的图像是 16 x 16 x 32,这比之前的 16 x 16 尺寸要大得多。此外,我们没有对图像进行压缩,因为它们现在包含的像素较少(16 x 16 x 32 = 8,192),但深度(32 x 32 x 3,072)比之前更多。这些信息已被重新排列,以便更有效地进行编码/解码过程。
-
计算并存储前五个样本的编码阶段输出:
encoder_output = Model(input_layer, encoded).predict(images[:5]) -
每个编码图像的形状为 16 x 16 x 32,这是由于为卷积阶段选择的滤波器数量。因此,我们不能在不进行修改的情况下直接可视化它们。我们将它们重塑为 256 x 32 大小以进行可视化:
encoder_output = encoder_output.reshape((-1, 256, 32)) -
获取前五个图像的解码器输出:
decoder_output = autoencoder.predict(images[:5]) -
绘制原始图像、均值编码器输出和解码器:
plt.figure(figsize=(10, 7)) for i in range(5): plt.subplot(3, 5, i + 1) plt.imshow(images[i], cmap='gray') plt.axis('off') plt.subplot(3, 5, i + 6) plt.imshow(encoder_output[i], cmap='gray') plt.axis('off') plt.subplot(3, 5, i + 11) plt.imshow(decoder_output[i]) plt.axis('off')输出结果如下:

图 5.33:原始图像、编码器输出和解码器
活动 11:MNIST 卷积自动编码器
在本次活动中,我们将通过使用 MNIST 数据集来加深对卷积自动编码器的理解。当使用合理大小的基于图像的数据集时,卷积自动编码器通常能显著提高性能。这在使用自动编码器生成人工图像样本时尤其有用:
-
导入
pickle、numpy和matplotlib,以及从keras.models中导入Model类,从keras.layers中导入Input、Conv2D、MaxPooling2D和UpSampling2D。 -
加载
mnist.pkl文件,该文件包含来自 MNIST 数据集的前 10,000 张图像及其对应标签,这些文件可在随附的源代码中找到。注意
你可以从
github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson05/Activity11下载mnist.pkl文件。 -
将图像重新缩放为值在 0 和 1 之间。
-
我们需要重塑图像,添加一个单一的深度通道,以便与卷积阶段一起使用。将图像重塑为 28 x 28 x 1 的形状。
-
定义输入层。我们将使用与图像相同形状的输入。
-
添加一个卷积阶段,使用 16 层或过滤器,3 x 3 的权重矩阵,ReLU 激活函数,并使用相同填充,这意味着输出的大小与输入图像相同。
-
在编码器中添加一个最大池化层,使用 2 x 2 的卷积核。
-
添加一个解码卷积层。
-
添加一个上采样层。
-
按照初始图像深度,使用 1 层添加最终的卷积阶段。
-
通过将网络的第一层和最后一层传递给
Model类来构建模型。 -
显示模型的结构。
-
使用二元交叉熵损失函数和
adadelta梯度下降法编译自编码器。 -
现在,让我们来拟合模型;再次将图像作为训练数据和期望的输出。训练 20 个周期,因为卷积网络需要较长的计算时间。
-
计算并存储编码阶段对前五个样本的输出。
-
为了可视化,将编码器的输出重塑为每张图像大小为 X*Y。
-
获取解码器对前五个图像的输出。
-
将解码器输出重塑为 28 x 28 的大小。
-
将原始图像重新调整回 28 x 28 的大小。
-
绘制原始图像、平均编码器输出和解码器。
在本次活动结束时,你将开发一个包含卷积层的自编码器神经网络。注意解码器表示的改进。输出将类似于以下内容:

图 5.34:期望的原始图像、编码器输出和解码器
注意
本活动的解决方案可以在第 340 页找到。
总结
在本章中,我们首先介绍了人工神经网络,它们的结构以及它们如何学习完成特定任务。从一个有监督学习的例子开始,我们构建了一个人工神经网络分类器来识别 CIFAR-10 数据集中的物体。然后,我们深入了解了神经网络的自编码器架构,学习了如何利用这些网络为无监督学习问题准备数据集。最后,我们完成了这项调查,查看了卷积神经网络以及这些附加层能带来的好处。通过这章内容,我们为接下来进行降维的最终章节做好了准备,学习如何使用和可视化通过 t-分布近邻(t-SNE)编码的数据。t-SNE 提供了一种非常有效的方法来可视化高维数据,即使在应用 PCA 等降维技术后也是如此。t-SNE 是无监督学习中特别有用的方法。
第六章:t-分布随机邻域嵌入(t-SNE)
学习目标
到本章结束时,你将能够:
-
描述并理解 t-SNE 背后的动机
-
描述 SNE 和 t-SNE 的推导过程
-
在 scikit-learn 中实现 t-SNE 模型
-
解释 t-SNE 的局限性
在本章中,我们将讨论随机邻域嵌入(SNE)和 t-分布随机邻域嵌入(t-SNE)作为可视化高维数据集的一种手段。
介绍
本章是关于降维技术和变换的微型系列的最后一篇。我们在本系列的前几章中描述了多种不同的降维方法,用于清理数据、提高计算效率或提取数据集中最重要的信息。虽然我们已经展示了许多降维高维数据集的方法,但在许多情况下,我们无法将维度数量减少到可以可视化的规模,即二维或三维,而不严重降低数据的质量。考虑我们在第五章《自编码器》中使用的 MNIST 数据集,它是 0 到 9 的手写数字的数字化集合。每个图像的大小为 28 x 28 像素,提供 784 个独立的维度或特征。如果我们将这 784 个维度降到 2 或 3 个以进行可视化,我们几乎会失去所有可用的信息。
在本章中,我们将讨论随机邻域嵌入(SNE)和 t-分布随机邻域嵌入(t-SNE)作为可视化高维数据集的一种手段。这些技术在无监督学习和机器学习系统设计中非常有用,因为数据的可视化是一种强大的工具。能够可视化数据可以帮助探索关系、识别群体并验证结果。t-SNE 技术已被用于可视化癌细胞核,其中有超过 30 种感兴趣的特征,而来自文档的数据可能有成千上万个维度,有时即使在应用 PCA 等技术后也如此。

图 6.1:MNIST 数据样本
在本章中,我们将使用 MNIST 数据集,并结合附带的源代码,通过实际示例来探索 SNE 和 t-SNE。在继续之前,我们将快速回顾 MNIST 及其包含的数据。完整的 MNIST 数据集包含 60,000 个训练样本和 10,000 个测试样本,所有样本为手写数字 0 到 9,表示为 28x28 像素大小的黑白(或灰度)图像(总计 784 个维度或特征),每种数字(或类别)的数量相等。由于其数据量大且质量高,MNIST 已成为机器学习中的经典数据集,通常作为许多研究论文中的参考数据集。与其他数据集相比,使用 MNIST 探索 SNE 和 t-SNE 的一个优势是,尽管样本具有较高的维度,但在降维后仍然可以可视化,因为它们可以表示为图像。图 6.1展示了一个 MNIST 数据集的样本,图 6.2展示了同一个样本,使用 PCA 降维到 30 个成分后的效果:

图 6.2:使用 PCA 将 MNIST 数据降维至 30 个成分
随机邻域嵌入(SNE)
随机邻域嵌入(SNE)是属于流形学习类别的众多方法之一,旨在将高维空间描述为低维流形或有界区域。一开始看,这似乎是一项不可能完成的任务;如果我们有一个至少包含 30 个特征的数据集,如何合理地在二维中表示数据呢?在我们推导 SNE 的过程中,期望你能看到这是如何实现的。别担心,我们不会深入探讨这一过程的数学细节,因为这些内容超出了本章的范围。构建 SNE 可以分为以下步骤:
-
将高维空间中数据点之间的距离转换为条件概率。假设我们有两个点,
和
,在高维空间中,我们想确定概率(
),即
将被选为
。为了定义这个概率,我们使用高斯曲线,可以看到对于邻近的点,概率较高,而对于远离的点,概率非常低。 -
我们需要确定高斯曲线的宽度,因为它控制概率选择的速率。宽曲线表明很多点距离较远,而窄曲线则表明点紧密集中。
-
一旦我们将数据投影到低维空间,我们还可以确定相应的概率 (
),这与对应的低维数据
和
之间的关系有关。 -
SNE 的目标是将数据定位到低维空间,以通过使用名为 Kullback-Leibler (KL) 散度的代价函数 (C),最小化
和
之间的差异,覆盖所有数据点:

图 6.3:Kullback-Leibler 散度。
注意
要构建高斯分布的 Python 代码,请参阅 github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/blob/master/Lesson06/GaussianDist.ipynb 中的 GaussianDist.ipynb Jupyter 笔记本。
高斯分布将数据映射到低维空间。为此,SNE 使用梯度下降过程来最小化 C,使用我们在上一章中讲解过的标准学习率和迭代次数参数,回顾了神经网络和自编码器的内容。SNE 在训练过程中引入了一个额外的术语——困惑度。困惑度是用来选择有效邻居数量的参数,对于困惑度值在 5 到 50 之间时,效果相对稳定。实际上,建议使用该范围内的困惑度值进行反复试验。
SNE 提供了一种有效的方式将高维数据可视化到低维空间中,尽管它仍然面临一个被称为 拥挤问题 的问题。如果我们有一些点在某个点 i 周围大致等距离地分布,就可能出现拥挤问题。当这些点被可视化到低维空间时,它们会紧密聚集在一起,导致可视化困难。如果我们试图让这些拥挤的点之间保持更大的间距,问题会加剧,因为任何远离这些点的其他点都会被置于低维空间的非常远的位置。实质上,我们在尝试平衡能够可视化接近的点,同时又不丢失远离点提供的信息。
t-分布 SNE
t-SNE 通过修改 KL 散度代价函数,并用学生 t 分布代替低维空间中的高斯分布来解决拥挤问题。学生 t 分布是一种连续分布,通常在样本量较小且未知总体标准差时使用,广泛应用于学生 t 检验中。
修改后的 KL 代价函数将低维空间中的成对距离视为相等,而学生分布在低维空间中采用重尾分布来避免拥挤问题。在高维概率计算中,仍然使用高斯分布,确保高维中的适度距离在低维中也能得到相应的表示。不同分布在各自空间中的组合使得在小距离和中等距离分离的数据点能够得到真实的表示。
注意
有关如何在 Python 中重现学生 t 分布的示例代码,请参考 Jupyter notebook:github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/blob/master/Lesson06/StudentTDist.ipynb。
幸运的是,我们不需要手动实现 t-SNE,因为 scikit-learn 提供了一个非常有效的实现,且 API 简单明了。我们需要记住的是,SNE 和 t-SNE 都是通过计算两个点在高维空间和低维空间中作为邻居的概率,旨在最小化这两个空间之间的概率差异。
练习 24:t-SNE MNIST
在本练习中,我们将使用 MNIST 数据集(随附源代码提供)来探索 scikit-learn 对 t-SNE 的实现。如前所述,使用 MNIST 使我们能够以其他数据集(如波士顿住房价格或鸢尾花数据集)无法实现的方式来可视化高维空间:
-
对于此练习,导入
pickle、numpy、PCA和TSNE来自 scikit-learn,以及matplotlib:import pickle import numpy as np import matplotlib.pyplot as plt from sklearn.decomposition import PCA from sklearn.manifold import TSNE -
加载并可视化随附源代码提供的 MNIST 数据集:
注意
with open('mnist.pkl', 'rb') as f: mnist = pickle.load(f) plt.figure(figsize=(10, 7)) for i in range(9): plt.subplot(3, 3, i + 1) plt.imshow(mnist['images'][i], cmap='gray') plt.title(mnist['labels'][i]) plt.axis('off') plt.show()输出如下:
![图 6.4:加载数据集后的输出]()
图 6.4:加载数据集后的输出
这表明 MNIST 数据集已成功加载。
-
在本练习中,我们将对数据集使用 PCA 降维,只提取前 30 个成分。
注意
model_pca = PCA(n_components=30) mnist_pca = model_pca.fit(mnist['images'].reshape((-1, 28 ** 2))) -
可视化将数据集降至 30 个成分的效果。为此,我们必须首先将数据集转换到低维空间,然后使用
inverse_transform方法将数据返回到原始大小,以便进行绘图。当然,我们还需要在转换过程前后调整数据的形状:mnist_30comp = model_pca.transform(mnist['images'].reshape((-1, 28 ** 2))) mnist_30comp_vis = model_pca.inverse_transform(mnist_30comp) mnist_30comp_vis = mnist_30comp_vis.reshape((-1, 28, 28)) plt.figure(figsize=(10, 7)) for i in range(9): plt.subplot(3, 3, i + 1) plt.imshow(mnist_30comp_vis[i], cmap='gray') plt.title(mnist['labels'][i]) plt.axis('off') plt.show()输出如下:
![图 6.5:可视化数据集降维的效果]()
图 6.5:可视化减少数据集的效果
请注意,虽然我们在图像中丧失了一些清晰度,但由于降维过程,大部分数字仍然相当清晰可见。有趣的是,数字四(4)似乎是受此过程影响最大的。也许 PCA 过程丢弃的许多信息包含了特定于数字四(4)样本的信息。
-
现在,我们将 t-SNE 应用于 PCA 转换后的数据,以在二维空间中可视化 30 个组件。我们可以使用 scikit-learn 中的标准模型 API 接口来构建 t-SNE 模型。我们将首先使用默认值,这些值指定我们将 30 维数据嵌入到二维空间中进行可视化,使用 30 的困惑度、200 的学习率和 1000 次迭代。我们将指定
random_state值为 0,并将verbose设置为 1:model_tsne = TSNE(random_state=0, verbose=1) model_tsne输出结果如下:
![图 6.6:将 t-SNE 应用于 PCA 转换后的数据]()
图 6.6:将 t-SNE 应用于 PCA 转换后的数据
在之前的截图中,我们可以看到 t-SNE 模型的多个配置选项,其中一些比其他选项更为重要。我们将重点关注
learning_rate、n_components、n_iter、perplexity、random_state和verbose的值。对于learning_rate,如前所述,t-SNE 使用随机梯度下降将高维数据投影到低维空间。学习率控制过程执行的速度。如果学习率过高,模型可能无法收敛到解决方案;如果学习率过低,可能需要很长时间才能收敛(如果能够收敛)。一个好的经验法则是从默认值开始;如果你发现模型产生了 NaN(非数值),则可能需要降低学习率。一旦你对模型满意,也可以降低学习率并让其运行更长时间(增加 n_iter);事实上,这样可能会得到略微更好的结果。n_components是嵌入(或可视化空间)的维度数量。通常,你希望获得数据的二维图,因此只需使用默认值2。n_iter是梯度下降的最大迭代次数。perplexity,如前一节所述,是在可视化数据时使用的邻居数量。通常,介于 5 到 50 之间的值是合适的,因为较大的数据集通常需要比较小的数据集更多的困惑度(perplexity)。
random_state是任何模型或算法中一个重要的变量,它在训练开始时会随机初始化其值。计算机硬件和软件工具提供的随机数生成器实际上并非真正的随机数生成器;它们实际上是伪随机数生成器。它们提供了一个良好的随机性近似值,但并不是真正的随机。计算机中的随机数是从一个称为种子的值开始的,之后通过复杂的方式生成。通过在过程开始时提供相同的种子,每次运行该过程时都会生成相同的“随机数”。虽然这听起来不直观,但对于复现机器学习实验来说,这非常有用,因为你不会看到仅仅由于参数初始化的不同而导致的性能差异。这可以提供更多的信心,认为性能变化是由于模型或训练的某些改变,例如神经网络的架构。注意
产生真正的随机序列实际上是计算机最难实现的任务之一。计算机的软件和硬件设计是为了每次执行时按完全相同的方式运行指令,从而得到相同的结果。执行中的随机差异,虽然对于生成随机数序列来说理想,但在自动化任务和调试问题时会造成噩梦。
verbose是模型的详细程度,描述了在模型拟合过程中打印到屏幕上的信息量。值为 0 表示没有输出,而 1 或更大的值表示输出中详细信息的增加。 -
使用 t-SNE 转换 MNIST 的分解数据集:
mnist_tsne = model_tsne.fit_transform(mnist_30comp)输出如下:
![图 6.7:转换分解数据集]()
图 6.7:转换分解数据集
在拟合过程中提供的输出能帮助我们了解 scikit-learn 完成的计算。我们可以看到它正在为所有样本进行索引和计算邻居,然后再批量地计算数据作为邻居的条件概率,每次批次为 10。过程结束时,它提供了一个标准差(方差)均值为 304.9988,且在梯度下降的 250 和 1,000 次迭代后得到了 KL 散度。
-
现在,可视化返回数据集中的维度数量:
mnist_tsne.shape输出如下:
1000,2所以,我们成功地将 784 个维度降到了 2 维进行可视化,那么它看起来是什么样的呢?
-
创建模型生成的二维数据的散点图:
plt.figure(figsize=(10, 7)) plt.scatter(mnist_tsne[:,0], mnist_tsne[:,1], s=5) plt.title('Low Dimensional Representation of MNIST');输出如下:
![图 6.8:MNIST 的二维表示(无标签)。]()
图 6.8:MNIST 的二维表示(无标签)。
在 图 6**.8 中,我们可以看到我们已经将 MNIST 数据表示为二维,但我们也可以看到它似乎被分组在一起。这里有很多不同的数据聚类或团块,它们与其他聚类通过一些空白区域分开。似乎大约有九个不同的数据组。所有这些观察结果表明,个别聚类之间以及聚类内可能存在某种关系。
-
绘制按图像标签分组的二维数据,并使用标记区分各个标签。结合数据,在图上添加图像标签,以研究嵌入数据的结构:
MARKER = ['o', 'v', '1', 'p' ,'*', '+', 'x', 'd', '4', '.'] plt.figure(figsize=(10, 7)) plt.title('Low Dimensional Representation of MNIST'); for i in range(10): selections = mnist_tsne[mnist['labels'] == i] plt.scatter(selections[:,0], selections[:,1], alpha=0.2, marker=MARKER[i], s=5); x, y = selections.mean(axis=0) plt.text(x, y, str(i), fontdict={'weight': 'bold', 'size': 30}) plt.show()输出结果如下:
![图 6.9:带标签的 MNIST 二维表示。]()
图 6.9:带标签的 MNIST 二维表示。
图 6.9 非常有趣!我们可以看到,数据集中的不同图像类别(从零到九)对应着不同的聚类。在无监督的情况下,即没有提前提供标签,PCA 和 t-SNE 的结合成功地将 MNIST 数据集中的各个类别分开并进行分组。特别有趣的是,数据中似乎存在一些混淆,尤其是数字四和数字九的图像,以及数字五和数字三的图像;这两个聚类有些重叠。如果我们查看从 步骤 4、练习 24 和 t-SNE MNIST 提取的数字九和数字四的 PCA 图像,这就可以理解:
![图 6.10:数字九的 PCA 图像。]()
图 6.10:数字九的 PCA 图像。
它们实际上看起来非常相似;也许是因为数字四的形状存在不确定性。看一下接下来的图像,我们可以看到左侧的数字四,两条垂直线几乎连接,而右侧的数字四则是两条平行线:
![图 6.11:数字四的形状]()
图 6.11:数字四的形状
在 图 6.9 中需要注意的另一个有趣特征是边缘案例,这在 Jupyter 笔记本中通过颜色显示得更清楚。我们可以看到每个聚类的边缘附近,一些样本在传统的监督学习中会被误分类,但它们实际上可能与其他聚类更为相似。让我们看一个例子;有许多数字三的样本,它们离正确的聚类相当远。
-
获取数据集中所有数字三的索引:
threes = np.where(mnist['labels'] == 3)[0] threes输出结果如下:
![图 6.12:数据集中三的索引。]()
图 6.12:数据集中三的索引。
-
查找
x值小于 0 的三类数据:tsne_threes = mnist_tsne[threes] far_threes = np.where(tsne_threes[:,0]< 0)[0] far_threes输出结果如下:
![图 6.13:x 值小于零的三类数据。]()
图 6.13:x 值小于零的三类
-
显示坐标以找到一个合理远离三的聚类的样本:
tsne_threes[far_threes]输出结果如下:
![图 6.14:远离三的聚类的坐标]()
图 6.14:远离三的聚类的坐标
-
选择一个具有较高负值的
x坐标的样本。在这个示例中,我们将选择第四个样本,即样本 10。显示该样本的图像:plt.imshow(mnist['images'][10], cmap='gray') plt.axis('off'); plt.show()输出结果如下:

图 6.15:样本十的图像
看着这个示例图像及其对应的 t-SNE 坐标,大约是(-8, 47),不难理解这个样本为何会靠近八和五的聚类,因为在这张图像中,八和五这两个数字有很多相似的特征。在这个示例中,我们应用了简化版的 SNE,展示了它的一些高效性以及可能的混淆源和无监督学习的输出结果。
注意
即使提供了随机数种子,t-SNE 也不能保证每次执行时输出完全相同,因为它基于选择概率。因此,您可能会注意到,内容中提供的示例与您的实现之间在细节上有所不同。尽管具体细节可能有所不同,但整体原则和技术依然适用。从实际应用的角度来看,建议多次重复该过程,以从数据中辨别出重要信息。
活动 12:葡萄酒 t-SNE
在本活动中,我们将通过使用葡萄酒数据集来巩固我们对 t-SNE 的理解。完成此活动后,您将能够为自己的自定义应用程序构建 SNE 模型。葡萄酒数据集 (archive.ics.uci.edu/ml/datasets/Wine) 是关于来自意大利三家不同生产商的葡萄酒化学分析的属性集合,但每个生产商的葡萄酒类型相同。此信息可作为示例,用于验证瓶装葡萄酒是否来自意大利特定地区的葡萄。13 个属性包括:酒精、苹果酸、灰分、灰的碱度、镁、总酚、类黄酮、非类黄酮酚、前花青素、颜色强度、色调、稀释酒的 OD280/OD315 比值,以及脯氨酸。
每个样本包含一个类别标识符(1 – 3)。
注意
本数据集来源于 archive.ics.uci.edu/ml/machine-learning-databases/wine/,可以从 github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson06/Activity12 下载。
UCI 机器学习库 [http://archive.ics.uci.edu/ml]。加利福尼亚州尔湾:加利福尼亚大学信息与计算机科学学院。
以下步骤将帮助您完成活动:
-
导入
pandas、numpy、matplotlib以及 scikit-learn 中的t-SNE和PCA模型。 -
使用随附源代码中的
wine.data文件加载 Wine 数据集,并显示前五行数据。注意
您可以通过使用
del关键字删除 Pandas DataFrame 中的列。只需将del和所选列放在方括号内。 -
第一列包含标签;提取该列并将其从数据集中移除。
-
执行 PCA,将数据集降至前六个主成分。
-
确定这六个成分所描述的数据中的方差量。
-
使用指定的随机状态和
verbose值为 1 创建 t-SNE 模型。 -
将 PCA 数据拟合到 t-SNE 模型中。
-
确认 t-SNE 拟合数据的形状是二维的。
-
创建二维数据的散点图。
-
创建一个二位数据的散点图,并应用类标签,以可视化可能存在的聚类。
在本活动结束时,您将构建一个基于 Wine 数据集六个成分的 t-SNE 可视化,并识别图中数据位置的一些关系。最终的图将类似于以下内容:

图 6.16:预期的图示
注意
本活动的解决方案可以在第 345 页找到。
在本节中,我们介绍了生成 SNE 图示的基础知识。在低维空间中表示高维数据的能力至关重要,特别是对于深入理解手头数据至关重要。有时,这些图示的解释可能会有些棘手,因为确切的关系有时会相互矛盾,导致误导性结构。
解释 t-SNE 图示
现在我们可以使用 t-分布 SNE 来可视化高维数据,重要的是理解此类图示的局限性,以及在解释和生成这些图示时需要关注的方面。在本章的这一部分,我们将突出 t-SNE 的一些重要特性,并演示在使用该可视化技术时应注意的事项。
困惑度
如 t-SNE 介绍中所述,困惑度值指定用于计算条件概率的最近邻数量。选择该值对最终结果有显著影响;当困惑度值较低时,数据中的局部变化占主导,因为计算中使用的样本数量较少。相反,较大的困惑度值会考虑更多的全局变化,因为使用了更多的样本进行计算。通常,尝试不同的值以调查困惑度的效果是值得的。通常,困惑度值在 5 到 50 之间的效果较好。
练习 25:t-SNE MNIST 与困惑度
在这个练习中,我们将尝试不同的困惑度值,并查看它在可视化图中的效果:
-
导入
pickle、numpy、matplotlib,以及来自 scikit-learn 的PCA和t-SNE:import pickle import numpy as np import matplotlib.pyplot as plt from sklearn.decomposition import PCA from sklearn.manifold import TSNE -
加载 MNIST 数据集:
注意
with open('mnist.pkl', 'rb') as f: mnist = pickle.load(f) -
使用 PCA,从图像数据中选择前 30 个方差成分:
model_pca = PCA(n_components=30) mnist_pca = model_pca.fit_transform(mnist['images'].reshape((-1, 28 ** 2))) -
在这个练习中,我们正在研究困惑度对 t-SNE 流形的影响。通过一个困惑度为 3、30 和 300 的模型/绘图循环进行迭代:
MARKER = ['o', 'v', '1', 'p' ,'*', '+', 'x', 'd', '4', '.'] for perp in [3, 30, 300]: model_tsne = TSNE(random_state=0, verbose=1, perplexity=perp) mnist_tsne = model_tsne.fit_transform(mnist_pca) plt.figure(figsize=(10, 7)) plt.title(f'Low Dimensional Representation of MNIST (perplexity = {perp})'); for i in range(10): selections = mnist_tsne[mnist['labels'] == i] plt.scatter(selections[:,0], selections[:,1], alpha=0.2, marker=MARKER[i], s=5); x, y = selections.mean(axis=0) plt.text(x, y, str(i), fontdict={'weight': 'bold', 'size': 30})输出结果如下:

图 6.17:通过模型进行迭代
请注意三个不同困惑度值下的 KL 散度,以及平均标准差(方差)的增加。通过查看以下三个带有类标签的 t-SNE 图,我们可以看到,当困惑度值较低时,聚类被很好地分隔,重叠较少。然而,聚类之间几乎没有空间。当我们增加困惑度时,聚类之间的空间改善,在困惑度为 30 时有相对清晰的区分。随着困惑度增加到 300,我们可以看到 8 和 5 的聚类,以及 9、4 和 7 的聚类,开始趋于融合。
从低困惑度值开始:

图 6.18:低困惑度值的绘图
将困惑度增加 10 倍后,聚类变得更加清晰:

图 6.19:困惑度增加 10 倍后的绘图
将困惑度增加到 300 后,我们开始将更多的标签合并在一起:

图 6.20:将困惑度值增加到 300
在这个练习中,我们加深了对困惑度影响及其对整体结果敏感性的理解。较小的困惑度值可能导致位置之间的混合更加均匀,且它们之间的空间非常小。增加困惑度可以更有效地分离聚类,但过大的值会导致聚类重叠。
活动 13:t-SNE 葡萄酒和困惑度
在这个活动中,我们将使用葡萄酒数据集进一步强化困惑度对 t-SNE 可视化过程的影响。在这个活动中,我们尝试确定是否可以根据葡萄酒的化学成分识别其来源。t-SNE 过程提供了一种有效的表示方法,可能帮助识别来源。
注意
该数据集来自于archive.ics.uci.edu/ml/machine-learning-databases/wine/,可以从github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson06/Activity13 下载。
UCI 机器学习库 [http://archive.ics.uci.edu/ml]。加利福尼亚州欧文市:加利福尼亚大学信息与计算机科学学院。
-
导入
pandas、numpy、matplotlib以及来自 scikit-learn 的t-SNE和PCA模型。 -
加载 Wine 数据集并检查前五行数据。
-
第一列提供了标签;从 DataFrame 中提取这些标签并存储在单独的变量中。确保该列从 DataFrame 中移除。
-
对数据集执行 PCA 操作,并提取前六个主成分。
-
构建一个循环,遍历不同的困惑度值(1、5、20、30、80、160、320)。对于每次循环,生成一个带有相应困惑度的 t-SNE 模型,并打印带标签的葡萄酒类别的散点图。注意不同困惑度值的效果。
在本活动结束时,你将生成 Wine 数据集的二维表示,并检查生成的图表,寻找数据的聚类或分组。
注意
该活动的解决方案可以在第 348 页找到。
迭代次数
我们将要实验研究的最后一个参数是迭代次数,正如我们在自动编码器中的研究所示,这只是应用于梯度下降的训练轮次数量。幸运的是,迭代次数是一个相对简单的参数,通常只需要一定的耐心,因为低维空间中点的位置会在最终位置上稳定下来。
练习 26:t-SNE MNIST 和迭代次数
在这个练习中,我们将观察一系列不同的迭代参数对 t-SNE 模型的影响,并突出一些可能需要更多训练的指示符。再次强调,这些参数的值在很大程度上依赖于数据集以及可用于训练的数据量。在本示例中,我们仍然使用 MNIST 数据集:
-
导入
pickle、numpy、matplotlib以及来自 scikit-learn 的 PCA 和 t-SNE:import pickle import numpy as np import matplotlib.pyplot as plt from sklearn.decomposition import PCA from sklearn.manifold import TSNE -
加载 MNIST 数据集:
注意
with open('mnist.pkl', 'rb') as f: mnist = pickle.load(f) -
使用 PCA,从图像数据中仅选择前 30 个方差成分:
model_pca = PCA(n_components=30) mnist_pca = model_pca.fit_transform(mnist['images'].reshape((-1, 28 ** 2))) -
在这个练习中,我们将研究迭代次数对 t-SNE 流形的影响。通过模型/绘图循环,进行迭代,迭代次数分别为
250、500和1000:MARKER = ['o', 'v', '1', 'p' ,'*', '+', 'x', 'd', '4', '.'] for iterations in [250, 500, 1000]: model_tsne = TSNE(random_state=0, verbose=1, n_iter=iterations, n_iter_without_progress=iterations) mnist_tsne = model_tsne.fit_transform(mnist_pca) -
绘制结果:
plt.figure(figsize=(10, 7)) plt.title(f'Low Dimensional Representation of MNIST (iterations = {iterations})'); for i in range(10): selections = mnist_tsne[mnist['labels'] == i] plt.scatter(selections[:,0], selections[:,1], alpha=0.2, marker=MARKER[i], s=5); x, y = selections.mean(axis=0) plt.text(x, y, str(i), fontdict={'weight': 'bold', 'size': 30})迭代次数较少会限制算法找到相关邻居的程度,导致聚类不清晰:
![图 6.21:250 次迭代后的绘图]
图 6.21:250 次迭代后的绘图
增加迭代次数为算法提供了足够的时间来充分投影数据:

图 6.22:将迭代次数增加到 500 后的绘图
一旦簇群稳定,增加迭代次数的影响非常小,基本上只是增加了训练时间:

图 6.23:1000 次迭代后的绘图
从前面的绘图来看,我们可以看到,迭代次数为 500 和 1000 时,簇群的位置稳定且在各个图之间几乎没有变化。最有趣的图是迭代次数为 250 的图,其中簇群似乎仍在移动过程中,正在向最终位置靠拢。因此,有充分的证据表明,500 次迭代足以。
活动 14:t-SNE 葡萄酒与迭代次数
在本活动中,我们将研究迭代次数对葡萄酒数据集可视化的影响。这是数据处理、清洗和理解数据关系的探索阶段中常用的一个过程。根据数据集和分析类型,我们可能需要尝试多种不同的迭代次数,就像本次活动中所做的那样。
注意
此数据集来自 archive.ics.uci.edu/ml/machine-learning-databases/wine/。它可以从 github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson06/Activity14 下载。
UCI 机器学习库 [http://archive.ics.uci.edu/ml]。加利福尼亚州欧文市:加利福尼亚大学信息与计算机科学学院。
以下步骤将帮助你完成活动:
-
导入
pandas、numpy、matplotlib,以及从 scikit-learn 导入t-SNE和PCA模型。 -
加载葡萄酒数据集并检查前五行数据。
-
第一列提供了标签;从 DataFrame 中提取这些标签并存储到一个单独的变量中。确保将该列从 DataFrame 中删除。
-
对数据集执行 PCA,并提取前六个主成分。
-
构建一个循环,遍历迭代值(
250、500、1000)。对于每个循环,生成一个具有相应迭代次数的 t-SNE 模型,并生成一个没有进度值的相同迭代次数的模型。 -
构建标记葡萄酒类别的散点图。注意不同迭代值的影响。
通过完成本活动,我们将研究修改模型迭代参数的效果。这是确保数据在低维空间中稳定在某个最终位置的重要参数。
注意
本活动的解决方案可以在第 353 页找到。
关于可视化的最终思考
在我们总结关于 t-分布随机邻域嵌入(t-SNE)这一章节时,有几个关于可视化的重要方面需要注意。首先,聚类的大小或聚类之间的相对空间可能并不能真正反映接近度。正如我们在本章前面讨论的,结合高斯分布和学生 t 分布被用来在低维空间中表示高维数据。因此,距离之间的线性关系并不能得到保证,因为 t-SNE 平衡了局部和全局数据结构的位置。局部结构中点之间的实际距离在可视化表示中可能看起来非常接近,但在高维空间中可能仍然存在一定的距离。
这个特性还有一个附带的后果,那就是有时随机数据看起来像是具有某种结构,并且通常需要生成多个可视化图像,使用不同的困惑度、学习率、迭代次数和随机种子值。
总结
在这一章中,我们介绍了 t-分布随机邻域嵌入(t-SNE)作为可视化高维信息的一种方法,这些信息可能来自先前的过程,如 PCA 或自编码器。我们讨论了 t-SNE 如何生成这种表示,并使用 MNIST 和 Wine 数据集以及 scikit-learn 生成了多个表示。在这一章中,我们能够看到无监督学习的一些强大之处,因为 PCA 和 t-SNE 能够在不知道真实标签的情况下对每张图片的类别进行聚类。在下一章中,我们将基于这次实践经验,探讨无监督学习的应用,包括篮子分析和主题建模。
第七章:主题建模
学习目标
本章结束时,您将能够:
-
为文本数据执行基本的清理技术
-
评估隐含狄利克雷分配模型
-
执行非负矩阵分解模型
-
解释主题模型的结果
-
为给定场景识别最佳主题模型
在本章中,我们将看到如何通过主题建模深入了解文档的潜在结构。
引言
主题建模是自然语言处理(NLP)的一个方面,这是计算机科学领域探索计算机与人类语言关系的一个领域,随着文本数据集的增加可用性的增加而日益流行。NLP 可以处理几乎任何形式的语言,包括文本、语音和图像。除了主题建模外,情感分析、对象字符识别和词汇语义是值得注意的 NLP 算法。如今,收集和需要分析的数据很少是以标准表格形式出现,而更频繁地以较少结构化的形式出现,包括文档、图像和音频文件。因此,成功的数据科学从业者需要精通处理这些多样化数据集的方法论。
下面是一个示例,演示如何识别文本中的单词并将其分配给主题:

图 7.1:示例,识别文本中的单词并将它们分配给主题
你可能立即问的问题是什么是主题?让我们通过一个例子来回答这个问题。你可以想象,或者可能已经注意到,在发生重大事件的日子,例如国家选举、自然灾害或体育冠军赛,社交媒体网站上的帖子往往会集中在这些事件上。帖子通常以某种方式反映当天的事件,并且会以不同的方式这样做。帖子可以并且将会有许多不同的观点。如果我们有关于世界杯决赛的推文,这些推文的主题可能涵盖各种不同的观点,从裁判的裁判质量到球迷行为。在美国,总统在每年的一月中旬到晚些时候发表一次称为国情咨文的年度演讲。通过足够数量的社交媒体帖子,我们将能够通过使用其中包含的特定关键词对帖子进行分组来推断或预测社交媒体社区对演讲的高级反应 - 主题。
主题模型
主题模型属于无监督学习类别,因为几乎总是无法事先知道要识别的主题。因此,没有目标可以进行回归或分类建模。在无监督学习中,主题模型最像聚类算法,特别是 k-means 聚类。你会记得,在 k-means 聚类中,首先确定聚类的数量,然后模型将每个数据点分配到预定的某个聚类中。主题模型通常也是如此。我们在开始时选择主题的数量,然后模型会提取出构成这些主题的词汇。这是一个很好的出发点,用于对主题建模进行高层次的概述。
在此之前,我们先检查一下是否安装了正确的环境和库,并准备好使用。下表列出了所需的库及其主要用途:

图 7.2:显示不同库及其用途的表格
练习 27:设置环境
为了检查环境是否准备好进行主题建模,我们将执行几个步骤。其中第一步是加载本章将需要的所有库:
注意
如果这些库中有任何一个或全部未安装,请通过命令行使用pip安装所需的软件包。例如,若未安装,运行pip install langdetect。
-
打开一个新的 Jupyter 笔记本。
-
导入所需的库:
import langdetect import matplotlib.pyplot import nltk import numpy import pandas import pyLDAvis import pyLDAvis.sklearn import regex import sklearn请注意,并非所有这些库都用于清理数据;其中一些库用于实际建模,但一次性导入所有所需的库是很方便的,所以我们现在就将所有库的导入处理好。
-
尚未安装的库将返回以下错误:
![图 7.3:库未安装错误]()
图 7.3:库未安装错误
如果返回此错误,请按照之前讨论的方法通过命令行安装相关库。成功安装后,使用
import重新运行库导入过程。 -
某些文本数据清理和预处理过程需要词典(更多内容稍后介绍)。在此步骤中,我们将安装其中的两个词典。如果导入了
nltk库,可以执行以下代码:nltk.download('wordnet') nltk.download('stopwords')输出如下:
![]()
图 7.4:导入库并下载词典
-
运行
matplotlib魔法命令并指定 inline,以便图表显示在笔记本中:%matplotlib inline
笔记本和环境已设置好,准备好加载数据。
主题模型的高层次概述
在分析大量可能相关的文本数据时,主题模型是一个常用方法。所谓相关,是指文档理想情况下来自相同来源。也就是说,调查结果、推文和新闻文章通常不会在同一个模型中同时进行分析。当然,也可以将它们一起分析,但结果可能非常模糊,因此没有意义。运行任何主题模型的唯一数据要求是文档本身。不需要额外的数据,无论是元数据还是其他类型的数据。
简单来说,主题模型通过分析文档中的词汇,识别一组文档(称为语料库)中的抽象主题(也称为主题)。也就是说,如果一句话中包含“薪水”、“员工”和“会议”等词汇,就可以推测这句话的主题是与工作相关的。需要注意的是,构成语料库的文档不一定是传统意义上的文档——可以是信件或合同等。文档可以是任何包含文本的内容,包括推文、新闻标题或转录的演讲。
主题模型假设同一文档中的词汇是相关的,并利用这一假设通过寻找反复出现在相近位置的词汇组来定义抽象主题。通过这种方式,这些模型属于经典的模式识别算法,其中被检测到的模式是由词汇组成的。一般的主题建模算法包含四个主要步骤:
-
确定主题的数量。
-
扫描文档并识别共现的词汇或短语。
-
自动学习表征文档的词汇组(或集群)。
-
输出表征语料库的抽象主题,作为词汇组合。
正如步骤 1 所示,在拟合模型之前需要选择主题的数量。选择合适的主题数量可能会有些棘手,但与大多数机器学习模型一样,这个参数可以通过拟合多个使用不同主题数量的模型,并基于某些性能指标选择最佳模型来优化。稍后我们会再深入讨论这个过程。
以下是通用主题建模工作流程:

图 7.5: 通用主题建模工作流程
选择最佳的主题数非常重要,因为这一参数会对主题的连贯性产生重大影响。这是因为模型会在预定义的主题数限制下,找到最适合语料库的词组。如果主题数过高,主题会变得过于狭窄。过于具体的主题被称为过度加工。同样地,如果主题数过低,主题会变得过于通用和模糊。这些类型的主题被认为是加工不足。过度加工和加工不足的主题有时可以通过分别减少或增加主题数来修正。话题模型的一个常见且不可避免的结果是,通常至少有一个话题会出现问题。
话题模型的一个关键方面是,它们不会生成特定的单词或短语主题,而是生成一组词汇,每个词汇代表一个抽象的主题。回想一下之前关于工作的假设句子。构建出来的主题模型识别该句子所属的假设语料库的主题时,不会返回单词“work”作为主题。它会返回一组词汇,例如“paycheck”(薪水)、“employee”(员工)和“boss”(老板);这些词汇描述了主题,并可以推断出一个单词或短语主题。这是因为话题模型理解的是词汇的接近度,而不是上下文。模型并不理解“paycheck”、“employee”和“boss”的含义;它只知道这些词汇通常在一起出现并且彼此接近:

图 7.6:从词组中推断主题
话题模型可以用来预测未见文档的主题,但如果你打算做预测,重要的是要认识到,话题模型只知道用来训练它们的词汇。也就是说,如果未见文档中包含训练数据中没有的词汇,模型将无法处理这些词汇,即使它们与训练数据中已识别的某个主题相关。因此,话题模型更多地用于探索性分析和推断,而不是预测。
每个话题模型输出两个矩阵。第一个矩阵包含词汇与主题的关系。它列出了每个词汇与每个主题的关联,并对这种关系进行量化。考虑到模型正在考虑的词汇数量,每个主题只会由相对较少的词汇描述。词汇可以被分配给一个主题,或者分配给多个主题,并在不同的量化下表示。词汇是被分配给一个主题还是多个主题,取决于算法。同样,第二个矩阵包含文档与主题的关系。它列出了与某个主题相关的每个文档,并对每个文档与该主题的关联程度进行量化。
在讨论主题建模时,重要的是要不断强调,代表主题的词汇组之间并不是概念上的关联,而仅仅是通过词汇的接近度来关联的。文档中某些单词的频繁接近足以定义主题,这是因为前面提到的假设——同一文档中的所有单词都是相关的。然而,这个假设可能并不成立,或者这些单词可能过于泛化,无法形成连贯的主题。解释抽象的主题涉及将文本数据的固有特性与生成的词汇组进行平衡。文本数据和语言一般来说是高度可变的、复杂的,并且具有上下文,这意味着任何普遍的结果都需要谨慎对待。这并不是贬低或否定模型的结果。在彻底清理过的文档和适当数量的主题下,词汇组,如我们将看到的,能够很好地指导语料库的内容,并且可以有效地融入到更大的数据系统中。
我们已经讨论了一些主题模型的局限性,但还有一些额外的注意事项。文本数据的噪声特性可能会导致主题模型将与某个主题无关的单词错误地分配给该主题。再考虑之前提到的关于工作的句子。单词 meeting 可能出现在代表“工作”主题的词汇组中。也有可能单词 long 也会出现在该组中,但单词 long 与工作没有直接关系。long 之所以可能出现在该组,是因为它常常出现在 meeting 旁边。因此,long 可能会被认为与工作虚假相关,并且如果可能的话,应该从该主题词汇组中移除。词汇组中的虚假相关词可能会在分析数据时引起严重问题。
这不一定是模型的缺陷,而是模型的一个特性,在面对噪声数据时,可能会从数据中提取出一些特征,这些特征可能会对结果产生负面影响。虚假的相关性可能是由于数据的收集方式、地点或时间造成的。如果文档仅在某些特定的地理区域内收集,那么与该区域相关的词汇可能会不正确地、尽管是偶然地,链接到模型输出的一个或多个词汇组。请注意,随着词汇组中单词数量的增加,我们可能会将更多的文档归类到该主题中,超出应归类的范围。显然,如果我们减少属于某一主题的词汇数量,那么该主题将会被分配给更少的文档。记住,这并不是什么坏事。我们希望每个词汇组只包含有意义的单词,从而将合适的主题分配给合适的文档。
有许多主题建模算法,但也许最为人熟知的两种是潜在狄利克雷分配(Latent Dirichlet Allocation)和非负矩阵分解(Non-Negative Matrix Factorization)。我们稍后将详细讨论这两种算法。
商业应用
尽管存在局限性,但如果在正确的上下文中正确使用,主题建模可以提供可操作的洞察,推动商业价值。现在,让我们回顾一下主题模型的一个重要应用。
其中一个使用案例是对新文本数据进行探索性数据分析,其中数据集的底层结构未知。这相当于为一个未见过的数据集绘制图表并计算总结统计量,该数据集包含需要理解其特征的数值和类别变量,才能在进行更复杂的分析之前做出合理的判断。通过主题建模的结果,可以确定此数据集在未来建模工作中的可用性。例如,如果主题模型返回了清晰且 distinct 的主题,那么该数据集将是进一步聚类分析的理想候选对象。
实际上,确定主题的作用是创建一个额外的变量,用于排序、分类和/或分块数据。如果我们的主题模型返回了汽车、农业和电子产品作为抽象主题,我们可以将大量的文本数据集过滤到只有农业作为主题的文档。一旦过滤完毕,我们可以进行进一步的分析,包括情感分析、另一次主题建模,或者任何我们能想到的分析。除了定义语料库中存在的主题外,主题建模还会间接返回许多其他信息,这些信息也可以用于分解大型数据集并理解其特征。
这里显示了文档排序的表示:

图 7.7:文档排序/分类
其中一个特征是主题的普遍性。想象一下对一个开放式问卷调查进行分析,这个问卷调查旨在评估对某个产品的反馈。我们可以设想主题模型返回的主题形式为情感。一组词可能是“好”,“优秀”,“推荐”和“质量”,而另一组则可能是“垃圾”,“损坏”,“差”和“失望”。鉴于这种类型的调查,主题本身可能不会太令人惊讶,但有趣的是,我们可以统计每个主题包含的文档数量。从统计结果中,我们可以得出这样的结论:例如,有 x 百分比的调查参与者对该产品有正面反应,而只有 y 百分比的参与者有负面反应。实质上,我们所创建的是情感分析的粗略版本。
当前,主题模型最常见的用途是作为推荐引擎的一个组件。如今的重点是个性化——为客户提供专门设计和策划的产品。以网站为例,无论是新闻网站还是其他类型的网站,它们的目的是传播文章。像雅虎和 Medium 这样的公司需要客户持续阅读才能维持生意,而让客户继续阅读的一种方法是向他们推送他们更倾向于阅读的文章。这就是主题建模的用武之地。通过使用一个由个体之前阅读的文章组成的语料库,主题模型基本上会告诉我们该订阅者喜欢阅读什么类型的文章。公司然后可以查找其库存中的相关文章,并通过用户的账户页面或电子邮件将其发送给该个体。这就是定制策划,以简化使用并保持用户的持续参与。
在我们开始为模型准备数据之前,让我们快速加载并浏览一下数据。
练习 28:数据加载
在这个练习中,我们将从一个数据集加载数据并进行格式化。本章中的所有练习的数据集均来自加利福尼亚大学尔湾分校(UCI)托管的机器学习库。要找到数据,请访问github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson07/Exercise27-Exercise%2038。
注意
该数据下载自archive.ics.uci.edu/ml/datasets/News+Popularity+in+Multiple+Social+Media+Platforms。可以通过github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson07/Exercise27-Exercise%2038访问。Nuno Moniz 和 Luís Torgo(2018),《在线新闻源的多源社交反馈》,CoRR UCI 机器学习库[archive.ics.uci.edu/ml]。加利福尼亚尔湾:加利福尼亚大学信息与计算机科学学院。
这是进行此练习所需的唯一文件。下载并保存在本地后,数据可以加载到笔记本中:
注意
在同一个笔记本中执行本章的练习。
-
定义数据的路径并使用
pandas加载它:path = "News_Final.csv" df = pandas.read_csv(path, header=0)注意
将文件添加到与您打开的笔记本相同的路径中。
-
通过执行以下代码简要检查数据:
def dataframe_quick_look(df, nrows): print("SHAPE:\n{shape}\n".format(shape=df.shape)) print("COLUMN NAMES:\n{names}\n".format(names=df.columns)) print("HEAD:\n{head}\n".format(head=df.head(nrows))) dataframe_quick_look(df, nrows=2)这个用户定义的函数返回数据的形状(行数和列数)、列名和数据的前两行。
![]()
图 7.8:原始数据
这是一个在特征上比运行主题模型所需的数据集要大得多的数据集。
-
请注意,其中一列名为
Topic,实际上包含了主题模型试图确定的信息。让我们简要查看提供的主题数据,以便在最终生成我们自己的主题时,能够进行比较。运行以下代码行,打印唯一的主题值及其出现次数:print("TOPICS:\n{topics}\n".format(topics=df["Topic"].value_counts()))输出如下:
TOPICS: economy 33928 obama 28610 microsoft 21858 palestine 8843 Name: Topic, dtype: int64 -
现在,我们提取标题数据并将提取的数据转换为一个列表对象。打印列表中的前五个元素以及列表长度,以确认提取是否成功:
raw = df["Headline"].tolist() print("HEADLINES:\n{lines}\n".format(lines=raw[:5])) print("LENGTH:\n{length}\n".format(length=len(raw)))

图 7.9:标题列表
现在数据已加载并正确格式化,让我们讨论文本数据清洗,然后进行一些实际的清洗和预处理。为了教学目的,清洗过程最初将在一个标题上构建和执行。建立并测试了该过程之后,我们将返回并在每个标题上运行该过程。
清洗文本数据
所有成功建模工作的一个关键组成部分是清洗过的数据集,该数据集已针对特定数据类型和分析方法进行了适当且充分的预处理。文本数据也不例外,因为它以原始形式几乎无法使用。无论运行什么算法:如果数据未经过适当准备,结果充其量是毫无意义,最坏的情况则可能是误导性的。正如俗话所说,“垃圾进,垃圾出。” 对于主题建模来说,数据清洗的目标是通过移除可能妨碍分析的内容,来提取每个文档中可能相关的词汇。
数据清洗和预处理几乎总是特定于数据集的,这意味着每个数据集都需要选择一套独特的清洗和预处理步骤,专门处理该数据集中的问题。对于文本数据,清洗和预处理步骤可能包括语言过滤、移除网址和用户名、词形还原、去除停用词等。在接下来的练习中,我们将清洗一个包含新闻标题的数据集,以进行主题建模。
数据清洗技术
重申一下之前的观点,清洗文本数据以进行主题建模的目标是提取每个文档中可能与发现语料库的抽象主题相关的词汇。这意味着需要去除常见词、短词(通常较为常见)、数字和标点符号。没有固定的清洗流程,因此理解数据类型中的典型问题点并进行广泛的探索性工作是非常重要的。
接下来,我们将讨论一些我们将要使用的文本数据清理技术。进行任何涉及文本的建模任务时,首先需要做的一件事是确定文本的语言。在这个数据集中,大多数标题是英语,因此为了简化处理,我们将删除非英语的标题。构建基于非英语文本数据的模型需要额外的技能,最基本的是流利掌握所处理的语言。
数据清理的下一个关键步骤是删除文档中所有与基于单词的模型无关的元素,或那些可能成为噪声源,进而影响结果的元素。需要删除的元素可能包括网站地址、标点符号、数字和停用词。停用词基本上是一些简单的日常用词,包括 we、are 和 the。需要注意的是,没有一个权威的停用词词典;相反,每个词典都会有所不同。尽管存在差异,每个词典都表示一些常见的单词,这些单词被认为与话题无关。主题模型试图识别那些既频繁又不太频繁的单词,以便能够描述一个抽象的主题。
删除网站地址有类似的动机。特定的网站地址出现的频率非常低,但即使一个特定的网站地址出现足够多,足以与某个话题相关联,网站地址的解释方式也不同于单词。从文档中移除不相关的信息,可以减少可能阻碍模型收敛或模糊结果的噪声。
词形还原,与语言检测一样,是所有涉及文本的建模活动中一个重要的组成部分。它是将单词还原为其基本形式的过程,以便将应该相同但实际不同的单词归为一类。考虑单词 running、runs 和 ran,它们的基本形式都是 run。词形还原的一个好处是,它在决定如何处理每个单词之前,会考虑整个句子的所有单词,换句话说,它会考虑上下文。像大多数前述的清理技术一样,词形还原只是减少数据中的噪声,从而使我们能够识别干净且可解释的主题。
现在,掌握了基本的文本清理技术后,让我们将其应用于实际数据。
练习 29:逐步清理数据
在这个练习中,我们将学习如何实现一些清理文本数据的关键技术。每个技术将在我们进行练习时进行解释。在每一步清理操作后,使用print输出示例标题,便于我们观察从原始数据到适合建模的数据的演变:
-
选择第五个标题作为我们构建和测试清理过程的示例。第五个标题不是随意选择的,它被选中是因为它包含了在清理过程中将要解决的特定问题:
example = raw[5] print(example)输出结果如下:
![图 7.10:第五个标题]()
图 7.10:第五个标题
-
使用
langdetect库来检测每个标题的语言。如果语言不是英语("en"),则从数据集中删除该标题:def do_language_identifying(txt): try: the_language = langdetect.detect(txt) except: the_language = 'none' return the_language print("DETECTED LANGUAGE:\n{lang}\n".format(lang=do_language_identifying(example) ))输出结果如下:
![]()
图 7.11:检测到的语言
-
将包含标题的字符串使用空格分割成片段,称为tokens。返回的对象是构成标题的单词和数字的列表。将标题字符串拆分成 tokens 会使清理和预处理过程更简单:
example = example.split(" ") print(example)输出结果如下:
![]()
图 7.12:使用空格分割字符串
-
使用正则表达式查找包含
http://或https://的 tokens 来识别所有 URL。将 URL 替换为'URL'字符串:example = ['URL' if bool(regex.search("http[s]?://", i))else i for i in example] print(example)输出结果如下:
![图 7.13:将 URLs 替换为 URL 字符串]()
图 7.13:将 URLs 替换为 URL 字符串
-
使用正则表达式将所有标点符号和换行符(
\n)替换为空字符串:example = [regex.sub("[^\\w\\s]|\n", "", i) for i in example] print(example)输出结果如下:
![图 7.14:将标点符号替换为换行符]()
图 7.14:将标点符号替换为换行符
-
使用正则表达式将所有数字替换为空字符串:
example = [regex.sub("^[0-9]*$", "", i) for i in example] print(example)输出结果如下:
![图 7.15:将数字替换为空字符串]()
图 7.15:将数字替换为空字符串
-
将所有大写字母转换为小写字母。将所有内容转换为小写并不是强制性步骤,但它有助于减少复杂性。将所有内容转换为小写后,跟踪的内容更少,因此出错的机会也更小:
example = [i.lower() if i not in "URL" else i for i in example] print(example)输出结果如下:
![图 7.16:将大写字母转换为小写字母]()
图 7.16:将大写字母转换为小写字母
-
删除步骤 4中作为占位符添加的
"URL"字符串。之前添加的"URL"字符串实际上对于建模来说并不需要。如果认为它留在里面无害,考虑到"URL"字符串可能自然出现在标题中,而我们不希望人为地增加它出现的频率。此外,"URL"字符串并非每个标题中都会出现,因此如果将其留在里面,可能会无意中创建"URL"字符串与某一主题之间的关联:example = [i for i in example if i not in "URL"] print(example)输出结果如下:
![图 7.17:字符串 URL 被移除]()
图 7.17:移除字符串 URL
-
从
nltk加载stopwords字典并打印出来:list_stop_words = nltk.corpus.stopwords.words("English") list_stop_words = [regex.sub("[^\\w\\s]", "", i) for i in list_stop_words] print(list_stop_words)输出结果如下:
![图 7.18:停用词列表]()
图 7.18:停用词列表
在使用字典之前,重要的是要重新格式化单词,以符合我们标题的格式。这包括确认所有内容都是小写且没有标点符号。
-
现在我们已经正确格式化了
stopwords字典,使用它来删除标题中的所有停用词:example = [i for i in example if i not in list_stop_words] print(example)输出结果如下:
![图 7.19:从标题中移除停用词]()
图 7.19:从标题中移除停用词
-
通过定义一个可以单独应用于每个标题的函数来执行词形还原。词形还原需要加载 WordNet 字典:
def do_lemmatizing(wrd): out = nltk.corpus.wordnet.morphy(wrd) return (wrd if out is None else out) example = [do_lemmatizing(i) for i in example] print(example)输出如下:
![图 7.20:执行词形还原后的输出]()
图 7.20:执行词形还原后的输出
-
从标记列表中移除所有长度为四个或更短的单词。该步骤的假设是,短单词通常更为常见,因此不会对我们希望从主题模型中提取的见解产生重要影响。这是数据清理和预处理的最后一步:
example = [i for i in example if len(i) >= 5] print(example)输出如下:

图 7.21:标题五号清理后的结果
现在,我们已经逐个标题完成了清理和预处理步骤,我们需要将这些步骤应用到近 100,000 个标题上。将这些步骤像对单个标题那样手动应用到每个标题上是不可行的。
练习 30:完整数据清理
在本练习中,我们将把练习 29中的第 2 步到第 12 步(逐步清理数据)合并成一个可以应用于每个标题的函数。该函数将接受一个字符串格式的标题作为输入,并输出一个以标记列表形式呈现的清理后标题。主题模型要求文档以字符串格式而非标记列表的形式呈现,因此,在第 4 步中,标记列表将被转换回字符串:
-
定义一个包含练习 29中清理过程所有单独步骤的函数(逐步清理数据):
def do_headline_cleaning(txt): # identify language of tweet # return null if language not English lg = do_language_identifying(txt) if lg != 'en': return None # split the string on whitespace out = txt.split(" ") # identify urls # replace with URL out = ['URL' if bool(regex.search("http[s]?://", i)) else i for i in out] # remove all punctuation out = [regex.sub("[^\\w\\s]|\n", "", i) for i in out] # remove all numerics out = [regex.sub("^[0-9]*$", "", i) for i in out] # make all non-keywords lowercase out = [i.lower() if i not in "URL" else i for i in out] # remove URL out = [i for i in out if i not in "URL"] # remove stopwords list_stop_words = nltk.corpus.stopwords.words("English") list_stop_words = [regex.sub("[^\\w\\s]", "", i) for i in list_stop_words] out = [i for i in out if i not in list_stop_words] # lemmatizing out = [do_lemmatizing(i) for i in out] # keep words 5 or more characters long out = [i for i in out if len(i) >= 5] return out -
在每个标题上执行该函数。Python 中的
map函数是将用户定义的函数应用于列表中每个元素的一个好方法。将map对象转换为列表,并将其赋值给clean变量。clean变量是一个列表的列表:clean = list(map(do_headline_cleaning, raw)) -
在
do_headline_cleaning中,如果检测到标题的语言不是英语,则返回None。最终清理后的列表元素应仅为列表,而不是None,因此需要移除所有None类型。使用print显示前五个清理后的标题和clean变量的长度:clean = list(filter(None.__ne__, clean)) print("HEADLINES:\n{lines}\n".format(lines=clean[:5])) print("LENGTH:\n{length}\n".format(length=len(clean)))输出如下:
![]()
图 7.22:标题及其长度
-
对每个单独的标题,使用空格分隔符连接标记。此时,标题应为一个非结构化的单词集合,尽管对人类读者来说毫无意义,但对主题建模来说是理想的:
clean_sentences = [" ".join(i) for i in clean] print(clean_sentences[0:10])清理后的标题应类似于以下内容:

图 7.23:用于建模的清理后的标题
回顾一下,清理和预处理工作实际做的事情是从数据中剔除噪声,使得模型能够集中于那些实际上可能提供洞察的数据元素。例如,任何与特定主题无关的词(或停用词)不应该影响主题的生成,但如果不小心留下,它们可能会影响主题的生成。为了避免所谓的“假信号”,我们移除了这些词。同样,由于主题模型无法识别上下文,标点符号是无关的,因此也被移除。即使模型在没有清理噪声数据的情况下能够找到主题,未清理的数据可能会有成千上万甚至更多的额外词汇和随机字符需要解析,具体取决于语料库中文档的数量,这可能会显著增加计算需求。因此,数据清理是主题建模的一个重要部分。
活动 15:加载并清理 Twitter 数据
在此活动中,我们将加载并清理 Twitter 数据,以便在后续活动中进行建模。我们对标题数据的使用仍在进行中,因此我们将在一个单独的 Jupyter notebook 中完成此活动,但所有要求和导入的库都保持一致。
目标是处理原始推文数据,清理它,并生成与前一个练习中步骤 4相同的输出。该输出应该是一个列表,列表长度类似于原始数据文件中的行数。长度类似,意味着可能不等于行数,因为在清理过程中可能会丢弃一些推文,原因包括推文可能不是英文的。列表中的每个元素应代表一条推文,且仅包含可能与主题形成相关的词汇。
完成此活动的步骤如下:
-
导入必要的库。
-
从
github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson07/Activity15-Activity17加载 LA Times 健康推特数据(latimeshealth.txt)。注意
此数据集下载自
archive.ics.uci.edu/ml/datasets/Health+News+in+Twitter。我们已将其上传至 GitHub,并可以从github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson07/Activity15-Activity17下载。Karami, A., Gangopadhyay, A., Zhou, B., & Kharrazi, H. (2017). 模糊方法在健康与医学语料库中的主题发现。《国际模糊系统杂志》。UCI 机器学习库[archive.ics.uci.edu/ml]。加利福尼亚大学欧文分校:加利福尼亚大学信息与计算机科学学院。 -
运行快速的探索性分析,确定数据的大小和结构。
-
提取推文文本并转换为列表对象。
-
编写一个函数来执行语言检测、基于空格的分词、将屏幕名称和网址替换为
SCREENNAME和URL。该函数还应删除标点、数字以及屏幕名称和网址的替换。将所有内容转换为小写,但屏幕名称和网址除外。它应删除所有停用词,执行词形还原,并保留五个或更多字母的单词。 -
将在 步骤 5 中定义的函数应用于每个推文。
-
删除输出列表中等于
None的元素。 -
将每个推文的元素转换回字符串。使用空格连接。
-
保持笔记本开放以备将来建模。
注意
本章中的所有活动都需要在同一个笔记本中执行。
-
输出将如下所示:

图 7.24:用于建模的已清理推文
注意
此活动的解决方案可以在第 357 页找到。
潜在狄利克雷分配
2003 年,David Biel、Andrew Ng 和 Michael Jordan 发表了关于主题建模算法的文章,称为潜在狄利克雷分配(LDA)。LDA 是一种生成概率模型。这意味着我们假定已知生成数据的过程,该过程以概率形式表达,并且从数据反推生成数据的参数。在这种情况下,我们感兴趣的是生成数据的主题。这里讨论的过程是 LDA 的最基本形式,但对于学习来说也是最易理解的。
对于语料库中的每个文档,假定生成过程如下:
-
选择
,其中
是单词的数量。 -
选择
,其中
是主题的分布。 -
对于每一个
单词
,选择主题
,并从
。
让我们更详细地回顾一下生成过程。前面提到的三个步骤会为语料库中的每个文档重复执行。初始步骤是通过从大多数情况下的泊松分布中抽样来选择文档中的单词数。需要注意的是,由于 N 与其他变量无关,因此在推导算法时,生成 N 的随机性大多被忽略。选择了N后,接下来是生成主题混合或每个文档独有的主题分布。可以将其看作是每个文档的主题列表,列表中的概率表示每个主题在文档中所占的比例。考虑三个主题:A、B 和 C。例如,一个文档可能是 100%的主题 A、75%的主题 B 和 25%的主题 C,或者是其他无数的组合。最后,文档中的特定单词是通过基于所选主题和该主题的单词分布的概率条件来选择的。需要注意的是,文档并不是以这种方式生成的,但这种方式是一个合理的代理。
这个过程可以看作是在分布上的分布。从文档集合(分布)中选择一个文档,并从该文档中通过多项分布选择一个主题,这个主题来自由狄利克雷分布生成的该文档的主题概率分布:

图 7.25:LDA 的图形表示
构建表示 LDA 解的公式最直接的方法是通过图形表示。这种特殊的表示法称为板符号图模型,因为它使用板块来表示过程中的两个迭代步骤。你可能还记得,生成过程会对语料库中的每个文档执行,因此最外层的板块,标记为 M,表示对每个文档进行迭代。同样,第 3 步中的单词迭代则通过图表中最内层的板块表示,标记为N。圆圈表示参数、分布和结果。带阴影的圆圈,标记为w,表示选定的单词,它是唯一已知的数据,因此用来反推生成过程。除了w之外,图中的其他四个变量定义如下:
-

图 7.26:变分推断过程
变分推断背后的思维过程是,如果实际分布是不可解的,那么应该找到一个简单的分布,称为变分分布,它非常接近可解的初始分布,以便使推断成为可能。
首先,选择一个分布族 q,并基于新的变分参数进行条件化。优化这些参数,使得原始分布(对于熟悉贝叶斯统计的人来说,实际上是后验分布)与变分分布尽可能接近。变分分布将足够接近原始的后验分布,可以作为代理,从而使得基于该分布进行的任何推断都适用于原始的后验分布。分布族 q 的通用公式如下:

图 7.27:分布族的公式,q
有大量潜在的变分分布可以用作后验分布的近似。首先从这些分布中选择一个初始变分分布,作为优化过程的起点,该过程会迭代地不断逼近最优分布。最优参数是能够最好地近似后验分布的分布参数。
两个分布的相似度通过 Kullback-Leibler(KL)散度来衡量。KL 散度也被称为相对熵。同样,找到最佳的变分分布 q,以近似原始后验分布 p,需要最小化 KL 散度。找到最小化散度的参数的默认方法是迭代的固定点方法,我们在这里不深入讨论。
一旦识别出最优分布(这意味着最优参数已被确定),就可以利用它来生成输出矩阵并进行任何必要的推断。
词袋模型
文本不能直接传递给任何机器学习算法;首先需要将其进行数值编码。在机器学习中处理文本的一种简单方法是通过词袋模型,该模型去除词语顺序的信息,严格关注每个词的出现程度,即计数或频率。可以利用 Python 的 sklearn 库,将前一个练习中创建的清理向量转换为 LDA 模型所需的结构。由于 LDA 是一个概率模型,我们不希望对词语出现次数进行任何缩放或加权;相反,我们选择输入原始计数。
词袋模型的输入将是从练习 4(完整数据清理)返回的清理后的字符串列表。输出将是文档编号、单词的数字编码及该单词在文档中出现的次数。这三项将以元组和整数的形式呈现。元组类似于(0,325),其中 0 是文档编号,325 是数字编码的单词。请注意,325 是该单词在所有文档中的编码。然后,整数表示该单词的出现次数。我们将在本章运行的词袋模型来自sklearn,分别为CountVectorizer和TfIdfVectorizer。第一个模型返回原始计数,第二个模型返回一个缩放值,我们稍后会讨论。
一个重要的说明是,本章介绍的两个主题模型的结果可能会有所不同,即使数据相同,也会因为随机性而有所变化。无论是 LDA 中的概率还是优化算法都不是确定性的,因此,如果您的结果与这里展示的结果略有不同,也不必惊讶。
练习 31:使用计数向量化器创建词袋模型
在本练习中,我们将运行CountVectorizer(在sklearn中)将我们之前创建的清洗过的标题向量转换为词袋数据结构。此外,我们还将定义一些变量,这些变量将在建模过程中使用。
-
定义
number_words、number_docs和number_features。前两个变量控制 LDA 结果的可视化,稍后会详细介绍。number_features变量控制将在特征空间中保留的单词数:number_words = 10 number_docs = 10 number_features = 1000 -
运行计数向量化器并打印输出。有三个关键输入参数,分别是
max_df、min_df和max_features。这些参数进一步筛选语料库中的单词,保留那些最有可能影响模型的单词。那些只出现在少数文档中的单词太稀有,无法归属到任何一个主题中,因此min_df用于丢弃在指定文档数以下出现的单词。而那些出现在太多文档中的单词则不够具体,无法与特定主题关联,因此max_df用于丢弃在超过指定百分比文档中出现的单词。最后,为了避免过拟合模型,限制用于拟合模型的单词数量为出现频率最高的指定数量单词(max_features):vectorizer1 = sklearn.feature_extraction.text.CountVectorizer( analyzer="word", max_df=0.5, min_df=20, max_features=number_features ) clean_vec1 = vectorizer1.fit_transform(clean_sentences) print(clean_vec1[0])输出如下:
![图 7.28:词袋数据结构]()
图 7.28:词袋数据结构
-
提取特征名称和向量化器中的单词。模型仅接受单词的数字编码,因此将特征名称向量与结果合并将使解释更加容易:
feature_names_vec1 = vectorizer1.get_feature_names()
困惑度
模型通常具有可以用来评估其性能的指标。主题模型也不例外,尽管在这种情况下,性能的定义略有不同。在回归和分类中,预测值可以与实际值进行比较,从中可以计算出明确的性能指标。而在主题模型中,预测的可靠性较差,因为模型只知道它训练时使用的单词,而新文档可能不包含这些单词,尽管它们可能涉及相同的主题。由于这一差异,主题模型使用一种特定于语言模型的指标来评估,即困惑度。困惑度(Perplexity,简称 PP)衡量的是在给定单词后,平均而言,有多少个不同的最可能的单词可以跟随它。我们可以用两个单词作为例子:the 和 announce。单词 the 后可以接大量的同样最可能的单词,而单词 announce 后可以接的同样最可能的单词则要少得多——尽管它们的数量仍然很大。
这个思路是:那些平均可以被更少数量的同样最可能的单词跟随的词语更具特异性,并且可以与主题更紧密地关联。因此,较低的困惑度得分意味着更好的语言模型。困惑度与熵非常相似,但通常使用困惑度,因为它更易于解释。正如我们接下来将看到的,它可以用来选择最佳的主题数。当 m 是单词序列中的单词数时,困惑度定义为:

图 7.29:困惑度公式
练习 32:选择主题数
如前所述,LDA 有两个必需的输入。第一个是文档本身,第二个是主题数。选择合适的主题数非常具有挑战性。寻找最佳主题数的一种方法是对多个主题数进行搜索,并选择对应于最小困惑度得分的主题数。在机器学习中,这种方法称为网格搜索(grid search)。
在本练习中,我们使用不同主题数下拟合的 LDA 模型的困惑度得分,来确定最终采用的主题数。请记住,原始数据集中的标题已经被分类成四个主题。让我们看看这种方法是否返回了四个主题:
-
定义一个函数,该函数根据不同的主题数拟合 LDA 模型并计算困惑度得分。返回两个项目:一个数据框,包含主题数及其困惑度得分;一个整数,表示最小困惑度得分对应的主题数:
def perplexity_by_ntopic(data, ntopics): output_dict = { "Number Of Topics": [], "Perplexity Score": [] } for t in ntopics: lda = sklearn.decomposition.LatentDirichletAllocation( n_components=t, learning_method="online", random_state=0 ) lda.fit(data) output_dict["Number Of Topics"].append(t) output_dict["Perplexity Score"].append(lda.perplexity(data)) output_df = pandas.DataFrame(output_dict) index_min_perplexity = output_df["Perplexity Score"].idxmin() output_num_topics = output_df.loc[ index_min_perplexity, # index "Number Of Topics" # column ] return (output_df, output_num_topics) -
执行在步骤 1 中定义的函数。
ntopics输入是一个包含任何值的主题数列表,可以具有任意长度。打印出数据框:df_perplexity, optimal_num_topics = perplexity_by_ntopic( clean_vec1, ntopics=[1, 2, 3, 4, 6, 8, 10] ) print(df_perplexity)输出结果如下:
![图 7.30:包含主题数和困惑度得分的数据框]()
图 7.30:包含主题数量和困惑度得分的数据框
-
将困惑度得分绘制为主题数量的函数。这只是查看步骤 2数据框中结果的另一种方式:
df_perplexity.plot.line("Number Of Topics", "Perplexity Score")图表如下所示:

图 7.31:困惑度作为主题数量函数的折线图
正如数据框和图表所示,使用困惑度(perplexity)得到的最佳主题数量是三个。将主题数量设置为四时得到的困惑度为第二低,因此,尽管结果与原始数据集中的信息不完全匹配,但结果足够接近,足以增强对网格搜索方法的信心,用于确定最佳主题数量。网格搜索返回三个而不是四个的原因可能有很多,未来的练习中我们将深入探讨这些原因。
练习 33:运行潜在狄利克雷分配(Latent Dirichlet Allocation)
在本练习中,我们实现 LDA 并检查结果。LDA 输出两个矩阵,第一个是主题-文档矩阵,第二个是词-主题矩阵。我们将查看这些矩阵,分别以模型返回的原始形式以及易于理解的格式化表格呈现:
-
使用练习 32中找到的最佳主题数量拟合 LDA 模型,选择主题数量:
lda = sklearn.decomposition.LatentDirichletAllocation( n_components=optimal_num_topics, learning_method="online", random_state=0 ) lda.fit(clean_vec1)输出如下所示:
![图 7.32:LDA 模型]()
图 7.32:LDA 模型
-
输出主题-文档矩阵及其形状,以确认其与主题数量和文档数量一致。矩阵的每一行是每个文档的主题分布:
lda_transform = lda.transform(clean_vec1) print(lda_transform.shape) print(lda_transform)输出如下所示:
![图 7.33:主题-文档矩阵及其维度]()
图 7.33:主题-文档矩阵及其维度
-
输出词-主题矩阵及其形状,以确认其与练习 31中指定的特征数量(词语)以及输入的主题数量一致。每一行基本上是每个词分配到该主题的计数(虽然不完全是计数),但这些准计数可以转化为每个主题的词分布:
lda_components = lda.components_ print(lda_components.shape) print(lda_components)输出如下所示:
![图 7.34:词-主题矩阵及其维度]()
图 7.34:词-主题矩阵及其维度
-
定义一个函数,将两个输出矩阵格式化为易于阅读的表格:
def get_topics(mod, vec, names, docs, ndocs, nwords): # word to topic matrix W = mod.components_ W_norm = W / W.sum(axis=1)[:, numpy.newaxis] # topic to document matrix H = mod.transform(vec) W_dict = {} H_dict = {} for tpc_idx, tpc_val in enumerate(W_norm): topic = "Topic{}".format(tpc_idx) # formatting w W_indices = tpc_val.argsort()[::-1][:nwords] W_names_values = [ (round(tpc_val[j], 4), names[j]) for j in W_indices ] W_dict[topic] = W_names_values # formatting h H_indices = H[:, tpc_idx].argsort()[::-1][:ndocs] H_names_values = [ (round(H[:, tpc_idx][j], 4), docs[j]) for j in H_indices ] H_dict[topic] = H_names_values W_df = pandas.DataFrame( W_dict, index=["Word" + str(i) for i in range(nwords)] ) H_df = pandas.DataFrame( H_dict, index=["Doc" + str(i) for i in range(ndocs)] ) return (W_df, H_df)该函数可能比较复杂,因此让我们一步步分析。首先创建W和H矩阵,包括将W的分配计数转化为每个主题的词分布。然后,遍历各个主题。在每次迭代中,识别与每个主题相关的顶级词汇和文档。将结果转换为两个数据框。
-
执行步骤 4中定义的函数:
W_df, H_df = get_topics( mod=lda, vec=clean_vec1, names=feature_names_vec1, docs=raw, ndocs=number_docs, nwords=number_words ) -
打印出单词-主题数据框。它展示了与每个主题相关的前十个单词,按分布值排序。从这个数据框中,我们可以识别出单词分组所代表的抽象主题。更多关于抽象主题的内容请参见后续:
print(W_df)输出如下:
![]()
图 7.35:单词-主题表
-
打印出主题-文档数据框。它展示了与每个主题最相关的 10 篇文档。数据来源于每篇文档的主题分布:
print(H_df)输出如下:

图 7.36:主题-文档表
单词-主题数据框的结果显示,抽象主题是巴拉克·奥巴马、经济和微软。有趣的是,描述经济的单词分组中包含了关于巴勒斯坦的提及。原始数据集中指定的四个主题在单词-主题数据框输出中都有体现,但并不是以预期的完全独立的方式呈现。我们可能面临两种问题。首先,涉及经济和巴勒斯坦的主题可能还不成熟,这意味着增加主题的数量可能会解决这个问题。另一个潜在的问题是,LDA 对于相关主题的处理较差。在练习 35,尝试四个主题中,我们将尝试扩展主题的数量,这将帮助我们更好地理解为什么某个单词分组似乎是多个主题的混合。
练习 34:可视化 LDA
可视化是探索主题模型结果的有用工具。在本练习中,我们将查看三种不同的可视化图表。这些可视化图表包括基本的直方图以及使用 t-SNE 和 PCA 的专业可视化。
为了创建一些可视化图表,我们将使用pyLDAvis库。这个库足够灵活,可以处理使用不同框架构建的主题模型。在这种情况下,我们将使用sklearn框架。这个可视化工具会返回一个直方图,展示与每个主题最相关的单词,以及一个二元图,常用于 PCA,其中每个圆圈代表一个主题。从二元图中,我们可以了解每个主题在整个语料库中的流行度,圆圈的大小反映了这一点;圆圈的接近度反映了主题之间的相似性。理想的情况是,圆圈在图中均匀分布,且大小适中。也就是说,我们希望主题之间是独立的,并且在语料库中一致地出现:
-
运行并显示
pyLDAvis。这个图表是交互式的。点击每个圆圈会更新直方图,显示与该主题最相关的词汇。以下是这个交互式图表的一个视图:lda_plot = pyLDAvis.sklearn.prepare(lda, clean_vec1, vectorizer1, R=10) pyLDAvis.display(lda_plot)图表如下所示:
![图 7.37:LDA 模型的直方图和二元图]()
图 7.37:LDA 模型的直方图和二元图
-
定义一个函数,拟合 t-SNE 模型并绘制结果:
def plot_tsne(data, threshold): # filter data according to threshold index_meet_threshold = numpy.amax(data, axis=1) >= threshold lda_transform_filt = data[index_meet_threshold] # fit tsne model # x-d -> 2-d, x = number of topics tsne = sklearn.manifold.TSNE( n_components=2, verbose=0, random_state=0, angle=0.5, init='pca' ) tsne_fit = tsne.fit_transform(lda_transform_filt) # most probable topic for each headline most_prob_topic = [] for i in range(tsne_fit.shape[0]): most_prob_topic.append(lda_transform_filt[i].argmax()) print("LENGTH:\n{}\n".format(len(most_prob_topic))) unique, counts = numpy.unique( numpy.array(most_prob_topic), return_counts=True ) print("COUNTS:\n{}\n".format(numpy.asarray((unique, counts)).T)) # make plot color_list = ['b', 'g', 'r', 'c', 'm', 'y', 'k'] for i in list(set(most_prob_topic)): indices = [idx for idx, val in enumerate(most_prob_topic) if val == i] matplotlib.pyplot.scatter( x=tsne_fit[indices, 0], y=tsne_fit[indices, 1], s=0.5, c=color_list[i], label='Topic' + str(i), alpha=0.25 ) matplotlib.pyplot.xlabel('x-tsne') matplotlib.pyplot.ylabel('y-tsne') matplotlib.pyplot.legend(markerscale=10)该函数首先通过输入阈值过滤主题-文档矩阵。由于有数万个标题,包含所有标题的任何图形都会难以阅读,因此不具备实用性。因此,只有当分布值大于或等于输入阈值时,函数才会绘制文档。一旦数据经过过滤,我们运行 t-SNE,其中组件数为 2,因此可以在二维中绘制结果。接下来,创建一个向量,指示与每个文档最相关的主题。该向量将用于按主题为图形着色。为了理解语料库中主题的分布情况以及阈值过滤的影响,该函数返回主题向量的长度,以及每个主题与最大分布值相关联的文档数。函数的最后一步是创建并返回图形。
-
执行该函数:
plot_tsne(data=lda_transform, threshold=0.75)输出结果如下:

图 7.38:带有关于主题分布度量的 t-SNE 图
可视化结果显示,使用三主题的 LDA 模型总体表现良好。在双变量图中,圆圈的大小适中,表明主题在语料库中出现的一致性较高,并且圆圈之间的间距良好。t-SNE 图展示了清晰的聚类,支持双变量图中圆圈之间的分隔。唯一显著的问题是,之前已讨论过的,某个主题包含了看起来与该主题不相关的词语。在下一个练习中,我们将使用四个主题重新运行 LDA。
练习 35:尝试四个主题
在这个练习中,LDA 模型的主题数被设置为四。这样做的动机是尝试解决三主题 LDA 模型中的一个可能不成熟的主题,该主题包含了与巴勒斯坦和经济相关的词汇。我们将首先按照步骤进行,然后在最后探索结果:
-
运行一个 LDA 模型,主题数设置为四:
lda4 = sklearn.decomposition.LatentDirichletAllocation( n_components=4, # number of topics data suggests learning_method="online", random_state=0 ) lda4.fit(clean_vec1)输出结果如下:
![]()
图 7.39:LDA 模型
-
执行前面代码中定义的
get_topics函数,生成更易读的词-主题和主题-文档表:W_df4, H_df4 = get_topics( mod=lda4, vec=clean_vec1, names=feature_names_vec1, docs=raw, ndocs=number_docs, nwords=number_words ) -
打印词-主题表:
print(W_df4)输出结果如下:
![图 7.40:使用四主题 LDA 模型的词-主题表]()
图 7.40:使用四主题 LDA 模型的词-主题表
-
打印文档-主题表:
print(H_df4)输出结果如下:
![图 7.41:使用四主题 LDA 模型的文档-主题表]()
图 7.41:使用四主题 LDA 模型的文档-主题表
-
使用
pyLDAvis显示 LDA 模型结果:lda4_plot = pyLDAvis.sklearn.prepare(lda4, clean_vec1, vectorizer1, R=10) pyLDAvis.display(lda4_plot)图形如下:

图 7.42:描述四主题 LDA 模型的直方图和双图
看着单词-主题表格,我们发现该模型找到的四个主题与原始数据集中指定的四个主题一致。这些主题分别是巴拉克·奥巴马、巴勒斯坦、微软和经济。现在的问题是,为什么使用四个主题建立的模型比使用三个主题的模型具有更高的困惑度得分?答案可以从步骤 5中生成的可视化中得出。双图中有大小合适的圆圈,但其中两个圆圈非常接近,这表明这两个主题——微软和经济——非常相似。在这种情况下,这种相似性其实是合乎直觉的。微软是一个全球性的大公司,既受到经济的影响,也影响着经济。如果我们继续下一步,可能会运行 t-SNE 图,以检查 t-SNE 图中的聚类是否重叠。现在,让我们将 LDA 的知识应用到另一个数据集上。
活动 16:潜在狄利克雷分配与健康推文
在这个活动中,我们将 LDA 应用于活动 15中加载并清理过的健康推文数据,加载和清理 Twitter 数据。记得使用与活动 15中相同的笔记本。执行完这些步骤后,讨论模型的结果。这些单词组合是否合理?
在这个活动中,让我们假设我们有兴趣获得关于主要公共卫生话题的高层次理解。也就是说,了解人们在健康领域讨论的内容。我们收集了一些数据,可以为这个问题提供一些线索。如我们所讨论的,识别数据集中主要话题的最简单方法是主题建模。
以下是完成此活动的步骤:
-
指定
number_words、number_docs和number_features变量。 -
创建一个词袋模型,并将特征名称分配给另一个变量以供以后使用。
-
确定最优的主题数。
-
使用最优的主题数拟合 LDA 模型。
-
创建并打印单词-主题表格。
-
打印文档-主题表格。
-
创建一个双图可视化。
-
保持笔记本开启以便进行未来的建模。
输出将如下所示:

图 7.43:用于健康推文的 LDA 模型训练的直方图和双图
注意
此活动的解决方案可以在第 360 页找到。
词袋模型后续处理
在运行 LDA 模型时,使用了计数向量器词袋模型,但这不是唯一的词袋模型。词频 – 逆文档频率(TF-IDF)类似于 LDA 算法中使用的计数向量器,不同之处在于,TF-IDF 返回的是一个权重,而不是原始计数,反映了给定单词在语料库中文档中的重要性。这种加权方案的关键组成部分是,对于给定单词在整个语料库中出现的频率有一个归一化组件。考虑单词 "have"。
单词 "have" 可能在单个文档中出现多次,表明它可能对区分该文档的主题很重要,但 "have" 会出现在许多文档中,如果不是大多数的话,在语料库中,可能因此使其无法用于区分主题。本质上,这个方案比仅返回文档中单词的原始计数更进一步,旨在初步识别可能有助于识别抽象主题的单词。TF-IDF 向量化器通过 sklearn 使用 TfidfVectorizer 执行。
练习 36:使用 TF-IDF 创建词袋模型
在这个练习中,我们将使用 TF-IDF 创建一个词袋模型:
-
运行 TF-IDF 向量化器并打印出前几行:
vectorizer2 = sklearn.feature_extraction.text.TfidfVectorizer( analyzer="word", max_df=0.5, min_df=20, max_features=number_features, smooth_idf=False ) clean_vec2 = vectorizer2.fit_transform(clean_sentences) print(clean_vec2[0])输出如下:
![]()
图 7.44:TF-IDF 向量化器的输出
-
返回特征名称,即语料库字典中的实际单词,用于分析输出时使用。你还记得我们在 练习 31 中运行
CountVectorizer时做的同样事情,使用计数向量器创建词袋模型:feature_names_vec2 = vectorizer2.get_feature_names() feature_names_vec2输出的一部分如下:
'abbas', 'ability', 'accelerate', 'accept', 'access', 'accord', 'account', 'accused', 'achieve', 'acknowledge', 'acquire', 'acquisition', 'across', 'action', 'activist', 'activity', 'actually',
非负矩阵分解
与 LDA 不同,非负矩阵分解(NMF)不是一个概率模型。相反,正如其名称所示,它是一种涉及线性代数的方法。使用矩阵分解作为主题建模的方法是由 Daniel D. Lee 和 H. Sebastian Seung 在 1999 年提出的。该方法属于分解类模型,包括 第四章 中介绍的 PCA,降维与 PCA 入门。
PCA 和 NMF 之间的主要区别在于,PCA 要求组件是正交的,同时允许它们是正数或负数。而 NMF 要求矩阵组件是非负的,如果你在数据的上下文中思考这个要求,应该能理解这一点。主题与文档之间不能有负相关,单词与主题之间也不能有负相关。如果你不信服,试着解读一个负权重将一个主题与文档关联起来。比如,主题 T 占文档 D 的 -30%;但这意味着什么呢?这是没有意义的,因此 NMF 对矩阵分解的每一部分都有非负的要求。
让我们定义要分解的矩阵,称之为 X,作为术语-文档矩阵,其中行是词,列是文档。矩阵 X 的每个元素是词 i(
图 7.45:矩阵分解
矩阵分解的形式为 
图 7.46:第一个更新规则
第二个更新规则如下:

图 7.47:第二个更新规则
W 和 H 会通过迭代更新,直到算法收敛。目标函数也可以证明是非递增的。也就是说,每次迭代更新 W 和 H 后,目标函数都会更接近最小值。注意,如果重新组织更新规则,乘法更新优化器实际上是一个缩放版的梯度下降算法。
构建成功的 NMF 算法的最后一个组件是初始化 W 和 H 组件矩阵,以便乘法更新能够快速工作。初始化矩阵的一个常用方法是奇异值分解(SVD),它是特征分解的推广。在接下来的练习中实现的 NMF 中,矩阵是通过非负双重奇异值分解(Double Singular Value Decomposition)进行初始化的,这基本上是 SVD 的一种更高级版本,且严格非负。对于理解 NMF,这些初始化算法的具体细节并不重要。只需要注意,初始化算法作为优化算法的起点,能够显著加速收敛过程。
练习 37:非负矩阵分解
在这个练习中,我们拟合了 NMF 算法,并输出了与之前使用 LDA 得到的相同的两个结果表。这些表格是单词-主题表,显示与每个主题相关的前 10 个单词,以及文档-主题表,显示与每个主题相关的前 10 个文档。NMF 算法函数中有两个我们之前没有讨论的额外参数,分别是 alpha 和 l1_ratio。如果担心模型过拟合,这些参数控制正则化应用到目标函数的方式(l1_ratio)和程度(alpha)。更多详细信息可以在 scikit-learn 库的文档中找到(scikit-learn.org/stable/modules/generated/sklearn.decomposition.NMF.html):
-
定义 NMF 模型,并使用 TF-IDF 向量化器的输出调用
fit函数:nmf = sklearn.decomposition.NMF( n_components=4, init="nndsvda", solver="mu", beta_loss="frobenius", random_state=0, alpha=0.1, l1_ratio=0.5 ) nmf.fit(clean_vec2)输出如下:
![]()
图 7.48:定义 NMF 模型
-
运行
get_topics函数生成两个输出表:W_df, H_df = get_topics( mod=nmf, vec=clean_vec2, names=feature_names_vec2, docs=raw, ndocs=number_docs, nwords=number_words ) -
打印
W表:print(W_df)输出结果如下:
![图 7.49:包含概率的词汇-主题表]()
图 7.49:包含概率的词汇-主题表
-
打印
H表:print(H_df)

图 7.50:包含概率的文档-主题表
词汇-主题表包含的词汇分组表明与练习 35中使用四主题 LDA 模型得到的抽象主题相同。然而,这次比较中有趣的部分是,一些包含在这些分组中的单词是新的,或者在分组中的位置发生了变化。考虑到两种方法论的不同,这是不足为奇的。鉴于与原始数据集中的主题对齐,我们已经证明这两种方法都是提取语料库潜在主题结构的有效工具。
练习 38:可视化 NMF
本练习的目的是可视化 NMF 的结果。通过可视化结果,可以深入了解各主题的独特性以及每个主题在语料库中的普遍性。在本练习中,我们使用 t-SNE 进行可视化,t-SNE 已在第六章中详细讨论过,t-分布随机邻居嵌入(t-SNE):
-
对清理后的数据运行
transform,以获取主题-文档分配。打印数据的形状和示例:nmf_transform = nmf.transform(clean_vec2) print(nmf_transform.shape) print(nmf_transform)输出结果如下:
![图 7.51:数据的形状和示例]()
图 7.51:数据的形状和示例
-
运行
plot_tsne函数以拟合 t-SNE 模型并绘制结果:plot_tsne(data=nmf_transform, threshold=0)图形如下:

图 7.52:t-SNE 图,带有总结语料库中主题分布的指标
t-SNE 图,未指定阈值,显示了部分主题重叠,并且跨语料库的主题频率有明显的差异。这两个事实解释了为什么在使用困惑度时,最佳的主题数是三个。似乎有一些主题之间的相关性,而模型未能完全处理这些相关性。即使有这些主题之间的相关性,当主题数设置为四时,模型仍能找出应该有的主题。
回顾一下,NMF(非负矩阵分解)是一个非概率性主题模型,旨在回答与 LDA(潜在狄利克雷分配)相同的问题。它使用了一个在线性代数中非常流行的概念——矩阵分解,这是将一个庞大且难以处理的矩阵分解成较小且更容易解释的矩阵的过程,这些矩阵可以帮助回答有关数据的许多问题。请记住,非负要求并不是源于数学,而是源于数据本身。任何文档的组件不应该是负数。在许多情况下,NMF 的表现不如 LDA,因为 LDA 结合了先验分布,为主题词分组提供了额外的信息层。然而,我们知道在某些情况下,特别是当主题高度相关时,NMF 可能表现得更好。这些情况之一就是所有练习所基于的头条数据。
活动 17:非负矩阵分解
本活动是对健康 Twitter 数据进行主题建模分析的总结,这些数据在第一项活动中被加载并清理过,并且在第二项活动中使用了 LDA。执行 NMF 非常简单,需要的编码很少,因此我建议利用这个机会调整模型参数,同时思考 NMF 的局限性和优点。
完成此活动的步骤如下:
-
创建适当的词袋模型,并将特征名称输出为另一个变量。
-
使用第二项活动中的主题数量(
n_components)值定义并拟合 NMF 算法。 -
获取主题-文档和词-主题结果表。花几分钟时间探索词语分组,并尝试定义抽象的主题。你能量化这些词语分组的含义吗?这些词语分组是否合理?结果是否与使用 LDA 时的结果相似?
-
调整模型参数并重新运行第 3 步和第 4 步。结果如何变化?
输出将如下所示:

图 7.53:带有概率的词-主题表
注意
这个活动的解决方案可以在第 364 页找到。
总结
当面临从尚未见过的大量文档集合中提取信息的任务时,主题建模是一个很好的方法,因为它提供了对文档底层结构的洞察。也就是说,主题模型通过接近度而非上下文来寻找词语分组。在这一章中,我们学习了如何应用两种最常见且最有效的主题建模算法:潜在狄利克雷分配(LDA)和非负矩阵分解(NMF)。现在我们应该能够通过多种不同的技术来清理原始文本文档;这些技术可以应用于许多其他建模场景。我们继续学习如何通过应用词袋模型将清理后的语料库转换为每篇文档的原始词频或词权重的适当数据结构。本章的主要焦点是拟合这两种主题模型,包括优化主题数、将输出转换为易于解释的表格,并可视化结果。有了这些信息,你应该能够应用功能完备的主题模型,为你的业务提取价值和洞察。
在下一章,我们将完全改变方向。我们将深入探讨市场篮子分析。
第八章:市场篮子分析
学习目标
到本章结束时,你将能够:
-
使用交易级别的数据
-
在合适的情境中使用市场篮子分析
-
运行 Apriori 算法并构建关联规则
-
对关联规则进行基本的可视化
-
解读市场篮子分析的关键指标
在本章中,我们将探讨一种基础且可靠的算法,用于分析交易数据。
介绍
在本章中,我们将完全改变方向。前一章探讨了主题模型,重点是自然语言处理、文本数据和应用相对较新开发的算法。大多数数据科学从业者会同意,自然语言处理(包括主题模型)处于数据科学的前沿,是一个活跃的研究领域。我们现在已经明白,主题模型可以并且应该在任何文本数据可能带来洞察或增长的地方应用,包括社交媒体分析、推荐引擎和新闻过滤。
本章将带我们进入零售领域,探索一种基础且可靠的算法,用于分析交易数据。虽然这个算法可能不是最前沿的,也不是最流行的机器学习算法之一,但它在零售领域无处不在,并且其影响力不可否认。它带来的洞察易于解读,立刻可以采取行动,并且对确定分析的下一步非常有帮助。如果你从事零售或交易数据分析工作,那么深入了解市场篮子分析将对你非常有益。
市场篮子分析
假设你为一家零售商工作,卖着几十种产品,你的老板走过来,问你以下问题:
-
哪些产品最常一起购买?
-
产品应该如何在商店中组织和定位?
-
我们如何识别最适合通过优惠券打折的产品?
你可能会感到完全困惑,因为这些问题非常广泛,而且似乎无法通过单一的算法和数据集直接回答。然而,所有这些问题以及更多问题的答案就是市场篮子分析。市场篮子分析背后的基本理念是识别并量化哪些商品或商品组经常一起购买,从而为顾客行为和产品关系提供洞察。
在深入分析之前,值得定义一下“市场篮子”这个术语。市场篮子是一个经济系统中永久存在的商品集合。在这里,永久并不一定意味着传统意义上的永久。它意味着,直到该商品被从目录中移除之前,它将始终可以购买。前述定义中的商品指的是任何商品、服务或一个群体的组成部分,包括自行车、给房子刷漆或一个网站。最后,经济系统可以是一个公司、一系列活动或一个国家。市场篮子的最简单例子是杂货店,它是由一系列食品和饮料商品组成的系统。

图 8.1:一个示例市场篮子,其中经济系统是肉店,永久集合的商品是肉店提供的所有肉类产品
即使不使用任何模型或分析,某些产品之间的关系也是显而易见的。让我们以肉类和蔬菜的关系为例。通常,市场篮子分析模型会返回比“肉类和蔬菜”更具体的关系,但为了讨论的方便,我们将其概括为“肉类和蔬菜”。好吧,肉类和蔬菜之间确实存在关系。那么呢?我们知道这些是常见的食材,通常会一起购买。我们可以利用这一信息,将蔬菜和肉类分别摆放在商店的两端,你会发现这两样商品常常被摆放在商店的对立面,这迫使顾客走完整个商店,从而增加他们购买额外商品的可能性,这些商品如果顾客不需要走遍整个商店,可能就不会买。
零售公司面临的一个难题是如何有效地打折商品。让我们考虑另一个显而易见的关系:花生酱和果冻。在美国,花生酱和果冻三明治非常受欢迎,尤其是在孩子们中间。当购物篮里有花生酱时,可以假设果冻也很可能在其中。既然我们知道花生酱和果冻是一起购买的,那么同时对两者打折就没有意义。如果我们希望顾客购买这两件商品,我们只需打折其中一件商品,知道如果我们能让顾客购买打折商品,他们很可能也会购买另一件商品,即使它是原价。

图 8.2:市场篮子分析的可视化
就像前一章中的主题模型一样,市场篮子分析的关键是识别频繁出现的组。在这里,我们寻找的是频繁出现的产品组,而在主题模型中,我们寻找的是频繁出现的词组。因此,正如它可以应用于主题模型一样,词汇聚类也可以应用于市场篮子分析。主要的不同之处在于,市场篮子分析中的聚类是微观的,每个聚类只有少数几个产品,并且在计算概率指标时,聚类中项目的顺序是至关重要的。我们将在本章后面深入探讨这些指标以及它们是如何计算的。
从前面两个例子中可以明显看出,在市场篮子分析中,零售商能够发现顾客购买的产品之间的关系——这些关系有时显而易见,有时又出乎意料。一旦这些关系被揭示出来,就可以用来指导和改善决策过程。市场篮子分析的一个重要特点是,尽管这种分析最初是在零售领域开发、讨论并应用的,但它同样可以应用于许多不同类型的企业。
执行这种分析的唯一要求是数据必须是一个项集合的列表。在零售案例中,这通常是一个包含多次交易的列表,每次交易中包含一组已购买的产品。另一个替代应用的例子是分析网站流量。对于网站流量,我们把网站视作产品,所以列表中的每个元素就是某个个体在特定时间段内访问的所有网站集合。不用说,市场篮子分析的应用远远超出了零售领域的主应用。
用例
在传统零售应用中,有三个主要的用例:定价优化、优惠券和折扣推荐以及商店布局。如前所述,零售商可以利用模型揭示的产品关联,策略性地在商店内摆放商品,从而促使顾客购买更多商品,并因此花费更多的钱。如果两个或更多产品之间的关系足够强大——即该产品组合在数据集中出现的频率很高,并且组合中的单个产品在其他时候很少单独出现——那么这些产品就可以放在商店的远离彼此的地方,而不会显著影响顾客购买这两种产品的几率。通过迫使顾客走遍整个商店去购买这两种产品,零售商增加了顾客注意到并购买其他商品的机会。同样,零售商也可以通过将两种相关性较弱或非基础性产品放在一起,提高顾客购买这两种商品的几率。显然,商店布局受许多因素的影响,但市场篮子分析无疑是其中一个重要因素:

图 8.3:产品关联如何帮助设计高效且有利可图的商店布局
定价提升和优惠券及折扣推荐是同一个问题的两个方面。可以简单地理解为在哪里提高价格,在哪里降低价格。考虑两个强相关商品的情况。这两件商品很可能会在同一笔交易中被购买,因此提高其中一件商品的价格是增加交易利润的一种方式。如果这两件商品之间的关联足够强,价格的提高几乎不会影响客户购买这两件商品的可能性。以类似的方式,零售商可以通过折扣或优惠券促使客户购买与其他商品有弱关联的商品。
例如,零售商可以将单个客户的购买历史与所有交易的市场篮分析结果进行比较,找出某些客户购买的商品与他们未购买的商品之间的弱关联。通过这个比较,零售商可以为这些客户提供折扣,推荐模型认为与他们之前购买的商品相关的尚未购买的商品。如果你曾在交易结束时收到打印出来的优惠券,极有可能这些商品与刚刚完成的交易中的商品是相关的。
市场篮分析的一个非传统但可行的应用是增强在线广告和搜索引擎优化。假设我们可以访问个人访问的网站列表。利用市场篮分析,我们可以找到网站之间的关系,并利用这些关系来策略性地排序和分组搜索引擎查询结果中的网站。在很多方面,这与商店布局的应用场景类似。
通过对市场篮分析的总体了解和对其应用场景的清晰认识,我们现在可以深入研究这些模型中使用的数据。
重要的概率度量
市场篮分析是建立在几个概率度量的计算基础上的。这里讨论的五个主要度量是支持度、置信度、提升度、杠杆度和确信度。在深入研究交易数据和具体的市场篮分析模型(包括Apriori 算法和关联规则)之前,我们应该花些时间定义并探讨这些度量,使用一个小的、虚构的交易数据集来说明。我们从编造一些数据开始。
练习 39:创建样本交易数据
由于这是本章的第一个练习,让我们设置环境。本章将使用与第七章、主题建模中相同的环境要求。如果任何一个包没有加载,就像前一章那样,使用pip通过命令行安装它们。我们将使用的一个库是mlxtend,它可能对你来说比较陌生。它是一个机器学习扩展库,包含了许多有用的辅助工具,包括集成、堆叠和市场篮分析模型。本次练习没有实际输出,我们将简单地创建一个示例交易数据集,用于后续的练习。
-
打开一个使用 Python 3 的 Jupyter 笔记本。
-
安装以下库:
matplotlib.pyplot,用于绘制模型的结果,mlxtend.frequent_patterns,用于运行模型,mlxtend.preprocessing,用于对数据进行编码和准备,以适应模型,numpy,用于处理数组,pandas,用于处理数据框:注意
import matplotlib.pyplot as plt import mlxtend.frequent_patterns import mlxtend.preprocessing import numpy import pandas -
创建 10 个虚拟交易,内容为杂货店项目。这些数据将以列表的形式出现,这种数据结构在后面讨论格式化交易数据以适应模型时会非常有用:
example = [ ['milk', 'bread', 'apples', 'cereal', 'jelly', 'cookies', 'salad', 'tomatoes'], ['beer', 'milk', 'chips', 'salsa', 'grapes', 'wine', 'potatoes', 'eggs', 'carrots'], ['diapers', 'baby formula', 'milk', 'bread', 'chicken', 'asparagus', 'cookies'], ['milk', 'cookies', 'chicken', 'asparagus', 'broccoli', 'cereal', 'orange juice'], ['steak', 'asparagus', 'broccoli', 'chips', 'salsa', 'ketchup', 'potatoes', 'salad'], ['beer', 'salsa', 'asparagus', 'wine', 'cheese', 'crackers', 'strawberries', 'cookies'], ['chocolate cake', 'strawberries', 'wine', 'cheese', 'beer', 'milk', 'orange juice'], ['chicken', 'peas', 'broccoli', 'milk', 'bread', 'eggs', 'potatoes', 'ketchup', 'crackers'], ['eggs', 'bread', 'cheese', 'turkey', 'salad', 'tomatoes', 'wine', 'steak', 'carrots'], ['bread', 'milk', 'tomatoes', 'cereal', 'chicken', 'turkey', 'chips', 'salsa', 'diapers'] ]
这个简单的数据集将使得解释和理解概率度量变得更加容易。
支持度
支持度简单来说就是项目集在数据中出现的概率,可以通过计算项目集出现的交易次数并将该次数除以总交易数来得到。需要注意的是,项目集可以是一个单独的项目,也可以是一组项目。尽管支持度非常简单,但它是一个重要的度量指标,因为它是用于确定项目集之间关联的可信度和强度的主要指标之一。例如,可能有两个项目只在彼此之间出现,表明它们的关联非常强,但在一个包含 100 个交易的数据集中,只有两次出现并不令人印象深刻。因为该项目集只在 2%的交易中出现,而 2%在原始出现次数中算是很小的,因此该关联不能被视为显著,因此在决策中可能无法使用。
请注意,由于支持度是一个概率值,它的范围将在[0,1]之间。如果项目集包含两个项目,X 和 Y,且 N 为总交易数,则公式如下所示。

图 8.4:支持度公式
让我们暂时回到练习 39中制作的数据,创建样本交易数据,并将商品集定义为牛奶和面包。我们可以轻松地查看这 10 个交易,并统计牛奶和面包商品集出现的次数——这是 4 次。鉴于总共有 10 个交易,牛奶和面包的支持度是 4 除以 10,即 0.4。是否足够大,这取决于数据集本身,我们将在后续部分进行讨论。
置信度
置信度度量可以通过条件概率来理解,它基本上是指在购买了产品 A 的前提下,购买产品 B 的概率。置信度通常表示为 A
B,并表达为包含 A 的交易中同时包含 B 的比例。因此,置信度是通过将交易全集筛选为包含 A 的交易,然后计算这些交易中包含 B 的比例来得出的。与支持度类似,置信度是一个概率值,因此其范围是[0,1]。使用与支持度部分相同的变量定义,以下是置信度的公式:

图 8.5:置信度公式
为了演示置信度,我们将使用啤酒和葡萄酒这两个商品。具体来说,我们来计算啤酒
葡萄酒的置信度。首先,我们需要找出包含啤酒的交易。有 3 个这样的交易,它们是交易 2、6 和 7。现在,在这些交易中,有多少包含葡萄酒?答案是所有的交易都包含葡萄酒。因此,啤酒
葡萄酒的置信度是 1。每次顾客购买了啤酒,他们也购买了葡萄酒。这可能很明显,但为了识别可操作的关联,更高的置信度值是更好的:
提升度和杠杆度
我们将同时讨论接下来的两个度量,提升度和杠杆度,因为尽管它们的计算方式不同,但都试图回答相同的问题。与置信度一样,提升度和杠杆度也表示为 A
B。我们要回答的问题是,是否可以通过一个物品,比如 A,来推断另一个物品,比如 B?换句话说,如果一个人购买了产品 A,我们能否在一定程度上确定他们是否会购买产品 B?这些问题通过将 A 和 B 的支持度在假设 A 和 B 不独立的标准情况下与假设两者独立的情况进行比较来回答。提升度计算这两种情况的比率,因此其范围是[0, 无限]。当提升度等于 1 时,两个产品是独立的,因此在购买产品 A 时,无法得出关于产品 B 的任何结论:

图 8.6:提升度公式
Leverage 计算两种情况之间的差异,因此其范围是[-1, 1]。Leverage 等于零可以解释为与 lift 等于一相同的含义:

图 8.7:杠杆公式
这些指标的值衡量项目之间关系的强度和方向。如果 lift 值为 0.1,我们可以说两个项目之间的关系在负方向上很强。也就是说,可以认为当购买一个产品时,购买第二个产品的机会会减少。正相关和负相关被独立性点所分隔,正如前面所说,lift 的独立性点为 1,leverage 的独立性点为 0,而值越远离这些点,关联越强。
信念
最后要讨论的指标是信念,它比其他指标稍微不直观。信念是指在 X 和 Y 独立的情况下,X 发生但 Y 不发生的预期频率与错误预测频率的比值。错误预测频率定义为 1 减去 X 的置信度
Y。记住,置信度可以定义为
,这意味着 .
。分子也可以视为
。两者的唯一区别是分子假设 X 和 Y 之间是独立的,而分母则没有。理想情况下,值大于 1,因为这意味着当 X 和 Y 之间的关联是随机偶然(换句话说,X 和 Y 是独立的)时,产品或项目集合 X 和 Y 之间的关联更常是错误的。再强调一遍,这表明 X 和 Y 之间的关联是有意义的。值为 1 表示独立性,而小于 1 的值则意味着 X 和 Y 之间的随机关系比定义为 X
Y 的关系更常见。在这种情况下,关系可能是反向的(换句话说,Y
X)。信念的范围是[0, ∞],其形式如下:

图 8.8:信念公式
让我们再次回到啤酒和葡萄酒这两个产品,但为了说明本次情况,我们将考虑葡萄酒
啤酒的相反关联。Support(Y),或在本例中为 Support(啤酒),是 3/10,而 Confidence X
Y,或在本例中为 Confidence(葡萄酒
啤酒),是 3/4。因此,Conviction(葡萄酒
啤酒)为(1-3/10) / (1-3/4) = (7/10) * (4/1)。我们可以得出结论,如果葡萄酒和啤酒是独立的,那么葡萄酒
啤酒的关联会错误出现 2.8 次。因此,之前提到的葡萄酒和啤酒的关联是有效的。
练习 40:计算指标
在本练习中,我们使用练习 39中的虚拟数据,创建样本事务数据,来计算之前描述的五个指标,我们将在讲解 Apriori 算法和关联规则时再次使用这些指标。我们将评估的关联是牛奶
面包。
注意
本章中的所有练习都需要在同一个 Jupyter 笔记本中完成。
-
定义并打印构成所有五个指标基础的频率,即 Frequency(牛奶)、Frequency(面包)和 Frequency(牛奶, 面包)。还需定义 N 为数据集中交易的总数:
N = len(example) f_x = sum(['milk' in i for i in example]) # milk f_y = sum(['bread' in i for i in example]) # bread f_x_y = sum([ all(w in i for w in ['milk', 'bread']) for i in example ]) print( "N = {}\n".format(N) + "Freq(x) = {}\n".format(f_x) + "Freq(y) = {}\n".format(f_y) + "Freq(x, y) = {}".format(f_x_y) )输出如下:
![图 8.9:频率截图]()
图 8.9:频率截图
-
计算并打印 Support(牛奶
面包):support = f_x_y / N print("Support = {}".format(round(support, 4)))x到y的支持度为0.4。根据经验,如果我们使用的是完整的交易数据集,那么这个支持值在许多情况下会被认为是非常大的。 -
计算并打印 Confidence(牛奶
面包):confidence = support / (f_x / N) print("Confidence = {}".format(round(confidence, 4)))x到y的置信度为0.5714。这意味着,给定已经购买了x,Y 被购买的概率略高于 50%。 -
计算并打印 Lift(牛奶
面包):lift = confidence / (f_y / N) print("Lift = {}".format(round(lift, 4)))x到y的提升度为1.1429。 -
计算并打印 Leverage(牛奶
面包):leverage = support - ((f_x / N) * (f_y / N)) print("Leverage = {}".format(round(leverage, 4)))x到y的杠杆度为0.05。提升度和杠杆度都可以用来说明关联x到y是正向的(换句话说,x意味着y),但弱。也就是说,值分别接近 1 和 0。 -
计算并打印 Conviction(牛奶
面包):conviction = (1 - (f_y / N)) / (1 - confidence) print("Conviction = {}".format(round(conviction, 4)))1.1667的置信度值可以解释为,如果牛奶和面包是独立的,那么牛奶
面包的关联会错误出现1.1667次。
在深入了解 Apriori 算法和实际数据上的关联规则学习之前,我们将先探索事务数据,并加载和准备一些零售数据进行建模。
事务数据的特点
在市场篮分析中使用的数据是交易数据,或者任何类似交易数据的数据。最基本的交易数据包含某种交易标识符,如发票号或交易号,以及与该标识符相关的产品列表。恰好这两项基本要素就是进行市场篮分析所需的全部内容。然而,交易数据很少——甚至可以说从未——以这种基本形式存在。交易数据通常还包括定价信息、日期和时间、客户标识符等许多其他信息:

图 8.10:每个可用的产品将映射到多个发票号码
由于交易数据的复杂性,数据清洗至关重要。在市场篮分析的背景下,数据清洗的目标是过滤掉所有不必要的信息,包括移除与分析无关的变量,并清理掉有问题的交易。完成这两步清洗所使用的技术有所不同,具体取决于交易数据文件的情况。为了避免在数据清洗中陷入困境,接下来的练习将使用来自 UCI 机器学习库的在线零售数据集的一个子集,活动将使用完整的数据集。这既限制了数据清洗的讨论,又为我们提供了一个机会,讨论当数据集大小变化时,结果如何变化。这一点很重要,因为如果你为零售商工作并进行市场篮分析,你需要理解并清楚地说明,随着数据量的增加,产品之间的关系可能会发生变化,而且很可能会发生变化。在讨论此数据集所需的具体清洗过程之前,让我们先加载在线零售数据集。
练习 41:加载数据
在本次练习中,我们将加载并查看一个示例的在线零售数据集。该数据集最初来自 UCI 机器学习库,可以在github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson08/Exercise39-Exercise45找到。下载数据集后,请保存并记下路径。现在,我们开始练习。本练习的输出结果是交易数据,未来建模练习中将使用这些数据,并通过一些探索性图形帮助我们更好地理解我们正在处理的数据。
注意
数据集来自archive.ics.uci.edu/ml/datasets/online+retail#。可以从github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson08/Exercise39-Exercise45下载。陈大庆、梁赛、郭坤,《面向在线零售行业的数据挖掘:基于 RFM 模型的客户细分案例研究》,《数据库营销与客户战略管理杂志》,第 19 卷,第 3 期,197-208 页,2012 年。
UCI 机器学习库 [http://archive.ics.uci.edu/ml]。加利福尼亚州欧文市:加利福尼亚大学信息与计算机科学学院。
-
使用
pandas中的read_excel函数加载数据。请注意,Excel 文件的第一行包含列名:online = pandas.read_excel( io="~/Desktop/Online Retail.xlsx", sheet_name="Online Retail", header=0 )注意
Online Retail.xlsx的路径应根据文件在系统中的位置进行修改。 -
打印出数据框的前 10 行。请注意,数据中包含一些与市场篮子分析无关的列:
online.head(10)输出结果如下:
![图 8.11:原始在线零售数据]()
图 8.11:原始在线零售数据
-
打印出数据框中每一列的类型。此信息在执行特定的清理任务时将非常有用:
online.dtypes输出结果如下:
![图 8.12:数据集中每列的数据类型]()
图 8.12:数据集中每列的数据类型
-
获取数据框的维度,以及唯一发票号和客户标识的数量:
print( "Data dimension (row count, col count): {dim}" .format(dim=online.shape) ) print( "Count of unique invoice numbers: {cnt}" .format(cnt=online.InvoiceNo.nunique()) ) print( "Count of unique customer ids: {cnt}" .format(cnt=online.CustomerID.nunique()) )输出结果如下:
Data dimension (row count, col count): (541909, 8) Count of unique invoice numbers: 25900 Count of unique customer ids: 4372
在本次练习中,我们已经加载了数据并进行了初步探索性工作。
数据清理与格式化
现在数据集已经加载,让我们深入探讨具体的数据清理过程。由于我们将数据筛选为仅包含发票号和商品项,我们将数据清理工作集中在数据集的这两列上。请记住,市场篮分析旨在识别所有客户在一段时间内购买商品之间的关联。因此,数据清理的主要工作是删除包含非正数商品的交易。这种情况可能发生在交易作废、商品退货或行政操作等场景中。这些类型的交易将通过两种方式进行筛选。首先,已取消的交易其发票号前面带有“C”,所以我们将识别这些特定的发票号并将其从数据中删除。另一种方法是删除所有商品数量为零或负数的交易。执行这两步后,数据将仅保留发票号和商品描述这两列,并删除任何包含至少一个缺失值的行。
数据清理的下一阶段涉及将数据转换为适合建模的格式。在此及后续的练习中,我们将使用完整数据的一个子集。这个子集通过获取前 5000 个唯一的发票号来创建。将数据缩减到前 5000 个唯一发票号后,我们将数据结构更改为运行模型所需的格式。请注意,目前数据是长格式,每个商品占一行。期望的格式是一个列表的列表,类似于本章早些时候所提到的虚构数据。每个子集列表表示一个唯一的发票号,因此,在本例中,外部列表应包含 5000 个子列表。子列表的元素是所有属于该发票号的商品。按照描述的清理过程,我们接下来开始练习。
练习 42:数据清理与格式化
在这个练习中,我们将执行之前描述的数据清理步骤。在处理过程中,我们将通过打印出数据的当前状态并计算一些基本的汇总指标来监控数据的变化。确保在加载数据的同一个笔记本中执行数据清理。
-
创建一个指示列,标明发票号是否以 "
C" 开头:online['IsCPresent'] = ( online['InvoiceNo'] .astype(str) .apply(lambda x: 1 if x.find('C') != -1 else 0) ) -
筛选出所有商品数量为零或负数的交易,使用第一步创建的列删除所有以“C”开头的发票号,将 DataFrame 子集化为
InvoiceNo和Description,最后删除所有包含至少一个缺失值的行。将 DataFrame 重命名为online1:online1 = ( online # filter out non-positive quantity values .loc[online["Quantity"] > 0] # remove InvoiceNos starting with C .loc[online['IsCPresent'] != 1] # column filtering .loc[:, ["InvoiceNo", "Description"]] # dropping all rows with at least one missing value .dropna() ) -
打印出过滤后的 DataFrame
online1的前 10 行:online1.head(10)![图 8.13:清理后的在线零售数据集]()
图 8.13:清理后的在线零售数据集
-
打印出清理后的 DataFrame 的维度以及唯一发票号的数量:
print( "Data dimension (row count, col count): {dim}" .format(dim=online1.shape) ) print( "Count of unique invoice numbers: {cnt}" .format(cnt=online1.InvoiceNo.nunique()) )输出结果如下:
Data dimension (row count, col count): (530693, 2) Count of unique invoice numbers: 20136请注意,我们已经删除了大约 10,000 行和 5,800 个发票号。
-
将发票号从 DataFrame 中提取为列表。删除重复元素,生成唯一发票号的列表。通过打印唯一发票号列表的长度来确认处理是否成功。与步骤 4的输出进行比较:
invoice_no_list = online1.InvoiceNo.tolist() invoice_no_list = list(set(invoice_no_list)) print( "Length of list of invoice numbers: {ln}" .format(ln=len(invoice_no_list)) )输出结果如下:
Length of list of invoice numbers: 20136 -
从第五步的列表中取出,仅保留前 5,000 个元素。打印新列表的长度以确认它确实是预期的 5,000 长度:
subset_invoice_no_list = invoice_no_list[0:5000] print( "Length of subset list of invoice numbers: {ln}" .format(ln=len(subset_invoice_no_list)) )输出结果如下:
Length of subset list of invoice numbers: 5000 -
仅保留前一步列表中的发票号,过滤
online1DataFrame:online1 = online1.loc[online1["InvoiceNo"].isin(subset_invoice_no_list)] -
打印出
online1的前 10 行:online1.head(10)输出结果如下:
![图 8.14:仅包含 5,000 个唯一发票号的清理后的数据集]()
图 8.14:仅包含 5,000 个唯一发票号的清理后的数据集
-
打印出 DataFrame 的维度和唯一发票号的数量,以确认过滤和清理过程是否成功:
print( "Data dimension (row count, col count): {dim}" .format(dim=online1.shape) ) print( "Count of unique invoice numbers: {cnt}" .format(cnt=online1.InvoiceNo.nunique()) )输出结果如下:
Data dimension (row count, col count): (129815, 2) Count of unique invoice numbers: 5000 -
将
online1中的数据转换为上述所述的列表形式,称为invoice_item_list。实现这一过程的方法是遍历唯一的发票号,在每次迭代时提取项目描述作为一个列表,并将该列表附加到更大的invoice_item_list列表中。打印列表中的前四个元素:invoice_item_list = [] for num in list(set(online1.InvoiceNo.tolist())): # filter dataset down to one invoice number tmp_df = online1.loc[online1['InvoiceNo'] == num] # extract item descriptions and convert to list tmp_items = tmp_df.Description.tolist() # append list invoice_item_list invoice_item_list.append(tmp_items) print(invoice_item_list[1:5])输出结果如下:

图 8.15:列表中包含四个元素的子列表,每个子列表包含属于单个发票的所有项目
注意
此步骤可能需要几分钟才能完成。
数据编码
虽然清理数据至关重要,但数据准备过程最重要的部分是将数据调整为正确的形式。在运行模型之前,当前以列表的形式存在的数据需要被编码并重新转换为 DataFrame。为此,我们将利用mlxtend的preprocessing模块中的TransactionEncoder。编码器输出的是一个多维数组,每一行的长度等于事务数据集中唯一项目的总数,元素为布尔变量,指示该特定项目是否与该行所表示的发票号相关联。数据编码后,我们可以将其重新转换为 DataFrame,其中行是发票号,列是事务数据集中的唯一项目。
在以下练习中,数据编码将使用 mlxtend 完成,但也可以不使用任何包进行编码,方法非常简单。第一步是将嵌套列表展平,返回一个包含原始嵌套列表中每个值的单一列表。接下来,去除重复的产品,并且如果需要,可以按字母顺序对数据进行排序。在进行实际编码之前,我们通过将所有元素初始化为 false 来初始化最终的 DataFrame,行数等于数据集中发票号码的数量,列名为非重复的产品名称列表。
在这种情况下,我们有 5,000 笔交易和超过 3,100 个唯一产品。因此,DataFrame 中包含超过 15,000,000 个元素。实际的编码是通过遍历每笔交易和每笔交易中的每个商品来完成的。如果交易包含某个产品,便将初始化数据集中第 i 行和第 j 列的值从 false 改为 true。由于我们需要遍历 15,000,000 个单元格,这个双重循环并不高效。虽然有一些方法可以提高性能,包括在 mlxtend 中实现的一些方法,但为了更好地理解这个过程,通过双重循环的方法是很有帮助的。以下是一个示例函数,用于在不借助任何包(除了 pandas)的情况下从头开始进行编码:
def manual_encoding(ll):
# unlist the list of lists input
# result is one list with all the elements of the sublists
list_dup_unsort_items = [element for sub in ll for element in sub]
# two cleaning steps:
# 1\. remove duplicate items, only want one of each item in list
# 2\. sort items in alphabetical order
list_nondup_sort_items = sorted(list(set(list_dup_unsort_items)))
# initialize DataFrame with all elements having False value
# name the columns the elements of list_dup_unsort_items
manual_df = pandas.DataFrame(
False,
index=range(len(ll)),
columns=list_dup_unsort_items
)
# change False to True if element is in individual transaction list
# each row is represents the contains of an individual transaction
# (sublist from the original list of lists)
for i in range(len(ll)):
for j in ll[i]:
manual_df.loc[i, j] = True
# return the True/False DataFrame
return manual_df
练习 43:数据编码
在本练习中,我们通过对上一练习中生成的嵌套列表进行编码,继续数据准备过程,以便以特定方式运行模型。
-
初始化并拟合事务编码器。打印出结果数据的示例:
online_encoder = mlxtend.preprocessing.TransactionEncoder() online_encoder_array = online_encoder.fit_transform(invoice_item_list) print(online_encoder_array)输出如下:
![图 8.16:包含表示每笔交易中产品存在的布尔变量的多维数组]()
图 8.16:包含表示每笔交易中产品存在的布尔变量的多维数组
-
将编码后的数组重构为名为
online_encoder_df的 DataFrame。打印出一个预定义的 DataFrame 子集,包含 true 和 false 值:online_encoder_df = pandas.DataFrame( online_encoder_array, columns=online_encoder.columns_ ) # this is a very big table, so for more # easy viewing only a subset is printed online_encoder_df.loc[ 4970:4979, online_encoder_df.columns.tolist()[0:8] ]输出结果将类似于以下内容:
![图 8.17:将编码数据的小部分重构为 DataFrame]()
图 8.17:将编码数据的小部分重构为 DataFrame
-
打印出编码后 DataFrame 的维度。它应该有 5,000 行,因为生成它的数据已经过滤为 5,000 个唯一的发票号码:
print( "Data dimension (row count, col count): {dim}" .format(dim=online_encoder_df.shape) )输出结果将类似于以下内容:
Data dimension (row count, col count): (5000, 3334)
数据现已准备好用于建模。在接下来的部分,我们将探索 Apriori 算法。
活动 18:加载并准备完整的在线零售数据
在本活动中,我们的任务是加载并准备一个大型事务数据集进行建模。最终输出将是一个适当编码的数据集,每个独特的事务占一行,每个独特的商品占一列。如果某个商品出现在某个事务中,那么该数据框中的该元素将标记为真。
本活动将大致重复前几次练习,但将使用完整的在线零售数据集文件。无需执行新的下载,但你需要先前下载文件的路径。请在单独的 Jupyter 笔记本中执行此活动。
以下步骤将帮助你完成此活动:
-
加载在线零售数据集文件:
注
此数据集来自
archive.ics.uci.edu/ml/datasets/online+retail#。它可以从 https://github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson08/Activity18-Activity20 下载。Daqing Chen、Sai Liang Sain 和 Kun Guo, 数据挖掘在在线零售行业中的应用:基于 RFM 模型的客户细分案例研究,发表于《数据库营销与客户战略管理杂志》,第 19 卷,第 3 期,197-208 页,2012 年。UCI 机器学习库 [http://archive.ics.uci.edu/ml]。加利福尼亚州尔湾:加利福尼亚大学信息与计算机科学学院。
-
清理并准备建模数据,包括将清理后的数据转换为列表的列表。
-
对数据进行编码并将其重塑为数据框:
注
本活动的解决方案可以在第 366 页找到。
输出将类似于以下内容:

图 8.18:从完整的在线零售数据集中清理、编码和重塑后的数据框的一个子集
Apriori 算法
Apriori 算法是一种数据挖掘方法,用于识别和量化事务数据中的频繁项集,是关联规则学习的基础组件。在下一节中,将讨论如何将 Apriori 算法的结果扩展到关联规则学习。Apriori 算法中作为频繁项集的最小值是模型的输入,因此是可调节的。频率在此通过支持度来量化,因此输入模型的值是分析中接受的最小支持度。模型随后识别所有支持度大于或等于输入给定的最小支持度的项集。请注意,最小支持度参数不是通过网格搜索可以优化的参数,因为 Apriori 算法没有评估指标。相反,最小支持度参数是根据数据、使用案例和领域专业知识来设置的。
Apriori 算法背后的主要思想是 Apriori 原则:任何频繁项集的子集必须本身也是频繁的。
另一个值得提及的方面是推论:不频繁项集的超集不可能是频繁的。
让我们举一些例子。如果项集 {锤子、锯子和钉子} 是频繁的,那么根据 Apriori 原则以及显而易见的道理,任何更简单的项集,例如 {锤子、锯子},也一定是频繁的。相反,如果同样的项集 {锤子、锯子、钉子} 是不频繁的,那么增加复杂性,比如在项集 {锤子、锯子、钉子} 中加入木材 {锤子、锯子、钉子、木材},也不会使该项集变得频繁。
计算事务数据库中每个项集的支持度值,并仅返回支持度大于或等于预设的最小支持度阈值的项集,可能看起来很简单,但实际上并非如此,因为需要进行大量计算。例如,考虑一个包含 10 个独特项的项集。这将导致 1,023 个单独的项集,需要计算它们的支持度值。现在,试着推算一下我们的工作数据集,它包含 3,135 个独特项。我们需要为这些项集计算支持度值的数量将是巨大的。计算效率是一个重大问题。

图 8.19:项集如何构建以及 Apriori 原则如何大幅减少计算需求的映射(所有灰色节点为不频繁项集)。
为了解决计算需求,Apriori 算法被定义为一个自下而上的模型,包含两个步骤。这些步骤包括通过向已存在的频繁项集中添加项目来生成候选项集,并将这些候选项集与数据集进行测试,以确定这些候选项集是否也是频繁的。对于包含不频繁项集的项集,不会计算支持度值。这个过程会一直重复,直到不再有候选项集存在:

图 8.20:假设最小支持度阈值为 0.4,图示显示了一般的 Apriori 算法结构。
前述结构包括建立项集、计算支持度值、过滤掉不频繁项集、创建新项集并重复此过程。
存在一个清晰的树状结构,作为识别候选项集的路径。所使用的具体搜索技术,是为遍历树状数据结构而设计的宽度优先搜索,这意味着搜索过程的每一步都专注于完全搜索树的一层,然后再移动到下一层,而不是逐分支进行搜索。
算法的高层步骤如下:
-
定义频繁项集。首先,这通常是单个项目的集合。
-
通过将频繁项集组合在一起,推导候选项集。每次增加一个项集的大小。也就是说,从一个项集的集合开始,逐渐增加到两个项集、三个项集,依此类推。
-
计算每个候选项集的支持度值。
-
创建一个新的频繁项集,由支持度值超过指定阈值的候选项集组成。
重复步骤 1到步骤 4,直到没有更多的频繁项集;也就是说,直到我们遍历了所有的组合。
Apriori 算法的伪代码如下:
L1 = {frequent items}
For k = 1 and L1 != empty set do
Ck+1 = candidate item sets derived from Lk
For each transaction t in the dataset do
Increment the count of the candidates in Ck+1 that appear in t
Compute the support for the candidates in Ck+1 using the appearance counts
Lk+1 = the candidates in Ck+1 meeting the minimum support requirement
End
Return L = UkLk = all frequent item sets with corresponding support values
尽管遵循 Apriori 原则,这个算法仍然可能面临显著的计算挑战,具体取决于事务数据集的大小。目前有几种策略被接受,以进一步减少计算需求。
计算修正
事务减少是一种减少计算负担的简单方法。请注意,在生成每个候选项集之后,必须扫描整个事务数据集,以统计每个候选项集的出现次数。如果我们能缩小事务数据集的大小,数据集扫描的大小将大幅减少。事务数据集的缩小通过意识到任何在第 i次迭代中不包含频繁项集的事务,在后续迭代中也不会包含频繁项集。因此,一旦每个事务不包含频繁项集,它就可以从未来扫描中使用的事务数据集中移除。
对事务数据集进行抽样并测试每个候选项集,是减少扫描事务数据集计算每个项集支持度所需的计算量的另一种方法。在实施这一方法时,重要的是要降低最小支持度要求,以确保最终数据中没有遗漏应包含的项集。由于抽样后的事务数据集会自然导致支持度值较小,因此如果将最小支持度保持在原值,将错误地从模型输出中移除那些应该是频繁项集的项集。
一种类似的方法是分区。在这种情况下,数据集被分割成若干个独立的数据集,在每个数据集上评估每个候选项集。如果某个项集在其中一个分区中频繁出现,那么它在完整的交易数据集中也被认为是频繁的。每个分区会被连续扫描,直到确定某个项集的频率。
无论是否使用这些技术,Apriori 算法的计算需求通常都会相当庞大。正如现在应该清楚的,算法的核心,支持度的计算,并不像本文讨论的其他模型那样复杂。
练习 44:执行 Apriori 算法
mlxtend使得执行 Apriori 算法变得简单。因此,本次练习将重点讲解如何操作输出的数据集以及如何解读结果。你将回忆起清洗和编码后的交易数据被定义为online_encoder_df。请在之前所有练习运行过的相同笔记本中执行本练习,因为我们将继续使用该笔记本中已经建立的环境、数据和结果。(因此,你应该使用包含 5000 条记录的缩减数据集的笔记本,而不是活动中使用的完整数据集。)
-
使用
mlxtend运行 Apriori 算法,不改变任何默认参数值:mod = mlxtend.frequent_patterns.apriori(online_encoder_df) mod输出是一个空的 DataFrame。默认的最小支持度值为 0.5,因此,由于返回了一个空的 DataFrame,我们知道所有项集的支持度都低于 0.5。根据交易的数量和可用项的多样性,没有项集支持度超过 0.5 并不罕见。
-
重新运行 Apriori 算法,但将最小支持度设置为 0.01。这个最小支持度值的含义是,在分析 5000 笔交易时,项集需要出现 50 次才被认为是频繁的。如前所述,最小支持度可以设置为[0,1]范围内的任何值。没有最优的最小支持度值;该值的设置完全是主观的。许多企业有自己的特定显著性阈值,但没有行业标准或优化此值的方法:
mod_minsupport = mlxtend.frequent_patterns.apriori( online_encoder_df, min_support=0.01 ) mod_minsupport.loc[0:6]输出将类似于以下内容:
![图 8.21:使用 mlxtend 运行的 Apriori 算法的基本输出]()
图 8.21:使用 mlxtend 运行的 Apriori 算法的基本输出
注意,输出中项集的标识是数字形式的,这使得结果难以解读。
-
重新运行 Apriori 算法,使用与步骤 2中相同的最小支持度,但这次将
use_colnames设置为 True。这样将用实际的项名称替代数字标识:mod_colnames_minsupport = mlxtend.frequent_patterns.apriori( online_encoder_df, min_support=0.01, use_colnames=True ) mod_colnames_minsupport.loc[0:6]输出将类似于以下内容:
![图 8.22:使用实际项名称而非数字标识的 Apriori 算法输出]()
图 8.22:Apriori 算法输出,使用实际商品名称代替数字表示
这个 DataFrame 包含了每个支持度值大于指定最小支持度值的商品集。也就是说,这些商品集出现的频率足够高,可能具有一定的意义,因而可以采取行动。
-
在步骤 3的输出中添加一列,包含商品集的大小,这有助于过滤和进一步分析:
mod_colnames_minsupport['length'] = ( mod_colnames_minsupport['itemsets'].apply(lambda x: len(x)) ) mod_colnames_minsupport.loc[0:6]输出结果将类似于以下内容:
![]()
图 8.23:Apriori 算法输出,外加一个包含商品集长度的额外列
-
查找包含'
10 COLOUR SPACEBOY PEN'的商品集的支持度:mod_colnames_minsupport[ mod_colnames_minsupport['itemsets'] == frozenset( {'10 COLOUR SPACEBOY PEN'} ) ]输出结果如下:
![图 8.24:输出的 DataFrame 被过滤为单一商品集]()
图 8.24:输出的 DataFrame 被过滤为单一商品集
这个单行 DataFrame 给出了这个特定商品集的支持度值,该商品集包含一个商品。支持度值表示该商品集出现在 1.5%的交易中。
-
返回所有长度为 2、支持度在[0.02, 0.021]范围内的商品集
mod_colnames_minsupport[ (mod_colnames_minsupport['length'] == 2) & (mod_colnames_minsupport['support'] >= 0.02) & (mod_colnames_minsupport['support'] < 0.021) ]输出结果将类似于以下内容:
![图 8.25:通过长度和支持度过滤后的 Apriori 算法输出 DataFrame]()
图 8.25:通过长度和支持度过滤后的 Apriori 算法输出 DataFrame
这个 DataFrame 包含了所有支持度值在步骤开始时指定范围内的商品集(成对购买的商品)。这些商品集出现在约 2.0%到 2.1%的交易中。
注意,当进行
support过滤时,最好指定一个范围而非具体的值,因为很有可能选择的值没有商品集。前面的输出有 18 个商品集。请记住这一点,并记住商品集中的具体商品,因为当我们扩展到完整数据时,可能会运行相同的过滤,并且我们希望执行对比。 -
绘制支持度值。请注意,这个图表中不会有小于 0.01 的支持度值,因为 0.01 是最小支持度值:
mod_colnames_minsupport.hist("support", grid=False, bins=30) plt.title("Support")输出结果将类似于以下图表:

图 8.26:Apriori 算法返回的支持度值分布
最大支持度值大约为 0.14,约为 700 笔交易。看似较小的值,考虑到可用商品的数量,实际上可能并不小。商品数量较多时,通常会导致较低的支持度值,因为商品组合的变化性增加。
希望你能想到更多的方式来利用这些数据,并且从支持零售业务的角度来考虑。我们将在下一节通过使用 Apriori 算法结果来生成关联规则,从而生成更多有用的信息。
活动 19:在完整的在线零售数据集上运行 Apriori 算法
想象你在一家在线零售商工作。你被提供了上个月的所有交易数据,并被要求找出在至少 1%的交易中出现的所有项集。确定符合条件的项集后,你接着被要求识别支持度值的分布。支持度值的分布将告诉所有相关方是否存在高概率一起购买的商品组,以及支持度值的平均值。让我们为公司领导和战略家收集所有信息。
在本次活动中,你将对完整的在线零售数据集运行 Apriori 算法。
注意
该数据集来自archive.ics.uci.edu/ml/datasets/online+retail#。你可以从github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson08/Activity18-Activity20下载。Daqing Chen, Sai Liang Sain, 和 Kun Guo, 《在线零售行业的数据挖掘:基于 RFM 模型的数据挖掘客户细分的案例研究》,《数据库营销与客户战略管理期刊》,第 19 卷,第 3 期,页 197-208,2012 年。
UCI 机器学习库[http://archive.ics.uci.edu/ml]。加利福尼亚州欧文市:加利福尼亚大学信息与计算机科学学院。
确保你在与之前活动相同的笔记本中完成此活动(换句话说,使用完整数据集的笔记本,而不是使用你为练习所用的 5000 个项目的子集的笔记本)。
这也将为你提供一个机会,将结果与仅使用 5000 个交易生成的结果进行比较。这是一个有趣的活动,因为它提供了一些关于随着更多数据的收集,数据如何变化的见解,同时也为支持度值在使用分区技术时如何变化提供了一些见解。请注意,练习中的做法并不是分区技术的完美代表,因为 5000 个交易数是一个任意的抽样数。
注意
本章的所有活动都需要在同一笔记本中完成。
以下步骤将帮助你完成该活动:
-
在完整数据上使用合理的参数设置运行 Apriori 算法。
-
将结果筛选至包含
10 COLOUR SPACEBOY PEN的项集。将其支持度值与练习 44,执行 Apriori 算法的结果进行比较。 -
添加另一列,包含项集长度。然后,筛选出那些长度为 2 且支持度在[0.02, 0.021]范围内的项集。将其与练习 44,执行 Apriori 算法的结果进行比较。
-
绘制
support值。注意
本次活动的解答可以在第 367 页找到。
本次活动的输出将类似于以下内容:

图 8.27:支持度值的分布
关联规则
关联规则学习是一种机器学习模型,旨在发掘交易数据中隐藏的模式(换句话说,关系),这些数据描述了任何零售商的客户的购物习惯。关联规则的定义在之前定义和解释常见的概率度量时已经有所暗示。
考虑虚拟的频繁项集 {牛奶, 面包}。可以从这个项集形成两个关联规则:牛奶
面包 和 面包
牛奶。为了简便,关联规则中的第一个项集称为前件,而第二个项集称为后件。一旦关联规则被识别出来,就可以计算之前讨论的所有度量,来评估这些关联规则的有效性,确定这些规则是否可以在决策过程中发挥作用。
关联规则的建立基于支持度和置信度。支持度,正如我们在上一节中讨论的,识别哪些项目集是频繁出现的,而置信度则衡量某个特定规则的真实性频率。置信度通常被称为有趣度的度量,因为它是决定是否应该形成关联的指标。因此,建立关联规则是一个两步过程。首先识别频繁的数据集,然后评估候选关联规则的置信度,如果该置信度值超过某个任意的阈值,则该规则就成为一个关联规则。
关联规则学习的一个主要问题是发现虚假的关联,这在潜在规则数量庞大的情况下是非常可能发生的。虚假关联被定义为那些在数据中出现的规律性令人惊讶的关联,尽管这些关联完全是偶然发生的。为了清楚地表达这个观点,假设我们处于一个拥有 100 条候选规则的情境中。如果我们在 0.05 的显著性水平上进行独立性统计检验,我们仍然会面临 5%的概率,即使没有关联,仍然会发现关联。进一步假设所有的 100 条候选规则都不是有效的关联。由于 5%的概率,我们仍然会期望发现 5 条有效的关联规则。现在,将这些假设的候选规则列表规模扩大到百万或十亿级别,那么这 5%的概率就会产生一个巨大的关联数量。这个问题与几乎所有模型面临的统计显著性和错误问题类似。值得指出的是,确实存在一些技术可以用来应对虚假关联问题,但这些技术既没有在常用的关联规则库中得到一致的应用,也不在本章的讨论范围内。
现在让我们将已掌握的关联规则学习知识应用到在线零售数据集上。
练习 45:推导关联规则
在本次练习中,我们将为在线零售数据集推导关联规则并探索相关度量。确保在与之前练习相同的笔记本中完成此练习(换句话说,使用 5,000 项子集的笔记本,而不是活动中的完整数据集)。
-
使用
mlxtend库为在线零售数据集推导关联规则。使用置信度作为有趣性度量,将最小阈值设置为 0.6,并返回所有的度量,而不仅仅是支持度。计算返回的关联规则数量:rules = mlxtend.frequent_patterns.association_rules( mod_colnames_minsupport, metric="confidence", min_threshold=0.6, support_only=False ) rules.loc[0:6]输出结果类似于以下内容:
![图 8.28: 仅使用 5,000 笔交易生成的关联规则的前 7 行]()
图 8.28: 仅使用 5,000 笔交易生成的关联规则的前 7 行
-
按如下方式打印关联规则的数量:
print("Number of Associations: {}".format(rules.shape[0]))找到的关联规则数量为 5,070 条。
注:
关联规则的数量可能不同。
-
尝试运行模型的另一个版本。选择任何最小阈值和有趣性度量。计算并探索返回的规则:
rules2 = mlxtend.frequent_patterns.association_rules( mod_colnames_minsupport, metric="lift", min_threshold=50, support_only=False ) rules2.loc[0:6]输出结果如下:
![图 8.29: 关联规则的前 7 行]()
图 8.29: 关联规则的前 7 行
-
按如下方式打印关联规则的数量:
print("Number of Associations: {}".format(rules2.shape[0]))使用 lift 度量和最小阈值为 50 时找到的关联规则数量为 26,比步骤 2中的数量明显少。我们将看到,50 是一个相当高的阈值,因此返回的关联规则较少并不令人惊讶。
-
将置信度与支持度作图并识别数据中的特定趋势:
rules.plot.scatter("support", "confidence", alpha=0.5, marker="*") plt.xlabel("Support") plt.ylabel("Confidence") plt.title("Association Rules") plt.show()输出如下:
![图 8.30:置信度与支持度的关系图]()
图 8.30:置信度与支持度的关系图
请注意,没有任何关联规则同时具有极高的置信度和极高的支持度。这应该是可以理解的。如果一个项集有很高的支持度,那么这些项很可能会与许多其他项一起出现,这就使得置信度很高的可能性非常低。
-
查看置信度的分布:
rules.hist("confidence", grid=False, bins=30) plt.title("Confidence")输出如下:
![图 8.31:置信度值的分布]()
图 8.31:置信度值的分布
-
现在,查看提升值的分布:
rules.hist("lift", grid=False, bins=30) plt.title("Lift")输出如下:
![图 8.32:提升值的分布]()
图 8.32:提升值的分布
如前所述,此图显示 50 是一个较高的阈值,因为在该值之上的点并不多。
-
现在,查看杠杆值的分布:
rules.hist("leverage", grid=False, bins=30) plt.title("Leverage")输出如下:
![图 8.33:杠杆值的分布]()
图 8.33:杠杆值的分布
-
现在,查看定罪度的分布:
plt.hist( rules[numpy.isfinite(rules['conviction'])].conviction.values, bins = 30 ) plt.title("Conviction")输出如下:

图 8.34:定罪度值的分布
四个分布中有趣的是,在图表的上端出现了不同大小的尖峰,这意味着有一些非常强的关联规则。随着置信度值增大,置信度的分布逐渐下降,但在最高值附近,分布略微上升。提升分布有最明显的尖峰。定罪度分布图在 50 左右有一个小的尖峰,也许更准确地说是一个小的突起。最后,杠杆分布在较高的值处并没有显著的尖峰,但它确实显示了一个长尾,包含一些非常高的杠杆值。
花一些时间探索模型发现的关联规则。产品配对对你有意义吗?当你改变模型参数值时,关联规则的数量发生了什么变化?你是否理解这些规则在尝试改善任何零售业务时可能产生的影响?
活动 20:在完整的在线零售数据集上查找关联规则
让我们继续在活动 19 完整在线零售数据集上的 Apriori 中设定的场景。公司领导回来说,知道每个项集在数据集中出现的频率很好,但我们可以采取哪些项集的行动?哪些项集可以用来改变商店布局或调整定价?为了找到这些答案,我们推导出了完整的关联规则。
在本次活动中,我们将从完整的在线零售交易数据集推导关联规则。确保你在使用完整数据集的笔记本中完成此活动(换句话说,就是使用完整零售数据集的笔记本,而不是练习中使用的包含 5,000 个商品子集的笔记本)。
以下步骤将帮助我们完成此活动:
-
在完整数据集上拟合关联规则模型。使用指标置信度,并设定最小阈值为 0.6。
-
计算关联规则的数量。这个数量与练习 45 的步骤 1中找到的数量是否不同?推导关联规则?
-
绘制置信度与支持度的关系图。
-
查看置信度、提升度、杠杆度和确信度的分布情况。
注意
本次活动的解决方案可以在第 370 页找到。
到本次活动结束时,你将获得关于提升度、杠杆度和确信度的图表。
总结
市场篮子分析用于分析和提取来自交易或类似交易的数据的见解,这些见解可以帮助推动多个行业的增长,最著名的就是零售行业。这些决策可能包括如何布置零售空间、打折哪些产品以及如何定价。市场篮子分析的核心支柱之一是建立关联规则。关联规则学习是一种机器学习方法,用于发现消费者购买商品之间足够强的关联,这些关联可以在商业决策中加以利用。关联规则学习依赖于 Apriori 算法,以计算高效的方式找到频繁项集。这些模型与传统的机器学习模型不同,因为它们不进行预测,结果不能通过单一指标来评估,且参数值不是通过网格搜索选择的,而是由特定问题的领域需求来决定。尽管如此,所有机器学习模型的核心目标——模式提取,在这里依然存在。在本章结束时,你应该能舒适地评估和解读概率指标,能够运行并调整使用mlxtend的 Apriori 算法和关联规则学习模型,并了解这些模型在商业中的应用。你应该知道,附近超市中的商品陈列和定价很可能是根据你和其他顾客过去的行为做出的决策!
在下一章,我们将探讨使用核密度估计的热点分析,毫无疑问,这是所有统计学和机器学习中最常用的算法之一。
第九章:热点分析
学习目标
在本章结束时,你将能够:
-
了解空间建模的一些应用
-
在适当的背景下部署热点模型
-
构建核密度估计模型
-
执行热点分析并可视化结果
在本章中,我们将学习核密度估计,并学习如何进行热点分析。
引言
让我们考虑一个假设的情景:一种新的疾病已开始在你所在的国家的多个社区传播,政府正在努力找出如何应对这一健康紧急情况。应对这一健康紧急情况的关键是流行病学知识,包括患者的位置和疾病的传播方式。能够定位和量化问题区域(通常称为热点)可以帮助卫生专业人员、政策制定者和应急响应团队制定最有效的应对策略。这个情景突出了热点建模的众多应用之一。
热点建模是一种用于识别人口在地理区域分布的方式的方法;例如,前面提到的疾病感染者在全国范围内的分布。创建这种分布依赖于代表性样本数据的可用性。请注意,人口可以是任何在地理术语上可定义的事物,包括但不限于犯罪、感染疾病的个体、具有特定人口特征的人群或飓风:

图 9.1:一个虚构的火灾位置数据示例,展示了一些潜在的热点
热点分析非常流行,主要是因为它易于可视化结果,并且易于阅读和解释这些可视化图像。报纸、网站、博客和电视节目都利用热点分析来支持其中的论点、章节和话题。虽然它可能不像最流行的机器学习模型那样知名,但主要的热点分析算法——核密度估计,无疑是最广泛使用的分析技术之一。核密度估计是一种热点分析技术,用于估计特定地理事件的真实人口分布。
空间统计
空间统计学是统计学的一个分支,专注于分析具有空间特性的数据显示,包括地理或拓扑坐标。它与时间序列分析相似,目标是分析在某一维度上发生变化的数据。在时间序列分析中,数据变化的维度是时间,而在空间统计学中,数据则在空间维度上变化。空间统计学涵盖了多种技术,但我们这里关注的技术是核密度估计。
如大多数统计分析的目标一样,在空间统计学中,我们试图通过采样地理数据并利用这些数据生成见解和做出预测。地震分析是空间统计分析常见的一个应用领域。通过收集地震位置数据,可以生成标识高低地震可能性的地图,这可以帮助科学家确定未来地震发生的可能位置及强度预期。
概率密度函数
核密度估计使用概率密度函数(PDF)的概念,这是统计学中的一个基础概念。概率密度函数是一个描述连续随机变量行为的函数。也就是说,它表示随机变量取某一范围值的可能性或概率。以美国男性的身高为例,通过使用美国男性身高的概率密度函数,我们可以确定某位美国男性身高在 1.9 米到 1.95 米之间的概率:

图 9.2:标准正态分布
可能是统计学中最流行的密度函数就是标准正态分布,它是以零为中心,标准差为一的正态分布。
与密度函数不同,统计学家或数据科学家通常只能获得从一个未知的总体分布中随机采集的样本值。这正是核密度估计的应用场景,它是一种利用样本数据估计随机变量的未知概率密度函数的技术:

图 9.3:三种正态分布的混合
在商业中使用热点分析
我们已经提到了一些可以利用热点建模对行业产生重要影响的方式。以下是热点建模的常见应用案例。
在报告传染性疾病时,卫生组织和媒体公司通常使用热点分析来传达疾病的地理分布及其根据地理位置的传播可能性。通过使用热点分析,这些信息可以被可靠地计算并传播。热点分析非常适合处理健康数据,因为可视化图表非常直接。这意味着数据被故意或无意地误解的可能性相对较低。
热点分析还可以用于预测某些事件在地理上的发生概率。越来越多的研究领域正在利用热点分析的预测能力,其中一个例子就是环境科学领域,包括自然灾害和极端天气事件的研究。例如,地震就以难以预测而闻名,因为重大地震之间的时间间隔可能较长,而所需的用于追踪和测量地震的机械设备相对较新。
在公共政策和资源部署方面,热点分析在分析人口统计数据时可以产生重大影响。确定应该部署哪些资源(无论是金钱还是人力)可能是具有挑战性的;然而,考虑到资源往往是特定于人口的,热点分析是一种有用的技术,因为它可以用于确定某些人口统计特征的分布。这里的人口统计特征指的是我们可以找到高中毕业生、来自特定全球区域的移民或年收入超过 10 万美元的个人的地理分布。
核密度估计
热点分析的主要方法之一是核密度估计。核密度估计通过样本数据和两个被称为核函数和带宽值的参数来构建估计密度。估计的密度与任何分布一样,本质上是对随机变量行为的一个指导。在这里,我们指的是随机变量取特定值的频率,
。在处理通常为地理数据的热点分析时,估计的密度回答了这个问题:特定的经纬度对出现的频率是多少?。如果某个特定的经纬度对,
,以及其他附近的经纬度对出现频率较高,那么使用样本数据构建的估计密度将预示着该经纬度对周围区域的可能性较高。
核密度估计被称为一种平滑算法,因为估计密度的过程实际上是通过忽略样本数据中的异常和离群值来估计数据的潜在形状。换句话说,核密度估计去除了数据中的噪声。该模型的唯一假设是数据确实属于某个可解释且有意义的密度,能够从中提取洞察并付诸实践。也就是说,存在一个真实的潜在分布。
可以说,比本书中的任何其他话题,核密度估计体现了统计学的基本思想,即利用有限大小的样本数据对总体做出推断。我们假设样本数据包含数据点的聚类,这些聚类意味着总体中高可能性的区域。创建高质量的真实总体密度估计的好处是,估计的密度可以用来从总体中抽取更多的数据。
在这段简要介绍之后,你可能会有以下两个问题:
-
什么是带宽值?
-
什么是核函数?
我们接下来会回答这两个问题。
带宽值
核密度估计中最关键的参数被称为带宽值,它对估计结果的影响不可过高估计。带宽值的高层次定义是,它决定了平滑的程度。如果带宽值低,那么估计的密度将具有有限的平滑度,这意味着密度会捕捉到样本数据中的所有噪声。如果带宽值高,那么估计的密度将非常平滑。过于平滑的密度会移除估计密度中那些真实的且不只是噪声的特征。
在更多的统计学或机器学习语言中,带宽参数控制着偏差-方差权衡。也就是说,低带宽值会导致高方差,因为密度对样本数据的方差非常敏感。低带宽值限制了模型适应样本数据中未在总体中出现的空缺的能力。使用低带宽值估计的密度往往会出现过拟合(这也被称为过度平滑的密度)。当使用高带宽值时,所得的密度会出现欠拟合,并且估计的密度会有较高的偏差(这也被称为过度平滑的密度)。
练习 46:带宽值的影响
在本练习中,我们将拟合九种不同的模型,每个模型使用不同的带宽值,来处理练习中创建的样本数据。这里的目标是巩固我们对带宽参数影响的理解,并明确表示,如果需要准确的估计密度,则必须小心选择带宽值。请注意,找到最优带宽值将是下一部分的主题。所有练习将在 Jupyter notebook 中使用 Python 3 完成;确保所有包的安装通过pip进行。安装mpl_toolkits中的basemap模块的最简单方法是使用Anaconda。下载和安装Anaconda的说明可以在本书的开头找到:
-
加载本章练习所需的所有库。在这里,
matplotlib库用于创建基本图形;basemap库用于创建涉及位置数据的图形;numpy库用于处理数组和矩阵;pandas库用于处理 DataFrame;scipy库用于 Python 中的科学计算;seaborn库用于创建更具吸引力和复杂的图形;sklearn库用于访问数据、处理数据和运行模型。此外,确保图形以内联方式运行并设置为seaborn,以便所有图形以seaborn图形的形式呈现:get_ipython().run_line_magic('matplotlib', 'inline') import matplotlib.pyplot as plt import mpl_toolkits.basemap import numpy import pandas import scipy.stats import seaborn import sklearn.datasets import sklearn.model_selection import sklearn.neighbors seaborn.set() -
创建一些样本数据(
vals),通过混合三个正态分布。除了样本数据外,还定义真实的密度曲线(true_density)以及数据将被绘制的范围(x_vec):x_vec = numpy.linspace(-30, 30, 10000)[:, numpy.newaxis] vals = numpy.concatenate(( numpy.random.normal(loc=1, scale=2.5, size=500), numpy.random.normal(loc=10, scale=4, size=500), numpy.random.normal(loc=-12, scale=5, size=500) ))[:, numpy.newaxis] true_density = ( (1 / 3) * scipy.stats.norm(1, 2.5).pdf(x_vec[:, 0]) + (1 / 3) * scipy.stats.norm(10, 4).pdf(x_vec[:, 0]) + (1 / 3) * scipy.stats.norm(-12, 5).pdf(x_vec[:, 0]) ) -
定义一个元组列表,用于指导创建多图形。每个元组包含特定子图的行和列索引,以及用于在该特定子图中创建估计密度的带宽值:
position_bandwidth_vec = [ (0, 0, 0.1), (0, 1, 0.4), (0, 2, 0.7), (1, 0, 1.0), (1, 1, 1.3), (1, 2, 1.6), (2, 0, 1.9), (2, 1, 2.5), (2, 2, 5.0) ] -
创建九个图形,每个图形使用不同的带宽值。第一个图形(索引为(0, 0))将具有最低带宽,最后一个图形(索引为(2, 2))将具有最高带宽。这些值不是绝对最低或绝对最高的带宽值,而仅仅是前一步中定义的列表中的最小和最大值:
fig, ax = plt.subplots(3, 3, sharex=True, sharey=True, figsize=(12, 9)) fig.suptitle('The Effect of the Bandwidth Value', fontsize=16) for r, c, b in position_bandwidth_vec: kde = sklearn.neighbors.KernelDensity(bandwidth=b).fit(vals) log_density = kde.score_samples(x_vec) ax[r, c].hist(vals, bins=50, density=True, alpha=0.5) ax[r, c].plot(x_vec[:, 0], numpy.exp(log_density), '-', linewidth=2) ax[r, c].set_title('Bandwidth = {}'.format(b))输出如下:

图 9.4:一个 3 x 3 的子图矩阵;每个子图都使用九个带宽值之一创建了一个估计的密度
注意,在较低带宽值时,密度曲线明显过度拟合数据。随着带宽值的增加,估计的密度变得更加平滑,直到明显低估数据。视觉上看,最优带宽可能在 1.6 左右。
下一步是设计一个算法来识别最优带宽值,从而使得估计的密度最为合理,因此也是最可靠和可操作的。
选择最优带宽
如前一个练习中提到的,我们可以通过仅仅比较几种密度来接近选择最优带宽。然而,这既不是选择参数值的最有效方法,也不是最可靠的方法。
有两种标准方法可以优化带宽值,这两种方法将在未来的练习和活动中出现。第一种方法是插件法(或公式化方法),它是确定性的,并且没有在样本数据上进行优化。插件方法通常实现更快,代码更简单,解释起来也更容易。然而,这些方法有一个重大缺点,即与在样本数据上进行优化的方法相比,其准确性往往较差。这些方法还具有分布假设。最流行的插件方法是 Silverman 规则和 Scott 规则。默认情况下,seaborn包(将在未来的练习中使用)使用 Scott 规则作为确定带宽值的方法。
第二种,也是更强健的方法,是通过搜索预定义的带宽值网格来找到最优的带宽值。网格搜索是一种经验性的方法,常用于机器学习和预测建模中,以优化模型的超参数。这个过程从定义带宽网格开始,带宽网格只是待评估的带宽值的集合。使用网格中的每个带宽值来创建估计的密度;然后,使用伪对数似然值对估计的密度进行评分。最优带宽值是具有最大伪对数似然值的那个。可以把伪对数似然值看作是获取我们确实获得数据点的概率和未获取数据点的概率。理想情况下,这两个概率应该都很大。考虑一下获取我们确实获得数据点的概率较低的情况。在这种情况下,意味着样本中的数据点是异常的,因为在真实分布下,获取我们确实获得的点的概率应该不高。
现在,让我们实现网格搜索方法来优化带宽值。
练习 47:使用网格搜索选择最优带宽
在本练习中,我们将为练习 46中创建的样本数据生成估计密度,带宽值的影响,并使用网格搜索和交叉验证方法确定最优带宽值。为了进行带有交叉验证的网格搜索,我们将使用sklearn,这是本书中一直使用的工具。这个练习是练习 1 的延续,因为我们使用的是相同的样本数据,并继续探索带宽值:
-
定义带宽值的网格和网格搜索交叉验证模型。理想情况下,应该使用逐一剔除交叉验证方法,但为了使模型在合理的时间内运行,我们将采用 10 倍交叉验证。按如下方式拟合模型:
bandwidths = 10 ** numpy.linspace(-1, 1, 100) grid = sklearn.model_selection.GridSearchCV( estimator=sklearn.neighbors.KernelDensity(kernel="gaussian"), param_grid={"bandwidth": bandwidths}, cv=10 #sklearn.model_selection.LeaveOneOut().get_n_splits(vals) ) grid.fit(vals) -
从模型中提取最优带宽值,如下所示:
best_bandwidth = grid.best_params_["bandwidth"] print( "Best Bandwidth Value: {}" .format(best_bandwidth) )最优带宽值应该大约为 2。我们可以将最优带宽值解释为生成最大伪对数似然值的带宽值。请注意,根据网格中包含的值,最优带宽值可能会有所变化。
-
绘制样本数据的直方图,叠加真实密度和估计密度。在这种情况下,估计密度将是最优估计密度:
fig, ax = plt.subplots(figsize=(14, 10)) ax.hist(vals, bins=50, density=True, alpha=0.5, label='Sampled Values') ax.fill( x_vec[:, 0], true_density, fc='black', alpha=0.3, label='True Distribution' ) log_density = numpy.exp(grid.best_estimator_.score_samples(x_vec)) ax.plot( x_vec[:, 0], log_density, '-', linewidth=2, label='Kernel = Gaussian' ) ax.legend(loc='upper right')输出如下:

图 9.5:随机样本的直方图,叠加了真实密度和最优估计密度
估计密度没有明显的过拟合或欠拟合,且它确实捕捉到了三个聚类。可以说,它可能会更好地映射到真实密度,但这只是由有局限性的模型生成的估计密度。
现在让我们继续第二个问题:什么是核函数,它在模型中扮演什么角色?
核函数
另一个需要设置的参数是核函数。核是一个非负函数,控制密度的形状。像主题模型一样,我们在一个非负环境中工作,因为负的可能性或概率是没有意义的。
核函数通过系统地加权点来控制估计密度的形状。这种加权的系统方法相当简单;与许多其他数据点接近的数据点会被加权,而孤立或远离其他数据点的数据点则会被减权。加权的数据点将对应于最终估计密度中较高可能性的点。
可以使用多种函数作为核,但六种常见选择是高斯核、顶帽核、埃潘尼基诺夫核、指数核、线性核和余弦核。这些函数分别代表独特的分布形状。请注意,在每个公式中,参数h表示带宽值:
- 高斯:

图 9.6:高斯核函数的公式
- Tophat 核:

图 9.7:Tophat 核函数的公式
- Epanechnikov 核:

图 9.8:Epanechnikov 核函数的公式
- 指数型:

图 9.9:指数核函数的公式
- 线性型:

图 9.10:线性核函数的公式
- 余弦型:

图 9.11:余弦核函数的公式
这里是六种核函数的分布形状:

图 9.12:六种核函数的整体形状
核函数的选择并非完全不重要,但它绝对没有带宽值的选择那么重要。一个合理的行动方案是,对于所有的密度估计问题使用高斯核函数,这也是我们在接下来的练习和活动中将要做的。
练习 48:核函数的影响
本练习的目标是理解核函数的选择如何影响密度估计的质量。就像我们在探索带宽值的影响时一样,我们将保持其他所有参数不变,使用在前两个练习中生成的相同数据,并使用先前指定的六种核函数运行六个不同的核密度估计模型。六个估算的密度之间应该能看到明显的差异,但这些差异应该比使用不同带宽值时的密度差异稍微小一些:
-
定义一个与之前定义的类似的元组列表。每个元组包括子图的行列索引,以及用于创建密度估计的核函数:
position_kernel_vec = [ (0, 0, 'gaussian'), (0, 1, 'tophat'), (1, 0, 'epanechnikov'), (1, 1, 'exponential'), (2, 0, 'linear'), (2, 1, 'cosine'), ] Fit and plot six kernel density estimation models using a different kernel function for each. To truly understand the differences between the kernel functions, we will set the bandwidth value to the optimal bandwidth value found in Exercise 2 and not adjust it: fig, ax = plt.subplots(3, 2, sharex=True, sharey=True, figsize=(12, 9)) fig.suptitle('The Effect of Different Kernels', fontsize=16) for r, c, k in position_kernel_vec: kde = sklearn.neighbors.KernelDensity( kernel=k, bandwidth=best_bandwidth ).fit(vals) log_density = kde.score_samples(x_vec) ax[r, c].hist(vals, bins=50, density=True, alpha=0.5) ax[r, c].plot(x_vec[:, 0], numpy.exp(log_density), '-', linewidth=2) ax[r, c].set_title('Kernel = {}'.format(k.capitalize()))输出结果如下:

图 9.13:一个 3 x 2 的子图矩阵,每个子图展示使用六种核函数之一估算的密度
在这六种核函数中,高斯核函数产生了最合理的估算密度。更进一步,注意到不同核函数下的估算密度之间的差异小于使用不同带宽值时的差异。这证明了之前的观点,即带宽值是更重要的参数,应该在模型构建过程中重点关注。
在我们大致理解的基础上,接下来让我们以高层次的方式讨论核密度估计的推导过程。
核密度估计推导
我们跳过正式的数学推导,而采用直观的流行推导方法。核密度估计将样本中的每个数据点转化为其自身的分布,其宽度由带宽值控制。然后,将这些单独的分布相加,以创建所需的密度估计。这个概念相对容易演示;然而,在接下来的练习中之前,让我们尝试以抽象的方式思考它。对于包含许多样本数据点的地理区域,单独的密度将会重叠,并且通过加总这些密度,将在估计密度中创建高概率点。同样,对于包含少量甚至没有样本数据点的地理区域,单独的密度不会重叠,因此在估计密度中对应的将是低概率点。
练习 49:模拟核密度估计的推导
这里的目标是演示将单独分布相加,以创建随机变量的总体估计密度的概念。我们将通过从一个样本数据点开始,逐步建立这个概念,随后增加更多的样本数据点。此外,还将应用不同的带宽值,从而进一步巩固我们对带宽值对这些单独密度影响的理解:
-
定义一个函数来评估正态分布。输入值包括代表随机变量范围的网格 X、采样数据点 m 和带宽 b:
def eval_gaussian(x, m, b): numerator = numpy.exp( -numpy.power(x - m, 2) / (2 * numpy.power(b, 2)) ) denominator = b * numpy.sqrt(2 * numpy.pi) return numerator / denominator -
将单个样本数据点绘制为直方图,并与不同带宽值下的单独密度进行比较:
m = numpy.array([5.1]) b_vec = [0.1, 0.35, 0.8] x_vec = numpy.linspace(1, 10, 100)[:, None] fig, ax = plt.subplots(2, 3, sharex=True, sharey=True, figsize=(15, 10)) for i, b in enumerate(b_vec): ax[0, i].hist(m[:], bins=1, fc='#AAAAFF', density=True) ax[0, i].set_title("Histogram: Normed") evaluation = eval_gaussian(x_vec, m=m[0], b=b) ax[1, i].fill(x_vec, evaluation, '-k', fc='#AAAAFF') ax[1, i].set_title("Gaussian Dist: b={}".format(b))输出如下:
![图 9.14:展示一个数据点及其在不同带宽值下的单独密度]()
图 9.14:展示一个数据点及其在不同带宽值下的单独密度
在这里,我们看到已经建立的结论,即较小的带宽值会产生非常狭窄的密度,容易导致数据过拟合。
-
重新生成步骤 2中的工作,但这次扩展到 16 个数据点:
m = numpy.random.normal(4.7, 0.88, 16) n = len(m) b_vec = [0.1, 0.35, 1.1] x_vec = numpy.linspace(-1, 11, 100)[:, None] fig, ax = plt.subplots(2, 3, sharex=True, sharey=True, figsize=(15, 10)) for i, b in enumerate(b_vec): ax[0, i].hist(m[:], bins=n, fc='#AAAAFF', density=True) ax[0, i].set_title("Histogram: Normed") sum_evaluation = numpy.zeros(len(x_vec)) for j in range(n): evaluation = eval_gaussian(x_vec, m=m[j], b=b) / n sum_evaluation += evaluation[:, 0] ax[1, i].plot(x_vec, evaluation, '-k', linestyle="dashed") ax[1, i].fill(x_vec, sum_evaluation, '-k', fc='#AAAAFF') ax[1, i].set_title("Gaussian Dist: b={}".format(b))输出如下:

图 9.15:展示 16 个数据点、它们在不同带宽值下的单独密度,以及它们单独密度的总和
如预期的那样,使用最小带宽值的图形呈现出严重过拟合的估计密度。也就是说,估计密度捕捉到了样本数据中的所有噪声。在这三种密度中,第二种密度,即带宽值设置为 0.35 时的估计密度,最为合理。
活动 21:估计一维密度
在这第一个活动中,我们将生成一些虚拟示例数据,并使用核密度估计估计密度函数。带宽值将通过网格搜索交叉验证进行优化。目标是通过在一个简单的单维案例中运行模型,巩固我们对这一有用方法论的理解。我们将再次使用 Jupyter notebooks 来完成这项工作。
假设我们将要创建的示例数据描述的是美国某州的房价。暂时忽略以下示例数据中的数值。问题是,房价的分布是什么样的,我们能否提取出房子价格落在某个特定范围内的概率?这些问题以及更多的问题都可以通过核密度估计来回答。
完成该活动的步骤如下:
-
打开一个新的笔记本,并安装所有必要的库。
-
从标准正态分布中采样 1,000 个数据点。将 3.5 加到样本的最后 625 个值上(即,375 到 1,000 之间的索引)。设置随机状态为 100。为此,使用
numpy.random.RandomState设置一个随机状态为 100,以保证相同的采样值,然后使用randn(1000)调用随机生成数据点。 -
将 1,000 个样本数据绘制成直方图,并在其下方添加散点图。
-
定义一个带宽值的网格。然后,定义并拟合一个网格搜索交叉验证算法。
-
提取最佳带宽值。
-
重新绘制步骤 3中的直方图,并叠加估计的密度。
输出结果如下:

图 9.16:带有最佳估计密度叠加的随机样本直方图
注意
这个活动的解决方案可以在第 374 页找到。
热点分析
首先,热点是数据点浓度较高的区域,例如犯罪率异常高的特定街区,或遭受异常多龙卷风影响的某些地区。热点分析是通过使用采样数据,在总体中寻找这些热点的过程。这个过程通常通过利用核密度估计来完成。
热点分析可以通过四个高层次步骤来描述:
-
收集数据:数据应包括对象或事件的位置。如我们简要提到的,进行分析并获得可操作结果所需的数据量相对灵活。理想状态是拥有一个能够代表总体的样本数据集。
-
确定基础地图:下一步是确定哪种基础地图最适合项目的分析和展示需求。在这张基础地图上,将叠加模型的结果,以便更容易地以城市、街区或区域等更易理解的术语表达热点的位置。
-
执行模型:在此步骤中,您将选择并执行一种或多种提取空间模式以识别热点的方法。对于我们来说,这种方法将是——毫无悬念——核密度估计。
-
创建可视化:热点图是通过将模型结果叠加在基础地图上生成的,旨在支持解决任何悬而未决的业务问题。
从可用性角度来看,热点分析的一个主要问题是热点的统计显著性并不是特别容易确定。关于统计显著性的大部分问题围绕热点的存在展开。也就是说,发生可能性的波动是否真的构成统计学上的显著波动?需要注意的是,执行核密度估计并不要求统计显著性,且我们在后续过程中将完全不涉及显著性问题。
尽管“热点”一词传统上用来描述一组地理位置数据点,但它不限于位置数据。任何类型的数据都可能有热点,无论这些数据是否被称为热点。在接下来的练习中,我们将对一些非位置数据进行建模,以找出热点,这些热点是特征空间中发生可能性较高或较低的区域。
练习 50:加载数据并使用 Seaborn 进行建模
在本次练习中,我们将使用 seaborn 库来拟合和可视化核密度估计模型。这将应用于位置数据和非位置数据。开始建模之前,我们加载数据,这些数据是与 sklearn 一起自动加载的加利福尼亚住房数据集。该数据集来源于 1990 年美国人口普查,描述了当时加利福尼亚的住房情况。数据集中的每一行描述了一个人口普查块组。人口普查块组的定义与本次练习无关,因此我们将跳过对其的定义,专注于更多的实操编码与建模。需要提到的是,所有变量都是按人口普查块进行聚合的。例如,MedInc 是每个人口普查块的家庭收入中位数。关于此数据集的更多信息,请访问 scikit-learn.org/stable/datasets/index.html#california-housing-dataset:
-
使用
fetch_california_housing()加载加利福尼亚住房数据集。使用pandas将数据转换为 DataFrame 并打印出 DataFrame 的前五行:housing = sklearn.datasets.fetch_california_housing() df = pandas.DataFrame(housing['data'], columns=housing['feature_names']) print("Dataframe Dimensions: {dims}".format(dims=df.shape)) df.head()输出如下:
![图 9.17:来自 sklearn 的加利福尼亚住房数据集的前五行]()
图 9.17:来自 sklearn 的加利福尼亚住房数据集的前五行
-
根据
HouseAge特征过滤数据框,该特征表示每个普查区块的房屋中位数年龄。仅保留HouseAge小于或等于 15 的行,并将数据框命名为dfLess15。打印数据框的前五行;然后,将数据框缩减为仅包含经度和纬度特征:dfLess15 = df[df['HouseAge'] <= 15.0] dfLess15 = dfLess15[['Latitude', 'Longitude']] print( "Less Than Or Equal To 15 Years Dataframe Dimensions: {dims}" .format(dims=dfLess15.shape) ) dfLess15.head()输出如下:
![图 9.18:过滤后的数据集前五行,仅包含列值小于或等于 15 的行]()
图 9.18:过滤后的数据集前五行,仅包含
HouseAge列值小于或等于 15 的行 -
使用
seaborn拟合并可视化基于经度和纬度数据点构建的核密度估计模型。seaborn拟合这些模型的方法使用了 Scott 规则。该模型有四个输入,它们是求估计密度的两列的名称(即经度和纬度)、这些列所属的数据框,以及密度估计的方法(即kde或核密度估计):seaborn.jointplot("Longitude", "Latitude", dfLess15, kind="kde")输出如下:
![图 9.19:包含二维估计密度以及 dfLess15 数据集的边际密度的联合图]()
图 9.19:包含二维估计密度以及 dfLess15 数据集的边际密度的联合图
如果我们将这些结果叠加到加利福尼亚州的地图上,我们将看到热点位于南加州,包括洛杉矶和圣地亚哥,湾区,包括旧金山,以及在一定程度上被称为中央谷地的地区。这个
seaborn图形的一个优点是,我们可以得到二维估计密度以及经度和纬度的边际密度。 -
基于
HouseAge特征创建另一个过滤后的数据框;这次仅保留HouseAge大于 40 的行,并将数据框命名为dfMore40。此外,移除所有列,保留经度和纬度。然后,打印数据框的前五行:dfMore40 = df[df['HouseAge'] > 40.0] dfMore40 = dfMore40[['Latitude', 'Longitude']] print( "More Than 40 Years Dataframe Dimensions: {dims}" .format(dims=dfMore40.shape) ) dfMore40.head()输出如下:
![图 9.20:过滤后的数据集顶部,仅包含列中大于 40 的行]()
图 9.20:过滤后的数据集顶部,仅包含
HouseAge列中大于 40 的行 -
重复步骤 3的过程,但这次使用新的过滤后的数据框:
seaborn.jointplot("Longitude", "Latitude", dfMore40, kind="kde")输出如下:
![图 9.21:包含二维估计密度以及 dfMore40 数据集的边际密度的联合图]()
图 9.21:包含二维估计密度以及 dfMore40 数据集的边际密度的联合图
这个估算密度要紧凑得多,因为数据几乎完全聚集在两个区域。这些区域是洛杉矶和湾区。将其与步骤 3中的图形进行比较,我们注意到住房开发已经遍布全州。此外,新建住房开发在更多普查区块中出现的频率更高。
-
我们再次创建一个新的过滤后的 DataFrame。这次仅保留
HouseAge小于或等于 5 的行,并将该 DataFrame 命名为dfLess5。绘制Population和MedInc的散点图,方法如下:dfLess5 = df[df['HouseAge'] <= 5] x_vals = dfLess5.Population.values y_vals = dfLess5.MedInc.values fig = plt.figure(figsize=(10, 10)) plt.scatter(x_vals, y_vals, c='black') plt.xlabel('Population', fontsize=18) plt.ylabel('Median Income', fontsize=16)输出结果如下:
![图 9.22:中位收入与人口的散点图,HouseAge 列中的值为 5 或以下]()
图 9.22:中位收入与人口的散点图,HouseAge 列中的值为 5 或以下
-
使用另一个
seaborn函数来拟合核密度估计模型。同样,使用 Scott 规则找到最优带宽。重新绘制直方图并叠加估算密度,如下所示:fig = plt.figure(figsize=(10, 10)) ax = seaborn.kdeplot( x_vals, y_vals, kernel='gau', cmap='Blues', shade=True, shade_lowest=False ) plt.scatter(x_vals, y_vals, c='black', alpha=0.05) plt.xlabel('Population', fontsize=18) plt.ylabel('Median Income', fontsize=18) plt.title('Density Estimation With Scatterplot Overlay', size=18)输出结果如下:

图 9.23:与步骤 6 中创建的散点图相同,叠加了估算密度
在这里,估算密度显示,人口较少的普查区块比人口较多的普查区块更有可能拥有较低的中位收入,而不是较高的中位收入。本步骤的目的是展示如何在非位置数据上使用核密度估计。
在展示热点分析结果时,应当使用某种地图,因为热点分析通常是基于位置数据进行的。获取可以叠加估算密度的地图并不是一个简单的过程。由于版权问题,我们将使用非常基础的地图,称为底图,来叠加我们的估算密度。如何将本章获得的知识扩展到更复杂和详细的地图将留给你自己去做。地图环境的下载和安装可能也会很复杂且耗时。
练习 51:与底图一起工作
这个练习利用了mpl_toolkits中的basemap模块。basemap是一个地图绘制库,可以用来创建基础的地图或地理区域的轮廓。这些地图可以叠加核密度估计的结果,从而清晰地看到热点的位置。
首先,通过在 Jupyter Notebook 中运行import mpl_toolkits.basemap来检查basemap是否已安装。如果加载没有错误,那么你已经准备好了,无需进一步操作。如果调用失败,则使用pip安装basemap,方法是运行python3 -m pip install basemap。在重新启动任何已打开的 Notebook 之后,你应该就可以正常使用。请注意,pip安装只有在安装了 Anaconda 的情况下才有效。
本练习的目标是重新建模并重新绘制练习 50中位置数据的图表,使用sklearn的核密度估计功能和basemap的映射能力。从名为dfLess15的筛选后的 DataFrame 中提取经纬度值,如下所示:
-
形成将要叠加估算密度的位置网格。位置网格是定义随机变量范围的一维向量的二维位置等价物,在练习 1 中已涉及。
xgrid15 = numpy.sort(list(dfLess15['Longitude'])) ygrid15 = numpy.sort(list(dfLess15['Latitude'])) x15, y15 = numpy.meshgrid(xgrid15, ygrid15) print("X Grid Component:\n{}\n".format(x15)) print("Y Grid Component:\n{}\n".format(y15)) xy15 = numpy.vstack([y15.ravel(), x15.ravel()]).T print("Grid:\n{}\n".format(xy15))输出如下:
![图 9.24:表示 dfLess15 数据集的网格的 x 和 y 分量]()
图 9.24:表示 dfLess15 数据集的网格的 x 和 y 分量
-
定义并拟合一个核密度估计模型。设置带宽值为 0.05,以节省运行时间;然后,为位置网格中的每个点创建似然值:
kde = sklearn.neighbors.KernelDensity( bandwidth=0.05, metric='minkowski', kernel='gaussian', algorithm='ball_tree' ) kde.fit(dfLess15.values) log_density = kde.score_samples(xy15) density = numpy.exp(log_density) density = density.reshape(x15.shape) print("Shape of Density Values:\n{}\n".format(density.shape))注意,如果你打印出似然值的形状,它是 3,287 行和 3,287 列,总共有 10,804,369 个似然值。这与预设的经纬度网格(称为
xy15)中的值数量相同。 -
创建加利福尼亚州的轮廓,并叠加在步骤 2中估算的密度值上:
fig = plt.figure(figsize=(10, 10)) fig.suptitle( """ Density Estimation: Location of Housing Blocks Where the Median Home Age <= 15 Years """, fontsize=16 ) the_map = mpl_toolkits.basemap.Basemap( projection='cyl', llcrnrlat=y15.min(), urcrnrlat=y15.max(), llcrnrlon=x15.min(),urcrnrlon=x15.max(), resolution='c' ) the_map.drawcoastlines(linewidth=1) the_map.drawcountries(linewidth=1) the_map.drawstates(linewidth=1) levels = numpy.linspace(0, density.max(), 25) plt.contourf(x15, y15, density, levels=levels, cmap=plt.cm.Reds) plt.show()输出如下:
![图 9.25:将 dfLess15 的估算密度叠加到加利福尼亚州的轮廓上]()
图 9.25:将 dfLess15 的估算密度叠加到加利福尼亚州的轮廓上
0.05 的值是故意设置为稍微过拟合数据的。你会注意到,与在练习 50,使用 Seaborn 加载数据和建模中组成密度的大型聚类不同,这里的估算密度由许多更小的聚类组成。这个稍微过拟合的密度可能比之前的版本更有帮助,因为它能更清楚地显示出高似然值的普查区块真正的位置。前一个密度中的一个高似然区域是南加州,但南加州是一个巨大的地区,拥有庞大的人口和许多市政区。请记住,在使用结果做出商业决策时,可能需要特定的精度级别,并且如果样本数据可以支持这样的结果,应该提供该精度或粒度。
-
重复步骤 1,但使用
dfMore40DataFrame:xgrid40 = numpy.sort(list(dfMore40['Longitude'])) ygrid40 = numpy.sort(list(dfMore40['Latitude'])) x40, y40 = numpy.meshgrid(xgrid40, ygrid40) print("X Grid Component:\n{}\n".format(x40)) print("Y Grid Component:\n{}\n".format(y40)) xy40 = numpy.vstack([y40.ravel(), x40.ravel()]).T print("Grid:\n{}\n".format(xy40))输出如下:
![图 9.26:表示 dfMore40 数据集的网格的 x 和 y 分量]()
图 9.26:表示 dfMore40 数据集的网格的 x 和 y 分量
-
使用步骤 4中建立的网格,重复步骤 2:
kde = sklearn.neighbors.KernelDensity( bandwidth=0.05, metric='minkowski', kernel='gaussian', algorithm='ball_tree' ) kde.fit(dfMore40.values) log_density = kde.score_samples(xy40) density = numpy.exp(log_density) density = density.reshape(x40.shape) print("Shape of Density Values:\n{}\n".format(density.shape)) -
使用步骤 3中估算的密度值,重复操作:
fig = plt.figure(figsize=(10, 10)) fig.suptitle( """ Density Estimation: Location of Housing Blocks Where the Median Home Age > 40 Years """, fontsize=16 ) the_map = mpl_toolkits.basemap.Basemap( projection='cyl', llcrnrlat=y40.min(), urcrnrlat=y40.max(), llcrnrlon=x40.min(),urcrnrlon=x40.max(), resolution='c' ) the_map.drawcoastlines(linewidth=1) the_map.drawcountries(linewidth=1) the_map.drawstates(linewidth=1) levels = numpy.linspace(0, density.max(), 25) plt.contourf(x40, y40, density, levels=levels, cmap=plt.cm.Reds) plt.show()输出如下:

图 9.27:将 dfMore40 的估算密度叠加到加利福尼亚州的轮廓上
这个估计密度是我们在练习 50中重新做的那个,使用 Seaborn 加载数据和建模。虽然第 3 步中的密度会为对房地产或人口普查感兴趣的人提供更多细节,但这个密度实际上与练习 50中的对应密度差别不大。聚类主要集中在洛杉矶和旧金山,几乎没有在其他地方出现数据点。
活动 22:伦敦犯罪分析
在此活动中,我们将对来自data.police.uk/data/的伦敦犯罪数据进行核密度估计的热点分析。由于处理地图数据的困难,我们将使用seaborn来可视化分析结果。不过,如果你觉得有信心并且能够运行练习 51中的所有图表,与底图一起工作,那么鼓励你尝试使用地图。
对这个犯罪数据进行热点分析的动机有两个方面。我们首先需要确定某些类型的犯罪在高概率区域发生的位置,以便能够最大化地分配警察资源。然后,作为后续分析,我们需要确定某些类型犯罪的热点是否随时间变化。这两个问题都可以通过核密度估计来回答。
注意事项
这个数据集是从data.police.uk/data/下载的。
你可以从 Packt GitHub 下载,网址是github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson09/Activity21-Activity22。
或者,为了直接从源下载数据,前往前面的警察网站,勾选大都市警察局,然后设置日期范围为2018 年 7 月至2018 年 12 月。接下来,点击生成文件,然后点击立即下载并将下载的文件命名为metro-jul18-dec18。确保你知道如何获取或能够找到下载目录的路径。
这个数据集包含根据开放政府许可证 v3.0 许可的公共部门信息。
以下是完成活动的步骤:
-
加载犯罪数据。使用你保存下载目录的路径,创建年份-月份标签的列表,使用
read_csv命令逐个加载文件,然后将这些文件合并在一起。 -
打印完整(六个月)和合并数据集的诊断信息。
-
将数据框架(DataFrame)缩小到四个变量(
经度、纬度、月份和犯罪类型)。 -
使用
seaborn中的jointplot函数,为 2018 年 7 月、9 月和 12 月的自行车盗窃数据拟合并可视化三个核密度估计模型。 -
重复第 4 步;这次,使用 2018 年 8 月、10 月和 11 月的商店盗窃犯罪数据。
-
重复第 5 步;这次,使用 2018 年 7 月、10 月和 12 月的入室盗窃犯罪数据。
第 6 步 的最后输出如下:

图 9.28:2018 年 12 月入室盗窃的联合和边缘密度估计
再次强调,本活动中找到的密度应该在地图上叠加显示,以便我们能够看到这些密度覆盖的具体区域。如果您有合适的地图平台,请尝试自行叠加结果在地图上显示。如果没有,您可以访问在线地图服务,并使用经度和纬度对来获取关于具体位置的洞察。
注意
此活动的解决方案可在第 377 页找到。
总结
核密度估计是一种经典的统计技术,与直方图技术属于同一类。它允许用户从样本数据中推断出对特定对象或事件的人群进行洞察和预测。这种推断以概率密度函数的形式呈现,这非常好,因为结果可以解读为概率或可能性。这个模型的质量取决于两个参数:带宽值和核函数。正如讨论的那样,成功利用核密度估计的关键组成部分是设置一个最佳的带宽值。最常用的方法是使用网格搜索交叉验证,以伪对数似然作为评分指标来确定最佳带宽。核密度估计的优点在于其简单性和适用性广泛。
在犯罪学、流行病学、气象学和房地产等多个领域中,经常可以找到核密度估计模型。无论您从事哪个领域的业务,核密度估计都应该适用。
在本书中,我们探讨了如何在 Python 库的支持下,使用无监督学习技术的最佳实践,并从非结构化数据中提取有意义的信息。现在,您可以自信地使用 Python 构建自己的模型。
第十一章:附录
关于
本节内容旨在帮助学生完成书中列出的活动。它包括学生需要执行的详细步骤,以完成并实现书中的目标。
第一章:聚类介绍
活动 1:实现 k-means 聚类
解决方案:
-
使用 pandas 加载 Iris 数据文件,pandas 是一个通过使用 DataFrame 使数据处理变得更加容易的包:
import pandas as pd import numpy as np import matplotlib.pyplot as plt from sklearn.metrics import silhouette_score from scipy.spatial.distance import cdist iris = pd.read_csv('iris_data.csv', header=None) iris.columns = ['SepalLengthCm', 'SepalWidthCm', 'PetalLengthCm', 'PetalWidthCm', 'species'] -
将
X特征和提供的y物种标签分开,因为我们希望将其视为无监督学习问题:X = iris[['SepalLengthCm', 'SepalWidthCm', 'PetalLengthCm', 'PetalWidthCm']] y = iris['species'] -
了解我们的特征是什么样的:
X.head()输出结果如下:
![图 1.22:数据的前五行]()
图 1.22:数据的前五行
-
将我们之前创建的
k_means函数拿出来参考:def k_means(X, K): #Keep track of history so you can see k-means in action centroids_history = [] labels_history = [] rand_index = np.random.choice(X.shape[0], K) centroids = X[rand_index] centroids_history.append(centroids) while True: # Euclidean distances are calculated for each point relative to centroids, #and then np.argmin returns # the index location of the minimal distance - which cluster a point is #assigned to labels = np.argmin(cdist(X, centroids), axis=1) labels_history.append(labels) #Take mean of points within clusters to find new centroids: new_centroids = np.array([X[labels == i].mean(axis=0) for i in range(K)]) centroids_history.append(new_centroids) # If old centroids and new centroids no longer change, k-means is complete and end. Otherwise continue if np.all(centroids == new_centroids): break centroids = new_centroids return centroids, labels, centroids_history, labels_history -
将我们的 Iris
X特征 DataFrame 转换为NumPy矩阵:X_mat = X.values -
在鸢尾矩阵上运行我们的
k_means函数:centroids, labels, centroids_history, labels_history = k_means(X_mat, 3) -
查看我们通过查看每个样本的预测物种列表得到的标签:
print(labels)输出结果如下:
![图 1.23:预测物种列表]()
图 1.23:预测物种列表
-
可视化我们在数据集上实现的 k-means 方法:
plt.scatter(X['SepalLengthCm'], X['SepalWidthCm']) plt.title('Iris - Sepal Length vs Width') plt.show()输出结果如下:
![图 1.24:执行的 k-means 实现的图]()
图 1.24:执行的 k-means 实现的图
如下所示可视化鸢尾物种的簇:
plt.scatter(X['SepalLengthCm'], X['SepalWidthCm'], c=labels, cmap='tab20b') plt.title('Iris - Sepal Length vs Width - Clustered') plt.show()输出结果如下:
![图 1.25:鸢尾物种的簇]()
图 1.25:鸢尾物种的簇
-
使用 scikit-learn 实现计算轮廓系数(Silhouette Score):
# Calculate Silhouette Score silhouette_score(X[['SepalLengthCm','SepalWidthCm']], labels)你将得到一个大约等于 0.369 的 SSI。由于我们只使用了两个特征,这是可以接受的,结合最终图中展示的聚类成员可视化。
第二章:层次聚类
活动 2:应用连接标准
解决方案:
-
可视化我们在 练习 7 中创建的
x数据集,构建层次结构:from scipy.cluster.hierarchy import linkage, dendrogram, fcluster from sklearn.datasets import make_blobs import matplotlib.pyplot as plt %matplotlib inline # Generate a random cluster dataset to experiment on. X = coordinate points, y = cluster labels (not needed) X, y = make_blobs(n_samples=1000, centers=8, n_features=2, random_state=800) # Visualize the data plt.scatter(X[:,0], X[:,1]) plt.show()输出结果如下:
![图 2.20:生成的簇数据集的散点图]()
图 2.20:生成的簇数据集的散点图
-
创建一个包含所有可能连接方法超参数的列表:
methods = ['centroid', 'single', 'complete', 'average', 'weighted'] -
遍历你刚才创建的列表中的每种方法,展示它们在同一数据集上的效果:
for method in methods: distances = linkage(X, method=method, metric="euclidean") clusters = fcluster(distances, 3, criterion="distance") plt.title('linkage: ' + method) plt.scatter(X[:,0], X[:,1], c=clusters, cmap='tab20b') plt.show()输出结果如下:

图 2.21:所有方法的散点图
分析:
从前面的图中可以看出,通过简单地更改连接标准,可以显著改变聚类的效果。在这个数据集中,质心法和平均法最适合找到合理的离散簇。这一点从我们生成的八个簇的事实中可以看出,质心法和平均法是唯一显示出使用八种不同颜色表示的簇的算法。其他连接类型则表现不佳,尤其是单链接法。
活动 3:比较 k-means 与层次聚类
解决方案:
-
从 scikit-learn 导入必要的包(
KMeans、AgglomerativeClustering和silhouette_score),如下所示:from sklearn.cluster import KMeans from sklearn.cluster import AgglomerativeClustering from sklearn.metrics import silhouette_score import pandas as pd import matplotlib.pyplot as plt -
将葡萄酒数据集读入 pandas DataFrame 并打印一个小样本:
wine_df = pd.read_csv("wine_data.csv") print(wine_df.head)输出如下:
![图 2.22:葡萄酒数据集的输出]()
图 2.22:葡萄酒数据集的输出
-
可视化葡萄酒数据集以理解数据结构:
plt.scatter(wine_df.values[:,0], wine_df.values[:,1]) plt.title("Wine Dataset") plt.xlabel("OD Reading") plt.ylabel("Proline") plt.show()输出如下:
![图 2.23:原始葡萄酒数据的聚类图]()
图 2.23:原始葡萄酒数据的聚类图
-
在葡萄酒数据集上使用 sklearn 实现的 k-means,已知有三种葡萄酒类型:
km = KMeans(3) km_clusters = km.fit_predict(wine_df) -
使用 sklearn 实现的层次聚类对葡萄酒数据集进行处理:
ac = AgglomerativeClustering(3, linkage='average') ac_clusters = ac.fit_predict(wine_df) -
如下所示,绘制 k-means 预测的聚类:
plt.scatter(wine_df.values[:,0], wine_df.values[:,1], c=km_clusters) plt.title("Wine Clusters from Agglomerative Clustering") plt.xlabel("OD Reading") plt.ylabel("Proline") plt.show()输出如下:
![图 2.24:k-means 聚类的聚类图]()
图 2.24:k-means 聚类的聚类图
-
如下所示,绘制层次聚类预测的聚类:
plt.scatter(wine_df.values[:,0], wine_df.values[:,1], c=ac_clusters) plt.title("Wine Clusters from Agglomerative Clustering") plt.xlabel("OD Reading") plt.ylabel("Proline") plt.show()输出如下:
![图 2.25:凝聚聚类的聚类图]()
图 2.25:凝聚聚类的聚类图
-
比较每种聚类方法的轮廓得分:
print("Silhouette Scores for Wine Dataset:\n") print("k-means Clustering: ", silhouette_score(X[:,11:13], km_clusters)) print("Agg Clustering: ", silhouette_score(X[:,11:13], ac_clusters))输出如下:

图 2.26:葡萄酒数据集的轮廓得分
从之前的轮廓得分可以看出,凝聚聚类在分离聚类时,平均簇内距离上略微优于 k-means 聚类。然而,并非所有版本的凝聚聚类都有这个效果。你可以尝试不同的连接类型,观察不同轮廓得分和聚类结果如何变化!
第三章:邻域方法与 DBSCAN
活动 4:从零实现 DBSCAN
解决方案:
-
生成一个随机的聚类数据集,如下所示:
from sklearn.cluster import DBSCAN from sklearn.datasets import make_blobs import matplotlib.pyplot as plt import numpy as np %matplotlib inline X_blob, y_blob = make_blobs(n_samples=500, centers=4, n_features=2, random_state=800) -
可视化生成的数据:
plt.scatter(X_blob[:,0], X_blob[:,1]) plt.show()输出如下:
![图 3.14:生成数据的聚类图]()
图 3.14:生成数据的聚类图
-
创建从零开始的函数,允许你在数据集上调用 DBSCAN:
def scratch_DBSCAN(x, eps, min_pts): """ param x (list of vectors): your dataset to be clustered param eps (float): neigborhood radius threshold param min_pts (int): minimum number of points threshold for a nieghborhood to be a cluster """ # Build a label holder that is comprised of all 0s labels = [0]* x.shape[0] # Arbitrary starting "current cluster" ID C = 0 # For each point p in x... # ('p' is the index of the datapoint, rather than the datapoint itself.) for p in range(0, x.shape[0]): # Only unvisited points can be evaluated as neighborhood centers if not (labels[p] == 0): continue # Find all of p's neighbors. neighbors = neighborhood_search(x, p, eps) # If there are not enough neighbor points, then it is classified as noise (-1). # Otherwise we can use this point as a neighborhood cluster if len(neighbors) < min_pts: labels[p] = -1 else: C += 1 neighbor_cluster(x, labels, p, neighbors, C, eps, min_pts) return labels def neighbor_cluster(x, labels, p, neighbors, C, eps, min_pts): # Assign the cluster label to original point labels[p] = C # Look at each neighbor of p (by index, not the points themselves) and evaluate i = 0 while i < len(neighbors): # Get the next point from the queue. potential_neighbor_ix = neighbors[i] # If potential_neighbor_ix is noise from previous runs, we can assign it to current cluster if labels[potential_neighbor_ix] == -1: labels[potential_neighbor_ix] = C # Otherwise, if potential_neighbor_ix is unvisited, we can add it to current cluster elif labels[potential_neighbor_ix] == 0: labels[potential_neighbor_ix] = C # Further find neighbors of potential neighbor potential_neighbors_cluster = neighborhood_search(x, potential_neighbor_ix, eps) if len(potential_neighbors_cluster) >= min_pts: neighbors = neighbors + potential_neighbors_cluster # Evaluate next neighbor i += 1 def neighborhood_search(x, p, eps): neighbors = [] # For each point in the dataset... for potential_neighbor in range(0, x.shape[0]): # If a nearby point falls below the neighborhood radius threshold, add to neighbors list if np.linalg.norm(x[p] - x[potential_neighbor]) < eps: neighbors.append(potential_neighbor) return neighbors -
使用你创建的 DBSCAN 实现,在生成的数据集中查找聚类。根据第五步中的性能,随意调整超参数:
labels = scratch_DBSCAN(X_blob, 0.6, 5) -
从零开始可视化你实现的 DBSCAN 聚类性能:
plt.scatter(X_blob[:,0], X_blob[:,1], c=labels) plt.title("DBSCAN from Scratch Performance") plt.show()输出如下:

图 3.15:DBSCAN 实现的聚类图
正如你可能注意到的,定制实现的运行时间相对较长。这是因为为了清晰起见,我们探索了该算法的非矢量化版本。接下来,你应该尽量使用 scikit-learn 提供的 DBSCAN 实现,因为它经过高度优化。
活动 5:比较 DBSCAN 与 k-means 和层次聚类
解决方案:
-
导入必要的包:
from sklearn.cluster import KMeans, AgglomerativeClustering, DBSCAN from sklearn.metrics import silhouette_score import pandas as pd import matplotlib.pyplot as plt %matplotlib inline -
从第二章 Hierarchical Clustering加载葡萄酒数据集,并再次熟悉数据的外观:
# Load Wine data set wine_df = pd.read_csv("../CH2/wine_data.csv") # Show sample of data set print(wine_df.head())输出如下:
![图 3.16: 葡萄酒数据集的前五行]()
图 3.16: 葡萄酒数据集的前五行
-
可视化数据:
plt.scatter(wine_df.values[:,0], wine_df.values[:,1]) plt.title("Wine Dataset") plt.xlabel("OD Reading") plt.ylabel("Proline") plt.show()输出如下:
![图 3.17: 数据的图]()
图 3.17: 数据的图
-
使用 k-means、凝聚聚类和 DBSCAN 生成聚类:
# Generate clusters from K-Means km = KMeans(3) km_clusters = km.fit_predict(wine_df) # Generate clusters using Agglomerative Hierarchical Clustering ac = AgglomerativeClustering(3, linkage='average') ac_clusters = ac.fit_predict(wine_df) -
评估 DSBSCAN 超参数的几个不同选项及其对轮廓分数的影响:
db_param_options = [[20,5],[25,5],[30,5],[25,7],[35,7],[35,3]] for ep,min_sample in db_param_options: # Generate clusters using DBSCAN db = DBSCAN(eps=ep, min_samples = min_sample) db_clusters = db.fit_predict(wine_df) print("Eps: ", ep, "Min Samples: ", min_sample) print("DBSCAN Clustering: ", silhouette_score(wine_df, db_clusters))输出如下:
![图 3.18: 打印聚类的轮廓分数]()
图 3.18: 打印聚类的轮廓分数
-
根据最高轮廓分数生成最终聚类 (
eps: 35,min_samples: 3):# Generate clusters using DBSCAN db = DBSCAN(eps=35, min_samples = 3) db_clusters = db.fit_predict(wine_df) -
可视化使用三种方法生成的聚类:
plt.title("Wine Clusters from K-Means") plt.scatter(wine_df['OD_read'], wine_df['Proline'], c=km_clusters,s=50, cmap='tab20b') plt.show() plt.title("Wine Clusters from Agglomerative Clustering") plt.scatter(wine_df['OD_read'], wine_df['Proline'], c=ac_clusters,s=50, cmap='tab20b') plt.show() plt.title("Wine Clusters from DBSCAN") plt.scatter(wine_df['OD_read'], wine_df['Proline'], c=db_clusters,s=50, cmap='tab20b') plt.show()输出如下:
![图 3.19: 使用不同算法绘制聚类图]()
图 3.19: 使用不同算法绘制聚类图
-
评估每种方法的轮廓分数:
# Calculate Silhouette Scores print("Silhouette Scores for Wine Dataset:\n") print("K-Means Clustering: ", silhouette_score(wine_df, km_clusters)) print("Agg Clustering: ", silhouette_score(wine_df, ac_clusters)) print("DBSCAN Clustering: ", silhouette_score(wine_df, db_clusters))输出如下:

图 3.20: Silhouette 分数
如您所见,DBSCAN 并不总是自动适合您的聚类需求。使其与其他算法不同的一个关键特征是将噪声用作潜在聚类。在某些情况下,这很好,因为它可以去除离群值,但是,可能存在调整不足的情况,会将太多点分类为噪声。您能通过调整超参数来提高轮廓分数吗?
第四章: 维度缩减和 PCA
活动 6: 手动 PCA 与 scikit-learn 对比
解决方案
-
导入
pandas、numpy和matplotlib绘图库以及 scikit-learn 的PCA模型:import pandas as pd import numpy as np import matplotlib.pyplot as plt from sklearn.decomposition import PCA -
加载数据集,并根据以前的练习选择仅选择萼片特征。显示数据的前五行:
df = pd.read_csv('iris-data.csv') df = df[['Sepal Length', 'Sepal Width']] df.head()输出如下:
![图 4.43: 数据的前五行]()
图 4.43: 数据的前五行
-
计算数据的协方差矩阵:
cov = np.cov(df.values.T) cov输出如下:
![图 4.44: 数据的协方差矩阵]()
图 4.44: 数据的协方差矩阵
-
使用 scikit-learn API 转换数据,仅使用第一个主成分。将转换后的数据存储在
sklearn_pca变量中:model = PCA(n_components=1) sklearn_pca = model.fit_transform(df.values) -
使用手动 PCA 仅使用第一个主成分来转换数据。将转换后的数据存储在
manual_pca变量中。eigenvectors, eigenvalues, _ = np.linalg.svd(cov, full_matrices=False) P = eigenvectors[0] manual_pca = P.dot(df.values.T) -
在同一图上绘制
sklearn_pca和manual_pca的值以可视化差异:plt.figure(figsize=(10, 7)); plt.plot(sklearn_pca, label='Scikit-learn PCA'); plt.plot(manual_pca, label='Manual PCA', linestyle='--'); plt.xlabel('Sample'); plt.ylabel('Transformed Value'); plt.legend();输出如下:
![图 4.45: 数据的图]()
图 4.45: 数据的图
-
请注意,两个图几乎完全相同,唯一的区别是一个是另一个的镜像,且两者之间存在偏移。显示
sklearn_pca和manual_pca模型的主成分:model.components_输出如下:
array([[ 0.99693955, -0.07817635]])现在打印
P:P输出如下:
array([-0.99693955, 0.07817635])注意符号的差异;值是相同的,但符号不同,产生了镜像的结果。这只是约定上的差异,并无实质意义。
-
将
manual_pca模型乘以-1并重新绘制:manual_pca *= -1 plt.figure(figsize=(10, 7)); plt.plot(sklearn_pca, label='Scikit-learn PCA'); plt.plot(manual_pca, label='Manual PCA', linestyle='--'); plt.xlabel('Sample'); plt.ylabel('Transformed Value'); plt.legend();输出如下:
![图 4.46: 重新绘制的数据]()
图 4.46: 重新绘制的数据
-
现在,我们需要做的就是处理两个之间的偏移。scikit-learn API 在变换之前会减去数据的均值。在使用手动 PCA 进行变换之前,从数据集中减去每列的均值:
mean_vals = np.mean(df.values, axis=0) offset_vals = df.values - mean_vals manual_pca = P.dot(offset_vals.T) -
将结果乘以
-1:manual_pca *= -1 -
重新绘制单独的
sklearn_pca和manual_pca值:plt.figure(figsize=(10, 7)); plt.plot(sklearn_pca, label='Scikit-learn PCA'); plt.plot(manual_pca, label='Manual PCA', linestyle='--'); plt.xlabel('Sample'); plt.ylabel('Transformed Value'); plt.legend();输出如下:

图 4.47: 重新绘制数据
最终的图将展示通过这两种方法完成的降维实际上是相同的。不同之处在于协方差矩阵的符号差异,因为这两种方法只是使用了不同的特征作为比较的基准。最后,两数据集之间也存在偏移,这是由于在执行 scikit-learn PCA 变换之前已将均值样本减去。
活动 7: 使用扩展的鸢尾花数据集进行主成分分析(PCA)
解决方案:
-
导入
pandas和matplotlib。为了启用 3D 绘图,您还需要导入Axes3D:import pandas as pd import numpy as np import matplotlib.pyplot as plt from sklearn.decomposition import PCA from mpl_toolkits.mplot3d import Axes3D # Required for 3D plotting -
读取数据集并选择
花萼长度、花萼宽度和花瓣宽度列:df = pd.read_csv('iris-data.csv')[['Sepal Length', 'Sepal Width', 'Petal Width']] df.head()输出如下:
![图 4.48: 花萼长度、花萼宽度和花瓣宽度]()
图 4.48: 花萼长度、花萼宽度和花瓣宽度
-
绘制三维数据:
fig = plt.figure(figsize=(10, 7)) ax = fig.add_subplot(111, projection='3d') ax.scatter(df['Sepal Length'], df['Sepal Width'], df['Petal Width']); ax.set_xlabel('Sepal Length (mm)'); ax.set_ylabel('Sepal Width (mm)'); ax.set_zlabel('Petal Width (mm)'); ax.set_title('Expanded Iris Dataset');输出如下:
![图 4.49: 扩展的鸢尾花数据集图]()
图 4.49: 扩展的鸢尾花数据集图
-
创建一个
PCA模型,不指定主成分数:model = PCA() -
将模型拟合到数据集:
model.fit(df.values)输出如下:
![图 4.50: 拟合到数据集的模型]()
图 4.50: 拟合到数据集的模型
-
显示特征值或
explained_variance_ratio_:model.explained_variance_ratio_输出如下:
array([0.8004668 , 0.14652357, 0.05300962]) -
我们希望减少数据集的维度,但仍保持至少 90% 的方差。为了保持 90% 的方差,所需的最小主成分数是多少?
前两个主成分需要至少 90% 的方差。前两个主成分提供了数据集中 94.7% 的方差。
-
创建一个新的
PCA模型,这次指定所需的主成分数,以保持至少 90% 的方差:model = PCA(n_components=2) -
使用新模型转换数据:
data_transformed = model.fit_transform(df.values) -
绘制转换后的数据:
plt.figure(figsize=(10, 7)) plt.scatter(data_transformed[:,0], data_transformed[:,1]);输出如下:
![图 4.51: 转换数据的图]()
图 4.51:转换数据的图
-
将转换后的数据恢复到原始数据空间:
data_restored = model.inverse_transform(data_transformed) -
在一个子图中绘制恢复后的三维数据,在第二个子图中绘制原始数据,以可视化去除部分方差的效果:
fig = plt.figure(figsize=(10, 14)) # Original Data ax = fig.add_subplot(211, projection='3d') ax.scatter(df['Sepal Length'], df['Sepal Width'], df['Petal Width'], label='Original Data'); ax.set_xlabel('Sepal Length (mm)'); ax.set_ylabel('Sepal Width (mm)'); ax.set_zlabel('Petal Width (mm)'); ax.set_title('Expanded Iris Dataset'); # Transformed Data ax = fig.add_subplot(212, projection='3d') ax.scatter(data_restored[:,0], data_restored[:,1], data_restored[:,2], label='Restored Data'); ax.set_xlabel('Sepal Length (mm)'); ax.set_ylabel('Sepal Width (mm)'); ax.set_zlabel('Petal Width (mm)'); ax.set_title('Restored Iris Dataset');输出如下:

图 4.52:扩展和恢复后的鸢尾花数据集图
看看图 4.52,我们可以看到,就像我们在二维图中做的那样,我们已经去除了数据中的大量噪声,但保留了关于数据趋势的最重要信息。可以看出,通常情况下,花萼长度与花瓣宽度成正比,并且在图中似乎有两个数据簇,一个在另一个上方。
注意
在应用 PCA 时,重要的是要考虑所建模数据的大小以及可用的系统内存。奇异值分解过程涉及将数据分解为特征值和特征向量,且可能非常占用内存。如果数据集过大,你可能无法完成该过程,或者会遭遇显著的性能损失,甚至可能导致系统崩溃。
第五章:自编码器
活动 8:使用 ReLU 激活函数建模神经元
解决方案:
-
导入
numpy和 matplotlib:import numpy as np import matplotlib.pyplot as plt -
允许在标签中使用 latex 符号:
plt.rc('text', usetex=True) -
将 ReLU 激活函数定义为 Python 函数:
def relu(x): return np.max((0, x)) -
定义神经元的输入(
x)和可调权重(theta)。在此示例中,输入(x)将是-5 到 5 之间线性间隔的 100 个数字。设置theta = 1:theta = 1 x = np.linspace(-5, 5, 100) x输出如下:
![图 5.35:打印输入]()
图 5.35:打印输入
-
计算输出(
y):y = [relu(_x * theta) for _x in x] -
绘制神经元的输出与输入的关系图:
fig = plt.figure(figsize=(10, 7)) ax = fig.add_subplot(111) ax.plot(x, y) ax.set_xlabel('$x$', fontsize=22); ax.set_ylabel('$h(x\Theta)$', fontsize=22); ax.spines['left'].set_position(('data', 0)); ax.spines['top'].set_visible(False); ax.spines['right'].set_visible(False); ax.tick_params(axis='both', which='major', labelsize=22)输出如下:
![图 5.36:神经元与输入的关系图]()
图 5.36:神经元与输入的关系图
-
现在,将
theta = 5并重新计算并存储神经元的输出:theta = 5 y_2 = [relu(_x * theta) for _x in x] -
现在,将
theta = 0.2并重新计算并存储神经元的输出:theta = 0.2 y_3 = [relu(_x * theta) for _x in x] -
在一张图上绘制神经元的三条不同输出曲线(
theta = 1,theta = 5,theta = 0.2):fig = plt.figure(figsize=(10, 7)) ax = fig.add_subplot(111) ax.plot(x, y, label='$\Theta=1$'); ax.plot(x, y_2, label='$\Theta=5$', linestyle=':'); ax.plot(x, y_3, label='$\Theta=0.2$', linestyle='--'); ax.set_xlabel('$x\Theta$', fontsize=22); ax.set_ylabel('$h(x\Theta)$', fontsize=22); ax.spines['left'].set_position(('data', 0)); ax.spines['top'].set_visible(False); ax.spines['right'].set_visible(False); ax.tick_params(axis='both', which='major', labelsize=22); ax.legend(fontsize=22);输出如下:

图 5.37:神经元的三条输出曲线
在这个活动中,我们创建了一个基于 ReLU 的人工神经网络神经元模型。我们可以看到,这个神经元的输出与 sigmoid 激活函数的输出非常不同。对于大于 0 的值,没有饱和区域,因为它仅返回函数的输入值。在负方向上,有一个饱和区域,如果输入小于 0,则返回 0。ReLU 函数是一种非常强大且常用的激活函数,在某些情况下,它比 sigmoid 函数更强大。ReLU 通常是一个很好的首选激活函数。
活动 9:MNIST 神经网络
解决方案:
在这个活动中,您将训练一个神经网络来识别 MNIST 数据集中的图像,并强化您的神经网络训练技能:
-
导入
pickle、numpy、matplotlib,以及从 Keras 中导入Sequential和Dense类:import pickle import numpy as np import matplotlib.pyplot as plt from keras.models import Sequential from keras.layers import Dense -
加载
mnist.pkl文件,其中包含来自 MNIST 数据集的前 10,000 张图像及其对应的标签,这些数据可以在随附的源代码中找到。MNIST 数据集是一系列 28 x 28 灰度图像,表示手写数字 0 到 9。提取图像和标签:with open('mnist.pkl', 'rb') as f: data = pickle.load(f) images = data['images'] labels = data['labels'] -
绘制前 10 个样本及其对应的标签:
plt.figure(figsize=(10, 7)) for i in range(10): plt.subplot(2, 5, i + 1) plt.imshow(images[i], cmap='gray') plt.title(labels[i]) plt.axis('off')输出如下:
![图 5.38:前 10 个样本]()
图 5.38:前 10 个样本
-
使用独热编码对标签进行编码:
one_hot_labels = np.zeros((images.shape[0], 10)) for idx, label in enumerate(labels): one_hot_labels[idx, label] = 1 one_hot_labels输出如下:
![图 5.39:独热编码结果]()
图 5.39:独热编码结果
-
为神经网络输入准备图像。提示:此过程包含两个独立的步骤:
images = images.reshape((-1, 28 ** 2)) images = images / 255. -
在 Keras 中构建一个神经网络模型,该模型接受准备好的图像,包含一个具有 600 个单位的隐藏层,并使用 ReLU 激活函数,输出层的单元数与类别数相同。输出层使用
softmax激活函数:model = Sequential([ Dense(600, input_shape=(784,), activation='relu'), Dense(10, activation='softmax'), ]) -
使用多类交叉熵、随机梯度下降以及准确度作为性能指标来编译模型:
model.compile(loss='categorical_crossentropy', optimizer='sgd', metrics=['accuracy']) -
训练模型。需要多少个 epoch 才能在训练数据上达到至少 95%的分类准确率?让我们来看看:
model.fit(images, one_hot_labels, epochs=20)输出如下:

图 5.40:训练模型
需要 15 个 epoch 才能在训练集上达到至少 95%的分类准确率。
在这个示例中,我们使用分类器训练时的数据来测量神经网络分类器的性能。一般来说,这种方法不应该使用,因为它通常报告比实际模型应该有的准确度更高。在监督学习问题中,有多种交叉验证技术应该被使用。由于这是一本关于无监督学习的书,交叉验证超出了本书的范围。
活动 10:简单的 MNIST 自编码器
解决方案:
-
导入
pickle、numpy和matplotlib,以及从 Keras 导入Model、Input和Dense类:import pickle import numpy as np import matplotlib.pyplot as plt from keras.models import Model from keras.layers import Input, Dense -
从提供的 MNIST 数据集样本(随附源代码
mnist.pkl)加载图像:with open('mnist.pkl', 'rb') as f: images = pickle.load(f)['images'] -
准备图像以输入神经网络。作为提示,此过程有 两个 独立步骤:
images = images.reshape((-1, 28 ** 2)) images = images / 255. -
构建一个简单的自编码器网络,使图像在编码阶段后缩小到 10 x 10:
input_stage = Input(shape=(784,)) encoding_stage = Dense(100, activation='relu')(input_stage) decoding_stage = Dense(784, activation='sigmoid')(encoding_stage) autoencoder = Model(input_stage, decoding_stage) -
使用二元交叉熵损失函数和
adadelta梯度下降法编译自编码器:autoencoder.compile(loss='binary_crossentropy', optimizer='adadelta') -
拟合编码器模型:
autoencoder.fit(images, images, epochs=100)输出结果如下:
![图 5.41: 训练模型]()
图 5.41: 训练模型
-
计算并存储编码阶段的前五个样本的输出:
encoder_output = Model(input_stage, encoding_stage).predict(images[:5]) -
将编码器输出重塑为 10 x 10(10 x 10 = 100)像素并乘以 255:
encoder_output = encoder_output.reshape((-1, 10, 10)) * 255 -
计算并存储解码阶段的前五个样本的输出:
decoder_output = autoencoder.predict(images[:5]) -
将解码器的输出重塑为 28 x 28,并乘以 255:
decoder_output = decoder_output.reshape((-1, 28, 28)) * 255 -
绘制原始图像、编码器输出和解码器:
images = images.reshape((-1, 28, 28)) plt.figure(figsize=(10, 7)) for i in range(5): plt.subplot(3, 5, i + 1) plt.imshow(images[i], cmap='gray') plt.axis('off') plt.subplot(3, 5, i + 6) plt.imshow(encoder_output[i], cmap='gray') plt.axis('off') plt.subplot(3, 5, i + 11) plt.imshow(decoder_output[i], cmap='gray') plt.axis('off')输出结果如下:

图 5.42: 原始图像、编码器输出和解码器
到目前为止,我们已经展示了如何使用编码和解码阶段的简单单层隐藏层来将数据降到低维空间。我们还可以通过在编码和解码阶段添加额外的层来使这个模型更加复杂。
活动 11: MNIST 卷积自编码器
解决方案:
-
导入
pickle、numpy、matplotlib,以及从keras.models导入Model类,导入从keras.layers的Input、Conv2D、MaxPooling2D和UpSampling2D:import pickle import numpy as np import matplotlib.pyplot as plt from keras.models import Model from keras.layers import Input, Conv2D, MaxPooling2D, UpSampling2D -
加载数据:
with open('mnist.pkl', 'rb') as f: images = pickle.load(f)['images'] -
将图像重新缩放为 0 和 1 之间的值:
images = images / 255. -
我们需要重塑图像,以便为卷积阶段添加一个单独的深度通道。将图像重塑为 28 x 28 x 1 的形状:
images = images.reshape((-1, 28, 28, 1)) -
定义输入层。我们将使用与图像相同的输入形状:
input_layer = Input(shape=(28, 28, 1,)) -
添加一个具有 16 层或滤波器的卷积阶段,使用 3 x 3 的权重矩阵、ReLU 激活函数,并使用相同的填充,这意味着输出的长度与输入图像相同:
hidden_encoding = Conv2D( 16, # Number of layers or filters in the weight matrix (3, 3), # Shape of the weight matrix activation='relu', padding='same', # How to apply the weights to the images )(input_layer) -
向编码器添加一个最大池化层,使用 2 x 2 的内核:
encoded = MaxPooling2D((2, 2))(hidden_encoding) -
添加一个解码卷积层:
hidden_decoding = Conv2D( 16, # Number of layers or filters in the weight matrix (3, 3), # Shape of the weight matrix activation='relu', padding='same', # How to apply the weights to the images )(encoded) -
添加上采样层:
upsample_decoding = UpSampling2D((2, 2))(hidden_decoding) -
添加最后的卷积阶段,使用一个层,按照初始图像深度进行:
decoded = Conv2D( 1, # Number of layers or filters in the weight matrix (3, 3), # Shape of the weight matrix activation='sigmoid', padding='same', # How to apply the weights to the images )(upsample_decoding) -
通过将网络的第一层和最后一层传递给
Model类来构建模型:autoencoder = Model(input_layer, decoded) -
显示模型的结构:
autoencoder.summary()输出结果如下:
![图 5.43: 模型结构]()
图 5.43: 模型结构
-
使用二元交叉熵损失函数和
adadelta梯度下降法编译自编码器:autoencoder.compile(loss='binary_crossentropy', optimizer='adadelta') -
现在,我们来拟合模型;再次传递图像作为训练数据和期望输出。由于卷积网络计算时间较长,因此训练 20 个周期:
autoencoder.fit(images, images, epochs=20)输出结果如下:
![图 5.44: 训练模型]()
图 5.44:训练模型
-
计算并存储前五个样本的编码阶段输出:
encoder_output = Model(input_layer, encoded).predict(images[:5]) -
为可视化重新调整编码器输出的形状,每个图像的大小为 X*Y:
encoder_output = encoder_output.reshape((-1, 14 * 14, 16)) -
获取前五张图像的解码器输出:
decoder_output = autoencoder.predict(images[:5]) -
将解码器输出调整为 28 x 28 的大小:
decoder_output = decoder_output.reshape((-1, 28, 28)) -
将原始图像调整回 28 x 28 的大小:
images = images.reshape((-1, 28, 28)) -
绘制原始图像、平均编码器输出和解码器:
plt.figure(figsize=(10, 7)) for i in range(5): plt.subplot(3, 5, i + 1) plt.imshow(images[i], cmap='gray') plt.axis('off') plt.subplot(3, 5, i + 6) plt.imshow(encoder_output[i], cmap='gray') plt.axis('off') plt.subplot(3, 5, i + 11) plt.imshow(decoder_output[i], cmap='gray') plt.axis('off')输出如下:

图 5.45:原始图像、编码器输出和解码器
在此活动结束时,你将开发一个包含卷积层的自编码器神经网络。注意解码器表示中的改进。与全连接神经网络层相比,这种架构在性能上具有显著优势,非常适用于处理基于图像的数据集和生成人工数据样本。
第六章:t-分布随机邻域嵌入(t-SNE)
活动 12:葡萄酒 t-SNE
解决方案:
-
导入
pandas、numpy、matplotlib以及来自 scikit-learn 的t-SNE和PCA模型:import pandas as pd import numpy as np import matplotlib.pyplot as plt from sklearn.decomposition import PCA from sklearn.manifold import TSNE -
使用随附源代码中的
wine.data文件加载葡萄酒数据集,并显示数据集的前五行:df = pd.read_csv('wine.data', header=None) df.head()输出如下:
![图 6.24:葡萄酒数据集的前五行。]()
图 6.24:葡萄酒数据集的前五行。
-
第一列包含标签;提取该列并将其从数据集中移除:
labels = df[0] del df[0] -
执行 PCA 降维,将数据集缩减到前六个组件:
model_pca = PCA(n_components=6) wine_pca = model_pca.fit_transform(df) -
确定这六个组件描述的数据中的方差量:
np.sum(model_pca.explained_variance_ratio_)输出如下:
0.99999314824536 -
创建一个使用指定随机状态和
verbose值为 1 的 t-SNE 模型:tsne_model = TSNE(random_state=0, verbose=1) tsne_model输出如下:
![图 6.25:创建 t-SNE 模型。]()
图 6.25:创建 t-SNE 模型。
-
将 PCA 数据拟合到 t-SNE 模型:
wine_tsne = tsne_model.fit_transform(wine_pca.reshape((len(wine_pca), -1)))输出如下:
![图 6.26:拟合 PCA 数据 t-SNE 模型]()
图 6.26:拟合 PCA 数据 t-SNE 模型
-
确认 t-SNE 拟合数据的形状是二维的:
wine_tsne.shape输出如下:
(172, 8) -
创建二维数据的散点图:
plt.figure(figsize=(10, 7)) plt.scatter(wine_tsne[:,0], wine_tsne[:,1]); plt.title('Low Dimensional Representation of Wine'); plt.show()输出如下:
![图 6.27:二维数据的散点图]()
图 6.27:二维数据的散点图
-
创建一个带有类别标签的二维数据次级散点图,以可视化可能存在的任何聚类:
MARKER = ['o', 'v', '^',] plt.figure(figsize=(10, 7)) plt.title('Low Dimensional Representation of Wine'); for i in range(1, 4): selections = wine_tsne[labels == i] plt.scatter(selections[:,0], selections[:,1], marker=MARKER[i-1], label=f'Wine {i}', s=30); plt.legend(); plt.show()输出如下:

图 6.28:二维数据的次级图
请注意,虽然酒类之间有重叠,但也可以看到数据中存在一些聚类。第一类酒主要位于图表的左上角,第二类酒位于右下角,第三类酒则位于前两者之间。这个表示方法当然不能用于高信心地对单个酒样进行分类,但它展示了一个总体趋势以及在之前无法看到的高维数据中包含的一系列聚类。
活动 13:t-SNE 酒类与困惑度
解决方案:
-
导入
pandas、numpy、matplotlib和t-SNE与PCA模型(来自 scikit-learn):import pandas as pd import numpy as np import matplotlib.pyplot as plt from sklearn.decomposition import PCA from sklearn.manifold import TSNE -
加载酒类数据集并检查前五行:
df = pd.read_csv('wine.data', header=None) df.head()输出结果如下:
![图 6.29:酒类数据的前五行]()
图 6.29:酒类数据的前五行
-
第一列提供了标签;从数据框中提取它们并将其存储在一个单独的变量中。确保该列从数据框中移除:
labels = df[0] del df[0] -
对数据集执行 PCA 并提取前六个主成分:
model_pca = PCA(n_components=6) wine_pca = model_pca.fit_transform(df) wine_pca = wine_pca.reshape((len(wine_pca), -1)) -
构建一个循环,遍历困惑度值(1、5、20、30、80、160、320)。对于每个循环,生成一个对应困惑度的 t-SNE 模型,并绘制标记酒类的散点图。注意不同困惑度值的效果:
MARKER = ['o', 'v', '^',] for perp in [1, 5, 20, 30, 80, 160, 320]: tsne_model = TSNE(random_state=0, verbose=1, perplexity=perp) wine_tsne = tsne_model.fit_transform(wine_pca) plt.figure(figsize=(10, 7)) plt.title(f'Low Dimensional Representation of Wine. Perplexity {perp}'); for i in range(1, 4): selections = wine_tsne[labels == i] plt.scatter(selections[:,0], selections[:,1], marker=MARKER[i-1], label=f'Wine {i}', s=30); plt.legend();1的困惑度值无法将数据分离成任何特定的结构:

图 6.30:困惑度值为 1 的图表
将困惑度增加到 5 会导致非常非线性的结构,难以分离,并且很难识别任何聚类或模式:

图 6.31:困惑度为 5 的图表
困惑度为 20 最终开始显示某种马蹄形结构。虽然在视觉上明显,但实现起来仍然可能有些困难:

图 6.32:困惑度为 20 的图表
困惑度为 30 显示出相当好的结果。投影结构之间存在一定的线性关系,并且酒类之间有一些分离:

图 6.33:困惑度为 30 的图表
最后,活动中的最后两张图片展示了随着困惑度的增加,图表如何变得越来越复杂和非线性:

图 6.34:困惑度为 80 的图表
这是困惑度为 160 的图表:

图 6.35:困惑度为 160 的图表
查看每个困惑度值的单独图形,困惑度对数据可视化的影响立刻显现出来。非常小或非常大的困惑度值会产生一系列不寻常的形状,无法显示出任何持续的模式。最合理的值似乎是 30,它产生了我们在前一个活动中看到的最线性的图形。
在这次活动中,我们展示了在选择困惑度时需要小心,并且可能需要一些迭代来确定正确的值。
活动 14:t-SNE 葡萄酒与迭代
解决方案:
-
导入
pandas、numpy、matplotlib,以及从 scikit-learn 导入的t-SNE和PCA模型:import pandas as pd import numpy as np import matplotlib.pyplot as plt from sklearn.decomposition import PCA from sklearn.manifold import TSNE -
加载葡萄酒数据集并检查前五行:
df = pd.read_csv('wine.data', header=None) df.head()输出结果如下:
![图 6.36:葡萄酒数据集的前五行]()
图 6.36:葡萄酒数据集的前五行
-
第一列提供标签;从 DataFrame 中提取这些标签并存储到一个单独的变量中。确保将该列从 DataFrame 中移除:
labels = df[0] del df[0] -
在数据集上执行 PCA 并提取前六个主成分:
model_pca = PCA(n_components=6) wine_pca = model_pca.fit_transform(df) wine_pca = wine_pca.reshape((len(wine_pca), -1)) -
构建一个循环,遍历迭代值(
250、500、1000)。对于每次循环,生成一个具有相应迭代次数的 t-SNE 模型,并生成相同迭代次数且没有进度值的模型:MARKER = ['o', 'v', '1', 'p' ,'*', '+', 'x', 'd', '4', '.'] for iterations in [250, 500, 1000]: model_tsne = TSNE(random_state=0, verbose=1, n_iter=iterations, n_iter_without_progress=iterations) mnist_tsne = model_tsne.fit_transform(mnist_pca) -
构建一个葡萄酒类别的散点图。注意不同迭代值的效果:
plt.figure(figsize=(10, 7)) plt.title(f'Low Dimensional Representation of MNIST (iterations = {iterations})'); for i in range(10): selections = mnist_tsne[mnist['labels'] == i] plt.scatter(selections[:,0], selections[:,1], alpha=0.2, marker=MARKER[i], s=5); x, y = selections.mean(axis=0) plt.text(x, y, str(i), fontdict={'weight': 'bold', 'size': 30})输出结果如下:

图 6.37:250 次迭代的葡萄酒类别散点图
这是 500 次迭代的图:

图 6.38:500 次迭代的葡萄酒类别散点图
这是 1,000 次迭代的图:

图 6.39:1,000 次迭代的葡萄酒类别散点图
再次,我们可以看到随着迭代次数的增加,数据结构的改善。即使在像这样的相对简单的数据集中,250 次迭代也不足以将数据结构投影到低维空间中。
正如我们在相应活动中观察到的,设置迭代参数时需要找到一个平衡点。在这个例子中,250 次迭代是不足够的,至少需要 1,000 次迭代才能最终稳定数据。
第七章:主题建模
活动 15:加载和清理 Twitter 数据
解决方案:
-
导入必要的库:
import langdetect import matplotlib.pyplot import nltk import numpy import pandas import pyLDAvis import pyLDAvis.sklearn import regex import sklearn -
从
github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-Python/tree/master/Lesson07/Activity15-Activity17加载 LA Times 健康 Twitter 数据(latimeshealth.txt):注意
path = '<Path>/latimeshealth.txt' df = pandas.read_csv(path, sep="|", header=None) df.columns = ["id", "datetime", "tweettext"] -
运行快速探索性分析以确定数据大小和结构:
def dataframe_quick_look(df, nrows): print("SHAPE:\n{shape}\n".format(shape=df.shape)) print("COLUMN NAMES:\n{names}\n".format(names=df.columns)) print("HEAD:\n{head}\n".format(head=df.head(nrows))) dataframe_quick_look(df, nrows=2)输出如下:
![图 7.54:数据的形状、列名和数据头]()
图 7.54:数据的形状、列名和数据头
-
提取推文文本并转换为列表对象:
raw = df['tweettext'].tolist() print("HEADLINES:\n{lines}\n".format(lines=raw[:5])) print("LENGTH:\n{length}\n".format(length=len(raw)))输出如下:
![图 7.55:标题及其长度]()
图 7.55:标题及其长度
-
编写函数,执行语言检测、基于空格的分词、替换屏幕名和 URL 为
SCREENNAME和URL。该函数还应删除标点、数字以及SCREENNAME和URL的替换。将所有内容转换为小写,除了SCREENNAME和URL。它应删除所有停用词,执行词形还原,并保留五个或更多字母的单词:注意
def do_language_identifying(txt): try: the_language = langdetect.detect(txt) except: the_language = 'none' return the_language def do_lemmatizing(wrd): out = nltk.corpus.wordnet.morphy(wrd) return (wrd if out is None else out) def do_tweet_cleaning(txt): # identify language of tweet # return null if language not english lg = do_language_identifying(txt) if lg != 'en': return None # split the string on whitespace out = txt.split(' ') # identify screen names # replace with SCREENNAME out = ['SCREENNAME' if i.startswith('@') else i for i in out] # identify urls # replace with URL out = ['URL' if bool(regex.search('http[s]?://', i)) else i for i in out] # remove all punctuation out = [regex.sub('[^\\w\\s]|\n', '', i) for i in out] # make all non-keywords lowercase keys = ['SCREENNAME', 'URL'] out = [i.lower() if i not in keys else i for i in out] # remove keywords out = [i for i in out if i not in keys] # remove stopwords list_stop_words = nltk.corpus.stopwords.words('english') list_stop_words = [regex.sub('[^\\w\\s]', '', i) for i in list_stop_words] out = [i for i in out if i not in list_stop_words] # lemmatizing out = [do_lemmatizing(i) for i in out] # keep words 4 or more characters long out = [i for i in out if len(i) >= 5] return out -
对每条推文应用第 5 步中定义的函数:
clean = list(map(do_tweet_cleaning, raw)) -
删除等于
None的输出列表元素:clean = list(filter(None.__ne__, clean)) print("HEADLINES:\n{lines}\n".format(lines=clean[:5])) print("LENGTH:\n{length}\n".format(length=len(clean)))输出如下:
![图 7.56:删除 None 后的标题和长度]()
图 7.56:删除 None 后的标题和长度
-
将每条推文的元素转换回字符串。使用空格连接:
clean_sentences = [" ".join(i) for i in clean] print(clean_sentences[0:10])输出列表的前 10 个元素应如下所示:
![图 7.57:用于建模的清洁推文]()
图 7.57:用于建模的清洁推文
-
保持笔记本打开以供将来建模。
活动 16:潜在狄利克雷分配与健康推文
解决方案:
-
指定
number_words、number_docs和number_features变量:number_words = 10 number_docs = 10 number_features = 1000 -
创建词袋模型,并将特征名分配给另一个变量以供以后使用:
vectorizer1 = sklearn.feature_extraction.text.CountVectorizer( analyzer=»word», max_df=0.95, min_df=10, max_features=number_features ) clean_vec1 = vectorizer1.fit_transform(clean_sentences) print(clean_vec1[0]) feature_names_vec1 = vectorizer1.get_feature_names()输出如下:
(0, 320) 1 -
确定最佳主题数:
def perplexity_by_ntopic(data, ntopics): output_dict = { «Number Of Topics": [], «Perplexity Score»: [] } for t in ntopics: lda = sklearn.decomposition.LatentDirichletAllocation( n_components=t, learning_method="online", random_state=0 ) lda.fit(data) output_dict["Number Of Topics"].append(t) output_dict["Perplexity Score"].append(lda.perplexity(data)) output_df = pandas.DataFrame(output_dict) index_min_perplexity = output_df["Perplexity Score"].idxmin() output_num_topics = output_df.loc[ index_min_perplexity, # index «Number Of Topics" # column ] return (output_df, output_num_topics) df_perplexity, optimal_num_topics = perplexity_by_ntopic( clean_vec1, ntopics=[i for i in range(1, 21) if i % 2 == 0] ) print(df_perplexity)输出如下:
![图 7.58:主题数与困惑分数数据框架]()
图 7.58:主题数与困惑分数数据框架
-
使用最佳主题数拟合 LDA 模型:
lda = sklearn.decomposition.LatentDirichletAllocation( n_components=optimal_num_topics, learning_method="online", random_state=0 ) lda.fit(clean_vec1)输出如下:
![图 7.59:LDA 模型]()
图 7.59:LDA 模型
-
创建并打印词-主题表:
def get_topics(mod, vec, names, docs, ndocs, nwords): # word to topic matrix W = mod.components_ W_norm = W / W.sum(axis=1)[:, numpy.newaxis] # topic to document matrix H = mod.transform(vec) W_dict = {} H_dict = {} for tpc_idx, tpc_val in enumerate(W_norm): topic = «Topic{}".format(tpc_idx) # formatting w W_indices = tpc_val.argsort()[::-1][:nwords] W_names_values = [ (round(tpc_val[j], 4), names[j]) for j in W_indices ] W_dict[topic] = W_names_values # formatting h H_indices = H[:, tpc_idx].argsort()[::-1][:ndocs] H_names_values = [ (round(H[:, tpc_idx][j], 4), docs[j]) for j in H_indices ] H_dict[topic] = H_names_values W_df = pandas.DataFrame( W_dict, index=["Word" + str(i) for i in range(nwords)] ) H_df = pandas.DataFrame( H_dict, index=["Doc" + str(i) for i in range(ndocs)] ) return (W_df, H_df) W_df, H_df = get_topics( mod=lda, vec=clean_vec1, names=feature_names_vec1, docs=raw, ndocs=number_docs, nwords=number_words ) print(W_df)输出如下:
![图 7.60:健康推文数据的词-主题表]()
图 7.60:健康推文数据的词-主题表
-
打印文档-主题表:
print(H_df)输出如下:
![图 7.61:文档主题表]()
图 7.61:文档主题表
-
创建双图可视化:
lda_plot = pyLDAvis.sklearn.prepare(lda, clean_vec1, vectorizer1, R=10) pyLDAvis.display(lda_plot)![图 7.62:在健康推文上训练的 LDA 模型的直方图和双图]()
图 7.62:在健康推文上训练的 LDA 模型的直方图和双图
-
保持笔记本打开以供将来建模。
活动 17:非负矩阵分解
解决方案:
-
创建适当的词袋模型,并将特征名作为另一个变量输出:
vectorizer2 = sklearn.feature_extraction.text.TfidfVectorizer( analyzer="word", max_df=0.5, min_df=20, max_features=number_features, smooth_idf=False ) clean_vec2 = vectorizer2.fit_transform(clean_sentences) print(clean_vec2[0]) feature_names_vec2 = vectorizer2.get_feature_names() -
定义并使用活动二中的主题数(
n_components)值拟合 NMF 算法:nmf = sklearn.decomposition.NMF( n_components=optimal_num_topics, init="nndsvda", solver="mu", beta_loss="frobenius", random_state=0, alpha=0.1, l1_ratio=0.5 ) nmf.fit(clean_vec2)输出如下:
![图 7.63:定义 NMF 模型]()
图 7.63:定义 NMF 模型
-
获取主题-文档和单词-主题的结果表格。花几分钟时间探索单词分组,并尝试定义抽象的主题:
W_df, H_df = get_topics( mod=nmf, vec=clean_vec2, names=feature_names_vec2, docs=raw, ndocs=number_docs, nwords=number_words ) print(W_df)![图 7.64:带有概率的单词-主题表]()
图 7.64:带有概率的单词-主题表
-
调整模型参数并重新运行 步骤 3 和 步骤 4。
第八章:市场购物篮分析
活动 18:加载和准备完整在线零售数据
解决方案:
-
加载在线零售数据集文件:
import matplotlib.pyplot as plt import mlxtend.frequent_patterns import mlxtend.preprocessing import numpy import pandas online = pandas.read_excel( io="Online Retail.xlsx", sheet_name="Online Retail", header=0 ) -
清洗并准备数据进行建模,包括将清洗后的数据转化为列表的列表:
online['IsCPresent'] = ( online['InvoiceNo'] .astype(str) .apply(lambda x: 1 if x.find('C') != -1 else 0) ) online1 = ( online .loc[online["Quantity"] > 0] .loc[online['IsCPresent'] != 1] .loc[:, ["InvoiceNo", "Description"]] .dropna() ) invoice_item_list = [] for num in list(set(online1.InvoiceNo.tolist())): tmp_df = online1.loc[online1['InvoiceNo'] == num] tmp_items = tmp_df.Description.tolist() invoice_item_list.append(tmp_items) -
对数据进行编码并将其重新构建为 DataFrame:
online_encoder = mlxtend.preprocessing.TransactionEncoder() online_encoder_array = online_encoder.fit_transform(invoice_item_list) online_encoder_df = pandas.DataFrame( online_encoder_array, columns=online_encoder.columns_ ) online_encoder_df.loc[ 20125:20135, online_encoder_df.columns.tolist()[100:110] ]输出结果如下:

图 8.35:从完整在线零售数据集中构建的清洗、编码和重新构建的 DataFrame 子集
活动 19:在完整在线零售数据集上运行 Apriori 算法
解决方案:
-
使用合理的参数设置在完整数据上运行 Apriori 算法:
mod_colnames_minsupport = mlxtend.frequent_patterns.apriori( online_encoder_df, min_support=0.01, use_colnames=True ) mod_colnames_minsupport.loc[0:6]输出结果如下:
![图 8.36:使用完整在线零售数据集的 Apriori 算法结果]()
图 8.36:使用完整在线零售数据集的 Apriori 算法结果
-
将结果过滤到包含
10 COLOUR SPACEBOY PEN的物品集。将其支持度值与 练习 44 中的支持度值进行比较,执行 Apriori 算法:mod_colnames_minsupport[ mod_colnames_minsupport['itemsets'] == frozenset( {'10 COLOUR SPACEBOY PEN'} ) ]输出结果如下:
![图 8.37:包含 10 支 COLOUR SPACEBOY PEN 的物品集结果]()
图 8.37:包含 10 支 COLOUR SPACEBOY PEN 的物品集结果
支持度值确实发生了变化。当数据集扩展到包含所有交易时,该物品集的支持度从 0.015 增加到 0.015793。也就是说,在用于练习的缩小数据集中,这个物品集出现在 1.5% 的交易中,而在完整数据集中,它出现在大约 1.6% 的交易中。
-
添加一个新的列,包含物品集的长度。然后,过滤出长度为 2 且支持度在 [0.02, 0.021] 范围内的物品集。是否与 练习 44 中找到的物品集相同?执行 Apriori 算法,步骤 6?
mod_colnames_minsupport['length'] = ( mod_colnames_minsupport['itemsets'].apply(lambda x: len(x)) ) mod_colnames_minsupport[ (mod_colnames_minsupport['length'] == 2) & (mod_colnames_minsupport['support'] >= 0.02) & (mod_colnames_minsupport['support'] < 0.021) ]![图 8.38:基于长度和支持度过滤结果的部分]()
图 8.38:基于长度和支持度过滤结果的部分
结果确实发生了变化。在查看具体的物品集及其支持度值之前,我们看到这个经过过滤的 DataFrame 比前一个练习中的 DataFrame 物品集要少。当我们使用完整数据集时,符合过滤标准的物品集较少;也就是说,只有 14 个物品集包含 2 个物品,并且支持度值大于或等于 0.02,且小于 0.021。在前一个练习中,有 17 个物品集符合这些标准。
-
绘制
support值:mod_colnames_minsupport.hist("support", grid=False, bins=30) plt.title("Support")

图 8.39:支持度值的分布
该图展示了完整交易数据集中的支持度值分布。正如你可能已经猜测的,分布是右偏的;也就是说,大多数项集的支持度较低,并且在高端范围内有一个长尾。考虑到存在大量独特的项集,单个项集在高比例交易中出现的情况并不令人惊讶。凭借这些信息,我们可以告诉管理层,即便是最突出的项集也仅在大约 10%的交易中出现,而绝大多数项集的出现频率不到 2%。这些结果可能不会支持改变商店布局,但很可能会对定价和折扣策略提供指导。通过公式化一些关联规则,我们可以获得更多有关如何构建这些策略的信息。
活动 20:在完整在线零售数据集上查找关联规则
解决方案:
-
将关联规则模型拟合到完整数据集上。使用置信度指标,最小阈值为 0.6:
rules = mlxtend.frequent_patterns.association_rules( mod_colnames_minsupport, metric="confidence", min_threshold=0.6, support_only=False ) rules.loc[0:6]输出如下:
![图 8.40:基于完整在线零售数据集的关联规则]()
图 8.40:基于完整在线零售数据集的关联规则
-
计算关联规则的数量。这个数量是否与练习 45,推导关联规则,步骤 1中找到的数量不同?
print("Number of Associations: {}".format(rules.shape[0]))存在
498条关联规则。 -
绘制置信度与支持度的关系图:
rules.plot.scatter("support", "confidence", alpha=0.5, marker="*") plt.xlabel("Support") plt.ylabel("Confidence") plt.title("Association Rules") plt.show()输出如下:
![图 8.41:置信度与支持度的关系图]()
图 8.41:置信度与支持度的关系图
该图揭示了该数据集中一些关联规则,它们具有相对较高的支持度和置信度值。
-
查看提升值、杠杆值和信念值的分布:
rules.hist("lift", grid=False, bins=30) plt.title("Lift")输出如下:

图 8.42:提升值的分布
rules.hist("leverage", grid=False, bins=30)
plt.title("Leverage")
输出如下:

图 8.43:杠杆值的分布
plt.hist(
rules[numpy.isfinite(rules['conviction'])].conviction.values,
bins = 30
)
plt.title("Conviction")
输出如下:

图 8.44:信念值的分布
得出关联规则后,我们可以向管理层提供更多信息,其中最重要的一点是,大约有七个项集在支持度和置信度上都有较高的值。查看支持度与置信度的散点图,看看哪些七个项集与其他项集有所区别。这七个项集的提升值也很高,从提升直方图中可以看出。看来我们已经识别出一些可以采取行动的关联规则,这些规则可以用来推动商业决策。
第九章:热点分析
活动 21:在一维空间中估算密度
解决方案:
-
打开一个新笔记本并安装所有必要的库。
get_ipython().run_line_magic('matplotlib', 'inline') import matplotlib.pyplot as plt import numpy import pandas import seaborn import sklearn.datasets import sklearn.model_selection import sklearn.neighbors seaborn.set() -
从标准正态分布中采样 1,000 个数据点。将 3.5 加到样本的最后 625 个值上(即,索引范围从 375 到 1,000)。为此,使用
numpy.random.RandomState设置随机状态为 100,以保证采样值一致,然后使用randn(1000)调用随机生成数据点:rand = numpy.random.RandomState(100) vals = rand.randn(1000) # standard normal vals[375:] += 3.5 -
将 1,000 个数据点的样本数据绘制为直方图,并在其下方添加散点图:
fig, ax = plt.subplots(figsize=(14, 10)) ax.hist(vals, bins=50, density=True, label='Sampled Values') ax.plot(vals, -0.005 - 0.01 * numpy.random.random(len(vals)), '+k', label='Individual Points') ax.legend(loc='upper right')输出如下:
![图 9.29:随机样本的直方图,下方是散点图]()
图 9.29:随机样本的直方图,下方是散点图
-
定义一组带宽值。然后,定义并拟合一个网格搜索交叉验证算法:
bandwidths = 10 ** numpy.linspace(-1, 1, 100) grid = sklearn.model_selection.GridSearchCV( estimator=sklearn.neighbors.KernelDensity(kernel="gaussian"), param_grid={"bandwidth": bandwidths}, cv=10 ) grid.fit(vals[:, None]) -
提取最佳带宽值:
best_bandwidth = grid.best_params_["bandwidth"] print( "Best Bandwidth Value: {}" .format(best_bandwidth) ) -
重新绘制步骤 3中的直方图,并叠加估计的密度:
fig, ax = plt.subplots(figsize=(14, 10)) ax.hist(vals, bins=50, density=True, alpha=0.75, label='Sampled Values') x_vec = numpy.linspace(-4, 8, 10000)[:, numpy.newaxis] log_density = numpy.exp(grid.best_estimator_.score_samples(x_vec)) ax.plot( x_vec[:, 0], log_density, '-', linewidth=4, label='Kernel = Gaussian' ) ax.legend(loc='upper right')输出如下:

图 9.30:随机样本的直方图,叠加了最佳估计的密度
活动 22:分析伦敦的犯罪情况
解决方案:
-
加载犯罪数据。使用保存下载目录的路径,创建年份-月份标签的列表,使用
read_csv命令逐个加载单独的文件,然后将这些文件合并在一起:base_path = ( "~/Documents/packt/unsupervised-learning-python/" "lesson-9-hotspot-models/metro-jul18-dec18/" "{yr_mon}/{yr_mon}-metropolitan-street.csv" ) print(base_path) yearmon_list = [ "2018-0" + str(i) if i <= 9 else "2018-" + str(i) for i in range(7, 13) ] print(yearmon_list) data_yearmon_list = [] for idx, i in enumerate(yearmon_list): df = pandas.read_csv( base_path.format(yr_mon=i), header=0 ) data_yearmon_list.append(df) if idx == 0: print("Month: {}".format(i)) print("Dimensions: {}".format(df.shape)) print("Head:\n{}\n".format(df.head(2))) london = pandas.concat(data_yearmon_list)输出如下:
![图 9.31:单个犯罪文件的示例]()
图 9.31:单个犯罪文件的示例
该打印信息仅针对加载的第一个文件,即 2018 年 7 月伦敦大都会警察局的犯罪信息。该文件包含近 100,000 条记录。你会注意到数据集中包含大量有趣的信息,但我们将重点关注
Longitude(经度)、Latitude(纬度)、Month(月份)和Crime type(犯罪类型)。 -
打印完整(六个月)和合并数据集的诊断信息:
print( "Dimensions - Full Data:\n{}\n" .format(london.shape) ) print( "Unique Months - Full Data:\n{}\n" .format(london["Month"].unique()) ) print( "Number of Unique Crime Types - Full Data:\n{}\n" .format(london["Crime type"].nunique()) ) print( "Unique Crime Types - Full Data:\n{}\n" .format(london["Crime type"].unique()) ) print( "Count Occurrences Of Each Unique Crime Type - Full Type:\n{}\n" .format(london["Crime type"].value_counts()) )输出如下:
![图 9.32:完整犯罪数据集的描述信息]()
图 9.32:完整犯罪数据集的描述信息
-
将数据框限制为四个变量(
Longitude,Latitude,Month和Crime type):london_subset = london[["Month", "Longitude", "Latitude", "Crime type"]] london_subset.head(5)输出如下:
![图 9.33:数据框中犯罪数据的子集,仅保留经度、纬度、月份和犯罪类型列]()
图 9.33:数据框中犯罪数据的子集,仅保留经度、纬度、月份和犯罪类型列
-
使用
seaborn的jointplot函数,拟合并可视化 2018 年 7 月、9 月和 12 月自行车盗窃的三种核密度估计模型:crime_bicycle_jul = london_subset[ (london_subset["Crime type"] == "Bicycle theft") & (london_subset["Month"] == "2018-07") ] seaborn.jointplot("Longitude", "Latitude", crime_bicycle_jul, kind="kde")输出如下:
![图 9.34:2018 年 7 月自行车盗窃的联合密度和边际密度估计]()
图 9.34:2018 年 7 月自行车盗窃的联合密度和边际密度估计
crime_bicycle_sept = london_subset[ (london_subset["Crime type"] == "Bicycle theft") & (london_subset["Month"] == "2018-09") ] seaborn.jointplot("Longitude", "Latitude", crime_bicycle_sept, kind="kde")输出如下:
![图 9.35:2018 年 9 月自行车盗窃的联合分布和边际分布估计]
图 9.35:2018 年 9 月自行车盗窃的联合分布和边际分布估计
crime_bicycle_dec = london_subset[ (london_subset["Crime type"] == "Bicycle theft") & (london_subset["Month"] == "2018-12") ] seaborn.jointplot("Longitude", "Latitude", crime_bicycle_dec, kind="kde")输出结果如下:
![图 9.36:2018 年 12 月自行车盗窃的联合分布和边际分布估计]
图 9.36:2018 年 12 月自行车盗窃的联合分布和边际分布估计
从月份到月份,自行车盗窃的密度保持相当稳定。密度之间有细微的差异,这在所预期之内,因为这些估计的密度是基于三个月的样本数据。根据这些结果,警察或犯罪学家应该对预测未来最可能发生自行车盗窃的地点充满信心。
-
重复步骤 4;这次,使用 2018 年 8 月、10 月和 11 月的商店盗窃数据:
crime_shoplift_aug = london_subset[ (london_subset["Crime type"] == "Shoplifting") & (london_subset["Month"] == "2018-08") ] seaborn.jointplot("Longitude", "Latitude", crime_shoplift_aug, kind="kde")输出结果如下:
![图 9.37:2018 年 8 月商店盗窃事件的联合分布和边际分布估计]
图 9.37:2018 年 8 月商店盗窃事件的联合分布和边际分布估计
crime_shoplift_oct = london_subset[ (london_subset["Crime type"] == "Shoplifting") & (london_subset["Month"] == "2018-10") ] seaborn.jointplot("Longitude", "Latitude", crime_shoplift_oct, kind="kde")输出结果如下:
![图 9.38:2018 年 10 月商店盗窃事件的联合分布和边际分布估计]
图 9.38:2018 年 10 月商店盗窃事件的联合分布和边际分布估计
crime_shoplift_nov = london_subset[ (london_subset["Crime type"] == "Shoplifting") & (london_subset["Month"] == "2018-11") ] seaborn.jointplot("Longitude", "Latitude", crime_shoplift_nov, kind="kde")输出结果如下:
![图 9.39:2018 年 11 月商店盗窃事件的联合分布和边际分布估计]
图 9.39:2018 年 11 月商店盗窃事件的联合分布和边际分布估计
与自行车盗窃的结果类似,商店盗窃的密度在各个月份之间保持相当稳定。2018 年 8 月的密度看起来与其他两个月有所不同;然而,如果你查看经纬度值,你会发现密度非常相似,只是发生了平移和缩放。这是因为可能存在一些离群点,迫使绘图区域变得更大。
-
重复步骤 5;这次使用 2018 年 7 月、10 月和 12 月的入室盗窃数据:
crime_burglary_jul = london_subset[ (london_subset["Crime type"] == "Burglary") & (london_subset["Month"] == "2018-07") ] seaborn.jointplot("Longitude", "Latitude", crime_burglary_jul, kind="kde")输出结果如下:
![图 9.40:2018 年 7 月入室盗窃的联合分布和边际分布估计]
图 9.40:2018 年 7 月入室盗窃的联合分布和边际分布估计
crime_burglary_oct = london_subset[
(london_subset["Crime type"] == "Burglary") &
(london_subset["Month"] == "2018-10")
]
seaborn.jointplot("Longitude", "Latitude", crime_burglary_oct, kind="kde")
输出结果如下:
![图 9.41:2018 年 10 月入室盗窃的联合分布和边际分布估计]
图 9.41:2018 年 10 月入室盗窃的联合分布和边际分布估计
crime_burglary_dec = london_subset[
(london_subset["Crime type"] == "Burglary") &
(london_subset["Month"] == "2018-12")
]
seaborn.jointplot("Longitude", "Latitude", crime_burglary_dec, kind="kde")
输出结果如下:
![图 9.42:2018 年 12 月入室盗窃的联合分布和边际分布估计]
图 9.42:2018 年 12 月入室盗窃的联合分布和边际分布估计
再次,我们可以看到各个月份的分布非常相似。唯一的区别是从七月到十二月,密度似乎变得更加宽广或分散。和往常一样,样本数据中固有的噪声和信息缺失导致了估计密度的微小变化。



























































和
,在高维空间中,我们想确定概率(
),即
将被选为
。为了定义这个概率,我们使用高斯曲线,可以看到对于邻近的点,概率较高,而对于远离的点,概率非常低。
),这与对应的低维数据
和
之间的关系有关。
和 

























,其中
是单词的数量。
,其中
是主题的分布。
单词
,选择主题
,并从
。
:这是主题的潜在变量
:这是每个文档主题分布的潜在变量














面包):


















































































浙公网安备 33010602011771号