Python-神经网络项目-全-
Python 神经网络项目(全)
原文:
annas-archive.org/md5/93f3d287f8d9008c40e3bf2960ed77ec译者:飞龙
前言
机器学习和人工智能(AI)已经无处不在,渗透进我们的日常生活中。无论我们去哪里,做什么,我们总是以某种方式与人工智能进行互动。而神经网络和深度学习正在推动这些人工智能的进步。借助神经网络,人工智能系统现在能够在许多领域实现类似人类的表现。
本书为你提供了从零开始创建六个不同神经网络项目的机会。通过这些项目,你将有机会创建一些我们今天常见的人工智能系统,包括人脸识别、情感分析和医学诊断。在每个项目中,我们将提供问题陈述、解决该问题所使用的具体神经网络架构、选择该神经网络的理由以及实现给定解决方案的 Python 代码。
到本书结束时,你将熟练掌握不同的神经网络架构,并用 Python 创建前沿的人工智能项目,这将立即增强你的机器学习作品集。
本书适合的人群
本书非常适合数据科学家、机器学习工程师和深度学习爱好者,他们希望在 Python 中创建实用的神经网络项目。读者应具备一定的 Python 和机器学习基础,以便跟上本书中的练习。
本书内容
第一章,机器学习与神经网络基础,涵盖了机器学习和神经网络的基础知识。第一章的目标是巩固你对机器学习和神经网络的理解。为此,我们将从零开始用 Python 构建我们自己的神经网络,不使用任何机器学习库。
第二章,使用多层感知器预测糖尿病,启动我们的第一个神经网络项目。我们将使用一个基本的神经网络——多层感知器,构建一个分类器,预测一个病人是否有糖尿病风险。
第三章,使用深度前馈网络预测出租车费用,在回归问题中使用深度前馈神经网络。特别地,我们将使用神经网络预测纽约市的出租车费用。
第四章,猫与狗——使用卷积神经网络进行图像分类,使用卷积神经网络(CNN)解决图像分类问题。我们将使用 CNN 来预测一张图像是包含猫还是狗。
第五章,使用自编码器去除图像噪声,利用自编码器进行图像噪声去除。图像来源于被咖啡渍和其他杂物污染的办公文档。我们将使用自编码器去除这些杂物,恢复图像的原始状态。
第六章,使用 LSTM 进行电影评论情感分析,使用长短期记忆(LSTM)神经网络分析和分类在线发布的电影评论的情感。我们将创建一个能够辨别英文句子情感的 LSTM 神经网络。
第七章,使用神经网络实现面部识别系统,使用孪生神经网络来构建一个面部识别系统,可以通过我们笔记本电脑的摄像头识别我们自己的面孔。
第八章,接下来做什么?,总结了我们在本书中所学到的所有内容。我们将展望未来,看看未来几年机器学习和人工智能的发展会是什么样子。
为了充分利用本书
你应该对 Python 编程有一定的基础了解,以便充分利用本书。然而,本书会带你完成项目的每一个步骤,并尽可能多地解释代码。
在硬件方面,你应该在一台相对现代的计算机上运行代码,至少需要 8 GB 的 RAM 和 15 GB 的硬盘空间(用于数据集)。训练深度神经网络需要强大的计算资源,如果你有专用的 GPU,运行速度会显著加快。然而,即使没有 GPU(例如在笔记本电脑上),也完全可以运行代码。在全书中,如果某些代码在没有 GPU 的情况下需要一些时间才能运行,我们会提醒你。
在每一章的开始,我们会告诉你该项目所需的必要 Python 库。为了简化设置过程,我们提供了一个environment.yml文件以及代码。environment.yml文件使你能够轻松地设置一个虚拟环境,其中安装了特定的 Python 版本和所需的库。这样,你可以确保你的代码将在我们设计的标准化虚拟环境中运行。详细的说明将会在第一章《机器学习与神经网络基础》中,设置计算机以进行机器学习部分以及每一章的开始部分提供。
下载示例代码文件
你可以从www.packt.com账户中下载本书的示例代码文件。如果你是从其他地方购买的本书,可以访问www.packt.com/support并注册,让他们直接将文件通过电子邮件发送给你。
你可以按照以下步骤下载代码文件:
-
在www.packt.com登录或注册。
-
选择“支持”标签。
-
点击“代码下载与勘误”。
-
在搜索框中输入书名,并按照屏幕上的说明操作。
文件下载完成后,请确保使用最新版的工具解压或提取文件夹:
-
Windows 版的 WinRAR/7-Zip
-
Mac 版的 Zipeg/iZip/UnRarX
-
Linux 版的 7-Zip/PeaZip
本书的代码包也托管在 GitHub 上,网址为:github.com/PacktPublishing/Neural-Network-Projects-with-Python。如果代码有更新,GitHub 上的现有代码库会进行更新。
我们的丰富书籍和视频目录中还有其他代码包,大家可以在github.com/PacktPublishing/查看,快去看看吧!
下载彩色图片
我们还提供了一份 PDF 文件,其中包含本书使用的截图/图表的彩色图片。你可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781789138900_ColorImages.pdf。
使用的约定
本书中使用了许多文本约定。
CodeInText:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。示例:“我们在这些图片上应用了之前定义的detect_faces函数。”
一段代码设置如下:
def detect_faces(img, draw_box=True):
# convert image to grayscale
grayscale_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
任何命令行输入或输出如下所示:
$ cd Neural-Network-Projects-with-Python
粗体:表示一个新术语、重要词汇,或你在屏幕上看到的词汇。例如,菜单或对话框中的词汇会像这样出现在文本中。
警告或重要说明会像这样展示。
提示和技巧会像这样展示。
联系我们
我们欢迎读者的反馈。
一般反馈:如果你对本书的任何部分有疑问,请在邮件主题中提及书名,并通过customercare@packtpub.com联系我们。
勘误:尽管我们已尽最大努力确保内容的准确性,但错误总是难免。如果你发现本书中的错误,我们将非常感激你能向我们报告。请访问www.packt.com/submit-errata,选择你的书籍,点击“勘误提交表单”链接并填写详细信息。
盗版:如果你在互联网上发现我们作品的任何非法复制品,我们将非常感激你能提供该资料的地址或网站名称。请通过copyright@packt.com与我们联系,并附上该资料的链接。
如果您有兴趣成为作者:如果您在某个领域拥有专业知识,并且有兴趣撰写或参与书籍的创作,请访问 authors.packtpub.com。
评价
请留下评价。阅读并使用本书后,为什么不在您购买的站点上留下评价呢?潜在的读者可以看到并参考您的客观意见来做出购买决策,我们 Packt 能了解您对我们产品的看法,作者们也可以看到您对他们书籍的反馈。谢谢!
有关 Packt 的更多信息,请访问 packt.com。
第一章:机器学习与神经网络入门
人工智能(AI)近年来吸引了大量关注。从智能手机中的人脸识别安全系统到通过 Alexa 预定 Uber 出行,AI 已经无处不在地渗透到我们的日常生活中。然而,我们不断被提醒,AI 的全部潜力尚未被完全实现,AI 将在未来变得更加具有变革性,深刻影响我们的生活。
当我们展望未来时,可以看到 AI 不断进步,并承诺改善我们的日常生活。得益于 AI,自动驾驶汽车正在从科幻走向现实。自动驾驶汽车旨在通过消除人为错误来减少交通事故,从而最终改善我们的生活。类似地,AI 在医疗领域的应用也有望改善治疗效果。特别是,英国的国家卫生服务系统(NHS)已宣布一项雄心勃勃的 AI 项目,用于早期癌症诊断,这可能拯救数千条生命。
AI 的变革性特质使得专家们称其为第四次工业革命。AI 是塑造现代产业的催化剂,拥有 AI 知识在这个新时代至关重要。在本书结束时,你将对驱动 AI 的算法有更深入的理解,并且通过这些前沿算法开发了实际项目。
在本章中,我们将讨论以下主题:
-
机器学习和神经网络入门
-
为机器学习配置计算机环境
-
从头到尾执行你的机器学习项目,使用机器学习工作流
-
在 Python 中从零开始创建自己的神经网络,而不使用任何机器学习库
-
使用 pandas 在 Python 中进行数据分析
-
利用像 Keras 这样的机器学习库来构建强大的神经网络
什么是机器学习?
尽管机器学习和 AI 经常被交替使用,但它们之间存在微妙的区别。AI 这一术语最早出现在 1950 年代,它指的是机器模仿智能人类行为的能力。为了这一目标,研究人员和计算机科学家们尝试了多种方法。AI 的早期工作集中于一种被称为符号 AI 的方法。符号 AI 尝试以声明性形式表达人类知识,使计算机可以处理。符号 AI 的巅峰成果是专家系统,这是一种模拟人类决策过程的计算机系统。
然而,符号 AI 的一个主要缺点是它依赖于人类专家的领域知识,并且需要将这些规则和知识硬编码到问题解决中。AI 作为一个科学领域经历了一段干旱期(被称为 AI 冬天),当时科学家们越来越失望于 AI 的局限性。
虽然符号 AI 在 1950 年代曾占据主导地位,但被称为机器学习的 AI 子领域在背后悄然发展。
机器学习是指计算机使用的算法,通过从数据中学习,使其能够对未来未见过的数据做出预测。
然而,早期的 AI 研究人员并没有太关注机器学习,因为当时的计算机既不够强大,也没有足够的存储能力来存储机器学习算法所需要的海量数据。事实证明,机器学习并没有被冷落太久。在 2000 年代末,AI 经历了复兴,机器学习在其中起到了推动作用。这次复兴的关键原因是计算机系统的成熟,这些系统可以收集并存储大量数据(大数据),并且处理器足够快,可以运行机器学习算法。因此,AI 的“夏天”开始了。
机器学习算法
现在我们已经讨论了什么是机器学习,我们需要理解机器学习算法是如何工作的。机器学习算法大致可以分为两类:
-
监督学习:使用带标签的训练数据,算法学习将输入变量映射到目标变量的规则。例如,一个监督学习算法可以学习如何从温度、时间、季节、气压等输入变量中预测是否会下雨(目标变量)。
-
无监督学习:使用无标签的训练数据,算法学习数据的关联规则。无监督学习算法最常见的应用是聚类分析,算法可以学习数据中那些未明确标记的隐藏模式和群体。
本书将重点讨论监督学习算法。作为一个监督学习算法的具体例子,假设我们遇到以下问题。你是一个动物爱好者和机器学习爱好者,你希望使用监督学习建立一个机器学习算法,预测一个动物是朋友(友好的小狗)还是敌人(危险的熊)。为了简化问题,假设你已经收集了不同品种的狗和熊的两个测量值——它们的体重和速度。收集完数据(称为训练数据集)后,你将它们绘制在图表上,并标注它们的标签(朋友或敌人):

立即,我们可以看到狗的体重往往较轻,通常更快,而熊则较重,通常更慢。如果我们在狗和熊之间画一条线(称为决策边界),我们可以用这条线来进行未来的预测。每当我们接收到一个新动物的测量值时,只需判断它是否位于线的左侧或右侧。朋友在左边,敌人则在右边。
但这是一个简单的数据集。如果我们收集了数百个不同的测量数据呢?那时图表将超过 100 维,人类将无法绘制分隔线。然而,对于机器学习来说,这样的任务并不成问题。
在这个例子中,机器学习算法的任务是学习最佳的决策边界,区分数据集。理想情况下,我们希望算法生成一个完全将两类数据分隔开的决策边界(尽管根据数据集的不同,这并不总是可能的):

有了这个决策边界,我们就可以对未来的未见数据进行预测。如果新实例位于决策边界的左侧,则我们将其分类为朋友。反之,如果新实例位于决策边界的右侧,则我们将其分类为敌人。
在这个简单的例子中,我们只使用了两个输入变量和两个类别。然而,我们可以将问题推广到包含多个输入变量和多个类别的情况。
自然,我们选择的机器学习算法会影响产生的决策边界类型。一些更常见的监督式机器学习算法如下:
-
神经网络
-
线性回归
-
逻辑回归
-
支持向量机(SVMs)
-
决策树
数据集的性质(如图像数据集或数值数据集)以及我们要解决的潜在问题应该决定所使用的机器学习算法。在本书中,我们将重点讨论神经网络。
机器学习工作流
我们已经讨论了什么是机器学习。那么,如何进行机器学习呢?从高层次来看,机器学习项目的核心是将原始数据作为输入,输出预测作为结果。为了实现这一点,有几个重要的中间步骤必须完成。这个机器学习工作流可以通过以下图示来概括:

我们机器学习工作流的输入始终是数据。数据可以来自不同的来源,具有不同的数据格式。例如,如果我们正在进行一个基于计算机视觉的项目,那么我们的数据很可能是图像。对于大多数其他机器学习项目,数据将以表格形式呈现,类似于电子表格。在一些机器学习项目中,数据收集将是一个重要的第一步。在本书中,我们假设数据将由我们提供,使我们可以专注于机器学习方面。
接下来的步骤是预处理数据。原始数据通常杂乱、易出错,并不适合用于机器学习算法。因此,在将数据输入模型之前,我们需要先对其进行预处理。如果数据来自多个来源,我们需要将这些数据合并成一个数据集。机器学习模型也需要数值型的数据集来进行训练。如果原始数据集中有任何类别变量(如性别、国家、星期几等),我们需要将这些变量编码为数值型变量。我们将在本章稍后部分展示如何实现这一点。对于某些机器学习算法,还需要进行数据缩放和归一化。其背后的直觉是,如果某些变量的数值范围远大于其他变量,那么某些机器学习算法可能会错误地将更多的重视放在这些占主导地位的变量上。
现实世界中的数据集通常是杂乱无章的。你会发现数据不完整,并且在多行多列中缺失数据。处理缺失数据有多种方法,每种方法都有其优缺点。最简单的方法是直接丢弃含有缺失数据的行和列。然而,这可能不切实际,因为我们可能会丢弃大量的数据。我们也可以用变量的均值来替代缺失的数据(如果这些变量是数字型的)。这种方法比丢弃数据更理想,因为它能保留我们的数据集。然而,用均值替代缺失值往往会影响数据的分布,这可能会对我们的机器学习模型产生负面影响。另一种方法是根据其他存在的值预测缺失值。然而,我们必须小心,因为这样做可能会引入显著的偏差。
最后,在数据预处理中,我们需要将数据集拆分为训练集和测试集。我们的机器学习模型只会在训练集上进行训练和拟合。一旦我们对模型的表现满意,我们将使用测试集对模型进行评估。需要注意的是,模型绝不应在测试集上进行训练。这确保了模型性能的评估是公正的,并能反映其在现实世界中的表现。
一旦数据预处理完成,我们将进入探索性数据分析(EDA)。EDA 是通过数据可视化从数据中发掘洞察的过程。EDA 使我们能够构造新的特征(称为特征工程),并将领域知识注入到我们的机器学习模型中。
最后,我们进入机器学习的核心部分。在完成数据预处理和 EDA 之后,我们进入模型构建阶段。如前文所述,我们可以使用多种机器学习算法,具体选择哪种算法应由问题的性质决定。在本书中,我们将专注于神经网络。在模型构建中,超参数调优是一个至关重要的步骤,正确的超参数能够大幅提升模型的性能。在后续章节中,我们将探讨神经网络中的一些超参数。训练完模型后,我们终于可以使用测试集来评估我们的模型了。
如我们所见,机器学习的工作流程包含许多中间步骤,每个步骤都对我们模型的整体表现至关重要。使用 Python 进行机器学习的主要优势是,整个机器学习工作流程可以完全在 Python 中端到端地执行,并且只需要使用少数开源库。在本书中,你将体验到如何在机器学习工作流程的每个步骤中使用 Python,并从零开始创建复杂的神经网络项目。
为机器学习配置你的计算机
在深入研究神经网络和机器学习之前,让我们确保你的计算机已经正确设置,这样你就可以顺利运行本书中的代码了。
在本书中,我们将使用 Python 编程语言进行每个神经网络项目的开发。除了 Python 本身,我们还需要一些 Python 库,如 Keras、pandas、NumPy 等等。安装 Python 和所需库有多种方式,但最简单的方式无疑是使用 Anaconda。
Anaconda 是一个免费的开源 Python 及其库的发行版。Anaconda 提供了一个方便的包管理器,让我们能够轻松安装 Python 以及所有其他所需的库。要安装 Anaconda,只需访问网站:www.anaconda.com/distribution/,然后下载 Anaconda 安装程序(选择 Python 3.x 版本的安装程序)。
除了 Anaconda,我们还需要 Git。Git 对于机器学习和软件工程至关重要。Git 使我们能够轻松地从 GitHub 下载代码,而 GitHub 可能是最广泛使用的软件托管服务。要安装 Git,请访问 Git 网站:git-scm.com/book/en/v2/Getting-Started-Installing-Git。你可以直接下载并运行适合你操作系统的安装程序。
一旦安装了 Anaconda 和 Git,我们就可以开始下载本书的代码了。本书中的代码可以在我们的 GitHub 仓库中找到。
要下载代码,只需在命令行中运行以下命令(如果你使用 macOS/Linux,请使用 Terminal;如果你使用 Windows,请使用 Anaconda 命令提示符):
$ git clone https://github.com/PacktPublishing/Neural-Network-Projects-with-Python
git clone 命令将会下载本书中所有的 Python 代码到你的计算机上。
一旦完成,运行以下命令来进入刚刚下载的文件夹:
$ cd Neural-Network-Projects-with-Python
在文件夹中,你会找到一个名为 environment.yml 的文件。有了这个文件,我们可以将 Python 和所有必需的库安装到一个虚拟环境中。你可以把虚拟环境想象成一个隔离的、沙盒式的环境,在这里我们可以安装一个全新的 Python 和所有必需的库。environment.yml 文件包含了 Anaconda 安装每个库特定版本的指令,确保 Python 代码将在我们设计的标准化环境中执行。
要使用 Anaconda 和 environment.yml 文件安装所需的依赖项,只需从命令行执行以下命令:
$ conda env create -f environment.yml
就像这样,Anaconda 将会在 neural-network-projects-python 虚拟环境中安装所有必需的包。要进入这个虚拟环境,我们执行以下命令:
$ conda activate neural-network-projects-python
就这样!我们现在处于一个已安装所有依赖项的虚拟环境中。要在这个虚拟环境中执行 Python 文件,我们可以运行类似以下的命令:
$ python Chapter01\keras_chapter1.py
要离开虚拟环境,我们可以运行以下命令:
$ conda deactivate
请注意,无论何时运行我们提供的任何 Python 代码,你都应该在虚拟环境中(首先运行 conda activate neural-network-projects-python)。
现在我们已经配置好了我们的计算机,让我们回到神经网络。我们将看看神经网络背后的理论,以及如何在 Python 中从头开始编程一个。
神经网络
神经网络是一类机器学习算法,它们受到人脑中神经元的松散启发。然而,不深入讨论大脑类比,我觉得更容易简单地将神经网络描述为一个将给定输入映射到所需输出的数学函数。为了理解这意味着什么,让我们来看看单层神经网络(即感知器)。
感知器可以用以下图示来说明:

感知器的核心是一个简单的数学函数,它接受一组输入,执行一些数学计算,并输出计算结果。在这种情况下,这个数学函数就是这样的:

表示感知器的权重。我们将在接下来的几节中解释神经网络中权重的含义。现在,我们只需记住神经网络简单地是一个将给定输入映射到期望输出的数学函数。
为什么选择神经网络?
在我们深入创建自己的神经网络之前,了解神经网络为何在机器学习和人工智能中占据如此重要的位置是值得的。
第一个原因是神经网络是通用的函数逼近器。这意味着,对于我们要建模的任何任意函数,无论多么复杂,神经网络总是能够表示这个函数。这对于神经网络和人工智能来说具有深远的意义。假设世界上任何问题都可以通过一个数学函数来描述(无论多么复杂),我们可以利用神经网络来表示这个函数,从而有效地建模世界上的任何事物。需要注意的是,尽管科学家们已经证明了神经网络的通用性,但一个庞大且复杂的神经网络可能永远无法正确地训练和泛化。
第二个原因是神经网络的架构具有高度的可扩展性和灵活性。正如我们将在下一节中看到的那样,我们可以轻松地在每个神经网络中堆叠层次,从而增加神经网络的复杂性。或许更有趣的是,神经网络的能力仅受限于我们自己的想象力。通过创造性的神经网络架构设计,机器学习工程师已经学会如何利用神经网络来预测时间序列数据(称为递归神经网络(RNNs)),这些网络被广泛应用于语音识别等领域。近年来,科学家们还证明,通过让两个神经网络相互对抗(称为生成对抗网络(GAN)),我们可以生成与人眼难以区分的逼真图像。
神经网络的基本架构
在这一节中,我们将了解神经网络的基本架构,这是所有复杂神经网络的基础构建模块。我们还将用 Python 从零开始编写自己的基本神经网络,且不依赖任何机器学习库。这个练习将帮助你直观地理解神经网络的内部工作原理。
神经网络由以下几个组件构成:
-
输入层,x
-
任意数量的隐藏层
-
输出层,ŷ
-
每层之间的一组权重和偏置,W 和 b
-
每个隐藏层的激活函数选择,σ
下图展示了一个两层神经网络的架构(请注意,输入层通常在计算神经网络层数时是被排除的):

在 Python 中从零开始训练神经网络
既然我们已经理解了神经网络的基本架构,那么让我们用 Python 从头开始创建自己的神经网络。
首先,我们在 Python 中创建一个 NeuralNetwork 类:
import numpy as np
class NeuralNetwork:
def __init__(self, x, y):
self.input = x
self.weights1 = np.random.rand(self.input.shape[1],4)
self.weights2 = np.random.rand(4,1)
self.y = y
self.output = np.zeros(self.y.shape)
注意,在前面的代码中,我们初始化权重(self.weights1和self.weights2)为一个具有随机值的 NumPy 数组。NumPy 数组用于表示 Python 中的多维数组。我们的权重的确切维度在np.random.rand()函数的参数中指定。对于第一个权重数组的维度,我们使用一个变量(self.input.shape[1])来创建一个具有可变维度的数组,具体取决于我们输入的大小。
一个简单的两层神经网络的输出ŷ如下所示:

您可能会注意到,在前述方程中,权重W和偏置b是影响输出ŷ的唯一变量。
当然,权重和偏置的正确值决定了预测的强度。从输入数据中微调权重和偏置的过程称为神经网络的训练。
训练过程的每一次迭代包括以下步骤:
-
计算预测输出ŷ,即前向传播
-
更新权重和偏置,被称为反向传播
下面的顺序图说明了这个过程:

前向传播
正如我们在前面的顺序图中所看到的,前向传播只是简单的微积分,对于一个基本的两层神经网络,神经网络的输出如下:

让我们在我们的 Python 代码中添加一个feedforward函数来完成这个任务。请注意,为了简单起见,我们假设偏置为0:
import numpy as np
def sigmoid(x):
return 1.0/(1 + np.exp(-x))
class NeuralNetwork:
def __init__(self, x, y):
self.input = x
self.weights1 = np.random.rand(self.input.shape[1],4)
self.weights2 = np.random.rand(4,1)
self.y = y
self.output = np.zeros(self.y.shape)
def feedforward(self):
self.layer1 = sigmoid(np.dot(self.input, self.weights1))
self.output = sigmoid(np.dot(self.layer1, self.weights2))
然而,我们仍然需要一种方法来评估我们预测的准确性(即我们的预测有多大的偏差)。loss函数正是允许我们做到这一点的方法。
loss函数
有许多可用的loss函数,我们问题的性质应该决定我们选择的loss函数。目前,我们将使用简单的平方误差和作为我们的loss函数:

平方误差和就是每个预测值与实际值之间的差异的平方和。差异被平方以便我们可以测量差异的绝对值。
我们在训练中的目标是找到最佳的权重和偏置集,以最小化loss函数。
反向传播
现在我们已经测量了我们预测的误差(损失),我们需要找到一种方法来将误差向后传播,并更新我们的权重和偏置。
为了知道我们应该调整权重和偏置的适当数量,我们需要知道loss函数对于权重和偏置的导数。
从微积分中可以知道,函数的导数就是函数的斜率:

如果我们有导数,就可以通过增大/减小权重和偏置来更新它们(参见前面的图示)。这就是梯度下降。
然而,我们不能直接计算 loss 函数相对于权重和偏置的导数,因为 loss 函数的方程中并不包含权重和偏置。我们需要链式法则来帮助我们计算它。在这一点上,我们不打算深入探讨链式法则,因为它背后的数学相当复杂。此外,像 Keras 这样的机器学习库已经为我们处理了梯度下降,不需要我们从头开始推导链式法则。我们需要了解的关键思想是,一旦我们得到 loss 函数关于权重的导数(斜率),我们就可以相应地调整权重。
现在让我们把 backprop 函数添加到我们的 Python 代码中:
import numpy as np
def sigmoid(x):
return 1.0/(1 + np.exp(-x))
def sigmoid_derivative(x):
return x * (1.0 - x)
class NeuralNetwork:
def __init__(self, x, y):
self.input = x
self.weights1 = np.random.rand(self.input.shape[1],4)
self.weights2 = np.random.rand(4,1)
self.y = y
self.output = np.zeros(self.y.shape)
def feedforward(self):
self.layer1 = sigmoid(np.dot(self.input, self.weights1))
self.output = sigmoid(np.dot(self.layer1, self.weights2))
def backprop(self):
# application of the chain rule to find the derivation of the
# loss function with respect to weights2 and weights1
d_weights2 = np.dot(self.layer1.T, (2*(self.y - self.output) *
sigmoid_derivative(self.output)))
d_weights1 = np.dot(self.input.T, (np.dot(2*(self.y - self.output)
* sigmoid_derivative(self.output), self.weights2.T) *
sigmoid_derivative(self.layer1)))
self.weights1 += d_weights1
self.weights2 += d_weights2
if __name__ == "__main__":
X = np.array([[0,0,1],
[0,1,1],
[1,0,1],
[1,1,1]])
y = np.array([[0],[1],[1],[0]])
nn = NeuralNetwork(X,y)
for i in range(1500):
nn.feedforward()
nn.backprop()
print(nn.output)
请注意,在之前的代码中,我们在前馈函数中使用了 sigmoid 函数。sigmoid 函数是一种激活函数,用来压缩值到 0 和 1 之间。这很重要,因为我们需要将预测值限制在 0 和 1 之间,以解决这个二元预测问题。我们将在下一章第二章《用多层感知机预测糖尿病》中更详细地讲解 sigmoid 激活函数。
综合起来看
现在我们已经有了完整的 Python 代码来进行前馈和反向传播,接下来让我们在一个例子上应用我们的神经网络,看看它的表现如何。
下表包含了四个数据点,每个数据点有三个输入变量(x[1],x[2],x[3])和一个目标变量(Y):
| x[1] | x[2] | x[3] | Y |
|---|---|---|---|
| 0 | 0 | 1 | 0 |
| 0 | 1 | 1 | 1 |
| 1 | 0 | 1 | 1 |
| 1 | 1 | 1 | 0 |
我们的神经网络应该能学到表示这个函数的理想权重集合。需要注意的是,仅仅通过观察我们并不容易算出这些权重。
让我们训练神经网络 1500 次迭代,看看会发生什么。从以下每次迭代的损失图表中,我们可以清晰地看到损失单调递减,趋向最小值。这与我们之前讨论的梯度下降算法是一致的:

让我们看看神经网络在 1500 次迭代后的最终预测(输出):
| 预测 | Y(实际) |
|---|---|
| 0.023 | 0 |
| 0.979 | 1 |
| 0.975 | 1 |
| 0.025 | 0 |
我们做到了!我们的前馈和反向传播算法成功地训练了神经网络,预测结果已经收敛到真实值。
请注意,预测值与实际值之间有些许差异。这是理想的,因为它能防止过拟合,并且使得神经网络能更好地泛化到未见过的数据。
现在我们已经了解了神经网络的内部工作原理,接下来将介绍我们在本书中将使用的 Python 机器学习库。如果你此时觉得从零开始创建自己的神经网络有些困难,不用担心。在本书接下来的部分中,我们将使用那些能够大大简化构建和训练神经网络过程的库。
深度学习与神经网络
那深度学习呢?它与神经网络有何不同?简单来说,深度学习是一种使用神经网络中多个层次进行学习的机器学习算法(也称为深度网络)。虽然我们可以将单层感知机看作是最简单的神经网络,但深度网络则是神经网络在复杂性谱系的另一端。
在深度神经网络(DNN)中,每一层都会学习越来越复杂的信息,然后将其传递给后续的层。例如,当一个 DNN 被训练用于人脸识别时,前几层学习识别人脸的边缘,接着是眼睛等轮廓,最终学习完整的面部特征。
尽管感知机早在 1950 年代就被提出,深度学习直到几年前才真正兴起。过去几个世纪深度学习进展相对缓慢的一个关键原因,主要是由于缺乏数据和计算能力。然而,在过去几年中,我们已经见证了深度学习推动机器学习和人工智能的关键创新。如今,深度学习已成为图像识别、自动驾驶、语音识别和游戏对战的首选算法。那么,过去几年发生了什么变化呢?
近年来,计算机存储变得足够经济实惠,可以收集和存储深度学习所需的大量数据。如今,存储海量数据在云端变得愈加可负担,并且可以通过一组计算机在地球上的任何地方进行访问。随着数据存储的可负担性提升,数据也变得越来越民主化。例如,像 ImageNet 这样的网页为深度学习研究者提供了 1400 万张不同的图像。数据不再是少数特权人士所拥有的商品。
深度学习所需的计算能力也变得更加经济实惠和强大。如今,大多数深度学习工作依赖于图形处理单元(GPU),它们在执行深度神经网络(DNN)所需的计算任务时表现出色。为了进一步推动数据民主化,许多网站也为深度学习爱好者提供免费的 GPU 计算能力。例如,Google Colab 为深度学习提供了免费的 Tesla K80 GPU,任何人都可以使用。
随着这些最新进展,深度学习正在变得对每个人都可用。在接下来的几个部分中,我们将介绍我们将在深度学习中使用的 Python 库。
pandas – Python 中的强大数据分析工具包
pandas 可能是 Python 中用于数据分析的最普及库。pandas 基于强大的 NumPy 库,提供了一个快速且灵活的数据结构,用于处理现实世界的数据集。原始数据通常以表格形式呈现,使用 .csv 文件格式共享。pandas 提供了一个简单的接口,用于将这些 .csv 文件导入到一个被称为 DataFrame 的数据结构中,这使得在 Python 中操作数据变得极其简单。
pandas DataFrame
pandas DataFrame 是二维数据结构,可以将其看作 Excel 中的电子表格。DataFrame 允许我们使用简单的命令轻松导入 .csv 文件。例如,下面的示例代码允许我们导入 raw_data.csv 文件:
import pandas as pd
df = pd.read_csv("raw_data.csv")
一旦数据作为 DataFrame 被导入,我们就可以轻松地对其进行数据预处理。让我们使用鸢尾花数据集来演示。鸢尾花数据集是一个常用的数据集,包含了几种花卉的测量数据(萼片的长度和宽度,花瓣的长度和宽度)。首先,让我们导入由加利福尼亚大学欧文分校(UCI)免费提供的数据集。请注意,pandas 可以直接从网址导入数据集:
URL = \
'https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data'
df = pd.read_csv(URL, names = ['sepal_length', 'sepal_width',
'petal_length', 'petal_width', 'class'])
现在数据已经进入 DataFrame,我们可以轻松地操作数据。首先,让我们获取数据的概述,因为了解我们正在处理什么样的数据总是很重要的:
print(df.info())
输出将如下截图所示:

看起来数据集中有 150 行,包含四个数值列,包含关于 sepal_length 和 sepal_width 的信息,以及关于 petal_length 和 petal_width 的信息。还有一个非数值列,包含关于花卉类别(即物种)的信息。
我们可以通过调用 describe() 函数来快速获取四个数值列的统计概要:
print(df.describe())
输出如下截图所示:

接下来,让我们看一下数据的前 10 行:
print(df.head(10))
输出如下截图所示:

简单吧?pandas 还允许我们轻松地进行数据清洗。例如,我们可以通过以下方法来筛选并选择 sepal_length 大于 5.0 的行:
df2 = df.loc[df['sepal_length'] > 5.0, ]
输出如下截图所示:

loc 命令允许我们访问一组行和列。
pandas 中的数据可视化
EDA 可能是机器学习工作流程中最重要的步骤之一,而 pandas 使得在 Python 中可视化数据变得非常容易。pandas 为流行的 matplotlib 库提供了一个高级 API,使得直接从 DataFrame 构建图表变得轻而易举。
作为一个例子,让我们使用 pandas 可视化 Iris 数据集,以揭示重要的见解。让我们绘制一个散点图来可视化sepal_width与sepal_length的关系。我们可以通过DataFrame.plot.scatter()方法轻松地构建一个散点图,该方法内置于所有 DataFrame 中:
# Define marker shapes by class
import matplotlib.pyplot as plt
marker_shapes = ['.', '^', '*']
# Then, plot the scatterplot
ax = plt.axes()
for i, species in enumerate(df['class'].unique()):
species_data = df[df['class'] == species]
species_data.plot.scatter(x='sepal_length',
y='sepal_width',
marker=marker_shapes[i],
s=100,
title="Sepal Width vs Length by Species",
label=species, figsize=(10,7), ax=ax)
我们将得到一个散点图,如下图所示:

从散点图中,我们可以发现一些有趣的见解。首先,sepal_width和sepal_length之间的关系取决于物种。Setosa(圆点)在sepal_width和sepal_length之间有相当线性的关系,而 versicolor(三角形)和 virginica(星形)通常具有比 Setosa 更大的sepal_length。如果我们正在设计一个机器学习算法来预测花卉的物种类型,我们知道sepal_width和sepal_length是我们模型中需要包括的重要特征。
接下来,让我们绘制一个直方图来研究分布情况。与散点图一致,pandas DataFrame 提供了一个内置方法来使用DataFrame.plot.hist()函数绘制直方图:
df['petal_length'].plot.hist(title='Histogram of Petal Length')
我们可以在以下截图中看到输出结果:

我们可以看到花瓣长度的分布本质上是双峰的。似乎某些物种的花瓣比其他物种更短。我们还可以绘制数据的箱型图。箱型图是数据科学家用来理解数据分布的重要可视化工具,基于第一四分位数、中位数和第三四分位数:
df.plot.box(title='Boxplot of Sepal Length & Width, and Petal Length & Width')
输出结果如下图所示:

从箱型图中,我们可以看到sepal_width的方差远小于其他数值变量,而petal_length具有最大的方差。
我们现在已经看到,使用 pandas 直接可视化数据是多么方便和容易。请记住,EDA(探索性数据分析)是机器学习管道中的一个关键步骤,我们将在本书的每个项目中继续执行此操作。
pandas 中的数据预处理
最后,让我们看看如何使用 pandas 进行数据预处理,特别是编码分类变量和填补缺失值。
编码分类变量
在机器学习项目中,通常会收到包含分类变量的数据集。以下是数据集中分类变量的一些示例:
-
性别:男,女
-
星期:星期一,星期二,星期三,星期四,星期五,星期六,星期天
-
国家:美国,英国,中国,日本
像神经网络这样的机器学习算法无法处理分类变量,因为它们期望的是数值变量。因此,在将这些变量输入到机器学习算法之前,我们需要对其进行预处理。
将这些类别变量转换为数值变量的一种常见方法是使用一种叫做独热编码(one-hot encoding)的技术,这在 pandas 中的get_dummies()函数中得到了实现。独热编码是一种将具有n个类别的类别变量转换为n个独特二进制特征的过程。以下表格展示了一个例子:

本质上,转换后的特征是二进制特征,若它代表原始特征则为1,否则为0。正如你所想的,手动编写这段代码会很麻烦。幸运的是,pandas 提供了一个非常方便的函数来完成这个工作。首先,让我们使用前面的表格数据在 pandas 中创建一个数据框:
df2 = pd.DataFrame({'Day': ['Monday','Tuesday','Wednesday',
'Thursday','Friday','Saturday',
'Sunday']})
我们可以在下面的截图中看到输出:

使用 pandas 进行前述类别特征的独热编码非常简单,只需调用以下函数:
print(pd.get_dummies(df2))
这是输出结果:

填充缺失值
如前所述,填充缺失值是机器学习工作流中的一个关键步骤。现实世界中的数据集通常是杂乱的,并且包含缺失值。大多数机器学习模型,如神经网络,无法处理缺失数据,因此在将数据输入模型之前,我们必须进行数据预处理。pandas 使得处理缺失值变得更加容易。
让我们使用之前的鸢尾花数据集。默认情况下,鸢尾花数据集没有缺失值。因此,为了这个练习,我们必须故意删除一些值。以下代码随机选择数据集中的10行,并删除这10行中的sepal_length值:
import numpy as np
import pandas as pd
# Import the iris data once again
URL = \
'https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data'
df = pd.read_csv(URL, names = ['sepal_length', 'sepal_width',
'petal_length', 'petal_width', 'class'])
# Randomly select 10 rows
random_index = np.random.choice(df.index, replace= False, size=10)
# Set the sepal_length values of these rows to be None
df.loc[random_index,'sepal_length'] = None
让我们使用这个修改后的数据集来看看如何处理缺失值。首先,让我们检查一下缺失值所在的位置:
print(df.isnull().any())
前面的print函数给出的输出如下:

不出所料,pandas 告诉我们在sepal_length列中存在缺失值(即空值)。这个命令对于找出数据集中哪些列包含缺失值非常有用。
处理缺失值的一种方法是简单地删除所有包含缺失值的行。pandas 提供了一个非常方便的dropna函数来实现这一点:
print("Number of rows before deleting: %d" % (df.shape[0]))
df2 = df.dropna()
print("Number of rows after deleting: %d" % (df2.shape[0]))
输出如下所示:

另一种方法是用非缺失的sepal_length值的均值来替换缺失的sepal_length值:
df.sepal_length = df.sepal_length.fillna(df.sepal_length.mean())
使用df.mean()计算均值时,pandas 会自动排除缺失值。
现在让我们确认没有缺失值:

处理完缺失值后,我们可以将数据框传递给机器学习模型。
在神经网络项目中使用 pandas
我们已经看到如何使用 pandas 导入 .csv 格式的表格数据,并直接使用 pandas 中的内置函数进行数据预处理和数据可视化。在本书的其余部分,当数据集是表格形式时,我们将使用 pandas。正如我们将在未来章节中看到的,pandas 在数据预处理和 EDA 中扮演着至关重要的角色。
TensorFlow 和 Keras – 开源深度学习库
TensorFlow 是一个由 Google Brain 团队开发的开源神经网络和深度学习库。TensorFlow 旨在具备可扩展性,可以在各种平台上运行,从桌面到移动设备,甚至是计算机集群。如今,TensorFlow 是最流行的机器学习库之一,并广泛应用于各种现实世界的应用中。例如,TensorFlow 驱动着我们今天使用的许多在线服务的 AI,包括图像搜索、语音识别和推荐引擎。TensorFlow 已经成为许多 AI 应用程序背后的“默默工作者”,即使我们可能都没有注意到它的存在。
Keras 是一个运行在 TensorFlow 之上的高级 API。那么,为什么是 Keras?为什么我们需要另一个库作为 TensorFlow 的 API?简单来说,Keras 去除了构建神经网络的复杂性,并使快速实验和测试变得容易,而无需让用户关注低级实现细节。Keras 为使用 TensorFlow 构建神经网络提供了一个简单直观的 API。它的指导原则是模块化和可扩展性。正如我们稍后将看到的,通过将 Keras API 调用按顺序堆叠起来,构建神经网络非常简单,可以像堆积乐高积木一样,创建更大的结构。这种对初学者友好的方法使得 Keras 成为了 Python 中最流行的机器学习库之一。在本书中,我们将使用 Keras 作为构建神经网络项目的主要机器学习库。
Keras 中的基本构建模块
Keras 中的基本构建模块是层,我们可以将这些层按线性方式堆叠起来以创建一个模型。我们选择的损失函数将提供训练模型时使用的度量标准,而训练时使用的将是优化器。回想一下,在之前从头构建神经网络时,我们必须定义并编写这些术语的代码。我们称这些为 Keras 中的基本构建模块,因为我们可以使用这些基本结构来构建任何神经网络。
以下图表展示了 Keras 中这些构建模块之间的关系:

层 – Keras 中神经网络的基本单元
你可以将 Keras 中的层看作原子,因为它们是我们神经网络中最小的单位。每一层接收输入,执行数学函数,然后将结果输出给下一层。Keras 中的核心层包括全连接层、激活层和丢弃层。还有一些更复杂的层,包括卷积层和池化层。在本书中,你将接触到使用所有这些层的项目。
现在,让我们更详细地了解一下全连接层,这是 Keras 中最常用的层。全连接层也叫做密集层。之所以称为全连接,是因为它使用了所有的输入(而不是部分输入)来执行它实现的数学函数。
全连接层实现以下功能:

是输出,
是激活函数,
是输入,
和
分别是权重和偏置。
这个方程式对你来说应该很熟悉。我们在之前从头构建神经网络时,使用了全连接层。
模型 – 层的集合
如果将层看作是原子,那么在 Keras 中,模型可以看作是分子。模型只是层的集合,Keras 中最常用的模型是Sequential模型。Sequential模型允许我们将层线性堆叠,其中每一层只与上一层连接。这使得我们能够轻松设计模型架构,而无需担心底层的数学原理。正如我们将在后面的章节中看到的,确保连续层维度兼容是需要大量思考的事情,而 Keras 已经在幕后为我们解决了这个问题!
一旦定义了我们的模型架构,就需要定义训练过程,使用 Keras 中的compile方法来完成。compile方法接受多个参数,但我们需要定义的最重要参数是优化器和损失函数。
损失函数 – 神经网络训练的误差度量
在前面的章节中,我们定义了损失函数作为评估我们预测结果好坏(即预测结果与实际结果的差距)的方式。我们使用的损失函数应该由问题的性质决定。Keras 中实现了多种损失函数,但最常用的损失函数是mean_squared_error、categorical_crossentropy和binary_crossentropy。
一般来说,选择损失函数的原则如下:
-
如果问题是回归问题,使用
mean_squared_error -
如果问题是多分类问题,使用
categorical_crossentropy -
如果问题是二分类问题,则使用
binary_crossentropy
在某些情况下,您可能会发现 Keras 中的默认损失函数不适合您的问题。在这种情况下,您可以通过在 Python 中定义一个自定义函数来定义自己的损失函数,然后将该自定义函数传递给 Keras 中的 compile 方法。
优化器 – 神经网络的训练算法
优化器是用于在训练过程中更新神经网络权重的算法。Keras 中的优化器基于梯度下降算法,我们在前面的章节中已做过讲解。
尽管我们不会详细讲解每个优化器之间的差异,但需要注意的是,我们选择优化器的依据应取决于问题的性质。一般而言,研究人员发现 Adam 优化器最适合深度神经网络(DNN),而 sgd 优化器则最适合浅层神经网络。Adagrad 优化器也是一个流行的选择,它根据特定权重更新的频率来调整算法的学习率。这个方法的主要优点是,它消除了手动调整学习率超参数的需要,这是机器学习流程中的一项耗时工作。
在 Keras 中创建神经网络
让我们来看一下如何使用 Keras 构建我们之前介绍的两层神经网络。为了构建一个线性堆叠的层,首先在 Keras 中声明一个 Sequential 模型:
from keras.models import Sequential
model = Sequential()
这将创建一个空的 Sequential 模型,我们现在可以向其中添加层。在 Keras 中添加层非常简单,类似于将乐高积木一个个叠加起来。我们从左边开始添加层(即离输入最近的层):
from keras.layers import Dense
# Layer 1
model.add(Dense(units=4, activation='sigmoid', input_dim=3))
# Output Layer
model.add(Dense(units=1, activation='sigmoid'))
在 Keras 中堆叠层就像调用 model.add() 命令一样简单。注意,我们必须定义每层的单元数量。通常,增加单元的数量会增加模型的复杂性,因为这意味着需要训练更多的权重。对于第一层,我们必须定义 input_dim。这告诉 Keras 数据集中的特征数量(即列数)。此外,请注意,我们使用了 Dense 层。Dense 层仅仅是一个全连接层。在后续章节中,我们将介绍其他类型的层,专门用于不同类型的问题。
我们可以通过调用 model.summary() 函数来验证模型的结构:
print(model.summary())
输出结果如下所示:

参数的数量是我们需要为刚定义的模型训练的权重和偏置的数量。
一旦我们对模型的架构感到满意,就可以编译它并开始训练过程:
from keras import optimizers
sgd = optimizers.SGD(lr=1)
model.compile(loss='mean_squared_error', optimizer=sgd)
请注意,我们已将 sgd 优化器的学习率设置为 1.0(lr=1)。一般来说,学习率是神经网络的一个超参数,需要根据具体问题仔细调整。在后续章节中,我们将详细探讨如何调整超参数。
Keras 中的 mean_squared_error 损失函数类似于我们之前定义的平方和误差。我们正在使用 SGD 优化器来训练模型。回顾一下,梯度下降法是通过将权重和偏差朝着损失函数相对于权重和偏差的导数方向更新的方式来优化模型。
让我们使用之前使用过的数据来训练我们的神经网络。这将使我们能够比较使用 Keras 获得的预测结果与我们之前从零开始创建神经网络时获得的预测结果。
让我们定义一个 X 和 Y 的 NumPy 数组,分别对应特征和目标变量:
import numpy as np
# Fixing a random seed ensures reproducible results
np.random.seed(9)
X = np.array([[0,0,1],
[0,1,1],
[1,0,1],
[1,1,1]])
y = np.array([[0],[1],[1],[0]])
最后,让我们训练模型 1500 次迭代:
model.fit(X, y, epochs=1500, verbose=False)
要获取预测结果,请在我们的数据上运行 model.predict() 命令:
print(model.predict(X))
上述代码的输出如下:

将此与我们之前获得的预测结果进行比较,可以看到结果非常相似。使用 Keras 的主要优势是,我们在构建神经网络时无需担心底层实现细节和数学问题,这与我们之前的做法不同。实际上,我们完全没有进行任何数学运算。在 Keras 中,我们所做的只是调用一系列 API 来构建神经网络。这使我们能够专注于高层次的细节,从而实现快速实验。
其他 Python 库
除了 pandas 和 Keras,我们还将使用其他 Python 库,如 scikit-learn 和 seaborn。scikit-learn 是一个开源的机器学习库,在机器学习项目中被广泛使用。我们在 scikit-learn 中使用的主要功能是在数据预处理时将数据分为训练集和测试集。seaborn 是 Python 中的另一种数据可视化工具,近年来越来越受到关注。在后续章节中,我们将看到如何使用 seaborn 来进行数据可视化。
总结
在本章中,我们了解了什么是机器学习,并查看了每个机器学习项目的完整端到端工作流程。我们还了解了什么是神经网络和深度学习,并从零开始在 Keras 中编写了我们自己的神经网络。
在本书的剩余部分,我们将创建自己的实际神经网络项目。每一章将涵盖一个项目,项目的复杂性按顺序递增。到书的最后,你将完成自己的神经网络项目,包括医学诊断、出租车费用预测、图像分类、情感分析等更多内容。在下一章,第二章,使用多层感知器预测糖尿病,我们将介绍使用多层感知器(MLP)进行糖尿病预测。让我们开始吧!
第二章:使用多层感知器预测糖尿病
在第一章中,我们讲解了神经网络的内部工作原理,如何使用 Python 库(如 Keras)构建自己的神经网络,以及端到端的机器学习工作流程。在本章中,我们将应用所学的内容,构建一个多层感知器(MLP),用于预测患者是否有糖尿病风险。这是我们从零开始构建的第一个神经网络项目。
在本章中,我们将涵盖以下主题:
-
理解我们要解决的问题——糖尿病
-
人工智能今天在医疗保健中的应用,以及人工智能将如何继续变革医疗保健
-
对糖尿病数据集的深入分析,包括使用 Python 进行数据可视化
-
理解 MLP,以及我们将使用的模型架构
-
使用 Keras 实现并训练 MLP 的逐步指南
-
结果分析
技术要求
本章所需的关键 Python 库如下:
-
matplotlib 3.0.2
-
pandas 0.23.4
-
Keras 2.2.4
-
NumPy 1.15.2
-
seaborn 0.9.0
-
scikit-learn 0.20.2
要下载本项目所需的数据集,请参考raw.githubusercontent.com/PacktPublishing/Neural-Network-Projects-with-Python/master/Chapter02/how_to_download_the_dataset.txt中的说明。
本章的代码可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Neural-Network-Projects-with-Python。
要将代码下载到您的计算机中,可以运行以下git clone命令:
$ git clone https://github.com/PacktPublishing/Neural-Network-Projects-with-Python.git
完成该过程后,将会有一个名为Neural-Network-Projects-with-Python的文件夹。运行以下命令进入该文件夹:
$ cd Neural-Network-Projects-with-Python
要在虚拟环境中安装所需的 Python 库,请运行以下命令:
$ conda env create -f environment.yml
请注意,您需要先在计算机上安装 Anaconda,然后才能运行此命令。要进入虚拟环境,请运行以下命令:
$ conda activate neural-network-projects-python
通过运行以下命令进入Chapter02文件夹:
$ cd Chapter02
以下文件位于该文件夹中:
-
main.py:这是神经网络的主代码。 -
utils.py:此文件包含辅助工具代码,帮助我们实现神经网络。 -
visualize.py:此文件包含用于探索性数据分析和数据可视化的代码。
要运行神经网络的代码,只需执行main.py文件:
$ python main.py
要重新创建本章中介绍的数据可视化内容,请执行visualize.py文件:
$ python visualize.py
糖尿病 – 理解问题
糖尿病是一种与体内血糖水平升高相关的慢性疾病。糖尿病常导致心血管疾病、中风、肾脏损害以及四肢(即四肢和眼睛)的长期损伤。
全球估计有 4.15 亿人患有糖尿病,每年约有 500 万人死于糖尿病相关并发症。在美国,糖尿病被认为是第七大死亡原因。显然,糖尿病已经成为现代社会福祉的一个严重问题。
糖尿病可以分为两种亚型:1 型和 2 型。1 型糖尿病是由于身体无法产生足够的胰岛素所导致的。与 2 型糖尿病相比,1 型糖尿病相对较为罕见,仅占糖尿病患者的约 5%。不幸的是,1 型糖尿病的确切原因尚不清楚,因此很难预防 1 型糖尿病的发生。
2 型糖尿病是由于身体对胰岛素的逐渐抵抗引起的。2 型糖尿病是全球最为常见的糖尿病类型,其主要原因包括超重、不规律的运动和不良的饮食习惯。幸运的是,如果早期诊断,2 型糖尿病是可以预防和逆转的。
早期检测和诊断糖尿病的障碍之一是糖尿病的早期阶段往往没有明显症状。处于糖尿病前期(即预糖尿病)的人们通常在为时已晚时才意识到自己已经患有糖尿病。
我们如何利用机器学习解决这一问题?如果我们拥有一个标注的数据集,其中包含一些患者的重要指标(例如,年龄和血液胰岛素水平),以及标明患者在这些指标采集后某个时刻是否发生糖尿病的真实标签,那么我们可以基于这些数据训练神经网络(机器学习分类器),并将其应用于对新患者的预测:

在接下来的部分,我们将简要探讨人工智能如何改变医疗行业。
医疗行业中的人工智能
除了利用机器学习预测糖尿病,整个医疗健康领域也正迎来人工智能的颠覆。根据埃森哲的一项研究,预计到 2021 年,人工智能在医疗行业的市场将迎来爆炸性增长,年复合增长率预计为 40%。这一显著增长主要受到人工智能和科技公司在医疗领域扩展的推动。
苹果公司首席执行官蒂姆·库克(Tim Cook)相信,苹果能够在医疗保健领域做出重大贡献。苹果对颠覆医疗保健的愿景可以通过其在可穿戴技术方面的进展来体现。2018 年,苹果发布了新一代智能手表,具有主动监测心血管健康的功能。苹果的智能手表现在可以实时进行心电图检查,甚至在心率异常时发出警告,这是心血管衰竭的早期征兆。苹果的智能手表还收集加速度计和陀螺仪的数据,以实时预测是否发生了重大跌倒。显然,人工智能对医疗保健的影响将是深远的。
人工智能在医疗保健中的价值不在于取代医生和其他医疗工作者,而在于增强他们的工作。人工智能有潜力在患者整个就医过程中支持医疗工作者,并通过数据帮助医疗工作者发现有关患者健康的见解。专家认为,人工智能在医疗保健领域的增长将集中在以下几个领域:

自动化诊断
让我们聚焦于自动化诊断,因为这是本项目关注的领域。专家们认为,人工智能将大大增强医学诊断的方式。目前,大多数医学诊断是由熟练的医疗专家进行的。在影像学诊断(如 X 光和 MRI 扫描)中,需要熟练的放射科医生提供诊断专业知识。这些经验丰富的医疗专家需要经过多年严格的培训才能获得认证,且某些国家的这类专家稀缺,导致了较差的诊疗效果。人工智能的作用是增强这些专家的能力,并卸载低级的日常诊断工作,这些工作可以由人工智能高效且准确地完成。
这与我们最初的问题陈述相呼应:使用人工智能预测哪些患者有糖尿病风险。正如我们将看到的,我们可以使用机器学习和神经网络来进行这一预测。在本章中,我们将设计并实现一个多层感知器(MLP),利用机器学习来预测糖尿病的发生。
糖尿病数据集
我们将用于本项目的数据集来自库马印第安人糖尿病数据集,由美国国家糖尿病、消化与肾脏疾病研究所提供(并由 Kaggle 托管)。
库马印第安人是生活在亚利桑那州的一群美洲土著人,由于他们遗传上容易患糖尿病,这群人得到了大量的研究。人们认为,库马印第安人携带一种基因,使得他们能够在长时间饥荒中生存。这种节俭基因使得库马印第安人能够将他们摄入的葡萄糖和碳水化合物储存在体内,这在经常发生饥荒的环境中具有遗传优势。
然而,随着社会的现代化,皮马印第安人开始改变饮食,转向加工食品,他们的 2 型糖尿病发病率也开始上升。如今,皮马印第安人中 2 型糖尿病的发病率是世界最高的。这使得他们成为一个高度研究的群体,研究人员试图找到皮马印第安人中糖尿病的遗传关联。
皮马印第安人糖尿病数据集包含从一组女性皮马印第安人样本中收集的诊断测量数据,以及一个标签,指示患者是否在初次测量后的五年内发展为糖尿病。在下一部分中,我们将对皮马印第安人糖尿病数据集进行探索性数据分析,以揭示数据中的重要洞察。
探索性数据分析
让我们深入数据集,以了解我们正在处理的数据类型。我们将数据集导入 pandas:
import pandas as pd
df = pd.read_csv('diabetes.csv')
让我们快速查看数据集的前五行,通过调用df.head()命令:
print(df.head())
我们得到以下输出:

看起来数据集有九列,具体如下:
-
Pregnancies:之前怀孕的次数 -
Glucose:血浆葡萄糖浓度 -
BloodPressure:舒张压 -
SkinThickness:从三头肌测量的皮肤褶皱厚度 -
Insulin:血清胰岛素浓度 -
BMI:身体质量指数 -
DiabetesPedigreeFunction:一个总结性的分数,表示患者的糖尿病遗传易感性,基于患者的家族糖尿病记录推算得出 -
Age:年龄(以年为单位) -
Outcome:我们试图预测的目标变量,1表示患者在初次测量后的五年内发展为糖尿病,0表示其他情况
让我们从可视化数据集中的九个变量的分布开始。我们可以通过绘制直方图来实现:
from matplotlib import pyplot as plt
df.hist()
plt.show()
我们得到以下输出:

直方图提供了一些关于数据的有趣洞察。从Age的直方图中,我们可以看到大多数数据来自年轻人,最常见的年龄段是在 20 至 30 岁之间。我们还可以看到,BMI、BloodPressure和Glucose浓度的分布呈正态分布(即钟形曲线),这也是我们在从一个群体中收集这些统计数据时所期望的。然而,注意到Glucose浓度的分布尾部出现了一些极端值。看起来有些人的血浆Glucose浓度接近 200。分布的另一端,我们可以看到有些人BMI、BloodPressure和Glucose的值为 0。从逻辑上讲,我们知道这些测量值不可能为 0。这些是缺失值吗?我们将在下一部分的数据预处理章节中进一步探讨。
如果我们查看 Pregnancies(怀孕次数)这一变量的分布,我们也可以看到一些异常值。我们可以看到有些患者曾有超过 15 次的怀孕历史。虽然这可能并不完全令人惊讶,但在分析时我们应考虑这些异常值,因为它们可能会影响我们的结果。
结果分布显示,大约 65% 的人群属于类别 0(没有糖尿病),而其余 35% 属于类别 1(糖尿病)。在构建机器学习分类器时,我们应该始终牢记训练数据中类别的分布。为了确保我们的机器学习分类器在现实世界中表现良好,我们应该确保训练数据中的类别分布与现实世界相匹配。在这种情况下,类别的分布与现实世界并不匹配,因为据世界卫生组织(WHO)估计,全球仅有 8.5% 的人口患有糖尿病。
对于这个项目,我们不需要担心训练数据中类别的分布,因为我们并不会将分类器部署到现实世界中。然而,对于数据科学家和机器学习工程师来说,检查训练数据中类别的分布是一种良好的做法,以确保模型在现实世界中的表现。
最后,重要的是要注意这些变量的尺度不同。例如,DiabetesPedigreeFunction 变量的范围是从 0 到 ~2.5,而 Insulin 变量的范围是从 0 到 ~800。尺度的差异可能会导致训练神经网络时出现问题,因为尺度较大的变量往往会主导尺度较小的变量。在接下来的数据预处理部分,我们将讨论如何对变量进行标准化。
我们还可以绘制密度图来调查每个变量与目标变量之间的关系。为此,我们将使用 seaborn。seaborn 是一个基于 matplotlib 的 Python 数据可视化库。
以下代码片段展示了如何为每个变量绘制密度图。为了可视化糖尿病患者与非糖尿病患者之间分布的差异,我们还将在每个图上分别绘制它们:
import seaborn as sns
# create a subplot of 3 x 3
plt.subplots(3,3,figsize=(15,15))
# Plot a density plot for each variable
for idx, col in enumerate(df.columns):
ax = plt.subplot(3,3,idx+1)
ax.yaxis.set_ticklabels([])
sns.distplot(df.loc[df.Outcome == 0][col], hist=False, axlabel= False,
kde_kws={'linestyle':'-',
'color':'black', 'label':"No Diabetes"})
sns.distplot(df.loc[df.Outcome == 1][col], hist=False, axlabel= False,
kde_kws={'linestyle':'--',
'color':'black', 'label':"Diabetes"})
ax.set_title(col)
# Hide the 9th subplot (bottom right) since there are only 8 plots
plt.subplot(3,3,9).set_visible(False)
plt.show()
我们将得到如下截图所示的输出:

以下截图显示的是接续前一个截图的输出:

前面的密度图看起来比较复杂,但让我们集中分析每个单独的图,看看能从中获得哪些洞察。如果我们查看Glucose变量的图,我们可以看到,在非糖尿病患者(实线)中,曲线呈正态分布,集中在值 100 左右。这告诉我们,在非糖尿病患者中,大多数人的血糖值为 100 mg/dL。另一方面,如果我们查看糖尿病患者(虚线),曲线较宽,集中在值 150 左右。这告诉我们,糖尿病患者的血糖值范围较宽,且平均血糖值大约为 150 mg/dL。因此,糖尿病与非糖尿病患者之间的血糖值有显著差异。类似的分析也可以用于BMI和Age变量。换句话说,Glucose、BMI和Age变量是糖尿病的强预测因子。糖尿病患者往往有更高的血糖值、更高的 BMI,并且年纪较大。
另一方面,我们可以看到,对于像BloodPressure和SkinThickness这样的变量,糖尿病患者和非糖尿病患者在分布上没有显著差异。这两组人群的血压和皮肤厚度值相似。因此,BloodPressure和SkinThickness是糖尿病预测的较差指标。
数据预处理
在前一节探索性数据分析中,我们发现某些列中存在0值,表明存在缺失值。我们还看到这些变量的尺度不同,这可能会对模型性能产生负面影响。在本节中,我们将进行数据预处理以处理这些问题。
处理缺失值
首先,让我们调用isnull()函数来检查数据集中是否有缺失值:
print(df.isnull().any())
我们将看到以下输出:

看起来数据集中没有缺失值,但我们确定吗?让我们获取数据集的统计摘要来进一步调查:
print(df.describe())
输出结果如下:

我们可以看到数据集中有768行数据,且Pregnancies、Glucose、BloodPressure、SkinThickness、Insulin和BMI列的最小值为0。这不太合理。Glucose、BloodPressure、SkinThickness、Insulin和BMI的测量值不应为0。这表明数据集中存在缺失值。值被记录为0可能是由于数据收集过程中出现了一些问题。也许设备故障,或者患者不愿意接受测量。
无论如何,我们需要处理这些0值。让我们看一下每列中有多少0值,以了解问题的严重程度:
print("Number of rows with 0 values for each variable")
for col in df.columns:
missing_rows = df.loc[df[col]==0].shape[0]
print(col + ": " + str(missing_rows))
我们得到以下结果:

在Insulin列中,有374行值为0。这几乎占据了我们数据的一半!显然,我们不能丢弃这些0值的行,因为那样会导致模型性能显著下降。
有几种技术可以处理这些缺失值:
-
删除(丢弃)任何包含缺失值的行。
-
使用非缺失值的均值/中位数/众数来替换缺失值。
-
使用一个单独的机器学习模型预测实际值。
由于缺失值来自于连续变量,如Glucose、BloodPressure、SkinThickness、Insulin和BMI,我们将用非缺失值的均值来替换缺失值。
首先,让我们将Glucose、BloodPressure、SkinThickness、Insulin和BMI列中的0值替换为NaN。这样,pandas 就能理解这些值是无效的:
import numpy as np
df['Glucose'] = df['Glucose'].replace(0, np.nan)
df['BloodPressure'] = df['BloodPressure'].replace(0, np.nan)
df['SkinThickness'] = df['SkinThickness'].replace(0, np.nan)
df['Insulin'] = df['Insulin'].replace(0, np.nan)
df['BMI'] = df['BMI'].replace(0, np.nan)
现在让我们确认Glucose、BloodPressure、SkinThickness、Insulin和BMI列中不再包含0值:
print("Number of rows with 0 values for each variable")
for col in df.columns:
missing_rows = df.loc[df[col]==0].shape[0]
print(col + ": " + str(missing_rows))
我们得到了以下结果:

注意,我们没有修改Pregnancies列,因为该列中的0值(即没有怀孕)是完全有效的。
现在,让我们将NaN值替换为非缺失值的均值。我们可以使用 pandas 中便捷的fillna()函数来完成此操作:
df['Glucose'] = df['Glucose'].fillna(df['Glucose'].mean())
df['BloodPressure'] = df['BloodPressure'].fillna(df['BloodPressure'].mean())
df['SkinThickness'] = df['SkinThickness'].fillna(df['SkinThickness'].mean())
df['Insulin'] = df['Insulin'].fillna(df['Insulin'].mean())
df['BMI'] = df['BMI'].fillna(df['BMI'].mean())
数据标准化
数据标准化是数据预处理中的另一项重要技术。数据标准化的目标是将数值变量转换,使得每个变量的均值为 0,方差为 1。
作为预处理步骤的变量标准化是许多机器学习算法的要求。在神经网络中,标准化数据非常重要,以确保反向传播算法按预期工作。数据标准化的另一个积极效果是它缩小了变量的量级,将它们转换为更加成比例的尺度。
正如我们之前看到的,像Insulin和DiabetesPedigreeeFunction这样的变量具有非常不同的量纲;Insulin的最大值为846,而DiabetesPedigreeeFunction的最大值仅为2.42。在如此不同的量纲下,量纲较大的变量在训练神经网络时往往会占主导地位,从而导致神经网络无意中对量纲较大的变量给予更多的关注。
为了标准化数据,我们可以使用来自 scikit-learn 的preprocessing类。让我们从 scikit-learn 导入preprocessing类,并使用它来对数据进行缩放:
from sklearn import preprocessing
df_scaled = preprocessing.scale(df)
由于preprocessing.scale()函数返回的对象不再是 pandas DataFrame,我们需要将其转换回:
df_scaled = pd.DataFrame(df_scaled, columns=df.columns)
最后,由于我们不想对Outcome列(即我们要预测的目标变量)进行标准化,因此我们将使用原始的Outcome列:
df_scaled['Outcome'] = df['Outcome']
df = df_scaled
让我们查看每个转换后变量的均值、标准差和最大值:
print(df.describe().loc[['mean', 'std','max'],].round(2).abs())
我们得到以下结果:

我们可以看到,每个变量的尺度现在更接近彼此。
将数据分为训练集、测试集和验证集
数据预处理的最后一步是将数据分为训练集、测试集和验证集:
-
训练集:神经网络将在这个数据子集上进行训练。
-
验证集:这一数据集允许我们使用无偏的数据源进行超参数调优(即调节隐藏层的数量)。
-
测试集:神经网络的最终评估将基于这个数据子集。
将数据分为训练集、测试集和验证集的目的是避免过拟合,并为评估模型性能提供一个无偏的数据来源。通常,我们会使用训练集和验证集来调整和改进我们的模型。验证集可以用于训练的早停,即我们只在验证集上的模型性能停止提高时继续训练神经网络。这可以帮助我们避免神经网络的过拟合。
测试集也被称为保留数据集,因为神经网络永远不会使用它进行训练。相反,我们将在最后使用测试集来评估模型。这为我们提供了一个准确反映模型在实际世界中表现的标准。
我们如何决定每个分割的比例?在这种情况下,竞争的关注点是,如果我们将大部分数据分配用于训练,模型的性能会提升,但可能会牺牲我们避免过拟合的能力。同样,如果我们将大部分数据分配用于验证和测试,模型性能会下降,因为可能没有足够的数据用于训练。
一般经验法则是,我们应将原始数据分割为 80%的训练集和 20%的测试集,然后将训练数据再分割为 80%的训练集和 20%的验证集。下图展示了这一过程:

一个重要的要点是,数据的分割必须是随机的。如果我们使用非随机的方法进行数据分割(例如,将前 80%的行分配到训练集,将后 20%的行分配到测试集),可能会引入偏差。例如,原始数据可能按时间顺序排序,因此使用非随机的分割方法可能意味着我们的模型只会在某个日期的数据上进行训练,这样的偏差非常大,并且在现实世界中表现不佳。
train_test_split函数来自 scikit-learn,它可以轻松地实现数据集的随机分割。
首先,让我们将数据集分为X(输入特征)和y(目标变量):
from sklearn.model_selection import train_test_split
X = df.loc[:, df.columns != 'Outcome']
y = df.loc[:, 'Outcome']
然后,根据前面的图示,第一次拆分将数据划分为训练集(80%)和测试集(20%):
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
最后,进行第二次拆分,创建最终的训练集和验证集:
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=0.2)
MLP
现在我们已经完成了探索性数据分析和数据预处理,接下来我们将把注意力转向神经网络架构的设计。在这个项目中,我们将使用 MLP。
MLP 是一类前馈神经网络,它与我们在第一章《机器学习与神经网络 101》中讨论的单层感知器的区别在于,MLP 至少有一个隐藏层,每个层都通过非线性激活函数进行激活。这样的多层神经网络架构和非线性激活函数使得 MLP 能够产生非线性决策边界,这在像 Pima 印度糖尿病数据集这样的多维真实世界数据集中至关重要。
模型架构
MLP 的模型架构可以图示如下:

如第一章《机器学习与神经网络 101》中讨论的那样,我们可以在 MLP 中使用任意数量的隐藏层。对于这个项目,我们将在 MLP 中使用两个隐藏层。
输入层
输入层(由粉色矩形中的圆圈表示)中的每个节点代表数据集中的每个特征(即每一列)。由于 Pima 印度数据集中有八个特征,因此在我们的 MLP 的输入层中应有八个节点。
隐藏层
输入层之后的下一层称为隐藏层。正如我们在第一章《机器学习与神经网络 101》中所看到的,隐藏层接收输入层并应用非线性激活函数。从数学角度来看,我们可以将隐藏层的函数表示如下:

表示来自前一层的输入,
表示非线性激活函数,
是权重,
表示偏置。
为了简化,我们在这个项目中仅使用两个隐藏层。增加隐藏层的数量往往会增加模型的复杂性和训练时间。对于这个项目来说,两个隐藏层就足够了,稍后我们将在查看模型性能时看到这一点。
激活函数
在设计神经网络模型架构时,我们还需要决定为每一层使用哪些激活函数。激活函数在神经网络中扮演着重要角色。你可以把激活函数看作是神经网络中的变换器;它们接受一个输入值,变换这个输入值,并将变换后的值传递给下一层。
在本项目中,我们将使用修正线性单元(ReLU)和sigmoid作为我们的激活函数。
ReLU
一般来说,ReLU 总是作为中间隐藏层(即非输出层)的激活函数使用。2011 年,研究人员证明,ReLU 在训练深度神经网络(DNNs)方面优于所有以前使用的激活函数。如今,ReLU 已成为 DNNs 中最受欢迎的激活函数选择,并且已成为默认的激活函数选择。
从数学角度看,我们可以将 ReLU 表示如下:

ReLU 函数的作用是仅考虑原始函数的非负部分,
,并将负部分视为0。下图说明了这一点:

Sigmoid 激活函数
对于最终的输出层,我们需要一个激活函数来预测标签的类别。在本项目中,我们做的是一个简单的二分类预测:1 表示患有糖尿病的患者,0 表示没有糖尿病的患者。Sigmoid 激活函数非常适合二分类问题。
从数学角度看,我们可以将 sigmoid 激活函数表示如下:

虽然这看起来很复杂,但其底层函数实际上非常简单。Sigmoid 激活函数只是将一个值压缩到0和1之间:

如果变换后的值
大于 0.5,那么我们将其分类为 1 类。类似地,如果变换后的值小于 0.5,我们将其分类为 0 类。Sigmoid 激活函数允许我们接受一个输入值,并输出一个二进制分类(1 或 0),这正是本项目所需要的(即预测一个人是否患有糖尿病)。
在 Python 中使用 Keras 构建模型
我们终于准备好在 Keras 中构建并训练我们的 MLP。
模型构建
正如我们在第一章《机器学习与神经网络 101》中提到的,Keras 中的 Sequential() 类允许我们像搭积木一样构建神经网络,将各层叠加在一起。
让我们创建一个新的 Sequential() 类:
from keras.models import Sequential
model = Sequential()
接下来,让我们堆叠第一个隐藏层。第一个隐藏层将有 32 个节点,输入维度为 8(因为X_train有 8 列)。注意,对于第一个隐藏层,我们需要指定输入维度。之后,Keras 会自动处理其他隐藏层的尺寸兼容性。
另一个需要注意的点是,我们随意决定了第一个隐藏层的节点数量。这个变量是一个超参数,应该通过反复试验来仔细选择。在这个项目中,我们跳过了超参数调优,直接使用 32 作为节点数量,因为对于这个简单数据集来说,它不会带来太大差异。
让我们添加第一个隐藏层:
from keras.layers import Dense
# Add the first hidden layer
model.add(Dense(32, activation='relu', input_dim=8))
使用的activation函数是relu,如前一节所讨论。
接下来,让我们堆叠第二个隐藏层。添加更多的隐藏层会增加模型的复杂度,但有时可能导致模型过拟合。对于这个项目,我们只使用两个隐藏层,因为这足以生成一个令人满意的模型。
让我们添加第二个隐藏层:
# Add the second hidden layer
model.add(Dense(16, activation='relu'))
最后,通过添加输出层来完成 MLP。这个层只有一个节点,因为我们在做二分类任务。使用的activation函数是sigmoid函数,它将输出值压缩在 0 和 1 之间(二值输出)。
现在我们按照如下方式添加输出层:
# Add the output layer
model.add(Dense(1, activation='sigmoid'))
模型编译
在我们开始训练模型之前,我们需要定义训练过程的参数,这可以通过compile方法来完成。
在训练过程中,我们需要定义三个不同的参数:
-
优化器:我们使用
adam优化器,它是 Keras 中常用的优化器。对于大多数数据集,adam优化器通常无需过多调节就能良好运行。 -
损失函数:我们将使用
binary_crossentropy作为我们的loss函数,因为我们面临的是一个二分类问题。 -
评估指标:我们将使用
accuracy(即正确分类样本的百分比)作为我们的评估指标。
然后,我们可以按照以下方式运行compile()函数:
# Compile the model
model.compile(optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy'])
模型训练
为了训练我们在前面步骤中定义的 MLP 模型,让我们调用fit函数。我们将训练模型200次迭代:
# Train the model for 200 epochs
model.fit(X_train, y_train, epochs=200)
我们得到了以下结果:

如我们所见,随着每个周期的进行,损失值在减少,准确率在增加,因为学习算法根据训练数据不断更新 MLP 中的权重和偏差。请注意,前述截图中显示的准确率是基于训练数据的。在下一节中,我们将看看基于测试数据的 MLP 表现以及其他一些重要指标。
结果分析
成功训练了 MLP 后,让我们基于测试准确率、混淆矩阵和接收者操作特征(ROC)曲线来评估模型。
测试准确度
我们可以使用evaluate()函数评估我们的训练集和测试集模型表现:
scores = model.evaluate(X_train, y_train)
print("Training Accuracy: %.2f%%\n" % (scores[1]*100))
scores = model.evaluate(X_test, y_test)
print("Testing Accuracy: %.2f%%\n" % (scores[1]*100))
我们得到了以下结果:

在训练集和测试集上的准确率分别为 91.85%和 78.57%。训练集和测试集之间的准确率差异并不令人惊讶,因为模型是在训练集上进行训练的。事实上,通过多次迭代训练模型,我们可以在训练集上达到 100%的准确率,但那并不理想,因为这意味着我们过拟合了模型。测试准确度应该始终用于评估我们模型的实际表现,因为测试集代表了模型之前从未见过的真实世界数据。
78.57%的测试准确率对于我们这个仅有两个隐藏层的简单 MLP 来说相当令人印象深刻。这意味着,给定来自一位新患者的八个测量值(如血糖、血压、胰岛素等),我们的 MLP 能够以约 80%的准确率预测该患者是否会在未来五年内发展为糖尿病。从本质上讲,我们已经开发出了我们的第一个 AI 代理!
混淆矩阵
混淆矩阵是一个有用的可视化工具,它提供了关于模型所做的真负例、假正例、假负例和真正例的分析。除了简单的准确度指标外,我们还应查看混淆矩阵,以了解模型的表现。
真负例、假正例、假负例和真正例的定义如下:
-
真负例:实际类别为负(无糖尿病),模型预测为负(无糖尿病)
-
假正例:实际类别为负(无糖尿病),但模型预测为正(糖尿病)
-
假负例:实际类别为正(糖尿病),但模型预测为负(无糖尿病)
-
真正例:实际类别为正(糖尿病),模型预测为正(糖尿病)
显然,我们希望假正例和假负例的数量尽可能低,而真负例和真正例的数量尽可能高。
我们可以使用sklearn中的confusion``_matrix类构建混淆矩阵,并使用seaborn进行可视化:
from sklearn.metrics import confusion_matrix
import seaborn as sns
y_test_pred = model.predict_classes(X_test)
c_matrix = confusion_matrix(y_test, y_test_pred)
ax = sns.heatmap(c_matrix, annot=True,
xticklabels=['No Diabetes','Diabetes'],
yticklabels=['No Diabetes','Diabetes'],
cbar=False, cmap='Blues')
ax.set_xlabel("Prediction")
ax.set_ylabel("Actual")
结果如下:

从前面的混淆矩阵中,我们可以看到大多数预测是真负例和真正例(如上一节中提到的 78.57%的测试准确率所示)。其余的 19 个预测为假负例,另有 14 个预测为假正例,这些都是不理想的。
对于糖尿病预测来说,假阴性可能比假阳性更具破坏性。假阴性意味着告诉病人他们在接下来的五年内不会得糖尿病,然而事实上他们会得。因此,当我们评估不同模型在预测糖尿病发生方面的表现时,假阴性较少的模型更为理想。
ROC 曲线
对于分类任务,我们也应该查看 ROC 曲线来评估我们的模型。ROC 曲线是一个图表,真正阳性率(TPR)位于 y 轴,假阳性率(FPR)位于 x 轴。TPR 和 FPR 定义如下:


当我们分析 ROC 曲线时,我们会查看曲线下的面积(AUC)来评估生成该曲线的模型的性能。较大的 AUC 表明模型能够高准确率地区分不同类别,而较小的 AUC 则表明模型预测不佳,常常出错。位于对角线上的 ROC 曲线表明模型的表现与随机猜测没有区别。以下图示说明了这一点:

让我们绘制模型的 ROC 曲线并分析其性能。和往常一样,scikit-learn 提供了一个有用的 roc_curve 类来帮助我们实现这一点。但首先,让我们使用 predict() 函数获取每个类别的预测概率:
from sklearn.metrics import roc_curve
import matplotlib.pyplot as plt
y_test_pred_probs = model.predict(X_test)
然后,运行 roc_curve 函数,以获得对应的假阳性率和真正阳性率,用于绘制 ROC 曲线:
FPR, TPR, _ = roc_curve(y_test, y_test_pred_probs)
现在使用 matplotlib 绘制图表中的值:
plt.plot(FPR, TPR)
plt.plot([0,1],[0,1],'--', color='black') #diagonal line
plt.title('ROC Curve')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
我们得到以下结果:

从之前的ROC 曲线中,我们可以看到该模型表现相当不错,接近前面图示中的ROC 曲线。这表明我们的模型能够有效地区分不同类别的样本,做出准确预测。
进一步改进
此时,值得思考是否有可能进一步提高我们模型的性能。我们如何才能进一步提高模型的准确性和/或改善假阴性率和假阳性率?
一般来说,性能上的任何局限性通常是由于数据集中缺乏强特征,而不是所使用神经网络的复杂性。皮马印第安人糖尿病数据集仅包含八个特征,可以说单靠这些特征不足以真正预测糖尿病的发生。
增加我们提供给模型的特征数量的一种方式是通过特征工程。特征工程是利用领域知识为机器学习算法创建新特征的过程。特征工程是数据科学中最重要的方面之一。事实上,许多过去的 Kaggle 竞赛获胜者将他们的成功归功于特征工程,而不仅仅是机器学习模型的调优。然而,特征工程是一把双刃剑,必须谨慎进行。添加不合适的特征可能会为我们的机器学习模型带来噪声,进而影响模型的性能。
在相反的方向上,我们也可以考虑去除某些特征以提高模型的性能。这被称为特征选择。当我们认为原始数据集包含太多噪声时,可以使用特征选择,去除那些噪声特征(即不强的预测特征),以改善模型性能。使用决策树是进行特征选择的常见方法。
决策树是一类具有树状数据结构的独立机器学习模型。决策树的优点在于它根据某些统计标准计算并排序最重要的特征。我们可以首先使用决策树拟合数据,然后利用决策树的输出去除被认为不重要的特征,最后将减少后的数据集输入神经网络。再次强调,特征选择是一把双刃剑,可能会影响模型的性能。
尽管在本项目中没有进行特征工程和特征选择,但我们将在后续章节的其他项目中看到它们的应用,因为我们将逐步处理更具挑战性的问题。
总结
在本章中,我们设计并实现了一个多层感知机(MLP),该模型能够以约 80%的准确率预测糖尿病的发生。
我们首先进行了探索性数据分析,查看了每个变量的分布情况,以及各个变量与目标变量之间的关系。然后,我们进行了数据预处理,去除了缺失数据,并对数据进行了标准化处理,使每个变量的均值为 0,标准差为 1。最后,我们将原始数据随机划分为训练集、验证集和测试集。
接着,我们查看了我们使用的 MLP 的架构,该网络包含 2 个隐藏层,第一个隐藏层有 32 个节点,第二个隐藏层有 16 个节点。然后,我们在 Keras 中使用顺序模型实现了这个 MLP,顺序模型允许我们将各层叠加在一起。接下来,我们使用训练集训练了我们的 MLP,在 200 次迭代中,Keras 使用 Adam 优化算法逐步调整神经网络中的权重和偏置,逐渐提高模型的准确性。
最后,我们使用测试准确率、混淆矩阵和 ROC 曲线等指标评估了我们的模型。我们看到了在评估模型时,查看假阴性和假阳性等指标的重要性,以及假阴性和假阳性在预测糖尿病发作的分类器中特别重要。
这结束了使用简单的 MLP 预测糖尿病发作的章节。在下一章节,第三章,使用深度前馈网络预测出租车费用,我们将使用一个更复杂的数据集,该数据集利用时间和地理位置信息来预测出租车费用。
问题
- 我们如何绘制 pandas DataFrame 中每个变量的直方图?为什么直方图很有用?
我们可以通过调用 df.hist() 函数来绘制直方图,该函数内置于 pandas DataFrame 类中。直方图提供了数值数据分布的准确表示。
- 我们如何检查 pandas DataFrame 中的缺失值(NaN 值)?
我们可以调用 df.isnull().any() 函数,轻松检查数据集中每一列是否有缺失值。
- 除了 NaN 值,还有哪些类型的缺失值可能出现在数据集中?
缺失值也可能以 0 值的形式出现。由于数据收集过程中可能出现某些问题,缺失值常常以 0 记录在数据集中——可能是设备故障,或者存在其他阻碍数据收集的问题。
- 为什么在用数据训练神经网络之前,删除数据集中的缺失值是至关重要的?
神经网络无法处理 NaN 值。由于神经网络在前向传播和反向传播过程中执行的数学运算,所有输入都必须是数值型的。
- 数据标准化做了什么?为什么在用数据训练神经网络之前进行数据标准化很重要?
数据标准化的目标是将数值变量转换,使得每个变量的均值为零,方差为单位。当训练神经网络时,确保数据已经标准化非常重要。这能确保在训练神经网络时,规模较大的特征不会主导规模较小的特征。
- 我们如何拆分数据集以确保模型性能的无偏评估?
在训练神经网络之前,我们应该将数据集分为训练集、验证集和测试集。神经网络将在训练集上训练,而验证集允许我们使用无偏的数据源进行超参数调整。最后,测试集提供了一个无偏的数据源,用于评估神经网络的性能。
- MLP 模型架构的特征是什么?
MLP(多层感知机)是前馈神经网络,至少有一层隐藏层,每一层都由非线性激活函数激活。这个多层神经网络架构和非线性激活使得 MLP 能够产生非线性的决策边界。
- 神经网络中激活函数的作用是什么?
激活函数对权重和偏差进行非线性转换,然后再传递到下一层。最常见且有效的隐藏层激活函数是 ReLU 激活函数。
- 在训练神经网络进行二分类问题时,应该使用什么样的损失函数?
二元交叉熵是训练神经网络进行二分类问题时最合适的损失函数。
- 混淆矩阵代表什么,我们如何利用它来评估神经网络的性能?
混淆矩阵提供了我们神经网络在真正负类、假正类、假负类和真正正类上的值。除了简单的准确性指标外,混淆矩阵还可以让我们深入了解神经网络所犯的错误类型(假正类和假负类)。
第三章:使用深度前馈网络预测出租车费用
在本章中,我们将使用深度前馈神经网络预测纽约市(NYC)的出租车费用,输入包括接送地点等。
在上一章,第二章,使用多层感知器预测糖尿病,我们看到如何使用具有两个隐藏层的 MLP 执行分类任务(患者是否有糖尿病风险)。在本章中,我们将构建一个深度神经网络来执行回归任务,即估算出租车费用。正如我们所见,我们需要一个更深的(即更复杂的)神经网络来实现这一目标。
本章将涵盖以下主题:
-
我们要解决的问题的动机——准确预测出租车费用
-
机器学习中的分类问题与回归问题
-
对纽约市出租车费用数据集的深入分析,包括地理位置数据可视化
-
深度前馈神经网络的架构
-
在 Keras 中训练用于回归问题的深度前馈神经网络
-
我们结果的分析
技术要求
本章所需的关键 Python 库如下:
-
matplotlib 3.0.2
-
pandas 0.23.4
-
Keras 2.2.4
-
NumPy 1.15.2
-
scikit-learn 0.20.2
要下载本项目所需的数据集,请参阅raw.githubusercontent.com/PacktPublishing/Neural-Network-Projects-with-Python/master/Chapter03/how_to_download_the_dataset.txt中的说明。
本章的代码可以在本书的 GitHub 仓库中找到,地址为github.com/PacktPublishing/Neural-Network-Projects-with-Python。
要将代码下载到你的计算机,请运行以下git clone命令:
$ git clone https://github.com/PacktPublishing/Neural-Network-Projects-with-Python.git
处理完成后,将会生成一个名为Neural-Network-Projects-with-Python的文件夹。通过运行以下命令进入该文件夹:
$ cd Neural-Network-Projects-with-Python
要在虚拟环境中安装所需的 Python 库,请运行以下命令:
$ conda env create -f environment.yml
请注意,在运行此命令之前,你应该先在计算机上安装了 Anaconda。要进入虚拟环境,请运行以下命令:
$ conda activate neural-network-projects-python
通过运行以下命令进入Chapter03文件夹:
$ cd Chapter03
以下文件位于此文件夹中:
-
main.py:这是神经网络的主要代码。 -
utils.py:此文件包含辅助工具代码,帮助我们实现神经网络。 -
visualize.py:此文件包含所有必要的代码,用于探索性数据分析和数据可视化。本章中的每个图表都可以通过运行此文件重新创建。
要运行神经网络的代码,只需执行main.py文件:
$ python main.py
要重现本章中涵盖的数据可视化,执行visualize.py文件:
$ python visualize.py
预测纽约市出租车费用
纽约市的黄色出租车可能是城市中最具标志性的象征之一。纽约市数以万计的通勤者依靠出租车在这个繁华的大都市中进行交通。近年来,纽约市的出租车行业受到 Uber 等乘车应用的日益增加的压力。
为了应对乘车应用的挑战,纽约市的黄色出租车正在寻求现代化运营,并提供与 Uber 相当的用户体验。2018 年 8 月,纽约市出租车和豪华轿车管理委员会推出了一个新的应用程序,允许通勤者通过手机预订黄色出租车。该应用程序在乘车前提供预先定价。创建一个算法以提供预先定价并不是一件简单的事情。该算法需要考虑各种环境变量,如交通状况、时间和上下车地点,以便准确预测车费。利用机器学习是实现这一目标的最佳方式。到本章结束时,您将创建并训练一个神经网络来实现这一目标。
纽约市出租车费用数据集
我们将在此项目中使用的数据集是 Kaggle 提供的纽约市出租车费用数据集。原始数据集包含来自 2009 年至 2015 年的 5500 万次行程记录,包括上车和下车地点、乘客数量和上车时间。这个数据集为在机器学习项目中使用大数据集提供了有趣的机会,同时也可以可视化地理位置数据。
探索性数据分析
让我们直接深入数据集。下载纽约市出租车费用数据集的说明可以在本书的 GitHub 存储库中找到(请参阅技术要求部分)。与上一章不同的是,第二章,使用多层感知器预测糖尿病,我们不打算导入 5500 万行的原始数据集。事实上,大多数计算机无法将整个数据集存储在内存中!相反,让我们只导入前 500 万行。这样做确实有其缺点,但这是在有效使用数据集的必要权衡。
要做到这一点,使用pandas的read_csv()函数:
import pandas as pd
df = pd.read_csv('NYC_taxi.csv', parse_dates=['pickup_datetime'], nrows=500000)
read_csv中的parse_dates参数允许pandas轻松识别某些列作为日期,这使我们可以灵活处理这些datetime值,正如我们将在本章后面看到的那样。
通过调用df.head()命令,让我们看看数据集的前五行:
print(df.head())
我们得到以下输出:

我们可以看到数据集中有八列:
-
key:这一列看起来与pickup_datetime列相同。它可能是数据库中作为唯一标识符使用的。我们可以安全地删除这一列,而不会丢失任何信息。 -
fare_amount:这是我们尝试预测的目标变量,即旅行结束时支付的票价金额。 -
pickup_datetime:这一列包含了接送日期(年份、月份、日)以及时间(小时、分钟、秒)。 -
pickup_longitude和pickup_latitude:接送地点的经度和纬度。 -
dropoff_longitude和dropoff_latitude:下车地点的经度和纬度。 -
passenger_count:乘客数量。
可视化地理位置信息
接送地点的经纬度数据对于预测票价至关重要。毕竟,纽约市出租车的票价主要由行驶的距离决定。
首先,让我们了解纬度和经度代表什么。纬度和经度是地理坐标系统中的坐标。基本上,纬度和经度允许我们通过一组坐标来指定地球上的任何位置。
以下图显示了纬度和经度坐标系统:

我们可以把地球看作是一个散点图,经度和纬度是坐标轴。然后,地球上的每个位置就只是散点图上的一个点。事实上,来,我们就按这个方式做;让我们在散点图上绘制接送和下车的纬度和经度。
首先,让我们将数据点限制为仅包含纽约市的接送和下车位置。纽约市的经度范围大致为-74.05到-73.75,纬度范围为40.63到40.85:
# range of longitude for NYC
nyc_min_longitude = -74.05
nyc_max_longitude = -73.75
# range of latitude for NYC
nyc_min_latitude = 40.63
nyc_max_latitude = 40.85
df2 = df.copy(deep=True)
for long in ['pickup_longitude', 'dropoff_longitude']:
df2 = df2[(df2[long] > nyc_min_longitude) & (df2[long] <
nyc_max_longitude)]
for lat in ['pickup_latitude', 'dropoff_latitude']:
df2 = df2[(df2[lat] > nyc_min_latitude) & (df2[lat] <
nyc_max_latitude)]
请注意,我们将原始数据框df复制到一个新的数据框df2中,以避免覆盖原始数据框。
现在,让我们定义一个新函数,该函数将以我们的数据框作为输入,并在散点图上绘制接送位置。我们还希望在散点图上叠加一些纽约市的关键地标。快速的谷歌搜索告诉我们,纽约市有两个主要机场(JFK 和拉瓜迪亚机场),它们的坐标,以及纽约市的主要区县如下:
landmarks = {'JFK Airport': (-73.78, 40.643),
'Laguardia Airport': (-73.87, 40.77),
'Midtown': (-73.98, 40.76),
'Lower Manhattan': (-74.00, 40.72),
'Upper Manhattan': (-73.94, 40.82),
'Brooklyn': (-73.95, 40.66)}
这是我们使用matplotlib绘制接送位置散点图的函数:
import matplotlib.pyplot as plt
def plot_lat_long(df, landmarks, points='Pickup'):
plt.figure(figsize = (12,12)) # set figure size
if points == 'pickup':
plt.plot(list(df.pickup_longitude), list(df.pickup_latitude),
'.', markersize=1)
else:
plt.plot(list(df.dropoff_longitude), list(df.dropoff_latitude),
'.', markersize=1)
for landmark in landmarks:
plt.plot(landmarks[landmark][0], landmarks[landmark][1],
'*', markersize=15, alpha=1, color='r')
plt.annotate(landmark, (landmarks[landmark][0]+0.005,
landmarks[landmark][1]+0.005), color='r',
backgroundcolor='w')
plt.title("{} Locations in NYC Illustrated".format(points))
plt.grid(None)
plt.xlabel("Latitude")
plt.ylabel("Longitude")
plt.show()
让我们运行刚才定义的函数:
plot_lat_long(df2, landmarks, points='Pickup')
我们将看到以下显示接送位置的散点图:

不觉得这很美吗?仅仅通过在散点图上绘制接送位置,我们就能清楚地看到纽约市的地图,以及纽约街道的网格布局。从前面的散点图中,我们可以得出一些观察结果:
-
在曼哈顿,大多数接送发生在
中城区域,其次是下曼哈顿。相比之下,上曼哈顿的接送数量要少得多。这是有道理的,因为上曼哈顿是一个住宅区,而更多的办公室和旅游景点位于中城和下曼哈顿。 -
曼哈顿以外的接送稀少。唯一的两个异常点出现在
拉瓜迪亚机场和JFK 机场。
让我们还绘制下车位置的散点图,并看看它与接送位置的差异。
plot_lat_long(df2, landmarks, points='Drop Off')
我们将看到以下散点图:

比较接送和下车的散点图,我们可以清楚地看到,在像上曼哈顿和布鲁克林这样的住宅区,下车次数比接送次数多。很有趣!
按天和小时的骑行情况
接下来,让我们研究一下每天和每小时的骑行数量变化。
记住,原始数据包含一个单一的pickup_datetime列,其中包含接送日期和时间(datetime格式)。首先,让我们将接送的年份、月份、日期、星期几和小时从原始的pickup_datetime列中分离出来,放入不同的列中:
df['year'] = df['pickup_datetime'].dt.year
df['month'] = df['pickup_datetime'].dt.month
df['day'] = df['pickup_datetime'].dt.day
df['day_of_week'] = df['pickup_datetime'].dt.dayofweek
df['hour'] = df['pickup_datetime'].dt.hour
由于我们之前在将数据导入 pandas 时使用了parse_dates参数,因此我们可以很容易地使用 pandas 中的dt函数识别并分离年份、月份、日期和小时组件。
现在,让我们绘制一个直方图来分析一周内骑行的分布:
import numpy as np
df['day_of_week'].plot.hist(bins=np.arange(8)-0.5, ec='black',
ylim=(60000,75000))
plt.xlabel('Day of Week (0=Monday, 6=Sunday)')
plt.title('Day of Week Histogram')
plt.show()
我们将看到以下直方图:

有趣的是,我们可以看到,骑行数量在每个工作日并不均匀分布。相反,骑行数量从周一到周五线性增加,并在周五达到峰值。周末的骑行数量略微下降,周六有所减少,而周日则急剧下降。
我们还可以按小时可视化骑行人数:
df['hour'].plot.hist(bins=24, ec='black')
plt.title('Pickup Hour Histogram')
plt.xlabel('Hour')
plt.show()
我们将看到以下关于接送小时的直方图:

我们可以看到,在晚上高峰时段,骑行次数比早高峰时段更多。事实上,骑行数量在一天内基本保持恒定。从下午 6 点开始,骑行数量逐渐增加,并在晚上 7 点达到峰值,然后从晚上 11 点开始下降。
数据预处理
回想一下之前的项目,我们必须通过删除缺失值和其他数据异常来预处理数据。在这个项目中,我们将执行相同的过程。我们还将进行特征工程,以在训练神经网络之前提升特征的质量和数量。
处理缺失值和数据异常
让我们检查一下数据集中是否有缺失值:
print(df.isnull().sum())
我们将看到以下输出,显示每列中的缺失值数量:

我们可以看到,只有五行数据(共 500,000 行)缺失。缺失数据的比例仅为 0.001%,看起来我们没有缺失数据的问题。我们接下来将删除这五行缺失数据:
df = df.dropna()
此时,我们还应检查数据中是否存在异常值。在如此庞大的数据集中,必定会有异常值,这些异常值可能会扭曲我们的模型。让我们对数据进行快速的统计汇总,以查看其分布:
print(df.describe())
describe方法生成了如下表格:

数据集中的最低票价是$-44.90。这不合理;票价不可能是负数!此外,最高票价是$500。乘客是不是被坑了?还是只是一个错误?让我们绘制一个直方图,更好地理解票价的分布:
df['fare_amount'].hist(bins=500)
plt.xlabel("Fare")
plt.title("Histogram of Fares")
plt.show()
我们将得到以下直方图:

看起来并没有太多异常值,因此我们可以安全地删除它们。我们还可以从直方图中观察到一个有趣的趋势,即票价在$50 附近出现了一个小的尖峰。这是否可能是某个特定地点的固定票价?城市通常会为往返机场的行程实施固定票价。通过快速的谷歌搜索,我们发现,往返 JFK 机场的行程会收取$52 的固定票价,加上过路费。这可能就是票价在$50 附近出现尖峰的原因!当我们进行特征工程时,我们会记住这个重要的事实。
目前,让我们删除票价低于$0 或高于$100 的行:
df = df[(df['fare_amount'] >=0) & (df['fare_amount'] <= 100)]
从前面的表格中,我们可以看到,passenger_count列中也存在异常值。让我们绘制乘客数量的直方图,看看它的分布:
df['passenger_count'].hist(bins=6, ec='black')
plt.xlabel("Passenger Count")
plt.title("Histogram of Passenger Count")
plt.show()
这将给我们带来以下直方图:

我们可以看到,有一小部分行的乘客数量为0。我们不会删除这些行,而是将异常值替换为众数(即1名乘客):
df.loc[df['passenger_count']==0, 'passenger_count'] = 1
我们也可以完全删除这些异常值,因为只有少数几行受影响。不过,我们选择将异常的乘客数量替换为众数。这两种方法都是有效的,但我们选择后者来说明通过直方图可视化数据以识别异常值和众数的重要性。
接下来,让我们检查上车和下车的纬度和经度数据,查看是否有异常值。在前一节关于数据可视化中,我们绘制了一个散点图,并且限制了点应位于纽约市的边界内。现在,我们不加限制地绘制一个散点图:
df.plot.scatter('pickup_longitude', 'pickup_latitude')
plt.show()
我们将看到如下散点图:

你看到异常值的位置了吗?散点图外围的点是异常值。它们的纬度值高达 1000,低至-3000。地球的地理坐标系统没有如此极端的纬度和经度!让我们移除这些异常值:
# range of longitude for NYC
nyc_min_longitude = -74.05
nyc_max_longitude = -73.75
# range of latitude for NYC
nyc_min_latitude = 40.63
nyc_max_latitude = 40.85
# only consider locations within NYC
for long in ['pickup_longitude', 'dropoff_longitude']:
df = df[(df[long] > nyc_min_longitude) & (df[long] <
nyc_max_longitude)]
for lat in ['pickup_latitude', 'dropoff_latitude']:
df = df[(df[lat] > nyc_min_latitude) & (df[lat] <
nyc_max_latitude)]
让我们总结一下数据预处理的工作。我们首先看到缺失值仅占数据集的 0.001%,因此我们可以安全地移除它们,而不影响训练数据的数量。接着,我们看到fare_amount、passenger_count以及提车和下车的纬度和经度存在异常值。我们移除了fare_amount、纬度和经度的异常值。对于passenger_count,我们将那些乘客数为0的行替换为passenger count = 1的众数。
让我们创建一个辅助函数,帮助我们完成所有这些数据预处理工作。在机器学习项目中,步骤数量往往会变得难以控制。因此,遵循强有力的软件工程实践,比如代码模块化,对于保持项目进展至关重要。
以下代码接受一个 pandas DataFrame 作为输入,返回经过数据预处理后的 DataFrame:
def preprocess(df):
# remove missing values in the dataframe
def remove_missing_values(df):
df = df.dropna()
return df
# remove outliers in fare amount
def remove_fare_amount_outliers(df, lower_bound, upper_bound):
df = df[(df['fare_amount'] >= lower_bound) &
(df['fare_amount'] <= upper_bound)]
return df
# replace outliers in passenger count with the mode
def replace_passenger_count_outliers(df):
mode = df['passenger_count'].mode()
df.loc[df['passenger_count'] == 0, 'passenger_count'] = mode
return df
# remove outliers in latitude and longitude
def remove_lat_long_outliers(df):
# range of longitude for NYC
nyc_min_longitude = -74.05
nyc_max_longitude = -73.75
# range of latitude for NYC
nyc_min_latitude = 40.63
nyc_max_latitude = 40.85
# only consider locations within New York City
for long in ['pickup_longitude', 'dropoff_longitude']:
df = df[(df[long] > nyc_min_longitude) &
(df[long] < nyc_max_longitude)]
for lat in ['pickup_latitude', 'dropoff_latitude']:
df = df[(df[lat] > nyc_min_latitude) &
(df[lat] < nyc_max_latitude)]
return df
df = remove_missing_values(df)
df = remove_fare_amount_outliers(df, lower_bound = 0,
upper_bound = 100)
df = replace_passenger_count_outliers(df)
df = remove_lat_long_outliers(df)
return df
我们将把这个辅助函数保存在项目文件夹中的utils.py文件下。然后,为了调用我们的数据预处理辅助函数,我们只需调用from utils import preprocess,就可以使用这个辅助函数了。这使得我们的代码更加整洁和可维护!
特征工程
如前一章中简要讨论的,第二章,使用多层感知器预测糖尿病,特征工程是利用个人对问题的领域知识为机器学习算法创建新特征的过程。在这一部分,我们将基于提车的日期和时间,以及与位置相关的特征来创建特征。
时间特征
如我们在数据可视化部分看到的,乘客量在很大程度上取决于星期几以及一天中的时间。
让我们通过运行以下代码来看一下pickup_datetime列的格式:
print(df.head()['pickup_datetime'])
我们得到以下输出:

记住,神经网络需要数值特征。因此,我们不能使用这种日期时间字符串来训练我们的神经网络。让我们将pickup_datetime列分离成不同的列,分别为year、month、day、day_of_week和hour:
df['year'] = df['pickup_datetime'].dt.year
df['month'] = df['pickup_datetime'].dt.month
df['day'] = df['pickup_datetime'].dt.day
df['day_of_week'] = df['pickup_datetime'].dt.dayofweek
df['hour'] = df['pickup_datetime'].dt.hour
让我们来看看新列:
print(df.loc[:5,['pickup_datetime', 'year', 'month',
'day', 'day_of_week', 'hour']])
我们得到以下输出:

我们可以看到,新列以适合我们神经网络的格式捕捉了pickup_datetime列中的原始信息。让我们从 DataFrame 中删除pickup_datetime列:
df = df.drop(['pickup_datetime'], axis=1)
地理定位特征
正如我们之前所看到的,数据集中包含了关于上下车坐标的信息。然而,数据集中并没有包含上下车点之间的距离信息,而这正是决定出租车费用的最重要因素之一。因此,让我们创建一个新特征,计算每对上下车点之间的距离。
回想几何学中的欧几里得距离,它是任何两点之间的直线距离:

让我们定义一个函数,计算给定两个点的纬度和经度之间的欧几里得距离:
def euc_distance(lat1, long1, lat2, long2):
return(((lat1-lat2)**2 + (long1-long2)**2)**0.5)
然后我们将这个函数应用到数据框中,创建新的distance列:
df['distance'] = euc_distance(df['pickup_latitude'],
df['pickup_longitude'],
df['dropoff_latitude'],
df['dropoff_longitude'])
我们的假设是,车费与行驶的距离密切相关。现在我们可以在散点图上绘制这两个变量,以分析它们的相关性,并查看我们的直觉是否正确:
df.plot.scatter('fare_amount', 'distance')
plt.show()
我们得到以下散点图:

很好!我们可以清楚地看到我们的假设是正确的。然而,单纯依赖行驶的距离并不能完整说明问题。如果我们看一下图表的中心部分,会看到三条垂直的点线。这些离群数据似乎表明,在某些情况下,行驶的距离并没有对车费产生影响(这些离群点的车费在$40 到$60 之间)。回想我们在数据可视化部分看到的,靠近机场的某些上下车点的车费是固定的$52,再加上通行费。这可能解释了这三条$40 到$60 之间的垂直点线!
很明显,我们需要构造一个新特征,告知神经网络从纽约市三个主要机场到上下车地点的距离。当我们在这个特征上训练神经网络时,它应该会学习到,靠近机场的上下车点的车费是一个固定的范围,在$40 到$60 之间。
我们可以使用之前定义的euc_distance函数,计算从纽约市三个主要机场到上下车地点的距离:
airports = {'JFK_Airport': (-73.78,40.643),
'Laguardia_Airport': (-73.87, 40.77),
'Newark_Airport' : (-74.18, 40.69)}
for airport in airports:
df['pickup_dist_' + airport] = euc_distance(df['pickup_latitude'],
df['pickup_longitude'],
airports[airport][1],
airports[airport][0])
df['dropoff_dist_' + airport] = euc_distance(df['dropoff_latitude'],
df['dropoff_longitude'],
airports[airport][1],
airports[airport][0])
让我们打印出前几行,并查看一些相关列,以验证欧几里得距离函数是否按预期工作:
print(df[['key', 'pickup_longitude', 'pickup_latitude',
'dropoff_longitude', 'dropoff_latitude',
'pickup_dist_JFK_Airport',
'dropoff_dist_JFK_Airport']].head())
我们得到以下输出:

我们可以对前面的几行进行快速计算,验证欧几里得距离函数是否正常工作。最后,注意到数据集中仍然存在一个key列。这个列类似于pickup_datetime列,可能在数据库中作为唯一标识符使用。我们可以安全地删除此列,而不会丢失任何信息。要删除key列,可以使用以下命令:
df = df.drop(['key'], axis=1)
总结一下,在这一部分,我们使用特征工程根据我们自己对问题的领域知识构造了新的特征。通过提供的原始日期时间信息,我们提取并构造了关于提车年份、月份、日期、星期几和小时的新特征。我们还构造了基于距离的特征,这些特征对于预测车费至关重要,例如提车点和下车点之间的距离,以及从纽约三大机场到提车点和下车点的距离。
与之前的数据预处理部分类似,我们将构建一个辅助函数来总结我们在特征工程中所做的工作。这个代码模块化方法将帮助我们保持代码的可管理性:
def feature_engineer(df):
# create new columns for year, month, day, day of week and hour
def create_time_features(df):
df['year'] = df['pickup_datetime'].dt.year
df['month'] = df['pickup_datetime'].dt.month
df['day'] = df['pickup_datetime'].dt.day
df['day_of_week'] = df['pickup_datetime'].dt.dayofweek
df['hour'] = df['pickup_datetime'].dt.hour
df = df.drop(['pickup_datetime'], axis=1)
return df
# function to calculate euclidean distance
def euc_distance(lat1, long1, lat2, long2):
return(((lat1-lat2)**2 + (long1-long2)**2)**0.5)
# create new column for the distance travelled
def create_pickup_dropoff_dist_features(df):
df['travel_distance'] = euc_distance(df['pickup_latitude'],
df['pickup_longitude'],
df['dropoff_latitude'],
df['dropoff_longitude'])
return df
# create new column for the distance away from airports
def create_airport_dist_features(df):
airports = {'JFK_Airport': (-73.78,40.643),
'Laguardia_Airport': (-73.87, 40.77),
'Newark_Airport' : (-74.18, 40.69)}
for k in airports:
df['pickup_dist_'+k]=euc_distance(df['pickup_latitude'],
df['pickup_longitude'],
airports[k][1],
airports[k][0])
df['dropoff_dist_'+k]=euc_distance(df['dropoff_latitude'],
df['dropoff_longitude'],
airports[k][1],
airports[k][0])
return df
df = create_time_features(df)
df = create_pickup_dropoff_dist_features(df)
df = create_airport_dist_features(df)
df = df.drop(['key'], axis=1)
return df
特征缩放
作为最后的预处理步骤,我们还应该在将特征传递给神经网络之前对它们进行缩放。回顾前一章,第二章,使用多层感知机预测糖尿病,缩放确保所有特征具有统一的缩放范围。这确保了具有较大缩放的特征(例如,年份的缩放范围大于 2000)不会主导具有较小缩放的特征(例如,乘客人数的缩放范围为 1 到 6)。
在我们对 DataFrame 中的特征进行缩放之前,最好先保留一个原始 DataFrame 的副本。特征的值在缩放后会发生变化(例如,2010 年可能会被转换成一个值,例如-0.134),这会使我们难以解释这些值。通过保留原始 DataFrame 的副本,我们可以轻松地引用原始值:
df_prescaled = df.copy()
我们还应该在缩放之前删除fare_amount目标变量,因为我们不希望修改目标变量:
df_scaled = df.drop(['fare_amount'], axis=1)
然后,通过调用 scikit-learn 中的scale函数来缩放特征:
from sklearn.preprocessing import scale
df_scaled = scale(df_scaled)
最后,将scale函数返回的对象转换为 pandas DataFrame,并将原先在缩放之前删除的fare_amount列拼接回来:
cols = df.columns.tolist()
cols.remove('fare_amount')
df_scaled = pd.DataFrame(df_scaled, columns=cols, index=df.index)
df_scaled = pd.concat([df_scaled, df['fare_amount']], axis=1)
df = df_scaled.copy()
深度前馈神经网络
到目前为止,在本章中,我们对数据集进行了深入的可视化分析,处理了数据集中的异常值,并且进行了特征工程以创建有用的特征供我们的模型使用。在本章的剩余部分,我们将讨论深度前馈神经网络的架构,并在 Keras 中为回归任务训练一个模型。
模型架构
在上一章,第二章,使用多层感知器预测糖尿病中,我们使用了一个相对简单的 MLP 作为我们的神经网络。在这个项目中,由于特征更多,我们将使用一个更深的模型来处理额外的复杂性。深度前馈网络将有四个隐藏层。第一个隐藏层将有 128 个节点,每个后续的隐藏层节点数将是前一个隐藏层的一半。这个神经网络的大小是我们一个不错的起点,训练这个神经网络应该不会花费太长时间。一个常见的经验法则是,我们应该从一个较小的神经网络开始,只有在需要时才增加它的复杂性(大小)。
在每个隐藏层之间,我们将使用 ReLU 激活函数来引入模型的非线性。由于这是一个回归问题,输出层中将只有一个节点(有关回归的更多信息,请参见下一小节)。请注意,我们不会对输出层应用 ReLU 激活函数,因为这样会改变我们的预测结果。
以下图示说明了深度前馈神经网络的模型架构:

回归问题的损失函数
了解什么是回归以及它如何影响我们神经网络的架构非常重要。我们在这个项目中的任务是预测出租车费用,这是一个连续变量。我们可以将其与上一章,第二章,使用多层感知器预测糖尿病中所做的分类项目进行对比,我们设计了一个神经网络来输出一个二元预测(1 或 0),表示患者是否处于糖尿病风险之中。
另一种思考回归和分类的方式是,在回归中,我们试图预测一个连续变量的值(例如,费用、时间或身高),而在分类中,我们试图预测一个类别(例如,糖尿病或无糖尿病)。
回想一下,在上一章,第二章,使用多层感知器预测糖尿病中,我们使用了百分比准确率作为衡量我们预测强度的标准。在回归中,均方根误差(RMSE)通常用作误差度量。
RMSE 的公式如下:

注意,公式如何计算预测值与实际值之间差异的平方。这是为了确保过高估计和过低估计被同等惩罚(因为误差的平方对于两者是相同的)。我们取平方根是为了确保误差的幅度与实际值相似。RMSE 提供了一个损失函数,供我们的神经网络在训练过程中调整权重,以减少预测的误差。
使用 Keras 在 Python 中构建模型
现在,让我们在 Keras 中实现我们的模型架构。就像在上一个项目中一样,我们将使用 Sequential 类逐层构建我们的模型。
首先,将 DataFrame 拆分为训练特征(X)和我们要预测的目标变量(y):
X = df.loc[:, df.columns != 'fare_amount']
y = df.loc[:, 'fare_amount']
然后,将数据拆分为训练集(80%)和测试集(20%):
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
接下来,让我们根据之前概述的神经网络架构,在 Keras 中构建我们的 Sequential 模型:
from keras.models import Sequential
from keras.layers import Dense
model = Sequential()
model.add(Dense(128, activation= 'relu', input_dim=X_train.shape[1]))
model.add(Dense(64, activation= 'relu'))
model.add(Dense(32, activation= 'relu'))
model.add(Dense(8, activation= 'relu'))
model.add(Dense(1))
在开始训练模型之前,验证模型的结构是一个良好的实践:
model.summary()
summary() 函数生成一个表格,显示每一层的层数和每层的节点数,以及每层的参数数量(即权重和偏差)。我们可以验证这与之前概述的模型架构一致。
这是 summary() 函数生成的表格:

最后,我们可以在训练数据上编译并训练我们的神经网络:
model.compile(loss='mse', optimizer='adam', metrics=['mse'])
model.fit(X_train, y_train, epochs=1)
由于数据量较大,训练神经网络需要一些时间。几分钟后,Keras 会在训练周期结束时输出以下内容:

结果分析
现在我们已经训练好了神经网络,让我们用它来进行一些预测,以了解它的准确性。
我们可以创建一个函数,使用测试集中的随机样本进行预测:
def predict_random(df_prescaled, X_test, model):
sample = X_test.sample(n=1, random_state=np.random.randint(low=0,
high=10000))
idx = sample.index[0]
actual_fare = df_prescaled.loc[idx,'fare_amount']
day_names = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday',
'Saturday', 'Sunday']
day_of_week = day_names[df_prescaled.loc[idx,'day_of_week']]
hour = df_prescaled.loc[idx,'hour']
predicted_fare = model.predict(sample)[0][0]
rmse = np.sqrt(np.square(predicted_fare-actual_fare))
print("Trip Details: {}, {}:00hrs".format(day_of_week, hour))
print("Actual fare: ${:0.2f}".format(actual_fare))
print("Predicted fare: ${:0.2f}".format(predicted_fare))
print("RMSE: ${:0.2f}".format(rmse))
predict_random 函数会从测试集随机抽取一行数据,并将其输入模型进行预测。然后,函数会计算并显示预测的 RMSE。请注意,df_prescaled 是必要的,因为它提供了星期几和小时的原始值,因为测试集中的值已经被转换过,不再是人类可读的(例如,星期几的值 -0.018778 对我们来说没有意义)。
让我们运行下面的 predict_random 函数,看看会得到什么样的结果:
predict_random(df_prescaled, X_test, model)
predict_random 函数输出的旅行详情如下:
Trip Details: Sunday, 10:00hrs
Actual fare: $4.90
Predicted fare: $5.60
RMSE: $0.70
以下地图显示了旅行详情:

接送点在上面的地图中可视化。实际费用为$4.90,而预测费用为$5.60,误差为$0.70。看起来我们的模型运行良好,预测也相当准确!请注意,上面截图中的地图和路线仅用于可视化,并不是原始数据集或代码的一部分。
让我们再运行predict_random几次,获取更多的结果:

predict_random函数输出的行程详情如下:
Trip Details: Wednesday, 7:00hrs
Actual fare: $6.10
Predicted fare: $6.30
RMSE: $0.20
我们对这次行程的预测几乎完全准确!实际费用为$6.10,而我们的神经网络预测的票价为$6.30。看来我们的神经网络对于短途行程的预测非常准确。
让我们看看当行程更远且更容易受到交通延误影响时,神经网络的表现如何:

predict_random函数输出的行程详情:
Trip Details: Monday, 10:00hrs
Actual fare: $35.80
Predicted fare: $38.11
RMSE: $2.31
从这个示例中我们可以看到,即使是长途旅行,我们的神经网络也表现得非常好。实际费用为$35.80,而我们的神经网络预测的费用为$38.11。误差为$2.31(约 6%的偏差),考虑到行程的距离,这个结果非常令人印象深刻。
作为最后一个示例,让我们看看我们的神经网络在固定票价行程中的表现。回想一下,从 JFK 机场出发或前往 JFK 机场的所有行程都需要支付固定的票价$52,再加上过路费,无论行程的距离如何:

predict_random函数输出的行程详情如下:
Trip Details: Saturday, 23:00hrs
Actual fare: $52.00
Predicted fare: $53.55
RMSE: $1.55
很好!我们的神经网络理解到这次行程是从 JFK 机场出发的,因此票价应该接近$52。这一点是通过特征工程实现的,我们引入了表示从 JFK 机场接送距离的新特征。这些新特征使得我们的神经网络学会了从 JFK 机场出发的行程应当具有接近$52的票价。这也说明了特征工程的重要性!
最后,让我们通过计算整个训练集和测试集的 RMSE 来总结结果:
from sklearn.metrics import mean_squared_error
train_pred = model.predict(X_train)
train_rmse = np.sqrt(mean_squared_error(y_train, train_pred))
test_pred = model.predict(X_test)
test_rmse = np.sqrt(mean_squared_error(y_test, test_pred))
print("Train RMSE: {:0.2f}".format(train_rmse))
print("Test RMSE: {:0.2f}".format(test_rmse))
我们得到以下输出:

RMSE 值显示,平均而言,我们的模型预测的票价准确度在 ~$3.50 之内。
将所有内容整合起来
在本章中我们已经完成了很多内容。让我们快速回顾一下目前为止我们写的代码。
我们首先定义了一个用于数据预处理的函数。这个preprocess函数接受一个 DataFrame 作为输入,并执行以下操作:
-
移除缺失值
-
移除票价中的异常值
-
用众数替换乘客数量中的异常值
-
移除纬度和经度中的异常值(即只考虑纽约市内的点)
这个函数保存在我们项目文件夹中的utils.py文件下。
接下来,我们还定义了一个feature_engineer函数用于特征工程。该函数以 DataFrame 作为输入,并执行以下操作:
-
为年份、月份、日期、星期几和小时创建新列
-
为接送点之间的欧几里得距离创建新列
-
创建了关于 JFK、拉瓜迪亚和纽瓦克机场的接送距离的新列
该函数也保存在我们的项目文件夹中的utils.py文件下。
现在我们已经定义了辅助函数,我们可以继续编写主神经网络代码。让我们创建一个新的 Python 文件main.py,用于存放我们的主神经网络代码。
首先,我们导入必要的模块:
from utils import preprocess, feature_engineer
import pandas as pd
import numpy as np
from sklearn.preprocessing import scale
from sklearn.model_selection import train_test_split
from keras.models import Sequential
from keras.layers import Dense
from sklearn.metrics import mean_squared_error
接下来,我们导入原始表格数据的前500000行:
df = pd.read_csv('NYC_taxi.csv', parse_dates=['pickup_datetime'],
nrows=500000)
我们使用之前定义的函数进行预处理和特征工程:
df = preprocess(df)
df = feature_engineer(df)
接下来,我们对特征进行缩放:
df_prescaled = df.copy()
df_scaled = df.drop(['fare_amount'], axis=1)
df_scaled = scale(df_scaled)
cols = df.columns.tolist()
cols.remove('fare_amount')
df_scaled = pd.DataFrame(df_scaled, columns=cols, index=df.index)
df_scaled = pd.concat([df_scaled, df['fare_amount']], axis=1)
df = df_scaled.copy()
接下来,我们将 DataFrame 拆分为训练集和测试集:
X = df.loc[:, df.columns != 'fare_amount']
y = df.fare_amount
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
我们在 Keras 中构建并训练了我们的深度前馈神经网络:
model=Sequential()
model.add(Dense(128, activation= 'relu', input_dim=X_train.shape[1]))
model.add(Dense(64, activation= 'relu'))
model.add(Dense(32, activation= 'relu'))
model.add(Dense(8, activation= 'relu'))
model.add(Dense(1))
model.compile(loss='mse', optimizer='adam', metrics=['mse'])
model.fit(X_train, y_train, epochs=1)
最后,我们分析了我们的结果:
train_pred = model.predict(X_train)
train_rmse = np.sqrt(mean_squared_error(y_train, train_pred))
test_pred = model.predict(X_test)
test_rmse = np.sqrt(mean_squared_error(y_test, test_pred))
print("Train RMSE: {:0.2f}".format(train_rmse))
print("Test RMSE: {:0.2f}".format(test_rmse))
def predict_random(df_prescaled, X_test, model):
sample = X_test.sample(n=1, random_state=np.random.randint(low=0,
high=10000))
idx = sample.index[0]
actual_fare = df_prescaled.loc[idx,'fare_amount']
day_names = ['Monday','Tuesday','Wednesday','Thursday','Friday',
'Saturday', 'Sunday']
day_of_week = day_names[df_prescaled.loc[idx,'day_of_week']]
hour = df_prescaled.loc[idx,'hour']
predicted_fare = model.predict(sample)[0][0]
rmse = np.sqrt(np.square(predicted_fare-actual_fare))
print("Trip Details: {}, {}:00hrs".format(day_of_week, hour))
print("Actual fare: ${:0.2f}".format(actual_fare))
print("Predicted fare: ${:0.2f}".format(predicted_fare))
print("RMSE: ${:0.2f}".format(rmse))
predict_random(df_prescaled, X_test, model)
这就是我们所有的代码!请注意,在utils.py中为预处理和特征工程创建辅助函数使得我们的主代码相对简短。通过将代码模块化为单独的辅助函数,我们可以专注于实现机器学习框架的每个步骤。
总结
在本章中,我们设计并实现了一个深度前馈神经网络,能够在纽约市预测出租车费用,误差约为 ~$3.50。我们首先进行了探索性数据分析,从中获得了关于影响出租车费用的重要见解。基于这些见解,我们进行了特征工程,即运用问题领域知识创建新特征。我们还介绍了在机器学习项目中模块化函数的概念,这使得我们的主代码保持相对简洁。
我们在 Keras 中创建了深度前馈神经网络,并使用预处理过的数据进行了训练。我们的结果显示,神经网络能够对短途和长途旅行做出高度准确的预测。即使是固定费率的旅行,我们的神经网络也能够给出非常准确的预测。
本章结束了使用深度前馈神经网络进行回归预测任务的内容。结合前一章,第二章,使用多层感知机预测糖尿病,我们已经看到如何利用神经网络进行分类和回归预测。在下一章,第四章,猫与狗 – 使用 CNN 进行图像分类,我们将介绍更多复杂的神经网络,适用于计算机视觉项目。
问题
- 使用 pandas 读取 CSV 文件时,pandas 如何识别某些列是日期时间类型?
在使用read_csv函数读取 CSV 文件时,我们可以使用parse_dates参数。
- 假设我们有一个 DataFrame
df,并且我们想选择身高值在160到180之间的行,如何筛选 DataFrame 来只选择这些行?
我们可以像这样筛选 DataFrame:
df = df[(df['height'] >= 160) & (df['height'] <= 180)]
这将返回一个新的 DataFrame,包含身高值在160和180之间的范围。
- 我们如何使用代码模块化来组织我们的神经网络项目?
我们可以通过模块化代码来组织我们的函数。例如,在这个项目中,我们在utils.py中定义了preprocess和feature_engineer函数,这使我们能够将预处理和特征工程功能的实现分开进行。
- 回归任务与分类任务有什么区别?
在回归中,我们尝试预测一个连续变量的值(例如,出租车费用),而在分类中,我们尝试预测一个类别(例如,是否患糖尿病)。
- 对还是错?对于回归任务,我们应该在输出层应用激活函数。
错误。对于回归任务,我们永远不应该在输出层应用激活函数,因为这样做会改变我们的预测结果,从而影响模型的性能。
- 在训练神经网络进行回归任务时,通常使用什么损失函数?
RMSE 是回归任务中常用的损失函数。RMSE 衡量的是预测值与实际目标变量之间的绝对差异。
第四章:猫与狗 - 使用 CNN 进行图像分类
在本章中,我们将使用卷积神经网络(CNNs)创建一个分类器,预测给定图像中是否包含猫或狗。
本项目是系列项目的第一部分,我们将在其中使用神经网络解决图像识别和计算机视觉问题。正如我们将看到的,神经网络已被证明是解决计算机视觉问题的极其有效工具。
本章将涵盖以下主题:
-
我们尝试解决的问题的动机:图像识别
-
计算机视觉中的神经网络与深度学习
-
理解卷积和最大池化
-
CNN 的架构
-
在 Keras 中训练 CNN
-
使用迁移学习来利用最先进的神经网络
-
我们结果的分析
技术要求
本章所需的关键 Python 库包括:
-
matplotlib 3.0.2
-
Keras 2.2.4
-
Numpy 1.15.2
-
Piexif 1.1.2
要下载此项目所需的数据集,请参阅github.com/PacktPublishing/Neural-Network-Projects-with-Python/blob/master/Chapter04/how_to_download_the_dataset.txt中的说明。
本章的代码可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Neural-Network-Projects-with-Python。
要将代码下载到您的计算机,请运行以下git clone命令:
$ git clone https://github.com/PacktPublishing/Neural-Network-Projects-with-Python.git
处理完成后,将会有一个名为Neural-Network-Projects-with-Python的文件夹。通过运行以下命令进入该文件夹:
$ cd Neural-Network-Projects-with-Python
要在虚拟环境中安装所需的 Python 库,请运行以下命令:
$ conda env create -f environment.yml
请注意,在运行此命令之前,您应该先在计算机上安装 Anaconda。要进入虚拟环境,请运行以下命令:
$ conda activate neural-network-projects-python
重要提示
本章需要一个额外的图像处理库,名为Piexif。
要下载Piexif,请运行以下命令:
$ pip install piexif
通过运行以下命令,导航到文件夹Chapter04:
$ cd Chapter04
以下文件位于该文件夹中:
-
main_basic_cnn.py:这是基本 CNN 的主要代码 -
main_vgg16.py:这是 VGG16 网络的主要代码 -
utils.py:此文件包含辅助工具代码,帮助我们实现神经网络 -
visualize_dataset.py:此文件包含探索性数据分析和数据可视化的代码 -
image_augmentation.py:此文件包含图像增强的示例代码
要运行神经网络的代码,只需执行main_basic_cnn.py和main_vgg16.py文件:
$ python main_basic_cnn.py
$ python main_vgg16.py
计算机视觉与物体识别
计算机视觉是一个工程领域,目标是创建能够从图像中提取意义的程序。根据一个城市传说,计算机视觉的起源可以追溯到 1960 年代,当时麻省理工学院的马文·敏斯基教授给一群本科生布置了一个暑期项目,要求他们将相机连接到计算机上,并让计算机描述它所看到的一切。该项目预计只需一个夏天便能完成。毫无疑问,由于计算机视觉是一个极其复杂的领域,科学家们直到今天仍在不断研究,它并没有在那个夏天完成。
计算机视觉的早期发展是温和的。20 世纪 60 年代,研究人员开始创建算法来检测照片中的形状、线条和边缘。接下来的几十年里,计算机视觉发展成多个子领域。计算机视觉研究人员致力于信号处理、图像处理、计算机光度学、物体识别等领域。
物体识别可能是计算机视觉中最普遍的应用之一。研究人员已经在物体识别方面工作了很长时间。早期的物体识别研究者面临的挑战是,物体的动态外观使得教计算机识别它们变得非常困难。早期的计算机视觉研究者专注于物体识别中的模板匹配,但由于角度、光照和遮挡的变化,往往遇到困难。
近年来,得益于神经网络和深度学习的进展,物体识别领域得到了飞速发展。2012 年,Alex Krizhevsky 等人凭借ImageNet 大规模视觉识别挑战赛(ILSVRC)以显著优势战胜其他参赛者。Alex Krizhevsky 等人提出的获胜方案是使用 CNN(被称为 AlexNet 的架构)进行物体识别。AlexNet 是物体识别领域的一个重要突破。从那时起,神经网络就成为物体识别和计算机视觉相关任务的首选技术。在本项目中,您将创建一个类似于 AlexNet 的 CNN。
物体识别的突破也促进了今天我们所知的 AI 的崛起。Facebook 使用面部识别技术自动标记和分类你和你朋友的照片。安全系统使用面部识别来检测入侵和嫌疑人。自动驾驶汽车使用物体识别来检测行人、交通标志和其他道路物体。在许多方面,社会开始将物体识别、计算机视觉和 AI 视为一个整体,尽管它们的根源非常不同。
物体识别任务的类型
了解不同种类的物体识别任务非常重要,因为所需的神经网络架构在很大程度上取决于任务的类型。物体识别任务可以大致分为三种类型:
-
图像分类
-
物体检测
-
实例分割
以下图示展示了每个任务之间的区别:

在图像分类中,问题的输入是图像,所需的输出仅仅是对图像所属类别的预测。这类似于我们的第一个项目,在该项目中,我们构建了一个分类器来预测一个病人是否有糖尿病风险。在图像分类中,问题是应用于像素作为输入数据(具体来说,是每个像素的强度值),而不是由 pandas DataFrame 表示的表格数据。在这个项目中,我们将专注于图像分类。
在目标检测中,问题的输入是图像,所需的输出是包围检测到的物体的边界框。你可以将这看作是图像分类任务的升级。神经网络不再假设图像中只有一个类别,而必须假设图像包含多个类别。神经网络接下来需要识别图像中每个类别的存在,并在每个类别周围画出边界框。正如你所想象的,这个任务并不简单,目标检测在神经网络出现之前是一个非常困难的问题。如今,神经网络可以高效地进行目标检测。2014 年,在 AlexNet 首次开发两年后,Girshick 等人展示了图像分类中的结果可以推广到目标 检测。他们方法的直观想法是提出多个可能包含感兴趣物体的框,然后使用 CNN 预测每个边界框内最可能的类别。这种方法被称为区域卷积神经网络(R-CNN)。
最后,在实例分割中,问题的输入是图像,输出是与每个类别对应的像素分组。你可以将实例分割看作是目标检测的细化。实例分割在今天的技术中尤为重要且广泛应用。许多智能手机相机中的人像模式功能依赖于实例分割,将前景物体与背景分离,从而创造出良好的景深(虚化)效果。实例分割在自动驾驶汽车中也至关重要,因为必须精确定位汽车周围每个物体的位置。在 2017 年,R-CNN 的改进版——Mask R-CNN,已被证明在实例分割中非常有效。
正如我们所见,最近在物体识别方面的进展是由 CNN 推动的。在这个项目中,我们将深入了解 CNN,并将在 Keras 中从零开始训练并创建一个 CNN。
数字图像作为神经网络输入
回想一下在前面的章节中,我们曾提到神经网络需要数值输入。我们看到如何使用独热编码将类别特征(如星期几)转换为数值特征。那么,如何将图像作为输入传递给神经网络呢?简短的回答是,所有数字图像本质上都是数值的!
为了理解这一点,考虑一张 28 x 28 的手写数字“3”图像,如下截图所示。假设此时图像是灰度图(黑白图)。如果我们查看组成图像的每个像素的强度,我们会看到某些像素是纯白的,而某些像素是灰色或黑色。在计算机中,白色像素用值 0 表示,黑色像素用值 255 表示。白色和黑色之间的所有其他灰度值(即灰色的不同层次)在 0 和 255 之间。因此,数字图像本质上是数值数据,神经网络可以从中学习:

那么彩色图像呢?彩色图像仅仅是具有三个通道的图像——红色、绿色和蓝色(通常称为 RGB)。每个通道中的像素值代表红色强度、绿色强度和蓝色强度。另一种理解方式是,对于一张纯红色的图像,红色通道的像素值为 255,绿色和蓝色通道的像素值为 0。
下图展示了一幅彩色图像,以及将彩色图像分解为 RGB 通道。请注意,彩色图像是以三维方式堆叠的。相比之下,灰度图像只有两个维度:

卷积神经网络(CNN)的构建模块
图像分类面临的挑战之一是物体的外观是动态的。正如猫和狗有许多不同的品种一样,猫和狗在图像中的呈现方式也是无穷无尽的。这使得基础的图像分类技术变得困难,因为不可能向计算机展示无数张猫狗的照片。
然而,这根本不应该成为问题。人类并不需要无数张猫狗的照片来区分这两者。一个幼儿只需看到几只猫狗,就能轻松区分它们。如果我们思考人类如何进行图像分类,我们会发现人类通常会寻找一些标志性特征来识别物体。例如,我们知道猫的体型通常比狗小,猫通常有尖耳朵,猫的嘴巴比狗短。人类在分类图像时本能地会寻找这些特征。
那么,我们可以教会计算机在整个图像中寻找这些特征吗?答案是肯定的!关键在于卷积。
滤波与卷积
在我们理解卷积之前,首先要理解滤波的概念。
假设我们有一个 9 x 9 的图像作为输入,并且需要将该图像分类为 X 或 O。以下图示展示了一些示例输入图像。
完美绘制的 O 展示在以下图示的最左侧框中,而另外两个框展示了绘制不好的 O:

完美绘制的 X 展示在最左侧框中,而另外两个框展示了绘制不好的 X:

无论哪种情况,我们都不能期望图形被完美地绘制出来。对于人类来说,这并不成问题,因为我们即使面对绘制得很差的情况,也能区分出 O 和 X。
让我们思考一下,是什么让人类容易区分这两者的呢?在图像中,哪些特征使得我们能够轻松区分它们?嗯,我们知道 O 通常具有平坦的水平边缘,而 X 则通常具有对角线。
以下图示展示了 O 的一个特征:

以下图示展示了 X 的一个特征:

在这种情况下,特征(也称为滤波器)的大小为 3 × 3。图像中存在特征时,会给我们提供关于图像类别的重要线索。例如,如果图像包含一个水平边缘,并且特征符合 O 的特征,那么这张图像很可能是 O。
那么,我们如何在图像中搜索这种特征呢?我们可以通过简单的暴力搜索来实现,方法是取一个 3 x 3 的滤波器,然后将它滑过图像中的每个像素,查找匹配项。
我们从图像的左上角开始。滤波器执行的数学操作(称为滤波)是滑动窗口与滤波器进行逐元素乘法。在左上角,滤波器的输出是 2(注意,这是一个完美匹配,因为窗口与滤波器是完全相同的)。
以下图示展示了图像左上角的滤波操作。请注意,为了简化起见,我们假设像素强度值为0或1(而不是实际数字图像中的 0-255):

接下来,我们将窗口向右滑动,覆盖图像中的下一个 3 x 3 区域。以下图示展示了对下一个 3 x 3 区域的滤波操作:

将窗口滑过整个图像并计算滤波值的过程称为卷积。执行卷积操作的神经网络层称为卷积层。本质上,卷积为我们提供了每个图像中找到特征的区域的映射。这确保了我们的神经网络能够像人类一样执行智能、动态的物体识别!
在前面的例子中,我们根据自己对 O 和 X 的了解手工制作了滤波器。请注意,当我们训练神经网络时,它会自动学习最适合的滤波器。回想一下,在前面的章节中,我们使用了全连接层(密集层),并且在训练过程中调整了层的权重。同样,卷积层的权重也将在训练过程中进行调整。
最后,注意卷积层有两个主要的超参数:
-
滤波器数量:在前面的例子中,我们只使用了一个滤波器。我们可以增加滤波器的数量,以找到多个特征。
-
滤波器大小:在前面的例子中,我们使用了一个 3 x 3 的滤波器大小。我们可以调整滤波器的大小,以表示更大的特征。
我们将在本章稍后的部分详细讨论这些超参数,当我们构建神经网络时。
最大池化
在 CNN 中,通常将最大池化层紧跟在卷积层后面。最大池化层的目标是减少每个卷积层之后的权重数量,从而减少模型复杂性并避免过拟合。
最大池化层通过查看传递给它的每个输入子集,并丢弃子集中除最大值以外的所有值,来实现这一点。让我们通过一个例子来看看这意味着什么。假设我们输入到最大池化层的是一个 4 x 4 的张量(张量只是一个 n 维数组,例如卷积层输出的那种),我们使用一个 2 x 2 的最大池化层。下面的图示展示了最大池化操作:

正如我们从前面的图示中看到的,最大池化只是查看输入的每个 2 x 2 区域,并丢弃该区域中除了最大值以外的所有值(前面的图示中已框出)。这实际上将原始输入的高度和宽度减半,在传递到下一层之前减少了参数的数量。
CNN 的基本架构
我们在前一节中已经看到 CNN 的基本构建块。现在,我们将这些构建块组合在一起,看看完整的 CNN 是什么样子。
卷积神经网络(CNN)通常将卷积层和池化层按块堆叠在一起。卷积层使用的激活函数通常是 ReLU,正如前几章所讨论的那样。
下图展示了典型卷积神经网络(CNN)中的前几层,包含了一系列的卷积层和池化层:

CNN 中的最终层将始终是全连接层(密集层),并采用 sigmoid 或 softmax 激活函数。请注意,sigmoid 激活函数用于二分类问题,而 softmax 激活函数则用于多类分类问题。
全连接层与我们在前两章中看到的相同:第一章,机器学习与神经网络基础,以及第二章,多层感知机预测糖尿病。此时,你可能会想,为什么要将全连接层放在 CNN 的末尾呢?在 CNN 中,早期的层学习并提取它们试图预测的数据的特征。例如,我们已经看到卷积层如何学习 Os 和 Xs 的空间特征。卷积层将这些信息传递给全连接层,而全连接层则学习如何进行准确预测,就像在多层感知机(MLP)中一样。
本质上,CNN 的早期层负责识别特征性的空间特征,而末端的全连接层则负责进行预测。这一点的含义非常重要。与我们在上一章中为机器学习算法手动创建特征(例如,星期几、距离等)不同,第三章,利用深度前馈神经网络预测出租车费用,我们只需将所有数据原样提供给 CNN。然后,CNN 会自动学习最佳的特征,以区分不同的类别。这才是真正的人工智能!
现代 CNN 的回顾
现在我们已经了解了 CNN 的基本架构,让我们来看一下现代、最先进的 CNN。我们将回顾 CNN 的发展历程,看看它们是如何随着时间的推移而变化的。我们不会深入讨论实现背后的技术和数学细节,而是提供一些最重要 CNN 的直观概述。
LeNet(1998)
第一款 CNN 是由 Yann LeCun 于 1998 年开发的,架构被称为 LeNet。LeCun 是第一个证明 CNN 在图像识别中有效的人,特别是在手写数字识别领域。然而,在 2000 年代,少数科学家能够在 LeCun 的工作基础上进行拓展,且 CNN(以及人工智能整体)并没有出现显著突破。
AlexNet(2012)
正如我们之前提到的,AlexNet 是由 Alex Krizhevsky 等人开发的,并且它在 2012 年赢得了 ILSVRC。AlexNet 基于与 LeNet 相同的原理,尽管 AlexNet 采用了更深的架构。AlexNet 中的可训练参数总数约为 6000 万个,是 LeNet 的 1000 倍以上。
VGG16(2014)
VGG16 由牛津大学的视觉几何组(VGG)开发,并且被认为是非常重要的神经网络。VGG16 是最早偏离大滤波器尺寸的 CNN 之一,它采用了 3 x 3 的卷积滤波器尺寸。
VGG16 在 2014 年 ILSVRC 的图像识别任务中获得了第二名。VGG16 的一个缺点是需要训练的参数更多,导致训练时间显著增加。
Inception(2014)
Inception 网络由谷歌的研究人员开发,并且在 2014 年赢得了 ILSVRC。Inception 网络的指导原则是高效地提供高度准确的预测。谷歌的兴趣是创建一种能够实时训练并在其服务器网络上部署的 CNN。为此,研究人员开发了一种叫做 Inception 模块的技术,大大提高了训练速度,同时保持了准确性。事实上,在 2014 年 ILSVRC 中,Inception 网络凭借较少的参数实现了比 VGG16 更高的准确率。
Inception 网络已不断改进。到本文写作时,最新的 Inception 网络已是其第四版本(通常称为 Inception-v4)。
ResNet(2015)
残差神经网络(ResNet)是由 Kaiming He 等人于 2015 年 ILSVRC 上提出的(到现在,你应该注意到这个竞赛对神经网络和计算机视觉至关重要,每年竞赛期间都会揭示新的最先进技术)。
ResNet 的显著特点是残差块技术,它使神经网络能够更深,同时保持参数数量适中。
我们今天所处的位置
正如我们所见,近年来 CNN 已经取得了指数级的进展和改进。事实上,最近的 CNN 在某些图像识别任务上能够超过人类。近年来的一个反复主题是使用创新技术提升模型性能,同时保持模型的复杂性。显然,神经网络的速度与准确性同样重要。
猫狗数据集
现在我们理解了 CNN 背后的理论,让我们深入探讨数据探索。猫狗数据集由微软提供。下载和设置数据集的说明可以在本章的技术要求部分找到。
让我们绘制图像,更好地理解我们正在处理的数据类型。为此,我们只需运行以下代码:
from matplotlib import pyplot as plt
import os
import random
# Get list of file names
_, _, cat_images = next(os.walk('Dataset/PetImages/Cat'))
# Prepare a 3x3 plot (total of 9 images)
fig, ax = plt.subplots(3,3, figsize=(20,10))
# Randomly select and plot an image
for idx, img in enumerate(random.sample(cat_images, 9)):
img_read = plt.imread('Dataset/PetImages/Cat/'+img)
ax[int(idx/3), idx%3].imshow(img_read)
ax[int(idx/3), idx%3].axis('off')
ax[int(idx/3), idx%3].set_title('Cat/'+img)
plt.show()
我们将看到以下输出:

我们可以对数据做出一些观察:
-
这些图像具有不同的尺寸。
-
这些物体(猫/狗)大多数情况下位于图像的中心。
-
这些物体(猫/狗)有不同的方向,而且它们可能在图像中被遮挡。换句话说,并不能保证我们总是能在图像中看到猫的尾巴。
现在,让我们对狗的图像做同样的操作:
# Get list of file names
_, _, dog_images = next(os.walk('Dataset/PetImages/Dog'))
# Prepare a 3x3 plot (total of 9 images)
fig, ax = plt.subplots(3,3, figsize=(20,10))
# Randomly select and plot an image
for idx, img in enumerate(random.sample(dog_images, 9)):
img_read = plt.imread('Dataset/PetImages/Dog/'+img)
ax[int(idx/3), idx%3].imshow(img_read)
ax[int(idx/3), idx%3].axis('off')
ax[int(idx/3), idx%3].set_title('Dog/'+img)
plt.show()
我们将看到如下输出:

管理 Keras 的图像数据
在图像分类的神经网络项目中,一个常见的问题是大多数计算机的内存不足,无法将整个数据集加载到内存中。即便是相对现代且强大的计算机,将整个图像集加载到内存并从中训练卷积神经网络(CNN)也会非常缓慢。
为了缓解这个问题,Keras 提供了一个有用的 flow_from_directory 方法,该方法接受图像路径作为输入,并生成数据批次作为输出。数据批次会在模型训练之前根据需要加载到内存中。这样,我们可以在大量图像上训练深度神经网络,而无需担心内存问题。此外,flow_from_directory 方法通过简单地传递参数,允许我们执行图像预处理步骤,例如调整图像大小和其他图像增强技术。该方法随后会在实时处理图像数据之前,执行必要的图像预处理步骤。
为了完成这些操作,我们必须遵循某些文件和文件夹管理的方案,以确保 flow_from_directory 能正常工作。特别是,我们需要为训练和测试数据创建子目录,并且在训练和测试子目录中,我们需要进一步为每个类别创建一个子目录。以下图示说明了所需的文件夹结构:

flow_from_directory 方法随后会根据文件夹结构推断图像的类别。
原始数据提供在 Cat 和 Dog 文件夹中,没有区分训练数据和测试数据。因此,我们需要按照之前的方案将数据拆分为 Train 和 Test 文件夹。为此,我们需要执行以下步骤:
-
创建
/Train/Cat、/Train/Dog、/Test/Cat和/Test/Dog文件夹。 -
随机将 80% 的图像分配为训练图像,20% 的图像分配为测试图像。
-
将这些图像复制到相应的文件夹中。
我们在 utils.py 中提供了一个辅助函数来执行这些步骤。我们只需要调用该函数,如下所示:
from utils import train_test_split
src_folder = 'Dataset/PetImages/'
train_test_split(src_folder)
如果在执行此代码块时遇到错误,错误信息为 ImportError: No Module Named Piexif,这意味着你尚未在 Python 虚拟环境中安装 Piexif。本章节需要一个额外的图像处理库。要下载 Piexif,请遵循本章节开始时的技术要求部分中的说明。
太棒了!我们的图像现在已经放置在 Keras 所需的适当文件夹中了。
图像增强
在我们开始构建 CNN 之前,先来看看图像增强,它是图像分类项目中一个重要的技术。图像增强是通过对图像进行某些方式的微小修改,创造出额外的训练数据,从而生成新图像。例如,我们可以做以下操作:
-
图像旋转
-
图像平移
-
水平翻转
-
对图像进行缩放
图像增强的动机是,CNN 需要大量的训练数据才能很好地泛化。然而,收集数据常常很困难,尤其是对于图像来说。通过图像增强,我们可以基于现有图像人工地创造新的训练数据。
和往常一样,Keras 提供了一个方便的ImageDataGenerator类,帮助我们轻松地执行图像增强。让我们创建这个类的一个新实例:
from keras.preprocessing.image import ImageDataGenerator
image_generator = ImageDataGenerator(rotation_range = 30,
width_shift_range = 0.2,
height_shift_range = 0.2,
zoom_range = 0.2,
horizontal_flip=True,
fill_mode='nearest')
ImageDataGenerator class. Each of the arguments control how much of a modification is done to the existing image. We should avoid extreme transformations, as those extremely distorted images do not represent images from the real world and may introduce noise into our model.
接下来,让我们用它来增强从/Train/Dog/文件夹中随机选择的一张图像。然后,我们可以绘制它,将增强后的图像与原始图像进行比较。我们可以通过运行以下代码来实现:
fig, ax = plt.subplots(2,3, figsize=(20,10))
all_images = []
_, _, dog_images = next(os.walk('Dataset/PetImages/Train/Dog/'))
random_img = random.sample(dog_images, 1)[0]
random_img = plt.imread('Dataset/PetImages/Train/Dog/'+random_img)
all_images.append(random_img)
random_img = random_img.reshape((1,) + random_img.shape)
sample_augmented_images = image_generator.flow(random_img)
for _ in range(5):
augmented_imgs = sample_augmented_images.next()
for img in augmented_imgs:
all_images.append(img.astype('uint8'))
for idx, img in enumerate(all_images):
ax[int(idx/3), idx%3].imshow(img)
ax[int(idx/3), idx%3].axis('off')
if idx == 0:
ax[int(idx/3), idx%3].set_title('Original Image')
else:
ax[int(idx/3), idx%3].set_title('Augmented Image {}'.format(idx))
plt.show()
我们将看到如下输出:

正如我们所看到的,每个增强后的图像都会按照传递给ImageDataGenerator类的参数随机地平移或旋转。这些增强后的图像将为我们的 CNN 提供补充的训练数据,提高我们模型的鲁棒性。
模型构建
我们终于准备好开始在 Keras 中构建 CNN 了。在这一部分,我们将采用两种不同的方法来构建模型。首先,我们从构建一个包含几层的相对简单的 CNN 开始。我们将查看简单模型的性能,并讨论它的优缺点。接下来,我们将使用一个几年前被认为是最先进的模型——VGG16 模型。我们将看看如何利用预训练的权重将 VGG16 模型调整用于猫狗图像分类。
构建一个简单的 CNN
在前面的部分,我们展示了 CNN 的基本构建模块,它由一系列卷积层和池化层组成。在这一部分,我们将构建一个由这些重复模式组成的基本 CNN,如下图所示:

这个基础 CNN 由两个重复的卷积和最大池化块组成,然后是两个全连接层。正如前面部分所讨论的,卷积和最大池化层负责学习类的空间特征(例如,识别猫的耳朵),而全连接层则使用这些空间特征进行预测。因此,我们可以以另一种方式表示我们基本 CNN 的架构(我们将在下一小节中看到以这种方式可视化神经网络的好处):

构建 CNN 类似于构建多层感知器(MLP)或前馈神经网络,就像我们在前几章中所做的那样。我们将通过声明一个新的Sequential模型实例开始:
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Dropout, Flatten, Dense
from keras.preprocessing.image import ImageDataGenerator
model = Sequential()
在添加任何卷积层之前,考虑我们将使用的超参数是非常有用的。对于卷积神经网络(CNN),有几个超参数:
-
卷积层滤波器大小:大多数现代 CNN 使用较小的滤波器大小
3x3。 -
滤波器数量:我们将使用
32个滤波器。这个数量在速度和性能之间达到了良好的平衡。 -
输入大小:正如我们在前面部分看到的,输入图像的大小各不相同,宽度和高度大约为 150 像素。我们将使用
32x32像素的输入大小。这会压缩原始图像,可能会导致一些信息丢失,但有助于加速神经网络的训练。 -
最大池化大小:常见的最大池化大小为
2x2。这将把输入层的维度减半。 -
批量大小:这对应于每个小批量中使用的训练样本数。在梯度下降中,较大的批量大小会导致更准确的训练,但训练时间更长,内存使用量也更大。我们将使用
16的批量大小。 -
每轮步数:这是每个训练周期中的迭代次数。通常,这个值等于训练样本数除以批量大小。
-
训练轮数:用于训练我们数据的轮数。请注意,在神经网络中,轮数指的是模型在训练过程中每次看到每个训练样本的次数。通常需要多个轮次,因为梯度下降是一种迭代优化方法。我们将训练模型
10轮。这意味着每个训练样本将在训练过程中传递给模型 10 次。
让我们为这些超参数声明变量,以便在代码中保持一致:
FILTER_SIZE = 3
NUM_FILTERS = 32
INPUT_SIZE = 32
MAXPOOL_SIZE = 2
BATCH_SIZE = 16
STEPS_PER_EPOCH = 20000//BATCH_SIZE
EPOCHS = 10
现在我们可以添加第一层卷积层,使用32个滤波器,每个滤波器的大小为(3 x 3):
model.add(Conv2D(NUM_FILTERS, (FILTER_SIZE, FILTER_SIZE),
input_shape = (INPUT_SIZE, INPUT_SIZE, 3),
activation = 'relu'))
接下来,我们添加一个最大池化层:
model.add(MaxPooling2D(pool_size = (MAXPOOL_SIZE, MAXPOOL_SIZE)))
这是我们 CNN 的基本卷积-池化模式。根据我们的模型架构,我们再重复一遍:
model.add(Conv2D(NUM_FILTERS, (FILTER_SIZE, FILTER_SIZE),
input_shape = (INPUT_SIZE, INPUT_SIZE, 3),
activation = 'relu'))
model.add(MaxPooling2D(pool_size = (MAXPOOL_SIZE, MAXPOOL_SIZE)))
我们现在完成了卷积层和池化层的设置。在继续添加全连接层之前,我们需要将输入进行展平。Flatten是 Keras 中的一个函数,它将一个多维向量转换为一个一维向量。例如,如果向量的形状是(5,5,3),在传递给Flatten之前,输出向量将变成形状为(75)的向量。
要添加一个Flatten层,我们只需运行以下代码:
model.add(Flatten())
现在我们可以添加一个全连接层,节点数为128:
model.add(Dense(units = 128, activation = 'relu'))
在添加最后一层全连接层之前,添加一个 Dropout 层是一个好习惯。Dropout 层会随机将输入的一部分设置为 0,从而帮助减少过拟合,确保模型不会过度依赖某些权重:
# Set 50% of the weights to 0
model.add(Dropout(0.5))
我们为模型添加了最后一个全连接层:
model.add(Dense(units = 1, activation = 'sigmoid'))
请注意,最后一层全连接层应该只有一个节点,因为我们在这个项目中进行的是二分类(猫或狗)。
我们将使用adam优化器编译我们的模型。adam优化器是随机梯度下降(SGD)算法的一个泛化,它在第一章《机器学习与神经网络 101》中介绍过,并且被广泛用于训练 CNN 模型。损失函数是binary_crossentropy,因为我们进行的是二分类:
model.compile(optimizer = 'adam', loss = 'binary_crossentropy',
metrics = ['accuracy'])
通常情况下,我们对于二分类问题使用binary_crossentropy,对于多分类问题使用categorical_crossentropy。
我们现在准备训练我们的 CNN。请注意,我们并没有将任何数据加载到内存中。我们将使用ImageDataGenerator和flow_from_directory方法实时训练我们的模型,该方法只在需要时将数据集的批次加载到内存中:
training_data_generator = ImageDataGenerator(rescale = 1./255)
training_set = training_data_generator. \
flow_from_directory('Dataset/PetImages/Train/',
target_size=(INPUT_SIZE,INPUT_SIZE),
batch_size=BATCH_SIZE,
class_mode='binary')
model.fit_generator(training_set, steps_per_epoch = STEPS_PER_EPOCH,
epochs=EPOCHS, verbose=1)
这将开始训练,一旦完成,你将看到以下输出:

我们可以清楚地看到,随着每个周期的进行,损失值在减少,而准确率在提高。
现在我们的模型已经训练完成,接下来让我们在测试集上评估它。我们将创建一个新的ImageDataGenerator,并对test文件夹中的图像调用flow_from_directory:
testing_data_generator = ImageDataGenerator(rescale = 1./255)
test_set = testing_data_generator. \
flow_from_directory('Dataset/PetImages/Test/',
target_size=(INPUT_SIZE,INPUT_SIZE),
batch_size=BATCH_SIZE,
class_mode = 'binary')
score = model.evaluate_generator(test_set, steps=len(test_set))
for idx, metric in enumerate(model.metrics_names):
print("{}: {}".format(metric, score[idx]))
我们将获得以下输出:

我们获得了 80%的准确率!考虑到我们仅使用了一个基础的 CNN,这相当令人印象深刻。这显示了 CNN 的强大功能;我们仅用几行代码就得到了接近人类表现的准确率。
利用迁移学习的预训练模型
我们能将模型推得更远吗?我们能达到接近 90%的准确率,接近人类水平的表现吗?正如我们在本节中将看到的那样,通过利用迁移学习,我们可以获得更好的性能。
迁移学习是机器学习中的一种技术,它通过将为某个任务训练的模型修改为用于另一个任务的预测。例如,我们可以使用训练好的汽车分类模型来分类卡车,因为它们相似。在卷积神经网络(CNN)的背景下,迁移学习涉及冻结卷积池化层,只重新训练最后的全连接层。以下图示说明了这一过程:

迁移学习是如何工作的?直观地说,卷积层和池化层的目的是学习类别的空间特征。因此,我们可以重用这些层,因为这两个任务的空间特征是相似的。我们只需要重新训练最后的全连接层,将神经网络重新定向到新类别的预测。自然,迁移学习的一个关键要求是任务 A 和任务 B 必须相似。
在本节中,我们将重新利用 VGG16 模型来对猫狗图像进行预测。VGG16 模型最初是为 ILSVRC 开发的,需要模型进行 1,000 类的多类别分类。这 1,000 类中包含了特定品种的猫和狗。换句话说,VGG16 知道如何识别特定品种的猫和狗,而不仅仅是猫和狗。因此,利用 VGG16 模型进行迁移学习来解决我们的猫狗图像分类问题是一种可行的方法。
VGG16 模型及其训练权重在 Keras 中已直接提供。我们可以按照以下代码创建一个新的VGG16模型:
from keras.applications.vgg16 import VGG16
INPUT_SIZE = 128 # Change this to 48 if the code takes too long to run
vgg16 = VGG16(include_top=False, weights='imagenet',
input_shape=(INPUT_SIZE,INPUT_SIZE,3))
请注意,我们在创建新的 VGG16 模型时使用了include_top=False。这个参数告诉 Keras 不要导入 VGG16 网络末尾的全连接层。
现在,我们将冻结 VGG16 模型中其余的层,因为我们不打算从头开始重新训练它们。我们可以通过运行以下代码片段来冻结这些层:
for layer in vgg16.layers:
layer.trainable = False
接下来,我们将在神经网络的最后添加一个带有1个节点的全连接层。实现这一点的语法稍有不同,因为 VGG16 模型不是我们习惯的 Keras Sequential 模型。无论如何,我们可以通过运行以下代码来添加这些层:
from keras.models import Model
input_ = vgg16.input
output_ = vgg16(input_)
last_layer = Flatten(name='flatten')(output_)
last_layer = Dense(1, activation='sigmoid')(last_layer)
model = Model(input=input_, output=last_layer)
这只是手动在 Keras 中添加层的方式,而.``add()函数在Sequential模型中已经简化了这个过程。其余的代码与我们在前一节中看到的类似。我们声明一个训练数据生成器,并通过调用flow_from_directory()来训练模型(仅训练新添加的层)。由于我们只需要训练最后一层,我们将仅训练模型3个周期:
注意
如果您没有在 GPU(显卡)上运行 Keras,以下代码块大约需要一个小时才能完成。如果代码在您的计算机上运行得太慢,您可以减少INPUT_SIZE参数来加速模型训练。然而,请注意,这样会降低模型的准确性。
# Define hyperparameters
BATCH_SIZE = 16
STEPS_PER_EPOCH = 200
EPOCHS = 3
model.compile(optimizer = 'adam', loss = 'binary_crossentropy',
metrics = ['accuracy'])
training_data_generator = ImageDataGenerator(rescale = 1./255)
testing_data_generator = ImageDataGenerator(rescale = 1./255)
training_set = training_data_generator. \
flow_from_directory('Dataset/PetImages/Train/',
target_size=(INPUT_SIZE,INPUT_SIZE),
batch_size = BATCH_SIZE,
class_mode = 'binary')
test_set = testing_data_generator. \
flow_from_directory('Dataset/PetImages/Test/',
target_size=(INPUT_SIZE,INPUT_SIZE),
batch_size = BATCH_SIZE,
class_mode = 'binary')
model.fit_generator(training_set, steps_per_epoch = STEPS_PER_EPOCH,
epochs = EPOCHS, verbose=1)
我们将看到以下输出:

训练准确度看起来与前一节中的基本 CNN 模型差别不大。这是预期的,因为两个神经网络在训练集上表现都非常好。然而,最终我们将使用测试准确度来评估模型的性能。让我们看看它在测试集上的表现如何:
score = model.evaluate_generator(test_set, len(test_set))
for idx, metric in enumerate(model.metrics_names):
print("{}: {}".format(metric, score[idx]))
我们将看到以下输出:

真了不起!通过使用迁移学习,我们成功地获得了 90.5%的测试准确率。请注意,这里的训练时间远远短于从头开始训练 VGG16 模型的时间(即使有强大的 GPU,也可能需要几天才能从头训练一个 VGG16 模型!),因为我们只训练了最后一层。这表明,我们可以利用像 VGG16 这样的预训练的最先进模型来为我们自己的项目进行预测。
结果分析
让我们更深入地分析我们的结果。特别是,我们希望了解我们的 CNN 在哪些图像上表现良好,哪些图像会出错。
记住,我们的 CNN 最后一层的 sigmoid 激活函数的输出是一个 0 到 1 之间的值列表(每个图像一个值/预测)。如果输出值< 0.5,那么预测为类别 0(即猫),如果输出值>= 0.5,则预测为类别 1(即狗)。因此,接近0.5的输出值意味着模型不太确定,而非常接近0.0或1.0的输出值则表示模型对其预测非常确信。
让我们逐一运行测试集中的图像,使用我们的模型对图像的类别进行预测,并将图像按三类进行分类:
-
强烈正确的预测:模型正确预测了这些图像,输出值为
> 0.8或< 0.2 -
强烈错误的预测:模型错误地预测了这些图像,输出值为
> 0.8或< 0.2 -
弱错误的预测:模型错误预测了这些图像,输出值介于
0.4和0.6之间
以下代码片段将为我们完成此操作:
# Generate test set for data visualization
test_set = testing_data_generator. \
flow_from_directory('Dataset/PetImages/Test/',
target_size = (INPUT_SIZE,INPUT_SIZE),
batch_size = 1,
class_mode = 'binary')
strongly_wrong_idx = []
strongly_right_idx = []
weakly_wrong_idx = []
for i in range(test_set.__len__()):
img = test_set.__getitem__(i)[0]
pred_prob = model.predict(img)[0][0]
pred_label = int(pred_prob > 0.5)
actual_label = int(test_set.__getitem__(i)[1][0])
if pred_label != actual_label and (pred_prob > 0.8 or
pred_prob < 0.2): strongly_wrong_idx.append(i)
elif pred_label != actual_label and (pred_prob > 0.4 and
pred_prob < 0.6): weakly_wrong_idx.append(i)
elif pred_label == actual_label and (pred_prob > 0.8 or
pred_prob < 0.2): strongly_right_idx.append(i)
# stop once we have enough images to plot
if (len(strongly_wrong_idx)>=9 and len(strongly_right_idx)>=9
and len(weakly_wrong_idx)>=9): break
让我们通过随机选择每组中的9张图像,并将它们绘制在一个 3×3 的网格中,来可视化这三组图像。以下辅助函数可以帮助我们做到这一点:
from matplotlib import pyplot as plt
import random
def plot_on_grid(test_set, idx_to_plot, img_size=INPUT_SIZE):
fig, ax = plt.subplots(3,3, figsize=(20,10))
for i, idx in enumerate(random.sample(idx_to_plot,9)):
img = test_set.__getitem__(idx)[0].reshape(img_size, img_size ,3)
ax[int(i/3), i%3].imshow(img)
ax[int(i/3), i%3].axis('off')
我们现在可以绘制9张从强烈正确预测组中随机选出的图像:
plot_on_grid(test_set, strongly_right_idx)
我们将看到以下输出:

选中的图像具有强烈的预测,并且预测正确
没有什么意外!这些几乎是经典的猫狗图像。请注意,猫的尖耳朵和狗的黑眼睛在前面的图像中都有体现。这些特征使我们的 CNN 能够轻松地识别它们。
现在让我们来看一下强烈错误预测组:
plot_on_grid(test_set, strongly_wrong_idx)
我们将得到以下输出:

选中的图像具有强烈的预测,但预测错误
我们注意到这些强烈错误预测中有一些共同点。首先,我们发现某些狗确实像猫一样有尖耳朵。也许我们的神经网络过分关注尖耳朵,将这些狗误分类为猫。另一个发现是,某些主体没有正对相机,这使得识别变得异常困难。难怪我们的神经网络将它们分类错误。
最后,我们来看一下预测弱错误的图像组:
plot_on_grid(test_set, weakly_wrong_idx)
我们将得到以下输出:

选择预测较弱且错误的图像
这些图像是我们的模型在分类时存在不确定性的情况。可能有相同数量的特征暗示对象既可能是狗也可能是猫。最明显的例子出现在第一行的图像中,第一行的小狗具有像猫一样的小框架,这可能使神经网络感到困惑。
总结
在本章中,我们构建了一个分类器,通过使用两种不同的卷积神经网络(CNN)来预测图像中是否包含猫或狗。我们首先学习了 CNN 背后的理论,并理解了 CNN 的基本构建模块是卷积层、池化层和全连接层。特别地,CNN 的前端由一块卷积-池化层组成,重复若干次。这个模块负责识别图像中的空间特征,这些特征可用于对图像进行分类。CNN 的后端由全连接层组成,类似于多层感知机(MLP)。这个模块负责做出最终的预测。
在第一个 CNN 中,我们使用了一个基础架构,在测试集上达到了 80%的准确率。这个基础 CNN 由两层卷积-最大池化层组成,后面接着两层全连接层。在第二个 CNN 中,我们使用了迁移学习,借助预训练的 VGG16 网络进行分类。我们移除了预训练网络中具有 1,000 个节点的最后一层全连接层,并添加了我们自己的全连接层(仅一个节点,用于二分类任务)。我们通过微调 VGG16 模型成功地获得了 90%的准确率。
最后,我们可视化了模型表现良好的图像和模型表现不佳的图像。我们发现,当主体没有正对相机时,或当主体具有既像猫又像狗的特征时,我们的模型无法确定。这一点在第一行的图像中尤为明显,第一行的小狗耳朵尖尖,可能让神经网络感到困惑。
本章内容到此为止,关于使用 CNN 进行图像识别的部分已结束。在下一章,第五章,使用自编码器去除图像噪声,我们将使用自编码器神经网络去除图像中的噪声。
问题
- 图像是如何在计算机中表示的?
图像在计算机中表示为一组像素,每个像素有自己的强度(值在 0 到 255 之间)。彩色图像有三个通道(红色、绿色和蓝色),而灰度图像只有一个通道。
- CNN 的基本构建模块是什么?
所有卷积神经网络由卷积层、池化层和全连接层组成。
- 卷积层和池化层的作用是什么?
卷积层和池化层负责从图像中提取空间特征。例如,在训练 CNN 识别猫的图像时,其中一个空间特征可能是猫的尖耳朵。
- 全连接层的作用是什么?
全连接层类似于多层感知机(MLPs)和前馈神经网络中的全连接层。它们的作用是使用空间特征作为输入,并输出预测的类别。
- 什么是迁移学习,它如何发挥作用?
迁移学习是一种机器学习技术,其中为某个任务训练的模型被修改以预测另一个任务。迁移学习使我们能够利用最先进的模型,如 VGG16,来为我们的目的提供支持,并且训练时间最小化。
第五章:使用自编码器去除图像噪声
在本章中,我们将研究一类神经网络,称为自编码器,这些网络近年来得到了广泛关注。特别是,自编码器去除图像噪声的能力已经得到了大量研究。在本章中,我们将构建并训练一个能够去噪并恢复损坏图像的自编码器。
在本章中,我们将涵盖以下主题:
-
什么是自编码器?
-
无监督学习
-
自编码器的类型——基础自编码器、深度自编码器和卷积自编码器
-
用于图像压缩的自编码器
-
用于图像去噪的自编码器
-
构建和训练自编码器的逐步指南(在 Keras 中)
-
我们结果的分析
技术要求
本章所需的 Python 库有:
-
matplotlib 3.0.2
-
Keras 2.2.4
-
Numpy 1.15.2
-
PIL 5.4.1
本章的代码和数据集可以在本书的 GitHub 仓库找到:github.com/PacktPublishing/Neural-Network-Projects-with-Python
要将代码下载到计算机中,您可以运行以下 git clone 命令:
$ git clone https://github.com/PacktPublishing/Neural-Network-Projects-with-Python.git
处理完成后,将会有一个名为 Neural-Network-Projects-with-Python 的文件夹。通过运行以下命令进入该文件夹:
$ cd Neural-Network-Projects-with-Python
要在虚拟环境中安装所需的 Python 库,请运行以下命令:
$ conda env create -f environment.yml
请注意,在运行此命令之前,您应该首先在计算机中安装 Anaconda。要进入虚拟环境,请运行以下命令:
$ conda activate neural-network-projects-python
通过运行以下命令导航到 Chapter05 文件夹:
$ cd Chapter05
以下文件位于文件夹中:
-
autoencoder_image_compression.py:这是本章 构建一个简单的自编码器 部分的代码 -
basic_autoencoder_denoise_MNIST.py和conv_autoencoder_denoise_MNIST.py:这些是本章 去噪自编码器 部分的代码 -
basic_autoencoder_denoise_documents.py和deep_conv_autoencoder_denoise_documents.py:这些是本章 用自编码器去噪文档 部分的代码
要运行每个文件中的代码,只需执行每个 Python 文件,如下所示:
$ python autoencoder_image_compression.py
什么是自编码器?
到目前为止,在本书中,我们已经看过了神经网络在监督学习中的应用。具体来说,在每个项目中,我们都有一个带标签的数据集(即特征x和标签y),我们的目标是利用这个数据集训练一个神经网络,使得神经网络能够从任何新的实例x中预测标签y。
一个典型的前馈神经网络如下面的图所示:

在本章中,我们将研究一类不同的神经网络,称为自编码器。自编码器代表了迄今为止我们所见过的传统神经网络的一种范式转变。自编码器的目标是学习输入的潜在 表示。这种表示通常是原始输入的压缩表示。
所有的自编码器都有一个编码器和一个解码器。编码器的作用是将输入编码为学习到的压缩表示,解码器的作用是使用压缩表示重构原始输入。
以下图表显示了典型自编码器的架构:

注意,在前面的图表中,与 CNNs 不同,我们不需要标签y。这个区别意味着自编码器是一种无监督学习形式,而 CNNs 属于监督学习的范畴。
潜在表示
此时,你可能会想知道自编码器的目的是什么。为什么我们要学习原始输入的表示,然后再重建一个类似的输出?答案在于输入的学习表示。通过强制学习到的表示被压缩(即与输入相比具有较小的维度),我们实质上迫使神经网络学习输入的最显著表示。这确保了学习到的表示仅捕捉输入的最相关特征,即所谓的潜在表示。
作为潜在表示的一个具体例子,例如,一个在猫和狗数据集上训练的自编码器,如下图所示:

在这个数据集上训练的自编码器最终将学习到猫和狗的显著特征是耳朵的形状、胡须的长度、吻的大小和可见的舌头长度。这些显著特征被潜在表示捕捉到。
利用这个由自编码器学习到的潜在表示,我们可以做以下工作:
-
减少输入数据的维度。潜在表示是输入数据的自然减少表示。
-
从输入数据中去除任何噪音(称为去噪)。噪音不是显著特征,因此应该通过使用潜在表示轻松识别。
在接下来的章节中,我们将为每个前述目的创建和训练自编码器。
请注意,在前面的例子中,我们已经使用了如耳朵形状和吻大小等描述作为潜在表示的描述。实际上,潜在表示只是一组数字的矩阵,不可能为其分配有意义的标签(也不需要)。我们在这里使用的描述仅仅为潜在表示提供了直观的解释。
用于数据压缩的自编码器
到目前为止,我们已经看到自编码器能够学习输入数据的简化表示。自然地,我们会认为自编码器在通用数据压缩方面做得很好。然而,事实并非如此。自编码器在通用数据压缩方面表现不佳,比如图像压缩(即 JPEG)和音频压缩(即 MP3),因为学习到的潜在表示仅代表它所训练过的数据。换句话说,自编码器只对与其训练数据相似的图像有效。
此外,自编码器是一种“有损”数据压缩形式,这意味着自编码器的输出相较于原始输入会丢失一些信息。这些特性使得自编码器在作为通用数据压缩技术时效果较差。其他数据压缩形式,如 JPEG 和 MP3,相较于自编码器更为优越。
MNIST 手写数字数据集
本章中我们将使用的一个数据集是 MNIST 手写数字数据集。MNIST 数据集包含 70,000 个手写数字样本,每个样本的大小为 28 x 28 像素。每个样本图像只包含一个数字,且所有样本都有标签。
MNIST 数据集在 Keras 中直接提供,我们只需运行以下代码即可导入:
from keras.datasets import mnist
training_set, testing_set = mnist.load_data()
X_train, y_train = training_set
X_test, y_test = testing_set
matplotlib to plot the data:
from matplotlib import pyplot as plt
fig, ((ax1, ax2, ax3, ax4, ax5), (ax6, ax7, ax8, ax9, ax10)) = plt.subplots(2, 5, figsize=(10,5))
for idx, ax in enumerate([ax1,ax2,ax3,ax4,ax5, ax6,ax7,ax8,ax9,ax10]):
for i in range(1000):
if y_test[i] == idx:
ax.imshow(X_test[i], cmap='gray')
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
break
plt.tight_layout()
plt.show()
我们得到以下输出:

我们可以看到这些数字确实是手写的,每个 28 x 28 的图像只包含一个数字。自编码器应该能够学习这些数字的压缩表示(小于 28 x 28),并使用这种压缩表示重建图像。
下图展示了这一点:

构建一个简单的自编码器
为了巩固我们的理解,让我们从构建最基本的自编码器开始,如下图所示:

到目前为止,我们强调过隐藏层(潜在 表示)的维度应该小于输入数据的维度。这样可以确保潜在表示是输入显著特征的压缩表示。那么,隐藏层应该有多小呢?
理想情况下,隐藏层的大小应当在以下两者之间保持平衡:
-
足够小,以便表示输入特征的压缩表示
-
足够大,以便解码器能够重建原始输入而不会有过多的损失
换句话说,隐藏层的大小是一个超参数,我们需要仔细选择以获得最佳结果。接下来,我们将看看如何在 Keras 中定义隐藏层的大小。
在 Keras 中构建自编码器
首先,让我们开始在 Keras 中构建我们的基本自编码器。和往常一样,我们将使用 Keras 中的 Sequential 类来构建我们的模型。
我们将从导入并定义一个新的 Keras Sequential 类开始:
from keras.models import Sequential
model = Sequential()
接下来,我们将向模型中添加隐藏层。从前面的图示中,我们可以清楚地看到隐藏层是一个全连接层(即一个Dense层)。在 Keras 的Dense类中,我们可以通过units参数定义隐藏层的大小。单元数量是一个超参数,我们将进行实验。现在,我们暂时使用一个节点(units=1)作为隐藏层。Dense层的input_shape是一个大小为784的向量(因为我们使用的是 28 x 28 的图像),activation函数是relu激活函数。
以下代码为我们的模型添加了一个包含单个节点的Dense层:
from keras.layers import Dense
hidden_layer_size = 1
model.add(Dense(units=hidden_layer_size, input_shape=(784,),
activation='relu'))
最后,我们将添加输出层。输出层也是一个全连接层(即一个Dense层),输出层的大小应该是784,因为我们试图输出原始的 28 x 28 图像。我们使用Sigmoid激活函数来约束输出值(每个像素的值)在 0 和 1 之间。
以下代码为我们的模型添加了一个包含784个单元的输出Dense层:
model.add(Dense(units=784, activation='sigmoid'))
在训练我们的模型之前,让我们检查模型的结构,确保它与我们的图示一致。
我们可以通过调用summary()函数来实现这一点:
model.summary()
我们得到了以下输出:

在我们进入下一步之前,让我们创建一个封装模型创建过程的函数。拥有这样的函数是有用的,因为它可以让我们轻松创建具有不同隐藏层大小的不同模型。
以下代码定义了一个创建基本自编码器的函数,其中包含hidden_layer_size变量:
def create_basic_autoencoder(hidden_layer_size):
model = Sequential()
model.add(Dense(units=hidden_layer_size, input_shape=(784,),
activation='relu'))
model.add(Dense(units=784, activation='sigmoid'))
return model
model = create_basic_autoencoder(hidden_layer_size=1)
下一步是预处理我们的数据。需要两个预处理步骤:
-
将图像从 28 x 28 的向量重塑为 784 x 1 的向量。
-
将向量的值从当前的 0 到 255 规范化到 0 和 1 之间。这个较小的值范围使得使用数据训练神经网络变得更加容易。
为了将图像从 28 x 28 的大小重塑为 784 x 1,我们只需运行以下代码:
X_train_reshaped = X_train.reshape((X_train.shape[0],
X_train.shape[1]*X_train.shape[2]))
X_test_reshaped = X_test.reshape((X_test.shape[0],
X_test.shape[1]*X_test.shape[2]))
请注意,第一个维度,X_train.shape[0],表示样本的数量。
为了将向量的值从 0 到 255 规范化到 0 和 1 之间,我们运行以下代码:
X_train_reshaped = X_train_reshaped/255.
X_test_reshaped = X_test_reshaped/255.
完成这些后,我们可以开始训练我们的模型。我们将首先使用adam优化器并将mean_squared_error作为loss函数来编译我们的模型。mean_squared_error在这种情况下是有用的,因为我们需要一个loss函数来量化输入与输出之间逐像素的差异。
以下代码使用上述参数编译我们的模型:
model.compile(optimizer='adam', loss='mean_squared_error')
最后,让我们训练我们的模型10个周期。请注意,我们使用X_train_reshaped作为输入(x)和输出(y)。这是合理的,因为我们试图训练自编码器使输出与输入完全相同。
我们使用以下代码训练我们的自编码器:
model.fit(X_train_reshaped, X_train_reshaped, epochs=10)
我们将看到以下输出:

在模型训练完成后,我们将其应用于测试集:
output = model.predict(X_test_reshaped)
我们希望绘制输出图像,并看看它与原始输入的匹配程度。记住,自编码器应该生成与原始输入图像相似的输出图像。
以下代码从测试集中随机选择五个图像,并将它们绘制在顶部行。然后,它将这五个随机选择的输入的输出图像绘制在底部行:
import random
fig, ((ax1, ax2, ax3, ax4, ax5),
(ax6, ax7, ax8, ax9, ax10)) = plt.subplots(2, 5, figsize=(20,7))
# randomly select 5 images
randomly_selected_imgs = random.sample(range(output.shape[0]),5)
# plot original images (input) on top row
for i, ax in enumerate([ax1,ax2,ax3,ax4,ax5]):
ax.imshow(X_test[randomly_selected_imgs[i]], cmap='gray')
if i == 0:
ax.set_ylabel("INPUT",size=40)
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
# plot output images from our autoencoder on the bottom row
for i, ax in enumerate([ax6,ax7,ax8,ax9,ax10]):
ax.imshow(output[randomly_selected_imgs[i]].reshape(28,28),
cmap='gray')
if i == 0:
ax.set_ylabel("OUTPUT",size=40)
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
plt.tight_layout()
plt.show()
我们将看到以下输出:

上图:提供给自编码器的原始图像作为输入;下图:自编码器输出的图像
等一下:输出图像看起来糟透了!它们看起来像模糊的白色涂鸦,完全不像我们的原始输入图像。显然,隐藏层节点数为一个节点的自编码器不足以对这个数据集进行编码。这个潜在表示对于我们的自编码器来说太小,无法充分捕捉我们数据的显著特征。
隐藏层大小对自编码器性能的影响
让我们尝试训练更多具有不同隐藏层大小的自编码器,看看它们的表现如何。
以下代码创建并训练五个不同的模型,隐藏层节点数分别为2、4、8、16和32:
hiddenLayerSize_2_model = create_basic_autoencoder(hidden_layer_size=2)
hiddenLayerSize_4_model = create_basic_autoencoder(hidden_layer_size=4)
hiddenLayerSize_8_model = create_basic_autoencoder(hidden_layer_size=8)
hiddenLayerSize_16_model = create_basic_autoencoder(hidden_layer_size=16)
hiddenLayerSize_32_model = create_basic_autoencoder(hidden_layer_size=32)
注意,每个连续模型的隐藏层节点数是前一个模型的两倍。
现在,让我们一起训练所有五个模型。我们在fit()函数中使用verbose=0参数来隐藏输出,如以下代码片段所示:
hiddenLayerSize_2_model.compile(optimizer='adam',
loss='mean_squared_error')
hiddenLayerSize_2_model.fit(X_train_reshaped, X_train_reshaped,
epochs=10, verbose=0)
hiddenLayerSize_4_model.compile(optimizer='adam',
loss='mean_squared_error')
hiddenLayerSize_4_model.fit(X_train_reshaped, X_train_reshaped,
epochs=10, verbose=0)
hiddenLayerSize_8_model.compile(optimizer='adam',
loss='mean_squared_error')
hiddenLayerSize_8_model.fit(X_train_reshaped, X_train_reshaped,
epochs=10, verbose=0)
hiddenLayerSize_16_model.compile(optimizer='adam',
loss='mean_squared_error')
hiddenLayerSize_16_model.fit(X_train_reshaped, X_train_reshaped,
epochs=10, verbose=0)
hiddenLayerSize_32_model.compile(optimizer='adam',
loss='mean_squared_error')
hiddenLayerSize_32_model.fit(X_train_reshaped, X_train_reshaped,
epochs=10, verbose=0)
一旦训练完成,我们将训练好的模型应用于测试集:
output_2_model = hiddenLayerSize_2_model.predict(X_test_reshaped)
output_4_model = hiddenLayerSize_4_model.predict(X_test_reshaped)
output_8_model = hiddenLayerSize_8_model.predict(X_test_reshaped)
output_16_model = hiddenLayerSize_16_model.predict(X_test_reshaped)
output_32_model = hiddenLayerSize_32_model.predict(X_test_reshaped)
现在,让我们绘制每个模型随机选择的五个输出,并看看它们与原始输入图像的对比:
fig, axes = plt.subplots(7, 5, figsize=(15,15))
randomly_selected_imgs = random.sample(range(output.shape[0]),5)
outputs = [X_test, output, output_2_model, output_4_model, output_8_model,
output_16_model, output_32_model]
# Iterate through each subplot and plot accordingly
for row_num, row in enumerate(axes):
for col_num, ax in enumerate(row):
ax.imshow(outputs[row_num][randomly_selected_imgs[col_num]]. \
reshape(28,28), cmap='gray')
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
plt.tight_layout()
plt.show()
我们得到以下输出:

难道这不美吗?我们可以清楚地看到,当我们将隐藏层节点数加倍时,输出图像逐渐变得更清晰,并且越来越接近原始输入图像。
在隐藏层节点数为 32 时,输出变得非常接近(尽管不完美)原始输入。有趣的是,我们将原始输入压缩了 24.5 倍(784÷32),仍然能够生成令人满意的输出。这是一个相当令人印象深刻的压缩比!
去噪自编码器
自编码器的另一个有趣应用是图像去噪。图像噪声定义为图像中亮度的随机变化。图像噪声可能源自数字相机的传感器。尽管如今的数字相机能够捕捉高质量的图像,但图像噪声仍然可能发生,尤其是在低光条件下。
多年来,去噪图像一直是研究人员的难题。早期的方法包括对图像应用某种图像滤波器(例如,均值平滑滤波器,其中像素值被其邻居的平均像素值替换)。然而,这些方法有时会失效,效果可能不尽如人意。
几年前,研究人员发现我们可以训练自动编码器进行图像去噪。这个想法很简单。在训练传统自动编码器时(如上一节所述),我们使用相同的输入和输出,而在这里我们使用一张带噪声的图像作为输入,并使用一张干净的参考图像来与自动编码器的输出进行比较。以下图展示了这一过程:

在训练过程中,自动编码器将学习到图像中的噪声不应成为输出的一部分,并将学会输出干净的图像。从本质上讲,我们是在训练自动编码器去除图像中的噪声!
让我们先给 MNIST 数据集引入噪声。我们将在原始图像的每个像素上加上一个介于-0.5和0.5之间的随机值。这将随机增加或减少像素的强度。以下代码使用numpy来实现这一操作:
import numpy as np
X_train_noisy = X_train_reshaped + np.random.normal(0, 0.5,
size=X_train_reshaped.shape)
X_test_noisy = X_test_reshaped + np.random.normal(0, 0.5,
size=X_test_reshaped.shape)
最后,我们将带噪声的图像裁剪到0和1之间,以便对图像进行归一化:
X_train_noisy = np.clip(X_train_noisy, a_min=0, a_max=1)
X_test_noisy = np.clip(X_test_noisy, a_min=0, a_max=1)
让我们像在上一节那样定义一个基础的自动编码器。这个基础的自动编码器有一个包含16个节点的单隐层。
以下代码使用我们在上一节中定义的函数来创建这个自动编码器:
basic_denoise_autoencoder = create_basic_autoencoder(hidden_layer_size=16)
接下来,我们训练我们的去噪自动编码器。记住,去噪自动编码器的输入是带噪声的图像,输出是干净的图像。以下代码训练我们的基础去噪自动编码器:
basic_denoise_autoencoder.compile(optimizer='adam',
loss='mean_squared_error')
basic_denoise_autoencoder.fit(X_train_noisy, X_train_reshaped, epochs=10)
一旦训练完成,我们将去噪自动编码器应用于测试图像:
output = basic_denoise_autoencoder.predict(X_test_noisy)
我们绘制输出并将其与原始图像和带噪声的图像进行比较:
fig, ((ax1, ax2, ax3, ax4, ax5), (ax6, ax7, ax8, ax9, ax10), (ax11,ax12,ax13,ax14,ax15)) = plt.subplots(3, 5, figsize=(20,13))
randomly_selected_imgs = random.sample(range(output.shape[0]),5)
# 1st row for original images
for i, ax in enumerate([ax1,ax2,ax3,ax4,ax5]):
ax.imshow(X_test_reshaped[randomly_selected_imgs[i]].reshape(28,28),
cmap='gray')
if i == 0:
ax.set_ylabel("Original \n Images", size=30)
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
# 2nd row for input with noise added
for i, ax in enumerate([ax6,ax7,ax8,ax9,ax10]):
ax.imshow(X_test_noisy[randomly_selected_imgs[i]].reshape(28,28),
cmap='gray')
if i == 0:
ax.set_ylabel("Input With \n Noise Added", size=30)
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
# 3rd row for output images from our autoencoder
for i, ax in enumerate([ax11,ax12,ax13,ax14,ax15]):
ax.imshow(output[randomly_selected_imgs[i]].reshape(28,28),
cmap='gray')
if i == 0:
ax.set_ylabel("Denoised \n Output", size=30)
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
plt.tight_layout()
plt.show()
我们得到如下输出:

它表现如何?嗯,肯定可以更好!这个基础的去噪自动编码器完全能够去除噪声,但在重建原始图像时并不做得很好。我们可以看到,这个基础的去噪自动编码器有时未能有效区分噪声和数字,尤其是在图像的中心部分。
深度卷积去噪自动编码器
我们能做得比基础的单隐层自动编码器更好吗?我们在上一章中看到,第四章,猫与狗 – 使用 CNN 进行图像分类,深度 CNN 在图像分类任务中表现出色。自然,我们也可以将相同的概念应用于自动编码器。我们不再仅使用一个隐层,而是使用多个隐层(即深度网络),并且不使用全连接的稠密层,而是使用卷积层。
以下图示说明了深度卷积自编码器的架构:

在 Keras 中构建深度卷积自编码器非常简单。我们再次使用 Keras 中的Sequential类来构建我们的模型。
首先,我们定义一个新的Sequential类:
conv_autoencoder = Sequential()
接下来,我们将添加作为编码器的前两层卷积层。在使用 Keras 中的Conv2D类时,有几个参数需要定义:
-
滤波器数量:通常,在编码器的每一层中,我们使用递减数量的滤波器。相反,在解码器的每一层中,我们使用递增数量的滤波器。我们可以为编码器的第一层卷积层使用 16 个滤波器,为第二层卷积层使用 8 个滤波器。相反,我们可以为解码器的第一层卷积层使用 8 个滤波器,为第二层卷积层使用 16 个滤波器。
-
滤波器大小:如上一章所示,第四章,猫狗对战——使用 CNN 进行图像分类,卷积层通常使用 3 x 3 的滤波器大小。
-
填充:对于自编码器,我们使用相同的填充方式。这确保了连续层的高度和宽度保持不变。这一点非常重要,因为我们需要确保最终输出的维度与输入相同。
以下代码片段将前述参数添加到模型中,包含前两层卷积层:
from keras.layers import Conv2D
conv_autoencoder.add(Conv2D(filters=16, kernel_size=(3,3),
activation='relu', padding='same',
input_shape=(28,28,1)))
conv_autoencoder.add(Conv2D(filters=8, kernel_size=(3,3),
activation='relu', padding='same'))
接下来,我们将解码器层添加到模型中。与编码器层类似,解码器层也是卷积层。唯一的不同是,在解码器层中,我们在每一层后使用递增数量的滤波器。
以下代码片段添加了作为解码器的两个卷积层:
conv_autoencoder.add(Conv2D(filters=8, kernel_size=(3,3),
activation='relu', padding='same'))
conv_autoencoder.add(Conv2D(filters=16, kernel_size=(3,3),
activation='relu', padding='same'))
最后,我们向模型中添加输出层。输出层应该是一个只有一个滤波器的卷积层,因为我们要输出一个 28 x 28 x 1 的图像。Sigmoid函数被用作输出层的激活函数。
以下代码添加了最终的输出层:
conv_autoencoder.add(Conv2D(filters=1, kernel_size=(3,3),
activation='sigmoid', padding='same'))
让我们看看模型的结构,以确保它与前面图示中展示的一致。我们可以通过调用summary()函数来实现:
conv_autoencoder.summary()
我们得到以下输出:

我们现在准备好训练我们的深度卷积自编码器。像往常一样,我们在compile函数中定义训练过程,并调用fit函数,如下所示:
conv_autoencoder.compile(optimizer='adam', loss='binary_crossentropy')
conv_autoencoder.fit(X_train_noisy.reshape(60000,28,28,1),
X_train_reshaped.reshape(60000,28,28,1),
epochs=10)
训练完成后,我们将得到以下输出:

让我们在测试集上使用训练好的模型:
output = conv_autoencoder.predict(X_test_noisy.reshape(10000,28,28,1))
看到这个深度卷积自编码器在测试集上的表现将会很有趣。记住,测试集代表了模型从未见过的图像。
我们绘制输出并将其与原始图像和噪声图像进行比较:
fig, ((ax1, ax2, ax3, ax4, ax5), (ax6, ax7, ax8, ax9, ax10), (ax11,ax12,ax13,ax14,ax15)) = plt.subplots(3, 5, figsize=(20,13))
randomly_selected_imgs = random.sample(range(output.shape[0]),5)
# 1st row for original images
for i, ax in enumerate([ax1,ax2,ax3,ax4,ax5]):
ax.imshow(X_test_reshaped[randomly_selected_imgs[i]].reshape(28,28),
cmap='gray')
if i == 0:
ax.set_ylabel("Original \n Images", size=30)
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
# 2nd row for input with noise added
for i, ax in enumerate([ax6,ax7,ax8,ax9,ax10]):
ax.imshow(X_test_noisy[randomly_selected_imgs[i]].reshape(28,28),
cmap='gray')
if i == 0:
ax.set_ylabel("Input With \n Noise Added", size=30)
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
# 3rd row for output images from our autoencoder
for i, ax in enumerate([ax11,ax12,ax13,ax14,ax15]):
ax.imshow(output[randomly_selected_imgs[i]].reshape(28,28),
cmap='gray')
if i == 0:
ax.set_ylabel("Denoised \n Output", size=30)
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
plt.tight_layout()
plt.show()
我们得到以下输出:

这难道不令人惊讶吗?我们的深度卷积自编码器的去噪输出效果如此出色,以至于我们几乎无法分辨原始图像和去噪后的输出。
尽管结果令人印象深刻,但需要记住的是,我们使用的卷积模型相对简单。深度神经网络的优势在于我们总是可以增加模型的复杂性(即更多的层和每层更多的滤波器),并将其应用于更复杂的数据集。这个扩展能力是深度神经网络的主要优势之一。
使用自编码器去噪文档
到目前为止,我们已经在 MNIST 数据集上应用了去噪自编码器,该数据集相对简单。现在让我们看看一个更复杂的数据集,它更好地代表了现实生活中去噪文档所面临的挑战。
我们将使用的数据集是由加利福尼亚大学欧文分校(UCI)免费提供的。有关数据集的更多信息,请访问 UCI 的网站:archive.ics.uci.edu/ml/datasets/NoisyOffice。
数据集可以在本书的配套 GitHub 仓库中找到。有关如何从 GitHub 仓库下载本章的代码和数据集的更多信息,请参阅本章前面的技术要求部分。
该数据集包含 216 张不同的噪声图像。这些噪声图像是扫描的办公室文档,受到咖啡渍、皱痕和其他典型的办公室文档缺陷的污染。对于每张噪声图像,提供了一张对应的干净图像,表示理想的无噪声状态下的办公室文档。
让我们看一下数据集,深入了解我们正在处理的内容。数据集位于以下文件夹:
noisy_imgs_path = 'Noisy_Documents/noisy/'
clean_imgs_path = 'Noisy_Documents/clean/'
Noisy_Documents文件夹包含两个子文件夹(noisy和clean),分别包含噪声图像和干净图像。
要将.png图像加载到 Python 中,我们可以使用 Keras 提供的load_img函数。为了将加载的图像转换为numpy数组,我们使用 Keras 中的img_to_array函数。
以下代码将位于/Noisy_Documents/noisy/文件夹中的噪声.png图像导入到numpy数组中:
import os
import numpy as np
from keras.preprocessing.image import load_img, img_to_array
X_train_noisy = []
for file in sorted(os.listdir(noisy_imgs_path)):
img = load_img(noisy_imgs_path+file, color_mode='grayscale',
target_size=(420,540))
img = img_to_array(img).astype('float32')/255
X_train_noisy.append(img)
# convert to numpy array
X_train_noisy = np.array(X_train_noisy)
为了验证我们的图像是否正确加载到numpy数组中,让我们打印数组的维度:
print(X_train_noisy.shape)
我们得到以下输出:

我们可以看到,数组中有 216 张图像,每张图像的维度为 420 x 540 x 1(宽度 x 高度 x 每张图像的通道数)。
对干净的图像执行相同操作。以下代码将位于/Noisy_Documents/clean/文件夹中的干净.png图像导入到numpy数组中:
X_train_clean = []
for file in sorted(os.listdir(clean_imgs_path)):
img = load_img(clean_imgs_path+file, color_mode='grayscale',
target_size=(420,540))
img = img_to_array(img).astype('float32')/255
X_train_clean.append(img)
# convert to numpy array
X_train_clean = np.array(X_train_clean)
让我们展示加载的图像,以便更好地了解我们正在处理的图像类型。以下代码随机选择3张图像并绘制它们,如下所示:
import random
fig, ((ax1,ax2), (ax3,ax4),
(ax5,ax6)) = plt.subplots(3, 2, figsize=(10,12))
randomly_selected_imgs = random.sample(range(X_train_noisy.shape[0]),3)
# plot noisy images on the left
for i, ax in enumerate([ax1,ax3,ax5]):
ax.imshow(X_train_noisy[i].reshape(420,540), cmap='gray')
if i == 0:
ax.set_title("Noisy Images", size=30)
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
# plot clean images on the right
for i, ax in enumerate([ax2,ax4,ax6]):
ax.imshow(X_train_clean[i].reshape(420,540), cmap='gray')
if i == 0:
ax.set_title("Clean Images", size=30)
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
plt.tight_layout()
plt.show()
我们得到如下截图中的输出:

我们可以看到这个数据集中噪声的类型与 MNIST 数据集中看到的显著不同。这个数据集中的噪声是随机的伪影,遍布整个图像。我们的自编码器模型需要能够清楚地理解信号与噪声的区别,才能成功去噪这个数据集。
在我们开始训练模型之前,让我们将数据集分成训练集和测试集,如下代码所示:
# use the first 20 noisy images as testing images
X_test_noisy = X_train_noisy[0:20,]
X_train_noisy = X_train_noisy[21:,]
# use the first 20 clean images as testing images
X_test_clean = X_train_clean[0:20,]
X_train_clean = X_train_clean[21:,]
基本卷积自编码器
我们现在已经准备好解决这个问题了。让我们从一个基本模型开始,看看能走多远。
和往常一样,我们定义一个新的Sequential类:
basic_conv_autoencoder = Sequential()
接下来,我们添加一个卷积层作为编码器层:
basic_conv_autoencoder.add(Conv2D(filters=8, kernel_size=(3,3),
activation='relu', padding='same',
input_shape=(420,540,1)))
我们添加一个卷积层作为解码器层:
basic_conv_autoencoder.add(Conv2D(filters=8, kernel_size=(3,3),
activation='relu', padding='same'))
最后,我们添加一个输出层:
basic_conv_autoencoder.add(Conv2D(filters=1, kernel_size=(3,3),
activation='sigmoid', padding='same'))
让我们查看模型的结构:
basic_conv_autoencoder.summary()
我们得到如下截图中的输出:

这是训练我们基本卷积自编码器的代码:
basic_conv_autoencoder.compile(optimizer='adam',
loss='binary_crossentropy')
basic_conv_autoencoder.fit(X_train_noisy, X_train_clean, epochs=10)
一旦训练完成,我们将模型应用到测试集上:
output = basic_conv_autoencoder.predict(X_test_noisy)
让我们绘制输出,看看得到的结果。以下代码在左列绘制原始噪声图像,在中列绘制原始干净图像,并在右列绘制从我们模型输出的去噪图像:
fig, ((ax1,ax2,ax3),(ax4,ax5,ax6)) = plt.subplots(2,3, figsize=(20,10))
randomly_selected_imgs = random.sample(range(X_test_noisy.shape[0]),2)
for i, ax in enumerate([ax1, ax4]):
idx = randomly_selected_imgs[i]
ax.imshow(X_test_noisy[idx].reshape(420,540), cmap='gray')
if i == 0:
ax.set_title("Noisy Images", size=30)
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
for i, ax in enumerate([ax2, ax5]):
idx = randomly_selected_imgs[i]
ax.imshow(X_test_clean[idx].reshape(420,540), cmap='gray')
if i == 0:
ax.set_title("Clean Images", size=30)
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
for i, ax in enumerate([ax3, ax6]):
idx = randomly_selected_imgs[i]
ax.imshow(output[idx].reshape(420,540), cmap='gray')
if i == 0:
ax.set_title("Output Denoised Images", size=30)
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
plt.tight_layout()
plt.show()
我们得到如下截图中的输出:

嗯,我们的模型确实能做得更好。去噪后的图像通常有灰色背景,而不是Clean Images中的白色背景。模型在去除Noisy Images中的咖啡渍方面也做得不太好。此外,去噪后的图像中的文字较为模糊,显示出模型在此任务上的困难。
深度卷积自编码器
让我们尝试使用更深的模型和每个卷积层中更多的滤波器来去噪图像。
我们首先定义一个新的Sequential类:
conv_autoencoder = Sequential()
接下来,我们添加三个卷积层作为编码器,使用32、16和8个滤波器:
conv_autoencoder.add(Conv2D(filters=32, kernel_size=(3,3),
input_shape=(420,540,1),
activation='relu', padding='same'))
conv_autoencoder.add(Conv2D(filters=16, kernel_size=(3,3),
activation='relu', padding='same'))
conv_autoencoder.add(Conv2D(filters=8, kernel_size=(3,3),
activation='relu', padding='same'))
同样地,对于解码器,我们添加三个卷积层,使用8、16和32个滤波器:
conv_autoencoder.add(Conv2D(filters=8, kernel_size=(3,3),
activation='relu', padding='same'))
conv_autoencoder.add(Conv2D(filters=16, kernel_size=(3,3),
activation='relu', padding='same'))
conv_autoencoder.add(Conv2D(filters=32, kernel_size=(3,3),
activation='relu', padding='same'))
最后,我们添加一个输出层:
conv_autoencoder.add(Conv2D(filters=1, kernel_size=(3,3),
activation='sigmoid', padding='same'))
让我们查看一下模型的结构:
conv_autoencoder.summary()
我们得到以下输出:

从前面的输出中,我们可以看到模型中有 12,785 个参数,大约是我们在上一节使用的基本模型的 17 倍。
让我们训练模型并将其应用于测试图像:
警告
以下代码可能需要一些时间运行,如果你没有使用带 GPU 的 Keras。若模型训练时间过长,你可以减少每个卷积层中的滤波器数量。
conv_autoencoder.compile(optimizer='adam', loss='binary_crossentropy')
conv_autoencoder.fit(X_train_noisy, X_train_clean, epochs=10)
output = conv_autoencoder.predict(X_test_noisy)
最后,我们绘制输出结果,以查看得到的结果类型。以下代码将原始噪声图像显示在左列,原始干净图像显示在中列,模型输出的去噪图像显示在右列:
fig, ((ax1,ax2,ax3),(ax4,ax5,ax6)) = plt.subplots(2,3, figsize=(20,10))
randomly_selected_imgs = random.sample(range(X_test_noisy.shape[0]),2)
for i, ax in enumerate([ax1, ax4]):
idx = randomly_selected_imgs[i]
ax.imshow(X_test_noisy[idx].reshape(420,540), cmap='gray')
if i == 0:
ax.set_title("Noisy Images", size=30)
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
for i, ax in enumerate([ax2, ax5]):
idx = randomly_selected_imgs[i]
ax.imshow(X_test_clean[idx].reshape(420,540), cmap='gray')
if i == 0:
ax.set_title("Clean Images", size=30)
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
for i, ax in enumerate([ax3, ax6]):
idx = randomly_selected_imgs[i]
ax.imshow(output[idx].reshape(420,540), cmap='gray')
if i == 0:
ax.set_title("Output Denoised Images", size=30)
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
plt.tight_layout()
plt.show()
我们得到以下输出:

结果看起来很棒!实际上,去噪后的图像看起来非常好,我们几乎无法区分它们与真实的干净图像。我们可以看到,咖啡渍几乎完全被去除了,揉皱纸张的噪声在去噪图像中消失了。此外,去噪图像中的文字清晰锐利,我们可以轻松地阅读去噪图像中的文字。
这个数据集真正展示了自编码器的强大功能。通过增加更多复杂性,例如更深的卷积层和更多的滤波器,模型能够区分信号与噪声,从而成功去除严重损坏的图像噪声。
总结
在本章中,我们研究了自编码器,这是一类学习输入图像潜在表示的神经网络。我们看到所有自编码器都有一个编码器和解码器组件。编码器的作用是将输入编码成一个学习到的压缩表示,而解码器的作用是使用这个压缩表示重构原始输入。
我们首先研究了用于图像压缩的自编码器。通过训练一个输入和输出相同的自编码器,自编码器能够学习输入的最显著特征。使用 MNIST 图像,我们构建了一个压缩率为 24.5 倍的自编码器。利用这个学习到的 24.5 倍压缩表示,自编码器能够成功地重构原始输入。
接下来,我们研究了去噪自编码器。通过训练一个以噪声图像为输入、干净图像为输出的自编码器,自编码器能够从噪声中提取信号,并成功地去除噪声图像的噪声。我们训练了一个深度卷积自编码器,该自编码器能够成功去除咖啡渍和其他类型的图像损坏。结果令人印象深刻,去噪自编码器几乎去除了所有噪声,输出几乎与真实的干净图像完全相同。
在下一章,第六章,使用 LSTM 进行电影评论情感分析,我们将使用长短期记忆(LSTM)神经网络来预测电影评论的情感。
问题
- 自编码器与传统的前馈神经网络有什么不同?
自编码器是学习输入压缩表示的神经网络,这种表示被称为潜在表示。它们不同于传统的前馈神经网络,因为自编码器的结构包含一个编码器和一个解码器组件,而这些组件在 CNN 中是不存在的。
- 当自编码器的潜在表示过小时会发生什么?
潜在表示的大小应该足够小,以便表示输入的压缩表示,同时又要足够大,使解码器能够重建原始图像而不会丢失太多信息。
- 训练去噪自编码器时的输入和输出是什么?
去噪自编码器的输入应为一张带噪声的图像,输出应为一张干净的参考图像。在训练过程中,自编码器通过loss函数学习输出不应包含任何噪声,并且自编码器的潜在表示应该只包含信号(即非噪声元素)。
- 我们可以通过哪些方法提高去噪自编码器的复杂度?
对于去噪自编码器,卷积层总是比全连接层表现更好,就像卷积神经网络(CNN)在图像分类任务中比传统的前馈神经网络表现更佳一样。我们还可以通过构建一个更深的网络(增加更多的层数),并在每个卷积层中使用更多的滤波器,来提高模型的复杂度。
第六章:使用 LSTM 进行电影评论情感分析
在前几章中,我们探讨了神经网络架构,例如基本的 MLP 和前馈神经网络,适用于分类和回归任务。随后我们看了 CNN,并了解了它们在图像识别任务中的应用。本章将重点讨论递归神经网络(RNNs)(特别是长短期记忆网络(LSTM)),以及它们如何用于顺序问题,如自然语言处理(NLP)。我们将开发并训练一个 LSTM 网络,用于预测 IMDb 上电影评论的情感。
本章将覆盖以下主题:
-
机器学习中的顺序问题
-
自然语言处理与情感分析
-
RNN 和 LSTM 网络简介
-
IMDb 电影评论数据集分析
-
词嵌入
-
在 Keras 中构建和训练 LSTM 网络的分步指南
-
我们结果的分析
技术要求
本章所需的 Python 库如下:
-
matplotlib 3.0.2
-
Keras 2.2.4
-
seaborn 0.9.0
-
scikit-learn 0.20.2
本章的代码可以在本书的 GitHub 仓库中找到。
要将代码下载到您的计算机上,您可以运行以下 git clone 命令:
$ git clone https://github.com/PacktPublishing/Neural-Network-Projects-with-Python.git
完成过程后,将会生成一个名为 Neural-Network-Projects-with-Python 的文件夹。运行以下命令进入该文件夹:
$ cd Neural-Network-Projects-with-Python
要在虚拟环境中安装所需的 Python 库,请运行以下命令:
$ conda env create -f environment.yml
请注意,在运行此命令之前,您应该先在计算机上安装 Anaconda。要进入虚拟环境,请运行以下命令:
$ conda activate neural-network-projects-python
通过运行以下命令进入 Chapter06 文件夹:
$ cd Chapter06
以下文件位于文件夹中:
lstm.py:这是本章的主要代码
要运行代码,只需执行 lstm.py 文件:
$ python lstm.py
机器学习中的顺序问题
顺序问题是机器学习中的一类问题,其中呈现给模型的特征顺序对于做出预测至关重要。顺序问题常见于以下场景:
-
自然语言处理,包括情感分析、语言翻译和文本预测
-
时间序列预测
例如,让我们考虑文本预测问题,如下图所示,它属于自然语言处理范畴:

人类天生具有这种能力,我们很容易知道空白处的词可能是Japanese。原因在于,当我们阅读句子时,我们将词汇处理为一个序列。这个词序列包含了进行预测所需的信息。相比之下,如果我们忽略序列信息,仅仅把单词当作独立的个体来看,就得到了一个词袋,如下面的示意图所示:

我们可以看到,现在我们预测空白处单词的能力受到严重影响。如果不知道单词的顺序,就无法预测空白处的单词。
除了文本预测,情感分析和语言翻译也是顺序问题。实际上,许多 NLP 问题都是顺序问题,因为我们所说的语言本身就是顺序性的,顺序传达了上下文和其他细微的差别。
顺序问题在时间序列问题中也自然发生。时间序列问题在股市中很常见。我们常常希望知道某只股票在某天是否会上涨或下跌。这个问题被准确地定义为时间序列问题,因为了解股票在前几个小时或几分钟的变化通常对预测股票是涨是跌至关重要。今天,机器学习方法在这个领域得到了广泛应用,算法交易策略推动着股票的买卖。
本章将重点讨论 NLP 问题。特别地,我们将为情感分析创建一个神经网络。
NLP 与情感分析
自然语言处理(NLP)是人工智能(AI)的一个子领域,专注于计算机与人类语言的交互。早在 1950 年代,科学家们就对设计能够理解人类语言的智能机器产生了兴趣。早期的语言翻译工作集中在基于规则的方法上,其中一组语言学专家手工编写了一套规则,并将其编码到机器中。然而,这种基于规则的方法产生的结果并不理想,而且通常无法将这些规则从一种语言转换到另一种语言,这意味着规模化变得困难。在许多年代里,NLP 领域进展缓慢,人类语言一直是 AI 无法达到的目标——直到深度学习的复兴。
随着深度学习和神经网络在图像分类领域的普及,科学家们开始思考神经网络的能力是否可以应用于自然语言处理(NLP)。在 2000 年代末,科技巨头如苹果、亚马逊和谷歌将 LSTM 网络应用于 NLP 问题,结果令人震惊。AI 助手(如 Siri 和 Alexa)能够理解不同口音说出的多种语言,正是得益于深度学习和 LSTM 网络。近年来,我们也看到了文本翻译软件(如 Google Translate)能力的巨大提升,它能够提供与人类语言专家相媲美的翻译结果。
情感分析也是 NLP 的一个领域,受益于深度学习的复兴。情感分析被定义为对文本的积极性预测。大多数情感分析问题都是分类问题(积极/中立/消极),而不是回归问题。
情感分析有许多实际应用。例如,现代客户服务中心通过情感分析预测客户在 Yelp 或 Facebook 等平台上提供的评论中的满意度。这使得企业能够在客户不满时立即介入,及时解决问题,避免客户流失。
情感分析也已应用于股票交易领域。2010 年,科学家们通过分析 Twitter 上的情感(积极与消极的推文),证明我们可以预测股市是否会上涨。同样,高频交易公司利用情感分析来分析与特定公司相关的新闻情感,并根据新闻的积极性自动执行交易。
为什么情感分析很困难
情感分析的早期工作面临许多障碍,因为人类语言中存在微妙的差异。同一个词往往会根据上下文传达不同的意义。举个例子,看看以下两句话:

我们知道第一句话的情感是消极的,因为它很可能意味着大楼真的着火了。另一方面,我们知道第二句话的情感是积极的,因为不太可能那个人真的着火了。相反,它可能意味着那个人正在经历一个好时光,这是积极的。基于规则的情感分析方法由于这些微妙的差异而遭遇困境,而且以规则化的方式编码这些知识非常复杂。
情感分析困难的另一个原因是讽刺。讽刺在许多文化中被广泛使用,尤其是在在线环境中。讽刺对计算机来说是难以理解的。事实上,即便是人类有时也难以察觉讽刺。举个例子,看一下以下这句话:

你可能能在前一句话中检测到讽刺,并得出结论情感是消极的。然而,对于程序来说,要理解这一点并不容易。
在下一节中,我们将探讨 RNN 和 LSTM 网络,以及它们如何被用于解决情感分析问题。
RNN
直到现在,我们在项目中使用了神经网络,如 MLP、前馈神经网络和 CNN。这些神经网络面临的限制是它们只能接受一个固定的输入向量,如图像,并输出另一个向量。这些神经网络的高层架构可以通过以下图示总结:

这种限制性的架构使得 CNN 难以处理顺序数据。为了处理顺序数据,神经网络需要在每个时间步获取数据的特定部分,并按其出现的顺序进行处理。这为 RNN 提供了灵感。RNN 的高层架构如下图所示:

从前面的图中我们可以看到,RNN 是一个多层神经网络。我们可以将原始输入拆分成时间步。例如,如果原始输入是一句话,我们可以将这句话拆分成单独的词(在这种情况下,每个词代表一个时间步)。然后,每个词将作为 输入 提供给 RNN 中相应的层。更重要的是,RNN 中的每一层将其输出传递给下一层。从层到层传递的中间输出被称为隐藏状态。本质上,隐藏状态使得 RNN 能够保持对顺序数据中间状态的记忆。
RNN 中包含了什么?
现在,让我们仔细看看 RNN 每一层内部发生了什么。下图展示了 RNN 每一层内部的数学函数:

RNN 的数学函数非常简单。RNN 中每一层 t 都有两个输入:
-
来自时间步 t 的输入
-
从前一层传递过来的隐藏状态 t-1
RNN 中的每一层简单地将两个输入相加,并对总和应用 tanh 函数。然后它输出结果,作为隐藏状态传递给下一层。就是这么简单!更正式地说,层 t 的输出隐藏状态为:

那么,tanh 函数到底是什么呢?tanh 函数是双曲正切函数,它将一个值压缩到 1 和 -1 之间。下图说明了这一点:

tanh 函数作为当前输入和前一隐藏状态的非线性转换是一个不错的选择,因为它可以确保权重不会过快地发散。它还有其他一些优良的数学性质,比如易于求导。
最后,为了从 RNN 的最后一层得到最终输出,我们只需对其应用 sigmoid 函数:

在前面的方程中,n 是 RNN 中最后一层的索引。从前面的章节中回顾,sigmoid 函数会产生一个介于 0 和 1 之间的输出,因此为每个类别提供概率作为预测。
我们可以看到,如果我们将这些层堆叠在一起,RNN 的最终输出依赖于不同时间步输入的非线性组合。
RNN 中的长短期依赖
RNN 的架构使其非常适合处理序列数据。我们来看一些具体的例子,了解 RNN 如何处理不同长度的序列数据。
首先,我们来看一段简短的文本作为我们的序列数据:

我们可以通过将这句话拆解成五个不同的输入,将每个单词作为每个时间步,来把它视作序列数据。下图说明了这一点:

现在,假设我们正在构建一个简单的 RNN 来预测是否下雪,基于这个序列数据。RNN 会像这样工作:

序列中的关键信息是第 4 个时间步(t[4],[红圈标注])的单词HOT。有了这条信息,RNN 能够轻松预测今天不会下雪。请注意,关键信息出现在最终输出之前不久。换句话说,我们可以说这个序列中存在短期依赖。
显然,RNN 在处理短期依赖时没有问题。但长期依赖怎么办呢?让我们现在来看一个更长的文本序列。我们以以下段落为例:

我们的目标是预测客户是否喜欢这部电影。显然,客户喜欢这部电影,但不喜欢影院,这也是段落中的主要投诉。让我们把段落分解成一系列输入,每个单词在每个时间步(段落中 32 个单词对应 32 个时间步)。RNN 会是这样处理的:

关键字liked the movie出现在第 3 到第 5 个时间步之间。请注意,关键时间步和输出时间步之间存在显著的间隔,因为其余文本对预测问题(客户是否喜欢这部电影)大多是无关的。换句话说,我们说这个序列中存在长期依赖。不幸的是,RNN 在处理长期依赖序列时效果不好。RNN 具有良好的短期记忆,但长期记忆较差。为了理解这一点,我们需要理解在训练神经网络时的消失梯度问题。
消失梯度问题
消失梯度问题是在使用基于梯度的训练方法(如反向传播)训练深度神经网络时出现的问题。回想一下,在之前的章节中,我们讨论了神经网络训练中的反向传播算法。特别地,loss 函数提供了我们预测准确性的反馈,并使我们能够调整每一层的权重,以减少损失。
到目前为止,我们假设反向传播能够完美地工作。不幸的是,事实并非如此。当损失向后传播时,损失在每一层逐步减少:

结果是,当损失向前传播到前几层时,损失已经减小到几乎没有变化,因此权重几乎没有发生变化。由于如此小的损失被反向传播,根本无法调整和训练前几层的权重。这个现象在机器学习中被称为梯度消失问题。
有趣的是,梯度消失问题并不会影响计算机视觉问题中的 CNN。然而,当涉及到序列数据和 RNN 时,梯度消失会产生重大影响。梯度消失问题意味着 RNN 无法从早期层(早期时间步)中学习,这导致它在长期记忆上的表现不佳。
为了解决这个问题,Hochreiter 等人提出了一种巧妙的 RNN 变体,称为长短期记忆(LSTM)网络。
LSTM 网络
LSTM 是 RNN 的一种变体,它解决了传统 RNN 面临的长期依赖问题。在深入探讨 LSTM 的技术细节之前,了解其背后的直觉是很有帮助的。
LSTM – 直觉
正如我们在前一节中所解释的,LSTM 被设计用来克服长期依赖问题。假设我们有以下这篇电影评论:

我们的任务是预测评论者是否喜欢这部电影。当我们阅读这篇评论时,我们立刻明白这篇评论是积极的。特别是以下这些(突出显示的)单词最为重要:

如果我们仔细想想,只有突出显示的单词才是重要的,其余的可以忽略不计。这是一个重要策略。通过有选择地记住某些单词,我们可以确保神经网络不会被太多不必要的单词所困扰,这些不必要的单词并没有提供太多预测能力。这是 LSTM 相较于传统 RNN 的一个重要区别。传统的 RNN 倾向于记住所有内容(即使是无用的输入),这会导致它无法从长序列中学习。相比之下,LSTM 有选择地记住重要的输入(如前面突出显示的文本),这使得它能够处理短期和长期的依赖。
LSTM 能够同时学习短期和长期依赖,因此得名长短期记忆(LSTM)。
LSTM 网络内部是什么?
LSTM 具有与我们之前看到的 RNN 相同的重复结构。然而,LSTM 在其内部结构上有所不同。
下图展示了 LSTM 重复单元的高层次概览:

前面的图表现在看起来可能会有点复杂,但别担心,我们会一步一步地讲解。正如我们在上一节提到的,LSTM 具有选择性记住重要输入并忘记其余部分的能力。LSTM 的内部结构使它能够做到这一点。
LSTM 与传统的 RNN 不同,除了隐藏状态外,还拥有一个单元状态。你可以将单元状态看作是 LSTM 的当前记忆。它从一个重复结构流向下一个,传递必须保留的重要信息。相比之下,隐藏状态是整个 LSTM 的总记忆。它包含了我们到目前为止看到的所有信息,包括重要和不重要的内容。
LSTM 如何在隐藏状态和单元状态之间释放信息?它通过三个重要的门来实现:
-
遗忘门
-
输入门
-
输出门
就像物理门一样,这三个门限制了信息从隐藏状态流向单元状态。
遗忘门
遗忘门(f) 在以下图中突出显示:

遗忘门(f) 形成 LSTM 重复单元的第一部分,其作用是决定我们应该忘记或记住从前一个单元状态中获取多少数据。它通过首先拼接前一个隐藏状态(h[t-1])和当前输入(x[t]),然后将拼接后的向量传递通过一个 sigmoid 函数来实现这一点。回想一下,sigmoid 函数输出一个值介于 0 和 1 之间的向量。0 的值意味着停止信息的传递(忘记),而 1 的值意味着通过信息(记住)。
遗忘门的输出,f,如下所示:

输入门
下一个门是输入门(i)。输入门(i)控制将多少信息传递给当前的单元状态。LSTM 的输入门在以下图中突出显示:

与遗忘门类似,输入门(i)的输入是前一个隐藏状态(h[t-1])和当前输入(x[t])的拼接。然后它将拼接后的向量通过一个 sigmoid 函数和一个 tanh 函数,再将它们相乘。
输入门的输出,i,如下所示:

此时,我们已具备计算当前单元状态(C[t])所需的信息,输出如下图所示:

当前的单元状态 C[t] 如下所示:

输出门
最后,输出门控制要在隐藏状态中保留多少信息。输出门在以下图中突出显示:

首先,我们将前一个隐藏状态(h[t−1])和当前输入(x[t])连接起来,并通过 sigmoid 函数传递。然后,我们将当前的单元状态(C[t])传递通过 tanh 函数。最后,我们将两者相乘,结果传递到下一个重复单元作为隐藏状态(h[t])。这个过程可以通过以下方程式总结:

理解这一点
许多 LSTM 的初学者常常会被涉及的数学公式吓到。虽然理解 LSTM 背后的数学函数是有用的,但试图将 LSTM 的直觉与数学公式联系起来往往很困难(并且不太有用)。相反,从高层次理解 LSTM,然后应用黑箱算法会更加有用,正如我们将在后续章节中看到的那样。
IMDb 电影评论数据集
此时,让我们在开始构建模型之前快速查看 IMDb 电影评论数据集。在构建模型之前,了解我们的数据总是一个好习惯。
IMDb 电影评论数据集是一个收录在著名电影评论网站www.imdb.com/上的电影评论语料库。每个电影评论都有一个标签,指示该评论是正面(1)还是负面(0)。
IMDb 电影评论数据集在 Keras 中提供,我们可以通过简单地调用以下代码来导入它:
from keras.datasets import imdb
training_set, testing_set = imdb.load_data(index_from = 3)
X_train, y_train = training_set
X_test, y_test = testing_set
我们可以如下打印出第一条电影评论:
print(X_train[0])
我们将看到以下输出:
[1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65, 458, 4468, 66, 3941, 4, 173, 36, 256, 5, 25, 100, 43, 838, 112, 50, 670, 22665, 9, 35, 480, 284, 5, 150, 4, 172, 112, 167, 21631, 336, 385, 39, 4, 172, 4536, 1111, 17, 546, 38, 13, 447, 4, 192, 50, 16, 6, 147, 2025, 19, 14, 22, 4, 1920, 4613, 469, 4, 22, 71, 87, 12, 16, 43, 530, 38, 76, 15, 13, 1247, 4, 22, 17, 515, 17, 12, 16, 626, 18, 19193, 5, 62, 386, 12, 8, 316, 8, 106, 5, 4, 2223, 5244, 16, 480, 66, 3785, 33, 4, 130, 12, 16, 38, 619, 5, 25, 124, 51, 36, 135, 48, 25, 1415, 33, 6, 22, 12, 215, 28, 77, 52, 5, 14, 407, 16, 82, 10311, 8, 4, 107, 117, 5952, 15, 256, 4, 31050, 7, 3766, 5, 723, 36, 71, 43, 530, 476, 26, 400, 317, 46, 7, 4, 12118, 1029, 13, 104, 88, 4, 381, 15, 297, 98, 32, 2071, 56, 26, 141, 6, 194, 7486, 18, 4, 226, 22, 21, 134, 476, 26, 480, 5, 144, 30, 5535, 18, 51, 36, 28, 224, 92, 25, 104, 4, 226, 65, 16, 38, 1334, 88, 12, 16, 283, 5, 16, 4472, 113, 103, 32, 15, 16, 5345, 19, 178, 32]
我们看到了一串数字,因为 Keras 在预处理过程中已经将单词编码为数字。我们可以使用 Keras 作为数据集一部分提供的内置单词到索引的字典将评论转换回单词:
word_to_id = imdb.get_word_index()
word_to_id = {key:(value+3) for key,value in word_to_id.items()}
word_to_id["<PAD>"] = 0
word_to_id["<START>"] = 1
id_to_word = {value:key for key,value in word_to_id.items()}
现在,我们可以将原始评论显示为文字:
print(' '.join(id_to_word[id] for id in X_train[159] ))
我们将看到以下输出:
<START> a rating of 1 does not begin to express how dull depressing and relentlessly bad this movie is
很明显,这条评论的情感是负面的!让我们通过打印y值来确认:
print(y_train[159])
我们将看到以下输出:
0
y值为0表示负面评论,y值为1表示正面评论。我们来看一个正面评论的例子:
print(' '.join(id_to_word[id] for id in X_train[6]))
我们将得到以下输出:
<START> lavish production values and solid performances in this straightforward adaption of jane austen's satirical classic about the marriage game within and between the classes in provincial 18th century england northam and paltrow are a salutory mixture as friends who must pass through jealousies and lies to discover that they love each other good humor is a sustaining virtue which goes a long way towards explaining the accessability of the aged source material which has been toned down a bit in its harsh scepticism i liked the look of the film and how shots were set up and i thought it didn't rely too much on successions of head shots like most other films of the 80s and 90s do very good results
要检查评论的情感,请尝试如下操作:
print(y_train[6])
我们得到如下输出:
1
将词语表示为向量
到目前为止,我们已经了解了 RNN 和 LSTM 网络表示的内容。还有一个我们需要解决的重要问题:我们如何将单词表示为神经网络的输入数据?在 CNN 的例子中,我们看到图像本质上是三维向量/矩阵,其维度由图像的宽度、高度和通道数(彩色图像有三个通道)表示。向量中的值代表每个像素的强度。
独热编码
我们如何为单词创建类似的向量/矩阵,以便它们可以作为神经网络的输入呢?在前面的章节中,我们看到如何通过为每个变量创建一个新特征,将诸如星期几这样的类别变量进行独热编码,转换为数值变量。可能会有人认为我们也可以通过这种方式对句子进行独热编码,但这种方法有显著的缺点。
让我们考虑以下短语:
-
Happy, excited
-
Happy
-
Excited
以下图展示了这些短语的独热编码二维表示:

在这种向量表示中,短语“Happy”和“excited”的两个轴都为1,因为短语中既有“Happy”也有“Excited”。类似地,短语Happy的Happy轴为1,Excited轴为0,因为它只包含单词Happy。
完整的二维向量表示如下表所示:
| Happy | Excited |
|---|---|
| 1 | 1 |
| 1 | 0 |
| 0 | 1 |
这种独热编码表示有几个问题。首先,轴的数量取决于数据集中唯一单词的数量。我们可以想象,英语词典中有成千上万的唯一单词。如果我们为每个单词创建一个轴,那么我们的向量大小将迅速膨胀。其次,这种向量表示会非常稀疏(充满零)。这是因为大多数单词在每个句子/段落中只出现一次。在这样的稀疏向量上训练神经网络是非常困难的。
最后,也是最重要的,向量表示并没有考虑单词之间的相似性。在我们之前的例子中,Happy和Excited都是传达积极情感的词。然而,这种独热编码表示并没有考虑到这一相似性。因此,当词语以这种形式表示时,重要的信息就会丢失。
如我们所见,独热编码的向量存在显著的缺点。在接下来的章节中,我们将探讨词嵌入,它能够克服这些缺点。
词嵌入
词嵌入是一种学习得到的单词向量表示形式。词嵌入的主要优势在于它们比独热编码表示的维度要少,并且它们将相似的单词彼此靠近。
以下图展示了一个词嵌入的示例:

注意,学习到的词嵌入知道“Elated”、“Happy”和“Excited”是相似的词,因此它们应该彼此靠近。同样,“Sad”、“Disappointed”、“Angry”和“Furious”处于词汇的对立面,它们应该远离彼此。
我们不会详细讲解词嵌入的创建过程,但本质上,它们是通过监督学习算法进行训练的。Keras 还提供了一个方便的 API,用于训练我们自己的词嵌入。在这个项目中,我们将在 IMDb 电影评论数据集上训练我们的词嵌入。
模型架构
让我们看看 IMDb 电影评论情感分析器的模型架构,如下图所示:

现在这一部分应该对你很熟悉了!让我们简要回顾一下每个组件。
输入
我们神经网络的输入将是 IMDb 电影评论。评论将以英语句子的形式出现。正如我们所见,Keras 提供的数据集已经将英语单词编码成数字,因为神经网络需要数值输入。然而,我们仍然面临一个问题需要解决。正如我们所知道的,电影评论的长度是不同的。如果我们将评论表示为向量,不同的评论将会有不同的向量长度,这对于神经网络来说是不可接受的。我们暂时记住这一点,随着神经网络的构建,我们将看到如何解决这个问题。
词嵌入层
我们神经网络的第一层是词嵌入层。正如我们之前所看到的,词嵌入是单词的学习型向量表示形式。词嵌入层接收单词作为输入,然后输出这些单词的向量表示。向量表示应该将相似的单词彼此接近,将不相似的单词相隔较远。词嵌入层在训练过程中学习这种向量表示。
LSTM 层
LSTM 层将词嵌入层的单词向量表示作为输入,并学习如何将这些向量表示分类为正面或负面。正如我们之前看到的,LSTM 是 RNN 的一种变体,我们可以把它看作是多个神经网络堆叠在一起。
全连接层
下一层是全连接层(dense layer)。全连接层接收 LSTM 层的输出,并将其转化为全连接方式。然后,我们对全连接层应用 sigmoid 激活函数,使得最终输出在 0 和 1 之间。
输出
输出是一个介于 0 和 1 之间的概率,表示电影评论是正面的还是负面的。接近 1 的概率意味着电影评论是正面的,而接近 0 的概率意味着电影评论是负面的。
在 Keras 中构建模型
我们终于准备好在 Keras 中开始构建我们的模型了。作为提醒,我们将使用的模型架构已在上一节中展示。
导入数据
首先,让我们导入数据集。IMDb 电影评论数据集已经在 Keras 中提供,我们可以直接导入:
from keras.datasets import imdb
imdb类有一个load_data主函数,它接收以下重要参数:
num_words:这定义为要加载的唯一单词的最大数量。只会加载前n个最常见的唯一单词(根据数据集中的出现频率)。如果n较小,训练时间会更快,但准确性会受到影响。我们将num_words = 10000。
load_data函数返回两个元组作为输出。第一个元组包含训练集,第二个元组包含测试集。请注意,load_data函数会将数据均等且随机地分割为训练集和测试集。
以下代码导入了数据,并使用了前面提到的参数:
training_set, testing_set = imdb.load_data(num_words = 10000)
X_train, y_train = training_set
X_test, y_test = testing_set
让我们快速检查一下我们拥有的数据量:
print("Number of training samples = {}".format(X_train.shape[0]))
print("Number of testing samples = {}".format(X_test.shape[0]))
我们将看到以下输出:

我们可以看到我们有25000个训练和测试样本。
零填充
在我们将数据作为输入用于神经网络之前,需要解决一个问题。回想上一节提到的,电影评论的长度不同,因此输入向量的大小也不同。这是一个问题,因为神经网络只接受固定大小的向量。
为了解决这个问题,我们将定义一个maxlen参数。maxlen参数将是每个电影评论的最大长度。评论长度超过maxlen的将被截断,长度不足maxlen的将被零填充。
以下图展示了零填充过程:

使用零填充,我们确保输入具有固定的向量长度。
一如既往,Keras 提供了一个方便的函数来执行零填充。在 Keras 的preprocessing模块下,有一个sequence类,允许我们对序列数据进行预处理。让我们导入sequence类:
from keras.preprocessing import sequence
sequence类有一个pad_sequences函数,允许我们对序列数据进行零填充。我们将使用maxlen = 100来截断并填充我们的训练和测试数据。以下代码展示了我们如何做到这一点:
X_train_padded = sequence.pad_sequences(X_train, maxlen= 100)
X_test_padded = sequence.pad_sequences(X_test, maxlen= 100)
现在,让我们验证零填充后的向量长度:
print("X_train vector shape = {}".format(X_train_padded.shape))
print("X_test vector shape = {}".format(X_test_padded.shape))
我们将看到以下输出:

词嵌入和 LSTM 层
数据预处理完成后,我们可以开始构建模型。像往常一样,我们将使用 Keras 中的Sequential类来构建模型。回想一下,Sequential类允许我们将各层堆叠在一起,使得逐层构建复杂模型变得非常简单。
像往常一样,让我们定义一个新的Sequential类:
from keras.models import Sequential
model = Sequential()
我们现在可以将词嵌入层添加到我们的模型中。词嵌入层可以直接通过keras.layers构建,如下所示:
from keras.layers import Embedding
Embedding类需要以下重要参数:
-
input_dim: 词嵌入层的输入维度。它应该与我们在加载数据时使用的num_words参数相同。本质上,这是数据集中唯一词汇的最大数量。 -
output_dim:词嵌入层的输出维度。这应该是一个需要调优的超参数。目前,我们使用128作为输出维度。
我们可以将前面提到的参数的嵌入层添加到我们的顺序模型中,如下所示:
model.add(Embedding(input_dim = 10000, output_dim = 128))
同样,我们可以直接从keras.layers中添加一个LSTM层,如下所示:
from keras.layers import LSTM
LSTM类接受以下重要参数:
-
units:这指的是LSTM层中循环单元的数量。更多的单元数会使得模型更加复杂,但也会增加训练时间,并可能导致过拟合。目前,我们使用128作为单位的典型值。 -
activation:这指的是应用于单元状态和隐藏状态的激活函数类型。默认值是 tanh 函数。 -
recurrent_activation:这指的是应用于遗忘门、输入门和输出门的激活函数类型。默认值是sigmoid函数。
你可能会注意到,Keras 中可用的激活函数种类比较有限。我们无法为遗忘门、输入门和输出门选择各自独立的激活函数,而只能为所有三个门选择一个共同的激活函数。这不幸是我们需要适应的一个限制。然而,好消息是,这一理论上的偏差并不会显著影响我们的结果。我们在 Keras 中构建的 LSTM 完全能够从顺序数据中学习。
我们可以将前面提到的参数的LSTM层添加到我们的顺序模型中,如下所示:
model.add(LSTM(units=128))
最后,我们添加一个Dense层,并将sigmoid作为activation激活函数。回想一下,这一层的目的是确保我们的模型输出的值在0到1之间,表示电影评论是正面的概率。我们可以如下添加Dense层:
from keras.layers import Dense
model.add(Dense(units=1, activation='sigmoid'))
Dense层是我们神经网络中的最后一层。让我们通过调用summary()函数来验证模型的结构:
model.summary()
我们得到如下输出:

很棒!我们可以看到,我们的 Keras 模型结构与上一节开始时介绍的模型架构图一致。
编译和训练模型
随着模型构建完成,我们准备好编译并训练我们的模型了。到现在为止,你应该已经熟悉了 Keras 中的模型编译过程。和往常一样,在编译模型时我们需要决定一些参数,它们如下:
-
损失函数:当目标输出是二分类时,我们使用
binary_crossentropy损失函数;当目标输出是多分类时,我们使用categorical_crossentropy损失函数。由于本项目中电影评论的情感是二元的(即正面或负面),因此我们将使用binary_crossentropy损失函数。 -
优化器:优化器的选择是 LSTM 中一个有趣的问题。无需深入技术细节,某些优化器可能对某些数据集无效,这可能是由于梯度消失和梯度爆炸问题(与梯度消失问题相反)。通常,很难预先知道哪个优化器对于数据集表现更好。因此,最好的方法是使用不同的优化器训练不同的模型,并选择结果最好的优化器。让我们尝试使用
SGD、RMSprop和adam优化器。
我们可以按如下方式编译我们的模型:
# try the SGD optimizer first
Optimizer = 'SGD'
model.compile(loss='binary_crossentropy', optimizer = Optimizer)
现在,让我们训练我们的模型10个周期,使用测试集作为验证数据。我们可以按以下方式进行:
scores = model.fit(x=X_train_padded, y=y_train,
batch_size = 128, epochs=10,
validation_data=(X_test_padded, y_test))
返回的scores对象是一个 Python 字典,提供了每个周期的训练和验证准确率以及损失。
在分析我们的结果之前,先将所有代码放入一个函数中。这使我们能够轻松测试和比较不同优化器的性能。
我们定义了一个train_model()函数,它接受一个Optimizer作为参数:
def train_model(Optimizer, X_train, y_train, X_val, y_val):
model = Sequential()
model.add(Embedding(input_dim = 10000, output_dim = 128))
model.add(LSTM(units=128))
model.add(Dense(units=1, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer = Optimizer,
metrics=['accuracy'])
scores = model.fit(X_train, y_train, batch_size=128,
epochs=10,
validation_data=(X_val, y_val),
verbose=0)
return scores, model
使用这个函数,我们将使用三种不同的优化器训练三种不同的模型,分别是SGD、RMSprop和adam优化器:
SGD_score, SGD_model = train_model(Optimizer = 'sgd',
X_train=X_train_padded,
y_train=y_train,
X_val=X_test_padded,
y_val=y_test)
RMSprop_score, RMSprop_model = train_model(Optimizer = 'RMSprop',
X_train=X_train_padded,
y_train=y_train,
X_val=X_test_padded,
y_val=y_test)
Adam_score, Adam_model = train_model(Optimizer = 'adam',
X_train=X_train_padded,
y_train=y_train,
X_val=X_test_padded,
y_val=y_test)
分析结果
让我们绘制三个不同模型的每个周期验证准确率。首先,我们绘制使用sgd优化器训练的模型:
from matplotlib import pyplot as plt
plt.plot(range(1,11), SGD_score.history['acc'], label='Training Accuracy')
plt.plot(range(1,11), SGD_score.history['val_acc'],
label='Validation Accuracy')
plt.axis([1, 10, 0, 1])
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.title('Train and Validation Accuracy using SGD Optimizer')
plt.legend()
plt.show()
我们得到以下输出:

你注意到有什么问题吗?训练和验证准确率停留在 50%!实际上,这表明训练失败了,我们的神经网络在这个二分类任务上的表现不比随机投掷硬币好。显然,sgd优化器不适合这个数据集和这个 LSTM 网络。如果使用另一个优化器能做得更好吗?让我们尝试使用RMSprop优化器。
我们绘制了使用RMSprop优化器训练的模型的训练和验证准确率,如下所示:
plt.plot(range(1,11), RMSprop_score.history['acc'],
label='Training Accuracy')
plt.plot(range(1,11), RMSprop_score.history['val_acc'],
label='Validation Accuracy')
plt.axis([1, 10, 0, 1])
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.title('Train and Validation Accuracy using RMSprop Optimizer')
plt.legend()
plt.show()
我们得到以下输出:

这好多了!在 10 个周期内,我们的模型能够达到超过 95%的训练准确率和约 85%的验证准确率。这个结果还不错。显然,RMSprop优化器在这个任务上表现得比sgd优化器更好。
最后,让我们尝试adam优化器,看看它的表现如何。我们绘制了使用adam优化器训练的模型的训练和验证准确率,如下所示:
plt.plot(range(1,11), Adam_score.history['acc'], label='Training Accuracy')
plt.plot(range(1,11), Adam_score.history['val_acc'],
label='Validation Accuracy')
plt.axis([1, 10, 0, 1])
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.title('Train and Validation Accuracy using Adam Optimizer')
plt.legend()
plt.show()
我们得到以下输出:

adam优化器表现得相当不错。从前面的图表可以看出,训练准确率在10轮后几乎达到了 100%,而验证准确率大约为 80%。这种 20%的差距表明,使用adam优化器时可能出现了过拟合。
相比之下,RMSprop优化器的训练准确率和验证准确率之间的差距较小。因此,我们得出结论,RMSprop优化器对于该数据集和 LSTM 网络是最优的,从现在起我们将使用基于RMSprop优化器构建的模型。
混淆矩阵
在第二章中,多层感知机的糖尿病预测,我们看到混淆矩阵是一个有用的可视化工具,用来评估模型的表现。让我们也使用混淆矩阵来评估我们在这个项目中的模型表现。
总结一下,这些是混淆矩阵中各个术语的定义:
-
真负类:实际类别为负(负面情感),模型也预测为负
-
假阳性:实际类别为负(负面情感),但模型预测为正
-
假阴性:实际类别为正(正面情感),但模型预测为负
-
真正类:实际类别为正(正面情感),模型预测为正
我们希望假阳性和假阴性的数量尽可能低,真负类和真正类的数量尽可能高。
我们可以使用sklearn中的confusion_matrix类构建混淆矩阵,并利用seaborn进行可视化:
from sklearn.metrics import confusion_matrix
import seaborn as sns
plt.figure(figsize=(10,7))
sns.set(font_scale=2)
y_test_pred = RMSprop_model.predict_classes(X_test_padded)
c_matrix = confusion_matrix(y_test, y_test_pred)
ax = sns.heatmap(c_matrix, annot=True, xticklabels=['Negative Sentiment',
'Positive Sentiment'], yticklabels=['Negative Sentiment',
'Positive Sentiment'], cbar=False, cmap='Blues', fmt='g')
ax.set_xlabel("Prediction")
ax.set_ylabel("Actual")
我们得到如下输出:

从前面的混淆矩阵可以看出,大多数测试数据被正确分类,真负类和真正类的比例约为 85%。换句话说,我们的模型在预测电影评论情感时的准确率为 85%。这相当令人印象深刻!
让我们来看一些被错误分类的样本,看看模型哪里出错了。以下代码获取了错误分类样本的索引:
false_negatives = []
false_positives = []
for i in range(len(y_test_pred)):
if y_test_pred[i][0] != y_test[i]:
if y_test[i] == 0: # False Positive
false_positives.append(i)
else:
false_negatives.append(i)
首先让我们来看假阳性。为了提醒大家,假阳性指的是那些实际上为负面的电影评论,但我们的模型错误地将其分类为正面。
我们选择了一个有趣的假阳性;如下所示:
"The sweet is never as sweet without the sour". This quote was essentially the theme for the movie in my opinion ..... It is a movie that really makes you step back and look at your life and how you live it. You cannot really appreciate the better things in life (the sweet) like love until you have experienced the bad (the sour). ..... Only complaint is that the movie gets very twisted at points and is hard to really understand...... I recommend you watch it and see for yourself.
即使作为人类,预测这条电影评论的情感也是困难的!电影的第一句可能设定了评论者的基调。然而,它写得非常微妙,我们的模型很难捕捉到这句话的意图。此外,评论的中段称赞了电影,但最后得出结论,“电影在某些时刻变得非常复杂,真的很难理解”。
现在,让我们来看一些假阴性:
I hate reading reviews that say something like 'don't waste your time this film stinks on ice'. It does to that reviewer yet for me it may have some sort of naïve charm ..... This film is not as good in my opinion as any of the earlier series entries ... But the acting is good and so is the lighting and the dialog. It's just lacking in energy and you'll likely figure out exactly what's going on and how it's all going to come out in the end not more than a quarter of the way through ..... But still I'll recommend this one for at least a single viewing. I've watched it at least twice myself and got a reasonable amount of enjoyment out of it both times
这篇评论确实有些模棱两可,整体看起来比较中立,评论者呈现了电影的优缺点。另一个需要注意的点是,在评论的开头,评论者引用了另一位评论者的话(我讨厌看到像'别浪费时间,这部电影糟透了'这种评论')。我们的模型可能未能理解这句话并不是该评论者的观点。引用文本对大多数 NLP 模型来说确实是一个挑战。
让我们来看另一个假阴性:
I just don't understand why this movie is getting beat up in here jeez. It is mindless, it isn't polished ..... I just don't get it. The jokes work on more then one level. If you didn't get it, I know what level you're at.
这篇电影评论可以看作是对其他电影评论的抱怨,类似于我们之前展示的评论。电影中多次出现负面词汇,可能误导了我们的模型,导致模型未能理解该评论是在反对所有其他负面评论。从统计学角度看,这类评论相对较少,且我们的模型很难学会准确理解此类评论的真正情感。
综合总结
本章内容已经涉及了很多内容。让我们在这里整合所有的代码:
from keras.datasets import imdb
from keras.preprocessing import sequence
from keras.models import Sequential
from keras.layers import Embedding
from keras.layers import Dense, Embedding
from keras.layers import LSTM
from matplotlib import pyplot as plt
from sklearn.metrics import confusion_matrix
import seaborn as sns
# Import IMDB dataset
training_set, testing_set = imdb.load_data(num_words = 10000)
X_train, y_train = training_set
X_test, y_test = testing_set
print("Number of training samples = {}".format(X_train.shape[0]))
print("Number of testing samples = {}".format(X_test.shape[0]))
# Zero-Padding
X_train_padded = sequence.pad_sequences(X_train, maxlen= 100)
X_test_padded = sequence.pad_sequences(X_test, maxlen= 100)
print("X_train vector shape = {}".format(X_train_padded.shape))
print("X_test vector shape = {}".format(X_test_padded.shape))
# Model Building
def train_model(Optimizer, X_train, y_train, X_val, y_val):
model = Sequential()
model.add(Embedding(input_dim = 10000, output_dim = 128))
model.add(LSTM(units=128))
model.add(Dense(units=1, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer = Optimizer,
metrics=['accuracy'])
scores = model.fit(X_train, y_train, batch_size=128, epochs=10,
validation_data=(X_val, y_val))
return scores, model
# Train Model
RMSprop_score, RMSprop_model = train_model(Optimizer = 'RMSprop', X_train=X_train_padded, y_train=y_train, X_val=X_test_padded, y_val=y_test)
# Plot accuracy per epoch
plt.plot(range(1,11), RMSprop_score.history['acc'],
label='Training Accuracy')
plt.plot(range(1,11), RMSprop_score.history['val_acc'],
label='Validation Accuracy')
plt.axis([1, 10, 0, 1])
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.title('Train and Validation Accuracy using RMSprop Optimizer')
plt.legend()
plt.show()
# Plot confusion matrix
y_test_pred = RMSprop_model.predict_classes(X_test_padded)
c_matrix = confusion_matrix(y_test, y_test_pred)
ax = sns.heatmap(c_matrix, annot=True, xticklabels=['Negative Sentiment',
'Positive Sentiment'], yticklabels=['Negative Sentiment',
'Positive Sentiment'], cbar=False, cmap='Blues', fmt='g')
ax.set_xlabel("Prediction")
ax.set_ylabel("Actual")
plt.show()
总结
在本章中,我们创建了一个基于 LSTM 的神经网络,能够以 85%的准确率预测电影评论的情感。我们首先了解了循环神经网络和 LSTM 的理论,并认识到它们是一类专门设计用于处理序列数据的神经网络,其中数据的顺序至关重要。
我们还了解了如何将如一段文本这样的序列数据转换为神经网络的数值向量输入。我们看到,词嵌入可以将这种数值向量的维度减少到一个更易于训练神经网络的可管理范围,而不一定会丢失信息。词嵌入层通过学习哪些词语彼此相似,并将这些词语聚集到一个簇中,来实现这一点。
我们还探讨了如何在 Keras 中使用Sequential模型轻松构建 LSTM 神经网络。我们还研究了不同优化器对 LSTM 的影响,发现当使用某些优化器时,LSTM 无法从数据中学习。更重要的是,我们看到调参和实验是机器学习过程中至关重要的一部分,能够最大化我们的结果。
最后,我们分析了我们的结果,发现基于 LSTM 的神经网络未能检测到讽刺和我们语言中的其他细微差别。自然语言处理(NLP)是机器学习中一个极具挑战性的子领域,研究人员至今仍在努力攻克。
在下一章,第七章,使用神经网络实现人脸识别系统中,我们将探讨孪生神经网络,以及它们如何用于创建人脸识别系统。
问题
- 在机器学习中,什么是序列问题?
序列问题是机器学习中的一类问题,其中呈现给模型的特征顺序对预测结果至关重要。序列问题的例子包括 NLP 问题(例如语音和文本)和时间序列问题。
- 有哪些原因使得 AI 在解决情感分析问题时面临挑战?
人类语言中往往包含根据上下文有不同含义的单词。因此,在做出预测之前,机器学习模型需要充分理解上下文。此外,讽刺在人的语言中很常见,这对于基于 AI 的模型来说是难以理解的。
- RNN 和 CNN 有何不同?
RNN 可以被看作是一个单一神经网络的多个递归副本。RNN 中的每一层将其输出作为输入传递给下一层。这使得 RNN 可以使用序列数据作为输入。
- RNN 的隐藏状态是什么?
RNN 中从一层传递到另一层的中间输出被称为隐藏状态。隐藏状态使得 RNN 能够保留来自序列数据的中间状态的记忆。
- 使用 RNN 处理序列问题有哪些缺点?
RNN 会遭遇梯度消失问题,这导致序列中较早的特征由于分配给它们的小权重而被“遗忘”。因此,我们说 RNN 存在长期依赖问题。
- LSTM 网络与传统 RNN 有何不同?
LSTM 网络旨在克服传统 RNN 中的长期依赖问题。一个 LSTM 网络包含三个门(输入门、输出门和遗忘门),使其能够强调某些特征(即单词),无论该特征在序列中的出现位置如何。
- 使用 one-hot 编码将单词转化为数值输入的缺点是什么?
one-hot 编码的单词向量的维度往往非常庞大(由于语言中有大量不同的单词),这使得神经网络难以从该向量中学习。此外,one-hot 编码的向量没有考虑语言中相似单词之间的关系。
- 词向量嵌入是什么?
词向量嵌入是一种对单词的学习式向量表示。词向量嵌入的主要优点是它们比 one-hot 编码的表示维度更小,而且它们将相似的单词彼此靠近。词向量嵌入通常是基于 LSTM 的神经网络中的第一层。
- 处理文本数据时需要什么重要的预处理步骤?
文本数据通常具有不均匀的长度,这会导致向量大小不同。神经网络无法接受大小不同的向量作为输入。因此,我们应用零填充作为预处理步骤,以便均匀地截断和填充向量。
- 调优和实验通常是机器学习过程中不可或缺的一部分。在这个项目中,我们做了哪些实验?
在这个项目中,我们尝试了不同的优化器(SGD、RMSprop 和 adam 优化器)来训练我们的神经网络。我们发现,SGD 优化器无法训练 LSTM 网络,而 RMSprop 优化器的准确度最好。
第七章:使用神经网络实现人脸识别系统
在本章中,我们将使用Siamese 神经网络实现人脸识别系统。此类人脸识别系统在智能手机及现代建筑和设施中的其他智能安防系统中广泛应用。我们将探讨 Siamese 神经网络背后的理论,并解释为什么人脸识别问题是图像识别中的一个特殊类别问题,使得传统的卷积神经网络(CNNs)难以解决。我们将训练并实现一个强大的模型,它能够识别面部,即使被识别对象的表情不同,或者照片拍摄角度不同。最后,我们将编写自己的程序,利用预训练的神经网络和网络摄像头来验证坐在电脑前的用户身份。
本章将涵盖的具体主题包括:
-
人脸识别问题
-
人脸检测与人脸识别
-
一次学习
-
Siamese 神经网络
-
对比损失
-
人脸数据集
-
在 Keras 中训练 Siamese 神经网络
-
创建你自己的面部识别系统
技术要求
本章所需的 Python 库如下:
-
Numpy 1.15.2
-
Keras 2.2.4
-
OpenCV 3.4.2
-
PIL 5.4.1
本章的代码可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Neural-Network-Projects-with-Python/tree/master/Chapter07
要将代码下载到你的计算机,可以运行以下git clone命令:
$ git clone https://github.com/PacktPublishing/Neural-Network-Projects-with-Python.git
完成流程后,会生成一个名为Neural-Network-Projects-with-Python的文件夹。通过运行以下命令进入该文件夹:
$ cd Neural-Network-Projects-with-Python
要在虚拟环境中安装所需的 Python 库,请运行以下命令:
$ conda env create -f environment.yml
请注意,在运行此命令之前,你应该先在计算机上安装 Anaconda。要进入虚拟环境,请运行以下命令:
$ conda activate neural-network-projects-python
通过运行以下命令,导航到Chapter07文件夹:
$ cd Chapter07
文件夹中包含以下文件:
-
face_detection.py包含使用 OpenCV 进行人脸检测的 Python 代码 -
siamese_nn.py包含创建和训练 Siamese 神经网络的 Python 代码 -
onboarding.py包含人脸识别系统入职流程的 Python 代码 -
face_recognition_system.py包含完整的人脸识别系统程序
请按以下顺序运行 Python 文件:
-
siamese_nn.py:训练用于人脸识别的 Siamese 神经网络 -
onboarding.py:启动人脸识别系统的入职流程 -
face_recognition_system.py:使用你的网络摄像头的实际人脸识别程序
要运行每个 Python 文件,只需按以下方式执行文件:
$ python siamese_nn.py
人脸识别系统
人脸识别系统已经在我们的日常生活中无处不在。当 iPhone X 于 2017 年首次发布时,苹果公司吹嘘其全新的先进面部识别系统能够通过一次简单的凝视立即识别并认证用户。其背后的驱动力是苹果 A11 仿生芯片,其中包含专门的神经网络硬件,使得 iPhone 能够快速进行人脸识别和机器学习操作。如今,几乎所有智能手机都配备了人脸识别安全系统。
2016 年,亚马逊开设了第一家具有先进人脸识别功能的超市,名为Amazon Go。与传统超市不同,Amazon Go 使用人脸识别来知道你何时进入超市,以及你何时从货架上拿起物品。当你购物结束时,你可以直接走出商店,无需排队结账,因为所有购买的商品都已被亚马逊的 AI 系统记录。这使得繁忙的购物者能够亲自到超市购物,而无需浪费时间排队等候结账。人脸识别系统不再属于反乌托邦的未来,它们已经成为我们日常生活的重要组成部分。
分解人脸识别问题
让我们把人脸识别问题分解成更小的步骤和子问题。这样,我们可以更好地理解人脸识别系统背后发生的事情。人脸识别问题可以分解成以下几个较小的子问题:
-
人脸 检测:在图像中检测并隔离面部。在包含多张面孔的图像中,我们需要分别检测每张面孔。在此步骤中,我们还应从原始输入图像中裁剪出检测到的面孔,以便单独识别。
-
人脸识别:对于图像中每一张被检测到的面孔,我们将通过神经网络对其进行分类。请注意,我们需要对每一张被检测到的面孔重复此步骤。
直观上,这个过程非常有道理。如果我们思考人类如何识别人脸,我们会发现它与我们所描述的过程非常相似。给定一张图像,我们的眼睛立即会集中到每张面孔(人脸检测),然后我们会单独识别每一张面孔(人脸识别)。
下图说明了人脸识别中的子过程:

人脸检测
首先,让我们来看看人脸检测。人脸检测问题实际上是计算机视觉中的一个相当有趣的问题,研究人员在这方面工作了很多年。2001 年,Viola 和 Jones 演示了如何在最小计算资源下进行实时、大规模的人脸检测。这在当时是一个重要的发现,因为研究人员希望实现实时、大规模的人脸检测(例如,实时监控大规模人群)。今天,人脸检测算法可以在简单的硬件上运行,例如我们的个人计算机,只需几行代码。事实上,正如我们很快会看到的,我们将使用 Python 中的 OpenCV 来构建一个人脸检测器,利用你自己的摄像头。
人脸检测有多种方法,包含以下几种:
-
Haar 级联
-
特征脸
-
方向梯度直方图(HOG)
我们将解释如何使用 Haar 级联进行人脸检测(由 Viola 和 Jones 在 2001 年提出),并且我们将看到该算法的优美简洁。
Viola-Jones 算法的核心思想是所有人类面部都有一些共同的特征,例如以下几点:
-
眼睛区域比额头和面颊区域更暗
-
鼻子区域比眼睛区域更亮
在正面、无遮挡的人脸图像中,我们可以看到如眼睛、鼻子和嘴巴等特征。如果我们仔细观察眼睛周围的区域,会发现有一种交替出现的明暗像素模式,如下图所示:

当然,前面的例子只是其中一种可能的特征。我们还可以构建其他特征来捕捉面部的其他区域,如鼻子、嘴巴、下巴等。以下图示展示了其他特征的一些例子:

这些具有交替明暗像素区域的特征被称为 Haar 特征。根据你的想象力,你可以构建几乎无限数量的特征。事实上,在 Viola 和 Jones 提出的最终算法中,使用了超过 6000 个 Haar 特征!
你能看到 Haar 特征和卷积滤波器之间的相似之处吗?它们都在图像中检测出识别的几何表示!不同之处在于 Haar 特征是根据我们已知的知识手工制作的特征,能检测出眼睛、鼻子、嘴巴等面部特征。而卷积滤波器则是在训练过程中生成的,使用带标签的数据集,且不是手工制作的。然而,它们执行的是相同的功能:识别图像中的几何表示。Haar 特征和卷积滤波器之间的相似性表明,机器学习和人工智能中的许多想法是共享的,并且多年来不断迭代改进。
为了使用 Haar 特征,我们将其滑动到图像中的每个区域,并计算该区域像素与 Haar 特征的相似度。然而,由于图像中的大多数区域并不包含人脸(想一想我们拍摄的照片——人脸通常只占照片中的一小部分区域),因此测试所有特征是计算上浪费的。为了解决这个问题,Viola 和 Jones 引入了级联分类器。其思路是从最简单的 Haar 特征开始。如果候选区域失败,即该特征预测该区域不包含人脸,我们立即跳到下一个候选区域。这样,我们就不会把计算资源浪费在那些不包含人脸的区域。我们逐步使用更复杂的 Haar 特征,重复这一过程。最终,图像中包含人脸的区域会通过所有 Haar 特征的测试。这个分类器被称为级联分类器。
使用 Haar 特征的 Viola-Jones 算法在人脸检测中展现出了显著的准确性和较低的假阳性率,同时计算效率也很高。事实上,当该算法在 2001 年首次提出时,它是在一个 700 MHz 的奔腾 III 处理器上运行的!
Python 中的人脸检测
人脸检测可以通过 Python 中的 OpenCV 库实现。OpenCV 是一个开源计算机视觉库,适用于计算机视觉任务。让我们来看看如何使用 OpenCV 进行人脸检测。
首先,我们导入 OpenCV:
import cv2
接下来,我们加载一个预训练的级联分类器用于人脸检测。这个级联分类器可以在随附的 GitHub 仓库中找到,并应已下载到你的计算机上(请参考技术要求部分):
face_cascades = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')
接下来,我们定义一个函数,该函数接收一张图像,对图像进行人脸检测,并在图像上绘制一个边框:
def detect_faces(img, draw_box=True):
# convert image to grayscale
grayscale_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# detect faces
faces = face_cascades.detectMultiScale(grayscale_img, scaleFactor=1.6)
# draw bounding box around detected faces
for (x, y, width, height) in faces:
if draw_box:
cv2.rectangle(img, (x, y), (x+width, y+height), (0, 255, 0), 5)
face_box = img[y:y+height, x:x+width]
face_coords = [x,y,width,height]
return img, face_box, face_coords
让我们在一些示例图像上测试我们的人脸检测器。这些图像可以在 'sample_faces' 文件夹中找到,它们看起来是这样的:

如我们所见,每张图像中都有相当多的噪声(即非人脸结构),这些噪声可能会影响我们的人脸检测器。在右下角的图像中,我们还可以看到多个面孔。
我们应用之前定义的 detect_faces 函数对这些图像进行处理:
import os
files = os.listdir('sample_faces')
images = [file for file in files if 'jpg' in file]
for image in images:
img = cv2.imread('sample_faces/' + image)
detected_faces, _, _ = detect_faces(img)
cv2.imwrite('sample_faces/detected_faces/' + image, detected_faces)
我们看到以下输出图像被保存在 'sample_faces/detected_faces' 文件夹中:

太棒了!我们的面部检测器表现得非常好。检测速度也令人印象深刻。我们可以看到,使用 OpenCV 在 Python 中进行人脸检测是简单且快速的。
人脸识别
完成人脸检测后,我们将注意力转向下一步:人脸识别。你可能已经注意到,人脸检测与神经网络无关!使用 Haar 特征的人脸检测是一个古老但可靠的算法,至今仍被广泛使用。然而,人脸检测仅仅是提取包含面部的区域。我们的下一步是使用提取到的人脸进行人脸识别。
本章的主要内容是使用神经网络进行人脸识别。接下来的章节,我们将重点讨论如何训练一个神经网络来进行人脸识别。
人脸识别系统的要求
到目前为止,你应该对使用神经网络进行图像识别任务相当熟悉。在第四章,猫与狗——使用 CNN 进行图像分类,我们构建了一个 CNN,用于对猫和狗的图像进行分类。那么,相同的技术能否应用于人脸识别呢?遗憾的是,CNN 在这一任务上存在不足。为了理解原因,我们需要了解人脸识别系统的要求。
速度
人脸识别系统的第一个要求是需要足够快速。如果我们看看智能手机中人脸识别系统的注册过程,通常需要用手机的前置摄像头扫描我们的面部,捕捉不同角度的图像,持续几秒钟。在这个短暂的过程中,手机会捕捉我们的面部图像,并用这些图像来训练神经网络识别我们。这个过程需要足够迅速。
下图展示了智能手机中人脸识别系统的典型注册过程:

CNN 能够满足这一速度要求吗?在第四章,猫与狗——使用 CNN 进行图像分类项目中,我们看到训练 CNN 来识别猫和狗的图像是多么缓慢。即便是使用强大的 GPU,训练 CNN 有时也需要几个小时(甚至几天!)。从用户体验的角度来看,对于人脸识别系统的注册过程来说,这样的速度是不可接受的。因此,CNN 无法满足人脸识别系统对速度的要求。
可扩展性
人脸识别系统的第二个要求是其需要具备可扩展性。我们训练的模型最终必须能够扩展到数百万个不同的用户,每个用户都有一个独特的面孔。同样,这就是卷积神经网络(CNN)无法满足的地方。回想一下,在第四章中,猫与狗——使用 CNN 进行图像分类,我们训练了一个 CNN 来区分猫和狗。这个神经网络只能识别和分类猫和狗的图像,不能识别其他动物,因为它没有经过相关训练。这意味着,如果我们使用 CNN 进行人脸识别,我们必须为每个用户训练一个单独的神经网络。从可扩展性角度来看,这显然是不可行的!这就意味着亚马逊需要为其数百万个用户训练一个独立的神经网络,并且每当用户走进亚马逊 Go 的门时,都需要运行数百万个不同的神经网络。
以下图示说明了 CNN 在进行人脸识别时面临的限制:

鉴于内存的限制,为每个用户训练一个神经网络是不切实际的。随着用户数量的增加,这样的系统会迅速变得无法承载。因此,CNN 无法为人脸识别提供可扩展的解决方案。
小数据高精度
人脸识别系统的第三个要求是,在使用少量训练数据时,它必须具有足够的准确性(因此也具有安全性)。在第四章中,猫与狗——使用 CNN 进行图像分类,我们使用了包含成千上万张猫和狗图像的大型数据集来训练我们的 CNN。相比之下,在人脸识别的数据集规模上,我们几乎从未享受过这种奢侈的条件。回到智能手机人脸识别的例子,我们可以看到,只拍摄了少数几张照片,而我们必须能够使用这个有限的数据集来训练我们的模型。
再次强调,CNN 无法满足这一要求,因为我们需要大量图像来训练 CNN。虽然 CNN 在图像分类任务中相当准确,但这也以需要巨大的训练集为代价。试想,如果我们在开始使用手机的人脸识别系统之前,需要用智能手机拍摄成千上万张自拍照!对于大多数人脸识别系统来说,这显然行不通。
一次性学习
鉴于人脸识别系统所面临的独特需求和限制,很明显,使用大量数据集来训练 CNN 进行分类的范式(即批量学习分类)不适用于人脸识别问题。相反,我们的目标是创建一个神经网络,它能够通过仅用一个训练样本来学习识别任何面孔。这种神经网络训练形式被称为一次性学习。
一次性学习带来了机器学习问题中的一种全新且有趣的范式。到目前为止,我们通常将机器学习问题视为分类问题。在第二章,多层感知机预测糖尿病中,我们使用 MLP 来分类处于糖尿病风险中的患者。在第四章,猫与狗——使用 CNN 的图像分类中,我们使用 CNN 来分类猫和狗的图像。在第六章,使用 LSTM 进行电影评论情感分析中,我们使用 LSTM 网络来分类电影评论的情感。在本章中,我们需要将人脸识别问题视为不仅仅是分类问题,还需要估算两张输入图像之间的相似度。
例如,一个一次性学习的人脸识别模型在判断给定的面孔是否属于某个特定人物(例如 A 人物)时,应执行以下任务:
-
获取存储的 A 人物图像(在入职过程中获得的)。这就是 A 人物的真实图像。
-
在测试时(例如,当某人试图解锁 A 人物的手机时),捕捉该人物的图像。这就是测试图像。
-
使用真实照片和测试照片,神经网络应输出两张照片中人脸的相似度评分。
-
如果神经网络输出的相似度评分低于某个阈值(即两张照片中的人看起来不相似),我们将拒绝访问;如果高于阈值,则允许访问。
下图展示了这一过程:

幼稚的一次性预测——两向量之间的欧几里得距离
在我们深入了解神经网络如何应用于一次性学习之前,让我们先看一种简单的方式。
给定真实图像和测试图像,一种简单的一次性预测方法是简单地衡量两张图像之间的差异。正如我们已经看到的,所有图像都是三维向量。我们知道,欧几里得距离提供了两个向量之间差异的数学公式。为了帮助回忆,两个向量之间的欧几里得距离如下图所示:

测量两张图像之间的欧几里得距离为我们提供了一种简单的单次预测方法。然而,它是否为人脸识别提供了令人满意的相似度评分?答案是否定的。尽管欧几里得距离在人脸识别中理论上有意义,但它的实际价值较低。在现实中,照片可能由于角度和光照的变化以及主体外貌的变化(如佩戴眼镜)而不同。正如你可以想象的那样,仅使用欧几里得距离的人脸识别系统在现实中表现会很差。
暹罗神经网络
到目前为止,我们已经看到纯粹的 CNN 和纯粹的欧几里得距离方法在面部识别中效果不好。然而,我们不必完全舍弃它们。它们各自提供了一些对我们有用的东西。我们能否将它们结合起来,形成更好的方法呢?
直观地说,人类通过比较面部的关键特征来识别人脸。例如,人类会利用眼睛的形状、眉毛的粗细、鼻子的大小、面部的整体轮廓等特征来识别一个人。这种能力对我们来说是自然而然的,我们很少受到角度和光照变化的影响。我们是否可以教会神经网络识别这些特征,从面部图像中提取出来,然后再使用欧几里得距离来衡量识别特征之间的相似度?这应该对你来说不陌生!正如我们在前几章中看到的,卷积层擅长自动寻找这种识别特征。对于人脸识别,研究人员发现,当卷积层应用于人脸时,它们会提取空间特征,如眼睛和鼻子。
这个洞察力构成了我们单次学习算法的核心:
-
使用卷积层从人脸中提取识别特征。卷积层的输出应该是将图像映射到一个低维特征空间(例如,一个 128 x 1 的向量)。卷积层应该将同一主体的人脸映射得尽可能靠近,而来自不同主体的人脸则应尽可能远离该低维特征空间。
-
使用欧几里得距离,测量从卷积层输出的两个低维向量之间的差异。请注意,这里有两个向量,因为我们在比较两张图像(真实图像和测试图像)。欧几里得距离与两张图像之间的相似度成反比。
这种方法比前面章节中提到的简单欧几里得距离方法(应用于原始图像像素)效果更好,因为卷积层在第一步的输出代表了人脸中的识别特征(如眼睛和鼻子),这些特征对于角度和光照是不变的。
需要注意的最后一点是,由于我们同时将两张图像输入到神经网络中,因此我们需要两组独立的卷积层。然而,我们要求这两组卷积层共享相同的权重,因为我们希望相似的面部图像被映射到低维特征空间中的同一点。如果这两组卷积层的权重不同,那么相似的面部将会被映射到不同的点,而欧氏距离将不再是一个有效的度量!
因此,我们可以将这两组卷积层视为“孪生”,因为它们共享相同的权重。下面的图示说明了我们刚才描述的神经网络:

这个神经网络被称为孪生神经网络,因为它就像一对连体婴儿,在卷积层部分有一个联合组件。
对比损失
这种基于距离预测的神经网络训练新范式,不同于基于分类的预测,它需要一种新的损失函数。回想一下,在前面的章节中,我们使用了简单的损失函数,例如类别交叉熵,来衡量分类问题中预测的准确性。
在基于距离的预测中,基于准确率的损失函数是无法工作的。因此,我们需要一种新的基于距离的损失函数来训练我们的孪生神经网络进行面部识别。我们将使用的基于距离的损失函数被称为对比损失函数。
看一下以下变量:
-
Y[true]:如果两张输入图像来自同一主题(相同的面部),则让Y[true] = 1;如果两张输入图像来自不同的主题(不同的面部),则让Y[true] = 0
-
D:神经网络输出的预测距离
所以,对比损失定义如下:

在这里,margin 只是一个常数正则化项。如果前面的公式看起来很吓人,不用担心!它的作用仅仅是:当预测距离较大且面部相似时,产生较大的损失(即惩罚),而当预测距离较小时产生较小的损失,面部不同的情况则相反。
下图展示了当面部相似(左)和面部不同(右)时,随着预测距离增大,损失的变化:

简而言之,对比损失函数确保我们的孪生神经网络能够在真实图像和测试图像中的面部相同的情况下预测出较小的距离,而在面部不同的情况下预测出较大的距离。
面部数据集
现在让我们来看一下我们将在此项目中使用的面部数据集。网上有许多公开可用的面部数据集,具体内容可以参见www.face-rec.org/databases/。
虽然我们可以使用许多面部数据集,但用于训练人脸识别系统的最合适数据集应该包含不同主体的照片,每个主体应有多张从不同角度拍摄的照片。理想情况下,数据集还应包含主体展示不同表情(如闭眼等)的照片,因为人脸识别系统通常会遇到此类照片。
考虑到这些因素,我们选择的数据集是由 AT&T 实验室剑桥分部创建的《面部数据库》。该数据库包含 40 个主体的照片,每个主体有 10 张照片。每个主体的照片都在不同的光照和角度下拍摄,并且有不同的面部表情。对于某些主体,还拍摄了戴眼镜和不戴眼镜的多张照片。您可以访问www.cl.cam.ac.uk/research/dtg/attarchive/facedatabase.html了解更多有关 AT&T 面部数据集的信息。
面部数据集与本章的代码一起提供。要从 GitHub 仓库下载数据集和代码,请按照本章前面技术要求部分中的说明操作。
下载 GitHub 仓库后,数据集位于以下路径:
'Chapter07/att_faces/'
图像存储在子文件夹中,每个子文件夹对应一个主体。让我们将原始图像文件导入为 Python 中的 NumPy 数组。我们首先声明一个包含文件路径的变量:
faces_dir = 'att_faces/'
接下来,我们要遍历目录中的每个子文件夹,将每个子文件夹中的图像加载为 NumPy 数组。为此,我们可以导入并使用keras.preprocessing.image中的load_img和img_to_array函数:
from keras.preprocessing.image import load_img, img_to_array
由于共有 40 个主体,我们将前 35 个主体的图像用作训练样本,剩余的 5 个主体用作测试样本。以下代码会遍历每个子文件夹,并相应地将图像加载到X_train和X_test数组中:
import numpy as np
X_train, Y_train = [], []
X_test, Y_test = [], []
# Get list of subfolders from faces_dir
# Each subfolder contains images from one subject
subfolders = sorted([f.path for f in os.scandir(faces_dir) if f.is_dir()])
# Iterate through the list of subfolders (subjects)
# Idx is the subject ID
for idx, folder in enumerate(subfolders):
for file in sorted(os.listdir(folder)):
img = load_img(folder+"/"+file, color_mode='grayscale')
img = img_to_array(img).astype('float32')/255
if idx < 35:
X_train.append(img)
Y_train.append(idx)
else:
X_test.append(img)
Y_test.append(idx-35)
请注意,Y_train和Y_test中的标签仅仅是我们遍历每个子文件夹时的索引(即,第一个子文件夹中的主体被分配标签1,第二个子文件夹中的主体被分配标签2,依此类推)。
最后,我们将X_train、Y_train、X_test和X_test转换为 NumPy 数组:
X_train = np.array(X_train)
X_test = np.array(X_test)
Y_train = np.array(Y_train)
Y_test = np.array(Y_test)
好的!现在我们已经有了训练和测试数据集。我们将使用训练集来训练我们的 Siamese 神经网络,并使用测试数据集中的照片进行测试。
现在,让我们绘制出某个主体的一些图像,以更好地了解我们正在处理的数据类型。以下代码绘制了某个特定主体的九张图像(如subject_idx变量所示):
from matplotlib import pyplot as plt
subject_idx = 4
fig, ((ax1,ax2,ax3),(ax4,ax5,ax6),
(ax7,ax8,ax9)) = plt.subplots(3,3,figsize=(10,10))
subject_img_idx = np.where(Y_train==subject_idx)[0].tolist()
for i, ax in enumerate([ax1,ax2,ax3,ax4,ax5,ax6,ax7,ax8,ax9]):
img = X_train[subject_img_idx[i]]
img = np.squeeze(img)
ax.imshow(img, cmap='gray')
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
plt.tight_layout()
plt.show()
我们看到以下输出:

如我们所见,每张照片中的主体都是在不同的角度拍摄的,且表情各异。在某些照片中,我们还能看到主体摘掉了眼镜。每张图像之间确实有很多差异。
我们还可以使用以下代码从前九个主题中绘制一张单独的图片:
# Plot the first 9 subjects
subjects = range(10)
fig, ((ax1,ax2,ax3),(ax4,ax5,ax6),
(ax7,ax8,ax9)) = plt.subplots(3,3,figsize=(10,12))
subject_img_idx = [np.where(Y_train==i)[0].tolist()[0] for i in subjects]
for i, ax in enumerate([ax1,ax2,ax3,ax4,ax5,ax6,ax7,ax8,ax9]):
img = X_train[subject_img_idx[i]]
img = np.squeeze(img)
ax.imshow(img, cmap='gray')
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
ax.set_title("Subject {}".format(i))
plt.show()
plt.tight_layout()
我们将得到以下输出:

很酷!看起来我们有一系列多样的主题可以处理。
在 Keras 中创建孪生神经网络
我们终于准备好在 Keras 中开始创建孪生神经网络了。在前面的部分中,我们讨论了孪生神经网络的理论和高层结构。现在,让我们更详细地了解孪生神经网络的架构。
下图展示了我们在本章中将构建的孪生神经网络的详细架构:

让我们从在 Keras 中创建共享的卷积网络(如前面图中的框选部分)开始。到目前为止,你应该已经熟悉了 Conv 层、Pooling 层和 Dense 层。如果你需要回顾一下,随时可以参考 第四章,猫狗大战——使用 CNN 进行图像分类,以获取它们的定义。
让我们定义一个函数,使用 Keras 中的 Sequential 类来构建这个共享卷积网络:
from keras.models import Sequential, Input
from keras.layers import Conv2D, MaxPooling2D, Flatten, Dense
def create_shared_network(input_shape):
model = Sequential()
model.add(Conv2D(filters=128, kernel_size=(3,3), activation='relu',
input_shape=input_shape))
model.add(MaxPooling2D())
model.add(Conv2D(filters=64, kernel_size=(3,3), activation='relu'))
model.add(Flatten())
model.add(Dense(units=128, activation='sigmoid'))
return model
我们可以看到,这个函数根据前面的架构创建了一个卷积网络。此时,你可能会想,我们如何在 Keras 中实现两个孪生网络共享权重呢? 好吧,简短的回答是,我们实际上不需要创建两个不同的网络。我们只需要在 Keras 中声明一个共享网络的单一实例。我们可以使用这个单一实例来创建上下卷积网络。由于我们重用了这个单一实例,Keras 会自动理解这些权重是要共享的。
这就是我们如何做的。首先,让我们使用我们之前定义的函数创建共享网络的单一实例:
input_shape = X_train.shape[1:]
shared_network = create_shared_network(input_shape)
我们通过 Input 类指定上下层的输入:
input_top = Input(shape=input_shape)
input_bottom = Input(shape=input_shape)
接下来,我们将共享网络堆叠到输入层的右侧,使用 Keras 中的 functional 方法。执行此操作的语法如下:
output_top = shared_network(input_top)
output_bottom = shared_network(input_bottom)
现在,这种语法可能对你来说比较陌生,因为到目前为止我们一直在使用更为简便的 Sequential 方法来构建模型。虽然它较为简单,但它失去了一些灵活性,某些事情是我们仅通过 Sequential 方法无法做到的,包括像这样构建网络。因此,我们使用 functional 方法来构建这种模型。
此时,我们的模型看起来是这样的:

很好!剩下的就是将顶部和底部的输出组合起来,并计算这两个输出之间的欧几里得距离。记住,此时顶部和底部的输出是 128 x 1 维向量,表示低维特征空间。
由于 Keras 中没有可以直接计算两个数组之间欧几里得距离的层,因此我们需要定义自己的层。Keras 中的Lambda层正是允许我们通过将任意函数封装为Layer对象来实现这一点。
我们来创建一个euclidean_distance函数,用于计算两个向量之间的欧几里得距离:
from keras import backend as K
def euclidean_distance(vectors):
vector1, vector2 = vectors
sum_square = K.sum(K.square(vector1 - vector2), axis=1, keepdims=True)
return K.sqrt(K.maximum(sum_square, K.epsilon()))
然后,我们可以将这个euclidean_distance函数封装到一个Lambda层中:
from keras.layers import Lambda
distance = Lambda(euclidean_distance, output_shape=(1,))([output_top,
output_bottom])
最后,我们将前一行定义的distance层与输入结合起来,完成我们的模型:
from keras.models import Model
model = Model(inputs=[input_top, input_bottom], outputs=distance)
我们可以通过调用summary()函数来验证模型的结构:
print(model.summary())
我们将看到以下输出:

如果我们查看前一张截图中的摘要,可以看到模型中有两个输入层,每个层的形状为 112 x 92 x 1(因为我们的图像是 112 x 92 x 1)。这两个输入层连接到一个共享的卷积网络。共享卷积网络的两个输出(每个为 128 维数组)然后被组合到一个Lambda层,该层计算这两个 128 维数组之间的欧几里得距离。最后,这个欧几里得距离从我们的模型中输出。
就这样!我们成功创建了我们的孪生神经网络。我们可以看到,网络中大部分的复杂性来自共享的卷积网络。有了这个基本框架,我们可以根据需要轻松调整和增加共享卷积网络的复杂度。
Keras 中的模型训练
现在我们已经创建了孪生神经网络,可以开始训练模型了。训练孪生神经网络与训练常规 CNN 略有不同。回想一下,当训练 CNN 时,训练样本是图像数组,并且每个图像都有相应的类别标签。相比之下,训练孪生神经网络时,我们需要使用图像对的数组,并且每对图像有相应的类别标签(即,如果图像对来自同一对象,则标签为 1;如果图像对来自不同对象,则标签为 0)。
以下图表展示了训练 CNN 和孪生神经网络之间的差异:

到目前为止,我们已经将原始图像加载到X_train NumPy 数组中,并附带了Y_train类标签的数组。我们需要编写一个函数,从X_train和Y_train中创建这些图像数组对。一个需要注意的重要点是,在这对图像数组中,类的数量应该相等(即正负对的数量相同,正对指的是来自同一主题的图像,负对指的是来自不同主题的图像),并且我们应该交替使用正负对。这可以防止模型产生偏差,并确保它能够同等地学习正负对图像。
以下函数从X_train和Y_train创建图像及其标签数组对:
import random
def create_pairs(X,Y, num_classes):
pairs, labels = [], []
# index of images in X and Y for each class
class_idx = [np.where(Y==i)[0] for i in range(num_classes)]
# The minimum number of images across all classes
min_images = min(len(class_idx[i]) for i in range(num_classes)) - 1
for c in range(num_classes):
for n in range(min_images):
# create positive pair
img1 = X[class_idx[c][n]]
img2 = X[class_idx[c][n+1]]
pairs.append((img1, img2))
labels.append(1)
# create negative pair
# list of classes that are different from the current class
neg_list = list(range(num_classes))
neg_list.remove(c)
# select a random class from the negative list.
# This class will be used to form the negative pair.
neg_c = random.sample(neg_list,1)[0]
img1 = X[class_idx[c][n]]
img2 = X[class_idx[neg_c][n]]
pairs.append((img1,img2))
labels.append(0)
return np.array(pairs), np.array(labels)
num_classes = len(np.unique(Y_train))
training_pairs, training_labels = create_pairs(X_train, Y_train,
len(np.unique(Y_train)))
test_pairs, test_labels = create_pairs(X_test, Y_test,
len(np.unique(Y_test)))
在开始训练模型之前,还有一件事需要做。我们需要为对比损失定义一个函数,因为对比损失不是 Keras 中的默认损失函数。
总结一下,这是对比损失的公式:

其中Y[true]是训练对的真实标签,D是神经网络输出的预测距离。
我们为计算对比损失定义了以下函数:
def contrastive_loss(Y_true, D):
margin = 1
return K.mean(Y_true*K.square(D)+(1 - Y_true)*K.maximum((margin-D),0))
请注意,函数中包含了K.mean、K.square和K.maximum。这些只是 Keras 的后端函数,用于简化数组计算,如均值、最大值和平方。
好的,我们已经具备了训练我们的孪生神经网络所需的所有函数。像往常一样,我们使用compile函数定义训练的参数:
model.compile(loss=contrastive_loss, optimizer='adam')
然后我们通过调用fit函数来训练模型10个周期:
model.fit([training_pairs[:, 0], training_pairs[:, 1]], training_labels,
batch_size=64, epochs=10)
一旦训练完成,我们将看到以下输出:

分析结果
让我们在保留的测试集上应用我们的模型,看看它表现如何。记住,我们的模型从未见过测试集中的图像和主题,因此这是衡量其现实世界表现的好方法。
首先,我们从同一个主题中选择两张图片,将它们并排展示,并将模型应用于这对图片:
idx1, idx2 = 21, 29
img1 = np.expand_dims(X_test[idx1], axis=0)
img2 = np.expand_dims(X_test[idx2], axis=0)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10,7))
ax1.imshow(np.squeeze(img1), cmap='gray')
ax2.imshow(np.squeeze(img2), cmap='gray')
for ax in [ax1, ax2]:
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
dissimilarity = model.predict([img1, img2])[0][0]
fig.suptitle("Dissimilarity Score = {:.3f}".format(dissimilarity), size=30)
plt.tight_layout()
plt.show()
我们将看到以下输出:

请注意,Dissimilarity Score 只是模型输出的距离。距离越大,两个面孔之间的差异越大。
我们的模型表现很好!我们可以清楚地看到照片中的主体是相同的。在第一张图片中,主体戴着眼镜,正视镜头并微笑。在第二张图片中,同一主体没有戴眼镜,未正视镜头,也没有微笑。我们的面部识别模型仍然能够识别这对照片中的两张脸属于同一个人,从低相似度得分可以看出这一点。
接下来,我们从不同的主题中选择一对面孔,看看我们的模型表现如何:
idx1, idx2 = 1, 39
img1 = np.expand_dims(X_test[idx1], axis=0)
img2 = np.expand_dims(X_test[idx2], axis=0)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10,7))
ax1.imshow(np.squeeze(img1), cmap='gray')
ax2.imshow(np.squeeze(img2), cmap='gray')
for ax in [ax1, ax2]:
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
dissimilarity = model.predict([img1, img2])[0][0]
fig.suptitle("Dissimilarity Score = {:.3f}".format(dissimilarity), size=30)
plt.tight_layout()
plt.show()
我们将看到以下输出:

我们的模型对于负对(图像中的人物不同的对)表现得也很好。在这种情况下,差异得分是 1.28。我们知道,正对有低差异得分,而负对有高差异得分。但分隔它们的阈值分数是多少呢?让我们对正对和负对做更多的测试来找出答案:
for i in range(5):
for n in range(0,2):
fig, (ax1, ax2) = plt.subplots(1,2, figsize=(7,5))
img1 = np.expand_dims(test_pairs[i*20+n, 0], axis=0)
img2 = np.expand_dims(test_pairs[i*20+n, 1], axis=0)
dissimilarity = model.predict([img1, img2])[0][0]
img1, img2 = np.squeeze(img1), np.squeeze(img2)
ax1.imshow(img1, cmap='gray')
ax2.imshow(img2, cmap='gray')
for ax in [ax1, ax2]:
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
plt.tight_layout()
fig.suptitle("Dissimilarity Score = {:.3f}".format(dissimilarity),
size=20)
plt.show()
以下截图显示了某些人脸对的结果。请注意,正对位于左侧,负对位于右侧:

你注意到什么有趣的事情了吗? 从前面的结果来看,阈值分数似乎大约是 0.5。低于 0.5 的应该被归类为正对(即,面孔匹配),高于 0.5 的应该被归类为负对。请注意,第二行右侧列中的负对分数非常接近阈值,分数为 0.501。有趣的是,这两个人确实很相似,戴着类似的眼镜和发型!
整合我们的代码
此时,整合我们的代码会很有帮助。到目前为止,我们已经编写了很多代码,包括辅助函数。让我们将这些辅助函数整合到一个utils.py文件中,如下所示。
首先,我们导入必要的库:
import numpy as np
import random
import os
import cv2
from keras.models import Sequential
from keras.layers import Flatten, Dense, Conv2D, MaxPooling2D
from keras import backend as K
from keras.preprocessing.image import load_img, img_to_array
我们还在utils.py文件中包含了训练 Siamese 神经网络所需的euclidean_distance、contrastive_loss和accuracy函数:
def euclidean_distance(vectors):
vector1, vector2 = vectors
sum_square = K.sum(K.square(vector1 - vector2), axis=1, keepdims=True)
return K.sqrt(K.maximum(sum_square, K.epsilon()))
def contrastive_loss(Y_true, D):
margin = 1
return K.mean(Y_true*K.square(D)+(1 - Y_true)*K.maximum((margin-D),0))
def accuracy(y_true, y_pred):
return K.mean(K.equal(y_true, K.cast(y_pred < 0.5, y_true.dtype)))
我们将create_pairs函数包含在utils.py文件中。回想一下,这个辅助函数用于生成用于训练 Siamese 神经网络的正对和负对图像:
def create_pairs(X,Y, num_classes):
pairs, labels = [], []
# index of images in X and Y for each class
class_idx = [np.where(Y==i)[0] for i in range(num_classes)]
# The minimum number of images across all classes
min_images = min(len(class_idx[i]) for i in range(num_classes)) - 1
for c in range(num_classes):
for n in range(min_images):
# create positive pair
img1 = X[class_idx[c][n]]
img2 = X[class_idx[c][n+1]]
pairs.append((img1, img2))
labels.append(1)
# create negative pair
neg_list = list(range(num_classes))
neg_list.remove(c)
# select a random class from the negative list.
# this class will be used to form the negative pair
neg_c = random.sample(neg_list,1)[0]
img1 = X[class_idx[c][n]]
img2 = X[class_idx[neg_c][n]]
pairs.append((img1,img2))
labels.append(0)
return np.array(pairs), np.array(labels)
我们还在utils.py文件中包含了create_shared_network辅助函数,该函数用于在 Keras 中创建一个 Siamese 神经网络:
def create_shared_network(input_shape):
model = Sequential(name='Shared_Conv_Network')
model.add(Conv2D(filters=64, kernel_size=(3,3), activation='relu',
input_shape=input_shape))
model.add(MaxPooling2D())
model.add(Conv2D(filters=64, kernel_size=(3,3), activation='relu'))
model.add(Flatten())
model.add(Dense(units=128, activation='sigmoid'))
return model
我们在utils.py文件中的最后一个辅助函数是get_data函数。该函数帮助我们将相应的原始图像加载到 NumPy 数组中:
def get_data(dir):
X_train, Y_train = [], []
X_test, Y_test = [], []
subfolders = sorted([file.path for file in os.scandir(dir) if
file.is_dir()])
for idx, folder in enumerate(subfolders):
for file in sorted(os.listdir(folder)):
img = load_img(folder+"/"+file, color_mode='grayscale')
img = img_to_array(img).astype('float32')/255
img = img.reshape(img.shape[0], img.shape[1],1)
if idx < 35:
X_train.append(img)
Y_train.append(idx)
else:
X_test.append(img)
Y_test.append(idx-35)
X_train = np.array(X_train)
X_test = np.array(X_test)
Y_train = np.array(Y_train)
Y_test = np.array(Y_test)
return (X_train, Y_train), (X_test, Y_test)
你可以在我们提供的代码中看到utils.py文件。
同样,我们可以创建一个siamese_nn.py文件。这个 Python 文件将包含创建和训练我们 Siamese 神经网络的主要代码:
'''
Main code for training a Siamese neural network for face recognition
'''
import utils
import numpy as np
from keras.layers import Input, Lambda
from keras.models import Model
faces_dir = 'att_faces/'
# Import Training and Testing Data
(X_train, Y_train), (X_test, Y_test) = utils.get_data(faces_dir)
num_classes = len(np.unique(Y_train))
# Create Siamese Neural Network
input_shape = X_train.shape[1:]
shared_network = utils.create_shared_network(input_shape)
input_top = Input(shape=input_shape)
input_bottom = Input(shape=input_shape)
output_top = shared_network(input_top)
output_bottom = shared_network(input_bottom)
distance = Lambda(utils.euclidean_distance, output_shape=(1,))([output_top, output_bottom])
model = Model(inputs=[input_top, input_bottom], outputs=distance)
# Train the model
training_pairs, training_labels = utils.create_pairs(X_train, Y_train,
num_classes=num_classes)
model.compile(loss=utils.contrastive_loss, optimizer='adam',
metrics=[utils.accuracy])
model.fit([training_pairs[:, 0], training_pairs[:, 1]], training_labels,
batch_size=128,
epochs=10)
# Save the model
model.save('siamese_nn.h5')
这个 Python 文件保存在我们提供的代码中,路径为'Chapter07/siamese_nn.py'。请注意,代码比之前短了很多,因为我们已经重构了代码,将辅助函数移到了utils.py中。
请注意,前面代码的最后一行将训练好的模型保存在Chapter07/siamese_nn.h5位置。这样,我们可以轻松地导入训练好的面部识别模型,而不需要从头开始重新训练模型。
创建实时人脸识别程序
我们终于来到了项目中最重要的部分。我们将把到目前为止编写的代码整合在一起,创建一个实时的人脸识别程序。这个程序将使用我们计算机中的网络摄像头进行人脸识别,验证坐在摄像头前的人是否真的是你。
为了实现这一点,程序需要完成以下任务:
-
训练一个用于人脸识别的 Siamese 神经网络(这部分已经在上一节完成)。
-
使用网络摄像头捕捉授权用户的真实图像。这是人脸识别系统的注册过程。
-
随后,当用户想要解锁程序时,使用步骤 1中的预训练 Siamese 神经网络和步骤 2中的真实图像来验证用户身份。
这个部分的项目需要一个网络摄像头(可以是你笔记本电脑内置的摄像头,也可以是你连接到计算机的外接摄像头)。如果你的计算机没有网络摄像头,可以跳过这个部分。
注册过程
让我们编写注册过程的代码。在注册过程中,我们需要激活摄像头来捕捉授权用户的真实图像。OpenCV 提供了一个名为VideoCapture的函数,它允许我们激活并从计算机的摄像头捕获图像:
import cv2
video_capture = cv2.VideoCapture(0)
在使用网络摄像头拍照之前,给用户五秒钟的准备时间。我们启动一个初始值为5的counter变量,当计时器达到0时,用摄像头拍照。注意,我们使用本章早些时候编写的face_detection.py文件中的代码来检测摄像头前的人脸。拍摄的照片将保存在与代码相同的文件夹下,命名为'true_img.png':
import math
import utils
import face_detection
counter = 5
while True:
_, frame = video_capture.read()
frame, face_box, face_coords = face_detection.detect_faces(frame)
text = 'Image will be taken in {}..'.format(math.ceil(counter))
if face_box is not None:
frame = utils.write_on_frame(frame, text, face_coords[0],
face_coords[1]-10)
cv2.imshow('Video', frame)
cv2.waitKey(1)
counter -= 0.1
if counter <= 0:
cv2.imwrite('true_img.png', face_box)
break
# When everything is done, release the capture
video_capture.release()
cv2.destroyAllWindows()
print("Onboarding Image Captured")
注册过程如下所示:

这段代码保存在我们提供的文件中,路径为Chapter07/onboarding.py。要运行注册过程,只需在命令提示符(Windows)或终端(macOS/Linux)中执行该 Python 文件,方法如下:
$ python onboarding.py
人脸识别过程
注册过程完成后,我们可以继续进行实际的人脸识别过程。我们首先询问用户的姓名。稍后该姓名将显示在人脸识别图像上,如我们所见。Python 中的input函数允许用户输入姓名:
name = input("What is your name?")
然后,用户将在命令行中输入姓名。
接下来,我们导入本章早些时候训练好的 Siamese 神经网络:
from keras.models import load_model
model = load_model('siamese_nn.h5',
custom_objects={'contrastive_loss':
utils.contrastive_loss,
'euclidean_distance':utils.euclidean_distance})
接下来,我们加载在注册过程中捕捉到的用户真实图像,并通过标准化、调整大小和重塑图像来为我们的 Siamese 神经网络预处理图像:
true_img = cv2.imread('true_img.png', 0)
true_img = true_img.astype('float32')/255
true_img = cv2.resize(true_img, (92, 112))
true_img = true_img.reshape(1, true_img.shape[0], true_img.shape[1], 1)
其余的代码使用了 OpenCV 中的VideoCapture函数从用户的摄像头捕捉视频,并将视频中的每一帧传递给我们的face_detection实例。我们使用一个固定长度的列表(由 Python 的collections.deque类实现)来收集 15 个最新的预测结果(每一帧一个预测)。我们对这 15 个最新预测的得分进行平均,如果平均相似度得分超过某个阈值,我们就认证用户。其余代码如下所示:
video_capture = cv2.VideoCapture(0)
preds = collections.deque(maxlen=15)
while True:
# Capture frame-by-frame
_, frame = video_capture.read()
# Detect Faces
frame, face_img, face_coords = face_detection.detect_faces(frame,
draw_box=False)
if face_img is not None:
face_img = cv2.cvtColor(face_img, cv2.COLOR_BGR2GRAY)
face_img = face_img.astype('float32')/255
face_img = cv2.resize(face_img, (92, 112))
face_img = face_img.reshape(1, face_img.shape[0],
face_img.shape[1], 1)
preds.append(1-model.predict([true_img, face_img])[0][0])
x,y,w,h = face_coords
if len(preds) == 15 and sum(preds)/15 >= 0.3:
text = "Identity: {}".format(name)
cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 5)
elif len(preds) < 15:
text = "Identifying ..."
cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 165, 255), 5)
else:
text = "Identity Unknown!"
cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 0, 255), 5)
frame = utils.write_on_frame(frame, text, face_coords[0],
face_coords[1]-10)
else:
# clear existing predictions if no face detected
preds = collections.deque(maxlen=15)
# Display the resulting frame
cv2.imshow('Video', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# When everything is done, release the capture
video_capture.release()
cv2.destroyAllWindows()
这段代码保存在'Chapter07/face_recognition_system.py'文件中,您可以按照以下方法在命令提示符(Windows)或终端(macOS/Linux)中执行该 Python 文件来运行程序:
$ python face_recognition_system.py
确保首先运行入职程序(以捕捉真实图像),然后再运行人脸识别程序。
这是程序初次尝试识别您面孔时的样子:

几秒钟后,程序应该会识别出您:

未来的工作
正如我们所见,我们的人脸识别系统在简单条件下确实工作得很好。然而,它绝对不是万无一失的,当然也不足够安全,无法应用于重要的场景。首先,人脸检测系统会被静态照片欺骗(自己试试看!)。从理论上讲,这意味着我们可以通过将授权用户的静态照片放在摄像头前来绕过认证。
解决这个问题的技术被称为反欺骗技术。反欺骗技术是人脸识别领域的一个重要研究方向。通常,今天使用的主要反欺骗技术有两种:
-
活体检测:由于照片是静态的二维图像,而真实面孔是动态且三维的,我们可以检查检测到的面孔的活体。进行活体检测的方式包括检查检测到的面孔的光流,以及检查检测到的面孔与周围环境的光照和纹理对比。
-
机器学习:我们也可以通过使用机器学习来区分真实面孔和图片!我们可以训练一个卷积神经网络(CNN)来分类检测到的面孔是属于真实面孔还是静态图像。然而,您需要大量标注数据(面孔与非面孔)才能实现这一点。
这是 Andrew Ng 的一段视频,展示了人脸识别(带活体检测)是如何在百度中国总部实现的:
www.youtube.com/watch?v=wr4rx0Spihs
如果您想了解苹果是如何在 iPhone 中实现面部 ID 系统的,可以参考苹果发布的论文www.apple.com/business/site/docs/FaceID_Security_Guide.pdf。
苹果的 Face ID 实现比我们在本章中使用的系统更为安全。苹果使用 TrueDepth 相机将红外线点投射到你的脸上,创建深度图,之后利用深度图进行人脸识别。
总结
在本章中,我们创建了一个基于 Siamese 神经网络的人脸识别系统。该人脸识别系统使用摄像头从实时视频流中提取帧,传递给预训练的 Siamese 神经网络,并使用用户的真实图像,系统能够验证站在摄像头前的用户。
我们首先将人脸识别问题分解成更小的子问题,我们看到人脸识别系统首先执行人脸检测步骤,将人脸从图像的其余部分分离出来,然后才进行实际的人脸识别步骤。我们看到人脸检测通常是通过 Viola-Jones 算法完成的,该算法使用 Haar 特征实时检测人脸。使用 Haar 滤波器的人脸检测在 Python 中通过 OpenCV 库实现,这使得我们只需几行代码就能执行人脸检测。
我们随后集中讨论了人脸识别,并讨论了人脸识别系统的需求(速度、可扩展性、在小数据下的高准确度)如何使得 CNNs 不适用于这个问题。我们介绍了 Siamese 神经网络的架构,以及如何利用 Siamese 神经网络中的基于距离的预测进行人脸识别。我们使用 AT&T 人脸数据集,从头开始在 Keras 中训练了一个 Siamese 神经网络。
最后,利用预训练的 Siamese 神经网络,我们在 Python 中创建了我们自己的人脸识别系统。人脸识别系统由两个步骤组成。在第一步(注册过程)中,我们使用 OpenCV 的人脸检测 API,通过摄像头捕捉用户的图像,作为 Siamese 神经网络的真实图像。在第二步中,系统利用真实图像识别和验证程序用户。
在下一章,也是最后一章,第八章,接下来是什么?,我们将总结和回顾到目前为止在本书中完成的不同项目。我们还将展望未来,看看神经网络和人工智能在未来几年会是什么样子。
问题
- 人脸检测与人脸识别有何不同?
人脸检测的目标是定位图像中的人脸。人脸检测过程的输出是围绕检测到的人脸的边界框。另一方面,人脸识别的目标是对人脸进行分类(即识别对象)。人脸检测和人脸识别是每个人脸识别系统中的两个关键步骤,人脸检测步骤的输出作为输入传递给人脸识别步骤。
- Viola-Jones 人脸检测算法是什么?
Viola-Jones 算法使用 Haar 特征进行人脸检测。Haar 特征是具有交替明暗区域的滤波器,表示人脸中像素强度的对比度。例如,人脸图像中的眼睛区域具有比额头和脸颊区域更暗的像素值。这些 Haar 滤波器用于定位图像中可能包含人脸的区域。
- 什么是单次学习(one-shot learning),它与批量学习(batch learning)有何不同?
在单次学习中,目标是使用非常少的数据来训练机器学习模型。与此不同,批量学习使用大量数据集来训练机器学习模型。单次学习通常用于图像识别任务,因为训练样本的数量可能非常稀疏。
- 描述 Siamese 神经网络的架构。
Siamese 神经网络由两个共享权重的联合卷积层组成,接受一对输入图像。联合卷积层将两个输入图像映射到一个低维特征空间。通过欧几里得距离层,我们计算并输出这两个低维向量的距离,该距离与两张图像的相似度成反比。
- 在训练用于人脸识别的 Siamese 神经网络时,使用的损失函数是什么?
我们使用对比损失函数来训练用于人脸识别的 Siamese 神经网络。对比损失函数鼓励神经网络在输入图像对相似时输出小距离,反之,在输入图像对不同时,鼓励输出较大的距离。
第八章:下一步是什么?
我们做到了!我们创建了六个不同的神经网络项目,每个项目都有其独特的架构。在本章的最后,我们将回顾我们所取得的成就。我们还将看看一些近期在神经网络和深度学习方面的进展,这些进展在前几章中并未涉及。最后,我们将展望未来,看看神经网络和人工智能的前景。
具体来说,以下是我们将在本章中讨论的主题:
-
本书中使用的不同神经网络回顾
-
神经网络关键概念回顾
-
神经网络的前沿进展
-
神经网络的局限性
-
人工智能与机器学习的未来
-
跟上机器学习的步伐
-
最喜爱的机器学习工具
-
你将创造什么?
将一切整合在一起
我们在本书中已经完成了很多工作。让我们快速回顾一下每一章中所构建的项目,以及支持它们的神经网络架构。本节也为我们在本书中涉及的关键神经网络概念提供了一个快速复习。
机器学习与神经网络基础
在第一章,机器学习与神经网络基础,我们从构建最简单的单层神经网络——感知器开始。从本质上讲,感知器只是一个数学函数,它接收一组输入,执行一些数学运算,然后输出计算结果。对于感知器,数学运算就是将权重与输入进行相乘。
因此,正确的权重集决定了我们的神经网络性能的好坏。一开始,神经网络的权重是随机初始化的。调整神经网络的权重以最大化模型性能的过程叫做模型训练。在训练过程中,神经网络的权重不断被调整,以最小化损失函数。损失函数只是一个数学函数,允许我们量化神经网络的表现。我们用来调整权重以最小化损失函数的算法称为梯度下降。
我们从零开始创建了我们的第一个神经网络,没有使用像 Keras 或 scikit-learn 这样的机器学习库。我们将这个简单的神经网络应用于一个玩具示例,在这个示例中,神经网络需要学习二元(即 1 或 0)预测。我们使用平方和误差作为损失函数来训练我们的神经网络,其中如果预测错误,误差为 1,如果预测正确,误差为 0。然后,我们对每个个体数据点的误差进行求和,得到平方和误差。我们看到我们的神经网络能够从训练示例中学习,并为测试数据提供准确的预测。
在理解了神经网络的概念后,我们接着讨论了 Python 中最重要的神经网络和机器学习库。我们了解到,在处理表格数据(即来自 CSV 文件的数据)时,pandas 是不可或缺的工具,并且它也可以用于数据可视化。更重要的是,我们讨论了 Keras,这是在 Python 中进行神经网络和深度学习工作时的核心库。
我们讨论了 Keras 中的基本构建模块——层。Keras 中有多种类型的层,但最重要的两种是卷积层和全连接层,它们是我们书中讨论的所有神经网络的构建模块。
多层感知机预测糖尿病
在第二章,使用多层感知机预测糖尿病,我们通过创建一个神经网络,开始了第一个项目,目标是预测患者是否有糖尿病风险。具体来说,我们使用了一个称为 MLP(多层感知机)的神经网络来进行这种分类预测。我们使用了 Pima 印第安人糖尿病数据集来进行这一问题的研究。该数据集包含 768 个不同的数据点,每个数据点有 8 个特征和一个标签。
作为机器学习工作流的一部分,在使用该数据集进行神经网络训练之前,我们需要进行数据预处理。我们需要填补缺失值、进行数据标准化,并将数据集分为训练集和测试集。
在这一章中,我们还用 Keras 构建了我们的第一个神经网络。我们展示了如何使用 Keras 中的 Sequential 类,一层一层地构建神经网络,就像将乐高积木一个个堆叠起来一样。我们还研究了 ReLU 和 sigmoid 激活 函数,这两种是最常用的激活函数。
我们通过使用 混淆矩阵 和 ROC 曲线 等度量指标,评估了神经网络的表现,这些工具对于帮助我们理解神经网络的性能非常重要。
使用深度前馈神经网络预测出租车费用
在第三章,使用深度前馈神经网络预测出租车费用,我们在一个 回归 预测 问题中,使用了 深度前馈神经网络,任务是预测纽约市出租车费用的美元金额,基于拾取和放下地点等特征。在这个项目中,我们需要处理一个包含缺失数据和数据异常的嘈杂数据集。我们了解到,数据可视化对于帮助我们识别数据集中的异常值,以及发现数据集中的重要趋势至关重要。
这个项目也是第一次进行特征工程的项目。利用现有特征,我们创建了其他特征,提升了神经网络的准确性。最后,我们使用自己的数据集,在 Keras 中创建并训练了一个深度前馈神经网络,最终得到了令人印象深刻的均方误差 3.50。
猫狗大战 – 使用 CNN 进行图像分类
在 第四章,猫狗大战 – 使用 CNN 进行图像分类,我们开始了第一个图像识别和计算机视觉领域的神经网络项目。具体来说,我们创建了一个能够分类猫狗图像的 CNN。
我们看到数字图像本质上是二维数组(对于灰度图像),每个数组值代表每个像素的强度。CNN 是解决大多数图像识别问题的首选神经网络架构。CNN 中的 过滤 和 卷积 操作用于识别图像中的重要空间特征,这使得它非常适用于图像识别问题。多年来,CNN 经历了多次迭代和改进。LeNet 在 1998 年首次出现,随后更复杂的神经网络如 VGG16 和 ResNet 在 2010 年代相继开发出来。
我们在 Keras 中创建了自己的 CNN,并使用 Keras 的 ImageDataGenerator 和 flow_from_directory 方法来训练神经网络,当数据集(猫狗图片)太大,无法一次性加载到内存时。我们创建的简单 CNN 达到了 80% 的准确率。我们还使用了 迁移学习,利用预训练的 VGG16 神经网络来解决猫狗分类问题。这种方法展示了 VGG16 模型的复杂性,达到了 90% 的准确率。
使用自编码器去除图像噪声
在 第五章,使用自编码器去除图像噪声,我们研究了 自编码器,一种特殊的神经网络,它学习输入的 潜在表示。自编码器有一个 编码器 部件,它将输入压缩为潜在表示,还有一个 解码器 部件,它使用潜在表示重建输入。
在自编码器中,用于潜在表示的隐藏层大小是一个重要的超参数,需要仔细调整。潜在表示的大小应该足够小,以表示输入特征的压缩表示,同时也要足够 大,使得解码器能够重建输入,而不会有太多损失。我们训练了一个自编码器来压缩 MNIST 图像。我们通过使用 32 × 1 的隐藏层大小,达到了 24.5 的压缩率,同时确保重建的图像与原始输入图像相似。
我们还研究了如何使用自编码器进行图像 去噪。通过使用噪声图像作为输入,清晰图像作为输出,我们可以训练自编码器识别不属于噪声的图像特征。这样,我们就可以应用自编码器从图像中去除噪声。这样的自编码器被称为去噪自编码器。我们训练并应用了一个去噪自编码器,针对包含脏污办公文件扫描图像的去噪办公文件数据集。在去噪自编码器中使用深度卷积层后,我们成功地几乎完全去除了办公文件中的噪声。
使用 LSTM 进行电影评论情感分析
在第六章《使用 LSTM 进行电影评论情感分析》中,我们探讨了情感分析,它是自然语言处理(NLP)领域中的一个序列问题。我们看到,情感分析对人类来说也是一个具有挑战性的问题,因为词语在不同语境中传递的意义不同。RNN 被认为是处理情感分析等序列问题的最佳神经网络形式。然而,传统的递归神经网络存在长期依赖问题,这使得它不适用于处理长篇文本。
一种变种的递归神经网络,称为 LSTM 网络,旨在克服长期依赖问题。LSTM 的直觉是,由于其能够为特定词语分配权重,我们可以有选择性地忘记不重要的词语,而记住更重要的词语。
我们还研究了如何通过词嵌入将词语表示为向量。词嵌入将词语转化为较低维度的特征空间,将相似的词语放置在彼此靠近的位置,而将不相似的词语放置得更远。
我们在 Keras 中创建并训练了一个 LSTM 网络,用于对 IMDB 电影评论数据集进行情感分析,同时研究了一些在训练 LSTM 网络时需要调整的重要超参数。特别是,我们看到了优化器对 LSTM 网络性能的显著影响。我们最终的 LSTM 网络在分类 IMDB 电影评论情感时达到了 85%的准确率。
使用神经网络实现面部识别系统
在第七章《使用神经网络实现人脸识别系统》中,我们使用 Siamese 神经网络创建了一个人脸识别系统。Siamese 神经网络是一类特殊的神经网络,具有共享的、联结的组件。Siamese 神经网络接受一对图像作为输入,并可以训练输出与两张图像相似度成反比的距离。这构成了使用 Siamese 神经网络进行人脸识别的基本思路。如果输入图像对中的两张人脸属于同一人物,那么输出的距离应该很小,反之亦然。通过使用对比损失训练 Siamese 神经网络,输入正对(属于同一人物的人脸)和负对(属于不同人物的人脸),它最终会学会为正对和负对输出适当的距离。
我们还讨论了人脸检测,它是人脸识别的重要前提。人脸检测用于从原始图像中隔离和提取人脸,提取后的图像会传递给神经网络进行人脸识别。人脸检测通常使用 Viola-Jones 算法,它利用 Haar 特征来检测图像中的面部特征。为了创建我们的人脸识别系统,我们结合了 OpenCV,它利用计算机摄像头的视频流进行人脸检测,以及我们训练的人脸识别 Siamese 神经网络。
神经网络的前沿进展
正如我们在前一节中看到的,本书涉及了很多内容。然而,神经网络的可能性实际上是无穷无尽的。还有一些重要的神经网络类型,我们在本书中还没有讨论。为了完整起见,我们将在本节中讨论它们。正如你将看到的,这些神经网络与我们到目前为止看到的非常不同,它应该会为你提供一个全新的视角。
生成对抗网络
生成对抗网络(GANs)是一类生成神经网络。要理解生成模型,重要的是将它们与判别模型进行对比。在本书之前的内容中,我们只关注了判别模型。判别模型关注学习特征到标签的映射。例如,当我们创建一个卷积神经网络(CNN)来分类猫狗图像时,CNN 是一个判别模型,它学习特征(图像)到标签(猫或狗)的映射。
另一方面,生成模型关注于在给定标签的情况下生成适当的特征。例如,给定标注为猫和狗的图像,生成模型将学习为每个标签生成适当的特征。换句话说,生成模型学会合成猫狗的图像!
GAN 是近年来人工智能领域最令人兴奋的发展之一。事实上,Yann LeCun 曾称 GAN 是过去 10 年中机器学习最有趣的想法。那么,GAN 是如何工作的呢?直观地讲,GAN 由两个部分组成——生成器和判别器。生成器的作用是生成特征(例如图像),判别器的作用是评估生成的特征与原始特征的相似度。在训练 GAN 时,我们将生成器与判别器对立起来(这也是 GAN 中“对抗”一词的来源)。最终,生成器会变得如此强大,以至于判别器无法区分生成的特征与原始特征,GAN 就能生成栩栩如生的图像。
为了了解 GAN 的强大程度,请查看以下由 NVIDIA 研究人员发布的论文:
在这篇论文中,你将看到一些由 GAN 生成的面孔样本,这些面孔与真实的人类面孔无法区分。GAN 的进步速度令人惊讶,现在我们已经能够生成超真实的人类面孔。
GAN 已被应用于几个有趣的案例。例如,研究人员已找到一种将 GAN 应用于风格迁移的方法。在风格迁移中,GAN 学习给定图像的艺术风格,并将其应用到另一幅图像上。例如,我们可以使用 GAN 来学习文森特·梵高著名的星夜画作的艺术风格,并将其应用于任何任意图像。可以访问github.com/jcjohnson/neural-style查看风格迁移的示例。
深度强化学习
强化学习是机器学习的一个分支,旨在学习在任何给定状态下采取最佳行动,以最大化未来的奖励。强化学习已被应用于象棋等游戏。在象棋中,棋盘上棋子的布局代表了我们所处的状态。强化学习的作用是学习在任何给定状态下应采取的最佳行动(即,应该移动哪些棋子)。
如果我们将任何任意状态下应采取的最佳行动表示为一个数学函数(称之为行动价值函数),那么我们可以使用神经网络来学习这个行动价值函数。一旦这个函数被学习出来,我们的神经网络就可以用来预测在任何给定状态下应该采取的最佳行动——本质上,我们的神经网络就成了一个无敌的棋手!将深度神经网络应用于强化学习的过程称为深度强化学习。
深度强化学习在游戏中取得了很多成功。2017 年,使用深度强化学习训练的 AI 游戏玩家 AlphaGo 成功战胜了世界顶级围棋选手之一柯洁。AlphaGo 的胜利引发了广泛的关注和讨论,尤其是关于人工智能未来的讨论。
2018 年,深度强化学习迎来了又一次飞跃,当时 OpenAI Five(由五个神经网络组成的团队)成功击败了 Dota 2 的业余玩家。Dota 2 是一款多人在线游戏,曾被认为是人工智能无法攻克的领域,原因是游戏的复杂性和动态性极高。职业 Dota 2 玩家是公认的明星,全球顶尖 Dota 2 玩家因其思维敏捷和反应迅速而深受粉丝喜爱。如今,OpenAI Five 不断突破其在 Dota 2 中的能力边界。OpenAI Five 每天通过自我对战进行训练,积累 180 年的游戏经验。OpenAI Five 将游戏状态视为由 20,000 个数字组成的数组,然后从中决定采取最佳行动。
要了解更多关于 OpenAI Five 的信息,并尝试互动演示,请访问 OpenAI 的官网:
除了游戏玩法,深度强化学习也为自动驾驶汽车做出了重要贡献。自动驾驶汽车通过计算机视觉算法来抽象周围的世界。这种抽象表示了车辆当前的状态。然后,深度强化学习根据车辆的状态选择最佳行动(例如,加速或刹车)。
神经网络的局限性
神经网络的潜力似乎是无限的,但实际上,神经网络和机器学习在一般情况下也有其局限性。
首先,神经网络的可解释性较差。换句话说,神经网络通常作为黑箱算法工作,难以解释神经网络产生的结果。以我们在第二章中的项目《使用多层感知机预测糖尿病》为例,我们使用神经网络预测患糖尿病风险的患者。神经网络输入数据,如血糖水平、血压、年龄等,并输出患者是否有糖尿病风险的预测。尽管神经网络能够高精度地做出预测,但我们实际上并不清楚哪些因素影响这些预测。这对于医生来说可能是不够的,因为他们可能希望为患者制定干预计划。
在实际应用中,这种缺乏可解释性的问题对商业用户来说是一个真实的担忧,因为他们可能不愿意使用黑箱算法。除了模型的性能之外,商业用户还希望了解模型的工作原理,以及哪些因素影响着与业务相关的目标变量。
提高神经网络可解释性是研究人员正在努力的领域之一。特别是,研究人员正在致力于在深度神经网络应用于计算机视觉问题时,生成可解释的结果。为此,一些研究人员提出将 CNN 的卷积层减少到图形模型,表示神经网络内部隐藏的语义层次结构。
神经网络的第二个局限性是,当应用于图像识别时,它们很容易被欺骗。在第四章,猫与狗—使用卷积神经网络进行图像分类中,我们在使用卷积神经网络(CNN)对猫和狗的图像进行分类时,达到了高精度(90%)。虽然 CNN 被认为是图像识别领域的最先进技术,但它们的致命弱点是容易受到恶意代理的欺骗。
近日,Nguyen 等人的一项研究表明,由于神经网络感知图像的方式与人类不同,一张人类完全无法识别的图像可以用来欺骗神经网络,从而导致神经网络做出错误预测。有关这些对人类无法识别,但可以用来欺骗神经网络的合成图像的示例,请参阅 Nguyen 等人的论文:
此外,研究人员表明,通过将这些合成图像以人类无法察觉的方式与现有图像结合,神经网络可以被欺骗,从而产生错误的预测。
这一发现对使用神经网络的计算机视觉安全系统的可行性产生了重大影响。恶意代理有可能向神经网络提供精心制作的输入图像,从而欺骗神经网络,绕过安全系统。
很显然,神经网络远非完美,它们绝不是解决我们所有问题的魔法方案。然而,仍然有理由保持乐观,因为每天都有新的突破不断被发现,这些突破提高了我们对神经网络的理解。
人工智能和机器学习的未来
接下来,让我们讨论一下人工智能和机器学习的未来。依我看,在未来几十年内,我们将看到以下几个关键发展的崛起:
-
人工通用智能
-
自动化机器学习
人工通用智能
人工通用智能(AGI)被定义为一种人工智能代理,具有执行任何人类能够完成的智力任务的能力。一些研究者区分了弱人工智能与强人工智能,其中弱人工智能用来描述当今的 AI 水平。现在的 AI 代理主要专注于执行单一任务。例如,我们训练 AI 代理预测患者是否有糖尿病风险,另一个 AI 代理则用来分类猫和狗的图像。这些 AI 代理是独立的,训练来执行某一特定任务的 AI 代理不能用来执行其他任务。这种狭隘的 AI 视角被称为弱人工智能。
另一方面,强人工智能指的是能够执行任何任务的通用 AI 代理。一个强人工智能代理可能是一个自我意识的、类人化的 AI 助手。目前,强人工智能还属于科幻领域。在我看来,目前我们掌握的机器学习算法(例如神经网络、决策树)不足以实现 AGI。正如弗朗索瓦·肖莱(Keras 的开发者)所说:
“仅仅通过扩大现有的深度学习技术,无法实现通用智能。”
- 弗朗索瓦·肖莱
要实现 AGI,需要一个重大的突破——类似于神经网络和深度学习曾经定义了我们今天所知的弱人工智能的那种突破。
自动化机器学习
尽管数据科学家被称为21 世纪最性感的职业,但现实情况是,大多数数据科学家花费了大量的时间在一些费时的任务上,如数据预处理和超参数调优。为了应对这个问题,像谷歌这样的公司正在开发工具来自动化机器学习过程。谷歌最近推出了AutoML,这是一种使用神经网络设计神经网络的解决方案。谷歌认为,AutoML 可以将目前数据科学家所拥有的专业知识进行打包,并通过云端按需提供这些专业知识作为服务。
当然,一些数据科学家对他们可能会被 AI 取代的想法感到不满,并声称自动化机器学习永远不可能成为现实。我的个人观点是,真相介于两者之间。如今,已经有一些 Python 库可以帮助我们自动化一些更为耗时的任务,比如超参数调优。这些库可以通过暴力搜索一系列超参数,选择最大化结果的一组超参数。甚至有一些 Python 库可以自动可视化数据集,自动绘制最相关的图表。随着这些库的逐步普及,我认为数据科学家将花费更少的时间在这些繁琐的活动上,更多的时间将用于其他有影响力的任务,如模型设计和特征工程。
跟上机器学习的步伐
机器学习和人工智能领域在不断发展,新的知识不断被发现。我们如何在这个不断变化的领域保持更新呢?就个人而言,我通过阅读书籍、科学期刊以及在真实数据集上进行实践来保持更新。
书籍
你正在阅读这本书,说明你已经致力于提升自己的知识!但遗憾的是,我们无法在本书中涵盖所有机器学习的每个话题。如果你喜欢本书,你可能会想参考 Packt 的图书目录。你会发现 Packt 在几乎每一个机器学习话题上都有书籍。Packt 团队还通过不断出版关于最新技术的书籍,确保读者能够与机器学习领域的最新发展保持同步。
Packt 的目录可以在 www.packtpub.com/all 找到。
科学期刊
人工智能研究人员一直相信开放共享。他们认为知识应该自由共享,社区成长的最佳方式就是通过分享。因此,大多数前沿的人工智能和机器学习科学论文都可以在网上免费找到。尤其是,许多 AI 研究人员会在以下网站分享他们的研究成果:
Arxiv 是一个开放获取的科学期刊存储库。大多数前沿研究成果一经发布,便会自由分享在 arxiv 上。这促使了快速的发展,思想不断在上一个基础上迭代。
在真实数据集上练习
最后,作为机器学习从业者,保持我们的技能锋利至关重要,定期练习是非常必要的。Kaggle 是一个举办数据科学竞赛的网站,使用真实的世界数据集。竞赛有不同的难度级别,初学者和专家都可以找到适合自己水平的内容。数据集的类型也各不相同,从表格数据到图像(计算机视觉问题),再到文本(自然语言处理问题)。
Kernels 可能是 Kaggle 上最有用的功能之一。通过 Kaggle Kernels,用户可以公开分享他们的代码和方法。这确保了结果的可重复性,通常你会学到一些自己之前不知道的技巧。Kaggle 还提供了一个免费的云环境来运行你的代码,包括 GPU 支持。如果你想挑战自己的技能,读完本书后,Kaggle 是一个很好的起点。
最喜爱的机器学习工具
在本书中,我使用了大量的 Python 和 Keras。除此之外,还有一些我认为很有用的机器学习工具:
-
Jupyter Notebook:Jupyter Notebook 是交互式笔记本,通常在机器学习项目的早期阶段使用。使用 Jupyter Notebook 的优势在于,它允许我们迭代地编写交互式代码。与
.py的 Python 文件不同,代码可以分块执行,输出(例如图表)可以与代码一起显示。 -
Google Colab:Google Colab 是一个免费的云平台,允许我们在云端编写 Jupyter Notebook 代码。所有的修改都会自动同步,团队成员可以在同一个笔记本上进行协作。Google Colab 的最大优势是,你可以在云端使用 GPU 实例运行代码,而这些实例是 Google 免费提供的!这意味着我们可以从世界任何地方高效地训练深度神经网络,即使我们没有强大的 GPU。
总结
在本章中,我们快速回顾了本书中涉及的所有不同类型的神经网络和关键概念。接着,我们探讨了一些神经网络的前沿发展,包括生成对抗网络和深度强化学习。尽管神经网络的潜力有时看起来无穷无尽,但我们必须记住,当前神经网络的状态也有其局限性。接下来,我们概述了机器学习和人工智能的整体发展,并展示了人工智能在不久的未来可能呈现的样貌。我们还给读者提供了一些关于如何跟上机器学习这一不断发展的领域的建议。
最后,我想通过提问来结束本书——你将创造什么?我们生活在一个高度先进的科技时代,信息可以自由获取。无论你现在处于什么水平,无论你是经验丰富的机器学习专家还是刚入门的初学者,你都拥有成功所需的所有资源。我鼓励你保持好奇心,始终渴望知识。许多这个领域的发现来自像你我一样的好奇者。我们每个人都能做出贡献。你将创造什么?


浙公网安备 33010602011771号