深度学习高级应用指南-全-

龙哥盟 / 2024-10-06 / 原文

深度学习高级应用指南(全)

原文:Advanced Applied Deep Learning

协议:CC BY-NC-SA 4.0

一、简介和开发环境设置

这本书假设你有一些机器学习、神经网络和 TensorFlow 的基本知识。 1 这是我的第一本书,应用深度学习:基于案例的方法 (ISBN 978-1-4842-3790-8),由 Apress 于 2018 年出版,并假设你知道并理解其中的解释。第一卷的目标是解释神经网络的基本概念,并为您提供深度学习的良好基础,而这本书的目标是解释更高级的主题,如卷积和循环神经网络。为了能够从本书中获益,您至少应该对以下主题有基本的了解:

  • 单个神经元及其组成部分如何工作(激活函数、输入、权重和偏差)

  • 如何用 TensorFlow 或 Keras 在 Python 中开发一个简单的多层神经网络

  • 什么是优化器以及它是如何工作的(至少你应该知道梯度下降是如何工作的)

  • 有哪些高级优化器可用,它们是如何工作的(至少是 RMSProp、Momentum 和 Adam)

  • 什么是正规化,最常用的方法有哪些(1、2 和退学)

  • 什么是超参数

  • 如何训练网络,哪些超参数起着重要作用(例如,学习率或时期数)

  • 什么是超参数调整以及如何调整

在接下来的章节中,我们将根据需要在低级 TensorFlow APIs 和 Keras(在下一章中介绍)之间自由切换,以便能够专注于更高级的概念,而不是实现细节。我们不会讨论为什么特定的优化器工作得更好,或者神经元是如何工作的。如果有任何不清楚的地方,你应该把我的第一本书放在手边,作为参考。

此外,并不是书中所有的 Python 代码都像我的第一本书那样被广泛讨论。您应该已经很好地理解了 Python 代码。但是,所有的新概念都有解释。如果你有一个良好的基础,你会非常明白这是怎么回事(以及为什么)。这本书不适合深度学习的初学者。如果你是一个,我建议买我的第一本书,并在开始这本书之前研究它。

我希望这本书会令人愉快,你会从中学到很多东西。但最重要的是,我希望它会很有趣。

GitHub 知识库和配套网站

与我在本书中讨论的代码相关的 Jupyter 笔记本可以在 GitHub 上找到。 2 要找到它们的链接,请访问这本书的新闻网页。在书的封面附近,可以找到一个按钮,上面写着“下载代码”。它指向 GitHub 存储库。笔记本包含书中讨论的特定主题,包括书中没有的额外材料的练习。甚至可以使用“问题”直接在 GitHub 上留下反馈(参见 https://goo.gl/294qg4 了解如何操作)。能收到你的来信真是太好了。GitHub 库充当了这本书的伴侣,这意味着它包含的代码比书中印刷的还要多。如果你是老师,我希望你能为你的学生使用这些笔记本。这些笔记本和我在大学课程中使用的是一样的,为了让它们对教学有用,我做了很多工作。

最好的学习方法是尝试。不要只是阅读这本书:尝试,玩弄代码,改变它,并将其应用于具体的问题。

还有一个配套网站,在那里可以找到关于这本书的新闻和其他有用的资料。它的网址是 www.applieddeeplearningbook.com

要求的数学水平

有几个部分在数学上更先进。你应该理解这些概念中的大部分,而不需要数学细节。但是,了解什么是矩阵,如何进行矩阵相乘,什么是转置等等是必不可少的。你基本上需要很好地掌握线性代数。如果不是这样,我建议在读这本书之前先复习一下线性代数的书。对微积分的基本理解也是有益的。重要的是不要跳过数学部分。它们可以帮助你理解我们为什么以特定的方式做事。你也不应该害怕更复杂的数学符号。这本书的目标不是给你一个数学基础;我想你已经知道了。深度学习和神经网络(一般来说,机器学习)很复杂,任何试图说服你的人都是在撒谎或不理解它们。

我们不会花时间证明或推导算法或方程式。此外,我们将不讨论特定方程的适用性。比如我们在计算导数的时候不会讨论函数的可微性问题。假设我们可以应用你在这里找到的公式。多年的实际实现已经向深度学习社区表明,那些方法和方程按照预期工作。这种高级讨论需要一本单独的书。

Python 开发环境

在本书中,我们专门与来自谷歌的 TensorFlow 和 Keras 合作,我们专门用 Jupyter 笔记本开发我们的代码,所以知道如何处理它们至关重要。使用书中的代码时,以及通常使用 Python 和 TensorFlow 时,有三种主要的可能性:

  • 使用 Google Colab,一个基于云的 Python 开发环境。

  • 在笔记本电脑或台式机上本地安装 Python 开发环境。

  • 使用 Google 提供的 Docker 映像,安装 TensorFlow。

让我们看看不同的选择,以决定哪一个是最适合你的。

Google Colab

如前所述,Google Colab 是一个基于云的环境。这意味着不需要在本地安装任何东西。你只需要一个谷歌账户和一个网络浏览器(最好是谷歌浏览器)。服务的网址是 https://colab.research.google.com/

只需使用 Google 帐户登录,如果您没有,也可以创建一个。

然后你会看到一个窗口,你可以打开现有的笔记本,如果你已经有一些在云中,或者创建新的。窗口看起来如图 1-1 所示。

img/470317_1_En_1_Fig1_HTML.jpg

图 1-1

登录 Google Colab 时看到的第一个屏幕。在此屏幕截图中,最近选项卡是打开的。有时,您第一次登录时会打开“最近”选项卡。

在右下角,您可以看到新的 PYTHON 3 笔记本链接(通常以蓝色显示)。如果你点击向下的小三角形,你可以选择创建一个 Python 2 笔记本。在本书中,我们专门使用 Python 3。如果你点击链接,你会得到一个空的 Jupyter 笔记本,如图 1-2 所示。

img/470317_1_En_1_Fig2_HTML.jpg

图 1-2

在 Google Colab 中创建新笔记本时看到的空 Jupyter 笔记本

该笔记本的工作方式与本地安装的 Jupyter 笔记本完全一样,只是键盘快捷键(这里简称为快捷键)与本地安装的不同。例如,按 X 删除单元格在这里不起作用(但在本地安装中起作用)。万一你卡住了,你没有找到你想要的快捷方式,你可以按 Ctrl+Shift+P 来弹出一个你可以搜索快捷方式的窗口。图 1-3 显示了该弹出窗口。

img/470317_1_En_1_Fig3_HTML.jpg

图 1-3

当按 Ctrl+Shift+P 时,用于搜索键盘快捷键的弹出窗口。请注意,您可以键入命令名来搜索它。你不需要滚动浏览它们。

例如,在弹出窗口中键入 DELETE 会告诉您,要删除一个单元格,您需要键入 Ctrl+M,然后键入 d。从这个 Google 笔记本开始学习 Google Colab 的功能是一个非常好的地方:

https://Colab.research.Google.com/notebooks/basic_features_overview.ipynb ( https://goo.gl/h9Co1f )。

注意

Google Colab 有一个很大的特点:它允许你使用 GPU(图形处理单元)和 TPU(张量处理单元) 3 硬件加速来进行你的实验。到时候我会解释这有什么不同以及如何使用它,但是没有必要尝试本书中的代码和例子。

Google Colab 的优点和缺点

Google Colab 是一个很棒的开发环境,但它有积极和消极的方面。这里是一个概述。

阳性:

  • 你不必在你的笔记本电脑/台式机上安装任何东西。

  • 您可以使用 GPU 和 TPU 加速,而无需购买昂贵的硬件。

  • 它有极好的共享可能性。

  • 多人可以同时协作编辑同一个笔记本。像 Google Docs 一样,您可以在文档内(右上角,评论按钮的左侧)和单元格内(单元格的右侧)设置协作者。 4

底片:

  • 您需要在线才能使用它。如果你想在通勤时在火车上学习这本书,你可能做不到。

  • 如果您有敏感数据,并且不允许您将它上传到云服务,则您不能使用它。

  • 该系统是为研究和实验而设计的,因此您不应该将它用作生产环境的替代品。

蟒蛇

使用和测试本书中代码的第二种方法是在您的笔记本电脑或台式机上本地安装 Python 和 TensorFlow。最简单的方法是使用 Anaconda。在这里,我详细描述了如何做到这一点。

要设置它,首先为您的系统下载并安装 Anaconda(我在 Windows 10 上使用 Anaconda,但是代码不依赖于它,所以如果您愿意,可以随意使用 Mac 或 Linux 版本)。可以从 https://anaconda.org/ 处获得蟒蛇。

在网页的右侧(见图 1-4 ),您会找到一个下载 Anaconda 的链接。

img/470317_1_En_1_Fig4_HTML.jpg

图 1-4

在 Anaconda 网站的右上角,您会找到下载该软件的链接

只需按照说明安装即可。当您在安装后启动它时,您应该会看到如图 1-5 所示的屏幕。

img/470317_1_En_1_Fig5_HTML.jpg

图 1-5

启动 Anaconda 时看到的屏幕

Python 包(比如 numpy)定期更新,而且非常频繁。软件包的新版本可能会使您的代码停止工作。函数被弃用和删除,并添加新的函数。为了解决这个问题,在 Anaconda 中,您可以创建一个所谓的环境。这是一个容器,包含特定的 Python 版本和您决定安装的包的特定版本。例如,通过这种方式,您可以拥有一个用于 Python 2.7 和 numpy 1.10 的容器,以及一个用于 Python 3.6 和 numpy 1.13 的容器。您可能必须使用已经存在的基于 Python 2.7 的代码,因此您需要一个具有正确 Python 版本的容器。然而,与此同时,你的项目可能需要 Python 3.6。有了容器,你可以同时做所有这些。有时不同的包会发生冲突,所以您必须小心,并且您应该避免在您的环境中安装所有您感兴趣的包,主要是如果您在截止日期前使用它进行开发的话。没有什么比发现你的代码不再工作,而你不知道为什么更糟糕的了。

注意

当您定义一个环境时,尝试只安装您需要的包,并在更新它们时注意确保升级不会破坏您的代码(记住函数经常被弃用、删除、添加或更改)。请在升级之前查看更新文档,并且仅在需要更新的功能时才这样做。

在本系列的第一本书( https://goo.gl/ytiQ1k )中,我解释了如何用图形界面创建环境,因此您可以查看以了解如何创建,或者您可以阅读 Anaconda 文档的以下页面以详细了解如何使用环境:

https://conda.io/docs/user-guide/tasks/manage-environments.html

在下一节中,我们将创建一个环境并一次性安装 TensorFlow,只需一个命令。

以 Anaconda 的方式安装 TensorFlow

安装 TensorFlow 并不复杂,自从我的上一本书以来,去年已经变得容易多了。首先(我们在这里描述 Windows 的过程),进入 Windows 的开始菜单,输入 Anaconda。您应该在 Apps 下看到 Anaconda 提示符。(您应该会看到类似于图 1-6 所示的内容。)

img/470317_1_En_1_Fig6_HTML.jpg

图 1-6

如果你在 Windows 10 的开始菜单搜索栏中输入 Anaconda,你应该会看到至少两个条目:Anaconda Navigator 和 Anaconda Prompt。

启动 Anaconda 提示符(见图 1-7 )。命令行界面应该会启动。这与简单的cmd.exe命令提示符的区别在于,在这里,所有的 Anaconda 命令都可以被识别,而无需设置 Windows 环境变量。

img/470317_1_En_1_Fig7_HTML.jpg

图 1-7

这是您在启动 Anaconda 提示符时应该看到的内容。请注意,用户名会有所不同。您将不会看到“umber”(我的用户名),而是您的用户名。

然后只需键入以下命令:

conda create -n tensorflow tensorflow
conda activate tensorflow

第一行创建一个名为tensorflow的环境,其中已经安装了 TensorFlow,第二行激活该环境。然后,您只需要用这段代码安装以下软件包:

conda install Jupyter
conda install matplotlib
conda install scikit-learn

请注意,有时通过使用以下命令导入 TensorFlow,您可能会得到一些警告:

import tensorflow as tf

这些警告很可能是由过时的hdf5版本引起的。要解决这个问题(如果发生在您身上),请尝试使用以下代码更新它(如果您没有收到任何警告,可以跳过这一步):

conda update hdf5

你应该都准备好了。如果您在本地安装了兼容的 GPU 图形卡,只需使用以下命令安装 TensorFlow 的 GPU 版本:

conda create -n tensorflow_gpuenv tensorflow-gpu

这将创建一个安装了 TensorFlow 版本的环境。如果您这样做,请记住激活环境,然后像我们在这里所做的那样,在这个新环境中安装所有附加的软件包。请注意,要使用 GPU,您需要在系统上安装额外的库。您可以在 https://www.tensorflow.org/install/gpu 找到不同操作系统(Windows、Mac 和 Linux)的所有必要信息。请注意,TensorFlow 网站建议,如果您使用 GPU 进行硬件加速,请使用 Docker 映像(将在本章后面讨论)。

木星笔记型电脑位置

能够键入代码并让它运行的最后一步是使用本地安装的 Jupyter 笔记本。Jupyter 笔记本可以(根据官网)描述如下:

Jupyter Notebook 是一个开源的网络应用程序,允许你创建和共享包含实时代码、公式、可视化和叙述性文本的文档。用途包括数据清理和转换、数值模拟、统计建模、数据可视化、机器学习等等。

它在机器学习社区中被广泛使用,学习如何使用它是一个好主意。在 http://Jupyter.org/ 查看 Jupyter 项目网站。它很有启发性,包含了许多可能的例子。

您在本书中找到的所有代码都是使用 Jupyter 笔记本开发和测试的。我假设您对这种基于 web 的开发环境有一些经验。如果您需要复习,我建议您查看文档。你可以在 Jupyter 项目网站上找到它,地址: http://Jupyter.org/documentation.html

要在您的新环境中启动一个笔记本,您必须返回到 Anaconda Navigator 并点击您的tensorflow环境右边的三角形(如果您使用了不同的名称,您必须点击您的新环境右边的三角形),如图 1-8 所示。然后点击用 Jupyter 打开笔记本选项。

img/470317_1_En_1_Fig8_HTML.jpg

图 1-8

要在新环境中启动 Jupyter 笔记本,请单击 TensorFlow 环境名称右侧的三角形,然后选择“用 Jupyter 笔记本打开”

您的浏览器从用户文件夹中的文件夹列表开始。(如果你使用的是 Windows,这个通常位于c:\Users\<YOUR USER NAME>,在这里用你的用户名替换<YOUR USER NAME>。)从那里,您应该导航到要保存笔记本文件的文件夹。点击新建按钮可以新建一个,如图 1-9 所示。

img/470317_1_En_1_Fig9_HTML.jpg

图 1-9

要创建新的笔记本,请单击位于页面右上角的“新建”按钮,然后选择 Python 3

将会打开一个看起来如图 1-10 所示的新页面。

img/470317_1_En_1_Fig10_HTML.jpg

图 1-10

创建后立即出现的空 Jupyter 笔记本

例如,您可以在第一个“单元格”(您可以键入的矩形空间)中键入以下代码。

a=1
b=2
print(a+b)

要评估代码,请按 Shift+Enter,您应该立即看到结果(3),如图 1-11 所示。

img/470317_1_En_1_Fig11_HTML.jpg

图 1-11

在单元格中键入一些代码后,按 Shift+Enter 会计算单元格中的代码

a+b的结果为 3(如图 1-11 )。在结果之后会自动创建一个新的空单元格供您键入。

要了解更多关于如何添加注释、等式、内联图等等的信息,我建议你访问 Jupyter 网站,查看他们的文档。

注意

如果您忘记了笔记本在哪个文件夹中,您可以查看该页面的 URL。例如,在我的例子中,我有 http://localhost:8888/notebooks/Documents/Data % 20 science/Projects/Applied % 20 advanced % 20 learning % 20(book)/chapter % 201/AADL % 20-% 20 chapter % 201% 20-% 20 introduction . ipynb。请注意,URL 只是显示笔记本所在位置的文件夹的串联,由正斜杠分隔。一个%20字符表示一个空格。在这种情况下,我的笔记本在Documents/Data Science/Projects/...文件夹中。我经常同时用几个笔记本工作,知道每个笔记本放在哪里很有用,以防你忘记(我经常这样)。

Anaconda 的优点和缺点

现在让我们来看看 Anaconda 的正反两面。

阳性:

  • 该系统不需要活动的互联网连接(安装时除外),因此您可以在任何地方使用它(例如在火车上)。

  • 如果您正在处理无法上传到云服务的敏感数据,这是适合您的解决方案,因为您可以在本地处理数据。

  • 您可以严密控制您要安装的软件包和创建的环境。

底片:

  • 用这种方法让 TensorFlow GPU 版本工作(你需要额外的库才能工作)是相当烦人的。TensorFlow 网站建议使用 Docker 图像(见下一节)。

  • 直接与他人分享你的工作是复杂的。如果分享是必不可少的,你应该考虑谷歌 Colab。

  • 如果您使用的是必须在防火墙或代理服务器后工作的企业笔记本电脑,那么使用 Jupyter 笔记本电脑是一项挑战,因为有时笔记本电脑可能需要连接到互联网,如果您在防火墙后,这可能是不可能的。在这种情况下,安装包也可能很复杂。

  • 代码的性能取决于笔记本电脑或台式机的功率和内存。如果你用的是一台很慢或者很旧的机器,你的代码可能会很慢。在这种情况下,Google Colab 可能是更好的选择。

Docker 图像

第三种选择是使用安装了 TensorFlow 的 Docker 映像。Docker ( https://www.docker.com/ )在某种程度上有点像虚拟机。然而,与虚拟机不同,它不是创建一个完整的虚拟操作系统,而是仅仅添加主机上不存在的组件。 5 首先,你需要为你的系统下载 Docker。了解它并下载它的一个很好的起点是在 https://docs.docker.com/install/

首先,在你的系统上安装 Docker。完成后,您可以使用以下命令访问所有不同类型的 TensorFlow 版本。您必须在命令行界面中键入该命令(例如,Windows 中的cmd,Mac 上的终端,或者 Linux 下的 shell):

docker pull TensorFlow/TensorFlow:<TAG>

如果您想从 Python 3.5 获得最新的稳定的基于 CPU 的版本,那么您应该用正确的文本(可以想象称为标签)来替换<TAG>,比如latest-py3。你可以在 https://hub.docker.com/r/TensorFlow/TensorFlow/tags/ 找到所有标签的更新列表。

在本例中,您需要键入:

docker pull tensorflow/tensorflow:latest-py3

该命令自动下载正确的图像。Docker 是高效的,你可以要求它立即运行映像。如果在本地找不到,它就下载它。您可以使用以下命令启动映像:

docker run -it -p 8888:8888 tensorflow/tensorflow:latest-py3

如果您还没有下载,这个命令会下载基于 Python 3 的最新 TensorFlow 版本并启动它。如果一切顺利,您应该会看到如下输出:

C:\Users\umber>docker run -it -p 8888:8888 tensorflow/tensorflow:latest-py3
Unable to find image 'TensorFlow/TensorFlow:latest-py3' locally
latest-py3: Pulling from TensorFlow/TensorFlow
18d680d61657: Already exists
0addb6fece63: Already exists
78e58219b215: Already exists
eb6959a66df2: Already exists
3b57572cd8ae: Pull complete
56ffb7bbb1f1: Pull complete
1766f64e236d: Pull complete
983abc49e91e: Pull complete
a6f427d2463d: Pull complete
1d2078adb47a: Pull complete
f644ce975673: Pull complete
a4eaf7b16108: Pull complete
8f591b09babe: Pull complete
Digest: sha256:1658b00f06cdf8316cd8a905391235dad4bf25a488f1ea989a98a9fe9ec0386e
Status: Downloaded newer image for TensorFlow/TensorFlow:latest-py3
[I 08:53:35.084 NotebookApp] Writing notebook server cookie secret to /root/.local/share/Jupyter/runtime/notebook_cookie_secret
[I 08:53:35.112 NotebookApp] Serving notebooks from local directory: /notebooks
[I 08:53:35.112 NotebookApp] The Jupyter Notebook is running at:
[I 08:53:35.112 NotebookApp] http://(9a30b4f7646e or 127.0.0.1):8888/?token=f2ff836cccb1d688f4d9ad8c7ac3af80011f11ea77edc425
[I 08:53:35.112 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
[C 08:53:35.113 NotebookApp]

    Copy/paste this URL into your browser when you connect for the first time, to login with a token:
        http://(9a30b4f7646e or 127.0.0.1):8888/?token=f2ff836cccb1d688f4d9ad8c7ac3af80011f11ea77edc425

此时,您可以简单地连接到一个从 Docker 映像运行的 Jupyter 服务器。

在之前所有消息的末尾,您会找到使用 Jupyter 笔记本时应该在浏览器中键入的 URL。当您复制 URL 时,只需将cbc82bb4e78c or 127.0.0.1替换为127.0.0.1。将其复制到浏览器的 URL 字段中。页面应该如图 1-12 所示。

img/470317_1_En_1_Fig12_HTML.jpg

图 1-12

使用 Docker image Jupyter 实例时看到的导航窗口

需要注意的是,如果您使用开箱即用的笔记本,您创建的所有文件和笔记本将在下次启动 Docker 映像时消失。

注意

如果您按原样使用 Jupyter 笔记本服务器,并创建新的笔记本和文件,它们将在您下次启动服务器时全部消失。您需要安装一个驻留在您机器上的本地目录,这样您就可以在本地保存文件,而不是在映像本身中。

让我们假设您使用的是 Windows 机器,并且您的笔记本位于本地c:\python。要在 Docker 映像中使用 Jupyter 笔记本时查看和使用它们,您需要以如下方式使用-v选项启动 Docker 实例:

docker run -it -v c:/python:/notebooks/python -p 8888:8888 TensorFlow/TensorFlow:latest-py3

这样,你就可以在 Docker 镜像中的一个名为python的文件夹中看到c:\python下的所有文件。您可以使用-v选项指定本地文件夹(文件位于本地)和 Docker 文件夹名称(使用 Jupyter 笔记本从 Docker 映像查看文件时,您希望看到的位置):

-v <LOCAL FOLDER>:/notebooks/<DOCKER FOLDER>

在我们的示例中,<LOCAL FOLDER>c:/python(您希望用于本地保存的笔记本的本地文件夹),而<DOCKER FOLDER>python(您希望 Docker 将文件夹与笔记本安装在一起的位置)。运行代码后,您应该会看到如下所示的输出:

[I 09:23:49.182 NotebookApp] Writing notebook server cookie secret to /root/.local/share/Jupyter/runtime/notebook_cookie_secret
[I 09:23:49.203 NotebookApp] Serving notebooks from local directory: /notebooks
[I 09:23:49.203 NotebookApp] The Jupyter Notebook is running at:
[I 09:23:49.203 NotebookApp] http://(93d95a95358a or 127.0.0.1):8888/?token=d564b4b1e806c62560ef9e477bfad99245bf967052bebf68
[I 09:23:49.203 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
[C 09:23:49.204 NotebookApp]

    Copy/paste this URL into your browser when you connect for the first time, to log in with a token:
        http://(93d95a95358a or 127.0.0.1):8888/?token=d564b4b1e806c62560ef9e477bfad99245bf967052bebf68

现在,当你用最后一条消息末尾给出的 URL 启动你的浏览器时(这里你必须用127.0.0.1代替93d95a95358a or 127.0.0.1,你应该会看到一个名为python的 Python 文件夹,如图 1-13 中圈出的文件夹所示。

img/470317_1_En_1_Fig13_HTML.jpg

图 1-13

使用正确的-v 选项启动 Docker 映像时应该看到的文件夹。在该文件夹中,您现在可以看到本地保存在 c:\python 文件夹中的所有文件。

您现在可以看到所有本地保存的笔记本,如果您在文件夹中保存了一个笔记本,当您重新启动 Docker 映像时,您会再次找到它。

最后一点,如果你有一个兼容的 GPU 供你使用, 6 你可以直接下载最新的 GPU TensorFlow 版本,例如,使用标签,latest-gpu。你可以在 https://www.TensorFlow.org/install/gpu 找到更多信息。

码头工人形象的利与弊

让我们来看看这个选择的积极和消极方面。

阳性:

  • 你不需要在本地安装任何东西,除了 Docker。

  • 安装过程很简单。

  • 你自动获得最新版本的 TensorFlow。

  • 如果要使用 TensorFlow 的 GPU 版本,是首选选择。

底片:

  • 您不能在多种环境中使用这种方法进行开发,也不能使用多个版本的包。

  • 安装特定的软件包版本很复杂。

  • 共享笔记本比其他选项更复杂。

  • 运行 Docker 映像的硬件限制了代码的性能。

你应该选择哪个选项?

您可以快速地从所描述的任何选项开始,稍后继续另一个选项。您的代码将继续工作。您需要注意的唯一一件事是,如果您在 GPU 支持下开发大量代码,然后试图在没有 GPU 支持的系统上运行这些代码,您可能需要大量修改代码。为了决定哪个选项最适合你,我提供了以下问题和答案。

  • 您需要处理敏感数据吗?

    如果您需要处理无法上传到云服务上的敏感数据(例如,医疗数据),您应该选择本地安装 Anaconda 或 Docker。你不能使用 Google Colab。

  • 你经常在没有互联网连接的环境中工作吗?

    如果您想在没有活跃的互联网连接的情况下编写代码和训练您的模型(例如,在通勤时),您应该选择 Anaconda 或 Docker 的本地安装,因为 Google Colab 需要活跃的互联网连接。

  • 您是否需要与其他人并行使用同一台笔记本电脑?

    如果你想和其他人分享你的工作,并和其他人同时工作,最好的解决方案是使用 Google Colab,因为它提供了很好的分享体验,这是本地安装选项所缺少的。

  • 你不想(或不能)在你的笔记本电脑/台式机上安装任何东西?

    如果你不想或不能在你的笔记本电脑或台式机(也许是企业笔记本电脑)上安装任何东西,你应该使用 Google Colab。你只需要一个互联网连接和一个浏览器。请记住,有些功能只适用于谷歌浏览器,不适用于 ie 浏览器。

注意

启动并运行 TensorFlow 并开始开发模型的最简单方法可能是使用 Google Colab,因为它不需要任何安装。直接上网站,登录,开始写代码。如果您需要在本地工作,Docker 选项可能是最简单的解决方案。启动并运行它非常简单,您可以使用最新版本的 TensorFlow 进行工作。如果您需要多种环境的灵活性以及对所使用的每个包的版本的精确控制,那么您唯一的解决方案就是执行 Python 开发环境(如 Anaconda)的完整本地安装。

Footnotes 1

TensorFlow、TensorFlow 徽标和任何相关标志是 Google Inc .的商标。

2

如果你不知道 GitHub 是什么,你可以通过本指南在 https://guides.github.com/activities/hello-world/ 学习基础知识

3

在深度学习中,大部分计算都是在张量(多维数组)之间完成的。GPU 和 TPU 是经过高度优化的芯片,可以在非常大的张量(多达一百万个元素)之间执行此类计算(如矩阵乘法)。在开发网络时,可以让 GPU 和 TPU 在 Google Colab 中进行如此昂贵的计算,加快网络的训练。

4

Google Colab 文档可以在 https://goo.gl/bKNWy8 找到

5

opensource.com/resources/what-docker【最后访问时间:2018 年 12 月 19 日】

6

https://developer.nvidia.com/cuda-gpus 可以找到所有兼容 GPU 的列表,在 https://www.TensorFlow.org/install/gpu 可以找到 TensorFlow 信息。

二、TensorFlow:高级主题

TensorFlow 库从第一次出现到现在已经走过了很长的路。尤其是在去年,更多的功能变得可用,可以使研究人员的生活变得容易得多。像渴望执行和 Keras 这样的东西允许科学家更快地测试和实验,并以以前不可能的方式调试模型。对于任何研究人员来说,了解这些方法并知道什么时候使用它们是有意义的是至关重要的。在这一章中,我们将研究其中的几个:渴望执行、GPU 加速、Keras、如何冻结网络的部分并只训练特定部分(经常使用,尤其是在迁移学习和图像识别中),最后是如何保存和恢复已经训练好的模型。那些技术技能将会非常有用,不仅仅是学习这本书,而是在现实生活的研究项目中。

本章的目标不是教你如何从头开始使用 Keras,也不是教你所有错综复杂的方法,而是向你展示一些解决一些特定问题的高级技术。将不同的部分视为提示。记住,学习官方文档总是一个好主意,因为方法和函数经常改变。在这一章中,我将避免复制官方文档,而是给你一些非常有用和经常使用的技术的高级例子。要深入了解(双关语),你应该研究一下 https://www.tensorflow.org/ 的官方 TensorFlow 文档。

要学习和理解高级主题,需要在 Tensorflow 和 Keras 方面有良好的基础。了解 Keras 的一个非常好的资源是 Jojo John Moolayil ( https://goo.gl/mW4Ubg )的书Learn Keras for Deep Neural Networks——用 Python 进行现代深度学习的快速方法。如果你没有太多的经验,我建议你在开始这本书之前,先拿到这本书研究一下。

Tensorflow 急切执行

TensorFlow 的急切执行是命令式编程环境。 1 这也意味着一个计算图在你没有注意到的情况下在后台建立起来了。操作会立即返回具体值,而不是先打开一个会话,然后运行它。这使得从 TensorFlow 开始非常容易,因为它类似于经典的 Python 编程。急切执行提供了以下优势:

  • 更容易的调试:您可以使用经典的 Python 调试工具来调试您的模型,以便立即进行检查

  • 直观的界面:您可以自然地构建您的代码,就像在经典 Python 程序中一样

  • 提供对 GPU 加速的支持

要使用这种执行模式,您需要最新版本的 TensorFlow。如果您尚未安装,请参见第一章了解如何安装。

启用急切执行

要启用急切执行,可以使用以下代码:

import tensorflow as tf
tf.enable_eager_execution()

请记住,您需要在开始时,在import s 之后和任何其他命令之前这样做。否则,您将得到一条错误消息。如果是这种情况,你可以简单地重启笔记本的内核。

例如,你可以很容易地将两个张量相加

print(tf.add(1, 2))

并立即得到这个结果

tf.Tensor(3, shape=(), dtype=int32)

如果您不启用急切执行并再次尝试print命令,您将会得到这个结果

Tensor("Add:0", shape=(), dtype=int32)

因为 TensorFlow 还没有对节点求值。您需要以下代码来获得结果:

sess = tf.Session()
print(sess.run(tf.add(1,2)))
sess.close()

结果当然是 3。该代码的第二个版本创建了一个图形,然后打开一个会话,然后对其进行评估。有了渴望,你马上就能得到结果。您可以很容易地检查您是否启用了急切执行:

tf.executing_eagerly()

它应该会返回TrueFalse,这取决于您是否启用了它。

执行迅速的多项式拟合

让我们在一个实际例子中检查一下急切执行是如何工作的。 2

请记住,您需要以下导入:

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import tensorflow.contrib.eager as tfe
tf.enable_eager_execution()

让我们为这个函数生成一些假数据

$$ y={x}³-4{x}²-2x+2 $$

用代码

x = np.arange(0, 5, 0.1)
y = x**3 - 4*x**2 - 2*x + 2
y_noise = y + np.random.normal(0, 1.5, size=(len(x),))

我们创建了两个 numpy 数组:y,它包含对数组x求值的函数,以及y_noise,它包含添加了一些噪声的y。你可以在图 2-1 中看到这些数据。

img/470317_1_En_2_Fig1_HTML.jpg

图 2-1

该图显示了两个 numpy 数组 y(地面真实值)和 y_noise(地面真实值+噪声)

现在我们需要定义一个我们想要拟合的模型,并定义我们的损失函数(我们想要用 TensorFlow 最小化的那个)。请记住,我们面临的是一个回归问题,所以我们将使用均方差(MSE)作为我们的损失函数。我们需要的函数如下:

class Model(object):
  def __init__(self):
    self.w = tfe.Variable(tf.random_normal([4])) # The 4 parameters

  def f(self, x):
    return self.w[0] * x ** 3 + self.w[1] * x ** 2 + self.w[2] * x + self.w[3]

def loss(model, x, y):
    err = model.f(x) - y
    return tf.reduce_mean(tf.square(err))

现在很容易最小化损失函数。首先让我们定义一些我们需要的变量:

model = Model()
grad = tfe.implicit_gradients(loss)
optimizer = tf.train.AdamOptimizer()

然后让我们用一个for循环,最小化损失函数:

iters = 20000
for i in range(iters):
  optimizer.apply_gradients(grad(model, x, y))
  if i % 1000 == 0:
        print("Iteration {}, loss: {}".format(i+1, loss(model, x, y).numpy()))

这段代码将产生一些输出,向您显示每 1000 次迭代的损失函数值。注意,我们将所有数据以一个批处理的形式提供给优化器(因为我们只有 50 个数据点,所以不需要使用小批处理)。

您应该会看到如下几行输出:

Iteration 20000, loss: 0.004939439240843058

在图 2-2 中可以看到损失函数图与迭代次数的关系,并且如预期的那样,损失函数图不断减小。

img/470317_1_En_2_Fig2_HTML.jpg

图 2-2

损失函数(MSE)与迭代次数的关系如预期的那样在下降。这清楚地表明,优化器在寻找最佳权重以最小化损失函数方面做得很好。

在图 2-3 中,您可以看到优化器通过最小化权重找到的函数。

img/470317_1_En_2_Fig3_HTML.jpg

图 2-3

红色虚线是通过使用 Adam 优化器最小化损失函数获得的函数。该方法非常有效,可以高效地找到正确的函数。

你应该注意的是,我们没有明确地创建一个计算图,然后在会话中对其进行评估。我们只是像对待任何 Python 代码一样使用这些命令。例如,在代码中

for i in range(iters):
  optimizer.apply_gradients(grad(model, x, y))

我们简单地在循环中调用 TensorFlow 操作,而不需要会话。有了热切的执行,很容易在没有太多开销的情况下快速开始使用 TensorFlow 操作。

急切执行的 MNIST 分类

为了给出另一个如何构建一个具有热切执行的模型的例子,让我们为著名的 MNIST 数据集构建一个分类器。这是一个包含 60000 个手写数字图像(从 0 到 9)的数据集,每个图像的灰度级为 28x28(每个像素的值范围为 0 到 255)。如果你没有看过 MNIST 数据集,我建议你在 https://goo.gl/yF0yH 查看原版网站,在那里你会找到所有的信息。我们将实现以下步骤:

  • 加载数据集。

  • 标准化特征并一次性编码标注。

  • 转换tf.data.Dataset对象中的数据。

  • 建立一个两层的 Keras 模型,每层有 1024 个神经元。

  • 定义优化器和损失函数。

  • 直接使用梯度和优化器来最小化损失函数。

我们开始吧。

在遵循代码的同时,请注意我们是如何像处理普通 numpy 那样实现每一部分的,这意味着不需要创建图形或打开 TensorFlow 会话。

首先,让我们使用keras.datasets.mnist包加载 MNIST 数据集,对其进行整形,并对标签进行一次性编码。

import tensorflow as tf
import tensorflow.keras as keras

num_classes = 10

mnist = tf.keras.datasets.mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()

image_vector_size = 28*28
x_train = x_train.reshape(x_train.shape[0], image_vector_size)
x_test = x_test.reshape(x_test.shape[0], image_vector_size)

y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)

然后让我们转换一个tf.data.Dataset对象中的数组。如果你不明白这是什么,不要担心,我们将在这一章的后面看更多。目前,只要知道在训练网络时使用小批量是一种方便的方法就足够了。

dataset = tf.data.Dataset.from_tensor_slices(
    (tf.cast(x_train/255.0, tf.float32),
     tf.cast(y_train,tf.int64)))

dataset = dataset.shuffle(60000).batch(64)

现在,让我们使用具有两层的前馈神经网络来构建模型,每层具有 1024 个神经元:

mnist_model = tf.keras.Sequential([
  tf.keras.layers.Dense(1024, input_shape=(784,)),
  tf.keras.layers.Dense(1024),
  tf.keras.layers.Dense(10)
])

到目前为止,我们还没有做什么特别新的事情,所以你应该很容易就能跟上我们的步伐。下一步是定义优化器(我们将使用 Adam)和包含损失函数历史的列表:

optimizer = tf.train.AdamOptimizer()
loss_history = []

现在我们可以开始实际训练了。我们将有两个嵌套循环——第一个用于 epochs,第二个用于 batches。

for i in range(10): # Epochs
  print ("\nEpoch:", i)
  for (batch, (images, labels)) in enumerate(dataset.take(60000)):
    if batch % 100 == 0:
      print('.', end=")
    with tf.GradientTape() as tape:
      logits = mnist_model(images, training=True) # Prediction of the model
      loss_value = tf.losses.sparse_softmax_cross_entropy(tf.argmax(labels, axis = 1), logits)

      loss_history.append(loss_value.numpy())
      grads = tape.gradient(loss_value, mnist_model.variables) # Evaluation of gradients
      optimizer.apply_gradients(zip(grads, mnist_model.variables),
                                global_step=tf.train.get_or_create_global_step())

对您来说可能比较陌生的代码部分是包含这两行代码的部分:

grads = tape.gradient(loss_value, mnist_model.variables)
optimizer.apply_gradients(zip(grads, mnist_model.variables),
                                global_step=tf.train.get_or_create_global_step())

第一行计算loss_value TensorFlow 操作相对于mnist_model.variables的梯度(基本上是权重),第二行使用梯度让优化器更新权重。要了解 Keras 是如何自动计算渐变的,建议你去 https://goo.gl/s9Uqjc 查看官方文档。运行代码将最终训练网络。随着训练的进行,您应该看到每个时期的输出如下:

Epoch: 0
..........

现在要检查准确性,您可以简单地运行下面两行(这应该是不言自明的):

probs = tf.nn.softmax(mnist_model(x_train))
print(tf.reduce_mean(tf.cast(tf.equal(tf.argmax(probs, axis=1), tf.argmax(y_train, axis = 1)), tf.float32)))

这将为您提供一个张量,其中包含模型达到的精度:

tf.Tensor(0.8980333, shape=(), dtype=float32)

在这个例子中,我们达到了 89.8%的准确率,对于这样一个简单的网络来说,这是一个相对较好的结果。当然,举例来说,您可以尝试为更多的时期训练模型,或者尝试改变学习率。如果你想知道我们在哪里定义了学习率,我们没有。当我们将优化器定义为tf.train.AdamOptimizer时,如果没有另外指定,TensorFlow 将使用标准值 103。你可以通过查看 https://goo.gl/pU7yrB 的文档来检查这一点。

我们可以很容易地检验一个预测。让我们从数据集中获取一幅图像:

image = x_train[4:5,:]
label = y_train[4]

如果我们绘制图像,我们将看到数字 9(见图 2-4 )。

img/470317_1_En_2_Fig4_HTML.jpg

图 2-4

来自 MNIST 数据集的一幅图像。这恰好是一个 9。

我们可以很容易地检查模型预测的内容:

print(tf.argmax(tf.nn.softmax(mnist_model(image)), axis = 1))

如我们所料,这会返回以下内容:

tf.Tensor([9], shape=(1,), dtype=int64)

你应该注意我们是如何写代码的。我们没有明确地创建一个图,但是我们简单地使用了函数和操作,就像我们使用 numpy 一样。不需要用图形和会话来思考。这就是热切执行的工作方式。

TensorFlow 和 Numpy 兼容性

TensorFlow 使 numpy 阵列之间的切换变得非常简单:

  • TensorFlow 将 numpy 数组转换为张量

  • Numpy 将张量转换为 numpy 数组

将张量转换成 numpy 数组非常容易,只需调用.numpy()方法即可。这种操作既快又便宜,因为 numpy 数组和张量共享内存,所以不会发生内存移位。现在,如果您使用 GPU 硬件加速,这是不可能的,因为 numpy 数组不能存储在 GPU 内存中,而张量可以。转换包括将数据从 GPU 内存复制到 CPU 内存。只是一些需要记住的事情。

注意

通常,TensorFlow 张量和 numpy 数组共享相同的内存。将一个转换成另一个是非常便宜的操作。但是如果使用 GPU 加速,张量可能会保存在 GPU 内存中,而 numpy 数组则不能,因此需要复制数据。就运行时间而言,这可能更昂贵。

硬件加速

检查 GPU 的可用性

简单介绍一下如何使用 GPU 以及它可能带来的不同是值得的,只是为了让您对它有一个感觉。如果你没看过,那是相当令人印象深刻的。测试 GPU 加速最简单的方法就是使用 Google Colab。在 Google Colab 新建一个笔记本,激活 GPU 3 加速,照常导入TensorFlow:

import tensorflow as tf

然后我们需要测试我们是否有 GPU 可供使用。这可以通过下面的代码轻松实现:

print(tf.test.is_gpu_available())

这将根据 GPU 是否可用而返回TrueFalse。用稍微复杂一点的方式,可以这样做:

device_name = tf.test.gpu_device_name()
if device_name != '/device:GPU:0':
  raise SystemError('GPU device not found.')
print('Found GPU at: {}'.format(device_name))

如果运行该代码,可能会出现以下错误:

SystemErrorTraceback (most recent call last)
<ipython-input-1-d1680108c58e> in <module>()
      2 device_name = tf.test.gpu_device_name()
      3 if device_name != '/device:GPU:0':
----> 4   raise SystemError('GPU device not found')
      5 print('Found GPU at: {}'.format(device_name))
SystemError: GPU device not found

原因是你可能还没有配置笔记本(如果你在 Google Colab)使用 GPU。或者,如果您在笔记本电脑或台式机上工作,您可能没有安装正确的 TensorFlow 版本,或者您可能没有兼容的 GPU 可用。

要在 Google Colab 中启用 GPU 硬件加速,请选择编辑➤笔记本设置菜单选项。然后会出现一个窗口,您可以在其中设置硬件加速器。默认情况下,它设置为无。如果您将它设置为 GPU 并再次运行前面的代码,您应该会得到以下消息:

Found GPU at: /device:GPU:0

设备名称

注意设备名,在我们的例子中是/device:GPU:0,是如何编码大量信息的。这个名称以GPU:<NUMBER>结尾,其中<NUMBER>是一个整数,可以和您拥有的 GPU 数量一样大。您可以使用以下代码获得您拥有的所有设备的列表:

local_device_protos = device_lib.list_local_devices()
print(local_device_protos)

您将获得所有设备的列表。每个列表条目都将类似于这个条目(此示例指的是 GPU 设备):

name: "/device:XLA_GPU:0"
device_type: "XLA_GPU"
memory_limit: 17179869184
locality {
}
incarnation: 16797530695469281809
physical_device_desc: "device: XLA_GPU device"

有这样一个功能:

def get_available_gpus():
    local_device_protos = device_lib.list_local_devices()
    return [x.name for x in local_device_protos if x.device_type.endswith('GPU')]

你会得到一个更容易阅读的结果,比如这个 4 :

['/device:XLA_GPU:0', '/device:GPU:0']

显式设备放置

在特定设备上进行操作非常容易。这可以通过使用tf.device上下文来实现。例如,要在 CPU 上执行操作,可以使用以下代码:

with tf.device("/cpu:0"):
    # SOME OPERATION

或者在 GPU 上进行操作,可以使用代码:

with tf.device('/gpu:0'):
    # SOME OPERATION

注意

除非明确声明,否则 TensorFlow 会自动决定每个操作必须在哪个设备上运行。不要假设如果你不明确指定设备,你的代码将在 CPU 上运行。

GPU 加速演示:矩阵乘法

看看硬件加速会有什么影响是很有趣的。要了解更多关于使用 GPU 的信息,阅读官方文档是有益的,可以在 https://www.TensorFlow.org/guide/using_gpu 找到。

从以下代码开始 5 :

config = tf.ConfigProto()
config.gpu_options.allow_growth = True
sess = tf.Session(config=config)

第二行是必需的,因为 TensorFlow 开始分配一点 GPU 内存。随着会话的启动和进程的运行,会根据需要分配更多的 GPU 内存。然后创建一个会话。让我们尝试将两个填充有随机值的 10000x10000 维矩阵相乘,看看使用 GPU 是否有所不同。以下代码将在 GPU 上运行乘法运算:

%%time
with tf.device('/gpu:0'):
  tensor1 = tf.random_normal((10000, 10000))
  tensor2 = tf.random_normal((10000, 10000))
  prod = tf.linalg.matmul(tensor1, tensor2)
  prod_sum = tf.reduce_sum(prod)

  sess.run(prod_sum)

下面的代码在 CPU 上运行:

%%time
with tf.device('/cpu:0'):
  tensor1 = tf.random_normal((10000, 10000))
  tensor2 = tf.random_normal((10000, 10000))
  prod = tf.linalg.matmul(tensor1, tensor2)
  prod_sum = tf.reduce_sum(prod)

  sess.run(prod_sum)

当我运行代码时,我在 GPU 上获得了总时间1.86 sec,在 CPU 上获得了总时间1min 4sec:快了 32 倍。你可以想象,当一遍又一遍地做这样的计算时(深度学习中经常出现这种情况),你会在评估中获得相当大的性能提升。使用 TPU 稍微复杂一些,超出了本书的范围,所以我们将跳过它。

注意

使用 GPU 并不总是能提升性能。当涉及的张量很小时,你不会看到使用 GPU 和 CPU 之间的巨大差异。当张量的维度开始增长时,真正的差异将变得明显。

如果你尝试在更小的张量上运行相同的代码,例如100x100,你将看不到使用 GPU 和 CPU 之间的任何区别。张量足够小,以至于 CPU 得到结果的速度和 GPU 一样快。对于两个100x100矩阵,GPU 和 CPU 都给出一个大致20ms的结果。通常,实践者让 CPU 做所有的预处理(例如,规范化、数据加载等。)然后让 GPU 在训练时执行所有的大张量运算。

注意

通常,您应该在 GPU 上只评估昂贵的张量运算(如矩阵乘法或卷积),并进行所有预处理(如数据加载、清理等)。)在一个 CPU 上。

我们将在本书的后面(如果适用的话)看到如何做到这一点。但是不要害怕。您将能够在没有 GPU 的情况下使用代码并遵循示例。

GPU 加速对 MNIST 示例的影响

看看硬件加速对 MNIST 例子的影响是很有启发性的。为了完全在 CPU 上运行模型的训练,我们需要强制 TensorFlow 去做,因为否则它将试图在可用的 GPU 上进行昂贵的操作。为此,您可以使用以下代码:

with tf.device('/cpu:0'):
  for i in range(10): # Loop for the Epochs
    print ("\nEpoch:", i)
    for (batch, (images, labels)) in enumerate(dataset.take(60000)): # Loop for the mini-batches
      if batch % 100 == 0:
        print('.', end=")
      with tf.GradientTape() as tape:
        logits = mnist_model(images, training=True)
        loss_value = tf.losses.sparse_softmax_cross_entropy(tf.argmax(labels, axis = 1), logits)

        loss_history.append(loss_value.numpy())
        grads = tape.gradient(loss_value, mnist_model.variables)
        optimizer.apply_gradients(zip(grads, mnist_model.variables),
                                  global_step=tf.train.get_or_create_global_step())

这段代码在 Google Colab 上运行大约需要 8 分 41 秒。如果我们把所有可能的操作放在一个 GPU 上,用这段代码:

for i in range(10): # Loop for the Epochs
  print ("\nEpoch:", i)

  for (batch, (images, labels)) in enumerate(dataset.take(60000)): # Loop for the mini-batches
    if batch % 100 == 0:
      print('.', end=")
    labels = tf.cast(labels, dtype = tf.int64)

    with tf.GradientTape() as tape:

      with tf.device('/gpu:0'):
        logits = mnist_model(images, training=True)

      with tf.device('/cpu:0'):
        tgmax = tf.argmax(labels, axis = 1, output_type=tf.int64)

      with tf.device('/gpu:0'):
        loss_value = tf.losses.sparse_softmax_cross_entropy(tgmax, logits)

        loss_history.append(loss_value.numpy())
        grads = tape.gradient(loss_value, mnist_model.variables)
        optimizer.apply_gradients(zip(grads, mnist_model.variables),
                                    global_step=tf.train.get_or_create_global_step())

它将在 1 分 24 秒内运行。将tf.argmax()放在 CPU 上的原因是,在编写本文时,tf.argmax的 GPU 实现有一个错误,不能按预期工作。

您可以清楚地看到 GPU 加速的显著效果,即使是在我们使用的简单网络上。

仅训练特定层

你现在应该知道 Keras 的工作与层。当你定义一个时,让我们说一个Dense层,如下所示:

layer1 = Dense(32)

您可以将一个trainable参数(布尔型)传递给层构造函数。这将停止优化器更新其权重

layer1 = dense(32, trainable = False)

但这不会很有用。需要的是在实例化之后改变这个属性的可能性。这很容易做到。例如,您可以使用下面的代码

layer = Dense(32)
# something useful happens here
layer.trainable = False

注意

为了使可训练属性的改变生效,您需要在您的模型上调用compile()方法。否则,在使用fit()方法时,更改不会有任何效果。

仅训练特定层:示例

为了更好地理解这一切是如何工作的,让我们看一个例子。让我们再次考虑具有两层的前馈网络:

model = Sequential()
model.add(Dense(32, activation="relu", input_dim=784, name = 'input'))
model.add(Dense(32, activation="relu", name = 'hidden1'))

请注意我们是如何创建一个具有两个Dense层和一个name属性的模型的。一个叫input,另一个叫hidden1。现在你可以用model.summary()检查网络结构。在这个简单的示例中,您将获得以下输出:

_______________________________________________________________
Layer (type)                 Output Shape              Param #
===============================================================
input (Dense)                (None, 32)                25120
_______________________________________________________________
hidden1 (Dense)              (None, 32)                1056
===============================================================
Total params: 26,176
Trainable params: 26,176
Non-trainable params: 0
_______________________________________________________________

请注意所有参数是如何可训练的,以及如何在第一列中找到层名称。请注意,因为给每一层分配一个名称在将来会很有用。要冻结名为hidden1的层,只需找到具有该名称的层,并更改其可训练属性,如下所示:

model.get_layer('hidden1').trainable = False

现在,如果您再次检查模型摘要,您将看到不同数量的可训练参数:

_______________________________________________________________
Layer (type)                 Output Shape              Param #
===============================================================
input (Dense)                (None, 32)                25120
_______________________________________________________________
hidden1 (Dense)              (None, 32)                1056
===============================================================
Total params: 26,176
Trainable params: 25,120
Non-trainable params: 1,056
_______________________________________________________________

如你所见,hidden1层包含的 1056 个参数不再可训练。该图层现已冻结。如果您还没有为层指定名称,并且您想要找出层的名称,您可以使用model.summary()函数,或者您可以简单地通过模型中的层进行循环:

for layer in model.layers:
  print (layer.name)

这段代码将给出以下输出:

input
hidden1

注意model.layers只是一个以层为元素的列表。因此,您可以使用传统的方式从列表中访问元素。例如,要访问最后一层,可以使用:

model.layers[-1]

或者要访问第一层,请使用:

model.layers[0]

例如,要冻结最后一层,只需使用:

model.layers[-1].trainable = False

注意

当你在 Keras 中改变一个层的属性时,比如trainable属性,记得用compile()函数重新编译模型。否则,更改将不会在培训期间生效。

总结一下,考虑下面的代码 6 :

x = Input(shape=(4,))
layer = Dense(8)
layer.trainable = False
y = layer(x)
frozen_model = Model(x, y)

现在,如果我们运行下面的代码:

frozen_model.compile(optimizer='Adam', loss="mse")
frozen_model.fit(data, labels)

它不会修改layer的权重。事实上,调用frozen_model.summary()给了我们这个:

_______________________________________________________________
Layer (type)                 Output Shape              Param #
===============================================================
input_1 (InputLayer)         (None, 4)                 0
_______________________________________________________________
dense_6 (Dense)              (None, 8)                 40
===============================================================
Total params: 40
Trainable params: 0
Non-trainable params: 40
_______________________________________________________________

正如所料,没有可训练的参数。我们可以简单地修改layer.trainable属性:

layer.trainable = True
trainable_model = Model(x, y)

现在我们编译并拟合模型:

trainable_model.compile(optimizer='Adam', loss="mse")
trainable_model.fit(data, labels)

这次将更新layer的权重。我们可以用trainable_model.summary()检查一下:

_______________________________________________________________
Layer (type)                 Output Shape              Param #
===============================================================
input_1 (InputLayer)         (None, 4)                 0
_______________________________________________________________
dense_6 (Dense)              (None, 8)                 40
===============================================================
Total params: 40
Trainable params: 40
Non-trainable params: 0
_______________________________________________________________

现在所有的参数都是可训练的,正如我们所希望的。

移除图层

删除模型中的一个或多个最后的层并添加不同的层来微调它是非常有用的。当你训练一个网络,并想通过只训练最后几层来微调它的行为时,这种想法经常被用在迁移学习中。让我们考虑以下模型:

model = Sequential()
model.add(Dense(32, activation="relu", input_dim=784, name = 'input'))
model.add(Dense(32, activation="relu", name = 'hidden1'))
model.add(Dense(32, activation="relu", name = 'hidden2'))

summary()调用将给出以下输出:

_______________________________________________________________
Layer (type)                 Output Shape              Param #
===============================================================
input (Dense)                (None, 32)                25120
_______________________________________________________________
hidden1 (Dense)              (None, 32)                1056
_______________________________________________________________
hidden2 (Dense)              (None, 32)                1056
===============================================================
Total params: 27,232
Trainable params: 27,232
Non-trainable params: 0
_______________________________________________________________

假设您想要建立第二个模型,将您训练的权重保持在inputhidden1层,但是您想要用不同的层(假设有 16 个神经元)替换hidden2层。您可以通过以下方式轻松做到这一点:

model2 = Sequential()
for layer in model.layers[:-1]:
  model2.add(layer)

这给了你:

Layer (type)                 Output Shape              Param #
===============================================================
input (Dense)                (None, 32)                25120
_______________________________________________________________
hidden1 (Dense)              (None, 32)                1056
===============================================================
Total params: 26,176
Trainable params: 26,176
Non-trainable params: 0
_______________________________________________________________

此时,您可以简单地添加一个新层,如下所示:

model2.add(Dense(16, activation="relu", name = 'hidden3'))

它具有以下结构:

_______________________________________________________________
Layer (type)                 Output Shape              Param #
===============================================================
input (Dense)                (None, 32)                25120
_______________________________________________________________
hidden1 (Dense)              (None, 32)                1056
_______________________________________________________________
hidden3 (Dense)              (None, 16)                528
===============================================================
Total params: 26,704
Trainable params: 26,704
Non-trainable params: 0
_______________________________________________________________

之后,记得编译你的模型。例如,对于一个回归问题,您的代码可能如下所示:

model.compile(loss='mse', optimizer="Adam", metrics=['mse'])

Keras 回调函数

更好地理解什么是 Keras 回调函数是有益的,因为它们在开发模型时经常被使用。这是来自官方文献 7 :

回调是在训练程序的给定阶段应用的一组功能。

这个想法是你可以传递一个回调函数列表给SequentialModel类的.fit()方法。在训练的每个阶段都会调用回调的相关方法[ https://keras.io/callbacks/ , Accessed 01/02/2019 ]。Keunwoo Choi 写了一篇关于如何编写回调类的很好的概述,你可以在 https://goo.gl/hL37wq 找到。我们在这里总结一下,用一些实际例子展开。

自定义回调类

名为Callback的抽象基类可以在本文撰写时的

tensorflow/python/keras/callbacks.py ( https://goo.gl/uMrMbH )。

首先,您需要定义一个自定义类。您希望重定义的主要方法通常如下

  • on_train_begin:训练开始时打电话

  • on_train_end:训练结束时调用

  • on_epoch_begin:在一个纪元开始时被调用

  • on_epoch_end:在一个时代结束时被调用

  • on_batch_begin:在处理一批之前调用

  • on_batch_end:在一批结束时调用

这可以通过下面的代码来完成:

import keras
class My_Callback(keras.callbacks.Callback):
    def on_train_begin(self, logs={}):
        return

    def on_train_end(self, logs={}):
        return

    def on_epoch_begin(self, epoch, logs={}):
        return

    def on_epoch_end(self, epoch, logs={}):
        return

    def on_batch_begin(self, batch, logs={}):
        return

    def on_batch_end(self, batch, logs={}):
        self.losses.append(logs.get('loss'))
        return

每种方法都有稍微不同的输入,您可以在您的类中使用。我们简单看一下(你可以在 https://goo.gl/uMrMbH 的 Python 原代码中找到)。

on_epoch_begin, on_epoch_end

Arguments:

        epoch: integer, index of epoch.

        logs: dictionary of logs.

on_train_begin, on_train_end

Arguments:

        logs: dictionary of logs.

on_batch_begin, on_batch_end

Arguments:

        batch: integer, index of batch within the current epoch.

        logs: dictionary of logs

.

让我们看一个如何使用这个类的例子。

自定义回调类的示例

让我们再次考虑 MNIST 的例子。这和你现在看到的代码是一样的:

import tensorflow as tf
from tensorflow import keras
(train_images, train_labels), (test_images, test_labels) = tf.keras.datasets.mnist.load_data()

train_labels = train_labels[:5000]
test_labels = test_labels[:5000]

train_images = train_images[:5000].reshape(-1, 28 * 28) / 255.0
test_images = test_images[:5000].reshape(-1, 28 * 28) / 255.0

让我们为我们的例子定义一个Sequential模型:

model = tf.keras.models.Sequential([
    keras.layers.Dense(512, activation=tf.keras.activations.relu, input_shape=(784,)),
    keras.layers.Dropout(0.2),
    \keras.layers.Dense(10, activation=tf.keras.activations.softmax)
  ])

model.compile(optimizer='adam',
                loss=tf.keras.losses.sparse_categorical_crossentropy,
                metrics=['accuracy'])

现在让我们编写一个自定义回调类,只重新定义其中一个方法来查看输入。例如,让我们看看logs变量在训练开始时包含什么:

class CustomCallback1(keras.callbacks.Callback):
    def on_train_begin(self, logs={}):
        print (logs)
        return

然后,您可以将它用于:

CC1 = CustomCallback1()
model.fit(train_images, train_labels,  epochs = 2,
          validation_data = (test_images,test_labels),
          callbacks = [CC1])  # pass callback to training

记住总是实例化类并传递CC1变量,而不是类本身。您将获得以下内容:

Train on 5000 samples, validate on 5000 samples
{}
Epoch 1/2
5000/5000 [==============================] - 1s 274us/step - loss: 0.0976 - acc: 0.9746 - val_loss: 0.2690 - val_acc: 0.9172
Epoch 2/2
5000/5000 [==============================] - 1s 275us/step - loss: 0.0650 - acc: 0.9852 - val_loss: 0.2925 - val_acc: 0.9114
{}
<tensorflow.python.keras.callbacks.History at 0x7f795d750208>

{}可以看出,logs字典是空的。让我们扩展一下我们的类:

class CustomCallback2(keras.callbacks.Callback):
    def on_train_begin(self, logs={}):
        print (logs)
        return

    def on_epoch_end(self, epoch, logs={}):
        print ("Just finished epoch", epoch)
        print (logs)
        return

现在我们用这个来训练网络:

CC2 = CustomCallback2()
model.fit(train_images, train_labels,  epochs = 2,
          validation_data = (test_images,test_labels),
          callbacks = [CC2])  # pass callback to training

这将给出以下输出(为简洁起见,此处仅报告一个时期):

Train on 5000 samples, validate on 5000 samples
{}
Epoch 1/2
4864/5000 [============================>.] - ETA: 0s - loss: 0.0511 - acc: 0.9879
Just finished epoch 0
{'val_loss': 0.2545496598124504, 'val_acc': 0.9244, 'loss': 0.05098680723309517, 'acc': 0.9878}

现在事情开始变得有趣了。字典现在包含了更多我们可以访问和使用的信息。在字典里,我们有val_lossval_accacc。因此,让我们对输出进行一些定制。让我们在fit()调用中设置verbose = 0来抑制标准输出,然后生成我们自己的输出。

我们的新班级将是:

class CustomCallback3(keras.callbacks.Callback):
    def on_train_begin(self, logs={}):
        print (logs)
        return

    def on_epoch_end(self, epoch, logs={}):
        print ("Just finished epoch", epoch)
        print ('Loss evaluated on the validation dataset =',logs.get('val_loss'))
        print ('Accuracy reached is', logs.get('acc'))
        return

我们可以通过以下方式训练我们的网络:

CC3 = CustomCallback3()
model.fit(train_images, train_labels,  epochs = 2,
          validation_data = (test_images,test_labels),
          callbacks = [CC3], verbose = 0)  # pass callback to training

我们会得到这个:

{}
Just finished epoch 0
Loss evaluated on the validation dataset = 0.2546206972360611

空的{}简单地表示on_train_begin收到的空的logs字典。当然,您可以每隔几个纪元打印一次信息。例如,通过修改on_epoch_end()功能如下:

def on_epoch_end(self, epoch, logs={}):
        if (epoch % 10 == 0):
          print ("Just finished epoch", epoch)
          print ('Loss evaluated on the validation dataset =',logs.get('val_loss'))
          print ('Accuracy reached is', logs.get('acc'))
        return

如果训练网络 30 个历元,您将获得以下输出:

{}
Just finished epoch 0
Loss evaluated on the validation dataset = 0.3692033936366439
Accuracy reached is 0.9932
Just finished epoch 10
Loss evaluated on the validation dataset = 0.3073081444747746
Accuracy reached is 1.0
Just finished epoch 20
Loss evaluated on the validation dataset = 0.31566708440929653
Accuracy reached is 0.9992
<tensorflow.python.keras.callbacks.History at 0x7f796083c4e0>

现在你应该开始了解如何在训练中完成几件事情。我们将在下一节中看到的回调的典型用法是每隔几个时期保存一次模型。但是,例如,您可以将准确度值保存在列表中,以便以后能够绘制它们,或者简单地绘制指标,以查看您的训练进展情况。

保存和加载模型

将模型保存在磁盘上通常很有用,以便能够在以后的阶段继续训练,或者重用以前训练过的模型。为了说明如何做到这一点,为了给出一个具体的例子,让我们再次考虑 MNIST 数据集。 8 全部代码可在本书 GitHub 资源库的专用笔记本chapter 2 文件夹中获得。

你将需要以下的import:

import os
import tensorflow as tf
from tensorflow import keras

同样,让我们加载 MNIST 数据集并获取前 5000 个观测值。

(train_images, train_labels), (test_images, test_labels) = tf.keras.datasets.mnist.load_data()
train_labels = train_labels[:5000]
test_labels = test_labels[:5000]
train_images = train_images[:5000].reshape(-1, 28 * 28) / 255.0
test_images = test_images[:5000].reshape(-1, 28 * 28) / 255.0

然后,让我们建立一个简单的 Keras 模型,使用一个有 512 个神经元的Dense层,一点点丢弃,以及用于分类的经典的 10 个神经元输出层(记住 MNIST 数据集有 10 个类)。

model = tf.keras.models.Sequential([
    keras.layers.Dense(512, activation=tf.keras.activations.relu, input_shape=(784,)),
    keras.layers.Dropout(0.2),
    keras.layers.Dense(10, activation=tf.keras.activations.softmax)
  ])

model.compile(optimizer='adam',
                loss=tf.keras.losses.sparse_categorical_crossentropy,
                metrics=['accuracy'])

我们增加了一点遗漏,因为这个模型有 407050 个可训练参数。您可以简单地使用model.summary()来检查这个数字。

我们需要做的是定义我们想要在磁盘上保存模型的位置。例如,我们可以这样做:

checkpoint_path = "training/cp.ckpt"
checkpoint_dir = os.path.dirname(checkpoint_path)

之后,我们需要定义一个回调函数(还记得我们在上一节中所做的)来保存权重:

cp_callback = tf.keras.callbacks.ModelCheckpoint(checkpoint_path,
                                                 save_weights_only=True,
                                                 verbose=1)

注意,现在我们不需要像上一节那样定义一个类,因为ModelCheckpoint继承自Callback类。

然后,我们可以简单地训练模型,指定正确的回调函数:

model.fit(train_images, train_labels,  epochs = 10,
          validation_data = (test_images,test_labels),
          callbacks = [cp_callback])

如果您运行一个!ls命令,您应该看到至少三个文件:

  • cp.ckpt.data-00000-of-00001:包含权重(如果权重的数量很大,你会得到很多这样的文件)

  • cp.ckpt.index:该文件表示哪些重量在哪些文件中

  • checkpoint:这个文本文件包含关于检查点本身的信息

我们现在可以测试我们的方法。前面的代码将为您提供一个在验证数据集上达到大约 92%准确度的模型。现在,如果我们这样定义第二个模型:

model2 = tf.keras.models.Sequential([
    keras.layers.Dense(512, activation=tf.keras.activations.relu, input_shape=(784,)),
    keras.layers.Dropout(0.2),
    keras.layers.Dense(10, activation=tf.keras.activations.softmax)
  ])

model2.compile(optimizer='adam',
                loss=tf.keras.losses.sparse_categorical_crossentropy,
                metrics=['accuracy'])

我们用这个在验证数据集上检查它的准确性:

loss, acc = model2.evaluate(test_images, test_labels)
print("Untrained model, accuracy: {:5.2f}%".format(100*acc))

我们将得到大约 8.6%的精确度。这是意料之中的,因为这个模型还没有被训练过。但是现在我们可以在这个模型中加载保存的权重,然后再试一次。

model2.load_weights(checkpoint_path)
loss,acc = model2.evaluate(test_images, test_labels)
print("Second model, accuracy: {:5.2f}%".format(100*acc))

我们应该得到这样的结果:

5000/5000 [==============================] - 0s 50us/step
Restored model, accuracy: 92.06%

这又是有意义的,因为新模型现在使用旧训练模型的权重。请记住,要在新模型中加载预训练的权重,新模型需要与原始模型具有完全相同的架构。

注意

要将保存的权重用于新模型,新模型必须与用于保存权重的模型具有相同的架构。使用预先训练好的权重可以节省你很多时间,因为你不需要浪费时间再次训练网络。

正如我们将一次又一次看到的,基本思想是使用回调并定义一个自定义回调来节省我们的权重。当然,我们可以自定义我们的回调函数。例如,如果希望每 100 个时期保存一次权重,每次使用不同的文件名,以便我们可以在需要时恢复特定的检查点,我们必须首先以动态方式定义文件名:

checkpoint_path = "training/cp-{epoch:04d}.ckpt"
checkpoint_dir = os.path.dirname(checkpoint_path)

我们还应该使用下面的回调:

cp_callback = tf.keras.callbacks.ModelCheckpoint(
    checkpoint_path, verbose=1, save_weights_only=True,
    period=1)

注意,checkpoint_path可以包含命名的格式化选项(在我们有{epoch:04d}的名称中),这些选项将由epoch的值和logs中的键填充(在on_epoch_end中传递,我们在上一节中看到过)。 9 你可以查看tf.keras.callbacks.ModelCheckpoint的原始代码,你会发现格式化是在on_epoch_end(self, epoch, logs)方法中完成的:

filepath = self.filepath.format(epoch=epoch + 1, **logs)

您可以用纪元编号和包含在logs字典中的值来定义您的文件名。

让我们回到我们的例子。让我们从保存模型的第一个版本开始:

model.save_weights(checkpoint_path.format(epoch=0))

然后我们可以像往常一样拟合模型:

model.fit(train_images, train_labels,
          epochs = 10, callbacks = [cp_callback],
          validation_data = (test_images,test_labels),
          verbose=0)

小心,因为这将保存大量文件。在我们的例子中,每个时期一个文件。例如,您的目录内容(可通过!ls training获得)可能如下所示:

checkpoint                  cp-0006.ckpt.data-00000-of-00001
cp-0000.ckpt.data-00000-of-00001  cp-0006.ckpt.index
cp-0000.ckpt.index          cp-0007.ckpt.data-00000-of-00001
cp-0001.ckpt.data-00000-of-00001  cp-0007.ckpt.index
cp-0001.ckpt.index          cp-0008.ckpt.data-00000-of-00001
cp-0002.ckpt.data-00000-of-00001  cp-0008.ckpt.index
cp-0002.ckpt.index          cp-0009.ckpt.data-00000-of-00001
cp-0003.ckpt.data-00000-of-00001  cp-0009.ckpt.index
cp-0003.ckpt.index          cp-0010.ckpt.data-00000-of-00001
cp-0004.ckpt.data-00000-of-00001  cp-0010.ckpt.index
cp-0004.ckpt.index          cp.ckpt.data-00000-of-00001
cp-0005.ckpt.data-00000-of-00001  cp.ckpt.index
cp-0005.ckpt.index

在继续之前的最后一个技巧是如何获得最新的检查点,而不需要搜索它的文件名。这可以通过下面的代码轻松完成:

latest = tf.train.latest_checkpoint('training')
model.load_weights(latest)

这将自动加载保存在最新检查点的重量。latest变量只是一个字符串,包含最后一个检查点文件名。在我们的例子中,那就是training/cp-0010.ckpt

注意

检查点文件是包含模型权重的二进制文件。所以你不能直接阅读它们,你也不需要这样做。

手动保存您的体重

当然,您可以在完成训练后简单地手动保存您的模型权重,而无需定义回调函数:

model.save_weights('./checkpoints/my_checkpoint')

这个命令将生成三个文件,都以您给定的字符串作为名称开始。这种情况下是my_checkpoint。运行前面的代码将生成我们上面描述的三个文件:

checkpoint
my_checkpoint.data-00000-of-00001
my_checkpoint.index

在新模型中重新加载砝码就像这样简单:

model.load_weights('./checkpoints/my_checkpoint')

请记住,为了能够在新模型中重新加载保存的权重,旧模型必须与新模型具有相同的架构。肯定是一模一样的。

保存整个模型

Keras 还允许您在磁盘上保存整个模型:权重、架构和优化器。这样,您可以通过移动一些文件来重新创建相同的模型。例如,我们可以使用下面的代码

model.save('my_model.h5')

这将把整个模型保存在一个名为my_model.h5的文件中。您可以简单地将文件移动到不同的计算机上,并使用以下代码重新创建相同的训练模型:

new_model = keras.models.load_model('my_model.h5')

请注意,该模型将具有与您的原始模型相同的训练权重,因此可以使用了。例如,如果您想要停止训练模型并在不同的机器上继续训练,这可能会有所帮助。或者你必须停止训练一段时间,以后再继续。

数据集抽象

tf.data.Dataset 10 是 TensorFlow 中的一个新抽象,对于构建数据管道非常有用。当您处理不适合内存的数据集时,它也非常有用。我们将在本书后面看到如何更详细地使用它。在接下来的部分中,我会给出一些在项目中使用它的提示。要学习如何使用它,一个很好的起点是在 https://www.tensorflow.org/guide/datasets 学习官方文档。请记住:当您想了解更多关于 TensorFlow 的特定方法或功能时,请始终从这里开始。

基本上,a Dataset它只是一个元素序列,其中每个元素包含一个或多个张量。通常,每个元素将是一个训练示例或一批训练示例。基本思想是,首先用一些数据创建一个Dataset,然后在其上链接方法调用。例如,您应用Dataset.map()来对每个元素应用一个函数。请注意,数据集由元素组成,每个元素都具有相同的结构。

像往常一样,让我们考虑一个例子来理解这是如何工作的以及如何使用它。假设我们有一个 10 行 10 列的矩阵作为输入,定义如下:

inp = tf.random_uniform([10, 10])

我们可以简单地用以下内容创建一个数据集:

dataset = tf.data.Dataset.from_tensor_slices(inp)

使用print(dataset),将得到以下输出:

<TensorSliceDataset shapes: (10,), types: tf.float32>

这告诉您数据集中的每个元素都是一个有 10 个元素的张量(inp张量中的行)。一种很好的可能性是对数据集中的每个元素应用特定的函数。例如,我们可以将所有元素乘以 2:

dataset2 = dataset.map(lambda x: x*2)

为了检查发生了什么,我们可以打印每个数据集中的第一个元素。这可以通过以下方法轻松实现(稍后将详细介绍):

dataset.make_one_shot_iterator().get_next()

dataset2.make_one_shot_iterator().get_next()

从第一行开始,您将得到(您的数字会有所不同,因为我们在这里处理的是随机数):

<tf.Tensor: id=62, shape=(10,), dtype=float32, numpy=
array([0.2215631 , 0.32099664, 0.04410303, 0.8502971 , 0.2472974 , 0.25522232, 0.94817066, 0.7719344 , 0.60333145, 0.75336015], dtype=float32)>

从第二行,你得到:

<tf.Tensor: id=71, shape=(10,), dtype=float32, numpy=
array([0.4431262 , 0.6419933 , 0.08820605, 1.7005942 , 0.4945948 , 0.51044464, 1.8963413 , 1.5438688 , 1.2066629 , 1.5067203 ], dtype=float32)>

正如所料,第二个输出包含第一个输出的所有数字乘以 2。

注意

tf.data.dataset旨在建立数据处理管道。例如,在图像识别中,你可以用这种方式进行数据扩充、准备、标准化等等。

我强烈建议您查看官方文档,以获得更多关于将函数应用于每个元素的不同方法的信息。例如,您可能需要对数据进行变换,然后展平结果(例如,参见flat_map(),)。

迭代数据集

一旦有了数据集,您可能希望逐个或成批地处理元素。为此,你需要一个迭代器。例如,要逐个处理您之前定义的元素,您可以实例化一个所谓的make_one_shot_iterator(),如下所示:

iterator = dataset.make_one_shot_iterator()

然后,您可以使用get_next()方法迭代元素:

for i in range(10):
  value = print(iterator.get_next())

这将为您提供数据集中的所有元素。它们看起来会像这样(请注意,您的编号会有所不同):

tf.Tensor(
[0.2215631  0.32099664 0.04410303 0.8502971  0.2472974  0.25522232
 0.94817066 0.7719344  0.60333145 0.75336015], shape=(10,), dtype=float32)

注意,一旦到达数据集的末尾,使用方法get_next()将引发一个tf.errors.OutOfRangeError

简单配料

最基本的批处理方法是将数据集的n个连续元素堆叠在一个组中。当我们用小批量训练我们的网络时,这将非常有用。这可以使用batch()方法来完成。让我们回到我们的例子。记住我们的数据集有 10 个元素。假设我们想要创建批处理,每个批处理有两个元素。这可以通过以下代码实现:

batched_dataset = dataset.batch(2)

现在让我们用下面的公式再次定义一个iterator:

iterator = batched_dataset.make_one_shot_iterator()

现在让我们看看get_next()将返回什么:

print(iterator.get_next())

输出将是:

tf.Tensor(
[[0.2215631  0.32099664 0.04410303 0.8502971  0.2472974  0.25522232
  0.94817066 0.7719344  0.60333145 0.75336015]
 [0.28381765 0.3738917  0.8146689  0.20919728 0.5753969  0.9356725
  0.7362906  0.76200795 0.01308048 0.14003313]], shape=(2, 10), dtype=float32)

这是我们数据集的两个元素。

注意

当我们用小批量训练一个神经网络时,用batch()方法分批真的很有用。我们不必费心自己创建批处理,因为tf.data.dataset会为我们做这件事。

使用 MNIST 数据集进行简单批处理

要尝试下面的代码,您可能需要重新启动正在使用的内核,以避免与前面示例中的急切执行发生冲突。完成后,加载数据(如前所述):

num_classes = 10

mnist = tf.keras.datasets.mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()

image_vector_size = 28*28
x_train = x_train.reshape(x_train.shape[0], image_vector_size)
x_test = x_test.reshape(x_test.shape[0], image_vector_size)

y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)

然后创建培训Dataset:

mnist_ds_train = tf.data.Dataset.from_tensor_slices((x_train, y_train))

现在,使用一个简单的两层前馈网络构建 Keras 模型:

img = tf.placeholder(tf.float32, shape=(None, 784))
x = Dense(128, activation="relu")(img)  # fully-connected layer with 128 units and ReLU activation
x = Dense(128, activation="relu")(x)
preds = Dense(10, activation="softmax")(x)
labels = tf.placeholder(tf.float32, shape=(None, 10))
loss = tf.reduce_mean(categorical_crossentropy(labels, preds))

correct_prediction = tf.equal(tf.argmax(preds,1), tf.argmax(labels,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

train_step = tf.train.AdamOptimizer(0.001).minimize(loss)
init_op = tf.global_variables_initializer()

现在我们需要定义批量大小:

train_batched = mnist_ds_train.batch(1000)

现在让我们定义迭代器:

train_iterator = train_batched.make_initializable_iterator() # So we can restart from the beginning
next_batch = train_iterator.get_next()
it_init_op = train_iterator.initializer

it_init_op操作将用于重置迭代器,并将从每个时期的开始处开始。注意next_batch操作具有以下结构:

(<tf.Tensor 'IteratorGetNext_6:0' shape=(?, 784) dtype=uint8>, <tf.Tensor 'IteratorGetNext_6:1' shape=(?, 10) dtype=float32>)

因为它包含图像和标签。在我们的培训中,我们需要获得以下形式的批次:

train_batch_x, train_batch_y = sess.run(next_batch)

最后,让我们来训练我们的网络:

with tf.Session() as sess:
    sess.run(init_op)

    for epoch in range(50):
      sess.run(it_init_op)
      try:
        while True:
          train_batch_x, train_batch_y = sess.run(next_batch)
          sess.run(train_step,feed_dict={img: train_batch_x, labels: train_batch_y})
      except tf.errors.OutOfRangeError:
        pass

      if (epoch % 10 == 0 ):
        print('epoch',epoch)
        print(sess.run(accuracy,feed_dict={img: x_train,
                                    labels: y_train}))

现在,我在这里使用了一些有用的技巧。特别是,由于您不知道您有多少个批处理,您可以使用下面的构造来避免得到错误消息:

      try:
        while True:
          # Do something
      except tf.errors.OutOfRangeError:
        pass

这样,当你运行完一个批处理时,当你得到一个OutOfRangeError时,异常将简单地继续,而不会中断你的代码。请注意,对于每个时期,我们如何调用此代码来重置迭代器:

sess.run(it_init_op)

否则,我们会立即得到一个OutOfRangeError。运行这段代码会让你很快达到大约 99%的准确率。您应该会看到这样的输出(为了简洁起见,我只显示了第 40 个纪元的输出):

epoch 40
0.98903334

这个数据集的快速概述并不详尽,但应该能让您了解它的威力。如果您想了解更多,最好的地方是,像往常一样,官方文档。

注意

是一种非常方便的构建数据管道的方式,从加载开始,到操作、规范化、扩充等等。特别是在图像识别问题中,这非常有用。请记住,使用它意味着向您的计算图添加节点。因此,在会话评估图形之前,不会处理任何数据。

在快速执行模式下使用 tf.data.Dataset

这一章以最后一个提示结束。如果您在急切执行模式下工作,那么您的数据集生活会更加轻松。例如,要迭代一个批处理数据集,您可以像使用经典 Python ( for x in ...)一样简单地完成。为了理解我的意思,让我们看一个简单的例子。首先,您需要启用急切执行:

import tensorflow as tf
from tensorflow import keras
import tensorflow.contrib.eager as tfe

tf.enable_eager_execution()

那么你可以简单地这样做:

dataset = tf.data.Dataset.from_tensor_slices(tf.random_uniform([4, 2]))
dataset = dataset.batch(2)
for batch in dataset:
  print(batch)

当您需要逐批迭代数据集时,这非常有用。输出将如下所示(由于tf.random.uniform()调用,您的数字会有所不同):

tf.Tensor(
[[0.07181489 0.46992648]
 [0.00652897 0.9028846 ]], shape=(2, 2), dtype=float32)
tf.Tensor(
[[0.9167508  0.8379569 ]
 [0.33501422 0.3299384 ]], shape=(2, 2), dtype=float32)

结论

这一章的目的是向你展示一些我们将在本书中用到的技术,这些技术将对你的项目非常有帮助。我们的目标不是详细解释这些方法,因为这需要一本单独的书。但是这一章应该在你尝试做具体的事情时为你指出正确的方向,比如定期保存你的模型的权重。在接下来的章节中,我们将会用到这些技术。如果你想了解更多,记得经常查阅官方文档。

Footnotes 1

https://www.tensorflow.org/guide/eager(2019 年 1 月 17 日访问)

2

你可以在图书仓库里找到带有代码的笔记本。要找到它,请访问 Apress book 网站并点击下载代码按钮。该链接指向 GitHub 存储库。笔记本在Chapter2文件夹里。

3

你可以在 https://goo.gl/hXKNnf 找到这篇文章来学习怎么做。

4

结果是在 Google Colab 笔记本中调用该函数时获得的。

5

该代码的灵感来自 Google Colab 文档中的 Google 代码。

6

https://keras.io/getting-started/faq/#how-can-i-freeze-keras-layers 查看官方文档中的示例。

7

https://keras.io/callbacks/

8

这个例子的灵感来自于 https://www.tensorflow.org/tutorials/keras/save_and_restore_models 的官方 Keras 文档。

9

https://goo.gl/SnKgyQ 查看官方文档。

10

https://www.tensorflow.org/guide/datasets

三、卷积神经网络基础

在这一章,我们将看看卷积神经网络(CNN)的主要组成部分:内核和池层。然后我们将看看典型的网络是什么样子的。然后,我们将尝试用一个简单的卷积网络解决一个分类问题,并尝试将卷积运算可视化。这样做的目的是试图理解,至少是直观地理解,学习是如何进行的。

内核和过滤器

CNN 的主要组件之一是滤波器,它是具有维度nnK的方阵,其中 n K 是整数,并且通常是小数字,如 3 或 5。有时过滤器也被称为内核。使用内核来自经典的图像处理技术。如果你用过 Photoshop 或者类似的软件,你就习惯了做锐化、模糊、浮雕之类的操作。 1 所有这些操作都是用内核来完成的。在这一节我们将会看到内核到底是什么以及它们是如何工作的。请注意,在本书中,我们将互换使用这两个术语(内核和过滤器)。让我们定义四种不同的滤波器,并在本章后面检查它们在卷积运算中的效果。对于这些示例,我们将使用 3 × 3 滤波器。目前,只是把下面的定义作为参考,我们将在本章的后面看到如何使用它们。

  • The following kernel will allow the detection of horizontal edges

    $$ {\mathfrak{I}}_H=\left(\begin{array}{ccc}1& 1& 1\ {}0& 0& 0\ {}-1& -1& -1\end{array}\right) $$

  • The following kernel will allow the detection of vertical edges

    $$ {\mathfrak{I}}_V=\left(\begin{array}{ccc}1& 0& -1\ {}1& 0& -1\ {}1& 0& -1\end{array}\right) $$

  • The following kernel will allow the detection of edges when luminosity changes drastically

    $$ {\mathfrak{I}}_L=\left(\begin{array}{ccc}-1& -1& -1\ {}-1& 8& -1\ {}-1& -1& -1\end{array}\right) $$

  • The following kernel will blur edges in an image

    $$ {\mathfrak{I}}_B=-\frac{1}{9}\left(\begin{array}{ccc}1& 1& 1\ {}1& 1& 1\ {}1& 1& 1\end{array}\right) $$

在接下来的章节中,我们将使用滤镜对测试图像进行卷积,看看它们的效果如何。

盘旋

理解 CNN 的第一步是理解卷积。最简单的方法是通过几个简单的案例来看它的实际应用。首先,在神经网络的环境中,卷积是在张量之间进行的。该操作得到两个张量作为输入,并产生一个张量作为输出。操作通常用操作符*表示。

让我们看看它是如何工作的。考虑两个张量,维数都是 3 × 3。卷积运算通过应用以下公式来完成:

$$ \left(\begin{array}{ccc}{a}_1& {a}_2& {a}_3\ {}{a}_4& {a}_5& {a}_6\ {}{a}_7& {a}_8& {a}_9\end{array}\right)\ast \left(\begin{array}{ccc}{k}_1& {k}_2& {k}_3\ {}{k}_4& {k}_5& {k}_6\ {}{k}_7& {k}_8& {k}_9\end{array}\right)=\sum \limits_{i=1}⁹{a}_i{k}_i $$

在这种情况下,结果仅仅是每个元素的总和, a i ,乘以各自的元素, k i 。在更典型的矩阵形式中,这个公式可以用一个双和写成

$$ \left(\begin{array}{ccc}{a}_{11}& {a}_{12}& {a}_{13}\ {}{a}_{21}& {a}_{22}& {a}_{23}\ {}{a}_{31}& {a}_{32}& {a}_{33}\end{array}\right)\ast \left(\begin{array}{ccc}{k}_{11}& {k}_{12}& {k}_{13}\ {}{k}_{21}& {k}_{22}& {k}_{23}\ {}{k}_{31}& {k}_{32}& {k}_{33}\end{array}\right)=\sum \limits_{i=1}³\sum \limits_{j=1}³{a}_{ij}{k}_{ij} $$

然而,第一个版本的优点是使基本思想非常清楚:来自一个张量的每个元素乘以第二个张量的对应元素(相同位置的元素),然后将所有值求和以获得结果。

在上一节中,我们谈到了核,原因是卷积通常是在张量和核之间进行的,我们可以在这里用 A 表示。典型地,核很小,3 × 3 或 5 × 5,而输入张量 A 通常更大。例如,在图像识别中,输入张量 A 是尺寸可能高达 1024 × 1024 × 3 的图像,其中 1024 × 1024 是分辨率,最后一个尺寸(3)是颜色通道的数量,即 RGB 值。

在高级应用中,图像甚至可能具有更高的分辨率。为了理解当我们有不同维数的矩阵时如何应用卷积,让我们考虑一个 4 × 4 的矩阵 A

$$ A=\left(\begin{array}{cccc}{a}_1& {a}_2& {a}_3& {a}_4\ {}{a}_5& {a}_6& {a}_7& {a}_8\ {}{a}_9& {a}_{10}& {a}_{11}& {a}_{12}\ {}{a}_{13}& {a}_{14}& {a}_{15}& {a}_{16}\end{array}\right) $$

在这个例子中,我们将取核为 3 × 3

$$ K=\left(\begin{array}{ccc}{k}_1& {k}_2& {k}_3\ {}{k}_4& {k}_5& {k}_6\ {}{k}_7& {k}_8& {k}_9\end{array}\right) $$

想法是从矩阵的左上角 A 开始,选择一个 3 × 3 的区域。在这个例子中

$$ {A}_1=\left(\begin{array}{ccc}{a}_1& {a}_2& {a}_3\ {}{a}_5& {a}_6& {a}_7\ {}{a}_9& {a}_{10}& {a}_{11}\end{array}\right) $$

或者,这里用粗体标记的元素:

$$ A=\left(\begin{array}{cccc}{\boldsymbol{a}}_{\mathbf{1}}& {\boldsymbol{a}}_{\mathbf{2}}& {\boldsymbol{a}}_{\mathbf{3}}& {a}_4\ {}{\boldsymbol{a}}_{\mathbf{5}}& {\boldsymbol{a}}_{\mathbf{6}}& {\boldsymbol{a}}_{\mathbf{7}}& {a}_8\ {}{\boldsymbol{a}}_{\mathbf{9}}& {\boldsymbol{a}}_{\mathbf{1}\mathbf{0}}& {\boldsymbol{a}}_{\mathbf{1}\mathbf{1}}& {a}_{12}\ {}{a}_{13}& {a}_{14}& {a}_{15}& {a}_{16}\end{array}\right) $$

然后,我们执行卷积,如开头所解释的,在这个更小的矩阵 A 1K 之间,得到(我们将用 B 1 表示结果):

$$ {B}_1={A}_1\ast K={a}_1{k}_1+{a}_2{k}_2+{a}_3{k}_3+{k}_4{a}_5+{k}_5{a}_5+{k}_6{a}_7+{k}_7{a}_9+{k}_8{a}_{10}+{k}_9{a}_{11} $$

然后我们需要将一列的矩阵 A 中所选的 3 × 3 区域向右移动,并选择这里用粗体标记的元素:

$$ A=\left(\begin{array}{cccc}{a}_1& {\boldsymbol{a}}_{\mathbf{2}}& {\boldsymbol{a}}_{\mathbf{3}}& {\boldsymbol{a}}_{\mathbf{4}}\ {}{a}_5& {\boldsymbol{a}}_{\mathbf{6}}& {\boldsymbol{a}}_{\mathbf{7}}& {\boldsymbol{a}}_{\mathbf{8}}\ {}{a}_9& {\boldsymbol{a}}_{\mathbf{10}}& {\boldsymbol{a}}_{\mathbf{11}}& {\boldsymbol{a}}_{\mathbf{12}}\ {}{a}_{13}& {a}_{14}& {a}_{15}& {a}_{16}\end{array}\right) $$

这将给我们第二子矩阵 A 2 :

$$ {A}_2=\left(\begin{array}{ccc}{a}_2& {a}_3& {a}_4\ {}{a}_6& {a}_7& {a}_8\ {}{a}_{10}& {a}_{11}& {a}_{12}\end{array}\right) $$

然后,我们再次执行这个更小的矩阵 A 2K 之间的卷积:

$$ {B}_2={A}_2\ast K={a}_2{k}_1+{a}_3{k}_2+{a}_4{k}_3+{a}_6{k}_4+{a}_7{k}_5+{a}_8{k}_6+{a}_{10}{k}_7+{a}_{11}{k}_8+{a}_{12}{k}_9 $$

我们不能再向右移动我们的 3 × 3 区域,因为我们已经到达了矩阵 A 的末尾,所以我们要做的是将它向下移动一行,并从左侧重新开始。下一个选择的区域将是

$$ {A}_3=\left(\begin{array}{ccc}{a}_5& {a}_6& {a}_7\ {}{a}_9& {a}_{10}& {a}_{11}\ {}{a}_{13}& {a}_{14}& {a}_{15}\end{array}\right) $$

同样,我们执行A?? 3 与 K 的卷积

$$ {B}_3={A}_3\ast K={a}_5{k}_1+{a}_6{k}_2+{a}_7{k}_3+{a}_9{k}_4+{a}_{10}{k}_5+{a}_{11}{k}_6+{a}_{13}{k}_7+{a}_{14}{k}_8+{a}_{15}{k}_9 $$

您可能已经猜到了这一点,最后一步是将我们的 3 × 3 选定区域向右移动一列,并再次执行卷积。我们选择的区域现在将是

$$ {A}_4=\left(\begin{array}{ccc}{a}_6& {a}_7& {a}_8\ {}{a}_{10}& {a}_{11}& {a}_{12}\ {}{a}_{14}& {a}_{15}& {a}_{16}\end{array}\right) $$

此外,卷积将给出以下结果:

$$ {B}_4={A}_4\ast K={a}_6{k}_1+{a}_7{k}_2+{a}_8{k}_3+{a}_{10}{k}_4+{a}_{11}{k}_5+{a}_{12}{k}_6+{a}_{14}{k}_7+{a}_{15}{k}_8+{a}_{16}{k}_9 $$

现在我们不能再移动我们的 3 × 3 区域了,无论是向右还是向下。我们计算了四个值: B 1B 2B 3B 4 。这些元素将形成卷积运算的结果张量,给出张量 B :

$$ B=\left(\begin{array}{cc}{B}_1& {B}_2\ {}{B}_3& {B}_4\end{array}\right) $$

当张量 A 较大时,可以应用相同的过程。你将简单地得到一个更大的结果 B 张量,但是得到元素 B i 的算法是相同的。在继续之前,我们还有一个小细节需要讨论,那就是 stride 的概念。在前面的过程中,我们总是将 3 × 3 区域向右移动一列,向下移动一行。在本例 1 中,行数和列数称为步距,通常用 s 表示。Stride s = 2 仅仅意味着我们在每一步将我们的 3 × 3 区域向右移动两列,向下移动两行。

我们需要讨论的另一件事是输入矩阵 A 中选定区域的大小。在此过程中,我们移动的选定区域的尺寸必须与所使用的内核的尺寸相同。如果你使用 5 × 5 的内核,你需要在 A 中选择一个 5 × 5 的区域。一般来说,给定一个nK×nK内核,你在 A 中选择一个nK×nK区域。

在更正式的定义中,在神经网络上下文中,与步幅 s 的卷积是这样一个过程,它取一个张量 A 的维数nA×nA和一个核KnK×n**

$$ {n}_B=\left\lfloor \frac{n_A-{n}_K}{s}+1\right\rfloor $$

这里我们用⌊ x ⌋表示 x 的整数部分(在编程界,这通常被称为 x 的底)。这个公式的证明需要花很长时间来讨论,但是很容易看出为什么它是正确的(试着推导它)。为了简单一点,假设 n K 是奇数。你很快就会明白为什么这很重要(虽然不是基本的)。让我们开始正式解释这个情况,步长为 1。该算法根据以下公式从输入张量 A 和核 K 生成新的张量 B

$$ {B}_{ij}={\left(A\ast K\right)}_{ij}=\sum \limits_{f=0}^{n_K-1}\kern1em \sum \limits_{h=0}^{n_K-1}{A}_{i+f,j+h}{K}_{i+f,j+h} $$

这个公式晦涩难懂。让我们再研究一些例子,以便更好地理解意思。在图 3-1 中,你可以看到卷积如何工作的直观解释。假设有一个 3 × 3 的滤镜。那么在图 3-1 中,你可以看到矩阵 A 的左上九个元素,用黑色实线画出的正方形标记,就是根据这个公式用来生成矩阵B1 的第一个元素。用虚线画的正方形标记的元素是用于生成第二个元素B2 的元素,以此类推。

img/470317_1_En_3_Fig1_HTML.jpg

图 3-1

卷积的直观解释

为了重申我们在开始的例子中讨论的内容,基本思想是将矩阵 A 的 3 × 3 平方的每个元素乘以核 K 的相应元素,并将所有数字求和。这个和就是新矩阵 B 的元素。计算出B1 的值后,将原始矩阵中一列的区域向右移动(图 3-1 中用虚线表示的方块)并重复操作。您继续向右移动区域,直到到达边界,然后向下移动一个元素,并从左侧重新开始。你继续以这种方式,直到矩阵的右下角。相同的内核用于原始矩阵中的所有区域。

以内核$$ {\mathfrak{I}}_H $$为例,你可以在图 3-2 中看到 A 的哪些元素乘以$$ {\mathfrak{I}}_H $$中的哪些元素,元素B1 的结果就是所有乘法的总和

$$ {B}_{11}=1\times 1+2\times 1+3\times 1+1\times 0+2\times 0+3\times 0+4\times \left(-1\right)+3\times \left(-1\right)+2\times \left(-1\right)=-3 $$

img/470317_1_En_3_Fig2_HTML.jpg

图 3-2

与内核卷积的可视化$$ {\mathfrak{I}}_H $$

在图 3-3 中,可以看到步长 s = 2 的卷积示例。

img/470317_1_En_3_Fig3_HTML.jpg

图 3-3

步幅为 s = 2 的卷积的直观解释

输出矩阵的维数只占的底(整数部分)

$$ \frac{n_A-{n}_K}{s}+1 $$

在图 3-4 中可以直观的看到。如果 s > 1,根据 A 的尺寸,可能发生的情况是,在某一点上你不能再在矩阵 A (例如你在图 3-3 中看到的黑色方块)上移动你的窗口,并且你不能完全覆盖矩阵 A 的全部。在图 3-4 中,您可以看到如何在矩阵 A (标有许多 X)的右侧需要一个额外的列来执行卷积运算。在图 3-4 中,我们选择了 s = 3,由于我们有nA= 5 和nK= 3,因此 B 将是一个标量。

$$ {n}_B=\left\lfloor \frac{n_A-{n}_K}{s}+1\right\rfloor =\left\lfloor \frac{5-3}{3}+1\right\rfloor =\left\lfloor \frac{5}{3}\right\rfloor =1 $$

img/470317_1_En_3_Fig4_HTML.jpg

图 3-4

直观解释为什么在评估生成的矩阵 B 尺寸时需要 floor 函数

从图 3-4 中你可以很容易地看到,一个 3 × 3 的区域,只能覆盖 A 的左上区域,由于步长 s = 3,你会在 A 之外结束,因此可以只考虑一个区域进行卷积运算。因此,你最终得到了一个标量张量 B

现在让我们看几个额外的例子,让这个公式更加清晰。先说一个 3 × 3 的小矩阵

$$ A=\left(\begin{array}{ccc}1& 2& 3\ {}4& 5& 6\ {}7& 8& 9\end{array}\right) $$

此外,让我们考虑内核

$$ K=\left(\begin{array}{ccc}{k}_1& {k}_2& {k}_3\ {}{k}_4& {k}_5& {k}_6\ {}{k}_7& {k}_8& {k}_9\end{array}\right) $$

步幅 s = 1。卷积将由下式给出

$$ B=A\ast K=1\cdotp {k}_1+2\cdotp {k}_2+3\cdotp {k}_3+4\cdotp {k}_4+5\cdotp {k}_5+6\cdotp {k}_6+7\cdotp {k}_7+8\cdotp {k}_8+9\cdotp {k}_9 $$

而且,结果 B 会是一个标量,因为nA= 3,nK= 3。

$$ {n}_B=\left\lfloor \frac{n_A-{n}_K}{s}+1\right\rfloor =\left\lfloor \frac{3-3}{1}+1\right\rfloor =1 $$

如果你考虑一个维数为 4 × 4 的矩阵 A ,或者nA= 4,nK= 3, s = 1,你将得到维数为 2 × 2 的矩阵 B ,因为

$$ {n}_B=\left\lfloor \frac{n_A-{n}_K}{s}+1\right\rfloor =\left\lfloor \frac{4-3}{1}+1\right\rfloor =2 $$

例如,您可以验证给定的

$$ A=\left(\begin{array}{cccc}1& 2& 3& 4\ {}5& 6& 7& 8\ {}9& 10& 11& 12\ {}13& 14& 15& 16\end{array}\right) $$

$$ K=\left(\begin{array}{ccc}1& 2& 3\ {}4& 5& 6\ {}7& 8& 9\end{array}\right) $$

我们有步距为s= 1

$$ B=A\ast K=\left(\begin{array}{cc}348& 393\ {}528& 573\end{array}\right) $$

我们用我给你的公式来验证其中一个元素: B 11 。我们有

$$ {\displaystyle \begin{array}{l}{B}_{11}=\sum \limits_{f=0}²\kern1em \sum \limits_{h=0}²{A}_{1+f,1+h}{K}_{1+f,1+h}=\sum \limits_{f=0}²\left({\mathrm{A}}_{1+\mathrm{f},\kern0.5em 1}{K}_{1+f,1}+{\mathrm{A}}_{1+\mathrm{f},\kern0.5em 2}{K}_{1+f,2}+{\mathrm{A}}_{1+\mathrm{f},\kern0.5em 3}{K}_{1+f,3}\right)=\ {}\kern3em \left({\mathrm{A}}_{1,\kern0.5em 1}{K}_{1,1}+{\mathrm{A}}_{1,\kern0.5em 2}{K}_{1,2}+{\mathrm{A}}_{1,\kern0.5em 3}{K}_{1,3}\right)+\left({\mathrm{A}}_{2,\kern0.5em 1}{K}_{2,1}+{\mathrm{A}}_{2,\kern0.5em 2}{K}_{2,2}+{\mathrm{A}}_{2,\kern0.5em 3}{K}_{2,3}\right)+\ {}\kern3em \left({\mathrm{A}}_{3,\kern0.5em 1}{K}_{3,1}+{\mathrm{A}}_{3,\kern0.5em 2}{K}_{3,2}+{\mathrm{A}}_{3,\kern0.5em 3}{K}_{3,3}\right)=\left(1\cdotp 1+2\cdotp 2+3\cdotp 3\right)+\left(5\cdotp 4+6\cdotp 5+7\cdotp 6\right)+\ {}\kern3em \left(9\cdotp 7+10\cdotp 8+11\cdotp 9\right)=14+92+242=348\end{array}} $$

请注意,我给你的卷积公式仅适用于步长 s = 1,但可以很容易地推广到其他值的 s

这个计算很容易用 Python 实现。对于 s = 1,下面的函数可以足够容易地计算两个矩阵的卷积(您可以在 Python 中使用现有的函数来完成,但我认为从头开始看如何做是有启发性的):

import numpy as np
def conv_2d(A, kernel):
    output = np.zeros([A.shape[0]-(kernel.shape[0]-1), A.shape[1]-(kernel.shape[0]-1)])

    for row in range(1,A.shape[0]-1):
        for column in range(1, A.shape[1]-1):
            output[row-1, column-1] = np.tensordot(A[row-1:row+2, column-1:column+2], kernel)

    return output

注意,输入矩阵 A 甚至不需要是平方矩阵,但是假设内核是并且它的维数nK是奇数。可以用下面的代码评估前面的示例:

A = np.array([[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]])
K = np.array([[1,2,3],[4,5,6],[7,8,9]])
print(conv_2d(A,K))

这给出了结果:

[[ 348\. 393.]
[ 528\. 573.]]

卷积的例子

现在,让我们尝试将我们在开始时定义的内核应用到一个测试图像中,看看结果。作为测试图像,让我们用代码创建一个尺寸为 160 × 160 像素的棋盘:

chessboard = np.zeros([8*20, 8*20])
for row in range(0, 8):
    for column in range (0, 8):
        if ((column+8*row) % 2 == 1) and (row % 2 == 0):
            chessboard[row*20:row*20+20, column*20:column*20+20] = 1
        elif ((column+8*row) % 2 == 0) and (row % 2 == 1):
            chessboard[row*20:row*20+20, column*20:column*20+20] = 1

在图 3-5 中,可以看到棋盘的样子。

img/470317_1_En_3_Fig5_HTML.jpg

图 3-5

用代码生成的棋盘图像

现在让我们用步长为 s = 1 的不同内核对该图像进行卷积。

使用内核,$$ {\mathfrak{I}}_H $$将检测水平边缘。这可以应用于代码

edgeh = np.matrix('1 1 1; 0 0 0; -1 -1 -1')
outputh = conv_2d (chessboard, edgeh)

在图 3-6 中,您可以看到输出的样子。使用以下代码可以很容易地生成图像:

img/470317_1_En_3_Fig6_HTML.jpg

图 3-6

在内核$$ {\mathfrak{I}}_H $$和棋盘图像之间执行卷积的结果

Import matplotlib.pyplot as plt
plt.imshow(outputh)

现在你可以理解为什么这个内核检测水平边缘了。此外,这个内核检测你什么时候从亮到暗,反之亦然。注意,正如所料,这张图片只有 158 × 158 像素,因为

$$ {n}_B=\left\lfloor \frac{n_A-{n}_K}{s}+1\right\rfloor =\left\lfloor \frac{160-3}{1}+1\right\rfloor =\left\lfloor \frac{157}{1}+1\right\rfloor =\left\lfloor 158\right\rfloor =158 $$

现在让我们使用这段代码来应用$$ {\mathfrak{I}}_V $$:

edgev = np.matrix('1 0 -1; 1 0 -1; 1 0 -1')
outputv = conv_2d (chessboard, edgev)

这给出了如图 3-7 所示的结果。

img/470317_1_En_3_Fig7_HTML.jpg

图 3-7

在内核$$ {\mathfrak{I}}_V $$和棋盘图像之间执行卷积的结果

现在我们可以使用内核$$ {\mathfrak{I}}_L $$:

edgel = np.matrix ('-1 -1 -1; -1 8 -1; -1 -1 -1')
outputl = conv_2d (chessboard, edgel)

这给出了如图 3-8 所示的结果。

img/470317_1_En_3_Fig8_HTML.jpg

图 3-8

在内核$$ {\mathfrak{I}}_L $$和棋盘图像之间执行卷积的结果

此外,我们可以应用模糊内核$$ {\mathfrak{I}}_B $$:

edge_blur = -1.0/9.0*np.matrix('1 1 1; 1 1 1; 1 1 1')
output_blur = conv_2d (chessboard, edge_blur)

在图 3-9 中,你可以看到两幅图——左边是模糊图像,右边是原始图像。这些图像只显示了原始棋盘的一小部分区域,以使模糊更加清晰。

img/470317_1_En_3_Fig9_HTML.jpg

图 3-9

模糊内核$$ {\mathfrak{I}}_B $$的效果左边是模糊图像,右边是原始图像。

为了结束这一部分,让我们试着更好地理解如何检测边缘。考虑具有急剧垂直过渡的以下矩阵,因为左边部分全是 10,右边部分全是 0。

ex_mat = np.matrix('10 10 10 10 0 0 0 0; 10 10 10 10 0 0 0 0; 10 10 10 10 0 0 0 0; 10 10 10 10 0 0 0 0; 10 10 10 10 0 0 0 0; 10 10 10 10 0 0 0 0; 10 10 10 10 0 0 0 0; 10 10 10 10 0 0 0 0')

这看起来像这样

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

我们来考虑一下内核$$ {\mathfrak{I}}_V $$。我们可以用这段代码执行卷积:

ex_out = conv_2d (ex_mat, edgev)

结果如下:

array([[ 0., 0., 30., 30., 0., 0.],
       [ 0., 0., 30., 30., 0., 0.],
       [ 0., 0., 30., 30., 0., 0.],
       [ 0., 0., 30., 30., 0., 0.],
       [ 0., 0., 30., 30., 0., 0.],
       [ 0., 0., 30., 30., 0., 0.]])

在图 3-10 中,可以看到原始矩阵(左边)和右边卷积的输出。与内核$$ {\mathfrak{I}}_V $$的卷积已经清楚地检测到原始矩阵中的急剧转变,在从黑到白的转变发生的地方用垂直黑线标记。例如,考虑B11= 0

$$ {\displaystyle \begin{array}{l}{B}_{11}=\left(\begin{array}{ccc}10& 10& 10\ {}10& 10& 10\ {}10& 10& 10\end{array}\right)\ast {\mathfrak{I}}_V=\left(\begin{array}{ccc}10& 10& 10\ {}10& 10& 10\ {}10& 10& 10\end{array}\right)\ast \left(\begin{array}{ccc}1& 0& -1\ {}1& 0& -1\ {}1& 0& -1\end{array}\right)\ {}=10\times 1+10\times 0+10\times -1+10\times 1+10\times 0+10\times -1+10\times 1+10\times 0+10\times -1=0\end{array}} $$

注意,在输入矩阵中

$$ \left(\begin{array}{ccc}10& 10& 10\ {}10& 10& 10\ {}10& 10& 10\end{array}\right) $$

没有过渡,因为所有值都是相同的。相反,如果你考虑B13 你需要考虑输入矩阵的这个区域

$$ \left(\begin{array}{ccc}10& 10& 0\ {}10& 10& 0\ {}10& 10& 0\end{array}\right) $$

其中有一个明显的过渡,因为最右列由 0 和其余的 10 组成。你现在得到一个不同的结果

$$ {\displaystyle \begin{array}{l}{B}_{11}=\left(\begin{array}{ccc}10& 10& 0\ {}10& 10& 0\ {}10& 10& 0\end{array}\right)\ast {\mathfrak{I}}_V=\left(\begin{array}{ccc}10& 10& 0\ {}10& 10& 0\ {}10& 10& 0\end{array}\right)\ast \left(\begin{array}{ccc}1& 0& -1\ {}1& 0& -1\ {}1& 0& -1\end{array}\right)\ {}=10\times 1+10\times 0+0\times -1+10\times 1+10\times 0+0\times -1+10\times 1+10\times 0+0\times -1=30\end{array}} $$

此外,这正是一旦水平方向上的值有显著变化,卷积就返回高值的原因,因为在核中乘以列 1 的值将更有意义。当沿着水平轴存在从小到大的值的转变时,乘以-1 的元素将给出绝对值更大的结果。因此,最终结果将是负的,绝对值很大。这就是为什么这个内核也可以检测到你是否从一个浅色到一个深色,反之亦然。如果您考虑不同假设矩阵中的相反转变(从 0 到 10),您将会

$$ {\displaystyle \begin{array}{l}{B}_{11}=\left(\begin{array}{ccc}0& 10& 10\ {}0& 10& 10\ {}0& 10& 10\end{array}\right)\ast {\mathfrak{I}}_V=\left(\begin{array}{ccc}0& 10& 10\ {}0& 10& 10\ {}0& 10& 10\end{array}\right)\ast \left(\begin{array}{ccc}1& 0& -1\ {}1& 0& -1\ {}1& 0& -1\end{array}\right)\ {}=0\times 1+10\times 0+10\times -1+0\times 1+10\times 0+10\times -1+0\times 1+10\times 0+10\times -1=-30\end{array}} $$

我们沿着水平方向从 0 移动到 10。

img/470317_1_En_3_Fig10_HTML.jpg

图 3-10

如文本中所述,矩阵ex_mat与核$$ {\mathfrak{I}}_V $$的卷积结果

请注意,正如所料,输出矩阵的维数是 5 × 5,因为原始矩阵的维数是 7 × 7,而核是 3 × 3。

联营

池化是 CNN 的第二个基本操作。这个运算比卷积容易理解得多。为了理解它,让我们看一个具体的例子,并考虑什么叫做 max pooling。再次考虑我们在卷积讨论中讨论过的 4 × 4 矩阵:

$$ A=\left(\begin{array}{cccc}{a}_1& {a}_2& {a}_3& {a}_4\ {}{a}_5& {a}_6& {a}_7& {a}_8\ {}{a}_9& {a}_{10}& {a}_{11}& {a}_{12}\ {}{a}_{13}& {a}_{14}& {a}_{15}& {a}_{16}\end{array}\right) $$

为了执行最大池,我们需要定义一个大小为nK×nK的区域,类似于我们对卷积所做的。我们来考虑一下nK= 2。我们需要做的是从我们的矩阵左上角的 A 开始,选择一个nK×nK区域,在我们的例子中是从 A 的 2 × 2。在这里,我们将选择

$$ \left(\begin{array}{cc}{a}_1& {a}_2\ {}{a}_5& {a}_6\end{array}\right) $$

或者,矩阵中以粗体标记的元素 A 在此:

$$ A=\left(\begin{array}{cccc}{\boldsymbol{a}}_{\mathbf{1}}& {\boldsymbol{a}}_{\mathbf{2}}& {a}_3& {a}_4\ {}{\boldsymbol{a}}_{\mathbf{5}}& {\boldsymbol{a}}_{\mathbf{6}}& {a}_7& {a}_8\ {}{a}_9& {a}_{10}& {a}_{11}& {a}_{12}\ {}{a}_{13}& {a}_{14}& {a}_{15}& {a}_{16}\end{array}\right) $$

从选择的元素中, a 1a 2a 5a 6 ,最大汇集运算选择最大值。结果用B1 表示

$$ {B}_1=\underset{i=1,2,5,6}{\max }{a}_i $$

然后,我们需要将 2 × 2 窗口向右移动两列,通常与所选区域的列数相同,并选择以粗体标记的元素:

$$ A=\left(\begin{array}{cccc}{a}_1& {a}_2& {\boldsymbol{a}}_{\mathbf{3}}& {\boldsymbol{a}}_{\mathbf{4}}\ {}{a}_5& {a}_6& {\boldsymbol{a}}_{\mathbf{7}}& {\boldsymbol{a}}_{\mathbf{8}}\ {}{a}_9& {a}_{10}& {a}_{11}& {a}_{12}\ {}{a}_{13}& {a}_{14}& {a}_{15}& {a}_{16}\end{array}\right) $$

或者换句话说,更小的矩阵

$$ \left(\begin{array}{cc}{a}_3& {a}_4\ {}{a}_7& {a}_8\end{array}\right) $$

然后,最大池算法将选择这些值中的最大值,并给出一个用 B 2 表示的结果

$$ {B}_2=\underset{i=3,4,7,8}{\max }{a}_i $$

此时,我们不能再将 2 × 2 区域向右移动,所以我们将其向下移动两行,并从 A 的左侧再次开始该过程,选择以粗体标记的元素并获得最大值,将其命名为 B 3

$$ A=\left(\begin{array}{cccc}{a}_1& {a}_2& {a}_3& {a}_4\ {}{a}_5& {a}_6& {a}_7& {a}_8\ {}{\boldsymbol{a}}_{\mathbf{9}}& {\boldsymbol{a}}_{\mathbf{10}}& {a}_{11}& {a}_{12}\ {}{\boldsymbol{a}}_{\mathbf{13}}& {\boldsymbol{a}}_{\mathbf{14}}& {a}_{15}& {a}_{16}\end{array}\right) $$

在这种情况下,步距 s 与我们在卷积中已经讨论过的意义相同。它只是在选择元素时移动区域的行数或列数。最后,我们选择 A 底部的最后一个区域 2 × 2,选择元素A11、A12、A15 和 a 16 。然后我们得到最大值,称之为B4。利用我们在此过程中获得的值,在本例中是四个值 B 1B 、 2 、 B 、 3 和 B4 ,我们将构建一个输出张量:

$$ B=\left(\begin{array}{cc}{B}_1& {B}_2\ {}{B}_3& {B}_4\end{array}\right) $$

在这个例子中,我们有 s = 2。基本上,该操作将矩阵 A 、步距 s 和内核大小 n K (我们在之前的示例中选择的区域的维度)作为输入,并返回新的矩阵 B ,其维度由我们针对卷积讨论的相同公式给出:

$$ {n}_B=\left\lfloor \frac{n_A-{n}_K}{s}+1\right\rfloor $$

为了重申这个想法,从矩阵 A 的左上角开始,取一个维度为nnK的区域,对所选元素应用 max 函数,然后向右移动 s 元素的区域,再次选择一个维度为 n K 的新区域在图 3-11 中,您可以看到如何从步长为 s = 2 的矩阵 A 中选择元素。

img/470317_1_En_3_Fig11_HTML.jpg

图 3-11

步长为 s = 2 的池的可视化

例如,对输入 A 应用最大池化

$$ A=\left(\begin{array}{cccc}1& 3& 5& 7\ {}4& 5& 11& 3\ {}4& 1& 21& 6\ {}13& 15& 1& 2\end{array}\right) $$

会给你这个结果(很容易验证):

$$ B=\left(\begin{array}{cc}4& 11\ {}15& 21\end{array}\right) $$

因为四是用粗体标记的值的最大值。

$$ A=\left(\begin{array}{cccc}\mathbf{1}& \mathbf{3}& 5& 7\ {}\mathbf{4}& \mathbf{5}& 11& 3\ {}4& 1& 21& 6\ {}13& 15& 1& 2\end{array}\right) $$

11 是这里用粗体标记的最大值:

$$ A=\left(\begin{array}{cccc}1& 3& \mathbf{5}& \mathbf{7}\ {}4& 5& \mathbf{11}& \mathbf{3}\ {}4& 1& 21& 6\ {}13& 15& 1& 2\end{array}\right) $$

诸如此类。值得一提的是另一种池化方法,尽管它没有 max-pooling 使用得那么广泛: 平均池化 。它不是返回所选值的最大值,而是返回平均值。

注意

最常用的池操作是最大池。平均池的使用并不广泛,但可以在特定的网络架构中找到。

填料

这里值得一提的是填充。有时,在处理图像时,从维度不同于原始图像的卷积运算中获得结果并不是最佳选择。这时需要填充。这个想法很简单:在最终图像的顶部和底部添加像素行,在右侧和左侧添加像素列,这样得到的矩阵与原始矩阵大小相同。一些策略用零填充添加的像素,用最接近的像素的值填充,等等。例如,在我们的例子中,带有零填充的ex_out矩阵如下所示

array([[ 0., 0., 0., 0., 0., 0., 0., 0.],
       [ 0., 0., 0., 30., 30., 0., 0., 0.],
       [ 0., 0., 0., 30., 30., 0., 0., 0.],
       [ 0., 0., 0., 30., 30., 0., 0., 0.],
       [ 0., 0., 0., 30., 30., 0., 0., 0.],
       [ 0., 0., 0., 30., 30., 0., 0., 0.],
       [ 0., 0., 0., 30., 30., 0., 0., 0.],
       [ 0., 0., 0., 0., 0., 0., 0., 0.]])

仅作为参考,在使用填充符 p (用作填充符的行和列的宽度)的情况下,在卷积和合并的情况下,矩阵 B 的最终尺寸由下式给出

$$ {n}_B=\left\lfloor \frac{n_A+2p-{n}_K}{s}+1\right\rfloor $$

注意

当处理真实图像时,你总是有彩色图像,用三个通道编码:RGB。这意味着卷积和合并必须在三个维度上完成:宽度、高度和颜色通道。这将增加算法的复杂性。

CNN 的构建模块

卷积和汇集操作用于构建 CNN 中使用的层。在 CNN 中,您通常可以找到以下层

  • 卷积层

  • 池层

  • 完全连接的层

全连接层正是我们在前面所有章节中看到的:一个层,其中的神经元与前一层和后一层的所有神经元相连。你已经认识他们了。另外两个需要一些额外的解释。

卷积层

卷积层将张量(由于三个颜色通道,它可以是三维的)作为输入,例如图像,应用特定数量的核,通常是 10、16 或更多,添加偏差,应用 ReLu 激活函数(例如)以将非线性引入卷积的结果,并产生输出矩阵 B

在前面的章节中,我展示了一些用一个内核应用卷积的例子。如何同时应用几个内核?答案很简单。最终的张量(我现在使用张量这个词,因为它不再是一个简单的矩阵) B 将不是二维而是三维。让我们用nc来表示您想要申请的内核数量(由于有时人们会谈到通道,所以会使用 c )。您只需将每个过滤器独立应用于输入,并将结果堆叠起来。因此,你得到的不是一个维数为 nB×nB×的单一矩阵BnB×BnBB$$ \overset{\sim }{B} $$这意味着这

$$ {\overset{\sim }{B}}_{i,j,1}\kern1em \forall i,j\in \left[1,{n}_B\right] $$

*将是输入图像与第一内核的卷积的输出,以及

$$ {\overset{\sim }{B}}_{i,j,2}\kern1em \forall i,j\in \left[1,{n}_B\right] $$

将是与第二个内核卷积的输出,以此类推。卷积层只是将输入转换成输出张量。然而,这一层的权重是什么呢?网络在训练阶段学习的权重或参数是内核本身的元素。我们讨论过我们有 n c 个内核,每个nK×nK个维度。这意味着卷积层中有$$ {n}_K²{n}_c $$参数。

注意

卷积层中的参数数量$$ {n}_K²{n}_c $$与输入图像大小无关。这个事实有助于减少过度拟合,尤其是在处理大输入图像时。

有时这一层用单词POOL表示,然后是一个数字。在我们的例子中,我们可以用POOL1来表示这个层。在图 3-12 中,你可以看到一个卷积层的示意图。通过应用与维度为nA×nA×nc的张量中的nc核的卷积来变换输入图像。

img/470317_1_En_3_Fig12_HTML.jpg

图 3-12

卷积层的表示 2

当然,卷积层不一定紧接在输入之后。当然,卷积层可以将任何其他层的输出作为输入。请记住,输入图像通常会有尺寸nA×nA×3,因为彩色图像有三个通道:红色、绿色和蓝色。在考虑彩色图像时,对 CNN 中张量的完整分析超出了本书的范围。在图中,层通常被简单地表示为立方体或正方形。

池层

池层通常用POOL和一个数字表示:例如POOL1。它将一个张量作为输入,在将池应用于输入后,给出另一个张量作为输出。

注意

一个池层没有需要学习的参数,但是它引入了额外的超参数: n K 和 stride v 。通常,在池化层中,不使用任何填充,因为使用池化的原因之一通常是为了减少张量的维数。

将层堆叠在一起

在 CNN 中,你通常将卷积层和池层堆叠在一起。一个接一个。在图 3-13 中,您可以看到一个卷积层和一个池层堆栈。卷积层之后总是有一个池层。有时这两层合在一起被称为。原因是池层没有可学习的权重,因此它仅仅被视为与卷积层相关联的简单操作。因此,当你阅读报纸或博客时,要注意并检查他们的意图。

img/470317_1_En_3_Fig13_HTML.jpg

图 3-13

如何堆叠卷积层和池层的表示

在图 3-14 中总结 CNN 的这一部分,你可以看到一个 CNN 的例子。在图 3-14 中,你可以看到一个非常著名的 LeNet-5 网络的例子,你可以在这里阅读更多内容: https://goo.gl/hM1kAL 。您有输入,然后两次卷积池层,然后三个完全连接的层,然后一个输出层,用一个softmax激活函数来执行多类分类。我在图中放了一些指示性的数字,让你对不同层的大小有个概念。

img/470317_1_En_3_Fig14_HTML.jpg

图 3-14

类似于著名的 LeNet-5 网络的 CNN 的代表

CNN 中的权重数

指出 CNN 中的权重在不同层中的位置是很重要的。

卷积层

在卷积层中,学习的参数是滤波器本身。例如,如果您有 32 个滤波器,每个尺寸为 5×5,您将获得 32×5×5 = 832 个可学习参数,因为对于每个滤波器,您还需要添加一个偏差项。请注意,这个数字不取决于输入图像的大小。在典型的前馈神经网络中,第一层中的权重数取决于输入大小,但在这里不是这样。

一般来说,卷积层中的权重数由下式给出:

$$ {n}_C\cdotp {n}_K\cdotp {n}_K+{n}_C $$

汇集层

池层没有可学习的参数,正如前面提到的,这是它通常与卷积层相关联的原因。在这一层(操作)中,没有可学习的权重。

致密层

在这一层中,权重是您从传统的前馈网络中知道的权重。所以数量取决于神经元的数量以及前一层和后一层的神经元数量。

注意

CNN 中唯一具有可学习参数的层是卷积层和致密层。

CNN: MNIST 数据集的例子

让我们从一些编码开始。我们将开发一个非常简单的 CNN,并尝试在 MNIST 数据集上进行分类。从第章到第章,你现在应该对数据集非常了解了。

像往常一样,我们首先导入必要的包:

from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten, Conv2D, MaxPool2D
from keras.utils import np_utils

import numpy as np
import matplotlib.pyplot as plt

在开始加载数据之前,我们需要一个额外的步骤:

from keras import backend as K
K.set_image_dim_ordering('th')

原因如下。为模型加载图像时,您需要将它们转换为张量,每个张量都有三个维度:

  • 沿 x 轴的像素数

  • 沿 y 轴的像素数

  • 颜色通道的数量(在灰色图像中,此数量为;如果您有彩色图像,这个数字是 3,每个 RGB 通道一个)

在做卷积时,Keras 必须知道它在哪个轴上找到信息。特别是,定义颜色通道维度的索引是第一个还是最后一个是相关的。为了实现这一点,我们可以用keras.backend.set_image_dim_ordering()来定义数据的排序。该函数接受一个字符串作为输入,该字符串可以有两个可能的值:

  • 'th'(对于库 Theano 使用的约定):Theano 期望通道维度是第二个(第一个将是观察指标)。

  • 'tf'(TensorFlow 使用的约定):tensor flow 期望通道维度是最后一个。

您可以使用这两种方法,但是在准备数据时要注意使用正确的约定。否则,你会得到关于张量维数的错误信息。在接下来的内容中,我们将转换张量中的图像,颜色通道维度作为第二个维度,稍后你会看到。

现在,我们准备用以下代码加载 MNIST 数据:

(X_train, y_train), (X_test, y_test) = mnist.load_data()

该代码将交付“扁平化”的图像,这意味着每个图像将是一个包含 784 个元素(28x28)的一维向量。我们需要将它们重塑为合适的图像,因为我们的卷积层需要图像作为输入。之后,我们需要标准化数据(记住图像是灰度的,每个像素可以有一个从 0 到 255 的值)。

X_train = X_train.reshape(X_train.shape[0], 1, 28, 28).astype('float32')
X_test = X_test.reshape(X_test.shape[0], 1, 28, 28).astype('float32')
X_train = X_train / 255.0
X_test = X_test / 255.0

请注意,既然我们已经将排序定义为'th',那么通道的数量(在本例中为 1)就是X数组的第二个元素。下一步,我们需要对标签进行一次性热编码:

y_train = np_utils.to_categorical(y_train)
y_test = np_utils.to_categorical(y_test)

我们知道我们有 10 个类,所以我们可以简单地定义它们:

num_classes = 10

现在让我们定义一个函数来创建和编译我们的 Keras 模型:

def baseline_model():
    # create model
    model = Sequential()
    model.add(Conv2D(32, (5, 5), input_shape=(1, 28, 28), activation="relu"))
    model.add(MaxPool2D(pool_size=(2, 2)))
    model.add(Dropout(0.2))
    model.add(Flatten())
    model.add(Dense(128, activation="relu"))
    model.add(Dense(num_classes, activation="softmax"))
    # Compile model
    model.compile(loss='categorical_crossentropy', optimizer="adam", metrics=['accuracy'])
    return model

你可以在图 3-15 中看到这个 CNN 的示意图。

img/470317_1_En_3_Fig15_HTML.jpg

图 3-15

描述我们在文中使用的 CNN 的图表。这些数字是每一层产生的张量的维数。

为了确定我们有哪种模型,我们简单地使用model.summary()调用。让我们首先创建一个模型,然后检查它:

model = baseline_model()
model.summary()

输出(查看图 3-15 中的图表)如下:

_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
conv2d_1 (Conv2D)            (None, 32, 24, 24)        832
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 32, 12, 12)        0
_________________________________________________________________
dropout_1 (Dropout)          (None, 32, 12, 12)        0
_________________________________________________________________
flatten_1 (Flatten)          (None, 4608)              0
_________________________________________________________________
dense_1 (Dense)              (None, 128)               589952
_________________________________________________________________
dense_2 (Dense)              (None, 10)                1290
=================================================================
Total params: 592,074
Trainable params: 592,074
Non-trainable params: 0

如果您想知道为什么 max-pooling 层产生 12x12 尺寸的张量,原因是因为我们没有指定跨距,Keras 将把过滤器的尺寸作为标准值,在我们的例子中是 2x2。步长为 2 的输入张量为 24x24,您将得到 12x12 的张量。

这个网络相当简单。在模型中,我们只定义了一个卷积和池化层,我们添加了一点 dropout,然后添加了一个具有 128 个神经元的密集层,然后为具有 10 个神经元的softmax添加了一个输出层。现在我们可以简单地用fit()方法训练它:

model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=1, batch_size=200, verbose=1)

这将只训练一个时期的网络,并且应该给出如下输出(您的数字可能会稍有不同):

Train on 60000 samples, validate on 10000 samples
Epoch 1/1
60000/60000 [==============================] - 151s 3ms/step - loss: 0.0735 - acc: 0.9779 - val_loss: 0.0454 - val_acc: 0.9853

我们已经达到了良好的精度,没有任何过度拟合。

注意

当您将优化器参数传递给compile()方法时,Keras 将使用它的标准参数。如果您想要更改它们,您需要单独定义一个优化器。例如,要指定一个起始学习率为 0.001 的 Adam 优化器,可以使用AdamOpt = adam(lr=0.001),然后用model.compile(optimizer=AdamOpt, loss="categorical_crossentropy", metrics=['accuracy'])将其传递给编译方法。

CNN 学习的可视化

简单题外话:keras.backend.function()

有时从计算图中获得中间结果是有用的。例如,出于调试目的,您可能对特定层的输出感兴趣。在低级 TensorFlow 中,您可以简单地在会话中评估图中的相关节点,但要理解如何在 Keras 中执行并不那么容易。为了找到答案,我们需要考虑 Keras 后端是什么。最好的解释方式就是引用官方文献( https://keras.io/backend/ ):

Keras 是一个模型级的库,为开发深度学习模型提供高级的构建模块。它本身不处理张量积、卷积等低级运算。相反,它依赖于一个专门的、优化良好的张量操作库来完成,充当 Keras 的“后端引擎”。

为了完整起见,需要注意的是 Keras 使用了三个后端:TensorFlow 后端、?? 后端和 ?? 后端。当您想要编写自己的特定函数时,您应该使用抽象的 Keras 后端 API,它可以用以下代码加载:

from keras import backend as K

理解如何使用 Keras 后端超出了本书的范围(记住本书的重点不是 Keras),但是我建议你花一些时间去了解它。可能会很有用。例如,要在使用 Keras 时重置会话,可以使用以下命令:

keras.backend.clear_session()

这一章我们真正感兴趣的是在后面提供的一个具体方法:function()。其论点如下:

  • 输入:占位符张量列表

  • 输出:输出张量列表

  • 更新:更新操作列表

  • **kwargs:传递到tf.Session.run

在本章中,我们将只使用前两个。为了理解如何使用它们,让我们以前面几节中创建的模型为例:

model = Sequential()
model.add(Conv2D(32, (5, 5), input_shape=(1, 28, 28), activation="relu"))
model.add(MaxPool2D(pool_size=(2, 2)))
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(128, activation="relu"))
model.add(Dense(num_classes, activation="softmax"))

例如,我们如何获得第一个卷积层的输出?我们可以通过创建一个函数轻松做到这一点:

get_1st_layer_output = K.function([model.layers[0].input],[model.layers[0].output])

这将使用以下参数

  • 输入:model.layers[0].input,这是我们网络的输入

  • outputs: model.layers[0].output,第一层的输出(索引为 0)

给定一组特定的输入,您只需让 Keras 评估您的计算图中的特定节点。注意到目前为止我们只定义了一个函数。现在我们需要将它应用到特定的数据集。例如,如果我们想将它应用到一个单一的图像,我们可以这样做:

layer_conv_output = get_1st_layer_output([np.expand_dims(X_test[21], axis=0)])[0]

这个多维数组的维数(1, 32, 24, 24)和预期的一样:一个图像,32 个过滤器,24x24 输出。在下一节中,我们将使用该函数来查看网络中学习过的滤波器的效果。

内核效应

有趣的是,可以看到学习后的核对输入图像的影响。为此,让我们从测试数据集中取一个图像(如果您打乱了数据集,您可能会在索引 21 处得到一个不同的数字)。

tst = X_test[21]

注意这个数组是如何拥有维度(1,28,28)的。这是一个六,如图 3-16 所示。

img/470317_1_En_3_Fig16_HTML.jpg

图 3-16

测试数据集中的第一个图像

为了获得第一层(卷积层)的效果,我们可以使用下面的代码(在前一节中解释过)

get_1st_layer_output = K.function([model.layers[0].input],[model.layers[0].output])
layer_conv_output = get_1st_layer_output([tst])[0]

请注意layer_conv_output是一个多维数组,它将包含输入图像与每个滤波器的卷积,相互堆叠。它的维数是(1,32,24,24)。第一个数字是 1,因为我们仅将该层应用于一个单个图像,第二个数字是 32,是我们拥有的过滤器的数量,第二个数字是 24,因为如我们所讨论的,conv层的输出张量维数由下式给出

$$ {n}_B=\left\lfloor \frac{n_A+2p-{n}_K}{s}+1\right\rfloor $$

此外,在我们的情况下

$$ {n}_B=\left\lfloor \frac{28-5}{1}+1\right\rfloor =24 $$

img/470317_1_En_3_Fig17_HTML.jpg

图 3-17

测试图像(a 6)与网络学习的前 12 个过滤器进行了卷积

既然在我们的网络中,我们有 n A = 28, p = 0,nK= 5,步距 s = 1。在图 3-17 中,你可以看到我们的测试图像与前 12 个滤波器(32 个对于一个图来说太多了)进行了卷积。

从图 3-17 中,您可以看到不同的过滤器如何学习检测不同的特征。例如,第三个过滤器(如图 3-18 所示)学会了检测对角线。

img/470317_1_En_3_Fig18_HTML.jpg

图 3-18

测试图像与第三滤波器卷积。它学会了检测对角线。

其他过滤器学习检测水平线或其他特征。

最大池效应

下一步是将最大池应用于卷积层的输出。正如我们所讨论的,这将减少张量的维数,并试图(直观地)浓缩相关信息。

在图 3-19 中,您可以看到来自前 12 个滤波器的张量输出。

img/470317_1_En_3_Fig19_HTML.jpg

图 3-19

当应用于来自卷积层的前 12 个张量时,池层的输出

让我们看看我们的测试图像是如何通过一个过滤器从一个卷积层和池层进行转换的(考虑第三个,只是为了说明)。在图 3-20 中可以很容易地看到效果。

img/470317_1_En_3_Fig20_HTML.jpg

图 3-20

数据集(图 a)中的原始测试图像;与第三学习滤波器卷积的图像(b 图);在最大池图层之后,使用第三个滤镜进行卷积的图像(面板 c)

请注意图像的分辨率是如何变化的,因为我们没有使用任何填充。在下一章中,我们将看看更复杂的架构,称为盗梦网络 ,它们在处理图像时比传统的 CNN(我们在本章中已经描述过)工作得更好。事实上,简单地增加越来越多的卷积层不会轻易提高预测的准确性,而更复杂的架构会更有效。

既然我们已经看到了 CNN 最基本的组成部分,我们准备进入一些更高级的话题。在下一章中,我们将探讨许多令人兴奋的话题,如初始网络、多重损失函数、自定义损失函数和迁移学习。

Footnotes 1

你可以在维基百科的 https://en.wikipedia.org/wiki/Kernel_(image_processing) 找到一个很好的概述。

2

猫图片来源: https://www.shutterstock.com/

*

四、高级 CNN 和迁移学习

在这一章中,我们来看看在开发 CNN 时通常使用的更高级的技术。特别是,我们将看到一个非常成功的新卷积网络,称为初始网络,它基于并行而不是顺序完成几个卷积运算的思想。然后,我们将看看如何使用多重成本函数,其方式与多任务学习中的方式类似。下一节将向您展示如何使用 Keras 提供的预训练网络,以及如何使用迁移学习来针对您的特定问题调整这些预训练网络。在本章的最后,我们将研究一种实现迁移学习的技术,这种技术在处理大数据集时非常有效。

多通道卷积

在前一章中,你学习了卷积是如何工作的。在示例中,我们已经明确描述了当输入是二维矩阵时如何执行它。但现实并非如此。例如,输入张量可以表示彩色图像,因此将具有三维:在 x 方向上的像素数量(沿着 x 轴的分辨率)、在 y 方向上的像素数量(沿着 y 轴的分辨率)、以及颜色通道的数量,当处理 RGB 图像时是三个(一个通道用于红色,一个用于绿色,一个用于蓝色)。情况可能会更糟。一个具有 32 个核的卷积层,每个核为 5 × 5,当期望输入每个为 28 × 28 的图像时(参见上一章中的 MNIST 示例),将具有维数为( m ,32,24,24)的输出,其中 m 是训练图像的数量。这意味着我们的卷积必须用 32 × 24 × 24 的张量来完成。那么我们如何对三维张量进行卷积运算呢?嗯,其实很简单。从数学上讲,如果内核 K 有维度nK×nK×nc,输入张量 A 有维度 n x

$$ \sum \limits_{i=1}^{n_x}\sum \limits_{j=1}^{n_y}\sum \limits_{k=1}^{n_c}{K}_{ijk}{A}_{ijk} $$

这意味着我们将对通道维度求和。在 Keras 中,当您在 2D 定义卷积层时,可以使用以下代码:

Conv2D(32, (5, 5), input_shape=(1, 28, 28), activation="relu")

其中第一个数字(32)是过滤器的数量,而(5,5)定义了内核的尺寸。Keras 没有告诉你的是,它自动取核 n c × 5 × 5 其中 n c 是输入张量的通道数。这就是为什么你需要给第一层input_shape参数。该信息中包含通道数。但是这三个数字哪个是正确的呢?Keras 怎么知道在这种情况下正确的是 1 而不是 28?

让我们更深入地看看我们在前一章中看到的具体例子。假设我们用以下代码导入 MNIST 数据集:

(X_train, y_train), (X_test, y_test) = mnist.load_data()

在前一章中,我们用

X_train = X_train.reshape(X_train.shape[0], 1, 28, 28).astype('float32')

您会注意到,我们在 28 的 xy 维度之前添加了一个维度 1。1 是图像中的通道数:因为它是灰度图像,所以只有一个通道。但是我们也可以在 28 的 xy 尺寸之后增加通道的数量。这是我们的选择。我们可以用我们在第三章中讨论的代码告诉 Keras 采用哪个维度:

K.set_image_dim_ordering('th')

这一行很重要,因为 Keras 需要知道哪一个是信道维度,以便能够为卷积运算提取正确的信道维度。记住,对于内核,我们只指定了 xy 维度,所以 Keras 需要自己找到第三维:在这个例子中是 1。你会记得,'th'的值会期望通道尺寸在 xy 尺寸之前,而'tf'的值会期望通道尺寸是最后一个。所以,这只是一个保持一致的问题。您用上面的代码告诉 Keras,通道维度在哪里,然后相应地修改您的数据。让我们考虑几个额外的例子来使这个概念更加清晰。

让我们假设当使用 MNIST 图像作为输入时,我们考虑具有set_image_dim_ordering('th')(我们将忽略观察数量的维度 m )的以下网络:

Input tensors shape: 1×28×28
Convolutional Layer 1 with 32 kernels, each 5×5: output shape 32×24×24
Convolutional Layer 2 with 16 kernels, each 3×3: output shape 16×22×22

第二卷积层中的核将具有 32 × 3 × 3 的维度。来自第一卷积层(32)的信道数量在确定第二卷积层的输出维度中不起作用,因为我们对该维度求和。事实上,如果我们将第一层中的内核数量更改为 128,我们会得到以下维度:

Input tensors shape: 1×28×28
Convolutional Layer 1 with 32 kernels, each 5×5: output shape 128×24×24
Convolutional Layer 2 with 16 kernels, each 3×3: output shape 16×22×22

如您所见,第二层的输出尺寸没有任何变化。

注意

Keras 在创建过滤器时会自动推断通道尺寸,因此您需要使用set_image_dim_ordering()告诉 Keras 哪个是正确的尺寸,然后相应地调整您的数据。

为什么 1 × 1 卷积会降低维数

在这一章中,我们将研究初始网络,我们将使用 1 × 1 核,理由是这样可以降低维数。乍一看,这似乎有悖常理,但您需要记住上一节的讨论,即过滤器总是有第三维的。考虑以下一组层:

Input tensors shape: 1 × 28 × 28
Convolutional Layer 1 with 32 kernels, each 5 × 5: output shape 128 × 24 × 24
Convolutional Layer 2 with 16 kernels, each 1 × 1: output shape 16 × 24 × 24

注意具有 1 × 1 核的层是如何降低前一层的维度的。它将尺寸从 128 × 24 × 24 更改为 16 × 24 × 24。1 × 1 内核不会改变张量的 xy 维度,但会改变通道维度。这就是为什么,如果你阅读关于盗梦空间网络的博客或书籍,你会读到这些核被用来减少张量的维数。

核 1 × 1 不改变张量的 xy 维度,但会改变通道维度。这就是为什么它们经常被用来降低流经网络的张量的维数。

初始网络的历史和基础

初始网络最初是在 Szegedy 等人的一篇著名论文中提出的,这篇论文的标题是用卷积深入1 我们将详细讨论的这种新架构是在不增加计算预算的情况下,努力在图像识别任务中获得更好结果的结果。 2 添加越来越多的层将创建具有越来越多参数的模型,这将越来越困难并且训练缓慢。此外,作者希望找到一些方法,可以用在功能可能不如大型数据中心中使用的机器上。正如他们在论文中所述,他们的模型被设计为保持“推理时 15 亿乘加的计算预算”。重要的是,推断是廉价的,因为这样就可以在功能不那么强大的设备上进行;比如在手机上。

请注意,本章的目标不是分析关于初始网络的整篇原始论文,而是解释已经使用的新构建模块和技术,并向您展示如何在您的项目中使用它们。为了开发初始网络,我们将需要开始使用功能性 Keras APIs,使用多个损失函数,并使用并行而非顺序评估的层对数据集执行操作。我们也不会查看该架构的所有变体,因为这只会要求我们列出一些论文的结果,而不会给读者带来任何额外的价值(阅读原始论文会更好)。如果你有兴趣,我能给你最好的建议就是下载下来,研究一下原论文。你会在那里找到很多有趣的信息。但在本章结束时,你将拥有真正理解这些新网络的工具,并能够用 Keras 开发一个。

让我们回到“经典的”CNN。通常,这些都有一个标准的结构:堆叠的卷积层(当然有池),后面是一组密集层。很容易通过增加层数、内核数量或大小来获得更好的结果。这导致过拟合问题,因此需要大量使用正则化技术(如 dropout)来解决这个问题。更大的尺寸(在层数和内核尺寸和数量方面)当然意味着更大数量的参数,因此需要越来越高的计算资源。总而言之,“经典”CNN 的一些主要问题如下:

  • 获得正确的内核大小非常困难。每个图像都不一样。一般来说,较大的内核适合于全球分布的信息,较小的内核适合于本地分布的信息。

  • 深度 CNN 容易过度拟合。

  • 具有许多参数的网络的训练和推断是计算密集型的。

初始模块:天真的版本

为了克服这些困难,Szegedy 和论文合著者的主要思想是并行地执行与多尺寸核的卷积,以便能够同时检测不同尺寸的特征,而不是一层接一层地顺序添加卷积。据说这些类型的网络会变得更宽,而不是更深,而不是 ??。

比如我们可能同时并行的用 1 × 1,3 × 3,5 × 5 核做卷积,甚至 max pooling,而不是一个接一个的加几个卷积层。在图 4-1 中,你可以看到不同的卷积是如何在所谓的天真的初始模块中并行完成的。

img/470317_1_En_4_Fig1_HTML.jpg

图 4-1

并行完成不同内核大小的不同卷积。这是在初始网络中使用的基本模块,称为初始模块。

在图 4-1 的例子中,1 × 1 内核将查看非常定位的信息,而 5 × 5 内核将能够发现更多的全局特征。在下一节中,我们将看看如何使用 Keras 来开发这一功能。

初始模块中的参数数量

让我们看看盗梦空间和经典 CNN 在参数数量上的差异。假设我们考虑图 4-1 中的例子。假设“前一图层”是包含 MNIST 数据集的输入图层。为了便于比较,我们将对所有层或卷积运算使用 32 个内核。在初始模块中,每个卷积运算的参数数量为

  • 1 × 1 卷积:64 个参数 3 个

  • 3 × 3 卷积:320 个参数

  • 5 × 5 卷积:832 个参数

请记住,最大池操作没有可学习的参数。我们总共有 1216 个可学习的参数。现在,让我们假设我们创建了一个具有三个卷积层的网络,一个接一个。第一个具有 32 个 1 × 1 核,然后一个具有 32 个 3 × 3 核,最后一个具有 32 个 5 × 5 核。现在,各层中的参数总数将为(记住,例如,具有 32 个 3 × 3 内核的卷积层将具有 32 个 1 × 1 内核的卷积层的输出作为输入):

  • 1 × 1 卷积层:64 个参数

  • 具有 3 × 3 卷积的层:9248 个参数

  • 具有 5 × 5 卷积的层:25632 个参数

总共有 34944 个可学习参数。参数数量大约是初始版本的 30 倍。您可以很容易地看到,这种并行处理大大减少了模型必须学习的参数数量。

具有降维的初始模块

在天真的初始模块中,相对于经典 CNN,我们得到的可学习参数数量较少,但实际上我们可以做得更好。我们可以在适当的地方使用 1 × 1 卷积(主要是在高维卷积之前)来降低维数。这允许我们在不增加计算预算的情况下使用越来越多的这种模块。在图 4-2 中,你可以看到这样一个模块的样子。

img/470317_1_En_4_Fig2_HTML.jpg

图 4-2

降维的初始模块示例

看到我们在这个模块中有多少可学习的参数是有益的。为了了解降维真正有帮助的地方,让我们假设前一层是前一个操作的输出,并且它的输出具有 256、28、28 的维度。现在让我们比较一下原始模块和图 4-2 中所示的降维模块。

天真模块:

  • 8 核 1 × 1 卷积:2056 个参数 4 个

  • 8 核 3 × 3 卷积:18440 个参数

  • 8 核 5 × 5 卷积:51208 个参数

总共有 71704 个可学习参数。

降维模块:

  • 8 核 1 × 1 卷积:2056 个参数

  • 1 × 1 后跟 3 × 3 卷积:2640 个参数

  • 1 × 1 后跟 5 × 5 卷积:3664 个参数

  • 3 × 3 最大池,后跟 1 × 1 卷积:2056 个参数

总共有 10416 个可学习参数。对比一下可学习参数的数量,就能看出为什么说这个模块降维了。由于 1 × 1 卷积的智能放置,我们可以防止可学习参数的数量不受控制地激增。

一个初始网络简单地通过一个接一个地堆叠这些模块来构建。

多重成本函数:GoogLeNet

在图 4-3 中,你可以看到赢得imagenet挑战的 GoogLeNet 网络的主要结构。正如在开始引用的论文中所描述的,这个网络一个接一个地堆叠了几个初始模型。问题是,正如原始论文的作者很快发现的那样,中间层往往会“死亡”。这意味着他们在学习中不再扮演任何角色。为了让它们免于“死亡”,作者沿着网络引入了分类器,如图 4-3 所示。

网络的每个部分(图 4-3 中的部分 1、部分 2 和部分 3)将被训练为独立的分类器。这三个部分的训练不是独立发生的,而是同时发生的,与多任务学习中发生的非常相似(MTL)。

img/470317_1_En_4_Fig3_HTML.jpg

图 4-3

谷歌网络的高层架构

为了防止网络的中间部分变得不那么有效并逐渐消失,作者沿着网络引入了两个分类器,如图 4-3 中黄色方框所示。他们引入了两个中间损失函数,然后将总损失函数计算为辅助损失的加权和,有效地使用了通过以下公式评估的总损失:

Total Loss = Cost Function 1 + 0.3 * (Cost Function 2) + 0.3 * (Cost Function 3)

其中Cost Function 1是用第一部分评估的成本函数,Cost Function 2是用第二部分评估的,Cost Function 3是用第三部分评估的。测试表明,这是非常有效的,你会得到一个比简单地训练整个网络作为一个单一的分类器更好的结果。当然,辅助损失仅用于训练,而不用于推理。

作者开发了几个版本的初始网络,模块越来越复杂。如果你感兴趣,你应该阅读原文,因为它们很有教育意义。在 https://arxiv.org/pdf/1512.00567v3.pdf 可以找到作者的第二篇更复杂架构的论文。

Keras 中的初始模块示例

使用 Keras 的功能 API 使得构建一个初始模块变得非常容易。让我们看看必要的代码。出于空间原因,我们将不使用数据集构建完整的模型,因为这将占用太多空间,并且会分散对主要学习目标的注意力,即了解如何使用 Keras 构建一个具有并行评估而非顺序评估的图层的网络。

为了这个例子,让我们假设我们的训练数据集是CIFAR105 这是用图像做的,都是 32 × 32 带三个通道(图像是彩色的)。因此,首先我们需要定义网络的输入层:

from keras.layers import Input
input_img = Input(shape = (32, 32, 3))

然后我们简单地定义一层接一层:

from keras.layers import Conv2D, MaxPooling2D
tower_1 = Conv2D(64, (1,1), padding="same", activation="relu")(input_img)
tower_1 = Conv2D(64, (3,3), padding="same", activation='relu')(tower_1)
tower_2 = Conv2D(64, (1,1), padding="same", activation="relu")(input_img)
tower_2 = Conv2D(64, (5,5), padding="same", activation="relu")(tower_2)
tower_3 = MaxPooling2D((3,3), strides=(1,1), padding="same")(input_img)
tower_3 = Conv2D(64, (1,1), padding="same", activation="relu")(tower_3)

这段代码将构建如图 4-4 所示的模块。Keras 功能 API 易于使用:您可以将层定义为另一层的功能。每个函数返回适当维数的张量。好的一面是你不用担心尺寸问题;你可以简单地定义一层又一层。只要注意使用正确的输入即可。例如,用这一行:

tower_1 = Conv2D(64, (1,1), padding="same", activation="relu")(input_img)

您定义了一个名为tower_1的张量,它是在使用input_img张量和 64 个 1 × 1 核进行卷积运算后计算的。然后这一行:

tower_1 = Conv2D(64, (3,3), padding="same", activation="relu")(tower_1)

定义一个新的张量,它是通过 64 个 3 × 3 核与前一行的输出进行卷积而获得的。我们取输入张量,与 64 个 1 × 1 核进行卷积,然后再次与 64 个 3 × 3 核进行卷积。

img/470317_1_En_4_Fig4_HTML.jpg

图 4-4

从给定代码构建的初始模块

层的连接很容易:

from keras.layers import concatenate
from tensorflow.keras import optimizers
output = concatenate([tower_1, tower_2, tower_3], axis = 3)

现在让我们添加几层文章:

from keras.layers import Flatten, Dense
output = Flatten()(output)
out    = Dense(10, activation="softmax")(output)

然后我们最终创建模型:

from keras.models import Model
model = Model(inputs = input_img, outputs = out)

然后可以像往常一样编译和训练这个模型。用法的一个例子可能是

epochs = 50
model.compile(loss='categorical_crossentropy', optimizer=optimizers.Adam(), metrics=['accuracy'])
model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=epochs, batch_size=32)

假设训练数据集由数组(X_trainy_train)组成,验证数据集由(X_test, y_test)组成。

注意

在 inception 模块的所有卷积运算中,您必须使用padding='same'选项,因为卷积运算的所有输出必须具有相同的维数。

本节简要介绍了如何使用 Keras 的功能 API 开发更复杂的网络架构。现在你应该对盗梦空间网络的工作原理和基本构件有了基本的了解。

题外话:喀拉斯的风俗损失

有时,在 Keras 中开发自定义损失是很有用的。来自官方的 Keras 文档( https://keras.io/losses/ ):

您可以传递现有损失函数的名称,也可以传递 TensorFlow/Theano 符号函数,该函数为每个数据点返回一个标量,并采用以下两个参数:

y_true :真标签。tensorlow/theano tensor。

y_pred :预言。与 y_true 形状相同的 TensorFlow/Theano 张量。

假设我们想要定义一个损失来计算预测的平均值。我们需要写这个

import keras.backend as K
def mean_predictions(y_true, y_pred):
    return K.mean(y_pred)

然后我们可以简单地在编译调用中使用它,如下所示:

model.compile(optimizer='rmsprop',
              loss=mean_predictions,
              metrics=['accuracy'])

尽管这与其说是损失,不如说是有意义的。现在这开始变得有趣了,损失函数可以仅使用特定层的中间结果来评估。但要做到这一点,我们需要使用一个小技巧。因为根据官方文档,该函数只能接受真实的标签和预测作为输入。为此,我们需要创建一个函数,返回一个只接受真实标签和预测的函数。看起来很复杂。让我们看一个例子来理解它。假设我们有这个模型:

inputs = Input(shape=(512,))
x1 = Dense(128, activation=sigmoid)(inputs)
x2 = Dense(64, activation=sigmoid)(x1)
predictions = Dense(10, activation="softmax")(x2)
model = Model(inputs=inputs, outputs=predictions)

我们可以用这个代码 6 定义一个依赖于x1的损失函数(损失在做什么无关):

def custom_loss(layer):
    def loss(y_true,y_pred):
        return K.mean(K.square(y_pred - y_true) + K.square(layer), axis=-1)
    return loss

那么我们可以像以前一样简单地使用损失函数:

model.compile(optimizer='adam',
              loss=custom_loss(x1),
              metrics=['accuracy'])

这是一种开发和使用定制损耗的简单方法。如初始网络中所述,有时能够训练具有多个损失的模型也是有用的。Keras 已经准备好了。定义损失函数后,可以使用以下语法

model.compile(loss = [loss1,loss2], loss_weights = [l1,l2], ...)

Keras 将用作损失函数

l1*loss1+l2*loss2

考虑到每个损失只会影响输入和损失函数之间路径上的权重。在图 4-5 中,你可以看到一个分成不同部分的网络:ABC。使用B的输出和Closs2计算loss1。因此,loss1只会影响AB中的权重,而loss2会影响ABC中的权重,如图 4-5 所示。

img/470317_1_En_4_Fig5_HTML.jpg

图 4-5

多个损失函数对不同网络部分影响的示意图

顺便提一下,这种技术在所谓的多任务学习 (MTL)中被大量使用。 7

如何使用预先训练的网络

Keras 提供预先训练的深度学习模型供您使用。这些被称为应用的模型可以用来预测新数据。这些模型已经在大数据集上进行了训练,因此不需要大数据集或长时间的训练。您可以在 https://keras.io/applications/ 的官方文档中找到所有申请信息。在撰写本文时,有 20 种型号可用,每一种都是以下型号之一的变体:

  • Xception

  • VGG16

  • VGG19

  • 瑞斯网

  • ResNetV2

  • ResNeXt

  • 不规则 3

  • InceptionResNetV2

  • MobileNet(移动网络)

  • MobileNetV2

  • DenseNEt

  • 纳西网

让我们看一个例子,同时,让我们讨论函数中使用的不同参数。前期准备好的型号都在keras.applications包里。每个型号都有自己的包装。比如 ResNet50 在keras.applications.resnet50里。假设我们有一个想要分类的图像。我们可以使用 VGG16 网络,这是一个在图像识别方面非常成功的著名网络。我们可以从下面的代码开始

import tensorflow as tf
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras.preprocessing import image
from tensorflow.keras.applications.vgg16 import preprocess_input , decode_predictions

import numpy as np

然后我们可以简单地用一行代码加载模型

model = VGG16(weights='imagenet')

weights参数非常重要。如果权重为None,则权重被随机初始化。这意味着你得到了 VGG16 架构,你可以自己训练它。但是要知道,它大约有 1.38 亿个参数,所以你需要一个非常大的训练数据集和足够的耐心(以及非常强大的硬件)。如果您使用值imagenet,权重是通过使用imagenet数据集训练网络获得的。 8 如果你想要一个预先训练好的网络,你应该使用weights = 'imagenet'

如果您在 Mac 上收到关于证书的错误信息,有一个简单的解决方案。上面的命令将尝试通过 SSL 下载权重,如果您刚刚从python.org安装了 Python,那么安装的证书将无法在您的机器上运行。只需打开一个 Finder 窗口,导航到Applications/Python 3.7(或者你已经安装的 Python 版本),双击Install Certificates.command。将会打开一个终端窗口,并运行一个脚本。之后,VGG16()调用将正常工作,不会出现错误消息。

之后,我们需要告诉 Keras 图像在哪里(假设您将它放在 Jupyter 笔记本所在的文件夹中)并加载它:

img_path = 'elephant.jpg'
img = image.load_img(img_path, target_size = (224, 224))

你可以在 GitHub 库的第四章的文件夹中找到这个图片。之后我们需要

x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)

首先,将图像转换为数组,然后需要扩展它的维度。意思如下:该模型处理成批图像,这意味着它将期望输入具有四个轴的张量(成批图像中的索引、沿 x 方向的分辨率、沿 y 方向的分辨率通道数量)。但是我们的图像只有三个维度,水平和垂直分辨率以及通道的数量(在我们的例子中是三个,用于 RGB 通道)。我们需要为样本维度添加一个维度。更具体地说,我们的图像有维度(224,244,3),但模型期望一个维度(1,224,224,3)的张量,所以我们需要添加第一个维度。

这可以用 numpy 函数expand_dims()来完成,它只是在张量中插入一个新的轴。 9 作为最后一步,您需要预处理输入图像,因为每个模型期望与preprocess_input(x)调用略有不同的东西(在+1 和-1 之间,或者在 0 和 1 之间,等等)。

现在,我们准备让模型预测图像的类别,如下所示:

preds = model.predict(x)

要获得预测的前三类,我们可以使用decode_predictions()函数。

print('Predicted:', decode_predictions(preds, top=3)[0])

它将产生(我们的图像)以下预测:

Predicted: [('n02504013', 'Indian_elephant', 0.7278206), ('n02504458', 'African_elephant', 0.14308284), ('n01871265', 'tusker', 0.12798567)]

decode_predictions()(class_name, class_description, score).的形式返回元组。第一个隐含的字符串是内部类名,第二个是描述(我们感兴趣的),最后一个是概率。根据 VGG16 网络,我们的图像似乎有 72.8%的可能性是印度大象。我不是大象方面的专家,但我会相信这个模型。要使用不同的预训练网络(例如 ResNet50),您需要更改以下导入:

from keras.applications.resnet50 import ResNet50
from keras.applications.resnet50 import preprocess_input, decode_predictions

你定义模型的方式:

model = ResNet50(weights='imagenet')

代码的其余部分保持不变。

迁移学习:导论

迁移学习是一种技术,在这种技术中,为解决特定问题而训练的模型被重新用于与第一个问题相关的新挑战。假设我们有一个多层网络。通常在图像识别中,第一层将学习检测一般特征,而最后一层将能够检测更具体的特征。 11 记住,在一个分类问题中,最后一层将有 N 个 softmax 神经元(假设我们正在分类 N 个类),因此必须学会针对你的问题非常具体。你可以通过下面的步骤直观地理解迁移学习,这里我们介绍一些我们将在接下来的章节中使用的符号。假设我们有一个有 n 层L 层 的网络。

  1. 我们在与我们的问题相关的大数据集(称为基础数据集)上训练一个基础网络(或者得到一个预训练的模型)。例如,如果我们想要对狗的图像进行分类,我们可以在这个步骤中在imagenet数据集上训练一个模型(因为我们基本上想要对图像进行分类)。重要的是,在这一步,数据集有足够的数据,并且任务与我们想要解决的问题相关。让一个网络接受语音识别训练将不会擅长狗的图像分类。这个网络可能不适合你的特定问题。

** 我们得到一个新的数据集,我们称之为目标数据集(例如,狗的品种图像),这将是我们新的训练数据集。通常,该数据集将比步骤 1 中使用的数据集小得多。

 *   然后你训练一个新的网络,在*目标数据集*上叫做*目标网络***。目标网络通常会有相同的第一个 *n* <sub>*k*</sub> (与*n*<sub>*k*</sub><*n*<sub>*L*</sub>)层我们的基础网络。前几层的可学习参数(假设 1 到 *n* <sub>*k*</sub> ,带*n*<sub>*k*</sub><*n*<sub>*L*</sub>)继承自步骤 1 中训练的基网络,在目标网络的训练过程中不改变。仅训练最后的和新的层(在我们的例子中从层 *n* <sub>*K*</sub> 到 *n* <sub>*L*</sub> )。其想法是,从 1 到 *n* <sub>*k*</sub> (来自基础网络)的层将在步骤 1 中学习足够的特征来区分狗和其他动物,并且 *n* <sub>*k*</sub> 到 *n* <sub>*L*</sub> (在目标网络中)的层将学习所需的特征有时,您甚至可以使用从基础网络继承的权重作为权重的初始值来训练整个目标网络,尽管这需要更强大的硬件。*** 

***### 注意

如果目标数据集很小,最佳策略是冻结从基础网络继承的图层,因为否则很容易使小数据集过拟合。

这背后的想法是,你希望在步骤 1 中,基本网络已经学会足够好地从图像中提取一般特征,因此你希望使用这种学到的知识,并避免再次学习的需要。但是,为了更好地进行预测,您需要针对具体情况对网络的预测进行微调,优化目标网络提取与问题相关的特定特征(通常发生在网络的最后一层)的方式。

换句话说,你可以这样想。要识别狗的品种,你必须遵循以下步骤:

  1. 你看着一张图片,决定它是不是一只狗。

  2. 如果你在观察一只狗,你把它分成几大类(例如,梗)。

  3. 之后,你把它们分成子类(例如,威尔士梗或西藏梗)。

迁移学习基于这样的想法,即步骤 1 和可能的步骤 2 可以从来自基本网络的大量通用图像(例如从imagenet数据集)中学习,并且步骤 3 可以在步骤 1 和步骤 2 中所学内容的帮助下通过小得多的数据集学习。

当目标数据集远小于基本数据集时,这是一个非常强大的工具,有助于避免训练数据集过拟合。

这种方法在用于预训练模型时非常有用。例如,使用在imagenet上训练的 VGG16 网络,然后仅重新训练最后几层通常是解决特定图像识别问题的极其有效的方式。您可以免费获得许多功能检测功能。请记住,在imagenet网络上训练这样的网络需要花费几千个 GPU 小时。对于没有必要的硬件和技术的研究人员来说,这通常是不可能的。在下一节中,我们将研究如何做到这一点。有了 Keras,这真的很容易,它将允许您解决图像分类问题的准确性,否则是不可能的。在图 4-6 中,你可以看到迁移学习过程的示意图。

img/470317_1_En_4_Fig6_HTML.jpg

图 4-6

迁移学习过程的示意图

狗和猫的问题

了解迁移学习在实践中如何工作的最好方法是在实践中尝试。我们的目标是能够尽可能地对狗和猫的图像进行分类,尽可能地用最少的努力(在计算资源上)。为了做到这一点,我们将使用狗和猫的图像数据集,你可以在 https://www.kaggle.com/c/dogs-vs-cats 的 Kaggle 上找到这些图像。警告:下载差不多 800MB。在图 4-7 中,你可以看到一些我们需要分类的图像。

img/470317_1_En_4_Fig7_HTML.jpg

图 4-7

包含在狗对猫数据集中的图像的随机样本

迁移学习的经典方法

解决这个问题的简单方法是创建一个 CNN 模型,并用图像训练它。首先,我们需要加载图像并调整它们的大小,以确保它们都具有相同的分辨率。如果检查数据集中的图像,您会注意到每个图像都有不同的分辨率。要做到这一点,让我们调整所有的图像到(150,150)像素。在 Python 中,我们会这样使用:

import glob
import numpy as np
import os

img_res = (150, 150)

train_files = glob.glob('training_data/*')
train_imgs = [img_to_array(load_img(img, target_size=img_res)) for img in train_files]
train_imgs = np.array(train_imgs)
train_labels = [fn.split('/')[1].split('.')[0].strip() for fn in train_files]

validation_files = glob.glob('validation_data/*')
validation_imgs = [img_to_array(load_img(img, target_size=img_res)) for img in validation_files]
validation_imgs = np.array(validation_imgs)
validation_labels = [fn.split('/')[1].split('.')[0].strip() for fn in validation_files]

假设我们在名为training_data的文件夹中有 3000 张训练图像,在名为validation_data的文件夹中有 1000 张验证图像,train_imgsvalidation_imgs的形状如下:

(3000, 150, 150, 3)
(1000, 150, 150, 3)

像往常一样,我们将需要正常化的图像。现在每个像素的值在 0 到 255 之间,并且是一个整数。首先,我们将数字转换为浮点型,然后除以 255 进行归一化,这样每个值现在都在 0 和 1 之间。

train_imgs_scaled = train_imgs.astype('float32')
validation_imgs_scaled  = validation_imgs.astype('float32')
train_imgs_scaled /= 255
validation_imgs_scaled /= 255

如果你检查train_labels,你会看到它们是字符串:'dog''cat'。我们需要将标签转换成整数,特别是 0 和 1。为此,我们可以使用名为LabelEncoder的 Keras 函数。

from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
le.fit(train_labels)
train_labels_enc = le.transform(train_labels)
validation_labels_enc = le.transform(validation_labels)

我们可以用以下代码检查标签:

print(train_labels[10:15], train_labels_enc[10:15])

这将给出:

['cat', 'dog', 'cat', 'cat', 'dog'] [0 1 0 0 1]

现在我们已经准备好构建我们的模型了。我们可以通过下面的代码轻松做到这一点:

from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout
from tensorflow.keras.models import Sequential
from tensorflow.keras import optimizers

model = Sequential()

model.add(Conv2D(16, kernel_size=(3, 3), activation="relu",
                 input_shape=input_shape))
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Conv2D(64, kernel_size=(3, 3), activation="relu"))
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Conv2D(128, kernel_size=(3, 3), activation="relu"))
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Flatten())
model.add(Dense(512, activation="relu"))
model.add(Dense(1, activation="sigmoid"))

model.compile(loss='binary_crossentropy',
              optimizer=optimizers.RMSprop(),
              metrics=['accuracy'])

这是一个小型网络,其结构如下:

Layer (type)                 Output Shape              Param #
==============================================================
conv2d_3 (Conv2D)            (None, 148, 148, 16)      448
______________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 74, 74, 16)        0
______________________________________________________________
conv2d_4 (Conv2D)            (None, 72, 72, 64)        9280
______________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 36, 36, 64)        0
______________________________________________________________
conv2d_5 (Conv2D)            (None, 34, 34, 128)       73856
______________________________________________________________
max_pooling2d_5 (MaxPooling2 (None, 17, 17, 128)       0
______________________________________________________________
flatten_1 (Flatten)          (None, 36992)             0
______________________________________________________________
dense_2 (Dense)              (None, 512)               18940416
______________________________________________________________
dense_3 (Dense)              (None, 1)                 513
==============================================================
Total params: 19,024,513
Trainable params: 19,024,513
Non-trainable params: 0
______________________________________________________________

在图 4-8 中,您可以看到网络的示意图,以便了解层序列。

img/470317_1_En_4_Fig8_HTML.jpg

图 4-8

网络的示意图,让您了解层序列

此时,我们可以使用以下内容训练网络:

batch_size = 30
num_classes = 2
epochs = 2
input_shape = (150, 150, 3)
model.fit(x=train_imgs_scaled, y=train_labels_enc,
                    validation_data=(validation_imgs_scaled, validation_labels_enc),
                    batch_size=batch_size,
                    epochs=epochs,
                    verbose=1)

通过两个时期,我们达到了大约 69%的验证准确率和 70%的训练准确率。不是一个好结果。让我们看看我们能否在短短两个时代内做得比这更好。在两个时期内这样做的原因仅仅是为了快速检查不同的可能性。对这样的网络进行多次训练只需几个小时。请注意,该模型过度拟合了训练数据。当训练更多的纪元时,这变得清晰可见,但这里的主要目标不是获得最佳模型,而是看看如何使用预训练的模型来获得更好的结果,所以我们将忽略这个问题。

现在我们来导入 VGG16 预训练网络。

from tensorflow.keras.applications import vgg16
from tensorflow.keras.models import Model
import tensorflow.keras as keras

base_model=vgg16.VGG16(include_top=False, weights="imagenet")

请注意,include_top=False参数删除了网络的最后三个完全连接的层。这样,我们可以将自己的层附加到基本网络中,代码如下:

from tensorflow.keras.layers import Dense,GlobalAveragePooling2D
x=base_model.output
x=GlobalAveragePooling2D()(x)
x=Dense(1024,activation='relu')(x)
preds=Dense(1,activation='softmax')(x)
model=Model(inputs=base_model.input,outputs=preds)

我们添加了一个 pooling 层,然后是一个有 1024 个神经元的Dense层,然后是一个有一个神经元的输出层,这个神经元有一个 softmax 激活函数,做二分类。我们可以使用以下内容检查结构:

model.summary()

输出很长,但是最后你会发现:

Total params: 15,242,050
Trainable params: 15,242,050
Non-trainable params: 0

目前所有的 22 层都是可训练的。为了能够真正进行迁移学习,我们需要冻结 VGG16 基础网络的所有层。为此,我们可以做到以下几点:

for layer in model.layers[:20]:
    layer.trainable=False
for layer in model.layers[20:]:
    layer.trainable=True

该代码将前 20 层设置为不可训练状态,后两层设置为可训练状态。然后我们可以如下编译我们的模型:

model.compile(optimizer='Adam',loss='sparse_categorical_crossentropy',metrics=['accuracy'])

注意,我们使用了loss='sparse_categorical_crossentropy'来使用标签,而不必对它们进行热编码。正如我们之前所做的,我们现在可以训练网络:

model.fit(x=train_imgs_scaled, y=train_labels_enc,
                    validation_data=(validation_imgs_scaled, validation_labels_enc),
                    batch_size=batch_size,
                    epochs=epochs,
                    verbose=1)

请注意,虽然我们只训练了网络的一部分,但这将比我们之前尝试的简单网络需要更多的时间。结果将是两个时期内惊人的 88%。比以前好得多的结果!您的输出应该如下所示:

Train on 3000 samples, validate on 1000 samples
Epoch 1/2
3000/3000 [==============================] - 283s 94ms/sample - loss: 0.3563 - acc: 0.8353 - val_loss: 0.2892 - val_acc: 0.8740
Epoch 2/2
3000/3000 [==============================] - 276s 92ms/sample - loss: 0.2913 - acc: 0.8730 - val_loss: 0.2699 - val_acc: 0.8820

这要归功于预先训练好的第一层,这为我们节省了很多工作。

迁移学习实验

如果我们想为目标网络尝试不同的体系结构,并且想再增加几层并重试,该怎么办?前一种方法有一个小小的缺点:我们需要在每次事件中训练整个网络,尽管只需要训练最后几层。从上一节可以看出,一个时期大约需要 4.5 分钟。我们能更有效率吗?事实证明我们可以。

考虑图 4-9 中描述的配置。

img/470317_1_En_4_Fig9_HTML.jpg

图 4-9

实践中一种更灵活的迁移学习方式的示意图

我们的想法是生成一个新的数据集,我们称之为带有冻结图层的特征数据集 。由于它们不会因训练而改变,这些层将总是生成相同的输出。我们可以使用这个特征数据集作为一个小得多的网络(我们称之为目标子网)的新输入,该网络仅由我们在上一节中添加到基础层的新层构成。我们只需要训练几层,这样会快很多。生成特征数据集将需要一些时间,但这必须只进行一次。此时,您可以为目标子网测试不同的架构,并为您的问题找到最佳配置。让我们看看如何在 Keras 做到这一点。基础数据集准备与之前相同,因此我们不再重复。

让我们像以前一样导入 VGG16 预训练网络:

from tensorflow.keras.applications import vgg16
from tensorflow.keras.models import Model
import tensorflow.keras as keras

vgg = vgg16.VGG16(include_top=False, weights="imagenet",
                                     input_shape=input_shape)

output = vgg.layers[-1].output
output = keras.layers.Flatten()(output)
vgg_model = Model(vgg.input, output)

vgg_model.trainable = False
for layer in vgg_model.layers:
    layer.trainable = False

其中input_shape(150, 150, 3)

我们可以简单地用几行代码生成features数据集(使用predict功能):

def get_ features(model, input_imgs):
    features = model.predict(input_imgs, verbose=0)
    return features

train_features_vgg = get_features(vgg_model, train_imgs_scaled)
validation_features_vgg = get_features(vgg_model, validation_imgs_scaled)

请注意,在现代笔记本电脑上,这将需要几分钟时间。在现代的 MacBook Pro 上,这将需要 40 分钟的 CPU 时间,这意味着如果你有更多的核心/线程,它将占用其中的一小部分。在我的笔记本电脑上,只需要 4 分钟。请记住,由于我们使用了参数include_top = False,网络末端的三个dense层已经被移除。train_features_vgg将只包含基本网络最后一层的输出,而没有最后三个dense层。此时,我们可以简单地构建我们的目标子网:

from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout, InputLayer
from tensorflow.keras.models import Sequential
from tensorflow.keras import optimizers

input_shape = vgg_model.output_shape[1]

model = Sequential()
model.add(InputLayer(input_shape=(input_shape,)))
model.add(Dense(512, activation="relu", input_dim=input_shape))
model.add(Dropout(0.3))
model.add(Dense(512, activation="relu"))
model.add(Dropout(0.3))
model.add(Dense(1, activation="sigmoid"))

model.compile(loss='binary_crossentropy',
              optimizer=optimizers.Adam(lr =1e-4),
              metrics=['accuracy'])

model.summary()

训练这个网络会比以前快很多。您将在几秒钟内获得 90%的准确率(请记住,这次您已经创建了一个新的训练数据集)。但是现在你可以改变这个网络,测试不同的架构会快得多。这一次,一个历元只需要 6 秒钟,而前一个例子需要 4.5 分钟。这个方法比前一个效率高得多。我们将培训分为两个阶段:

  1. 创建特征数据集。只做过一次。(在我们的示例中,这需要大约四分钟。)

  2. 使用特征数据集作为输入,将新图层训练为独立网络。(每个时期需要 6 秒钟。)

如果我们想训练我们的网络 100 个纪元,用这种方法我们需要 14 分钟。使用上一节描述的方法,我们将需要 7.5 小时!缺点是您需要为每个想要使用的数据集创建新的特征数据集。在我们的例子中,我们需要为训练和验证数据集这样做。

Footnotes 1

原文可以在 arXiv 档案上通过以下链接获得: http://toe.lt/4

2

通过计算预算,我们可以确定执行特定计算(例如,训练网络)所需的时间和硬件资源。

3

记住在这种情况下,我们有一个权重和一个偏差。

4

记住在这种情况下,我们有一个权重和一个偏差。

5

您可以在 https://www.cs.toronto.edu/~kriz/cifar.html 找到数据集的所有信息。

6

代码的灵感来自 http://toe.lt/7

7

你可以在 https://en.wikipedia.org/wiki/Multi-task_learning 找到更多信息

8

http://www.image-net.org

9

您可以在 http://toe.lt/5 查看该功能的官方文档。

10

这个术语已经被洋辛基用在了 https://arxiv.org/abs/1411.1792 中。

11

你可以在 https://arxiv.org/abs/1411.1792 找到 Yosinki 等人关于这个主题的一篇非常有趣的论文。

***

五、成本函数和风格迁移

在这一章中,我们将更深入地研究成本函数在神经网络模型中的作用。特别是,我们将讨论 MSE(均方误差)和交叉熵,并讨论它们的来源和解释。我们将着眼于为什么我们可以使用它们来解决问题,MSE 如何在统计意义上解释,以及交叉熵如何与信息论相关。然后,给你一个更高级的特殊损失函数的例子,我们将学习如何进行神经风格迁移,这里我们将讨论一个神经网络以著名画家的风格绘画。

神经网络模型的组件

至此,您已经看到并开发了几个试图解决不同类型问题的模型。你现在应该知道,在所有的神经网络模型中,(至少)有三个主要构件:

  • 网络架构(层数、层类型、激活功能等。)

  • 损失函数(MSE,交叉熵等。)

  • 优化器

优化器通常不是特定于问题的。例如,为了解决回归或分类问题,您需要选择不同的体系结构和损失函数,但是在这两种情况下您可以使用相同的优化器。在回归中,您可以使用前馈网络和 MSE 作为损失函数。在分类中,你可以选择卷积神经网络和交叉熵损失函数。但是在这两种情况下,您都可以使用 Adam 优化器。在决定网络可以学习什么方面起最大作用的组件是损失函数。改变它,你就会改变你的网络能够预测和学习的东西。

培训被视为一个优化问题

让我们试着更详细地理解为什么会这样。从纯理论的角度来看,训练一个网络无非就是解决一个真正复杂的优化问题。连续优化问题的标准公式是寻找给定函数的最小值

$$ \underset{x}{\min }f(x) $$

受制于两种约束类型

$$ {\displaystyle \begin{array}{c}{g}_i(x)\le 0,\kern1em i=1,\dots, m\ {}{p}_j(x)=0,\kern1em j=1,\dots, n\end{array}} $$

其中f:n是我们要最小化的连续函数,gI(x)≤0 表示不等式约束,pj(x)= 0 表示等式约束, m 当然,没有约束也有可能出现问题。但是这和神经网络有什么关系呢?可以得出以下相似之处:

  • 函数 f ( x )是我们在建立神经网络模型时选择的损失函数。

  • 输入 x n 是我们网络的权值(可学习参数)。请记住,我们可能选择的任何损失函数总是网络输出的函数(我们用$$ \hat{y} $$表示),并且输出总是权重 W (网络的可学习参数)的函数。

当我们训练一个网络时,我们实际上是在解决一个优化问题,一个我们想要最小化关于权重的损失函数的问题。我们隐式地拥有约束,尽管我们通常不会显式地声明它们。例如,我们可能有这样的约束,即我们希望一次观察所需的推断时间少于 10ms。在这种情况下,我们将有 n = 0(没有等式约束), m = 1(一个不等式约束),其中 g 1 是推理运行时间。引用维基百科 1 :

损失函数或成本函数是将一个事件或一个或多个变量的值映射到一个实数上的函数,该实数直观地表示与事件相关的一些“成本”

通常,损失函数衡量模型对数据的理解程度。让我们来看几个简单的例子,这样你就可以在一个具体的案例中理解网络训练的这个公式。

一个具体的例子:线性回归

如你所知,如果你选择身份函数 2 作为激活函数,你可以用一个只有一个神经元的网络进行线性回归。我们用x[I]ni = 1、…, m 来表示观察值集合,其中 m 是我们拥有的观察值的数量。神经元(以及网络)将会有输出

$$ {\hat{y}}^{\left[i\right]}=\sum \limits_{k=1}n{w}_k{x}_k{\left[i\right]}+b $$

这里我们用 w = ( w 1 ,… w n )来表示权重。我们可以选择损失函数作为均方误差(MSE):

$$ J\left(w,b\right)=\frac{1}{m}\sum \limits_{k=1}m{\left({\hat{y}}{\left[i\right]}-{y}^{\left[i\right]}\right)}² $$

其中 y [ i ] 是我们要为第 i th 观察预测的目标变量。很容易看出,我们定义的损失函数是权重和偏差的函数。事实上,我们有

$$ J\left(w,b\right)=\frac{1}{m}\sum \limits_{i=1}m{\left({\hat{y}}{\left[i\right]}-{y}^{\left[i\right]}\right)}²=\frac{1}{m}\sum \limits_{i=1}^m{\left(\sum \limits_{k=1}n{w}_k{x}_k{\left[i\right]}+b-{y}^{\left[i\right]}\right)}² $$

像我们通常使用(例如)梯度下降算法那样训练该网络无非是解决一个无约束优化问题,其中我们有(使用我们在开始时使用的符号):

$$ f:= J $$

成本函数

数学符号

让我们定义一些我们将在下一节中使用的符号。我们将使用

$$ {\hat{y}}^{\left[\mathrm{i}\right]}\in {\mathbb{R}}^k $$是对 i * th * 观察的网络输出。

$$ \hat{Y}\in {\mathbb{R}}^{m\times k} $$是包含所有观测值的网络输出的张量。3

$$ {x}^{\left[i\right]}\in {\mathbb{R}}^{n_x\times {n}_y\times {n}_c} $$代表 i * th 观察输入特性(一般来说,对于图像我们会有 n c 通道,以及分辨率为nx×ny*)。

$$ X\in {\mathbb{R}}^{m\times {n}_x\times {n}_y\times {n}_c} $$是包含所有输入观测值的张量。

w 是网络中使用的所有可学习参数的集合(包括偏差)。

m 是观察次数。

n c 是图像通道的数量(对于 RGB 图像为 3)。

n x 是输入图像的水平分辨率。

ny是输入图像的垂直分辨率。

J 是成本函数。

通常,我们将所谓的成本(或损失)函数 J 一般定义如下:

$$ J\left(X,\hat{Y}\left(\mathrm{W}\right)\right) $$

除了网络架构之外,该功能将定义我们的神经网络模型能够解决什么样的问题。注意这个函数是如何

  • 取决于网络架构,因为它取决于网络输出$$ \hat{Y} $$(并因此取决于可学习的参数 W)

  • 取决于输入数据集,因为它取决于输入 X

这是寻找最佳权重时将使用的函数。在几乎所有的优化器中,权重都以某种形式使用$$ {\nabla}_{\mathrm{W}}J\left(X,\hat{Y}\left(\mathrm{W}\right)\right) $$来更新。

典型成本函数

正如我们在前面章节中看到的,在训练神经网络时,可以使用几个成本函数。在接下来的章节中,我们将详细介绍两个最常用的词汇,并试图理解它们的含义和来源。

均方误差

均方误差函数

$$ J\left(w,b\right)=\frac{1}{m}\sum \limits_{k=1}m{\left({\hat{y}}{\left[i\right]}-{y}^{\left[i\right]}\right)}² $$

可能是开发回归模型时最常用的成本函数。对于这个成本函数有几种解释,但是下面两种应该可以帮助你对它有一个直观和更正式的理解。

直观的解释

J 无非是预测值和实测值的平方差的平均值。所以基本上,它衡量的是预测值与期望值的差距。一个能够完美预测数据的完美模型($$ {\hat{y}}{\left[i\right]}={y}{\left[i\right]} $$代表所有的 i = 1,…, m )应该是 J = 0。一般来说,它保持最小的 J 预测越好。

注意

一般来说,它认为 MSE 越小,预测越好(因此,模型越好)。

最小化 MSE 意味着找到参数,使我们的网络输出尽可能接近我们的训练数据。请注意,您可以通过使用以下公式给出的 MAE(平均绝对误差)来获得类似的结果

$$ MAE=\frac{1}{m}\sum \limits_{k=1}m\left|{\hat{y}}{\left[i\right]}-{y}^{\left[i\right]}\right| $$

尽管通常不这样做。

MSE 作为矩生成函数的二阶矩

有一种更正式的方法来解释 MSE。让我们定义数量

$$ \Delta {Y}{\left[i\right]}={\hat{y}}{\left[i\right]}-{y}^{\left[i\right]} $$

让我们定义力矩生成函数

$$ {M}_{\Delta Y}(t):= E\left[{e}^{t\varDelta Y}\right] $$

这里我们有 t,我们用 E [ ]表示变量在所有观测中的期望值。我们将跳过关于期望值存在与否的讨论,取决于δY的特性,因为这超出了本书的范围。我们可以用泰勒级数展开法来展开eY(我们假设我们可以这样做):

$$ {e}^{t\varDelta \mathrm{Y}}=1+t\Delta Y+\frac{t²\varDelta {Y}²}{2!}+\dots $$

因此

$$ {M}_{\varDelta Y}(t):= E\left[{e}^{t\varDelta \mathrm{Y}}\right]=1+ tE\left[\Delta Y\right]+\frac{t²E\left[\varDelta {Y}²\right]}{2!}+\dots $$

E【δYn称为函数的 n thMδY(t)。你可以看到,这些时刻很容易解释(至少第一次):

  • E**δY:MδY(t)-δY 的一阶矩

** E[δY2:MδY(t)-就是我们定义的 MSE 函数**

**   *E*[*δY*<sup>3</sup>:M*<sub>*δY*</sub>(*t*)-*偏斜度* <sup>[5</sup>*

**   *E**δY*<sup>4</sup>:M*<sub>*δY*</sub>(*t*)-*峰度* <sup>[6</sup>**** 

**我们可以简单地把二阶矩写成观测值的平均值

$$ E\left[\varDelta {Y}²\right]:= \frac{1}{m}\sum \limits_{k=1}^m\Delta {Y^{\left[i\right]}}²=\frac{1}{m}\sum \limits_{k=1}m{\left({\hat{y}}{\left[i\right]}-{y}^{\left[i\right]}\right)}² $$

如果我们假设我们的模型用E【δY】= 0 来预测数据,那么E【δY2(因此 MSE)无非是我们的数据点分布的方差δY[I。在这种情况下,它只是测量我们的点在平均值(即零)周围的分布范围:完美的预测。记住,如果对于一个观测,我们有δY[I]= 0,这意味着我们有$$ {\hat{y}}{\left[i\right]}={y}{\left[i\right]} $$,意味着预测是完美的。只是为了给出正确的术语,如果E[δY]不为零,那么这些矩有时被称为非中心矩 。如果你正在处理非中心矩,你不能再直接把它们解释为统计量(方差)。

注意

如果你正在处理非中心矩,你不能再直接把它们解释为统计量(方差)。如果δY[I]的平均值为零,那么 MSE 就是我们预测的分布的方差。当然,值越小,预测就越准确。

交叉熵

有几种理解交叉熵损失函数的方法,但我认为最迷人的方法是从信息论开始讨论。在这一节中,我们将在更直观的基础上讨论一些基本概念,以给你足够的信息和理解,从而对交叉熵有一个非常有力的理解。

事件的自我信息或抑制

我们需要从自我信息的概念开始,或者说一个事件的极限。为了对它有一个直观的理解,请考虑以下几点:当一个事件发生一个不可能的结果时,我们把它与高层次的信息联系起来。当一个结果总是发生时,通常它没有太多的相关信息。换句话说,当不太可能的事件发生时,我们会更惊讶;因此,它也被称为一个结果的上限。我们如何用数学的形式来表达它呢?我们来考虑一个随机变量 Xn 可能结果 x 1x 2 ,…, x n 和概率质量函数7P(X)。让我们用I=P(xI)来表示事件xI发生的概率。在 0 和 1 之间的任何单调递减函数I(pI)都可以用来表示随机变量 X 的上界(或自身信息)。但是这个函数必须有一个重要的性质:如果事件是独立的,那么 I 应该满足**

$$ I\left({p}_i{p}_j\right)=I\left({p}_i\right)+I\left({p}_j\right) $$

*如果结果 ij 是独立的。人们马上想到一个具有这种特性的函数:对数。事实上,这是事实

$$ \ln \left({p}_i{p}_j\right)=\log {p}_i+\log {p}_j $$

为了让它单调递减,我们可以选择以下公式:

$$ I\left({p}_i\right)=-\log {p}_i $$

与事件 X 相关的 Suprisal

总的来说,我们有多少关于特定事件的信息?这是通过对 X 的所有可能结果的期望值来衡量的(我们将用 P 来表示这个集合)。数学上,我们可以把它写成

$$ H(X)={E}_P\left[I(X)\right]=\sum \limits_{i=1}^nP\left({x}_i\right)I\left({x}_i\right)=-\sum \limits_{i=1}^nP\left({x}_i\right){\log}_bP\left({x}_i\right) $$

H ( X )被称为香农熵,而 b 是算法的基础,通常被选为 2、10 或 e

交叉熵

现在让我们假设我们想要比较事件 X 的两种概率分布。我们来分析一下,当我们训练一个神经网络进行分类时,我们做了什么。请考虑以下几点:

  • 我们的例子给出了事件的“真实”或预期分布(真实标签)。他们的分布将是我们的 P。例如,我们的观测可能包含具有一定概率的猫类(假设这是类 1)P(x1,其中x1 是结果“这个图像中有一只猫”。我们有给定的概率质量函数, P

  • 我们训练的网络将会给我们一个不同的概率质量函数, Q ,因为预测将不会与训练数据完全相同。结局x1(“图像里有一只猫”)会以不同的概率发生, Q ( x 1 )。您应该记得,在构建分类网络时,我们对输出层使用了一个softmax激活函数来将输出解释为概率。你看到所有的事情突然变得更有意义了吗?

我们希望有一个尽可能反映给定标签的预测,这意味着我们希望有一个尽可能类似于 P 的概率质量函数 Q

为了比较两个概率质量函数(我们感兴趣的),我们可以简单地用实例得到的分布来计算我们的网络得到的自我信息的期望值。以更数学的形式

$$ H\left(Q,P\right)={E}_P\left[I(Q)\right]={E}_P\left[-{\log}_bQ\right]=-\sum \limits_{i=1}^nP\left({x}_i\right){\log}_bQ\left({x}_i\right) $$

如果你有信息论方面的经验, H ( QP )会给出两个概率质量函数 QP 相似性的度量。为了理解为什么,让我们考虑一个实际的例子。这将是一场公平的掷硬币游戏。 X 将有两种可能的结果:X1 将是硬币的头部,而X2 将是硬币的尾部。“真实的”概率质量函数当然是一个常数函数,其中P(x1)= 0.5,P(x2)= 0.5。现在让我们考虑另一种概率质量函数 Q i (为了说明的目的,我们将只考虑 9 个可能的值):

  • I= 1→q1(x1= 0.1,q

  • I= 2→q2(x??= 0.2,q**

  • I= 3→【q】()= 0.3,【q11】

** I= 4→q4(x1= 0.4,q

*   *I*= 5→q<sub>5</sub>(*x*<sub>1</sub>= 0.5,*q*

*   *I*= 6→q6(*x*??= 0.6,*q*

*   *I*= 7→q<sub>7</sub>(*x*<sub>1</sub>= 0.7,*q*

*   *I*= 8→q8(*x*1= 0.8,*q*

*   *I*= 9→q<sub>9</sub>(*x*<sub>1</sub>= 0.9,*q** 

我们来计算一下H(QIP )对于 i = 1,…5。对于 i = 6,..我们不需要计算 H 。。,9 既然函数是对称的,意思就是比如说那个H(Q4P)=H(Q6P )。在图 5-1 中可以看到H(QI*, P )的剧情。你可以看到当两个概率质量函数相同时,当 i = 5 时达到最大值。

img/470317_1_En_5_Fig1_HTML.jpg

图 5-1

H(Q i ,P)为 i = 1,…5。当两个概率质量函数完全相同时,对于 i = 5 获得最小值。

注意

交叉熵 H ( QP )是两个质量概率函数 QP 相似程度的度量。

二元分类的交叉熵

现在让我们考虑一个二元分类问题,看看交叉熵是如何工作的。假设我们的事件 X 是给定图像的两类分类。可能的结果只有两个:1 类或 2 类。为了说明的目的,让我们假设我们的图像属于类别 1。我们对于图像的“真实”概率质量函数将具有P(x1)= 1.0,P(x2)= 0。换句话说,由于我们知道真实值,我们的概率质量函数 P 只能是 0 或 1。

你会记得,在一个二元分类问题中,我们使用了以下内容

$$ \mathcal{L}\left({\hat{y}}{(j)},{y}{(j)}\right)=-\left({y}^{(j)}\ \log {\hat{y}}{(j)}+\left(1-{y}{(j)}\right)\log \left(1-{\hat{y}}^{(j)}\right)\right) $$

其中 y ( j ) 表示真实标签(0 表示类 1,1 表示类 2),而$$ {\hat{y}}^{(j)} $$是图像 j 属于类 2 的概率,或者换句话说,是假设值为 1 的网络输出的概率。我们将最小化的成本函数由所有观察值(或例子)的总和给出

$$ J\left(\boldsymbol{w},b\right)=\frac{1}{m}\sum \limits_{j=1}m\mathcal{L}\left({\hat{y}}{(j)},{y}^{(j)}\right) $$

使用上一节的符号,我们可以为图像 j 编写

$$ {p}_j\left({x}_1\right)=1-{y}^{(j)} $$

$$ {p}_j\left({x}_2\right)={y}^{(j)} $$

记住y??(j)只能是 0 或 1;所以我们只有两种可能:pj(x1)= 1,pj(x2)= 0 或者pj(x 我们也可以写网络的预测

$$ {q}_j\left({x}_1\right)=1-{\hat{y}}^{(j)} $$

$$ {q}_j\left({x}_2\right)={\hat{y}}^{(j)} $$

请记住:这个结果是由我们如何构建我们的网络(因为我们在输出层使用了softmax激活函数来获得概率)和我们如何编码我们的标签(0 和 1,以便它们可以被解释为概率)决定的。现在,让我们使用我们的神经网络符号来编写上一节中定义的交叉熵,但是对所有示例求和(请记住,我们希望获得所有事件的整个交叉熵,换句话说,所有图像的交叉熵):

$$ H\left(Q,P\right)=-\sum \limits_{j=1}^m\sum \limits_{i=1}²{p}_j\left({x}_i\right){\log}_b{q}_j\left({x}_i\right)=-\sum \limits_{j=1}m\left({y}{(j)}{\log}_b{\hat{y}}{(j)}+\left(1-{y}{(j)}\right){\log}_b\left(1-{\hat{y}}^{(j)}\right)\right) $$

所以基本上$$ \mathcal{L}\left({\hat{y}}{(i)},{y}{(i)}\right) $$只不过是在信息论中得到的交叉熵。

注意

直觉上,当我们最小化二元分类问题中的交叉熵时,我们最小化了当我们的预测与我们的期望不同时我们可能有的惊讶。

H ( QP )衡量我们的预测概率密度函数( Q )与我们的训练样本概率密度函数( P )的匹配程度。

注意

当我们使用交叉熵设计用于分类的网络,并且我们在最终层使用softmax激活函数来将输出解释为概率时,我们简单地构建了基于信息论的复杂分类系统。我们应该感谢香农 8 用神经网络进行分类。

成本函数:最后一句话

现在应该很清楚,成本函数决定了神经网络可以学习什么。改变它,网络就会学到完全不同的东西。毫不奇怪,要获得特殊的结果,比如艺术,只需要选择正确的架构和正确的成本函数。在本章的下一部分,我们将着眼于神经类型转移,选择正确的成本函数(在这个例子中,我们将看到多个)是实现非凡结果的关键,这一点将变得非常清楚。

神经类型转移

此时,您已经拥有了开始使用网络进行更高级技术的所有工具:使用预先训练的 CNN,从隐藏层提取信息,以及使用自定义成本函数。这开始成为高级材料,所以你需要很好地理解我们在前面章节中讨论的所有基础知识。如果有什么不清楚的地方,回头再研究一遍。

CNN 的一个有趣而好玩的应用是制作艺术品,神经风格迁移(NST)指的是一种操纵数字图像的技术,采用另一幅图像的外观或风格 9 。一个有趣的应用程序是拍摄一幅图像,让网络操纵它,使其采用著名画家的风格,比如梵高。使用深度学习的 NST 最早出现在 Gatys 等人 2015 年的一篇论文中 10 。这是一种新技术。Gatys 开发的方法使用预先训练的深度 CNN 来将图像的内容与风格分开。

这个想法是将一幅图像输入预先训练好的 VGG-19 11 CNN,在imagenet数据集上进行训练。作者假设图像的内容可以在网络中间层输出中找到(图像通过每层中的学习过滤器),而风格在于不同层输出的相关性(编码在格拉米矩阵中)。预先训练的网络可以很好地识别图像的内容,因此每一层学习的特征必须与图像的内容紧密相关,而不是与风格相关。事实上,一个擅长识别图像的健壮的 CNN 并不太在乎风格。直观地说,风格包含在图像空间上不同的滤波器响应是如何相关的。画家可能会使用宽或窄的笔触,可能会使用许多彼此接近的颜色或仅使用几种颜色,等等。请记住,在 CNN 中,每一层都只是图像过滤器的集合;因此,给定层的输出只是输入图像的不同过滤版本的集合 10。

另一种方式是,当你从远处看一幅图像时(你不太关心细节),内容被发现,而当你在更近的尺度上看图像时,风格被发现,这取决于图像的不同部分如何相互联系。Gatys 等人聪明地用数学方法简单地实现了这些想法。为了给你一个思路,请看图 5-2 。一个网络已经将原始图像(左上)处理成了右上角梵高画作的风格,以获得底部的图像。

img/470317_1_En_5_Fig2_HTML.jpg

图 5-2

NST 的一个例子。该方法将原始图像(左上)处理成右上的梵高绘画风格,以获得底部的图像。

NST 背后的数学

原始论文使用的是 VGG19 网络,Keras 提供给我们下载和使用。我们在这里用 x 表示的输入图像(我将尽可能使用原始符号)被编码在 CNN 的每一层中。带有Nl过滤器(有时也称为内核)的图层将具有 N l 特征地图作为输出。在该算法中,这些输出将在尺寸为 M l 的一维向量中展平,其中 M l 是当应用于输入图像时每个滤波器的输出的高度乘以宽度。层 l 的响应可以被编码到张量$$ {F}^l\in {\mathbb{R}}^{N_l\times {M}_l} $$中。让我们在这里暂停一下,试着用一个具体的例子来理解我们的意思。

假设我们使用彩色图像作为输入图像,每个图像的尺寸为 32 × 32。让我们考虑用代码创建的 CNN 中的第一个卷积层:

Conv2D(32, (3, 3), padding="same", activation="relu", input_shape=input_shape))

当然是哪里input_shape = (32,32,3)。图层的输出将具有以下尺寸

(None, 32, 32, 32)

其中当然None将假设所使用的观察值的数量。这是因为我们使用了参数padding = 'same'。在这种情况下,层 l = 1 的输出是 32 个特征图(或输入图像与 32 个过滤器卷积的结果),每个尺寸为 32 × 32。在这种情况下,我们将有 N l = 1 = 32 和Ml= 1= 32×32 = 1024。在计算格拉米矩阵之前,将展平每个 32 × 32 的特征图。您将在后面的代码中清楚地看到这是如何实现的。

我们姑且称原图 p 。这就是我们想要改变的形象。作为输出生成的图像被称为 x 。我们将用 P lF l 表示它们各自从图层 l 中得到的特征图。我们定义称为内容损失函数的平方误差损失如下:

$$ {\mathcal{L}}_{content}\left(p,x,l\right)=\frac{1}{2}\sum \limits_{i,j}{\left({F}_{ij}l-{P}_{ij}l\right)}² $$

在 Keras 中,我们将使用以下代码实现这一点:

content_loss = tf.add_n([tf.reduce_mean((content_outputs[name]-content_targets[name])**2)
                             for name in content_outputs.keys()])

其中content_outputs[]content_targets[]将分别包含应用于输入(content_outputs)和生成的图像(content_targets)时 VGG19 的特定层的输出(已经展平)。稍后我们将更详细地讨论它;如果你没有完全理解,暂时不要担心。你可能想知道为什么我们没有因子 1/2,但我们并不需要它,因为$$ {\mathcal{L}}_{content}\left(p,x,l\right) $$将乘以另一个因子,这将使 1/2 无用。

我们需要计算损失函数相对于图像的梯度。这是相当重要的一点。这意味着我们想要学习的参数是我们想要改变的图像的像素值。网络的参数是固定的,我们不需要改变它们。对于 Keras,我们需要使用以下形式的tape.gradient函数:

tape.gradient(loss, image)

我们需要将图像定义为一个 TensorFlowVariable(稍后会详细介绍)。如果你不熟悉tape.gradient的工作原理,我建议你去 https://www.tensorflow.org/tutorials/eager/automatic_differentiation 查阅官方文档。

注意

我们要学习的参数是我们要改变的图像的像素值,而不是网络的权重。

现在我们需要注意风格。为此,我们需要为样式定义一个损失函数。为此,我们需要定义 Gramian 矩阵Gl,它是图层 l 中展平后的特征图 ij 之间的内积。换句话说

$$ {G}_{ij}^l=\sum \limits_k{F}_{ik}l{F}_{kj}l $$

有了这个新定义的量,我们将定义一个样式损失函数$$ {\mathcal{L}}_{style}\left(a,x\right) $$,其中 a 是我们想要使用样式的图像

$$ {\mathcal{L}}_{style}\left(a,x\right)=\sum \limits_{l=1}⁵{w}_l{E}_l $$

在哪里

$$ {E}_l=\frac{1}{4{N}_l²{M}_l²}\sum \limits_{i,j}{\left({G}_{ij}l-{A}_{ij}l\right)}² $$

其中 w l 为原试卷中选取的权重,等于 1/5。在 Keras 中,我们将通过代码实现这种丢失(我们将在后面查看细节):

tf.add_n([tf.reduce_mean((style_outputs[name]-style_targets[name])**2)
                           for name in style_outputs.keys()])

style_outputsstyle_targets变量将包含 VGG19 网络五层的输出。在原始论文中,使用了以下五层:

l=1 - block1_conv1
l=2 - block2_conv1
l=3 - block3_conv1
l=4 - block4_conv1
l=5 - block5_conv1

这些是 VGG19 网络中每个模块的第一层。请记住,您可以通过以下代码从 VGG19 中获取图层名称:

vgg = tf.keras.applications.VGG19(include_top=False, weights="imagenet")

print()
for layer in vgg.layers:
  print(layer.name)

会得到这样的结果:

input_1
block1_conv1
block1_conv2
block1_pool
block2_conv1
block2_conv2
block2_pool
block3_conv1
block3_conv2
block3_conv3
block3_conv4
block3_pool
block4_conv1
block4_conv2
block4_conv3
block4_conv4
block4_pool
block5_conv1
block5_conv2
block5_conv3
block5_conv4
block5_pool

注意,我们没有密集层,因为我们使用了include_top=False。最后,我们将最小化下面的损失函数

$$ {\mathcal{L}}_{total}\left(p,x,a\right)=\alpha {\mathcal{L}}_{style}\left(a,x\right)+\beta \sum \limits_{l=1}⁵{\mathcal{L}}_{content}\left(p,x,l\right) $$

使用梯度下降(例如),相对于我们想要改变的图像。可以选择常数 αβ 来赋予样式或内容更大的权重。对于图 5-1 中的结果,我选择了 α = 1.0, β = 10 4 。其他典型值有α= 10—2β = 10 4

Keras 风格迁移的一个例子

我们将在此讨论的代码取自最初的 TensorFlow NST 教程,并针对本次讨论进行了极大的简化。为了简化讨论,我们将只讨论部分代码,因为整个代码相对较长。你可以在这本书的 GitHub 资源库的 Chapter 5 文件夹中找到完整的简化版。我建议你在启用 GPU 的情况下运行 Google Colab 中的代码,因为它的计算量相当大。给你一个概念,在我的笔记本电脑上,一个 epoch 大约需要 13 秒,而在谷歌 Colab 上,处理 512 × 512 像素的图像需要 0.5 秒。

为了确保您安装了最新的 TensorFlow 版本,您应该在笔记本的开头运行以下代码:

from __future__ import absolute_import, division, print_function, unicode_literals
!pip install tensorflow-gpu==2.0.0-alpha0
import tensorflow as tf

如果您在 Google Colab 上运行代码,您需要将想要处理的图像保存在 Google drive 上并挂载它。为此,您需要在硬盘上上传两个图像:

  • 一个风格形象:比如一幅名画。这是您想要从中获取样式的图像。

  • 内容图像:例如,您拍摄的风景或照片。这是您要修改的图像。

我在这里假设你已经把你的图片上传到了 Google drive 根目录下的一个名为data的文件夹中。你现在需要做的是在 Google Colab 中安装你的 Google drive 来访问这些图片。为此,您需要以下代码:

from google.colab import drive
drive.mount('/content/drive')

如果您运行这段代码,您需要转到一个特定的 URL(由 Google Colab 提供),在那里您将收到需要粘贴到笔记本中的代码。在 http://toe.lt/a 可以找到关于如何做到这一点的很好的概述。装载后,您将获得目录中的文件列表,如下所示:

!ls "/content/drive/My Drive/data"

我们可以定义我们将使用的图像的文件名

content_path = '/content/drive/My Drive/data/landscape.jpg'
style_path = '/content/drive/My Drive/data/vangogh_landscape.jpg'

当然,您需要将文件名改为您自己的文件名。但是如果您想尝试使用这些图片,您可以在 GitHub 存储库中找到我在这个例子中使用的图片。如果没有的话,您需要创建data目录,并将图像复制到那里。图像将通过load_img()功能加载。请注意,在开头的函数中,我们调整了图像的大小,使其最大尺寸等于 512(load_img()函数的完整代码可以在 GitHub 上找到)。这是一个可管理的大小,但是如果您想生成更好看的图像,您需要增加这个值。图 5-1 中的图像是用max_dim = 1024生成的。该函数开始于

def load_img(path_to_img):
  max_dim = 512
  img = tf.io.read_file(path_to_img)

因此,您更改了max_dim变量的值来处理更大的图像。现在,我们只需要选择一些层的输出,正如我们在上一节中所描述的。为此,我们将想要使用的层的名称放在两个列表中:

# Content layer where will pull our feature maps
content_layers = ['block5_conv2']

# Style layer we are interested in
style_layers = ['block1_conv1',
                'block2_conv1',
                'block3_conv1',
                'block4_conv1',
                'block5_conv1']

这样,我们可以使用名称选择正确的层。我们需要的是一个模型,从每一层获取输入并返回所有的特征地图。为此,我们使用以下代码

def vgg_layers(layer_names):
  vgg = tf.keras.applications.VGG19(include_top=False, weights="imagenet")
  vgg.trainable = False

  outputs = [vgg.get_layer(name).output for name in layer_names]

  model = tf.keras.Model([vgg.input], outputs)
  return model

此函数获取一个带有图层名称的列表作为输入,并使用以下代码行选择给定图层的网络层输出:

outputs = [vgg.get_layer(name).output for name in layer_names]

请注意,没有检查,所以如果你有一个错误的层名称,你不会得到你期望的结果。但是由于我们需要的层是固定的,所以不需要检查网络中是否存在这些名称。这条线

model = tf.keras.Model([vgg.input], outputs)

根据layer_names输入列表中的层数,创建一个有一个输入(vgg.input)和一个或多个输出的模型。

为了计算$$ {G}_{ij}^l $$(格拉米矩阵),我们使用这个函数

def gram_matrix(input_tensor):
  result = tf.linalg.einsum('bijc,bijd->bcd', input_tensor, input_tensor)
  input_shape = tf.shape(input_tensor)
  num_locations = tf.cast(input_shape[1]*input_shape[2], tf.float32)
  return result/(num_locations)

其中变量num_locations简单来说就是 M l 。现在有趣的部分来了:损失函数的定义。我们需要定义一个名为StyleContentModel的类,它将接受我们的模型,并在每次迭代中返回不同层的输出。该类有一个__init__部分,我们将在这里跳过(您可以在 Jupyter 笔记本中找到代码)。有趣的部分是call()功能:

def call(self, inputs):
    inputs = inputs*255.0
    preprocessed_input = tf.keras.applications.vgg19.preprocess_input(inputs)
    outputs = self.vgg(preprocessed_input)
    style_outputs, content_outputs = (outputs[:self.num_style_layers],
                                      outputs[self.num_style_layers:])

    style_outputs = [gram_matrix(style_output)
                     for style_output in style_outputs]

    content_dict = {content_name:value
                    for content_name, value
                    in zip(self.content_layers, content_outputs)}

    style_dict = {style_name:value
                  for style_name, value
                  in zip(self.style_layers, style_outputs)}

    return {'content':content_dict, 'style':style_dict}

该函数将返回一个包含两个元素的字典— content_dict包含内容层及其输出,而style_dict包含样式层及其输出。您可以使用此功能:

extractor = StyleContentModel(style_layers, content_layers)

然后:

style_targets = extractor(style_image)['style']
content_targets = extractor(content_image)['content']

这样,当应用于不同的图像时,我们可以得到不同层的输出。请记住,当应用于我们的梵高画作时,我们需要样式层的输出,但当应用于风景(或您的图像)图像时,我们需要内容层的输出。让我们将内容图像(风景或您的图像)保存在一个变量中,并定义一个函数(它将在后面 9 中有用),该函数将在 0 和 1 之间裁剪数组的值:

image = tf.Variable(content_image)
def clip_0_1(image):
  return tf.clip_by_value(image, clip_value_min=0.0, clip_value_max=1.0)

那么我们可以将两个变量 αβ 定义如下:

style_weight=1e-2
content_weight=1e4

现在我们有了定义损失函数所需的一切:

def style_content_loss(outputs):
    style_outputs = outputs['style']
    content_outputs = outputs['content']
    style_loss = tf.add_n([tf.reduce_mean((style_outputs[name]-style_targets[name])**2)
                           for name in style_outputs.keys()])
    style_loss *= style_weight / num_style_layers

    content_loss = tf.add_n([tf.reduce_mean((content_outputs[name]-content_targets[name])**2)
                             for name in content_outputs.keys()])
    content_loss *= content_weight / num_content_layers
    loss = style_loss + content_loss
    return loss

这段代码是不言自明的,因为我们已经讨论过它的各个部分。这个函数期望我们使用StyleContentModel类获得的字典作为输入。

现在让我们创建一个更新权重的函数:

@tf.function()
def train_step(image):
  with tf.GradientTape() as tape:
    outputs = extractor(image)
    loss = style_content_loss(outputs)

  grad = tape.gradient(loss, image)
  opt.apply_gradients([(grad, image)])
  image.assign(clip_0_1(image))

我们使用tf.GradientTape来更新图像。注意,当你用@tf.function注释一个函数时,你仍然可以像调用其他函数一样调用它。但是会被编译成图,这意味着你获得了更快执行的好处,在 GPU 或者 TPU 上运行,或者导出到 SavedModel(参见 https://www.tensorflow.org/alpha/guide/autograph )。请记住,变量extractor是通过以下代码获得的:

extractor = StyleContentModel(style_layers, content_layers)

并且是具有不同层的输出的字典。

现在,这段代码在开始时理解起来相当高级和复杂,所以不要着急,同时打开 Jupyter 笔记本阅读页面,以便能够理解代码和解释。如果一开始你不明白所有的事情,不要气馁。该行:

grad = tape.gradient(loss, image)

将计算损失函数相对于我们已经定义的变量image的梯度。每个更新步骤都可以通过一行简单的代码来完成:

train_step(image)

现在我们可以轻松地进行最后一个循环了:

epochs = 20
steps_per_epoch = 100

step = 0
for n in range(epochs):
  for m in range(steps_per_epoch):
    step += 1
    train_step(image)
    print(".", end=")
  display.clear_output(wait=True)
  imshow(image.read_value())
  plt.title("Train step: {}".format(step))
  plt.show()

当它运行时,你会看到图像每一个时代都在变化,你可以见证它是如何变化的。

有剪影的 NST

你可以用 NST 做一个有趣的应用,它与剪影 12 有关。一个剪影是一个用单一颜色的固体形状表示的图像。在图 5-3 中,可以看到一个例子;如果你是星球大战的粉丝,你知道是谁(提示:达斯维达 13 )。

img/470317_1_En_5_Fig3_HTML.jpg

图 5-3

《星球大战》角色达斯·维德的剪影

你应该在互联网上搜索类似马赛克或彩色玻璃的图像,如图 5-4 所示。

img/470317_1_En_5_Fig4_HTML.jpg

图 5-4

马赛克般的图像

目标是获得如图 5-5 所示的图像。

img/470317_1_En_5_Fig5_HTML.jpg

图 5-5

NST 在应用蒙版后在剪影上完成(稍后会详细介绍)

掩饰

屏蔽有几种含义,取决于您使用它的领域。这里我指的蒙版是根据剪影把图像的部分变成绝对白色的过程。这个想法在图 5-6 中进行了图示。你可以这样想:你在你的图像上放一个剪影(它们应该有相同的分辨率),只保留剪影是黑色的部分。

img/470317_1_En_5_Fig6_HTML.jpg

图 5-6

应用于图 5-4 中镶嵌图像的遮蔽

这没问题,但是有点不满意,因为例如你在结果中没有边。马赛克形状简单地从中间切开。视觉上这不是很令人满意。但是我们可以使用 NST 来使最终图像更好。该过程如下:

  • 您使用类似马赛克的图像作为样式图像。

  • 您使用您的轮廓图像作为内容图像。

  • 最后,使用剪影图像将遮罩应用到最终结果中。

你可以在图 5-5 中看到结果(使用相同的代码)。你可以看到你得到了很好的边缘,马赛克瓷砖没有被切成两半。

你可以在本书第五章的 GitHub 知识库中找到完整的代码。但是作为参考,让我们假设您将图像保存为 numpy 数组。让我们假设剪影保存在一个名为mask的数组中,而你的图像保存在一个名为result的数组中。假设(您应该检查一下)掩码数组将只包含 0 或 255 个值(黑和白)。然后简单地用这个做屏蔽:

result[mask] = 255

这只是使白色的结果图像中有白色的轮廓,其余的保持不变。

Footnotes 1

https://en.wikipedia.org/wiki/Loss_function

2

这个例子在 Michelucci,Umberto,2018 中有详细讨论。应用深度学习:基于案例的理解深度神经网络的方法。1.奥弗拉格。纽约:新闻。国际标准书号 978-1-4842-3789-2。可从: https://doi.org/10.1007/978-1-4842-3790-8

3

记住维度的顺序取决于你如何构建你的网络,你可能需要改变它。此处的尺寸仅用于说明目的。

4

https://en.wikipedia.org/wiki/Taylor_series

5

https://en.m.wikipedia.org/wiki/Skewness .E**δY= 0 的情况下。

[6

https://en.m.wikipedia.org/wiki/Kurtosis 。在E[δY]= 0 的情况下。

7

在概率和统计中,概率质量函数(PMF)是给出离散随机变量恰好等于某个值的概率的函数【Stewart,William J. (2011)。 概率、马尔可夫链、队列、模拟:性能建模的数学基础 普林斯顿大学出版社。第 105 页。ISBN978-1-4008-3281-1。]

8

https://en.wikipedia.org/wiki/Claude_Shannon

9

https://en.wikipedia.org/wiki/Neural_Style_Transfer

10

莱昂·加蒂斯;亚历山大·埃克;马蒂亚斯·贝斯吉(2015 年 8 月 26 日)。《艺术风格的神经算法》。https://arxiv.org/abs/1508.06576

11

“用于大规模视觉识别的非常深的 CNN”。Robots.ox.ac.uk。2014.检索到 2019 年 2 月 13 日, http://www.robots.ox.ac.uk/~vgg/research/very_deep/

12

本章的这一部分受到了中帖 https://becominghuman.ai/creating-intricate-art-with-neural-style-transfer-e5fee5f89481 的启发。

13

https://en.wikipedia.org/wiki/Darth_Vader

14

请注意,本章中使用的所有图像都是无版权和免费使用的图像。如果你在你的论文或作品中使用图像,确保你可以自由使用它们,否则你需要支付版税。

****

六、对象分类:简介

在这一章中,我们将研究用神经网络可以实现的更高级的图像处理任务。我们将着眼于语义分割、定位、检测和实例分割。本章的目标不是让你成为专家,因为你可以很容易地阅读许多关于这个主题的书籍,而是给你足够的信息,以便能够理解算法和阅读原始论文。我希望,在这一章结束的时候,你会明白这些方法之间的区别,你会对这些方法的构建模块有一个直观的理解。

这些算法需要许多先进的技术,如多损失函数和多任务学习,我们在前面的章节中已经讨论过了。我们将在这一章中再看几个。请记住,在某些情况下,关于方法的原始论文只有几年的历史,所以要掌握这个主题,您需要亲自动手阅读原始论文。

训练和使用论文中描述的网络在简单的笔记本电脑上是不可行的,因此你会在本章(和下一章)中发现更少的代码和例子。我试图为您指出正确的方向,并告诉您在撰写本文时有哪些经过预先培训的库和网络可用,以防您想在自己的项目中使用这些技术。那将是下一章的主题。在相关的地方,我试图指出不同方法的区别、优点和缺点。我们将以非常肤浅的方式来看待最先进的方法,因为细节非常复杂,只有研究原始论文才能给你自己实现那些算法所需的所有信息。

什么是对象定位?

让我们从直观的理解什么是对象定位开始。我们已经看到了许多形式的图像分类:它告诉我们图像的内容是什么。这听起来很容易,但在很多情况下这很难,而且不是因为算法。例如,考虑当你在一个图像中同时有一只狗和一只猫的情况。图像的类别是什么:猫还是狗?图像的内容是什么:一只猫还是一只狗?当然,两者都在那里,但是分类算法只给你一个类别,所以它们不能告诉你你在图像中有两种动物。如果你有很多猫和狗呢?如果你有几个对象呢?你明白了。

知道猫和狗在图像中的位置可能很有趣。考虑一下自动驾驶汽车的问题:知道一个人在哪里很重要,因为这可能意味着一个死去的路人和一个活着的人之间的区别。正如我们在前面章节中所看到的,分类通常不能单独用于解决图像的实际问题。通常,识别图像中的对象有许多实例需要找到它们在图像中的位置,并能够区分它们。为此,我们需要能够找到图像中每个实例的位置及其边界。这是图像识别技术中最有趣(也是最困难)的任务之一,可以用 CNN 来解决。

通常,对于对象定位,我们希望确定一个对象(例如,一个人或一辆车)在图像中的位置,并在其周围绘制一个矩形边界框。

注意

对于对象定位,我们希望确定一个或多个对象(例如,人或汽车)在图像中的位置,并在其周围绘制一个矩形边界框。

有时在文献中,当图像只包含一个对象实例(例如,只有一个人或一辆车)时,研究人员使用术语定位,当图像包含多个对象实例时,使用术语检测

注意

定位通常是指当一幅图像只包含一个对象的实例时,而检测是指当一幅图像中有多个对象的实例时。

为了对术语进行总结和澄清,以下是所有使用的词语和术语的概述(图 6-1 中显示了直观的解释):

img/470317_1_En_6_Fig1_HTML.jpg

图 6-1

描述在图像中定位一个或多个对象的一般任务的不同术语的直观解释

  • 分类:给一张图片贴上标签,换句话说,就是“理解”图片里的东西。例如,一只猫的图像可能有“猫”的标签(在前面的章节中我们已经看到了几个这样的例子)。

  • 分类和定位:给一幅图像加一个标签,确定其中包含的对象的边界(通常在对象周围画一个矩形)。

  • 对象检测:当一幅图像中有一个对象的多个实例时,使用这个术语。在对象检测中,您想要确定几个对象(例如,人、汽车、标志等)的所有实例。)并在它们周围绘制边界框。

  • 实例分割:你要为每一个单独的实例用特定的类来标记图像的每一个像素,以便能够找到对象实例的确切界限。

  • 语义分割:你要给图像的每一个像素点贴上特定的类别标签。实例分段的不同之处在于,您不在乎是否有几个汽车实例作为实例。属于汽车的所有像素将被标记为“汽车”。在实例分割中,您仍然能够知道一辆汽车有多少个实例,以及它们的确切位置。为了理解其中的区别,参见图 6-1 。

分割通常是所有任务中最困难的,并且在特定情况下分割尤其困难。许多先进的技术结合起来解决这些问题。需要记住的一点是,获得足够的训练数据并不容易。请记住,这比简单的分类要困难得多,因为有人需要标记物体的位置。对于分割,需要有人对图像中的每个像素进行分类,这意味着训练数据非常昂贵且难以收集。

最重要的可用数据集

可以用来解决这些问题的一个众所周知的数据集是位于 http://cocodataset.org 的微软 COCO 数据集。该数据集包含 91 种对象类型,在 328,000 幅图像中总共有 250 万个标记实例。 1 为了让您对所使用的标注类型有个概念,图 6-2 显示了数据集中的一些例子。您可以看到对象的特定实例(如人和猫)是如何在像素级别分类的。

img/470317_1_En_6_Fig2_HTML.jpg

图 6-2

COCO 数据集中的图像示例

关于大小的快速说明:2017 年的训练图像大约为 118,000 个,需要 18GB 2 的硬盘空间,所以请记住这一点。用如此大量的数据训练一个网络并不简单,需要时间和大量的计算能力。有一个 API 可以下载 COCO 图像,您可以使用它,Python 中也有这个 API。更多信息可以在主网页或 API GitHub 库 https://github.com/cocodataset/cocoapi 找到。图像有五种注释类型:对象检测、关键点检测、填充分割、全景分割和图像字幕。更多信息请访问 http://cocodataset.org/#format-data

你可能遇到的另一个数据集是 Pascal VOC 数据集。不幸的是,该网站并不稳定,因此镜像存在于你可以找到文件的地方。一面镜子是 https://pjreddie.com/projects/pascal-voc-dataset-mirror/ 。请注意,这是一个比 COCO 数据集小得多的数据集。

在这一章和下一章,我们将主要集中在对象分类和定位。我们将假设在图像中我们只有一个特定对象的实例,任务是确定它是什么类型的对象,并围绕它绘制一个边界框(矩形)。这些现在已经足够挑战了!我们将简要地看一下分段是如何工作的,但我们不会深入研究它的许多细节,因为它的问题极难解决。我会提供参考资料,你可以自己检查和研究。

并集交集(IoU)

让我们考虑对图像进行分类,然后在其中的对象周围绘制一个边界框的任务。在图 6-3 中,你可以看到一个我们期望的输出示例(这里的类是cat)。

img/470317_1_En_6_Fig3_HTML.jpg

图 6-3

对象分类和定位的例子 3

这是一项完全监督的任务。这意味着我们需要知道边界框在哪里,并将它们与一些给定的事实进行比较。我们需要一个度量来量化预测的边界框和实际情况之间的重叠程度。这通常是通过 Union 上的交集)完成的。在图 6-4 中,你可以看到它的直观解释。作为一个公式,我们可以写成

$$ IOU=\frac{Area\ of\ overlap}{Area\ of\ union} $$

img/470317_1_En_6_Fig4_HTML.jpg

图 6-4

IOU 指标的直观解释

在完美重叠的理想情况下,我们有 IOU = 1,而如果完全没有重叠,我们有 IOU = 0。你会在博客和书籍中找到这个术语,所以知道如何使用基本事实测量边界框是个好主意。

一种解决目标定位的简单方法(滑动窗口方法)

解决定位问题的一个简单方法如下(剧透:这不是一个好主意,但是看看为什么会有启发性):

  1. 您从左上角开始剪切输入图像的一小部分。假设你的图像有维度 xy ,你的那部分有维度 w xw y ,有wx<xwy

** 你使用一个预先训练好的网络(你如何训练它或者你如何得到它在这里是不相关的),你让它对你剪切的图像部分进行分类。

 *   你移动这个窗口一个我们称之为*步距*的量,然后用 *s* 向右下方指示。你用网络来分类这第二部分。

 *   一旦滑动窗口覆盖了整个图像,你就选择给你最高分类概率的窗口位置。这个位置会给你对象的边界框(记住你的窗口有尺寸 *w* <sub>*x*</sub> , *w* <sub>*y*</sub> )。

 *

在图 6-5 中,可以看到算法的图解说明(我们假设wx=wy=s*)。

img/470317_1_En_6_Fig5_HTML.jpg

图 6-5

解决目标定位问题的滑动窗口方法的图解说明

如图 6-5 所示,我们从左上角开始,向右滑动窗口。一旦我们到达图像的右边界,并且我们没有任何空间将窗口进一步向右移动,我们回到左边界,但是我们将它向下移动像素。我们继续以这种方式,直到我们到达图像的右下角。

您可能会立即发现这种方法的一些问题:

  • 根据wxT5、 w ys 的选择,我们可能无法覆盖整个图像。(您是否看到图 6-5 中窗口 4 右侧的一小部分图像未被分析?)

  • 如何选择wxT5、 w ys ?这是一个相当棘手的问题,因为我们的对象的边界框将正好具有尺寸 w xw y 。如果物体更大或更小呢?我们通常不知道它的尺寸,如果我们想要精确的边界框,这是一个很大的问题。

  • 如果我们的对象流过两个窗口呢?在图 6-5 中,你可以想象物体一半在窗口 2,一半在窗口 3。那么你的边界框将不会是正确的,如果你按照所描述的算法。

我们可以通过使用 s = 1 来解决第三个问题,以确保我们涵盖了所有可能的情况,但是前两个问题并不那么容易解决。为了解决窗口大小的问题,我们应该尝试所有可能的大小和所有可能的比例。你觉得这里有什么问题吗?你需要对你的网络进行的进化的数量正在失去控制,并且很快在计算上变得不可行。

滑动窗口方法的问题和局限性

在本书的 GitHub 资源库中,在 Chapter 6 文件夹中,您可以找到滑动窗口算法的实现。为了使事情变得更简单,我决定使用 MNIST 数据集,因为在这一点上你应该非常了解它,并且它是一个易于使用的数据集。作为第一步,我建立了一个在 MNIST 数据集上训练的 CNN,准确率达到了 99.3%。然后,我开始将模型和权重保存在磁盘上。我使用的 CNN 具有以下结构:

_______________________________________________________________
Layer (type)                 Output Shape              Param #
===============================================================
conv2d_1 (Conv2D)            (None, 26, 26, 32)        320
_______________________________________________________________
conv2d_2 (Conv2D)            (None, 24, 24, 64)        18496
_______________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 12, 12, 64)        0
_______________________________________________________________
dropout_1 (Dropout)          (None, 12, 12, 64)        0
_______________________________________________________________
flatten_1 (Flatten)          (None, 9216)              0
_______________________________________________________________
dense_1 (Dense)              (None, 128)               1179776
_______________________________________________________________
dropout_2 (Dropout)          (None, 128)               0
_______________________________________________________________
dense_2 (Dense)              (None, 10)                1290
===============================================================
Total params: 1,199,882
Trainable params: 1,199,882
Non-trainable params: 0
_______________________________________________________________

然后,我使用下面的代码保存了模型和权重(我们已经讨论过如何做到这一点):

model_json = model.to_json()
with open("model_mnist.json", "w") as json_file:
    json_file.write(model_json)
model.save_weights("model_mnist.h5")

您可以在图 6-6 中看到网络训练和准确度如何随着历元数的变化而变化。

img/470317_1_En_6_Fig6_HTML.jpg

图 6-6

训练(实线)和验证(虚线)数据集的损失函数值和准确度与历元数的关系

权重和模型可以在 GitHub 存储库中找到。我这样做是为了避免每次都要重新训练 CNN。我每次都可以通过重新加载来重用模型。你可以用这段代码来做这件事(如果你想像我一样在 Google Colab 中运行这段代码,就在你安装了 Google drive 之后):

model_path = '/content/drive/My Drive/pretrained-models/model_mnist.json'
weights_path = '/content/drive/My Drive/pretrained-models/model_mnist.h5'

json_file = open(model_path, 'r')
loaded_model_json = json_file.read()
json_file.close()
loaded_model = model_from_json(loaded_model_json)
loaded_model.load_weights(weights_path)

让事情变得简单。我决定创建一个中间有一个数字的更大的图像,看看如何有效地在它周围放置一个边界框。为了创建图像,我使用了以下代码:

from PIL import Image, ImageOps
src_img = Image.fromarray(x_test[5].reshape(28,28))
newimg = ImageOps.expand(src_img,border=56,fill='black')

生成的图像为 140x140 像素。在图 6-7 中可以看到。

img/470317_1_En_6_Fig7_HTML.jpg

图 6-7

通过在 MNIST 数据集中的一个数字周围添加 56 像素的白色边框创建的新图像

现在让我们从一个 28x28 像素的滑动窗口开始。我们可以编写一个函数来尝试定位数字,并获取图像作为输入,步幅 s ,以及值wxw y :

def localize_digit(bigimg, stride, wx, wy):
  slidx, slidy = wx, wy

  digit_found = -1
  max_prob = -1
  bbx = -1 # Bounding box x upper left
  bby = -1 # Bounding box y upper left
  max_prob_ = 0.0
  bbx_ = -1
  bby_ = -1
  most_prob_digit = -1

  maxloopx = (bigimg.shape[0] -wx) // stride
  maxloopy = (bigimg.shape[1] -wy) // stride
  print((maxloopx, maxloopy))

  for slicey in range (0, maxloopx*stride, stride):
    for slicex in range (0, maxloopy*stride, stride):
      slice_ = bigimg[slicex:slicex+wx, slicey:slicey+wx]
      img_ = Image. fromarray(slice_).resize((28, 28), Image.NEAREST)
      probs = loaded_model.predict(np.array(img_).reshape(1,28,28,1))
      if (np.max(probs > 0.2)):
        most_prob_digit = np.argmax(probs)
        max_prob_ = np.max(probs)
        bbx_ = slicex
        bby_ = slicey

      if (max_prob_ > max_prob):
        max_prob = max_prob_
        bbx = bbx_
        bby = bby_
        digit_found = most_prob_digit

  print("Digit "+str(digit_found)+ " found, with probability "+str(max_prob)+" at coordinates "+str(bbx)+" "+str(bby))

  return (max_prob, bbx, bby, digit_found)

我们的形象是这样的:

localize_digit(np.array(newimg), 28, 28, 28)

返回此代码:

Digit 1 found, with probability 1.0 at coordinates 56 56
(1.0, 56, 56, 1)

由此产生的边界框如图 6-8 所示。

img/470317_1_En_6_Fig8_HTML.jpg

图 6-8

滑动窗口法找到的边界框 w x = 28, w y = 28,步距 s = 28

所以这很有效。但是您可能已经注意到,我们使用了值为 28 的 w xw ys ,这是我们图像的大小。如果我们改变了会发生什么?例如,考虑图 6-9 中描述的情况。您可以清楚地看到,一旦窗口的大小和比例变为不同于 28 的值,这种方法就会停止工作。

img/470317_1_En_6_Fig9_HTML.jpg

图 6-9

w xw ys 不同值的滑动窗口算法结果

检查左下框中图 6-9 中分类的置信度。挺低的。例如,对于 40x40 的窗口和 10 的跨距,数字的分类是正确的(a 1 ),但是以 21%的概率完成。那就是低价值!在右下角的方框中,分类完全错误。请记住,您需要调整从图像中剪切的小部分的大小,因此它看起来可能与您使用的训练数据不同。

在这种情况下,选择正确的窗口大小和比例似乎很容易,因为您知道图像看起来像什么,但通常您不知道什么值会起作用。你必须测试不同的比例和大小,得到几个可能的边界框和分类,然后决定哪一个是最好的。您可以很容易地看到,对于可能包含几个具有不同尺寸和比例的对象的真实图像,这在计算上是不可行的。

分类和定位

我们已经看到滑动窗口方法是一个坏主意。更好的方法是使用多任务学习。这个想法是,我们可以建立一个网络,同时学习类和边界框的位置。我们可以通过在 CNN 的最后一层之后增加两层致密层来实现。一个具有(例如) N c 神经元(用于分类 N c 类)将预测具有交叉熵损失函数的类(我们将用 J 分类 来表示),一个具有四个神经元,将学习具有 2 损失函数的边界框(即你可以在图 6-10 中看到网络图。

img/470317_1_En_6_Fig10_HTML.jpg

图 6-10

描述可以同时预测类和边界框位置的网络的图

由于这将是一个多任务学习问题,我们将需要最小化两个损失函数的线性组合:

$$ {J}_{classification}+\alpha {J}_{BB} $$

当然, α 是一个需要调优的附加超参数。正如参考 a2 损失与 MSE 成正比

$$ {\ell}_2\ Loss\ Function=\sum \limits_{i=1}m{\left({y}_{true}{(i)}-{y}_{predicted}^{(i)}\right)}² $$

像往常一样,我们用 m 表示我们所拥有的观察数据的数量。同样的想法也非常成功地用于人体姿态估计,这可以找到人体的特定点(例如关节),如图 6-11 所示。

img/470317_1_En_6_Fig11_HTML.jpg

图 6-11

人体姿态估计的一个例子。CNN 可以被训练来寻找人体的重要点,例如关节。

在这个领域有很多研究正在进行,在接下来的章节中,我们将看看这些方法是如何工作的。实现变得非常复杂和耗时。如果你想和这些算法打交道,最好的办法就是看看原始论文,研究一下。不幸的是,没有即插即用的库可以用来完成这些任务,尽管您可以找到一个 GitHub 库来帮助您。在这一章中,我们将看看最常见的 CNN 变体来进行对象定位——R-CNN、快速 R-CNN 和更快 R-CNN。在下一章,我们将研究 YOLO(你只看一次)算法。接下来的部分应该只作为相关论文的指针,并且会让你对网络的构建有一个基本的了解。这绝不是对这些实现的详尽分析,因为这需要大量的空间。

基于区域的 CNN (R-CNN)

基于区域的 CNN(也称为 R-CNN)的基本思想非常简单(但实现起来却不简单)。正如我们所讨论的,简单方法的主要问题是你需要测试大量的窗口来找到最佳匹配的边界框。搜索每个可能的位置在计算上是不可行的,因为它测试所有可能的纵横比和窗口大小。

因此,Girshick 等人 4 提出了一种方法,他们使用一种称为选择性搜索 5 的算法,首先从图像中提出 2000 个区域(称为区域建议),然后,他们不是对大量区域进行分类,而是仅对这 2000 个区域进行分类。

选择性搜索与机器学习无关,使用经典方法来确定哪些区域可能包含对象。该算法的第一步是使用像素强度和基于图形的方法分割图像(例如,Felzenszwalb 和 Huttenlocher 6 的方法)。你可以在图 6-12 中看到这种分割的结果。

img/470317_1_En_6_Fig12_HTML.jpg

图 6-12

应用于图像的分割示例(图像来源: http://cs.brown.edu/people/pfelzens/segment/ )

在此步骤之后,基于以下特征的相似性将相邻区域分组在一起:

  • 颜色相似性

  • 纹理相似性

  • 尺寸相似性

  • 形状兼容性

如何做到这一点的具体细节超出了本书的范围,因为这些技术通常用在图像处理算法中。

在 OpenCV 7 库中,有算法的实现,可以试试。在图 6-13 中,你可以看到一个例子。我将该算法应用于我拍摄的一张照片,并要求该算法提出 40 个区域。

img/470317_1_En_6_Fig13_HTML.jpg

图 6-13

OpenCV 库中实现的选择性搜索算法的输出示例

我用的 Python 代码可以在以下网站找到: https://www.learnopencv.com/selective-search-for-object-detection-cpp-python/ 。R-CNN 的主要思想是使用 CNN 来标记该算法提出的区域,然后使用支持向量机进行最终分类。

例如,在图 6-9 中,您可以看到笔记本电脑未被识别为物体。但这就是为什么在 R-CNN 中使用 2000 个区域,以确保有足够的区域被提出。人工检查许多区域不能由人用肉眼完成。区域的数量及其重叠如此之大,以至于该任务不再可行。如果您尝试该算法的 OpenCV 实现,您会注意到它相当慢。这是开发其他方法的主要原因之一。例如,手动方法不适合实时对象检测(例如,在自动驾驶汽车中)。

R-CNN 可以概括为以下几个步骤(这些步骤已从 http://toe.lt/d )开始:

  1. 拿一个预先训练好的imagenet CNN(比如 Alexnet)。

  2. 用需要检测的对象和“无对象”类重新训练最后一个完全连接的层。

  3. 从选择性搜索中获得所有建议(每张图片大约 2000 个地区建议),并调整它们的大小以匹配 CNN 的输入。

  4. 训练 SVM 对物体和背景之间的每个区域进行分类(每个类别一个二元 SVM)。

  5. 使用边界框回归。训练一个线性回归分类器,它将为边界框输出一些校正因子。

快速 R-CNN

Girshick 改进了它的算法,创造了所谓的“快速 R-CNN”。 8 这个算法背后的主要思想如下

  1. 图像通过 CNN 并提取特征图(卷积层的输出)。

  2. 提出区域,不是基于初始图像,而是基于特征图。

  3. 然后,相同的特征图和建议的区域被用于传递到分类器,该分类器决定哪个对象在哪个区域中。

解释这些步骤的图表如图 6-14 所示。

img/470317_1_En_6_Fig14_HTML.jpg

图 6-14

描述快速 R-CNN 算法的主要步骤的图

这种算法比 R-CNN 更快的原因是,你不必每次 9 都向卷积神经网络馈送 2000 个区域提议——你只需要做一次。

更快的 R-CNN

注意,R-CNN 和快速 R-CNN 都使用选择性搜索来提议区域,因此相对较慢。即使快速的 R-CNN 对于每个图像也需要大约两秒钟,使得这种变化不适合实时对象检测。R-CNN 需要 50 秒左右,快速 R-CNN 需要两秒左右。但事实证明,我们可以做得更好,通过消除使用选择性搜索的需要,因为这是两种算法的瓶颈。

任等人 10 提出了一个新的想法:使用神经网络从标记数据中学习区域,完全去除了缓慢的选择性搜索算法。更快的 R-CNN 需要大约 0.2 秒,使它们成为对象检测的快速算法。在 http://toe.lt/e 有一个非常好的图表,描述了更快的 R-CNN 的主要步骤。 11 我们在图 6-15 中为您报告了它,因为我认为它确实有助于直观地理解更快的 R-CNN 的主要构件。细节往往相当复杂,因此直观和肤浅的描述不会为你服务。要理解这些步骤和微妙之处,你需要更多的时间和经验。

img/470317_1_En_6_Fig15_HTML.jpg

图 6-15

描述快速 R-CNN 主要部分的图表。图片来源: http://toe.lt/e

在下一章,我们将看另一个算法(YOLO ),看看你如何在你自己的项目中使用这些技术。

Footnotes 1

描述数据集的原始论文是:宗-林逸、迈克尔·梅尔、塞尔日·贝隆吉、卢博米尔·布尔德夫、罗斯·吉尔希克、詹姆斯·海斯、皮埃特罗·佩罗娜、德瓦·拉曼南、c·劳伦斯·齐特尼克、皮奥特·多拉尔、微软可可:上下文中的常见对象、 https://arxiv.org/abs/1405.0312

2

http://cocodataset.org/#download

3

图片来源: http://www.cbsr.ia.ac.cn/users/ynyu/detection.html

4

https://arxiv.org/pdf/1311.2524.pdf

5

Jasper R. R. Uijlings,Koen E. A. van de Sande,Theo Gevers,Arnold w . m . smulders《国际计算机视觉杂志》,第 104 卷(2),第 154-171 页,2013 年[ http://toe.lt/b ]

6

页(page 的缩写)Felzenszwalb,D. Huttenlocher ,有效的基于图的图像分割,国际计算机视觉杂志,第 59 卷,第 2 期,2004 年 9 月

7

https://opencv.org

8

https://arxiv.org/pdf/1504.08083.pdf

9

http://toe.lt/c

10

https://arxiv.org/pdf/1506.01497.pdf

11

图像的一部分出现在 Ren 的原始论文中,但是额外的标签和信息由 Leonardo Araujo dos Santos(https://legacy.gitbook.com/@leonardoaraujosantos)添加。

*

七、对象定位:Python 中的一个实现

在这一章,我们将看看 YOLO(你只看一次)方法的对象检测。这一章分为两部分:第一部分我们学习算法是如何工作的,第二部分我将给出一个例子,说明如何在自己的 Python 项目中使用它。

请记住,YOLO 是非常复杂的,所以对于 99%的人来说,预先训练的模型是进行对象检测的最佳选择。对于处于研究前沿的 1%,你可能不需要这本书,你应该知道如何从头开始做物体检测。

这一章(和前一章一样)应该为你指出正确的方向,给你理解算法所需的基础知识,并给你对象检测的第一次经验。你很快就会注意到这些方法很慢,很难实现,并且有很多限制。这是一个非常活跃的研究领域,也非常年轻。描述 YOLO 版本 3(我们将在 Python 代码的本章稍后使用)的论文刚刚在 2018 年 4 月发表。写这篇文章的时候,还不到两岁!那些算法很难实现,很难理解,也很难训练。我希望在这一章结束的时候,你会理解它的基础,并且你可以用模型执行你的第一次测试。

注意

那些算法很难实现,很难理解,也很难训练。

你只看一次(YOLO)法

在上一章中,我们看了几种物体检测的方法。我还向您展示了为什么使用滑动窗口是一个坏主意,以及困难在哪里。2015 年,Redmon J .等人提出了一种新的方法来进行物体检测:他们将其称为 YOLO(你只看一次)。他们开发了一个网络,可以执行所有必要的任务(检测物体在哪里,对多个物体进行分类等。)一气呵成。这是这种方法速度快并且经常用于实时应用的原因之一。

在文献中,您会发现该算法的三个版本:YOLOv1、YOLOv2 和 YOLOv3。v2 和 v3 是对 v1 的改进(稍后会详细介绍)。最初的网络是用 darknet 开发和训练的,darknet 是由最初算法的作者 Redmon J 开发的神经网络框架,你不会找到一个可以与 Keras 一起使用的易于下载、预训练的模型。稍后我会给你一个例子,告诉你如何在你的项目中使用它。

读关于 YOLO 的论文原文很有启发,可以在 https://arxiv.org/abs/1506.02640 找到。

注意

该方法的主要思想是将检测问题重构为一个单一的回归问题,从作为输入的图像像素到边界框坐标和类别概率 1

我们来详细看看它是如何工作的。

YOLO 是如何运作的

为了理解 YOLO 是如何工作的,最好一步一步地研究这个算法。

将图像划分为单元格

第一步是将图像分成 S × S 个细胞。对于每个单元格,我们预测单元格中有什么(以及是否有)对象。每个单元只能预测一个对象,因此一个单元不能预测多个对象。然后,对于每个单元,预测应该包含对象的一定数量( B )的边界框。在图 7-1 中,你可以看到网络可能预测的网格和边界框(作为一个例子)。原论文中,图像被划分为 7 × 7 的网格,但为了图 7-1 的清晰起见,我将图像划分为 5×5 的网格。

img/470317_1_En_7_Fig1_HTML.jpg

图 7-1

分成 5 × 5 网格的图像。对于单元格 D3,我们将预测鼠标,并将预测边界框(黄色框)。对于单元格 B2,我们将预测一个瓶子及其边界框(红色矩形)。

让我们以图 7-1 中的单元格 D3 为例。该单元将预测鼠标的存在,然后它将预测一定数量的边界框(黄色矩形)的 B 。类似地,单元格 B2 将同时预测瓶子和 B 边界框(图 7-1 中的红色矩形)的存在。此外,该模型预测每个边界框的类别置信度(一个数字)。准确地说,每个单元的模型输出如下:

  • 对于每个包围盒(总共 B ,有四个值: xywh 。这些是中心的位置,宽度和高度。注意,中心的位置是相对于单元位置给出的,而不是绝对值。

  • For each bounding box (B in total), there is a confidence score, which is a number that reflects how likely the box contains the object. In particular, at training time, if we indicate the probability of the cell containing the object as Pr(Object), the confidence is calculated as follows:

    $$ \Pr (Object)\times IOU $$

    其中 IOU 表示并集上的交集,这是使用训练数据计算的(有关该术语的解释以及如何计算,请参见上一章)。该数字同时编码了特定对象在框中的概率以及边界框与该对象的适合程度。

因此,假设我们有 S = 5, B = 2,并且假设网络可以分类 N c = 80 个类别,网络将具有如下大小的输出:

$$ S\times S\times \left(B\times 5+{N}_c\right)=5\times 5\times \left(2\times 5+80\right)=2250 $$

在原始论文中,作者使用了 S = 7, B = 2,并使用了具有 20 个标记类的 VOC 数据集 2 。因此,网络的输出如下:

$$ S\times S\times \left(B\times 5+{N}_c\right)=7\times 7\times \left(2\times 5+20\right)=1470 $$

网络结构相当简单。它只是几个卷积层的集合(其中有一些 maxpool ),最后有一个大的密集层来预测必要的值(记住这个问题是作为回归问题提出的)。在最初的论文中,作者受到了 GoogLeNet 模型的启发。该网络有 24 层,后面是两个密集层(最后一层有 1470 个神经元;你知道为什么吗?).正如作者提到的,训练持续了整整一周。他们在培训中使用了一些技巧,如果你感兴趣,我强烈建议你阅读原文。这很有启发性(例如,他们还以一种不寻常的方式使用学习率衰减,在开始时增加学习率的值,然后在后来降低它)。他们还使用了辍学和广泛的数据扩充。训练这些模型不是一件小事。

YOLOv2(也称为 YOLO9000)

最初的 YOLO 版本有一些缺点。例如,它不太擅长检测太近的物体。在第二个版本中, 3 作者引入了一些优化,其中最重要的是锚盒。该网络给出预先确定的框集,而不是完全从零开始预测边界框,它只是预测与锚框集的偏差。可以根据您想要预测的对象类型来选择锚框,从而使网络更好地完成某些特定任务(例如,小或大的对象)。

在这个版本中,他们还改变了网络结构,使用了 19 层,然后又增加了 11 层专门用于对象检测,总共 30 层。这个版本也很难处理小对象(也是在使用锚盒的时候)。这是因为图层对图像进行了向下采样,并且在向前传递的过程中,网络信息丢失了,这使得检测微小的事物变得困难。

约洛夫 3 号

最后一个版本 4 引入了一些新的概念,使得这个模型非常强大。以下是主要的改进:

  • 预测不同比例的盒子:可以说,该模型预测了不同维度的盒子(比这要复杂一点,但这应该会让您对正在发生的事情有一个直观的了解)。

  • 网络要大得多:一共 53 层。

  • 网络使用跳跃连接。基本上,这意味着一层的输出不仅会被馈送到下一层,而且还会被馈送到网络中的下一层。这样,尚未下采样的信息将在以后使用,以使检测小对象更容易。跳过连接在 ResNets 中使用(本书不讨论),在 http://toe.lt/w 可以找到很好的介绍。

  • 这个版本使用九个锚盒,每个音阶三个。

  • 这个版本为每个单元格预测了更多的边界框。

所有这些改进使得 YOLOv3 相当不错,但也相当慢,因为处理所有这些数字需要增加计算能力。

非极大值抑制

一旦你有了所有预测的边界框,你需要选择一个最好的。请记住,对于每个单元格和对象,模型会预测几个边界框(无论您使用哪个版本)。基本上,你通过以下步骤选择最佳边界框(称为非最大值抑制):

  1. 它首先丢弃对象存在的概率小于给定阈值(通常为 0.6)的所有单元。

  2. 它将所有最有可能有物体存在的细胞都包括在内。

  3. 它采用具有最高分数的边界框,并且彼此移除 IOU 大于特定阈值(通常为 0.5)的所有其他边界框。这意味着它会删除所有与所选边框非常相似的边框。

损失函数

注意,前面提到的网络有大量的输出,所以不要指望简单的损失函数就能起作用。还要注意,最后一层的不同部分有非常不同的含义。一部分是边界框位置,一部分是类别概率,等等。损失函数有三个部分:

  • 分类损失

  • 定位损失(预测边界框和预期结果之间的误差)

  • 信心损失(盒子里是否有物体)

让我们仔细看看这三个方面的损失。

分类损失

所用的分类损失由下式确定

$$ \sum \limits_{i=0}{S²}{\mathbbm{I}}_i{obj}\sum \limits_{c\in classes\ }{\left({p}_i(c)-{\hat{p}}_i(c)\right)}² $$

在哪里

$$ {\mathbbm{I}}_i^{obj} is\ 1 $$如果一个对象在单元格 i 中,否则为 0。

$$ {\hat{p}}_i(c) $$表示在单元格 i 中拥有类别 c 的概率。

定位损失

该损失测量预测的边界框相对于预期边界框的误差。

$$ {\lambda}_{coord}\sum \limits_{i=0}^{S²}\sum \limits_{j=0}B{\mathbbm{I}}_i{obj}\left[{\left({x}_i-{\hat{x}}_i\right)}²+{\left({y}_i-{\hat{y}}_i\right)}²\right]+{\lambda}_{coord}\sum \limits_{i=0}^{S²}\sum \limits_{j=0}B{\mathbbm{I}}_i{obj}\left[{\left(\sqrt{w_i}-\sqrt{{\hat{w}}_i}\right)}²+{\left(\sqrt{h_i}-\sqrt{{\hat{h}}_i}\right)}²\right] $$

信心丧失

置信度损失度量了当决定一个对象是否在盒子中时的误差。

$$ \sum \limits_{i=0}^{S²}\sum \limits_{j=0}B{\mathbbm{I}}_{ij}{obj}{\left({\mathrm{C}}_{\mathrm{i}}-{\hat{\mathrm{C}}}_{\mathrm{i}}\right)}² $$

在哪里

$$ {\hat{\mathrm{C}}}_{\mathrm{i}} $$是盒子 j 在单元格 i 的置信度。

$$ {\mathbbm{I}}_{ij}^{obj} is\ 1 $$如果单元格中的 j * th * 包围盒 i 负责检测物体。

由于大多数细胞不包含一个物体,我们必须小心。网络可以知道背景是重要的。我们需要在成本函数中增加一项来弥补这一点。这是通过附加术语实现的:

$$ {\lambda}_{noobj}\sum \limits_{i=0}^{S²}\sum \limits_{j=0}B{\mathbbm{I}}_{ij}{noobj}{\left({\mathrm{C}}_{\mathrm{i}}-{\hat{\mathrm{C}}}_{\mathrm{i}}\right)}² $$

其中$$ {\mathbbm{I}}_{ij}^{noobj} $$$$ {\mathbbm{I}}_{ij}^{obj} $$的反义词。

总损失函数

总损失函数就是所有项的总和:

$$ L=\sum \limits_{i=0}{S²}{\mathbbm{I}}_i{obj}\sum \limits_{c\in classes\ }{\left({p}_i(c)-{\hat{p}}_i(c)\right)}²+{\lambda}_{coord}\sum \limits_{i=0}^{S²}\sum \limits_{j=0}B{\mathbbm{I}}_i{obj}\left[{\left({x}_i-{\hat{x}}_i\right)}²+{\left({y}_i-{\hat{y}}_i\right)}²\right]+{\lambda}_{coord}\sum \limits_{i=0}^{S²}\sum \limits_{j=0}B{\mathbbm{I}}_i{obj}\left[{\left(\sqrt{w_i}-\sqrt{{\hat{w}}_i}\right)}²+{\left(\sqrt{h_i}-\sqrt{{\hat{h}}_i}\right)}²\right]+\sum \limits_{i=0}^{S²}\sum \limits_{j=0}B{\mathbbm{I}}_{ij}{obj}{\left({\mathrm{C}}_{\mathrm{i}}-{\hat{\mathrm{C}}}_{\mathrm{i}}\right)}²+{\lambda}_{noobj}\sum \limits_{i=0}^{S²}\sum \limits_{j=0}B{\mathbbm{I}}_{ij}{noobj}{\left({\mathrm{C}}_{\mathrm{i}}-{\hat{\mathrm{C}}}_{\mathrm{i}}\right)}² $$

如您所见,这是一个实现起来很复杂的公式。这是进行对象检测的最简单方法是下载并使用预训练模型的原因之一。从头开始需要一些时间和努力。相信我。

在接下来的章节中,我们将看看如何在自己的 Python 项目中使用 YOLO 算法(尤其是 YOLOv3)。

YOLO 在 Python 和 OpenCV 中的实现

YOLO 的暗网实现

如果您遵循了前面的章节,您会理解从头开始为 YOLO 开发您自己的模型对于初学者(以及几乎所有的从业者)来说是不可行的,因此,正如我们在前面的章节中所做的,我们需要使用预训练的模型来在您的项目中使用对象检测。你可以找到你想要的所有预训练模型的网页是 https://pjreddie.com 。这是黑暗网络的维护者约瑟夫·c·雷德蒙的主页。

注意

Darknet 是用 C 和 CUDA 编写的开源神经网络框架。它速度快,易于安装,支持 CPU 和 GPU 计算。

在一个子页( https://pjreddie.com/darknet/yolo/ )上,你会找到你需要的关于 YOLO 算法的所有信息。你可以从这个页面下载几个预训练模型的重量。对于每个型号,您将始终需要两个文件:

  • 一个.cfg文件,里面基本包含了网络的结构。

  • 一个.weights文件,包含训练后得到的权重。

为了让你对文件的内容有个概念,.cfg文件包含了所有使用的层的信息。下面是一个例子:

[convolutional]
batch_normalize=1
filters=64
size=3
stride=1
pad=1
activation=leaky

这说明了特定卷积层的结构。文件中包含的最重要的信息是关于:

  • 网络体系结构

  • 锚箱

  • 班级数量

  • 学习率和使用的其他参数

  • 批量

另一个文件(.weights)包含执行推理所需的预训练权重。注意,它们不是以 Keras 兼容的格式保存的(就像我们到目前为止使用的.h5文件),所以它们不能被加载到 Keras 模型中,除非你首先转换它们。

没有标准的工具或实用程序来转换这些文件,因为格式不是恒定的(例如,它在 YOLOv2 和 YOLOv3 之间发生了变化)。如果你有兴趣使用 YOLO 到 v2,你可以使用 YAD2K 库(另一个 Darknet 2 Keras),可以在 https://github.com/allanzelener/YAD2K 找到。

请注意,这不适用于 YOLOv3 .cfg文件。相信我,我试过了。但是如果您对 YOLOv2 满意,您可以使用这个存储库中的代码将.weight文件转换成一种更加 Keras 友好的格式。

我还想指出另一个 GitHub 库,它在 https://github.com/qqwweee/keras-yolo3 为 YOLOv3 实现了一个转换器。它有一些限制(例如,您必须使用标准锚点),但是它可能是转换文件的一个好的起点。然而,使用预训练模型有一个更简单的方法,那就是使用 OpenCV,我们将在本章后面看到。

用暗网测试目标检测

如果你只是想对一幅图像进行分类,最简单的方法就是按照 darknet 网站上的说明去做。让我们看看这是如何工作的。请注意,如果您使用的是 Linux 或 MacOS X 系统,这些说明仍然有效。在 Windows 上,您需要安装makegcc和其他几个工具。如网站所述,安装只需要几行代码:

git clone https://github.com/pjreddie/darknet
cd darknet
make
wget https://pjreddie.com/media/files/yolov3.weights

在这一点上,你可以简单地执行你的对象检测与此: 5

./darknet detect cfg/yolov3.cfg yolov3.weights table.jpg

注意,.weight文件非常大(大约 237MB)。下载时请记住这一点。在 CPU 上,这相当慢;一台非常现代的 2018 款 MacBook Pro 用了 18 秒就下载完了。你可以在图 7-2 中看到结果。

img/470317_1_En_7_Fig2_HTML.jpg

图 7-2

YOLOv3 与测试图像上的暗网一起使用

默认情况下,使用阈值 0.25。但是您可以使用-thresh XYZ参数指定一个不同的值。您必须将XYZ更改为您想要使用的阈值。

这种方法很适合进行对象检测,但是很难在 Python 项目中使用。为此,您需要能够在代码中使用预先训练好的模型。有几种方法可以做到这一点,但最简单的方法是使用opencv库。如果您正在处理图像,很可能您已经在处理这个库了。如果你从未听说过它,我强烈建议你去看看,因为它是一个很棒的图像库。你可以在 https://opencv.org 找到官方网页。

像往常一样,你可以在 GitHub 资源库中找到完整的代码,在本书的 Chapter 7 文件夹中。为了简洁起见,我们将只讨论最重要的部分。

你需要安装最新的opencv库。我们在这里讨论的代码是用版本 4.1.0 开发的。要确定您拥有的版本,请使用以下命令:

import cv2
print (cv2.__version__)

要尝试我们在这里讨论的代码,您需要来自 https://pjreddie.com 网站的三个文件:

  • coco.names

  • yolov3.cfg

  • yolov3.weights

coco.names包含预训练模型可以分类的类别的标签。yolov3.cfgyolov3.weights文件包含模型配置参数(正如我们已经讨论过的)和我们需要使用的权重。为了您的方便,由于yolov3.weights大约 240MB,无法上传到 GitHub,您可以在 http://toe.lt/r 下载三者的 ZIP 文件。在代码中,我们需要指定文件的位置。例如,您可以使用以下代码:

weightsPath = "yolo-coco/yolov3.weights"
configPath = "yolo-coco/yolov3.cfg"

您需要将位置更改为您在系统上保存文件的位置。OpenCV 提供了一个加载权重而无需转换权重的函数:

net = cv2.dnn.readNetFromDarknet(configPath, weightsPath)

这是很容易做到的,因为你不需要分析或者编写你自己的加载函数。它返回一个模型对象,我们稍后将使用它进行推理。如果你还记得本章开始时关于方法的讨论,我们需要得到输出层,以便得到我们需要的所有信息,比如边界框或预测类。我们可以通过下面的代码轻松做到这一点:

ln = net.getLayerNames()
ln = [ln[i[0] - 1] for i in net.getUnconnectedOutLayers()]

getUnconnectedOutLayers()函数返回未连接输出的层的索引,这正是我们要寻找的。ln变量将包含以下层:

['yolo_82', 'yolo_94', 'yolo_106']

然后,我们需要在一个 416x416 的正方形图像中调整图像的大小,并通过将像素值除以 255.0 对其进行归一化:

blob = cv2.dnn.blobFromImage(image, 1 / 255.0, (416, 416), swapRB=True, crop=False)

然后我们需要使用它作为保存在net模型中的模型的输入:

net.setInput(blob)

然后我们可以使用forward()调用对预训练模型进行正向传递:

layerOutputs = net.forward(ln)

我们还没有完成,所以不要放松。我们需要提取边界框,我们将把它们保存在boxes列表中,然后是置信度,保存在confidences列表中,然后是预测类,保存在classIDs列表中。

我们首先如下初始化列表:

boxes = []
confidences = []
classIDs = []

然后我们循环遍历这些层,提取我们需要的信息。我们可以执行如下循环:

for output in layerOutputs:
    for detection in output:

现在分数保存在从第五个开始的元素中,在detection变量中,我们可以用np.argmax(scores)提取预测的类:

scores = detection[5:]
classID = np.argmax(scores)

置信度当然是预测类的分数:

confidence = scores[classID]

我们希望预测的可信度大于零。在这里使用的代码中,我们选择了 0.15 的限值。预测边界框包含在detection变量的前四个值中:

box = detection[0:4] * np.array([W, H, W, H])
(centerX, centerY, width, height) = box.astype("int")

如果你还记得,YOLO 预测边界框的中心,所以我们需要提取左上角的位置:

x = int(centerX - (width / 2))
y = int(centerY - (height / 2))

然后我们可以简单地将找到的值添加到列表中:

boxes.append([x, y, int(width), int(height)])
confidences.append(float(confidence))
classIDs.append(classID)

然后,我们需要使用非最大值抑制(如前几节所述)。OpenCV 还为它提供了一个函数 6 :

idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.6,0.2)

该函数需要以下参数:

  • 一组边界框(保存在boxes变量中)

  • 一组置信度(保存在confidences变量中)

  • 一个阈值,用于根据分数过滤框(在前面的代码中为0.6)

  • 非最大抑制中使用的阈值(前面代码中的0.2)

然后我们可以用这个简单的代码获得正确的坐标:

for i in idxs.flatten():
        # extract the bounding box coordinates
        (x, y) = (boxes[i][0], boxes[i][1])
        (w, h) = (boxes[i][2], boxes[i][3])

你可以在图 7-3 中看到这段代码的结果。

img/470317_1_En_7_Fig3_HTML.jpg

图 7-3

使用 OpenCV 获得的 YOLOv3 结果

这完全是应该的——与图 7-2 中的结果相同。另外,我们有盒子上预测的概率。你可以看到这是多么容易。您只需将这几行代码添加到您的项目中。

请记住,我们使用预训练权重构建的模型将仅检测包含在图像数据集中的对象,预训练模型已通过该图像数据集进行了训练。如果您需要在不同的对象上使用模型,您需要微调模型,或者为您的对象从头开始训练模型。描述如何完全从头开始训练模型超出了本书的范围,但是在下一节中,我会提供一些提示,以防您需要这样做。

为你的特定图像训练一个 YOLO 模型

我不会描述你训练你自己的 YOLO 模型所需要的不同程序,因为那本身就需要几个章节,但是我希望我能给你指出正确的方向。让我们假设您想要专门为您的图像训练一个模型。作为第一步,您需要训练数据。假设你有足够多的图片,你首先需要给它们贴上标签。请记住,您需要为每张图像标记正确的边界框。手动完成这项任务几乎是不可能的,因此我建议两个项目来帮助您标记训练数据。

  • BBox-Label-Tool by dark flow Annotations:该工具可以在 https://github.com/enriqueav/BBox-Label-Tool 找到。该工具按照 Darkflow(一个可以使用 darknet 权重文件的 Python 包装器, https://github.com/thtrieu/darkflow )所期望的格式保存注释。

  • labelImg :该工具可以在 https://github.com/tzutalin/labelImg 找到。这个工具可以用于几种 Python 安装(例如,包括 Anaconda)和几种操作系统(包括 Windows)。

如果您想尝试根据您的数据训练您的 YOLO 模型,请查看它们。由于描述整个过程远远超出了本书的范围,我建议你阅读下面这篇中型文章,它很好地描述了如何做到这一点: http://toe.lt/v 。记住,您需要修改一个cfg文件,这样您就可以指定您试图识别的类的正确数量。例如,在yolov3.cfg文件中,您会发现这一行(在第 610 行):

classes=80

它告诉你有多少类你可以用模型来识别。您需要修改这一行来反映问题中的类的数量。

在 YOLO 的官方网站上,有关于如何做的详细描述: https://pjreddie.com/darknet/yolo/ 。向下滚动,直到找到用您自己的数据集训练模型的部分。不要低估这项任务的复杂性。需要大量的阅读和测试。

结束语

您可能已经注意到,使用这些高级技术是相当复杂的,不仅仅是复制几行代码的问题。你需要确保你理解算法是如何工作的,以便能够在你自己的项目中使用它们。根据您需要检测的对象,您可能需要花费相当多的时间来构建适合您的问题的定制模型。这将需要大量的测试和编码。这并非易事。我写这一章的目的是给你足够的工具来帮助你并给你指明正确的方向。

在前面的章节之后,你现在已经对高级技术有了足够的理解,能够自己重新实现像 YOLO 那样复杂的算法,尽管这需要时间和努力。你会遭受很多,但如果你不放弃,你会得到成功的回报。我确信这一点。

在下一章中,我们看一个在真实数据上使用 CNN 的完整例子,在这里我们使用到目前为止学到的所有技术。把第八章当作一个练习。尝试处理数据并重现那里描述的结果。希望你玩得开心!

Footnotes 1

Redmon J .等人《你只看一次:统一的、实时的物体检测》, https://arxiv.org/abs/1506.02640

2

http://host.robots.ox.ac.uk/pascal/VOC/

3

雷德蒙 j .、法尔哈迪 a .,“YOLO9000:更好、更快、更强”, https://arxiv.org/abs/1612.08242

4

雷德蒙·j .、法尔哈迪·a .,“约洛夫 3:一种增量改进”, https://arxiv.org/pdf/1804.02767.pdf

5

您可以在第七章的 GitHub 库中找到用于测试的图像。

6

你可以在 http://toe.lt/t 找到官方文档。

八、组织学组织分类

现在是时候把我们所学的东西放在一起,看看我们到目前为止所学的技术是如何在真实数据集上使用的。我们将使用一个数据集,这个数据集是我在关于深度学习的大学课程中成功使用的最终项目:“结直肠癌组织学中的纹理集合”。 1 这个数据集可以在几个网站找到:

  • http://toe.lt/f : 上zenodo.org

  • http://toe.lt/g :关于 Kaggle(该数据集最初由凯文·马德 2 和我准备,用于我们在苏黎世应用科技大学 2018 年秋季学期举办的大学课程)

  • http://toe.lt/h :从 TensorFlow 2.0 开始,这也可以作为预读数据集使用(链接指向数据集 API 的 TensorFlow GitHub 存储库)

先不要下载数据。我为你准备了一个 pickle (稍后会详细介绍)文件,其中包含所有可以使用的数据。你会在下一部分找到所有的信息。

本章中我们将使用的是Kather_texture_2016_image_tiles_5000文件夹,它包含 5000 张 150 x 150 px(74x 74 μm)的组织学图像。每张图像都属于八个组织类别中的一个类别(由 Zenodo 网站上的文件夹名称指定)。在代码中,我假设在你放 Jupyter 笔记本的文件夹中,有一个data文件夹,在那个data文件夹下,有一个Kather_texture_2016_image_tiles_5000文件夹。

在本书的 GitHub 存储库中,第八章的文件夹包含了您可以使用的完整代码。在这一章中,我们将只看与我们的讨论相关的部分。如果你想试试这个,请使用 GitHub 库。代码是完整的,可以直接使用。这个项目的目标是建立一个分类器,可以将不同的图像分为八类。我们将在接下来的部分中研究它们,看看困难在哪里。像往常一样,让我们从数据开始。

大部分代码是由杨奇煜·塔拉德( https://www.linkedin.com/in/fabientarrade/ )为我的大学课程开发的,他很友好地允许我使用它。我对它进行了相当多的更新,使它可以在这个例子中使用。注意,所有的工作都要感谢杨奇煜,所有的错误都是我的错。

数据分析和准备

这一节的代码包含在名为01- Data exploration and preparation.ipynb的笔记本中,该笔记本位于本书的 GitHub 资源库中的第八章文件夹中。您可以在您的计算机上打开一个窗口来尝试该代码,然后继续讨论。由于我们将图像放在不同的文件夹中,我们需要将它们加载到 pandas 数据框架中,并根据文件夹名称自动生成一个标签。例如,文件夹01_TUMOR中的图像1A11_CRC-Prim-HE-07_022.tif_Row_601_Col_151.tif is contained,因此必须将"TUMOR"作为其标签。

我们可以用一种非常简单的方式来自动化这个过程。我们从这段代码开始(所有的import请查看 GitHub 中的代码):

df = pd.DataFrame({'path': glob(os.path.join(base_dir, '*', '*.tif'))})

这会生成一个只有一列'path'的数据帧。该列包含我们要加载的每个图像的路径。变量base_dir包含了Kather_texture_2016_image_tiles_5000文件夹的路径。例如,我在 Google Colab 中运行代码,我的base_dir看起来像这样:

base_dir = '/content/drive/My Drive/Book2-ch8/data/Kather_texture_2016_image_tiles_5000'

我的数据帧的前五条记录如下所示:

/content/drive/My Drive/Book2-ch8/data/Kather_texture_2016_image_tiles_5000/05_DEBRIS/5434_CRC-Prim-HE-04_002.tif_Row_451_Col_1351.tif
/content/drive/My Drive/Book2-ch8/data/Kather_texture_2016_image_tiles_5000/05_DEBRIS/626A_CRC-Prim-HE-08_024.tif_Row_451_Col_1.tif
/content/drive/My Drive/Book2-ch8/data/Kather_texture_2016_image_tiles_5000/05_DEBRIS/148A7_CRC-Prim-HE-04_004.tif_Row_151_Col_901.tif
/content/drive/My Drive/Book2-ch8/data/Kather_texture_2016_image_tiles_5000/05_DEBRIS/6B37_CRC-Prim-HE-08_024.tif_Row_1501_Col_301.tif
/content/drive/My Drive/Book2-ch8/data/Kather_texture_2016_image_tiles_5000/05_DEBRIS/6B44_CRC-Prim-HE-03_010.tif_Row_301_Col_451.tif

现在我们可以使用.map()函数提取我们需要的所有信息并创建新的列。

df['file_id'] = df['path'].map(lambda x: os.path.splitext(os.path.basename(x))[0])
df['cell_type'] = df['path'].map(lambda x: os.path.basename(os.path.dirname(x)))
df['cell_type_idx'] = df['cell_type'].map(lambda x: int(x.split('_')[0]))
df['cell_type'] = df['cell_type'].map(lambda x: x.split('_')[1])
df['full_image_name'] = df['file_id'].map(lambda x: x.split('_Row')[0])
df['full_image_row'] = df['file_id'].map(lambda x: int(x.split('_')[-3]))
df['full_image_col'] = df['file_id'].map(lambda x: int(x.split('_')[-1]))

你可以很容易地检查每个调用在做什么。列名应该告诉你在每一列中你将有什么。在图 8-1 中,你可以看到目前为止数据帧的前两条记录。

img/470317_1_En_8_Fig1_HTML.jpg

图 8-1

加载图像前数据帧 df 的前两条记录

此时,我们必须用imread()读取图像。为此,我们可以简单地使用

df['image'] = df['path'].map(imread)

请记住,这可能需要一些时间(取决于您在哪里运行它)。这将创建一个名为image的新列,其中将包含图像。为了方便起见,我使用了to_pickle() pandas 调用将数据帧保存到磁盘。酸洗是将 Python 对象层次转换成字节流 3 然后保存到磁盘上的过程。这个文件叫做dataframe_Kather_texture_2016_image_tiles_5000.pkl。您可以加载以下内容:

df=pd.read_pickle('/content/drive/My Drive/Book2-ch8/data/dataframe_Kather_texture_2016_image_tiles_5000.pkl')

这样,你可以节省很多时间。你甚至不需要下载数据,因为你可以简单地使用我为你准备的泡菜。注意,pickles 对于 GitHub 来说太大了,所以我把它们保存在一个服务器上,你可以从那里下载。你可以在 GitHub 和本节末尾找到链接。首先:这个数据集中有哪些类?我们可以用这个代码检查我们的标签:

df['cell_type'].unique()

这将为我们提供以下信息:

array(['DEBRIS', 'ADIPOSE', 'LYMPHO', 'EMPTY', 'STROMA', 'TUMOR',
       'MUCOSA', 'COMPLEX'], dtype=object)

这是我们的八个班级。我们有 5000 张图片,我们可以用这个来检查:

df.shape

它给了我们这个:

(5000, 8)

下一步是检查我们是否有一个平衡的班级分布。我们可以数一数每门课有多少张图片:

df['cell_type'].value_counts()

幸运的是,我们每个班正好有 625 张图片。

EMPTY      625
ADIPOSE    625
STROMA     625
COMPLEX    625
LYMPHO     625
DEBRIS     625
TUMOR      625
MUCOSA     625
Name: cell_type, dtype: int64

奇怪的是,有五个重复的图像。您可以使用以下代码来检查:

df['full_image_name'][df.duplicated('full_image_name')]

这将报告出现两次的图像的名称。你可以在图 8-2 中看到它们。既然只有五个,我们就干脆忽略这个问题。

img/470317_1_En_8_Fig2_HTML.jpg

图 8-2

五幅图像在数据集中出现两次

在图 8-3 中,你可以看到每个类的几个例子。

img/470317_1_En_8_Fig3_HTML.jpg

图 8-3

每个类别中的图像示例

正如所料,每个图像的大小为(150,150,3):

df['image'][0].shape
(150, 150, 3)

请注意这些类是如何排序的,这取决于我们加载数据的方式。首先是DEBRIS类,然后是ADIPOSE,以此类推。如图 8-4 所示,可以使用类别标签与索引的关系图进行检查。

img/470317_1_En_8_Fig4_HTML.jpg

图 8-4

显示数据帧中的图像如何排序的图

现在我们可以随机打乱元素:

import random
rows = df.index.values
random.shuffle(rows)
print(rows)

那会给你

array([1115, 4839, 3684, ...,  187, 1497, 2375])

您可以看到索引现在被随机打乱了。我们需要采取的最后一步是修改实际的数据帧:

df=df.reindex(rows)
df.sort_index(inplace=True)

至此,元素被洗牌。现在我们需要对标签进行一次热编码。熊猫为这一过程提供了一个非常有用且易于使用的方法:

df_label = pd.get_dummies(df['cell_type'])

它会给你一个热编码标签,如图 8-5 所示。

img/470317_1_En_8_Fig5_HTML.jpg

图 8-5

使用 get_dummies() pandas 函数对标签进行一次热编码的结果

在 Keras 中使用数据需要几个步骤。一是我们需要将数据帧转换成 numpy 数组:

data=np.array(df['image'].tolist())

然后,像往常一样,我们需要创建一个培训、测试和开发数据集来进行所有常规检查:

x, x_test, y, y_test = train_test_split(data, label, test_size=0.2,train_size=0.8)
x_train, x_val, y_train, y_val = train_test_split(x, y, test_size = 0.25,train_size =0.75)

您可以使用以下代码轻松检查三个数据集的维度:

print('1- Training set:', x_train.shape, y_train.shape)
print('2- Validation set:', x_val.shape, y_val.shape)
print('3- Testing set:', x_test.shape, y_test.shape)

这将为您提供以下内容:

1- Training set: (3000, 150, 150, 3) (3000, 8)
2- Validation set: (1000, 150, 150, 3) (1000, 8)
3- Testing set: (1000, 150, 150, 3) (1000, 8)

现在,您将看到数据的类型是 integer。我们需要将它们转换成浮点数,因为我们希望以后对它们进行规范化。为此,我们使用以下代码:

x_train = np.array(x_train, dtype=np.float32)
x_test = np.array(x_test, dtype=np.float32)
x_val = np.array( x_val, dtype=np.float32)

然后我们可以标准化数据集(记住每个像素的最大值是 255):

x_train /= 255.0
x_test /= 255.0
x_val /= 255.0

为了您的方便,我将所有准备好的数据集保存为 pickles。如果您想从这里开始使用数据,您需要使用以下命令加载 pickles(您需要更改保存文件的文件夹名称):

x_train=pickle.load(open('/content/drive/My Drive/Book2-ch8/data/x_train.pkl', 'rb'))
x_test=pickle.load(open('/content/drive/My Drive/Book2-ch8/data/x_test.pkl', 'rb'))
x_val=pickle.load(open('/content/drive/My Drive/Book2-ch8/data/x_val.pkl', 'rb'))
y_train=pickle.load(open('/content/drive/My Drive/Book2-ch8/data/y_train.pkl', 'rb'))
y_test=pickle.load(open('/content/drive/My Drive/Book2-ch8/data/y_test.pkl', 'rb'))
y_val=pickle.load(open('/content/drive/My Drive/Book2-ch8/data/y_val.pkl', 'rb'))

然后你会准备好一切。请记住,包含数据的文件(x_trainx_testx_val)是大文件,其中x_train被解压缩为 800MB。如果你打算下载这些文件或者把它们上传到你的 Google drive 上,请记住这一点。当然,您需要更改保存数据的文件夹。这会节省你的时间。通常会保存 Pickles,因为您不想在每次试验数据时都重新运行整个数据准备过程。在01- Data explorationpreparation.ipynb文件中,你还会发现一些直方图分析和数据扩充的例子。出于篇幅原因,为了保持本章简洁,我们将不讨论直方图分析,但我们将在本章后面讨论数据扩充,因为这是一种非常有效的对抗过度拟合的方法。

文件对 GitHub 来说太大了,所以我把它们放在了一个服务器上,你可以在那里下载。在 GitHub 资源库(第章第 8 文件夹)中,你会找到所有的信息。如果您无法访问 GitHub,但仍想下载文件,以下是链接:

  • dataframe_Kather_texture_201_image_tiles_5000。pkl (340MB 解压后): http://toe.lt/j

  • x_test.pkl (270MB 解压后): http://toe.lt/k

  • x_train.pkl (810MB 解压后): http://toe.lt/m

  • x_val.pkl (270MB 解压后): http://toe.lt/n

  • y_trainy_testy_val(全部压缩在一起)(解压后约 50KB):http://toe.lt/p

模型结构

是时候建立一些模型了。你会在本书的 GitHub 资源库中找到所有代码(第章第 8 文件夹,在02_Model_building.ipynb笔记本中),所以我们不会在这里查看所有细节。最好的方法是打开笔记本,在阅读本文的同时尝试代码。如前所述,我们首先需要加载 pickle 文件。我们可以用下面的代码做到这一点:

x_train=pickle.load(open(base_dir+'x_train.pkl', 'rb'))
x_test=pickle.load(open(base_dir+'x_test.pkl', 'rb'))
x_val=pickle.load(open(base_dir+'x_val.pkl', 'rb'))
y_train=pickle.load(open(base_dir+'y_train.pkl', 'rb'))
y_test=pickle.load(open(base_dir+'y_test.pkl', 'rb'))
y_val=pickle.load(open(base_dir+'y_val.pkl', 'rb'))

然后我们需要定义 CNN 需要的input_shape变量。在代码中,我们总是定义返回 Keras 模型的函数。例如,我们的第一次尝试是这样的:

def model_cnn_v1():

    # must define the input shape in the first layer of the neural network
    model = tf.keras.models.Sequential()
    model.add(tf.keras.layers.Conv2D(32, 3, 3, input_shape=input_shape))
    model.add(tf.keras.layers.Activation('relu'))
    model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2)))

    model.add(tf.keras.layers.Conv2D(64, 3, 3))
    model.add(tf.keras.layers.Activation('relu'))
    model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2)))

    model.add(tf.keras.layers.Flatten())
    model.add(tf.keras.layers.Dense(64))
    model.add(tf.keras.layers.Activation('relu'))
    model.add(tf.keras.layers.Dropout(0.5))
    model.add(tf.keras.layers.Dense(8))
    model.add(tf.keras.layers.Activation('sigmoid'))

    model.compile(loss='categorical_crossentropy',
                  optimizer='adam',
                  metrics=['accuracy'])
    return model

这是一个简单的网络,您可以使用summary()功能进行检查:

_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
conv2d (Conv2D)              (None, 50, 50, 32)        896
_________________________________________________________________
activation (Activation)      (None, 50, 50, 32)        0
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 25, 25, 32)        0
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 8, 8, 64)          18496
_________________________________________________________________
activation_1 (Activation)    (None, 8, 8, 64)          0
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 4, 4, 64)          0
_________________________________________________________________
flatten (Flatten)            (None, 1024)              0
_________________________________________________________________
dense (Dense)                (None, 64)                65600
_________________________________________________________________
activation_2 (Activation)    (None, 64)                0
_________________________________________________________________
dropout (Dropout)            (None, 64)                0
_________________________________________________________________
dense_1 (Dense)              (None, 8)                 520
_________________________________________________________________
activation_3 (Activation)    (None, 8)                 0
=================================================================
Total params: 85,512
Trainable params: 85,512
Non-trainable params: 0
_________________________________________________________________

为了确保会话被重置,我们总是使用:

tf.keras.backend.clear_session()

然后我们创建模型的一个实例,如下所示:

model_cnn_v1=model_cnn_v1()

然后,我们还保存初始重量,以确保如果我们稍后运行,我们从这些相同的重量开始:

initial_weights = model_cnn_v1.get_weights()

然后我们用这个来训练模型:

model_cnn_v1.set_weights(initial_weights)

# define path to save the mnodel
path_model=base_dir+'model_cnn_v1.weights.best.hdf5'
shutil.rmtree(path_model, ignore_errors=True)

checkpointer = ModelCheckpoint(filepath=path_model,
                               verbose = 1,
                               save_best_only=True)
EPOCHS=200
BATCH_SIZE=256

history=model_cnn_v1.fit(x_train,
                         y_train,
                         batch_size=BATCH_SIZE,
                         epochs=EPOCHS,
                         validation_data=(x_test, y_test),
                         callbacks=[checkpointer])

请注意以下几点:

  • 我们创建一个定制的回调类ModelCheckpoint,它将在每次损失函数减小时保存训练期间网络的权重。

  • 我们使用fit()调用训练网络,并将其输出保存在history变量中,以便能够在以后绘制损耗和指标。

注意

如果你在笔记本电脑或台式机上训练这样的网络可能会非常慢,这取决于你所拥有的硬件。我强烈建议你在 Google Colab 上这样做,因为这会加快你的测试速度。该书 GitHub 资源库中的所有笔记本都已经在 Google Colab 上进行了测试,可以直接从 GitHub 在 Google Colab 中打开。

在 Google Colab 上,训练之前的网络大约需要三分钟。它将达到以下精度:

  • 训练数据集的准确率:85%

  • 验证数据集上的准确率:82.7%

这些结果还不错,我们也没有太多的过拟合(你可以在图 8-6 中看到精度和损耗是如何随着历元变化的)。

img/470317_1_En_8_Fig6_HTML.jpg

图 8-6

文中描述的第一个网络的精度和损失函数

让我们来看一个不同的模型,我们称之为v2。这个比之前的有更多的参数:

_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
conv2d (Conv2D)              (None, 150, 150, 128)     9728
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 75, 75, 128)       0
_________________________________________________________________
dropout (Dropout)            (None, 75, 75, 128)       0
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 75, 75, 64)        73792
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 37, 37, 64)        0
_________________________________________________________________
dropout_1 (Dropout)          (None, 37, 37, 64)        0
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 37, 37, 64)        36928
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 18, 18, 64)        0
_________________________________________________________________
dropout_2 (Dropout)          (None, 18, 18, 64)        0
_________________________________________________________________
flatten (Flatten)            (None, 20736)             0
_________________________________________________________________
dense (Dense)                (None, 256)               5308672
_________________________________________________________________
dense_1 (Dense)              (None, 64)                16448
_________________________________________________________________
dense_2 (Dense)              (None, 32)                2080
_________________________________________________________________
dense_3 (Dense)              (None, 8)                 264
=================================================================
Total params: 5,447,912
Trainable params: 5,447,912
Non-trainable params: 0
_________________________________________________________________

同样,您可以在 GitHub 资源库中找到所有代码。我们将再次对其进行训练,但这一次,由于时间的原因,将训练 50 个历元,并且批次大小稍小,为 64。

EPOCHS=50
BATCH_SIZE=64

history=model_cnn_v2.fit(x_train,
                         y_train,
                         batch_size=BATCH_SIZE,
                         epochs=EPOCHS,
                         validation_data=(x_test, y_test),
                         callbacks=[checkpointer])

否则,一切照旧。这一次,由于大量的参数,您会注意到我们得到了一个明显的过度拟合。事实上,我们得到了以下精度:

  • 训练数据集上的准确率:99.5%

  • 验证数据集的准确率:74%

在图 8-7 中,您可以清楚地看到过拟合,查看精度与周期数的关系图。

img/470317_1_En_8_Fig7_HTML.jpg

图 8-7

v2 网络的精度和损失函数与历元数的关系

我们需要做更多的工作来获得更合理的结果。现在让我们使用一个参数更少的网络(特别是内核更少的网络):

_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
conv2d (Conv2D)              (None, 150, 150, 16)      448
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 150, 150, 16)      2320
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 150, 150, 16)      2320
_________________________________________________________________
dropout (Dropout)            (None, 150, 150, 16)      0
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 50, 50, 16)        0
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 50, 50, 32)        4640
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 50, 50, 32)        9248
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 50, 50, 32)        9248
_________________________________________________________________
dropout_1 (Dropout)          (None, 50, 50, 32)        0
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 16, 16, 32)        0
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 16, 16, 64)        18496
_________________________________________________________________
conv2d_7 (Conv2D)            (None, 16, 16, 64)        36928
_________________________________________________________________
conv2d_8 (Conv2D)            (None, 16, 16, 64)        36928
_________________________________________________________________
dropout_2 (Dropout)          (None, 16, 16, 64)        0
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 5, 5, 64)          0
_________________________________________________________________
conv2d_9 (Conv2D)            (None, 5, 5, 128)         73856
_________________________________________________________________
conv2d_10 (Conv2D)           (None, 5, 5, 128)         147584
_________________________________________________________________
conv2d_11 (Conv2D)           (None, 5, 5, 256)         295168
_________________________________________________________________
dropout_3 (Dropout)          (None, 5, 5, 256)         0
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 1, 1, 256)         0
_________________________________________________________________
global_max_pooling2d (Global (None, 256)               0
_________________________________________________________________
dense (Dense)                (None, 8)                 2056
=================================================================
Total params: 639,240
Trainable params: 639,240
Non-trainable params: 0

我们将称这个网络为v3。这一次,情况也好不到哪里去,如图 8-8 所示。

img/470317_1_En_8_Fig8_HTML.jpg

图 8-8

精度和损失函数与。v3 网络的纪元数量。

我们为什么不利用目前所学的知识呢?我们用迁移学习,看看能不能用一个预先训练好的网络。让我们下载VGG16网络并用我们的数据重新训练最后几层。为此,我们需要使用下面的代码(我们称这个网络为vgg-v4):

def model_vgg16_v4():

    # load the VGG model
    vgg_conv = tf.keras.applications.VGG16(weights='imagenet', include_top=False, input_shape = input_shape)

    # freeze the layers except the last 4 layers
    for layer in vgg_conv.layers[:-4]:
          layer.trainable = False

    # Check the trainable status of the individual layers
    for layer in vgg_conv.layers:
        print(layer, layer.trainable)

    # create the model
    model = tf.keras.models.Sequential()

    # add the vgg convolutional base model
    model.add(vgg_conv)

    # add new layers
    model.add(tf.keras.layers.Flatten())
    model.add(tf.keras.layers.Dense(1024, activation="relu"))
    model.add(tf.keras.layers.Dropout(0.5))
    model.add(tf.keras.layers.Dense(8, activation="softmax"))

    model.compile(loss='categorical_crossentropy',
                  optimizer='adam',
                  metrics=['accuracy'])

    return model

请注意我们是如何下载预先训练好的网络(正如我们在前面章节中看到的)的代码:

vgg_conv = tf.keras.applications.VGG16(weights='imagenet', include_top=False, input_shape = input_shape)

我们使用了include_top=False参数,因为我们想要移除最后的密集层,并在它们的位置放置我们自己的层。我们在最后添加一个有 1024 个神经元的层:

model.add(tf.keras.layers.Dense(1024, activation="relu"))

然后我们添加一个输出层,用8作为分类的softmax激活函数:

model.add(tf.keras.layers.Dense(8, activation="softmax"))

summary()通话将为您提供以下概述:

_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
vgg16 (Model)                (None, 4, 4, 512)         14714688
_________________________________________________________________
flatten (Flatten)            (None, 8192)              0
_________________________________________________________________
dense (Dense)                (None, 1024)              8389632
_________________________________________________________________
dropout (Dropout)            (None, 1024)              0
_________________________________________________________________
dense_1 (Dense)              (None, 8)                 8200
=================================================================
Total params: 23,112,520
Trainable params: 15,477,256
Non-trainable params: 7,635,264
_________________________________________________________________

整个vgg16网络浓缩成一条线(vgg16 (Model))。在这个网络中,我们有 15'477'256 个可训练参数。相当多。事实上,在 Google Colab 上训练这个网络 30 个纪元需要大约 11 分钟。你可以在图 8-9 中看到精度和损耗是如何随着历元数而变化的。

img/470317_1_En_8_Fig9_HTML.jpg

图 8-9

vgg-v4 网络的精度和损失函数与历元数的关系

如您所见,情况有所改善,但我们仍然过度适应。没有之前那么戏剧化,但还是挺引人注目的。我们唯一能与之对抗的策略就是数据增强。在接下来的章节中,我们将看到在 Keras 中进行数据扩充是多么容易,以及它所带来的影响。

日期增加

对抗过度拟合的一个显而易见的策略(尽管在现实生活中很少可行)是获取更多的训练数据。在我们这里,这是不可能的。给出的图像是唯一可用的。但是这种情况下我们还是可以做一些事情:数据增强。我们这样说到底是什么意思?通常,数据扩充包括通过对现有图像应用某种变换来从现有图像生成新图像,并将它们用作额外的训练数据。

注意

数据扩充包括通过对现有图像应用某种变换来从现有图像生成新图像,并将它们用作额外的训练数据。

最常见的转换如下:

  • 将图像水平或垂直移动一定数量的像素

  • 旋转图像

  • 改变它的亮度

  • 更改缩放比例

  • 改变对比度

  • 剪切图像4

让我们看看如何在 Keras 中进行数据扩充,并看看数据集中的几个例子。我们需要用到的函数是ImageDataGenerator。首先,您需要从keras_preprocessing.image导入它:

from keras_preprocessing.image import ImageDataGenerator

请注意,该功能不会生成新图像并将它们保存到磁盘,但会在训练期间以随机方式为您及时创建增强图像数据(稍后将会清楚如何使用它)。这不需要太多额外的内存,但会增加模型训练的时间。这个函数可以做很多转换,发现它们的最好方法是查看 https://keras.io/preprocessing/image/ 的官方文档。我们会用例子来看最重要的。

水平和垂直移动

要水平和垂直移动图像,可以使用以下代码:

datagen = ImageDataGenerator(width_shift_range=.2,
                             height_shift_range=.2,
                             fill_mode='nearest')

# fit parameters from data
datagen.fit(x_train)

结果如图 8-10 中的几张随机图像所示。

img/470317_1_En_8_Fig10_HTML.jpg

图 8-10

水平和垂直移动图像的结果

如果你检查图像,你会注意到边界处出现了奇怪的特征。因为我们要移动图像,所以我们需要告诉 Keras 如何填充图像中的空白部分。考虑图 8-11 ,这里我们水平移动图像。您可能会注意到,图像中用 A 标记的部分仍然是空的,我们可以使用fill_mode参数告诉 Keras 如何填充该部分。

img/470317_1_En_8_Fig11_HTML.jpg

图 8-11

在水平方向上移动图像的例子。A 标记了结果图像中将保持空白的部分。

理解fill_mode不同可能性的最佳方式是考虑一维情况。解释来自该函数的官方文档。假设我们有一组四个像素,这些像素有一些值,我们用 a、b、c 和 d 表示。假设我们有需要填充的边界。需要填充的部分标有o。图 8-12 显示了四种可能性的图形解释:常量、最近、反射和环绕。

img/470317_1_En_8_Fig12_HTML.jpg

图 8-12

fill_mode参数的可能值和可能性的图形解释

图 8-11 中的图像是使用nearest填充模式生成的。虽然这种变换引入了人工特征,但使用这些额外的图像进行训练可以提高模型的准确性,并非常有效地防止过度拟合,这一点我们将在本章后面看到。最常见的填充空零件的方法是nearest

垂直翻转图像

要垂直翻转图像,可以使用以下代码:

datagen = ImageDataGenerator(vertical_flip=True)

# fit parameters from data
datagen.fit(x_train)

随机旋转图像

您可以使用以下代码随机旋转图像:

datagen = ImageDataGenerator(rotation_range=40, fill_mode = 'constant')

# fit parameters from data
datagen.fit(x_train)

而且,与移位变换一样,您可以选择不同的方式来填充空白区域。你可以在图 8-13 中看到这段代码的效果。

img/470317_1_En_8_Fig13_HTML.jpg

图 8-13

将图像沿随机方向旋转最多 40 度的效果(旋转量随机选择,最多 40 度)。图像中因旋转而留下的空白部分已经用常数值填充。

在图 8-14 中,你可以看到填充fill_mode = 'nearest'后的旋转效果。通常,这是填充图像的首选方式,以避免将图像的黑色(或纯色)部分提供给网络。

img/470317_1_En_8_Fig14_HTML.jpg

图 8-14

将图像随机旋转 40 度的效果。旋转留下的空白图像部分已经用最近的模式填充。

放大图像

您现在应该明白这些图像转换是如何工作的了。缩放与之前的转换一样简单:

datagen = ImageDataGenerator(zoom_range=0.2)

# fit parameters from data
datagen.fit(x_train)

把所有的放在一起

Keras 的一个优点是,您不需要一次执行一个转换。你可以一蹴而就。例如,考虑以下代码:

datagen = ImageDataGenerator(rotation_range=40,
                             width_shift_range=0.2,
                             height_shift_range=0.2,
                             shear_range=0.2,
                             zoom_range=0.2,
                             horizontal_flip=True,
                             fill_mode="nearest")

这将极大地增强您的数据集,同时完成几个转换:

  • 循环

  • 变化

  • 大剪刀

  • 一款云视频会议软件

  • 翻转

让我们把所有的东西放在一起,看看这个技术有多有效。

具有数据增强功能的 VGG16

现在是时候用迁移学习和图像增强来训练我们的vgg16网络了。对我们之前看到的代码的唯一修改是我们如何输入数据来训练模型。

现在我们需要使用以下代码:

history=model_vgg16_v4.fit_generator(datagen.flow(x_train, y_train, batch_size=BATCH_SIZE),
                                     validation_data=(x_test, y_test),
                                     epochs=EPOCHS,
                                     callbacks=[checkpointer])

代替经典的fit()调用,我们需要使用fit_generator()。为了解释这两个函数之间的主要区别,有必要稍微离题一下。Keras 包括不是两个而是三个可用于训练模型的函数:

  • fit()

  • fit_generator()

  • train_on_batch()

fit()函数

到目前为止,我们在训练我们的 Keras 模型时使用了fit()函数。使用此方法时,主要的隐含假设是您提供给模型的数据集将完全适合内存。我们不需要将批处理移入和移出内存。这是一个相当大的假设,尤其是如果你正在处理大数据集,而你的笔记本电脑或台式机没有太多的可用内存。此外,假设不需要进行实时数据扩充(正如我们在这里想要做的)。

注意

fit()函数适用于可以放入系统内存且不需要实时数据扩充的小型数据集。

函数的作用是

当数据不再适合内存时,我们需要一个更智能的函数来帮助我们处理它。请注意,我们之前创建的ImageDataGenerator将以随机的方式生成需要提供给模型的批次。fit_generator()函数假设有一个函数为它生成数据。使用fit_generator()时,Keras 遵循以下流程:

  1. Keras 调用生成批处理的函数。在我们的代码中,那是datagen.flow()

  2. 这个生成器函数返回一个批处理,其大小由参数batch_size=BATCH_SIZE指定。

  3. 然后,fit.generator()函数执行反向传播并更新权重。

  4. 这一过程一直重复,直到达到所需的历元数。

注意

fit_generator()函数旨在用于不适合内存的较大数据集,以及当您需要进行数据扩充时。

注意,在我们的代码中有一个重要的参数没有使用:steps_per_epochdatagen.flow()函数每次都会生成一批图像,但是 Keras 需要知道我们每个时期需要多少批图像,因为datagen.flow()可以继续生成我们需要的数量(记住它们是随机生成的)。我们需要决定在宣布每个时期结束之前需要多少批次。可以用steps_per_epoch参数决定,但是如果不指定,Keras 会用len(generator) 5 作为步数。

函数的作用是

如果您需要微调您的训练,可以使用train_on_batch()功能。

注意

train_on_batch()函数接受一批数据,执行反向传播,然后更新模型参数。

该批数据可以任意调整大小,理论上可以是您需要的任何格式。例如,当您需要执行标准 Keras 函数无法完成的自定义数据扩充时,您需要这个函数。

注意

正如他们所说——如果你不知道你是否需要train_on_batch()函数,你可能不需要。

您可以在 https://keras.io/models/sequential/ 的官方文档中找到更多信息。

训练网络

我们终于可以训练我们的网络,看看它表现如何。对其进行 50 个时期的训练,批次大小为 128,得出以下准确度:

  • 训练数据集上的准确率:93.3%

  • 验证数据集的准确率:91%

这是一个伟大的结果。实际上没有过拟合和高精度。这个网络在 Google Colab 上花了大约 15 分钟,相当快。图 8-15 显示了精度和损耗与历元数的关系。

img/470317_1_En_8_Fig15_HTML.jpg

图 8-15

具有迁移学习和数据扩充的 VGG16 网络的准确度和损失函数与历元数的关系

总的来说,我们从一个简单的 CNN 开始,这个 CNN 还不算太差,但是我们很快意识到越深入(更多层)和增加复杂性(更多内核)会导致过度拟合。增加辍学并没有真正的帮助,所以唯一的解决方案是使用数据增强。

请注意,由于篇幅原因,我们没有展示本章中描述的第一个具有数据扩充功能的网络,但是您应该这样做。如果你尝试,你会意识到你非常有效地对抗过度拟合,但是精度下降了。使用预先训练的网络给了我们一个非常好的起点,并允许我们在几个时期内进入 90%的准确度范围。

现在玩得开心点…

在这本书里,你学到了强大的技术,可以让你阅读研究论文,理解它们,并开始实现更先进的网络,超越你在博客和网站上找到的简单的 CNN。我希望你喜欢这本书,它将帮助你走向深度学习的掌握。深度学习真的很有趣,是一个非常有创造力的研究领域。我希望你现在对算法的可能性和其中的创造性有所了解。我喜欢反馈,也希望收到您的反馈。不要犹豫,联系我,告诉我这本书是如何(尤其是如果)帮助你学习那些算法的。

翁贝托·米其奇,杜本多夫,2019 年 6 月

Footnotes 1

Kather JN,Weis CA,比安科尼 F,Melchers SM,Schad LR,Gaiser T,Marx A,Zollner F:结肠直肠癌组织学中的多级纹理分析 (2016),科学报告(正在出版中)

2

https://www.linkedin.com/in/kevinmader/

3

来自 Python 官方文档: https://docs.python.org/2/library/pickle.html

4

在平面几何中,剪切映射是一个线性映射,它在一个固定的方向上移动每个点,移动的量与它与平行于该方向并通过原点的直线的有符号距离成比例。 https://en.wikipedia.org/wiki/Shear_mapping见。

5

https://keras.io/models/sequential/