应用人工智能研讨会-全-

应用人工智能研讨会(全)

原文:annas-archive.org/md5/0323474b1674bee4360c291447287161

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

关于本书

你已经知道人工智能AI)和机器学习ML)已经出现在你日常使用的许多工具中。但你是否希望能够创建你自己的 AI 和 ML 模型,并在这些领域提升你的技能,从而启动你的 AI 职业生涯?

应用人工智能工作坊通过实践练习和有用的示例帮助你入门,所有内容巧妙结合,帮助你获得技能,转变职业生涯。

本书首先教你如何使用回归分析预测结果。接下来,你将学习如何使用K-近邻算法KNN)和支持向量机SVM)分类器对数据进行分类。随着学习的深入,你将通过学习如何构建一个可靠的决策树模型来探索各种决策树,这个模型可以帮助你的公司找到客户可能购买的汽车。最后几章将介绍深度学习和神经网络。通过各种活动,如预测股价和识别手写数字,你将学习如何训练和实施卷积神经网络CNN)和递归神经网络RNN)。

本书结束时,你将学会如何预测结果并训练神经网络,并能够使用各种技术来开发 AI 和 ML 模型。

受众

应用人工智能工作坊是为软件开发人员和数据科学家设计的,旨在帮助他们通过机器学习丰富项目。虽然你不需要人工智能方面的经验,但建议你具备高中水平的数学知识,并至少掌握一种编程语言,最好是 Python。尽管这是一本入门书籍,但经验丰富的学生和程序员也可以通过专注于本书中的实际应用来提高他们的 Python 编程技能。

关于章节

第一章人工智能概述,介绍了人工智能。你还将通过一个简单的井字游戏实现你的第一个人工智能,在这个游戏中,你将教会程序如何战胜人类玩家。

第二章回归分析简介,介绍了回归分析。你将遇到各种技术,例如单变量和多变量的线性回归,以及多项式回归和支持向量回归。

第三章分类简介,介绍了分类。这里,你将实现各种技术,包括 K-近邻算法和支持向量机。

第四章决策树简介,介绍了决策树和随机森林分类器。

第五章人工智能:聚类,开始引导你用全新的方式思考,进行第一个无监督学习模型。你将了解聚类的基础知识,并使用 k-means 算法实现平面聚类,使用均值漂移算法实现层次聚类。

第六章神经网络与深度学习,介绍了 TensorFlow、卷积神经网络(CNN)和循环神经网络(RNN)。你还将使用神经网络和深度学习实现图像分类程序。

约定

文本中的代码词汇、文件夹名称、文件名、文件扩展名、路径名和用户输入以如下方式显示:“请注意,此函数位于 tensorflow 命名空间中,默认情况下不会引用它。”

一段代码设置如下:

features_train = features_train / 255.0
features_test = features_test / 255.0

新术语和重要词汇以这样的方式显示:“均值漂移是层次聚类的一个例子,其中聚类算法决定了聚类的数量。”

代码展示

跨越多行的代码通过反斜杠(\\)进行拆分。当代码执行时,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)

设置你的环境

在我们详细探索本书之前,我们需要设置特定的软件和工具。在接下来的部分中,我们将看到如何做到这一点。

在系统上安装 Jupyter

要在 Windows、MacOS 和 Linux 上安装 Jupyter,请执行以下步骤:

  1. 前往 www.anaconda.com/distribution/ 安装 Anaconda Navigator,这是一个界面,您可以通过它访问本地的 Jupyter Notebook。

  2. 现在,根据你的操作系统(Windows、MacOS 或 Linux),你需要下载 Anaconda 安装程序。请查看以下图示,我们已经为 Windows 下载了 Anaconda 文件:图 0.1:Anaconda 主界面

    ](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_00_01.jpg)

图 0.1:Anaconda 主界面

启动 Jupyter Notebook

从 Anaconda Navigator 启动 Jupyter Notebook 时,需要执行以下步骤:

  1. 一旦你安装了 Anaconda Navigator,你将看到 图 0.2 中所示的界面:图 0.2:Anaconda 安装界面

    ](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_00_02.jpg)

    图 0.2:Anaconda 安装界面

  2. 现在,点击 Jupyter Notebook 选项下的 Launch 启动你的本地系统中的 notebook:图 0.3:Jupyter Notebook 启动选项

    ](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_00_03.jpg)

图 0.3:Jupyter Notebook 启动选项

你已经成功在系统上安装了 Jupyter Notebook。

安装库

pip 已预装在 Anaconda 中。一旦 Anaconda 安装在你的机器上,所有必需的库都可以使用 pip 安装,例如 pip install numpy。或者,你也可以使用 pip install –r requirements.txt 一次性安装所有必需的库。你可以在 packt.live/3erXq0B 找到 requirements.txt 文件。

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

一些重要的包

本章的一些练习需要以下包:

  • EasyAI

  • Quandl

  • TensorFlow 2.1.0

按照此指南进行安装。在 Windows 上,打开命令提示符。在 macOS 或 Linux 上,打开终端。

要安装 easyAIQuandl,请输入以下命令:

pip install easyAI==1.0.0.4 Quandl==3.5.0 tensorflow==2.1.0

访问代码文件

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

我们已经尽力支持所有活动和练习的互动版本,但我们也建议进行本地安装,以防某些情况下无法使用此支持。

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

第一章:1. 人工智能简介

概述

本章将向你介绍人工智能(AI)的基础知识,这些知识是各种人工智能领域的基础。你还将通过简单的编码练习使用 Python 编程语言接触不同的算法,包括 MinMax 和 A*。你还将通过一个简单的井字棋游戏实现你的第一个人工智能程序,在这个游戏中,你将教会程序如何击败人类玩家。到本章结束时,你将学会如何使用流行的 Python 库开发智能的人工智能驱动程序。

介绍

在讨论不同的人工智能技术和算法之前,我们将先了解人工智能和机器学习的基础知识,并介绍一些基本定义。将通过现实世界的例子以易于理解的方式呈现人工智能的基本概念。

人工智能尝试使用硬件和软件解决方案复制人类智能。它基于逆向工程。例如,人工神经网络是模拟人类大脑工作方式的模型。除了神经网络之外,神经科学中还有许多其他模型可以用来通过人工智能解决现实世界的问题。已知在各自领域中使用人工智能的公司包括:谷歌(Google),提供 Google 翻译;苹果(Apple),提供 Face ID;亚马逊(Amazon),提供 Alexa 产品;甚至 Uber 和特斯拉(Tesla),他们仍在致力于开发自动驾驶汽车。

另一方面,机器学习是一个常常与人工智能混淆的术语。它起源于 1950 年代,并由阿瑟·李·塞缪尔(Arthur Lee Samuel)于 1959 年首次定义。

在他的书《机器学习》中,汤姆·米切尔提出了一个简单的定义:“机器学习领域关注的是如何构建能够随着经验自动改进的计算机程序。

我们可以理解为,机器学习是一个领域,目标是构建一个能够从数据中学习模式并随着更多数据的增加而提高学习能力的计算机程序。

他还提出了一个更正式的定义,即计算机程序如果在任务T上,通过性能度量P衡量后,其在T上的表现随着经验E的积累而改进,那么可以说它在从经验中学习。这可以转化为计算机程序需要什么才能被认为是在学习。我们可以将E(经验)理解为需要输入给机器的数据,T作为机器需要执行的决策类型,而P则是衡量其表现的标准。

从这两个定义中,我们可以得出结论:机器学习是实现人工智能的一种方式。然而,你也可以不使用机器学习就拥有人工智能。例如,如果你硬编码规则和决策树,或者应用搜索技术,你仍然可以创建一个人工智能代理,尽管你的方法与机器学习关系不大。

人工智能和机器学习帮助科学界应对大数据的爆炸性增长,每秒钟都会生成越来越多的数据。通过人工智能和机器学习,科学家们可以从这些庞大的数据集中提取人眼无法快速处理的信息。

现在我们已经了解了人工智能和机器学习,接下来让我们聚焦于人工智能。

人工智能如何解决问题?

人工智能根据人类大脑处理信息的方式来自动化人类智能。

每当我们解决问题或与他人互动时,我们都会经历一个过程。通过这样做,我们限制了问题或互动的范围。这个过程通常可以在人工智能中建模并自动化。

人工智能使计算机看起来像人类一样思考。

有时,感觉人工智能知道我们需要什么。只需想想你在网上购物后收到的个性化优惠券。人工智能知道我们最可能购买的产品。通过实施不同的技术和模型,机器能够学习你的偏好,我们将在本书后面讨论这些内容。

人工智能是由执行低级指令的计算机实现的。

即使一个解决方案看起来似乎很智能,我们仍然编写代码,就像在任何其他人工智能软件解决方案中一样。即使我们在模拟神经元,简单的机器代码和计算机硬件也执行着思维过程。

大多数人工智能应用都有一个主要目标。当我们与一个人工智能应用互动时,它看起来像人类,因为它能够将问题领域限制在一个主要目标上。因此,人工智能实现目标的过程可以被分解为更小、更简单的低级指令。

人工智能可能会刺激人类感官和思维过程,应用于专业领域。

你必须能够模拟人类的感官和思维,有时甚至要欺骗人工智能相信我们正在与另一个人互动。在某些特殊情况下,我们甚至可以增强我们自己的感官。

类似地,当我们与聊天机器人互动时,我们希望机器人能理解我们。我们期望聊天机器人甚至语音识别系统提供一个计算机与人类之间的接口,满足我们的期望。为了满足这些期望,计算机需要模拟人类的思维过程。

人工智能中的学科多样性

一辆无法感知高速公路上其他行驶的车辆的自动驾驶汽车将非常危险。人工智能代理需要处理并感知周围的环境才能驾驶汽车。然而,这还不够,因为如果没有理解运动物体的物理学,在正常环境中驾驶汽车几乎是不可能的,更不用说可能是致命的任务了。

为了创建一个可用的人工智能解决方案,涉及不同的学科,例如:

  • 机器人学:在空间中移动物体

  • 算法理论:构建高效的算法

  • 统计学:推导有用的结果、预测未来并分析过去

  • 心理学:模拟人类大脑的工作方式

  • 软件工程:创建可维护的解决方案,能够经受住时间的考验

  • 计算机科学或计算机编程:在实践中实现我们的软件解决方案

  • 数学:执行复杂的数学运算

  • 控制理论:创建前馈和反馈系统

  • 信息理论:表示、编码、解码和压缩信息

  • 图论:对空间中的不同点进行建模与优化,并表示层次结构

  • 物理学:对现实世界进行建模

  • 计算机图形学和图像处理:显示和处理图像与电影

本书将介绍这些学科中的一些内容,包括算法理论、统计学、计算机科学、数学和图像处理。

AI 的领域与应用

现在我们已经了解了 AI 的基本概念,让我们继续看看它在现实生活中的应用。

人类行为的模拟

人类有五种基本感官,可以分为视觉(看)、听觉(听)、运动觉(动)、嗅觉(闻)和味觉(尝)。然而,为了理解如何创建智能机器,我们可以将这些学科分开如下:

  • 听力和口语

  • 理解语言

  • 记忆事物

  • 思维

  • 视觉

  • 运动

其中一些超出了我们讨论的范围,因为本章的目的是理解基础知识。例如,要移动机器臂,我们必须学习复杂的大学数学才能理解其原理,但在本书中,我们将仅关注实际应用:

  • 听力和口语:通过语音识别系统,AI 可以从用户处收集信息。利用语音合成,AI 可以将内部数据转化为可理解的声音。语音识别和语音合成技术处理的是人类发出的声音或人类能够理解的声音的识别与构建。

    例如,假设你正在前往一个你不懂当地语言的国家旅行。你可以对着手机的麦克风说话,期望它能够理解你所说的内容,然后将其翻译成另一种语言。相反,当当地人说话时,AI 也可以将声音翻译成你能理解的语言。语音识别和语音合成使得这一切成为可能。

    注意

    语音合成的一个例子是谷歌翻译。你可以访问translate.google.com/,通过点击翻译后的单词下方的扬声器按钮,让翻译器朗读非英语的单词。

  • 理解语言:我们可以通过处理自然语言来理解它。这个领域被称为自然语言处理(NLP)。

    在 NLP(自然语言处理)领域,我们往往通过学习音节之间的统计关系来学习语言。

  • 记忆:我们需要表示我们对世界的了解。这就是创建知识库和分层表示(称为本体论)的作用。本体论对我们世界中的事物和观念进行分类,并包含这些类别之间的关系。

  • 思考:我们的人工智能系统必须通过使用专家系统成为某一领域的专家。专家系统可以基于数学逻辑以确定性方式进行构建,也可以以模糊的、非确定性的方式进行构建。

    专家系统的知识库使用不同的技术表示。随着问题领域的扩大,我们创建了分层的本体论。

    我们可以通过将网络建模为大脑的构建模块来复制这个结构。这些构建模块称为神经元,而网络本身称为神经网络。

  • 视觉:我们必须通过感官与现实世界互动。到目前为止,我们仅触及了听觉感官,涉及语音识别和合成。那么如果我们需要看东西呢?如果是这样,我们就必须创建计算机视觉技术来了解我们的环境。毕竟,识别面孔是有用的,而且大多数人类在这方面都是专家。

    计算机视觉依赖于图像处理。尽管图像处理本身不是直接的人工智能学科,但它是人工智能所需的学科。

  • 运动:运动和触摸对我们人类来说是自然的,但对计算机来说却是非常复杂的任务。运动由机器人技术处理。这是一个非常需要数学的课题。

    机器人技术基于控制理论,您需要创建一个反馈回路,并根据收集到的反馈控制物体的运动。控制理论在其他领域也有应用,这些领域与空间中的物体运动完全无关。这是因为所需的反馈回路与经济学中建模的反馈回路相似。

模拟智能——图灵测试

艾伦·图灵,图灵机的发明者,图灵机是算法理论中使用的一个抽象概念,他提出了一种测试智能的方法。这个测试在人工智能文献中被称为图灵测试

使用文本接口,提问者与人类和聊天机器人进行对话。聊天机器人的任务是误导提问者,使其无法分辨计算机是否是人类。

我们需要哪些学科才能通过图灵测试?

首先,我们需要理解口语语言,以了解提问者在说什么。我们通过使用自然语言处理NLP)来实现这一点。我们还必须通过学习之前的问题和答案,并使用 AI 模型以可信的方式回应提问者。

我们需要成为人类大脑倾向于感兴趣的事物的专家。我们需要建立一个人类学的专家系统,涉及我们世界中的物体和抽象思想的分类法,以及历史事件甚至情感。

通过图灵测试非常困难。目前的预测表明,我们在 2020 年代末之前无法创建足够好的系统通过图灵测试。如果这还不够,我们可以进一步推进到完全图灵测试,它还包括运动和视觉。

接下来,我们将继续讨论人工智能中的工具和学习模型。

人工智能工具与学习模型

在前面的章节中,我们发现了人工智能的基础知识。人工智能的核心任务之一是学习。这就是智能体出现的地方。

智能体

在解决人工智能问题时,我们在环境中创建一个能够从周围环境收集数据并影响其环境的行为者。这个行为者被称为智能体

一个智能体如下所示:

  • 是自主的

  • 通过传感器观察其周围环境

  • 通过执行器在其环境中行动(执行器是负责移动和控制机制的组件)

  • 将其活动指向实现目标

智能体还可以学习并访问知识库。

我们可以将智能体看作是一个将感知映射到行动的函数。如果智能体有一个内部知识库,那么感知、行动和反应也可能会改变知识库。

行动可能会得到奖励或惩罚。设定正确的目标并实施胡萝卜与大棒的策略有助于智能体学习。如果目标设定正确,智能体有机会战胜通常更复杂的人脑。这是因为人脑的主要目标是生存,不论我们在玩什么游戏。智能体的主要动机是达成目标。因此,智能体在没有任何知识的情况下做出随机动作时,并不会感到尴尬。

Python 在人工智能中的作用

为了将基本的人工智能概念付诸实践,我们需要一种支持人工智能的编程语言。在本书中,我们选择了 Python。有几个原因使得 Python 成为人工智能的良好选择:

  • 便捷性与兼容性: Python 是一种高级编程语言。这意味着你无需担心内存分配、指针或机器代码等问题。你可以便捷地编写代码,并依赖 Python 的强大功能。Python 也具有跨平台兼容性。

  • 流行度: 强调开发者体验使得 Python 成为软件开发人员的热门选择。事实上,根据www.hackerrank.com的 2018 年开发者调查,Python 在所有年龄段的软件开发人员中,都是最受欢迎的编程语言。这是因为 Python 易于阅读且简单。因此,Python 非常适合快速应用开发。

  • 效率: 尽管 Python 是一种解释型语言,但它与数据科学中使用的其他语言,如 R,具有可比性。其主要优势在于内存效率,因为 Python 能够处理大型内存数据库。

    注意

    Python 是一种多用途语言。它可以用于创建桌面应用、数据库应用、移动应用和游戏。Python 的网络编程功能也值得一提。此外,Python 是一个优秀的原型设计工具。

为什么 Python 在机器学习、数据科学和人工智能中占主导地位?

要理解 Python 在机器学习、数据科学和人工智能领域的主导地位,我们必须将 Python 与其他在这些领域也有应用的语言进行比较。

相比于 R 这种为统计学家设计的编程语言,Python 更加多功能和简便,因为它允许程序员构建各种各样的应用,从游戏到人工智能应用。

与 Java 和 C++ 相比,使用 Python 编写程序的速度要快得多。Python 还提供了高度的灵活性。

有一些语言在灵活性和便利性方面与 Python 类似:Ruby 和 JavaScript。Python 相对于这些语言有优势,因为 Python 拥有一个强大的人工智能生态系统。在任何领域,开源的第三方库支持在很大程度上决定了该语言的成功。Python 的第三方 AI 库支持非常出色。

Python 中的 Anaconda

我们在前言中安装了 Anaconda。Anaconda 将是我们进行 AI 实验时的首选工具。

Anaconda 附带了包、IDE、数据可视化库和高性能并行计算工具,所有这些功能都集中在一个地方。Anaconda 隐藏了配置问题和维护数据科学、机器学习与 AI 堆栈的复杂性。这个特点在 Windows 系统中特别有用,因为在 Windows 中,版本不匹配和配置问题往往最为常见。

Anaconda 附带了 Jupyter Notebook,你可以在其中以文档样式编写代码和注释。当你实验 AI 特性时,代码的执行步骤像是一个互动式教程,你可以逐步运行每个代码片段。

注意

IDE 代表 集成开发环境。文本编辑器提供了一些高亮和格式化代码的功能,而 IDE 在文本编辑器的基础上提供了更多功能,包括自动重构、测试、调试、打包、运行和部署代码的工具。

Python 的人工智能库

这里列出的库并不完整,因为 Anaconda 中可用的库超过 700 个。然而,这些特定的库会为你打下一个良好的基础,因为它们能帮助你在 Python 中实现基本的 AI 算法:

  • NumPy:NumPy 是 Python 的一个计算库。由于 Python 本身没有内置数组数据结构,我们必须使用库来高效地表示向量和矩阵。在数据科学中,我们需要这些数据结构来执行简单的数学运算。在未来的章节中,我们将广泛使用 NumPy。

  • SciPy:SciPy 是一个包含用于数据科学的算法的高级库。它是 NumPy 的一个很好的补充库,因为它提供了你需要的所有高级算法,无论是线性代数算法、图像处理工具,还是矩阵操作。

  • pandas:pandas 提供了快速、灵活且富有表现力的数据结构,如一维序列和二维 DataFrame。它高效地加载、格式化和处理不同类型的复杂表格数据。

  • scikit-learn:scikit-learn 是 Python 主要的机器学习库,基于 NumPy 和 SciPy 库。scikit-learn 提供了执行分类、回归、数据预处理以及监督和非监督学习所需的功能。

  • NLTK:本书不会涉及自然语言处理(NLP),但 NLTK 值得一提,因为它是 Python 的主要自然语言工具包。你可以使用这个库执行分类、分词、词干提取、标注、解析、语义推理等多种操作。

  • TensorFlow:TensorFlow 是 Google 的神经网络库,非常适合实现深度学习 AI。TensorFlow 的灵活核心可以用来解决各种数值计算问题。TensorFlow 的一些现实应用包括 Google 语音识别和物体识别。

NumPy 库简要介绍

NumPy 库在本书中将发挥重要作用,因此值得深入探讨。

启动 Jupyter Notebook 后,你可以简单地按如下方式导入 numpy

import numpy as np

一旦导入了 numpy,你可以通过它的别名 np 来访问它。NumPy 包含了高效实现的一些数据结构,如向量和矩阵。

让我们看看如何定义向量和矩阵:

np.array([1,3,5,7])

预期输出是这样的:

array([1, 3, 5, 7])

我们可以使用以下语法声明一个矩阵:

A = np.mat([[1,2],[3,3]])
A

预期输出是这样的:

matrix([[1, 2],
        [3, 3]])

array 方法创建一个数组数据结构,而 .mat 创建一个矩阵。

我们可以对矩阵执行许多操作,包括加法、减法和乘法。我们来看看这些操作:

矩阵中的加法:

A + A

预期输出是这样的:

matrix([[2, 4],
        [6, 6]])

矩阵中的减法:

A - A

预期输出是这样的:

matrix([[0, 0],
        [0, 0]])

矩阵中的乘法:

A * A

预期输出是这样的:

matrix([[ 7,  8],
        [12, 15]])

矩阵的加法和减法按单元逐个进行。

矩阵乘法遵循线性代数规则。要手动计算矩阵乘法,你需要将两个矩阵对齐,具体如下:

图 1.1:两个矩阵的乘法计算

图 1.1:两个矩阵的乘法计算

要获取矩阵的 (i,j) 元素,你需要计算矩阵第 i 行与第 j 列的点积(标量积)。两个向量的标量积是它们对应坐标的乘积之和。

另一个常见的矩阵运算是矩阵的行列式。行列式是与方阵相关的一个数字。使用 NumPy 的 linalg 函数(线性代数算法)计算行列式的代码如下所示:

np.linalg.det( A )

预期输出如下所示:

-3.0000000000000004

从技术上讲,行列式可以计算为 1*3 – 2*3 = -3。请注意,NumPy 使用浮点运算来计算行列式,因此结果的准确性并不完美。误差是由于大多数编程语言中浮点数表示的方式造成的。

我们还可以转置一个矩阵,如以下代码所示:

np.matrix.transpose(A)

预期输出如下所示:

matrix([[1, 3],
        [2, 3]])

计算矩阵的转置时,我们将其值翻转至主对角线的位置。

NumPy 还有许多其他重要的功能,因此我们将在本书的大部分章节中使用它。

练习 1.01:使用 NumPy 进行矩阵运算

我们将使用 Jupyter Notebook 和以下矩阵来解决这个练习。

我们将计算矩阵的平方,即矩阵的行列式和矩阵的转置,如下图所示,使用 NumPy:

图 1.2:一个简单的矩阵表示

](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_01_02.jpg)

图 1.2:一个简单的矩阵表示

以下步骤将帮助您完成此练习:

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 导入 numpy 库并命名为 np

    import numpy as np
    
  3. 使用 np.mat 创建一个名为 A 的二维数组,用于存储 [[1,2,3],[4,5,6],[7,8,9]] 矩阵:

    A = np.mat([[1,2,3],[4,5,6],[7,8,9]])
    A
    

    预期输出如下所示:

    matrix([[1, 2, 3],
            [4, 5, 6],
            [7, 8, 9]])
    

    注意

    如果您创建的是 np.array 而非 np.mat,则数组乘法的结果将不正确。

  4. 接下来,我们使用星号进行矩阵乘法,并将结果保存在一个名为 matmult 的变量中,如以下代码片段所示:

    matmult = A * A
    matmult
    

    预期输出如下所示:

    matrix([[ 30, 36, 42],
            [ 66, 81, 96],
            [102, 126, 150]])
    
  5. 接下来,通过进行矩阵乘法手动计算 A 的平方。例如,矩阵的左上角元素的计算方式如下:

    1 * 1 + 2 * 4 + 3 * 7
    

    预期输出如下所示:

    30
    
  6. 使用 np.linalg.det 计算矩阵的行列式,并将结果保存在名为 det 的变量中:

    det = np.linalg.det( A )
    det
    

    预期输出(可能会略有不同)如下所示:

    0.0
    
  7. 使用 np.matrix.transpose 获取矩阵的转置,并将结果保存在名为 transpose 的变量中:

    transpose = np.matrix.transpose(A)
    transpose
    

    预期输出如下所示:

    matrix([[1, 4, 7],
            [2, 5, 8],
            [3, 6, 9]])
    

    如果 T 是矩阵 A 的转置,那么 T[j][i] 等于 A[i][j]

    注意

    若要访问该部分的源代码,请参阅 packt.live/316Vd6Z

    您也可以在网上运行此示例,访问 packt.live/2BrogHL。您必须执行整个 Notebook 以获得预期结果。

完成此练习后,您已经看到 NumPy 提供了许多用于向量、矩阵和其他数学结构的有用功能。

在接下来的部分,我们将使用 Python 实现一个有趣的井字游戏 AI。

游戏 AI 的 Python 实现

AI 游戏玩家其实就是一个有明确目标的智能代理:赢得游戏并击败所有其他玩家。AI 实验在游戏方面取得了惊人的结果。今天,没有人类能够在国际象棋游戏中击败 AI。

围棋曾是最后一个人类玩家能够持续击败电脑玩家的游戏。然而,在 2017 年,谷歌的围棋 AI AlphaGo 击败了世界排名第一的围棋选手。

游戏中的智能代理

智能代理根据游戏规则进行操作。代理可以通过其传感器感知当前游戏状态,并评估潜在的步骤。一旦代理找到最好的步骤,它就会通过执行器执行该动作。代理根据它拥有的信息找到最佳的动作以达到目标。动作会获得奖励或惩罚。胡萝卜和大棒就是奖励和惩罚的典型例子。想象一下,在你的小车前面有一只驴。你把胡萝卜放在驴眼前,于是它开始朝着胡萝卜走去。只要驴停下来,骑手可能就会用大棒惩罚它。这并不是人类的移动方式,但奖励和惩罚在某种程度上控制了生物体的行为。人类在学校、工作和日常生活中也会面临同样的情况。我们没有胡萝卜和大棒,而是通过收入和法律惩罚来塑造我们的行为。

在大多数游戏中,一系列好的动作会导致奖励。当人类玩家感到被奖励时,会感到快乐。人类倾向于采取最大化自己幸福的方式行动。而智能代理则只关心自己的目标,即最大化奖励并最小化影响其表现的惩罚。

在建模游戏时,我们必须确定它们的状态空间。一个动作会导致状态转变。当我们探索所有可能动作的后果时,我们得到一个决策树。随着我们开始探索所有玩家未来可能的动作,这棵树会变得更深,直到游戏结束。

AI 的优势在于每秒可以执行数百万种可能的步骤。因此,游戏 AI 通常归结为一次搜索练习。在探索游戏中所有可能的棋步序列时,我们得到一个游戏的状态树。

试想一下一个国际象棋 AI。通过构建一个包含所有可能棋步序列的状态树来评估所有可能的棋步,问题在哪里?

国际象棋是一个在复杂度上属于 EXPTIME 级别的游戏。可能的棋步数以组合方式爆炸式增长。

白方开始时有 20 种可能的走法:8 个兵可以走一步或两步,2 匹马可以走“上-上-左”或“上-上-右”。然后,黑方可以做这 20 种走法中的任何一种。仅仅每个玩家走一步,就已经有 20*20 = 400 种可能的组合。

第二步之后,我们得到了 8,902 种可能的棋盘排列,这个数字不断增长。只需要七步,你就得搜索 10,921,506 种可能的排列。

一局棋的平均长度大约是 40 步。有些特别的游戏可能需要超过 200 步才能结束。

结果是,计算机玩家根本没有足够的时间去探索整个状态空间。因此,搜索活动必须通过适当的奖励、惩罚和规则简化来引导。

广度优先搜索和深度优先搜索

创建游戏 AI 通常是一个搜索过程。因此,我们需要熟悉两种主要的搜索技术:

  • 广度优先搜索BFS

  • 深度优先搜索DFS

这些搜索技术应用于有向根树。

树是一种数据结构,它有节点,这些节点通过边连接在一起,使得树中任意两个节点之间正好有一条路径相连。请看下图:

图 1.3:有向根树

图 1.3:有向根树

当树被根植时,树中有一个特殊的节点,称为根节点,这是我们开始遍历的地方。一个有向树是指树的边只能单向遍历。节点可以是内部节点或叶节点。内部节点至少有一条边,通过这条边我们可以离开该节点。叶节点没有任何指向外部的边。

在 AI 搜索中,树的根节点是起始状态。我们从这个状态开始,生成搜索树的后继节点并进行遍历。搜索技术的不同,取决于我们访问这些后继节点的顺序。

广度优先搜索(BFS)

BFS 是一种搜索技术,它从根节点(节点 1)开始,首先探索同一深度(或层级)上最靠近的节点,然后再移动到下一个深度:

图 1.4:广度优先搜索(BFS)树

图 1.4:广度优先搜索(BFS)树

在前面的图中,你可以看到 BFS 技术的搜索顺序。从根节点(1)开始,BFS 会移动到下一层,并探索最接近的节点(2),然后再查看同一层上的其他节点(34)。接着,它会移动到下一层,探索 56,因为它们彼此接近,然后回到节点 3,最后到达节点 7,以此类推。

深度优先搜索(DFS)

DFS 是一种搜索技术,它从根节点(节点 1)开始,尽可能多地探索同一分支,然后再移动到下一个最接近的分支:

图 1.5:深度优先搜索(DFS)树

图 1.5:深度优先搜索(DFS)树

在前面的图示中,你可以看到 DFS 技术的搜索顺序。从根节点(1)开始,DFS 会转到最接近的节点(2),并在第三层深度探索到分支的尽头(3),然后回到节点(2),完成探索它的第二个分支(4)。接着,它将返回到第二层深度并以相同的方式开始下一分支(6),最终完成最后一个分支(7)。

现在,假设我们有一个由根节点定义的树,并且有一个函数从根节点生成所有后继节点。在以下示例中,每个节点都有一个值和深度。我们从1开始,可能将值增加12。我们的目标是达到值5

注意

\ ) 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.
root = {'value': 1, 'depth': 1}
def succ(node):
    if node['value'] == 5:
        return []
    elif node['value'] == 4:
        return [{'value': 5,'depth': node['depth']+1}]
    else:
        return [{'value': node['value']+1, \
                 'depth':node['depth']+1}, \
                {'value': node['value']+2, \
                 'depth':node['depth']+1}]

在前面的代码片段中,我们将根节点初始化为值和深度为1。然后,我们创建了一个名为succ的函数,该函数以一个节点为输入。这个函数有三种不同的情况:

  • 如果输入的节点值是5,则不返回任何内容,因为我们已经达到了目标(5)。

  • 如果输入节点值为4,则返回值5并将当前深度加1

  • 如果值是其他任何内容,则将深度加1,并为该值创建两个情况,+1+2

首先,我们将执行 BFS,如下所示:

def bfs_tree(node):
    nodes_to_visit = [node]
    visited_nodes = []
    while len(nodes_to_visit) > 0:
        current_node = nodes_to_visit.pop(0)
        visited_nodes.append(current_node)
        nodes_to_visit.extend(succ(current_node))
    return visited_nodes
bfs_tree(root)

在前面的代码片段中,我们通过将一个节点作为输入实现了bfs_tree函数。这个函数可以分为三部分:

nodes_to_visitvisited_nodes变量。

第二部分是实现 BFS 的地方:

  • current_node变量从nodes_to_visit变量中取出第一个元素。

  • visited_nodes变量将此元素添加到它的列表中。

  • nodes_to_visit变量将通过调用succ函数并以current_node作为输入生成的新节点添加到其中。

前面提到的三条指令被包装成一个由nodes_to_visit变量中元素数量定义的循环。只要nodes_to_visit中至少有一个元素,循环就会继续进行。

visited_nodes变量。

期望的输出是这样的:

[{'value': 1, 'depth': 1},
 {'value': 2, 'depth': 2},
 {'value': 3, 'depth': 2},
 {'value': 3, 'depth': 3},
 {'value': 4, 'depth': 3},
 {'value': 4, 'depth': 3},
 {'value': 5, 'depth': 3},
 {'value': 4, 'depth': 4},
 {'value': 5, 'depth': 4},
 {'value': 5, 'depth': 4},
 {'value': 5, 'depth': 4},
 {'value': 5, 'depth': 5}]

如你所见,BFS 在深入到下一个深度层级之前,先在当前深度层级中遍历所有值,并且可以看到深度和数值是按顺序递增的。DFS 则不如此。

如果我们需要遍历一个图而不是一个有向根树,BFS 将有所不同:每当我们访问一个节点时,我们需要检查该节点是否已经访问过。如果该节点已访问过,我们将直接忽略它。

在这一章中,我们将仅在树上使用广度优先遍历(BFS)。深度优先遍历(DFS)与 BFS 非常相似。DFS 和 BFS 的区别在于访问节点的顺序。BFS 会在访问其他节点之前先访问一个节点的所有子节点,而 DFS 则深入树的深层。

看一下以下示例,在其中我们实现了 DFS:

def dfs_tree(node):
    nodes_to_visit = [node]
    visited_nodes = []
    while len(nodes_to_visit) > 0:
        current_node = nodes_to_visit.pop()
        visited_nodes.append(current_node)
        nodes_to_visit.extend(succ(current_node))
    return visited_nodes
dfs_tree(root)

在上面的代码片段中,我们通过将一个节点作为输入实现了 dfs_tree 函数。该函数可以分为三部分:

nodes_to_visitvisited_nodes 变量。

第二部分是实现 DFS 的地方:

  • current_node 变量移除 nodes_to_visit 变量中的最后一个元素。

  • visited_nodes 变量将此元素添加到其列表中。

  • nodes_to_visit 变量将通过调用 succ 函数并将 current_node 作为输入生成的新节点添加到其中。

上面的三条指令被封装在一个循环中,循环的次数由 nodes_to_visit 变量中的元素个数决定。只要 nodes_to_visit 至少有一个元素,循环就会继续进行。

最后,也就是在 visited_nodes 中。

如你所见,BFS 和 DFS 之间的主要区别在于我们从 nodes_to_visit 中取出元素的顺序。对于 BFS,我们取第一个元素,而对于 DFS,我们取最后一个元素。

预期的输出是这样的:

[{'value': 1, 'depth': 1},
 {'value': 3, 'depth': 2},
 {'value': 5, 'depth': 3},
 {'value': 4, 'depth': 3},
 {'value': 5, 'depth': 4},
 {'value': 2, 'depth': 2},
 {'value': 4, 'depth': 3},
 {'value': 5, 'depth': 4},
 {'value': 3, 'depth': 3},
 {'value': 5, 'depth': 4},
 {'value': 4, 'depth': 4},
 {'value': 5, 'depth': 5}]

注意到 DFS 算法如何快速深入(深度比 BFS 更快地达到更高的值)。它不一定先找到最短路径,但可以保证在探索第二条路径之前先找到一个叶子节点。

在游戏 AI 中,BFS 算法通常更适合评估游戏状态,因为 DFS 可能会迷失。想象一下,开始一场国际象棋游戏,DFS 算法可能会轻易迷失在探索走法选项中。

探索游戏的状态空间

让我们探索一个简单游戏的状态空间:井字游戏。状态空间是指游戏中所有可能的配置,在这种情况下,意味着所有可能的移动。

在井字游戏中,给定一个 3x3 的棋盘。两个玩家玩这个游戏。一个玩家使用标记 X,另一个玩家使用标记 O。X 先开始游戏,之后两名玩家轮流进行移动。游戏的目标是横向、纵向或对角线排列三个自己的标记。

让我们按照以下方式表示井字游戏棋盘的单元格:

图 1.6:井字游戏棋盘

图 1.6:井字游戏棋盘

在以下示例中,X 从位置 1 开始。O 在位置 5 反击,X 在位置 9 移动,然后 O 移动到位置 3

图 1.7:井字游戏棋盘,标记为圈和叉

图 1.7:井字游戏棋盘,标记为圈和叉

这是第二个玩家的错误,因为现在 X 被迫将标记放在单元格 7 上,创造了两种未来的赢得比赛的情景。无论 O 是否通过移动到单元格 48 来防守——X 都会通过选择另一个未占用的单元格赢得比赛。

注意

你可以在 www.half-real.net/tictactoe/ 尝试这个游戏。

为了简化,我们将只探讨 AI 玩家先手时的状态空间。我们将从一个随机下棋的 AI 玩家开始,它会在空白格子中随机放置一个符号。在与这个 AI 玩家对弈之后,我们将创建一棵完整的决策树。一旦我们生成了所有可能的游戏状态,你将体验到它们的组合爆炸。由于我们的目标是将这些复杂性简化,我们将使用几种不同的技术使 AI 玩家更智能,并减少决策树的大小。

在这个实验结束时,我们将拥有一棵少于 200 种不同游戏结局的决策树,作为奖励,AI 玩家将永远不会输掉一场游戏。

要进行随机移动,你需要知道如何使用 Python 从列表中选择一个随机元素。我们将使用random库的choice函数来实现:

from random import choice
choice([2, 4, 6, 8])

这次输出是6,但对你来说,它可以是列表中的任何数字。

choice函数的输出是列表中的一个随机元素。

注意

在接下来的部分中,我们将使用阶乘符号。阶乘用"!"符号表示。根据定义,0! = 1,且 n! = n(n-1)!。在我们的例子中,9! = 98! = 987! = … = 987654321。

估算井字游戏中可能状态的数量

粗略估算井字游戏状态空间每一层的可能状态数量:

  • 在我们的估算中,我们不会在游戏结束前停止,直到所有格子都被填满。虽然玩家可能在游戏结束前就获胜,但为了统一起见,我们将继续进行游戏。

  • 第一个玩家将选择九个格子中的一个。第二个玩家将选择剩下的八个格子中的一个。然后第一个玩家可以选择剩下的七个格子中的一个。这个过程一直持续,直到任一玩家获胜,或者第一个玩家被迫进行第九步最后一步。

  • 因此,可能的决策序列数为 9! = 362,880。由于玩家可能在少于九步的情况下获胜,所以其中一些序列是无效的。至少需要五步才能获胜,因为第一个玩家需要三次移动。

  • 为了计算状态空间的准确大小,我们需要计算在五步、六步、七步和八步内获胜的游戏数量。这个计算很简单,但由于其蛮力性质,超出了我们的讨论范围。因此,我们将专注于状态空间的数量级。

    注意

    在生成所有可能的井字游戏后,研究人员统计了 255,168 种可能的游戏。在这些游戏中,131,184 场由第一玩家获胜,77,904 场由第二玩家获胜,46,080 场以平局结束。请访问www.half-real.net/tictactoe/allgamesoftictactoe.zip下载所有可能的井字游戏。

即使是简单的井字游戏也有很多状态。试想一下,探索所有可能的象棋游戏会有多难。因此,我们可以得出结论,暴力搜索很少是理想的选择。

练习 1.02:为井字游戏创建一个具有随机行为的 AI

在这个练习中,我们将为井字游戏创建一个实验框架。我们将假设 AI 玩家总是先开始游戏。您将创建一个打印内部表示的函数,允许对手随机输入一个移动,并判断玩家是否获胜。

注意

为确保此操作正确进行,您需要完成前面的练习。

以下步骤将帮助您完成此练习:

  1. 首先,打开一个新的 Jupyter Notebook 文件。

  2. 我们将从random库中导入choice函数:

    from random import choice
    
  3. 现在,为了简化问题,将九个单元格建模为一个简单的字符串。一个九个字符长的 Python 字符串按以下顺序存储这些单元格:"123456789"。我们来确定必须包含匹配符号的索引三元组,以便玩家获胜:

    combo_indices = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], \
                     [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]]
    
  4. 定义空单元格、AI 和对手玩家的符号常量:

    EMPTY_SIGN = '.'
    AI_SIGN = 'X'
    OPPONENT_SIGN = 'O'
    

    在前面的代码片段中,我们为 AI 和玩家分配了不同的符号。

  5. 创建一个打印棋盘的函数。我们将在棋盘的前后添加一个空行,以便更容易地读取游戏状态:

    def print_board(board):
        print(" ")
        print(' '.join(board[:3]))
        print(' '.join(board[3:6]))
        print(' '.join(board[6:]))
        print(" ")
    
  6. 描述人类玩家的移动。输入参数是棋盘、从13的行号和从13的列号。该函数的返回值是包含新移动的棋盘:

    def opponent_move(board, row, column):
        index = 3 * (row - 1) + (column - 1)
        if board[index] == EMPTY_SIGN:
            return board[:index] + OPPONENT_SIGN + board[index+1:]
        return board
    

    在这里,我们定义了一个名为opponent_move的函数,它将帮助我们根据输入(行和列)计算棋盘上的索引。您将能够看到在棋盘上的结果位置。

  7. 现在,我们需要为 AI 玩家定义一个随机移动。我们将使用all_moves_from_board函数生成所有可能的移动,然后从可能的移动列表中选择一个随机移动:

    def all_moves_from_board(board, sign):
        move_list = []
        for i, v in enumerate(board):
            if v == EMPTY_SIGN:
                move_list.append(board[:i] + sign + board[i+1:])
        return move_list
    def ai_move(board):
        return choice(all_moves_from_board(board, AI_SIGN))
    

    在前面的代码片段中,我们定义了一个名为all_moves_from_board的函数,它遍历棋盘上的所有索引并检查它们是否为空(v == EMPTY_SIGN)。如果是这样,这意味着该位置可以进行移动,并且该索引已添加到移动列表(move_list)中。最后,我们定义了ai_move函数,以便 AI 随机选择一个与游戏中的移动相等的索引。

  8. 确定玩家是否获胜:

    def game_won_by(board):
        for index in combo_indices:
            if board[index[0]] == board[index[1]] == \
               board[index[2]] != EMPTY_SIGN:
                return board[index[0]]
        return EMPTY_SIGN
    

    在前面的代码片段中,我们定义了game_won_by函数,该函数检查棋盘上是否包含来自combo_indices变量的三个相同索引的组合,从而结束游戏。

  9. 最后,创建一个游戏循环,以便我们测试计算机玩家和人类玩家之间的互动。在接下来的示例中,我们将进行暴力搜索:

    def game_loop():
        board = EMPTY_SIGN * 9
        empty_cell_count = 9
        is_game_ended = False
        while empty_cell_count > 0 and not is_game_ended:
            if empty_cell_count % 2 == 1:
                board = ai_move(board)
            else:
                row = int(input('Enter row: '))
                col = int(input('Enter column: '))
                board = opponent_move(board, row, col)
            print_board(board)
            is_game_ended = game_won_by(board) != EMPTY_SIGN
            empty_cell_count = sum(1 for cell in board \
                                   if cell == EMPTY_SIGN)
        print('Game has been ended.')
    

    在前面的代码片段中,我们定义了函数,可以将其分解为不同部分。

    第一部分是初始化棋盘并填充空标记(board = EMPTY_SIGN * 9)。然后,我们创建一个空格计数器,这将帮助我们创建一个循环并确定 AI 的回合。

    第二部分是创建一个函数,让玩家和 AI 引擎相互对战。一旦某一玩家进行移动,empty_cell_count变量将减少 1。循环将继续,直到game_won_by函数找到胜者,或棋盘上没有更多可能的移动。

  10. 使用game_loop函数来运行游戏:

    game_loop()
    

    期望的输出(部分展示)是:

    图 1.8:游戏的最终输出(部分展示)

图 1.8:游戏的最终输出(部分展示)

注意

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

你也可以在packt.live/3hVzjcT上运行这个示例。你必须执行整个 Notebook,才能得到期望的结果。

完成这个练习后,你会发现,即使是一个随机游戏的对手,如果其对手犯了错误,也有可能会偶尔获胜。

活动 1.01:生成井字游戏中的所有可能步骤序列

本活动将探索当两名玩家随机游戏时可能出现的组合爆炸。我们将使用一个程序,基于之前的结果,生成计算机玩家和人类玩家之间所有可能的移动序列。

假设人类玩家可以做任何可能的移动。在这个例子中,由于计算机玩家是随机移动的,我们将检查两个随机玩家之间的胜负和平局:

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

  1. 重用前一个练习 1.02步骤 2-9的所有函数代码,创建一个随机行为的井字游戏 AI

  2. 创建一个函数,将all_moves_from_board函数应用于棋盘空间/方格列表中的每个元素。通过这种方式,我们将得到决策树的所有节点。决策树从[ EMPTY_SIGN * 9 ]开始,并在每次移动后扩展。

  3. 创建一个filter_wins函数,将已完成的游戏从移动列表中移除,并将它们追加到包含 AI 玩家和对手玩家获胜状态的数组中。

  4. 创建一个count_possibilities函数,打印并返回以平局结束、第一玩家获胜和第二玩家获胜的决策树叶子数:

  5. 每个状态下我们最多有九个步骤。在第 0、2、4、6 和 8 次迭代中,AI 玩家移动。在所有其他迭代中,对手玩家移动。我们创建所有步骤中的所有可能移动,并从移动列表中移除已完成的游戏。

  6. 最后,执行可能性的数量以体验组合爆炸。

期望的输出是这个:

step 0\. Moves: 1
step 1\. Moves: 9
step 2\. Moves: 72
step 3\. Moves: 504
step 4\. Moves: 3024
step 5\. Moves: 13680
step 6\. Moves: 49402
step 7\. Moves: 111109
step 8\. Moves: 156775
First player wins: 106279
Second player wins: 68644
Draw 91150
Total 266073

注意

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

到目前为止,我们已经理解了智能代理的意义。我们还检查了游戏 AI 的游戏状态。现在,我们将专注于如何为代理创建和引入智能。

我们将研究如何减少状态空间中的状态数量,分析游戏棋盘可能经历的阶段,并使环境以一种我们能够获胜的方式运作。

看一下以下练习,我们将在其中教导智能代理获胜。

练习 1.03:教代理获胜

在本练习中,我们将看到如何减少获胜所需的步骤。我们将让我们在前一节中开发的代理检测能够获胜的游戏局面。

以下步骤将帮助你完成此练习:

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 重用步骤 2–6活动 1的前面代码,生成井字游戏中所有可能的步骤序列

  3. 定义两个函数,ai_moveall_moves_from_board

    我们创建了ai_move,使其返回一个考虑自身先前走法的步骤。如果在该步中游戏可以获胜,ai_move将选择该步:

    def ai_move(board):
        new_boards = all_moves_from_board(board, AI_SIGN)
        for new_board in new_boards:
            if game_won_by(new_board) == AI_SIGN:
                return new_board
        return choice(new_boards)
    

    在前面的代码片段中,我们定义了ai_move函数,它将在适用的情况下从当前游戏状态下的所有可能走法中选择一个获胜的走法。如果没有,它仍然会选择一个随机的走法。

  4. 接下来,使用游戏循环测试代码片段。每当 AI 有机会赢得游戏时,它总是会将X放在正确的格子里:

    game_loop()
    

    期望的输出是这个:

    图 1.9:代理获胜

    图 1.9:代理获胜

  5. 现在,计算所有可能的走法。为此,我们必须修改all_moves_from_board函数以包括这一改进。我们必须这么做,以便如果游戏被AI_SIGN获胜,它将返回该值:

    def all_moves_from_board(board, sign):
        move_list = []
        for i, v in enumerate(board):
            if v == EMPTY_SIGN:
                new_board = board[:i] + sign + board[i+1:]
                move_list.append(new_board)
                if game_won_by(new_board) == AI_SIGN:
                    return [new_board]
        return move_list
    

    在前面的代码片段中,我们定义了一个生成所有可能走法的函数。一旦我们找到一个能让 AI 获胜的步骤,我们就返回它。我们不在乎 AI 是否有多个选项可以在一步内获胜——我们只是返回第一个可能性。如果 AI 无法获胜,我们返回所有可能的走法。让我们看看在每一步中计算所有可能性的意义。

  6. 输入以下函数来查找所有可能性。

    first_player, second_player, \
    draw, total = count_possibilities()
    

    期望的输出是这个:

    step 0\. Moves: 1
     step 1\. Moves: 9
     step 2\. Moves: 72
     step 3\. Moves: 504
     step 4\. Moves: 3024
     step 5\. Moves: 8525
     step 6\. Moves: 28612
     step 7\. Moves: 42187
     step 8\. Moves: 55888
     First player wins: 32395
     Second player wins: 23445
     Draw 35544
     Total 91384
    

    注意

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

    你也可以在线运行这个示例,访问packt.live/2YnLpDS。你必须执行整个 Notebook 才能获得期望的结果。

通过这一点,我们已经看到 AI 在大多数情况下仍然无法获胜。这意味着我们需要引入更多的概念来增强 AI 的能力。为了教会 AI 如何获胜,我们需要教它如何进行防守性移动,以避免失败。

防守 AI 免受失败

在下一个活动中,我们将使 AI 电脑玩家的表现优于之前的练习,以减少状态空间和失败次数。

活动 1.02:教代理识别防守失败的情境

在这个活动中,我们将强制电脑防守,以防玩家在行、列或对角线上放置第三个标记:

  1. 重用之前的步骤 2–6中的所有代码,练习 1.03教代理获胜

  2. 创建一个名为player_can_win的函数,该函数通过all_moves_from_board函数获取棋盘上的所有移动,并通过一个名为next_move的变量遍历它。在每次迭代中,检查该标记是否能赢得比赛,然后返回truefalse

  3. 扩展 AI 的移动,使其倾向于选择安全的移动。如果对方在下一步无法赢得游戏,那么该移动就是安全的。

  4. 测试新应用程序。你会发现 AI 做出了正确的移动。

  5. 将这一逻辑放入状态空间生成器,并通过生成所有可能的游戏,检查电脑玩家的表现。

预期输出如下:

step 0\. Moves: 1
step 1\. Moves: 9
step 2\. Moves: 72
step 3\. Moves: 504
step 4\. Moves: 3024
step 5\. Moves: 5197
step 6\. Moves: 18606
step 7\. Moves: 19592
step 8\. Moves: 30936
First player wins: 20843
Second player wins: 962
Draw 20243
Total 42048

注意

该活动的解决方案可以在第 325 页找到。

一旦我们完成这个活动,我们会注意到,尽管我们努力提高 AI 的能力,它仍然能以962种方式输掉比赛。我们将在下一个活动中消除所有这些失败。

活动 1.03:修正 AI 的第一步和第二步,使其变得无敌

在这个活动中,我们将结合之前的活动,教 AI 如何识别胜利和失败,这样它就可以集中精力寻找比其他移动更有用的移动。我们将通过硬编码第一步和第二步来减少可能的游戏数:

  1. 重用之前的步骤 2–4中的所有代码,活动 1.02教代理识别防守失败的情境

  2. 计算棋盘上空白格的数量,并在空白格有 9 个或 7 个时进行硬编码的移动。你可以尝试不同的硬编码移动。

  3. 占据任何一个角落,然后占据对角的另一个角落,可以避免失败。如果对方占据了对角的另一个角落,那么在中间下棋也不会失败。

  4. 修正前两步后,我们只需要处理 8 种可能性,而不是 504 种。我们还需要引导 AI 进入一个状态,在这个状态下,硬编码规则足够使其永远不会输掉游戏。

预期输出如下:

step 0\. Moves: 1
step 1\. Moves: 1
step 2\. Moves: 8
step 3\. Moves: 8
step 4\. Moves: 48
step 5\. Moves: 38
step 6\. Moves: 108
step 7\. Moves: 76
step 8\. Moves: 90
First player wins: 128
Second player wins: 0
Draw 60
Total 188

注意

该活动的解决方案可以在第 328 页找到。

让我们总结一下到目前为止我们应用的减少状态空间的重要技巧:

  • 经验性简化:我们接受了最佳的第一步是角落移动。我们直接硬编码了一个移动,而不是考虑其他可能的选择,以便集中精力关注游戏的其他方面。在更复杂的游戏中,经验性移动往往具有误导性。最著名的国际象棋 AI 胜利通常包含了对国际象棋大师们普遍认知的违反。

  • 对称性:在我们从角落位置开始移动后,我们注意到位置 1、3、7 和 9 在赢得游戏的角度上是等效的。即使我们没有进一步探讨这一点,我们也发现可以旋转棋盘来进一步减少状态空间,并将所有四个角落的移动视为相同的移动。

  • 不同排列的简化导致相同状态:假设我们可以进行 A 或 B 的移动,并假设我们的对手进行了 X 移动,其中 X 既不等于 A 也不等于 B。如果我们探索 A、X、B 的序列,然后开始探索 B、X 的序列,那么我们就不必再考虑 B、X、A 的序列。这是因为这两个序列会导致完全相同的游戏状态,而我们之前已经探索过包含这三步的状态,即 A、X 和 B。序列的顺序并不重要,因为它们会导致相同的结果。这使我们能够显著减少可能的移动数量。

  • 玩家的强制性移动:当玩家在水平方向、垂直方向或对角线方向上收集了两个符号,并且该行的第三个格子为空时,我们被迫占据该空格,无论是为了赢得游戏,还是为了防止对手赢得游戏。强制性移动可能会暗示其他强制性移动,从而进一步减少状态空间。

  • 对手的强制性移动:当对手的某一移动显然是最佳时,考虑对手不做最佳移动的情形就没有意义。如果对手通过占据某个格子可以赢得游戏,我们就不需要在对手错失最佳移动时进行长时间的探索。通过不探索对手未能阻止我们赢得游戏的情况,我们节省的时间非常有限。这是因为在对手犯错后,我们会直接赢得游戏。

  • 随机移动:当我们无法决定且没有能力进行搜索时,我们会随机移动。随机移动几乎总是不如基于搜索的推测,但有时我们别无选择。

启发式方法

在本节中,我们将通过定义和应用启发式方法来规范化信息化搜索技术,以引导我们的搜索。接下来的章节中,我们将探讨启发式方法并进行创建。

无信息和有信息搜索

在井字棋的例子中,我们实现了一个贪婪算法,首先专注于赢得比赛,然后专注于避免输掉比赛。当谈到立即赢得游戏时,贪婪算法是最优的,因为没有比赢得游戏更好的步骤。至于避免输掉比赛,关键在于我们如何避免失败。我们的算法仅仅选择一个随机的安全移动,而不考虑我们创造了多少个赢的机会。

广度优先搜索(BFS)和深度优先搜索(DFS)是无信息搜索的一部分,因为它们考虑了游戏中的所有可能状态,这可能非常耗时。另一方面,启发式信息搜索将智能地探索可用状态的空间,以更快地到达目标。

创建启发式方法

如果我们想做出更好的决策,可以应用启发式方法,通过考虑长期收益来指导搜索朝着正确的方向前进。通过这种方式,我们可以根据未来可能发生的事情,在当前做出更有信息的决策。这也有助于我们更快地解决问题。

我们可以按如下方式构建启发式方法:

  • 从在游戏中做出移动的效用来看

  • 从玩家的角度来看,给定游戏状态的效用

  • 从距离目标的远近来看

启发式方法是根据它们的效用评估游戏状态或转移到新游戏状态的函数。启发式方法是使搜索问题具备信息的基石。

在本书中,我们将效用和成本作为对立的术语来使用。最大化效用和最小化移动成本被视为同义词。

启发式评估函数的一个常见示例出现在路径寻找问题中。假设我们想要到达一个目标或目的地。每一步都有一个与之相关的成本,表示旅行的距离。我们的目标是最小化到达目的地或目标的成本(即最小化旅行距离)。

解决此路径寻找问题的一个启发式评估示例是,取当前状态(位置)和目标(目的地)之间的坐标,计算这两点之间的距离。两点之间的距离是连接这两点的直线的长度。这个启发式方法被称为欧几里得距离(如图 1.10所示)。

现在,假设我们将路径寻找问题定义为一个迷宫,其中我们只能向上、向下、向左或向右移动。迷宫中有一些障碍物阻挡我们的移动,因此使用欧几里得距离并不理想。一个更好的启发式方法是使用曼哈顿距离,可以定义为当前状态和目标之间的水平和垂直距离之和。

可接受和不可接受的启发式方法

我们刚才定义的关于路径寻找问题的两个启发式方法,在其给定问题领域中被称为可接受启发式方法。

可接受性意味着我们可能低估了达到终局状态的成本,但从不高估它。稍后我们将探讨一种算法,找出当前状态和目标状态之间的最短路径。这个算法的最优性质取决于我们能否定义一个可接受的启发式函数。

一个非可接受启发式的例子就是应用于现实地图的欧几里得距离。

想象一下,我们想从曼哈顿的 A 点移动到 B 点。这里,欧几里得距离是两点之间的直线距离,但正如我们所知,在像曼哈顿这样的城市中,我们不可能直接走直线(除非我们能飞)。在这种情况下,欧几里得距离低估了到达目标的成本。一个更好的启发式方法应该是曼哈顿距离:

图 1.10:曼哈顿市中的欧几里得(蓝线)距离和曼哈顿(红线)距离

图 1.10:曼哈顿市中的欧几里得距离(蓝线)和曼哈顿距离(红线)

注意

上面的曼哈顿地图来源于 Google 地图。

由于我们高估了从当前节点到目标的旅行成本,当我们不能进行对角线移动时,欧几里得距离就不再适用了。

启发式评估

我们可以从起始玩家的角度,通过定义一步的效用值,为我们的井字游戏状态创建一个启发式评估。

启发式 1:简单的残局评估

我们来定义一个简单的启发式方法,通过评估棋盘。我们可以将游戏的效用值设为以下之一:

  • 如果状态表明 AI 玩家将赢得游戏:+1。

  • 如果状态表明 AI 玩家将输掉游戏:-1。

  • 如果已经平局或无法从当前状态明确识别出赢家:0。

这个启发式方法很简单,因为任何人都可以看着棋盘分析一个玩家是否即将获胜。

这个启发式的效用值取决于我们能否提前进行多步操作。请注意,我们甚至在五步内都无法赢得比赛。在活动 1.01生成所有可能的井字游戏步骤序列中,我们看到到达第五步时,我们已经有 13,680 种可能的组合。在这 13,680 种情况中,由于我们尚未能识别出明确的赢家,我们的启发式方法返回的值为零。

如果我们的算法只看这五步,那么我们对于如何开始游戏完全没有头绪。因此,我们应该发明一个更好的启发式方法。

启发式 2:一步的效用

我们来改变游戏的效用值,如下所示:

  • 如果在一行、列或对角线中有两个 AI 标志,且第三个格子为空:为空格子加+1000。

  • 如果对手在一行、列或对角线中有两个标志,且第三个格子为空:为空格子加+100。

  • 如果在一行、列或对角线中有一个 AI 标志,另外两个格子为空:为空格子加+10。

  • 如果在一行、列或对角线中没有 AI 或对手的标志:为空格子加+1。

  • 被占用的单元格将获得负无穷大值。实际上,由于规则的性质,-1 也足够了。

为什么我们将前面三个规则与第四个规则的乘数因子设为 10?我们这样做是因为有八种可能的方式可以形成三连线、三列或对角线。所以,即使我们对游戏一无所知,我们也能确定较低级别的规则不会覆盖更高级别的规则。换句话说,如果我们能赢得比赛,我们就永远不会防守对手的棋步。

注意

由于我们的对手的任务也是赢得比赛,我们可以从对手的角度计算这个启发式函数。我们的任务是最大化这个值,从而防止对手做出最优的棋步。这也是 Minmax 算法背后的理念,稍后在本章会介绍。如果我们想将这个启发式函数转换为描述当前棋盘的函数,我们可以计算所有开放单元格的启发式值,并取出 AI 角色的最大值,这样我们就可以最大化我们的效用。

对于每个棋盘,我们将创建一个效用矩阵。

例如,考虑以下棋盘,其中 O 代表玩家,X 代表 AI:

图 1.11:井字游戏状态

图 1.11:井字游戏状态

从这里,我们可以构建其效用矩阵,如下图所示:

图 1.12:井字游戏效用矩阵

图 1.12:井字游戏效用矩阵

在第二行,左侧单元格如果选择了它将没有益处。请注意,如果我们有一个更优的效用函数,我们会奖励阻挡对手的棋步。

第三列的两个单元格因为两连线而分别获得10分的加成。

右上角的单元格也因为防御对手的对角线而获得100分。

从这个矩阵可以明显看出,我们应该选择右上角的移动。在游戏的任何阶段,我们都能够定义每个单元格的效用;这是一种静态的启发式函数评估。

我们可以使用这个启发式函数来指导我们做出最优的下一步棋,或者通过取这些值的最大值来更有根据地评分当前棋盘。实际上,我们已经以硬编码规则的形式使用了部分启发式函数。不过请注意,启发式函数的真正作用不是对棋盘进行静态评估,而是为限制搜索空间提供指导。

练习 1.04:使用启发式函数进行井字游戏静态评估

在这个练习中,您将使用启发式函数对井字游戏进行静态评估。

以下步骤将帮助您完成此练习:

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 重用 步骤 2-6 中的代码,活动 1.01:生成井字游戏的所有可能步骤序列

  3. 创建一个函数,该函数以棋盘为输入,如果单元格为空则返回0,如果单元格不为空则返回-1

    def init_utility_matrix(board):
        return [0 if cell == EMPTY_SIGN \
                else -1 for cell in board]
    
  4. 接下来,创建一个函数,它接受可能走法的效用向量,取效用向量中的三个索引表示一个三元组,并返回一个函数,如以下代码片段所示:

    def generate_add_score(utilities, i, j, k):
        def add_score(points):
            if utilities[i] >= 0:
                utilities[i] += points
            if utilities[j] >= 0:
                utilities[j] += points
            if utilities[k] >= 0:
                utilities[k] += points
        return add_score
    

    在前面的代码片段中,返回的函数将期望一个points参数和utilities向量作为输入,并将分数添加到(ijk)的每个单元格,只要该单元格的原始值非负(>=0)。换句话说,我们仅增加了空白单元格的效用。

  5. 现在,创建属于任意棋盘配置的效用矩阵,你将添加先前定义的generate_add_score函数来更新分数。你还将实现我们在本活动之前讨论的规则。具体规则如下:

    两个 AI 标记在行、列或对角线连续出现,且第三个单元格为空:空白单元格得+1000 分。

    对手在行、列或对角线连续出现两个标记,且第三个单元格为空:空白单元格得+100 分。

    一个 AI 标记在行、列或对角线连续出现,另外两个单元格为空:空白单元格得+10 分。

    没有 AI 或对手在行、列或对角线连续出现:空白单元格得+1 分。

    现在让我们创建效用矩阵:

    def utility_matrix(board):
        utilities = init_utility_matrix(board)
        for [i, j, k] in combo_indices:
            add_score = generate_add_score(utilities, i, j, k)
            triple = [board[i], board[j], board[k]]
            if triple.count(EMPTY_SIGN) == 1:
                if triple.count(AI_SIGN) == 2:
                    add_score(1000)
                elif triple.count(OPPONENT_SIGN) == 2:
                    add_score(100)
            elif triple.count(EMPTY_SIGN) == 2 and \
                              triple.count(AI_SIGN) == 1:
                add_score(10)
            elif triple.count(EMPTY_SIGN) == 3:
                add_score(1)
        return utilities
    
  6. 创建一个函数,选择具有最高效用值的走法。如果多个走法具有相同效用,该函数返回这两个走法:

    def best_moves_from_board(board, sign):
        move_list = []
        utilities = utility_matrix(board)
        max_utility = max(utilities)
        for i, v in enumerate(board):
            if utilities[i] == max_utility:
                move_list.append(board[:i] \
                                 + sign \
                                 + board[i+1:])
        return move_list
    def all_moves_from_board_list(board_list, sign):
        move_list = []
        get_moves = best_moves_from_board if sign \
                    == AI_SIGN else all_moves_from_board
        for board in board_list:
            move_list.extend(get_moves(board, sign))
        return move_list
    
  7. 现在,运行应用程序,如以下代码片段所示:

    first_player, second_player, \
    draw, total = count_possibilities()
    

    预期输出如下:

    step 0\. Moves: 1
    step 1\. Moves: 1
    step 2\. Moves: 8
    step 3\. Moves: 24
    step 4\. Moves: 144
    step 5\. Moves: 83
    step 6\. Moves: 214
    step 7\. Moves: 148
    step 8\. Moves: 172
    First player wins: 504
    Second player wins: 12
    Draw 91
    Total 607
    

    注意

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

    你也可以在网上运行此示例:packt.live/2YnyO3K。你必须执行整个笔记本才能得到预期的结果。

通过完成此练习,我们观察到 AI 的表现较我们之前的活动 Activity 1.03固定 AI 的第一步和第二步,使其无敌)有所下降。在这种情况下,将前两步硬编码比设置启发式方法更好,但这是因为我们还没有正确设置启发式方法。

使用启发式方法进行有指导的搜索

我们尚未真正体验到启发式方法的强大,因为我们在没有了解未来走法效果的情况下做出了决策,从而影响了对手的合理玩法。

因此,更准确的启发式方法比单纯将前两步固定的做法带来了更多的失败。请注意,在前一部分中,我们是根据通过运行具有固定前两步的游戏所生成的统计数据来选择这两步的。这种方法本质上就是启发式搜索应该实现的目标。

启发式方法的类型

静态评估无法与生成数十万种未来状态并选择最大化奖励的玩法竞争。这是因为我们的启发式方法并不精确,且很可能也不可接受。

我们在前面的练习中看到,启发式并不总是最优的。我们提出了规则,使得 AI 总是赢得游戏或以平局结束。这些启发式让 AI 很频繁地获胜,但也以少数几次失败为代价。启发式被称为可接受的,当我们低估了游戏状态的效用,但从未高估它。

在井字游戏的例子中,我们可能高估了某些游戏状态的效用,为什么呢?因为我们最终输掉了 12 次。那些导致失败的游戏状态具有最高的启发式得分。为了证明我们的启发式不是可接受的,我们需要做的就是找到一个我们忽略的潜在获胜游戏状态,而这个状态在选择导致失败的游戏状态时被忽视。

还有两个描述启发式的特征,即:最优和完整:

  • 最优启发式始终能够找到最佳的解决方案。

  • 完整的启发式有两个定义,取决于我们如何定义问题领域。从宽泛的意义上讲,如果启发式始终能够找到一个解,它就被称为完整的。严格来说,如果启发式能够找到所有可能的解,那么它才是完整的。我们的井字游戏启发式并不完整,因为我们故意忽略了许多可能的获胜状态,偏向选择了一个失败状态。

正如你所看到的,定义准确的启发式需要很多细节和思考,才能获得一个完美的 AI 智能体。如果你没有正确估计游戏状态中的效用,那么你可能会得到一个表现不佳的硬编码规则的 AI。

在下一节中,我们将探讨一种更好的方法,用于执行当前状态与目标状态之间的最短路径寻找。

使用 A* 算法进行路径寻找

在前两节中,我们学习了如何定义智能体,以及如何创建引导智能体走向期望状态的启发式。我们了解到,这种方法并不完美,因为有时我们会偏向忽略一些获胜状态,而选择一些失败状态。

现在,我们将学习一种结构化且最优的方法,以便使用 A(“A star”而不是“A asterisk*”)算法执行搜索,寻找当前状态与目标状态之间的最短路径。

请查看以下图像:

图 1.13:在迷宫中寻找最短路径

图 1.13:在迷宫中寻找最短路径

对于人类来说,只需通过查看图像就可以轻松找到最短路径。我们可以得出结论,最短路径有两个潜在候选:第一条路线从上方开始,第二条路线从左侧开始。然而,AI 并不了解这些选择。事实上,对于计算机玩家来说,最合理的第一步是移动到下图中由数字 3 标示的方格。

为什么?因为这是唯一一个能够减少起始状态与目标状态之间距离的步骤。所有其他步骤最初都会远离目标状态:

图 1.14:带有工具的最短路径寻找游戏板

图 1.14:带有工具的最短路径寻找游戏板

在下一个练习中,我们将看到 BFS 算法在路径寻找问题中的表现,然后再向你介绍 A* 算法。

练习 1.05:使用 BFS 寻找最短路径

在这个练习中,我们将使用 BFS 算法找到到达目标的最短路径。

以下步骤将帮助你完成这个练习:

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 首先导入 math 库:

    import math
    
  3. 接下来,使用 Python 描述棋盘、初始状态和最终状态。创建一个返回可能后继状态的函数。使用元组,其中第一个坐标表示行号(从 17),第二个坐标表示列号(从 19):

    size = (7, 9)
    start = (5, 3)
    end = (6, 9)
    obstacles = {(3, 4), (3, 5), (3, 6), (3, 7), (3, 8), \
                 (4, 5), (5, 5), (5, 7), (5, 9), (6, 2), \
                 (6, 3), (6, 4), (6, 5), (6, 7),(7, 7)}
    
  4. 接下来,使用数组推导生成后继状态,如下代码所示:

    def successors(state, visited_nodes):
        (row, col) = state
        (max_row, max_col) = size
        succ_states = []
        if row > 1:
            succ_states += [(row-1, col)]
        if col > 1:
            succ_states += [(row, col-1)]
        if row < max_row:
            succ_states += [(row+1, col)]
        if col < max_col:
            succ_states += [(row, col+1)]
        return [s for s in succ_states if s not in \
                visited_nodes if s not in obstacles]
    

    该函数生成当前区域所有可能的移动,前提是这些移动不会被障碍物阻塞。我们还添加了一个过滤器,以排除返回到我们已经访问过的区域的移动,从而避免无限循环。

  5. 接下来,实现初始代价,如下代码片段所示:

    def initialize_costs(size, start):
        (h, w) = size
        costs = [[math.inf] * w for i in range(h)]
        (x, y) = start
        costs[x-1][y-1] = 0
        return costs
    
  6. 现在,使用 costscurrent_nodesuccessor_node 实现更新后的代价:

    def update_costs(costs, current_node, successor_nodes):
        new_cost = costs[current_node[0]-1]\
                   [current_node[1]-1] + 1
        for (x, y) in successor_nodes:
            costs[x-1][y-1] = min(costs[x-1][y-1], new_cost)
    
  7. 最后,实现 BFS 算法来搜索树的状态,并将结果保存在名为 bfs 的变量中:

    def bfs_tree(node):
        nodes_to_visit = [node]
        visited_nodes = []
        costs = initialize_costs(size, start)
        while len(nodes_to_visit) > 0:
            current_node = nodes_to_visit.pop(0)
            visited_nodes.append(current_node)
            successor_nodes = successors(current_node, \
                                         visited_nodes)
            update_costs(costs, current_node, successor_nodes)
            nodes_to_visit.extend(successor_nodes)
        return costs
    bfs = bfs_tree(start)
    bfs
    

    在前面的代码片段中,我们重新使用了本书 广度优先搜索 部分中我们已经看过的 bfs_tree 函数。但我们添加了 update_costs 函数来更新代价。

    期望的输出是这样的:

    [[6, 5, 4, 5, 6, 7, 8, 9, 10],
     [5, 4, 3, 4, 5, 6, 7, 8, 9],
     [4, 3, 2, inf, inf, inf, inf, inf, 10],
     [3, 2, 1, 2, inf, 12, 13, 12, 11],
     [2, 1, 0, 1, inf, 11, inf, 13, inf],
     [3, inf, inf, inf, inf, 10, inf, 14, 15],
     [4, 5, 6, 7, 8, 9, inf, 15, 16]]
    

    在这里,你可以看到一个简单的 BFS 算法成功地确定了从起始节点到任何节点(包括目标节点)的代价。

  8. 现在,测量找到目标节点所需的步骤数,并将结果保存在bfs_v变量中,如下代码片段所示:

    def bfs_tree_verbose(node):
        nodes_to_visit = [node]
        visited_nodes = []
        costs = initialize_costs(size, start)
        step_counter = 0
        while len(nodes_to_visit) > 0:
            step_counter += 1
            current_node = nodes_to_visit.pop(0)
            visited_nodes.append(current_node)
            successor_nodes = successors(current_node, \
                                         visited_nodes)
            update_costs(costs, current_node, successor_nodes)
            nodes_to_visit.extend(successor_nodes)
            if current_node == end:
                print('End node has been reached in ', \
                      step_counter, ' steps')
                return costs
        return costs
    bfs_v = bfs_tree_verbose(start)
    bfs_v
    

    在前面的代码片段中,我们添加了一个步骤计数器变量,以便在搜索结束时打印步数。

    期望的输出是这样的:

    End node has been reached in 110 steps
    [[6, 5, 4, 5, 6, 7, 8, 9, 10],
     [5, 4, 3, 4, 5, 6, 7, 8, 9],
     [4, 3, 2, inf, inf, inf, inf, inf, 10],
     [3, 2, 1, 2, inf, 12, 13, 12, 11],
     [2, 1, 0, 1, inf, 11, inf, 13, inf],
     [3, inf, inf, inf, inf, 10, inf, 14, 15],
     [4, 5, 6, 7, 8, 9, inf, 15, 16]]
    

    注意

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

    你也可以在packt.live/3duuLqp上在线运行这个示例。你必须执行整个 Notebook 才能得到预期结果。

在这个练习中,我们使用了 BFS 算法来找到最短路径。BFS 用了110步到达目标。现在,我们将学习一个可以从起始节点找到目标节点最短路径的算法:A* 算法。

介绍 A* 算法

A* 是一个完整且最优的启发式搜索算法,能够找到当前游戏状态到获胜状态之间的最短路径。在这种状态下,完整性和最优性的定义如下:

  • 完整性意味着 A* 总是能找到一个解决方案。

  • 最优意味着 A*会找到最佳解。

要设置 A*算法,我们需要以下内容:

  • 初始状态

  • 目标状态的描述

  • 用于衡量向目标状态推进的可接受启发式方法

  • 生成通向目标的下一步方式

设置完成后,我们将使用以下步骤在初始状态下执行 A*算法:

  1. 我们生成所有可能的下一步。

  2. 我们按距离目标的远近顺序存储这些子节点。

  3. 我们首先选择得分最好的子节点,然后对这个子节点应用这三步,将其作为初始状态。这样可以得到从起点到某个节点的最短路径。

例如,以下图形:

图 1.15:带有启发式距离的树

图 1.15:带有启发式距离的树

第一步是生成所有从起点A开始的可能移动方式,分别是从AB (A,B)或到C (A,C)

第二步是使用启发式值(距离)来排序两个可能的移动,(A,B)的启发式值为10,而(A,C)的启发式值为100,显然(A,B)较短。

第三步是选择最短的启发式值,(A,B),并移动到B

现在,我们将以B为起点重复相同的步骤。

最终,我们将通过路径(A,B,D,F)到达目标F,其累计启发式值为 24。如果我们选择另一条路径,例如(A,B,E,F),其累计启发式值为 30,明显高于最短路径。

我们甚至没有考虑到(A,C,F),因为它已经远远超出了最短路径。

在路径寻找中,一个好的启发式方法是欧几里得距离。如果当前节点为(x, y),目标节点为(u, v),则我们有以下公式:

distance_from_end( node ) = sqrt( abs( x – u ) ** 2 + abs( y – v ) ** 2 )

在这里,distance_from_end(node)是一个可接受的启发式估算,表示当前节点距离目标节点的距离。

我们还需要考虑以下内容:

  • sqrt是平方根函数。别忘了从math库导入它。

  • abs是绝对值函数,即abs( -2 ) = abs( 2 ) = 2

  • x ** 2表示x的平方。

我们将使用distance_from_start矩阵来存储从起点到各个节点的距离。在算法中,我们会将这个成本矩阵称为distance_from_start(n1)。对于任何节点n1,其坐标为(x1, y1),该距离等同于distance_from_start[x1][y1]

我们将使用succ(n)符号来生成从n出发的后继节点列表。

注意

""" ) shown in the code snippet below are used to denote the start and end points of a multi-line code comment. Comments are added into code to help explain specific bits of logic.

看一下算法的伪代码:

frontier = [start], internal = {}
# Initialize the costs matrix with each cell set to infinity.
# Set the value of distance_from_start(start) to 0\. 
while frontier is not empty: 
    """
    notice n has the lowest estimated total 
    distance between start and end.
    """
    n = frontier.pop()
    # We'll learn later how to reconstruct the shortest path
    if n == end: 
        return the shortest path. 
    internal.add(n) 
    for successor s in succ(n): 
        if s in internal: 
            continue # The node was already examined
        new_distance = distance_from_start(n) + distance(n, s) 
        if new_distance >= distance_from_start(s): 
            """
            This path is not better than the path we have 
            already examined.
            """
            continue 
        if s is a member of frontier:
            update the priority of s
        else:
            Add s to frontier.

关于最短路径的检索,我们可以使用costs矩阵。该矩阵包含从起点到路径上每个节点的距离。由于在回退时成本总是减少,我们所需要做的就是从终点开始,贪婪地向着成本递减的方向回退:

path = [end_node], distance = get_distance_from_start( end_node )
while the distance of the last element in the path is not 0:
    for each neighbor of the last node in path:
        new_distance = get_distance_from_start( neighbor )
        if new_distance < distance: 
            add neighbor to path, and break out from the for loop
return path

当我们只有一个起始状态和一个目标状态时,A算法表现得尤为出色。A算法的复杂度是O( E ),其中E表示场景中所有可能的边。在我们的示例中,每个节点最多有四条边:上、下、左、右。

注意

要按正确顺序排序边界列表,我们必须使用一种特殊的 Python 数据结构:优先队列。

看看下面的示例:

# Import heapq to access the priority queue
import heapq
# Create a list to store the data
data = []
"""
Use heapq.heappush to push (priorityInt, value) 
pairs to the queue
"""
heapq.heappush(data, (2, 'first item'))
heapq.heappush(data, (1, 'second item'))
"""
The tuples are stored in data in the order 
of ascending priority
"""
[(1, 'second item'), (2, 'first item')]
"""
heapq.heappop pops the item with the lowest score 
from the queue
"""
heapq.heappop(data)

预期的输出是这个:

(1, 'second item')

数据仍然包含第二个项目。如果你输入以下命令,你将能看到它:

data

预期的输出是这个:

[(2, 'first item')]

为什么算法使用的启发式方法是可接受的,这一点很重要?

因为这就是我们确保算法最优性的方式。对于任何节点x,我们在测量从起始节点到x的距离的总和。这是从x到终点节点的估计距离。如果这种估计从未高估x到终点节点的距离,我们就不会高估总距离。一旦到达目标节点,我们的估计就是零,从起点到终点的总距离变为准确值。

我们可以确信我们的解决方案是最优的,因为优先队列中没有其他项具有更低的估算成本。鉴于我们从不高估成本,我们可以确定,算法边界中的所有节点的总成本要么与我们找到的路径相似,要么更高。

在下面的示例中,我们可以看到如何实现 A*算法以找到最低成本的路径:

 图 1.16:最短路径寻路游戏板

图 1.16:最短路径寻路游戏板

我们导入了mathheapq

import math
import heapq

接下来,我们将重用上一个步骤 2–5中的初始化代码,练习 1.05使用 BFS 找到最短路径

注意

我们省略了更新成本的函数,因为我们将在 A*算法中进行更新:

接下来,我们需要初始化 A算法的边界和内部列表。对于frontier,我们将使用 Python 的PriorityQueue。不要直接执行此代码;我们将在 A搜索函数中使用这四行代码:

frontier = []
internal = set()
heapq.heappush(frontier, (0, start))
costs = initialize_costs(size, start)

现在,是时候实现一个启发式函数,用来衡量当前节点与目标节点之间的距离,采用我们在启发式部分看到的算法:

def distance_heuristic(node, goal):
    (x, y) = node
    (u, v) = goal
    return math.sqrt(abs(x - u) ** 2 + abs(y - v) ** 2)

最后的步骤将是将 A*算法转换为可运行的代码:

def astar(start, end):
    frontier = []
    internal = set()
    heapq.heappush(frontier, (0, start))
    costs = initialize_costs(size, start)
    def get_distance_from_start(node):
       return costs[node[0] - 1][node[1] - 1]
    def set_distance_from_start(node, new_distance):
        costs[node[0] - 1][node[1] - 1] = new_distance
    while len(frontier) > 0:
        (priority, node) = heapq.heappop(frontier)
        if node == end:
            return priority
        internal.add(node)
        successor_nodes = successors(node, internal)
        for s in successor_nodes:
            new_distance = get_distance_from_start(node) + 1
            if new_distance < get_distance_from_start(s):
                set_distance_from_start(s, new_distance)
                # Filter previous entries of s
                frontier = [n for n in frontier if s != n[1]]
                heapq.heappush(frontier, \
                              (new_distance \
                              + distance_heuristic(s, end), s))
astar(start, end)

预期的输出是这个:

15.0

我们的实现与原始算法有一些差异。

我们定义了一个distance_from_start函数,使得访问costs矩阵更简便且更具语义。请注意,我们的节点索引从 1 开始,而在矩阵中,索引是从 0 开始的。因此,我们从节点值中减去 1 来获取索引。

在生成后继节点时,我们自动排除了在内部集合中的节点。successors = succ(node, internal)确保我们只得到尚未关闭检查的邻居节点,意味着它们的分数不一定是最优的。

因此,我们可以跳过步骤检查,因为内部节点永远不会出现在succ(n)中。

由于我们使用的是优先队列,我们必须在插入节点之前确定节点的预估优先级。然而,只有当我们知道该节点没有更低分数的条目时,才会将其插入到前沿中。

可能会出现节点已经在前沿队列中,且分数更高的情况。在这种情况下,我们会在将其插入优先队列的正确位置之前,移除这个条目。当我们找到终点节点时,我们只需返回最短路径的长度,而不是路径本身。

为了了解 A*算法的工作过程,请执行以下示例代码并观察日志:

def astar_verbose(start, end):
    frontier = []
    internal = set()
    heapq.heappush(frontier, (0, start))
    costs = initialize_costs(size, start)
    def get_distance_from_start(node):
        return costs[node[0] - 1][node[1] - 1]
    def set_distance_from_start(node, new_distance):
        costs[node[0] - 1][node[1] - 1] = new_distance
    steps = 0
    while len(frontier) > 0:
        steps += 1
        print('step ', steps, '. frontier: ', frontier)
        (priority, node) = heapq.heappop(frontier)
        print('node ', node, \
              'has been popped from frontier with priority', \
              priority)
        if node == end:
            print('Optimal path found. Steps: ', steps)
            print('Costs matrix: ', costs)
            return priority
        internal.add(node)
        successor_nodes = successors(node, internal)
        print('successor_nodes', successor_nodes)
        for s in successor_nodes:
            new_distance = get_distance_from_start(node) + 1
            print('s:', s, 'new distance:', new_distance, \
                  ' old distance:', get_distance_from_start(s))
            if new_distance < get_distance_from_start(s):
                set_distance_from_start(s, new_distance)
                # Filter previous entries of s
                frontier = [n for n in frontier if s != n[1]]
                new_priority = new_distance \
                               + distance_heuristic(s, end)
                heapq.heappush(frontier, (new_priority, s))
                print('Node', s, \
                      'has been pushed to frontier with priority', \
                      new_priority)
    print('Frontier', frontier)
    print('Internal', internal)
    print(costs)
astar_verbose(start, end)

在这里,我们通过重新使用astar函数中的代码并添加打印功能来创建astar_verbose函数,从而生成日志。

预期的输出结果是:

图 1.17:Astar 函数日志

图 1.17:Astar 函数日志

我们已经看到 A*搜索返回了正确的值。问题是,我们如何重建整个路径呢?

为此,我们从代码中删除了print语句以提高清晰度,并继续使用我们在前一步实现的 A算法。我们不再返回最短路径的长度,而是返回路径本身。我们将编写一个函数,通过从终点节点向后遍历并分析costs矩阵来提取这个路径。暂时不要全局定义这个函数,我们将在之前创建的 A算法中将其定义为局部函数:

def get_shortest_path(end_node):
    path = [end_node]
    distance = get_distance_from_start(end_node)
    while distance > 0:
        for neighbor in successors(path[-1], []):
            new_distance = get_distance_from_start(neighbor)
            if new_distance < distance:
                path += [neighbor]
                distance = new_distance
                break  # for
    return path

现在我们已经看到了如何解构路径,让我们把它返回到 A*算法中:

def astar_with_path(start, end):
    frontier = []
    internal = set()
    heapq.heappush(frontier, (0, start))
    costs = initialize_costs(size, start)
    def get_distance_from_start(node):
        return costs[node[0] - 1][node[1] - 1]
    def set_distance_from_start(node, new_distance):
        costs[node[0] - 1][node[1] - 1] = new_distance
    def get_shortest_path(end_node):
        path = [end_node]
        distance = get_distance_from_start(end_node)
        while distance > 0:
            for neighbor in successors(path[-1], []):
                new_distance = get_distance_from_start(neighbor)
                if new_distance < distance:
                    path += [neighbor]
                    distance = new_distance
                    break  # for
        return path
    while len(frontier) > 0:
        (priority, node) = heapq.heappop(frontier)
        if node == end:
            return get_shortest_path(end)
        internal.add(node)
        successor_nodes = successors(node, internal)
        for s in successor_nodes:
            new_distance = get_distance_from_start(node) + 1
            if new_distance < get_distance_from_start(s):
                set_distance_from_start(s, new_distance)
                # Filter previous entries of s
                frontier = [n for n in frontier if s != n[1]]
                heapq.heappush(frontier, \
                              (new_distance \
                              + distance_heuristic(s, end), s))
astar_with_path( start, end )

在前面的代码片段中,我们重新使用了之前定义的a-star函数,主要的不同之处在于添加了get_shortest_path函数。然后,我们使用这个函数替代优先队列,因为我们希望算法总是选择最短的路径。

预期的输出结果是:

图 1.18:显示优先队列的输出

图 1.18:显示优先队列的输出

从技术角度来看,我们不需要从costs矩阵中重建路径。我们可以在矩阵中记录每个节点的父节点,并简单地检索坐标以节省一些查找时间。

我们不指望你理解前面脚本的所有内容,因为它相当高级,所以我们将使用一个简化它的库。

使用 simpleai 库中的 A*搜索实践

simpleai库可在 GitHub 上获取,包含许多流行的 AI 工具和技术。

注意

你可以在此访问该库:github.com/simpleai-team/simpleaisimpleai 库的文档可以在此访问:simpleai.readthedocs.io/en/latest/。要使用 simpleai 库,首先需要安装它。

simpleai 库可以通过以下方式安装:

pip install simpleai

一旦安装了 simpleai,你就可以在 Jupyter Notebook 中导入 simpleai 库的类和函数:

from simpleai.search import SearchProblem, astar

SearchProblem 为定义任何搜索问题提供了框架。astar 导入负责在搜索问题中执行 A* 算法。

为了简化起见,我们在之前的代码示例中没有使用类,以便专注于以简单的旧式风格呈现算法,避免杂乱无章。

注意

请记住,simpleai 库将强制我们使用类。

要描述一个搜索问题,你需要提供以下内容:

  • constructor:初始化状态空间,从而描述问题。我们通过将 SizeStartEndObstacles 作为属性添加到对象中,使这些值可用。在构造函数的末尾,不要忘记调用父类构造函数,并且不要忘记提供初始状态。

  • actions( state ):返回我们可以从给定状态执行的动作列表。我们将使用此函数生成新状态。从语义上讲,创建像 UP、DOWN、LEFT 和 RIGHT 这样的动作常量会更有意义,然后将这些常量作为结果进行解释。然而,在这个实现中,我们将简单地将动作解释为“移动到 (x, y)”,并将这个命令表示为 (x, y)。此函数大致包含了我们在 succ 函数中实现的逻辑,只不过我们不会根据已访问的节点集合来筛选结果。

  • result( state0, action):返回应用于 state0 的动作所产生的新状态。

  • is_goal( state ):如果状态是目标状态,则返回 true。在我们的实现中,我们需要将状态与终点状态坐标进行比较。

  • cost( self, state, action, newState ):这是从 statenewState 通过 action 的移动成本。在我们的示例中,每次移动的成本均为 1。

请查看以下示例:

import math
from simpleai.search import SearchProblem, astar
class ShortestPath(SearchProblem):
    def __init__(self, size, start, end, obstacles):
        self.size = size
        self.start = start
        self.end = end
        self.obstacles = obstacles
        super(ShortestPath, \
              self).__init__(initial_state=self.start)
    def actions(self, state):
        (row, col) = state
        (max_row, max_col) = self.size
        succ_states = []
        if row > 1:
            succ_states += [(row-1, col)]
        if col > 1:
            succ_states += [(row, col-1)]
        if row < max_row:
            succ_states += [(row+1, col)]
        if col < max_col:
            succ_states += [(row, col+1)]
        return [s for s in succ_states \
                if s not in self.obstacles]
    def result(self, state, action):
        return action
    def is_goal(self, state):
        return state == end
    def cost(self, state, action, new_state):
        return 1
    def heuristic(self, state):
        (x, y) = state
        (u, v) = self.end
        return math.sqrt(abs(x-u) ** 2 + abs(y-v) ** 2)
size = (7, 9)
start = (5, 3)
end = (6, 9)
obstacles = {(3, 4), (3, 5), (3, 6), (3, 7), (3, 8), \
             (4, 5), (5, 5), (5, 7), (5, 9), (6, 2), \
             (6, 3), (6, 4), (6, 5), (6, 7), (7, 7)} 
searchProblem = ShortestPath(size, start, end, obstacles)
result = astar(searchProblem, graph_search=True)
result.path()

在前面的代码片段中,我们使用了 simpleai 包来简化我们的代码。我们还必须定义一个名为 ShortestPath 的类才能使用该包。

预期输出如下:

图 1.19:使用 simpleai 库显示队列的输出

图 1.19:使用 simpleai 库显示队列的输出

simpleai 库使得搜索描述比手动实现要简单得多。我们只需要定义一些基本方法,然后就可以访问一个有效的搜索实现。

在下一部分,我们将讨论 Minmax 算法及其剪枝技术。

使用 Minmax 算法和 Alpha-Beta 剪枝的游戏 AI

在前两个部分中,我们看到为简单的井字棋游戏创建一个获胜策略是多么困难。上一部分介绍了几种利用 A* 算法解决搜索问题的结构。我们还看到,像 simpleai 库这样的工具帮助我们减少了在代码中描述任务时所付出的努力。

我们将利用所有这些知识来提升我们的游戏 AI 技能,并解决更复杂的问题。

回合制多人游戏的搜索算法

类似井字棋这类回合制多人游戏与路径寻找问题相似。我们有一个初始状态,并且有一组结束状态,在这些状态下我们赢得了游戏。

回合制多人游戏的挑战在于对手可能移动的组合爆炸。这种差异使得我们有理由将回合制游戏与常规的路径寻找问题区别开来。

例如,在井字棋游戏中,从空白棋盘开始,我们可以选择九个格子中的一个并将我们的标志放在那里,假设我们先开始游戏。我们可以用 succ 函数来表示这个算法,象征着后继状态的创建。假设我们有一个初始状态,记作 Si

在这里,我们有 succ(Si) returns [ S1, S2, ..., Sn ],其中 S1, S2, ..., Sn 是后继状态:

图 1.20:表示函数后继状态的树形图

图 1.20:表示函数后继状态的树形图

然后,对手也进行移动,这意味着我们需要从每个可能的状态开始,检查更多的状态:

图 1.21:表示父节点-后继节点关系的树形图

图 1.21:表示父节点-后继节点关系的树形图

可能未来状态的扩展会在两种情况下停止:

  • 游戏结束。

  • 由于资源限制,对于某一效用的状态,超出某一深度后再继续解释更多的移动是不值得的。

一旦我们停止扩展,我们就必须对状态进行静态启发式评估。这正是我们之前在 A* 算法中所做的,选择最佳移动时;然而,我们从未考虑过未来的状态。

因此,尽管我们的算法变得越来越复杂,但如果不使用可能的未来状态知识,我们很难判断当前的移动是可能的胜利还是失败。

我们掌控未来的唯一方法是改变我们的启发式,同时知道未来我们会赢多少局、输多少局,或者平局。我们可以最大化我们的胜利,或者最小化我们的失败。我们仍然没有深入探讨,是否通过 AI 更聪明的操作,我们的失败本可以避免。

所有这些问题都可以通过深入挖掘未来状态并递归评估各分支的效用来避免。

为了考虑未来的状态,我们将学习 Minmax 算法及其变种 NegaMax 算法。

Minmax 算法

假设有一个游戏,其中启发式函数可以从 AI 玩家角度评估游戏状态。例如,我们在井字棋练习中使用了一个特定的评估方法:

  • +1,000 分,表示获胜的棋步

  • +100 分,表示通过一步棋阻止对手获胜

  • +10 分,表示通过一步棋形成两行、列或对角线

  • +1 分,表示通过一步棋形成一行、列或对角线

这种静态评估在任何节点上实现都很简单。问题是,当我们深入到所有可能的未来状态的树中时,我们还不知道如何处理这些分数。这个时候,Minmax 算法就派上用场了。

假设我们构建了一棵树,树中包含每个玩家在某一深度范围内可能采取的每一步棋。在树的底部,我们评估每个选项。为了简单起见,假设我们的搜索树如下所示:

图 1.22:到达一定深度的搜索树示例

图 1.22:到达一定深度的搜索树示例

AI 以 X 作为棋子,玩家以 O 作为棋子。一个包含 X 的节点表示该轮是 X 走棋。一个包含 O 的节点表示该轮是 O 走棋。

假设树的底部全是 O 叶节点,并且由于资源限制,我们没有计算更多的值。我们的任务是评估这些叶节点的效用:

图 1.23:可能走法的搜索树示例

图 1.23:可能走法的搜索树示例

我们必须从自己的角度选择最佳的走法,因为我们的目标是最大化走法的效用。最大化收益的愿望代表了 Minmax 算法中的 Max 部分:

图 1.24:最佳走法的搜索树示例

图 1.24:最佳走法的搜索树示例

如果我们向上一层移动,那么就轮到对手行动了。我们的对手会选择对我们最不利的值。因为对手的任务是最小化我们获胜的机会。这就是 Minmax 算法中的 Min 部分:

图 1.25:最小化获胜机会

图 1.25:最小化获胜机会

在最上面,我们可以在效用为 101 和效用为 21 的两个走法之间进行选择。由于我们要最大化我们的价值,我们应该选择效用为 101 的走法:

图 1.26:最大化获胜机会

图 1.26:最大化获胜机会

让我们看看如何实现这个想法:

def min_max( state, depth, is_maximizing):
    if depth == 0 or is_end_state( state ):
        return utility( state )
    if is_maximizing:
        utility = 0
        for s in successors( state ):
            score = MinMax( s, depth - 1, false )
            utility = max( utility, score )
        return utility
    else:
        utility = infinity
        for s in successors( state ):
            score = MinMax( s, depth - 1, true )
            utility = min( utility, score )
        return utility

这是 Minmax 算法。我们从自己的角度评估叶节点。然后,从下往上,我们应用递归定义:

  • 我们的对手通过选择对我们最不利的节点来进行最佳游戏。

  • 我们通过选择对我们最有利的节点来进行最佳游戏。

为了理解 Minmax 算法在井字棋游戏中的应用,我们还需要一些额外的信息:

  • is_end_state是一个函数,用于判断是否应该评估当前状态,而不是继续深入搜索。这通常发生在游戏结束时,或者因为强制性走法导致游戏即将结束。利用我们的效用函数,可以安全地说,一旦我们获得 1,000 分或更高分数时,我们就有效地赢得了游戏。因此,is_end_state可以简单地检查节点的得分,判断是否需要继续深入。

  • 尽管successors函数只依赖于状态,但传递轮到谁走的信息是很实际的。因此,如果需要,可以添加一个参数;你不必完全遵循伪代码。

  • 我们希望尽量减少在实现 Minmax 算法时的努力。因此,我们将评估现有的算法实现。同时,在本节的其余部分,我们将简化算法描述的对偶性。

  • 所建议的效用函数与我们在该算法中可能使用的效用函数相比,十分准确。一般而言,越是向深层次搜索,我们的效用函数所需的准确性就越低。例如,如果我们可以深入到井字棋的九步之内,我们所需要做的只是为胜利奖励 1 分,为平局奖励 0 分,为失败扣 1 分,因为在九步内,棋盘已完成,我们拥有了做出评估所需的所有信息。如果我们只能看到四步深度,这个效用函数在游戏开始时就完全没用了,因为我们至少需要五步才能赢得游戏。

  • Minmax 算法可以通过修剪树进一步优化。修剪是一种通过去除不贡献于结果的分支来优化算法的操作。通过消除不必要的计算,我们节省了宝贵的资源,这些资源可以用于深入搜索树。

使用 Alpha-Beta 剪枝优化 Minmax 算法

上述思考过程中的最后一个考虑因素促使我们探索通过集中注意力于关键节点,减少搜索空间,从而实现优化。

树中有一些节点组合,我们可以确定评估某个子树不会对最终结果产生贡献。我们将找出这些组合,进行分析,并总结出它们以优化 Minmax 算法。

让我们通过之前的节点示例来检查剪枝过程:

图 1.27:演示剪枝节点的搜索树

图 1.27:演示剪枝节点的搜索树

在计算了值为10123110的节点后,我们可以得出结论:在两层以上的地方,值为101的节点将被选择。为什么?

  • 假设 X <= 110。在这种情况下,将选择110和 X 中的最大值,即110,而 X 将被忽略。

  • 假设 X > 110。这里,110 和 X 的最大值是 X。往上一层,算法将从两者中选择最小值。101 和 X 的最小值始终是 101,因为 X > 110。因此,X 会在上一层被省略。

这就是我们修剪树的方式。

在右侧,假设我们计算了分支 1021。它们的最大值是 21。计算这些值的意义是我们可以省略对节点 Y1、Y2 和 Y3 的计算,并且知道 Y4 的值小于或等于 21。为什么?

21 和 Y3 的最小值永远不会大于 21。因此,Y4 的值永远不会大于 21

现在,我们可以在一个效用值为 101 的节点和一个效用最大值为 21 的节点之间做选择。显然,我们必须选择效用值为 101 的节点:

图 1.28:修剪树的示例

图 1.28:修剪树的示例

这就是 alpha-beta 剪枝的核心思想。我们剪枝掉那些我们知道不会被使用的子树。

让我们看看如何在 Minmax 算法中实现 alpha-beta 剪枝。

首先,我们将向 Minmax 的参数列表中添加 alpha 和 beta 参数:

def min_max(state, depth, is_maximizing, alpha, beta):
    if depth == 0 or is_end_state(state):
        return utility(state)
    if is_maximizing:
        utility = 0
        for s in successors(state):
            score = MinMax(s, depth - 1, false, alpha, beta)
            utility = max(utility, score)
            alpha = max(alpha, score)
            if beta <= alpha:
                break
        return utility
    else:
        utility = infinity
        for s in successors(state):
            score = MinMax(s, depth - 1, true, alpha, beta)
            utility = min(utility, score)
        return utility

在前面的代码片段中,我们将 alphabeta 参数添加到 MinMax 函数中,以便在最大化分支中计算出 alphabeta 之间的最大值作为新的 alpha 值。

现在,我们需要对最小化分支做相同的操作:

def min_max(state, depth, is_maximizing, alpha, beta):
    if depth == 0 or is_end_state( state ):
        return utility(state)
    if is_maximizing:
        utility = 0
        for s in successors(state):
            score = min_max(s, depth - 1, false, alpha, beta)
            utility = max(utility, score)
            alpha = max(alpha, score)
            if beta <= alpha: break
        return utility
    else:
        utility = infinity
        for s in successors(state):
            score = min_max(s, depth - 1, true, alpha, beta)
            utility = min(utility, score)
            beta = min(beta, score)
            if beta <= alpha: break
        return utility

在前面的代码片段中,我们在 else 分支中添加了新的 beta 值,它是最小化分支中 alphabeta 之间的最小值。

我们已经完成了实现。建议你在我们的示例树上逐步执行算法,以便更好地理解实现过程。

有一个重要部分缺失,导致我们无法正确执行:alphabeta 的初始值。任何在可能的效用值范围外的数字都可以作为初始值。我们将使用正负无穷大作为初始值来调用 Minmax 算法:

alpha = infinity
beta = -infinity

在下一节中,我们将探讨使用 NegaMax 算法时的 DRY 技术。

使 Minmax 算法更简洁——NegaMax 算法

Minmax 算法运行良好,特别是结合了 alpha-beta 剪枝。唯一的问题是算法中存在 ifelse 分支,它们实际上是相互否定的。

正如我们所知,在计算机科学中,有 DRY 代码和 WET 代码。DRY 代表 Don't Repeat Yourself(不要重复自己)。WET 代表 Write Everything Twice(写两遍所有内容)。当我们重复编写相同的代码时,我们增加了写错的机会,也加大了每次维护时出错的概率。因此,最好复用代码。

在实现 Minmax 算法时,我们总是从 AI 玩家的角度计算节点的效用。这就是为什么在实现中,我们需要有效用最大化的分支和有效用最小化的分支,它们本质上是对立的。由于我们偏向于编写描述问题的简洁代码,我们可以通过改变评估的视角来消除这种对立。

每当 AI 玩家的回合到来时,算法不需要做任何改变。

每当对手的回合到来时,我们会反转视角。最小化 AI 玩家的效用等同于最大化对手的效用。

这简化了 Minmax 算法:

def Negamax(state, depth, is_players_point_of_view):
    if depth == 0 or is_end_state(state):
        return utility(state, is_players_point_of_view)
    utility = 0
    for s in successors(state):
        score = Negamax(s,depth-1,not is_players_point_of_view)
    return score

使用 NegaMax 算法时有一些必要条件;例如,棋盘状态的评估必须是对称的。如果从第一玩家的角度,某个游戏状态的价值为+20,那么从第二玩家的角度,它的价值应为-20。因此,我们通常将得分归一化,使其围绕零。

使用 EasyAI 库

我们已经使用过simpleai库,它帮助我们执行路径寻找问题的搜索。现在,我们将使用EasyAI库,它可以轻松处理二人对战游戏的 AI 搜索,将井字棋问题的实现简化为编写几个用于评估棋盘效用并判断游戏结束的函数。

要安装EasyAI,请在 Jupyter Notebook 中输入以下命令:

!pip install easyAI

你可以在 GitHub 上查看该库的文档,网址为github.com/Zulko/easyAI

活动 1.04:四连棋

在本活动中,我们将练习使用EasyAI库并开发启发式方法。我们将使用游戏四连棋进行练习。游戏棋盘宽 7 格,高 7 格。每当你进行一次移动时,你只能选择一个列来放置你的棋子。然后,重力会将棋子拉到底部的空格。你的目标是在对方之前,横向、纵向或斜向连接四个自己的棋子,或者当棋盘没有空位时,游戏结束。

游戏规则可以在en.wikipedia.org/wiki/Connect_Four找到。

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 编写init方法,生成游戏中所有可能的胜利组合,并保存以备将来使用。

  3. 编写一个函数来列举所有可能的移动。然后,检查每一列是否有空位。如果有空位,则将该列视为可能的移动。

  4. 创建一个函数来进行移动(它将类似于可能移动函数),然后检查该列并从底部开始找到第一个空格。

  5. 重用井字棋示例中的输掉函数。

  6. 实现显示方法,打印棋盘并尝试游戏。

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

预期的输出是这样的:

图 1.29:四连棋游戏的预期输出

](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_01_29.jpg)

图 1.29:游戏《四子棋》的预期输出

摘要

在本章中,我们已经看到 AI 如何用于增强或替代人类的能力,例如听、说、理解语言、存储和检索信息、思考、看和移动。

然后,我们开始学习智能体以及它们如何与环境互动,通过看似智能的方式解决问题并追求目标。

接着,我们介绍了 Python,并学习了它在 AI 中的作用。我们研究了一些重要的 Python 库,用于开发 AI,并为智能体准备了数据。随后,我们基于预定义的规则创建了一个井字棋游戏。我们将这些规则量化成数字,这个过程我们称之为启发式。我们学习了如何在 A*搜索算法中使用启发式来找到问题的最优解。

最后,我们了解了 Minmax 和 NegaMax 算法,使得 AI 能够在双人游戏中获胜。在下一章中,你将接触到回归分析。

第二章:2. 回归简介

概述

在本章中,您将学习回归技术。回归在尝试使用历史数据预测未来变量时非常有用。您将学习各种回归技术,如单变量和多变量线性回归,以及多项式回归和支持向量回归(SVR)。您将使用这些技术预测未来的股价。到本章结束时,您将能够熟练地运用回归技术解决各个领域的实际问题。

介绍

在上一章中,您已经了解了人工智能AI)的基本知识,并使用它创建了井字游戏。在本章中,我们将讨论回归,它是一种机器学习算法,可用于衡量独立变量(称为特征)与依赖变量(称为标签)之间的关系。

线性回归是一个广泛应用于各个领域的概念,从金融(预测资产价格)到商业(预测产品销量),甚至到经济学(预测经济增长)。

本章大部分内容将讨论不同形式的线性回归,包括单变量线性回归、多变量线性回归、单变量多项式回归和多变量多项式回归。Python 提供了许多回归操作的支持,我们也将在本章稍后介绍这些内容。

我们还将使用一种替代回归模型,称为支持向量回归SVR),以及不同形式的线性回归。在本章中,我们将使用一些样本数据集,以及从Quandl Python 库加载的股价数据,利用不同类型的回归来预测未来价格。

注意

尽管不建议您使用本章中的模型来提供交易或投资建议,但这是一次非常激动人心且有趣的旅程,它解释了回归的基本原理。

单变量线性回归

一个通用的回归问题可以通过以下示例定义。假设我们有一组数据点,需要找出最佳拟合曲线来大致拟合给定的数据点。这条曲线将描述输入变量x(即数据点)和输出变量y(即曲线)之间的关系。

请记住,在现实生活中,我们通常有多个输入变量决定输出变量。然而,使用单变量的线性回归有助于我们理解输入变量如何影响输出变量。

回归的类型

在本章中,我们将处理二维平面上的回归问题。这意味着我们的数据点是二维的,我们正在寻找一条曲线来近似如何从一个变量计算另一个变量。

在本章中,我们将遇到以下几种回归类型:

  • 使用一次多项式的单变量线性回归:这是最基本的回归形式,其中一条直线近似未来数据的轨迹。

  • 使用一次多项式的多变量线性回归:我们将使用一次方程,但也允许多个输入变量,称为特征。

  • 单变量的多项式回归:这是单变量线性回归的通用形式。由于用来近似输入与输出之间关系的多项式可以是任意的次数,因此我们可以创建比直线更适合数据点的曲线。回归依然是线性的——不是因为多项式是线性的,而是因为回归问题可以通过线性代数来建模。

  • 多变量的多项式回归:这是最通用的回归问题,使用高次多项式和多个特征来预测未来。

  • SVR:这种回归形式使用支持向量机SVMs)来预测数据点。包括这种回归类型是为了说明 SVR 与其他四种回归类型的区别。

现在我们将处理第一种类型的线性回归:我们将使用一个变量,并且回归的多项式将描述一条直线。

在二维平面上,我们将使用笛卡尔坐标系,也被称为笛卡尔坐标系。我们有一个x轴和一个y轴,这两条轴的交点是原点。我们用xy坐标来表示点的位置。

例如,点(2, 1)对应于下列坐标系中的黑色点:

图 2.1:点(2, 1)在坐标系中的表示

图 2.1:点(2, 1)在坐标系中的表示

一条直线可以用方程y = ax + b来描述,其中a是方程的斜率,决定了方程上升的陡峭度,b是常数,决定了直线与y*轴的交点。

图 2.2中,您可以看到三个方程:

  • 这条直线的方程是y = 2x + 1*。

  • 虚线的方程是y = x + 1

  • 虚线的方程是y = 0.5x + 1*。

您可以看到,所有三个方程都在y轴上交于1,它们的斜率由我们乘以x的因子来决定。

如果你知道x,你就可以求解y。类似地,如果你知道y,你也可以求解x。这个方程是一个一次多项式方程,它是单变量线性回归的基础:

图 2.2:y = 2*x + 1、y = x + 1 和 y = 0.5*x + 1 在坐标系中的表示

图 2.2:y = 2x + 1、y = x + 1 和 y = 0.5x + 1 在坐标系中的表示

我们可以使用多项式方程来描述曲线而非直线;例如,多项式方程 4x4-3x3-x2-3x+3 将生成 图 2.3。这种类型的方程是具有一个变量的多项式回归的基础:

图 2.3:多项式方程的表示

图 2.3:多项式方程的表示

注意

如果你想进一步实验笛卡尔坐标系,可以使用以下绘图工具:s3-us-west-2.amazonaws.com/oerfiles/College+Algebra/calculator.html

特征和标签

在机器学习中,我们区分特征和标签。特征被视为我们的输入变量,而标签则是我们的输出变量。

在谈到回归时,标签的可能值是一个连续的有理数集合。可以将特征看作是 x 轴上的值,而标签则是 y 轴上的值。

回归任务是基于特征值预测标签值。

我们通常通过将特征的值投影到未来来创建标签。

例如,如果我们想通过历史月度数据来预测下个月的股票价格,我们将通过将股票价格特征向未来移动一个月来创建标签:

  • 对于每个股票价格特征,标签将是下个月的股票价格特征。

  • 对于最后一个月,预测数据将无法获得,因此这些值都是 NaN(不是一个数字)。

假设我们有 1 月2 月3 月 的数据,并且我们想要预测 4 月 的价格。我们每个月的特征将是当前月的价格,标签将是下一个月的价格。

例如,看看以下表格:

图 2.4:特征和标签的示例

图 2.4:特征和标签的示例

这意味着 1 月 的标签是 2 月 的价格,2 月 的标签实际上是 3 月 的价格。3 月 的标签是未知的(NaN),因为这是我们要预测的值。

特征缩放

有时,我们会有多个特征(输入),它们可能具有完全不同范围的值。想象一下将地图上的微米与现实世界中的千米进行比较。由于数量级差异有九个零,这些值处理起来会非常困难。

一个较小的差异是英制和公制数据之间的差异。例如,磅和千克,厘米和英寸,它们的比较并不好。

因此,我们通常将特征缩放到更容易处理的归一化值,因为我们可以更轻松地比较这些范围的值。

我们将演示两种类型的缩放:

  • Min-max 归一化

  • 均值归一化

Min-max 归一化的计算方法如下:

1

这里,XMIN 是特征的最小值,XMAX 是最大值。

特征缩放后的值将在 [0;1] 的范围内。

均值归一化的计算方法如下:

2

这里,AVG 是平均值。

特征缩放后的值将在 [-1;1] 的范围内。

这是在斐波那契数列的前 13 个数字上应用两种归一化的例子。

我们首先找到 min-max 归一化:

fibonacci = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]
# Min-Max normalization:
[(float(i)-min(fibonacci))/(max(fibonacci)-min(fibonacci)) \
for i in fibonacci]

预期输出是这样的:

[0.0,
 0.006944444444444444,
 0.006944444444444444,
 0.013888888888888888,
 0.020833333333333332,
 0.034722222222222224,
 0.05555555555555555,
 0.09027777777777778,
 0.14583333333333334,
 0.2361111111111111,
 0.3819444444444444,
 0.6180555555555556,
 1.0]

现在,查看以下代码片段以找到均值归一化:

# Mean normalization:
avg = sum(fibonacci) / len(fibonacci)
# 28.923076923076923
[(float(i)-avg)/(max(fibonacci)-min(fibonacci)) \
for i in fibonacci]

预期输出是这样的:

[-0.20085470085470086,
 -0.19391025641025642,
 -0.19391025641025642,
 -0.18696581196581197,
 -0.18002136752136752,
 -0.16613247863247863,
 -0.1452991452991453,
 -0.11057692307692307,
 -0.05502136752136752,
 0.035256410256410256,
 0.18108974358974358,
 0.4172008547008547,
 0.7991452991452992]

注意

缩放可能会增加处理时间,但通常它是一个需要执行的重要步骤。

在 scikit-learn 库中,我们可以使用 preprocessing.scale 函数,它可以对 NumPy 数组进行缩放:

import numpy as np
from sklearn import preprocessing
preprocessing.scale(fibonacci)

预期输出是这样的:

array([-0.6925069 , -0.66856384, -0.66856384, -0.64462079,
       -0.62067773-0.57279161, -0.50096244, -0.38124715,
       -0.18970269,  0.12155706, 0.62436127,  1.43842524,
       2.75529341]

scale 方法执行标准化,这是一种归一化方式。注意,结果是一个 NumPy 数组。

将数据分为训练集和测试集

现在我们已经学会了如何对数据集进行归一化,我们还需要学习训练-测试拆分。为了衡量我们的模型能否很好地概括其预测性能,我们需要将数据集分为训练集和测试集。训练集供模型学习以构建预测,然后,模型将使用测试集来评估其预测的性能。

当我们分割数据集时,我们首先需要对其进行洗牌,以确保我们的测试集能够代表整个数据集。通常,拆分比例为 90% 用于训练集,10% 用于测试集。

通过训练和测试,我们可以衡量模型是否发生了过拟合或欠拟合。

过拟合发生在训练好的模型对训练数据拟合得过于完美时。模型在训练数据上表现得非常准确,但在实际应用中无法使用,因为它在其他数据上的准确度会降低。模型会对训练数据中的随机噪声进行调整,并假设这些噪声中存在的模式,从而得出错误的预测。

欠拟合发生在训练好的模型无法充分拟合训练数据,导致无法识别数据中的重要模式。因此,它无法对新数据做出准确的预测。一个例子是当我们尝试对一个非线性数据集进行线性回归时。例如,斐波那契数列是非线性的;因此,基于类似斐波那契数列的数据建模也不可能是线性的。

我们可以使用 scikit-learn 的 model_selection 库来进行训练-测试拆分。

假设,在我们的例子中,我们已经对斐波那契数据进行了缩放,并将其索引定义为标签:

features = preprocessing.scale(fibonacci)
label = np.array(range(13))

现在,让我们将数据的 10%用作测试数据,test_size=0.1,并指定random_state参数,以便每次运行代码时都能得到完全相同的分割:

from sklearn import model_selection
(x_train, x_test, y_train, y_test) = \
model_selection.train_test_split(features, \
                                 label, test_size=0.1, \
                                 random_state=8)

我们的数据集已被划分为特征(x_trainx_test)和标签(y_trainy_test)的测试集和训练集。

最后,让我们检查每个数据集,从x_train特征开始:

x_train

预期输出为:

array([ 1.43842524, -0.18970269, -0.50096244,  2.75529341,
       -0.6925069 , -0.66856384, -0.57279161,  0.12155706,
       -0.66856384, -0.62067773, -0.64462079])

接下来,我们检查x_test

x_test

预期输出为:

array([-0.38124715, 0.62436127])

然后,我们检查y_train

y_train

预期输出为:

array([11, 8, 6, 12, 0, 2, 5, 9, 1, 4, 3])

接下来,我们检查y_test

y_test

预期输出为:

array([7, 10])

在前面的输出中,我们可以看到我们的数据分割已正确执行;例如,我们的标签已被分割为y_test,其中包含710索引,y_train包含其余的11个索引。相同的逻辑已应用于我们的特征,x_test中有2个值,x_train中有11个值。

如果你记得笛卡尔坐标系,你会知道水平轴是x轴,垂直轴是y轴。我们的特征位于x轴上,而我们的标签位于y轴上。因此,我们将特征和x视为同义词,而标签通常用y表示。因此,x_test表示特征测试数据,x_train表示特征训练数据,y_test表示标签测试数据,y_train表示标签训练数据。

使用 scikit-learn 在数据上拟合模型

现在我们将通过一个只有一个特征且数据最小的例子来说明回归过程。

由于我们只有一个特征,我们必须通过x_train.reshape (-1,1)x_train格式化为包含一个特征的 NumPy 数组。

因此,在执行拟合最佳直线的代码之前,先执行以下代码:

x_train = x_train.reshape(-1, 1)
x_test = x_test.reshape(-1, 1)

我们可以用以下代码在数据上拟合一个线性回归模型:

from sklearn import linear_model
linear_regression = linear_model.LinearRegression()
model = linear_regression.fit(x_train, y_train)
model.predict(x_test)

预期输出为:

array([4.46396931, 7.49212796])

我们还可以计算与模型相关的得分:

model.score(x_test, y_test)

预期输出为:

-1.8268608450379087

这个得分表示模型的准确性,定义为 R2 或决定系数。它表示我们可以从标签中预测特征的程度。

在我们的例子中,-1.8268的 R2 表示一个非常糟糕的模型,因为最佳得分为1。如果我们始终使用特征的平均值来预测标签,则可以获得0的得分。

在本书中,我们将省略此得分的数学背景。

我们的模型表现不佳有两个原因:

  • 如果我们检查之前的斐波那契数列,11 个训练数据点和 2 个测试数据点显然不足以进行适当的预测分析。

  • 即使我们忽略数据点的数量,斐波那契数列并没有描述xy之间的线性关系。用直线近似非线性函数仅在我们观察到两个非常接近的数据点时才有用。

使用 NumPy 数组进行线性回归

NumPy 数组比 Python 列表更方便的一个原因是它们可以被视为向量。向量上定义了一些操作,可以简化我们的计算。我们可以对长度相同的向量执行操作。

让我们以两个向量 V1 和 V2 为例,每个向量有三个坐标:

V1 = (a, b, c),其中 a=1,b=2,c=3

V2 = (d, e, f),其中 d=2,e=0,f=2

这两个向量的加法将是:

V1 + V2 = (a+d, b+e, c+f) = (1+2, 2+0, 3+2) = (3,2,5)

这两个向量的乘积将是:

V1 + V2 = (ad, be, cf) = (12, 20, 32) = (2,0,6)

你可以将每个向量看作我们的数据集,例如,第一个向量是我们的特征集,第二个向量是我们的标签集。因为 Python 能够进行向量计算,这将大大简化我们线性回归模型所需的计算。

现在,让我们通过以下示例使用 NumPy 构建线性回归。

假设我们有两个数据集,每个数据集有 13 个数据点;我们想要构建一条最佳拟合所有数据点的线性回归线。

我们的第一组数据定义如下:

[2, 8, 8, 18, 25, 21, 32, 44, 32, 48, 61, 45, 62]

如果我们将这个数据集的值(2,8,8,18,25,21,32,44,32,48,61,45,62)绘制为y轴,而每个值的索引(1,2,3,4,5,6,7,8,9,10,11,12,13)绘制为x轴,我们将得到如下图表:

![图 2.5:第一个数据集的绘制图]

](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_02_05.jpg)

图 2.5:第一个数据集的绘制图

我们可以看到,这个数据集的分布似乎呈线性,如果我们想画一条尽可能靠近每个点的直线,这并不难。在这种情况下,简单的线性回归看起来是合适的。

我们的第二组数据是前 13 个值,按照我们在特征缩放部分中看到的斐波那契数列进行缩放:

[-0.6925069, -0.66856384, -0.66856384, -0.64462079, -0.62067773, -0.57279161, -0.50096244, -0.38124715, -0.18970269, 0.12155706, 0.62436127, 1.43842524, 2.75529341]

如果我们将这个数据集的值作为 y 轴,每个值的索引作为 x 轴,我们将得到如下图表:

![图 2.6:第二个数据集的绘制图]

](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_02_06.jpg)

图 2.6:第二个数据集的绘制图

我们可以看到,这个数据集的分布似乎不是线性的,如果我们想画一条尽可能靠近每个点的直线,线可能会错过很多点。在这种情况下,简单的线性回归可能会遇到困难。

我们知道,直线的方程是3

在这个方程中,4 是斜率,而5y 截距。为了找到最佳拟合线,我们必须找到67 的系数。

为了做到这一点,我们将使用最小二乘法,通过完成以下步骤来实现:

  1. 对每个数据点,计算 x² 和 xy

    求所有 xyx² 和 x * y 的和,得出8

  2. 计算斜率,9,作为 10,其中 N 是数据点的总数。

  3. 计算 y 截距,11,作为 12

现在,让我们以 NumPy 为例,使用以下代码应用这些步骤来处理第一个数据集。

让我们看一下第一步:

import numpy as np
x = np.array(range(1, 14))
y = np.array([2, 8, 8, 18, 25, 21, 32, 44, 32, 48, 61, 45, 62])
x_2 = x**2
xy = x*y

对于 x_2,输出将是:

array([  1,   4,   9,  16,  25,  36,  49,  64,  81, 
       100, 121, 144, 169],  dtype=int32)

对于 xy,输出将是:

array([2, 16, 24, 72, 125, 126, 224, 
       352, 288, 480, 671, 540, 806])

现在,让我们继续下一步:

sum_x = sum(x)
sum_y = sum(y)
sum_x_2 = sum(x_2)
sum_xy = sum(xy)

对于 sum_x,输出将是:

91

对于 sum_y,输出将是:

406

对于 sum_x_2,输出将是:

819

对于 sum_xy,输出将是:

3726

现在,让我们继续下一步:

N = len(x)
a = (N*sum_xy - (sum_x*sum_y))/(N*sum_x_2-(sum_x)**2)

对于 N,输出将是:

13

对于 a,输出将是:

4.857142857142857

现在,让我们继续到最后一步:

b = (sum_y - a*sum_x)/N

对于 b,输出将是:

-2.7692307692307647

一旦我们用前述系数绘制出线 13,我们将得到以下图形:

图 2.7:第一数据集的线性回归绘制图

图 2.7:第一数据集的线性回归绘制图

如你所见,我们的线性回归模型在这个具有线性分布的数据集上表现得非常好。

注意

你可以在 www.endmemo.com/statistics/lr.php 找到线性回归计算器。你还可以查看该计算器,以了解在给定数据集上最优拟合线的形态。

我们将对第二数据集重复相同的步骤:

import numpy as np
x = np.array(range(1, 14))
y = np.array([-0.6925069, -0.66856384, -0.66856384, \
              -0.64462079, -0.62067773, -0.57279161, \
              -0.50096244, -0.38124715, -0.18970269, \
              0.12155706, 0.62436127, 1.43842524, 2.75529341])
x_2 = x**2
xy = x*y
sum_x = sum(x)
sum_y = sum(y)
sum_x_2 = sum(x_2)
sum_xy = sum(xy)
N = len(x)
a = (N*sum_xy - (sum_x*sum_y))/(N*sum_x_2-(sum_x)**2)
b = (sum_y - a*sum_x)/N

对于 a,输出将是:

0.21838173510989017

对于 b,输出将是:

-1.528672146538462

一旦我们用前述系数绘制出线 14,我们将得到以下图形:

图 2.8:第二数据集的线性回归绘制图

图 2.8:第二数据集的线性回归绘制图

显然,在线性分布下,我们的线性回归模型能够很好地拟合数据。

注意

我们不必使用这种方法来进行线性回归。许多库,包括 scikit-learn,可以帮助我们自动化这一过程。一旦我们进行多个变量的线性回归,最好使用库来为我们执行回归。

使用 NumPy Polyfit 拟合模型

NumPy Polyfit 还可以用于创建具有一个变量的最佳拟合线进行线性回归。

回忆一下最优拟合线的计算:

import numpy as np
x = np.array(range(1, 14))
y = np.array([2, 8, 8, 18, 25, 21, 32, 44, 32, 48, 61, 45, 62])
x_2 = x**2
xy = x*y
sum_x = sum(x)
sum_y = sum(y)
sum_x_2 = sum(x_2)
sum_xy = sum(xy)
N = len(x)
a = (N*sum_xy - (sum_x*sum_y))/(N*sum_x_2-(sum_x)**2)
b = (sum_y - a*sum_x)/N

求解系数的方程式 1516 相当复杂。幸运的是,Python 中的 numpy.polyfit 函数可以执行这些计算,找出最优拟合线的系数。polyfit 函数接受三个参数:x 值的数组、y 值的数组,以及要查找的多项式的次数。由于我们要查找的是一条直线,因此多项式中 x 的最高次方为 1

import numpy as np
x = np.array(range(1, 14))
y = np.array([2, 8, 8, 18, 25, 21, 32, 44, 32, 48, 61, 45, 62])
[a,b] = np.polyfit(x, y, 1)

对于 [a,b],输出将是:

[4.857142857142858, -2.769230769230769]

在 Python 中绘制结果

假设你有一组数据点和一条回归线,我们的任务是将这些点和回归线一起绘制,以便我们能直观地看到结果。

我们将使用 matplotlib.pyplot 库来实现这一点。这个库有两个重要的函数:

  • scatter:此函数显示在平面上分布的散点,由一组 x 坐标和一组 y 坐标定义。

  • plot:此函数接受两个参数,绘制由两个点定义的线段或由多个点定义的一系列线段。与散点图类似,区别在于它不是显示单独的点,而是通过线条将它们连接起来。

使用三个参数的绘图函数绘制一个线段和/或根据第三个参数格式化的两个点。

一条线段由两个点定义。当 x 在 1 和 13 之间变化时(请记住数据集包含 13 个数据点),在 0 和 15 之间显示一条线段是合理的。我们必须将 x 的值代入方程式 17 以获得相应的 y 值:

import numpy as np
import matplotlib.pyplot as plot
x = np.array(range(1, 14))
y = np.array([2, 8, 8, 18, 25, 21, 32, 44, 32, 48, 61, 45, 62])
x_2 = x**2
xy = x*y
sum_x = sum(x)
sum_y = sum(y)
sum_x_2 = sum(x_2)
sum_xy = sum(xy)
N = len(x)
a = (N*sum_xy - (sum_x*sum_y))/(N*sum_x_2-(sum_x)**2)
b = (sum_y - a*sum_x)/N
# Plotting the points
plot.scatter(x, y)
# Plotting the line
plot.plot([0, 15], [b, 15*a+b])
plot.show()

输出结果如下:

图 2.9:使用 matplotlib 绘制的第一个数据集线性回归图

图 2.9:使用 matplotlib 绘制的第一个数据集线性回归图

回归线和散布的数据点如预期显示。

然而,绘图函数有一个更复杂的签名。你可以使用 plot 在图形上绘制散点、线条或任何曲线。这些变量按三元组进行解析:

  • x

  • y

  • 以字符串形式的格式化选项

让我们创建一个函数,从一组近似的 x 值数组推导出一组近似的 y 值数组:

def fitY( arr ):
    return [4.857142857142859 * x - 2.7692307692307843 for x in arr]

我们将使用 fit 函数绘制这些值:

plot.plot(x, y, 'go',x, fitY(x), 'r--o')

每个第三个参数处理格式化。字母 g 代表绿色,而字母 r 代表红色。你也可以使用 b 来表示蓝色,y 来表示黄色,等等。在没有指定颜色的情况下,每个三元组将以不同的颜色显示。o 字符表示我们希望在每个数据点的位置显示一个点。因此,go 与移动无关——它请求绘图工具绘制绿色的点。- 字符负责绘制虚线。如果只使用 -1,将会显示一条直线而不是虚线。

输出结果如下:

图 2.10:使用拟合函数的绘图函数图

图 2.10:使用拟合函数的绘图函数图

Python 的绘图库为你解决大多数图形问题提供了简单的解决方案。你可以在图形上绘制任意数量的线条、点和曲线。

在显示曲线时,绘图工具将点连接起来形成线段。还要记住,即使是复杂的曲线序列,也只是将点连接起来的近似值。例如,如果你执行来自 gist.github.com/traeblain/1487795 的代码,你将看到 batman 函数的线段被连接成了直线:

图 2.11:蝙蝠侠函数的图表

](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_02_11.jpg)

图 2.11:蝙蝠侠函数的图表

有许多不同的方法可以绘制曲线。我们已经看到,NumPy 库的 polyfit 方法返回一个系数数组来描述线性方程:

import numpy as np
x = np.array(range(1, 14))
y = np.array([2, 8, 8, 18, 25, 21, 32, 44, 32, 48, 61, 45, 62])
np.polyfit(x, y, 1)

这里的输出如下:

[4.857142857142857, -2.769230769230768]

这个数组描述了方程 4.85714286 * x - 2.76923077

假设我们现在想绘制一条曲线,18。这个二次方程由系数数组 [-1, 3, -2] 描述,如19。我们可以自己编写一个函数来计算属于 x 值的 y 值。然而,NumPy 库已经有一个功能可以为我们完成这项工作——np.poly1d

import numpy as np
x = np.array(range( -10, 10, 1 ))
f = np.poly1d([-1,3,-2])

poly1d 调用创建的 f 函数不仅适用于单一值,还适用于列表或 NumPy 数组:

f(5)

预期输出如下:

-12

同样,对于 f(x)

f(x)

输出将是:

array ([-132\. -110, -90, -72, -56, -42, -30, -20, -12, -6, -2,
        0, 0, -2, -6, -12, -20, -30, -42, -56])

现在,我们可以使用这些值来绘制非线性曲线:

import matplotlib.pyplot as plot
plot.plot(x, f(x))

输出如下:

图 2.12:非线性曲线的图表

](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_02_12.jpg)

图 2.12:非线性曲线的图表

如你所见,我们可以使用 pyplot 库轻松创建非线性曲线的图表。

使用线性回归预测值

假设我们对属于 x 坐标 20y 值感兴趣。根据线性回归模型,我们需要做的就是将 20 的值代入之前使用的代码中的 x

x = np.array(range(1, 14))
y = np.array([2, 8, 8, 18, 25, 21, 32, 44, 32, 48, 61, 45, 62])
# Plotting the points
plot.scatter(x, y)
# Plotting the prediction belonging to x = 20
plot.scatter(20, a * 20 + b, color='red')
# Plotting the line
plot.plot([0, 25], [b, 25*a+b])

输出如下:

图 2.13:显示使用线性回归预测值的图表

](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_02_13.jpg)

图 2.13:显示使用线性回归预测值的图表

在这里,我们用红色表示了预测值。这个红点位于最优拟合直线的上方。

让我们看下一个练习,在这个练习中,我们将根据线性回归预测人口。

练习 2.01:预测小学的学生容量

在这个练习中,你将尝试预测小学容量的需求。你的任务是预测 2025 年和 2030 年入学儿童的数量。

注意

数据包含在 population.csv 文件中,你可以在我们的 GitHub 仓库中找到它:packt.live/2YYlPoj

以下步骤将帮助你完成此练习:

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 导入 pandasnumpy

    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plot
    
  3. 接下来,将 CSV 文件作为 DataFrame 加载到 Notebook 中并读取 CSV 文件:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/The-Applied-Artificial-'\
               'Intelligence-Workshop/master/Datasets/'\
               'population.csv'
    df = pd.read_csv(file_url)
    df
    

    预期输出如下:

    图 2.14:读取 CSV 文件

    ](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_02_14.jpg)

    图 2.14:读取 CSV 文件

  4. 现在,将 DataFrame 转换为两个 NumPy 数组。为了简便起见,我们可以假设 year 特征从 20012018,对应的值为 118

    x = np.array(range(1, 19))
    y = np.array(df['population'])
    

    x 的输出将是:

    array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18])
    

    y 输出将是:

    array([147026, 144272, 140020, 143801, 146233,
           144539, 141273, 135389, 142500, 139452,
           139722, 135300, 137289, 136511, 132884,
           125683, 127255, 124275], dtype=int64)
    
  5. 现在,使用两个 NumPy 数组,使用 polyfit 方法(由于我们只有一个特征,设置多项式的次数为 1)来确定回归线的系数:

    [a, b] = np.polyfit(x, y, 1)
    

    [a, b] 的输出将是:

    [-1142.0557275541803, 148817.5294117647]
    
  6. 现在,使用 matplotlib.pyplot 绘制结果,并预测未来至 2030 年:

    plot.scatter( x, y ) 
    plot.plot( [0, 30], [b, 30*a+b] )
    plot.show()
    

    预期的输出是:

    图 2.15:显示 2030 年未来情况的图

    图 2.15:显示 2030 年未来情况的图

    如你所见,数据呈线性关系,我们的模型似乎拟合得很好。

  7. 最后,预测 2025 年和 2030 年的人口:

    population_2025 = 25*a+b
    population_2030 = 30*a+b
    

    population_2025 的输出将是:

    120266.1362229102
    

    population_2030 的输出将是:

    114555.85758513928
    

    注意

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

    你也可以在线运行此示例,网址为packt.live/317qeIc。你必须执行整个 Notebook 才能得到期望的结果。

通过完成这个练习,我们现在可以得出结论:未来开始上小学的儿童人口将减少,因此,如果我们当前已经满足需求,就不需要增加小学的容量。

多元线性回归

在前一节中,我们处理了一个变量的线性回归。现在我们将学习线性回归的扩展版本,在该版本中,我们将使用多个输入变量来预测输出。

多元线性回归

如果你回想一下线性回归中最佳拟合线的公式,它被定义为 20,其中 21 是线的斜率,22 是线的 y 截距,x 是特征值,y 是计算出的标签值。

在多元回归中,我们有多个特征和一个标签。如果我们有三个特征,x1、x2 和 x3,我们的模型将变成 23

在 NumPy 数组格式中,我们可以将这个方程写成如下形式:

y = np.dot(np.array([a1, a2, a3]), np.array([x1, x2, x3])) + b

为了方便起见,最好将整个方程定义为向量乘法形式。系数 24 将是 1

y = np.dot(np.array([b, a1, a2, a3]) * np.array([1, x1, x2, x3]))

多元线性回归是两个向量的简单标量积,其中系数 25262728 决定了四维空间中最优拟合方程。

要理解多元线性回归的公式,你需要计算两个向量的标量积。由于标量积的另一个名称是点积,因此执行此操作的 NumPy 函数称为 dot

import numpy as np
v1 = [1, 2, 3]
v2 = [4, 5, 6]
np.dot(v1, v2)

输出将是 32,因为 np.dot(v1, v2) = 1 * 4 + 2 * 5 + 3 * 6 = 32

我们仅需将每个坐标的乘积相加。

我们可以通过最小化数据点与由方程描述的最接近点之间的误差来确定这些系数。为了简单起见,我们将省略最佳拟合方程的数学解法,而改用 scikit-learn。

注意

n维空间中,其中n大于 3,维度的数量决定了我们模型中不同的变量。在前面的例子中,我们有三个特征(x1、x2 和x3)和一个标签y。这就得到了四个维度。如果你想象四维空间,你可以将其视为一个有时间维度的三维空间。五维空间可以想象成四维空间,其中每个时间点都有一个温度。维度只是特征(和标签);它们不一定与我们对三维空间的概念相关。

线性回归的过程

我们将按照以下简单步骤解决线性回归问题:

  1. 从数据源加载数据。

  2. 准备预测数据。数据以(normalizeformatfilter)格式准备。

  3. 计算回归线的参数。无论我们使用单变量还是多变量线性回归,都将遵循以下步骤。

从数据源导入数据

有多个库可以为我们提供数据源访问。由于我们将处理股票数据,让我们先看两个专门用于获取财务数据的例子:Quandl 和 Yahoo Finance。在继续之前,请先查看以下几个重要点:

  • Scikit-learn 提供了一些数据集,可以用于练习你的技能。

  • www.quandl.com为你提供免费的和付费的财务数据集。

  • pandas.pydata.org/帮助你加载任何 CSV、Excel、JSON 或 SQL 数据。

  • Yahoo Finance 为你提供财务数据集。

使用 Yahoo Finance 加载股票价格

使用 Yahoo Finance 加载股票数据的过程非常简单。你只需在 Jupyter Notebook 中使用以下命令安装yfinance包:

!pip install yfinance

我们将下载一个数据集,其中包含从 2015 年到 2020 年 1 月 1 日的标准普尔 500 指数的开盘价、最高价、最低价、收盘价、调整后收盘价和成交量值。标准普尔 500 指数是衡量美国 500 家大型上市公司股票表现的股市指数:

import yfinance as yahoo
spx_data_frame = yahoo.download(“^GSPC”, “2015-01-01”, “2020-01-01”)

注意

数据集文件也可以在我们的 GitHub 仓库找到:packt.live/3fRI5Hk

原始数据集可以在这里找到:github.com/ranaroussi/yfinance

这就是你需要做的。包含标准普尔 500 指数的数据框已经准备好了。

你可以使用plot方法绘制指数收盘价:

spx_data_frame.Close.plot()

输出如下:

图 2.16:显示自 2015 年以来 S&P 500 指数收盘价的图表

图 2.16:显示自 2015 年以来 S&P 500 指数收盘价的图表

数据似乎不是线性的;多项式回归可能是该数据集更合适的模型。

也可以使用以下代码将数据保存为 CSV 文件:

spx_data_frame.to_csv(“yahoo_spx.csv”)

注意

www.quandl.com 是我们在本章中将使用的可靠的金融和经济数据集来源。

练习 2.02:使用 Quandl 加载股票价格

本练习的目标是从 Quandl 包下载数据,并将其加载到一个 DataFrame 中,类似于我们之前在 Yahoo Finance 上所做的操作。

以下步骤将帮助您完成练习:

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 使用以下命令安装 Quandl

    !pip install quandl
    
  3. 使用 Quandl 下载 S&P 500 的数据到 DataFrame 中。其代码为 “YALE/SPCOMP”

    import quandl
    data_frame = quandl.get(“YALE/SPCOMP”)
    
  4. 使用 DataFrame 的 head() 方法检查 DataFrame 中前五行数据:

    data_frame.head()
    

    输出如下:

    图 2.17:数据集作为输出显示

图 2.17:数据集作为输出显示

注意

要访问本节的源代码,请参考 packt.live/3dwDUz6

您也可以通过 packt.live/31812B6 在线运行此示例。您必须执行整个 Notebook 才能获得预期的结果。

完成本练习后,我们学会了如何下载外部数据集(CSV 格式)并将其导入为 DataFrame。我们还学习了 .head() 方法,它可以快速查看 DataFrame 中的前五行数据。

在下一节中,我们将继续准备数据集以执行多元线性回归。

为预测准备数据

在对数据集执行多元线性回归之前,我们必须选择相关的特征和回归所用的数据范围。

准备数据以进行预测是回归过程中的第二步。此步骤还包含几个子步骤。我们将在接下来的练习中逐步讲解这些子步骤。

练习 2.03:为预测准备 Quandl 数据

本练习的目标是从 Quandl 库下载外部数据集,并将其准备好以便在我们的线性回归模型中使用。

以下步骤将帮助您完成本练习:

  1. 打开一个新的 Jupyter Notebook 文件。

    注意

    如果您的系统未安装 Quandl 库,请记得运行命令 !pip install quandl

  2. 接下来,使用 Quandl 下载 S&P 500 从 1950 年到 2019 年的数据到 DataFrame 中。其代码为 “YALE/SPCOMP”

    import quandl
    import numpy as np
    from sklearn import preprocessing
    from sklearn import model_selection
    data_frame = quandl.get(“YALE/SPCOMP”, \
                            start_date=”1950-01-01”, \
                            end_date=”2019-12-31”)
    
  3. 使用 head() 方法来查看 data_frame.head() DataFrame 中的列:

    data_frame.head()
    

    输出如下:

    图 2.18:数据集作为输出显示

    图 2.18:数据集作为输出显示

    有些特征似乎高度相关。例如,Real Dividend列与Real Price按比例增长。它们之间的比率并不总是相似,但它们确实存在相关性。

    由于回归并不是为了检测特征之间的相关性,我们宁愿去除那些我们知道是相关的特征,并对那些没有相关性的特征进行回归。在这种情况下,我们将保留Long Interest RateReal PriceReal Dividend列。

  4. 仅保留Long Interest RateReal PriceReal Dividend数据框中相关的列:

    data_frame = data_frame[['Long Interest Rate', \
                             'Real Price', 'Real Dividend']]
    data_frame
    

    输出如下所示:

    图 2.19:仅显示相关列的数据集

    图 2.19:仅显示相关列的数据集

    你可以看到数据框中有一些缺失值NaN。由于回归无法处理缺失值,我们需要将它们替换或删除。在实际情况中,我们通常选择替换它们。在这种情况下,我们将使用一种叫做前向填充的方法,用前一个值来替换缺失值。

  5. 我们可以使用以下代码片段通过前向填充来替换缺失值:

    data_frame.fillna(method='ffill', inplace=True)
    data_frame
    

    输出如下所示:

    图 2.20:缺失值已被替换

    图 2.20:缺失值已被替换

    现在我们已经清理了缺失数据,需要创建我们的标签。我们希望使用当前的Real PriceLong Interest RateReal Dividend列预测Real Price列的未来 3 个月数据。为了创建标签,我们需要将Real Price的值向上移动三个单位,并将其命名为Real Price Label

  6. 使用以下代码通过将Real Price移位 3 个月来创建Real Price Label标签:

    data_frame['Real Price Label'] = data_frame['Real Price'].shift(-3)
    data_frame
    

    输出如下所示:

    图 2.21:新标签已创建

    图 2.21:新标签已创建

    将这些值移位的副作用是,Real Price Label的最后三行将出现缺失值,因此我们需要移除最后三行数据。然而,在此之前,我们需要将特征转换为 NumPy 数组并进行缩放。我们可以使用数据框的drop方法删除标签列,并使用sklearn中的预处理函数来缩放特征。

  7. 使用以下代码创建一个 NumPy 数组来表示特征并进行缩放:

    features = np.array(data_frame.drop('Real Price Label', 1))
    scaled_features = preprocessing.scale(features)
    scaled_features
    

    输出如下所示:

    array([[-1.14839975, -1.13009904, -1.19222544],
           [-1.14114523, -1.12483455, -1.18037146],
           [-1.13389072, -1.12377394, -1.17439424],
           ...,
           [-1.360812  ,  2.9384288 ,  3.65260385],
           [-1.32599032,  3.12619329,  3.65260385],
           [-1.29116864,  3.30013894,  3.65260385]])
    

    第二个参数中的1指定我们要删除列。由于原始数据框未被修改,因此可以直接从中提取标签。现在,特征已经被缩放,我们需要删除特征中的最后三个值,因为它们是标签列中缺失值的特征。我们将它们保存以供稍后在预测部分使用。

  8. 使用以下代码移除features数组中的最后三个值,并将它们保存到另一个数组中:

    scaled_features_latest_3 = scaled_features[-3:]
    scaled_features = scaled_features[:-3]
    scaled_features
    

    scaled_features的输出如下所示:

    array([[-1.14839975, -1.13009904, -1.19222544],
           [-1.14114523, -1.12483455, -1.18037146],
           [-1.13389072, -1.12377394, -1.17439424],
           ...,
           [-1.38866935,  2.97846643,  3.57443947],
           [-1.38866935,  2.83458633,  3.6161088 ],
           [-1.36429417,  2.95488131,  3.65260385]])
    

    scaled_features变量不再包含三个数据点,因为它们现在位于scaled_features_latest_3中。现在我们可以从 DataFrame 中删除最后三行缺失数据,然后使用sklearn将标签转换为 NumPy 数组。

  9. 在以下代码中删除缺失数据的行:

    data_frame.dropna(inplace=True)
    data_frame
    

    data_frame的输出如下:

    图 2.22:更新后的数据集,删除了缺失值

    图 2.22:更新后的数据集,删除了缺失值

    正如您所见,DataFrame 中的最后三行也被删除了。

  10. 现在让我们看看我们是否准确地创建了我们的标签。继续运行以下代码:

    label = np.array(data_frame['Real Price Label'])
    label
    

    label的输出如下:

    图 2.23:显示预期标签的输出

    图 2.23:显示预期标签的输出

    我们的变量包含所有标签,与 DataFrame 中的Real Price Label列完全相同。

    我们的下一个任务是将训练数据和测试数据分开。正如我们在将数据分割为训练和测试部分中看到的,我们将使用数据的 90%作为训练数据,剩下的 10%作为测试数据。

  11. 使用sklearn中的以下代码将features数据分为训练集和测试集:

    from sklearn import model_selection
    (features_train, features_test, \
    label_train, label_test) = model_selection\
                               .train_test_split(scaled_features, \
                                                 label, test_size=0.1, \
                                                 random_state=8)
    

    train_test_split函数对我们数据的行进行了洗牌,保持了对应关系,并将约 10%的所有数据放入测试变量中,保留了 90%用于训练变量。我们还使用random_state=8来复制结果。我们的数据现在可以用于多元线性回归模型。

    注意

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

    您还可以在packt.live/2zW8WCH上在线运行此示例。必须执行整个 Notebook 才能获得所需的结果。

通过完成此练习,我们已经学会了在执行回归之前进行数据准备的所有必要步骤。

执行和验证线性回归

现在我们的数据已准备好,我们可以执行线性回归。之后,我们将测量我们的模型性能,看看它的表现如何。

现在我们可以基于训练数据创建线性回归模型:

from sklearn import linear_model
model = linear_model.LinearRegression()
model.fit(features_train, label_train)

一旦模型准备好,我们可以使用它来预测属于测试特征值的标签,并使用模型的score方法查看其准确性:

label_predicted = model.predict(features_test)
model.score(features_test, label_test)

输出如下:

0.9847223874806746

通过得分或 R2 为0.985,我们可以得出结论,该模型非常准确。这并不令人意外,因为金融市场的增长约为每年 6-7%。这是线性增长,模型基本上预测市场将以线性速度继续增长。得出市场 tend to increase in the long run 的结论并不是火箭科学。

预测未来

现在我们的模型已经训练完成,我们可以用它来预测未来的值。我们将使用scaled_features_latest_3变量,它是通过取特征 NumPy 数组中的最后三个值来创建的,并使用它来预测接下来三个月的指数价格,代码如下:

label_predicted = model.predict(scaled_features_latest_3) 

输出结果如下:

array ([3046.2347327, 3171.47495182, 3287.48258298])

通过观察输出,你可能会觉得预测标普 500 指数的值似乎很容易,并用它来通过投资赚钱。不幸的是,在实践中,使用这个模型来通过预测赚钱并不比在赌场赌博更好。这只是一个用来说明预测的例子,并不足以用于市场价格的短期或长期投机。除此之外,股价对许多外部因素敏感,如经济衰退和政府政策。这意味着过去的模式不一定能反映未来的模式。

多项式与支持向量回归

在执行多项式回归时,xy之间的关系,或者使用它们的其他名称,特征和标签,并不是线性方程,而是多项式方程。这意味着我们可以在方程中使用多个系数和多个x的幂,而不是29方程。

更复杂的是,我们可以使用多个变量进行多项式回归,其中每个特征可能有不同的系数,分别乘以特征的不同幂。

我们的任务是找到一条最能拟合我们数据集的曲线。一旦将多项式回归扩展到多个变量,我们将学习支持向量机(SVM)模型来执行多项式回归。

一维多项式回归

回顾一下,到目前为止我们已经执行了两种回归:

  • 简单线性回归:30

  • 多元线性回归:31

我们现在将学习如何进行一维多项式线性回归。多项式线性回归的方程是33

多项式线性回归具有一个系数向量34,乘以一个多项式中x的幂次向量35

有时,多项式回归比线性回归效果更好。如果标签与特征之间的关系可以用线性方程描述,那么使用线性方程是非常合适的。如果我们有非线性增长的情况,多项式回归往往能更好地逼近特征与标签之间的关系。

最简单的一维线性回归实现是 NumPy 库中的polyfit方法。在接下来的练习中,我们将执行二次和三次的多项式线性回归。

注意

即使我们的多项式回归方程包含x的高阶系数,文献中仍将此方程称为多项式线性回归。回归之所以是线性的,并不是因为我们限制了方程中x的更高次幂的使用,而是因为方程中的系数a1、a2 等是线性的。这意味着我们使用线性代数的工具集,通过矩阵和向量来找到最小化逼近误差的缺失系数。

练习 2.04:一次、二次和三次多项式回归

此练习的目标是在本章早些时候使用的两个样本数据集上执行一次、二次和三次多项式回归。第一个数据集具有线性分布,第二个是斐波那契序列,具有非线性分布。

以下步骤将帮助您完成此练习:

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 导入numpymatplotlib包:

    import numpy as np
    from matplotlib import pyplot as plot
    
  3. 定义第一个数据集:

    x1 = np.array(range(1, 14))
    y1 = np.array([2, 8, 8, 18, 25, 21, 32, \
                   44, 32, 48, 61, 45, 62])
    
  4. 定义第二个数据集:

    x2 = np.array(range(1, 14))
    y2 = np.array([0, 1, 1, 2, 3, 5, 8, 13, \
                   21, 34, 55, 89, 144])
    
  5. 使用以下代码在第一个数据集上进行一次、二次和三次多项式回归,使用numpy中的polyfit方法:

    f1 = np.poly1d(np.polyfit(x1, y1, 1))
    f2 = np.poly1d(np.polyfit(x1, y1, 2))
    f3 = np.poly1d(np.polyfit(x1, y1, 3))
    

    函数f1的输出如下:

    poly1d([ 4.85714286, -2.76923077])
    

    如您所见,一次多项式回归具有两个系数。

    函数f2的输出如下:

    poly1d([-0.03196803, 5.3046953, -3.88811189])
    

    如您所见,二次多项式回归具有三个系数。

    函数f3的输出如下:

    poly1d([-0.01136364, 0.20666833, -3.91833167, -1.97902098])
    

    如您所见,三次多项式回归具有四个系数。

    现在我们已经计算了三次多项式回归,可以将它们与数据一起绘制在图表上,观察它们的行为。

  6. 在以下代码中将三个多项式回归和数据绘制在图表上:

    import matplotlib.pyplot as plot
    plot.plot(x1, y1, 'ko', # black dots \
              x1, f1(x1),'k-',  # straight line \
              x1, f2(x1),'k--',  # black dashed line \
              x1, f3(x1),'k-.' # dot line
    )
    plot.show()
    

    输出如下:

    图 2.24:显示第一个数据集多项式回归的图表

    图 2.24:显示第一个数据集多项式回归的图表

    当系数按从左到右的降阶顺序枚举时,我们可以看到更高阶的系数保持接近可忽略的状态。换句话说,三条曲线几乎重合,我们只能在右边缘附近检测到分歧。这是因为我们处理的数据集可以用线性模型非常好地逼近。

    实际上,第一个数据集是基于线性函数创建的。任何非零的x2 和x3 系数都是基于可用数据过拟合模型的结果。与任何更高阶多项式相比,线性模型更适合预测训练数据范围之外的值。

    让我们将这种行为与第二个例子进行对比。我们知道斐波那契序列是非线性的。因此,使用线性方程来近似它是欠拟合的明显情况。在这里,我们期望更高的多项式次数能够表现更好。

  7. 使用以下代码,使用numpy中的polyfit方法在第二组数据集上执行 1 次、2 次和 3 次的多项式回归:

    g1 = np.poly1d(np.polyfit(x2, y2, 1))
    g2 = np.poly1d(np.polyfit(x2, y2, 2))
    g3 = np.poly1d(np.polyfit(x2, y2, 3))
    

    g1的输出如下:

    poly1d([ 9.12087912, -34.92307692])
    

    如你所见,1 次多项式回归有 2 个系数。

    g2的输出如下:

    poly1d([ 1.75024975, -15.38261738, 26.33566434])
    

    如你所见,2 次多项式回归有 3 个系数。

    g3的输出如下:

    poly1d([ 0.2465035, -3.42632368, 14.69080919, -15.07692308])
    

    如你所见,3 次多项式回归有 4 个系数。

  8. 在以下代码中绘制三条多项式回归曲线和数据点:

    plot.plot(x2, y2, 'ko', # black dots \
              x2, g1(x2),'k-',  # straight line \
              x2, g2(x2),'k--',  # black dashed line \
              x2, g3(x2),'k-.' # dot line
    )
    plot.show()
    

    输出如下:

    图 2.25:展示第二组数据点和三条多项式曲线

    ](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_02_25.jpg)

图 2.25:展示第二组数据点和三条多项式曲线

差异很明显。二次曲线比线性曲线更好地拟合数据点。三次曲线更好。

注意

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

你也可以在packt.live/2B09xDN上在线运行这个示例。你必须执行整个笔记本才能获得期望的结果。

如果你研究比内特公式,你会发现斐波那契函数是一个指数函数,因为第n个斐波那契数是常数的n次幂。因此,使用的多项式次数越高,我们的近似值就会越精确。

多变量的多项式回归

当我们有一个次数为n的变量时,方程中有n+1个系数,如36所示。

一旦我们处理了多个特征,x1, x2, …, xm,以及它们的最高次幂为n时,我们将得到一个m * (n+1)的系数矩阵。当我们开始探索细节并证明多项式模型如何工作时,数学推导会变得相当冗长。我们还会失去二维曲线的美观可视化。

因此,我们将应用在前一节中学习的单变量多项式回归的概念,并省略数学推导。当训练和测试一个线性回归模型时,我们可以计算均方误差来衡量模型的拟合效果。

在 scikit-learn 中,用于近似的多项式次数是模型中的一个简单参数。

由于多项式回归是一种线性回归形式,我们可以在不改变回归模型的情况下执行多项式回归。我们需要做的只是转换输入并保持线性回归模型。输入的转换通过PolynomialFeatures包的fit_transform方法完成。

首先,我们可以重用练习 2.03中的代码,为预测准备 Quandl 数据,直到步骤 9,并从sklearnpreprocessing模块中导入PolynomialFeatures

!pip install quandl
import quandl
import numpy as np
from sklearn import preprocessing
from sklearn import model_selection
from sklearn import linear_model
from matplotlib import pyplot as plot
from sklearn.preprocessing import PolynomialFeatures
data_frame = quandl.get(“YALE/SPCOMP”, \
                        start_date=”1950-01-01”, \
                        end_date=”2019-12-31”)
data_frame = data_frame[['Long Interest Rate', \
                         'Real Price', 'Real Dividend']]
data_frame.fillna(method='ffill', inplace=True)
data_frame['Real Price Label'] = data_frame['Real Price'].shift(-3)
features = np.array(data_frame.drop('Real Price Label', 1))
scaled_features = preprocessing.scale(features)
scaled_features_latest_3 = scaled_features[-3:]
scaled_features = scaled_features[:-3]
data_frame.dropna(inplace=True)
label = np.array(data_frame['Real Price Label'])

现在,我们可以使用PolynomialFeaturesfit_transform方法创建一个三次多项式回归:

poly_regressor = PolynomialFeatures(degree=3)
poly_scaled_features = poly_regressor.fit_transform(scaled_features)
poly_scaled_features

poly_scaled_features 的输出如下:

array([[ 1\.        , -1.14839975, -1.13009904, ..., -1.52261953,
        -1.60632446, -1.69463102],
       [ 1\.        , -1.14114523, -1.12483455, ..., -1.49346824,
        -1.56720585, -1.64458414],
       [ 1\.        , -1.13389072, -1.12377394, ..., -1.48310475,
        -1.54991107, -1.61972667],
       ...,
       [ 1\.        , -1.38866935,  2.97846643, ..., 31.70979016,
        38.05472653, 45.66924612],
       [ 1\.        , -1.38866935,  2.83458633, ..., 29.05499915,
        37.06573938, 47.28511704],
       [ 1\.        , -1.36429417,  2.95488131, ..., 31.89206605,
        39.42259303, 48.73126873]])

然后,我们需要将数据分为测试集和训练集:

(poly_features_train, poly_features_test, \
poly_label_train, poly_label_test) = \
model_selection.train_test_split(poly_scaled_features, \
                                 label, test_size=0.1, \
                                 random_state=8)

train_test_split 函数会打乱数据行,保持其对应关系,并将大约 10% 的数据放入测试变量,剩下的 90% 用于训练变量。我们还使用 random_state=8 来重现结果。

我们的数据现在已准备好用于多项式回归模型;我们还将使用 score 函数来衡量其性能:

model = linear_model.LinearRegression()
model.fit(poly_features_train, poly_label_train)
model.score(poly_features_test, poly_label_test)

输出如下:

0.988000620369118

在得分或 R2 为 0.988 的情况下,我们的多项式回归模型略优于我们在练习 2.03为预测准备 Quandl 数据中构建的多元线性回归模型(0.985)。可能两者模型都存在过拟合数据集的情况。

scikit-learn 中还有另一个执行多项式回归的模型,称为 SVM 模型。

支持向量回归

SVM 是二分类器,通常用于分类问题(你将在第三章分类简介中学到更多)。SVM 分类器接收数据并尝试预测数据属于哪个类别。一旦确定了数据点的分类,它就会被标记。但 SVM 也可以用于回归;也就是说,除了标记数据,它还可以预测序列中的未来值。

SVR 模型使用我们数据之间的空间作为误差范围。基于误差范围,它对未来值进行预测。

如果误差范围过小,我们可能会导致模型过拟合现有数据集。如果误差范围过大,我们可能会导致模型欠拟合现有数据集。

对于分类器,核函数描述了划分状态空间的曲面,而在回归中,核函数衡量误差范围。这个核可以使用线性模型、多项式模型或许多其他可能的模型。默认的核函数是 RBF,即 径向基函数

SVR 是一个高级话题,超出了本书的范围。因此,我们只会坚持一个简单的演示,作为在数据上尝试另一个回归模型的机会。

我们可以重复使用练习 2.03为预测准备 Quandl 数据中的代码,直到步骤 11

import quandl
import numpy as np
from sklearn import preprocessing
from sklearn import model_selection
from sklearn import linear_model
from matplotlib import pyplot as plot

data_frame = quandl.get(“YALE/SPCOMP”, \
                        start_date=”1950-01-01”, \
                        end_date=”2019-12-31”)
data_frame = data_frame[['Long Interest Rate', \
                         'Real Price', 'Real Dividend']]
data_frame.fillna(method='ffill', inplace=True)
data_frame['Real Price Label'] = data_frame['Real Price'].shift(-3)
features = np.array(data_frame.drop('Real Price Label', 1))
scaled_features = preprocessing.scale(features)
scaled_features_latest_3 = scaled_features[-3:]
scaled_features = scaled_features[:-3]
data_frame.dropna(inplace=True)
label = np.array(data_frame['Real Price Label'])
(features_train, features_test, label_train, label_test) = \
model_selection.train_test_split(scaled_features, label, \
                                 test_size=0.1, \
                                 random_state=8)

然后,我们可以通过简单地将线性模型改为支持向量模型,使用 sklearn 中的 svm 方法来执行回归:

from sklearn import svm
model = svm.SVR()
model.fit(features_train, label_train)

如你所见,执行 SVR 与执行线性回归完全相同,唯一的区别是将模型定义为 svm.SVR()

最后,我们可以预测并衡量模型的性能:

label_predicted = model.predict(features_test)
model.score(features_test, label_test)

输出如下:

0.03262153550014424

如你所见,得分或 R2 值相当低,我们的 SVR 参数需要优化,以提高模型的准确性。

具有 3 次多项式核的支持向量机

让我们将 SVM 的核函数切换为多项式函数(默认度数为3),并测量新模型的性能:

model = svm.SVR(kernel='poly') 
model.fit(features_train, label_train) 
label_predicted = model.predict(features_test) 
model.score(features_test, label_test)

输出如下:

0.44465054598560627

我们通过将 SVM 的核函数更改为多项式函数,成功提高了模型的性能;然而,模型仍然需要大量的调优,才能达到线性回归模型的相同性能。

活动 2.01:使用多项式回归(1、2、3 次)和多个变量预测波士顿房价

在这个活动中,你需要使用 scikit-learn 进行 1、2、3 次线性多项式回归,并找到最优模型。你将使用波士顿房价数据集。波士顿房价数据集非常著名,并已作为回归模型研究的示例。

注意

关于波士顿房价数据集的更多详情,请访问archive.ics.uci.edu/ml/machine-learning-databases/housing/

数据集文件也可以在我们的 GitHub 仓库中找到:packt.live/2V9kRUU

你需要根据波士顿房屋的特征(特征)预测房价(标签)。你的主要目标是使用数据集中的所有特征,构建 3 个线性模型,分别是 1 次、2 次和 3 次多项式回归。你可以找到以下数据集描述:

图 2.26:波士顿房价数据集描述

图 2.26:波士顿房价数据集描述

我们将定义MEDV字段作为标签,它是房屋的中位数价格(单位:千美元)。所有其他字段将作为我们模型的特征。由于这个数据集没有缺失值,我们不需要像之前的练习那样填补缺失值。

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

  1. 打开一个 Jupyter Notebook。

  2. 导入所需的包并将波士顿房价数据加载到一个 DataFrame 中。

  3. 准备数据集进行预测,将标签和特征转换为 NumPy 数组,并对特征进行缩放。

  4. 通过将缩放后的特征转换为适合每个多项式回归的格式,创建三组不同的特征。

  5. 将数据集拆分为训练集和测试集,random state = 8

  6. 执行一个一次多项式回归,并评估模型是否存在过拟合现象。

  7. 执行一个二次多项式回归,并评估模型是否存在过拟合现象。

  8. 执行一个三次多项式回归,并评估模型是否存在过拟合现象。

  9. 比较三种模型在测试集上预测的结果与标签的差异。

预期输出如下:

图 2.27:基于预测的预期输出

图 2.27:基于预测的预期输出

注意

这个活动的解决方案可以在第 334 页找到。

总结

在本章中,我们已经学习了线性回归的基础知识。在了解了一些基础数学知识后,我们研究了使用单个变量和多个变量的线性回归数学原理。

然后,我们学习了如何从外部数据源加载数据,如 CSV 文件、Yahoo Finance 和 Quandl。在加载数据后,我们学习了如何识别特征和标签,如何对数据进行缩放,以及如何格式化数据以执行回归。

我们学习了如何训练和测试线性回归模型,以及如何预测未来。我们的结果通过一个易于使用的 Python 图形绘图库pyplot进行了可视化。

我们还学习了更复杂的线性回归形式:使用任意次数的线性多项式回归。我们学习了如何在多个变量上定义这些回归问题,并比较它们在波士顿房价数据集上的表现。作为多项式回归的替代方法,我们还介绍了支持向量机(SVM)作为回归模型,并试验了两种核函数。

在下一章中,你将学习分类及其模型。

第三章:3. 分类简介

概述

本章将带您了解分类。您将实现各种技术,如 k 近邻和支持向量机(SVM)。您将使用欧几里得距离和曼哈顿距离来处理 k 近邻算法。您将应用这些概念来解决一些有趣的问题,例如预测一个信用卡申请人是否有违约风险,或者判断一个员工是否会在公司工作超过两年。在本章结束时,您将足够自信使用分类来处理任何数据,并得出明确结论。

简介

在上一章中,您了解了回归模型,并学习了如何拟合一个包含单一或多个变量的线性回归模型,以及如何使用高次多项式进行拟合。

与回归模型不同,回归模型侧重于学习如何预测连续的数值(这些数值可以有无限多种可能性),而分类问题(将在本章中介绍)则是将数据划分成不同的组,也叫做类。

例如,模型可以被训练来分析电子邮件,并预测它们是否是垃圾邮件。在这种情况下,数据被分类为两种可能的类别(或类)。这种分类类型也称为二分类,我们将在本章中看到一些例子。然而,如果有多个(超过两个)类别(或类),那么您将处理多分类问题(您将在第四章,决策树简介中遇到一些示例)。

那么什么是真实世界中的分类问题呢?考虑一个模型,试图预测一个用户对电影的评分,其中评分只能取以下值:喜欢中立不喜欢。这是一个分类问题。

在本章中,我们将学习如何使用 k 近邻分类器和 SVM 算法进行数据分类。就像我们在上一章中做的回归一样,我们将基于清理和准备好的训练数据来构建分类器,并使用测试数据来测试分类器的性能。

我们将从分类的基础知识开始。

分类的基础

如前所述,任何分类问题的目标是使用训练集将数据准确地划分为相关的组。此类项目在不同行业中有很多应用,例如在教育行业中,模型可以预测学生是否通过考试;在医疗保健行业中,模型可以评估每位患者某种疾病的严重程度。

分类器是一个模型,用于确定任何数据点所属的标签(输出)或值(类)。例如,假设您有一组观察数据,包含信用良好的个人,另一组则包含信用偿还倾向上存在风险的个人。

我们将第一组称为 P,第二组称为 Q。以下是此类数据的示例:

图 3.1:示例数据集

](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_03_01.jpg)

图 3.1:示例数据集

使用这些数据,你将训练一个分类模型,该模型能够将新的观察结果正确分类到这两组之一(这是二分类问题)。模型可以找到一些模式,例如年薪超过 60,000 美元的人风险较低,或者拥有超过 10 倍的按揭/收入比率使个人更有可能无法偿还债务。这将是一个多类别分类练习。

分类模型可以分为不同的算法家族。最著名的几种如下:

  • 基于距离的算法,如k-近邻

  • 线性模型,如逻辑回归支持向量机(SVM)

  • 基于树的算法,如随机森林

在本章中,你将接触到来自前两种类型家族的两种算法:k-近邻(基于距离的)和支持向量机(SVM)(线性模型)。

注意

我们将在第四章《决策树简介》中为你讲解基于树的算法,如随机森林。

但是,在深入研究模型之前,我们需要清理并准备本章将要使用的数据集。

在接下来的部分,我们将使用德国信用批准数据集,并进行所有数据准备,以便进入建模阶段。我们先从加载数据开始。

练习 3.01:预测信用卡违约风险(加载数据集)

在本练习中,我们将数据集加载到 pandas DataFrame 中并探索其内容。我们将使用德国信用批准的数据集来判断一个人是否有违约风险。

注意

该数据集的 CSV 版本可以在我们的 GitHub 仓库中找到:

packt.live/3eriWTr

原始数据集及其相关信息可以在 archive.ics.uci.edu/ml/datasets/Statlog+%28German+Credit+Data%29 找到。

数据文件位于 archive.ics.uci.edu/ml/machine-learning-databases/statlog/german/

引用 - Dua, D., & Graff, C.. (2017). UCI 机器学习库

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 导入 pandas 包并将其命名为 pd

    import pandas as pd
    
  3. 创建一个新变量 file_url,它将包含原始数据集文件的 URL,如以下代码片段所示:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/'\
               'The-Applied-Artificial-Intelligence-Workshop/'\
               'master/Datasets/german_credit.csv'
    
  4. 使用 pd.read_csv() 方法导入数据:

    df = pd.read_csv(file_url)
    
  5. 使用 df.head() 打印 DataFrame 的前五行:

    df.head()
    

    期望的输出是这样的:

    图 3.2:数据集的前五行

    图 3.2:数据集的前五行

    如你所见,前面的截图输出展示了数据集的特征,这些特征可以是数值型或类别型(文本)。

  6. 现在,使用df.tail()打印 DataFrame 的最后五行:

    df.tail()
    

    预期的输出是这样的:

    图 3.3:数据集的最后五行

    图 3.3:数据集的最后五行

    DataFrame 的最后几行与我们之前看到的前几行非常相似,因此我们可以假设行间结构是一致的。

  7. 现在,使用df.dtypes打印列及其数据类型的列表:

    df.dtypes
    

    预期的输出是这样的:

    图 3.4:列及其数据类型的列表

    ](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_03_04.jpg)

图 3.4:列及其数据类型的列表

注意

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

你还可以在线运行此示例,网址为packt.live/3fN0DrT。你必须执行整个 Notebook,才能获得期望的结果。

从前面的输出中,我们可以看到该 DataFrame 有一些数值特征(int64),但也有文本特征(object)。我们还可以看到这些特征大多数是与个人相关的细节,如年龄,或财务信息,如信用历史或信用额度。

通过完成这个练习,我们已经成功地将数据加载到 DataFrame 中,并且初步了解了它所包含的特征和信息。

在接下来的章节中,我们将讨论如何对这些数据进行预处理。

数据预处理

在构建分类器之前,我们需要格式化数据,以便将相关数据保持在最适合分类的格式中,并删除我们不感兴趣的所有数据。

以下几点是实现这一目标的最佳方法:

  • 数据集中的N/A(或NA)值,我们可能最好将这些值替换为我们可以处理的数值。回顾上一章,NA表示缺失值,或者将其替换为异常值。

    df.fillna(-1000000, inplace=True)
    

    fillna()方法将所有NA值更改为数值。

    这个数值应该远离 DataFrame 中任何合理的值。负一百万被分类器识别为异常值,假设只有正值存在,如前面的说明所提到的。

  • 0) 指定我们删除行,而不是列。第二个参数(inplace=True)指定我们在不克隆 DataFrame 的情况下执行删除操作,并将结果保存在同一个 DataFrame 中。由于该 DataFrame 没有缺失值,因此dropna()方法没有改变 DataFrame。

    df.drop(['telephone'], 1, inplace=True)
    

    第二个参数(值为1)表示我们要删除列,而不是行。第一个参数是我们想要删除的列的枚举(在这里是['telephone'])。inplace参数用于让该操作修改原始的 DataFrame。

  • MinMaxScaler方法属于 scikit-learn 的preprocessing工具,代码片段如下所示:

    from sklearn import preprocessing
    import numpy as np
    data = np.array([[19, 65], \
                     [4, 52], \
                     [2, 33]])
    preprocessing.MinMaxScaler(feature_range=(0,1)).fit_transform(data)
    

    预期的输出是这样的:

    array([[1\.        , 1\.        ],
           [0.11764706, 0.59375   ],
           [0\.        , 0\.        ]])
    

    二值化将数据基于条件转换为 1 和 0,如下代码片段所示:

    preprocessing.Binarizer(threshold=10).transform(data)
    

    预期输出是这样的:

    array([[1, 1],
           [0, 1],
           [0, 1]])
    

在上面的例子中,我们根据每个值是否大于 10(由 threshold=10 参数定义)将原始数据 ([19, 65],[4, 52],[2, 33]) 转换为二进制形式。例如,第一个值 19 大于 10,因此在结果中被替换为 1

标签编码对于准备特征(输入)以进入建模阶段非常重要。尽管某些特征是字符串标签,scikit-learn 算法期望这些数据转换为数字。

这时,scikit-learn 的 preprocessing 库派上了用场。

注意

你可能注意到,在信用评分的例子中,有两个数据文件。一个包含字符串形式的标签,另一个包含整数形式的标签。我们加载了带字符串标签的数据,以便你能体验如何使用标签编码器正确地预处理数据。

标签编码不是火箭科学。它创建了字符串标签和数值之间的映射,以便我们可以向 scikit-learn 提供数字,以下是一个示例:

from sklearn import preprocessing
labels = ['Monday', 'Tuesday', 'Wednesday', \
          'Thursday', 'Friday']
label_encoder = preprocessing.LabelEncoder()
label_encoder.fit(labels)

让我们列举一下编码:

[x for x in enumerate(label_encoder.classes_)]

预期输出是这样的:

[(0, 'Friday'),
 (1, 'Monday'),
 (2, 'Thursday'),
 (3, 'Tuesday'),
 (4, 'Wednesday')]

上面的结果显示,scikit-learn 为每周的每一天创建了一个映射关系;例如,Friday 映射为 0Tuesday 映射为 3

注意

默认情况下,scikit-learn 通过按字母顺序排序原始值来分配映射的数字。这就是为什么 Friday 被映射为 0 的原因。

现在,我们可以使用这个映射(也叫做编码器)来转换数据。

让我们通过 transform() 方法在两个例子上试试:WednesdayFriday

label_encoder.transform(['Wednesday', 'Friday'])

预期输出是这样的:

array([4, 0], dtype=int64)

如预期所示,我们得到了 40 的结果,这分别是 WednesdayFriday 的映射值。

我们还可以使用这个编码器通过 inverse_transform 函数执行逆向转换。让我们用值 04 来试试:

label_encoder.inverse_transform([0, 4])

预期输出是这样的:

array(['Friday', 'Wednesday'], dtype='<U9')

如预期所示,我们得到了 FridayWednesday 的值。现在,让我们在德国数据集上练习我们学到的内容。

练习 3.02:应用标签编码将类别变量转换为数值变量

在本练习中,我们将使用我们刚学到的一个预处理技术——标签编码,将所有类别变量转换为数值变量。在训练任何机器学习模型之前,这一步是必要的。

注意

我们将使用与上一个练习中相同的数据集:德国信用审批数据集:packt.live/3eriWTr

以下步骤将帮助你完成此练习:

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 导入 pandas 包并命名为 pd

    import pandas as pd
    
  3. 创建一个新的变量 file_url,其中将包含原始数据集的 URL:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/'\
               'The-Applied-Artificial-Intelligence-Workshop/'\
               'master/Datasets/german_credit.csv'
    
  4. 使用 pd.read_csv() 方法加载数据:

    df = pd.read_csv(file_url)
    
  5. 导入scikit-learn中的preprocessing

    from sklearn import preprocessing
    
  6. 定义一个名为fit_encoder()的函数,该函数接受一个 DataFrame 和列名作为参数,并在该列的值上拟合一个标签编码器。你将使用preprocessing中的.LabelEncoder().fit()以及pandas中的.unique()(它将提取 DataFrame 列中的所有可能值):

    def fit_encoder(dataframe, column):
        encoder = preprocessing.LabelEncoder()
        encoder.fit(dataframe[column].unique())
        return encoder
    
  7. 定义一个名为encode()的函数,该函数接受一个 DataFrame、列名和标签编码器作为参数,并使用标签编码器转换该列的值。你将使用.transform()方法来完成这项工作:

    def encode(dataframe, column, encoder):
        return encoder.transform(dataframe[column])
    
  8. 创建一个名为cat_df的新 DataFrame,其中只包含非数字列,并打印其前五行。你将使用 pandas 中的.select_dtypes()方法,并指定exclude='number'

    cat_df = df.select_dtypes(exclude='number')
    cat_df.head()
    

    预期输出(并未显示所有列)如下:

    图 3.5:只包含非数字列的 DataFrame 前五行

    图 3.5:只包含非数字列的 DataFrame 前五行

  9. 创建一个名为cat_cols的列表,其中包含cat_df的列名,并打印其内容。你将使用 pandas 中的.columns来完成:

    cat_cols = cat_df.columns
    cat_cols
    

    预期输出如下:

    Index(['account_check_status', 'credit_history', 'purpose', 
           'savings', 'present_emp_since', 'other_debtors', 
           'property', 'other_installment_plans', 'housing', 
           'job', 'telephone', 'foreign_worker'], dtype='object')
    
  10. 创建一个for循环,迭代cat_cols中的每一列,使用fit_encoder()来拟合标签编码器,并用encode()函数转换该列:

    for col in cat_cols:
        label_encoder = fit_encoder(df, col)
        df[col] = encode(df, col, label_encoder)
    
  11. 打印df的前五行:

    df.head()
    

    预期输出如下:

    图 3.6:编码后的 DataFrame 前五行

图 3.6:编码后的 DataFrame 前五行

注意

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

你也可以在网上运行这个示例,网址是packt.live/2YZhtx5。你必须执行整个 Notebook 才能得到期望的结果。

我们已经成功编码了非数字列。现在,我们的 DataFrame 仅包含数字值。

特征和标签识别

在训练模型之前,我们仍然需要执行两个最终步骤。第一个步骤是将特征与标签分开(也称为响应变量或因变量)。label列是我们希望模型预测的内容。对于德国信用数据集,在我们的案例中,它将是名为default的列,它告诉我们一个人是否存在违约风险。

特征是数据集中所有其他列。模型将使用这些列中的信息,找到相关的模式,以准确预测相应的标签。

scikit-learn 包要求标签和特征存储在两个不同的变量中。幸运的是,pandas 包提供了一个方法.pop()来提取 DataFrame 中的一列。

我们将提取default列,并将其存储在一个名为label的变量中:

label = df.pop('default')
label

预期输出如下:

0      0
1      1
2      0
3      0
4      1
      ..
995    0
996    0
997    0
998    1
999    0
Name: default, Length: 1000, dtype: int64

现在,如果我们查看df的内容,我们会看到default列已经不再存在:

df.columns

预期的输出结果是这样的:

Index(['account_check_status', 'duration_in_month',
       'credit_history', 'purpose', 'credit_amount',
       'savings', 'present_emp_since',
       'installment_as_income_perc', 'other_debtors',
       'present_res_since', 'property', 'age',
       'other_installment_plans', 'housing', 
       'credits_this_bank', 'job', 'people_under_maintenance',
       'telephone', 'foreign_worker'],
      dtype='object')

现在我们已经准备好了特征和标签,接下来需要将数据集分成训练集和测试集。

使用 Scikit-Learn 拆分数据为训练集和测试集

在训练分类器之前需要完成的最后一步是将数据拆分为训练集和测试集。我们已经在第二章回归简介中看过如何做了:

from sklearn import model_selection
features_train, features_test, \
label_train, label_test = \
model_selection.train_test_split(df, label, test_size=0.1, \
                                 random_state=8)

train_test_split方法会将我们的特征和标签打乱并拆分为训练集和测试集。

我们可以将测试集的大小指定为介于01之间的一个数字。test_size0.1意味着10%的数据将进入测试数据集。你还可以指定random_state,这样如果再次运行这段代码,结果将是完全相同的。

我们将使用训练集来训练我们的分类器,并使用测试集来评估其预测性能。通过这种方式,我们可以评估我们的模型是否过拟合,并且是否学习到仅对训练集相关的模式。

在接下来的章节中,我们将为你介绍著名的 k-近邻分类器。

K-近邻分类器

现在我们已经有了训练数据和测试数据,接下来是准备我们的分类器来执行 k-近邻分类。在介绍完 k-近邻算法后,我们将使用 scikit-learn 来执行分类。

介绍 K-近邻算法(KNN)

分类算法的目标是将数据划分,以便我们能够确定哪些数据点属于哪个组。

假设我们给定了一组已分类的点。我们的任务是确定一个新的数据点属于哪个类别。

为了训练一个 k-近邻分类器(也称为 KNN),我们需要为训练集中的每个观测值提供相应的类别,也就是它属于哪个组。该算法的目标是找出特征之间的相关关系或模式,这些关系或模式将引导至这个类别。k-近邻算法基于一种接近度度量,计算数据点之间的距离。

两种最著名的接近度(或距离)度量是欧几里得距离和曼哈顿距离。我们将在下一节中详细介绍。

对于任何新的给定点,KNN 将找到它的 k 个最近邻,查看这 k 个邻居中哪个类别最频繁,并将其分配给这个新的观测值。但你可能会问,k 是什么?确定 k 的值完全是任意的。你需要预先设置这个值。这不是一个可以由算法学习的参数;它需要由数据科学家设置。这类参数称为超参数。理论上,你可以将 k 的值设定为 1 到正无穷之间的任何数。

有两种主要的最佳实践需要考虑:

  • k 应该始终是一个奇数。这样做的原因是我们希望避免出现平局的情况。例如,如果你设置 k=4,恰好有两个邻居属于 A 类,另外两个邻居属于 B 类,那么 KNN 就无法决定应该选择哪个类。为了避免这种情况,最好选择 k=3k=5

  • k 越大,KNN 的准确度就越高。例如,如果我们比较 k=1k=15 的情况,后者会让你更有信心,因为 KNN 在做出决策之前会查看更多邻居。另一方面,k=1 只查看最接近的邻居,并将同一类分配给观测值。但我们怎么能确定它不是异常值或特殊情况呢?询问更多的邻居会降低做出错误决策的风险。不过,这也有一个缺点:k 越大,KNN 做出预测的时间就越长。这是因为它需要执行更多计算,才能获得观测点所有邻居之间的距离。因此,你需要找到一个“甜蜜点”,既能给出正确的预测,又不至于在预测时间上妥协太多。

使用 K 最近邻分类器的距离度量在 Scikit-Learn 中

许多距离度量方法都可以与 k 最近邻算法一起使用。我们将介绍其中最常用的两种:欧几里得距离和曼哈顿距离。

欧几里得距离

两个点 AB 之间的距离,其中 A=(a1, a2, …, an)B=(b1, b2, …, bn),是连接这两个点的线段的长度。例如,如果 A 和 B 是二维数据点,欧几里得距离 d 将如下所示:

图 3.7:A 点和 B 点之间的欧几里得距离的可视化表示

图 3.7:A 点和 B 点之间的欧几里得距离的可视化表示

计算欧几里得距离的公式如下:

图 3.8:A 点和 B 点之间的距离

图 3.8:A 点和 B 点之间的距离

因为本书将使用欧几里得距离,接下来我们来看一下如何使用 scikit-learn 来计算多个点之间的距离。

我们需要从 sklearn.metrics.pairwise 导入 euclidean_distances。这个函数接受两组点并返回一个矩阵,矩阵包含每个点与第一组和第二组点之间的成对距离。

让我们以一个观测点 Z 为例,其坐标为 (4, 4)。在这里,我们希望计算与另外 3 个点 A、B 和 C 的欧几里得距离,这些点的坐标分别为 (2, 3)、(3, 7) 和 (1, 6):

from sklearn.metrics.pairwise import euclidean_distances
observation = [4,4]
neighbors = [[2,3], [3,7], [1,6]]
euclidean_distances([observation], neighbors)

预期的输出结果如下:

array([[2.23606798, 3.16227766, 3.60555128]])

这里,Z=(4,4) 和 B=(3,7) 之间的距离大约为 3.162,这就是我们在输出中得到的结果。

我们还可以计算同一组中各点之间的欧几里得距离:

euclidean_distances(neighbors)

预期的输出是这样的:

array([[0\.        , 4.12310563, 3.16227766],
       [4.12310563, 0\.        , 2.23606798],
       [3.16227766, 2.23606798, 0\.        ]])

包含 0 值的对角线对应于每个数据点与自身的欧几里得距离。这个矩阵是关于对角线对称的,因为它计算了两个点之间的距离以及反向的距离。例如,第一行的值 4.12310563 是 A 和 B 之间的距离,而第二行的相同值则是 B 和 A 之间的距离。

曼哈顿/汉明距离

曼哈顿(或汉明)距离的公式与欧几里得距离非常相似,但它并不使用平方根,而是依赖于计算数据点坐标差的绝对值:

图 3.9:曼哈顿距离与汉明距离

图 3.9:曼哈顿距离与汉明距离

你可以把曼哈顿距离想象成我们在网格上计算距离,而不是使用直线:

图 3.10:曼哈顿距离在 A 和 B 之间的可视化表示

图 3.10:曼哈顿距离在 A 和 B 之间的可视化表示

如上图所示,曼哈顿距离将遵循网格定义的路径,从 A 到达 B。

另一个有趣的属性是,A 和 B 之间可能有多个最短路径,但它们的曼哈顿距离将相等。在上面的示例中,如果网格中的每个单元格表示 1 单位,那么所有三条突出显示的最短路径的曼哈顿距离将都是 9。

欧几里得距离是更准确的距离泛化方法,而曼哈顿距离稍微容易计算一些,因为你只需要计算绝对值之间的差异,而不是计算平方差后再取平方根。

练习 3.03:在 Matplotlib 中展示 K 最近邻分类器算法

假设我们有一份员工数据列表。我们的特征是每周工作的小时数和年薪。我们的标签表示员工是否在公司工作超过 2 年。如果停留时间少于 2 年,则用零表示,若大于或等于 2 年,则用一表示。

我们希望创建一个三近邻分类器,来判断一名员工是否会在公司待满至少 2 年。

然后,我们希望使用这个分类器来预测一名要求每周工作 32 小时并且年收入 52,000 美元的员工是否会在公司工作 2 年或更长时间。

按照以下步骤完成本练习:

注意

上述数据集可以在 GitHub 上找到,链接为 packt.live/2V5VaV9

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 导入 pandas 包并命名为 pd

    import pandas as pd
    
  3. 创建一个新的变量,命名为 file_url(),它将包含原始数据集的 URL:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/'\
               'The-Applied-Artificial-Intelligence-Workshop/'\
               'master/Datasets/employees_churned.csv'
    
  4. 使用 pd.read_csv() 方法加载数据:

    df = pd.read_csv(file_url)
    
  5. 打印数据框的行:

    df
    

    预期的输出是这样的:

    图 3.11:员工数据集的 DataFrame

    图 3.11:员工数据集的 DataFrame

  6. scikit-learn导入preprocessing

    from sklearn import preprocessing
    
  7. 实例化一个MinMaxScaler,并设置feature_range=(0,1),将其保存到名为scaler的变量中:

    scaler = preprocessing.MinMaxScaler(feature_range=(0,1))
    
  8. 使用.fit_transform()缩放数据框,将结果保存到名为scaled_employees的新变量中,并打印其内容:

    scaled_employees = scaler.fit_transform(df)
    scaled_employees
    

    预期输出如下:

    array([[0\.        , 0.18518519, 0\.        ],
           [0.2       , 0\.        , 0\.        ],
           [0.6       , 0.11111111, 0\.        ],
           [0.2       , 0.37037037, 0\.        ],
           [1\.        , 0.18518519, 0\.        ],
           [1\.        , 0.62962963, 1\.        ],
           [1\.        , 0.11111111, 1\.        ],
           [0.6       , 0.37037037, 1\.        ],
           [1\.        , 1\.        , 1\.        ],
           [0.6       , 0.55555556, 1\.        ]])
    

    在前面的代码片段中,我们已经将原始数据集缩放,使得所有值都在 0 到 1 之间。

  9. 从缩放后的数据中提取每一列,并将它们保存到名为hours_workedsalaryover_two_years的三个变量中,如下所示的代码片段所示:

    hours_worked = scaled_employees[:, 0]
    salary = scaled_employees[:, 1]
    over_two_years = scaled_employees[:, 2]
    
  10. 导入matplotlib.pyplot包,并命名为plt

    import matplotlib.pyplot as plt
    
  11. 使用plt.scatter创建两个散点图,hours_worked作为* x 轴,salary作为 y 轴,然后根据over_two_years的值创建不同的标记。你可以使用plt.xlabelplt.ylabel添加 x 轴和 y *轴的标签。使用plt.show()显示散点图:

    plt.scatter(hours_worked[:5], salary[:5], marker='+')
    plt.scatter(hours_worked[5:], salary[5:], marker='o')
    plt.xlabel("hours_worked")
    plt.ylabel("salary")
    plt.show()
    

    预期输出如下:

    图 3.12:缩放数据的散点图

    + points represent the employees that stayed less than 2 years, while the o ones are for the employees who stayed for more than 2 years. 
    

    现在,假设我们有一个新的观察值,并且我们想计算与缩放数据集中的数据的欧几里得距离。

  12. 创建一个名为observation的新变量,坐标为[0.5, 0.26]

    observation = [0.5, 0.26]
    
  13. sklearn.metrics.pairwise导入euclidean_distances函数:

    from sklearn.metrics.pairwise import euclidean_distances
    
  14. 创建一个名为features的新变量,它将提取缩放数据集中的前两列:

    features = scaled_employees[:,:2]
    
  15. 使用euclidean_distances计算observationfeatures之间的欧几里得距离,将结果保存到名为dist的变量中,并打印其值,如下所示的代码片段所示:

    dist = euclidean_distances([observation], features)
    dist
    

    预期输出如下:

    array([[0.50556627, 0.39698866, 0.17935412, 0.3196586 ,
            0.50556627, 0.62179262, 0.52169714, 0.14893495,
            0.89308454, 0.31201456]])
    

    注意

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

    你也可以在在线运行此示例,网址为packt.live/3esx7HF。你必须执行整个 Notebook 才能获得期望的结果。

从前面的输出中,我们可以看到三个最近的邻居如下:

  • 对于点[0.6, 0.37037037, 1.],欧几里得距离是0.1564897

  • 对于点[0.6, 0.11111111, 0.],欧几里得距离是0.17114358

  • 对于点[0.6, 0.55555556, 1.],欧几里得距离是0.32150303

如果选择k=3,KNN 将查看这三个最近邻居的类别,由于其中有两个的标签为1,它将把此类别分配给我们的新观察值[0.5, 0.26]。这意味着我们的三邻居分类器会将这个新员工分类为更有可能至少待满 2 年的员工。

通过完成此练习,我们了解了 KNN 分类器如何通过找到新观察值的三个最近邻居,并使用欧几里得距离将最频繁的类别分配给它来对新观察值进行分类。

在 scikit-learn 中对 K-最近邻分类器进行参数化

分类器的参数化是你微调分类器准确度的地方。由于我们尚未学习所有可能的 k 近邻变种,我们将专注于你基于本主题能够理解的参数:

注意

你可以在这里访问 k 近邻分类器的文档:scikit-learn.org/stable/modules/generated/sklearn.neighbors.KNeighborsClassifier.html

  • n_neighbors:这是 k 近邻算法中的 k 值。默认值为5

  • metric:在创建分类器时,你会看到一个名称——Minkowski。不要担心这个名字——你已经学过了第一阶和第二阶明可夫斯基度量。这个度量有一个power参数。对于p=1,明可夫斯基度量和曼哈顿度量相同;对于p=2,明可夫斯基度量和欧几里得度量相同。

  • p:这是明可夫斯基度量的幂次。默认值为2

创建分类器时,你必须指定以下这些参数:

classifier = neighbors.KNeighborsClassifier(n_neighbors=50, p=2)

然后,你需要使用训练数据来拟合 KNN 分类器:

classifier.fit(features, label)

predict()方法可用于预测任何新数据点的标签:

classifier.predict(new_data_point)

在下一个练习中,我们将使用 scikit-learn 的 KNN 实现,自动查找最近邻并分配相应的类别。

练习 3.04:在 scikit-learn 中进行 K 近邻分类

在本练习中,我们将使用 scikit-learn,自动训练一个 KNN 分类器,基于德国信用审批数据集,并尝试不同的n_neighborsp超参数值,以获得最优的输出值。在拟合 KNN 之前,我们需要对数据进行缩放。

按照以下步骤完成本练习:

注意

本练习是练习 3.02的延续,应用标签编码将分类变量转化为数值型。我们已经将练习 3.02的结果数据集保存到 GitHub 仓库:packt.live/2Yqdb2Q

  1. 打开一个新的 Jupyter Notebook。

  2. 导入pandas包并将其命名为pd

    import pandas as pd
    
  3. 创建一个名为file_url的新变量,该变量将包含原始数据集的 URL:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/'\
               'The-Applied-Artificial-Intelligence-Workshop/'\
               'master/Datasets/german_prepared.csv'
    
  4. 使用pd.read_csv()方法加载数据:

    df = pd.read_csv(file_url)
    
  5. scikit-learn导入preprocessing

    from sklearn import preprocessing
    
  6. 使用feature_range=(0,1)实例化MinMaxScaler,并将其保存为名为scaler的变量:

    scaler = preprocessing.MinMaxScaler(feature_range=(0,1))
    
  7. 拟合缩放器并使用.fit_transform()方法应用相应的转换到 DataFrame,并将结果保存到名为scaled_credit的变量中:

    scaled_credit = scaler.fit_transform(df)
    
  8. response变量(第一列)提取到一个新的变量中,命名为label

    label = scaled_credit[:, 0]
    
  9. 将特征(所有列,除了第一列)提取到一个名为features的新变量中:

    features = scaled_credit[:, 1:]
    
  10. sklearn导入model_selection.train_test_split

    from sklearn.model_selection import train_test_split
    
  11. 使用train_test_split将缩放后的数据集分成训练集和测试集,test_size=0.2random_state=7

    features_train, features_test, \
    label_train, label_test = \
    train_test_split(features, label, test_size=0.2, \
                     random_state=7)
    
  12. sklearn导入neighbors

    from sklearn import neighbors
    
  13. 实例化KNeighborsClassifier并将其保存到名为classifier的变量中:

    classifier = neighbors.KNeighborsClassifier()
    
  14. 在训练集上拟合 K 最近邻分类器:

    classifier.fit(features_train, label_train)
    

    由于我们没有提到 k 的值,默认值是5

  15. 使用.score()打印训练集的准确率:

    acc_train = classifier.score(features_train, label_train)
    acc_train
    

    你应该得到以下输出:

    0.78625
    

    通过这些操作,我们在训练集上获得了0.78625的准确率,使用的是默认的超参数值:k=5 和欧几里得距离。

    让我们看一下测试集的得分。

  16. 使用.score()打印测试集的准确率:

    acc_test = classifier.score(features_test, label_test)
    acc_test
    

    你应该得到以下输出:

    0.75
    

    测试集的准确率降至0.75。这意味着我们的模型出现了过拟合,不能很好地对未见过的数据进行泛化。在下一个活动中,我们将尝试不同的超参数值,看看是否能改善这一点。

    注意

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

    你还可以在线运行此示例,地址是packt.live/2VbDTKx。你必须执行整个 Notebook 才能获得预期结果。

在本练习中,我们学习了如何将数据集划分为训练集和测试集,并拟合 KNN 算法。我们的最终模型可以准确预测一个人 75%的概率是否会违约。

活动 3.01:提高信用评分的准确性

在本活动中,你将实现 K 最近邻分类器的参数化并观察最终结果。目前,信用评分的准确率是 75%。你需要找到一种方法,将其提高几个百分点。

你可以尝试不同的 k 值(510152550),以及欧几里得距离和曼哈顿距离。

注意

这项活动要求你首先完成练习 3.04scikit-learn 中的 K 最近邻分类,因为我们将在这里使用之前准备好的数据。

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

  1. sklearn导入neighbors

  2. 创建一个函数来实例化指定超参数的KNeighborsClassifier,用训练数据拟合,并返回训练集和测试集的准确率。

  3. 使用你创建的函数,评估 k =(510152550)时,欧几里得距离和曼哈顿距离的准确率。

  4. 寻找最佳的超参数组合。

预期输出如下:

(0.775, 0.785)

注意

该活动的解决方案可以在第 343 页找到。

在接下来的部分,我们将介绍另一种机器学习分类器:支持向量机SVM)。

支持向量机分类

我们在 第二章回归入门 中首先使用了 SVM 进行回归。在本节中,你将学习如何使用 SVM 进行分类。和往常一样,我们将使用 scikit-learn 来实践我们的示例。

什么是支持向量机分类器?

SVM 的目标是找到一个 n 维空间中的表面,将该空间中的数据点分成多个类别。

在二维空间中,这个表面通常是直线。然而,在三维空间中,SVM 通常找到一个平面。这些表面是最优的,因为它们基于机器可以利用的信息,从而优化了 n 维空间的分隔。

SVM 找到的最佳分隔面被称为最佳分隔超平面。

SVM 用于找到一个分隔两组数据点的表面。换句话说,SVM 是 二分类器。这并不意味着 SVM 只能用于二分类。尽管我们只讨论了一个平面,SVM 可以通过对任务本身进行推广,将空间划分为任意数量的类别。

分隔面是最优的,因为它最大化了每个数据点与分隔面之间的距离。

向量是定义在 n 维空间中的数学结构,具有大小(长度)和方向。在二维空间中,你可以从原点绘制向量 (x, y) 到点 (x, y)。基于几何学,你可以使用勾股定理计算向量的长度,并通过计算向量与水平轴之间的角度来确定向量的方向。

例如,在二维空间中,向量 (3, -4) 的大小如下:

np.sqrt( 3 * 3 + 4 * 4 )

预期输出如下:

5.0

它具有以下方向(以度为单位):

np.arctan(-4/3) / 2 / np.pi * 360

预期输出如下:

-53.13010235415597

理解支持向量机

假设给定了两组具有不同类别(类别 0 和类别 1)的点。为了简单起见,我们可以想象一个二维平面,具有两个特征:一个映射在水平轴上,另一个映射在垂直轴上。

SVM 的目标是找到最佳分隔线,将属于类别 0 的点ADCBH与属于类别 1 的点EFG分开:

图 3.13:分隔红色和蓝色成员的线

图 3.13:分隔红色和蓝色成员的线

但是,分隔并不总是那么明显。例如,如果类别 0 的新点位于 EFG 之间,就没有一条线能够分开所有的点而不导致错误。如果类别 0 的点围绕类别 1 的点形成一个完整的圆圈,就没有直线能够分开这两组点:

图 3.14:带有两个异常点的图

图 3.14:带有两个异常点的图

例如,在前面的图中,我们容忍了两个异常点,OP

在以下解决方案中,我们不容忍异常值,而是通过两个半线来构建最佳分隔路径,代替使用一条线:

图 3.15:移除两个异常值的分隔图

图 3.15:移除两个异常值的分隔图

完美分隔所有数据点通常并不值得投入过多资源。因此,支持向量机可以通过正则化来简化并限制最佳分隔形状的定义,从而允许异常值的存在。

支持向量机的正则化参数决定了允许的误差率或禁止误分类的程度。

支持向量机有一个核函数参数。线性核使用线性方程来严格描述最佳分隔超平面。多项式核使用多项式,而指数核使用指数表达式来描述超平面。

边距是围绕分隔超平面的一片区域,其边界由最靠近分隔超平面的点所限定。一个平衡的边距是从每个类别中选出的点,它们距离分隔线等远。

当涉及到定义最佳分隔超平面的允许误差率时,gamma 参数决定了在确定分隔超平面位置时,是仅考虑接近分隔超平面的点,还是考虑最远离分隔线的点。gamma 值越高,影响分隔超平面位置的点数越少。

scikit-learn 中的支持向量机

我们的切入点是活动 3.02scikit-learn 中的支持向量机优化。一旦我们划分了训练数据和测试数据,就可以设置分类器了:

features_train, features_test, \
label_train, label_test = \
model_selection.train_test_split(scaled_features, label,\
                                 test_size=0.2)

我们将使用 svm.SVC() 分类器,而不是使用 k 最近邻分类器:

from sklearn import svm
classifier = svm.SVC()
classifier.fit(features_train, label_train)
classifier.score(features_test, label_test)

期望的输出是这样的:

0.745

看起来 scikit-learn 的默认支持向量机分类器比 k 最近邻分类器稍微更好。

scikit-learn 支持向量机的参数

以下是 scikit-learn 支持向量机的参数:

  • kernel:这是一个字符串或可调用参数,用于指定算法中使用的核函数。预定义的核函数包括 linearpolyrbfsigmoidprecomputed。默认值是 rbf

  • degree:当使用多项式核时,你可以指定多项式的度数。默认值是 3

  • gamma:这是用于 rbfpolysigmoid 核函数的核系数。默认值是 auto,它的计算方式是 1/特征数量

  • C:这是一个浮动数值,默认值为 1.0,表示误差项的惩罚参数。

    注意

    你可以在参考文档中阅读关于这些参数的详细信息,地址为 scikit-learn.org/stable/modules/generated/sklearn.svm.SVC.html

下面是一个支持向量机的例子:

classifier = svm.SVC(kernel="poly", C=2, degree=4, gamma=0.05)

活动 3.02:scikit-learn 中的支持向量机优化

在这个活动中,你将使用、比较和对比不同 SVM 分类器的参数。通过这些,你将找到一组在我们加载并准备好的训练和测试数据中,能够产生最高分类准确率的参数,这些数据在活动 3.01中已经准备好,提高信用评分的准确性

你必须使用不同的 SVM 超参数组合:

  • kernel="linear"

  • kernel="poly", C=1, degree=4, gamma=0.05

  • kernel="poly", C=1, degree=4, gamma=0.05

  • kernel="poly", C=1, degree=4, gamma=0.25

  • kernel="poly", C=1, degree=4, gamma=0.5

  • kernel="poly", C=1, degree=4, gamma=0.16

  • kernel="sigmoid"

  • kernel="rbf", gamma=0.15

  • kernel="rbf", gamma=0.25

  • kernel="rbf", gamma=0.5

  • kernel="rbf", gamma=0.35

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

  1. 打开一个新的 Jupyter Notebook 文件,并执行前面提到的所有步骤,练习 3.04在 scikit-learn 中进行 K 近邻分类

  2. sklearn导入svm

  3. 创建一个函数,用于实例化一个 SVC 模型,设置指定的超参数,使用训练数据进行拟合,并返回训练集和测试集的准确度评分。

  4. 使用你创建的函数,评估不同超参数组合的准确度评分。

  5. 找到最佳的超参数组合。

预期输出如下:

(0.78125, 0.775)

注意

本次活动的解答可以在第 347 页找到。

总结

在本章中,我们学习了分类的基础知识,以及回归问题之间的区别。分类是关于预测一个具有有限可能值的响应变量。对于任何数据科学项目,数据科学家都需要在训练模型之前准备好数据。在本章中,我们学习了如何标准化数值并替换缺失值。接着,你了解了著名的 k 近邻算法,并发现它如何使用距离度量来寻找与数据点最接近的邻居,并从中分配最常见的类别。我们还学习了如何将 SVM 应用于分类问题,并调优其一些超参数,以提高模型性能并减少过拟合。

在下一章中,我们将带你了解另一种类型的算法,叫做决策树。

第四章:4. 决策树简介

概述

本章将详细介绍两种类型的监督学习算法。第一个算法将帮助你使用决策树对数据点进行分类,而另一个算法则帮助你使用随机森林对数据点进行分类。此外,你还将学习如何手动和自动计算模型的精确度、召回率和 F1 分数。到本章结束时,你将能够分析用于评估数据模型效用的指标,并基于决策树和随机森林算法对数据点进行分类。

介绍

在前两章中,我们学习了回归问题与分类问题的区别,并且我们看到了如何训练一些最著名的算法。本章中,我们将介绍另一种算法类型:基于树的模型。

基于树的模型非常流行,因为它们可以建模复杂的非线性模式,并且相对容易理解。本章中,我们将介绍决策树和随机森林算法,这些是行业中最广泛使用的基于树的模型之一。

决策树

决策树有叶子节点、分支和节点。节点是做出决策的地方。决策树由我们用来制定决策(或预测)数据点的规则组成。

决策树的每个节点代表一个特征,而从内部节点出来的每条边代表树的一个可能值或值区间。树的每个叶子节点代表树的一个标签值。

这可能听起来有些复杂,但让我们来看一个应用实例。

假设我们有一个数据集,具有以下特征,并且响应变量是判断一个人是否有信用:

图 4.1:用于制定规则的示例数据集

图 4.1:用于制定规则的示例数据集

记住,决策树只是一些规则的集合。查看图 4.1中的数据集,我们可以得出以下规则:

  • 所有有房贷的人都被确定为有信用的人。

  • 如果债务人有工作并且在学习,那么贷款是有信用的。

  • 年收入超过 75,000 的人是有信用的。

  • 年收入在 75,000 以下的、有车贷并且有工作的人员是有信用的。

按照我们刚才定义的规则顺序,我们可以建立一棵树,如图 4.2所示,并描述一种可能的信用评分方法:

图 4.2:贷款类型的决策树

图 4.2:贷款类型的决策树

首先,我们确定贷款类型。根据第一个规则,房屋贷款自动为信用良好。学习贷款由第二个规则描述,结果是一个包含另一个关于就业的决策的子树。由于我们已经涵盖了房屋贷款和学习贷款,因此只剩下汽车贷款。第三个规则描述了收入决策,而第四个规则描述了关于就业的决策。

每当我们必须对一个新的债务人进行评分,以确定其是否具有信用时,我们必须从决策树的顶部到底部进行遍历,并观察底部的真假值。

显然,基于七个数据点的模型准确性非常低,因为我们无法推广那些与现实完全不符的规则。因此,规则通常是基于大量数据来确定的。

这并不是构建决策树的唯一方法。我们还可以基于其他规则的顺序来构建决策树。让我们从图 4.1的数据集中提取一些其他规则。

观察 1:注意到所有收入超过 75,000 的个人都是信用良好的。

收入 > 75,000 => 信用良好 这个规则成立。

规则 1 将七个数据点中的四个(ID C、E、F、G)分类;对于剩下的三个数据点,我们需要更多的规则。

观察 2:在剩下的三个数据点中,有两个没有就业。其中一个是就业的(ID D),并且是信用良好的。由此,我们可以得出以下规则:

收入 <= 75,000 时,以下情况成立:在职 == true => 信用良好

注意,使用这个第二个规则,我们也可以将剩下的两个数据点(ID A 和 B)分类为不信用良好。通过这两个规则,我们准确地分类了该数据集中的所有观察点:

图 4.3:收入的决策树

图 4.3:收入的决策树

第二个决策树比较简单。同时,我们不能忽视这样一个事实:模型表示,低收入的在职人员更可能无法偿还贷款。不幸的是,训练数据不足(这个例子中只有七个观察点),这使得我们很可能得出错误的结论。

过拟合是决策树中常见的问题,当我们仅基于少量数据点做出决策时,这个决策通常不能代表整体情况。

由于我们可以按照任何可能的顺序构建决策树,因此定义一种高效的决策树构建方式是有意义的。因此,我们将探讨一种用于在决策过程中排序特征的度量方法。

在信息论中,熵衡量一个属性的可能值分布的随机程度。随机程度越高,属性的熵值越高。

熵是事件的最高可能性。如果我们事先知道事件的结果,那么这个事件就没有随机性。因此,熵值为

我们使用熵来排序决策树中节点的分裂。以之前的例子为例,我们应该从哪个规则开始?是Income <= 75000还是is employed?我们需要使用一个度量标准来告诉我们哪个特定的分裂比另一个更好。一个好的分裂可以通过它清楚地将数据分成两个同质的组来定义。一个这样的度量是信息增益,它基于熵。

这里是计算熵的公式:

图 4.4:熵公式

图 4.4:熵公式

pi 表示目标变量某一可能值发生的概率。所以,如果这一列有n个不同的唯一值,那么我们将为每个值计算概率([p1, p2, ..., pn])并应用公式。

要在 Python 中手动计算分布的熵,我们可以使用 NumPy 库中的np.log2np.dot()方法。在numpy中没有自动计算熵的函数。

看看下面的例子:

import numpy as np
probabilities = list(range(1,4)) 
minus_probabilities = [-x for x in probabilities]
log_probabilities = [x for x in map(np.log2, probabilities)]
entropy_value = np.dot(minus_probabilities, log_probabilities)

概率以 NumPy 数组或常规列表的形式给出,在第 2 行pi。

我们需要创建一个包含第 3 行分布中取反值的向量:- pi。

第 4 行,我们必须对分布列表中的每个值取以 2 为底的对数:logi pi。

最后,我们通过标量积来计算总和,也称为两个向量的点积:

图 4.5:两个向量的点积

图 4.5:两个向量的点积

注意

你第一次在第二章《回归分析导论》中学习了点积。两个向量的点积是通过将第一个向量的第i个坐标与第二个向量的第i个坐标相乘来计算的,针对每个i。一旦我们得到所有的乘积,就将它们求和:

np.dot([1, 2, 3], [4, 5, 6])

这将得到 14 + 25 + 3*6 = 32。

在下一个练习中,我们将计算一个小样本数据集的熵。

练习 4.01:计算熵

在这个练习中,我们将计算数据集中特征的熵,如图 4.6所示:

图 4.6:用于制定规则的样本数据集

图 4.6:用于制定规则的样本数据集

注意

数据集文件也可以在我们的 GitHub 仓库中找到:

packt.live/2AQ6Uo9

我们将为EmployedIncomeLoanTypeLoanAmount特征计算熵。

以下步骤将帮助你完成这个练习:

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 导入numpy包并命名为np

    import numpy as np
    
  3. 定义一个名为entropy()的函数,它接收一个概率数组并返回计算得到的熵值,如下代码片段所示:

    def entropy(probabilities):
        minus_probabilities = [-x for x in probabilities]
        log_probabilities = [x for x in map(np.log2, \
                                            probabilities)]
        return np.dot(minus_probabilities, log_probabilities)
    

    接下来,我们将计算 Employed 列的熵。该列仅包含两个可能的值:truefalse。在七行数据中,true 出现了四次,因此其概率为 4/7。类似地,false 的概率为 3/7,因为它在数据集中出现了三次。

  4. 使用 entropy() 函数计算 Employed 列的熵,概率分别为 4/73/7

    H_employed = entropy([4/7, 3/7])
    H_employed
    

    你应该得到以下输出:

    0.9852281360342515
    

    这个值接近零,意味着这些组是相当同质的。

  5. 现在,使用 entropy() 函数计算 Income 列的熵及其对应的概率列表:

    H_income = entropy([1/7, 2/7, 1/7, 2/7, 1/7])
    H_income
    

    你应该得到以下输出:

    2.2359263506290326
    

    Employed 列相比,Income 的熵较高。这意味着该列的概率分布更为分散。

  6. 使用 entropy 函数计算 LoanType 列的熵及其对应的概率列表:

    H_loanType = entropy([3/7, 2/7, 2/7])
    H_loanType
    

    你应该得到以下输出:

    1.5566567074628228
    

    这个值大于 0,因此该列的概率分布较为分散。

  7. 让我们使用 entropy 函数计算 LoanAmount 列的熵及其对应的概率列表:

    H_LoanAmount = entropy([1/7, 1/7, 3/7, 1/7, 1/7])
    H_LoanAmount
    

    你应该得到以下输出:

    2.128085278891394
    

    LoanAmount 的熵较高,因此其值相当随机。

    注意

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

    你也可以在线运行这个示例,网址为 packt.live/2By7aI6。你必须执行整个 Notebook 才能得到预期的结果。

在这里,你可以看到 Employed 列的熵在四个不同的列中是最低的,因为它的值变化最小。

完成这个练习后,你已经学会了如何手动计算数据集每一列的熵。

信息增益

当我们根据某个属性的值对数据集中的数据点进行划分时,我们减少了系统的熵。

为了描述信息增益,我们可以计算标签的分布。在 图 4.1 中,我们的数据集包含五个可信和两个不可信的个体。初始分布的熵如下:

H_label = entropy([5/7, 2/7])
H_label

输出结果如下:

0.863120568566631

让我们看看如果根据贷款金额是否大于 15,000 来划分数据集,会发生什么:

  • 在组 1 中,我们得到一个属于 15,000 贷款金额的数据点。这个数据点是不可信的。

  • 在组 2 中,我们有五个可信的个体和一个不可信的个体。

每个组中标签的熵如下:

对于组 1,我们有如下情况:

H_group1 = entropy([1]) 
H_group1

输出结果如下:

-0.0

对于组 2,我们有如下情况:

H_group2 = entropy([5/6, 1/6]) 
H_group2

输出结果如下:

0.6500224216483541

为了计算信息增益,让我们计算组熵的加权平均值:

H_group1 * 1/7 + H_group2 * 6/7

输出结果如下:

0.5571620756985892

现在,为了找到信息增益,我们需要计算原始熵(H_label)与我们刚刚计算出的熵之间的差异:

Information_gain = 0.863120568566631 - 0.5572
Information_gain

输出如下:

0.30592056856663097

通过使用这个规则拆分数据,我们获得了一些信息。

在创建决策树时,在每个节点,我们的任务是使用能够最大化信息增益的规则来划分数据集。

我们也可以使用基尼不纯度代替基于熵的信息增益来构建最佳的决策树拆分规则。

基尼不纯度

除了熵之外,还有另一种广泛使用的度量标准可以用来衡量分布的随机性:基尼不纯度。

基尼不纯度的定义如下:

图 4.7:基尼不纯度

图 4.7:基尼不纯度

pi 代表目标变量可能值之一发生的概率。

熵的计算可能稍微慢一些,因为涉及到对数运算。另一方面,基尼不纯度在衡量随机性时的精确度较低。

注意

一些程序员更倾向于使用基尼不纯度,因为不需要进行对数计算。从计算的角度来看,这两种方法都不算特别复杂,因此可以使用其中任意一种。在性能方面,以下研究得出的结论是,这两种度量标准之间的差异通常非常小:www.unine.ch/files/live/sites/imi/files/shared/documents/papers/Gini_index_fulltext.pdf

通过这一点,我们已经了解到,可以通过基于信息增益或基尼不纯度来优化决策树。不幸的是,这些度量标准仅适用于离散值。如果标签是在一个连续区间内定义的,比如价格范围或薪资范围,该怎么办呢?

我们必须使用其他度量标准。理论上,你可以理解基于连续标签创建决策树的思路,那就是回归。我们在本章中可以重用的一个度量标准是均方误差。与基尼不纯度或信息增益不同,我们必须最小化均方误差以优化决策树。由于这是一个初学者课程,我们将省略这一度量标准。

在下一部分,我们将讨论决策树的退出条件。

退出条件

我们可以根据越来越具体的规则持续地拆分数据点,直到决策树的每个叶子节点的熵为零。问题是,这种最终状态是否是理想的。

通常,这不是我们所期望的,因为我们可能会面临过拟合模型的风险。当模型的规则过于具体且过于挑剔,而做出决策的样本量又过小,我们就有可能得出错误的结论,从而在数据集中识别出一个实际上并不存在于现实中的模式。

例如,如果我们转动轮盘三次,得到的结果分别是 12、25 和 12,这就得出结论:每次奇数次转动结果为 12 的策略并不明智。假设每次奇数次转动结果都是 12,我们就发现了一个完全由随机噪音引起的规则。

因此,对我们仍然可以分割的数据集的最小大小施加限制是一个在实际中效果良好的退出条件。例如,如果在数据集小于 50、100、200 或 500 时就停止分割,就可以避免对随机噪音得出结论,从而最大限度地减少过拟合模型的风险。

另一种常见的退出条件是树的最大深度限制。一旦达到固定的树深度,我们就在叶子节点对数据点进行分类。

使用 scikit-learn 构建决策树分类器

我们已经学习了如何从.csv文件加载数据,如何对数据进行预处理,以及如何将数据拆分为训练集和测试集。如果你需要复习这些知识,可以回到前面的章节,在回归和分类的背景下重新学习这一过程。

现在,我们假设已经通过scikit-learn train-test-split调用返回了一组训练特征、训练标签、测试特征和测试标签:

from sklearn import model_selection
features_train, features_test, \
label_train, label_test = \
model_selection.train_test_split(features, label, test_size=0.1, \
                                 random_state=8)

在前面的代码片段中,我们使用了train_test_split将数据集(特征和标签)拆分为训练集和测试集。测试集占观测数据的 10%(test_size=0.1)。random_state参数用于获取可重复的结果。

我们不会专注于如何获得这些数据点,因为这个过程与回归和分类的情况完全相同。

现在是时候导入并使用 scikit-learn 的决策树分类器了:

from sklearn.tree import DecisionTreeClassifier
decision_tree = DecisionTreeClassifier(max_depth=6)
decision_tree.fit(features_train, label_train)

我们在DecisionTreeClassifier中设置了一个可选参数,即max_depth,用于限制决策树的深度。

注意

你可以阅读官方文档,获取参数的完整列表:scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html

以下是一些更重要的参数:

  • criterion: Gini 代表基尼不纯度,entropy 代表信息增益。这将定义在每个节点上用于评估分割质量的度量标准。

  • max_depth: 这是定义树的最大深度的参数。

  • min_samples_split: 这是分割内部节点所需的最小样本数。

你也可以尝试文档中列出的所有其他参数。我们将在本节中省略它们。

一旦模型构建完成,我们就可以使用决策树分类器进行数据预测:

decision_tree.predict(features_test)

你将在本节末尾的活动中构建一个决策树分类器。

分类器的性能评估指标

在拆分训练数据和测试数据之后,决策树模型有一个 score 方法,用来评估模型对测试数据分类的效果(也称为准确率)。我们在前两章中学习了如何使用 score 方法:

decision_tree.score(features_test, label_test)

score 方法的返回值是一个小于或等于 1 的数字。我们越接近 1,模型就越好。

现在,我们将学习另一种评估模型的方法。

注意

你也可以将此方法应用于你在上一章构建的模型。

假设我们有一个测试特征和一个测试标签:

predicted_label = decision_tree.predict(features_test)

让我们使用之前的信用评分示例,假设我们训练了一个决策树,现在有了它的预测结果:

图 4.8:用于制定规则的示例数据集

图 4.8:用于制定规则的示例数据集

我们的模型一般来说做出了很好的预测,但也有少数错误。它错误地预测了 ID ADE 的结果。它的准确率得分将是 4 / 7 = 0.57。

我们将使用以下定义来定义一些度量标准,帮助你评估分类器的好坏:

  • Creditworthy 列(在我们的示例中)和相应的预测值都是 Yes。在我们的示例中,ID CFG 将属于这一类别。

  • No。只有 ID B 会被分类为真正负类。

  • Yes,但真实标签实际上是 No。这种情况适用于 ID A

  • No,但真实标签实际上是 Yes,例如 ID DE

使用前面四个定义,我们可以定义四个度量标准,用来描述我们的模型如何预测目标变量。#( X ) 符号表示 X 中的值的数量。从技术术语上讲,#( X ) 表示 X 的基数:

定义(准确率)#(真正例)+ #(真正负类) / #(数据集)

准确率是一个用来衡量分类器给出正确答案次数的指标。这是我们用来评估分类器得分的第一个度量标准。

在我们之前的示例中(图 4.8),准确率得分将是 TP + TN / 总数 = (3 + 1) / 7 = 4/7。

我们可以使用 scikit-learn 提供的函数来计算模型的准确率:

from sklearn.metrics import accuracy_score
accuracy_score(label_test, predicted_label)

定义(精度)#真正例 / (#真正例 + #假正例)

精度关注的是分类器认为是正类的值。这些结果中,有些是真正例,而有些是假正例。高精度意味着假正例的数量相对于真正例非常低。这意味着一个精确的分类器在找到正类时很少犯错。

定义(召回率)#真正例 / (#真正例 + #假负例)

召回率关注的是测试数据中正类值的情况。分类器找到的这些结果是正例(True Positive)。那些分类器未找到的正类值是假阴性(False Negative)。一个召回率高的分类器能够找到大多数正类值。

使用我们之前的示例(图 4.8),我们将得到以下度量:

  • 精确度 = TP / (TP + FP) = 4 / (4 + 1) = 4/6 = 0.8

  • 召回率 = TP / (TP + FN) = 4 / (4 + 2) = 4/6 = 0.6667

通过这两个度量,我们可以轻松看到我们的模型在哪些地方表现更好或更差。在这个例子中,我们知道它倾向于误分类假阴性案例。这些度量比准确率分数更为细致,准确率分数仅提供一个整体分数。

F1 分数是一个结合精确度和召回率的度量。它的值范围在 0 到 1 之间。如果 F1 分数为 1,则表示模型完美地预测了正确的结果。另一方面,F1 分数为 0 则表示模型无法准确预测目标变量。F1 分数的优点是它考虑了假阳性和假阴性。

计算 F1 分数的公式如下:

图 4.9:计算 F1 分数的公式

图 4.9:计算 F1 分数的公式

最后需要指出的是,scikit-learn 包还提供了一个非常实用的函数,可以一次性显示所有这些度量:classification_report()。分类报告有助于检查我们预测的质量:

from sklearn.metrics import classification_report
print(classification_report(label_test, predicted_label))

在下一个练习中,我们将练习如何手动计算这些分数。

练习 4.02:精确度、召回率和 F1 分数计算

在本练习中,我们将计算两个不同分类器在模拟数据集上的精确度、召回率和 F1 分数。

以下步骤将帮助你完成此练习:

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 使用以下代码导入numpy包,并将其命名为np

    import numpy as np
    
  3. 创建一个名为real_labelsnumpy数组,包含值[True, True, False, True, True]。该列表表示我们模拟数据集的目标变量的真实值。打印其内容:

    real_labels = np.array([True, True, False, True, True])
    real_labels
    

    预期输出如下:

    array([ True, True, False, True, True])
    
  4. 创建一个名为model_1_predsnumpy数组,包含值[True, False, False, False, False]。该列表表示第一个分类器的预测值。打印其内容:

    model_1_preds = np.array([True, False, False, False, False])
    model_1_preds
    

    预期输出如下:

    array([ True, False, False, False, False])
    
  5. 创建另一个名为model_2_predsnumpy数组,包含值[True, True, True, True, True]。该列表表示第一个分类器的预测值。打印其内容:

    model_2_preds = np.array([True, True, True, True, True])
    model_2_preds
    

    预期输出如下:

    array([ True,  True,  True,  True,  True])
    
  6. 创建一个名为model_1_tp_cond的变量,用来找到第一个模型的真正例:

    model_1_tp_cond = (real_labels == True) \
                       & (model_1_preds == True)
    model_1_tp_cond
    

    预期输出如下:

    array([ True, False, False, False, False])
    
  7. 创建一个名为model_1_tp的变量,通过求和model_1_tp_cond来获取第一个模型的真正例数量:

    model_1_tp = model_1_tp_cond.sum()
    model_1_tp
    

    预期输出如下:

    1
    

    第一个模型只有1个真实阳性案例。

  8. 创建一个名为model_1_fp的变量,用于获取第一个模型的假阳性数量:

    model_1_fp = ((real_labels == False) \
                   & (model_1_preds == True)).sum()
    model_1_fp
    

    预期输出将如下所示:

    0
    

    第一个模型没有假阳性。

  9. 创建一个名为model_1_fn的变量,用于获取第一个模型的假阴性数量:

    model_1_fn = ((real_labels == True) \
                   & (model_1_preds == False)).sum()
    model_1_fn
    

    预期输出将如下所示:

    3
    

    第一个分类器有3个假阴性案例。

  10. 创建一个名为model_1_precision的变量,用于计算第一个模型的精度:

    model_1_precision = model_1_tp / (model_1_tp + model_1_fp)
    model_1_precision
    

    预期输出将如下所示:

    1.0
    

    第一个分类器的精度得分为1,因此它没有预测出任何假阳性。

  11. 创建一个名为model_1_recall的变量,用于计算第一个模型的召回率:

    model_1_recall = model_1_tp / (model_1_tp + model_1_fn)
    model_1_recall
    

    预期输出将如下所示:

    0.25
    

    第一个模型的召回率仅为0.25,因此它预测了相当多的假阴性。

  12. 创建一个名为model_1_f1的变量,用于计算第一个模型的 F1 分数:

    model_1_f1 = 2*model_1_precision * model_1_recall\
                 / (model_1_precision + model_1_recall)
    model_1_f1
    

    预期输出将如下所示:

    0.4
    

    如预期所示,第一个模型的 F1 分数相当低。

  13. 创建一个名为model_2_tp的变量,用于获取第二个模型的真实阳性数量:

    model_2_tp = ((real_labels == True) \
                   & (model_2_preds == True)).sum()
    model_2_tp
    

    预期输出将如下所示:

    4
    

    第二个模型有4个真实阳性案例。

  14. 创建一个名为model_2_fp的变量,用于获取第二个模型的假阳性数量:

    model_2_fp = ((real_labels == False) \
                   & (model_2_preds == True)).sum()
    model_2_fp
    

    预期输出将如下所示:

    1
    

    第二个模型只有一个假阳性。

  15. 创建一个名为model_2_fn的变量,用于获取第二个模型的假阴性数量:

    model_2_fn = ((real_labels == True) \
                   & (model_2_preds == False)).sum()
    model_2_fn
    

    预期输出将如下所示:

    0
    

    第二个分类器没有假阴性。

  16. 创建一个名为model_2_precision的变量,用于计算第二个模型的精度:

    model_2_precision = model_2_tp / (model_2_tp + model_2_fp) 
    model_2_precision
    

    预期输出将如下所示:

    0.8
    

    第二个模型的精度得分相当高:0.8。它在假阳性方面并没有犯太多错误。

  17. 创建一个名为model_2_recall的变量,用于计算第二个模型的召回率:

    model_2_recall = model_2_tp / (model_2_tp + model_2_fn)
    model_2_recall
    

    预期输出将如下所示:

    1.0
    

    在召回率方面,第二个分类器表现出色,没有将任何观测值错误分类为假阴性。

  18. 创建一个名为model_2_f1的变量,用于计算第二个模型的 F1 分数:

    model_2_f1 = 2*model_2_precision*model_2_recall \
                 / (model_2_precision + model_2_recall)
    model_2_f1
    

    预期输出将如下所示:

    0.888888888888889
    

    第二个模型的 F1 分数相当高。

    注意

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

    你也可以在packt.live/2NoxLdo在线运行此示例。你必须执行整个笔记本以获得预期的结果。

在本次练习中,我们展示了如何手动计算两个不同模型的精度、召回率和 F1 分数。第一个分类器具有优秀的精度,但召回率较差,而第二个分类器具有优秀的召回率和相当不错的精度。

使用 scikit-learn 评估分类器的性能

scikit-learn 包提供了一些函数,用于自动计算精度、召回率和 F1 分数。你需要先导入这些函数:

from sklearn.metrics import recall_score, \
precision_score, f1_score

要获得精度分数,你需要从模型中获取预测结果,如下所示的代码片段:

label_predicted = decision_tree.predict(data)
precision_score(label_test, predicted_label, \
                average='weighted')

计算 recall_score 可以这样进行:

recall_score(label_test, label_predicted, average='weighted')

计算 f1_score 可以这样进行:

f1_score(label_test, predicted_label, average='weighted')

在下一部分,我们将学习如何使用另一个工具——混淆矩阵,来分析分类器的性能。

混淆矩阵

之前,我们学习了如何使用一些计算指标来评估分类器的性能。还有另一个非常有趣的工具可以帮助你评估多类分类模型的性能:混淆矩阵。

混淆矩阵是一个方阵,其中行数和列数等于不同标签值(或类别)的数量。在矩阵的列中,我们放置每个测试标签值。在矩阵的行中,我们放置每个预测标签值。

混淆矩阵如下所示:

图 4.10:示例混淆矩阵

图 4.10:示例混淆矩阵

在前面的示例中,混淆矩阵的第一行展示了模型的表现:

  • 正确预测 A 类 88

  • 在真实值为 B 时预测 A 类 3

  • 在真实值为 C 时预测 A 类 2

我们还可以看到,当模型预测 C 类时,真实值为 A(16 次)的错误情况。混淆矩阵是一个强大的工具,可以快速轻松地发现模型在某些类别上表现良好或不良。

scikit-learn 包提供了一个函数来计算并显示混淆矩阵:

from sklearn.metrics import confusion_matrix
confusion_matrix(label_test, predicted_label)

在下一个活动中,你将构建一个决策树模型,用来将汽车分类为不可接受、可接受、良好和非常好四个类别,以便客户使用。

活动 4.01:汽车数据分类

在本活动中,你将构建一个可靠的决策树模型,帮助公司找到客户可能购买的汽车。我们假设租车公司正在专注于与客户建立长期关系。你的任务是构建一个决策树模型,将汽车分类为不可接受、可接受、良好和非常好四个类别。

注意

数据集文件也可以在我们的 GitHub 仓库中找到:packt.live/2V95I6h

本活动的数据集可以在这里访问:archive.ics.uci.edu/ml/datasets/Car+Evaluation

引用 – Dua, D., & Graff, C.. (2017). UCI Machine Learning Repository

它由六个不同的特征组成:buyingmaintenancedoorspersonsluggage_bootsafety。目标变量对给定汽车的接受度进行排名。它可以取四个不同的值:unaccaccgoodvgood

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

  1. 将数据集加载到 Python 中并导入必要的库。

  2. 使用 scikit-learn 中的LabelEncoder()进行标签编码。

  3. 使用pop()从 pandas 中提取label变量。

  4. 现在,使用 scikit-learn 中的train_test_split()将训练数据和测试数据分开。我们将使用 10%的数据作为测试数据。

  5. 使用DecisionTreeClassifier()及其方法fit()predict()构建决策树分类器。

  6. 使用score()检查基于测试数据的模型评分。

  7. 使用 scikit-learn 中的classification_report()对模型进行更深入的评估。

预期输出:

图 4.11:显示预期分类报告的输出

图 4.11:显示预期分类报告的输出

注意

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

在接下来的部分,我们将研究随机森林分类器。

随机森林分类器

如果你考虑一下随机森林分类器这个名称,可以这样解释:

  • 一片森林由多棵树组成。

  • 这些树可以用于分类。

  • 由于到目前为止我们唯一使用的分类树是决策树,因此可以理解随机森林是一片决策树的森林。

  • 树的随机性意味着我们的决策树是以随机化的方式构建的。

因此,我们将根据信息增益或基尼不纯度来构建决策树。

一旦你理解了这些基本概念,你就基本上知道随机森林分类器的内容了。森林中的树木越多,预测的准确性就越高。在执行预测时,每棵树都执行分类。我们收集结果,获得最多投票的类别获胜。

随机森林可以用于回归和分类。在使用随机森林进行回归时,我们不再统计某个类别的最多投票数,而是取预测结果的算术平均值(平均数)并返回。尽管随机森林在分类中的表现非常理想,但在回归中的效果不如分类,因为用于预测值的模型通常是失控的,且经常返回一个范围较广的值。这些值的平均值往往意义不大。管理回归中的噪声比分类更为困难。

随机森林通常比简单的决策树表现更好,因为它们提供了冗余性。它们能更好地处理异常值,并且具有较低的过拟合模型的概率。决策树在使用训练数据时表现得很好,但一旦用于预测新数据,随机森林则失去其优势。随机森林广泛应用于分类问题,无论是银行或电子商务的客户细分、图像分类,还是医学领域。如果你拥有一台带有 Kinect 的 Xbox,你的 Kinect 设备就包含了一个随机森林分类器,用于检测你的身体。

随机森林是一种集成算法。集成学习的思想是,我们通过多个可能有不同弱点的代理的决策来获得一个综合视角。由于集体投票,这些弱点会被抵消,大多数投票结果很可能代表正确的结果。

使用 scikit-learn 进行随机森林分类

正如你可能已经猜到的,scikit-learn 包提供了RandomForest分类器的实现,使用的是RandomForestClassifier类。这个类提供了与迄今为止所有 scikit-learn 模型完全相同的方法——你需要实例化一个模型,然后使用.fit()方法对其进行训练,最后使用.predict()方法进行预测:

from sklearn.ensemble import RandomForestClassifier
random_forest_classifier = RandomForestClassifier()
random_forest_classifier.fit(features_train, label_train)
labels_predicted = random_forest_classifier.predict\
                   (features_test)

在下一节中,我们将查看随机森林分类器的参数化。

随机森林分类器的参数化

我们将基于已有的知识,考虑可能的参数子集,这些知识来源于构建随机森林的描述:

  • n_estimators:随机森林中的树木数量。默认值为 10。

  • criterion:使用 Gini 或熵来确定在每棵树中是使用 Gini 不纯度还是信息增益。它将用于找到每个节点的最佳分裂。

  • max_features:每棵树考虑的最大特征数。可能的值包括整数。你还可以添加一些字符串,如sqrt,表示特征数量的平方根。

  • max_depth:每棵树的最大深度。

  • min_samples_split:在给定节点中,数据集中的最小样本数,以执行分裂。这也可以减少树的大小。

  • bootstrap:一个布尔值,指示在构建树时是否对数据点使用自助法。

特征重要性

随机森林分类器为你提供了每个特征在数据分类过程中有多重要的信息。记住,我们使用了许多随机构建的决策树来对数据点进行分类。我们可以衡量这些数据点的表现准确性,同时也能看到在决策过程中哪些特征是至关重要的。

我们可以通过以下查询来检索特征重要性得分的数组:

random_forest_classifier.feature_importances_

在这个包含六个特征的分类器中,第四个和第六个特征显然比其他特征更重要。第三个特征的重要性得分非常低。

特征重要性分数在我们有很多特征时特别有用,尤其是当我们想要减少特征数量以避免分类器在细节中迷失时。当我们有大量特征时,我们容易导致模型过拟合。因此,通过剔除最不重要的特征来减少特征数量通常是有帮助的。

交叉验证

之前,我们学习了如何使用不同的度量标准来评估分类器的性能,例如在训练集和测试集上的准确率、精确率、召回率或 F1 分数。目标是在两个数据集上都获得较高的分数,并且这两个分数要非常接近。如果是这样,你的模型就能表现良好,并且不容易过拟合。

测试集用于作为代理,评估你的模型是否能够很好地泛化到未见过的数据,或者是否仅仅学习了对训练集有意义的模式。

但是,在需要调整多个超参数的情况下(例如对于RandomForest),你需要训练大量不同的模型并在测试集上测试它们。这种做法实际上削弱了测试集的作用。把测试集当作最终考试,决定你是否能通过某一科目。你不会被允许反复通过或重考。

避免过度使用测试集的一个解决方案是创建验证集。你将在训练集上训练模型,并使用验证集根据不同的超参数组合来评估模型的得分。一旦找到最佳模型,你将使用测试集确保它不会过度拟合。这通常是任何数据科学项目推荐的做法。

这种方法的缺点是,你减少了用于训练集的观测数据。如果你的数据集包含数百万行,这不是问题。但对于一个小型数据集,这可能会造成问题。这就是交叉验证派上用场的地方。

以下图 4.12显示了这种技术,在这种技术中,你会创建多个训练数据的分割。对于每个分割,训练数据被分为若干个折叠(本例中为五个),其中一个折叠将作为验证集,其余折叠用于训练。

例如,在上面的分割中,第五折将用于验证,其余四个折叠(1 到 4)将用于训练模型。你将对每个分割执行相同的过程。经过每个分割后,你将使用整个训练数据,最终的性能得分将是每个分割上训练的所有模型的平均值:

图 4.12:交叉验证示例

图 4.12:交叉验证示例

使用 scikit-learn,你可以轻松地执行交叉验证,以下是一个代码示例:

from sklearn.ensemble import RandomForestClassifier
random_forest_classifier = RandomForestClassifier()
from sklearn.model_selection import cross_val_score
cross_val_score(random_forest_classifier, features_train, \
                label_train, cv=5, scoring='accuracy')

cross_val_score函数接受两个参数:

在下一部分,我们将探讨RandomForest的一个特定变体,称为extratrees

极端随机森林

极端随机树通过在已经随机化的随机森林因素基础上随机化分裂规则,从而增加了随机森林内部的随机化程度。

参数化方式类似于随机森林分类器。你可以在这里查看完整的参数列表:scikit-learn.org/stable/modules/generated/sklearn.ensemble.ExtraTreesClassifier.html

Python 实现如下:

from sklearn.ensemble import ExtraTreesClassifier
extra_trees_classifier = \
ExtraTreesClassifier(n_estimators=100, \
                     max_depth=6)
extra_trees_classifier.fit(features_train, label_train)
labels_predicted = extra_trees_classifier.predict(features_test)

在接下来的活动中,我们将优化在活动 4.01中构建的分类器,汽车数据分类

活动 4.02:为你的汽车租赁公司进行随机森林分类

在本活动中,你将优化分类器,以便在为你的车队选择未来的汽车时,更好地满足客户需求。我们将对你在本章前一个活动中使用的汽车经销商数据集进行随机森林和极端随机森林分类。

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

  1. 按照之前活动 4.01步骤 1 - 4汽车数据分类

  2. 使用RandomForestClassifier创建一个随机森林。

  3. 使用.fit()训练模型。

  4. 导入confusion_matrix函数来评估RandomForest的质量。

  5. 使用classification_report()打印分类报告。

  6. 使用.feature_importance_打印特征重要性。

  7. 使用extratrees模型重复步骤 2 到 6

预期输出:

array([0.08844544, 0.0702334 , 0.01440408, 0.37662014,
       0.05965896, 0.39063797])

注意

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

完成本活动后,你已经学会了如何拟合RandomForestextratrees模型,并分析它们的分类报告和特征重要性。现在,你可以尝试不同的超参数,看看能否提高结果。

总结

在本章中,我们学习了如何使用决策树进行预测。通过集成学习技术,我们创建了复杂的强化学习模型来预测任意数据点的类别。

决策树在表面上表现得非常准确,但它们容易导致模型过拟合。随机森林和极端随机树通过引入一些随机元素和投票算法来减少过拟合,其中多数投票获胜。

除了决策树、随机森林和极端随机树之外,我们还了解了评估模型效用的新方法。在使用了众所周知的准确率评分后,我们开始使用精确率、召回率和 F1 评分指标来评估我们的分类器的表现。所有这些数值都是从混淆矩阵中得出的。

在接下来的章节中,我们将描述聚类问题,并比较和对比两种聚类算法。

第五章:5. 人工智能:聚类

概述

本章将介绍聚类的基础知识,这是一种无监督学习方法,与前几章中看到的监督学习方法相对。你将实现不同类型的聚类,包括使用 k-means 算法的平面聚类,以及使用均值漂移算法和聚合层次模型的层次聚类。你还将学习如何通过内在和外在方法来评估你的聚类模型的表现。到本章结束时,你将能够使用聚类分析数据,并将这一技能应用于解决各种领域的挑战。

引言

在上一章中,你已经了解了决策树及其在分类中的应用。你还在第二章《回归介绍》中了解了回归。回归和分类都是监督学习方法的一部分。然而,在本章中,我们将探讨无监督学习方法;我们将处理没有标签(输出)的数据集。机器需要根据我们定义的一组参数来告诉我们标签是什么。在本章中,我们将通过使用聚类算法来执行无监督学习。

我们将使用聚类分析数据,以发现特定的模式并创建群体。除此之外,聚类还可以用于许多其他目的:

  • 市场细分帮助识别市场中你应该关注的最佳股票。

  • 客户细分通过使用顾客的消费模式来识别顾客群体,以更好地推荐产品。

  • 在计算机视觉中,图像分割是通过聚类来执行的。通过这种方式,我们可以在图像中找到不同的物体。

  • 聚类也可以与分类相结合,生成多种特征(输入)的紧凑表示,然后可以将其输入分类器。

  • 聚类还可以通过检测异常值来筛选数据点。

无论我们是在对基因学、视频、图像还是社交网络应用聚类,如果我们使用聚类分析数据,我们可能会发现数据点之间的相似性,值得将其统一处理。

例如,考虑一个店铺经理,负责确保店铺的盈利。店铺中的产品被划分为不同的类别,而有不同的顾客偏好不同的商品。每个顾客有自己的偏好,但他们之间也有一些相似之处。你可能会有一个顾客对生物产品感兴趣,倾向于选择有机产品,这也正是素食顾客感兴趣的商品。即使他们有所不同,但在偏好或模式上有相似之处,因为他们都倾向于购买有机蔬菜。这可以看作是一个聚类的例子。

第三章分类简介中,你学习了分类,它是监督学习方法的一部分。在分类问题中,我们使用标签来训练模型,以便能够对数据点进行分类。而在聚类中,由于我们没有特征标签,我们需要让模型自己找出这些特征属于哪个簇。这通常基于每个数据点之间的距离。

在本章中,你将学习 k-means 算法,它是最广泛使用的聚类算法,但在此之前,我们需要先定义聚类问题。

定义聚类问题

我们将定义聚类问题,以便能够发现数据点之间的相似性。例如,假设我们有一个由多个数据点组成的数据集。聚类帮助我们通过描述这些数据点的分布情况来理解数据的结构。

让我们看一下图 5.1中二维空间中数据点的示例:

图 5.1:二维空间中的数据点

图 5.1:二维空间中的数据点

现在,看看图 5.2。很明显,存在三个簇:

图 5.2:使用二维空间中的数据点形成的三个簇

图 5.2:使用二维空间中的数据点形成的三个簇

这三个簇很容易识别,因为这些点彼此接近。这里你可以看到,聚类算法能够识别彼此接近的数据点。你可能还注意到,数据点M1、O1 和 N1 并不属于任何一个簇;这些是离群点。你构建的聚类算法应该能够妥善处理这些离群点,而不是将它们归入某个簇。

虽然在二维空间中很容易识别簇,但我们通常处理的是多维数据点,即具有多个特征的数据。因此,了解哪些数据点彼此接近非常重要。此外,定义用于检测数据点之间接近程度的距离度量也至关重要。一个著名的距离度量是欧几里得距离,我们在第一章人工智能简介中学习过。在数学中,我们通常使用欧几里得距离来测量两个点之间的距离。因此,欧几里得距离是聚类算法中一个直观的选择,使我们能够在定位簇时判断数据点的接近度。

然而,大多数距离度量方法(包括欧几里得距离)存在一个缺点:随着维度的增加,这些距离会变得更加均匀。只有在维度或特征较少时,才容易看到哪些点与其他点最接近。然而,当我们添加更多特征时,相关特征与所有其他数据一起嵌入,并且很难从中区分出真正的相关特征,因为它们像噪声一样影响模型。因此,去除这些噪声特征可能会大大提高我们聚类模型的准确性。

注意

数据集中的噪声可以是无关信息或不需要的随机性。

在接下来的部分,我们将探讨两种不同的聚类方法。

聚类方法

聚类有两种类型:

  • 平面聚类

  • 层次聚类

在平面聚类中,我们需要指定机器要找到的聚类数目。平面聚类的一个例子是 k-means 算法,其中 k 指定我们希望算法使用的聚类数目。

然而,在层次聚类中,机器学习算法本身会自动确定需要的聚类数量。

层次聚类也有两种方法:

  • 凝聚性或自底向上的层次聚类开始时将每个点视为一个独立的聚类。然后,最接近的聚类会合并在一起。这个过程会一直重复,直到最终得到一个包含所有数据点的单一聚类。

  • 分裂性或自顶向下的层次聚类开始时将所有数据点视为一个聚类。然后,选择最远的数据点将聚类拆分成更小的聚类。这个过程会一直重复,直到每个数据点都变成自己的聚类。

图 5.3 给出了这两种聚类方法的更准确描述。

图 5.3:展示这两种方法的图

图 5.3:展示这两种方法的图

现在我们已经熟悉了不同的聚类方法,接下来让我们看看 scikit-learn 支持的不同聚类算法。

scikit-learn 支持的聚类算法

在本章中,我们将学习 scikit-learn 支持的两种聚类算法:

  • k-means 算法

  • 均值漂移算法

K-means 是平面聚类的一个示例,其中我们必须事先指定聚类的数量。k-means 是一种通用的聚类算法,当聚类数量不是很高且聚类大小相对均匀时,它表现良好。

均值漂移是层次聚类的一种示例,在这种算法中,聚类算法会自动确定聚类的数量。当我们事先不知道聚类数量时,可以使用均值漂移。与 k-means 相比,均值漂移支持那些聚类数量很多且大小差异较大的应用场景。

Scikit-learn 包含许多其他算法,但本章我们将重点关注 k-means 和均值漂移算法。

注意

有关聚类算法的完整描述,包括性能比较,请访问 scikit-learn 的聚类页面:scikit-learn.org/stable/modules/clustering.html

在下一节中,我们将从 k-means 算法开始。

K-Means 算法

k-means 算法是一种平面聚类算法,正如前面提到的,它的工作原理如下:

  • 设置 k 的值。

  • 从数据集中选择 k 个数据点作为各个聚类的初始中心。

  • 计算每个数据点到所选中心点的距离,并将每个点归类到初始中心点最接近的聚类中。

  • 一旦所有点都被分配到 k 个聚类中,计算每个聚类的中心点。这个中心点不必是数据集中现有的数据点,它只是一个平均值。

  • 重复这一过程,将每个数据点分配给与数据点最接近的聚类中心。重复此过程,直到中心点不再移动。

为了确保 k-means 算法能够终止,我们需要以下条件:

  • 设定一个最大阈值,算法将在达到此值时终止。

  • 移动点的最大重复次数。

由于 k-means 算法的特性,它很难处理大小差异较大的聚类。

k-means 算法有许多应用案例,已经成为我们日常生活的一部分,例如:

  • 市场细分:公司收集各种关于客户的数据。对客户进行 k-means 聚类分析将揭示具有明确特征的客户群体(聚类)。属于同一细分市场的客户可以看作有着相似的模式或偏好。

  • 内容标签化:任何内容(视频、书籍、文档、电影或照片)都可以分配标签,以便将相似的内容或主题进行分组。这些标签是聚类的结果。

  • 欺诈和犯罪活动检测:欺诈者通常会留下与其他客户行为不同的异常线索。例如,在车险行业,正常客户会因事故索赔损坏的汽车,而欺诈者则会索赔故意损坏的车辆。聚类可以帮助检测损坏是否来自真实事故或伪造的事故。

在下一个练习中,我们将实现 scikit-learn 中的 k-means 算法。

练习 5.01:在 scikit-learn 中实现 K-Means。

在这个练习中,我们将在二维平面上绘制一个数据集,并使用 k-means 算法对其进行聚类。

以下步骤将帮助你完成这个练习:

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 现在创建一个作为 NumPy 数组的人工数据集,以演示 k-means 算法。数据点如下代码片段所示:

    import numpy as np
    data_points = np.array([[1, 1], [1, 1.5], [2, 2], \
                            [8, 1], [8, 0], [8.5, 1], \
                            [6, 1], [1, 10], [1.5, 10], \
                            [1.5, 9.5], [10, 10], [1.5, 8.5]])
    
  3. 现在,使用matplotlib.pyplot在二维平面上绘制这些数据点,如下代码片段所示:

    import matplotlib.pyplot as plot
    plot.scatter(data_points.transpose()[0], \
                 data_points.transpose()[1])
    

    预期的输出是这样的:

    图 5.4:使用 matplotlib.pyplot 在二维平面上显示数据点的图表    matplotlib.pyplot

    图 5.4:使用 matplotlib.pyplot 在二维平面上显示数据点的图表

    注意

    我们使用了transpose数组方法来获取第一个特征和第二个特征的值。我们也可以使用适当的数组索引来访问这些列:dataPoints[:,0],这等同于dataPoints.transpose()[0]

    现在我们已经有了数据点,是时候对其执行 k-means 算法了。

  4. 在 k-means 算法中,将k定义为3。我们期望图表的左下角、左上角和右下角各有一个聚类。添加random_state = 8以便重现相同的结果:

    from sklearn.cluster import KMeans
    k_means_model = KMeans(n_clusters=3,random_state=8)
    k_means_model.fit(data_points)
    

    在前面的代码片段中,我们使用了sklearn.cluster中的KMeans模块。像往常一样,在使用sklearn时,我们需要先定义一个模型并设置参数,然后将模型应用到数据集上。

    预期的输出是这样的:

    KMeans(algorithm='auto', copy_x=True, init='k-means++', 
           max_iter=300, n_clusters=3, n_init=10, n_jobs=None,
           precompute_distances='auto',
           random_state=8, tol=0.0001, verbose=0)
    

    输出显示了我们 k-means 模型的所有参数,但重要的参数是:

    max_iter:表示 k-means 算法的最大迭代次数。

    n_clusters:表示由 k-means 算法形成的聚类数量。

    n_init:表示 k-means 算法初始化随机点的次数。

    tol:表示检查 k-means 算法是否可以终止的阈值。

  5. 聚类完成后,访问每个聚类的中心点,如下代码片段所示:

    centers = k_means_model.cluster_centers_
    centers
    

    centers的输出将如下所示:

    array([[7.625     , 0.75      ],
           [3.1       , 9.6       ],
           [1.33333333, 1.5       ]])
    

    该输出显示了我们三个聚类中心的坐标。如果你回顾图 5.4,你会看到聚类的中心点似乎位于图表的左下角(1.3, 1.5)、左上角(3.1, 9.6)和右下角(7.265, 0.75)。左上角聚类的x坐标为3.1,很可能是因为它包含了我们位于[10, 10]的异常数据点。

  6. 接下来,用不同的颜色绘制聚类及其中心点。要找出哪个数据点属于哪个聚类,我们必须查询 k-means 分类器的labels属性,如下代码片段所示:

    labels = k_means_model.labels_
    labels
    

    labels的输出将如下所示:

    array([2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 1, 1])
    

    输出数组显示了每个数据点属于哪个聚类。这就是我们绘制数据所需要的所有信息。

  7. 现在,按照以下代码片段绘制数据:

    plot.scatter(centers[:,0], centers[:,1])
    for i in range(len(data_points)):
        plot.plot(data_points[i][0], data_points[i][1], \
                  ['k+','kx','k_'][k_means_model.labels_[i]])
    plot.show()
    

    在前面的代码片段中,我们使用了matplotlib库将数据点与每个坐标的中心一起绘制。每个聚类都有自己的标记(x+-),其中心由一个实心圆表示。

    预期的输出是这样的:

    图 5.5:显示三个簇的中心点图

    图 5.5:显示三个簇的中心点图

    查看图 5.5,你可以看到中心点位于它们的簇内,这些簇由x+-标记表示。

  8. 现在,重新使用相同的代码,只选择两个簇而不是三个:

    k_means_model = KMeans(n_clusters=2,random_state=8)
    k_means_model.fit(data_points)
    centers2 = k_means_model.cluster_centers_
    labels2 = k_means_model.labels_
    plot.scatter(centers2[:,0], centers2[:,1])
    for i in range(len(data_points)):
        plot.plot(data_points[i][0], data_points[i][1], \
                  ['k+','kx'][labels2[i]])
    plot.show()
    

    期望输出为:

    图 5.6:显示两个簇的数据点图

    图 5.6:显示两个簇的数据点图

    这一次,我们只有x+两种点,并且可以清楚地看到一个底部簇和一个顶部簇。有趣的是,第二次尝试中的顶部簇包含了与第一次尝试中的顶部簇相同的点。第二次尝试中的底部簇由第一次尝试中连接左下角和右下角簇的数据点组成。

  9. 最后,使用 k-means 模型进行预测,如以下代码片段所示。输出将是一个包含每个数据点所属簇编号的数组:

    predictions = k_means_model.predict([[5,5],[0,10]])
    predictions
    

    predictions的输出如下:

    array([0, 1], dtype=int32)
    

    这意味着我们的第一个点属于第一个簇(位于底部),第二个点属于第二个簇(位于顶部)。

    注意

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

    你也可以在packt.live/2Nnv7F2上在线运行此示例。你必须执行整个 Notebook 才能获得期望的结果。

完成本练习后,你能够在样本数据点上使用一个简单的 k-means 聚类模型。

scikit-learn 中的 K-Means 算法参数化

第二章《回归介绍》、第三章《分类介绍》和第四章《决策树介绍》中的分类和回归模型类似,k-means 算法也可以进行参数化。完整的参数列表可以在scikit-learn.org/stable/modules/generated/sklearn.cluster.KMeans.html找到。

一些示例如下:

  • n_clusters:将数据点分为的簇的数量。默认值为8

  • max_iter:最大迭代次数。

  • tol:检查是否可以终止 k-means 算法的阈值。

我们还使用了两个属性来检索簇中心点和簇本身:

  • cluster_centers_:返回簇中心点的坐标。

  • labels_:返回一个整数数组,表示数据点所属的簇编号。编号从零开始。

练习 5.02:检索中心点和标签

在本练习中,你将能够理解cluster_centers_labels_的用法。

以下步骤将帮助你完成练习:

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 接下来,创建与练习 5.01在 scikit-learn 中实现 K-Means相同的 12 个数据点,但在这里执行四个簇的 k-means 聚类,如下所示的代码片段:

    import numpy as np
    import matplotlib.pyplot as plot
    from sklearn.cluster import KMeans
    data_points = np.array([[1, 1], [1, 1.5], [2, 2], \
                            [8, 1], [8, 0], [8.5, 1], \
                            [6, 1], [1, 10], [1.5, 10], \
                            [1.5, 9.5], [10, 10], [1.5, 8.5]])
    k_means_model = KMeans(n_clusters=4,random_state=8)
    k_means_model.fit(data_points)
    centers = k_means_model.cluster_centers_
    centers
    

    centers的输出如下:

    array([[ 7.625     ,  0.75      ],
           [ 1.375     ,  9.5       ],
           [ 1.33333333,  1.5       ],
           [10\.        , 10\.        ]])
    

    cluster_centers_属性的输出显示了中心点的xy坐标。

    从输出结果中,我们可以看到4个中心,分别是右下角(7.6, 0.75)、左上角(1.3, 9.5)、左下角(1.3, 1.5)和右上角(10, 10)。我们还可以注意到,第四个簇(右上角簇)只有一个数据点。这个数据点可以被认为是一个异常值

  3. 现在,在簇上应用labels_ 属性

    labels = k_means_model.labels_
    labels
    

    labels的输出如下:

    array([2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 3, 1], dtype=int32)
    

    labels_属性是一个长度为12的数组,显示了每个12个数据点所属的簇。第一个簇与数字 0 相关,第二个与 1 相关,第三个与 2 相关,以此类推(请记住,Python 的索引从 0 开始,而不是从 1 开始)。

    注意

    要访问该部分的源代码,请参考packt.live/3dmHsDX

    您还可以在packt.live/2B0ebld上在线运行此示例。您必须执行整个 Notebook 以获得期望的结果。

通过完成此练习,您可以获取簇的中心坐标。同时,您也可以看到每个数据点被分配到哪个标签(簇)。

销售数据的 K-Means 聚类

在接下来的活动中,我们将查看销售数据,并对这些销售数据执行 k-means 聚类。

活动 5.01:使用 K-Means 聚类销售数据

在本活动中,您将处理销售交易数据集(Weekly dataset),该数据集包含了 800 种产品在 1 年内的每周销售数据。我们的数据集不会包含任何有关产品的信息,除了销售数据。

你的目标是使用 k-means 聚类算法识别具有相似销售趋势的产品。你需要尝试不同的簇数,以找到最佳簇数。

注意

数据集可以在archive.ics.uci.edu/ml/datasets/Sales_Transactions_Dataset_Weekly找到。

数据集文件也可以在我们的 GitHub 存储库中找到:packt.live/3hVH42v

引用:Tan, S., & San Lau, J. (2014). 时间序列聚类:市场篮子分析的优越替代方法。在《第一届国际先进数据与信息工程会议论文集(DaEng-2013)》(第 241-248 页)中

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

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 将数据集加载为 DataFrame,并检查数据。

  3. 使用 pandas 的drop函数创建一个没有不必要列的新 DataFrame(即数据集中的前55列),并使用 pandas 的inplace参数。

  4. 创建一个包含8个集群且random_state = 8的 k-means 聚类模型。

  5. 从第一个聚类模型中提取标签。

  6. 从第一个 DataFrame,df,只保留W列和标签作为新列。

  7. 使用 pandas 中的groupby函数进行所需的聚合,以便获取每个集群的年度平均销售额。

预期输出如下:

图 5.7:使用 k-means 算法处理销售交易数据的预期输出

图 5.7:使用 k-means 算法处理销售交易数据的预期输出

注意

本活动的解决方案见第 363 页。

现在你已经详细了解了 k-means 算法,我们将继续介绍另一种聚类算法,即均值漂移算法。

均值漂移算法

均值漂移是一种层次聚类算法,通过计算集群中心并在每次迭代时将其移动到模式位置来分配数据点到某个集群。模式是数据点最多的区域。在第一次迭代中,随机选择一个点作为集群中心,然后算法将计算所有在某个半径范围内的邻近数据点的均值。该均值将成为新的集群中心。第二次迭代将从计算所有邻近数据点的均值并将其设为新集群中心开始。每次迭代时,集群中心将向数据点最多的地方移动。当新集群中心无法包含更多数据点时,算法将停止。当算法停止时,每个数据点将被分配到一个集群。

与 k-means 算法不同,均值漂移算法还会确定所需的集群数量。这一点非常有优势,因为我们通常并不知道需要多少个集群。

这个算法也有许多应用场景。例如,Xbox Kinect 设备使用均值漂移算法来检测人体部位。每个主要部位(头部、手臂、腿部、手等)都是由均值漂移算法分配的数据点集群。

在下一个练习中,我们将实现均值漂移算法。

练习 5.03:实现均值漂移算法

在这个练习中,我们将使用均值漂移算法来实现聚类。

我们将使用scipy.spatial库来计算欧几里得距离,见于第一章人工智能导论。这个库简化了计算坐标列表之间的距离(如欧几里得距离或曼哈顿距离)。更多关于此库的细节可以在docs.scipy.org/doc/scipy/reference/spatial.distance.html#module-scipy.spatial.distance找到。

以下步骤将帮助你完成练习:

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 我们将使用练习 5.01中的数据点,在 scikit-learn 中实现 K 均值算法

    import numpy as np
    data_points = np.array([[1, 1], [1, 1.5], [2, 2], \
                            [8, 1], [8, 0], [8.5, 1], \
                            [6, 1], [1, 10], [1.5, 10], \
                            [1.5, 9.5], [10, 10], [1.5, 8.5]])
    import matplotlib.pyplot as plot
    plot.scatter(data_points.transpose()[0], \
                 data_points.transpose()[1])
    

    我们现在的任务是找到点 P(x, y),使得从点 P 出发,半径 R 范围内的数据点数量最大化。点的分布如下:

    图 5.8:显示数据点的图表,来自 data_points 数组

    图 5.8:显示数据点的图表,来自 data_points 数组

  3. 将点P1等同于我们列表中的第一个数据点[1, 1]

    P1 = [1, 1]
    
  4. 找到位于此点r = 2距离范围内的点。我们将使用scipy库,它简化了数学计算,包括空间距离:

    from scipy.spatial import distance
    r = 2
    points1 = np.array([p0 for p0 in data_points if \
                        distance.euclidean(p0, P1) <= r])
    points1
    

    在前面的代码片段中,我们使用了欧几里得距离来找到所有位于点P1的半径r范围内的点。

    points1的输出将如下所示:

    array([[1\. , 1\. ],
           [1\. , 1.5],
           [2\. , 2\. ]])
    

    从输出中,我们可以看到我们找到了三个位于P1半径内的点。它们是我们之前在图表左下方看到的三个点,位于本章的图 5.8中。

  5. 现在,计算数据点的均值,以获得P2的新坐标:

    P2 = [np.mean( points1.transpose()[0] ), \
          np.mean(points1.transpose()[1] )]
    P2
    

    在前面的代码片段中,我们计算了包含三个数据点的数组的均值,以便获得P2的新坐标。

    P2的输出将如下所示:

    [1.3333333333333333, 1.5]
    
  6. 现在,P2已被计算出来,再次检索给定半径内的点,如下所示的代码片段:

    points2 = np.array([p0 for p0 in data_points if \
                        distance.euclidean( p0, P2) <= r])
    points2
    

    points的输出将如下所示:

    array([[1\. , 1\. ],
           [1\. , 1.5],
           [2\. , 2\. ]])
    

    这些是我们在步骤 4中找到的相同的三个点,因此我们可以在此停止。我们已经找到了围绕均值[1.3333333333333333, 1.5]的三个点。以该中心为圆心,半径为2的点形成了一个簇。

  7. 由于数据点[1, 1.5][2, 2]已经与[1,1]形成一个簇,我们可以直接继续使用我们列表中的第四个点[8, 1]

    P3 = [8, 1]
    points3 = np.array( [p0 for p0 in data_points if \
                         distance.euclidean(p0, P3) <= r])
    points3
    

    在前面的代码片段中,我们使用了与步骤 4相同的代码,但采用了新的P3

    points3的输出将如下所示:

    array([[8\. , 1\. ],
           [8\. , 0\. ],
           [8.5, 1\. ],
           [6\. , 1\. ]])
    

    这次,我们找到了四个位于P4半径r内的点。

  8. 现在,按照以下代码片段计算均值:

    P4 = [np.mean(points3.transpose()[0]), \
          np.mean(points3.transpose()[1])]
    P4
    

    在前面的代码片段中,我们计算了包含四个数据点的数组的均值,以便获得P4的新坐标,如步骤 5所示。

    P4的输出将如下所示:

    [7.625, 0.75]
    

    这个均值不会改变,因为在下一次迭代中,我们会找到相同的数据点。

  9. 请注意,我们在选择点[8, 1]时比较幸运。如果我们从P = [8, 0]P = [8.5, 1]开始,我们只能找到三个点,而不是四个。让我们尝试使用P5 = [8, 0]

    P5 = [8, 0]
    points4 = np.array([p0 for p0 in data_points if \
                       distance.euclidean(p0, P5) <= r])
    points4
    

    在前面的代码片段中,我们使用了与步骤 4相同的代码,但采用了新的P5

    points4的输出将如下所示:

    array([[8\. , 1\. ],
           [8\. , 0\. ],
           [8.5, 1\. ]])
    

    这次,我们找到了三个位于P5的半径r内的点。

  10. 现在,使用步骤 5中所示的偏移均值重新运行距离计算:

    P6 = [np.mean(points4.transpose()[0]), \
          np.mean(points4.transpose()[1])]
    P6
    

    在前面的代码片段中,我们计算了包含三个数据点的数组的均值,以获得新的 P6 坐标。

    P6 的输出将如下所示:

    [8.166666666666666, 0.6666666666666666]
    
  11. 现在再做一次,但使用 P7 = [8.5, 1]

    P7 = [8.5, 1]
    points5 = np.array([p0 for p0 in data_points if \
                        distance.euclidean(p0, P7) <= r])
    points5
    

    在前面的代码片段中,我们使用了与 步骤 4 中相同的代码,但用了一个新的 P7

    points5 的输出将如下所示:

    array([[8\. , 1\. ],
           [8\. , 0\. ],
           [8.5, 1\. ]])
    

    这一次,我们又在 P 的半径 r 内找到了相同的三个点。这意味着,从 [8,1] 开始,我们得到了一个比从 [8, 0][8.5, 1] 开始更大的聚类。因此,我们必须选择包含最多数据点的中心点。

  12. 现在,让我们看看如果从第四个数据点 [6, 1] 开始会发生什么:

    P8 = [6, 1]
    points6 = np.array([p0 for p0 in data_points if \
                        distance.euclidean(p0, P8) <= r])
    points6
    

    在前面的代码片段中,我们使用了与 步骤 4 中相同的代码,但用了一个新的 P8

    points6 的输出将如下所示:

    array([[8., 1.],
           [6., 1.]])
    

    这次,我们只在 P8 的半径 r 内找到了两个数据点。我们成功找到了数据点 [8, 1]

  13. 现在,将均值从 [6, 1] 移动到计算得到的新均值:

    P9 = [np.mean(points6.transpose()[0]), \
          np.mean(points6.transpose()[1]) ]
    P9
    

    在前面的代码片段中,我们计算了包含三个数据点的数组的均值,以获得新的 P9 坐标,正如 步骤 5 中所示。

    P9 的输出将如下所示:

    [7.0, 1.0]
    
  14. 检查是否通过这个新的 P9 得到了更多的点:

    points7 = np.array([p0 for p0 in data_points if \
                        distance.euclidean(p0, P9) <= r])
    points7
    

    在前面的代码片段中,我们使用了与 步骤 4 中相同的代码,但用了一个新的 P9

    points7 的输出将如下所示:

    array([[8\. , 1\. ],
           [8\. , 0\. ],
           [8.5, 1\. ],
           [6\. , 1\. ]])
    

    我们成功找到了所有四个点。因此,我们已经成功定义了一个大小为 4 的聚类。均值将与之前相同:[7.625, 0.75]

    注意

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

    你还可以在 packt.live/2YoSu78 在线运行这个示例。你必须执行整个 Notebook 才能得到期望的结果。

这是一个简单的聚类示例,应用了均值迁移算法。我们只是展示了算法在寻找聚类时会考虑什么。

然而,仍然有一个问题,那就是半径的值应该是多少?

请注意,如果没有设置 2 的半径,我们可以简单地从一个包含所有数据点的大半径开始,然后逐步减小半径,或者从一个非常小的半径开始,确保每个数据点都在其所属的聚类中,然后逐步增加半径,直到得到期望的结果。

在下一节中,我们将介绍均值迁移算法,但这次使用 scikit-learn。

scikit-learn 中的均值迁移算法

让我们使用与 k-means 算法相同的数据点:

import numpy as np
data_points = np.array([[1, 1], [1, 1.5], [2, 2], \
                        [8, 1], [8, 0], [8.5, 1], \
                        [6, 1], [1, 10], [1.5, 10], \
                        [1.5, 9.5], [10, 10], [1.5, 8.5]])

均值迁移聚类算法的语法与 k-means 聚类算法的语法类似:

from sklearn.cluster import MeanShift
mean_shift_model = MeanShift()
mean_shift_model.fit(data_points)

一旦聚类完成,我们就可以访问每个聚类的中心点:

mean_shift_model.cluster_centers_

期望的输出是这样的:

array([[ 1.375     ,  9.5       ],
       [ 8.16666667,  0.66666667],
       [ 1.33333333,  1.5       ],
       [10\.        , 10\.        ],
       [ 6\.        ,  1\.        ]])

均值迁移模型找到了五个聚类,中心如前面的代码所示。

和 k-means 一样,我们也可以获得标签:

mean_shift_model.labels_

预期输出是这样的:

array([2, 2, 2, 1, 1, 1, 4, 0, 0, 0, 3, 0], dtype=int64)

输出数组显示哪个数据点属于哪个簇。这是我们绘制数据所需要的全部内容:

import matplotlib.pyplot as plot
plot.scatter(mean_shift_model.cluster_centers_[:,0], \
             mean_shift_model.cluster_centers_[:,1])
for i in range(len(data_points)): 
    plot.plot(data_points[i][0], data_points[i][1], \
              ['k+','kx','kv', 'k_', 'k1']\
              [mean_shift_model.labels_[i]])
plot.show()

在之前的代码片段中,我们绘制了数据点和五个簇的中心。属于同一簇的每个数据点将使用相同的标记。簇中心则用点标记。

预期输出是这样的:

图 5.9:显示五个簇的数据点的图形

图 5.9:显示五个簇的数据点的图形

我们可以看到,三个簇包含多个点(左上、左下和右下)。那两个作为单独簇的数据点可以被视为异常值,正如之前提到的,它们离其他簇太远,因此不属于任何簇。

现在我们已经了解了均值漂移算法,可以来看看层次聚类,尤其是聚合层次聚类(自下而上的方法)。

层次聚类

层次聚类算法分为两类:

  • 聚合(或自下而上)层次聚类

  • 分割(或自上而下)层次聚类

本章我们将只讨论聚合层次聚类,因为它是两种方法中最广泛使用且最有效的。

聚合层次聚类(Agglomerative Hierarchical Clustering)最开始将每个数据点视为一个单独的簇,然后依次将最接近的簇按对进行合并(或聚合)。为了找到最接近的数据簇,聚合层次聚类使用欧几里得距离或曼哈顿距离等启发式方法来定义数据点之间的距离。还需要一个聚类函数来聚合簇内数据点之间的距离,从而定义簇之间的接近度的唯一值。

聚类函数的示例包括单一聚类(简单距离)、平均聚类(平均距离)、最大聚类(最大距离)和沃德聚类(平方差)。具有最小聚类值的簇对将被归为一组。此过程将重复进行,直到只剩下一个包含所有数据点的簇。最终,当只剩下一个簇时,该算法终止。

为了直观地表示簇的层次结构,可以使用树状图(dendrogram)。树状图是一棵树,底部的叶子表示数据点。两个叶子之间的每个交点表示这两个叶子的分组。根(顶部)表示一个包含所有数据点的唯一簇。请看图 5.10,它展示了一个树状图。

图 5.10:树状图示例

图 5.10:树状图示例

scikit-learn 中的聚合层次聚类

看一下以下示例,我们使用了与 k-means 算法相同的数据点:

import numpy as np
data_points = np.array([[1, 1], [1, 1.5], [2, 2], \
                        [8, 1], [8, 0], [8.5, 1], \
                        [6, 1], [1, 10], [1.5, 10], \
                        [1.5, 9.5], [10, 10], [1.5, 8.5]])

为了绘制树状图,我们需要先导入scipy库:

from scipy.cluster.hierarchy import dendrogram
import scipy.cluster.hierarchy as sch

然后我们可以使用 SciPy 中的ward连接函数绘制一个树状图,因为它是最常用的连接函数:

dendrogram = sch.dendrogram(sch.linkage(data_points, \
                            method='ward'))

树状图的输出结果如下所示:

图 5.11:基于随机数据点的树状图

图 5.11:基于随机数据点的树状图

使用树状图时,我们通常可以通过简单地在图 5.12中所示的最大垂直距离区域画一条水平线,并计算交点数量,来大致猜测一个合适的聚类数。在这个例子中,应该是两个聚类,但我们将选择下一个较大的区域,因为两个聚类的数量太小了。

图 5.12:树状图中的聚类划分

图 5.12:树状图中的聚类划分

y轴表示聚类的接近度,而x轴表示每个数据点的索引。因此,我们的前三个数据点(0,1,2)属于同一个聚类,接下来的四个数据点(3,4,5,6)组成另一个聚类,数据点10独立成一个聚类,剩余的(7,8,9,11)则组成最后一个聚类。

层次聚类算法的语法与 k-means 聚类算法类似,不同之处在于我们需要指定affinity的类型(在这里我们选择欧几里得距离)和连接方式(在这里我们选择ward连接):

from sklearn.cluster import AgglomerativeClustering
agglomerative_model = AgglomerativeClustering(n_clusters=4, \
                                              affinity='euclidean', \
                                              linkage='ward')
agglomerative_model.fit(data_points)

输出结果为:

AgglomerativeClustering(affinity='euclidean', 
                        compute_full_tree='auto',
                        connectivity=None, 
                        distance_threshold=None,
                        linkage='ward', memory=None,
                        n_clusters=4, pooling_func='deprecated')

类似于 k-means,我们也可以通过以下代码片段获得标签:

agglomerative_model.labels_

预期输出为:

array([2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 3, 1], dtype=int64)

输出数组显示了每个数据点所属的聚类。这就是我们绘制数据所需的全部信息:

import matplotlib.pyplot as plot
for i in range(len(data_points)): 
    plot.plot(data_points[i][0], data_points[i][1], \
              ['k+','kx','kv', 'k_'][agglomerative_model.labels_[i]])
plot.show()

在前面的代码片段中,我们绘制了数据点和四个聚类中心的图。每个属于同一聚类的数据点将具有相同的标记。

预期输出为:

图 5.13:显示四个聚类数据点的图

图 5.13:显示四个聚类数据点的图

我们可以看到,与均值漂移方法的结果相比,层次聚类能够正确地将位于(6,1)的数据点与右下角的聚类组合在一起,而不是单独形成一个聚类。在像这样的情况下,当数据量非常少时,层次聚类和均值漂移比 k-means 效果更好。然而,它们的计算时间需求非常高,这使得它们在非常大的数据集上表现不佳。不过,k-means 非常快速,是处理大数据集的更好选择。

现在我们已经了解了一些不同的聚类算法,接下来需要开始评估这些模型并进行比较,以选择最适合聚类的最佳模型。

聚类性能评估

与监督学习不同,监督学习中我们始终可以使用标签来评估预测结果,而无监督学习则稍显复杂,因为我们通常没有标签。为了评估聚类模型,可以根据是否有标签数据采取两种方法:

  • 第一种方法是外在方法,它需要标签数据的存在。这意味着,如果没有标签数据,必须有人为干预来标记数据,或者至少是其中的一部分。

  • 另一种方法是内在方法。一般来说,外在方法试图根据标签数据为聚类分配一个分数,而内在方法则通过检查聚类的分离度和紧凑度来评估聚类。

    注意

    我们将跳过数学解释,因为它们相当复杂。

    您可以在 sklearn 网站上找到更多数学细节,网址为:scikit-learn.org/stable/modules/clustering.html#clustering-performance-evaluation

我们将从外在方法开始(因为它是最广泛使用的方法),并使用 sklearn 在我们的 k 均值示例中定义以下分数:

  • 调整后的兰德指数

  • 调整后的互信息

  • 同质性

  • 完整性

  • V-度量

  • Fowlkes-Mallows 分数

  • 互信息矩阵

让我们看一个示例,在这个示例中,我们首先需要从sklearn.cluster导入metrics模块:

from sklearn import metrics

我们将重用在练习 5.01中使用的代码,在 scikit-learn 中实现 K 均值算法:

import numpy as np
import matplotlib.pyplot as plot
from sklearn.cluster import KMeans
data_points = np.array([[1, 1], [1, 1.5], [2, 2], \
                        [8, 1], [8, 0], [8.5, 1], \
                        [6, 1], [1, 10], [1.5, 10], \
                        [1.5, 9.5], [10, 10], [1.5, 8.5]])
k_means_model = KMeans(n_clusters=3,random_state = 8)
k_means_model.fit(data_points)
k_means_model.labels_

我们使用k_means_model.labels_预测标签的输出是:

array([2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 1, 1])

最后,定义该数据集的真实标签,如下代码片段所示:

data_labels = np.array([0, 0, 0, 2, 2, 2, 2, 1, 1, 1, 3, 1])

调整后的兰德指数

调整后的兰德指数是一个度量聚类预测与标签之间相似性的函数,同时忽略排列顺序。当标签是大而相等的聚类时,调整后的兰德指数表现得很好。

调整后的兰德指数的范围是[-1.1],其中负值是不理想的。负分数意味着我们的模型表现比随机分配标签还要差。如果我们随机分配标签,分数接近 0。但是,越接近 1,说明我们的聚类模型在预测正确标签方面表现越好。

使用sklearn,我们可以通过以下代码轻松计算调整后的兰德指数:

metrics.adjusted_rand_score(data_labels, k_means_model.labels_)

期望的输出是这样的:

0.8422939068100358

在这种情况下,调整后的兰德指数表明我们的 k 均值模型与真实标签之间的差距不大。

调整后的互信息

调整后的互信息是一个度量聚类预测与标签之间的熵的函数,同时忽略排列顺序。

调整互信息没有固定的范围,但负值被认为是差的。我们越接近 1,聚类模型预测正确标签的效果就越好。

使用sklearn,我们可以通过以下代码轻松计算它:

metrics.adjusted_mutual_info_score(data_labels, \
                                   k_means_model.labels_)

预期输出如下:

0.8769185235006342

在这种情况下,调整互信息表明我们的 k-means 模型相当优秀,与真实标签的差距不大。

V-Measure、同质性和完整性

V-Measure 被定义为同质性和完整性的调和均值。调和均值是一种平均值(其他类型的平均值有算术平均和几何平均),它使用倒数(倒数是一个数的倒数。例如,2 的倒数是公式,3 的倒数是37)。

调和均值的公式如下:

图 5.14:调和均值公式

](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_05_14.jpg)

图 5.14:调和均值公式

公式 3是值的数量,公式 4是每个点的值。

为了计算 V-Measure,我们首先需要定义同质性和完整性。

完美的同质性指的是每个聚类中的数据点都属于相同的标签。该同质性得分将反映我们的每个聚类在将数据从相同标签分组时的效果。

完美的完整性指的是所有属于相同标签的数据点被聚集到同一个聚类中的情况。同质性得分将反映每个标签的所有数据点是否都被很好地分组到同一个聚类中。

因此,V-Measure 的公式如下:

图 5.15:V-Measure 公式

](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_05_15.jpg)

图 5.15:V-Measure 公式

公式 5的默认值为1,但可以根据需要进行调整,以进一步强调同质性或完整性。

这三个得分的范围是[0,1],其中0表示最差得分,1表示完美得分。

使用sklearn,我们可以通过以下代码轻松计算这三个得分:

metrics.homogeneity_score(data_labels, k_means_model.labels_)
metrics.completeness_score(data_labels, k_means_model.labels_)
metrics.v_measure_score(data_labels, k_means_model.labels_, \
                        beta=1)

homogeneity_score的输出如下:

0.8378758055108827

在这种情况下,同质性得分表明我们的 k-means 模型中的聚类包含了不同的标签。

completeness_score的输出如下:

1.0

在这种情况下,完整性得分表明我们的 k-means 模型成功地将每个标签的所有数据点放入同一个聚类中。

v_measure_score的输出如下:

0.9117871871412709

在这种情况下,V-Measure 表明我们的 k-means 模型虽然不完美,但总体得分良好。

Fowlkes-Mallows 得分

Fowlkes-Mallows 得分是衡量标签聚类内相似性和聚类预测的一个指标,它定义为精确度和召回率的几何均值(你在第四章决策树入门中学到过这个概念)。

Fowlkes-Mallows 分数的公式如下:

图 5.16:Fowlkes-Mallows 公式

图 5.16:Fowlkes-Mallows 公式

我们来分解一下:

  • 真阳性(TP):是所有预测与标签簇相同的观测值。

  • 假阳性(FP):是所有预测在同一簇中,但与标签簇不同的观测值。

  • 假阴性(FN):是所有预测不在同一簇中,但位于同一标签簇中的观测值。

Fowlkes-Mallows 分数的范围是 [0, 1],其中 0 是最差的分数,1 是完美的分数。

使用 sklearn,我们可以通过以下代码轻松计算它:

metrics.fowlkes_mallows_score(data_labels, k_means_model.labels_)

期望的输出是:

0.8885233166386386

在这种情况下,Fowlkes-Mallows 分数表明我们的 k-means 模型相当不错,并且与真实标签相差不远。

聚类矩阵

聚类矩阵不是一个分数,而是报告每个真实/预测簇对的交集基数以及所需的标签数据。它非常类似于在《第四章,决策树简介》中看到的 混淆矩阵。该矩阵必须与标签和簇名称一致,因此我们需要小心地将簇命名为与标签相同,而在之前看到的分数中并未做到这一点。

我们将把标签从以下内容修改为:

data_labels = np.array([0, 0, 0, 2, 2, 2, 2, 1, 1, 1, 3, 1])

到此:

data_labels = np.array([2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 3, 0])

然后,使用 sklearn,我们可以通过以下代码轻松计算聚类矩阵:

from sklearn.metrics.cluster import contingency_matrix
contingency_matrix(k_means_model.labels_,data_labels)

contingency_matrix 的输出如下:

array([[0, 4, 0, 0],
       [4, 0, 0, 1],
       [0, 0, 3, 0]])

contingency_matrix 输出的第一行表示有 4 个数据点的真实簇为第一簇(0)。第二行表示有 4 个数据点的真实簇为第二簇(1);然而,在这个簇中错误地预测了一个额外的 1,但它实际上属于第四簇(3)。第三行表示有 3 个数据点的真实簇为第三簇(2)。

现在我们将看一下内在方法,当我们没有标签时,这种方法是必需的。我们将使用 sklearn 在我们的 k-means 示例中定义以下分数:

  • Silhouette Coefficient(轮廓系数)

  • Calinski-Harabasz 指数

  • Davies-Bouldin 指数

Silhouette Coefficient(轮廓系数)

Silhouette Coefficient(轮廓系数)是一个内在评价的例子。它衡量数据点与其簇之间的相似度,相对于其他簇的相似度。

它包括两个分数:

  • a:数据点与同一簇内所有其他数据点之间的平均距离。

  • b:数据点与最近簇内所有数据点之间的平均距离。

轮廓系数公式为:

图 5.17:轮廓系数公式

图 5.17:轮廓系数公式

轮廓系数的范围为[-1,1],其中-1表示错误的聚类。接近零的分数表示聚类存在重叠。接近1的分数表示所有数据点都分配到了合适的聚类中。

然后,使用sklearn,我们可以通过以下代码轻松计算轮廓系数:

metrics.silhouette_score(data_points, k_means_model.labels_)

silhouette_score的输出如下:

0.6753568188872228

在这种情况下,轮廓系数表明我们的 k-means 模型存在一些重叠的聚类,并且可以通过将某些数据点从其中一个聚类中分离出来来进行改进。

Calinski-Harabasz 指数

Calinski-Harabasz 指数衡量每个聚类内部数据点的分布情况。它被定义为聚类之间方差与每个聚类内部方差的比值。Calinski-Harabasz 指数没有范围,起始值为0。分数越高,聚类越密集。密集的聚类表明聚类定义得较好。

使用sklearn,我们可以通过以下代码轻松计算它:

metrics.calinski_harabasz_score(data_points, k_means_model.labels_)

calinski_harabasz_score的输出如下:

19.52509172315154

在这种情况下,Calinski-Harabasz 指数表明我们的 k-means 模型聚类比较分散,建议我们可能有重叠的聚类。

Davies-Bouldin 指数

Davies-Bouldin 指数衡量聚类之间的平均相似性。相似性是聚类与其最近聚类之间的距离与聚类内每个数据点与聚类中心之间的平均距离之比。Davies-Bouldin 指数没有范围,起始值为0。分数越接近0越好,这意味着聚类之间分离得很好,是聚类效果良好的指示。

使用sklearn,我们可以通过以下代码轻松计算 Davies-Bouldin 指数:

metrics.davies_bouldin_score(data_points, k_means_model.labels_)

davies_bouldin_score的输出如下:

0.404206621415983

在这种情况下,Calinski-Harabasz 评分表明我们的 k-means 模型存在一些重叠的聚类,通过更好地分离某些数据点,可以对其中一个聚类进行改进。

活动 5.02:使用均值漂移算法和聚合层次聚类对红酒数据进行聚类

在这个活动中,您将使用红酒质量数据集,具体来说是红葡萄酒数据。该数据集包含 1,599 个红酒的质量数据以及它们的化学测试结果。

您的目标是构建两个聚类模型(使用均值漂移算法和聚合层次聚类),以识别相似质量的葡萄酒是否具有相似的物理化学性质。您还需要使用外部和内部方法评估并比较这两个聚类模型。

注意

数据集可以通过以下网址找到:archive.ics.uci.edu/ml/datasets/Wine+Quality

数据集文件可以在我们的 GitHub 仓库中找到,链接:packt.live/2YYsxuu

引用:P. Cortez, A. Cerdeira, F. Almeida, T. Matos 和 J. Reis. 通过数据挖掘物理化学性质建模葡萄酒偏好。在《决策支持系统》期刊,Elsevier,47(4):547-553,2009

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

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 将数据集加载为 DataFrame,使用sep = ";",并检查数据。

  3. 创建一个均值迁移聚类模型,然后提取该模型的预测标签及其创建的聚类数。

  4. 在创建树状图并选择最佳聚类数后,创建一个聚合层次聚类模型。

  5. 从第一个聚类模型中提取标签。

  6. 为两个模型计算以下外部方法得分:

    调整后的兰德指数

    调整后的互信息

    V 度量

    Fowlkes-Mallows 分数

  7. 为两个模型计算以下内部方法得分:

    轮廓系数

    Calinski-Harabasz 指数

    Davies-Bouldin 指数

期望输出为:

均值迁移聚类模型的每个得分值如下:

  • 调整后的兰德指数:0.0006771608724007207

  • 调整后的互信息:0.004837187596124968

  • V 度量:0.021907254751144124

  • Fowlkes-Mallows 分数:0.5721233634622408

  • 轮廓系数:0.32769323700400077

  • Calinski-Harabasz 指数:44.62091774102674

  • Davies-Bouldin 指数:0.8106334674570222

聚合层次聚类的每个得分值如下:

  • 调整后的兰德指数:0.05358047852603172

  • 调整后的互信息:0.05993098663692826

  • V 度量:0.07549735446050691

  • Fowlkes-Mallows 分数:0.3300681478007641

  • 轮廓系数:0.1591882574407987

  • Calinski-Harabasz 指数:223.5171774491095

  • Davies-Bouldin 指数:1.4975443816135114

    注意

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

完成此活动后,你已对多个产品的多个列进行了均值迁移和聚合层次聚类。你还学习了如何使用外部和内部方法评估聚类模型。最后,你利用模型的结果及其评估,找到了现实世界问题的答案。

总结

在本章中,我们学习了聚类的基础知识。聚类是一种无监督学习形式,其中给定了特征,但没有标签。聚类算法的目标是根据数据点的相似性找到标签。

我们还学到聚类有两种类型,平面聚类和层次聚类,其中第一种类型需要指定聚类的数量,而第二种类型能够自动找到最佳的聚类数量。

k-means 算法是平面聚类的一个例子,而均值漂移和凝聚层次聚类则是层次聚类算法的例子。

我们还学习了评估聚类模型性能的众多评分标准,包括有标签的外部方法和无标签的内部方法。

第六章神经网络与深度学习中,你将接触到一个在过去十年因计算能力的爆炸性增长以及廉价、可扩展的在线服务器容量而变得流行的领域。这个领域是神经网络与深度学习的科学。

第六章:6. 神经网络与深度学习

概述

在本章中,你将了解神经网络和深度学习的最终主题。你将学习 TensorFlow、卷积神经网络(CNNs)和递归神经网络(RNNs)。你将利用深度学习的核心概念来评估个人的信用 worthiness 并预测社区的房价。稍后,你还将使用学到的技能实现一个图像分类程序。到本章结束时,你将牢牢掌握神经网络和深度学习的概念。

介绍

在上一章,我们学习了聚类问题的概念,并了解了几种算法,如 k-means,它们可以自动将数据点分组。在本章中,我们将学习神经网络和深度学习网络。

神经网络与深度学习网络的区别在于网络的复杂性和深度。传统的神经网络只有一个隐藏层,而深度学习网络则有多个隐藏层。

尽管我们将使用神经网络和深度学习进行监督学习,但请注意,神经网络也可以建模无监督学习技术。这种模型在 1980 年代其实非常流行,但由于当时计算能力有限,直到最近,这种模型才被广泛采用。随着图形处理单元(GPU)和云计算的普及,我们现在可以获得巨大的计算能力。这正是神经网络,尤其是深度学习,重新成为热门话题的主要原因。

深度学习能够建模比传统神经网络更复杂的模式,因此如今在计算机视觉(如面部检测和图像识别)和自然语言处理(如聊天机器人和文本生成)中得到了广泛应用。

人工神经元

人工神经网络ANNs),顾名思义,试图模仿人类大脑的工作方式,特别是神经元的工作方式。

神经元是大脑中的一种细胞,通过电信号与其他细胞进行通信。神经元能够响应声音、光线、触摸等刺激。它们也能引发动作,比如肌肉收缩。平均而言,人脑包含约 100 亿到 200 亿个神经元。这是一个相当庞大的网络,对吧?这也是人类能够实现如此多惊人事物的原因。这也是为什么研究人员试图模仿大脑的运作,并由此创造了人工神经网络。

人工神经网络由多个相互连接的人工神经元组成,形成一个网络。人工神经元简单来说是一个处理单元,它对一些输入(x1x2、……、xn)进行数学运算,并将最终结果(y)返回给下一个单元,如下图所示:

图 6.1:人工神经元的表示

图 6.1:人工神经元的表示

我们将在接下来的章节中更详细地了解人工神经元的工作原理。

TensorFlow 中的神经元

TensorFlow 目前是最流行的神经网络和深度学习框架。它由 Google 创建并维护。TensorFlow 用于语音识别和语音搜索,也是 translate.google.com 背后的大脑。稍后在本章中,我们将使用 TensorFlow 来识别书写字符。

TensorFlow API 支持多种编程语言,包括 Python、JavaScript、Java 和 C。TensorFlow 与 张量 一起工作。你可以将张量视为一个容器,它由一个矩阵(通常是高维的)和与其将执行的操作相关的附加信息组成(如权重和偏差,你将在本章稍后看到)。没有维度(即无秩)的张量是标量。秩为 1 的张量是向量,秩为 2 的张量是矩阵,秩为 3 的张量是三维矩阵。秩表示张量的维度。在本章中,我们将讨论秩为 2 和 3 的张量。

注意

数学家使用矩阵和维度的术语,而深度学习程序员则使用张量和秩。

TensorFlow 还提供了数学函数来变换张量,例子包括以下内容:

  • addmultiply

  • explog

  • greaterlessequal

  • concatslicesplit

  • matrix_inversematrix_determinantmatmul

  • sigmoidrelusoftmax

我们将在本章稍后详细讲解这些内容。

在下一个练习中,我们将使用 TensorFlow 来计算一个人工神经元。

练习 6.01:使用基本操作和 TensorFlow 常量

在本练习中,我们将使用 TensorFlow 中的算术操作,通过执行矩阵乘法和加法,并应用非线性函数 sigmoid 来模拟一个人工神经元。

以下步骤将帮助你完成练习:

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 导入 tensorflow 包并将其命名为 tf

    import tensorflow as tf
    
  3. 创建一个名为 W 的形状为 [1,6](即 1 行 6 列)的张量,使用 tf.constant(),并使其包含矩阵 [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]。打印其值:

    W = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[1, 6]) 
    W
    

    预期输出如下:

    <tf.Tensor: shape=(1, 6), dtype=float32, numpy=array([[1., 2., 3., 4., 5., 6.]], dtype=float32)>
    
  4. 创建一个名为 X 的形状为 [6,1](即 6 行 1 列)的张量,使用 tf.constant(),并使其包含 [7.0, 8.0, 9.0, 10.0, 11.0, 12.0]。打印其值:

    X = tf.constant([7.0, 8.0, 9.0, 10.0, 11.0, 12.0], \
                    shape=[6, 1]) 
    X
    

    预期输出如下:

    <tf.Tensor: shape=(6, 1), dtype=float32, numpy= 
    array([[ 7.],
           [ 8.],
           [ 9.],
           [10.],
           [11.],
           [12.]], dtype=float32)>
    
  5. 现在,创建一个名为 b 的张量,使用 tf.constant(),并使其包含 -88。打印其值:

    b = tf.constant(-88.0)
    b
    

    预期输出如下:

    <tf.Tensor: shape=(), dtype=float32, numpy=-88.0>
    
  6. WX 之间执行矩阵乘法,使用 tf.matmul,将其结果保存在 mult 变量中,并打印其值:

    mult = tf.matmul(W, X)
    mult
    

    预期输出如下:

    <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[217.]], dtype=float32)>
    
  7. multb 之间执行矩阵加法,并将结果保存在名为 Z 的变量中,打印其值:

    Z = mult + b
    Z
    

    预期输出如下:

    <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[129.]], dtype=float32)>
    
  8. 使用 tf.math.sigmoidZ 应用 sigmoid 函数,将其结果保存在名为 a 的变量中,并打印其值。sigmoid 函数将任何数值转化到 01 的范围内(我们将在后续章节中深入了解这一点):

    a = tf.math.sigmoid(Z)
    a
    

    预期的输出是这样的:

    <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[1.]], dtype=float32)>
    

sigmoid 函数将 Z 的原始值 129 转换为 1

注意

若要访问本节的源代码,请参考 packt.live/31ekGLM

你也可以在 packt.live/3evuKnC 在线运行此示例。你必须执行整个笔记本才能获得预期的结果。

在本次练习中,你成功地使用 TensorFlow 实现了一个人工神经元。这是任何神经网络模型的基础。

在接下来的章节中,我们将深入研究神经网络的架构。

神经网络架构

神经网络是人工智能AI)最新的分支。神经网络的灵感来源于人类大脑的工作方式。它们由 Warren McCulloch 和 Walter Pitts 在 1940 年代发明。神经网络是一个数学模型,用于描述人类大脑如何解决问题。

当我们谈论人类大脑时,使用 ANN 来指代数学模型和生物神经网络。

神经网络的学习方式比其他分类或回归模型更为复杂。神经网络模型具有许多内部变量,输入和输出变量之间的关系可能涉及多个内部层次。神经网络的准确性通常高于其他监督学习算法。

注意

精通使用 TensorFlow 构建神经网络是一个复杂的过程。本节的目的是为你提供一个入门资源,帮助你开始学习。

在本章中,我们将使用的主要示例是从图像中识别数字。我们采用这种格式是因为每张图像较小,并且我们有约 70,000 张图像可供使用。处理这些图像所需的计算能力类似于普通计算机的能力。

神经网络(ANN)工作原理与人类大脑相似。人类大脑中的树突与细胞核相连,细胞核与轴突相连。在神经网络中,输入相当于树突,计算发生的地方是细胞核,输出则是轴突。

人工神经元被设计成模拟细胞核的工作方式。它将通过矩阵乘法计算和激活函数来转化输入信号。如果激活函数判定神经元必须激发,那么一个信号将出现在输出端。这个信号可以是网络中其他神经元的输入:

图 6.2:显示神经网络如何工作的图

图 6.2:显示神经网络如何工作的图

通过以 n=4 为例,我们可以进一步理解前面的图形。在这种情况下,适用的公式为:

  • X 是输入矩阵,由 x1x2x3x4 组成。

  • W,权重矩阵,将由 w1w2w3w4 组成。

  • b 是偏置。

  • f 是激活函数。

我们将首先通过矩阵乘法和偏置计算 Z(神经元的左侧):

Z = W * X + b = x1*w1 + x2*w2 + x3*w3 + x4*w4 + b

然后,输出 y 将通过应用函数 f 来计算:

y = f(Z) = f(x1*w1 + x2*w2 + x3*w3 + x4*w4 + b)

很好——这就是人工神经元在幕后工作的方式。它是两个矩阵运算,先乘积后求和,再经过函数变换。

现在我们进入下一部分——权重。

权重

y

一个单一的神经元是加权和与激活函数的组合,可以称之为隐藏层。具有一个隐藏层的神经网络称为常规神经网络

图 6.3:神经元 1、2 和 3 构成了这个示例网络的隐藏层

](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_06_03.jpg)

图 6.3:神经元 1、2 和 3 构成了这个示例网络的隐藏层

在连接输入和输出时,我们可能会有多个隐藏层。具有多个层的神经网络称为深度神经网络

“深度学习”一词来自于多个层次的存在。在创建人工神经网络ANN)时,我们可以指定隐藏层的数量。

偏置

之前,我们看到神经元的方程如下:

y = f(x1*w1 + x2*w2 + x3*w3 + x4*w4)

这个方程的问题在于,缺少一个依赖于输入 x1x2x3x4 的常数因子。前面的方程可以表示任何经过原点的线性函数:如果所有 w 值都等于 0,那么 y 也会等于 0。但对于那些不经过原点的其他函数呢?例如,假设我们正在预测某员工的流失概率,按其在职月份来算。即使他们还没有工作满一个月,流失概率也不可能为零。

为了适应这种情况,我们需要引入一个新参数 b,它可以等于 0.5,因此新雇员在第一个月的流失概率将是 50%。

因此,我们在方程中加入偏置:

y = f(x1*w1 + x2*w2 + x3*w3 + x4*w4 + b)
y = f(x  w + b)

第一个方程是详细形式,描述了每个坐标、权重系数和偏置的作用。第二个方程是向量形式,其中 x = (x1, x2, x3, x4)w = (w1, w2, w3, w4)。向量之间的点运算符表示这两个向量的点积或标量积。这两个方程是等效的。我们在实践中将使用第二种形式,因为使用 TensorFlow 定义变量向量比逐一定义每个变量更为简便。

类似地,对于 w1w2w3w4,偏置 b 是一个变量,意味着它的值在学习过程中可以发生变化。

通过在每个神经元中内置这个常数因子,神经网络模型变得更加灵活,能够更好地拟合特定的训练数据集。

注意

由于某些负权重的存在,可能会发生产品p = x1*w1 + x2*w2 + x3*w3 + x4*w4为负的情况。我们可能仍然希望给予模型一定的灵活性,使其在值大于某个负数时能够激活(或触发)神经元。因此,添加一个常数偏置,例如b = 5,可以确保神经元在-50之间的值也能触发。

TensorFlow 提供了Dense()类来建模神经网络的隐藏层(也叫做全连接层):

from tensorflow.keras import layers
layer1 = layers.Dense(units=128, input_shape=[200])

在这个示例中,我们创建了一个包含128个神经元的全连接层,该层的输入为形状为200的张量。

注意

你可以在www.tensorflow.org/api_docs/python/tf/keras/layers/Dense找到有关此 TensorFlow 类的更多信息。

Dense()类要求输入是一个展平的数组(只有一行)。例如,如果输入的形状是28×28,你需要先使用Flatten()类将其展平,才能得到一个包含 784 个神经元(28 * 28)的单行数据:

from tensorflow.keras import layers
input_layer = layers.Flatten(input_shape=(28, 28))
layer1 = layers.Dense(units=128)

注意

你可以在www.tensorflow.org/api_docs/python/tf/keras/layers/Flatten找到有关此 TensorFlow 类的更多信息。

在接下来的部分中,我们将学习如何使用额外的参数扩展这一神经元层。

人工神经网络(ANNs)的应用场景

人工神经网络(ANNs)在监督学习技术中占有一席之地。它们可以处理分类和回归问题。分类器神经网络试图找到特征与标签之间的关系。特征是输入变量,而分类器可以选择作为返回值的每个类别是一个独立的输出。在回归的情况下,输入变量是特征,而只有一个输出:预测值。尽管传统的分类和回归技术在人工智能中有其应用场景,但人工神经网络通常更擅长发现输入与输出之间复杂的关系。

在下一节中,我们将探讨激活函数及其不同类型。

激活函数

如前所述,一个神经元需要通过应用激活函数来执行变换。神经网络中可以使用不同的激活函数。如果没有这些函数,神经网络将仅仅是一个线性模型,可以通过矩阵乘法轻松描述。

神经网络的激活函数提供了非线性,因此能够建模更复杂的模式。两种非常常见的激活函数是sigmoidtanh(双曲正切函数)。

Sigmoid

sigmoid的公式如下:

图 6.4:Sigmoid 公式

图 6.4:Sigmoid 公式

sigmoid函数的输出值范围是01。这种激活函数通常用于神经网络的最后一层,用于二分类问题。

Tanh

双曲正切的公式如下:

图 6.5:tanh 公式

图 6.5:tanh 公式

tanh 激活函数与 sigmoid 函数非常相似,并且直到最近都非常流行。它通常用于神经网络的隐藏层。它的值范围在 -11 之间。

ReLU

另一个重要的激活函数是 reluReLU 代表 Rectified Linear Unit,目前是最广泛使用的隐藏层激活函数。其公式如下:

图 6.6:ReLU 公式

图 6.6:ReLU 公式

目前有不同版本的 relu 函数,例如 leaky ReLUPReLU

Softmax

该函数将列表中的值缩放到 softmax 函数如下所示的范围:

图 6.7:softmax 公式

图 6.7:softmax 公式

softmax 函数通常作为神经网络的最后一层,用于多类分类问题,因为它能够为每个不同的输出类别生成概率。

请记住,在 TensorFlow 中,我们可以通过激活函数扩展 Dense() 层;只需要设置 activation 参数即可。在以下示例中,我们将添加 relu 激活函数:

from tensorflow.keras import layers
layer1 = layers.Dense(units=128, input_shape=[200], \
                      activation='relu')

让我们使用这些不同的激活函数,观察它们如何通过解决以下练习来抑制加权输入。

练习 6.02:激活函数

在本次练习中,我们将使用 numpy 包实现以下激活函数:sigmoidtanhrelusoftmax

以下步骤将帮助你完成练习:

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 导入 numpy 包并命名为 np

    import numpy as np
    
  3. 创建一个 sigmoid 函数,如下所示的代码片段,使用 np.exp() 方法实现 sigmoid 公式(如前所述):

    def sigmoid(x): 
        return 1 / (1 + np.exp(-x))
    
  4. 计算 sigmoid 函数在值 -1 上的结果:

    sigmoid(-1)
    

    预期输出如下:

    0.2689414213699951
    

    这是对值 -1 执行 sigmoid 变换后的结果。

  5. 导入 matplotlib.pyplot 包并命名为 plt

    import matplotlib.pyplot as plt
    
  6. 创建一个名为 xnumpy 数组,包含从 -1010 的均匀间隔的值,增量为 0.1,使用 np.arange() 方法。打印其值:

    x = np.arange(-10, 10, 0.1)
    x
    

    预期输出如下:

    array(-1.00000000e+01, -9.90000000e+00, -9.80000000e+00,
           -9.70000000e+00, -9.60000000e+00, -9.50000000e+00,
           -9.40000000e+00, -9.30000000e+00, -9.20000000e+00,
           -9.10000000e+00, -9.00000000e+00, -8.90000000e+00,
           -8.80000000e+00, -8.70000000e+00, -8.60000000e+00,
           -8.50000000e+00, -8.40000000e+00, -8.30000000e+00,
           -8.20000000e+00, -8.10000000e+00, -8.00000000e+00,
           -7.90000000e+00, -7.80000000e+00, -7.70000000e+00,
           -7.60000000e+00, -7.50000000e+00, -7.40000000e+00,
           -7.30000000e+00, -7.20000000e+00, -7.10000000e+00,
           -7.00000000e+00, -6.90000000e+00,
    

    很好——我们生成了一个包含从 -1010 的值的 numpy 数组。

    注意

    前面的输出已被截断。

  7. 使用 plt.plot()plt.show() 绘制 xsigmoid(x) 的折线图:

    plt.plot(x, sigmoid(x))
    plt.show()
    

    预期输出如下:

    ![图 6.8:使用 sigmoid 函数的折线图 图 6.8:使用 sigmoid 函数的折线图 我们可以看到,sigmoid 函数的输出范围在 01 之间。对于接近 0 的值,斜率非常陡峭。1. 创建一个 tanh() 函数,使用 np.exp() 方法实现 Tanh 公式(如前所述): py def tanh(x):     return 2 / (1 + np.exp(-2*x)) - 1 1. 使用plt.plot()plt.show()绘制xtanh(x)的折线图: py plt.plot(x, tanh(x)) plt.show() 预期输出是这样的: 图 6.9:使用 tanh 函数的折线图

    图 6.9:使用 tanh 函数的折线图

    tanh函数的形状与sigmoid非常相似,但在接近0的值时,它的斜率更陡。记住,它的值域介于-11之间。

  8. 创建一个relu函数,使用np.maximum()方法实现 ReLU 公式(如上一节所示):

    def relu(x):
        return np.maximum(0, x)
    
  9. 使用plt.plot()plt.show()绘制xrelu(x)的折线图:

    plt.plot(x, relu(x))
    plt.show()
    

    预期输出是这样的:

    图 6.10:使用 relu 函数的折线图

    图 6.10:使用 relu 函数的折线图

    当值为负数时,ReLU 函数等于0,当值为正数时,ReLU 函数等于恒等函数f(x)=x

  10. 创建一个softmax函数,使用np.exp()方法实现 softmax 公式(如上一节所示):

    def softmax(list): 
        return np.exp(list) / np.sum(np.exp(list))
    
  11. 计算列表[0, 1, 168, 8, 2]softmax的输出:

    result = softmax( [0, 1, 168, 8, 2]) 
    result
    

    预期输出是这样的:

    array([1.09276566e-73, 2.97044505e-73, 1.00000000e+00, 
           3.25748853e-70, 8.07450679e-73])
    

如预期的那样,第三个位置的项具有最高的 softmax 概率,因为它的原始值是最高的。

注意

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

你也可以在网上运行这个示例,网址是 packt.live/3188pZi。你必须执行整个 Notebook,才能获得预期的结果。

完成此练习后,我们实现了神经网络中一些最重要的激活函数。

前向传播和损失函数

到目前为止,我们已经看到一个神经元如何接收输入并对其进行一些数学运算以得到输出。我们了解到,神经网络是由多个神经元层组合而成的。

将神经网络的输入转化为结果的过程称为前向传播(或前向传递)。我们要求神经网络做的事情是,通过将多个神经元应用于输入数据,来进行预测(神经网络的最终输出):

图 6.11:展示前向传播的图示

图 6.11:展示前向传播的图示

神经网络依赖于每个神经元的权重矩阵、偏置和激活函数来计算预测的输出值,b。目前,我们假设权重矩阵和偏置的值已预设。激活函数在设计神经网络架构时定义。

对于任何监督式机器学习算法,目标都是做出准确的预测。这意味着我们需要评估预测与真实值之间的准确度。对于传统的机器学习算法,我们使用诸如均方误差、准确率或 F1 分数等评分指标。这同样适用于神经网络,但唯一的区别是,这些分数有两种不同的使用方式:

  • 数据科学家使用它们来评估模型在训练集和测试集上的表现,然后根据需要调整超参数。这同样适用于神经网络,所以这里没有什么新鲜的东西。

  • 神经网络利用它们自动从错误中学习,并更新权重矩阵和偏差。这个过程将在下一节详细解释,那个部分讲的是反向传播。因此,神经网络将使用一个度量标准(也叫做损失函数)来比较预测值与真实标签(y),并学习如何自动进行更好的预测。

损失函数对神经网络学习做出良好预测至关重要。这是一个超参数,需要数据科学家在设计神经网络架构时定义。选择使用哪种损失函数是完全任意的,取决于你想解决的数据集或问题,你会选择一种或另一种。幸运的是,有一些基本的经验法则在大多数情况下都有效:

  • 如果你正在处理回归问题,可以使用均方误差。

  • 如果是二分类问题,损失函数应该使用二元交叉熵。

  • 如果是多类分类问题,那么你应该选择类别交叉熵作为损失函数。

最后需要注意的是,损失函数的选择也将决定你在神经网络最后一层使用哪种激活函数。每个损失函数都需要特定类型的数据来正确评估预测性能。

以下是根据损失函数和项目/问题类型列出的激活函数:

图 6.12:不同激活函数及其应用概述

图 6.12:不同激活函数及其应用概述

使用 TensorFlow 时,为了构建自定义架构,你可以实例化 Sequential() 类,并按如下代码片段添加完全连接的神经元层:

import tensorflow as tf
from tensorflow.keras import layers
model = tf.keras.Sequential()
input_layer = layers.Flatten(input_shape=(28,28))
layer1 = layers.Dense(128, activation='relu')
model.add(input_layer)
model.add(layer1)

现在是时候看看神经网络是如何通过反向传播改进预测的。

反向传播

之前,我们学习了神经网络如何通过使用其神经元的权重矩阵和偏置(我们可以将它们合并成一个单一的矩阵)来进行预测。通过使用损失函数,网络可以确定预测结果的好坏。如果它能利用这些信息并相应地更新参数,那就太好了。这正是反向传播的目的:优化神经网络的参数。

训练神经网络涉及多次执行前向传播和反向传播,以便进行预测并根据误差更新参数。在第一次传播中,我们首先初始化神经网络的所有权重。然后,进行前向传播,接着进行反向传播,后者会更新权重。

我们多次应用这个过程,神经网络会逐步优化其参数。你可以通过设置神经网络遍历整个数据集的最大次数(也叫做 epoch)来决定是否停止这个学习过程,或者如果神经网络在几个 epoch 后得分不再提升,则定义一个提前停止的阈值。

优化器和学习率

在前一节中,我们看到神经网络遵循一个迭代过程,以找到任何输入数据集的最佳解决方案。它的学习过程是一个优化过程。你可以使用不同的优化算法(也叫做AdamSGDRMSprop)。

神经网络优化器的一个重要参数是学习率。这个值定义了神经网络更新权重的速度。设置过低的学习率会减慢学习过程,神经网络需要很长时间才能找到合适的参数。另一方面,学习率过高会导致神经网络无法学习到合适的解决方案,因为它在每次更新时都会做出比需要更大的权重变化。一个好的做法是从一个不是很小的学习率开始(例如0.010.001),然后当神经网络的得分开始平稳或变差时停止训练,接着降低学习率(例如降低一个数量级)并继续训练网络。

使用 TensorFlow,你可以从tf.keras.optimizers实例化一个优化器。例如,下面的代码片段展示了如何创建一个学习率为0.001Adam优化器,然后通过指定损失函数('sparse_categorical_crossentropy')和要显示的指标('accuracy')来编译我们的神经网络:

import tensorflow as tf
optimizer = tf.keras.optimizers.Adam(0.001)
model.compile(loss='sparse_categorical_crossentropy', \
              optimizer=optimizer, metrics=['accuracy'])

一旦模型编译完成,我们就可以像这样使用.fit()方法训练神经网络:

model.fit(features_train, label_train, epochs=5)

在这里,我们对训练集进行了5个 epoch 的神经网络训练。训练完成后,我们可以使用该模型在测试集上进行评估,并通过.evaluate()方法评估其性能:

model.evaluate(features_test, label_test)

注意

你可以在www.tensorflow.org/api_docs/python/tf/keras/optimizers上找到更多关于 TensorFlow 优化器的信息。

在下一个练习中,我们将基于数据集训练一个神经网络。

练习 6.03:分类信用批准

在本练习中,我们将使用德国信用批准数据集,并训练一个神经网络来分类个人是否具备信用。

注意

数据集文件也可以在我们的 GitHub 仓库中找到:

packt.live/2V7uiV5

以下步骤将帮助你完成练习:

  1. 打开一个新的 Jupyter Notebook 文件。

  2. numpy中导入loadtxt方法:

    from numpy import loadtxt
    
  3. 创建一个名为file_url的变量,包含指向原始数据集的链接:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/'\
               'The-Applied-Artificial-Intelligence-Workshop'\
               '/master/Datasets/german_scaled.csv'
    
  4. 使用loadtxt()加载数据到一个名为data的变量中,并指定delimiter=','参数。打印其内容:

    data = loadtxt(file_url, delimiter=',')
    data
    

    期望的输出是这样的:

    array([[0\.        , 0.33333333, 0.02941176, ..., 0\.      , 1\.      ,
            1\.        ],
           [1\.        , 0\.        , 0.64705882, ..., 0\.      , 0\.      ,
            1\.        ],
           [0\.        , 1\.        , 0.11764706, ..., 1\.      , 0\.      ,
            1\.        ],
           ...,
           [0\.        , 1\.        , 0.11764706, ..., 0\.      , 0\.      ,
            1\.        ],
           [1\.        , 0.33333333, 0.60294118, ..., 0\.      , 1\.      ,
            1\.        ],
           [0\.        , 0\.        , 0.60294118, ..., 0\.      , 0\.      ,
            1\.        ]])
    
  5. 创建一个名为label的变量,包含来自第一列的数据(这将是我们的响应变量):

    label = data[:, 0]
    
  6. 创建一个名为features的变量,包含除第一列外的所有数据(第一列对应于响应变量):

    features = data[:, 1:]
    
  7. sklearn.model_selection中导入train_test_split方法:

    from sklearn.model_selection import train_test_split
    
  8. 将数据拆分为训练集和测试集,并将结果保存到四个变量中,分别为features_trainfeatures_testlabel_trainlabel_test。使用 20%的数据作为测试集,并指定random_state=7

    features_train, features_test, \
    label_train, label_test = train_test_split(features, \
                                               label, \
                                               test_size=0.2, \
                                               random_state=7)
    
  9. 导入numpynptensorflowtf,并从tensorflow.keras中导入layers

    import numpy as np
    import tensorflow as tf
    from tensorflow.keras import layers
    
  10. 使用np.random_seed()tf.random.set_seed()分别将1设置为numpytensorflow的种子:

    np.random.seed(1)
    tf.random.set_seed(1)
    
  11. 实例化一个tf.keras.Sequential()类,并将其保存到一个名为model的变量中:

    model = tf.keras.Sequential()
    
  12. 实例化一个layers.Dense()类,包含16个神经元,activation='relu'input_shape=[19],然后将其保存到一个名为layer1的变量中:

    layer1 = layers.Dense(16, activation='relu', \
                          input_shape=[19])
    
  13. 实例化第二个layers.Dense()类,包含1个神经元,activation='sigmoid',然后将其保存到一个名为final_layer的变量中:

    final_layer = layers.Dense(1, activation='sigmoid')
    
  14. 使用.add()将你刚才定义的两个层添加到模型中:

    model.add(layer1)
    model.add(final_layer)
    
  15. 实例化一个tf.keras.optimizers.Adam()类,学习率为0.001,并将其保存到名为optimizer的变量中:

    optimizer = tf.keras.optimizers.Adam(0.001)
    
  16. 使用.compile()编译神经网络,loss='binary_crossentropy'optimizer=optimizermetrics=['accuracy'],如下代码片段所示:

    model.compile(loss='binary_crossentropy', \
                  optimizer=optimizer, metrics=['accuracy'])
    
  17. 使用.summary()打印模型的摘要:

    model.summary()
    

    期望的输出是这样的:

    图 6.13:顺序模型的摘要

    ](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_06_13.jpg)

    图 6.13:顺序模型的摘要

    这个输出总结了我们神经网络的架构。我们可以看到它由三层组成,符合预期,并且知道每一层的输出大小和参数数量,这些参数对应于权重和偏置。例如,第一层有16个神经元和320个需要学习的参数(权重和偏置)。

  18. 接下来,使用训练集拟合神经网络并指定epochs=10

    model.fit(features_train, label_train, epochs=10)
    

    期望的输出是这样的:

    图 6.14:使用训练集拟合神经网络

图 6.14:使用训练集拟合神经网络

输出提供了大量关于神经网络训练的信息。第一行告诉我们训练集由800个观测值组成。然后我们可以看到每一轮训练的结果:

总处理时间(秒)

每个数据样本的处理时间(微秒/样本)

损失值和准确度得分

这个神经网络的最终结果是最后一轮训练(epoch=10),我们在此达到了0.6888的准确度得分。但我们可以看到趋势在持续改善:每一轮训练后准确度得分都在增加。因此,如果我们通过增加训练轮数或降低学习率来延长训练时间,可能会得到更好的结果。

注意

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

你也可以在 packt.live/2Njghza 在线运行这个示例。你必须执行整个 Notebook,才能得到预期的结果。

完成这个练习后,你就训练了你的第一个分类器。在传统的机器学习算法中,你需要编写更多的代码才能实现这一点,因为你需要定义神经网络的整个架构。在这里,神经网络在10轮训练后达到了0.6888,但如果让它训练得更长一些,性能仍然有提升空间。你可以尝试自己做一下。

接下来,我们将探讨正则化。

正则化

与任何机器学习算法一样,神经网络在学习仅对训练集相关的模式时,可能会面临过拟合的问题。在这种情况下,模型无法对未见过的数据进行泛化。

幸运的是,有多种技术可以帮助减少过拟合的风险:

  • L1 正则化,它向损失函数中添加了一个惩罚参数(权重的绝对值)

  • L2 正则化,它向损失函数中添加了一个惩罚参数(权重的平方值)

  • 早停法,它会在验证集的误差增加而训练集的误差减少时停止训练

  • Dropout,它会在训练过程中随机移除一些神经元

所有这些技术都可以添加到我们创建的神经网络的每一层。在下一个练习中,我们将会深入探讨这一点。

练习 6.04:使用正则化预测波士顿房价

在这个练习中,你将构建一个神经网络,用来预测波士顿某个郊区的房价中位数,并了解如何为网络添加正则化方法。

注意

数据集文件也可以在我们的 GitHub 仓库中找到:packt.live/2V9kRUU

引用:数据最初由Harrison, D. 和 Rubinfeld, D.L. 'Hedonic prices and the demand for clean air', J. Environ. Economics & Management, vol.5, 81-102, 1978发布。

数据集包含12个不同的特征,提供有关郊区的信息和一个目标变量(MEDV)。目标变量是数字型,表示以$1,000 为单位的自有住房的中位数价值。

以下步骤将帮助你完成练习:

  1. 打开一个新的 Jupyter Notebook 文件。

  2. pandas包导入为pd

    import pandas as pd
    
  3. 创建一个包含原始数据集链接的file_url变量:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/'\
               'The-Applied-Artificial-Intelligence-Workshop'\
               '/master/Datasets/boston_house_price.csv'
    
  4. 使用pd.read_csv()将数据集加载到一个名为df的变量中:

    df = pd.read_csv(file_url)
    
  5. 使用.head()显示前五行:

    df.head()
    

    预期输出是这样的:

    图 6.15:输出显示数据集的前五行

    图 6.15:输出显示数据集的前五行

  6. 使用.pop()提取目标变量,并将其保存到一个名为label的变量中:

    label = df.pop('MEDV')
    
  7. sklearn.preprocessing导入scale函数:

    from sklearn.preprocessing import scale
    
  8. 对 DataFrame df进行缩放,并将结果保存到一个名为scaled_features的变量中。打印其内容:

    scaled_features = scale(df)
    scaled_features
    

    预期输出是这样的:

    array([[-0.41978194,  0.28482986, -1.2879095 , ..., -0.66660821,
            -1.45900038, -1.0755623 ],
           [-0.41733926, -0.48772236, -0.59338101, ..., -0.98732948,
            -0.30309415, -0.49243937],
           [-0.41734159, -0.48772236, -0.59338101, ..., -0.98732948,
            -0.30309415, -1.2087274 ],
           ...,
           [-0.41344658, -0.48772236,  0.11573841, ..., -0.80321172,
             1.17646583, -0.98304761],
           [-0.40776407, -0.48772236,  0.11573841, ..., -0.80321172,
             1.17646583, -0.86530163],
           [-0.41500016, -0.48772236,  0.11573841, ..., -0.80321172,
    

    从输出中可以看到,我们的所有特征现在已经标准化。

  9. sklearn.model_selection导入train_test_split

    from sklearn.model_selection import train_test_split
    
  10. 将数据拆分为训练集和测试集,并将结果保存到四个变量中,分别为features_trainfeatures_testlabel_trainlabel_test。使用 10%的数据作为测试集,并指定random_state=8

    features_train, features_test, \
    label_train, label_test = train_test_split(scaled_features, \
                                               label, \
                                               test_size=0.1, \
                                               random_state=8)
    
  11. numpy导入为nptensorflow导入为tf,并从tensorflow.keras导入layers

    import numpy as np
    import tensorflow as tf
    from tensorflow.keras import layers
    
  12. 使用np.random_seed()tf.random.set_seed()设置 NumPy 和 TensorFlow 的种子为8

    np.random.seed(8)
    tf.random.set_seed(8)
    
  13. 实例化一个tf.keras.Sequential()类,并将其保存到一个名为model的变量中:

    model = tf.keras.Sequential()
    
  14. 接下来,使用tf.keras.regularizers.l1_l2创建一个结合了l1l2正则化器,l1=0.01l2=0.01。将其保存到一个名为regularizer的变量中:

    regularizer = tf.keras.regularizers.l1_l2(l1=0.1, l2=0.01)
    
  15. 实例化一个layers.Dense()类,使用10个神经元,activation='relu'input_shape=[12],以及kernel_regularizer=regularizer,并将其保存到一个名为layer1的变量中:

    layer1 = layers.Dense(10, activation='relu', \
             input_shape=[12], kernel_regularizer=regularizer)
    
  16. 实例化第二个layers.Dense()类,使用1个神经元,并将其保存到一个名为final_layer的变量中:

    final_layer = layers.Dense(1)
    
  17. 使用.add()将刚定义的两个层添加到模型中,并在它们之间添加一个layers.Dropout(0.25)层:

    model.add(layer1)
    model.add(layers.Dropout(0.25))
    model.add(final_layer)
    

    我们在每个全连接层之间添加了一个 dropout 层,它会随机移除 25%的神经元。

  18. 实例化一个tf.keras.optimizers.SGD()类,学习率为0.001,并将其保存到一个名为optimizer的变量中:

    optimizer = tf.keras.optimizers.SGD(0.001)
    
  19. 使用.compile()编译神经网络,配置loss='mse'optimizer=optimizermetrics=['mse']

    model.compile(loss='mse', optimizer=optimizer, \
                  metrics=['mse'])
    
  20. 使用.summary()打印模型的摘要:

    model.summary()
    

    预期输出是这样的:

    图 6.16:模型摘要

    图 6.16:模型摘要

    此输出总结了我们神经网络的架构。我们可以看到它由三层组成,其中有两层密集层和一层丢弃层。

  21. 实例化一个tf.keras.callbacks.EarlyStopping()类,设置monitor='val_loss'patience=2作为学习率,并将其保存到名为callback的变量中:

    callback = tf.keras.callbacks.EarlyStopping(monitor='val_loss', \
                                                patience=2)
    

    我们刚刚定义了一个回调,声明神经网络将在验证损失(monitor='val_loss')在2个 epochs(patience=2)内未改善时停止训练。

  22. 使用训练集拟合神经网络,并指定epochs=50validation_split=0.2callbacks=[callback]verbose=2

    model.fit(features_train, label_train, \
              epochs=50, validation_split = 0.2, \
              callbacks=[callback], verbose=2)
    

    预期输出如下:

    图 6.17:使用训练集拟合神经网络

图 6.17:使用训练集拟合神经网络

在输出中,我们看到神经网络在第 22 个 epoch 后停止了训练。它在最大 epochs 数50之前停止了训练。这是因为我们之前设置的回调:如果验证损失在两个 epoch 后没有改善,训练应该停止。

注意

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

你也可以在packt.live/37SVSu6在线运行此示例。你必须执行整个 Notebook 才能获得预期的结果。

你刚刚应用了多种正则化技术,并训练了一个神经网络来预测波士顿郊区的住房中位数值。

活动 6.01:为数字数据集找到最佳准确度得分

在本活动中,你将训练和评估一个神经网络,该网络将识别由 MNIST 数据集提供的手写数字图像。你将专注于实现最佳的准确度得分。

注意

你可以在 TensorFlow 官网上阅读更多关于该数据集的信息:www.tensorflow.org/datasets/catalog/mnist

引用:该数据集最初由Yann Lecun分享。

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

  1. 导入 MNIST 数据集。

  2. 通过将数据除以255来标准化数据。

  3. 创建一个具有以下层的神经网络架构:

    一个扁平化的输入层,使用layers.Flatten(input_shape=(28,28))

    一个完全连接的层,使用layers.Dense(128, activation='relu')

    一个丢弃层,使用layers.Dropout(0.25)

    一个完全连接的层,使用layers.Dense(10, activation='softmax')

  4. 指定一个学习率为0.001Adam优化器。

  5. 在验证损失上定义一个早停机制,patience=5

  6. 训练模型。

  7. 评估模型并找到准确度得分。

预期输出如下:

图 6.18:预期的准确度得分

图 6.18:预期的准确度得分

注意

该活动的解决方案可以在第 378 页找到

在下一部分,我们将深入探讨深度学习的主题。

深度学习

现在我们已经能够构建和训练一个具有一个隐藏层的神经网络,我们可以研究更复杂的深度学习架构。

深度学习只是传统神经网络的扩展,但具有更深和更复杂的架构。深度学习可以建模非常复杂的模式,应用于检测图像中的物体、将文本翻译成不同语言等任务。

浅层与深层网络

现在我们已经能够构建和训练一个具有一个隐藏层的神经网络,我们可以研究更复杂的深度学习架构。

如前所述,我们可以在神经网络中添加更多的隐藏层。这将增加需要学习的参数数量,但有可能帮助建模更复杂的模式。这就是深度学习的核心:增加神经网络的深度,以解决更复杂的问题。

例如,我们可以在前向传播和损失函数部分中呈现的神经网络中添加第二层:

图 6.19:显示神经网络中两个隐藏层的图

图 6.19:显示神经网络中两个隐藏层的图

理论上,我们可以添加无限多个隐藏层。但深层网络有一个缺点。增加深度还会增加需要优化的参数数量。因此,神经网络需要训练更长时间。所以,作为良好的实践,最好从更简单的架构开始,然后逐步增加其深度。

计算机视觉与图像分类

深度学习在计算机视觉和自然语言处理方面取得了惊人的成果。计算机视觉是一个涉及分析数字图像的领域。数字图像是由像素组成的矩阵。每个像素的值在0255之间,这个值表示像素的强度。一张图像可以是黑白的,并且只有一个通道。但它也可以是彩色的,在这种情况下,它将有三个通道,分别代表红色、绿色和蓝色。这种数字版本的图像可以输入到深度学习模型中。

计算机视觉有多种应用,例如图像分类(识别图像中的主要物体)、物体检测(定位图像中的不同物体)和图像分割(寻找图像中物体的边缘)。本书将只关注图像分类。

在下一节中,我们将讨论一种特定类型的架构:CNNs。

卷积神经网络(CNNs)

卷积神经网络(CNNs)是针对图像相关模式识别优化的人工神经网络。CNNs 基于卷积层,而不是全连接层。

卷积层用于通过滤波器检测图像中的模式。滤波器只是一个矩阵,通过卷积操作应用到输入图像的某个部分,输出将是另一张图像(也称为特征图),其中突出显示了滤波器找到的模式。例如,一个简单的滤波器可以识别花朵上的垂直线条,如下图所示:

图 6.20:卷积在图像中检测模式

图 6.20:卷积在图像中检测模式

这些滤波器不是事先设定好的,而是通过卷积神经网络(CNN)自动学习的。训练结束后,CNN 可以识别图像中的不同形状。这些形状可以出现在图像的任何地方,卷积操作符无论图像的精确位置和方向如何,都能识别类似的信息。

卷积操作

卷积是一种特定类型的矩阵运算。对于输入图像,大小为n*n的滤波器将遍历图像的特定区域,执行逐元素相乘和求和,并返回计算结果:

图 6.21:卷积操作

图 6.21:卷积操作

在前面的例子中,我们将滤波器应用到图像的左上部分。然后,我们执行逐元素相乘,将输入图像中的一个元素与滤波器上对应的值相乘。在这个例子中,我们计算了以下内容:

  • 第 1 行,第 1 列:5 * 2 = 10

  • 第 1 行,第 2 列:10 * 0 = 0

  • 第 1 行,第 3 列:15 * (-1) = -15

  • 第 2 行,第 1 列:10 * 2 = 20

  • 第 2 行,第 2 列:20 * 0 = 0

  • 第 2 行,第 3 列:30 * (-1) = -30

  • 第 3 行,第 1 列:100 * 2 = 200

  • 第 3 行,第 2 列:150 * 0 = 0

  • 第 3 行,第 3 列:200 * (-1) = -200

最后,我们对这些值进行求和:10 + 0 -15 + 20 + 0 - 30 + 200 + 0 - 200 = -15

然后,我们将通过将滤波器向右滑动一个列来执行相同的操作。我们会一直滑动滤波器,直到覆盖整个图像:

图 6.22:在不同的行和列上进行卷积操作

图 6.22:在不同的行和列上进行卷积操作

我们不仅可以按列滑动,也可以按两列、三列或更多列滑动。定义滑动操作长度的参数称为步幅

你可能已经注意到,卷积操作的结果是一个尺寸比输入图像小的图像(或特征图)。如果你想保持相同的尺寸,可以在输入图像的边界周围添加额外的行和列,值为 0。这种操作称为填充

这就是卷积操作背后的原理。卷积层就是应用这种操作并使用多个滤波器。

我们可以在 TensorFlow 中用以下代码片段声明一个卷积层:

from tensorflow.keras import layers
layers.Conv2D(32, kernel_size=(3, 3), strides=(1,1), \
              padding="valid", activation="relu")

在上面的示例中,我们已经实例化了一个具有 32 个滤波器的卷积层(也叫做 (3, 3) 卷积,步幅为 1(每次滑动窗口按 1 列或 1 行移动),并且没有填充(padding="valid")。

注意

您可以在 TensorFlow 的官方网站上阅读更多关于 Conv2D 类的信息,网址为 www.tensorflow.org/api_docs/python/tf/keras/layers/Conv2D

在 TensorFlow 中,卷积层期望输入为具有以下格式的张量:(行数高度宽度通道数)。根据数据集的不同,您可能需要重塑图像以符合此要求。TensorFlow 提供了一个函数来执行此操作,如下所示:

features_train.reshape(60000, 28, 28, 1)

池化层

CNN 架构中另一个常见的层是池化层。我们之前已经看到,如果没有添加填充,卷积层会减小图像的大小。这种行为是预期的吗?为什么我们不保持输入图像的完全相同的大小?通常,在 CNN 中,我们倾向于随着不同层的推进,逐渐减少特征图的大小。这样做的主要原因是,我们希望在网络的末端有更多和更多特定的模式检测器。

在网络的前端,CNN 更倾向于具有更通用的滤波器,例如垂直或水平线检测器;但随着网络的加深,例如,如果我们训练一个 CNN 来区分猫和狗,它可能会有可以检测狗尾巴或猫胡须的滤波器;或者如果我们分类水果图像,滤波器可以检测物体的纹理。此外,较小的特征图能降低检测到错误模式的风险。

通过增加步幅,我们可以进一步减小输出特征图的大小。但还有另一种方法可以做到这一点:在卷积层后添加池化层。池化层是一个给定大小的矩阵,它会对特征图的每个区域应用聚合函数。最常见的聚合方法是找到一组像素中的最大值:

图 6.23:池化层的工作原理

图 6.23:池化层的工作原理

在上面的示例中,我们使用了大小为 (2, 2) 且步幅为 2 的最大池化。我们查看特征图的左上角,找到像素 6812 中的最大值,得到结果 8。然后,我们通过步幅 2 滑动最大池化,对像素 6174 执行相同的操作。我们对底部的区域重复相同的操作,得到一个大小为 (2,2) 的新特征图。

在 TensorFlow 中,我们可以使用 MaxPool2D() 类来声明一个最大池化层:

from tensorflow.keras import layers
layers.MaxPool2D(pool_size=(2, 2), strides=2)

注意

您可以在 TensorFlow 的官方网站上阅读更多关于 Conv2D 类的信息,网址为 www.tensorflow.org/api_docs/python/tf/keras/layers/MaxPool2D

CNN 架构

正如你之前看到的,你可以通过指定隐藏层的类型和数量、激活函数等,定义你自己的自定义 CNN 架构。但对于初学者来说,这可能有些令人畏惧。我们如何知道每一层需要添加多少个滤波器,或者适当的步幅是多少呢?我们必须尝试多种组合,看看哪种效果最好。

幸运的是,深度学习领域的许多研究人员已经做了这样的探索性工作,并公开了他们设计的架构。目前,最著名的架构包括:

活动 6.02:使用 CNN 评估时尚图像识别模型

在本活动中,我们将训练一个 CNN 来识别属于 Fashion MNIST 数据集中的 10 个不同类别的服装图像。我们将计算这个 CNN 模型的准确性。

注意

你可以在 TensorFlow 的网站上阅读更多关于这个数据集的信息,网址为www.tensorflow.org/datasets/catalog/fashion_mnist

原始数据集由Han Xiao分享。

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

  1. 导入 Fashion MNIST 数据集。

  2. 重新调整训练集和测试集的形状。

  3. 通过对数据进行255除法标准化。

  4. 创建一个具有以下层的神经网络架构:

    三个卷积层,使用Conv2D(64, (3,3), activation='relu'),后跟MaxPooling2D(2,2)

    一个 Flatten 层

    一个全连接层,使用Dense(128, activation=relu)

    一个全连接层,使用Dense(10, activation='softmax')

  5. 指定一个学习率为0.001Adam优化器。

  6. 训练模型。

  7. 在测试集上评估模型。

预期输出如下:

10000/10000 [==============================] - 1s 108us/sample - loss: 0.2746 - accuracy: 0.8976
[0.27461639745235444, 0.8976]

注意

这个活动的解决方案可以在第 382 页找到。

在接下来的部分中,我们将学习另一种深度学习架构:RNN。

循环神经网络(RNNs)

在上一部分中,我们学习了如何使用 CNN 进行计算机视觉任务,如图像分类。随着深度学习的发展,计算机现在能够实现甚至超越人类的表现。另一个正在吸引研究人员大量关注的领域是自然语言处理,这是一个 RNN 表现突出的领域。

在过去的几年里,我们见证了 RNN 技术的许多不同应用,如语音识别、聊天机器人和文本翻译应用。但 RNN 在预测时间序列模式方面也表现得相当出色,这一点在股票市场预测中得到了广泛应用。

RNN 层

所有先前提到的应用程序的共同点是它们的输入是顺序的。输入中有一个时间组件。例如,句子是单词的序列,单词的顺序很重要;股票市场数据由一系列日期和相应的股价组成。

为了适应这样的输入,我们需要神经网络能够处理输入序列,并能够保持对它们之间关系的理解。一种方法是创建记忆,让网络考虑先前的输入。这正是基本循环神经网络的工作方式:

图 6.24:单个循环神经网络概述

图 6.24:单个循环神经网络概述

在前面的图中,我们可以看到一个神经网络接收称为 Xt 的输入,并进行一些转换,并给出输出结果,a。到目前为止没有新东西。

但您可能已经注意到,还有一个名为 Ht-1 的附加输出,它是一个输出,但也是神经网络的输入。这就是 RNN 模拟记忆的方式 - 通过考虑其先前的结果并将其作为额外输入。因此,结果 b 将取决于输入 xt,但也取决于 Ht-1。现在,我们可以表示四个输入的序列,这些输入被馈送到同一个神经网络中。

图 6.25:循环神经网络概述

图 6.25:循环神经网络概述

我们可以看到神经网络在每个时间步(tt+1、…、t+3)中接收一个输入(x)并生成一个输出(y),同时生成另一个输出(h),该输出将传递到下一次迭代。

注意

前面的图可能有点误导性 - 这里实际上只有一个循环神经网络(中间的所有循环神经网络框形成一个神经网络),但从这种格式更容易看出顺序如何工作。

RNN 单元内部如下所示:

图 6.26:使用 tanh 的 RNN 的内部工作方式

图 6.26:使用 tanh 的 RNN 的内部工作方式

它与简单神经元非常相似,但接受更多的输入,并使用 tanh 作为激活函数。

注意

在 RNN 单元中,您可以使用任何激活函数。在 TensorFlow 中,默认值是 tanh

这是 RNN 的基本逻辑。在 TensorFlow 中,我们可以使用 layers.SimpleRNN 实例化一个 RNN 层:

from tensorflow.keras import layers
layers.SimpleRNN(4, activation='tanh')

在代码片段中,我们创建了一个具有 4 个输出和 tanh 激活函数(这是用于 RNN 的最常用的激活函数)的 RNN 层。

GRU 层

前一类型层的一个缺点是最终输出考虑了所有先前的输出。如果您有 1,000 个输入单元的序列,则最终输出 y 受每一个先前结果的影响。如果此序列由 1,000 个单词组成,而我们试图预测下一个单词,则必须记住所有 1,000 个单词真的太多了。可能只需查看最终输出之前的前 100 个单词。

这正是 门控循环单元GRU)单元的作用。我们来看看它们的内部结构:

图 6.27:使用 tanh 和 sigmoid 的 RNN 内部工作原理

图 6.27:使用 tanh 和 sigmoid 的 RNN 内部工作原理

与简单的 RNN 单元相比,GRU 单元有更多的元素:

  • 第二个激活函数,即 sigmoid

  • 在生成输出之前执行的乘法运算 39 和 Ht

通常使用的 tanh 路径仍然负责进行预测,但这次我们称之为“候选”。sigmoid 路径充当“更新”门。它将告诉 GRU 单元是否需要丢弃此候选值。记住,输出范围在 01 之间。如果接近 0,则更新门(即 sigmoid 路径)会表示我们不应考虑这个候选。

另一方面,如果它更接近 1,则我们肯定应该使用这个候选的结果。

记住,输出 Ht 与 Ht-1 相关,Ht-1 又与 Ht-2 相关,依此类推。因此,更新门还将定义我们应保留多少“记忆”。它倾向于优先考虑与当前输出更接近的先前输出。

这就是 GRU 的基本逻辑(请注意,GRU 单元还有一个额外的组件,即重置门,但为了简化,我们将不讨论它)。在 TensorFlow 中,我们可以通过 layers.GRU 实例化这样的层:

from tensorflow.keras import layers
layers.GRU(4, activation='tanh', \
           recurrent_activation='sigmoid')

在代码片段中,我们创建了一个具有 4 个输出单元的 GRU 层,候选预测使用 tanh 激活函数,更新门使用 sigmoid 激活函数。

LSTM 层

还有另一种非常流行的 RNN 架构单元,叫做 LSTM 单元。LSTM 代表 长短期记忆。LSTM 比 GRU 先出现,但后者更简单,这就是我们首先介绍 GRU 的原因。LSTM 的内部结构如下:

图 6.28:LSTM 概述

图 6.28:LSTM 概述

一开始,这看起来非常复杂。它由几个元素组成:

  • 细胞状态:这是所有先前输出的拼接。它是 LSTM 单元的“记忆”。

  • 遗忘门:它负责定义是否应该保留或忘记给定的记忆。

  • 输入门:它负责定义是否需要更新新的记忆候选。然后,将此新记忆候选加入到先前的记忆中。

  • 输出门:它负责根据先前的输出(Ht-1)、当前输入(xt)和记忆进行预测。

LSTM 单元可以考虑先前的结果,也可以考虑过去的记忆,这就是它如此强大的原因。

在 TensorFlow 中,我们可以通过 layers.SimpleRNN 实例化这样的层:

from tensorflow.keras import layers
layers.LSTM(4, activation='tanh', \
            recurrent_activation='sigmoid')

在代码片段中,我们创建了一个具有 4 个输出单元的 LSTM 层,候选预测使用 tanh 激活函数,更新门使用 sigmoid 激活函数。

注意

你可以在这里阅读更多关于 TensorFlow 中 SimpleRNN 实现的内容:www.tensorflow.org/api_docs/python/tf/keras/layers/SimpleRNN

活动 6.03:评估基于 RNN 的 Yahoo 股票模型

在本活动中,我们将使用 LSTM 训练一个 RNN 模型,以预测基于过去 30 天数据的 Yahoo! 股票价格。我们将寻找最佳的均方误差值,并检查模型是否发生过拟合。我们将使用在第二章《回归简介》中看到的相同的 Yahoo 股票数据集。

注意

数据集文件也可以在我们的 GitHub 仓库中找到:packt.live/3fRI5Hk

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

  1. 导入 Yahoo 股票数据集。

  2. 提取 close price 列。

  3. 对数据集进行标准化。

  4. 创建过去30天的股票价格特征。

  5. 重塑训练集和测试集。

  6. 创建神经网络架构,包含以下层:

    五个 LSTM 层,使用 LSTM(50, (3,3), activation='relu') 后跟 Dropout(0.2)

    一个全连接层,使用 Dense(1)

  7. 指定学习率为 0.001Adam 优化器。

  8. 训练模型。

  9. 在测试集上评估模型。

期望的输出结果是:

1000/1000 [==============================] - 0s 279us/sample - loss: 0.0016 - mse: 0.0016
[0.00158528157370165, 0.0015852816]

注意

该活动的解决方案可以在第 387 页找到。

在接下来的部分,我们将讨论深度学习所需的硬件。

深度学习硬件

如你所见,训练深度学习模型比传统机器学习算法需要更长的时间。这是因为前向传播和反向传播过程中需要进行大量计算。在本书中,我们训练了只有少数几层的简单模型。但也有一些架构有数百层,甚至更多。那种网络的训练可能需要几天,甚至几周。

为了加速训练过程,建议使用一种名为 GPU 的特定硬件。GPU 擅长执行数学运算,因此非常适合深度学习。与中央处理单元CPU)相比,GPU 在训练深度学习模型时速度可以快到 10 倍。你可以亲自购买 GPU 并搭建自己的深度学习计算机,只需要确保所购买的 GPU 支持 CUDA(目前只有 NVIDIA 的 GPU 支持)。

另一种选择是使用像 AWS 或 Google Cloud Platform 这样的云服务提供商,并在云端训练模型。你只需为实际使用的部分付费,且在完成后可以随时关闭它们。好处是,你可以根据项目的需求调整配置的规模,但要注意费用。如果你的实例一直开启,即使没有训练模型,也会产生费用。所以,如果不使用时,记得关闭实例。

最后,谷歌最近发布了一些专门用于深度学习的新硬件:张量处理单元TPUs)。它们比 GPU 快得多,但成本也相当高。目前,只有谷歌云平台在其云实例中提供这种硬件。

挑战与未来趋势

像任何新技术一样,深度学习也面临着挑战。其中之一是巨大的入门门槛。要成为一名深度学习从业者,你曾经需要非常了解深度学习背后的所有数学理论,并且是一名熟练的程序员。此外,你还需要学习你选择使用的深度学习框架的具体知识(无论是 TensorFlow、PyTorch、Caffe,还是其他任何框架)。一段时间以来,深度学习无法接触到广泛的受众,主要局限于研究人员。但这种情况已经发生了变化,尽管仍不完美。例如,TensorFlow 现在有了一个更高级的 API,叫做 Keras(这就是你在本章看到的),它比核心 API 更容易使用。希望这种趋势会继续下去,让深度学习框架对任何有兴趣的人都更加易于接触。

第二个挑战是,深度学习模型需要大量的计算能力,正如前面一节所提到的。这再次成为任何想要尝试的人面临的主要障碍。尽管 GPU 的成本已经下降,深度学习仍然需要一些前期投资。幸运的是,现在我们有了一个免费的选择来使用 GPU 训练深度学习模型:Google Colab。这是谷歌推出的一项倡议,旨在通过提供免费的临时云计算来促进研究。你需要的唯一条件是一个 Google 账号。注册后,你可以创建笔记本(类似于 Jupyter 笔记本),并选择一个内核,在 CPU、GPU(每天限制 10 小时)甚至 TPU(每天限制½小时)上运行。因此,在投资购买或租赁 GPU 之前,你可以先在 Google Colab 上进行练习。

注意

你可以在colab.research.google.com/上找到更多关于 Google Colab 的信息。

更先进的深度学习模型可能非常深,训练可能需要数周。因此,基础的从业者很难使用这种架构。但幸运的是,许多研究人员已经接受了开源运动,并不仅分享了他们设计的架构,还分享了网络的权重。这意味着你现在可以访问最先进的预训练模型,并对其进行微调,以适应你自己的项目。这被称为迁移学习(此书不涉及)。它在计算机视觉领域非常流行,你可以在 ImageNet 或 MS-Coco 上找到预训练模型,例如,这些是包含大量图片的数据集。迁移学习也在自然语言处理领域发生,但其发展程度不如计算机视觉领域。

注意

你可以在www.image-net.org/cocodataset.org/找到有关这些数据集的更多信息。

另一个与深度学习相关的重要话题是对模型结果进行解释的需求日益增加。很快,这些算法可能会受到监管,深度学习从业者将必须能够解释模型为何做出某个特定决策。目前,由于网络的复杂性,深度学习模型更像是黑盒。研究人员已经提出了一些倡议,寻找解释和理解深度神经网络的方法,例如Zeiler 和 Fergus的《Visualizing and Understanding Convolutional Networks》,ECCV 2014。然而,随着这些技术在我们日常生活中的民主化,仍然需要在该领域做更多的工作。例如,我们需要确保这些算法不会存在偏见,并且不会做出影响特定群体的、不公平的决策。

总结

我们刚刚完成了整本《应用人工智能工作坊(第二版)》。在这个工作坊中,我们学习了 AI 的基本原理及其应用。我们写了一个 Python 程序来玩井字游戏。我们学习了广度优先搜索和深度优先搜索等搜索技术,并了解了它们如何帮助我们解决井字游戏。

在接下来的几章中,我们学习了使用回归和分类的监督学习。这些章节包括数据预处理、训练-测试拆分以及在多个实际场景中使用的模型。线性回归、多项式回归和支持向量机在预测股票数据时都发挥了重要作用。分类是通过 k-最近邻和支持向量分类器进行的。几个活动帮助你将分类的基础应用于一个有趣的实际用例:信用评分。

第四章决策树简介中,你学习了决策树、随机森林和极端随机树。本章介绍了评估模型效用的不同方法。我们学习了如何计算模型的准确率、精确度、召回率和 F1 分数。我们还学会了如何创建模型的混淆矩阵。本章的模型通过对汽车数据的评估得以实践。

第五章人工智能:聚类中介绍了无监督学习,以及 k-均值和层次聚类算法。一个有趣的方面是,这些算法在聚类过程中没有预先给定标签,而是通过聚类过程来检测标签。

本次研讨会以第六章神经网络与深度学习为结束,其中介绍了如何使用 TensorFlow 进行神经网络和深度学习。我们运用了这些技术,在实际应用中取得了最佳准确度,例如手写数字检测、图像分类和时间序列预测。

附录

1. 人工智能简介

活动 1.01:生成井字棋游戏中的所有可能步骤序列

解决方案

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

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 重用先前步骤步骤 2–9中的函数代码,练习 1.02为井字棋游戏创建具有随机行为的 AI

  3. 创建一个函数,将all_moves_from_board_list函数映射到棋盘列表的每个元素。这样,我们将在每个深度获得决策树的所有节点:

    def all_moves_from_board_list(board_list, sign):
        move_list = []
        for board in board_list:
            move_list.extend(all_moves_from_board(board, sign))
        return move_list
    

    在前面的代码片段中,我们定义了all_moves_from_board函数,它将列举所有棋盘上的可能移动,并将这些动作添加到一个名为move_list的列表中。

  4. 创建一个名为 board 的变量,包含EMPTY_SIGN * 9的决策树,并使用all_moves_from_board_list函数对该棋盘和AI_SIGN进行调用。将其输出保存到一个名为all_moves的变量中,并打印其内容:

    board = EMPTY_SIGN * 9
    all_moves = all_moves_from_board(board, AI_SIGN )
    all_moves
    

    预期输出如下:

    ['X........',
     '.X.......',
     '..X......',
     '...X.....',
     '....X....',
     '.....X...',
     '......X..',
     '.......X.',
     '........X']
    
  5. 创建一个filter_wins函数,将结束的游戏从动作列表中提取,并将它们追加到一个包含 AI 玩家和对手玩家获胜棋盘状态的数组中:

    def filter_wins(move_list, ai_wins, opponent_wins):
        for board in move_list:
            won_by = game_won_by(board)
            if won_by == AI_SIGN:
                ai_wins.append(board)
                move_list.remove(board)
            elif won_by == OPPONENT_SIGN:
                opponent_wins.append(board)
                move_list.remove(board)
    

    在前面的代码片段中,我们定义了一个filter_wins函数,它将每个玩家的获胜状态添加到棋盘的列表中。

  6. 使用count_possibilities函数,该函数打印并返回决策树叶子节点的数量,分别为平局、第一玩家获胜和第二玩家获胜,如下方代码片段所示:

    def count_possibilities():
        board = EMPTY_SIGN * 9
        move_list = [board]
        ai_wins = []
        opponent_wins = []
        for i in range(9):
            print('step ' + str(i) + '. Moves: ' \
                  + str(len(move_list)))
            sign = AI_SIGN if \
                   i % 2 == 0 else OPPONENT_SIGN
            move_list = all_moves_from_board_list\
                        (move_list, sign)
            filter_wins(move_list, ai_wins, \
                        opponent_wins)
        print('First player wins: ' + str(len(ai_wins)))
        print('Second player wins: ' + str(len(opponent_wins)))
        print('Draw', str(len(move_list)))
        print('Total', str(len(ai_wins) \
              + len(opponent_wins) + len(move_list)))
        return len(ai_wins), len(opponent_wins), \
               len(move_list), len(ai_wins) \
               + len(opponent_wins) + len(move_list)
    

    我们在每个状态中最多有9个步骤。在第 0、2、4、6 和 8 次迭代中,AI 玩家进行移动。在所有其他迭代中,对手进行移动。我们在所有步骤中创建所有可能的动作,并从动作列表中提取完成的游戏。

  7. 执行可能性数量以体验组合爆炸,并将结果保存在四个变量中,分别为first_playersecond_playerdrawtotal

    first_player, second_player, \
    draw, total = count_possibilities()
    

    预期输出如下:

    step 0\. Moves: 1
    step 1\. Moves: 9
    step 2\. Moves: 72
    step 3\. Moves: 504
    step 4\. Moves: 3024
    step 5\. Moves: 13680
    step 6\. Moves: 49402
    step 7\. Moves: 111109
    step 8\. Moves: 156775
    First player wins: 106279
    Second player wins: 68644
    Draw 91150
    Total 266073
    

如你所见,棋盘状态的树由总共266073个叶子节点组成。count_possibilities函数本质上实现了一个 BFS 算法,用于遍历游戏的所有可能状态。请注意,我们会多次计算这些状态,因为在步骤 1中将X放置在右上角与在步骤 3中将X放置在左上角会导致与从左上角开始再将X放置在右上角类似的状态。如果我们实现了重复状态的检测,我们需要检查的节点会更少。然而,在此阶段,由于游戏深度有限,我们将省略此步骤。

然而,决策树与 count_possibilities 检查的数据结构是相同的。在决策树中,我们通过探索所有可能的未来步骤来评估每个行动的效用。在我们的示例中,我们可以通过观察固定前几步后的胜负情况来计算初始步骤的效用。

注意

树的根节点是初始状态。树的内部状态是游戏尚未结束并且仍有可能进行移动的状态。树的叶子节点包含一个游戏已结束的状态。

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

你也可以在网上运行此示例,访问 packt.live/3dpnuIz

你必须执行整个 Notebook 才能得到期望的结果。

活动 1.02:教授代理人认识到它何时防止失败

解决方案

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

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 重用之前 步骤 2–6 中的所有代码,来自 练习 1.03教授代理人获胜

  3. 创建一个名为 player_can_win 的函数,该函数使用 all_moves_from_board 函数获取所有棋盘上的移动,并通过 next_move 变量进行迭代。

    在每次迭代中,它检查玩家是否可以获胜。

    def player_can_win(board, sign):
        next_moves = all_moves_from_board(board, sign)
        for next_move in next_moves:
            if game_won_by(next_move) == sign:
                return True
        return False
    
  4. 扩展 AI 移动,使其更倾向于进行安全移动。如果一个移动是安全的,即对手无法在下一步获胜,那么该移动就被视为安全的:

    def ai_move(board):
        new_boards = all_moves_from_board(board, AI_SIGN)
        for new_board in new_boards:
            if game_won_by(new_board) == AI_SIGN:
                return new_board
        safe_moves = []
        for new_board in new_boards:
            if not player_can_win(new_board, OPPONENT_SIGN):
                safe_moves.append(new_board)
        return choice(safe_moves) \
        if len(safe_moves) > 0 else new_boards[0]
    

    在前面的代码片段中,我们定义了 ai_move 函数,该函数通过查看所有可能的列表并选择一个玩家无法在下一步获胜的选项来告诉 AI 如何行动。如果你测试我们的新应用,你会发现 AI 已经做出了正确的决定。

  5. 现在,将此逻辑放入状态空间生成器中,并通过生成所有可能的游戏来检查电脑玩家的表现:

    def all_moves_from_board(board, sign):
        move_list = []
        for i, v in enumerate(board):
            if v == EMPTY_SIGN:
                new_board = board[:i] + sign + board[i+1:]
                move_list.append(new_board)
                if game_won_by(new_board) == AI_SIGN:
                    return [new_board]
        if sign == AI_SIGN:
            safe_moves = []
            for move in move_list:
                if not player_can_win(move, OPPONENT_SIGN):
                    safe_moves.append(move)
            return safe_moves if len(safe_moves) > 0 else move_list[0:1]
        else:
            return move_list
    

    在前面的代码片段中,我们定义了一个生成所有可能移动的函数。一旦我们找到了能够让玩家获胜的下一步,我们就返回一个反制的移动。我们不关心玩家是否有多个获胜选项——我们只返回第一个可能性。如果 AI 无法阻止玩家获胜,我们就返回所有可能的移动。

    让我们看看这在每一步计数所有可能性时意味着什么。

  6. 计算所有可能的选项:

    first_player, second_player, \
    draw, total = count_possibilities()
    

    预期输出是这样的:

    step 0\. Moves: 1
    step 1\. Moves: 9
    step 2\. Moves: 72
    step 3\. Moves: 504
    step 4\. Moves: 3024
    step 5\. Moves: 5197
    step 6\. Moves: 18606
    step 7\. Moves: 19592
    step 8\. Moves: 30936
    First player wins: 20843
    Second player wins: 962
    Draw 20243
    Total 42048
    

我们现在做得比之前更好了。我们不仅再次去除了几乎 2/3 的可能游戏,而且,大多数时候,AI 玩家要么获胜,要么以平局收场。

注意

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

你也可以在网上运行此示例,访问 packt.live/2V7qLpO

你必须执行整个 Notebook 才能得到期望的结果。

活动 1.03:修复 AI 的第一步和第二步,使其无敌

解决方案

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

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 重用前面的步骤 2–4中的代码,活动 1.02教会代理在防止输局时识别局势

  3. 现在,计算棋盘上空白格子的数量,并在空白格子有 9 个或 7 个时做出硬编码的移动。你可以尝试不同的硬编码移动。我们发现,占据任意一个角落,然后占据对角的角落,能够保证不输。如果对手占据了对角的角落,那么在中间位置做出移动也不会失败:

    def all_moves_from_board(board, sign):
        if sign == AI_SIGN:
            empty_field_count = board.count(EMPTY_SIGN)
            if empty_field_count == 9:
                return [sign + EMPTY_SIGN * 8]
            elif empty_field_count == 7:
                return [board[:8] + sign if board[8] == \
                        EMPTY_SIGN else board[:4] + sign + board[5:]]
        move_list = []
        for i, v in enumerate(board):
            if v == EMPTY_SIGN:
                new_board = board[:i] + sign + board[i+1:]
                move_list.append(new_board)
                if game_won_by(new_board) == AI_SIGN:
                    return [new_board]
        if sign == AI_SIGN:
            safe_moves = []
            for move in move_list:
                if not player_can_win(move, OPPONENT_SIGN):
                    safe_moves.append(move)
            return safe_moves if len(safe_moves) > 0 else move_list[0:1]
        else:
            return move_list
    
  4. 现在,验证状态空间:

    first_player, second_player, draw, total = count_possibilities()
    

    预期的输出是这样的:

    step 0\. Moves: 1
    step 1\. Moves: 1
    step 2\. Moves: 8
    step 3\. Moves: 8
    step 4\. Moves: 48
    step 5\. Moves: 38
    step 6\. Moves: 108
    step 7\. Moves: 76
    step 8\. Moves: 90
    First player wins: 128
    Second player wins: 0
    Draw 60
    Total 188
    

修正前两个步骤后,我们只需要处理 8 种可能,而不是 504 种。我们还引导 AI 进入了一个状态,在这个状态下,硬编码规则足够使它永远不会输掉游戏。修正步骤并不重要,因为我们会给 AI 提供硬编码的步骤,但它很重要,因为它是用来评估和比较每一步的工具。修正前两个步骤后,我们只需要处理 8 种可能,而不是 504 种。我们还引导 AI 进入了一个状态,在这个状态下,硬编码规则足以让它永不输掉游戏。正如你所见,AI 现在几乎是无敌的,它只会获胜或平局。

玩家对抗这个 AI 时,最好的结果是平局。

注意

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

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

你必须执行整个 Notebook,以获得预期的结果。

活动 1.04:四子棋

解决方案

  1. 打开一个新的 Jupyter Notebook 文件。

    我们通过编写init方法来设置TwoPlayersGame框架。

  2. 将棋盘定义为一维列表,就像井字游戏的示例一样。我们也可以使用二维列表,但建模的难度不会有太大变化。除了像井字游戏那样进行初始化外,我们还将进一步操作。我们将生成游戏中所有可能的胜利组合,并将其保存以供以后使用,代码如下所示:

    from easyAI import TwoPlayersGame, Human_Player
    class ConnectFour(TwoPlayersGame):
        def __init__(self, players):
            self.players = players
            self.board = [0 for i in range(42)]
            self.nplayer = 1  
            def generate_winning_tuples():
                tuples = []
                # horizontal
                tuples += [list(range(row*7+column, \
                           row*7+column+4, 1)) \
                           for row in range(6) \
                           for column in range(4)]
                # vertical
                tuples += [list(range(row*7+column, \
                           row*7+column+28, 7)) \
                           for row in range(3) \
                           for column in range(7)]
                # diagonal forward
                tuples += [list(range(row*7+column, \
                           row*7+column+32, 8)) \
                           for row in range(3) \
                           for column in range(4)]
                # diagonal backward
                tuples += [list(range(row*7+column, \
                           row*7+column+24, 6)) \
                           for row in range(3) \
                           for column in range(3, 7, 1)]
                return tuples
            self.tuples = generate_winning_tuples()
    
  3. 接下来,处理possible_moves函数,这是一个简单的枚举。请注意,我们在移动名称中使用从17的列索引,因为在玩家界面中,从1开始列索引比从零开始更为方便。对于每一列,我们检查是否有空闲格子。如果有空位,我们就将该列设为一个可能的移动:

        def possible_moves(self):
            return [column+1 \
                    for column in range(7) \
                    if any([self.board[column+row*7] == 0 \
                            for row in range(6)])
                    ]
    
  4. 执行一次移动就像possible_moves函数一样。我们检查该移动的列,并从底部开始找到第一个空单元格。一旦找到,就占据它。你也可以阅读make_moveunmake_move两个函数的实现。在unmake_move函数中,我们从上到下检查列,并在第一个非空单元格处移除移动。请注意,我们依赖于easyAI的内部表示,以确保它不会撤销自己没有做的移动。否则,这个函数会在不检查的情况下删除对方玩家的棋子:

        def make_move(self, move):
            column = int(move) - 1
            for row in range(5, -1, -1):
                index = column + row*7
                if self.board[index] == 0:
                    self.board[index] = self.nplayer
                    return
        # optional method (speeds up the AI)
        def unmake_move(self, move):
            column = int(move) - 1
            for row in range(6):
                index = column + row*7
                if self.board[index] != 0:
                    self.board[index] = 0
                    return
    
  5. 由于我们已经有了必须检查的元组,我们可以大部分复用来自井字游戏示例的lose函数:

        def lose(self):
            return any([all([(self.board[c] == self.nopponent)
                             for c in line])
                        for line in self.tuples])
        def is_over(self):
            return (self.possible_moves() == []) or self.lose()
    
  6. 我们的最终任务是实现show方法,该方法打印棋盘。我们将重用井字游戏的实现,只需更改showscoring变量:

        def show(self):
            print('\n'+'\n'.join([
                ' '.join([['.', 'O', 'X']\
                          [self.board[7*row+column]] \
                          for column in range(7)])
                for row in range(6)]))
        def scoring(self):
            return -100 if self.lose() else 0
    if __name__ == "__main__":
        from easyAI import AI_Player, Negamax
        ai_algo = Negamax(6)
        ConnectFour([Human_Player(), \
                     AI_Player(ai_algo)]).play()
    
  7. 现在所有函数都已完成,你可以尝试示例。随时和对手玩一两局。

    预期输出是这样的:

    图 1.30:连接四子游戏的预期输出

图 1.30:连接四子游戏的预期输出

完成此活动后,你会发现对手并不完美,但它的表现相当不错。如果你有一台强大的计算机,你可以增加Negamax算法的参数。我们鼓励你提出更好的启发式方法。

注意

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

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

你必须执行整个 Notebook,才能获得期望的结果。

2. 回归介绍

活动 2.01:使用 1、2 和 3 度多变量的多项式回归进行波士顿房价预测

解决方案

  1. 打开一个 Jupyter Notebook。

  2. 导入所需的包并从sklearn加载波士顿房价数据到 DataFrame 中:

    import numpy as np
    import pandas as pd
    from sklearn import preprocessing
    from sklearn import model_selection
    from sklearn import linear_model
    from sklearn.preprocessing import PolynomialFeatures
    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/'\
               'The-Applied-Artificial-Intelligence-Workshop/'\
               'master/Datasets/boston_house_price.csv'
    df = pd.read_csv(file_url)
    

    df的输出如下:

    图 2.28:显示数据集的输出

    图 2.28:显示数据集的输出

    在本章前面,你学习了执行线性回归所需的大部分包都来自sklearn。我们需要导入preprocessing模块来缩放数据,linear_model模块来训练线性回归,PolynomialFeatures模块来转换多项式回归的输入,以及model_selection模块来评估每个模型的性能。

  3. 通过将标签和特征转换为 NumPy 数组并缩放特征,准备预测数据集:

    features = np.array(df.drop('MEDV', 1))
    label = np.array(df['MEDV'])
    scaled_features = preprocessing.scale(features)
    

    features的输出如下:

    图 2.29:标签和特征转换为 NumPy 数组

    array([[-0.41978194,  0.28482986, -1.2879095 , ..., 
            -0.66660821, -1.45900038, -1.0755623 ],
           [-0.41733926, -0.48772236, -0.59338101, ..., 
            -0.98732948, -0.30309415, -0.49243937],
           [-0.41734159, -0.48772236, -0.59338101, ..., 
            -0.98732948, -0.30309415, -1.2087274 ],
           ...,
           [-0.41344658, -0.48772236,  0.11573841, ..., 
            -0.80321172,  1.17646583, -0.98304761],
           [-0.40776407, -0.48772236,  0.11573841, ..., 
            -0.80321172,  1.17646583, -0.86530163],
           [-0.41500016, -0.48772236,  0.11573841, ..., 
            -0.80321172,  1.17646583, -0.66905833]])
    

    如你所见,我们的特征已经被正确缩放。

    由于我们没有缺失值,并且不像在练习 2.03中那样试图预测未来值(准备 Quandl 数据进行预测),我们可以直接将标签('MEDV')和特征转换为 NumPy 数组。然后,我们可以使用 preprocessing.scale() 函数对特征数组进行缩放。

  4. 通过将缩放特征转换为适合每个多项式回归的格式,创建三组不同的特征集:

    poly_1_scaled_features = PolynomialFeatures(degree=1)\
                             .fit_transform(scaled_features)
    poly_2_scaled_features = PolynomialFeatures(degree=2)\
                             .fit_transform(scaled_features)
    poly_3_scaled_features = PolynomialFeatures(degree=3)\
                             .fit_transform(scaled_features)
    

    poly_1_scaled_features 的输出如下:

    array([[ 1\.        , -0.41978194,  0.28482986, ..., -0.66660821,
            -1.45900038, -1.0755623 ],
           [ 1\.        , -0.41733926, -0.48772236, ..., -0.98732948,
            -0.30309415, -0.49243937],
           [ 1\.        , -0.41734159, -0.48772236, ..., -0.98732948,
            -0.30309415, -1.2087274 ],
           ...,
           [ 1\.        , -0.41344658, -0.48772236, ..., -0.80321172,
             1.17646583, -0.98304761],
           [ 1\.        , -0.40776407, -0.48772236, ..., -0.80321172,
             1.17646583, -0.86530163],
           [ 1\.        , -0.41500016, -0.48772236, ..., -0.80321172,
             1.17646583, -0.66905833]])
    

    我们的 scaled_features 变量已正确转换,用于度数为 1 的多项式回归。

    poly_2_scaled_features 的输出如下:

    ![图 2.31:显示 poly_2_scaled_features 输出的图像]

    ](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_02_31.jpg)

    array([[ 1\.        , -0.41978194,  0.28482986, ..., -2.28953024,
            -1.68782164, -1.24424733],
           [ 1\.        , -0.41733926, -0.48772236, ..., -0.04523847,
            -0.07349928, -0.11941484],
           [ 1\.        , -0.41734159, -0.48772236, ..., -0.11104103,
            -0.4428272 , -1.76597723],
           ...,
           [ 1\.        , -0.41344658, -0.48772236, ..., -1.36060852,
             1.13691611, -0.9500001 ],
           [ 1\.        , -0.40776407, -0.48772236, ..., -1.19763962,
             0.88087515, -0.64789192],
           [ 1\.        , -0.41500016, -0.48772236, ..., -0.9260248 ,
             0.52663205, -0.29949664]])
    

    我们的 scaled_features 变量已正确转换,用于度数为 3 的多项式回归。

    我们必须以三种不同的方式转换缩放特征,因为每个多项式回归度数需要不同的输入转换。

  5. 将数据分为训练集和测试集,random state = 8

    (poly_1_features_train, poly_1_features_test, \
    poly_label_train, poly_label_test) = \
    model_selection.train_test_split(poly_1_scaled_features, \
                                     label, \
                                     test_size=0.1, \
                                     random_state=8)
    (poly_2_features_train, poly_2_features_test, \
    poly_label_train, poly_label_test) = \
    model_selection.train_test_split(poly_2_scaled_features, \
                                     label, \
                                     test_size=0.1, \
                                     random_state=8)
    (poly_3_features_train, poly_3_features_test, \
    poly_label_train, poly_label_test) = \
    model_selection.train_test_split(poly_3_scaled_features, \
                                     label, \
                                     test_size=0.1, \
                                     random_state=8)
    

    由于我们有三组不同的缩放转换特征,但相同的标签集,我们必须执行三次不同的拆分。通过在每次拆分中使用相同的标签集和 random_state,我们确保每次拆分都获得相同的 poly_label_trainpoly_label_test

  6. 执行度数为 1 的多项式回归,并评估模型是否存在过拟合:

    model_1 = linear_model.LinearRegression()
    model_1.fit(poly_1_features_train, poly_label_train)
    model_1_score_train = model_1.score(poly_1_features_train, \
                                        poly_label_train)
    model_1_score_test = model_1.score(poly_1_features_test, \
                                       poly_label_test)
    

    model_1_score_train 的输出如下:

    0.7406006443486721
    

    model_1_score_test 的输出如下:

    0.6772229017901507
    

    为了估计模型是否过拟合,我们需要比较应用于训练集和测试集的模型得分。如果训练集的得分远高于测试集,则表示过拟合。在此案例中,度数为 1 的多项式回归在训练集上的得分为 0.74,而测试集上的得分为 0.68

  7. 执行度数为 2 的多项式回归,并评估模型是否存在过拟合:

    model_2 = linear_model.LinearRegression()
    model_2.fit(poly_2_features_train, poly_label_train)
    model_2_score_train = model_2.score(poly_2_features_train, \
                                        poly_label_train)
    model_2_score_test = model_2.score(poly_2_features_test, \
                                       poly_label_test)
    

    model_2_score_train 的输出如下:

    0.9251199698832675
    

    model_2_score_test 的输出如下:

    0.8253870684280571
    

    就像一次多项式回归度数为 1 的情况,我们的多项式回归度数为 2 时过拟合的情况更加严重,但最终却取得了更好的结果。

  8. 执行度数为 3 的多项式回归,并评估模型是否存在过拟合:

    model_3 = linear_model.LinearRegression()
    model_3.fit(poly_3_features_train, poly_label_train)
    model_3_score_train = model_3.score(poly_3_features_train, \
                                        poly_label_train)
    model_3_score_test = model_3.score(poly_3_features_test, \
                                       poly_label_test)
    

    model_3_score_train 的输出如下:

    0.9910498071894897
    

    model_3_score_test 的输出如下:

    -8430.781888645262
    

    这些结果非常有趣,因为三次多项式回归成功地达到了接近完美的分数0.99(1 是最高分)。这给出了一个警告信号,表明我们的模型过度拟合了训练数据。当模型应用于测试集时,我们得到了一个非常低的负分数-8430,这进一步确认了过拟合问题。提醒一下,0 分是通过将数据的均值作为预测值得到的。这意味着我们的第三个模型的预测结果比单纯使用均值还要差。

  9. 比较 3 个模型在测试集上的预测结果与标签:

    model_1_prediction = model_1.predict(poly_1_features_test)
    model_2_prediction = model_2.predict(poly_2_features_test)
    model_3_prediction = model_3.predict(poly_3_features_test)
    df_prediction = pd.DataFrame(poly_label_test)
    df_prediction.rename(columns = {0:'label'}, inplace = True)
    df_prediction['model_1_prediction'] = \
    pd.DataFrame(model_1_prediction)
    df_prediction['model_2_prediction'] = \
    pd.DataFrame(model_2_prediction)
    df_prediction['model_3_prediction'] = \
    pd.DataFrame(model_3_prediction)
    

    df_prediction的输出如下:

    图 2.32:显示期望预测值的输出

图 2.32:显示期望预测值的输出

在对每个模型应用predict函数,得到它们各自测试集的预测值之后,我们将它们与标签值一起转换成一个单独的df_prediction数据框。增加多项式回归的次数并不意味着模型的表现一定会比低次模型更好。实际上,增加次数会导致模型在训练数据上的过拟合。

注意

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

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

你必须执行整个 Notebook 才能得到期望的结果。

在这个活动中,我们学习了如何对波士顿房价数据集进行 1 到 3 度的多项式回归,并看到增加度数会导致模型过拟合。

3. 分类简介

活动 3.01:提高信用评分的准确度

解决方案

  1. 打开一个新的 Jupyter Notebook 文件,执行上一个练习Exercise 3.04中的所有步骤,Scikit-Learn 中的 K-最近邻分类

  2. sklearn导入neighbors

    from sklearn import neighbors
    
  3. 创建一个名为fit_knn的函数,该函数接受以下参数:kpfeatures_trainlabel_trainfeatures_testlabel_test。这个函数将使用训练集拟合KNeighborsClassifier,并打印训练集和测试集的准确度评分,如下代码片段所示:

    def fit_knn(k, p, features_train, label_train, \
                features_test, label_test):
        classifier = neighbors.KNeighborsClassifier(n_neighbors=k, p=p)
        classifier.fit(features_train, label_train)
        return classifier.score(features_train, label_train), \
               classifier.score(features_test, label_test)
    
  4. 调用fit_knn()函数,设置k=5p=2,将结果保存到2个变量中并打印。这些变量是acc_train_1acc_test_1

    acc_train_1, acc_test_1 = fit_knn(5, 2, features_train, \
                                      label_train, \
                                      features_test, label_test)
    acc_train_1, acc_test_1
    

    期望的输出是这样的:

    (0.78625, 0.75)
    

    使用k=5p=2时,KNN 取得了接近0.78的不错的准确率。但训练集和测试集的分数差异较大,这意味着模型存在过拟合问题。

  5. 调用fit_knn()函数,设置k=10p=2,将结果保存到2个变量中并打印。这些变量是acc_train_2acc_test_2

    acc_train_2, acc_test_2 = fit_knn(10, 2, features_train, \
                                      label_train, \
                                      features_test, label_test)
    acc_train_2, acc_test_2
    

    期望的输出是这样的:

    (0.775, 0.785)
    

    将邻居数量增加到 10 降低了训练集的准确性,但现在它与测试集的准确性非常接近。

  6. 使用k=15p=2调用fit_knn()函数,将结果保存在2个变量中,并打印它们。这些变量是acc_train_3acc_test_3

    acc_train_3, acc_test_3 = fit_knn(15, 2, features_train, \
                                      label_train, \
                                      features_test, label_test)
    acc_train_3, acc_test_3
    

    预期的输出是这样的:

    (0.76625, 0.79)
    

    使用k=15p=2时,训练集和测试集之间的差异增加了。

  7. 使用k=25p=2调用fit_knn()函数,将结果保存在2个变量中,并打印它们。这些变量是acc_train_4acc_test_4

    acc_train_4, acc_test_4 = fit_knn(25, 2, features_train, \
                                      label_train, \
                                      features_test, label_test)
    acc_train_4, acc_test_4
    

    预期的输出是这样的:

    (0.7375, 0.77)
    

    将邻居数量增加到25对训练集产生了显著影响,但模型仍然存在过拟合。

  8. 使用k=50p=2调用fit_knn()函数,将结果保存在2个变量中,并打印它们。这些变量是acc_train_5acc_test_5

    acc_train_5, acc_test_5 = fit_knn(50, 2, features_train, \
                                      label_train, \
                                      features_test, label_test)
    acc_train_5, acc_test_5
    

    预期的输出是这样的:

    (0.70625, 0.775)
    

    将邻居数量增加到50既没有改善模型性能,也没有解决过拟合问题。

  9. 使用k=5p=1调用fit_knn()函数,将结果保存在2个变量中,并打印它们。这些变量是acc_train_6acc_test_6

    acc_train_6, acc_test_6 = fit_knn(5, 1, features_train, \
                                      label_train, \
                                      features_test, label_test)
    acc_train_6, acc_test_6
    

    预期的输出是这样的:

    (0.8, 0.735)
    

    更改为曼哈顿距离有助于提高训练集的准确性,但模型仍然存在过拟合。

  10. 使用k=10p=1调用fit_knn()函数,将结果保存在2个变量中,并打印它们。这些变量是acc_train_7acc_test_7

    acc_train_7, acc_test_7 = fit_knn(10, 1, features_train, \
                                      label_train, \
                                      features_test, label_test)
    acc_train_7, acc_test_7
    

    预期的输出是这样的:

    (0.77, 0.785)
    

    使用k=10时,训练集和测试集的准确性相差无几:大约为0.78

  11. 使用k=15p=1调用fit_knn()函数,将结果保存在2个变量中,并打印它们。这些变量是acc_train_8acc_test_8

    acc_train_8, acc_test_8 = fit_knn(15, 1, features_train, \
                                      label_train, \
                                      features_test, label_test)
    acc_train_8, acc_test_8
    

    预期的输出是这样的:

    (0.7575, 0.775)
    

    k增加到15时,模型的准确性有所提高,且过拟合情况不再那么严重。

  12. 使用k=25p=1调用fit_knn()函数,将结果保存在2个变量中,并打印它们。这些变量是acc_train_9acc_test_9

    acc_train_9, acc_test_9 = fit_knn(25, 1, features_train, \
                                      label_train, \
                                      features_test, label_test)
    acc_train_9, acc_test_9
    

    预期的输出是这样的:

    (0.745, 0.8)
    

    使用k=25时,训练集和测试集准确性之间的差异在增加,因此模型存在过拟合。

  13. 使用k=50p=1调用fit_knn()函数,将结果保存在2个变量中,并打印它们。这些变量是acc_train_10acc_test_10

    acc_train_10, acc_test_10 = fit_knn(50, 1, features_train, \
                                        label_train, \
                                        features_test, label_test)
    acc_train_10, acc_test_10
    

    预期的输出是这样的:

    (0.70875, 0.78)
    

    使用k=50时,模型在训练集上的表现显著下降,且模型显然出现了过拟合。

在此活动中,我们尝试了多个n_neighborsp的超参数组合。我们发现的最佳组合是n_neighbors=10p=2。使用这些超参数,模型几乎没有过拟合,并且在训练集和测试集上都达到了大约78%的准确性。

注意

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

你也可以在网上运行这个例子,链接:packt.live/2Bx0yd8

你必须执行整个 Notebook 才能获得期望的结果。

活动 3.02:在 scikit-learn 中进行支持向量机优化

解决方案

  1. 打开一个新的 Jupyter Notebook 文件并执行之前提到的所有步骤,练习 3.04scikit-learn 中的 K 最近邻分类

  2. sklearn导入svm

    from sklearn import svm
    
  3. 创建一个名为fit_knn的函数,接受以下参数:features_trainlabel_trainfeatures_testlabel_testkernel="linear"C=1degree=3gamma='scale'。该函数将使用训练集拟合 SVC,并打印训练集和测试集的准确度得分:

    def fit_svm(features_train, label_train, \
                features_test, label_test, \
                kernel="linear", C=1, \
                degree=3, gamma='scale'):
        classifier = svm.SVC(kernel=kernel, C=C, \
                             degree=degree, gamma=gamma)
        classifier.fit(features_train, label_train)
        return classifier.score(features_train, label_train), \
               classifier.score(features_test, label_test)
    
  4. 调用fit_knn()函数,使用默认的超参数值,将结果保存在2个变量中并打印。这些变量是acc_train_1acc_test_1

    acc_train_1, \
    acc_test_1 =  fit_svm(features_train, \
                          label_train, \
                          features_test, \
                          label_test)
    acc_train_1,  acc_test_1
    

    期望的输出是这样的:

    (0.71625, 0.75)
    

    使用默认的超参数值(线性模型),模型在训练集和测试集上的表现差异较大。

  5. 调用fit_knn()函数,设置kernel="poly"C=1degree=4gamma=0.05,将结果保存在2个变量中并打印。这些变量是acc_train_2acc_test_2

    acc_train_2, \
    acc_test_2 = fit_svm(features_train, label_train, \
                         features_test, label_test, \
                         kernel="poly",  C=1, \
                         degree=4, gamma=0.05)
    acc_train_2,  acc_test_2
    

    期望的输出是这样的:

    (0.68875, 0.745)
    

    使用四次多项式时,模型在训练集上的表现不佳。

  6. 调用fit_knn()函数,设置kernel="poly"C=2degree=4gamma=0.05,将结果保存在2个变量中并打印。这些变量是acc_train_3acc_test_3

    acc_train_3, \
    acc_test_3 = fit_svm(features_train, \
                         label_train, features_test, \
                         label_test, kernel="poly",  \
                         C=2, degree=4, gamma=0.05)
    acc_train_3,  acc_test_3
    

    期望的输出是这样的:

    (0.68875, 0.745)
    

    增加正则化参数C对模型的表现没有任何影响。

  7. 调用fit_knn()函数,设置kernel="poly"C=1degree=4gamma=0.25,将结果保存在2个变量中并打印。这些变量是acc_train_4acc_test_4

    acc_train_4, \
    acc_test_4 = fit_svm(features_train, \
                         label_train, features_test, \
                         label_test, kernel="poly",  \
                         C=1, degree=4, gamma=0.25)
    acc_train_4,  acc_test_4
    

    期望的输出是这样的:

    (0.84625, 0.775)
    

    将 gamma 值增加到0.25显著提高了模型在训练集上的表现。然而,测试集的准确度较低,因此模型存在过拟合现象。

  8. 调用fit_knn()函数,设置kernel="poly"C=1degree=4gamma=0.5,将结果保存在2个变量中并打印。这些变量是acc_train_5acc_test_5

    acc_train_5, \
    acc_test_5 = fit_svm(features_train, \
                         label_train, features_test, \
                         label_test, kernel="poly",  \
                         C=1, degree=4, gamma=0.5)
    acc_train_5,  acc_test_5
    

    期望的输出是这样的:

    (0.9575, 0.73)
    

    将 gamma 值增加到0.5极大地改善了模型在训练集上的表现,但它明显出现了过拟合,因为测试集上的准确度得分明显降低。

  9. 调用fit_knn()函数,设置kernel="poly"C=1degree=4gamma=0.16,将结果保存在2个变量中并打印。这些变量是acc_train_6acc_test_6

    acc_train_6, \
    acc_test_6 = fit_svm(features_train, label_train, \
                         features_test, label_test, \
                         kernel="poly",  C=1, \
                         degree=4, gamma=0.16)
    acc_train_6,  acc_test_6
    

    期望的输出是这样的:

    (0.76375, 0.785)
    

    使用gamma=0.16时,模型的准确度得分比最佳的 KNN 模型更高。训练集和测试集的得分都接近0.77

  10. 调用fit_knn()函数,使用kernel="sigmoid",将结果保存在2个变量中并打印出来。这些变量是acc_train_7acc_test_7

    acc_train_7, \
    acc_test_7 = fit_svm(features_train, label_train, \
                         features_test, label_test, \
                         kernel="sigmoid")
    acc_train_7,  acc_test_7
    

    预期输出如下:

    (0.635, 0.66)
    

    Sigmoid 核的准确率得分较低。

  11. 调用fit_knn()函数,使用kernel="rbf"gamma=0.15,将结果保存在2个变量中并打印出来。这些变量是acc_train_8acc_test_8

    acc_train_8, \
    acc_test_8 = fit_svm(features_train, \
                         label_train, features_test, \
                         label_test, kernel="rbf", \
                         gamma=0.15)
    acc_train_8,  acc_test_8
    

    预期输出如下:

    (0.7175, 0.765)
    

    rbf核在gamma=0.15时获得了较好的分数。尽管如此,模型仍然有些过拟合。

  12. 调用fit_knn()函数,使用kernel="rbf"gamma=0.25,将结果保存在2个变量中并打印出来。这些变量是acc_train_9acc_test_9

    acc_train_9, \
    acc_test_9 = fit_svm(features_train, \
                         label_train, features_test, \
                         label_test, kernel="rbf", \
                         gamma=0.25)
    acc_train_9,  acc_test_9
    

    预期输出如下:

    (0.74, 0.765)
    

    模型性能在gamma=0.25时有所提升,但仍然存在过拟合。

  13. 调用fit_knn()函数,使用kernel="rbf"gamma=0.35,将结果保存在2个变量中并打印出来。这些变量是acc_train_10acc_test_10

    acc_train_10, \
    acc_test_10 = fit_svm(features_train, label_train, \
                          features_test, label_test, \
                          kernel="rbf", gamma=0.35)
    acc_train_10, acc_test_10
    

    预期输出如下:

    (0.78125, 0.775)
    

使用rbf核和gamma=0.35时,我们在训练集和测试集上获得了非常相似的结果,模型的性能高于我们在前一个活动中训练的最佳 KNN 模型。这是我们在德国信用数据集上的最佳模型。

注意

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

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

必须执行整个 Notebook 才能得到期望的结果。

在本次活动中,我们尝试了 SVM 分类器的不同主超参数值:kernelgammaCdegrees。我们观察了这些超参数如何影响模型的表现以及它们过拟合的趋势。经过反复试验,我们最终找到了最佳的超参数组合,并获得了接近 0.78 的准确率。这个过程被称为超参数调优,是任何数据科学项目的重要步骤。

4. 决策树简介

活动 4.01:汽车数据分类

解决方案

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 导入pandas包并命名为pd

    import pandas as pd
    
  3. 创建一个名为file_url的新变量,该变量将包含原始数据集的 URL:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/'\
               'The-Applied-Artificial-Intelligence-Workshop/'\
               'master/Datasets/car.csv'
    
  4. 使用pd.read_csv()方法加载数据:

    df = pd.read_csv(file_url) 
    
  5. 打印df的前五行:

    df.head()
    

    输出结果如下:

    图 4.13:数据集的前五行

    图 4.13:数据集的前五行

  6. 导入preprocessing模块,来自sklearn

    from sklearn import preprocessing
    
  7. 创建一个名为encode()的函数,该函数接受一个 DataFrame 和列名作为参数。该函数将实例化LabelEncoder(),用列的唯一值进行拟合,并转化其数据。它将返回转换后的列:

    def encode(data_frame, column):
        label_encoder = preprocessing.LabelEncoder()
        label_encoder.fit(data_frame[column].unique())
        return label_encoder.transform(data_frame[column])
    
  8. 创建一个for循环,该循环将遍历df的每一列,并使用encode()函数对它们进行编码:

    for column in df.columns:
        df[column] = encode(df, column)
    
  9. 现在,打印df的前五行:

    df.head()
    

    输出结果如下:

    图 4.14:数据集的前五行已更新

    ](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_04_14.jpg)

    图 4.14:数据集的前五行已更新

  10. 使用.pop()从 pandas 中提取类别列,并将其保存到名为label的变量中:

    label = df.pop('class')
    
  11. sklearn导入model_selection

    from sklearn import model_selection
    
  12. 使用test_size=0.1random_state=88将数据集划分为训练集和测试集:

    features_train, features_test, label_train, label_test = \
    model_selection.train_test_split(df, label, \
                                     test_size=0.1, \
                                     random_state=88)
    
  13. sklearn导入DecisionTreeClassifier

    from sklearn.tree import DecisionTreeClassifier
    
  14. 实例化DecisionTreeClassifier()并将其保存到名为decision_tree的变量中:

    decision_tree = DecisionTreeClassifier()
    
  15. 使用训练集拟合决策树:

    decision_tree.fit(features_train, label_train)
    

    输出结果如下:

    图 4.15:决策树在训练集上的拟合

    ](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_04_15.jpg)

    图 4.15:决策树在训练集上的拟合

  16. 打印决策树在测试集上的得分:

    decision_tree.score( features_test, label_test )
    

    输出结果如下:

    0.953757225433526
    

    决策树在我们第一次尝试时的准确率为0.95,这非常值得注意。

  17. sklearn.metrics导入classification_report

    from sklearn.metrics import classification_report
    
  18. 打印测试标签和预测的分类报告:

    print(classification_report(label_test, \
          decision_tree.predict(features_test)))
    

    输出结果如下:

    图 4.16:展示预期的分类报告的输出

    ](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_04_16.jpg)

图 4.16:展示预期的分类报告的输出

从这个分类报告中,我们可以看到模型在四个类别的精确度得分上表现得相当好。至于召回率,我们可以看到它在最后一个类别上的表现较差。

注意

若要访问此部分的源代码,请参阅packt.live/3hQDLtr

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

你必须执行整个 Notebook 才能获得预期的结果。

完成此活动后,你已准备好汽车数据集并训练了决策树模型。你已学会如何获取其准确率和分类报告,从而分析其精确度和召回率得分。

活动 4.02:为你的租车公司进行随机森林分类

解决方案

  1. 打开一个 Jupyter Notebook。

  2. 重用活动 1步骤 1 - 4的代码,汽车数据分类

  3. sklearn.ensemble导入RandomForestClassifier

    from sklearn.ensemble import RandomForestClassifier
    
  4. 实例化一个随机森林分类器,设置n_estimators=100max_depth=6,并设置random_state=168。将其保存到一个名为random_forest_classifier的变量中:

    random_forest_classifier = \
    RandomForestClassifier(n_estimators=100, \
                           max_depth=6, random_state=168)
    
  5. 用训练集拟合随机森林分类器:

    random_forest_classifier.fit(features_train, label_train)
    

    输出结果如下:

    图 4.17:展示随机森林分类器及其超参数值的日志

    ](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_04_17.jpg)

    图 4.17:展示随机森林分类器及其超参数值的日志

    这些是RandomForest分类器及其超参数值的日志。

  6. 使用随机森林分类器对测试集进行预测,并将其保存到名为rf_preds_test的变量中。打印其内容:

    rf_preds_test = random_forest_classifier.fit(features_train, \
                                                 label_train)
    rf_preds_test
    

    输出结果如下:

    图 4.18:展示测试集上预测结果的输出

    ](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_04_18.jpg)

    图 4.18:显示测试集预测的输出

  7. sklearn.metrics导入classification_report

    from sklearn.metrics import classification_report
    
  8. 打印带有标签和测试集预测的分类报告:

    print(classification_report(label_test, rf_preds_test))
    

    输出如下:

    图 4.19:显示带有标签和测试集预测的分类报告的输出

    图 4.19:显示带有标签和测试集预测的分类报告的输出

    前述报告中的 F1 得分告诉我们,随机森林在类2上的表现良好,但在类03上表现不佳。模型无法准确预测类1,但测试集中只有 9 个观测值。准确率为0.84,而 F1 得分为0.82

  9. sklearn.metrics导入confusion_matrix

    from sklearn.metrics import confusion_matrix
    
  10. 显示测试集的真实标签和预测标签的混淆矩阵:

    confusion_matrix(label_test, rf_preds_test)
    

    输出如下:

    array([[ 32, 0, 10, 0], 
          [ 8, 0, 0, 1], 
          [ 5, 0, 109, 0], 
          [ 3, 0, 0, 5]])
    

    从这个混淆矩阵中,我们可以看出,RandomForest模型在准确预测第一类时遇到困难。它错误地预测了 16 个案例(8 + 5 + 3)为这一类。

  11. 使用.feature_importance_打印测试集的特征重要性得分,并将结果保存在名为rf_varimp的变量中。打印其内容:

    rf_varimp = random_forest_classifier.feature_importances_
    rf_varimp
    

    输出如下:

    array([0.12676384, 0.10366314, 0.02119621, 0.35266673, 
           0.05915769, 0.33655239])
    

    前述输出显示最重要的特征是第四个和第六个,分别对应personssafety

  12. sklearn.ensemble导入ExtraTreesClassifier

    from sklearn.ensemble import ExtraTreesClassifier
    
  13. 实例化ExtraTreesClassifier,设置n_estimators=100max_depth=6random_state=168,并将其保存在名为random_forest_classifier的变量中:

    extra_trees_classifier = \
    ExtraTreesClassifier(n_estimators=100, \
                         max_depth=6, random_state=168)
    
  14. 使用训练集拟合extratrees分类器:

    extra_trees_classifier.fit(features_train, label_train)
    

    输出如下:

    图 4.20:使用训练集的 extratrees 分类器的输出

    图 4.20:使用训练集的 extratrees 分类器的输出

    这些是extratrees分类器及其超参数值的日志。

  15. 使用extratrees分类器对测试集进行预测,并将结果保存在名为et_preds_test的变量中。打印其内容:

    et_preds_test = extra_trees_classifier.predict(features_test)
    et_preds_test
    

    输出如下:

    图 4.21:使用 extratrees 对测试集进行预测

    图 4.21:使用 extratrees 对测试集进行预测

  16. 打印带有标签和测试集预测的分类报告:

    print(classification_report(label_test, \
          extra_trees_classifier.predict(features_test)))
    

    输出如下:

    图 4.22:带有标签和测试集预测的分类报告

    图 4.22:带有标签和测试集预测的分类报告

    前面报告中的 F1 得分显示,随机森林在类别2上的表现良好,但在类别0上的表现较差。模型无法准确预测类别13,但测试集中只有分别为98的观测值。准确率为0.82,F1 得分为0.78。因此,我们的RandomForest分类器在extratrees上表现得更好。

  17. 显示测试集的真实标签与预测标签的混淆矩阵:

    confusion_matrix(label_test, et_preds_test)
    

    输出将如下所示:

    array([[ 28,   0,  14,   0],
           [  9,   0,   0,   0],
           [  2,   0, 112,   0],
           [  7,   0,   0,   1]])
    

    从这个混淆矩阵中,我们可以看出,extratrees模型在准确预测第一和第三类别时遇到了困难。

  18. 使用.feature_importance_打印测试集上的特征重要性分数,并将结果保存在一个名为et_varimp的变量中。打印其内容:

    et_varimp = extra_trees_classifier.feature_importances_
    et_varimp
    

    输出将如下所示:

    array([0.08844544, 0.0702334 , 0.01440408, 0.37662014, 0.05965896,
           0.39063797])
    

前面的输出向我们展示了最重要的特征是第六个和第四个特征,分别对应safetypersons。有趣的是,RandomForest也有相同的两个最重要特征,但顺序不同。

注意

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

你也可以在packt.live/3eswBcW上在线运行这个示例。

必须执行整个 Notebook 才能获得所需的结果。

5. 人工智能:聚类

活动 5.01:使用 K-Means 聚类销售数据

解决方案

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 将数据集加载为 DataFrame 并检查数据:

    import pandas as pd
    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/'\
               'The-Applied-Artificial-Intelligence-Workshop/'\
               'master/Datasets/'\
               'Sales_Transactions_Dataset_Weekly.csv'
    df = pd.read_csv(file_url)
    df
    

    df的输出如下所示:

    图 5.18:显示数据集内容的输出

    ](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_05_18.jpg)

    图 5.18:显示数据集内容的输出

    如果你查看输出,你会注意到我们的数据集包含811行,每一行代表一个产品。它还包含107列,第一列是产品代码,然后是52列以W开头,表示每周的销售数量,最后是52列的归一化版本,以Normalized开头。归一化列比绝对销售列W更适合用于 k-means 算法,它将帮助我们的算法更快地找到每个聚类的中心。由于我们将处理归一化列,因此可以删除每个W列和Product_Code列。我们还可以删除MINMAX列,因为它们对我们的聚类没有任何帮助。还要注意,周数从051而不是从152

  3. 接下来,创建一个没有不必要列的新 DataFrame,如以下代码片段所示(数据集的前55列)。你应该使用inplace参数来帮助你:

    df2 = df.drop(df.iloc[:, 0:55], inplace = False, axis = 1)
    

    df2的输出如下所示:

    图 5.19:修改后的 DataFrame

    ](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/app-ai-ws/img/B16060_05_19.jpg)

    drop function of the pandas DataFrame in order to remove the first 55 columns. We also set the inplace parameter to False in order to not remove the column of our original df DataFrame. As a result, we should only have the normalized columns from 0 to 51 in df2 and df should still be unchanged.
    
  4. 创建一个具有8个聚类和random state = 8的 k-means 聚类模型:

    from sklearn.cluster import KMeans
    k_means_model = KMeans(n_clusters=8, random_state=8)
    k_means_model.fit(df2)
    

    我们构建了一个 k-means 模型,除了n_clusters=8random_state=8之外,其他参数都使用默认值,以获得8个聚类并实现可重复的结果。

  5. 从聚类算法中提取标签:

    labels = k_means_model.labels_
    labels
    

    labels的输出将如下所示:

    图 5.20:标签输出数组

    图 5.20:标签输出数组

    从这个输出中很难看出任何意义,但labels的每个索引代表基于相似的周销售趋势,产品被分配到的聚类。现在,我们可以使用这些聚类标签将产品聚集在一起。

  6. 现在,从第一个 DataFrame df中,仅保留W列,并将标签作为新列添加,如以下代码片段所示:

    df.drop(df.iloc[:, 53:], inplace = True, axis = 1)
    df.drop('Product_Code', inplace = True, axis = 1)
    df['label'] = labels
    df
    

    在前面的代码片段中,我们删除了所有不需要的列,并将labels作为新列添加到了 DataFrame 中。

    df的输出将如下所示:

    图 5.21:包含新标签作为新列的更新 DataFrame

    图 5.21:包含新标签作为新列的更新 DataFrame

    现在我们已经有了标签,可以对label列进行聚合,计算每个聚类的年销售平均值。

  7. 执行聚合(使用 pandas 中的groupby函数),以便获得每个聚类的年销售平均值,如以下代码片段所示:

    df_agg = df.groupby('label').sum()
    df_final = df[['label','W0']].groupby('label').count()
    df_final=df_final.rename(columns = {'W0':'count_product'})
    df_final['total_sales'] = df_agg.sum(axis = 1)
    df_final['yearly_average_sales']= \
    df_final['total_sales'] / df_final['count_product']
    df_final.sort_values(by='yearly_average_sales', \
                         ascending=False, inplace = True)
    df_final
    

    在前面的代码片段中,我们首先使用groupby函数和 DataFrame 的sum()方法,计算每个W列和聚类的每个产品的销售总和,并将结果存储在df_agg中。然后,我们在df的单个列(任意选择)上使用groupby函数和count()方法,计算每个聚类的产品总数(注意我们还必须在聚合后重命名W0列)。接下来的步骤是对df_agg中的所有销售列求和,以获得每个聚类的总销售额。最后,我们通过将total_sales除以count_product计算每个聚类的yearly_average_sales。我们还加入了最后一步,通过yearly_average_sales对聚类进行排序。

    df_final的输出将如下所示:

    图 5.22:销售交易数据集的预期输出

图 5.22:销售交易数据集的预期输出

现在,通过这个输出,我们可以看到我们的 k-means 模型成功地将表现相似的产品聚集在一起。我们可以清楚地看到,3号聚类中的115个产品是最畅销的产品,而1号聚类中的123个产品表现非常差。这对于任何企业都是非常有价值的,因为它帮助企业自动识别并将表现相似的产品聚集在一起,而不受产品名称或描述的偏见影响。

注:

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

你还可以在网上运行此示例:packt.live/3hW24Gk

你必须执行整个 Notebook 才能获得期望的结果。

完成这个活动后,你已经学会了如何对多个列进行 k-means 聚类,适用于多个产品。你还学会了聚类在没有标签数据的情况下对企业有多么有用。

活动 5.02: 使用均值迁移算法和凝聚层次聚类对红酒数据进行聚类

解决方案

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 使用sep = ";"加载数据集为 DataFrame,并检查数据:

    import pandas as pd
    import numpy as np
    from sklearn import preprocessing
    from sklearn.cluster import MeanShift
    from sklearn.cluster import AgglomerativeClustering
    from scipy.cluster.hierarchy import dendrogram
    import scipy.cluster.hierarchy as sch
    from sklearn import metrics
    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/'\
               'The-Applied-Artificial-Intelligence-Workshop/'\
               'master/Datasets/winequality-red.csv'
    df = pd.read_csv(file_url,sep=';')
    df
    

    df的输出如下:

    图 5.23: df 显示数据集作为输出

    图 5.23: df 显示数据集作为输出

    注意

    前面截图中的输出被截断了。

    我们的数据集包含1599行,每一行代表一瓶红酒。它还包含12列,最后一列是葡萄酒的质量。我们可以看到其余的 11 列将是我们的特征,我们需要对它们进行缩放,以帮助提高我们模型的准确性和速度。

  3. 从初始的 DataFrame df 创建 featureslabelscaled_features 变量:

    features = df.drop('quality', 1)
    label = df['quality']
    scaled_features = preprocessing.scale(features)
    

    在前面的代码片段中,我们将标签(quality)与特征分开。然后,我们使用来自sklearnpreprocessing.scale函数对特征进行缩放,因为这将提高我们的模型性能。

  4. 接下来,创建一个均值迁移聚类模型,然后获取该模型预测的标签和创建的聚类数量:

    mean_shift_model = MeanShift()
    mean_shift_model.fit(scaled_features)
    n_cluster_mean_shift = len(mean_shift_model.cluster_centers_)
    label_mean_shift = mean_shift_model.labels_
    n_cluster_mean_shift
    

    n_cluster_mean_shift的输出如下:

    10
    

    我们的均值迁移模型已经创建了10个聚类,这已经超过了我们在quality标签中所拥有的组数。这可能会影响我们的外部评分,并可能是一个早期的指标,表明具有相似物理化学性质的葡萄酒不应该属于同一质量组。

    label_mean_shift的输出如下:

    图 5.24: label_mean_shift 的输出数组

    图 5.24: label_mean_shift 的输出数组

    这是一个非常有趣的输出,因为它清楚地显示出我们数据集中大多数葡萄酒非常相似;聚类0中的葡萄酒数量远多于其他聚类。

  5. 现在,在创建树状图并为其选择最佳聚类数量后,创建一个凝聚层次聚类模型:

    dendrogram = sch.dendrogram(sch.linkage(scaled_features, \
                                method='ward'))
    agglomerative_model = \
    AgglomerativeClustering(n_clusters=7, \
                            affinity='euclidean', \
                            linkage='ward')
    agglomerative_model.fit(scaled_features)
    label_agglomerative = agglomerative_model.labels_
    

    dendrogram的输出如下:

    图 5.25: 显示聚类树状图的输出

    图 5.25: 显示聚类树状图的输出

    从这个输出中,我们可以看到七个聚类似乎是我们模型的最佳数量。我们通过在y轴上寻找最低分支和最高分支之间的最大差异来得到这个数字。在我们的案例中,对于七个聚类,最低分支的值为29,而最高分支的值为41

    label_agglomerative的输出如下:

    图 5.26: 显示 label_agglomerative 的数组

    图 5.26: 显示 label_agglomerative 的数组

    我们可以看到我们有一个主要的聚类,1,但不像均值漂移模型中那样显著。

  6. 现在,计算以下两种模型的外部方法得分:

    a. 从调整后的 Rand 指数开始:

    ARI_mean=metrics.adjusted_rand_score(label, label_mean_shift)
    ARI_agg=metrics.adjusted_rand_score(label, label_agglomerative)
    ARI_mean
    

    ARI_mean的输出将如下所示:

    0.0006771608724007207
    

    接下来,输入ARI_agg以获取预期值:

    ARI_agg
    

    ARI_agg的输出将如下所示:

    0.05358047852603172
    

    我们的聚合模型的adjusted_rand_score比均值漂移模型高得多,但两个得分都非常接近0,这意味着两个模型在真实标签的表现上都不太理想。

    b. 接下来,计算调整后的互信息:

    AMI_mean = metrics.adjusted_mutual_info_score(label, \
                                                  label_mean_shift)
    AMI_agg = metrics.adjusted_mutual_info_score(label, \
                                                 label_agglomerative)
    AMI_mean
    

    AMI_mean的输出将如下所示:

    0.004837187596124968
    

    接下来,输入AMI_agg以获取预期值:

    AMI_agg
    

    AMI_agg的输出将如下所示:

    0.05993098663692826
    

    我们的聚合模型的adjusted_mutual_info_score比均值漂移模型高得多,但两个得分都非常接近,V_mean将如下所示:

    0.021907254751144124
    

    接下来,输入V_agg以获取预期值:

    V_agg
    

    V_agg的输出将如下所示:

    0.07549735446050691
    

    我们的聚合模型的 V-Measure 比均值漂移模型高,但两个得分都非常接近,FM_mean将如下所示:

    0.5721233634622408
    

    接下来,输入FM_agg以获取预期值:

    FM_agg
    

    FM_agg的输出将如下所示:

    0.3300681478007641
    

    这一次,我们的均值漂移模型的 Fowlkes-Mallows 得分高于聚合模型,但两个模型的得分仍然处于得分范围的较低位置,这意味着两个模型在真实标签的表现上都不太理想。

    总结而言,通过外部方法评估,我们的两个模型都未能根据葡萄酒的物理化学特性找到包含相似质量的聚类。我们将通过使用内部方法评估来确认这一点,以确保我们的模型聚类已被良好定义,并且能正确地将相似的葡萄酒分组在一起。

  7. 现在,计算以下两种模型的内部方法得分:

    a. 从轮廓系数开始:

    Sil_mean = metrics.silhouette_score(scaled_features, \
                                        label_mean_shift)
    Sil_agg = metrics.silhouette_score(scaled_features, \
                                       label_agglomerative)
    Sil_mean
    

    Sil_mean的输出将如下所示:

    0.32769323700400077
    

    接下来,输入Sil_agg以获取预期值:

    Sil_agg
    

    Sil_agg的输出将如下所示:

    0.1591882574407987
    

    我们的均值漂移模型的轮廓系数(Silhouette Coefficient)高于聚合模型,但两个得分都非常接近,CH_mean将如下所示:

    44.62091774102674
    

    接下来,输入CH_agg以获取预期值:

    CH_agg
    

    CH_agg的输出将如下所示:

    223.5171774491095
    

    我们的聚合模型的 Calinski-Harabasz 指数比均值漂移模型高得多,这意味着聚合模型的聚类更加密集且界限更加明确。

    c. 最后,计算 Davies-Bouldin 指数:

    DB_mean = metrics.davies_bouldin_score(scaled_features, \
                                           label_mean_shift)
    DB_agg = metrics.davies_bouldin_score(scaled_features, \
                                          label_agglomerative)
    DB_mean
    

    DB_mean的输出将如下所示:

    0.8106334674570222
    

    接下来,输入DB_agg以获取预期值:

    DB_agg
    

    DB_agg的输出将如下所示:

    1.4975443816135114
    

    我们的凝聚模型的 David-Bouldin 指数高于均值迁移模型,但两者的分数都接近0,这意味着两者在定义其聚类方面表现良好。

    注意:

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

    你也可以在线运行这个示例,链接:packt.live/2Bs7sAp

    你必须执行整个 Notebook 才能得到期望的结果。

总结来说,通过内在方法评估,我们的两个模型都得到了很好的定义,并且验证了我们对红酒数据集的直觉,即相似的物理化学属性与相似的质量无关。我们还看到,在大多数评分中,凝聚层次模型的表现优于均值迁移模型。

6. 神经网络与深度学习

活动 6.01:为数字数据集找到最佳准确度评分

解决方案

  1. 打开一个新的 Jupyter Notebook 文件。

  2. 导入tensorflow.keras.datasets.mnistmnist

    import tensorflow.keras.datasets.mnist as mnist
    
  3. 使用mnist.load_data()加载mnist数据集,并将结果保存到(features_train, label_train), (features_test, label_test)

    (features_train, label_train), \
    (features_test, label_test) = mnist.load_data()
    
  4. 打印label_train的内容:

    label_train
    

    预期输出是这样的:

    array([5, 0, 4, ..., 5, 6, 8], dtype=uint8)
    

    label列包含与0910个手写数字对应的数值:

  5. 打印训练集的形状:

    features_train.shape
    

    预期输出是这样的:

    (60000, 28, 28)
    

    训练集由60,000个形状为28 x 28的观察值组成。我们需要将输入展平以适应神经网络。

  6. 打印测试集的形状:

    features_test.shape
    

    预期输出是这样的:

    (10000, 28, 28)
    

    测试集由10,000个形状为28 x 28的观察值组成。

  7. 通过将features_trainfeatures_test除以255来进行标准化:

    features_train = features_train / 255.0
    features_test = features_test / 255.0
    
  8. 导入numpynptensorflowtf,以及从tensorflow.keras导入layers

    import numpy as np
    import tensorflow as tf
    from tensorflow.keras import layers
    
  9. 使用np.random_seed()tf.random.set_seed()分别设置 NumPy 和 TensorFlow 的种子为8

    np.random.seed(8)
    tf.random.set_seed(8)
    
  10. 实例化一个tf.keras.Sequential()类并将其保存到一个名为model的变量中:

    model = tf.keras.Sequential()
    
  11. 实例化layers.Flatten(),并设置input_shape=(28,28),然后将其保存到名为input_layer的变量中:

    input_layer = layers.Flatten(input_shape=(28,28))
    
  12. 实例化一个layers.Dense()类,设置128个神经元并使用activation='relu',然后将其保存到名为layer1的变量中:

    layer1 = layers.Dense(128, activation='relu')
    
  13. 实例化第二个layers.Dense()类,设置1个神经元并使用activation='softmax',然后将其保存到名为final_layer的变量中:

    final_layer = layers.Dense(10, activation='softmax')
    
  14. 使用.add()将刚刚定义的三层添加到模型中,并在每一层之间(展平层除外)添加一个layers.Dropout(0.25)层:

    model.add(input_layer)
    model.add(layer1)
    model.add(layers.Dropout(0.25))
    model.add(final_layer)
    
  15. 实例化一个tf.keras.optimizers.Adam()类,学习率为0.001,并将其保存到名为optimizer的变量中:

    optimizer = tf.keras.optimizers.Adam(0.001)
    
  16. 使用.compile()编译神经网络,参数为loss='sparse_categorical_crossentropy', optimizer=optimizer, metrics=['accuracy']

    model.compile(loss='sparse_categorical_crossentropy', \
                  optimizer=optimizer, \
                  metrics=['accuracy'])
    
  17. 使用.summary()打印模型的概述:

    model.summary()
    

    预期输出是这样的:

    图 6.29:模型概述

    图 6.29:模型总结

    该输出总结了我们神经网络的架构。我们可以看到它由四层组成,包括一层展平层,两层密集层和一层 Dropout 层。

  18. 实例化tf.keras.callbacks.EarlyStopping()类,使用monitor='val_loss'patience=5作为学习率,并将其保存为名为callback的变量:

    callback = tf.keras.callbacks.EarlyStopping(monitor='val_loss', \
                                                patience=5)
    
  19. 使用训练集拟合神经网络,并指定epochs=10validation_split=0.2callbacks=[callback]verbose=2

    model.fit(features_train, label_train, epochs=10, \
              validation_split = 0.2, \
              callbacks=[callback], verbose=2)
    

    期望的输出是这样的:

    图 6.30:使用训练集拟合神经网络

图 6.30:使用训练集拟合神经网络

在仅经过10个 epochs 后,我们在训练集上的准确率为0.9825,在验证集上的准确率为0.9779,这些结果非常惊人。在本节中,您学习了如何使用 TensorFlow 从头开始构建并训练神经网络以进行数字分类。

注意

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

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

您必须执行整个 Notebook 才能得到预期的结果。

活动 6.02:使用 CNN 评估 Fashion 图像识别模型

解决方案

  1. 打开一个新的 Jupyter Notebook。

  2. 导入tensorflow.keras.datasets.fashion_mnistfashion_mnist

    import tensorflow.keras.datasets.fashion_mnist as fashion_mnist
    
  3. 使用fashion_mnist.load_data()加载 Fashion MNIST 数据集,并将结果保存到(features_train, label_train), (features_test, label_test)中:

    (features_train, label_train), \
    (features_test, label_test) = fashion_mnist.load_data()
    
  4. 打印训练集的形状:

    features_train.shape
    

    期望的输出是这样的:

    (60000, 28, 28)
    

    训练集由60,000张大小为28*28的图像组成。

  5. 打印测试集的形状:

    features_test.shape
    

    期望的输出是这样的:

    (10000, 28, 28)
    

    测试集由10,000张大小为28*28的图像组成。

  6. 按照以下代码片段的方式,重塑训练集和测试集的维度为(number_rows, 28, 28, 1):

    features_train = features_train.reshape(60000, 28, 28, 1)
    features_test = features_test.reshape(10000, 28, 28, 1)
    
  7. 通过将features_trainfeatures_test除以255来标准化它们:

    features_train = features_train / 255.0
    features_test = features_test / 255.0
    
  8. 导入numpynptensorflowtf,并从tensorflow.keras导入layers

    import numpy as np
    import tensorflow as tf
    from tensorflow.keras import layers
    
  9. 使用np.random_seed()tf.random.set_seed()8设置为numpytensorflow的种子:

    np.random.seed(8)
    tf.random.set_seed(8)
    
  10. 实例化一个tf.keras.Sequential()类,并将其保存为名为model的变量:

    model = tf.keras.Sequential()
    
  11. 使用64个形状为(3,3)的卷积核和activation='relu',以及input_shape=(28,28),实例化layers.Conv2D()并将其保存为名为conv_layer1的变量:

    conv_layer1 = layers.Conv2D(64, (3,3), \
                  activation='relu', input_shape=(28, 28, 1))
    
  12. 使用64个形状为(3,3)的卷积核和activation='relu',实例化layers.Conv2D()并将其保存为名为conv_layer2的变量:

    conv_layer2 = layers.Conv2D(64, (3,3), activation='relu')
    
  13. 使用128个神经元和activation='relu',实例化layers.Flatten(),然后将其保存为名为fc_layer1的变量:

    fc_layer1 = layers.Dense(128, activation='relu')
    
  14. 使用10个神经元和activation='softmax',实例化layers.Flatten(),然后将其保存为名为fc_layer2的变量:

    fc_layer2 = layers.Dense(10, activation='softmax')
    
  15. 使用 .add() 方法将刚才定义的四个层添加到模型中,并在每个卷积层之间添加一个大小为 (2,2)MaxPooling2D() 层:

    model.add(conv_layer1)
    model.add(layers.MaxPooling2D(2, 2))
    model.add(conv_layer2)
    model.add(layers.MaxPooling2D(2, 2))
    model.add(layers.Flatten())
    model.add(fc_layer1)
    model.add(fc_layer2)
    
  16. 实例化一个 tf.keras.optimizers.Adam() 类,学习率为 0.001,并将其保存到名为 optimizer 的变量中:

    optimizer = tf.keras.optimizers.Adam(0.001)
    
  17. 使用 .compile() 编译神经网络,设置 loss='sparse_categorical_crossentropy'optimizer=optimizermetrics=['accuracy']

    model.compile(loss='sparse_categorical_crossentropy', \
                  optimizer=optimizer, metrics=['accuracy'])
    
  18. 使用 .summary() 打印模型概述:

    model.summary()
    

    预期的输出是这样的:

    图 6.31:模型概述

    图 6.31:模型概述

    概述显示该模型需要优化超过 240,000 个参数。

  19. 使用训练集拟合神经网络,并指定 epochs=5validation_split=0.2verbose=2

    model.fit(features_train, label_train, \
              epochs=5, validation_split = 0.2, verbose=2)
    

    预期的输出是这样的:

    图 6.32:使用训练集拟合神经网络

    图 6.32:使用训练集拟合神经网络

    经过 5 个训练周期后,我们在训练集上达到了 0.925 的准确率,在验证集上达到了 0.9042 的准确率。我们的模型有些过拟合。

  20. 评估模型在测试集上的表现:

    model.evaluate(features_test, label_test)
    

    预期的输出是这样的:

    10000/10000 [==============================] - 1s 108us/sample - loss: 0.2746 - accuracy: 0.8976
    [0.27461639745235444, 0.8976]
    

我们在测试集上预测来自 Fashion MNIST 数据集的服装图像时,达到了 0.8976 的准确率。你可以尝试自己提高这个得分并减少过拟合。

注意

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

你也可以在网上运行这个示例,访问 packt.live/2NlM5nd

必须执行整个 Notebook 才能获得预期的结果。

在本次活动中,我们设计并训练了一个用于识别来自 Fashion MNIST 数据集服装图像的 CNN 架构。

活动 6.03:使用 RNN 评估雅虎股票模型

解决方案

  1. 打开一个 Jupyter Notebook。

  2. 导入 pandas 作为 pdnumpy 作为 np

    import pandas as pd
    import numpy as np
    
  3. 创建一个名为 file_url 的变量,包含指向原始数据集的链接:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/'\
               'The-Applied-Artificial-Intelligence-Workshop/'\
               'master/Datasets/yahoo_spx.csv'
    
  4. 使用 pd.read_csv() 加载数据集,并将其保存到名为 df 的新变量中:

    df = pd.read_csv(file_url)
    
  5. 使用 .iloc.values 提取第二列的值,并将结果保存到名为 stock_data 的变量中:

    stock_data = df.iloc[:, 1:2].values
    
  6. sklearn.preprocessing 导入 MinMaxScaler

    from sklearn.preprocessing import MinMaxScaler
    
  7. 实例化 MinMaxScaler() 并将其保存到名为 sc 的变量中:

    sc = MinMaxScaler()
    
  8. 使用 .fit_transform() 标准化数据,并将结果保存到名为 stock_data_scaled 的变量中:

    stock_data_scaled = sc.fit_transform(stock_data)
    
  9. 创建两个空数组,命名为 X_datay_data

    X_data = []
    y_data = []
    
  10. 创建一个名为 window 的变量,它将包含值 30

    window = 30
    
  11. 创建一个从 window 值开始的 for 循环,遍历数据集的长度。在每次迭代中,使用 windowstock_data_scaled 的前几行添加到 X_data 中,并将当前的 stock_data_scaled 值添加到其中:

    for i in range(window, len(df)):
        X_data.append(stock_data_scaled[i - window:i, 0])
        y_data.append(stock_data_scaled[i, 0])
    

    y_data 将包含每天的开盘股票价格,X_data 将包含过去 30 天的股票价格。

  12. X_datay_data转换为 NumPy 数组:

    X_data = np.array(X_data)
    y_data = np.array(y_data)
    
  13. X_data调整形状为(行数,列数,1):

    X_data = np.reshape(X_data, (X_data.shape[0], \
                        X_data.shape[1], 1))
    
  14. 使用前1,000行作为训练数据,并将它们保存到两个变量中,分别叫做features_trainlabel_train

    features_train = X_data[:1000]
    label_train = y_data[:1000]
    
  15. 使用第1,000行之后的行作为测试数据,并将它们保存到两个变量中,分别叫做features_testlabel_test

    features_test = X_data[:1000]
    label_test = y_data[:1000]
    
  16. 导入numpynptensorflowtf,并从tensorflow.keras导入layers

    import numpy as np
    import tensorflow as tf
    from tensorflow.keras import layers
    
  17. 设置8为 NumPy 和 TensorFlow 的seed,使用np.random_seed()tf.random.set_seed()

    np.random.seed(8)
    tf.random.set_seed(8)
    
  18. 实例化一个tf.keras.Sequential()类,并将其保存到一个变量中,名为model

    model = tf.keras.Sequential()
    
  19. 实例化layers.LSTM(),使用50个单元,return_sequences='True',并且input_shape=(X_train.shape[1], 1),然后将其保存到一个变量中,名为lstm_layer1

    lstm_layer1 = layers.LSTM(units=50,return_sequences=True,\
                              input_shape=(features_train.shape[1], 1))
    
  20. 实例化layers.LSTM(),使用50个单元和return_sequences='True',然后将其保存到一个变量中,名为lstm_layer2

    lstm_layer2 = layers.LSTM(units=50,return_sequences=True)
    
  21. 实例化layers.LSTM(),使用50个单元和return_sequences='True',然后将其保存到一个变量中,名为lstm_layer3

    lstm_layer3 = layers.LSTM(units=50,return_sequences=True)
    
  22. 实例化layers.LSTM(),使用50个单元,并将其保存到一个变量中,名为lstm_layer4

    lstm_layer4 = layers.LSTM(units=50)
    
  23. 实例化layers.Dense(),使用1个神经元,并将其保存到一个变量中,名为fc_layer

    fc_layer = layers.Dense(1)
    
  24. 使用.add()方法将你刚才定义的五个层添加到模型中,并在每个 LSTM 层之间添加一个Dropout(0.2)层:

    model.add(lstm_layer1)
    model.add(layers.Dropout(0.2))
    model.add(lstm_layer2)
    model.add(layers.Dropout(0.2))
    model.add(lstm_layer3)
    model.add(layers.Dropout(0.2))
    model.add(lstm_layer4)
    model.add(layers.Dropout(0.2))
    model.add(fc_layer)
    
  25. 实例化一个tf.keras.optimizers.Adam()类,学习率为0.001,并将其保存到一个变量中,名为optimizer

    optimizer = tf.keras.optimizers.Adam(0.001)
    
  26. 使用.compile()编译神经网络,参数为loss='mean_squared_error', optimizer=optimizer, metrics=[mse]

    model.compile(loss='mean_squared_error', \
                  optimizer=optimizer, metrics=['mse'])
    
  27. 使用.summary()打印模型的摘要:

    model.summary()
    

    预期的输出是这样的:

    图 6.33:模型摘要

    图 6.33:模型摘要

    汇总显示我们有超过71,051个参数需要优化。

  28. 使用训练集拟合神经网络,并指定epochs=10, validation_split=0.2, verbose=2

    model.fit(features_train, label_train, epochs=10, \
              validation_split = 0.2, verbose=2)
    

    预期的输出是这样的:

    图 6.34:使用训练集拟合神经网络

    图 6.34:使用训练集拟合神经网络

    经过10个训练周期后,我们在训练集上获得了0.0025的均方误差分数,在验证集上获得了0.0033,说明我们的模型有些过拟合。

  29. 最后,评估模型在测试集上的表现:

    model.evaluate(features_test, label_test)
    

    预期的输出是这样的:

    1000/1000 [==============================] - 0s 279us/sample - loss: 0.0016 - mse: 0.0016
    [0.00158528157370165, 0.0015852816]
    

我们在测试集上获得了0.0017的均方误差分数,这意味着我们可以相当准确地使用过去 30 天的股价数据作为特征来预测雅虎的股价。

注意

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

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

你必须执行整个 Notebook 才能获得期望的结果。

在这项活动中,我们设计并训练了一个 RNN 模型,用于预测基于过去 30 天数据的 Yahoo 股票价格。

posted @ 2025-07-17 15:21  绝不原创的飞龙  阅读(11)  评论(0)    收藏  举报