计算机视觉与创客技术-全-
计算机视觉与创客技术(全)
一、机器学习导论
机器学习被定义为通过机器执行一项任务的一套技术,而这项任务并不是明确为它编写的。它有时被视为动态编程的子集。如果你以前有过一些传统编程的经验,你就会知道构建一个软件需要明确地为机器提供一组明确的指令,这些指令可以顺序或并行执行,以完成某项任务。如果你的软件的目的是计算购买的佣金,或者向用户显示一个仪表板,或者向一个连接的设备读写数据,这就很好了。这些类型的问题通常涉及有限数量的明确定义的步骤,以便执行它们的任务。然而,如果你的软件的任务是识别一张图片是否包含一只猫呢?即使您构建了一个软件,能够在一些特定的样本图片上正确识别猫的形状(例如,通过检查样本图片中的一些特定像素是否到位),如果您为该软件提供不同的猫图片,或者甚至是您自己的样本图片的稍微编辑版本,该软件也可能无法执行其任务。如果你必须开发一个软件来检测垃圾邮件呢?当然,你可能仍然可以用传统的编程来做到这一点——例如,你可以建立一个垃圾邮件中常见的大量单词或短语的列表——但是如果你的软件提供了与你的列表中相似但不在你的列表中的单词,那么它可能会失败。
后一类包括传统上人类被认为比机器更擅长完成的任务:在执行有限的步骤序列甚至解决高等数学问题方面,机器比人快一百万倍,但它会可耻地失败(至少在传统编程中),无法分辨某张图片是描绘猫还是交通灯。在这些任务中,人类的大脑通常比机器更好,因为他们已经接触了很多年的例子和基于感觉的经验。我们可以在几分之一秒内判断出一张照片中是否有一只猫,即使没有对所有可能的品种、它们的特征和它们所有可能的姿势的完整经验。这是因为我们可能以前见过其他的猫,我们可以很快执行一个心理分类的过程,将图片的主题标记为我们过去已经见过的东西。换句话说,我们的大脑经过多年的训练,或连线,变得非常擅长识别模糊世界中的模式,而不是在虚拟世界中快速执行一系列复杂但确定的任务。
机器学习是一套试图模仿我们大脑执行任务的方式的技术——通过反复试验,直到我们可以从获得的经验中推断出模式,而不是通过明确的步骤声明。
有必要快速澄清一下机器学习和人工智能 (AI)之间的区别。虽然这两个术语今天经常被用作同义词,但机器学习是一套技术,通过接触(通常是许多)例子,可以指导机器解决它没有专门编程的问题。人工智能是一个更广泛的分类,包括任何擅长执行通常人类更擅长的任务的机器或算法,或者根据一些人的说法,显示某种形式的类人智能的任务。人工智能的实际定义实际上非常模糊——一些人可能会认为,能够检测到图片中的物体或两个城市之间的最短路径是否真的是一种“智能”——机器学习可能只是实现这一点的一种可能工具(例如,专家系统在 21 世纪初非常流行)。因此,通过这本书,我通常会谈论工具(机器学习算法),而不是这种算法可能要实现的哲学目标(人工智能)。
在我们深入研究机器学习的具体细节之前,可能有必要提供一些背景和历史,以了解该学科多年来是如何发展的,以及我们现在所处的位置。
1.1 历史
尽管机器学习在过去的十年里非常流行,但它的存在时间可能和数字计算机一样长。建造一台能够模仿人类行为和特征及其所有细微差别的机器的梦想比计算机科学本身还要古老。然而,在经历今天的爆炸之前,这门学科在过去的半个世纪里经历了一系列的起伏。
今天最流行的机器学习技术利用了唐纳德·赫布在 1949 年首次理论化的概念[1]。在他的书《行为的组织》中,他首次提出理论,认为人类大脑中的神经元通过加强或削弱它们的相互连接来对外部环境的刺激做出反应。赫布写道,“当一个细胞重复协助另一个细胞放电时,第一个细胞的轴突在与第二个细胞的胞体接触时,会发展出突触旋钮(或者扩大它们,如果它们已经存在的话)。”这种模型(fire together,wire together)激发了人们对如何构建一个人工神经元的研究,该人工神经元可以通过动态调整其与其他神经元的链接的权重(突触)来响应其收集的经验。这个概念是现代神经网络背后的理论基础。
一年后的 1950 年,著名的英国数学家(计算机科学之父)艾伦·图灵提出了可能是第一个已知的人工智能定义。他提出了一个实验,要求一个人与藏在屏幕后面的“某人/某物”进行对话。如果在对话结束时,受试者不能说出他/她是与人还是与机器交谈,那么机器就通过了“人工智能”测试。这种测试如今被称为著名的图灵测试。
1951 年,Christopher Strachey 编写了一个可以下跳棋的程序,Dietrich Prinz 编写了一个可以下国际象棋的程序。后来在 20 世纪 50 年代的改进导致了可以有效挑战业余选手的程序的发展。这样的早期发展导致游戏经常被用作衡量机器学习进展的标准基准——直到 IBM 的深蓝在国际象棋中击败卡斯帕罗夫,AlphaGo 在围棋中击败李·塞多尔。
与此同时,20 世纪 50 年代中期数字计算机的出现引领了一股乐观主义浪潮,这就是所谓的象征性人工智能。一些研究人员认识到,可以处理数字的机器也可以处理符号,如果符号是人类思维的基础,那么就有可能设计出会思考的机器。1955 年,艾伦·纽厄尔和未来的诺奖得主司马贺创造了逻辑理论家,一个可以通过推理证明数学定理的程序给出了一组逻辑公理。它成功地证明了伯特兰·罗素的数学原理的前 52 个定理中的 38 个。
这样的理论背景导致了早期研究者的热情。它引发了乐观情绪,并在 1956 年达特茅斯学院举行的研讨会上达到高潮,一些学者预测,与人类一样智能的机器将在一代人内出现,并获得数百万美元来实现这一愿景。今天,这个会议被认为是人工智能作为一门学科的基础。
1957 年,弗兰克·罗森布拉特设计了感知机。他应用 Hebb 的神经模型设计了一台可以进行图像识别的机器。该软件最初是为 IBM 704 设计的,安装在一台名为 Mark 1 感知器的定制机器上。它的主要目标是从照片中识别特征——尤其是面部特征。感知器在功能上就像一个单个神经元,可以从提供的例子中学习(即调整其突触权重),并对其从未见过的例子进行预测或猜测。感知器基础上的数学过程(逻辑回归)是神经网络的构建模块,我们将在本章稍后介绍。
尽管方向肯定是好的,但网络本身相对简单,1957 年的硬件肯定不能让今天的机器创造奇迹。每当你想知道 Raspberry Pi 是否是运行机器学习模型的正确选择时,请记住,你正在处理的机器几乎比 Frank Rosenblatt 用来训练第一个可以识别人脸的模型的机器强大一百万倍[4,5]。
感知器实验后的失望导致了对我们今天所知的机器学习领域的兴趣下降(直到上世纪 90 年代末,当改进的硬件开始显示出该理论的潜力时,该领域才再次兴起),而更多的注意力则放在了人工智能的其他分支上。在 20 世纪 60 年代和 70 年代,特别是出现了作为搜索的推理,这种方法将找到特定解决方案的问题基本上转化为在表示可用知识的连通图中搜索路径的问题。寻找两个单词的意思有多“接近”变成了在语义图中寻找两个相关节点之间的最短路径的问题。在一盘国际象棋中寻找最佳走法变成了在所有可能场景的图形中寻找具有最小成本或最大利润的路径的问题。证明一个定理是真还是假变成了从它的命题加上相关的公理构建一个决策树的问题,并找到一条可以导致真或假陈述的路径。这些领域的进展导致了令人印象深刻的早期成就,例如今天被认为是聊天机器人的第一个例子的 ELIZA。它于 1964 年至 1966 年间在麻省理工学院开发,用于模拟人类对话,它可能会欺骗用户(至少在最初的几次互动中),认为对方是人类。事实上,早期版本背后的算法相对简单,因为它只是重复或重新表述了用户提出的一些问题(对许多人来说,这给人一种与心理医生交谈的印象),但请记住,我们仍然在谈论第一个视频游戏诞生前的几年。这样的成就导致了当时对人工智能的极度乐观。这种早期乐观的几个例子:
-
1958 年:“十年内数字计算机将成为世界象棋冠军,数字计算机将发现并证明一个重要的新数学定理”[6]。
-
1965 年:“二十年内,机器将能做任何人能做的工作”。
-
1967 年:“在一代人的时间内,创造‘人工智能’的问题将得到实质性的解决”[8]。
-
1970 年:“在三到八年内,我们将拥有一台具有普通人一般智力的机器”[9]。
当然,事情并不完全是那样的。大约在 20 世纪 70 年代中期,大多数研究人员意识到他们肯定低估了这个问题。当然,主要问题在于当时的计算能力。到 20 世纪 60 年代末,研究人员意识到训练多层感知机网络比训练单个感知机效果更好,到 20 世纪 70 年代中期,反向传播(网络如何“学习”的构建模块)被理论化。换句话说,现代神经网络的基本形状在 20 世纪 70 年代中期就已经理论化了。然而,训练一个类似神经的模型需要大量的 CPU 能力来执行收敛到最优解所需的计算,而这样的硬件能力在接下来的 25-30 年内是不可用的。
与此同时,推理作为搜索方法面临组合爆炸问题。将决策过程转化为图搜索问题对于下棋、证明几何定理或寻找同义词来说是可以的,但更复杂的现实世界问题很容易导致庞大的图,因为它们的复杂性将随着输入的数量呈指数增长——这使得人工智能主要成为研究实验室内的玩具项目,而不是现实世界的应用。
最后,研究人员了解到了被称为莫拉维克悖论的东西:对于一台确定性机器来说,证明一个定理或解决一个几何问题确实很容易,但要执行更多“模糊”的任务就困难得多,比如识别人脸或在不撞到物体的情况下行走。当研究成果未能实现时,研究经费就耗尽了。
人工智能在 20 世纪 80 年代以专家系统的形式复兴。专家系统是一种软件,它应用从人类专家的知识中得出的推理规则,在特定的知识领域内回答问题或解释文本的内容。20 世纪 70 年代末引入的通过关系和基于图形的数据库对知识的正式表示导致了这场新的人工智能革命,这场革命专注于如何最好地表示人类知识以及如何从中推断决策。
专家系统经历了又一波乐观浪潮,随后又是一次崩溃。虽然它们在提供简单的特定领域问题的答案方面相对较好,但它们与人类专家提供的知识一样好。这使得它们的维护和更新非常昂贵,并且每当输入看起来与知识库中提供的略有不同时,就非常容易出错。它们在特定的环境中是有用的,但是它们不能被放大来解决更通用的问题。基于逻辑的人工智能的整个框架在 20 世纪 90 年代受到越来越多的批评。许多研究人员认为,真正智能的机器应该是自下而上而不是自上而下设计的。如果机器不知道雨和伞的实际含义或样子,它就不能对这些东西或概念做出逻辑推理——换句话说,如果它不能基于直觉和获得的经验进行某种形式的类似人类的分类。
这种思考逐渐导致了对人工智能机器学习方面的新兴趣,而不是基于知识或符号的方法。此外,20 世纪 90 年代末的硬件比 20 世纪 60 年代麻省理工学院研究人员可用的硬件好得多,在当时被证明具有难以置信的挑战性的简单计算机视觉任务,如识别手写字母或检测简单物体,可以通过摩尔定律得到解决,该定律指出,芯片中晶体管的数量大约每 18 个月翻一番。感谢网络和这些年来它提供的大量数据,以及分享这些数据的日益便利。
今天,神经网络是机器学习和人工智能中普遍存在的组件。然而,需要注意的是,在某些情况下,其他方法可能仍然适用。在你家和办公室之间骑自行车寻找最快的路径在很大程度上仍然是一个图形搜索问题。非结构化文本中的意图检测仍然依赖于语言模型。其他问题,如一些游戏或真实世界的模拟,可能会使用遗传算法。一些特定的领域仍然可以利用专家系统。然而,即使采用了其他算法,现在的神经网络也常常是连接所有组件的“粘合剂”。如今,不同的算法或网络通常是通过数据管道连接在一起的模块化模块。
在过去的十年里,深度学习已经变得越来越受欢迎,因为更好的硬件和更多的数据使得将更多的数据训练到更大的网络和更多的层成为可能。深度学习,在引擎盖下,是通过添加更多的神经元和更多的层来解决早期网络的一些常见问题(如过度拟合)的过程。通常,当你增加网络的层数和节点数时,网络的准确性也会增加,因为网络更善于发现非线性问题中的模式。然而,深度学习也可能受到一些问题的困扰。其中之一是消失渐变问题,当渐变穿过越来越多的层时,渐变会慢慢缩小。另一个更具体的问题与其环境影响有关:尽管向网络中投入更多的数据和更多的神经元以及运行更多的训练迭代似乎可以使网络更加准确,但这也是一个非常耗电的解决方案,无法持续长期增长。
1.2 监督和非监督学习
既然人工智能和机器学习之间的区别已经很明显,并且我们已经了解了我们是如何走到现在这一步的,那么让我们将注意力转移到机器学习和两个主要的“学习”类别上:有监督的和无监督的学习:
-
我们将监督学习定义为一组算法,其中模型在数据集上训练,该数据集包括示例输入数据和相关的预期输出。
-
另一方面,在无监督学习中,我们在不包括预期输出的数据集上训练模型。在这些算法中,我们期望模型自己“找出”模式。
当谈到监督学习时,训练模型通常包括计算函数,该函数将给定的输入向量
映射到输出向量
,使得预测值和期望值之间的平均误差最小化。监督学习算法的一些应用包括
-
给定一个包含一百万张猫的图片和一百万张没有猫的图片的训练集,建立一个在以前看不到的图片中识别猫的模型。在这种情况下,训练集通常会包含一个真/假标签来判断图片中是否包含一只猫。这通常被认为是一个分类问题——也就是说,给定一些输入值和它们的标签(统称为训练集,你希望你的模型预测正确的类,或者标签——例如,“包含/不包含一只猫。”或者,如果您为模型提供许多被标记为垃圾邮件/非垃圾邮件的电子邮件示例,您可以训练您的分类器来检测以前未见过的电子邮件中的垃圾邮件。
-
给定包含城市中大量公寓的特征(大小、位置、建造年份等)的训练集。)连同它们的价格,建立一个可以预测市场上新公寓价值的模型。这通常被认为是一个回归问题——也就是说,给定一个训练集,您想要训练一个模型来预测新提供的输入的最佳数值近似值,例如,美元、米或千克。
另一方面,无监督学习通常用于解决问题,其目标是找到数据中的潜在结构、分布或模式。由于没有预期的标签,这些类型的问题既没有确切/正确的答案,也无法与预期值进行比较。无监督学习问题的一些例子包括
-
给定电子商务网站上具有相关输入特征(年龄、性别、地址、过去一年的购买清单等)的大量客户列表。),找到细分客户群的最佳方式,以便策划几个广告活动。这通常被认为是一个聚类问题——也就是说,给定一组输入,找到将它们分组在一起的最佳方式。
-
给定音乐流平台上的用户及其相关特征(年龄、性别、上个月收听的曲目列表等。),构建一个可以推荐音乐品味相近的用户简档的模型。这通常被认为是一个推荐系统,或者关联问题——也就是说,一个寻找特定节点最近邻居的模型。
最后,在监督学习和非监督学习之间可能存在一些问题。想象一个大型生产数据库,其中一些图像被标记(例如,“猫”、“狗”等)。)但有些则不然——例如,因为雇佣足够多的人来手动标记所有记录的成本很高,或者因为问题非常大,很难提供所有可能标签的全貌。在这种情况下,您可能希望依靠混合实现,使用监督学习从可用的标记数据中学习,并利用非监督学习在未标记的数据中查找模式。
在本书的其余部分,我们将主要关注监督学习,因为这一类别包括当今使用的大多数神经架构,以及所有的回归问题。然而,一些流行的无监督学习算法值得一提,因为它们可能经常与监督算法共生使用。
1.3 准备工具
在谈论了这么多机器学习之后,让我们来介绍一下我们将在旅程中使用的工具。在本章中,你还不需要树莓派:当我们讨论用于机器学习的算法和软件工具时,你自己的笔记本电脑将会完成这项工作。
在接下来的几节中,我将假设您对以下方面有一些知识/经验
-
任何编程语言(如果有 Python 的经验就更好了)。在过去的几年中,Python 已经成为机器学习最流行的选择,但是即使你没有太多的经验,也不要担心——它相对简单,我会尽可能多地对代码进行注释。
-
高中(或更高)水平的数学。如果你熟悉微积分、统计学和线性代数,那就更好了。如果没有,也不用担心。虽然需要一些微积分和线性代数概念来掌握机器学习如何在引擎盖下工作,但我会尽量不要过多地挖掘理论,每当我提到梯度、张量或回忆时,我都会确保更多地关注它们的直观含义,而不是仅仅关注它们的形式定义。
软件工具
我们将在旅程中使用以下软件工具:
-
Python 编程语言(3.6 或更高版本)。
-
TensorFlow ,可能是当今最流行的构建、训练和查询机器学习模型的框架。
-
Keras ,一个非常受欢迎的神经网络和回归模型库,可以轻松集成到 TensorFlow 之上。
-
numpy 和 pandas ,最常用的 Python 库,分别用于数值操作和数据分析。
-
matplotlib ,一个用于绘制数据和图像的 Python 库。
-
seaborn ,一个经常用于统计数据可视化的 Python 库。
-
jupyter ,这是一个非常流行的 Python 解决方案,用于通过笔记本进行原型开发(对于 Python 中的数据科学来说,这基本上是一个事实上的标准)。
-
git ,我们将使用它从 GitHub 下载样本数据集和笔记本。
1.3.2 设置环境
-
下载 git 并安装到您的系统上,如果它还不可用的话。
-
从
https://python.org/downloads
下载并安装最新版本的 Python,如果你的系统上还没有的话。请确保您使用的 Python 版本高于 3(不推荐使用 Python 2)。打开一个终端,检查python
和pip
命令是否存在。 -
(可选)创建新的 Python 虚拟环境。虚拟环境允许您将机器学习设置与主 Python 安装分开,而不会干扰任何系统范围的依赖关系,并且它还允许您在非特权用户空间中安装 Python 包。如果您希望在系统范围内安装依赖项,可以跳过这一步(尽管您可能需要 root/administrator 权限)。
-
安装依赖项(根据您的连接和 CPU 能力,可能需要一段时间):
# Create a virtual environment under your home folder
python -m venv $HOME/venv
# Activate the environment
cd $HOME/venv
source bin/activate
# Whenever you are done, run 'deactivate'
# to go back to your standard environment
deactivate
-
下载“mlbook-code”存储库,其中包含我们将在本书中用到的一些数据集和代码片段。我们称
<REPO_DIR>
为您克隆存储库的目录。 -
启动 Jupyter 服务器:
pip install tensorflow
pip install keras
pip install numpy
pip install pandas
pip install matplotlib
pip install jupyter
pip install seaborn
-
在浏览器中打开
http://localhost:8888
。您应该会看到如图 1-1 所示的登录屏幕。您可以决定是使用令牌进行鉴定还是设置密码。 -
选择您想要存储笔记本的文件夹(从现在开始,我们将其标识为
<NOTEBOOK_DIR>
),并创建您的第一个笔记本。Jupyter 笔记本是单元格列表;每个单元格都可以包含 Python 代码、markdown 元素、图像等等。开始熟悉环境,尝试运行一些 Python 命令,确保一切正常。
jupyter notebook
git clone https://github.com/BlackLight/mlbook-code
现在所有的工具都准备好了,让我们动手做一些算法。
图 1-2
作为面积函数的房价分布
图 1-1
http://localhost:8888
处的 Jupyter 登录屏幕
1.4 线性回归
线性回归是我们旅途中遇到的第一个机器学习算法,也是最重要的。正如我们将看到的,大多数监督机器学习算法是线性回归的变体或线性回归在规模上的应用。线性回归适用于解决这样的问题:您有一些由 n 维表示的输入数据,并且您希望从这些数据的分布中学习,以便对未来的数据点进行预测,例如,在给定一系列历史数据的情况下,预测某个社区的房屋或股票的价格。线性回归也被广泛用作金融、物流和经济学中的统计工具,以预测商品的价格、特定时期的需求或其他宏观经济变量。它是其他类型回归的构建模块,如逻辑回归,而逻辑回归又是神经网络的构建模块。
如果输入数据由单个变量( n = 1)表示,则回归模型称为简单回归模型;如果回归模型对由多个维度定义的输入数据进行操作,则称为多元回归。如果它的输出是一条最接近输入数据的线,它被称为线性回归。也存在其他类型的回归——例如,如果您试图通过数据拟合抛物线而不是直线,您将得到一个二次回归,如果您试图通过数据拟合三次多项式曲线,您将得到一个三次回归,等等。我们将从简单的线性回归开始,并将其基本机制扩展到其他回归问题。
1.4.1 加载和绘制数据集
首先,在您的<NOTEBOOK_DIR>
下创建一个新的 Jupyter 笔记本并加载<REPO_DIR>/datasets/house-size-price-1.csv
。这是一个 CSV 文件,其中包含某个城市的房价(以千美元计)与房价大小(以平方米计)的函数关系。现在让我们假设大小是我们得到的唯一参数,我们希望创建一个基于此数据训练的模型,该模型可以预测给定大小的新房子的价格。
在开始定义任何机器学习模型之前,你应该做的第一件事是可视化你的数据,并试图了解什么是最好的模型。这就是我们如何使用pandas
加载 CSV 并使用matplotlib
可视化它:
import pandas as pd
import matplotlib.pyplot as plt
# Download the CSV file from GitHub
csv_url = 'https://raw.githubusercontent.com/' +
'BlackLight/mlbook-code/master/' +
'datasets/house-size-price-1.csv'
data = pd.read_csv(csv_url)
# The first column contains the size in m2
size = data[columns[0]]
# The second column contains the price in thousands of dollars
price = data[columns[1]]
# Create a new figure and plot the data
fig = plt.figure()
plot = fig.add_subplot()
plot.set_xlabel(columns[0])
plot.set_ylabel(columns[1])
points = plot.scatter(size, price)
运行完单元格的内容后,您应该会看到如图 1-2 所示的图表。你会注意到数据有点分散,但如果我们能拟合一条直线通过它,它仍然可以很好地近似。我们现在的目标是找到最接近我们数据的直线。
1.4.2 回归背后的思想
让我们暂时把笔记本放在一边,试着想想这样一个功能应该有什么特点。第一,必须是一条线;因此,它必须有这样的形式:
(1.1)
在等式 1.1 中, x 表示输入数据,不包括输出标签。在房子大小-价格模型的情况下,我们有一个输入变量(大小)和一个输出变量(价格);因此,输入和输出向量都具有单一的大小,但是其他模型可以具有多个输入变量和/或多个输出变量。 θ 0 和 θ 1 是该行的数值系数。特别是, θ 0 告诉我们直线与 y 轴的交叉点,而 θ 1 告诉我们直线的方向以及它有多“陡”——它通常被称为直线的斜率。hθ(x)是我们的模型将用于基于向量的值进行预测的函数,也称为模型的权重。在我们的问题中,输入
和期望输出
是通过训练集提供的;因此,线性回归问题是在前面的方程中寻找
参数的问题,使得hθ(x)是 x 和 y 之间线性相关性的良好近似。hθ(x)常被表示为假设函数,或简称为模型。阶为 n 的单变量模型的通用公式(线性为 n = 1,二次为 n = 2,以此类推。)将
(1.2)
另外,请注意,本书中符号顶部的“条”或“上标”将表示一个向量。vector 是一个固定大小的数值元素列表,所以实际上是一种更简洁的写法[ * θ * 0 , θ 1 或者[θ0……θn]。
那么如何才能将“足够好的线性逼近”这一直观概念形式化为算法呢?直觉是选择参数,使得它们相关的hθ(x)函数对于给定的 x 值“足够接近”所提供的样本 y 。更正式地说,对于训练集中提供的所有 m 个数据点,我们希望最小化采样值 y 和预测值hθ(x)之间的均方误差——如果预测值和实际值之间的误差较低,则模型表现良好:
(1.3)
让我们将前面公式的论点重新表述为参数 :
(1.4)的函数
函数 J 也被称为成本函数(或损失函数),因为它表达了与参数的特定选择相关联的线路的成本(或,在这种情况下,近似误差)。因此,为您的数据寻找最佳线性近似就是寻找使前面的成本函数(即样本和预测之间的均方误差之和)最小化的
值的问题。注意hθ(x)和
的区别:前者是我们的模型假设,即以其参数
的向量表示的、以 x 为变量的模型的预测函数。
是成本函数,参数
是变量,我们的目标是找到最小化该函数的
的值,以便计算最佳的hθ(x)。如果我们想要开始形式化该过程,我们可以说找到最佳回归模型的问题可以表达如下:
-
从一组初始参数
开始,用 n 作为你回归的阶数( n = 1 表示线性, n = 2 表示二次,以此类推。).
-
使用这些值来公式化假设hθ(x)如等式 1.2 所示。
-
如等式 1.4 所示,计算与该假设相关的成本函数
。
-
不断改变
的值,直到我们收敛到最小化成本函数的点。
既然已经清楚了回归算法是如何建模的,以及如何衡量它对数据的逼近程度,那么让我们来看看如何实现前面列表中的最后一点,即实际的“学习”阶段。
梯度下降
如果你对微积分有一些记忆的话,希望这么多提到的“误差最小化”已经敲响了警钟!微分(或导数)是大多数最小化/最大化问题中的数学工具。特别是:
- 函数的一阶导数告诉我们该函数是在某个点附近增加还是减少,或者它是“静止的”(即该点是局部最小值/最大值或拐点)。几何上可以形象化为切线对函数在某一点的斜率。如果我们命名f’(x)一个函数的一阶导数 f ( x )(暂且用
),那么它在一个点上的值 x 0 就会是(假设函数在 x 0 中可微)
(1.5)
- 函数的二阶导数告诉我们该函数在某一点周围的“凹度”,即曲线在该点周围是面向“上”还是“下”。围绕给定点x0 的二阶导数f(x)将为
(1.6)
通过结合这两种工具,我们可以找出,给定曲面上的某一点,该函数的最小值在哪个方向。换句话说,最小化成本函数是寻找值
的向量的问题,使得 J 对
的一阶导数为零(或者足够接近零),并且其二阶导数为正(即,该点是局部最小值)。在本书的大多数算法中,我们实际上不需要二阶导数(当今许多流行的机器学习模型都是围绕凸成本函数构建的,即具有一个单点最小值的成本函数),但具有更高多项式模型的应用程序可能会利用二阶导数背后的凹度来判断一个点是最小值、最大值还是拐点。
这种直觉适用于单变量的情况。但是, J 是一个参数向量的函数,线性回归的长度为 2,二次的长度为 3,以此类推。对于多元函数,使用梯度的概念,而不是用于一元函数的导数。多元函数的梯度(通常用符号𝛻表示)是针对每个变量计算的该函数的偏导数(通常用 ∂ 符号表示)的向量。在我们的成本函数的例子中,它的梯度将是
(1.7)
在实践中,偏导数是计算多元函数导数的过程,假设它的变量中只有一个是实际变量,其他都是常数。
图 1-3
线性回归模型的成本函数的典型形状:三维空间中的抛物面
梯度向量表示一条 n 维曲线围绕某一点增加的方向以及增加的“速度”。在具有一个输入变量的线性回归的情况下,我们已经在方程 1.4 中看到,成本函数是两个变量的函数( θ 0 和 θ 1 ),并且是二次函数。事实上,通过组合方程 1.1 和 1.4 ,我们可以导出单变量输入的线性回归的成本函数:
(1.8)
这个表面可以像抛物面一样在 3D 空间中表示(如果你将抛物线绕其轴旋转 360 度,就会得到一个碗的形状,如图 1-3 所示)。这让我们处于一个相对幸运的位置。就像抛物线一样,抛物面只有一个最小值——也就是说,只有一个点的矢量梯度为零,而这个点也恰好是全局最小值。这意味着,如果我们从的一些随机值开始,然后开始在该点的梯度的相反方向上“行走”,我们应该最终在最优点结束——也就是说,我们可以插入我们的假设函数hθ(x)的参数
的向量,以获得良好的预测。记住,给定曲面上的一个点,梯度向量会告诉你函数“向上”的方向如果你朝相反的方向走,你就会下去。如果你的表面是碗的形状,如果你从表面的任何地方继续向下,你最终会到达底部。如果你使用更复杂的回归类型(二次、三次等)。),您可能并不总是那么幸运——您可能会陷入局部最小值,这并不代表函数的整体最小值。因此,假设我们一开始向最小值收敛就很高兴,那么梯度下降算法可以被表达为这样一个过程,在该过程中,我们首先为
选取随机值,然后在每一步 k 中,我们通过下面的公式更新这些值:
(1.9)
或者,以向量形式:
(1.10)
α 是一个介于零和一之间的参数,被称为机器学习模型的学习速率,它表达了该模型从新数据中学习的速度——换句话说,沿着梯度向量的方向应该“跳跃”多大。较大的值 α 将导致模型在开始时可以快速学习,但可能会超过最小值或“错过停止点”——在某个点上,它可能会在成本函数的最小值附近着陆,并可能会因采取更长的步骤而错过它,从而导致在收敛之前来回“反弹”。另一方面,较小的值 α 可能在开始时学习速度较慢,可能需要更多的迭代,但基本上可以保证在最小值附近不会有太多的反弹。
打个比喻,通过梯度下降进行学习就像蒙着眼睛把球推下山谷。首先,你必须用梯度向量的方向作为指南针,找出谷底的方向。然后,你要找出你要对球施加多大的力才能让它触底。如果你用很大的力推它,它可能会更快地到达底部,但它可能会来回几次才能沉淀下来。相反,如果你只让重力发挥作用,球可能需要更长时间才能到达底部,但一旦到达底部,它就不太可能摆动太多。在当今的大多数应用程序中,学习率是动态的:在第一阶段(当您的模型还不太了解数据时),您可能通常想要更高的值 α ,并在接近结束时(当您的模型已经看到大量数据点和/或成本函数正在收敛时)降低它。现在一些流行的算法也执行某种学习率洗牌以防卡住。
我们可能还想为算法设置一个退出条件:例如,如果它没有收敛到使成本函数无效的参数向量上,如果成本函数的梯度足够接近零,或者从上一步没有显著的改进,或者相应的模型已经足够好地逼近我们的问题,我们可能仍然想退出。
通过组合方程 1.8 和 1.9 并应用微分规则,我们可以导出线性回归情况下θ0 和θ1 的精确更新步骤:
(1.11)
(1.12)
因此,线性回归的完整算法可以总结如下:
-
为 θ 0 和 θ 1 选取随机值。
-
分别通过方程 1.11 和 1.12 保持更新θ?? 0 和θ1。
-
在执行预设数量的训练迭代后(通常称为时期)或达到收敛时(即,与前一步骤相比,某一步骤未测量到显著改善)终止。
有许多工具和库可以执行有效的回归,所以您不得不从头开始实现算法是不常见的。然而,由于前面的步骤是已知的,所以用 Python 编写一个仅含numpy
的单变量线性回归算法相对简单:
import numpy as np
def gradient_descent(x, y, theta, alpha):
m = len(x)
new_theta = np.zeros(2)
# Perform the gradient descent on theta
for i in range(m):
new_theta[0] += theta[0] + theta[1]*x[i] - y[i]
new_theta[1] += (theta[0] + theta[1]*x[i] - y[i]) * x[i]
return theta - (alpha/m) * new_theta
def train(x, y, steps, alpha=0.001):
# Initialize theta randomly
theta = np.random.randint(low=0, high=10, size=2)
# Perform the gradient descent <steps> times
for i in range(steps):
theta = gradient_descent(x, y, theta, alpha)
# Return the linear function associated to theta
return lambda x: theta[0] + theta[1] * x
输入标准化
让我们从离开的地方捡起我们的笔记本。我们现在应该有一个清晰的想法,如何训练一个回归模型来预测给定大小的房子的价格。我们将使用 TensorFlow+Keras 来定义和训练模型。
然而,在继续之前,重要的是花点时间了解一下输入规范化(或特性缩放)。非常重要的一点是,即使某些数据以不同于训练集中使用的单位提供,或者某些任意常数被添加或乘以您的输入,您的模型也要足够稳健。这就是为什么在将输入输入到任何机器学习模型之前,对其进行标准化非常重要。不仅如此,如果输入在以原点为中心的特定范围内分布良好,模型的收敛速度将比提供没有特定范围的原始输入快得多。更糟糕的是,非标准化的训练集通常会导致成本函数根本不收敛。
输入规范化通常通过应用以下变换来完成:
(1.13)
其中xI是你的 m -long 输入向量的第 i 个元素, μ 是输入的向量的算术平均值, σ 是其标准差:
(1.14)
(1.15)
通过将等式 1.13 应用于我们的输入,我们基本上在零点附近转置输入值,并在σ、 σ 范围内对大部分输入值进行分组。在预测值时,我们反而希望对提供的输出进行反规格化,这可以通过从等式 1.13:
![$$ {x}_i=\sigma {\hat{x}}_i+\mu $$
(1.16)推导出 x i 来轻松实现
用 Python 编写这些函数并把它们插入到我们的笔记本中是相当容易的。首先,使用pandas describe
方法获取数据集统计信息:
dataset_stats = data.describe()
然后定义规范化和反规范化数据的函数:
def normalize(x, stats):
return (x - stats['mean']) / stats['std']
def denormalize(x, stats):
return stats['std'] * x + stats['mean']
norm_size = normalize(size, dataset_stats['size'])
norm_price = normalize(price, dataset_stats['price'])
1.4.5 定义和训练模型
使用 TensorFlow+Keras 定义和训练线性回归模型非常简单:
from tensorflow.keras.experimental import LinearModel
model = LinearModel(units=1, activation="linear", dtype="float32")
model.compile(optimizer='rmsprop', loss="mse", metrics=['mse'])
history = model.fit(norm_size, norm_price, epochs=1000, verbose=0)
这里发生了很多事情:
-
首先,我们用一个输入变量(
units=1
)、linear
激活函数(即模型的输出值不经过非线性输出函数的转换直接返回)和float32
数值类型定义一个LinearModel
。 -
然后,我们
compile
模型,让它准备好接受训练。Keras 中的optimizer
做很多事情。对优化器的深入理解需要一个专门的章节,但是为了保持简洁,我们将快速介绍我们使用它们时它们做了什么。rmsprop
初始化学习率,并根据最近的梯度在训练迭代中逐渐调整学习率[10]。默认情况下,rmsprop
用learning_rate=0.001
初始化。但是,您可以尝试调整它,看看它如何/是否会影响您的模型: -
其他常见的优化器包括
-
SGD
或随机梯度下降,它实现了等式 1.9 中描述的梯度下降算法,并进行一些优化和调整,如学习率衰减和内斯特罗夫动量[11] -
adam
,一种基于一阶梯度优化算法,最近获得了相当大的发展势头,特别是在深度学习中[12] -
nadam
,它在adam
算法之上实现了对内斯特罗夫动量的支持【13】
-
-
随意试验不同的优化器和不同的学习率,看看它如何影响你的模型的性能。
-
loss
参数定义了要优化的损失/成本函数。mse
是指均方误差,我们已经在等式 1.4 中定义了它。其他常见的损失函数包括-
mae
,或表示绝对误差—类似于mse
,但它使用的是h(xI)yI的绝对值,而不是等式 1.4 中的平方值。 -
mape
或表示绝对百分比误差——类似于mae
,但它使用与前一次迭代相比的绝对误差百分比作为目标度量。 -
mean_squared_logarithmic_error
—与mse
类似,但它使用均方误差的对数(如果您的曲线具有指数特征,这很有用)。 -
几种交叉熵损失函数(如
categorical_crossentropy
、sparse_categorical_crossentropy
、binary_crossentropy
,常用于分类问题。
-
-
metrics
属性是一个列表,它标识了用于评估模型性能的指标。在本例中,我们使用与损失/成本函数相同的指标(均方误差),但也可以使用其他指标。度量在概念上类似于损失/成本函数,只是度量函数的结果仅用于评估模型,而不是训练模型。如果希望根据多个特征评估模型,也可以使用多个度量。其他常见指标包括-
mae
或表示绝对误差。 -
accuracy
及其派生的度量标准(binary_accuracy
、categorical_accuracy
、sparse_categorical_accuracy
、top_k_categorical_accuracy
等)。).准确性通常用于分类问题,它表示训练集中正确标记的项目占训练集中项目总数的比例。 -
还可以定义自定义指标。正如我们稍后处理分类问题时将会看到的,精确度、召回率和 F1 分数是非常流行的评估指标。这些还不是核心框架的一部分(还不是?),但它们很容易被定义。
-
-
最后,我们使用
fit
函数根据上一步得到的标准化数据训练我们的模型。该函数的第一个参数将是输入值的向量,第二个参数将是预期输出值的向量,然后我们指定历元的数量,即我们希望对该数据执行的训练迭代。
from tensorflow.keras.optimizers import RMSprop
rmsprop = RMSprop(learning_rate=0.005)
model.compile(optimizer=rmsprop, loss="mse", metrics=['mse'])
历元值在很大程度上取决于您的数据集。您将呈现给模型的输入样本的累积数量由×个时期给出,其中 m 是数据集的大小。在我们的例子中,我们有一个相对较小的数据集,这意味着您想要运行更多的训练时期,以确保您的模型已经看到了足够的数据。但是,如果您有较大的训练集,您可能希望运行较少的训练迭代。在同一批数据上运行太多训练迭代的风险,正如我们将在后面看到的,是过度拟合你的数据,也就是说,创建一个模型,如果提供的值足够接近它已经被训练过的值,那么它将紧密模拟预期的输出,但在没有被训练过的数据点上不准确。
1.4.6 评估模型
现在,我们已经定义并训练了我们的模型,并对如何衡量其性能有了清晰的想法,让我们看看它的主要指标(均方差)在训练阶段是如何改进的:
epochs = history.epoch
loss = history.history['loss']
fig = plt.figure()
plot = fig.add_subplot()
plot.set_xlabel('epoch')
plot.set_ylabel('loss')
plot.plot(epochs, loss)
您应该会看到类似于图 1-4 所示的图。你会注意到损耗曲线急剧下降,这很好;这意味着当我们输入数据点时,我们的模型实际上是在学习,而不会陷入梯度“山谷”这也意味着学习率得到了很好的校准——如果你的学习率太高,模型不一定会收敛,如果太低,那么在许多训练时期之后,它可能仍在朝着收敛的方向前进。还有,0.07 左右有一个短尾。这也是好的:这意味着我们的模型在最近的迭代中已经收敛了。
图 1-4
线性回归模型损失随训练的演变
如果尾部太长,这意味着您已经为模型训练了太多的时期,您可能希望减少时期的数量或训练集的大小,以防止过度拟合。如果尾部太短或根本没有尾部,则您的模型没有在足够的数据点上进行训练,您可能需要增加训练集的大小或时期的数量来获得准确性。
您还可以(read: should)在一些不一定属于您的训练集的数据上评估您的模型,以检查模型在新数据点上的表现如何。稍后,我们将深入探讨如何拆分您的训练集和测试集。现在,给定相对较小的数据集,我们可以通过evaluate
函数在数据集本身上评估模型:
model.evaluate(norm_size, norm_price)
您应该会看到如下输出:
0s 2ms/step - loss: 0.0733 - mse: 0.0733
[0.0733143612742424, 0.0733143612742424]
返回的向量分别包含损失函数和度量函数的值——在我们的例子中,因为我们对两者都使用了mse
,所以它们是相同的。
最后,让我们看看线性模型相对于数据集的实际情况,并开始使用它进行预测。首先,定义一个predict
函数
-
将一个房屋列表
sizes
作为输入,并根据您的训练集的平均值和标准偏差对其进行标准化 -
查询线性模型以获得预测价格
-
使用训练集的平均值和标准差对输出进行反规范化,以转换以千美元为单位的价格
图 1-5
根据数据集绘制线性模型
def predict_prices(*x):
x = normalize(x, dataset_stats['size'])
return denormalize(model.predict(x), dataset_stats['price'])
让我们用这个函数来预测一下房价:
predict_prices(90)
array([[202.53752]], dtype=float32)
“predict”函数将返回与您的输入相关联的输出列表,其中每一项都是包含预测值的向量(在本例中,是单位大小的向量,因为我们的模型只有一个输出单元)。如果你回头看图 1-2 ,你会发现尺码 = 90 的预测价格 202.53752 实际上看起来与数据的分布并没有那么远——这很好。为了直观显示我们的线性模型相对于数据集的外观,让我们再次绘制数据集,并计算模型上的两个点以绘制直线:
# Draw the linear regression model as a line between the first and
# the last element of the numeric series. x will contain the lowest
# and highest size (assumption: the series is ordered) and y will
# contain the price predictions for those inputs.
x = [size[0], size.iat[-1]]
y = [p[0] for p in predict_prices(*x)]
# Create a new figure and plot both the input data points and the
# linear model approximation.
fig = plt.figure()
data_points = fig.add_subplot()
data_points.scatter(size, price)
data_points.set_xlabel(columns[0])
data_points.set_ylabel(columns[1])
model_line = fig.add_subplot()
model_line.plot(x, y, 'r')
前面代码的输出有望如图 1-5 所示。这告诉我们,模型计算出的线实际上与我们的数据并没有那么远。如果您碰巧看到您的线远离模型,这可能意味着您没有在足够的数据上训练您的模型,或者学习率太高/太低,或者您的数据集中的指标之间没有很强的线性相关性,可能您需要更高的多项式模型,或者可能您的模型中有许多“异常值”-即,主分布之外的许多数据点将线“拖”出来。
1.4.7 保存和加载您的模型
您的模型现在已加载到笔记本的内存中,但是一旦 Jupyter 笔记本停止运行,您将会丢失它。您可能希望将它保存到文件系统中,这样以后就可以恢复它,而不必再次经历训练阶段。或者您可以将它包含在另一个脚本中进行预测。幸运的是,在文件系统上保存和加载 Keras 模型非常容易——然而,下面的例子将假设您使用的是 TensorFlow \geq 2.0
的一个版本:
def model_save(model_dir, overwrite=True):
import json
import os
os.makedirs(model_dir, exist_ok=True)
# The TensorFlow model save won't keep track of the labels of
# your model. It's usually a good practice to store them in a
# separate JSON file.
labels_file = os.path.join(model_dir, 'labels.json')
with open(labels_file, 'w') as f:
f.write(json.dumps(list(columns)))
# Also, you may want to keep track of the x and y mean and
# standard deviation to correctly normalize/denormalize your
# data before/after feeding it to the model.
stats = [
dict(dataset_stats['size']),
dict(dataset_stats['price']),
]
stats_file = os.path.join(model_dir, 'stats.json')
with open(stats_file, 'w') as f:
f.write(json.dumps(stats))
# Then, save the TensorFlow model using the save primitive
model.save(model_dir, overwrite=overwrite)
然后,您可以将该模型加载到另一个笔记本、脚本、应用程序等中(TensorFlow 的绑定可用于当今使用的大多数编程语言),并将其用于您的预测:
def model_load(model_dir):
import json
import os
from tensorflow.keras.models import load_model
labels = []
labels_file = os.path.join(model_dir, 'labels.json')
if os.path.isfile(labels_file):
with open(labels_file) as f:
labels = json.load(f)
stats = []
stats_file = os.path.join(model_dir, 'stats.json')
if os.path.isfile(stats_file):
with open(stats_file) as f:
stats = json.load(f)
m = load_model(model_dir)
return m, stats, labels
model, stats, labels = model_load(model_dir)
price = predict_prices(90)
1.5 多元线性回归
到目前为止,我们已经探索了具有单个输入和输出变量的线性回归模型。现实世界的回归问题通常更复杂,输出特征通常表示为多个变量的函数。例如,一栋房子的价格不仅取决于它的大小,还取决于它的建造年份、卧室数量、是否有花园或露台等额外设施、离市中心的距离等等。在这种一般情况下,我们将每个输入数据点表示为一个向量,等式 1.2 中看到的回归表达式被重新表示为
(1.17)
按照惯例,多元回归情况下的输入向量重写为,其中x0= 1,因此前面的表达式可以更简洁地写成
(1.18)
或者,通过使用向量符号,假设函数可以写成参数向量和特征向量
:
(1.19)之间的标量积
请记住,按照惯例,向量被表示为值的列。 T 符号表示转置的向量,因此向量表示为一行。将一个行向量乘以一个列向量,就得到两个向量的标量积,所以是将
的标量积表示成
的一种简洁方式。
因此,在多变量情况下,等式 1.8 中的均方误差成本函数可以重写为
(1.20)
或者,用矢量符号:
(1.21)
由于输入不再是单一的,我们不再谈论平面上的线,而是一个 n 维空间中的超曲面——它们是由一个变量定义的 2D 空间中的 1D 线,由两个变量定义的 3D 空间中的 2D 平面,由三个变量定义的 4D 空间中的 3D 空间,等等。成本函数本身将具有参数-虽然在单变量情况下它是 3D 空间中的抛物面,但在 n 输入要素的情况下,它将是 n + 1 维表面。这使得多变量情况比单变量情况更难可视化,但我们仍然可以依靠我们的性能指标来评估模型的表现,或者按特征分解线性 n 维表面,以分析每个变量相对于输出特征的表现。
通过应用方程 1.10 中所示的梯度下降的一般矢量方程,我们也可以以如下方式重写方程 1.11 和 1.12 中的参数更新公式(记住x0= 1 的约定):
(1.22)
我们现在应该有了所有的工具来编写一个仅用numpy
的多元回归算法:
import numpy as np
def gradient_descent(x, y, theta, alpha):
m = len(x)
n = len(theta)
new_theta = np.zeros(n)
# Perform the gradient descent on theta
for i in range(m):
# s = theta[0] + (theta[1]*x[1] + .. + theta[n]*x[n]) - y[i]
s = theta[0]
for j in range(1, n):
s += theta[j]*x[i][j-1]
s -= y[i]
new_theta[0] += s
for j in range(1, n):
new_theta[j] += s * x[i][j-1]
return theta - (alpha/m) * new_theta
def train(x, y, steps, alpha=0.001):
# Initialize theta randomly
theta = np.random.randint(low=0, high=10, size=len(x[0])+1)
# Perform the gradient descent <steps> times
for i in range(steps):
theta = gradient_descent(x, y, theta, alpha)
# Return the linear function associated to theta
def model(x):
y = theta[0]
for i in range(len(theta)-1):
y += theta[i+1] * x[i]
return y
return model
这些变化将使针对单变量情况分析的线性回归算法也适用于一般的多变量情况。
在前面的代码中,为了清楚起见,我将所有的矢量运算扩展到了for
语句中,但是请记住,大多数真正的回归算法都使用本地矢量运算来执行矢量和以及标量积(如果有的话)。你现在应该已经注意到梯度下降是一个计算量很大的过程。该算法在一个 m 大小的数据集上遍历个时期次,每次执行一些矢量和与标量积,每个数据集项由 n 个特征组成。一旦你在多个节点上执行梯度下降,事情变得更加计算昂贵,就像在神经网络中一样。以矢量形式表达前面的步骤允许利用硬件或软件中可用的一些优化和并行化。我将把它作为一个带回家的练习,使用矢量原语编写前面的 n 维梯度下降算法。
在进入一个实际的例子之前,让我花一些时间来讨论多特征问题中两个相当重要的话题:特征选择和训练/测试集分割。
1.5.1 冗余功能
向模型中添加更多的输入要素通常会使模型在真实示例中更加准确。在我们解决的第一个回归问题中,我们的模型可以仅根据房子的大小来预测房子的价格。我们凭直觉知道,从现实世界的例子中添加更多的特征通常会使价格预测更加准确。我们凭直觉知道,如果我们还输入一些特征,如房屋的建造年份、该道路上房屋的平均价格、是否有阳台或花园,或者离市中心的距离,我们可能会得到更准确的预测。然而,这是有限度的。非常重要的一点是,你提供给你的模型的特征彼此之间是线性独立的。给定一个向量列表,如果这个列表中的一个向量
可以写成
(1.23),那么这个向量就定义为线性相关
用。换句话说,如果一个特征可以表示为任何其他数量的特征的线性组合,那么这个特征就是多余的 ??。冗余特征例子包括
-
用欧元和美元表示的价格相同
-
用米和英尺或米和公里表示的相同距离
-
包含产品净重、皮重和毛重的数据集
-
包含产品的基础价格、增值税率和最终价格的数据集
上述场景都是其他要素线性组合的示例。在将数据集输入模型之前,应移除数据集中所有冗余的要素;否则,模型的预测将会向构成派生特征的特征倾斜,或偏向。有两种方法可以做到这一点:
-
手动:查看您的数据,并尝试了解是否有任何属性是冗余的——即,是其他特征的线性组合的特征。
-
解析:你的数据集中有 m 个输入,每个输入由 n 个特征表示。你可以把它们排列成一个 m × n 矩阵
。这个矩阵的秩、 ρ ( X )定义为其线性无关向量(行或列)的个数。如果其关联矩阵 X 是这样的,我们可以说我们的数据集没有冗余特征
(1.24)
如果ρ(X)= min(m,n)–1,那么数据集有一个线性相关向量。如果ρ(X)= min(m,n)–2,那么它有两个线性相关向量,以此类推。numpy
和scipy
都有内置的方法来计算矩阵的秩,所以如果你想仔细检查数据集中是否有多余的特征,这可能是一个好方法。
尝试删除数据集中的重复行也同样重要,因为它们可能会在某些特定范围内“拉动”您的模型。然而,如果m≫n-也就是说,如果数据样本的数量远大于要素的数量,则数据集中重复行的影响不会像重复列那样糟糕。
主成分分析
从训练集中删除冗余特征的一种分析(并且易于自动化)方法是执行所谓的主成分分析,或 PCA 。当您有大量的输入要素,并且手动执行函数依赖关系分析可能很麻烦时,PCA 特别有用。PCA 是一种用于特征提取的算法,即通过将 A 中的点映射到一个新的 k 维空间A’中来减少一个 n 维输入空间 A 中的维数,使得 A 中的特征
PCA 背后的数学初看起来可能有点难,但它依赖于一个非常直观的几何概念,所以请相信我接下来的几个公式。
PCA 的第一步是特征归一化,正如我们在方程 1.13 :
(1.25)中看到的
用μx表示的算术平均值, σ * x * 表示其标准差。
然后,给定具有输入特征的归一化训练集,我们计算
的协方差矩阵为
(1.26)
您可能还记得线性代数中的两个向量的乘积,通常被称为标量积或点积,它返回一个实数,而两个向量的乘积
,返回一个大小为n×n的矩阵,这就是我们用前面的协方差矩阵得到的结果。向量协方差矩阵背后的直觉是拥有输入分布的几何表示,这就像一维情况下方差概念的多维扩展。
然后我们继续计算的特征向量。矩阵的特征向量定义为当我们对其应用矩阵所描述的几何变换时,保持不变的非零向量(或者至多被标量 λ 缩放,称为特征值)。例如,考虑我们的星球围绕它的轴的旋转运动。旋转运动可以映射到一个矩阵中,给定地球上的任何一点,该矩阵可以告诉该点在应用旋转后将位于何处。应用旋转时,位置不会改变的点只有那些沿着旋转轴的点。然后,我们可以说旋转轴是与我们行星的旋转相关的矩阵的特征向量,并且至少在这种特定情况下,该向量的特征值是λ= 1——沿着轴的点在旋转期间根本不变;它们甚至没有被缩放。球体旋转的情况具有单个特征向量(旋转轴),但是其他几何变换可能具有多个特征向量,每个特征向量具有不同的相关特征值。为了使这种直觉形式化,我们说,给定一个描述 n 维空间中某个几何变换的矩阵
,当我们对它应用 A 时,它的特征向量
必定是一个至多按因子 λ 缩放的向量:
图 1-6
归一化数据集的主成分分析。绿色向量表示对数据分布影响更大的组件。您可能希望将输入数据映射到这个新的坐标系。你也可以只选择最大的向量,而不会丢失很多信息
(1.27)
通过将前面等式中的分组,我们得到
(1.28)
其中 I n 是大小为 n × n 的单位矩阵(主对角线上为 1,其他地方为 0 的矩阵)。前面的向量符号可以展开成一组 n 方程,通过求解它们,我们可以得到 A 的特征值 λ 。通过替换前面方程中的特征值,我们可以得到与矩阵相关的特征向量。
在计算自协方差矩阵的特征向量的背后有一种几何直觉。这些特征向量表明在 n 维输入空间的哪个方向上数据的“扩散”最大。这些方向是您的输入空间的主成分,也就是说,这些成分与您的数据分布更相关,因此您可以将原始空间映射到较低维度的新空间,而实际上不会丢失太多信息。如果一个输入特征可以表示为一些其他输入特征的线性组合,那么输入空间的协方差矩阵将具有两个或多个具有相同特征值的特征向量,因此,维数可以被压缩。您还可以通过选择具有 k 个最高关联特征值的 k 个特征向量,来决定删减一些对数据分布影响很小的特征,即使它们不是严格的线性相关,直观上,这些特征向量是数据分布中最重要的组成部分,图 1-6 中显示了一个示例。
一旦我们有了输入空间的主分量,我们需要通过沿着特征向量重新定向输入空间的轴来转换输入空间——注意,这些特征向量是相互正交的,就像笛卡尔平面的轴一样。我们把从选择的主成分(自协方差矩阵的特征向量)构造的矩阵称为 W 。给定一个用矩阵 X 表示的归一化数据集,它的点将通过
(1.29)映射到新的空间
然后,我们将在新矩阵上训练我们的算法,新矩阵的维数将等于或低于初始特征数,而没有显著的信息损失。
许多用于机器学习和数据科学的 Python 库已经提供了一些用于主成分分析的功能。使用“scikit-learn”的示例:
import numpy as np
from sklearn.decomposition import PCA
# Input vector
x = np.array([[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]])
# Define a PCA model that brings the components down to 2
pca = PCA(n_components=2)
# Fit the input data through the model
pca.fit(x)
PCA 有一些明显的优势,其中之一是,它通过分析减少了具有大量特征的输入空间的维数,降低了过度拟合的风险,并提高了算法的性能。然而,它将原始输入空间映射到由主成分构建的新空间,这些新的合成特征可能无法像真实世界的特征(如“大小”、“距离”或“时间”)那样直观地掌握此外,如果您选择的组件数量少于影响数据的组件的实际数量,您可能会丢失信息,因此通常比较应用 PCA 前后模型的性能以检查您是否删除了一些实际需要的功能是一种很好的做法。
1.5.3 训练集和测试集
我们看到的第一个线性回归例子是在一个非常小的数据集上训练的;因此,我们决定在同一个数据集上训练和测试模型。然而,在所有现实世界的场景中,您通常会在非常大的数据集上训练您的模型,并且在您的模型尚未被训练的数据点上评估您的模型是很重要的。为此,输入数据集通常分为两部分:
-
训练集包含你的模型将被训练的数据点。
-
测试集包含你的模型将被评估的数据点。
图 1-8
看一下自动 MPG 数据集
图 1-7
70/30 训练集/测试集拆分示例
这通常是通过根据预定义的分数分割你的数据集来完成的——透视表左边的项目将构成训练集,右边的项目将构成测试集,如图 1-7 所示。对数据集分割的一些观察:
-
您想要选择的分割分数在很大程度上取决于您的数据集。如果您有一个非常大的数据集(假设数百万个数据点或更多),那么您可以选择一个大的训练集分割(例如,90%的训练集和 10%的测试集),因为即使测试集的部分很小,它仍将包括数万或数十万个项目,并且这仍将足以评估您的模型。在具有较小数据集的场景中,您可能希望试验不同的部分,以在用于训练目的的可用数据的利用和为评估您的模型而选择的测试集的统计显著性之间找到最佳平衡。换句话说,您可能希望在用于训练目的的可用数据的利用和基于统计显著数据集的模型的评估之间找到平衡。
-
如果数据集是根据某个要素排序的,请确保在执行分割之前对其进行洗牌。模型训练和评估的数据尽可能一致是非常重要的。
1.5.4 加载和可视化数据集
在本例中,我们将加载汽车 MPG 数据集[15],该数据集包括几个关于 20 世纪 70-80 年代汽车的参数(气缸、重量、加速度、年份、马力、燃油效率等)。).我们希望建立一个模型,在给定相应输入特征的情况下,预测那些年汽车的燃油效率。
首先,让我们下载数据集,将其加载到我们的笔记本中,并查看它:
import pandas as pd
import matplotlib.pyplot as plt
dataset_url = 'http://archive.ics.uci.edu/ml/' +
'machine-learning-databases/' +
'auto-mpg/auto-mpg.data'
# These are the dataset columns we are interested in
columns = ['MPG','Cylinders','Displacement','Horsepower',
'Weight', 'Acceleration', 'Model Year']
# Load the CSV file
dataset = pd.read_csv(dataset_url, names=columns,
na_values = "?", comment="\t",
sep=" ", skipinitialspace=True)
# The dataset contains some empty cells - remove them
dataset = dataset.dropna()
# Take a look at the last few records of the dataset
dataset.tail()
你可能会看到如图 1-8 所示的表格。让我们看看这些特性是如何相互关联的。我们将使用seaborn
库,特别是pairplot
方法来可视化每个指标的数据点如何与每个其他指标进行对比:
sns.pairplot(dataset[["MPG", "Cylinders", "Displacement", "Weight"]], diag_kind="kde")
您应该会看到类似于图 1-9 所示的图。每个图表都绘制了由一对指标分解的数据点:这是发现相关性的一个有用的方法。你会注意到,气缸数量、排量和重量与 MPG 有很大关系,而其他指标之间的关系更松散。
我们现在将数据集分成两部分,如第 1.5.3 节所示。训练集将包含 80%的数据,测试集包含剩余的 20%:
# Random state initializes the random seed for randomizing the
# seed. If None then it will be calculated automatically
train_dataset = dataset.sample(frac=0.8, random_state=1)
# The test dataset contains all the records after the split
test_dataset = dataset.drop(train_dataset.index)
# Fuel efficiency (MPG) will be our output label, so drop
# it from the training and test datasets
train_labels = train_dataset.pop('MPG')
test_labels = test_dataset.pop('MPG')
并将数据标准化:
图 1-9
通过 seaborn 了解每个功能与其他功能之间的关系
def normalize(x, stats):
return (x - stats['mean']) / stats['std']
def denormalize(x, stats):
return x * stats['std'] + stats['mean']
data_stats = train_dataset.describe().transpose()
label_stats = train_labels.describe().transpose()
norm_train_data = normalize(train_dataset, data_stats)
norm_train_labels = normalize(train_labels, label_stats)
然后,就像前面的例子一样,我们将定义一个线性模型,并尝试通过我们的数据来拟合它:
from tensorflow.keras.experimental import LinearModel
model = LinearModel(len(train_dataset.keys()),
activation='linear', dtype="float32")
model.compile(optimizer='rmsprop', loss="mse", metrics=['mae', 'mse'])
history = model.fit(norm_train_data, norm_train_labels, epochs=200,
verbose=0)
这次的不同之处在于
-
上例中的模型只有一个输入单元;这一个的输入单元与我们的训练集的列一样多(不包括输出特征)。
-
这次我们使用两个评估指标,都是
mae
和mse
。在大多数情况下,保留一个主要的评估指标而不是损失函数是一个很好的实践。
让我们再次绘制训练迭代的损失函数:
epochs = history.epoch
loss = history.history['loss']
fig = plt.figure()
plot = fig.add_subplot()
plot.set_xlabel('epoch')
plot.set_ylabel('loss')
plot.plot(epochs, loss)
您应该会看到如图 1-10 所示的图形。
此外,我们现在可以在测试集上评估模型,并查看它在未经训练的数据上的表现:
图 1-10
多元回归训练中损失函数的研究进展
norm_test_data = normalize(test_dataset, data_stats)
norm_test_labels = normalize(test_labels, label_stats)
model.evaluate(norm_test_data, norm_test_labels)
请记住,到目前为止,我们一直在使用一个单一的回归单元:当我们将它们打包到一个神经网络中时,事情会变得更好。我们可以从测试集中挑选一些值,看看模型的预测与预期的标签有多远:
sampled_data = norm_test_data.iloc[:10]
sampled_labels = denormalize(norm_test_labels.iloc[:10], label_stats)
predictions = [p[0] for p in
denormalize(model.predict(sampled_data), label_stats)]
for i in range(10):
print(f'predicted: {predictions[i]} ' +
f'actual: {sampled_labels.iloc[i]}')
在条形图上绘制这些值应该会产生如图 1-11 所示的图形。
然后,您可以对我们在第 1.4.7 节中遇到的model_save
和model_load
进行最小的更改,以从另一个笔记本或应用程序中保存和加载您的模型。
1.6 多项式回归
我们现在知道如何创建一个或多个输入变量的线性回归模型。我们已经看到,这些模型可以在几何上由 n 维线性超曲面表示,在 n + 1 维空间中,该空间由 n 维输入特征加上 y 维输出特征组成—该模型将是一条穿过 2D 空间中的点的线。在一元线性回归的情况下,如果有两个输入特征和一个输出变量,则该模型将是一个穿过 3D 空间中的点的 2D 曲面,依此类推。
图 1-11
预测值和期望值之间的比较
然而,并不是现实世界中的所有问题都可以用线性模型来近似。考虑<REPO_DIR>/datasets/house-size-price-2.csv
下的数据集。这是我们之前遇到的房屋大小与价格数据集的变体,但这一次,我们在 100-130 平方米的范围内达到了一个平台期(例如,这种大小或具有特定房间配置的房屋在该位置卖得不多),然后价格在 130 平方米后再次保持上涨。其表示如图 1-12 所示。你不能仅仅用一条直线来捕捉这样的增长-停止-增长序列。更糟糕的是,如果您试图通过我们之前分析的线性过程拟合穿过该数据集的直线,该直线可能会被平台周围的点“拉”下来,以最小化总均方误差,最终也会损失数据集中剩余点的精度。
在这种情况下,您可能想利用一个多项式模型。请记住,线性回归可以以最简单的形式建模为hθ(x)=θ0+θ1x,但是没有什么可以阻止我们定义一个假设函数hθ(x 例如,这种房屋大小-价格非线性模型可能不能很好地用二次函数来表示——记住抛物线首先上升,然后下降,当大小增加时,你不会真的期望房价显著下降。然而,三次模型可以很好地工作——记住,平面上的三次函数看起来像两个“半抛物线”粘在一起,围绕着一个拐点,这也是函数的对称点。因此,这种情况下的假设函数可能是这样的:
(1.30)
图 1-12
无法用线性模型精确表示的房屋大小与价格数据集的示例
找到使前面的函数最小化的的值的一个聪明的方法是将 x 的额外幂视为额外变量,执行变量替换,然后将其视为一般的多元线性回归问题。换句话说,我们希望将多项式函数的梯度下降问题转化为多元线性回归问题。例如,我们可以将前面的hθ(x)表达式改写为
的函数,其中
(1.31)
所以hθ(x)可以改写为
(1.32)
以这种形式写出的假设与我们在方程 1.17 中看到的假设相同。因此,我们可以通过我们分析过的线性多元梯度下降程序继续计算 θ 的值,只要您记住以下几点:
-
当你进行预测时,你从算法中得到的
的值必须插入到方程 1.30 中的三次假设函数中,而不是我们在那一节中看到的线性函数中。
-
特征缩放/输入归一化在回归模型中总是很重要,但是当涉及到多项式回归问题时,它甚至更加重要。如果房子的平方米大小在 0,…,10 3 的范围内,那么它的平方值将在[0,…,10 6 的范围内,它的立方值将在[0,…,10 9 的范围内。如果在将输入输入到模型之前不对其进行归一化,最终会得到一个最高多项式项的权重远远大于其余项的模型,这样的模型可能根本不会收敛。
-
在前面的示例中,我们选择了三次函数,因为它看起来很适合数据,有些增长,拐点,然后再次增长。然而,这并不适用于所有的模型。一些模型可能在更高的多项式幂(例如, x 的 4 次或 5 次幂)或 x 的分数次幂(例如,平方根或立方根)下表现更好。或者,在多个输入特征的情况下,一些关系可以通过一些特征的乘积或比率来很好地表达。这里重要的一点是总是在推理什么是最适合它的分析函数之前查看你的数据集。有时,您可能还想尝试绘制几个样本假设函数,看看哪一个函数的形状最适合您的数据。
总的来说,将多项式回归问题转化为多元回归问题是一个好主意,因为正如我们之前看到的,线性模型的成本函数通常由简单的 n 维二次模型表示,该模型保证只有一个点具有零梯度,并且该点也是全局最小值。在这种配置中,一个设计良好的梯度下降算法应该能够通过简单地跟随梯度向量的方向而收敛到最优解,而不会在对高次多项式函数进行微分时可能会遇到的凸起和凹陷处“卡住”。
1.7 正规方程
梯度下降无疑是解决回归问题最流行的方法之一。然而,这不是唯一的方法。正如我们已经看到的,梯度下降通过沿着梯度的方向迭代“行走”直到我们达到最小值,找到优化成本函数的参数![$$ \overline{\theta} $$。正规方程提供了一种代数方法来一次性计算。我们很快就会看到,这种方法与梯度下降法相比,既有一些优点,也有一些缺点。在这一节中,我们将简要介绍什么是正规方程以及它是如何推导出来的,而不会过多地深入到形式证明中。我假设你对线性代数和向量/矩阵运算(逆、转置和乘积)有所了解。然而,如果不是这样,请随意跳过这一节,或者只记下最后一个等式。法线方程为最小化问题提供了梯度下降的分析替代方案,但它并不严格要求建立模型。
我们已经看到,回归模型的一般成本函数可以写成
(1.33)
而寻找最优模型的问题就是寻找的值使得
的梯度向量为零:
(1.34)
或者,换句话说:
(1.35)
如果对一个数据集展开方程 1.33 中的标量积和求和,其中 n 是特征的数量, m 是输入样本的数量,
表示输出特征的向量,并求解偏导数,则得到 n + 1 个线性方程,其中
表示变量。像所有的线性方程组一样,这个方程组也可以表示为一个矩阵×矢量积的解,我们可以求解相关的方程来计算
。原来
可以通过解以下方程来推断:
(1.36)
其中 X 是与数据集的输入要素相关联的 m × n 矩阵(在每个向量的开头添加了一个X0= 1 项,如我们之前所见),而 X T 表示转置的矩阵(即通过交换行和列得到的矩阵),而-1
法线方程比梯度下降算法有几个优点:
-
你不需要执行多次迭代,也没有陷入困境或发散的风险:
的值可以通过求解 n + 1 相关的梯度线性方程组直接计算出来。
-
因此,你不需要选择一个学习率参数 α ,因为不涉及增量学习过程。
然而,它也有一些缺点:
-
即使输入特征 n 的数量非常大,梯度下降仍将表现良好。在你的θ-更新步骤中,更多的特征转化为更大的标量积,并且当 n 增长时,标量积的复杂度线性增加。另一方面, n 的值越大,意味着等式 1.36 中的XTX矩阵越大,计算一个非常大的矩阵的逆是一个计算量非常大的过程(它有一个O(n3)复杂度)。这意味着,对于输入要素数量较少的回归问题,法线方程可能是一个很好的解决方案,而梯度下降法在包含更多列的数据集上表现更好。
-
The normal equation works only if the XTX matrix is invertible. A matrix is invertible only if it is a full rank square matrix, that is, its rank equals its number of rows/columns, and therefore, it has a non-zero determinant. If XTX is not full rank, it means that either you have some linearly dependent rows or columns (therefore, you have to remove some redundant features or rows) or that you have more features than dataset items (therefore, you have to either remove some features or add some training examples). These normalization steps are also important in gradient descent, but while a non-invertible dataset matrix could result in either a biased or non-optimal model if you apply gradient descent, it will fail on a division by zero if you apply the normal equation. However, most of the modern frameworks for machine learning also work in the case of non-invertible matrices, as they use mathematical tools for the calculation of the pseudo-inverse such as the Moore-Penrose inverse. Anyway, even if the math will still work, keep in mind that a non-invertible characteristic matrix is usually a flag for linearly dependent metrics that may affect the performance of your model, so it’s usually a good idea to prune them before calculating the normal equation.
图 1-13
逻辑函数图
1.8 逻辑回归
线性回归解决了为数值预测创建模型的问题。然而,并不是所有的问题都需要在数值连续域中进行预测。我们之前提到过,分类问题构成了机器学习中的另一大类问题,也就是说,你希望你的模型对所提供的输入的类或类型(例如,它是垃圾邮件吗?是异常吗?里面有猫吗?).幸运的是,我们不需要为了使回归过程适应分类问题而对它进行很多修改。我们已经学习了定义一个将映射到真实值的
假设函数。我们只需要找到一个假设函数,它输出这样的值
,并定义一个阈值函数,将输出值映射到一个数值类(例如,0 表示假,1 表示真)。换句话说,给定一个线性模型
,我们需要找到一个函数 g 使得
(1.37)
(1.38)
对于 g 的一个常见选择是 sigmoid 函数,或者逻辑函数,这也给这种类型的回归起了个名字逻辑回归。变量 z 的逻辑函数有如下公式:
(1.39)
这种功能的形状如图 1-13 所示。函数值会随着函数的减小而接近零,随着函数的增大而接近一,函数在 z = 0 附近有一个拐点,其值为 0.5。因此,将线性回归的输出映射到[0…1]范围是一个很好的选择,利用原点周围的强非线性来映射“跳跃”。通过将我们的线性模型代入方程 1.39 ,我们得到逻辑回归的公式:
(1.40)
现在让我们坚持使用单个输出类的逻辑回归(假/真)。我们将很快看到如何将它扩展到多类问题。如果我们坚持这个定义,那么逻辑曲线更早地表达了输入为“真”的概率您可以将逻辑函数的输出值解释为一个贝叶斯概率——在给定输入特征的情况下,一个项目属于/不属于输出类的概率:
(1.41)
给定 sigmoid 函数的形状,我们可以将分类问题形式化为:
(1.42)
由于当 z ≥ 0 时 g ( z ) ≥ 0.5,我们可以将前面的表达式重新表述为
(1.43)
逻辑回归背后的思想是画一个决策边界。如果基础回归模型是线性模型,那么想象在你的数据上画一条线(或者一个超曲面)。左边的点代表负值,右边的点代表正值。如果基础模型是更复杂的多项式模型,则决策边界在模拟数据点之间的非线性关系时会更复杂。
我们举个例子:考虑一下<REPO_DIR>/datasets/spam-email-1.csv
下的数据集。它包含用于垃圾邮件检测的数据集,每一行都包含与电子邮件相关联的元数据。
-
第一行
blacklist_word_count
,报告电子邮件中有多少单词与垃圾邮件黑名单中的单词相匹配。 -
第二行
sender_spam_score
是由垃圾邮件过滤器分配的介于 0 和 1 之间的分数,表示基于发件人的电子邮件地址、域或内部域策略,该电子邮件是垃圾邮件的概率。 -
第三行
is_spam
,如果电子邮件不是垃圾邮件,则为 0,如果电子邮件是垃圾邮件,则为 1。
我们可以绘制数据集,看看指标之间是否有任何关联。我们将把blacklist_word_count
标在 x 轴上,把sender_spam_score
标在 y 轴上,如果是垃圾邮件,用红色表示关联的点,如果不是垃圾邮件,用蓝色表示关联的点:
图 1-14
垃圾邮件数据集图
import pandas as pd
import matplotlib.pyplot as plt
csv_url = 'https://raw.githubusercontent.com/BlackLight' +
'/mlbook-code/master/datasets/spam-email-1.csv')
data = pd.read_csv(csv_url)
# Split spam and non-spam rows
spam = data[data['is_spam'] == 1]
non_spam = data[data['is_spam'] == 0]
columns = data.keys()
fig = plt.figure()
# Plot the non-spam data points in blue
non_spam_plot = fig.add_subplot()
non_spam_plot.set_xlabel(columns[0])
non_spam_plot.set_ylabel(columns[1])
non_spam_plot.scatter(non_spam[columns[0]], non_spam[columns[1]], c="b")
# Plot the spam data points in red
spam_plot = fig.add_subplot()
spam_plot.scatter(spam[columns[0]], spam[columns[1]], c="r")
您应该会看到如图 1-14 所示的图表。我们可以直观地看到,我们可以在平面上大致画一条线来区分垃圾邮件和非垃圾邮件。根据我们选择的线的斜率,我们可能会让一些情况漏过,但一条好的分隔线应该足够精确,以便在大多数情况下做出好的预测。逻辑回归的任务是找到我们可以在方程 1.40 中绘制的 θ 的参数,以获得良好的预测模型。
图 1-15
具有一个输入变量的逻辑回归成本函数的示例图
1.8.1 成本函数
我们在线性回归中见过的大多数原理(线性或多项式模型的定义、成本/损失函数、梯度下降、特征归一化等。)也适用于逻辑回归。主要区别在于我们如何编写成本函数。虽然当您想要计算价格预测和实际价格之间的平均误差时,均方误差是一个有意义的指标,但当您想要确定在给定离散数量的类的情况下,您是否预测了数据点的正确类时,它就没有多大意义了。
我们希望构建一个成本函数来表达分类误差—即预测的类别是正确还是错误—以及分类误差有多大,即模型对其分类的“确定/不确定”程度。让我们通过调用的新参数:
(1.44),重写我们之前定义的成本函数
当涉及到逻辑回归时,函数 C 经常以这种形式表示(我们现在将坚持一个二元分类问题,即只有两个输出类的问题,像真/假;我们后面会扩展到多个类):
(1.45)
直觉如下:
-
如果 y i = 1(第 i 个数据点的类为正)并且预测值
也等于 1,那么代价将为零(–log(1)= 0,即没有预测误差)。随着预测值远离 1,成本将逐渐增加。如果yI= 1 并且
,那么代价函数将假设一个无穷大的值( log (0) = ∞)。在实际应用中,我们当然不会用无穷大,但我们可能会用一个非常大的数来代替。实际值为 1 而预测值为 0 的情况类似于模型以 100%的置信度预测外面正在下雨,而实际上并没有下雨-如果发生这种情况,您希望通过应用大的成本函数使模型回到正轨。
-
同样,如果 y i = 0,预测值也等于 0,那么代价函数将为 null(–log(1–0)= 0)。如果取而代之的是 y i = 0 和
,那么成本将会趋于无穷大。
在一个输入和一个输出变量的情况下,成本函数的曲线将如图 1-15 所示。如果我们将等式 1.45 中的表达式组合在一起,我们可以将等式 1.44 中的逻辑回归成本函数重写为
(1.46)
我们已经将方程 1.45 的两个表达式压缩在一起。如果yI= 1,那么方括号内的和的第一项适用,如果yI= 0,那么第二项适用。
就像线性回归中,寻找最优值的问题是一个最小化代价函数的问题——也就是执行梯度下降。因此,我们仍然可以应用等式 1.9 中所示的梯度更新步骤,以获得到成本函数表面底部的方向。此外,就像线性回归一样,我们利用了一个凸成本函数——也就是说,一个具有单个零梯度点的成本函数恰好也是全局最小值。
通过用方程 1.40 中定义的逻辑函数替换前面公式中的,并求解方程 1.9 中的偏导数,我们可以推导出
在k+1-逻辑回归步骤:
(1.47)
你会注意到,θ-更新步骤的公式与我们在方程 1.22 中看到的线性回归基本相同,尽管我们是通过不同的途径得到它的。我们可能已经预料到了,因为我们的问题仍然是找到一条在某种程度上符合我们数据的线。唯一的区别是,线性情况下的假设函数 h θ 是参数和输入向量(
)的线性组合,而在逻辑回归情况下,它是我们在方程 1.40 中引入的 sigmoid 函数。
1.8.2 从头构建回归模型
现在,我们可以将之前看到的线性情况下的梯度下降算法扩展到逻辑回归。让我们实际上把我们到目前为止分析的所有部分(假设函数、成本函数和梯度下降)放在一起,为回归问题建立一个小框架。在大多数情况下,您不需要从头开始构建回归算法,但是这是一个很好的方式来查看我们到目前为止所涉及的概念在实践中是如何工作的。首先,让我们定义逻辑回归的假设函数:
import math
import numpy as np
def h(theta):
"""
Return the hypothesis function associated to
the parameters theta
"""
def _model(x):
"""
Return the hypothesis for an input vector x
given the parameters theta. Note that we use
numpy.dot here as a more compact way to
represent the scalar product theta*x
"""
ret = 1./(1 + math.exp(-np.dot(theta, x)))
# Return True if the hypothesis is >= 0.5,
# otherwise return False
return ret >= 0.5
return _model
注意,如果我们用标量积替换前面的假设函数,我们将逻辑回归问题转化为线性回归问题:
import numpy as np
def h(theta):
def _model(x):
return np.dot(theta, x)
return _model
然后让我们编写梯度下降算法:
def gradient_descent(x, y, theta, alpha):
"""
Perform the gradient descent.
:param x: Array of input vectors
:param y: Output vector
:param theta: Values for theta
:param alpha: Learning rate
"""
# Number of samples
m = len(x)
# Number of features+1
n = len(theta)
new_theta = np.zeros(n)
# Perform the gradient descent on theta
for j in range(n):
for i in range(m):
new_theta[j] += (h(theta)(x[i]) - y[i]) * x[i][j]
new_theta[j] = theta[j] - (alpha/m) * new_theta[j]
return new_theta
然后是由个时期个梯度下降迭代组成的train
方法:
def train(x, y, epochs, alpha=0.001):
"""
Train a model on the specified dataset
:param x: Array of normalized input vectors
:param y: Normalized output vector
:param epochs: Number of training iterations
:param alpha: Learning rate
"""
# Set x0=1
new_x = np.ones((x.shape[0], x.shape[1]+1))
new_x[:, 1:] = x
x = new_x
# Initialize theta randomly
theta = np.random.randint(low=0, high=10, size=len(x[0]))
# Perform the gradient descent <epochs> times
for i in range(epochs):
theta = gradient_descent(x, y, theta, alpha)
# Return the hypothesis function associated to
# the parameters theta
return h(theta)
最后,一个预测函数,给定一个输入向量、数据集的统计数据和模型
-
归一化输入向量
-
设置x?? 0= 1
-
根据给定的假设返回预测值hθ
def normalize(x, stats):
return (x - stats['mean']) / stats['std']
def denormalize(x, stats):
return stats['std'] * x + stats['mean']
def predict(x, stats, model):
"""
Make a prediction given a model and an input vector
"""
# Normalize the values
x = normalize(x, stats).values
# Set x0=1
x = np.insert(x, 0, 1.)
# Get the prediction
return model(x)
最后,一个给出输入值和预期输出列表的evaluate
函数评估给定假设函数的准确度(正确分类的输入数除以输入总数):
def evaluate(x, y, stats, model):
"""
Evaluate the accuracy of a model.
:param x: Array of input vectors
:param y: Vector of expected outputs
:param stats: Input normalization stats
:param model: Hypothesis function
"""
n_samples = len(x)
ok_samples = 0
for i, row in x.iterrows():
expected = y[i]
predicted = predict(row, stats, model)
if expected == predicted:
ok_samples += 1
return ok_samples/n_samples
现在,让我们通过在我们之前加载的数据集上训练和评估垃圾邮件检测模型来尝试这个框架:
columns = dataset_stats.keys()
# x contains the input features (first two columns)
inputs = data.loc[:, columns[:2]]
# y contains the output features (last column)
outputs = data.loc[:, columns[2]]
# Get the statistics for inputs and outputs
x_stats = inputs.describe().transpose()
y_stats = outputs.describe().transpose()
# Normalize the features
norm_x = normalize(inputs, x_stats)
norm_y = normalize(outputs, y_stats)
# Train a classifier on the normalized data
spam_classifier = train(norm_x, norm_y, epochs=100)
# Evaluate the accuracy of the classifier
accuracy = evaluate(inputs, outputs, x_stats, spam_classifier)
print(accuracy)
如果我们回头看看原始数据是如何分布的,以及我们定义了一个线性决策边界的事实,希望您应该测量出超过 85%的准确性,这并不是那么糟糕。
1.8.3 TensorFlow 方式
现在,我们已经了解了回归模型的所有细节,让我们构建一个逻辑回归模型,用 TensorFlow 解决我们的垃圾邮件分类问题。我们之前看到的线性回归的例子只需要做一些调整:
from tensorflow.keras.experimental import LinearModel
columns = dataset_stats.keys()
# Input features are on the first two columns
inputs = data.loc[:, columns[:2]]
# Output feature is on the last column
outputs = data.loc[:, columns[2:]]
# Normalize the inputs
x_stats = inputs.describe().transpose()
norm_x = normalize(inputs, x_stats)
# Define and compile the model
model = LinearModel(2, activation="sigmoid", dtype="float32")
model.compile(optimizer='sgd', loss="sparse_categorical_crossentropy",
metrics=['accuracy', 'sparse_categorical_crossentropy'])
# Train the model
history = model.fit(norm_x, outputs, epochs=700, verbose=0)
与线性模型相比,前面的代码有一些变化:
-
我们使用方程 1.39 中定义的
sigmoid
激活函数,而不是linear
。 -
我们使用
sgd
优化器— 随机梯度下降。 -
我们使用分类交叉熵损失函数,类似于等式 1.46 中定义的函数。
-
我们使用
accuracy
(即正确分类的样本数除以样本总数)作为性能指标。
图 1-16
logistic 模型在训练时期的损失函数
让我们看看损失函数在训练迭代中是如何发展的:
epochs = history.epoch
loss = history.history['loss']
fig = plt.figure()
plot = fig.add_subplot()
plot.set_xlabel('epoch')
plot.set_ylabel('loss')
plot.plot(epochs, loss)
您应该会看到如图 1-16 所示的图表。
我们在线性回归案例中看到的所有其他建模方法——evaluate
、predict
、save
和load
——也适用于逻辑回归案例。
多类回归
到目前为止,我们已经讨论了多个输入要素的逻辑回归,但只有一个输出类——对或错。扩展逻辑回归以处理多个输出类是一个非常直观的过程,称为 one vs. all 。
假设在我们之前看到的情况下,不是二进制垃圾邮件/非垃圾邮件分类,而是有三种可能的分类:正常、重要和垃圾邮件。其思想是将原来的分类问题分解成三个二元逻辑回归假设函数 h θ ,每个类一个:
-
对决策边界正常/不正常建模的函数
-
对决策边界重要/不重要建模的函数
-
对决策边界垃圾邮件/非垃圾邮件建模的函数
图 1-17
具有非线性决策边界的数据集的绘图
每个假设函数表示输入向量属于特定类别的概率。因此,具有最高值的假设函数是输入所属的函数。换句话说,给定一个输入和 c 类,我们想要为
选择具有最高相关假设函数值的类:
(1.48)
直觉告诉我们,具有最高值的假设函数代表了具有最高概率的类,这就是我们想要选择的预测。
非线性边界
到目前为止,我们已经探索了逻辑回归模型,其中 sigmoid 的自变量是线性函数。然而,就像线性回归一样,并不是所有的分类问题都可以通过绘制线性决策边界来建模。图 1-17 中数据集的分布绝对不适合线性决策边界,但椭圆决策边界可以很好地完成这项工作。就像我们在线性回归中看到的那样,通过变量替换,非线性模型函数可以有效地转化为线性多元函数,然后我们可以最小化该函数,以获得的值。在前面的例子中,一个好的假设函数可能是这样的:
(1.49)
然后,我们可以应用我们已经看到的变量替换程序,用新变量替换高次多项式项,并继续用线性模型解决相关的多元回归问题,以获得的值。就像线性回归一样,为了更好地表达复杂的决策边界,可以添加到模型中的额外多项式项的数量没有限制-无论如何要考虑到,添加太多多项式项可能最终会对决策边界进行建模,这可能会使数据过度拟合。正如我们将在后面看到的,检测非线性边界的另一种常见方法是将多个逻辑回归单元连接在一起——也就是说,建立一个神经网络。
二、神经网络
现在,我们已经知道如何使用回归来训练模型,是时候探索下一步了,即将多个回归单元拟合到神经网络中。
神经网络是解决输入特征之间具有非线性相关性的复杂回归或分类问题的更好方法。我们已经看到,如果基础数据的特征由线性关系绑定,那么线性回归和具有线性函数的逻辑回归可以表现得很好。在这种情况下,通过数据拟合一条线(或线性超曲面)是训练模型的好策略。我们还看到,更复杂的非线性关系可以通过向假设函数添加更多的多项式项来表达,例如,二次或三次关系或具有某些特征乘积的项。
然而,作为额外的多项式项添加更多的合成要素是有限制的。考虑图 2-1 所示的例子。线性分割边界在描述输入要素之间的关系时可能不太准确。我们可以添加更多的多项式项(例如,x1x2或
),这确实可以很好地拟合我们的数据,但我们有过度拟合数据集的风险(即,生成一个对特定训练输入表现良好但对其他情况不够通用的模型)。此外,如果我们只有两个输入特征,该方法可能仍然是可持续的。但是,请尝试想象一个真实的场景,例如房屋的价格,它可能依赖于大量不一定线性相关的要素,您会注意到模型所需的额外多项式要素的数量很容易激增。使用多个输入要素和多个非线性项来训练回归模型有两大缺点:
-
这些特征之间的关系很难想象。
-
这是一种倾向于组合爆炸的方法。您很可能会以大量的特征来表达所有的关系。这种模型训练起来非常昂贵,但仍然容易过度拟合。
当我们转向图像检测领域时,事情只会变得更加棘手。请记住,计算机只能看到图像中的原始像素值,图像中拍摄的对象通常具有非线性边界。
图 2-1
两个变量之间的非线性边界的例子可能很难单独用逻辑回归来表达
当您的数据由许多输入要素组成,并且数据的分布或其类之间的边界呈非线性时,将回归分类器组织成一个网络以更好地捕捉增加的复杂性通常是一个更好的主意,而不是尝试构建一个单一的回归分类器,该分类器可能带有许多多项式项来最好地描述您的训练集,但最终很容易过度拟合您的数据而实际上无法提供良好的预测。这个想法类似于大多数动物(和人类)的学习方式。我们系统中的神经元通过一种类似于图 2-2 所示的结构牢固地连接在一起,它们通过微小的电势变化不断地与身体周围(在称为轴突的纤维上,每个轴突都连接到一个神经元,如果你将更多的轴突捆绑在一起,你就会得到一个神经)以及与其他神经元(在称为树突的连接纤维上)相互作用。一个神经元的轴突和下一个神经元的树突之间的连接被称为突触。通过这些连接发送的电化学信号使动物能够看到、听到、闻到或感觉到疼痛,并使我们能够有意识地移动手臂或腿。然而,这些联系在人的一生中不断变异。神经元之间的联系是基于从周围环境中收集的感觉信号和我们收集的经验而形成的。我们天生不知道如何正确地抓住一个物体,但我们在生命的最初几个月里逐渐学会了。我们天生不知道如何说我们父母的语言,但随着我们接触越来越多的例子,我们逐渐学会了。在物理上,这是通过神经元之间连接的连续微调过程来实现的,以便优化我们执行某项任务的方式。神经元通过与它们相连的邻居同步,快速专门化地执行某项任务——位于我们脑后的神经元处理来自我们眼睛的图像,而位于前额皮质的神经元通常负责抽象思维和规划。每当净输入信号高于或低于某个阈值时,神经元很快学会激发电化学脉冲或保持沉默,并且根据一起激发的神经元更强地连接在一起的想法,连接被不断地重新建模。神经系统不仅负责创建新的连接以更好地对环境做出反应,还负责保持连接数量的优化——如果每个神经元都与所有其他神经元紧密相连,我们的身体将需要大量能量来保持所有连接的运行——因此在一定时间内不使用的神经路径最终会失去它们的力量;举例来说,这就是为什么我们倾向于忘记那些我们很久没有刷新的概念。
图 2-2
物理神经元的主要成分
人工神经网络的建模方式非常类似于它们的生物学对应物。不仅如此,人工智能和神经科学领域有着相互影响的悠久传统——人工神经网络是模仿生物网络建模的,而人工神经网络的发展进展往往揭示了大脑被忽视的特征。将人工神经网络中的每个神经元想象为具有一定数量输入的计算单元,这些输入近似映射物理细胞中的树突。每个输入“线”都有一个可以在学习阶段调整的权重,它近似地映射了物理神经元的突触。神经元本身可以被视为一个计算单元,它执行其输入的加权和,并应用非线性函数(如我们之前看到的逻辑曲线)来映射开/关输出状态。如果激活函数的输出高于某个阈值,那么神经元将“发射”一个信号。每个神经元的输出既可以被馈送到另一个神经元,也可以是网络的终端输出之一。网络最简单的例子是感知机(见图 2-3 ),类似于 Frank Rosenblatt 在 1957 年设计的试图识别图像中的人。它是一个单个神经元的网络,具有一组 n + 1 个输入特征(就像在回归的情况下,我们使用一个附件 x 0 = 1 个输入以更紧凑的方式表达线性模型)。每个输入都通过一个 n + 1 权重
的向量连接到一个神经元。该单元将输出一个激活值
,定义为其输入的加权和:
(2.1)
这样的激活值将通过一个激活函数**hθ(x),通常是我们在等式 1.39 中看到的 sigmoid/logistic 函数,它将真实值映射到一个离散的开/关值:
(2.2)
图 2-3
单神经元(感知器)网络的逻辑模型
这种学习单元可以用于一些简单的情况,但在更复杂的情况下,它将面临罗森布拉特的第一个感知机遇到的相同问题。在图像中识别人或物体是一个具有大量输入的学习问题——在其最简单的实现中,图像的每个像素都将是模型的输入。一般来说,单个神经元不足以模拟如此高水平的可变性。这就是为什么现在更常见的是将多个感知器单元打包成一个神经网络,其中每个神经元的每个输出都连接到下一层神经元的输入,如图 2-4 所示。通过封装更多相互连接的神经元,学习单元通常能够更好地识别高维问题中的复杂模式。图中的示例显示了一个具有三层的神经网络。对于简单的情况来说,这是一个非常常见的架构,但是我们很快就会看到,具有更细微模式的问题可以通过具有更多中间层的网络来更好地解决。按照惯例,第一层被称为输入层,它通常具有与输入数据集的维度一样多的神经元(加一,其中x0= 1)。第二层和任何其他中间层通常被称为隐藏层,因为它们完成大部分推理工作,但是它们既不直接连接到网络的输入也不直接连接到网络的输出。网络的最后一层称为输出层,它包含与输出类/标签一样多的输出(在图中的例子中是一个真/假类,但是我们很快就会看到具有更多输出的网络)。
注意,表示网络第 j 层中第 i 单元的激活值,而
表示模型的假设(或预测)与配置参数θ的函数关系,计算为
而在回归情况下,模型的参数(或权重)是向量,在这种情况下,每个第 j 层将具有相关联的θ(j)矩阵,以映射第 j 层和第 j + 1 层之间的权重。记住 j 中的每个单元都连接到 j + 1 中的每个单元,所以θj将是一个 m × n 矩阵,其中 m 是 j 中的单元数, n 是 j 中的单元数因此,我们可以把θ想象成一个 3D 张量。直观上,张量是矩阵的多维推广。在我们的例子中,张量θ的每个 j 的“切片”代表第 j 层的 2D 权重矩阵。
如果我们将迄今为止收集的所有信息放在一起,我们可以将图中每个神经元的激活功能形式化如下:
图 2-4
三层人工神经网络的逻辑模型
-
The units in the first layer are usually mapped one-to-one to the input features, unless you want to assign each feature a different weight:
-
The activation values of the units in the second layer is the logistic function of the weighted sum of the inputs:
-
The activation value of the unit in the last layer is the logistic function of the weighted sum of the outputs of the units from the previous layer:
我们可以将第 j 层中第 i 个单元的激活函数公式概括如下:
(2.3)
或者,使用向量符号,我们可以将第 j 层中单元的激活值向量描述为
(2.4)
这种算法通常被称为前向传播,它是一个具有特定权重集的神经网络如何在给定一些输入数据的情况下做出预测——直觉基本上是通过每个节点从输入层到输出层传播输入值。前向传播可以被视为在多单元场景中逻辑回归中使用的假设函数的推广。一些观察结果:
-
请记住, g 是逻辑函数,我们在每一层使用它来“离散化”输入的加权和。
-
到目前为止,我们已经设置了 x 0 = 1,因此每个输入向量的大小实际上都是 n + 1。我们对神经网络也保持这种做法,每个 j 层将有其自己的偏置单元
。我们可以假设这些偏置值现在总是等于 1,但是我们将在后面看到如何调整偏置向量来提高我们的模型的性能。
-
在我们到目前为止考虑的例子中,输入层和隐藏层都有 n + 1 个单元。虽然在大多数情况下输入层确实有 n + 1 个单元,其中 n 是输入的维数,并且输出层的单元数通常与输出类的数量一样多,但是对隐藏层中的单元数没有限制。实际上,在许多情况下,拥有比输入维度的数量更多的单元(至少在第一个隐藏层中)是一种常见的做法,但是要注意模型的性能,以确保不要一直添加超过实际上不会提高性能指标的隐藏单元。
-
正如我前面提到的,隐藏层的数量也没有限制。事实上,添加更多的隐藏层通常会在许多情况下提高模型的性能,因为您的网络将能够检测到更细微的模式。但是,就像处理单元数量的良好做法一样,您可能也不希望通过添加比您要解决的分类问题类型实际所需更多的层来过度设计您的模型,因为添加过多的中间层或单元在最好的情况下可能对网络没有可测量的影响,在最坏的情况下会因为过度拟合而降低网络的性能。同样,最佳实践是使用不同数量的单元尝试不同数量的层,并查看您何时能在良好的模型性能指标和良好的系统性能指标之间找到“最佳点”。
2.1 反向传播
如果前向传播是神经网络进行预测的方式,那么反向传播就是神经网络“学习”的方式——即,在给定训练集的情况下,它如何调整其权重张量θ。就像我们对回归所做的一样,对于神经网络,学习阶段可以通过定义我们想要优化的成本函数来推断。
在一般情况下,您将拥有一个具有 K 个输出的神经网络,其中这些输出是您想要检测的类。每个输出表示某个输入属于那个类的概率,在 0 和 1 之间。例如,如果网络的输入是衣服的图片,并且您想要检测图片中是否包含衬衫、裙子或裤子,则可能需要用三个输出单元来建模神经网络。例如,如果一幅图片包含一件衬衫,您希望该网络输出类似于[1,0,0]的内容。如果是一条裤子,你希望它输出类似于[0,0,1]的东西,以此类推。因此,与我们到目前为止看到的只有一个输出变量的假设函数不同,您将拥有一个包含 K 个值的向量的假设函数,每个类一个。您的模型的预测类通常是具有最高值的
函数的索引:
在前面的示例中,如果您为某个图片获得了一个类似[0.8,0.2,0.1]的假设向量,并且您的输出单位按照[衬衫、裙子、裤子]的顺序设置,那么该图片很可能包含一件衬衫。
因此,神经网络的成本函数的工作是最小化训练集中标签的第 i 个向量和预测输出向量
之间的分类误差。当我们涵盖逻辑回归:
(2.5)时,我们已经在等式 1.46 中的真/假二元分类问题的情况下看到了这种类型的成本函数
如果对于第 i 个输入样本,我们有一个由 K 个项组成的向量,而不是一个单个标签 y ( i ) ,并且我们有一个由权重 θ 组成的 3D 张量,其中每个切片代表权重矩阵,以将一层映射到下一层,那么成本函数可以重写如下:
总和中的第二项(乘以)是将每层的偏置输入(
元素的权重)编码为第 l 层和第 l + 1 层之间的权重的平方和的传统方式。 λ 是网络的偏差率或正则化率,它定义了网络对变化的惯性——在这种情况下,高值会导致更为保守的模型,也就是说,该模型对其权重应用校正的速度较慢,而低值会导致模型更快地适应变化,但存在过拟合数据的风险。训练阶段通常以较低的偏置率开始,以便在开始时快速调整到随时间缓慢降低的校正。
就像在回归的情况下,寻找θ的最优值是最小化前面的成本函数的问题;因此,执行某种形式的梯度下降,以找到它的最小值。在神经网络中,这个过程通常是逐层进行的,从输出层开始,逐层向后调整权重(这就是为什么称之为反向传播)。直觉是首先将网络的输出与预期样本进行比较,并且当我们调整输出层中单元的权重以更紧密地匹配输出时,我们计算前一层中单元的新的中间预期结果,并且我们继续调整权重,直到到达第一层。
让我们考虑一个具有 4 层和两个输出的网络,如图 2-5 所示。如果我们给它一个输入和一个期望标签的向量
,并对其应用前向传播,我们可以计算它的假设函数
:
然后,就像回归的情况一样,我们希望找到使成本函数J(θ)最小化的θ(在这种情况下是 3D 张量)的值。换句话说,对于每一层 l ,我们要计算它的梯度向量∇j(θ(l))。我们希望这个向量的值尽可能接近零:
这意味着J(θ(l))相对于其权重的偏导数应该设置为零,因此我们可以从得到的等式中导出最佳权重:
图 2-5
L = 4 层,n = 3 个输入,K = 2 个输出的神经网络示例
然后我们定义一个量作为第 l 层中第 j 个单元的误差系数,从最后一层开始,其中系数定义为
或者,以矢量的形式:
考虑到输出层上的误差,我们通过以下方式计算将这些单元连接到先前层中的单元的权重的校正:
这里发生了相当多的事情,所以让我们一项一项地挖掘:
-
θ(3)是包含连接网络第二层到第三层的权重的矩阵。如果如图 2-5 所示,第三层有三个单元,第二层有四个单元,那么θ(3)就是一个 3 × 4 的矩阵。
-
我们在一层中的权重的转置矩阵和在下一层计算的 δ 校正系数的向量之间执行矩阵-向量乘积。结果是一个矢量,它包含的元素数量与层中的单元数量一样多。
-
We then perform an element-wise product (or Hadamard product, denoted by ⊙) between that vector and the vector of partial derivatives of the activation function of the units in the layer (using the notation we have seen in Equation 2.4). The element-wise product is intuitively the element-by-element product between two vectors with the same size, for example:
-
∇ g 是为每个单元计算的激活函数(通常是 sigmoid 函数)的梯度向量。
我们可以推广前面的表达式,将第 l 层中单元的校正系数表示为
如方程 2.4 所示,激活函数表示第 l 、第a??(l)层单元的激活值。通过求解导数,我们可以推导出此公式为δ(l):2.7
2.7
有可能证明(尽管推导过程相当冗长,我们将在本章跳过)成本函数的偏导数和系数(暂时忽略偏差系数和归一化率 λ ):
(2.8)
由于成本函数的偏导数正是我们想要最小化的,我们可以使用等式 2.7 和 2.8 的结果来定义具有1层的网络如何通过训练集上的正向和反向传播循环进行“学习”:
-
在你的模型中初始化权重θ,随机地或者根据一些启发,并且初始化一个张量δ,该张量包含每个权重的成本函数的偏导数,对于第 l + 1 层中的第 i 单元与第 l 层中的第 j 单元的每个连接使用
。
-
迭代规范化训练集
中的所有项目。
-
对于每个第 i 个训练项目,设置
—网络的输入单元将使用每个标准化的输入向量进行初始化。
-
Perform forward-propagation to compute the activation values of the units in the next layers,
, with 1 < l ≤ L:
-
set
—你的网络的预测等于最后一层单元的激活值。
-
Start applying back-propagation by computing the δ vector for the last layer, as the difference between the predicted and expected values:
-
Continue back-propagation by computing the δ vectors for the other layers, starting from the L − 1-th layer and moving all the way back until the input layer:
-
Update the tensor of the corrections to be applied to the weights:
-
After iterating on the training set, we will have our tensor Δ fully calculated. We can now take regularization into account by introducing for each layer the bias unit (j = 0) and dividing each of the partial derivatives by the number of samples in the training set:
-
We know that
-
We can therefore plug these values into a gradient descent logic and use a learning rate α to update each weight according to these quantities:
-
对训练集的给定数量的个时期应用这种算法,或者直到满足某些收敛标准,你就拥有了训练网络的所有要素。
2.2 实施指南
为了优化神经网络的性能,您可能需要遵循一些好的做法:
-
随机初始化网络的权重。在大多数情况下,在预设间隔内随机初始化权重[-ϵ, ϵ ]是初始化网络的最佳方式。如果您用零(或任何其他常数)初始化权重,那么在第一次迭代中您将获得可预测的输出值。随机初始化打破了这种对称性,如果您多次训练模型,它比总是以相同方式初始化权重的解决方案更有可能将您的模型指向正确的方向。
-
在训练阶段之前或期间,对您的模型进行坡度检查。与我们在线性和逻辑回归的情况下看到的成本函数不同,神经网络的成本函数不能保证是凸的。这意味着,如果你从任何一点沿着梯度向量的方向,并不能保证模型收敛到全局最小值,因为你不再是把球滚下碗状的山坡。这意味着您可能希望检查以下两个方面:( 1)您为梯度下降选择的初始方向实际上导致了成本函数的显著降低(即,您没有被困在局部山谷中),( 2)学习率得到了很好的校准——如果学习率太低,您可能下降得太慢,如果学习率太高,模型可能会超过最小值而根本不会收敛。
-
试验你的网络架构。对于解决某个问题,多少层和多少个单元是最好的,并没有确定的规则。一般经验表明,中间层越多、中间层中的单元越多,网络的性能通常越好。然而,你可能也想避免过度工程化:一个识别 8×8 像素图像中手写数字的简单网络不一定需要 10 个中间层,每个中间层有数百个单元。不仅如此,在某个点之后增加更多的单元或层会导致过度拟合。因此,尝试不同的架构,看看在相同的训练集和训练迭代次数下,增加单元或层的数量会如何影响模型的性能,并在较大网络带来的性能提升可以忽略不计之前选择一个最佳点。
-
在向网络输入数据之前,一定要将输入数据标准化。谈到回归时,我已经足够强调这一点了,它在神经网络中也很重要。
2.2.1 欠配和过配
当你在高偏差/低方差(或欠拟合)和高方差/低偏差(或过拟合)之间训练任何模型时,总是要寻求一个健康的平衡。当我们讨论回归模型时,我们已经看到了这些问题,并且我们发现了绘制归一化数据集的重要性,以便在选择具有较少多项式项(欠拟合,模型的线/表面过于“平滑”并且没有真正遵循数据的分布)或太多多项式项(过拟合)的函数之前了解数据的分布情况, 模型的线/表面精确地遵循数据的分布,但是当被提供任何看起来不像它被训练的那些数据点时,在准确性上失败)。 这些观察也适用于神经网络。针对欠适应/过适应评估网络性能的最佳方法还是将数据集分成两部分,即训练集和测试集。在训练集上训练您的网络,并评估成本函数在训练迭代中的进展情况:
-
如果成本函数没有减少(或者,更糟的是,增加,或者经历上升/下降周期),那么模型没有向最小值收敛:确保数据是归一化的,修改你的梯度下降策略,或者降低学习速率 α 。
-
如果成本函数下降太慢,那么它在适应训练集中出现的变化方面不够快:您可能希望增加学习率 α ,降低归一化率 λ ,或者向您的数据添加更多的特征。
-
如果成本函数以令人满意的方式降低,并且您的模型似乎对训练集做出了准确的预测,那么在测试集上对其进行评估(这次不进行训练:只执行前向传播,不执行反向传播)。如果测试集上的成本函数或模型的准确性差得多,那么
-
您没有在训练数据和测试数据之间执行很好的拆分,这通常是通过在拆分之前对数据集中的项目进行洗牌来实现的,以保证数据的分布更加均匀。
-
网络没有提供足够的数据点来有效地检测数据集中的模式-您可以通过添加更多的数据点来修复它。
-
数据集包含太多要素:您可能希望应用主成分分析或任何类型的降维算法来移除冗余要素(其他要素的线性组合)或不会真正影响数据分布和模式的要素。
-
网络过拟合训练集中的点。在这种情况下,您可能希望使用单元/图层数量较少的网络进行试验,或者为正则化率 λ 选择一个较高的值,以便增加网络对数据集中“波动”的“惯性”。
-
正如我们已经看到的,一旦我们找到了一种给定训练集的收敛方法,我们主要有两个参数可以调整以调整模型的性能:学习率 α 和正则化率 λ 。我们已经看到, α 决定了网络在收到新数据时的学习速度,而 λ 则表达了网络对变化的“抵抗力”。有时数据集被分成三份而不是两份,以便分别调整这两个值:
-
首先,我们在训练集上训练模型,并确保其成本函数不断降低。这个阶段的目标是找到最小化成本函数的权重θ的值和一个 α (或一个在迭代 t 中返回 α 的函数 α ( t )的值,这是速度和鲁棒性之间的一个合理的折衷(表示为模型收敛的趋势,与起点无关)。
-
然后,我们使用交叉验证集来调整 λ 。这个阶段的目标是选择一个值 λ (或者一个函数 λ ( t )在迭代 t 中返回 λ ,这是欠拟合和过拟合之间的一个合理的折衷。
-
最后,我们在测试集上评估模型,以评估模型在它尚未看到的数据点上的整体性能。我们根据这些数据评估模型的成本函数和任何附件性能指标,并使用它们来确定模型是否表现足够好,或者是否需要更多的训练、不同的参数调整或不同的架构。
另一个好的实践是编写小的测试来检查模型的性能。到目前为止,我们已经介绍了帮助我们对模型的整体性能进行定量分析的数学工具,但是在现实世界的问题中,您可能更清楚您的模型在特定情况下应该预测什么。因此,从你的数据中挑选一些足够重要和足够多样的案例,并编写一些测试来衡量这些案例中有多少是正确的或错误的。这是一个有用的工具,可以有效地跟踪模型随时间的演变。使用此类测试来查看模型中的特定更改是否会导致这些“核心案例”的更好分类,并确保模型的后续更改不会降低其在这些数据点上的性能。
另一个通用的良好做法是记住,机器学习模型仍然是软件的一部分,像任何其他软件一样,它们应该经历类似的过程。使用像 Jupyter notebooks 这样的工具来交互式地可视化数据并训练您的模型,为该过程增加了许多价值和生产力,但请记住,您的工作成果不应只是由笔记本训练的模型文件,该文件将被扔掉或保存在个人笔记本电脑上。训练模型时,除了模型文件之外,您的工作输出还应包括:
-
一个干净的(最好是版本化的)代码库,可以重用它来重新训练模型,调试它,或者训练不同的模型。将代码库的公共部分(如保存和加载模型、规范化数据或初始化分类器)提取到可重用的模块中,这些模块可以轻松导入,这样您就不必重新发明轮子或走上几乎不可维护的复制/粘贴之路。在笔记本上制作脚本,这样训练、评估和预测阶段可以作为独立实体在其他系统上轻松运行,而不需要 Jupyter 环境。
-
按照前面描述的指导原则,对您的模型进行测试。
-
请记住,在真实的应用程序中,您的模型通常是更大的业务逻辑链中的一个模块。在实际应用中,您通常会从某个地方生成或接收数据,对这些数据运行一些自定义逻辑,使用您的机器学习模型对这些数据进行一些预测,并使用这些预测来运行一些额外的业务逻辑。因此,请记住,就像更复杂系统中的任何其他模块一样,在设计机器学习逻辑时,最好考虑到可扩展性和内部通信。从 CSV 文件中读取并在标准输出中打印结果是调试和测试模型的好方法,但是在实际的应用程序中,您可能希望将您的模型包装到例如 WSGI 或 Flask web 应用程序中,这样就可以很容易地在 REST API 上使用它。或者将它设计成可以使用来自消息队列或 WebSocket 的查询或训练/评估命令。如果需要将它部署在多个环境中,您甚至可以考虑将其部署为 Docker 微服务,这样您就不必直接在目标系统上安装所有依赖项,而且通常它还有助于防止“但它可以在我的笔记本电脑上工作”的问题。
-
尽可能跟踪用于训练模型的数据。在过去几年中,机器学习应用数量的增加伴随着越来越多的问题,这些问题与由不良/有偏见的数据导致的不良预测有关。在海量数据上训练模型的公司很难跟踪哪些有偏见的训练输入导致了哪些有偏见的分类,机器学习模型通常被视为黑盒神谕——我们知道它们预测什么,但我们无法说出它们做出这些预测的确切原因。这就是为什么跟踪用于训练模型的数据变得越来越重要,并且最好对其进行版本化/标记:这使得在模型性能下降的情况下更容易找到根本原因,并且还有助于增加模型的可问责性。
2.3 误差指标
到目前为止,我们已经分析了一些指标来评估模型的性能。其中包括以下内容:
-
均方误差,常用作回归问题中模型的驱动成本函数
-
平均绝对误差,有时用作额外的性能指标
-
分类误差,在逻辑回归和神经网络中用作驱动成本函数
-
准确度,定义为正确分类的数量除以样本总数,可能是最流行的性能指标之一
然而,准确性并不总是给出模型对于特定问题表现如何的准确描述。假设您想要训练一个模型来检测注册到您网站的用户是否是潜在的机器人/骗子/欺诈者。在正常情况下,这样的用户可能只代表网站流量的一小部分,因此,您的数据集可能会描述这样一种情况,其中 99%的用户是普通用户,1%的用户是假冒的。在这种情况下,您可以通过这个简单的函数获得 99%的准确度:
def is_fake(user):
return False
准确性的问题是,当分类问题涉及偏斜类时,它无法提供模型实际性能的良好描述,即,具有非常不同分布的类,通常与异常检测问题或一般涉及罕见事件预测的问题相关。
对于这种情况,它通常有助于一种比查看整体准确性更细粒度的方法。为了简单起见,让我们考虑一个二元分类问题: y = 0 表示一个负的数据点,而 y = 1 表示一个正的数据点。我们的模型对每个数据点进行预测——要么是(负面预测),要么是
(正面预测)。我们可以根据预测值定义以下指标:
-
真阳性 ( TP ):标记为阳性和预测为阳性的数据点( y = 1 和
)
-
真阴性 ( TN ):标记为阴性和预测为阴性的数据点( y = 1 和
)
-
假阳性 ( FP ):标记为阴性但预测为阳性的数据点( y = 0 和
)
-
假阴性 ( FN ):标记为阳性但预测为阴性的数据点( y = 1 和
)
图 2-6
混淆矩阵的结构。在运行模型验证后,每个单元格报告符合所选类别的项目数
通常这些度量在一个混淆矩阵中可视化,其结构如图 2-6 所示。
有了这个新的形式主义,我们可以将模型的精度定义如下:
准确性是回答问题“可用项目的哪个部分被正确分类了?”
我们将精度定义为回答问题“预测为正的项目中有多少部分实际上是正的?”
而召回是回答问题“标记为肯定的项目中有多少部分被预测为肯定的?”
让我们将这两个新指标应用到前面显示的is_fake(user)
函数中。假设我们在一个包含 100 个用户的测试集上运行这个总是返回假的天真模型,其中 1 个是假的,99 个是正常的。因此,我们有
-
TP= 0
-
TN = 99
-
FP = 0
-
联合国 = 1
和
召回值为 0 显然表明分类器有问题,尽管总体准确率为 99%。请注意,精确度和召回率并不总是可测量的:在一些极限情况下,比如我们的朴素is_fake(user)
函数,分母可能是零——但这两个指标中至少有一个通常是可计算的。
您可以使用这两个额外的度量来更好地评估模型的性能,并优化具有偏斜类的分类问题的性能-如果需要,甚至会以牺牲整体准确性为代价。您还可以根据您的业务逻辑找到这两个指标之间的折衷。假设您的模型根据 X 射线图像预测患者是否患有癌症:您可以根据您对问题的回答优化其精确度或召回率,告诉健康患者他们患有癌症更糟糕,还是告诉癌症患者他们健康更糟糕?
如果您的模型从银行的摄像头图像中检测到潜在的入侵,您可能希望优化召回-如果真实入侵的成本非常高,那么确保检测到任何潜在的入侵可能是安全的,即使以更高数量的误报为代价。相反,如果您的模型向某个部门的所有员工发送关于某个系统上潜在流量峰值的通知,您可能希望精确度高于召回率—当我们非常确信存在峰值时发送通知,以防止向员工发送误报垃圾邮件。
有时,一个结合了精确度和召回率的度量标准被用来评估模型的性能: F1 分数被定义为精确度和召回率的调和平均值,并且经常被用作一个更细粒度的精确度度量标准:
总而言之,到目前为止,我们已经介绍了
-
神经网络背后的直觉,如何使用它们进行预测(前向传播),以及如何训练它们(反向传播)
-
如何评估训练过程的质量——防止欠适应和过适应的措施、标准化、规范化和特征选择
-
调试、测试、设计、打包和分发我们的机器学习模型的最佳实践是什么
-
如果我们的类别有偏差,或者我们想要检测异常情况,可以使用哪些附件性能指标来评估模型
现在,我们已经具备了开始动手编写代码的所有要素。
2.4 实施网络识别服装项目
如今,使用 TensorFlow 和 Keras 等库实现神经网络相对容易。我不会像对回归那样从头开始介绍 Python 中正向传播和反向传播的完整实现,但是即使您不太可能发现自己处于必须自己实现完整算法的情况下,我也强烈建议您尝试从头实现它,以确保您掌握了所有的直觉。毕竟,就在不久之前,开发人员还必须自己实现这些算法——我相信,我用 C++编写的 12 年之久的神经网络库仍然丢失在旧的谷歌代码门户的某个地方。即使现在可以用三行 Python 代码来完成模型的初始化、编译和训练,该框架也不会对数据进行规范化,而且 TensorFlow 和 Keras 提供的方法仍然需要一些关于算法如何工作的调整和知识,如果您想让您的模型在现实世界的应用程序中工作的话。
在这一节中,我们将介绍一个通常被认为是神经网络的新 hello 世界的例子:最初由 Zalando 上传的Fashion MNIST
数据集。传统的 MNIST 数据集多年来一直被用来向学生介绍机器学习,它包括一个带有手写和标签数字的大型图像列表。时尚 MNIST 数据集在原始问题的基础上增加了一点复杂性——你必须训练一个从图片中检测服装的模型。默认情况下,典型 TensorFlow+Keras 安装会提供时尚 MNIST 数据集,您可以像这样将其加载到笔记本中:
import tensorflow as tf
from tensorflow import keras
fashion_mnist = keras.datasets.fashion_mnist
(train_images, train_labels), (test_images, test_labels) = \
fashion_mnist.load_data()
数据集中包含了十种服装,但是它们的类不是直接作为字符串提供的。您可以用关联的类名初始化数组:
class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal', 'Shirt', 'Sneaker', 'Bag',
'Ankle boot']
通过查看数据,我们注意到训练集包含 60,000 幅图像,而测试集包含 10,000 幅图像,在这两种情况下,我们处理的都是 28x28 像素的黑白图像:
train_images.shape
# Output: (60000, 28, 28)
test_images.shape
# Output: (10000, 28, 28)
说到标签,它们的值在 0-9 的范围内,可以映射到我们的class_names
向量:
train_labels
# Output: array([9, 0, 0, ..., 3, 0, 5], dtype=uint8)
test_labels
# Output: array([9, 2, 1, ..., 8, 1, 5], dtype=uint8)
图 2-7
来自时尚 MNIST 数据集的图像直方图
当我们处理图像时,我们要做的第一件事是查看数据集中的一些图像,以获得关于颜色空间、范围和外观的提示:该数据集中的图像已经被修剪以仅包括服装项目,但是在现实世界的场景中,您可能会得到大型数据集,这些数据集在被馈送到神经网络之前需要某种形式的预处理(如修剪、缩小或颜色转换):
import matplotlib.pyplot as plt
plt.figure()
plt.imshow(train_images[0])
plt.colorbar()
plt.grid(False)
plt.show()
您将看到如图 2-7 所示的图片。我们正在处理黑白图像,关于每个像素的信息被编码在一个字节中,因此每个像素具有 0(黑色)和 255(白色)之间的值。对于对图像进行操作的模型,第一步是对其进行归一化,对于黑白图像,这通常通过应用将[0,255]范围转换为[0,1.0]范围的变换来完成:
def normalize(images):
return images / 255.0
train_images = normalize(train_images)
test_images = normalize(test_images)
关于色彩空间的快速注释。当谈到处理图像的模型时,如果您真的想提高性能,选择正确的颜色空间是非常重要的。虽然 RGB 是导出图像的最常见选项,但它不一定是训练模型的最佳格式。在为你的模型选择一个特定的颜色空间之前,问你自己这样一个问题:我想在这些图像中发现什么样的信息或模式?如果你的神经网络应该用于自动驾驶汽车,以识别交通灯的颜色,那么 RGB 是一个很好的选择。如果你想在背景下检测形状,黑白通常是更好的选择——它不仅使模型更简单、更快,而且更鲁棒,因为同一物体或形状的不同图片可能会根据照明或环境条件显示不同的颜色。其他应用程序使用更奇特的色彩空间可能会执行得更好。例如,如果你的模型应该识别房间内的照明条件,那么考虑亮度的色彩空间(如 YUV 或 YCbCr)可以比 RGB 或灰度表现得更好。在其他应用中,模式取决于图像的颜色或饱和度,考虑这些指标的色彩空间(如 HSL 和 HSV)可能是最佳选择。请记住,您选择的色彩空间会影响网络能够推断的模式。不仅如此,还要根据模型应该识别的内容为模型选择正确的数据源。如果你想对物体进行分类,来自光学相机的图像非常有用。相反,如果您想要检测人的存在,那么红外或热感相机可以提供更好的性能,因为光学相机的图像会有很大的可变性——一个人可以在房间的不同位置以不同的姿势站着、坐着或躺着,并且您也可以在同一房间中有多个人,而红外相机只会为您提供模型实际需要的信息:“图像中是否有大约 36–37°C 的人形热源?”在其他应用中,您可能希望依赖更多的输入:如果您还集成了麦克风或其他环境传感器的数据,则检测人的存在的模型会更加准确。
机器学习通常被描述为一个过程,在这个过程中,你向模型输入数据,模型会自己“学习”,但现实比这更复杂。选择正确的信息来源、收集数据、删除冗余信息、修整和转换数据、检测任何潜在的偏差来源,以及对数据进行标准化,这些实际上决定了模型 90%的性能。我们到目前为止所探索的数学,如今往往在库和框架中实现;它相对复杂,但是您不必从头开始实现它(即使这并不意味着您不需要理解这些模型是如何工作的)。如今真正重要的是你使用的数据的质量,以及你在编写第一行代码之前收集数据的能力。机器学习不像给模型输入数据,让模型自己学习。这更类似于企鹅喂养后代的方式——成年企鹅负责捕鱼、咀嚼和预消化食物,然后再喂给幼崽。
也就是说,让我们继续我们的衣服分类器。一个好主意是查看一组图像在数据集中的外观以及它们的分类:
plt.figure(figsize=(10,10))
# Plot the first 25 images and their
# associated classes in a 5x5 grid
for i in range(25):
plt.subplot(5,5,i+1)
plt.xticks([])
plt.yticks([])
plt.grid(False)
plt.imshow(train_images[i], cmap=plt.cm.binary)
plt.xlabel(class_names[train_labels[i]])
plt.show()
图 2-8
对时尚 MNIST 数据集的前 25 幅图像进行采样
您应该会看到如图 2-8 所示的图形。
如果分类看起来是正确的,并且数据是标准化的,那么让我们继续构建神经网络分类器。这通常在 Keras 中使用Sequential
模型来完成,该模型将多个定制层链接在一起。通常,处理图像分类问题的模型具有以下结构:
-
包含与每个图像的像素数一样多的单元的输入层。然而,我们已经看到,网络的输入层是单位的一维向量,而这里我们处理的是二维图像。因此,第一层通常是
Flatten
类型,它将二维图像“展开”成一维数组,这些数组可以传播到下一层。我们已经看到,时尚 MNIST 数据集中的图像是 28x28 像素的图像:这意味着我们的第一层将有 28 × 28 = 784 个单位。 -
输出图层的单元数量与我们想要检测的类别数量一样多。在我们的例子中,10 个类意味着输出层中的 10 个单元,具有最高激活值的单元是我们想要关联到特定数据点的单元。
-
输入层和输出层之间的可变数量的隐藏层,具有可变数量的单元。我们之前已经看到,增加中间单元和层的数量是提高模型精度的一种好方法,但是增加太多可能会导致过度拟合,通常可以通过调整单元和层的数量来克服这一问题,直到在训练集和测试集的精度之间达到令人满意的平衡,或者通过增加正则化率来使网络更加紧密地“锚定”我们将对输出层和中间层使用
Dense
Keras 层类型,它初始化一个层,使它的每个单元都连接到上一层和下一层中的每个单元。
综上所述,让我们继续编写初始化模型的代码:
model = keras.Sequential([
keras.layers.Flatten(input_shape=(28, 28)),
keras.layers.Dense(500, activation="sigmoid"),
keras.layers.Dense(200, activation="sigmoid"),
keras.layers.Dense(10, activation="softmax")
])
该代码定义了一个具有一个输入层、两个隐藏层和一个输出层的网络(keras.Sequential
)。第一层以我们归一化的 28 × 28 图像向量作为输入,将其转换为一维向量(keras.Flatten
)。两个隐藏层分别包含 500 和 200 个单元(您可以随意试验单元和隐藏层的数量,看看它是如何影响模型的)。他们使用一种sigmoid
激活功能——与我们目前探索的功能相同。输出图层有 10 个单元,与类的数量一样多。每个单元的值将表示给定输入图像属于该类别的概率。每当我们有多个类时,我们可能希望对输出层使用softmax
激活函数,并且我们希望将每个单元的值表示为概率/置信水平。
接下来,就像在回归模型中一样,我们希望编译这个模型,这样它就可以接受训练了:
model.compile(optimizer='adam',
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
# or loss="categorical_crossentropy",
metrics=['accuracy'])
挖掘这里发生的事情:
-
我们使用
adam
作为网络的优化器,这是一种基于一阶梯度的优化算法,于 2014 年首次提出,在过去几年中获得了相当大的人气,用于训练深度神经网络。我们已经在回归一章中介绍了其他优化器。其中许多算法——随机梯度下降(SGD)、nadam、RMSprop 等——也常用于神经网络。同样,掌握优化器的最好方法是阅读那些最常用的优化器,并试验哪一个对您的数据执行得更好。 -
然后,我们定义一个我们希望使用优化器最小化的成本函数(就像我们已经在回归的情况下看到的那样,Keras 框架将它们命名为损失函数而不是成本函数,但它们基本上意味着相同的东西)。虽然均方误差(或均对数平方误差)是线性回归问题的常见选择,但交叉熵函数是分类问题的常见选择,包括逻辑回归和通过神经网络的分类。交叉熵的概念非常接近我们在分类问题中分析过的成本函数
的类型。一般来说,在信息论中,同一组事件上的两个分布 p 和 q 之间的交叉熵表示将 p 转换为 q 所需的平均比特数(或信息片段)。如果 p 和 q 分别是我们对某一组数据点的预期值和预测值,那么交叉熵就可以直观地测量我们的预测值与预期值之间的“距离”——或者说我们平均需要改变多少位才能使我们的预测值与预期值相匹配。看待交叉熵的另一种方式是从概率的角度:你可以把它看作是对你的预测正确的可能性的一种度量。如果要为真/假预测构建模型,通常会使用二元交叉熵损失函数。在我们的例子中,我们希望对多个类进行预测,因此分类或稀疏分类交叉熵函数通常是一种流行的选择。
-
与回归的情况一样,我们希望定义一个或多个额外的指标作为“健康”指标,以确保模型确实在学习,而不是根据提供的成本函数过度拟合数据点。在这种情况下,就像我们在回归的情况下所做的那样,我们使用精确度,但请记住,根据数据的分布(尤其是在倾斜数据集的情况下)以及您想要在假阳性和假阴性之间实现的权衡,您还可以使用精确度和召回率或任何其他指标。
然后,就像我们在回归案例中看到的那样,我们使用fit
方法在训练集上训练我们编译的模型:
history = model.fit(train_images, train_labels, epochs=10)
在这种情况下,我们指定对数据点进行 10 次迭代。同样,请记住,历元的数量可以决定您的模型是否会欠拟合、过拟合或“恰好”拟合数据,因此您可能希望查看笔记本的输出,以了解模型的性能如何随历元而变化:
Epoch 1/10
1875/1875 [======] - 6s 3ms/step - loss: 0.5423 - accuracy: 0.8091
Epoch 2/10
1875/1875 [======] - 6s 3ms/step - loss: 0.3781 - accuracy: 0.8621
Epoch 3/10
1875/1875 [======] - 7s 4ms/step - loss: 0.3396 - accuracy: 0.8755
Epoch 4/10
1875/1875 [======] - 7s 4ms/step - loss: 0.3144 - accuracy: 0.8842
Epoch 5/10
1875/1875 [======] - 9s 5ms/step - loss: 0.2956 - accuracy: 0.8912
Epoch 6/10
1875/1875 [======] - 7s 4ms/step - loss: 0.2805 - accuracy: 0.8961
Epoch 7/10
1875/1875 [======] - 7s 4ms/step - loss: 0.2649 - accuracy: 0.9014
Epoch 8/10
1875/1875 [======] - 7s 4ms/step - loss: 0.2508 - accuracy: 0.9062
Epoch 9/10
1875/1875 [======] - 7s 4ms/step - loss: 0.2387 - accuracy: 0.9105
Epoch 10/10
1875/1875 [======] - 8s 4ms/step - loss: 0.2303 - accuracy: 0.9128
解释您的指标的一些常见经验法则:
-
很重要的一点是,你的损失/成本函数在各个时期持续下降。如果它没有明显地趋向于零,那么你可能想要正常化/改善你的训练数据。如果它上下波动,那么成本函数可能会有一些“颠簸”——要么检查您的数据,要么调整学习率、正则化率或优化器。如果您在某个点之后没有注意到很大的改进,这意味着成本函数已经提前收敛,您可以减少历元的数量,或者您可能会遇到过度拟合的问题。
-
虽然成本函数预计会持续下降,但您的次要指标(准确度、精确度或召回率)预计会持续上升。如果他们没有,那么你可能要调查可能的过度拟合问题或调整学习/调整率。
您还可以绘制模型的精度在训练时期内的变化情况:
epochs = history.epoch
accuracy = history.history['accuracy']
fig = plt.figure()
plot = fig.add_subplot()
plot.set_xlabel('epoch')
plot.set_ylabel('accuracy')
plot.plot(epochs, accuracy)
一旦我们对训练阶段的性能指标感到满意,就该在测试集上评估新训练的模型了:
图 2-9
超过 10 个训练时期的模型精确度的进展
test_loss, test_acc = model.evaluate(test_images, test_labels, verbose=2)
您可能会看到如下输出:
313/313 - 1s - loss: 0.3185 - accuracy: 0.8871
绘制各时期的精度图应该会得到如图 2-9 所示的图表。
这意味着我们的模型在 313 个项目的测试集上有 88.71%的概率猜对这个类。如果我们将其与之前的输出进行比较,这比上一次训练迭代所达到的精度低 2.5%。在真实情况下,由您(或项目负责人)决定这样的结果是否足够好。如果测试集的准确性与训练集的准确性相差太多,那么,您可能需要再次调查过度拟合。增加测试集中的样本数量也是一个很好的做法,这样可以得到更有统计学意义的观察结果。
现在,看一眼测试集中的一些图像,看看神经网络在这些图像上的表现是一个好主意。让我们首先定义几个效用函数,将测试集的一些预测显示在网格上,每个元素包含测试图像、预期标签、预测标签和模型预测该标签的置信度:
import numpy as np
import matplotlib.pyplot as plt
# Plot the image, the predicted/expected label
# and the confidence level
def plot_image_and_predictions(prediction, classes, true_label, img):
plt.grid(False)
plt.xticks([])
plt.yticks([])
plt.imshow(img, cmap=plt.cm.binary)
predicted_label = int(np.argmax(prediction))
confidence = 100 * np.max(prediction)
color = 'blue' if predicted_label == true_label else 'red'
plt.xlabel('{predicted} {confidence:2.0f}% ({expected})'.format(
predicted=classes[predicted_label],
confidence=confidence,
expected=classes[int(true_label)]), color=color)
# Plot a bar chart with the confidence level of each label
def plot_value_array(prediction, true_label):
plt.grid(False)
plt.xticks([])
plt.yticks([])
thisplot = plt.bar(range(len(prediction)), prediction, color="#777777")
plt.ylim([0, 1])
predicted_label = np.argmax(prediction)
thisplot[predicted_label].set_color('red')
thisplot[true_label].set_color('blue')
# Plot the first N test images, their predicted and expected label.
# It colors correct predictions in blue, incorrect predictions in red.
def plot_results(images, labels, predictions, classes, rows, cols):
n_images = rows * cols
plt.figure(figsize=(2 * 2 * cols, 2 * rows))
for i in range(n_images):
plt.subplot(rows, 2 * cols, 2 * i + 1)
plot_image_and_predictions(predictions[i], classes,
labels[i], images[i])
plt.subplot(rows, 2 * cols, 2 * i + 2)
plot_value_array(predictions[i], labels[i])
plt.show()
# predictions will contain the predicted values for the test set
predictions = model.predict(test_images)
# Plot the predictions for the first 25 values of the test set
plot_results(images=test_images, labels=test_labels, classes=class_names,
predictions=predictions, rows=5, cols=5)
您可能会看到如图 2-10 所示的图形。这种应用于测试集的可视化有助于您了解网络如何对不在训练集中的图像执行操作,并且您可以使用它来找出可以帮助您改进模型的常见模式,例如通常被错误标注或具有“接近”误差范围的项目类别。您可能希望使用这种可视化来细化您的输入数据,改进您的图像预处理管道,或者使用目前为止看到的策略(调整学习率、标准化率、神经元数量、时期数量、成本函数等)来调整模型。)来提高业绩,直到你满意为止。
图 2-10
绘制测试集中前 25 幅图像的预测类别,以及预期标签和分类置信度
一旦您对您的模型感到满意,不要忘记保存它。该过程与我们之前看到的保存张量流回归模型的过程相同:
def model_save(model_dir, labels, overwrite=True):
import json
import os
# Create the model directory if it doesn't exist
os.makedirs(model_dir, exist_ok=True)
# The TensorFlow model save won't keep track of the
# labels of your model. It's usually a good practice to
# store them in a separate JSON file.
labels_file = os.path.join(model_dir, 'labels.json')
with open(labels_file, 'w') as f:
f.write(json.dumps(list(labels)))
# Then, save the TensorFlow model using the save primitive
model.save(model_dir, overwrite=overwrite)
model_dir = '/home/user/models/fashion-mnist'
model_save(model_dir, labels=class_names)
同样,您可以从您的应用程序中加载保存的模型,而无需再次经历培训阶段:
def model_load(model_dir):
import json
import os
from tensorflow.keras.models import load_model
labels = []
labels_file = os.path.join(model_dir, 'labels.json')
if os.path.isfile(labels_file):
with open(labels_file) as f:
labels = json.load(f)
m = load_model(model_dir)
return m, labels
model, labels = model_load(model_dir)
恭喜你训练并保存了你的第一个用于图像分类的神经网络!
2.5 卷积神经网络
时尚 MNIST 数据集非常适合引入神经网络,但它比许多真实世界的图像数据集更简单。该网络是在一组预处理的 28x28 单色图像上训练的,所有图像都包含应该被识别的物品——在许多现实世界的场景中,你通常不会处理这样整齐修剪的数据集。理想情况下,我们希望构建足够健壮的模型,以便在我们输入的一些图像的特征与模型被训练时的特征略有不同时也能对项目进行分类,特别是,我们希望我们的模型对修剪、旋转和少量模糊或颜色/亮度变化具有鲁棒性。
卷积神经网络(或 CNN)更接近人类大脑处理图像的方式。当他们对周围环境进行视觉分类或解释时,我们的大脑不会简单地将通过视神经传递的原始亮度和颜色信号均匀地传递到视觉皮层的所有区域。这样的组织需要大量的生物能量,因为大脑皮层的所有输入神经元一直都是活跃的,而且还需要大量的下游连接。相反,输入信号最初由视觉皮层的一个区域进行预处理,这个区域被称为感受野【16】【17】。感受野就像一个过滤器,对一些输入信号进行预处理。它丢弃不需要的信息;它根据例如环境亮度和方向来调整/标准化数据;最后,它识别一些特征或模式(由例如边缘、发光区域或空间特征确定),这些特征或模式应该激发下游的一些特定神经元。大多数哺乳动物的感觉网络旨在检测模式并快速检测它们,专注于周围环境中最相关的元素,同时丢弃不需要的信息,并且它们被建模为在亮度、距离和方向变化的情况下也能足够稳健地工作。对灵长类动物的研究已经证明,某些感受野负责在不同的亮度和方向情况下过滤和规范感觉信号,当在不同的亮度条件下呈现给同一物体时,这些感受野向下游神经元传递的信号是相似的——换句话说,动物视觉皮层中的感受野负责规范数据,并确保视觉分类的过程独立于环境的亮度【18】。
图 2-11
卷积神经网络的典型架构。该图像显示了其卷积层(用于特征提取)、汇集层(用于降维)、展平层和下游全连接神经网络(用于分类)(鸣谢:走向数据科学[19])
CNN 可以被看作是这一原则的人工应用。在 CNN 中,一组过滤器被应用于原始图像,以便提取诸如形状和颜色区域之类的特征,并降低初始复杂度。这些特征然后被输入传统的神经网络。由于神经网络对提取的特征集而不是原始像素集进行操作,这些网络通常比相同大小但没有卷积层的等效神经网络在分类图像方面表现更好,因为它们更善于捕捉图像中区域之间的空间和时间依赖性。此外,当输入样本的大小增加时,CNN 的伸缩性更好。我们在前面的例子中设计的网络的输入单元数量与图像中的像素数量完全相同。使网络能够处理更大的图像需要缩小图像或者增加输入层中的单元数量,这反过来通常需要重新训练模型。相反,在 CNN 中,可以简单地调整卷积层/滤波器,以处理不同大小的图像,通常不需要改变下游网络的架构。卷积层的作用是降低图像的维度,以便更容易处理图像,更容易缩放模型,而不会丢失对获得良好预测至关重要的任何特征。CNN 通常由三种类型的组件组成:
图 2-12
原始图像上卷积层中的核张量/滤波张量的移动(来源:数据科学[19])
-
一个或多个卷积层,其工作是通过称为滤波器或内核的矩阵/张量对输入图像迭代应用变换。卷积层的目的是通过不仅查看每个像素中存储的信息,而且查看每个像素与其“邻域”之间的关系(例如,它是否在边上)来捕获输入图像中的高级特征。周围的像素是否有不同程度的颜色/亮度?).随着我们添加越来越多的卷积层,所提取的特征的复杂度也在增加。第一层通常会捕获边缘、颜色渐变和方向等低级特征,而下游层会发现更复杂的特征,如对象、大小、距离等。
-
一个或多个池层,其输入单元通常链接到卷积层的输出单元。他们的工作是进一步降低输入数据的维度,并选择上游卷积层提取的主要特征,特别是那些对旋转或平移等变换不变的特征——池层的目的在功能上类似于我们之前分析的主成分分析算法。
-
最后,从原始数据中提取的特征矩阵/张量被展平,并被馈送到将执行分类过程的完全连接的神经网络中。
这种网络的最终高层架构如图 2-11 所示。下面我们来逐一分析一下它的层次。
2.5.1 卷积层
输入图像通常被提供为 w × h × c 矩阵/张量,其中 w 和 h 分别是其宽度和高度,而 c 是其颜色空间的深度(在单色图像的情况下为 1,在 RGB/HSV/YUV 等的情况下为 3。).一个滤波器或内核矩阵/张量 K 大小为m×n×c,其中 m < h 和 n < h ,要么在层中静态编码,要么动态计算。 K 在整个图像上移动,如图 2-12 所示。在每次迭代中,如果一行中有更多的像素要处理,则内核从左向右移动,否则从上到下移动(在下一行中将方向从右向左改变),直到处理完整个图像。在每次迭代中, K 、 k 、 00 的左上元素将与输入矩阵的( i 、 j )像素 A 、Aij对齐,其中 0 ≤ i < h 让我们定义Aij为 A 被 K 覆盖的子集:
图 2-13
单色输入图像 A 和内核 K 之间的 2D 卷积运算的例子
A ij 和 K 都是大小为m×n×c的张量——即使前面的公式为了简单起见将每个像素显示为单个数字,因此Aij显示为 2D 矩阵。然后,我们可以将内核覆盖的图像子集与内核本身之间的卷积运算Aij∫K定义为Aij和 K :
的元素之间的逐元素乘积之和
这种操作非常类似于我们在反向传播算法中看到的元素矢量积(或 Hadamard 积), 2D 的例子如图 2-13 所示。在 Python 中,您可以这样写:
def conv_product(A, K):
conv = 0.0
for x in range(len(K)):
for y in range(len(K[0])):
for z in range(len(K[0][0])):
conv += A[x][y][z] * K[x][y][z]
return conv
应用图 2-12 :
所示的“蛇形”运动,可以计算出整幅图像 A 与核 K 之间的卷积张量 Conv
在 Python 中:
def submatrix(A, i, j, m, n):
# Calculate the submatrix of a matrix A starting from the
# element (i, j) up to (i+m, j+n)
return [
[
A[i][j]
for j in range(j, j+n)
]
for i in range(i, i+m)
]
def conv(A, K):
# The result will be a (w-m)*(h-n) matrix
return [
[
conv_product(submatrix(A, i, j, len(K), len(K[0])), K)
for j in range(len(A[0])-len(K[0])+1)
]
for i in range(len(A)-len(K)+1)
]
这种内核和过滤器已经在计算机视觉中使用了很长时间。其中最流行的可以说是索贝尔图,或者索贝尔-费尔德曼算子【20】。这个滤镜实际上由两个 3 × 3 的矩阵组成, S x 和 S y ,分别用于计算 x 和 y 维度的亮度/颜色渐变的近似值:
这些矩阵与原始图像之间卷积运算的结果分别近似为图像在特定点周围的 x 和 y 颜色梯度:
图 2-14
应用于图像的 Sobel 内核示例。右图像中的每个像素表示 Sobel 映射和原始图像中的相关像素之间的卷积运算的幅度。物体边缘的像素比其他像素更亮
这个向量的模表示图像中特定点的梯度向量的大小——值越高,该点越有可能属于图像中对象的边缘:
相反,矢量的相位确定了特定点的渐变方向——这可以用来判断物体位于边缘像素的哪一边:
卷积层中使用的核类似于 Sobel 映射(有些甚至可以使用 Sobel 映射进行边缘检测),就像 Sobel 映射一样,它们可以用于检测图像中的边缘和梯度等特征(参见图 2-14 中的示例)。
最后,除了选择核及其大小之外,卷积层中还有两个可以调整的系数如下:
-
Stride :它决定了在卷积乘积的每次迭代中,内核应该在图像上移动多少。在我们在这一段看到的例子中,内核的步幅是 1——我们在图像上一次移动一个位置——这也是最常用的值。较大的步幅将导致较小的输出张量。只要记住非常大的值有更大的机会丢弃有用的信息,就可以使用更大的步幅来执行更大的维度缩减。
-
Padding: The original image can either be processed directly through the convolutional operation or padded with zeros before the operation. In this paragraph, we have shown examples of valid padding (or no padding)—the original matrices/tensors were not padded before applying the convolution. If instead you add two rows of zeros to the top and bottom and two columns of zeros to the left and right of the images, you will be performing what is called same padding . Valid padding performs dimensional reduction as well as feature extraction (the output tensor will be smaller than the input), while same padding performs feature extraction but keeps the same dimensions.
图 2-15
应用于输入 2D 矩阵的 2 × 2 最大和平均池操作的示例结果
因此,您可以调整步幅和填充来调整在将输出张量传递给下一层之前,该层应该减少多少输入图像的尺寸。跨距= 1 且填充相同的配置不会导致尺寸的实际减少,在这种情况下,您可能希望在下游的池层上完全执行减少。
2.5.2 汇集层
卷积层的输出通常连接到一个汇集层。汇集层的目的是减少张量的维数,同时可能不丢失正确分类所需的任何相关信息。此外,提取相对于旋转和位置不变的图像特征是有用的,这使得模型对于图像变换更加鲁棒。最后,它作为一个降噪器,消除或减少与周围环境太不相似的噪声像素对模型的影响。
与卷积层类似,池层通过在图像上移动一个 m × n 过滤器来工作( m 和 n 不一定要与前一层使用的内核的尺寸匹配)。不同之处在于,这一次过滤器对图像的每个 m × n 部分应用了减少/分组功能。两个池函数是最常用的:
-
Max pooling :选择输入张量底层子集中的最大/最高值。
-
平均池:选择输入张量的底层子集中的平均值。
图 2-16
一个全连接神经网络上的辍学迭代的例子(学分:[21])
图 2-15 显示了这两种操作的示例。Max pooling 通常比 average pooling 更受欢迎,因为它在降噪方面更有效,它只取滤波器下最高数据点的值,而丢弃其他值。
卷积层和池层的组合实际上构成了一个完整的卷积层。您可以在模型中堆叠许多这样的图层,每个图层都将检测越来越高级别的要素-但是,请记住,添加更多图层也会增加模型定型的计算需求。因此,汇集层的输出可以附加到另一个卷积层或平坦层(“将矩阵/张量展开成 1D 向量”),该平坦层又将它馈送到完全连接的神经网络。
2.5.3 全连接层和漏接
CNN 的最后一部分是完全连接的神经网络,展示了与我们之前看到的相同的架构。它将从卷积层获得展平张量作为输入,并且它将在输出层中具有与我们希望模型识别的类别数量一样多的单元,每个单元的激活值表示特定图像属于特定类别的概率。
dropout 技术通常应用于 CNN 的全连接层,以防止过度拟合。我们已经分析了防止过拟合的几种方法(添加偏差单元、从训练集中移除冗余项、减少参数或单元的数量、调整正则化率和学习率、应用主成分分析等)。).辍学在一个稍微不同的层面上起作用。它考虑到,在具有许多神经元和相对较小的训练集的神经网络中,过度拟合主要来自于对最终分类贡献过多或过少的单个神经元,最终对模型的性能产生不利影响。在训练阶段应用参数 p 的丢失迭代将以概率 p 从网络中移除某个神经元,并触发没有这些神经元的训练迭代,如图 2-16 所示。通过这样做,我们迫使网络在不依赖单个神经元(或一组神经元)进行预测的情况下应对失败。相反,在缺少一些单元的情况下,网络将更多地依赖于层中神经元之间的共识。
2.5.4 用于识别水果图像的网络
让我们通过挑选一个比以前的时尚 MNIST 更复杂的数据集,来看一个用于图像识别的卷积神经网络的实际例子。例如,让我们选择 Kaggle [22]的 Fruits 360 数据集,但请记住,本章中报告的信息可用于在任何图像数据集上训练模型。
水果 360 数据集包含大约 90,000 个水果图像,分为 131 个类别,每个类别的大小为 100 × 100 像素。从 Kaggle 下载数据集的 zip 文件并提取它—在接下来的例子中,我将假设数据集存储在“/datasets/fruit-360”下。您会注意到数据集具有这种结构(如果数据集具有这种结构,您通常可以发现高质量的数据集):
fruit-360
\-> Training
\-> Apple Braeburn
\-> image01.jpg
\-> image02.jpg
...
\-> Apple Crimson Snow
...
\-> Test
\-> Apple Braeburn
\-> image01.jpg
\-> image02.jpg
...
\-> Apple Crimson Snow
...
我们有一个用于训练图像的目录和一个用于测试图像的目录,每个目录包含一个用于每个类的目录,每个类目录包含与该类相关联的图像。这通常被认为是构建图像数据集的良好实践,它使其他开发人员和应用程序可以轻松使用它。
现在,让我们继续导入探索数据集和训练模型所需的模块:
import os
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras import Sequential, layers
from tensorflow.keras.preprocessing.image import ImageDataGenerator
让我们还定义一个实用函数来从数据集中提取类名:
def parse_classes(directory):
"""
Get the classes of a dataset directory as a vector of strings.
"""
return sorted([
d for d in os.listdir(directory)
if os.path.isdir(os.path.join(directory, d))
])
classes = parse_classes(train_dir)
和一些用于定义模型的变量:
train_dir = os.path.expanduser('~/datasets/fruits-360/Training')
test_dir = os.path.expanduser('~/datasets/fruits-360/Test')
img_size = (100, 100)
channels = 3 # RGB
epochs = 5 # Number of training epochs
batch_size = 64 # Batch size
批次大小是模型更新前处理的图像数量,可以通过调整来调整模型的性能。
对于图像识别来说,一个好的实践是在图像集上使用 TensorFlow 的ImageDataGenerator
类。生成器将应用几个随机变换(旋转,裁剪,缩放等)。)添加到输入影像中,并生成一组新的(混洗的)影像,在对与数据集中提供的原始影像相比被旋转、剪切、模糊、翻转或缩放的影像进行分类时,这些影像可用于使您的模型更加稳健:
train_generator = ImageDataGenerator(rescale=1/255,
# Rotate the images
rotation_range=40,
# Cut the images
shear_range=0.2,
# Zoom the images
zoom_range=0.2,
# Flip the images
horizontal_flip=True,
fill_mode='nearest')
test_generator = ImageDataGenerator(rescale = 1/255)
# Output:
# Found 67692 images belonging to 131 classes.
# Found 22688 images belonging to 131 classes.
需要注意一些事情:
-
rescale
操作对图像进行标准化——每个像素都有范围为[0,255]的数据,我们希望将它映射到范围[0,1]。 -
将所有花哨的变换应用于训练集是一个好主意,但测试集通常只是重新缩放。
图 2-17
Fruits 360 数据集的训练集图像示例
让我们来看看这些图片的样子:
# Take the first batch of the training set
batch = train_data.next()
# Initialize the plot
plt.figure(figsize=(10,10))
for i in range(min(25, len(batch[0]))):
# The first item of batch contains the raw image data
# The second element contains the labels
img = batch[0][i]
label = classes[np.argmax(batch[1][i])]
plt.subplot(5,5,i+1)
plt.xticks([])
plt.yticks([])
plt.grid(False)
plt.imshow(img, cmap=plt.cm.binary)
plt.xlabel(label)
plt.show()
输出可能看起来如图 2-17 所示。
现在是最后定义和编译模型的时候了:
model = Sequential([
# First convolutional layer
layers.Conv2D(filters=32,
kernel_size=(3,3),
strides=(1,1),
padding='valid',
activation='relu',
input_shape=(*img_size, channels)
),
# First pooling layer
layers.MaxPooling2D(pool_size=(2,2)),
# Second convolutional layer
layers.Conv2D(filters=64,
kernel_size=(3,3),
strides=(1,1),
padding='valid',
activation='relu',
input_shape=(*img_size, channels)
),
# Second pooling layer
layers.MaxPooling2D(pool_size=(2,2)),
# Flatten output before feeding it to the network
layers.Flatten(),
# Neural network input layer
layers.Dense(units=100, activation="sigmoid"),
# Link dropout with 15% probability
layers.Dropout(0.15),
# Neural network hidden layer
layers.Dense(units=200, activation="sigmoid"),
# Link dropout with 15% probability
layers.Dropout(0.15),
# Neural network output layer
layers.Dense(len(classes), activation="softmax")
])
model.compile(loss='categorical_crossentropy',
optimizer='adam',
metrics=['accuracy'])
在这些方面,我们定义了一个 CNN,它具有两对卷积/池层,连接到一个全连接神经网络,该网络具有一个输入层、一个隐藏层和一个输出层。让我们仔细看看这些层。
首先,卷积层被定义为一个Conv2D
对象——2D,因为在这种情况下,我们正在对 2D 图像进行操作,但请记住Conv1D
和Conv3D
也存在。filters
指定应用于输入的过滤器数量——模型将学习哪些过滤器从图像中提取最相关的特征。如果构建一个具有多个卷积层的模型,通常会随着网络的深入而增加过滤器的数量-第一层上的过滤器将突出显示低级特征(如边缘和亮度区域),而下游的层将使用突出显示高级特征(如形状和边界)的过滤器。
kernel_size
参数定义了要使用的过滤器的大小——在这种情况下,我们将使用简单的 3 × 3 内核——而strides
定义了过滤器在每次迭代中将在图像上移动多少;我们在 x 和 y 方向上坚持一个像素。padding
定义输入是否应该填充——同样,valid
实际上意味着不填充(即执行维度缩减),而same
将填充输入以保持输出上的维度不变。卷积层具有激活功能,就像神经层的单元一样。relu
( 整流线性单元)通常是最常用的选项:给定一个输入 x ,它简单地返回 max( x ,0),但有时也可以使用其他激活函数。最后,我们将每个输入元素的大小指定为(宽度、高度、通道)。
卷积层然后连接到池层—在这种情况下,我们使用一个最大池层。pool_size
参数指定输入上的池应该有多大——在这种情况下,我们使用 2 × 2 池,这意味着输入上的每个 2 × 2 像素正方形将被映射到输出上的一个元素,因此维数减少了 4 倍。然后,我们连接另一对卷积+池层,尝试从输入中提取更多的特征,然后我们将最后一个池层连接到一个Flatten
层,该层将一个 n 维输入“解包”成一个一维数组,该数组可以输入到完全连接的神经网络的输入中。
然后,我们使用前一个示例中探索的结构定义完全连接的网络——输出层具有与我们想要检测的类的数量一样多的单元,同时您可以随意试验中间层和单元的数量,以查看它如何影响性能。我们还引入了两个Dropout
层,分别位于输入层和隐藏层之间以及隐藏层和输出层之间,其率= 0.15——也就是说,在训练期间,与神经元的连接有 15%的可能性被切断。请记住,退出逻辑可以非常有效地防止过度拟合,并有助于使模型更加稳健,减少对单个神经元贡献的依赖,但过高的退出率将对其准确性产生不利影响,因为在训练阶段,太多的神经元将被淘汰。最后,我们使用categorical_crossentropy
损失函数(我们希望对属于多个类别的项目进行分类)、?? 优化器以及针对accuracy
的优化来编译模型。
现在我们已经定义了 CNN,是时候训练它并验证它了。当我们使用图像数据生成器类时,可以通过fit_generator
方法而不是通常的fit
将训练和验证组合在一起:
history = model.fit(
train_data,
steps_per_epoch=train_data.samples/batch_size,
validation_data=test_data,
validation_steps=test_data.samples/batch_size,
epochs=epochs
)
是时候去给自己泡杯咖啡或茶了——因为我们正在训练一个比我们之前在时尚 MNIST 上训练的香草神经网络有更多层和更多图像的模型,这个阶段可能需要 30 到 90 分钟才能完成,具体取决于机器的能力:
Epoch 1/5
loss: 2.6444 - accuracy: 0.3622 - val_loss: 1.5080 - val_accuracy: 0.7355
Epoch 2/5
loss: 0.7892 - accuracy: 0.8084 - val_loss: 0.9297 - val_accuracy: 0.8696
Epoch 3/5
loss: 0.3591 - accuracy: 0.9142 - val_loss: 0.2119 - val_accuracy: 0.9212
Epoch 4/5
loss: 0.2093 - accuracy: 0.9474 - val_loss: 0.0216 - val_accuracy: 0.9448
Epoch 5/5
loss: 0.1590 - accuracy: 0.9570 - val_loss: 0.0087 - val_accuracy: 0.9573
结果精度历史如图 2-18 所示。
然后,我们可以继续分析训练时期内的精度进展。
图 2-18
在 5 个时期内模型精度的进步
epochs = history.epoch
accuracy = history.history['accuracy']
fig = plt.figure()
plot = fig.add_subplot()
plot.set_xlabel('epoch')
plot.set_ylabel('accuracy')
plot.plot(epochs, accuracy)
您会注意到训练集和测试集的准确率都非常高(> 95%),远远高于我们之前涉及简单回归或普通神经网络的示例。考虑到这一次我们有 131 个可能的输出类,这是一个相当令人印象深刻的成就,它显示了如何向神经网络添加一个或多个卷积层,并利用 dropout 等机制来防止过度拟合,可以有效地提高模型的性能。您还可以使用evaluate
函数在测试集上评估模型的性能,就像前面的例子一样,因为它也支持生成器:
model.evaluate(test_data)
最后,我们可以使用模型进行简单的预测,例如,我们可以从测试集中获取一些图像:
test_batch = test_data.next()
test_images = test_batch[0]
test_labels = test_batch[1]
test_img = test_images[0]
expected_class = classes[np.argmax(test_labels[0])]
predicted_class = classes[np.argmax(model.predict(np.asarray([test_img])))]
print(f'Expected class: {expected_class}.\n' +
f'Predicted class: {predicted_class}')
我们还可以对测试集中的一些图像运行该模型,并使用前面示例中定义的plot_results
函数绘制它们的预期和预测类以及预测的置信水平:
plot_results(
images=test_images,
labels=[np.argmax(label_values) for label_values in test_labels],
classes=classes,
predictions=predictions,
rows=6, cols=3
)
你可能会看到如图 2-19 所示的图形。最后一步,不要忘记保存你的模型(使用我们在前面的例子中定义的model_save
函数);否则,你将不得不再次经历整个训练阶段!
现在,您应该拥有了训练图像识别神经网络的所有基本工具,我们可以将重点从如何在一些样本数据集上构建神经网络转移到如何收集图像以用于我们自己的应用程序。
图 2-19
针对测试集中的一些图像评估模型
三、树莓派的计算机视觉
既然我们已经很好地理解了如何使用 TensorFlow 构建机器学习模型,那么是时候将我们的知识付诸实践,训练一个可以识别房间中人的存在,并且可以使用一些廉价硬件在树莓 Pi 上运行的模型了。
Raspberry Pi 可以说是过去十年中开发的最成功的信用卡大小的片上系统(SoC)。其紧凑的外形、灵活性和实惠的价格(价格从微型单核 Raspberry Pi Zero 的约 10 美元到 8GB RAM 的四核 Raspberry Pi 4 的约 80 美元)使其成为许多物联网项目的一个非常有吸引力的选择。当谈到机器学习应用程序时,它肯定不是一匹马力强劲的马(如果你正在寻找一台更强大的嵌入式机器来训练更复杂的模型,你可能会选择 NVIDIA Jetson boards 等解决方案),我的建议通常是在你的笔记本电脑或更强大的机器上训练你的模型,但一旦你训练了一个不太复杂的模型,Raspberry Pi 肯定是一个很好的预测候选人。然而,请注意,在运行 TensorFlow 代码时,功能最弱的选项(如 Raspberry Pi Zero 和 Raspberry Pi A+)可能会遇到一些延迟,但我已经在 Raspberry Pi 3 和 4 设备上运行了许多模型,只要模型不太复杂,我就没有遇到任何问题。
在这一章中,我们将看到如何使用 Raspberry Pi 和廉价的红外摄像机来构建一个实时模型,以识别房间中的人。虽然前几章中探讨的许多例子都涉及到根据光学相机的“正常”图像训练的模型,但根据我的经验,在小环境中检测人的存在是一项由红外相机更好地执行的任务。如果你想到这一点,人们在一个房间里可能有许多站着或坐着的方式,你也可能有不同数量的人在房间里,在离你的相机任意的距离,在任意的亮度条件下。这使得在光学图像上建立一个鲁棒的存在检测模型的任务非常具有挑战性——该模型必须在尽可能代表真实环境所有可变性的庞大数据集上进行训练,并且它可能有许多层来辨别所有可能的模式,这使得它容易过度拟合。红外摄像机更适合这项任务。由于红外摄像机可以检测到其前方任何物体的温度梯度变化,因此它对亮度条件的变化不敏感,对人的位置变化也不敏感。我使用这种方法遇到的唯一问题是当环境过于温暖时——如果人体温度在 36–37°C 左右,环境较冷,红外摄像机是检测人的非常好的工具,但如果环境温度与人体温度相同,那么梯度温度不足以检测人的存在——但如果您房间的温度通常低于 36 度,那么您可以继续使用这种方法!然而,作为后续措施,您也可以轻松地将本章中说明的过程应用于光学图像,这可能需要更大的数据集、更长的训练阶段和更多层的 CNN,但过程完全相同。本章中说明的过程可以很容易地扩展到任何需要数据收集、标记、训练模型以及部署该模型进行实时预测的应用程序。
该项目包括四个阶段:
-
准备硬件和软件。
-
构建逻辑,定期从红外摄像机捕获快照,将其规范化,并将其存储在某个地方。
-
给图片贴上标签(检测到存在/没有检测到存在),并在上面训练一个模型。
-
在 Raspberry Pi 上部署该模型,并针对新捕获的图像定期运行该模型,以检测房间中是否有人。可选地,我们可以添加一些额外的逻辑,在模型运行时运行一些自动化部分(例如,当有人进入/离开房间时打开/关闭灯,或者如果检测到存在但我们不在家时收到移动通知)。
3.1 准备硬件和软件
本章中的例子已经在 Raspberry Pi 上进行了广泛的测试,但是它们在任何基于 Linux 的 SoC 上只需很少的修改或者不需要修改就可以很好地工作。
准备硬件
您可以使用任何 Raspberry Pi [23]设备来捕捉图像,部署您的模型,并使用它进行预测。然而,如前所述,低功耗设备(如 Raspberry Pi Zero)可能会经历更多的延迟——即使我已经成功地将人员检测模型部署到 Raspberry Pi Zero,但在捕捉图像或进行预测时,我无法获得低于 2-3 秒的基本延迟。然而,任何 Raspberry Pi 3 或更高版本都应该为基本的机器学习项目提供流畅的性能。
所以,为了开始,你需要
图 3-1
Pimoroni MLX90640 热感摄像机分线点
-
Raspberry Pi 或任何类似的基于 Linux 的 SoC。
-
一个空的 micro SD 卡(最好 16GB 以上)。检查你的 SD 卡的质量和速度也是一个好主意——根据我的经验,超快的 SanDisk 卡是闪存 Raspberry Pi 操作系统的好选择,但任何足够快和足够强大的东西都应该可以完成这项工作。
-
红外摄像机。本章中的示例将基于基于 Pimoroni MLX90640 的热感相机分线点 24 ),这是一款相对便宜的 24x32 热感相机,在捕捉几米深度的热梯度方面表现出色,但任何热感相机或红外相机都应该可以工作。
如果您使用 MLX90640 分线摄像机或除 USB 摄像机或原生 PiCamera 之外的任何摄像机,请注意硬件协议。这一突破超越了 I 2 C 协议。当谈到 Raspberry Pi、Arduino 和其他物联网设备的电子设备时,您通常会发现三种流行的硬件协议:
-
I 2 C
-
SPI(串行外设接口)
-
直接 GPIO
直接 GPIO 基本上意味着您的设备和主设备(Raspberry Pi、Arduino、ESP 等)的引脚之间的直接映射。).对于引脚数和传输速率较低的简单设备,这通常是一个受欢迎的选择,而吞吐量较高的设备通常选择基于总线的接口,即 I 2 C 和 SPI 通常是该领域最受欢迎的协议。这两种总线接口的高级比较如图 3-2 所示。 I 2 C 最初是由飞利浦半导体在 1982 年开发的,它已经存在了足够长的时间,被许多硬件设备广泛使用。这是一种同步、双向、基于数据包的串行通信协议,依赖于每个设备上的两个连接器:
图 3-2
I 2 C (左)和 SPI(右)的物理总线连接比较(鸣谢:Lifewire)
-
SDA(串行数据线),用于通过串行总线接口双向传输数据
-
SCL(串行时钟线),用于同步连接的设备并调节对总线的访问
MLX90640 使用该接口,因此,它包括 SDA 和 SCL 连接器,可以连接到 Raspberry Pi 的 I 2 C 接口(或任何具有兼容引脚排列的 SoC)。VCC 和 GND 连接器必须连接到与 Raspberry Pi 或任何 Raspberry Pi 3.3V/GND 引脚相同的电源和地线,而我们可以在这个项目中省略 INT(中断)连接,它通常用于引发异步事件。将 I 2 C 或 SPI 设备连接到 Raspberry Pi 主要有三种选择:
图 3-3
树莓派 GPIO(鸣谢:树莓派基金会)
-
硬件I??【2】??C连接。尽管 Raspberry Pi 的 GPIO 引脚应该是通用的(顾名思义),但一些引脚在硬件层面进行了优化,以更好地实现某些目的。GPIO 引脚 2 和 3 就是这种情况,如图 3-3 所示,它们分别被配置为 SDA 和 SCL 接口。因此,最快的选择是将您的 I 2 C 摄像机的 SDA 和 SCL 引脚直接连接到这些引脚。这种连接的优点是速度快(因为它使用了I2C协议的本地硬件实现),并且几乎不需要软件配置即可工作。缺点是 Raspberry Pi 只有一对专门的 SDA/SCL 引脚,你只能将一个设备连接到这个接口(如果你只将 Raspberry Pi GPIO 用于热感相机,这不是一个大问题,但如果你想连接更多的I2C设备,这可能是一个问题)。
-
软件I??【2】??C连接。在这种配置中,您可以使用任何 GPIO 引脚对作为 SDA/SCL 接口。这种方法的优点是,您可以更加灵活地连接更多的I2C 器件,或者更一般地说,您可以连接更多的器件,而不必占用 GPIO 引脚 2 和 3。这种方法的缺点表现在它的速度(协议由软件管理,特别是由内核管理,这通常比基于硬件的实现慢)以及它可能需要对软件配置进行更多调整的事实。
-
使用类似于分线架【25】的分线板(参见图 3-4 )。这可能是我最喜欢的方法。分线板可以直接插在您的 Raspberry Pi GPIO 引脚的顶部,它们充当 I 2 C 和 SPI 设备的硬件多路复用器。它们提供了一个硬件接口来连接多达四个I2C 器件和两个 SPI 器件,并且它们使连接变得像将器件插入插槽一样简单——不需要布线也不需要焊接。
准备操作系统
一旦你把所有的硬件都准备好了,就该在 SD 卡上刷新你的 Raspberry Pi 的操作系统了。最受欢迎的选项通常是 NOOBS [26]和 Raspberry Pi OS 27,前者是基于 Debian 的发行版,对于那些不太熟悉终端的人来说也很容易使用。作为 Arch Linux 的坚定支持者,大多数示例都在运行 Arch Linux ARM 的设备上进行了深入的测试,但是由于在嵌入式设备上运行 Arch 的学习曲线通常比使用 NOOBS 或 Raspberry Pi 操作系统要高,所以本章中的示例将主要针对这两个发行版。然而,它们应该在任何其他 Raspberry Pi 操作系统上进行较小的修改或不进行修改,唯一的区别可能是您安装一些系统包的方式,例如,通过“pacman”或“yum”而不是“apt-get”。
为您的设备下载 Raspberry Pi 操作系统或 NOOBS 的映像,并将其闪存到 SD 卡中。你可以使用任何软件来写图像 Raspberry Pi 基金会为 Windows 和 macOS 提供了一个 Imager 程序来写图像,但是你可以通过网络搜索找到其中的许多。如果您在 Linux 上,您可以使用内置的dd
命令轻松地编写映像:
# FIRST check where your SD card is mounted!!
# Make sure that you don't write the image to any other hard drive!
cat /proc/partitions # Find something like e.g. /dev/sdb
[sudo] dd if=/path/to/raspberrypi-os-version.img of=/dev/sdb \
conv=fsync bs=4M status=progress
一旦图像被刷新,(安全地)从你的电脑中取出 SD 卡,并将其插入 Raspberry Pi。至少在第一次启动时,建议还插上一个显示器(通过 HDMI)和一个 USB 键盘/鼠标。一旦一切都连接好了,插上 USB 电源,启动树莓派。几秒钟后,您应该会在连接的显示器上看到欢迎屏幕。如果需要登录,默认凭证是 user=pi 和 password=raspberry 。其他系统可能有不同的默认凭据—如果是这样,请查阅他们的网页。然而,尽快更改默认凭证是一个非常好的做法,尤其是如果您计划启用远程 SSH 访问——只需打开一个终端并键入 passwd 。
图 3-4
分会场花园I2C/SPI 硬件多路复用器(鸣谢:皮莫尔尼)
如果您的 Raspberry Pi 是通过网线连接的,那么它可能会自行连接到网络,无需任何配置。否则,如果您计划通过 Wi-Fi 连接它,现在启用该界面是一个好主意——您可以通过应用程序面板中的 Wi-Fi 图标或通过终端(raspi-config
命令)来实现。其他选项包括手动创建和启用netctl
配置文件或使用另一个网络管理器。
一旦连接了 Raspberry Pi,最好启用 SSH(或者 VNC ),这样您就可以轻松地从笔记本电脑上访问它,而无需连接屏幕和鼠标/键盘。使用raspi-config
启用 SSH 服务或手动启动并启用sshd
服务:
[sudo] systemctl start sshd.service
[sudo] systemctl enable sshd.service
记下设备的 IP 地址(ifconfig
或ip addr
),回到您的计算机,使用 PuTTY 或命令行 ssh 客户端连接到您的 Raspberry Pi:
ssh pi@[ip-of-the-rpi]
登录后,就可以安装软件依赖项来运行我们的项目了。
3.2 安装软件依赖项
本章中的例子将使用 Platypush [28]作为平台来自动捕获图像、运行模型和执行自动化例程。在过去的几年里,我自己构建了 Platypush,现在它已经足够成熟,可以在 SoC 设备上执行许多任务。然而,将本章中的例子移植到其他常见的自动化平台应该相对容易——比如 Home Assistant 或 OpenHAB。
首先,通过python3 –version
在 Raspberry Pi 上检查 Python 的版本——你至少需要 3.6 或更高版本才能运行 Platypush。这在大多数现代发行版上应该不是问题,但是旧发行版可能有旧版本——如果是这样,要么升级发行版,要么手动编译更高版本的 Python。
是时候更新 apt 镜像以查看是否有任何软件包更新了:
[sudo] apt-get update
[sudo] apt-get upgrade
如果尚未安装,则安装pip
:
[sudo] apt-get install python3-pip
然后安装 Platypush——现在用http
模块。有两种方法可以做到:
-
通过
pip
安装最新的稳定版本: -
从 GitHub 安装最新的快照。如果您计划使用 MLX90640 分线点或任何其他需要从 Platypush 代码库编译特定驱动程序的设备,则特别建议使用这种方法。首先确保安装了 git:
[sudo] pip3 install 'platypush[http]'
[sudo] apt-get install git
然后克隆存储库及其子模块:
mkdir -p ~/projects && cd ~/projects
git clone https://github.com/BlackLight/platypush
cd platypush
git submodule init
git submodule update
[sudo] pip3 install '.[http]'
此外,Platypush 依赖 Redis 作为消息传递系统在不同的组件之间分发命令。在 Raspberry Pi 上安装、启动和启用 Redis:
# On other systems the Redis server is called simply redis
[sudo] apt-get install redis-server
[sudo] systemctl start redis-server.service
[sudo] systemctl enable redis-server.service
现在是时候看看我们需要的 Platypush 模块了。Platypush 附带了一组广泛的集成,记录在官方文档页面[29]上,每个集成可能需要不同的依赖项或自己的配置。默认情况下,从~/.config/platypush/config.yaml
读取配置;通过使用构造函数参数中显示的相同属性,可以在这个文件(YAML 格式)中配置每个模块(另外,强烈建议以非根用户身份运行 Platypush)。模块可以分为插件和后端。插件(通常)是无状态的,可以用来执行动作——比如开灯、播放音乐、捕捉相机画面、根据模型进行预测等等。相反,后端是在后台运行的服务,并在发生某些事情时触发事件(例如,播放某些媒体文件、接收电子邮件、创建日历事件、从传感器读取某些数据等)。)—虽然有些插件也可能引发事件。这些事件可以被定制的事件钩子捕获,钩子可以运行任何你喜欢的逻辑。一些模块需要额外的依赖——它们通常在模块的文档中报告,并且通常可以通过pip
安装。依赖项也在项目的requirements.txt
中报告——您可以取消注释您需要的依赖项,然后通过
[sudo] pip3 install -r requirements.txt
依赖项也在项目的setup.py
文件中报告,它们可以通过
[sudo] pip3 install 'platypush[module1,module2,module3]'
出于这个项目的目的,我们希望首先从我们的相机定期捕获图像,并将它们存储在本地,以便我们可以在以后使用它们来训练我们的模型。如果你使用的是 MLX90640 热感摄像机,那么你首先要编译 Pimoroni 提供的驱动程序。首先,安装所需的依赖项:
[sudo] apt-get install libi2c-dev build-essentials
然后转到之前克隆的 Platypush 存储库目录,编译驱动程序:
cd ~/projects/platypush/plugins/camera/ir/mlx90640/lib
make clean
make bcm2835
make examples/rawrgb I2C_MODE=LINUX
如果编译过程顺利,您应该会在文件夹examples
下找到一个名为rawrgb
的可执行文件。记下这个可执行文件的路径,或者将其复制到另一个bin
目录。如果您尝试运行它,并且 MLX90640 分线点正确连接,您应该会看到连续的字节流,这是相机捕捉的帧的 RGB 表示。如果出错,通常是因为树莓派上的I2C 总线没有启用。如果是这种情况,您可以通过raspi-config
或者手动将该行添加到/boot/config.txt
来启用I2C 接口:
dtparam=i2c_arm=on
# Optionally, increase the throughput on the bus
dtparam=i2c1_baudrate=400000
请注意,在某些系统上,dtparam
可能被命名为i2c
而不是i2c_arm
,并且更改 I 2 C 配置可能需要重新启动系统。一旦rawrgb
可执行文件可以成功捕获帧,安装 Platypush 通用摄像机模块依赖项:
cd ~/projects/platypush
[sudo] pip3 install '.[camera]'
# Or, if you installed Platypush directly from pip:
[sudo] pip3 install 'platypush[camera]'
如果您选择了可以通过硬件 Raspberry Pi 摄像头接口连接的摄像头,您应该安装picamera
模块(同时确保 Pi camera 接口在raspi-config
中启用):
cd ~/projects/platypush
[sudo] pip3 install '.[picamera]'
# Or, if you installed Platypush directly from pip:
[sudo] pip3 install 'platypush[picamera]'
相反,如果你有一个 USB 连接的相机,你可以通过camera.cv
、camera.ffmpeg
或camera.gstreamer
插件将 Platypush 连接到它,这些插件分别通过 OpenCV、FFmpeg 和 GStreamer 与相机设备交互(查看它们的文档页面或setup.py
了解它们所需的依赖关系)。Platypush 提供的相机接口提供了一个 API 来透明地与这些插件进行交互。一旦安装了所有的依赖项,就可以继续配置 Platypush 自动化了。
首先,在 Platypush 中启用 web 服务器——我们将使用它从 web 界面访问相机,并通过 web API 测试捕捉。将这些行添加到~/.config/platypush/config.yaml
:
backend.http:
port: 8008 # Default listen port
其次,我们将配置camera.ir.mlx90640
插件并指定rawrgb
路径的位置:
camera.ir.mlx90640:
rawrgb_path: ~/bin/rawrgb
# You may want to specify the rotation of the camera
rotate: 270
# Optionally, specify the number of frames per second
fps: 16
# And flip the image vertically/horizontally
vertical_flip: True
horizontal_flip: True
如果您选择通过 PiCamera 兼容的光学或红外相机收集图像,配置将如下所示:
camera.pi:
# Same options as camera.ir.mlx90640
# except it doesn't need the rawrgb_path
或者,对于兼容 OpenCV/FFmpeg/GStreamer 的摄像机:
camera.cv:
device: /dev/video0
# Same options available for camera.pi
camera.ffmpeg:
device: /dev/video0
# Same options available for camera.pi
camera.gstreamer:
device: /dev/video0
# Same options available for camera.pi
现在您可以通过platypush
命令启动服务。将它注册为用户服务也是一个好主意,这样您就不必在每次重新启动时或者它终止时手动重新启动它:
图 3-5
MLX90640 红外摄像机的快照
mkdir -p ~/.config/systemd/user
cd ~/projects/platypush
cp examples/systemd/platypush.service ~/.config/systemd/user
# You may also want to modify the ExecStart parameter if
# Platypush was installed on a path other than /usr/bin
systemctl --user daemon-reload
systemctl --user start platypush.service
systemctl --user enable platypush.service
如果 Platypush 成功启动,您可以在http://raspberry-pi-ip:8008/
检查是否可以从浏览器访问 web 面板。首次访问时,您需要设置用户名和密码。登录后,您可以选择与红外摄像机关联的面板(通常由一个太阳形状的图标标识)并开始传输。
如果一切顺利,您应该会看到如图 3-5 和 3-6 所示的图像流,显示检测到较冷温度的蓝绿色区域和温度较高的黄红色区域。如果你使用了另一个相机插件(camera.pi
、camera.cv
、camera.ffmpeg
或camera.gstreamer
),你也应该在标签中看到它的界面,不管你使用的是什么相机界面,下面的说明都会起作用——你只需要用你使用的相机插件的名称替换camera.ir.mlx90640
。
您也可以通过直接打开捕获 URL 来捕获单个图像:
http://raspberry-pi-ip:8008/camera/ir.mlx90640/photo?scale_x=10&scale_y=10
可能需要scale_x
和scale_y
参数来提高图像的分辨率,因为 MLX90640 以较小的 24x32 分辨率捕捉图像。如果您想访问连续流,只需将前面 URL 中的photo
替换为video
,如果您使用不同的插件,将camera/ir.mlx90640
替换为camera.pi
或camera.cv
。
图 3-6
网络面板 MLX90640 界面预览
Platypush 也通过 HTTP 公开其 API,您可以使用它以编程方式从相机拍摄照片或记录视频流,例如:
curl -XPOST -u 'user:pass' -H 'Content-Type: application/json' -d '
{
"type":"request",
"action":"camera.ir.mlx90640.capture_image",
"args": {
"image_file": "~/image.jpg"
}
}' http://raspberrypi-pi-ip:8008/execute
curl -XPOST -u 'user:pass' -H 'Content-Type: application/json' -d '
{
"type":"request",
"action":"camera.ir.mlx90640.capture_video",
"args": {
"video_file": "~/video.mp4"
}
}' http://raspberrypi-pi-ip:8008/execute
这个 API 也可以在其他后端上公开。例如,如果您启用了backend.mqtt
,您可以通过 MQTT 向 Platypush 发送类似前面的 JSON 格式的消息(默认情况下,该服务将监听关于主题platypush_bus_mq/hostname
的命令),类似的原则也适用于backend.websocket
、backend.kafka
和backend.redis
。所以请记住,如果您不想公开 web 服务,您可以使用多个接口来运行您的命令。
该 API 也可用于其他相机插件——只需将camera.ir.mlx90640
替换为你的相机插件名称。一般来说,插件文档中显示的任何方法都可以通过 HTTP API 调用。
3.3 捕捉图像
现在我们已经准备好了所有的硬件和软件,让我们配置 Platypush 定期捕捉相机图像并将其存储在本地——我们稍后将使用这些图像来训练我们的模型。
Platypush 提供了 cronjobs 的概念,基本上是可以定期执行并运行一些自定义操作的过程。让我们添加一个 cron 到我们的config.yaml
中,它从传感器中获取图片并将它们存储在本地目录中。首先,在 Raspberry Pi 上创建图像目录:
mkdir -p ~/datasets/people_detect
然后在config.yaml
中添加 cron 的逻辑:
cron.ThermalCameraSnapshotCron:
cron_expression: '* * * * *'
actions:
- action: camera.ir.mlx90640.capture_image
args:
image_file: ~/datasets/people_detect/\
${int(__import__('time').time())}.jpg
grayscale: true
一些观察结果:
-
Platypush cronjobs 由
cron.<CRON_NAME>
语法标识。 -
cron_expression
定义了 cron 应该多久执行一次。这与 UNIX cronjob 的语法相同,所以在本例中,* * * * *
表示每分钟拍摄一张照片。秒也支持更高的粒度,但是为了向后兼容 UNIX cron 表达式,它们通常在表达式的末尾报告——所以如果您想每 30 秒运行一次这个过程,那么表达式应该是* * * * * */30
。 -
要执行的动作在
actions
部分定义为一个列表。 -
每个
action
都有一个<plugin_name>.<method_name>
语法和一个可选的args
属性来指定它的参数。某个插件可用的动作列表在插件本身的文档中报告,以及支持的参数列表。 -
您可以使用
${}
语法在 Platypush cron、过程或事件钩子的定义中嵌入 Python 的片段。在这种情况下,我们使用~/datasets/people_detect/${int(__import__('time').time())}.jpg
参数将数据集目录下的每个图像保存为一个时间戳命名的文件。 -
说到红外/热成像图片,我体验过将 RGB 输出转换为灰度的最佳性能 MLX90640 的 Platypush 插件已经有一个内置逻辑,通过为红色组件分配更多权重并减去蓝色组件的贡献,将 RGB 热成像图片转换为灰度。通过为颜色分量适当分配权重的灰度转换,可以非常容易地生成图像,这些图像可以清晰地以白色显示温暖的区域,以黑色显示寒冷的区域,这可以使机器学习模型非常快地收敛。如果您使用不同的红外或热感相机也输出 RGB 伪像,请检查它们的温度范围和灵敏度,以了解如何利用输出颜色空间来提高转换为灰度时要检测的温度范围。
-
同样,cron 只需稍加修改就可以与任何其他相机插件一起工作——只需用您想要使用的插件替换
camera.ir.mlx90640
,只要它实现抽象的camera
接口,它就可以与相同的 API 一起工作。
定义相机捕捉逻辑后,(重新)启动 Platypush,等待下一分钟的滴答。如果一切顺利的话,第一张灰度热图应该已经存储在~/datasets/people_detect
下了。让逻辑运行至少 1-2 天,以捕捉足够的图片——根据我的经验,当用大约 900-1000 张图像训练时,模型已经可以很好地执行。尽可能地丰富数据集——在房间里走一圈,站在房间的不同点,在房间里有更多人时捕捉图像,在远离传感器时拍照,等等,这就足够了。训练集中捕获的条件的可变性越高,模型在真实场景中的表现就越准确。此外,确保传感器前有和没有人的照片数量平衡——理想情况下,目标是 50/50。
3.4 标记图像
一旦你捕捉到足够多的图像,就该把它们复制到你的电脑上,给它们贴上标签,并训练模型。如果您遵循了本章前面所述的说明,并在 Raspberry Pi 上启用了 SSH(并且您在 Raspberry Pi 或您的主计算机上运行了 SSH 服务器),这将与在您的 Raspberry Pi 上运行以下命令一样简单:
scp -r ~/datasets user@your-pc:/home/user/
无聊的部分正等着我们——手动将图像标记为正面或负面。我用一个脚本让这个任务变得不那么单调乏味了,这个脚本允许您在查看图像的同时交互式地标记图像,并把它们移动到正确的目标目录。在本地计算机上安装依赖项并克隆存储库:
# The script uses OpenCV as a cross-platform
# tool to display images.
[sudo] pip3 install opencv
# Create a folder for the image utils and
# clone the repository
mkdir -p ~/projects
cd ~/projects
git clone https://github.com/BlackLight/imgdetect-utils
标签脚本将在一个目录中查找图像文件,并将任何子目录视为一个标签。让我们继续创建我们的标签,并开始贴标过程:
图 3-7
通过utils/label.py
脚本的图像标记阶段的屏幕截图
UTILS_DIR=~/projects/imgdetect-utils
IMG_DIR=~/datasets/people_detect
# Create the directories for the labels
cd $IMG_DIR
mkdir -p positive negative
# Do the labelling
cd $UTILS_DIR
python3 utils/label.py -d "$IMG_DIR" --scale-factor 10
您应该会看到如图 3-7 所示的窗口。您可以使用数字键(1 代表阴性,2 代表阳性)将某个图像标记为阳性或阴性,s
跳过图像,d
删除图像,ESC
/ q
终止标记。传递给脚本的–scale-factor 10
告诉在预览时将图像放大 10 倍——这在我们标记微小的 24x32 图像时非常有用。让时间戳来指导您(例如,了解人何时在房间里,何时不在房间里),并记住,较亮的区域代表较热的身体,而较暗的区域代表较冷的身体或背景,因此您可能会在照片中看到人体作为“白色光环”,其大小和亮度取决于他们与传感器的距离以及他们的位置。还要记住,如果其他热源在相机传感器的范围内,它们可能会出现在图像中——如果你有水壶、锅炉或只是在房间里走动的宠物,请记住这一点——但如果它们是“背景”的一部分,并且它们出现在大多数照片中,那么它们应该不是一个大问题。例如,我的 MLX90640 位于一个突破花园中,就在一个具有主动冷却功能的树莓 Pi 4 的顶部,我可以清楚地看到树莓 Pi 风扇散发的热量在大多数拍摄的图片底部发出亮光。然而,由于发光基本上总是存在(在标记为负的图片中也是如此),该模型将学习将其视为背景的一部分,预计不会触发许多误报。然而,请记住,如果你有一只猫时不时地在传感器前走过,情况可能就不是这样了。
在标记阶段之后,数据集目录将如下所示:
-> ~/datasets/people_detect
-> negative
-> IMG0001.jpg
-> IMG0002.jpg
...
-> positive
-> IMG0003.jpg
-> IMG0004.jpg
...
一旦您完成了标记过程,您应该已经用您的训练图像正确填充了数据集中的两个目录(positive
和negative
),并且您已经准备好继续下一阶段——训练模型来检测人的存在。
3.5 训练模型
如果您应用了前几章中探索的相同技术,这一部分应该非常简单。我们有一个整齐标记的数据集,包含存储在~/datasets/people_detect
下的 24x32 灰度热感相机图片,我们希望训练一个神经网络,它可以学习图像何时包含人形,何时不包含人形——所以是时候打开一个新的 Jupyter 笔记本了。
让我们从定义几个变量开始:
import os
# Define the dataset directories
datasets_dir = os.path.join(os.path.expanduser('~'), 'datasets')
dataset_dir = os.path.join(datasets_dir, 'people_detect')
# Define the size of the input images. In the case of an
# MLX90640 it will be (24, 32) for horizontal images and
# (32, 24) for vertical images
image_size = (32, 24)
# Image generator batch size
batch_size = 64
# Number of training epochs
epochs = 5
在这种情况下,数据并没有像前面的一些例子那样整齐地划分为训练集和测试集,但是我们可以利用 Keras ImageDataGenerator
类的validation_split
参数,让它自动将数据集划分为训练集和测试集——特别是划分值将告诉构造函数应该将多少百分比的数据点划分到测试/验证集中。然后我们可以使用flow_from_directory
的subset
参数来提取这两个集合。
from tensorflow.keras.preprocessing.image import ImageDataGenerator
# 30% of the images goes into the test set
generator = ImageDataGenerator(rescale=1./255, validation_split=0.3)
train_data = generator.flow_from_directory(dataset_dir,
target_size=image_size,
batch_size=batch_size,
subset='training',
class_mode='categorical',
color_mode='grayscale')
test_data = generator.flow_from_directory(dataset_dir,
target_size=image_size,
batch_size=batch_size,
subset='validation',
class_mode='categorical',
color_mode='grayscale')
与上一章中的示例不同,这里我们假设您的 Raspberry Pi 和摄像机不会移动太多,如果您要监控同一房间内的人的存在,那么快照应该总是捕捉相同的视图,因此我们不需要太多的图像转换——图像生成器执行的唯一转换是1/255
重新缩放,以标准化[0, 1]
范围内的图像像素值。相反,如果您希望移动相机或将它安装在一些移动组件的顶部,那么向图像生成器添加诸如horizontal_flip
、vertical_flip
、rotate
等变换来创建更健壮的数据集仍然是一个好主意。此外,由于我们正在处理灰度图像,我们需要通过color_mode
参数为flow_from_directory
指定正确的颜色空间。像前面的例子一样,让我们看一下数据集,看看是否一切正常:
import numpy as np
import matplotlib.pyplot as plt
index_to_label = {
index: label
for label, index in train_data.class_indices.items()
}
plt.figure(figsize=(10, 10))
batch = train_data.next()
for i in range(min(25, len(batch[0]))):
img = batch[0][i]
label = index_to_label[np.argmax(batch[1][i])]
plt.subplot(5, 5, i+1)
plt.xticks([])
plt.yticks([])
plt.grid(False)
# Note the np.squeeze call - matplotlib can't
# process grayscale images unless the extra
# 1-sized dimension is removed.
plt.imshow(np.squeeze(img))
plt.xlabel(label)
plt.show()
您应该会看到如图 3-8 所示的图形。
图 3-9
热像仪人员检测模型在 5 个训练时期内的准确性
图 3-8
训练集中某些项目的预览
定义和训练模型的时间。如果你用足够多的图片来训练它,这个例子的模型可以非常简单,但仍然可以达到令人印象深刻的准确性。例如,让我们定义一个展平 32 × 24 灰度图像的模型,该模型包括两个隐藏层,其单元数分别为输入像素数的 80%和 30%,并在两个单元的输出层上输出预测值— negative
和positive
:
import tensorflow as tf
from tensorflow import keras
model = keras.Sequential([
keras.layers.Flatten(input_shape=image_size),
keras.layers.Dense(round(0.8 * image_size[0] * image_size[1]),
activation=tf.nn.relu),
keras.layers.Dense(round(0.3 * image_size[0] * image_size[1]),
activation=tf.nn.relu),
keras.layers.Dense(len(train_data.class_indices),
activation=tf.nn.softmax)
])
model.compile(loss='categorical_crossentropy',
optimizer='adam',
metrics=['accuracy'])
让我们通过之前声明的数据生成器来训练它:
history = model.fit(
train_data,
steps_per_epoch=train_data.samples/batch_size,
validation_data=test_data,
validation_steps=test_data.samples/batch_size,
epochs=epochs
)
我的系统上的输出如下所示:
Epoch 1/5
loss: 0.2529 - accuracy: 0.9196 - val_loss: 0.0543 - val_accuracy: 0.9834
Epoch 2/5
loss: 0.0572 - accuracy: 0.9801 - val_loss: 0.0213 - val_accuracy: 0.9967
Epoch 3/5
loss: 0.0254 - accuracy: 0.9915 - val_loss: 0.0080 - val_accuracy: 1.0000
Epoch 4/5
loss: 0.0117 - accuracy: 0.9979 - val_loss: 0.0053 - val_accuracy: 0.9967
Epoch 5/5
loss: 0.0058 - accuracy: 1.0000 - val_loss: 0.0046 - val_accuracy: 0.9983
这意味着在 5 个时期后,训练集的准确率为 100%,测试集的准确率为 99.83%——考虑到我们使用的是一个相对简单的网络,没有卷积层,这一点都不差。与前面的示例一样,我们可以直观地看到模型的准确性是如何随着训练时期而提高的:
epochs = history.epoch
accuracy = history.history['accuracy']
fig = plt.figure()
plot = fig.add_subplot()
plot.set_xlabel('epoch')
plot.set_ylabel('accuracy')
plot.plot(epochs, accuracy)
您应该会看到类似于图 3-9 所示的图。
尽管数据集相对较小(我在这些示例中使用了大约 1400 张图像的数据集)并且网络架构简单,但性能如此之高的原因是,我们甚至在构建模型之前就使用了正确的工具来解决问题。如果问题没有得到适当的约束,人物检测的问题很容易导致复杂模型的创建,例如,如果您使用来自通用光学相机的通用大型数据集,这些相机在大量不同的背景下拍摄人物。如果您正在构建一个需要安装在(例如)用于自动驾驶汽车的摄像头硬件上的通用应用程序,那么从大型通用数据集构建复杂模型的策略肯定是可行的,因为自动驾驶汽车需要在所有可能的位置、距离、方向和情况下识别人体。但是,如果你对问题进行足够的限制,例如,从一个不动的静态相机识别是否有人在你的房间里,该相机检测温度梯度,而不是依赖于在物体上反射的光,在一个为此目的优化的颜色空间中,并且在底片的情况下使用通常产生非常相似图像的输入源,那么模型不一定要复杂,数据集不一定要庞大。这是因为我们已经将检测图片中人的存在的问题转化为检测灰度图片中比平常更多光晕的存在的问题。在大多数情况下,定义良好的输入源、输入空间和输入数据集是构建良好模型的最重要因素。
和前面的例子一样,让我们定义一些效用函数,看看模型对测试集中的一些图像的表现如何:
图 3-10
模型对测试集的一批项目进行预测的示例
def plot_image_and_predictions(prediction, classes, true_label, img):
import numpy as np
import matplotlib.pyplot as plt
plt.grid(False)
plt.xticks([])
plt.yticks([])
plt.imshow(np.squeeze(img))
predicted_label = int(np.argmax(prediction))
confidence = 100 * np.max(prediction)
color = 'blue' if predicted_label == true_label else 'red'
plt.xlabel('{predicted} {confidence:2.0f}% ({expected})'.format(
predicted=classes[predicted_label],
confidence=confidence,
expected=classes[int(true_label)]), color=color)
def plot_value_array(prediction, true_label):
import numpy as np
import matplotlib.pyplot as plt
plt.grid(False)
plt.xticks([])
plt.yticks([])
thisplot = plt.bar(range(len(prediction)), prediction, color="#777777")
plt.ylim([0, 1])
predicted_label = np.argmax(prediction)
thisplot[predicted_label].set_color('red')
thisplot[true_label].set_color('blue')
# Plot the first X test images, their predicted label, and the true label
# Color correct predictions in blue, incorrect predictions in red
def plot_results(images, labels, predictions, classes, rows, cols):
n_images = rows * cols
plt.figure(figsize=(2 * 2 * cols, 2 * rows))
for i in range(n_images):
plt.subplot(rows, 2 * cols, 2 * i + 1)
plot_image_and_predictions(
predictions[i], classes, labels[i], images[i])
plt.subplot(rows, 2 * cols, 2 * i + 2)
plot_value_array(predictions[i], labels[i])
plt.show()
并用第一批测试集的图像样本调用它:
test_batch = test_data.next()
test_images = test_batch[0]
test_labels = test_batch[1]
predictions = model.predict(test_images)
index_to_label = {
index: label
for label, index in train_data.class_indices.items()
}
plot_results(
images=test_images,
labels=[np.argmax(label_values) for label_values in test_labels],
classes=index_to_label,
predictions=predictions,
rows=6, cols=3)
您应该会看到如图 3-10 所示的图像。
3.6 部署模型
一旦您对模型的性能感到满意,就可以保存它了,使用类似于前面探索的保存标签名称的逻辑:
def model_save(model, target, labels=None, overwrite=True):
import json
import pathlib
# Check if we should save it like a .h5/.pb
# file or as a directory
model_dir = pathlib.Path(target)
if str(target).endswith('.h5') or \
str(target).endswith('.pb'):
model_dir = model_dir.parent
# Create the model directory if it doesn't exist
pathlib.Path(model_dir).mkdir(parents=True, exist_ok=True)
# Save the TensorFlow model using the save method
model.save(target, overwrite=overwrite)
# Save the label names of your model in a separate JSON file
if labels:
labels_file = os.path.join(model_dir, 'labels.json')
with open(labels_file, 'w') as f:
f.write(json.dumps(list(labels)))
model_dir = os.path.expanduser('~/models/people_detect')
model_save(model, model_dir,
labels=train_data.class_indices.keys(), overwrite=True)
在前面的代码片段中,我们将模型保存为 TensorFlow 模型目录(~/models/people_detect
),但是您也可以选择将其保存为单个 Protobuf ( .pb
扩展名)或分层数据格式(HDF4/HDF5,.h4
/ .h5
扩展名)文件。TensorFlow 的最新版本可以加载和保存这些格式中的任何一种,Platypush 提供的 TensorFlow 插件也可以,但如果您计划将模型导入到其他应用程序中,通常最好仔细检查它们支持哪些格式。在任何一种情况下,如果提供了标签列表,model_save
方法也会生成一个labels.json
文件——这可以帮助将输出节点映射回实际的人类可读的类标签,我还没有找到一种标准的方法将它们原生添加到 TensorFlow 模型中。
一旦保存了模型,您就可以将它导出到 Raspberry Pi。回到 Raspberry Pi 并通过 SSH 复制它:
mkdir -p ~/models
scp -r user@your-pc:/home/user/models/people_detect ~/models
模型文件现在应该已经复制到您的 Raspberry Pi 的/home/pi/models/people_detect
下了。一旦模型上传到设备上,我们需要一种方法来使用它对实时数据进行预测。在 Raspberry Pi 上使用 TensorFlow/Keras 模型主要有两种方式:
-
使用本机 TensorFlow 库。
-
使用 OpenCV 。
直到不久前,安装这两个库并在 Raspberry Pi 上正常工作还是一个小小的技术挑战,但是如果您使用的是带有最新版本的 Raspberry Pi/Raspberry Pi 操作系统(或任何其他最新支持的发行版)的 Raspberry Pi 4,这应该相对容易。
OpenCV 方式
使用 OpenCV 从树莓 Pi 上的训练模型进行预测曾经是我最喜欢的解决方案,直到不久前(事实上,我在 2019 年写了一篇文章,展示了如何使用之前训练的 TensorFlow 模型使用这种方法在树莓 Pi 上进行实时预测)。然而,这主要是因为让 TensorFlow 在 Raspberry Pi 上构建和运行过去是一个漫长而乏味的过程,但在 Raspberry Pi 4 上情况发生了很大的变化。OpenCV 方法主要有两个限制:
-
在撰写本文时,
cv2.dnn
OpenCV 包只能读取模型——它不能用于实时训练,也不能保存模型。 -
与 TensorFlow 格式的兼容性非常有限。它无法读取以 HDF5 格式保存的模型(在加载之前需要转换为 ProtobufWeb 上很少有脚本可以做到这一点),我也遇到过加载最近 TensorFlow/Keras 版本保存的一些模型的问题。
然而,如果您的 Raspberry Pi 架构/发行版本身不支持 TensorFlow,OpenCV 可能是进行预测的一个很好的替代方案。
首先,您必须确保 OpenCV 安装在带有contrib
包的设备上——这个包实际上包含了cv2.dnn
模块。如果您在 Raspberry Pi 4 或更高版本上使用 Raspbian Buster,这应该像下面这样简单
[sudo] pip3 install opencv-contrib-python
如果一切顺利,检查是否可以成功导入模块:
>>> import cv2.dnn
>>>
如果在这个过程中出现任何问题,或者如果您使用另一个 OS/Raspberry Pi/SoC 设备,请在线查找在您的平台上安装 OpenCV Python3(和 contrib 包)的方法——一些用户已经发布了针对最棘手情况的分步解决方案。
一旦依赖关系就绪,您可能必须将 HDF5 模型导出到单个 Protobuf 文件中——我在导入最近 TensorFlow 版本生成的基于目录的saved_model.pb
模型时遇到了问题,但是将.h5
文件导出到.pb
仍然有效。有几种工具可用于此目的,例如:
git clone https://github.com/amir-abdi/keras_to_tensorflow
cd keras_to_tensorflow
python3 keras_to_tensorflow.py \
--input_model=/home/pi/models/people_detect/model.h5 \
--output_model=/home/pi/models/people_detect/exported_model.pb
然后,使用您的模型进行预测应该像运行以下代码行一样简单:
import os
import json
import sys
import numpy as np
import cv2
assert len(sys.argv) >= 2, f'Usage: {sys.argv[0]} <image_file>'
image_file = os.path.expanduser(sys.argv[1])
model_dir = os.path.expanduser('~/models/people_detect')
model_file = os.path.join(model_dir, 'exported_model.pb')
labels_file = os.path.join(model_dir, 'labels.json')
model = cv2.dnn.readNet(model_file)
with open(labels_file, 'r') as f:
labels = json.load(f)
img = cv2.imread(image_file)
model.setInput(img)
output = model.forward()
class_ = int(np.argmax(output))
label = labels[class_]
print('Predicted label for {img}: {label}. Confidence: {conf}%'.format(
img=image_file, label=label, conf=100 * output[class_]))
如果前面的脚本运行良好,您可以通过 Platypush ml.cv
插件使用您保存的模型,该插件通过 HTTP API(或任何其他在 Platypush 上启用的后端,例如 MQTT、WebSockets、Kafka 等)免费导出模型。).官方文档中报告了ml.cv
的完整接口。例如,您可以使用它来预测 cURL(注意图像文件必须存在于 Raspberry Pi 存储中):
curl -XPOST -u 'user:pass' -H 'Content-Type: application/json' -d '
{
"type":"request",
"action":"ml.cv.predict",
"args": {
"img": "~/dataset/people_detect/positive/some_image.jpg",
"model_file": "~/models/people_detect/exported_model.pb",
"classes": ["negative", "positive"]
}
}' http://raspberrypi-pi-ip:8008/execute
回应:
{
"id": "<response-id>",
"type": "response",
"target": "http",
"origin": "raspberrypi",
"response": {
"output": "positive",
"errors": []
}
}
然而,请注意,OpenCV Platypush 插件仅限于单个图像作为输入,并且由于cv2.dnn
模块的限制,它只能用于现有训练模型的预测——没有实时训练。
3.6.2 张量流方式
如果您的设备和发行版支持安装和运行 TensorFlow 的简单方式,那么这可能是您最喜欢的方式。在装有 Raspbian Buster 或更高版本的 Raspberry Pi 4 上,这应该可以通过以下命令实现:
[sudo] apt-get install python3-numpy
[sudo] apt-get install libatlas-base-dev
[sudo] apt-get install libblas-dev
[sudo] apt-get install liblapack-dev
[sudo] apt-get install python3-dev
[sudo] apt-get install gfortran
[sudo] apt-get install python3-setuptools
[sudo] apt-get install python3-scipy
[sudo] apt-get install python3-h5py
[sudo] pip3 install tensorflow keras
如果一切顺利,您可以测试是否可以使用我们之前看到的model_load
TensorFlow 函数对之前训练的模型进行预测:
import os
import json
import sys
import numpy as np
from tensorflow.keras.models import load_model
from tensorflow.keras.preprocessing import image
assert len(sys.argv) >= 2, f'Usage: {sys.argv[0]} <image_file>'
image_file = os.path.expanduser(sys.argv[1])
model_dir = os.path.expanduser('~/models/people_detect')
model_file = os.path.join(model_dir, 'saved_model.h5')
labels_file = os.path.join(model_dir, 'labels.json')
with open(labels_file, 'r') as f:
labels = json.load(f)
model = load_model(model_file)
img = image.load_img(image_file, color_mode="grayscale")
data = image.img_to_array(img)
# Remove the extra color dimension if it's a grayscale image
data = np.squeeze(data)
output = model.predict(np.array([data]))[0]
class_ = np.argmax(output)
label = labels[class_]
print('Predicted label for {img}: {label}. Confidence: {conf}%'.format(
img=image_file, label=label, conf=100 * output[class_]))
如果预测有效,您可以继续在 Platypush 中测试模型,通过tensorflow
插件做出类似的预测:
curl -XPOST -u 'user:pass' -H 'Content-Type: application/json' -d '
{
"type":"request",
"action":"tensorflow.predict",
"args": {
"inputs": "~/datasets/people_detect/positive/some_image.jpg",
"model": "~/models/people_detect/saved_model.h5"
}
}' http://raspberrypi-pi-ip:8008/execute
预期产出:
{
"id": "<response-id>",
"type": "response",
"target": "http",
"origin": "raspberrypi",
"response": {
"output": {
"model": "~/models/people_detect/saved_model.h5",
"outputs": [
{
"negative": 0,
"positive": 1
}
],
"predictions": [
"positive"
]
},
"errors": []
}
}
在这种情况下,outputs
包含每个输入样本(在这种情况下,我们仅使用一幅图像)的输出单元的值(如果可用,则包含其相关联的标签),并且predictions
包含每个输入样本的预测标签列表,或者如果标签不可用,则包含它们的类别索引。
图 3-11
Platypush web 面板中light.hue
选项卡的屏幕截图
3.7 构建您的自动化流程
既然您已经能够在 Raspberry Pi 上使用预先训练的模型进行预测,那么是时候利用 Platypush 将这些预测集成到自动化流程中,并根据预测的输出在其他设备上运行操作了。
例如,让我们构建一个执行以下操作的自动化:
-
它以固定的时间间隔(例如,每分钟一次)从热感相机捕获图片,并将捕获的帧存储到临时 JPEG 文件中。
-
如果检测到存在,打开灯。如果没有检测到存在,关闭灯。
我们可以再次利用 Platypush 的 crons 来完成这项工作。在这个例子中,我将介绍 Philips Hue 或任何与light.hue
插件兼容的智能灯泡的实现,但是任何其他与 Platypush 插件兼容并实现抽象light
插件的设备也应该可以工作。第一,如果你用飞利浦 Hue,那么安装 Platypush 插件依赖项(应该只包括phue
):
[sudo] pip3 install 'platypush[hue]'
并将以下配置添加到您的config.yaml
:
light.hue:
bridge: 192.168.1.123
groups:
# Default light groups to be managed
- Living Room
然后重启 Platypush,在浏览器中打开http://raspberry-pi-ip:8008
。您可能需要通过按下物理同步按钮来授权到色调桥的第一次连接。在 Raspberry Pi 被授权后,刷新页面,你应该会看到一个如图 3-11 所示的面板——在灯泡图标下面。您可以通过尝试打开或关闭某些灯或改变颜色来测试连接。像所有插件一样,light.hue
的动作也可以通过 API 获得,您可以轻松地将它们嵌入到您的流中。
为此,我们将创建一个作为 Python 脚本的 Platypush 过程——也可以通过文档化的 YAML 语法直接在config.yaml
中完成,但是 YAML 语法对于复杂的流来说有点死板和冗长。首先,准备 Platypush 用户脚本目录(如果还没有的话):
mkdir -p ~/.config/platypush/scripts
touch ~/.config/platypush/scripts/__init__.py
touch ~/.config/platypush/scripts/camera.py
您也可以直接在__init__.py
中添加您的过程的代码,但是为了更好的模块化,我更喜欢将过程组合在模块中。将以下内容添加到camera.py
:
import os
from platypush.context import get_plugin
from platypush.procedure import procedure
@procedure
def check_presence(**context):
# Get plugins by name
camera = get_plugin('camera.ir.mlx90640')
tensorflow = get_plugin('tensorflow')
lights = get_plugin('light.hue')
image_file = '/tmp/frame.jpg'
model_file = os.path.expanduser('~/models/people_detect/saved_model.h5')
camera.capture_image(
image_file=image_file, grayscale=True)
prediction = tensorflow.predict(
inputs=image_file, model=model_file)['predictions'][0]
if prediction == 'positive':
lights.on()
else:
lights.off()
然后将程序导入/.config/platypush/scripts/__init__.py
以便在config.yaml
中使用:
from scripts.camera import check_presence
最后,用一个新的 cron 替换config.yaml
中以前的 cron,它只是简单地捕捉照片,而新的 cron 每分钟都调用新创建的过程来检查是否存在:
cron.CheckPresenceCron:
cron_expression: '* * * * *'
actions:
- action: procedure.check_presence
重启 Platypush,应该就是这样了——当有人进入或退出热感相机的视野时,灯光的状态会发生变化。是时候炫耀你的朋友了!请记住,Platypush 还提供了许多其他集成——从音乐、媒体和相机到云服务,到许多其他物联网设备,到 MQTT,到语音助手,等等——因此,您可以轻松地应用相同的基本成分来构建基于实时机器学习预测的其他智能流。
3.8 构建小型家庭监控系统
可以检测人的存在的模型的第二个示例应用是设置一个小型家庭监视系统,当我们不在家时,它会通知是否有人在家。我们可以使用以下构件来建立这样一个项目:
-
使用手机上支持地理围栏的应用程序,即检测您何时进入或退出某个区域,并在此类事件发生时触发操作。对于这个例子,我们将为 Android 使用任务器和自动定位。
-
例如,当您进入或离开您的家庭区域时,通过 MQTT 从您的手机向 Raspberry Pi 发送一条消息,并配置 Platypush 监听此类消息并更新
HOME
状态。 -
保持每分钟左右从热感相机拍摄照片,并使用之前训练的模型来预测人的存在。
-
如果检测到存在并且
HOME
状态为假,则从光学相机(例如,PiCamera)拍摄照片,并通过附件向手机发回消息(例如,通过推板或电报)以通知可能的入侵。
一步一步来,首先设置一个 MQTT 服务器,它在您的网络内外都是可访问的。有几个选项可以实现这一点:
-
如果你在云中的某个地方有一个 Linux 机器(比如 Amazon 实例或 VPS),在上面安装一个 MQTT 代理,比如 Mosquitto 如果这个机器有一个公共 IP 地址,那么当它不在你的家庭网络中时,你的手机也可以访问它。
-
如果您的家庭网络路由器/提供商支持,请使用 Dyn(以前的 DynDNS)这样的服务来获取您的家庭路由器的主机名(例如,
my-home-router.gotdns.org
),并运行ddclient
或inadyn
这样的客户端来保持主机名-IP 关联是最新的。在您的 Raspberry Pi 或网络中的其他设备上安装 Mosquitto,并在路由器上使用端口转发来暴露 MQTT 端口。注意:如果您遵循这条路线,您基本上将向外部世界公开您的网络内部的服务。在这种情况下,建议在 MQTT 服务器上设置身份验证和加密。 -
例如,您可以使用 OpenVPN 或 WireGuard 设置来设置家庭 VPN,并通过 Android 客户端将您的手机连接到相同的 VPN。如果您在家庭网络中运行 MQTT 代理,也可以通过 VPN 地址从您的电话访问它。
-
使用公共 MQTT 代理服务(如 HiveMQ、Adafruit。IO,或 MaQiaTTo)—它消除了本地安装 Mosquitto、VPN 和端口转发的需要,但您可能需要为无限制的服务支付一点费用。
无论您喜欢哪个选项,在这个过程的最后,您应该有一个 MQTT 代理的地址和端口,可以在您的网络内外访问它。回到您的 Raspberry Pi,安装 MQTT 集成的依赖项:
[sudo] pip3 install 'platypush[mqtt]'
然后在您的config.yaml
中添加backend.mqtt
的配置:
backend.mqtt:
host: your-mqtt-address
port: 1883
listeners:
- topics:
- sensors/platypush/at_home
listeners
部分指示 Platypush 应该在哪些主题上监听新消息——在本例中,我们将使用名为sensors/platypush/at_home
的主题。
接下来,在你的(Android)手机上安装 Tasker 和 AutoLocation。如果您想将消息从手机发送到 MQTT 代理,您还需要一个支持 Tasker 集成的 MQTT 客户端应用程序——Join,也是由构建 Tasker 和 AutoLocation 的同一开发人员开发的,如果您使用的是没有身份验证或加密的简单 MQTT 代理,应该没问题,但如果不是这样,您应该选择 MQTT 客户端。配置 MQTT 应用程序,并确保它可以连接到您的代理并接收消息。然后,您可以根据自己的喜好调整自动定位,例如,它是否应该使用 GPS 数据、蜂窝 ID 信息或两者都使用;它将多久检查一次位置并配置后台监视器;诸如此类。然后在运行位置更新的 Tasker 界面中创建一个新的配置文件。以您的家庭住址为中心创建一个新位置,并选择一个敏感半径(例如,50、100 或 200 米)。然后创建一个新的任务,在您进入这个区域时运行,创建一个新的动作,选择您的 MQTT 集成,并指定您的代理的地址和端口、主题(例如,sensors/platypush/at_home
)和消息(例如, 1 )。类似地,创建一个当您退出该区域时运行的任务(长按配置文件中的任务并选择添加退出任务)并将 0 发送到主题。如果一切顺利,无论您何时进入或退出您的家庭区域,您的手机都将开始向您的 MQTT 实例发送选定主题的 0 或 1 。
每当收到关注主题的新消息时,Platypush 就会触发一个MQTTMessageEvent
。您可以很容易地在事件上定义钩子,也就是说,每当接收到匹配某个标准的事件时运行的逻辑片段,并且可以使用 YAML 和 Python 语法创建它们。例如,让我们在 Raspberry Pi 上创建一个钩子,它对在 home presence MQTT 主题上收到的新消息做出反应,并设置一个状态变量,我们可以在其他脚本或应用程序中使用该变量来表示我们是否在家。例如,将下列行添加到~/.config/platypush/scripts/home.py
:
from platypush.context import get_plugin
from platypush.event.hook import hook
from platypush.message.event.mqtt import MQTTMessageEvent
@hook(MQTTMessageEvent, topic='sensors/platypush/at_home')
def on_home_state_changed(event, **context):
# Use the variable plugin to persist state variables
# on the local storage
variable = get_plugin('variable')
variable.set('HOME', int(event.args['msg']))
并将事件挂钩导入到/.config/platypush/scripts/__init__.py
中,使其对配置可见:
from scripts.home import on_home_state_changed
现在,无论您何时进入或退出主区域,Raspberry Pi 都会保持HOME
变量的值同步。
接下来,我们将需要一些消息集成,以便在发生事情时将消息从 Raspberry Pi 发送到您的手机。有多种方法可以实现这一点——通过 Pushbullet、Telegram 或 Twilio 集成发送消息,发送电子邮件,触发 IFTTT 规则,等等。出于这个例子的目的,我们将看到如何用 Pushbullet 传递消息,因为它需要的步骤最少。安装推杆集成:
[sudo] pip3 install 'platypush[pushbullet]'
在你的手机上安装应用程序,然后前往 https://docs.pushbullet.com
为你的账户获取一个 API 访问令牌。获得访问令牌后,配置 Platypush 来使用它:
pushbullet:
token: your-token
可选地,如果你还想在检测到存在时发送你房间的照片,你需要一个光学相机插件— camera.pi
、camera.ffmpeg
、camera.gstreamer
和camera.cv
将完成这项工作。例如,如果您有一台 PiCamera,您可以安装依赖项:
[sudo] pip3 install 'platypush[picamera]'
并启用插件:
camera.pi:
enabled: True
最后,让我们通过修改前面的check_presence
cron 将所有的部分放在一起
-
它从热感相机上捕捉到一张照片。
-
它使用之前训练的模型来预测照片中是否有人。
-
如果我们在家,运行之前的逻辑——有人在画面就开灯;否则请关闭它们。
-
如果我们不在家,并且检测到有人在,从 PiCamera 中拍摄一张照片,并通过 Pushbullet 发送到我们的手机,以通知我们有人可能在我们的房子里。
将所有的碎片放在一起:
import os
from platypush.context import get_plugin
from platypush.procedure import procedure
@procedure
def check_presence(**context):
# Get plugins by name
thermal_camera = get_plugin('camera.ir.mlx90640')
pi_camera = get_plugin('camera.pi')
variable = get_plugin('variable')
tensorflow = get_plugin('tensorflow')
lights = get_plugin('light.hue')
pushbullet = get_plugin('pushbullet')
ir_image_file = '/tmp/ir-frame.jpg'
pi_image_file = '/tmp/pi-frame.jpg'
model_file = os.path.expanduser('~/models/people_detect/saved_model.h5')
# Check if we are at home
response = variable.get('HOME')
at_home = int(response.get('HOME'))
# Capture an image from the thermal camera
thermal_camera.capture_image(
image_file=ir_image_file, grayscale=True)
# Use the model to predict if there is someone in the picture
prediction = tensorflow.predict(
inputs=image_file, model=model_file)['predictions'][0]
# If we are at home, run the light on/off logic
if at_home:
if prediction == 'positive':
lights.on()
else:
lights.off()
elif prediction == 'positive':
# Otherwise, if presence is detected and we are not at home,
# take a picture from the PiCamera and send it over Pushbullet
# to notify of a possible intrusion
pi_camera.capture_image(image_file=pi_image_file)
pushbullet.send_note(body='Possible intrusion detected!')
pushbullet.send_file(filename=pi_image_file)
重启 Platypush,你的新家监控逻辑应该到位了!
3.9 现场培训和半监督学习
将经过训练的模型加载到内存中并使用远程 API 的一个很好的功能是,您可以使用新数据实时训练模型并保存它,而不必在您的笔记本电脑中寻找您用来训练它的特定笔记本,并且可以在远程服务上记录透明流。
此外,随着更多数据的处理,这种方法可用于增量训练模型。在 Platypush 的情况下,tensorflow
插件公开了tensorflow.train
方法,用于加载模型的现场训练。cURL 上训练会话示例:
# Load the model from disk
curl -XPOST -u 'user:pass' -H 'Content-Type: application/json' -d '
{
"type":"request",
"action":"tensorflow.load",
"args": {
"model": "~/models/people_detect/saved_model.h5"
}
}' http://raspberrypi-pi-ip:8008/execute
# Train the model with some new data.
# For instance, a new camera picture that we already
# know to be positive.
curl -XPOST -u 'user:pass' -H 'Content-Type: application/json' -d '
{
"type":"request",
"action":"tensorflow.train",
"args": {
"model": "~/models/people_detect/saved_model.h5",
"inputs": ["/home/pi/datasets/people_detect/positive/some-image.jpg"],
"outputs": ["positive"]
}
}' http://raspberrypi-pi-ip:8008/execute
# Save the model once done
curl -XPOST -u 'user:pass' -H 'Content-Type: application/json' -d '
{
"type":"request",
"action":"tensorflow.save",
"args": {
"model": "~/models/people_detect/saved_model.h5"
}
}' http://raspberrypi-pi-ip:8008/execute
# Unload the model once saved to save memory
curl -XPOST -u 'user:pass' -H 'Content-Type: application/json' -d '
{
"type":"request",
"action":"tensorflow.unload",
"args": {
"model": "~/models/people_detect/saved_model.h5"
}
}' http://raspberrypi-pi-ip:8008/execute
train
API 上的inputs
字段非常灵活,它目前支持图像列表、CSV/TSV 文件、numpy 未压缩/压缩文件和原始数组,并且该 API 还公开了其他有用的属性——例如批量大小、时期数、验证数据和验证分割、权重等。
现场培训方法对于一种我喜欢称之为导师学习的方法特别有意思。你可以给你的 Raspberry Pi 配备其他设备来预感人的存在,例如运动探测器、光探测器或安装在房间不同位置的摄像机。您可以配置一个 cron,同时在所有这些设备上运行数据捕获。一旦传感器中的一个检测到存在(例如,通过运动),则同时从热感相机拍摄的相应图片将被标记为阳性,并用于实时训练模型。类似地,该模型可以与光度传感器的输出配对,建立诸如“如果房间里很暗,并且没有检测到运动,并且时间在午夜和早上 8 点之间,那么在这个时间范围内拍摄的照片很可能不包括人”的推断
因此,您可以基于来自其他传感器的数据点构建一个半监督的训练逻辑,这些数据点可以更确定地跟踪您想要预测的指标。如果您将新训练的模型移动到另一台机器,或者您只是移除附件传感器,该模型应该仍然可以很好地跟踪存在,就好像其他传感器或摄像机仍然在那里一样。
作为另一个例子,您可以训练一个模型,用于从光学摄像机图像中检测人物,该模型使用来自热摄像机模型的数据作为导师。我们之前已经探讨了为什么在小环境中热源比光学相机更可靠来检测人的存在。但是,您可以首先根据热摄像机的输出训练一个检测模型,然后创建一个 cron,同时从热传感器和光学摄像机捕获帧。如果热模型的精度非常高,那么它的预测可以用作光学摄像机帧的标签,并用于向例如更复杂的 CNN 架构提供实时动态数据集。在使用这种(半)自动化策略对模型执行了充分的现场训练之后,并且一旦模型的性能度量足够令人满意,就可以拔掉热感相机,并且独立的光学相机模型应该仍然能够进行预测。
3.10 分类器即服务
为了总结 TensorFlow 模型和物联网工具之间的协同作用所提供的可能性,让我们看一个示例,其中通过 MLX90640 进行人员检测的先前模型的整个管理是通过服务(例如,Platypush)而不是笔记本进行的。
Platypush 提供了一个 API 来创建和编译模型,除了训练它们并使用它们进行预测——尽管其他流行的物联网解决方案如 Home Assistant 也可能提供类似的功能,如果它们提供 TensorFlow 集成的话。这种方法的优点是模型将由服务在内部进行一致的管理。此外,训练逻辑发生在 API 调用或 crons/event 挂钩上,而不是 Jupyter 笔记本上(有时很乱,很难跟踪)。
例如,MLX90640 存在检测模型可以通过 API 调用动态创建:
curl -XPOST -u 'user:pass' -H 'Content-Type: application/json' -d '
{
"type": "request",
"action": "tensorflow.create_network",
"args": {
"name": "people_detect",
"output_names": ["negative", "positive"],
"optimizer": "adam",
"loss": "categorical_crossentropy",
"metrics": ["accuracy"],
"layers": [
{
"type": "Flatten",
"input_shape": [24, 32]
},
{
"type": "Dense",
# ~= 0.8 * 32 * 24
"units": 614,
"activation": "relu"
},
{
"type": "Dense",
# ~= 0.3 * 32 * 24
"units": 230,
"activation": "relu"
},
{
"type": "Dense",
"units": 2,
"activation": "softmax"
}
]
}
}' http://raspberrypi-pi-ip:8008/execute
tensorflow.create_network
动作的作用与我们之前用来定义模型的keras.Sequential
调用相同。它可用于定义模型名称、输出标签、优化器、损失函数、性能指标和网络结构。然后,我们可以用收集的数据训练模型:
curl -XPOST -u 'user:pass' -H 'Content-Type: application/json' -d '
{
"type": "request",
"action": "tensorflow.train",
"args": {
"model": "people_detect",
"epochs": 5,
"inputs": "~/datasets/ir_presence_detector/images",
"validation_split": 0.3
}
}' http://raspberrypi-pi-ip:8008/execute
我们使用之前定义的摄像机图像数据集,这些图像被组织到negative
和positive
子文件夹中,并指定历元数和验证分割——在这种情况下,30%的图像将用于模型验证。
tensorflow.train
动作将在训练阶段生成几个事件,如果您想要创建您的定制挂钩,您可以附加到这些事件上——例如,如果性能指标没有降级,则在训练完成后将模型复制到另一台机器上,或者如果您正在处理连续流,则删除属于已经处理的批处理的图像,或者将模型在各个时期的性能记录到 CSV 文件中。其中一些事件是
-
TensorflowTrainStartedEvent
—训练开始时 -
TensorflowTrainEndedEvent
—训练阶段结束时 -
TensorflowBatchStartedEvent
—开始处理批次时 -
TensorflowBatchEndedEvent
—处理一个批次时 -
当一个训练时期开始时
-
TensorflowEpochEndedEvent
—当一个训练时期结束时
在该过程结束时,HTTP 客户端应该会收到如下所示的输出:
{
"response": {
"output": {
"model": "people_detect",
"epochs": [
0,
1,
2,
3,
4
],
"history": {
"loss": [
0.9747824668884277,
0.6165147423744202,
0.07518807053565979,
0.06354894489049911,
0.06809689849615097
],
"accuracy": [
0.9494661688804626,
0.9843416213989258,
0.9957295656204224,
0.9950177669525146,
0.9928825497627258
],
"val_loss": [
0.5309795141220093,
0.4760192930698395,
0.10130093991756439,
0.32663050293922424,
0.7078392505645752
],
"val_accuracy": [
0.9834162592887878,
0.9850746393203735,
0.9917080998420715,
0.9867330193519592,
0.9834162592887878
]
}
},
"errors": []
}
}
history
的每个字段报告每个时期的训练和验证集的损失和性能度量。一旦您对模型满意,您就可以保存它:
curl -XPOST -u 'user:pass' -H 'Content-Type: application/json' -d '
{
"type":"request",
"action":"tensorflow.save",
"args": {
"model": "people_detect"
}
}' http://raspberrypi-pi-ip:8008/execute
模型将保存在~/.local/share/platypush/tensorflow/models/people_detect
下。它可以导入到与 TensorFlow 模型兼容的其他应用程序中,或者导入到您自己的预测脚本中,并且可以在重启时轻松地重新加载到 Platypush 中:
curl -XPOST -u 'user:pass' -H 'Content-Type: application/json' -d '
{
"type":"request",
"action":"tensorflow.load",
"args": {
"model": "people_detect"
}
}' http://raspberrypi-pi-ip:8008/execute
并用于实时预测:
curl -XPOST -u 'user:pass' -H 'Content-Type: application/json' -d '{
"type": "request",
"action": "tensorflow.predict",
"args": {
"model": "people_detect",
"inputs": "/path/to/an/image.jpg"
}
}' http://raspberrypi-pi-ip:8008/execute
这应该涵盖了如何使用远程 API 创建、训练、评估和管理您的模型的所有步骤——并且,理想情况下,不需要编写一行代码。