ANN-计算机视觉应用构建指南-全-

ANN 计算机视觉应用构建指南(全)

原文:Building Computer Vision Applications Using Artificial Neural Networks

协议:CC BY-NC-SA 4.0

一、先决条件和软件安装

这是一本描述如何用 Python 编程语言开发计算机视觉应用的实践书籍。在本书中,您将学习如何使用 OpenCV 操作图像,并使用 TensorFlow 构建机器学习模型。

OpenCV 最初由英特尔开发,用 C++编写,是一个开源的计算机视觉和机器学习库,由 2500 多种用于处理图像和视频的优化算法组成。TensorFlow 是一个用于高性能数值计算和大规模机器学习的开源框架。它是用 C++编写的,提供了对 GPU 的原生支持。Python 是开发机器学习应用最广泛的编程语言。它被设计成与 C++一起工作。TensorFlow 和 OpenCV 都提供 Python 接口来访问它们的底层功能。虽然 TensorFlow 和 OpenCV 提供了其他编程语言的接口,如 Java、C++和 MATLAB,但我们将使用 Python 作为主要语言,因为它很简单,而且有很大的支持社区。

这本书的先决条件是 Python 的实用知识和对 NumPy 和 Pandas 的熟悉。本书假设您熟悉 Python 中的内置数据容器,如字典、列表、集合和元组。以下是一些可能有助于满足先决条件的资源:

在我们继续之前,让我们准备好我们的工作环境,并为我们将要进行的练习做好准备。在这里,我们将从下载和安装所需的软件库和软件包开始。

Python 和 PIP

Python 是我们主要的编程语言。PIP 是 Python 的包安装程序,也是安装和管理 Python 包的事实上的标准。为了设置我们的工作环境,我们将从在我们的工作计算机上安装 Python 和 PIP 开始。安装步骤取决于您使用的操作系统(OS)。确保您按照操作系统的说明进行操作。如果您已经安装了 Python 和 PIP,请确保您使用的是 Python 版本 3.6 或更高版本以及 PIP 版本 19 或更高版本。要检查 Python 的版本号,请在您的终端上执行以下命令:

$ python3 --version

这个命令的输出应该是这样的:Python 3.6.5。

要检查 PIP 的版本号,请在您的终端上执行以下命令:

$ pip3 --version

该命令应该显示 PIP 3 的版本号,例如 PIP 19.1。

在 Ubuntu 上安装 Python 和 PIP

在 Ubuntu 终端中运行以下命令:

sudo apt update
sudo apt install python3-dev python3-pip

在 macOS 上安装 Python 和 PIP

在 macOS 上运行以下命令:

brew update
brew install python

这将同时安装 Python 和 PIP。

在 CentOS 7 上安装 Python 和 PIP

在 CentOS 7 上运行以下命令:

sudo yum install rh-python36
sudo yum groupinstall 'Development Tools'

在 Windows 上安装 Python 和 PIP

安装 Microsoft Visual C++ 2015 可再发行更新 3。这是 Visual Studio 2015 附带的,但可以通过以下步骤单独安装:

  1. 前往 https://visualstudio.microsoft.com/vs/older-downloads/ 的 Visual Studio 下载。

  2. 选择可再发行软件和构建工具。

  3. 下载并安装 Microsoft Visual C++ 2015 可再发行更新 3。

确保在 Windows 上启用了长路径。下面是这样做的说明: https://superuser.com/questions/1119883/windows-10-enable-ntfs-long-paths-policy-option-missing

https://www.python.org/downloads/windows/ 安装用于 Windows 的 64 位 Python 3 版本(选择 PIP 作为可选功能)。

如果这些安装说明在您的环境中不起作用,请参考位于 https://www.python.org/ 的 Python 官方文档。

virtualenv(虚拟环境)

virtualenv 是一个创建独立 Python 环境的工具。virtualenv 创建一个目录,其中包含使用 Python 项目所需的包所需的所有可执行文件。virtualenv 提供了以下优势:

  • virtualenv 允许您拥有同一个库的两个版本,这样您的两个程序都可以继续运行。假设你有一个程序需要 Python 库的版本 1,而另一个程序需要同一个库的版本 2;virtualenv 将允许您同时运行这两种功能。

  • virtualenv 为您的开发工作创建了一个有用的独立和自包含的环境,可以在生产环境中使用,而无需安装依赖项。

接下来,我们将安装 virtualenv,并使用所有必需的软件配置环境。对于本书的其余部分,我们将假设我们的引用程序依赖项将包含在这个 virtualenv 中。

使用以下 PIP 命令安装 virtualenv(该命令在所有操作系统上都是相同的):

$ sudo pip3 install -U virtualenv

这将在系统范围内安装 virtualenv。

安装和激活 virtualenv

首先,创建一个要安装 virtualenv 的目录。我已经把这个目录命名为cv(“计算机视觉”的简称)。

$ mkdir cv
Then create the virtualenv in this directory, cv
$ virtualenv --system-site-packages -p python3 ./cv

以下是运行此命令的输出示例(在我的 MacBook 上):

Running virtualenv with interpreter /anaconda3/bin/python3
Already using interpreter /anaconda3/bin/python3
Using base prefix '/anaconda3'
New python executable in /Users/sansari/cv/bin/python3
Also creating executable in /Users/sansari/cv/bin/python
Installing setuptools, pip, wheel...
done.

使用特定于 shell 的命令激活虚拟环境。

$ source ./cv/bin/activate  # for sh, bash, ksh, or zsh

当 virtualenv 处于活动状态时,您的 shell 提示符会带有前缀(cv)。这里有一个例子:

(cv) Shamshads-MacBook-Air:~ sansari$

在虚拟环境中安装软件包,而不影响主机系统设置。从升级 PIP 开始(确保在 virtualenv 中不要以 root 或 sudo 身份运行任何命令)。

$ pip install --upgrade pip

$ pip list  # show packages installed within the virtual environment

完成后,如果您想退出 virtualenv,请执行以下操作:

$ deactivate  # don't exit until you're done with your programming

TensorFlow

TensorFlow 是一个用于数值计算和大规模机器学习的开源库。您将在后续章节中了解更多关于 TensorFlow 的内容。让我们首先安装它,并为我们的深度学习练习做好准备。

安装 TensorFlow

我们将安装 PyPI ( https://pypi.org/project/tensorflow/ )最新版本的 TensorFlow。我们将为 CPU 安装 TensorFlow。确保您处于 virtualenv 中,并运行以下命令:

(cv) $ pip install --upgrade tensorflow

通过运行以下命令测试 TensorFlow 安装:

(cv) $ python -c "import tensorflow as tf"

如果 TensorFlow 安装成功,输出应该不会显示任何错误。

我喜欢这里

您可以使用您最喜欢的 IDE 来编写和管理 Python 代码,但是出于本书的目的,我们将使用 PyCharm 的社区版本,这是一个 Python IDE。

安装 PyCharm

进入 PyCharm 官网 https://www.jetbrains.com/pycharm/download/#section=linux ,选择合适的操作系统,点击下载(社区版下)。下载完成后,单击下载的软件包,并按照屏幕上的说明进行操作。以下是不同操作系统的直接链接:

配置 PyCharm 以使用 virtualenv

按照以下步骤使用我们之前创建的 virtualenv,cv:

img/493065_1_En_1_Fig1_HTML.jpg

图 1-1

选择口译员

  1. 启动 PyCharm IDE,为 Windows 和 Linux 选择文件➤设置,或者为 macOS 选择 PyCharm ➤首选项。

  2. 在设置/首选项对话框中,选择项目 ➤项目解释器。

  3. 点击img/493065_1_En_1_Figa_HTML.gif图标,然后点击添加。

  4. 在添加 Python 解释器对话框的左侧窗格中,选择现有环境。

  5. 展开解释器列表并选择任何现有的解释器。或者,点击img/493065_1_En_1_Figb_HTML.gif并在您的文件系统中指定 Python 可执行文件的路径,例如/Users/sansari/cv/bin/python3.6(参见图 1-1 )。

  6. 如果您愿意,请选择“对所有项目可用”复选框。

开放计算机视觉

OpenCV 是最流行和广泛使用的图像处理库之一。本书中的所有代码示例都基于 OpenCV 4。因此,我们的安装步骤是针对 OpenCV 版本 4 的。

使用 OpenCV

OpenCV 是用 C/C++编写的,因为它依赖于平台,所以不同的操作系统有不同的安装说明。换句话说,OpenCV 需要为您的特定平台/操作系统构建,以便平稳运行。我们将使用 Python 绑定来调用 OpenCV 以满足任何图像处理需求。

像任何其他库一样,OpenCV 也在发展;因此,如果以下安装说明在您的情况下不起作用,请查看官方网站了解确切的安装过程。

我们将采用一种简单的方法,使用 PIP 安装 OpenCV 4 和 Python 3 绑定。我们将在之前创建的虚拟环境中安装来自 PyPI 的opencv-python-contrib包。

所以我们开始吧!

使用 Python 绑定安装 OpenCV4

确保你在你的虚拟环境中。只需将目录切换到您的 virtualenv 目录(我们之前创建的cv目录)并键入以下命令:

$ source cv/bin/activate

使用以下命令快速安装 OpenCV:

$ pip install opencv-contrib-python

附加库

在我们研究一些例子时,还需要一些额外的库。让我们安装并保存它们。

安装 SciPy

使用以下内容安装 SciPy:

$ pip install scipy

安装 Matplotlib

使用以下内容安装 Matplotlib:

$ pip install matplotlib

请注意,本章中安装的库经常更新。强烈建议查看官方网站的更新、这些库的新版本以及最新的安装说明。

二、图像和视频处理的核心概念

本章介绍了图像的构造块,并描述了操作它们的各种方法。本章中我们的学习目标如下:

  • 了解图像的最小单位(像素)以及颜色是如何表现的

  • 了解图像中的像素是如何组织的,以及如何访问和操作它们

  • 在图像上绘制不同的形状,如线条、矩形和圆形

  • 使用 Python 编写代码并使用 OpenCV 处理示例来访问和操作图像

图像处理

图像处理是一种处理数字图像以获得增强图像或从中提取有用信息的技术。在图像处理中,输入是图像,输出可以是图像或与该图像相关联的一些特性或特征。视频是一系列图像或帧。因此,图像处理技术也适用于视频处理。在这一章中,我将解释数字图像处理的核心概念。我还将向您展示如何处理图像并编写代码来操作它们。

图像基础

数字图像是对象/场景或扫描文档的电子表示。图像的数字化意味着将其转换成一系列数字,并将这些数字存储在计算机存储系统中。理解这些数字是如何排列的以及如何操作它们是本章的主要目标。在这一章中,我将解释图像是由什么组成的,以及如何使用 OpenCV 和 Python 来操作它。

像素

想象一系列按行和列排列的点,这些点有不同的颜色。这差不多就是图像的形成过程。形成图像的点称为像素。这些像素用数字表示,数字的值决定了像素的颜色。将图像想象成一个由正方形单元组成的网格,每个单元由特定颜色的一个像素组成。例如,300×400 像素的图像意味着图像被组织成 300 行 400 列的网格。这意味着我们的图像有 300×400 = 120,000 个像素。

像素颜色

像素以两种方式表示:灰度和颜色。

灰度等级

在灰度图像中,每个像素取 0 到 255 之间的值。值 0 代表黑色,255 代表白色。介于两者之间的值是不同的灰度。接近 0 的值是较暗的灰色阴影,接近 255 的值是较亮的灰色阴影。

颜色

RGB(代表红色、蓝色和绿色)颜色模型是最流行的像素颜色表示之一。还有其他颜色模型,但在本书中我们将坚持使用 RGB。

在 RGB 模型中,每个像素被表示为三个值的元组,一般表示如下:(红色分量的值,绿色分量的值,蓝色分量的值)。这三种颜色中的每一种都用从 0 到 255 的整数来表示。以下是一些例子:

  • (0,0,0)是黑色。

  • (255,0,0)是纯红色。

  • (0,255,0)是纯绿色。

(0,0,255)代表什么颜色?

(255,255,255)代表什么颜色?

这个 w3school 网站( https://www.w3schools.com/colors/colors_rgb.asp )是一个玩 RGB 元组不同组合探索更多模式的好地方。

探究以下每个元组代表什么颜色:

  • (0,0,128)

  • (128,0,128)

  • (128,128,0)

让我们试着做黄色。这里有一个线索:红色和绿色组成黄色。这意味着一个纯红色(255),一个纯绿色(255),没有蓝色(0)将使黄色。因此,黄色的 RGB 元组是(255,255,0)。

现在我们对像素和它们的颜色有了很好的了解,让我们来了解像素在图像中是如何排列的,以及如何访问它们。下一节将讨论图像处理中坐标系的概念。

坐标系统

图像中的像素以网格的形式排列,网格由行和列组成。想象一个八行八列的正方形网格。这将形成一个 8×8 或 64 像素的图像。这可以想象成一个 2D 坐标系,其中(0,0)是左上角。图 2-1 显示了我们的示例 8×8 像素图像。

img/493065_1_En_2_Fig1_HTML.png

图 2-1

像素坐标系

左上角是图像坐标系的起点或原点。右上角的像素用(7,0)表示,左下角的像素用(7,0)表示,右下角的像素用(7,7)表示。这可以概括为(x,y),其中 x 是单元格距图像左边缘的位置,y 是距图像上边缘的垂直位置。在图 2-1 中,红色像素位于左起第五个位置和上起第四个位置。由于坐标系从 0 开始,所以图 2-1 中红色像素的坐标为(4,3)。

为了更清楚一点,让我们想象一个 8×8 像素的图像,上面写着字母 H (如图 2-3 )。此外,为了简单起见,假设这是一个灰度图像,字母 H 用黑色书写,图像的其余区域为白色。

img/493065_1_En_2_Fig2_HTML.png

图 2-2

像素坐标系示例

记住,在灰度模型中,黑色像素用 0 表示,白色像素用 255 表示。图 2-3 显示了 8×8 网格内每个像素的值。

img/493065_1_En_2_Fig3_HTML.jpg

图 2-3

像素矩阵和值

那么,位置(1,4)的像素值是多少呢?而在位置(2,2)?

我希望你现在已经清楚图像是如何用排列在网格中的数字来表示的了。这些数字被序列化并存储在计算机的存储系统中,并在屏幕上显示为图像。至此,您已经知道了如何使用坐标系访问像素,以及如何为这些像素分配颜色。

我们已经建立了坚实的基础,并学习了图像表示的基本概念。让我们自己动手练习一些 Python 和 OpenCV 编码。在下一节中,我将一步一步地向您展示如何编写代码来从计算机磁盘加载图像、访问像素、操作它们,以及将它们写回磁盘。事不宜迟,我们开始吧!

Python 和 OpenCV 代码来操作图像

OpenCV 将图像的像素值表示为 NumPy 数组。(不熟悉 NumPy?可以在 https://numpy.org/devdocs/user/quickstart.html 找到“入门”教程)。换句话说,当您加载一个图像时,OpenCV 会创建一个 NumPy 数组。通过简单地提供(x,y)坐标,可以从 NumPy 中获得像素值。

当您给出(x,y)坐标时,NumPy 将返回这些坐标处像素的颜色值,如下所示:

  • 对于灰度图像,NumPy 返回的值将是 0 到 255 之间的单个值。

  • 对于彩色图像,NumPy 返回的值将是一个红色、绿色和蓝色的元组。请注意,OpenCV 以相反的顺序维护 RGB 序列。请记住 OpenCV 的这个重要特性,以避免在使用 OpenCV 时出现任何混淆。

换句话说,OpenCV 在 BGR 序列中存储颜色,在 RGB 序列中存储而不是

在我们写任何代码之前,让我们确保我们总是使用我们的 virtualenv,在~/cv目录中,我们已经用 PyCharm 设置了它。

启动你的 PyCharm IDE,做一个项目(我把我的项目命名为 cviz,是“计算机视觉”的简称)。参考图 2-4 并确保您已经选择了现有的解释器并选择了我们的 virtualenv Python 3.6(cv)。

img/493065_1_En_2_Fig4_HTML.jpg

图 2-4

PyCharm IDE,显示了使用 virtualenv 的项目设置

程序:加载、浏览和显示图像

清单 2-1 展示了加载、浏览和显示图像的 Python 代码。

Filename: Listing_2_1.py
1    from __future__ import print_function
2    import cv2
3
4    # image path
5    image_path = "images/marsrover.png"
6    # Read or load image from its path
7    image = cv2.imread(image_path)
8    # image is a NumPy array
9    print("Dimensions of the image: ", image.ndim)
10   print("Image height: ", format(image.shape[0]))
11   print("Image width: ", format(image.shape[1]))
12   print("Image channels: ", format(image.shape[2]))
13   print("Size of the image array: ", image.size)
14   # Display the image and wait until a key is pressed
15   cv2.imshow("My Image", image)
16   cv2.waitKey(0)

Listing 2-1Python Code to Load, Explore, and Display an Image

这里解释清单 2-1 中的代码。

在第 1 行和第 2 行,我们从 OpenCV 的__future__包和cv2中导入 Python 的print_function

第 5 行只是我们要从一个目录中加载的图像的路径。如果您的输入路径在不同的目录中,您应该给出图像文件的完整或相对路径。

在第 7 行,使用 OpenCV 的cv2.imread()函数,我们将图像读入一个 NumPy 数组,并赋给一个名为image的变量(这个变量可以是您喜欢的任何东西)。

在第 9 行到第 13 行,使用 NumPy 特性,我们显示了图像数组的维度、高度、宽度、通道数和数组的大小(即像素数)。

第 15 行使用 OpenCV 的imshow()函数显示图像。

在第 16 行中,waitKey()函数允许程序不立即终止并等待用户按下任何键。当您看到将在第 15 行显示的图像窗口时,按任意键终止程序,否则程序将阻塞。

图 2-5 显示了清单 2-1 的输出。

img/493065_1_En_2_Fig5_HTML.jpg

图 2-5

输出和图像显示

image NumPy 数组由三个维度组成:高×宽×通道。数组的第一个元素是高度,它告诉我们像素网格有多少行。类似地,第二个元素是宽度,它表示网格的列数。这三个通道代表 BGR(不是 RBG)颜色分量。数组的大小为 400×640×3 = 768,000。这实际上意味着我们的图像有 400×640 = 256000 个像素,每个像素有三个颜色值。

程序:访问和操作像素的 OpenCV 代码

在下一个程序中,我们将看到如何使用我们之前学过的坐标系来访问和修改像素值。清单 2-2 显示了代码示例,后面有逐行解释。

Filename: Listing_2_2.py
1    from __future__ import print_function
2    import cv2
3
4    # image path
5    image_path = "images/marsrover.png"
6    # Read or load image from its path
7    image = cv2.imread(image_path)
8
9    # Access pixel at (0,0) location
10   (b, g, r) = image[0, 0]
11   print("Blue, Green and Red values at (0,0): ", format((b, g, r)))
12
13   # Manipulate pixels and show modified image
14   image[0:100, 0:100] = (255, 255, 0)
15   cv2.imshow("Modified Image", image)
16   cv2.waitKey(0)

Listing 2-2Code Example to Access and Manipulate Image Pixels

列表 2-2 在此说明。

第 1 行到第 7 行从一个目录路径导入和读取图像(如讨论清单 2-1 时所解释的)。

在第 10 行,我们获得了坐标(0,0)处像素的 BGR(而不是 RBG)值,并使用 NumPy 语法将它们分配给(b,g,r)元组。

第 11 行显示了 BGR 值。

在第 14 行中,我们沿着 y 轴从 0 到 100 以及沿着 x 轴从 0 到 100 的像素范围来形成一个 100×100 的正方形,并且将值(255,255,0)或者纯蓝色、纯绿色和无红色分配给该正方形内的所有像素。

第 16 行显示修改后的图像。

第 17 行等待用户按任意键退出程序。

图 2-6 显示了清单 2-2 的一些示例输出。

img/493065_1_En_2_Fig6_HTML.png

图 2-6

输出和修改的图像显示

如图 2-6 所示,修改后的图像在左上角有一个 100×100 像素的正方形,颜色为浅绿色,用 BGR 方案的(255,255,0)表示。

图画

OpenCV 提供了在图像上绘制形状的便捷方法。我们将学习如何使用以下方法在图像上绘制直线、矩形和圆形:

  • 线 : cv2.line()

  • 长方形 : cv2.rectangle()

  • : cv2.circle()

在图像上画线

我们将使用一种简单的方法在图像上画线,如下所示:

  1. 将图像加载到 NumPy 数组中。

  2. 确定直线起始位置的坐标。

  3. 确定直线终点的坐标。

  4. 设置线条的颜色。

  5. 或者,设置线条的粗细。

清单 2-3 演示了如何在图像上画线。

Filename: Listing_2_3.py
1    from __future__ import print_function
2    import cv2
3
4    # image path
5    image_path = "images/marsrover.png"
6    # Read or load image from its path
7    image = cv2.imread(image_path)
8
9    # set start and end coordinates
10   start = (0, 0)
11   end = (image.shape[1], image.shape[0])
12   # set the color in BGR
13   color = (255,0,0)
14   # set thickness in pixel
15   thickness = 4
16   cv2.line(image, start, end, color, thickness)
17
18   #display the modified image
19   cv2.imshow("Modified Image", image)
20   cv2.waitKey(0)

Listing 2-3Drawing a Line on an Image

下面是对代码的逐行解释。

第 1 行和第 2 行是通常的导入。从现在开始,我不会重复进口,除非我们有一个新的提及。

第 5 行是图像路径。

第 7 行实际上将图像加载到一个名为 image 的 NumPy 数组中。

第 10 行定义了绘制直线的起点坐标。回想一下,位置(0,0)是图像的左上角。

第 11 行指定了图像端点的坐标。您会注意到表达式(image.shape[1], image.shape[0])代表图像右下角的坐标。

你现在可能已经猜到我们在画一条对角线。

第 13 行设置我们要画的线的颜色,第 15 行设置它的粗细。

实际的线画在第 16 行。cv2.line()函数采用以下参数:

  • 图像编号。这就是我们正在划线的图像。

  • 开始坐标。

  • 结束坐标。

  • 颜色。

  • 厚度。(这是可选的。如果您不传递这个参数,我们的线条将有一个默认的厚度 1。)

最后,修改后的图像显示在第 19 行。第 20 行等待用户按任意键来终止程序。图 2-7 显示了我们刚刚画了一条线的图像的样本输出。

img/493065_1_En_2_Fig7_HTML.jpg

图 2-7

带有蓝色对角线的图像

在图像上绘制矩形

用 OpenCV 画矩形很容易。让我们直接深入代码(清单 2-4 )。我们将首先加载一个图像,并为其绘制一个矩形。我们会将修改后的图像保存到磁盘。

Filename: Listing_2_4.py
1    from __future__ import print_function
2    import cv2
3
4    # image path
5    image_path = "images/marsrover.png"
6    # Read or load image from its path
7    image = cv2.imread(image_path)
8    # set the start and end coordinates
9    # of the top-left and bottom-right corners of the rectangle
10   start = (100,70)
11   end = (350,380)
12   # Set the color and thickness of the outline
13   color = (0,255,0)
14   thickness = 5
15   # Draw the rectangle
16   cv2.rectangle(image, start, end, color, thickness)
17   # Save the modified image with the rectangle drawn to it.
18   cv2.imwrite("rectangle.jpg", image)
19   # Display the modified image
20   cv2.imshow("Rectangle", image)
21   cv2.waitKey(0)

Listing 2-4Loading an Image, Drawing a Rectangle to It, Saving It, and Displaying the Modified Image

下面是清单 2-4 的逐行解释。

1 号线和 2 号线是我们通常的进口货。

第 5 行指定了图像路径。

第 6 行从它的路径中读取图像。

第 10 行设置了我们想要在图像上绘制的矩形的起点。起点由矩形左上角的坐标组成。

第 11 行设置矩形的端点。这表示矩形右下角的坐标。

第 13 行设置颜色,第 14 行设置矩形轮廓的粗细。

第 16 行实际上绘制了矩形。我们使用 OpenCV 的rectangle()函数,它接受以下参数:

  • 保存图像像素值的 NumPy 数组

  • 起始坐标(矩形的左上角)

  • 结束坐标(矩形的右下角)

  • 轮廓的颜色

  • 轮廓的粗细

注意,第 16 行没有任何赋值操作符。换句话说,我们没有将来自cv2.rectangle()函数的返回值赋给任何变量。作为参数传递给cv2.rectangle()函数的 NumPy 数组image被修改。

第 18 行将修改后的画有矩形的图像保存到磁盘上的一个文件中。

第 20 行显示修改后的图像。

第 21 行调用waitKey()函数,允许图像保持显示在屏幕上,直到按下一个键。函数waitKey()无限期等待一个按键事件,或者等待一定的毫秒级延迟。由于操作系统在切换线程之间有一个最小时间间隔,在按键后,waitKey()函数不会等待作为参数传递给waitKey()函数的延迟时间。实际等待时间取决于按下按键和调用waitKey()功能时您的电脑可能正在运行的其他程序。

图 2-8 显示了画有矩形的图像的输出。

img/493065_1_En_2_Fig8_HTML.jpg

图 2-8

绘制了矩形的图像

在前面的例子中,我们首先从磁盘中读取一个图像,并在上面画了一个矩形。我们现在将稍微修改这个例子,并在空白画布上绘制矩形。我们将首先创建一个画布(而不是加载一个现有的图像),并在其上绘制一个矩形。然后,我们将保存并显示结果图像。参见清单 2-5 。

Filename: Listing 2_5.py
1    from __future__ import print_function
2    import cv2
3    import numpy as np
4
5    # create a new canvas
6    canvas = np.zeros((200, 200, 3), dtype = "uint8")
7    start = (10,10)
8    end = (100,100)
9    color = (0,0,255)
10   thickness = 5
11   cv2.rectangle(canvas, start, end, color, thickness)
12   cv2.imwrite("rectangle.jpg", canvas)
13   cv2.imshow("Rectangle", canvas)
14   cv2.waitKey(0)

Listing 2-5Drawing a Rectangle on a New Canvas and Saving the Image

在清单 2-5 中,除了第 3 行和第 6 行之外的所有行与清单 2-4 中的相同。

第 3 行导入了我们将用来创建画布的 NumPy 库。

第 6 行是我们创建图像的地方(称为画布)。我们的画布是 200×200 像素,每个像素保存三个通道(保存 BGR 值)。变量名canvas是一个 NumPy 数组,在本例中,它为每个像素保存一个零值。请注意,画布的每个像素值的数据类型是一个 8 位无符号整数(如第一章所述)。

你会怎么画一个实心矩形(意思是,用特定颜色填充的矩形)?

线索:厚度设为-1。

图 2-9 显示了清单 2-5 的输出。图 2-10 显示了一个画有实心矩形的画布。

img/493065_1_En_2_Fig10_HTML.jpg

图 2-10

厚度为-1 的实心矩形

img/493065_1_En_2_Fig9_HTML.jpg

图 2-9

边框粗细为 5 的矩形

在图像上画一个圆

在图像上画一个圆也同样容易。您可以创建自己的画布或加载现有图像,然后设置圆心坐标、半径、颜色和圆轮廓的粗细。

清单 2-6 显示了一段在空白画布上画圆的工作代码。图 2-11 显示了该代码清单的输出。

Filename: Listing_2_6.py
1    from __future__ import print_function
2    import cv2
3    import numpy as np
4
5    # create a new canvas
6    canvas = np.zeros((200, 200, 3), dtype = "uint8")
7    center = (100,100)
8    radius = 50
9    color = (0,0,255)
10   thickness = 5
11   cv2.circle(canvas, center, radius, color, thickness)
12   cv2.imwrite("circle.jpg", canvas)
13   cv2.imshow("My Circle", canvas)
14   cv2.waitKey(0)

Listing 2-6Drawing a Circle on a Canvas

清单 2-6 中的代码与清单 2-5 中的代码差别不大,除了第 7 行定义了圆心。

此外,第 8 行设置半径,第 9 行定义颜色,第 10 行设置圆的厚度。最后,第 11 行画圆并接受以下参数:

  • 要在其上绘制圆的图像。这是包含图像像素的 NumPy 数组。

  • 圆心的坐标。

  • 圆的半径。

  • 圆轮廓的颜色。

  • 轮廓的粗细。

img/493065_1_En_2_Fig11_HTML.jpg

图 2-11

画在黑色画布中心的圆

给你做个练习:

  1. 在画布的中心画一个实心圆。

  2. 画两个同心圆,最外圆的半径是内圆半径的 1.5 倍。

摘要

在这一章中,我们学习了图像的基础知识,从像素开始,以及它们如何在不同的配色方案中表示,即灰色和彩色。坐标系有助于定位特定的像素并处理它们的值。我们学习了如何在图像上绘制一些基本的形状,如直线、矩形和圆形。虽然这些都是非常基本和容易的,但它们是在图像处理中做任何事情的重要概念。

在下一章,我们将探索图像处理中使用的不同技术和算法。

三、图像处理技术

在计算机视觉应用中,图像通常从其来源获取,如相机、存储在计算机磁盘上的文件或来自另一个应用的流。在大多数情况下,这些输入图像从一种形式转换成另一种形式。例如,我们可能需要调整大小、旋转或改变它们的颜色。在某些情况下,我们可能需要删除背景像素或合并两幅图像。在其他情况下,我们可能需要找到图像中特定对象周围的边界。

本章通过 Python 和 OpenCV 中的例子探讨了图像转换的各种技术。我们本章的学习目标如下:

  • 探索最常用的转换技术

  • 学习图像处理中使用的算法

  • 学习清洁图像的技术,如降噪

  • 学习合并两个或多个图像或分割通道的技巧

  • 学习如何检测和绘制图像中对象周围的轮廓(边界)

转换

在处理任何计算机视觉问题时,您经常需要将图像转换成不同的形式。本章通过一组 Python 示例探索了不同的图像转换技术。

调整大小

让我们从第一个转换开始,调整大小。为了调整图像的大小,我们增加或减少图像的高度和宽度。纵横比是调整图像大小时要记住的一个重要概念。纵横比是宽度与高度的比例,通过宽度除以高度来计算。计算纵横比的公式如下:

  • 长宽比=宽度/高度

正方形图像的长宽比为 1:1,长宽比为 3:1 意味着宽度是高度的三倍。如果图像的高度为 300 像素,宽度为 600 像素,则其纵横比为 2:1。

调整大小时,保持原始纵横比可以确保调整后的图像看起来不会被拉伸或压缩。

清单 3-1 显示了以下两种不同的图像大小调整技术:

  • 在保持纵横比的同时,将图像调整到所需的像素大小。换句话说,如果您知道图像的期望高度,您可以使用纵横比计算相应的宽度。

  • 按因子调整图像大小。例如,将图像宽度放大 1.5 倍或高度放大 2.5 倍。

OpenCV 提供了一个函数cv2.resize()来执行这两种调整大小的技术。

Filename: Listing_3_1.py
1    from __future__ import print_function
2    import cv2
3    import numpy as np
4
5    # Load image
6    imagePath = "images/zebra.png"
7    image = cv2.imread(imagePath)
8
9    # Get image shape which returns height, width, and channels as a tuple. Calculate the aspect ratio
10   (h, w) = image.shape[:2]
11   aspect = w / h
12
13   # lets resize the image to  decrease height by half of the original image.
14   # Remember, pixel values must be integers.
15   height = int(0.5 * h)
16   width =  int(height * aspect)
17
18   # New image dimension as a tuple
19   dimension = (height, width)
20   resizedImage = cv2.resize(image, dimension, interpolation=cv2.INTER_AREA)
21   cv2.imshow("Resized Image", resizedImage)
22
23   # Resize using x and y factors
24   resizedWithFactors = cv2.resize(image, None, fx=1.2, fy=1.2, interpolation=cv2.INTER_LANCZOS4)
25   cv2.imshow("Resized with factors", resizedWithFactors)
26   cv2.waitKey(0)

Listing 3-1Code to Calculate Aspect Ratio and Resize the Image

清单 3-1 展示了如何使用 OpenCV 的cv2.resize()函数来调整图像的大小。resize()函数将以下参数作为参数:

  • 第一个参数是由 NumPy 数组表示的原始图像。

  • 第二个参数是要调整的尺寸。这是一个整数元组,表示调整后的图像的高度和宽度。如果您想要使用水平或垂直因子来调整大小,请将此参数作为None传递,稍后将对此进行解释。

  • 第三和第四个参数fxfy是水平(横向)和垂直(高度)方向上的调整大小因子。这两个参数是可选的。

  • 最后一个参数是插值。这是 OpenCV 内部用来调整图像大小的算法名称。可用的插补算法有INTER_AREAINTER_LINEARINTER_CUBICINTER_NEAREST。一会儿将简要描述这些算法。

    插值是在调整图像大小时计算像素值的过程。OpenCV 支持以下五种插值算法:

    INTER_LINEAR:这实际上是一个双线性插值,其中确定四个最近的邻居(2×2 = 4)并计算它们的加权平均值,以确定下一个像素的值。

    INTER_NEAREST:这使用最近邻插值方法,当给定某个空间中某个非给定点周围(相邻)点的函数值时,近似该点的函数值。换句话说,为了计算一个像素的值,它的最近邻被认为是插值函数的近似值。

    INTER_CUBIC:这使用双三次插值算法来计算像素值。与双线性插值类似,它使用 4×4 = 16 个最近邻来确定下一个像素的值。当速度不是一个问题时,双三次插值比双线性插值提供了更好的调整图像大小。

    INTER_LANCZOS4:这使用 8×8 最近邻插值。

    INTER_AREA:像素值的计算通过使用像素面积关系来执行(如 OpenCV 官方文档所述)。我们使用这种算法来创建无莫尔条纹的调整大小的图像。当图像尺寸被放大时,INTER_AREA类似于INTER_NEAREST方法。

让我们检查清单 3-1 中的代码。

第 1 行到第 3 行是库导入。

第 6 行分配图像路径,第 7 行将图像作为 NumPy 数组读取,并分配给一个名为image的变量。

NumPy 的 shape 函数返回数组中对象的尺寸。调用图像的 shape 函数以元组的形式返回通道的高度、宽度和数量。第 10 行通过指定索引长度 2 ( image.shape[,:2])仅检索高度和宽度。高度和宽度存储在变量hw中。

如果我们不指定索引长度,它将返回具有高度、宽度和通道的元组,如下所示:

(h, w, c) = image.shape[:]

在本例中,我们希望将图像尺寸缩小 50 %,同时保持原始的纵横比。我们可以简单地将原来的高度和宽度乘以 0.5,得到想要的高度和宽度。如果我们只知道期望的高度,我们可以通过将原始的新高度乘以长宽比来计算期望的宽度。第 15 行和第 16 行演示了这一点。

第 19 行将所需的高度和宽度设置为一个元组。

第 20 行调用 OpenCV 的cv2.resize()函数,并将原始图像 NumPy、期望的尺寸和插值算法(本例中为INTER_AREA)作为参数传递给resize()函数。

第 24 行演示了当我们知道图像的高度和/或宽度需要增加或减少的因素时,使用第二种方法的 resize 操作。在本例中,高度和宽度都放大了 1.2 倍。

图 3-1 和图 3-2 显示了我们的调整大小程序的示例输出。

img/493065_1_En_3_Fig2_HTML.jpg

图 3-2

调整图像大小

img/493065_1_En_3_Fig1_HTML.jpg

图 3-1

原象

翻译

图像平移是指沿着 x-y- 轴向左、向右、向上或向下移动图像。

移动图像时有两个主要步骤:定义一个平移矩阵和调用cv2.warpAffine函数。平移矩阵定义了移动的方向和量。warpAffine函数是执行实际移动的 OpenCV 函数。cv2.warpAffine函数有三个参数:图像数量、平移矩阵和图像尺寸。

让我们通过一个代码示例来理解这一点(参见清单 3-2 )。

Filename: Listing_3_2.py
1    from __future__ import print_function
2    import cv2
3    import numpy as np
4
5    #Load image
6    imagePath = "images/soccer-in-green.jpg"
7    image = cv2.imread(imagePath)
8
9    #Define translation matrix
10   translationMatrix = np.float32([[1,0,50],[0,1,20]])
11
12   #Move the image
13   movedImage = cv2.warpAffine(image, translationMatrix, (image.shape[1], image.shape[0]))
14
15   cv2.imshow("Moved image", movedImage)
16   cv2.waitKey(0)

Listing 3-2Image Translation Along the x- and y-Axes

清单 3-2 演示了翻译操作。平移矩阵在第 10 行定义,这里我们定义了移动方向,并定义了图像应该移动多少像素。下面是对第 10 行的解释。

在这个例子中,平移矩阵是 2×3 矩阵或 2D 阵列。

由[1,0,50]定义的第一行表示沿 x 轴向右移动 50 个像素。如果这个数组的第三个元素是负数,将向左移动。

由[0,1,20]表示的第二行定义了沿着 y 轴向下移动 20 个像素。如果第二行的第三个元素是负数,这将沿着 y 轴向上移动图像。

在第 13 行,我们调用 OpenCV 的warpAffine函数。该函数采用以下参数:

  • 我们要移动的图像的 NumPy 表示。

  • 定义移动方向和移动量的平移矩阵。

  • 最后一个参数是一个元组,它具有我们要在其中移动图像的画布的宽度和高度。在这个例子中,我们保持画布大小与图像的原始高度和宽度相同。

图 3-3 和图 3-4 显示了结果。

img/493065_1_En_3_Fig4_HTML.jpg

图 3-4

移动图像

img/493065_1_En_3_Fig3_HTML.jpg

图 3-3

原象

这里有一个练习:将一幅图像向左移动 50 像素,向上移动 60 像素。

循环

为了将图像旋转某个角度θ,我们首先使用 OpenCV 的cv2.getRotationMatrix2D.定义一个旋转矩阵。我将在清单 3-3 中解释如何创建这个旋转矩阵。要旋转图像,我们只需像前面的翻译一样调用相同的cv2.warpAffine函数。让我们逐行查看旋转代码。

Filename: Listing_3_3.py
1    from __future__ import print_function
2    import cv2
3    import numpy as np
4
5    # Load image
6    imagePath = "images/zebrasmall.png"
7    image = cv2.imread(imagePath)
8    (h,w) = image.shape[:2]
9
10   #Define translation matrix
11   center = (h//2, w//2)
12   angle = -45
13   scale = 1.0
14
15   rotationMatrix = cv2.getRotationMatrix2D(center, angle, scale)
16
17   # Rotate the image
18   rotatedImage = cv2.warpAffine(image, rotationMatrix, (image.shape[1], image.shape[0]))
19
20   cv2.imshow("Rotated image", rotatedImage)
21   cv2.waitKey(0)

Listing 3-3Image Rotation Around the Center of the Image

清单 3-3 展示了如何将图像围绕其中心旋转 45 度角(顺时针)。

第 11 行计算图像的中心。注意,我们通过使用//来划分高度和宽度,只得到它的整数部分。

第 12 行简单地给我们想要旋转图像的角度赋值。负值将顺时针旋转图像,而正值将逆时针旋转。

第 13 行设置旋转比例,它被设置为在旋转时调整图像的大小。值 1.0 在旋转后保持原始大小。如果我们将它设置为 0.5,旋转后的图像将会缩小一半。

在第 15 行,我们使用 OpenCV 的函数cv2.getRotationMatrix2D定义旋转矩阵,并传递以下参数:

  • 一个元组,表示图像需要围绕其旋转的点

  • 以度为单位的旋转角度

  • 调整比例

第 18 行按照旋转矩阵的定义旋转图像。我们使用我们用来翻译图像的相同的warpAffine函数。唯一的区别是,在旋转的情况下,我们传递在第 15 行创建的旋转矩阵。

第 20 行显示旋转的图像,第 21 行在显示的图像关闭之前等待按键。

图 3-5 和图 3-6 显示了我们代码的示例输出。

img/493065_1_En_3_Fig6_HTML.jpg

图 3-6

旋转图像

img/493065_1_En_3_Fig5_HTML.jpg

图 3-5

原象

轻弹

沿着 x 轴水平翻转图像,或者沿着 y 轴垂直翻转图像,都可以通过调用 OpenCV 的便捷函数cv2.flip()轻松完成。这个cv2.flip()函数有两个参数。

  • 原始图像

  • 翻转的方向

    • 0 表示垂直翻转。

    • 1 表示水平翻转。

    • -1 表示先水平翻转,再垂直翻转。

让我们看看清单 3-4 中的图像向不同方向翻转。

Filename: Listing_3_4.py
1    from __future__ import print_function
2    import cv2
3    import numpy as np
4
5    # Load image
6    imagePath = "images/zebrasmall.png"
7    image = cv2.imread(imagePath)
8
9    # Flip horizontally
10   flippedHorizontally = cv2.flip(image, 1)
11   cv2.imshow("Flipped Horizontally", flippedHorizontally)
12   cv2.waitKey(-1)
13
14   # Flip vertically
15   flippedVertically = cv2.flip(image, 0)
16   cv2.imshow("Flipped Vertically", flippedVertically)
17   cv2.waitKey(-1)
18   # Flip horizontally and then vertically
19   flippedHV = cv2.flip(image, -1)
20   cv2.imshow("Flipped H and V", flippedHV)
21   cv2.waitKey(-1)

Listing 3-4Image Flipping Horizontally, Vertically, and Then Horizontally plus Vertically

清单 3-4 不言自明。以防万一,这里是执行翻转的线的解释。

第 10 行调用cv2.flip()函数并传递原始图像和水平翻转的 0 值。

类似地,第 15 行垂直翻转图像,而第 19 行有一个参数-1,使图像先水平翻转,然后垂直翻转。图 3-7 到 3-10 显示了这些翻转的样子。

img/493065_1_En_3_Fig10_HTML.jpg

图 3-10

水平翻转,然后垂直翻转

img/493065_1_En_3_Fig9_HTML.jpg

图 3-9

垂直翻转

img/493065_1_En_3_Fig8_HTML.jpg

图 3-8

水平翻转

img/493065_1_En_3_Fig7_HTML.jpg

图 3-7

原象

种植

图像裁剪是指删除图像中不需要的外部区域。回想一下,OpenCV 将图像表示为 NumPy 数组。裁剪图像是通过对图像数组进行切片来实现的。OpenCV 中没有裁剪图像的特殊功能。我们使用 NumPy 数组特征对图像进行切片。清单 3-5 展示了如何裁剪图像。

Filename: Listing_3_5.py
1    from __future__ import print_function
2    import cv2
3    import numpy as np
4
5    # Load image
6    imagePath = "images/zebrasmall.png"
7    image = cv2.imread(imagePath)
8    cv2.imshow("Original Image", image)
9    cv2.waitKey(0)
10
11   # Crop the image to get only the face of the zebra
12   croppedImage = image[0:150, 0:250]
13   cv2.imshow("Cropped Image", croppedImage)
14   cv2.waitKey(0)

Listing 3-5Image Cropping

第 12 行显示了如何分割 NumPy 数组。在这个例子中,我们使用 150 像素的高度和 250 像素的宽度来裁剪我们的图像,只提取斑马的面部部分。

图 3-11 显示原始图像,图 3-12 显示裁剪后的图像。

img/493065_1_En_3_Fig12_HTML.jpg

图 3-12

裁剪的图像

img/493065_1_En_3_Fig11_HTML.jpg

图 3-11

原象

图像算术和位运算

在构建计算机视觉应用时,您经常需要增强输入图像的属性。为此,您可能需要执行某些算术运算,如加法和减法,以及按位运算,如 OR、and、NOT 和 XOR。

到目前为止,我们已经了解到图像中的每个像素可以有 0 到 255 之间的任何整数值。当你给一个像素加上一个常数,使得结果值大于 255 或者小于 0,如果你从中减去一个常数,会发生什么?例如,假设图像中的一个像素值为 230,您给它加上 30。当然,像素的值不能是 260。那么,我们该怎么办呢?我们应该截断该值以保持像素最大值为 255,还是将其绕回以使其为 4(意思是在 255 之后,回到 0,并在 255 之后添加余数)?

当像素值超出范围[0,255]时,有两种方法来处理这种情况:

  • 饱和运行(或微调):本次运行,230+30 255。

  • 模运算:这里是这样执行模运算的:(230+30)% 256 4。

您可以使用 OpenCV 和 NumPy 的内置函数来执行算术运算。但是,它们处理操作的方式不同。

OpenCV 的加法是饱和操作。另一方面,NumPy 执行模运算。

请注意 NumPy 和 OpenCV 之间的区别,因为这两种技术产生不同的结果,并且在哪里使用它们取决于您的情况和需求。

添加

OpenCV 提供了两种方便的方法来添加两幅图像。

  • cv2.add(),它将两个大小相等的图像作为参数,并将它们的像素值相加以产生结果。

  • cv2.addWeighted(),一般用于两幅图像的融合。稍后将提供关于此功能的更多细节。

请注意,要添加两个图像,它们必须具有相同的深度和类型。

让我们写一些代码来理解这两个加法有什么不同。参见清单 3-6 。

Filename: Listing_3_6.py
1    from __future__ import print_function
2    import cv2
3    import numpy as np
4
5    image1Path = "images/zebra.png"
6    image2Path = "images/nature.jpg"
7
8    image1 = cv2.imread(image1Path)
9    image2 = cv2.imread(image2Path)
10
11   # resize the two images to make them of the same dimension. This is a must to add two images
12   resizedImage1 = cv2.resize(image1,(300,300),interpolation=cv2.INTER_AREA)
13   resizedImage2 = cv2.resize(image2,(300,300),interpolation=cv2.INTER_AREA)
14
15   # This is a simple addition of two images
16   resultant = cv2.add(resizedImage1, resizedImage2)
17
18   # Display these images to see the difference
19   cv2.imshow("Resized 1", resizedImage1)
20   cv2.waitKey(0)
21
22   cv2.imshow("Resized 2", resizedImage2)
23   cv2.waitKey(0)
24
25   cv2.imshow("Resultant Image", resultant)
26   cv2.waitKey(0)
27
28   # This is weighted addition of the two images
29   weightedImage = cv2.addWeighted(resizedImage1,0.7, resizedImage2, 0.3, 0)
30   cv2.imshow("Weighted Image", weightedImage)
31   cv2.waitKey(0)
32
33   imageEnhanced = 255*resizedImage1
34   cv2.imshow("Enhanced Image", imageEnhanced)
35   cv2.waitKey(0)
36
37   arrayImage = resizedImage1+resizedImage2
38   cv2.imshow("Array Image", arrayImage)
39   cv2.waitKey(0)

Listing 3-6Addition of Two Images

第 8 行和第 9 行从磁盘加载了两个不同的映像。正如我前面提到的,图像必须有相同的大小和深度才能加在一起;您可能已经猜到了第 12 行和第 13 行的目的。图像大小调整为 300×300 像素。

第 16 行是这两个图像相加的地方。我们使用 OpenCV 的简单加法函数cv2.add(),它将两幅图像作为参数。参见图 3-15 中的输出图像,查看两幅图像简单相加的结果。

在第 29 行,我们使用 OpenCV 的cv2.addWeighted()函数进行加权加法,工作方式如下:

  • 反相结果=x 图像 1+【β】x 图像 2+(1)****

****其中𝝰是图像 1 的权重,𝛃是图像 2 的权重,𝛄是常数。通过改变这些权重的值,我们创建了期望的加法效果。

通过查看前面的等式,您可以很容易地猜出需要传递给函数cv2.addWeighted()的参数。以下是参数列表:

  • 图像 1 的 NumPy 数组

  • 图像 1 的权重𝝰(在我们的示例代码中,我们传递了一个值 0.7)

  • 图像 2 数组的数量

  • 图像 2 的权重𝛃(在示例代码中我们传递了值 0.3)

  • 最后一个参数,𝛄(在我们的例子中我们传递了一个零值)

让我们检查清单 3-6 的输入和输出。图 3-13 和图 3-14 为原始图像,调整为 300x300,使其尺寸相等。

图 3-15 是使用函数add()将这两幅图像相加后的输出。

图 3-16 是使用功能addWeighted()将输入相加后的结果图像。

img/493065_1_En_3_Fig14_HTML.jpg

图 3-14

添加的原始图像

img/493065_1_En_3_Fig13_HTML.jpg

图 3-13

原象

通过参考图 3-15 和图 3-16 所示的输出,注意简单addaddWeighted功能之间的区别。

img/493065_1_En_3_Fig16_HTML.jpg

图 3-16

cv2.addWeighted()的结果

img/493065_1_En_3_Fig15_HTML.jpg

图 3-15

cv2.add()的结果

减法

图像相减是指从一幅图像的对应像素值中减去另一幅图像的像素值。我们也可以从图像像素中减去一个常数。当我们减去两个图像时,重要的是要注意这两个图像必须具有相同的大小和深度。

当你从一幅图像中减去它本身会发生什么?那么,合成图像的所有像素值将为零(意味着黑色)。该属性在检测图像中的任何变化/改变时是有用的。如果没有变化,两幅图像相减的结果就是一幅全黑的图像。

减去图像的另一个原因是消除任何不均匀的部分或阴影。

我们将通过代码示例看到一些有趣的图像减法结果。参见清单 3-7 。

Filename: Listing_3_7.py
1    import cv2
2    import numpy as np
3
4
5    image1Path = "images/cat1.png"
6    image2Path = "images/cat2.png"
7
8    image1 = cv2.imread(image1Path)
9    image2 = cv2.imread(image2Path)
10
11   # resize the two images to make them of the same dimensions. This is a must to subtract two images
12   resizedImage1 = cv2.resize(image1,(int(500*image1.shape[1]/image1.shape[0]), 500),interpolation=cv2.INTER_AREA)
13   resizedImage2 = cv2.resize(image2,(int(500*image2.shape[1]/image2.shape[0]), 500),interpolation=cv2.INTER_AREA)
14
15   cv2.imshow("Cat 1", resizedImage1)
16   cv2.imshow("Cat 2", resizedImage2)
17
18   # Subtract image 1 from 2
19   cv2.imshow("Diff Cat1 and Cat2",cv2.subtract(resizedImage2, resizedImage1))
20   cv2.waitKey(0)
21
22
23   # subtract images 2 from 1
24   subtractedImage = cv2.subtract(resizedImage1, resizedImage2)
25   cv2.imshow("Cat2 subtracted from Cat1", subtractedImage)
26   cv2.waitKey(0)
27
28   # Numpy Subtraction Cat2 from Cat1
29   subtractedImage2 = resizedImage2 - resizedImage1
30   cv2.imshow("Numpy Subracts Images", subtractedImage2)
31   cv2.waitKey(0)
32
33   # A constant subtraction
34   subtractedImage3 = resizedImage1 - 50
35   cv2.imshow("Constant Subtracted from the image", subtractedImage3)
36   cv2.waitKey(0)

Listing 3-7Image Subtraction

清单 3-7 展示了一些有趣的图像减法行为。这是我们在清单中的内容。

第 5 行到第 9 行是我们从磁盘加载图像的地方(从目录路径)。我们正在加载两只猫的图像,我们试图确定这两只看起来很像的猫是否有什么不同。图 3-17 和 3-18 所示的图像是本例中使用的输入图像。

第 12 行和第 13 行用于调整图像的大小,以确保它们的尺寸相同。请记住,这是必须减去两个图像数组。

在第 19 行,我们显示了从cat2.中减去cat1的结果。为了确定差异,我们使用了 OpenCV 的cv2.subtract()函数,并传递了两幅图像的 NumPy 表示(调整了大小的图像)。在这种情况下,我们要从cat2中减去cat1;因此,我们首先传递resizedImage2变量,然后将resizedImage1作为函数中的第二个参数。从图 3-19 和图 3-20 所示的输出中可以明显看出,顺序很重要。

为了演示顺序的效果,第 24 行在cv2.subtract()函数中将resizedImage1放在第一位,将resizedImage2作为第二个参数。

第 29 行没有使用 OpenCV 的减法函数。这是一个简单的 NumPy 数组减法。注意图 3-21 中显示的输出差异。

第 34 行从图像中减去一个常数。输出如图 3-22 所示。

img/493065_1_En_3_Fig22_HTML.jpg

图 3-22

从图像中减去的常数

img/493065_1_En_3_Fig21_HTML.jpg

图 3-21

数字减法

img/493065_1_En_3_Fig20_HTML.jpg

图 3-20

从图像 1 中减去图像 2

img/493065_1_En_3_Fig19_HTML.jpg

图 3-19

从图像 2 中减去图像 1

img/493065_1_En_3_Fig18_HTML.jpg

图 3-18

Cat2 图像

img/493065_1_En_3_Fig17_HTML.jpg

图 3-17

第一类图像

到目前为止,我们已经学习了两种强大的图像算术技术:加法和减法。现在让我们学习如何对图像像素执行按位逻辑运算。

位运算

计算机视觉中一些最有用的运算是位运算,包括 AND、OR、NOT 和 XOR。

如果您还记得您的布尔代数类,这些位运算是二元运算,并且只处理像素的两种状态:开和关。在灰度图像中,像素可以是 0 到 255 之间的任何值。那么,我们把“开”叫做什么,把“关”叫做什么呢?在图像处理中,对于灰度二值图像,像素值 0 表示关闭,大于 0 的值表示打开。基于像素开或关的概念,我们将探索下面的位运算。

如果“a”和“b”都是 1,则两个操作数“a”和“b”的按位“与”结果为 1;否则,结果为 0。

在图像处理中,两个图像数组的按位 AND 运算计算元素间的合取。需要注意的是,两个数组的维数必须相等,才能执行按位 AND 运算。也可以对数组和标量执行按位 AND 运算。

OpenCV 提供了一个方便的函数cv2.bitwise_and(imageArray1, imageAyyar2)来执行按位 AND 运算。该函数将两个图像数组作为参数。清单 3-8 展示了按位 AND 运算。

运筹学

如果“a”和“b”中的一个或两个为 1,则两个操作数“a”和“b”的按位“或”运算结果为 1;否则,结果为 0。按位 OR 运算计算两个数组或一个数组和一个标量的元素析取。在 OpenCV 中,函数cv2.bitwise_or(imageArray1, imageArray2)计算两个输入数组的按位 OR。清单 3-8 显示了 OR 运算的一个工作示例。

按位 NOT 反转其操作数的位值。OpenCV 的cv2.bitwise_not(imageArray)函数只接受一个图像数组作为参数,对该图像执行按位非运算。参见清单 3-8 中的示例。

异或运算

如果两个操作数“a”和“b”都是 1,但不是,则两个操作数“a”和“b”的按位异或结果为 1;否则,结果为 0。OpenCV 提供了一个方便的函数cv2.bitwise_xor(imageArray1, imageArray2)来执行按位异或。同样,两个图像数组的维数必须相等。清单 3-8 展示了一个按位异或的工作示例。

下表总结了我们将用于各种图像处理需求的按位运算,例如遮罩:

|

操作员

|

用法

|

描述

|
| --- | --- | --- |
| 按位 AND | a 和 b | 在两个操作数的对应位都为 1 的每个位位置返回 1 |
| 按位或 | a 还是 b | 在一个或两个操作数的对应位为 1 的每个位位置返回 1 |
| 按位异或 | 就是 | 在每个位位置返回 1,其中任一操作数(但不是两个操作数)的对应位为 1 |
| 按位非 | 一个也不 | 反转其操作数的位 |

让我们用清单 3-8 中的程序来理解这些位运算。我们将首先创建两个图像——一个圆形和一个正方形——并执行位运算来查看它们的效果。

Filename: Listing_3_8.py
1    import cv2
2    import numpy as np
3
4    # create a circle
5    circle = cv2.circle(np.zeros((200, 200, 3), dtype = "uint8"), (100,100), 90, (255,255,255), -1)
6    cv2.imshow("A white circle", circle)
7    cv2.waitKey(0)
8
9    # create a square
10   square = cv2.rectangle(np.zeros((200,200,3), dtype= "uint8"), (30,30), (170,170),(255,255,255), -1)
11   cv2.imshow("A white square", square)
12   cv2.waitKey(0)
13
14   #bitwise AND
15   bitwiseAnd = cv2.bitwise_and(square, circle)
16   cv2.imshow("AND Operation", bitwiseAnd)
17   cv2.waitKey(0)
18
19   #bitwise OR
20   bitwiseOr = cv2.bitwise_or(square, circle)
21   cv2.imshow("OR Operation", bitwiseOr)
22   cv2.waitKey(0)
23
24   #bitwise XOR
25   bitwiseXor = cv2.bitwise_xor(square, circle)
26   cv2.imshow("XOR Operation", bitwiseXor)
27   cv2.waitKey(0)
28
29   #bitwise NOT
30   bitwiseNot = cv2.bitwise_not(square)
31   cv2.imshow("NOT Operation", bitwiseNot)
32   cv2.waitKey(0)

Listing 3-8Bitwise Operations

让我们了解一下清单 3-8 中是怎么回事。

第 5 行在 200×200 画布的中心创建了一个白色圆圈。参见清单 2-5 了解如何在画布上画一个圆。

类似地,第 10 行在 200×200 的画布上绘制了一个白色正方形。参见清单 2-4 了解如何在画布上绘制矩形。

第 15 行显示了cv2.bitwise_and()函数的用法。该函数的参数是圆形和方形图像(由 NumPy 数组表示)。

类似地,第 20 行和第 25 行分别显示了cv2.bitwise_or()cv2.bitwise_xor()操作。

所有这三个用于 AND、or 和 XOR 的函数都需要两个数组来运算。

第 30 行显示了cv2.bitwise_not()函数,它只接受一个参数来计算按位 NOT。

图 3-23 至 3-28 显示了列表 3-8 的输出。

img/493065_1_En_3_Fig28_HTML.jpg

图 3-28

按位非

img/493065_1_En_3_Fig27_HTML.jpg

图 3-27

按位异或

img/493065_1_En_3_Fig26_HTML.jpg

图 3-26

按位或

img/493065_1_En_3_Fig25_HTML.jpg

图 3-25

按位 AND

img/493065_1_En_3_Fig24_HTML.jpg

图 3-24

白色正方形

img/493065_1_En_3_Fig23_HTML.jpg

图 3-23

白色圆圈

掩饰

遮蔽是计算机视觉中最强大的技术之一。遮罩是指图像的“隐藏”或“过滤”。

当我们屏蔽一幅图像时,我们用其他图像隐藏了图像的一部分。换句话说,我们通过在图像的剩余部分应用遮罩来将焦点放在图像的一部分上。例如,图 3-29 中有数字 1、2 和 3,而图 3-30 是带有白色剪裁的黑色图像。当我们混合这两个图像时,数字 1 和 3 将被隐藏,唯一可见的数字是数字 2。屏蔽的结果如下图 3-31 所示。

img/493065_1_En_3_Fig31_HTML.png

图 3-31

掩蔽效应

img/493065_1_En_3_Fig30_HTML.png

图 3-30

遮罩影像

img/493065_1_En_3_Fig29_HTML.png

图 3-29

原象

掩蔽技术应用于图像的平滑或模糊以及检测图像内的边缘和轮廓。掩蔽技术也用于目标检测,我们将在本书后面探讨。

清单 3-9 展示了如何使用 OpenCV 执行屏蔽。

Filename: Listing_3_9.py
1    import cv2
2    import numpy as np
3
4    # Load an image
5    natureImage = cv2.imread("images/nature.jpg")
6    cv2.imshow("Original Nature Image", natureImage)
7
8    # Create a rectangular mask
9    maskImage = cv2.rectangle(np.zeros(natureImage.shape[:2], dtype="uint8"), (50, 50), (int(natureImage.shape[1])-50, int(natureImage.shape[0] / 2)-50), (255, 255, 255), -1)
10
11   cv2.imshow("Mask Image", maskImage)
12   cv2.waitKey(0)
13
14   # Using bitwise_and operation perform masking. Notice the mask=maskImage argument
15   masked = cv2.bitwise_and(natureImage, natureImage, mask=maskImage)
16   cv2.imshow("Masked image", masked)
17   cv2.waitKey(0)

Listing 3-9Masking Using Bitwise AND Operation

在 OpenCV 中,图像遮罩是通过使用按位 AND 运算来执行的(还记得按位运算吗?).清单 3-9 展示了一个简单的例子,展示了如何遮蔽图像的一个区域。对于这个例子,我们的目标是提取如图 3-32 所示的云的矩形部分。

清单 3-9 的第 5 行你现在应该很熟悉了。我们在这里所做的就是加载图像(图 3-32 )。

在第 9 行,我们创建了一个黑色的画布,顶部有一个白色的矩形区域(有一些空白)。画布的大小与原始图像的大小相同。注意在图 3-33 中,较大的矩形在顶部有另一个矩形白色部分,该矩形的其余区域为黑色。

第 15 行是执行屏蔽的地方。注意,我们使用的是cv2.bitwise_and()函数,它带有两个强制参数,在本例中是原始图像本身和一个可选的屏蔽参数(mask=maskImage)。这里发生的事情是,这个函数计算图像与其自身的 AND 运算,并按照参数mask=maskImage.的指示应用遮罩。当 OpenCV 看到这个mask参数时,它将只检查遮罩(maskImage)数组中打开的那些像素。该屏蔽操作的输出如图 3-34 所示。

img/493065_1_En_3_Fig34_HTML.jpg

图 3-34

屏蔽图像

img/493065_1_En_3_Fig33_HTML.jpg

图 3-33

用于从图 3-32 中提取云的遮罩

img/493065_1_En_3_Fig32_HTML.jpg

图 3-32

要遮罩的原始图像

标记是计算机视觉中最常用的图像处理技术之一。我们将在机器学习和神经网络的后续章节中了解更多关于它的实际应用。

拆分和合并频道

回想一下第二章中的内容,一幅彩色图像由多个通道(R、G、B)组成。我们已经学习了如何访问这些通道,并将它们表示为 NumPy 数组。在这一节中,我们将学习如何分割这些通道,并将它们存储为单独的图像。OpenCV 提供了一个方便的函数split()来做到这一点。使用这个split()函数,我们可以将图像分割成相应的颜色分量。这里有一个工作代码示例来说明这一点。对于这个例子,我们将再次把我们的“自然”图像(如图 3-32 所示)分割成它的组成颜色。

在清单 3-10 中,第 5 行加载图像。第 8 行将图像分成三个部分,并将它们存储在单独的 NumPy 变量中(bgr)。回想一下,NumPy 以蓝色、绿色和红色(BGR)序列存储颜色,而不是以 RGB 序列存储颜色。第 11、14 和 17 行显示了这些分割图像。输出如图 3-35 、 3-36 和 3-37 所示。

img/493065_1_En_3_Fig37_HTML.jpg

图 3-37

蓝色信道

img/493065_1_En_3_Fig36_HTML.jpg

图 3-36

绿色通道

img/493065_1_En_3_Fig35_HTML.png

图 3-35

红色通道

Filename: Listing_3_10.py
1    import cv2
2    import numpy as np
3
4    # Load the image
5    natureImage = cv2.imread("images/nature.jpg")
6
7    # Split the image into component colors
8    (b,g,r) = cv2.split(natureImage)
9
10   # show the blue image
11   cv2.imshow("Blue Image", b)
12
13   # Show the green image
14   cv2.imshow("Green image", g)
15
16   # Show the red image
17   cv2.imshow("Red image", r)
18
19   cv2.waitKey(0)

Listing 3-10Splitting Channels into Color Components

我们可以通过使用 OpenCV 的merge()函数来合并通道,该函数采用 BGR 序列中的数组。清单 3-11 显示了merge()函数的用法。

Filename: Listing_3_11.py
1    import cv2
2    import numpy as np
3
4    # Load the image
5    natureImage = cv2.imread("images/nature.jpg")
6
7    # Split the image into component colors
8    (b,g,r) = cv2.split(natureImage)
9
10   # show the blue image
11   cv2.imshow("Blue Image", b)
12
13   # Show the green image
14   cv2.imshow("Green image", g)
15
16   # Show the red image
17   cv2.imshow("Red image", r)
18
19   merged = cv2.merge([b,g,r])
20   cv2.imshow("Merged Image", merged)
21   cv2.waitKey(0)

Listing 3-11Split and Merge Functions

第 5 行加载图像。第 8 行到第 17 行与我们之前的 split 函数相关。我们进行了拆分,因此我们有三个组件来演示merge()功能。

第 19 行是我们合并频道的地方。我们简单地将单个通道作为参数传递给merge()函数。请注意,通道是按 BGR 顺序排列的。执行前面的程序,观察最终输出。你拿到原始图像了吗?

分割和合并是有助于为机器学习执行特征工程的图像处理技术。我们将在接下来的章节中应用其中的一些概念。

使用平滑和模糊减少噪声

平滑,也称为模糊,是一种重要的图像处理技术,用于减少图像中存在的噪声。我们在图像中通常会遇到以下类型的噪声:

  • 椒盐噪声:包含随机出现的黑白像素

  • 脉冲噪声:白色像素随机出现

  • 高斯噪声:强度变化遵循高斯正态分布

在本节中,我们将探讨以下用于降噪的模糊/平滑技术。

均值滤波或平均

在平均技术中,我们取图像的一小部分,比如说 k×k 个像素。图像的这一小部分被称为滑动窗口。我们从左到右和从上到下移动这个滑动窗口。这个 k×k 矩阵中心的像素被它周围所有像素的平均值所取代。这个 k×k 矩阵也被称为卷积核或者简称为。典型地,该内核被视为奇数,因此可以计算出明确的中心。内核越大,图像越模糊。例如,与 3×3 内核相比,5×5 内核将产生更模糊的图像。

OpenCV 提供了一个方便的函数来模糊图像。函数cv2.blur()用于通过使用均值滤波或平均技术来模糊图像。这个函数有两个参数。

  • 需要模糊的原始图像的数字表示

  • k×k 核矩阵

清单 3-12 显示了使用不同内核大小的图像模糊。

Filename: Listing_3_12.py
1    import cv2
2    import numpy as np
3
4    # Load the image
5    park = cv2.imread("images/nature.jpg")
6    cv2.imshow("Original Park Image", park)
7
8    #Define the kernel
9    kernel = (3,3)
10   blurred3x3 = cv2.blur(park,karnal)
11   cv2.imshow("3x3 Blurred Image", blurred3x3)
12
13   blurred5x5 = cv2.blur(park,(5,5))
14   cv2.imshow("5x5 Blurred Image", blurred5x5)
15
16   blurred7x7 = cv2.blur(park, (7,7))
17   cv2.imshow("7x7 Blurred Image", blurred7x7)
18   cv2.waitKey(0)

Listing 3-12Smoothing/Blurring by Mean Filtering or Averaging

像往常一样,我们首先加载图像并将其赋给一个数组变量(清单 3-12 中第 5 行的park变量)。

第 9 行定义了一个 3×3 内核。

在第 10 行,我们使用了cv2.blur()函数,并将park图像和kernel作为参数传递。这将产生一个使用 3×3 内核的模糊图像。

为了比较内核大小的影响,第 13 行和第 16 行使用了 5×5 和 7×7 的内核大小。注意在图 3-38 到 3-41 中,随着内核大小的增加,模糊度的增加顺序。

img/493065_1_En_3_Fig41_HTML.jpg

图 3-41

使用 7×7 内核模糊

img/493065_1_En_3_Fig40_HTML.jpg

图 3-40

使用 5×5 内核模糊

img/493065_1_En_3_Fig39_HTML.jpg

图 3-39

使用 3×3 内核进行模糊处理

img/493065_1_En_3_Fig38_HTML.jpg

图 3-38

原象

高斯滤波

高斯滤波是图像处理中最有效的模糊技术之一。这用于减少高斯噪声。与平均技术相比,这种模糊技术给出了更自然的平滑结果。在这个过滤过程中,我们提供了一个高斯核,而不是一个装箱的固定核。

高斯核由 X 和 Y 方向上的高度、宽度和标准偏差组成。

OpenCV 提供了一个方便的函数cv2.GaussianBlur()来执行高斯滤波。此函数cv2.GaussianBlur()采用以下参数:

  • NumPy 数组表示的图像。

  • k×k 矩阵作为内核的高度和宽度。

  • sigmaXsigmaY是 X 和 Y 方向的标准差。

以下是关于标准差的几点说明:

  • 如果仅指定了sigmaX,则sigmaYsigmaX.相同

  • 如果两者都为零,则根据内核大小计算标准偏差。

  • OpenCV 提供了一个函数cv2.getGaussianKernel(),用于自动计算标准偏差。

对于那些有兴趣知道高斯滤波中使用的公式的人,这里是高斯方程:

$$ {G}_0\left(x,y\right)={Ae}^{\frac{-{\left(x-{\mu}_x\right)}²}{2{\sigma}_x²}+\frac{-{\left(y-{\mu}_y\right)}²}{2{\sigma}_y²}} $$

其中 μ 是平均值(峰值),而σ2 是方差(对于变量xy中的每一个)。

清单 3-13 是一个演示高斯模糊的工作示例。

Filename: Listing_3_13.py
1    import cv2
2    import numpy as np
3
4    # Load the park image
5    parkImage = cv2.imread("images/park.jpg")
6    cv2.imshow("Original Image", parkImage)
7
8    # Gaussian blurring with 3x3 kernel and 0 for standard deviation to calculate from the kernel
9    GaussianFiltered = cv2.GaussianBlur(parkImage, (5,5), 0)
10   cv2.imshow("Gaussian Blurred Image", GaussianFiltered)
11
12   cv2.waitKey(0)

Listing 3-13Smoothing Using the Gaussian Technique

这里我们再次开始加载我们的park图像(清单 3-13 的第 5 行)。第 9 行展示了 OpenCV 的cv2.GaussianBlur()函数的用法。我们提供了一个 5×5 内核和一个 0 来告诉 OpenCV 计算内核大小的标准偏差。

图 3-42 为原图,图 3-43 为高斯模糊效果。

img/493065_1_En_3_Fig43_HTML.jpg

图 3-43

5×5 核高斯模糊图像

img/493065_1_En_3_Fig42_HTML.jpg

图 3-42

原象

中间模糊

中值模糊是减少椒盐噪声的有效技术。中值模糊类似于均值模糊,只是内核的中心值被周围像素的中值所取代。我们使用 OpenCV 的cv2.medianBlur()函数来减少椒盐噪声(参见清单 3-14 )。该函数采用以下两个参数:

  • 需要模糊的原始图像。

  • 内核大小 k. 注意,在均值模糊的情况下,内核大小 k 类似于 k×k 矩阵。

Filename: Listing_3_14.py
1    import cv2
2
3    # Load a noisy image
4    saltpepperImage = cv2.imread("images/salt-pepper.jpg")
5    cv2.imshow("Original noisy image", saltpepperImage)
6
7    # Median filtering for noise reduction
8    blurredImage3 = cv2.medianBlur(saltpepperImage, 3)
9    cv2.imshow("Blurred image 3", blurredImage3)
10
11   # Median filtering for noise reduction
12   blurredImage5 = cv2.medianBlur(saltpepperImage, 5)
13   cv2.imshow("Blurred image 5", blurredImage5)
14
15
16   cv2.waitKey(0)

Listing 3-14Salt-and-Pepper Noise Reduction Using Median Blurring

清单 3-14 显示了cv2.medianBlur()功能的使用。第 8 行和第 12 行从第 4 行加载的原始图像创建模糊图像。注意,函数的kernel参数是一个标量,而不是元组或矩阵。

图 3-44 显示了带有椒盐噪声的图像。请注意,当我们应用不同的内核大小时,噪声降低的程度是不同的。图 3-45 显示了应用内核大小 3 时的输出图像。注意图 3-45 仍然有一些噪声。图 3-45 显示了当内核大小为 5 且应用了中值模糊时,几乎没有噪声的清晰输出。

img/493065_1_En_3_Fig46_HTML.jpg

图 3-46

内核大小为 5 的中值模糊(噪声几乎被移除)

img/493065_1_En_3_Fig45_HTML.jpg

图 3-45

内核大小为 3 的中值模糊(有一些噪点)

img/493065_1_En_3_Fig44_HTML.jpg

图 3-44

椒盐噪声图像

图 3-44 显示了带有椒盐噪声的噪声图像。你会注意到中值模糊在减少噪点方面做得相当不错。图 3-45 显示了使用核大小为 3 的模糊图像。如图 3-46 所示,核大小为 5 时效果较好。

双侧模糊

前三种模糊技术产生模糊的图像,其副作用是我们丢失了图像的边缘。为了在保留边缘的同时模糊图像,我们使用双边模糊,这是高斯模糊的增强。双边模糊需要两个高斯分布来执行计算。

第一个高斯函数考虑空间邻居(x 和 y 空间中靠得很近的像素)。第二个高斯函数考虑相邻像素的像素强度。这确保了只有那些与中心像素具有相似亮度的像素被考虑用于模糊,而边缘保持完整,因为与其他像素相比,边缘往往具有更高的亮度。

虽然这是一种优越的模糊技术,但与其他技术相比,它的速度较慢。

我们使用cv2.bilateralFilter()来执行这种模糊。该函数的参数如下:

  • 需要模糊的图像。

  • 像素邻域的直径。

  • 颜色值。较大的颜色值意味着在计算模糊度时将考虑更多的邻域像素颜色。

  • 空间或距离。较大的空间值意味着将考虑远离中心像素的像素。

让我们检查清单 3-15 来理解双边过滤。

Filename: Listing_3_15.py
1    import cv2
2
3    # Load a noisy image
4    noisyImage = cv2.imread("images/nature.jpg")
5    cv2.imshow("Original image", noisyImage)
6
7    # Bilateral Filter with
8    fileteredImag5 = cv2.bilateralFilter(noisyImage, 5, 150,50)
9    cv2.imshow("Blurred image 5", fileteredImag5)
10
11   # Bilateral blurring with kernal 7
12   fileteredImag7 = cv2.bilateralFilter(noisyImage, 7, 160,60)
13   cv2.imshow("Blurred image 7", fileteredImag7)
14
15   cv2.waitKey(0)

Listing 3-15Bilateral Blurring Example

如清单 3-15 所示,第 8 行和第 12 行用于使用cv2.bilateralFilter()模糊输入图像。第一组参数(第 8 行)是用 NumPy 表示的图像像素、内核或直径、颜色阈值和距中心的距离。

图 3-47 至 3-49 显示了列表 3-15 的输出。

img/493065_1_En_3_Fig49_HTML.jpg

图 3-49

直径为 7 的双侧模糊

img/493065_1_En_3_Fig48_HTML.jpg

图 3-48

直径为 5 的双边模糊

img/493065_1_En_3_Fig47_HTML.jpg

图 3-47

原象

我们已经学习了模糊或平滑图像的不同技术。我们将在整本书中使用这些模糊技术。

在下一节中,我们将学习如何借助一种叫做阈值的技术将灰度图像转换成二进制图像。

阈值二值化

图像二值化是将灰度图像转换为二值图像(黑白图像)的过程。我们应用一种叫做阈值的技术对图像进行二值化。

我们首先决定一个阈值。大于该阈值的像素值被更改为 255,小于该阈值的像素值被设置为 0。生成的图像将只有两个像素值,即 0 和 255,它们是黑白颜色值。因此,灰度图像被转换成黑白图像(也称为二进制 ima ge)。

二值化技术用于从图像中提取重要信息,例如,从扫描的文档中提取光学字符识别(OCR)中的字符。

OpenCV 支持以下类型的阈值技术。

简单阈值处理

在简单的阈值分割中,我们手动选择一个阈值, T. 所有大于这个 T 的像素被设置为 255,所有小于等于 T 的像素被设置为 0。

有时,进行二进制化的逆过程会很有帮助,在这种情况下,大于阈值的像素被设置为 0,小于阈值的像素被设置为 255。

让我们看一个如何使用 OpenCV 的cv2.threshold()函数对图像进行二值化的例子。该函数采用以下参数:

  • 需要二值化的原始灰度图像

  • 阈值 T

  • 像素值大于阈值时将设置的最大值

  • 阈值方法,如cv2.THRESH_BINARYcv2.THRESH_BINARY_INV

threshold 函数返回包含阈值和二值化图像的元组。

清单 3-16 将灰度图像转换为二值图像。

Filename: Listing_3_16.py
1    import cv2
2    import numpy as np
3
4    # Load an image
5    image = cv2.imread("images/scanned_doc.png")
6    # convert the image to grayscale
7    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
8    cv2.imshow("Original Grayscale Receipt", image)
9
10   # Binarize the image using thresholding
11   (T, binarizedImage) = cv2.threshold(image, 60, 255, cv2.THRESH_BINARY)
12   cv2.imshow("Binarized Receipt", binarizedImage)
13
14   # Binarization with inverse thresholding
15   (Ti, inverseBinarizedImage) = cv2.threshold(image, 60, 255, cv2.THRESH_BINARY_INV)
16   cv2.imshow("Inverse Binarized Receipt", inverseBinarizedImage)
17   cv2.waitKey(0)

Listing 3-16Binarization Using Simple Thresholding

清单 3-16 显示了两种二值化方法:简单二值化和逆二值化。第 5 行加载图像,第 8 行将图像转换为灰度图像,因为阈值函数的输入应该是灰度图像。

第 11 行调用 OpenCV 的cv2.threshold()函数,并将灰度图像、阈值、最大像素值和阈值方法cv2.THRESH_BINARY.作为参数传递。threshold()函数返回一个元组,其中包含我们在参数和二值化图像中提供的相同阈值。在前面的示例中,对于值大于 60 的所有像素,像素值将被设置为最大值 255,对于值等于或小于 60 的像素,像素值将被设置为 0。

第 15 行类似于第 11 行,除了threshold()函数的最后一个参数是cv2.THRESH_BINARY_INV.通过传递cv2.THRESH_BINARY_INV,我们指示threshold()方法做与cv2.THRESH_BINARY方法相反的事情:如果像素强度小于 60,将像素值设置为 255;否则,将其设置为 0。

图 3-50 至 3-52 显示了两种阈值方法的样本输出以及原始图像。

img/493065_1_En_3_Fig52_HTML.jpg

图 3-52

简单逆阈值二值化图像

img/493065_1_En_3_Fig51_HTML.jpg

图 3-51

简单阈值二值化图像

img/493065_1_En_3_Fig50_HTML.jpg

图 3-50

带有深色背景补丁/污点的原始灰度图像

为了演示这个例子,我们拍摄了一张污迹斑斑的文档的扫描图像(图 3-50 ,并使用简单的阈值处理将其二值化。方法cv2.THRESH_BINARY生成了输出,其中包含白色背景上的黑色文本。方法cv2.THRESH_BINARY_INV用黑色背景上的白色文本创建了图像。

在简单的阈值处理中,一个全局阈值应用于图像中的所有像素,您需要预先知道阈值。如果您正在处理大量图像,并且希望根据图像类型和强度变化来调整阈值,简单阈值可能不是理想的方法。

在下面的部分中,我们将检查其他阈值方法:自适应阈值和 Otsu 方法。

自适应阈值

自适应阈值处理用于二值化具有不同程度的像素强度的灰度图像,并且一个单一的阈值可能不适于从图像中提取信息。在自适应阈值处理中,该算法基于像素周围的小区域来确定像素的阈值。这将为同一幅图像中的不同区域提供不同的阈值。当像素强度在图像内变化时,自适应阈值处理与简单的阈值处理相比往往给出更好的结果。

清单 3-17 显示了使用自适应阈值将灰度图像二值化。

Filename: Listing_3_17.py
1    import cv2
2    import numpy as np
3
4    # Load an image
5    image = cv2.imread("images/boat.jpg")
6    # convert the image to grayscale
7    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
8
9    cv2.imshow("Original Grayscale Image", image)
10
11   # Binarization using adaptive thresholding and simple mean
12   binarized = cv2.adaptiveThreshold(image, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 7, 3)
13   cv2.imshow("Binarized Image with Simple Mean", binarized)
14
15   # Binarization using adaptive thresholding and Gaussian Mean
16   binarized = cv2.adaptiveThreshold(image, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 3)
17   cv2.imshow("Binarized Image with Gaussian Mean", binarized)
18
19   cv2.waitKey(0)

Listing 3-17Binarization Using Adaptive Thresholding

我们使用了一个具有不同程度阴影和颜色强度的示例图像。使用自适应阈值,我们希望将图像转换为二值图像。下面是对清单 3-17 中发生的事情的解释。

像往常一样,第 5 行加载图像。第 7 行将图像转换为灰度图像,因为阈值函数的输入是灰度图像。

第 12 行实际上是使用 OpenCV 的cv2.adaptiveThreshold()函数执行二值化。该函数采用以下参数:

  • 需要二值化的灰度图像

  • 最大值

  • 计算阈值的方法(稍后将提供更多信息)

  • 二值化方法,如cv2.THRESH_BINARYcv2.THRESH_BINARY_INV

  • 计算阈值时要考虑的邻域大小

  • 将从计算的阈值中减去的常数值 C

在我们的例子中,在第 12 行,我们使用了cv2.ADAPTIVE_THRESH_MEAN_C来表示我们想要通过取周围像素的平均值来计算像素的阈值。在我们的例子中,邻域的大小是 7×7。第 12 行的最后一个参数 3 是将从计算的阈值中减去的常数。

第 16 行类似于第 12 行,除了我们使用cv2.ADAPTIVE_GAUSSIAN_C来表示我们想要通过取一个像素周围所有像素的加权平均值来计算该像素的阈值。

图 3-53 至 3-55 显示了清单 3-17 的部分输出样本。

img/493065_1_En_3_Fig55_HTML.jpg

图 3-55

高斯均值自适应阈值二值化图像

img/493065_1_En_3_Fig54_HTML.jpg

图 3-54

简单均值自适应阈值二值化图像

img/493065_1_En_3_Fig53_HTML.jpg

图 3-53

原象

大津二值化

在简单的阈值处理中,我们选择一个任意选择的全局阈值。很难知道阈值的正确值是多少,所以我们可能需要做几次试错实验,才能得到正确的值。即使您获得了一种情况下的理想值,它也可能不适用于具有不同像素强度特征的其他图像。

Otsu 的方法从图像直方图中确定最佳全局阈值。我们将在下一章学习更多关于直方图的知识。现在,就把直方图想象成像素值的频率分布。

为了执行 Otsu 的二进制化,我们在cv2.threshold()函数中传递cv2.THRESH_OTSU作为额外的标志。例如,我们在threshold()函数中传递cv2.THRESH_BINARY+cv2.THRESH_OTSU来指示使用 Otsu 的方法。threshold()方法需要一个阈值。当使用 Otsu 的方法时,我们传递一个任意值(可能是 0),算法自动计算阈值并作为输出之一返回。

清单 3-18 显示了如何使用 Otsu 的二进制化方法的代码示例。

Filename: Listing_3_18.py
1    import cv2
2    import numpy as np
3
4    # Load an image
5    image = cv2.imread("images/scanned_doc.png")
6    # convert the image to grayscale
7    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
8    cv2.imshow("Original Grayscale Receipt", image)
9
10   # Binarize the image using thresholding
11   (T, binarizedImage) = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)
12   print("Threshold value with Otsu binarization", T)
13   cv2.imshow("Binarized Receipt", binarizedImage)
14
15   # Binarization with inverse thresholding
16   (Ti, inverseBinarizedImage) = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
17   cv2.imshow("Inverse Binarized Receipt", inverseBinarizedImage)
18   print("Threshold value with Otsu inverse binazarion", Ti)
19   cv2.waitKey(0)

Listing 3-18Otsu’s Binarization

你会注意到清单 3-18 中的代码示例与清单 3-16 中的代码几乎相同,除了以下例外:

  • 第 11 行使用一个附加标志cv2.THRESH_OTSUcv2.THRESH_BINARY,阈值作为 0 传递。

  • 第 16 行使用标志cv2.THRESH_OTSUcv2.THRESH_BINARY_INV,并且阈值再次被设置为 0。

  • 我们在第 12 行和第 18 行使用了print语句来打印计算出的阈值。图 3-56 显示了这些print语句的样本输出。

图 3-57 到 3-59 显示了 Otsu 的输出样本。

img/493065_1_En_3_Fig59_HTML.jpg

图 3-59

用 Otsu 法进行反二值化

img/493065_1_En_3_Fig58_HTML.jpg

图 3-58

用 Otsu 法进行二值化

img/493065_1_En_3_Fig57_HTML.jpg

图 3-57

具有不同背景阴影(污点和暗斑)的原始图像

img/493065_1_En_3_Fig56_HTML.jpg

图 3-56

根据 Otsu 方法计算的阈值输出示例

二值化是从图像中提取显著特征的一种有用的图像处理技术。在这一节中,我们已经学习了不同的二值化技术,以及它们基于像素强度及其变化的用法。在接下来的部分,我们将学习另一种强大的图像处理技术,称为边缘检测

梯度和边缘检测

边缘检测涉及一组方法来寻找图像中像素亮度明显变化的点。

我们将学习两种在图像中寻找边缘的方法:寻找梯度和 Canny 边缘检测。

OpenCV 提供了以下两种寻找渐变的方法。

索贝尔衍生物(cv2。Sobel()函数)

Sobel 方法是高斯平滑和 Sobel 微分的组合,其计算图像强度函数的梯度的近似值。由于高斯平滑,这种方法是抗噪声的。

通过分别传递参数xorderyorder,我们可以在水平或垂直方向上进行求导。Sobel()函数还有一个参数ksize,我们用它来定义内核的大小。如果我们将ksize设置为-1,OpenCV 将在内部应用一个 3×3 的 Schar 过滤器,与 3×3 的 Sobel 过滤器相比,它通常会给出更好的结果。

我们将在清单 3-19 中看到 Sobel 函数的运行。

Filename: Listing_3_19.py
1    import cv2
2    import numpy as np
3    # Load an image
4    image = cv2.imread("images/sudoku.jpg")
5    cv2.imshow("Original Image", image)
6    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
7    image = cv2.bilateralFilter(image, 5, 50, 50)
8    cv2.imshow("Blurred image", image)
9
10   # Sobel gradient detection
11   sobelx = cv2.Sobel(image,cv2.CV_64F,1,0,ksize=3)
12   sobelx = np.uint8(np.absolute(sobelx))
13   sobely = cv2.Sobel(image,cv2.CV_64F,0,1,ksize=3)
14   sobely = np.uint8(np.absolute(sobely))
15
16   cv2.imshow("Sobel X", sobelx)
17   cv2.imshow("Sobel Y", sobely)
18
19   # Schar gradient detection by passing ksize = -1 to Sobel function
20   scharx = cv2.Sobel(image,cv2.CV_64F,1,0,ksize=-1)
21   scharx = np.uint8(np.absolute(scharx))
22   schary = cv2.Sobel(image,cv2.CV_64F,0,1,ksize=-1)
23   schary = np.uint8(np.absolute(schary))
24   cv2.imshow("Schar X", scharx)
25   cv2.imshow("Schar Y", schary)
26
27   cv2.waitKey(0)

Listing 3-19Sobel and Schar Gradient Detection

这里发生了很多事情。因此,让我们通过仔细阅读代码清单来理解渐变的概念。

第 4 行只是从磁盘加载一个图像。我们应用双边滤波器来减少第 7 行中的噪声。图 3-60 显示原始输入图像,图 3-61 显示用作 Sobel 和 Schar 梯度检测功能输入的模糊图像。

梯度检测从第 11 行开始。我们使用了接受以下参数的cv2.Sobel()函数:

  • 我们想要检测渐变的模糊图像。

  • 一种数据类型,cv2.CV_64F,它是一种 64 位浮点数。为什么?从黑到白的过渡被认为是正斜率,而从白到黑的过渡是负斜率。8 位无符号整数不能保存负数。因此,我们需要使用 64 位浮点数;否则,当从白色到黑色的过渡发生时,我们将丢失渐变。

  • 第三个参数表示我们是否要计算 X 方向的梯度。值 1 意味着我们要计算 X 方向的梯度。

  • 类似地,第四个参数指示是否计算 Y 方向的梯度。1 表示是,0 表示不是。

  • 第五个参数,ksize,定义了内核大小。ksize=5表示内核大小为 5×5。

由于我们想要确定第 11 行 X 方向的梯度,我们将函数cv2.Sobel()中的第三个参数设置为 1,并将第四个参数设置为 0。

第 12 行简单地获取梯度的绝对值,并将其转换回 8 位无符号整数。请记住,图像被表示为一个 8 位无符号整数 NumPy 数组。

第 13 行类似于第 11 行,只是第三个参数设置为 0,第四个参数设置为 1,以指示 Y 方向的梯度计算。

如前所述,第 14 行将 64 位浮点数转换为 8 位无符号整数。

图 3-62 和图 3-63 显示了 16 和 17 线的样本输出。您会注意到,X 和 Y 方向上的边缘检测都不是很清晰。让我们尝试一个简单的改进,看看对边缘清晰度的影响。

img/493065_1_En_3_Fig63_HTML.jpg

图 3-63

Y 方向上的 Sobel 边缘检测

img/493065_1_En_3_Fig62_HTML.jpg

图 3-62

X 方向上的 Sobel 边缘检测

img/493065_1_En_3_Fig61_HTML.jpg

图 3-61

模糊图象

img/493065_1_En_3_Fig60_HTML.jpg

图 3-60

原象

第 20 至 23 行与清单 3-19 的第 11 至 14 行相似。不同的是ksize的值是-1,它指示 OpenCV 内部调用内核大小为 3×3 的 Schar 函数。你会注意到,与 Sobel 函数相比,边缘的清晰度要好得多。图 3-64 和图 3-65 是图 3-61 所示图像的沙尔滤波结果。

img/493065_1_En_3_Fig65_HTML.jpg

图 3-65

X 方向上的沙尔边缘检测

img/493065_1_En_3_Fig64_HTML.jpg

图 3-64

X 方向上的沙尔边缘检测

Sobel 和 Schar 计算沿 X 和 Y 方向的梯度幅度,允许我们确定沿水平和垂直方向的边缘。

拉普拉斯导数(cv2。拉普拉斯()函数)

拉普拉斯算子计算像素强度函数的二阶导数,以确定图像中的边缘。拉普拉斯算子基于以下等式计算梯度:

$$ \mathrm{Laplace}\left(\mathrm{f}\right)=\frac{\partial²\mathrm{f}}{\partial {x}²}+\frac{\partial²\mathrm{f}}{\partial {y}²} $$

OpenCV 提供了一个函数cv2.Laplacian(),用于计算边缘检测的梯度。该函数采用以下参数:

  • 需要检测边缘的图像

  • 数据类型,通常是用来保存浮点值的cv2.CV_64F

清单 3-20 显示了使用 OpenCV 的拉普拉斯函数进行边缘检测的工作示例。

像往常一样,第 5 行加载图像,第 6 行将图像转换为灰度,第 8 行使用双边过滤模糊图像。

第 12 行是调用cv2.Laplacian()函数进行梯度计算以检测图像边缘的地方。同样,当从白色到黑色的过渡发生时,我们传递了CV_64F数据类型来保存梯度的可能负值。

第 13 行将 64 位浮点数转换为 8 位无符号整数。

Filename: Listing_3_20.py
1    import cv2
2    import numpy as np
3
4    # Load an image
5    image = cv2.imread("images/sudoku.jpg")
6    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
7
8    image = cv2.bilateralFilter(image, 5, 50, 50)
9    cv2.imshow("Blurred image", image)
10
11   # Laplace function for edge detection
12   laplace = cv2.Laplacian(image,cv2.CV_64F)
13   laplace = np.uint8(np.absolute(laplace))
14
15   cv2.imshow("Laplacian Edges", laplace)
16
17   cv2.waitKey(0)

Listing 3-20Edge Detection Using Laplacian Derivatives

图 3-66 显示了Laplacian()功能的示例显示。

img/493065_1_En_3_Fig66_HTML.jpg

图 3-66

使用拉普拉斯导数的边缘检测

Canny 边缘检测

Canny 边缘检测是图像处理中最流行的边缘检测方法之一。这是一个多步骤的过程。它首先模糊图像以减少噪声,然后计算 X 和 Y 方向上的 Sobel 梯度,抑制计算非最大值的边缘,最后通过应用滞后阈值来确定像素是否是“边缘样的”。

OpenCV 的cv2.canny()函数将所有这些步骤封装成一个函数。让我们直接看代码,看一个使用 Canny 函数进行边缘检测的例子。参见清单 3-21 。

Filename: Listing_3_21.py
1    import cv2
2    import numpy as np
3
4    # Load an image
5    image = cv2.imread("images/sudoku.jpg")
6    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
7    cv2.imshow("Blurred image", image)
8
9    # Canny function for edge detection
10   canny = cv2.Canny(image, 50, 170)
11   cv2.imshow("Canny Edges", canny)
12
13   cv2.waitKey(0)

Listing 3-21Canny Edge Detection

清单 3-21 中重要的一行是第 10 行,我们在这里调用cv2.Canny()函数,并将最小和最大阈值传递给需要检测边缘的图像。任何大于最大阈值的梯度值都被视为边缘。低于最小阈值的任何值都不被视为边缘。根据边缘的强度变化,考虑边缘之间的梯度值。

图 3-67 显示了 Canny 边缘检测器的样本输出。请注意,在这种情况下,边缘非常清晰。

img/493065_1_En_3_Fig67_HTML.jpg

图 3-67

Canny 边缘检测

轮廓

等高线是连接相同强度的连续点的曲线。确定轮廓对于物体识别、人脸检测和识别是有用的。

为了检测轮廓,我们执行以下操作:

  1. 将图像转换为灰度。

  2. 使用任何阈值方法将图像二值化。

  3. 应用 Canny 边缘检测方法。

  4. 使用findContours()方法找到图像中的所有轮廓。

  5. 最后,如果需要,使用drawContours()功能绘制轮廓。

我们将在清单 3-22 中看到轮廓检测和绘图。

Filename: Listing_3_22.py
1    import cv2
2    import numpy as np
3
4    # Load an image
5    image = cv2.imread("images/sudoku.jpg")
6    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
7    cv2.imshow("Blurred image", image)
8
9    # Binarize the image
10   (T,binarized) = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
11   cv2.imshow("Binarized image", binarized)
12
13   # Canny function for edge detection
14   canny = cv2.Canny(binarized, 0, 255)
15   cv2.imshow("Canny Edges", canny)
16
17   (contours, hierarchy) = cv2.findContours(canny,cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
18   print("Number of contours determined are ", format(len(contours)))
19
20   copiedImage = image.copy()
21   cv2.drawContours(copiedImage, contours, -1, (0,255,0), 2)
22   cv2.imshow("Contours", copiedImage)
23   cv2.waitKey(0)

Listing 3-22Contour Detection and Drawing

下面是清单 3-22 的逐行解释。

第 5 行加载图像。第 6 行将图像转换为灰度,第 10 行使用 Otsu 的方法将图像二值化。第 14 行使用 Canny 函数计算边缘检测的梯度。

第 17 行调用 OpenCV 的cv2.findContours()函数来确定轮廓。该函数的参数如下:

  • 第一个参数是我们想要使用 Canny 函数检测边缘的图像。

  • 第二个参数cv2.RET_EXTERNAL,决定了我们感兴趣的轮廓类型。cv2.RET_EXTERNAL仅检索最外面的轮廓。我们也可以使用cv2.RET_LIST来检索所有轮廓,cv2.RET_COMP acv2.RET_TREE,以包括层次轮廓。

  • 第三个参数cv2.CHAIN_APPROAX_SIMPLE,去掉多余的点,压缩轮廓,从而节省内存。cv2.CHAIN_APPROAX_NONE存储轮廓的所有点(需要更多内存来存储)。

cv2.findContours()函数的输出是一个元组,其中包含以下项目:

  • 元组的第一项是图像中所有轮廓的 Python 列表。每个单独的轮廓是对象边界点的(x,y)坐标的 NumPy 数组。

  • 输出元组的第二项是轮廓层次。

注意第 18 行,这里我们打印了识别出的轮廓数。

绘制等高线

我们正在使用cv2.drawContours()功能绘制等高线(清单 3-22 的第 21 行)。以下是该函数的参数:

  • 第一个参数是要在其中绘制轮廓的图像。

  • 第二个参数是所有轮廓点的列表。

  • 第三个参数是要绘制的轮廓的索引。如果我们想画第一个轮廓,传递一个 0。同样,传递 1 来绘制第二个轮廓,以此类推。如果要绘制所有轮廓,请将-1 传递给该参数。

  • 第四个参数是轮廓的颜色。

  • 第五个也是最后一个参数是轮廓的厚度。

图 3-68 至 3-70 显示了清单 3-22 的部分输出样本。

img/493065_1_En_3_Fig70_HTML.jpg

图 3-70

在原始图像上绘制的轮廓

img/493065_1_En_3_Fig69_HTML.jpg

图 3-69

使用 Canny 函数的轮廓

img/493065_1_En_3_Fig68_HTML.jpg

图 3-68

模糊图象

摘要

在这一章中,我们探讨了对构建计算机视觉应用有用的各种图像处理技术。我们学习了各种图像变换的方法,如调整大小、旋转、翻转和裁剪。我们还学习了如何对图像进行算术和位运算。本章的后半部分介绍了一些强大而有用的图像处理功能,如遮罩、降噪、二值化、边缘和轮廓检测。

我们将在后面的章节中使用大多数图像处理技术,特别是当我们学习机器学习的特征提取和工程时。****

四、构建基于机器学习的计算机视觉系统

在前一章中,您学习了各种图像处理技术。在本章中,我们将讨论开发机器学习计算机视觉系统的步骤。本章是下一章的入门,下一章将详细介绍各种深度学习算法,以及如何用 Python 编写代码在 TensorFlow 上执行。

图像处理流水线

计算机视觉 (CV)是计算机捕捉和分析图像并对其做出解释和决策的能力。例如,CV 可用于检测和识别图像,并识别其中的模式或对象。一个人工智能 (AI)系统摄取图像,处理它们,提取特征,并对它们进行解释。换句话说,图像从一个系统或组件移动到另一个系统或组件,并转换成各种形式,以便机器识别模式并检测其中的对象。

图像通过一组组件进行处理,这些组件执行各种类型的变换,从而产生最终产品。这个过程被称为图像处理流水线计算机视觉流水线。图 4-1 显示了处理流水线的高级视图。

img/493065_1_En_4_Fig1_HTML.png

图 4-1

图像流水线

如图 4-1 所示,真实世界的物体被感应设备,如照相机,捕捉,并转换成数字图像。这些数字图像由计算机系统处理,并生成最终输出。输出可以是关于图像本身(图像分类)或嵌入在图像中的一些模式和对象的检测。例如,在医疗保健行业,图像可能是由 MRI 或 X 射线仪器创建的。该图像可以被输入到图像处理流水线中,以检测肿瘤的存在或不存在。

这本书涵盖了什么进入计算机处理单元和输出是如何产生的。让我们来看看计算机系统中处理图像的数据流流水线(见图 4-2 )。

img/493065_1_En_4_Fig2_HTML.jpg

图 4-2

计算机视觉中的图像处理流水线

以下是对这一计算机视觉流水线的简要描述:

  1. 视觉流水线从图像摄取开始。图像被捕获、数字化并存储在计算机磁盘上。在视频的情况下,图像的数字帧被摄取并存储在磁盘上,从那里它们被读取和分析。在某些情况下,视频帧从摄像机实时摄取到计算机中。

  2. 摄取图像后,它们会经历不同的转换阶段。该转换也称为预处理,是标准化图像所必需的。确保用于特定目的的所有图像具有相同的大小、形状和颜色模式是很重要的。常用的变换有调整图像大小、颜色处理、平移、旋转和裁剪。其他有助于特征提取的高级变换包括图像二值化、阈值处理、梯度和边缘检测。关于这些技术的回顾,请参见第三章。

  3. 特征提取是视觉流水线的核心组件。在机器学习中,我们输入一组特征来预测结果或类别。没有一个好的特征集,我们就不可能有一个好的机器学习结果。您将在下一节“特征提取”中了解更多关于特征提取的信息,但现在让我们记住,一个好的特征集对于任何机器学习系统都是重要的。

  4. 然后是机器学习算法。机器学习有两个阶段。在第一阶段,我们将大量数据集输入数学算法进行学习。这种学习算法的结果被称为训练模型或简称为模型。在第二阶段,我们向训练好的模型提供数据集,以预测结果或类别。这个阶段被称为预测阶段。我将在第五章描述一些最流行和高效的计算机视觉机器学习模型。我将在那一章中介绍 Keras 和 TensorFlow,我们将通过一些代码示例来训练模型并使用这些模型进行预测。

  5. 视觉流水线的最后一个组成部分是输出,这是您希望视觉系统实现的最终目标。

特征抽出

在机器学习中,特征是被观察的对象或事件的单个可测量属性。在计算机视觉中,特征是关于图像的区别信息。特征提取是机器学习中的一个重要步骤。事实上,机器学习的一切都围绕着特征。因此,对于高质量的机器学习结果来说,识别和提取有区别的独立特征是至关重要的。

给定一幅车轮图像,考虑尝试确定该图像是摩托车还是汽车。在这种情况下,轮子不是显著特征。我们需要更多的特征,比如门、屋顶等。此外,从单个摩托车或汽车提取的特征不足以用于实际的机器学习用途。我们需要借助重复出现的事件或特征来建立模式,因为在现实世界中,一个对象可能不会以呈现特征的方式呈现。因此,可重复性是一个好特性的重要特征。

在车轮的例子中,我们只有一个特征,但是在实际操作中,可能有大量的特征,例如颜色、轮廓、边缘、拐角、角度、光强度等等。你提取的特征越多,你的模型就越好。

机器学习模型和为训练模型提供的特征一样好。问题是,如何提取一组好的特征?没有适合所有情况的解决方案,但这里有一些实用的方法,可以帮助您完成特征提取任务。以下是一些方法的非穷尽列表:

  • 特征必须是可区分的或可识别的。

  • 特征必须避免混淆重叠的特征。

  • 特征必须避免很少出现的特征。

  • 不同条件和视角下的特征应该是一致的。

  • 特征应该是可以直接识别的,也可以通过一些处理技术来识别。

  • 你应该收集大量的样本来建立模式。

如何表示特征

从图像中提取的特征被表示为一个向量,称为特征向量。我们用一个例子来理解这个。为了简单起见,让我们考虑一个灰度图像。该图像的特征是像素值。我们知道,灰度图像中的像素被组织为二维矩阵,每个像素的值在 0 到 255 之间。如果这些像素值是我们的特征,我们将这些值表示为一维(1D)行矩阵(这是一个向量或 1D 数组)。图 4-3 显示了这种情况的图示。

img/493065_1_En_4_Fig3_HTML.png

图 4-3

特征的矢量表示

对于大多数机器学习算法,我们需要提取特征,并将其提供给模型训练所考虑的算法。一些深度学习算法,如卷积神经网络(CNN),会自动提取特征,然后训练模型。第五章提供了关于深度学习算法以及如何训练计算机视觉模型的细节。以下部分讨论了从图像中提取特征的各种方法。我们将使用 Python 和 OpenCV 编写代码来完成特征提取的例子。

颜色直方图

直方图是图像中像素强度的分布。通常直方图以图形(或图表)的形式显现。该图的 x 轴代表像素值(或值的范围),而 y 轴代表特定值或值的范围的像素的频率(或计数)。图形的峰值显示像素数量最多的颜色。

我们已经知道一个像素的值可以在 0 到 255 之间。这意味着直方图在 x 轴上将有 256 个值,而 y 轴将有这些值的像素数。在 x 轴上有很多数字。出于最实际的目的,我们将这些像素值分成“箱”例如,我们可以将x-值分成 8 个面元,其中每个面元将具有 32 像素的颜色。我们对每个面元内的像素数量求和,以计算出 y 值。

那么,我们为什么关心直方图呢?直方图给出了图像中颜色、对比度和亮度分布的概念。灰度图像只有一个颜色通道,但 RGB 方案中的彩色图像有三个通道。当我们绘制彩色图像的直方图时,我们通常绘制三个直方图,每个通道一个,以更好地了解每个颜色通道的强度分布。直方图可以用作机器学习算法的特征。直方图还有一个有趣的用途,那就是增强图像的质量。使用直方图增强图像的技术称为直方图均衡化。在本章的后面你会学到更多关于直方图均衡化的知识。

如何计算直方图

我们将使用 Python 和 OpenCV 来计算直方图,并且我们将使用 Matplotlib 包中的pyplot来绘制直方图。(还记得 Matplotlib 吗?我们在第一章中安装并设置了它。)

OpenCV 提供了一个易于使用的函数来计算直方图。下面是对calcHist()功能的描述:

calcHist(images, channels, mask, histSize, ranges, accumulate)

该函数采用以下参数:

  • 这是图像像素的 NumPy 数组。如果只有一个图像,只需将 NumPy 变量放在一对方括号中,例如[image]

  • channels:这是我们要计算直方图的频道索引数组。对于灰度图像,该值为[0],对于 RGB 彩色图像,该值为[0,1,2]。

  • 这是一个可选参数。如果不提供蒙版,将为图像中的所有像素计算直方图。如果您提供一个遮罩,将只为被遮罩的像素计算直方图。还记得第三章里的面具吗?

  • 这是箱子的数量。如果我们将该值作为[64,64,64]传递,这意味着每个通道将有 64 个面元。对于不同的频道,仓的大小可以不同。

  • ranges:这是像素值的范围,对于灰度和 RGB 彩色图像,通常为[0,255]。这个值在其他配色方案中可能会有所不同,但是现在,让我们只坚持使用 RGB。

  • accumulate:这是累计标志。如果设置了该值,直方图在分配时不会在开始时被清除。此功能使您能够从几组数组计算单个直方图,或者及时更新直方图。默认值为None

灰度直方图

让我们写一些代码来学习如何计算灰度图像的直方图,并将其可视化为图形(参见清单 4-1 )。注意,我们从 Matplotlib 包中导入了pyplot。这是我们将用来绘制显示直方图的图形的库。

Filename: Listing_4_1.py
1    import cv2
2    import numpy as np
3    from matplotlib import pyplot as plot
4
5    # Read an image and convert it to grayscale
6    image = cv2.imread("images/nature.jpg")
7    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
8    cv2.imshow("Original Image", image)
9
10   # calculate histogram
11   hist = cv2.calcHist([image], [0], None, [256], [0,255])
12
13   # Plot histogram graph
14   plot.figure()
15   plot.title("Grayscale Histogram")
16   plot.xlabel("Bins")
17   plot.ylabel("Number of Pixels")
18   plot.plot(hist)
19   plot.show()
20   cv2.waitKey(0)

Listing 4-1Histogram of a Grayscale Image

清单 4-1 的第 11 行计算我们的灰度图像的直方图。注意,image 变量被包在一对大括号中,因为cv2.calcHist()函数接受 NumPy 数组的数组。即使我们只有一个图像,我们仍然需要将它包装在一个数组中。

第二个参数[0]表示我们想要计算第零个颜色通道的直方图。因为我们只有一个通道,所以我们在数组中只传递一个索引值:[0]。

第三个参数None,意味着我们不想提供任何屏蔽。换句话说,我们计算所有像素的直方图。

[256]是 bin 信息。这表明我们需要 256 个面元,意味着每个像素一个面元。除非我们想要对图像像素分布进行细粒度的分析,否则这可能没有用。出于大多数实际目的,您希望通过较小的纸盒尺寸,如[32]或[64]等。

最后一个参数[0,255]告诉函数存在 0 到 255 之间的像素值。

hist变量保存计算输出。如果你打印这个变量,你会看到一堆数字,可能不容易解释。为了便于解释,我们以图表的形式绘制直方图。

第 14 行配置空白图。第 15 行为我们的图指定了一个名称。第 16 和 17 行分别设置了 x 轴和 y 轴标签。第 18 行实际绘制了图表。最后,第 19 行在屏幕上显示了漂亮的情节。图 4-4 显示原始图像,图 4-5 显示输出。

img/493065_1_En_4_Fig5_HTML.jpg

图 4-5

图 4-4 中灰度图像的直方图

img/493065_1_En_4_Fig4_HTML.jpg

图 4-4

原始灰度图像

那么,你在这个直方图中看到了什么?最大像素数(3,450)的颜色值为 20,接近黑色。大多数像素在 100 和 150 的颜色范围内。

这里有一个练习:用 32 个面元绘制一幅图像的直方图。尝试解释输出图。

RGB 颜色直方图

让我们回顾一下清单 4-2 中的程序,并理解如何绘制基于 RGB 的彩色图像的所有三个通道的直方图。在 RGB 方案中,彩色图像有三个通道。值得注意的是,OpenCV 在 BGR 序列中保持颜色信息,而不是在 RGB 序列中。

在清单 4-2 中,第 6 行是我们通常的图像读取行,在这里我们从磁盘读取彩色图像。

你会注意到我们在 BGR 序列中创建了一个颜色元组来保存我们所有的通道颜色(第 10 行)。

为什么我们在第 12 行有一个for循环?cv2.calcHist()函数的第二个参数接受值为 0、1 或 2 的数组。如果我们传递值[0],我们实际上是指示calcHist()函数计算第 0 个索引中颜色通道的直方图,也就是蓝色通道。类似地,值[1]指示calcHist()函数计算红色通道的直方图,值[2]表示计算绿色通道的直方图。for循环的第一次迭代首先是计算和绘制蓝色的直方图,第二次迭代是绿色的,最后一次迭代是绿色通道的。

请再次注意,我们已经将[32]作为第四个参数传递给了我们的calcHist()函数。这是为了让该函数知道,我们想要为每个通道计算 32 个仓的直方图。

最后一个参数[0,256]给出了颜色范围。

在第 15 行的for循环中,plot()函数将直方图作为第一个参数,将可选颜色作为第二个参数。

Filename: Listing_4_2.py
1    import cv2
2    import numpy as np
3    from matplotlib import pyplot as plot
4
5    # Read a color image
6    image = cv2.imread("images/nature.jpg")
7
8    cv2.imshow("Original Color Image", image)
9    #Remember OpenCV stores color in BGR sequence instead of RBG.
10   colors = ("blue", "green", "red")
11   # calculate histogram
12   for i, color in enumerate(colors):
13      hist = cv2.calcHist([image], [i], None, [32], [0,256])
14      # Plot histogram graph
15      plot.plot(hist, color=color)
16
17   plot.title("RGB Color Histogram")
18   plot.xlabel("Bins")
19   plot.ylabel("Number of Pixels")
20   plot.show()
21   cv2.waitKey(0)

Listing 4-2Histogram of Three Channels of RGB Color Image

图 4-6 和图 4-7 显示了列表 4-2 的输出。

img/493065_1_En_4_Fig7_HTML.jpg

图 4-7

图 4-6 中图像的三个颜色通道的直方图

img/493065_1_En_4_Fig6_HTML.jpg

图 4-6

原始彩色图像

在图 4-7 中,x-轴最多只有 32 个值,因为我们只为每个通道使用了 32 个箱。

这里有一个练习:创建一个蒙版图像的直方图。

提示创建一个掩码 NumPy 数组,并将该数组作为第三个参数传递给cv2.calcHist()函数。阅读第三章来刷新你关于如何创建一个面具的记忆。

直方图均衡器

现在我们对直方图有了很好的理解,让我们用这个概念来增强图像的质量。直方图均衡化是一种调整图像对比度的图像处理技术。这是一种重新分配像素强度的方法,使填充不足的像素强度等于填充过度的像素强度,如图 4-8 所示。

img/493065_1_En_4_Fig8_HTML.png

图 4-8

直方图均衡化(来源:维基百科)

让我们写一些代码,看看这个直方图均衡化的作用。清单 4-3 中有很多代码,但是如果你看看这个清单的顶部,从第 1 行到第 19 行,你会注意到这些行与清单 4-1 中的那些是一样的。这里我们只是计算和绘制灰度图像的直方图。

在第 21 行,我们使用 OpenCV 的cv2.equalizeHist()函数,该函数获取原始图像并调整其像素强度以增强其对比度。

第 22 到 33 行计算并显示增强(均衡)图像的直方图。

图 4-9 至 4-12 显示了列表 4-3 的输出以及原始图像和均衡图像的直方图对比。

img/493065_1_En_4_Fig12_HTML.jpg

图 4-12

图 4-11 均衡图像直方图

img/493065_1_En_4_Fig11_HTML.jpg

图 4-11

对比度增强的均衡图像

img/493065_1_En_4_Fig10_HTML.jpg

图 4-10

图 4-9 中图像的直方图

img/493065_1_En_4_Fig9_HTML.jpg

图 4-9

原始灰度图像

Filename: Listing_4_3.py
1    import cv2
2    import numpy as np
3    from matplotlib import pyplot as plot
4
5    # Read an image and convert it into grayscale
6    image = cv2.imread("images/nature.jpg")
7    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
8    cv2.imshow("Original Image", image)
9
10   # calculate histogram of the original image
11   hist = cv2.calcHist([image], [0], None, [256], [0,255])
12
13   # Plot histogram graph
14   #plot.figure()
15   plot.title("Grayscale Histogram of Original Image")
16   plot.xlabel("Bins")
17   plot.ylabel("Number of Pixels")
18   plot.plot(hist)
19   plot.show()
20
21   equalizedImage = cv2.equalizeHist(image)
22   cv2.imshow("Equalized Image", equalizedImage)
23
24   # calculate histogram of the original image
25   histEqualized = cv2.calcHist([equalizedImage], [0], None, [256], [0,255])
26
27   # Plot histogram graph
28   #plot.figure()
29   plot.title("Grayscale Histogram of Equalized Image")
30   plot.xlabel("Bins")
31   plot.ylabel("Number of Pixels")
32   plot.plot(histEqualized)
33   plot.show()
34   cv2.waitKey(0)

Listing 4-3Histogram Equalization

[军]GroundLaunchedCruiseMissile

灰度共生矩阵(GLCM)是给定偏移量内同时出现的像素值的分布。偏移量是相邻像素的位置(距离和方向)。顾名思义,GLCM 总是针对灰度图像进行计算。

GLCM 计算像素值 i 与像素值j水平、垂直或对角共存的次数

对于 GLCM 计算,我们指定偏移距离 d 和角度θ(theta)。角度θ(theta)可以是 0(水平)、90(垂直)、45(右上斜)或 135(左上斜),如图 4-13 所示。

img/493065_1_En_4_Fig13_HTML.png

图 4-13

相邻像素位置的图示(距离和角度)

GLCM 的重要性在于它提供了图像上空间关系的信息。这与直方图不同,因为直方图不提供关于图像大小、像素位置或它们之间关系的任何信息。

虽然 GLCM 是如此重要的矩阵,但我们并不直接将其用作机器学习的特征向量。我们使用 GLCM 计算关于图像的某些关键统计,并且这些统计被用作任何机器学习训练的特征。在本节中,我们将了解这些统计数据以及如何计算它们。

尽管 OpenCV 在内部使用 GLCM,但它并不直接公开任何函数来计算它。为了计算 GLCM,我们将使用另一个 Python 库:skimage 的feature包。

下面是我们将用来计算 GLCM 的函数的描述:

greycomatrix(image, distances, angles, levels, symmetric,normed)

greycomatrix()函数采用以下参数:

  • 这是灰度图像的 NumPy 表示。记住,图像必须是灰度的。

  • distances:这是像素对距离偏移的列表。

  • angles:这是一对像素之间的角度列表。确保角度是弧度而不是度。

  • levels:这是一个可选参数,用于具有 16 位像素值的图像。在大多数情况下,我们使用 8 位图像像素,其值范围从 0 到 255。对于 8 位图像,该参数的最大值是 256。

  • symmetric:可选参数,取布尔值。值True意味着输出矩阵将是对称的。默认是False

  • normed:这也是一个可选参数,采用布尔值。布尔型True表示每个输出矩阵通过除以给定偏移的累计同现总数来归一化。默认是False

greycomatrix()函数返回一个 4D n 数组。这是灰度共生直方图。输出值P[i,j,d,theta]表示灰度级 j 在距离灰度级j距离 d 和角度θ处出现多少次。如果参数 normed 为False(这是默认值),则输出为类型uint32(32 位无符号整数);否则,它就是float64(64 位浮点)。

清单 4-4 向您展示了如何使用 skimage 库计算特征统计量来计算 GLCM。

Filename: Listing_4_4.py
1    import cv2
2    import skimage.feature as sk
3    import numpy as np
4
5    #Read an image from the disk and convert it into grayscale
6    image = cv2.imread("images/nature.jpg")
7    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
8
9    #Calculate GLCM of the grayscale image
10   glcm = sk.greycomatrix(image,[2],[0, np.pi/2])
11   print(glcm)

Listing 4-4GLCM Calculation Using the greycomatrix() Function

第 10 行通过传递 image NumPy 变量和距离[2],使用greycomatrix()计算 GLCM。第三个参数以弧度为单位。np.pi/2是 90 度角的弧度。最后一行,即第 11 行,简单地打印了 4D ndarray。

如前所述,GLCM 不直接用作特征,但我们使用它来计算一些有用的统计数据,这给了我们关于图像纹理的想法。下表列出了我们可以得出的统计数据:

|

统计的

|

描述

|
| --- | --- |
| 对比 | 测量 GLCM 中的局部变化。 |
| 相互关系 | 测量指定像素对出现的联合概率。 |
| 活力 | 提供 GLCM 中元素的平方和。也称为均匀性或角二阶矩。 |
| 同种 | 测量 GLCM 中元素分布与 GLCM 对角线的接近程度。 |

在这里,我们为您提供一些用于计算之前统计数据的高级公式。这些公式的正式数学处理超出了本书的范围;然而,我们鼓励你探索这些统计数据的数学基础。

  • 对比度= $$ {\sum}_{i,j=0}^{levels-1}{P}_{i,j}{\left(i-j\right)}² $$

  • 相异度= $$ {\sum}_{i,j=0}^{levels-1}{P}_{i,j}\left|i-j\right| $$

  • 同质性= $$ {\sum}_{i,j=0}^{levels-1}\frac{P_{i,j}}{1+{\left(i-j\right)}²} $$

  • ASM = $$ {\sum}_{i,j=0}^{levels-1}{P}_{i,j}² $$

  • 能量= $$ \sqrt{ASM} $$

  • 相关性= $$ \sum \limits_{i,j=0}^{levels-1}{P}_{i,j}\left[\frac{\left(i-{\mu}_i\right);\left(j-{\mu}_j\right)}{\sqrt{\left({\sigma}_i²\right);\left({\sigma}_j²\right)}}\right] $$

其中, P 是要计算指定属性的 GLCM 直方图。值 P ijdθ是灰度级 j 在距离 d 处和与灰度级 i 成角度θ处出现的次数。

我们将使用 skimage 包中的greycoprops()来计算 GLCM 中的这些统计数据。该函数的定义如下:

greycoprops(P, prop="contrast")

第一个参数是 GLCM 直方图(参见清单 [4-4 ,第 10 行)。

第二个参数是我们要计算的属性。我们可以为这个参数传递以下任何属性:contrastdissimilarityhomogeneityenergycorrelationASM.

如果没有传递第二个参数,它将默认为contrast

清单 4-5 展示了如何计算这些统计数据。

Filename: Listing_4_5.py
1    import cv2
2    import skimage.feature as sk
3    import numpy as np
4
5    #Read an image from the disk and convert it into grayscale
6    image = cv2.imread("images/nature.jpg")
7    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
8
9    #Calculate GLCM of the grayscale image
10   glcm = sk.greycomatrix(image,[2],[0, np.pi/2])
11
12   #Calculate Contrast
13   contrast = sk.greycoprops(glcm)
14   print("Contrast:",contrast)
15
16   #Calculate 'dissimilarity'
17   dissimilarity = sk.greycoprops(glcm, prop='dissimilarity')
18   print("Dissimilarity: ", dissimilarity)
19
20   #Calculate 'homogeneity'
21   homogeneity = sk.greycoprops(glcm, prop='homogeneity')
22   print("Homogeneity: ", homogeneity)
23
24   #Calculate 'ASM'
25   ASM = sk.greycoprops(glcm, prop='ASM')
26   print("ASM: ", ASM)
27
28   #Calculate 'energy'
29   energy = sk.greycoprops(glcm, prop='energy')
30   print("Energy: ", energy)
31
32   #Calculate 'correlation'
33   correlation = sk.greycoprops(glcm, prop='correlation')
34   print("Correlation: ", correlation)

Listing 4-5Calculation of Image Statistics from the GLCM

清单 4-5 展示了如何使用greycoprops()函数并将不同的参数传递给prop来计算各自的统计数据。图 4-14 显示了列表 4-5 的输出。

img/493065_1_En_4_Fig14_HTML.png

图 4-14

基于 GLCM 的各种统计输出

方向梯度直方图是计算机视觉和机器学习中用于目标检测的重要特征描述符。猪描述图像中对象的结构形状和外观。HOG 算法计算图像局部的梯度方向的出现。

HOG 算法分五个阶段工作,如下所述。

阶段 1:全局图像标准化:这是一个可选阶段,只需要减少照明效果的影响。在此阶段,通过以下方法之一对图像进行全局标准化:

  • 伽玛(幂律)压缩:应用 log(p)改变每个像素值 p 。这样会过度压缩像素,不建议这样做。

  • 平方根归一化:每个像素值 p 变为[?][?][?] p (像素值的平方根)。这种方法对像素的压缩小于伽玛压缩,被认为是首选的归一化技术。

  • 方差归一化:对于大多数机器学习工作,我使用这种技术,与其他两种方法相比,得到了更好的结果。在这个方法中,我们首先计算像素值的平均值(𝜇)和标准差(【σ)。然后,根据以下公式归一化每个像素值 p :

  • TP=(p-)/

***阶段 2:计算 x 和 y 上的梯度图像:第二阶段计算一阶图像梯度,以捕捉轮廓、轮廓和一些纹理信息。如果你需要捕捉棒状特征,比如人类的四肢,你还需要包含二阶图像导数。清单 3-19 和 3-20(在第三章)展示了如何计算 X 和 Y 方向的梯度。如果需要的话,请继续阅读第三章的“渐变和边缘检测”一节。假设 X 方向上的梯度是 G x 并且 Y 方向上的梯度是 G y ,使用下面的公式计算梯度幅度:

$$ \left|\mathrm{G}\right|=\sqrt{G_x²+{G}_y²} $$

最后,使用以下公式计算梯度方向:

$$ \Theta =\arctan;\left({\mathrm{G}}_{\mathrm{y}}/{\mathrm{G}}_{\mathrm{x}}\right) $$

一旦计算出梯度和方向的值,就可以计算直方图。

阶段 3:计算梯度直方图:图像被分成小的空间区域,称为单元。使用前面的|G|和θ公式,我们在每个单元的所有像素上累积梯度或边缘方向的局部 1D 直方图。每个方向直方图将梯度角度范围分成固定数量的预定仓。单元中像素的梯度幅度被用于投票到方向直方图中。投票的权重仅仅是给定像素处的梯度幅度|G|。

阶段 4:跨块标准化:将少量单元组合在一起形成一个正方形块。整个图像现在被分成块(由一组单元组成)。块的形成通常通过在几个块之间共享单元来完成。因此,细胞以不同的标准化在最终的输出载体中出现几次。然后对这些局部块进行归一化。这是通过累积局部块内的局部直方图“能量”的度量来执行的。这些标准化的块描述符就是 HOG。图 4-15 显示了区块形成。

img/493065_1_En_4_Fig15_HTML.png

图 4-15

通过细胞分组形成细胞块(3×3 细胞块)

阶段 5:展平成一个特征向量:在所有的块都被归一化之后,我们将得到的直方图连接起来以构建我们最终的特征向量。

如果所有这些关于 HOG 的细节看起来让人不知所措,不要担心。我们不需要自己编写代码来实现这些;有几个库提供了简单计算 HOG 的函数。

我们将使用 scikit-image 库来计算图像的 HOG。scikit-image 库的包skimage中的子包feature提供了一种计算 HOG 的方便方法。下面是函数签名:

out, hog_image = hog(image, orientations=9, pixels_per_cell=(8, 8), cells_per_block=(3, 3), block_norm='L2-Hys', visualize=False, transform_sqrt=False, feature_vector=True, multichannel=None)

参数描述如下:

image:这是输入图像的 NumPy 表示。

orientation:定位仓数量默认为 9 个。

pixels_per_cell:这是作为一个元组的每个单元格的像素数;对于 8×8 像元大小,它默认为(8,8)。

cells_per_block:这是每个块的单元数,为一个元组;默认为(3,3),是针对 3×3 的单元格,而不是像素。

block_norm:这是块标准化方法,作为一个字符串,具有以下值之一:L1,L1-sqrt,L2,L2-Hys。这些规范化字符串解释如下:

L1:使用 L1-诺姆标准化使用此公式:

L1-诺姆= $$ \sum \limits_{r=1}^n\left|{X}_r\right| $$

L1-sqrt:L1 标准化值的平方根。它使用这个公式:

L1-sqrt = $$ \sqrt{\sum \limits_{r=1}^n\left|{X}_r\right|} $$

L2:使用 L2-诺姆标准化使用此公式:

L2-诺姆= $$ \sqrt{\sum \limits_{r=1}^n{\left|{X}_r\right|}²} $$

L2-海斯:这是参数block_norm的默认规范化。通过首先进行 L2 归一化,将结果限制在最大值 0.2,然后重新计算 L2 归一化来计算 L2-海斯。

visualize:如果设置为True,该功能还会返回猪的图像。其默认值设置为False

Transform_sqrt:如果设置为True,该功能将在处理前应用幂律压缩对图像进行归一化处理。

feature_vector:该参数的默认值设置为True,指示函数将输出数据作为特征向量返回。

multichannel:将该参数的值设置为True,表示输入图像包含多通道。图像的尺寸一般表示为高×宽×通道。如果此参数的值为True,则最后一个维度(通道)被解释为颜色通道,否则被解释为空间通道。

这个hog()函数返回什么?

out:该函数返回一个包含(n_blocks_row, n_blocks_col, n_cells_row, n_cells_col, n_orient)的 ndarray。这是图像的 HOG 描述符。如果参数feature_vectorTrue,则返回一个 1D(展平)数组。

hog_image:如果参数visualize设置为True,该函数也返回猪图像的可视化。

清单 4-6 展示了如何使用 skimage 包计算 HOG。

Filename: Listing_4_6.py
1    import cv2
2    import numpy as np
3    from skimage import feature as sk
4
5    #Load an image from the disk
6    image = cv2.imread("images/obama.jpg")
7    #Resize the image.
8    image = cv2.resize(image,(int(image.shape[0]/5),int(image.shape[1]/5)))
9
10   # HOG calculation
11   (HOG, hogImage) = sk.hog(image, orientations=9, pixels_per_cell=(8, 8),
12       cells_per_block=(2, 2), visualize=True, transform_sqrt=True, block_norm="L2-Hys", feature_vector=True)
13
14   print("Image Dimension",image.shape)
15   print("Feature Vector Dimension:", HOG.shape)
16
17   #showing the original and HOG images
18   cv2.imshow("Original image", image)
19   cv2.imshow("HOG Image", hogImage)
20   cv2.waitKey(0)

Listing 4-6HOG Calculation

理解猪很重要。我们将在第 6 、 7 和 8 章中应用猪的概念来建造一些真实而有趣的东西。尽管我们花了一些时间来理解这个概念,但是 HOG 的计算只用一行代码就完成了(第 11 行,清单 4-6 )。我们使用了 skimage 包的feature子包中的hog()函数。传递给hog()函数的参数在前面已经解释过了。

我们如何知道我们传递的是hog()函数中参数的正确值?嗯,真的没有既定的规则。根据经验,我们应该从所有默认参数开始,并在分析结果时对它们进行调整。

值得一提的是,hog()函数生成了一个非常高维度的直方图。带有pixel_per_cell=(4,4)cells_per_block=(2,2)的 32×32 的图像将产生 1764 维的结果。类似地,128×128 像素的图像将产生 34,596 维的输出。因此,注意参数并适当调整图像大小以减少输出尺寸是非常重要的。这将对内存、存储需求和网络传输时间产生巨大影响。

图 4-16 到 4-18 显示了列表 4-6 的输出。

img/493065_1_En_4_Fig18_HTML.png

图 4-18

print()语句的维度输出

img/493065_1_En_4_Fig17_HTML.png

图 4-17

猪形象

img/493065_1_En_4_Fig16_HTML.png

图 4-16

调整图像大小

垂直线间的距离

局部二值模式是一种用于图像纹理分类的特征描述符。LBP 特征提取的工作方式如下:

img/493065_1_En_4_Fig19_HTML.png

图 4-19

LBP 像素值计算

  1. 对于图像中的每个像素,比较周围像素的像素值。如果周围像素的值小于中心像素,则标记为 0;否则 1。在图 4-19 中,中心像素的值为 20,周围有 8 个邻居。图 4-19 的中间部分显示了根据像素值是小于还是大于中心像素(本例中为 20)将像素值转换为 0 或 1。

  2. 从邻居的任何一个像素开始,向任何方向移动,我们将 0 和 1 的序列组合成一个 8 位二进制数。在下面的例子中,我们从右上角开始,顺时针移动,将数字组合成 10101000 个二进制数。这个二进制数转换成十进制得到中心像素的像素值,如图 4-19 所示。

  3. 对于图像中的每个像素,我们重复前面的步骤来获得基于邻居像素的像素值。确保所有像素的起始位置和方向保持一致。

  4. 当所有像素都完成后,我们将像素值排列在一个 LBP 数组中。

  5. 最后,我们计算 LBP 阵列的直方图。这个直方图被作为 LBP 特征向量。

这种计算 LBP 特征向量的方法允许我们捕捉图像纹理的更精细的细节。但是对于大多数机器学习分类问题,细粒度特征可能不会给出期望的结果,特别是当输入图像具有不同尺度的纹理时。

为了克服这个问题,我们有一个 LBP 的增强版本,如下所述。

LBP 的增强版本允许可变的邻域大小。现在,我们有两个额外的参数可以使用。

  • 代替固定的正方形邻域,我们可以定义圆形对称邻域中的点数 p

  • 圆的半径, r ,允许我们定义不同的邻域大小。

图 4-20 显示了绿点的点数和半径变化的虚线圆。半径越小,捕捉的纹理越精细。增加半径使我们能够对不同尺度的纹理进行分类。

我们现在准备学习如何实现 LBP。我们将再次使用 scikit-image(特别是来自skimage包的feature子包)。以下是我们将用于 LBP 计算的函数签名:

local_binary_pattern(image, P, R, method="default")

参数解释如下:

image:灰度图像的 NumPy 表示。

P:沿着围绕计算 LBP 的点的圆的邻近点的数量。这是图 4-20 中绿点的数量。

img/493065_1_En_4_Fig20_HTML.jpg

图 4-20

基于邻域大小和点数的 LBP 计算

R:浮点数,定义圆的半径。

method:该参数取以下任意一个字符串值:defaultroruniformvar。这些方法值的含义解释如下:

  • default:指示函数根据灰度计算原始 LBP,不考虑旋转不变量。旋转不变二进制描述符的描述超出了本书的范围。要了解这方面的更多信息,请查看 http://ivg.au.tsinghua.edu.cn/~jfeng/pubs/Xuetal_TIP14_Descriptor.pdf 的论文“OSRI:旋转不变的二元描述符”。

  • ror:该方法指示函数使用旋转不变的二进制描述符。

  • uniform:这使用了改进的旋转不变性,具有均匀的模式和角度空间的更精细的量化,是灰度和旋转不变的。如果在二进制数字序列中至多有两个 0-1 到 1-0 的转换,则认为二进制模式是一致的。例如,00100101 是一个统一的模式,因为它有两个转换(显示为红色和蓝色)。类似地,00010001 也是均匀模式,因为它有一个 0-1 到 1-0 的转换。另一方面,01010100 不是一个统一的模式。在 LBP 直方图的计算中,直方图对于每个均匀模式具有单独的仓,并且所有非均匀模式被分配到单个仓。使用统一的模式,单个细胞的特征向量的长度从 256 减少到 59。

  • nri_uniform:非旋转不变均匀模式变体,仅灰度不变。

  • var:局部图像纹理对比度的旋转不变方差度量,具有旋转不变性,但不具有灰度不变性。

函数local_binary_pattern()的输出是表示 LBP 图像的 n 数组。

我们已经介绍了足够的背景知识,可以开始实施 LBP 并看到它的实际应用。清单 4-7 演示了local_binary_pattern()函数的用法。

它首先从磁盘加载图像,调整其大小,并将其转换为灰度。

第 12 行计算原始图像的直方图。第 14 行到第 16 行绘制了原始图像直方图。

Filename: Listing_4_7.py
1    import cv2
2    import numpy as np
3    from skimage import feature as sk
4    from matplotlib import pyplot as plt
5
6    #Load an image from the disk, resize and convert to grayscale
7    image = cv2.imread("images/obama.jpg")
8    image = cv2.resize(image, (int(image.shape[0]/5), int(image.shape[1]/5)))
9    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
10
11   # calculate Histogram of original image and plot it
12   originalHist = cv2.calcHist(image, [0], None, [256], [0,256])
13
14   plt.figure()
15   plt.title("Histogram of Original Image")
16   plt.plot(originalHist, color='r')
17
18   # Calculate LBP image and histogram over the LBP, then plot the histogram
19   radius = 3
20   points = 3*8
21   # LBP calculation
22   lbp = sk.local_binary_pattern(image, points, radius, method='default')
23   lbpHist, _ = np.histogram(lbp, density=True, bins=256, range=(0, 256))
24
25   plt.figure()
26   plt.title("Histogram of LBP Image")
27   plt.plot(lbpHist, color='g')
28   plt.show()
29
30   #showing the original and LBP images
31   cv2.imshow("Original image", image)
32   cv2.imshow("LBP Image", lbp)
33   cv2.waitKey(0)

Listing 4-7LBP Image and Histogram Calculation and Comparison with Original Image

LBP 图像的计算在第 22 行执行。注意,我们使用默认方法计算 LBP,半径为 3,点数为 24。第 22 行使用了skimage包的feature子包中的local_binary_pattern()函数。

第 23 行计算了 LBP 图像的直方图。为什么我们使用了 NumPy 的直方图函数?如果您尝试对 LBP 图像使用cv2.calcHist()函数,您将会收到一条错误消息,提示“-210 不支持的格式或格式组合。”这是因为local_binary_pattern()的输出格式不同,OpenCV 的calcHist()函数不支持。出于这个原因,我们使用 NumPy 的histogram()功能。

图 4-21 为原图。我们来看看清单 4-7 的输出。图 4-22 是从输入图像计算出的 LBP 图像(图 4-21 )。请注意它是如何巧妙地捕捉到原始图像的纹理的。比较图 4-23 和图 4-24 分别从原始图像和 LBP 图像绘制的直方图。

img/493065_1_En_4_Fig24_HTML.jpg

图 4-24

LBP 图像的直方图

img/493065_1_En_4_Fig23_HTML.jpg

图 4-23

原始图像的直方图

img/493065_1_En_4_Fig22_HTML.jpg

图 4-22

LBP 图像

img/493065_1_En_4_Fig21_HTML.jpg

图 4-21

原始灰度图像

请注意,有时 LBP 与 HOG 一起使用,以提高对象检测的准确性。

在本节中,我们的重点是学习不同的技术来执行特征提取。我们重点学习了这些特征提取技术的概念,这将在下一章学习机器学习和神经网络时有所帮助。我们将在第 6 到 9 章开发真实世界的用例时利用这些概念。

本章的下一节是关于特征选择策略。

特征选择

在机器学习中,特征选择是选择与模型训练相关且有用的变量或属性的过程。这是一个消除不必要或不相关的特征并选择对模型学习有重要贡献的特征子集的过程。了解特征选择的原因如下:

  • 降低模型的复杂性并使其更易于解释

  • 为了减少机器学习训练时间

  • 通过输入正确的变量集来提高模型的准确性

  • 为了减少过度拟合

那么,特征选择和特征提取有什么不同呢?特征提取是创建特征的过程,而特征选择是利用特征子集或去除不必要特征的过程。特征提取和选择统称为特征工程

统计证明,存在一个最佳特征数量,超过这个数量,模型性能开始下降。问题是,我们如何知道最佳数量是多少,以及我们如何决定使用哪些功能,不使用哪些功能?本节试图回答这个问题。

有许多特征选择技术。我们现在将探索用于机器学习的一些常见特征选择技术。

过滤方法

您有一个特征集,并且您想要选择一个子集来提供给您的机器学习算法。换句话说,您希望在机器学习被触发之前已经选择了特征。过滤是一个允许您进行预处理以选择特征子集的过程。在此过程中,您将确定特征和目标变量之间的相关性,并根据统计分数确定它们之间的关系。注意,过滤过程独立于任何机器学习算法。仅基于特征变量和目标变量之间的关系来选择(或拒绝)特征。有几种统计方法可以帮助我们根据目标变量对特征进行评分。

下表提供了选择方法以确定特征-目标关系的实用指南:

|

特征变量类型

|

目标变量类型

|

统计方法名称

|
| --- | --- | --- |
| 连续的 | 连续的 | 皮尔逊相关 |
| 连续的 | 绝对的 | 线性判别分析(LDA) |
| 绝对的 | 绝对的 | 卡方检验 |
| 绝对的 | 连续的 | 方差分析 |

对这些统计方法的描述超出了本书的范围。关于这些古老的方法,有各种各样的书籍和在线资源。

包装方法

在包装方法中,您使用特征的子集并训练模型。评估模型,并根据结果添加或移除特征并重新训练模型。重复这个过程,直到你得到一个精度可以接受的模型。这更像是一种试错法来寻找正确的功能子集。另外,这在计算上是很昂贵的,因为你必须实际构建多个模型(并且很可能扔掉所有你不满意的)。

有几种方法实际用于在包装方法下执行特征选择,如下所示:

  • 正向选择:从一个特征开始,建立并评估模型。迭代地,不断添加能够最好地改进模型的特性。

  • 逆向淘汰:从所有特征入手,建立并评估模型。通过消除特征进行迭代,直到获得最佳模型。重复此操作,直到在特征移除上没有观察到改进。

  • 递归特征消除:在递归特征消除过程中,我们重复创建模型,并在每次迭代中留出性能最好或最差的特征。这些特征或者按照它们的系数或者按照特征的重要性排序,最不重要的特征被排除。递归地,我们用剩余的特性创建新的模型,直到所有的特性都用尽。

嵌入式方法

在嵌入式方法中,特征选择由机器学习算法在模型被训练时完成。用于回归算法的 LASSO 和 RIDGE 正则化方法就是此类算法的示例,其中评估了对模型精度有贡献的最合适的特征。

Lasso 回归使用 L1 正则化,并添加一个相当于系数大小绝对值的惩罚。

岭回归使用 L2 正则化,并添加一个相当于系数大小平方的惩罚。

由于模型本身评估特征的重要性,这是特征选择的最便宜的方法之一。

这本书是关于如何构建基于机器学习和深度学习的计算机视觉应用。虽然特征提取和选择是任何机器学习算法的重要部分,这本书只涵盖了关于它的介绍性信息。这是一个很大的主题,值得为这个主题写一本单独的书。

模特培训

让我们回顾一下图 4-2 中的图像处理流程。到目前为止,我们已经学习了如何摄取图像并进行预处理以提高图像质量。这种预处理使我们能够将输入图像转换成适合流水线中的下一个步骤的格式:特征提取和选择。在本章的前一节,我们探讨了特征工程的各种技术。我希望你已经掌握了目前为止提出的概念,并且你已经准备好学习应用于计算机视觉的机器学习。

如何进行机器学习

假设您已经从大量图像中提取并选择了特征。顺便问一下,什么是大数字?嗯,这个问题没有幻数可以回答。该数字应该是我们试图建模的实际场景的真实(或者至少接近真实)表示。记住,好的特性集的特征之一是可重复性。虽然没有很好的方法来得出一个“大”的数字,但是经验法则是“越多越好”才能得到一个好的模型结果。

这些特征集被馈送给数学算法以确定某些模式(我们将在后面更多地讨论这些算法)。算法的输出被称为一个模型,创建这个模型的过程被称为训练模型。换句话说,计算机使用一种算法从输入特征集中学习模式。用于训练模型的特征集被称为训练集。见图 4-25 。

img/493065_1_En_4_Fig25_HTML.jpg

图 4-25

ML 模型训练图解

概括地说,有两种类型的训练集,因此也有两种类型的机器学习:监督学习和非监督学习。这些将在下面描述。

监督学习

假设你有一个 8×8 的图像,所有 64 个像素的值就是你的特征。此外,假设您有几个这样的图像,并且已经从中提取了像素值来构成一个特征集。一幅图像的所有 64 个特征被排列成一个数组(或向量)。特征集将具有与训练集中的图像数量一样多的行,每行代表一个不同的图像。现在,使用该数据集,您希望训练一个模型,该模型可以将输入图像分类到某个类别。例如,您希望根据图像中包含的是一只狗还是一只猫来对图像进行分类(现在让我们保持简单)。

进一步假设这些训练图像已经被标记,这意味着它们已经被识别并被标记为哪个图像包含狗,哪个图像包含猫。这意味着我们已经为每张图片确定了正确的类别。

图 4-26 显示了一个带标签的训练集的样本。图 4-26 的第 1 列是唯一标识图像的图像 ID。第 2 列到第 65 列显示了所有 64 列的像素值(因为在本例中我们的图像尺寸是 8×8)。这些像素值一起形成了我们的特征向量(X)。最后一列是标签列(y ),狗的值为 0,猫的值为 1(标签必须是数字才能输入机器学习)。这些标签也被称为目标变量因变量

img/493065_1_En_4_Fig26_HTML.jpg

图 4-26

带有标注特征向量的数据集示例

当我们通过将包含特征向量和相关标签的数据集馈送给学习算法来训练机器学习模型时,它被称为监督学习

监督学习算法(见图 4-27 )通过优化一个以特征向量作为输入并生成标签作为输出的函数来学习。在下一章你将会学到更多关于各种优化函数的知识。

img/493065_1_En_4_Fig27_HTML.png

图 4-27

监督学习图解

有几种监督学习算法,如支持向量机(SVM)、线性回归、逻辑回归、决策树、随机森林、人工神经网络(ANN)和卷积神经网络(CNN)。

这本书是关于应用深度学习或神经网络(ANN 和 CNN)来训练计算机视觉的模型。在下一章中,你将学习关于这些深度学习算法的细节,以及如何为计算机视觉训练模型。

无监督学习

在前面的例子中,每个特征向量都有一个相关联的标签。这种标记数据集的学习目标是找到特征向量和标记之间的关系。如果没有与特征向量相关联的标签会怎样?换句话说,您对模型的输入只有特征向量,没有输出或标签,并且您希望您的机器学习算法从该输入数据集进行学习。你将从只有特征向量的数据集训练的模型被称为无监督学习

无监督学习算法(见图 4-28 )将仅包含特征向量的数据集作为输入,并确定数据中的结构或模式,如数据的分组或聚类。这意味着算法从没有任何标记数据的训练集中学习,并在数据中找到共性。

img/493065_1_En_4_Fig28_HTML.png

图 4-28

无监督学习

无监督学习用于对数据集进行聚类或分组。无监督学习的另一个应用是为你的监督学习算法创建标签。

一些常用的无监督算法是 K 均值聚类、自动编码器、深度信念网和 hebbian 学习。

这本书只涵盖计算机视觉中使用的监督学习。

模型部署

那么,在你创建了一个经过训练的机器学习模型之后,会发生什么呢?

在回答这个问题之前,让我们先了解一下我们用一个训练好的模型做什么。

在监督学习的情况下,经过训练的模型为我们提供了一个函数,该函数将一个特征集作为输入,并给我们一个输出。输出通常被称为预测。换句话说,模型根据输入数据预测结果。这种预测可以是连续的值或类。

类似地,在无监督学习的情况下,经过训练的模型采用一个特征集,并作为输入特征所属的组或聚类给出输出。分组或聚类可进一步用于创建监督学习的标签。

现在,为了回答我们的第一个问题,我们部署了一个经过训练的模型,以便我们可以预测或分类外部业务应用可能提供的图像(或输入数据集)。基于业务用例,这些预测/分类用于各种分析和决策制定。

输入图像可能由一些外部应用生成。这些图像以与在用于模型训练的特征工程期间处理图像相同的方式被摄取和处理。从摄取的图像中提取特征,并传递给模型函数以获得预测或分类。

虽然模型开发是一个迭代的过程,但是一旦模型给出了可接受的准确性,我们通常会对模型进行版本化,并在生产中部署它。在实践中,直到准确度开始下降,或者直到用新数据集重新训练有望提高准确度,才改变模型或者用新数据重新训练模型。

然而,一个模型被期望比重新训练模型更频繁地使用。在某些情况下,每秒可能需要预测或分类数百或数千个输入图像。在其他情况下,我们可能需要在一天内或以一定的频率批量分类数百万张图像。因此,我们需要以这样一种方式部署我们的模型,即它们可以根据输入量和处理负载进行伸缩。

拥有正确的部署架构对于我们能够在生产中有效地利用模型是至关重要的。让我们探索在生产中为模型服务的不同方式。

  • 嵌入式模型:模型工件被用作消费应用代码中的依赖项。它是与调用模型函数作为内部库函数的应用一起构建和部署的。对于边缘计算设备的嵌入式应用(如物联网),这是一种很好的方法,但不适合数据量大且处理需要扩展的企业应用。此外,在这种情况下,部署新版本的模型更加困难;您可能需要重新构建整个应用代码并再次部署。

  • 作为独立服务部署的模型:在这种方法中,模型被包装在服务中。服务是独立部署的,并与消费应用相分离。这允许我们在不影响其他应用的情况下更新和重新部署模型。消费应用通过远程调用进行服务调用,这可能会带来一些延迟。

  • 作为 RESTful web 服务部署的模型:这类似于前面描述的方法。在这种情况下,使用 TCP/IP 协议通过 RESTful API 调用来调用模型。这种方法提供了可伸缩性和负载平衡,但是网络延迟可能是一个问题。

  • 为分布式处理部署的模型:这是一个高度可伸缩的模型部署。在这种方法中,输入图像(数据集)存储在一个集群的所有节点都可以访问的分布式存储中。模型部署在所有集群节点中。所有参与节点从分布式存储中获取输入数据,对其进行处理,并将预测结果存储到分布式存储中供应用使用。分布式存储的一些例子是 Hadoop 分布式文件系统(HDFS)、亚马逊 S3、谷歌云存储和 Azure Blob 存储。

您将在第十章中了解如何在云上扩展模型开发和部署。

摘要

这一章,连同之前的所有章节,为使用人工神经网络开发计算机视觉应用奠定了坚实的基础。在这一章中,我们探讨了图像处理流水线、其组件以及它们在构建基于机器学习的计算机视觉系统中的作用。您学习了各种特征提取和选择技术。我们还在高层次上探索了不同的机器学习算法、模型训练和部署。

下一章第五章是本书的中心主题。在那一章中,我们将讨论各种机器学习模型,并实现应用于计算机视觉的 ANN、CNN、RNN 和 YOLO 模型。我们将使用 Keras 深度学习库编写 Python 代码,并在 TensorFlow 上执行。

这可能是一个完美的时间去回顾和回顾在所有以前的章节中提出的概念。如果您已经完成了所有的代码示例,那么您的开发环境很可能已经为下一章做好了准备。如果没有,返回第一章,安装所有必备软件,并准备好您的开发计算机。我们要做一些严肃的工作,学习一些真正有趣的东西。如果你们都准备好了,我们走吧!***

五、深度学习和人工神经网络

本章将涵盖深度学习和人工神经网络。本章将通过工作代码示例来探讨这一主题,以展示如何在计算机视觉中应用深度学习概念。我们本章的学习目标如下:

  • 理解神经网络、它们的架构以及在幕后工作的各种数学函数和算法。

  • 在 TensorFlow 中编写代码来摄取图像,提取特征,并训练不同类型的神经网络。

  • 编写代码并了解如何在图像分类中使用预训练和定制训练的模型。我们还将学习如何重新培训现有模型。

  • 了解如何评估模型和调整参数,以优化模型在准确性方面的性能。

本章将包括一些数学概念和方程式。虽然没有必要对本章中列出的方程的数学有一个正式的理解,但是我们确实为你提供了一些参考资料来探索这些方程的数学处理。

人工神经网络导论

人工神经网络(ANN)是一种计算系统,它被设计成以人脑的方式工作。让我们用一个简单的例子来理解这一点。

假设你看到一个你从未见过的物体。有人告诉你这是一辆车。然后你会看到许多其他物体,并学会识别它们。然后你看到另一个物体,你试着猜它是什么。你可能会说,“我想我以前见过这个。”或者你可以说,“我猜这是一辆汽车。”这意味着你不能 100%确定物体的身份。现在,假设你看到许多不同形状、大小、方向和颜色的汽车。你受过识别“汽车”物体的全面训练。最有可能的是,你不会说“我猜”,但你会说,“这是一辆车。”这意味着,随着你通过观察大量汽车而更好地训练自己,你识别汽车的信心会增加。

这里所发生的是,当你只看一次或几次一辆车时,如果它以你以前看到的相同或相似的方式出现,你就学会了认出它。但是当你以各种各样的方式看到大量的样本时,你学会了以 100%或接近 100%的准确率来识别物体。让我们看看图 5-1 中的图表,看看信息在我们的大脑中是如何处理的(人脑功能的简化版)。

img/493065_1_En_5_Fig1_HTML.jpg

图 5-1

人类的眼睛作为传感装置,向储存模式的大脑输入信息

我们的眼睛是一个传感装置。当我们看到一个物体时,我们的眼睛捕捉到该物体的图像,并将其作为输入信号传递给大脑。我们大脑中的神经元对输入信号进行计算并产生输出。

如图 5-2 所示,树突接收输入信号(X)。神经元组合这些输入信号,并使用某种函数执行计算。输出被传送到轴突末梢。

img/493065_1_En_5_Fig2_HTML.jpg

图 5-2

人类神经元的信息处理

人体有数十亿个神经元,它们之间有数万亿个相互连接。这些相互连接的神经元被称为一个神经网络。

计算机科学家受到人类视觉系统的启发,试图通过创建一个像我们的大脑一样学习和运行的计算机系统来模仿神经网络。这个学习系统被称为人工神经网络 (ANN)。

图 5-3 与图 5-1 相似。相机作为一种传感设备工作,就像我们的眼睛捕捉物体的图像一样。图像被传输到解释系统,如计算机,在那里它们以类似于神经元处理输入信号的方式被处理。其他感测设备的一些例子是 X 射线、CT 扫描和 MRI 机器;卫星成像系统;和文档扫描仪。诸如计算机之类的解释设备提供对由照相机获取的数据的处理。大多数与计算机视觉相关的计算,如特征提取和模式确定,都是在计算机中进行的。

img/493065_1_En_5_Fig3_HTML.jpg

图 5-3

向计算机输入图像的人工传感设备(照相机)

图 5-4 类似于图 5-2 所示的人类神经元。变量 x1,x2,..xn 是具有特定权重 w1,w2,..wn 与每个输入信号相关联。使用一些数学函数处理这些输入信号以产生输出。组合这些输入信号的处理单元被称为神经元,以人类神经元命名。计算神经元输出的数学函数被称为激活函数。在图 5-4 中,标有函数符号 f(x) 的圆圈就是神经元。输出 y 由神经元产生。

img/493065_1_En_5_Fig4_HTML.png

图 5-4

人工神经元

感知器

神经网络的单个神经元称为感知器。感知器执行数学功能,对输入信号进行运算并产生输出。图 5-4 是感知器的一个例子。感知器是最简单的神经网络。我们将在后面看到,用于机器学习的典型神经网络由几个神经元组成。神经元的输入或者来自源(照相机或传感设备),或者来自其他神经元的输出。

感知器如何学习

感知器的学习目标是确定每个输入信号的理想权重。学习算法任意地给每个输入信号分配权重。信号值乘以其相应的权重。将每个信号的乘积(加权乘以信号值)相加以计算输出。该计算由以下等式表示:

$$ \boldsymbol{f}\left(\boldsymbol{x}\right)=\boldsymbol{w}\boldsymbol{1}\boldsymbol{x}\boldsymbol{1}+\boldsymbol{w}\boldsymbol{2}\boldsymbol{x}\boldsymbol{2}+\boldsymbol{w}\boldsymbol{3}\boldsymbol{x}\boldsymbol{3}+\dots +\boldsymbol{w}\boldsymbol{nxn} $$

(等式 5-1)

有时一个偏差 x 0 也被加到方程中,如下所示:

$$ \boldsymbol{f}\left(\boldsymbol{x}\right)=\boldsymbol{x}\boldsymbol{0}+\boldsymbol{w}\boldsymbol{1}\boldsymbol{x}\boldsymbol{1}+\boldsymbol{w}\boldsymbol{2}\boldsymbol{x}\boldsymbol{2}+\boldsymbol{w}\boldsymbol{3}\boldsymbol{x}\boldsymbol{3}+\dots +\boldsymbol{w}\boldsymbol{nxn} $$

(方程 5-2)

方程式 5-2 也可以写成:

$$ {\displaystyle \begin{array}{c}i=\mathrm{n}\ {}\mathrm{f}\left(\mathrm{x}\right)={\mathrm{X}}_0+\left[?\right]\left[?\right]\left[?\right]{\mathrm{W}}_{\mathrm{i}}{\mathrm{X}}_{\mathrm{i}}\ {}i=1\end{array}} $$

(方程式 5-3)

神经元使用等式 5-2 对大量输入进行计算。优化功能通过使用特定的数学算法优化权重,称为优化器,并使用新的权重重复计算。这种权重优化和计算以及重新优化在多次迭代中执行,直到权重对于给定的输入集完全优化。我们将在本章的后面了解关于这个优化函数的更多信息。完全优化的权重是神经元的实际学习。

多层感知器

就像人脑包含数十亿个神经元一样,人工神经网络包含几个神经元或感知机。输入由一组神经元处理。组中的每个神经元独立处理输入。这组神经元的输出被馈送到另一个神经元或另一组神经元进行进一步处理。你可以想象这些神经元排列成层,其中一层的输出作为输入输入到下一层。您可以根据需要设置任意多个层来训练您的神经网络。这种在神经网络中排列神经元的多层方法通常被称为多层感知器 (MLP)。图 5-5 显示了一个 MLP 的例子。

img/493065_1_En_5_Fig5_HTML.png

图 5-5

多层感知器

为什么是 MLP?

让我们考虑具有单个输入的单个神经元。等式 5-1 看起来会像下面这样:

$$ \boldsymbol{f}\left(\boldsymbol{x}\right)=\boldsymbol{x}\boldsymbol{0}+\boldsymbol{w}\boldsymbol{1}\boldsymbol{x}\boldsymbol{1} $$

这代表一条截距为 x 0 且斜率(与水平线或x-轴的角度)等于 w 1 的直线方程。

如果你不理解这个数学,不要担心。这是为了向你展示单个神经元模拟输入到输出的线性关系。机器学习算法,如线性回归和逻辑回归,对线性关系进行建模。大多数现实世界的问题并不呈现线性关系。多层感知器对非线性进行建模,可以比基于单个神经元的模型更准确地对现实世界的问题进行建模。

什么是深度学习?

深度学习是多层人工神经网络或多层感知器的别称。根据神经网络架构及其工作原理,我们有不同类型的深度学习系统。例如,前馈神经网络、卷积网络、递归神经网络、自动编码器和深度信念是不同类型的深度学习系统。

以下部分首先解释多层感知器的高级架构。在本书中,我们将互换使用 MLP 和深度学习。

深度学习或多层感知器架构

一个多层感知器至少由三类层组成:输入层、隐藏层、输出层(如图 5-5 )。您可以有多个隐藏层。每层包含一个或多个神经元。神经元对它获得的输入进行一些计算,并产生输出。神经元的输出作为输入发送到下一层,但输出层除外,它生成最终输出供应用使用。

MLP 体系结构包括以下内容:

img/493065_1_En_5_Fig6_HTML.png

图 5-6

具有偏置节点的多层感知器

  • 输入层:神经网络的第一层称为输入层。这一层接受来自外部源的输入,例如来自传感设备的图像。该层的输入为特征(参见第四章了解特征详情)。

    输入层中的节点不做任何计算。这些节点只是将它们的输入传递给下一层。

    输入层中神经元的数量与特征的数量相同。有时,在每层中添加一个额外的节点。这个附加节点被称为偏置节点。添加偏置节点是为了控制该层的输出。在深度学习中,不需要偏差,但增加一个偏差是常见的做法。

    图 5-6 显示了带有偏置节点的神经网络架构。以橙色显示的节点是添加到每个层中的偏移节点。

    问题:神经网络输入层的神经元总数是多少?

    答案:输入层神经元数=无偏差输入特征数=(有偏差输入特征数+ 1)

  • 隐层:输入层和输出层之间的神经元层称为隐层。神经网络必须至少有一个隐藏层。这是学习发生的一层。这一层的神经元进行学习所需的计算。在大多数情况下,一个隐藏层对于学习来说就足够了,但是您可以根据需要拥有任意多个层来模拟真实世界的情况。随着隐藏层数量的增加,计算复杂性也随着计算时间的相应增加而增加。

    隐藏层应该有多少个神经元?没有神奇的数字,有几种可行的策略。常见的做法是取前一层神经元数量的三分之二(或 66%)。例如,如果输入层中的神经元数量为 100,则第一个隐藏层中的神经元数量为 66,下一个隐藏层中的神经元数量为 43,依此类推。同样,没有神奇的数字,您应该根据模型的准确性来调整神经元的数量。

  • 输出层:神经网络的最后一层是输出层。输出层从最后一个隐藏层获取输入。输出层中神经元的数量取决于您希望神经网络解决的问题类型,如下所述:

  • 对于网络必须预测连续值(如股票收盘价)的回归问题,输出节点只有一个神经元。

  • 对于分类问题,当网络必须预测许多类中的一个时,输出层具有与所有可能的类的数量一样多的神经元。例如,如果网络被训练来预测四类动物中的一类——猫、狗、狮子、公牛——输出层将有四个神经元,每类一个。

  • 边或权重连接:权重也被称为系数输入乘数。神经元的每个输入特征都乘以一个权重。形象地说,从输入到神经元的每个连接都用一条加权线连接。加权线表示特征在预测我们试图建模的结果中的贡献。将权重视为输入要素的贡献或重要性。权重越高,特征的贡献越大。如果权重为负,则该特征具有负面影响。如果权重为零,则输入特征不重要,可以从训练集中移除。

    神经网络的训练目标是为每层神经元的每个连接计算每个输入特征的最佳权重。在本章中,我们将了解神经网络如何通过调整权重来学习。如果使用偏差,神经网络也会学习偏差。

激活功能

决定一个神经元输出的数学函数叫做激活函数

神经元使用以下线性方程对输入进行操作:

$$ \mathrm{z}={\mathrm{X}}_0+\sum \limits_{i=0}^{i=n}{w}_i{x}_i $$

(方程 5-4)

但是神经元的输出不是等式 5-4 的结果。激活函数对 z 值(根据等式 5-4 计算)进行操作,并确定神经元的输出。

激活函数根据神经元的输入是否与模型预测相关来确定它所连接的神经元是否应该被激活(打开或关闭)。实际上,激活函数将每个神经元的输出标准化到 0 和 1 之间或-1 和 1 之间的范围。

有几个数学函数被用作不同用途的激活。我们将探索 TensorFlow 开箱即用支持的以下激活功能。我们将在下一节学习更多关于 TensorFlow 的知识。

线性激活函数

线性激活函数根据等式 f(x) = x0+ w1x1 + w2x2 + w3x3 +通过将权重乘以输入来计算神经元输出....+ wnxn 。线性激活函数的输出从-∞到+∞,如图 5-7 所示。这意味着线性激活函数和没有激活一样好。

img/493065_1_En_5_Fig7_HTML.jpg

图 5-7

线性激活函数图

线性激活函数有以下两个主要问题,不用于深度学习:

  • 深度学习使用一种叫做反向传播的方法(稍后会有更多介绍),这种方法使用一种叫做梯度下降的技术。梯度下降需要计算输入的一阶导数,在线性激活的情况下,该导数是常数。常数的一阶导数为零。这意味着它与输入无关。因此,不可能返回并更新输入的权重。

  • 如果使用线性激活函数,不管神经网络的层数是多少,最后一层都将是第一层的线性函数。换句话说,一个线性激活函数把你的网络变成一层。这意味着你的网络只能学习输入到输出的线性相关性,这不适合解决复杂的问题,如计算机视觉。

乙状结肠或逻辑激活功能

sigmoid 激活函数使用 sigmoid 函数计算神经元输出,如下所示:

$$ \sigma \left(\mathrm{z}\right)=1/\left(1+{e}^{-\mathrm{Z}}\right) $$

(等式 5-5)

其中 z 使用等式 5-4 计算。

sigmoid 函数总是产生 0 到 1 之间的值。这使得当输入值波动时,输出平滑,没有很多跳跃。另一个优点是,这是一个非线性函数,不会从一阶导数中产生常数值。这使得它适用于基于梯度下降更新权重的反向传播深度学习。参见图 5-8 。

img/493065_1_En_5_Fig8_HTML.jpg

图 5-8

Sigmoid 激活函数图

sigmoid 函数的最大缺点是输出不会在大或小输入值之间变化,这使得它不适合特征向量包含大或小值的情况。克服这一缺点的一种方法是将特征向量归一化,使其值介于-1 和 1 之间或 0 和 1 之间。

从图 5-8 中你会注意到的另一个特征是 S 形曲线不是以零为中心。

正切/双曲正切

TanH 类似于 sigmoid 激活函数,只是 TanH 以零为中心。参见图 5-9 并注意 S 形曲线穿过原点。

img/493065_1_En_5_Fig9_HTML.jpg

图 5-9

双曲正切激活函数图(零中心)

双曲正切函数使用以下公式计算神经元输出:

$$ \tanh \left(\mathrm{z}\right)=\left({e}{\mathrm{z}}-{e}{-\mathrm{z}}\right)/\left({e}{\mathrm{z}}+{e}{-\mathrm{z}}\right) $$

(等式 5-6)

因为双曲正切函数以零为中心,所以它使用具有小、大和中性值的输入进行建模。

整流器线性单元

整流线性单元 (ReLu)根据等式 5-4 计算的 z 值确定神经元输出。如果 z 的值为正,ReLU 将该值作为输出;否则,它输出为零。ReLU 的输出范围在 0 和+∞之间。ReLU 函数表示如下(参见图 5-10 ):

$$ f(z)=\max \left(0,z\right) $$

(等式 5-7)

img/493065_1_En_5_Fig10_HTML.jpg

图 5-10

ReLU 激活图(取值范围在 0 到无穷大之间)

ReLU 激活功能的优点是计算效率高,并且允许网络快速收敛。此外,ReLU 是非线性的,并且它具有导数函数,这使得它适合于在神经网络学习时用于权重调整的反向传播。

ReLU 函数的最大缺点是,对于零或负输入,函数的梯度变为零。这使得当输入具有负值时,它不适合反向传播。

ReLU 广泛用于大多数计算机视觉模型训练,因为图像像素没有负值。

李奇注意到了

漏 ReLU 提供了 ReLU 的微小变化。它不是使 z 的负值(根据等式 5-3 计算)为零,而是将 z 的负值乘以一个小数值,如 0.01。图 5-11 描述了泄漏的 ReLU 输出。

img/493065_1_En_5_Fig11_HTML.jpg

图 5-11

泄漏 ReLU 图(通过取负值乘以一个小数字来修改 ReLU)

泄漏 ReLU 在负区域具有小斜率,并允许负输入反向传播。

缺点是泄漏 ReLU 的结果与负值不一致。

标度指数线性单位

一个比例指数线性单元 (SELU)使用下面的等式计算神经元输出:

$$ f\left(\alpha, x\right)=\uplambda;\left{\begin{array}{l}\alpha \left({e}^x-1\right);\mathrm{for};x<0\ {}x\kern2.04em \mathrm{for};x\ge 0\end{array}\right. $$

(等式 5-8)

其中λ的值= 1.05070098,𝞪的值= 1.67326324。这些值是固定的,在反向传播期间不会改变。

图 5-12 中的图表显示了 SELU 特性。

img/493065_1_En_5_Fig12_HTML.jpg

图 5-12

SELU 激活图

SELU 具有“自归一化”特性(参见参考文献 1 中关于 SELU 的原始论文)。SELU 的发明者已经用数学方法证明了 SELU 产生的输出是用平均值 0 和标准差 1 归一化的。

在 TensorFlow 或 Keras 中,如果您通过使用方法tf.keras.initializers.lecun_normal 将权重初始化方法用作以零为中心的截断正态分布,您将获得所有网络组件的归一化输出,例如各层的权重、偏差和激活。

那么,我们为什么关心网络产生的归一化输出呢?初始化函数lecun_normal将网络的参数初始化为正态分布或高斯分布。SELU 也生成标准化输出。这意味着整个网络表现出正常的行为。因此,最后一层的输出也是归一化的。

使用 SELU,学习是高度健壮的,并且允许训练网络有许多层。

由于使用 SELU,整个网络是自标准化的,因此它在计算方面是高效的,并且趋向于更快地收敛。另一个优点是,当输入特征太高或太低时,它克服了爆炸或消失梯度的问题。

Softplus 激活功能

softplus 激活函数将平滑应用于激活函数值 z(由等式 5-4 计算)。它使用指数的对数如下:

$$ f(x)=\ln \left(1+{e}^z\right) $$

(等式 5-9)

Softplus 也称为 SmoothReLU 函数。softplus 函数的一阶导数为 1/(1+e -z ,与 sigmoid 激活函数相同。见图 5-13 。

img/493065_1_En_5_Fig13_HTML.jpg

图 5-13

Softplus 激活图

Softmax(软件最大值)

Softmax 是一个函数,它采用实数的输入向量,将其归一化为概率分布,并生成范围为(0,1)的输出,输出值之和等于 1。

它最常用于激活分类神经网络的最后一层(输出层)。结果被解释为每个类别的预测概率。

使用以下公式计算 softmax 变换:

$$ \sigma {\left(\mathrm{z}\right)}_i=\frac{e{z_i}}{\sum_{j=1}K{e}^{z_j}};\mathrm{for};i=1,\dots, K; and;\mathrm{z}=\left({z}_1,\cdots, {z}_K\right)\in {\mathrm{\mathbb{R}}}^K $$

(等式 5-10)

前面等式的归一化输出始终在 0 和 1 之间。当您添加这些输出时,结果将是 1。

前馈

前馈神经网络是一种人工神经网络,其中神经元之间的连接不形成循环。到目前为止,我们所了解的网络是一个前馈神经网络。

前馈神经网络是最简单的神经网络。在这个网络中,信息沿一个方向(正向)流动,从输入层开始到隐藏层,一直到输出层。在这个网络中,没有回送或反馈机制。

图 5-2 和 5-3 所示的示例网络是前馈人工神经网络。

在本书的大部分内容中,我们将使用前馈网络。

误差函数

什么是错误?一个误差,在机器学习的上下文中,是预期结果和预测结果之间的差异。误差方程可以简化为如下形式:

误差=预期结果-预测结果

我们已经知道,神经网络的学习目标是计算权重的优化值。当误差最小(理想情况下为零)时,权重被认为是针对给定数据集优化的。我们已经看到,当网络开始学习过程时,它初始化权重,并通过使用其中一个激活函数来计算每个神经元的输出。然后,它计算误差,调整权重,计算输出,重新计算误差,并与先前计算的误差进行比较,直到找到最小误差。给出最小误差的权重作为最终权重。在这个阶段,网络被认为是“习得的”。

从微积分来看,如果一个函数的一阶导数为零,那么该点的函数不是最小就是最大。找到一阶导数为零的最小点是神经网络训练过程的目标。因此,神经网络必须有一个误差函数,它将计算一阶导数并找到误差函数最小的点(权重和偏差)。这个误差函数应该是什么取决于我们想要训练的模型的类型。误差函数也被称为损失函数,或者简称为损失

计算导数和寻找权重最佳值的数学方法超出了本书的范围。我们将探讨一些常用的误差函数以及它们的应用场合。为了让本书专注于我们的学习目标:构建计算机视觉应用,我们不会深入这些误差函数背后的数学。如果你没有任何微积分背景,不要担心。只要确保你明白在解决计算机视觉问题时应该使用什么样的误差函数。

误差函数大致分为以下三类:

  • 当我们希望训练模型来预测连续值结果(如股票价格和住房价格)时,会使用回归损失函数。

  • 当我们希望训练模型来预测最多两个类别(例如猫对狗或癌症对非癌症)时,会使用二元分类损失函数。

  • 当我们的模型需要预测两个以上的类别时,例如对象检测,使用多类别分类损失函数。

以下部分概述了不同的误差函数、它们的用法以及它们兼容的激活函数类型。使用本节作为指南,为您的特定建模工作确定适当的误差函数。

回归损失函数

误差函数名称:均方误差(MSE)损失。

简述:这是回归问题的默认误差函数。如果目标变量的分布是正态或高斯分布,这是首选的损失函数。

用在什么地方:目标变量的分布为正态分布时。

适用的激活功能 : model.add(Dense(1, activation="linear"))

TensorFlow 示例 : model.compile(loss='mean_squared_error') or model.compile(loss='mse')

误差函数名称:均方对数误差(MSLE)损失。

简述:该函数首先计算预测值的对数,并计算均方误差。

在哪里使用:当目标变量有一系列值时,以及当预测一个大值时,您可能不希望像均方差那样严重地惩罚一个模型。这通常在模型预测未缩放值时使用。

适用的激活功能 : model.add(Dense(1, activation="linear"))

TensorFlow 示例 : model.compile(loss='mean_squared_logarithmic_error')

误差函数名称:平均绝对误差损失。

简要说明:这是预期值和预测值之间的绝对差值的平均值。

用在什么地方:当目标变量是正态分布且有一些异常值时。

适用的激活功能 : model.add(Dense(1, activation="linear"))

TensorFlow 示例 : model.compile(loss='mean_absolute_error')

二元分类损失函数

误差函数名:二元交叉熵。

简述:这是二分类问题的默认损失函数,优先于其他函数。交叉熵计算一个分数,该分数总结了预测类别 1 的实际概率分布和预测概率分布之间的平均差异。分数被最小化,并且完美的交叉熵值被设置为 0。

哪里用:目标值在(0,1)范围内时。

适用的激活功能 : model.add(Dense(1, activation="sigmoid"))

TensorFlow 示例 : model.compile(loss='binary_crossentropy', metrics=['accuracy'])

错误功能名称:铰链丢失。

简述:主要用于支持基于向量机的二值分类。

哪里用:目标变量在范围(-1,1)内时。

适用的激活功能 : model.add(Dense(1, activation="tanh"))

TensorFlow 示例 : model.compile(loss='hinge', metrics=['accuracy'])

误差函数名:平方铰链损耗。

简述:该函数计算乐谱铰链损耗的平方。它平滑了误差函数的表面,使其在数值上更容易处理。

哪里用:目标变量在范围(-1,1)内时。

适用的激活功能 : model.add(Dense(1, activation="tanh"))

TensorFlow 示例 : model.compile(loss='squared_hinge', metrics=['accuracy'])

多类分类损失函数

错误函数名:多类交叉熵损失。

简述:这是多类分类问题的默认损失函数,优于其他函数。交叉熵计算一个分数,该分数总结了预测类别 1 的实际概率分布和预测概率分布之间的平均差异。分数被最小化,并且完美的交叉熵值被设置为 0。

哪里用:目标值在集合{0,1,3,4,...,n},其中每个类被分配一个唯一的整数值。

适用的激活功能 : model.add(Dense(4, activation="softmax"))

TensorFlow 示例 : model.compile(loss='categorical_crossentropy', metrics=['accuracy'])

错误函数名:稀疏多类交叉熵损失。

简述:稀疏交叉熵执行相同的误差交叉熵计算,而不要求目标变量在训练之前是热编码的。

用在什么地方:当你在目标中有大量的类的时候,比如预测字典单词。

适用的激活功能 : model.add(Dense(100, activation="softmax"))

TensorFlow 示例 : model.compile(loss='sparse_categorical_crossentropy', metrics=['accuracy'])

误差函数名:库尔贝克-莱布勒散度(KLD)损失。

简要描述 : KLD 测量一个概率分布与基线分布的差异。KL 散度损失为 0 意味着分布是相同的。如果使用预测的概率分布来近似期望的目标概率分布,则它确定丢失了多少信息(以比特为单位)。

在哪里使用:这是用来解决复杂的问题,比如学习密集特征的自动编码器。如果这用于多类分类,它作为多类交叉熵工作。

适用的激活功能 : model.add(Dense(100, activation="softmax"))

TensorFlow 示例 : model.compile(loss='kullback_leibler_divergence', metrics=['accuracy'])

优化算法

神经网络的学习目标是确定损失最小的最佳权重(和偏差)。当网络开始学习时,它为每个输入连接分配权重。最初,这些权重很少被优化。通过测量损失(或误差)来确定权重偏离优化的程度。为了确定理想的权重,学习算法优化损失函数,以便找到使损失函数具有最小值的权重。权重(和偏差)被更新,并且该过程被重复,直到不再有优化的余地。优化损失函数的数学函数被称为优化算法优化器

有几种优化算法可以提供不同程度的准确性、速度和并行性。我们将在这一部分探讨一些最受欢迎的。我们将提供介绍性的信息,而不深入这些算法中使用的数学。您将很好地了解在哪里使用哪些优化算法。

梯度下降

梯度下降是一种优化算法,在损失函数(也称为成本函数)为零或最小的地方寻找权重。梯度下降是一种寻找最小成本函数的技术。它是这样工作的:

  1. 成本函数或误差函数由以下等式表示:

$$ f(w)=\frac{1}{N}\sum \left({y}_i-{w}_i{x}_i\right) $$

(方程式 5-11)

其中yI为实际/已知值wI为第 I 个样本的特征向量I对应的权重。wIxI是实际值减去预测值I来计算误差或损失。****

****从微积分中我们知道,函数在一点的一阶导数给出了函数在该点的斜率或梯度。如果绘制成本函数 f(w),会看到一条多维曲线(如图 5-14 )。计算导数以获得梯度,从而确定沿着曲线向哪个方向移动以获得新的权重集。因为目标是最小化成本,所以算法向负梯度的方向移动。

img/493065_1_En_5_Fig14_HTML.jpg

图 5-14

向最小值梯度移动的成本函数

例如,假设只有一个特征,因此我们只需要计算一个权重(w)。成本函数将如图 5-14 中的左图所示。

该算法首先计算初始重量的成本或损失,假设该损失为 f(w),并假设该损失在图 5-14 (左图)中的点 1 处计算。

  1. 然后,该算法计算梯度(增量)并沿曲线向下移动;方向由负梯度决定。

  2. 当它下降时,算法使用以下公式计算新的权重:

$$ \boldsymbol{weight}=\boldsymbol{weight}+\boldsymbol{alpha}\ast \left(\hbox{-} \boldsymbol{delta}\right)=\boldsymbol{weight}\hbox{-} \boldsymbol{alpha}\ast \boldsymbol{delta} $$

(方程式 5-12)

这里,α被称为学习率。学习率决定了梯度沿曲线下降到达最小点的步长。

  1. 使用新的权重值再次计算误差,并且重复该过程,直到算法找到最终的最小成本。
局部和全局最小值

为了简单起见,我们只考虑一个特征,因此只有一个权重。但是在实践中,可能有数十甚至数百个特征需要学习权重。图 5-14 右侧的图像显示了需要优化多个砝码时的误差曲线。在这种情况下,曲线可能有多个出现最小值的点,称为局部最小值。梯度下降算法的目标是找到全局最小值以优化权重。

学习率

如等式 5-12 所示,参数alpha称为学习率。学习率决定了梯度下降算法沿着曲线向下移动以找到全局最小值的步长的大小。

这个学习率的值应该是多少?学习率的大值可能错过最小值点,并且可能来回振荡,并且永远不会找到最小值。另一方面,学习率的小值将需要许多步骤来达到最小点。

学习率小,会让学习变慢。图 5-15 显示了大学习率和小学习率的影响。

img/493065_1_En_5_Fig15_HTML.jpg

图 5-15

大小学习率的影响

所以一定要适当设定学习率。学习率的实用范围是 0.01 到 0.1。我们通常从这个范围内的学习率开始,并根据需要进行调整。

正规化

如果其中一个特征的权重比所有其他特征都高,会发生什么?该特征将具有更高的权重,并且将在整体预测中具有显著的影响。正则化是一种控制一个或几个大权重的效果的方法。我们在成本函数中添加另一个参数,称为正则化,以平衡可能导致我们的预测受到严重影响的过多权重。正则化参数惩罚大的权重以减少其影响。

现在让我们保持简单。当我们编写一些代码来训练我们自己的模型时,我会解释正则化。

随机梯度下降

梯度下降在每一步和每一次迭代中计算整个训练样本的梯度。这是大量的计算,并且它们需要时间来收敛。根据训练集的大小,在单台机器上运行该算法在计算上可能是不可行的,因为它必须将全部数据存储在内存(RAM)中。此外,对于并行计算,处理不能是分布式的。随机梯度下降 (SGD)克服了这些问题。

SGD 计算训练集的一个小子集的梯度,它可以很容易地适应内存。

SGD 是这样工作的:

  1. 随机化输入数据集以消除任何偏差。

  2. 计算随机选择的单个数据或小批量数据的梯度。

  3. 使用公式weight = weight-alpha * delta更新权重。

通常,SGD 中的权重更新是针对几个训练示例而不是单个示例计算的,因为这减少了导致稳定收敛的权重的方差。128 或 256 的小批量是一个很好的起点。对于不同的应用、架构和计算机硬件容量,最佳批量可能会有所不同。

面向分布式和并行计算的 SGD

如果您有一个大的训练数据集,您可以将随机化的训练集分成小的小批。这些小批量可以分布在集群架构中的多台计算机上。SGD 可以在拥有少量数据的单个计算机上独立并行地计算权重。可以将来自各个计算机的结果合并到中央计算机,以获得最终的优化重量。

SGD 还可以通过在具有多个 CPU 或 GPU 的单台计算机中使用并行处理来优化权重。

使用 SGD 算法计算优化权重的分布式和并行操作有助于更快地收敛。

带动量的 SGD

如果你绘制你的成本函数,你会看到峡谷形的曲线,有陡峭的壁和狭窄的底部,你应该考虑使用 SGD 的动量。峡谷在局部极小点周围更为突出。在这种情况下,SGD 在最小值附近波动,可能达不到目标。标准 SGD 通常会延迟转换,尤其是在几次迭代之后。参见图 5-16 。

img/493065_1_En_5_Fig16_HTML.png

图 5-16

带动量的 SGD

动量是一种通过控制梯度运动来控制振荡的方法。动量更新由下面的等式给出:

$$ v= yv+\mathrm{alpha}\ast \mathrm{delta} $$

(等式 5-13)

其中,δ是使用 SGD 计算的梯度,α是学习率。

𝒗是与参数(或权重)具有相同维数的速度向量。

𝞬的值在(0,1)范围内,通常默认取 0.9。

最后,使用下面的等式更新权重:

$$ \boldsymbol{weight}=\boldsymbol{weight}+\boldsymbol{v} $$

自适应梯度算法

梯度下降和 SGD 要求我们手动设置和调整学习率。如果学习太高,算法会错过最小点,如果太低,算法会花很多时间收敛。找到一个完美的学习率是一个手工的过程。当神经网络具有多维性时,选择正确的学习速率尤其困难。一种选择是为每个维度设置不同的学习率。然而,大多数神经网络都有数百甚至数千个维度,这使得人工选择学习速率几乎是不可能的。

Adagrad 通过查看过去来计算每个参数的正确学习率,从而解决了这个问题。它对不常见的特征产生较大的学习率,而对较高频率的特征产生较低的学习率。这意味着每个参数都有自己的学习率,可以提高稀疏梯度问题的性能。

Adagrad 非常适合处理稀疏数据,例如在计算机视觉或 NLP 中。

Adagrad 的最大缺点之一是自适应学习率随着时间的推移会变得非常小。

RMSProp

还记得有气势的 SGD 吗?动量的引入控制了较陡曲线中的梯度运动。RMSProp 为 SGD 提供了动量增强。它限制渐变在垂直方向上的移动。这样想:如果你有一个陡峭的曲线,水平方向的小运动会引起垂直方向的大运动。RMSProp 控制垂直移动,以便垂直和水平方向上的移动不会不均匀,并导致更快地找到最小点。

适应力矩(Adam)

Adam 优化算法专为深度学习而设计,是首选的优化器。它将 SGD 与 momentum 和 RMSProp 结合在一起。Adam 根据训练数据迭代更新网络权重。

Adam 没有像在 RMSProp 中那样基于平均一阶矩(平均值)来调整参数学习率,而是利用梯度的二阶矩的平均值。

亚当背后的数学问题超出了本书的范围(再次强调,要专注于本书的核心主题)。有关如何计算和更新梯度的更多详细信息,请参见位于 https://arxiv.org/pdf/1412.6980.pdf 的原始论文。

该文件描述了自动协调机制的以下好处:

  • 易于实施

  • 计算效率高

  • 几乎没有内存需求

  • 对梯度的对角重标度不变

  • 非常适合在数据和/或参数方面很大的问题

  • 适用于非稳定目标

  • 适用于有噪音和/或稀疏梯度的问题

  • 超参数具有直观的解释,通常几乎不需要调整

反向传播

为了训练一个神经网络,我们需要以下三样东西:

  • 输入数据或输入要素

  • 一种前馈多层神经网络

  • 误差函数

网络为每个输入特征分配初始权重。使用优化算法,例如 SGD 或 Adam,误差函数被优化以计算最小误差,并且权重被更新。

多层感知器至少包含三层:输入层、隐藏层和输出层。可以有多个隐藏层。

在前向网络中,神经元的输出是正向计算的,从第一个隐藏层开始,然后是第二个隐藏层,依此类推,最后是输出层。

下一步是估计误差,以便更新权重。在反向传播方法中,首先在最后一层计算权重的梯度,最后计算第一层的梯度。来自一个层的梯度的部分计算被重新用于前一层的梯度的计算。误差信息的这种反向流动允许有效计算每一层的梯度。换句话说,梯度计算不是在每一层独立完成的。

为什么首先计算最后一层的误差?原因很简单,隐藏层没有目标变量。输出图层映射到标注数据集的目标变量。因此,首先计算最后一层的误差非常有意义。

本节概述了神经网络的工作原理以及不同的算法在幕后的工作原理。我们还发现有几个参数,如学习率和动量,我们可以控制来调整我们的训练。我们可以设置或调整来训练一个好模型的参数被称为超参数。我们将在本章后面了解更多关于超参数的知识。

在下面几节中,我们将编写代码来实现本章前面几节中涉及的一些概念。我们将编写 Python 代码并使用 TensorFlow 来完成示例。我们将从 TensorFlow 的高级介绍开始,涵盖与计算机视觉相关的特性和功能。我们将在本章的剩余部分使用 TensorFlow 代码,我们将在实现神经网络概念时提供相关的解释。

TensorFlow 简介

TensorFlow 是一个面向端到端机器学习的开源平台。它提供了一个高级且易于使用的 API 来创建机器学习模型。TensorFlow 是 Keras 的执行引擎,Keras 是用 Python 编写的高级神经网络 API。

在写这本书的时候,TensorFlow 版本 2 (TF2)是可用的。但本书中涉及的一些核心概念(如对象检测)仅适用于 TensorFlow 版本 1 (TF1)。在大多数情况下,我们将使用 TF2 和 TF1 主要用于对象检测。

tensorflow 安装

如果您遵循了第一章中的说明,TensorFlow 和 Keras 应该已经安装在您的工作环境中。如果没有,检查第一章,并遵循 TensorFlow 的安装说明。

如何使用 TensorFlow

要在代码中使用 TensorFlow,必须按如下方式导入它:

import tensorflow as tf

您可以通过以下方式访问 Keras API:

tf.keras

在我们深入研究神经网络之前,让我们理解 TensorFlow 的一些术语。

张量

张量是包含基本数据类型的 n 维数组的数据结构。

  • 如果 n 的值为 0,则称为标量,标量的秩为 0 或 0 维。

  • 如果 n 的值为 1,则称为向量,向量的秩为 1 或 1 维。

  • 如果 n 的值为 2,则称之为矩阵,矩阵的秩为 2 或 2 维。

  • 如果 n 的值为 3 或更大,则称为张量。取决于 n 的值,它的秩是 3 或更多。

因此,张量是向量和矩阵在高维空间的推广。表 5-1 总结了标量、矢量、矩阵和张量之间的区别。

表 5-1

标量、向量、矩阵和张量的定义

|

数据结构

|

维度或等级

( n 的值)

|

例子

|
| --- | --- | --- |
| 数量 | Zero | scalar_s = 231 |
| 矢量 | one | vector_v = [1,2,3,4,5] |
| [数]矩阵 | Two | matrix_m = [[1,2,3],[4,5,6],[7,8,9]] |
| 张量 | 3 或更多 | tensor_3d = [``[[1,2,3],    [4,5,6],    [7,8,9]],``[[11,12,13], [14,15,16], [17,18,19]],``[[21,22,23], [24,25,26], [27,28,29]],``] |

在内部,TensorFlow 定义、操作和计算张量。它提供了一个Tensor类,可通过以下方式访问:

tf.Tensor

Tensor类具有以下属性:

  • 数据类型,例如uint8int32float32string。张量的每个元素必须是相同的数据类型。

  • 一个形状,它是维度的数量和每个维度的大小。

可变的

TensorFlow 有一个名为Variable的类,可以通过使用tf.Variable来访问。tf.Variable类表示一个张量,它的值由读取和修改等操作来操作。在本章的后面,你将了解到tf.keras使用tf.Variable来存储模型参数。清单 5-1 展示了如何使用变量的 python 示例。

常数

TensorFlow 还支持常量,常量的值一旦初始化就不能更改。要创建常数,请调用此函数:

tf.constant(value, dtype=None, shape=None, name="Const")

在哪里

  • value是实际值或设置为常量的列表。

  • dtype是由常数表示的结果张量的数据类型。

  • shape是可选参数,表示结果张量的维数。

  • name是张量的名称。

如果不指定数据类型,tf.constant()将从常量的值中推断出它。

函数tf.constant()返回一个常量张量。

清单 5-1 展示了一个创建tensor变量的简单代码示例。

Filename: Listing_5_1.py
1    import tensorflow as tf
2
3    # create a tensor variable with zero filled with default datatype float32
4    a_tensor = tf.Variable(tf.zeros([2,2,2]))
5
6    # Create a 0-D array or scalar variable with data type tf.int32
7    a_scalar = tf.Variable(200, tf.int32)
8
9    # Create a 1-D array or vector with data type tf.int32
10   an_initialized_vector = tf.Variable([1, 3, 5, 7, 9, 11], tf.int32)
11
12   # Create a 2-D array or matrix with default data type which is tf.float32
13   an_initialized_matrix = tf.Variable([ [2, 4], [5, 25] ])
14
15   # Get the tensor's rank and shape
16   rank = tf.rank(a_tensor)
17   shape = tf.shape(a_tensor)
18
19   # Create a constant initialized with a fixed value.
20   a_constant_tensor = tf.constant(123.100)
21   print(a_constant_tensor)
22   tf.print(a_constant_tensor)

Listing 5-1Creating a Tensor Variable

清单 5-1 的第 1 行导入了 TensorFlow 包。第 4 行创建了一个用零填充的形状为[2,2,2]的张量。默认情况下,它创建一个数据类型为tf.float32的张量(如果在创建张量时没有指定数据类型,它将默认为float32)。但是,数据类型是从初始值推断出来的。

第 7 行创建了一个类型为int32的标量数据,第 10 行创建了一个数据类型为int32的向量,第 13 行创建了一个默认数据类型为float32的 2×2 矩阵。

第 16 行显示了如何获得张量的秩(见表 1-1),第 17 行显示了如何获得形状。

第 20 行创建了一个常量张量,其值初始化为 123.100。它的数据类型由它的初始化值来解释。

第 20 行和第 21 行显示了打印张量的两种不同方式。执行代码,注意两个print语句的区别。

要计算张量,使用Tensor.eval()方法,该方法创建一个等价的 NumPy 数组,其形状与张量相同。请注意,仅当默认tf.Session处于活动状态时,张量才会被评估。

这本书不是关于 TensorFlow 的。我们将只涵盖与编写构建计算机视觉和深度学习模型的代码相关的功能。您应该访问 TensorFlow 官方网站,并学习使用 TensorFlow 的 Python 函数。下面是 API 规范: https://www.tensorflow.org/api_docs/python/tf

我们将在接下来的几乎所有章节中重新讨论 TensorFlow。

我们第一个具有深度学习的计算机视觉模型:手写数字的分类

我们现在准备建立和训练我们的第一个计算机视觉模型。我们将从著名的“Hello World”类型的深度学习模型开始,并学习如何构建一个简单的多层感知器分类器。当你完成这一部分的时候,你将会有一个真正工作的计算机视觉模型。和以前一样,我们将逐行解释我们将编写的 TensorFlow 代码。在我们开始编写我们的第一个模型之前,让我们了解我们要构建什么以及步骤是什么。

我们的目标是使用人工神经网络训练一个模型来分类手写数字(0 到 9)的图像。

我们将建立一个神经网络来执行监督学习。对于任何监督学习,我们都需要一个包含标记数据的数据集。换句话说,我们需要已经标记了它们所代表的数字的图像。例如,如果图像包含手写数字 5,它将被标记为 5。同样,我们要在培训中使用的所有图像都必须标有相应的标签。

我们的数据集有十个类,每个类对应一个数字。类索引从 0 开始。因此,我们的类在范围(0,9)内。

标记的影像数据集通常以 70:30 的比例分为两部分。

  • 训练集:70%的标记图像用于实际训练。为了获得好的结果,我们应该确保训练数据是平衡的,这意味着它几乎平等地代表了所有的类。

    如果你的训练集没有一个平衡的类呢?多数阶级会对模型产生更大的影响,而你的少数阶级可能永远不会或很少被预测到。

    为了平衡你的类,你可以做过采样或欠采样。在过采样中,您应该添加更多少数类的图像,并使它们接近等于多数类。在欠采样中,从多数类中移除图像,使其在数量上接近少数类。

    还有其他的合成方法来平衡你的类,但是不推荐用于计算机视觉。合成少数过采样技术(SMOTE)就是这样一种方法,但不推荐用于计算机视觉。然而,在 https://arxiv.org/pdf/1710.05381.pdf 发表的研究论文得出结论,欠采样的性能与过采样相当,因此在计算效率方面应该是首选。

  • 测试集:标记数据的 30%作为测试集。来自测试集的图像通过训练的模型,预测的结果与标签进行比较,以评估模型的准确性。

    确保测试集不具有也存在于训练集中的相同图像是很重要的。同样,测试集以相等的比例包含所有的类也是很重要的。

我们将执行以下任务来构建模型:

  1. https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz 下载包含手写数字及其标签的图像数据集。

  2. 配置一个有四层的多层感知器分类器:输入层、两个隐藏层和输出层。

  3. 用训练集拟合 MLP 模型。拟合模型意味着训练模型。

  4. 使用测试集评估已训练的模型。

  5. 使用不同数据集上的模型进行预测(不用于定型集或测试集中)并显示结果。

最后,我们到达了这样一个点,我们逐行查看 TensorFlow 代码,以了解如何训练基于深度学习的模型,用于对手写数字进行分类的计算机视觉。

让我们来探索清单 5-2 ,它演示了如何训练一个基于深度学习的计算机视觉模型。

Filename: Listing_5_2.py
1    import tensorflow as tf
2    import matplotlib.pyplot as plt
3    # Load MNIST data using built-in datasets download function
4    mnist = tf.keras.datasets.mnist
5    (x_train, y_train), (x_test, y_test) = mnist.load_data()
6
7    #Normalize the pixel values by dividing each pixel by 255
8    x_train, x_test = x_train / 255.0, x_test / 255.0
9
10   # Build the 4-layer neural network (MLP)
11   model = tf.keras.models.Sequential([
12    tf.keras.layers.Flatten(input_shape=(28, 28)),
13    tf.keras.layers.Dense(128, activation='relu'),
14    tf.keras.layers.Dense(60, activation='relu'),
15    tf.keras.layers.Dense(10, activation='softmax')
16   ])
17
18   # Compile the model and set optimizer,loss function and metrics
19   model.compile(optimizer='adam',
20                loss='sparse_categorical_crossentropy',
21                metrics=['accuracy'])
22
23   # Finally, train or fit the model
24   trained_model = model.fit(x_train, y_train, validation_split=0.3, epochs=100)
25
26   # Visualize loss  and accuracy history
27   plt.plot(trained_model.history['loss'], 'r--')
28   plt.plot(trained_model.history['accuracy'], 'b-')
29   plt.legend(['Training Loss', 'Training Accuracy'])
30   plt.xlabel('Epoch')
31   plt.ylabel('Percent')
32   plt.show();
33
34   # Evaluate the result using the test set.\
35   evalResult = model.evaluate(x_test,  y_test, verbose=1)
36   print("Evaluation", evalResult)
37   predicted = model.predict(x_test)
38   print("Predicted", predicted)

Listing 5-2Four-Layer MLP for Classification of Images with Handwritten Digits

第 1 行导入 TensorFlow 包。这个包提供了对 Keras 深度学习库和其他几个与深度学习相关的功能的访问。第 2 行导入 matplotlib。

第 4 行初始化keras.datasets.mnist模块。该模块提供了一个内置功能来下载修改后的国家标准与技术研究所(MNIST)手写数字图像数据。MNIST 数据库是大量手写数字的集合,广泛用于训练各种计算机视觉系统。数据库在 http://yann.lecun.com/exdb/mnist/ 可用。

第 5 行下载 MNIST 数据集。mnist模块中的load_data()函数下载数字数据库并返回 NumPy 数组的元组。默认情况下,它会将数据库下载到您的主目录位置~/.keras/datasets,默认文件名为mnist.npz。您可以通过提供绝对文件路径下载到任何其他位置,例如在函数load_data(path='/absolute/path/mnist.npz')中。确保该目录已经存在。

load_data()函数返回一组 NumPy 数组,如下所示:

  • 这个 NumPy 数组包含我们将用于训练的图像的像素值。

  • y_train:这个 NumPy 数组包含了x_train中每个图像的标签。

  • x_testy_test:这些是测试数据集的图像和相应标签的像素值。

在第 8 行,我们知道图像的像素值范围从 0 到 255。我们需要标准化像素值,使它们在 0 和 1 之间。将每个像素除以 255 将使其正常化,如第 8 行所示。x_trainx_test NumPy 数组被标量 255 除,以归一化这些数组。

在本例中,我们使用 TensorFlow 中的内置函数下载公开可用的数据集。如果您的本地磁盘或任何分布式文件系统中有数据,TensorFlow 会提供加载数据的函数。我们将在本章的后面演示如何从本地文件系统加载文件。

在第 11 行到第 16 行,虽然这是一条语句,但为了清楚起见被分成了多行,这是我们定义神经网络的地方。让我们看看它的不同部分。

  • 这是一个 TensorFlow 类,它提供了创建神经网络层的功能。在这个例子中,我们创建了四个层,并作为数组传递给Sequential类的构造函数。

  • tf.keras.layers:这个模块提供 API 来创建不同类型的神经网络层。在本例中:

    • tf.keras.layers.Flatten(input_shape=(28, 28))通过初始化Flatten()功能定义输入层。我们的输入图像是单通道的 28×28 像素。此函数的参数是输入形状。这个 flatten 函数将在输入层创建 28×28 = 784 个神经元。请记住,输入层中神经元的数量与特征的数量相同(如果使用了偏差,则加 1)。我们的数字图像为 28×28 像素,每个像素值作为一个输入特征;因此,这一层的节点数是 784。在本章的后面,我们将会看到更多具有复杂特征的例子。现在让我们把事情简单化。

    • tf.keras.layers.Dense在神经网络中创建密集层。密集层有两个重要的参数:神经元的数量和激活函数。请注意,在清单 5-2 中,我们的神经网络有三个密集层。

      • 隐层 1 :神经元个数为 128,激活函数为relu

      • 隐层 2 :神经元个数为 60,激活函数为relu

      • 输出层(最后一层):神经元个数为 10,激活函数为 softmax。

为什么激活功能在隐藏层relu?回想一下“激活功能”部分和图 5-10 中的内容,即relu总是产生从 0 到无穷大范围内的输出,并且不产生任何负数。归一化后的像素值在范围(0,1)内。因此,RELU 非常适合这一层。

softmax 为什么在输出层?记住,softmax 生成神经元输出的概率分布。输出层生成每个类别的概率。在本例中,对于每个输入图像,它将生成 10 个概率,每个类别一个。这些概率的总和将等于 1。具有最高概率的类别通常被作为输入图像的预测类别。

为什么我们在输出层只有十个神经元?是因为我们只有十位数要预测,分类问题的输出层要有和要预测的类别数一样多的神经元。

第 19 行到第 21 行调用compile()函数,用我们之前提供的配置构建神经网络。函数compile()采用以下内容:

  • optimizer = ‘adam':试图寻找损失函数最小值的优化函数的名称。

  • loss = ‘sparse_categorical_crossentropy':将被优化的损失函数。这是一个多类分类,我们选择的是sparse_categorical_crossentropy损失函数。

  • metrics= [‘accuracy']:在训练和测试过程中,模型要评估的指标列表。因为我们有一个单一的输出模型,并且这是一个分类问题,所以我们在这个列表中只传递一个度量,即“准确性”。

第 24 行实际上符合这个模型。当这一行执行时,模型开始学习。这需要这些参数:

  • x_train:像素的归一化值的 NumPy 表示

  • y_train:标签数量

  • validation_split = 0.3,告知算法保留 30%的训练数据用于验证

  • epochs = 100,训练迭代次数

如果您想要使用您的测试数据集,或者您可以访问的任何其他数据集来进行验证,而不是使用validation_split,那么您可以使用validation_data=(x_test, y_test)

问题是,我们应该使用多少迭代或历元来训练我们的模型?通常,神经网络需要多次迭代来学习。这是您需要调整的参数之一。当您的模型开始学习时,您将看到控制台中打印的输出(例如,如果您在 PyCharm 中执行代码,则是 PyCharm 控制台)。它显示了每个历元的损失和精度。随着每个时代的到来,损耗应该下降,而精度应该上升。如果您开始注意到损失不再减少或精度不再增加,您应该将您的历元值设置在该水平。

图 5-17 显示了具有 100 个时期的样本训练输出。

img/493065_1_En_5_Fig17_HTML.jpg

图 5-17

每个历元的损失和精度的控制台输出示例

在第 27 行到第 32 行,我们想要绘制损失对时期和准确度对时期的图表,以了解我们的训练有多好。我们训练过的模型维护了每个时期的损失和准确度的历史,可通过使用history[‘loss']history[‘accuracy']来访问。

在图 5-18 中,您会注意到损耗(红线所示)随着每个时期而减少,大约在第十个时期开始变得平缓。最有可能的是,更多的迭代不会进一步减少损失。因此,将 epoch 设置为大约 10,这样可以避免更多的计算。

img/493065_1_En_5_Fig18_HTML.jpg

图 5-18

训练损失和精度与历元的关系图

类似地,精度水平增加,并在几个时期后变得平坦。这两者——损失和准确性——将帮助您确定训练神经网络的迭代次数。

您可以通过调用history.keys()来打印History对象中的所有键。您可能还想绘制val_accval_loss图,看看您的模型如何评估 30%的验证数据。

第 35 行根据测试数据集评估模型。我们使用接受这些参数的evaluate()函数:

  • 包含所有测试图像的归一化像素值的 NumPy

  • 包含测试数据集标签的数量

  • verbose =1作为可选参数打印输出

正如您从示例输出中看到的,在图 5-19 中,我们的模型在测试数据集上的准确度是 0.9787 或 97.87%,这被认为是一个相当好的模型。

img/493065_1_En_5_Fig19_HTML.png

图 5-19

评估输出

图 5-19 显示了evaluate()功能的样本输出。我们的模型评估的总体准确率为 97.87%,损失为 0.2757%。

如果您有一个测试数据集,就像我们在这个例子中拥有的一样,您不需要像第 24 行那样保留 30%的训练集。如果您想像我们在第 35 行中那样使用测试数据执行评估,那么参数validation_split = 0.3是可选的。

在第 37 行,到目前为止,我们构建、训练和评估了神经网络。第 37 行使用训练的模型来预测在模型训练中没有使用的输入图像的类别。任何新的图像(具有归一化的像素值)都可以被输入到模型中以预测其类别。

为了预测一个类,我们使用函数model.predict(),它将图像 NumPy 作为一个参数。

predict()函数的输出是一组数组。这个数组的元素是每个类的概率。最大概率的指标是该图像的预测类别。

例如,带有手写数字的输入图像得到预测概率,如图 5-20 所示。从零开始,第六个指数(用黄色突出显示)的概率最大,为 0.99844。因此,如图所示,输入图像的预测类别是 7,这与手写数字相匹配。

img/493065_1_En_5_Fig20_HTML.png

图 5-20

输入图像和预测概率

恭喜你!你为计算机视觉建立并训练了你的第一个神经网络。在以下部分中,我们将学习如何评估我们的模型是好是坏,以及如何调整参数,使我们的模型在更低的损耗和更高的精度方面更好。

模型评估

在我们训练一个模型之后,我们通过分析损失和准确性来对它进行评估。这种损失和准确性是基于训练数据计算的。即使精确度很高,损失很小,我们也不能确定当一组新的数据输入模型时,模型会以同样的精确度进行预测。通过输入测试数据来分析模型的性能是很重要的,这些数据必须不同于训练集。下面是几种常用的评估方法,都在实践中。

过度拟合

过度拟合模型可以很好地学习训练数据,因此它在训练数据中表现良好,但在评估和测试数据中表现不佳。例如,如果具有训练数据的模型的准确性很高(比如 97%),但是具有测试集或验证集的模型的准确性较低(比如 70%),则该模型被称为过度拟合。图 5-21 描述了测试精度低于训练精度的过拟合情况。

img/493065_1_En_5_Fig21_HTML.jpg

图 5-21

过度拟合的示例

如何避免过度拟合?

有几种方法可以控制或避免过度拟合。

  • 正则化:我们已经了解了什么是正则化,以及正则化如何影响模型。

  • 辍学:辍学也是一种正规化技术。使用 dropout 时,神经元会被随机丢弃,这意味着被丢弃的神经元的输出不会作为下一层的输入。辍学是暂时的,仅适用于特定的通行证。这意味着权重更新不会应用于该特定过程中临时移除的神经元。

    在 TensorFlow 中,通过添加一个称为辍学层的层,并指定辍学比率或概率(例如,20%)来实现辍学。可以在输入图层或隐藏图层中添加下降图层。出于大多数实际目的,我们保持这个丢失概率很小,以避免丢失重要的特性。

    在清单 5-2 中,我们可以添加一个脱落层,如清单 5-3 所示。

....
model = tf.keras.models.Sequential([
 tf.keras.layers.Flatten(input_shape=(28, 28)),
 tf.keras.layers.Dense(128, activation='relu'),
 tf.keras.layers.Dropout(0.2),
 tf.keras.layers.Dense(60, activation='relu'),
 tf.keras.layers.Dense(10, activation='softmax')
])
.....

Listing 5-3Code Fragment to Show the Dropout Layer

欠拟合

当一个模型不能从训练数据中捕捉到潜在的趋势时,它就被认为是不适合的。欠拟合模型仅仅意味着模型不能很好地拟合数据。当我们有一个小的数据集,或者数据集不是我们试图建模的实际场景的真实表示时,通常会发生这种情况。欠拟合模型的准确性对训练集和测试集都不好。应该避免这种模式。避免拟合不足的一个好方法是向训练集添加更多数据,或者拥有足够的数据,这些数据包含您试图建模的所有变化和趋势。此外,选择正确特征的特征工程有助于减少欠拟合。

评估指标

还有其他重要的度量标准,你应该看看,以评估你的模型质量。这里对它们进行了描述。通过将预测结果与标签值进行比较,从测试数据集中计算出这些指标。

|   |

猫(预测)

|

狗(预测)

|
| --- | --- | --- |
| cat(实际) | Eighty | Ten |
| 狗(实际) | eight | Ninety-two |

  • 真阳性率(TPR) 或灵敏度:如果预测值与标签值匹配,则称为真阳性 (TP)。TPR 定义如下:

    TPR =所有 TPs 的总数/所有阳性病例的总数

  • 真阴性率【TNR】**或特异性:TNR 定义如下:

    TNR =真阴性总数/阴性病例总数

  • 【FPR】**误报率:FPR 定义如下:

    FPR =假阳性病例总数/阴性病例总数

  • 假阴性率 或漏检率:假阴性率定义如下:

    FNR =假阴性病例总数/阳性病例总数

  • 混淆矩阵:混淆矩阵也叫误差矩阵。它以网格的形式显示了每个类的积极和消极的数量。例如,如果您有两个类,dogcat,混淆矩阵可能如下所示:

在这个例子中,cat类有 80 个真阳性、10 个假阳性和 8 个假阴性。类似地,对于dog类,有 92 个真阳性、8 个假阳性和 10 个假阴性。

清单 5-4 显示了计算混淆矩阵并以数组形式显示的代码示例。

.....

40   confusion = tf.math.confusion_matrix(y_test, np.argmax(predicted, axis=1), num_classes=10)
41   tf.print(confusion)
.....

Listing 5-4Confusion Matrix Calculation

清单 5-4 是清单 5-2 的延伸。清单 5-2 的第 37 行使用测试数据集从模型中进行预测。输出是每个输入的概率的 NumPy 数组。np.argmax(predicted, axis=1)获取数组中最大概率的索引。索引代表预测的类别。

在清单 5-4 中,tf.math.confusio_matrix()计算混淆矩阵。它采用这些参数:

  • x_test:测试数据集的图像特征的数量

  • np.argmax(predicted, axis=1):预测类

可选参数num_classes = 10表示我们希望模型预测的类的数量。

confusion_matrix()函数返回一个张量。如果你使用print(confusion)直接打印这个张量,它不会显示这个张量的值。您将需要执行张量,以便它在显示到控制台之前计算所有的值。

清单 5-4 中的第 40 和 41 行显示了如何生成混淆矩阵,并使用tf.print()语句在控制台上打印出来。

图 5-22 显示了我们在本例中使用的测试集的样本混淆矩阵。

img/493065_1_En_5_Fig22_HTML.png

图 5-22

混淆矩阵输出样本

  • 精度:精度定义为真阳性总数与预测阳性总数之比。

    精度=真阳性数/预测阳性数

    =真阳性/(真阳性+假阳性)

    = TP/(TP + FP)

    理想情况下,您的模型不应该有任何假阳性,即 FP = 0。那么,precision = 1,即 100%。换句话说,越精确,模型越好。

  • 召回:召回是真阳性总数与实际阳性总数之比。召回率和真阳性率是一样的。计算召回的公式如下:

    召回数=真阳性总数/阳性总数

    =真阳性总数/(真阳性总数+假阴性总数)

    = TP / (TP +联合国)

    理想情况下,您的模型不应有任何假阴性,即 FN = 0。那么,回忆= 1,或者 100%。所以召回越多,模型越好。

  • F1 得分:从精确度和召回率两方面来看,我们看到这两个指标在理想模式下都应该接近 100%。如果精度和召回率中的一个比另一个小,你会如何判断你的模型?F1 分数有助于做出决定。F1 分数结合了精确度和召回率,以获得有助于判断我们的模型好坏的综合指标。F1 分数是精确度和召回率的调和平均值,使用以下公式计算:

    F1-Score = 2 ×精度×召回/(精度+召回)

  • 准确度:准确度定义如下:

    准确度 = (TP + TN) /总样本数

    = (TP + TN)/ (T + N)

    = (TP + TN)/ (TP + TN + FP + FN)

这些指标帮助我们决定该模型是否适合在生产中部署,或者调整参数并重新训练该模型。

超参数

超参数是我们在学习过程开始之前设置的神经网络模型的参数。这些被认为是外部参数,而不是算法根据训练数据计算的参数。在训练模型时,算法不能推断超参数。这些超参数影响模型的整体性能,包括准确性和训练执行时间。

以下是为计算机视觉训练神经网络时可能需要调整的一些常见超参数:

  • 网络中的隐藏层数

  • 隐藏层中的神经元数量

  • 辍学率和学习率

  • 优化算法

  • 激活功能

  • 损失函数

  • 时期或迭代次数

  • 验证集的分割

  • 批量

  • 动力

张量板

通常,您需要了解机器学习工作流运行时发生了什么。TensorBoard 是一个工具,它将帮助您可视化您的机器学习测量和指标。使用 TensorBoard,您将能够跟踪实验指标,如损失和准确性,可视化模型图,将嵌入投影到低维空间,等等。

TensorBoard 提供了一个 HParams 仪表板,帮助我们确定最佳实验或最有希望的超参数集。我们将采用我们在上一节中得出的同一个神经网络示例,并可视化各种超参数,以了解我们应该如何调整它们。

在您完成以下示例之前,请确保您安装了 TensorBoard。如果您在 virtualenv 命令提示符下,只需运行以下命令来检查 TensorBoard 安装:

(cv) username $: tensorboard --logdir mylogdir

如果一切顺利,您应该会看到类似这样的输出:

TensorBoard 2.1.0 at http://localhost:6006/ (Press CTRL+C to quit)

将您的浏览器指向http://localhost:6066,您应该会看到 TensorBoard web UI。

超参数调谐实验

清单 5-5 中的代码示例演示了一个简单的实验,对于一个简单的神经网络只有三个超参数。出于学习目的,我们保持了示例的简单性。

我们的目标是用以下参数进行实验:

  • 第一个隐藏层中的神经元数量

  • 优化功能

  • 辍学率

实验完成后,我们希望在 TensorBoard web UI 中可视化结果,并使用 HParams 仪表板来分析结果。

清单 5-5 显示了代码流。

1    import tensorflow as tf
2    from tensorboard.plugins.hparams import api as hp
3
4    # Load MNIST data using built-in datasets download function
5    mnist = tf.keras.datasets.mnist
6    (x_train, y_train), (x_test, y_test) = mnist.load_data()
7
8    x_train, x_test = x_train / 255.0, x_test / 255.0
9
10   HP_NUM_UNITS = hp.HParam('num_units', hp.Discrete([16, 32]))
11   HP_DROPOUT = hp.HParam('dropout', hp.RealInterval(0.1, 0.2))
12   HP_OPTIMIZER = hp.HParam('optimizer', hp.Discrete(['adam', 'sgd']))
13
14   METRIC_ACCURACY = 'accuracy'
15
16   with tf.summary.create_file_writer('logs/hparam_tuning').as_default():
17    hp.hparams_config(
18      hparams=[HP_NUM_UNITS, HP_DROPOUT, HP_OPTIMIZER],
19      metrics=[hp.Metric(METRIC_ACCURACY, display_name="Accuracy")],
20    )
21
22
23   def train_test_model(hparams):
24        model = tf.keras.models.Sequential([
25            tf.keras.layers.Flatten(),
26            tf.keras.layers.Dense(hparams[HP_NUM_UNITS], activation=tf.nn.relu),
27            tf.keras.layers.Dropout(hparams[HP_DROPOUT]),
28            tf.keras.layers.Dense(10, activation=tf.nn.softmax),
29        ])
30        model.compile(
31            optimizer=hparams[HP_OPTIMIZER],
32            loss='sparse_categorical_crossentropy',
33            metrics=['accuracy'],
34        )
35
36        model.fit(x_train, y_train, epochs=5)
37        _, accuracy = model.evaluate(x_test, y_test)
38        return accuracy
39   def run(run_dir, hparams):
40    with tf.summary.create_file_writer(run_dir).as_default():
41      hp.hparams(hparams)  # record the values used in this trial
42      accuracy = train_test_model(hparams)
43      tf.summary.scalar(METRIC_ACCURACY, accuracy, step=1)
44
45   session_num = 0
46
47   for num_units in HP_NUM_UNITS.domain.values:
48    for dropout_rate in (HP_DROPOUT.domain.min_value, HP_DROPOUT.domain.max_value):
49      for optimizer in HP_OPTIMIZER.domain.values:
50        hparams = {
51            HP_NUM_UNITS: num_units,
52            HP_DROPOUT: dropout_rate,
53            HP_OPTIMIZER: optimizer,
54        }
55        run_name = "run-%d" % session_num
56        print('--- Starting trial: %s' % run_name)
57        print({h.name: hparams[h] for h in hparams})
58        run('logs/hparam_tuning/' + run_name, hparams)
59        session_num += 1

Listing 5-5Hyperparameter Tuning and Visualization on HParams of TensorBoard

第 5 行到第 8 行加载了我们之前处理过的相同的 MNIST 数字数据。

第 10 行设置了神经元或单元数量的值:16 和 32。

第 11 行设置了辍学率:0.1 和 0.2。

第 12 行设置优化函数:adamsgd

代码结构的其余部分很简单,不需要任何解释。注意,对于三个超参数的每个组合,在嵌套的for循环中调用了model.fit()函数(第 47 行到第 59 行)。度量输出被写入一个日志文件logs/hparam_tuning

实验成功执行后,使用以下命令启动 TensorBoard(确保您处于我们在本书中一直使用的名为cv的虚拟环境中):

(cv)用户名$:tensor board-logdir logs/hparam _ tuning

您可能需要将绝对路径传递给logs/hparam_tuning目录。

启动浏览器,指向http://localhost:6006。您应该会看到 TensorBoard web UI。从右上角的下拉列表中,选择 HPARAMS。您应该会看到类似于图 5-23 中的仪表盘。

img/493065_1_En_5_Fig23_HTML.jpg

图 5-23

显示 h 参数视图的张量板,包含对应于每个超参数组合的精度

从这个仪表板中,您可以看到给出最高准确性的超参数组合:32 个神经元的 96.160%的准确性,0.1 的漏失,以及 adam 优化器。

或者,点击平行坐标视图选项卡启动图 5-24 。

img/493065_1_En_5_Fig24_HTML.jpg

图 5-24

HPARAMS 的平行组合视图

如图 5-24 所示,点击最高精度(或您想要检查的任何精度)的链接,您将看到绿色高亮路径,该路径代表生成精度的超参数组合。

保存和恢复模型

通常情况下,您会希望保存训练好的模型,以便以后可以使用它来分类或预测新图像。毕竟你不想每次想用的时候都训练一个模型。

实际上,模型训练是一个耗时的过程。根据您的数据大小、硬件容量和神经网络配置,训练过程可能需要几个小时或几天。您可能希望在培训期间和培训结束后保存模型。如果培训被中断,您可以从中断的地方重新开始,避免培训被中断前的时间损失。

在本节中,我们将探讨如何训练和保存神经网络,稍后加载它,并在我们的应用中使用它。

培训期间保存模型检查点

清单 5-6 几乎包含了我们在清单 5-2 的第一个模型训练代码中看到的所有代码行。我们将突出显示不同的行,以及它们在保存训练权重的上下文中的意义。

Filename: Listing_5_6.py
1    import tensorflow as tf
2    import matplotlib.pyplot as plt
3    import os
4
5    # The file path where the checkpoint will be saved.
6    checkpoint_path = "cv_checkpoint_dir/mnist_model.ckpt"
7    checkpoint_dir = os.path.dirname(checkpoint_path)
8
9    # Create a callback that saves the model's weights.
10   cp_callback = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_path,
11                                                   save_weights_only=True,
12                                                   verbose=1)
13
14   # Load MNIST data using built-in datasets download function.
15   mnist = tf.keras.datasets.mnist
16   (x_train, y_train), (x_test, y_test) = mnist.load_data()
17
18   # Normalize the pixel values by dividing each pixel by 255.
19   x_train, x_test = x_train / 255.0, x_test / 255.0
20
21   # Build the ANN with 4-layers.
22   model = tf.keras.models.Sequential([
23    tf.keras.layers.Flatten(input_shape=(28, 28)),
24    tf.keras.layers.Dense(128, activation="relu"),
25    tf.keras.layers.Dense(60, activation="relu"),
26    tf.keras.layers.Dense(10, activation="softmax")
27   ])
28
29   # Compile the model and set optimizer,loss function and metrics
30   model.compile(optimizer='adam',
31                loss='sparse_categorical_crossentropy',
32                metrics=['accuracy'])
33
34   # Finally, train or fit the model, pass callbacks to save the model weights.
35   trained_model = model.fit(x_train, y_train, validation_split=0.3, epochs=10, callbacks=[cp_callback])
36
37   # Visualize loss  and accuracy history
38   plt.plot(trained_model.history['loss'], 'r--')
39   plt.plot(trained_model.history['accuracy'], 'b-')
40   plt.legend(['Training Loss', 'Training Accuracy'])
41   plt.xlabel('Epoch')
42   plt.ylabel('Percent')
43   plt.show();
44
45   # Evaluate the result using the test set.
46   evalResult = model.evaluate(x_test,  y_test, verbose=1)
47   print("Evaluation Result: ", evalResult)

Listing 5-6Model Weights Are Saved During the Training

第 3 行导入了提供文件系统相关函数的os包,这些函数用于将模型保存到文件路径。

第 6 行是将存储我们的模型权重的文件名。

第 7 行创建特定于操作系统的文件路径对象。

第 10 行通过传递以下参数初始化名为ModelCheckpoint的 TensorFlow 回调类:

  • 这是我们在第 7 行创建的文件路径对象。

  • 我们应该只保存权重,而不是在训练过程中保存整个模型。默认情况下,这被设置为False,这意味着保存整个模型。通过将此设置为True,我们让神经网络知道我们只想保存权重。

  • verbose = 1打印日志并在控制台上运行状态。否则,默认值 0 表示静音。

基于我们的意图,我们可能想要传递其他的论点。以下是附加参数列表:

  • save_best_only:默认为False。如果设置为True,该算法将评估并保存由我们传递的指标确定的最佳权重。

  • save_frequency:默认值为epoch,这意味着我们希望在每个时期结束时保存检查点。还可以传递一个整数来指示保存检查点的频率。例如,如果您设置了save_frequency = 5,这将意味着每五个时间点保存一次检查点。

您会注意到,在清单 5-6 中,除了符合模型的第 35 行之外,所有其他行都与清单 5-2 中的相同。

第 35 行对fit()函数增加了一个参数。附加参数callbacks = [cp_callback]是为了在模型训练期间保存检查点。

注意,我们在清单 5-6 中设置了epoch=10。图 5-25 和图 5-26 显示了该模型的损耗和精度的部分输出样本。有测试数据的模型精度为 0.9775,损耗为 0.084755。

img/493065_1_En_5_Fig26_HTML.jpg

图 5-26

历元=2 的模型评估

img/493065_1_En_5_Fig25_HTML.jpg

图 5-25

训练损失和准确性

手动保存重量

如果您想要手动保存权重,而不是每个时期或定期保存检查点,您可以简单地添加此函数:

# Save the model weights
checkpoint_path = "cv_checkpoint_dir/mnist_model.ckpt"
model.save_weights(checkpoint_path)

加载保存的权重并重新训练模型

如果您希望加载保存的权重,因为您希望在中断后恢复训练,或者因为您有更多的数据或任何其他原因,只需在创建/配置神经网络后添加以下行:

# Load saved weights
model.load_weights(checkpoint_path)

确保你已经像清单 5-6 的第 22 行和第 30 行一样初始化了你的神经网络。请务必注意,网络体系结构必须与存储检查点的网络相同。

保存整个模型

调用model.save()函数保存整个模型,包括模型架构、权重和训练配置。确保在调用fit()方法后调用函数model.save()。即在清单 5-6 的第 35 行后调用save()函数。下面是保存整个模型的代码片段:

# Save the entire model to a file name “my_ann_model.h5”.
# You can also give the absolute pass to save the model.

model.save('mv_ann_model.h5')

保存一个完整的功能模型是很有用的。

  • 您可以从模型停止的地方加载并重新训练模型。

  • 您可以与其他研究人员或团队成员共享该模型,以便在不同的系统上运行。

  • 您可以在任何其他应用中使用该模型。

重新训练现有模型

如果您想用额外的数据重新训练现有的模型,下面的代码片段将帮助您做到这一点:

# Load and create the exact same model, including its weights and the optimizer
model = tf.keras.models.load_model('mv_ann_model.h5')

# Show the model architecture
model.summary()

#Retrain the model
retrained_model = model.fit(x_train, y_train, validation_split=0.3, epochs=10)

在应用中使用经过训练的模型

如果您已经有一个保存在文件系统中的训练模型,您可以加载该模型并调用predict()函数来使用该模型。这里有一个例子:

# Load and create the exact same model, including its weights and the optimizer
model = tf.keras.models.load_model('mv_ann_model.h5')

# Predict the class of the input image from the loaded model
predicted = model.predict(x_pixel_data)
print("Predicted", predicted)

卷积神经网络

卷积神经网络 (CNN)是一种特殊的人工神经网络。CNN 与传统 ANN 的最大不同在于,特征工程是在 CNN 中自动执行的。

我们将学习 CNN 用来从输入图像中提取和选择特征的技术。一路上,我们将学习一些与 CNN 相关的常用术语。我们将编写 TensorFlow 代码来训练我们自己的 CNN 模型来对图像进行分类,像以前一样,我们将提供对代码的逐行解释。我们将通过一个示例对胸部 x 光进行分类,以检测肺炎。

CNN 的体系结构

传统的人工神经网络或 MLP 由一个输入层、一个或多个隐藏层和一个输出层组成。CNN 有一组附加层,称为卷积层(见图 5-27 )。输入图像被馈送到该卷积层的第一层。卷积层的输出被馈送到全连接 MLP 的“输入”层。卷积层实现一种算法,该算法执行输入图像的特征工程。MLP 实现了传统的深度学习算法来分类图像。

img/493065_1_En_5_Fig27_HTML.png

图 5-27

CNN 架构

卷积层有两个部分。

  • 卷积:该层从图像中提取特征(特征提取)。

  • 二次采样:该层从提取的特征中选择(特征选择)。

图 5-28 描绘了一个完整的 CNN。

img/493065_1_En_5_Fig28_HTML.jpg

图 5-28

具有卷积、子采样和全连接 MLP 层的 CNN

CNN 是如何工作的

我们在第二章中看到,计算机将一幅单通道的黑白图像视为像素值的 2D 矩阵(如图 5-28 )。具有 RGB 通道(三个通道)的彩色图像显示为这些 2D 矩阵的堆叠。这些矩阵的堆叠形成了一个 3D 张量(还记得张量吗?).图 5-29 和图 5-30 显示了 3D 图像张量的视觉呈现。

img/493065_1_En_5_Fig30_HTML.jpg

图 5-30

作为一堆 2D 矩阵的三通道彩色图像的张量表示

img/493065_1_En_5_Fig29_HTML.jpg

图 5-29

一张黑白图像(左)被电脑视为 2D 矩阵(右)

在图像如何被表示为张量的背景下,让我们理解卷积过程。

盘旋

想象一下,我们有一个用放大镜浏览的图像,记录下我们观察到的重要图案。这是卷积如何工作的一个很好的类比。

以下是使用卷积从图像中提取重要特征的步骤:

img/493065_1_En_5_Fig31_HTML.jpg

图 5-31

卷积(图片由 Andrej Karpathy 提供)

  1. 将图像分成大小为 k × k 像素的网格。这被称为内核,它被表示为一个 k × k 矩阵。

  2. 定义一个或多个与内核维度相同的过滤器。

  3. 取其中一个通道的第一个核(从 2D 矩阵的左上角开始),用第一个滤波器进行逐元素乘法,并将乘法结果相加。对其他通道执行相同的操作,并将所有三个通道的结果相加,以获得新创建的要素的像素值。

    如图 5-31 所示。对于这个例子,我们取一个 7×7×3 的图像,其核大小为 3×3。我们有两组过滤器:W0 和 W1(显示为红色)。滤波器 W0 的偏差为 1,而滤波器 W1 没有任何偏差。输出特征显示在绿色网格中(显示在下方最右侧)。

输出计算如下所示:

Channel 1 Output = 0x(-1) +  0x(-1) + 0x1 + 0x1 + 2x1 + 1x0 + 0x0 + 0x(-1)+1x(-1) = 2
Channel 2 Output = 0x0 + 0x0 + 0x(-1) + 0x1 + 2x1 + 0x0 + 0x(-1) + 2x(-1) + 0x1 = 0
Channel 3 Output = 0x0 + 0x0 + 0x1 + 0x0 + 0x0 + 2x0 + 0x0 + 2x0 + 2x1 = 1
Feature Value = Channel 1 Output + Channel 2 Output + Channel 3 Output + bias
Feature Value    = 2 + 0 + 1 + 1 = 4

值 4 突出显示在顶部绿色网格的左上角。

  1. 内核现在向右移动,特征值的计算如前所述。当内核一直向右移动时,它从该行最左边的像素开始向下移动到下一行。内核移动到水平和垂直方向扫描整个图像的步数称为步距。步幅表示为 s (例如 2 或 3 等。).步幅为 2 意味着内核将向右移动两步,当它到达图像的右边缘时,它将向下移动 2 个像素。

  2. 当扫描整个图像时,创建特征矩阵。在我们的例子中,特征矩阵的维数是 3×3(对于 7×7×3 像素的图像,3×3 内核,2×2 步长)。该特征矩阵也称为特征图,如图 5-31 所示,位于顶部绿色 3×3 网格中(右侧)。

  3. 对下一组滤波器重复相同的卷积过程,并且创建特征图。图 5-31 中底部的绿色网格显示了第二个过滤器的特征图。

  4. 对所有过滤器重复该过程,并且从每个过滤器生成特征图。

合并/子采样/缩减采样

卷积从图像中提取特征。这些特征被表示为 n × n 矩阵。这些特征或 n × n 矩阵被馈送到另一个层,称为池层,它执行“下采样”,很像特征选择。最大池和平均池是对要素进行缩减采样的两种常用方法。

最大池化

在汇集层中,很像卷积阶段,特征矩阵被划分成具有步长为 sk × k 的网格(例如,图 5-32 中的 2×2 像素的核)(例如,示例中的步长 1)。在最大池层中,获取每个内核区域的最大像素值,并生成缩减采样矩阵。对来自前一层的每个滤波器输出重复该过程。

img/493065_1_En_5_Fig32_HTML.jpg

图 5-32

最大池向下采样特征(图片由 Andrej Karpathy 提供)

平均池

平均池的工作方式与最大池相同,只是平均池采用内核像素的平均值(不是最大值)来创建缩减像素采样矩阵。

CNN 通常由交替卷积层和池层以及多层感知器组成(如图 5-33 所示)。

img/493065_1_En_5_Fig33_HTML.png

图 5-33

CNN 层,交替卷积和 MLP 池层

CNN 概念概述

以下是我们了解到的情况:

  • CNN 由交替的卷积层和汇集层组成,最后是 MLP。每个卷积图层不一定都有缩减像素采样图层。

  • 卷积是卷积层中的特征提取过程。

  • 定义了维度为 k × k 的核来将输入图像划分成网格。

  • 与内核维数相同的滤波器与内核中的像素相乘,然后对每个像素和每个图像通道的结果求和。可选的偏差被添加到结果中以生成特征矩阵。

  • 池化图层实施缩减采样算法(最大池化或平均池化)对要素进行缩减采样。

  • 对每对卷积汇集层重复该过程,其中来自一个汇集层的输出作为输入被馈送到下一个卷积层。

  • 最后一个卷积/池图层将要素矩阵提供给 MLP 的输入图层。

  • 网络的 MLP 部分像传统的 MLP 网络一样学习。

训练一个 CNN 模型:从胸部 x 光检测肺炎

Keras 的 TensorFlow 使得训练 CNN 模型变得极其简单。只需几行代码,您就可以实现一个 CNN。

在本节中,我们将编写代码来训练一个模型,以便从胸部 X 射线中检测肺炎。这里介绍的模型是一个简单的 CNN 网络,用于学术和学习目的,不得用于诊断任何医疗状况。

胸部 x 光数据集

我们已经从位于 Kaggle 网站 https://www.kaggle.com/paultimothymooney/chest-xray-pneumonia 的公开数据集下载了胸部 x 光图像。这些图片在知识共享许可协议下可用, https://creativecommons.org/licenses/by/4.0/

该数据集由代表正常胸部 X 射线(无疾病肺)和肺炎感染肺的图像组成。这些正常和肺炎图像被分开并存储在单独的目录中;所有正常图像存储在名为NORMAL的目录中,肺炎图像存储在PNEUMONIA目录中。此外,数据集分为训练集、测试集和验证集。从 Kaggle 的网站下载图像后,我们将它们保存在本地磁盘中。图 5-34 显示了一个示例目录结构。

img/493065_1_En_5_Fig34_HTML.jpg

图 5-34

胸部 x 光图像的目录结构

代码结构

我们将保持我们的代码简单易懂。有更好的方法来组织代码,使其更加面向对象和可重用,这是生产质量工作的强烈建议。为了灵活性和可维护性,您必须将代码参数化,并避免任何硬编码。但是,为了便于学习,我们简化了下面的代码,并使用了一些硬编码的值来保持简单性。

CNN 模型训练

清单 5-7 显示了用于训练 CNN 模型的代码示例,该模型根据胸部 x 光片预测肺炎。

1    import numpy as np
2    import pathlib
3    import cv2
4    import tensorflow as tf
5    import matplotlib.pyplot as plt
6
7
8    # Section1: Loading images from directories for training and test
9    trainig_img_dir ="images/chest_xray/train"
10  test_img_dir ="images/chest_xray/test"
11
12   # ImageDataGenerator class provides a mechanism to load both small and large dataset.
13   # Instruct ImageDataGenerator to scale to normalize pixel values to range (0, 1)
14   datagen = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1./255.)
15   #Create a training image iterator that will be loaded in a small batch size. Resize all images to a #standard size.
16   train_it = datagen.flow_from_directory(trainig_img_dir, batch_size=8, target_size=(1024,1024))
17   # Create a training image iterator that will be loaded in a small batch size. Resize all images to a #standard size.
18   test_it = datagen.flow_from_directory(test_img_dir, batch_size=8, target_size=(1024, 1024))
19
20   # Lines 22 through 24 are optional to explore your images.
21   # Notice, next() function call returns both pixel and labels values as numpy arrays.
22   train_images, train_labels = train_it.next()
23   test_images, test_labels = test_it.next()
24   print('Batch shape=%s, min=%.3f, max=%.3f' % (train_images.shape, train_images.min(), train_images.max()))
25
26   # Section 2: Build CNN network and train with training dataset.
27   # You could pass argument parameters to build_cnn() function to set some of the values
28   # such as number of filters, strides, activation function, number of layers etc.
29   def build_cnn():
30      model =  tf.keras.models.Sequential()
31      model.add(tf.keras.layers.Conv2D(32, (3, 3), activation="relu", strides=(2,2), input_shape=(1024, 1024, 3)))
32      model.add(tf.keras.layers.MaxPooling2D((2, 2)))
33      model.add(tf.keras.layers.Conv2D(64, (3, 3), strides=(2,2),activation='relu'))
34      model.add(tf.keras.layers.MaxPooling2D((2, 2)))
35      model.add(tf.keras.layers.Conv2D(128, (3, 3), strides=(2,2),activation='relu'))
36      model.add(tf.keras.layers.Flatten())
37      model.add(tf.keras.layers.Dense(128, activation="relu"))
38      model.add(tf.keras.layers.Dense(2, activation="softmax"))
39      return model
40
41   # Build CNN model
42   model = build_cnn()
43   #Compile the model with optimizer and loss function
44   model.compile(optimizer='adam',
45                loss='categorical_crossentropy',
46                metrics=['accuracy'])
47
48   # Fit the model. fit_generator() function iteratively loads large number of images in batches
49   history = model.fit_generator(train_it, epochs=10, steps_per_epoch=16,
50                      validation_data=test_it, validation_steps=8)
51
52   # Section 3: Save the CNN model to disk for later use.
53   model_path = "models/pneumiacnn"
54   model.save(filepath=model_path)
55
56   # Section 4: Display evaluation metrics
57   print(history.history.keys())
58   plt.plot(history.history['accuracy'], label="accuracy")
59   plt.plot(history.history['val_accuracy'], label = 'val_accuracy')
60   plt.plot(history.history['loss'], label="loss")
61   plt.plot(history.history['val_loss'], label = 'val_loss')
62
63   plt.xlabel('Epoch')
64   plt.ylabel('Metrics')
65   plt.ylim([0.5, 1])
66   plt.legend(loc='lower right')
67   plt.show()
68   test_loss, test_acc = model.evaluate(test_images,  test_labels, verbose=2)
69   print(test_acc)

Listing 5-7Code to Train CNN Model to Predict Pneumonia from Chest X-rays

清单 5-7 中用于 CNN 模型训练的代码在逻辑上分为以下四个部分:

img/493065_1_En_5_Fig36_HTML.jpg

图 5-36

用于训练和评估的度量标准(一个时期内的损失和准确性)的样本图

img/493065_1_En_5_Fig35_HTML.png

图 5-35

CNN 模型训练的样本输出

  • 加载图像(第 9 行到第 24 行):我们将训练和测试图像存储在前面描述的目录中。为了加载这些图像进行训练和验证,我们使用了 Keras 提供的一个强大的类ImageDataGenerator。下面是我们如何使用这个类的逐行解释:

    第 9 行和第 10 行是子目录中有训练和测试图像的目录。

    第 14 行初始化了ImageDataGenerator类。我们传递了参数rescale = 1/255,因为我们想要将像素值标准化到 0 和 1 之间的范围内。这种标准化是通过将图像的每个像素乘以 1/255 来完成的。我们称这条线为datagen,如变量名所示。

    第 16 行调用了datagen对象的flow_from_directory()函数。该功能从training_img_directory目录中批量加载图像(如batch_size = 8),并将图像调整到target_size指定的尺寸(如 1024×1024px)。这是一个高度可扩展的功能,将能够加载数百万张图像,而无需将它们全部加载到内存中。它将一次加载尽可能多的图像,如batch_size参数所示。将所有图像调整到标准大小对于大多数机器学习练习都很重要。注意,这个函数的默认 resize 值是 256。如果省略 resize 参数,所有输入图像都将被调整为 256×256。

    第 17 行和第 16 行做的一样,只是它从test目录加载图像。虽然我们的目录中有验证数据(从 Kaggle 网站下载的数据集包含验证图像),但数量很少,因此我们决定使用测试数据集进行验证。

    函数flow_from_directory()返回一个迭代器。如果对这个迭代器进行迭代,将得到一个由两个 NumPy 数组组成的元组——图像像素值数组和标签数组。

    Note that labels are interpreted from the subdirectories the images are read from. For example, all images from the NORMAL directories will get the label NORMAL, and similarly images belonging to the PNEUMONIA subdirectory will get the PNEUMONIA label. But wait. Aren’t these labels supposed to be numeric? These directory names are sorted by their names and indexed, starting from 0. In our case, NORMAL will be indexed as 0 and PNEUMONIA as 1. But, it does not stop here. The function flow_from_directory() takes an additional argument called class_mode. By default the value of class_mode is categorical. You could also pass a value to it as binary or sparse. The differences between these three are as follows:

    • categorical将返回 2D 的独热编码标签。

    • binary将返回 1D 二进制标签。

    • sparse将返回 1D 整数标签。

    第 22 到 24 行是可选的,训练模型时不需要。我们提供它们是为了向您展示如何从迭代器中探索从flow_from_directory()函数返回的值。

  • CNN 配置和训练(第 29 到 50 行):第 29 到 39 行实现了构建 CNN 的功能。这几行是我们这一节的重点。所以,让我们试着理解这是怎么回事。

    第 30 行创建了一个连续的神经网络,我们在上面堆叠了几层。回想一下,我们使用相同的tf.keras.model.Sequential类来创建顺序模型。model对象的add()函数用于按顺序添加图层——首先添加的图层首先执行,以此类推。

    Line 31 adds our first layer to the network. If you recall from our previous discussion on CNN, our first layer of the CNN must be a convolution layer that takes the input (image pixel values). Here we are using the Conv2D class to define our convolution layer. We are passing five important parameters to Conv2D().

    • 过滤器,在我们的例子中是 64。

    • 内核维度,在本例中是 3×3 像素,并作为元组(3,3)传递。

    • 激活函数,在我们的例子中是relu(因为像素值的范围是从 0 到 1,并且从不为负)。

    • 下一个参数是设置步幅,如果没有设置,默认为(1,1)。在我们的例子中,我们将其设置为(2,2)。

    • 最后一个参数是设置输入大小。由于我们的图像被调整为 1024×1024 像素的彩色(具有三个通道),因此,input_shape 为(1024,1024,3)。

    第 32 行添加了池层,MaxPooling2D。回想一下,除了 MLP 层之前的层之外,卷积层和池层是交替成对出现的。我们传递参数来设置网格或内核的大小。在我们的例子中,它被设置为(2,2)。

    第 33、34 和 35 行也是我们的卷积和池层。您可以根据需要拥有任意多的卷积和池化图层来实现所需的精度级别。

    卷积层(线 35)的输出被馈送到 MLP 的第一层。回想一下,MLP 的第一层称为输入层,接下来是隐藏层,最后是输出层。

    线 36 使线 35 的输出变平。

    第 37 行是 MLP 的隐藏层,已经在 ANN 部分解释过了。

    第 38 行是最后一层,即输出层。如前所述,我们在解决涉及两个类别的分类问题时使用了激活函数 softmax。

    第 42 行简单地调用了build_cnn()函数并创建了一个model对象。

    第 44 行编译模型,正如我们前面看到的 ann。你会注意到在损失函数中清单 5-6 的第 44 行和第 30 行的区别。这里我们使用的是损失函数categorical_crossentropy,而不是我们在清单 5-6 中使用的sparse_categorical_crossentropy。你能猜到原因吗?

    最后,我们在 49 线开始训练。注意,我们没有像在清单 5-6 中那样调用函数fit()。我们正在调用fit_generator()函数。该功能与ImageDataGenerator配合使用,以小批量加载图像。如果使用简单的fit()函数,它将获取第一批输入并训练模型,而这显然不是我们想要的。函数fit_generator()有一个名为steps_per_epoch的重要参数,即它在每个时期内完成的批次数量。以下是官方定义:

    steps_per_epoch:在宣布一个时期结束并开始下一个时期之前,从generator(数据加载器)产生的总步骤数(样品批次)。它通常应该等于数据集的样本数除以批量大小。例如,如果您的训练集中有 1000 个文件,并且您的 batch_size 为 8,那么您应该将steps_per_epoch设置为 1000/8 = 125。

    该函数的另一个重要参数是validation_steps,定义如下:

    validation_steps:仅当validation_data是发电机时相关。它是停止前从generator(数据加载器)产生的总步骤数(样品批次)。

  • 将 CNN 模型保存到磁盘(第 53 和 54 行):第 54 行将训练好的模型保存到第 53 行指定的目录。你也可以省去训练关卡。

  • 评估和可视化(第 57 行到第 69 行):我们绘制了一个训练损失、验证损失、训练准确性和测试准确性相对于时期的图表。第 68 行评估模型,并简单地在第 69 行打印精度。

    图 5-35 显示了模型运行时的示例输出。图 5-36 显示了培训和验证指标的样本图。如图所示,训练和验证的损失随着时期数的增加而减少。此外,精确度随着时间的推移而提高。

肺炎预测

清单 5-8 显示了如何使用之前训练的 CNN 模型从一组新图像中预测肺炎。

1    import numpy as np
2    import pathlib
3    import cv2
4    import tensorflow as tf
5    import matplotlib.pyplot as plt
6
7    model_path = "models/pneumiacnn"
8
9    val_img_dir ="images/chest_xray/val"
10   # ImageDataGenerator class provides a mechanism to load both small and large dataset.
11   # Instruct ImageDataGenerator to scale to normalize pixel values to range (0, 1)
12   datagen = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1./255.)
13   # Create a training image iterator that will be loaded in a small batch size. Resize all images to a #standard size.
14   val_it = datagen.flow_from_directory(val_img_dir, batch_size=8, target_size=(1024,1024))
15
16
17   # Load and create the exact same model, including its weights and the optimizer
18   model = tf.keras.models.load_model(model_path)
19
20   # Predict the class of the input image from the loaded model
21   predicted = model.predict_generator(val_it, steps=24)
22   print("Predicted", predicted)

Listing 5-8Code for Predicting Pneumonia by Using the Trained CNN Model

用于对存在肺炎的图像进行分类或预测的代码分为三个部分。

img/493065_1_En_5_Fig37_HTML.jpg

图 5-37

样本预测输出

  • 加载图像(第 9 行到第 14 行):从磁盘目录加载图像,如清单 5-7 所述。第 14 行和我们之前一样使用了flow_from_directory()

  • 加载保存的模型(第 18 行):从清单 5-7 中回忆,我们在目录models/pneumiacnn中保存了训练好的模型。第 18 行从磁盘目录加载保存的模型。

  • 预测肺炎(第 21 行):第 21 行使用model.predict_generator()函数。该函数类似于fit_generator()函数,因为这两个函数都是批量从磁盘读取图像。predict_generator()功能通过批量加载图像来预测图像是否代表肺炎。

    预测的结果打印在第 22 行。

    图 5-37 显示了一个样本预测输出。

预测输出是一个 NumPy 数组,由每个图像的所有类别的概率组成。在前一个输出样本中,在第一个打印输出行中,第二类的概率最高。大约是 98%,因此第一个输入的预测类是 1(这是概率最高的类的索引)。

CNN 是计算机视觉中最强大的算法之一。在本节中,您了解了 CNN 的概念及其工作原理。我们还通过一些代码示例来训练我们自己的 CNN 模型来预测肺炎。

流行 CNN 的例子

我们在清单 5-7 中构建的 CNN 不是一个生产质量的网络。我们建立了一个简单的网络来学习基础知识。让我们来看看一些在全球范围内被证明是成功的流行网络。

莱内-5

LeNet-5 CNN 体系结构由 LeCun 等人在 1998 年的论文“基于梯度的学习应用于文档识别”中首次提出。该架构主要用于从文档中识别手写和机器生成的字符(光学字符识别[OCR])。该架构简单明了,因此在教学中广泛使用。以下是 LeNet-5 架构的显著特征:

  • 这是一个 CNN 网络,它由七层组成。

  • 在这七层中,有三个卷积层(C1、C3 和 C5)。

  • 有两个子采样层(S2 和 S4)。

  • 有一个全连接层(F6)和一个输出层。

  • 卷积层使用步长为 1 的 5×5 卷积核。

  • 子采样层是 2×2 平均池层。

  • 除了输出层使用 softmax 外,整个网络都使用 TanH 激活函数。

图 5-38 显示了 LeNet-5 网络。

img/493065_1_En_5_Fig38_HTML.jpg

图 5-38

LeNet-5(图片礼遇 of〔??〕〔http://yann〕。莱孔。页:1。pdf

这里有一个练习:修改清单 5-7 中的 TensorFlow 代码并实现 LeNet-5。

阿勒克斯网

AlexNet 是由 Alex Krizhevsky 等人设计的卷积神经网络架构。当 AlexNet 在 2012 年参加 ImageNet 大规模视觉识别挑战赛时,它变得流行起来,并取得了 15.3%的前五名错误,比亚军低了 10.8 个百分点以上。AlexNet 是一个深度网络,尽管计算量很大,但由于使用了 GPU,它变得可行。

AlexNet 的特点如下:

  • 它是一个包含八层的深度卷积神经网络。

  • 输入尺寸为 224×224×3 彩色图像。

  • 前五层是卷积层和最大池层的组合,具有以下配置:

  • 卷积层 1 :内核 11×11,滤镜 96,步长 4×4,激活 ReLU

  • 池化层 1 :内核大小为 3×3 的最大池化,步长为 2×2

  • 卷积层 2 :内核 5×5,滤波器 256,步长 1×1,激活 ReLU

  • 池化层 2 :内核大小为 3×3 的最大池化,步长为 2×2

  • 卷积层 3 :内核 3×3,滤波器 384,步长 1×1,激活 ReLU

  • 卷积层 4 :内核 3×3,滤波器 384,步长 1×1,激活 ReLU

  • 卷积层 5 :内核 3×3,滤波器 384,步长 1×1,激活 ReLU

  • 池化层 5:内核大小为 3×3 的最大池化,步长为 2×2

  • 最后三层是一个完全连接的 MLP。

  • 所有卷积层都使用 ReLU 激活函数。

  • 输出层使用 softmax 激活。

  • 输出层有 1000 个类。

  • 网络有 6000 万个参数,65 万个神经元,在一个 GPU 上训练大概需要 3 天。

图 5-39 显示了 AlexNet 的图示。

img/493065_1_En_5_Fig39_HTML.jpg

图 5-39

具有五个卷积层和三个全连接 MLP 的 AlexNet

VGG-16

我们要探索的下一个著名的深度神经网络是 VGG-16,它在 2014 年赢得了 ImageNet 大规模视觉识别挑战(ILSVRC)比赛。VGG 是由牛津视觉几何小组(VGG)的研究人员设计的。他们的出版物在 https://arxiv.org/abs/1409.1556 有售。

图 5-40 显示了 VGG-16 网络。以下是它的突出特点:

img/493065_1_En_5_Fig40_HTML.jpg

图 5-40

具有 16 层(13 个卷积层和 3 个密集层)的 VGG-16 架构

  • VGG-16 是由 16 层组成的卷积神经网络。

  • 它有 13 个卷积层和 3 个全连接密集层。

  • 16 个卷积层具有以下特征:

  • 卷积层 1 :输入尺寸 224×224×3,内核 3×3,过滤器 64,激活 ReLU

  • 卷积层 2 :内核 3×3,滤波器 64,激活 ReLU

  • 池层:最大池,内核大小 2×2,步数 2×2

  • 卷积层 3 :内核 3×3,滤波器 128,激活 ReLU

  • 卷积层 4 :内核 3×3,滤镜 128,激活 ReLU

  • 池层:最大池,内核大小 2×2,步数 2×2

  • 卷积层 5 :内核 3×3,滤镜 256,激活 ReLU

  • 卷积层 6 :内核 3×3,滤波器 256,激活 ReLU

  • 卷积层 7 :内核 3×3,滤波器 256,激活 ReLU

  • 池层:最大池,内核大小 2×2,步数 2×2

  • 卷积层 8 :内核 3×3,滤镜 512,激活 ReLU

  • 卷积层 9 :内核 3×3,滤镜 512,激活 ReLU

  • 卷积层 10:内核 3×3,滤镜 512,激活 ReLU

  • 池层:最大池,内核大小 2×2,步数 2×2

  • 卷积层 11 :内核 3×3,滤镜 512,激活 ReLU

  • 卷积层 12 :内核 3×3,滤镜 512,激活 ReLU

  • 卷积层 13 :内核 3×3,滤镜 512,激活 ReLU

  • 池层:最大池,内核大小 2×2,步长 2×2

  • 全连通第 14 层(MLP 输入层):展平密集层,输入尺寸 25088

  • 全连通隐藏层 15 :输入尺寸为 4096 的密集层

  • 1000 个类的全连接输出层。

  • 这个网络有 1.38 亿个参数。

这里有一个练习:修改清单 5-7 并使用 TensorFlow 实现 VGG-16 网络。

摘要

在本章中,我们学习了人工神经网络和卷积神经网络的基础知识。我们编写了基于 TensorFlow 的代码来训练我们自己的 ANN 和 CNN 模型,评估结果,并使用保存的模型对图像进行分类。我们还学习了如何调整超参数,以及如何在 TensorBoard 的 HParams 仪表板中可视化分析。此外,我们探索了一些流行的 CNN:LeNet-5,AlexNet 和 VGG-16。

在这一章中,我们解决了分类问题。换句话说,我们的模型被训练来辨别输入图像属于哪一类。在下一章,我们将学习如何检测图像中的物体。****

六、目标检测中的深度学习

在前一章中,我们发现了如何使用标准多层感知器(MLP)和卷积神经网络(CNN)对图像进行分类。在分类任务中,我们预测整个图像的类别,而不关心图像中的对象类型。在本章中,我们将检测图像中的对象及其位置。

本章的学习目标如下:

  • 我们将探索一些用于对象检测的流行深度学习算法。

  • 我们将在 GPU 上使用 TensorFlow 训练我们自己的对象检测模型。

  • 我们将使用经过训练的模型来预测图像中的对象。

本章介绍的概念将在接下来的三章中用于开发真实世界的计算机视觉应用。

目标检测

对象检测涉及两组不同的活动:定位对象和分类对象。在图像中定位对象被称为定位,它通常通过在对象周围绘制边界框来执行。在深度学习算法变得流行之前,对象定位是通过标记图像中包含对象的每个像素来执行的。例如,使用边缘检测、绘制轮廓和猪等技术进行对象检测(重温第 3 和 4 章)。这些技术计算量大、速度慢且不准确。

与非深度学习算法相比,使用深度学习技术的对象检测已被证明更快且更准确。学习过程通常是计算密集型的,但是实际检测是快速的,并且适合于实时检测对象。例如,基于深度学习的对象检测被用于以下情况:

  • 无人驾驶汽车

  • 机场安检

  • 视频监控

  • 工业生产中的缺陷检测

  • 工业质量保证

  • 面部识别

用于对象检测的深度学习算法已经随着时间的推移而发展。在这一章中,我们将学习两种不同的用于目标检测的卷积神经网络:两步卷积和单步卷积。一个基于区域的卷积神经网络 (R-CNN)是两步算法。你只看一次(YOLO)单镜头检测 (SSD)都是单步算法进行目标检测的例子。

在我们深入研究对象检测算法之前,我们将定义一个重要的度量,称为并集上的交集,它广泛用于对象检测器中。

并集上的交集

交集/并集(IoU),也称为 Jaccard 指数,是目标检测算法中最常用的评估指标之一。它用于测量两个任意形状的同一性。

在对象检测中,我们通过在对象周围绘制边界框进行标记来创建训练集。训练集中的这些边界框也被称为基本事实。在模型学习期间,对象检测算法预测边界框,并将它们与真实情况情况进行比较。IoU 用于评估预测边界框与真实情况重叠的紧密程度。

使用图 6-1 所示的公式计算预测边界框 A 和地面真值框 B 之间的 IoU。

img/493065_1_En_6_Fig1_HTML.png

图 6-1

借据

当我们给图像加标签时,我们通常在图像中的对象周围画出矩形框。物体周围的这个矩形区域就是地面真相。在图 6-2 中,地面实况由绿色矩形框显示。

img/493065_1_En_6_Fig2_HTML.jpg

图 6-2

IoU,与地面真值相交的预测边界框

当算法学习时,它会预测对象周围的边界框。在图 6-2 中,红色矩形区域是预测的边界框。

学习算法计算基本事实和预测边界框之间的 IoU。如果预测值和实际值之间的 IoU 小于 50 %,则认为预测值和实际值不匹配。如果 IoU 在 50%到 95%之间,则认为匹配良好。大于 95%的借据被认为是极好的匹配。

目标检测算法的学习目标是优化 IoU。

现在让我们来探索用于对象检测的各种深度学习算法。我们还将回顾它们的优势和劣势,以及它们之间的比较。

基于区域的卷积神经网络

R-CNN 是第一个使用大型卷积神经网络来检测图像中对象的成功模型。Ross Girshick 等人在他们 2014 年题为“精确对象检测和语义分割的丰富特征层次”( https://arxiv.org/pdf/1311.2524.pdf )的论文中描述了该检测方法。图 6-3 展示了 R-CNN 方法。

img/493065_1_En_6_Fig3_HTML.jpg

图 6-3

R-CNN 模型(图片来源:Girshick 等人)

R-CNN 由以下三个模块组成:

  • 区域提议:R-CNN 算法首先在图像中找到可能包含物体的区域。这些地区被称为地区提案。它们被称为建议,因为这些区域可能包含也可能不包含对象,并且学习函数的目标是消除那些不包含对象的区域。这些区域建议是对象周围的边界框(如图 6-3 ,图 2 所示)。

    Girshick 等人提出的 R-CNN 系统对于找到区域提议的算法是不可知的。这意味着你可以使用任何算法,比如 HOG,来寻找这些区域。他们使用了一种被称为选择性搜索的算法。选择性搜索算法通过不同大小的网格查看图像。对于每个网格大小,该算法试图通过比较纹理、颜色或像素值来识别对象,从而将相邻像素分组在一起。使用这种方法,可以创建区域方案。总之,该算法创建了一组潜在目标对象的边界框。

  • 特征提取:从图像中裁剪出区域建议并调整大小。这些裁剪后的图像然后被传送到标准的 CNN 以提取特征(图 6-3 ,图 3)。根据原始论文,使用 AlexNet 深度学习 CNN 进行特征提取。从每个区域中,提取 4096 维特征向量。

  • 分类器:使用标准的分类算法对提取的特征进行分类,如线性 SVM 模型(图 6-3 的图 4)。

R-CNN 是第一个成功的基于深度学习的对象检测系统,但它在性能方面遇到了严重的问题。其时间性能问题是由于以下原因:

  • 每个区域提议被传递给 CNN 进行特征提取。这可能相当于每幅图像大约 2000 遍。

  • 需要训练三个不同的模型:用于特征提取的 CNN,用于预测图像类别的分类器模型,以及用于收紧边界框的回归模型。训练是计算密集型的,并且增加了计算时间。

  • 需要预测每个区域提案。因为地区的数量,CNN 的预测会很慢。

快速 R-CNN

为了克服 R-CNN 的局限性,来自微软的 Ross Girshick 在 2015 年发表了一篇题为“快速 R-CNN”的论文,提出了一个单一的模型来学习并直接输出区域和分类( https://arxiv.org/pdf/1504.08083.pdf )。

快速 R-CNN 也使用一种算法,例如边缘框,来生成区域提议。与裁剪和调整区域提议的 R-CNN 不同,快速 R-CNN 处理整个图像。快速 R-CNN 不是对每个区域进行分类,而是将对应于每个区域提议的 CNN 特征汇集在一起。

图 6-4 展示了快速 R-CNN 架构。它将整个图像作为输入,并生成一组区域建议。深度 CNN 的最后一层有一个特殊的层,叫做感兴趣区域 (ROI)池层。ROI 汇集层从特定于给定输入候选区域的特征图中提取固定长度的特征向量。

img/493065_1_En_6_Fig4_HTML.jpg

图 6-4

快速 R-CNN 架构(图片来源:Ross Girshick)

来自 ROI 池的每个 ROI 特征向量被馈送到全连接 MLP,该全连接生成两组输出-一组用于对象类,另一组用于边界框。softmax 激活函数预测对象类,线性回归器生成对应于预测类的边界框。对 ROI 池中的每个感兴趣区域重复该过程。

正如原始论文所述,Ross Girshick 将 VGG-16 的快速 R-CNN 应用于微软 COCO 数据集,以建立初步基线。COCO 数据集( http://cocodataset.org/ )是大规模的对象检测、分割和字幕数据集,可在公共领域免费获得。快速 R-CNN 训练集由 80,000 幅图像组成,训练迭代了 240,000 个时期。模型质量评估如下:

  • PASCAL 对象数据集的平均精度(mAP):35.9%

  • COCO 数据集的平均精度(AP)为 19.7%

与 R-CNN 相比,快速 R-CNN 在训练和预测方面要快得多。然而,对于每个输入图像,它仍然需要一组候选区域提议,并且单独的模型预测这些区域。

更快的 R-CNN

微软研究院的任等人在 2016 年发表了一篇题为“更快的 R-CNN:利用区域提议网络实现实时对象检测”( https://arxiv.org/pdf/1506.01497.pdf )的论文。本文从训练速度和检测精度的角度描述了快速 R-CNN 的改进版本。除了区域提议方法之外,更快的 R-CNN 在架构上类似于快速 R-CNN。

更快的 R-CNN 架构由区域提议网络(RPN)组成,该网络与检测网络共享完整图像卷积特性,从而实现几乎无成本的区域提议。

RPN 是一个完全卷积的网络。它同时预测图像每个位置的对象边界和对象性得分。RPN 接受端到端的培训,以生成高质量的区域建议书。这些区域提议被快速 R-CNN 用于检测。如图 6-5 所示。

img/493065_1_En_6_Fig5_HTML.jpg

图 6-5

更快的 R-CNN 与 RPN,一个更快的目标检测的统一网络(图片来源:邵青任等)

更快的 R-CNN 由两部分组成:RPN 和快速 R-CNN。

区域提案网络

RPN 是一种深度 CNN,它接受图像输入,并生成作为一组矩形对象提议的输出。每个矩形提议都有一个“反对”分数。

图 6-6 显示了 RPN 如何生成区域建议。我们取最后共享的卷积层生成的卷积特征图,滑动一个小网络。这个小网络将输入卷积特征图的一个 n × n 空间窗口作为输入。每个滑动窗口被映射到较低维度的特征,例如 AlexNet 的 256 维特征或 VGG-16 的 5126 维特征。

img/493065_1_En_6_Fig6_HTML.jpg

图 6-6

使用滑动窗口和锚点的区域检测(图片来源:任等)

此要素被提供给两个完全连接的同级图层-用于预测边界框的盒回归图层和用于预测对象类的盒分类图层。

在每个滑动窗口位置预测多个区域提议。假设在每个窗口位置的最大提议数是 k,边界框坐标的总数将是 4k,对象类的数目将是 2k(一个是对象的概率,另一个不是对象的概率)。每个窗口的这些区域框被称为锚点

快速 R-CNN

更快的 R-CNN 的第二部分是检测网络。这部分和快速 R-CNN(如前所述)完全一样。快速 R-CNN 从 RPN 获取输入来检测图像中的对象。

屏蔽 R-CNN

掩码 R-CNN 扩展了更快的 R-CNN。更快的 R-CNN 由于其检测速度而被广泛用于对象检测任务。我们已经看到,对于给定的图像,更快的 R-CNN 预测图像中每个对象的类别标签和边界框坐标。遮罩 R-CNN 增加了一个额外的分支,用于预测对象遮罩以及对象类别和边界框坐标(查看第三章中的遮罩概念)。

以下是 R-CNN 与其前身更快的 R-CNN 的不同之处:

  • 更快的 R-CNN 有两个输出:类别标签和边界框坐标。

  • 遮罩 R-CNN 有三个输出:类别标签、边界框坐标和对象遮罩。

Ross Girshick 等人在他们 2017 年题为“Mask R-CNN”(https://arxiv.org/pdf/1703.06870.pdf)的论文中解释了 Mask R-CNN。在掩模 R-CNN 中,每个像素被分类到一组固定的类别中,而不区分对象实例。它在神经网络的输出层和输入层之间引入了一个叫做像素到像素对齐的概念。每个像素的类别决定了 ROI 中的遮罩。

图 6-7 说明了 Mask R-CNN 网络架构。

img/493065_1_En_6_Fig7_HTML.png

图 6-7

屏蔽 R-CNN。快速 R-CNN 中的附加掩码预测分支

如图 6-7 所示,网络由三个模块组成——主干、RPN 和输出头。

毅力

主干是标准的深度神经网络。原始论文描述了使用 ResNet-50 和 ResNet-101。主干的主要作用是特征提取。

除了 ResNet,特征金字塔网络(FPN)用于提取图像的更精细的特征细节。

FPN 由 CNN 大小递减的层组成,在这种情况下,每个前层具有较少数量的神经元。

如图 6-8 所示,每一个更高的层将特征传递给更低的层,并且在每一层进行预测。较高图层的尺寸较小,这意味着要素尺寸将小于之前的图层。这种方法以不同的比例捕捉图像的特征,从而允许您检测图像中较小的对象。

img/493065_1_En_6_Fig8_HTML.jpg

图 6-8

FPN(图像来源:Tsung-Yi Lin 等)

FPN 是主干网的附加部分,通常独立于 ResNet 或其他主干网执行。FPN 不仅可以添加到掩模 R-CNN,还可以添加到快速 R-CNN,以便能够检测不同大小的对象。

RPN

如前所述,RPN 模块用于生成区域建议书。屏蔽 R-CNN 情况下的 RPN 架构与更快 R-CNN 情况下的相同。

输出头

如图 6-8 所示,最后一个模块由更快的 R-CNN 和一个额外的输出分支组成。因此,该模块总共产生三个输出。输出——对象类和边界框坐标——与快速 R-CNN 的情况相同。第三个输出是对象遮罩,它是定义对象轮廓的像素列表。

面具的意义是什么?

遮罩 R-CNN(类似于更快的 R-CNN)生成对象类和边界框。这两者的结合有助于我们在图像中定位物体。从网络输出的掩模用于对象分割。这种对象分割广泛用于光学字符识别(OCR)中,以从文档中提取文本。R-CNN 使用屏蔽的另一个例子是在机场安检中,旅客的行李通过屏蔽进行扫描和可视化。图 6-9 显示了屏蔽 R-CNN 的典型显示。

img/493065_1_En_6_Fig9_HTML.jpg

图 6-9

显示带有边框和遮罩的图像(图像来源:Ross Girshick 等人)

人体姿态估计中的掩模 R-CNN

面罩 R-CNN 的一个有趣的用途是估计人的姿势。该网络可被扩展为将关键点的位置建模为一键掩码。关键点被定义为图像上感兴趣的点。对于人类,这些关键点代表主要关节,如肘、肩或膝。选择关键点,使得它们不会随着旋转、移动、收缩、平移和扭曲而改变。掩模 R-CNN 被训练来预测 K 个掩模,每个掩模对应于 K 个关键点类型(例如,左肩、右肘)。见图 6-10 。

img/493065_1_En_6_Fig10_HTML.jpg

图 6-10

使用关键点预测显示人体姿态估计(图片来源:Ross Girshick 等人)

为了训练网络来估计人体姿态,训练图像用实例对象的 K 个关键点来标记。对于每个关键点,训练目标是一个一键 m × m 二进制掩码,其中只有一个像素被标记为前景。

根据原始论文,作者使用了一种变体的雷斯网-FPN 架构作为特征提取主干。头部结构(或输出模块)类似于常规面罩 R-CNN。关键点头部由八个 3×3 512-D 卷积层的堆叠组成,其后是去卷积层和 2×双线性放大。这产生了 56×56 的输出分辨率。据估计,关键点级定位精度需要相对高分辨率的输出(与掩模相比)。

单次多盒检测

R-CNN 及其变体是两级检测器。他们有两个专用网络:一个网络生成区域建议来预测边界框,另一个网络预测对象类。这些两级检测器相当精确,但是它们的计算成本很高。这意味着这些检测器不适合实时检测流视频中的对象。

单次对象检测器在网络的单次前向传递中预测边界框和对象类别。

单次多盒检测(SSD)是由刘威等人在 2016 年发表的题为“SSD:单次多盒检测器”( https://arxiv.org/pdf/1512.02325.pdf )的论文中解释的。首先我们将回顾 SSD 的工作原理,在本章的后面,我们将使用 TensorFlow 训练一个自定义 SSD 模型。

固态硬盘网络架构

SSD 神经网络由两部分组成:基网络和预测网络。

  • 基网络:基网络是深度卷积网络,在任何分类层之前被截断。例如,删除 ResNet 或 VGG 的全连接层,为 SSD 创建基础网络。基本网络用于从输入图像中提取特征。

  • 检测网络:在基础网络上附加一些额外的卷积层,这些卷积层实际上会做包围盒和对象类的预测。检测网络具有以下特征。

用于检测的多尺度特征图

连接到基础网络末端的卷积层的设计方式是这些层的大小逐渐减小。这让我们可以预测多种尺度下的物体。如图 6-11 所示。

img/493065_1_En_6_Fig11_HTML.png

图 6-11

尺寸递减的卷积层,用于预测对象类别和比例范围内的边界框

如图 6-11 所示,每个检测层和可选的基础网络的最后一层预测边界框和对象类别类别的四个坐标的偏移。如何预测边界框和对象?通过锚箱。我们先来了解一下锚箱的概念。

用于检测的锚盒和卷积预测器

锚点是在特征图的每个卷积点设置的一个或多个矩形形状。在图 6-12 中,有五个矩形锚点(显示为红色轮廓)设置在一个点(显示为蓝色)。

img/493065_1_En_6_Fig12_HTML.png

图 6-12

锚箱

在 SSD 中,通常在每个点选择五个锚盒。这些锚中的每一个都作为一个探测器。这意味着,在特征图的每个位置通常有五个检测器,每个检测器检测五个不同的对象(或者没有对象)。这些探测器的不同尺寸允许它们探测不同尺寸的物体。较小的探测器将探测较小的物体,而较大的探测器能够探测较大的物体。

在特征图上的每个卷积点(在图 6-12 中以蓝色显示),算法预测边界框相对于锚框的偏移。它还预测类分数,这些分数指示每个框中类实例的存在。

默认框和纵横比

值得注意的是,这些锚点是预先选择为常量的。在 SSD 中,一组固定的“默认锚点”被映射到每个卷积点。

假设每个位置有 K 个箱子;我们计算 C 类的分数和相对于默认框的四个偏移坐标。这将导致在每个卷积点周围总共有( C +4)× K 个滤波器。假设特征尺寸为 m × n ,输出张量尺寸将为(C+4)×K×m×n

这些默认锚点应用于每个检测卷积层(如图 6-11 所示)。这些卷积层的大小逐渐减小,使我们能够生成不同分辨率的多个特征图。

图 6-13 显示了整体网络架构。

img/493065_1_En_6_Fig13_HTML.jpg

图 6-13

具有用于检测的附加卷积层的截断的骨干网(图像来源:刘等人,arxiv . org/pdf/1512 . 02325 . pdf

培养

在下一节中,我们将探索 SSD 模型如何通过优化损失函数来学习,以及它遵循的对象匹配策略。

匹配策略

在训练期间,该算法确定哪些默认框对应于地面实况,然后相应地训练网络。为了将默认框与地面实况相匹配,它使用 IoU 来确定重叠。这种基于 IoU 的重叠也被称为 Jaccard 重叠。0.5 的 IoU 阈值被视为确定默认框是否与任何地面实况重叠。使用 IoU 的这种重叠在每一层执行,允许网络大规模学习。SSD 从作为预测的默认框开始,并试图回归到更接近真实情况边界框。图 6-14 说明了默认框的重叠和选择的概念。

img/493065_1_En_6_Fig14_HTML.png

图 6-14

默认框与基础真值框的匹配

培训目标

SSD 的学习目标是优化损失函数,该损失函数是所有匹配默认框的定位损失(loc)和置信度损失(conf)的加权和。

为默认框选择比例和纵横比

SSD 网络的检测层的尺寸不断减小,这使得它能够学习不同的对象比例。随着训练的进行,特征图的尺寸减小。算法如何确定每层默认框的大小?

对于每个图层,该算法使用以下公式计算比例:

Sk= Smin+{(Smax—Smin

其中 m 为特征图的大小,Smin= 0.2 为最低层,Smax= 0.9 为最高层。中间的所有其他层间距相等。回想一下,SSD 中使用了五个默认框。这些默认框是为不同的长宽比设置的:ar∈{ 1,2,3,,1/3}。使用以下公式计算每个默认框的宽度和高度:

宽度=【k】$$ \sqrt{{\boldsymbol{a}}_{\boldsymbol{r}}} $$

高度=/$$ \sqrt{{\boldsymbol{a}}_{\boldsymbol{r}}} $$**

对于长宽比为 1 的情况,另一个框的比例【S’k**=$$ \sqrt{\left({S}_k{S}_{k+1}\right)} $$进行计算。这意味着确定了每个要素地图的六个默认框。默认框的中心使用此公式设置: *** ( (i+0.5)/ |fk|,(j+0.5)/ |fk| ) *** ,其中 |fk| 为第 k 个正方形特征图的大小,I,j ∈ [0,|fk|)。

通过组合来自许多特征地图的所有位置的具有不同比例和纵横比的所有默认框的预测,生成了一组不同的预测。这涵盖了各种输入对象的大小和形状。

在下一节中,您将了解到 YOLO 使用 K-means 聚类来动态选择锚盒。此外,在 YOLO,这些锚被称为先验边界框先验

硬负开采

在每个图层和每个要素地图上,都会创建许多默认框。在与地面实况(其中 IoU ≥ 0.5)匹配后,这些默认框中的大多数不会与地面实况重叠。这些不重叠的默认框(IoU < 0.5) are called 负框)和那些与地面实况匹配的是正框。在大多数情况下,负面的数量远远高于正面的数量。这导致了等级不平衡,这将扭曲预测。为了平衡这些类,对负盒进行分类,取最上面的可能的负盒,并丢弃其余的,以使负∶正比率最多为 3∶1。已经发现,这个比率导致更快的优化。

日期增加

SDD 对各种输入对象大小和形状都是鲁棒的。为了增强鲁棒性,每个训练图像都通过以下选项之一进行采样:

  • 使用整个原始图像。

  • 对一个贴片进行采样,使最小 IoU 为 0.1、0.3、0.5、0.7 或 0.9。

  • 随机抽取一个补丁。

每个样品的特征如下:

  • 每个采样的小块的大小是原始图像大小的[0.1,1]。

  • 纵横比介于和 2 之间。

  • 如果地面真值框的中心在采样面片中,则保留地面真值框的重叠部分。

在这些采样步骤之后,除了应用一些光度扭曲之外,每个采样的面片被调整到固定的大小,并且以 0.5 的概率水平翻转。

非最大抑制

在推断时,在 SSD 的向前传递期间生成大量的盒子。处理所有这些边界框将是计算密集型和耗时的。因此,重要的是去掉那些包围盒,它们包含对象的置信度低,并且具有低 IoU。只有具有最大 IoU 和置信度的顶部 N 边界框被选择,而非最大值的框被丢弃或抑制。这消除了重复,并确保网络只保留最有可能的预测。

SSD 结果

SSD 是一种快速、强大且准确的型号。凭借 VGG-16 基础架构,SSD 在准确性和速度方面都优于其最先进的对象检测器。在 PASCAL VOC 和 COCO 数据集上,SSD-512 模型(使用 512×512 输入图像的最高分辨率网络)比最先进的更快 R-CNN 至少快三倍,而且更准确。SSD-300 型号以每秒 59 帧的速度在流视频中更准确地执行实时对象检测,这比第一版 YOLO 更快。在第七章中,你将学习如何使用固态硬盘检测视频中的物体。

YOLO

YOLO 是一种快速、实时、多目标检测算法。YOLO 由一个单一的卷积神经网络组成,该网络同时预测边界框和其中对象的类别概率。YOLO 在完整的图像上训练,并且建立网络来解决回归问题以检测对象。因此,YOLO 不需要复杂的处理流水线,这使得它非常快。

一个基本网络在 Titan X GPU 上每秒运行 45 帧。速度更快的 GPU 版本速度更高,可以达到每秒 150 帧。这使得 YOLO 适合于以小于 25 毫秒的延迟实时检测流视频中的对象。此外,YOLO 的平均精度(mAP)是其他实时系统的两倍多。

YOLO 是由约瑟夫·雷德蒙(Joseph Redmon)、桑托什·迪夫瓦拉(Santosh Divvala)、罗斯·吉斯克(Ross Girshick)和阿里·法尔哈迪(Ali Davis)于 2016 年在他们题为“你只看一次:统一的实时目标检测”( https://arxiv.org/pdf/1506.02640.pdf )的论文中创建的。

检测过程如图 6-15 所示,并在原图中描述如下:

img/493065_1_En_6_Fig15_HTML.jpg

图 6-15

YOLO 物体探测图解(图片来源:Joseph Redmon 等人)

  1. 输入图像被分成 S×S 个网格。

  2. 如果对象的中心落在网格内,则该网格负责检测该对象。

  3. 每个网格单元预测 B 个边界框和这些边界框的置信度得分。

  4. 使用以下公式计算置信度得分:

    置信度得分=客观概率 x 预测框与真实情况之间的 IOU。

    如果边界框不包含任何对象,置信度得分为零。

  5. 对于每个边界框,网络做出五个预测:x、y、w、h 和置信度,其中

    • (x,y)坐标表示相对于网格单元边界的盒子中心。

    • w 和 h 是相对于整个图像的宽度和高度。

    • 置信度预测表示预测框和任何基本事实框之间的 IOU。

  6. 同时,网络为每个网格单元预测以包含对象的网格单元为条件的类别条件概率 C 。不管预测了多少个边界框 B,每个网格单元只预测了一个条件概率。

  7. 为了获得每个盒子的特定于类别的置信度得分,应用以下公式:

    类置信度得分= Pr(Classi|Object) x Pr(Object) x 预测与真实情况之间的 IOU。

    其中Pr(Classi|Object)表示给定网格单元内对象的类的概率。

  8. 这些预测被编码为一个S×S×(B×5+C)张量。

YOLO 的发明人使用以下设置进行评估:

最终的预测产生了一个 7 × 7 × (2 × 5 + 20) = 7 × 7 × 30 的张量。

YOLO 网络设计

YOLO 网络体系结构的灵感来自于用于图像分类的 GoogLeNet。用于 YOLO 的稍加修改的 GoogLeNet 包括 24 个卷积层,最大池继之以两个完全连接的层。请注意图 6-16 中完整网络中最后一层产生的输出张量或维度 7×7×30。

img/493065_1_En_6_Fig16_HTML.jpg

图 6-16

YOLO 神经网络架构。(图片来源:约瑟夫·雷德蒙等人)

YOLO 的局限性

虽然 YOLO 是最快的对象检测算法之一,但它有一些限制。

  • 它与成群出现的小物体斗争,比如成群的鸟。

  • 它只能预测单元网格内的一类对象。

  • 它不能很好地预测对象是否具有在训练集中没有看到的不寻常的纵横比。

  • 它的准确性低于一些最先进的算法,如更快的 R-CNN。

YOLO9000 或 YOLOv2

YOLOv2 是 YOLO 的改进型。与 YOLO 相比,它提高了检测精度和速度。它被训练来检测超过 9000 个物体类别;所以给它取了 YOLO9000 这个名字。约瑟夫·雷德蒙和阿里·法尔哈迪( https://arxiv.org/pdf/1612.08242.pdf )在 2016 年 12 月发表的题为“YOLO9000:更好、更快、更强”的论文中描述了这种改进和检测算法。

YOLOv2 旨在克服 YOLO 的一些限制,特别是精确度和召回水平。此外,它能够检测到具有看不见的纵横比的对象。

以下是 YOLOv2 中为实现更好、更快、更强的结果而进行的改进:

img/493065_1_En_6_Fig17_HTML.jpg

图 6-17

Darknet-19(资料来源:Joseph Redmon 等人, arxiv。页:1。08242 号房。pdf

  • 联合分类和检测 : YOLOv2 可以从包含分类和检测标签的数据集中学习。在训练期间,当网络看到被标记用于检测的图像时,它执行完整的 YOLOv2 损失函数优化。并且,当它看到分类的图像时,它使用网络的分类部分反向传播损失。YOLOv2 的数据集是通过合并 COCO 和 ImageNet 的数据集创建的。与普通 YOLO 相比,能够从分类和检测数据集学习的网络建立了更强的模型。

  • 批量归一化 : YOLOv2 增加了 YOLO 所有卷积层的批量归一化。回想一下,批处理规范化有助于正则化模型。通过使用批量标准化,YOLOv2 显示了超过 2%的地图改进。

  • 高分辨率分类器 : YOLOv2 经过微调,可以从更高分辨率的输入图像中学习。在 448×448 分辨率下,网络输出提高了 4 个百分点。

  • 与锚盒卷积 : YOLOv2 去掉了全连接层,使用了全卷积层。它还引入了锚盒来预测包围盒。虽然精确度略有下降,但通过使用锚盒,YOLOv2 能够在每张图像上检测到 1,000 多个物体,而 YOLO 只有 98 个。

  • 维度聚类:锚框的大小通过使用 VOC 2017 训练集的 K-means 聚类来确定。k=5 时,平均 IOU/模型复杂度达到最佳平衡。借据平均为 61.0%。

  • 细粒度特征 : YOLOv2 使用一个传递层,通过将相邻特征堆叠到不同的通道而不是空间位置来连接更高分辨率的特征。这种方法只带来了 1%的性能提升。

  • 多尺度训练 : YOLOv2 能够检测不同大小图像中的物体。YOLOv2 不是固定输入图像的大小,而是每隔几次迭代就动态地改变网络。例如,每 10 批网络随机选择一个新的图像维度。这意味着同一网络可以预测不同分辨率的检测。在低分辨率下,YOLOv2 作为一种廉价且相当精确的探测器运行。

    288×288 YOLOv2 网络运行速度超过 90 FPS,mAP 几乎与快速 R-CNN 一样好。这使得它非常适合较小的 GPU、高帧速率视频或多个视频流。在高分辨率下,YOLOv2 是一种最先进的检测器,在 VOC 2007 上的 mAP 为 78.6,同时仍以高于实时速度的速度运行。

  • DarkNet 而不是 GoogLeNet : YOLOv2 使用了一种叫做 DarkNet-19 的卷积神经网络。这个网络有 19 个卷积层和 5 个最大池层。Darknet-19 处理一幅图像只需要 55.8 亿次运算,而 VGG 需要 306.7 亿次,YOLO 需要 85.2 亿次。然而,它在 ImageNet 上实现了 72.9%的前一名准确率和 91.2%的前五名准确率。图 6-17 显示了 Darknet-19 网络架构。

下表总结了 YOLOv2 的改进及其对准确性和速度的影响(与普通 YOLO 相比):

|   |

修改

|

效果

|
| --- | --- | --- |
| 较好的 | 批量标准化 | 2%的地图改进 |
|   | 高分辨率分类器 | 4%的地图改进 |
|   | 与锚盒卷积 | 每幅图像能够检测超过 1000 个物体 |
|   | 维度群集 | 地图改善了 4.8% |
|   | 精细特征 | 1%的地图改进 |
|   | 多尺度训练 | 1.1%的地图改进 |
| 更快的 | 暗网-19 | 计算减少 33 %,地图改善 0.4% |
|   | 卷积预测层 | 0.3%的地图改进 |
| 更强壮的 | 联合分类和检测 | 能够探测超过 9000 个物体 |

约洛夫 3 号

YOLO 的最新版本是 YOLOv3,它对 YOLOv2 进行了一些改进。约瑟夫·雷德蒙(Joseph Redmon)和阿里·法尔哈迪( https://arxiv.org/pdf/1804.02767.pdf )在 2018 年 4 月发表的题为“YOLOv3:一种增量改进”的论文中描述了 YOLOv3。

YOLOv3 的特性和改进如下:

  • 边界框预测:在检测边界框时,YOLOv3 与 YOLOv2 相比没有变化。YOLOv3 在训练期间使用误差损失平方和。它还使用逻辑回归预测每个边界框的客观性分数。如果边界框先验比任何其他边界框先验更多地重叠真实情况对象,则对象性得分被取为 1。仅为每个真实情况对象分配一个边界框先验。如果边界框先验不是最佳的,但是与真实情况对象重叠超过某个阈值,则预测被忽略。YOLOv3 的发明人使用 0.5 的阈值。系统只为每个真实情况对象分配一个边界框。

  • 对象类别预测:网络预测一个边界框内对象的多个类别。softmax 激活函数不适用于预测多标签类。因此,YOLOv3 使用回归分类器,而不是 softmax。

  • 跨尺度预测 : YOLOv3 预测三种不同尺度的边界框。它仍然使用 K 均值聚类来确定包围盒先验。它有九个聚类和三个任意选择的尺度,然后它在尺度之间均匀地划分聚类。

例如,在 COCO 数据集上,九个聚类如下:(10×13)、(16×30)、(33×23)、(30×61)、(62×45)、(59×119)、(116×90)、(156×198)、(373×326)。

img/493065_1_En_6_Fig18_HTML.jpg

图 6-18

YOLOv3 中使用的 Darknet-53(来源:arxiv . org/pdf/1804 . 02767 . pdf)

  • 训练:与 YOLOv2 相比,YOLOv3 的训练方法没有变化。使用多尺度数据、批量归一化以及混合分类和检测标签,在完整图像上执行训练。

  • 特征提取器:yolo v3 作为特征提取骨干,使用的是 Darknet-19 的改进版本。这个网络被命名为暗网-53。它有 53 个卷积层。图 6-18 显示了 Darknet-53 网络架构。

以下是 YOLOv3 的测试结果:

  • 对于整个地图,YOLOv3 的性能显著下降,因为网络更宽(53 层,而 YOLOv2 为 19 层)。

  • YOLOv3 使用 608×608 分辨率的图像在 51 毫秒的推理时间内获得了 33.0%的 mAP,而 retina net-101–50–500 在 73 毫秒的推理时间内仅获得了 32.5%的 mAP。

  • YOLOv3 的精度水平与固态硬盘相当,检测时间快 3 倍。

目标检测算法的比较

在本节中,我们探讨了三种不同的对象检测算法类:R-CNN 及其变体,SSD 和 YOLO。这些算法在两个流行的数据集上进行训练——VOC 和 COCO——并针对速度和准确性进行了基准测试。本节提供的比较可用作指南,以确定一种算法相对于另一种算法在构建目标检测系统中的适用性和适用性。性能指标和基准测试结果主要来自于 2019 年 4 月发表的论文“深度学习的对象检测:综述”,该论文由钟、、寿、吴新东撰写( https://arxiv.org/pdf/1807.05511.pdf )。

建筑比较

表 6-1 提供了目标检测算法在它们使用的神经网络架构方面的比较。

表 6-1

目标检测器的神经网络结构比较

|

物体探测器

|

区域提案

|

激活功能

|

损失函数

|

Softmax 层

|
| --- | --- | --- | --- | --- |
| 流程图 | 选择性搜索 | 签名于 | 铰链损失(分类),包围盒回归 | 是 |
| 快速 R-CNN | 选择性搜索 | 签名于 | 类日志丢失+包围盒回归 | 是 |
| 更快的 R-CNN | RPN | 签名于 | 类日志丢失+包围盒回归 | 是 |
| 美国有线电视新闻网 | RPN | 签名于 | 类别日志损失+包围盒回归+语义 sigmoid 损失 | 是 |
| (同 solid-statedisk)固态(磁)盘 | 没有人 | 签名于 | 类和-平方误差损失+包围盒回归 | 不 |
| YOLO | 没有人 | 签名于 | 类别和-平方误差损失+包围盒回归+对象置信度+背景置信度 | 是 |
| 约洛夫 2 号 | 没有人 | 签名于 | 类别和-平方误差损失+包围盒回归+对象置信度+背景置信度 | 是 |
| 约洛夫 3 号 | 没有人 | 签名于 | 类别和-平方误差损失+包围盒回归+对象置信度+背景置信度 | 逻辑分类器 |

性能比较

表 6-2 提供了在 Microsoft COCO 数据集上训练的对象检测算法的性能比较。培训是在英特尔 i7-6700K 单核 CPU 和 Nvidia Titan X GPU 上进行的。

表 6-2

目标检测模型的性能比较

|

物体探测器

|

接受培训

|

地图

|

测试速度(秒/图像)

|

每秒帧数(FPS)

|

适合实时视频?

|
| --- | --- | --- | --- | --- | --- |
| 流程图 | 可可 2007 | 66.0% | Thirty-two point eight four | Zero point zero three | 不 |
| 快速 R-CNN | 可可 2007 和 2012 | 66.9% | One point seven two | Zero point six | 不 |
| 更快的 R-CNN (VGG-16) | 可可 2007 和 2012 | 73.2% | Zero point one one | Nine point one | 不 |
| 更快的 R-CNN (RestNet-101) | 可可 2007 和 2012 | 83.8% | Two point two four | Zero point four | 不 |
| 固态硬盘 300 | 可可 2007 和 2012 | 74.3% | Zero point zero two | Forty-six | 是 |
| 固态硬盘 512 | 可可 2007 和 2012 | 76.8% | Zero point zero five | Nineteen | 是 |
| YOLO | 可可 2007 和 2012 | 73.4% | Zero point zero two | Forty-six | 是 |
| 约洛夫 2 号 | 可可 2007 和 2012 | 78.6% | Zero point zero three | Forty | 是 |
| 约洛夫 3,608x608 | 可可 2007 和 2012 | 76.0% | Zero point zero two nine | Thirty-four | 是 |
| 约洛夫 3 416×416 | 可可 2007 和 2012 | 75.9% | Zero point zero five one | Nineteen | 是 |

基于 TensorFlow 的训练目标检测模型

我们现在准备编写代码来构建和训练我们自己的对象检测模型。我们将使用 TensorFlow API 并用 Python 编写代码。对象检测模型是计算密集型的,需要大量的内存和强大的处理器。大多数通用膝上型电脑或计算机可能无法处理构建和训练对象检测模型所需的计算。例如,具有 32GB RAM 和八核 CPU 的 MacBook Air 无法运行涉及约 7000 张图像的检测模型。谢天谢地,谷歌免费提供了有限的基于 GPU 的计算。事实证明,这些模型在 GPU 上的运行速度比在 CPU 上快很多倍。因此,学习如何在 GPU 上训练模型非常重要。出于演示和学习的目的,我们将使用免费版本的 Google GPU。让我们首先确定我们的学习目标是什么,以及我们希望如何实现它。

  • 目标:学习如何使用 Keras 和 TensorFlow 训练目标检测模型。

  • 数据集:牛津-IIIT Pet 数据集,在robots.ox.ac.uk/~vgg/data/pets/免费提供。该数据集由 37 类宠物组成,每类大约有 200 张图片。这些图像在比例、姿态和光照方面有很大的差异。它们已经用边界框进行了注释和标注。

  • 执行环境:我们将使用 Google Colaboratory ( colab.research.google.com ),简称 Colab。我们将利用 Colab 免费提供的 GPU 硬件加速器。Google Colab 是一个免费的 Jupyter 笔记本环境,不需要设置,完全在云中运行。Jupyter notebook 是一个基于 web 的开源应用,用于编写和执行 Python 程序。要了解如何使用 Jupyter 笔记本的更多信息,请访问 https://jupyter.org .文档可在 https://jupyter-notebook.readthedocs.io/en/stable/ .获得。我们将在编写代码的过程中学习 Colab 笔记本。

  • 重要提示:在写这本书的时候,TensorFlow 第 2 版还不支持对象检测的自定义模型的训练。因此,我们将使用 TensorFlow 版本来训练模型。TensorFlow 团队和开源社区正在努力迁移版本 1 代码,以支持版本 2 中自定义对象检测模型的训练。因此,我们这里的一些步骤将来可能会改变。这本书的 GitHub 位置将有版本 2 的更新步骤。

我们会在 Google Colab 上用 TensorFlow 1 对检测模型进行训练,模型训练好之后,再用 TensorFlow 2 下载使用。我们也将学习如何去做。

带 GPU 的 Google Colab 上的 TensorFlow

Google Colab 免费提供了一款用于机器学习教育和培训的 Jupyter 笔记本。它提供了大约 13GB 的内存,130GB 的磁盘和一个 Nvidia GPU,可连续使用 12 小时。如果会话过期或超过了 12 小时的限制,您可以重新创建运行时。当您执行代码时,它是在一个为您的私人帐户创建的虚拟机上执行的。会话到期后,虚拟机将终止,虚拟磁盘中保存的所有数据都将丢失。然而,Colab 提供了一种将 Google Drive 目录挂载到 Colab 虚拟磁盘的方法。您的数据将存储在您的 Google Drive 上,您可以在创建 Google Colab 会话时检索这些数据。让我们从 Google Colab 开始,并设置我们将用于执行 TensorFlow 代码的运行时环境。

访问 Google Colab

您必须拥有 Google(或 Gmail)帐户才能访问 Google Colab。如果您还没有帐户,您需要先在 https://accounts.google.com .注册一个帐户

使用您的网络浏览器,在 http://colab.research.google.com .访问 Google Colab 网址如果您已经使用您的 Google 帐户登录,您将可以访问 Colab;否则,您需要登录您的帐户才能访问它。

连接到宿主运行时

点击位于屏幕右上角用户和设置图标下方的连接按钮,然后点击“连接到托管运行时”(图 6-19 )。至此,您的 Colab 会话创建完毕。

img/493065_1_En_6_Fig19_HTML.jpg

图 6-19

连接到宿主运行时

选择 GPU 硬件加速器

点击编辑,然后点击“笔记本设置”(图 6-20 ,打开一个模态窗口。选择 GPU 作为硬件加速器。确保为运行时类型选择了 Python 3。点击保存按钮(图 6-21 )。

img/493065_1_En_6_Fig21_HTML.jpg

图 6-21

选择 GPU 作为加速器

img/493065_1_En_6_Fig20_HTML.jpg

图 6-20

访问笔记本设置

创建 Colab 项目

单击文件,然后单击“新建 Python 3 笔记本”您的新笔记本将在新的浏览器选项卡中打开。给这个笔记本起个有意义的名字,比如目标检测模型训练。默认情况下,此笔记本保存在您的 Google Drive 中。

为 TensorFlow 和模型训练设置运行时环境

单击+代码将代码单元格插入笔记本。注意在笔记本的主要区域中有一个空单元格的代码块。您可以在这个单元格中编写任何 Python 代码,并通过单击执行图标img/493065_1_En_6_Figa_HTML.gif来执行它。

Google Colab 是一个交互式编程环境,不提供对底层操作系统的直接访问。您可以使用%%shell调用 shell,它在调用它的单个代码单元块中保持活动状态。您可以根据需要从任意数量的代码块中调用 shell。

要设置我们的环境,我们将遵循以下步骤:

  1. 安装执行 TensorFlow 代码和训练模型所需的必要库。清单 6-1 显示了安装所需库的命令。
Filename: Listing_6_1
1    %%shell
2    %tensorflow_version 1.x
3    sudo apt-get install protobuf-compiler python-pil python-lxml python-tk
4    pip install --user Cython
5    pip install --user contextlib2
6    pip install --user pillow
7    pip install --user lxml
8    pip install --user matplotlib

Listing 6-1Installing the Necessary Libraries and Packages

第 1 行在它所属的代码块的上下文中调用 shell。这允许在这个块中运行任何 shell 命令。

第 2 行告诉笔记本我们想用 TensorFlow 版本 1。 x 而不是最新的版本 2,这是 Google Colab 上机器学习的默认执行引擎。如果您在使用 TensorFlow 2 时遇到任何与 Colab 实例有关的问题,请使用以下命令安装 tensor flow 1.15:pip install tensor flow = = 1.15。

第 3 行使用操作系统命令安装 Protobuf 编译器和一些其他软件。Protobuf 用于编译 TensorFlow 源代码。第 4 行到第 8 行安装 Python 库。

  1. 从 GitHub 资源库下载 TensorFlow“模型”项目,并在您的工作环境中构建和安装它。清单 6-2 展示了如何做到这一点。
1    %%shell
2    mkdir computer_vision
3    cd computer_vision
4    git clone https://github.com/ansarisam/models.git
5    #git clone https://github.com/tensorflow/models.git
6    cd models/research
7
8    protoc object_detection/protos/*.proto --python_out=.
9
10   export PYTHONPATH=$PYTHONPATH:/content/computer_vision/models/research
11   export PYTHONPATH=$PYTHONPATH:/content/computer_vision/models/research/slim
12
13
14   python setup.py build
15   python setup.py install

Listing 6-2Downloading the TensorFlow Models Project, Building It, and Setting It Up

第 1 行调用 shell。

第 2 行创建了一个名为computer_vision的新目录。这是我们想要组织所有代码和数据的目录。第 3 行将当前工作目录更改为我们刚刚创建的新目录。

第 4 行克隆了一个 GitHub 存储库,并下载了 TensorFlow models 项目的源代码。这个库是从官方 TensorFlow 模型库中派生出来的。第 5 行列出了官方存储库以供参考。

models存储库包含许多在 TensorFlow 中实现的模型。在它下载了源代码之后,您将会在models目录中看到两个子目录——officialresearch。“官方”目录包含 TensorFlow 官方支持的所有模型,这些模型是在您安装 TensorFlow 时安装的。research目录包含大量由研究人员创建和维护的模型,还没有得到官方支持。我们感兴趣的对象检测模型在research目录中,还不是正式版本的一部分。

第 6 行将工作目录更改为modes/research目录。

第 8 行使用 Protobuf 编译器构建了与对象检测相关的源代码。

第 10 行和第 11 行将环境变量PYTHONPATH设置为researchresearch/slim目录。

第 14 行使用setup.py执行构建命令,这是 Python script目录中提供的一个脚本。类似地,第 15 行在我们的工作环境中安装了对象检测模型。

要测试您的代码,请逐个执行每个单元块,或者通过单击 Runtime 并从 Colab 的顶部菜单上下文中选择“Run all”来执行所有单元块。如果一切顺利,你的 TensorFlow 版本 1。 x 环境准备好训练目标检测模型。

下载牛津-IIIT 宠物数据集

让我们在笔记本中插入另一个代码单元。我们将从官方网站下载带注释和标签的 pet 数据集到我们 Colab 工作区的一个目录中。清单 6-3 包含下载 pet 数据集和注释的代码。

1    %%shell
2    cd computer_vision
3    mkdir petdata
4    cd petdata
5    wget http://www.robots.ox.ac.uk/~vgg/data/pets/data/images.tar.gz
6    wget http://www.robots.ox.ac.uk/~vgg/data/pets/data/annotations.tar.gz
7    tar -xvf annotations.tar.gz
8    tar -xvf images.tar.gz

Listing 6-3Downloading and Uncompressing the Images and Annotations of the Pet Dataset

第 1 行调用 shell。如果我们想使用任何 shell 命令,我们需要在每个单元块中这样做。

第 2 行将我们的工作目录更改为computer_vision目录。

第 3 行在computer_vision目录中创建了另一个名为petdata的目录。我们将在petdata目录下下载宠物数据集。

第 4 行将工作目录更改为petdata目录。

第 5 行下载宠物图片,第 6 行下载注释。

第 7 行和第 8 行解压缩下载的图像和注释文件。

如果您执行这个代码块,您将看到在petdata目录中下载的图像和注释。图像将存储在images子目录中,注释将存储在petdata目录下的annotations子目录中。

生成 TensorFlow TFRecord 文件

TFRecord 是一种存储二进制记录序列的简单格式。TFRecord 中的数据被序列化并存储在较小的块中(例如,100MB 到 200MB),这使得它们能够更高效地跨网络传输和串行读取。你将在第九章中了解更多关于 TFRecord、其格式以及如何将图像和相关注释转换成 TFRecord 文件格式的信息。现在,我们将使用从 GitHub 下载的 TensorFlow 源代码的research目录中提供的 Python 脚本。脚本位于路径research/object_detection/dataset_tools/create_pet_tf_record.py.

对象检测算法将 TFRecord 文件作为神经网络的输入。TensorFlow 提供了一个 Python 脚本来将 Oxford pet 图像注释文件转换为一组 TFRecord 文件。清单 6-4 将训练集和测试集转换成 TFRecords。

1    %%shell
2    cd computer_vision
3    cd models/research
4
5    python object_detection/dataset_tools/create_pet_tf_record.py \
6       --label_map_path=object_detection/data/pet_label_map.pbtxt \
7       --data_dir=/content/computer_vision/petdata \
8       --output_dir=/content/computer_vision/petdata/

Listing 6-4Converting Image Annotation Files to TFRecord Files

第 2 行和第 3 行将工作目录更改为research目录。

第 5 行到第 8 行运行 Python 脚本,create_pet_tf_record.py,,它采用以下参数:

  • label_map_path:这个文件有一个 ID(从 1 开始)和对应的类名的映射。对于 pet 数据集,映射文件已经存在于object_detection/data/pet_label_map.pbtxt文件中。你将在第九章中学习如何生成这个映射文件。但是现在,让我们只使用已经可用的。这是一个 JSON 格式的文件。映射文件的几个示例条目如下所示:

    item {
      id: 1
      name: 'Abyssinian'
    }
    
    item {
      id: 2
      name: 'american_bulldog'
    }
    ...
    
    
  • data_dir:这是imagesannotations子目录的父目录。

  • output_dir:这是存储 TFRecord 文件的目标目录。您可以给出任何现有的目录名。图像和注释转换后,TFRecord 文件将保存在此目录中。

在这个代码块执行之后,它在output_directory中创建一组*.record文件。脚本create_pet_tf_record.py创建了训练集和评估集。

  • 训练集:输出目录现在应该包含 10 个训练文件和 10 个评估文件。根据您的输入大小,*.record文件的数量可能会有所不同。训练集的*.record文件命名为pet_faces_train.record-?????-of-00010.,正则表达式?????0000100010依次取值。

  • 评估或测试集:评估数据集命名为pet_faces_eval.record-?????-of-00010.

下载用于迁移学习的预训练模型

从头开始训练一个最先进的对象检测模型需要几天时间,即使使用 GPU 也是如此。为了加快训练速度,我们将下载在不同数据集(如 COCO)上训练的现有模型,并重用它的一些参数(包括权重)来初始化我们的新模型。重用来自预训练模型的权重和参数来训练新模型被称为迁移学习。我们将在本节描述迁移学习过程。

在 COCO 和其他数据集上训练的对象检测模型的集合位于“TensorFlow 检测模型动物园”( https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/detection_model_zoo.md )。

以下是 COCO 训练过的模特名单:

|

型号名称

|

速度(毫秒)

|

COCO 地图[¹]

|

输出

|
| --- | --- | --- | --- |
| ssd_mobilenet_v1_coco | Thirty | Twenty-one | 盒子 |
| ssd_mobilenet_v1_0.75_depth_coco ☆ | Twenty-six | Eighteen | 盒子 |
| ssd_mobilenet_v1_quantized_coco ☆ | Twenty-nine | Eighteen | 盒子 |
| ssd_mobilenet_v1_0.75_depth_quantized_coco ☆ | Twenty-nine | Sixteen | 盒子 |
| ssd_mobilenet_v1_ppn_coco ☆ | Twenty-six | Twenty | 盒子 |
| ssd_mobilenet_v1_fpn_coco ☆ | fifty-six | Thirty-two | 盒子 |
| ssd_resnet_50_fpn_coco ☆ | Seventy-six | Thirty-five | 盒子 |
| ssd_mobilenet_v2_coco | Thirty-one | Twenty-two | 盒子 |
| ssd_mobilenet_v2_quantized_coco | Twenty-nine | Twenty-two | 盒子 |
| ssdlite_mobilenet_v2_coco | Twenty-seven | Twenty-two | 盒子 |
| ssd_inception_v2_coco | forty-two | Twenty-four | 盒子 |
| faster_rcnn_inception_v2_coco | Fifty-eight | Twenty-eight | 盒子 |
| faster_rcnn_resnet50_coco | eighty-nine | Thirty | 盒子 |
| faster_rcnn_resnet50_lowproposals_coco | Sixty-four |   | 盒子 |
| rfcn_resnet101_coco | Ninety-two | Thirty | 盒子 |
| faster_rcnn_resnet101_coco | One hundred and six | Thirty-two | 盒子 |
| faster_rcnn_resnet101_lowproposals_coco | Eighty-two |   | 盒子 |
| faster_rcnn_inception_resnet_v2_atrous_coco | Six hundred and twenty | Thirty-seven | 盒子 |
| faster_rcnn_inception_resnet_v2_atrous_lowproposals_coco | Two hundred and forty-one |   | 盒子 |
| faster_rcnn_nas | One thousand eight hundred and thirty-three | Forty-three | 盒子 |
| faster_rcnn_nas_lowproposals_coco | Five hundred and forty |   | 盒子 |
| mask_rcnn_inception_resnet_v2_atrous_coco | Seven hundred and seventy-one | Thirty-six | 面具 |
| mask_rcnn_inception_v2_coco | Seventy-nine | Twenty-five | 面具 |
| mask_rcnn_resnet101_atrous_coco | Four hundred and seventy | Thirty-three | 面具 |
| mask_rcnn_resnet50_atrous_coco | Three hundred and forty-three | Twenty-nine | 面具 |

对于我们的培训,我们将从 http://download.tensorflow.org/models/object_detection/ssd_inception_v2_coco_2018_01_28.tar.gz 下载ssd_inception_v2_coco模型。您可以下载任何经过训练的模型,并按照剩余的步骤来训练您自己的模型。清单 6-5 中的命令集下载 SSD inception 模型。

1    %%shell
2    cd computer_vision
3    mkdir pre-trained-model
4    cd pre-trained-model
5    wget http://download.tensorflow.org/models/object_detection/ssd_inception_v2_coco_2018_01_28.tar.gz
6    tar -xvf ssd_inception_v2_coco_2018_01_28.tar.gz

Listing 6-5Downloading a Pre-trained SSD Inception Object Detection Model

我们在computer_vision目录中创建了一个名为pre-trained-model的新目录,并将工作目录更改为新目录(第 2、3 和 4 行)。

第 5 行使用wget命令下载ssd_inception-v2_coco模型作为压缩文件。

第 6 行将下载的文件解压缩到一个目录中,ssd_inception_v2_coco_2018_01_28.

在 Google Colab 窗口中,展开左侧面板并检查文件选项卡。您应该会看到类似于图 6-22 所示的目录结构。

img/493065_1_En_6_Fig22_HTML.jpg

图 6-22

预训练模型目录结构

配置对象检测流水线

我们需要向 TensorFlow 对象检测 API 提供一个配置文件来训练我们的模型。这个配置文件被称为训练流水线,它有一个定义良好的模式。训练流水线的模式可在目录research中的位置object_detection/protos/pipeline.proto获得。

JSON 格式的培训流水线大致分为五个部分,如下所示:

  • 这定义了我们想要训练的模型的类型。

  • train_config:定义模型参数的设置。

  • eval_config:这决定了将报告哪组指标进行评估。

  • train_input_config:定义模型应该用什么数据集进行训练。

  • eval_input_config:定义模型将在哪个数据集上进行评估。

model: {
        (... Add model config here...)
}

train_config : {
        (... Add train_config here...)
}

train_input_reader: {
        (... Add train_input configuration here...)
}

eval_config: {
        (... Add eval_configuration here...)
}

eval_input_reader: {
        (... Add eval_input configuration here...)
}

在图 6-22 中,注意模型目录中的文件pipeline.configssd_inception_v2_coco_2018_01_28.从 Colab 下载pipeline.config文件(右击并下载),将其保存在本地计算机中,并对其进行编辑以配置模型的流水线。以下是我们将用于模型培训的已编辑文件的示例:

model {
  ssd {
    num_classes: 37
    image_resizer {
      fixed_shape_resizer {
        height: 300
        width: 300
      }
    }
    feature_extractor {
      type: "ssd_inception_v2"
      depth_multiplier: 1.0
      min_depth: 16
      conv_hyperparams {
        regularizer {
          l2_regularizer {
            weight: 3.99999989895e-05
          }
        }
        initializer {
          truncated_normal_initializer {
            mean: 0.0
            stddev: 0.0299999993294
          }
        }
        activation: RELU_6
        batch_norm {
          decay: 0.999700009823
          center: true
          scale: true
          epsilon: 0.0010000000475
          train: true
        }
      }
        override_base_feature_extractor_hyperparams: true
    }
    box_coder {
      faster_rcnn_box_coder {
        y_scale: 10.0
        x_scale: 10.0
        height_scale: 5.0
        width_scale: 5.0
      }
    }
    matcher {
      argmax_matcher {
        matched_threshold: 0.5
        unmatched_threshold: 0.5
        ignore_thresholds: false
        negatives_lower_than_unmatched: true
        force_match_for_each_row: true
      }
    }
    similarity_calculator {
      iou_similarity {
      }
    }
    box_predictor {
      convolutional_box_predictor {
        conv_hyperparams {
          regularizer {
            l2_regularizer {
              weight: 3.99999989895e-05
            }
          }
          initializer {
            truncated_normal_initializer {
              mean: 0.0
              stddev: 0.0299999993294
            }
          }
          activation: RELU_6
        }
        min_depth: 0
        max_depth: 0
        num_layers_before_predictor: 0
        use_dropout: false
        dropout_keep_probability: 0.800000011921
        kernel_size: 3
        box_code_size: 4
        apply_sigmoid_to_scores: false
      }
    }
    anchor_generator {
      ssd_anchor_generator {
        num_layers: 6
        min_scale: 0.20000000298
        max_scale: 0.949999988079
        aspect_ratios: 1.0
        aspect_ratios: 2.0
        aspect_ratios: 0.5
        aspect_ratios: 3.0

        aspect_ratios: 0.333299994469
        reduce_boxes_in_lowest_layer: true
      }
    }
    post_processing {
      batch_non_max_suppression {
        score_threshold: 0.300000011921
        iou_threshold: 0.600000023842
        max_detections_per_class: 100
        max_total_detections: 100
      }
      score_converter: SIGMOID
    }
    normalize_loss_by_num_matches: true
    loss {
      localization_loss {
        weighted_smooth_l1 {
        }
      }
      classification_loss {
        weighted_sigmoid {
        }
      }
      hard_example_miner {
        num_hard_examples: 3000
        iou_threshold: 0.990000009537
        loss_type: CLASSIFICATION
        max_negatives_per_positive: 3
        min_negatives_per_image: 0
      }
      classification_weight: 1.0
      localization_weight: 1.0
    }
  }
}
train_config {
  batch_size: 24
  data_augmentation_options {
    random_horizontal_flip {
    }
  }
  data_augmentation_options {
    ssd_random_crop {
    }
  }
  optimizer {
    rms_prop_optimizer {
      learning_rate {
        exponential_decay_learning_rate {
          initial_learning_rate: 0.00400000018999
          decay_steps: 800720
          decay_factor: 0.949999988079
        }
      }
      momentum_optimizer_value: 0.899999976158
      decay: 0.899999976158
      epsilon: 1.0
    }
  }
  fine_tune_checkpoint: "PATH_TO_BE_CONFIGURED/model.ckpt"
  from_detection_checkpoint: true
  num_steps: 100000
}
train_input_reader {
  label_map_path: "PATH_TO_BE_CONFIGURED/mscoco_label_map.pbtxt"
  tf_record_input_reader {
    input_path: "PATH_TO_BE_CONFIGURED/mscoco_train.record"
  }

}
eval_config {
  num_examples: 8000
  max_evals: 10
  use_moving_averages: false
}
eval_input_reader {
  label_map_path: "PATH_TO_BE_CONFIGURED/mscoco_label_map.pbtxt"
  shuffle: false
  num_readers: 1
  tf_record_input_reader {
    input_path: "PATH_TO_BE_CONFIGURED/mscoco_val.record"
  }
}

由于pipeline.config文件是在训练我们下载用于迁移学习的模型时保存的,因此除了那些使用粗体突出显示的部分之外,我们将保留大多数部分。以下是我们应该根据 Colab 环境中的设置进行更改的参数:

  • 37,代表我们数据集中的 37 类宠物。

  • fine_tune_checkpoint : /content/computer_vision/pre-trained-model/ssd_inception_v2_coco_2018_01_28/model.ckpt,这是我们存储预训练模型检查点的路径。注意在图 6-22 中,模型检查点的文件名是model.ckpt.data-00000-of-00001,但是在fine_tune_checkpoint配置中,我们只提供到model.ckpt(你不能包括检查点文件的全名)。要获取此检查点文件的路径,请在 Colab 文件浏览器中,右键单击文件名,然后单击“复制路径”

  • num_steps: 100000,这是算法应该执行的步骤数。您可能需要调整这个数字,以获得理想的精度水平。

  • Train_input_readerlabel_map_path : /content/computer_vision/models/research/object_detection/data/pet_label_map.pbtxt,是包含 ID 和类名映射的文件的路径。对于 pet 数据集,这可以在研究目录中找到。

  • Train_input_readerinput_path : /content/computer_vision/petdata/pet_faces_train.record-?????-of-00010,这是训练数据集 TFRecord 文件的路径。注意,我们在训练集路径中使用了一个正则表达式(?????)。这对于包含所有培训 TFRecord 文件非常重要。

  • Eval_input_readerlabel_map_path : /content/computer_vision/models/research/object_detection/data/pet_label_map.pbtxt,与训练标签图相同。

  • Eval_input_readerinput_path : /content/computer_vision/petdata/pet_faces_eval.record-?????-of-00010,这是评估数据集 TFRecord 文件的路径。注意,我们在评估集路径中使用了一个正则表达式(?????)。这对于包含所有评估 TFRecord 文件非常重要。

需要注意的是,pipeline.config将参数override_base_feature_extractor_hyperparams设置为true.

编辑完pipeline.config文件后,需要上传到 Colab。您可以将它上传到任何目录位置,但在这种情况下,我们将它上传到下载它的原始位置。我们将首先删除旧的pipeline.config文件,然后上传更新的文件。

要从 Colab 目录位置删除旧的pipeline.config文件,右键单击它,然后单击 delete。要从本地计算机上传更新的pipeline.config文件,右键单击 Colab 目录(ssd_inception_v2_coco_2018_01_28),单击上传,从您的计算机浏览并上传文件。

执行模型训练

我们准备好开始训练了。列表 6-6 触发训练执行。

1    %%shell
2    export PYTHONPATH=$PYTHONPATH:/content/computer_vision/models/research
3    export PYTHONPATH=$PYTHONPATH:/content/computer_vision/models/research/slim
4    cd computer_vision/models/research/
5    PIPELINE_CONFIG_PATH=/content/computer_vision/pre-trained-model/ssd_inception_v2_coco_2018_01_28/pipeline.config
6    MODEL_DIR=/content/computer_vision/pet_detection_model/
7    NUM_TRAIN_STEPS=1000
8    SAMPLE_1_OF_N_EVAL_EXAMPLES=1
9    python object_detection/model_main.py \
10      --pipeline_config_path=${PIPELINE_CONFIG_PATH} \
11      --model_dir=${MODEL_DIR} \
12      --num_train_steps=${NUM_TRAIN_STEPS} \
13      --sample_1_of_n_eval_examples=$SAMPLE_1_OF_N_EVAL_EXAMPLES \
14      --alsologtostderr

Listing 6-6Executing the Model Training

TensorFlow 提供了一个 Python 脚本model_main.py,用来触发模型训练。这个脚本位于目录models/research/object_detection中。该脚本采用以下参数:

  • pipeline_config_path:这是pipeline.config文件的路径。

  • 这是你训练好的模型将被保存的目录。

  • 这是我们希望我们的网络训练的步数。这将覆盖pipeline.config文件中的num_steps参数。

  • sample_1_of_n_eval_examples:这决定了模型应该使用多少个样本中的一个进行评估。

执行 Colab 中前面的代码块,等待模型从您的图像集中学习。当模型学习时,您将会在 Colab 控制台中看到迭代损失。如果一切顺利,您将在model_dir目录中保存一个经过训练的对象检测模型。

导出 TensorFlow 图

在模型被成功训练之后,模型连同检查点被保存在model_dir,中,在我们的例子中是pet_detection_model。该目录包含培训期间生成的所有检查点。这些检查点必须转换成最终模型。为了在预测对象和边界框时使用这个模型,我们需要导出这个模型。以下是步骤。

首先,我们需要确定要导出的候选检查点。这可能是我们通过查看文件名中的序列号可以识别的最后一个检查点。检查点通常由以下三个文件组成(暂时忽略目录中的其余文件):

  • model.ckpt-${CHECKPOINT_NUMBER}.data-00000-of-00001

  • model.ckpt-${CHECKPOINT_NUMBER}.index

  • model.ckpt-${CHECKPOINT_NUMBER}.meta

取具有最大${CHECKPOINT_NUMBER}值的检查点。我们的模型运行了 10,000 步,因此我们的最大检查点文件应该如下所示:

  • model.ckpt-10000.data-00000-of-00001

  • model.ckpt-10000.index

  • Model.ckpt-10000.meta

清单 6-7 将我们的目标检测训练模型导出到用户定义的目录中。

1    %%shell
2    export PYTHONPATH=$PYTHONPATH:/content/computer_vision/models/research
3    export PYTHONPATH=$PYTHONPATH:/content/computer_vision/models/research/slim
4    cd computer_vision/models/research
5
6    python object_detection/export_inference_graph.py \
7       --input_type image_tensor \
8       --pipeline_config_path /content/computer_vision/pre-trained-model/ssd_inception_v2_coco_2018_01_28/pipeline.config \
9       --trained_checkpoint_prefix /content/computer_vision/pet_detection_model/model.ckpt-100 \
10      --output_directory /content/computer_vision/pet_detection_model/final_model

Listing 6-7Exporting the TensorFlow Graph

第 6 到 10 行通过调用位于目录models/research/object_detection中的脚本export_inference_graph.py来导出 TensorFlow 图。该脚本采用以下参数:

img/493065_1_En_6_Fig23_HTML.jpg

图 6-23

在 final_model 目录中导出的模型

  • input_type:对于我们的型号来说,会是image_tensor

  • pipeline_config_path:这与我们之前使用的pipeline.config文件路径相同。

  • trained_checkpoint_prefix:这是我们之前确定的候选检查点的路径(model.ckpt-ckpt-10000)。不要在检查点前缀中使用.index.meta或任何东西。

  • output_directory:这是保存导出图形的目录。图 6-23 显示了执行导出脚本后的输出目录结构。

下载对象检测模型

Google Colab 不允许你下载目录。您可以下载文件,但不能下载目录。当然,您可以从final_model目录中一个接一个地下载每个文件,但是这样效率不高。但是,我们将学习如何将您完全训练好的模型保存到您的私人 Google Drive 中。

Google Colab 将终止您的虚拟机,并在连续使用 12 小时后或您的会话到期后删除您的所有数据。这意味着如果您不下载它,您将会丢失您的模型。您可以直接将您的模型和任何数据保存到 Google Drive。如果你的模型要运行几个小时,在开始训练过程之前,你最好把所有的数据和模型保存在 Google Drive 中。

以下是实现这一点的步骤。

要安装 Google Drive,请在左侧面板中单击“文件”,然后单击“安装驱动器”。一些新代码被插入到笔记本区域。点击代码块中的img/493065_1_En_6_Figb_HTML.gif图标执行代码。

单击授权链接以生成授权码。您可能需要再次登录您的 Google 帐户。复制授权码并将其粘贴到笔记本中,然后按回车键。见图 6-24 。安装驱动器后,您会在左侧面板的文件选项卡上看到一个目录列表(如图 6-25 )。请注意,图 6-25 中的示例 Google Drive 有一个名为computervision的目录,该目录已经在驱动器中创建。随意创建你想要的任何目录。

img/493065_1_En_6_Fig25_HTML.jpg

图 6-25

Google dDive 目录结构

img/493065_1_En_6_Fig24_HTML.jpg

图 6-24

Google Drive 安装

final_model目录移动到 Google Drive 目录。

要将经过训练的对象检测模型保存到 Google Drive 目录,只需将final_directory从 Colab 目录拖到 Google Drive 目录。

您还必须将以下检查点文件复制到 Google Drive:

  • model.ckpt-10000.data-00000-of-00001

  • Model.ckpt-10000.index

  • Model.ckpt-10000.meta

要从 Google Drive 下载模型,请登录到您的 Google Drive,并将训练好的模型下载到您的本地计算机。您应该下载整个final_model目录。

在 TensorBoard 中可视化训练结果

要查看训练统计和模型结果,使用 Colab 中清单 6-8 中的代码启动 TensorBoard 仪表板。--logdir是我们保存模型检查点的目录。

1    %load_ext tensorboard
2    %tensorboard --logdir /content/computer_vision/pet_detection_model

Listing 6-8Launching the TensorBoard Dashboard to See the Training Results

第 1 行加载 TensorBoard 笔记本扩展。这将显示嵌入在 Colab 屏幕中的 TensorBoard 仪表板。

图 6-26 显示了显示图像页面的 TensorBoard 仪表盘。

img/493065_1_En_6_Fig26_HTML.jpg

图 6-26

TensorBoard 仪表板中的模型训练结果

或者,如果您想在您的本地计算机上而不是在 Colab 上离线评估模型,您可以下载我们保存模型检查点的整个pet_detection_model目录。我们将训练模型导出到的目录final_model不包含完整的模型统计数据和训练结果。因此,你必须下载整个pet_detection_model目录。

在您的计算机终端(或命令提示符)中,通过将路径传递到pet_detection_model目录来启动 TensorBoard。确保你在虚拟环境中(如第一章所述)。以下是命令:

(cv) username$ tensorboard --logdir ~/Downloads/pet_detection_model

成功执行前一个命令后,打开您的网络浏览器并转到http://localhost:6006查看 TensorBoard 仪表盘。点击顶部菜单中的图像选项卡,查看图像上带有边框的评估输出,如图 6-26 所示。

使用训练模型检测对象

正如我们之前所了解的,模型训练不是一项频繁的活动,当我们有一个相当好的模型(高精度或 mAP)时,只要模型给出准确的预测,我们就可能不需要重新训练模型。此外,模型训练是计算密集型的,即使在 GPU 上训练一个好的模型也需要几个小时或几天。有时,在云上训练计算机视觉模型并使用 GPU 是可取和经济的。当模型准备就绪时,将其下载到您的本地计算机或应用服务器中使用,它们将使用该模型来检测图像中的对象。

在本节中,我们将解释如何使用我们在 Google Colab 上训练的模型在您的本地计算机中开发对象检测预测器。我们将使用 PyCharm,这是我们在本书中一直使用的 IDE。当然,您可以使用 Colab 来开发对象检测预测器,但是从生产部署的角度来看,这并不理想。

虽然在 TensorFlow 版本 2 中还不支持对象检测模型训练,但是我们在这里将要编写的检测代码可以在 TensorFlow 2 上工作。

我们将遵循这个高级计划来开发我们的预测器:

  1. 从 GitHub 资源库下载并安装 TensorFlow models项目。

  2. 编写 Python 代码,该代码将利用导出的 TensorFlow 图(导出的模型)来预测新图像中未包含在训练集或测试集中的对象。

安装 TensorFlow 的模型项目

TensorFlow models项目的安装过程和我们在 Google Colab 上做的一样。不同之处可能在于 Protobuf 的安装,因为它是依赖于平台的软件。在我们开始之前,确保您的 PyCharm IDE 配置为使用我们在第一章中创建的虚拟环境。我们将在 PyCharm 的终端窗口中执行命令。如果您选择使用操作系统的 shell 来执行命令,请确保您已经为 shell 会话激活了虚拟环境。(参见第一章复习 virtualenv。)下面是安装和配置models项目的完整步骤:

img/493065_1_En_6_Fig27_HTML.jpg

图 6-27

由 TensorFlow 模型项目组成的示例目录结构

表 6-3

安装依赖项的命令

| `pip install --user Cython``pip install --user contextlib2``pip install --user pillow``pip install --user lxml` |
  1. 首先,让我们安装一些构建和安装models项目所需的必要库。在终端或命令提示符下执行表 6-3 中所示的命令(从 virtualenv 中)。

  2. 安装谷歌的 Protobuf 编译器。安装过程取决于您使用的操作系统。遵循您的操作系统的以下说明:记住您安装 Protobuf 的目录位置,因为在构建 TensorFlow 代码时,您需要提供到bin/protoc的完整路径。

    1. 关于 Ubuntu: sudo apt-get install protobuf-compiler

    2. 在其他 Linux 操作系统上:

      wget -O protobuf.ziphttps://github.com/google/protobuf/releases/download/v3.0.0/protoc-3.0.0-linux-x86_64.zip
      unzip protobuf.zip
      
      

      Remember the directory location you have installed Protobuf in, as you will need to provide the full path to

    3. 在 Mac OS 上:brew install protobuf

  3. 使用以下代码从 GitHub 克隆 TensorFlow models项目:

    git clone https://github.com/ansarisam/models.git

    您也可以通过 https://github.com/tensorflow/models.git 从 TensorFlow 官方资源库下载模型。

    如图 6-27 所示,我们已经将 TensorFlow models项目下载到了名为chapter6的目录下。

  4. Compile the models project using the Protobuf compiler. Run the following set of commands from the models/research directory:

    $ cd models/research
    $ protoc object_detection/protos/*.proto --python_out=.
    
    

    如果您手动安装了 Protobuf 并将其解压缩到一个目录中,请在前面的命令中提供到bin/protoc的完整路径。

  5. 设置以下环境变量。标准做法是在~/.bash_profile中设置这些环境变量。以下是操作说明:

    1. 打开命令提示符或终端,键入vi ~/.bash_profile。您可以使用任何其他编辑器(如 nano)来编辑.bash_profile文件。

    2. .bash_profile的末尾增加以下三行。确保路径与您计算机中的目录路径匹配。

      export PYTHONPATH=$PYTHONPATH:~/cviz_tf2_3/chapter6/models/research/object_detection
      
      export PYTHONPATH=$PYTHONPATH:~/cviz_tf2_3/chapter6/models/research
      
      export PYTHONPATH=$PYTHONPATH:~/cviz_tf2_3/chapter6/models/research/slim
      
      
    3. 添加前一行后保存文件~/.bash_profile

    4. 关闭终端,然后重新启动以使更改生效。您需要关闭 PyCharm IDE 来更新 IDE 中的环境变量。要测试设置,请在 PyCharm 终端窗口中键入命令echo $PYTHONPATH。它应该打印出我们刚刚设置的路径。

  6. 构建并安装我们刚刚使用 Protobuf 构建的research项目。从models/research目录执行以下命令:

    python setup.py build
    python setup.py install
    
    

如果该命令成功运行,它应该在最后显示如下内容:

Finished processing dependencies for object-detection==0.1

我们已经做好了环境准备,准备编写代码来检测图像中的对象。我们将使用从 Colab 下载的导出模型。如果您还没有这样做,现在是时候从 Google Colab 或 Drive 下载最终模型了(如果您将模型保存在 Google Drive 中)。

目标检测代码

现在,我们已经准备好了编码环境和 TensorFlow models项目的 GitHub 检验,并且所有必要的设置都已完成,我们准备编写代码来检测图像中的对象并在它们周围绘制边界框。为了使代码简单易懂,我们将其分为以下几个部分:

  • 配置和初始化:在这段代码中,我们初始化模型路径、图像输入和输出目录。清单 6-9 显示了代码的第一部分,包括库导入和路径设置。
Filename: Listing_6_9.py
1    import os
2    import pathlib
3    import random
4    import numpy as np
5    import tensorflow as tf
6    import cv2
7    # Import the object detection module.
8    from object_detection.utils import ops as utils_ops
9    from object_detection.utils import label_map_util
10
11   # to make gfile compatible with v2
12   tf.gfile = tf.io.gfile
13
14   model_path = "ssd_model/final_model"
15   labels_path = "models/research/object_detection/data/pet_label_map.pbtxt"
16   image_dir = "images"
17   image_file_pattern = "*.jpg"
18   output_path="output_dir"
19
20   PATH_TO_IMAGES_DIR = pathlib.Path(image_dir)
21   IMAGE_PATHS = sorted(list(PATH_TO_IMAGES_DIR.glob(image_file_pattern)))
22
23   # List of the strings that is used to add the correct label for each box.
24   category_index = label_map_util.create_category_index_from_labelmap(labels_path, use_display_name=True)
25   class_num =len(category_index)

Listing 6-9Imports and Path Initialization Part of the Object Detection Code

1 至 6 号线是我们通常的进口货。第 8 行和第 9 行从 TensorFlow models项目的research模块导入对象检测 API。确保PYTHONPATH环境变量设置正确(如前所述)。

第 12 行在 TensorFlow2 兼容模式下初始化gfilegfile在 TensorFlow 中提供 I/O 功能。

第 14 行初始化我们的对象检测训练模型所在的目录路径。

第 15 行初始化映射文件路径。我们设置了相同的 JSON 格式的文件,其中包含我们在培训中使用的类 ID 和类名映射。

第 16 行是输入目录路径,包含需要检测对象的图像。

第 17 行定义了输入图像路径中文件名的模式。如果您想从目录中加载所有文件,请使用*.*

第 18 行是输出目录路径,检测到的对象周围带有边框的图像将保存在该路径中。

第 20 行和第 21 行用于创建 iterable path 对象,我们将遍历这些对象来逐个读取图像,并检测每个图像中的对象。

第 24 行使用标签映射文件创建一个类别或类索引。

第 25 行将类的数量分配给了class_num变量。

除了前面的初始化之外,我们还初始化了一个颜色表,我们将在绘制边界框时使用它。清单 6-10 显示了代码。

  • 通过加载定型模型来创建模型对象。清单 6-11 显示了将模型路径作为输入的函数load_model()。第 40 行从目录中加载保存的模型,并创建一个由该函数返回的模型对象。我们将使用这个模型对象来预测对象和边界框。
27   def get_color_table(class_num, seed=0):
28      random.seed(seed)
29      color_table = {}
30      for i in range(class_num):
31          color_table[i] = [random.randint(0, 255) for _ in range(3)]
32      return color_table
33
34   colortable = get_color_table(class_num)
35

Listing 6-10Creating a Color Table Based on the Number of Object Classes

  • 运行预测并以可用的形式构建输出。我们已经编写了一个名为run_inference_for_single_image()的函数,它有两个参数:模型对象和图像数量。这个函数返回一个 Python 字典。输出字典包含以下密钥对:

    detection_boxes,这是一个由边界框的四个角组成的 2D 数组。

    detection_scores,这是与每个边界框相关联的分数的 1D 阵列。

    detection_classes,其是与每个边界框相关联的对象类索引的整数表示的 1D 阵列。

    num_detections,表示预测对象类别的数量的标量。

    清单 6-12 显示了函数run_inference_for_single_image()的实现。

    让我们一行一行地检查代码清单。

    TensorFlow 模型对象采用一批图像张量来预测对象类及其周围的边界框。第 48 行将图像 NumPy 转换成张量。因为我们一次处理一幅图像,而模型对象需要一批图像,所以我们需要将图像张量转换成一批图像。第 50 行就是这么做的。当使用一次时,tf.newaxis表达式用于将现有数组的维数增加 1。因此,1D 阵列将成为 2D 阵列。2D 阵列将变成三维阵列。诸如此类。

36   # # Model preparation and loading the model from the disk
37   def load_model(model_path):
38
39      model_dir = pathlib.Path(model_path) / "saved_model"
40      model = tf.saved_model.load(str(model_dir))
41      model = model.signatures['serving_default']
42      return model
43

Listing 6-11Loading the Model from a Directory

44   # Predict objects and bounding boxes and format the result

45   def run_inference_for_single_image(model, image):
46
47      # The input needs to be a tensor, convert it using `tf.convert_to_tensor`.
48      input_tensor = tf.convert_to_tensor(image)
49      # The model expects a batch of images, so add an axis with `tf.newaxis`.
50      input_tensor = input_tensor[tf.newaxis, ...]
51
52      # Run prediction from the model
53      output_dict = model(input_tensor)
54
55      # Input to model is a tensor, so the output is also a tensor
56      # Convert to numpy arrays, and take index [0] to remove the batch dimension.
57      # We're only interested in the first num_detections.
58      num_detections = int(output_dict.pop('num_detections'))
59      output_dict = {key: value[0, :num_detections].numpy()
60                     for key, value in output_dict.items()}
61      output_dict['num_detections'] = num_detections
62
63      # detection_classes should be ints.
64      output_dict['detection_classes'] = output_dict['detection_classes'].astype(np.int64)
65
66      # Handle models with masks:
67      if 'detection_masks' in output_dict:
68          # Reframe the the bbox mask to the image size.
69          detection_masks_reframed = utils_ops.reframe_box_masks_to_image_masks(
70              output_dict['detection_masks'], output_dict['detection_boxes'],
71              image.shape[0], image.shape[1])
72          detection_masks_reframed = tf.cast(detection_masks_reframed > 0.5,
73                                             tf.uint8)
74          output_dict['detection_masks_reframed'] = detection_masks_reframed.numpy()
75
76      return output_dict

Listing 6-12Predicting Objects and Bounding Boxes and Organizing the Output

第 53 行是进行实际对象检测的行。函数model(input_tensor)预测对象类别、边界框和相关分数。model(input_tensor)函数返回一个字典,我们将以一种可用的形式对其进行格式化,以便它只包含与输入图像相对应的输出。

由于模型获取一批图像,因此该函数返回该批图像的输出。因为我们只有一个图像,所以我们对这个输出字典的第一个结果感兴趣(通过第 0 个索引访问)。第 59 行提取第一个输出并重新分配output_dict变量。

第 61 行在字典中存储了一些检测,这样我们在处理结果时就可以方便地使用这个数字。

当需要预测屏蔽时,第 66 到 74 行仅适用于屏蔽 R-CNN。对于所有其他预测,这些线可以省略。

第 76 行返回输出字典,它由检测到的边界框的坐标、对象类、分数和检测数量组成。在屏蔽 R-CNN 的情况下,它还包括对象屏蔽。

接下来,我们将检查如何使用output_dict在图像中检测到的物体周围绘制边界框。

  • 我们现在将编写代码来推断输出,在检测到的对象周围绘制边界框,并存储结果。清单 6-13 中的函数infer_object()用于推断函数run_inference_for_single_image()返回的output_dict。这个名为infer_object()的函数在图像中每个检测到的对象周围绘制边界框。它还用类名和分数标记对象,最后将结果保存到输出目录位置。清单 6-13 是代码的逐行解释。
79   def infer_object(model, image_path):
80      # Read the image using openCV and create an image numpy
81      # The final output image with boxes and labels on it.
82      imagename = os.path.basename(image_path)
83
84      image_np = cv2.imread(os.path.abspath(image_path))
85      # Actual detection.
86      output_dict = run_inference_for_single_image(model, image_np)
87
88      # Visualization of the results of a detection.
89      for i in range(output_dict['detection_classes'].size):
90
91          box = output_dict['detection_boxes'][i]
92          classes = output_dict['detection_classes'][i]
93          scores = output_dict['detection_scores'][i]
94
95          if scores > 0.5:
96              h = image_np.shape[0]
97              w = image_np.shape[1]
98              classname = category_index[classes]['name']
99              classid =category_index[classes]['id']
100             #Draw bounding boxes
101             cv2.rectangle(image_np, (int(box[1] * w), int(box[0] * h)), (int(box[3] * w), int(box[2] * h)), colortable[classid], 2)
102
103             #Write the class name on top of the bounding box
104             font = cv2.FONT_HERSHEY_COMPLEX_SMALL
105             size = cv2.getTextSize(str(classname) + ":" + str(scores), font, 0.75, 1)[0][0]
106
107             cv2.rectangle(image_np,(int(box[1] * w), int(box[0] * h-20)), ((int(box[1] * w)+size+5), int(box[0] * h)), colortable[classid],-1)
108             cv2.putText(image_np, str(classname) + ":" + str(scores),
109                     (int(box[1] * w), int(box[0] * h)-5), font, 0.75, (0,0,0), 1, 1)
110         else:
111             break
112     # Save the result image with bounding boxes and class labels in file system
113     cv2.imwrite(output_path+"/"+imagename, image_np)

Listing 6-13Drawing Bounding Boxes Around Detected Objects in Input Images

第 79 行定义了接受两个参数的函数infer_object():模型对象和输入图像的路径。

第 82 行只是获取第 110 行中使用的图像的文件名,并将结果图像以相同的名称存储到输出目录中。

第 84 行使用 OpenCV 读取图像,并将其转换为 NumPy 数组。

第 85 行调用函数run_inference_for_single_image(),向其传递模型对象和图像 NumPy。回想一下,函数run_inference_for_single_image()返回一个包含检测到的对象和边界框的字典。

输出字典可能包含多个对象和边界框。我们需要遍历这些对象,并在这些对象周围绘制边界框,这些对象的得分超过了阈值。在前面的代码示例中,第 13 行循环遍历每个检测到的对象类。输出字典中的分数按降序排序。因此,当分数小于阈值时,退出循环。

第 91 到 93 行简单地提取了三个重要的输出数组——边界框坐标、在这个边界框中检测到的对象类以及相关的预测分数——并将它们分配给相应的变量。

在第 91 行,变量box是一个包含边界框四个角的数组,如下所述:

  • box[0]是 y 坐标,box[0]是矩形边界框左上角的 x 坐标。

  • box[1]box[2]是边界框右下角的 y 和 x 坐标。

第 95 行检查分数是否大于阈值。在本例中,我们使用了阈值 0.5,但是您也可以使用适合您特定应用的值。仅当分数大于阈值时,才会在图像上绘制边界框;否则,将退出for循环。

回想一下,在将图像输入到模型中进行训练之前,会调整图像的大小。根据我们在培训中使用的pipeline.config中的高度和宽度设置来调整图像的大小。因此,预测的边界框也根据尺寸调整后的图像进行缩放。因此,我们需要根据用于检测的输入图像的原始大小来重新缩放边界框。将方框坐标乘以图像的高度和宽度,缩放图像大小的坐标。

第 101 行使用 OpenCV 的rectangle()函数绘制矩形边界框(查看章节 2 中的rectangle()函数)。注意,我们使用了colortable来动态地为不同的类获取不同的颜色。

第 105 行将预测的类名和相应的分数写在边界框的正上方。如果你愿意,你可以改变第 104 行的字体样式。在我们的例子中,文本的字体颜色和边界框的边框是相同的。您可以通过使用不同的值调用colortable函数来使用不同的颜色。例如,向类索引添加一个常数,并调用颜色表来表示文本颜色。

正如我们前面提到的,分数是按照最高分在数组顶部排序的。阈值之后的第一种情况的得分将打破循环,以避免不必要的处理。

第 113 行将结果图像保存到输出目录中,检测到的对象周围有边界框。

现在我们已经定义了所有正确的设置和函数,我们需要调用它们来触发检测过程。清单 6-14 向您展示了如何触发检测。

116  # Obtain the model object
117  detection_model = load_model(model_path)
118
119  # For each image, call the prediction
120  for image_path in IMAGE_PATHS:
121     infer_object(detection_model, image_path)

Listing 6-14Function Calls to Trigger the Detection Process

在清单 6-14 中,第 117 行通过将路径传递给训练好的模型来调用load_model()函数。该函数返回将在后续调用中使用的模型对象。

第 120 行遍历每个图像文件,并为每个图像调用infer_object()。为每幅图像调用函数infer_object(),将检测到的物体周围带有边界框的最终输出保存在输出目录中。

让我们把所有这些放在一起,看看完整的对象检测源代码。清单 6-15 是完整的工作代码。

Filename: Listing_6_15.py
1    import os
2    import pathlib
3    import random
4    import numpy as np
5    import tensorflow as tf
6    import cv2
7    # Import the object detection module.
8    from object_detection.utils import ops as utils_ops
9    from object_detection.utils import label_map_util
10
11   # to make gfile compatible with v2
12   tf.gfile = tf.io.gfile
13
14   model_path = "ssd_model/final_model"
15   labels_path = "models/research/object_detection/data/pet_label_map.pbtxt"
16   image_dir = "images"
17   image_file_pattern = "*.jpg"
18   output_path="output_dir"
19
20   PATH_TO_IMAGES_DIR = pathlib.Path(image_dir)
21   IMAGE_PATHS = sorted(list(PATH_TO_IMAGES_DIR.glob(image_file_pattern)))
22
23   # List of the strings that are used to add the correct label for each box.
24   category_index = label_map_util.create_category_index_from_labelmap(labels_path, use_display_name=True)
25   class_num =len(category_index)
26
27   def get_color_table(class_num, seed=0):
28      random.seed(seed)
29      color_table = {}
30      for i in range(class_num):
31          color_table[i] = [random.randint(0, 255) for _ in range(3)]
32      return color_table
33
34   colortable = get_color_table(class_num)

35
36   # # Model preparation and loading the model from the disk
37   def load_model(model_path):
38
39      model_dir = pathlib.Path(model_path) / "saved_model"
40      model = tf.saved_model.load(str(model_dir))
41      model = model.signatures['serving_default']
42      return model
43
44   # Predict objects and bounding boxes and format the result
45   def run_inference_for_single_image(model, image):
46
47      # The input needs to be a tensor, convert it using `tf.convert_to_tensor`.
48      input_tensor = tf.convert_to_tensor(image)
49      # The model expects a batch of images, so add an axis with `tf.newaxis`.
50      input_tensor = input_tensor[tf.newaxis, ...]
51
52      # Run prediction from the model
53      output_dict = model(input_tensor)
54
55      # Input to model is a tensor, so the output is also a tensor
56      # Convert to numpy arrays, and take index [0] to remove the batch dimension.
57      # We're only interested in the first num_detections.
58      num_detections = int(output_dict.pop('num_detections'))
59      output_dict = {key: value[0, :num_detections].numpy()
60                     for key, value in output_dict.items()}
61      output_dict['num_detections'] = num_detections
62
63      # detection_classes should be ints.
64      output_dict['detection_classes'] = output_dict['detection_classes'].astype(np.int64)
65
66      # Handle models with masks:
67      if 'detection_masks' in output_dict:
68          # Reframe the the bbox mask to the image size.
69          detection_masks_reframed = utils_ops.reframe_box_masks_to_image_masks(
70              output_dict['detection_masks'], output_dict['detection_boxes'],
71              image.shape[0], image.shape[1])
72          detection_masks_reframed = tf.cast(detection_masks_reframed > 0.5,
73                                             tf.uint8)
74          output_dict['detection_masks_reframed'] = detection_masks_reframed.numpy()
75
76      return output_dict
77
78
79   def infer_object(model, image_path):
80      # Read the image using openCV and create an image numpy
81      # The final output image with boxes and labels on it.
82      imagename = os.path.basename(image_path)
83
84      image_np = cv2.imread(os.path.abspath(image_path))
85      # Actual detection.
86      output_dict = run_inference_for_single_image(model, image_np)
87
88      # Visualization of the results of a detection.
89      for i in range(output_dict['detection_classes'].size):
90
91          box = output_dict['detection_boxes'][i]
92          classes = output_dict['detection_classes'][i]
93          scores = output_dict['detection_scores'][i]
94
95          if scores > 0.5:
96              h = image_np.shape[0]
97              w = image_np.shape[1]
98              classname = category_index[classes]['name']
99              classid =category_index[classes]['id']
100             #Draw bounding boxes
101             cv2.rectangle(image_np, (int(box[1] * w), int(box[0] * h)), (int(box[3] * w), int(box[2] * h)), colortable[classid], 2)
102
103             #Write the class name on top of the bounding box
104             font = cv2.FONT_HERSHEY_COMPLEX_SMALL
105             size = cv2.getTextSize(str(classname) + ":" + str(scores), font, 0.75, 1)[0][0]
106
107             cv2.rectangle(image_np,(int(box[1] * w), int(box[0] * h-20)), ((int(box[1] * w)+size+5), int(box[0] * h)), colortable[classid],-1)
108             cv2.putText(image_np, str(classname) + ":" + str(scores),
109                     (int(box[1] * w), int(box[0] * h)-5), font, 0.75, (0,0,0), 1, 1)
110         else:
111             break
112     # Save the result image with bounding boxes and class labels in file system
113     cv2.imwrite(output_path+"/"+imagename, image_np)
114     # cv2.imshow(imagename, image_np)
115
116  # Obtain the model object
117  detection_model = load_model(model_path)
118
119  # For each image, call the prediction
120  for image_path in IMAGE_PATHS:
121     infer_object(detection_model, image_path)

Listing 6-15Fully Working Code for Object Detection Using a Pretrained Model

图 6-28 显示了一些样本输出,其中检测到的对象包含在边界框内。

img/493065_1_En_6_Fig28_HTML.jpg

图 6-28

带有检测到的动物面孔和周围方框的输出图像示例

为对象检测训练 YOLOv3 模型

YOLOv3 是我们在本章中研究的所有对象检测算法中最年轻的。它还没有进入 TensorFlow 对象检测 API。YOLOv3 的作者约瑟夫·雷德蒙和阿里·法尔哈迪已经公开了他们的 API。他们还提供了基于 COCO 数据集的训练模型的权重。如本章 YOLOv3 一节所述,YOLOv3 使用 Darknet-53 架构来训练模型。

我们将使用官方 API 和预训练模型的权重,从我们在之前的 SSD 模型中使用的同一牛津-IIIT Pet 数据集来执行 YOLOv3 模型的迁移学习。我们将在 Google Colab 上运行培训,并使用 GPU 硬件加速器。

在我们开始之前,请登录您的 Google Colab 帐户并创建一个新项目。如果您遵循 SSD 培训流程,对您来说应该很容易。否则,请查看前面部分的 Google Colab 部分。我们开始吧!

安装 Darknet 框架

Darknet 是一个用 C 和 CUDA 编写的开源神经网络框架,可以在 CPU 和 GPU 上运行。首先,克隆 Darknet GitHub 存储库,然后构建源代码。清单 6-16 展示了如何在 Google Colab 笔记本中做到这一点。

1    %%shell
2    git clone https://github.com/ansarisam/darknet.git
3    # Official repository
4    #git clone https://github.com/pjreddie/darknet.git

Listing 6-16Cloning a Darknet Repository

第 2 行从我们的 GitHub 资源库中签出了 Darknet 项目,该项目是从官方的 Darknet 资源库派生出来的。如果您更愿意从官方存储库中下载它,取消对第 4 行和第 2 行的注释。

在存储库被克隆之后,展开文件浏览器,导航到darknet目录,并将Makefile下载到您的本地计算机。编辑Makefile(粗体字突出显示),更改GPU=1OPENCV=1,如下图所示:

  • GPU=1

  • 库丁烷=0

  • OPENCV=1

  • OPENMP=0

  • 调试=0

确保没有对Makefile,进行其他更改,否则您可能会在构建暗网代码时遇到麻烦。

完成前面的更改后,将Makefile上传到 Colab 的darknet目录。

现在我们已经准备好构建 Darknet 框架了。清单 6-17 显示了构建命令。

1    %%shell
2    cd darknet/
3    make

Listing 6-17Running the make Command to Build Darknet

构建过程成功完成后,运行清单 6-18 中的命令来测试您的安装。如果安装成功,应该会打印出usage: ./darknet <function>

1    %%shell
2    cd darknet
3    ./darknet

Listing 6-18Testing the Darknet Installation

下载预先训练的卷积权重

清单 6-19 下载在 Darknet-53 框架上训练的 COCO 数据集的预训练权重。

1    %%shell
2    mkdir pretrained
3    cd pretrained
4    wget https://pjreddie.com/media/files/darknet53.conv.74

Listing 6-19Downloading Pre-trained Darknet-53 Weights

下载带注释的牛津-IIIT Pet 数据集

清单 6-20 下载带有图像和注释的 pet 数据集。这已经在前面与 SSD 培训相关的章节中解释过了。

1    %%shell
2    mkdir petdata
3    cd petdata
4    wget http://www.robots.ox.ac.uk/~vgg/data/pets/data/images.tar.gz
5    wget http://www.robots.ox.ac.uk/~vgg/data/pets/data/annotations.tar.gz
6    tar -xvf images.tar.gz
7    tar -xvf annotations.tar.gz

Listing 6-20Downloading the Pet Dataset Images and Annotations

Note

images目录包含几个扩展名为.mat的文件,这会导致训练中断。清单 6-21 删除这些.mat文件。

1    %%shell
2    cd /content/petdata/images
3    rm *.mat

Listing 6-21Deleting the Invalid File Extension .mat

准备数据集

YOLOv3 训练 API 期望数据集具有特定的格式和目录结构。我们下载的 pet 数据有两个子目录:imagesannotations.``images目录包含所有我们将用于训练和测试的标记图像。annotations目录包含 XML 格式的注释文件,每个图像一个 XML 文件。

YOLOv3 需要以下文件:

  • train.txt:该文件包含图像的绝对路径——每行一个图像路径——将用于训练。

  • test.txt:该文件包含图像的绝对路径——每行一个图像路径——将用于测试。

  • class.data:该文件包含对象类的名称列表——每行一个名称。

  • labels:该目录与train.txttest.txt在同一个位置。这个labels目录包含注释文件,每个图像一个文件。该目录中的文件名必须与图像文件名相同,只是扩展名为.txt。例如,如果图像文件名为Abyssinian_1.jpg,则labels目录中的注释文件名必须为Abyssinian_1.txt。每个注释文本文件必须在一行中包含注释的边界框和对象类,格式如下:

<object-class> <x_center> <y_center> <width> <height>

在哪里

<object-class>是对象的整数类索引,从 0 到(num_class-1)。

<x_center><y_center>是浮点值,表示边界框相对于图像高度和宽度的中心。

<width> <height>是相对于图像高度和宽度的边界框的宽度和高度。

请注意,该文件中的条目由空格分隔,而不是由逗号或任何其他分隔符分隔。

注释文本文件的条目示例如下(确保字段由空格分隔,而不是逗号或任何其他分隔符。):

10 0.63 0.285000000000003 0.285000000000003 0.215

清单 6-22 将 pet 数据注释转换成 YOLOv3 要求的格式。这是标准的 Python 代码,不需要任何解释。

1    import os
2    import glob
3    import pandas as pd
4    import xml.etree.ElementTree as ET
5
6
7    def xml_to_csv(path, img_path, label_path):
8       if not os.path.exists(label_path):
9           os.makedirs(label_path)
10
11      class_list = []
12      for xml_file in glob.glob(path + '/*.xml'):
13          xml_list = []
14          tree = ET.parse(xml_file)
15          root = tree.getroot()
16          for member in root.findall('object'):
17              imagename = str(root.find('filename').text)
18              print("image", imagename)
19              index = int(imagename.rfind("_"))
20              print("index: ", index)
21              classname = imagename[0:index]
22
23              class_index = 0
24              if (class_list.count(classname) > 0):
25                  class_index = class_list.index(classname)
26
27              else

:
28                  class_list.append(classname)
29                  class_index = class_list.index(classname)
30
31              print("width: ", root.find("size").find("width").text)
32              print("height: ", root.find("size").find("height").text)
33              print("minx: ", member[4][0].text)
34              print("ymin:", member[4][1].text)
35              print("maxx: ", member[4][2].text)
36              print("maxy: ", member[4][3].text)
37              w = float(root.find("size").find("width").text)
38              h = float(root.find("size").find("height").text)
39              dw = 1.0 / w
40              dh = 1.0 / h
41              x = (float(member[4][0].text) + float(member[4][2].text)) / 2.0 - 1
42              y = (float(member[4][1].text) + float(member[4][3].text)) / 2.0 - 1
43              w = float(member[4][2].text) - float(member[4][0].text)
44              h = float(member[4][3].text) - float(member[4][1].text)
45              x = x * dw
46              w = w * dw
47              y = y * dh
48              h = h * dh
49
50              value = (class_index,
51                       x,
52                       y,
53                       y,
54                       h
55                       )
56              print("The line value is: ", value)
57              print("csv file name: ", os.path.join(label_path, imagename.rsplit('.', 1)[0] + '.txt'))
58              xml_list.append(value)
59              df = pd.DataFrame(xml_list)
60              df.to_csv(os.path.join(label_path, imagename.rsplit('.', 1)[0] + '.txt'), index=None, header=False, sep=' ')
61
62      class_df = pd.DataFrame(class_list)
63      return class_df
64
65
66   def create_training_and_test(image_dir, label_dir):
67      file_list = []
68      for img in glob.glob(image_dir + "/*"):
69          print(os.path.abspath(img))
70
71          imagefile = os.path.basename(img)
72
73          textfile = imagefile.rsplit('.', 1)[0] + '.txt'
74
75          if not os.path.isfile(label_dir + "/" + textfile):
76              print("delete image file ", img)
77              os.remove(img)
78              continue
79          file_list.append(os.path.abspath(img))
80
81      file_df = pd.DataFrame(file_list)
82      train = file_df.sample(frac=0.7, random_state=10)
83      test = file_df.drop(train.index)
84      train.to_csv("petdata/train.txt", index=None, header=False)
85      test.to_csv("petdata/test.txt", index=None, header=False)
86
87
88   def main():
89      img_dir = "petdata/images"
90      label_dir = "petdata/labels"
91
92      xml_path = os.path.join(os.getcwd(), 'petdata/annotations/xmls')
93      img_path = os.path.join(os.getcwd(), img_dir)
94      label_path = os.path.join(os.getcwd(), label_dir)
95
96      class_df = xml_to_csv(xml_path, img_path, label_path)
97      class_df.to_csv('petdata/class.data', index=None, header=False, delimiter=r"\s+")
98      create_training_and_test(img_dir, label_path)
99      print('Successfully converted xml to csv.')
100
101
102  main()

Listing 6-22Converting Image Annotations from XML to TXT

配置培训输入

我们需要一个包含训练集和测试集路径信息的配置文件。配置文件的格式如下:

classes= 37
train  = /content/petdata/train.txt
valid  = /content/petdata/test.txt
names = /content/petdata/class.data
backup = /content/yolov3_model

其中,classes变量获取我们的训练图像的对象类的数量(在我们的例子中是 37 个宠物类),trainvalid变量获取我们之前创建的训练和验证列表的路径,names获取包含类名的文件的路径,backup变量指向将保存已训练的 YOLO 模型的目录路径。请确保该目录存在,否则执行将引发异常。

保存该文本文件,并以扩展名.cfg命名。在我们的例子中,我们将这个文件保存为pet_input.cfg。然后,我们将把这个文件上传到目录路径/content/darknet/cfg中的 Colab。

配置暗网神经网络

从 Colab 下载来自/content/darknet/cfg/yolov3-voc.cfg的示例网络配置文件,并将其保存在您的本地计算机中。您可以将该文件重命名为与您的数据集相关的名称。例如,在本练习中,我们将其重命名为yolov3-pet.cfg

我们将编辑该文件以匹配我们的数据。我们将要编辑的文件中最重要的部分是yolo层。

在配置文件中搜索段[yolo]。应该有三层yolo。我们将编辑对象类的数量,在我们的例子中是 37。在这三个地方,我们都将班级人数改为 37 人。此外,我们将在所有三个地方的yolo层之前更改卷积层中的filters值。yolo层之前的卷积层中的filters的值由以下公式确定:

*过滤器=数量/3 (数量 _ 类别+5)

滤镜= (9/3) * (37 + 5) = 126

关于[yolo]部分之前的[yolo]部分和[convolutional]部分的示例,请参见以下代码:

....
[convolutional]
size=1
stride=1
pad=1
filters=126
activation=linear

[yolo]
mask = 0,1,2
anchors = 10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326
classes=37
num=9
jitter=.3
ignore_thresh = .5
truth_thresh = 1
random=1
...

确保在配置文件中的三个地方更改了classesfilters的值。

我们将编辑的其他参数如下:

  • width=416,输入图像的宽度。所有图像都将调整到这个宽度。

  • height=416,输入图像的高度。所有图像都将调整到这个高度。

  • batch=64,表示我们希望权重更新的频率。

  • subdivisions=16,表示如果 GPU 没有足够大的内存来加载与批处理大小相等的数据示例,将在内存中加载多少个示例。如果您在执行培训时看到“内存不足”异常,请调整这个数字并逐渐减小它,直到您看不到内存错误。

  • max_batches=74000,表示训练应该运行多少批次。如果设置过高,训练可能需要很长时间才能完成。如果太低,网络学习不够。实际上,已经确定max_batch的大小应该是类数量的 2000 倍。在我们的例子中,我们有 37 个类,所以max_batch值应该是 2000×37 = 74000。如果只有一个类,将max_batches的值设置为最小值 4000。

保存配置文件,然后上传到cfg目录路径:/content/darknet/cfg.

训练 YOLOv3 模型

使用清单 6-23 中的命令执行 YOLOv3 训练。

1    %%shell
2    cd darknet/
3    ./darknet detector train cfg/pet_input.cfg cfg/yolov3-pet.cfg /content/pretrained/darknet53.conv.74

Listing 6-23Training the YOLOv3 Model

如清单 6-23 所示,训练的参数是到pet_input.cfgyolov3-pet.cfg的路径,以及预训练的暗网模型。

如果一切顺利,您将在配置中指定的目录路径中拥有一个经过训练的模型,其中backup被设置为/content/yolov3_model.。当网络正在学习时,它会将中间权重作为检查点保存在backup目录中。

在训练过程中观察控制台输出。你会注意到三条重要的线显示了三个区域的平均 IOU,82、94 和 106(如图 6-29 所示)。

img/493065_1_En_6_Fig29_HTML.png

图 6-29

YOLOv3 培训期间的控制台输出示例(输出仅显示 500 次迭代,这对于真实模型来说通常是不够的)

这三个区域意味着暗网框架中的 YOLO 层 82、层 94 和层 106。你也可能观察到部分地区的 IOU 为-nan,这是完全正常的。经过几次迭代后,区域 IOU 将开始显示数字。

注意到图 6-29 中样本输出的第一个数字是 499,这表明完成了 499 个批次的训练,批次水平损失为 4.618134,总平均损失为 4.148183,学习率为 0.000062,完成该批次花费了 13.985329 秒。这将让您了解完成培训需要多长时间。损失值给出了学习进行得有多好的想法。

请注意最后三行,它们是在培训完全结束时打印出来的。它显示了保存检查点、中间重量和最终重量的位置。

您应该将包含最终模型的整个目录复制到您的私有 Google Drive,这样您就可以在您的应用中使用经过训练的模型。

训练进行时,控制台会打印大量信息,显示在 web 浏览器中。过一会儿,web 浏览器变得没有响应。清除控制台输出可能是一个好主意,以防止浏览器被杀死。要清除日志输出,请单击位于笔记本单元块左上角的执行按钮正下方的 X 按钮。在训练运行时,您会看到三个点,悬停时,它会变成一个 X 按钮。

培训应该持续多长时间

一般来说,每个类的训练应该至少运行 2,000 次迭代,但是总共不少于 4,000 次迭代。在我们的 pet 数据集的例子中,我们有 37 个类。这意味着我们应该将max_batches设置为 74000。

在训练过程中观察输出,并注意每次迭代后的损失。如果损失稳定下来,并且在几个批次中没有变化,我们应该考虑停止训练。理想情况下,损失应该接近于零。然而,出于最实际的目的,我们的目标应该是将损耗稳定在 0.05 以下。

最终模型

网络完成学习后,最终的 YOLOv3 模型将保存在目录/content/yolov3_model.中,模型文件的名称为yolov3-pet_final.weights

下载此模型或将其保存到您的私人 Google Drive 文件夹中,因为当会话到期时,Google Colab 会删除您的所有文件。我们将在图像和视频中的实时对象检测中使用该模型。

使用训练的 YOLOv3 模型检测对象

我们将编写一些 Python 代码,并在本地计算机中执行对象检测,就像我们在 SSD 中所做的那样。我们将使用从 Google Colab 下载的训练模型(参见“最终模型”部分)。

让我们从在 PyCharm 中设置我们的开发环境开始。

将 Darknet 安装到本地计算机

使用以下步骤在本地计算机上安装并构建 Darknet 框架:

img/493065_1_En_6_Fig31_HTML.png

图 6-31

使用 make 命令生成源代码

img/493065_1_En_6_Fig30_HTML.png

图 6-30。命令来显示目录结构并克隆 GitHub 存储库

  1. 打开命令提示符、shell 终端或 PyCharm 的终端,并cd到您想要安装 Darknet 框架的目录。确保你处于我们在第一章中创建的同一虚拟环境中。

  2. 克隆 GitHub 库, https://github.com/ansarisam/darknet.git .这个库是从原来的 darknet 库中派生出来的, https://github.com/pjreddie/darknet .我们在 C 代码中做了一些修改(在src/image.c中)来生成输出中的边界框。此外,我们还提供了一个 Python 脚本yolov3_detector.py,用于预测对象和边界框,然后将输出保存为 JSON。见图 6-30

  3. 源代码克隆完成后,编辑位于darknet目录下的Makefile。如果你使用的是 GPU,设置GPU=1并保存文件。如果您正在使用 CPU,请不要对此Makefile进行任何更改。

  4. 使用make命令构建 C 源代码。只需从darknet目录中键入命令,如图 6-31 所示。

    如果一切都运行成功,那么您就已经为对象检测准备好了 PyCharm 环境。

  5. 通过从darknet目录键入命令./darknet来测试安装。该命令应该打印出类似于usage.: ./darknet <function>的输出。

用于对象检测的 Python 代码

清单 6-24 提供了 Python 代码来检测图像中的对象。

1    import os
2    import subprocess
3    import pandas as pd
4    image_path="test_images/dog.jpg"
5    yolov3_weights_path="backup/yolov3.weights"
6    cfg_path="cfg/yolov3.cfg"
7    output_path="output_path"
8    image_name = os.path.basename(image_path)
9    process = subprocess.Popen(['./darknet', 'detect', cfg_path, yolov3_weights_path, image_path],
10                       stdout=subprocess.PIPE,
11                       stderr=subprocess.PIPE)
12   stdout, stderr = process.communicate()
13
14   std_string = stdout.decode("utf-8")
15   std_string = std_string.split(image_path)[1]
16   count = 0
17   outputList = []
18   rowDict = {}
19   for line in std_string.splitlines():
20
21      if count > 0:
22          if count%2 > 0:
23              obj_score = line.split(":")
24              obj = obj_score[0]
25              score = obj_score[1]
26              rowDict["object"] = obj
27              rowDict["score"] = score
28          else:
29              bbox = line.split(",")
30              rowDict["bbox"] = bbox
31              outputList.append(rowDict)
32              rowDict = {}
33      count = count +1
34   rowDict["image"] = image_path
35   rowDict["predictions"] = outputList
36
37   df = pd.DataFrame(rowDict)
38   df.to_json(output_path+"/"+image_name.replace(".jpg", ".json").replace(".png", ".json"),orient='records')

Listing 6-24Object Detection with Results Stored as JSON to Output Location

1 至 3 号线是我们通常的进口货。

第 4 行设置需要检测对象的图像位置的路径。

第 5 行设置了训练模型的权重的路径(从 Colab 下载)。

第 6 行设置了我们用于训练的暗网神经网络配置。

第 7 行是输出位置,其中包含检测到的对象、相关分数和封闭边界框的最终结果以 JSON 格式保存。

第 9 行到第 12 行执行一个 shell 命令,并将输出和错误传递给stdoutstderr变量。我们使用的subprocess包产生了新的进程,连接到它们的输入/输出/错误流水线,并获得它们的返回代码。子流程返回的输出和错误以字节为单位。因此,我们在第 13 行将输出字节转换成 UTF-8 编码的字符串。

在幕后,该子进程执行以下 shell 命令:

./darknet detect <cfg_path> <yolov3_model_weights_path> <image_path>

您可以从终端的darknet目录中直接执行这个命令。该命令将在控制台上打印大量信息,如网络配置、检测到的对象、检测分数和边界框。

第 15 到 35 行将输出解析成结构化的 JSON 格式。最终输出包含图像路径、对象类的预测列表、边界框的坐标以及相关的分数。边界框坐标的格式为[left, top, right, bottom]

第 37 行使用 Pandas 创建一个数据帧,第 38 行将数据帧以 JSON 格式保存到输出位置。

图 6-32 显示了通过从图 6-33 所示的图像中预测对象而创建的 JSON 格式的样本输出。

img/493065_1_En_6_Fig33_HTML.png

图 6-33

YOLOv3 预测器的 JSON 输出

img/493065_1_En_6_Fig32_HTML.jpg

图 6-32

包含要从 YOLOv3 模型中检测的对象的原始图像

摘要

在本章中,我们学习了不同的目标检测算法,以及它们在检测速度和准确性方面的相互比较。我们训练了两个检测模型,SSD 和 YOLOv3,并从头到尾经历了从摄取数据到保存预测输出的过程。

我们还学习了如何使用 Google Colab 在云上训练检测模型,并使用 GPU 的力量。

在这一章中,我们主要关注于检测图像中的物体,并没有涉及视频的例子。检测视频中的对象的过程类似于图像中的检测,因为视频只是图像的帧。第七章专门讨论这个话题。然后我们将把本章提出的概念应用到第 9 和 10 章,利用深度学习开发计算机视觉的真实世界用例。******

七、实际例子:视频中的对象跟踪

本章的重点是计算机视觉的两个关键能力:目标检测和目标跟踪。一般而言,在一组图像的背景下,对象检测提供了识别图像中的一个或多个对象的能力,而对象跟踪提供了在一组图像上跟踪检测到的对象的能力。在之前的章节中,我们探讨了训练深度学习模型以检测对象的技术方面。在这一章中,我们将探索一个简单的例子,将这些知识应用到视频中。

视频中的对象跟踪,或简称为视频跟踪,涉及检测和定位对象并随时间跟踪它。视频跟踪不仅要检测不同帧中的目标,还要跨帧跟踪目标。当第一次检测到一个对象时,它的唯一身份被提取,然后在后续的帧中被跟踪。

对象跟踪在现实世界中有许多应用,例如:

  • 自动驾驶汽车

  • 安全和监控

  • 交通控制

  • 增强现实

  • 犯罪侦查和犯罪追踪

  • 医学成像等

在这一章中,我们将学习如何实现视频跟踪,并完成代码示例。在本章结束时,你将拥有一个功能齐全的视频跟踪系统。

我们的高级实施计划如下:

  1. 视频源:我们将使用 OpenCV 从网络摄像头或笔记本电脑的内置摄像头读取实时视频流。您也可以从文件或 IP 摄像头读取视频。

  2. 对象检测模型:我们将使用在 COCO 数据集上预先训练的 SSD 模型。您可以为您的特定用例训练您自己的模型(查看第六章了解关于训练对象检测模型的信息)。

  3. 预测:我们将预测视频每一帧内的物体类别(检测)及其包围盒(定位)(查看第六章了解检测图像中物体的信息)。

  4. 唯一标识:我们将使用散列算法来创建每个对象的唯一标识。我们将在本章后面了解更多关于散列算法的知识。

  5. 跟踪:我们将使用汉明距离算法(本章稍后会详细介绍)来跟踪之前检测到的物体。

  6. Display :我们将流式输出视频,以便在 web 浏览器中显示。我们将为此使用烧瓶。Flask 是一个轻量级的 web 应用微框架。

准备工作环境

让我们建立一个目录结构,这样就可以很容易地遵循代码并完成下面的例子。我们将看到前面描述的六个步骤的代码片段。最后,我们将把所有的东西放在一起,使目标跟踪系统完整和可行。

我们有一个名为video_tracking的目录。其中有一个名为templates的子目录,里面有一个名为index.html的 HTML 文件。子目录templates是 Flask 查找 HTML 页面的标准位置。在video_tracking目录中,我们有四个 Python 文件:videoasync.pyobject_tracker.pytracker.pyvideo_server.py。图 7-1 显示了该目录结构。

img/493065_1_En_7_Fig1_HTML.jpg

图 7-1

代码目录结构

我们将把videoasync作为一个模块导入到object_tracker.py中。因此,目录video_tracking必须被认为是 PyCharm 中的源目录。要使其成为 PyCharm 中的源目录,点击屏幕左上方的 PyCharm 菜单选项,然后点击 Preferences,展开左侧面板中的 Project,点击 Project Structure,高亮显示video_tracking目录,点击 Mark as Source(位于屏幕上方),如图 7-2 所示。最后,单击确定关闭窗口。

img/493065_1_En_7_Fig2_HTML.png

图 7-2

在 PyCharm 中将目录标记为源

读取视频流

OpenCV 提供了连接视频源和从视频帧中读取图像的便捷方法。OpenCV 在内部将这些帧中的图像转换成 NumPy 数组。这些 NumPy 数组被进一步处理以检测和跟踪其中的对象。检测过程是计算密集型的,它可能跟不上读取帧的速度。因此,在主线程中读取帧和执行检测操作会表现出较低的性能,尤其是在处理高清(HD)视频时。在清单 7-1 中,我们将实现多线程来捕捉帧。我们称之为视频帧的异步读取

1    # file: videoasync.py
2    import threading
3    import cv2
4
5    class VideoCaptureAsync:
6       def __init__(self, src=0):
7           self.src = src
8           self.cap = cv2.VideoCapture(self.src)
9           self.grabbed, self.frame = self.cap.read()
10          self.started = False
11          self.read_lock = threading.Lock()
12
13      def set(self, key, value):
14          self.cap.set(key, value)
15
16      def start(self):
17          if self.started:
18              print('[Warning] Asynchronous video capturing is already started.')
19              return None
20          self.started = True
21          self.thread = threading.Thread(target=self.update, args=())
22          self.thread.start()
23          return self
24
25      def update(self):
26          while self.started:
27              grabbed, frame = self.cap.read()
28              with self.read_lock:
29                  self.grabbed = grabbed
30                  self.frame = frame
31
32      def read(self):
33          with self.read_lock:
34              frame = self.frame.copy()
35              grabbed = self.grabbed
36          return grabbed, frame
37
38      def stop(self):
39          self.started = False
40         self.thread.join()
41
42
43
44      def __exit__(self, exec_type, exc_value, traceback):
45          self.cap.release()

Listing 7-1Implementation of Asynchronous Reading of Video Frames

文件videoasync.py实现了类VidoCaptureAsync(第 5 行),它由一个构造函数和启动线程、读取帧和停止线程的函数组成。

第 6 行定义了一个将视频源作为参数的构造函数。该源的默认值src=0(也称为设备索引)代表来自笔记本电脑内置摄像头的输入。如果您有 USB 摄像头,请相应地设置此src的值。如果您的计算机端口上连接了多个摄像机,则没有标准的方法来查找设备索引。一种方法是从起始索引 0 开始循环,直到连接到设备。您可以打印设备属性来标识您想要连接的设备。对于基于 IP 的摄像机,传递 IP 地址或 URL。

如果您的视频源是一个文件,请传递视频文件的路径。

第 8 行使用 OpenCV 的VideoCapture()函数,并传递源 ID 来连接视频源。分配给self.cap变量的VideoCapture对象用于读取帧。

第 9 行读取第一帧,并占用与摄像机的连接。

第 10 行是用于管理锁的标志。第 11 行实际上获得了线程锁。

第 13 行和第 14 行实现了一个函数来设置VideoCapture对象的属性,比如帧高、宽度和每秒帧数(FPS)。

第 16 到 23 行实现了启动线程异步读取帧的函数。

第 25 到 30 行实现了一个update()函数来读取帧并更新类级别的帧变量。更新函数在第 21 行的开始函数中内部使用,以异步读取视频帧。

第 32 到 36 行实现了read()函数。read()功能只是返回在update()功能块中更新的帧。这还会返回一个布尔值来指示该帧是否被成功读取。

第 38 到 40 行实现了stop()函数来停止线程并将控制权返回给主线程。join()函数防止主线程关闭,直到子线程完成它的执行。

在退出时,视频源被释放(第 45 行)。

我们现在将编写代码来利用异步视频读取模块。在同一个目录video_tracking中,我们将创建一个名为object_tracker.py的 Python 文件,它实现了以下功能。

加载对象检测模型

我们将使用我们在第六章中使用的相同的预训练 SSD 模型来检测图像中的对象。如果您已经根据自己的图像训练了一个模型,则可以使用该模型。您所要做的就是提供模型目录的路径。清单 7-2 显示了如何从磁盘加载训练好的模型。回想一下,这是我们在第六章的清单 6-11 中使用的同一个函数。我们将只加载模型一次,并使用它来检测所有帧中的对象。

43   # # Model preparation
44   def load_model(model_path):
45      model_dir = pathlib.Path(model_path) / "saved_model"
46      model = tf.saved_model.load(str(model_dir))
47      model = model.signatures['serving_default']
48      return model
49
50   model = load_model(model_path)

Listing 7-2load_model() Function to Load Trained Model from the Disk

检测视频帧中的对象

探测物体的代码和我们在第六章中使用的代码几乎一样。不同之处在于,这里我们创建了一个无限循环,在这个循环中,我们一次读取一个图像帧,并调用函数track_object()来跟踪该帧中的对象。track_object()函数内部调用我们在第六章的清单 6-12 中实现的同一个run_inference_for_single_image()函数。

run_inference_for_single_image()函数的输出是一个包含detection_classesdetection_boxesdetection_scores的字典。我们将利用这些值来计算每个对象的唯一身份,并跟踪它们的位置。

清单 7-3 显示了streamVideo()函数,该函数实现了从视频源读取流帧的无限循环。

在清单 7-3 中,第 115 行启动了streamVideo()函数的块。第 116 行使用了带有线程锁的关键字global

第 117 行开始无限的while循环。在这个循环中,第一行,第 118 行,通过调用VideoCaptureAsync类的read()函数读取当前的视频帧(图像)。read()函数返回一个指示帧是否被成功读取的布尔值元组和一个图像帧的 NumPy 数组。

如果帧被成功检索(第 119 行),则获取锁(第 120 行),以便在当前线程的图像仍在被检测对象时,其他线程不会修改帧编号。

第 121 行通过传递模型对象和框架 NumPy 来调用track_object()函数。我们将在后面的清单 7-13 中看到这个track_object()函数的作用。在第 123 行,输出的 NumPy 数组被转换成压缩的.jpg图像,因此它是轻量级的,易于在网络上传输。我们使用cv2.imencode()将 NumPy 数组转换成图像。此函数返回一个布尔值元组,指示转换是否成功,并返回编码图像。

如果图像转换不成功,则跳过该帧(第 125 行)。

最后,在第 127 行,它产生了字节编码的图像。yield关键字从while循环返回一个只读一次的迭代器。

第 130 到 137 行是当程序被终止或者屏幕被按 Q 键退出时的清理函数。

114  # Function to implement infinite while loop to read video frames and generate the output   #for web browser
115  def streamVideo():
116     global lock
117     while (True):
118         retrieved, frame = cap.read()
119         if retrieved:
120             with lock:
121                 frame = track_object(model, frame)
122
123                 (flag, encodedImage) = cv2.imencode(".jpg", frame)
124                 if not flag:
125                     continue
126
127                 yield (b'--frame\r\n' b'Content-Type: img/jpeg\r\n\r\n' +
128                    bytearray(encodedImage) + b'\r\n')
129
130         if cv2.waitKey(1) & 0xFF == ord('q'):
131             cap.stop()
132             cv2.destroyAllWindows()
133             break
134
135     # When everything done, release the capture
136     cap.stop()
137     cv2.destroyAllWindows()

Listing 7-3Implementing Infinite Loop for Reading Streams of Video Frames and Internally Calling an Object Tracking Function for Each Frame

使用 dHash 为对象创建唯一标识

我们使用感知散列来创建在图像中检测到的对象的唯一身份。差分哈希,简称 dHash,是计算图像唯一哈希最常用的算法之一。dHash 提供了几个优点,使其成为识别和比较图像的合适选择。以下是使用 dHash 的一些好处:

  • 如果长宽比改变,图像哈希不会改变。

  • 亮度或对比度的变化将不会改变图像散列或稍微改变它。这意味着哈希仍然以不同的对比度接近其他哈希。

  • dHash 的计算速度非常快。

我们不使用加密哈希,如 MD-5 或 SHA-1。原因是,对于这些哈希算法,如果映像中有微小的变化,加密哈希将完全不同。即使是单个像素的变化,也会产生完全不同的哈希。因此,如果两个图像在感知上相似,它们的加密哈希将完全不同。这使得当我们必须比较两幅图像时,它不适合应用。

dHash 算法很简单。以下是计算 dHash 的步骤:

  1. 将图像或图像片段转换为灰度。这使得计算速度更快,如果颜色有轻微的变化,dHash 也不会改变太多。在对象检测中,我们使用边界框裁剪检测到的对象,并将裁剪后的图像转换为灰度。

  2. 调整灰度图像的大小。为了计算 64 位哈希,图像被调整为 9×8 像素,忽略其纵横比。纵横比被忽略,以确保得到的图像散列将匹配相似的图像,而不管它们的初始空间尺寸。

    为什么是 9×8 像素?在 dHash 中,该算法计算相邻像素的梯度差。9 行与相邻行的差异将在结果中仅产生 8 行,从而产生具有 8×8 像素的最终输出,这将给出 64 位散列。

  3. 通过应用“大于”公式将每个像素转换为 0 或 1 来构建哈希,如下所示:

如果 P[x=1] > P[x],那么 1 否则 0。

然后,二进制值被转换为整数哈希。

清单 7-4 展示了 dHash 的 Python 和 OpenCV 实现。

32      def getCropped(self, image_np, xmin, ymin, xmax, ymax):
33          return image_np[ymin:ymax, xmin:xmax]
34
35      def resize(self, cropped_image, size=8):
36          resized = cv2.resize(cropped_image, (size+1, size))
37          return resized
38
39      def getHash(self, resized_image):
40          diff = resized_image[:, 1:] > resized_image[:, :-1]
41          # convert the difference image to a hash
42          dhash = sum([2 ** i for (i, v) in enumerate(diff.flatten()) if v])
43          return int(np.array(dhash, dtype="float64"))

Listing 7-4Calculating the dHash from an Image

第 32 行和第 33 行实现了裁剪功能。我们传递完整图像帧的 NumPy 数组和围绕对象的边界框的四个坐标。该功能裁剪图像中包含检测到的对象的部分。

第 35 到 37 行用于将裁剪后的图像调整为 9×8 的大小。

第 39 到 43 行实现了 dHash 的计算。第 40 行通过应用前面描述的大于规则找到相邻像素的差异。第 42 行从二进制位值构建数字散列。第 43 行将散列转换成整数,并从函数中返回 dhash。

利用汉明距离确定图像相似度

汉明距离通常用于比较两个哈希。汉明距离测量两个散列中不同比特的数量。

如果两个散列的汉明距离为零,则意味着这两个散列是相同的。汉明距离越低,两个哈希越相似。

清单 7-5 展示了如何计算两个散列之间的汉明距离。

45      def hamming(self, hashA, hashB):
46          # compute and return the Hamming distance between the integers
47          return bin(int(hashA) ^ int(hashB)).count("1")

Listing 7-5Calculation of the Hamming Distance

第 45 行的函数hamming()将两个散列作为输入,并返回位数,这两个输入散列中的位数是不同的。

目标跟踪

在图像中检测到对象后,通过计算包含该对象的图像的裁剪部分的 dHash 来创建其唯一身份。通过计算物体的 dHash 的汉明距离,从一帧到另一帧跟踪物体。跟踪有许多用例。在我们的示例中,我们创建了两个跟踪函数来完成以下任务:

  1. 从对象在一帧中的第一次出现到后续帧中的所有出现,跟踪对象的路径。该函数跟踪边界框的中心,并绘制一条连接所有这些中心的线或路径。清单 7-6 展示了这个实现。函数createHammingDict()获取当前对象的轮廓、边界框的中心以及所有对象及其中心的历史。该函数将当前对象的数据与迄今为止看到的所有数据进行比较,并使用汉明距离来查找相似的对象,以跟踪其运动或路径。

  2. 获取对象的唯一标识符,并跟踪检测到的唯一对象的数量。清单 7-7 实现了一个名为getObjectCounter()的函数,它计算跨帧检测到的唯一对象的数量。它将当前对象的 dHash 与所有先前帧中计算的所有 dHash 进行比较。

49      def createHammingDict(self, dhash, center, hamming_dict):
50          centers = []
51          matched = False
52          matched_hash = dhash
53          # matched_classid = classid
54
55          if hamming_dict.__len__() > 0:
56              if hamming_dict.get(dhash):
57                  matched = True
58
59              else:
60                  for key in hamming_dict.keys():
61
62                      hd = self.hamming(dhash, key)
63
64                      if(hd < self.threshold):
65                          centers = hamming_dict.get(key)
66                          if len(centers) > self.max_track_frame:
67                              centers.pop(0)
68                          centers.append(center)
69                          del hamming_dict[key]
70                          hamming_dict[dhash] = centers
71                          matched = True
72                          break
73
74          if not matched:
75              centers.append(center)
76              hamming_dict[dhash] = centers
77
78          return  hamming_dict

Listing 7-6Tracking the Centers of Bounding Boxes of Detected Objects Between Multiple Frames

79
80      def getObjectCounter(self, dhash, hamming_dict):
81          matched = False
82          matched_hash = dhash
83          lowest_hamming_dist = self.threshold
84          object_counter = 0
85
86          if len(hamming_dict) > 0:
87              if dhash in hamming_dict:
88                  lowest_hamming_dist = 0
89                  matched_hash = dhash
90                  object_counter = hamming_dict.get(dhash)
91                  matched = True
92
93              else:
94                  for key in hamming_dict.keys():
95                      hd = self.hamming(dhash, key)
96                      if(hd < self.threshold):
97                          if hd < lowest_hamming_dist:
98                              lowest_hamming_dist = hd
99                              matched = True
100                             matched_hash = key
101                             object_counter = hamming_dict.get(key)
102         if not matched:
103             object_counter = len(hamming_dict)
104         if matched_hash in hamming_dict:
105             del hamming_dict[matched_hash]
106
107         hamming_dict[dhash] = object_counter
108         return  hamming_dict
109

Listing 7-7Function to Track Count of Unique Objects Detected in Video Frames

在网络浏览器中显示实况视频流

我们将把我们的视频跟踪代码发布到 Flask,一个轻量级的 web 框架。这将允许我们使用 URL 在 web 浏览器中查看带有跟踪对象的视频直播流。您可以使用其他框架,比如 Django,来发布可以从 web 浏览器访问的视频。我们选择 Flask 作为我们的示例,因为它是轻量级的、灵活的,并且只需要几行代码就可以轻松实现。

让我们探索一下如何在我们当前的上下文中使用 Flask。我们将从安装 Flask 到我们的 virtualenv 开始。

安装烧瓶

我们将使用pip命令来安装 Flask。确保激活 virtualenv 并执行命令pip install flask,如下所示:

 (cv_tf2) computername:~ username$ pip install flask

烧瓶目录结构

参见图 7-1 中的目录结构。我们在video_tracking目录中创建了一个名为templates的子目录。我们将创建一个 HTML 文件,index.html,它将包含显示视频流的代码。我们将把index.html保存到templates目录中。目录名必须是templates,因为 Flask 会在这个目录中查找 HTML 文件。

用于显示视频流的 HTML

清单 7-8 显示了保存在index.html页面中的 HTML 代码。第 7 行是显示实时视频流的最重要的一行。这是 HTML 的标准<img>标签,通常用于在 web 浏览器中显示图像。第 7 行代码的{{...}}部分是 Flask 符号,指示 Flask 从一个 URL 加载图像。当这个 HTML 页面被加载时,它将调用/video_feed URL 并从那里获取图像以显示在<img>标签中。

1    <html>
2     <head>
3       <title>Computer Vision</title>
4     </head>
5     <body>
6       <h1>Video Surveillance</h1>
7       <img src="{{ url_for('video_feed') }}" > </img>
8     </body>
9    </html>
10

Listing 7-8HTML Code for Displaying the Video Stream

现在我们需要一些服务于这个 HTML 页面的服务器端代码。我们还需要一个服务器端实现来在调用/video_feed URL 时提供图像。

我们将在一个单独的 Python 文件video_server.py中实现这两个函数,这个文件保存在video_tracking目录中。确保这个video_server.py文件和templates目录在同一个父目录下。

清单 7-9 展示了 Flask 服务的服务器端实现。2 号线进口烧瓶及其相关包装。第 3 行导入了我们的object_tracker包,它实现了对象检测和跟踪。

第 4 行使用构造函数app = Flask(__name__)创建了一个 Flask 应用,它将当前模块作为参数。通过调用构造函数,我们实例化 Flask web 应用框架,并将其赋给一个名为app的变量。我们将把所有的服务器端服务绑定到这个app

所有 Flask 服务都是通过 URL 提供的,我们必须将 URL 或路由绑定到它将提供的服务。下面是我们需要为我们的示例实现的两个服务:

  • 将从主页 URL 呈现index.html的服务,例如http://localhost:5019/

  • 将从/video_feed URL 提供视频流的服务,例如http://localhost:5019/video_feed

用于加载 HTML 页面的 Flask

清单 7-9 的第 6 行有一个路由绑定/,它表示 home URL。当从 web 浏览器调用 home URL 时,调用函数index()来服务请求(第 7 行)。index()函数只是从模板index.html中呈现一个 HTML 页面,我们在清单 7-8 中创建了这个模板。

提供视频流的烧瓶

清单 7-9 的第 11 行将/video_feed URL 绑定到 Python 函数video_feed()。这个函数反过来调用我们实现的用于检测和跟踪视频中物体的streamVideo()函数。第 15 行从视频帧中创建Response对象,并向调用者发送一个多部分 HTTP 响应。

1    # video_server.py
2    from flask import Flask, render_template, Response
3    import object_tracker as ot
4    app = Flask(__name__)
5
6    @app.route("/")
7    def index():
8       # return the rendered template
9    return render_template("index.html")
10
11   @app.route("/video_feed")
12   def video_feed():
13       # return the response generated along with the specific media
14       # type (mime type)
15       return Response(ot.streamVideo(),mimetype = "multipart/x-mixed-replace; boundary=frame")
16
17   if __name__ == '__main__':
18       app.run(host="localhost", port="5019", debug=True,
19                    threaded=True, use_reloader=False)
20

Listing 7-9Flask Server-Side Code to Launch index.html and Serve Video Stream

运行 Flask 服务器

通过从video_tracking目录键入命令python video_server.py从终端执行video_server.py文件。确保您已经激活了 virtualenv。

(cv) computername:video_tracking username$ python video_server.py

这将启动 Flask 服务器并在host="localhost"port="5019"上运行(清单 7-9 的第 18 行)。您应该为您的生产环境更改主机和端口。此外,通过在第 18 行设置debug=False来关闭调试模式。

当服务器启动时,将您的网络浏览器指向 URL http://localhost:5019/以查看带有对象跟踪的实时视频流。

把所有的放在一起

我们已经探索了视频跟踪系统的构建模块。让我们把它们放在一起,形成一个功能齐全的系统。图 7-3 显示了我们的视频跟踪系统的高级函数调用序列。

img/493065_1_En_7_Fig3_HTML.jpg

图 7-3

视频跟踪系统的功能调用序列示意图

当使用 URL http://localhost:5019/启动 web 浏览器时,Flask 后端服务器服务于index.html页面,该页面在内部调用调用服务器端函数video_feed()的 URL http://localhost:5019/video_feed。其余的函数调用,如图 7-3 所示,完成后将检测到物体的视频帧及其跟踪信息发送到网页浏览器显示。清单 7-10 到 7-14 提供了视频跟踪系统的完整源代码。

清单 7-10 的文件路径为video_tracking/templates/index.html

<html>
 <head>
   <title>Computer Vision</title>
 </head>
 <body>
   <h1>Video Surveillance</h1>
   <img src="{{ url_for('video_feed') }}" > </img>
 </body>
</html>

Listing 7-10index.html

清单 7-11 的文件路径为video_tracking/video_server.py

# video_server.py
from flask import Flask, render_template, Response
import object_tracker as ot
app = Flask(__name__)

@app.route("/")
def index():
   # return the rendered template
  return render_template("index.html")

@app.route("/video_feed")
def video_feed():
  # return the response generated along with the specific media
  # type (mime type)
  return Response(ot.streamVideo(),mimetype = "multipart/x-mixed-replace; boundary=frame")

if __name__ == '__main__':
  app.run(host="localhost", port="5019", debug=True,
        threaded=True, use_reloader=False)

Listing 7-11video_server.py

清单 7-12 的文件路径为video_tracking/object_tracker.py

import os
import pathlib
import random
import numpy as np
import tensorflow as tf
import cv2
import threading

# Import the object detection module.
from object_detection.utils import ops as utils_ops
from object_detection.utils import label_map_util

from videoasync import VideoCaptureAsync
import tracker as hasher

lock = threading.Lock()

# to make gfile compatible with v2
tf.gfile = tf.io.gfile

model_path = "./../model/ssd_inception_v2_coco_2018_01_28"
labels_path = "./../model/mscoco_label_map.pbtxt"

# List of the strings that is used to add correct label for each box

.
category_index = label_map_util.create_category_index_from_labelmap(labels_path, use_display_name=True)
class_num =len(category_index)+100
object_ids = {}
hasher_object = hasher.ObjectHasher()

#Function to create color table for each object class
def get_color_table(class_num, seed=50):
   random.seed(seed)
   color_table = {}
   for i in range(class_num):
       color_table[i] = [random.randint(0, 255) for _ in range(3)]
   return color_table

colortable = get_color_table(class_num)

# Initialize and start the asynchronous video capture thread
cap = VideoCaptureAsync().start()

# # Model preparation
def load_model(model_path):
   model_dir = pathlib.Path(model_path) / "saved_model"
   model = tf.saved_model.load(str(model_dir))
   model = model.signatures['serving_default']
   return model

model = load_model(model_path)

# Predict objects and bounding boxes and format the result
def run_inference_for_single_image(model, image):
   # The input needs to be a tensor, convert it using `tf.convert_to_tensor`.
   input_tensor = tf.convert_to_tensor(image)
   # The model expects a batch of images, so add an axis with `tf.newaxis`.
   input_tensor = input_tensor[tf.newaxis, ...]

   # Run prediction from the model
   output_dict = model(input_tensor)

   # Input to model is a tensor, so the output is also a tensor
   # Convert to NumPy arrays, and take index [0] to remove the batch dimension.
   # We're only interested in the first num_detections.
   num_detections = int(output_dict.pop('num_detections'))
   output_dict = {key: value[0, :num_detections].numpy()
                  for key, value in output_dict.items()}
   output_dict['num_detections'] = num_detections

   # detection_classes should be ints.
   output_dict['detection_classes'] = output_dict['detection_classes'].astype(np.int64)

   return output_dict

# Function to draw bounding boxes and tracking information on the image frame
def track_object(model, image_np):
   global object_ids, lock
   # Actual detection.
   output_dict = run_inference_for_single_image(model, image_np)

   # Visualization of the results of a detection.
   for i in range(output_dict['detection_classes'].size):

       box = output_dict['detection_boxes'][i]
       classes = output_dict['detection_classes'][i]
       scores = output_dict['detection_scores'][i]

       if scores > 0.5:
           h = image_np.shape[0]
           w = image_np.shape[1]

           classname = category_index[classes]['name']
           classid =category_index[classes]['id']
           #Draw bounding boxes
           cv2.rectangle(image_np, (int(box[1] * w), int(box[0] * h)), (int(box[3] * w), int(box[2] * h)), colortable[classid], 2)

           #Write the class name on top of the bounding box
           font = cv2.FONT_HERSHEY_COMPLEX_SMALL

           hash, object_ids = hasher_object.getObjectId(image_np, int(box[1] * w), int(box[0] * h), int(box[3] * w),
                                            int(box[2] * h), object_ids)

           size = cv2.getTextSize(str(classname) + ":" + str(scores)+"[Id: "+str(object_ids.get(hash))+"]", font, 0.75, 1)[0][0]

           cv2.rectangle(image_np,(int(box[1] * w), int(box[0] * h-20)), ((int(box[1] * w)+size+5), int(box[0] * h)), colortable[classid],-1)
           cv2.putText(image_np, str(classname) + ":" + str(scores)+"[Id: "+str(object_ids.get(hash))+"]",
                   (int(box[1] * w), int(box[0] * h)-5), font, 0.75, (0,0,0), 1, 1)

           cv2.putText(image_np, "Number of objects detected: "+str(len(object_ids)),
                       (10,20), font, 0.75, (0, 0, 0), 1, 1)
       else:
           break
   return image_np

# Function to implement infinite while loop to read video frames and generate the output for web browser
def streamVideo():
   global lock
   while (True):
       retrieved, frame = cap.read()
       if retrieved:
           with lock:
               frame = track_object(model, frame)

               (flag, encodedImage) = cv2.imencode(".jpg", frame)
               if not flag:
                   continue

               yield (b'--frame\r\n' b'Content-Type: img/jpeg\r\n\r\n' +
                  bytearray(encodedImage) + b'\r\n')

       if cv2.waitKey(1) & 0xFF == ord('q'):
           cap.stop()
           cv2.destroyAllWindows()
           break

   # When everything done, release the capture
   cap.stop()
   cv2.destroyAllWindows()

Listing 7-12object_tracker.py

清单 7-13 的文件路径为video_tracking/videoasync.py

# file: videoasync.py
import threading
import cv2

class VideoCaptureAsync:
   def __init__(self, src=0):
       self.src = src
       self.cap = cv2.VideoCapture(self.src)
       self.grabbed, self.frame = self.cap.read()
       self.started = False
       self.read_lock = threading.Lock()

   def set(self, var1, var2):
       self.cap.set(var1, var2)

   def start(self):
       if self.started:
           print('[Warning] Asynchronous video capturing is already started.')
           return None
       self.started = True
       self.thread = threading.Thread(target=self.update, args=())
       self.thread.start()
       return self

   def update(self):
       while self.started:
           grabbed, frame = self.cap.read()
           with self.read_lock:
               self.grabbed = grabbed
               self.frame = frame

   def read(self):
       with self.read_lock:
           frame = self.frame.copy()
           grabbed = self.grabbed
       return grabbed, frame

   def stop(self):
       self.started = False
       # self.cap.release()
       # cv2.destroyAllWindows()
       self.thread.join()

   def __exit__(self, exec_type, exc_value, traceback):
       self.cap.release()

Listing 7-13videoasync.py

清单 7-14 的文件路径为video_tracking/tracker.py

# tracker.py
import numpy as np
import cv2

class ObjectHasher:
   def __init__(self, threshold=20, size=8, max_track_frame=10, radius_tracker=5):
       self.threshold = 20
       self.size = 8
       self.max_track_frame = 10
       self.radius_tracker = 5

   def getCenter(self, xmin, ymin, xmax, ymax):
       x_center = int((xmin + xmax)/2)
       y_center = int((ymin+ymax)/2)
       return (x_center, y_center)

   def getObjectId(self, image_np, xmin, ymin, xmax, ymax, hamming_dict={}):
       croppedImage = self.getCropped(image_np,int(xmin*0.8), int(ymin*0.8), int(xmax*0.8), int(ymax*0.8))
       croppedImage = cv2.cvtColor(croppedImage, cv2.COLOR_BGR2GRAY)

       resizedImage = self.resize(croppedImage, self.size)

       hash = self.getHash(resizedImage)
       center = self.getCenter(xmin*0.8, ymin*0.8, xmax*0.8, ymax*0.8)

       # hamming_dict = self.createHammingDict(hash, center, hamming_dict)
       hamming_dict = self.getObjectCounter(hash, hamming_dict)
       return hash, hamming_dict

   def getCropped(self, image_np, xmin, ymin, xmax, ymax):
       return image_np[ymin:ymax, xmin:xmax]

   def resize(self, cropped_image, size=8):
       resized = cv2.resize(cropped_image, (size+1, size))
       return resized

   def getHash(self, resized_image):
       diff = resized_image[:, 1:] > resized_image[:, :-1]
       # convert the difference image to a hash
       dhash = sum([2 ** i for (i, v) in enumerate(diff.flatten()) if v])
       return int(np.array(dhash, dtype="float64"))

   def hamming(self, hashA, hashB):
       # compute and return the Hamming distance between the integers
       return bin(int(hashA) ^ int(hashB)).count("1")

   def createHammingDict(self, dhash, center, hamming_dict):
       centers = []
       matched = False
       matched_hash = dhash
       # matched_classid = classid

       if hamming_dict.__len__() > 0:
           if hamming_dict.get(dhash):
               matched = True

           else:
               for key in hamming_dict.keys():

                   hd = self.hamming(dhash, key)

                   if(hd < self.threshold):
                       centers = hamming_dict.get(key)
                       if len(centers) > self.max_track_frame:
                           centers.pop(0)
                       centers.append(center)
                       del hamming_dict[key]
                       hamming_dict[dhash] = centers
                       matched = True
                       break

       if not matched:
           centers.append(center)
           hamming_dict[dhash] = centers

       return  hamming_dict

   def getObjectCounter(self, dhash, hamming_dict):
       matched = False
       matched_hash = dhash
       lowest_hamming_dist = self.threshold
       object_counter = 0

       if len(hamming_dict) > 0:
           if dhash in hamming_dict:
               lowest_hamming_dist = 0
               matched_hash = dhash
               object_counter = hamming_dict.get(dhash)
               matched = True

           else:
               for key in hamming_dict.keys():
                   hd = self.hamming(dhash, key)
                   if(hd < self.threshold):
                       if hd < lowest_hamming_dist:
                           lowest_hamming_dist = hd
                           matched = True
                           matched_hash = key
                           object_counter = hamming_dict.get(key)
       if not matched:
           object_counter = len(hamming_dict)
       if matched_hash in hamming_dict:
           del hamming_dict[matched_hash]

       hamming_dict[dhash] = object_counter

       return  hamming_dict

   def drawTrackingPoints(self, image_np, centers, color=(0,0,255)):
       image_np = cv2.line(image_np, centers[0], centers[len(centers) - 1], color)
       return image_np

Listing 7-14tracker.py

通过从终端执行命令python video_server.py来运行 Flask 服务器。要观看实时视频流,启动您的网络浏览器并指向 URL http://localhost:5019

摘要

在本章中,我们使用预先训练的 SSD 模型开发了一个全功能视频跟踪系统。我们还学习了差分哈希(dHash)算法,并使用汉明距离来确定图像的相似性。我们将我们的系统部署到 Flask microweb 框架,以在 web 浏览器中呈现实时视频跟踪。

八、实际例子:人脸识别

人脸识别是在图像或视频中检测和识别人脸的计算机视觉问题。面部识别的第一步是在输入图像中检测和定位面部的位置。这是一个典型的对象检测任务,就像我们在前面的章节中了解到的那样。检测到面部后,从面部的各个关键点创建特征集,也称为面部足迹面部嵌入。一张人脸有 80 个节点或区分标志,用于创建特征集(USPTO 专利号 US7634662B2, https://patents.google.com/patent/US7634662B2/ )。然后将嵌入的人脸与数据库进行比较,以确定人脸的身份。

面部识别在现实世界中有许多应用,例如:

  • 作为进入高安全区域的密码

  • 在机场海关和边境保护方面

  • 在识别遗传疾病方面

  • 作为预测个人年龄和性别的一种方式(例如,用于控制基于年龄的访问,如酒精购买)

  • 在执法中(例如,警察通过扫描数百万张照片来发现潜在的犯罪嫌疑人和证人)。

  • 在组织数字相册(例如,社交媒体上的照片)时

在这一章中,我们将探索由谷歌工程师开发的流行的人脸识别算法 FaceNet。我们将学习如何训练基于 FaceNet 的神经网络来开发人脸识别模型。最后,我们将编写代码来开发一个全功能的人脸识别系统,该系统可以从视频流中实时检测人脸。

FaceNet(网面)

FaceNet 是由三位谷歌工程师 Florian Schroff、Dmitry Kalenichenko 和 James Philbin 发明的。他们于 2015 年在一篇题为“FaceNet:人脸识别和聚类的统一嵌入”( https://arxiv.org/pdf/1503.03832.pdf )的论文中发表了他们的工作。

FaceNet 是一个统一的系统,提供以下功能:

  • 人脸验证(这是同一个人吗?)

  • 认可(这个人是谁?)

  • 聚类(有相似的脸吗?)

FaceNet 是一种深度神经网络,具有以下功能:

  • 从输入图像中计算 128D 紧凑特征向量,称为面部嵌入。回想一下第四章中的内容,特征向量包含描述物体重要特征的信息。128D 特征向量是 128 个实数值的列表,表示试图量化面部的输出。

  • 通过优化三重损失函数来学习。我们将在本章后面探讨损失函数。

FaceNet 神经网络体系结构

图 8-1 显示了 FaceNet 架构。

img/493065_1_En_8_Fig1_HTML.png

图 8-1

FaceNet 神经网络体系结构

以下部分描述了 FaceNet 网络的组件。

输入图像

训练集由从图像中裁剪的面部缩略图组成。除了平移和缩放之外,不需要对面裁剪进行其他对齐。

深度 CNN

使用具有反向传播的 SGD 和 AdaGrad 优化器,使用深度卷积神经网络来训练 FaceNet。初始学习率取为 0.05,并随着迭代减少以最终确定模型。培训是在基于 CPU 的集群上进行的,时间为 1,000 到 2,000 小时。

FaceNet 论文描述了具有不同权衡的深度卷积神经网络的两种不同架构。第一个架构的灵感来自泽勒和弗格斯,第二个架构来自谷歌。这两种体系结构主要在两个方面不同:参数的数量和每秒浮点运算次数(FLOPS)。FLOPS 是衡量需要浮点计算的计算机性能的标准。

泽勒和弗格斯 CNN 架构由 22 层组成,在 1.4 亿个参数上训练,每幅图像 16 亿次浮点运算。这种 CNN 架构称为 NN1,其输入大小为 220×220。

表 8-1 显示了 FaceNet 中使用的基于泽勒和弗格斯的网络配置。

表 8-1

深度 CNN 基于泽勒和弗格斯网络架构(来源:施罗夫等人, https://arxiv.org/pdf/1503.03832.pdf )

| - ![img/493065_1_En_8_Figa_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-cv-zh/raw/master/docs/build-comp-vis-app-ann/img/493065_1_En_8_Figa_HTML.jpg) |

第二种类型的网络是基于 GoogLeNet 的初始模型。该模型的参数减少了 20 倍(约 660 万至 750 万),FLOPS 减少了 5 倍(约 5 亿至 16 亿)。

基于输入的大小,有一些初始模型的变体。这里对它们进行了简要描述:

  • 这是一个初始模型,拍摄尺寸为 224×224 的图像,并以每幅图像 16 亿次浮点运算对 750 万个参数进行训练。

表 8-2 显示了 FaceNet 中使用的 NN2 初始模型。

表 8-2

基于 GoogLeNet 的 Inception 模型架构(来源:Schroff 等人, https://arxiv.org/pdf/1503.03832.pdf )

| - ![img/493065_1_En_8_Figb_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-cv-zh/raw/master/docs/build-comp-vis-app-ann/img/493065_1_En_8_Figb_HTML.jpg) |
  • NN3 :与 NN2 相比,它在架构上完全相同,只是它使用 160×160 的输入尺寸,导致网络尺寸更小。

  • NN4 :该网络具有 96×96 的输入大小,导致参数大幅减少,每个图像仅需要 2.85 亿次浮点运算(相比之下,NN1 和 NN2 需要 16 亿次浮点运算)。由于 NN4 的尺寸更小,FLOPS 要求的 CPU 时间更少,因此适合移动设备。

  • NNS1 :由于其尺寸较小,这也被称为“迷你”初始。它具有 165×165 的输入大小和 2600 万个参数,每个图像只需要 2.2 亿次浮点运算。

  • NNS2 :这被称为“微小的”开始。它的输入大小为 140×116,有 430 万个参数,需要 2000 万个触发器。

NN4、NNS1 和 NNS2 适用于移动设备,因为其参数数量较少,要求每个映像的 CPU FLOPS 较低。

值得一提的是,FLOPS 越大,模型精度越高。一般来说,FLOPS 越低的网络运行速度越快,消耗的内存越少,但精度也越低。

图 8-2 显示了不同类型 CNN 架构的 FLOPS 与精度的关系图。

img/493065_1_En_8_Fig2_HTML.jpg

图 8-2

FLOPS 与准确性(来源:FaceNet,arxiv . org/pdf/1503 . 03832 . pdf)

人脸嵌入

从深度 CNN 的 L2 归一化层生成大小为 1×1×128 的人脸嵌入(如图 8-1 和表 8-1 和 8-2 )。

在计算嵌入之后,通过计算嵌入之间的欧几里德距离并基于以下找到相似的面部来执行面部验证(或找到相似的面部):

  • 同一个人的脸之间的距离较小

  • 不同人的脸有更大的距离

通过标准的 K-最近邻(K-NN)分类来执行人脸识别。

使用像 K-means 或凝聚聚类技术这样的算法来完成聚类。

三重损失函数

FaceNet 中使用的损失函数被称为三重损失函数

相同人脸的嵌入称为,不同人脸的嵌入称为。被分析的人脸被称为主播。为了计算损失,形成由锚、正和负嵌入组成的三元组,并分析它们的欧几里德距离。FaceNet 的学习目标是最小化锚和正面之间的距离,最大化锚和负面之间的距离。

图 8-3 说明了三重损失函数和学习过程。

img/493065_1_En_8_Fig3_HTML.jpg

图 8-3

三重损失最小化了锚和具有相同身份的正片之间的距离,并且最大化了锚和不同身份的负片之间的距离。(来源:FaceNet,arxiv . org/pdf/1503 . 03832 . pdf。)

每一张人脸图像都是一个特征向量,代表一个 d 维欧氏超球面,用函数| |f(x)| |2= 1来表示。

假设人脸图像$$ {x}_i^a $$(主播)比不同人的$$ {x}_i^n $$(硬负)人脸更接近同一个人的人脸$$ {x}_i^p $$(硬正)。此外,假设在训练集中有 N 个三元组。三重损失函数由下式表示:

$$ {\sum}_iN\left[ {\left\Vert f\left({x}_ia\right)-f\left({x}_i^p\right)\ \right\Vert}_2²-{\left\Vert f\left({x}_ia\right)-f\left({x}_in\right)\ \right\Vert}_2²+\alpha\ \right] $$其中 α 是正嵌入和负嵌入之间的距离余量。

如果我们考虑三元组的每一种可能的组合,将会有很多三元组,而前面的函数可能需要很长时间才能收敛。此外,并不是每个三元组都有助于模型学习。因此,我们需要一种方法来选择正确的三元组,以便我们的模型训练是有效的,并且精度是最佳的。

三联体选择

理想情况下,我们应该以这样的方式选择三元组:$$ {\left\Vert f\left({x}_ia\right)-f\left({x}_ip\right)\ \right\Vert}_2² $$最小,$$ {\left\Vert f\left({x}_ia\right)-f\left({x}_in\right)\ \right\Vert}_2² $$最大。但是计算所有数据集的最小值和最大值可能是不可行的。因此,我们需要一种有效计算最小和最大距离的方法。这可以离线完成,然后馈送给算法,或者使用一些算法在线确定。

在在线方法中,我们将嵌入分成小批量。每个小批量包含一小组阳性和一些随机选择的阴性。FaceNet 的发明人使用了由 40 个阳性和随机选择的阴性嵌入组成的小批量。计算每个小批量的最小和最大距离,以创建三元组。

在接下来的部分中,我们将学习如何基于 FaceNet 训练我们自己的模型,并构建一个实时人脸识别系统。

训练人脸识别模型

FaceNet 最流行的 TensorFlow 实现之一是由大卫·桑德伯格实现的。这是一个开源版本,可以在 GitHubhttps://github.com/davidsandberg/facenet的 MIT 许可下免费获得。我们已经分叉了原始的 GitHub 库,并提交了一个稍微修改的版本到我们位于 https://github.com/ansarisam/facenet 的 GitHub 库。我们没有修改核心神经网络和三重损失函数的实现。我们修改过的 FaceNet 版本来自大卫·桑德伯格的知识库,使用 OpenCV 来读取和操作图像。我们还升级了 TensorFlow 的部分库函数。FaceNet 的这个实现需要 TensorFlow 版本 1。并且目前不能在版本 2 上运行。

在下面的例子中,我们将使用 Google Colab 来训练我们的人脸检测模型。值得注意的是,人脸检测模型是计算密集型的,可能需要几天时间来学习,即使在 GPU 上也是如此。因此,Colab 不是训练长期运行模型的理想平台,因为在 Colab 会话到期后,您将丢失所有数据和设置。您应该考虑使用基于云的 GPU 环境来训练生产质量的人脸识别模型。第十章将向您展示如何在云上扩展您的模型训练。现在,出于学习的目的,让我们使用 Colab。

在开始之前,创建一个新的 Colab 项目,并给它起一个有意义的名字,比如 FaceNet Training。

从 GitHub 查看 FaceNet

查看 FaceNet 的 TensorFlow 实现的源代码。在 Colab 中,通过单击+Code 图标添加一个代码单元格。编写命令来克隆 GitHub 库,如清单 8-1 所示。单击执行按钮运行命令。成功执行后,您应该在 Colab 文件浏览器面板中看到目录facenet

1    %%shell
2    git clone https://github.com/ansarisam/facenet.git

Listing 8-1Cloning the GitHub Repository of TensorFlow Implementation of FaceNet

资料组

我们将使用 VGGFace2 数据集来训练我们的人脸识别模型。VGGFace2 是用于人脸识别的大规模图像数据集,由视觉几何组 https://www.robots.ox.ac.uk/~vgg/data/vgg_face2/ 提供。

VGGFace2 数据集由 9000 多人的 330 万张人脸组成(简称为身份)。数据样本中每个身份有 362 张图片(平均)。该数据集在 2018 年由 Q. Cao,L. Shen,W. Xie,O. M. Parkhi 和 A. Zisserman 发表的论文 http://www.robots.ox.ac.uk/~vgg/publications/2018/Cao18/cao18.pdf 中进行了描述。

训练集的大小为 35GB,测试集的大小为 1.9GB。数据集以压缩文件的形式提供。面部图像被组织在子目录中。每个子目录的名称是格式为n< classID >的身份类 ID。图 8-4 显示了包含训练图像的样本目录结构。

img/493065_1_En_8_Fig4_HTML.jpg

图 8-4

包含图像的子目录

提供了 CSV 格式的单独元数据文件。该元数据文件的文件头如下:

身份 ID、姓名、样本号、训练/测试标志、性别

下面是一个简短的描述:

  • Identity ID映射到子目录名称。

  • name是包含人脸图像的人的名字。

  • sample number代表子目录中图片的数量。

  • train/test flag表示身份是在训练集还是测试集中。训练集由标志 1 表示,测试集为 0。

  • gender是人的性别。

需要注意的是,这个数据集的大小太大,不适合 Google Colab 或 Google Drive 的免费版本。

如果整个数据集不适合 Colab 的免费版本,您可以使用数据的一个子集(可能是几百个身份)来学习。

当然,如果你想建立一个自定义的人脸识别模型,你可以使用你自己的图像。你需要做的就是将同一个人的图像保存在一个目录中,每个人都有自己的目录,并将目录结构匹配成如图 8-4 所示。确保您的目录名和图像文件名没有任何空格。

正在下载 VGGFace2 数据

要下载图像,您需要在 http://zeus.robots.ox.ac.uk/vgg_face2/signup/ 注册。注册完成后,直接从 http://www.robots.ox.ac.uk/~vgg/data/vgg_face2/ 登录下载数据,将压缩后的训练和测试文件保存到您的本地驱动器,然后上传到 Colab。

如果您喜欢直接在 Colab 中下载图像,您可以使用清单 8-2 中的代码。使用正确的 URL 运行程序,下载训练集和测试集。

1    import sys
2    import getpass
3    import requests
4
5    VGG_FACE_URL = "http://zeus.robots.ox.ac.uk/vgg_face2/login/"
6    IMAGE_URL = "http://zeus.robots.ox.ac.uk/vgg_face2/get_file?fname=vggface2_train.tar.gz"
7    TEST_IMAGE_URL="http://zeus.robots.ox.ac.uk/vgg_face2/get_file?fname=vggface2_test.tar.gz"
8
9    print('Please enter your VGG Face 2 credentials:')
10   user_string = input('    User: ')
11   password_string = getpass.getpass(prompt='    Password: ')
12
13   credential = {
14      'username': user_string,
15      'password': password_string
16   }
17
18   session = requests.session()
19   r = session.get(VGG_FACE_URL)
20
21   if 'csrftoken' in session.cookies:
22      csrftoken = session.cookies['csrftoken']
23   elif 'csrf' in session.cookies:
24      csrftoken = session.cookies['csrf']
25   else:
26      raise ValueError("Unable to locate CSRF token.")
27
28   credential['csrfmiddlewaretoken'] = csrftoken
29
30   r = session.post(VGG_FACE_URL, data=credential)
31
32   imagefiles = IMAGE_URL.split('=')[-1]

33
34   with open(imagefiles, "wb") as files:
35      print(f"Downloading the file: `{imagefiles}`")
36      r = session.get(IMAGE_URL, data=credential, stream=True)
37      bytes_written = 0
38      for data in r.iter_content(chunk_size=400096):
39          files.write(data)
40          bytes_written += len(data)
41          MegaBytes = bytes_written / (1024 * 1024)
42          sys.stdout.write(f"\r{MegaBytes:0.2f} MiB downloaded...")
43          sys.stdout.flush()
44
45   print("\n Images are successfully downloaded. Exiting the process.")

Listing 8-2Python Code to Download VGGFace2 Images (Source: https://github.com/MistLiao/jgitlib/blob/master/download.py)

下载完训练集和测试集后,按照图 8-4 所示的结构解压缩得到训练和测试目录及其子目录。要解压缩,您可以执行清单 8-3 中的命令。

1    %%shell
2    tar xvzf vggface2_train.tar.gz
3    tar xvzf vggface2_test.tar.gz

Listing 8-3Commands to Uncompress Files

数据准备

FaceNet 的训练集应该只是脸部的图像。因此,如果需要的话,我们需要裁剪图像来提取人脸,对齐它们,并调整它们的大小。我们将使用一种称为多任务级联卷积网络 (MTCNNs)的算法,该算法已被证明在保持实时性能的同时优于许多人脸检测基准。

我们从 GitHub 存储库中克隆的 FaceNet 源代码有一个 MTCNN 的 TensorFlow 实现。这个模型的实现超出了本书的范围。我们将使用align模块中可用的 Python 程序align_dataset_mtcnn.py来获取在训练和测试集中检测到的所有人脸的边界框。该程序将保留目录结构,并将裁剪后的图像保存在相同的目录层次中,如图 8-4 所示。

清单 8-4 显示了执行面裁剪和对齐的脚本。

1    %%shell
2    %tensorflow_version 1.x
3    export PYTHONPATH=$PYTHONPATH:/content/facenet
4    export PYTHONPATH=$PYTHONPATH:/content/facenet/src
5    for N in {1..10}; do \
6    python facenet/src/align/align_dataset_mtcnn.py \
7    /content/train \
8    /content/train_aligned \
9    --image_size 182 \
10   --margin 44 \
11   --random_order \
12   --gpu_memory_fraction 0.10 \
13   & done

Listing 8-4Code for Face Detection Using MTCNN, Cropping and Alignment

在清单 8-4 中,第 1 行激活 shell,第 2 行将 TensorFlow 版本设置为 1。 x 让 Colab 知道我们不想使用版本 2,这是 Colab 中的默认版本。

第 3 行和第 4 行将环境变量PYTHONPATH设置为facenetfacenet/src目录。如果您使用的是虚拟机或物理机,并且可以直接访问操作系统,那么您应该考虑在~/.bash_profile文件中设置环境变量。

为了加速面部检测和对齐过程,我们创建了十个并行过程(第 5 行),对于每个过程,我们使用 10%的 GPU 内存(第 12 行)。如果数据集较小,并且希望在单个进程中处理 MTCNN,只需删除第 5、12 和 13 行。

第 6 行调用文件align_dataset_mtcnn.py并传递以下参数:

  • 第一个参数/content/train是训练图像所在的目录路径。

  • 第二个参数/content/train_aligned是存储对齐图像的目录路径。

  • 第三个参数--image_size,是裁剪图像的大小。我们将其设置为 182×182 像素。

  • 参数--margin设置为 44,在裁剪图像的所有四边创建一个边距。

  • 下一个参数--random_order,如果存在,将通过并行处理以随机顺序选择图像。

  • 最后一个参数--gpu_memory_fraction用于告诉算法每个并行进程使用 GPU 内存的多少部分。

在前面的脚本中,裁剪后的图像大小为 182×182 像素。Inception-ResNet-v1 的输入只有 160×160。这为随机作物提供了额外的利润。附加页边空白 44 的使用用于向模型添加任何上下文信息。额外的 44 页边空白应该根据您的具体情况进行调整,并且应该评估裁剪性能。

执行前面的脚本开始裁剪和对齐过程。请注意,这是一个计算密集型过程,可能需要几个小时才能完成。

对测试图像重复前面的过程。

模特培训

清单 8-5 用于训练具有三重损失函数的面网模型。

%tensorflow_version 1.x
!export PYTHONPATH=$PYTHONPATH:/content/facenet/src
!python facenet/src/train_tripletloss.py \
--logs_base_dir logs/facenet/ \
--models_base_dir /content/drive/'My Drive'/chapter8/facenet_model/ \
--data_dir /content/drive/'My Drive'/chapter8/train_aligned/ \
--image_size 160 \
--model_def models.inception_resnet_v1 \
--optimizer ADAGRAD \
--learning_rate 0.01 \
--weight_decay 1e-4 \
--max_nrof_epochs 10 \
--epoch_size 200

Listing 8-5Script to Train the FaceNet Model with the Triplet Loss Function

如前所述,FaceNet 的当前实现运行在 TensorFlow 版本 1 上。 x 与 TensorFlow 2 不兼容(1 号线设置版本 1。 x

第 2 行是将PYTHONPATH环境变量设置到facenet/src目录。

第 3 行使用三元组损失函数执行 FaceNet 训练。可以为训练设置许多参数,但我们将在此仅列出重要的参数。有关参数及其解释的详细列表,请查看位于facenet/src目录中的train_tripletloss.py的源代码。

为模型定型传递了以下参数:

  • --logs_base_dir:这是保存训练日志的目录。我们将 TensorBoard 连接到此目录,以使用 TensorBoard 仪表板评估模型。

  • --model_base_dir:这是存储模型检查点的基本目录。注意,我们已经提供了路径/content/drive/'My Drive'/chapter8/facenet_model/来存储 Google Drive 的模型检查点。这是为了将模型检查点永久保存到 Google Drive,避免因为 Colab 的会话终止而丢失模型。如果 Colab 会话终止,我们可以从它停止的地方重新启动模型。请注意,由于名称中有空格,所以我的驱动器用单引号括起来。

  • --data_dir:这是用于训练的对齐图像的基础目录。

  • --image_size:训练用的输入图像将根据该参数调整大小。Inception-ResNet-v1 采用 160×160 像素的输入图像尺寸。

  • --model_def:这是型号的名称。在这个例子中,我们使用了inception_resnet_v1

  • --optimizer:这是要使用的优化算法。您可以使用任何优化器ADAGRADADADELTAADAMRMSPROPMOM,默认为ADAGRAD

  • --learning_rate:我们设定学习率为 0.01。根据需要进行调整。

  • 这可以防止重量变得太大。

  • --max_nrof_epochs:训练应该运行的最大时期数。

  • --epoch_size:这是每个时期的批次数。

单击 Colab 中的 Run 按钮执行培训。根据您的训练规模和训练参数,完成模型可能需要几个小时甚至几天。

在模型被成功训练之后,检查点被保存在目录--model_base_dir中,这是我们之前在清单 8-5 ,第 5 行中配置的。

估价

当模型运行时,每个时期和每个批次的损失将打印到控制台。这应该能让你了解模型是如何学习的。理想情况下,损耗应该减少,并且应该稳定在非常低的值,接近于零。图 8-5 显示了训练进行过程中的样本输出。

img/493065_1_En_8_Fig5_HTML.jpg

图 8-5

训练过程中的 Colab 控制台输出。它显示了每批每时期的损失

您还可以使用 TensorBoard 评估模型性能。使用清单 8-6 中的命令启动 TensorBoard 仪表板。

1 %tensorflow_version 2.x
2 %load_ext tensorboard
3 %tensorboard --logdir /content/logs/facenet

Listing 8-6Launching TensorBoard by Pointing to the logs Directory

开发实时人脸识别系统

人脸识别系统需要三个重要的条件。

  • 人脸检测模型

  • 分类模型

  • 图像或视频源

人脸检测模型

在上一节中,我们学习了如何训练人脸检测模型。我们可以使用我们构建的模型,也可以使用符合我们要求的预训练模型。表 8-3 列出了公开免费提供的预训练模型。

表 8-3

大卫·桑德伯格提供的人脸识别预训练模型

|

型号名称

|

训练数据集

|

下载位置

|
| --- | --- | --- |
| 20180408-102900 | CASIA-WebFace | https://drive.google.com/open?id=1R77HmFADxe87GmoLwzfgMu_HY0IhcyBz |
| 20180402-114759 | VGGFace2 | https://drive.google.com/open?id=1EXPBSXwTaqrSC0OhUdXNmKSh9qJUQ55- |

这些模型可在以下位置免费下载。

针对在 http://vis-www.cs.umass.edu/lfw/ 可用的野生(LFW)数据集中的标记人脸来评估模型。表 8-4 显示了模型架构和精度。

表 8-4

在 CASIA-WebFace 和 VGGFace2 数据集上训练的 FaceNet 模型的准确性评估结果(由大卫·桑德伯格提供的信息)

|

型号名称

|

LFW 准确度

|

训练数据集

|

体系结构

|
| --- | --- | --- | --- |
| 20180408-102900 | 0.9905 | CASIA-WebFace | Inception ResNet v1 |
| 20180402-114759 | 0.9965 | VGGFace2 | Inception ResNet v1 |

对于我们的示例,我们将使用 VGGFace2 模型。

人脸识别分类器

我们将建立一个模型来识别人脸(这个人是谁)。我们将训练模型来识别乔治·w·布什、巴拉克·奥巴马和唐纳德·特朗普这三位最近的美国总统。

为了简单起见,我们将下载三位总统的一些图片,并将它们组织在子目录中,看起来如图 8-6 所示。

img/493065_1_En_8_Fig6_HTML.jpg

图 8-6

输入图像目录结构

我们将在我们的个人电脑/笔记本电脑上开发人脸检测器。在我们训练分类器之前,我们需要克隆 FaceNet GitHub 存储库。执行以下命令:

git 克隆 https://github.com/ansarisam/facenet.git

克隆 FaceNet 源代码后,将PYTHONPATH设置为facenet/src,并将其添加到环境变量中。

  • 汇出 python path = $ python path:/home/user/facenet/src

src目录的路径必须是您电脑中的实际目录路径。

面部对齐

在本节中,我们将执行图像的面部对齐。我们将使用与上一节相同的 MTCNN 模型。由于我们有一个小的图像集,我们将使用一个单一的过程来对齐这些脸。清单 8-7 显示了面部对齐的脚本。

1    python facenet/src/align/align_dataset_mtcnn.py \
2    ~/presidents/ \
3   ~/presidents_aligned \
4    --image_size 182 \
5    --margin 44

Listing 8-7Script for Face Alignment Using MTCNN

Note

在基于 Mac 的计算机上,图像目录可能有一个名为.DS_Store的隐藏文件。确保从包含输入图像的所有子目录中删除该文件。另外,确保子目录只包含图像,不包含其他文件。

执行前面的脚本来裁剪和对齐面。图 8-7 显示了一些样本输出。

img/493065_1_En_8_Fig7_HTML.png

图 8-7

三位美国总统修剪整齐的脸

分类器训练

有了这个最小的设置,我们就可以训练分类器了。清单 8-8 显示了启动分类器训练的脚本。

1    python facenet/src/classifier.py TRAIN \
2    ~/presidents_aligned \
3    ~/20180402-114759/20180402-114759.pb \
4    ~/presidents_aligned/face_classifier.pkl \
5    --batch_size 1000 \
6    --min_nrof_images_per_class 40 \
7    --nrof_train_images_per_class 35 \
8    --use_split_dataset

Listing 8-8Script to Launch the Face Classifier Training

在清单 8-8 中,第 1 行调用classifier.py并传递参数TRAIN,表示我们要训练一个分类器。该 Python 脚本的其他参数如下:

  • 包含对齐的面部图像的输入基本目录(第 2 行)。

  • 我们自己构建的或者从上一节提供的 Google Drive 链接下载的预训练人脸检测模型的路径(第 3 行)。如果您已经训练了自己的保存检查点的模型,请提供包含检查点的目录的路径。在清单 8-8 中,我们提供了冻结模型的路径(*.pb)。

  • 第 4 行是我们的分类器模型将被保存的路径。注意,这是一个扩展名为.pkl的 Pickle 文件。Pickle 是一个 Python 序列化和反序列化模块。

分类器模型成功执行后,训练好的分类器存储在清单 8-8 第 4 行提供的文件中。

视频流中的人脸识别

在清单 7-1 中,我们使用 OpenCV 的便利函数cv2.VideoCapture()从计算机的内置摄像头或 USB 或 IP 摄像头读取视频帧。VideoCapture()函数的参数 0 通常用于从内置摄像机中读取帧。在这一节中,我们将讨论如何使用 YouTube 作为我们的视频源。

为了阅读 YouTube 视频,我们将使用一个名为pafy的 Python 库,内部使用了youtube_dl库。在您的开发环境中使用 PIP 安装这些库。只需执行清单 8-9 中的命令来安装pafy

pip install pafy
pip install youtube_dl

Listing 8-9Commands to Install YouTube-Related Libraries

我们为这个练习克隆的 FaceNet 存储库在contributed模块中提供了源代码real_time_face_recognition.py,用于识别视频中的人脸。清单 8-10 展示了如何使用 Python API 从视频中检测和识别人脸。

1   python real_time_face_recognition.py \
2   --source youtube \
3   --url https://www.youtube.com/watch?v=ZYkxVbYxy-c \
4   --facenet_model_checkpoint ~/20180402-114759/20180402-114759.pb \
5   --classfier_model ~/presidents_aligned/face_classifier.pkl

Listing 8-10Script to Call Real-Time Face Recognition API

在清单 8-10 中,第 1 行调用real_time_face_recognition.py并传递以下参数:

  • 第 2 行设置参数--source的值,在本例中是youtube。如果您跳过此参数,它将默认为计算机的内置摄像头。您可以显式地传递参数webcam来从内置相机读取帧。

  • 第 3 行是传递 YouTube 视频 URL。在摄像机源的情况下,不需要这个参数。

  • 第 4 行提供了到预训练的 FaceNet 模型的路径。您可以提供检查点目录或冻结的*.pb模型的路径。

  • 第 5 行提供了我们在上一节中训练的分类器模型的文件路径,例如用于识别三位美国总统的脸的分类器模型。

当您执行清单 8-10 时,它将读取 YouTube 视频帧并显示带有边框的已识别人脸。图 8-8 显示了一个样本识别。

img/493065_1_En_8_Fig8_HTML.jpg

图 8-8

从人脸识别视频中截取的示例截图。视频的输入源是 YouTube

摘要

人脸检测是一个有趣的计算机视觉问题,涉及检测分类人脸嵌入,以识别图像中的人是谁。在这一章中,我们探讨了 FaceNet,一种基于 ResNet 的流行的人脸识别算法。我们学习了使用 MTCNN 算法来裁剪图像的面部部分的技术。我们还训练了自己的分类器,并通过一个例子对三位美国总统的面部进行了分类。最后,我们从 YouTube 上获取视频流,并实现了一个实时人脸识别系统。

九、工业应用:工业制造中的实时缺陷检测

计算机视觉在工业制造中有许多应用。一个这样的应用是用于质量控制和保证的视觉检查的自动化。

大多数制造公司培训他们的人员手动执行目视检查,这是一个手动的检查过程,可能是主观的,导致准确性取决于个别检查员的经验和意见。还应该注意到,这个过程是劳动密集型的。

如果出现机器校准问题、环境设置或设备故障,整批产品都可能出现故障。在这种情况下,事后的人工检查可能会被证明是昂贵的,因为产品可能已经生产出来,并且整批(可能数百或数千)有缺陷的产品可能需要被丢弃。

总之,手动检查过程缓慢、不准确且成本高昂。

基于计算机视觉的视觉检测系统可以通过分析视频帧流来实时检测表面缺陷。当检测到一个缺陷或一系列缺陷时,系统可以实时发送警报,以便停止生产,避免任何损失。

在这一章中,我们将开发一个基于深度学习的计算机视觉系统来检测表面缺陷,如补丁,划痕,坑洼表面和银纹。

我们将使用包含热轧钢带标记图像的数据集。我们将首先转换数据集,训练 SSD 模型,并利用该模型来构建缺陷检测器。我们还将学习如何为任何对象检测任务标记我们自己的图像。

实时表面缺陷检测系统

在本节中,我们将首先检查用于训练和测试表面缺陷检测模型的数据集。我们将把图像和注释转换成 TFRecord 文件,并在 Google Colab 上训练一个 SSD 模型。我们将应用第六章中介绍的目标检测概念。

资料组

我们将利用东北大学(NEU)的 K. Song 和 Y. Yan 提供的数据集。该数据集由六种类型的热轧钢带表面缺陷组成。这些缺陷标记如下:

  • 轧制氧化皮(RS),通常在轧制过程中将氧化皮轧制成金属时出现。

  • 补片(Pa),可以是不规则的曲面补片。

  • 银纹(Cr),即表面上的网状裂纹。

  • 麻面(PS)由许多小的浅孔组成。

  • 夹杂物(In),它是嵌入钢内部的复合材料

  • 划痕(Sc)

图 9-1 显示了带有这六种缺陷的钢表面的标记图像。

img/493065_1_En_9_Fig1_HTML.jpg

图 9-1

具有六种不同类型缺陷的表面的标记图像样本。东北大学。edu。cn/云燕/ NEU_ 表面 _ 缺陷 _ 数据库。html

数据集包括 1800 幅灰度图像,每种缺陷类别有 300 个样本。

该数据集可在 https://drive.google.com/file/d/1qrdZlaDi272eA79b0uCwwqPrm2Q_WI3k/view 免费下载,用于教育和研究目的。从这个链接下载数据集并解压缩。未压缩的数据集组织在如图 9-2 所示的目录结构中。图像在子目录IMAGES中。ANNOTATIONS子目录包含边界框注释的 XML 文件和 PASCAL VOC 注释格式的缺陷类。

img/493065_1_En_9_Fig2_HTML.jpg

图 9-2

NEU-DET 数据集目录结构

Google Colab 笔记本电脑

首先在 Google Colab 上创建一个新的笔记本,并给它起一个名字(例如,表面缺陷检测 1.0 版)。

由于 NEU 数据集位于 Google Drive 上,我们可以直接将其复制到我们的私有 Google Drive 上。在 Colab 上,我们将挂载私有的 Google Drive,解压缩数据集,并设置开发环境(清单 9-1 )。请回顾第六章以刷新您对实施的理解。

1    # Code block 1: Mount Google Drive
2    from google.colab import drive
3    drive.mount('/content/drive')
4
5    # Code block 2: uncompress NEU data
6    %%shell
7    ls /content/drive/'My Drive'/NEU-DET.zip
8    unzip /content/drive/'My Drive'/NEU-DET.zip
9
10   # Code block 3: Clone github repository of Tensorflow model project
11   !git clone https://github.com/ansarisam/models.git
12
13   # Code block 4: Install Google protobuf compiler and other dependencies
14   !sudo apt-get install protobuf-compiler python-pil python-lxml python-tk
15
16   # Code block 4: Install dependencies
17   %%shell
18   cd models/research
19   pwd
20   protoc object_detection/protos/*.proto --python_out=.
21   pip install --user Cython
22   pip install --user contextlib2
23   pip install --user pillow
24   pip install --user lxml
25   pip install --user jupyter
26   pip install --user matplotlib
27
28   # Code block 5: Build models project

29   %%shell
30   export PYTHONPATH=$PYTHONPATH:/content/models/research:/content/models/research/slim
31   cd /content/models/research
32   python setup.py build
33   python setup.py install

Listing 9-1Mounting Google Drive, Downloading, Building, and Installing TensorFlow Models

数据转换

我们将把 NEU 数据集转换成 TFRecord 格式(查看第六章的 SSD 模型训练部分)。清单 9-2 是基于 TensorFlow 的代码,用于将图像和注释转换成 TFRecord。

File name: generic_xml_to_tf_record.py
1    from __future__ import absolute_import
2    from __future__ import division
3    from __future__ import print_function
4
5    import hashlib
6    import io
7    import logging
8    import os
9
10   from lxml import etree
11   import PIL.Image
12   import tensorflow as tf
13
14   from object_detection.utils import dataset_util
15   from object_detection.utils import label_map_util
16   import random
17
18   flags = tf.app.flags
19   flags.DEFINE_string('data_dir', '', 'Root directory to raw PASCAL VOC dataset.')
20
21   flags.DEFINE_string('annotations_dir', 'annotations',
22                     '(Relative) path to annotations directory.')
23   flags.DEFINE_string('image_dir', 'images',
24                     '(Relative) path to images directory.')
25
26   flags.DEFINE_string('output_path', '', 'Path to output TFRecord')
27   flags.DEFINE_string('label_map_path', 'data/pascal_label_map.pbtxt',
28                     'Path to label map proto')
29   flags.DEFINE_boolean('ignore_difficult_instances', False, 'Whether to ignore '
30                      'difficult instances')
31   FLAGS = flags.FLAGS
32
33   # This function generates a list of images for training and validation.
34   def create_trainval_list(data_dir):
35     trainval_filename = os.path.abspath(os.path.join(data_dir,"trainval.txt"))
36     trainval = open(os.path.abspath(trainval_filename), "w")
37     files = os.listdir(os.path.join(data_dir, FLAGS.image_dir))
38     for f in files:
39         absfile =os.path.abspath(os.path.join(data_dir, FLAGS.image_dir, f))
40         trainval.write(absfile+"\n")
41         print(absfile)
42     trainval.close()
43
44
45   def dict_to_tf_example(data,
46                        dataset_directory,
47                        label_map_dict,
48                        ignore_difficult_instances=False,
49                        image_subdirectory=FLAGS.image_dir):
50   """Convert XML derived dict to tf.Example proto.
51
52   Notice that this function normalizes the bounding box coordinates provided
53   by the raw data.
54
55   Args:
56     data: dict holding PASCAL XML fields for a single image
57     dataset_directory: Path to root directory holding PASCAL dataset
58     label_map_dict: A map from string label names to integers ids.
59     ignore_difficult_instances: Whether to skip difficult instances in the
60       dataset  (default: False).
61     image_subdirectory: String specifying subdirectory within the
62       PASCAL dataset directory holding the actual image data.
63
64   Returns:
65     example: The converted tf.Example.
66
67   Raises:
68     ValueError: if the image pointed to by data['filename'] is not a valid JPEG
69   """
70   filename = data['filename']
71
72   if filename.find(".jpg") < 0:
73       filename = filename+".jpg"
74   img_path = os.path.join("",image_subdirectory, filename)
75   full_path = os.path.join(dataset_directory, img_path)
76
77   with tf.gfile.GFile(full_path, 'rb') as fid:
78     encoded_jpg = fid.read()
79   encoded_jpg_io = io.BytesIO(encoded_jpg)
80   image = PIL.Image.open(encoded_jpg_io)
81   if image.format != 'JPEG':
82     raise ValueError('Image format not JPEG')
83   key = hashlib.sha256(encoded_jpg).hexdigest()
84
85   width = int(data['size']['width'])
86   height = int(data['size']['height'])
87
88   xmin = []
89   ymin = []
90   xmax = []
91   ymax = []
92   classes = []
93   classes_text = []
94   truncated = []
95   poses = []
96   difficult_obj = []
97   if 'object' in data:
98     for obj in data['object']:
99       difficult = bool(int(obj['difficult']))
100      if ignore_difficult_instances and difficult:
101        continue
102
103      difficult_obj.append(int(difficult))
104
105      xmin.append(float(obj['bndbox']['xmin']) / width)
106      ymin.append(float(obj['bndbox']['ymin']) / height)
107      xmax.append(float(obj['bndbox']['xmax']) / width)
108      ymax.append(float(obj['bndbox']['ymax']) / height)
109      classes_text.append(obj['name'].encode('utf8'))
110      classes.append(label_map_dict[obj['name']])
111      truncated.append(int(obj['truncated']))
112      poses.append(obj['pose'].encode('utf8'))
113
114  example = tf.train.Example(features=tf.train.Features(feature={
115      'img/height': dataset_util.int64_feature(height),
116      'img/width': dataset_util.int64_feature(width),
117      'img/filename': dataset_util.bytes_feature(
118          data['filename'].encode('utf8')),
119      'img/source_id': dataset_util.bytes_feature(
120          data['filename'].encode('utf8')),
121      'img/sha256': dataset_util.bytes_feature(key.encode('utf8')),
122      'img/encoded': dataset_util.bytes_feature(encoded_jpg),
123      'img/format': dataset_util.bytes_feature('jpeg'.encode('utf8')),
124      'img/xmin': dataset_util.float_list_feature(xmin),
125      'img/xmax': dataset_util.float_list_feature(xmax),
126      'img/ymin': dataset_util.float_list_feature(ymin),
127      'img/ymax': dataset_util.float_list_feature(ymax),
128      'img/text': dataset_util.bytes_list_feature(classes_text),
129      'img/label': dataset_util.int64_list_feature(classes),
130      'img/difficult': dataset_util.int64_list_feature(difficult_obj),
131      'img/truncated': dataset_util.int64_list_feature(truncated),
132      'img/view': dataset_util.bytes_list_feature(poses),
133  }))
134  return example
135
136  def create_tf(examples_list, annotations_dir, label_map_dict, dataset_type):
137    writer = None
138    if not os.path.exists(FLAGS.output_path+"/"+dataset_type):
139        os.mkdir(FLAGS.output_path+"/"+dataset_type)
140
141    j = 0
142    for idx, example in enumerate(examples_list):
143
144        if idx % 100 == 0:
145            logging.info('On image %d of %d', idx, len(examples_list))
146            print((FLAGS.output_path + "/tf_training_" + str(j) + ".record"))
147            writer = tf.python_io.TFRecordWriter(FLAGS.output_path + "/"+dataset_type+"/tf_training_" + str(j) + ".record")
148            j = j + 1
149
150        path = os.path.join(annotations_dir, os.path.basename(example).replace(".jpg", '.xml'))
151
152        with tf.gfile.GFile(path, 'r') as fid:
153            xml_str = fid.read()
154        xml = etree.fromstring(xml_str)
155        data = dataset_util.recursive_parse_xml_to_dict(xml)['annotation']
156
157        tf_example = dict_to_tf_example(data, FLAGS.data_dir, label_map_dict,
158                                    FLAGS.ignore_difficult_instances)
159        writer.write(tf_example.SerializeToString())
160
161  def main(_):
162
163    data_dir = FLAGS.data_dir
164    create_trainval_list(data_dir)
165
166    label_map_dict = label_map_util.get_label_map_dict(FLAGS.label_map_path)
167
168    examples_path = os.path.join(data_dir,'trainval.txt')
169    annotations_dir = os.path.join(data_dir, FLAGS.annotations_dir)
170    examples_list = dataset_util.read_examples_list(examples_path)
171
172    random.seed(42)
173    random.shuffle(examples_list)
174    num_examples = len(examples_list)
175    num_train = int(0.7 * num_examples)
176    train_examples = examples_list[:num_train]
177    val_examples = examples_list[num_train:]
178
179    create_tf(train_examples, annotations_dir, label_map_dict, "train")
180    create_tf(val_examples, annotations_dir, label_map_dict, "val")
181
182  if __name__ == '__main__':
183    tf.app.run()
184

Listing 9-2Transforming Images and Annotations in PASCAL VOC Format into TFRecord

清单 9-2 执行以下操作:

  1. 首先,调用函数create_trainval_list()创建一个文本文件,其中包含来自IMAGES子目录的所有图像的绝对路径列表。

  2. 将图像路径列表拆分为 70:30 的比例,以便为训练集和验证集生成单独的图像列表。

  3. 对于训练集中的每个图像,使用函数dict_to_tf_example()创建一个 TFRecord。TFRecord 包含图像的字节、边界框、带注释的类名和其他几个关于图像的元数据。TFRecord 被序列化并写入文件。创建多个 TFRecord 文件,文件数量取决于图像总数和每个 TFRecord 文件中包含的图像数量。

  4. 类似地,为每个验证图像创建 TFRecords 并序列化到文件中。

  5. 训练集和验证集保存在output目录下的两个独立的子目录中——分别是trainval

如果您克隆清单 9-1 中提到的 GitHub 库,Python 文件generic_xml_to_tf_record.py已经包含在内了。但是如果您克隆官方 TensorFlow 模型的存储库,那么您需要将清单 9-2 中的代码保存到generic_xml_to_tf_record.py中,并将其上传到您的 Colab 环境中(例如,上传到/content目录中)。

我们需要一个映射文件来映射类索引和类名。该文件包含 JSON 内容,通常具有扩展名.pbtxt。我们有六个缺陷类,我们可以手动编写标签映射文件,如下所示:

File name: steel_label_map.pbtxt
item {
  id: 1
  name: 'rolled-in_scale'
}

item {
  id: 2
  name: 'patches'
}

item {
  id: 3
  name: 'crazing'
}

item {
  id: 4
  name: 'pitted_surface'
}

item {
  id: 5
  name: 'inclusion'
}

item {
  id: 6
  name: 'scratches'
}

steel_label_map.pbtxt文件上传到您的 Colab 环境中的/content目录(或者您想要的任何其他目录,只要您在清单 9-3 中提供正确的路径)。

清单 9-3 中的脚本通过提供以下参数来执行generic_xml_to_tf_record.py:

  • --label_map_path:到steel_label_map.pbtxt的路径。

  • --data_dir:图像和注释目录所在的根目录。

  • --output_path:保存生成的 TFRecord 文件的路径。请确保该目录存在。如果没有,请在执行该脚本之前创建该目录。

  • --annotations_dir:标注 XML 文件所在的子目录名。

  • --image_dir:图像所在的子目录名。

1    %%shell
2    %tensorflow_version 1.x
3
4    python /content/generic_xml_to_tf_record.py \
5       --label_map_path=/content/steel_label_map.pbtxt \
6       --data_dir=/content/NEU-DET \
7       --output_path=/content/NEU-DET/out \
8       --annotations_dir=ANNOTATIONS \
9       --image_dir=IMAGES

Listing 9-3Executing generic_xml_to_tf_record.py That Creates TFRecord Files

运行清单 9-3 中的脚本,在输出目录中创建 TFRecord 文件。您将看到两个子目录——trainval——保存用于训练和验证的 TFRecords。

请注意,输出目录必须存在。否则,在执行清单 9-3 中的代码之前创建一个。

培训 SSD 模型

我们现在已经准备好了 TFRecord 格式的正确输入集来训练我们的 SSD 模型。培训步骤与我们在第六章中遵循的步骤完全相同。首先,根据我们之前创建的训练和验证集,下载一个预训练的 SSD 模型进行迁移学习。

清单 9-4 显示了我们在第六章中使用的相同代码(清单 6-5)。

1    %%shell
2    %tensorflow_version 1.x
3    mkdir pre-trained-model
4    cd pre-trained-model
5    wget http://download.tensorflow.org/models/object_detection/ssd_inception_v2_coco_2018_01_28.tar.gz
6    tar -xvf ssd_inception_v2_coco_2018_01_28.tar.gz

Listing 9-4Downloading a Pre-trained Object Detection Model

我们现在将编辑pipeline.config文件,如第六章“配置对象检测流水线”一节所述。清单 9-5 显示了根据当前配置编辑的pipeline.config文件的各个部分。

model {
  ssd {
    num_classes: 6
    image_resizer {
      fixed_shape_resizer {
        height: 300
        width: 300
      }
    }
   ......
        batch_norm {
          decay: 0.999700009823
          center: true
          scale: true
          epsilon: 0.0010000000475
          train: true
        }
      }
            override_base_feature_extractor_hyperparams: true
    }
    .....
    matcher {
      argmax_matcher {
        matched_threshold: 0.5
        unmatched_threshold: 0.5
        ignore_thresholds: false
        negatives_lower_than_unmatched: true
        force_match_for_each_row: true
      }
    }
   ......

  fine_tune_checkpoint: "/content/pre-trained-model/ssd_inception_v2_coco_2018_01_28/model.ckpt"
  from_detection_checkpoint: true
  num_steps: 100000

}
train_input_reader {
  label_map_path: "/content/steel_label_map.pbtxt"
  tf_record_input_reader {
    input_path: "/content/NEU-DET/out/train/*.record"
  }
}
eval_config {
  num_examples: 8000
  max_evals: 10
  use_moving_averages: false
}
eval_input_reader {
  label_map_path: "/content/steel_label_map.pbtxt"
  shuffle: false
  num_readers: 1
  tf_record_input_reader {
    input_path: "/content/NEU-DET/out/val/*.record"
  }
}

Listing 9-5Section of pipeline.config That Must to Be Edited to Point to the Appropriate Directory Structure

如清单 9-5 所示,我们必须编辑清单 9-5 中用黄色突出显示的部分。

num_classes: 6
fine_tune_checkpoint: path to pre-trained model checkpoint
label_map_path: path to .pbtxt file
input_path: path to the training TFRecord files.
label_map_path: path to the .pbtxt file
input_path: path to the validation TFRecord files.

编辑pipeline.config文件并上传到 Colab 环境。

使用清单 9-6 中所示的脚本执行模型训练。回顾第六章中的清单 6-6 来更新概念。

1    %%shell
2    %tensorflow_version 1.x
3    export PYTHONPATH=$PYTHONPATH:/content/models/research:/content/models/research/slim
4    cd models/research/
5    PIPELINE_CONFIG_PATH=/content/pre-trained-model/ssd_inception_v2_coco_2018_01_28/steel_defect_pipeline.config
6    MODEL_DIR=/content/neu-det-models/
7    NUM_TRAIN_STEPS=10000
8    SAMPLE_1_OF_N_EVAL_EXAMPLES=1
9    python object_detection/model_main.py \
10      --pipeline_config_path=${PIPELINE_CONFIG_PATH} \
11      --model_dir=${MODEL_DIR} \
12      --num_train_steps=${NUM_TRAIN_STEPS} \
13      --sample_1_of_n_eval_examples=$SAMPLE_1_OF_N_EVAL_EXAMPLES \
14      --alsologtostderr

Listing 9-6Executing the Model Training

当模型学习时,日志被打印在 Colab 控制台上。记下每个时期的损失,并根据需要调整模型的超参数。

导出模型

训练成功完成后,检查点保存在清单 9-6 第 6 行指定的目录中。

为了利用该模型进行实时检测,我们需要导出 TensorFlow 图。查看第六章的“导出 TensorFlow 图”部分,了解详细信息。

清单 9-7 展示了如何导出我们刚刚训练的 SSD 模型。

1    %%shell
2    %tensorflow_version 1.x
3    export PYTHONPATH=$PYTHONPATH:/content/models/research
4    export PYTHONPATH=$PYTHONPATH:/content/models/research/slim
5    cd /content/models/research
6
7    python object_detection/export_inference_graph.py \
8       --input_type image_tensor \
9       --pipeline_config_path /content/pre-trained-model/ssd_inception_v2_coco_2018_01_28/steel_defect_pipeline.config \
10      --trained_checkpoint_prefix /content/neu-det-models/model.ckpt-10000 \
11      --output_directory /content/NEU-DET/final_model

Listing 9-7Exporting the Model to the TensorFlow Graph

导出模型后,应该保存到 Google Drive。从 Google Drive 下载最终模型到你的本地电脑。我们可以使用这个模型从视频帧中实时检测表面缺陷。回顾第七章中介绍的概念。

模型评估

启动 TensorBoard 仪表板以评估模型质量。清单 9-8 展示了如何启动 TensorBoard 仪表板。

1    %tensorflow_version 2.x
2    %load_ext tensorboard
3    %tensorboard --logdir /drive/'My Drive'/NEU-DET-models/

Listing 9-8Launching the TensorBoard Dashboard

图 9-3 显示了 TensorBoard 的样本训练输出。

img/493065_1_En_9_Fig3_HTML.jpg

图 9-3

表面缺陷检测模型训练的张量板输出显示

预报

如果您已经按照第六章“使用训练模型检测物体”一节所述设置了工作环境,那么您应该已经具备了预测图像中表面缺陷所需的一切。只需更改清单 6-15 中的变量,并执行清单 9-9 中所示的 Python 代码。

model_path = "/Users/sansari/Downloads/neu-det-models/final_model"

labels_path = "/Users/sansari/Downloads/steel_label_map.pbtxt"

image_dir = "/Users/sansari/Downloads/NEU-DET/test/IMAGES"

image_file_pattern = "*.jpg"

output_path="/Users/sansari/Downloads/surface_defects_out"

Listing 9-9Variable Initialization Portion of Code from Listing 6-15

图 9-4 显示了不同类别缺陷预测的一些样本输出。

img/493065_1_En_9_Fig4_HTML.png

图 9-4

具有包围盒的缺陷表面的样本预测输出

实时缺陷检测器

遵循第七章中提供的说明,部署检测系统,该系统将从摄像机读取视频图像并实时检测表面缺陷。如果有多台摄像机连接到同一个设备,请为函数cv2.VideoCapture(x)中的参数x使用适当的值。默认情况下,x=0从电脑的内置摄像头读取视频。x=1x=2等的值。,将读取连接到计算机端口的视频。对于基于 IP 的摄像机,x的值应该是 IP 地址。

图像注释

在前面的所有例子中,我们使用了已经被标注和标记的图像。在本节中,我们将探讨如何为对象检测或人脸识别的图像添加注释。

有几个用于图像标注的开源和商业工具。我们将探索微软视觉对象标记工具(VoTT),这是一个用于图像和视频资产的开源注释和标记工具。VoTT 的源代码可以在 https://github.com/microsoft/VoTT 获得。

安装 VoTT

vott 需要 node.js 和 npm

要安装 NodeJS,请从官方网站 https://nodejs.org/en/download/ 下载您的操作系统的可执行二进制文件。比如下载安装 Windows Installer ( .msi)在 Windows OS 上安装 NodeJS,下载安装 macOS Installer ( .pkg)在 Mac 上安装,或者选择 Linux 二进制(x64)用于 Linux。

NPM 安装了 NodeJS。要检查您的计算机上是否安装了 NodeJS 和 NPM,请在您的终端窗口中执行以下命令:

node -v
npm -v

不同操作系统的 VoTT 安装程序在 GitHub ( https://github.com/Microsoft/VoTT/releases )维护。为您的操作系统下载安装程序。在撰写本书时,最新的 VoTT 版本是 2.1.1,可以从以下位置下载:

通过运行下载的可执行文件在您的计算机上安装 VoTT。

要从源运行 VoTT,请在终端上执行以下命令:

git clone https://github.com/Microsoft/VoTT.git
 cd VoTT
 npm ci
 npm start

npm start命令运行 VoTT 将启动电子版和浏览器版。两个版本的主要区别在于浏览器版本不能访问本地文件系统,而电子版本可以。

由于我们的图像在本地文件系统中,我们将探索 VoTT 的电子版本。

当您启动 VoTT 用户界面时,您将看到主屏幕,可以创建一个新项目,打开一个本地项目,或者打开一个云项目。

要注释图像,我们将按照下一节中的步骤进行。

创建连接

我们将创建两个连接:一个用于输入,另一个用于输出。

输入连接到存储未标记图像的目录。

输出连接是存储注释的地方。

目前,VoTT 支持连接到以下设备:

  • Azure Blob 存储

  • 必应图片搜索

  • 本地文件系统

我们将创建一个到本地文件系统的连接。要创建新连接,请单击左侧导航栏中的新建连接图标,以启动连接屏幕。单击左上角面板中与标签“连接”对应的加号图标。参见图 9-5 。

img/493065_1_En_9_Fig5_HTML.jpg

图 9-5

创建新连接

为提供者字段选择本地文件系统。单击选择文件夹打开本地文件系统目录结构。选择包含需要标记的输入图像的目录。单击保存连接按钮。

类似地,创建另一个连接来存储输出。

创建新项目

图像注释和标记的任务在一个项目下管理。要创建项目,请单击主页图标,然后单击新建项目以打开项目设置页面。参见图 9-6 。

img/493065_1_En_9_Fig6_HTML.jpg

图 9-6

“项目设置”页来创建新项目

“项目设置”页面上的两个重要字段是源连接和目标连接。为输入和输出目录选择我们在上一步中创建的适当连接。单击保存项目按钮。

创建类别标签

保存项目设置后,屏幕切换到主标签页面。要创建类别标签,请单击位于右侧面板右上角的标签标签对应的(+)图标(如图 9-7 所示)。创建所有的类别标签,如网纹、补丁、包含等。

img/493065_1_En_9_Fig7_HTML.jpg

图 9-7

创建分类标签

给图像贴标签

从左侧面板中选择一个图像缩略图,该图像将在主标记区域中打开。在图像的缺陷区域周围绘制矩形或多边形,并选择适当的标签来注释图像。参见图 9-8 。

img/493065_1_En_9_Fig8_HTML.jpg

图 9-8

在缺陷区域周围绘制矩形并选择类别标签来注释图像

同样,逐个注释所有图像。

出口标签

VoTT 支持以下导出格式:

  • Azure 自定义视觉服务

  • 微软认知工具包(CNTK)

  • TensorFlow(帕斯卡 VOC 和 TFRecords)

  • 通用 JSON 模式

  • 逗号分隔值(CSV)

我们将配置设置,以 TensorFlow TFRecord 文件格式导出我们的注释。

要进行配置,请单击左侧导航栏中的导出图标。导出图标看起来像一个向上倾斜的箭头。将打开“导出设置”页面。对于提供者字段,选择 TensorFlow 记录并点击保存导出设置按钮(图 9-9 )。

img/493065_1_En_9_Fig9_HTML.jpg

图 9-9

导出设置页面

返回到项目页面(单击标记编辑器图标)。点击顶部工具栏中的img/493065_1_En_9_Figb_HTML.gif图标,将注释导出到 TensorFlow 记录文件。

检查本地文件系统的输出文件夹。您会注意到,在输出目录中已经创建了一个名为包含TFRecords-export的目录。

导出到 TFRecord 格式还会生成一个包含类和索引映射的tf_label_map.pbtxt文件。

有关图像标签的最新信息和说明,请访问由微软维护的 VoTT 项目的官方 GitHub 页面: https://github.com/microsoft/VoTT

摘要

在这一章中,我们开发了一个表面缺陷检测系统。我们在具有六类缺陷的热轧钢带的已标记图像集上训练 SSD 模型。我们使用训练好的模型来预测图像和视频中的表面缺陷。我们还探索了一个名为 VoTT 的图像注释工具,它可以帮助注释图像并将标签导出为 TFRecord 格式。

十、云环境下的计算机视觉建模

训练最先进的卷积神经网络可能需要大量的计算机资源。根据训练样本的数量、网络配置和可用的硬件资源,训练网络可能需要几个小时或几天的时间。单个 GPU 可能不适合训练包含大量训练图像的复杂网络。模型需要在多个 GPU 上训练。单台机器上只能安装有限数量的 GPU。一台具有多个 GPU 的机器可能不足以对大量图像进行训练。如果在多台机器上训练模型,并且每台机器都有多个 GPU,那么速度会更快。

很难估计在某个时间框架内训练一个模型所需的 GPU 和机器数量。在大多数实际情况下,事先并不知道建模需要多少台机器以及培训将运行多长时间。还有,建模不是经常做的。预测准确度高的模型可能不需要几天、几周、几个月的重新训练,或者只要它给出准确的结果就可以。因此,在模型被重新训练之前,为建模而采购的任何硬件都可以保持空闲。

在云上建模是跨多台机器和 GPU 扩展培训的好方法。大多数云提供商以按需购买的模式提供虚拟机、计算资源和存储。这意味着您将只为模型学习期间使用的云资源付费。模型成功定型后,您可以将模型导出到应用服务器,在那里它将用于预测。此时,所有不再需要的云资源都可以删除,这样会降低成本。

TensorFlow 提供 API,用于在安装在单台或多台机器上的多个 CPU 和 GPU 上训练机器学习模型。

在这一章中,我们将探索分布式建模,并在云上训练大规模的计算机视觉模型。

本章的学习目标如下:

  • 探索用于分布式培训的 TensorFlow APIs

  • 在三个流行的云提供商上建立分布式 TensorFlow 集群,包括多个虚拟机和 GPU:Amazon Web Services(AWS)、Google Cloud Platform (GCP)和 Microsoft Azure

  • 在云上的分布式集群上训练计算机视觉模型

TensorFlow 分布式训练

本节将介绍 TensorFlow 分布式培训。

什么是分布式培训?

用于计算机视觉的最先进的神经网络从大量图像中计算出数百万个参数。如果所有的计算都在单个 CPU 或 GPU 上执行,那么训练是非常耗时的。此外,需要将整个训练数据集加载到内存中,这可能会超过单台机器的内存。

在分布式训练中,计算在多个 CPU 或 GPU 上同时执行,结果被组合以创建最终的模型。理想情况下,计算应该与 GPU 或 CPU 的数量成线性比例。换句话说,如果在一个 GPU 上训练一个模型需要 H 个小时,那么在 N 个 GPU 上训练模型就应该需要 H/N 个小时。

分布式训练中常用的并行实现方法有两种:数据并行和模型并行。TensorFlow 提供 API,通过在多个设备(CPU、GPU 或计算机)上拆分模型来分发培训。

数据并行性

大型训练数据集可以分成更小的小批。小型批处理可以分布在集群架构中的多台计算机上。SGD 可以在拥有少量数据的计算机上独立并行地计算权重。可以将来自各个计算机的结果合并到中央计算机,以获得最终的优化重量。

SGD 还可以通过在具有多个 CPU 或 GPU 的单台计算机中使用并行处理来优化权重。使用 SGD 算法计算优化权重的分布式和并行操作有助于更快地收敛。

图 10-1 显示了数据并行性的图示。

img/493065_1_En_10_Fig1_HTML.png

图 10-1

数据并行性和批量计算

数据并行可以通过以下两种方式实现:

img/493065_1_En_10_Fig3_HTML.png

图 10-3

使用参数服务器的异步数据并行

img/493065_1_En_10_Fig2_HTML.png

图 10-2

同步数据并行

  • 异步:在这种情况下,所有节点通过一个名为参数服务器的专用服务器对输入数据进行独立训练并异步更新变量,如图 10-3 所示。

  • 同步:在这种情况下,所有节点在不同的输入数据块上训练,并在每一步聚合梯度。梯度的同步是通过 all-reduce 方法完成的,如图 10-2 所示。

模型并行性

深度神经网络,如 Darknet,计算数十亿个参数。在单个 CPU 或 GPU 的内存中加载整个网络是一个挑战,即使批量很小。模型并行是一种将模型分成不同部分的方法,每个部分在物理计算机硬件的不同 CPU、GPU 或节点中对同一组数据执行操作。相同的数据批次被复制到集群中的所有节点,但是节点获得模型的不同部分。这些模型部件在不同的节点上同时对其输入数据集进行操作。

当模型的各个部分并行运行时,它们的共享参数需要同步。这种并行方法最适用于同一台机器上有多个 CPU 或 GPU 的情况,因为这些设备通过高速总线连接。

我们现在将探索 TensorFlow 如何在多个 GPU 或机器之间分配训练。

TensorFlow 分布策略

TensorFlow 提供了一个高级 API 来跨多个 GPU 或多个节点分发训练。API 是通过tf.distribute.Strategy类公开的。只需添加几行代码和少量代码更改,我们就可以分布我们在前面所有示例中探索过的神经网络。

我们可以将tf.distribute.Strategy与 Keras 一起使用,来分发使用 Keras API 构建的网络。我们也可以用它来分发定制的训练循环。一般来说,TensorFlow 中的任何计算都可以使用这个 API 来分发。

TensorFlow 支持以下类型的分发策略。

镜像策略

MirroredStrategy支持一台机器上多个 GPU 的同步分布式训练。模型的所有变量都在所有 GPU 上镜像。这些变量统称为 MirroredVariables。训练的计算在每个 GPU 上并行执行。通过应用相同的更新,变量彼此同步。

使用 all-reduce 算法在所有设备上更新 MirroredVariables。all-reduce 算法通过将所有设备上的张量相加来聚合它们,并使它们在每个设备上可用。图 10-2 展示了一个全归约算法的例子。这些算法是高效的,并且没有太多用于同步的通信开销。

有几种全归约算法。TensorFlow 在MirroredStrategy中使用英伟达 NCCL 作为默认的 all-reduce 算法。

我们将探讨如何使用MirroredStrategy来分配深度神经网络的训练。为了让它简单易懂,让我们修改清单 5-2 中的代码,并把它分发出去。参见清单 5-2 的第 11、19、24 行。下面是这几行代码的样子:

  • 第 11 行,列表 5-2 : model = tf.keras.models.Sequential([...])

  • 第 19 行,列表 5-2 : model.compile(...)

  • 第 24 行,列表 5-2 : history = model.fit(...)

以下是将清单 5-2 的训练并行化的步骤:

  1. 创建一个MirroredStrategy的实例。

  2. 将模型的创建和编译(清单 5-2 的第 11 行和第 19 行)移到MirroredStrategy对象的scope()方法中。

  3. 拟合模型(第 24 行,没有任何变化)。

清单 5-2 的所有其他行保持不变。

清单 10-1 展示了这个概念。

1   strategy = tf.distribute.MirroredStrategy()

2   with strategy.scope():
3     model = tf.keras.Sequential([...])
4     model.compile(...)

5   model.fit(...)

Listing 10-1Synchronous Distributed Training Using MirroredStrategy

因此,只需增加两行代码和少量调整,我们就可以将我们的训练分布到一台机器上的多个 GPU 上。

如清单 10-1 所示,在MirroredStrategy对象的scope()方法中,我们创建了想要以分布式并行方式运行的计算。MirroredStrategy对象负责在可用的 GPU 上复制模型的训练,聚合渐变等等。

每批输入在副本中平均分配。例如,如果输入批量大小为 16,我们将MirroredStrategy与两个 GPU 一起使用,每个 GPU 将在每个步骤中获得八个输入示例。我们应该适当地调整批量大小,以有效地利用 GPU 的计算能力。

tf.distribute.MirroredStrategy()方法创建默认对象,该对象使用 TensorFlow 可见的所有可用 GPU。如果您只想使用机器的部分 GPU,只需执行以下操作:

strategy = tf.distribute.MirroredStrategy(devices=["/gpu:0", "/gpu:1"])

这里有一个练习:修改清单 5-4 中所示的代码示例,并使用MirroredStrategy以分布式模式训练数字识别模型。

中央仓库战略

CentralStorageStrategy将模型变量放在 CPU 上,并在一台机器上的所有本地 GPU 上复制计算。除了将变量放置在 CPU 上而不是在 GPU 上复制它们之外,CentralStorageStrategyMirroredStrategy相似。

在写这本书的时候,CentralStorageStrategy是实验性的,将来可能会改变。要分发CentralStorageStrategy下的培训,只需将清单 10-1 的第 1 行替换为以下内容:

strategy = tf.distribute.experimental.CentralStorageStrategy()

多重工作镜像策略

MultiWorkerMirroredStrategy类似于MirroredStrategy。它将训练分布在多台机器上,每台机器都有一个或多个 GPU。它将模型中的所有变量复制到所有机器的每台设备上。这些进行计算的机器被称为工人

为了保持所有工人之间的变量同步,它使用CollectiveOps作为 all-reduce 通信方法。集合运算是 TensorFlow 图中的单个运算。它可以根据硬件、网络拓扑和张量大小在 TensorFlow 运行时自动选择 all-reduce 算法。

要将培训分配给MultiWorkerMirroredStrategy下的多个工人,只需将清单 10-1 的第 1 行替换为以下内容:

strategy = tf.distribute.experimental.MultiWorkerMirroredStrategy()

这将创建默认的MultiWorkerMirroredStrategy,并将CollectiveCommunication.AUTO作为CollectiveOps的默认设置。您可以选择以下两种CollectiveOps实现之一:

  • CollectiveCommunication.RING使用 gRPC 作为通信层实现基于环的集合。gRPC 是 Google 开发的远程过程调用的开源实现。要使用它,调用前面的实例化如下:

    strategy = tf.distribute.experimental.MultiWorkerMirroredStrategy(
            tf.distribute.experimental.CollectiveCommunication.RING)
    
    
  • CollectiveCommunication.NCCL使用英伟达 NCCL 实现集体。下面是一个用法示例:

    strategy = tf.distribute.experimental.MultiWorkerMirroredStrategy(
            tf.distribute.experimental.CollectiveCommunication.NCCL)
    
    
集群配置

TensorFlow 可以轻松地将培训分配给多个员工。但是它如何知道集群配置呢?在我们运行使用MultiWorkerMirroredStrategy来分发培训的代码之前,我们必须为将要参与模型培训的所有工人设置TF_CONFIG环境变量。TF_CONFIG将在本节稍后描述。

数据集分片

如何向员工提供数据?

当我们使用model.fit(x=train_datasets, epochs=3, steps_per_epoch=5)时,我们将训练集直接传递给fit()函数。在多工作人员培训中,数据集会自动分片。

容错

如果任何一个工作线程失败,整个集群都会失败。TensorFlow 中没有内置的故障恢复机制。然而,tf.distribute.Strategy with Keras 通过保存训练检查点提供了一种容错机制。如果任何一个工作者失败,所有其他工作者将等待失败的工作者重新启动。由于保存了检查点,一旦失败的工作人员恢复工作,训练将从停止的点开始。

为了使您的分布式集群容错,您必须保存训练检查点(查看第五章,了解如何使用回调保存检查点)。

贸易策略

张量处理单元(TPU)是由谷歌设计的专用集成电路(ASICs),旨在大幅加速机器学习工作负载。云 TPU 和 Google Colab 上都有 TPU。

在实现方面,TPUStrategyMirroredStrategy相同,只是模型变量被镜像到 TPU。清单 10-2 展示了如何实例化TPUStrategy

1   cluster_resolver = tf.distribute.cluster_resolver.TPUClusterResolver(
    tpu=tpu_address)
2   tf.config.experimental_connect_to_cluster(cluster_resolver)

3   tf.tpu.experimental.initialize_tpu_system(cluster_resolver)

4   tpu_strategy = tf.distribute.experimental.TPUStrategy(cluster_resolver)

Listing 10-2Instantiation of TPUStrategy

在第 1 行,通过将 TPU 地址传递给参数tpu=tpu_address来指定它。

参数服务器策略

ParameterServerStrategy中,模型变量被放置在一个专用的机器上,称为参数服务器。在这种情况下,一些机器被指定为工作机,一些被指定为参数服务器。当参数服务器中的变量被更新时,计算在所有工作线程的所有 GPU 上被复制。

ParameterServerStrategy的实现与MultiWorkerMirroredStrategy相同。我们必须在每台参与的机器上设置TF_CONFIG环境变量。TF_CONFIG接下来解释。

要分发ParameterServerStrategy下的培训,只需将清单 10-1 的第 1 行替换为以下内容:

strategy = tf.distribute.experimental.ParameterServerStrategy()

OneDeviceStrategy

有时,我们希望在将分布式代码转移到涉及多个设备的完全分布式系统之前,在单个设备(GPU)上测试我们的分布式代码。OneDeviceStrategy就是为此而设计的。当我们使用这种策略时,模型变量被放在一个指定的设备上。

要使用这个策略,只需使用下面的代码并替换清单 10-1 的第 1 行:

strategy = tf.distribute.OneDeviceStrategy(device="/gpu:0")

这个策略仅用于测试代码。在完全分布式环境中训练您的模型之前,请切换到其他策略。

值得注意的是,除了MirroredStrategy之外,所有之前用于分布式培训的策略目前都是实验性的。

TF_CONFIG: TensorFlow 群集配置

用于分布式训练的 TensorFlow 集群由一台或多台机器组成,称为 workers 。模型训练的计算在每个工人中进行。有一种专门的工人,叫做师傅首席工人,他们除了是一名普通工人之外还有额外的责任。首席工作者的额外职责包括保存检查点和为 TensorBoard 编写摘要文件。

TensorFlow 集群还可以包括用于参数服务器的专用机器。在ParameterServerStrategy的情况下,参数 server 是强制的。

TensorFlow 集群配置由一个TF_CONFIG环境变量指定。我们必须在集群上的所有机器上设置这个环境变量。

TF_CONFIG的格式是 JSON 文件,由两部分组成:clustertask

cluster组件提供了关于参与模型训练的工人和参数服务器的信息。这是工作人员主机名和通信端口的字典列表(例如,localhost:1234)。

task组件为当前任务指定工作者的角色。通常将工人列表中索引为 0 的第一个工人指定为主工人或主要工人。

表 10-1 描述了TF_CONFIG的键值对。

表 10-1

TF_CONFIG 格式描述

|

钥匙

|

描述

|

例子

|
| --- | --- | --- |
| cluster | 包含关键字workerchiefps的字典。这些键中的每一个都是参与培训的所有机器的hostname:port列表。 | cluster: {``worker:["host1:12345","host2:2345"]``} |
| task | 指定特定机器将执行的任务。它有以下键:type:指定工人类型,取一串workerchiefpsindex:任务的从零开始的索引。大多数分布式培训作业都有一个主任务、一个或多个参数服务器以及一个或多个工作人员。trial:执行超参数调谐时使用。该值设置要训练的试验次数。这有助于识别当前正在运行的试验。这需要一个包含试验编号的字符串值,从 1 开始。 | task: { type: chief, index:0}这表明host1:1234是主节点。 |
| job | 启动作业时使用的作业参数。这是可选的,在大多数情况下可以忽略。 |   |

TF_CONFIG 示例

假设我们有一个由三台机器组成的集群,希望用于分布式培训。这些机器的主机名分别是host1.localhost2.localhost3.local。假设它们都通过端口 8900 进行通信。

此外,假设每台机器都有以下角色:

worker: host1.local (chief worker)
worker: host2.local (normal worker)
ps: host3.local (parameter server)

三台机器上都需要设置的TF_CONFIG环境变量,如表 10-2 所示。

表 10-2

具有两个工作线程和一个参数服务器的三节点集群中的 TF_CONFIG 环境变量示例

|

掌握

|

工人

|

著名图象处理软件

|
| --- | --- | --- |
| 'cluster': {``'worker': ["host1.local:8900", "host2.local:8900"], "ps":["host3.local:8900"]``},``'task': {'type': worker, 'index': 0}``} | 'cluster': {``'worker': ["host1.local:8900", "host2.local:8900"], "ps":["host3.local:8900"]``},``'task': {'type': worker, 'index': 1}``} | 'cluster': {``'worker': ["host1.local:8900", "host2.local:8900"], "ps":["host3.local:8900"]``},``'task': {'type': ps, 'index': 0}``} |

带参数服务器的分布式培训示例代码

清单 10-3 ,清单 5-2 的修改版本,展示了ParameterServerStrategy的一个简单实现,将培训分配给多个工人。我们将探索如何在云上执行这些代码。

File name: distributed_training_ps.py
01: import argparse
02: import tensorflow as tf
03: from tensorflow_core.python.lib.io import file_io
04:
05: #Disable eager execution
06: tf.compat.v1.disable_eager_execution()
07:
08: #Instantiate the distribution strategy -- ParameterServerStrategy.
    #This needs to be in the beginning of the code.
09: strategy = tf.distribute.experimental.ParameterServerStrategy()
10:
11: #Parse the command line arguments
12: parser = argparse.ArgumentParser()
13: parser.add_argument(
14:      "--input_path",
15:      type=str,
16:      default="",
17:      help="Directory path to the input file. Could you be cloud storage"
18: )
19: parser.add_argument(
20:      "--output_path",
21:      type=str,
22:      default="",
23:      help="Directory path to the input file. Could you be cloud storage"
24: )
25: FLAGS, unparsed = parser.parse_known_args()
26:
27: # Load MNIST data using built-in datasets' download function
28: mnist = tf.keras.datasets.mnist
29: (x_train, y_train), (x_test, y_test) = mnist.load_data()
30:
31: #Normalize the pixel values by dividing each pixel by 255
32: x_train, x_test = x_train / 255.0, x_test / 255.0
33:
34: BUFFER_SIZE = len(x_train)
35: BATCH_SIZE_PER_REPLICA = 16
36: GLOBAL_BATCH_SIZE = BATCH_SIZE_PER_REPLICA * 2
37: EPOCHS = 10
38: STEPS_PER_EPOCH = int(BUFFER_SIZE/EPOCHS)
39:
40: train_dataset = tf.data.Dataset.from_tensor_slices((x_train,       y_train)).shuffle(BUFFER_SIZE).batch(GLOBAL_BATCH_SIZE)
41: test_dataset = tf.data.Dataset.from_tensor_slices((x_test, y_test)).batch(GLOBAL_BATCH_SIZE)
42:
43:
44: with strategy.scope():

45:     # Build the ANN with 4-layers
46:    model = tf.keras.models.Sequential([
47:    tf.keras.layers.Flatten(input_shape=(28, 28)),
48:    tf.keras.layers.Dense(128, activation="relu"),
49:    tf.keras.layers.Dense(60, activation="relu"),
50:    tf.keras.layers.Dense(10, activation="softmax")])
51:
52:    # Compile the model and set optimizer,loss function and metrics
53:    model.compile(optimizer='adam',
54:              loss='sparse_categorical_crossentropy',
55:              metrics=['accuracy'])
56:
57: #Save checkpoints to the output location--most probably on a cloud storage, such as GCS
58: callback = tf.keras.callbacks.ModelCheckpoint(filepath=FLAGS.output_path)
59: # Finally, train or fit the model
60: history = model.fit(train_dataset, epochs=EPOCHS, steps_per_epoch=STEPS_PER_EPOCH, callbacks=[callback])
61:
62: # Save the model to the cloud storage
63: model.save("model.h5")
64: with file_io.FileIO('model.h5', mode="r") as input_f:
65:     with file_io.FileIO(FLAGS.output_path+ '/model.h5', mode='w+') as output_f:
66:        output_f.write(input_f.read())

Listing 10-3Distributing Training Across Multiple Workers Using ParameterServerStrategy

清单 10-3 中的代码可以分为四个逻辑部分。

  • 读取和解析命令行参数(第 11 行到第 25 行)。它接受两个参数:用于保存检查点和最终模型的训练数据输入路径和输出路径。

  • 加载输入图像并创建训练集和测试集(第 27 到 41 行)。需要注意的是,ParameterServerStrategy不支持最后的部分批处理,当数据集在多个工作线程上不平衡时,将steps_per_epoch参数传递给model.fit()。注意第 38 行中steps_per_epoch的计算。

  • ParameterServerStrategy范围内创建和编译 Keras 模型(第 9 行和第 44 至 55 行)。以下是需要考虑的几个要点:

  • 在程序的开头创建ParameterServerStrategyMultiWorkerMirroredStrategy的实例,并在策略实例化后放入可能创建 ops 的代码。

  • 需要分发的代码部分必须包含在策略的范围内。

  • 第 44 行定义了scope()块,我们在其中包装了模型定义和编译。

  • 第 45 到 50 行在策略范围内创建模型。

  • 第 53 到 55 行在策略范围内编译模型。

  • 训练模型并保存检查点和最终模型(第 57 到 66 行)。

    1. 第 58 行创建了模型检查点对象,该对象被传递给模型的fit()函数,以便在模型训练时保存检查点。

    2. 第 60 行通过调用fit()函数触发模型训练。如前所述,传递给fit()函数的train_dataset由分发策略自动分发(在本例中为ParameterServerStrategy)。

    3. 第 63 行将完整的模型保存在本地目录中。第 64 到 66 行将本地模型复制到云存储中,比如谷歌云存储(GCS)或者亚马逊 S3。

    4. 请注意,第 57 行到第 66 行超出了策略的范围。

我们现在有了模型训练代码,它可以分布在多个工作人员中,并使用参数服务器以并行模式进行训练。接下来,我们将使用图 10-4 所示的架构在云上运行该培训。

img/493065_1_En_10_Fig4_HTML.png

图 10-4

TensorFlow 集群架构,在云虚拟机上有主服务器、工作服务器和参数服务器。数据和模型位于可扩展存储系统上

在云上运行分布式培训的步骤

我们将基于图 10-4 所示的架构在云上部署 TensorFlow 集群,并执行以下步骤来执行培训:

  1. 创建 TensorFlow 集群。

    1. 参数服务器、首席和工作节点:所有三个云提供商——AWS、GCP、Azure——都提供了基于浏览器的外壳和图形用户界面(UI)来创建和管理虚拟机。根据数据大小和神经网络的复杂性,我们可以创建基于 GPU 的虚拟机或基于 CPU 的虚拟机。
  2. 在所有虚拟机上安装 TensorFlow 和所有必备库:查看第一章了解安装必备库的说明。为了运行清单 10-3 中的代码,我们将只安装 TensorFlow。

  3. 创建云存储目录(也称为存储桶):根据云提供商,我们将创建以下内容之一:

    • AWS S3 铲斗

    • 谷歌云存储(GCS)桶

    • 天蓝色容器

  4. 上传 Python 代码并在每台机器上执行培训:使用云 shell 或任何其他 SSH 客户端,登录到每个节点并执行以下操作:

    • 将包含依赖项和模型训练代码(清单 10-3 )的 Python 包上传到每个节点。通过scp或任何其他文件传输协议上传代码。因为我们的代码是在 GitHub 中提交的,所以我们可以克隆存储库并跨所有节点下载代码。

      在每台机器上,克隆 GitHub 库,如清单 10-4 所示。

  • 我们需要在每台机器上设置特定于机器角色的TF_CONFIG环境变量,并执行 Python 代码进行分布式训练,如清单 10-5 所示。
git clone https://github.com/ansarisam/dist-tf-modeling.git

Listing 10-4Cloning the GitHub Repository

export TF_CONFIG=-CONFIG;python distributed_training_ps.py --input_path gs://cv_training_data --output_path gs://cv_distributed_model/output

Listing 10-5Executing Distributed Training

在每个节点上手动执行清单 10-5 中的命令效率不高,尤其是当有大量工作人员时。我们可以编写脚本,在大型集群上自动启动分布式培训。清单 10-4 中显示的 GitHub 库有一个 Python 脚本,可以用于自动化。为了了解其工作原理,我们将按照手动步骤,在每个虚拟机上逐一启动培训。

基于谷歌云的分布式培训

谷歌云平台(GCP)是一套云计算服务,运行在谷歌内部用于其最终用户产品(如谷歌搜索和 YouTube)的相同基础设施上。

我们将使用两个 GCP 服务来运行分布式培训。这两项服务是用于保存检查点和训练模型的谷歌云存储(GCS)和用于虚拟机(VM)的计算引擎。

我们开始吧!

注册 GCP 访问

如果您已经有一个 GCP 帐户,请跳过这一部分。如果没有,请在 https://cloud.google.com 创建一个 GCP 账户。谷歌为教育和学习提供 300 分的学分。我们将在本节的练习中使用这个免费帐户。您必须为业务和生产部署启用计费。

创建帐户后,在 https://console.cloud.google.com 登录谷歌云控制台。成功登录后,您将进入 GCP 仪表盘,如图 10-5 所示。

img/493065_1_En_10_Fig5_HTML.jpg

图 10-5

谷歌云平台仪表板

创建一个谷歌云存储桶

GCS 是 Google Cloud 上的一个高度持久的对象存储。它可以扩展到存储数十亿字节的数据。GCS 桶类似于文件系统中的目录。我们可以通过以下两种方式之一创建 GCS 存储桶。

从 Web 用户界面创建 GCS 存储桶

要使用 web UI 创建存储桶,请执行以下步骤:

img/493065_1_En_10_Fig8_HTML.jpg

图 10-8

时段详细信息页面

  1. Log in to the Google Cloud Console, at https://cloud.google.com. From the left-side navigation menu, click Storage and then Browse to launch the storage browser page (see Figure 10-6).

    img/493065_1_En_10_Fig6_HTML.jpg

    图 10-6

    存储菜单

  2. 单击页面顶部的创建存储桶按钮。

  3. On the next page, fill in the bucket name (e.g., cv_model) and click Continue. Select Region for the location type, select the appropriate location such as “us-east4 (Northern Virginia),” and then click Continue (see Figure 10-7).

    img/493065_1_En_10_Fig7_HTML.jpg

    图 10-7

    创建存储桶的表单

  4. 选择“标准”作为默认存储类别,然后单击“继续”。

  5. 为访问控制选择统一,然后单击继续。

  6. 单击“创建”按钮创建存储桶。

  7. 在下一页上,点击概览选项卡以查看铲斗详情(参见图 10-8 )。

从云外壳创建 GCS 存储桶

如果您已经使用 web UI 创建了存储桶,那么您不需要遵循这些步骤。使用命令行很容易创建 bucket。

  1. 点击右上角的img/493065_1_En_10_Figa_HTML.gif图标激活云壳。在图 10-5 中,该图标标有一个红色矩形。云壳将在屏幕底部打开(在同一个浏览器窗口中)。

  2. 在云 Shell 中执行清单 10-6 中的命令来创建 bucket。

gsutil mb -c regional -l us-east4 gs://cv_model

Listing 10-6gsutil Command to Create GCS Bucket

在清单 10-6 的命令中提供适当的区域和存储桶名称。如果您已经使用 web UI 创建了存储桶,请确保使用不同的存储桶名称。

gsutil是一个 Python 应用,允许我们从命令行访问云存储。

图 10-9 显示了gsutil命令在云壳中的执行。

img/493065_1_En_10_Fig9_HTML.jpg

图 10-9

gsutil 命令使用云 Shell 创建一个 bucket

启动 GCP 虚拟机

在我们的练习中,我们将启动以下类型的虚拟机:

  • 一个基于 GPU 的虚拟机:参数服务器

  • 一个基于 GPU 的虚拟机:主节点

  • 两个基于 GPU 的虚拟机:工作节点

虚拟机将在我们的 GCS 存储区所在的同一区域启动(上例中为 us-east4)。

要启动虚拟机,请执行以下步骤:

img/493065_1_En_10_Fig12_HTML.jpg

图 10-12

单击“更改”按钮启动“启动盘”选择页面

  1. 在主导航菜单中,单击计算引擎,然后单击“虚拟机实例”以启动显示先前启动的虚拟机列表的页面。

  2. Click Create to launch the web form that we need to fill in to create the instance. Figure 10-10 and Figure 10-11 show the instance creation form.

    img/493065_1_En_10_Fig11_HTML.jpg

    图 10-11

    实例创建表单的底部

    img/493065_1_En_10_Fig10_HTML.jpg

    图 10-10

    表单(顶部)提供创建虚拟机的信息

  3. 我们将创建四个基于 GPU 的虚拟机来创建集群。在实例创建表单中,点击“启动盘”下图片旁边的更改按钮(如图 10-12 )。

在下一个屏幕上(如图 10-13 所示),为操作系统选择 Linux 上的深度学习,为版本选择深度学习映像:TensorFlow 1.15.0 m45。

img/493065_1_En_10_Fig13_HTML.jpg

图 10-13

基于 CUDA 10 的 Linux 操作系统,预装 TensorFlow 1.15

img/493065_1_En_10_Fig14_HTML.jpg

图 10-14

创建的所有虚拟机的列表

  1. 图 10-14 显示了列出我们创建的所有四个虚拟机的屏幕。

登录到每个虚拟机的 SSH

我们将使用云 Shell 和gsutil登录到之前创建的所有四个虚拟机。激活云壳并点击+图标(在图 10-15 中用红色矩形标记)。

img/493065_1_En_10_Fig15_HTML.jpg

图 10-15

通过单击+图标创建云壳的多个选项卡

要通过 SSH 登录,请执行清单 10-7 中所示的命令(在四个 Cloud Shell 选项卡中)。

SSH to parameter server    gcloud compute ssh parameter-server
SSH to chief               gcloud compute ssh chief
SSH to worker-0            gcloud compute ssh worker-0
SSH to worker-1            gcloud compute ssh worker-1

Listing 10-7SSH to Log In to All 4VMs Using Cloud Shell

上传分布式培训的代码或克隆 GitHub 存储库

当通过 SSH 登录时,执行以下命令来克隆包含分布式模型训练代码的 GitHub 存储库(如清单 10-8 所示)。这需要在所有机器上进行。

git clone https://github.com/ansarisam/dist-tf-modeling.git

Listing 10-8Command to Clone the GitHub Repository

如果git命令不起作用,使用sudo apt-get install git命令安装git

安装先决条件和 TensorFlow

镜像“Linux 上的深度学习”已经预装了所有的先决条件和 TensorFlow。然而,如果我们想要配置我们的环境,执行清单 10-9 中的所有命令(查看章节 1 以获得详细说明)。

sudo apt-get update
sudo apt-get -y upgrade && sudo apt-get install -y python-pip python-dev
sudo apt-get install python3-dev python3-pip
sudo pip3 install -U virtualenv
mkdir cv
virtualenv --system-site-packages -p python3 ./cv
source ./cv/bin/activate
pip install tensorflow==1.15

Listing 10-9Installing Prerequisites Including TensorFlow

运行分布式培训

确保您已经在所有机器上克隆了 GitHub 库(如清单 10-8 所示)。此外,确保您通过 SSH(使用云 Shell)登录到每个虚拟机。在每个虚拟机上执行以下命令,启动分布式培训。

下面是参数服务器的命令:

cd dist_tf_modeling
export TF_CONFIG='{"task": {"index": 0, "type": "ps"}, "cluster": {"chief":["chief:8900"],"worker": ["worker-0:8900", "worker-1:8900"],  "ps":["parameter-server:8900"]}}';python distributed_training_ps.py --output_path gs://cv_model_v1

以下是主节点的命令:

cd dist_tf_modeling
export TF_CONFIG='{"task": {"index": 0, "type": "chief"}, "cluster": {"chief":["chief:8900"],"worker": ["worker-0:8900", "worker-1:8900"],  "ps":["parameter-server:8900"]}}';python distributed_training_ps.py --output_path gs://cv_model_v1

以下是 worker-0 节点的命令:

cd dist_tf_modeling
export TF_CONFIG='{"task": {"index": 0, "type": "worker"}, "cluster": {"chief":["chief:8900"],"worker": ["worker-0:8900", "worker-1:8900"],  "ps":["parameter-server:8900"]}}';python distributed_training_ps.py --output_path gs://cv_model_v1

以下是 worker-1 节点的命令:

cd dist_tf_modeling
export TF_CONFIG='{"task": {"index": 1, "type": "worker"}, "cluster": {"chief":["chief:8900"],"worker": ["worker-0:8900", "worker-1:8900"],  "ps":["parameter-server:8900"]}}';python distributed_training_ps.py --output_path gs://cv_model_v1

注意,所有参与节点必须能够通过TF_CONFIG中配置的端口与参数服务器通信。此外,节点必须对 GCS 存储桶拥有必要的读写权限。

模型检查点保存在 GCS 中的路径gs://cv_model_v1。训练好的模型在gs://cv_model_v1中保存为model.h5

使用 GPU 的 GCP 实例非常昂贵。如果不再使用它们以避免任何费用,您应该终止它们。

Azure 上的分布式培训

微软 Azure 是一种云计算服务,用于通过微软管理的数据中心构建、测试、部署和管理应用和服务。

清单 10-3 中的ParameterServerStrategy的分布式训练也将在 Azure 上工作,几乎与它在 GCP 上工作的方式相同。GCP 和 Azure 的区别在于我们创建虚拟机节点的方式。我们将探索一种不同的分布式训练策略,而不是重复在 Azure 集群上分发基于参数服务器的训练的过程。

我们将使用MirroredStrategy在具有多个 GPU 的单个节点上分发培训。在本节中,我们将了解以下内容:

  • 如何使用 web 界面在 Azure 上创建基于多 GPU 的虚拟机

  • 如何设置 TensorFlow 在 GPU 上运行

  • 要让清单 10-3 中的代码在多个 GPU 上工作,需要做哪些改变

  • 如何执行培训并监督培训

请注意,对 TensorFlow 的 GPU 支持适用于 Ubuntu 和 Windows 以及支持 CUDA 的卡。在本练习中,我们将创建一个基于 Ubuntu 18.4 的带有两个 GPU 的虚拟机。

在 Azure 上创建具有多个 GPU 的虚拟机

我们需要首先在 https://azure.microsoft.com/ 注册创建一个免费账户。然后进入 https://portal.azure.com/ 并登录你的账户。免费帐户允许您创建一个只有一个 GPU 的虚拟机。要创建具有多个 GPU 的虚拟机,您必须激活计费。要激活它,请遵循以下说明:

  1. 单击主导航(展开位于左上角的汉堡图标)。

  2. 选择成本管理+计费,点击“Azure 订阅”

  3. 单击添加。

  4. 遵循屏幕上的说明。

要创建虚拟机,请执行以下操作:

img/493065_1_En_10_Fig18_HTML.jpg

图 10-18

显示公共 IP 地址的虚拟机详细信息页面

img/493065_1_En_10_Fig17_HTML.jpg

图 10-17

设备大小(GPU)选择屏幕

img/493065_1_En_10_Fig16_HTML.jpg

图 10-16

用于创建虚拟机的 Azure 配置页面

  1. 在主页上,单击“虚拟机”图标

  2. 单击页面底部的“创建虚拟机”按钮,或单击左上角的+添加图标。

  3. 填写表格以配置虚拟机。图 10-16 显示了基本配置的顶部。对于字段图像,选择 Ubuntu Server 10.04 LTS 版。

  4. 我们将为虚拟机添加 GPU。点击图 10-16 中红色矩形内显示的链接“更改尺寸”。这将启动一个页面,显示您在图 10-3 的区域字段中选择的区域内所有可用设备的列表。

    如图 10-17 所示,首先清除所有过滤器,搜索 NC 找到 NC 系列的 GPU。我们将选择 NC12_Promo 虚拟机大小,这为我们提供了两个 GPU、12 个 vCPUs 和 112GB 内存。突出显示与 NC12_Promo 大小相对应的行,并单击位于屏幕底部的选择按钮。

    有关其他虚拟机大小的更多信息,请访问 https://docs.microsoft.com/en-us/azure/virtual-machines/linux/sizes-gpu

    如果与我们要使用的 GPU 对应的行呈灰色显示,这意味着您尚未升级您的订阅,或者您没有足够的配额来使用该虚拟机。

    您可以要求 Microsoft 增加您的配额。请访问 https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/error-resource-quota 了解有关如何申请增加配额的更多信息。

    On the Basic configuration screen (Figure 10-3), you can select either of the following (depending on your security policy) for the authentication type:

    • SSH 公钥:粘贴您将用来访问这个虚拟机的 SSH 公钥。

    • 密码:创建一个用户名和密码,您需要在通过 SSH 连接时提供。我们将在练习中使用该选项。

  5. 将其他内容保留为默认设置,然后单击屏幕左下角的“查看+创建”按钮。在下一页,我们将检查我们的配置,以确保所有内容都选择正确,然后最后单击“Create”按钮。如果一切顺利,将创建具有两个 GPU 的 VM。我们的虚拟机可能需要几分钟才能准备就绪。

    在这种情况下,我们没有创建任何磁盘,因为虚拟机附带了足够大的磁盘来运行我们的培训。这不是一个永久磁盘,如果虚拟机终止,它将被删除。因此,在生产中,您必须添加一个永久磁盘以避免丢失数据。

  6. 在我们的虚拟机准备就绪后,如果我们没有离开上一页,我们将看到一个警告,表明虚拟机已准备就绪。我们还可以返回主页,单击“Virtual machines”图标,查看我们创建的虚拟机列表。点击虚拟机名称,打开详细页面,如图 10-18 所示。

  7. 记下或复制公共 IP 地址,因为我们将需要它来 SSH 到我们的虚拟机。使用 SSH 客户端,如用于 Windows 的 Putty 或用于 Mac 或 Linux 的 Shell 终端,使用您之前选择的身份验证方法登录到虚拟机。以下是通过两种身份验证方法对 SSH 执行的命令:

    • 基于密码的认证:

      - ssh username@13.82.230.148
      username@13.82.230.148's password:
      
      
    • 基于 SSH 公钥的认证:

- ssh -i ~/sshkey.pem 13.82.230.148

如果成功通过身份验证,您将登录到虚拟机。

安装 GPU 驱动程序和库

要在基于 GPU 的机器上运行 TensorFlow,我们需要安装 GPU 驱动程序和一些库。执行以下步骤:

  1. 在终端上执行清单 10-10 的所有命令(确保通过 SSH 登录)。

  2. 如果成功添加了 NVIDIA 软件包库,请使用清单 10-11 中的命令安装 NVIDIA 驱动程序。

# Add NVIDIA package repositories
- wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/cuda-repo-ubuntu1804_10.1.243-1_amd64.deb
- sudo dpkg -i cuda-repo-ubuntu1804_10.1.243-1_amd64.deb
sudo apt-key adv --fetch-keys - https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/7fa2af80.pub
- sudo apt-get update
- wget http://developer.download.nvidia.com/compute/machine-learning/repos/ubuntu1804/x86_64/nvidia-machine-learning-repo-ubuntu1804_1.0.0-1_amd64.deb
- sudo apt install ./nvidia-machine-learning-repo-ubuntu1804_1.0.0-1_amd64.deb
sudo apt-get update

Listing 10-10Commands to Add NVIDIA Package Repositories

  1. 要使之前的安装生效,您需要重新启动虚拟机。在 SSH 终端 shell 上,执行命令sudo reboot

  2. 再次对虚拟机进行 SSH。

  3. 要测试 NVIDIA 驱动程序是否安装成功,请执行以下命令:

- sudo apt-get install --no-install-recommends nvidia-driver-418

Listing 10-11Installing the NVIDIA Driver

- nvidia-smi

该命令应该显示类似图 10-19 的内容。

img/493065_1_En_10_Fig19_HTML.jpg

图 10-19

nvidia-smi 命令的输出

  1. 我们现在将安装开发和运行时库(清单 10-12 )。这将是大约 4GB 的大小。

  2. 安装 TensorRT 库(清单 10-13 )。

- sudo apt-get install --no-install-recommends \
    cuda-10-1 \
    libcudnn7=7.6.4.38-1+cuda10.1  \
    libcudnn7-dev=7.6.4.38-1+cuda10.1

Listing 10-12Installing Development and Runtime Libraries

- sudo apt-get install -y --no-install-recommends libnvinfer6=6.0.1-1+cuda10.1 \
    libnvinfer-dev=6.0.1-1+cuda10.1 \
    libnvinfer-plugin6=6.0.1-1+cuda10.1

Listing 10-13Installing TensorRT

创建 virtualenv 并安装 TensorFlow

按照第一章中提供的说明安装您需要的所有库和依赖项。我们将执行清单 10-14 中的命令来安装我们当前练习所需的所有先决条件。

- sudo apt update
- sudo apt-get install python3-dev python3-pip
- sudo pip3 install -U virtualenv
- mkdir cv
- virtualenv --system-site-packages -p python3 ./cv
- source ./cv/bin/activate
(cv) - pip install  tensorflow
(cv) - pip install tensorflow-gpu

Listing 10-14Installing Python, Creating virtualenv, and Installing TensorFlow

实施镜像策略

参见清单 10-3 第 9 行。我们将创建一个MirroredStrategy的实例,而不是实例化ParameterServerStrategy,如下所示:

strategy = tf.distribute.MirroredStrategy()

清单 10-3 的所有其他行将保持不变。

我们已经向 GitHub 库提交了修改后的代码,该代码实现了用于分布式训练的MirroredStrategy。GitHub 库位置为 https://github.com/ansarisam/dist-tf-modeling.git ,包含MirroredStrategy代码的文件名为mirrored_strategy.py

运行分布式培训

通过 SSH 登录到我们之前创建的虚拟机。然后克隆 GitHub 库,如清单 10-15 所示。

- git clone https://github.com/ansarisam/dist-tf-modeling.git

Listing 10-15Cloning GitHub Repository

执行清单 10-16 中所示的 Python 代码来训练分布式模型。

- python dist-tf-modeling/mirrored_strategy.py

Listing 10-16Executing the MirroredStrategy-Based Distributed Model

如果一切顺利,您将看到终端控制台上打印的训练进度。图 10-20 显示了一些示例输出。

img/493065_1_En_10_Fig20_HTML.jpg

图 10-20

显示培训进度和评估结果的示例屏幕

为了检查 GPU 是否用于分布式训练,从不同的终端 SSH 到 VM,并执行清单 10-17 中所示的命令。

- nvidia-smi

Listing 10-17Checking the GPU Status

图 10-21 和 10-22 显示了该命令的输出。

img/493065_1_En_10_Fig22_HTML.jpg

图 10-22

训练过程中的 GPU 状态

img/493065_1_En_10_Fig21_HTML.jpg

图 10-21

培训开始前的 GPU 状态

如果您不再需要虚拟机,您应该终止它以避免任何成本,因为这些基于 GPU 的虚拟机非常昂贵。在终止虚拟机之前,请确保将训练好的模型和检查点下载并存储到永久存储中。

AWS 分布式培训

亚马逊网络服务(AWS)是亚马逊的一家子公司,以计量付费的方式向个人、公司和政府提供按需云计算平台和 API。在这一节中,我们将探索如何在 AWS 上训练一个分布式模型。

清单 10-3 的分布式培训也适用于 AWS。我们需要做的就是创建虚拟机,并按照我们为训练 GCP 模型所做的步骤进行操作。

类似地,我们可以在拥有多个 GPU 的 AWS 虚拟机上训练基于MirroredStrategy的模型。除了创建基于多 GPU 的虚拟机的方法之外,所有关于 Azure 的培训说明对于 AWS 都是一样的。

在这里,我们将探索另一种在云上训练可伸缩模型的技术。我们将学习如何使用 Horovod 在 AWS 上分发培训。我们先来了解一下 Horovod 框架是什么,以及如何在分布式模型训练中使用它。

霍罗沃德

官方文档将 Horovod 描述为用于 TensorFlow、Keras、PyTorch 和 Apache MXNet 的分布式深度学习训练框架。旨在让分布式深度学习变得快速易用。Horovod 是在优步开发的,由 Linux 基金会 AI 托管。

带有文档的源代码保存在位于 https://github.com/horovod/horovod 的 GitHub 库中。官方文档在 https://horovod.readthedocs.io/en/latest/summary_include.html

要使用 Horovod,我们需要对 TensorFlow 代码进行一些小的修改,以便进行模型训练。我们将使用清单 5-2 中的相同示例代码,并进行修改以使其与 Horovod 兼容。

如何使用 Horovod

当我们定义一个神经网络时,我们指定优化算法,例如 AdaGrad,我们希望我们的网络使用它来优化梯度。在分布式学习中,在多个节点中计算梯度,使用全部减少或全部收集算法进行平均,并使用优化算法进行进一步优化。Horovod 提供了一个包装器函数来将优化分配给所有参与节点,并将梯度优化任务委托给我们在 Horovod 中包装的原始优化算法。

我们将使用 Horovod 和 TensorFlow 将模型训练分发到多个节点,每个节点都有一个或多个 GPU。我们将处理清单 5-2 中的同一个代码示例,对它做一些小的修改,使它与 Horovod 兼容,并在 AWS 上执行培训。要使用 Horovod,我们需要对清单 5-2 的代码进行如下修改:

  1. horovod.tensorflow作为hvd导入。

  2. 使用hvd.init()初始化 Horovod。

  3. 使用以下命令锁定将处理渐变的 GPU(每个进程一个 GPU):

    config = tf.ConfigProto()
    config.gpu_options.visible_device_list = str(hvd.local_rank())
    
    
  4. 像我们通常在 TensorFlow 中做的那样建立模型。定义损失函数。

  5. 定义 TensorFlow 优化函数,如下所示:

  6. 调用 Horovod 分布式优化函数,并传递步骤 5 中的原始 TensorFlow 优化器。这是霍洛佛德的核心。

opt = tf.train.AdagradOptimizer(0.01 * hvd.size())

  1. 创建一个 Horovod 钩子向所有处理器广播训练变量。
opt = hvd.DistributedOptimizer(opt)

hooks = [hvd.BroadcastGlobalVariablesHook(0)]

0 表示所有处理器的等级为零(例如,第一个 GPU)。

  1. 最后,使用以下代码训练模型:
train_op = opt.minimize(loss)

让我们将所有这些放在一起,并将清单 5-2 中的代码转换成 Horovod 兼容的代码,这些代码可以通过多个 GPU 分布在多个节点上。清单 10-18 显示了我们可以在 Horovod 集群上使用 TensorFlow 作为执行引擎执行的完整代码。取自 Horovod 官方源代码的examples目录的代码维护在https.// github.com/horovod/horovod.git

File name: horovod_tensorflow_mnist.py
01: import tensorflow as tf
02: import horovod.tensorflow.keras as hvd
03:
04: # Horovod: initialize Horovod.
05: hvd.init()
06:
07: # Horovod: pin GPU to be used to process local rank (one GPU per process)
08: gpus = tf.config.experimental.list_physical_devices('GPU')
09: for gpu in gpus:
10:     tf.config.experimental.set_memory_growth(gpu, True)
11: if gpus:
12:     tf.config.experimental.set_visible_devices(gpus[hvd.local_rank()], 'GPU')
13:
14: # Load MNIST data using built-in datasets download function

15: mnist = tf.keras.datasets.mnist
16: (x_train, y_train), (x_test, y_test) = mnist.load_data()
17:
18: #Normalize the pixel values by dividing each pixel by 255
19: x_train, x_test = x_train / 255.0, x_test / 255.0
20:
21: BUFFER_SIZE = len(x_train)
22: BATCH_SIZE_PER_REPLICA = 16
23: GLOBAL_BATCH_SIZE = BATCH_SIZE_PER_REPLICA * 2
24: EPOCHS = 100
25: STEPS_PER_EPOCH = int(BUFFER_SIZE/EPOCHS)
26:
27: train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train)).repeat().shuffle(BUFFER_SIZE).batch(GLOBAL_BATCH_SIZE,drop_remainder=True)
28: test_dataset = tf.data.Dataset.from_tensor_slices((x_test, y_test)).batch(GLOBAL_BATCH_SIZE)
29:
30:
31: mnist_model = tf.keras.Sequential([
32:     tf.keras.layers.Conv2D(32, [3, 3], activation="relu"),
33:     tf.keras.layers.Conv2D(64, [3, 3], activation="relu"),
34:     tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
35:     tf.keras.layers.Dropout(0.25),
36:     tf.keras.layers.Flatten(),
37:     tf.keras.layers.Dense(128, activation="relu"),
38:     tf.keras.layers.Dropout(0.5),
39:     tf.keras.layers.Dense(10, activation="softmax")
40: ])
41:
42: # Horovod: adjust learning rate based on number of GPUs.
43: opt = tf.optimizers.Adam(0.001 * hvd.size())
44:
45: # Horovod: add Horovod DistributedOptimizer.
46: opt = hvd.DistributedOptimizer(opt)
47:
48: # Horovod: Specify `experimental_run_tf_function=False` to ensure TensorFlow

49: # uses hvd.DistributedOptimizer() to compute gradients.
50: mnist_model.compile(loss=tf.losses.SparseCategoricalCrossentropy(),
51:                     optimizer=opt,
52:                     metrics=['accuracy'],
53:                     experimental_run_tf_function=False)
54:
55: callbacks = [
56:     # Horovod: broadcast initial variable states from rank 0 to all other processes.
57:     # This is necessary to ensure consistent initialization of all workers when
58:     # training is started with random weights or restored from a checkpoint.
59:     hvd.callbacks.BroadcastGlobalVariablesCallback(0),
60:
61:     # Horovod: average metrics among workers at the end of every epoch.
62:     #
63:     # Note: This callback must be in the list before the ReduceLROnPlateau,
64:     # TensorBoard or other metrics-based callbacks.
65:     hvd.callbacks.MetricAverageCallback(),
66:
67:     # Horovod: using `lr = 1.0 * hvd.size()` from the very beginning leads to worse final
68:     # accuracy. Scale the learning rate `lr = 1.0` ---> `lr = 1.0 * hvd.size()` during
69:     # the first three epochs. See https://arxiv.org/abs/1706.02677 for details.
70:     hvd.callbacks.LearningRateWarmupCallback(warmup_epochs=3, verbose=1),
71: ]
72:
73: # Horovod: save checkpoints only on worker 0 to prevent other workers from corrupting them.
74: if hvd.rank() == 0:
75:     callbacks.append(tf.keras.callbacks.ModelCheckpoint('./checkpoint-{epoch}.h5'))
76:
77: # Horovod: write logs on worker 0.
78: verbose = 1 if hvd.rank() == 0 else 0
79:
80: # Train the model.
81: # Horovod: adjust the number of steps based on the number of GPUs.
82: mnist_model.fit(train_dataset, steps_per_epoch=500 // hvd.size(), callbacks=callbacks, epochs=24, verbose=verbose)

Listing 10-18Distributed Training with Horovod

使用 Horovod APIs 的代码部分在注释中用标签Horovod:标记。代码被适当地注释以帮助你理解如何使用 Horovod。所有其他代码行已经在第五章中解释过了。

在 AWS 上创建 Horovod 集群

您必须拥有 AWS 帐户,并且能够登录到您的 AWS web 控制台。如果您没有帐户,请在 https://aws.amazon.com 创建一个。AWS 免费提供某些类型的资源一年。但是,为了在 Horovod 集群上训练我们的模型,我们需要的资源类型可能需要您启用计费。您的帐户可能会为您将用于运行分布式培训的资源付费。您可能还需要请求增加某些资源(如 vCPU 和 GPU)的配额。在 https://aws.amazon.com/about-aws/whats-new/2019/06/introducing-service-quotas-view-and-manage-quotas-for-aws-services-from-one-location/ 有增加配额的说明。

Horovod 星团

AWS 提供了一种便捷的方式,只需点击几下鼠标就可以创建大规模可伸缩的 Horovod 集群。出于本节练习的目的,我们将创建一个包含两个节点的集群,每个节点只有一个 GPU。我们将执行以下操作:

img/493065_1_En_10_Fig32_HTML.jpg

图 10-32

将 id_rsa.pub 内容粘贴到 authorized_keys 文件的末尾

img/493065_1_En_10_Fig31_HTML.jpg

图 10-31

猫~/。ssh/id_ras.pub 输出。从 ssh-rsa 开始复制整个文本

img/493065_1_En_10_Fig30_HTML.jpg

图 10-30

ssh-keygen 输出

  1. 登录您的 AWS 帐户以访问 AWS 管理控制台; https://console.aws.amazon.com

  2. Click Services, then EC2, then Instances, and then Launch Instance (as shown in Figure 10-23).

    img/493065_1_En_10_Fig23_HTML.jpg

    图 10-23

    AWS 实例启动屏幕

  3. On the next screen, search for deep learning and select “Deep Learning AMI (Deep Learning AMI (Amazon Linux) Version 26.0 - ami-02bd97932dabc037b)” from the list of Amazon machine images (AMIs). See Figure 10-24.

    img/493065_1_En_10_Fig24_HTML.jpg

    图 10-24

    AMI 选择屏幕

  4. On the Choose an Instance Type page, select the GPU instances, type g2.2xlarge, set the vCPUs to 8, and set the memory to 15GB (as shown in Figure 10-25). You can select any GPU-based instance to meet your training requirements. Click the Next: Configuration Instance Details button at the bottom of the screen.

    img/493065_1_En_10_Fig25_HTML.jpg

    图 10-25

    选择实例类型选择屏幕

  5. 填写配置实例详细信息页面(如图 10-26 )。在 Number of Instances 字段中,我们输入 2 在集群中创建两个节点。您可以根据需要创建任意数量的节点来扩展您的训练。

    对于放置组,选中“将实例添加到放置组”框,并创建一个新组或添加到现有组。为安置组策略选择“集群”。

    We will leave everything else at the default settings on this page. Click the Next: Add Storage button.

    img/493065_1_En_10_Fig26_HTML.jpg

    图 10-26

    配置实例详细信息

  6. On the Add Storage page (as shown in Figure 10-27), provide the numbers for the disk size as per your needs. In this example, we will leave everything as is. Click the Next: Add Tags button and then the Next: Configure Security Groups button.

    img/493065_1_En_10_Fig27_HTML.jpg

    图 10-27

    添加存储页面

  7. Either create a new security group or use “Select an existing security group” if you want an existing security group (see Figure 10-28). Click Review and Launch followed by the Launch buttons. This will display a pop-up screen to either create or select a key pair. This key pair is used to log on to the VM using SSH. Follow the on-screen instructions (as shown in Figure 10-29).

    img/493065_1_En_10_Fig29_HTML.jpg

    图 10-29

    创建或选择密钥对的弹出屏幕

    img/493065_1_En_10_Fig28_HTML.jpg

    图 10-28

    用于创建或选择安全组的页面

  8. 成功启动实例后,我们将需要创建无密码 SSH,以使每个节点能够相互通信。我们在一台机器上创建一个 RSA 密钥,并将公钥从rsa_id.pub文件复制到所有节点的authorized_keys文件。以下是步骤:

    1. SSH 到机器 1,并从它的主目录执行命令ssh-keygen。每出现一个提示就按 Enter 键,直到看到指纹印在屏幕上。终端输出应如图 10-30 所示。

    2. ~/.ssh/id_rsa.pub的内容复制到~/.ssh/authorized_keys,如图 10-31 和图 10-32 所示。

    3. 将一台机器的id_rsa.pub内容复制到所有节点的authorized_keys文件的末尾。

    4. 重复该过程,在其余机器上创建ssh-keygen,并将id_rsa.pub的内容复制到每个节点的authorized_keys的末尾。

    5. 您应该通过 SSH 从一台机器登录到另一台机器来进行验证。它应该允许您不需要任何密码就可以登录。如果 SSH 提示输入密码,这意味着您没有从一台机器到另一台机器的无密码通信。为了让 Horovod 工作,所有机器必须能够在没有密码的情况下与其他机器进行通信。

运行分布式培训

我们在这个例子中使用的 AMI 包含以分布式模式启动培训的脚本。有一个位于/home/ec2-user/examples/horovod/tensorflowtrain_synthetic.sh shell 脚本。您可以修改这个脚本以指向您的代码并启动培训。

这个示例脚本在我们刚刚创建的 Horovod 集群上启动了一个基于 RestNet 的培训。只需按如下方式执行:

sh /home/ec2-user/examples/horovod/tensorflow/train_synthetic.sh 2

参数 2 表示群集中 GPU 的数量。

如果一切顺利,您将拥有一个经过训练的模型,您可以将它下载到您将托管使用该模型来预测结果的应用的机器上。

我们用的 AMI 已经安装了 Horovod。如果您想使用没有 Horovod 的 VM,请遵循下一节中的安装说明。

安装 Horovod

Horovod 依赖于 OpenMPI 来运行。首先,我们需要使用清单 10-19 中所示的命令安装 OpenMPI。

# Download Open MPI
- wget https://download.open-mpi.org/release/open-mpi/v4.0/openmpi-4.0.2.tar.gz
# Uncompress
- gunzip -c openmpi-4.0.2.tar.gz | tar xf -
- cd openmpi-4.0.2
- ./configure --prefix=/usr/local
- make all install

Listing 10-19Installing OpenMPI

安装 OpenMPI 需要几分钟时间。

OpenMPI 成功安装后,使用pip命令安装 Horovod,如清单 10-20 所示。

- pip install horovord

Listing 10-20Installing Horovord

清单 10-19 和 10-20 必须在集群的所有机器上执行。

运行 Horovod 以执行分布式培训

要在具有四个 GPU 的计算机上运行,请使用以下命令:

- horovodrun -np 4 -H localhost:4 python horovod_tensorflow_mnist.py

要在四台各有四个 GPU 的机器上运行,请运行以下命令:

- horovodrun -np 16 -H host1:4,host2:4,host3:4,host4:4 python horovod_tensorflow_mnist.py

您还可以在主机文件中指定主机节点。这里有一个例子:

- cat horovod_cluster.conf
host1 slots=2
host2 slots=2
host3 slots=2

这个例子列出了主机名(host1host2host3)以及每个主机名有多少个“插槽”。槽表示训练可能在一个节点上执行的 GPU 数量。

要在名为horovod_cluster.conf的文件中指定的主机上运行,请运行以下命令:

- horovodrun -np 6 -hostfile horovod_cluster.conf python horovod_tensorflow_mnist.py

使用 GPU 的虚拟机成本高昂。因此,如果不再使用虚拟机,建议将其终止。图 10-33 显示了如何终止实例。

img/493065_1_En_10_Fig33_HTML.jpg

图 10-33

终止 AWS 虚拟机

摘要

本章首先介绍了计算机视觉模型的分布式训练。我们探索了 TensorFlow 中支持的各种分布式策略,并学习了如何为分布式培训编写代码。

我们基于 GCP、Azure 和 AWS 云基础设施上的 MNIST 数据集训练了我们的手写识别模型。我们探索了在三个云平台上训练模型的三种不同技术。我们的示例训练基于 TensorFlow 支持的分发策略:ParameterServerStrategyMirroredStrategy。还学习了如何使用 Horovod 进行计算机视觉模型的大规模训练。

posted @   绝不原创的飞龙  阅读(36)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~
点击右上角即可分享
微信分享提示