Caffe2-快速启动指南-全-
Caffe2 快速启动指南(全)
原文:
annas-archive.org/md5/4b5aa5b3c00e280042288d19c365220f译者:飞龙
序言
Caffe2 是一个流行的深度学习框架,专注于可伸缩性、高性能和可移植性。它用 C++编写,提供了 C++ API 和 Python API。本书是你快速入门 Caffe2 的指南。内容包括安装 Caffe2,使用其操作符组合网络,训练模型以及将模型部署到推理引擎、边缘设备和云端。我们还将展示如何使用 ONNX 交换格式在 Caffe2 和其他深度学习框架之间进行工作。
本书适合读者
数据科学家和机器学习工程师希望在 Caffe2 中创建快速且可扩展的深度学习模型,将会发现本书非常有用。
本书内容
第一章,介绍和安装,介绍了 Caffe2,并探讨了如何构建和安装它。
第二章,网络组合,教你如何使用 Caffe2 操作符以及如何组合它们来构建简单的计算图和神经网络,用于识别手写数字。
第三章,网络训练,介绍了如何使用 Caffe2 来组合一个用于训练的网络,以及如何训练解决 MNIST 问题的网络。
第四章,与 Caffe 一起工作,探索了 Caffe 与 Caffe2 之间的关系,以及如何处理在 Caffe 中训练的模型。
第五章,与其他框架一起工作,探讨了 TensorFlow 和 PyTorch 等当代深度学习框架,以及如何在 Caffe2 和这些框架之间交换模型。
第六章,将模型部署到加速器进行推理,讨论了推理引擎及其在训练后将 Caffe2 模型部署到加速器上的关键工具。我们专注于两种流行的加速器类型:NVIDIA GPU 和 Intel CPU。我们将探讨如何安装和使用 TensorRT 来在 NVIDIA GPU 上部署我们的 Caffe2 模型。同时,我们还将介绍如何安装和使用 OpenVINO 来在 Intel CPU 和加速器上部署我们的 Caffe2 模型。
第七章,边缘设备和云中的 Caffe2,展示了 Caffe2 的两个应用场景,以展示其扩展能力。作为 Caffe2 在边缘设备中的应用,我们将介绍如何在树莓派单板计算机上构建 Caffe2,并在其上运行 Caffe2 应用程序。作为 Caffe2 在云中的应用,我们将探讨如何在 Docker 容器中使用 Caffe2。
为了充分利用本书
一些基本机器学习概念的理解和对 C++和 Python 等编程语言的先前接触将会很有帮助。
下载示例代码文件
您可以从您的帐户在www.packt.com下载本书的示例代码文件。如果您是在其他地方购买的本书,您可以访问www.packt.com/support并注册,以便直接通过邮件收到文件。
您可以按照以下步骤下载代码文件:
-
在www.packt.com登录或注册。
-
选择“支持”选项卡。
-
点击“代码下载与勘误”。
-
在搜索框中输入书名并按照屏幕上的指示操作。
一旦文件下载完成,请确保使用最新版本的工具解压或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
本书的代码包也托管在 GitHub 上,地址为github.com/PacktPublishing/Caffe2-Quick-Start-Guide。如果代码有更新,它将在现有的 GitHub 仓库中进行更新。
我们还有来自我们丰富书籍和视频目录的其他代码包,您可以在github.com/PacktPublishing/找到它们。赶紧去看看吧!
使用的约定
本书中使用了多种文本约定。
CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账号。例如:“SoftMax函数的输出值具有良好的性质。”
一块代码按如下方式呈现:
model = model_helper.ModelHelper("MatMul model")
model.net.MatMul(["A", "B"], "C")
任何命令行输入或输出都按如下方式书写:
$ sudo apt-get update
粗体:表示一个新术语、重要单词或您在屏幕上看到的词语。例如,菜单或对话框中的词语会以这种方式出现在文本中。举个例子:“例如,在 Ubuntu 上,它给了我一个下载可自定义包或单一大包的选项。”
警告或重要说明以这种方式出现。
小贴士和技巧以这种方式出现。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何部分有疑问,请在邮件主题中提及书名,并通过customercare@packtpub.com联系我们。
勘误:尽管我们已经尽力确保内容的准确性,但错误还是会发生。如果您在本书中发现错误,我们将非常感激您向我们报告。请访问www.packt.com/submit-errata,选择您的书籍,点击“勘误提交表单”链接,并输入详细信息。
盗版:如果您在互联网上发现任何形式的我们作品的非法复制品,我们将非常感激您提供该位置地址或网站名称。请通过copyright@packt.com与我们联系,并附上材料链接。
如果你有兴趣成为作者:如果你在某个领域有专业知识,并且有兴趣撰写或为书籍做贡献,请访问 authors.packtpub.com。
书评
请留下书评。在阅读并使用本书之后,为什么不在你购买该书的网站上留下评论呢?潜在的读者可以看到并参考你客观的意见来做出购买决策,我们在 Packt 可以了解你对我们产品的看法,而我们的作者也可以看到你对他们书籍的反馈。谢谢!
有关 Packt 的更多信息,请访问 packt.com。
第一章:介绍与安装
欢迎阅读 Caffe2 快速入门指南。本书旨在为您提供 Caffe2 深度学习框架的快速入门,并指导您如何使用它来训练和部署深度学习模型。本书通过代码示例创建、训练并运行推理,解决实际问题。通过这种方式,读者可以快速将本书中的代码应用到自己的应用程序中。
本章简要介绍 Caffe2,并展示如何在您的计算机上构建和安装它。本章将涵盖以下内容:
-
深度学习和 Caffe2 介绍
-
构建和安装 Caffe2
-
测试 Caffe2 Python API
-
测试 Caffe2 C++ API
深度学习简介
如 人工智能 (AI)、机器学习 (ML) 和 深度学习 (DL) 等术语目前非常流行。这种流行可以归因于深度学习技术在过去几年中带来了显著的进展,使计算机能够“看”、“听”、“读”并“创造”。首先,我们将介绍这三个领域及其交集:

图 1.1:深度学习、机器学习和人工智能之间的关系
人工智能
人工智能 (AI) 是一个通用术语,用来指代计算机的智能,特别是它们推理、感知、认知和响应的能力。它用来指代任何具备智能的非生物系统,而这种智能是由一套规则的结果。无论这些规则是由人类手动创建,还是计算机通过分析数据自动学习得到,都不影响 AI 的定义。AI 的研究始于 1956 年,此后经历了许多起伏和几次低谷,称为 AI 冬天。
ML
机器学习 (ML) 是 AI 的一个子集,利用统计学、数据和学习算法教计算机从给定的数据中学习。这些数据被称为 训练数据,它是针对特定问题而收集的,包含每个输入及其预期输出的示例。ML 算法通过训练数据自动学习模型或表示,这些模型可用于对新的输入数据进行预测。
机器学习中有许多流行的模型类型,包括 人工神经网络 (ANNs)、贝叶斯网络、支持向量机 (SVM) 和随机森林。本书中我们关注的 ML 模型是人工神经网络(ANN)。ANN 的结构灵感来自大脑中的连接。这些神经网络模型最初在机器学习中非常流行,但后来由于当时计算能力的不足,它们逐渐被冷落。
深度学习
在过去的十年中,利用图形处理单元(GPU)的并行处理能力来解决一般计算问题变得流行。这种类型的计算被称为通用 GPU 计算(GPGPU)。GPU 价格相对较低,并且通过使用如CUDA和OpenCL等 GPGPU 编程模型和 API 作为加速器使用非常方便。从 2012 年开始,神经网络研究人员利用 GPU 训练具有大量层次的神经网络,并开始在计算机视觉、语音识别和其他问题的解决方面取得突破。具有多层神经元的深度神经网络的使用,产生了深度学习这一术语。深度学习算法是机器学习的一个子集,利用多层抽象来学习和参数化数据的多层神经网络模型。
Caffe2 介绍
深度学习的普及和成功受到许多流行和开源深度学习框架的推动,这些框架可用于神经网络的训练和推断。Caffe是最早流行的深度学习框架之一。它由贾扬清在加州大学伯克利分校为其博士论文创建,并于 2013 年底向公众发布。主要用 C++编写,并提供了 C++ API。Caffe 还提供了一个基本的 Python API,封装了 C++ API。Caffe 框架通过层来创建网络。用户通过在文本文件(通常称为prototxt)中列出和描述其层来创建网络。
随着 Caffe 的流行,许多大学、公司和个人创建并推出了许多深度学习框架。如今一些流行的框架包括 Caffe2、TensorFlow、MXNet 和 PyTorch。TensorFlow 由 Google 推动,MXNet 得到了亚马逊的支持,而 PyTorch 主要由 Facebook 开发。
Caffe 的创始人贾扬清后来加入了 Facebook,在那里创建了 Caffe 的后续版本——Caffe2。与其他深度学习框架相比,Caffe2 专注于可伸缩性、高性能和可移植性。它用 C++编写,同时具有 C++ API 和 Python API。
Caffe2 和 PyTorch
Caffe2 和 PyTorch 都是流行的深度学习框架,由 Facebook 维护和推动。PyTorch 源自Torch深度学习框架。它以 Python API 为特点,易于设计不同的网络结构,并在其上进行训练参数和方案的实验。虽然 PyTorch 可以用于云端和边缘上的生产应用的推断,但在这方面效率不及。
Caffe2 具有 Python API 和 C++ API。它设计供那些对现有网络结构进行调试,并使用来自 PyTorch、Caffe 和其他 DL 框架的预训练模型,并准备将其部署到应用程序、本地工作站、边缘低功耗设备、移动设备和云中的从业者使用。
观察到 PyTorch 和 Caffe2 的互补特性后,Facebook 计划合并这两个项目。正如我们稍后将看到的那样,Caffe2 的源代码已经组织在 PyTorch 的 Git 存储库的一个子目录下。未来,预计这两个项目将更加融合,最终目标是将它们融合成一个单一的深度学习框架,易于实验和调试,高效训练和部署,并能够从云端到边缘,从通用处理器到特定加速器进行扩展。
硬件要求
使用深度学习模型,特别是训练过程,需要大量的计算能力。尽管您可以在 CPU 上训练流行的神经网络,但这通常需要数小时甚至数天,具体取决于网络的复杂性。强烈建议使用 GPU 进行训练,因为与 CPU 相比,GPU 通常能将训练时间缩短一个数量级或更多。Caffe2 使用 CUDA 来访问 NVIDIA GPU 的并行处理能力。CUDA 是一个 API,使开发者能够利用 NVIDIA GPU 的并行计算能力,因此您需要使用 NVIDIA GPU。您可以在本地计算机上安装 NVIDIA GPU,也可以使用像 Amazon AWS 这样的云服务提供商,提供带有 NVIDIA GPU 的实例。在长时间训练之前,请注意这些云实例的运行成本。
一旦您使用 Caffe2 训练了一个模型,您可以使用 CPU、GPU 或许多其他处理器进行推断。我们将在本书的第六章,将模型部署到加速器进行推断,和第七章,Caffe2 在边缘和云端的应用中探讨几种这样的选项。
软件要求
深度学习研究和开发的主要部分目前在 Linux 计算机上进行。Ubuntu是一种 Linux 发行版,非常受深度学习研究和开发者欢迎。在本书中,我们将选择 Ubuntu 作为操作系统。如果您使用不同版本的 Linux,您应该能够在线搜索到类似于 Ubuntu 命令的命令,用于执行描述的大多数操作。如果您使用 Windows 或 macOS,您需要用相应的命令替换本书中的 Linux 命令。所有的代码示例都应该在 Linux、Windows 和 macOS 上无需或只需做很小的修改就能运行。
构建和安装 Caffe2
Caffe2 可以很容易地从源代码构建和安装。从源代码安装 Caffe2 可以为我们提供更多的灵活性和对应用程序设置的控制。构建和安装过程分为四个阶段:
-
安装依赖项
-
安装加速库
-
构建 Caffe2
-
安装 Caffe2
安装依赖项
我们首先需要安装 Caffe2 所依赖的包,以及构建它所需的工具和库。
- 首先,通过使用
apt-get工具查询 Ubuntu 在线仓库,获取关于最新版本的 Ubuntu 软件包的信息:
$ sudo apt-get update
- 接下来,使用
apt-get工具,安装构建 Caffe2 所需的库,以及 Caffe2 运行时所需的库:
$ sudo apt-get install -y --no-install-recommends \
build-essential \
cmake \
git \
libgflags2 \
libgoogle-glog-dev \
libgtest-dev \
libiomp-dev \
libleveldb-dev \
liblmdb-dev \
libopencv-dev \
libopenmpi-dev \
libsnappy-dev \
libprotobuf-dev \
openmpi-bin \
openmpi-doc \
protobuf-compiler \
python-dev \
python-pip
这些包包括下载 Caffe2 源代码所需的工具(Git)以及构建 Caffe2 所需的工具(build-essential、cmake和python-dev)。其余的包是 Caffe2 所依赖的库,包括 Google Flags(libgflags2)、Google Log(libgoogle-glog-dev)、Google Test(libgtest-dev)、LevelDB(libleveldb-dev)、LMDB(liblmdb-dev)、OpenCV(libopencv-dev)、OpenMP(libiomp-dev)、OpenMPI(openmpi-bin和openmpi-doc)、Protobuf(libprotobuf-dev和protobuf-compiler)以及 Snappy(libsnappy-dev)。
- 最后,安装 Python Pip 工具,并使用它安装其他 Python 库,如
NumPy和ProtobufPython API,这些在使用 Python 时非常有用:
$ sudo apt-get install -y --no-install-recommends python-pip
$ pip install --user \
future \
numpy \
protobuf
安装加速库
使用 Caffe2 训练深度学习网络并用于推理涉及大量数学计算。使用数学运算和深度学习基本操作的加速库,可以通过加速训练和推理任务帮助 Caffe2 用户。CPU 和 GPU 的供应商通常提供这样的库,如果系统中可用,Caffe2 也支持使用这些库。
英特尔数学核心库(MKL)是加速在英特尔 CPU 上训练和推理的关键。此库对个人和社区使用是免费的。可以通过在此注册进行下载:software.seek.intel.com/performance-libraries。安装过程包括解压下载的包,并以超级用户身份运行install.sh安装脚本。默认情况下,库文件会安装到/opt/intel目录中。如果 MKL 被安装到默认目录,接下来的 Caffe2 构建步骤会自动找到并使用 MKL 的 BLAS 和 LAPACK 例程。
CUDA 和 CUDA 深度神经网络 (cuDNN) 库对于在 NVIDIA GPU 上进行更快的训练和推理是必不可少的。CUDA 在此注册后可免费下载:developer.nvidia.com/cuda-downloads。cuDNN 可以从这里下载:developer.nvidia.com/cudnn。注意,你需要有一块现代的 NVIDIA GPU,并且已经安装了 NVIDIA GPU 驱动程序。作为 GPU 驱动程序的替代方案,你可以使用随 CUDA 一起安装的驱动程序。CUDA 和 cuDNN 库的文件通常会安装在 Linux 系统的 /usr/local/cuda 目录下。如果安装在默认目录,Caffe2 的构建步骤会自动找到并使用 CUDA 和 cuDNN。
构建 Caffe2
使用 Git,我们可以克隆包含 Caffe2 源代码及其所需所有子模块的 Git 仓库:
$ git clone --recursive https://github.com/pytorch/pytorch.git && cd pytorch
$ git submodule update --init
请注意,Caffe2 的源代码现在位于 PyTorch 源代码库的一个子目录中。这是因为 Facebook 为这两个流行的深度学习框架制定了共同发展计划,旨在在一段时间内合并两个框架的最佳特性。
Caffe2 使用 CMake 作为其构建系统。CMake 使得 Caffe2 能够轻松地为多种编译器和操作系统进行构建。
要使用 CMake 构建 Caffe2 源代码,我们首先创建一个构建目录,并从其中调用 CMake:
$ mkdir build
$ cd build
$ cmake ..
CMake 会检查可用的编译器、操作系统、库和软件包,并决定启用哪些 Caffe2 特性以及使用哪些编译选项。这些选项可以在根目录下的 CMakeLists.txt 文件中查看。选项以 option(USE_FOOBAR "使用 Foobar 库" OFF) 的形式列出。你可以通过在 CMakeLists.txt 文件中将这些选项设置为 ON 或 OFF 来启用或禁用它们。
这些选项也可以在调用 CMake 时进行配置。例如,如果你的 Intel CPU 支持 AVX/AVX2/FMA,并且你希望使用这些特性来加速 Caffe2 操作,那么可以通过以下方式启用 USE_NATIVE_ARCH 选项:
$ cmake -DUSE_NATIVE_ARCH=ON ..
安装 Caffe2
CMake 最终会生成一个 Makefile 文件。我们可以使用以下命令来构建 Caffe2 并将其安装到系统中:
$ sudo make install
这一步涉及构建大量的 CUDA 文件,可能会非常慢。建议使用 make 的并行执行功能,利用 CPU 的所有核心来加快构建速度。我们可以通过以下命令来实现:
$ sudo make -j install
使用 make 安装方法来构建和安装,会使后续更新或卸载 Caffe2 变得困难。
相反,我更倾向于创建 Caffe2 的 Debian 软件包并进行安装。这样,我可以方便地卸载或更新它。我们可以使用 checkinstall 工具来实现这一点。
要安装 checkinstall 并使用它来构建和安装 Caffe2,请使用以下命令:
$ sudo apt-get install checkinstall
$ sudo checkinstall --pkgname caffe2
这个命令还会生成一个 Debian .deb 包文件,你可以用它在其他计算机上安装或与他人共享。例如,在我的计算机上,这个命令生成了一个名为 caffe2_20181207-1_amd64.deb 的文件。
如果你需要更快速的构建,可以使用 make 的并行执行功能以及 checkinstall:
$ sudo checkinstall --pkgname caffe2 make -j install
如果你以后需要卸载 Caffe2,现在可以使用以下命令轻松卸载:
$ sudo dpkg -r caffe2
测试 Caffe2 Python API
我们现在已经安装了 Caffe2,但需要确保它已正确安装,并且 Python API 能正常工作。一种简单的方法是返回到你的主目录,并检查 Caffe2 的 Python API 是否能被导入并正常执行。可以使用以下命令进行检查:
$ cd ~
$ python -c "from caffe2.python import core"
不要在 Caffe2 目录内运行前面的命令。这样做是为了避免 Python 在选择使用你已安装的 Caffe2 文件和源代码或构建目录中的文件时产生歧义。
如果你的 Caffe2 没有正确安装,你可能会看到某种错误,例如以下代码块中显示的错误:
$ python -c "from caffe2.python import core"
Traceback (most recent call last):
File "<string>", line 1, in <module>
ImportError: No module named caffe2.python
如果你的 Caffe2 已经正确安装,那么你可能不会看到错误。然而,如果没有 GPU,可能仍然会看到警告:
$ python -c "from caffe2.python import core"
WARNING:root:This caffe2 python run does not have GPU support. Will run in CPU only mode.
测试 Caffe2 C++ API
我们现在已经安装了 Caffe2,但需要确保它已正确安装,并且 C++ API 能正常工作。一种简单的方法是创建一个小的 C++ 程序,初始化 Caffe2 的全局环境。这是通过调用一个名为 GlobalInit 的方法并传入程序的参数来完成的。这通常是 Caffe2 C++ 应用程序中的第一个调用。
创建一个名为 ch1.cpp 的 C++ 源文件,代码如下:
// ch1.cpp
#include "caffe2/core/init.h"
int main(int argc, char** argv)
{
caffe2::GlobalInit(&argc, &argv);
return 0;
}
我们可以使用以下命令编译这个 C++ 源文件:
$ g++ ch1.cpp -lcaffe2
我们要求链接器通过使用 -lcaffe2 选项来链接 libcaffe2.so 共享库文件。编译器使用默认的头文件位置,链接器使用默认的共享库文件位置,所以我们不需要指定这些路径。
默认情况下,Caffe2 的头文件安装在 /usr/local/include 下的 caffe2 子目录中。这个路径通常会自动包含在 C++ 编译中。同样,Caffe2 的共享库文件默认安装在 /usr/local/lib 中。如果你将 Caffe2 安装到其他位置,需要使用 -I 选项指定包含目录的位置,并使用 -L 选项指定共享库文件的位置。
我们现在可以执行编译后的二进制文件:
$ ./a.out
如果执行成功,那么你的 Caffe2 安装是正确的。你现在可以开始编写 Caffe2 C++ 应用程序。
总结
恭喜!本章简要介绍了深度学习和 Caffe2。我们研究了在系统中构建和安装 Caffe2 的过程。现在我们准备好通过构建自己的网络、训练自己的模型并将其用于解决实际问题,来探索深度学习的世界。
在下一章中,我们将学习 Caffe2 操作符,并了解如何将它们组合以构建简单的计算图。然后,我们将继续构建一个能够识别手写数字的神经网络。
第二章:构建网络
在本章中,我们将了解 Caffe2 操作符,并学习如何使用这些操作符构建网络。为了学习如何使用操作符,我们将从零开始构建一个简单的计算图。然后,我们将解决一个实际的计算机视觉问题——MNIST(通过构建一个带有训练参数的真实神经网络)并用于推理。
本章涵盖以下主题:
-
Caffe2 操作符简介
-
操作符与层的区别
-
如何使用操作符构建网络
-
MNIST 问题简介
-
为 MNIST 问题构建网络
-
通过 Caffe2 网络进行推理
操作符
在 Caffe2 中,神经网络可以被看作是一个有向图,其中节点是操作符,边表示操作符之间数据流的传递。操作符是 Caffe2 网络中的基本计算单元。每个操作符都定义了一定数量的输入和输出。当操作符被执行时,它读取输入,执行与之相关的计算,并将结果写入输出。
为了获得最佳性能,Caffe2 操作符通常用 C++ 实现,以便在 CPU 上执行,并用 CUDA 实现,以便在 GPU 上执行。Caffe2 中的所有操作符都来自一个通用接口。你可以在 Caffe2 源代码中的 caffe2/proto/caffe2.proto 文件中看到这个通用接口的定义。
以下是我在 caffe2.proto 文件中找到的 Caffe2 操作符接口:
// Operator Definition
message OperatorDef {
repeated string input = 1; // the name of the input blobs
repeated string output = 2; // the name of output top blobs
optional string name = 3; // the operator name. This is optional.
// the operator type. This is needed to create the object from the
//operator
// registry.
optional string type = 4;
repeated Argument arg = 5;
// The device option that the operator should run under.
optional DeviceOption device_option = 6;
// Optionally, one can specify an engine when there are multiple
// implementations available simultaneously for one device type.
// If one specifies an engine but that engine does not exist in the
//compiled
// Caffe2 binary, Caffe2 will fall back to the default engine of that
//device
// type.
optional string engine = 7;
// Additional 'fake' inputs used for expressing control dependencies
// in the operator graph. This can be used to ensure that an
// operator does not run until another operator is ready, for e.g.
// scheduling control. These are not passed as actual inputs to the
// Operator implementation, and are only used by the Net class for
// scheduling purposes.
repeated string control_input = 8;
// is_gradient_op argument is only used as a hint in shape inference
// and has no runtime significance
optional bool is_gradient_op = 9 [0,1][default = false];
// debug information associated with the construction of the
//operator.
// This is an optional string with no assumed characteristics as
// operators can be constructed in any language.
optional string debug_info = 10;
}
上述代码片段是Google 协议缓冲区(ProtoBuf)格式的定义。ProtoBuf 是一种用于需要序列化和反序列化结构化数据的应用程序的机制。ProtoBuf 的序列化和反序列化机制被大多数流行语言和平台所支持。Caffe2 使用 ProtoBuf,使得它的所有结构,如操作符和网络,都可以通过多种编程语言、不同操作系统和 CPU 架构轻松访问。
从上面的操作符定义中,我们可以看到,Caffe2 中的操作符定义了 input 和 output 数据块,并具有 name(名称)、type(类型)、device(执行设备,如 CPU 或 GPU)、engine(执行引擎)以及其他信息。
Caffe2 的一大亮点是,它拥有大量已经定义并为你优化的操作符,数量多达数百个。这样做的好处是,你可以使用丰富的操作符来构建自己的网络,并且你从其他地方借用的网络很可能在 Caffe2 中得到完全支持。这减少了你定义自己操作符的需求。你可以在 Caffe2 操作符目录中找到 Caffe2 操作符的完整列表及其文档,网址为 caffe2.ai/docs/operators-catalogue.html。
示例——MatMul 操作符
作为 Caffe2 操作符的一个例子,考虑MatMul操作符,它可用于执行矩阵乘法。这一线性代数操作在深度学习中至关重要,并且是许多重要神经网络层(如全连接层和卷积层)实现的核心。我们将在本章稍后以及第三章《训练网络》中分别学习这些层。矩阵乘法操作如图 2.1 所示:

图 2.1:矩阵乘法
如果我们查阅 Caffe2 操作符目录中的 MatMul 操作符,我们将找到如图 2.2 所示的文档:

图 2.2:Caffe2 中 MatMul 操作符的文档
在图 2.2 中的 MatMul 操作符文档中,我们可以看到操作符的功能描述。在接口部分,我们看到它有两个输入:分别为大小为 M×K 和 K×N 的二维矩阵 A 和 B。它执行矩阵 A 和 B 的乘法,并生成一个大小为 M×N 的二维矩阵 C。我们还可以看到它有一些可选参数,用来指定 A 和 B 是否有专属轴,或者它们是否是转置矩阵。最后,我们还看到 Caffe2 文档贴心地为我们指向了定义MatMul操作符的实际 C++源代码。Caffe2 中所有操作符的文档都有以下有用结构:定义、输入、输出、可选参数,以及源代码指针。
学习了MatMul操作符的定义后,下面是一个代码片段,用于创建模型并向其中添加MatMul操作符:
model = model_helper.ModelHelper("MatMul model")
model.net.MatMul(["A", "B"], "C")
在前面的代码中,我们首先使用 Caffe2 model_helper模块中的ModelHelper类创建一个名为“MatMul model”的模型。模型是用于承载网络的结构,网络是一个由操作符组成的有向图。model_helper是一个高级的 Caffe2 Python 模块,其ModelHelper类可以轻松创建和管理模型。我们之前创建的model对象在其net成员中持有网络定义。
我们通过调用模型网络定义中的MatMul方法将MatMul操作符添加到这个模型中。请注意MatMul操作符的两个参数。第一个参数是一个列表,包含需要乘法的两个矩阵的名称。在这里,“A”和“B”是 Caffe2 工作空间中存储矩阵元素的 blob 的名称。(我们将在本章稍后学习 Caffe2 工作空间的内容。)同样,第二个参数“C”表示工作空间中的输出矩阵 blob。
层与操作符的区别
旧版的深度学习框架,如 Caffe,并没有操作符。相反,它们的基本计算单元被称为层。这些旧框架选择使用“层”这一名称,灵感来源于神经网络中的层。
然而,现代框架如 Caffe2、TensorFlow 和 PyTorch 更倾向于使用术语操作符来表示它们的基本计算单元。操作符和层之间存在细微的差别。在早期的框架中,比如 Caffe,一个层由该层的计算函数和该层的训练参数组成。与此不同,Caffe2 中的操作符仅包含计算函数。训练参数和输入是外部的,需要显式地提供给操作符。
示例 – 一个全连接操作符
为了说明层和操作符之间的区别,考虑 Caffe2 中的全连接(FC)操作符。全连接层是神经网络中最传统的层。早期的神经网络大多由一个输入层、一个或多个全连接层以及一个输出层组成:

图 2.3:Caffe2 操作符目录中的 FC 操作符接口文档
输入 X 到 FC 操作符的大小应为 M×K。这里,M 是批次大小。这意味着,如果我们将 10 个不同的输入作为一个批次输入神经网络,M 将是批次大小的值,即 10。因此,每个输入实际上是一个 1×K 的向量传递给该操作符。我们可以看到,与之前介绍的没有训练参数的MatMul操作符不同,FC 操作符有输入的训练参数:W 和 b。训练参数 W 是一个 K×N 的二维矩阵,表示权重值;训练参数 b 是一个一维向量,表示偏置值。FC 操作符将输出 Y 计算为 X×W+b。这意味着,每个 1×K 的输入向量在经过该操作符处理后会产生一个 1×N 的输出。事实上,这也解释了全连接层的名称:每个 1×K 的输入都与每个 1×N 的输出完全连接:

图 2.4:Caffe 层和 Caffe2 操作符之间的区别
在像 Caffe 这样的旧框架中,全连接层的权重和偏置训练参数与层一同存储。相比之下,在 Caffe2 中,FC 操作符不存储任何参数。训练参数和输入会被传递给操作符。图 2.4展示了 Caffe 层与 Caffe2 操作符之间的区别,以全连接层为例。由于大多数深度学习文献仍将这些实体称为层,因此我们将在本书的其余部分中交替使用层和操作符这两个词。
构建计算图
在本节中,我们将学习如何使用model_helper在 Caffe2 中构建一个网络。(model_helper在本章前面已经介绍过。)为了保持这个例子的简单性,我们使用不需要训练参数的数学操作符。所以,我们的网络是一个计算图,而不是神经网络,因为它没有从训练数据中学习的训练参数。我们将构建的网络由图 2.5 所示:

图 2.5:我们的简单计算图,包含三个操作符
如你所见,我们向网络提供了两个输入:一个矩阵A和一个向量B。对A和B应用MatMul操作符,并将其结果传递给Sigmoid函数,在图 2.5 中由σ表示。Sigmoid函数的结果被传递给SoftMax函数。(接下来我们将进一步了解Sigmoid和SoftMax操作符。)Sigmoid函数的输出E即为网络的输出。
以下是构建上述图形、提供输入并获取输出的 Python 代码:
#!/usr/bin/env python2
"""Create a network that performs some mathematical operations.
Run inference on this network."""
from caffe2.python import workspace, model_helper
import numpy as np
# Initialize Caffe2
workspace.GlobalInit(["caffe2",])
# Initialize a model with the name "Math model"
model = model_helper.ModelHelper("Math model")
# Add a matrix multiplication operator to the model.
# This operator takes blobs "A" and "B" as inputs and produces blob "C" as output.
model.net.MatMul(["A", "B"], "C")
# Add a Sigmoid operator to the model.
# This operator takes blob "C" as input and produces blob "D" as output.
model.net.Sigmoid("C", "D")
# Add a Softmax operator to the model.
# This operator takes blob "D" as input and produces blob "E" as output.
model.net.Softmax("D", "E", axis=0)
# Create input A, a 3x3 matrix initialized with some values
A = np.linspace(-0.4, 0.4, num=9, dtype=np.float32).reshape(3, 3)
# Create input B, a 3x1 matrix initialized with some values
B = np.linspace(0.01, 0.03, num=3, dtype=np.float32).reshape(3, 1)
# Feed A and B to the Caffe2 workspace as blobs.
# Provide names "A" and "B" for these blobs.
workspace.FeedBlob("A", A)
workspace.FeedBlob("B", B)
# Run the network inside the Caffe2 workspace.
workspace.RunNetOnce(model.net)
# Extract blob "E" from the workspace.
E = workspace.FetchBlob("E")
# Print inputs A and B and final output E
print A
print B
print E
该程序可以分为四个阶段:
-
初始化 Caffe2
-
构建模型网络
-
向工作区添加输入数据
-
在工作区中运行模型的网络并获取输出
你可以在自己的程序中使用类似的结构来组成网络并将其用于推理。
让我们详细查看每个阶段的 Python 代码。
初始化 Caffe2
在调用任何 Caffe2 方法之前,我们需要导入可能需要的 Caffe2 Python 模块:
- 首先,导入
workspace和module_helper模块:
from caffe2.python import workspace, model_helper
import numpy as np
这一步骤还导入了numpy模块,以便我们可以在程序中轻松创建矩阵和向量。NumPy是一个流行的 Python 库,提供多维数组(包括向量和矩阵)以及一大堆可以应用于这些数组的数学操作。
- 接下来,使用以下调用初始化默认的 Caffe2 工作区:
workspace.GlobalInit(["caffe2",])
工作区是 Caffe2 中所有数据创建、读取和写入的地方。这意味着我们将使用工作区加载输入、网络的训练参数、操作符之间的中间结果以及网络的最终输出。我们还使用工作区在推理过程中执行网络。
我们之前已经创建了 Caffe2 的默认工作区。我们也可以创建其他具有独特名称的工作区。例如,要创建第二个工作区并切换到它,可以执行以下代码:workspace.SwitchWorkspace("Second Workspace", True)
构建模型网络
- 我们使用
ModelHelper类(在本章前面描述)来创建一个空的模型并命名为Math model:
# Initialize a model with the name "Math model"
model = model_helper.ModelHelper("Math model")
- 接下来,我们将第一个操作符
MatMul添加到该模型的网络中:
# Add a matrix multiplication operator to the model.
# This operator takes blobs "A" and "B" as inputs and produces blob "C" as output.
model.net.MatMul(["A", "B"], "C")
MatMul运算符在本章前面有描述。我们在调用中指明了输入 blob["A", "B"]和输出 blob"C"的名称。Blob是一个具有名称的 N 维数组,存储相同类型的值。例如,我们可以将浮动点值矩阵表示为二维 blob。Blob 不同于大多数 Python 数据结构,如list和dict,因为它其中的所有值必须具有相同的数据类型(如float或int)。神经网络中使用的所有输入数据、输出数据和训练参数都以 blob 的形式存储在 Caffe2 中。
我们还没有在工作区创建这些 blob。我们将运算符添加到网络中,并告知 Caffe2,在网络实际使用时,这些名称的 blob 将会出现在工作区。
- 之后,我们将下一个运算符
Sigmoid添加到网络中:
# Add a Sigmoid operator to the model.
# This operator takes blob "C" as input and produces blob "D" as output.
model.net.Sigmoid("C", "D")
Sigmoid 运算符
Sigmoid运算符实现了Sigmoid 函数。这个函数在神经网络中非常流行,也被称为逻辑函数。其定义如下:

图 2.6 展示了该函数的图形:

图 2.6:Sigmoid 函数的图形
Sigmoid是一个非线性函数,通常在神经网络中作为激活函数使用。激活函数是引入在一个或多个层序列之间的常见层。它将输入转换为激活值,决定接下来层中的神经元是否被激活(或触发)。激活函数通常会向网络中引入非线性特性。
注意Sigmoid的形状像字母S。它看起来像一个平滑的阶跃函数,其输出被限制在 0 和 1 之间。例如,它可以用于将任何输入值分类,判断该值是否属于某个类别(值为1.0)或不属于(值为0.0)。
Caffe2 中的 Sigmoid 函数是一个逐元素运算符。这意味着它会单独应用于输入的每一个元素。在我们之前的代码片段中,我们告知 Caffe2,网络中添加的这个运算符将从工作区获取名为"C"的输入 blob,并将输出写入工作区中的"D" blob。
作为最终的第三个运算符,我们将Softmax运算符添加到网络中:
# Add a Softmax operator to the model.
# This operator takes blob "D" as input and produces blob "E" as output.
model.net.Softmax("D", "E", axis=0)
Softmax 运算符
Softmax运算符实现了SoftMax 函数。该函数接受一个向量作为输入,并将该向量的元素标准化为概率分布。它在每个向量元素上的定义如下:

SoftMax 函数的输出值具有良好的特性。每个输出值
都被限制在
之间,并且输出向量的所有值总和为 1。由于这些特性,该函数通常作为分类神经网络中的最后一层使用。
在上面的代码片段中,我们向网络添加了一个 Softmax 操作符,该操作符将使用名为 "D" 的 Blob 作为输入,并将输出写入名为 "E" 的 Blob。axis 参数用于指示沿哪个轴将输入的 N 维数组拆分并强制转换为二维数组。通常,axis=1 表示 Blob 的第一个轴是批次维度,其他部分应被强制转换为向量。由于我们在本示例中使用的是单一输入,因此我们使用 axis=0 来表示整个输入应该被强制转换为 1D 向量以供 Softmax 使用。
将输入 Blob 添加到工作区
现在我们的模型已准备好。我们将通过 NumPy 初始化两个输入 Blob A 和 B,并为其分配线性分布的值:
# Create input A, a 3x3 matrix initialized with some values
A = np.linspace(-0.4, 0.4, num=9, dtype=np.float32).reshape(3, 3)
# Create input B, a 3x1 matrix initialized with some values
B = np.linspace(0.01, 0.03, num=3, dtype=np.float32).reshape(3, 1)
请注意,我们指定了这些数组中的所有值都将是浮点数类型。在 NumPy 中,这通过 np.float32 来表示。NumPy 的 reshape 函数用于将一维数组的值转换为大小为
和
的矩阵。
由于我们将在 Caffe2 中对网络执行推理操作,我们需要将输入 Blob 设置到工作区中。推理 是将输入数据传递给经过训练的神经网络并进行“推断”或获取输出的过程。在 Caffe2 中,将 Blob 设置到工作区并为其指定名称和值的操作称为 喂入。
喂入输入 Blob 是通过 FeedBlob 调用来执行的,如下所示:
# Feed A and B to the Caffe2 workspace as blobs.
# Provide names "A" and "B" for these blobs.
workspace.FeedBlob("A", A)
workspace.FeedBlob("B", B)
在前面的代码片段中,我们将张量 A 和 B 输入到工作区,并分别将这些 Blob 命名为 "A" 和 "B"。
运行网络
我们构建了一个网络,并且已经准备好在工作区中输入数据。现在我们准备在该网络上执行推理操作。在 Caffe2 中,这个过程称为 运行。我们在工作区的网络上执行运行操作,如下所示:
# Run the network inside the Caffe2 workspace.
workspace.RunNetOnce(model.net)
运行结束后,我们可以从工作区中提取或获取输出 Blob,并打印我们的输入和输出 Blob 以供参考:
# Extract blob "E" from the workspace.
E = workspace.FetchBlob("E")
# Print inputs A and B and final output E
print A
print B
print E
当执行此计算图代码时,应该产生如下所示的输出:
$ ./computation_graph.py
A: [[-0.4 -0.3 -0.2]
[-0.1 0\. 0.1]
[ 0.2 0.3 0.4]]
B: [[0.01]
[0.02]
[0.03]]
E: [[0.3318345 ]
[0.33333108]
[0.33483443]]
你可以处理矩阵乘法、Sigmoid 和 SoftMax 图层,使用输入 A 和 B,并验证 E 确实具有正确的输出值。
构建多层感知器神经网络
在本节中,我们介绍了 MNIST 问题,并学习如何使用 Caffe2 构建 多层感知器 (MLP) 网络来解决该问题。我们还学习了如何将预训练的参数加载到网络中并用于推理。
MNIST 问题
MNIST 问题是一个经典的图像分类问题,曾在机器学习中非常流行。如今,最先进的方法已经可以在该问题上实现超过 99%的准确率,因此它不再具有实际意义。然而,它作为我们学习如何构建一个解决实际机器学习问题的 Caffe2 网络的垫脚石。
MNIST 问题的核心在于识别 28 x 28 像素大小的灰度图像中的手写数字。这些图像来自 MNIST 数据库,这是一个修改过的扫描文档数据集,最初由国家标准与技术研究院(NIST)共享,因此得名修改版 NIST(MNIST)。该数据集中的一些示例如图 2.7 所示:

图 2.7:MNIST 数据集中每个数字(0 到 9)的 10 张随机样本
注意,有些手写数字即使对人类来说也可能难以分类。
每张 MNIST 数据集中的图像包含一个手写数字,范围从 0 到 9。每张图像中的灰度值已被归一化,并且手写数字被居中处理。因此,MNIST 是一个适合初学者的数据集,因为在用于推理或训练之前,我们无需进行图像清理、预处理或增强操作。(如果使用其他图像数据集,通常需要进行这些操作。)通常,我们会使用该数据集中的 60,000 张图像作为训练数据,另外 10,000 张图像用于测试。
构建一个 MNIST MLP 网络
为了解决 MNIST 问题,我们将创建一个神经网络,称为多层感知器(MLP)。这是给具有输入层、输出层和一个或多个隐藏层的神经网络的经典名称。MLP 是一种前馈神经网络,因为它的网络是一个有向无环图(DAG);即,它没有循环。
用于创建本节所述的 MLP 网络、加载预训练参数并用于推理的 Python 代码,可以在本书附带的mnist_mlp.py文件中找到。在接下来的章节中,我们将对这段代码进行拆解并尝试理解它。
初始化全局常量
我们的 Python Caffe2 代码用于 MNIST MLP 网络的开始部分是初始化一些 MNIST 常量:
# Number of digits in MNIST
MNIST_DIGIT_NUM = 10
# Every grayscale image in MNIST is of dimensions 28x28 pixels in a single channel
MNIST_IMG_HEIGHT = 28
MNIST_IMG_WIDTH = 28
MNIST_IMG_PIXEL_NUM = MNIST_IMG_HEIGHT * MNIST_IMG_WIDTH
MNIST 数据集中有10个(MNIST_DIGIT_NUM)数字(0-9),我们需要识别这些数字。每个 MNIST 图像的尺寸为 28 x 28 像素(MNIST_IMG_HEIGHT,MNIST_IMG_WIDTH)。
构建网络层
以下是我们将要构建的 MNIST MLP 网络的示意图:

图 2.8:我们的 MNIST MLP 网络,包含输入层、三对 FC 和 ReLU 层以及最终的 SoftMax 层
我们将构建一个简单的前馈神经网络,包含三对全连接层和 ReLU 激活层。每一对层都连接到前一对层的输出。第三对全连接层和 ReLU 激活层的输出将通过 SoftMax 层,以获得网络的输出分类值。该网络结构如图 2.8 所示。
为了构建这个网络,我们首先使用ModelHelper初始化一个模型,就像我们在之前的计算图示例中做的一样。然后,我们使用Brew API 来添加网络的各个层。
虽然像我们在计算图示例中那样使用原始操作符调用也是可行的,但如果我们构建真实的神经网络,使用 Brew 更为优选。这是因为 Brew 中的helper函数使得为每层初始化参数和为每层选择设备变得非常容易。使用操作符方法来做相同的事情则需要多次调用并传入多个参数。
调用 Brew helper函数来添加一层通常需要这些参数:
-
包含我们正在添加该层的网络的模型
-
输入数据块或前一层的名称
-
该层的名称
-
该层的输入维度
-
该层的输出维度
我们首先通过以下代码添加第一对全连接层和 ReLU 层:
# Create first pair of fullyconnected and ReLU activation layers
# This FC layer is of size (MNIST_IMG_PIXEL_NUM * 2)
# On its input side it is fed the MNIST_IMG_PIXEL_NUM pixels
# On its output side it is connected to a ReLU layer
fc_layer_0_input_dims = MNIST_IMG_PIXEL_NUM
fc_layer_0_output_dims = MNIST_IMG_PIXEL_NUM * 2
fc_layer_0 = brew.fc(
model,
input_blob_name,
"fc_layer_0",
dim_in=fc_layer_0_input_dims,
dim_out=fc_layer_0_output_dims
)
relu_layer_0 = brew.relu(model, fc_layer_0, "relu_layer_0")
注意,在这一对层中,输入的维度是MNIST_IMG_PIXEL_NUM,而输出的维度是MNIST_IMG_PIXEL_NUM * 2。
ReLU 层
以下图示展示了 ReLU 函数:

图 2.9:ReLU 函数
在构建计算图时,我们引入了一个激活层,叫做 Sigmoid。在这里,我们使用另一个流行的激活层叫做修正线性单元(ReLU)。该函数在图 2.9中可以看到,其定义如下:

我们使用以下代码添加第二对和第三对层,分别命名为("fc_layer_1","relu_layer_1")和("fc_layer_2","relu_layer_2"):
# Create second pair of fullyconnected and ReLU activation layers
fc_layer_1_input_dims = fc_layer_0_output_dims
fc_layer_1_output_dims = MNIST_IMG_PIXEL_NUM * 2
fc_layer_1 = brew.fc(
model,
relu_layer_0,
"fc_layer_1",
dim_in=fc_layer_1_input_dims,
dim_out=fc_layer_1_output_dims
)
relu_layer_1 = brew.relu(model, fc_layer_1, "relu_layer_1")
# Create third pair of fullyconnected and ReLU activation layers
fc_layer_2_input_dims = fc_layer_1_output_dims
fc_layer_2_output_dims = MNIST_IMG_PIXEL_NUM
fc_layer_2 = brew.fc(
model,
relu_layer_1,
"fc_layer_2",
dim_in=fc_layer_2_input_dims,
dim_out=fc_layer_2_output_dims
)
relu_layer_2 = brew.relu(model, fc_layer_2, "relu_layer_2")
第二对层接受MNIST_IMG_PIXEL_NUM * 2大小的输入,输出MNIST_IMG_PIXEL_NUM * 2。第三对层接受MNIST_IMG_PIXEL_NUM * 2并输出MNIST_IMG_PIXEL_NUM。
在使用神经网络解决分类问题时,我们通常需要对类别进行概率分布。我们在网络的末尾添加一个 SoftMax 层来实现这一点:
# Create a softmax layer to provide output probabilities for each of
# 10 digits. The digit with highest probability value is considered to be
# the prediction of the network.
softmax_layer = brew.softmax(model, relu_layer_2, "softmax_layer")
注意,当brew.softmax方法能够从其连接的输入中获取输入和输出的维度时,它不需要显式地告知输入和输出的维度。这是使用 Brew 方法的优势之一。
设置网络层的权重
在组建完网络后,我们将预训练的层权重整合到网络中。这些权重是通过在 MNIST 训练数据上训练该网络得到的。我们将在下一章学习如何训练一个网络。在本章中,我们将重点介绍如何将这些预训练权重加载到我们的网络中并执行推理。
请注意,在我们使用的三种层类型中,只有全连接层需要预训练权重。我们已将这些权重存储为 NumPy 文件,以便于加载。可以通过使用 NumPy 的load方法从磁盘加载这些文件。这些值通过workspace.FeedBlob方法在工作区中设置,并通过指定它们所属的层名称来完成。
实现这一点的代码片段如下:
for i, layer_blob_name in enumerate(inference_model.params):
layer_weights_filepath = "mnist_mlp_weights/{}.npy".format(str(i))
layer_weights = np.load(layer_weights_filepath, allow_pickle=False)
workspace.FeedBlob(layer_blob_name, layer_weights)
运行网络
所以我们已经构建了一个网络,并且用预训练权重初始化了它的层。现在我们可以将输入数据提供给它,并通过网络执行推理来获得输出。
我们可以逐个将输入图像喂给网络,并获得输出的分类结果。然而,在生产系统中这样做并不能有效利用 CPU 或 GPU 的计算资源,且推理吞吐量低。因此,几乎所有深度学习框架都允许用户将一批输入数据同时提供给网络,无论是在推理还是训练过程中。
为了说明如何输入一批图像,我们有一个mnist_data.npy文件,其中包含一批 64 个 MNIST 图像的数据。我们从文件中读取这批数据,并将其设置为工作区中的数据块,使其作为网络的输入:
# Read MNIST images from file to use as input
input_blob = None
with open("mnist_data.npy") as in_file:
input_blob = np.load(in_file)
# Set MNIST image data as input data
workspace.FeedBlob("data", input_blob)
我们通过调用workspace.RunNetOnce方法并将网络作为输入来执行网络推理:
workspace.RunNetOnce(inference_model.net)
我们从工作区获取网络的输出数据块,对于每一个 64 个输入,我们确定哪个 MNIST 数字类别的置信度最高;也就是说,网络认为该 MNIST 图像是哪个数字:
network_output = workspace.FetchBlob("softmax_layer")
for i in range(len(network_output)):
# Get prediction and confidence by finding max value and its index
# in preds array
prediction, confidence = max(enumerate(network_output[i]),
key=operator.itemgetter(1))
print("Input: {} Prediction: {} Confidence: {}".format(i,
prediction, confidence)
当我们执行此脚本时,我们会得到如下输出:
Input: 0 Prediction: 5 Confidence: 0.609326720238
Input: 1 Prediction: 7 Confidence: 0.99536550045
Input: 2 Prediction: 9 Confidence: 0.877566576004
Input: 3 Prediction: 9 Confidence: 0.741059184074
Input: 4 Prediction: 2 Confidence: 0.794860899448
Input: 5 Prediction: 0 Confidence: 0.987336695194
Input: 6 Prediction: 7 Confidence: 0.900308787823
Input: 7 Prediction: 1 Confidence: 0.993218839169
Input: 8 Prediction: 6 Confidence: 0.612009465694
这意味着网络认为第一个输入图像是数字 5,第二个是数字 7,以此类推。
总结
在本章中,我们了解了 Caffe2 操作符,以及它们与旧深度学习框架中使用的层的区别。我们通过组合多个操作符构建了一个简单的计算图。然后,我们解决了 MNIST 机器学习问题,使用 Brew 辅助函数构建了一个 MLP 网络。我们将预训练的权重加载到该网络中,并用于对一批输入图像进行推理。我们还介绍了几个常见的层,例如矩阵乘法、全连接、Sigmoid、SoftMax 和 ReLU。
在本章中,我们学习了如何对我们的网络进行推理。在下一章,我们将学习训练网络,并了解如何训练网络来解决 MNIST 问题。
第三章:训练网络
在第二章,构建网络中,我们学习了如何创建 Caffe2 操作符,以及如何利用它们构建网络。在本章中,我们将重点介绍神经网络的训练。我们将学习如何创建一个用于训练的网络,并使用 Caffe2 进行训练。我们将继续使用 MNIST 数据集作为示例。然而,与上一章中构建的 MLP 网络不同,我们将在本章创建一个名为 LeNet 的流行网络。
本章将涵盖以下主题:
-
神经网络训练简介
-
构建 LeNet 的训练网络
-
训练和监控 LeNet 网络
训练简介
本节将简要概述神经网络的训练过程。这将帮助我们理解后续章节中如何使用 Caffe2 实际训练网络。
神经网络的组件
我们使用神经网络来解决某些类型的问题,对于这些问题,设计计算机算法将会非常繁琐或困难。例如,在 MNIST 问题中(在第二章,构建网络中介绍),手工设计一个复杂的算法来检测每个数字的常见笔画模式,并据此确定每个数字,将会非常繁琐。相反,设计一个适合该问题的神经网络,然后使用大量数据对其进行训练(如本章后面所示)来实现这一目标,会更为简单。如果训练数据多样且训练过程谨慎,这样的网络在处理输入数据的变化时也会比任何确定性的手工算法更加健壮。
神经网络有两个主要组件:其结构和权重。我们通常先设计网络结构,然后使用训练算法和训练数据来确定权重。经过训练后,网络结构及其嵌入的权重可以用于对新的未见过的数据进行推理,如下图所示:

图 3.1:用于推理的网络结构和权重
神经网络的结构
网络的结构是各层的系列、它们的类型和配置。这个结构通常由研究人员或熟悉神经网络所要解决问题的实践者设计。例如,为了解决图像分类问题,计算机视觉研究人员通常会在网络中使用一系列卷积层。(我们将在本章稍后学习卷积层。)每层的各种配置参数也需要事先确定,比如卷积层中卷积滤波器的大小和数量。现在有很多兴趣将深度学习本身用于确定适合特定问题的网络结构。然而,关于这个元学习主题的讨论超出了本书的范围。
神经网络的权重
网络的第二个组成部分是它的权重和偏置。我们通常将它们统称为权重,有时也称为参数。这些是网络中每一层的浮点值参数。每一层的权重如何使用取决于层的类型。例如,在全连接层中,较大的权重值可能表示输入信号和网络输出之间的更强相关性。在卷积层中,卷积滤波器的权重可能表示它在输入中寻找的特定模式或形状。
总结一下,我们坐下来设计一个网络的结构以解决特定问题。这个过程中我们的选择将受到对问题空间的理解、深度学习框架中可用的层类型、我们使用的加速器的硬件限制,以及我们愿意花费多少训练时间的限制。例如,GPU 或 CPU 中可用的内存可能会限制我们在一层中使用的权重数量或网络中使用的层数。我们愿意花费的训练时间也限制了我们可以在网络中使用的权重和层的数量,因为使用越多的权重和层,网络收敛和训练的时间可能会越长。
训练过程
一旦我们构建了一个网络结构,我们就可以使用像 Caffe2 这样的深度学习框架来描述该结构。然后,我们在训练数据上应用框架中可用的多种训练算法之一。这将训练网络并学习每一层的权重,以最佳方式放大信号并抑制噪声。这个过程如图 3.2 所示:

图 3.2:训练是使用训练算法和数据学习神经网络权重的过程
神经网络通常使用基于梯度的优化算法进行训练。为此,我们首先为网络定义一个目标函数或损失函数。该函数通过将网络在给定输入上的输出与该输入的真实结果进行比较,从而计算出损失或误差值。训练过程迭代地选择训练数据并计算其损失,然后使用优化算法更新权重,以减少误差。这个过程会不断重复,直到我们看到网络准确率没有进一步提高为止:

图 3.3:训练中迭代的三个阶段
训练过程的单次迭代如图 3.3所示。我们可以看到,它有三个不同的阶段。第一阶段是前向传播,在这一阶段,我们基本上使用当前权重对网络进行推理,以获得网络的结果或假设。第二阶段,我们使用损失函数计算网络的损失。第三阶段是反向传播,我们使用一种叫做反向传播(backpropagation)的算法来更新网络的权重。
梯度下降的变体
我们通常可以使用三种梯度下降的变体来采样每次训练迭代中使用的训练数据。如果每次迭代都使用整个训练数据集,这个过程叫做批量梯度下降。如果每次迭代都只使用一个随机选择的训练样本,那么这个过程叫做随机梯度下降。最常用的变体是小批量梯度下降,即每次迭代时使用一个随机选择的训练数据子集。为了获得最佳结果,我们会将训练数据打乱,然后将其划分为小批量,每次迭代使用一个小批量。完成一次训练数据的处理后,我们称之为一个周期(epoch),然后再一次打乱并重新划分成小批量,继续训练。
在本章的其余部分,我们将学习可以用于 MNIST 的 LeNet 网络,如何构建它,以及如何使用 Caffe2 进行训练。
LeNet 网络
在第二章,构建网络中,我们构建了一个由多个全连接层和激活层对组成的 MLP 网络。在本章中,我们将构建并训练一个卷积神经网络(CNN)。这种类型的网络之所以得名,是因为它主要使用卷积层(将在下一节介绍)。对于计算机视觉问题,CNN 相比 MLP 已经证明可以用更少的参数提供更好的结果。最早成功的 CNN 之一被用来解决我们之前提到的 MNIST 问题。这个名为LeNet-5的网络是由 Yann LeCun 及其同事创建的:

图 3.4:我们 LeNet 模型的结构
我们将构建一个与 LeNet 精神相似的网络。我们将在本书余下部分中称之为 LeNet 模型。从 图 3.4 中可以看到,我们的 LeNet 网络有八层。输入层之后,有两对卷积层和池化层。它们之后是一个全连接层和 ReLU 激活层的对,接着是另一个全连接层。最后使用 SoftMax 层来获得 MNIST 分类结果。
接下来,我们将介绍 CNN 中两个重要的层,它们是 LeNet 的一部分:卷积层和池化层。
卷积层
卷积层 是神经网络中最重要的一层,用于解决涉及图像和视频的计算机视觉问题。卷积层的输入张量至少具有三个维度:
。也就是说,输入有
个通道,每个通道都是一个高度为
和宽度为
的 2D 矩阵。这自然符合图像的布局。例如,RGB 图像有三个通道,每个通道具有一定的高度和宽度。
当我们提到 卷积 时,通常指的是 2 维 (2D) 卷积。一个 2D 卷积层有两组在训练过程中学习的参数:滤波器参数和偏置参数。
与 2D 卷积层相关的第一组参数是
滤波器。每个 滤波器 或 卷积核 都是一个 3 维 (3D) 张量,形状为
,包含在训练过程中学习到的浮点值。因此,对于一个 2D 卷积层,在训练过程中需要学习的滤波器参数总数为
。请注意,2D 卷积层的卷积核与层的输入具有相同的通道数
。
与 2D 卷积层相关的第二组参数是
偏置值,每个偏置值与之前描述的
个滤波器相关。
在卷积过程中,每个
卷积核会在输入的宽度和高度上滑动。在每个卷积核停止的地方,会计算卷积核值与输入值的点积,这些输入值与卷积核重叠,从而获得该位置的一个输出值。最后,卷积核相关的偏置值会加到每个输出值上。这个过程如图 3.5 所示:

图 3.5:二维卷积操作,输入
使用两个形状为
的滤波器。输出形状为
。
请注意,通过与每个卷积核进行卷积,结果输出张量的大小为
。因此,当输入大小为
时,输入到二维卷积层后的输出大小为
。如果我们将一批
输入送入二维卷积层,结果输出的大小为
。
二维卷积层还有其他一些参数。两个重要的参数是步幅(stride)和填充(padding)。步幅表示卷积核沿高度和宽度移动多少个值后才停下来进行卷积。例如,如果步幅为
,卷积核只会访问输入中的每隔一个位置。填充表示为了进行卷积,输入的高度和宽度可以假设扩展了多少,通常是使用零填充,即填充零值。
池化层
另一种在卷积神经网络(CNN)中常用的层叫做池化层。它通常用于减少前一层输出的宽度和高度。池化层通过子采样输入来生成输出。与卷积层不同,池化层没有任何预训练参数。池化层有两个与之相关的参数:窗口大小和缩减函数。类似于卷积层,池化层也有一些参数,如步幅和填充。
池化层的作用是沿着输入滑动具有指定宽度和高度的窗口。在每个停靠位置,它会将其缩减函数应用于窗口中的输入值,以产生一个单一的输出值:

图 3.6:池化层在池化输入
时,使用
池化窗口产生的输出
。
常见的缩减函数有最大池化(max)和平均池化(average)。在最大池化层中,输入窗口中值的最大值成为输出值。在平均池化层中,输入窗口中值的平均值成为输出值。图 3.6 展示了最大池化层中的一个操作。
池化窗口是二维的,并沿着输入的宽度和高度进行移动。因此,它只减少输入的宽度和高度,输入和输出中的通道数保持不变。
我们现在准备查看一个训练 LeNet 网络的代码示例。完整的源代码可以在 ch3/mnist_lenet.py 中找到。我们将在下一节开始读取 MNIST 训练数据。
训练数据
我们在本章中使用 brew 来简化构建 LeNet 网络的过程。首先,我们使用 ModelHelper 初始化模型,该方法在上一章中介绍过:
# Create the model helper for the train model
train_model = model_helper.ModelHelper(name="mnist_lenet_train_model")
然后我们使用 add_model_inputs 方法将输入添加到训练网络中:
# Specify the input is from the train lmdb
data, label = add_model_inputs(
train_model,
batch_size=64,
db=os.path.join(data_folder, "mnist-train-nchw-lmdb"),
db_type="lmdb",
)
训练数据通常存储在 数据库(DB)中,以便高效访问。读取数据库通常比从数千个单独的文件中读取要快。对于 MNIST 数据集中的每张训练图像,数据库存储图像的
灰度像素值和图像中的数字。每个灰度像素值是一个 8 位无符号整数,取值范围为
。每张图像中的实际数字称为 标签,通常是由人工通过检查图像进行注释的。例如,如果图像中的手写数字是 9,那么人工注释者会查看图像并给它标注为 9。
在我们的 add_model_inputs 方法中,我们使用一个便捷的 brew 辅助函数 db_input 将 DB 连接到我们的模型:
# Load data from DB
input_images_uint8, input_labels = brew.db_input(
model,
blobs_out=["input_images_uint8", "input_labels"],
batch_size=batch_size,
db=db,
db_type=db_type,
)
我们指定工作区中存储图像和标签数据的 blob 名称:input_images_uint8 和 input_labels。我们还指定了批量大小和访问数据库所需的信息,如数据库的名称和类型。
神经网络几乎总是使用浮动值,理想情况下,值需要标准化到范围
。因此,我们将输入图像数据(它是一个 8 位无符号整数数据类型)转换为浮动数据类型并进行标准化:
# Cast grayscale pixel values to float
# Scale pixel values to [0, 1]
input_images = model.Cast(input_images_uint8, "input_images", to=core.DataType.FLOAT)
input_images = model.Scale(input_images, input_images, scale=float(1./256))
请注意 Caffe2 的 ModelHelper 提供了有用的方法,使得执行这两项操作变得轻松:Cast 和 Scale。
最后,我们向图像数据 blob 添加一个 StopGradient 操作符,指示反向传递算法不要为其计算梯度:
# We do not need gradient for backward pass
# This op stops gradient computation through it
input_images = model.StopGradient(input_images, input_images)
我们这样做是因为输入层不是神经网络的实际层。它没有可学习的参数,也没有需要训练的内容。因此,反向传递可以在这里停止,无需继续进行。StopGradient 是 Caffe2 中的一个伪操作符,它实现了这一效果。
构建 LeNet
我们通过在脚本中调用 build_mnist_lenet 方法来构建用于推理的 LeNet 层:
# Build the LeNet network
softmax_layer = build_mnist_lenet(train_model, data)
请注意,我们只将图像像素数据输入到该网络,而不是标签。推理时不需要标签;标签用于训练或测试,以作为真实值与网络最终层预测结果进行比较。
接下来的各小节描述了如何添加卷积和池化层的对,完全连接层和 ReLU 层,以及最终的 SoftMax 层,以构建 LeNet 网络。
层 1 – 卷积
LeNet 的第一层是卷积层,我们在本章之前已经介绍过。我们使用 Caffe2 的二维卷积操作符 Conv2D 来构建该层,可以从操作符目录中找到。可以使用方便的 brew.conv 方法将其添加到模型中。
在创建操作符时,我们指定输入是一个单通道的灰度值矩阵。我们还指明输出应该有20个通道,每个通道保存一个矩阵。最后,我们指定每个卷积核的宽度和高度为5像素。在 Caffe2 中,我们只需要提供这样的最小信息,API 会自动处理其余的必要参数:
# Convolution layer that operates on the input MNIST image
# Input is grayscale image of size 28x28 pixels
# After convolution by 20 kernels each of size 5x5,
# output is 20 channels, each of size 24x24
layer_1_input_dims = 1 # Input to layer is grayscale, so 1 channel
layer_1_output_dims = 20 # Output from this layer has 20 channels
layer_1_kernel_dims = 5 # Each kernel is of size 1x5x5
layer_1_conv = brew.conv(
model,
input_blob_name,
"layer_1_conv",
dim_in=layer_1_input_dims,
dim_out=layer_1_output_dims,
kernel=layer_1_kernel_dims,
)
让我们扩展这些值,以更好地了解该层的输入、输出和卷积核的尺寸。由于 MNIST 数据集是一个
的单通道灰度值网格,因此网络第一层的输入是一个尺寸为
的 3D 数组。我们这里执行的是二维卷积,每个卷积核的通道数与输入层的通道数相同。此外,我们指明卷积核的宽度和高度为 5。因此,每个卷积核的尺寸为
。由于我们要求此层的输出有 20 个通道,所以需要 20 个这样的卷积核。因此,这一层卷积核参数的实际尺寸为
。卷积层还使用偏置值,每个输出通道一个,所以偏置值的尺寸为
。
如果对一个
卷积核在
输入上进行卷积,步幅为 1,结果是
。当使用 20 个这样的卷积核时,结果是
。这就是该层输出的尺寸。
层 2 – 最大池化
第一个卷积层的输出连接到一个最大池化层,这个池化层在本章之前已经介绍过。我们使用 Caffe2 的最大池化操作符 MaxPool 来构建该层,该操作符可以从操作符目录中找到。可以使用方便的 brew.max_pool 方法将其添加到模型中。在创建此操作符时,我们指定其卷积核大小为 2 x 2,并且步幅为 2:
# Max-pooling layer that operates on output from previous convolution layer
# Input is 20 channels, each of size 24x24
# After pooling by 2x2 windows and stride of 2, the output of this layer
# is 20 channels, each of size 12x12
layer_2_kernel_dims = 2 # Max-pool over 2x2 windows
layer_2_stride = 2 # Stride by 2 pixels between each pool
layer_2_pool = brew.max_pool(
model,
layer_1_conv,
"layer_2_pool",
kernel=layer_2_kernel_dims,
stride=layer_2_stride,
)
上一层卷积的输出尺寸为
。当使用窗口大小
和步幅为 2 进行最大池化时,输出尺寸为
。
层 3 和层 4 – 卷积和最大池化
第一个卷积层和池化层后面是 LeNet 中的另一对卷积层和池化层,进一步减少宽度和高度并增加通道数:
# Convolution layer that operates on output from previous pooling layer.
# Input is 20 channels, each of size 12x12
# After convolution by 50 kernels, each of size 20x5x5,
# the output is 50 channels, each of size 8x8
layer_3_input_dims = 20 # Number of input channels
layer_3_output_dims = 50 # Number of output channels
layer_3_kernel_dims = 5 # Each kernel is of size 50x5x5
layer_3_conv = brew.conv(
model,
layer_2_pool,
"layer_3_conv",
dim_in=layer_3_input_dims,
dim_out=layer_3_output_dims,
kernel=layer_3_kernel_dims,
)
# Max-pooling layer that operates on output from previous convolution layer
# Input is 50 channels, each of size 8x8
# Apply pooling by 2x2 windows and stride of 2
# Output is 50 channels, each of size 4x4
layer_4_kernel_dims = 2 # Max-pool over 2x2 windows
layer_4_stride = 2 # Stride by 2 pixels between each pool
layer_4_pool = brew.max_pool(
model,
layer_3_conv,
"layer_4_pool",
kernel=layer_4_kernel_dims,
stride=layer_4_stride,
)
第二对卷积层和池化层与第一对相似,卷积核大小为 5 x 5,步长为 2,而最大池化窗口大小为
,步长为 2。不同之处在于,第二个卷积层使用 50 个卷积核,生成一个具有 50 个通道的输出。在第二个卷积层之后,输出大小为
。在第二个最大池化层之后,输出为
。注意,输入的宽度和高度已经大幅度下降。
第 5 层和第 6 层 – 完全连接层和 ReLU 层
卷积层和池化层后面跟着一对完全连接层和 ReLU 层,通过便捷的方法brew.fc和brew.relu添加:
# Fully-connected layer that operates on output from previous pooling layer
# Input is 50 channels, each of size 4x4
# Output is vector of size 500
layer_5_input_dims = 50 * 4 * 4
layer_5_output_dims = 500
layer_5_fc = brew.fc(
model,
layer_4_pool,
"layer_5_fc",
dim_in=layer_5_input_dims,
dim_out=layer_5_output_dims,
)
# ReLU layer that operates on output from previous fully-connected layer
# Input and output are both of size 500
layer_6_relu = brew.relu(
model,
layer_5_fc,
"layer_6_relu",
)
完全连接层的输入大小为
。这个 3D 输入在传递给完全连接层时被展平为一个大小为 800 的向量。我们已经指定了该层的输出大小为500。因此,在训练过程中,这一层需要学习
个值,再加上一个偏置值,这些值将在推理时使用。完全连接层的输出被传递给一个 ReLU 层,作为激活函数。
第 7 层和第 8 层 – 完全连接层和 Softmax 层
LeNet-5 使用第二个完全连接层将输出减少到预测 10 个数字概率所需的10个值:
# Fully-connected layer that operates on output from previous ReLU layer
# Input is of size 500
# Output is of size 10, the number of classes in MNIST dataset
layer_7_input_dims = 500
layer_7_output_dims = 10
layer_7_fc = brew.fc(
model,
layer_6_relu,
"layer_7_fc",
dim_in=layer_7_input_dims,
dim_out=layer_7_output_dims,
)
最终的 SoftMax 层将完全连接层的 10 个输出值转换为概率分布:
# Softmax layer that operates on output from previous fully-connected layer
# Input and output are both of size 10
# Each output (0 to 9) is a probability score on that digit
layer_8_softmax = brew.softmax(
model,
layer_7_fc,
"softmax",
)
训练层
在前面的章节中,我们构建了 LeNet 网络的层结构,这些层是推理所需的,并且添加了图像像素的输入以及与每张图像对应的标签。在这一部分,我们在网络的末端添加了一些层,旨在计算损失函数并进行反向传播。这些层只在训练过程中需要,使用训练好的网络进行推理时可以丢弃。
损失层
正如我们在训练简介部分中提到的,我们需要在网络的最后设置一个loss函数,用来确定网络的误差。Caffe2 在其操作符目录中提供了许多常见的损失函数的实现。
在本示例中,我们使用分类交叉熵损失计算损失值。该损失通常用于衡量分类模型的性能,其输出介于0和1之间。在 Caffe2 中,这个损失可以通过两个操作符的组合来实现,分别是LabelCrossEntropy和AveragedLoss,如以下所示:
# Compute cross entropy between softmax scores and labels
cross_entropy = train_model.LabelCrossEntropy([softmax_layer, label], "cross_entropy")
# Compute the expected loss
loss = train_model.AveragedLoss(cross_entropy, "loss")
优化层
在训练介绍一节中,我们提到过,基于梯度的优化算法是训练过程的核心。
我们首先指示 Caffe2 使用我们之前添加的损失层的输出,在训练期间的反向传播计算梯度:
# Use the average loss we just computed to add gradient operators to the model
train_model.AddGradientOperators([loss])
AddGradientOperators 方法免去了我们显式指定这些操作符的麻烦,并将其添加到网络中。
最后,我们指定基于梯度的优化算法随机梯度下降(SGD)用于我们的训练:
# Specify the optimization algorithm
optimizer.build_sgd(
train_model,
base_learning_rate=0.1,
policy="step",
stepsize=1,
gamma=0.999,
)
我们指定了重要的 SGD 参数,例如使用的学习率、调整学习率的策略、步长和 gamma。
优化算法在 Caffe2 中作为 Optimizer 实现。该深度学习框架实现了许多常见的优化算法,包括 SGD、Adam、AdaGrad、RMSProp 和 AdaDelta。在我们之前的调用中,我们使用了 optimizer 模块提供的有用包装器 build_sgd,该包装器为我们配置了 SGD 优化器。
准确率层
最后,我们通过调用 add_accuracy_op 方法来指示应该跟踪模型的准确性,该方法包含如下语句:
brew.accuracy(model, [softmax_layer, label], "accuracy")
请注意函数调用的第二个参数。这表明 Caffe2 应该将 SoftMax 层的输出与真实标签进行比较,以确定模型的准确性。
accuracy 层有助于对训练过程进行人工监督。我们可以在训练过程的任何阶段执行推理,并通过准确率层的输出,了解网络在该阶段的准确性。
训练与监控
我们通过在工作空间中创建网络并初始化网络的所有参数 blob 来开始训练过程。这是通过调用工作空间的 RunNetOnce 方法完成的:
# The parameter initialization network only needs to be run once.
workspace.RunNetOnce(train_model.param_init_net)
接下来,我们请求 Caffe2 在内存中创建网络:
# Creating an actual network as a C++ object in memory.
# We need this as the object is going to be used a lot
# so we avoid creating an object every single time it is used.
workspace.CreateNet(train_model.net, overwrite=True)
我们终于准备好开始训练。我们会迭代一个预定的次数,并在每次迭代中使用工作空间的 RunNet 方法运行前向传播和反向传播。
训练像我们的 LeNet 模型这样的小型网络在 CPU 和 GPU 上都非常快速。然而,许多真实的模型训练可能需要数小时甚至数天。因此,建议通过在每次训练迭代后提取损失和准确性值来持续监控训练过程。
对于我们的 LeNet 模型,我们使用以下代码从我们先前添加到训练网络的损失层和准确率层的输出 blobs 中提取每次训练迭代后的损失和准确性值:
# Run the network for some iterations and track its loss and accuracy
total_iters = 100
accuracy = np.zeros(total_iters)
loss = np.zeros(total_iters)
for i in range(total_iters):
workspace.RunNet(train_model.net)
accuracy[i] = workspace.blobs["accuracy"]
loss[i] = workspace.blobs["loss"]
print("Iteration: {}, Loss: {}, Accuracy: {}".format(i, loss[i],
accuracy[i]))
我们可以通过查看原始值、将其导入电子表格或绘制图表来监控训练的健康状况。图 3.7 显示了从一次训练会话的值绘制的图表:

图 3.7:训练我们模型的损失与准确性
我们可以看到,训练开始时损失值较高。这是预期的,因为我们通常以零或随机的权重初始化网络。随着训练的进行,损失值逐渐减小,相应地,网络的准确度也在提高。如果你没有看到损失值减少或准确度提升,那么说明我们的训练参数或训练数据可能存在问题。如果训练进度缓慢或出现值爆炸的情况,可能需要调整学习率等参数。
通常,我们可以在损失曲线趋于平稳且准确度适当时停止训练。为了便于在某一迭代时导出模型,最好在每次迭代后将模型导出到磁盘(如第五章中所示,与其他框架的协作)。这样,训练完成后,你可以选择在最佳迭代时保存的模型。
另一个有用的做法是,在每个 epoch 后,使用验证数据集来测量准确度。验证数据通常是从训练数据中分离出来的一部分,事先为此目的而准备。为了简化起见,我们在示例中没有使用验证数据。
总结
在本章中,我们学习了使用基于梯度的优化算法进行神经网络的一般训练过程。我们了解了卷积神经网络(CNN)及经典的 LeNet CNN,并使用它解决了 MNIST 问题。我们构建了这个网络,并学习了如何向其添加训练层和测试层,以便用于训练。最后,我们使用这个网络进行训练,并学习了如何在训练过程中使用 Caffe2 监控网络。在接下来的章节中,我们将学习如何使用其他框架(如 Caffe、TensorFlow 和 PyTorch)训练的模型。
第四章:使用 Caffe
在第二章中,组合网络,以及第三章中,训练网络,我们分别学习了如何组合网络和训练网络。在本章中,我们将探讨 Caffe2 与 Caffe 之间的关系,并看看如何在 Caffe2 中使用 Caffe 模型,反之亦然。
本章的目标如下:
-
Caffe 与 Caffe2 的关系
-
AlexNet 简介
-
构建和安装 Caffe
-
Caffe 模型文件格式
-
Caffe2 模型文件格式
-
将 Caffe 模型转换为 Caffe2
-
将 Caffe2 模型转换为 Caffe
Caffe 与 Caffe2 的关系
在 2012 年举办的NIPS学术会议上,Alex Krizhevsky 及其合作者,包括神经网络先驱 Geoffrey Hinton,展示了ImageNet 大规模视觉识别竞赛(ILSVRC)的突破性结果。研究团队在使用 ImageNet 数据集的各种图像识别任务中竞争。Krizhevsky 在图像分类任务上的结果比当时的技术水平提高了 10.8%。他首次利用 GPU 训练了一个具有多层的 CNN。这种网络结构后来被广泛称为AlexNet。设计这样一个具有大量层次的深度神经网络正是深度学习领域得名的原因。Krizhevsky 分享了他的整个网络源代码,现在称为cuda-convnet,以及高度 GPU 优化的训练代码。
不久之后,贾扬清及其来自加州大学伯克利视觉与学习中心(BVLC)的合作者尝试复制这些结果,将其软件命名为DeCaf。这个库后来经过改进和简化,并以Caffe的名义发布。
与其时代大多数存在缺陷和设计不良的研究代码不同,Caffe 是一个设计良好的深度学习库,通过 prototxt 文本文件轻松组合网络。其设计具有模块化特性,使得研究人员可以轻松添加新层和训练算法。这使得 Caffe 在 2012 年至 2016 年期间非常流行。图像识别领域的大多数重要网络和模型都是在 Caffe 中发布的。这就是为什么 Caffe 是一个重要的深度学习框架,你可能仍然会发现几个经典模型只能在 Caffe 中使用的原因。
与此同时,人们对 Caffe 的替代方案产生了越来越大的兴趣。这是因为 Caffe 开始暴露出其局限性。尽管在 2014 年底添加了 Python API,但 Caffe 主要是一个 C++库。这个 C++的要求导致了实验和开发的速度较慢。Caffe 还主要是为图像识别问题设计的。实践者发现,很难为解决其他问题(如语音识别)添加功能。其他有用的功能,如不同精度数据类型的利用、量化和多 GPU 训练,在 Caffe 中也没有。这些功能后来被痛苦地添加到 Caffe 中,但在工程和维护方面并不理想。
这些问题促使了新一代深度学习库的出现,这些库以易用性、分布式训练和定制化为核心,逐渐流行起来。这些库包括 TensorFlow 和 PyTorch。
颜清佳从大学转到 Facebook,并领导创建了现代深度学习库 Caffe2,本书的主题。因为他也创建了 Caffe,所以 Caffe2 借鉴了许多 Caffe 的好点子,并且被构建为与 Caffe 兼容。
AlexNet 简介
我们在前面的部分中提到过 AlexNet,它是一个开创性的网络结构,因为它首次采用了大量的层,并展示了如何通过利用 GPU 在合理的时间内训练如此深的神经网络。
图 4.1 显示了由 Caffe 的网络可视化工具draw_net.py生成的 AlexNet 网络结构。此工具使用 GraphViz 库来呈现图形布局:

图 4.1:使用 GraphViz 布局的 AlexNet 网络结构
在此可视化中,层被绘制为矩形,而层之间的数据张量被绘制为拉长的八边形。例如,输入层之后的第一个矩形表示一个名为conv1的卷积层。它使用大小为
的卷积核,步幅为4,填充为0。
从图 4.1中检查 AlexNet 结构时,我们可以看到,AlexNet 在精神上与我们在第三章中看到的 LeNet 模型相似,训练网络。然而,与 LeNet 相比,AlexNet 有更多的卷积层和末端的全连接层。此外,它还用 ReLU 代替了传统的 tanh 和 sigmoid 层。Krizhevsky 在他的论文中描述了这些变化,以及一些训练创新和 GPU 的使用,如何使得训练这样一个深层网络变得可行。
在本章的其余部分,我们将使用 AlexNet 作为示例模型,学习如何理解 Caffe 和 Caffe2 网络描述语言,并学习如何在两者之间转换。
构建与安装 Caffe
BVLC 维护的 Caffe 版本可以从 github.com/BVLC/caffe 免费下载。由 NVIDIA 维护的 GPU 优化版 Caffe 可以从 github.com/NVIDIA/caffe 下载。接下来的讨论中,我们将使用 BVLC 版本的 Caffe,尽管 NVIDIA 的 Caffe 也应能成功构建并类似地工作。
请注意,Caffe 提供了使用 CMake 或 Make 进行构建。在本书中,我们将讨论 CMake 构建过程。如果你希望 Caffe 使用 GPU,你需要提前安装 CUDA 和 cuDNN 库。
安装 Caffe 的前置依赖
安装以下前置依赖:
- 首先,安装 Caffe 所依赖的库:
$ sudo apt install libboost-all-dev libprotobuf-dev libleveldb-dev libsnappy-dev libopencv-dev libhdf5-serial-dev protobuf-compiler libgflags-dev libgoogle-glog-dev liblmdb-dev
- 对于 CPU 上的 BLAS,最佳性能来自安装英特尔的 MKL 库。(MKL 的安装步骤已在第一章,“简介与安装”中描述。)如果没有 MKL,或者你不使用英特尔 CPU,那么可以安装 ATLAS 或 OpenBLAS:
$ sudo apt install libatlas-base-dev libopenblas-dev
- 要构建 Python 接口以使用 Caffe,请确保已安装以下软件包:
$ sudo apt install libboost-python-dev python-skimage python-protobuf
我们现在准备从源代码构建 Caffe。
构建 Caffe
要构建 Caffe,请按照以下步骤操作:
- 由于我们选择使用 CMake,构建过程既简单又直接:
$ mkdir build
$ cd build
$ cmake ..
$ make
- 要构建并运行 Caffe 单元测试,请执行以下命令:
$ make runtest
这可能需要相当长的时间才能完成。
- 要构建 Caffe 的 Python 接口,请执行以下命令:
$ make pycaffe
$ make install
- 默认情况下,安装目录将是
build目录下的一个子目录。将此build/install/python路径添加到PYTHONPATH环境变量中,然后再将 Caffe 导入 Python。
Caffe 模型文件格式
为了在 Caffe2 中使用 Caffe 模型,我们首先需要了解 Caffe 能够导出的模型文件格式。Caffe 会将训练好的模型导出为两个文件,如下所示:
-
神经网络的结构以
.prototxt文件形式存储 -
神经网络各层的权重存储为
.caffemodel文件
Prototxt 文件
prototxt 是一个文本文件,包含神经网络结构的信息:
-
神经网络中各层的列表
-
每一层的参数,如其名称、类型、输入维度和输出维度
-
各层之间的连接
Caffe 通过使用 Google 协议缓冲区(Protocol Buffers, ProtoBuf)序列化库将神经网络导出为序列化格式。prototxt 文件是神经网络结构在 ProtoBuf 文本格式中的序列化。
我们可以查看 Caffe 源代码中 models 目录下的一些流行 CNN 网络的 prototxt 文件。(参见构建与安装 Caffe部分,了解如何获取 Caffe 源代码。)你可能会在其中找到几个 prototxt 文件名,每个文件名都有不同的用途。
以下是一些典型 Caffe prototxt 文件名的含义:
-
deploy.prototxt:这个文件描述了可以用于推理的网络结构。它不包括通常用于训练网络的额外层。(我们在第三章,训练网络中查看了为训练网络添加的额外层或操作符。)这是我们通常希望的 prototxt 文件,如果我们希望将预训练的 Caffe 模型用于 Caffe2 中的推理。 -
train_val.prototxt:这个文件描述了用于训练的网络结构。它包括所有为帮助训练和验证过程而添加的额外层。如果我们希望将预训练的 Caffe 模型继续训练或微调到 Caffe2 中,这是我们通常需要的 prototxt 文件。
现在,让我们以 AlexNet 为例。(AlexNet 在本章前面介绍过。)AlexNet 预训练模型的一个版本可以在 Caffe 源代码中的 models/bvlc_alexnet 目录下找到。
下面是 AlexNet 的 deploy.prototxt 文件中的前两层:
$ head -26 models/bvlc_alexnet/deploy.prototxt
name: "AlexNet"
layer {
name: "data"
type: "Input"
top: "data"
input_param { shape: { dim: 10 dim: 3 dim: 227 dim: 227 } }
}
layer {
name: "conv1"
type: "Convolution"
bottom: "data"
top: "conv1"
param {
lr_mult: 1
decay_mult: 1
}
param {
lr_mult: 2
decay_mult: 0
}
convolution_param {
num_output: 96
kernel_size: 11
stride: 4
}
}
我们可以看到,prototxt 文件格式易于人类阅读和修改。请注意,网络的名称是 "AlexNet"。我们可以在前面的代码片段中看到两个名为 "data" 和 "conv1" 的层。"data" 层是一个 Input 层,我们可以看到它要求输入的尺寸是
。"conv1" 层是一个 Convolution 层,我们可以看到它的许多参数,包括核大小为
和步幅为
。
用于描述神经网络的 Caffe prototxt 文件的语法,本身是在一个 caffe.proto 文本文件中描述的。这个文件是用 Google 协议缓冲语言编写的。你可以在 Caffe 源代码中的 src/caffe/proto/caffe.proto 找到这个文件。
作为一个例子,下面是来自 caffe.proto 文件的 ConvolutionParameter 部分描述:
message ConvolutionParameter {
optional uint32 num_output = 1; // The number of outputs for the layer
optional bool bias_term = 2 [default = true]; // whether to have bias
//terms
// Pad, kernel size, and stride are all given as a single value for equal
// dimensions in all spatial dimensions, or once per spatial dimension.
repeated uint32 pad = 3; // The padding size; defaults to 0
repeated uint32 kernel_size = 4; // The kernel size
repeated uint32 stride = 6; // The stride; defaults to 1
optional uint32 pad_h = 9 [default = 0]; // The padding height (2D only)
optional uint32 pad_w = 10 [default = 0]; // The padding width (2D only)
optional uint32 group = 5 [default = 1]; // The group size for group conv
// ... Omitted other details.
// ... Find all details in caffe.proto
}
通过查看这个,我们可以开始轻松理解 deploy.prototxt 中的卷积层参数,例如,num_outputs、kernel_size 和 stride 这些参数的含义。
通过这种方式,你可以理解遇到的任何 Caffe prototxt 文件。它本质上是一个层列表,包含层的名称和参数,并链接到前后层。有关特定层类型的信息,请参阅 caffe.proto 文件。
Caffemodel 文件
caffemodel 文件是一个二进制文件,保存了神经网络各层的权重。这个文件是训练好的神经网络的 ProtoBuf 二进制格式 序列化。使用二进制格式是因为需要存储表示权重的浮动点或整数值。这个文件通常很大,通常达到数百兆字节,因此通常需要单独下载。
对于 Caffe 提供的每个流行模型,其源代码中都有相应的 readme.md 文件,其中包含下载该网络的 caffemodel 文件所需的详细信息。例如,图 4.2 显示了 AlexNet 模型的 readme.md:

图 4.2:AlexNet 模型的 readme.md
下载 Caffe 模型文件
Caffe 在其源代码中提供了一个 Python 脚本 scripts/download_model_binary.py,用于下载模型的 caffemodel 文件。此脚本需要提供模型目录作为输入。例如,要下载 AlexNet 的 caffemodel 文件,可以调用以下命令:
$ python scripts/download_model_binary.py models/bvlc_alexnet/
此脚本在输入模型目录中寻找 readme.md(类似于 Figure 4.2 中的文件),从 readme.md 的序言中找出 caffemodel 的 URL,下载 caffemodel 文件,并通过将其 SHA1 哈希与序言中提供的哈希进行匹配,确保下载文件正确。
Caffe2 模型文件格式
要能够在 Caffe2 中使用 Caffe 模型,我们还需要了解 Caffe2 可以导入的模型文件格式。与 Caffe 类似,Caffe2 也使用 Protobuf 对其模型文件进行序列化和反序列化。Caffe2 从两个文件导入训练好的模型:
-
神经网络的结构以
predict_net.pb文件或predict_net.pbtxt文件存储 -
神经网络运算符的权重存储为
init_net.pb文件
predict_net 文件
predict_net 二进制文件通常命名为 predict_net.pb,保存神经网络中的运算符列表、每个运算符的参数以及运算符之间的连接。该文件以 ProtoBuf 二进制格式序列化神经网络结构。
我们可以观察到,与 Caffe 使用的文本序列化文件相比,Caffe2 使用了二进制序列化文件。在 Caffe2 中,这并不会带来太多麻烦,因为它有一个 Python API,可以在导入文件后轻松理解网络结构。
可选地,我们还可以使用 predict_net 文本文件,通常命名为 predict_net.pbtxt,这是一个等效于 predict_net 二进制文件的文本文件,但以 ProtoBuf 文本格式存储。
继续我们的 AlexNet 示例,该网络的第一个卷积层将出现在 predict_net.pbtxt 中,显示如下:
name: "AlexNet"
op {
input: "data"
input: "conv1_w"
input: "conv1_b"
output: "conv1"
type: "Conv"
arg {
name: "stride"
i: 4
}
arg {
name: "pad"
i: 0
}
arg {
name: "kernel"
i: 11
}
}
注意 predict_net 文本文件如何对人类非常易读,就像 Caffe 的 prototxt 文本文件一样。
描述 Caffe2 predict_net 文件的语法本身在 caffe2.proto 文本文件中描述。这是一个使用 Google protocol buffer 语言编写的文件。您可以在 Caffe2 源代码的 proto/caffe2.proto 中找到此文件。
这里是来自 caffe2.proto 的运算符定义:
// Operator Definition.
message OperatorDef {
repeated string input = 1; // the name of the input blobs
repeated string output = 2; // the name of output top blobs
optional string name = 3; // the operator name. This is optional.
// the operator type. This is needed to create the object from the
//operator
// registry.
optional string type = 4;
repeated Argument arg = 5;
// The device option that the operator should run under.
optional DeviceOption device_option = 6;
// Optionally, one can specify an engine when there are multiple
// implementations available simultaneously for one device type.
// If one specifies an engine but that engine does not exist in the
//compiled
// Caffe2 binary, Caffe2 will fall back to the default engine of that
//device
// type.
optional string engine = 7;
// ... Omitted other details.
// ... Find all details in caffe2.proto
}
我们可以看到,Caffe2 在更一般的术语中定义了操作符,而不是像 Caffe 那样明确地定义每个操作符(或层)。
init_net 文件
init_net 二进制文件,通常命名为 init_net.pb,包含神经网络操作符的权重。该文件是经过训练的神经网络在 ProtoBuf 二进制格式中的序列化。与 Caffe 的 caffemodel 文件类似,这个文件通常也会很大,通常在几百兆字节的数量级。之所以命名为 init_net,是因为文件中的权重可用于初始化网络中的操作符。
将 Caffe 模型转换为 Caffe2
为了在 Caffe2 中使用 Caffe 模型,我们需要将其从 Caffe 格式转换为 Caffe2 文件格式。Caffe2 提供了一个名为 python/caffe_translator.py 的脚本,可用于此目的。
例如,我们可以通过如下方式将 AlexNet 文件从 Caffe 转换为 Caffe2:
$ python python/caffe_translator.py path_to_caffe/models/bvlc_alexnet/deploy.prototxt path_to_caffe/models/bvlc_alexnet/bvlc_alexnet.caffemodel --init_net init_net.pb --predict_net predict_net.pb
运行此脚本会生成三个文件,分别是 predict_net.pb、predict_net.pbtxt 和 init_net.pb,用于 AlexNet:

图 4.3:Caffe2 中的 AlexNet 网络结构
图 4.3 显示了将 AlexNet 从 Caffe 模型转换为 Caffe2 后的网络结构图。该图的可视化是使用 Caffe2 net_drawer.py 工具生成的,该工具利用 GraphViz 来布局网络。你可以在 第七章,Caffe2 在边缘和云端 中找到有关 Caffe2 模型可视化的更多信息。
从图中可以看出,每个 Caffe 层都被替换成了 Caffe2 操作符。操作符以矩形表示,权重和数据张量以延长的八边形表示。通过查看第一个卷积操作符,我们注意到它有三个张量——一个用于数据(命名为 data),两个用于该操作符的权重和偏置(分别命名为 conv1_w 和 conv1_b)。
将 Caffe2 模型转换为 Caffe
在本章的前几节中,我们主要讨论了如何将 Caffe 模型转换为 Caffe2 模型。由于 Caffe 当前没有积极开发,而 Caffe2 部分是为了取代 Caffe2 创建的,因此大多数用户关注的就是将 Caffe 模型迁移到 Caffe2 的路径。
然而,如果你需要在 Caffe 中使用 Caffe2 模型,那么这个过程会更为艰难。目前似乎没有直接的方法将 Caffe2 模型转换为 Caffe。如果你确定 Caffe2 的操作符及其参数在 Caffe 中是完全支持的,那么你可以尝试通过一个中间格式,如 ONNX(参见 第五章,与其他框架的协作)来进行转换。
如果 ONNX 路径不可行,那么你可能需要手动执行以下任务:
-
导出 Caffe2 操作符、参数和模型的权重
-
手动创建一个 Caffe 网络,将 Caffe2 操作符与相应的 Caffe 层匹配
-
如果没有与操作符匹配的层,使用 C++ 实现新的 Caffe 层
-
手动加载权重到 Caffe 层并使用该 Caffe 网络进行推理
摘要
本章介绍了 Caffe 深度学习框架,并探讨了 Caffe 和 Caffe2 之间的关系。我们研究了 Caffe 和 Caffe2 的模型文件格式。以 AlexNet 为例网络,我们查看了如何将 Caffe 模型转换为 Caffe2 格式。最后,我们探讨了将 Caffe2 模型转换为 Caffe 的难点。
Caffe 是一个深度学习框架,已经进入生命周期的尾声,不再添加新特性。在下一章中,我们将讨论当代深度学习框架,如 TensorFlow 和 PyTorch,并了解如何在 Caffe2 和这些其他框架之间交换模型。
第五章:与其他框架合作
在第四章,与 Caffe 合作中,我们了解了 Caffe 及其与 Caffe2 的关系。我们研究了 Caffe 和 Caffe2 的模型文件格式,并以 AlexNet 为例,探讨了如何将一个预训练的 Caffe 模型导入 Caffe2。在本章中,我们将学习如何从其他流行的深度学习框架导出和导入到 Caffe2。同时,我们还将探讨如何使其他深度学习框架使用 Caffe2 训练的模型。
本章将涉及以下主题:
-
ONNX 模型格式
-
Caffe2 对 ONNX 的支持
-
如何将 Caffe2 模型导出为 ONNX 格式
-
如何将 ONNX 模型导入到 Caffe2
-
如何可视化 ONNX 模型
开放神经网络交换
开放神经网络交换 (ONNX),通常发音为on-niks,是一种表示计算图的格式,支持多种运算符和数据类型。这种格式足够通用,支持神经网络和传统机器学习模型。由 Facebook 和微软发起,这种格式迅速成为大多数深度学习框架中,导入和导出深度神经网络的流行格式。
安装 ONNX
ONNX 的源代码可以在线找到:github.com/onnx/onnx。其中包括格式定义和操作 ONNX 文件的脚本。用于从特定深度学习框架格式进行转换的库和工具通常由深度学习框架提供。
内建支持 ONNX 的深度学习框架包括 Caffe2、PyTorch、MXNet 和 Chainer。也有转换器可以在不同的深度学习框架之间进行转换,比如 TensorFlow。还有一些运行时可以在专用硬件加速器上使用 ONNX 模型。例如,TensorRT 提供了支持 ONNX 的推理运行时,用于 NVIDIA GPU,而 OpenVINO 则为 Intel CPU 提供了相应的支持。(我们将在第六章,将模型部署到加速器进行推理中讨论 TensorRT 和 OpenVINO。)
ONNX 的 Python 库可以轻松安装,使用以下命令在 Ubuntu 上进行安装:
$ sudo pip install onnx
你可以通过以下命令在终端执行来检查安装是否成功:
$ python -c "import onnx"
ONNX 格式
ONNX 是一个开源格式,其规范和源代码可以在线找到:github.com/onnx/onnx。在第四章,与 Caffe 合作中,我们观察到 Caffe2 和 Caffe 都使用 Google ProtoBuf 来定义数据结构,以便进行网络结构和权重的序列化和反序列化。ONNX 也使用 Google ProtoBuf,支持 ProtoBuf 的版本 2 和 3。
图的定义,如神经网络或一般任何机器学习模型的图,定义了图所包含的各种操作符、操作符的参数以及操作符之间的关系。这些信息的语法和语义在 ONNX 中定义为两种不同的表示方式。中间表示(IR)定义了构造,例如图、节点和张量。操作符定义了图中可能的各种操作符类型。
ONNX IR
ONNX 计算图及其数据类型的 ProtoBuf 定义可以在 ONNX 源代码中的onnx/onnx.in.proto文件中找到。这些也被称为 ONNX 的 IR。
通过检查前述文件中 ONNX 的 IR 定义,我们可以看到以下定义:
-
NodeProto:用于定义神经网络中的每一层或其他机器学习模型中的每个节点。 -
ModelProto:用于定义一个模型及其相关图。 -
GraphProto:用于定义神经网络的有向无环图(DAG)结构或其他机器学习模型的图结构。 -
TensorProto:用于定义一个 N 维张量。 -
TypeProto:用于定义 ONNX 数据类型。
ONNX 操作符
ONNX 中操作符的定义可以在 ONNX 源代码中的onnx/onnx-operators.in.proto文件中找到。我们可以在该文件中找到OperatorProto、OperatorSetProto和FunctionProto的定义。
ONNX 中所有支持的操作符的实际定义可以在 ONNX 源代码中的onnx/defs目录下的子目录中的 C++源文件defs.cc中找到。例如,许多常见的神经网络操作符可以在 ONNX 源代码中的onnx/defs/math/defs.cc和onnx/defs/nn/defs.cc文件中找到。
另一个例子是我们在第三章《训练网络》中介绍的 ReLU 操作符。该操作符在 ONNX 中的名称为Relu(注意小写lu),并且在 ONNX 源代码中的onnx/defs/math/defs.cc文件中定义如下:
static const char* Relu_ver6_doc = R"DOC(
Relu takes one input data (Tensor<T>) and produces one output data
(Tensor<T>) where the rectified linear function, y = max(0, x), is
applied to the tensor elementwise.
)DOC";
ONNX_OPERATOR_SET_SCHEMA(
Relu,
6,
OpSchema()
.SetDoc(Relu_ver6_doc)
.Input(0, "X", "Input tensor", "T")
.Output(0, "Y", "Output tensor", "T")
.TypeConstraint(
"T",
{"tensor(float16)", "tensor(float)", "tensor(double)"},
"Constrain input and output types to float tensors.")
.TypeAndShapeInferenceFunction(propagateShapeAndTypeFromFirstInput));
我们可以看到,每个操作符都是使用ONNX_OPERATOR_SET_SCHEMA宏定义的。这个宏在onnx/defs/schema.h源文件中定义,如下所示:
#define ONNX_OPERATOR_SET_SCHEMA(name, ver, impl) \
ONNX_OPERATOR_SET_SCHEMA_EX(name, Onnx, ONNX_DOMAIN, ver, true, impl)
我们可以看到,每个操作符定义都有三个组成部分:名称(name)、版本(ver)和实现(impl)。
因此,对于我们在前述定义中看到的Relu操作符示例,我们可以推导出以下特征:
-
名称:操作符在 ONNX 中的名称。在本例中,它是
Relu。请注意,单独的深度学习框架可能将这个名称映射到它们自己深度学习框架中的不同操作符或层名称。也就是说,ONNX 中的名称与深度学习框架中的对应名称不一定总是相同。 -
版本:该操作符定义的版本。在本例中,它是版本 6。
-
实现:
-
解释该操作符作用的文档字符串。在本例中,它如下所示:
"Relu 接受一个输入数据(Tensor
-
输入操作数。在这种情况下,是一个单一的张量。
-
输出操作数。在这种情况下,是一个单一的张量。
-
张量值的数据类型限制。在这种情况下,ONNX 声明它仅支持浮点数(32 位)、双精度(64 位)和 float16(16 位,有时称为半精度)作为张量值的数据类型。
-
用于推断张量操作数类型和形状的函数。在这种情况下,它声明输出张量必须具有与输入张量相同的类型和形状。它通过使用名为
propagateShapeAndTypeFromFirstInput的函数来实现这一点。
从前面定义的 Relu 操作符示例中,我们可以看到每个操作符定义中都包含了大量文档内容。所有这些内容用于自动生成完整的 ONNX 操作符文档。这个自动生成的文档可以在 ONNX 源代码中的 docs/Operators.md 文件中找到。当我们在寻找合适的 ONNX 操作符或尝试理解某个特定 ONNX 操作符的细节时,这是一个有用的参考。
例如,我们之前考虑过的 Relu 操作符的自动生成文档在图 5.1 中显示如下:

图 5.1:ONNX 中自动生成的 Relu 操作符文档
ONNX 在 Caffe2 中
Caffe2 原生支持 ONNX。这包括将 Caffe2 模型导出为 ONNX 格式的支持,以及直接导入 ONNX 模型进行 Caffe2 推理的支持。与 Caffe2 支持 ONNX 相关的 C++ 源文件可以在 Caffe2 源代码中的 onnx 目录找到。提供 ONNX 前端和后端支持的 Python 源文件可以在 Caffe2 源代码中的 python/onnx 目录找到。
onnx/onnx_exporter.h 和 onnx/onnx_exporter.cc 包含了将 Caffe2 模型导出为 ONNX 格式所需的定义。从 Caffe2 导出到 ONNX 的支持包括操作符、数据类型的映射以及数据转换等细节。
例如,在 onnx/onnx_exporter.cc 中,我们可以找到一些 Caffe2 操作符到 ONNX 操作符的映射:
const std::unordered_map<std::string, std::string>&
OnnxExporter::get_renamed_operators() const {
const static std::unordered_map<std::string, std::string> kRenamedOperators{
{"SpatialBN", "BatchNormalization"},
{"Conv1D", "Conv"},
{"Conv2D", "Conv"},
{"Conv3D", "Conv"},
{"ConvTranspose1D", "ConvTranspose"},
{"ConvTranspose2D", "ConvTranspose"},
{"ConvTranspose3D", "ConvTranspose"},
{"MaxPool1D", "MaxPool"},
{"MaxPool2D", "MaxPool"},
{"MaxPool3D", "MaxPool"},
{"AveragePool1D", "AveragePool"},
{"AveragePool2D", "AveragePool"},
{"AveragePool3D", "AveragePool"}};
return kRenamedOperators;
}
每个使用 ONNX 的深度学习框架都有这样的映射。这是因为每个深度学习框架通常都有自己独特的操作符或层命名方式,并且在定义操作符特性和操作符之间的关系时有不同的术语。因此,清晰而完整的映射对于深度学习框架能够将 ONNX 模型定义转化为其自身的图定义至关重要。
从 Caffe2 和 ONNX 之间的映射关系可以看出,Caffe2 的 SpatialBN 操作符在 ONNX 中被重命名为 BatchNormalization 操作符。同样,Caffe2 的 Conv2D 操作符在 ONNX 中被重命名为 Conv 操作符。
将 Caffe2 模型导出为 ONNX
Caffe2 模型可以通过 Python 轻松导出为 ONNX 格式。这使得大量其他深度学习框架可以使用我们的 Caffe2 模型进行训练和推理。Caffe2-ONNX 提供的frontend模块完成了导出的所有繁重工作。该模块位于 Caffe2 源代码中的python/onnx/frontend.py文件。
本书源代码中提供的ch5/export_to_onnx.py脚本展示了如何将现有的 Caffe2 模型导出为 ONNX 格式。举个例子,考虑将我们在第四章中创建的 AlexNet Caffe2 模型转换为 ONNX。我们将这个网络的操作符和权重分别导出到predict_net.pb和init_net.pb文件中。
我们可以调用 ONNX 转换脚本,按照以下方式将这个 Caffe2 模型转换为名为alexnet.onnx的 ONNX 文件:
./export_to_onnx.py predict_net.pb init_net.pb alexnet.onnx
让我们看看这个脚本中帮助我们从 Caffe2 导出到 ONNX 的相关部分。
首先是导入,如下代码所示:
import onnx import caffe2.python.onnx.frontend from caffe2.proto import caffe2_pb2
caffe2.proto.caffe2_pb2模块具备导入存储在protobuf格式中的 Caffe2 模型所需的功能。onnx和caffe2.python.onnx.frontend模块具备导出到 ONNX 格式所需的功能。
在以下脚本中,我们还定义了模型输入的名称和形状:
INPUT_NAME = "data"
INPUT_SHAPE = (1, 3, 227, 227)
在第四章,使用 Caffe中,你可能注意到输入层和参数是以 Caffe 的protobuf格式注解的。然而,这些信息并没有同时存储在 Caffe2 的protobuf格式和 ONNX 格式中。每次使用 Caffe2 和 ONNX 模型时,我们需要显式指定输入的名称和形状。
我们在这个示例中使用了 AlexNet 模型,该模型的输入名为data,输入形状为(1, 3, 227, 227)。请注意,并不是所有模型都有这个输入形状。例如,流行的 CNN 模型的输入形状是(1, 3, 224, 224)。
我们现在准备好使用caffe2_pb2方法读取 Caffe2 模型文件,示例如下:
# Read Caffe2 predict and init model files to protobuf
predict_net = caffe2_pb2.NetDef()
with open(predict_net_fpath, "rb") as f:
predict_net.ParseFromString(f.read())
init_net = caffe2_pb2.NetDef()
with open(init_net_fpath, "rb") as f:
init_net.ParseFromString(f.read())
我们需要读取predict_net.pb和init_net.pb这两个 Caffe2 模型文件,分别表示网络和其权重。我们通过使用熟悉的ParserFromString方法来实现这一点,该方法来自 Google 的 ProtoBuf Python 库。
接下来,我们应该初始化输入的数据类型和张量形状,并使用 Python 字典将这些信息与输入名称关联起来,如下所示:
# Network input type, shape and name
data_type = onnx.TensorProto.FLOAT
value_info = {INPUT_NAME: (data_type, INPUT_SHAPE)}
我们现在可以使用frontend模块的caffe2_net_to_onnx_model方法将 Caffe2 的protobuf对象转换为 ONNX 的protobuf对象,如下所示:
# Convert Caffe2 model protobufs to ONNX
onnx_model = caffe2.python.onnx.frontend.caffe2_net_to_onnx_model(
predict_net,
init_net,
value_info,
)
请注意,此转换方法需要转换所需的输入信息,这些信息存储在value_info中。
最后,我们可以使用 ProtoBuf 的SerializeToString方法将 ONNX 的protobuf对象序列化为字节缓冲区,然后将该缓冲区写入磁盘,如下所示:
# Write ONNX protobuf to file
print("Writing ONNX model to: " + onnx_model_fpath)
with open(onnx_model_fpath, "wb") as f:
f.write(onnx_model.SerializeToString())
ch5/export_to_onnx.py脚本的完整源代码如下所示:
#!/usr/bin/env python2
"""Script to convert Caffe2 model files to ONNX format.
Input is assumed to be named "data" and of dims (1, 3, 227, 227)
"""
# Std
import sys
# Ext
import onnx
import caffe2.python.onnx.frontend
from caffe2.proto import caffe2_pb2
INPUT_NAME = "data"
INPUT_SHAPE = (1, 3, 227, 227)
def main():
# Check if user provided all required inputs
if len(sys.argv) != 4:
print(__doc__)
print("Usage: " + sys.argv[0] + " <path/to/caffe2/predict_net.pb> <path/to/caffe2/init_net.pb> <path/to/onnx_output.pb>")
return
predict_net_fpath = sys.argv[1]
init_net_fpath = sys.argv[2]
onnx_model_fpath = sys.argv[3]
# Read Caffe2 model files to protobuf
predict_net = caffe2_pb2.NetDef()
with open(predict_net_fpath, "rb") as f:
predict_net.ParseFromString(f.read())
init_net = caffe2_pb2.NetDef()
with open(init_net_fpath, "rb") as f:
init_net.ParseFromString(f.read())
print("Input Caffe2 model name: " + predict_net.name)
# Network input type, shape and name
data_type = onnx.TensorProto.FLOAT
value_info = {INPUT_NAME: (data_type, INPUT_SHAPE)}
# Convert Caffe2 model protobufs to ONNX
onnx_model = caffe2.python.onnx.frontend.caffe2_net_to_onnx_model(
predict_net,
init_net,
value_info,
)
# Write ONNX protobuf to file
print("Writing ONNX model to: " + onnx_model_fpath)
with open(onnx_model_fpath, "wb") as f:
f.write(onnx_model.SerializeToString())
if __name__ == "__main__":
main()
在 Caffe2 中使用 ONNX 模型
在前面的部分中,我们将一个 Caffe2 模型转换为 ONNX 格式,以便与其他深度学习框架一起使用。在这一节中,我们将学习如何使用从其他深度学习框架导出的 ONNX 模型在 Caffe2 中进行推断。
Caffe2 ONNX 包中提供的backend模块使得将 ONNX 模型导入到 Caffe2 成为可能。这可以在 Caffe2 源代码中的python/onnx/backend.py文件中看到。
本书附带的ch5/run_onnx_model.py脚本展示了如何加载一个 ONNX 模型到 Caffe2,并在输入图像上使用该模型进行推断。
首先,我们需要导入 Python 模块以处理图像(PIL.Image)、Caffe2 和 ONNX(caffe2.python.onnx.backend):
# Std
import PIL.Image
import json
import sys
# Ext
import numpy as np
from caffe2.python import workspace
import onnx
import caffe2.python.onnx.backend
prepare_input_image方法从输入文件路径读取图像,并准备将其作为 blob 传递给 Caffe2,如下例所示:
def prepare_input_image(img_fpath):
"""Read and prepare input image as AlexNet input."""
# Read input image as 3-channel 8-bit values
pil_img = PIL.Image.open(sys.argv[1])
# Resize to AlexNet input size
res_img = pil_img.resize((IMG_SIZE, IMG_SIZE), PIL.Image.LANCZOS)
# Convert to NumPy array and float values
img = np.array(res_img, dtype=np.float32)
# Change HWC to CHW
img = img.swapaxes(1, 2).swapaxes(0, 1)
# Change RGB to BGR
img = img[(2, 1, 0), :, :]
# Mean subtraction
img = img - MEAN
# Change CHW to NCHW by adding batch dimension at front
img = img[np.newaxis, :, :, :]
return img
在上述代码中,我们首先使用PIL.Image模块从输入文件中读取图像作为 3 通道字节值。然后,我们将图像调整大小以符合 AlexNet 所需的大小,并使用 NumPy 更轻松地进行其余的图像处理。PIL 按照HWC(高度、宽度、通道)顺序读取图像通道,通道以RGB顺序排列。但是 AlexNet 期望数据按BGR通道和HW大小排列。因此,我们进行了格式转换。最后,我们从图像值中减去平均值,然后添加了一个批次维度到前面,重新格式化数据为NCHW格式。
如果您使用onnx包的load方法,从文件加载 ONNX 模型是非常简单的,如下所示:
model = onnx.load("alexnet.onnx")
最后,我们可以直接使用加载的 ONNX 模型进行推断,使用如下所示的predict_img_class方法:
def predict_img_class(onnx_model, img):
"""Get image class determined by network."""
results = caffe2.python.onnx.backend.run_model(onnx_model, [img])
class_index = np.argmax(results[0])
class_prob = results[0][0, class_index]
imgnet_classes = json.load(open("imagenet1000.json"))
class_name = imgnet_classes[class_index]
return class_index, class_name, class_prob
我们需要使用 Caffe2 ONNX 后端提供的run_model方法(caffe2.python.backend)来传递输入并获取模型推断后的结果。因为我们使用的是 ImageNet 模型,所以应该使用一个 JSON 文件,其中包含从 ImageNet 类索引号到其类名的映射。我们应该选择具有最高概率值的类索引,并找到其对应的 ImageNet 类名。
可视化 ONNX 模型
在使用 ONNX 模型时,有一个有用的工具可以帮助可视化网络结构。ONNX 自带了一个名为net_drawer.py的脚本。您可以在 ONNX 源代码库的onnx/onnx/tools目录中找到此工具。如果您从其 Python 包中安装了 ONNX,则可以在/usr/local/lib/python2.7/dist-packages/onnx/tools/net_drawer.py找到此脚本。
该脚本可用于将 ONNX 文件转换为图网络的有向无环图表示,采用 GraphViz DOT 格式。例如,考虑我们在前一节将 Caffe2 模型转换为 ONNX 模型时获得的 alexnet.onnx 文件。
我们可以使用以下命令将这个 AlexNet ONNX 文件转换为 DOT 文件:
$ python /usr/local/lib/python2.7/dist-packages/onnx/tools/net_drawer.py --input alexnet.onnx --output alexnet.dot
要将 DOT 文件转换为 PNG 图像文件进行查看,请使用以下命令:
$ dot alexnet.dot -Tpng -o alexnet.png
由此生成的图像展示了 AlexNet 的可视化效果。
另一个优秀的 ONNX 模型可视化工具是 Netron。该工具的使用方法在第七章,边缘和云中的 Caffe2 中有详细介绍。
总结
本章介绍了 ONNX 格式的细节,这是一种流行的深度学习模型表示格式。我们探讨了它如何表示中间表示和操作符。然后,我们查看了 Caffe2 对 ONNX 的支持。以 AlexNet 为例,我们演示了如何将 Caffe2 模型文件转换为 ONNX 格式。接着,我们又探讨了反向过程:将 ONNX 模型文件导入 Caffe2,并用于推理。最后,我们介绍了一个有用的工具,用于可视化 ONNX 文件的图形表示。
第六章:将模型部署到加速器上进行推理
在第三章,训练网络中,我们学习了如何使用 Caffe2 训练深度神经网络。在本章中,我们将专注于推理:将训练好的模型部署到现场,对新数据进行推理。为了实现高效的推理,训练好的模型通常会针对部署时的加速器进行优化。本章将重点讨论两种常见的加速器:GPU 和 CPU,以及可以用于在它们上部署 Caffe2 模型的推理引擎 TensorRT 和 OpenVINO。
本章我们将讨论以下主题:
-
推理引擎
-
NVIDIA TensorRT
-
Intel OpenVINO
推理引擎
常见的深度学习框架,如 TensorFlow、PyTorch 和 Caffe,主要设计用于训练深度神经网络。它们侧重于提供更有利于研究人员轻松实验不同网络结构、训练方案和技术的功能,以实现最佳训练精度,解决实际问题。在神经网络模型成功训练后,实践者可以继续使用相同的深度学习框架将训练好的模型部署用于推理。然而,也有更高效的推理部署解决方案。这些推理软件将训练好的模型编译成最适合所用加速器硬件的计算引擎,能够在延迟或吞吐量方面达到最佳性能。
与 C 或 C++ 编译器类似,推理引擎将训练好的模型作为输入,并对训练神经网络的图结构、层次、权重和格式应用多种优化技术。例如,它们可能会去除仅在训练过程中有用的层。引擎可能会将多个水平相邻的层或垂直相邻的层合并,以加快计算并减少内存访问次数。
虽然训练通常在 FP32(4 字节浮点数)下进行,但推理引擎可能会提供更低精度的数据类型进行推理,例如 FP16(2 字节浮点数)和 INT8(1 字节整数)。为了实现这一点,这些引擎可能会将模型的权重参数转换为较低精度,并可能使用量化技术。使用这些低精度数据类型通常能大幅加速推理,同时对训练网络的精度影响微乎其微。
目前可用的推理引擎和库通常侧重于优化训练好的模型,以适应特定类型的加速器硬件。例如,NVIDIA TensorRT 推理引擎(不要与 Google TensorFlow 深度学习框架混淆)专注于优化在 NVIDIA 显卡和嵌入式设备上进行推理的训练神经网络。同样,Intel OpenVINO 推理引擎专注于优化训练好的网络,以适应 Intel CPU 和加速器。
在本章的其余部分,我们将探讨如何使用 TensorRT 和 OpenVINO 作为推理引擎,在 GPU 和 CPU 上部署 Caffe2 模型进行推理。
NVIDIA TensorRT
TensorRT 是部署已训练模型到 NVIDIA GPU 上进行推理的最流行推理引擎。毫不奇怪,这个库及其工具集由 NVIDIA 开发,并且可以免费下载安装。每个新的 TensorRT 版本通常会与每个新的 NVIDIA GPU 架构发布同步,添加对新 GPU 架构的优化,并且支持新的层、运算符和深度学习框架。
安装 TensorRT
TensorRT 安装程序可以从developer.nvidia.com/tensorrt网站下载。安装包可用于 x86-64(Intel 或 AMD 64 位 CPU)计算机、PowerPC 计算机、嵌入式硬件(如 NVIDIA TX1/TX2)以及用于汽车的 NVIDIA Xavier 系统。支持的操作系统包括 Linux、Windows 和 QNX(用于汽车的实时操作系统)。
对于 Linux,支持多个 LTS 版本的 Ubuntu,例如 14.04、16.04 和 18.04。其他 Linux 发行版,如 CentOS/Red Hat,也得到支持。每个 TensorRT 包都是为特定版本的 CUDA 构建的,例如 9.0 或 10.0。典型的安装程序下载网页如图 6.1 所示:

图 6.1:TensorRT 版本 5.0 的安装程序网页。注意查看安装指南、Ubuntu、Red Hat、Windows 的安装包,以及用于嵌入式系统的 Jetpack 和用于汽车的 DRIVE。
你需要下载与硬件、操作系统和已安装 CUDA 版本匹配的 TensorRT 安装程序。例如,在我的 x86-64 笔记本电脑上运行 Ubuntu 18.04,已安装 CUDA 10.0。所以,我将下载与该配置匹配的安装程序。
下载完安装包后,请按照 TensorRT 安装指南文档中的说明进行安装。你可以在安装页面找到该指南的 PDF 文档(见图 6.1)。安装通常需要使用 Ubuntu 上的sudo dpkg -i命令,或在 Red Hat 上使用yum命令。如果你下载了.tar.gz压缩包,可以将其解压到你选择的位置。不论采用哪种安装方式,TensorRT 包包含以下组件:C++头文件、C++共享库文件、C++示例、Python 库以及 Python 示例。
使用 TensorRT
使用 TensorRT 进行推理通常涉及以下三个阶段:
-
导入预训练网络或创建网络
-
从网络构建优化后的引擎
-
使用引擎执行上下文进行推理
我们将在接下来的章节中详细介绍这三个阶段。
导入预训练网络或创建网络
模型是在深度学习框架中训练的,例如 Caffe2、Caffe、PyTorch 或 TensorFlow。一些实践者可能使用自己定制的框架来训练模型。第一步是在 TensorRT 中构建一个网络结构,并将这些深度学习框架模型中预训练的权重加载到 TensorRT 网络的各层中。此过程如图 6.2 所示,具体步骤如下:

图 6.2:如何在 TensorRT 中构建网络
如果您使用流行的深度学习框架训练了一个模型,那么 TensorRT 提供了 parsers(解析器)来解析您的预训练模型文件并从中构建网络。TensorRT 提供了一个名为 IONNXConfig 的 ONNX 解析器,可用于加载和导入已转换为 ONNX 格式的 Caffe2 预训练模型文件。有关如何将 Caffe2 模型转换为 ONNX 的信息,请参见 第五章,与其他框架一起使用。
TensorRT 提供了一个名为 ICaffeParser 的 Caffe 解析器,可以用来加载和导入您的 Caffe 模型。同样,它还提供了一个名为 IUffConfig 的 TensorFlow 解析器,用于加载和导入您的 TensorFlow 模型。
不是所有 Caffe2、ONNX 或其他框架中的层和运算符都能在 TensorRT 中得到支持。此外,如果您使用自己定制的训练框架训练了模型,则无法使用这些解析器。为了解决这种情况,TensorRT 提供了逐层创建网络的能力。TensorRT 不支持的自定义层可以通过 TensorRT 插件实现。通常,您需要在 CUDA 中实现不支持的层,以便与 TensorRT 达到最佳性能。这些用例的示例可以在与 TensorRT 一起发布的示例代码中找到。
无论您遵循上述哪一种过程,最终都会得到一个名为 INetworkDefinition 的 TensorRT 网络。该网络可以在第二阶段进行优化。
从网络构建优化引擎
一旦网络及其权重在 TensorRT 中表示,我们就可以对该网络定义进行优化。这个优化步骤是由一个名为 builder 的模块执行的。builder 应该在您计划稍后进行推理的相同 GPU 上执行。虽然模型是使用 FP32 精度训练的,但您可以请求 builder 使用较低精度的 FP16 或 INT8 数据类型,这些数据类型占用更少的内存,并且在某些 GPU 上可能具有优化的指令。此过程如图 6.3 所示,具体步骤如下:

图 6.3:TensorRT 中的构建过程以生成引擎
构建器会尝试各种针对你所运行 GPU 的特定优化。它会尝试适用于特定 GPU 架构和 GPU 型号的内核和数据格式。它会对所有这些优化机会进行计时,并选择最优的候选项。它所生成的网络优化版本被称为 引擎。这个引擎可以序列化成一个文件,通常称为 PLAN 文件。
使用引擎执行上下文进行推理
要在 TensorRT 中使用引擎进行推理,我们首先需要创建一个运行时。运行时可以在反序列化后从 PLAN 文件加载引擎。然后,我们可以从运行时创建一个或多个执行上下文,并使用它们进行运行时推理。这个过程如图 6.4 所示:

图 6.4:使用 TensorRT 引擎进行推理的过程
TensorRT API 和使用方法
TensorRT 提供了 C++ API 和 Python API,供你使用。这些 API 可用于执行前面各个阶段所示的所有操作。你可以查看 TensorRT 附带的示例,以了解如何编写自己的 C++ 和 Python 程序来实现这些操作。例如,TensorRT 附带的 sampleMNISTAPI 示例演示了如何构建一个简单的网络来解决 MNIST 问题(见 第二章,构建网络),并将预训练权重加载到每一层中。
要使用 C++ API,基本上需要包含 NvInfer.h 和相关的头文件,并编译你的程序。当你需要链接程序时,确保 libnvinfer.so 和其他相关的 TensorRT 库文件在你的 LD_LIBRARY_PATH 环境变量中。
TensorRT 附带一个名为 trtexec 的工具,可以用来实验导入预训练模型并用于推理。例如,我们将展示如何在 TensorRT 中使用来自 第五章 与其他框架的合作 的 AlexNet ONNX 模型进行推理。
首先,我们需要导入我们的 AlexNet ONNX 模型文件(从 Caffe2 protobuf 文件转换而来),并从中构建一个优化后的引擎文件。这可以通过以下方式使用 trtexec 完成:
./trtexec --onnx=/path/to/alexnet.onnx --output=prob --saveEngine=alexnet.plan
--onnx 选项用于指定输入的 ONNX 文件。如果你要导入 Caffe 或 TensorFlow 模型,分别可以使用类似的 --deploy 和 --uff 选项。--output 选项用于指定模型的最终输出名称。如果模型有多个输入或输出,可以使用多个 --input 和 --output 选项。--saveEngine 选项用于指定一个文件路径,工具将利用这个路径来序列化优化后的引擎。有关更多信息,请尝试 ./trtexec --help。
接下来,我们可以加载已保存的优化引擎,然后使用它进行推理,如下所示:
./trtexec --output=prob --loadEngine=alexnet.plan
该工具将 PLAN 文件反序列化为引擎,从引擎创建运行时,然后从运行时创建执行上下文。它使用此上下文运行一批随机输入,并报告该模型在所运行 GPU 上的推理运行时性能。trtexec 和所有 TensorRT 示例的源代码可在 TensorRT 包中找到。这些源代码是学习如何将 TensorRT 融入 C++ 或 Python 推理应用程序中的一个很好的教学辅助手段。
英特尔 OpenVINO
OpenVINO 由英特尔创建的库和工具组成,使你能够从任何框架优化训练好的深度学习模型,然后使用推理引擎在英特尔硬件上进行部署。支持的硬件包括英特尔 CPU、英特尔 CPU 中的集成显卡、英特尔 Movidius 神经计算棒和 FPGA。OpenVINO 可以免费从英特尔获得。
OpenVINO 包含以下组件:
-
模型优化器:一个工具,用于导入来自其他深度学习框架的训练好的深度学习模型,进行转换并优化它们。支持的深度学习框架包括 Caffe、TensorFlow、MXNet 和 ONNX。请注意,不支持 Caffe2 或 PyTorch。
-
推理引擎:这些是加载模型优化器生成的优化模型的库,并为你的应用程序提供在英特尔硬件上运行模型的能力。
-
演示和示例:这些简单的应用程序展示了 OpenVINO 的使用,并帮助你将其集成到你的应用程序中。
OpenVINO 旨在进行推理;它不提供研究新网络结构或训练神经网络的功能。使用 OpenVINO 是一个独立的大话题。在本书中,我们将重点介绍如何安装它、如何测试它,以及如何使用 Caffe2 模型进行推理。
安装 OpenVINO
在本节中,我们将介绍在 Ubuntu 上安装和测试 OpenVINO 的步骤。其他 Linux 发行版(如 CentOS)和其他操作系统(如 Windows)上的安装和测试步骤也类似。有关所有这些的指导,请参考适用于你操作系统的 OpenVINO 安装指南。该指南可在线查看并在安装程序中找到。
适用于你的操作系统或 Linux 发行版的 OpenVINO 安装文件可以从 software.intel.com/en-us/openvino-toolkit 下载。例如,对于 Ubuntu,它给我提供了下载可自定义包或单个大文件包的选项。图 6.5 展示了这些选项,如下所示:

图 6.5:Ubuntu 上 OpenVINO 安装程序的下载选项
下载的文件通常具有 l_openvino_toolkit_p_<version>.tgz 这样的文件名。解压缩此文件的内容到一个目录并进入该目录。您会在此找到两种格式的安装脚本:控制台和 GUI。可以按以下方式执行其中任何一个:
$ sudo ./install_GUI.sh
$ sudo ./install.sh
这两种选项都提供了一个有用的向导,帮助您选择安装 OpenVINO 文件的位置以及您希望安装的 OpenVINO 组件。如果在没有使用 sudo 的情况下运行脚本,它们会提供一个选项,将文件安装到您主目录下的 intel 子目录中。使用 sudo 运行则会帮助您将文件安装到 /opt/intel,这是大多数英特尔工具传统上安装的位置。
图 6.6 显示了安装过程中可以选择的 OpenVINO 组件。至少,我建议安装模型优化器、推理引擎和 OpenCV。如果您想读取图像并将其传递给推理引擎,则需要安装 OpenCV。图 6.6 如下所示:

图 6.6:可以通过 GUI 安装程序向导安装的 OpenVINO 组件
完成主安装后,我们还需要通过执行以下命令来安装 OpenVINO 的一些外部依赖项:
$ cd /opt/intel/openvino/install_dependencies
$ chmod +x install_openvino_dependencies.sh
$ sudo -E ./install_openvino_dependencies.sh
如果您没有使用 sudo 安装,您可以将前面的命令中的 /opt/intel 替换为您在主目录中安装 OpenVINO 的路径。
现在,我们准备设置 OpenVINO 所需的环境变量。我们可以通过以下命令来完成此操作:
$ source /opt/intel/openvino/bin/setupvars.sh
接下来,我们配置 OpenVINO 以支持我们要导入的深度学习框架模型。我们可以通过以下命令拉取所有受支持的深度学习框架的配置:
$ cd /opt/intel/openvino/deployment_tools/model_optimizer/install_prerequisites
$ sudo ./install_prerequisites.sh
现在我们准备测试我们的 OpenVINO 安装是否正常工作。我们可以通过运行一个 OpenVINO 演示来完成这一操作,该演示会下载一个使用 Caffe 训练的 SqueezeNet 模型,使用模型优化器优化它,并通过推理引擎在一张汽车图像上运行,具体操作如下:
$ cd /opt/intel/openvino/deployment_tools/demo
$ ./demo_squeezenet_download_convert_run.sh
运行后,我们应该能够看到汽车图像的分类结果。概率最高的类别是运动型汽车,从而确认使用 OpenVINO 进行模型推理是正常的。这一点在图 6.7 中有显示,如下所示:

图 6.7:OpenVINO 在一张运动汽车图片上的演示分类结果
模型转换
OpenVINO 不支持 Caffe2 模型格式。然而,它支持流行的 ONNX 模型表示方式。因此,要在 OpenVINO 中使用 Caffe2 模型,我们需要遵循一个两步流程。
首先,我们需要将 Caffe2 模型转换为 ONNX 格式。此过程在 第五章,与其他框架的协作 中有详细描述。之后,我们可以使用 OpenVINO 模型优化器来导入、优化并将其转换为 OpenVINO 中间表示(IR)格式。
让我们查看使用模型优化器处理 AlexNet 模型的过程,该模型我们在第五章中已转换为 ONNX,与其他框架的协作。我们已将 AlexNet Caffe2 模型转换为生成alexnet.onnx文件。
要使用模型优化器将此 AlexNet ONNX 模型转换为 OpenVINO IR,我们可以使用以下mo.py脚本:
$ cd /opt/intel/openvino/deployment_tools/model_optimizer
$ python3 mo.py --input_model /path/to/alexnet.onnx
Model Optimizer arguments:
Common parameters:
- Path to the Input Model: /path/to/alexnet.onnx
- Path for generated IR: /opt/intel/openvino_2019.1.094/deployment_tools/model_optimizer/.
- IR output name: alexnet
- Log level: ERROR
- Batch: Not specified, inherited from the model
- Input layers: Not specified, inherited from the model
- Output layers: Not specified, inherited from the model
- Input shapes: Not specified, inherited from the model
- Mean values: Not specified
- Scale values: Not specified
- Scale factor: Not specified
- Precision of IR: FP32
- Enable fusing: True
- Enable grouped convolutions fusing: True
- Move mean values to preprocess section: False
- Reverse input channels: False
ONNX specific parameters:
Model Optimizer version: 2019.1.0-341-gc9b66a2
[ SUCCESS ] Generated IR model.
[ SUCCESS ] XML file: /opt/intel/openvino_2019.1.094/deployment_tools/model_optimizer/./alexnet.xml
[ SUCCESS ] BIN file: /opt/intel/openvino_2019.1.094/deployment_tools/model_optimizer/./alexnet.bin
[ SUCCESS ] Total execution time: 6.48 seconds.
这个转换过程会生成三个文件:
-
.bin文件:该文件包含模型的权重。这也是为什么这个文件通常较大的原因。 -
.xml文件:这是一个包含网络结构的 XML 文件。此文件中存储的详细信息包括关于模型的元数据、层列表、每个层的配置参数,以及层之间的边缘列表。 -
.mapping文件:这是一个 XML 文件,包含从输入文件层到输出 OpenVINO 文件层的映射。
我们只需要.bin文件和.xml文件即可使用 OpenVINO 推理引擎进行模型推理。
模型推理
OpenVINO 提供了 C++ 和 Python 版本的推理引擎 API。这个 API 可以用于为训练好的 Caffe2 模型程序化创建网络结构。然后,您可以将每个网络层的权重加载到 OpenVINO 网络中,并在 Intel 硬件上进行推理。如果 OpenVINO 当前不支持您的训练模型所使用的网络层或操作符类型,那么您需要使用 OpenVINO 插件层来实现这一点。所有这些努力都是值得的,因为一旦 Caffe2 训练的模型在 OpenVINO 推理引擎上运行后,您将受益于延迟和吞吐量的提升。
对于大多数网络,有一个更简单的替代方法:使用 OpenVINO 模型优化器将 Caffe2 训练的模型转换为 OpenVINO IR。我们在前一节中已经介绍了如何执行此操作。完成此步骤后,可以使用 OpenVINO 推理引擎中的功能自动导入此 IR 模型进行推理。OpenVINO 提供了许多推理引擎示例,可以用来尝试此过程。
记得在执行此操作前运行/opt/intel/openvino/bin/setupvars.sh脚本。此脚本设置了 OpenVINO 使用所需的环境变量和配置。
转到推理引擎的samples目录,查看各种样本。这里有适用于许多常见用例的样本。例如,包含用于测试分类模型、测试物体检测模型、测试文本检测、测试延迟和吞吐量性能等样本。
要构建所有的推理引擎样本,请按照以下步骤进行:
$ cd /opt/intel/openvino/deployment_tools/inference_engine/samples
$ ./build_samples.sh
推理引擎样本将使用 CMake 构建。样本的二进制文件将安装到你主目录下名为inference_engine_samples_build/intel64/Release的目录中。
这些示例使得快速尝试在 IR 模型上使用 OpenVINO 推理引擎变得非常方便。这些示例可能使用一些与 OpenVINO 一起安装的额外库。因此,如果你发现某个示例缺少某个库(.so文件),你可能需要将该库的路径添加到LD_LIBRARY_PATH环境变量中。
我发现使用以下LD_LIBRARY_PATH对我有效:
$ export LD_LIBRARY_PATH=/opt/intel/openvino/deployment_tools/inference_engine/external/mkltiny_lnx/lib:/opt/intel/openvino/deployment_tools/inference_engine/lib:.:$LD_LIBRARY_PATH
最简单的示例之一是hello_classification。该示例接受两个输入:一个 OpenVINO IR 分类模型的路径和一个图像文件的路径。它通过导入 IR 模型并使用图像运行推理,创建了一个 OpenVINO 推理引擎。
要在我们之前从 AlexNet Caffe2 模型创建的 IR 模型上尝试 OpenVINO 推理引擎,请使用以下命令:
$ ./hello_classification /path/to/alexnet.xml /path/to/sunflower.jpg
Top 10 results:
Image /path/to/sunflower.jpg
classid probability
------- -----------
985 0.9568181
309 0.0330606
328 0.0035365
946 0.0012036
308 0.0007907
310 0.0005121
723 0.0004805
108 0.0004062
950 0.0003640
947 0.0003286
我们可以看到,hello_classification示例成功地将 IR 模型加载到其推理引擎中,并对输入的向日葵图像进行了分类。它报告了 ImageNet 类 985(雏菊)作为最高分,这是在 1,000 个 ImageNet 类中与向日葵最匹配的类别。
OpenVINO 推理引擎可以用于执行 FP16 和 INT8 模式的推理。有关详细信息,请参阅 OpenVINO 文档。
摘要
在本章中,我们了解了推理引擎及其作为加速器上部署训练好的 Caffe2 模型的重要工具。我们重点讨论了两种流行的加速器:NVIDIA GPU 和 Intel CPU。我们学习了如何安装和使用 TensorRT 来将 Caffe2 模型部署到 NVIDIA GPU 上。我们还了解了如何安装和使用 OpenVINO,将 Caffe2 模型部署到 Intel CPU 和加速器上。
许多其他公司,如谷歌、Facebook、亚马逊以及像 Habana 和 GraphCore 这样的初创公司,正在开发用于推理 DL 模型的新型加速器硬件。还有像 ONNX Runtime 这样的努力,旨在将来自多个供应商的推理引擎汇聚到一个平台下。请评估这些选项,并选择最适合部署 Caffe2 模型的加速器硬件和软件。
在下一章中,我们将了解在树莓派上使用 Caffe2 进行边缘计算、在云端使用容器的 Caffe2 以及 Caffe2 模型可视化。
第七章:边缘计算中的 Caffe2 和云端中的 Caffe2
在本书的第 1 到第六章中,我们已经学习了如何安装和使用 Caffe2 来训练深度学习(DL)神经网络,以及如何与其他流行的 DL 框架一起工作。我们还学习了如何将训练好的 Caffe2 模型部署到流行的推理引擎中。在本章的最后,我们将探讨 Caffe2 的应用,特别是它如何从小型边缘设备(如 Raspberry Pi)扩展到在云中的容器运行。我们还将学习如何可视化 Caffe2 模型。
本章将涵盖以下主题:
-
在 Raspberry Pi 上的 Caffe2 边缘计算
-
使用容器在云中运行 Caffe2
-
Caffe2 模型可视化
在 Raspberry Pi 上的 Caffe2 边缘计算
在边缘计算中使用深度学习的兴趣非常大。这是将深度学习应用于在设备上或靠近设备进行计算的方案,这些设备使用传感器和摄像头捕捉数据。另一种深度学习边缘计算的替代方案是将边缘数据捕获后发送到云中进行处理。然而,边缘计算中的深度学习具有较低的延迟和更高的安全性优势。边缘设备通常便宜、小巧且功耗较低,处理器或加速器的计算能力较弱。Caffe2 的一个关键优势是它从一开始就被设计和开发为具有可扩展性:从多 GPU、多 CPU 服务器到微小的边缘设备。在这一节中,我们将以 Raspberry Pi 作为边缘设备的例子,学习如何在其上使用 Caffe2。
Raspberry Pi(树莓派)
Raspberry Pi 是一系列由英国 Raspberry Pi 基金会推出的单板通用计算机。图 7.1 展示的是 Raspberry Pi B+ 单元的最新 Rev3 板,如下所示:

图 7.1:2018 年发布的 Raspberry Pi B+ Rev3 板
自 2012 年推出以来,Raspberry Pi(树莓派)已经在全球掀起了一股热潮,广泛应用于学校教学、个人项目和边缘计算的实际部署。每个树莓派的价格大约为 $35,非常适合各种类型的项目。树莓派计算机如此有用的原因在于其小巧的外形,它大约和一副扑克牌的大小相当。树莓派需要的电力很少,只需要一个 5V 的 micro-USB 电源供应。树莓派是一台完全通用的计算机,配备了常见的存储和 I/O 接口,如 SD/microSD 卡插槽、USB 接口、无线连接、以太网端口、HDMI 输出和复合视频输出。树莓派相比其他同类设备的最大优势之一是它提供了 Raspbian 操作系统,这是一个专门为树莓派移植的流行 Debian Linux 发行版。通过 Raspbian,树莓派用户可以使用与主流 Linux 发行版上相同的工具、编译器和编程库。
我们在 Raspberry Pi 上进行的 Caffe2 实验将涉及以下步骤:
-
安装 Raspbian
-
在 Raspbian 上构建和使用 Caffe2
安装 Raspbian
按照以下步骤安装 Raspbian:
-
从
www.raspberrypi.org/downloads/raspbian/下载 Raspbian 版本。每个 Debian 版本都有一个对应的 Raspbian 版本。最新的 Debian 版本 9 被称为 Stretch,对应的 Raspbian 被称为 Raspbian 9 或 Raspbian Stretch。 -
选择适合你的 Raspbian 版本。为了满足不同的应用需求,提供了三种类型的 Raspbian Stretch 套件。对于我们的目的,最小的套件 Raspbian Stretch Lite 就足够了。如果你想使用桌面环境和图形应用程序,可以尝试其他带有这些功能的套件。一旦你的 Raspbian 连接到网络,你可以通过 SSH 进入并获得完全访问权限,使用 Bash shell 执行命令、控制台工具和编辑器。如果你需要,之后也可以选择安装其他图形应用程序。Stretch Lite 足以满足所有这些需求。
-
选择一个工具将 Raspbian 镜像刷写到 SD 卡中。一个推荐的易用工具是 Etcher。你可以从
www.balena.io/etcher/下载它。 -
安装 Etcher 后,将一个最小容量为 4 GB 的 SD 卡插入电脑的 SD 卡槽。使用 Etcher 将 Raspbian 镜像刷写到 SD 卡中。
Raspberry Pi 可以作为一个无头计算机使用,通过 SSH 登录而不是直接本地操作。如果你希望从 Raspbian 的第一次启动就启用此功能,请将已经刷写的 SD 卡重新插入电脑。然后,在 SD 卡的根目录下创建一个名为 ssh 的空文件。
-
到此,我们已经完成了将 Raspbian 刷写到 SD 卡的操作。将这张 SD 卡插入 Raspberry Pi 板上的 SD 卡槽。确保 Pi 通过以太网电缆连接到你的家庭无线路由器。你还可以选择通过 HDMI 电缆将 Pi 连接到电视或电脑显示器,以便查看其启动信息。
-
打开 Pi 电源。你可以在电视或显示器上看到 Raspbian 的启动信息。在启动过程结束时,它会显示由 DHCP 分配的 IP 地址,并要求你进行本地登录。你也可以通过检查无线路由器的管理控制台来找出 Pi 的 IP 地址。现在,你可以使用以下命令从任何网络上的计算机通过 SSH 登录到 Raspbian。
$ ssh pi@<IP address of Pi>
-
使用默认密码:
raspberry。首次成功登录后,Raspbian 会提醒你更改默认密码。请通过在命令行中输入passwd命令来更改密码。从下次通过 SSH 登录到 Pi 时,可以使用此新密码。 -
最后,确保使用以下命令更新软件包仓库并更新已安装的包:
$ sudo apt update
$ sudo apt upgrade
在 Raspbian 上构建 Caffe2
Caffe2 已经移植到 Raspbian 上。但是,没有简单的方法可以从你的 x86_64 计算机交叉编译到树莓派,因此必须在树莓派本身上构建 Caffe2。
我们可以 SSH 连接到 Pi 并在其上克隆 Caffe2 Git 仓库。然而,完整的 PyTorch 和 Caffe2 仓库以及它们的子模块总大小超过 400 MB,这样的克隆操作在树莓派上可能需要很长时间才能完成。此外,请注意,克隆到 SD 卡比克隆到通过 USB 连接到树莓派的硬盘更快。后者速度非常慢,因为树莓派仅支持 USB 2.0(比 USB 3.0 慢),并且 USB 端口和以太网端口共享同一总线,进一步限制了 Git 克隆速度。
让我们开始在 Raspbian 上构建 Caffe 2:
- 由于在本地计算机上克隆最为简便,首先使用以下命令在本地进行克隆:
$ git clone --recursive https://github.com/pytorch/pytorch.git
$ cd pytorch
$ git submodule update --init
- 克隆完成后,通过删除 Git 仓库数据来减少此目录的大小,操作如下:
$ rm -rf .git
- 现在,将其压缩为
.tar.gz档案并通过 SSH 复制到 Pi,操作如下:
$ cd ..
$ tar zcvf pytorch.tar.gz pytorch
$ scp pytorch.tar.gz pi@<IP address of Pi>:/home/pi
- SSH 连接到 Pi 并解压复制到那里的压缩档案,操作如下:
$ tar xvf pytorch.tar.gz
$ cd pytorch
- 用于在 Raspbian 上构建 Caffe2 的脚本是
scripts/build_raspbian.sh。请注意,这个 Raspbian 构建最近没有得到维护。因此,在运行它之前,我们需要安装一些对于成功编译至关重要的 Python 包,具体如下:
$ sudo apt install python-future python-typing python-yaml
$ sudo pip install -U protobuf
- 我们现在准备好通过调用以下脚本来进行构建:
$ cd scripts
$ ./build_raspbian.sh
- 就像我们在第一章中使用的构建过程一样,简介与安装,这个过程也使用了 CMake,首先配置构建过程,然后调用
make来构建必要的组件,并将构建的产物放入build子目录中。
请注意,构建过程需要很长时间,可能会花费半天时间。树莓派的内存为 500 MB 到 1 GB(取决于你使用的 Pi 型号),而 Raspbian 默认只分配大约 100 MB 的交换空间。因此,构建过程有时会因为内存不足而失败。如果发生这种情况,你可以通过打开/etc/dphys-swapfile文件并增加CONF_SWAPSIZE值来增加交换空间。我发现将其从100增加到1000就足以成功编译。
编译完成后,你可以像我们在第一章中所做的那样,安装并测试 Caffe2,简介与安装,如以下示例所示:
$ cd ../build
$ sudo make install
$ python -c "from caffe2.python import core"
现在你已经在树莓派上成功运行 Caffe2。你可以连接传感器或摄像头模块,读取其中的图像和数据,并通过深度学习网络进行分类、检测和理解。
使用容器在云端运行 Caffe2
容器现在是一个普遍存在且必要的工具,用于在生产环境中强健地部署软件,既可以在本地,也可以在云端。它们使开发者能够为应用程序创建理想的软件环境,并确保在开发者工作站、测试计算机、预发布计算机以及最终部署到本地服务器或云实例时,完全复制这一软件环境。容器还帮助为每个应用程序创建一个独立的软件环境,在同一服务器上运行多个应用程序时,为每个应用程序创建一个专属的环境。
在众多可用的容器工具中,Docker 是最受欢迎的。我们将在本节中重点介绍如何使用 Docker。Docker 适用于所有流行的 Linux 发行版、macOS X 和 Windows。通过 Docker,你可以从指定的 Ubuntu 版本创建一个 Ubuntu 软件环境,并在不同版本的 RedHat 主机操作系统上运行你的 Caffe2 应用程序。Docker 使得这种多样化的部署变得轻松可行,并且只需几分钟。
安装 Docker
按照以下步骤进行安装:
-
要使用与你的操作系统或发行版特定的软件包仓库和包来安装 Docker,请按照这里的说明进行操作
docs.docker.com/engine/installation/linux/docker-ce/ubuntu/。 -
安装成功后,记得使用如下命令将你的用户名添加到
docker用户组中:
$ sudo adduser myusername docker
为了使这个小组的更改生效,你可能需要注销并重新登录。
- 最后,为了测试你的 Docker 设置是否正确,可以运行
hello-world镜像。如果成功,你将看到类似于以下示例的欢迎信息:
$ docker run hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
1\. The Docker client contacted the Docker daemon.
2\. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3\. The Docker daemon created a new container from that image which
runs the executable that produces the output you are currently
reading.
4\. The Docker daemon streamed that output to the Docker client,
which sent it to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/
For more examples and ideas, visit:
https://docs.docker.com/get-started/
作为最后的实验,你可以在一个 Ubuntu 容器内获取 Bash shell,并使用以下命令在该 Ubuntu 实例内进行探索:
$ docker run --rm -it ubuntu bash
在这里,我们启动一个 Ubuntu 容器。-it 选项表示这是一个交互式会话。也就是说,我们希望运行应用程序(bash)并保持该会话,直到退出容器。这与正常流程(例如在 hello-world 容器中的操作)相反,后者会在 Docker 执行应用程序并完成后自动退出。--rm 选项表示 Docker 应在我们退出容器后自动销毁容器。通常,Docker 会将容器保留在后台,随时准备再次使用。
你会注意到 Docker 会以 root 用户身份登录,并且你会获得一个 root shell。你会被放置在文件系统的根目录下。root 权限仅在此 Docker 容器内有效。在容器内创建或更改的任何文件都是临时的。当你退出容器时,这些文件会丢失。
完成对 Ubuntu 容器的探索后,你可以通过按 Ctrl + D 或输入 exit 来退出。
安装 nvidia-docker
在执行前述步骤后,你可以在 Docker 中运行 Caffe2、Python 和 C++ 应用程序(CPU 版本)。但是,如果你想在 GPU 上运行 Caffe2 应用程序,那么你需要安装并使用 nvidia-docker。
NVIDIA-Docker 为运行在 Docker 内的应用程序提供完全和不受限制的访问权限,能够访问系统上的 NVIDIA GPU。请注意,此功能依赖于安装在主机系统上的 NVIDIA GPU 驱动程序。然而,你无需在主机系统上安装 CUDA 或 cuDNN,因为你可以启动一个容器,并在其中安装你想要的任何 CUDA 版本。这是一种方便的方式,可以在不同的 CUDA 版本下构建和测试应用程序。
NVIDIA Docker 安装的说明可以在 github.com/NVIDIA/nvidia-docker 上找到。撰写本文时,可以使用以下步骤安装 nvidia-docker:
- 首先,添加
nvidia-docker仓库并更新软件包缓存,操作如下:
$ curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add - distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
$ curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list
$ sudo apt-get update
- 接下来,安装 NVIDIA Docker 运行时,操作如下:
$ sudo apt-get install -y nvidia-docker2
- 最后,重启 Docker 守护进程,操作如下:
$ sudo pkill -SIGHUP dockerd
现在,我们准备好测试我们的 NVIDIA Docker 是否正常工作并能访问系统上的 NVIDIA GPU。为此,我们需要在容器内运行 nvidia-smi 应用程序,操作如下:
$ docker run --runtime=nvidia --rm nvidia/cuda:9.0-base nvidia-smi
nvidia-smi 是一个与主机系统上的 NVIDIA GPU 驱动程序通信的工具,用于打印系统上可用的 GPU 信息。如果你的 NVIDIA Docker 安装成功,你应该能够看到 nvidia-smi 列表、NVIDIA GPU 驱动程序版本和已安装的 GPU。
请注意我们在此命令中使用的 Docker 标签:nvidia/cuda:9.0-base。这是一个包含 CUDA 9.0 的 Docker 镜像。可用的 Docker 镜像和标签的完整列表可以在此处查看:hub.docker.com/r/nvidia/cuda/tags。每个 CUDA 版本兼容的 GPU 驱动版本的表格可以在 github.com/NVIDIA/nvidia-docker/wiki/CUDA 上找到。
在前述命令中,我们通过 --runtime=nvidia 选项指定了使用 NVIDIA Docker 运行时。我们也可以使用 nvidia-docker 别名来运行相同的命令,而无需指定运行时,操作如下:
$ nvidia-docker run --rm nvidia/cuda:9.0-base nvidia-smi
运行 Caffe2 容器
Caffe2 项目为不同版本的 Caffe2 和 Ubuntu 提供了 Docker 镜像,既有 CPU 版本,也有 GPU 版本。可用 Docker 镜像的完整列表可以在 hub.docker.com/r/caffe2ai/caffe2/tags 找到。Caffe2 镜像的 Docker 标签简明扼要地描述了其功能。例如,c2v0.8.1.cpu.min.ubuntu16.04 标签表示该镜像包含了 Ubuntu 16.04 上的 Caffe2 v0.8.1 CPU 版本。c2v0.8.1.cuda8.cudnn7.ubuntu16.04 标签表示该镜像包含了 Ubuntu 16.04 上带有 CUDA 8.1 和 cuDNN 7 的 Caffe2 v0.8.1 GPU 版本。
我们可以启动一个 Caffe2 CPU 镜像,并检查 Caffe2 是否在其中工作,方法如下:
$ docker run --rm -ti caffe2ai/caffe2:c2v0.8.1.cpu.min.ubuntu16.04
root@13588569ad8f:/# python -c "from caffe2.python import core"
WARNING:root:This caffe2 python run does not have GPU support. Will run in CPU only mode.
我们可以启动一个 Caffe2 GPU 镜像,并检查 Caffe2 是否在其中工作,方法如下:
$ nvidia-docker run --rm -ti caffe2ai/caffe2:c2v0.8.1.cuda8.cudnn7.ubuntu16.04
root@9dd026974563:/# python -c "from caffe2.python import core"
注意,如果我们使用的是 Caffe2 GPU 镜像,我们需要使用nvidia-docker而不是docker。
一旦你的 Caffe2 容器工作正常,你可以将 Caffe2 应用和数据挂载到容器中并执行它们。你可以使用-v选项将主机目录挂载到 Docker 容器中,并指定要挂载到的目标目录,如下所示:
$ docker run --rm -ti -v /home/joe/caffe2_apps:/joe_caffe2_apps caffe2ai/caffe2:c2v0.8.1.cpu.min.ubuntu16.04
这将你的/home/joe/caffe2_apps目录挂载为容器内的/joe_caffe2_apps。现在你可以在容器内构建 Caffe2 应用,并将这些应用通过容器部署到本地或云服务器上。
Caffe2 模型可视化
深度学习模型包含大量的层。每一层都有许多参数,例如它们的名称、类型、权重维度、特定层类型的参数、输入和输出张量名称。虽然典型的前馈神经网络结构没有循环,但递归神经网络(RNN)和其他网络结构有循环和其他拓扑结构。因此,能够可视化深度学习模型的结构非常重要,既对设计新网络来解决问题的研究人员来说如此,也对使用新网络的实践者来说如此。
使用 Caffe2 net_drawer 进行可视化
Caffe2 附带了一个用 Python 编写的简单可视化工具,名为net_drawer。你可以在 Caffe2 安装目录中找到这个 Python 脚本。例如,如果你将 Caffe2 安装在/usr/local,那么该工具会位于你的系统中/usr/local/lib/python2.7/dist-packages/caffe2/python/net_drawer.py。你也可以在 Caffe2 源代码中找到这个工具,路径为caffe2/python/net_drawer.py。
我们可以通过使用net_drawer来可视化来自第四章的 AlexNet 模型,与 Caffe 配合使用,如下所示:
$ python net_drawer.py --input /path/to/bvlc_alexnet/predict_net.pb --rankdir TB
我们指定了要使用--rankdir TB选项按照从上到下的顺序可视化图中的节点。该命令渲染了在第四章中显示的 AlexNet 图,图示见图 4.3,与 Caffe 配合使用。
该命令会生成两个文件。第一个是名为AlexNet.dot的文本文件,包含以易于人类阅读的 GraphViz DOT 格式表示的图结构。第二个是名为AlexNet.pdf的 PDF 文件,包含该结构的图形渲染。
请注意,这个工具提供了其他选项来定制可视化。你可以通过使用--help选项来查找这些选项,如下所示:
$ python net_drawer.py --help
usage: net_drawer.py [-h] --input INPUT [--output_prefix OUTPUT_PREFIX]
[--minimal] [--minimal_dependency] [--
append_output]
[--rankdir RANKDIR]
Caffe2 net drawer.
optional arguments:
-h, --help show this help message and exit
--input INPUT The input protobuf file.
--output_prefix OUTPUT_PREFIX
The prefix to be added to the output filename.
--minimal If set, produce a minimal visualization.
--minimal_dependency If set, only draw minimal dependency.
--append_output If set, append the output blobs to the operator
names.
--rankdir RANKDIR The rank direction of the pydot graph.
使用 Netron 进行可视化
Netron 是一个基于浏览器的深度学习模型可视化工具,使用 Python 编写。它是开源的,并且可以在github.com/lutzroeder/netron找到。
与net_drawer相比,Netron 具有现代化的可视化风格,并允许更好地与图形节点交互,以查看其参数。此外,Netron 的缩放功能使其在处理大型网络时更加方便。使用 Netron 的最大优势是它支持来自多个深度学习框架的模型可视化,如 Caffe2、Caffe、TensorFlow,以及 ONNX 格式。
可以通过以下命令从 PyPI 仓库安装 Netron:
$ pip3 install --user netron
我们可以使用 Netron 可视化我们的 Caffe2 AlexNet protobuf 文件,如下所示:
$ netron -b /path/to/predict_net.pb
这将在您的浏览器中打开一个新标签页,地址为http://localhost:8080,并展示 AlexNet 模型的可视化。我们可以通过鼠标的滚动功能进行缩放。点击模型中的任何一层,会在右侧显示该层的参数。如图 7.2所示,我们的 AlexNet 模型如下:

图 7.2:Netron 可视化的 AlexNet,右侧显示第一层卷积层的参数
总结
在本指南的最后一章,我们介绍了 Caffe2 的两个应用案例,展示了它的能力。作为 Caffe2 在边缘设备上的应用,我们探讨了如何在树莓派单板计算机上构建 Caffe2 并在其上运行 Caffe2 应用程序。作为 Caffe2 在云端的应用,我们探讨了如何在 Docker 容器内构建和运行 Caffe2 应用程序。作为帮助理解深度学习模型结构的工具,我们还研究了两个有助于可视化 Caffe2 模型的工具。


浙公网安备 33010602011771号