无监督机器学习研讨会-全-

无监督机器学习研讨会(全)

原文:annas-archive.org/md5/59ef285f4ffb779ed4c411e356902e16

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章:The

无监督学习

研讨会

开始使用无监督学习算法,并简化你的未整理数据,以帮助进行未来的预测

Aaron Jones, Christopher Kruger, 和 Benjamin Johnston

无监督学习研讨会

版权 © 2020 Packt Publishing

版权所有。未经出版商事先书面许可,本课程的任何部分不得以任何形式或任何手段复制、存储于检索系统中或传播,但在关键文章或评论中嵌入简短的引用除外。

本课程在准备过程中已尽力确保所提供信息的准确性。然而,本课程所包含的信息是按“原样”销售的,不提供任何形式的明示或暗示的担保。无论是作者、Packt Publishing 还是其经销商和分销商,都不对因本课程直接或间接引起的或被声称引起的任何损害负责。

Packt Publishing 力求通过恰当使用大写字母提供课程中提及的所有公司和产品的商标信息。然而,Packt Publishing 无法保证这些信息的准确性。

作者: Aaron Jones, Christopher Kruger, 和 Benjamin Johnston

审阅人: Richard Brooker, John Wesley Doyle, Priyanjit Ghosh, Sani Kamal, Ashish Pratik Patil, Geetank Raipuria, 和 Ratan Singh

执行编辑: Rutuja Yerunkar

采购编辑: Manuraj Nair, Royluis Rodrigues, Anindya Sil, 和 Karan Wadekar

生产编辑: Salma Patel

编辑委员会: Megan Carlisle, Samuel Christa, Mahesh Dhyani, Heather Gopsill, Manasa Kumar, Alex Mazonowicz, Monesh Mirpuri, Bridget Neale, Dominic Pereira, Shiny Poojary, Abhishek Rane, Brendan Rodrigues, Erol Staveley, Ankita Thakur, Nitesh Thakur, 和 Jonathan Wray

初版:2020 年 7 月

生产参考:1280720

ISBN:978-1-80020-070-8

由 Packt Publishing Ltd. 出版

Livery Place, 35 Livery Street

英国伯明翰 B3 2PB

目录

前言    i

1. 聚类简介    1

简介    2

无监督学习与有监督学习   2

聚类    4

识别聚类   5

二维数据   6

练习 1.01:数据中的聚类识别   7

k-means 聚类简介   11

无数学 k-means 步骤详解   11

k-means 聚类深入讲解    13

替代距离度量 – 曼哈顿距离 14

更深的维度 15

练习 1.02:在 Python 中计算欧氏距离 16

练习 1.03:通过距离概念形成聚类 18

练习 1.04:从零开始的 k-means – 第一部分:数据生成 20

练习 1.05:从零开始的 k-means – 第二部分:实现 k-means 24

聚类性能 – 轮廓系数 29

练习 1.06:计算轮廓系数 31

活动 1.01:实现 k-means 聚类 33

总结 35

2. 层次聚类 37

简介 38

聚类复习 38

k-means 复习 39

层次结构的组织 39

层次聚类简介 41

层次聚类步骤 43

层次聚类示例演练 43

练习 2.01:构建层次结构 47

连接 52

练习 2.02:应用连接标准 53

凝聚型与分裂型聚类 58

练习 2.03:使用 scikit-learn 实现凝聚层次聚类 60

活动 2.01:比较 k-means 和层次聚类 64

k-means 与层次聚类 68

总结 69

3. 邻域方法与 DBSCAN 71

简介 72

聚类作为邻域 73

DBSCAN 简介 75

DBSCAN 详细解析 76

DBSCAN 算法演练 77

练习 3.01:评估邻域半径大小的影响 80

DBSCAN 属性 - 邻域半径 84

活动 3.01:从头实现 DBSCAN 86

DBSCAN 属性 - 最小点数 88

练习 3.02:评估最小点数阈值的影响 89

活动 3.02:将 DBSCAN 与 k-means 和层次聚类进行比较 93

DBSCAN 与 k-means 和层次聚类的对比 95

总结 96

4. 主成分分析与降维技术 99

介绍 100

什么是降维? 100

降维技术的应用 102

维度灾难 104

降维技术概览 106

降维 108

主成分分析 109

均值 109

标准差 109

协方差 110

协方差矩阵 110

练习 4.01:使用 pandas 库计算均值、标准差和方差 111

特征值与特征向量 116

练习 4.02:计算特征值和特征向量 117

PCA 的过程 121

练习 4.03:手动执行 PCA 123

练习 4.04:使用 scikit-learn 进行 PCA 128

活动 4.01:手动 PCA 与 scikit-learn 对比 133

恢复压缩后的数据集 136

练习 4.05:使用手动 PCA 可视化方差减少 136

练习 4.06:使用 scikit-learn 可视化方差减少 143

练习 4.07:在 Matplotlib 中绘制 3D 图 147

活动 4.02: 使用扩展的种子数据集进行 PCA 150

总结 153

5. 自编码器 155

简介 156

人工神经网络基础 157

神经元 159

Sigmoid 函数 160

修正线性单元 (ReLU) 161

练习 5.01: 建模人工神经网络的神经元 161

练习 5.02: 使用 ReLU 激活函数建模神经元 165

神经网络: 架构定义 169

练习 5.03: 定义一个 Keras 模型 171

神经网络: 训练 173

练习 5.04: 训练一个 Keras 神经网络模型 175

活动 5.01: MNIST 神经网络 185

自编码器 187

练习 5.05: 简单自编码器 188

活动 5.02: 简单 MNIST 自编码器 193

练习 5.06: 多层自编码器 194

卷积神经网络 199

练习 5.07: 卷积自编码器 200

活动 5.03: MNIST 卷积自编码器 205

总结 207

6. t-分布随机邻居嵌入 209

简介 210

MNIST 数据集 210

随机邻居嵌入 (SNE) 212

t-分布 SNE 213

练习 6.01: t-SNE MNIST 214

活动 6.01: 葡萄酒 t-SNE 227

解释 t-SNE 图 229

困惑度 230

练习 6.02: t-SNE MNIST 和困惑度 230

活动 6.02: t-SNE 葡萄酒与困惑度 235

迭代   236

练习 6.03:t-SNE MNIST 和迭代   237

活动 6.03:t-SNE 葡萄酒和迭代   242

关于可视化的最终思考   243

总结   243

7. 主题建模   245

简介   246

主题模型   247

练习 7.01:设置环境   249

主题模型的高级概览   250

商业应用   254

练习 7.02:数据加载   256

清理文本数据   259

数据清理技术   260

练习 7.03:逐步清理数据   261

练习 7.04:完整数据清理   266

活动 7.01:加载和清理 Twitter 数据   268

潜在狄利克雷分配   270

变分推理   272

词袋模型   275

练习 7.05:使用计数向量化器创建词袋模型   276

困惑度   277

练习 7.06:选择主题数量   279

练习 7.07:运行 LDA   281

可视化   286

练习 7.08:可视化 LDA   287

练习 7.09:尝试四个主题   291

活动 7.02:LDA 和健康推文   296

练习 7.10:使用 TF-IDF 创建词袋模型   298

非负矩阵分解   299

弗罗贝纽斯范数   301

乘法更新算法   301

练习 7.11:非负矩阵分解   302

练习 7.12:可视化 NMF   306

活动 7.03:非负矩阵分解 309

总结 310

市场购物篮分析 313

介绍 314

市场购物篮分析 314

应用案例 317

重要的概率性指标 318

练习 8.01:创建样本事务数据 319

支持度 321

置信度 322

提升度和杠杆度 323

信念度 324

练习 8.02:计算指标 325

事务数据的特征 328

练习 8.03:加载数据 329

数据清理和格式化 333

练习 8.04:数据清理和格式化 334

数据编码 339

练习 8.05:数据编码 341

活动 8.01:加载和准备完整的在线零售数据 343

Apriori 算法 344

计算修正 347

练习 8.06:执行 Apriori 算法 348

活动 8.02:在完整的在线零售数据集上运行 Apriori 算法 354

关联规则 356

练习 8.07:推导关联规则 358

活动 8.03:在完整的在线零售数据集上找出关联规则 365

总结 367

热点分析 369

介绍 370

空间统计 371

概率密度函数 372

在商业中使用热点分析 374

核密度估计 375

带宽值 376

练习 9.01:带宽值的影响 376

选择最优带宽 380

练习 9.02:使用网格搜索选择最优带宽 381

核函数 384

练习 9.03:核函数的影响 387

核密度估计推导 389

练习 9.04:模拟核密度估计推导 389

活动 9.01:一维密度估计 393

热点分析 394

练习 9.05:使用 Seaborn 加载数据和建模 396

练习 9.06:与底图一起工作 404

活动 9.02:分析伦敦犯罪 411

总结 414

附录 417

前言

关于本书

你是否觉得很难理解像 WhatsApp 和 Amazon 这样的知名公司如何从大量杂乱无章的数据中提取有价值的洞察?无监督学习工作坊将让你自信地应对混乱且没有标签的数据集,以轻松互动的方式使用无监督算法。

本书从介绍最流行的无监督学习聚类算法开始。你将了解层次聚类与 k-means 的不同,并理解如何将 DBSCAN 应用于复杂且噪声较多的数据。接下来,你将使用自编码器进行高效的数据编码。

随着学习的深入,你将使用 t-SNE 模型将高维信息转换为低维,以便更好地可视化,同时还会使用主题建模来实现自然语言处理。在后续章节中,你将使用市场篮分析来发现顾客和商家之间的关键关系,然后使用热点分析来估算某一地区的人口密度。

到本书结束时,你将掌握在混乱的数据集中应用无监督算法来发现有用模式和洞察的技能。

读者群体

如果你是刚入门的数据科学家,想要学习如何实现机器学习算法以构建预测模型,那么本书适合你。为了加快学习进程,建议你具备扎实的 Python 编程语言基础,因为你将编辑类和函数,而不是从零开始创建它们。

关于各章节

第一章聚类简介,介绍了聚类(无监督学习中最知名的算法家族),然后深入讲解最简单、最流行的聚类算法——k-means。

第二章层次聚类,讲解了另一种聚类技术——层次聚类,并说明它与 k-means 的不同。本章将教你两种主要的聚类方法:凝聚型和分裂型。

第三章邻域方法和 DBSCAN,探讨了涉及邻居的聚类方法。与另外两种聚类方法不同,邻域方法允许存在未被分配到任何特定聚类的异常点。

第四章降维与 PCA,教你如何通过主成分分析来减少特征数量,同时保持整个特征空间的解释能力,从而在大型特征空间中导航。

第五章自编码器,向你展示如何利用神经网络找到数据编码。数据编码就像是特征的组合,能够降低特征空间的维度。自编码器还会解码数据并将其恢复到原始形式。

第六章t-分布随机邻居嵌入,讨论了将高维数据集降维到二维或三维进行可视化的过程。与 PCA 不同,t-SNE 是一种非线性概率模型。

第七章主题建模,探讨了自然语言处理的基本方法论。你将学习如何处理文本数据,并将潜在的狄利克雷分配和非负矩阵分解模型应用于标记与文本相关的主题。

第八章市场篮子分析,探讨了零售业务中使用的经典分析技术。你将以可扩展的方式,构建解释项目组之间关系的关联规则。

第九章热点分析,教你如何使用样本数据估算某些随机变量的真实人口密度。该技术适用于许多领域,包括流行病学、天气、犯罪和人口学。

约定

文本中的代码词、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名显示如下:

"使用我们从matplotlib.pyplot导入的散点图功能绘制坐标点。"

屏幕上看到的词(例如,在菜单或对话框中)以相同的格式显示。

一段代码如下所示:

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
seeds = pd.read_csv('Seed_Data.csv')

新术语和重要词汇显示如下:

"无监督学习是一个实践领域,旨在帮助在杂乱的数据中找到模式,是当前机器学习中最令人兴奋的发展领域之一。"

长的代码片段会被截断,并在截断代码的顶部放置对应的 GitHub 代码文件名称。整个代码的永久链接将放在代码片段下方。应该如下所示:

Exercise1.04-Exercise1.05.ipynb
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)
The complete code for this step can be found at https://packt.live/2JM8Q1S.

代码呈现

跨越多行的代码使用反斜杠(\)分隔。当代码执行时,Python 将忽略反斜杠,并将下一行代码视为当前行的直接延续。

例如:

history = model.fit(X, y, epochs=100, batch_size=5, verbose=1, \
                    validation_split=0.2, shuffle=False)

注释被添加到代码中,以帮助解释特定的逻辑。单行注释使用#符号表示,如下所示:

# Print the sizes of the dataset
print("Number of Examples in the Dataset = ", X.shape[0])
print("Number of Features for each example = ", X.shape[1])

多行注释被三引号包围,如下所示:

"""
Define a seed for the random number generator to ensure the 
result will be reproducible
"""
seed = 1
np.random.seed(seed)
random.set_seed(seed)

设置你的环境

在我们详细探讨本书之前,我们需要设置特定的软件和工具。在接下来的部分中,我们将看到如何进行这些操作。

硬件要求

为了获得最佳用户体验,我们推荐 8GB 内存。

安装 Python

接下来的部分将帮助你在 Windows、macOS 和 Linux 系统中安装 Python。

在 Windows 上安装 Python

  1. 在官方安装页面www.python.org/downloads/windows/上找到你所需的 Python 版本。

  2. 确保根据你的计算机系统安装正确的“-bit”版本,可以是 32-bit 或 64-bit。你可以在操作系统的 系统属性 窗口中查看此信息。

  3. 下载安装程序后,简单地双击文件并按照屏幕上的用户友好提示进行操作。

在 Linux 上安装 Python

  1. 打开终端并通过运行 python3 --version 验证 Python 3 是否已安装。

  2. 要安装 Python 3,运行以下命令:

    sudo apt-get update
    sudo apt-get install python3.7
    
  3. 如果遇到问题,有很多在线资源可以帮助你排查问题。

在 macOS 上安装 Python

以下是在 macOS 上安装 Python 的步骤:

  1. 通过按住 Cmd + Space,在打开的搜索框中输入 terminal,然后按 Enter 打开终端。

  2. 通过运行 xcode-select --install 命令在命令行中安装 Xcode。

  3. 安装 Python 3 最简单的方法是使用 Homebrew,可以通过运行 ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 在命令行中安装 Homebrew。

  4. 将 Homebrew 添加到你的 PATH 环境变量中。在命令行中运行 sudo nano ~/.profile 打开你的配置文件,并在文件底部插入 export PATH="/usr/local/opt/python/libexec/bin:$PATH"

  5. 最后一步是安装 Python。在命令行中运行 brew install python

  6. 请注意,如果安装 Anaconda,最新版本的 Python 将会自动安装。

安装 pip

Python 默认不包含 pip(Python 的包管理器),因此我们需要手动安装它。一旦安装了 pip,就可以按照 安装库 部分提到的方法安装其余的库。安装 pip 的步骤如下:

  1. 转到 bootstrap.pypa.io/get-pip.py 并将文件保存为 get-pip.py

  2. 转到保存get-pip.py的文件夹。在该文件夹中打开命令行(Linux 用户使用 Bash,Mac 用户使用 Terminal)。

  3. 在命令行中执行以下命令:

    python get-pip.py
    

    请注意,你应该先安装 Python 后再执行此命令。

  4. 一旦安装了 pip,你就可以安装所需的库。要安装 pandas,只需执行 pip install pandas。要安装某个特定版本的库,例如 pandas 的版本 0.24.2,可以执行 pip install pandas=0.24.2

安装 Anaconda

Anaconda 是一个 Python 包管理器,能够轻松地安装并使用本课程所需的库。

在 Windows 上安装 Anaconda

  1. Windows 上的 Anaconda 安装非常用户友好。请访问下载页面,在 www.anaconda.com/distribution/#download-section 获取安装可执行文件。

  2. 双击计算机上的安装程序。

  3. 按照屏幕上的提示完成 Anaconda 的安装。

  4. 安装完成后,你可以访问 Anaconda Navigator,它将像其他应用程序一样正常显示。

在 Linux 上安装 Anaconda

  1. 访问 Anaconda 下载页面,获取安装 shell 脚本,网址为www.anaconda.com/distribution/#download-section

  2. 要直接将 shell 脚本下载到你的 Linux 实例中,你可以使用curlwget下载库。下面的示例展示了如何使用curl从 Anaconda 下载页面找到的 URL 下载文件:

    curl -O https://repo.anaconda.com/archive/Anaconda3-2019.03-Linux-x86_64.sh
    
  3. 下载完 shell 脚本后,你可以使用以下命令运行它:

    bash Anaconda3-2019.03-Linux-x86_64.sh
    

    运行上述命令后,你将进入一个非常用户友好的安装过程。系统会提示你选择安装 Anaconda 的位置以及如何配置 Anaconda。在这种情况下,你只需保持所有默认设置。

在 macOS X 上安装 Anaconda

  1. 在 macOS 上安装 Anaconda 非常用户友好。访问下载页面获取安装可执行文件,网址为www.anaconda.com/distribution/#download-section

  2. 确保选择了 macOS,并双击Download按钮以下载 Anaconda 安装程序。

  3. 按照屏幕上的提示完成 Anaconda 安装。

  4. 安装完成后,你可以访问 Anaconda Navigator,它将像其他应用程序一样正常显示。

设置虚拟环境

  1. 安装完 Anaconda 后,你必须创建环境来安装你希望使用的包。Anaconda 环境的一个优点是,你可以为你正在进行的具体项目构建独立的环境。要创建新的环境,使用以下命令:

    conda create --name my_packt_env python=3.7
    

    在这里,我们将环境命名为my_packt_env并指定 Python 版本为 3.7。这样,你可以在环境中安装多个版本的 Python,这些版本将是虚拟隔离的。

  2. 创建环境后,你可以使用名称恰当的activate命令激活它:

    conda activate my_packt_env
    

    就这样。你现在已经进入了自己的定制化环境,可以根据项目需要安装包。要退出环境,你只需使用conda deactivate命令。

安装库

pip 已预先安装在 Anaconda 中。一旦 Anaconda 安装到你的计算机上,你可以使用pip安装所有所需的库,例如,pip install numpy。或者,你可以使用pip install –r requirements.txt安装所有必需的库。你可以在packt.live/2CnpCEp找到requirements.txt文件。

练习和活动将在 Jupyter Notebooks 中执行。Jupyter 是一个 Python 库,可以像其他 Python 库一样通过 pip install jupyter 安装,但幸运的是,它已经随 Anaconda 一起预安装了。要打开一个笔记本,只需在终端或命令提示符中运行命令 jupyter notebook

第九章热点分析中,使用了 mpl_toolkits 中的 basemap 模块来生成地图。这个库可能会很难安装。最简单的方法是安装 Anaconda,它包含了 mpl_toolkits。安装 Anaconda 后,可以通过 conda install basemap 来安装 basemap。如果你希望避免重复安装库,而是一次性安装所有库,你可以按照下一节的说明进行操作。

设置机器

如果你是按章节逐步安装依赖项,可能会遇到库的版本不同的情况。为了同步系统,我们提供了一个包含所用库版本的 requirements.txt 文件。使用此文件安装库后,你就不需要在整本书中再次安装任何其他库。假设你现在已经安装了 Anaconda,可以按照以下步骤进行操作:

  1. 从 GitHub 下载 requirements.txt 文件。

  2. 转到 requirements.txt 文件所在的文件夹,并打开命令提示符(Linux 为 Bash,Mac 为 Terminal)。

  3. 在其上执行以下命令:

    conda install --yes --file requirements.txt --channel conda-forge
    

    它应该会安装本书中所有编程活动所需的包。

访问代码文件

你可以在 packt.live/34kXeMw 找到本书的完整代码文件。你也可以通过使用 packt.live/2ZMUWW0 提供的交互式实验环境,直接在你的网页浏览器中运行许多活动和练习。

我们已尽力支持所有活动和练习的交互式版本,但我们仍然推荐进行本地安装,以便在无法获得此支持的情况下使用。

如果你在安装过程中遇到任何问题或有任何疑问,请通过电子邮件联系我们:workshops@packt.com

第二章:1. 聚类介绍

概述

在数据中寻找洞察和价值是机器学习崛起时所承诺的雄心壮志。在机器学习中,有些方法是通过预测性方式深入理解密集信息,还有一些方法则是根据输入的变化来预测结果。在本章中,我们将了解监督学习和无监督学习是什么,以及它们如何应用于不同的使用场景。一旦你对无监督学习的应用领域有了更深入的理解,我们将逐步介绍一些能快速提供价值的基础技术。

到本章结束时,你将能够使用内置的 Python 包实现 k-means 聚类算法,并计算轮廓系数。

介绍

你有没有被要求查看一些数据,但最终一无所获?也许你对数据集不熟悉,或者甚至不知道从哪里开始。这可能让你感到非常沮丧,甚至尴尬,尤其是如果是别人要求你负责这项任务的话。

你不是孤单的,有趣的是,数据本身有时也太混乱,无法理清。当你试图弄清楚电子表格中那些数字的意义时,你很可能是在模仿许多无监督算法的做法,它们试图从数据中找到意义。现实情况是,许多未经处理的真实世界数据集可能没有任何有用的见解。一个值得考虑的例子是,现如今,个人每天都会生成大量的细粒度数据——无论是他们在网站上的行为、购买历史,还是他们手机上使用的应用程序。如果你仅仅从表面上查看这些信息,它会是一团大杂烩,完全没有任何明确的意义。但别担心;本书将帮助你准备好应对这些繁重的任务,让你在处理数据探索任务时不再感到沮丧,无论数据有多庞大。

本书中,我们开发了一些最佳的内容,帮助你理解无监督算法如何工作以及如何使用它们。我们将涵盖如何在数据中找到聚类的基础知识,如何减少数据的大小以便更容易理解,以及无监督学习的这些方面如何应用于现实世界。我们希望你能从本书中收获关于无监督学习的扎实实际理解,了解它能解决的问题以及无法解决的问题。

无监督学习与监督学习

无监督学习是帮助在杂乱数据中寻找模式的领域,是当前机器学习中最令人兴奋的一个发展方向。如果你曾经研究过机器学习的书籍,你大概熟悉问题通常会分为监督学习或无监督学习这两种类型。监督学习涵盖了有标记数据集的问题集,数据集可以用于对数据进行分类(例如,在分析肺部健康数据集时预测吸烟者和非吸烟者)或在明确定义的数据中寻找模式(例如,根据房屋的卧室数量预测房屋的售价)。该模型最接近人类直观的学习方式。

例如,如果你想学习如何在基本的烹饪知识下避免烧焦食物,你可以通过将食物放在炉灶上并观察食物烧焦所需的时间(输入),来构建一个数据集(输出)。随着你不断地烧焦食物,你将建立一个心理模型,了解何时会发生烧焦并学会如何避免将来再发生。监督学习的发展曾经是快速且有价值的,但近年来已经逐渐平稳。许多关于了解数据的障碍已经得到解决,并在以下图片中列出:

图 1.1:无监督学习和监督学习的区别

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_01_01.jpg)

图 1.1:无监督学习和监督学习的区别

相反,无监督学习涵盖了拥有大量未标记数据的问题集。在这种情况下,标记数据是指具有给定“目标”结果的数据,你需要通过提供的数据找出与之相关的关系。例如,在前面的例子中,你知道你的“目标结果”是食物是否烧焦;这就是标记数据的一个例子。未标记数据则是你不知道“目标”结果是什么,只提供了输入数据。

基于前面的例子,假设你被放到地球上,完全不知道烹饪是怎么回事。你被给了 100 天时间,一个炉灶,以及一冰箱满满的食物,却没有任何指导说明该做什么。你初次探索厨房的过程可能会走向无数个方向。第 10 天,你可能终于学会如何打开冰箱;第 30 天,你可能学会了食物可以放到炉灶上;而经过更多的时间,你可能会不小心做出一顿可以食用的饭菜。正如你所看到的,试图在没有足够信息结构的厨房里找到意义,往往会导致产生非常杂乱无序的数据,这些数据对于实际做饭完全没有用处。

无监督学习可能是解决此问题的答案。回顾你的 100 天数据,你可以使用聚类来找出各天之间相似属性的模式,并推断哪些食物相似,可能会导致一顿“好”饭。然而,无监督学习并不是一种神奇的解决方案。仅仅找到聚类,同样可能帮助你发现一些相似却最终没有用的数据。以烹饪为例,我们可以通过“第三变量”的概念来说明这一缺点。仅仅因为你有一个很好的菜谱聚类,并不意味着它们是无懈可击的。在你的研究过程中,你可能发现一个统一的因素,所有的好饭菜都是在炉子上做的。这并不意味着每一顿在炉子上做的饭菜都会是好饭菜,而且你不能轻易地把这个结论套用到所有未来的场景中。

这正是使无监督学习如此令人兴奋的挑战。我们如何找到更智能的技术,加速找到对最终目标有益的信息聚类的过程?以下章节将帮助我们回答这个问题。

聚类

聚类是一个涉及在数据集中找到相似数据组的总体过程,如果你试图发现数据的潜在含义,它可以非常有价值。如果你是一个商店老板,想要了解哪些顾客更有价值,但又没有确切的价值定义,聚类是一个很好的起点,可以帮助你在数据中找出模式。你可能有一些关于什么是有价值顾客的高层次想法,但在面对大量可用数据时,你并不完全确定。通过聚类,你可以发现数据中相似群体之间的共性。例如,如果你深入观察一个相似群体的聚类,你可能会发现这个群体的每个人在你的网站上停留的时间都比其他人长。这可以帮助你了解什么是价值,并为未来的监督学习实验提供干净的样本数据。

确定聚类

以下图像显示了两个散点图:

图 1.2:两个不同的散点图

图 1.2:两个不同的散点图

以下图像将这两个散点图分成了两个不同的聚类:

图 1.3:清晰显示提供的数据集中存在聚类的散点图

图 1.3:清晰显示提供的数据集中存在聚类的散点图

图 1.2图 1.3 显示了从两个不同位置的高斯分布中随机生成的数对(x 和 y 坐标)。仅通过看第一张图,应该很容易看出数据中存在的聚类;但在现实中,情况绝不会如此简单。现在你已经知道数据可以清晰地分成两个聚类,你可以开始理解这两个群体之间存在的差异。

从无监督学习在更大机器学习环境中的定位开始,让我们先了解聚类的基本构建块。最基本的定义将聚类视为一个较大数据集的子集,即相似数据的分组。举个例子,假设你有一个房间,里面有 10 个人,每个人的工作要么是在金融领域,要么是科学家。如果你让所有金融工作者站到一起,所有科学家也站在一起,你就已经根据职业类型有效地形成了两个聚类。寻找聚类可以在识别相似的项时非常有价值,而在另一端,它也能帮助识别出彼此间有较大差异的项。

二维数据

为了理解这一点,假设你从雇主那里获得了一个简单的 1,000 行数据集,包含两列数值数据,如下所示:

图 1.4:二维原始数据在数组中的表示

图 1.4:二维原始数据在数组中的表示

初看之下,这个数据集没有提供任何实际的结构或理解。

数据集中的维度是另一种简单的方式,用来计数可用特征的数量。在大多数有组织的数据表中,你可以通过列数来查看特征的数量。因此,使用一个 1,000 行的数据集示例,大小为(1,000 x 2),你将有 1,000 个观测值,跨越两个维度。请注意,数据集的维度不应与数组的维度混淆。

你可以通过将第一列与第二列进行绘图,以便更好地了解数据结构。会有很多时候,组之间差异的原因可能显得微不足道;然而,能够采取行动的那些差异会极具回报。

练习 1.01:识别数据中的聚类

你会得到两个维度的数据图表,怀疑这些数据可能存在相似的聚类。请查看练习中提供的二维图表,并识别数据点的分组,目的是强调机器学习的重要性。在不使用任何算法方法的情况下,识别这些聚类在数据中的位置。

这个练习将帮助你建立直觉,了解我们如何使用自己的眼睛和思维过程来识别聚类。在完成这个练习时,思考为什么一组数据点应该被视为一个聚类,而另一组数据点则不应被视为聚类。按照以下步骤完成这个练习:

  1. 在以下散点图中识别聚类:图 1.5:二维散点图

    图 1.5:二维散点图

    聚类如下:

    图 1.6:散点图中的聚类

    图 1.6:散点图中的聚类

  2. 在以下散点图中识别聚类:图 1.7:二维散点图

    图 1.7:二维散点图

    聚类如下:

    图 1.8:散点图中的聚类

    图 1.8:散点图中的聚类

  3. 在下图中识别聚类:图 1.9:二维散点图

图 1.9:二维散点图

聚类如下:

图 1.10:散点图中的聚类

图 1.10:散点图中的聚类

这些例子大多数对你来说应该非常容易理解,这正是重点。人类的大脑和眼睛在发现现实世界中的模式方面非常出色。在看到每个图表的毫秒内,你就能分辨出哪些是匹配的,哪些不是。虽然对你来说很容易,但计算机并不具备像我们一样查看和处理图表的能力。

然而,这并不总是坏事。回顾前面的散点图。你能仅通过查看该图就找到数据中的六个离散聚类吗?你可能只找到了三到四个聚类,而计算机能够看到所有六个。人类的大脑非常强大,但它也缺乏基于严格逻辑的方法所带来的细节。通过算法聚类,你将学会如何建立一个比人类更擅长完成这些任务的模型。

我们将在下一节中介绍聚类算法。

k-means 聚类简介

希望到现在为止,你能看到在机器学习工作流中,寻找聚类是非常有价值的。但是,你如何实际找到这些聚类呢?一种最基本但又非常流行的方法是使用一种叫做 k-means 聚类 的聚类分析技术。k-means 聚类通过在数据中寻找 k 个聚类来工作,这个工作流实际上非常直观。我们将从无数学推导的 k-means 介绍开始,然后进行 Python 实现。聚类成员是指在算法处理数据时,点被分配到哪里。你可以将其看作是为一支运动队挑选队员,其中所有队员都在一个池中,但每一次运行后,队员会被分配到某个队伍(在这里是某个聚类)。

无数学推导的 k-means 算法演示

无数学推导的 k-means 聚类算法非常简单:

  1. 首先,我们会选择“k”个质心,其中“k”是我们预期的聚类数量。k 的值由我们选择,决定了我们得到的聚类类型。

  2. 然后,我们会将“k”个质心随机放置在现有的训练数据中。

  3. 接下来,将计算每个质心到训练数据中所有点的距离。我们稍后会详细讲解距离函数,但现在,我们先把它当作是点与点之间的距离。

  4. 现在,所有的训练点将与其最近的质心分组。

  5. 将分组的训练点与各自的质心分开,计算该组中的均值数据点,并将之前的质心移动到均值位置。

  6. 这个过程需要重复,直到收敛或达到最大迭代次数。

就是这样。下面的图像表示原始原始数据:

图 1.11:原始原始数据在 x 和 y 坐标上的图示

图 1.11:原始原始数据在 x 和 y 坐标上的图示

根据前面图像中的原始数据,我们可以通过展示每一步的预测聚类来可视化 k-means 的迭代过程:

图 1.12:从左到右读取,红色点是随机初始化的质心,并将最近的数据点分配给每个质心的分组

图 1.12:从左到右读取,红色点是随机初始化的质心,并将最近的数据点分配给每个质心的分组

K-means 聚类深入讲解

为了更深入地理解 k-means,我们再通过引言中提供的示例,并结合一些支持 k-means 的数学原理进行讲解。支撑这个算法的最重要的数学公式是距离函数。距离函数基本上是任何能够定量地表示一个物体距离另一个物体有多远的公式,其中最常用的是欧几里得距离公式。这个公式通过相减每个点的相应组件并平方以去除负值,然后将结果的距离加起来并开平方:

图 1.13:欧几里得距离公式

图 1.13:欧几里得距离公式

如果你注意到,前面的公式适用于只有二维数据点(坐标数)的情况。表示高维数据点的一般方式如下:

图 1.14:高维点的欧几里得距离公式

图 1.14:高维点的欧几里得距离公式

让我们来看一下在高维空间中计算两点 pq 之间欧几里得距离时涉及的术语。这里,n 是这两个点的维度数。我们计算点 pq 的相应组件之间的差异(pi 和 qi 分别是点 pq 的第 i 组件),并将每个差值平方。然后,将所有 n 维度的平方差值相加,再对这个和开平方。这个值表示点 pq 之间的欧几里得距离。如果你将 n = 2 代入前面的公式,它将分解为图 1.13 中表示的公式。

现在再次回到我们关于 k-means 的讨论。质心在开始时会随机设置为你 n 维空间中的点。这些质心中的每一个都作为 (a, b) 输入到前面的公式中,而空间中的一个点则作为 (x, y) 输入。然后计算每个点与每个质心坐标之间的距离,选择距离最短的质心作为该点的分组。

举个例子,我们选择三个随机质心,一个任意点,并使用欧几里得距离公式,计算从每个点到质心的距离:

  • 随机质心:[ (2,5), (8,3), (4,5) ]。

  • 任意点 x:(0, 8)。

  • 从点到每个质心的距离:[ 3.61, 9.43, 5.00 ]。

由于任意点 x 最接近第一个质心,它将被分配给第一个质心。

曼哈顿距离:替代距离度量

欧几里得距离是许多机器学习应用中最常用的距离度量,并且通常被俗称为距离度量;然而,它并不是唯一的,甚至在某些情况下也不是最佳的距离度量。另一个流行的可以用于聚类的距离度量是曼哈顿距离

曼哈顿距离之所以叫这个名字,是因为它类似于在拥有许多方形街区的大城市(例如纽约市)中旅行的概念。欧几里得距离由于基于勾股定理而依赖于对角线,而曼哈顿距离则将距离限制为仅在直角之间。曼哈顿距离的公式如下:

图 1.15:曼哈顿距离公式

图 1.15:曼哈顿距离公式

这里 pi 和 qi 分别是点 pq 的第 i 个分量。基于我们之前讨论的欧几里得距离的例子,如果我们想找到两个点之间的距离,假设这两个点分别是 (1,2) 和 (2,3),那么曼哈顿距离将等于 |1-2| + |2-3| = 1 + 1 = 2。这种功能可以扩展到任何维度。在实践中,当处理高维数据时,曼哈顿距离可能会优于欧几里得距离。

更高维度

当数据只有二维时,前面的示例可以清晰地可视化。这是为了方便起见,帮助阐明 k-means 的工作原理,同时可能会导致你误解聚类的容易程度。在你自己的许多应用中,数据可能会大得多,以至于无法通过可视化来感知(超过三维的数据对于人类来说是不可感知的)。在前面的示例中,你可以通过脑中计算几个二维的线条来将数据分成不同的组。在更高维度的情况下,你需要借助计算机来找到一个 n 维超平面,合理地分隔数据集。实际上,这就是 k-means 等聚类方法提供显著价值的地方。下图展示了二维、三维和 n 维的图形:

图 1.16:二维、三维和 n 维图形

图 1.16:二维、三维和 n 维图形

在下一个练习中,我们将计算欧几里得距离。我们将通过使用 NumPyMath Python 包来构建工具集。NumPy 是一个用于 Python 的科学计算包,它将常见的数学函数预先打包为高度优化的格式。

顾名思义,Math 包是一个基础库,它使实现基础数学构件(如指数和平方根)变得更加容易。通过使用像 NumPyMath 这样的包,我们可以减少从头开始创建自定义数学函数的时间,而是专注于开发解决方案。你将在接下来的练习中看到如何实际使用这些包。

练习 1.02:在 Python 中计算欧几里得距离

在这个练习中,我们将创建一个示例点,以及三个示例质心,帮助说明欧几里得距离的工作原理。理解这个距离公式是我们在聚类中工作的基础。

执行以下步骤以完成此练习:

  1. 打开 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))
    \ ) to split the logic across multiple lines. When the code is executed, Python will ignore the backslash, and treat the code on the next line as a direct continuation of the current line.
    

    这种方法被认为是幼稚的,因为它对数据点进行逐元素计算(较慢),与使用向量和矩阵数学的更现实的实现相比,后者可以显著提高性能。

  2. 在 Python 中创建数据点,如下所示:

    centroids = [ (2, 5), (8, 3), (4,5) ]
    x = (0, 8)
    
  3. 使用你创建的公式来计算 步骤 1 中的欧几里得距离:

    # Calculating Euclidean Distance between x and centroid
    centroid_distances =[]
    for centroid in centroids:
        print("Euclidean Distance between x {} and centroid {} is {}"\
              .format(x ,centroid, dist(x,centroid)))
        centroid_distances.append(dist(x,centroid))
    Euclidean Distance between x (0, 8) and centroid (2, 5) 
    is 3.605551275463989
    Euclidean Distance between x (0, 8) and centroid (8, 3) 
    is 9.433981132056603
    Euclidean Distance between x (0, 8) and centroid (4, 5) is 5.0
    

    我们的点 x 与质心之间的最短距离是 3.61,相当于 (0, 8)(2, 5) 之间的距离。由于这是最小距离,我们的示例点 x 将被分配到第一个质心所在的组。

在这个例子中,我们的公式应用于一个单独的点 x (0, 8)。在这个单点之后,相同的过程将对数据集中的每个剩余点重复,直到每个点都被分配到一个簇。每分配一个点后,都会计算该簇内所有点的均值。计算这些点的均值与计算单个整数的均值是一样的。

虽然这个示例中只有一个点,但通过完成这个过程,你实际上已经使用欧几里得距离将一个点分配到了它的第一个簇。我们将在下一个练习中使用多个点来扩展这个方法。

注意

要访问该特定部分的源代码,请参考packt.live/2VUvCuz

你也可以在线运行这个示例,网址是packt.live/3ebDwpZ

练习 1.03:使用距离的概念形成簇

对我们人类的大脑来说,看到图中的点组并判断哪些点属于不同的簇是非常直观的。然而,如何让计算机重复执行这一任务呢?在这个练习中,你将帮助计算机学习如何使用“距离”的概念来形成自己的簇。我们将在下一个练习中进一步探讨如何使用这些距离度量:

  1. 创建一个点的列表,[ (0,8), (3,8), (3,4) ],它们被分配到第一个簇:

    cluster_1_points =[ (0,8), (3,8), (3,4) ]
    
  2. 要在点集列表中找到新的质心,计算所有点的平均点。均值的计算适用于无限个点,你只需将每个位置的整数相加并除以点的总数。例如,如果你的两个点是(0,1,2)和(3,4,5),那么均值计算为[ (0+3)/2, (1+4)/2, (2+5)/2 ]:

    mean =[ (0+3+3)/3, (8+8+4)/3 ]
    print(mean)
    

    输出结果如下:

    [2.0, 6.666666666666667]
    

    计算出新的质心后,重复我们在练习 1.02中看到的簇成员计算,在 Python 中计算欧几里得距离,然后重复前面两个步骤以找到新的簇质心。最终,新的簇质心将与簇成员计算之前的质心相同,练习将完成。这个过程重复多少次取决于你正在聚类的数据。

    一旦你将质心位置移动到新的均值点(2, 6.67),你可以将其与最初输入问题时的质心列表进行比较。如果新的均值点与当前列表中的质心不同,你将需要再次执行前面两个练习的迭代过程。一旦你计算出的新均值点与开始时的质心相同,就意味着你完成了一次 k-means 运行,并达到了一个叫做收敛的点。然而,在实际操作中,达到收敛所需的迭代次数可能非常大,这种大规模计算在实践中可能不可行。在这种情况下,我们需要设定一个最大迭代次数限制。一旦达到这个迭代限制,我们就停止进一步处理。

    注意

    要获取本节的源代码,请参考packt.live/3iJ3JiT

    你还可以在线运行此示例,访问链接:packt.live/38CCpOG

在下一个练习中,我们将从头实现 k-means。为此,我们将开始使用 Python 生态系统中的常见包,这些包将作为你职业生涯中其他部分的构建模块。最受欢迎的机器学习库之一是 scikit-learn(scikit-learn.org/stable/user_guide.html),它包含许多内置的算法和函数,支持你理解算法的工作原理。我们还将使用 SciPy(docs.scipy.org/doc/scipy/reference/)中的函数,它是一个类似 NumPy 的包,抽象出基本的科学数学函数,从而实现更高效的部署。最后,下一个练习将介绍matplotlibmatplotlib.org/3.1.1/contents.html),这是一个绘图库,可以创建你所处理数据的图形表示。

练习 1.04:从头实现 K-means – 第一部分:数据生成

接下来的两个练习将专注于生成练习数据和从头实现 k-means 算法。这个练习依赖于 scikit-learn,一个开源的 Python 包,能够快速原型化流行的机器学习模型。在 scikit-learn 中,我们将使用datasets功能来创建一个合成的 blob 数据集。除了利用 scikit-learn 的强大功能,我们还将依赖于 Matplotlib,这是一个流行的 Python 绘图库,使我们能够轻松地可视化数据。为此,按照以下步骤操作:

  1. 导入必要的库:

    from sklearn.datasets import make_blobs
    from sklearn.cluster import KMeans
    import matplotlib.pyplot as plt
    import numpy as np
    import math
    np.random.seed(0)
    %matplotlib inline
    

    注意

    你可以在KMeans库的官方文档中找到更多细节:scikit-learn.org/stable/modules/clustering.html#k-means

  2. 生成一个随机的聚类数据集来进行实验,X = 坐标点,y = 聚类标签,并定义随机的质心。我们将使用从sklearn.datasets导入的make_blobs函数来实现,正如其名字所示,它生成数据点的簇。

    X, y = make_blobs(n_samples=1500, centers=3, \
                      n_features=2, random_state=800)
    centroids = [[-6,2],[3,-4],[-5,10]]
    

    这里,n_samples参数决定了由数据点簇生成的总数据点数量。centers参数决定了数据簇的质心数量。n_feature属性定义了数据集生成的维度数量。这里,数据将是二维的。

    为了在所有迭代中生成相同的数据点(这些数据点是随机生成的),以保证结果的可复现性,我们将random_state参数设置为800random_state参数的不同值会产生不同的结果。如果不设置random_state参数,每次执行时都会获得不同的结果。

  3. 打印数据:

    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]])
    
  4. 使用从matplotlib.pyplot导入的散点图功能绘制坐标点。此函数接受坐标点的输入列表,并将其图形化展示,以便更容易理解。如果您想更深入地了解该函数提供的参数,请查阅matplotlib文档:

    plt.scatter(X[:, 0], X[:, 1], s=50, cmap='tab20b')
    plt.show()
    

    绘图如下所示:

    图 1.17:坐标图

    图 1.17:坐标图

  5. 打印y数组,该数组是由 scikit-learn 提供的标签,并作为比较的基准真值。

    y
    

    输出如下:

    array([2, 2, 1, ..., 1, 0, 2])
    
  6. 使用正确的聚类标签绘制坐标点:

    plt.scatter(X[:, 0], X[:, 1], c=y,s=50, cmap='tab20b')
    plt.show()
    

    绘图如下所示:

    图 1.18:带有正确聚类标签的坐标图

图 1.18:带有正确聚类标签的坐标图

通过完成前面的步骤,您已经生成了数据,并通过可视化了解了数据的构成。通过可视化基准真值,您为算法准确性提供了一个相对度量的基准。

注意

要访问该特定部分的源代码,请参考packt.live/2JM8Q1S

您还可以在packt.live/3ecjKdT在线运行此示例。

拥有数据后,在接下来的练习中,我们将继续构建无监督学习工具集,使用从SciPy包中优化过的欧几里得距离函数cdist。您将比较一个非向量化的、易于理解的版本和cdist,后者经过特别优化,以实现最佳性能。

练习 1.05:从头开始实现 K-means 算法——第二部分:实现 K-means

让我们自己重现这些结果。我们将通过一个示例进行讲解,并进行一些优化。

注意

本练习是上一练习的延续,应在同一个 Jupyter notebook 中进行。

本次练习,我们将依赖于 SciPy,这是一个 Python 包,允许轻松访问高效优化的科学计算版本。特别地,我们将使用 cdist 实现欧几里得距离,其功能在更高效的方式下复制了我们度量距离的基本实现。请按照以下步骤完成此练习:

  1. 本练习的基础将是将一个基本的欧几里得距离实现与 SciPy 提供的优化版本进行比较。首先,导入优化的欧几里得距离参考:

    from scipy.spatial.distance import cdist
    
  2. 确定您想要探索的 X 子集。对于本例,我们仅选择了五个点以使讲解更清晰;然而,这种方法适用于任意数量的点。我们选择了点 105-109(包括 105 和 109):

    X[105:110]
    

    输出结果如下:

    array([[-3.09897933,  4.79407445],
           [-3.37295914, -7.36901393],
           [-3.372895  ,  5.10433846],
           [-5.90267987, -3.28352194],
           [-3.52067739,  7.7841276 ]])
    
  3. 计算距离并选择最短距离的索引作为一个簇:

    """
    Finds distances from each of 5 sampled points to all of the centroids
    """
    for x in X[105:110]:
        calcs = cdist(x.reshape([1,-1]),centroids).squeeze()
        print(calcs, "Cluster Membership: ", np.argmin(calcs))
    [4.027750355981394, 10.70202290628413, 5.542160268055164] 
     Cluster Membership:  0
    [9.73035280174993, 7.208665829113462, 17.44505393393603] 
     Cluster Membership:  1
    [4.066767506545852, 11.113179986633003, 5.1589701124301515] 
     Cluster Membership:  0
    [5.284418164665783, 8.931464028407861, 13.314157359115697] 
     Cluster Membership:  0
    [6.293105164930943, 13.467921029846712, 2.664298385076878] 
     Cluster Membership:  2
    
  4. 如下所示定义 k_means 函数并随机初始化 k-中心。重复此过程,直到新旧 centroids 之间的差异为 0,使用 while 循环:

    Exercise1.04-Exercise1.05.ipynb
    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)
    The complete code for this step can be found at https://packt.live/2JM8Q1S.
    

    注意

    请不要中断此代码,因为这可能会导致错误。

  5. 将历史步骤的中心和它们的标签捆绑在一起:

    history = zip(centers_hist, labels_hist)
    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.19:第一个散点图

图 1.19:第一个散点图

第二个图如下所示:

图 1.20:第二个散点图

图 1.20:第二个散点图

第三个图如下所示:

图 1.21:第三个散点图

图 1.21:第三个散点图

第四个图如下所示:

图 1.22:第四个散点图

图 1.22:第四个散点图

第五个图如下所示:

图 1.23:第五个散点图

图 1.23:第五个散点图

如前面图片所示,k-means 使用迭代方法根据距离优化集群。该算法从随机初始化中心开始,并根据数据的复杂度,迅速找到最合理的分割。

注意

要访问此特定部分的源代码,请参考 packt.live/2JM8Q1S

您也可以在线运行此示例,网址是 packt.live/3ecjKdT

聚类性能 – 轮廓分数

理解无监督学习方法的表现本质上比监督学习方法要困难得多,因为没有可用的真实标签。对于监督学习,有许多强健的性能指标,其中最直接的就是通过比较模型预测的标签与实际标签,看看模型预测对了多少个。不幸的是,对于聚类,我们没有标签可依赖,需要建立对我们的簇有多“不同”的理解。我们通过轮廓得分指标来实现这一点。我们还可以使用轮廓得分来找到无监督学习方法的最佳“K”值。

轮廓指标通过分析一个点在其簇内的拟合程度来工作。该指标的范围从-1 到 1。如果你的聚类的平均轮廓得分为 1,那么你将获得完美的簇,并且对于每个点属于哪个簇几乎没有任何混淆。在前面的练习中的图形中,轮廓得分将接近 1,因为这些簇紧密聚集,并且每个簇之间有相当大的距离。然而,这种情况非常罕见;轮廓得分应该被视为尽力而为的一个尝试,因为得到 1 是非常不可能的。如果轮廓得分为正,意味着该点离其分配的簇比离邻近的簇更近。如果轮廓得分为 0,则表示该点位于分配的簇和下一个最接近簇之间的边界上。如果轮廓得分为负,则表示该点被分配到了错误的簇,实际上该点可能属于邻近的簇。

从数学上讲,轮廓得分的计算相当简单,可以通过简化轮廓指数SSI)来获得:

SSIi = bi - ai/ max(ai, bi)

这里的ai 是点i到其自身簇质心的距离,bi 是点i到最近簇质心的距离。

这里捕捉到的直觉是,ai 表示点i所在簇的凝聚力,即它作为一个清晰簇的程度,而 bi 表示各簇之间的距离。我们将在活动 1.01中使用 scikit-learn 中silhouette_score的优化实现,实现 k-means 聚类。使用这个方法很简单,只需要传入特征数组和来自 k-means 聚类方法的预测簇标签。

在下一个练习中,我们将使用pandas库(pandas.pydata.org/pandas-docs/stable/)来读取 CSV 文件。Pandas 是一个 Python 库,通过 DataFrame 简化数据处理。如果回顾您使用 NumPy 构建的数组,您可能会注意到,最终的数据结构相当笨重。为了从数据中提取子集,您需要使用方括号和特定的行号进行索引。与这种方法不同,pandas 提供了一种更易于理解的数据操作方法,使得将数据移到适合无监督学习及其他机器学习技术所需的格式变得更加简单。

注意

要在 Python 中读取数据,您将使用variable_name = pd.read_csv('file_name.csv', header=None)

这里,参数header = None明确表示没有列名。如果您的文件包含列名,则保留这些默认值。此外,如果您的文件包含列名,但您指定了header = None,Pandas 将把包含列名的行当作数据行处理。

练习 1.06:计算轮廓系数

在这个练习中,我们将计算一个固定聚类数的数据集的轮廓系数。为此,我们将使用种子数据集,数据集可在packt.live/2UQA79z获得。以下说明提供了关于此数据集的更多信息,并将在下一个活动中进行进一步探索。为了完成此练习,请忽略该数据集具体包含哪些内容,因为学习轮廓系数更为重要。在下一个活动中,您将根据需要获得更多的背景知识,以创建智能的机器学习系统。按照以下步骤完成此练习:

注意

此数据集来自archive.ics.uci.edu/ml/datasets/seeds。它可以通过packt.live/2UQA79z访问。

引用:贡献者衷心感谢波兰科学院农业物理研究所(位于卢布林)对他们工作的支持。

  1. 使用 pandas 加载种子数据文件,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
    np.random.seed(0)
    seeds = pd.read_csv('Seed_Data.csv')
    
  2. 分离X特征,因为我们希望将其视为无监督学习问题:

    X = seeds[['A','P','C','LK','WK','A_Coef','LKG']]
    
  3. 带回我们之前制作的k_means函数供参考:

    Exercise 1.06.ipynb
    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)
    The complete code for this step can be found at https://packt.live/2UOqW9H.
    
  4. 将我们的种子X特征 DataFrame 转换为NumPy矩阵:

    X_mat = X.values
    
  5. 在种子矩阵上运行我们的k_means函数:

    centroids, labels, centroids_history, labels_history = \
    k_means(X_mat, 3)
    
  6. 计算Area ('A')Length of Kernel ('LK')列的轮廓系数:

    silhouette_score(X[['A','LK']], labels)
    

    输出应该类似于以下内容:

    0.5875704550892767
    

在这个练习中,我们计算了Area ('A')Length of Kernel ('LK')列的轮廓系数。我们将在下一个活动中使用这一技术来确定我们 k-means 聚类算法的性能。

注意

要访问此特定部分的源代码,请参考packt.live/2UOqW9H

您还可以在packt.live/3fbtJ4y在线运行此示例。

活动 1.01:实现 k-means 聚类

您正在从头实现一个 k-means 聚类算法,以证明您理解其工作原理。您将使用 UCI ML 库提供的种子数据集。种子数据集在数据科学界是经典之作,包含了小麦种子特征,用于预测三种不同类型的小麦品种。下载位置将在本活动中后续提供。

在本活动中,您应该使用 Matplotlib、NumPy、scikit-learn 指标和 pandas。

通过轻松加载和重塑数据,您可以将更多精力集中在学习 k-means 上,而不是编写数据加载器功能。

提供以下种子数据特征供参考:

1\. area (A), 
2\. perimeter (P)
3\. compactness (C) 
4\. length of kernel (LK)
5\. width of kernel (WK)
6\. asymmetry coefficient (A_Coef)
7\. length of kernel groove (LKG)

这里的目标是深入理解 k-means 是如何工作的。为此,您需要将前面章节中学到的知识付诸实践,在 Python 中从头实现 k-means。

请打开您喜欢的编辑平台,尝试以下步骤:

  1. 使用NumPymath包和欧几里得距离公式,编写一个函数,计算两个坐标之间的距离。

  2. 编写一个函数,计算数据集中每个点到质心的距离,并返回聚类成员关系。

  3. 编写一个 k-means 函数,该函数接受数据集和聚类数(K)作为输入,返回最终的聚类质心以及组成该聚类的成员数据点。在从头实现 k-means 之后,将您的自定义算法应用于种子数据集,数据集位于:packt.live/2Xh2FdS

    注意

    本数据集来源于archive.ics.uci.edu/ml/datasets/seeds,可以通过packt.live/2Xh2FdS访问。

    UCI 机器学习库 [archive.ics.uci.edu/ml]。加利福尼亚州尔湾:加利福尼亚大学信息与计算机科学学院。

    引用:贡献者衷心感谢波兰科学院农业物理研究所对其工作的支持。

  4. 移除此数据集中提供的类别,并查看您的 k-means 算法是否能仅凭植物特征将不同的小麦品种分组到正确的类别中!

  5. 使用 scikit-learn 实现计算轮廓系数。

完成本练习后,你已获得了在真实世界数据集上调优 k-means 聚类算法的实践经验。种子数据集被视为数据科学领域中的经典“hello world”问题,有助于测试基础技术。你的最终聚类算法应能有效地识别数据中存在的三种小麦物种类型,如下所示:

图 1.24:预期的三种小麦物种聚类图

图 1.24:预期的三种小麦物种聚类图

注意

本活动的解决方案可以在第 418 页找到。

总结

在本章中,我们探讨了什么是聚类及其在各种数据挑战中的重要性。在掌握聚类知识的基础上,你实现了 k-means,这是最简单但也是最受欢迎的无监督学习方法之一。如果你已经阅读了本总结,并能逐步向朋友解释 k-means 的工作原理,那么你已经准备好进入更复杂的聚类形式。

接下来,我们将介绍层次聚类,在某种配置下,它重用了我们在 k-means 中使用的质心学习方法。我们将在下一章通过概述更多的聚类方法和技术,进一步发展这一方法。

第三章:2. 层次聚类

概述

在本章中,我们将使用常见的 Python 包从头实现层次聚类算法,并进行凝聚层次聚类。我们还将比较 k-means 和层次聚类。我们将利用层次聚类构建更强的、更有逻辑性的分组。在本章结束时,我们将能够使用层次聚类构建更强的、更有逻辑性的分组。

引言

在本章中,我们将在第1 章聚类简介的基础概念上展开,通过相似度的概念来围绕聚类展开。我们将再次实现欧几里得距离的不同形式,以捕捉相似性的概念。需要牢记的是,欧几里得距离只是最常用的距离度量之一,并非唯一的度量。通过这些距离度量,我们将在前一章探索的简单邻居计算基础上引入层次结构的概念。通过使用层次结构来传达聚类信息,我们可以构建更强的、更有逻辑性的分组。与 k-means 类似,层次聚类对于客户细分或识别相似产品类型等场景非常有帮助。然而,层次聚类的一个小优势是,它能够以更清晰的方式解释结果。在本章中,我们将概述一些层次聚类可能是你所需要的解决方案的情况。

聚类复习

第一章聚类简介,讲解了最基本的聚类算法之一:k-means 的高层概念和深入细节。尽管它确实是一个简单的方法,但不要小看它;它将成为你继续探索无监督学习世界时非常有价值的工具。在许多实际应用场景中,公司通过最简单的方法,如 k-means 或线性回归(用于有监督学习),获得了宝贵的发现。一个例子是评估大量的客户数据——如果直接在表格中评估这些数据,通常难以发现有价值的信息。然而,即使是一个简单的聚类算法,也能够识别数据中哪些组是相似的,哪些是不同的。为了复习一下,我们来快速回顾一下什么是聚类,以及 k-means 如何找到这些聚类:

图 2.1:区分有监督与无监督问题的属性

图 2.1:区分有监督与无监督问题的属性

如果你得到了一组随机的数据,没有任何指导方针,你可能会用基本的统计方法开始探索——例如,计算每个特征的均值、中位数和众数。给定一个数据集,选择监督学习或无监督学习作为推导洞察的方法,取决于你为自己设定的数据目标。如果你确定其中一个特征实际上是标签,并且你想要查看数据集中的其他特征如何影响它,那么这将成为一个监督学习问题。然而,如果在初步探索后,你意识到你拥有的数据仅仅是一组没有目标的特征(例如一组健康指标、网上商店的购买发票等),那么你可以通过无监督方法进行分析。

无监督学习的经典例子是通过分析来自网上商店的一组发票,找到类似客户的聚类。你的假设是,通过找到最相似的人群,你可以创建更具针对性的营销活动,吸引每个聚类的兴趣。实现这些相似用户聚类的一种方法是使用 k-means。

k-means 复习

k-means 聚类通过找到数据中“k”个聚类来工作,采用某些距离计算方法,如欧几里得距离、曼哈顿距离、汉明距离、明科夫斯基距离等。“K”个点(也称为质心)在数据中随机初始化,然后计算每个数据点到每个质心的距离。这些距离中的最小值决定了某个数据点属于哪个聚类。每个点被分配到一个聚类后,计算该聚类内的数据点的均值作为新的质心。这个过程会重复进行,直到新计算出的聚类质心的位置不再变化,或者达到最大迭代次数。

层级结构的组织

无论是自然界还是人造世界,都有许多将系统组织成层级结构的例子,并且大多数情况下,这样做是非常有意义的。从这些层级结构中发展出来的一个常见表示法可以在基于树的结构中看到。想象你有一个父节点,下面有若干子节点,子节点可以进一步成为父节点。通过将信息组织成树形结构,你可以构建一个信息密集型的图表,清晰地显示事物之间与同类及其更大的抽象概念的关系。

从自然界中有一个例子可以帮助说明这个概念,即我们如何看待动物的层级结构,从父类到各个物种:

图 2.2:动物物种在层级树结构中的关系

图 2.2:动物物种在层级树结构中的关系

在前面的示意图中,你可以看到一个例子,展示了动物品种之间的关系信息是如何以既节省空间又能传达大量信息的方式进行映射的。这个示例既可以看作是一棵独立的树(展示了猫和狗的区别,但它们都是家养动物),也可以看作是一个更大树的一部分,展示了家养与非家养动物的分类。

作为一个面向商业的示例,让我们回到一个销售产品的在线商店的概念。如果你售卖种类繁多的产品,那么你可能会想为客户创建一个层级导航系统。通过避免一次性展示所有的产品目录信息,客户只会看到与他们兴趣相匹配的树形路径。下面的示意图展示了一个层级导航系统的示例:

图 2.3:层级树结构中的产品分类

图 2.3:层级树结构中的产品分类

显然,层级导航系统在提升客户体验方面的好处不容小觑。通过将信息组织成层级结构,你可以为数据构建一个直观的结构,展示明确的嵌套关系。如果这听起来像是另一种在数据中寻找簇的方法,那么你肯定走在了正确的道路上。通过使用类似的距离度量,例如 k-means 的欧氏距离,我们可以开发出一棵树,展示许多数据切割,让用户可以根据自己的需求主观地创建簇。

层次聚类简介

到目前为止,我们已经展示了层级结构如何成为组织信息的优秀方式,能够清晰地显示数据点之间的嵌套关系。虽然这有助于我们理解项目之间的父子关系,但在形成簇时也非常实用。以前一节中的动物示例为基础,假设你仅仅得到了两种动物的特征:它们的身高(从鼻尖到尾部的长度)和体重。使用这些信息,你需要重新创建一个层级结构,以便识别数据集中哪些记录对应的是狗和猫,以及它们相对的亚种。

由于你只给出了动物的身高和体重,无法推断每个物种的具体名称。然而,通过分析你所提供的特征,你可以在数据中构建一个结构,近似于数据中存在哪些动物物种。这为无监督学习问题奠定了基础,而层次聚类是解决此类问题的理想方法。在以下图表中,你可以看到我们在左侧创建的两个特征,左列为动物身高,右列为动物体重。然后,这些数据被绘制在一个二维坐标图上,X 轴为身高,Y 轴为体重:

图 2.4:一个包含动物身高的双特征数据集示例和动物体重

图 2.4:一个包含动物身高和动物体重的双特征数据集示例

一种处理层次聚类的方法是从每个数据点开始,将其视为自己的簇,并递归地将相似的点组合在一起形成簇——这就是聚合层次聚类。在聚合与分裂聚类章节中,我们将详细介绍不同的层次聚类方法。

在聚合层次聚类方法中,数据点相似度的概念可以借鉴我们在 k-means 中看到的范式。在 k-means 中,我们使用欧几里得距离来计算每个数据点到期望的“k”个簇的质心的距离。在层次聚类方法中,我们将重新使用相同的距离度量来确定数据集中记录之间的相似性。

最终,通过递归地将数据中的单个记录与其最相似的记录组合在一起,你将从底部向上构建一个层次结构。最终,单独的单一成员簇将合并成一个位于层次结构顶部的单一簇。

执行层次聚类的步骤

为了理解聚合层次聚类的工作原理,我们可以跟踪一个简单的示例程序,它是如何通过合并形成层次结构的:

  1. 给定 n 个样本数据点,将每个点视为一个单独的“簇”,其中只有该点作为成员(质心)。

  2. 计算数据集中所有簇的质心之间的成对欧几里得距离。(在此,簇之间的最小距离、最大距离、平均距离或两个质心之间的距离也可以考虑。在这个示例中,我们考虑的是两个簇质心之间的距离)。

  3. 将最接近的簇/点组合在一起。

  4. 重复步骤 2步骤 3,直到你得到一个包含所有数据的单一簇。

  5. 绘制树状图以展示数据如何在层次结构中聚合。树状图只是用于表示树状结构的图示,显示簇从上到下的排列。我们将在接下来的演示中详细讨论这如何有助于理解数据的结构。

  6. 决定你希望在哪个层级创建簇。

层次聚类的示例演示

尽管比 k-means 稍微复杂一些,层次聚类在逻辑上实际上与它非常相似。这里有一个简单的示例,稍微详细地介绍了前面的步骤:

  1. 给定四个样本数据点的列表,将每个点视为一个质心,它也是其自身的簇,点的索引从 0 到 3:

    Clusters (4): [ (1,7) ], [ (-5,9) ], [ (-9,4) ] , [ (4, -2) ]
    Centroids (4): [ (1,7) ], [ (-5,9) ], [ (-9,4) ] , [ (4, -2) ]
    
  2. 计算所有簇的质心之间的成对欧几里得距离。

    请参考第一章K-means 聚类深入演练部分,复习欧几里得距离的计算方法。

    图 2.5中显示的矩阵里,点的索引在水平方向和垂直方向上都是 0 到 3,表示各个点之间的距离。注意,数值在对角线两侧是镜像对称的——这是因为你在比较每个点与其他所有点的距离,所以只需要关注对角线一侧的数值:

    图 2.5:一个距离数组

    图 2.5:一个距离数组

  3. 将距离最短的点对组合在一起。

    在这种情况下,点 [1,7] 和 [-5,9] 因为距离最近而合并成一个簇,其余两个点作为单成员簇保留:

    图 2.6:一个距离数组

    [ [1,7], [-5,9] ]
    [-9,4]
    [4,-2] 
    
  4. 计算两个成员簇的点之间的均值,以找到新的质心:

    mean([ [1,7], [-5,9] ]) = [-2,8]
    
  5. 将质心添加到两个单成员质心中,并重新计算距离:

    Clusters (3): 
    [ [1,7], [-5,9] ]
    [-9,4]
    [4,-2] 
    

    质心(3):

    [-2,8]
    [-9,4]
    [4,-2]
    

    再次,我们将计算点与质心之间的欧几里得距离:

    图 2.7:一个距离数组

    图 2.7:一个距离数组

  6. 如前图所示,点 [-9,4] 是距离质心最近的点,因此它被添加到簇 1。现在,簇列表更新为以下内容:

    Clusters (2): 
    [ [1,7], [-5,9], [-9,4] ]
    [4,-2] 
    
  7. 只剩下点 [4,-2] 作为距离其邻近点最远的点,你可以将它直接加入簇 1 来统一所有簇:

    Clusters (1): 
    [ [ [1,7], [-5,9], [-9,4], [4,-2] ] ]
    
  8. 绘制一个树状图以展示数据点与簇之间的关系:图 2.8:显示数据点与簇之间关系的树状图

图 2.8:显示数据点与簇之间关系的树状图

树状图展示了数据点之间的相似性,并且与我们之前讨论的层次树结构非常相似。与任何可视化技术一样,它会丢失一些信息;然而,当确定你希望形成多少个聚类时,树状图是非常有帮助的。在前面的示例中,你可以在 X 轴上看到四个潜在的聚类,如果每个点都是一个独立的聚类。当你垂直查看时,可以看到哪些点距离最接近,可能会被归为同一聚类。例如,在前面的树状图中,索引 0 和 1 的点最接近,可以形成一个聚类,而索引 2 仍然是一个单独的聚类。

回顾前面的动物分类示例,其中涉及狗和猫物种,假设你看到了以下树状图:

图 2.9:动物分类树状图

图 2.9:动物分类树状图

如果你只对将物种数据集分为狗和猫感兴趣,你可以在第一个分组层级停止聚类。然而,如果你想将所有物种分为家养动物和非家养动物,你可以在第二个层级停止聚类。层次聚类和树状图的一个优点是,你可以看到潜在聚类的完整划分,以供选择。

练习 2.01:构建层级结构

让我们在 Python 中实现前述的层次聚类方法。通过概述直观的框架,我们现在可以使用 SciPy 提供的一些辅助函数来探索构建层次聚类的过程。SciPy(www.scipy.org/docs.html)是一个开源库,提供了对科学计算和技术计算有用的函数。它的示例包括线性代数和微积分相关方法的简单实现。在这个练习中,我们将专门使用 SciPycluster子模块的有用函数。除了 scipy,我们还将使用 matplotlib 来完成这个练习。按照以下步骤完成这个练习:

  1. 生成一些虚拟数据,如下所示:

    from scipy.cluster.hierarchy import linkage, dendrogram, fcluster
    from sklearn.datasets import make_blobs
    import matplotlib.pyplot as plt
    %matplotlib inline
    
  2. 生成一个随机的聚类数据集进行实验。X = 坐标点,y = 聚类标签(不需要):

    X, y = make_blobs(n_samples=1000, centers=8, \
                      n_features=2, random_state=800)
    
  3. 可视化数据,如下所示:

    plt.scatter(X[:,0], X[:,1])
    plt.show()
    

    输出如下:

    图 2.10:虚拟数据的绘图

    图 2.10:虚拟数据的绘图

    在绘制这个简单的玩具示例后,应该能清楚地看到我们的虚拟数据包含了八个聚类。

  4. 我们可以使用内置的SciPy包中的linkage轻松生成距离矩阵。稍后我们将深入了解linkage函数的原理;但是,目前了解有现成的工具可以计算点与点之间的距离是非常重要的:

    # Generate distance matrix with 'linkage' function
    distances = linkage(X, method="centroid", metric="euclidean")
    print(distances)
    

    输出如下:

    图 2.11:距离矩阵

    图 2.11:距离矩阵

    如果你尝试通过自动填写 linkage 函数的 method 超参数进行不同的方法实验,你会看到它们如何影响整体性能。链接方法通过简单地计算每个数据点之间的距离来工作。我们将在 Linkage 主题中详细讲解它具体计算的内容。在 linkage 函数中,我们可以选择度量和方法(稍后会详细介绍)。

    在确定链接矩阵后,我们可以轻松地通过 SciPy 提供的 dendrogram 函数进行传递。顾名思义,dendrogram 函数利用 步骤 4 中计算的距离生成一种直观简洁的方式来解析分组信息。

  5. 我们将使用一个自定义函数来清理原始输出的样式(请注意,以下代码片段中的函数使用的是 SciPy 的基础树状图实现,唯一的自定义代码是用来清理视觉输出的):

    # Take normal dendrogram output and stylize in cleaner way
    def annotated_dendrogram(*args, **kwargs):
        # Standard dendrogram from SciPy
        scipy_dendro = dendrogram(*args, truncate_mode='lastp', \
                                  show_contracted=True,\
                                  leaf_rotation=90.)
        plt.title('Blob Data Dendrogram')
        plt.xlabel('cluster size')
        plt.ylabel('distance')
        for i, d, c in zip(scipy_dendro['icoord'], \
                           scipy_dendro['dcoord'], \
                           scipy_dendro['color_list']):
            x = 0.5 * sum(i[1:3])
            y = d[1]
            if y > 10:
                plt.plot(x, y, 'o', c=c)
                plt.annotate("%.3g" % y, (x, y), xytext=(0, -5), \
                             textcoords='offset points', \
                             va='top', ha='center')
        return scipy_dendro
    dn = annotated_dendrogram(distances)
    plt.show()
    

    输出如下:

    图 2.12:距离的树状图

    图 2.12:距离的树状图

    这个图形将帮助我们从数据潜在的分群角度获取一些见解。基于之前步骤计算的距离,它展示了一个潜在路径,我们可以用它来创建三个独立的群组,距离为七,且这些群组足够独特,能够独立存在。

  6. 利用这些信息,我们可以通过使用 SciPy 中的 fcluster 函数来结束我们的层次聚类练习:

    scipy_clusters = fcluster(distances, 3, criterion="distance")
    plt.scatter(X[:,0], X[:,1], c=scipy_clusters)
    plt.show()
    

    fcluster 函数利用来自树状图的距离和信息,根据指定的阈值将数据分组。前面示例中的数字 3 代表你可以设置的最大聚类间距离阈值超参数。这个超参数可以根据你正在处理的数据集进行调整;然而,在本练习中,它被设定为 3。最终的输出如下:

    图 2.13:距离的散点图

图 2.13:距离的散点图

在前面的图中,你可以看到通过使用我们的阈值超参数,我们已经识别出八个不同的聚类。只需调用 SciPy 提供的几个辅助函数,你就能轻松地在几行代码中实现聚合聚类。尽管 SciPy 确实帮助处理了许多中间步骤,但这个示例仍然相对冗长,可能不完全符合你日常工作中的精简代码。我们稍后会介绍更简化的实现方式。

注意

要访问此部分的源代码,请参考 packt.live/2VTRp5K

你也可以在线运行这个示例,访问 packt.live/2Cdyiww

链接

练习 2.01建立层次结构 中,你使用被称为质心连接的方法实现了层次聚类。连接是确定如何计算聚类之间距离的概念,并且依赖于你面临的问题类型。选择质心连接用于练习 2.02应用连接准则,因为它基本上镜像了我们在 k-means 中使用的新质心搜索。然而,这并不是聚类数据点时唯一的选项。确定聚类之间距离的另外两个常见选择是单一连接和完全连接。

单一连接 通过找到两聚类之间一对点之间的最小距离作为连接的标准。简单来说,它基本上通过根据两个聚类之间最接近的点来组合聚类。数学上表示如下:

dist(a,b) = min( dist( a[i]), b[j] ) )

在前面的代码中,a[i] 是第一个聚类中的 i 点,而 b[j] 是第二个聚类中的 j 点。

完全连接 是单一连接的对立面,它通过找到两聚类之间一对点之间的最大距离作为连接的标准。简单来说,它通过根据两个聚类之间最远的点来组合聚类。数学上表示如下:

dist(a,b) = max( dist( a[i]), b[j] ) )

在前面的代码中,a[i]b[j] 分别是第一个和第二个聚类中的 ij 点。确定哪种连接准则最适合你的问题既是艺术也是科学,而且它在很大程度上依赖于你的特定数据集。选择单一连接的一个原因是如果你的数据在最近邻的意义上相似;因此,当存在差异时,数据将非常不相似。由于单一连接通过寻找最接近的点来工作,它不会受到这些遥远异常值的影响。然而,由于单一连接通过寻找一对点之间的最小距离来工作,它很容易受到分布在聚类之间的噪声影响。相反,如果你的数据在聚类间的状态上相距较远,那么完全连接可能是更好的选择;当聚类的空间分布不平衡时,完全连接会导致错误的分割。质心连接具有类似的优点,但如果数据非常嘈杂且聚类的“中心”不够明显,它就会失效。通常,最佳的方法是尝试几种不同的连接准则选项,看看哪种最适合你的数据,并以对你的目标最相关的方式进行操作。

练习 2.02:应用连接准则

回忆我们在前一个练习中生成的八个聚类的虚拟数据。在现实世界中,你可能会得到类似于离散高斯簇的真实数据。假设这些虚拟数据代表了某个商店中不同购物群体的情况。商店经理要求你分析这些购物者数据,以便将客户分为不同的群体,进而为每个群体量身定制营销材料。

使用我们在前一个练习中生成的数据,或者生成新数据,你将分析哪些连接方法能更好地将客户分组到不同的聚类中。

一旦生成数据,查看通过 SciPy 提供的文档,了解在 linkage 函数中有哪些连接类型可用。然后,通过将它们应用于你的数据来评估这些连接类型。你应测试的连接类型在以下列表中展示:

['centroid', 'single', 'complete', 'average', 'weighted']

我们尚未覆盖所有之前提到的连接类型——这项活动的关键部分是学习如何解析使用包提供的文档字符串,以探索它们的所有功能。请按照以下步骤完成本练习:

  1. 可视化我们在 练习 2.01 中创建的 x 数据集,构建层次结构

    from scipy.cluster.hierarchy import linkage, dendrogram, fcluster
    from sklearn.datasets import make_blobs
    import matplotlib.pyplot as plt
    %matplotlib inline
    
  2. 生成一个随机的聚类数据集进行实验。X = 坐标点,y = 聚类标签(不需要):

    X, y = make_blobs(n_samples=1000, centers=8, \
                      n_features=2, random_state=800)
    
  3. 如下所示可视化数据:

    plt.scatter(X[:,0], X[:,1])
    plt.show()
    

    输出如下:

    图 2.14: 生成的聚类数据集的散点图

    图 2.14: 生成的聚类数据集的散点图

  4. 创建一个包含所有可能连接方法超参数的列表:

    methods = ['centroid', 'single', 'complete', \
               'average', 'weighted']
    
  5. 遍历你刚创建的列表中的每个方法,并显示它们对相同数据集的影响:

    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.15: 重心连接方法的散点图

图 2.15: 重心连接方法的散点图

单连接法的图示如下:

图 2.16: 单连接方法的散点图

图 2.16: 单连接方法的散点图

完全连接法的图示如下:

图 2.17: 完全连接方法的散点图

图 2.17: 完全连接方法的散点图

平均连接法的图示如下:

图 2.18: 平均连接方法的散点图

图 2.18: 平均连接方法的散点图

加权连接法的图示如下:

图 2.19: 加权连接方法的散点图

图 2.19: 加权连接方法的散点图

如前面的图所示,通过简单地改变连接准则,你可以显著改变聚类的效果。在这个数据集中,质心和平均连接在找到有意义的离散聚类方面效果最好。从我们生成的包含八个聚类的数据集来看,质心和平均连接是唯一能够显示出用八种不同颜色表示的聚类的连接方式。其他连接类型效果较差,最明显的是单一连接。单一连接的效果较差,因为它假设数据是以细长的“链”格式存在,而不是聚类的形式。其他连接方法更优秀,因为它们假设数据是以聚集组的形式呈现。

注意

要访问此特定部分的源代码,请参考packt.live/2VWwbEv

你也可以在在线运行这个示例:packt.live/2Zb4zgN

聚合型与分裂型聚类

到目前为止,我们的层次聚类实例都是聚合型的——也就是说,它们是从底部向上构建的。虽然这是这种聚类类型最常见的方法,但重要的是要知道,这并不是创建层次结构的唯一方式。相反的层次方法——即从顶部向下构建,也可以用于创建你的分类法。这种方法称为分裂型层次聚类,它通过将数据集中的所有数据点放入一个大的聚类中来工作。分裂型方法的许多内部机制与聚合型方法非常相似:

图 2.20:聚合型与分裂型层次聚类

图 2.20:聚合型与分裂型层次聚类

与大多数无监督学习中的问题一样,决定最佳方法通常高度依赖于你所面临的具体问题。

想象一下,你是一位刚刚购买了一家新杂货店的创业者,现需要为店铺备货。你收到了一大批食品和饮料的运输箱,但你已经失去了所有的运输信息。为了有效地销售你的产品,你必须将相似的产品归为一类(如果你把所有东西随便放到货架上,店铺会变得一团糟)。为了实现这个组织目标,你可以采取自下而上或自上而下的方法。在自下而上的方法中,你会仔细查看运输箱里的物品,并认为一切都是杂乱无章的——然后,你会拿起一个随机物品,找出最相似的产品。例如,你可能拿起苹果汁,意识到将其与橙汁放在一起是有道理的。在自上而下的方法中,你会认为所有物品一开始就已按某种方式组织成一个大类。然后,你会逐步检查库存,并根据相似度的最大差异将其拆分成多个组。例如,如果你正在组织一家杂货店,你可能一开始认为苹果和苹果汁是属于同一类,但再想想,它们其实是相当不同的。因此,你会将它们拆分成更小的、不相似的组。

一般来说,可以将凝聚聚类看作自下而上的方法,将分裂聚类看作自上而下的方法——但它们在性能上有何权衡?这种立刻抓住最接近的物品的行为被称为“贪婪学习”;它有可能被局部邻居所欺骗,无法看到它在任何给定时刻所形成的簇的更大影响。另一方面,分裂方法的优势在于它从一开始就能看到整个数据分布作为一个整体,并选择最好的方式来拆分簇。对整个数据集外观的这种洞察对于创建更精确的簇非常有帮助,不容忽视。不幸的是,自上而下的方法通常会以牺牲更高的准确性为代价,换来更深的复杂性。在实践中,凝聚方法大多数情况下效果较好,应当作为层次聚类的首选起点。如果在查看层次结构后,你对结果不满意,尝试使用分裂方法可能会有所帮助。

练习 2.03:使用 scikit-learn 实现凝聚聚类

在大多数商业应用场景中,你很可能会使用一个抽象了所有内容的包来实现层次聚类,比如 scikit-learn。Scikit-learn 是一个免费包,是 Python 中进行机器学习时不可或缺的工具。它便捷地提供了最流行算法(如回归、分类和聚类)的高度优化版本。通过使用像 scikit-learn 这样的优化包,你的工作变得更加轻松。然而,只有在你完全理解层次聚类的工作原理之后,才应该使用它,正如我们在前面的章节中讨论的那样。这个练习将比较使用 SciPy 和 scikit-learn 进行聚类的两种潜在路线。通过完成此练习,你将了解它们各自的优缺点,以及从用户角度来看哪个最适合你。请按照以下步骤完成此练习:

  1. Scikit-learn 使得实现变得轻松,仅需几行代码。首先,导入必要的包并将模型分配给 ac 变量。然后,创建如前述练习中所示的 blob 数据:

    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)
    

    首先,我们通过传入熟悉的参数(如 affinity(距离函数)和 linkage)将模型分配给 ac 变量。

  2. 然后重用我们在之前练习中使用的 linkage 函数和 fcluster 对象:

    distances = linkage(X, method="centroid", metric="euclidean")
    sklearn_clusters = ac.fit_predict(X)
    scipy_clusters = fcluster(distances, 3, criterion="distance")
    

    在将模型实例化为一个变量后,我们可以简单地使用 .fit_predict() 将数据集拟合到所需模型,并将其赋值给另一个变量。这将为我们提供有关理想聚类的信息,作为模型拟合过程的一部分。

  3. 然后,我们可以通过绘图比较每种方法的最终聚类结果。让我们来看一下 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.21:scikit-learn 方法的图示

图 2.21: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.22:SciPy 方法的图示

图 2.22:SciPy 方法的图示

如你所见,两个方法基本上汇聚到了相同的聚类结果。

注意

要访问此部分的源代码,请参考 packt.live/2DngJuz

你还可以在网上运行此示例,网址为 packt.live/3f5PRgy

虽然这对于玩具问题来说很棒,但在下一次活动中,你将了解到,输入参数的微小变化可能会导致截然不同的结果。

活动 2.01:比较 K-means 和层次聚类

您正在管理一家商店的库存,并收到了大量葡萄酒货物,但品牌标签在运输过程中从瓶子上脱落。幸运的是,您的供应商为每瓶酒提供了化学成分数据,并附上了各自的序列号。不幸的是,您无法打开每瓶酒进行品尝测试——您必须找到一种方法,根据化学成分数据将未贴标签的酒瓶重新分组。您从订单单上知道,您订购了三种不同类型的葡萄酒,并且只提供了两种葡萄酒属性来重新分组这些酒。在此活动中,我们将使用葡萄酒数据集。该数据集包含三种不同类型葡萄酒的化学成分数据,根据 UCI 机器学习库中的来源,包含以下特征:

本活动的目的是在葡萄酒数据集上实现 k-means 和层次聚类,并确定哪种方法在为每种葡萄酒类型形成三个独立的聚类时更加准确。您可以尝试不同的 scikit-learn 实现组合,并使用 SciPy 和 NumPy 中的辅助函数。您还可以使用轮廓系数来比较不同的聚类方法,并在图表上可视化聚类结果。

完成此活动后,您将亲眼看到两种不同的聚类算法在同一数据集上的表现,从而便于在超参数调优和整体性能评估时进行比较。您可能会注意到,根据数据的形状,一种方法的表现优于另一种方法。此活动的另一个关键结果是理解超参数在任何给定用例中的重要性。

以下是完成此活动的步骤:

  1. 从 scikit-learn 导入必要的包(KMeansAgglomerativeClusteringsilhouette_score)。

  2. 将葡萄酒数据集读取到 pandas DataFrame 中并打印一个小样本。

  3. 通过绘制 OD 读取特征与前花青素特征的关系,来可视化数据集中的一些特征。

  4. 使用sklearn实现的 k-means 算法对葡萄酒数据集进行聚类,知道数据集中有三种葡萄酒类型。

  5. 使用sklearn实现的层次聚类对葡萄酒数据集进行聚类。

  6. 绘制 k-means 算法预测的聚类。

  7. 绘制层次聚类算法预测的聚类。

  8. 比较每种聚类方法的轮廓系数。

完成此活动后,你应该已经绘制出了从 k-means 获得的预测簇,如下所示:

图 2.23:来自 k-means 方法的预期簇

图 2.23:来自 k-means 方法的预期簇

对于层次聚类预测的簇,也应获得类似的图,如下所示:

图 2.24:来自聚合方法的预期簇

图 2.24:来自聚合方法的预期簇

注意

此活动的解决方案可以在第 423 页找到。

k-means 与层次聚类

在上一章中,我们探讨了 k-means 聚类的优点。现在,了解层次聚类在其中的作用是非常重要的。正如我们在连接部分提到的那样,当使用质心对数据点进行分组时,可能存在一些直接的重叠。到目前为止,我们提到的所有方法都普遍使用了距离函数来确定相似性。由于我们在上一章的深入探讨,我们在这里使用了欧几里得距离,但我们理解任何距离函数都可以用来确定相似性。

实际上,选择一种聚类方法而非另一种时,可以参考以下几点:

  • 层次聚类的优势在于不需要预先传入明确的“k”簇数。这意味着你可以找到所有潜在的簇,并在算法完成后决定哪些簇最有意义。

  • 从简便性角度来看,k-means 聚类具有优势——在商业用例中,通常面临一个挑战:如何找到既能向非技术观众解释又能生成高质量结果的方法。k-means 可以轻松填补这一空缺。

  • 层次聚类在处理形状异常的数据时,调整的参数比 k-means 聚类更多。虽然 k-means 在发现离散簇方面表现优秀,但在处理混合簇时可能会遇到困难。通过调整层次聚类中的参数,你可能会得到更好的结果。

  • 基本的 k-means 聚类通过实例化随机质心并找到与这些质心最接近的点来工作。如果它们被随机实例化在特征空间中远离数据的区域,那么可能需要相当长的时间才能收敛,或者可能根本无法达到收敛点。层次聚类则较不容易受到这种缺陷的影响。

总结

本章中,我们讨论了层次聚类的工作原理以及它可能的最佳应用场景。特别是,我们讨论了如何通过评估树状图来主观地选择簇的各个方面。如果你完全不知道在数据中寻找什么,这比 k 均值聚类有一个巨大的优势。我们还讨论了两个推动层次聚类成功的关键参数:合并方法和分裂方法,以及聚类标准。合并聚类采用自下而上的方法,通过递归地将邻近的数据合并在一起,直到结果为一个大簇。分裂聚类采用自上而下的方法,从一个大簇开始,递归地将其拆分,直到每个数据点都进入其自己的簇。分裂聚类由于从一开始就可以全面查看数据,因此有可能更准确;然而,它增加了一层复杂性,这可能会降低稳定性并增加运行时间。

聚类标准涉及如何计算候选簇之间的距离。我们已经探讨了质心如何在 k 均值聚类之外再次出现,以及单一链接和完全链接标准。单一链接通过比较每个簇中最近的点来计算簇间距离,而完全链接通过比较每个簇中较远的点来计算簇间距离。通过本章所学的知识,你现在能够评估 k 均值聚类和层次聚类如何最好地适应你正在处理的挑战。

虽然由于层次聚类的复杂性增加,它可能比 k 均值聚类表现更好,但请记住,更多的复杂性并不总是好的。作为无监督学习的从业者,你的职责是探索所有选项,并确定既高效又具有性能的解决方案。在下一章中,我们将介绍一种在处理高度复杂和噪声数据时最适用的聚类方法:基于密度的空间聚类应用与噪声

第四章:3. 邻域方法与 DBSCAN

概述

在本章中,我们将展示基于邻域的聚类方法如何从头到尾工作,并通过使用软件包从零开始实现基于密度的空间聚类算法(带噪声)DBSCAN)算法。我们还将从 k-means、层次聚类和 DBSCAN 中识别出最适合解决您问题的算法。到本章结束时,我们将看到 DBSCAN 聚类方法如何在处理高度复杂数据时为我们提供最佳服务。

引言

在前面的章节中,我们评估了多种不同的数据聚类方法,包括 k-means 和层次聚类。虽然 k-means 是最简单的聚类形式,但在适当的场景中,它仍然非常强大。在 k-means 无法捕捉数据集复杂性的情况下,层次聚类证明是一个强有力的替代方法。

无监督学习的关键挑战之一是,您会获得一组特征数据,但没有额外的标签告诉您目标状态是什么。虽然您可能无法明确知道目标标签是什么,但通过将相似的群体聚集在一起并观察群体内的相似性,您可以从数据中提取出一些结构。我们首先介绍的聚类相似数据点的方法是 k-means。K-means 聚类最适合处理那些速度至关重要的简单数据问题。只需查看最接近的数据点(聚类中心),就不需要太多计算开销;然而,当面对高维数据集时,也会面临更大的挑战。如果您不知道需要查找的聚类数目,k-means 也不是理想选择。我们在第二章层次聚类》中探讨的一个例子是,通过分析化学特征来确定哪些葡萄酒在一次杂乱的运输中属于同一类。之所以能够顺利进行,是因为我们知道有三种葡萄酒类型已被订购;然而,如果您不知道原始顺序是什么,k-means 可能就不会那么成功。

我们探索的第二种聚类方法是层次聚类。该方法有两种工作方式——聚合性(agglomerative)或分裂性(divisive)。聚合性聚类采用自下而上的方法,将每个数据点视为自己的簇,并根据链接标准递归地将它们组合在一起。分裂性聚类则采用相反的方式,将所有数据点视为一个大类,并递归地将它们分解成更小的簇。该方法的优点在于能够全面理解数据的分布,因为它计算了分割潜力;然而,由于其复杂性较高,通常在实践中不被采用。层次聚类对于当你对数据一无所知时是一个很有竞争力的选择。通过使用树状图(dendrogram),你可以可视化数据中的所有分割,并考虑事后哪个聚类数最合理。这在特定的使用场景中非常有用,但它的计算成本高于 k-means。

在本章中,我们将介绍一种在处理高度复杂数据时最适合的聚类方法:基于密度的空间聚类应用与噪声(DBSCAN)。传统上,这种方法在具有大量密集交错数据的数据库中一直被认为是一种高效的聚类方法。让我们一起看看它为什么在这些应用场景中表现得如此出色。

作为邻域的簇

到目前为止,我们探讨的相似性概念是通过欧几里得距离来描述的——距离某一点较近的数据点可以看作是相似的,而在欧几里得空间中距离较远的数据点则被视为不相似。这个概念在 DBSCAN 算法中再次出现。如同其冗长的名字所暗示的那样,DBSCAN 方法不仅基于距离度量评估,还引入了密度的概念。如果有一些数据点聚集在同一区域内,它们可以视为同一簇的成员:

图 3.1:邻居与簇之间有直接联系

图 3.1:邻居与簇之间有直接联系

在前面的图中,我们可以看到四个邻域。与我们之前讨论的仅仅基于距离的方法相比,基于密度的方法有很多优势。如果你只关注距离作为聚类的阈值,那么在面对稀疏特征空间和离群点时,你可能会发现聚类结果毫无意义。无论是 k-means 还是层次聚类,都会自动将空间中的所有数据点分组,直到没有剩余的点。

尽管层次聚类在一定程度上提供了解决此问题的途径,因为你可以在聚类运行后使用树状图来确定聚类的形成位置,但 k-means 作为聚类的最简单方法,最容易失败。当我们开始评估基于邻域的聚类方法时,这些陷阱就不那么明显了。在下面的树状图中,你可以看到一个典型的陷阱,其中所有数据点都被归为一类。显然,当你沿着树状图向下走时,许多具有潜在差异的数据点被聚在一起,因为每个点都必须属于一个聚类。使用基于邻域的聚类时,这个问题要少得多:

图 3.2:示例树状图

图 3.2:示例树状图

通过在 DBSCAN 中引入邻居密度的概念,我们可以选择性地将异常值排除在聚类之外,具体取决于我们在运行时选择的超参数。只有那些具有相近邻居的数据点才会被视为同一聚类中的成员,而那些距离较远的点则会被视为未聚类的异常值。

DBSCAN 简介

在 DBSCAN 中,密度是通过邻域半径和在被认为是一个聚类的邻域内找到的最小点数的组合来评估的。如果我们重新考虑一下你负责为商店整理一批未标记的葡萄酒货物的场景,这个概念就会变得更清晰。在前面的例子中,已经明确了我们可以根据葡萄酒的特征,如化学特性,来找到相似的葡萄酒。知道这些信息后,我们可以更容易地将相似的葡萄酒归为一类,迅速地将我们的产品整理好,准备好销售。然而,在现实世界中,你为商店订购的产品将反映出真实的购买模式。为了在库存中促使多样性,同时保证最受欢迎的葡萄酒有足够的存货,你的产品种类会呈现出极为不均衡的分布。大多数人喜欢经典的葡萄酒,如白葡萄酒和红葡萄酒;然而,你可能还会为喜欢昂贵酒款的客户提供一些更为独特的葡萄酒。这使得聚类变得更加困难,因为不同类别的分布不均(例如,你不会为每种葡萄酒都订购 10 瓶)。

DBSCAN 与 k-means 和层次聚类的不同之处在于,你可以将这种直觉融入到我们评估所关注的客户聚类中。它能够更轻松地去除噪声,仅指向那些在营销活动中具有最高再营销潜力的客户。

通过利用邻域的概念进行聚类,我们可以将那些一次性光顾的客户(可以看作是随机噪声)与那些反复光顾我们商店的更有价值的客户区分开来。这种方法让我们重新思考在确定邻域半径和每个邻域的最小点数时,如何建立最佳数值。

作为一种高级启发式方法,我们希望邻域半径较小,但又不能过小。在一个极端情况下,邻域半径可以非常大——这可能会将特征空间中的所有点都视为一个庞大的簇。在另一个极端情况下,邻域半径可以非常小。过小的邻域半径可能导致没有点被聚类在一起,从而产生大量单一成员的簇。

类似的逻辑适用于构成簇的最小点数。最小点数可以看作是一个次要阈值,它根据你的数据空间中的数据来调整邻域半径。如果特征空间中的所有数据都非常稀疏,最小点数就变得非常重要,与邻域半径一起工作,确保你不会只得到大量不相关的数据点。当你拥有非常密集的数据时,最小点数阈值就不像邻域半径那样成为一个主导因素。

正如你从这两个超参数规则中看到的,最佳选项通常依赖于数据集的具体情况。通常,你会希望找到一个完美的“黄金中介”区域,既不过小,也不过大。

DBSCAN 详细介绍

为了了解 DBSCAN 如何工作,我们可以通过一个简单的示例程序的路径,查看它如何将数据点合并成不同的簇和噪声标签的数据点:

  1. n 个未访问的样本数据点中,我们首先通过每个点并标记它为已访问。

  2. 从每个点出发,我们将查看到数据集中每个其他点的距离。

  3. 所有位于邻域半径超参数范围内的点都应视为邻居。

  4. 邻居的数量应至少与所需的最小点数相等。

  5. 如果达到了最小点阈值,点应被归为一个簇,否则标记为噪声。

  6. 这个过程应该重复进行,直到所有数据点都被分类为簇或噪声。

在某些方面,DBSCAN 相当简单——尽管有邻域半径和最小点的新概念,但本质上,它仍然只是使用距离度量来进行评估。

DBSCAN 算法演示

以下步骤将更详细地引导你完成此路径:

  1. 给定六个样本数据点,将每个点视为其自己的簇:[ (1,3) ], [ (-8,6) ], [ (-6,4) ], [ (4,-2) ], [ (2,5) ], [ (-2,0) ]: 图 3.3:样本数据点的绘图

    图 3.3:样本数据点的绘图

  2. 计算每两个点之间的欧几里得距离:图 3.4:点之间的距离

    图 3.4:点之间的距离

  3. 从每个点出发,向外扩展邻域大小并形成簇。为了方便理解,假设你通过一个半径为五的邻域。这意味着,如果两点之间的距离小于五个单位,它们就会成为邻居。例如,点 (1,3) 有点 (2,5) 和 (-2,0) 作为邻居。

    根据给定点邻域中点的数量,该点可以被分类为以下三类:

    核心点:如果观察点在其邻域中拥有比最低点数要求更多的数据点,这些点构成了一个簇,则该点被称为该簇的核心点。所有核心点所在的邻域中的其他核心点都属于同一簇。然而,所有不在同一邻域内的核心点属于另一个簇。

    边界点:如果观察点周围没有足够的邻居(数据点),但至少有一个核心点(在其邻域内),则该点表示该簇的边界点。边界点属于其最近核心点的同一簇。

    噪声点:如果数据点在其邻域内没有满足要求的最小数据点数量,并且与任何核心点无关,则该点被视为噪声点,并从聚类中排除。

  4. 拥有邻居的点将进行评估,以查看它们是否通过最小点数的阈值。在这个例子中,如果我们通过了最小点数阈值为二,那么点 (1,3),(2,5),和 (-2,0) 可以正式归为同一簇。如果我们通过最小点数阈值为四,那么这三个数据点将被视为多余的噪声。

  5. 拥有比邻域内所需最小邻居数量更少邻居,并且其邻域不包含核心点的点被标记为噪声点,且不参与聚类。因此,点 (-6,4),(4,-2),和 (-8,6) 属于这一类别。然而,像 (2,5) 和 (2,0) 这样的点,尽管它们不满足邻域内最小点数的标准,但它们确实包含一个核心点作为邻居,因此被标记为边界点。

  6. 以下表格总结了某个特定点的邻居,并将其分类为核心点、边界点和噪声数据点(如前述步骤所提到),邻域半径为 5,最小邻居标准为 2。图 3.5:表格展示了给定点的邻居详细信息

    图 3.5:表格展示了给定点的邻居详细信息

  7. 对任何剩余的未访问数据点重复此过程。

在此过程结束时,你将把整个数据集划分为聚类或无关的噪声。DBSCAN 的性能在很大程度上取决于你选择的阈值超参数。这意味着你可能需要多次运行 DBSCAN,并使用不同的超参数选项,以了解它们如何影响整体性能。

注意,DBSCAN 不需要我们在 k-means 和以质心为中心的层次聚类实现中看到的质心。这个特点使得 DBSCAN 对于复杂数据集更有效,因为大多数数据并不是像干净的块状物一样的形状。与 k-means 或层次聚类相比,DBSCAN 在应对离群点和噪声方面也更有效。

现在让我们看看随着邻域半径大小的变化,DBSCAN 的性能如何变化。

练习 3.01: 评估邻域半径大小的影响

在本次练习中,我们将颠倒我们通常在前面示例中看到的顺序,首先查看 scikit-learn 中的 DBSCAN 打包实现,然后再自己实现它。这是故意的,目的是充分探索不同邻域半径大小如何显著影响 DBSCAN 的性能。

完成这个练习后,你将熟悉调整邻域半径大小如何改变 DBSCAN 的性能。了解 DBSCAN 的这些方面非常重要,因为它们可以通过高效排查聚类算法问题,为你节省未来的时间:

  1. 导入 scikit-learn 和 matplotlib 中本次练习所需的包:

    from sklearn.cluster import DBSCAN
    from sklearn.datasets import make_blobs
    import matplotlib.pyplot as plt
    %matplotlib inline
    
  2. 生成一个随机聚类数据集来进行实验;X = 坐标点,y = 聚类标签(不需要):

    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.6: 可视化的玩具数据示例

    图 3.6: 可视化的玩具数据示例

  3. 在为这个玩具问题绘制虚拟数据之后,你将看到数据集有两个特征,大约七到八个聚类。要使用 scikit-learn 实现 DBSCAN,你需要实例化一个新的 scikit-learn 类:

    db = DBSCAN(eps=0.5, min_samples=10, metric='euclidean')
    

    我们的示例 DBSCAN 实例存储在 db 变量中,并且我们的超参数在创建时传入。为了本示例,你可以看到邻域半径(eps)被设置为 0.5,而最小点数设置为 10。为了与我们之前的章节保持一致,我们将再次使用欧几里得距离作为我们的距离度量。

    注意

    eps 代表 epsilon,是你的算法在查找邻居时会搜索的邻域半径。

  4. 让我们设置一个循环,允许我们交互式地探索潜在的邻域半径大小选项:

    eps = [0.2,0.7,4]
    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.7: 结果图

图 3.7: 结果图

从图中可以看出,将邻域大小设置得过小会导致所有内容都被视为随机噪声(紫色点)。稍微增加邻域大小后,我们能够形成更有意义的簇。再大的 epsilon 值会将整个数据集合并为一个簇(紫色数据点)。尝试重新创建前面的图并实验不同的 eps 大小。

注意

要访问这一部分的源代码,请参考 packt.live/3gEijGC

你也可以在 packt.live/2ZPBfeJ 在线运行这个例子。

DBSCAN 属性 – 邻域半径

在前面的练习中,你看到设置合适的邻域半径对 DBSCAN 实现性能的影响。如果你的邻域设置得太小,那么所有数据都会被当作噪声处理,无法聚类。如果将邻域设置得过大,所有数据也会被归为一个簇,无法提供任何价值。如果你用自己的 eps 值进一步探索了前面的练习,你可能会注意到,单纯依靠邻域大小进行有效聚类是非常困难的。这时,最小点数阈值就派上了用场。我们稍后会讲到这个话题。

为了更深入地理解 DBSCAN 的邻域概念,我们来看一下在实例化时传入的 eps 超参数。这个 epsilon 值会被转换为一个半径,围绕任意给定的数据点以圆形方式进行扫描,作为邻域的定义:

图 3.8:邻域半径的可视化;红色圆圈表示邻域

图 3.8:邻域半径的可视化;红色圆圈表示邻域

在这个例子中,中心点会有四个邻居,正如前面的图中所示。

这里需要观察的一个关键点是,你的邻域搜索形成的形状在二维空间中是圆形的,在三维空间中是球形的。这可能会根据数据的结构影响你模型的性能。再一次,簇可能看起来像是直观的结构,但这不一定总是如此。幸运的是,DBSCAN 很好地解决了这类簇的问题,虽然这些簇可能是你感兴趣的,但并不符合显式的簇结构:

图 3.9:邻域半径大小变化的影响

图 3.9:邻域半径大小变化的影响

左侧的数据点将被分类为随机噪声。右侧的数据点有多个邻居,可能会形成一个自己的簇。

活动 3.01:从零开始实现 DBSCAN

在面试中,你被要求使用生成的二维数据集从零开始创建 DBSCAN 算法。为此,你需要将邻域搜索的理论转化为生产代码,并通过递归调用来添加邻居。正如前一节所解释的,你将使用距离扫描,在指定点周围的空间内添加这些邻居。

根据你在前几章中学到的关于 DBSCAN 和距离度量的知识,从零开始用 Python 实现 DBSCAN。你可以自由使用 NumPy 和 SciPy 来评估距离。

以下步骤将帮助你完成此活动:

  1. 生成一个随机聚类数据集。

  2. 可视化数据。

  3. 从零开始创建函数,允许你在数据集上调用 DBSCAN。

  4. 使用你创建的 DBSCAN 实现来找到生成数据集中的聚类。根据需要调整超参数,依据其性能来调优。

  5. 可视化你从零开始实现的 DBSCAN 聚类性能。

这个练习的目标是让你在使用 scikit-learn 中的完整实现之前,从零开始实现 DBSCAN 的工作原理。通过这种方式从零开始处理任何机器学习算法是非常重要的,它帮助你“赚取”使用更简便实现的能力,同时仍然能够在未来深入讨论 DBSCAN:

图 3.10:预期结果

图 3.10:预期结果

注意

此活动的解决方案可以在第 428 页找到。

DBSCAN 属性 – 最小点数

除了邻域半径外,成功实现 DBSCAN 的另一个核心组成部分是聚类内所需的最小数据点数,以证明数据点属于该聚类。如前所述,较低的阈值在稀疏数据集上对算法的优化非常明显。这并不意味着它在密集数据中没有用处;然而,虽然将单个随机分布的数据点轻松归类为噪声,但当我们有两个到三个点随机分布时,就变得模糊不清。例如,这些数据点应该是自己的聚类,还是也应该被分类为噪声?最小点数阈值有助于解决这个问题。

在 scikit-learn 实现的 DBSCAN 中,这个超参数出现在创建 DBSCAN 实例时传递的 min_samples 字段。这个字段与邻域半径大小超参数一起非常有价值,能够完善你的基于密度的聚类方法:

图 3.11:决定数据点是否为噪声或聚类的最小点数阈值

图 3.11:决定数据点是否为噪声或聚类的最小点数阈值

如果最小点数阈值为 10 个点,则它将在该邻域内将数据归类为噪声。

在实际应用中,当你有大量数据时,最小点数会产生很大的影响。回到葡萄酒聚类的例子,如果你的商店实际上是一个大型葡萄酒仓库,你可能有成千上万瓶酒,其中只有一两瓶酒可以被视为一个单独的聚类。根据你的用例,这可能是有用的;然而,需要牢记的是,数据中的主观量级。如果你有数百万个数据点,随机噪声很容易被视为数百甚至数千个单独的销售记录。然而,如果你的数据规模是几百或几千个数据点,单个数据点也可能被视为随机噪声。

练习 3.02:评估最小点数阈值的影响

类似于练习 3.01评估邻域半径大小的影响,我们探讨了设置合适邻域半径大小的值,本次我们将重复该练习,但改为在各种数据集上改变最小点数阈值。

使用我们当前的 DBSCAN 实现,我们可以轻松调整最小点数阈值。调整此超参数,并观察它在生成数据上的表现。

通过调整 DBSCAN 的最小点数阈值,你将了解它如何影响你的聚类预测质量。

再次,我们从随机生成的数据开始:

  1. 生成一个随机聚类数据集,如下所示:

    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)
    
  2. 如下所示,可视化数据:

    # Visualize the data
    plt.scatter(X[:,0], X[:,1])
    plt.show()
    

    输出结果如下:

    图 3.12:生成数据的图表

    图 3.12:生成数据的图表

  3. 使用与之前相同的绘图数据,让我们选取练习 3.01评估邻域半径大小的影响中的一个表现更好的邻域半径大小——eps = 0.7

    db = DBSCAN(eps=0.7, min_samples=10, metric='euclidean')
    

    注意

    eps是一个可调的超参数。在前一个练习的步骤 3中,我们使用了0.5的值。在这一步,我们根据对该参数的实验使用了eps = 0.7

  4. 实例化 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()
    

    查看生成的第一个图表,我们可以看到如果你严格按照练习 3.01评估邻域半径大小的影响的要求,用 10 个最小点作为聚类成员资格的阈值,你会得到的结果:

    图 3.13:具有最小 10 个点的玩具问题图

图 3.13:具有最小 10 个点的玩具问题图

剩余的两个超参数选项可以明显影响你的 DBSCAN 聚类算法的性能,并展示了数字的微小变化如何极大地影响性能:

图 3.14:玩具问题的图表

图 3.14:玩具问题的图表

正如您所看到的,仅仅将最小点数从 19 改为 20 就会在我们的特征空间中增加一个额外(不正确的!)的聚类。通过本练习学到的关于最小点数的知识,您现在可以调整 scikit-learn 实现中的 epsilon 和最小点数阈值,以达到最佳聚类数。

注意

在我们原始生成的数据中,我们创建了八个聚类。这表明最小点数的微小变化可能会添加完全不应存在的新聚类。

要访问此特定部分的源代码,请参阅packt.live/3fa4L5F

您还可以在packt.live/31XUeqi上在线运行此示例。

活动 3.02:比较 DBSCAN 与 k-means 和层次聚类

在前面的章节中,我们尝试使用层次聚类将不同的葡萄酒分组在一起。让我们再次尝试使用 DBSCAN 这种方法,看看邻域搜索是否能取得更好的效果。作为提醒,您正在管理商店的库存,并收到了大量葡萄酒的运输过程中标签掉落的情况。幸运的是,您的供应商提供了每瓶酒的化学读数及其对应的序列号。不幸的是,您无法打开每瓶酒品尝其差异 - 您必须找到一种方法将未贴标签的酒重新按照其化学读数分组!您从订单列表中知道,您订购了三种不同类型的葡萄酒,并且只给出了两种葡萄酒属性来将酒类重新分组。

在之前的章节中,我们能够看到 k-means 和层次聚类在葡萄酒数据集上的表现。在我们的最佳情况下,我们能够实现 0.59 的轮廓分数。使用 scikit-learn 的 DBSCAN 实现,让我们看看是否可以获得更好的聚类效果。

这些步骤将帮助您完成活动:

  1. 导入必要的包。

  2. 加载葡萄酒数据集并检查数据的外观。

  3. 可视化数据。

  4. 使用 k-means、凝聚聚类和 DBSCAN 生成聚类。

  5. 评估几种不同的 DSBSCAN 超参数选项及其对轮廓分数的影响。

  6. 基于最高轮廓分数生成最终的聚类。

  7. 可视化使用三种方法生成的聚类。

    注意

    我们从archive.ics.uci.edu/ml/datasets/wine获取了此数据集。[引用:Dua, D. and Graff, C. (2019). UCI Machine Learning Repository [archive.ics.uci.edu/ml]。Irvine, CA: 加利福尼亚大学,信息与计算机科学学院]。您还可以在packt.live/3bW8NME上访问它。

完成此活动后,你将重新构建一个完整的聚类问题工作流程。你已经在第二章层次聚类中熟悉了数据,并且在本活动结束时,你将进行模型选择,找到适合你数据集的最佳模型和超参数。你将为每种聚类类型获得酒类数据集的轮廓系数。

注意

本活动的解决方案可以在第 431 页找到。

DBSCAN 与 k 均值和层次聚类

现在你已经理解了 DBSCAN 的实现方式,以及可以调整的多个超参数来驱动性能,让我们来看看它与我们之前介绍的聚类方法——k 均值聚类和层次聚类——有何不同。

你可能在活动 3.02比较 DBSCAN 与 k 均值和层次聚类中注意到,DBSCAN 在通过轮廓系数找到最优簇时可能有些挑剔。这是邻域方法的一个缺点——当你对数据中簇的数量有一些了解时,k 均值和层次聚类的表现通常非常好。在大多数情况下,这个簇的数量较少,你可以通过多次尝试不同的数量来观察其表现。相比之下,DBSCAN 采取了一种自下而上的方法,通过调整超参数并找到它认为重要的簇。在实际操作中,当前两种方法失败时,考虑使用 DBSCAN 会很有帮助,因为它需要较多的调整才能正常工作。尽管如此,当你的 DBSCAN 实现正常工作时,它通常会远远优于 k 均值和层次聚类(在实际中,这通常发生在高度交织但仍然离散的数据中,例如包含两个半月形的特征空间)。

与 k 均值和层次聚类相比,DBSCAN 可能更高效,因为它只需要查看每个数据点一次。与多次迭代寻找新质心并评估其最近邻的过程不同,在 DBSCAN 中,一旦一个点被分配到某个簇,它的簇成员关系就不会再发生变化。DBSCAN 与层次聚类相比,另一大关键特点是,它不需要在创建时显式地传递期望的簇数量,而 k 均值却需要。这一点在你没有外部指导如何拆分数据集时非常有帮助。

总结

在本章中,我们讨论了层次聚类和 DBSCAN,以及它们最适合应用的情况。虽然层次聚类在某些方面可以被看作是 k-means 中最近邻方法的扩展,但 DBSCAN 通过应用密度的概念来解决寻找邻居的问题。当数据非常复杂且交织在一起时,这种方法可能非常有用。虽然 DBSCAN 非常强大,但它并不是万无一失的,具体效果也取决于原始数据的表现,有时甚至可能显得过于复杂。

然而,结合了 k-means 和层次聚类,DBSCAN 在聚类任务中为无监督学习提供了一个强大的工具箱。在面对该领域的任何问题时,比较每种方法的表现并观察哪种方法效果最佳是非常值得的。

在探索了聚类后,我们将进入无监督学习中另一个关键技能:降维。通过智能地减少维度,我们可以使聚类更加易于理解,并能够与利益相关者进行沟通。降维对于以最有效的方式创建各种机器学习模型也至关重要。在下一章,我们将深入研究主题模型,并查看在这些章节中学习的聚类方面如何应用于自然语言处理(NLP)类型的问题。

第五章:4. 降维技术与 PCA

概述

在本章中,我们将应用降维技术,并描述主成分和降维背后的概念。在使用 scikit-learn 解决问题时,我们将应用主成分分析PCA)。我们还将比较手动 PCA 与 scikit-learn 的实现。到本章结束时,你将能够通过提取数据中最重要的方差成分,来减小数据集的规模。

介绍

在上一章,我们讨论了聚类算法及其在从大量数据中提取潜在意义方面的应用。本章将探讨在无监督学习算法中使用不同特征集(或特征空间),我们将从讨论降维开始,特别是主成分分析PCA)。接着,我们将通过探索两种独立强大的机器学习架构——基于神经网络的自动编码器,来扩展我们对不同特征空间优势的理解。神经网络无疑在监督学习问题中享有强大的声誉。此外,通过使用自动编码器阶段,神经网络已经被证明在无监督学习问题中具有足够的灵活性。最后,我们将基于神经网络实现和降维进行扩展,介绍在第六章中涵盖的 t-分布最近邻方法,t-分布随机邻居嵌入。这些技术在处理高维数据时非常有用,如图像处理或包含多个特征的数据集。一些降维技术的一个重要商业优势是,它有助于去除那些对最终输出影响不大的特征。这为提升算法效率创造了机会,而不损失性能。

什么是降维?

降维是数据科学家工具箱中的一项重要工具,由于其广泛的应用场景,几乎已成为该领域的基本知识。因此,在我们考虑降维及其降维的原因之前,我们必须首先清楚理解“维度”是什么。简单来说,维度是与数据样本相关的维度、特征或变量的数量。通常,可以将其视为电子表格中的列数,其中每个样本占一行,每列描述样本的一个属性。以下表格就是一个例子:

图 4.1:具有三种不同特征的两组数据样本

图 4.1:具有三种不同特征的两组数据样本

在上表中,我们有两个数据样本,每个样本有三个独立的特征或维度。根据正在解决的问题或数据集的来源,我们可能希望减少每个样本的维度数量,而不丢失提供的信息。这就是降维可以帮助我们的地方。但是降维究竟如何帮助我们解决问题呢?我们将在接下来的部分详细介绍其应用。然而,假设我们有一个非常大的时间序列数据集,比如心电图或心电图(在某些国家也称为心电图),如下图所示:

图 4.2:心电图(ECG 或 EKG)

图 4.2:心电图(ECG 或 EKG)

这些信号是从贵公司新款手表中捕获的,我们需要寻找心脏病发作或中风的迹象。在查看数据集后,我们可以得出一些观察结果:

  • 大多数单独的心跳非常相似。

  • 数据中存在来自记录系统或患者在记录期间移动时的一些噪声。

  • 尽管存在噪声,心跳信号仍然可见。

  • 数据量非常大 - 使用手表上可用的硬件无法处理太多数据。

正是在这种情况下,降维技术真正发挥了作用。通过使用降维,我们能够从信号中去除大部分噪声,这反过来将有助于算法对数据的性能以及减小数据集大小以满足更低的硬件要求。本章中要讨论的技术,特别是 PCA 和自编码器,已在研究和工业中得到了有效地应用,以有效地处理、聚类和分类这类数据集。

降维的应用场景

在我们开始详细研究降维和 PCA 之前,我们将讨论这些技术的一些常见应用:

  • 预处理/特征工程:最常见的应用之一是在开发机器学习解决方案的预处理或特征工程阶段。在算法开发过程中提供的信息质量,以及输入数据与期望结果之间的相关性,对于设计出高性能的解决方案至关重要。在这种情况下,PCA 可以提供帮助,因为我们能够从数据中分离出最重要的信息成分,并将其提供给模型,以便仅提供最相关的信息。这也有一个次要的好处,即我们减少了提供给模型的特征数量,从而可以减少需要完成的计算量。这可以减少系统的整体训练时间。这个特征工程的一个典型应用案例是预测某笔交易是否存在信用卡盗刷风险。在这种情况下,您可能会面临数百万笔交易,每笔交易有几十个甚至上百个特征。这将是资源密集型的,甚至在实时运行预测算法时都几乎不可能;然而,通过使用特征预处理,我们可以将许多特征提炼成仅仅是最重要的 3-4 个特征,从而减少运行时间。

  • 降噪:降维也可以作为一种有效的降噪/过滤技术。预期信号或数据集中的噪音并不占据数据变化的主要部分。因此,我们可以通过去除较小的变化成分来去除信号中的一些噪音,然后将数据恢复到原始的数据空间。在下面的示例中,左侧的图像已经过滤到前 20 个最重要的数据源,这为我们提供了右侧的图像。我们可以看到图像的质量有所降低,但关键信息仍然存在:图 4.3:使用降维过滤的图像

    ](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_04_03.jpg)

图 4.3:使用降维过滤的图像

注意

这张照片由 Arthur Brognoli 拍摄,来自 Pexels,并根据www.pexels.com/photo-license/提供免费下载。在这种情况下,左侧是原始图像,右侧是经过过滤的图像。

  • 生成合理的人工数据集:由于主成分分析(PCA)将数据集分解为信息(或变化)的组件,我们可以研究每个组件的影响,或者通过调整特征值之间的比例来生成新的数据集样本。本章稍后会详细介绍特征值。我们可以对这些组件进行缩放,这实际上是增加或减少该特定组件的重要性。这也被称为统计形状建模,因为一个常见的方法是使用它来创建合理的形状变体。它还用于在主动形状建模过程中检测图像中的面部标志点。

  • 金融建模/风险分析:降维为金融行业提供了一个有用的工具包,因为能够将大量的市场指标或信号合并成较少的几个组件,可以加快计算速度并提高效率。类似地,这些组件可以用于突出那些风险较高的产品/公司。

高维灾难

在我们理解使用降维技术的好处之前,我们必须首先理解为什么需要减少特征集的维度。高维灾难是一个常用术语,用来描述在处理具有高维特征空间的数据时所遇到的问题;例如,为每个样本收集的属性数量。考虑一下《吃豆人》游戏中的点位置数据集。你的角色,吃豆人,处于一个由两个维度或坐标(x, y)定义的虚拟世界中的某个位置。假设我们正在创建一个新的电脑敌人:一个由人工智能驱动的鬼怪,用来对抗玩家,它需要一些关于玩家角色的信息来做出自己的游戏逻辑决策。为了让这个机器人有效,我们需要玩家的位置信息(x, y)和他们在各个方向上的速度(vx, vy),以及玩家最近五个(x, y)位置的数据,剩余的心数和迷宫中剩余的能量豆数量(能量豆暂时允许吃豆人吃掉鬼怪)。因此,在每个时刻,我们的机器人需要 16 个单独的特征(或维度)来做出决策。这 16 个特征对应于 5 个先前的位置数据乘以 2 个xy坐标 + 当前玩家位置的 2 个xy坐标 + 玩家速度的 2 个xy坐标 + 1 个心数特征 + 1 个能量豆特征 = 16。这显然比单纯由位置提供的两个维度要多得多:

图 4.4:吃豆人游戏中的维度

图 4.4:吃豆人游戏中的维度

为了解释降维的概念,我们将考虑一个虚构的数据集(参见图 4.5),其中xy坐标作为特征,构成了特征空间中的两个维度。需要注意的是,这个例子绝不是数学证明,而是旨在提供一种可视化增加维度效果的方法。在这个数据集中,我们有六个单独的样本(或点),我们可以在特征空间中可视化目前占据的体积,约为(3 – 1) x (4 – 2) = 2 x 2 = 4平方单位:

图 4.5:二维特征空间中的数据

图 4.5:二维特征空间中的数据

假设数据集包含相同数量的点,但每个样本增加了一个特征(z坐标)。现在,占据的数据体积大约是2 x 2 x 2 = 8立方单位。因此,我们现在有相同数量的样本,但数据集所包围的空间变大了。因此,数据在可用空间中的相对体积变小,变得更加稀疏。这就是维度的诅咒;随着我们增加可用特征的数量,数据的稀疏性增加,从而使得统计相关性更难以识别。回到我们创建一个视频游戏机器人与人类玩家对战的例子,我们有 16 个特征,这些特征是不同类型的混合:位置、速度、能量道具和生命值。根据每个特征的可能取值范围以及每个特征对数据集的方差,这些数据可能会变得非常稀疏。即使在受限的吃豆人世界中,每个特征的潜在方差也可能非常大,有些特征的方差甚至比其他特征大得多。

因此,在不处理数据集稀疏性的情况下,额外的特征提供了更多的信息,但可能无法提高我们机器学习模型的性能,因为统计相关性更难以识别。我们希望做的是保留额外特征提供的有用信息,同时最小化稀疏性的负面影响。这正是降维技术的设计目的,这些技术在提高机器学习模型性能方面可以非常强大。

在本章中,我们将讨论多种不同的降维技术,并将更加详细地介绍其中最重要和最有用的方法——主成分分析(PCA),并提供一个例子。

降维技术概述

任何降维技术的目标都是管理数据集的稀疏性,同时保留其中提供的有用信息。在我们的分类案例中,降维通常作为一个重要的预处理步骤,在实际分类之前进行。大多数降维技术的目标是通过特征投影的过程来完成这一任务,将数据从高维空间调整到低维空间,以去除数据的稀疏性。同样,为了可视化投影过程,考虑一个三维空间中的球体。我们可以将球体投影到低维的二维空间,得到一个圆形,尽管丢失了一些信息(z坐标的值),但保留了描述其原始形状的大部分信息。我们仍然知道球体的原点、半径和流形(轮廓),并且仍然非常清楚它是一个圆。因此,根据我们要解决的问题,我们可能已经在保留重要信息的同时降低了维度:

图 4.6:3D 球体投影到 2D 空间

图 4.6:3D 球体投影到 2D 空间

通过在数据集上进行降维预处理,可以获得的第二个好处是改进的计算性能。由于数据已经被投影到低维空间,它将包含更少的但可能更强大的特征。特征较少意味着,在后续的分类或回归阶段,处理的数据集规模显著更小。这可能会减少分类/回归所需的系统资源和处理时间,并且在某些情况下,降维技术还可以直接用于完成分析。

这个类比还引入了降维中的一个重要考虑因素。我们总是在尝试平衡由于投影到低维空间而导致的信息丢失,同时减少数据的稀疏性。根据问题的性质和使用的数据集,正确的平衡可能会显现出来,并且相对简单。在一些应用中,这个决策可能依赖于额外验证方法的结果,比如交叉验证(特别是在监督学习问题中)或领域专家的评估。在这种情况下,交叉验证指的是将数据集的滚动部分划分出来进行测试,而其余部分作为训练集,直到数据集的所有部分都被使用。此方法有助于减少机器学习问题中的偏差。

我们喜欢以一种方式思考降维中的权衡:考虑在计算机上传输文件或图像时进行压缩。降维技术,如 PCA,本质上是将信息压缩到更小的尺寸以便传输的方法,在许多压缩方法中,压缩过程中会发生一些损失。有时,这些损失是可以接受的;例如,如果我们需要将一个 50 MB 的图像缩小到 5 MB 以进行传输,我们仍然可以看到图像的主要内容,但可能一些较小的背景细节会变得模糊不清。我们也不会期望能够从压缩后的版本恢复出原始图像的像素完美表示,但我们可以期待恢复时会有一些额外的伪影,例如模糊。

降维

降维技术在机器学习中有许多应用,因为提取数据集中的有用信息能够在许多机器学习问题中提供性能提升。它们在无监督学习中尤为有用,因为无监督学习方法的数据集不包含任何真实标签或目标。无监督学习中,训练环境被用来以适合解决问题的方式组织数据(例如,通过聚类进行分类),这通常是基于数据集中的最重要信息。降维提供了一种有效的提取重要信息的方法,并且由于我们可以使用多种不同的方法,因此回顾一些可用的选项是有益的:

  • 线性判别分析 (LDA):这是一种特别实用的技术,既可以用于分类,也可以用于降维。LDA 将在第七章主题建模中进行详细介绍。

  • 非负矩阵分解 (NMF):像许多降维技术一样,这依赖于线性代数的性质来减少数据集中的特征数。NMF 也将在第七章主题建模中进行详细介绍。

  • 奇异值分解 (SVD):这与 PCA(本章将详细介绍)有些相关,也是一个矩阵分解过程,与 NMF 没有太大区别。

  • 独立成分分析 (ICA):这与 SVD 和 PCA 有一些相似之处,但通过放宽数据为高斯分布的假设,使得非高斯数据也能被分离出来。

迄今为止所描述的每种方法都使用线性变换来减少数据在原始实现中的稀疏性。这些方法中的一些还有使用非线性核函数进行分离过程的变体,提供了以非线性方式减少稀疏性的能力。根据所使用的数据集,非线性核可能在从信号中提取最有用的信息时更为有效。

主成分分析(PCA)

如前所述,PCA 是一种常用且非常有效的降维技术,它通常是许多机器学习模型和技术的预处理阶段。因此,我们将把本书的这一部分专门用来详细探讨 PCA,而不仅仅是其他方法。PCA 通过将数据分离成一系列组件来减少数据集的稀疏性,其中每个组件代表数据中的一个信息源。正如其名称所示,PCA 生成的第一个组件,即主成分,包含了数据中大部分的信息或方差。主成分通常可以被认为是在均值之外,贡献最多的有趣信息。随着每个后续组件的生成,贡献的信息虽然减少,但压缩数据中的微妙之处也增多。如果我们将所有这些组件放在一起使用,那么 PCA 将没有任何好处,因为它会还原回原始数据集。为了澄清这个过程以及 PCA 返回的信息,我们将通过一个实际的例子,手动完成 PCA 的计算。但首先,我们必须复习一些执行 PCA 计算所需的基础统计概念。

均值

均值,或称平均值,就是将所有值相加并除以数据集中的值的数量。

标准差

通常被称为数据的分布,并与方差相关,标准差是衡量数据有多少接近均值的一个指标。在正态分布的数据集中,约 68%的数据位于均值的一个标准差内(即在(均值 - 1std)到(均值 + 1std)之间,如果数据是正态分布的,你可以找到 68%的数据)。

方差和标准差之间的关系相当简单——方差是标准差的平方。

协方差

当标准差或方差是基于单一维度计算的数据的分布时,协方差则是一个维度(或特征)相对于另一个维度的方差。当计算某一维度相对于自身的协方差时,结果与简单计算该维度的方差相同。

协方差矩阵

协方差矩阵是数据集可能计算出的协方差值的矩阵表示。除了在数据探索中非常有用,协方差矩阵在执行数据集的主成分分析(PCA)时也是必需的。要确定一个特征相对于另一个特征的方差,我们只需在协方差矩阵中查找对应的值。在下图中,我们可以看到,在第 1 列第 2 行的值是特征或数据集 Y 相对于 X 的方差(cov(Y, X)))。我们还可以看到,协方差矩阵的对角线列包含了对相同特征或数据集计算的协方差值;例如,cov(X, X)。在这种情况下,值就是 X 的方差:

图 4.7:协方差矩阵

图 4.7:协方差矩阵

通常,每个协方差的确切值并不像查看协方差矩阵中每个协方差的大小和相对大小那样有趣。一个特征相对于另一个特征的协方差值很大,意味着一个特征相对于另一个特征的变化显著,而接近零的值则意味着变化很小。协方差中另一个值得注意的方面是其符号;正值表示随着一个特征的增加或减少,另一个特征也会增加或减少,而负协方差则表示两个特征相互背离,一个特征增加时另一个特征减少,反之亦然。

幸运的是,numpyscipy 提供了高效执行这些计算的函数。在接下来的练习中,我们将在 Python 中计算这些值。

练习 4.01:使用 pandas 库计算均值、标准差和方差

在这个练习中,我们将简要回顾如何使用 numpypandas Python 库计算一些基础的统计概念。在本练习中,我们将使用一个包含不同品种小麦种子测量数据的数据库,该数据集是通过 X 射线成像创建的。这个数据集可以在附带的源代码中找到,包含了来自三种不同小麦品种:Kama、Rosa 和 Canadian 的七个独立测量值(area Aperimeter Pcompactness Clength of kernel LKwidth of kernel WKasymmetry coefficient A_Coeflength of kernel groove LKG)。

注意

这个数据集来源于 archive.ics.uci.edu/ml/datasets/seed(UCI 机器学习库 [archive.ics.uci.edu/ml])。加利福尼亚大学尔湾分校信息与计算机科学学院。引用:贡献者感激地感谢波兰科学院农物理研究所(Institute of Agrophysics of the Polish Academy of Sciences in Lublin)对其工作的支持。数据集也可以从 packt.live/2RjpDxk 下载。

要执行的步骤如下:

  1. 导入pandasnumpymatplotlib包以供使用:

    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    
  2. 加载数据集并预览前五行数据:

    df = pd.read_csv('../Seed_Data.csv')
    df.head()
    

    输出如下:

    图 4.8:数据头

    图 4.8:数据头

  3. 我们只需要面积A和内核长度LK特征,因此删除其他列:

    df = df[['A', 'LK']]
    df.head()
    

    输出如下:

    图 4.9:清洗数据后的数据头

    图 4.9:清洗数据后的数据头

  4. 通过绘制ALK的值来可视化数据集:

    plt.figure(figsize=(10, 7))
    plt.scatter(df['A'], df['LK'])
    plt.xlabel('Area of Kernel')
    plt.ylabel('Length of Kernel')
    plt.title('Kernel Area versus Length')
    plt.show()
    

    输出如下:

    图 4.10:数据的绘图

    图 4.10:数据的绘图

  5. 使用pandas方法计算均值:

    df.mean()
    

    输出如下:

    A     14.847524
    LK     5.628533
    dtype: float64
    
  6. 使用numpy方法计算均值:

    np.mean(df.values, axis=0)
    

    输出如下:

    array([14.84752381,  5.62853333])
    
  7. 使用pandas方法计算标准差值:

    df.std()
    

    输出如下:

    A     2.909699
    LK    0.443063
    dtype: float64
    
  8. 使用numpy方法计算标准差值:

    np.std(df.values, axis=0)
    

    输出如下:

    array([2.90276331, 0.44200731])
    
  9. 使用pandas方法计算方差值:

    df.var()
    

    输出如下:

    A     8.466351
    LK    0.196305
    dtype: float64
    
  10. 使用numpy方法计算方差值:

    np.var(df.values, axis=0)
    

    输出如下:

    array([8.42603482, 0.19537046])
    
  11. 使用pandas方法计算协方差矩阵:

    df.cov()
    

    输出如下:

    图 4.11:使用 pandas 方法的协方差矩阵

    图 4.11:使用 pandas 方法的协方差矩阵

  12. 使用numpy方法计算协方差矩阵:

    np.cov(df.values.T)
    

    输出如下:

    array([[8.46635078, 1.22470367],
           [1.22470367, 0.19630525]])
    

现在我们已经知道如何计算基础的统计值,我们将把注意力转向 PCA 的其他组成部分。

注意

要查看本节的源代码,请参考packt.live/2BHiLFz

你也可以在线运行此示例,访问packt.live/2O80UtW

特征值和特征向量

特征值和特征向量是物理学和工程学领域中非常重要的数学概念,它们也是计算数据集主成分的最后步骤。特征值和特征向量的准确数学定义超出了本书的范围,因为它涉及较多的内容,并且需要对线性代数有一定的理解。任何一个大小为 n x n的方阵A,都存在一个形状为n x 1的向量x,使得它满足以下关系:

图 4.12:表示 PCA 的方程

图 4.12:表示 PCA 的方程

这里,术语C:\Users\user\Downloads\B15923_04_Formula_01.PNG是一个数值,表示特征值,而x表示相应的特征向量。N表示矩阵A的阶数。矩阵A将有n个特征值和特征向量。我们不深入探讨 PCA 的数学细节,接下来让我们看一下另一种表示前述方程的方式,如下所示:

图 4.13:表示 PCA 的替代方程式

图 4.13:表示 PCA 的替代方程式

简单来说,将其应用于 PCA,我们可以推导出以下结论:

  • 协方差矩阵 (A):如前一节所述,在进行特征值分解之前,矩阵A应该是方阵。由于在我们的数据集中,行数大于列数(假设数据集的形状是m x n,其中m是行数,n是列数),因此我们无法直接进行特征值分解。为了对矩形矩阵执行特征值分解,首先需要通过计算其协方差矩阵将其转换为方阵。协方差矩阵的形状为n x n,即它是一个n阶的方阵。

  • 特征向量 (U) 是对数据集贡献信息的组成部分,如本节第一段中所述的主成分称为特征向量。每个特征向量描述数据集中的某些变化性。这种变化性由相应的特征值表示。特征值越大,贡献越大。特征向量矩阵的形状为n x n

  • 特征值 (C:\Users\user\Downloads\B15923_04_Formula_02.PNG) 是描述每个特征向量对数据集贡献大小的单个值。如前所述,描述最大贡献的特征向量称为主成分,因此具有最大的特征值。相应地,具有最小特征值的特征向量对数据的方差或信息贡献最小。特征值是对角矩阵,其中对角元素表示特征值。

请注意,即使是数据的协方差矩阵的 SVD 也会产生特征值分解,我们将在练习 4.04中看到,scikit-learn PCA。然而,SVD 使用不同的过程来分解矩阵。请记住,特征值分解仅适用于方阵,而 SVD 也可以应用于矩阵。

注意

方阵:方阵的行数和列数相等。方阵的行数被称为矩阵的阶数。行列数不等的矩阵称为矩形矩阵。

对角矩阵:对角矩阵的非对角元素全为零。

练习 4.02:计算特征值和特征向量

如前所述,手动推导和计算特征值和特征向量有些复杂,超出了本书的范围。幸运的是,numpy为我们提供了计算这些值的所有功能。再次,我们将使用 Seeds 数据集作为示例:

注意

该数据集来源于archive.ics.uci.edu/ml/datasets/seeds。(UCI 机器学习库 [archive.ics.uci.edu/ml]。加利福尼亚州尔湾:加利福尼亚大学信息与计算机科学学院。)引用:贡献者由衷感谢波兰科学院农业物理研究所(位于卢布林)对其工作的支持。该数据集还可以从packt.live/34gOQ0B下载。

  1. 导入 pandasnumpy 包:

    import pandas as pd
    import numpy as np
    
  2. 加载数据集:

    df = pd.read_csv('../Seed_Data.csv')
    df.head()
    

    输出如下:

    图 4.14:数据集的前五行

    图 4.14:数据集的前五行

  3. 再次,我们只需要 ALK 特征,所以删除其他列:

    df = df[['A', 'LK']]
    df.head()
    

    输出如下:

    图 4.15:核特征的面积和长度

    图 4.15:核特征的面积和长度

  4. numpy 的线性代数模块中,使用 eig 函数计算 eigenvalueseigenvectors 特征向量。注意此处使用了数据的协方差矩阵:

    eigenvalues, eigenvectors = np.linalg.eig(np.cov(df.T))
    

    注意

    numpy 函数 cov 可以用来计算数据的协方差矩阵。它产生一个与数据特征数相等的方阵。

  5. 看一下特征值;我们可以看到第一个值是最大的,所以第一个特征向量贡献了最多的信息:

    eigenvalues
    

    输出如下:

    array([8.64390408, 0.01875194])
    
  6. 观察特征值作为数据集总方差百分比是很方便的。我们将使用一个累积和函数来实现这一点:

    eigenvalues = np.cumsum(eigenvalues)
    eigenvalues
    

    输出如下:

    array([8.64390408, 8.66265602])
    
  7. 除以最后一个或最大值,将特征值转换为百分比:

    eigenvalues /= eigenvalues.max()
    eigenvalues
    

    输出如下:

    array([0.99783531, 1.])
    

    我们可以看到,第一个(或主)成分包含了数据中 99%的变化,因此,包含了大部分信息。

  8. 现在,让我们来看看 eigenvectors

    eigenvectors
    

    输出的部分如下:

    array([[ 0.98965371, -0.14347657],
           [ 0.14347657,  0.98965371]])
    
  9. 确认特征向量矩阵的形状是(n x n)格式;即 2 x 2

    eigenvectors.shape
    

    输出如下:

    (2, 2)
    
  10. 所以,从特征值中,我们看到主成分是第一个特征向量。看看第一个特征向量的值:

    P = eigenvectors[0]
    P
    

    输出如下:

    array([0.98965371, -0.14347657])
    

我们已将数据集分解为主成分,并通过特征向量进一步降低了数据的维度。

注意

要访问此特定部分的源代码,请参阅packt.live/3e5x3N3

你也可以在线运行这个例子,访问packt.live/3f5Skrk

在后续示例中,我们将考虑 PCA,并将此技术应用于一个示例数据集。

PCA 过程

现在,我们已经准备好所有的部分来完成 PCA,以减少数据集中的维度。

完成 PCA 的整体算法如下:

  1. 导入所需的 Python 包(numpypandas)。

  2. 加载整个数据集。

  3. 从可用的数据中,选择您希望在降维中使用的特征。

    注意

    如果数据集中的特征之间存在显著的规模差异;例如,一个特征的值范围在 0 和 1 之间,另一个特征的值范围在 100 和 1000 之间,您可能需要对其中一个特征进行归一化,因为这种量级差异可能会消除较小特征的影响。在这种情况下,您可能需要将较大的特征除以其最大值。

    作为示例,看看这个:

    x1 = [0.1, 0.23, 0.54, 0.76, 0.78]

    x2 = [121, 125, 167, 104, 192]

    # 将 x2 归一化到 0 到 1 之间

    x2 = (x2-np.min(x2)) / (np.max(x2)-np.min(x2))

  4. 计算所选(并可能归一化后的)数据的协方差矩阵。

  5. 计算协方差矩阵的特征值和特征向量。

  6. 将特征值(及其对应的特征向量)从大到小排序。

  7. 计算特征值占数据集中总方差的百分比。

  8. 选择特征值和对应特征向量的数量。它们将被要求组成一个预定值的最小成分方差。

    注意

    在此阶段,排序后的特征值表示数据集总方差的百分比。因此,我们可以使用这些值来选择所需的特征向量数量,无论是用于解决问题,还是充分减少应用于模型的数据集的大小。例如,假设我们要求在 PCA 的输出中至少占有 90% 的方差。我们就会选择那些至少占有 90% 方差的特征值(及其对应的特征向量)。

  9. 将数据集与选定的特征向量相乘,您就完成了 PCA,从而减少了表示数据的特征数量。

  10. 绘制结果。

在进行下一个练习之前,请注意,转置是线性代数中的一个术语,指的是将矩阵的行和列互换。假设我们

有一个矩阵X=[1, 2, 3],那么,X的转置就是C:\Users\user\Downloads\B15923_04_Formula_03.PNG

练习 4.03:手动执行 PCA

对于本练习,我们将手动完成 PCA,再次使用 Seeds 数据集。对于这个例子,我们希望足够减少数据集中的维度,以包含至少 75% 的可用方差:

注意

该数据集来源于archive.ics.uci.edu/ml/datasets/seeds。 (UCI 机器学习数据集 [archive.ics.uci.edu/ml]。加利福尼亚大学,信息与计算机科学学院。)引用:贡献者感谢波兰科学院农物理研究所对其工作的支持。该数据集也可以从packt.live/2Xe7cxO下载。

  1. 导入pandasnumpy包:

    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    
  2. 加载数据集:

    df = pd.read_csv('../Seed_Data.csv')
    df.head()
    

    输出如下:

    图 4.16:数据集的前五行

    图 4.16:数据集的前五行

  3. 由于我们只需要ALK特征,因此删除其他列。在此示例中,我们没有对所选数据集进行归一化:

    df = df[['A', 'LK']]
    df.head()
    

    输出如下:

    图 4.17:核特征的面积和长度

    图 4.17:核特征的面积和长度

  4. 计算所选数据的协方差矩阵。请注意,我们需要对协方差矩阵进行转置,以确保其基于特征数量(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
    

    输出如下:

    array([[8.46635078, 1.22470367],
           [1.22470367, 0.19630525]])
    
  5. 计算协方差矩阵的特征向量和特征值。同样,使用full_matrices函数参数:

    eigenvectors, eigenvalues, _ = np.linalg\
                                   .svd(data, full_matrices=False)
    
  6. 特征值按从大到小排序返回:

    eigenvalues
    

    输出如下:

    array([8.64390408, 0.01875194])
    
  7. 特征向量作为矩阵返回:

    eigenvectors
    

    输出如下:

    array([[-0.98965371, -0.14347657],
           [-0.14347657,  0.98965371]])
    
  8. 计算数据集内方差占比的特征值:

    eigenvalues = np.cumsum(eigenvalues)
    eigenvalues /= eigenvalues.max()
    eigenvalues
    

    输出如下:

    array([0\. 99783531, 1\.        ])
    
  9. 根据练习的介绍,我们需要描述至少包含 75% 可用方差的数据。如步骤 7所述,主成分包含了 99% 的可用方差。因此,我们只需要数据集中的主成分。什么是主成分?让我们来看看:

    P = eigenvectors[0]
    P
    

    输出如下:

    array([-0.98965371, -0.14347657])
    

    现在,我们可以应用降维过程。执行主成分与数据集转置的矩阵乘法。

    注意

    降维过程是将所选特征向量与需要转换的数据进行矩阵乘法。

  10. 如果不对df.values矩阵进行转置,将无法进行乘法运算:

    x_t_p = P.dot(df.values.T)
    x_t_p
    

    输出的一部分如下:

    图 4.18:矩阵乘法的结果

    图 4.18:矩阵乘法的结果

    注意

    为了执行矩阵乘法,需要对数据集进行转置,因为矩阵的内维度必须相同才能进行矩阵乘法。对于A(“A 点 B”)有效,A必须具有m x n的形状,B必须具有n x p的形状。在此示例中,AB的内维度均为n。得到的矩阵将具有m x p的维度。

    在以下示例中,PCA 的输出是一个单列的 210 样本数据集。因此,我们已将初始数据集的大小减少了一半,涵盖了大约 99%的数据方差。

  11. 绘制主成分的值:

    plt.figure(figsize=(10, 7))
    plt.plot(x_t_p)
    plt.title('Principal Component of Selected Seeds Dataset')
    plt.xlabel('Sample')
    plt.ylabel('Component Value')
    plt.show() 
    

    输出结果如下,显示了 210 样本数据集的新组件值,正如前一步中打印出来的所示:

    图 4.19:使用手动 PCA 转换的 Seeds 数据集

图 4.19:使用手动 PCA 转换的 Seeds 数据集

在本练习中,我们仅计算了数据集的协方差矩阵,而未事先对数据集进行任何变换。如果两个特征的均值和标准差大致相同,这是完全可以的。然而,如果一个特征的值远大于另一个特征(并且均值有所不同),那么在分解成组件时,该特征可能会主导另一个特征。这可能会导致小特征所提供的信息完全丢失。一个简单的归一化技术是在计算协方差矩阵之前从特征中减去各自的均值,从而使数据集以零为中心。我们将在练习 4.05使用手动 PCA 可视化方差减少中演示这一点。

注意

若要访问此特定部分的源代码,请参考packt.live/3fa8X57

你也可以在packt.live/3iOvg2P上在线运行此示例。

练习 4.04:scikit-learn PCA

通常,我们不会手动完成 PCA,特别是当 scikit-learn 提供了一个优化过的 API,包含方便的方法,可以让我们轻松地将数据转换到降维空间并从中还原。在本练习中,我们将更详细地使用 scikit-learn 的 PCA 来处理 Seeds 数据集:

注意

该数据集来源于archive.ics.uci.edu/ml/datasets/seeds。(UCI 机器学习库[archive.ics.uci.edu/ml]。加利福尼亚大学欧文分校,信息与计算机科学学院。)引用:贡献者在此感谢波兰科学院农物理研究所(位于卢布林)对其工作的支持。该数据集也可以从packt.live/2Ri6VGk下载。

  1. sklearn包中导入pandasnumpyPCA模块:

    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.decomposition import PCA
    
  2. 加载数据集:

    df = pd.read_csv('../Seed_Data.csv')
    df.head()
    

    输出结果如下:

    图 4.20:数据集的前五行

    图 4.20:数据集的前五行

  3. 再次说明,我们只需要ALK特征,因此需要删除其他列。在此示例中,我们不会对选择的数据集进行归一化:

    df = df[['A', 'LK']]
    df.head()
    

    输出结果如下:

    图 4.21:种子特征的面积和长度

    图 4.21:种子特征的面积和长度

  4. 将数据拟合到 scikit-learn 的 PCA 模型中,使用的是协方差数据。使用我们在此处的默认值,将为数据集生成最大数量的特征值和特征向量:

    model = PCA()
    model.fit(df.values)
    

    输出如下:

    PCA(copy=True, iterated_power='auto', n_components=None, 
        random_state=None, 
        svd_solver='auto', tol=0.0, whiten=False)
    

    这里,copy表示在应用任何计算之前,数据适配到模型中时会被复制。如果将copy设置为False,则传递给 PCA 的数据将被覆盖。iterated_power表示ALK特征是要保留的主成分的数量。默认值为None,它选择的组件数为样本数和特征数中较小值减一。random_state允许用户为 SVD 求解器使用的随机数生成器指定种子。svd_solver指定在 PCA 过程中使用的 SVD 求解器。tol是 SVD 求解器使用的容忍度值。使用whiten时,组件向量会乘以样本数的平方根。这会移除一些信息,但可以提高某些下游估计器的性能。

  5. 由组件(特征值)描述的方差百分比包含在explained_variance_ratio_属性中。显示explained_variance_ratio_的值:

    model.explained_variance_ratio_
    

    输出如下:

    array([0.99783531, 0.00216469])
    
  6. 通过components_属性显示特征向量:

    model.components_
    

    输出如下:

    array([[0.98965371, 0.14347657]])
    
  7. 在本次练习中,我们将仅使用主成分,因此我们将创建一个新的PCA模型,这次指定组件数(特征向量/特征值)为1

    model = PCA(n_components=1)
    
  8. 使用fit方法将covariance矩阵拟合到PCA模型中,并生成相应的特征值/特征向量:

    model.fit(df.values)
    

    输出如下:

    PCA(copy=True, iterated_power='auto', n_components=1, 
        random_state=None,
        svd_solver='auto', tol=0.0, whiten=False)
    

    该模型是使用多个默认参数拟合的,如前面输出所示。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可以在声明 PCA 对象时传递,例如model = PCA(n_components=1)

  9. 使用components_属性显示特征向量:

    model.components_
    

    输出如下:

    array([[0.98965371, 0.14347657]])
    
  10. 使用模型的fit_transform方法将 Seeds 数据集转换到低维空间。将转换后的值赋给data_t变量:

    data_t = model.fit_transform(df.values)
    
  11. 绘制转换后的值以可视化结果:

    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')
    plt.show()
    

    输出如下:

    图 4.22:使用 scikit-learn PCA 转换后的 seeds 数据集

图 4.22:使用 scikit-learn PCA 转换后的 seeds 数据集

你刚刚使用手动 PCA 和 scikit-learn API 将 Seeds 数据集的维度降低了。但是,在我们过早庆祝之前,请比较图 4.19图 4.22;这两张图应该是相同的,对吧?我们使用了两种不同的方法在相同的数据集上完成 PCA,并且都选择了主成分。在接下来的活动中,我们将探讨为什么这两者之间会有差异。

注意

要访问此特定章节的源代码,请参考packt.live/2ZQV85c

你也可以在packt.live/2VSG99R上在线运行此示例。

活动 4.01:手动 PCA 与 scikit-learn

假设你被要求将一个旧应用程序中手动执行 PCA 的遗留代码移植到一个使用 scikit-learn 的新应用程序中。在移植过程中,你注意到手动 PCA 和移植后输出之间有一些差异。为什么我们的手动 PCA 和 scikit-learn 输出之间会有差异?比较两种方法在 Seeds 数据集上的结果。它们之间的差异是什么?

本活动的目的是通过从头开始实现 PCA,深入理解其工作原理,然后将你的实现与 scikit-learn 中包含的实现进行比较,以查看是否存在重大差异:

注意

该数据集来自archive.ics.uci.edu/ml/datasets/seeds。 (UCI 机器学习库[archive.ics.uci.edu/ml]。加利福尼亚大学欧文分校信息与计算机科学学院)。引用:贡献者感谢波兰科学院农物理研究所(位于卢布林)对他们工作的支持。该数据集也可以从packt.live/2JIH1qT下载。

  1. 导入pandasnumpymatplotlib绘图库,以及 scikit-learn 的PCA模型。

  2. 加载数据集,并根据之前的练习选择仅包含核心特征的数据。显示数据的前五行。

  3. 计算数据的covariance矩阵。

  4. 使用 scikit-learn API 并仅使用第一个主成分转换数据。将转换后的数据存储在sklearn_pca变量中。

  5. 使用手动 PCA 并仅使用第一个主成分来转换数据。将转换后的数据存储在manual_pca变量中。

  6. 注意

  7. 请注意,这两张图几乎看起来相同,但有一些关键区别。这些区别是什么?

  8. 查看是否可以修改手动 PCA 过程的输出,使其与 scikit-learn 版本一致。

    这张图将展示两种方法完成的降维结果实际上是相同的。

    提示:scikit-learn 的 API 在变换前会减去数据的均值。

sklearn_pcamanual_pca的值绘制在同一图表上,以可视化差异。

图 4.23:预期的最终图

注意到这两张图几乎完全相同,但有一些关键的区别。这些区别是什么?

图 4.23:预期的最终图

这张图将演示两种方法完成的降维结果实际上是相同的。

注意

本活动的解决方案可以在第 437 页找到。

恢复压缩后的数据集

现在我们已经涵盖了将数据集转换为低维空间的几个不同示例,我们应该考虑这种转换对数据产生了什么实际影响。使用 PCA 作为预处理步骤来减少数据中的特征数量将导致部分方差被丢弃。以下练习将引导我们完成这一过程,让我们看到转换丢弃了多少信息。

练习 4.05:使用手动 PCA 可视化方差减少

降维的一个最重要方面是理解由于降维过程从数据集中去除了多少信息。去除过多的信息将给后续处理带来额外的挑战,而去除的信息不足则违背了 PCA 或其他技术的目的。在本练习中,我们将可视化通过 PCA 从 Seeds 数据集中去除的信息量:

注意

图 4.24:核特征

  1. 导入pandasnumpymatplotlib绘图库:

    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    
  2. 从 Seeds 数据集中读取wheat kernel特征:

    df = pd.read_csv('../Seed_Data.csv')[['A', 'LK']]
    df.head()
    

    输出如下:

    图 4.24:核特征

    该数据集来源于archive.ics.uci.edu/ml/datasets/seeds。 (UCI 机器学习库[archive.ics.uci.edu/ml]。加利福尼亚大学尔湾分校信息与计算机科学学院。)引用:贡献者感谢波兰科学院农业物理研究所(位于卢布林)对他们工作的支持。该数据集也可以从packt.live/2RhnDFS下载。

    预期输出:在本活动结束时,您将使用手动 PCA 方法和 scikit-learn PCA 方法分别转换数据集。您将生成一个图表,展示这两个降维后的数据集实际上是相同的,并且您应该能够理解它们最初看起来非常不同的原因。最终图应类似于以下内容:

  3. 通过减去各自的均值,将数据集居中于零。

    means = np.mean(df.values, axis=0)
    means
    

    输出如下:

    array([14.84752381,  5.62853333])
    
  4. 要计算数据并打印结果,使用以下代码:

    data = df.values - means
    data
    

    输出的一部分如下:

    图 4.25:输出的一部分

    图 4.25:输出的一部分

  5. 使用手动 PCA 对数据进行转换,基于第一个主成分:

    eigenvectors, eigenvalues, _ = np.linalg.svd(np.cov(data.T), \
                                   full_matrices=False)
    P = eigenvectors[0]
    P
    

    输出结果如下:

    array([-0.98965371, -0.14347657])
    
  6. 通过将之前的P与数据矩阵的转置版本进行点积,将数据转换为低维空间:

    data_transformed = P.dot(data.T)
    
  7. 重新塑造主成分以供后续使用:

    P = P.reshape((-1, 1))
    
  8. 要计算降维数据集的逆转换,我们需要将选定的特征向量恢复到高维空间。为此,我们将对矩阵进行求逆。矩阵求逆是另一种线性代数技术,我们将只简要介绍。一个方阵,A,如果存在另一个方阵B,并且AB=BA=I,其中I是一个特殊的矩阵,称为单位矩阵,它的对角线只有值1

    P_transformed = np.linalg.pinv(P)
    P_transformed
    

    输出结果如下:

    array([[-0.98965371, -0.14347657]])
    
  9. 准备转换后的数据以供矩阵乘法使用:

    data_transformed = data_transformed.reshape((-1, 1))
    
  10. 计算降维数据的逆转换并绘制结果,以可视化去除数据方差的效果:

    data_restored = data_transformed.dot(P_transformed)
    data_restored
    

    输出的一部分如下:

    图 4.26:降维数据的逆转换

    图 4.26:降维数据的逆转换

  11. means数组添加回转换后的数据:

    data_restored += means
    
  12. 通过绘制原始数据集和转换后数据集的图形来可视化结果:

    plt.figure(figsize=(10, 7))
    plt.plot(data_restored[:,0], data_restored[:,1], \
             linestyle=':', label='PCA restoration')
    plt.scatter(df['A'], df['LK'], marker='*', label='Original')
    plt.legend()
    plt.xlabel('Area of Kernel')
    plt.ylabel('Length of Kernel')
    plt.title('Inverse transform after removing variance')
    plt.show()
    

    输出结果如下:

    图 4.27:去除方差后的逆转换

    图 4.27:去除方差后的逆转换

  13. 该数据集只有两个变异成分。如果我们不移除任何成分,逆转换的结果会是什么?再次将数据转换为低维空间,但这次使用所有的特征向量:

    P = eigenvectors
    data_transformed = P.dot(data.T)
    
  14. 转置data_transformed,以便将其放入适合矩阵乘法的正确形状:

    data_transformed = data_transformed.T
    
  15. 现在,将数据恢复到高维空间:

    data_restored = data_transformed.dot(P)
    data_restored
    

    输出的一部分如下:

    图 4.28:恢复的数据

    图 4.28:恢复的数据

  16. 将均值添加回恢复的数据:

    data_restored += means
    
  17. 在原始数据集的背景下可视化恢复的数据:

    plt.figure(figsize=(10, 7))
    plt.scatter(data_restored[:,0], data_restored[:,1], \
                marker='d', label='PCA restoration', c='k')
    plt.scatter(df['A'], df['LK'], marker='o', \
                label='Original', c='#1f77b4')
    plt.legend()
    plt.xlabel('Area of Kernel')
    plt.ylabel('Length of Kernel')
    plt.title('Inverse transform after removing variance')
    plt.show()
    

    输出结果如下:

    图 4.29:去除方差后的逆转换

图 4.29:去除方差后的逆转换

如果我们比较在本次练习中生成的两个图,我们可以看到 PCA 降维后的结果,恢复的数据集基本上是两组特征之间的负线性趋势线。我们可以将其与从所有可用成分恢复的数据集进行比较,在该数据集中,我们已经整体重建了原始数据集。

注意

要访问此特定部分的源代码,请参考packt.live/38EztBu

你还可以在packt.live/3f8LDVC在线运行这个示例。

练习 4.06:使用 scikit-learn 可视化方差减少

在这个练习中,我们将再次可视化数据集降维的效果;然而,这次我们将使用 scikit-learn API。由于 scikit-learn 模型的强大和简便性,这是你在实际应用中常用的方法:

注意

该数据集来源于archive.ics.uci.edu/ml/datasets/seeds。 (UCI 机器学习库 [archive.ics.uci.edu/ml]。美国加利福尼亚州欧文市:加利福尼亚大学信息与计算机科学学院。) 引用:贡献者衷心感谢波兰科学院农业物理研究所(位于卢布林)对其工作的支持。该数据集也可以从packt.live/3bVlJm4下载。

  1. 导入 pandasnumpymatplotlib 绘图库和 scikit-learn 中的 PCA 模型:

    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.decomposition import PCA
    
  2. 从 Seeds 数据集中读取 Wheat Kernel 特征:

    df = pd.read_csv('../Seed_Data.csv')[['A', 'LK']]
    df.head()
    

    输出如下:

    图 4.30:来自 Seeds 数据集的小麦种子特征

    图 4.30:来自 Seeds 数据集的小麦种子特征

  3. 使用 scikit-learn API 基于第一个主成分对数据进行变换:

    model = PCA(n_components=1)
    data_p = model.fit_transform(df.values)
    
  4. 计算降维数据的逆变换并绘制结果,以可视化从数据中去除方差的效果:

    data = model.inverse_transform(data_p)
    plt.figure(figsize=(10, 7))
    plt.plot(data[:,0], data[:,1], linestyle=':', \
             label='PCA restoration')
    plt.scatter(df['A'], df['LK'], marker='*', label='Original')
    plt.legend()
    plt.xlabel('Area of Kernel')
    plt.ylabel('Length of Kernel')
    plt.title('Inverse transform after removing variance')
    plt.show()
    

    输出如下:

    图 4.31:去除方差后的逆变换

    图 4.31:去除方差后的逆变换

  5. 该数据集中只有两个变异成分。如果我们不移除任何成分,那么逆变换的结果会是什么?我们通过计算逆变换并观察在不移除任何成分的情况下结果如何变化来找出答案:

    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['A'], df['LK'], marker='o', \
                label='Original', c='#1f77b4')
    plt.legend()
    plt.xlabel('Area of Kernel')
    plt.ylabel('Length of Kernel')
    plt.title('Inverse transform after removing variance')
    plt.show()
    

    输出如下:

    图 4.32:去除方差后的逆变换

图 4.32:去除方差后的逆变换

正如我们在这里看到的,如果我们不移除 PCA 中的任何成分,进行逆变换时它将重建原始数据。我们已经演示了从数据集中去除信息的效果,并展示了使用所有可用特征向量重建原始数据的能力。

注意

要访问此特定部分的源代码,请参考packt.live/2O362zv

你还可以在packt.live/3fdWYDU在线运行这个示例。

先前的练习指定了使用 PCA 将维度减少到两维,部分目的是为了便于结果的可视化。然而,我们可以使用 PCA 将维度减少到小于原始数据集的任何值。以下示例演示了如何使用 PCA 将数据集减少到三维,从而实现可视化。

练习 4.07:在 Matplotlib 中绘制 3D 图

matplotlib中创建 3D 散点图,遗憾的是不像提供一系列(x, y, z)坐标那样简单。在本练习中,我们将使用种子数据集进行一个简单的 3D 绘图示例:

注意

这个数据集来自于archive.ics.uci.edu/ml/datasets/seeds。 (UCI 机器学习库[archive.ics.uci.edu/ml]。美国加利福尼亚州欧文市:加利福尼亚大学信息与计算机科学学院。)

引用:贡献者感谢波兰科学院农物理研究所(位于卢布林)对他们工作的支持。该数据集也可以从packt.live/3c2tAhT下载。

  1. 导入pandasmatplotlib。为了启用 3D 绘图,您还需要导入Axes3D

    from mpl_toolkits.mplot3d import Axes3D
    import pandas as pd
    import matplotlib.pyplot as plt
    
  2. 读取数据集并选择ALKC列:

    df = pd.read_csv('../Seed_Data.csv')[['A', 'LK', 'C']]
    df.head()
    

    输出结果如下:

    图 4.33:数据的前五行

    图 4.33:数据的前五行

  3. 在三维空间中绘制数据,并使用projection='3d'参数与add_subplot方法一起创建 3D 图:

    fig = plt.figure(figsize=(10, 7))
    # Where Axes3D is required
    ax = fig.add_subplot(111, projection='3d')
    ax.scatter(df['A'], df['LK'], df['C'])
    ax.set_xlabel('Area of Kernel')
    ax.set_ylabel('Length of Kernel')
    ax.set_zlabel('Compactness of Kernel')
    ax.set_title('Expanded Seeds Dataset')
    plt.show()
    

    绘图将如下所示:

    图 4.34:扩展的种子数据集

图 4.34:扩展的种子数据集

注意

尽管Axes3D库已导入但未直接使用,它对于配置三维绘图窗口是必需的。如果省略了Axes3D的导入,projection='3d'参数将会返回一个AttributeError异常。

要访问此特定部分的源代码,请参考packt.live/3gPM1J9

您也可以在packt.live/2AFVXFr上在线运行此示例。

活动 4.02:使用扩展的种子数据集进行 PCA

在此活动中,我们将使用完整的种子数据集,观察在 PCA 分解中选择不同数量的组件的影响。此活动旨在模拟在实际问题中通常完成的过程,我们尝试确定选择的最佳组件数量,平衡维度减少和信息丢失的程度。因此,我们将使用 scikit-learn 的 PCA 模型:

注意

该数据集来自archive.ics.uci.edu/ml/datasets/seeds。 (UCI 机器学习库 [archive.ics.uci.edu/ml]。加利福尼亚州欧文市:加利福尼亚大学信息与计算机科学学院。)引用:贡献者感激波兰科学院农业物理研究所(位于卢布林)对其工作的支持。该数据集也可以从packt.live/3aPY0nj下载。

以下步骤将帮助你完成该活动:

  1. 导入pandasmatplotlib。为了启用三维绘图,你还需要导入Axes3D

  2. 读取数据集并选择Area of KernelLength of KernelCompactness of Kernel列。

  3. 在三维中绘制数据。

  4. 创建一个不指定成分数的PCA模型。

  5. 将模型拟合到数据集。

  6. 显示特征值或explained_variance_ratio_

  7. 我们希望减少数据集的维度,但仍保持至少 90% 的方差。为了保持 90% 方差,所需的最小成分数是多少?

  8. 创建一个新的PCA模型,这次指定需要保持至少 90% 方差的成分数。

  9. 使用新模型转换数据。

  10. 绘制转换后的数据。

  11. 将转换后的数据恢复到原始数据空间。

  12. 在一个子图中绘制恢复后的三维数据,在第二个子图中绘制原始数据,以可视化去除部分方差的效果:

    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.35:预期图

图 4.35:预期图

注意

本活动的解决方案可以在第 443 页找到。

总结

本章我们介绍了降维和 PCA 的过程。我们完成了一些练习,并掌握了通过提取数据中最重要的方差成分来减少数据集大小的技巧,使用了手动的 PCA 过程和 scikit-learn 提供的模型。在本章中,我们还将降维后的数据集恢复到原始数据空间,并观察去除方差对原始数据的影响。最后,我们讨论了 PCA 和其他降维过程的多种潜在应用。在下一章中,我们将介绍基于神经网络的自动编码器,并使用 Keras 包实现它们。

第六章:5. 自编码器

概述

在本章中,我们将讨论自编码器及其应用。我们将了解自编码器如何用于降维和去噪。我们将使用 Keras 框架实现一个人工神经网络和自编码器。到本章结束时,你将能够使用卷积神经网络实现一个自编码器模型。

引言

当我们将注意力转向自编码器时,我们将继续讨论降维技术。自编码器是一个特别有趣的研究领域,因为它提供了一种基于人工神经网络的监督学习方法,但在无监督的环境下使用。自编码器基于人工神经网络,是执行降维的极为有效的手段,同时也提供了额外的好处。随着数据、处理能力和网络连接的不断增加,自编码器在使用和研究上迎来了复兴,这种现象自 1980 年代末自编码器起源以来未曾见过。这与人工神经网络的研究是一致的,后者最早在 1960 年代被描述和实现为一种概念。目前,你只需进行简单的互联网搜索,就能发现神经网络的普及和强大功能。

自编码器可以与其他方法结合使用,如递归神经网络或长短期记忆网络LSTM)架构,用于去噪图像和生成人工数据样本,以预测数据序列。使用人工神经网络所带来的灵活性和强大功能,使得自编码器能够形成非常高效的数据表示,这些表示可以直接作为极其高效的搜索方法,或作为后续处理的特征向量使用。

考虑在图像去噪应用中使用自编码器,我们看到左边的图像(图 5.1),它受到了某些随机噪声的影响。我们可以使用专门训练的自编码器去除这些噪声,如下图右侧所示。通过学习如何去除噪声,自编码器还学会了如何编码组成图像的关键信息,并如何将这些信息解码(或重构)为更清晰的原始图像版本:

图 5.1:自编码器去噪

图 5.1:自编码器去噪

注意

这张图片已从www.freenzphotos.com/free-photos-of-bay-of-plenty/stormy-fishermen/在 CC0 授权下修改。

这个例子展示了自编码器的一个方面,使其在无监督学习(编码阶段)中非常有用,并且另一个方面使其在生成新图像时(解码阶段)也很有用。我们将进一步探讨自编码器的这两个有用阶段,并将自编码器的输出应用于 CIFAR-10 数据集的聚类(www.cs.toronto.edu/~kriz/cifar.html)。

下面是编码器和解码器的表示:

图 5.2:编码器/解码器表示

图 5.2:编码器/解码器表示

人工神经网络基础

由于自编码器基于人工神经网络,因此理解神经网络对理解自编码器也至关重要。本章的这一部分将简要回顾人工神经网络的基础知识。需要注意的是,神经网络有许多方面超出了本书的范围。神经网络的主题很容易填满,并且已经填满了许多书籍,这一部分并不打算成为该主题的详尽讨论。

如前所述,人工神经网络主要用于监督学习问题,在这些问题中,我们有一组输入信息,比如一系列图像,我们正在训练一个算法,将这些信息映射到期望的输出,比如类别或分类。以图 5.3中的 CIFAR-10 数据集为例,它包含 10 个不同类别(飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车)的图像,每个类别有 6000 张图像。

图 5.3:CIFAR-10 数据集

图 5.3:CIFAR-10 数据集

当神经网络用于监督学习时,图像被输入到网络中,网络的期望输出是对应类别标签的表示。

然后,网络将被训练以最大化其推断或预测给定图像的正确标签的能力。

注意

这张图来自www.cs.toronto.edu/~kriz/cifar.html,出自《从微小图像中学习多个特征层》(www.cs.toronto.edu/~kriz/learning-features-2009-TR.pdf),Alex Krizhevsky,2009 年。

神经元

人工神经网络得名于大脑中常见的生物神经网络。虽然这种类比的准确性确实值得商榷,但它是一个有用的隐喻,可以帮助我们理解人工神经网络的概念。与生物神经网络一样,神经元是所有神经网络的构建块,通过不同的配置连接多个神经元,形成更强大的结构。在图 5.4中,每个神经元由四个部分组成:一个输入值、一个可调权重(theta)、一个作用于权重与输入值乘积的激活函数,以及由此产生的输出值:

图 5.4:神经元的解剖结构

图 5.4:神经元的解剖结构

激活函数的选择取决于神经网络设计的目标,常见的函数包括tanhsigmoidlinearReLU(修正线性单元)。在本章中,我们将使用sigmoidReLU激活函数,因此我们可以稍微深入了解它们。

Sigmoid 函数

由于 sigmoid 激活函数能够将输入值转换为接近二进制的输出,因此它在神经网络分类中的输出中被广泛使用。Sigmoid 函数产生以下输出:

图 5.5:Sigmoid 函数的输出

图 5.5:Sigmoid 函数的输出

我们可以在前面的图中看到,随着x的增加,sigmoid 函数的输出渐近于 1(趋近但永远无法达到),而当x向负方向远离 0 时,输出渐近于 0。该函数常用于分类任务,因为它提供接近二进制的输出。

我们可以看到,sigmoid 具有渐近性质。由于这一特性,当输入值接近极限时,训练过程会变得缓慢(称为梯度消失)。这是训练中的瓶颈。因此,为了加速训练过程,神经网络的中间阶段使用修正线性单元ReLU)。然而,ReLU 也有一定的局限性,因为它存在死神经元和偏置问题。

修正线性单元(ReLU)

修正线性单元(ReLU)是一种非常有用的激活函数,通常在神经网络的中间阶段使用。简而言之,对于小于 0 的输入值,ReLU 将其输出为 0,而对于大于 0 的输入值,则返回实际值。

图 5.6:ReLU 的输出

图 5.6:ReLU 的输出

练习 5.01:建模人工神经网络中的神经元

在本练习中,我们将实际介绍如何在NumPy中以编程方式表示神经元,并使用sigmoid函数。我们将固定输入,调整可调权重,以研究其对神经元的影响。为了将这一框架与监督学习中的常见模型关联起来,我们在本练习中的方法与逻辑回归相同。执行以下步骤:

  1. 导入numpymatplotlib包:

    import numpy as np
    import matplotlib.pyplot as plt
    
  2. 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)

  3. 定义神经元的输入(x)和可调权重(theta)。在本例中,输入(x)将是-55之间线性间隔的100个数字。设置theta = 1

    theta = 1
    x = np.linspace(-5, 5, 100)
    x
    

    输出结果如下:

    图 5.7:打印输入

    图 5.7:打印输入

  4. 计算神经元的输出(y):

    y = sigmoid(x * theta)
    
  5. 绘制神经元输出与输入的关系图:

    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)
    plt.show()
    

    在以下输出中,您可以看到绘制的sigmoid函数——请注意,它通过原点并在0.5处交叉。

    图 5.8:神经元与输入的关系图

    图 5.8:神经元与输入的关系图

  6. 将可调参数theta设置为5,然后重新计算并存储神经元的输出:

    theta = 5
    y_2 = sigmoid(x * theta)
    
  7. 将可调参数theta改为0.2,然后重新计算并存储神经元的输出:

    theta = 0.2
    y_3 = sigmoid(x * theta)
    
  8. 在一个图表中绘制三条不同的神经元输出曲线(theta = 1theta = 5theta = 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)
    plt.show()
    

    输出结果如下:

    图 5.9:神经元的输出曲线

图 5.9:神经元的输出曲线

在本练习中,我们用sigmoid激活函数模拟了人工神经网络的基本构建块。我们可以看到,使用sigmoid函数增加了梯度的陡峭度,这意味着只有小的x值才会将输出推向接近 1 或 0。同样,减小theta会降低神经元对非零值的敏感度,导致需要极端的输入值才能将输出结果推向 0 或 1,从而调节神经元的输出。

注意

要访问此特定部分的源代码,请参考packt.live/2AE9Kwc

您还可以在packt.live/3e59UdK在线运行此示例。

练习 5.02:使用 ReLU 激活函数建模神经元

类似于 练习 5.01人工神经网络神经元建模,我们将再次建模一个网络,这次使用 ReLU 激活函数。在这个练习中,我们将为 ReLU 激活的神经元开发一系列响应曲线,并描述改变 theta 值对神经元输出的影响:

  1. 导入 numpymatplotlib

    import numpy as np
    import matplotlib.pyplot as plt
    
  2. 将 ReLU 激活函数定义为 Python 函数:

    def relu(x):
        return np.max((0, x))
    
  3. 定义神经元的输入(x)和可调权重(theta)。在这个示例中,输入(x)将是线性间隔在-55之间的 100 个数字。设置 theta = 1

    theta = 1
    x = np.linspace(-5, 5, 100)
    x
    

    输出如下:

    图 5.10:打印输入

    图 5.10:打印输入

  4. 计算输出(y):

    y = [relu(_x * theta) for _x in x]
    
  5. 绘制神经元输出与输入的关系图:

    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)
    plt.show()
    

    输出如下:

    图 5.11:神经元与输入的关系图

    图 5.11:神经元与输入的关系图

  6. 现在,设置 theta = 5,重新计算并保存神经元的输出:

    theta = 5
    y_2 = [relu(_x * theta) for _x in x]
    
  7. 现在,设置 theta = 0.2,重新计算并保存神经元的输出:

    theta = 0.2
    y_3 = [relu(_x * theta) for _x in x]
    
  8. 在同一张图表上绘制神经元的三条不同输出曲线(theta = 1theta = 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)
    plt.show()
    

    输出如下:

    图 5.12:神经元的三条输出曲线

图 5.12:神经元的三条输出曲线

在这个练习中,我们创建了一个基于 ReLU 的人工神经网络神经元模型。我们可以看到,这个神经元的输出与 sigmoid 激活函数的输出有很大不同。对于大于 0 的值,没有饱和区域,因为它仅仅返回函数的输入值。在负方向上,当输入小于 0 时,存在饱和区域,只有 0 会被返回。ReLU 函数是一种非常强大且常用的激活函数,在某些情况下,它比 sigmoid 函数更强大。ReLU 经常是首选的激活函数。

注意

要访问此特定章节的源代码,请参考 packt.live/2O5rnIn

你也可以在 packt.live/3iJ2Kzu 上在线运行此示例。

神经网络:架构定义

单个神经元在孤立状态下并不是特别有用;它提供了激活函数和调节输出的手段,但单个神经元的学习能力是有限的。当多个神经元结合并在网络结构中连接在一起时,神经元的功能就变得更加强大。通过使用多个不同的神经元并结合各个神经元的输出,可以建立更复杂的关系,并构建更强大的学习算法。在本节中,我们将简要讨论神经网络的结构,并使用 Keras 机器学习框架实现一个简单的神经网络(keras.io/)。Keras 是一个高层次的神经网络 API,基于现有的库(如 TensorFlow 或 Theano)之上。Keras 使得在低层框架之间切换变得容易,因为它提供的高层接口在不同的底层库之间保持不变。在本书中,我们将使用 TensorFlow 作为底层库。

以下是一个具有隐藏层的神经网络的简化表示:

图 5.13:神经网络的简化表示

图 5.13:神经网络的简化表示

上图展示了一个两层完全连接的神经网络结构。我们可以做出的第一个观察是,这个结构包含了大量的信息,并且具有高度的连接性,箭头表示了每个节点之间的连接。我们从图像的左侧开始,可以看到神经网络的输入值,由(x)值表示。在这个示例中,每个样本有m个输入值,因此,从x11 到x1m 的值代表这些输入值。每个样本的这些值被称为数据的属性或特征,并且每次仅输入一个样本到网络中。然后,这些值会与神经网络第一层对应的权重相乘 (C:\Users\user\Desktop\Lesson05\C12626_05_Formula_03.png),然后传入对应神经元的激活函数。这被称为前馈神经网络。在上图中,用来标识权重的符号是C:\Users\user\Desktop\Lesson05\C12626_05_Formula_04.png,其中i是权重所属的层,j是输入节点的编号(从顶部开始为 1),而k是后续层中该权重连接的节点。

观察第一层(也叫做隐藏层)输出与输出层输入之间的互联关系,我们可以看到有大量可调节的参数(权重),这些参数可以用来将输入映射到期望的输出。前图的网络代表了一个 n 类神经网络分类器,其中每个 n 个节点的输出表示输入属于相应类别的概率。

每一层都可以使用不同的激活函数,如 h1 和 h2 所示,因此允许不同的激活函数混合使用,例如第一层可以使用 ReLU,第二层可以使用 tanh,第三层可以使用 sigmoid。最终输出是通过将前一层输出与相应的权重相乘,并通过激活函数计算结果来获得的。

如果我们考虑第一层第一节点的输出,它可以通过将输入与相应的权重相乘,求和结果并通过激活函数来计算:

图 5.14:计算最后一个节点的输出

图 5.14:计算最后一个节点的输出

随着输入和输出之间的层数增加,我们增加了网络的深度。深度的增加意味着可训练参数的增加,以及网络描述数据内在关系的复杂度增加。此外,当我们在每一层添加更多神经元时,我们增加了神经网络的高度。通过增加神经元,网络对数据集的描述能力增强,同时可训练参数也增多。如果增加了过多的神经元,网络可能会记住数据集的内容,但无法对新样本进行泛化。构建神经网络的诀窍在于找到一个平衡点,既能充分描述数据内在关系,又不会过于复杂以至于记住训练样本。

练习 5.03:定义一个 Keras 模型

在本练习中,我们将使用 Keras 机器学习框架定义一个神经网络架构(类似于图 5.13),用于分类 CIFAR-10 数据集的图像。由于每个输入图像的大小为 32 x 32 像素,输入向量将由 32*32 = 1,024 个值组成。CIFAR-10 有 10 个类别,神经网络的输出将由 10 个值组成,每个值表示输入数据属于相应类别的概率。

注意

CIFAR-10 数据集 (www.cs.toronto.edu/~kriz/cifar.html) 由 60,000 张图像组成,涵盖 10 个类别。这 10 个类别包括飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车,每个类别有 6,000 张图像。通过前面的链接了解更多关于这个数据集的信息。

  1. 对于本练习,我们将需要 Keras 机器学习框架。如果您还没有安装 Keras 和 TensorFlow,请在 Jupyter 笔记本中使用conda进行安装:

    !conda install tensorflow keras
    

    或者,您也可以通过pip安装:

    !pip install tensorflow keras
    
  2. 我们将需要分别从keras.modelskeras.layers导入SequentialDense类。导入这些类:

    from keras.models import Sequential
    from keras.layers import Dense
    

    如前所述,输入层将接收 1,024 个值。第二层(层 1)将包含 500 个单元,并且由于该网络需要分类 10 个不同的类别,输出层将有 10 个单元。在 Keras 中,通过将有序的层列表传递给Sequential模型类来定义模型。

  3. 本示例使用了Dense层类,这是一个全连接神经网络层。第一层将使用 ReLU 激活函数,而输出层将使用softmax函数来确定每个类别的概率。定义模型:

    model = Sequential\
            ([Dense(500, input_shape=(1024,), activation='relu'),\
              Dense(10, activation='softmax')])
    
  4. 定义好模型后,我们可以使用summary方法确认模型的结构以及模型中的可训练参数(或权重)数量:

    model.summary()
    

    输出如下:

    图 5.15:模型中可训练参数的结构和数量

图 5.15:模型中可训练参数的结构和数量

该表总结了神经网络的结构。我们可以看到,我们指定的两个层,其中第一个层有 500 个单元,第二个层有 10 个输出单元。Param #列告诉我们该特定层中有多少个可训练的权重。该表还告诉我们,网络中总共有 517,510 个可训练的权重。

注意

要访问本节的源代码,请参考packt.live/31WaTdR

您还可以在packt.live/3gGEtbA上在线运行此示例。

在本练习中,我们创建了一个 Keras 神经网络模型,包含超过 500,000 个权重,可用于分类 CIFAR-10 图像。在接下来的章节中,我们将训练这个模型。

神经网络:训练

定义好神经网络模型后,我们可以开始训练过程;在此阶段,我们将以监督方式训练模型,以便在开始训练自编码器之前对 Keras 框架有所了解。监督学习模型通过提供输入信息和已知输出进行训练;训练的目标是构建一个网络,使其仅使用模型的参数,接受输入信息并返回已知的输出。

在像 CIFAR-10 这样的有监督分类示例中,输入信息是图像,而已知的输出是该图像所属的类别。在训练过程中,对于每个样本的预测,使用指定的误差函数计算前馈网络预测中的误差。然后,模型中的每个权重都会被调整,试图减少误差。这个调整过程被称为反向传播,因为误差从输出反向传播通过网络,直到网络的起始部分。

在反向传播过程中,每个可训练的权重都会根据其对总误差的贡献进行调整,调整的幅度与一个被称为学习率的值成比例,学习率控制着可训练权重变化的速度。观察下图,我们可以看到,增大学习率的值可以加快误差减少的速度,但也存在不能收敛到最小误差的风险,因为我们可能会越过最小值。学习率过小可能导致我们失去耐心,或者根本没有足够的时间找到全局最小值。在神经网络训练中,我们的目标是找到误差的全局最小值——基本上就是训练过程中,权重调节到一个无法再进一步减少错误的点。因此,找到合适的学习率是一个试错的过程,虽然从较大的学习率开始并逐渐减小它通常是一个有效的方法。下图表示选择学习率对成本函数优化的影响。

图 5.16:选择正确的学习率

图 5.16:选择正确的学习率

在前面的图中,您可以看到一个周期内的学习误差,在这种情况下是随着时间的推移变化的。一个周期对应着训练数据集中的完整循环。训练会反复进行,直到预测误差不再减少,或者开发者等待结果时耐心耗尽。为了完成训练过程,我们首先需要做出一些设计决策,其中最重要的是选择最合适的误差函数。可供使用的误差函数种类繁多,从简单的均方差到更复杂的选项都有。分类交叉熵(在接下来的练习中使用)是一个非常有用的误差函数,尤其适用于多类分类问题。

定义了误差函数后,我们需要选择更新可训练参数的方法。最节省内存且有效的更新方法之一是随机梯度下降SGD)。SGD 有多种变体,所有变体都涉及根据每个权重对计算误差的贡献来调整权重。最终的训练设计决策是选择评估模型的性能指标,并选择最佳架构;在分类问题中,这可能是模型的分类准确率,或者在回归问题中,可能是产生最低误差得分的模型。这些比较通常使用交叉验证方法进行。

练习 5.04:训练一个 Keras 神经网络模型

感谢我们不需要手动编程神经网络的组件,如反向传播,因为 Keras 框架会为我们管理这些。在本次练习中,我们将使用 Keras 训练一个神经网络,使用前一练习中定义的模型架构对 CIFAR-10 数据集的一个小子集进行分类。与所有机器学习问题一样,第一步也是最重要的一步是尽可能多地了解数据集,这将是本次练习的初步重点:

注意

你可以从packt.live/3eexo1s下载data_batch_1batches.meta文件。

  1. 导入picklenumpymatplotlib以及keras.models中的Sequential类,和keras.layers中的Dense。我们将在本练习中使用pickle来序列化 Python 对象,以便传输或存储:

    import pickle
    import numpy as np
    import matplotlib.pyplot as plt
    from keras.models import Sequential
    from keras.layers import Dense
    import tensorflow.python.util.deprecation as deprecation
    deprecation._PRINT_DEPRECATION_WARNINGS = False
    
  2. 加载随附源代码提供的 CIFAR-10 数据集样本,该样本位于data_batch_1文件中:

    with open('data_batch_1', 'rb') as f:
        batch_1 = pickle.load(f, encoding='bytes')
    
  3. 数据以字典形式加载。显示字典的键:

    batch_1.keys()
    

    输出如下:

    dict_keys([b'batch_label', b'labels', b'data', b'filenames'])
    
  4. 请注意,键是以二进制字符串形式存储的,表示为b'。我们关注的是数据和标签的内容。首先查看标签:

    labels = batch_1[b'labels']
    labels
    

    一部分输出如下,每个类别号对应一个文本标签(飞机、汽车等):

    图 5.17:显示标签

    图 5.17:显示标签

  5. 我们可以看到,标签是一个值为 0-9 的列表,表示每个样本所属的类别。现在,查看data键的内容:

    batch_1[b'data']
    

    输出如下:

    图 5.18:数据键的内容

    图 5.18:数据键的内容

  6. 数据键提供了一个 NumPy 数组,其中存储了所有图像数据。图像数据的形状是什么?

    batch_1[b'data'].shape
    

    输出如下:

    (10000, 3072)
    
  7. 我们可以看到我们有 1,000 个样本,但每个样本是一个维度为 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')
    """
    Breaking the 3,072 samples of each single image into thirds, 
    which correspond to Red, Green, Blue channels
    """
    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
    
  8. 显示前 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')
    

    以下输出显示了我们数据集中低分辨率图像的一个样本——这是由于我们最初收到的 32 x 32 分辨率图像所导致的:

    图 5.19:前 12 张图片

    图 5.19:前 12 张图片

    标签的实际含义是什么?我们将在下一步中找到答案。

  9. 使用以下代码加载 batches.meta 文件:

    with open('batches.meta', 'rb') as f:
        label_strings = pickle.load(f, encoding='bytes')
    label_strings
    

    输出如下:

    图 5.20:标签的含义

    图 5.20:标签的含义

  10. 解码二进制字符串以获得实际标签:

    actual_labels = [label.decode() for label in \
                     label_strings[b'label_names']]
    actual_labels
    

    输出如下:

    图 5.21:打印实际标签

    图 5.21:打印实际标签

  11. 打印前 12 张图片的标签:

    for lab in labels[:12]:
        print(actual_labels[lab], end=', ')
    

    输出如下:

    frog, truck, truck, deer, automobile, automobile, 
    bird, horse, ship, cat, deer, horse, 
    
  12. 现在我们需要准备数据来训练模型。第一步是准备输出。目前,输出是一个包含数字 0-9 的列表,但我们需要每个样本表示为一个包含 10 个单元的向量,按照之前的模型来处理。

    one_hot_labels = np.zeros((images.shape[0], 10))
    for idx, lab in enumerate(labels):
        one_hot_labels[idx, lab] = 1
    
  13. 显示前 12 个样本的 one-hot 编码值:

    one_hot_labels[:12]
    

    输出如下:

    图 5.22:前 12 个样本的 one-hot 编码值

    图 5.22:前 12 个样本的 one-hot 编码值

  14. 该模型有 1,024 个输入,因为它期望一个 32 x 32 的灰度图像。对于每张图像,取三个通道的平均值将其转换为 RGB:

    images = images.mean(axis=-1)
    
  15. 再次显示前 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.23:再次显示前 12 张图片。

    图 5.23:再次显示前 12 张图片。

  16. 最后,将图像缩放到 0 到 1 之间,这是神经网络输入所要求的。由于图像中的最大值是 255,我们将其直接除以 255:

    images /= 255.
    
  17. 我们还需要将图像调整为 10,000 x 1,024 的形状。我们将选择前 7,000 个样本进行训练,最后 3,000 个样本用于评估模型:

    images = images.reshape((-1, 32 ** 2))
    x_train = images[:7000]
    y_train = one_hot_labels[:7000]
    x_test = images[7000:]
    y_test = one_hot_labels[7000:]
    
  18. 使用与 练习 5.03定义一个 Keras 模型 相同的架构重新定义模型:

    model = Sequential\
            ([Dense(500, input_shape=(1024,), activation='relu'),\
              Dense(10, activation='softmax')])
    
  19. 现在我们可以在 Keras 中训练模型。我们首先需要编译方法来指定训练参数。我们将使用类别交叉熵、Adam 优化器和分类准确度作为性能度量:

    model.compile(loss='categorical_crossentropy',\
                  optimizer='adam',\
                  metrics=['accuracy'])
    
  20. 使用反向传播训练模型 100 个周期,并使用模型的 fit 方法:

    model.fit(x_train, y_train, epochs=100, \
              validation_data=(x_test, y_test), \
              shuffle = False)
    

    输出结果如下。请注意,由于神经网络训练的随机性,你的结果可能会略有不同:

    图 5.24:训练模型

    图 5.24:训练模型

    注意

    这里,我们使用 Keras 来训练我们的神经网络模型。Keras 层中的权重初始化是随机进行的,无法通过任何随机种子来控制。因此,每次运行代码时,结果可能会有所不同。

  21. 使用这个网络,我们在训练数据上达到了大约 75.67%的分类准确率,在验证数据上达到了 32.47%的分类准确率(在图 5.24中显示为acc: 0.7567val_acc: 0.3247),该网络处理了 10,000 个样本。再次检查前 12 个样本的预测结果:

    predictions = model.predict(images[:12])
    predictions
    

    输出结果如下:

    图 5.25:打印预测结果

    图 5.25:打印预测结果

  22. 我们可以使用argmax方法来确定每个样本的最可能类别:

    np.argmax(predictions, axis=1)
    

    输出结果如下:

    array([6, 9, 9, 4, 1, 1, 2, 7, 8, 3, 4, 7], dtype=int64)
    
  23. 与标签进行比较:

    labels[:12]
    

    输出结果如下:

    [6, 9, 9, 4, 1, 1, 2, 7, 8, 3, 4, 7]
    

    注意

    要访问此特定部分的源代码,请参考packt.live/2CgH25b

    你也可以在packt.live/38CKwuD在线运行这个示例。

我们现在已经在 Keras 中训练了一个神经网络模型。完成下一个活动以进一步强化你在训练神经网络方面的技能。

活动 5.01:MNIST 神经网络

在这个活动中,你将训练一个神经网络来识别 MNIST 数据集中的图像,并强化你在训练神经网络方面的技能。这个活动是许多神经网络架构的基础,尤其是在计算机视觉中的分类问题。从物体检测与识别到分类,这种通用结构在各种应用中得到了使用。

这些步骤将帮助你完成该活动:

  1. 导入picklenumpymatplotlib以及 Keras 中的SequentialDense类。

  2. 加载mnist.pkl文件,它包含来自 MNIST 数据集的前 10,000 张图像及其对应的标签,源代码中提供了这些数据。MNIST 数据集是一个包含 0 到 9 手写数字的 28x28 灰度图像系列。提取图像和标签。

    注意

    你可以在packt.live/2JOLAQB找到mnist.pkl文件。

  3. 绘制前 10 个样本及其对应的标签。

  4. 使用独热编码对标签进行编码。

  5. 准备将图像输入到神经网络中。作为提示,这个过程包含两个独立的步骤。

  6. 在 Keras 中构建一个神经网络模型,该模型接受准备好的图像,并具有 600 个单元的隐藏层,使用 ReLU 激活函数,输出层的单元数与类别数相同。输出层使用softmax激活函数。

  7. 使用多类交叉熵、随机梯度下降和准确性度量来编译模型。

  8. 训练模型。需要多少个周期才能在训练数据上达到至少 95%的分类准确率?

完成这个任务后,你将训练一个简单的神经网络来识别手写数字 0 到 9。你还将开发一个通用框架,用于构建分类问题的神经网络。借助这个框架,你可以扩展和修改网络来处理其他各种任务。你将要分类的数字的预览图像如下所示:

图 26:待分类数字的预览

图 5.26:待分类数字的预览

注意

该活动的解决方案可以在第 449 页找到。

自编码器

自编码器是一种专门设计的神经网络架构,旨在以高效且描述性强的方式将输入信息压缩到较低的维度空间中。自编码器网络可以分解为两个独立的子网络或阶段:编码阶段和解码阶段。

以下是一个简化的自编码器模型,使用 CIFAR-10 数据集:

图 5.27:简单的自编码器网络架构

图 5.27:简单的自编码器网络架构

第一个阶段,即编码阶段,将输入信息压缩到一个比输入样本大小更小的后续层中。后续的解码阶段则会扩展压缩后的图像数据,并尝试将其恢复为原始形式。因此,网络的输入和期望输出是相同的;网络输入,例如 CIFAR-10 数据集中的一张图像,并试图恢复成相同的图像。这个网络架构如上图所示;在这张图中,我们可以看到自编码器的编码阶段减少了表示信息的神经元数量,而解码阶段则将压缩格式恢复为原始状态。使用解码阶段有助于确保编码器正确表示了信息,因为恢复图像所需的仅仅是压缩后的表示。

练习 5.05:简单自编码器

在本次练习中,我们将为 CIFAR-10 数据集样本构建一个简单的自编码器,压缩图像中的信息以供后续使用。

注意

在本次练习中,我们将使用data_batch_1文件,它是 CIFAR-10 数据集的一个样本。该文件可以从packt.live/3bYi5I8下载。

  1. 导入picklenumpymatplotlib,以及从keras.models导入Model类,从keras.layers导入InputDense

    import pickle
    import numpy as np
    import matplotlib.pyplot as plt
    from keras.models import Model
    from keras.layers import Input, Dense
    import tensorflow.python.util.deprecation as deprecation
    deprecation._PRINT_DEPRECATION_WARNINGS = False
    
  2. 加载数据:

    with open('data_batch_1', 'rb') as f:
        batch_1 = pickle.load(f, encoding='bytes')
    
  3. 由于这是一个无监督学习方法,我们只关注图像数据。加载图像数据:

    images = np.zeros((10000, 32, 32, 3), dtype='uint8')
    for idx, img in enumerate(batch_1[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
    
  4. 将图像转换为灰度图,缩放到 0 到 1 之间,并将每张图像展平为一个长度为 1,024 的向量:

    images = images.mean(axis=-1)
    images = images / 255.0
    images = images.reshape((-1, 32 ** 2))
    images
    

    输出结果如下:

    图 5.28:缩放后的图像

    图 5.28:缩放后的图像

  5. 定义自编码器模型。由于我们需要访问编码器阶段的输出,因此我们将采用一种与之前略有不同的方法来定义模型。定义一个包含1024个单元的输入层:

    input_layer = Input(shape=(1024,))
    
  6. 定义一个后续的Dense层,包含256个单元(压缩比为 1024/256 = 4),并使用 ReLU 激活函数作为编码阶段。注意,我们已将该层分配给一个变量,并通过call方法将前一层传递给该类:

    encoding_stage = Dense(256, activation='relu')(input_layer)
    
  7. 使用 sigmoid 函数作为激活函数,并与输入层相同的形状定义一个后续的解码器层。选择 sigmoid 函数是因为输入值仅介于 0 和 1 之间:

    decoding_stage = Dense(1024, activation='sigmoid')\
                          (encoding_stage)
    
  8. 通过将网络的第一层和最后一层传递给Model类来构建模型:

    autoencoder = Model(input_layer, decoding_stage)
    
  9. 使用二元交叉熵损失函数和adadelta梯度下降编译自编码器:

    autoencoder.compile(loss='binary_crossentropy',\
                        optimizer='adadelta')
    

    注意

    adadelta是一种更为复杂的随机梯度下降版本,其中学习率基于最近的梯度更新窗口进行调整。与其他调整学习率的方法相比,它可以防止非常旧的周期梯度影响学习率。

  10. 现在,让我们开始训练模型;同样,我们将图像作为训练数据并作为期望的输出。训练 100 个周期:

    autoencoder.fit(images, images, epochs=100)
    

    输出如下:

    图 5.29:训练模型

    图 5.29:训练模型

  11. 计算并存储前五个样本的编码阶段输出:

    encoder_output = Model(input_layer, encoding_stage)\
                    .predict(images[:5])
    
  12. 将编码器输出重新调整为 16 x 16(16 x 16 = 256)像素,并乘以 255:

    encoder_output = encoder_output.reshape((-1, 16, 16)) * 255
    
  13. 计算并存储前五个样本的解码阶段输出:

    decoder_output = autoencoder.predict(images[:5])
    
  14. 将解码器的输出重新调整为 32 x 32 并乘以 255:

    decoder_output = decoder_output.reshape((-1, 32,32)) * 255
    
  15. 重新调整原始图像:

    images = images.reshape((-1, 32, 32))
    plt.figure(figsize=(10, 7))
    for i in range(5):
        # Plot the original images  
        plt.subplot(3, 5, i + 1)
        plt.imshow(images[i], cmap='gray')
        plt.axis('off')
        # Plot the encoder output
        plt.subplot(3, 5, i + 6)
        plt.imshow(encoder_output[i], cmap='gray')
        plt.axis('off')   
        # Plot the decoder output
        plt.subplot(3, 5, i + 11)
        plt.imshow(decoder_output[i], cmap='gray')
        plt.axis('off')
    

    输出如下:

    图 5.30:简单自编码器的输出

图 5.30:简单自编码器的输出

在前面的图中,我们可以看到三行图像。第一行是原始的灰度图像,第二行是对应的自编码器输出,第三行是从编码输入中重构的原始图像。我们可以看到第三行解码后的图像包含了图像基本形状的信息;我们可以看到青蛙和鹿的主体,以及样本中卡车和汽车的轮廓。由于我们只训练了 100 个样本,因此增加训练周期的数量将有助于进一步提升编码器和解码器的性能。现在,我们已经得到了训练好的自编码器阶段的输出,可以将其作为其他无监督算法(如 K 均值或 K 近邻)的特征向量。

注意

要访问此特定部分的源代码,请参考packt.live/2BQH03R

你也可以在packt.live/2Z9CMgI在线运行此示例。

活动 5.02:简单的 MNIST 自动编码器

在本活动中,您将为随附源代码中的 MNIST 数据集创建一个自动编码器网络。像本活动中构建的自动编码器网络在无监督学习的预处理阶段非常有用。网络生成的编码信息可以用于聚类或分割分析,例如基于图像的网页搜索:

  1. 导入picklenumpymatplotlib,以及 Keras 中的ModelInputDense类。

  2. 从随附源代码中提供的 MNIST 数据集样本中加载图像(mnist.pkl)。

    注意

    你可以从packt.live/2wmpyl5下载mnist.pkl文件。

  3. 为神经网络准备图像。作为提示,整个过程有两个独立的步骤。

  4. 构建一个简单的自动编码器网络,将图像大小减少到编码阶段后的 10 x 10。

  5. 使用二元交叉熵损失函数和adadelta梯度下降法编译自动编码器。

  6. 适配编码器模型。

  7. 计算并存储前五个样本的编码阶段输出。

  8. 将编码器输出重塑为 10 x 10(10 x 10 = 100)像素,并乘以 255。

  9. 计算并存储解码阶段前五个样本的输出。

  10. 将解码器的输出重塑为 28 x 28 并乘以 255。

  11. 绘制原始图像、编码器输出和解码器的图像。

完成此活动后,您将成功训练一个自动编码器网络,从数据集中提取关键信息,为后续处理做好准备。输出将类似于以下内容:

![图 5.31:原始图像、编码器输出和解码器的预期图]

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_31.jpg)

图 5.31:原始图像、编码器输出和解码器的预期图

注意

该活动的解决方案可以在第 452 页找到。

练习 5.06:多层自动编码器

在本练习中,我们将为 CIFAR-10 数据集样本构建一个多层自动编码器,将图像中存储的信息压缩,以便后续使用:

注意

你可以从packt.live/2VcY0a9下载data_batch_1文件。

  1. 导入picklenumpymatplotlib,以及keras.models中的Model类,导入keras.layers中的InputDense

    import pickle
    import numpy as np
    import matplotlib.pyplot as plt
    from keras.models import Model
    from keras.layers import Input, Dense
    import tensorflow.python.util.deprecation as deprecation
    deprecation._PRINT_DEPRECATION_WARNINGS = False
    
  2. 加载数据:

    with open('data_batch_1', 'rb') as f:
        dat = pickle.load(f, encoding='bytes')
    
  3. 由于这是一个无监督学习方法,我们只关心图像数据。请按照前面的练习加载图像数据:

    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
    
  4. 将图像转换为灰度图,缩放到 0 和 1 之间,并将每个图像展平为一个长度为 1,024 的向量:

    images = images.mean(axis=-1)
    images = images / 255.0
    images = images.reshape((-1, 32 ** 2))
    images
    

    输出如下:

    ![图 5.32:缩放后的图像]

    ](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_32.jpg)

    图 5.32:缩放图像

  5. 定义多层自动编码器模型。我们将使用与简单自动编码器模型相同的输入形状:

    input_layer = Input(shape=(1024,))
    
  6. 我们将在 256 自动编码器阶段之前添加另一个层——这次使用 512 个神经元:

    hidden_encoding = Dense(512, activation='relu')(input_layer)
    
  7. 我们使用与练习 5.05简单自动编码器相同大小的自动编码器,但这次层的输入是hidden_encoding层:

    encoding_stage = Dense(256, activation='relu')(hidden_encoding)
    
  8. 添加解码隐藏层:

    hidden_decoding = Dense(512, activation='relu')(encoding_stage)
    
  9. 使用与上一练习相同的输出阶段,这次连接到隐藏解码阶段:

    decoding_stage = Dense(1024, activation='sigmoid')\
                          (hidden_decoding)
    
  10. 通过将网络的第一个和最后一个层传递给Model类来构建模型:

    autoencoder = Model(input_layer, decoding_stage)
    
  11. 使用二进制交叉熵损失函数和adadelta梯度下降编译自动编码器:

    autoencoder.compile(loss='binary_crossentropy',\
                        optimizer='adadelta')
    
  12. 现在,让我们拟合模型;再次将图像作为训练数据和期望的输出。训练 100 epochs:

    autoencoder.fit(images, images, epochs=100)
    

    输出如下:

    图 5.33:训练模型

    图 5.33:训练模型

  13. 计算并存储编码阶段前五个样本的输出:

    encoder_output = Model(input_stage, encoding_stage)\
                     .predict(images[:5])
    
  14. 将编码器的输出调整为 16 x 16(16 x 16 = 256)像素并乘以 255:

    encoder_output = encoder_output.reshape((-1, 16, 16)) * 255
    
  15. 计算并存储解码阶段前五个样本的输出:

    decoder_output = autoencoder.predict(images[:5])
    
  16. 将解码器的输出调整为 32 x 32 并乘以 255:

    decoder_output = decoder_output.reshape((-1, 32, 32)) * 255
    
  17. 绘制原始图像、编码器输出和解码器:

    images = images.reshape((-1, 32, 32))
    plt.figure(figsize=(10, 7))
    for i in range(5):
        # Plot original images
        plt.subplot(3, 5, i + 1)
        plt.imshow(images[i], cmap='gray')
        plt.axis('off')
        # Plot encoder output
        plt.subplot(3, 5, i + 6)
        plt.imshow(encoder_output[i], cmap='gray')
        plt.axis('off')
        # Plot decoder output 
        plt.subplot(3, 5, i + 11)
        plt.imshow(decoder_output[i], cmap='gray')
        plt.axis('off')
    

    输出如下:

    图 5.34:多层自动编码器的输出

图 5.34:多层自动编码器的输出

通过查看简单自动编码器和多层自动编码器产生的误差得分,并比较图 5.30图 5.34,我们可以看到两种编码器结构的输出几乎没有差别。两张图的中间行显示出这两种模型学到的特征实际上是不同的。我们可以使用许多方法来改善这两种模型,例如训练更多的 epochs、使用不同数量的单元或神经元,或使用不同数量的层。本练习的目的是展示如何构建和使用自动编码器,但优化通常是一个系统性的试错过程。我们鼓励你调整一些模型参数,并自己探索不同的结果。

注意

若要访问此特定部分的源代码,请参阅packt.live/2ZbaT81

你也可以在packt.live/2ZHvOyo在线运行此示例。

卷积神经网络

在构建所有以前的神经网络模型时,您可能已经注意到,在将图像转换为灰度图像并将每个图像展平为长度为 1,024 的单一向量时,我们移除了所有颜色信息。这样做实质上丢失了很多可能对我们有用的信息。图像中的颜色可能与图像中的类或对象特定相关;此外,我们还丢失了关于图像的空间信息,例如卡车图像中拖车相对驾驶室的位置或鹿的腿相对头部的位置。卷积神经网络不会遭受这种信息丢失。这是因为它们不是使用可训练参数的平面结构,而是将权重存储在网格或矩阵中,这意味着每组参数可以在其结构中有多层。通过将权重组织在网格中,我们可以防止空间信息的丢失,因为权重是以滑动方式应用于图像的。此外,通过具有多个层,我们可以保留与图像相关的颜色通道。

在开发基于卷积神经网络的自编码器时,MaxPooling2D 和 Upsampling2D 层非常重要。MaxPooling 2D 层通过在输入的窗口内选择最大值来在两个维度上减少或缩小输入矩阵的大小。假设我们有一个 2 x 2 的矩阵,其中三个单元格的值为 1,一个单元格的值为 2:

图 5.35:示例矩阵演示

图 5.35:示例矩阵演示

如果提供给 MaxPooling2D 层,则该矩阵将返回一个值为 2 的单个值,从而在两个方向上将输入的大小减少一半。

UpSampling2D 层的作用与 MaxPooling2D 层相反,它增加输入的大小而不是减小它。上采样过程重复数据的行和列,从而使输入矩阵的大小加倍。对于前面的例子,您将把一个 2 x 2 的矩阵转换成一个 4 x 4 的矩阵,其中右下角的 4 个像素值为 2,其余为 1。

练习 5.07:卷积自编码器

在这个练习中,我们将开发基于卷积神经网络的自编码器,并与之前的全连接神经网络自编码器性能进行比较:

注意

您可以从 packt.live/2x31ww3 下载 data_batch_1 文件。

  1. 导入 picklenumpymatplotlib,以及从 keras.models 导入 Model 类,从 keras.layers 导入 InputConv2DMaxPooling2DUpSampling2D

    import pickle
    import numpy as np
    import matplotlib.pyplot as plt
    from keras.models import Model
    from keras.layers import Input, Conv2D, MaxPooling2D, UpSampling2D
    import tensorflow.python.util.deprecation as deprecation
    deprecation._PRINT_DEPRECATION_WARNINGS = False
    
  2. 加载数据:

    with open('data_batch_1', 'rb') as f:
        batch_1 = pickle.load(f, encoding='bytes')
    
  3. 由于这是一种无监督学习方法,我们只对图像数据感兴趣。按照前面的练习加载图像数据:

    images = np.zeros((10000, 32, 32, 3), dtype='uint8')
    for idx, img in enumerate(batch_1[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
    
  4. 由于我们使用卷积网络,我们可以仅对图像进行重新缩放使用:

    images = images / 255.
    
  5. 定义卷积自编码器模型。我们将使用与图像相同的形状输入:

    input_layer = Input(shape=(32, 32, 3,))
    
  6. 添加一个包含 32 层或滤波器的卷积阶段,使用 3 x 3 的权重矩阵,ReLU 激活函数,并使用相同的填充,这意味着输出的长度与输入图像相同。

    hidden_encoding = Conv2D\
                      (32, # Number of filters in the weight matrix
                      (3, 3), # Shape of the weight matrix
                       activation='relu', padding='same', \
                       # Retaining dimensions between input and output \
                       )(input_layer)
    
  7. 向编码器中添加一个 2 x 2 核的最大池化层。MaxPooling会查看图像中的所有值,使用 2 x 2 矩阵进行扫描。在每个 2 x 2 区域中返回最大值,从而将编码层的大小减少一半:

    encoded = MaxPooling2D((2, 2))(hidden_encoding)
    
  8. 添加一个解码卷积层(该层应该与之前的卷积层相同):

    hidden_decoding = \
    Conv2D(32, # Number of filters in the weight matrix \
           (3, 3), # Shape of the weight matrix \
           activation='relu', \
           # Retaining dimensions between input and output \
           padding='same', \
           )(encoded)
    
  9. 现在我们需要将图像恢复到原始大小,方法是将上采样设置为与MaxPooling2D相同的大小:

    upsample_decoding = UpSampling2D((2, 2))(hidden_decoding)
    
  10. 添加最后的卷积阶段,使用三层来处理图像的 RGB 通道:

    decoded = \
    Conv2D(3, # Number of filters in the weight matrix \
           (3, 3), # Shape of the weight matrix \
           activation='sigmoid', \
           # Retaining dimensions between input and output \
           padding='same', \
           )(upsample_decoding)
    
  11. 通过将网络的第一层和最后一层传递给Model类来构建模型:

    autoencoder = Model(input_layer, decoded)
    
  12. 显示模型的结构:

    autoencoder.summary()
    

    输出如下:

    图 5.36:模型结构

    图 5.36:模型结构

    注意

    与之前的自编码器示例相比,我们的可训练参数要少得多。这是一个特定的设计决策,旨在确保该示例能在各种硬件上运行。卷积网络通常需要更多的处理能力,并且常常需要像图形处理单元(GPU)这样的特殊硬件。

  13. 使用二元交叉熵损失函数和adadelta梯度下降编译自编码器:

    autoencoder.compile(loss='binary_crossentropy',\
                        optimizer='adadelta')
    
  14. 现在,让我们拟合模型;再次地,我们将图像作为训练数据和期望输出传递。与之前训练 100 个周期不同,这次我们将使用 20 个周期,因为卷积网络的计算时间要长得多:

    autoencoder.fit(images, images, epochs=20)
    

    输出如下:

    图 5.37:训练模型

    图 5.37:训练模型

    注意,在第二个周期后,误差已经比之前的自编码器练习更小,这表明编码/解码模型更好。这个误差减少主要归功于卷积神经网络没有丢弃大量数据,且编码后的图像为 16 x 16 x 32,比之前的 16 x 16 尺寸要大得多。此外,我们没有压缩图像本身,因为它们现在包含的像素较少(16 x 16 x 32 = 8,192),但比之前有更多的深度(32 x 32 x 3 = 3,072)。这些信息已经重新排列,以便进行更有效的编码/解码处理。

  15. 计算并存储前五个样本的编码阶段输出:

    encoder_output = Model(input_layer, encoded).predict(images[:5])
    
  16. 每个编码后的图像的形状为 16 x 16 x 32,这是由于为卷积阶段选择的滤波器数量。因此,在没有修改的情况下,我们无法对其进行可视化。我们将其重塑为 256 x 32 的大小,以便进行可视化:

    encoder_output = encoder_output.reshape((-1, 256, 32))
    
  17. 获取前五张图像的解码器输出:

    decoder_output = autoencoder.predict(images[:5])
    
  18. 绘制原始图像、平均编码器输出和解码器:

    plt.figure(figsize=(10, 7))
    for i in range(5):
        # Plot original images
        plt.subplot(3, 5, i + 1)
        plt.imshow(images[i], cmap='gray')
        plt.axis('off')
        # Plot encoder output
        plt.subplot(3, 5, i + 6)
        plt.imshow(encoder_output[i], cmap='gray')
        plt.axis('off')
        # Plot decoder output
        plt.subplot(3, 5, i + 11)
        plt.imshow(decoder_output[i])
        plt.axis('off')
    

    输出如下:

    图 5.38:原始图像、编码器输出和解码器输出

图 5.38:原始图像、编码器输出和解码器输出

注意

要获取此特定部分的源代码,请参考packt.live/2VYprpq

你也可以在线运行此示例,网址为packt.live/38EDgic

活动 5.03:MNIST 卷积自编码器

在这个活动中,我们将通过 MNIST 数据集加强卷积自编码器的知识。卷积自编码器通常在处理大小适中的基于图像的数据集时能够显著提高性能。这在使用自编码器生成人工图像样本时特别有用:

  1. 导入picklenumpymatplotlib,以及从keras.models导入Model类,并从keras.layers导入InputConv2DMaxPooling2DUpSampling2D

  2. 加载包含前 10,000 个图像及其对应标签的mnist.pkl文件,这些数据可以在附带的源代码中找到。

    注意

    你可以从packt.live/3e4HOR1下载mnist.pkl文件。

  3. 将图像重新缩放,使其值介于 0 和 1 之间。

  4. 我们需要重塑图像,增加一个单一的深度通道以供卷积阶段使用。将图像重塑为 28 x 28 x 1 的形状。

  5. 定义一个输入层。我们将使用与图像相同的输入形状。

  6. 添加一个卷积阶段,包含 16 层或滤波器,一个 3 x 3 的权重矩阵,一个 ReLU 激活函数,并使用相同的填充方式,这意味着输出图像的尺寸与输入图像相同。

  7. 向编码器添加一个最大池化层,使用 2 x 2 的核。

  8. 添加一个解码卷积层。

  9. 添加一个上采样层。

  10. 根据初始图像深度,添加最终的卷积阶段,使用一层。

  11. 通过将网络的第一层和最后一层传递给Model类来构建模型。

  12. 显示模型的结构。

  13. 使用二进制交叉熵损失函数和adadelta梯度下降来编译自编码器。

  14. 现在,让我们开始拟合模型;我们再次将图像作为训练数据并作为期望的输出。训练 20 个周期,因为卷积神经网络需要更长的计算时间。

  15. 计算并存储前五个样本的编码阶段输出。

  16. 为了可视化,重新调整编码器输出的形状,使每个图像为X*Y大小。

  17. 获取前五个图像的解码器输出。

  18. 将解码器输出重塑为28 x 28的大小。

  19. 将原始图像重塑为28 x 28的大小。

  20. 绘制原始图像、平均编码器输出和解码器输出。

在本次活动结束时,你将开发出一个包含卷积层的自编码器神经网络。请注意,解码器表示中所做的改进。输出将类似于以下内容:

图 5.39:预期的原始图像、编码器输出和解码器输出

图 5.39:预期的原始图像、编码器输出和解码器

注释

本活动的解决方案可以在第 455 页找到。

总结

在本章中,我们首先介绍了人工神经网络的基本概念,讲解了它们的结构以及它们是如何学习完成特定任务的。以一个有监督学习的例子为起点,我们构建了一个人工神经网络分类器来识别 CIFAR-10 数据集中的物体。接着,我们探讨了神经网络的自编码器架构,并学习了如何使用这些网络来准备数据集,以便在无监督学习问题中使用。最后,我们通过自编码器的研究,进一步了解了卷积神经网络,并探讨了这些额外层能够带来的好处。本章为我们最终探讨降维问题做好了准备,我们将在降维过程中学习如何使用和可视化编码后的数据,使用 t 分布最近邻(t-SNE)算法。t-SNE 提供了一种极其有效的可视化高维数据的方法,即便在应用了诸如 PCA 等降维技术之后。t-SNE 在无监督学习中尤其有用。在下一章中,我们将进一步探讨嵌入技术,它们是处理高维数据的重要工具。正如你在本章中的 CIFAR-10 数据集中看到的那样,彩色图像文件的大小可能会迅速增大,从而减慢任何神经网络算法的性能。通过使用降维技术,我们可以最小化高维数据的影响。

第七章:6. t-分布随机邻域嵌入

概述

在本章中,我们将讨论随机邻域嵌入SNE)和t-分布随机邻域嵌入t-SNE)作为可视化高维数据集的一种方法。我们将实现 t-SNE 模型并解释 t-SNE 的局限性。能够将高维信息提取到低维空间将有助于可视化和探索性分析,同时也能与我们在前几章中探讨的聚类算法相结合。到本章结束时,我们将能够在低维空间中找到高维数据的聚类,例如用户级别信息或图像。

介绍

到目前为止,我们已经描述了多种不同的方法来减少数据集的维度,作为清洗数据、减少计算效率所需的大小或提取数据集中最重要信息的手段。虽然我们已经展示了许多减少高维数据集的方法,但在许多情况下,我们无法将维度的数量减少到可以可视化的大小,也就是二维或三维,而不会过度降低数据质量。考虑我们之前在本书中使用的 MNIST 数据集,这是一个包含数字 0 到 9 的手写数字图像的集合。每个图像的大小为 28 x 28 像素,提供 784 个独立的维度或特征。如果我们将这 784 个维度减少到 2 或 3 个以便进行可视化,我们几乎会失去所有可用的信息。

在本章中,我们将讨论 SNE 和 t-SNE 作为可视化高维数据集的一种手段。这些技术在无监督学习和机器学习系统设计中非常有用,因为能够可视化数据是一件强大的事情。能够可视化数据可以探索关系、识别群体并验证结果。t-SNE 技术已被用于可视化癌细胞核,这些细胞核具有超过 30 个特征,而文档中的数据可能具有上千维,有时即使在应用了像 PCA 这样的技术后也是如此。

MNIST 数据集

现在,我们将使用附带源代码提供的 MNIST 数据集作为实际示例,探索 SNE 和 t-SNE。在继续之前,我们将快速回顾一下 MNIST 及其中的数据。完整的 MNIST 数据集包含 60,000 个训练样本和 10,000 个测试样本,这些样本是手写数字 0 到 9,表示为黑白(或灰度)图像,大小为 28 x 28 像素(即 784 个维度或特征),每个数字类别的样本数量相等。由于数据集的大小和数据质量,MNIST 已经成为机器学习中最具代表性的数据集之一,通常被作为许多机器学习研究论文中的参考数据集。与其他数据集相比,使用 MNIST 探索 SNE 和 t-SNE 的一个优势是,虽然样本包含大量维度,但即使在降维后,仍然可以将其可视化,因为它们可以表示为图像。图 6.1展示了 MNIST 数据集的一个样本:

图 6.1:MNIST 数据样本

图 6.1:MNIST 数据样本

下图展示了通过 PCA 将相同样本降至 30 个主成分:

图 6.2:通过 PCA 将 MNIST 数据集降至 30 个主成分

图 6.2:通过 PCA 将 MNIST 数据集降至 30 个主成分

随机邻居嵌入(SNE)

SNE 是多种流形学习方法中的一种,旨在描述低维流形或有界区域中的高维空间。乍一看,这似乎是一个不可能完成的任务;如果我们有一个至少包含 30 个特征的数据集,如何合理地在二维空间中表示数据呢?随着我们逐步推导 SNE 的过程,希望你能够看到这是如何可能的。别担心——我们不会在这一章中深入探讨这个过程的数学细节,因为那超出了本章的范围。构建 SNE 可以分为以下几个步骤:

  1. 将高维空间中数据点之间的距离转换为条件概率。假设我们有两个点,xi 和xj,位于高维空间中,并且我们想要确定xj 作为xi 邻居的概率(pi|j)。为了定义这个概率,我们使用高斯曲线。这样,我们可以看到,对于附近的点,概率较高,而对于远离的点,概率非常低。

  2. 我们需要确定高斯曲线的宽度,因为它控制着概率选择的速率。宽曲线意味着许多邻近点相距较远,而窄曲线则意味着它们紧密地聚集在一起。

  3. 一旦我们将数据投影到低维空间,我们还可以确定相应低维数据之间的概率(qi|j),即yi 和yj 之间的概率。

  4. SNE 的目标是通过使用成本函数(C)最小化所有数据点之间的pi|j 和qi|j 之间的差异,将数据放置到低维空间中。这被称为Kullback-Leibler (KL)散度:图 6.3:KL 散度

图 6.3:KL 散度

要构建高斯分布的 Python 代码,请参考GaussianDist.ipynb Jupyter 笔记本,链接为packt.live/2UMVubU

当在 SNE 中使用高斯分布时,它通过保持局部模式来减少数据的维度。为了实现这一点,SNE 使用梯度下降过程来最小化 C,使用学习率和训练周期等标准参数,正如我们在前一章中讨论神经网络和自编码器时所提到的那样。SNE 在训练过程中实现了一个额外的项——困惑度。困惑度是在比较中选择有效邻居数量的一个参数,对于困惑度值在 5 到 50 之间时,它相对稳定。实际上,建议在这一范围内使用困惑度值进行反复试验。

本章后面将详细讨论困惑度。

SNE 提供了一种有效的方式,将高维数据可视化到低维空间,尽管它仍然存在一个被称为拥挤问题的问题。拥挤问题可能出现在我们有一些点大致等距地分布在一个点周围的区域内,i。当这些点在低维空间中被可视化时,它们会相互拥挤,导致可视化困难。如果我们试图在这些拥挤的点之间留出更多空间,问题会加剧,因为任何距离更远的点会在低维空间中被放置得非常远。实质上,我们是在努力平衡既能可视化近距离点,又不失去远离点所提供的信息。

t-分布 SNE

t-SNE 旨在通过修改后的 KL 散度成本函数,使用学生 t 分布替代低维空间中的高斯分布,从而解决拥挤问题。学生 t 分布是一种概率分布,类似于高斯分布,通常用于样本量较小且总体标准差未知的情况。它常用于学生 t 检验中。

修改后的 KL 成本函数在低维空间中对每对数据点的距离给予相等的权重,而学生分布在低维空间中采用较重的尾部以避免拥挤问题。在高维概率计算中,仍然使用高斯分布,以确保在高维空间中适度的距离在低维空间中也能得到忠实的表示。不同分布在各自空间中的组合,允许忠实地表示由小距离和适度距离分开的数据点。

注意

若需要一些关于如何在 Python 中重现学生 t 分布的示例代码,请参考 packt.live/2UMVubU 中的 Jupyter notebook。

幸运的是,我们不需要手动实现 t-SNE,因为 scikit-learn 提供了一个非常有效的实现,且其 API 非常简洁。我们需要记住的是,SNE 和 t-SNE 都是通过计算两个点在高维空间和低维空间中作为邻居的概率,并尽量最小化两个空间之间概率的差异。

练习 6.01:t-SNE MNIST

在本练习中,我们将使用 MNIST 数据集(随附源代码提供)来探索 scikit-learn 中 t-SNE 的实现。正如我们之前描述的那样,使用 MNIST 让我们能够以其他数据集(如波士顿房价数据集或鸢尾花数据集)无法实现的方式来可视化高维空间。请执行以下步骤:

  1. 对于本练习,导入 picklenumpyPCATSNE(来自 scikit-learn),以及 matplotlib

    import pickle
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.decomposition import PCA
    from sklearn.manifold import TSNE
    np.random.seed(2)
    
  2. 加载并可视化提供的 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 数据集已成功加载。

  3. 在本练习中,我们将对数据集应用 PCA,提取前 30 个主成分。

    model_pca = PCA(n_components=30)
    mnist_pca = model_pca.fit(mnist['images'].reshape((-1, 28 ** 2)))
    
  4. 可视化将数据集降至 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 特有样本相关的信息。

  5. 现在,我们将应用 t-SNE 算法对 PCA 变换后的数据进行处理,以在二维空间中可视化 30 个主成分。我们可以通过 scikit-learn 中的标准模型 API 接口来构建一个 t-SNE 模型。我们将从使用默认值开始,这些值指定了我们将在二维空间中嵌入 30 个维度进行可视化,使用的困惑度为 30,学习率为 200,迭代次数为 1,000。我们将设置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_raten_componentsn_iterperplexityrandom_stateverbose的值。对于learning_rate,正如我们之前所讨论的,t-SNE 使用随机梯度下降将高维数据投影到低维空间。学习率控制该过程执行的速度。如果学习率太高,模型可能无法收敛到一个解;如果太低,可能需要很长时间才能得到结果(如果能得到的话)。一个好的经验法则是从默认值开始;如果你发现模型产生了 NaN(非数值)结果,可能需要降低学习率。一旦对模型的结果满意,最好降低学习率并让其运行更长时间(增加n_iter),这样可能会得到稍微更好的结果。n_components是嵌入空间(或可视化空间)的维度数。通常情况下,你会希望数据的可视化是二维图,所以只需要使用默认值2n_iter是梯度下降的最大迭代次数。perplexity是可视化数据时使用的邻居数量。

    通常,5 到 50 之间的值是合适的,考虑到较大的数据集通常需要比较小的数据集更多的困惑度。random_state是任何模型或算法中的一个重要变量,它会在训练开始时初始化其值。计算机硬件和软件工具中提供的随机数生成器实际上并不是真正的随机数生成器;它们实际上是伪随机数生成器。它们提供了接近随机性的良好近似,但并不是真正的随机。计算机中的随机数从一个称为种子的值开始,然后以复杂的方式生成。通过在过程开始时提供相同的种子,每次运行该过程时都会生成相同的“随机数”。虽然这听起来违反直觉,但它对于再现机器学习实验非常有用,因为你不会看到仅由于训练开始时参数初始化的不同而导致的性能差异。这可以提供更多信心,表明性能的变化是由于对模型或训练的有意改变;例如,神经网络的架构。

    注意

    生成真正的随机序列实际上是用计算机完成的最困难的任务之一。计算机软件和硬件的设计方式是,提供的指令每次执行时都完全相同,以便你获得相同的结果。执行中的随机差异,虽然在生成随机数字序列时理想,但在自动化任务和调试问题时将是噩梦。

    verbose是模型的详细程度,描述了在模型拟合过程中打印到屏幕上的信息量。值为 0 表示没有输出,而值为 1 或更大表示输出中详细程度的增加。

  6. 使用 t-SNE 转换 MNIST 的分解数据集:

    mnist_tsne = model_tsne.fit_transform(mnist_30comp)
    

    输出如下:

    图 6.7:转换分解后的数据集

    图 6.7:转换分解后的数据集

    在拟合过程中提供的输出提供了对 scikit-learn 所完成计算的洞察。我们可以看到它正在为所有样本建立索引并计算邻居,然后以批次为 10 的数据来确定邻居的条件概率。在过程结束时,它提供了一个均值标准差值304.9988,并且在 250 次和 1,000 次梯度下降迭代后,给出了 KL 散度。

  7. 现在,视觉化返回数据集中的维度数量:

    mnist_tsne.shape
    

    输出如下:

    10000,2
    

    我们已经成功地将 784 个维度降到 2 维以便可视化,那么它看起来怎么样呢?

  8. 创建由模型生成的二维数据的散点图:

    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 的二维表示(无标签)

    在上面的图中,我们可以看到已经将 MNIST 数据表示为二维形式,但也可以看到它似乎被聚集在一起。数据集中有许多不同的簇或数据块聚集在一起,并且通过一些空白区域与其他簇分开。似乎有大约九个不同的数据组。所有这些观察结果表明,在各个簇内部和它们之间存在某种关系。

  9. 绘制按相应图像标签分组的二维数据,并使用标记将各个标签分开。

    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 二维表示

    上面的图非常有趣。在这里,我们可以看到各个簇与数据集中的不同图像类别(从零到九)对应。通过无监督的方式,也就是不提前提供标签,结合 PCA 和 t-SNE,已经能够将 MNIST 数据集中的各个类别分离并归类。特别有趣的是,数据中似乎存在一些混淆,尤其是数字四与数字九的图像,五和三的图像之间也有一定重叠;这两个簇部分重合。如果我们查看在步骤 4中的数字九和数字四的 PCA 图像,t-SNE MNIST,这一点就更有意义了:

    图 6.10:九的 PCA 图像

    图 6.10:九的 PCA 图像

    它们确实看起来非常相似;也许这与数字四的形状的不确定性有关。观察接下来的图像,我们可以从左侧的四中看到,两条垂直线几乎相交,而右侧的四则是两条线平行:

    图 6.11:数字四的形状

    图 6.11:数字四的形状

    图 6.9中另一个有趣的特征是边缘情况,这些在 Jupyter 笔记本中以不同颜色显示。在每个簇的边缘,我们可以看到一些样本在传统的监督学习中会被错误分类,但它们代表的是与其他簇更相似的样本,而不是它们自己的簇。我们来看一个例子;有一些数字三的样本距离正确的簇非常远。

  10. 获取数据集中所有数字三的索引:

    threes = np.where(mnist['labels'] == 3)[0]
    threes
    

    输出如下:

     array([   7,   10,   12, ..., 9974, 9977, 9991], dtype=int64)
    
  11. 查找 x 值小于 0 的数字三:

    tsne_threes = mnist_tsne[threes]
    far_threes = np.where(tsne_threes[:,0]< -30)[0]
    far_threes
    

    输出如下:

    图 6.12:x 值小于零的三

    图 6.12:x 值小于零的三

  12. 显示坐标,找到一个与三类簇相距较远的点:

    tsne_threes[far_threes]
    

    输出如下:

    图 6.13:远离三类簇的坐标

    图 6.13:远离三类簇的坐标

  13. 选择一个具有合理高负值作为 x 坐标的样本。在本例中,我们将选择第二个样本,即样本 11

    plt.imshow(mnist['images'][11], cmap='gray')
    plt.axis('off');
    plt.show()
    

    输出如下:

    图 6.14:样本 11 的图像

图 6.14:样本 11 的图像

查看这个示例图像及其对应的 t-SNE 坐标,即大约 (-33, 26),并不奇怪,因为这个样本位于数字 8 和 5 的群集附近,这些数字在这幅图像中有许多共同特征。在这个例子中,我们应用了简化的 SNE,展示了它的一些效率以及可能的混淆来源和无监督学习的输出。

注意

要访问此特定部分的源代码,请参阅 packt.live/3iDsCNf

您还可以在 packt.live/3gBdrSK 上在线运行此示例。

活动 6.01:葡萄酒 t-SNE

在这个活动中,我们将通过使用葡萄酒数据集加强我们对 t-SNE 的了解。通过完成此活动,您将能够为自己的定制应用程序构建 t-SNE 模型。葡萄酒数据集(archive.ics.uci.edu/ml/datasets/Wine)收集了关于意大利葡萄酒化学分析的属性,来自三个不同生产商,但每个生产商都是同一种类型的葡萄酒。这些信息可以用作验证特定意大利地区葡萄酒制成的瓶子的有效性的示例。13 个属性包括酒精、苹果酸、灰分、灰的碱性、镁、总酚、类黄酮、非黄烷类酚、前花青素、颜色强度、色调、稀释酒的 OD280/OD315 和 脯氨酸。

每个样本包含一个类别标识符(1 - 3)。

注意

此数据集来源于 archive.ics.uci.edu/ml/machine-learning-databases/wine/(UCI 机器学习库 [archive.ics.uci.edu/ml]。尔湾,加利福尼亚:加利福尼亚大学信息与计算机科学学院)。也可以从 packt.live/3e1JOcY 下载。

这些步骤将帮助您完成此活动:

  1. 导入 pandasnumpymatplotlib,以及从 scikit-learn 导入的 t-SNEPCA 模型。

  2. 使用附带源代码中包含的 wine.data 文件加载 Wine 数据集,并显示前五行数据。

    注意

    您可以使用 del 关键字在 pandas DataFrames 中删除列。只需将 del 关键字传递给数据帧和在平方根内选择的列即可。

  3. 第一列包含标签;提取此列并从数据集中删除。

  4. 执行 PCA 将数据集减少到前六个组件。

  5. 确定描述这六个组件的数据中的方差量。

  6. 使用指定的随机状态创建 t-SNE 模型,并将 verbose 值设置为 1。

  7. 将 PCA 数据拟合到 t-SNE 模型。

  8. 确认 t-SNE 拟合数据的形状是二维的。

  9. 创建二维数据的散点图。

  10. 创建一个带有类标签的二维数据散点图,以可视化可能存在的任何聚类。

到本活动结束时,你将使用 Wine 数据集的六个成分构建一个 t-SNE 可视化图,并在图中的数据位置识别一些关系。最终的图形将类似于以下内容:

图 6.15:预期的绘图

图 6.15:预期的绘图

注意

该活动的解决方案可以在第 460 页找到。

解释 t-SNE 图

现在我们可以使用 t 分布的 SNE 可视化高维数据,重要的是要理解此类图表的局限性以及在解读和生成这些图表时需要关注哪些方面。在本节中,我们将重点介绍 t-SNE 的一些重要特性,并演示在使用这种可视化技术时需要小心的地方。

困惑度

正如我们在 t-SNE 的介绍中所描述的,困惑度值指定了计算条件概率时要使用的最近邻居数量。选择该值会对最终结果产生重大影响;当困惑度值较低时,数据中的局部变化主导,因为计算中只使用了少量样本。相反,困惑度值较大时,会考虑更多的全局变化,因为计算中使用了更多的样本。通常,尝试一系列不同的值来研究困惑度的效果是值得的。再次强调,困惑度值在 5 到 50 之间通常效果不错。

练习 6.02:t-SNE MNIST 和困惑度

在这个练习中,我们将尝试不同的困惑度值,并观察其在可视化图中的效果:

  1. 导入 picklenumpymatplotlib,以及从 scikit-learn 中导入 PCAt-SNE

    import pickle
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.decomposition import PCA
    from sklearn.manifold import TSNE
    np.random.seed(2)
    
  2. 加载 MNIST 数据集。

    with open('mnist.pkl', 'rb') as f:
        mnist = pickle.load(f)
    
  3. 使用 PCA,只选择图像数据的前 30 个方差成分:

    model_pca = PCA(n_components=30)
    mnist_pca = model_pca.fit_transform\
                (mnist['images'].reshape((-1, 28 ** 2)))
    
  4. 在本练习中,我们正在研究困惑度对 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})
    plt.show()
    

    输出如下:

    图 6.16:迭代模型

图 6.16:迭代模型

注意

前面的输出已被截断以便展示。像这样的标准输出通常会更长。不过,还是将其包含在内,因为在模型训练时,保持关注此类输出非常重要。

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

让我们从一个较低的困惑度值开始:

图 6.17:低困惑度值的图表

图 6.17:低困惑度值的图表

注意

第 4 步中的绘图函数将生成此图。接下来的输出是不同困惑度值下的图表。

将困惑度增加 10 倍后,聚类变得更加清晰:

图 6.18:将困惑度增加 10 倍后的图表

图 6.18:将困惑度增加 10 倍后的图表

通过将困惑度增加到 300,我们开始将更多标签合并在一起:

图 6.19:将困惑度值增加到 300

图 6.19:将困惑度值增加到 300

在这个练习中,我们加深了对困惑度影响的理解,并了解了该值对整体结果的敏感性。较小的困惑度值可能导致位置之间空间非常小的更均匀混合。增加困惑度可以更有效地分离聚类,但过高的值会导致聚类重叠。

注意

要访问此特定部分的源代码,请参阅 packt.live/3gI0zdp

你还可以在网上运行这个示例,访问 packt.live/3gDcjxR

活动 6.02:t-SNE 葡萄酒与困惑度

在本活动中,我们将使用 Wine 数据集进一步强化困惑度对 t-SNE 可视化过程的影响。在本活动中,我们将尝试根据葡萄酒的化学成分来判断其来源。t-SNE 过程提供了一种有效的手段来表示并可能识别来源。

注意

这个数据集来源于 archive.ics.uci.edu/ml/machine-learning-databases/wine/ (UCI 机器学习库 [archive.ics.uci.edu/ml])。它可以从 packt.live/3aPOmRJ 下载。

  1. 导入 pandasnumpymatplotlib,以及来自 scikit-learn 的 t-SNEPCA 模型。

  2. 加载 Wine 数据集并检查前五行数据。

  3. 第一列提供标签;从 DataFrame 中提取这些标签,并将它们存储在一个单独的变量中。确保从 DataFrame 中删除该列。

  4. 对数据集执行 PCA,并提取前六个成分。

  5. 构建一个循环,遍历困惑度值(1、5、20、30、80、160、320)。在每次循环中,生成一个具有相应困惑度的 t-SNE 模型,并绘制标记的葡萄酒类别的散点图。注意不同困惑度值的影响。

在本活动结束时,你将生成 Wine 数据集的二维表示,并检查生成的图形,以查找数据的簇或分组。困惑度值 320 的图形如下所示:

图 6.20:预期输出

图 6.20:预期输出

注意

该活动的解决方案可以在第 464 页找到。

迭代次数

我们将要实验的最后一个参数是迭代次数,正如我们在自编码器中的研究所示,它实际上就是应用于梯度下降的训练周期数。幸运的是,迭代次数是一个相对简单的参数,通常只需要一定的耐心,因为低维空间中点的位置会稳定在最终位置。

练习 6.03:t-SNE MNIST 与迭代次数

在本练习中,我们将观察不同迭代参数对 t-SNE 模型的影响,并突出显示一些指标,表明可能需要更多的训练。再次强调,这些参数的值高度依赖于数据集和可用于训练的数据量。在这个例子中,我们将使用 MNIST:

  1. 导入picklenumpymatplotlib,以及从 scikit-learn 导入PCAt-SNE

    import pickle
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.decomposition import PCA
    from sklearn.manifold import TSNE
    np.random.seed(2)
    
  2. 加载 MNIST 数据集:

    with open('mnist.pkl', 'rb') as f:
        mnist = pickle.load(f)
    
  3. 使用 PCA,从图像数据中选择前 30 个方差成分:

    model_pca = PCA(n_components=30)
    mnist_pca = model_pca.fit_transform(mnist['images']\
                                        .reshape((-1, 28 ** 2)))
    
  4. 在本练习中,我们正在研究迭代对 t-SNE 流形的影响。通过迭代模型/绘图循环,使用迭代进度值2505001000

    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)
    

    输出结果如下:

    图 6.21:遍历模型

    图 6.21:遍历模型

  5. 绘制结果:

        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})
    plt.show()
    

    迭代次数减少会限制算法找到相关邻居的能力,导致簇的定义不清:

    图 6.22:250 次迭代后的图形

图 6.22:250 次迭代后的图形

增加迭代次数为算法提供了足够的时间来充分投影数据:

图 6.23:将迭代次数增加到 500 后的图形

图 6.23:将迭代次数增加到 500 后的图形

一旦簇稳定下来,增加迭代次数的影响极小,实际上只是增加了训练时间:

图 6.24:1,000 次迭代后的图形

图 6.24:1,000 次迭代后的图形

从之前的图表来看,我们可以看到迭代值为 500 和 1,000 的聚类位置是稳定的,并且在不同的图表之间几乎没有变化。最有趣的图表是迭代值为 250 的那一张,似乎聚类仍处于运动过程中,正朝着最终位置移动。因此,有足够的证据表明迭代值 500 已经足够。

注意

若要访问此特定部分的源代码,请参见packt.live/2Zaw1uZ

你还可以在线运行这个示例,网址为packt.live/3gCOiHf

活动 6.03:t-SNE 酒类数据与迭代

在这个活动中,我们将研究迭代次数对酒类数据集可视化效果的影响。这个过程在数据处理、清理和理解数据关系的探索阶段中非常常见。根据数据集和分析类型,我们可能需要尝试多种不同的迭代次数,就像我们在本活动中将要看的那样。

正如我们之前提到的,这个过程对于将高维数据降维到更低且更易理解的维度非常有帮助。在这个例子中,我们的数据集有 13 个特征;然而,在现实世界中,你可能会遇到具有数百甚至数千个特征的数据集。一个常见的例子是个人级别的数据,它可能包含任何数量的与人口统计或行为相关的特征,这使得常规的现成分析变得不可能。t-SNE 是一个有助于将高维数据转化为更直观状态的工具。

注意

该数据集来源于archive.ics.uci.edu/ml/machine-learning-databases/wine/(UCI 机器学习库[archive.ics.uci.edu/ml]。加利福尼亚州尔湾:加利福尼亚大学信息与计算机科学学院)。它可以从packt.live/2xXgHXo下载。

这些步骤将帮助你完成此活动:

  1. 导入pandasnumpymatplotlib,以及来自 scikit-learn 的t-SNEPCA模型。

  2. 加载酒类数据集并检查前五行。

  3. 第一列提供了标签;从 DataFrame 中提取这些标签,并将它们存储在一个单独的变量中。确保该列已从 DataFrame 中移除。

  4. 在数据集上执行 PCA 并提取前六个主成分。

  5. 构建一个循环,迭代不同的迭代值(2505001000)。对于每次循环,生成一个具有相应迭代次数的 t-SNE 模型,以及一个没有进度值的相同迭代次数。

  6. 构建一个带有标签的酒类散点图。注意不同迭代值的效果。

完成本活动后,我们将看到修改模型迭代参数的效果。这是一个重要的参数,确保数据在低维空间中已经稳定到一个相对最终的位置。

注意

本活动的解答可以在第 473 页找到。

关于可视化的最终思考

在本章结束时,有几个关于可视化的重要方面需要注意。第一个是聚类的大小或聚类之间的相对空间,可能并不能真正反映它们的接近程度。正如本章前面所讨论的,SNE 通过组合高斯分布和 Student's t 分布来将高维数据表示在低维空间中。因此,由于 t-SNE 平衡了局部数据结构和全局数据结构的位置,距离之间没有线性关系的保证。在局部结构中的点之间的实际距离,可能在可视化中看起来非常接近,但在高维空间中可能仍然有一定的距离。

这一特性还有其他后果,即有时随机数据可能看起来像是具有某种结构,通常需要使用不同的困惑度、学习率、迭代次数和随机种子值来生成多个可视化图。

总结

本章介绍了 t-分布 SNE(t-distributed SNE)作为一种可视化高维信息的方法,这些信息可能来自于先前的处理过程,例如 PCA 或自动编码器。我们讨论了 t-SNE 如何生成这种表示,并使用 MNIST 和 Wine 数据集以及 scikit-learn 生成了多个可视化结果。在本章中,我们能够看到无监督学习的一些强大之处,因为 PCA 和 t-SNE 能够在不知道真实标签的情况下,将每个图像的类别进行聚类。在下一章中,我们将通过研究无监督学习的应用(包括篮子分析和主题建模)来基于这一实践经验进行进一步探讨。

第八章:7. 主题建模

概述

在这一章中,我们将对文本数据进行基本的清洗技术,然后对清洗后的数据进行建模,以推导出相关主题。你将评估潜在狄利克雷分配LDA)模型,并执行非负矩阵分解NMF)模型。最后,你将解释主题模型的结果,并为给定场景识别最佳的主题模型。我们将看到主题建模如何为文档的潜在结构提供洞察力。在本章结束时,你将能够构建完整的主题模型,为你的业务提供价值和洞察。

介绍

在上一章中,讨论重点是使用降维和自编码技术为建模准备数据。大规模特征集在建模时可能会带来问题,因为多重共线性和大量计算可能会阻碍实时预测。使用主成分分析的降维方法是解决这一问题的一种方法。同样,自编码器旨在找到最佳的特征编码。你可以把自编码器看作是识别数据集中优质交互项的一种手段。现在,让我们超越降维,看看一些实际的建模技术。

主题建模是自然语言处理NLP)的一部分,NLP 是计算机科学领域,研究自然语言的句法和语义分析,随着文本数据集的增加而越来越受欢迎。NLP 几乎可以处理任何形式的语言,包括文本、语音和图像。除了主题建模之外,情感分析、实体识别和对象字符识别也是值得注意的 NLP 应用。

目前,收集和分析的数据不再是以标准表格形式出现,而是更多以不太结构化的形式,如文档、图像和音频文件。因此,成功的数据科学从业者需要精通处理这些多样化数据集的方法。

下面是一个识别文本中的单词并将其分配到主题的示范:

图 7.1:在文本中识别单词并将其分配到主题的示例

图 7.1:在文本中识别单词并将其分配到主题的示例

你立刻想到的问题可能是什么是主题? 让我们通过一个例子来回答这个问题。你可以想象,或者可能已经注意到,在发生重大事件的日子里(例如国家选举、自然灾害或体育赛事冠军),社交媒体网站上的帖子通常会集中讨论这些事件。帖子通常以某种方式反映当天的事件,并且会以不同的方式呈现。这些帖子可以并且会有多个不同的观点,这些观点可以被归类为高层次的主题。如果我们有关于世界杯决赛的推文,这些推文的主题可能涵盖从裁判质量到球迷行为的不同观点。在美国,总统会在每年 1 月中下旬发表一次国情咨文演讲。通过足够数量的社交媒体帖子,我们可以通过使用帖子中的特定关键词来对社交媒体社区对演讲的高层次反应(主题)进行推断或预测。主题模型之所以重要,是因为它们在文本数据中起到的作用类似于经典的统计汇总在数值数据中的作用。也就是说,它们提供了数据的有意义总结。让我们回到国情咨文的例子。快速浏览的目标是确认演讲中的主要观点,这些观点要么引起了观众的共鸣,要么被观众忽视。

主题模型

主题模型属于无监督学习范畴,因为几乎总是,所识别的主题在事先是未知的。因此,没有目标可以进行回归或分类建模。从无监督学习的角度来看,主题模型与聚类算法最为相似,特别是 k 均值聚类。你可能还记得,在 k 均值聚类中,首先确定聚类的数量,然后模型将每个数据点分配到预定数量的聚类中。主题模型通常也是如此。我们在开始时选择主题的数量,然后模型会隔离出形成这些主题的词汇。这是进行高层次主题建模概述的一个很好的起点。

在此之前,让我们检查一下是否已安装并准备好使用正确的环境和库。下表列出了所需的库及其主要用途:

图 7.2:显示不同库及其用途的表格

图 7.2:显示不同库及其用途的表格

如果当前未安装这些库中的任何一个或全部,请通过命令行使用pip安装所需的包;例如,pip install langdetect

即将进行的练习的第 3 步涵盖了从nltk包安装词典。词典只是为特定用途整理的单词集合。下面安装的停用词词典包含了英语中常见的词,这些词无法澄清上下文、含义或意图。这些常见词可能包括theanain等。WordNet 词典提供了帮助词形还原过程的单词映射——如下所述。这些单词映射将诸如runrunningran等词汇联系在一起,所有这些词基本上意味着相同的意思。从高层次来看,词典为数据科学家提供了一种准备文本数据以供分析的方式,无需深入了解语言学或花费大量时间定义单词列表或单词映射。

注意

在下面的练习和活动中,由于支持拉普拉斯·狄利希雷分配(Latent Dirichlet Allocation)和非负矩阵分解(Non-negative Matrix Factorization)的优化算法,结果可能会与显示的略有不同。许多函数没有设置种子功能。

练习 7.01:环境设置

为了检查环境是否准备好进行主题建模,我们将执行几个步骤。其中第一步是加载本章所需的所有库:

  1. 打开一个新的 Jupyter 笔记本。

  2. 导入所需的库:

    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重新执行库导入过程。

  3. 某些文本数据清理和预处理过程需要词典。在这里,我们将安装两个这样的词典。如果已导入nltk库,请执行以下代码:

    nltk.download('wordnet')
    nltk.download('stopwords')
    

    输出如下:

    图 7.4:导入库和下载词典

    图 7.4:导入库和下载词典

  4. 运行matplotlib并指定内联,以便图形可以显示在笔记本中:

    %matplotlib inline
    

笔记本和环境现在已经设置好,可以开始加载数据了。

注意

要访问此部分的源代码,请参考packt.live/34gLGKa

你也可以在packt.live/3fbWQES上在线运行这个示例。

必须执行整个笔记本才能获得预期的结果。

主题模型的高层概述

在分析大量潜在相关的文本数据时,主题模型是一个常用的方法。这里所说的“相关”是指文档描述的是相似的主题。运行任何主题模型所需的唯一数据就是文档本身。不需要额外的数据(无论是元数据还是其他数据)。

简单来说,主题模型通过使用文档中的单词,识别出文档集合(称为语料库)中的抽象主题(也称为主题)。也就是说,如果一个句子包含单词薪水员工会议,可以合理推测该句子的主题是工作。值得注意的是,构成语料库的文档不必是传统意义上的文档——可以是信件或合同。文档可以是任何包含文本的内容,包括推文、新闻标题或转录的语音。

主题模型假设同一文档中的单词是相关的,并利用这一假设通过寻找反复出现在相近位置的单词群体来定义抽象主题。通过这种方式,这些模型是经典的模式识别算法,其中检测到的模式由单词组成。通用的主题建模算法有四个主要步骤:

  1. 确定主题的数量。

  2. 扫描文档并识别共现的单词或短语。

  3. 自动学习描述文档的单词群体(或聚类)。

  4. 输出描述语料库的抽象主题,作为单词群体。

正如步骤 1所述,主题的数量需要在拟合模型之前选择。选择合适的主题数量可能有点棘手,但就像大多数机器学习模型一样,可以通过使用不同主题数量拟合多个模型并基于性能指标选择最佳模型来优化此参数。我们稍后会再次深入探讨这个过程。

以下是通用主题建模的工作流:

图 7.5:通用主题建模工作流

图 7.5:通用主题建模工作流

优化主题数量的参数非常重要,因为这个参数会极大影响主题的连贯性。因为模型在预定义的主题数量约束下,找到最适合语料库的单词群体。如果主题数量过高,主题就会变得不适当的狭窄。过于具体的主题称为过度处理。同样,如果主题数量过低,主题就会变得泛化和模糊。这些类型的主题被认为是欠处理。过度处理和欠处理的主题有时可以通过分别减少或增加主题数量来解决。实际上,主题模型的一个常见且不可避免的结果是,通常至少会有一个主题存在问题。

主题模型的一个关键方面是,它们不会生成具体的单词或短语作为主题,而是生成一组词,每个词代表一个抽象主题。回想之前关于工作的假想句子。构建的主题模型旨在识别该句子所属的假设语料库中的主题时,并不会返回工作这个词作为主题。它会返回一组词,例如薪水单员工老板——这些词描述了该主题,可以从中推断出单词或短语主题。这是因为主题模型理解词的接近性,而不是上下文。模型并不知道薪水单员工老板的含义,它只知道这些词通常在出现时,彼此之间会出现在接近的位置:

图 7.6:从词组推断主题

图 7.6:从词组推断主题

主题模型可以用来预测未知文档所属的主题,但如果你打算进行预测,重要的是要认识到,主题模型只知道用来训练它们的词汇。也就是说,如果未知文档中有训练数据中没有的词,模型将无法处理这些词,即使它们与训练数据中已识别的某个主题相关联。由于这一事实,主题模型往往更多地用于探索性分析和推理,而非预测。

每个主题模型会输出两个矩阵。第一个矩阵包含了词和主题的关系。它列出了与每个主题相关的每个词,并对关系进行了量化。鉴于模型所考虑的词汇数量,每个主题只会用相对较少的词来描述。

词语可以被分配到一个主题,也可以分配到多个主题,并赋予不同的量化值。词语是否被分配到一个或多个主题取决于算法。类似地,第二个矩阵包含了文档和主题的关系。它通过量化文档与主题组合的关系,将每个文档映射到每个主题。

在讨论主题建模时,必须不断强调这样一个事实:代表主题的词组在概念上并不相关,它们仅仅是通过接近性相关。文档中某些词的频繁接近足以定义主题,因为之前提到的假设——同一文档中的所有词都是相关的。

然而,这个假设可能并不成立,或者这些词可能过于通用,无法形成连贯的主题。解释抽象主题涉及平衡文本数据的内在特性与生成的词组。文本数据和语言通常具有高度的变异性、复杂性和上下文性,这意味着任何泛化的结果都需要谨慎对待。

这并不是贬低或无效化模型的结果。在彻底清洗过的文档和适当数量的主题下,词汇组(正如我们将看到的)可以很好地指引我们理解语料库中的内容,并能有效地纳入更大的数据系统。

我们已经讨论了一些主题模型的局限性,但仍有一些额外的要点需要考虑。文本数据的噪声特性可能导致主题模型将与某个主题无关的词语错误地分配到该主题。

再次考虑之前关于工作的句子。词语meeting可能会出现在表示工作主题的词汇组中。也有可能词语long出现在这个组中,但long并不直接与工作相关。Long之所以出现在该组中,可能是因为它经常与词语meeting相近。因此,long可能被错误地(或虚假地)认为与工作相关,并且应该尽可能从主题词汇组中移除。词汇组中的虚假相关词语可能会在分析数据时造成重大问题。

这不一定是模型的缺陷。相反,这是一个特性,考虑到数据中的噪声,模型可能会从数据中提取出一些特殊性,这可能会对结果产生负面影响。虚假的相关性可能是由于数据的收集方式、地点或时间所导致的。如果文档仅在某个特定的地理区域收集,那么与该区域相关的词语可能会不正确地(尽管是偶然的)与模型输出的一个或多个词汇组关联起来。

请注意,随着词汇组中词语的增加,我们可能会将更多文档错误地附加到该主题。应该很容易理解的是,如果我们减少属于某个主题的词语数量,那么该主题将会被分配到更少的文档中。请记住,这并不是坏事。我们希望每个词汇组只包含那些合适的词语,以便将适当的主题分配给适当的文档。

有许多主题建模算法,但也许最著名的两种是潜在狄利克雷分配LDA)和非负矩阵分解NMF)。我们稍后会详细讨论这两种方法。

商业应用

尽管存在一些局限性,主题建模仍然可以提供有助于推动商业价值的可操作性洞察,如果正确使用并在合适的情境下应用。现在,让我们回顾一下主题模型的一些最大应用。

其中一个使用场景是在处理新文本数据时进行探索性数据分析,这些数据集的潜在结构尚不清楚。这相当于为一个未见过的数据集绘制图表并计算摘要统计量,其中包括需要理解其特征的数值和分类变量,在进一步的复杂分析能够合理进行之前。这些主题建模的结果可以帮助我们评估该数据集在未来建模工作中的可用性。例如,如果主题模型返回清晰且明确的主题,那么该数据集将是进一步进行聚类类分析的理想候选者。

确定主题会创建一个额外的变量,可以用来对数据进行排序、分类和/或分块。如果我们的主题模型返回“汽车”、“农业”和“电子产品”作为抽象主题,我们可以将大规模文本数据集筛选至仅包含“农业”作为主题的文档。筛选后,我们可以进行进一步的分析,包括情感分析、再一次的主题建模,或任何我们能想到的其他分析。除了定义语料库中存在的主题外,主题建模还间接返回了许多其他信息,这些信息可以用来进一步分解大型数据集并理解其特征。

其中一个特征是主题的普遍性。想象一下,在一个旨在衡量对某个产品反应的开放式问卷调查中进行分析。我们可以设想,主题模型返回的主题形式为情感。一组词可能是优秀推荐质量,而另一组则可能是垃圾坏掉差劲失望

鉴于这种调查方式,主题本身可能并不令人惊讶,但有趣的是,我们可以统计包含每个主题的文档数量,并从中获取有用的见解。通过这些统计数据,我们可以得出这样的结论:例如,x% 的调查参与者对产品持积极反应,而只有 y% 的参与者持消极反应。实际上,我们所做的就是创建了一个粗略版本的情感分析。

当前,主题模型最常见的用途是作为推荐引擎的一部分。今天的重点是个性化——向消费者提供专门为他们设计和策划的产品。以网站为例,无论是新闻网站还是其他,致力于传播文章的公司,例如雅虎和 Medium,需要让客户继续阅读才能保持运营。保持客户阅读的一种方式是向他们推送他们更有可能阅读的文章。这就是主题建模的作用所在。通过使用由个体之前阅读的文章组成的语料库,主题模型基本上可以告诉我们该订阅者喜欢阅读哪些类型的文章。然后,公司可以访问其库存,找到具有相似主题的文章,并通过该用户的账户页面或电子邮件将它们发送给该用户。这是为了简化使用并保持用户参与的定制策划。

在我们开始准备数据以供模型使用之前,让我们快速加载并探索数据。

练习 7.02: 数据加载

在本练习中,我们将加载并格式化数据。我们将在与练习 7.01设置环境相同的笔记本中执行此练习。尽可能彻底地理解我们将要处理的数据集非常重要。理解的过程从了解数据的大致样貌、数据的大小、存在的列以及识别哪些数据集的方面可能对解决我们要解决的问题有帮助开始。我们将在下面回答这些基本问题。

注意

该数据集来源于archive.ics.uci.edu/ml/datasets/News+Popularity+in+Multiple+Social+Media+Platforms(UCI 机器学习库[archive.ics.uci.edu/ml]。加利福尼亚大学欧文分校信息与计算机科学学院)。

引用:Nuno Moniz 和 Luís Torgo. "在线新闻源的多源社交反馈".CoRR [arXiv:1801.07055 [cs.SI]] (2018)。

数据集也可以从packt.live/2Xin2HC下载。

这是本练习所需的唯一文件。下载并保存到本地后,数据可以加载到笔记本中。

  1. 定义数据路径并使用pandas加载数据:

    path = "News_Final.csv"
    df = pandas.read_csv(path, header=0)
    

    注意

    将文件添加到与您打开笔记本的相同文件夹中。

  2. 执行以下代码简要检查数据:

    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.7: 原始数据

    图 7.7: 原始数据

    从特征上讲,这是一个比运行主题模型所需的更大的数据集。

  3. 请注意,其中一列名为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
    
  4. 现在,提取标题数据并将提取的数据转换为列表对象。打印列表的前五个元素以及列表的长度,以确认提取是否成功:

    raw = df["Headline"].tolist()
    print("HEADLINES:\n{lines}\n".format(lines=raw[:5]))
    print("LENGTH:\n{length}\n".format(length=len(raw)))
    

    输出结果如下:

    图 7.8:标题列表

图 7.8:标题列表

现在数据已加载并正确格式化,我们来谈谈文本数据清洗,然后进行一些实际的清洗和预处理。出于教学目的,清洗过程最初将在单个标题上进行构建和执行。一旦我们建立了清洗过程并在示例标题上进行了测试,我们将返回并对每个标题运行该过程。

注意

要访问此特定部分的源代码,请参见packt.live/34gLGKa

你也可以在网上运行这个示例,访问packt.live/3fbWQES

你必须执行整个 Notebook 才能得到期望的结果。

清洗文本数据

所有成功建模练习的一个关键组成部分是一个经过适当和充分预处理的干净数据集,专门为特定的数据类型和分析任务进行预处理。文本数据也不例外,因为它在原始形式下几乎无法使用。无论运行什么算法:如果数据没有经过适当准备,结果最好的情况下是没有意义的,最坏的情况下是误导性的。正如谚语所说,垃圾进,垃圾出。对于主题建模,数据清洗的目标是通过去除所有可能干扰的内容,来孤立每个文档中可能相关的词汇。

数据清洗和预处理几乎总是特定于数据集的,这意味着每个数据集都需要一组独特的清洗和预处理步骤,专门用于处理其中的问题。对于文本数据,清洗和预处理步骤可能包括语言过滤、移除网址和屏幕名称、词形还原以及停用词移除等。我们将在接下来的章节中详细探讨这些步骤,并在即将进行的练习中实施这些思想,届时一个包含新闻标题的数据集将被清理用于主题建模。

数据清洗技术

重申一下之前的观点,清洗文本以进行主题建模的目标是从每个文档中提取可能与发现语料库抽象主题相关的单词。这意味着需要去除常见词、短词(通常更常见)、数字和标点符号。清洗数据没有固定的流程,因此理解所清洗数据类型中的典型问题点并进行广泛的探索性工作非常重要。

现在,让我们讨论一些我们将在数据清洗中使用的文本清洗技巧。进行任何涉及文本的建模任务时,首先需要做的事情之一是确定文本的语言。在这个数据集中,大多数标题是英文的,因此为了简便起见,我们将删除非英文的标题。构建非英文文本数据的模型需要额外的技能,其中最基本的是对所建模语言的流利掌握。

数据清洗的下一个关键步骤是移除文档中所有与基于单词的模型无关的元素,或者可能成为噪声来源、掩盖结果的元素。需要移除的元素可能包括网站地址、标点符号、数字和停用词。停用词基本上是一些简单的、常用的词(包括wearethe)。需要注意的是,并没有一个权威的停用词词典;每个词典略有不同。尽管如此,每个词典都包含一些常见词,这些词被认为与主题无关。主题模型试图识别那些既频繁又不那么频繁的词,这些词足以描述一个抽象的主题。

移除网站地址有类似的动机。特定的网站地址出现的频率非常低,但即使某个特定网站地址足够多次出现在文档中并且能与某个主题相关联,网站地址的解释方式却不同于单词。去除文档中无关的信息,可以减少那些可能妨碍模型收敛或掩盖结果的噪声。

词形还原,像语言检测一样,是所有涉及文本的建模活动中的一个重要组成部分。它是将单词还原为其基本形式的过程,目的是将应该相同但因时态或词性变化而不同的单词归为一类。考虑单词runningrunsran。这三个单词的基本形式是run。词形还原的一个很好的方面是,它会查看句子中的所有单词(换句话说,它会考虑上下文),然后决定如何改变每个单词。词形还原,像大多数前述的清洗技巧一样,简单地减少了数据中的噪声,使我们能够识别出干净且易于解释的主题。

现在,拥有了基本的文本清洗技巧知识,让我们将这些技巧应用于实际数据中。

练习 7.03:逐步清洗数据

在本练习中,我们将学习如何实现一些清理文本数据的关键技术。每个技术将在我们进行练习时进行解释。每一步清理后,都会使用print输出示例标题,以便我们观察从原始数据到模型数据的演变:

  1. 选择第六个标题作为我们构建并测试清理过程的示例。第六个标题并不是随机选择的,它是因为包含了在清理过程中将要处理的特定问题:

    example = raw[5]
    print(example)
    

    输出如下:

    图 7.9:第六个标题

    图 7.9:第六个标题

  2. 使用 langdetect 库来检测每个标题的语言。如果语言不是英语(en),则从数据集中删除该标题。detect 函数仅仅是检测传入文本的语言。当该函数无法检测出语言时(偶尔会发生),只需将语言设置为 none,以便稍后删除:

    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)))
    

    输出如下:

    DETECTED LANGUAGE:
    en
    
  3. 使用空格将包含标题的字符串拆分成片段,称为标记。返回的对象是由构成标题的单词和数字组成的列表。将标题字符串拆分成标记,使清理和预处理过程更加简单。市场上有多种类型的标记器。请注意,NLTK 本身提供了多种类型的标记器。每个标记器考虑了将句子拆分成标记的不同方式。最简单的一种是基于空格拆分文本。

    example = example.split(" ")
    print(example)
    

    输出如下:

    图 7.10:使用空格拆分字符串

    图 7.10:使用空格拆分字符串

  4. 使用正则表达式搜索包含 http://https:// 的标记来识别所有 URL。将 URL 替换为 'URL' 字符串:

    example = ['URL' if bool(regex.search("http[s]?://", i)) \
               else i for i in example]
    print(example)
    

    输出如下:

    图 7.11:将 URL 替换为 URL 字符串

    图 7.11:将 URL 替换为 URL 字符串

  5. 使用正则表达式将所有标点符号和换行符号(\n)替换为空字符串:

    example = [regex.sub("[^\\w\\s]|\n", "", i) for i in example]
    print(example)
    

    输出如下:

    图 7.12:使用正则表达式将标点符号替换为空字符串

    图 7.12:使用正则表达式将标点符号替换为空字符串

  6. 使用正则表达式将所有数字替换为空字符串:

    example = [regex.sub("^[0-9]*$", "", i) for i in example]
    print(example)
    

    输出如下:

    图 7.13:将数字替换为空字符串

    图 7.13:将数字替换为空字符串

  7. 将所有大写字母转换为小写字母。虽然将所有内容转换为小写字母不是强制步骤,但它有助于简化复杂性。将所有内容转换为小写字母后,跟踪的内容较少,因此出错的机会也较小:

    example = [i.lower() if i not in ["URL"] else i for i in example]
    print(example)
    

    输出如下:

    图 7.14:将大写字母转换为小写字母

    图 7.14:将大写字母转换为小写字母

  8. 移除在 步骤 4 中添加的 'URL' 字符串作为占位符。先前添加的 'URL' 字符串实际上在建模中并不需要。如果它似乎无害,留着它也无妨,但要考虑到 'URL' 字符串可能自然出现在标题中,我们不希望人为地增加它的出现频率。此外,'URL' 字符串并非出现在每个标题中,因此,留下它可能会无意间在 'URL' 字符串和某些主题之间建立联系:

    example = [i for i in example if i not in ["URL",""]]
    print(example)
    

    输出结果如下:

    图 7.15:字符串 URL 已移除

    图 7.15:字符串 URL 已移除

  9. 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.16:停用词列表

    图 7.16:停用词列表

    在使用字典之前,重要的是要重新格式化单词,使其与我们标题的格式匹配。这包括确认所有内容都是小写且没有标点符号。

  10. 现在我们已经正确格式化了 stopwords 字典,使用它从标题中移除所有停用词:

    example = [i for i in example if i not in list_stop_words]
    print(example)
    

    输出结果如下:

    图 7.17:从标题中移除停用词

    图 7.17:从标题中移除停用词

  11. 通过定义一个可以应用于每个标题的函数来执行词形还原。词形还原需要加载 wordnet 字典。morphy 函数会处理文本中的每个单词,并返回其标准形式(如果识别到的话)。例如,如果输入的单词是 runningranmorphy 函数将返回 run

    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.18:执行词形还原后的输出

    图 7.18:执行词形还原后的输出

  12. 从词元列表中移除所有长度为四个字符或更少的单词。这个步骤的假设是,短单词通常更为常见,因此不会为我们从主题模型中提取的洞察提供帮助。请注意,移除某些长度的单词并不是一种适用于所有情况的技巧;它仅适用于特定情况。例如,短单词有时可能非常指示某些主题,如识别动物(例如:dog,cat,bird)。

    example = [i for i in example if len(i) >= 5]
    print(example)
    

    输出结果如下:

图 7.19:第六个标题清理后的结果

图 7.19:第六个标题清理后的结果

现在我们已经逐一完成了清理和预处理步骤,接下来需要将这些步骤应用到接近 100,000 个标题上。最有效的方法是编写一个包含上述所有步骤的函数,并以某种迭代方式将该函数应用于语料库中的每个文档。这个过程将在下一个练习中进行。

注意

要访问此特定部分的源代码,请参考 packt.live/34gLGKa

您还可以在packt.live/3fbWQES在线运行此示例。

您必须执行整个 Notebook 才能获得所需的结果。

练习 7.04:完整数据清洗

在本次练习中,我们将把步骤 2步骤 12练习 7.03《逐步清洗数据》整合为一个函数,应用于每个标题。该函数将以字符串格式的标题作为输入,输出将是一个清洗后的标题列表(tokens)。主题模型要求文档格式为字符串,而不是 tokens 的列表,因此在步骤 4中,tokens 列表将被转换回字符串:

  1. 定义一个函数,包含练习 7.03《逐步清洗数据》中的所有独立步骤:

    Exercise7.01-Exercise7.12.ipynb
    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]
    The complete code for this step can be found at https://packt.live/34gLGKa.
    
  2. 在每个标题上执行该函数。Python 中的map函数是一种很好的方式,可以将用户定义的函数应用于列表中的每个元素。将map对象转换为列表,并将其分配给clean变量。clean变量是一个列表的列表:

    tick = time()
    clean = list(map(do_headline_cleaning, raw))
    print(time()-tick)
    
  3. 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.20:示例标题及标题列表的长度

    图 7.20:示例标题及标题列表的长度

  4. 对于每个单独的标题,使用空格分隔符连接 tokens。现在这些标题将变成一个无结构的单词集合,对于人类阅读者来说没有意义,但对于主题建模来说是理想的:

    clean_sentences = [" ".join(i) for i in clean]
    print(clean_sentences[0:10])
    

    清洗后的标题应类似于以下内容:

    图 7.21:为建模清洗后的标题

图 7.21:为建模清洗后的标题

注意

要访问此特定部分的源代码,请参考packt.live/34gLGKa

您还可以在packt.live/3fbWQES在线运行此示例。

您必须执行整个 Notebook 才能获得所需的结果。

总结一下,清洗和预处理的工作实际上是剔除数据中的噪音,以便模型能够专注于数据中可能推动洞察的元素。例如,任何与主题无关的词语不应影响主题,但如果不小心留下这些词,它们可能会干扰。

为了避免我们可以称之为假信号的内容,我们会移除这些词语。同样,由于主题模型无法辨别上下文,标点符号是无关的,因此会被移除。即便模型可以在不清洗数据的情况下找到主题,未经清洗的数据可能包含成千上万甚至百万个多余的单词和随机字符(取决于语料库中的文档数量),这可能显著增加计算需求。因此,数据清洗是主题建模的一个重要部分。你将在接下来的活动中练习这个过程。

活动 7.01:加载和清洗 Twitter 数据

在本活动中,我们将加载并清洗 Twitter 数据,以便在后续活动中进行建模。我们对头条数据的使用是持续进行的,因此让我们在一个单独的 Jupyter 笔记本中完成此活动,但所有的要求和导入的库保持一致。

目标是处理原始推文数据,清洗它,并生成与前一个练习中第 4 步相同的输出。输出应该是一个列表,其长度应该与原始数据文件中的行数相似,但可能不完全相同。这是因为推文在清洗过程中可能会被丢弃,原因可能有很多,比如推文使用了非英语语言。列表中的每个元素应该代表一条推文,并且只包含可能与主题形成相关的推文内容。

以下是完成此活动的步骤:

  1. 导入必要的库。

  2. packt.live/2Xje5xF加载 LA Times 健康 Twitter 数据(latimeshealth.txt)。

    注意

    数据集来源于archive.ics.uci.edu/ml/datasets/Health+News+in+Twitter(UCI 机器学习库[archive.ics.uci.edu/ml]。加利福尼亚大学欧文分校信息与计算机科学学院)。

    引用:Karami, A., Gangopadhyay, A., Zhou, B., & Kharrazi, H.(2017)。健康和医学语料库中的模糊方法主题发现。《国际模糊系统杂志》,1-12。

    它也可以在 GitHub 上找到,packt.live/2Xje5xF

  3. 进行快速的探索性分析,确定数据的大小和结构。

  4. 提取推文文本并将其转换为列表对象。

  5. 编写一个函数,执行语言检测和基于空格的分词,然后分别用SCREENNAMEURL替换屏幕名称和网址。该函数还应移除标点符号、数字以及SCREENNAMEURL的替换内容。将所有内容转换为小写字母,除了SCREENNAMEURL。它应移除所有停用词,执行词形还原,并且只保留长度为五个字母或以上的单词。

  6. 第 5 步中定义的函数应用于每一条推文。

  7. 移除输出列表中值为None的元素。

  8. 将每条推文的元素重新转换为字符串。使用空格进行连接。

  9. 保持笔记本打开,以便进行未来的活动。

    注意

    本章中的所有活动需要在同一个笔记本中执行。

    输出结果如下:

    图 7.22:已清洗的推文,用于建模

图 7.22:已清洗的推文,用于建模

注意

本活动的解决方案可以在第 478 页找到。

潜在狄利克雷分配(Latent Dirichlet Allocation)

2003 年,David Blei、Andrew Ng 和 Michael Jordan 发表了他们关于主题建模算法潜在狄利克雷分配LDA)的文章。LDA 是一种生成概率模型。这意味着建模过程从文本开始,反向工作,通过假设生成它的过程,以识别感兴趣的参数。在这种情况下,感兴趣的是生成数据的主题。这里讨论的过程是 LDA 的最基本形式,但对于学习来说,它也是最容易理解的。

语料库中有 M 个可用于主题建模的文档。每个文档可以视为N个单词的序列,即序列(w1,w2… wN)。

对于语料库中的每个文档,假设的生成过程是:

  1. 选择一个包含餐具的图片    自动生成的描述,其中N是单词的数量,λ是控制泊松分布的参数。

  2. 选择面部插图    自动生成的描述,其中 主题分布是主题的分布。

  3. 对于每个N个单词,Wn,选择主题 面部插图    自动生成的描述,并从中选择单词Wn,来自一个包含剪贴画的图片    自动生成的描述

让我们更详细地了解一下生成过程。前面提到的三个步骤会对语料库中的每个文档重复。初始步骤是通过从大多数情况下的泊松分布中采样来选择文档中的单词数。需要注意的是,由于 N 与其他变量是独立的,因此与其生成相关的随机性在算法推导中大多被忽略。

在选择N之后,接下来是生成主题混合或主题分布,这对每个文档来说是独特的。可以将其视为每个文档的主题列表,概率表示每个主题所代表的文档部分。考虑三个主题:A、B 和 C。一个示例文档可能是 100%的主题 A,75%的主题 B 和 25%的主题 C,或者是无数其他的组合。

最后,文档中的特定单词是通过概率语句从所选主题及该主题的单词分布中选择的。请注意,文档并不真正以这种方式生成,但它是一个合理的代理方法。

这个过程可以被看作是一个分布上的分布。一个文档从文档集合(分布)中选择出来,然后从该文档的主题概率分布中选择一个主题(通过多项式分布),该分布由 Dirichlet 分布生成。

图 7.23:LDA 的图形表示

图 7.23:LDA 的图形表示

构建表示 LDA 解法的公式最直接的方法是通过图形表示。这个特定的表示方法被称为板符号图形模型,因为它使用板块来表示过程中的两个迭代步骤。

你会记得生成过程是针对语料库中的每个文档执行的,因此最外层的板块(标记为M)表示对每个文档的迭代。类似地,步骤 3中对词汇的迭代通过图中的最内层板块表示,标记为N

圆圈代表参数、分布和结果。阴影部分的圆圈,标记为w,是选定的词汇,这是唯一已知的数据,因此用于反向推导生成过程。除了w,图中的其他四个变量定义如下:

  • 包含剪刀、工具的图片    描述自动生成:主题-文档 Dirichlet 分布的超参数。

  • 包含家具的图片    描述自动生成:每个主题的词汇分布。

  • 描述自动生成:这是主题的潜在变量。

  • 这是潜在变量:这是每个文档主题分布的潜在变量。

包含剪刀、工具的图片描述自动生成包含家具的图片描述自动生成 控制文档中主题的频率和主题中词汇的频率。如果包含剪刀、工具的图片描述自动生成 增加时,文档变得越来越相似,因为每个文档中的主题数量增加。另一方面,如果包含剪刀、工具的图片描述自动生成 减少时,文档之间的相似度逐渐降低,因为每个文档中的主题数量减少。 包含家具的图片描述自动生成 参数表现类似。如果包含家具的图片描述自动生成 增加时,文档中使用的词汇更多,用来建模一个主题,而较低的包含家具的图片自动生成的描述导致每个主题所使用的词汇数量较少。鉴于 LDA 中分布的复杂性,没有直接的解决方案,因此需要某种近似算法来生成结果。LDA 的标准近似算法将在下一节中讨论。

变分推断

LDA 的一个主要问题是条件概率(分布)的评估难以管理,因此,概率不是直接计算,而是通过近似来得到。变分推断是其中一种较为简单的近似算法,但它有一个广泛的推导过程,需要对概率有深入的理解。为了更多地关注 LDA 的应用,本节将简要介绍变分推断在该背景下的应用,但不会深入探讨该算法。

让我们花点时间直观地理解变分推断算法。从随机地将语料库中每篇文档中的每个单词分配到一个主题开始。然后,分别为每个文档和每个文档中的每个单词计算两个比例。这些比例分别是当前分配给该主题的文档中单词的比例,P(Topic|Document),以及特定单词在所有文档中分配到该主题的比例,P(Word|Topic)。将这两个比例相乘,使用得到的比例将该单词分配到一个新的主题。重复这个过程,直到达到一个稳定状态,在这个状态下,主题分配不会发生显著变化。然后,使用这些分配来估计文档内部的主题混合和主题内的单词混合。

图 7.24:变分推断过程

图 7.24:变分推断过程

变分推断的思路是,如果实际分布是不可处理的,那么应找到一个更简单的分布,称为变分分布,它非常接近真实分布且是可处理的,从而使得推断成为可能。换句话说,由于由于实际分布的复杂性,推断实际分布是不可能的,我们试图找到一个更简单的分布,它能够很好地近似实际分布。

让我们暂时从理论中休息一下,来看一个例子。变分推断就像是在拥挤的动物园中观察动物。动物园里的动物处于一个封闭的栖息地,在这个例子中,栖息地就是后验分布。游客无法实际进入栖息地,因此他们必须尽可能靠近栖息地观察,这就是后验近似(即栖息地的最佳近似)。如果动物园里有很多人,可能很难找到那个最佳的观察点。人们通常从人群的后面开始,逐步朝着最佳观察点移动。游客从人群后面移动到最佳观察点的路径就是优化路径。变分推断实际上就是在知道无法真正到达期望点的情况下,尽可能接近期望点的过程。

首先,选择一个分布族(即二项分布、高斯分布、指数分布等),q,并根据新的变分参数进行条件化。这些参数经过优化,使得原始分布(实际上是后验分布,对于熟悉贝叶斯统计的人来说)和变分分布尽可能接近。变分分布会足够接近原始的后验分布,因此可以作为代理,基于它进行的任何推断都适用于原始后验分布。分布族 q 的通用公式如下:

图 7.25:分布族的公式,q

图 7.25:分布族的公式,q

有一大堆潜在的变分分布可以用作后验分布的近似。从这些分布中选择一个初始的变分分布,作为优化过程的起点,该过程会不断接近最佳分布。最佳参数是指最能近似后验分布的分布参数。使用Kullback-LeiblerKL)散度来衡量这两个分布的相似性。KL 散度表示如果我们用一个分布来近似另一个分布时,所产生的预期误差量。具有最佳参数的分布将具有最小的 KL 散度,并且与真实分布相比。

一旦确定了最佳分布,也就意味着确定了最佳参数,可以利用它来生成输出矩阵并执行任何需要的推断。

词袋模型

文本不能直接传递给任何机器学习算法;它首先需要被数值编码。在机器学习中处理文本的一个直接方法是使用词袋模型,它移除了关于单词顺序的所有信息,专注于每个单词的出现程度(即计数或频率)。

Python 的sklearn库可以用来将前一个练习中创建的清洗后的向量转换为 LDA 模型所需的结构。由于 LDA 是一个概率模型,我们不希望对单词出现频率进行任何缩放或加权;相反,我们选择仅输入原始计数。

词袋模型的输入将是练习 7.04中返回的清洗字符串列表,即完整数据清理。输出将是文档编号、单词的数值编码以及该单词在文档中出现的次数。这三个项目将以元组和整数的形式呈现。

元组将类似于(0, 325),其中 0 是文档编号,325 是数值编码的单词。请注意,325 将是该单词在所有文档中的编码。整数部分将是计数。我们将在本章中运行的词袋模型来自sklearn,分别称为CountVectorizerTfIdfVectorizer。第一个模型返回原始计数,第二个返回一个缩放值,我们稍后将讨论这一点。

一个重要的注意事项是,本章涵盖的两种主题模型的结果可能会因运行而异,即使数据相同,这也是由于随机性所导致。LDA 中的概率和优化算法都不是确定性的,因此不要惊讶于你的结果与接下来展示的结果有所不同。在下一个练习中,我们将运行计数向量化器,以数值方式编码我们的文档,以便能够继续使用 LDA 进行主题建模。

练习 7.05:使用计数向量化器创建词袋模型

在这个练习中,我们将运行sklearn中的CountVectorizer,将之前创建的清洗后的标题向量转换为词袋数据结构。此外,我们还将定义一些将在建模过程中使用的变量:

  1. 定义number_wordsnumber_docsnumber_features。前两个变量控制 LDA 结果的可视化。number_features变量控制将在特征空间中保留的词汇数量:

    number_words = 10
    number_docs = 10
    number_features = 1000
    
  2. 运行计数向量化器并打印输出。这里有三个关键输入参数:max_dfmin_dfmax_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.26:词袋数据结构

    图 7.26:词袋数据结构

  3. 从向量化器中提取特征名称和单词。模型只接收单词的数值编码,因此将特征名称向量与结果合并,将使得解释过程更加容易:

    feature_names_vec1 = vectorizer1.get_feature_names()
    

这个练习涉及文档的枚举,用于 LDA 模型。所需的格式是词袋模型。也就是说,词袋模型仅仅是列出每个文档中出现的所有词汇,并计算每个词在每个文档中出现的次数。通过使用sklearn完成这一任务后,接下来是探索 LDA 模型评估过程。

注意

要访问此特定部分的源代码,请参考packt.live/34gLGKa

你还可以在网上运行此示例,访问packt.live/3fbWQES

你必须执行整个 Notebook 才能获得所需的结果。

困惑度

模型通常具有可用于评估其性能的指标。主题模型也不例外,尽管在这种情况下,性能的定义稍有不同。在回归和分类中,预测值可以与实际值进行比较,从中可以计算出明确的性能度量。

对于主题模型,预测的可靠性较低,因为模型仅了解它所训练过的词汇,而新文档可能并未包含这些词汇,尽管它们可能包含相同的主题。由于这一差异,主题模型的评估使用了专门针对语言模型的度量指标,称为困惑度

困惑度(Perplexity,缩写为 PP)衡量的是在任何给定词语后平均可以跟随的不同且同样最可能的词汇数量。我们以两个词为例:theannounce。词the可以引出大量同样最可能的词汇,而词announce后面可以跟随的同样最可能的词汇数量则明显较少——尽管仍然是一个很大的数字。

其思想是,平均而言,后面能够跟随更少数量的同样最可能出现的单词的单词,越具体,越能够紧密地与主题联系。因此,较低的困惑度分数意味着更好的语言模型。困惑度与熵非常相似,但通常使用困惑度,因为它更容易解释。正如我们稍后将看到的,它可以用于选择最佳的主题数量。假设m是单词序列中的单词数,困惑度定义为:

图 7.27:困惑度公式

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_27.jpg)

图 7.27:困惑度公式

在这个公式中,w1,…,wm 是构成测试数据集中某文档的单词。这些单词的联合概率,P(w1,…,wm),衡量了测试文档与现有模型的契合度。较高的概率意味着模型更强。概率会被提升到-1/m*的幂,以根据每个文档中的单词数量对分数进行标准化,并使较低的值更优。两者的变化都增加了分数的可解释性。困惑度分数,类似于均方根误差,作为单独的指标意义不大。它通常作为一个比较指标使用。即,构建几个模型,计算它们的困惑度分数并进行比较,以确定最佳的模型,从而继续前进。

如前所述,LDA 有两个必需的输入。第一个是文档本身,第二个是主题数量。选择合适的主题数量可能非常棘手。找到最佳主题数量的一种方法是对多个主题数量进行搜索,并选择与最小困惑度分数对应的主题数量。在机器学习中,这种方法被称为网格搜索。接下来的练习中,我们将使用网格搜索来找到最佳主题数量。

练习 7.06:选择主题数量

在本练习中,我们使用适配不同主题数量的 LDA 模型的困惑度分数来确定应该继续使用的主题数量。请记住,原始数据集中的标题已经被分成了四个主题。让我们看看这种方法是否能得到四个主题:

  1. 定义一个函数,适配不同主题数量的 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)
    
  2. 执行在步骤 1中定义的函数。ntopics输入是一个包含主题数量的数字列表,列表的长度和数值均可变。打印出数据框:

    df_perplexity, optimal_num_topics = \
    perplexity_by_ntopic(clean_vec1, ntopics=[1, 2, 3, 4, 6, 8, 10])
    print(df_perplexity)
    

    输出如下:

    图 7.28:包含主题数量和困惑度分数的数据框

    ](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_28.jpg)

    图 7.28:包含主题数量和困惑度分数的数据框

  3. 绘制困惑度分数作为主题数的函数。这只是查看 步骤 2 中 DataFrame 中结果的另一种方式:

    df_perplexity.plot.line("Number Of Topics", "Perplexity Score")
    

    绘图结果如下:

    ![Figure 7.29: 主题数与困惑度的线图视图 B15923_07_29.jpg

图 7.29:主题数与困惑度的线图视图

正如 DataFrame 和绘图所示,使用困惑度得出的最佳主题数为三。将主题数设为四产生了第二低的困惑度。因此,虽然结果与原始数据集中包含的信息并不完全匹配,但这些结果足以让我们对网格搜索方法识别最佳主题数感到满意。关于为何网格搜索返回三个而不是四个结果,我们将在即将进行的练习中深入探讨。

要访问本节的源代码,请参阅 packt.live/34gLGKa

您也可以在 packt.live/3fbWQES 上线运行此示例。

您必须执行整个笔记本才能获得所需的结果。

现在我们已经选择了最佳主题数,将使用该主题数构建我们的官方 LDA 模型。然后,该模型将用于创建可视化效果,并定义语料库中存在的主题列表。

练习 7.07:运行 LDA

在本练习中,我们将实施 LDA 并检查结果。LDA 输出两个矩阵。第一个是主题-文档矩阵,第二个是词-主题矩阵。我们将查看这些矩阵,这些矩阵是模型返回的,并且格式化为更易于理解的表格:

  1. 使用在 练习 7.06选择主题数 中找到的最佳主题数拟合 LDA 模型:

    lda = sklearn.decomposition.LatentDirichletAllocation\
          (n_components=optimal_num_topics,\
           learning_method="online",\
           random_state=0)
    lda.fit(clean_vec1)
    

    输出如下:

    ![Figure 7.30: LDA 模型 B15923_07_30.jpg

    图 7.30:LDA 模型

  2. 输出主题-文档矩阵及其形状,以确认其与主题数和文档数的对齐情况。矩阵的每一行是主题的文档分布:

    lda_transform = lda.transform(clean_vec1)
    print(lda_transform.shape)
    print(lda_transform)
    

    输出如下:

    (92946, 3)
    [[0.04761958 0.90419577 0.04818465]
     [0.04258906 0.04751535 0.90989559]
     [0.16656181 0.04309434 0.79034385]
     ...
     [0.0399815  0.51492894 0.44508955]
     [0.06918206 0.86099065 0.06982729]
     [0.48210053 0.30502833 0.21287114]]
    
  3. 输出词-主题矩阵及其形状,以确认其与 练习 7.05使用计数向量化器创建词袋模型 中指定的特征数(词)和输入的主题数的对齐情况。每一行基本上是每个单词分配给该主题的流行度。流行度分数可以转换为每个主题的词分布:

    lda_components = lda.components_
    print(lda_components.shape)
    print(lda_components)
    

    输出如下:

    (3, 1000)
    [[3.35570079e-01 1.98879573e+02 9.82489014e+00 ... 3.35388004e-01
      2.04173562e+02 4.03130268e-01]
     [2.74824227e+02 3.94662558e-01 3.63412044e-01 ... 3.45944379e-01
      1.77517291e+02 4.61625408e+02]
     [3.37041234e-01 7.36749100e+01 2.05707096e+02 ... 2.31714093e+02
      1.21765267e+02 7.71397922e-01]]
    
  4. 定义一个函数,将两个输出矩阵格式化为易于阅读的表格:

    Exercise7.01-Exercise7.12.ipynb
    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 = {}
    The complete code for this step can be found at https://packt.live/34gLGKa.
    

    该函数可能有些难以操作,所以让我们一起逐步分析。首先创建WH矩阵,包括将W的分配计数转换为每个主题的词汇分布。然后,遍历每个主题。在每次遍历中,识别与每个主题相关的前几个词汇和文档。最后,将结果转换为两个数据框。

  5. 执行在步骤 4中定义的函数:

    W_df, H_df = get_topics(mod=lda, \
                            vec=clean_vec1, \
                            names=feature_names_vec1, \
                            docs=raw, \
                            ndocs=number_docs, \
                            nwords=number_words)
    
  6. 打印出词汇-主题数据框。它展示了与每个主题相关的前 10 个词汇(按分布值排序)。通过这个数据框,我们可以识别出词汇分组所代表的抽象主题。关于抽象主题的更多内容将在后续介绍:

    print(W_df)
    

    输出如下:

    图 7.31:词汇-主题表

    图 7.31:词汇-主题表

  7. 打印出主题-文档数据框。这显示了与每个主题最密切相关的 10 篇文档。其值来自每篇文档的主题分布:

    print(H_df)
    

    输出如下:

    图 7.32:主题-文档表

图 7.32:主题-文档表

词汇-主题数据框的结果显示,抽象主题包括巴拉克·奥巴马、经济和微软。有趣的是,描述经济的词汇分组中包含了对巴勒斯坦的提及。原始数据集中的四个主题都在词汇-主题数据框的输出中得到了体现,但并没有以预期的完全独立的方式展现出来。我们可能面临两种问题。

首先,引用经济和巴勒斯坦的主题可能还不够成熟,这意味着增加主题的数量可能会解决这个问题。另一个潜在的问题是 LDA 在处理相关主题时效果不佳。在练习 7.09中,尝试四个主题,我们将尝试扩展主题的数量,这将帮助我们更好地理解为什么其中一个词汇分组似乎是多个主题的混合。

注意

要访问此特定部分的源代码,请参考packt.live/34gLGKa

你也可以在线运行这个示例,访问packt.live/3fbWQES

你必须执行整个 Notebook 才能得到预期的结果。

可视化

使用sklearn的 LDA 模型在 Python 中的输出可能难以直接解读。与大多数建模工作一样,数据可视化在解读和传达模型结果时有很大帮助。一种 Python 库pyLDAvis直接与sklearn模型对象集成,生成直观的图形。这个可视化工具返回一个直方图,展示与每个主题最紧密相关的词汇,以及一个双变量图(PCA 中常用),每个圆圈代表一个主题。通过双变量图,我们可以了解每个主题在整个语料库中的普遍性,这通过圆圈的面积来反映,以及主题之间的相似性,这通过圆圈的接近程度来体现。

理想的情况是图中的圆圈应均匀分布,且大小合理一致。也就是说,我们希望主题清晰区分,并在语料库中均匀分布。除了 pyLDAvis 图形外,我们还将利用前一章节讨论的 t-SNE 模型,生成主题-文档矩阵的二维表示,这个矩阵的每一行表示一个文档,每一列表示该主题描述该文档的概率。

在完成 LDA 模型拟合后,让我们创建一些图形,帮助我们深入理解结果。

练习 7.08:可视化 LDA

可视化是探索主题模型结果的有力工具。在本练习中,我们将观察三种不同的可视化方式。它们分别是基本的直方图和使用 t-SNE 及 PCA 的专业可视化:

  1. 运行并显示 pyLDAvis。此图是交互式的。点击每个圆圈时,直方图会更新,显示与该特定主题相关的顶部词汇。以下是此交互式图的一种视图:

    lda_plot = pyLDAvis.sklearn\
              .prepare(lda, clean_vec1, vectorizer1, R=10)
    pyLDAvis.display(lda_plot)
    

    绘图结果如下:

    图 7.33:LDA 模型的直方图和双变量图

    图 7.33:LDA 模型的直方图和双变量图

  2. 定义一个拟合 t-SNE 模型并绘制结果的函数。定义完该函数后,将详细描述函数的各个部分,以便步骤清晰:

    Exercise7.01-Exercise7.12.ipynb
    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 = []
    The complete code for this step can be found at https://packt.live/34gLGKa.
    

    步骤 1:该函数首先通过输入的阈值过滤主题-文档矩阵。由于有成千上万的标题,包含所有标题的图形会难以阅读,因此不具有帮助性。因此,只有当分布值大于或等于输入阈值时,函数才会绘制该文档:

    index_meet_threshold = numpy.amax(data, axis=1) >= threshold
    lda_transform_filt = data[index_meet_threshold]
    

    步骤 2:数据过滤完成后,运行 t-SNE,其中组件数量为 2,以便我们能够在二维中绘制结果:

    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)
    

    步骤 3:创建一个向量,用来标示每个文档最相关的主题。该向量将用于根据主题为绘图着色:

    most_prob_topic = []
    for i in range(tsne_fit.shape[0]):
        most_prob_topic.append(lda_transform_filt[i].argmax())
    

    步骤 4:为了了解主题在语料库中的分布以及阈值筛选的影响,该函数返回主题向量的长度,并给出每个主题及其分布值最大文档数:

    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))
    

    步骤 5:创建并返回绘图:

    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)
    
  3. 执行函数:

    plot_tsne(data=lda_transform, threshold=0.75)
    

    输出如下:

    图 7.34:t-SNE 绘图,展示了主题在语料库中的分布指标

图 7.34:t-SNE 绘图,展示了主题在语料库中的分布指标

可视化结果显示,使用三个主题的 LDA 模型整体产生了良好的结果。在双图中,圆圈的大小适中,表明这些主题在语料库中呈现一致性,且圆圈之间的间隔较好。t-SNE 图显示出明显的聚类,支持双图中圆圈之间的分离。唯一的明显问题,之前已经讨论过,就是其中一个主题包含了似乎与该主题不太相关的词汇。

注意

要访问此特定部分的源代码,请参阅 packt.live/34gLGKa

你也可以在线运行此示例,网址:packt.live/3fbWQES

必须执行整个 Notebook 才能获得预期的结果。

在下一个练习中,让我们使用四个主题重新运行 LDA 模型。

练习 7.09:尝试四个主题

在这个练习中,LDA 模型的主题数量设置为四。这样做的动机是尝试解决三主题 LDA 模型中可能存在的一个问题,该主题包含与巴勒斯坦和经济相关的词汇。我们首先会执行这些步骤,然后在最后查看结果:

  1. 运行一个主题数量为四的 LDA 模型:

    lda4 = sklearn.decomposition.LatentDirichletAllocation(\
           n_components=4,  # number of topics data suggests \
           learning_method="online", \
           random_state=0)
    lda4.fit(clean_vec1)
    

    输出结果如下:

    图 7.35:LDA 模型

    图 7.35:LDA 模型

  2. 执行之前定义的 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)
    
  3. 打印词汇-主题表:

    print(W_df4)
    

    输出结果如下:

    图 7.36:使用四主题 LDA 模型的词汇-主题表

    图 7.36:使用四主题 LDA 模型的词汇-主题表

  4. 打印文档-主题表:

    print(H_df4)
    

    输出结果如下:

    图 7.37:使用四主题 LDA 模型的文档-主题表

    图 7.37:使用四主题 LDA 模型的文档-主题表

  5. 使用 pyLDAvis 显示 LDA 模型的结果:

    lda4_plot = pyLDAvis.sklearn\
               .prepare(lda4, clean_vec1, vectorizer1, R=10)
    pyLDAvis.display(lda4_plot)
    

    图像如下:

    图 7.38:描述四主题 LDA 模型的直方图和双图

图 7.38:描述四主题 LDA 模型的直方图和双图

查看词汇-主题表,我们可以看到这个模型找到的四个主题与原始数据集中的四个主题一致。这些主题分别是巴拉克·奥巴马、巴勒斯坦、微软和经济。现在的问题是,为什么使用四个主题构建的模型具有比使用三个主题的模型更高的困惑度得分?这个答案可以从步骤 5 生成的可视化结果中找到。

双变量图有合理大小的圆圈,但其中两个圆圈相距非常近,这表明这两个主题(微软和经济)非常相似。在这种情况下,相似性实际上是直观上有道理的。微软是一家全球性的大公司,影响并受经济的影响。如果我们要进行下一步,那就是运行 t-SNE 图,以检查 t-SNE 图中的簇是否有重叠。

注意

要访问此特定部分的源代码,请参见 packt.live/34gLGKa

你也可以在网上运行这个例子,访问 packt.live/3fbWQES

必须执行整个笔记本才能获得期望的结果。

现在让我们将 LDA 的知识应用于另一个数据集。

活动 7.02:LDA 和健康推文

在本活动中,我们将应用 LDA 于 活动 7.01 中加载和清理过的健康推文数据,加载并清理 Twitter 数据。记得使用该活动中使用的同一笔记本。一旦步骤执行完毕,讨论模型的结果。这些单词分组有意义吗?

对于本活动,让我们假设我们有兴趣获得对主要公共卫生话题的高层次理解。也就是说,了解人们在健康领域谈论的内容。我们已经收集了一些数据,可能会揭示这一问题的答案。正如我们所讨论的,识别数据集中主要话题的最简单方法是主题建模。

以下是完成该活动的步骤:

  1. 指定 number_wordsnumber_docsnumber_features 变量。

  2. 创建一个词袋模型,并将特征名称分配给另一个变量,以便以后使用。

  3. 确定最佳的主题数量。

  4. 使用最佳的主题数量来拟合 LDA 模型。

  5. 创建并打印出词-主题表。

  6. 打印出文档-主题表。

  7. 创建一个双变量图可视化。

  8. 保持笔记本打开,以便以后进行建模。

    输出将如下:

    图 7.39:在健康推文上训练的 LDA 模型的直方图和双变量图

图 7.39:在健康推文上训练的 LDA 模型的直方图和双变量图

注意

本活动的解决方案可以在第 482 页找到。

练习 7.10:使用 TF-IDF 创建词袋模型

在本练习中,我们将使用 TF-IDF 创建一个词袋模型:

  1. 运行 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.40:TF-IDF 向量化器的输出

    图 7.40:TF-IDF 向量化器的输出

  2. 返回用于分析输出的特征名称(语料库字典中的实际单词)。你会记得我们在执行 CountVectorizer 时也做过同样的事情,出现在 练习 7.05使用 Count Vectorizer 创建词袋模型

    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',
    

在这个练习中,我们以词袋模型的形式总结了语料库。为每个文档-词组合计算了权重。这个词袋输出将在我们下一步的主题模型拟合中再次使用。下一节将介绍 NMF。

注意

要访问此特定部分的源代码,请参阅packt.live/34gLGKa

你也可以在packt.live/3fbWQES上在线运行此示例。

你必须执行整个笔记本才能获得预期结果。

非负矩阵分解

与 LDA 不同,非负矩阵分解NMF)不是一个概率模型。相反,正如其名称所示,它是一种涉及线性代数的方法。将矩阵分解作为主题建模的方法由 Daniel D. Lee 和 H. Sebastian Seung 于 1999 年提出。该方法属于模型的分解类,包括 PCA,这是一种在第四章中介绍的建模技术,降维与 PCA 简介

PCA 和 NMF 之间的主要区别在于,PCA 要求组件是垂直的,但允许它们是正数或负数。而 NMF 要求矩阵组件是非负的,如果你从数据的角度思考这一要求,这应该是有道理的。主题与文档之间不能是负相关的,词汇与主题之间也不能是负相关的。

如果你还不信服,试着解释将一个负权重与主题和文档关联起来。这就像,主题 T 占文档 D 的-30%;但这是什么意思呢?这毫无意义,因此 NMF 对矩阵分解的每个部分都有非负的要求。

让我们定义要分解的矩阵X为术语-文档矩阵,其中行是词汇,列是文档。矩阵X的每个元素要么是词* i(行)在文档j(列)中的出现次数,要么是词 i与文档j之间关系的其他量化。矩阵X*自然是一个稀疏矩阵,因为术语-文档矩阵中的大多数元素将为零,因为每个文档只包含有限数量的词汇。稍后会讲到如何创建这个矩阵并推导量化方法。

图 7.41: 矩阵分解

图 7.41: 矩阵分解

矩阵分解的形式为 其中两个 ,其中两个组件矩阵,WH,分别表示主题词集合和每个文档的主题权重。更具体地说,Wnxk 是一个词对主题的矩阵,而Hkxm 是一个主题对文档的矩阵,正如前面所述,Xnxm 是一个词对文档的矩阵。

思考这个因式分解的一种好方式是将其看作是定义抽象主题的加权词组的总和。矩阵因式分解公式中的等价符号表明,因式分解WH是一个近似值,因此这两个矩阵的乘积不会完全重现原始的术语-文档矩阵。

目标和 LDA 一样,是找到最接近原始矩阵的近似值。像X一样,WH也是稀疏矩阵,因为每个主题只与少数几个词相关,每个文档仅由少数几个主题组成——在许多情况下是一个主题。

Frobenius 范数

解决 NMF 的目标与 LDA 相同:找到最佳近似值。为了衡量输入矩阵与近似值之间的距离,NMF 可以使用几乎任何距离度量,但标准是 Frobenius 范数,也称为欧几里得范数。Frobenius 范数是元素平方和的总和。

数学上表示为错误 C:\Users\user\Downloads\B15923_07_Formula_18.png

选择好距离度量后,下一步是定义目标函数。最小化 Frobenius 范数将返回最好的原始术语-文档矩阵的近似值,从而得到最合理的主题。请注意,目标函数是相对于WH最小化的,以使两个矩阵都

是非负的。它的表达式为 C:\Users\user\Downloads\B15923_07_Formula_19.png

乘法更新算法

1999 年 Lee 和 Seung 在他们的论文中用于解决 NMF 的优化算法是乘法更新算法,它仍然是最常用的解决方案之一。在本章后面的练习和活动中将会实现该算法。

WH的更新规则是通过展开目标函数并对WH分别取偏导数得到的。导数并不难,但需要相当广泛的线性代数知识,而且时间较长,所以我们跳过导数,直接给出更新规则。请注意,在更新规则中,i是当前的迭代次数,T表示矩阵的转置。第一个更新规则如下:

图 7.42:第一个更新规则

图 7.42:第一个更新规则

第二个更新规则如下:

图 7.43:第二个更新规则

图 7.43:第二个更新规则

WH会迭代更新,直到算法收敛。目标函数也可以被证明是非递减的;即,在每次迭代更新WH时,目标函数会更接近最小值。请注意,乘法更新优化器,如果更新规则重新组织,是一种重新缩放的梯度下降算法。

构建成功的 NMF 算法的最后一个组成部分是初始化WH矩阵,以确保乘法更新能够快速工作。一种流行的初始化矩阵的方法是奇异值分解SVD),它是特征分解的推广。

在接下来的练习中实现的 NMF 方法中,矩阵通过非负双奇异值分解进行初始化,基本上这是 SVD 的一个更高级版本,严格要求非负。关于这些初始化算法的详细信息,对于理解 NMF 并不重要。只需注意,初始化算法是优化算法的起点,能够显著加速收敛过程。

练习 7.11:非负矩阵分解

在本练习中,我们将拟合 NMF 算法,并输出与之前使用 LDA 时相同的两个结果表。这些表是词-主题表,显示与每个主题相关的前 10 个词,和文档-主题表,显示与每个主题相关的前 10 个文档。

NMF 算法函数中有两个我们之前没有讨论过的额外参数,分别是alphal1_ratio。如果存在过拟合模型的风险,这些参数控制正则化在目标函数中的应用方式(l1_ratio)和程度(alpha):

注意

更多细节可以在 scikit-learn 库的文档中找到(scikit-learn.org/stable/modules/generated/sklearn.decomposition.NMF.html)。

  1. 定义 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.44:定义 NMF 模型

    图 7.44:定义 NMF 模型

  2. 运行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)
    
  3. 打印W表:

    print(W_df)
    

    输出如下:

    图 7.45:包含概率的词-主题表

    图 7.45:包含概率的词-主题表

  4. 打印H表:

    print(H_df)
    

    输出如下:

    图 7.46:包含概率的文档-主题表

图 7.46:包含概率的文档-主题表

词-主题表包含词语分组,表明与四主题 LDA 模型在练习 7.09《尝试四个主题》中生成的抽象主题相同。然而,比较中有趣的是,这些分组中包含的一些个别词语是新的,或者它们在分组中的位置发生了变化。考虑到这两种方法学是不同的,这并不令人惊讶。鉴于与原始数据集中指定的主题一致性,我们已经证明这两种方法都是提取语料库潜在主题结构的有效工具。

就像我们之前对 LDA 模型的拟合一样,我们将可视化我们的 NMF 模型的结果。

注意

若要访问该部分的源代码,请参考 packt.live/34gLGKa

你也可以在线运行这个示例,访问链接 packt.live/3fbWQES

你必须执行整个 Notebook 才能获得期望的结果。

练习 7.12:可视化 NMF

这个练习的目的是可视化 NMF 的结果。通过可视化结果,可以深入了解主题的独特性以及每个主题在语料库中的流行度。在这个练习中,我们将使用 t-SNE 来进行可视化,t-SNE 在第六章中有详细讨论,t-分布随机邻域嵌入

  1. 在清理后的数据上运行 transform,以获取主题-文档分配。打印数据的形状和一个示例:

    nmf_transform = nmf.transform(clean_vec2)
    print(nmf_transform.shape)
    print(nmf_transform)
    

    输出结果如下:

    (92946, 4)
    [[5.12653315e-02 3.60582233e-15 3.19729419e-34 8.17267206e-16]
     [7.43734737e-04 2.04138105e-02 6.85552731e-15 2.11679327e-03]
     [2.92397552e-15 1.94083984e-02 4.76691813e-21 1.24269313e-18]
     ...
     [9.83404082e-06 3.41225477e-03 6.14009658e-04 3.23919592e-02]
     [6.51294966e-07 1.32359509e-07 3.32509174e-08 6.14671536e-02]
     [4.53925928e-05 1.16401194e-04 1.84755839e-02 2.00616344e-03]]
    
  2. 运行 plot_tsne 函数来拟合 t-SNE 模型并绘制结果:

    plot_tsne(data=nmf_transform, threshold=0)
    

    图形显示如下:

    图 7.47:带有度量的 t-SNE 图,汇总了语料库中的主题分布

图 7.47:带有度量的 t-SNE 图,汇总了语料库中的主题分布

注意

结果可能略有不同,因为支持 LDA 和 NMF 的优化算法有所不同。许多函数没有设定种子值的功能。

若要访问该部分的源代码,请参考 packt.live/34gLGKa

你也可以在线运行这个示例,访问链接 packt.live/3fbWQES

你必须执行整个 Notebook 才能获得期望的结果。

t-SNE 图没有指定阈值,显示了一些主题重叠,并且语料库中的主题频率存在明显差异。这两点解释了为何在使用困惑度时,最佳的主题数量为三个。似乎存在某些主题之间的关联,模型无法完全处理。即使存在主题之间的关联,当主题数设置为四时,模型仍能找到正确的主题。

总结一下,NMF 是一种非概率主题模型,旨在回答与 LDA 相同的问题。它使用线性代数中的一种常用概念——矩阵分解,即将一个庞大且难以处理的矩阵分解为较小、更易解释的矩阵,从而帮助回答许多与数据相关的问题。请记住,非负性要求并不是数学上的要求,而是数据本身的要求。任何文档的组件不可能为负数。

在许多情况下,NMF 的表现不如 LDA,因为 LDA 包含先验分布,这为主题词组提供了额外的信息层。然而,我们知道在某些情况下,尤其是当主题高度相关时,NMF 的表现更好。正是这种情况发生在所有练习所依据的标题数据上。

现在让我们尝试将新学到的 NMF 知识应用到前面活动中使用的 Twitter 数据集。

活动 7.03:非负矩阵分解

本活动总结了在活动 7.01加载与清理 Twitter 数据中加载并清理的健康 Twitter 数据上的主题建模分析,以及在活动 7.02LDA 与健康推文中进行的 LDA 分析。执行 NMF 非常简单,所需代码有限。我们可以借此机会在思考 NMF 的局限性和优势时调整模型参数。

以下是完成本活动的步骤:

  1. 创建适当的词袋模型,并将特征名称输出为另一个变量。

  2. 使用活动 7.02LDA 与健康推文中的主题数量(n_components)值,定义并拟合 NMF 算法。

  3. 获取主题-文档和词-主题表格。花几分钟探索词组,并尝试定义抽象的主题。你能量化这些词组的含义吗?这些词组合理吗?与使用 LDA 产生的结果是否相似?

  4. 调整模型参数并重新运行步骤 3步骤 4。结果如何变化?

    输出结果如下:

    图 7.48:带有概率的词-主题表格

图 7.48:带有概率的词-主题表格

注意

本活动的解决方案可以在第 487 页找到。

总结

当面对从尚未看到的大量文档中提取信息的任务时,主题建模是一个很好的方法,因为它可以提供有关文档潜在结构的洞察。也就是说,主题模型通过接近性而非语境来寻找词组。

在本章中,我们学习了如何应用两种最常见且最有效的主题建模算法:潜在 Dirichlet 分配(LDA)和非负矩阵分解(NMF)。现在你应该能够熟练使用几种不同的技术清理原始文本文档,这些技术可以在许多其他建模场景中使用。接着,我们学习了如何通过应用词袋模型,将清理过的语料库转换为适当的数据结构,即每个文档的原始词频或词权重。

本章的主要内容是拟合这两种主题模型,包括优化主题数量、将输出转换为易于理解的表格,并可视化结果。有了这些信息,你应该能够应用完全功能的主题模型,为你的业务提取价值和洞察。

在下一章,我们将完全改变方向。我们将深入探讨市场篮子分析。

第九章:8. 市场篮子分析

概述

在本章中,我们将探讨市场篮子分析,这是一种最初设计用于帮助零售商了解和改善其业务的算法。然而,正如我们将在本章中讨论的,它并不仅限于零售业。市场篮子分析揭示了顾客购买的商品之间的潜在关系。本章结束时,你应该对交易数据、定义两件商品关系的基本指标、Apriori 算法和关联规则有一个扎实的理解。

介绍

大多数数据科学从业者都认为,自然语言处理(包括主题建模)是数据科学的前沿领域,是一个活跃的研究方向。我们现在理解到,主题模型可以并且应该在任何潜在能够提供洞察或推动增长的文本数据中被应用,包括社交媒体分析、推荐引擎和新闻过滤等。上一章探讨了主题模型的基本特征和两种主要算法。在本章中,我们将完全改变方向。

本章带我们进入零售领域,探索一种用于分析交易数据的基础且可靠的算法。虽然这种算法可能不是最前沿的,也不在最流行的机器学习算法目录中,但它在零售领域中无处不在,并且具有不可否认的影响力。它所提供的洞察易于解读、可以立即付诸行动,并且对确定分析的下一步至关重要。如果你在零售领域工作或涉及交易数据的分析,深入学习市场篮子分析将大有裨益。市场篮子分析的重要性在于,它提供了关于为什么人们会将某些商品一起购买的洞察,以及这些商品组合是否能被利用来加速增长或增加盈利。

市场篮子分析

假设你为一家零售商工作,卖着数十种产品,你的老板来找你,问你以下问题:

  • 哪些产品最常一起购买?

  • 产品应该如何在商店中组织和定位?

  • 我们如何识别最适合通过优惠券打折的产品?

你可能会完全困惑地回应,因为这些问题非常多样,看起来似乎无法用单一的算法和数据集来回答。然而,所有这些问题以及更多问题的答案都是市场篮子分析。市场篮子分析的基本理念是识别和量化哪些商品或商品组合是经常一起购买的,从而深入了解顾客行为和产品关系。

在我们深入分析之前,值得定义一下市场篮子这一术语。市场篮子是一个经济系统中的固定产品集合。固定并不一定意味着传统意义上的永久性。它意味着,直到产品从目录中移除之前,它将始终可以购买。上述定义中提到的产品可以是任何商品、服务或某一群体的元素,包括一辆自行车、给房子粉刷,或者一个网站。最后,经济系统可以是一个公司、一组活动或一个国家。市场篮子的最简单例子是一个超市,它是由一系列食品和饮品组成的系统:

图 8.1: 一个示例市场篮子

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_01.jpg)

图 8.1: 一个示例市场篮子

即使不使用任何模型或分析,某些产品之间的关系也是显而易见的。我们来看看肉类和蔬菜之间的关系。通常,市场篮子分析模型会返回比肉类和蔬菜更为具体的关系,但为了讨论的方便,我们将其泛化为肉类和蔬菜。好吧,肉类和蔬菜之间有关系。那又怎么样呢?嗯,我们知道这些是常见的主食,通常是一起购买的。我们可以利用这一信息,将蔬菜和肉类放置在商店的两侧,你会发现这两个物品通常就是这样被摆放的,迫使顾客走遍整个商店,这样会增加他们购买更多商品的可能性,哪怕他们原本并没有打算购买那些商品。

零售公司面临的一个挑战是如何有效地打折商品。我们再考虑一个显而易见的关系:花生酱和果冻。在美国,花生酱果冻三明治非常受欢迎,尤其是在孩子们中间。当购物篮里有花生酱时,可以假设果冻也会有很高的可能性。由于我们知道花生酱和果冻通常是一起购买的,因此同时对两者打折并没有意义。如果我们希望顾客购买这两样商品,我们只需对其中一项商品打折,因为如果我们能让顾客购买打折的商品,他们很可能也会购买另一项商品,尽管它是全价的。就像前一章中的主题模型一样,市场篮子分析的核心就是识别频繁出现的组合。下图展示了这样的组合的一个例子:

图 8.2: 市场篮子分析的可视化

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_02.jpg)

图 8.2: 市场篮子分析的可视化

在市场篮子分析中,我们寻找的是频繁出现的产品组合,而在主题模型中,我们寻找的是频繁出现的词汇组合。因此,正如它适用于主题模型一样,聚类这个词也可以应用于市场篮子分析。主要的区别在于,市场篮子分析中的聚类是微型的——每个聚类中只有少数几种产品——并且当涉及到计算概率度量时,聚类中项目的顺序非常重要。我们将在本章稍后深入探讨这些度量及其计算方法。

从前两个例子中可以明显看出,在市场篮子分析中,零售商可以发现顾客购买产品之间的关系——无论是显而易见的还是出乎意料的。一旦这些关系被揭示出来,就可以用来指导和改善决策过程。市场篮子分析的一个重要特点是,尽管这一分析方法最初是与零售领域相关开发、讨论和应用的,但它也可以应用于许多不同类型的商业。

执行这种分析的唯一要求是数据必须是项集合的列表。在零售情况下,这将是一个交易列表,其中每个交易都是一组已购买的产品。另一个应用的例子是分析网站流量。在网站流量分析中,我们将产品视为网站,因此列表的每个元素就是一个人在指定时间段内访问的所有网站集合。不用说,市场篮子分析的应用远远超出了最初的零售应用。

使用案例

传统零售应用中的三个主要使用案例是:定价优化、优惠券和折扣推荐以及店铺布局。如前所述,通过使用模型揭示的产品关联,零售商可以有策略地将产品放置在商店中,促使顾客购买更多商品,从而花费更多的钱。如果两个或更多产品之间的关联足够强,意味着在数据集中该产品组合出现得很频繁,并且组合中的单独产品很少脱离该组合出现,那么这些产品就可以放置在商店的远离位置,而不会显著影响顾客购买两种产品的几率。通过迫使顾客横跨整个商店去购买这两种产品,零售商增加了顾客注意到并购买更多产品的机会。同样,零售商也可以通过将两种关联较弱或非日常产品放置在一起,增加顾客购买这两种产品的几率。显然,商店布局受到许多因素的影响,但市场篮子分析绝对是其中的一个因素。

图 8.3:可以帮助制定高效且有利可图的店铺布局的产品关联

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_03.jpg)

图 8.3:帮助优化高效和有利可图商店布局的产品关联

定价优化和优惠券及折扣推荐是同一枚硬币的两面。它们可以简单地解释为在哪里提高价格,在哪里降低价格。考虑到两个强关联的商品。这两件商品很可能会在同一交易中购买,所以增加交易的利润的一种方式就是提高其中一件商品的价格。如果这两件商品之间的关联足够强,那么价格的提升几乎没有客户不购买两者的风险。类似地,零售商可以通过折扣或优惠券鼓励客户购买一件与另一件商品弱相关的商品。

例如,零售商可以将单个客户的购买历史与基于所有交易的市场篮子分析结果进行比较,找出某些客户购买的商品与他们当前未购买的商品之间的弱关联。通过这种对比,零售商可以为这些客户提供尚未购买商品的折扣,而这些商品是模型建议与他们之前购买的商品相关的。如果你曾在交易结束时收到与收据一同打印的优惠券,那么很可能这些商品就是通过这种方式发现与刚刚完成的交易中涉及的商品相关。

市场篮子分析的一个非传统但可行的应用是增强在线广告和搜索引擎优化。假设我们能够访问个人访问过的网站列表。利用市场篮子分析,我们可以找到网站之间的关系,并利用这些关系来战略性地排列和分组搜索引擎查询结果中的网站。在很多方面,这与商店布局的应用场景相似。

对于市场篮子分析的基本概念和它的应用场景有了大致了解后,让我们深入研究这些模型所使用的数据。

重要的概率度量

市场篮子分析建立在多个概率度量的计算基础上。这里涉及的五个主要度量是支持度置信度提升度杠杆度定罪度。在深入了解交易数据和具体的市场篮子分析模型(包括Apriori 算法关联规则)之前,我们应该花些时间使用一个小型的虚构交易集来定义和探索这些度量。我们首先构建一些数据来使用。

练习 8.01:创建示例交易数据

由于这是本章的第一个练习,我们先设置环境。本章将使用与第七章主题建模中相同的环境要求。如果任何包无法加载,就像前一章那样,请通过命令行使用pip进行安装。我们将使用的一个库是mlxtend,这对您来说可能比较陌生。它是一个机器学习扩展库,包含有用的补充工具,包括集成、堆叠,当然还有市场篮子分析模型。这个练习没有实际输出,我们只是创建一个示例交易数据集,以供后续练习使用:

  1. 打开一个带有 Python 3 的 Jupyter 笔记本。

  2. 安装以下库: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
    
  3. 创建 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']]
    print(example)
    

    输出结果如下:

    图 8.4:打印交易

图 8.4:打印交易

现在我们已经创建了数据集,接下来我们将探讨几种概率度量,量化项目对之间的关系。

注意

要访问此特定部分的源代码,请参考packt.live/3fhf9bS

您还可以在packt.live/303vIBJ上在线运行此示例。

您必须执行整个笔记本,以便获得预期的结果。

支持度

支持度只是给定项集在数据中出现的概率,可以通过计算项集出现的交易次数,并将该次数除以总交易次数来计算。

注意

项集可以是单个项或一组项。

支持度是一个重要的度量,尽管它非常简单,因为它是用来确定项集之间关联的可信度和强度的主要度量之一。例如,可能会有两个只在一起出现的项,表明它们之间的关联非常强,但在包含 100 个交易的数据集中,仅出现两次并不令人印象深刻。因为项集只出现在 2%的交易中,而 2%在出现次数上是较小的,所以该关联不能被认为是显著的,因此可能在决策中无法使用。

请注意,由于支持度是一个概率,因此它的取值范围为 [0,1]。如果商品集是两个商品 X 和 Y,并且 N 是总交易数,则该公式具有以下形式:

图 8.5:支持度公式

图 8.5:支持度公式

在进行市场篮分析时,如果某个商品或商品组合的支持度低于预定义的阈值,那么购买该商品或商品组合的情况就足够少,无法采取行动。让我们暂时回到来自练习 8.01创建样本交易数据的虚构数据,并将一个商品集定义为牛奶和面包。我们可以轻松地查看这 10 笔交易,并计算包含此牛奶和面包商品集的交易次数 - 共有 4 次。考虑到总共有 10 笔交易,牛奶和面包的支持度为 4 除以 10,即 0.4. 支持度是否足够大取决于数据集本身,我们将在后面的章节中详细介绍。

置信度

置信度指标可以从条件概率的角度来理解,因为它基本上是指购买产品 A 后购买产品 B 的概率。置信度通常表示为 A and expressed B,并且表示包含 A 的交易中也包含 B 的比例。因此,置信度是通过将完整的交易集筛选到包含 A 的交易中,然后计算这些交易中包含 B 的比例来确定的。与支持度一样,置信度是一个概率,因此其范围是 [0,1]。使用与支持度部分相同的变量定义,以下是置信度的公式:

图 8.6:置信度公式

图 8.6:置信度公式

为了演示置信度,我们将使用啤酒和葡萄酒这两种商品。具体来说,让我们计算啤酒 Formula 葡萄酒的置信度。首先,我们需要确定包含啤酒的交易。有三笔交易,它们是第 2、6 和 7 笔。现在,这些交易中有多少包含葡萄酒?答案是全部。因此,啤酒 Formula 葡萄酒的置信度为 1. 每次客户购买啤酒时,他们也购买了葡萄酒。这可能是显而易见的,但对于识别可操作的关联,较高的置信度值更为有利。

提升度和杠杆

我们将同时讨论接下来的两个指标:提升(lift)和杠杆(leverage),因为尽管它们的计算方式不同,但都旨在回答相同的问题。像置信度一样,提升杠杆的表示形式是 A 公式 B。我们要回答的问题是,一个项目,比如 A,能否用来推断另一个项目,比如 B?换句话说,如果某个人购买了产品 A,我们能否在某种程度上有信心地说他们是否会购买产品 B?这些问题通过比较 A 和 B 在标准情况下(即假设 A 和 B 不独立时)与假设这两个产品独立时的支持度来回答。提升计算这两种情况的比率,因此它的取值范围是 [0, 无穷大]。当提升值为 1 时,两个产品是独立的,因此在购买产品 A 时,无法得出关于产品 B 的任何结论:

图 8.7:提升公式

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_07.jpg)

图 8.7:提升公式

杠杆计算这两种情况的差异,因此它的取值范围是 [-1, 1]。杠杆值为零的解释方式与提升值为 1 时相同:

图 8.8:杠杆公式

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_08.jpg)

图 8.8:杠杆公式

这些指标的值衡量了项目之间关系的程度和方向(换句话说,正向或负向)。当提升值不为 1 时,说明项目之间存在某种依赖关系。当提升值大于 1 时,购买第一个产品时,第二个产品更可能被购买。同样,当提升值小于 1 时,购买第一个产品时,第二个产品的购买可能性较小。如果提升值为 0.1,我们可以说这两个项目之间的关系是强烈的负向关系。也就是说,可以说当购买了一个产品时,购买第二个产品的机会会减少。提升值为 1 表示产品之间是独立的。在杠杆的情况下,正值表示正相关,负值表示负相关。正相关和负相关由独立点分开,正如前面所说,提升的独立点是 1,而杠杆的独立点是 0,值离这些独立点越远,关联越强。

确信度

接下来讨论的最后一个度量是信念(conviction),它比其他度量更不直观。信念是指在 X 和 Y 相互独立的情况下,X 发生而 Y 不发生的预期频率与错误预测频率的比率。错误预测频率定义为 1 减去 X 的置信度 公式 Y。请记住,置信度可以定义为 P(Y|X),这意味着 1 – P(Y|X) = P(Not Y|X)。分子也可以被理解为 1 – P(Y|X) = P(Not Y|X)。这两者之间的唯一区别在于,分子假设 X 和 Y 之间是独立的,而分母则没有这个假设。理想情况下,值大于 1,因为这意味着如果 X 和 Y 之间的关联是随机的(换句话说,X 和 Y 是独立的),则 X 和 Y 之间的关系会更常出现错误。重申一下,这表明 X 和 Y 之间的关联是有意义的。值为 1 时表示独立,值小于 1 则表示随机的 X 和 Y 关系比已定义的 X 和 Y 关系正确的情况更常见。此时,关系可能是反向的(换句话说,Y 公式 X)。信念的取值范围是 [0, 无穷大],其公式如下:

图 8.9:信念公式

图 8.9:信念公式

让我们再次回到啤酒和葡萄酒这两个产品,但在本次解释中,我们将考虑葡萄酒 公式 啤酒的反向关联。Support(Y),或者在这个例子中,Support(Beer) 是 3/10,Confidence X 公式 Y,或者在这个例子中,Confidence(Wine 公式 Beer) 是 3/4。因此,Conviction(Wine 公式 Beer) 是 (1-3/10) / (1-3/4) = (7/10) * (4/1)。我们可以得出结论,如果葡萄酒和啤酒是独立的,那么葡萄酒 公式 啤酒的关联会错误出现 2.8 次。因此,之前所述的葡萄酒和啤酒之间的关联是合法的。

练习 8.02:计算度量

在本次练习中,我们将使用 练习 8.01 中的虚拟数据,即 创建样本事务数据,来计算之前描述的五个度量,这些度量将再次用于 Apriori 算法和关联规则的讲解。将评估这些度量的关联是 Milk 公式 Bread:

注意

本章中的所有练习都需要在同一个 Jupyter notebook 中进行。

  1. 定义并打印所有五个度量的基础频率,包括 Frequency(Milk)、Frequency(Bread) 和 Frequency(Milk, Bread)。此外,定义 N 为数据集中事务的总数:

    # the number of transactions
    N = len(example)
    # the frequency of milk
    f_x = sum(['milk' in i for i in example])
    # the frequency of bread
    f_y = sum(['bread' in i for i in example]) 
    # the frequency of milk and bread
    f_x_y = sum([all(w in i for w in ['milk', 'bread']) \
                 for i in example])
    # print out the metrics computed above
    print("N = {}\n".format(N) + "Freq(x) = {}\n".format(f_x) \
          + "Freq(y) = {}\n".format(f_y) \
          + "Freq(x, y) = {}".format(f_x_y))
    

    输出结果如下:

    N = 10
    Freq(x) = 7
    Freq(y) = 5
    Freq(x, y) = 4
    
  2. 计算并打印支持度(Milk Formula Bread):

    support = f_x_y / N
    print("Support = {}".format(round(support, 4)))
    

    xy的支持度为0.4。根据经验,如果我们处理的是完整的交易数据集,在许多情况下,这个支持度值会被认为是非常大的。

  3. 计算并打印置信度(Milk Formula Bread):

    confidence = support / (f_x / N)
    print("Confidence = {}".format(round(confidence, 4)))
    

    xy的置信度为0.5714。这意味着,给定x已购买,Y 被购买的概率仅略高于 50%。

  4. 计算并打印提升度(Milk Formula Bread):

    lift = confidence / (f_y / N)
    print("Lift = {}".format(round(lift, 4)))
    

    xy的提升度为1.1429

  5. 计算并打印杠杆率(Milk Formula Bread):

    leverage = support - ((f_x / N) * (f_y / N))
    print("Leverage = {}".format(round(leverage, 4)))
    

    xy的杠杆率为0.05。提升度和杠杆率都可以用来表示xy的关联是正向的(换句话说,x意味着y),但关联较弱。提升度和杠杆率的值分别接近 1 和 0。

  6. 计算并打印确定度(Milk Formula Bread):

    conviction = (1 - (f_y / N)) / (1 - confidence)
    print("Conviction = {}".format(round(conviction, 4)))
    

    1.1667的确定度值可以解释为:如果牛奶和面包是独立的,Milk Formula Bread 关联会错误地出现1.1667倍。

    在这个练习中,我们探索了一系列旨在量化两个项目之间关系的概率度量。五个度量分别是支持度、置信度、提升度、杠杆率和确定度。我们将在后续的 Apriori 算法和关联规则中再次使用这些度量,它们是这两者的基础。

    注意

    要访问本节的源代码,请参考packt.live/3fhf9bS

    你还可以在网上运行这个示例,网址是packt.live/303vIBJ

    你必须执行整个 Notebook 才能获得预期的结果。

在实际进行 Apriori 算法和关联规则学习之前,我们将探索交易数据,并加载一些零售数据,为建模做好准备。

交易数据的特点

在市场篮子分析中使用的数据是交易数据或任何类似交易数据的类型。最基本的形式下,交易数据包含某种交易标识符,比如发票或交易号,以及与该标识符关联的产品列表。恰好这两个基本元素就是执行市场篮子分析所需的全部。然而,交易数据很少——甚至可以说从未——以这种最基本的形式存在。交易数据通常包括定价信息、日期和时间、客户标识符等许多其他信息。以下是每个产品如何映射到多个发票的方式:

图 8.10:每个可用的产品都将映射回多个发票号码

图 8.10:每个可用的产品都将映射回多个发票号码

由于交易数据的复杂性,数据清洗至关重要。在市场篮子分析的背景下,数据清洗的目标是过滤掉所有不必要的信息,包括删除数据中不相关的变量,并筛选掉有问题的交易。完成这两步清洗的技术方法因具体的交易数据文件而异。为了避免过度陷入数据清洗,本书从此开始使用 UCI 机器学习库中的一个在线零售数据集的子集,活动则会使用整个数据集。这既限制了数据清洗的讨论,也为我们提供了一个机会,讨论数据集大小变化时结果的变化。这一点非常重要,因为如果你在零售商工作并进行市场篮子分析,了解并能够清晰表达一个事实是非常关键的:随着更多数据的接收,产品之间的关系可能会发生变化,而且很可能会发生变化。在讨论此数据集所需的特定清洗过程之前,我们先来加载在线零售数据集。

注意

在接下来的所有练习和活动中,输出结果可能与下面展示的有所不同。这可能是由于以下两种原因之一:数据加载的问题(换句话说,行数据被打乱)或是mlxtend没有设置种子选项来确保执行结果的一致性。

练习 8.03:加载数据

在本练习中,我们将加载并查看一个示例的在线零售数据集。该数据集最初来自 UCI 机器学习库,可以在packt.live/2XeT6ft找到。下载数据集后,请保存并记录路径。现在,让我们继续进行练习。此练习的输出是将用于未来建模练习的交易数据,以及一些探索性图形,帮助我们更好地理解我们所使用的数据。

注意

该数据集来源于archive.ics.uci.edu/ml/datasets/online+retail#(UCI 机器学习库 [archive.ics.uci.edu/ml]。加利福尼亚州尔湾:加利福尼亚大学信息与计算机科学学院)。

引用:这是从 UCI 机器学习库获取的在线零售数据集的一个子集。Daqing Chen, Sai Liang Sain 和 Kun Guo, 在线零售行业的数据挖掘基于 RFM 模型的数据挖掘客户细分案例研究,《数据库营销与客户战略管理杂志》,第 19 卷,第 3 期,页码 197-208,2012 年。

它可以从packt.live/2XeT6ft下载。

执行以下步骤:

  1. 使用pandas中的read_excel函数加载数据。注意,我们可以通过在read_excel函数中添加header=0来定义第一行作为列名:

    online = pandas.read_excel(io="./Online Retail.xlsx", \
                               sheet_name="Online Retail", \
                               header=0)
    

    注意

    Online Retail.xlsx的路径应根据文件在你系统上的位置进行更改。

  2. 打印出 DataFrame 的前 10 行。注意,数据包含一些与市场篮分析无关的列:

    online.head(10)
    

    输出如下:

    图 8.11:原始在线零售数据

    图 8.11:原始在线零售数据

  3. 打印出 DataFrame 中每列的数据类型。这些信息在执行特定清理任务时非常有用。列需要具备正确的数据类型,才能确保过滤和计算按预期执行:

    online.dtypes
    

    输出如下:

    InvoiceNo              object
    StockCode              object
    Description            object
    Quantity                int64
    InvoiceDate    datetime64[ns]
    UnitPrice             float64
    CustomerID            float64
    Country                object
    dtype: object
    
  4. 获取 DataFrame 的维度,以及唯一发票号码和客户标识的数量:

    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
    

从前面的输出中,我们可以看到数据成功加载,并且获得了一些关键的信息,这些信息将在本章练习中进一步使用。

注意

若要访问此特定部分的源代码,请参见packt.live/3fhf9bS

你也可以在packt.live/303vIBJ上在线运行此示例。

你必须执行整个 Notebook,才能得到期望的结果。

数据清理与格式化

现在数据集已经加载完成,接下来让我们深入了解具体的数据清理过程。由于我们要将数据过滤到仅包含发票号码和商品,因此我们将数据清理重点放在这两列上。记住,市场篮分析旨在识别所有顾客在一段时间内购买的商品之间的关联。因此,数据清理的主要任务是去除包含非正数商品数量的交易。这种情况可能发生在交易作废时、商品退货时,或交易为行政操作时。这类交易将通过两种方式进行过滤。第一种方式是,取消的交易会在发票号码前加上“C”字母,因此我们会识别出这些特定的发票号码并将其从数据中删除。另一种方式是移除所有商品数量为零或负数的交易。执行完这两个步骤后,数据将被筛选至仅包含发票号码和商品描述两列,并且任何具有至少一个缺失值的两列数据行都将被删除。

数据清洗练习的下一阶段是将数据转换为适合建模的格式。在本练习和后续练习中,我们将使用完整数据的一个子集。该子集将通过取前 5,000 个唯一发票号来创建。一旦我们将数据减少到前 5,000 个唯一发票号,我们将数据结构更改为运行模型所需的结构。请注意,目前数据每行一个商品,因此多件商品的交易占用多行。所需的格式是一个列表的列表,就像本章早些时候提到的虚构数据一样。每个子列表代表一个唯一的发票号,因此在这种情况下,外部列表应包含 5,000 个子列表。子列表的元素是属于该发票号的所有商品。按照描述的清洗过程,让我们继续进行练习。

练习 8.04:数据清洗与格式化

在本练习中,我们将执行之前描述的清洗步骤。在处理过程中,通过打印出数据的当前状态并计算一些基本的汇总指标来监控数据的演变。请确保在加载数据的同一本笔记本中执行数据清洗:

  1. 创建一个指示列,规定发票号是否以 "C" 开头:

    # create new column called IsCPresent
    online['IsCPresent']  = (# looking for C in InvoiceNo column \
                             .astype(str)\
                             .apply(lambda x: 1 if x.find('C') \
                                    != -1 else 0))
    
  2. 筛选出所有包含零个或负数商品(换句话说,商品已退还)的交易,使用第一个步骤创建的列删除所有以"C"开头的发票号,将数据框子集化为InvoiceNoDescription,最后删除所有含有至少一个缺失值的行。将数据框重命名为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())
    
  3. 打印出过滤后的online1数据框的前 10 行:

    online1.head(10)
    

    输出如下:

    图 8.12:清洗后的在线零售数据集

    图 8.12:清洗后的在线零售数据集

  4. 打印出清洗后数据框的维度以及通过nunique函数计算的唯一发票号数量,该函数计算数据框列中唯一值的数量:

    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 个发票号。

  5. 将发票号从数据框中提取为列表。删除重复元素,创建一个唯一发票号的列表。通过打印唯一发票号列表的长度来确认该过程是否成功。与第 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
    
  6. 第 5 步中获取列表,仅保留前 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
    
  7. 通过只保留前一步骤中列出的发票号来过滤online1数据框:

    online1 = online1.loc[online1["InvoiceNo"]\
                         .isin(subset_invoice_no_list)]
    
  8. 打印出online1的前 10 行:

    online1.head(10)
    

    输出如下:

    图 8.13:仅包含 5,000 个唯一发票号的清洗后数据集

    图 8.13:清洗后的数据集,仅包含 5000 个唯一的发票编号

  9. 打印出数据框的维度以及唯一发票编号的数量,以确认过滤和清洗过程成功:

    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): (133315, 2)
    Count of unique invoice numbers: 5000
    
  10. 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.14:列表中的四个元素

图 8.14:列表中的四个元素

在前述的列表中,每个子列表包含属于单个发票的所有项目。

注意

这个步骤可能需要几分钟才能完成。

在这个练习中,数据框被过滤并子集化为仅包含所需的列和相关的行。接着我们将完整的数据集缩减为前 5000 个唯一的发票编号。完整的数据集将在接下来的活动中使用。最后一步将数据框转换为列表形式,这是数据需要为编码器准备的格式,接下来会讨论该编码器。

注意

要访问该部分的源代码,请参考packt.live/3fhf9bS

你也可以在线运行这个示例,网址为packt.live/303vIBJ

你必须执行整个笔记本才能获得期望的结果。

数据编码

虽然清洗数据至关重要,但数据准备过程最重要的部分是将数据塑造成正确的格式。在运行模型之前,目前以列表形式存在的数据需要进行编码,并重新转换为数据框。为此,我们将利用mlxtend库中的preprocessing模块中的TransactionEncoder。编码器的输出是一个多维数组,其中每行的长度等于交易数据集中唯一项目的总数,元素是布尔变量,表示该特定项目是否与该行表示的发票编号相关联。编码后的数据可以重新转化为数据框,其中行是发票编号,列是交易数据集中的唯一项目。

在接下来的练习中,数据编码将使用 mlxtend 完成,但实际上不使用任何包也可以轻松编码数据。第一步是将列表中的列表拆开,并返回一个包含原始列表中所有值的单一列表。接下来,过滤掉重复的产品,并且如果需要的话,可以按字母顺序对数据进行排序。在进行实际编码之前,我们通过让所有元素初始值为 false,行数等于数据集中的发票号数量,列名为去重后的产品名称列表,来初始化最终的 DataFrame。

在这个案例中,我们有 5,000 个交易和超过 3,100 个独特的产品。因此,DataFrame 中有超过 15,000,000 个元素。实际的编码是通过遍历每个交易和每个交易中的每个商品来完成的。如果第 i 个交易包含第 j 个产品,则将初始化数据集中的第 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

练习 8.05:数据编码

在本练习中,我们将继续数据准备过程,通过使用前一练习中生成的列表列表并按照特定方式对数据进行编码,以便运行模型:

  1. 初始化并拟合交易编码器。打印结果数据的一个示例:

    online_encoder = mlxtend.preprocessing.TransactionEncoder()
    online_encoder_array = online_encoder\
                           .fit_transform(invoice_item_list)
    print(online_encoder_array)
    

    输出如下:

    [[False False False ... False False False]
     [False False False ... False False False]
     [False False False ... False False False]
     ...
     [False False False ... False False False]
     [False False False ... False False False]
     [False False False ... False False False]]
    

    上述数组包含布尔变量,指示每个交易中是否包含某个产品。

  2. 将编码后的数组重新构建为名为 online_encoder_df 的 DataFrame。打印该 DataFrame 的预定义子集,其中包含 TrueFalse 值:

    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.15:重新构建为 DataFrame 的编码数据的小部分

    图 8.15:重新构建为 DataFrame 的编码数据的一个小部分

  3. 打印编码后的 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, 3135)
    

数据现在已经为建模做好准备,我们将在 练习 8.06 中执行 Apriori 算法

注意

要访问此特定部分的源代码,请参阅 packt.live/3fhf9bS

你也可以在 packt.live/303vIBJ 在线运行此示例。

你必须执行整个 Notebook 才能获得所需的结果。

活动 8.01:加载并准备完整的在线零售数据

在这项活动中,我们负责加载和准备大型交易数据集用于建模。最终输出将是一个适当编码的数据集,其中每个唯一交易在数据集中有一行,并且数据集中每个唯一项目有一列。如果一个项目出现在单个交易中,数据框的该元素将标记为true

此活动将主要重复前几个练习,但将使用完整的在线零售数据集文件。不需要执行新的下载,但需要先前下载文件的路径。请在单独的 Jupyter 笔记本中执行此活动。

下列步骤将帮助您完成这项活动:

  1. 加载在线零售数据集文件。

    注意

    此数据集来源于archive.ics.uci.edu/ml/datasets/online+retail#(UCI 机器学习库[archive.ics.uci.edu/ml]。加州尔湾:加州大学信息与计算机科学学院)。

    引用:Daqing Chen,Sai Liang Sain 和 Kun Guo,《面向在线零售行业的数据挖掘:基于数据挖掘的 RFM 模型客户分割的案例研究》,《数据库营销和客户战略管理杂志》,第 19 卷,第 3 期,第 197-208 页,2012 年。

    可以从packt.live/39Nx3iQ下载。

  2. 清理和准备建模数据,包括将清理后的数据转换为列表的列表。

  3. 编码数据并将其重新转换为数据框。

    注意

    此活动的解决方案可以在第 490 页找到。

输出将类似于以下内容:

图 8.16:从完整在线零售数据集构建的已清理、编码和重新转换的数据框的子集

图 8.16:从完整在线零售数据集构建的已清理、编码和重新转换的数据框的子集

Apriori 算法

Apriori 算法是识别和量化交易数据中频繁项集的数据挖掘方法论,是关联规则学习的基础组件。将 Apriori 算法的结果扩展到关联规则学习将在下一节讨论。项集被认为频繁的阈值是模型的输入(即超参数),因此可调整。在这里频率被量化为支持度,因此输入到模型中的值是分析中所接受的最小支持度。然后,模型识别所有支持度大于或等于模型提供的最小支持度的项集。

注意

最小支持超参数不是可以通过网格搜索优化的值,因为 Apriori 算法没有评估度量标准。相反,最小支持参数是基于数据、用例和领域专业知识设置的。

Apriori 算法背后的主要思想是 Apriori 原理:任何频繁物品集的子集必须本身也是频繁的。

另一个值得提及的方面是推论:不频繁物品集的任何超集都不能是频繁的。

让我们举一些例子。如果物品集{锤子、锯子和钉子}是频繁的,那么根据 Apriori 原理,并且这应该是显而易见的,从中派生的任何更简单的物品集,例如{锤子、锯子},也是频繁的。相反,如果同样的物品集{锤子、锯子、钉子}是不频繁的,那么通过增加复杂性,如将木材加入物品集{锤子、锯子、钉子、木材},是不会使该物品集变得频繁的。

对于事务数据库中的每个物品集计算支持度值,并仅返回那些支持度大于或等于预设最小支持度阈值的物品集,似乎很简单,但由于所需的计算量,这并非易事。例如,假设有一个包含 10 个独特物品的物品集,这将导致 1,023 个单独的物品集需要计算支持度值。现在,试着推算我们的工作数据集,它包含 3,135 个独特物品。这将是一个庞大的物品集数量,我们需要为这些物品集计算支持度值。计算效率是一个重大问题:

图 8.17:计算效率问题的表现

图 8.17:计算效率问题的表现

前面的图示展示了物品集的构建方式以及 Apriori 原理如何显著降低计算需求(所有灰色节点都是不频繁的)。

为了解决计算需求,Apriori 算法被定义为一个自底向上的模型,包含两个步骤。这些步骤涉及通过向已有的频繁物品集添加物品来生成候选物品集,并将这些候选物品集与数据集进行测试,以确定这些候选物品集是否也频繁。对于包含不频繁物品集的物品集,不计算支持度值。该过程会重复,直到没有更多的候选物品集为止:

图 8.18:一般 Apriori 算法结构

图 8.18:一般 Apriori 算法结构

假设最低支持度阈值为 0.4,前面的图示展示了 Apriori 算法的一般结构。

图 8.20包括建立物品集、计算支持度值、筛选掉不频繁的物品集、创建新物品集,并重复这一过程。

有一个明确的树状结构,作为识别候选物品集的路径。所使用的具体搜索技术是广度优先搜索,它是为遍历树状数据结构而构建的,这意味着搜索过程的每一步都集中在完全搜索树的一个层级之后再继续,而不是逐个分支地进行搜索。

算法的高层步骤旨在完成以下任务:

  1. 定义频繁项集(换句话说,只选择那些支持度大于预定义阈值的项)。通常,首先选择的是单个项集。

  2. 通过组合频繁项集来推导候选项集。每次只增加一个项的大小。也就是说,从包含一个项的项集开始,逐步扩展到包含两个项、三个项,以此类推。

  3. 计算每个候选项集的支持度。

  4. 创建一个新的频繁项集,由支持度超过指定阈值的候选项集组成。

重复步骤 1步骤 4,直到没有更多的频繁项集为止;也就是说,直到我们处理完所有的组合。

Apriori 算法的伪代码如下:

L1 = {frequent items}
k = 1
L = {}
while Lk.Length is not an 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
    L.append(Lk)
    k = k + 1
End
Return L = all frequent item sets with corresponding support values

尽管遵循 Apriori 原则,该算法仍可能面临显著的计算挑战,这取决于事务数据集的大小。目前有几种公认的策略可以进一步减少计算需求。

计算修正

事务简化是一种减少计算负载的简单方法。注意,在生成每个候选项集之后,需要扫描整个事务数据集,以统计每个候选项集的出现次数。如果我们能缩小事务数据集的大小,数据集扫描的规模将大大减少。事务数据集的缩小是通过实现以下方式:任何在第i次迭代中不包含频繁项集的事务,在后续迭代中也不会包含任何频繁项集。因此,一旦每个事务不包含频繁项集,就可以从用于未来扫描的事务数据集中移除。

对事务数据集进行抽样,并在其上测试每个候选项集,是另一种减少计算需求的方法,这种方法可以避免扫描事务数据集来计算每个项集的支持度。在实现这种方法时,重要的是降低最小支持度要求,以确保不会遗漏任何应包含在最终数据中的项集。由于抽样的事务数据集会自然导致支持度值较小,因此将最小支持度保持在原始值可能会错误地删除那些应该是频繁项集的项集。

一种类似的方法是分区。在这种情况下,数据集被随机划分为多个单独的数据集,每个数据集上都会执行对每个候选项集的评估。如果一个项集在其中一个分区中是频繁的,那么它就被认为在完整事务数据集中是频繁的。每个分区依次扫描,直到确定一个项集的频率。如果频率在第一个分区中得以确定,那么我们就不需要在大多数分区上测试它,就可以为整个数据集确立该频率。像抽样一样,分区也是另一种避免在完整数据集上测试每个项集的方法,如果完整数据集非常大,测试会非常耗费计算资源。

无论是否采用这些技术,Apriori 算法的计算需求通常都会相当庞大。如现在所见,算法的核心——支持度的计算——并不像本书中讨论的其他模型那样复杂。

练习 8.06:执行 Apriori 算法

使用 mlxtend 执行 Apriori 算法变得非常简单。因此,本练习将重点关注如何处理输出数据集并解读结果。你会记得,清理并编码后的事务数据被定义为 online_encoder_df

注意

在与之前所有练习相同的笔记本中执行此练习,因为我们将继续使用该笔记本中已经建立的环境、数据和结果。(因此,你应该使用包含 5,000 张发票的简化数据集的笔记本,而不是在活动中使用的完整数据集。)

  1. 使用 mlxtend 运行 Apriori 算法,而不改变任何默认的参数值:

    mod = mlxtend.frequent_patterns.apriori(online_encoder_df)
    mod
    

    输出是一个空的 DataFrame。默认的最小支持度值设置为 0.5,因此由于返回的是空的 DataFrame,我们知道所有项集的支持度都低于 0.5。根据事务的数量和可用项的多样性,没有任何项集具有超过 0.5 的支持度值并不奇怪。

  2. 重新运行 Apriori 算法,但将最小支持度设置为 0.01:

    mod_minsupport = mlxtend.frequent_patterns\
                     .apriori(online_encoder_df, \
                     min_support=0.01)
    mod_minsupport.loc[0:6]
    

    这个最小支持度值相当于在分析 5,000 个事务时,需要一个项集出现 50 次才会被认为是频繁的。如前所述,最小支持度可以设置为 [0,1] 范围内的任何值。没有最佳的最小支持度值;该值的设置完全是主观的。许多企业有自己特定的显著性阈值,但没有行业标准或优化此值的方法。

    输出将类似于以下内容:

    图 8.19:使用 mlxtend 运行的 Apriori 算法的基本输出

    图 8.19:使用 mlxtend 运行的 Apriori 算法的基本输出

    请注意,输出中的项集以数字形式标识,这使得结果难以解读。

  3. 重新运行 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.20:Apriori 算法输出,实际项名代替了数值标识

    图 8.20:Apriori 算法输出,实际项名代替了数值标识

    这个 DataFrame 包含了所有支持度大于指定最小支持度值的项集。也就是说,这些项集出现的频率足够高,有可能是有意义的,因此是可操作的。

  4. 步骤 3 的输出中添加一个额外的列,包含项集的大小(换句话说,就是该项集中有多少项),这将有助于筛选和进一步分析:

    mod_colnames_minsupport['length'] = \
    (mod_colnames_minsupport['itemsets'].apply(lambda x: len(x)))
    mod_colnames_minsupport.loc[0:6]
    

    输出将类似于以下内容:

    图 8.21:Apriori 算法输出加上一个额外的列,包含项集的长度

    图 8.21:Apriori 算法输出加上一个额外的列,包含项集的长度

  5. 查找包含 10 COLOUR SPACEBOY PEN 的项集的支持度:

    mod_colnames_minsupport[mod_colnames_minsupport['itemsets'] \
                            == frozenset({'10 COLOUR SPACEBOY PEN'})]
    

    输出如下:

    图 8.22:筛选至单个项集的输出 DataFrame

    图 8.22:筛选至单个项集的输出 DataFrame

    这个单行 DataFrame 给出了包含一个项的特定项集的支持度值。支持度值表示该特定项集出现在 1.78% 的交易中。

  6. 返回所有支持度在 [0.02, 0.021] 范围内,且长度为 2 的项集:

    mod_colnames_minsupport[(mod_colnames_minsupport['length'] == 2) \
                             & (mod_colnames_minsupport\
                                ['support'] >= 0.02) \
                             & (mod_colnames_minsupport\
                                ['support'] < 0.021)]
    

    输出将类似于以下内容:

    图 8.23:通过长度和支持度筛选后的 Apriori 算法输出 DataFrame

    图 8.23:通过长度和支持度筛选后的 Apriori 算法输出 DataFrame

    这个 DataFrame 包含了所有支持度值在步骤开始时指定的范围内的项集(即一对一起购买的商品)。每个项集出现在 2.0% 到 2.1% 的交易中。

    注意,当按 support 进行筛选时,最好指定一个范围,而不是一个具体值,因为很可能会选择一个没有项集的值。前面的输出中有 32 个项集;这里只显示了一个子集。记住这些项集中的具体项,因为我们将在扩展到完整数据时使用相同的筛选条件,并且需要执行对比。

  7. 绘制支持度值图。注意,该图不会包含支持度小于 0.01 的数据,因为该值作为 步骤 2 中的最小支持度值:

    mod_colnames_minsupport.hist("support", grid=False, bins=30)
    plt.xlabel("Support of item")
    plt.ylabel("Number of items")
    plt.title("Frequency distribution of Support")
    plt.show()
    

    输出将类似于以下图表:

    图 8.24:Apriori 算法返回的支持度值分布

图 8.24:Apriori 算法返回的支持度值分布

最大支持值大约为 0.14,约为 700 次交易。看似较小的值可能并不小,因为涉及的产品数量较多。产品数量越大,支持值往往越低,因为商品组合的变动性增加。

注意

要访问此特定部分的源代码,请参考packt.live/3fhf9bS

您也可以在线运行此示例,网址为packt.live/303vIBJ

您必须执行整个笔记本才能获得期望的结果。

希望您能想到更多使用这些数据的方法。在下一节中,我们将通过使用 Apriori 算法结果生成关联规则,产生更多有用的信息。

活动 8.02:在完整的在线零售数据集上运行 Apriori 算法

假设您为一家在线零售商工作。您获得了上个月所有的交易数据,并被告知找出至少出现在 1%交易中的商品集。一旦识别出符合条件的商品集,接下来您需要找出支持度值的分布情况。支持度值的分布将告知所有相关方,哪些商品组合是以较高概率共同购买的,并给出支持度值的均值。让我们收集所有这些信息,供公司领导层和战略人员参考。

在此活动中,您将对完整的在线零售数据集运行 Apriori 算法。

注意

该数据集来源于archive.ics.uci.edu/ml/datasets/online+retail#(UCI 机器学习库 [archive.ics.uci.edu/ml])。加利福尼亚大学尔湾分校信息与计算机科学学院。

引用:Daqing Chen, Sai Liang Sain, 和 Kun Guo, 面向在线零售行业的数据挖掘:基于 RFM 模型的客户细分案例研究,《数据库营销与客户战略管理期刊》,第 19 卷,第 3 期,197-208 页,2012 年。

它可以从packt.live/39Nx3iQ下载。

确保在与前一个活动相同的笔记本中完成此活动(换句话说,即使用完整数据集的笔记本,而不是使用 5,000 个发票子集的笔记本,后者是用于练习的)。

这也为您提供了一个机会,将结果与仅使用 5,000 笔交易生成的结果进行比较。这是一个有趣的活动,因为它能提供一些关于随着更多数据收集,数据变化的方式,以及在使用分区技术时支持度值变化的见解。请注意,在练习中所做的工作并不是分区技术的完美代表,因为 5,000 笔交易是随机抽取的一个样本量。

注意

本章中的所有活动需要在同一个笔记本中进行。

以下步骤将帮助你完成这个活动:

  1. 在整个数据集上运行 Apriori 算法,并设置合理的参数。

  2. 将结果筛选到包含10 COLOUR SPACEBOY PEN的项集。将支持度值与练习 8.06执行 Apriori 算法中的支持度值进行比较。

  3. 添加一列包含项集长度的列。然后筛选出那些长度为二且支持度在[0.02, 0.021]范围内的项集。将此结果与练习 8.06执行 Apriori 算法中的结果进行比较。

  4. 绘制支持度值。

    注意

    此活动的解决方案可以在第 492 页找到。

此活动的输出结果将类似于以下内容:

图 8.25:支持度值的分布

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_25.jpg)

图 8.25:支持度值的分布

关联规则

关联规则学习是一种机器学习模型,旨在发掘交易数据中的隐藏模式(换句话说,就是描述任何零售商顾客购物习惯的关系)。关联规则的定义在本章早些时候定义并解释的常见概率度量中有所暗示。

考虑一个虚拟的频繁项集{牛奶,面包}。可以从该项集形成两个关联规则:牛奶 公式 面包和面包 公式 牛奶。为了简便起见,关联规则中的第一个项集称为前提,而第二个项集称为后果。一旦识别出关联规则,就可以计算所有之前讨论的度量,以评估关联规则的有效性,进而决定这些规则是否能在决策过程中加以利用。

关联规则的建立基于支持度和置信度。正如我们在上一节中讨论的,支持度用于识别哪些项集是频繁的,而置信度衡量特定规则的真实性频率。置信度通常被视为有趣性度量之一,因为它是决定是否形成关联规则的度量之一。因此,建立关联规则是一个两步过程。首先识别频繁的数据集,然后评估候选关联规则的置信度,如果该置信度值超过某个临界值,则结果为一个关联规则。

关联规则学习的一个主要问题是发现虚假关联,考虑到潜在规则的巨大数量,这些虚假关联很可能存在。所谓虚假关联是指在数据中出现惊人的频繁性,考虑到这种关联完全是偶然发生的。为了清楚地阐明这个观点,假设我们处于这样一种情况:我们有 100 条候选规则。如果我们以 0.05 的显著性水平运行独立性统计测试,我们仍然面临 5% 的机会,即当不存在关联时也发现关联的可能性。再进一步假设这 100 条候选规则都不是有效的关联。考虑到这 5% 的机会,我们仍然期望找到 5 条有效的关联规则。现在,将想象的候选规则列表扩展到数百万或数十亿,以便 5% 对应大量的关联。这个问题与几乎每个模型都面临的统计显著性和误差问题类似。值得注意的是,一些技术用于应对虚假关联问题,但它们既没有被一贯地纳入常用的关联规则库中,也不在本章的范围内。

现在让我们将我们对关联规则学习的工作知识应用到在线零售数据集中。

练习 8.07:推导关联规则

在这个练习中,我们将为在线零售数据集推导关联规则并探索相关指标。

注意

确保您在与之前练习相同的笔记本中完成此练习(换句话说,使用了 5,000 发票子集的笔记本,而不是来自活动的完整数据集)。

  1. 使用 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.26:仅使用 5,000 笔交易生成的前七行关联规则

    图 8.26:仅使用 5,000 笔交易生成的前七行关联规则

  2. 打印关联数量:

    print("Number of Associations: {}".format(rules.shape[0]))
    

    找到了 1,064 条关联规则。

    注意

    关联规则的数量可能有所不同。

  3. 尝试运行模型的另一个版本。选择任意最小阈值和任意的趣味度度量。探索返回的规则:

    rules2 = mlxtend.frequent_patterns\
             .association_rules(mod_colnames_minsupport, \
             metric="lift", \
             min_threshold=50,\
             support_only=False)
    rules2.loc[0:6]
    

    输出如下:

    图 8.27:关联规则的前七行

    图 8.27:关联规则的前七行

  4. 打印关联数量:

    print("Number of Associations: {}".format(rules2.shape[0]))
    

    使用提升度度量和最小阈值 50 找到的关联规则数量为 176,这比 步骤 2 中的数量显著低。我们将在后续步骤中看到 50 是相当高的阈值,因此返回的关联规则较少并不令人意外。

  5. 绘制置信度与支持度的图表,并识别数据中的特定趋势:

    rules.plot.scatter("support", "confidence", \
                       alpha=0.5, marker="*")
    plt.xlabel("Support")
    plt.ylabel("Confidence")
    plt.title("Association Rules")
    plt.show()
    

    输出如下:

    图 8.28:置信度与支持度的关系图

    图 8.28:置信度与支持度的关系图

    注意,既具有极高置信度又具有极高支持度的关联规则并不存在。这应该是有道理的。如果一个项集有高支持度,那么这些项很可能会与许多其他项一起出现,这使得置信度很难达到很高。

  6. 看看置信度的分布:

    rules.hist("confidence", grid=False, bins=30)
    plt.xlabel("Confidence of item")
    plt.ylabel("Number of items")
    plt.title("Frequency distribution of Confidence")
    plt.show()
    

    输出如下:

    图 8.29:置信度值的分布

    图 8.29:置信度值的分布

  7. 现在,看看提升度的分布:

    rules.hist("lift", grid=False, bins=30)
    plt.xlabel("Lift of item")
    plt.ylabel("Number of items")
    plt.title("Frequency distribution of Lift")
    plt.show()
    

    输出如下:

    图 8.30:提升度值的分布

    图 8.30:提升度值的分布

    如前所述,这个图表显示50是一个较高的阈值,因为很少有数据点超过这个值。

  8. 现在,看看杠杆的分布:

    rules.hist("leverage", grid=False, bins=30)
    plt.xlabel("Leverage of item")
    plt.ylabel("Number of items")
    plt.title("Frequency distribution of Leverage")
    plt.show()
    

    输出如下:

    图 8.31:杠杆值的分布

    图 8.31:杠杆值的分布

  9. 现在,看看信念度的分布:

    plt.hist(rules[numpy.isfinite(rules['conviction'])]\
                                  .conviction.values, bins = 30)
    plt.xlabel("Coviction of item")
    plt.ylabel("Number of items")
    plt.title("Frequency distribution of Conviction")
    plt.show()
    

    输出如下:

    图 8.32:信念度值的分布

图 8.32:信念度值的分布

有趣的是,四个分布的图表顶部出现了不同大小的尖峰,这意味着存在一些非常强的关联规则。置信度分布随着置信值增大而逐渐减少,但在最右端,接近最大值时,分布会稍微上升。提升度分布有最明显的尖峰。信念度分布图在 50 左右显示出一个小尖峰,也许更准确地说是一个小突起。最后,杠杆分布在较高的值处并没有显示出尖峰,但它具有一个较长的尾部,包含一些非常高的杠杆值。

花些时间探索模型发现的关联规则。这些产品配对对你来说合理吗?当你改变模型参数值时,关联规则的数量发生了什么变化?你是否意识到这些规则在试图改善任何零售业务时可能产生的影响?

在之前的练习中,我们构建并绘制了关联规则。关联规则可能很难解释,并且在很大程度上依赖于用于创建它们的阈值和度量标准。上一段中的问题旨在激发您思考算法如何工作以及如何使用规则。我们逐一讨论这些问题。显然有很多规则,所以关于这些规则是否合理的问题很难一概而论。对一些规则对的抽查似乎表明这些规则是合理的。例如,三组对包括儿童杯子和碗、茶杯和盘子、以及游乐屋厨房和客厅,这些都很有道理。当度量标准和参数发生变化时,结果也会发生变化。正如几乎所有建模练习中的情况一样,理想的做法是查看在不同情况下的结果,并利用所有发现做出最佳决策。

注意

要访问此特定部分的源代码,请参考packt.live/3fhf9bS

您还可以在packt.live/303vIBJ在线运行此示例。

您必须执行整个笔记本才能获得所需的结果。

活动 8.03:在完整的在线零售数据集上查找关联规则

让我们继续进行活动 8.02中设定的场景,在完整的在线零售数据集上运行 Apriori 算法。公司领导回来说,知道每个商品集在数据集中出现的频率非常好,但我们可以采取哪些商品集进行操作呢?哪些商品集可以用于改变商店布局或调整定价?为了找到这些答案,我们推导出完整的关联规则。

在此活动中,让我们从完整的在线零售交易数据集中推导关联规则。确保在使用完整数据集的笔记本中完成此活动(换句话说,要在包含完整零售数据集的笔记本中进行,而不是使用 5,000 个发票子集的练习笔记本)。

以下步骤将帮助我们执行此活动:

  1. 在完整的数据集上拟合关联规则模型。使用置信度度量标准和0.6的最低阈值。

  2. 统计关联规则的数量。这个数量与练习 8.07步骤 1中的数量是否不同?推导关联规则

  3. 绘制置信度与支持度的关系图。

  4. 查看置信度、提升度、杠杆度和置信度的分布。

预期的关联规则输出如下:

图 8.33:预期的关联规则

图 8.33:预期的关联规则

注意

此活动的解决方案可在第 496 页找到。

在此活动结束时,您将获得提升度、杠杆度和置信度的图表。

总结

市场篮子分析用于分析和提取交易或类似交易的数据洞察,这些洞察可以帮助推动许多行业的发展,最著名的就是零售行业。这些决策可能包括如何布局零售空间、哪些产品进行折扣、以及如何定价产品。市场篮子分析的核心支柱之一是关联规则的建立。关联规则学习是一种机器学习方法,用于揭示个体购买产品之间的关联,这些关联足够强大,可以用来做商业决策。关联规则学习依赖于 Apriori 算法,以计算高效的方式找到频繁项集。这些模型不同于典型的机器学习模型,因为它们不进行预测,结果也无法用任何单一指标进行评估,参数值不是通过网格搜索选择的,而是根据特定问题的领域需求来选择的。尽管如此,所有机器学习模型核心的模式提取目标在这里依然是存在的。

本章结束时,你应该能够舒适地评估和解读概率性指标,能够使用mlxtend运行和调整 Apriori 算法及关联规则学习模型,并了解这些模型在商业中的应用。你应该知道,你所在社区的杂货店内物品的定位和定价,可能是基于你和许多其他顾客过去的购买行为来做出的选择!

在下一章,我们将探讨使用核密度估计的热点分析,核密度估计无疑是所有统计学和机器学习中最常用的算法之一。

第十章:9. 热点分析

概述

在本章中,我们将进行热点分析,并可视化热点分析的结果。我们将使用核密度估计,这是构建分布时最常用的算法,使用的是一组观察值。我们将构建核密度估计模型,并描述概率密度函数背后的基本原理。在本章结束时,你应该能够利用 Python 库构建多维密度估计模型,并处理地理空间数据。

引言

在前一章中,我们探讨了市场篮子分析。市场篮子分析,正如你希望记得的那样,是一种算法,旨在理解交易数据中所有项目和项目组之间的关系。这些关系随后被用来帮助零售商优化店铺布局、更加准确地订货库存,并在不减少每笔交易中的商品数量的情况下调整价格。现在,我们将转向探索热点建模。

让我们考虑一个假设场景:一种新的疾病开始在你所在国家的多个社区中蔓延,政府正在努力寻找应对这一公共卫生紧急情况的方法。应对这一健康危机的关键是流行病学知识,包括患者所在的位置以及疾病的传播情况。定位和量化问题区域(通常称为热点)能够帮助卫生专业人员、政策制定者和应急响应团队制定最有效和高效的抗疫策略。这个场景突出了热点建模的众多应用之一。

热点建模是一种用于识别一个群体在地理区域分布情况的方法;例如,如何将之前提到的疾病感染者的群体分布在全国各地。创建这种分布依赖于代表性样本数据的可用性。请注意,群体可以是任何在地理学上可定义的事物,包括但不限于犯罪、感染疾病的个体、具有某些人口特征的人群或飓风等:

图 9.1:一个虚构的火灾位置数据示例,展示了一些潜在的热点区域

图 9.1:一个虚构的火灾位置数据示例,展示了一些潜在的热点区域

热点分析非常流行,这主要是因为它在可视化和解释结果方面非常容易。报纸、网站、博客和电视节目都利用热点分析来支持其中的论点、章节和话题。尽管它可能不像最流行的机器学习模型那样广为人知,但主要的热点分析算法,即核密度估计,无疑是最广泛使用的分析技术之一。人们甚至在日常生活中不自觉地进行核密度估计。核密度估计是一种热点分析技术,用于估计特定地理事件的真实人口分布。在深入了解算法本身之前,我们需要简要回顾一下空间统计学和概率密度函数。

空间统计学

空间统计学是统计学的一个分支,专注于分析具有空间属性的数据,包括地理或拓扑坐标。它与时间序列分析类似,目标是分析在某个维度上变化的数据。在时间序列分析中,数据变化的维度是时间,而在空间统计学中,数据则在空间维度上发生变化。空间统计学涵盖了多种技术,其中我们这里关注的技术是核密度估计。与大多数统计分析的目标相同,在空间统计学中,我们试图通过采集地理数据样本来生成洞察并做出预测。地震分析是空间统计分析常用的一个领域。通过收集地震位置数据,可以生成显示高低地震概率区域的地图,这可以帮助科学家确定未来地震可能发生的地点以及地震强度的预期。

概率密度函数

核密度估计采用了概率密度函数PDF)的思想,这是统计学中的基本概念之一。概率密度函数是描述连续随机变量行为的函数。也就是说,它表达了随机变量取某个范围内值的可能性或概率。以美国男性身高为例,利用美国男性身高的概率密度函数,我们可以计算出某位美国男性身高在 1.9 米到 1.95 米之间的概率。

图 9.2:标准正态分布

图 9.2:标准正态分布

统计学中最常见的密度函数可能是标准正态分布,它就是以零为中心,标准差为一的正态分布。

与密度函数不同,统计学家或数据科学家通常获得的是来自未知总体分布的随机收集的样本值。这时,核密度估计就派上了用场;它是一种利用样本数据估计随机变量未知概率密度函数的技术。下图表示了一个简单但更合理的分布示例,这是我们希望通过核密度估计来估计的分布。我们会选择一些观察值(样本数据点),并使用这些观察值来创建一个平滑的分布,模拟我们无法知晓的真实底层分布。

图 9.3:三种正态分布的混合

图 9.3:三种正态分布的混合

在商业中使用热点分析

我们已经提到了一些热点建模可以有效影响行业的方法。在报告传染病时,卫生组织和媒体公司通常会使用热点分析来传达疾病的地理分布和根据地理位置感染该疾病的可能性。通过使用热点分析,这些信息可以可靠地计算和传播。热点分析非常适用于处理健康数据,因为可视化非常直观。这意味着数据被故意或无意误解的可能性相对较低。

热点分析也可以用来预测某些事件可能发生的地理位置。越来越多的研究领域正在利用热点分析的预测能力,其中包括自然灾害和极端天气事件的研究。以地震为例,由于重大地震之间的时间间隔可能很长,而且需要跟踪和测量地震的设备相对较新,因此地震预测一直以难度大著称。

在公共政策和资源部署方面,热点分析在处理人口统计学分析时非常有影响力。确定资源(包括金钱和人员)应该部署到哪里可能是具有挑战性的;然而,鉴于资源通常是人口特定的,热点分析是一种有用的技术,因为它可以用来确定特定人口特征的分布。我们所说的人口特征是指我们可以找到高中毕业生、来自特定全球地区的移民,或者年收入达到或超过$100,000 的人的地理分布。

热点建模的应用几乎是无穷无尽的。我们这里只讨论了其中的三种主要应用。

核密度估计

热点分析的主要方法之一是核密度估计。核密度估计通过样本数据和两个参数(即核函数带宽值)构建估计的密度。估计的密度像任何分布一样,本质上是随机变量行为的指导。这里的意思是,随机变量在任何特定值{x1, ….., xn}上出现的频率。在处理通常是地理数据的热点分析时,估计的密度回答了这个问题:给定事件的特定经纬度对出现的频率如何? 如果某个特定经纬度对{xlongitude, xlatitude}以及附近的其他经纬度对出现的频率很高,那么使用样本数据构建的估计密度将显示出上述经纬度对周围区域的出现概率较高。

核密度估计被称为一种平滑算法,因为它在样本数据上绘制了一条平滑曲线。如果数据是一个具有代表性的样本,这条曲线可以很好地估计真实的总体密度函数。换句话说,当核密度估计方法正确应用时,它旨在去除样本数据中固有的噪声,而这些噪声并不是总体特征。该模型的唯一假设是数据确实属于某种可解释且有意义的密度,从中可以获得见解并付诸实践。也就是说,存在一个真实的潜在分布。我们假设样本数据中包含数据点簇,这些簇与真实总体中的高概率区域对齐。创建真实总体密度的高质量估计的一个好处是,估计的密度可以用于从总体中采样更多的数据。

在这一简短的介绍之后,您可能会有以下两个问题:

  • 什么是带宽值?

  • 什么是核函数?

接下来我们将回答这两个问题。

带宽值

核密度估计中最关键的参数被称为带宽值,它对估计质量的影响不容小觑。带宽值的高阶定义是它决定了平滑的程度。如果带宽值较低,则估计的密度会呈现有限的平滑,这意味着密度将捕捉到样本数据中的所有噪声。如果带宽值较高,则估计的密度将非常平滑。过于平滑的密度会去除估计密度中的真实密度特征,而这些特征是合法的,而不是噪声。

用更统计学的术语来说,带宽参数控制着偏差-方差的权衡。也就是说,低带宽值会导致高方差,因为密度对样本数据的方差非常敏感。低带宽值会限制模型适应和解决样本数据中不存在于总体中的空隙的能力。使用低带宽值估计的密度往往会过度拟合数据(这也称为欠平滑的密度)。当使用高带宽值时,结果密度会发生欠拟合,估计密度的偏差较大(这也称为过度平滑的密度)。

注意

在所有随后的练习和活动中,输出可能会略有不同于下文所示的结果。原因如下:数据样本的差异可能会导致输出略有不同,而且sklearnseaborn库中有一些非确定性元素,可能导致结果在每次运行时有所不同。

练习 9.01:带宽值的影响

在本练习中,我们将使用九个不同的带宽值拟合九个不同的模型,来处理本练习中创建的样本数据。此处的目标是巩固我们对带宽参数影响的理解,并明确指出,如果要得到准确的估计密度,带宽值需要谨慎选择。请注意,寻找最佳带宽值将是下一节的主题。所有练习都将在使用 Python 3 的 Jupyter 笔记本中完成;请确保通过pip安装所有必要的包。安装mpl_toolkits中的basemap模块最简单的方法是使用Anaconda。有关下载和安装Anaconda的说明,请参见本书开头:

  1. 加载本章所需的所有库。basemap库用于创建涉及位置数据的图形。其他所有库都在本书的前面部分使用过。

    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.model_selection
    import sklearn.neighbors
    seaborn.set()
    
  2. 通过混合三个正态分布来创建一些样本数据(vals)。除了样本数据外,还需要定义真实的密度曲线(true_density)和数据将被绘制的范围(x_vec):

    x_vec = numpy.linspace(-30, 30, 10000)[:, numpy.newaxis]
    numpy.random.seed(42)
    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]))
    
  3. 定义一个元组列表,用于指导多图形的创建。每个元组包含特定子图的行和列索引,以及用于在该子图中创建估计密度的带宽值。请注意,为了本练习的方便,带宽值是随机选择的,但实际上选择最佳带宽值是有一定策略的。我们将在下一节深入探讨这一点。

    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)]
    
  4. 创建九个图,每个图使用不同的带宽值。第一个图(索引为(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))
    plt.show()
    

    输出结果如下:

    图 9.4:一个 3 x 3 矩阵的子图

图 9.4:一个 3 x 3 矩阵的子图

请注意,在第九个子图中(带宽为 5 的地方),估计的密度曲线明显不足以拟合数据。随着带宽值的增加,估计的密度变得更加平滑,直到它明显不足以拟合数据。从视觉效果来看,最优带宽值可能约为1.6

注意

要访问该特定部分的源代码,请参考 packt.live/2UOHbTZ

您还可以在网上运行此示例,网址为 packt.live/38DbmTo

您必须执行整个 Notebook 才能获得预期的结果。

下一步是设计一个算法来识别最优带宽值,以使估计的密度最合理,从而是最可靠且可操作的。

选择最优带宽

如前面练习中提到的,我们可以通过简单地通过视觉比较几种密度来接近选择最优带宽。然而,这既不是选择参数值的最有效方法,也不是最可靠的方法。

优化带宽值有两种标准方法,这两种方法都会出现在未来的练习和活动中。第一种方法是插件法(或公式化方法),它是确定性的,并且没有在样本数据上进行优化。插件法通常实现速度更快,编码更简单,解释也更容易。然而,这些方法有一个大缺点,那就是与在样本数据上进行优化的方法相比,它们的准确性往往较低。这些方法还存在分布假设。最流行的插件方法是 Silverman 法则和 Scott 法则。详细解释这些法则超出了本文的范围,并且对于完全理解核密度估计并非必需,且需要一些复杂的数学工作,因此我们将跳过进一步的探讨。不过,如果有兴趣,公开的许多优秀资源都详细解释了这些法则,并且有不同深度的说明。默认情况下,seaborn包(将在未来的练习中使用)使用 Scott 法则作为确定带宽值的方法。

寻找最优带宽值的第二种方法,也是更强健的方法,是通过搜索一个预定义的带宽值网格。网格搜索是一种经验性方法,在机器学习和预测建模中经常用来优化模型的超参数。该过程从定义带宽网格开始,带宽网格就是要评估的一系列带宽值。带宽网格是随机选择的。使用网格中的每个带宽值来创建估计密度;然后,使用伪对数似然值来评分估计密度。最优带宽值是具有最大伪对数似然值的那个带宽值。可以把伪对数似然值看作是获得数据点的概率,数据点出现在我们希望的地方,而没有数据点出现的地方的概率。理想情况下,这两个概率应该都很大。考虑一种情况,即获得数据点的概率很低,这意味着样本中的数据点可能是异常的,因为在真实分布下,我们不会期望在某个地方获得数据点,且其高概率值不成立。伪对数似然值是一种评估指标,其作用与分类问题中的准确度分数和回归问题中的均方根误差相同。

现在,让我们实现网格搜索方法来优化带宽值。

练习 9.02:使用网格搜索选择最优带宽

在本练习中,我们将为练习 9.01中的样本数据创建一个估计的密度,带宽值的影响,并通过网格搜索和交叉验证确定一个最优带宽值。为了执行网格搜索和交叉验证,我们将使用sklearn,这是我们在本书中一直使用的工具。

注意

本练习是练习 9.01带宽值的影响的延续,因为我们使用的是相同的样本数据,并继续探讨带宽值的影响。

  1. 定义带宽值网格和网格搜索交叉验证模型。理想情况下,应该使用留一交叉验证方法,但为了使模型在合理的时间内运行,我们将使用 10 折交叉验证。将模型拟合到样本数据,如下所示:

    # define a grid of 100 possible bandwidth values
    bandwidths = 10 ** numpy.linspace(-1, 1, 100)
    # define the grid search cross validation model
    grid = sklearn.model_selection.GridSearchCV\
           (estimator=sklearn.neighbors.KernelDensity(),\
            param_grid={"bandwidth": bandwidths},\
            cv=10)
    # run the model on the previously defined data
    grid.fit(vals)
    

    输出结果如下:

    图 9.5:交叉验证模型的输出

    图 9.5:交叉验证模型的输出

  2. 从模型中提取最优带宽值。best_params_函数从模型对象中提取网格中表现最好的参数。

    best_bandwidth = grid.best_params_["bandwidth"]
    print("Best Bandwidth Value: {}" \
          .format(best_bandwidth))
    

    最优带宽值应大约为1.6。我们可以将最优带宽值解释为产生最大伪对数似然值的带宽值。请注意,根据网格中包含的值,最优带宽值可能会发生变化。

  3. 绘制样本数据的直方图,并叠加真实密度和估计密度。在这种情况下,估计的密度将是最佳估计密度:

    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')
    plt.show()
    

    输出结果如下:

    图 9.6:随机样本的直方图

    ](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_06.jpg)

图 9.6:随机样本的直方图

在这张直方图中,真实密度和最佳估计密度重叠显示。估计密度没有明显的过拟合或欠拟合,且能够很好地捕捉到三个聚类。可以说,它可能更好地贴合真实密度,但这仅仅是由模型根据给定的数据集生成的估计密度。

注意

若要访问此特定部分的源代码,请参考 packt.live/2UOHbTZ

你还可以在线运行这个示例,地址是 packt.live/38DbmTo

你必须执行整个 Notebook 才能得到预期的结果。

现在让我们进入第二个问题:什么是核函数,它在其中扮演什么角色?

核函数

另一个需要设置的参数是核函数。核函数是一个非负函数,它控制密度的形状。像主题模型一样,我们在一个非负环境中工作,因为出现负的似然性或概率是没有意义的。核函数通过以系统的方式加权数据点来控制估计密度的形状。这种加权的系统方法相对简单;与许多其他数据点接近的数据点会被加权,而那些孤立或远离其他数据点的数据点会被减权。被加权的数据点在最终估计的密度中将对应于较高的似然点。

可以使用许多函数作为核函数,但六种常见的选择是高斯(Gaussian)、顶帽(Tophat)、埃潘尼切科夫(Epanechnikov)、指数(Exponential)、线性(Linear)和余弦(Cosine)。这些函数各自代表了不同的分布形状。请注意,在每个公式中,参数 h 代表带宽值:

图 9.7:高斯核函数的公式

图 9.8:顶帽核函数的公式

  • 埃潘尼切科夫:每个观察值具有丘状的权重。

图 9.9:埃潘尼切科夫核函数的公式

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_09.jpg)

图 9.9:埃潘尼切科夫核函数的公式

  • 指数:每个观察值具有三角形的权重,三角形的边是凹形的。

图 9.10:指数核函数的公式

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_10.jpg)

图 9.10:指数核函数的公式

  • 线性:每个观察值具有三角形的权重。

图 9.11:线性核函数的公式

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_11.jpg)

图 9.11:线性核的公式

  • 余弦核:每个观测值都有一个圆顶形的权重。这个圆顶形比埃潘尼科夫核在顶部更窄。

图 9.12:余弦核的公式

图 9.12:余弦核的公式

以下是六个核函数的分布形状:

图 9.13:六个核函数的整体形状

图 9.13:六个核函数的整体形状

核函数的选择并非完全无关紧要,但它肯定不像带宽值的选择那么重要。一个合理的做法是,对于所有密度估计问题,都使用高斯核,这也是我们在接下来的练习和活动中所做的。

练习 9.03:核函数的影响

我们将演示核函数的选择如何影响密度估计的质量。就像我们在探索带宽值效应时做的那样,我们将保持其他所有参数不变,使用在前两次练习中生成的相同数据,并使用之前指定的六个核函数运行六个不同的核密度估计模型。六个估计的密度之间应该能看到明显的差异,但这些差异应该比使用不同带宽值估计的密度之间的差异稍微小一些。请注意,本练习应在与之前练习相同的 Notebook 中执行。

  1. 定义一个元组列表,格式与之前定义的相同。每个元组包含子图的行和列索引,以及用于创建密度估计的核函数:

    position_kernel_vec = [(0, 0, 'gaussian'), (0, 1, 'tophat'), \
                           (1, 0, 'epanechnikov'), \
                           (1, 1, 'exponential'), \
                           (2, 0, 'linear'), (2, 1, 'cosine'),]
    
  2. 使用不同的核函数拟合并绘制六个核密度估计模型。为了真正理解核函数之间的差异,我们将带宽值设置为在练习 9.02中找到的最优带宽值,即使用网格搜索选择最优带宽,并且不调整它:

    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()))
    plt.show()
    

    输出如下:

    图 9.14:一个 3 x 2 的子图矩阵

图 9.14:一个 3 x 2 的子图矩阵

在六个核函数中,高斯核生成了最合理的估计密度。除此之外,注意到不同核函数估计的密度之间的差异小于不同带宽值估计的密度之间的差异。这印证了之前的观点,即带宽值是更重要的参数,应在模型构建过程中作为重点。

注意

要访问此特定部分的源代码,请参阅packt.live/2UOHbTZ

您也可以在packt.live/38DbmTo在线运行此示例。

必须执行整个 Notebook 才能获得所需的结果。

在我们大致理解的基础上,让我们讨论核密度估计的推导过程。

核密度估计推导

我们跳过正式的数学推导,转而采用直观的流行推导方法。核密度估计将每个样本数据点转化为其自身的分布,其宽度由带宽值控制。然后,将这些个体分布相加,生成所需的密度估计。这一概念相对容易展示;然而,在接下来的练习中,我们在进行演示之前,先尝试从抽象的角度进行思考。对于包含许多样本数据点的地理区域,个体密度将会重叠,并通过相加这些密度,产生在估计密度中更高的可能性点。类似地,对于包含少量或没有样本数据点的地理区域,个体密度将不会重叠,因此在估计密度中会对应较低的可能性点。

练习 9.04:模拟核密度估计的推导

这里的目标是展示将个体分布相加,创建随机变量的整体估计密度的概念。我们将通过从一个样本数据点开始,逐步建立这一概念,并逐渐增加样本数据点的数量。此外,还会应用不同的带宽值,因此我们对带宽值对这些个体密度影响的理解将进一步巩固。请注意,本练习应与所有其他练习一起在同一个笔记本中完成。

  1. D 热点分析定义一个函数,用于评估正态分布。输入值包括表示随机变量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
    
  2. 将一个样本数据点绘制为直方图,并显示其在不同带宽值下的个体密度:

    m = numpy.array([5.1])
    b_vec = [0.1, 0.35, 0.8]
    x_vec = numpy.linspace(1, 10, 100)[:, None]
    figOne, 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))
    plt.show()
    

    输出结果如下:

    图 9.15:显示一个数据点及其在不同带宽值下的个体密度

    图 9.15:显示一个数据点及其在不同带宽值下的个体密度

    在这里,我们可以看到已经建立的结论:较低的带宽值会产生非常狭窄的密度,往往会导致过拟合数据。

  3. 重复执行步骤 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]
    figMulti, 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))
    plt.show()
    

    输出结果如下:

    图 9.16:绘制数据点

图 9.16:绘制数据点

前面的图像展示了 16 个数据点,它们在不同带宽值下的个体密度,以及它们个体密度的总和。

再次,毫不意外,利用最小带宽值的图表展示了一个过度拟合的估计密度。也就是说,估计的密度捕捉了样本数据中的所有噪声。在这三种密度中,第二种密度(带宽值设置为0.35)是最合理的。

注意

若要访问该特定部分的源代码,请参考packt.live/2UOHbTZ

你也可以在packt.live/38DbmTo上在线运行此示例。

您必须执行整个 Notebook 才能获得所需的结果。

活动 9.01:在一维中估计密度

在本活动中,我们将生成一些伪造的样本数据,并使用核密度估计估算密度函数。带宽值将通过网格搜索交叉验证进行优化。目标是通过在简单的一维情况下运行模型来巩固我们对这种有用方法的理解。我们将再次利用 Jupyter 笔记本来完成工作。

假设我们将创建的样本数据描述的是美国某州的房屋价格。暂时忽略以下样本数据中的数值。问题是,房价的分布是什么样的?我们能否提取出某个特定价格区间内房屋的概率? 这些问题以及更多问题可以通过核密度估计来解答。

以下是完成活动的步骤:

  1. 打开一个新的笔记本并安装所有必要的库。

  2. 从标准正态分布中抽取 1,000 个数据点。在样本的最后 625 个值上加上 3.5(即索引在 375 到 1,000 之间)。使用numpy.random.RandomState设置随机状态为 100,以保证相同的样本值,然后使用rand.randn(1000)调用随机生成数据点。

  3. 将 1,000 个数据点样本数据绘制为直方图,并在其下方添加散点图。

  4. 定义一个带宽值网格。然后,定义并拟合一个网格搜索交叉验证算法。

  5. 提取最佳带宽值。

  6. 步骤 3重新绘制直方图并叠加估计的密度。

输出将如下所示:

图 9.17:叠加最佳估计密度的随机样本直方图

图 9.17:叠加最佳估计密度的随机样本直方图

本活动的解决方案可以在第 501 页找到。

热点分析

一开始,热点是数据点高度集中的区域,例如犯罪率异常高的特定社区,或受异常数量的龙卷风影响的国家大片区域。热点分析是通过抽样数据,在一个人群中寻找这些可能存在的热点。这个过程通常通过利用核密度估计来完成。

热点分析可以分为四个高层次的步骤:

  1. 收集数据:数据应包括物体或事件的位置。正如我们简要提到的,运行并实现可操作结果所需的数据量是相对灵活的。最佳状态是拥有一个能代表总体的样本数据集。

  2. 识别基础地图:下一步是识别哪种基础地图最适合项目的分析和展示需求。在这张基础地图上,模型的结果将被叠加,以便能更清晰地表达热点的位置,使用更易于理解的术语,如城市、邻里或区域。

  3. 执行模型:在这一步骤中,你需要选择并执行一种或多种空间模式提取方法,以识别热点。对我们来说,这个方法将是——毫不奇怪——核密度估计。

  4. 创建可视化:热点地图通过将模型结果叠加在基础地图上生成,以支持未解决的业务问题。

从可用性角度来看,热点分析的主要问题之一是,热点的统计显著性并不容易确定。大多数关于统计显著性的问题围绕热点的存在展开。也就是说,发生概率的波动是否真的构成了统计上显著的波动?需要注意的是,进行核密度估计并不需要统计显著性,我们在接下来的工作中将完全不涉及显著性。

虽然“热点”一词传统上用于描述位置数据点的聚集,但它并不限于位置数据。任何数据类型都可能有热点,不管它们是否被称为热点。在接下来的练习中,我们将对一些非位置数据进行建模,以寻找热点,这些热点将是特征空间中具有高或低发生概率的区域。

练习 9.05:加载数据并使用 Seaborn 建模

在本练习中,我们将使用 seaborn 库来拟合并可视化核密度估计模型。这将应用于位置数据和非位置数据。在进行建模之前,我们需要加载数据,该数据是来自 sklearn 的加州住房数据集,并以 CSV 格式提供。文件需要从 GitHub 仓库下载并保存在本地计算机上。该数据集取自 1990 年的美国人口普查,描述了当时加州的住房情况。每一行数据描述一个普查区块组。普查区块组的定义对于本练习并不重要,因此我们将在此跳过定义,专注于更多的实践编码和建模。需要强调的是,所有变量都已经按普查区块进行了汇总。例如,MedInc 是每个普查区块中家庭的中位数收入。有关此数据集的更多信息,请访问 scikit-learn.org/stable/datasets/index.html#california-housing-dataset

  1. 使用 california_housing.csv 加载加州住房数据集。打印数据框的前五行:

    df = pandas.read_csv('./california_housing.csv', header=0)
    df.head()
    

    注意

    文件的路径取决于文件在系统中的位置。

    输出如下:

    图 9.18:来自 sklearn 的加利福尼亚州房屋数据集的前五行

    图 9.18:来自 sklearn 的加利福尼亚州房屋数据集的前五行

  2. 根据HouseAge特征过滤数据框,该特征是每个普查区块的房屋中位数年龄。仅保留HouseAge小于或等于 15 的行,并将数据框命名为dfLess15。打印数据框的前五行,然后将数据框缩减为仅包含经度和纬度特征:

    dfLess15 = df[df['HouseAge'] <= 15.0]
    dfLess15 = dfLess15[['Latitude', 'Longitude']]
    dfLess15.head()
    

    输出如下:

    图 9.19:过滤后的数据集前五行

    图 9.19:过滤后的数据集前五行

  3. 使用seaborn对基于经度和纬度数据点构建的核密度估计模型进行拟合和可视化。该模型有四个输入参数:需要估计密度的两个列的名称(即经度和纬度)、这些列所属的数据框,以及密度估计方法(即kde或核密度估计):

    seaborn.jointplot("Longitude", "Latitude", dfLess15, kind="kde")
    

    输出如下:

    图 9.20:联合图

    图 9.20:联合图

    注意

    图表可能有所不同,因为每次的估计都不完全相同。

    这个联合图包含了二维估计密度以及dfLess15数据集的边际密度。

    如果我们将这些结果叠加到加利福尼亚州的地图上,我们会看到热点区域集中在南加利福尼亚,包括洛杉矶和圣地亚哥,湾区,包括旧金山,以及在一定程度上被称为中央山谷的区域。这个seaborn图形的一个优点是,我们可以获得二维的估计密度图,并且分别展示了经度和纬度的边际密度。

  4. 基于HouseAge特征创建另一个过滤后的数据框;这次仅保留HouseAge大于 40 的行,并将数据框命名为dfMore40。此外,移除除经度和纬度之外的所有列。然后,打印数据框的前五行:

    dfMore40 = df[df['HouseAge'] > 40.0]
    dfMore40 = dfMore40[['Latitude', 'Longitude']]
    dfMore40.head()
    

    输出如下:

    图 9.21:过滤后的数据集顶部

    图 9.21:过滤后的数据集顶部

  5. 重复步骤 3中的过程,但使用这个新的过滤后的数据框:

    seaborn.jointplot("Longitude", "Latitude", dfMore40, kind="kde")
    

    输出如下:

    图 9.22:联合图

    图 9.22:联合图

    这个联合图包含了二维估计密度以及dfMore40数据集的边际密度。

    这个估计密度要紧凑得多,因为数据几乎完全聚集在两个区域。这些区域分别是洛杉矶和湾区。与步骤 3中的图表对比,我们注意到房屋开发已经扩展到整个州。此外,新的住房开发在更多的普查区块中出现的频率也显著提高。

  6. 我们再创建另一个过滤后的数据框。这次仅保留HouseAge小于或等于五的行,并将数据框命名为dfLess5。绘制PopulationMedInc的散点图,如下所示:

    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.23:中位收入与人口的散点图

    图 9.23:中位收入与人口的散点图

    这是HouseAge列中值小于或等于五的中位收入与人口的散点图。

  7. 使用另一个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.24:与步骤 6 中创建的相同散点图,叠加了估算的密度

图 9.24:与步骤 6 中创建的相同散点图,叠加了估算的密度

在这里,估算的密度显示,人口较少的普查区块相比于拥有较高中位收入的区块,更有可能拥有较低的中位收入。这个步骤的重点是展示如何在非位置数据上使用核密度估计。像这样的图通常被称为等高线图。

注意

要访问该部分的源代码,请参阅packt.live/2UOHbTZ

你还可以在packt.live/38DbmTo在线运行此示例。

你必须执行整个笔记本才能获得预期的结果。

在展示热点分析结果时,通常需要涉及某种类型的地图,因为热点分析通常是在位置数据上进行的。获取可以叠加估算密度的地图并不是一件容易的事。由于版权问题,我们将使用非常基础的地图,称为底图,在其上叠加我们的估算密度。将留给你自己扩展本章所学的知识,去处理更复杂和更详细的地图。地图环境也可能很复杂,下载和安装起来很耗时。

练习 9.06:与底图一起工作

本练习使用了mpl_toolkitsbasemap模块。basemap是一个地图绘制库,可以用来创建基础地图或地理区域的轮廓。这些地图可以叠加核密度估计的结果,从而清晰地展示热点位置。

首先,在 Jupyter 笔记本中运行import mpl_toolkits.basemap检查basemap是否已安装。如果没有错误加载,说明你已经准备好了,不需要采取任何进一步的操作。如果调用失败,请通过运行python3 -m pip install basemap使用pip安装basemap。重新启动任何已打开的笔记本后,你应该可以正常使用。请注意,pip安装仅在已安装 Anaconda 的情况下有效。

本练习的目标是重新建模并重新绘制练习 9.05中的位置数据,使用 Seaborn 加载数据与建模,使用sklearn的核密度估计功能和basemap的映射功能。从过滤后的数据框dfLess15中提取经度和纬度值,按步骤进行操作。请注意,本练习应与其他所有练习在同一个笔记本中完成。

  1. 构建位置网格,以便在其上铺设估计的密度。位置网格是练习 9.01中定义随机变量范围的一维向量的二维位置等效物:

    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 
    

    输出结果如下:

    图 9.25:表示 dfLess15 数据集的网格的 x 和 y 分量

    图 9.25:表示 dfLess15 数据集的网格的 x 和 y 分量

  2. 定义并拟合一个核密度估计模型。将带宽值设置为 0.05。然后为位置网格上的每个点创建可能性值:

    kde15 = sklearn.neighbors.KernelDensity(bandwidth=0.05, \
                                            metric='minkowski', \
                                            kernel='gaussian', \
                                            algorithm='ball_tree')
    kde15.fit(dfLess15.values)
    

    核密度估计模型的输出结果如下:

    KernelDensity(algorithm='ball_tree', atol=0, bandwidth=0.05, \
                  breadth_first=True, kernel='gaussian', \
                  leaf_size=40, metric='minkowski', \
                  metric_params=None, rtol=0)
    
  3. 将训练好的模型拟合到xy网格上,并打印出形状,如下所示:

    log_density = kde15.score_samples(xy15)
    density = numpy.exp(log_density)
    density = density.reshape(x15.shape)
    print("Shape of Density Values:\n{}\n".format(density.shape))
    

    输出结果如下:

    Shape of Density Values:
    (3287, 3287)
    

    请注意,如果你打印出可能性值的形状,它是 3,287 行 × 3,287 列,即 10,804,369 个可能性值。这与预设的经度和纬度网格xy15中的值数相同。

  4. 创建加利福尼亚州的轮廓,并叠加步骤 2中计算的估计密度:

    Exercise9.01-Exercise9.06.ipynb
    fig15 = plt.figure(figsize=(10, 10))
    fig15.suptitle(\
        """
        Density Estimation:
        Location of Housing Blocks
        Where the Median Home Age <= 15 Years
        """,\
        fontsize=16)
    The complete code for this step can be found at https://packt.live/38DbmTo.
    

    输出结果如下:

    图 9.26:将 dfLess15 的估计密度叠加到加利福尼亚州的轮廓上

    图 9.26:将 dfLess15 的估计密度叠加到加利福尼亚州的轮廓上

    0.05的值是故意设置为略微过拟合数据。你会注意到,与练习 9.05中的较大聚类(构成密度的部分)不同,使用 Seaborn 加载数据与建模中的估计密度由许多更小的聚类组成。这种略微过拟合的密度可能比之前版本的密度更有帮助,因为它能更清晰地显示高概率的普查区块的确切位置。之前密度中一个高概率区域是南加州,但南加州是一个面积庞大、人口众多且有许多市政区的地方。请记住,在使用这些结果做商业决策时,可能需要某些特定的精确度,且如果样本数据能支持该级别的精确度或细化结果,应该提供相关信息。

  5. 重复步骤 1,但使用dfMore40数据框:

    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 
    

    输出结果如下:

    图 9.27:表示 dfMore40 数据集的网格的 x 和 y 分量

    图 9.27:表示 dfMore40 数据集的网格的 x 和 y 分量

  6. 使用步骤 4中建立的网格重复步骤 2

    kde40 = sklearn.neighbors.KernelDensity(bandwidth=0.05, \
                                            metric='minkowski', \
                                            kernel='gaussian', \
                                            algorithm='ball_tree')
    kde40.fit(dfMore40.values)
    log_density = kde40.score_samples(xy40)
    density = numpy.exp(log_density)
    density = density.reshape(x40.shape)
    print("Shape of Density Values:\n{}\n".format(density.shape))
    
  7. 使用步骤 5中计算出的估算密度重复执行步骤 3

Exercise9.01-Exercise9.06.ipynb
fig40 = plt.figure(figsize=(10, 10))
fig40.suptitle(\
    """
    Density Estimation:
    Location of Housing Blocks
    Where the Median Home Age > 40 Years
    """, \
    fontsize=16)
The complete code for this step can be found at https://packt.live/38DbmTo.

输出结果如下:

图 9.28:估算的 dfMore40 密度叠加在加利福尼亚州的轮廓上

图 9.28:估算的 dfMore40 密度叠加在加利福尼亚州的轮廓上

注意

要访问该特定部分的源代码,请参考packt.live/2UOHbTZ

你也可以在packt.live/38DbmTo上在线运行这个示例。你必须执行整个 Notebook 才能获得预期的结果。

这个估算密度实际上是我们在练习 9.05加载数据与使用 Seaborn 建模中所做的密度的重新计算。虽然步骤 3中的密度能为那些对房地产或人口普查感兴趣的人提供更多细节,但这个密度与练习 9.05中对应的密度实际上看起来没有太大不同。热点主要集中在洛杉矶和旧金山,几乎没有其他地方的点。

活动 9.02:分析伦敦的犯罪数据

在这个活动中,我们将使用来自data.police.uk/data/的伦敦犯罪数据,通过核密度估计进行热点分析。由于处理地图数据的难度较大,我们将使用seaborn来可视化分析结果。然而,如果你觉得勇敢,并且已经能够运行练习 9.06与底图一起工作中的所有图形,那么鼓励你尝试使用地图。

对该犯罪数据进行热点分析的动机有两个方面。首先,我们被要求确定某些类型的犯罪发生的高概率位置,以便能够最大化警力资源的分配。接着,作为后续工作,我们需要确定某些类型的犯罪热点是否随着时间变化而发生变化。这两个问题都可以通过使用核密度估计来解答。

注意

该数据集来自data.police.uk/data/。它包含根据开放政府许可证 v3.0 许可的公共部门信息。

你还可以从 Packt 的 GitHub 下载,链接为packt.live/2JIWs2z

或者,若要直接从源网站下载数据,请访问前述的警察网站,勾选Metropolitan Police Service,然后设置日期范围为2018 年 7 月2018 年 12 月。接着,点击生成文件,然后点击立即下载,并将下载的文件命名为metro-jul18-dec18。确保你知道如何或能够找到下载目录的路径。

该数据集包含根据开放政府许可证 v3.0 许可的公共部门信息。

完成此活动的步骤如下:

  1. 加载犯罪数据。使用你保存下载目录的路径,创建一个包含年月标签的列表,使用read_csv命令逐个加载文件,然后将这些文件连接在一起。

  2. 打印完整(六个月)和合并数据集的诊断信息。

  3. 将数据框缩减为四个变量(经度纬度月份,和犯罪类型)。

  4. 使用seaborn中的jointplot函数,为 2018 年 7 月、9 月和 12 月的自行车盗窃案件拟合并可视化三个核密度估计模型。

  5. 重复步骤 4;这次,使用 2018 年 8 月、10 月和 11 月的盗窃案件数据。

  6. 重复步骤 5;这次,使用 2018 年 7 月、10 月和 12 月的入室盗窃案件数据。

步骤 6 最后部分的输出如下:

图 9.29:2018 年 12 月入室盗窃案件的估计联合和边际密度

图 9.29:2018 年 12 月入室盗窃案件的估计联合和边际密度

再次澄清,活动中得到的密度应该已经叠加在地图上,以便我们能清楚地看到这些密度覆盖了哪些区域。如果你有合适的地图平台,建议尝试自己将结果叠加到地图上。如果没有,你可以访问在线地图服务,使用经纬度对来了解具体位置。

注意

本活动的解决方案可以在第 505 页找到。

摘要

核密度估计是一种经典的统计技术,属于与直方图相同类别的技术。它允许用户从样本数据中外推,以对特定对象或事件的总体做出洞察和预测。这种外推以概率密度函数的形式呈现,这很好,因为结果可以以可能性或概率的形式解读。该模型的质量依赖于两个参数:带宽值和核函数。如前所述,成功使用核密度估计的最关键部分是设置最优带宽。最优带宽通常通过使用网格搜索交叉验证和伪对数似然作为评分标准来确定。核密度估计的优点在于其简洁性和广泛的适用性。

核密度估计模型在犯罪学、流行病学、气象学和房地产等领域中非常常见,仅举几例。无论你从事哪个行业,核密度估计都应该是适用的。

在监督学习和无监督学习之间,无监督学习无疑是最少使用和最不被重视的学习类别。但情况不应该是这样的。监督学习技术是有限的,并且大多数可用数据并不适合回归和分类。扩展你的技能集,学习无监督学习技术,意味着你将能够利用不同的数据集,以更具创意的方式解决业务问题,甚至增强你现有的监督学习模型。本文并不详尽介绍所有无监督学习算法,但它是一个很好的起点,可以激发你的兴趣,并推动你继续学习。

附录

1. 聚类简介

活动 1.01:实现 k-means 聚类

解决方案:

  1. 导入所需的库:

    from sklearn.datasets import make_blobs
    from sklearn.cluster import KMeans
    from sklearn.metrics import accuracy_score, silhouette_score
    import matplotlib.pyplot as plt
    import pandas as pd
    import numpy as np
    from scipy.spatial.distance import cdist
    import math
    np.random.seed(0)
    %matplotlib inline
    
  2. 使用 pandas 加载种子数据文件:

    seeds = pd.read_csv('Seed_Data.csv')
    
  3. 返回数据集的前五行,如下所示:

    seeds.head()
    

    输出如下:

    图 1.25:显示数据集的前五行

    图 1.25:显示数据集的前五行

  4. 按如下方式分离 X 特征:

    X = seeds[['A','P','C','LK','WK','A_Coef','LKG']]
    y = seeds['target']
    
  5. 按如下方式检查特征:

    X.head()
    

    输出如下:

    图 1.26:打印特征

    图 1.26:打印特征

  6. 定义 k_means 函数,如下所示,并随机初始化 k-质心。使用 while 循环重复此过程,直到新旧 centroids 之间的差值为 0

    Activity 1.01.ipynb
    def k_means(X, K):
        # Keep track of history so you can see K-Means in action
        centroids_history = []
        labels_history = []
    
        # Randomly initialize Kcentroids
        rand_index = np.random.choice(X.shape[0], K)  
        centroids = X[rand_index]
        centroids_history.append(centroids)
    The complete code for this step can be found at https://packt.live/2JPZ4M8.
    
  7. 将 pandas DataFrame 转换为 NumPy 矩阵:

    X_mat = X.values
    
  8. 将我们的种子矩阵传递给之前创建的 k_means 函数:

    centroids, labels, centroids_history, labels_history = \
    k_means(X_mat, 3)
    
  9. 打印标签:

    print(labels)
    

    输出如下:

    图 1.27:打印标签

    图 1.27:打印标签

  10. 按如下方式绘制坐标:

    plt.scatter(X['A'], X['LK'])
    plt.title('Wheat Seeds - Area vs Length of Kernel')
    plt.show()
    plt.scatter(X['A'], X['LK'], c=labels, cmap='tab20b')
    plt.title('Wheat Seeds - Area vs Length of Kernel')
    plt.show()
    

    输出如下:

    图 1.28:绘制坐标

    图 1.28:绘制坐标

  11. 按如下方式计算轮廓系数:

    silhouette_score(X[['A','LK']], labels)
    

    输出如下:

    0.5875704550892767
    

通过完成此活动,您已经获得了在实际数据集上调优 k-means 聚类算法的实践经验。种子数据集被视为数据科学领域经典的“Hello World”类型问题,对于测试基础技术非常有帮助。

注意

要访问此特定部分的源代码,请参考 packt.live/2JPZ4M8

您还可以在 packt.live/2Ocncuh 上在线运行此示例。

2. 层次聚类

活动 2.01:将 k-means 与层次聚类进行比较

解决方案:

  1. 从 scikit-learn 导入必要的包(KMeansAgglomerativeClusteringsilhouette_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
    
  2. 将酒类数据集读取到 Pandas DataFrame 中并打印一个小样本:

    wine_df = pd.read_csv("wine_data.csv")
    print(wine_df.head())
    

    输出如下:

    图 2.25:酒类数据集的输出

    图 2.25:酒类数据集的输出

  3. 可视化酒类数据集以理解数据结构:

    plt.scatter(wine_df.values[:,0], wine_df.values[:,1])
    plt.title("Wine Dataset")
    plt.xlabel("OD Reading")
    plt.ylabel("Proline")
    plt.show()
    

    输出如下:

    图 2.26:原始酒类数据的绘图

    图 2.26:原始酒类数据的绘图

  4. 在酒类数据集上使用 sklearn 实现 k-means 聚类,已知有三种酒类:

    km = KMeans(3)
    km_clusters = km.fit_predict(wine_df)
    
  5. 使用 sklearn 实现层次聚类,应用于酒类数据集:

    ac = AgglomerativeClustering(3, linkage='average')
    ac_clusters = ac.fit_predict(wine_df)
    
  6. 按如下方式绘制 k-means 的预测聚类:

    plt.scatter(wine_df.values[:,0], \
                wine_df.values[:,1], c=km_clusters)
    plt.title("Wine Clusters from K-Means Clustering")
    plt.xlabel("OD Reading")
    plt.ylabel("Proline")
    plt.show()
    

    输出如下:

    图 2.27:k-means 聚类的聚类图

    图 2.27:k-means 聚类的聚类图

  7. 如下所示绘制层次聚类的预测聚类:

    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.28:从聚合聚类中绘制的聚类图

    图 2.28:聚合聚类的聚类图

    注意

    图 2.23图 2.24中,每种颜色代表一个单独的聚类。每次执行代码时,聚类的颜色都会发生变化。

  8. 比较每种聚类方法的轮廓分数:

    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))
    

    输出将如下所示:

    Silhouette Scores for Wine Dataset:
    K-Means Clustering:  0.5809421087616886
    Agg Clustering:  0.5988495817462
    

从前面的轮廓指标可以看出,在按平均簇内距离分离聚类时,聚合聚类稍微优于 k-means 聚类。然而,这并不是每个聚合聚类版本的情况。相反,尝试不同的连接类型,并检查每种类型下的轮廓分数和聚类的变化。

注意

要访问该特定部分的源代码,请参考packt.live/2AFA60Z

你也可以在线运行此示例,访问packt.live/3fe2lTi

3. 邻域方法和 DBSCAN

活动 3.01:从零实现 DBSCAN

解决方案:

  1. 生成一个随机的聚类数据集,如下所示:

    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)
    
  2. 可视化生成的数据:

    plt.scatter(X_blob[:,0], X_blob[:,1])
    plt.show()
    

    输出如下所示:

    图 3.15:生成的数据的绘图

    图 3.15:生成的数据的绘图

  3. 从头开始创建函数,允许你在数据集上调用 DBSCAN:

    Activity3.01.ipynb
    def scratch_DBSCAN(x, eps, min_pts): 
        """
        param x (list of vectors): your dataset to be clustered
        param eps (float): neighborhood radius threshold 
        param min_pts (int): minimum number of points threshold for 
        a neighborhood 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
    The complete code for this step can be found at https://packt.live/3c1rONO.
    
  4. 使用你创建的 DBSCAN 实现来查找生成数据集中的聚类。根据需要调整超参数,并根据它们在第 5 步中的表现进行调优:

    labels = scratch_DBSCAN(X_blob, 0.6, 5)
    
  5. 可视化你实现的 DBSCAN 聚类性能:

    plt.scatter(X_blob[:,0], X_blob[:,1], c=labels)
    plt.title("DBSCAN from Scratch Performance")
    plt.show()
    

    输出如下所示:

    图 3.16:DBSCAN 实现的绘图

图 3.16:DBSCAN 实现的绘图

在前面的输出中,你可以看到我们生成的数据中有四个明确定义的聚类。未突出显示的点超出了邻域范围,因此被视为噪声。尽管这可能不是理想的,因为不是每个点都被考虑在内,但对于大多数商业案例来说,这种噪声是可以接受的。如果在你的场景中不能接受,你可以调整提供的超参数,使得距离容忍度更高。

正如你可能已经注意到的,定制实现的运行时间较长。这是因为我们为了清晰起见,探索了非向量化版本的算法。在大多数情况下,你应该使用 scikit-learn 提供的 DBSCAN 实现,因为它经过高度优化。

注意

要访问该特定部分的源代码,请参考packt.live/3c1rONO

你也可以在线运行此示例,访问packt.live/2ZVoFuO

活动 3.02:将 DBSCAN 与 k-means 和层次聚类进行比较

解决方案:

  1. 导入必要的包:

    from sklearn.cluster \
    import KMeans, AgglomerativeClustering, DBSCAN
    from sklearn.metrics import silhouette_score
    import pandas as pd
    import matplotlib.pyplot as plt
    %matplotlib inline
    
  2. 第二章层次聚类中加载葡萄酒数据集,并再次熟悉数据的外观:

    # Load Wine data set
    wine_df = pd.read_csv("wine_data.csv")
    # Show sample of data set
    print(wine_df.head())
    

    输出如下所示:

    图 3.17:葡萄酒数据集的前五行

    图 3.17:葡萄酒数据集的前五行

  3. 可视化数据:

    plt.scatter(wine_df.values[:,0], wine_df.values[:,1])
    plt.title("Wine Dataset")
    plt.xlabel("OD Reading")
    plt.ylabel("Proline")
    plt.show()
    

    输出结果如下:

    图 3.18:数据图示

    图 3.18:数据图示

  4. 使用 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)
    
  5. 评估不同的 DSBSCAN 超参数选项及其对轮廓得分的影响:

    db_param_options = [[20,5],[25,5],[30,5],[25,7],[35,7],[40,5]]
    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.19:打印聚类的轮廓得分

    图 3.19:打印聚类的轮廓得分

  6. 根据最高的轮廓得分生成最终聚类(eps35min_samples3):

    # Generate clusters using DBSCAN
    db = DBSCAN(eps=40, min_samples = 5)
    db_clusters = db.fit_predict(wine_df)
    
  7. 可视化使用三种方法生成的聚类:

    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.20:使用不同算法绘制聚类图

    图 3.20:使用不同算法绘制聚类图

  8. 评估每种方法的轮廓得分:

    # 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))
    

    输出结果如下:

    Silhouette Scores for Wine Dataset:
    K-Means Clustering:  0.5809421087616886
    Agg Clustering:  0.5988495817462
    DBSCAN Clustering:  0.5739675293567901
    

如你所见,DBSCAN 并不是自动适合你聚类需求的最佳选择。与其他算法的一个关键区别在于它将噪声视为潜在的聚类。在某些情况下,这很有帮助,因为它能够去除异常值;然而,也可能会出现它调整得不够好,导致过多的点被分类为噪声的情况。你可以通过在拟合聚类算法时调整超参数进一步提高轮廓得分——尝试几种不同的组合,看看它们如何影响得分。

注意

要获取此特定部分的源代码,请参考 packt.live/2BNSQvC

你也可以在网上运行这个示例,链接地址为 packt.live/3iElboS

4. 降维技术与 PCA

活动 4.01:手动 PCA 与 scikit-learn

解决方案:

  1. 导入 pandasnumpymatplotlib 绘图库以及 scikit-learn 的 PCA 模型:

    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.decomposition import PCA
    
  2. 加载数据集,并按照之前的练习仅选择花萼特征。显示数据的前五行:

    df = pd.read_csv('../Seed_Data.csv')
    df = df[['A', 'LK']]
    df.head()
    

    输出结果如下:

    图 4.36:数据的前五行

    图 4.36:数据的前五行

  3. 计算数据的协方差矩阵:

    cov = np.cov(df.values.T)
    cov
    

    输出结果如下:

    array([[8.46635078, 1.22470367],
           [1.22470367, 0.19630525]])
    
  4. 使用 scikit-learn API 转换数据,并仅使用第一个主成分。将转换后的数据存储在 sklearn_pca 变量中:

    model = PCA(n_components=1)
    sklearn_pca = model.fit_transform(df.values)
    
  5. 使用手动 PCA 转换数据,并仅使用第一个主成分。将转换后的数据存储在 manual_pca 变量中:

    eigenvectors, eigenvalues, _ = \
    np.linalg.svd(cov, full_matrices=False)
    P = eigenvectors[0]
    manual_pca = P.dot(df.values.T)
    
  6. 在同一图上绘制 sklearn_pcamanual_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()
    plt.show()
    

    输出结果如下:

    图 4.37:数据图示

    图 4.37:数据图示

  7. 注意,两个图几乎一模一样,只是一个是另一个的镜像,且两者之间有偏移。显示 sklearn_pcamanual_pca 模型的组件:

    model.components_
    

    输出结果如下:

    array([[0.98965371, 0.14347657]])
    
  8. 现在,打印 P

    P
    

    输出结果如下:

    array([-0.98965371, -0.14347657])
    

    注意符号上的差异;数值是相同的,但符号不同,产生镜像结果。这只是约定上的差异,没有实际意义。

  9. 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()
    plt.show()
    

    输出如下:

    图 4.38:重新绘制的数据

    图 4.38:重新绘制的数据

  10. 现在,我们只需处理两者之间的偏移。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)
    
  11. 将结果乘以-1

    manual_pca *= -1
    
  12. 重新绘制各个sklearn_pcamanual_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()
    plt.show()
    

    输出如下:

    图 4.39:重新绘制数据

图 4.39:重新绘制数据

最终图将展示两种方法完成的降维实际上是相同的。差异体现在covariance矩阵符号上的不同,因为两种方法仅使用不同的特征作为基准进行比较。最后,两个数据集之间也存在偏移,这归因于在执行 scikit-learn PCA 转换之前已减去均值样本。

注意

要访问此特定部分的源代码,请参考packt.live/2O9MEk4

你也可以在线运行此示例,访问packt.live/3gBntTU

活动 4.02:使用扩展的种子数据集进行 PCA

解决方案:

  1. 导入pandasmatplotlib。为了启用 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
    
  2. 读取数据集并选择ALKC列:

    df = pd.read_csv('../Seed_Data.csv')[['A', 'LK', 'C']]
    df.head()
    

    输出如下:

    图 4.40:内核的面积、长度和紧凑性

    图 4.40:内核的面积、长度和紧凑性

  3. 在三维空间中绘制数据:

    fig = plt.figure(figsize=(10, 7))
    # Where Axes3D is required
    ax = fig.add_subplot(111, projection='3d')
    ax.scatter(df['A'], df['LK'], df['C'])
    ax.set_xlabel('Area of Kernel')
    ax.set_ylabel('Length of Kernel')
    ax.set_zlabel('Compactness of Kernel')
    ax.set_title('Expanded Seeds Dataset')
    plt.show()
    

    输出如下:

    图 4.41:扩展的种子数据集绘图

    图 4.41:扩展的种子数据集绘图

  4. 创建一个不指定主成分数量的PCA模型:

    model = PCA()
    
  5. 将模型拟合到数据集:

    model.fit(df.values)
    

    输出如下:

    PCA(copy=True, iterated_power='auto', n_components=None, 
        random_state=None,
        svd_solver='auto', tol=0.0, whiten=False)
    
  6. 显示特征值或explained_variance_ratio_

    model.explained_variance_ratio_
    

    输出如下:

    array([9.97794495e-01, 2.19418709e-03, 1.13183333e-05])
    

    我们希望减少数据集的维度,但仍保留至少 90%的方差。为了保留 90%的方差,最少需要多少个主成分?

    至少需要第一主成分以保留 90%以上的方差。第一主成分提供了数据集 99.7%的方差。

  7. 创建一个新的PCA模型,这次指定所需的主成分数量,以保留至少 90%的方差:

    model = PCA(n_components=1)
    
  8. 使用新模型转换数据:

    data_transformed = model.fit_transform(df.values)
    
  9. 将转换后的数据恢复到原始数据空间:

    data_restored = model.inverse_transform(data_transformed)
    
  10. 在一个子图中绘制恢复后的三维数据,在第二个子图中绘制原始数据,以可视化去除部分方差的效果:

    fig = plt.figure(figsize=(10, 14))
    # Original Data
    ax = fig.add_subplot(211, projection='3d')
    ax.scatter(df['A'], df['LK'], df['C'], label='Original Data');
    ax.set_xlabel('Area of Kernel');
    ax.set_ylabel('Length of Kernel');
    ax.set_zlabel('Compactness of Kernel');
    ax.set_title('Expanded Seeds 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('Area of Kernel');
    ax.set_ylabel('Length of Kernel');
    ax.set_zlabel('Compactness of Kernel');
    ax.set_title('Restored Seeds Dataset');
    

    输出如下:

    图 4.42:扩展和恢复后的 Seeds 数据集的绘图

图 4.42:扩展和恢复后的 Seeds 数据集的绘图

从前面的图表中可以看出,我们已经去除了数据中的大部分噪声,但保留了关于数据趋势的最重要信息。你可以看到,通常情况下,小麦粒的紧密度随着面积的增大而增加。

注意

在应用 PCA 时,必须考虑所建模数据的大小以及可用的系统内存。奇异值分解过程涉及将数据分离为特征值和特征向量,这可能会占用大量内存。如果数据集过大,可能会导致无法完成处理、性能显著下降或系统崩溃。

要访问此特定部分的源代码,请参考 packt.live/2ZVpaFc

你也可以在线运行这个示例,网址:packt.live/3gIrR3D

5. 自编码器

活动 5.01:MNIST 神经网络

解决方案:

在这个活动中,你将训练一个神经网络来识别 MNIST 数据集中的图像,并强化你在训练神经网络方面的技能:

  1. 导入 picklenumpymatplotlib 以及 Keras 中的 SequentialDense 类:

    import pickle
    import numpy as np
    import matplotlib.pyplot as plt
    from keras.models import Sequential
    from keras.layers import Dense
    import tensorflow.python.util.deprecation as deprecation
    deprecation._PRINT_DEPRECATION_WARNINGS = False
    
  2. 加载 mnist.pkl 文件,该文件包含来自 MNIST 数据集的前 10,000 张图像及其对应标签,这些数据可在随附的源代码中找到。MNIST 数据集是包含 0 到 9 手写数字的 28 x 28 灰度图像系列。提取图像和标签:

    with open('mnist.pkl', 'rb') as f:
        data = pickle.load(f)
    images = data['images']
    labels = data['labels']
    
  3. 绘制前 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.40:前 10 个样本

    图 5.40:前 10 个样本

  4. 使用独热编码对标签进行编码:

    one_hot_labels = np.zeros((images.shape[0], 10))
    for idx, label in enumerate(labels):
        one_hot_labels[idx, label] = 1
    one_hot_labels
    

    输出结果如下:

    array([[0., 0., 0., ..., 0., 0., 0.],
           [1., 0., 0., ..., 0., 0., 0.],
           [0., 0., 0., ..., 0., 0., 0.],
           ...,
           [0., 0., 0., ..., 0., 0., 0.],
           [0., 0., 0., ..., 0., 0., 1.],
           [0., 0., 0., ..., 1., 0., 0.]])
    
  5. 准备好输入神经网络的图像。作为提示,这个过程分为两个独立的步骤:

    images = images.reshape((-1, 28 ** 2))
    images = images / 255.
    
  6. 在 Keras 中构建一个神经网络模型,接受已准备好的图像,具有 600 个单元的隐藏层并使用 ReLU 激活函数,输出层的单元数与类别数相同。输出层使用 softmax 激活函数:

    model = Sequential([Dense(600, input_shape=(784,), \
                        activation='relu'), \
                        Dense(10, activation='softmax'),])
    
  7. 使用多类交叉熵、随机梯度下降和准确率性能指标来编译模型:

    model.compile(loss='categorical_crossentropy', \
                  optimizer='sgd', metrics=['accuracy'])
    
  8. 训练模型。需要多少个 epoch 才能在训练数据上达到至少 95% 的分类准确率?我们来看看:

    model.fit(images, one_hot_labels, epochs=20)
    

    输出结果如下:

    图 5.41:训练模型

图 5.41:训练模型

需要 15 个 epoch 才能在训练集上达到至少 95% 的分类准确率。

在这个示例中,我们使用分类器训练的数据来衡量神经网络分类器的性能。通常情况下,不应该使用这种方法,因为它通常报告比你期望的模型准确度更高。在监督学习问题中,应使用若干 交叉验证 技术。由于这是一本关于无监督学习的课程,交叉验证超出了本书的范围。

注意

要访问此特定部分的源代码,请参考 packt.live/2VZpLnZ

你也可以在网上运行这个示例,访问 packt.live/2Z9ueGz

活动 5.02:简单的 MNIST 自动编码器

解决方案:

  1. 导入 picklenumpymatplotlib,以及 Keras 中的 ModelInputDense 类:

    import pickle
    import numpy as np
    import matplotlib.pyplot as plt
    from keras.models import Model
    from keras.layers import Input, Dense
    import tensorflow.python.util.deprecation as deprecation
    deprecation._PRINT_DEPRECATION_WARNINGS = False
    
  2. 从提供的 MNIST 数据集样本中加载图像,样本随附有源代码 (mnist.pkl):

    with open('mnist.pkl', 'rb') as f:
        images = pickle.load(f)['images']
    
  3. 准备将图像输入到神经网络中。作为提示,这个过程有 两个 独立的步骤:

    images = images.reshape((-1, 28 ** 2))
    images = images / 255.
    
  4. 构建一个简单的自动编码器网络,将图像大小在编码阶段压缩至 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)
    
  5. 使用二元交叉熵损失函数和 adadelta 梯度下降法编译自动编码器:

    autoencoder.compile(loss='binary_crossentropy', \
                        optimizer='adadelta')
    
  6. 拟合编码器模型:

    autoencoder.fit(images, images, epochs=100)
    

    输出如下:

    图 5.42:训练模型

    图 5.42:训练模型

  7. 计算并存储编码阶段前五个样本的输出:

    encoder_output = Model(input_stage, encoding_stage)\
                     .predict(images[:5])
    
  8. 将编码器的输出重塑为 10 x 10(10 x 10 = 100)像素,并乘以 255:

    encoder_output = encoder_output.reshape((-1, 10, 10)) * 255
    
  9. 计算并存储解码阶段前五个样本的输出:

    decoder_output = autoencoder.predict(images[:5])
    
  10. 将解码器的输出重塑为 28 x 28,并乘以 255:

    decoder_output = decoder_output.reshape((-1, 28, 28)) * 255
    
  11. 绘制原始图像、编码器输出和解码器:

    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.43:原始图像、编码器输出和解码器

图 5.43:原始图像、编码器输出和解码器

到目前为止,我们已经展示了如何使用编码和解码阶段的简单单隐层将数据降维。我们还可以通过向编码和解码阶段添加额外的层,使该模型更加复杂。

注意

要访问此特定部分的源代码,请参考 packt.live/3f5ZSdH

你也可以在网上运行这个示例,访问 packt.live/2W0ZkhP

活动 5.03:MNIST 卷积自动编码器

解决方案:

  1. 导入 picklenumpymatplotlibkeras.models 中的 Model 类,导入 keras.layers 中的 InputConv2DMaxPooling2DUpSampling2D

    import pickle
    import numpy as np
    import matplotlib.pyplot as plt
    from keras.models import Model
    from keras.layers \
    import Input, Conv2D, MaxPooling2D, UpSampling2D
    import tensorflow.python.util.deprecation as deprecation
    deprecation._PRINT_DEPRECATION_WARNINGS = False
    
  2. 加载数据:

    with open('mnist.pkl', 'rb') as f:
        images = pickle.load(f)['images']
    
  3. 将图像缩放至 0 到 1 之间:

    images = images / 255.
    
  4. 我们需要重塑图像,以添加一个深度通道,以便用于卷积阶段。将图像重塑为 28 x 28 x 1:

    images = images.reshape((-1, 28, 28, 1))
    
  5. 定义输入层。我们将使用与图像相同形状的输入:

    input_layer = Input(shape=(28, 28, 1,))
    
  6. 添加一个具有 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)
    
  7. 为编码器添加一个最大池化层,使用 2 x 2 的卷积核:

    encoded = MaxPooling2D((2, 2))(hidden_encoding)
    
  8. 添加解码卷积层:

    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)
    
  9. 添加上采样层:

    upsample_decoding = UpSampling2D((2, 2))(hidden_decoding)
    
  10. 添加最后一个卷积阶段,按照初始图像的深度使用一层:

    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)
    
  11. 通过将网络的第一层和最后一层传递给 Model 类来构建模型:

    autoencoder = Model(input_layer, decoded)
    
  12. 显示模型结构:

    autoencoder.summary()
    

    输出如下:

    图 5.44: 模型结构

    图 5.44: 模型结构

  13. 使用二元交叉熵损失函数和 adadelta 梯度下降法编译自编码器:

    autoencoder.compile(loss='binary_crossentropy', \
                        optimizer='adadelta')
    
  14. 现在,让我们拟合模型;再次,将图像作为训练数据和期望的输出。由于卷积网络的计算时间较长,因此训练 20 个 epoch:

    autoencoder.fit(images, images, epochs=20)
    

    输出如下:

    图 5.45: 训练模型

    图 5.45: 训练模型

  15. 计算并存储前五个样本的编码阶段输出:

    encoder_output = Model(input_layer, encoded).predict(images[:5])
    
  16. 为了可视化,重塑编码器输出,每个图像的大小为 X*Y:

    encoder_output = encoder_output.reshape((-1, 14 * 14, 16))
    
  17. 获取解码器对前五个图像的输出:

    decoder_output = autoencoder.predict(images[:5])
    
  18. 将解码器输出重塑为 28 x 28 的大小:

    decoder_output = decoder_output.reshape((-1, 28, 28))
    
  19. 将原始图像重塑回 28 x 28 的大小:

    images = images.reshape((-1, 28, 28))
    
  20. 绘制原始图像、平均编码器输出和解码器:

    plt.figure(figsize=(10, 7))
    for i in range(5):
        # Plot the original digit images
        plt.subplot(3, 5, i + 1)
        plt.imshow(images[i], cmap='gray')
        plt.axis('off')
        # Plot the encoder output
        plt.subplot(3, 5, i + 6)
        plt.imshow(encoder_output[i], cmap='gray')
        plt.axis('off')
        # Plot the decoder output
        plt.subplot(3, 5, i + 11)
        plt.imshow(decoder_output[i], cmap='gray')
        plt.axis('off')
    

    输出如下:

    图 5.46: 原始图像、编码器输出和解码器

图 5.46: 原始图像、编码器输出和解码器

在本次活动结束时,您将开发一个包含卷积层的自编码器神经网络。注意解码器表示的改进。与全连接神经网络层相比,这种架构在性能上有显著优势,并且在处理基于图像的数据集和生成人工数据样本时极为有用。

注意

要访问此特定部分的源代码,请参见 packt.live/2CdpIxY

您还可以在在线运行此示例 packt.live/3iKz8l2

6. t-分布随机邻域嵌入

活动 6.01: 葡萄酒 t-SNE

解决方案:

  1. 导入 pandasnumpymatplotlib,以及来自 scikit-learn 的 t-SNEPCA 模型:

    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.decomposition import PCA
    from sklearn.manifold import TSNE
    
  2. 使用随附源代码中的 wine.data 文件加载葡萄酒数据集,并显示数据的前五行:

    df = pd.read_csv('wine.data', header=None)
    df.head()
    

    输出如下:

    图 6.25: 葡萄酒数据集的前五行

    图 6.25: 葡萄酒数据集的前五行

  3. 第一列包含标签;提取该列并将其从数据集中移除:

    labels = df[0]
    del df[0]
    
  4. 执行 PCA,将数据集降到前六个主成分:

    model_pca = PCA(n_components=6)
    wine_pca = model_pca.fit_transform(df)
    
  5. 确定这六个成分描述的数据中的方差量:

    np.sum(model_pca.explained_variance_ratio_)
    

    输出如下:

    0.99999314824536
    
  6. 创建一个使用指定随机状态并设置verbose值为 1 的 t-SNE 模型:

    tsne_model = TSNE(random_state=0, verbose=1)
    tsne_model
    

    输出如下:

    图 6.26:创建 t-SNE 模型

    图 6.26:创建 t-SNE 模型

  7. 将 PCA 数据拟合到 t-SNE 模型:

    wine_tsne = tsne_model.fit_transform\
                (wine_pca.reshape((len(wine_pca), -1)))
    

    输出如下:

    图 6.27:将 PCA 数据拟合到 t-SNE 模型

    图 6.27:将 PCA 数据拟合到 t-SNE 模型

  8. 确认 t-SNE 拟合数据的形状是二维的:

    wine_tsne.shape
    

    输出如下:

    (178, 2)
    
  9. 创建二维数据的散点图:

    plt.figure(figsize=(10, 7))
    plt.scatter(wine_tsne[:,0], wine_tsne[:,1])
    plt.title('Low Dimensional Representation of Wine')
    plt.show()
    

    输出如下:

    图 6.28:二维数据的散点图

    图 6.28:二维数据的散点图

  10. 创建带有类别标签的二维数据二次散点图,以可视化可能存在的聚类:

    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.29:二维数据的二次图

图 6.29:二维数据的二次图

请注意,尽管不同种类的葡萄酒存在重叠,但也可以看到数据中存在一些聚类。第一种葡萄酒类别主要位于图表的左上角,第二种葡萄酒类别位于右下角,而第三种葡萄酒类别位于前两者之间。这种表示方式肯定不能用来高信心地分类单个葡萄酒样本,但它展示了一个总体趋势以及我们之前无法看到的高维数据中的一系列聚类。

本节中,我们介绍了生成 SNE 图的基础知识。将高维数据表示为低维空间的能力至关重要,尤其是为了更全面地理解手头的数据。有时,这些图表可能很难解读,因为其中的确切关系有时是矛盾的,有时会导致误导性的结构。

注意

要访问此特定部分的源代码,请参考packt.live/2ZSVKrf

你也可以在线运行这个例子,访问packt.live/2CgAWBE

活动 6.02:t-SNE 葡萄酒与困惑度

解决方案:

  1. 导入pandasnumpymatplotlib,以及从 scikit-learn 导入t-SNEPCA模型:

    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.decomposition import PCA
    from sklearn.manifold import TSNE
    
  2. 加载葡萄酒数据集并检查前五行:

    df = pd.read_csv('wine.data', header=None)
    df.head()
    

    输出如下:

    图 6.30:葡萄酒数据集的前五行

    图 6.30:葡萄酒数据集的前五行

  3. 第一列提供了标签;从 DataFrame 中提取它们并存储到单独的变量中。确保将该列从 DataFrame 中删除:

    labels = df[0]
    del df[0]
    
  4. 对数据集执行 PCA 并提取前六个成分:

    model_pca = PCA(n_components=6)
    wine_pca = model_pca.fit_transform(df)
    wine_pca = wine_pca.reshape((len(wine_pca), -1))
    
  5. 构建一个循环,遍历困惑度值(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()
    plt.show()
    

    困惑度值为 1 无法将数据分离成任何特定结构:

    ![图 6.31:困惑度为 1 的图]

    ](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_31.jpg)

图 6.31:困惑度为 1 的图

增加困惑度到 5 会导致非常非线性的结构,难以分离,并且很难识别任何簇或模式:

![图 6.32:困惑度为 5 的图]

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_32.jpg)

图 6.32:困惑度为 5 的图

困惑度为 20 时,最终开始显示某种马蹄形结构。虽然在视觉上很明显,但实现起来仍然很棘手:

![图 6.33:困惑度为 20 的图]

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_33.jpg)

图 6.33:困惑度为 20 的图

困惑度值为 30 显示了相当不错的结果。投影结构之间存在一定的线性关系,且葡萄酒类型之间有一定的分隔:

![图 6.34:困惑度为 30 的图]

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_34.jpg)

图 6.34:困惑度为 30 的图

最后,活动中的最后两张图展示了随着困惑度(perplexity)增加,图表变得越来越复杂和非线性的程度:

![图 6.35:困惑度为 80 的图]

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_35.jpg)

图 6.35:困惑度为 80 的图

这是困惑度值为 160 时的图:

![图 6.36:困惑度为 160 的图]

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_36.jpg)

图 6.36:困惑度为 160 的图

最后,这是困惑度值为 320 时的图:

![图 6.37:困惑度为 320 的图]

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_37.jpg)

图 6.37:困惑度为 320 的图

通过查看每个困惑度值的单独图表,困惑度对数据可视化的影响立刻显而易见。非常小或非常大的困惑度值会产生一系列不寻常的形状,这些形状无法表示任何持久的模式。最合适的值似乎是 30(图 6.35),它产生了最线性的图表。

在这个活动中,我们展示了在选择困惑度时需要小心,并且可能需要一些迭代才能确定正确的值。

注意

要访问此特定部分的源代码,请参阅 packt.live/3faqESn

你还可以在线运行此示例,网址为 packt.live/2AF12Oi

活动 6.03:t-SNE 葡萄酒和迭代

解决方案:

  1. 导入 pandasnumpymatplotlib,以及来自 scikit-learn 的 t-SNEPCA 模型:

    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.decomposition import PCA
    from sklearn.manifold import TSNE
    
  2. 加载葡萄酒数据集并检查前五行:

    df = pd.read_csv('wine.data', header=None)
    df.head()
    

    输出如下:

    ![图 6.38:葡萄酒数据集的前五行]

    ](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_38.jpg)

    图 6.38:葡萄酒数据集的前五行

  3. 第一列提供标签;从 DataFrame 中提取这些标签并将它们存储在一个单独的变量中。确保从 DataFrame 中删除该列:

    labels = df[0]
    del df[0]
    
  4. 对数据集执行 PCA 并提取前六个组件:

    model_pca = PCA(n_components=6)
    wine_pca = model_pca.fit_transform(df)
    wine_pca = wine_pca.reshape((len(wine_pca), -1))
    
  5. 构建一个循环,遍历迭代值(2505001000)。对于每个循环,生成具有相应迭代次数和相同进展值但不同迭代次数的 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)
        wine_tsne = model_tsne.fit_transform(wine_pca)
    
  6. 绘制标记的葡萄酒类别散点图。注意不同迭代值的影响:

        plt.figure(figsize=(10, 7))
        plt.title(f'Low Dimensional Representation of Wine \
    (iterations = {iterations})')
        for i in range(10):
            selections = wine_tsne[labels == i]
            plt.scatter(selections[:,0], selections[:,1], \
                        alpha=0.7, marker=MARKER[i], s=10);
            x, y = selections.mean(axis=0)
            plt.text(x, y, str(i), \
                     fontdict={'weight': 'bold', 'size': 30})
    plt.show()
    

    输出如下:

    图 6.39:带有 250 次迭代的葡萄酒类别散点图

图 6.39:带有 250 次迭代的葡萄酒类别散点图

这是 500 次迭代的情况图:

图 6.40:带有 500 次迭代的葡萄酒类别散点图

图 6.40:带有 500 次迭代的葡萄酒类别散点图

这是 1,000 次迭代的情况图:

图 6.41:带有 1,000 次迭代的葡萄酒类别散点图

图 6.41:带有 1,000 次迭代的葡萄酒类别散点图

同样,我们可以看到随着迭代次数的增加,数据结构得到了改善。即使在这样一个相对简单的数据集中,250 次迭代也不足以将数据结构投射到低维空间中。

正如我们在本活动中观察到的,设置迭代参数是一个平衡点。在本例中,250 次迭代不足,至少需要 1,000 次迭代才能稳定数据。

注意

要访问此特定部分的源代码,请参阅packt.live/2ZOJuYv

您还可以在packt.live/2Z8wEoP上线上运行此示例。

7. 主题建模

活动 7.01:加载和清洗 Twitter 数据

解决方案:

  1. 导入必要的库:

    import warnings
    warnings.filterwarnings('ignore')
    import langdetect 
    import matplotlib.pyplot 
    import nltk
    nltk.download('wordnet')
    nltk.download('stopwords')
    import numpy 
    import pandas 
    import pyLDAvis 
    import pyLDAvis.sklearn 
    import regex 
    import sklearn 
    
  2. packt.live/2Xje5xF加载 LA Times 健康 Twitter 数据(latimeshealth.txt)。

    path = 'latimeshealth.txt' 
    df = pandas.read_csv(path, sep="|", header=None)
    df.columns = ["id", "datetime", "tweettext"]
    
  3. 运行快速的探索性分析来确定数据的大小和结构:

    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.49:数据形状、列名和数据头

    图 7.49:数据形状、列名和数据头

  4. 提取推文文本并将其转换为列表对象:

    raw = df['tweettext'].tolist() 
    print("HEADLINES:\n{lines}\n".format(lines=raw[:5])) 
    print("LENGTH:\n{length}\n".format(length=len(raw))) 
    

    输出如下:

    图 7.50:标题及其长度

    图 7.50:标题及其长度

  5. 编写一个函数,在空格上执行语言检测和分词,然后将屏幕名称和网址分别替换为SCREENNAMEURL。该函数还应删除标点符号、数字以及SCREENNAMEURL的替换内容。将所有内容转换为小写,除了SCREENNAMEURL之外。它应该移除所有停用词,执行词形归并,并仅保留五个或更多字母的单词。

    Activity7.01-Activity7.03.ipynb
    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)
    The complete code for this step can be found at https://packt.live/3e3VifV.
    
  6. 将第 5 步定义的函数应用于每条推文:

    clean = list(map(do_tweet_cleaning, raw)) 
    
  7. 删除输出列表中等于 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.51: 移除 None 后的标题和长度

    ](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_51.jpg)

    图 7.51: 移除 None 后的标题和长度

  8. 将每条推文的元素重新转换为字符串。使用空格连接:

    clean_sentences = [" ".join(i) for i in clean]
    print(clean_sentences[0:10])
    

    输出列表的前 10 个元素应类似于以下内容:

    图 7.52: 已清理用于建模的推文

    ](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_22.jpg)

图 7.52: 已清理用于建模的推文

保持笔记本打开,便于未来的活动。完成此活动后,你现在应该能够相当自如地处理文本数据并为主题建模做好准备。一个重要的提示是,要注意练习和活动之间在数据清理需求上的细微差别。建模不是一成不变的过程,如果你在开始建模工作前花足够的时间探索数据,这一点会非常明显。

注意

要访问此特定部分的源代码,请参考packt.live/3e3VifV

你也可以在packt.live/3fegXlU在线运行这个示例。你必须执行整个笔记本,才能得到期望的结果。

活动 7.02: LDA 和健康推文

解决方案:

  1. 指定 number_wordsnumber_docsnumber_features 变量:

    number_words = 10
    number_docs = 10
    number_features = 1000
    
  2. 创建一个词袋模型并将特征名称赋值给另一个变量,以便后续使用:

    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 
    
  3. 确定最优主题数量:

    Activity7.01-Activity7.03.ipynb
    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))
    The complete code for this step can be found at https://packt.live/3e3VifV.
    

    输出结果如下:

    图 7.53: 主题数量与困惑度分数的数据框

    ](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_53.jpg)

    图 7.53: 主题数量与困惑度分数的数据框

  4. 使用最优主题数量拟合 LDA 模型:

    lda = sklearn.decomposition.LatentDirichletAllocation\
          (n_components=optimal_num_topics, \
           learning_method="online", \
           random_state=0)
    lda.fit(clean_vec1) 
    

    输出结果如下:

    图 7.54: LDA 模型

    ](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_54.jpg)

    图 7.54: LDA 模型

  5. 创建并打印词汇-主题表:

    Activity7.01-Activity7.03.ipynb
    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 = {} 
    The complete code for this step can be found at https://packt.live/3e3VifV.
    

    输出结果如下:

    图 7.55: 健康推文数据的词汇-主题表

    ](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_55.jpg)

    图 7.55: 健康推文数据的词汇-主题表

    注意

    由于支持 LDA 和 NMF 的优化算法,结果可能与所示略有不同。许多函数没有设置种子值的功能。

  6. 打印文档-主题表:

    print(H_df)
    

    输出结果如下:

    图 7.56: 文档主题表

    ](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_56.jpg)

    图 7.56: 文档主题表

  7. 创建一个双变量图可视化:

    lda_plot = pyLDAvis.sklearn.prepare(lda, clean_vec1, \
                                        vectorizer1, R=10)
    pyLDAvis.display(lda_plot)
    

    输出结果如下:

    图 7.57: LDA 模型对健康推文的直方图和双变量图

    ](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_39.jpg)

    图 7.57: LDA 模型对健康推文的直方图和双变量图

  8. 保持笔记本打开,以便进行未来的建模。

在讨论下一个主题建模方法之前——非负矩阵分解,我们先通过另一个词袋建模方法。你还记得 CountVectorizer 算法是如何返回每个词在每个文档中出现次数的简单计数的吧?在这个新方法中,称为 TF-IDF(词频-逆文档频率),返回的是代表每个词在每个文档中重要性的权重,而不是原始的计数。

CountVectorizerTfidfVectorizer 两种方法同样有效。何时以及如何使用它们,取决于语料库、使用的主题建模方法以及文档中的噪声量。在接下来的练习中,我们将使用 TfidfVectorizer,并利用输出构建本章稍后出现的 NMF 模型。

注意

要访问该特定部分的源代码,请参考 packt.live/3e3VifV

您还可以在 packt.live/3fegXlU 在线运行此示例。您必须执行整个 Notebook 才能获得期望的结果。

活动 7.03:非负矩阵分解

解决方案:

  1. 创建适当的词袋模型,并将特征名称作为另一个变量输出:

    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() 
    
  2. 使用 活动 7.02 中的主题数(n_components)值来定义并拟合 NMF 算法,LDA 和健康推文

    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.58:定义 NMF 模型

    图 7.58:定义 NMF 模型

  3. 获取主题-文档表和词-主题表。花几分钟探索词汇分组,并尝试定义抽象的主题:

    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.59:词-主题表

    print(H_df)
    

    输出如下:

    图 7.60:带有概率的主题-文档表

    图 7.60:带有概率的主题-文档表

  4. 调整模型参数,并重新运行 步骤 3步骤 4

在这个活动中,我们使用 TF-IDF 词袋模型和非负矩阵分解进行了一个主题建模示例。这里真正重要的是理解这些算法在做什么——而不仅仅是如何拟合它们——并理解结果。处理文本数据通常是复杂的,必须认识到并非每个算法每次都会返回有意义的结果。有时,结果根本就没有用。这不是对算法或从业者的反映,而是从数据中提取洞察的挑战之一。

注意

要访问该特定部分的源代码,请参考 packt.live/3e3VifV

您还可以在 packt.live/3fegXlU 在线运行此示例。您必须执行整个 Notebook 才能获得期望的结果。

8. 市场篮分析

活动 8.01:加载和准备完整的在线零售数据

解决方案:

  1. 导入所需的库:

    import matplotlib.pyplot as plt
    import mlxtend.frequent_patterns
    import mlxtend.preprocessing
    import numpy
    import pandas
    
  2. 加载在线零售数据集文件:

    online = pandas.read_excel(io="./Online Retail.xlsx", \
                               sheet_name="Online Retail", \
                               header=0)
    
  3. 清理并准备数据用于建模,包括将清理后的数据转换为列表的列表:

    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)
    
  4. 对数据进行编码并将其重新构建为数据框。由于数据量相当大,因此为了确保一切顺利执行,请使用至少具有 8 GB 内存的计算机:

    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.34:从完整的在线零售数据集构建的清理、编码和重构后的数据框的一个子集

图 8.34:从完整的在线零售数据集构建的清理、编码和重构后的数据框的一个子集

注:

要访问此特定部分的源代码,请参阅 packt.live/2Wf2Rcz

本节目前没有在线交互示例,需要在本地运行。

活动 8.02:在完整在线零售数据集上运行 Apriori 算法

解决方案:

  1. 在完整数据上运行 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.35:使用完整在线零售数据集的 Apriori 算法结果

    图 8.35:使用完整在线零售数据集的 Apriori 算法结果

  2. 将结果过滤到包含 10 COLOUR SPACEBOY PEN 的项集。将支持度值与 练习 8.06 中的结果进行比较,执行 Apriori 算法

    mod_colnames_minsupport[mod_colnames_minsupport['itemsets'] \
    == frozenset({'10 COLOUR SPACEBOY PEN'})]
    

    输出结果如下:

    图 8.36:包含 10 COLOUR SPACEBOY PEN 的项集结果

    图 8.36:包含 10 COLOUR SPACEBOY PEN 的项集结果

    支持度值确实发生了变化。当数据集扩展到包含所有交易时,该项集的支持度从 0.0178 降到 0.015793。也就是说,在用于练习的简化数据集里,该项集出现在 1.78% 的交易中,而在完整数据集中,它出现在大约 1.6% 的交易中。

  3. 添加另一列,包含项集的长度。然后,过滤掉那些长度为 2 且支持度在 [0.02, 0.021] 范围内的项集。与 练习 8.06 中的项集相同吗?执行 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.37:基于长度和支持度过滤的结果

    图 8.37:基于长度和支持度过滤的结果

    结果确实发生了变化。在查看特定项集及其支持度值之前,我们发现这个过滤后的数据框比前一个练习中的数据框少了项集。当我们使用完整数据集时,符合过滤条件的项集更少;也就是说,只有 17 个项集包含 2 个项目,并且支持度值大于或等于 0.02,小于 0.021。在上一个练习中,有 32 个项集符合这些标准。

  4. 绘制 support 值:

    mod_colnames_minsupport.hist("support", grid=False, bins=30)
    plt.xlabel("Support of item")
    plt.ylabel("Number of items")
    plt.title("Frequency distribution of Support")
    plt.show()
    

    输出结果如下:

    图 8.38:支持度值的分布

图 8.38:支持度值的分布

该图显示了完整交易数据集的支持度分布。如你所料,分布呈右偏;即,大多数项集的支持度较低,并且在分布的高端有一长尾。鉴于存在如此多的独特项集,单一项集出现在大多数交易中的比例并不高也不足为奇。有了这些信息,我们可以告诉管理层,即使是最显著的项集,也仅出现在约 10% 的交易中,而绝大多数项集出现在不到 2% 的交易中。这些结果可能无法支持店面布局的调整,但却可以为定价和折扣策略提供很好的参考。通过形式化一系列关联规则,我们可以获得更多关于如何制定这些策略的信息。

注意

要访问该特定部分的源代码,请参见 packt.live/2Wf2Rcz

目前本节没有在线互动示例,需在本地运行。

活动 8.03:在完整在线零售数据集上寻找关联规则

解决方案:

  1. 在完整数据集上拟合关联规则模型。使用置信度度量和最小阈值 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.39:基于完整在线零售数据集的关联规则

    图 8.39:基于完整在线零售数据集的关联规则

  2. 计算关联规则的数量。这个数字与 练习 8.07推导关联规则步骤 1 中的结果有区别吗?

    print("Number of Associations: {}".format(rules.shape[0]))
    

    498 条关联规则。是的,数量有所不同。

  3. 绘制置信度与支持度的关系图:

    rules.plot.scatter("support", "confidence", \
                       alpha=0.5, marker="*")
    plt.xlabel("Support")
    plt.ylabel("Confidence")
    plt.title("Association Rules")
    plt.show()
    

    输出如下:

    图 8.40:置信度与支持度的关系图

    图 8.40:置信度与支持度的关系图

    该图揭示了该数据集中一些关联规则具有较高的支持度和置信度值。

  4. 查看提升值、杠杆值和信念值的分布:

    rules.hist("lift", grid=False, bins=30)
    plt.xlabel("Lift of item")
    plt.ylabel("Number of items")
    plt.title("Frequency distribution of Lift")
    plt.show()
    

    输出如下:

    图 8.41:提升值的分布

图 8.41:提升值的分布

绘制杠杆值,如下所示:

rules.hist("leverage", grid=False, bins=30)
plt.xlabel("Leverage of item")
plt.ylabel("Number of items")
plt.title("Frequency distribution of Leverage")
plt.show()

输出如下:

图 8.42:杠杆值的分布

图 8.42:杠杆值的分布

绘制信念值,如下所示:

plt.hist(rules[numpy.isfinite(rules['conviction'])]\
         .conviction.values, bins = 3)
plt.xlabel("Conviction of item")
plt.ylabel("Number of items")
plt.title("Frequency distribution of Conviction")
plt.show()

输出如下:

图 8.43:信念值的分布

图 8.43:信念值的分布

在推导出关联规则后,我们可以为管理层提供附加信息,其中最重要的是大约有七个项目集在支持度和置信度方面都有合理的较高值。查看置信度与支持度的散点图,看看这七个项目集与其他所有项目集是如何分开的。这七个项目集也有较高的提升值,如提升直方图所示。看来我们已经识别出了一些可操作的关联规则——这些规则可以用来推动商业决策。

注意

若要访问此特定部分的源代码,请参阅packt.live/2Wf2Rcz

本节目前没有在线交互式示例,需要在本地运行。

9. 热点分析

活动 9.01:一维密度估计

解决方案:

  1. 打开一个新的笔记本并安装所有必要的库。

    get_ipython().run_line_magic('matplotlib', 'inline')
    import matplotlib.pyplot as plt
    import numpy
    import pandas
    import seaborn
    import sklearn.model_selection
    import sklearn.neighbors
    seaborn.set()
    
  2. 从标准正态分布中抽取 1,000 个数据点。将 3.5 加到样本中最后 625 个值(即索引在 375 到 1,000 之间的值)。使用numpy.random.RandomState设置随机状态为 100,以保证采样值一致,然后使用rand.randn(1000)调用随机生成数据点:

    rand = numpy.random.RandomState(100)
    vals = rand.randn(1000)  # standard normal
    vals[375:] += 3.5
    
  3. 将 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')
    plt.show()
    

    输出如下:

    图 9.30:带有散点图的随机样本直方图

    图 9.30:带有散点图的随机样本直方图

  4. 定义一组带宽值的网格。然后,定义并拟合一个网格搜索交叉验证算法:

    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])
    

    输出如下:

    图 9.31:交叉验证模型的输出

    图 9.31:交叉验证模型的输出

  5. 提取最优带宽值:

    best_bandwidth = grid.best_params_["bandwidth"]
    print("Best Bandwidth Value: {}".format(best_bandwidth))
    

    最优带宽值大约为0.4

  6. 重新绘制第 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')
    plt.show()
    

    输出如下:

    图 9.32:带有最优估计密度的随机样本直方图

图 9.32:带有最优估计密度的随机样本直方图

注意

若要访问此特定部分的源代码,请参阅packt.live/2wmh5yj

您也可以在网上运行此示例,网址为packt.live/2W0EAGK

活动 9.02:伦敦犯罪分析

解决方案:

  1. 加载犯罪数据。使用保存下载目录的路径,创建一个年份-月份标签的列表,使用read_csv命令迭代加载各个文件,然后将这些文件合并在一起:

    # define the file base path
    base_path = "./metro-jul18-dec18/{yr_mon}/{yr_mon}\
    -metropolitan-street.csv"
    print(base_path)
    

    输出如下:

    ./metro-jul18-dec18/{yr_mon}/{yr_mon}-metropolitan-street.csv
    
  2. 定义年份和月份组合的列表如下:

    yearmon_list = ["2018-0" + str(i) if i <= 9 else "2018-" + str(i) \
                    for i in range(7, 13)]
    print(yearmon_list)
    

    输出如下:

    ['2018-07', '2018-08', '2018-09', \
     '2018-10', '2018-11', '2018-12']
    
  3. 加载数据并打印一些基本信息,如下所示:

    data_yearmon_list = []
    # read each year month file individually
    #print summary statistics
    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)))
    # concatenate the list of year month data frames together
    london = pandas.concat(data_yearmon_list)
    

    输出如下:

    图 9.33:其中一个单独犯罪文件的示例

    图 9.33:单个犯罪文件的示例

    打印出的信息仅适用于加载的第一个文件,即 2018 年 7 月来自大都会警察局的犯罪数据。该文件包含近 10 万个条目。你会注意到,这个数据集中有大量有趣的信息,但我们将重点关注Longitude(经度)、Latitude(纬度)、Month(月份)和Crime type(犯罪类型)。

  4. 打印完整且合并的数据集的诊断信息:

    Activity9.01-Activity9.02.ipynb
    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()))
    The complete code for this step can be found at https://packt.live/2wmh5yj.
    

    输出结果如下:

    图 9.34:完整犯罪数据集的描述符

    图 9.34:完整犯罪数据集的描述符

  5. 将数据框缩小为四个变量(LongitudeLatitudeMonthCrime type):

    london_subset = london[["Month", "Longitude", "Latitude", \
                            "Crime type"]]
    london_subset.head(5)
    

    输出结果如下:

    图 9.35:以数据框格式展示的犯罪数据

    图 9.35:以数据框格式展示的犯罪数据

  6. 使用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.36: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.37: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.38:2018 年 12 月自行车盗窃案件的联合与边际密度估计

    图 9.38:2018 年 12 月自行车盗窃案件的联合与边际密度估计

    从一个月到另一个月,自行车盗窃的密度保持相对稳定。密度之间有些许差异,这是可以预期的,因为这些估计密度的基础数据是三个一个月的样本。根据这些结果,警方或犯罪学家应当对预测未来自行车盗窃事件最可能发生的地方有较高的信心。

  7. 重复步骤 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.39: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.40: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.41:2018 年 11 月扒窃案件的联合与边际密度估计

    图 9.41:2018 年 11 月扒窃案件的联合与边际密度估计

    与自行车盗窃的结果类似,商店扒窃的密度在几个月内相当稳定。2018 年 8 月的密度看起来与其他两个月不同;但是,如果您查看经度和纬度值,您会注意到密度非常相似——只是偏移和缩放了。其原因是可能存在一些离群值,迫使创建一个更大的绘图区域。

  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.42:2018 年 7 月入室盗窃的联合和边际密度的估计

图 9.42:2018 年 7 月入室盗窃的联合和边际密度的估计

对于 2018 年 10 月,用于入室盗窃的核密度估计模型的拟合和可视化代码如下:

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.43:2018 年 10 月入室盗窃的联合和边际密度的估计

图 9.43:2018 年 10 月入室盗窃的联合和边际密度的估计

对于 2018 年 12 月,用于入室盗窃的核密度估计模型的拟合和可视化代码如下:

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.44:2018 年 12 月入室盗窃的联合和边际密度的估计

图 9.44:2018 年 12 月入室盗窃的联合和边际密度的估计

再次可以看到,这些分布在几个月内非常相似。唯一的区别是密度似乎从 7 月到 12 月扩展或分散。总是由于样本数据中的噪声和固有信息不足导致估计密度的小变化。

注意

要访问此特定部分的源代码,请参阅packt.live/2wmh5yj

您还可以在packt.live/2W0EAGK上在线运行此示例。

posted @ 2025-07-14 17:27  绝不原创的飞龙  阅读(15)  评论(0)    收藏  举报