TensorFlow-1-x-机器学习-全-

TensorFlow 1.x 机器学习(全)

原文:annas-archive.org/md5/1386ae5de8c0086da5a8cba927050ae7

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

机器学习已经彻底改变了现代世界。许多机器学习算法,特别是深度学习,已经被广泛应用于全球范围,从移动设备到基于云的服务。TensorFlow 是领先的开源软件库之一,帮助你构建、训练和部署各种应用的机器学习系统。本实用书籍旨在为你带来 TensorFlow 的精华,帮助你构建真实世界的机器学习系统。

在本书结束时,你将对 TensorFlow 有深入的了解,并能够将机器学习技术应用到你的应用程序中。

本书涵盖内容

第一章,TensorFlow 入门,展示了如何在 Ubuntu、macOS 和 Windows 上安装 TensorFlow 并开始使用。

第二章,你的第一个分类器,带你走进手写识别器的第一次旅程。

第三章,TensorFlow 工具箱,概述了 TensorFlow 提供的工具,帮助你更加高效、轻松地工作。

第四章,猫与狗,教你如何使用 TensorFlow 中的卷积神经网络构建一个图像分类器。

第五章,序列到序列模型—Parlez-vous Français?,讨论了如何使用序列到序列模型构建一个从英语到法语的翻译器。

第六章,寻找意义,探索通过情感分析、实体提取、关键词提取和词语关系提取来寻找文本中的意义的方法。

第七章,用机器学习赚钱,深入探讨了一个数据量庞大的领域:金融世界。你将学习如何处理时间序列数据来解决金融问题。

第八章,医生马上就诊,探讨了如何利用深度神经网络解决一个企业级问题——医疗诊断。

第九章,巡航控制 - 自动化,教你如何创建一个生产系统,从训练到服务模型。该系统还能接收用户反馈并每天自动进行训练。

第十章,上线并扩展,带你进入亚马逊 Web 服务的世界,并展示如何在亚马逊服务器上利用多个 GPU 系统。

第十一章,更进一步 - 21 个问题,介绍了 21 个现实生活中的问题,阅读本书后,你可以利用深度学习—TensorFlow 来解决这些问题。

附录,高级安装,讨论了 GPU,并重点介绍了逐步 CUDA 设置和基于 GPU 的 TensorFlow 安装。

本书所需的条件

对于软件,本书完全基于 TensorFlow。你可以使用 Linux、Windows 或 macOS。

对于硬件,你将需要一台运行 Ubuntu、macOS 或 Windows 的计算机或笔记本电脑。作为作者,我们建议如果你打算使用深度神经网络,特别是在处理大规模数据集时,最好拥有一块 NVIDIA 显卡。

本书适合谁阅读

本书非常适合那些有志于构建智能且实用的机器学习系统,能够应用于真实世界的用户。你应该对机器学习概念、Python 编程、集成开发环境(IDEs)和命令行操作感到熟悉。本书对于那些作为职业程序员、科学家和工程师的人非常有用,特别是当他们需要学习机器学习和 TensorFlow 来支持他们的工作时。

约定

在本书中,你会发现一些不同的文本样式,用来区分不同类型的信息。以下是这些样式的几个示例及其含义的解释。

书中提到的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号等,显示方式如下:“我们可以通过使用 include 指令来包含其他上下文。”

代码块的显示方式如下:

batch_size = 128 
  num_steps = 10000 
  learning_rate = 0.3 
  data_showing_step = 500

当我们希望你注意某一部分代码时,相关的行或项会以粗体显示:

Layer 1 CONV (32, 28, 28, 4) 
Layer 2 CONV (32, 14, 14, 4) 
Layer 3 CONV (32, 7, 7, 4)

任何命令行输入或输出的显示方式如下:

sudo apt-get install python-pip python-dev

新术语重要词汇 以粗体显示。

警告或重要注意事项如下所示。

提示和技巧如下所示。

读者反馈

我们总是欢迎读者的反馈。请告诉我们你对本书的看法——你喜欢或不喜欢的部分。读者反馈对我们非常重要,它帮助我们开发出能够让你真正受益的书籍。

如果你想向我们提供一般反馈,只需发送电子邮件至 feedback@packtpub.com,并在邮件主题中提到本书的标题。

如果你有某个领域的专业知识,并且有兴趣撰写或为书籍贡献内容,请查看我们的作者指南:www.packtpub.com/authors

客户支持

现在,你已经成为 Packt 书籍的骄傲拥有者,我们提供了许多帮助你充分利用购买内容的资源。

下载示例代码

你可以通过你的账户从 www.packtpub.com 下载本书的示例代码文件。如果你是从其他地方购买的本书,你可以访问 www.packtpub.com/support,并注册以便将文件直接发送到你的邮箱。

你可以按照以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的 SUPPORT 标签上。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入书名。

  5. 选择您希望下载代码文件的书籍。

  6. 从下拉菜单中选择您购买此书的地方。

  7. 点击“代码下载”。

下载文件后,请确保使用最新版本的以下工具解压或提取文件:

  • WinRAR / 7-Zip for Windows

  • Zipeg / iZip / UnRarX for Mac

  • 7-Zip / PeaZip for Linux

本书的代码包也托管在 GitHub 上,链接为github.com/PacktPublishing/Machine-Learning-with-TensorFlow-1.x。我们还有其他来自我们丰富图书和视频目录的代码包,链接为github.com/PacktPublishing/。快来看看吧!

下载本书的彩色图片

我们还为您提供了一个包含本书中使用的截图/图表彩色图片的 PDF 文件。这些彩色图片将帮助您更好地理解输出中的变化。您可以从www.packtpub.com/sites/default/files/downloads/MachineLearningwithTensorFlow1.x_ColorImages.pdf下载该文件。

勘误

尽管我们已尽力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——例如文本或代码中的错误——我们将非常感激您向我们报告此问题。这样,您可以帮助其他读者避免困扰,并帮助我们改进书籍的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata并选择您的书籍,点击“勘误提交表格”链接,输入勘误的详细信息来报告勘误。您的勘误经验证后,将被接受并上传到我们的网站,或添加到该书的勘误部分。

若要查看之前提交的勘误信息,请访问www.packtpub.com/books/content/support,并在搜索框中输入书名。所需信息将显示在勘误部分。

盗版

互联网盗版版权材料的问题在所有媒体中持续存在。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现任何形式的非法复制品,请立即提供相关网址或网站名称,以便我们采取措施。

如发现盗版内容,请通过 copyright@packtpub.com 与我们联系,并提供涉嫌盗版材料的链接。

我们感谢您在保护我们的作者和我们为您提供宝贵内容方面的帮助。

问题

如果您对本书的任何部分有疑问,您可以通过 questions@packtpub.com 与我们联系,我们将尽力解决问题。

第一章:开始使用 TensorFlow

大型公共数据集的普及、低廉的 GPU 价格和开放的开发者文化在近年来彻底改变了机器学习的努力。训练数据——机器学习的命脉——近年来变得广泛可用且易于获取。计算能力也让小企业甚至个人都能拥有所需的算力。对于数据科学家而言,当前的十年是极其激动人心的。

行业内使用的一些顶级平台包括 Caffe、Theano 和 Torch。尽管这些底层平台在积极开发并开放共享,但由于安装困难、配置不直观以及在生产环境中的应用难度,使用者主要是机器学习从业者。

2015 年底和 2016 年带来了更多平台的加入——TensorFlow(来自谷歌)、CNTK(来自微软)以及Veles(来自三星)等。谷歌的 TensorFlow 因为多个原因而尤为引人注目。

TensorFlow 是所有平台中安装最简便的之一,将机器学习能力引入了普通爱好者和初学者程序员的领域。同时,像多 GPU 支持这样的高性能特性,也使得该平台对经验丰富的数据科学家和工业应用同样具有吸引力。TensorFlow 还提供了一个重新构想的流程和多个用户友好的工具,例如TensorBoard,以帮助管理机器学习工作。最后,平台得到了全球最大的机器学习巨头——谷歌的强力支持和社区支持。这一切发生在我们甚至还没讨论其背后强大的技术优势之前,我们稍后会深入探讨。

本章将涵盖以下主题:

  • macOS X

  • Microsoft Windows 和 Linux,包括核心软件和所有依赖项

  • 启动虚拟机以启用 Windows 安装

当前的使用情况

虽然 TensorFlow 公开发布仅两年,但已有大量社区努力成功地将现有的机器学习项目移植过来。一些例子包括手写识别、语言翻译、动物分类、医学影像分诊和情感分析。机器学习广泛应用于多个行业和问题,总是让人感到好奇。使用 TensorFlow,这些问题不仅可行,而且容易实现。事实上,我们将在本书中逐一解决上述问题!

安装 TensorFlow

TensorFlow 便捷地提供了几种安装类型,并支持多种操作系统。基本安装仅限于 CPU,而更高级的安装则通过将计算任务推送到显卡,甚至多块显卡,来释放强大的计算能力。我们建议您先从基本的 CPU 安装开始。更复杂的 GPU 和 CUDA 安装将在附录中讨论,高级安装

即使只有基本的 CPU 安装,TensorFlow 也提供了多种选项,具体如下:

  • 基本的 Python pip安装

  • 通过 Virtualenv 进行隔离的 Python 安装

  • 通过 Docker 进行完全隔离的容器化安装

我们推荐通过 Virtualenv 进行 Python 安装,但我们的示例将使用基本的 Python pip安装,帮助您专注于任务的核心内容,即使 TensorFlow 运行起来。再次强调,更高级的安装类型将在附录中讨论,高级安装

TensorFlow 可以在 Linux 和 macOS 上与 Python 2.7 和 3.5 一起完全运行。在 Windows 上,我们只能使用 Python 3.5.x 或 3.6.x 版本的 TensorFlow。通过运行Linux 虚拟机VM),TensorFlow 也可以在 Windows 上轻松使用。使用 Ubuntu 虚拟机时,我们可以在 Python 2.7 环境下使用 TensorFlow。但在虚拟机中,无法使用 GPU 支持的 TensorFlow。从 TensorFlow 1.2 版本开始,TensorFlow 不再在 macOS 上提供 GPU 支持。因此,如果您想在 macOS 上使用 GPU 支持的 TensorFlow,您必须从源代码进行编译,这超出了本章的讨论范围。否则,您仍然可以使用 TensorFlow 1.0 或 1.1,这些版本在 macOS 上提供开箱即用的 GPU 支持。Linux 和 Windows 用户可以同时使用 CPU 和 GPU 支持的 TensorFlow。

Ubuntu 安装

Ubuntu 是最适合与 TensorFlow 配合使用的 Linux 发行版之一。我们强烈推荐您使用 Ubuntu 机器,特别是如果您打算使用 GPU。我们大部分的工作将在 Ubuntu 终端上进行。我们将从通过以下命令安装python-pippython-dev开始:

sudo apt-get install python-pip python-dev

成功安装后的输出将如下所示:

如果您发现缺少软件包,可以通过以下命令来修复它们:

sudo apt-get update --fix-missing

然后,您可以继续安装pythonpip

我们现在准备安装 TensorFlow。我们将进行 CPU-only 的安装,如果您希望进行 GPU 支持的高级安装,我们将在附录中讨论,高级安装

CPU 安装通过以下命令启动:

sudo pip install tensorflow

成功安装后的输出将如下所示:

macOS 安装

如果你使用 Python,你可能已经安装了 Python 包管理器pip。但是,如果没有,你可以通过运行easy_install pip命令轻松安装它。你会注意到我们实际上执行了sudo easy_install pip—因为安装需要管理员权限,所以需要加上sudo前缀。

我们假设你已经有了基本的包管理器easy_install;如果没有,你可以从pypi.python.org/pypi/setuptools安装它。成功安装后,将显示如下截图:

接下来,我们将安装six包:

sudo easy_install --upgrade six

成功安装后,将显示如下截图:

令人惊讶的是,这就是 TensorFlow 的两个前提条件,现在我们可以安装核心平台了。我们将使用前面提到的pip包管理器,并直接从 Google 的站点安装 TensorFlow。截止到写本书时,最新版本是 v1.3,但你应该根据需要更改为你想使用的最新版本:

sudo pip install tensorflow

pip安装程序将自动收集所有其他必需的依赖项。你将看到每个独立的下载和安装,直到软件完全安装完成。

成功安装后,将显示如下截图:

就这样!如果你已经完成了到这里的步骤,你可以开始训练并运行你的第一个模型。跳转到第二章,你的第一个分类器,开始训练你的第一个模型。

希望完全隔离安装的 macOS X 用户可以使用虚拟机(VM),如 Windows 安装部分所述。

Windows 安装

如前所述,TensorFlow 与 Python 2.7 在 Windows 上无法原生运行。在本节中,我们将指导你通过安装 Python 3.5 的 TensorFlow,并为你提供如何设置 Linux 虚拟机以使用 Python 2.7 版 TensorFlow 的说明。

首先,我们需要从以下链接安装 Python 3.5.x 或 3.6.x 64 位版本:

www.python.org/downloads/release/python-352/

www.python.org/downloads/release/python-362/

请确保下载包含amd64的 64 位版本的 Python,如python-3.6.2-amd64.exe。Python 3.6.2 的安装界面如下:

我们将选择“Add Python 3.6 to PATH”并点击“Install Now”。安装过程完成后,将显示以下屏幕:

我们将点击禁用路径长度限制,然后点击关闭完成 Python 安装。现在,让我们在 Windows 菜单下打开 Windows PowerShell 应用程序。我们将使用以下命令安装仅 CPU 版本的 TensorFlow:

pip3 install tensorflow

安装结果如下所示:

恭喜,您现在可以在 Windows 上使用 Python 3.5.x 或 3.6.x 支持的 TensorFlow。在接下来的部分,我们将向您展示如何设置虚拟机并使用 Python 2.7 配置 TensorFlow。如果您不需要 Python 2.7,可以跳到 第二章的 测试安装 部分,您的第一个分类器

现在,我们将向您展示如何在 Linux 上设置虚拟机并使用 Python 2.7 配置 TensorFlow。我们推荐使用免费的 VirtualBox 系统,可以在 www.virtualbox.org/wiki/Downloads 下载。本文写作时的最新稳定版本是 v5.0.14,下载链接如下:

download.virtualbox.org/virtualbox/5.1.28/VirtualBox-5.1.28-117968-Win.exe

成功的安装将允许您运行 Oracle VM VirtualBox 管理器仪表盘,仪表盘如下所示:

虚拟机设置

Linux 有许多不同的版本,但由于 TensorFlow 文档大多数提到了 Ubuntu,我们将使用 Ubuntu Linux。您可以使用任何 Linux 版本,但需要注意,不同版本和每个版本的细节会有所不同。大多数差异无关紧要,但有些差异可能会导致安装或使用 TensorFlow 时出现问题。

即使选择了 Ubuntu,仍然有许多版本和配置,您可以在 cdimage.ubuntu.com/ubuntu-gnome/releases/14.04/release/ 查看一些。

我们将安装最流行的版本,即 Ubuntu 14.04.4 LTS(确保下载适合您计算机的版本)。标有 x86 的版本设计用于 32 位机器,而标有 64 位变体的版本则设计用于 64 位机器。大多数现代机器是 64 位的,因此如果不确定,请选择后者。

安装是通过 ISO 文件进行的,ISO 文件本质上是安装光盘的文件版本。Ubuntu 14.04.4 LTS 的 ISO 文件是 ubuntu-gnome-14.04-desktop-amd64.iso

一旦下载了安装 ISO 文件,我们将设置虚拟机,并使用该 ISO 文件在虚拟机上安装 Ubuntu Linux。

在 Oracle VM VirtualBox 管理器上设置虚拟机相对简单,但请注意,默认选项不足以满足 TensorFlow 的要求。您将经历以下七个界面,最后系统会提示您输入刚刚下载的安装文件。

我们将首先设置操作系统类型,并配置分配给虚拟机的 随机访问内存 (RAM):

  1. 注意,我们选择了 64 位安装,因为我们使用的是该镜像;如果需要,你可以选择使用 32 位镜像:

  1. 分配多少 RAM 取决于你的机器有多少。在以下截图中,我们将把一半的内存(8 GB)分配给我们的虚拟机。请记住,这仅在运行虚拟机时消耗内存,因此我们可以大胆分配。至少可以分配 4 GB:

  2. 我们的虚拟机需要一个硬盘。我们将创建一个 虚拟硬盘 (VHD),如以下截图所示:

  3. 然后,我们将选择虚拟机的硬盘类型,即 VDI(VirtualBox 磁盘镜像),如以下截图所示:

  4. 接下来,我们将选择为 VHD 分配多少空间。这一点非常重要,因为我们很快将处理非常大的数据集:

  5. 我们将分配 12 GB,因为 TensorFlow 和典型的 TensorFlow 应用程序有许多依赖项,例如 NumPySciPyPandas。我们的练习还将下载大型数据集,用于训练:

  6. 设置虚拟机后,它将出现在左侧的虚拟机列表中。选择它并点击启动。这相当于启动计算机:

  1. 当机器第一次启动时,插入安装 CD(在我们的例子中,是我们之前下载的 Ubuntu ISO 文件):

按照安装说明操作,你将完成 Ubuntu Linux 的完整安装并准备好使用!之后,你可以按照本章开头的 Ubuntu 安装指南进行操作。

测试安装

在本节中,我们将使用 TensorFlow 来计算一个简单的数学运算。首先,在 Linux/macOS 上打开终端,或者在 Windows 上打开 PowerShell。

现在,我们需要运行 python 来使用 TensorFlow,执行以下命令:

python

在 Python shell 中输入以下程序:

import tensorflow as tf
a = tf.constant(1.0)
b = tf.constant(2.0)
c = a + b
sess = tf.Session()
print(sess.run(c))

结果将类似于以下屏幕,其中 3.0 会出现在末尾:

摘要

本章介绍了在三大主要操作系统上安装 TensorFlow,因此所有读者都应该已经顺利运行该平台。Windows 用户遇到了额外的挑战,因为 TensorFlow 在 Windows 上只支持 Python 3.5.x 或 Python 3.6.x 64 位版本。然而,现在即使是 Windows 用户也应该能顺利运行了。恭喜你,现在有趣的部分开始了!

现在你已经安装了 TensorFlow。接下来的直接步骤是通过一个内置的训练样本来测试安装情况。接下来,我们将从零开始编写我们的第一个分类器——一个手写识别器。

在接下来的章节中,我们将回顾 TensorFlow 工具,并在我们的项目中使用它们。我们还将回顾主要的深度学习概念,并在项目的背景下使用每一个概念。你将有机会尝试多个行业的项目,涵盖从金融到医疗再到语言等领域。

第二章:你的第一个分类器

TensorFlow 安装完成后,我们需要开始实践。我们将通过编写第一个分类器并从头到尾进行训练和测试来进行实践!

我们的第一个分类器将是一个手写识别器。最常用的训练数据集之一是MNIST手写数字数据集。我们将使用一个类似的数据集,名为notMNIST,它包含了英文字母表中的前十个字母。

关键部分

大多数机器学习分类器有三个关键部分,分别如下:

  • 训练流程

  • 神经网络设置和训练输出

  • 使用流程

训练流程获取数据,对数据进行分阶段处理、清洗、标准化,并将其转化为神经网络可接受的格式。不要惊讶,最初训练流程可能占用你 80%到 85%的工作量——这是大多数机器学习工作的现实。通常,训练数据越真实,训练流程所花费的时间就越多。在企业环境中,训练流程可能是一个持续的工作,不断进行增强。随着数据集的增大,这一点尤为真实。

第二部分是神经网络的设置和训练,对于常规问题,训练过程可能很快,但对于更难的问题,则可能需要进行研究级的努力。你可能会发现自己反复调整网络设置,直到最终达到所需的分类器精度。训练是计算最为密集的部分,因此需要时间才能评估每次增量修改的结果。

一旦初始设置完成,并且网络训练到足够的精度,我们就可以反复使用它。在第十章,Go Live and Go Big中,我们将探索更多的高级话题,如持续学习,甚至使用过程本身也可以反馈进一步训练分类器。

获取训练数据

机器学习需要训练数据——通常需要大量的训练数据。机器学习的一大优点是可以使用标准的训练数据集。这些数据集通常用于基准测试节点模型和配置,并提供一致的标准来衡量与之前进展的性能对比。许多数据集也用于年度全球比赛。

本章使用的训练数据由机器学习研究员 Yaroslav Bulatov 慷慨提供。

下载训练数据

你应该从以下链接下载训练数据:

我们将通过编程下载数据,但首先应该手动下载数据集,先大致了解一下数据和归档的结构。这在我们编写数据流水线时非常重要,因为我们需要理解数据结构,以便操作数据。

这个小数据集非常适合快速查看。你可以通过以下命令行操作,或者直接使用浏览器下载文件,利用解压工具提取文件(我建议你熟悉命令行,因为所有这些操作都需要自动化处理):

cd ~/workdir
wget http://yaroslavvb.com/upload/notMNIST/notMNIST_small.tar.gz
tar xvf notMNIST_small.tar.gz

前面的命令行将显示一个名为notMNIST_small的容器文件夹,下面有十个子文件夹,每个字母(从aj)对应一个文件夹。在每个字母文件夹下,有成千上万张 28x28 像素的字母图片。另外,需要注意的是,每个字母图像的文件名(QnJhbmRpbmcgSXJvbi50dGY=)表明它是一个随机字符串,并不包含有用信息。

理解类

我们正在编写的分类器旨在将未知图像分配到某个类。类可以是以下几种类型:

  • 猫科动物与犬科动物

  • 二与七

  • 肿瘤与正常

  • 微笑与皱眉

在我们的案例中,我们将每个字母作为一个类别,共有 10 个类别。训练集将显示 10 个子文件夹,每个子文件夹下有成千上万的图像。子文件夹的名称很重要,因为它是每张图像的标签。这些细节将被流水线用于准备 TensorFlow 的数据。

自动化训练数据设置

理想情况下,我们希望整个过程都能自动化。这样,无论我们在哪台电脑上使用,都可以轻松地端到端地运行这个过程,而不需要携带额外的资产。这个过程在后续会很重要,因为我们通常会在一台电脑(开发机)上进行开发,然后在另一台电脑(生产服务器)上进行部署。

我已经为本章节以及其他所有章节写好了代码,代码可以在github.com/mlwithtf/MLwithTF找到。我们的方法是边理解边重写代码。像这样直接的部分可以跳过。我建议你分叉这个仓库,并为你的项目克隆一份本地副本:

cd ~/workdir
git clone https://github.com/mlwithtf/MLwithTF
cd chapter_02

本节的代码可通过以下链接获取——github.com/mlwithtf/mlwithtf/blob/master/chapter_02/download.py.

准备数据集是训练过程中的一个重要部分。在深入代码之前,我们将运行download.py来自动下载并准备数据集:

python download.py

结果将如下所示:

现在,我们将查看几个在download.py中使用的函数。你可以在这个文件中找到代码:

github.com/mlwithtf/mlwithtf/blob/master/data_utils.py

以下downloadFile函数将自动下载文件并验证文件大小是否符合预期:

 from __future__ import print_function 
 import os 
 from six.moves.urllib.request import urlretrieve 
 import datetime 
 def downloadFile(fileURL, expected_size): 
    timeStampedDir=datetime.datetime.now()
     .strftime("%Y.%m.%d_%I.%M.%S") 
    os.makedirs(timeStampedDir) 
    fileNameLocal = timeStampedDir + "/" +     
    fileURL.split('/')[-1] 
    print ('Attempting to download ' + fileURL) 
    print ('File will be stored in ' + fileNameLocal) 
    filename, _ = urlretrieve(fileURL, fileNameLocal) 
    statinfo = os.stat(filename) 
    if statinfo.st_size == expected_size: 
        print('Found and verified', filename) 
    else: 
        raise Exception('Could not get ' + filename) 
    return filename 

该函数可以按如下方式调用:

 tst_set = 
 downloadFile('http://yaroslavvb.com/upload/notMNIST/notMNIST_small
 .tar.gz', 8458043) 

提取内容的代码如下(请注意,额外的导入是必需的):

 import os, sys, tarfile 
 from os.path import basename 

 def extractFile(filename): 
    timeStampedDir=datetime.datetime.now()
     .strftime("%Y.%m.%d_%I.%M.%S") 
    tar = tarfile.open(filename) 
    sys.stdout.flush() 
    tar.extractall(timeStampedDir) 
    tar.close() 
    return timeStampedDir + "/" + os.listdir(timeStampedDir)[0] 

我们按顺序调用download和 extract 方法,代码如下:

 tst_src='http://yaroslavvb.com/upload/notMNIST/notMNIST_small.tar.
 gz' 
 tst_set = downloadFile(tst_src, 8458043) 
 print ('Test set stored in: ' + tst_set) 
 tst_files = extractFile(tst_set) 
 print ('Test file set stored in: ' + tst_files) 

额外的设置

下一部分将重点介绍图像处理和操作。这需要一些额外的库,可能你还没有安装。此时,最好安装所有常见的科学计算所需的包,可以按以下方式完成:

sudo apt-get install python-numpy python-scipy python-matplotlib 
ipython ipython-notebook python-pandas python-sympy python-nose

此外,安装图像处理库、一些外部矩阵数学库及其底层依赖,方法如下:

sudo pip install ndimage
sudo apt-get install libatlas3-base-dev gcc gfortran g++

将图像转换为矩阵

机器学习的很多工作其实就是在矩阵上进行操作。接下来,我们将通过将图像转换成一系列矩阵来开始这个过程——本质上是一个 3D 矩阵,其宽度与我们拥有的图像数量相同。

本章及全书中几乎所有的矩阵操作都将使用 NumPy——Python 科学计算领域最流行的库。你可以在www.numpy.org/找到 NumPy。你应该在运行接下来的操作之前安装它。

以下代码打开图像并创建数据矩阵(请注意现在需要额外的三个导入):

 import numpy as np 
 from IPython.display import display, Image 
 from scipy import ndimage 

 image_size = 28  # Pixel width and height. 
 pixel_depth = 255.0  # Number of levels per pixel. 
 def loadClass(folder): 
  image_files = os.listdir(folder) 
  dataset = np.ndarray(shape=(len(image_files), 
  image_size, 
   image_size), dtype=np.float32) 
  image_index = 0 
  print(folder) 
  for image in os.listdir(folder): 
    image_file = os.path.join(folder, image) 
    try: 
      image_data =  
     (ndimage.imread(image_file).astype(float) -  
                    pixel_depth / 2) / pixel_depth 
      if image_data.shape != (image_size, image_size): 
        raise Exception('Unexpected image shape: %s' % 
     str(image_data.shape)) 
      dataset[image_index, :, :] = image_data 
      image_index += 1 
     except IOError as e: l
      print('Could not read:', image_file, ':', e, '-   
      it\'s ok, 
       skipping.') 
     return dataset[0:image_index, :, :] 

我们已经从上一节中提取了文件。现在,我们可以对所有提取的图像执行这个过程,代码如下:

 classFolders = [os.path.join(tst_files, d) for d in 
 os.listdir(tst_files) if os.path.isdir(os.path.join(tst_files, 
 d))] 
 print (classFolders) 
 for cf in classFolders: 
    print ("\n\nExaming class folder " + cf) 
    dataset=loadClass(cf) 
    print (dataset.shape) 

该过程基本上将字母加载到一个类似于这样的矩阵中:

然而,观察矩阵会揭示出更多微妙之处。可以通过打印堆栈中的某一层来查看(例如,np.set_printoptions(precision=2); print(dataset[47]))。你会发现一个不是由位组成的矩阵,而是由浮点数组成的:

图像首先会被加载到一个值范围为 0 到 255 的矩阵中:

这些图像将被缩放到-0.5 到 0.5 之间,稍后我们将回顾为什么这样做。最后,我们将得到一个看起来像这样的图像堆栈:

这些都是灰度图像,因此我们只处理一个层级。我们将在后续章节处理中彩色图像;在那些情况下,每张照片将具有三行的矩阵,并且分别包含红色、绿色和蓝色的矩阵。

合理的停止点

下载我们的训练文件花费了很长时间。即使提取所有图像也花费了一些时间。为了避免重复这些步骤,我们将尽量只做一次所有的工作,然后创建pickle 文件——这些是 Python 数据结构的存档。

以下过程会遍历我们的训练集和测试集中的每个类,并为每个类创建一个单独的pickle文件。在以后的运行中,我们将直接从这里开始:

 def makePickle(imgSrcPath): 
    data_folders = [os.path.join(tst_files, d) for d in 
     os.listdir(tst_files) if os.path.isdir(os.path.join(tst_files, 
     d))] 
    dataset_names = [] 
    for folder in data_folders: 
        set_filename = folder + '.pickle' 
        dataset_names.append(set_filename) 
        print('Pickling %s.' % set_filename) 
        dataset = loadClass(folder) 
        try: 
            with open(set_filename, 'wb') as f: 
                pickle.dump(dataset, f, pickle.HIGHEST_PROTOCOL) 
        except Exception as e: 
            print('Unable to save data to', set_filename, ':', e) 
    return dataset_names 

Pickle 文件本质上是可持久化并可重建的字典转储。

机器学习公文包

我们刚刚创建了漂亮、干净的pickle文件,里面是经过预处理的图像,用于训练和测试我们的分类器。然而,我们最终得到了 20 个pickle文件。这有两个问题。首先,我们有太多文件,难以轻松管理。其次,我们只完成了管道的一部分,即处理了我们的图像集,但还没有准备好一个 TensorFlow 可用的文件。

现在,我们需要创建三个主要的数据集——训练集、验证集和测试集。训练集将用于训练我们的分类器,验证集用于评估每次迭代的进展。测试集将在训练结束后保密,届时会用来测试我们训练模型的效果。

实现这些操作的代码比较长,所以我们将让你自行查看 Git 仓库。请特别注意以下三个函数:

 def randomize(dataset, labels): 
    permutation = np.random.permutation(labels.shape[0]) 
    shuffled_dataset = dataset[permutation, :, :] 
    shuffled_labels = labels[permutation] 
    return shuffled_dataset, shuffled_labels  

 def make_arrays(nb_rows, img_size): 
    if nb_rows: 
        dataset = np.ndarray((nb_rows, img_size, img_size),   
 dtype=np.float32) 
        labels = np.ndarray(nb_rows, dtype=np.int32) 
    else: 
        dataset, labels = None, None 
    return dataset, labels 

 def merge_datasets(pickle_files, train_size, valid_size=0): 
  num_classes = len(pickle_files) 
  valid_dataset, valid_labels = make_arrays(valid_size,  
  image_size) 
  train_dataset, train_labels = make_arrays(train_size,  
  image_size) 
  vsize_per_class = valid_size // num_classes 
  tsize_per_class = train_size // num_classes 

  start_v, start_t = 0, 0 
  end_v, end_t = vsize_per_class, tsize_per_class 
  end_l = vsize_per_class+tsize_per_class 
  for label, pickle_file in enumerate(pickle_files): 
    try: 
      with open(pickle_file, 'rb') as f: 
        letter_set = pickle.load(f) 
        np.random.shuffle(letter_set) 
        if valid_dataset is not None: 
          valid_letter = letter_set[:vsize_per_class, :, :] 
          valid_dataset[start_v:end_v, :, :] = valid_letter 
          valid_labels[start_v:end_v] = label 
          start_v += vsize_per_class 
          end_v += vsize_per_class 

        train_letter = letter_set[vsize_per_class:end_l, :, :] 
        train_dataset[start_t:end_t, :, :] = train_letter 
        train_labels[start_t:end_t] = label 
        start_t += tsize_per_class 
        end_t += tsize_per_class 
    except Exception as e: 
      print('Unable to process data from', pickle_file, ':', e) 
      raise 

  return valid_dataset, valid_labels, train_dataset, train_labels 

这三部分完成了我们的管道方法。但是,我们仍然需要使用这个管道。为此,我们首先将定义训练、验证和测试集的大小。你可以更改这些参数,但当然应该保持它们小于可用的完整大小:

     train_size = 200000 
     valid_size = 10000 
     test_size = 10000 

然后,这些尺寸将用于构建合并后的(即,合并我们所有的类别)数据集。我们将传入pickle文件的列表作为数据源,并获取一个标签向量和一个图像矩阵堆栈。最后,我们会打乱数据集,如下所示:

 valid_dataset, valid_labels, train_dataset, train_labels = 
  merge_datasets( 
   picklenamesTrn, train_size, valid_size) 
 _, _, test_dataset, test_labels = merge_datasets(picklenamesTst, 
  test_size) 
 train_dataset, train_labels = randomize(train_dataset, 
  train_labels) 
 test_dataset, test_labels = randomize(test_dataset, test_labels) 
 valid_dataset, valid_labels = randomize(valid_dataset, 
  valid_labels) 

我们可以通过以下方式查看我们新合并的数据集:

 print('Training:', train_dataset.shape, train_labels.shape) 
 print('Validation:', valid_dataset.shape, valid_labels.shape) 
 print('Testing:', test_dataset.shape, test_labels.shape) 

哎呀!这真是太费劲了,我们以后不想再重复这些工作了。幸运的是,我们不必再做这些,因为我们会将三个新数据集重新打包成一个巨大的pickle文件。今后,所有的学习都将跳过前面的步骤,直接从这个巨大的pickle文件开始:

 pickle_file = 'notMNIST.pickle' 

 try: 
   f = open(pickle_file, 'wb') 
   save = { 
      'datTrn': train_dataset, 
    'labTrn': train_labels, 
    'datVal': valid_dataset, 
    'labVal': valid_labels, 
    'datTst': test_dataset, 
    'labTst': test_labels, 
     } 
   pickle.dump(save, f, pickle.HIGHEST_PROTOCOL) 
   f.close() 
 except Exception as e: 
   print('Unable to save data to', pickle_file, ':', e) 
   raise 

 statinfo = os.stat(pickle_file) 
 print('Compressed pickle size:', statinfo.st_size) 

将矩阵输入 TensorFlow 的理想方式其实是作为一维数组;因此,我们将把 28x28 的矩阵重新格式化为 784 个小数的字符串。为此,我们将使用以下reformat方法:

 def reformat(dataset, labels): 
   dataset = dataset.reshape((-1, image_size * 
    image_size)).astype(np.float32) 
   labels = (np.arange(num_labels) == 
    labels[:,None]).astype(np.float32) 
   return dataset, labels 

我们的图像现在看起来是这样的,每一行代表训练集、验证集和测试集中的一张图像:

最后,为了打开并操作pickle文件的内容,我们只需读取之前选定的变量名,并像操作哈希图一样提取数据:

 with open(pickle_file, 'rb') as f: 
   pkl = pickle.load(f) 
   train_dataset, train_labels = reformat(pkl['datTrn'], 
    pkl['labTrn']) 
   valid_dataset, valid_labels = reformat(pkl['datVal'], 
    pkl['labVal']) 
   test_dataset, test_labels = reformat(pkl['datTst'], 
    pkl['labTst']) 

训练日

现在,我们来到了有趣的部分——神经网络。训练这个模型的完整代码可以通过以下链接查看:github.com/mlwithtf/mlwithtf/blob/master/chapter_02/training.py

为了训练模型,我们将导入更多的模块:

 import sys, os
 import tensorflow as tf
 import numpy as np
 sys.path.append(os.path.realpath('..'))
 import data_utils
 import logmanager 

然后,我们将定义一些用于训练过程的参数:

 batch_size = 128
 num_steps = 10000
 learning_rate = 0.3
 data_showing_step = 500

之后,我们将使用data_utils包加载前一部分中下载的数据集:

 dataset, image_size, num_of_classes, num_of_channels =  
 data_utils.prepare_not_mnist_dataset(root_dir="..")
 dataset = data_utils.reformat(dataset, image_size, num_of_channels,   
 num_of_classes)
 print('Training set', dataset.train_dataset.shape,  
 dataset.train_labels.shape)
 print('Validation set', dataset.valid_dataset.shape,  
 dataset.valid_labels.shape)
 print('Test set', dataset.test_dataset.shape,  
 dataset.test_labels.shape)

我们将从一个完全连接的网络开始。目前,只需相信网络的设置(我们稍后会深入探讨网络的设置理论)。我们将在下面的代码中将神经网络表示为一个图,称为graph

 graph = tf.Graph()
 with graph.as_default():
 # Input data. For the training data, we use a placeholder that will  
 be fed
 # at run time with a training minibatch.
 tf_train_dataset = tf.placeholder(tf.float32,
 shape=(batch_size, image_size * image_size * num_of_channels))
 tf_train_labels = tf.placeholder(tf.float32, shape=(batch_size,  
 num_of_classes))
 tf_valid_dataset = tf.constant(dataset.valid_dataset)
 tf_test_dataset = tf.constant(dataset.test_dataset)
 # Variables.
 weights = {
 'fc1': tf.Variable(tf.truncated_normal([image_size * image_size *  
 num_of_channels, num_of_classes])),
 'fc2': tf.Variable(tf.truncated_normal([num_of_classes,  
 num_of_classes]))
 }
 biases = {
 'fc1': tf.Variable(tf.zeros([num_of_classes])),
 'fc2': tf.Variable(tf.zeros([num_of_classes]))
 }
 # Training computation.
 logits = nn_model(tf_train_dataset, weights, biases)
 loss = tf.reduce_mean(
 tf.nn.softmax_cross_entropy_with_logits(logits=logits,  
 labels=tf_train_labels))
 # Optimizer.
 optimizer =  
 tf.train.GradientDescentOptimizer(learning_rate).minimize(loss)
 # Predictions for the training, validation, and test data.
 train_prediction = tf.nn.softmax(logits)
 valid_prediction = tf.nn.softmax(nn_model(tf_valid_dataset,  
 weights, biases))
 test_prediction = tf.nn.softmax(nn_model(tf_test_dataset, weights,  
 biases))
 The most important line here is the nn_model where the neural  
 network is defined:
 def nn_model(data, weights, biases):
 layer_fc1 = tf.matmul(data, weights['fc1']) + biases['fc1']
 relu_layer = tf.nn.relu(layer_fc1)
 return tf.matmul(relu_layer, weights['fc2']) + biases['fc2']

用于训练模型的loss函数也是这个过程中的一个重要因素:

 loss = tf.reduce_mean(
 tf.nn.softmax_cross_entropy_with_logits(logits=logits,  
 labels=tf_train_labels))
 # Optimizer.
 optimizer =  
 tf.train.GradientDescentOptimizer(learning_rate).minimize(loss)

这是使用的优化器(随机梯度下降),以及learning_rate (0.3)和我们试图最小化的函数(带交叉熵的 softmax)。

真实的操作,且最耗时的部分,发生在接下来的最终阶段——训练循环:

我们可以在chapter_02目录中使用以下命令运行这个训练过程:

python training.py

运行该过程会生成以下输出:

我们正在进行数百次的循环,并且每 500 次循环打印一次指示性结果。当然,你可以修改任何这些设置。重要的是要理解循环的过程:

  • 我们将多次循环执行这个过程。

  • 每次,我们都会创建一个小批量的照片,这些照片是完整图像集的一个切割部分。

  • 每一步都会运行 TensorFlow 会话,产生一个损失值和一组预测结果。每一步还会对验证集进行预测。

  • 在迭代周期的最后,我们将在之前一直保密的测试集上做最终预测。

  • 对于每次做出的预测,我们将通过预测准确度来观察我们的进展。

我们之前没有讨论accuracy方法。这个方法简单地将预测标签与已知标签进行比较,从而计算出一个百分比得分:

 def accuracy(predictions, labels): 
  return (100.0 * np.sum(np.argmax(predictions, 1) == 
   np.argmax(labels, 1)) 
          / predictions.shape[0])

仅仅运行前面的分类器就能获得大约 85%的准确率。这是相当了不起的,因为我们才刚刚开始!我们仍然可以继续进行更多的调整。

为了持续使用,保存模型

为了将 TensorFlow 会话中的变量保存以便将来使用,你可以使用Saver()函数,代码如下:

 saver = tf.train.Saver() 

之后,你可以通过恢复以下检查点来获取模型的状态,避免繁琐的重新训练:

 ckpt = tf.train.get_checkpoint_state(FLAGS.checkpoint_dir) 
 if ckpt and ckpt.model_checkpoint_path: 
 saver.restore(sess, ckpt.model_checkpoint_path) 

为什么要隐藏测试集?

注意,我们直到最后一步才使用测试集。为什么不早点使用?这是一个相当重要的细节,确保测试集能够保持良好。随着我们在训练集上迭代并微调分类器的行为,我们有时会将分类器包裹在图像中或过度训练。当你学习训练集而非每个类别内的特征时,这种情况就会发生。

当我们过度训练时,训练集的迭代轮次中的准确度看起来很有希望,但那完全是虚假的希望。拥有一个从未见过的测试集应该将现实带回到过程当中。在训练集上表现出色却在测试集上结果不佳,通常意味着过拟合。

这就是为什么我们保留了一个单独的测试集。它帮助指示分类器的真实准确性。这也是为什么你绝对不应将数据集混合或与测试集打乱的原因。

使用分类器

我们将使用notMNIST_small.tar.gz来演示分类器的使用,它将作为测试集。对于分类器的持续使用,你可以自己提供图像,并通过类似的管道对其进行测试,而非训练。

你可以自己创建一些 28x28 的图像,并将它们放入测试集中进行评估。你会感到惊喜的!

在实际应用中,遇到的一个问题是野外图像的异质性。你可能需要找到图像、裁剪它们、缩小它们,或者进行其他一系列变换。这些都属于我们之前讨论过的使用管道的一部分。

另一种处理大图像的技术是,在图像上滑动一个小窗口,并将图像的每个子部分输入分类器进行分类,这适用于像在页面尺寸的图像上查找字母这样的任务。

在未来的章节中,我们将把模型投入生产环境,但作为预览,一种常见的配置是将训练好的模型迁移到云端服务器上。系统的外观可能是一个智能手机应用程序,用户拍照后,照片会被发送到后台进行分类。在这种情况下,我们会用一个网络服务将整个程序包装起来,接受传入的分类请求并进行程序化响应。流行的配置有很多,我们将在第九章中探讨其中的几个,巡航控制 - 自动化

深入探索神经网络

注意我们是如何达到 86%的准确率的。对于仅仅两小时的工作,这个结果已经很不错,但我们能做得更好。未来的潜力大部分来自于改变神经网络。我们之前的应用使用的是全连接设置,在这种设置中,每一层的节点都与上一层的节点相连,形状如下:

正如你将在后续章节中学到的,随着网络结构变得更复杂,这种设置虽然快速,但并不理想。最大的问题是参数的数量庞大,这可能导致模型在训练数据上过拟合。

学到的技能

你应该在本章中学到了这些技能:

  • 准备训练数据和测试数据

  • 创建一个 TensorFlow 可消费的训练集

  • 设置基本的神经网络图

  • 训练 TensorFlow 分类器

  • 验证分类器

  • 输入真实世界数据

总结

进展非常顺利!我们刚刚构建了一个十年前也可以被认为是世界级的手写字分类器。此外,我们围绕这个过程构建了一个完整的管道,能够完全自动化训练设置和执行。这意味着我们的程序几乎可以迁移到任何服务器上,并继续正常运行,几乎是开箱即用。

第三章:TensorFlow 工具箱

大多数机器学习平台面向的是学术或工业领域的科学家和从业人员。因此,尽管它们功能强大,但往往比较粗糙,缺乏许多用户体验功能。

在各个阶段查看模型并查看和汇总不同模型和运行的性能是需要付出相当多的努力的。即使是查看神经网络,所需的努力也可能比预期的要多。

虽然在神经网络简单且仅有几层时,这样做是可以接受的,但今天的网络要深得多。2015 年,微软使用一款具有 152 层的深度网络赢得了年度 ImageNet 大赛。可视化这样深的网络可能会很困难,查看权重和偏差可能会让人不知所措。

从业者们开始使用自建的可视化工具和引导工具来分析他们的网络并进行性能评估。TensorFlow 通过在整体平台发布时直接发布 TensorBoard 改变了这一点。TensorBoard 开箱即用,无需额外安装或设置。

用户只需要根据他们想要捕捉的内容来为代码添加监控。它具有绘制事件、学习率和损失随时间变化的图表;权重和偏差的直方图;以及图像。图形浏览器允许交互式地查看神经网络。

在本章中,我们将重点讨论以下几个领域:

  • 我们将从使用四个常见模型和数据集作为示例,介绍为 TensorBoard 提供输入所需的监控内容,并突出所需的更改。

  • 然后,我们将回顾捕捉到的数据和如何解读它们。

  • 最后,我们将回顾图形浏览器可视化的常见图形。这将帮助您可视化常见的神经网络设置,这些设置将在后续章节和项目中介绍。这也是对常见网络的可视化入门。

快速预览

即使没有安装 TensorFlow,您也可以尝试 TensorBoard 的参考实现。您可以从这里开始:

www.tensorflow.org/tensorboard/index.html#graphs.

您可以在这里跟随代码:

[`github.com/tensorflow/tensorflow/blob/master/tensorflow/model

s/image/cifar10/cifar10_train.py.`](https://github.com/tensorflow/models/blob/master/tutorials/image/cifar10/cifar10_train.py)

该示例使用 CIFAR-10 图像集。CIFAR-10 数据集包含 60,000 张图像,分为 10 类,由 Alex Krizhevsky、Vinod Nair 和 Geoffrey Hinton 编制。该数据集已成为机器学习领域的几个标准学习工具和基准之一。

我们从图形浏览器开始。我们可以立即看到正在使用卷积网络。这并不令人惊讶,因为我们在这里尝试对图像进行分类:

这只是图形的其中一种视图。您也可以尝试使用图形浏览器。它允许深入查看单个组件。

我们在快速预览中的下一站是EVENTS标签页。该标签页展示了随时间变化的标量数据。不同的统计信息被分组到右侧的单独标签中。以下截图展示了多个网络部分的常见标量统计数据,例如损失、学习率、交叉熵和稀疏度:

HISTOGRAMS标签页是一个近亲,它展示了随时间变化的张量数据。尽管名字是直方图,但从 TensorFlow v0.7 开始,它实际上并不显示直方图。相反,它通过百分位数来展示张量数据的汇总信息。

总结视图如下图所示。就像在EVENTS标签页中一样,数据被分组到右侧的标签中。可以切换不同的运行,并且可以将多个运行叠加显示,便于进行有趣的对比。

它展示了三次运行,我们可以在左侧看到,我们将只查看softmax函数及其相关参数。

暂时不必太担心这些的含义,我们只需要看看我们为自己的分类器能做到什么:

然而,总结视图无法充分体现HISTOGRAMS标签页的实用性。相反,我们将放大单个图表,观察具体情况。如下图所示:

请注意,每个直方图图表展示了九条时间序列线。顶部是最大值,中间是中位数,底部是最小值。中位数上下的三条线分别表示 1½标准差、1 标准差和½标准差的位置。

显然,这确实表示了多模态分布,因为它不是一个直方图。然而,它确实提供了一个快速的概览,否则这些数据将会是需要翻阅的大量数据。

有几点需要注意:数据如何按运行收集和分隔,如何收集不同的数据流,如何放大视图,以及如何缩放到每个图表。

够了,先放下这些图形,让我们跳进代码中,亲自运行一下吧!

安装 TensorBoard

TensorFlow 已经预装了 TensorBoard,因此它会自动安装。它作为本地提供的 Web 应用程序运行,通过浏览器访问http://0.0.0.0:6006。方便的是,不需要任何服务器端的代码或配置。

根据路径的不同,您可能能够直接运行它,如下所示:

tensorboard --logdir=/tmp/tensorlogs

如果路径不正确,您可能需要根据需要在应用程序前加上前缀,如以下命令行所示:

tf_install_dir/ tensorflow/tensorboard --
logdir=/tmp/tensorlogs

在 Linux 上,您可以将其在后台运行,并让它保持运行,如下所示:

nohup tensorboard --logdir=/tmp/tensorlogs &

不过,目录结构应当谨慎考虑。仪表板左侧的“Runs”列表是由logdir位置中的子目录驱动的。以下图片展示了两个运行——MNIST_Run1MNIST_Run2。组织良好的runs文件夹可以帮助并排绘制连续的运行结果,从而查看不同之处:

在初始化writer时,您需要将日志的目录作为第一个参数传递,示例如下:

   writer = tf.summary.FileWriter("/tmp/tensorlogs",   
   sess.graph) 

考虑保存一个基础位置,并为每次运行附加特定的子目录。这将有助于组织输出,而无需再多加思考。我们稍后会进一步讨论这一点。

将 hooks 集成到我们的代码中

使用 TensorBoard 的最佳方法是通过借用现有的工作示例并用 TensorBoard 所需的代码进行标记。我们将针对几个常见的训练脚本进行这样的操作。

手写数字

让我们从典型的机器学习入门例子——MNIST 手写数字分类练习开始。

正在使用的 MNIST 数据库包含 60,000 张用于训练的图像和 10,000 张用于测试的图像。该数据最初由 Chris Burges 和 Corinna Cortes 收集,并由 Yann LeCun 进行了增强。您可以在 Yann LeCun 的官方网站上了解更多关于数据集的信息(yann.lecun.com/exdb/mnist/)。

TensorFlow 方便地提供了一个测试脚本,演示了如何使用 MNIST 手写数据集构建卷积神经网络,代码可以在github.com/tensorflow/models/blob/master/tutorials/image/mnist/convolutional.py找到。

让我们修改这个脚本以便使用 TensorBoard。如果您希望提前查看、下载金标准或查看更改,完整的修改集可以在本书的 GitHub 仓库中找到(github.com/mlwithtf/mlwithtf)。

目前,我们建议您跟着教程一步步修改,以理解整个过程。

main类的早期,我们将定义convn_weightsconvn_biases和其他参数的占位符。紧接着,我们将编写以下代码,将它们添加到histogram中:

    tf.summary.histogram('conv1_weights', conv1_weights) 
    tf.summary.histogram('conv1_biases', conv1_biases) 
    tf.summary.histogram('conv2_weights', conv2_weights) 
    tf.summary.histogram('conv2_biases', conv2_biases) 
    tf.summary.histogram('fc1_weights', fc1_weights) 
    tf.summary.histogram('fc1_biases', fc1_biases) 
    tf.summary.histogram('fc2_weights', fc2_weights) 
    tf.summary.histogram('fc2_biases', fc2_biases) 

前面的几行捕获了“HISTOGRAMS”标签页中的值。请注意,捕获的值会在“HISTOGRAMS”标签页上形成子部分,以下截图展示了这一点:

接下来,让我们记录一些loss数据。我们有如下代码作为起点:

    loss += 5e-4 * regularizers 

我们将在前面的行之后为loss数字添加一个scalar汇总:

    tf.summary.scalar("loss", loss) 

同样,我们将从计算learning_rate的标准代码开始:

     learning_rate = tf.train.exponential_decay( 
        0.01,  # Base learning rate. 
        batch * BATCH_SIZE,  # Current index into the    
        dataset. 
        train_size,  # Decay step. 
        0.95,  # Decay rate. 
        staircase=True) 

我们将为learning_rate数据添加一个scalar汇总,代码如下:

    tf.summary.scalar("learning_rate", learning_rate) 

就这两行代码,帮助我们在“EVENTS”标签页中捕获这两个重要的标量指标:

最后,让我们指示脚本保存图形设置。我们来找到创建session的脚本部分:

    # Create a local session to run the training. 
    start_time = time.time() 
    with tf.Session() as sess: 

在定义sess句柄之后,我们将如下捕获图形:

    writer = tf.summary.FileWriter("/tmp/tensorlogs",  
    sess.graph) 
    merged = tf.summary.merge_all() 

我们在运行会话时需要添加我们的merged对象。我们原本有以下代码:

    l, lr, predictions = sess.run([loss, learning_rate,  
    train_prediction], feed_dict=feed_dict) 

我们将在运行会话时添加我们的merged对象,如下所示:

    # Run the graph and fetch some of the nodes.       
    sum_string, l, lr, predictions = sess.run([merged,  
    loss,  
    learning_rate, train_prediction],  
    feed_dict=feed_dict) 

最后,我们需要在指定步骤写入摘要,就像我们通常定期输出验证集的准确度一样。因此,我们在计算sum_string后添加了一行:

    writer.add_summary(sum_string, step) 

就是这样!我们刚刚捕获了我们的损失和学习率、神经网络的关键中间参数以及图形结构。我们已经查看了 EVENTS 和 HISTOGRAMS 标签,现在让我们看看 GRAPH 标签:

AlexNet

任何参与图像深度学习的人都应该熟悉 AlexNet。该网络是在 Alex Krizhevsky、Ilya Sutskever 和 Geoffrey E. Hinton 的标志性论文《ImageNet Classification with Deep Convolutional Neural Networks》中介绍的。论文可以在www.cs.toronto.edu/~fritz/absps/imagenet.pdf查看。

该网络架构在当时的 ImageNet 年度竞赛中取得了创纪录的准确率。该架构在他们的论文中有所描述,如下图所示。我们将在后续章节中使用此网络架构,但现在,让我们通过 TensorBoard 浏览网络:

我们不会逐行审查现有的 AlexNet 代码更改,但读者可以通过注意 Google 提供的原始模型代码与我们包含在书本代码库中的修订代码之间的差异,轻松看到更改。

来自 Google 的原始 AlexNet TensorFlow 实现可以在以下位置找到:

github.com/tensorflow/models/blob/master/tutorials/image/alexnet/alexnet_benchmark.py.

修订后的 AlexNet TensorFlow 实现与 TensorBoard 工具集成可以在以下位置找到:

github.com/mlwithtf/mlwithtf/blob/master/chapter_03/alexnet_benchmark.py.

所做的更改与我们为 MNIST 示例所做的非常相似。

首先,找到以下代码的位置:

    sess = tf.Session(config=config) 
    sess.run(init) 

然后,用以下代码替换它:

    sess = tf.Session(config=config) 
    writer = tf.summary.FileWriter("/tmp/alexnet_logs",  
    sess.graph) 
    sess.run(init) 

最后,您可以运行 Python 文件alexnet_benchmark.py并使用 TensorBoard 命令来可视化图形:

python alexnet_benchmark.py
tensorboard --logdir /tmp/alexnet_logs

本节的重点是图形部分。下图显示了 Graph Explorer 的一个部分。我们深入研究了第 3 层卷积层,正在查看该层的权重和偏差。

点击图中的权重节点很有趣,因为我们可以看到形状等细节:{"shape":{"dim":[{"size":3},{"size":3},{"size":192},{"size":384}]}}。我们可以将这些细节与原始论文和之前提到的图表进行匹配!我们还可以追溯这些细节到代码中的网络设置:

    with tf.name_scope('conv3') as scope: 
      kernel = tf.Variable(tf.truncated_normal([3, 3, 192, 384], 
                               dtype=tf.float32, 
                               stddev=1e-1), name='weights') 
      conv = tf.nn.conv2d(pool2, kernel, [1, 1, 1, 1], 
       padding='SAME') 
      biases = tf.Variable(tf.constant(0.0, shape=[384], 
       dtype=tf.float32), 
                         trainable=True, name='biases') 
      bias = tf.nn.bias_add(conv, biases) 
      conv3 = tf.nn.relu(bias, name=scope) 
      parameters += [kernel, biases] 

图形浏览器和代码中的细节是等价的,但使用 TensorBoard 可以更直观地查看数据流。你还可以轻松地折叠重复的部分,展开感兴趣的部分:

图形是本节最有趣的部分,当然,你也可以运行我们修改过的脚本并查看训练性能,以及我们捕获的其他大量数据。你甚至可以捕获更多的数据。试试看吧!

自动化运行

在训练分类器时,我们通常会遇到多个我们不知道合适设置的变量。查看类似问题的解决方案所使用的值是一个很好的起点。然而,我们通常会面临一组需要测试的可能值。更复杂的是,我们通常有多个这样的参数,这将导致需要测试的组合数量大大增加。

对于这种情况,我们建议将感兴趣的参数保留为可以传递给训练器的值。然后,wrapper 脚本可以传递不同的参数组合,并带有一个可能带有描述性名称的唯一输出日志子目录。

这将使得跨多个测试结果和中间值的比较变得简单。下图展示了四次运行的损失曲线图。我们可以轻松看到表现不佳和表现过好的组合:

总结

在本章中,我们覆盖了 TensorBoard 的主要区域——EVENTS、HISTOGRAMS 和查看 GRAPH。我们修改了流行模型,以查看 TensorBoard 启动所需的精确变化。这应该展示了启动 TensorBoard 所需的非常少的工作量。

最后,我们通过查看流行模型的网络设计进行了重点研究。我们通过在代码中插入 TensorBoard hooks,并使用 TensorBoard 图形浏览器深入了解网络设置来实现这一点。

读者现在应该能够更有效地使用 TensorBoard,评估训练性能,计划运行并修改训练脚本。

接下来,我们将深入探讨卷积网络。我们将使用之前工作的部分内容,以便可以迅速开始。但我们将集中精力在更高级的神经网络设置上,以获得更好的准确性。对训练准确度的关注反映了大多数从业者的努力方向,因此,这是我们面临挑战的时刻。

第四章:猫和狗

在第二章《你的第一个分类器》中,我们为字符识别任务构建了一个简单的神经网络。我们在这一章的结尾取得了令人称赞的 80%中期准确率。不错的开始,但我们可以做得更好!

本章中,我们将为之前的分类器添加更强大的网络架构。接下来,我们将深入探讨一个更具挑战性的问题——处理 CIFAR-10 数据集中的彩色图像。图像会更加复杂(猫、狗、飞机等),因此我们将使用更强大的工具——具体来说,是卷积神经网络。让我们开始吧。

重新审视 notMNIST

让我们从渐进的方式开始,在第二章《你的第一个分类器》中使用的notMNIST数据集上尝试技术变更。你可以在阅读本章时编写代码,或者在书籍的代码库中进行操作:

github.com/mlwithtf/mlwithtf/blob/master/chapter_02/training.py

我们将从以下导入开始:

    import sys, os 
    import tensorflow as tf 
    sys.path.append(os.path.realpath('../..')) 
    from data_utils import * 
    from logmanager import * 
    import math

这里没有太多实质性的变化。真正的计算能力已经通过tensorflow包导入。你会注意到,我们再次使用了之前的data_utils工作。然而,我们那里需要做一些更改。

与之前唯一的不同是math包,我们将使用它来处理辅助的math函数,例如ceiling

程序配置

现在,让我们来看一下我们以前的程序配置,如下所示:

    batch_size = 128 
    num_steps = 10000 
    learning_rate = 0.3 
    data_showing_step = 500 

这次我们需要更多的配置。以下是我们现在将使用的配置:

 batch_size = 32 
 num_steps = 30000 
 learning_rate = 0.1 
 data_showing_step = 500 
 model_saving_step = 2000 
 log_location = '/tmp/alex_nn_log' 

 SEED = 11215 

 patch_size = 5 
 depth_inc = 4 
 num_hidden_inc = 32 
 dropout_prob = 0.8 
 conv_layers = 3 
 stddev = 0.1 

前四个配置是熟悉的:

  • 我们仍然会训练一定数量的步骤(num_steps),就像以前一样。但你会注意到,步骤数量已经增加。它们将变得更加庞大,因为我们的数据集会更复杂,需要更多的训练。

  • 我们稍后会重新审视学习率(learning_rate)的细节,但首先你已经熟悉它了。

  • 我们将在每五百步时中期回顾结果,这可以通过data_showing_step变量轻松控制。

  • 最后,log_location控制我们 TensorBoard 日志的存储位置。我们在第三章《TensorFlow 工具箱》中已经对它非常熟悉。这一章我们将再次使用它,但这次不再做解释。

下一个配置——随机种子SEED)变量——可能会很有帮助。这个变量可以不设置,TensorFlow 会在每次运行时随机化数字。然而,设置一个seed变量并且在每次运行中保持一致,将有助于我们在调试系统时保持一致性。如果你使用它(推荐从一开始就使用),你可以将它设置为任何你喜欢的数字:你的生日、纪念日、第一次电话号码或幸运数字。我用的是我心爱的社区的邮政编码。享受那些小事吧。

最后,我们会遇到七个新变量——batch_sizepatch_sizedepth_incnum_hidden_incconv_layersstddevdropout_prob。这些是我们更新版、更先进的卷积神经网络CNN)工作的核心,在我们探讨所使用的网络时,会在具体上下文中引入。

理解卷积神经网络

卷积神经网络(CNN)是专为机器学习中的图像处理而设计的更先进的神经网络。与我们之前使用的隐藏层不同,CNN 有一些未完全连接的层。这些卷积层除了宽度和高度外,还有深度。一般原则是,图像是按补丁逐个分析的。我们可以如下可视化图像中的 7x7 补丁:

这反映了一个 32x32 的灰度图像,使用了 7x7 的补丁。以下是从左到右滑动补丁的示例:

如果这是一张彩色图像,我们会同时在三个相同的层上滑动补丁。

你可能注意到我们每次滑动补丁时,只移动了一个像素。这也是一种配置;我们也可以滑动得更多,也许每次移动两个甚至三个像素。这就是步幅配置。正如你猜测的那样,步幅越大,我们最终覆盖的补丁越少,因此输出层会更小。

矩阵运算,我们在此不作详细讨论,用于将补丁(其完整深度由通道数决定)压缩成一个输出深度列。输出只有一个高度和宽度,但深度很大。随着我们迭代地滑动补丁,深度列的序列形成了一个具有新长度、宽度和高度的块。

此外,还有一种配置在此起作用——图像边缘的填充。如你所想,填充越多,补丁滑动的空间就越大,可以越过图像的边缘。这使得步幅增大,从而输出体积的长度和宽度也更大。你稍后会在代码中看到这个配置,padding='SAME'padding='VALID'

让我们来看这些是如何累加的。首先我们选择一个补丁:

然而,补丁不仅仅是方形的,而是整个深度(针对彩色图像):

然后我们将其卷积成一个 1x1 的体积,但有深度,如下图所示。结果体积的深度是可配置的,我们将在程序中使用inct_depth来设置这个配置:

最后,当我们继续滑动补丁时,补丁会多次穿越原始图像,生成多个 1x1xN 的体积,这些将组合成一个体积:

然后我们将其卷积成一个 1x1 的体积。

最后,我们将使用POOL操作压缩结果体积的每一层。这里有许多类型,但简单的最大池化是最典型的:

就像我们之前使用的滑动补丁一样,这里会有一个补丁(不过这次我们会取补丁的最大值)和一个步长(这次我们需要一个更大的步长来压缩图像)。我们本质上是在减少图像大小。在这里,我们将使用一个 3x3 的补丁,步长为 2。

重新审视配置

现在我们已经介绍了卷积神经网络,让我们重新审视之前遇到的配置:batch_sizepatch_sizedepth_incnum_hidden_incconv_layersstddevdropout_prob

  • 批量大小(batch_size

  • 补丁大小(patch_size

  • 深度增量(depth_inc

  • 隐藏层增量(num_hidden_inc

  • 卷积层(conv_layers

  • 标准差(stddev

  • 丢弃概率(dropout_prob

构建卷积网络

我们将跳过对两个工具函数reformataccuracy的解释,因为我们已经在第二章中遇到过它们,你的第一个分类器。相反,我们将直接跳到神经网络配置部分。为了比较,以下图展示了我们在第二章中看到的模型,你的第一个分类器,接下来的图展示了我们的新模型。我们将在相同的notMNIST数据集上运行新模型,看看我们能获得的准确率提升(提示:好消息!)。

下图是我们的新模型:

首先,我们将遇到一个helper函数,具体如下:

    def fc_first_layer_dimen(image_size, layers): 
       output = image_size 
       for x in range(layers): 
        output = math.ceil(output/2.0) 
       return int(output) 

然后,我们将在稍后调用它,如下所示:

    fc_first_layer_dimen(image_size, conv_layers) 

fc_first_layer_dimen函数计算第一个全连接层的维度。回想一下,CNN 通常使用一系列逐层变小的窗口层。在这里,我们决定将每个卷积层的维度缩小一半。这也说明了为什么输入图像在被 2 的幂次方整除时,事情变得干净而简洁。

现在让我们解析实际的网络。它是通过nn_model方法生成的,并在训练模型时稍后调用,在验证集和测试集上进行测试时再次调用。

记得 CNN 通常由以下几层组成:

  • 卷积层

  • 线性整流单元(ReLU)层

  • 池化层

  • 全连接层

卷积层通常与RELU层配对并重复使用。这正是我们所做的——我们将三个几乎相同的CONV-RELU层堆叠在一起。

每一对配对的层如下所示:

    with tf.name_scope('Layer_1') as scope: 
        conv = tf.nn.conv2d(data, weights['conv1'], strides=[1, 1, 
         1, 1], padding='SAME', name='conv1')        
        bias_add = tf.nn.bias_add(conv, biases['conv1'], 
         name='bias_add_1') 
        relu = tf.nn.relu(bias_add, name='relu_1') 
        max_pool = tf.nn.max_pool(relu, ksize=[1, 2, 2, 1], 
         strides=[1, 2, 2, 1], padding='SAME', name=scope)

三个几乎相同的层(Layer_1Layer_2Layer_3)之间的主要区别在于如何将一个层的输出传递给下一个层。所以,第一层开始时接受数据(图像数据),但第二层开始时接受来自第一层的池化层输出,具体如下:

    conv = tf.nn.conv2d(max_pool, weights['conv2'], strides=[1, 1, 1, 
     1], padding='SAME', name='conv2')

同样,第三层开始时接受来自第二层的池化层输出,具体如下:

    conv = tf.nn.conv2d(max_pool, weights['conv3'], strides=[1, 1, 1, 
     1], padding='SAME', name='conv3')

三个CONV-RELU层之间有另一个主要的区别,即这些层被压缩了。你可以通过在每个层声明后使用几个print语句来看一下conv变量,这可能会有所帮助:

    print "Layer 1 CONV", conv.get_shape() 
    print "Layer 2 CONV", conv.get_shape() 
    print "Layer 3 CONV", conv.get_shape() 

这将揭示以下结构:

Layer 1 CONV (32, 28, 28, 4) 
Layer 2 CONV (32, 14, 14, 4) 
Layer 3 CONV (32, 7, 7, 4) 
Layer 1 CONV (10000, 28, 28, 4) 
Layer 2 CONV (10000, 14, 14, 4) 
Layer 3 CONV (10000, 7, 7, 4) 
Layer 1 CONV (10000, 28, 28, 4) 
Layer 2 CONV (10000, 14, 14, 4) 
Layer 3 CONV (10000, 7, 7, 4) 

我们用notMNIST数据集运行了这个,因此我们将看到原始输入大小为 28x28,这不奇怪。更有趣的是连续层的大小——14x14 和 7x7。注意,连续卷积层的滤波器是如何被压缩的。

让我们让事情变得更有趣,检查整个堆栈。添加以下print语句来查看CONVRELUPOOL层:

 print "Layer 1 CONV", conv.get_shape() 
 print "Layer 1 RELU", relu.get_shape() 
 print "Layer 1 POOL", max_pool.get_shape() 

在其他两个CONV-RELU-POOL堆栈后添加类似的语句,你将得到以下输出:

Layer 1 CONV (32, 28, 28, 4) 
Layer 1 RELU (32, 28, 28, 4) 
Layer 1 POOL (32, 14, 14, 4) 
Layer 2 CONV (32, 14, 14, 4) 
Layer 2 RELU (32, 14, 14, 4) 
Layer 2 POOL (32, 7, 7, 4) 
Layer 3 CONV (32, 7, 7, 4) 
Layer 3 RELU (32, 7, 7, 4) 
Layer 3 POOL (32, 4, 4, 4) 
... 

我们将忽略来自验证集和测试集实例的输出(它们是相同的,只是由于我们在处理验证集和测试集而不是小批量数据,因此高度是 10000 而不是32)。

我们将从输出中看到,POOL层如何压缩维度(从2814),以及这种压缩如何传递到下一个CONV层。在第三个也是最后一个POOL层,我们将得到一个 4x4 的大小。

最终CONV堆栈中还有一个特性——我们在训练时会使用的dropout层,具体如下:

 max_pool = tf.nn.dropout(max_pool, dropout_prob, seed=SEED, 
  name='dropout')

这个层利用了我们之前设置的dropout_prob = 0.8配置。它随机丢弃该层上的神经元,以通过禁止节点与相邻节点共同适应而防止过拟合;节点永远不能依赖某个特定节点的存在。

让我们继续前进,看看我们的网络。我们会找到一个全连接层,后面跟着一个RELU层:

    with tf.name_scope('FC_Layer_1') as scope: 
        matmul = tf.matmul(reshape, weights['fc1'], 
         name='fc1_matmul')       
         bias_add = tf.nn.bias_add(matmul, biases['fc1'], 
         name='fc1_bias_add') 
        relu = tf.nn.relu(bias_add, name=scope) 

最后,我们将以一个全连接层结束,具体如下:

    with tf.name_scope('FC_Layer_2') as scope: 
        matmul = tf.matmul(relu, weights['fc2'], 
         name='fc2_matmul')       
        layer_fc2 = tf.nn.bias_add(matmul, biases['fc2'], 
         name=scope)

这是卷积网络中的典型做法。通常,我们会以一个全连接的RELU层结束,最后是一个全连接层,它保存每个类别的得分。

我们在过程中跳过了一些细节。我们的大多数层都通过三个其他值进行了初始化——weightsbiasesstrides

weightsbiases本身是通过其他变量初始化的。我没有说这会很容易。

这里最重要的变量是patch_size,它表示我们滑过图像的滤波器大小。回想一下,我们早些时候将其设置为 5,所以我们将使用 5x5 的补丁。我们还将重新介绍我们之前设置的stddevdepth_inc配置。

完成

很可能,到现在为止,你脑海中一定有许多问题——为什么是三个卷积层而不是两个或四个?为什么步幅是 1?为什么补丁大小是 5?为什么最终是全连接层,而不是从全连接层开始?

这里有一定的方法可循。从核心上讲,卷积神经网络(CNN)是围绕图像处理构建的,而补丁则围绕着待提取的特征构建。为什么某些配置效果很好,而其他配置效果不佳,目前尚不完全理解,尽管一些普遍的规则符合直觉。准确的网络架构是通过数千次的试验和许多错误发现、磨练的,并且不断朝着完美的方向迈进。这仍然是一个研究级的任务。

从业者的一般方法是找到一个已经有效的现有架构(例如,AlexNet、GoogLeNet、ResNet),并针对特定数据集进行调整。这就是我们所做的;我们从 AlexNet 开始并进行了调整。也许,这并不令人满足,但它有效,并且在 2016 年仍然是实践的常态。

训练日

然而,看到我们的训练过程并了解如何改进我们之前所做的工作,会更加令人满足。

我们将按照如下方式准备训练数据集和标签:

    tf_train_dataset = tf.placeholder(tf.float32, 
    shape=(batch_size, image_size, image_size,   
    num_channels), 
    name='TRAIN_DATASET')    
    tf_train_labels = tf.placeholder(tf.float32, 
    shape=(batch_size, num_of_classes), 
    name='TRAIN_LABEL') 
    tf_valid_dataset = tf.constant(dataset.valid_dataset,   
    name='VALID_DATASET') 
    tf_test_dataset = tf.constant(dataset.test_dataset,  
    name='TEST_DATASET') 

然后,我们将运行训练器,如下所示:

    # Training computation. 
    logits = nn_model(tf_train_dataset, weights, biases,  
    True) 
    loss = tf.reduce_mean( 
        tf.nn.softmax_cross_entropy_with_logits(logits, 
         tf_train_labels)) 
    # L2 regularization for the fully connected  
    parameters. 
    regularizers = (tf.nn.l2_loss(weights['fc1']) + 
     tf.nn.l2_loss(biases['fc1']) + 
     tf.nn.l2_loss(weights['fc2']) + 

    tf.nn.l2_loss(biases['fc2'])) 
    # Add the regularization term to the loss. 
    loss += 5e-4 * regularizers 
    tf.summary.scalar("loss", loss) 

这与我们在第二章中做的非常相似,你的第一个分类器。我们实例化了网络,传入了一组初始权重和偏差,并定义了一个使用训练标签的loss函数。然后,我们定义了优化器,目标是最小化该loss,如下所示:

    optimizer = tf.train.GradientDescentOptimizer
     (learning_rate).minimize(loss)

然后,我们将使用weightsbiases来预测验证集标签,最终是训练集标签:

    train_prediction = tf.nn.softmax(nn_model(tf_train_dataset,  
    weights, biases, TRAIN=False)) 
    valid_prediction = tf.nn.softmax(nn_model(tf_valid_dataset, 
     weights, biases))    test_prediction =  
     tf.nn.softmax(nn_model(tf_test_dataset, 
     weights, biases))

完整的训练代码如下:

最后,我们将运行会话。我们将使用之前设置的num_steps变量,并按块(batch_size)遍历训练数据。我们将加载小块的训练数据和相应的标签,并按如下方式运行会话:

    batch_data = dataset.train_dataset[offset:(offset + 
     batch_size), :]   
    batch_labels = dataset.train_labels[offset: 
     (offset + 
     batch_size), :]

我们将对小批量数据进行预测,并将其与实际标签进行比较,以计算小批量的准确率。

我们将使用之前声明的valid_prediction

    valid_prediction =   
    tf.nn.softmax(nn_model(tf_valid_dataset, 
     weights, biases))

然后,我们将评估验证集的预测结果与实际标签进行比较,如下所示:

    accuracy(valid_prediction.eval(), 
    dataset.valid_labels) 

在我们完成所有步骤之后,我们将在测试集上做同样的事情:

    accuracy(test_prediction.eval(), dataset.test_labels)

如你所见,实际的训练、验证和测试执行与之前没有太大不同。不同之处在于准确率。注意,我们已经突破了 80%的准确率,进入了测试集准确率的 90%:

实际的猫和狗

我们已经在notMNIST数据集上展示了我们新的工具,这很有帮助,因为它为我们提供了与之前更简单网络设置的比较。现在,让我们进阶到一个更困难的问题——实际的猫和狗。

我们将使用 CIFAR-10 数据集。这个数据集中不只有猫和狗,还有 10 个类别——飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车。与 notMNIST 数据集不同,这里有两个主要的复杂性,具体如下:

  • 照片中有更多的异质性,包括背景场景

  • 这些照片是彩色的

我们之前没有处理过彩色数据集。幸运的是,它与通常的黑白数据集并没有太大区别——我们只需增加一个维度。回想一下,我们之前的 28x28 图像是平面矩阵。现在,我们将有 32x32x3 的矩阵——额外的维度代表每个红色、绿色和蓝色通道的层。这样做确实让数据集的可视化变得更加困难,因为堆叠图像将进入第四维度。所以,我们的训练/验证/测试集现在将是 32x32x3xSET_SIZE 的维度。我们只需要习惯处理那些我们无法在熟悉的 3D 空间中可视化的矩阵。

颜色维度的机制是一样的。就像之前我们有浮动点数字表示灰度的不同深浅,现在我们将有浮动点数字表示红色、绿色和蓝色的不同深浅。

还记得我们如何加载 notMNIST 数据集吗?

    dataset, image_size, num_of_classes, num_channels = 
     prepare_not_mnist_dataset() 

num_channels 变量决定了颜色通道。直到现在,它只有一个通道。

我们将类似地加载 CIFAR-10 数据集,不过这次我们会返回三个通道,如下所示:

    dataset, image_size, num_of_classes, num_channels = 
     prepare_cifar_10_dataset()

不要重新发明轮子。

还记得我们如何自动化抓取、提取和准备 notMNIST 数据集的过程吗?这在第二章《你的第一个分类器》中讲解过。我们将这些流程函数放进了 data_utils.py 文件中,以便将流程代码与实际的机器学习代码分开。拥有这样的清晰分离,并保持干净、通用的函数,可以让我们在当前项目中重用它们。

特别地,我们将重用其中的九个函数,具体如下:

  • download_hook_function

  • download_file

  • extract_file

  • load_class

  • make_pickles

  • randomize

  • make_arrays

  • merge_datasets

  • pickle_whole

回想一下我们如何在一个总的函数 prepare_not_mnist_dataset 中使用那些函数,这个函数为我们运行了整个流程。我们之前只是重用了这个函数,为自己节省了不少时间。

让我们为 CIFAR-10 数据集创建一个类似的函数。一般来说,你应该保存自己的流程函数,尝试将它们通用化,独立成一个模块,并在不同项目中重用它们。当你做自己的项目时,这会帮助你专注于关键的机器学习工作,而不是花时间去重建流程。

注意更新版的 data_utils.py;我们有一个总的函数叫做 prepare_cifar_10_dataset,它将数据集细节和这个新数据集的流程隔离开来,如下所示:

  def prepare_cifar_10_dataset(): 
    print('Started preparing CIFAR-10 dataset') 
    image_size = 32 
    image_depth = 255 
    cifar_dataset_url = 'https://www.cs.toronto.edu/~kriz/cifar-
     10-python.tar.gz' 
    dataset_size = 170498071 
    train_size = 45000 
    valid_size = 5000 
    test_size = 10000 
    num_of_classes = 10 
    num_of_channels = 3 
    pickle_batch_size = 10000 

这是之前代码的快速概览:

  • 我们将使用 cifar_dataset_url = 'https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz' 从亚历克斯·克里日夫斯基(Alex Krizhevsky)在多伦多大学的站点获取数据集

  • 我们将使用 dataset_size = 170498071 来验证我们是否已成功接收到文件,而不是某个被截断的半下载文件

  • 我们还将基于对数据集的了解声明一些细节

  • 我们将把 60,000 张图片划分为训练集、验证集和测试集,分别包含 45000500010000 张图片

  • 数据集共有十个类别,因此我们有 num_of_classes = 10

  • 这些是带有红、绿、蓝三个通道的彩色图像,因此我们有 num_of_channels = 3

  • 我们知道图片的尺寸是 32x32 像素,因此我们设置 image_size = 32,并将其应用于宽度和高度

  • 最后,我们知道每个通道的图像深度为 8 位,因此我们设置 image_depth = 255

  • 数据将被保存到 /datasets/CIFAR-10/ 目录

就像我们之前处理 notMNIST 数据集一样,我们只有在尚未拥有数据集的情况下才会下载它。我们将解压数据集,进行必要的转换,并使用 pickle_cifar_10 将预处理后的矩阵保存为 pickle 文件。如果我们找到 pickle 文件,就可以使用 load_cifar_10_from_pickles 方法重新加载中间数据。

以下是我们将用来保持主方法简洁的三个辅助方法:

  • pickle_cifar_10

  • load_cifar_10_from_pickles

  • load_cifar_10_pickle

函数定义如下:

load_cifar_10_pickle 方法会分配 NumPy 数组来存储训练和测试数据及其标签,并将现有的 pickle 文件加载到这些数组中。由于我们需要执行两次这个操作,我们将提取出 load_cifar_10_pickle 方法,它实际上加载数据并将其中心化:

就像之前一样,我们将检查 pickle 文件是否已存在,如果存在就加载。如果不存在(即 else 部分),我们才会将已准备好的数据保存为 pickle 文件。

保存模型以供后续使用

为了将 TensorFlow 会话中的变量保存以供将来使用,你可以使用 Saver() 函数。我们从创建一个 saver 变量开始,紧接着是 writer 变量:

    writer = tf.summary.FileWriter(log_location, session.graph)
    saver = tf.train.Saver(max_to_keep=5)

然后,在训练循环中,我们将添加以下代码,以便在每次 model_saving_step 后保存模型:

 if step % model_saving_step == 0 or step == num_steps + 1: 
   path = saver.save(session, os.path.join(log_location,  
 "model.ckpt"), global_step=step) 
   logmanager.logger.info('Model saved in file: %s' % path) 

之后,每当我们想要使用 saved 模型恢复模型时,我们可以轻松创建一个新的 Saver() 实例并使用 restore 函数,如下所示:

 checkpoint_path = tf.train.latest_checkpoint(log_location) 
 restorer = tf.train.Saver() 
 with tf.Session() as sess: 
    sess.run(tf.global_variables_initializer()) 
    restorer.restore(sess, checkpoint_path) 

在之前的代码中,我们使用 tf.train.latest_checkpoint,这样 TensorFlow 会自动选择最新的模型检查点。然后,我们创建一个新的 Saver 实例,命名为 restore。最后,我们可以使用 restore 函数将 saved 模型加载到会话图中:

    restorer.restore(sess, checkpoint_path) 

你应该注意到,在运行 tf.global_variables_initializer 后,我们必须进行恢复。否则,加载的变量将被初始化器覆盖。

使用分类器

现在我们已经增强了分类器,以加载随机图像,接下来我们将选择与训练/测试图像大小和形状完全相同的随机图像。我们需要为这些用户提供的图像添加占位符,因此我们将在适当的位置添加以下行:

 tf_random_dataset = tf.placeholder(tf.float32, shape=(1, 
  image_size, image_size, num_channels),  
 name='RANDOM_DATA')random_prediction =  
 tf.nn.softmax(nn_model(tf_random_dataset, 
  weights, biases))

接下来,我们将通过以下命令行参数获取用户提供的图像,并在该图像上运行我们的会话:

我们将按照与之前几乎相同的顺序进行操作。通过 -e 开关运行 test 文件将产生额外的输出,如下所示:

    The prediction is: 2 

好了!我们刚刚对一个任意图像进行了分类。

学到的技能

你应该在本章中学会了这些技能:

  • 准备更高级的颜色训练和测试数据

  • 设置卷积神经网络图

  • 与 CNN 相关的参数和配置

  • 创建一个完整的系统,包括 TensorBoard 的钩子

  • 导入真实世界数据

总结

很棒!我们刚刚建立了一个更高级的分类器,切换了不同的模型,甚至开始将我们的分类器应用于任意模型。正如本章名称所示,我们还训练了系统区分猫和狗。

在下一章中,我们将开始使用序列到序列的模型,并使用 TensorFlow 编写一个英法翻译器。

第五章:序列到序列模型——你会说法语吗?

到目前为止,我们的工作主要集中在图像上。处理图像是有帮助的,因为结果几乎令人难以置信,进展可以如此快速和简洁。然而,机器学习的世界更为广泛,接下来的几章将涉及这些其他方面。我们将从序列到序列模型开始。结果同样令人惊叹,尽管设置稍微复杂一些,且训练数据集要大得多。

本章将重点介绍以下几个领域:

  • 理解序列到序列模型的工作原理

  • 理解如何为序列到序列模型提供数据

  • 使用序列到序列模型编写英语到法语的翻译器

快速预览

是的,你没看错……我们将编写一个英语到法语的翻译器。机器学习前的世界可能会用一系列解析器和规则来翻译单词和短语,但我们的方法将更加优雅、通用且快速。我们将只使用例子,很多例子,来训练我们的翻译器。

这里的任务是找到一个包含足够多英语句子并已翻译成法语的数据集(实际上,它适用于任何语言)。翻译的文章和新闻报道不太有用,因为我们未必能够将一个语言中的具体句子与另一语言的句子逐一对应。因此,我们需要更具创意。幸运的是,像联合国这样的组织常常需要做这种工作——它们需要逐行翻译,以满足其多样化群体的需求。对我们来说,这真是太方便了!

统计机器翻译研讨会在 2010 年召开了一次会议,发布了一个很好的训练集包,可以使用。详细信息可见www.statmt.org/wmt10/

我们将仅使用特定的法语文件,如下所示:

以下是源数据在英语端的一个摘录:

  • 食品——欧洲通胀的隐性问题

  • 食品价格飙升是推动欧元区通胀加速的主要因素

  • 欧元区 13 个国家的 11 月价格上涨高于预期,10 月的年通胀率为 2.6%,11 月为 3.1%,欧盟卢森堡的统计局报告称

  • 官方预测仅为 3%,彭博社报道

  • 与美国、英国和加拿大的中央银行不同,欧洲中央银行ECB)并未降息,理由是降息结合原材料价格上涨和失业率下降,将引发通货膨胀的螺旋。

  • 欧洲中央银行希望将通胀控制在 2%以下,或者在该范围内

  • 根据一位分析师的说法,欧洲央行陷入了一个两难境地,它需要降低通胀,以避免在稍后的阶段采取行动来将其压低。

这里是法语的对应版本:

  • 在欧洲,食品价格推动了通胀的失控。

  • 欧元区的加速通胀,主要是由于食品价格的快速上涨。

  • 11 月,欧元区 13 个国家的价格上涨幅度超过了预测,在 10 月通胀率为 2.6%的情况下,年通胀率达到了 3.1%,欧盟统计局位于卢森堡的办公室表示。

  • 官方预测仅为 3 个百分点,彭博社报道。

  • 与美国、英国和加拿大的中央银行不同,欧洲中央银行(ECB)没有降低基准利率,称降息将会导致原材料价格上涨和失业率下降,从而引发通胀螺旋。

  • 欧洲央行希望将通胀率保持在接近但低于 2%。

  • 根据一位分析师的说法,欧洲央行陷入了“Catch 22”:必须“遏制”通胀,以避免稍后对此采取干预措施。

通常,在可能的情况下进行快速的合理性检查是很好的做法,以确保文件确实对齐。我们可以看到两个文件第 7 行都出现了Catch 22这一短语,这让我们感到放心。

当然,7 行远远不足以进行统计分析。我们只有通过大量数据才能实现一个优雅、可泛化的解决方案。我们将为训练集获取的海量数据将由 20GB 的文本组成,每行翻译就像前面的摘录一样。

就像我们处理图像数据一样,我们将使用子集进行训练、验证和测试。我们还将定义损失函数并尝试最小化该损失。让我们从数据开始吧。

从消防栓里喝水

就像你之前做的那样,你应该从github.com/mlwithtf/MLwithTF/抓取代码。

我们将专注于chapter_05子文件夹,它包含以下三个文件:

  • data_utils.py

  • translate.py

  • seq2seq_model.py

第一个文件处理我们的数据,让我们从这个开始。prepare_wmt_dataset函数处理这个问题。它与我们过去抓取图像数据集的方式非常相似,只不过现在我们抓取的是两个数据子集:

  • giga-fren.release2.fr.gz

  • giga-fren.release2.en.gz

当然,这就是我们想要关注的两种语言。我们即将构建的翻译器的美妙之处在于,它的方式是完全通用的,因此我们可以轻松地为德语或西班牙语等语言创建翻译器。

以下截图展示了特定的代码子集:

接下来,我们将逐行处理之前提到的两个文件,做两件事——创建词汇表并对单个单词进行分词。这些工作将通过create_vocabularydata_to_token_ids函数完成,我们稍后会介绍。现在,让我们观察如何在庞大的训练集以及一个小的开发集(newstest2013.frdev/newstest2013.en)上创建词汇表和进行分词:

我们之前使用以下create_vocabulary函数创建了一个词汇表。我们将从一个空的词汇映射开始,vocab = {},然后逐行读取数据文件,对每一行,使用基本的分词器创建一个单词桶。(警告:这与后面 ID 函数中的更重要的令牌不应混淆。)

如果我们遇到一个我们词汇表中已有的单词,我们将按如下方式递增它:

    vocab[word] += 1 

否则,我们将初始化该单词的计数,如下所示:

    vocab[word] += 1 

我们将继续这样做,直到我们的训练数据集中的所有行都处理完毕。接下来,我们将使用sorted(vocab, key=vocab.get, reverse=True)按频率对我们的词汇表进行排序。

这很重要,因为我们不会保留每一个单词,我们只会保留最频繁的 k个单词,其中k是我们定义的词汇表大小(我们将其定义为 40,000,但你可以选择不同的值,看看结果如何变化):

尽管处理句子和词汇表是直观的,但此时需要更抽象一些——我们将暂时把我们学到的每个词汇单词转换成一个简单的整数。我们将逐行使用sequence_to_token_ids函数来完成这项工作:

我们将使用data_to_token_ids函数将这种方法应用于整个数据文件,该函数读取我们的训练文件,逐行迭代,并运行sequence_to_token_ids函数,然后使用我们的词汇表将每个句子中的单个单词转换为整数:

这让我们处于什么状态呢?我们有了两个仅包含数字的数据集。我们只是暂时将我们的英语到法语的问题转化为一个数字到数字的问题,包含两组将单词映射到词汇表单词的数字序列。

如果我们从["Brooklyn", "has", "lovely", "homes"]开始,并生成一个{"Brooklyn": 1, "has": 3, "lovely": 8, "homes": 17}的词汇表,那么我们将得到[1, 3, 8, 17]

输出看起来怎样?以下是典型的文件下载:

    ubuntu@ubuntu-PC:~/github/mlwithtf/chapter_05$: python translate.py
    Attempting to download http://www.statmt.org/wmt10/training-giga-  
    fren.tar
    File output path:   
    /home/ubuntu/github/mlwithtf/datasets/WMT/training-giga-fren.tar
    Expected size: 2595102720
    File already downloaded completely!
    Attempting to download http://www.statmt.org/wmt15/dev-v2.tgz
    File output path: /home/ubuntu/github/mlwithtf/datasets/WMT/dev- 
    v2.tgz
    Expected size: 21393583
    File already downloaded completely!
    /home/ubuntu/github/mlwithtf/datasets/WMT/training-giga-fren.tar 
    already extracted to  
    /home/ubuntu/github/mlwithtf/datasets/WMT/train
    Started extracting /home/ubuntu/github/mlwithtf/datasets/WMT/dev- 
    v2.tgz to /home/ubuntu/github/mlwithtf/datasets/WMT
    Finished extracting /home/ubuntu/github/mlwithtf/datasets/WMT/dev-  
    v2.tgz to /home/ubuntu/github/mlwithtf/datasets/WMT
    Started extracting  
    /home/ubuntu/github/mlwithtf/datasets/WMT/train/giga- 
    fren.release2.fixed.fr.gz to  
    /home/ubuntu/github/mlwithtf/datasets/WMT/train/data/giga- 
    fren.release2.fixed.fr
    Finished extracting  
    /home/ubuntu/github/mlwithtf/datasets/WMT/train/giga-
    fren.release2.fixed.fr.gz to 
    /home/ubuntu/github/mlwithtf/datasets/WMT/train/data/giga-  
    fren.release2.fixed.fr
    Started extracting  
    /home/ubuntu/github/mlwithtf/datasets/WMT/train/giga-
    fren.release2.fixed.en.gz to 
    /home/ubuntu/github/mlwithtf/datasets/WMT/train/data/giga- 
    fren.release2.fixed.en
    Finished extracting  
    /home/ubuntu/github/mlwithtf/datasets/WMT/train/giga-
    fren.release2.fixed.en.gz to 
    /home/ubuntu/github/mlwithtf/datasets/WMT/train/data/giga- 
    fren.release2.fixed.en
    Creating vocabulary  
    /home/ubuntu/github/mlwithtf/datasets/WMT/train/data/vocab40000.fr 
    from 
    data /home/ubuntu/github/mlwithtf/datasets/WMT/train/data/giga- 
    fren.release2.fixed.fr
      processing line 100000
      processing line 200000
      processing line 300000
     ...
      processing line 22300000
      processing line 22400000
      processing line 22500000
     Tokenizing data in 
     /home/ubuntu/github/mlwithtf/datasets/WMT/train/data/giga-
     fren.release2.fr
      tokenizing line 100000
      tokenizing line 200000
      tokenizing line 300000
     ...
      tokenizing line 22400000
      tokenizing line 22500000
     Creating vocabulary 
     /home/ubuntu/github/mlwithtf/datasets/WMT/train/data/vocab
     40000.en from data 
     /home/ubuntu/github/mlwithtf/datasets/WMT/train/data/giga-
     fren.release2.en
      processing line 100000
      processing line 200000
      ...

我不会重复数据集处理中的英语部分,因为它完全相同。我们将逐行读取庞大的文件,创建词汇表,并逐行对每个语言文件中的单词进行分词。

训练日

我们努力的关键是训练,这在我们之前遇到的第二个文件translate.py中展示。我们之前回顾过的prepare_wmt_dataset函数,当然是起点,它创建了我们的两个数据集并将它们标记为干净的数字。

训练如下开始:

准备好数据后,我们将像往常一样创建一个 TensorFlow 会话,并构建我们的模型。我们稍后会详细介绍模型;现在,先来看一下我们的准备工作和训练循环。

我们稍后会定义开发集和训练集,但现在,我们将定义一个范围从 0 到 1 的浮动分数。这部分没有什么复杂的;真正的工作将在接下来的训练循环中进行。这与我们在之前章节中所做的非常不同,所以需要格外注意。

我们的主要训练循环旨在最小化我们的误差。这里有两个关键的语句。首先是第一个:

    encoder_inputs, decoder_inputs, target_weights = 
     model.get_batch(train_set, bucket_id)

第二个关键点如下:

    _, step_loss, _ = model.step(sess, encoder_inputs, decoder_inputs, 
     target_weights, bucket_id, False)

get_batch函数本质上是用来将这两个序列转换为批量主向量和相关的权重。然后,这些会被用于模型步骤,从而返回我们的损失。

我们并不会直接处理损失,而是使用perplexity,它是损失的e次方:

每经过X步,我们将使用previous_losses.append(loss)保存进度,这非常重要,因为我们将比较当前批次的损失与之前的损失。当损失开始上升时,我们将使用以下方式降低学习率:

sess.run(model.learning_rate_decay_op),并在dev_set上评估损失,就像我们在之前的章节中使用验证集一样:

当我们运行时,我们会得到以下输出:

    put_count=2530 evicted_count=2000 eviction_rate=0.790514 and 
     unsatisfied allocation rate=0
    global step 200 learning rate 0.5000 step-time 0.94 perplexity 
     1625.06
      eval: bucket 0 perplexity 700.69
      eval: bucket 1 perplexity 433.03
      eval: bucket 2 perplexity 401.39
      eval: bucket 3 perplexity 312.34
    global step 400 learning rate 0.5000 step-time 0.91 perplexity   
     384.01
      eval: bucket 0 perplexity 124.89
      eval: bucket 1 perplexity 176.36
      eval: bucket 2 perplexity 207.67
      eval: bucket 3 perplexity 239.19
    global step 600 learning rate 0.5000 step-time 0.87 perplexity   
     266.71
      eval: bucket 0 perplexity 75.80
      eval: bucket 1 perplexity 135.31
      eval: bucket 2 perplexity 167.71
      eval: bucket 3 perplexity 188.42
    global step 800 learning rate 0.5000 step-time 0.92 perplexity  
     235.76
      eval: bucket 0 perplexity 107.33
      eval: bucket 1 perplexity 159.91
      eval: bucket 2 perplexity 177.93
      eval: bucket 3 perplexity 263.84  

我们会在每 200 步看到输出。这是我们使用的约十几个设置之一,它们在文件顶部定义:

 tf.app.flags.DEFINE_float("learning_rate"", 0.5, ""Learning  
                             rate."") 
 tf.app.flags.DEFINE_float("learning_rate_decay_factor"", 0.99, 
                          "Learning rate decays by this much."") 
 tf.app.flags.DEFINE_float("max_gradient_norm"", 5.0, 
                          "Clip gradients to this norm."") 
 tf.app.flags.DEFINE_integer("batch_size"", 64, 
                            "Batch size to use during training."") 
 tf.app.flags.DEFINE_integer("en_vocab_size"", 40000, ""Size ...."") 
 tf.app.flags.DEFINE_integer("fr_vocab_size"", 40000, ""Size  
                              of...."") 
 tf.app.flags.DEFINE_integer("size"", 1024, ""Size of each  
                              model..."") 
 tf.app.flags.DEFINE_integer("num_layers"", 3, ""#layers in the 
                model."")tf.app.flags.DEFINE_string("train_dir"", 
  os.path.realpath(''../../datasets/WMT''), ""Training directory."") 
 tf.app.flags.DEFINE_integer("max_train_data_size"", 0, 
                            "Limit size of training data "") 
 tf.app.flags.DEFINE_integer("steps_per_checkpoint"", 200, 
                            "Training steps to do per 
                             checkpoint."")

我们将在构建模型对象时使用这些设置。也就是说,最终的拼图就是模型本身,所以让我们来看一下这个部分。我们将回到我们项目中的第三个也是最后一个文件——seq2seq_model.py

还记得我们在训练开始时创建模型的过程吗?我们定义的大多数参数用于初始化以下模型:

    model = seq2seq_model.Seq2SeqModel( 
      FLAGS.en_vocab_size, FLAGS.fr_vocab_size, _buckets, 
      FLAGS.size, FLAGS.num_layers, FLAGS.max_gradient_norm, 
       FLAGS.batch_size, 
      FLAGS.learning_rate, FLAGS.learning_rate_decay_factor, 
      forward_only=forward_only) 

然而,初始化所做的工作在seq2seq_model.py中,所以让我们跳到那部分。

你会发现模型非常庞大,这就是为什么我们不逐行解释,而是逐块解释。

第一部分是模型的初始化,通过以下两张图示范:

模型从初始化开始,这会设置所需的参数。我们将跳过这些参数的设置部分,因为我们已经熟悉它们——我们在训练之前就已经自己初始化了这些参数,只需将值传入模型构建语句中,最终通过self.xyz赋值将它们传递到内部变量中。

回想一下我们是如何传入每个模型层的大小(size=1024)和层数(3)的。这些非常重要,因为我们在构建权重和偏置(proj_wproj_b)时用到了它们。权重是* A x B ,其中A是层的大小,B*是目标语言的词汇大小。偏置则仅根据目标词汇的大小传入。

最后,我们从output_project元组中获取权重和偏置 - output_projection = (w, b) - 并使用转置的权重和偏置来构建我们的softmax_loss_function,我们将反复使用它来衡量性能:

接下来的部分是步进函数,如下图所示。前半部分只是错误检查,我们会跳过。最有趣的是使用随机梯度下降法构建输出馈送:

模型的最后部分是get_batch函数,如下图所示。我们将通过内联注释解释各个部分:

当我们运行此程序时,我们可以获得一个完美的训练过程,如下所示:

global step 200 learning rate 0.5000 step-time 0.94 perplexity 
  1625.06
   eval: bucket 0 perplexity 700.69
   eval: bucket 1 perplexity 433.03
   eval: bucket 2 perplexity 401.39
   eval: bucket 3 perplexity 312.34
   ...

或者,我们可能会发现,在损失持续增加后,我们的学习速率有所降低。无论哪种情况,我们都会在开发集上进行测试,直到我们的准确率提高。

总结

在本章中,我们介绍了序列到序列网络,并利用一系列已知的逐句翻译作为训练集编写了一个语言翻译器。我们介绍了作为工作基础的循环神经网络(RNN),并可能在训练过程中跨越了大数据的门槛,因为我们使用了一个 20GB 的训练数据集。

接下来,我们将进入表格数据,并对经济和金融数据进行预测。我们将使用之前工作的一部分,以便我们能够迅速开始,具体来说,就是我们迄今为止编写的初始数据管道工作,用于下载和准备训练数据。然而,我们将重点关注时间序列问题,因此这与我们迄今为止所做的图像和文本工作将有很大的不同。

第六章:寻找意义

到目前为止,我们主要使用 TensorFlow 进行图像处理,在文本序列处理方面的使用较少。在本章中,我们将重新审视书面文字,寻找文本中的意义。这是通常被称为自然语言处理NLP)的一个领域。该领域中的一些活动包括:

  • 情感分析—这从文本中提取出一般的情感类别,而不提取句子的主题或动作

  • 实体提取—这从一段文本中提取出主题,例如人、地点和事件

  • 关键词提取—这从一段文本中提取关键术语

  • 词语关系提取—这不仅提取实体,还提取与每个实体相关的动作和词性

这仅仅是自然语言处理(NLP)的冰山一角——还有其他技术,并且每种技术的复杂度不同。最初,这看起来有些学术,但考虑到仅这四种技术可以实现的功能,举例如下:

  • 阅读新闻并理解新闻的主题(个人、公司、地点等等)

  • 获取前面的新闻并理解情感(高兴、伤心、愤怒等等)

  • 解析产品评论并理解用户对产品的情感(满意、失望等等)

  • 编写一个机器人,以自然语言响应用户的聊天框命令

就像我们之前探讨的机器学习工作一样,设置也需要相当大的努力。在这种情况下,我们将花时间编写脚本,实际从感兴趣的来源抓取文本。

额外的设置

需要额外的设置来包含文本处理所需的库。请查看以下几点:

  1. 第一个是Bazel。在 Ubuntu 上,你需要按照官方教程中的链接来安装 Bazel。docs.bazel.build/versions/master/install-ubuntu.html。在 macOS 上,你可以使用 HomeBrew 来install bazel,如下所示:
      $ brew install bazel
  1. 然后,我们将安装swig,它将允许我们包装 C/C++函数,以便在 Python 中调用。在 Ubuntu 上,你可以使用以下命令安装它:
 $ sudo apt-get install swig

在 Mac OS 上,我们也将按如下方式使用brew安装它:

      $ brew install swig
  1. 接下来,我们将安装协议缓冲支持,它将使我们能够以比 XML 更高效的方式存储和检索序列化数据。我们特别需要版本3.3.0,可以按如下方式安装:
      $ pip install -U protobuf==3.3.0
  1. 我们的文本分类将以树的形式表示,因此我们需要一个库来在命令行上显示树。我们将按如下方式安装:
      $ pip install asciitree
  1. 最后,我们需要一个科学计算库。如果你做过图像分类章节,你已经熟悉这个。如果没有,请按如下方式安装NumPy
      $ pip install numpy autograd 

完成所有这些之后,我们现在将安装SyntaxNet,它为我们的 NLP 工作提供了强大的支持。SyntaxNet 是一个基于 TensorFlow 的开源框架(www.tensorflow.org/),提供了基础功能。谷歌用英语训练了一个 SyntaxNet 模型,并命名为Parsey McParseface,该模型将包含在我们的安装中。我们将能够训练我们自己的、更好或更特定的英语模型,或者在其他语言中进行训练。

训练数据始终是一个挑战,因此我们将从使用预训练的英语模型 Parsey McParseface 开始。

那么,让我们抓取该包并进行配置,命令行如下所示:

$ git clone --recursive https://github.com/tensorflow/models.git
$ cd models/research/syntaxnet/tensorflow
$ ./configure

最后,让我们按如下方式测试系统:

$ cd ..
$ bazel test ...

这将需要一些时间,请耐心等待。如果你严格按照所有指示操作,所有测试都会通过。可能会出现一些如下的错误:

  • 如果你发现bazel无法下载某个包,你可以尝试使用以下命令,并再次运行测试命令:
      $ bazel clean --expunge
  • 如果你遇到一些测试失败,我们建议你将以下内容添加到home目录中的.bazelrc文件,以便获得更多的错误信息用于调试:
 test --test_output=errors

现在,让我们进行一个更常规的测试。我们提供一个英文句子,看看它是如何被解析的:

$ echo 'Faaris likes to feed the kittens.' | bash  
./syntaxnet/demo.sh

我们通过 echo 语句输入一个句子,并将其传递到接受来自控制台标准输入的syntaxnet演示脚本中。请注意,为了让示例更有趣,我将使用一个不常见的名字,比如Faaris。运行这个命令将产生大量的调试信息,显示如下。我省略了堆栈跟踪,使用了省略号(...):

    I syntaxnet/term_frequency_map.cc:101] Loaded 46 terms from 
 syntaxnet/models/parsey_mcparseface/label-map.
    I syntaxnet/embedding_feature_extractor.cc:35] Features: input.digit 
 input.hyphen; input.prefix(length="2") input(1).prefix(length="2") 
 input(2).prefix(length="2") input(3).prefix(length="2") input(-
 1).prefix(length="2")...
    I syntaxnet/embedding_feature_extractor.cc:36] Embedding names: 
 other;prefix2;prefix3;suffix2;suffix3;words
    I syntaxnet/embedding_feature_extractor.cc:37] Embedding dims: 
 8;16;16;16;16;64
    I syntaxnet/term_frequency_map.cc:101] Loaded 46 terms from 
 syntaxnet/models/parsey_mcparseface/label-map.
    I syntaxnet/embedding_feature_extractor.cc:35] Features: 
 stack.child(1).label stack.child(1).sibling(-1).label stack.child(-
 1)....
    I syntaxnet/embedding_feature_extractor.cc:36] Embedding names: 
 labels;tags;words
    I syntaxnet/embedding_feature_extractor.cc:37] Embedding dims: 
 32;32;64
    I syntaxnet/term_frequency_map.cc:101] Loaded 49 terms from 
 syntaxnet/models/parsey_mcparseface/tag-map.
    I syntaxnet/term_frequency_map.cc:101] Loaded 64036 terms from 
 syntaxnet/models/parsey_mcparseface/word-map.
    I syntaxnet/term_frequency_map.cc:101] Loaded 64036 terms from 
 syntaxnet/models/parsey_mcparseface/word-map.
    I syntaxnet/term_frequency_map.cc:101] Loaded 49 terms from 
 syntaxnet/models/parsey_mcparseface/tag-map.
    INFO:tensorflow:Building training network with parameters: 
 feature_sizes: [12 20 20] domain_sizes: [   49    51 64038]
    INFO:tensorflow:Building training network with parameters: 
 feature_sizes: [2 8 8 8 8 8] domain_sizes: [    5 10665 10665  8970  
 8970 64038]
    I syntaxnet/term_frequency_map.cc:101] Loaded 46 terms from 
 syntaxnet/models/parsey_mcparseface/label-map.
    I syntaxnet/embedding_feature_extractor.cc:35] Features: 
 stack.child(1).label stack.child(1).sibling(-1).label stack.child(-
 1)....
    I syntaxnet/embedding_feature_extractor.cc:36] Embedding names: 
 labels;tags;words
    I syntaxnet/embedding_feature_extractor.cc:37] Embedding dims: 
 32;32;64
    I syntaxnet/term_frequency_map.cc:101] Loaded 49 terms from 
 syntaxnet/models/parsey_mcparseface/tag-map.
    I syntaxnet/term_frequency_map.cc:101] Loaded 64036 terms from 
 syntaxnet/models/parsey_mcparseface/word-map.
    I syntaxnet/term_frequency_map.cc:101] Loaded 49 terms from 
 syntaxnet/models/parsey_mcparseface/tag-map.
    I syntaxnet/term_frequency_map.cc:101] Loaded 46 terms from 
 syntaxnet/models/parsey_mcparseface/label-map.
    I syntaxnet/embedding_feature_extractor.cc:35] Features: input.digit 
 input.hyphen; input.prefix(length="2") input(1).prefix(length="2") 
 input(2).prefix(length="2") input(3).prefix(length="2") input(-
 1).prefix(length="2")...
    I syntaxnet/embedding_feature_extractor.cc:36] Embedding names: 
 other;prefix2;prefix3;suffix2;suffix3;words
    I syntaxnet/embedding_feature_extractor.cc:37] Embedding dims: 
 8;16;16;16;16;64
    I syntaxnet/term_frequency_map.cc:101] Loaded 64036 terms from 
 syntaxnet/models/parsey_mcparseface/word-map.
    INFO:tensorflow:Processed 1 documents
    INFO:tensorflow:Total processed documents: 1
    INFO:tensorflow:num correct tokens: 0
    INFO:tensorflow:total tokens: 7
    INFO:tensorflow:Seconds elapsed in evaluation: 0.12, eval metric: 
 0.00%
    INFO:tensorflow:Processed 1 documents
    INFO:tensorflow:Total processed documents: 1
    INFO:tensorflow:num correct tokens: 1
    INFO:tensorflow:total tokens: 6
    INFO:tensorflow:Seconds elapsed in evaluation: 0.47, eval metric: 
 16.67%
    INFO:tensorflow:Read 1 documents
    Input: Faaris likes to feed the kittens .
    Parse:
    likes VBZ ROOT
     +-- Faaris NNP nsubj
     +-- feed VB xcomp
     |   +-- to TO aux
     |   +-- kittens NNS dobj
     |       +-- the DT det
     +-- . . punct

最后一部分,从Input:开始,是最有趣的部分,也是我们在程序化使用这个基础时将消费的输出。注意这个句子是如何被分解为词性和实体-动作-对象的配对?我们看到的一些词性标记包括:nsubjxcompauxdobjdetpunct。其中一些标记是显而易见的,而另一些则不然。如果你喜欢深入研究,我们建议你查阅斯坦福依赖层次结构:nlp-ml.io/jg/software/pac/standep.html

在继续之前,我们再尝试一个句子:

Input: Stop speaking so loudly and be quiet !
Parse:
Stop VB ROOT
+-- speaking VBG xcomp
|   +-- loudly RB advmod
|       +-- so RB advmod
|       +-- and CC cc
|       +-- quiet JJ conj
|           +-- be VB cop
+-- ! . punct

同样,在这里我们会发现模型在解析这个短语时表现得相当好。你也可以尝试自己的一些例子。

接下来,让我们实际训练一个模型。训练 SyntaxNet 相对简单,因为它是一个已编译的系统。到目前为止,我们通过标准输入(STDIO)输入了数据,但我们也可以输入一份文本语料库。记得我们安装的协议缓冲库吗?现在我们将使用它来编辑源文件——syntaxnet/models/parsey_mcparseface/context.pbtxt

此外,我们将根据以下代码段,改变数据源,使用其他训练源或我们自己的数据源:

 input { 
  name: 'wsj-data' 
  record_format: 'conll-sentence' 
  Part { 
    file_pattern: './wsj.conll' 
   } 
 } 
 input { 
  name: 'wsj-data-tagged' 
  record_format: 'conll-sentence' 
  Part { 
    file_pattern: './wsj-tagged.conll' 
   } 
 } 

这就是我们将训练的数据集;然而,做得比原生训练模型 Parsey McParseface 更好将是相当具有挑战性的。所以让我们使用一个有趣的数据集,训练一个新的模型——一个卷积神经网络CNN)来处理文本。

我有点偏爱我的母校,所以我们将使用康奈尔大学计算机科学系编制的电影评论数据。数据集可以在以下网址获取:

www.cs.cornell.edu/people/pabo/movie-review-data/

我们首先会下载并处理电影评论数据集,然后在其上进行训练,最后基于该数据集进行评估。

我们的所有代码都可以在以下链接找到—— github.com/dennybritz/cnn-text-classification-tf

这段代码的灵感来自于 Yoon Kim 关于 CNN 在句子分类中的应用的论文,由谷歌的 Denny Britz 实现并维护。接下来,我们将逐步解析代码,看看 Denny Britz 是如何实现该网络的。

我们从图 1 开始,使用常见的帮助程序。这里唯一的新成员是数据帮助程序,它会下载并准备这个特定的数据集,如下图所示:

我们开始定义参数。现在,训练参数应该非常熟悉——它们定义了每次遍历时处理的批次大小以及我们将进行的训练轮数或完整运行次数。我们还会定义评估进度的频率(这里是每 100 步)以及保存模型检查点的频率(以便评估和重新继续训练)。接下来,在图 2 中,我们有加载和准备数据集的代码,如下所示:

然后,我们将查看代码中的训练部分:

图 3 展示了我们实例化我们的 CNN——一个自然语言 CNN,并使用之前定义的部分参数。我们还设置了启用 TensorBoard 可视化的代码。

图 4 展示了我们为 TensorBoard 捕获的更多项——训练集和评估集的损失和准确率:

接下来,在图 5 中,我们将定义训练和评估方法,这些方法与我们在图像处理中的方法非常相似。我们将接收一组训练数据和标签,并将其存储在字典中。然后,我们将在数据字典上运行 TensorFlow 会话,捕获返回的性能指标。

我们将在顶部设置方法,然后按批次遍历训练数据,对每批数据应用训练和评估方法。

在特定的间隔,我们还会保存检查点,以便进行可选的评估:

我们可以运行这个,并且在仅使用 CPU 的机器上经过一小时的训练后,得到一个训练好的模型。训练好的模型将被存储为一个检查点文件,然后可以输入到图 6 所示的评估程序中:

评估程序只是一个使用示例,但我们还是来了解一下它。我们将从典型的导入和参数设置开始。在这里,我们还将检查点目录作为输入,并加载一些测试数据;不过,您应该使用自己的数据。

接下来,让我们查看以下图形:

我们将从检查点文件开始,只需加载它并从中重新创建一个 TensorFlow 会话。这使我们能够针对刚刚训练的模型进行评估,并反复使用它。

接下来,我们将批量运行测试数据。在常规使用中,我们不会使用循环或批次,但由于我们有一个相当大的测试数据集,因此我们将以循环方式进行处理。

我们将简单地对每一组测试数据运行会话,并保存返回的预测结果(负面与正面)。以下是一些示例正面评论数据:

 insomnia loses points when it surrenders to a formulaic bang-bang , 
 shoot-em-up scene at the conclusion . but the performances of pacino 
 , williams , and swank keep the viewer wide-awake all the way through 
 .
    what might have been readily dismissed as the tiresome rant of an 
 aging filmmaker still thumbing his nose at convention takes a 
 surprising , subtle turn at the midway point .
    at a time when commercialism has squeezed the life out of whatever 
 idealism american moviemaking ever had , godfrey reggio's career 
 shines like a lonely beacon .
    an inuit masterpiece that will give you goosebumps as its uncanny 
 tale of love , communal discord , and justice unfolds .
    this is popcorn movie fun with equal doses of action , cheese , ham 
 and cheek ( as well as a serious debt to the road warrior ) , but it 
 feels like unrealized potential
    it's a testament to de niro and director michael caton-jones that by 
 movie's end , we accept the characters and the film , flaws and all .
    performances are potent , and the women's stories are ably intercut 
 and involving .
    an enormously entertaining movie , like nothing we've ever seen 
 before , and yet completely familiar .
    lan yu is a genuine love story , full of traditional layers of 
 awakening and ripening and separation and recovery .
    your children will be occupied for 72 minutes .
    pull[s] off the rare trick of recreating not only the look of a 
 certain era , but also the feel .
    twohy's a good yarn-spinner , and ultimately the story compels .
    'tobey maguire is a poster boy for the geek generation . '
     . . . a sweetly affecting story about four sisters who are coping , 
 in one way or another , with life's endgame .
    passion , melodrama , sorrow , laugther , and tears cascade over the 
 screen effortlessly . . .
    road to perdition does display greatness , and it's worth seeing . 
 but it also comes with the laziness and arrogance of a thing that 
 already knows it's won .

类似地,我们有负面数据。它们都位于 data 文件夹中,分别为 rt-polarity.posrt-polarity.neg

这是我们使用的网络架构:

它与我们用于图像的架构非常相似。事实上,整个过程看起来非常相似,实际上也是如此。这些技术的美妙之处在于它们的广泛适用性。

让我们先查看训练输出,其结果如下:

$ ./train.py
...
2017-06-15T04:42:08.793884: step 30101, loss 0, acc 1
2017-06-15T04:42:08.934489: step 30102, loss 1.54599e-07, acc 1
2017-06-15T04:42:09.082239: step 30103, loss 3.53902e-08, acc 1
2017-06-15T04:42:09.225435: step 30104, loss 0, acc 1
2017-06-15T04:42:09.369348: step 30105, loss 2.04891e-08, acc 1
2017-06-15T04:42:09.520073: step 30106, loss 0.0386909, acc 
0.984375
2017-06-15T04:42:09.676975: step 30107, loss 8.00917e-07, acc 1
2017-06-15T04:42:09.821703: step 30108, loss 7.83049e-06, acc 1
...
2017-06-15T04:42:23.220202: step 30199, loss 1.49012e-08, acc 1
2017-06-15T04:42:23.366740: step 30200, loss 5.67226e-05, acc 1

Evaluation:
2017-06-15T04:42:23.781196: step 30200, loss 9.74802, acc 0.721
...
Saved model checkpoint to /Users/saif/Documents/BOOK/cnn-text-
classification-tf/runs/1465950150/checkpoints/model-30200

现在让我们看一下评估步骤:

$ ./eval.py –eval_train --checkpoint_dir==./runs/1465950150/checkpoints/

Parameters:
ALLOW_SOFT_PLACEMENT=True
BATCH_SIZE=64
CHECKPOINT_DIR=/Users/saif/Documents/BOOK/cnn-text-classification-
tf/runs/1465950150/checkpoints/
LOG_DEVICE_PLACEMENT=False

Loading data...
Vocabulary size: 18765
Test set size 10662
Evaluating...
Total number of test examples: 10662
Accuracy: 0.973832 

在我们所拥有的数据集上,这个准确度相当不错。下一步将是将训练好的模型应用于常规使用。可能有一些有趣的实验,可以从其他来源获取电影评论数据,比如 IMDB 或 Amazon。由于数据未必已经标记,我们可以使用正面百分比作为网站之间的一般一致性指标。

然后我们可以将模型应用于实际场景。假设你是一个产品制造商,你可以实时追踪来自各种来源的所有评论,并筛选出高度负面的评论。然后,你的现场代表可以尝试解决这些问题。可能性是无限的,因此我们提出了一个有趣的项目,结合了我们学到的两项内容。

编写一个 Twitter 流读取器,获取每条推文并提取推文的主题。对于特定的一组主题,比如公司,评估推文是积极的还是消极的。创建积极和消极百分比的运行指标,评估不同时间尺度下的主题。

学到的技能

在本章中,您应该已经学会了以下技能:

  • 设置更高级的 TensorFlow 库,包括那些需要 Bazel 驱动编译的库

  • 处理文本数据

  • 将 RNN 和 CNN 应用于文本而非图像

  • 使用保存的模型评估文本

  • 使用预构建的库来提取句子结构细节

  • 基于正面和负面情感将文本分类

摘要

太棒了!我们刚刚将对神经网络的理解应用到文本中,以便理解语言。这是一个了不起的成就,因为完全自动化可以实现大规模处理。即使某些评估不完全正确,从统计学角度来看,我们依然手握一项强大的工具,这同样是使用相同的构建模块构建的。

第七章:利用机器学习赚钱

到目前为止,我们主要使用 TensorFlow 进行图像处理,并在较小程度上进行文本序列处理。在本章中,我们将处理一种特定类型的表格数据:时间序列数据。

时间序列数据来自许多领域,通常有一个共同点——唯一不断变化的字段是时间或序列字段。这在许多领域中都很常见,尤其是在经济学、金融、健康、医学、环境工程和控制工程中。我们将在本章中通过例子来深入探讨,但关键点是要记住顺序很重要。与前几章我们可以自由打乱数据不同,时间序列数据如果被随意打乱就会失去意义。一个额外的复杂性是数据本身的可获取性;如果我们拥有的数据仅限于当前时刻且没有进一步的历史数据可供获取,那么再多的数据收集也无法生成更多数据——你只能受到基于时间的可用性限制。

幸运的是,我们将深入探讨一个数据量庞大的领域:金融世界。我们将探索一些对冲基金和其他复杂投资者可能使用时间序列数据的方式。

本章将涵盖以下主题:

  • 什么是时间序列数据及其特殊属性

  • 投资公司在其定量和机器学习驱动的投资努力中可能使用的输入类型和方法

  • 金融时间序列数据及其获取方式;我们还将获取一些实时金融数据

  • 修改后的卷积神经网络在金融中的应用

输入和方法

投资公司的内部专有交易团队采用多种手段进行投资、交易和赚钱。相对不受监管的对冲基金使用更加广泛、更有趣和更复杂的投资手段。有些投资基于直觉或大量思考,另一些则主要基于过滤、算法或信号。两种方法都可以,但我们当然会关注后一种方法。

在定量方法中,有许多技术;其中一些如下:

  • 基于估值

  • 基于异常和信号

  • 基于外部信号

  • 基于过滤和分段的队列分析

其中一些方法将使用传统的机器学习技术,如 K-近邻算法、朴素贝叶斯和支持向量机。特别是队列分析,几乎非常适合 KNN 类型的方法。

另一个流行的技术是情感分析和基于群众情绪的信号。我们在上一章中讨论了这一点,当时我们通过分析文本情感,并将段落分类为基本类别:正面、负面、开心、生气等等。想象一下,如果我们收集了更多的数据,并过滤出所有涉及特定股票的数据,我们就能得到股票的情绪倾向。现在,想象一下如果我们拥有一种广泛的(可能是全球性的)、大流量且高速度的文本来源——其实你不需要想象,这一切在过去十年已经成为现实。Twitter 通过 API 提供他们的firehose数据,Facebook 也是,其他一些社交媒体平台也同样如此。事实上,一些对冲基金会消耗整个 Twitter 和 Facebook 的数据流,并试图从中提取关于股票、市场板块、商品等的公众情绪。然而,这是一种外部的基于自然语言处理(NLP)信号的投资策略,实践者们用它来预测时间序列的方向性和/或强度。

在本章中,我们将使用内部指标,利用时间序列本身来预测时间序列中的未来数据。预测实际的未来数据实际上是一项非常困难的任务,结果发现,这并不是完全必要的。通常,只需一个方向性的观点就足够了。方向性的观点与运动的强度结合起来会更好。

对于许多类型的投资,甚至连观点本身可能也不能给你完全的保证,平均来看,做得比错得稍多一些就足够了。想象一下每次投掷硬币下注一分钱——如果你能 51%的时间猜对,并且能够玩成千上万次,这可能足以让你盈利,因为你赚得比亏得多。

这一切对基于机器学习的努力来说是一个好兆头,虽然我们可能对我们的答案没有 100%的信心,但从统计上来看,我们可能具有很强的预测能力。归根结底,我们希望领先一步,因为即使是微小的优势,也能通过成千上万次的循环被放大,从而带来可观的利润。

获取数据

让我们首先获取一些数据。为了本章的目的,我们将使用 Quandl 的数据,Quandl 一直是技术精通的独立投资者的长期最爱。Quandl 通过多种机制提供许多股票的数据。一个简单的机制是通过 URL API。要获取例如 Google 股票的数据,我们可以点击www.quandl.com/api/v3/datasets/WIKI/GOOG/data.json。类似地,我们可以将GOOG替换为其他指数代码,以获取其他股票的数据。

通过 Python 自动化这个过程是相当容易的;我们将使用以下代码来实现:

import requests 

API_KEY = '<your_api_key>' 

start_date = '2010-01-01' 
end_date = '2015-01-01' 
order = 'asc' 
column_index = 4 

stock_exchange = 'WIKI' 
index = 'GOOG' 

data_specs = 'start_date={}&end_date={}&order={}&column_index={}&api_key={}' 
   .format(start_date, end_date, order, column_index, API_KEY) 
base_url = "https://www.quandl.com/api/v3/datasets/{}/{}/data.json?" + data_specs 
stock_data = requests.get(base_url.format(stock_exchange, index)).json()

因此,在这里,stock_data变量中将包含从 WIKI/GOOG 获取的股票数据,数据来源于格式化的 URL,日期范围为2010-01-012015-01-01column_index = 4变量告诉服务器仅获取历史数据中的收盘值。

请注意,你可以在你的 GitHub 仓库中找到本章的代码—(github.com/saifrahmed/MLwithTF/tree/master/book_code/chapter_07)。

那么,什么是这些收盘值呢?股票价格每天都会波动。它们以某个特定值开盘,在一天内达到一定的最高值和最低值,最终在一天结束时以某个特定值收盘。下图展示了股票价格在一天内的变化:

所以,在股票开盘后,你可以投资它们并购买股票。到一天结束时,你将根据所买股票的收盘值获得利润或亏损。投资者使用不同的技术来预测哪些股票在特定的日子里有上涨潜力,并根据他们的分析进行投资。

接近问题

在本章中,我们将研究股票价格是否会根据其他时区市场的涨跌而涨跌(这些市场的收盘时间比我们想投资的股票早)。我们将分析来自欧洲市场的数据,这些市场的收盘时间比美国股市早大约 3 或 4 小时。我们将从 Quandl 获取以下欧洲市场的数据:

  • WSE/OPONEO_PL

  • WSE/VINDEXUS

  • WSE/WAWEL

  • WSE/WIELTON

我们将预测接下来美国市场的收盘涨跌:WIKI/SNPS。

我们将下载所有市场数据,查看下载的市场收盘值图表,并修改数据以便可以在我们的网络上进行训练。然后,我们将看到我们的网络在假设下的表现。

本章中使用的代码和分析技术灵感来源于 Google Cloud Datalab 笔记本,地址为github.com/googledatalab/notebooks/blob/master/samples/TensorFlow/Machine%20Learning%20with%20Financial%20Data.ipynbhere

步骤如下:

  1. 下载所需数据并进行修改。

  2. 查看原始数据和修改后的数据。

  3. 从修改后的数据中提取特征。

  4. 准备训练并测试网络。

  5. 构建网络。

  6. 训练。

  7. 测试。

下载和修改数据

在这里,我们将从codes变量中提到的来源下载数据,并将其放入我们的closings数据框中。我们将存储原始数据、scaled数据和log_return

codes = ["WSE/OPONEO_PL", "WSE/VINDEXUS", "WSE/WAWEL", "WSE/WIELTON", "WIKI/SNPS"] 
closings = pd.DataFrame() 
for code in codes: 
    code_splits = code.split("/") 
    stock_exchange = code_splits[0] 
    index = code_splits[1] 
    stock_data = requests.get(base_url.format(stock_exchange,  
    index)).json() 
    dataset_data = stock_data['dataset_data'] 
    data = np.array(dataset_data['data']) 
    closings[index] = pd.Series(data[:, 1].astype(float)) 
    closings[index + "_scaled"] = closings[index] / 
     max(closings[index]) 
    closings[index + "_log_return"] = np.log(closings[index] / closings[index].shift()) 
closings = closings.fillna(method='ffill')  # Fill the gaps in data 

我们将数据缩放,使得股票值保持在01之间;这有助于与其他股票值进行最小化比较。它将帮助我们看到股票相对于其他市场的趋势,并使得视觉分析更为简便。

对数回报帮助我们获取市场涨跌图,相较于前一天的数据。

现在,让我们看看我们的数据长什么样。

查看数据

以下代码片段将绘制我们下载并处理的数据:

def show_plot(key="", show=True): 
    fig = plt.figure() 
    fig.set_figwidth(20) 
    fig.set_figheight(15) 
    for code in codes: 
        index = code.split("/")[1] 
        if key and len(key) > 0: 
            label = "{}_{}".format(index, key) 
        else: 
            label = index 
        _ = plt.plot(closings[label], label=label) 

    _ = plt.legend(loc='upper right') 
    if show: 
        plt.show() 

show = True 
show_plot("", show=show) 
show_plot("scaled", show=show) 
show_plot("log_return", show=show) 

原始市场数据转换为收盘值。正如你在这里看到的,WAWEL的值比其他市场大几个数量级:

WAWEL 的收盘值在视觉上减少了其他市场数据的趋势。我们将缩放这些数据,这样我们可以更清楚地看到。请看以下截图:

缩放后的市场值帮助我们更好地可视化趋势。现在,让我们看看log_return的样子:

对数回报是市场的收盘值

提取特征

现在,我们将提取所需的特征来训练和测试我们的数据:

feature_columns = ['SNPS_log_return_positive', 'SNPS_log_return_negative'] 
for i in range(len(codes)): 
    index = codes[i].split("/")[1] 
    feature_columns.extend([ 
        '{}_log_return_1'.format(index), 
        '{}_log_return_2'.format(index), 
        '{}_log_return_3'.format(index) 
    ]) 
features_and_labels = pd.DataFrame(columns=feature_columns) 
closings['SNPS_log_return_positive'] = 0 
closings.ix[closings['SNPS_log_return'] >= 0, 'SNPS_log_return_positive'] = 1 
closings['SNPS_log_return_negative'] = 0 
closings.ix[closings['SNPS_log_return'] < 0, 'SNPS_log_return_negative'] = 1 
for i in range(7, len(closings)): 
    feed_dict = {'SNPS_log_return_positive': closings['SNPS_log_return_positive'].ix[i], 
        'SNPS_log_return_negative': closings['SNPS_log_return_negative'].ix[i]} 
    for j in range(len(codes)): 
        index = codes[j].split("/")[1] 
        k = 1 if j == len(codes) - 1 else 0 
        feed_dict.update({'{}_log_return_1'.format(index): closings['{}_log_return'.format(index)].ix[i - k], 
                '{}_log_return_2'.format(index): closings['{}_log_return'.format(index)].ix[i - 1 - k], 
                '{}_log_return_3'.format(index): closings['{}_log_return'.format(index)].ix[i - 2 - k]}) 
    features_and_labels = features_and_labels.append(feed_dict, ignore_index=True) 

我们将所有特征和标签存储在features_and_label变量中。SNPS_log_return_positiveSNPS_log_return_negative键分别存储 SNPS 的对数回报为正和负的点。如果为真,则为1,如果为假,则为0。这两个键将作为网络的标签。

其他键用于存储过去三天的其他市场值(对于 SNPS,由于今天的值无法获取,我们还需要存储前 3 天的数据)。

为训练和测试做准备

现在,我们将把特征分成traintest子集。我们不会随机化我们的数据,因为在金融市场的时间序列中,数据是以规律的方式每天提供的,我们必须按原样使用它。你不能通过训练未来的数据来预测过去的行为,因为那样毫无意义。我们总是对股票市场的未来行为感兴趣:

features = features_and_labels[features_and_labels.columns[2:]] 
labels = features_and_labels[features_and_labels.columns[:2]] 
train_size = int(len(features_and_labels) * train_test_split) 
test_size = len(features_and_labels) - train_size 
train_features = features[:train_size] 
train_labels = labels[:train_size] 
test_features = features[train_size:] 
test_labels = labels[train_size:]

构建网络

用于训练我们时间序列的网络模型如下所示:

sess = tf.Session() 
num_predictors = len(train_features.columns) 
num_classes = len(train_labels.columns) 
feature_data = tf.placeholder("float", [None, num_predictors]) 
actual_classes = tf.placeholder("float", [None, 2]) 
weights1 = tf.Variable(tf.truncated_normal([len(codes) * 3, 50], stddev=0.0001)) 
biases1 = tf.Variable(tf.ones([50])) 
weights2 = tf.Variable(tf.truncated_normal([50, 25], stddev=0.0001)) 
biases2 = tf.Variable(tf.ones([25])) 
weights3 = tf.Variable(tf.truncated_normal([25, 2], stddev=0.0001)) 
biases3 = tf.Variable(tf.ones([2])) 
hidden_layer_1 = tf.nn.relu(tf.matmul(feature_data, weights1) + biases1) 
hidden_layer_2 = tf.nn.relu(tf.matmul(hidden_layer_1, weights2) + biases2) 
model = tf.nn.softmax(tf.matmul(hidden_layer_2, weights3) + biases3) 
cost = -tf.reduce_sum(actual_classes * tf.log(model)) 
train_op1 = tf.train.AdamOptimizer(learning_rate=0.0001).minimize(cost) 
init = tf.initialize_all_variables() 
sess.run(init) 
correct_prediction = tf.equal(tf.argmax(model, 1), tf.argmax(actual_classes, 1)) 
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float")) 

这只是一个简单的网络,包含两个隐藏层。

训练

现在,让我们来训练我们的网络:

for i in range(1, 30001): 
    sess.run(train_op1, feed_dict={feature_data: train_features.values, 
            actual_classes: train_labels.values.reshape(len(train_labels.values), 2)}) 
    if i % 5000 == 0: 
        print(i, sess.run(accuracy, feed_dict={feature_data: train_features.values, 
                actual_classes: train_labels.values.reshape(len(train_labels.values), 2)})) 

测试

我们的网络测试结果如下所示:

feed_dict = { 
    feature_data: test_features.values, 
    actual_classes: test_labels.values.reshape(len(test_labels.values), 2) 
} 
tf_confusion_metrics(model, actual_classes, sess, feed_dict) 

进一步探讨

假设你刚刚训练了一个优秀的分类器,展示了对市场的某些预测能力,那么你应该开始交易吗?就像我们迄今为止做的其他机器学习项目一样,你需要在独立的测试集上进行测试。在过去,我们通常会将数据分成以下三个集合:

  • 训练集

  • 开发集,也叫验证集

  • 测试集

我们可以做类似于当前工作的事情,但金融市场为我们提供了一个额外的资源——持续的数据流!

我们可以使用早期获取的数据源继续拉取更多的数据;从本质上来说,我们拥有一个不断扩展、看不见的数据集!当然,这也取决于我们使用的数据频率——如果我们使用的是日数据,那么这需要一段时间才能完成。如果使用的是小时数据或每分钟数据,就会更容易,因为我们会更快地获得更多的数据。基于报价量的逐笔数据通常会更好。

由于可能涉及真实的资金,大多数人通常会进行模拟交易——基本上是几乎实时地运行系统,但实际上并不花费任何资金,而只是跟踪系统在真实环境下的表现。如果这一方法有效,下一步将是进行真实交易,也就是说,使用真实的资金(通常是小额资金用于测试系统)。

个人的实际考虑

假设你训练了一个不错的分类器,并且在盲测或实时数据集上也展示了良好的结果,那么现在应该开始交易吗?虽然理论上是可能的,但并不那么简单。以下是一些原因:

  • 历史数据分析与实时数据流:历史数据通常是经过清洗的,接近完美,但实时数据则没有这样的优势。你将需要编写代码来评估数据流并剔除可能不可靠的数据。

  • 买卖价差:这是新手面临的最大惊讶。市场上实际上有两个价格:一个是你可以买入的价格,另一个是你可以卖出的价格。你并不是在看到的典型市场价格下同时进行买卖(那只是买卖双方的最后碰撞点,称为最后成交价)。买入持仓后立即卖出会因为这个差价而亏损,所以从净利润来看,你已经处于亏损状态。

  • 交易成本:这可以低至每笔交易 1 美元,但它仍然是一个障碍,需要在策略能盈利之前克服。

  • 税务问题:这通常被忽视,可能是因为税务意味着净收益,而这通常是一件好事。

  • 退出能力:仅仅因为理论上可以卖出,并不意味着实际上有市场可以买入你的持仓,而且即便有市场,也可能无法一次性卖出全部持仓。猜猜看?还需要更多的编码。这一次,你需要查看买盘价格、这些价格的成交量以及订单簿的深度。

  • 成交量和流动性:仅仅因为信号告诉你买入,并不意味着市场上有足够的成交量可供购买;你可能只看到订单簿顶部的价格,而底下实际的成交量很少。更多的编码仍然是必要的!

  • 与交易 API 的集成:调用库很简单,但涉及资金时就不那么容易了。你需要交易协议、API 协议等。然而,成千上万的个人已经做过了,Interactive Brokers 是寻求 API 进行买卖持仓的最受欢迎的经纪商。方便的是,他们也提供一个 API 来提供市场数据。

学到的技能

在本章中,你应该已经学习了以下技能:

  • 理解时间序列数据

  • 为时间序列数据设置管道

  • 集成原始数据

  • 创建训练集和测试集

  • 实际考虑因素

总结

对金融数据的机器学习与我们使用的其他数据并无不同,事实上,我们使用的网络与处理其他数据集时相同。我们还有其他可用的选项,但总体方法保持不变。特别是在进行资金交易时,我们会发现,周围的代码相对于实际的机器学习代码部分会变得更大。

在下一章,我们将探讨如何将机器学习应用于医学领域。

第八章:现在医生来接诊

到目前为止,我们已经使用深度网络处理了图像、文本和时间序列数据。虽然大多数示例都很有趣且相关,但它们并不具备企业级的标准。现在,我们将挑战一个企业级问题——医学诊断。我们之所以称之为企业级问题,是因为医学数据具有一些在大型企业外部不常见的属性,比如专有数据格式、大规模原生数据、不方便的类别数据和非典型特征。

本章将涵盖以下主题:

  • 医学影像文件及其特性

  • 处理大规模图像文件

  • 从典型医学文件中提取类别数据

  • 使用“预训练”的网络进行非医学数据的应用

  • 扩展训练以适应医学数据的规模

获取医学数据本身就是一项挑战,因此我们将依赖于一个受欢迎的网站,所有读者都应该熟悉——Kaggle。虽然有许多医学数据集可以免费访问,但大多数都需要繁琐的注册过程才能访问它们。许多数据集仅在医学影像处理领域的特定子社区中公开,而且大多数都需要特定的提交流程。Kaggle 可能是最规范化的医学影像数据集来源,同时也有非医学数据集供你尝试。我们将特别关注 Kaggle 的糖尿病视网膜病变检测挑战。

数据集包含训练集和盲测集。训练集用于训练我们的网络,测试集则用于在 Kaggle 网站上提交我们的网络结果。

由于数据量相当大(训练集为 32GB,测试集为 49GB),它们都被划分成多个约 8GB 的 ZIP 文件。

这里的测试集是盲测集——我们不知道它们的标签。这是为了确保我们的网络训练结果在提交测试集时具有公平性。

就训练集而言,其标签存储在trainLabels.csv文件中。

挑战

在我们深入代码之前,请记住,大多数机器学习的工作都有两个简单的目标之一——分类或排序。在许多情况下,分类本身也是一种排序,因为我们最终选择排名最高的分类(通常是概率)。我们在医学影像方面的探索也不例外——我们将把图像分类为以下两个二元类别之一:

  • 疾病状态/阳性

  • 正常状态/阴性

或者,我们将把它们分类为多个类别,或者对它们进行排序。在糖尿病视网膜病变的情况下,我们将按以下方式对其进行排名:

  • 类别 0: 无糖尿病视网膜病变

  • 类别 1: 轻度

  • 类别 2: 中度

  • 类别 3: 严重

  • 类别 4: 广泛的糖尿病视网膜病变

通常,这被称为评分。Kaggle 友好地为参与者提供了超过 32 GB 的训练数据,其中包含超过 35,000 张图片。测试数据集甚至更大——达到了 49 GB。目标是使用已知评分对这 35,000+ 张图像进行训练,并为测试集提出评分。训练标签看起来是这样的:

图像 级别
10_left 0
10_right 0
13_left 0
13_right 0
15_left 1
15_right 2
16_left 4
16_right 4
17_left 0
17_right 1

这里有些背景——糖尿病视网膜病变是一种眼内视网膜的疾病,因此我们有左眼和右眼的评分。我们可以将它们视为独立的训练数据,或者我们可以稍后发挥创意,将它们考虑在一个更大的单一患者的背景下。让我们从简单的开始并逐步迭代。

到现在为止,你可能已经熟悉了将一组数据划分为训练集、验证集和测试集的过程。这对我们使用过的一些标准数据集来说效果不错,但这个数据集是一个竞赛的一部分,并且是公开审计的,所以我们不知道答案!这很好地反映了现实生活。有一个小问题——大多数 Kaggle 竞赛允许你提出答案并告诉你你的总评分,这有助于学习和设定方向。它也有助于他们和社区了解哪些用户表现良好。

由于测试标签是盲测的,我们需要改变之前做过的两件事:

  • 我们将需要一个用于内部开发和迭代的流程(我们可能会将训练集分成训练集、验证集和测试集)。我们将需要另一个用于外部测试的流程(我们可能会找到一个有效的设置,运行它在盲测集上,或者首先重新训练整个训练集)。

  • 我们需要以非常特定的格式提交正式提案,将其提交给独立审计员(在此案例中为 Kaggle),并根据进展情况进行评估。以下是一个示例提交的样式:

图像 级别
44342_left 0
44342_right 1
44344_left 2
44344_right 2
44345_left 0
44345_right 0
44346_left 4
44346_right 3
44350_left 1
44350_right 1
44351_left 4
44351_right 4

不出所料,它看起来和训练标签文件非常相似。你可以在这里提交你的内容:

www.kaggle.com/c/diabetic-retinopathy-detection/submithttps://www.kaggle.com/c/diabetic-retinopathy-detection/submit

你需要登录才能打开上述链接。

数据

让我们开始看看数据。打开一些示例文件并准备好迎接惊讶——这些既不是 28x28 的手写字块,也不是带有猫脸的 64x64 图标。这是一个来自现实世界的真实数据集。事实上,图像的大小甚至不一致。欢迎来到现实世界。

你会发现图像的大小从每边 2,000 像素到接近 5,000 像素不等!这引出了我们第一个实际任务——创建一个训练管道。管道将是一组步骤,抽象掉生活中的艰难现实,并生成一组干净、一致的数据。

管道

我们将智能地进行处理。Google 使用其 TensorFlow 库中的不同网络制作了许多管道模型结构。我们要做的是从这些模型结构和网络中选择一个,并根据我们的需求修改代码。

这很好,因为我们不会浪费时间从零开始构建管道,也不必担心集成 TensorBoard 可视化工具,因为它已经包含在 Google 的管道模型中。

我们将从这里使用一个管道模型:

github.com/tensorflow/models/

如你所见,这个仓库中有许多由 TensorFlow 制作的不同模型。你可以深入了解一些与自然语言处理(NLP)、递归神经网络及其他主题相关的模型。如果你想理解复杂的模型,这是一个非常好的起点。

对于本章,我们将使用Tensorflow-Slim 图像分类模型库。你可以在这里找到这个库:

github.com/tensorflow/models/tree/master/research/slim

网站上已经有很多详细信息,解释了如何使用这个库。他们还告诉你如何在分布式环境中使用该库,并且如何利用多 GPU 来加速训练时间,甚至部署到生产环境中。

使用这个的最佳之处在于,他们提供了预训练的模型快照,你可以利用它显著减少训练网络的时间。因此,即使你有较慢的 GPU,也不必花费数周的时间来训练这么大的网络,便可达到一个合理的训练水平。

这叫做模型的微调,你只需要提供一个不同的数据集,并告诉网络重新初始化网络的最终层以便重新训练它们。此外,你还需要告诉它数据集中有多少个输出标签类。在我们的案例中,有五个独特的类别,用于识别不同等级的糖尿病视网膜病变DR)。

预训练的快照可以在这里找到:

github.com/tensorflow/models/tree/master/research/slim#Pretrained

如你在上面的链接中所见,他们提供了许多可以利用的预训练模型。他们使用了ImageNet数据集来训练这些模型。ImageNet是一个标准数据集,包含 1,000 个类别,数据集大小接近 500 GB。你可以在这里了解更多:

image-net.org/

理解管道

首先,开始将models仓库克隆到你的计算机中:

git clone https://github.com/tensorflow/models/

现在,让我们深入了解从 Google 模型仓库中获得的管道。

如果你查看仓库中这个路径前缀(models/research/slim)的文件夹,你会看到名为datasetsdeploymentnetspreprocessingscripts的文件夹;这些文件涉及生成模型、训练和测试管道,以及与训练ImageNet数据集相关的文件,还有一个名为flowers的数据集

我们将使用download_and_convert_data.py来构建我们的 DR 数据集。这个图像分类模型库是基于slim库构建的。在这一章中,我们将微调在nets/inception_v3.py中定义的 Inception 网络(稍后我们会详细讨论网络的规格和概念),其中包括计算损失函数、添加不同的操作、构建网络等内容。最后,train_image_classifier.pyeval_image_classifier.py文件包含了为我们的网络创建训练和测试管道的通用程序。

对于这一章,由于网络的复杂性,我们使用基于 GPU 的管道来训练网络。如果你想了解如何在你的机器上安装适用于 GPU 的 TensorFlow,请参考本书中的附录 A,高级安装部分。此外,你的机器中应该有大约120 GB的空间才能运行此代码。你可以在本书代码文件的Chapter 8文件夹中找到最终的代码文件。

准备数据集

现在,让我们开始准备网络的数据集。

对于这个 Inception 网络,我们将使用TFRecord类来管理我们的数据集。经过预处理后的输出数据集文件将是 proto 文件,TFRecord可以读取这些文件,它只是以序列化格式存储的我们的数据,以便更快的读取速度。每个 proto 文件内包含一些信息,如图像的大小和格式。

我们这样做的原因是,数据集的大小太大,我们不能将整个数据集加载到内存(RAM)中,因为它会占用大量空间。因此,为了高效使用内存,我们必须分批加载图像,并删除当前没有使用的已经加载的图像。

网络将接收的输入大小是 299x299。因此,我们将找到一种方法,首先将图像大小缩小到 299x299,以便得到一致的图像数据集。

在减少图像大小后,我们将制作 proto 文件,稍后可以将这些文件输入到我们的网络中,网络将对我们的数据集进行训练。

你需要首先从这里下载五个训练 ZIP 文件和标签文件:

www.kaggle.com/c/diabetic-retinopathy-detection/data

不幸的是,Kaggle 仅允许通过账户下载训练的 ZIP 文件,因此无法像之前章节那样自动化下载数据集文件的过程。

现在,假设你已经下载了所有五个训练 ZIP 文件和标签文件,并将它们存储在名为 diabetic 的文件夹中。diabetic 文件夹的结构将如下所示:

  • diabetic

    • train.zip.001

    • train.zip.002

    • train.zip.003

    • train.zip.004

    • train.zip.005

    • trainLabels.csv.zip

为了简化项目,我们将手动使用压缩软件进行解压。解压完成后,diabetic 文件夹的结构将如下所示:

  • diabetic

    • train

    • 10_left.jpeg

    • 10_right.jpeg

    • ...

    • trainLabels.csv

    • train.zip.001

    • train.zip.002

    • train.zip.003

    • train.zip.004

    • train.zip.005

    • trainLabels.csv.zip

在这种情况下,train 文件夹包含所有 .zip 文件中的图像,而 trainLabels.csv 文件包含每张图像的真实标签。

模型库的作者提供了一些示例代码,用于处理一些流行的图像分类数据集。我们的糖尿病问题也可以用相同的方法来解决。因此,我们可以遵循处理其他数据集(如 flowerMNIST 数据集)的代码。我们已经在本书的 github.com/mlwithtf/mlwithtf/ 库中提供了修改代码,便于处理糖尿病数据集。

你需要克隆仓库并导航到 chapter_08 文件夹。你可以按照以下方式运行 download_and_convert_data.py 文件:

python download_and_convert_data.py --dataset_name diabetic --dataset_dir D:\\datasets\\diabetic

在这种情况下,我们将使用 dataset_namediabetic,而 dataset_dir 是包含 trainLabels.csvtrain 文件夹的文件夹。

它应该能够顺利运行,开始将数据集预处理成适合的 (299x299) 格式,并在新创建的 tfrecords 文件夹中生成一些 TFRecord 文件。下图展示了 tfrecords 文件夹的内容:

解释数据准备过程

现在,让我们开始编写数据预处理的代码。从现在开始,我们将展示我们对 tensorflow/models 原始库所做的修改。基本上,我们将处理 flowers 数据集的代码作为起点,并对其进行修改以满足我们的需求。

download_and_convert_data.py 文件中,我们在文件开头添加了一行新的代码:

from datasets import download_and_convert_diabetic 
and a new else-if clause to process the dataset_name "diabetic" at line 69: 
  elif FLAGS.dataset_name == 'diabetic': 
      download_and_convert_diabetic.run(FLAGS.dataset_dir)

使用这段代码,我们可以调用 datasets 文件夹中的 download_and_convert_diabetic.py 文件中的 run 方法。这是一种非常简单的方法,用于分离多个数据集的预处理代码,但我们仍然可以利用 image classification 库的其他部分。

download_and_convert_diabetic.py 文件是对 download_and_convert_flowers.py 文件的复制,并对其进行了修改,以准备我们的糖尿病数据集。

download_and_convert_diabetic.py 文件的 run 方法中,我们做了如下更改:

  def run(dataset_dir): 
    """Runs the download and conversion operation. 

    Args: 
      dataset_dir: The dataset directory where the dataset is stored. 
    """ 
    if not tf.gfile.Exists(dataset_dir): 
        tf.gfile.MakeDirs(dataset_dir) 

    if _dataset_exists(dataset_dir): 
        print('Dataset files already exist. Exiting without re-creating   
        them.') 
        return 

    # Pre-processing the images. 
    data_utils.prepare_dr_dataset(dataset_dir) 
    training_filenames, validation_filenames, class_names =   
    _get_filenames_and_classes(dataset_dir) 
    class_names_to_ids = dict(zip(class_names,    
    range(len(class_names)))) 

    # Convert the training and validation sets. 
    _convert_dataset('train', training_filenames, class_names_to_ids,   
    dataset_dir) 
    _convert_dataset('validation', validation_filenames,    
    class_names_to_ids, dataset_dir) 

    # Finally, write the labels file: 
    labels_to_class_names = dict(zip(range(len(class_names)),    
    class_names)) 
    dataset_utils.write_label_file(labels_to_class_names, dataset_dir) 

    print('\nFinished converting the Diabetic dataset!')

在这段代码中,我们使用了来自 data_utils 包的 prepare_dr_dataset,该包在本书仓库的根目录中准备好。稍后我们会看这个方法。然后,我们修改了 _get_filenames_and_classes 方法,以返回 trainingvalidation 的文件名。最后几行与 flowers 数据集示例相同:

  def _get_filenames_and_classes(dataset_dir): 
    train_root = os.path.join(dataset_dir, 'processed_images', 'train') 
    validation_root = os.path.join(dataset_dir, 'processed_images',   
    'validation') 
    class_names = [] 
    for filename in os.listdir(train_root): 
        path = os.path.join(train_root, filename) 
        if os.path.isdir(path): 
            class_names.append(filename) 

    train_filenames = [] 
    directories = [os.path.join(train_root, name) for name in    
    class_names] 
    for directory in directories: 
        for filename in os.listdir(directory): 
            path = os.path.join(directory, filename) 
            train_filenames.append(path) 

    validation_filenames = [] 
    directories = [os.path.join(validation_root, name) for name in    
    class_names] 
    for directory in directories: 
        for filename in os.listdir(directory): 
            path = os.path.join(directory, filename) 
            validation_filenames.append(path) 
    return train_filenames, validation_filenames, sorted(class_names) 

在前面的这个方法中,我们查找了 processed_images/trainprocessed/validation 文件夹中的所有文件名,这些文件夹包含了在 data_utils.prepare_dr_dataset 方法中预处理过的图像。

data_utils.py 文件中,我们编写了 prepare_dr_dataset(dataset_dir) 函数,负责整个数据的预处理工作。

让我们首先定义必要的变量来链接到我们的数据:

num_of_processing_threads = 16 
dr_dataset_base_path = os.path.realpath(dataset_dir) 
unique_labels_file_path = os.path.join(dr_dataset_base_path, "unique_labels_file.txt") 
processed_images_folder = os.path.join(dr_dataset_base_path, "processed_images") 
num_of_processed_images = 35126 
train_processed_images_folder = os.path.join(processed_images_folder, "train") 
validation_processed_images_folder = os.path.join(processed_images_folder, "validation") 
num_of_training_images = 30000 
raw_images_folder = os.path.join(dr_dataset_base_path, "train") 
train_labels_csv_path = os.path.join(dr_dataset_base_path, "trainLabels.csv")

num_of_processing_threads 变量用于指定在预处理数据集时我们希望使用的线程数,正如你可能已经猜到的那样。我们将使用多线程环境来加速数据预处理。随后,我们指定了一些目录路径,用于在预处理时将数据存放在不同的文件夹中。

我们将提取原始形式的图像,然后对它们进行预处理,将其转换为适当的一致格式和大小,之后,我们将使用 download_and_convert_diabetic.py 文件中的 _convert_dataset 方法从处理过的图像生成 tfrecords 文件。之后,我们将这些 tfrecords 文件输入到训练和测试网络中。

正如我们在前一部分所说的,我们已经提取了 dataset 文件和标签文件。现在,既然我们已经提取了所有数据并将其存储在机器中,我们将开始处理图像。来自 DR 数据集的典型图像如下所示:

我们想要做的是去除这些多余的黑色空间,因为它对我们的网络来说并不必要。这将减少图像中的无关信息。之后,我们会将这张图像缩放成一个 299x299 的 JPG 图像文件。

我们将对所有训练数据集重复此过程。

剪裁黑色图像边框的函数如下所示:

  def crop_black_borders(image, threshold=0):
     """Crops any edges below or equal to threshold

     Crops blank image to 1x1.

     Returns cropped image.

     """
     if len(image.shape) == 3:
         flatImage = np.max(image, 2)
     else:
         flatImage = image
     assert len(flatImage.shape) == 2

     rows = np.where(np.max(flatImage, 0) > threshold)[0]
     if rows.size:
         cols = np.where(np.max(flatImage, 1) > threshold)[0]
         image = image[cols[0]: cols[-1] + 1, rows[0]: rows[-1] + 1]
     else:
         image = image[:1, :1]

     return image 

这个函数接收图像和一个灰度阈值,低于此值时,它会去除图像周围的黑色边框。

由于我们在多线程环境中执行所有这些处理,我们将按批次处理图像。要处理一个图像批次,我们将使用以下函数:

  def process_images_batch(thread_index, files, labels, subset):

     num_of_files = len(files)

     for index, file_and_label in enumerate(zip(files, labels)):
         file = file_and_label[0] + '.jpeg'
         label = file_and_label[1]

         input_file = os.path.join(raw_images_folder, file)
         output_file = os.path.join(processed_images_folder, subset,   
         str(label), file)

         image = ndimage.imread(input_file)
         cropped_image = crop_black_borders(image, 10)
         resized_cropped_image = imresize(cropped_image, (299, 299, 3),   
         interp="bicubic")
         imsave(output_file, resized_cropped_image)

         if index % 10 == 0:
             print("(Thread {}): Files processed {} out of  
             {}".format(thread_index, index, num_of_files)) 

thread_index 告诉我们调用该函数的线程 ID。处理图像批次的多线程环境在以下函数中定义:

   def process_images(files, labels, subset):

     # Break all images into batches with a [ranges[i][0], ranges[i] 
     [1]].
     spacing = np.linspace(0, len(files), num_of_processing_threads +  
     1).astype(np.int)
     ranges = []
     for i in xrange(len(spacing) - 1):
         ranges.append([spacing[i], spacing[i + 1]])

     # Create a mechanism for monitoring when all threads are finished.
     coord = tf.train.Coordinator()

     threads = []
     for thread_index in xrange(len(ranges)):
         args = (thread_index, files[ranges[thread_index] 
         [0]:ranges[thread_index][1]],
                 labels[ranges[thread_index][0]:ranges[thread_index] 
                 [1]],
                 subset)
         t = threading.Thread(target=process_images_batch, args=args)
         t.start()
         threads.append(t)

     # Wait for all the threads to terminate.
     coord.join(threads) 

为了从所有线程中获取最终结果,我们使用一个 TensorFlow 类,tf.train.Coordinator(),它的 join 函数负责处理所有线程的最终处理点。

对于线程处理,我们使用threading.Thread,其中target参数指定要调用的函数,args参数指定目标函数的参数。

现在,我们将处理训练图像。训练数据集分为训练集(30,000 张图像)和验证集(5,126 张图像)。

总的预处理过程如下所示:

def process_training_and_validation_images():
     train_files = []
     train_labels = []

     validation_files = []
     validation_labels = []

     with open(train_labels_csv_path) as csvfile:
         reader = csv.DictReader(csvfile)
         for index, row in enumerate(reader):
             if index < num_of_training_images:
                 train_files.extend([row['image'].strip()])
                 train_labels.extend([int(row['level'].strip())])
             else:
                 validation_files.extend([row['image'].strip()])

   validation_labels.extend([int(row['level'].strip())])

     if not os.path.isdir(processed_images_folder):
         os.mkdir(processed_images_folder)

     if not os.path.isdir(train_processed_images_folder):
         os.mkdir(train_processed_images_folder)

     if not os.path.isdir(validation_processed_images_folder):
         os.mkdir(validation_processed_images_folder)

     for directory_index in range(5):
         train_directory_path =   
    os.path.join(train_processed_images_folder,   
    str(directory_index))
         valid_directory_path =   
   os.path.join(validation_processed_images_folder,  
   str(directory_index))

         if not os.path.isdir(train_directory_path):
             os.mkdir(train_directory_path)

         if not os.path.isdir(valid_directory_path):
             os.mkdir(valid_directory_path)

     print("Processing training files...")
     process_images(train_files, train_labels, "train")
     print("Done!")

     print("Processing validation files...")
     process_images(validation_files, validation_labels,  
     "validation")
     print("Done!")

     print("Making unique labels file...")
     with open(unique_labels_file_path, 'w') as unique_labels_file:
         unique_labels = ""
         for index in range(5):
             unique_labels += "{}\n".format(index)
         unique_labels_file.write(unique_labels)

     status = check_folder_status(processed_images_folder, 
     num_of_processed_images,
     "All processed images are present in place",
     "Couldn't complete the image processing of training and  
     validation files.")

     return status 

现在,我们将查看准备数据集的最后一个方法,即在download_and_convert_diabetic.py文件中调用的_convert_dataset方法:

def _get_dataset_filename(dataset_dir, split_name, shard_id): 
    output_filename = 'diabetic_%s_%05d-of-%05d.tfrecord' % ( 
        split_name, shard_id, _NUM_SHARDS) 
    return os.path.join(dataset_dir, output_filename) 
def _convert_dataset(split_name, filenames, class_names_to_ids, dataset_dir): 
    """Converts the given filenames to a TFRecord dataset. 

    Args: 
      split_name: The name of the dataset, either 'train' or  
     'validation'. 
      filenames: A list of absolute paths to png or jpg images. 
      class_names_to_ids: A dictionary from class names (strings) to  
      ids 
        (integers). 
      dataset_dir: The directory where the converted datasets are  
     stored. 
    """ 
    assert split_name in ['train', 'validation'] 

    num_per_shard = int(math.ceil(len(filenames) /  
    float(_NUM_SHARDS))) 

    with tf.Graph().as_default(): 
        image_reader = ImageReader() 

        with tf.Session('') as sess: 

            for shard_id in range(_NUM_SHARDS): 
                output_filename = _get_dataset_filename( 
                    dataset_dir, split_name, shard_id) 

                with tf.python_io.TFRecordWriter(output_filename)
                as   
                tfrecord_writer: 
                    start_ndx = shard_id * num_per_shard 
                    end_ndx = min((shard_id + 1) * num_per_shard,  
                    len(filenames)) 
                    for i in range(start_ndx, end_ndx): 
                        sys.stdout.write('\r>> Converting image  
                         %d/%d shard %d' % ( 
                            i + 1, len(filenames), shard_id)) 
                        sys.stdout.flush() 

                        # Read the filename: 
                        image_data =  
                    tf.gfile.FastGFile(filenames[i], 'rb').read() 
                        height, width =          
                    image_reader.read_image_dims(sess, image_data) 

                        class_name =  
                     os.path.basename(os.path.dirname(filenames[i])) 
                        class_id = class_names_to_ids[class_name] 

                        example = dataset_utils.image_to_tfexample( 
                            image_data, b'jpg', height, width,   
                             class_id) 

                 tfrecord_writer.write(example.SerializeToString()) 

                  sys.stdout.write('\n') 
                  sys.stdout.flush() 

在前面的函数中,我们将获取图像文件名,并将它们存储在tfrecord文件中。我们还会将trainvalidation文件拆分为多个tfrecord文件,而不是只使用一个文件来存储每个分割数据集。

现在,数据处理已经完成,我们将正式将数据集形式化为slim.dataset的实例。数据集来自Tensorflow Slim。在datasets/diabetic.py文件中,你将看到一个名为get_split的方法,如下所示:

_FILE_PATTERN = 'diabetic_%s_*.tfrecord' 
SPLITS_TO_SIZES = {'train': 30000, 'validation': 5126} 
_NUM_CLASSES = 5 
_ITEMS_TO_DESCRIPTIONS = { 
    'image': 'A color image of varying size.', 
    'label': 'A single integer between 0 and 4', 
} 
def get_split(split_name, dataset_dir, file_pattern=None, reader=None): 
  """Gets a dataset tuple with instructions for reading flowers. 
  Args: 
    split_name: A train/validation split name. 
    dataset_dir: The base directory of the dataset sources. 
    file_pattern: The file pattern to use when matching the dataset sources. 
      It is assumed that the pattern contains a '%s' string so that the split 
      name can be inserted. 
    reader: The TensorFlow reader type. 
  Returns: 
    A `Dataset` namedtuple. 
  Raises: 
    ValueError: if `split_name` is not a valid train/validation split. 
  """ 
  if split_name not in SPLITS_TO_SIZES: 
    raise ValueError('split name %s was not recognized.' % split_name) 

  if not file_pattern: 
    file_pattern = _FILE_PATTERN 
  file_pattern = os.path.join(dataset_dir, file_pattern % split_name) 

  # Allowing None in the signature so that dataset_factory can use the default. 
  if reader is None: 
    reader = tf.TFRecordReader 

  keys_to_features = { 
      'image/encoded': tf.FixedLenFeature((), tf.string, default_value=''), 
      'image/format': tf.FixedLenFeature((), tf.string, default_value='png'), 
      'image/class/label': tf.FixedLenFeature( 
          [], tf.int64, default_value=tf.zeros([], dtype=tf.int64)), 
  } 
  items_to_handlers = { 
      'image': slim.tfexample_decoder.Image(), 
      'label': slim.tfexample_decoder.Tensor('image/class/label'), 
  } 
  decoder = slim.tfexample_decoder.TFExampleDecoder( 
      keys_to_features, items_to_handlers) 

  labels_to_names = None 
  if dataset_utils.has_labels(dataset_dir): 
    labels_to_names = dataset_utils.read_label_file(dataset_dir) 

  return slim.dataset.Dataset( 
      data_sources=file_pattern, 
      reader=reader, 
      decoder=decoder, 
      num_samples=SPLITS_TO_SIZES[split_name], 
      items_to_descriptions=_ITEMS_TO_DESCRIPTIONS, 
      num_classes=_NUM_CLASSES, 
      labels_to_names=labels_to_names) 

之前的方法将在训练和评估过程中被调用。我们将创建一个slim.dataset的实例,包含关于我们的tfrecord文件的信息,以便它可以自动解析二进制文件。此外,我们还可以使用slim.dataset.Dataset,结合DatasetDataProvider,通过 Tensorflow Slim 来并行读取数据集,从而提高训练和评估的效率。

在开始训练之前,我们需要从Tensorflow Slim 图像分类库中下载 Inception V3 的预训练模型,这样我们就可以利用 Inception V3 的性能,而无需从头开始训练。

预训练快照可以在这里找到:

github.com/tensorflow/models/tree/master/research/slim#Pretrained

在本章中,我们将使用 Inception V3,因此我们需要下载inception_v3_2016_08_28.tar.gz文件,并解压缩它以获得名为inception_v3.ckpt的检查点文件。

训练过程

现在,让我们继续进行模型的训练和评估。

训练脚本位于train_image_classifer.py文件中。由于我们遵循了该库的工作流程,因此可以保持该文件不变,并使用以下命令运行训练过程:

python train_image_classifier.py --train_dir=D:\datasets\diabetic\checkpoints --dataset_name=diabetic --dataset_split_name=train --dataset_dir=D:\datasets\diabetic\tfrecords --model_name=inception_v3 --checkpoint_path=D:\datasets\diabetic\checkpoints\inception_v3\inception_v3.ckpt --checkpoint_exclude_scopes=InceptionV3/Logits,InceptionV3/AuxLogits --trainable_scopes=InceptionV3/Logits,InceptionV3/AuxLogits --learning_rate=0.000001 --learning_rate_decay_type=exponential 

在我们的设置中,我们已经让训练过程运行了一整夜。现在,我们将运行训练好的模型,通过验证过程来查看其效果。

验证过程

你可以使用以下命令运行验证过程:

python eval_image_classifier.py --alsologtostderr --checkpoint_path=D:\datasets\diabetic\checkpoints\model.ckpt-92462 --dataset_name=diabetic --dataset_split_name=validation --dataset_dir=D:\datasets\diabetic\tfrecords --model_name=inception_v3

如你所见,当前的准确率大约是 75%。在进一步探索部分,我们将给出一些提高准确率的建议。

现在,我们将查看 TensorBoard,来可视化训练过程。

使用 TensorBoard 可视化输出

现在,我们将使用 TensorBoard 来可视化训练结果。

首先,你需要将command-line目录更改为包含检查点的文件夹。在我们的例子中,它是上一条命令中的train_dir参数,D:\datasets\diabetic\checkpoints。然后,你应该运行以下命令:

tensorboard -logdir .

以下是我们运行 TensorBoard 时的输出:

前面的图像显示了包含 RMSprop 优化器的节点,用于训练网络以及它所包含的用于 DR 分类输出的一些 logits。下一张截图展示了作为输入的图像及其预处理和修改:

在此截图中,你可以看到训练过程中网络输出的图形:

这张截图显示了训练过程中网络的总原始损失:

Inception 网络

Inception 网络的主要概念是将不同的卷积操作结合在同一层中。通过将 7x7、5x5、3x3 和 1x1 的卷积组合在一起,传递给下一层。通过这种方式,网络可以提取更多的特征,从而提高准确性。以下是 Google Inception V3 网络的示意图。你可以尝试访问chapter_08/nets/inception_v3.py中的代码。

该图像来自github.com/tensorflow/models/blob/master/research/inception/g3doc/inception_v3_architecture.png

继续深入

我们从运行该网络中得到的结果是在验证集上的准确率为 75%。这并不算很好,因为该网络的使用非常关键。在医学中,错误的余地非常小,因为人的健康状况直接关系到生命。

为了提高准确性,我们需要定义不同的评估标准。你可以在这里阅读更多内容:

en.wikipedia.org/wiki/Confusion_matrix

同时,你可以平衡数据集。现在的数据集是不平衡的,病人数量远少于正常患者。因此,网络对正常患者特征更加敏感,而对病人特征较不敏感。

为了修复这个问题,我们可以对数据集进行 SMOTE 处理。SMOTE 基本上是通过复制较少频繁类别的数据(例如水平或垂直翻转图像、改变饱和度等)来创建一个平衡的数据集。SMOTE 代表合成少数类过采样技术

这是一本关于该主题的优秀读物:

www.jair.org/media/953/live-953-2037-jair.pdf

其他医学数据挑战

可以理解的是,医疗数据不像其他数据集那样容易发布,因此公开领域的数据集要少得多。这一情况正在缓慢改变,但与此同时,以下是一些你可以尝试的公开数据集和相关挑战。需要注意的是,许多挑战已经被克服,但幸运的是,它们仍然继续发布数据集。

ISBI 大奖挑战

ISBI 是国际生物医学影像学大会,这是一个推动本章中所述工作的受欢迎的会议场所。他们的年度会议通常会向学术界提出多个挑战。2016 年他们提出了几个挑战。

一个受欢迎的挑战是 AIDA-E:内窥镜图像分析检测异常。挑战网站是isbi-aida.grand-challenge.org/

另一个受欢迎的挑战是淋巴结中的癌症转移检测,涉及病理数据。挑战网站是camelyon16.grand-challenge.org/

在放射学方面,2016 年一个受欢迎的挑战是数据科学碗挑战赛,聚焦心脏病诊断。该挑战名为转变我们诊断心脏病的方式,目标是对心脏磁共振成像(MRI)数据的部分进行分割,以衡量心脏泵血量,这一数据被用作心脏健康的代理指标。挑战网站及数据集为www.datasciencebowl.com/competitions/transforming-how-we-diagnose-heart-disease/

另一个受欢迎的放射学数据集是 Lung Image Database Consortium(LIDC)中的计算机断层扫描CT)数据,属于 LIDC-IDRI 图像集。这是一个诊断和肺癌筛查胸部 CT 扫描的数据集。有趣的是,除了图像级别的类别外,该数据集还标注了病变的实际位置。

这两个放射学竞赛还有两个有趣的原因:

  • 它们包括三维体积数据,本质上是由二维图像组成的有序堆叠,这些图像共同构成了一个实际的空间。

  • 它们包括分割任务,在这些任务中,你需要将图像或体积的某些部分分类到特定类别。这是一个常见的分类挑战,不同之处在于我们还尝试对图像中的特征进行定位。在一种情况下,我们尝试定位特征并指出它的位置(而不是对整个图像进行分类),在另一种情况下,我们则尝试将一个区域进行分类,以此来衡量一个区域的大小。

我们稍后会更多地讨论如何处理体积数据,但目前你已经有了一些非常有趣和多样化的数据集可以使用。

阅读医疗数据

尽管存在挑战,但糖尿病视网膜病变挑战并不像想象中那么复杂。实际图像是以 JPEG 格式提供的,但大多数医学数据并非 JPEG 格式。它们通常是容器格式,如 DICOM。DICOM 代表 医学中的数字成像与通信,并且有多个版本和变体。它包含医学图像,但也有头数据。头数据通常包括一般的病人和研究数据,但它还可以包含其他几十个自定义字段。如果你幸运的话,它也会包含诊断信息,你可以将其作为标签。

DICOM 数据为我们之前讨论的流程增加了一个步骤,因为现在我们需要读取 DICOM 文件,提取头信息(并希望包括类/标签数据),并提取底层图像。DICOM 并不像 JPEG 或 PNG 那么容易使用,但也不算太复杂。它需要一些额外的包。

由于我们几乎所有的工作都是用 Python 完成的,因此让我们使用一个用于 DICOM 处理的 Python 库。最流行的是 pydicom,可以在github.com/darcymason/pydicom找到。

文档可以在pydicom.readthedocs.io/en/stable/getting_started.html获取。

应注意,pip 安装当前存在问题,因此必须从源代码仓库克隆并通过设置脚本进行安装,才能使用。

来自文档的一个简短摘录将有助于我们理解如何处理 DICOM 文件:

>>> import dicom 
>>> plan = dicom.read_file("rtplan.dcm") 
>>> plan.PatientName 
'Last^First^mid^pre' 
>>> plan.dir("setup")    # get a list of tags with "setup" somewhere in the name 
['PatientSetupSequence'] 
>>> plan.PatientSetupSequence[0] 
(0018, 5100) Patient Position                    CS: 'HFS' 
(300a, 0182) Patient Setup Number                IS: '1' 
(300a, 01b2) Setup Technique Description         ST: '' 

这看起来可能有些凌乱,但这正是你在处理医学数据时应该预期的交互方式。更糟糕的是,每个供应商通常将相同的数据,甚至是基本数据,放入略有不同的标签中。行业中的典型做法就是“到处看看!”我们通过以下方式转储整个标签集来做到这一点:

>>> ds 
(0008, 0012) Instance Creation Date              DA: '20030903' 
(0008, 0013) Instance Creation Time              TM: '150031' 
(0008, 0016) SOP Class UID                       UI: RT Plan Storage 
(0008, 0018) Diagnosis                        UI: Positive  
(0008, 0020) Study Date                          DA: '20030716' 
(0008, 0030) Study Time                          TM: '153557' 
(0008, 0050) Accession Number                    SH: '' 
(0008, 0060) Modality                            CS: 'RTPLAN'

假设我们在寻找诊断信息。我们会查看几个标签文件,尝试看看诊断是否始终出现在标签(0008, 0018) Diagnosis下,如果是,我们通过从大部分训练集中提取这个字段来验证我们的假设,看它是否始终被填充。如果是的话,我们就可以进入下一步。如果不是,我们需要重新开始并查看其他字段。从理论上讲,数据提供者、经纪人或供应商可以提供这些信息,但从实际情况来看,这并不那么简单。

下一步是查看值域。这一点非常重要,因为我们希望看到我们的类的表现。理想情况下,我们会得到一个干净的值集合,例如{Negative, Positive},但实际上,我们通常会得到一条长尾的脏值。所以,典型的做法是遍历每一张图片,并统计每个遇到的唯一值域值,具体如下:

>>> import dicom, glob, os 
>>> os.chdir("/some/medical/data/dir") 
>>> domains={} 
>>> for file in glob.glob("*.dcm"): 
>>>    aMedFile = dicom.read_file(file) 
>>>    theVal=aMedFile.ds[0x10,0x10].value 
>>>    if domains[theVal]>0: 
>>>       domains[theVal]= domains[theVal]+1 
>>>    else: 
>>>       domains[theVal]=1 

在这一点上,最常见的发现是,99%的领域值存在于少数几个领域值之间(如正向负向),而剩下的 1%的领域值是脏数据(如正向,但待审阅@#Q#\(%@#\)%,或已发送重新阅读)。最简单的方法是丢弃这些长尾数据——只保留好的数据。如果有足够的训练数据,这尤其容易做到。

好的,我们已经提取了类信息,但我们仍然需要提取实际的图像。我们可以按照以下步骤进行:

>>> import dicom 
>>> ds=dicom.read_file("MR_small.dcm") 
>>> ds.pixel_array 
array([[ 905, 1019, 1227, ...,  302,  304,  328], 
       [ 628,  770,  907, ...,  298,  331,  355], 
       [ 498,  566,  706, ...,  280,  285,  320], 
       ..., 
       [ 334,  400,  431, ..., 1094, 1068, 1083], 
       [ 339,  377,  413, ..., 1318, 1346, 1336], 
       [ 378,  374,  422, ..., 1369, 1129,  862]], dtype=int16) 
>>> ds.pixel_array.shape 
(64, 64)

不幸的是,这仅仅给了我们一个原始的像素值矩阵。我们仍然需要将其转换为可读的格式(理想情况下是 JPEG 或 PNG)。我们将按以下步骤进行下一步操作:

接下来,我们将把图像缩放到我们需要的比特长度,并使用另一个库将矩阵写入文件,该库专门用于将数据写入目标格式。在我们的例子中,我们将使用 PNG 输出格式,并使用png库将其写入。这意味着需要额外的导入:

import os 
from pydicom import dicomio 
import png 
import errno 
import fnmatch

我们将这样导出:

学到的技能

你应该在本章中学到这些技能:

  • 处理晦涩难懂且专有的医学影像格式

  • 处理大型图像文件,这是医学图像的一个常见特征

  • 从医疗文件中提取类数据

  • 扩展我们现有的管道以处理异构数据输入

  • 应用在非医学数据上预训练的网络

  • 扩展训练以适应新数据集。

总结

在本章中,我们为医学诊断这一企业级问题创建了一个深度神经网络,用于图像分类问题。此外,我们还引导你完成了读取 DICOM 数字医学影像数据的过程,为进一步研究做准备。在下一章,我们将构建一个可以通过学习用户反馈自我改进的生产系统。

第九章:自适应巡航控制 - 自动化

在本章中,我们将创建一个生产系统,从训练到提供模型服务。我们的系统将能够区分 37 种不同的狗和猫品种。用户可以向我们的系统上传图像并接收结果。系统还可以从用户那里接收反馈,并每天自动进行训练以改善结果。

本章将重点讲解以下几个方面:

  • 如何将迁移学习应用到新数据集

  • 如何使用 TensorFlow Serving 提供生产模型服务

  • 创建一个通过众包标注数据集并在用户数据上自动微调模型的系统

系统概览

以下图表提供了我们系统的概述:

在这个系统中,我们将使用一个初始数据集在训练服务器上训练一个卷积神经网络模型。然后,模型将在生产服务器上通过 TensorFlow Serving 提供服务。在生产服务器上,会有一个 Flask 服务器,允许用户上传新图像并在模型出现错误时修正标签。在一天的某个特定时间,训练服务器将会将所有用户标记过的图像与当前数据集合并,以自动微调模型并将其发送到生产服务器。以下是允许用户上传并接收结果的网页界面框架:

设置项目

在本章中,我们将微调一个已经在 ImageNet 数据上训练过的 VGG 模型,该数据集有 1,000 个类别。我们已经提供了一个包含预训练 VGG 模型和一些实用文件的初始项目。你可以去下载 github.com/mlwithtf/mlwithtf/tree/master/chapter_09 上的代码。

chapter-09 文件夹中,你将看到以下结构:

- data
--VGG16.npz
- samples_data
- production
- utils
--__init__.py
--debug_print.py
- README.md

有两个文件是你需要理解的:

  • VGG16.npz 是从 Caffe 模型导出的预训练模型。第十一章,深入学习 - 21 个问题 将展示如何从 Caffe 模型创建这个文件。在本章中,我们将把它作为模型的初始值。你可以从 chapter_09 文件夹中的 README.md 下载此文件。

  • production 是我们创建的 Flask 服务器,用作用户上传和修正模型的网页接口。

  • debug_print.py 包含了一些我们将在本章中使用的方法,用于理解网络结构。

  • samples_data 包含了一些我们将在本章中使用的猫、狗和汽车的图像。

加载预训练模型以加速训练

在这一节中,我们将专注于在 TensorFlow 中加载预训练模型。我们将使用由牛津大学的 K. Simonyan 和 A. Zisserman 提出的 VGG-16 模型。

VGG-16 是一个非常深的神经网络,具有许多卷积层,后面接着最大池化层和全连接层。在ImageNet挑战中,VGG-16 模型在 1000 类图像的验证集上的 Top-5 分类错误率为 8.1%(单尺度方法):

首先,在project目录中创建一个名为nets.py的文件。以下代码定义了 VGG-16 模型的图:

    import tensorflow as tf 
    import numpy as np 

    def inference(images): 
    with tf.name_scope("preprocess"): 
        mean = tf.constant([123.68, 116.779, 103.939],  
    dtype=tf.float32, shape=[1, 1, 1, 3], name='img_mean') 
        input_images = images - mean 
    conv1_1 = _conv2d(input_images, 3, 3, 64, 1, 1,   
    name="conv1_1") 
    conv1_2 = _conv2d(conv1_1, 3, 3, 64, 1, 1, name="conv1_2") 
    pool1 = _max_pool(conv1_2, 2, 2, 2, 2, name="pool1") 

    conv2_1 = _conv2d(pool1, 3, 3, 128, 1, 1, name="conv2_1") 
    conv2_2 = _conv2d(conv2_1, 3, 3, 128, 1, 1, name="conv2_2") 
    pool2 = _max_pool(conv2_2, 2, 2, 2, 2, name="pool2") 

    conv3_1 = _conv2d(pool2, 3, 3, 256, 1, 1, name="conv3_1") 
    conv3_2 = _conv2d(conv3_1, 3, 3, 256, 1, 1, name="conv3_2") 
    conv3_3 = _conv2d(conv3_2, 3, 3, 256, 1, 1, name="conv3_3") 
    pool3 = _max_pool(conv3_3, 2, 2, 2, 2, name="pool3") 

    conv4_1 = _conv2d(pool3, 3, 3, 512, 1, 1, name="conv4_1") 
    conv4_2 = _conv2d(conv4_1, 3, 3, 512, 1, 1, name="conv4_2") 
    conv4_3 = _conv2d(conv4_2, 3, 3, 512, 1, 1, name="conv4_3") 
    pool4 = _max_pool(conv4_3, 2, 2, 2, 2, name="pool4") 

    conv5_1 = _conv2d(pool4, 3, 3, 512, 1, 1, name="conv5_1") 
    conv5_2 = _conv2d(conv5_1, 3, 3, 512, 1, 1, name="conv5_2") 
    conv5_3 = _conv2d(conv5_2, 3, 3, 512, 1, 1, name="conv5_3") 
    pool5 = _max_pool(conv5_3, 2, 2, 2, 2, name="pool5") 

    fc6 = _fully_connected(pool5, 4096, name="fc6") 
    fc7 = _fully_connected(fc6, 4096, name="fc7") 
    fc8 = _fully_connected(fc7, 1000, name='fc8', relu=False) 
    outputs = _softmax(fc8, name="output") 
    return outputs 

在上述代码中,有一些事项需要注意:

  • _conv2d_max_pool_fully_connected_softmax是分别定义卷积层、最大池化层、全连接层和 softmax 层的方法。我们稍后将实现这些方法。

  • preprocess命名空间中,我们定义了一个常量张量mean,它会从输入图像中减去。这是 VGG-16 模型训练时所用的均值向量,用于将图像的均值调整为零。

  • 接下来,我们使用这些参数定义卷积层、最大池化层和全连接层。

  • fc8层中,我们不对输出应用 ReLU 激活,而是将输出送入softmax层,以计算 1000 个类别的概率。

现在,我们将在nets.py文件中实现_conv2d_max_pool_fully_connected_softmax方法。

以下是_conv2d_max_pool方法的代码:

 def _conv2d(input_data, k_h, k_w, c_o, s_h, s_w, name, relu=True,  
 padding="SAME"): 
    c_i = input_data.get_shape()[-1].value 
    convolve = lambda i, k: tf.nn.conv2d(i, k, [1, s_h, s_w, 1],  
 padding=padding) 
    with tf.variable_scope(name) as scope: 
        weights = tf.get_variable(name="kernel", shape=[k_h, k_w,  
 c_i, c_o], 

 initializer=tf.truncated_normal_initializer(stddev=1e-1,  
 dtype=tf.float32)) 
        conv = convolve(input_data, weights) 
        biases = tf.get_variable(name="bias", shape=[c_o],  
 dtype=tf.float32, 

 initializer=tf.constant_initializer(value=0.0)) 
        output = tf.nn.bias_add(conv, biases) 
        if relu: 
            output = tf.nn.relu(output, name=scope.name) 
        return output 
 def _max_pool(input_data, k_h, k_w, s_h, s_w, name,  
 padding="SAME"): 
    return tf.nn.max_pool(input_data, ksize=[1, k_h, k_w, 1], 
                          strides=[1, s_h, s_w, 1], padding=padding,  
 name=name) 

如果你阅读过第四章,猫和狗,大部分上面的代码是自解释的,但仍有一些行需要稍作解释:

  • k_hk_w是卷积核的高度和宽度。

  • c_o表示通道输出,即卷积层特征图的数量。

  • s_hs_wtf.nn.conv2dtf.nn.max_pool层的步幅参数。

  • 使用tf.get_variable代替tf.Variable,因为在加载预训练权重时我们还需要再次使用get_variable

实现fully_connected层和softmax层非常简单:

 def _fully_connected(input_data, num_output, name, relu=True): 
    with tf.variable_scope(name) as scope: 
        input_shape = input_data.get_shape() 
        if input_shape.ndims == 4: 
            dim = 1 
            for d in input_shape[1:].as_list(): 
                dim *= d 
            feed_in = tf.reshape(input_data, [-1, dim]) 
        else: 
            feed_in, dim = (input_data, input_shape[-1].value) 
        weights = tf.get_variable(name="kernel", shape=[dim,  
 num_output], 

 initializer=tf.truncated_normal_initializer(stddev=1e-1,  
 dtype=tf.float32)) 
        biases = tf.get_variable(name="bias", shape=[num_output], 
 dtype=tf.float32, 

 initializer=tf.constant_initializer(value=0.0)) 
        op = tf.nn.relu_layer if relu else tf.nn.xw_plus_b 
        output = op(feed_in, weights, biases, name=scope.name) 
        return output 
 def _softmax(input_data, name): 
    return tf.nn.softmax(input_data, name=name) 

使用_fully_connected方法时,我们首先检查输入数据的维度,以便将输入数据重塑为正确的形状。然后,使用get_variable方法创建weightsbiases变量。最后,我们检查relu参数,决定是否应使用tf.nn.relu_layertf.nn.xw_plus_b对输出应用relutf.nn.relu_layer将计算relu(matmul(x, weights) + biases),而tf.nn.xw_plus_b则只计算matmul(x, weights) + biases

本节的最后一个方法用于将预训练的caffe权重加载到已定义的变量中:

   def load_caffe_weights(path, sess, ignore_missing=False): 
    print("Load caffe weights from ", path) 
    data_dict = np.load(path).item() 
    for op_name in data_dict: 
        with tf.variable_scope(op_name, reuse=True): 
            for param_name, data in   
    data_dict[op_name].iteritems(): 
                try: 
                    var = tf.get_variable(param_name) 
                    sess.run(var.assign(data)) 
                except ValueError as e: 
                    if not ignore_missing: 
                        print(e) 
                        raise e 

为了理解这个方法,我们必须了解数据是如何存储在预训练模型VGG16.npz中的。我们创建了一段简单的代码来打印预训练模型中的所有变量。你可以将以下代码放在nets.py的末尾,并使用 Python nets.py运行:

    if __name__ == "__main__": 
    path = "data/VGG16.npz" 
    data_dict = np.load(path).item() 
    for op_name in data_dict: 
        print(op_name) 
        for param_name, data in     ].iteritems(): 
            print("\t" + param_name + "\t" + str(data.shape)) 

这里是一些结果的代码行:

conv1_1
    weights (3, 3, 3, 64)
    biases  (64,)
conv1_2
    weights (3, 3, 64, 64)
    biases  (64,)

如你所见,op_name是层的名称,我们可以通过data_dict[op_name]访问每层的weightsbiases

让我们来看一下load_caffe_weights

  • 我们使用tf.variable_scopereuse=True参数,以便我们能获取图中定义的weightsbiases的准确变量。之后,我们运行 assign 方法为每个变量设置数据。

  • 如果变量名称未定义,get_variable方法将会抛出ValueError。因此,我们将使用ignore_missing变量来决定是否抛出错误。

测试预训练模型

我们已经创建了一个 VGG16 神经网络。在这一部分,我们将尝试使用预训练模型对汽车、猫和狗进行分类,以检查模型是否已成功加载。

nets.py文件中,我们需要将当前的__main__代码替换为以下代码:

    import os 
    from utils import debug_print 
    from scipy.misc import imread, imresize 

    if __name__ == "__main__": 
    SAMPLES_FOLDER = "samples_data" 
    with open('%s/imagenet-classes.txt' % SAMPLES_FOLDER, 'rb') as   
    infile: 
     class_labels = map(str.strip, infile.readlines()) 

    inputs = tf.placeholder(tf.float32, [None, 224, 224, 3],   
    name="inputs") 
    outputs = inference(inputs) 

    debug_print.print_variables(tf.global_variables()) 
    debug_print.print_variables([inputs, outputs]) 

    with tf.Session() as sess: 
     load_caffe_weights("data/VGG16.npz", sess,   
    ignore_missing=False) 

        files = os.listdir(SAMPLES_FOLDER) 
        for file_name in files: 
            if not file_name.endswith(".jpg"): 
                continue 
            print("=== Predict %s ==== " % file_name) 
            img = imread(os.path.join(SAMPLES_FOLDER, file_name),  
            mode="RGB") 
            img = imresize(img, (224, 224)) 

            prob = sess.run(outputs, feed_dict={inputs: [img]})[0] 
            preds = (np.argsort(prob)[::-1])[0:3] 

            for p in preds: 
                print class_labels[p], prob[p]

在前面的代码中,有几件事需要注意:

  • 我们使用debug_print.print_variables辅助方法,通过打印变量名称和形状来可视化所有变量。

  • 我们定义了一个名为inputs的占位符,形状为[None, 224, 224, 3],这是 VGG16 模型所需的输入大小:

      We get the model graph with outputs = inference(inputs). 
  • tf.Session()中,我们调用load_caffe_weights方法并设置ignore_missing=False,以确保能够加载预训练模型的所有权重和偏置。

  • 图片使用scipy中的imreadimresize方法加载和调整大小。然后,我们使用带有feed_dict字典的 sess.run方法,并接收预测结果。

  • 以下是我们在章节开始时提供的samples_datacar.jpgcat.jpgdog.jpg的预测结果:

    == Predict car.jpg ==== 
    racer, race car, racing car 0.666172
    sports car, sport car 0.315847
    car wheel 0.0117961
    === Predict cat.jpg ==== 
    Persian cat 0.762223
    tabby, tabby cat 0.0647032
    lynx, catamount 0.0371023
    === Predict dog.jpg ==== 
    Border collie 0.562288
    collie 0.239735
    Appenzeller 0.0186233

上述结果是这些图像的准确标签。这意味着我们已经成功加载了在 TensorFlow 中的预训练 VGG16 模型。在下一部分,我们将向你展示如何在我们的数据集上微调模型。

为我们的数据集训练模型

在这一部分,我们将演示如何创建数据集、微调模型以及导出模型以供生产使用。

Oxford-IIIT Pet 数据集简介

Oxford-IIIT Pet 数据集包含 37 种犬类和猫类,每个类别有 200 张图像,图像的尺度、姿势和光照变化较大。标注数据包含物种、头部位置和每张图片的像素分割。在我们的应用中,我们只使用物种名称作为模型的类别名称:

数据集统计

以下是狗狗和猫咪品种的数据集:

  1. 狗狗品种:
品种 总数
美国斗牛犬 200
美国比特犬 200
巴吉犬 200
小猎兔犬 200
拳师犬 199
吉娃娃 200
英国可卡犬 196
英国猎狐犬 200
德国短毛指示犬 200
大比利犬 200
哈瓦那犬 200
日本狆犬 200
凯斯犬 199
莱昂贝格犬 200
迷你雪达犬 200
纽芬兰犬 196
博美犬 200
哈巴狗 200
圣伯纳犬 200
萨摩耶犬 200
苏格兰梗 199
柴犬 200
斯塔福郡斗牛梗 189
小麦梗 200
约克夏梗 200
总计 4978
  1. 猫品种:
品种 数量
阿比西尼亚猫 198
孟加拉猫 200
伯曼猫 200
孟买猫 184
英国短毛猫 200
埃及猫 190
缅因猫 200
波斯猫 200
拉格多尔猫 200
俄罗斯蓝猫 200
暹罗猫 199
无毛猫 200
总计 2371
  1. 宠物总数:
家族 数量
2371
4978
总计 7349

下载数据集

我们可以从牛津大学的网站www.robots.ox.ac.uk/~vgg/data/pets/获取数据集。我们需要下载数据集和真实标签数据,分别命名为images.tar.gzannotations.tar.gz。我们将 TAR 文件存储在data/datasets文件夹中,并解压所有.tar文件。确保data文件夹具有以下结构:

- data
-- VGG16.npz
-- datasets
---- annotations
------ trainval.txt
---- images
------ *.jpg

准备数据

在开始训练过程之前,我们需要将数据集预处理为更简单的格式,以便在进一步的自动微调中使用。

首先,我们在project文件夹中创建一个名为scripts的 Python 包。然后,我们创建一个名为convert_oxford_data.py的 Python 文件,并添加以下代码:

    import os 
    import tensorflow as tf 
    from tqdm import tqdm 
    from scipy.misc import imread, imsave 

    FLAGS = tf.app.flags.FLAGS 

    tf.app.flags.DEFINE_string( 
    'dataset_dir', 'data/datasets', 
    'The location of Oxford IIIT Pet Dataset which contains    
     annotations and images folders' 
    ) 

    tf.app.flags.DEFINE_string( 
    'target_dir', 'data/train_data', 
    'The location where all the images will be stored' 
    ) 

    def ensure_folder_exists(folder_path): 
    if not os.path.exists(folder_path): 
        os.mkdir(folder_path) 
    return folder_path 

    def read_image(image_path): 
    try: 
        image = imread(image_path) 
        return image 
    except IOError: 
        print(image_path, "not readable") 
    return None 

在这段代码中,我们使用tf.app.flags.FLAGS来解析参数,以便轻松定制脚本。我们还创建了两个helper方法来创建目录和读取图像。

接下来,我们添加以下代码,将 Oxford 数据集转换为我们喜欢的格式:

 def convert_data(split_name, save_label=False): 
    if split_name not in ["trainval", "test"]: 
    raise ValueError("split_name is not recognized!") 
    target_split_path =  
    ensure_folder_exists(os.path.join(FLAGS.target_dir, split_name)) 
    output_file = open(os.path.join(FLAGS.target_dir, split_name +  
    ".txt"), "w") 

    image_folder = os.path.join(FLAGS.dataset_dir, "images") 
    anno_folder = os.path.join(FLAGS.dataset_dir, "annotations") 

    list_data = [line.strip() for line in open(anno_folder + "/" +  
    split_name + ".txt")] 

    class_name_idx_map = dict() 
    for data in tqdm(list_data, desc=split_name): 
      file_name,class_index,species,breed_id = data.split(" ") 
      file_label = int(class_index) - 1 

      class_name = "_".join(file_name.split("_")[0:-1]) 
      class_name_idx_map[class_name] = file_label 

      image_path = os.path.join(image_folder, file_name + ".jpg") 
      image = read_image(image_path) 
      if image is not None: 
      target_class_dir =  
       ensure_folder_exists(os.path.join(target_split_path,    
       class_name)) 
      target_image_path = os.path.join(target_class_dir,  
       file_name + ".jpg") 
            imsave(target_image_path, image) 
            output_file.write("%s %s\n" % (file_label,  
            target_image_path)) 

    if save_label: 
        label_file = open(os.path.join(FLAGS.target_dir,  
        "labels.txt"), "w") 
        for class_name in sorted(class_name_idx_map,  
        key=class_name_idx_map.get): 
        label_file.write("%s\n" % class_name) 

 def main(_): 
    if not FLAGS.dataset_dir: 
    raise ValueError("You must supply the dataset directory with  
    --dataset_dir") 

    ensure_folder_exists(FLAGS.target_dir) 
    convert_data("trainval", save_label=True) 
    convert_data("test") 

 if __name__ == "__main__": 
    tf.app.run() 

现在,我们可以使用以下代码运行scripts

python scripts/convert_oxford_data.py --dataset_dir data/datasets/ --target_dir data/train_data.

脚本读取 Oxford-IIIT 数据集的真实标签data,并在data/train_data中创建一个新的dataset,其结构如下:

- train_data
-- trainval.txt
-- test.txt
-- labels.txt
-- trainval
---- Abyssinian
---- ...
-- test
---- Abyssinian
---- ...

让我们稍微讨论一下这些内容:

  • labels.txt包含了我们数据集中 37 个物种的列表。

  • trainval.txt包含了我们将在训练过程中使用的图像列表,格式为<class_id> <image_path>

  • test.txt包含了我们将用来检验模型准确度的图像列表。test.txt的格式与trainval.txt相同。

  • trainvaltest文件夹包含 37 个子文件夹,这些文件夹分别是每个类别的名称,且每个类别的所有图像都包含在相应的文件夹中。

设置训练和测试的输入管道

TensorFlow 允许我们创建一个可靠的输入管道,方便快速的训练。在这一部分中,我们将实现tf.TextLineReader来读取训练和测试文本文件。我们将使用tf.train.batch来并行读取和预处理图像。

首先,我们需要在project目录中创建一个名为datasets.py的新 Python 文件,并添加以下代码:

    import tensorflow as tf 
    import os 

    def load_files(filenames): 
    filename_queue = tf.train.string_input_producer(filenames) 
    line_reader = tf.TextLineReader() 
    key, line = line_reader.read(filename_queue) 
    label, image_path = tf.decode_csv(records=line, 

    record_defaults=[tf.constant([], dtype=tf.int32),   
    tf.constant([], dtype=tf.string)], 
                                      field_delim=' ') 
    file_contents = tf.read_file(image_path) 
    image = tf.image.decode_jpeg(file_contents, channels=3) 

    return image, label 

load_files方法中,我们使用tf.TextLineReader读取文本文件的每一行,例如trainval.txttest.txttf.TextLineReader需要一个字符串队列来读取数据,因此我们使用tf.train.string_input_producer来存储文件名。之后,我们将行变量传入tf.decode_cvs,以便获取labelfilename。图片可以通过tf.image.decode_jpeg轻松读取。

现在我们已经能够加载图片,我们可以继续进行,创建image批次和label批次用于training

datasets.py中,我们需要添加一个新方法:

 def input_pipeline(dataset_dir, batch_size, num_threads=8,   
    is_training=True, shuffle=True): 
    if is_training: 
        file_names = [os.path.join(dataset_dir, "trainval.txt")] 
    else: 
        file_names = [os.path.join(dataset_dir, "test.txt")] 
    image, label = load_files(file_names) 

    image = preprocessing(image, is_training) 

    min_after_dequeue = 1000 
    capacity = min_after_dequeue + 3 * batch_size 
    if shuffle: 
     image_batch, label_batch = tf.train.shuffle_batch( 
     [image, label], batch_size, capacity,  
     min_after_dequeue, num_threads 
      ) 
    else: 
        image_batch, label_batch = tf.train.batch( 
            [image, label], batch_size, num_threads, capacity 
            ) 
    return image_batch, label_batch

我们首先使用load_files方法加载imagelabel。然后,我们通过一个新的预处理方法处理图片,该方法稍后将实现。最后,我们将imagelabel传入tf.train.shuffle_batch进行训练,并通过tf.train.batch进行测试:

 def preprocessing(image, is_training=True, image_size=224,  
 resize_side_min=256, resize_side_max=312): 
    image = tf.cast(image, tf.float32) 

    if is_training: 
        resize_side = tf.random_uniform([], minval=resize_side_min,  
        maxval=resize_side_max+1, dtype=tf.int32) 
        resized_image = _aspect_preserving_resize(image,  
        resize_side) 

        distorted_image = tf.random_crop(resized_image, [image_size,  
        image_size, 3]) 

        distorted_image =  
        tf.image.random_flip_left_right(distorted_image) 

        distorted_image =  
        tf.image.random_brightness(distorted_image, max_delta=50) 

        distorted_image = tf.image.random_contrast(distorted_image,  
        lower=0.2, upper=2.0) 

        return distorted_image 
    else: 
        resized_image = _aspect_preserving_resize(image, image_size) 
        return tf.image.resize_image_with_crop_or_pad(resized_image,  
        image_size, image_size)

在训练和测试中,预处理有两种不同的方法。在训练中,我们需要增强数据,以从当前数据集中创建更多的训练数据。预处理方法中使用了几种技术:

  • 数据集中的图片可能有不同的分辨率,但我们只需要 224x224 的图片。因此,在执行random_crop之前,我们需要将图像调整为合适的大小。下图描述了裁剪是如何工作的。_aspect_preserving_resize方法将稍后实现:

  • 在裁剪图片后,我们通过tf.image.random_flip_left_righttf.image.random_brightnesstf.image.random_contrast对图片进行失真处理,从而生成新的训练样本。

  • 在测试过程中,我们只需要通过_aspect_preserving_resizetf.image.resize_image_with_crop_or_pad来调整图片大小。tf.image.resize_image_with_crop_or_pad允许我们对图像进行中心裁剪或填充,以匹配目标的widthheight

现在,我们需要将最后两个方法添加到datasets.py中,如下所示:

    def _smallest_size_at_least(height, width, smallest_side): 
      smallest_side = tf.convert_to_tensor(smallest_side,   
      dtype=tf.int32) 

      height = tf.to_float(height) 
      width = tf.to_float(width) 
      smallest_side = tf.to_float(smallest_side) 

      scale = tf.cond(tf.greater(height, width), 
                    lambda: smallest_side / width, 
                    lambda: smallest_side / height) 
      new_height = tf.to_int32(height * scale) 
      new_width = tf.to_int32(width * scale) 
      return new_height, new_width 

    def _aspect_preserving_resize(image, smallest_side): 
      smallest_side = tf.convert_to_tensor(smallest_side,   
      dtype=tf.int32) 
      shape = tf.shape(image) 
      height = shape[0] 
      width = shape[1] 
      new_height, new_width = _smallest_size_at_least(height, width,   
      smallest_side) 
      image = tf.expand_dims(image, 0) 
      resized_image = tf.image.resize_bilinear(image, [new_height,   
      new_width], align_corners=False) 
      resized_image = tf.squeeze(resized_image) 
      resized_image.set_shape([None, None, 3]) 
      return resized_image 

到目前为止,我们已经做了很多工作来准备datasetinput管道。在接下来的部分中,我们将为我们的datasetlossaccuracytraining操作定义模型,以执行training过程。

定义模型

我们的应用程序需要对37类狗和猫进行分类。VGG16 模型支持 1,000 种不同的类别。在我们的应用中,我们将重用所有层直到fc7层,并从头开始训练最后一层。为了使模型输出37个类别,我们需要修改nets.py中的推理方法,如下所示:

    def inference(images, is_training=False): 
    # 
    # All the code before fc7 are not modified. 
    # 
    fc7 = _fully_connected(fc6, 4096, name="fc7") 
    if is_training: 
        fc7 = tf.nn.dropout(fc7, keep_prob=0.5) 
    fc8 = _fully_connected(fc7, 37, name='fc8-pets', relu=False) 
    return fc8
  • 我们向方法中添加了一个新参数is_training。在fc7层之后,如果推理是训练过程,我们将添加一个tf.nn.dropout层。这个 dropout 层有助于模型在面对未见过的数据时更好地正则化,并避免过拟合。

  • fc8层的输出数量从 1,000 改为 37。此外,fc8层的名称必须更改为另一个名称;在这种情况下,我们选择fc8-pets。如果不更改fc8层的名称,load_caffe_weights仍然会找到新层并分配原始权重,而这些权重的大小与我们新的fc8层不同。

  • 推理方法最后的softmax层也被移除了,因为我们稍后将使用的loss函数只需要未经归一化的输出。

定义训练操作

我们将在一个新的 Python 文件models.py中定义所有操作。首先,让我们创建一些操作来计算lossaccuracy

 def compute_loss(logits, labels): 
   labels = tf.squeeze(tf.cast(labels, tf.int32)) 

   cross_entropy =   
   tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits,    
   labels=labels) 
   cross_entropy_mean = tf.reduce_mean(cross_entropy) 
   tf.add_to_collection('losses', cross_entropy_mean) 

   return tf.add_n(tf.get_collection('losses'),    
   name='total_loss') 

 def compute_accuracy(logits, labels): 
   labels = tf.squeeze(tf.cast(labels, tf.int32)) 
   batch_predictions = tf.cast(tf.argmax(logits, 1), tf.int32) 
   predicted_correctly = tf.equal(batch_predictions, labels) 
   accuracy = tf.reduce_mean(tf.cast(predicted_correctly,   
   tf.float32)) 
   return accuracy

在这些方法中,logits是模型的输出,labels是来自dataset的真实数据。在compute_loss方法中,我们使用tf.nn.sparse_softmax_cross_entropy_with_logits,因此我们不需要通过softmax方法对logits进行归一化。此外,我们也不需要将labels转换为一个热编码向量。在compute_accuracy方法中,我们通过tf.argmaxlogits中的最大值与labels进行比较,从而得到accuracy

接下来,我们将定义learning_rateoptimizer的操作:

 def get_learning_rate(global_step, initial_value, decay_steps,          
   decay_rate): 
   learning_rate = tf.train.exponential_decay(initial_value,   
   global_step, decay_steps, decay_rate, staircase=True) 
   return learning_rate 

 def train(total_loss, learning_rate, global_step, train_vars): 

   optimizer = tf.train.AdamOptimizer(learning_rate) 

   train_variables = train_vars.split(",") 

   grads = optimizer.compute_gradients( 
       total_loss, 
       [v for v in tf.trainable_variables() if v.name in   
       train_variables] 
       ) 
   train_op = optimizer.apply_gradients(grads,   
   global_step=global_step) 
   return train_op 

train方法中,我们配置了optimizer仅对train_vars字符串中定义的某些变量进行compute和应用gradients。这样,我们只更新最后一层fc8weightsbiases,而冻结其他层。train_vars是一个包含通过逗号分隔的变量列表的字符串,例如models/fc8-pets/weights:0,models/fc8-pets/biases:0

执行训练过程

现在我们已经准备好训练模型了。让我们在scripts文件夹中创建一个名为train.py的 Python 文件。首先,我们需要为training过程定义一些参数:

 import tensorflow as tf 
 import os 
 from datetime import datetime 
 from tqdm import tqdm 

 import nets, models, datasets 

 # Dataset 
 dataset_dir = "data/train_data" 
 batch_size = 64 
 image_size = 224 

 # Learning rate 
 initial_learning_rate = 0.001 
 decay_steps = 250 
 decay_rate = 0.9 

 # Validation 
 output_steps = 10  # Number of steps to print output 
 eval_steps = 20  # Number of steps to perform evaluations 

 # Training 
 max_steps = 3000  # Number of steps to perform training 
 save_steps = 200  # Number of steps to perform saving checkpoints 
 num_tests = 5  # Number of times to test for test accuracy 
 max_checkpoints_to_keep = 3 
 save_dir = "data/checkpoints" 
 train_vars = 'models/fc8-pets/weights:0,models/fc8-pets/biases:0' 

 # Export 
 export_dir = "/tmp/export/" 
 export_name = "pet-model" 
 export_version = 2 

这些变量是不言自明的。接下来,我们需要定义一些用于training的操作,如下所示:

 images, labels = datasets.input_pipeline(dataset_dir, batch_size,   
 is_training=True) 
 test_images, test_labels = datasets.input_pipeline(dataset_dir,  
 batch_size, is_training=False) 

 with tf.variable_scope("models") as scope: 
    logits = nets.inference(images, is_training=True) 
    scope.reuse_variables() 
    test_logits = nets.inference(test_images, is_training=False) 

 total_loss = models.compute_loss(logits, labels) 
 train_accuracy = models.compute_accuracy(logits, labels) 
 test_accuracy = models.compute_accuracy(test_logits, test_labels) 

 global_step = tf.Variable(0, trainable=False) 
 learning_rate = models.get_learning_rate(global_step,  
 initial_learning_rate, decay_steps, decay_rate) 
 train_op = models.train(total_loss, learning_rate, global_step,  
 train_vars) 

 saver = tf.train.Saver(max_to_keep=max_checkpoints_to_keep) 
 checkpoints_dir = os.path.join(save_dir,  
 datetime.now().strftime("%Y-%m-%d_%H-%M-%S")) 
 if not os.path.exists(save_dir): 
    os.mkdir(save_dir) 
 if not os.path.exists(checkpoints_dir): 
    os.mkdir(checkpoints_dir) 

这些操作是通过调用我们在datasets.pynets.pymodels.py中定义的方法创建的。在这段代码中,我们为训练创建了一个输入管道,并为测试创建了另一个管道。之后,我们创建了一个新的variable_scope,命名为models,并通过nets.inference方法创建了logitstest_logits。你必须确保添加了scope.reuse_variables,因为我们希望在测试中重用训练中的weightsbiases。最后,我们创建了一个saver和一些目录,以便每隔save_steps保存检查点。

training过程的最后一部分是training循环:

 with tf.Session() as sess: 
    sess.run(tf.global_variables_initializer()) 
    coords = tf.train.Coordinator() 
    threads = tf.train.start_queue_runners(sess=sess, coord=coords) 

    with tf.variable_scope("models"): 
       nets.load_caffe_weights("data/VGG16.npz", sess,  
       ignore_missing=True) 

    last_saved_test_accuracy = 0 
    for i in tqdm(range(max_steps), desc="training"): 
                  _, loss_value, lr_value = sess.run([train_op,    
                  total_loss,  learning_rate]) 

      if (i + 1) % output_steps == 0: 
          print("Steps {}: Loss = {:.5f} Learning Rate =  
          {}".format(i + 1, loss_value, lr_value)) 

      if (i + 1) % eval_steps == 0: 
          test_acc, train_acc, loss_value =  
          sess.run([test_accuracy, train_accuracy, total_loss]) 
          print("Test accuracy {} Train accuracy {} : Loss =  
          {:.5f}".format(test_acc, train_acc, loss_value)) 

      if (i + 1) % save_steps == 0 or i == max_steps - 1: 
          test_acc = 0 
          for i in range(num_tests): 
              test_acc += sess.run(test_accuracy) 
          test_acc /= num_tests 
      if test_acc > last_saved_test_accuracy: 
            print("Save steps: Test Accuracy {} is higher than  
            {}".format(test_acc, last_saved_test_accuracy)) 
             last_saved_test_accuracy = test_acc 
             saved_file = saver.save(sess, 

     os.path.join(checkpoints_dir, 'model.ckpt'), 
                  global_step=global_step) 
          print("Save steps: Save to file %s " % saved_file) 
      else: 
          print("Save steps: Test Accuracy {} is not higher  
                than {}".format(test_acc, last_saved_test_accuracy)) 

    models.export_model(checkpoints_dir, export_dir, export_name,  
    export_version) 

    coords.request_stop() 
    coords.join(threads) 

training循环很容易理解。首先,我们加载预训练的VGG16模型,并将ignore_missing设置为True,因为我们之前更改了fc8层的名称。然后,我们循环max_steps步,在每output_steps步时打印loss,每eval_steps步时打印test_accuracy。每save_steps步,我们检查并保存检查点,如果当前的测试准确率高于之前的准确率。我们仍然需要创建models.export_model,以便在training之后导出模型供服务使用。不过,在继续之前,你可能想要先检查一下training过程是否正常工作。让我们注释掉以下这一行:

    models.export_model(checkpoints_dir, export_dir, export_name,  
    export_version) 

然后,使用以下命令运行training脚本:

python scripts/train.py

这是控制台中的一些输出。首先,我们的脚本加载了预训练的模型。然后,它会输出loss

('Load caffe weights from ', 'data/VGG16.npz')
training:   0%|▏                | 9/3000 [00:05<24:59,  1.99it/s]
Steps 10: Loss = 31.10747 Learning Rate = 0.0010000000475
training:   1%|▎                | 19/3000 [00:09<19:19,  2.57it/s]
Steps 20: Loss = 34.43741 Learning Rate = 0.0010000000475
Test accuracy 0.296875 Train accuracy 0.0 : Loss = 31.28600
training:   1%|▍                | 29/3000 [00:14<20:01,  2.47it/s]
Steps 30: Loss = 15.81103 Learning Rate = 0.0010000000475
training:   1%|▌                | 39/3000 [00:18<19:42,  2.50it/s]
Steps 40: Loss = 14.07709 Learning Rate = 0.0010000000475
Test accuracy 0.53125 Train accuracy 0.03125 : Loss = 20.65380  

现在,让我们停止training并取消注释export_model方法。我们需要models.export_model方法将最新的模型(具有最高测试准确率)导出到名为export_name、版本为export_versionexport_dir文件夹中。

为生产环境导出模型

 def export_model(checkpoint_dir, export_dir, export_name,  
 export_version): 
    graph = tf.Graph() 
    with graph.as_default(): 
        image = tf.placeholder(tf.float32, shape=[None, None, 3]) 
        processed_image = datasets.preprocessing(image,  
        is_training=False) 
        with tf.variable_scope("models"): 
         logits = nets.inference(images=processed_image,  
          is_training=False) 

        model_checkpoint_path =  
        get_model_path_from_ckpt(checkpoint_dir) 
        saver = tf.train.Saver() 

        config = tf.ConfigProto() 
        config.gpu_options.allow_growth = True 
        config.gpu_options.per_process_gpu_memory_fraction = 0.7 

        with tf.Session(graph=graph) as sess: 
            saver.restore(sess, model_checkpoint_path) 
            export_path = os.path.join(export_dir, export_name,  
            str(export_version)) 
            export_saved_model(sess, export_path, image, logits) 
            print("Exported model at", export_path)

export_model方法中,我们需要创建一个新的图表来在生产中运行。在生产中,我们不需要像training那样的所有变量,也不需要输入管道。然而,我们需要使用export_saved_model方法导出模型,具体如下:

 def export_saved_model(sess, export_path, input_tensor,  
 output_tensor): 
    from tensorflow.python.saved_model import builder as  
 saved_model_builder 
    from tensorflow.python.saved_model import signature_constants 
    from tensorflow.python.saved_model import signature_def_utils 
    from tensorflow.python.saved_model import tag_constants 
    from tensorflow.python.saved_model import utils 
    builder = saved_model_builder.SavedModelBuilder(export_path) 

    prediction_signature = signature_def_utils.build_signature_def( 
        inputs={'images': utils.build_tensor_info(input_tensor)}, 
        outputs={ 
            'scores': utils.build_tensor_info(output_tensor) 
        }, 
        method_name=signature_constants.PREDICT_METHOD_NAME) 

    legacy_init_op = tf.group( 
        tf.tables_initializer(), name='legacy_init_op') 
    builder.add_meta_graph_and_variables( 
        sess, [tag_constants.SERVING], 
        signature_def_map={ 
          'predict_images': 
           prediction_signature, 
        }, 
        legacy_init_op=legacy_init_op) 

    builder.save() 

通过这种方法,我们可以为生产环境创建一个模型的元图。我们将在后面的部分介绍如何在生产中提供模型服务。现在,让我们运行scripts,在 3000 步后自动训练并导出:

python scripts/train.py

在我们的系统中,使用 Core i7-4790 CPU 和一块 TITAN-X GPU,训练过程需要 20 分钟才能完成。以下是我们控制台中的最后几条输出:

Steps 3000: Loss = 0.59160 Learning Rate = 0.000313810509397
Test accuracy 0.659375 Train accuracy 0.853125: Loss = 0.25782
Save steps: Test Accuracy 0.859375 is not higher than 0.921875
training: 100%|██████████████████| 3000/3000 [23:40<00:00,  1.27it/s]
    I tensorflow/core/common_runtime/gpu/gpu_device.cc:975] Creating TensorFlow device (/gpu:0) -> (device: 0, name: GeForce GTX TITAN X, pci bus id: 0000:01:00.0)
    ('Exported model at', '/home/ubuntu/models/pet-model/1')

很棒!我们有一个具有 92.18%测试准确率的模型。我们还得到了一个导出的模型文件(.pb 格式)。export_dir文件夹将具有以下结构:

- /home/ubuntu/models/
-- pet_model
---- 1
------ saved_model.pb
------ variables

在生产环境中提供模型服务

在生产中,我们需要创建一个端点,用户可以通过该端点发送图像并接收结果。在 TensorFlow 中,我们可以轻松地使用 TensorFlow Serving 来提供我们的模型服务。在本节中,我们将安装 TensorFlow Serving,并创建一个 Flask 应用,允许用户通过 Web 界面上传他们的图像。

设置 TensorFlow Serving

在你的生产服务器中,你需要安装 TensorFlow Serving 及其前提条件。你可以访问 TensorFlow Serving 的官方网站:tensorflow.github.io/serving/setup。接下来,我们将使用 TensorFlow Serving 提供的标准 TensorFlow 模型服务器来提供模型服务。首先,我们需要使用以下命令构建tensorflow_model_server

bazel build   
//tensorflow_serving/model_servers:tensorflow_model_server

将训练服务器中的/home/ubuntu/models/pet_model文件夹中的所有文件复制到生产服务器中。在我们的设置中,我们选择/home/ubuntu/productions作为存放所有生产模型的文件夹。productions文件夹将具有以下结构:

- /home/ubuntu/productions/
-- 1
---- saved_model.pb
---- variables

我们将使用tmux来保持模型服务器的运行。让我们通过以下命令安装tmux

sudo apt-get install tmux

使用以下命令运行tmux会话:

tmux new -s serving

tmux会话中,让我们切换到tensorflow_serving目录并运行以下命令:

    bazel-bin/tensorflow_serving/model_servers/tensorflow_model_server --port=9000 --model_name=pet-model --model_base_path=/home/ubuntu/productions

控制台的输出应如下所示:

    2017-05-29 13:44:32.203153: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:274] Loading SavedModel: success. Took 537318 microseconds.
    2017-05-29 13:44:32.203243: I tensorflow_serving/core/loader_harness.cc:86] Successfully loaded servable version {name: pet-model version: 1}
    2017-05-29 13:44:32.205543: I tensorflow_serving/model_servers/main.cc:298] Running ModelServer at 0.0.0.0:9000 ...  

如你所见,模型在主机0.0.0.0和端口9000上运行。在下一节中,我们将创建一个简单的 Python 客户端,通过 gRPC 将图像发送到该服务器。

你还应该注意,当前在生产服务器上使用的是 CPU 进行服务。使用 GPU 构建 TensorFlow Serving 超出了本章的范围。如果你更倾向于使用 GPU 进行服务,可以阅读附录 A,高级安装,它解释了如何构建支持 GPU 的 TensorFlow 和 TensorFlow Serving。

运行和测试模型

在项目仓库中,我们已经提供了一个名为production的包。在这个包中,我们需要将labels.txt文件复制到我们的dataset中,创建一个新的 Python 文件client.py,并添加以下代码:

    import tensorflow as tf 
    import numpy as np 
    from tensorflow_serving.apis import prediction_service_pb2,     
    predict_pb2 
    from grpc.beta import implementations 
    from scipy.misc import imread 
    from datetime import datetime 

    class Output: 
    def __init__(self, score, label): 
        self.score = score 
        self.label = label 

    def __repr__(self): 
        return "Label: %s Score: %.2f" % (self.label, self.score) 

    def softmax(x): 
    return np.exp(x) / np.sum(np.exp(x), axis=0) 

    def process_image(path, label_data, top_k=3): 
    start_time = datetime.now() 
    img = imread(path) 

    host, port = "0.0.0.0:9000".split(":") 
    channel = implementations.insecure_channel(host, int(port)) 
    stub =  
    prediction_service_pb2.beta_create_PredictionService_stub(channel) 

    request = predict_pb2.PredictRequest() 
    request.model_spec.name = "pet-model" 
    request.model_spec.signature_name = "predict_images" 

    request.inputs["images"].CopyFrom( 
        tf.contrib.util.make_tensor_proto( 
            img.astype(dtype=float), 
            shape=img.shape, dtype=tf.float32 
        ) 
    ) 

    result = stub.Predict(request, 20.) 
    scores =    
    tf.contrib.util.make_ndarray(result.outputs["scores"])[0] 
    probs = softmax(scores) 
    index = sorted(range(len(probs)), key=lambda x: probs[x],  
    reverse=True) 

    outputs = [] 
    for i in range(top_k): 
        outputs.append(Output(score=float(probs[index[i]]),  
        label=label_data[index[i]])) 

    print(outputs) 
    print("total time", (datetime.now() -   
    start_time).total_seconds()) 
    return outputs 

    if __name__ == "__main__": 
    label_data = [line.strip() for line in   
    open("production/labels.txt", 'r')] 
    process_image("samples_data/dog.jpg", label_data) 
    process_image("samples_data/cat.jpg", label_data) 

在此代码中,我们创建了一个process_image方法,该方法将从图片路径读取图像,并使用一些 TensorFlow 方法创建张量,然后通过 gRPC 将其发送到模型服务器。我们还创建了一个Output类,以便我们可以轻松地将其返回给caller方法。在方法结束时,我们打印输出和总时间,以便我们可以更轻松地调试。我们可以运行此 Python 文件,看看process_image是否有效:

python production/client.py

输出应如下所示:

    [Label: saint_bernard Score: 0.78, Label: american_bulldog Score: 0.21, Label: staffordshire_bull_terrier Score: 0.00]
    ('total time', 14.943942)
    [Label: Maine_Coon Score: 1.00, Label: Ragdoll Score: 0.00, Label: Bengal Score: 0.00]
    ('total time', 14.918235)

我们得到了正确的结果。然而,每张图片的处理时间几乎是 15 秒。原因是我们正在使用 CPU 模式的 TensorFlow Serving。如前所述,你可以在附录 A 中构建支持 GPU 的 TensorFlow Serving,高级安装。如果你跟随那个教程,你将得到以下结果:

    [Label: saint_bernard Score: 0.78, Label: american_bulldog Score: 0.21, Label: staffordshire_bull_terrier Score: 0.00]
    ('total time', 0.493618)
    [Label: Maine_Coon Score: 1.00, Label: Ragdoll Score: 0.00, Label: Bengal Score: 0.00]
    ('total time', 0.023753)

第一次调用时的处理时间是 493 毫秒。然而,之后的调用时间将只有大约 23 毫秒,比 CPU 版本快得多。

设计 Web 服务器

在本节中,我们将设置一个 Flask 服务器,允许用户上传图片,并在模型出错时设置正确的标签。我们已经在生产包中提供了所需的代码。实现带有数据库支持的 Flask 服务器超出了本章的范围。在本节中,我们将描述 Flask 的所有要点,以便你能更好地跟随和理解。

允许用户上传和修正标签的主要流程可以通过以下线框图进行描述。

该流程通过以下路由实现:

路由 方法 描述
/ GET 此路由返回一个网页表单,供用户上传图片。
/upload_image POST 这个路由从 POST 数据中获取图像,将其保存到上传目录,并调用 client.py 中的 process_image 来识别图像并将结果保存到数据库。
/results<result_id> GET 这个路由返回数据库中对应行的结果。
/results<result_id> POST 这个路由将用户的标签保存到数据库,以便我们可以稍后微调模型。
/user-labels GET 这个路由返回所有用户标注图像的列表。在微调过程中,我们会调用此路由获取标注图像的列表。
/model POST 这个路由允许从训练服务器启动微调过程,提供一个新的训练模型。此路由接收压缩模型的链接、版本号、检查点名称以及模型名称。
/model GET 这个路由返回数据库中最新的模型。微调过程将调用此路由来获取最新的模型并从中进行微调。

我们应该在 tmux 会话中运行此服务器,使用以下命令:

tmux new -s "flask"
python production/server.py

测试系统

现在,我们可以通过 http://0.0.0.0:5000 访问服务器。

首先,你会看到一个表单,用来选择并提交一张图像。

网站会被重定向到 /results 页面,显示对应的图像及其结果。用户标签字段为空。页面底部也有一个简短的表单,你可以提交模型的更正标签。

在生产环境中进行自动微调

在运行系统一段时间后,我们将拥有一些用户标注的图像。我们将创建一个微调过程,使其每天自动运行,并使用新数据微调最新的模型。

让我们在 scripts 文件夹中创建一个名为 finetune.py 的文件。

加载用户标注的数据

首先,我们将添加代码以从生产服务器下载所有用户标注的图像:

    import tensorflow as tf 
    import os 
    import json 
    import random 
    import requests 
    import shutil 
    from scipy.misc import imread, imsave 
    from datetime import datetime 
    from tqdm import tqdm 

    import nets, models, datasets 

    def ensure_folder_exists(folder_path): 
    if not os.path.exists(folder_path): 
        os.mkdir(folder_path) 
    return folder_path 

    def download_user_data(url, user_dir, train_ratio=0.8): 
    response = requests.get("%s/user-labels" % url) 
    data = json.loads(response.text) 

    if not os.path.exists(user_dir): 
        os.mkdir(user_dir) 
    user_dir = ensure_folder_exists(user_dir) 
    train_folder = ensure_folder_exists(os.path.join(user_dir,   
    "trainval")) 
    test_folder = ensure_folder_exists(os.path.join(user_dir,   
    "test")) 

    train_file = open(os.path.join(user_dir, 'trainval.txt'), 'w') 
    test_file = open(os.path.join(user_dir, 'test.txt'), 'w') 

    for image in data: 
        is_train = random.random() < train_ratio 
        image_url = image["url"] 
        file_name = image_url.split("/")[-1] 
        label = image["label"] 
        name = image["name"] 

        if is_train: 
          target_folder =  
          ensure_folder_exists(os.path.join(train_folder, name)) 
        else: 
          target_folder =   
          ensure_folder_exists(os.path.join(test_folder, name)) 

        target_file = os.path.join(target_folder, file_name) +   
        ".jpg" 

        if not os.path.exists(target_file): 
            response = requests.get("%s%s" % (url, image_url)) 
            temp_file_path = "/tmp/%s" % file_name 
            with open(temp_file_path, 'wb') as f: 
                for chunk in response: 
                    f.write(chunk) 

            image = imread(temp_file_path) 
            imsave(target_file, image) 
            os.remove(temp_file_path) 
            print("Save file: %s" % target_file) 

        label_path = "%s %s\n" % (label, target_file) 
        if is_train: 
            train_file.write(label_path) 
        else: 
            test_file.write(label_path) 

download_user_data 中,我们调用 /user-labels 端点获取用户标注图像的列表。JSON 的格式如下:

   [ 
    { 
     "id": 1,  
     "label": 0,  
     "name": "Abyssinian",  
     "url": "/uploads/2017-05-23_14-56-45_Abyssinian-cat.jpeg" 
    },  
    { 
     "id": 2,  
      "label": 32,  
      "name": "Siamese",  
     "url": "/uploads/2017-05-23_14-57-33_fat-Siamese-cat.jpeg" 
    } 
   ] 

在这个 JSON 中,label 是用户选择的标签,URL 是用来下载图像的链接。对于每一张图像,我们会将其下载到 tmp 文件夹,并使用 scipy 中的 imreadimsave 来确保图像是 JPEG 格式。我们还会创建 trainval.txttest.txt 文件,和训练数据集中的文件一样。

对模型进行微调

为了微调模型,我们需要知道哪个是最新的模型及其对应的检查点,以恢复 weightsbiases。因此,我们调用 /model 端点来获取检查点名称和版本号:

    def get_latest_model(url): 
    response = requests.get("%s/model" % url) 
    data = json.loads(response.text) 
    print(data) 
    return data["ckpt_name"], int(data["version"]) 

响应的 JSON 应该是这样的:

    { 
     "ckpt_name": "2017-05-26_02-12-49",  
     "id": 10,  
     "link": "http://1.53.110.161:8181/pet-model/8.zip",  
     "name": "pet-model",  
     "version": 8 
    } 

现在,我们将实现微调模型的代码。让我们从一些参数开始:

    # Server info 
    URL = "http://localhost:5000" 
    dest_api = URL + "/model" 

    # Server Endpoints 
    source_api = "http://1.53.110.161:8181" 

    # Dataset 
    dataset_dir = "data/train_data" 
    user_dir = "data/user_data" 
    batch_size = 64 
    image_size = 224 

    # Learning rate 
    initial_learning_rate = 0.0001 
    decay_steps = 250 
    decay_rate = 0.9 

    # Validation 
    output_steps = 10  # Number of steps to print output 
    eval_steps = 20  # Number of steps to perform evaluations 

    # Training 
    max_steps = 3000  # Number of steps to perform training 
    save_steps = 200  # Number of steps to perform saving    
    checkpoints 
    num_tests = 5  # Number of times to test for test accuracy 
    max_checkpoints_to_keep = 1 
    save_dir = "data/checkpoints" 
    train_vars = 'models/fc8-pets/weights:0,models/fc8- 
    pets/biases:0' 

    # Get the latest model 
    last_checkpoint_name, last_version = get_latest_model(URL) 
    last_checkpoint_dir = os.path.join(save_dir,   
    last_checkpoint_name) 

    # Export 
    export_dir = "/home/ubuntu/models/" 
    export_name = "pet-model" 
    export_version = last_version + 1 

然后,我们将实现微调循环。在以下代码中,我们调用 download_user_data 来下载所有用户标注的图像,并将 user_dir 传递给 input_pipeline,使其能够加载新图像:

    # Download user-labels data 
    download_user_data(URL, user_dir) 

    images, labels = datasets.input_pipeline(dataset_dir,     
    batch_size, is_training=True, user_dir=user_dir) 
    test_images, test_labels =    
    datasets.input_pipeline(dataset_dir, batch_size,    
    is_training=False, user_dir=user_dir) 

     with tf.variable_scope("models") as scope: 
     logits = nets.inference(images, is_training=True) 
     scope.reuse_variables() 
     test_logits = nets.inference(test_images, is_training=False) 

    total_loss = models.compute_loss(logits, labels) 
    train_accuracy = models.compute_accuracy(logits, labels) 
    test_accuracy = models.compute_accuracy(test_logits,  
    test_labels) 

    global_step = tf.Variable(0, trainable=False) 
    learning_rate = models.get_learning_rate(global_step,      
    initial_learning_rate, decay_steps, decay_rate) 
    train_op = models.train(total_loss, learning_rate,  
    global_step, train_vars) 

    saver = tf.train.Saver(max_to_keep=max_checkpoints_to_keep) 
    checkpoint_name = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") 
    checkpoints_dir = os.path.join(save_dir, checkpoint_name) 
    if not os.path.exists(save_dir): 
      os.mkdir(save_dir) 
    if not os.path.exists(checkpoints_dir): 
      os.mkdir(checkpoints_dir) 

    with tf.Session() as sess: 
      sess.run(tf.global_variables_initializer()) 
      coords = tf.train.Coordinator() 
      threads = tf.train.start_queue_runners(sess=sess,   
      coord=coords) 

    saver.restore(sess,  
    models.get_model_path_from_ckpt(last_checkpoint_dir)) 
    sess.run(global_step.assign(0)) 

    last_saved_test_accuracy = 0 
    for i in range(num_tests): 
        last_saved_test_accuracy += sess.run(test_accuracy) 
    last_saved_test_accuracy /= num_tests 
    should_export = False 
    print("Last model test accuracy    
    {}".format(last_saved_test_accuracy)) 
    for i in tqdm(range(max_steps), desc="training"): 
        _, loss_value, lr_value = sess.run([train_op, total_loss,   
        learning_rate]) 

     if (i + 1) % output_steps == 0: 
       print("Steps {}: Loss = {:.5f} Learning Rate =   
       {}".format(i + 1, loss_value, lr_value)) 

        if (i + 1) % eval_steps == 0: 
          test_acc, train_acc, loss_value =  
          sess.run([test_accuracy, train_accuracy, total_loss]) 
            print("Test accuracy {} Train accuracy {} : Loss =  
            {:.5f}".format(test_acc, train_acc, loss_value)) 

        if (i + 1) % save_steps == 0 or i == max_steps - 1: 
          test_acc = 0 
          for i in range(num_tests): 
            test_acc += sess.run(test_accuracy) 
            test_acc /= num_tests 

        if test_acc > last_saved_test_accuracy: 
          print("Save steps: Test Accuracy {} is higher than  
          {}".format(test_acc, last_saved_test_accuracy)) 
          last_saved_test_accuracy = test_acc 
          saved_file = saver.save(sess, 

        os.path.join(checkpoints_dir, 'model.ckpt'), 
                                        global_step=global_step) 
                should_export = True 
                print("Save steps: Save to file %s " % saved_file) 
            else: 
                print("Save steps: Test Accuracy {} is not higher  
       than {}".format(test_acc, last_saved_test_accuracy)) 

    if should_export: 
        print("Export model with accuracy ",  
        last_saved_test_accuracy) 
        models.export_model(checkpoints_dir, export_dir,   
        export_name, export_version) 
        archive_and_send_file(source_api, dest_api,  
        checkpoint_name, export_dir, export_name, export_version) 
      coords.request_stop() 
      coords.join(threads)

其他部分与训练循环非常相似。但是,我们不是从caffe模型中加载权重,而是使用最新模型的检查点,并运行测试多次以获取其测试准确性。

在微调循环的末尾,我们需要一个名为archive_and_send_file的新方法来从exported模型创建归档,并将链接发送到生产服务器:

    def make_archive(dir_path): 
    return shutil.make_archive(dir_path, 'zip', dir_path) 

    def archive_and_send_file(source_api, dest_api, ckpt_name,    
    export_dir, export_name, export_version): 
    model_dir = os.path.join(export_dir, export_name,    
    str(export_version)) 
    file_path = make_archive(model_dir) 
    print("Zip model: ", file_path) 

    data = { 
        "link": "{}/{}/{}".format(source_api, export_name,  
     str(export_version) + ".zip"), 
        "ckpt_name": ckpt_name, 
        "version": export_version, 
        "name": export_name, 
    } 
     r = requests.post(dest_api, data=data) 
    print("send_file", r.text) 

您应该注意,我们创建了一个带有source_api参数的链接,这是指向训练服务器的链接,http://1.53.110.161:8181。我们将设置一个简单的 Apache 服务器来支持此功能。但是,在现实中,我们建议您将归档模型上传到云存储,如 Amazon S3。现在,我们将展示使用 Apache 的最简单方法。

我们需要使用以下命令安装 Apache:

sudo apt-get install apache2

现在,在/etc/apache2/ports.conf中,第 6 行,我们需要添加此代码以使apache2监听端口8181

    Listen 8181 

然后,在/etc/apache2/sites-available/000-default.conf的开头添加以下代码以支持从/home/ubuntu/models目录下载:

    <VirtualHost *:8181> 
      DocumentRoot "/home/ubuntu/models" 
      <Directory /> 
        Require all granted 
      </Directory> 
    </VirtualHost> 

最后,我们需要重新启动apache2服务器:

sudo service apache2 restart

到目前为止,我们已经设置了所有执行微调所需的代码。在第一次运行微调之前,我们需要向/model端点发送POST请求,以获取关于我们第一个模型的信息,因为我们已经将模型复制到生产服务器。

project代码库中,让我们运行finetune脚本:

python scripts/finetune.py

控制台中的最后几行将如下所示:

    Save steps: Test Accuracy 0.84 is higher than 0.916875
    Save steps: Save to file data/checkpoints/2017-05-29_18-46-43/model.ckpt-2000
    ('Export model with accuracy ', 0.916875000000004)
    2017-05-29 18:47:31.642729: I tensorflow/core/common_runtime/gpu/gpu_device.cc:977] Creating TensorFlow device (/gpu:0) -> (device: 0, name: GeForce GTX TITAN X, pci bus id: 0000:01:00.0)
    ('Exported model at', '/home/ubuntu/models/pet-model/2')
    ('Zip model: ', '/home/ubuntu/models/pet-model/2.zip')
    ('send_file', u'{\n  "ckpt_name": "2017-05-29_18-46-43", \n  "id": 2, \n  "link": "http://1.53.110.161:8181/pet-model/2.zip", \n  "name": "pet-model", \n  "version": 2\n}\n')

正如您所见,新模型的测试准确率为 91%。该模型也被导出并存档到/home/ubuntu/models/pet-model/2.zip。代码还在调用/model端点将链接发布到生产服务器。在生产服务器的 Flask 应用日志中,我们将得到以下结果:

('Start downloading', u'http://1.53.110.161:8181/pet-model/2.zip')
('Downloaded file at', u'/tmp/2.zip')
('Extracted at', u'/home/ubuntu/productions/2')
127.0.0.1 - - [29/May/2017 18:49:05] "POST /model HTTP/1.1" 200 -

这意味着我们的 Flask 应用程序已从训练服务器下载了2.zip文件,并将其内容提取到/home/ubuntu/productions/2。在 TensorFlow Serving 的 tmux 会话中,您还将获得以下结果:

    2017-05-29 18:49:06.234808: I tensorflow_serving/core/loader_harness.cc:86] Successfully loaded servable version {name: pet-model version: 2}
    2017-05-29 18:49:06.234840: I tensorflow_serving/core/loader_harness.cc:137] Quiescing servable version {name: pet-model version: 1}
    2017-05-29 18:49:06.234848: I tensorflow_serving/core/loader_harness.cc:144] Done quiescing servable version {name: pet-model version: 1}
    2017-05-29 18:49:06.234853: I tensorflow_serving/core/loader_harness.cc:119] Unloading servable version {name: pet-model version: 1}
    2017-05-29 18:49:06.240118: I ./tensorflow_serving/core/simple_loader.h:226] Calling MallocExtension_ReleaseToSystem() with 645327546
    2017-05-29 18:49:06.240155: I tensorflow_serving/core/loader_harness.cc:127] Done unloading servable version {name: pet-model version: 1}

此输出表明 TensorFlow 模型服务器已成功加载pet-modelversion 2并卸载了version 1。这也意味着我们已经为在训练服务器上训练并通过/model端点发送到生产服务器的新模型提供了服务。

设置每天运行的 cron 任务

最后,我们需要设置每天运行的微调,并自动将新模型上传到服务器。我们可以通过在训练服务器上创建crontab轻松实现这一点。

首先,我们需要运行crontab命令:

crontab -e

然后,我们只需添加以下行来定义我们希望finetune.py运行的时间:

0 3 * * * python /home/ubuntu/project/scripts/finetune.py

正如我们所定义的,Python 命令将每天凌晨 3 点运行。

总结

在本章中,我们实现了一个完整的真实生产环境,从训练到服务深度学习模型。我们还在 Flask 应用中创建了一个 web 界面,用户可以上传他们的图像并获得结果。我们的模型可以每天自动进行微调,以提高系统的质量。以下是一些你可以考虑的改进方案:

  • 模型和检查点应保存在云存储中。

  • Flask 应用和 TensorFlow Serving 应由另一个更好的进程管理系统来管理,例如 Supervisor。

  • 应该有一个 web 界面,供团队审批用户选择的标签。我们不应完全依赖用户来决定训练集。

  • TensorFlow Serving 应该构建为支持 GPU 以实现最佳性能。

第十章:实时启用并扩展

在本章中,我们将深入了解 Amazon Web ServicesAWS)以及如何创建深度神经网络来解决视频动作识别问题。我们将展示如何使用多个 GPU 加速训练。章末,我们将简要介绍 Amazon Mechanical Turk 服务,它允许我们收集标签并纠正模型的结果。

快速浏览 Amazon Web Services

Amazon Web ServicesAWS)是最受欢迎的云平台之一,由 Amazon.com 开发。它提供许多服务,包括云计算、存储、数据库服务、内容分发以及其他功能。在本节中,我们将重点关注亚马逊 EC2 上的虚拟服务器服务。Amazon EC2 允许我们创建多个服务器,这些服务器可以支持模型的服务并进行训练。当涉及到为最终用户提供模型服务时,你可以阅读第九章,巡航控制 - 自动化,以了解 TensorFlow Serving。在训练中,Amazon EC2 提供了许多实例类型供我们使用。我们可以使用它们的 CPU 服务器来运行我们的网络机器人,从互联网上收集数据。还有几种实例类型配备了多个 NVIDIA GPU。

亚马逊 EC2 提供了多种实例类型,以适应不同的使用场景。这些实例类型分为五类,具体如下:

  • 通用型

  • 计算优化

  • 内存优化

  • 存储优化

  • 加速计算实例

前四类最适合运行后端服务器。加速计算实例配备了多个 NVIDIA GPU,可用于为模型提供服务并用高端 GPU 训练新模型。共有三种实例类型——P2、G2 和 F1。

P2 实例

P2 实例配备高性能的 NVIDIA K80 GPU,每个 GPU 拥有 2,496 个 CUDA 核心和 12 GB 的 GPU 内存。P2 实例有三种型号,具体见下表:

型号 GPU 数量 vCPU 内存 (GB) GPU 内存 (GB)
p2.xlarge 1 4 61 12
p2.8xlarge 8 32 488 96
p2.16xlarge 16 64 732 192

这些配备大容量 GPU 内存的型号最适合用于训练模型。更多的 GPU 内存可以让我们使用更大的批量大小和更多参数的神经网络来训练模型。

G2 实例

G2 实例配备高性能的 NVIDIA GPU,每个 GPU 拥有 1,536 个 CUDA 核心和 4 GB 的 GPU 内存。G2 实例有两种型号,具体见下表:

型号 GPU 数量 vCPU 内存 (GB) SSD 存储 (GB)
g2.2xlarge 1 8 15 1 x 60
g2.8xlarge 4 32 60 2 x 120

这些模型只有 4 GB 的 GPU 内存,因此在训练上有所限制。然而,4 GB 的 GPU 内存通常足够将模型服务于最终用户。一个重要因素是,G2 实例比 P2 实例便宜得多,这使得我们可以在负载均衡器下部署多个服务器,以实现高可扩展性。

F1 实例

F1 实例支持现场可编程门阵列FPGAs)。F1 有两种型号,如下表所示:

型号 GPU vCPU 内存(GB) SSD 存储 (GB)
f1.2xlarge 1 8 122 470
f1.16xlarge 8 64 976 4 x 940

拥有高内存和计算能力的 FPGA 在深度学习领域前景广阔。然而,TensorFlow 和其他流行的深度学习库并不支持 FPGA。因此,在下一节中,我们将只介绍 P2 和 G2 实例的价格。

定价

让我们在aws.amazon.com/emr/pricing/探索这些实例的定价。

亚马逊 EC2 提供三种实例定价选项——按需实例、预留实例和 Spot 实例:

  • 按需实例使你能够不受干扰地运行服务器。如果你只打算使用实例几天或几周,它非常合适。

  • 预留实例允许你以显著的折扣预定一个一或三年的实例,相比按需实例更具成本优势。如果你想将服务器用于生产环境,它非常合适。

  • Spot 实例允许你对服务器进行竞标。你可以选择每小时实例的最大支付价格。这可以帮助你节省大量费用。然而,如果有人出价高于你的价格,这些实例可能会随时被终止。如果你的系统能够处理中断或你只是想探索服务,那么这种类型适合你。

亚马逊提供了一个网站来计算每月账单。你可以在calculator.s3.amazonaws.com/index.html查看。

你可以点击“添加新行”按钮并选择一个实例类型。

在下图中,我们选择了一个 p2.xlarge 服务器。写作时该服务器一个月的价格为$658.80:

现在点击“Billing Option”列。你将看到一个 p2.xlarge 服务器的预留实例价格:

还有许多其他实例类型。我们建议你查看其他类型并选择最适合你需求的服务器。

在下一节中,我们将创建一个新的模型,使用 TensorFlow 执行视频动作识别。我们还将利用多 GPU 提高训练性能。

应用程序概览

人体动作识别是计算机视觉和机器学习中一个非常有趣的问题。解决这一问题有两种流行的方式,即静态图像动作识别视频动作识别。在静态图像动作识别中,我们可以微调一个从 ImageNet 预训练的模型,并基于静态图像对动作进行分类。你可以回顾前几章以获得更多信息。在本章中,我们将创建一个能够从视频中识别人体动作的模型。在本章结束时,我们将展示如何使用多 GPU 来加速训练过程。

数据集

有许多可用的数据集,我们可以在训练过程中使用,具体如下:

  • UCF101 (crcv.ucf.edu/data/UCF101.php)是一个包含 101 个动作类别的真实动作视频数据集。该数据集共有 13,320 个视频,涵盖了 101 个动作类别,因此它成为许多研究论文的理想选择。

  • ActivityNet (activity-net.org/)是一个用于理解人体活动的大型数据集。该数据集包含 200 个类别,总计 648 小时以上的视频,每个类别大约有 100 个视频。

  • Sports-1M (cs.stanford.edu/people/karpathy/deepvideo/)是另一个用于体育识别的大型数据集。该数据集总共有 1,133,158 个视频,并标注了 487 个体育标签。

在本章中,我们将使用 UCF101 进行训练过程。我们还建议你尝试将本章讨论的技术应用于一个大型数据集,以充分利用多 GPU 训练。

准备数据集和输入管道

UCF101 数据集包含 101 个动作类别,如篮球投篮、弹吉他和冲浪。我们可以从crcv.ucf.edu/data/UCF101.php下载该数据集。

在网站上,你需要下载名为UCF101.rar的 UCF101 数据集,并下载名为UCF101TrainTestSplits-RecognitionTask.zip的动作识别训练/测试数据集划分。在进入下一节之前,你需要解压数据集,接下来我们将对视频进行预处理,然后进行训练。

对视频进行预处理以用于训练

UCF101 包含 13,320 个视频片段,固定帧率为 25 FPS,分辨率为 320 x 240。所有视频片段均以 AVI 格式存储,因此在 TensorFlow 中使用起来不太方便。因此,在本节中,我们将从所有视频中提取视频帧并保存为 JPEG 文件。我们只会以固定帧率 4 FPS 提取视频帧,这样可以减少网络的输入大小。

在我们开始实现代码之前,需要从mikeboers.github.io/PyAV/installation.html安装 av 库。

首先,在 root 文件夹中创建一个名为 scripts 的 Python 包。然后,在 scripts/convert_ucf101.py 路径下创建一个新的 Python 文件。在新创建的文件中,添加第一个代码来导入并定义一些参数,如下所示:

 import av 
 import os 
 import random 
 import tensorflow as tf 
 from tqdm import tqdm 

 FLAGS = tf.app.flags.FLAGS 
 tf.app.flags.DEFINE_string( 
    'dataset_dir', '/mnt/DATA02/Dataset/UCF101', 
    'The folder that contains the extracted content of UCF101.rar' 
 ) 

 tf.app.flags.DEFINE_string( 
    'train_test_list_dir',   
 '/mnt/DATA02/Dataset/UCF101/ucfTrainTestlist', 
    'The folder that contains the extracted content of  
 UCF101TrainTestSplits-RecognitionTask.zip' 
 ) 

 tf.app.flags.DEFINE_string( 
    'target_dir', '/home/ubuntu/datasets/ucf101', 
    'The location where all the images will be stored' 
 ) 

 tf.app.flags.DEFINE_integer( 
    'fps', 4, 
    'Framerate to export' 
 ) 

 def ensure_folder_exists(folder_path): 
    if not os.path.exists(folder_path): 
        os.mkdir(folder_path) 

    return folder_path 

在上述代码中,dataset_dirtrain_test_list_dir 分别是包含 UCF101.rarUCF101TrainTestSplits-RecognitionTask.zip 提取内容的文件夹的位置。target_dir 是存储所有训练图像的文件夹。ensure_folder_exists 是一个 utility 函数,用于在文件夹不存在时创建该文件夹。

接下来,让我们定义 Python 代码的 main 函数:

 def main(_): 
    if not FLAGS.dataset_dir: 
        raise ValueError("You must supply the dataset directory with  
 --dataset_dir") 

    ensure_folder_exists(FLAGS.target_dir) 
    convert_data(["trainlist01.txt", "trainlist02.txt",  
 "trainlist03.txt"], training=True) 
    convert_data(["testlist01.txt", "testlist02.txt",  
 "testlist03.txt"], training=False) 

 if __name__ == "__main__": 
    tf.app.run() 

main 函数中,我们创建了 target_dir 文件夹,并调用了我们稍后将创建的 convert_data 函数。convert_data 函数接受数据集中的训练/测试文本文件列表和一个布尔值 training,指示文本文件是否用于训练过程。

以下是某个文本文件中的一些行:

ApplyEyeMakeup/v_ApplyEyeMakeup_g08_c01.avi 1
ApplyEyeMakeup/v_ApplyEyeMakeup_g08_c02.avi 1
ApplyEyeMakeup/v_ApplyEyeMakeup_g08_c03.avi 1

每一行文本文件包含 video 文件的路径和正确的标签。在这种情况下,我们有三个来自 ApplyEyeMakeup 类别的视频路径,这是数据集中第一个类别。

这里的主要思想是我们读取每一行文本文件,提取视频帧并将其保存为 JPEG 格式,并保存提取文件的路径和对应的标签,供后续训练使用。以下是 convert_data 函数的代码:

 def convert_data(list_files, training=False): 
    lines = [] 
    for txt in list_files: 
        lines += [line.strip() for line in  
 open(os.path.join(FLAGS.train_test_list_dir, txt))] 

    output_name = "train" if training else "test" 

    random.shuffle(lines) 

    target_dir = ensure_folder_exists(os.path.join(FLAGS.target_dir,  
 output_name)) 
    class_index_file = os.path.join(FLAGS.train_test_list_dir,  
 "classInd.txt") 
    class_index = {line.split(" ")[1].strip(): int(line.split(" ") 
 [0]) - 1 for line in open(class_index_file)} 

    with open(os.path.join(FLAGS.target_dir, output_name + ".txt"),  
 "w") as f: 
        for line in tqdm(lines): 
            if training: 
                filename, _ = line.strip().split(" ") 
            else: 
                filename = line.strip() 
            class_folder, video_name = filename.split("/") 

            label = class_index[class_folder] 
            video_name = video_name.replace(".avi", "") 
            target_class_folder =  
 ensure_folder_exists(os.path.join(target_dir, class_folder)) 
            target_folder =  
 ensure_folder_exists(os.path.join(target_class_folder, video_name)) 

            container = av.open(os.path.join(FLAGS.dataset_dir,  
            filename)) 
            frame_to_skip = int(25.0 / FLAGS.fps) 
            last_frame = -1 
            frame_index = 0 
            for frame in container.decode(video=0): 
                if last_frame < 0 or frame.index > last_frame +  
                frame_to_skip: 
                    last_frame = frame.index 
                    image = frame.to_image() 
                    target_file = os.path.join(target_folder,  
                   "%04d.jpg" % frame_index) 
                    image.save(target_file) 
                    frame_index += 1 
            f.write("{} {} {}\n".format("%s/%s" % (class_folder,  
           video_name), label, frame_index)) 

    if training: 
        with open(os.path.join(FLAGS.target_dir, "label.txt"), "w")  
        as f: 
            for class_name in sorted(class_index,  
            key=class_index.get): 
                f.write("%s\n" % class_name) 

上面的代码很简单。我们从文本文件中加载视频路径,并使用 av 库打开 AVI 文件。然后,我们使用 FLAGS.fps 控制每秒需要提取多少帧。你可以通过以下命令运行 scripts/convert_ucf101.py 文件:

python scripts/convert_ucf101.py

整个过程大约需要 30 分钟来转换所有视频片段。最后,target_dir 文件夹将包含以下文件:

label.txt  test  test.txt  train  train.txt

train.txt 文件中,行的格式如下:

Punch/v_Punch_g25_c03 70 43
Haircut/v_Haircut_g20_c01 33 36
BrushingTeeth/v_BrushingTeeth_g25_c02 19 33
Nunchucks/v_Nunchucks_g03_c04 55 36
BoxingSpeedBag/v_BoxingSpeedBag_g16_c04 17 21

这个格式可以理解如下:

<Folder location of the video> <Label> <Number of frames in the folder>  

有一件事你必须记住,那就是 train.txttest.txt 中的标签是从 0 到 100 的。然而,UCF101 中的标签是从 1 到 101 的。这是因为 TensorFlow 中的 sparse_softmax_cross_entropy 函数要求类别标签从 0 开始。

使用 RandomShuffleQueue 的输入管道

如果你已经阅读过第九章,定速巡航 - 自动化,你会知道我们可以使用 TensorFlow 中的 TextLineReader 来简单地逐行读取文本文件,并利用这一行直接在 TensorFlow 中读取图像。然而,当数据仅包含文件夹位置和标签时,事情变得更加复杂。而且,我们只想从一个文件夹中选择一部分帧。例如,如果帧数是 30,我们只想选择 10 帧用于训练,我们会从 0 到 20 随机选择一个起始帧,然后从那里选择 10 帧。因此,在本章中,我们将使用另一种机制,在纯 Python 中对视频帧进行采样,并将选择的帧路径放入RandomShuffleQueue中进行训练。我们还使用tf.train.batch_join来利用多个预处理线程进行训练。

首先,在root文件夹中创建一个新的 Python 文件,命名为utils.py,并添加以下代码:

def lines_from_file(filename, repeat=False): 
    with open(filename) as handle: 
        while True: 
            try: 
                line = next(handle) 
                yield line.strip() 
            except StopIteration as e: 
                if repeat: 
                    handle.seek(0) 
                else: 
                    raise 

if __name__ == "__main__": 
    data_reader = lines_from_file("/home/ubuntu/datasets/ucf101/train.txt", repeat=True) 

    for i in range(15): 
        print(next(data_reader)) 

在这段代码中,我们创建了一个名为lines_from_filegenerator函数,用于逐行读取文本文件。我们还添加了一个repeat参数,以便当generator函数读取到文件末尾时,可以从头开始重新读取文本。

我们添加了一个主程序部分,你可以尝试运行它,看看generator是如何工作的:

python utils.py 

现在,在root文件夹中创建一个新的 Python 文件,命名为datasets.py,并添加以下代码:

 import tensorflow as tf 
 import cv2 
 import os 
 import random 

 from tensorflow.python.ops import data_flow_ops 
 from utils import lines_from_file 

 def sample_videos(data_reader, root_folder, num_samples,  
 num_frames): 
    image_paths = list() 
    labels = list() 
    while True: 
        if len(labels) >= num_samples: 
            break 
        line = next(data_reader) 
        video_folder, label, max_frames = line.strip().split(" ") 
        max_frames = int(max_frames) 
        label = int(label) 
        if max_frames > num_frames: 
            start_index = random.randint(0, max_frames - num_frames) 
            frame_paths = list() 
            for index in range(start_index, start_index +  
 num_frames): 
                frame_path = os.path.join(root_folder, video_folder,  
 "%04d.jpg" % index) 
                frame_paths.append(frame_path) 
            image_paths.append(frame_paths) 
            labels.append(label) 
    return image_paths, labels 

 if __name__ == "__main__": 
    num_frames = 5 
    root_folder = "/home/ubuntu/datasets/ucf101/train/" 
    data_reader =  
 lines_from_file("/home/ubuntu/datasets/ucf101/train.txt",  
 repeat=True) 
 image_paths, labels = sample_videos(data_reader,  
 root_folder=root_folder, 
 num_samples=3,  
 num_frames=num_frames) 
    print("image_paths", image_paths) 
    print("labels", labels) 

sample_videos函数易于理解。它将接收来自lines_from_file函数的generator对象,并使用next函数获取所需的样本。你可以看到,我们使用了random.randint方法来随机化起始帧的位置。

你可以运行主程序部分,使用以下命令查看sample_videos如何工作:

python datasets.py

到目前为止,我们已经将数据集文本文件读取到image_pathslabels变量中,它们是 Python 列表。在后续的训练过程中,我们将使用 TensorFlow 中的内建RandomShuffleQueue来将image_pathslabels入队。

现在,我们需要创建一个方法,在训练过程中使用它从RandomShuffleQueue获取数据,在多个线程中进行预处理,并将数据传送到batch_join函数中,以创建一个用于训练的迷你批次。

dataset.py文件中,添加以下代码:

 def input_pipeline(input_queue, batch_size=32, num_threads=8,  
 image_size=112): 
    frames_and_labels = [] 
    for _ in range(num_threads): 
        frame_paths, label = input_queue.dequeue() 
        frames = [] 
        for filename in tf.unstack(frame_paths): 
            file_contents = tf.read_file(filename) 
            image = tf.image.decode_jpeg(file_contents) 
            image = _aspect_preserving_resize(image, image_size) 
            image = tf.image.resize_image_with_crop_or_pad(image,  
            image_size, image_size) 
            image = tf.image.per_image_standardization(image) 
            image.set_shape((image_size, image_size, 3)) 
            frames.append(image) 
        frames_and_labels.append([frames, label]) 

    frames_batch, labels_batch = tf.train.batch_join( 
        frames_and_labels, batch_size=batch_size, 
        capacity=4 * num_threads * batch_size, 
    ) 
    return frames_batch, labels_batch 

在这段代码中,我们准备了一个名为frames_and_labels的数组,并使用num_threads迭代的 for 循环。这是一种非常方便的方式,能够在预处理过程中添加多线程支持。在每个线程中,我们将调用input_queuedequeue方法来获取frame_pathslabel。从前一节的sample_video函数中,我们知道frame_paths是一个选定视频帧的列表。因此,我们使用另一个 for 循环来遍历每一帧。在每一帧中,我们读取、调整大小并进行图像标准化。这部分与第九章的代码类似,巡航控制 - 自动化。在输入管道的末尾,我们添加了带有batch_size参数的frames_and_labels。返回的frames_batchlabels_batch将用于后续的训练流程。

最后,你需要添加以下代码,其中包含_aspect_preserving_resize函数:

 def _smallest_size_at_least(height, width, smallest_side): 
    smallest_side = tf.convert_to_tensor(smallest_side,  
 dtype=tf.int32) 

    height = tf.to_float(height) 
    width = tf.to_float(width) 
    smallest_side = tf.to_float(smallest_side) 

    scale = tf.cond(tf.greater(height, width), 
                    lambda: smallest_side / width, 
                    lambda: smallest_side / height) 
    new_height = tf.to_int32(height * scale) 
    new_width = tf.to_int32(width * scale) 
    return new_height, new_width 

 def _aspect_preserving_resize(image, smallest_side): 
    smallest_side = tf.convert_to_tensor(smallest_side,  
 dtype=tf.int32) 
    shape = tf.shape(image) 
    height = shape[0] 
    width = shape[1] 
    new_height, new_width = _smallest_size_at_least(height, width,  
 smallest_side) 
    image = tf.expand_dims(image, 0) 
    resized_image = tf.image.resize_bilinear(image, [new_height,  
 new_width], align_corners=False) 
    resized_image = tf.squeeze(resized_image) 
    resized_image.set_shape([None, None, 3]) 
    return resized_image 

这段代码与第九章中的代码相同,巡航控制 - 自动化

在下一节中,我们将创建一个深度神经网络架构,使用该架构执行 101 个类别的视频动作识别。

神经网络架构

在本章中,我们将创建一个神经网络,该网络将接受 10 帧视频作为输入,并输出 101 个动作类别的概率。我们将基于 TensorFlow 中的 conv3d 操作来创建该神经网络。这个网络的灵感来源于 D. Tran 等人的研究《使用 3D 卷积网络学习时空特征》。然而,我们简化了模型,使其在本章中更容易解释。我们还使用了一些 Tran 等人未提及的技术,例如批量归一化和 dropout。

现在,创建一个名为nets.py的新 Python 文件,并添加以下代码:

 import tensorflow as tf 
 from utils import print_variables, print_layers 
 from tensorflow.contrib.layers.python.layers.layers import  
 batch_norm 
 def inference(input_data, is_training=False): 
    conv1 = _conv3d(input_data, 3, 3, 3, 64, 1, 1, 1, "conv1") 
    pool1 = _max_pool3d(conv1, 1, 2, 2, 1, 2, 2, "pool1") 

    conv2 = _conv3d(pool1, 3, 3, 3, 128, 1, 1, 1, "conv2") 
    pool2 = _max_pool3d(conv2, 2, 2, 2, 2, 2, 2, "pool2") 

    conv3a = _conv3d(pool2, 3, 3, 3, 256, 1, 1, 1, "conv3a") 
    conv3b = _conv3d(conv3a, 3, 3, 3, 256, 1, 1, 1, "conv3b") 
    pool3 = _max_pool3d(conv3b, 2, 2, 2, 2, 2, 2, "pool3") 

    conv4a = _conv3d(pool3, 3, 3, 3, 512, 1, 1, 1, "conv4a") 
    conv4b = _conv3d(conv4a, 3, 3, 3, 512, 1, 1, 1, "conv4b") 
    pool4 = _max_pool3d(conv4b, 2, 2, 2, 2, 2, 2, "pool4") 

    conv5a = _conv3d(pool4, 3, 3, 3, 512, 1, 1, 1, "conv5a") 
    conv5b = _conv3d(conv5a, 3, 3, 3, 512, 1, 1, 1, "conv5b") 
    pool5 = _max_pool3d(conv5b, 2, 2, 2, 2, 2, 2, "pool5") 

    fc6 = _fully_connected(pool5, 4096, name="fc6") 
    fc7 = _fully_connected(fc6, 4096, name="fc7") 
    if is_training: 
        fc7 = tf.nn.dropout(fc7, keep_prob=0.5) 
    fc8 = _fully_connected(fc7, 101, name='fc8', relu=False) 

    endpoints = dict() 
    endpoints["conv1"] = conv1 
    endpoints["pool1"] = pool1 
    endpoints["conv2"] = conv2 
    endpoints["pool2"] = pool2 
    endpoints["conv3a"] = conv3a 
    endpoints["conv3b"] = conv3b 
    endpoints["pool3"] = pool3 
    endpoints["conv4a"] = conv4a 
    endpoints["conv4b"] = conv4b 
    endpoints["pool4"] = pool4 
    endpoints["conv5a"] = conv5a 
    endpoints["conv5b"] = conv5b 
    endpoints["pool5"] = pool5 
    endpoints["fc6"] = fc6 
    endpoints["fc7"] = fc7 
    endpoints["fc8"] = fc8 

    return fc8, endpoints 

 if __name__ == "__main__": 
    inputs = tf.placeholder(tf.float32, [None, 10, 112, 112, 3],  
 name="inputs") 
    outputs, endpoints = inference(inputs) 

    print_variables(tf.global_variables()) 
    print_variables([inputs, outputs]) 
    print_layers(endpoints) 

inference函数中,我们调用 _conv3d_max_pool3d_fully_connected来创建网络。这与之前章节中用于图像的 CNN 网络没有太大区别。在函数的末尾,我们还创建了一个名为endpoints的字典,它将在主部分中用于可视化网络架构。

接下来,让我们添加_conv3d_max_pool3d函数的代码:

 def _conv3d(input_data, k_d, k_h, k_w, c_o, s_d, s_h, s_w, name,  
 relu=True, padding="SAME"): 
    c_i = input_data.get_shape()[-1].value 
    convolve = lambda i, k: tf.nn.conv3d(i, k, [1, s_d, s_h, s_w,  
 1], padding=padding) 
    with tf.variable_scope(name) as scope: 
        weights = tf.get_variable(name="weights",  
 shape=[k_d, k_h, k_w, c_i, c_o], 
 regularizer = tf.contrib.layers.l2_regularizer(scale=0.0001), 

 initializer=tf.truncated_normal_initializer(stddev=1e-1,  
 dtype=tf.float32)) 
        conv = convolve(input_data, weights) 
        biases = tf.get_variable(name="biases",  
 shape=[c_o], dtype=tf.float32, 
 initializer = tf.constant_initializer(value=0.0)) 
        output = tf.nn.bias_add(conv, biases) 
        if relu: 
            output = tf.nn.relu(output, name=scope.name) 
        return batch_norm(output) 

 def _max_pool3d(input_data, k_d, k_h, k_w, s_d, s_h, s_w, name,  
 padding="SAME"): 
    return tf.nn.max_pool3d(input_data,  
 ksize=[1, k_d, k_h, k_w, 1], 
 strides=[1, s_d, s_h, s_w, 1], padding=padding, name=name) 

这段代码与之前的章节相似。然而,我们使用了内置的tf.nn.conv3dtf.nn.max_pool3d函数,而不是针对图像的tf.nn.conv2dtf.nn.max_pool3d函数。因此,我们需要添加k_ds_d参数,以提供关于滤波器深度的信息。此外,我们将从头开始训练这个网络,而不是使用任何预训练模型。因此,我们需要使用batch_norm函数将批量归一化添加到每一层。

让我们添加全连接层的代码:

 def _fully_connected(input_data, num_output, name, relu=True): 
    with tf.variable_scope(name) as scope: 
        input_shape = input_data.get_shape() 
        if input_shape.ndims == 5: 
            dim = 1 
            for d in input_shape[1:].as_list(): 
                dim *= d 
            feed_in = tf.reshape(input_data, [-1, dim]) 
        else: 
            feed_in, dim = (input_data, input_shape[-1].value) 
        weights = tf.get_variable(name="weights",  
 shape=[dim, num_output],  
 regularizer = tf.contrib.layers.l2_regularizer(scale=0.0001),                                   
 initializer=tf.truncated_normal_initializer(stddev=1e-1,  
 dtype=tf.float32)) 
        biases = tf.get_variable(name="biases", 
 shape=[num_output], dtype=tf.float32, 

 initializer=tf.constant_initializer(value=0.0)) 
        op = tf.nn.relu_layer if relu else tf.nn.xw_plus_b 
        output = op(feed_in, weights, biases, name=scope.name) 
        return batch_norm(output) 

这个函数与我们使用图像时的有所不同。首先,我们检查 input_shape.ndims 是否等于 5,而不是 4。其次,我们将批量归一化添加到输出中。

最后,让我们打开 utils.py 文件并添加以下 utility 函数:

 from prettytable import PrettyTable 
 def print_variables(variables): 
    table = PrettyTable(["Variable Name", "Shape"]) 
    for var in variables: 
        table.add_row([var.name, var.get_shape()]) 
    print(table) 
    print("") 

 def print_layers(layers): 
    table = PrettyTable(["Layer Name", "Shape"]) 
    for var in layers.values(): 
        table.add_row([var.name, var.get_shape()]) 
    print(table) 
    print("") 

现在我们可以运行 nets.py 来更好地理解网络的架构:

    python nets.py

在控制台结果的第一部分,你将看到如下表格:

    +------------------------------------+---------------------+
    |           Variable Name            |        Shape        |
    +------------------------------------+---------------------+
    |          conv1/weights:0           |   (3, 3, 3, 3, 64)  |
    |           conv1/biases:0           |        (64,)        |
    |       conv1/BatchNorm/beta:0       |        (64,)        |
    |   conv1/BatchNorm/moving_mean:0    |        (64,)        |
    | conv1/BatchNorm/moving_variance:0  |        (64,)        |
    |               ...                  |         ...         |
    |           fc8/weights:0            |     (4096, 101)     |
    |            fc8/biases:0            |        (101,)       |
    |        fc8/BatchNorm/beta:0        |        (101,)       |
    |    fc8/BatchNorm/moving_mean:0     |        (101,)       |
    |  fc8/BatchNorm/moving_variance:0   |        (101,)       |
    +------------------------------------+---------------------+ 

这些是网络中 variables 的形状。如你所见,每一层都有三个带有 BatchNorm 文本的 variables 被添加进去。这些 variables 增加了网络需要学习的总参数。然而,由于我们将从头开始训练,没有批量归一化的情况下,训练网络将更加困难。批量归一化还提高了网络对未见数据的正则化能力。

在控制台的第二个表格中,你将看到如下表格:

    +---------------------------------+----------------------+
    |          Variable Name          |        Shape         |
    +---------------------------------+----------------------+
    |             inputs:0            | (?, 10, 112, 112, 3) |
    | fc8/BatchNorm/batchnorm/add_1:0 |       (?, 101)       |
    +---------------------------------+----------------------+

这些是网络的输入和输出的形状。如你所见,输入包含 10 帧大小为 (112, 112, 3) 的视频帧,输出包含一个 101 个元素的向量。

在最后的表格中,你将看到输出在每一层经过网络时形状的变化:

    +------------------------------------+-----------------------+
    |             Layer Name             |         Shape         |
    +------------------------------------+-----------------------+
    |  fc6/BatchNorm/batchnorm/add_1:0   |       (?, 4096)       |
    |  fc7/BatchNorm/batchnorm/add_1:0   |       (?, 4096)       |
    |  fc8/BatchNorm/batchnorm/add_1:0   |        (?, 101)       |
    |               ...                  |         ...           |
    | conv1/BatchNorm/batchnorm/add_1:0  | (?, 10, 112, 112, 64) |
    | conv2/BatchNorm/batchnorm/add_1:0  |  (?, 10, 56, 56, 128) |
    +------------------------------------+-----------------------+

在前面的表格中,我们可以看到 conv1 层的输出与输入的大小相同,而 conv2 层的输出由于最大池化的作用发生了变化。

现在,让我们创建一个新的 Python 文件,命名为 models.py,并添加以下代码:

 import tensorflow as tf 

 def compute_loss(logits, labels): 
    labels = tf.squeeze(tf.cast(labels, tf.int32)) 

    cross_entropy =  
 tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits,  
 labels=labels) 
    cross_entropy_loss= tf.reduce_mean(cross_entropy) 
    reg_loss =  
 tf.reduce_mean(tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES 
 )) 

    return cross_entropy_loss + reg_loss, cross_entropy_loss,  
 reg_loss 

 def compute_accuracy(logits, labels): 
    labels = tf.squeeze(tf.cast(labels, tf.int32)) 
    batch_predictions = tf.cast(tf.argmax(logits, 1), tf.int32) 
    predicted_correctly = tf.equal(batch_predictions, labels) 
    accuracy = tf.reduce_mean(tf.cast(predicted_correctly,  
    tf.float32)) 
    return accuracy 

 def get_learning_rate(global_step, initial_value, decay_steps,  
 decay_rate): 
    learning_rate = tf.train.exponential_decay(initial_value,  
    global_step, decay_steps, decay_rate, staircase=True) 
    return learning_rate 

 def train(total_loss, learning_rate, global_step): 
    optimizer = tf.train.AdamOptimizer(learning_rate) 
    train_op = optimizer.minimize(total_loss, global_step) 
    return train_op 

这些函数创建计算 lossaccuracylearning rate 的操作,并执行训练过程。这与前一章相同,因此我们不再解释这些函数。

现在,我们有了训练网络以识别视频动作所需的所有函数。在下一部分中,我们将开始在单个 GPU 上的训练过程,并在 TensorBoard 上可视化结果。

单 GPU 训练过程

在脚本包中,创建一个名为 train.py 的新 Python 文件。我们将首先定义一些参数,如下所示:

 import tensorflow as tf 
 import os 
 import sys 
 from datetime import datetime 
 from tensorflow.python.ops import data_flow_ops 

 import nets 
 import models 
 from utils import lines_from_file 
 from datasets import sample_videos, input_pipeline 

 # Dataset 
 num_frames = 16 
 train_folder = "/home/ubuntu/datasets/ucf101/train/" 
 train_txt = "/home/ubuntu/datasets/ucf101/train.txt" 

 # Learning rate 
 initial_learning_rate = 0.001 
 decay_steps = 1000 
 decay_rate = 0.7 

 # Training 
 image_size = 112 
 batch_size = 24 
 num_epochs = 20 
 epoch_size = 28747 

 train_enqueue_steps = 100 
 min_queue_size = 1000 

 save_steps = 200  # Number of steps to perform saving checkpoints 
 test_steps = 20  # Number of times to test for test accuracy 
 start_test_step = 50 

 max_checkpoints_to_keep = 2 
 save_dir = "/home/ubuntu/checkpoints/ucf101" 

这些参数不言自明。现在,我们将定义一些用于训练的操作:

 train_data_reader = lines_from_file(train_txt, repeat=True) 

 image_paths_placeholder = tf.placeholder(tf.string, shape=(None,  
 num_frames), name='image_paths') 
 labels_placeholder = tf.placeholder(tf.int64, shape=(None,),  
 name='labels') 

 train_input_queue =  
 data_flow_ops.RandomShuffleQueue(capacity=10000, 

 min_after_dequeue=batch_size, 
 dtypes= [tf.string, tf.int64], 
 shapes= [(num_frames,), ()]) 

 train_enqueue_op =  
 train_input_queue.enqueue_many([image_paths_placeholder,  
 labels_placeholder]) 

 frames_batch, labels_batch = input_pipeline(train_input_queue,   
 batch_size=batch_size, image_size=image_size) 

 with tf.variable_scope("models") as scope: 
    logits, _ = nets.inference(frames_batch, is_training=True) 

 total_loss, cross_entropy_loss, reg_loss =  
 models.compute_loss(logits, labels_batch) 
 train_accuracy = models.compute_accuracy(logits, labels_batch) 

 global_step = tf.Variable(0, trainable=False) 
 learning_rate = models.get_learning_rate(global_step,  
 initial_learning_rate, decay_steps, decay_rate) 
 train_op = models.train(total_loss, learning_rate, global_step) 

在这段代码中,我们从文本文件中获取一个 generator 对象。然后,我们创建两个占位符用于 image_pathslabels,它们将被加入到 RandomShuffleQueue 中。我们在 datasets.py 中创建的 input_pipeline 函数将接收 RandomShuffleQueue 并返回一批 frames 和标签。最后,我们创建操作来计算损失、准确率,并执行训练操作。

我们还希望记录训练过程并在 TensorBoard 中可视化它。所以,我们将创建一些摘要:

 tf.summary.scalar("learning_rate", learning_rate) 
 tf.summary.scalar("train/accuracy", train_accuracy) 
 tf.summary.scalar("train/total_loss", total_loss) 
 tf.summary.scalar("train/cross_entropy_loss", cross_entropy_loss) 
 tf.summary.scalar("train/regularization_loss", reg_loss) 

 summary_op = tf.summary.merge_all() 

 saver = tf.train.Saver(max_to_keep=max_checkpoints_to_keep) 
 time_stamp = datetime.now().strftime("single_%Y-%m-%d_%H-%M-%S") 
 checkpoints_dir = os.path.join(save_dir, time_stamp) 
 summary_dir = os.path.join(checkpoints_dir, "summaries") 

 train_writer = tf.summary.FileWriter(summary_dir, flush_secs=10) 

 if not os.path.exists(save_dir): 
    os.mkdir(save_dir) 
 if not os.path.exists(checkpoints_dir): 
    os.mkdir(checkpoints_dir) 
 if not os.path.exists(summary_dir): 
    os.mkdir(summary_dir) 

savertrain_writer 分别负责保存检查点和摘要。现在,让我们通过创建 session 并执行训练循环来完成训练过程:

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

 with tf.Session(config=config) as sess: 
    coords = tf.train.Coordinator() 
    threads = tf.train.start_queue_runners(sess=sess, coord=coords) 

    sess.run(tf.global_variables_initializer()) 

    num_batches = int(epoch_size / batch_size) 

    for i_epoch in range(num_epochs): 
        for i_batch in range(num_batches): 
            # Prefetch some data into queue 
            if i_batch % train_enqueue_steps == 0: 
                num_samples = batch_size * (train_enqueue_steps + 1) 

                image_paths, labels =  
 sample_videos(train_data_reader, root_folder=train_folder, 

 num_samples=num_samples, num_frames=num_frames) 
                print("\nEpoch {} Batch {} Enqueue {}  
 videos".format(i_epoch, i_batch, num_samples)) 

                sess.run(train_enqueue_op, feed_dict={ 
                    image_paths_placeholder: image_paths, 
                    labels_placeholder: labels 
                }) 

            if (i_batch + 1) >= start_test_step and (i_batch + 1) %  
 test_steps == 0: 
                _, lr_val, loss_val, ce_loss_val, reg_loss_val,  
 summary_val, global_step_val, train_acc_val = sess.run([ 
                    train_op, learning_rate, total_loss,  
 cross_entropy_loss, reg_loss, 
                    summary_op, global_step, train_accuracy 
                ]) 
                train_writer.add_summary(summary_val, 
 global_step=global_step_val) 

                print("\nEpochs {}, Batch {} Step {}: Learning Rate  
 {} Loss {} CE Loss {} Reg Loss {} Train Accuracy {}".format( 
                    i_epoch, i_batch, global_step_val, lr_val,  
 loss_val, ce_loss_val, reg_loss_val, train_acc_val 
                )) 
            else: 
                _ = sess.run(train_op) 
                sys.stdout.write(".") 
                sys.stdout.flush() 

          if (i_batch + 1) > 0 and (i_batch + 1) % save_steps ==  0: 
                saved_file = saver.save(sess, 

 os.path.join(checkpoints_dir, 'model.ckpt'), 
                                        global_step=global_step) 
                print("Save steps: Save to file %s " % saved_file) 

    coords.request_stop() 
    coords.join(threads) 

这段代码非常直接。我们将使用 sample_videos 函数获取图像路径和标签的列表。然后,我们将调用 train_enqueue_op 操作将这些图像路径和标签添加到 RandomShuffleQueue 中。之后,训练过程可以通过使用 train_op 来运行,而无需使用 feed_dict 机制。

现在,我们可以通过在 root 文件夹中运行以下命令来启动训练过程:

export PYTHONPATH=.
python scripts/train.py

如果您的 GPU 内存不足以处理批大小为 32 时,您可能会看到 OUT_OF_MEMORY 错误。在训练过程中,我们创建了一个会话并设置了 gpu_options.allow_growth,因此您可以尝试更改 batch_size 来有效使用您的 GPU 内存。

训练过程需要几个小时才能收敛。我们将通过 TensorBoard 查看训练过程。

在您选择保存检查点的目录中,运行以下命令:

tensorboard --logdir .

现在,打开您的 web 浏览器并导航至 http://localhost:6006

使用单个 GPU 的正则化损失和总损失如下:

如您在这些图像中所见,训练准确率大约经过 10,000 步达到了训练数据的 100% 准确率。这 10,000 步在我们的机器上花费了 6 小时,您的配置可能会有所不同。

训练损失在下降,如果我们训练更长时间,它可能会进一步减少。然而,训练准确率在 10,000 步后几乎没有变化。

现在,让我们进入本章最有趣的部分。我们将使用多个 GPU 进行训练,并观察这对训练的帮助。

多 GPU 训练流程

在我们的实验中,我们将使用我们定制的机器,而不是 Amazon EC2。但是,您可以在任何有 GPU 的服务器上获得相同的结果。在本节中,我们将使用两块 Titan X GPU,每块 GPU 的批大小为 32。这样,我们可以在一个步骤中处理最多 64 个视频,而不是单 GPU 配置下的 32 个视频。

现在,让我们在 scripts 包中创建一个名为 train_multi.py 的新 Python 文件。在这个文件中,添加以下代码以定义一些参数:

 import tensorflow as tf 
 import os 
 import sys 
 from datetime import datetime 
 from tensorflow.python.ops import data_flow_ops 

 import nets 
 import models 
 from utils import lines_from_file 
 from datasets import sample_videos, input_pipeline 

 # Dataset 
 num_frames = 10 
 train_folder = "/home/aiteam/quan/datasets/ucf101/train/" 
 train_txt = "/home/aiteam/quan/datasets/ucf101/train.txt" 

 # Learning rate 
 initial_learning_rate = 0.001 
 decay_steps = 1000 
 decay_rate = 0.7 

 # Training 
 num_gpu = 2 

 image_size = 112 
 batch_size = 32 * num_gpu 
 num_epochs = 20 
 epoch_size = 28747 

 train_enqueue_steps = 50 

 save_steps = 200  # Number of steps to perform saving checkpoints 
 test_steps = 20  # Number of times to test for test accuracy 
 start_test_step = 50 

 max_checkpoints_to_keep = 2 
 save_dir = "/home/aiteam/quan/checkpoints/ucf101" 

这些参数与之前的 train.py 文件相同,唯一不同的是 batch_size。在本实验中,我们将使用数据并行策略,通过多个 GPU 进行训练。因此,批大小不再使用 32,而是使用 64。然后,我们将批次拆分成两部分,每部分由一块 GPU 处理。之后,我们将结合两块 GPU 的梯度来更新网络的权重和偏差。

接下来,我们将使用与之前相同的操作,具体如下:

 train_data_reader = lines_from_file(train_txt, repeat=True) 

 image_paths_placeholder = tf.placeholder(tf.string, shape=(None,  
 num_frames), name='image_paths') 
 labels_placeholder = tf.placeholder(tf.int64, shape=(None,),  
 name='labels') 

 train_input_queue =  
 data_flow_ops.RandomShuffleQueue(capacity=10000, 

 min_after_dequeue=batch_size, 
 dtypes= [tf.string, tf.int64], 
 shapes= [(num_frames,), ()]) 

 train_enqueue_op =  
 train_input_queue.enqueue_many([image_paths_placeholder,  
 labels_placeholder]) 

 frames_batch, labels_batch = input_pipeline(train_input_queue,  
 batch_size=batch_size, image_size=image_size) 

 global_step = tf.Variable(0, trainable=False) 
 learning_rate = models.get_learning_rate(global_step,  
 initial_learning_rate, decay_steps, decay_rate) 

现在,不再使用 models.train 创建训练操作,

我们将创建一个优化器,并在每个 GPU 上计算梯度。

 optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate) 

 total_gradients = [] 

 frames_batch_split = tf.split(frames_batch, num_gpu) 
 labels_batch_split = tf.split(labels_batch, num_gpu) 
 for i in range(num_gpu): 
    with tf.device('/gpu:%d' % i): 
        with tf.variable_scope(tf.get_variable_scope(), reuse=(i >  
 0)): 
            logits_split, _ = nets.inference(frames_batch_split[i],  
 is_training=True) 
            labels_split = labels_batch_split[i] 
            total_loss, cross_entropy_loss, reg_loss =  
 models.compute_loss(logits_split, labels_split) 
            grads = optimizer.compute_gradients(total_loss) 
            total_gradients.append(grads) 
            tf.get_variable_scope().reuse_variables() 

 with tf.device('/cpu:0'): 
    gradients = models.average_gradients(total_gradients) 
    train_op = optimizer.apply_gradients(gradients, global_step) 

    train_accuracy = models.compute_accuracy(logits_split,   
 labels_split) 

梯度将在每个 GPU 上计算,并添加到名为 total_gradients 的列表中。最终的梯度将在 CPU 上使用 average_gradients 计算,我们将很快创建这个函数。然后,通过调用优化器的 apply_gradients 来创建训练操作。

现在,让我们在 root 文件夹中的 models.py 文件中添加以下函数来计算 average_gradient

 def average_gradients(gradients): 
    average_grads = [] 
    for grad_and_vars in zip(*gradients): 
        grads = [] 
        for g, _ in grad_and_vars: 
            grads.append(tf.expand_dims(g, 0)) 

        grad = tf.concat(grads, 0) 
        grad = tf.reduce_mean(grad, 0) 

        v = grad_and_vars[0][1] 
        grad_and_var = (grad, v) 
        average_grads.append(grad_and_var) 
    return average_grads 

现在,在 train_multi.py 文件中,我们将创建 saversummaries 操作,用来保存 checkpointssummaries,就像以前一样:

 tf.summary.scalar("learning_rate", learning_rate) 
 tf.summary.scalar("train/accuracy", train_accuracy) 
 tf.summary.scalar("train/total_loss", total_loss) 
 tf.summary.scalar("train/cross_entropy_loss", cross_entropy_loss) 
 tf.summary.scalar("train/regularization_loss", reg_loss) 

 summary_op = tf.summary.merge_all() 

 saver = tf.train.Saver(max_to_keep=max_checkpoints_to_keep) 
 time_stamp = datetime.now().strftime("multi_%Y-%m-%d_%H-%M-%S") 
 checkpoints_dir = os.path.join(save_dir, time_stamp) 
 summary_dir = os.path.join(checkpoints_dir, "summaries") 

 train_writer = tf.summary.FileWriter(summary_dir, flush_secs=10) 

 if not os.path.exists(save_dir): 
    os.mkdir(save_dir) 
 if not os.path.exists(checkpoints_dir): 
    os.mkdir(checkpoints_dir) 
 if not os.path.exists(summary_dir): 
    os.mkdir(summary_dir) 

最后,让我们添加训练循环来训练网络:

 config = tf.ConfigProto(allow_soft_placement=True) 
 config.gpu_options.allow_growth = True 

 sess = tf.Session(config=config) 
 coords = tf.train.Coordinator() 
 threads = tf.train.start_queue_runners(sess=sess, coord=coords) 

 sess.run(tf.global_variables_initializer()) 

 num_batches = int(epoch_size / batch_size) 

 for i_epoch in range(num_epochs): 
    for i_batch in range(num_batches): 
        # Prefetch some data into queue 
        if i_batch % train_enqueue_steps == 0: 
            num_samples = batch_size * (train_enqueue_steps + 1) 
            image_paths, labels = sample_videos(train_data_reader,  
 root_folder=train_folder, 

 num_samples=num_samples, num_frames=num_frames) 
            print("\nEpoch {} Batch {} Enqueue {} 
 videos".format(i_epoch, i_batch, num_samples)) 

            sess.run(train_enqueue_op, feed_dict={ 
                image_paths_placeholder: image_paths, 
                labels_placeholder: labels 
            }) 

        if (i_batch + 1) >= start_test_step and (i_batch + 1) %  
 test_steps == 0: 
            _, lr_val, loss_val, ce_loss_val, reg_loss_val, 
 summary_val, global_step_val, train_acc_val = sess.run([ 
                train_op, learning_rate, total_loss, 
 cross_entropy_loss, reg_loss, 
                summary_op, global_step, train_accuracy 
            ]) 
            train_writer.add_summary(summary_val,  
 global_step=global_step_val) 

            print("\nEpochs {}, Batch {} Step {}: Learning Rate {} 
 Loss {} CE Loss {} Reg Loss {} Train Accuracy {}".format( 
                i_epoch, i_batch, global_step_val, lr_val, loss_val, 
 ce_loss_val, reg_loss_val, train_acc_val 
            )) 
        else: 
            _ = sess.run([train_op]) 
            sys.stdout.write(".") 
            sys.stdout.flush() 

        if (i_batch + 1) > 0 and (i_batch + 1) % save_steps == 0: 
            saved_file = saver.save(sess, 
                                    os.path.join(checkpoints_dir,  
 'model.ckpt'), 
                                    global_step=global_step) 
            print("Save steps: Save to file %s " % saved_file) 

 coords.request_stop() 
 coords.join(threads) 

训练循环与之前类似,不同之处在于我们为会话配置添加了 allow_soft_placement=True 选项。这个选项将允许 TensorFlow 在必要时更改 variables 的位置。

现在,我们可以像之前一样运行训练脚本:

python scripts/train_multi.py

经过几小时的训练后,我们可以查看 TensorBoard 来比较结果:

图 04—多个 GPU 训练过程在 Tensorboard 上的绘图

如你所见,在我们的计算机上,多个 GPU 训练大约经过 6000 步后,约四小时内达到了 100% 的准确率。这几乎将训练时间缩短了一半。

现在,让我们看看这两种训练策略的对比:

图 05—在 TensorBoard 上,单个 GPU 和多个 GPU 结果并排对比的绘图

橙色线表示多个 GPU 的结果,蓝色线表示单个 GPU 的结果。我们可以看到,多个 GPU 设置比单个 GPU 更早地实现了更好的结果,虽然差异不大。然而,随着 GPU 数量的增加,我们可以实现更快的训练。在 Amazon EC2 的 P1 实例上,甚至有 8 个和 16 个 GPU。如果我们在大规模数据集上进行训练,例如 ActivityNet 或 Sports 1M,多个 GPU 的训练优势将更加明显,因为单个 GPU 需要很长时间才能收敛。

在下一部分,我们将快速了解另一个 Amazon 服务——机械土耳其。

机械土耳其概览

机械土耳其是一项服务,允许我们创建和管理在线人类智能任务,这些任务将由人类工作者完成。很多任务是人类比计算机更擅长的。因此,我们可以利用这项服务来支持我们的机器学习系统。

你可以在 www.mturk.com 查看这个系统。这里是该服务的网站:

下面是一些你可以用来支持机器学习系统的任务示例:

  • 数据集标注:你通常会有很多未标记的数据,可以利用机械土耳其帮助你为机器学习工作流构建一致的真实标签。

  • 生成数据集:你可以让工作人员构建大量的训练数据。例如,我们可以要求工作人员为自然语言系统创建文本翻译或聊天句子。你还可以要求他们为评论标注情感。

除了标注,Mechanical Turk 还可以清理你凌乱的数据集,使其准备好进行训练、数据分类和元数据标注。你甚至可以使用此服务让他们评判你的系统输出。

总结

我们已经查看了 Amazon EC2 服务,了解我们可以使用多少种服务器类型。然后,我们创建了一个神经网络,在单个 GPU 上进行人类视频动作识别。之后,我们应用了数据并行策略来加速训练过程。最后,我们简要了解了 Mechanical Turk 服务。我们希望你能够利用这些服务将你的机器学习系统提升到一个更高的水平。

第十一章:更深入 - 21 个问题

在本章中,我们将介绍 21 个实际生活中的问题,您可以使用深度学习和 TensorFlow 来解决。我们将从讨论一些公共的大规模数据集和竞赛开始。然后,我们将展示一些精彩的 TensorFlow 项目。我们还将介绍一些在其他深度学习框架中完成的有趣项目,以便您获得灵感并实现自己的 TensorFlow 解决方案。最后,我们将介绍一种简单的技术,将 Caffe 模型转换为 TensorFlow 模型,并介绍如何使用高级 TensorFlow 库 TensorFlow-Slim。

在本章中,我们将讨论以下主题:

  • 大规模公共数据集和竞赛

  • 精彩的 TensorFlow 项目

  • 一些其他框架的深度学习启发项目

  • 将 Caffe 模型转换为 TensorFlow 模型

  • 介绍 TensorFlow-Slim

数据集和挑战

在本节中,我们将向您展示一些流行的数据集和竞赛。

问题 1 - ImageNet 数据集

项目链接: image-net.org/

ImageNet 是一个大型视觉识别挑战赛,自 2010 年以来每年举办一次。数据集按照 WorkNet 层级结构进行组织。数据集中包含超过一千万个带有手动标注标签的图像 URL,以指示图片中的物体。至少有一百万张图像包括了边界框。

ImageNet 挑战赛每年举办一次,评估算法在以下三个问题中的表现:

  • 1,000 个类别的物体定位。

  • 200 个完全标注类别的目标检测。

  • 30 个完全标注类别的视频目标检测。2017 年 7 月 17 日,2017 年挑战赛的结果公布,出现了许多先进且有趣的算法。

问题 2 - COCO 数据集

项目链接: mscoco.org/

COCO 是一个用于图像识别、分割和标注的数据集,由微软赞助。该数据集中有 80 个对象类别,包含超过 300,000 张图片和 200 万个实例。每年还会举办检测、标注和关键点的挑战赛。

问题 3 - Open Images 数据集

项目链接: github.com/openimages/dataset

Open Images 是谷歌推出的一个新数据集,包含超过九百万个 URL,涵盖超过 6000 个类别。每张图像都经过谷歌的视觉模型处理,并由人工验证。截至 2017 年 7 月 20 日,该数据集还包含超过两百万个边界框标注,涵盖 600 多个物体。

不同之处在于,Open Images 涵盖了比其他数据集更多的现实生活中的物体,这在开发现实应用时非常有用。

问题 4 - YouTube-8M 数据集

项目链接: research.google.com/youtube8m/

YouTube-8M 是谷歌提供的一个大规模视频数据集,包含 700 万个视频 URL,涵盖 4,716 个类别和 450,000 小时的视频。谷歌还提供了预计算的先进音视频特征,可以让你基于这些特征轻松构建模型。从原始视频进行训练可能需要数周,这在正常情况下是不现实的。该数据集的目标是实现视频理解、表示学习、噪声数据建模、迁移学习和视频领域适应。

问题 5 - AudioSet 数据集

项目链接:research.google.com/audioset/

AudioSet 是谷歌提供的一个大规模音频事件数据集,包含 632 个音频事件类别,并收录了超过 210 万条人工标注的音频片段。音频类别涵盖了从人类和动物的声音到乐器声以及常见的日常环境声音。利用该数据集,你可以构建一个系统,用于音频事件识别,支持音频理解、安全应用等众多领域。

问题 6 - LSUN 挑战赛

项目链接:lsun.cs.princeton.edu/2017/

LSUN 挑战赛提供了一个大规模的场景理解数据集,涵盖了三个主要问题:

  • 场景分类

  • 街景图像的分割任务

  • 显著性预测

在场景分类问题中,算法的预期输出是图像中最可能的场景类别。撰写时,有 10 个不同的类别,如卧室、教室和餐厅。在分割问题中,你可以尝试解决像素级分割和特定实例的分割。在显著性预测问题中,目标是预测人在场景图像中的注视位置。

问题 7 - MegaFace 数据集

项目链接:megaface.cs.washington.edu/

MegaFace 提供了一个大规模的人脸识别数据集。MegaFace 数据集分为三部分:

  • 训练集

  • 测试集

  • 干扰样本

训练集包含 470 万张照片,涵盖 672,057 个独特身份。测试集包含来自 FaceScrub 和 FGNet 数据集的图像。干扰集包含 100 万张照片,来自 690,572 个独特用户。目前,MegaFace 网站上有两个挑战。在挑战 1 中,你可以使用任何数据集进行训练,并通过 100 万个干扰样本来测试你的方法。你的方法需要区分一组已知的人,并将干扰样本分类为未知的人。在挑战 2 中,你将使用包含 672K 独特身份的训练集进行训练,并通过 100 万个干扰样本进行测试。MegaFace 是目前(撰写时)最大的人脸识别数据集。

问题 8 - 2017 数据科学大赛挑战

项目链接:www.kaggle.com/c/data-science-bowl-2017

Data Science Bowl 2017 是一个价值百万美元的挑战,专注于肺癌检测。在数据集中,您将获得超过一千张高风险患者的 CT 图像。此挑战的目标是创建一个自动化系统,能够判断患者是否会在一年内被诊断为肺癌。这是一个非常有趣且重要的项目,未来能够拯救成千上万的人。

问题 9 - 星际争霸游戏数据集

项目链接: github.com/TorchCraft/StarData

这是截至本书写作时最大的《星际争霸——母巢之战》重放数据集。该数据集包含了超过 60,000 场游戏,大小为 365GB,1535 百万帧,496 百万玩家动作。这个数据集最适合那些想要研究 AI 游戏玩法的人。

基于 TensorFlow 的项目

在本节中,我们将介绍几个在 TensorFlow 中实现并开源在 Github 上的问题。我们建议你看看这些项目,学习如何提升你的 TensorFlow 技能。

问题 10 - 人体姿态估计

项目链接: github.com/eldar/pose-tensorflow

该项目是人类姿态估计中 Deep Cut 和 ArtTrack 的开源实现。该项目的目标是共同解决检测和姿态估计任务。我们可以将这种方法应用于各种场景,如安防中的人脸检测或人体动作理解。该项目还为进一步的人体形状估计研究提供了很好的起点,应用领域包括虚拟试穿或服装推荐。

问题 11 - 目标检测 - YOLO

项目链接: github.com/thtrieu/darkflow

目标检测是计算机视觉中的一个有趣问题。解决这个问题的方法有很多。YOLO,由 Joseph Redmon 等人提出,是其中一种最先进的技术。YOLO 使用深度神经网络实现实时目标检测。YOLO 的第 2 版可以实时识别多达 9,000 种不同的物体,并具有很高的准确性。原始的 YOLO 项目是用 darknet 框架编写的。

在 TensorFlow 中,有一个很棒的 YOLO 实现,叫做 darkflow。darkflow 仓库甚至提供了一个工具,可以让你将模型导出并在移动设备上部署。

问题 12 - 目标检测 - Faster RCNN

项目链接: github.com/smallcorgi/Faster-RCNN_TF

Faster RCNN 是另一种用于目标检测的最先进方法。该方法提供了高精度的结果,并且激发了许多其他问题的方法。Faster RCNN 的推理速度不如 YOLO 快。然而,如果你需要高精度的检测结果,可能会想要考虑使用 Faster RCNN。

问题 13 - 人物检测 - tensorbox

项目链接:github.com/Russell91/TensorBox

Tensorbox 是 Russell Stewart 和 Mykhaylo Andriluka 提出的一个方法的 TensorFlow 实现。这个方法的目标与前面提到的几种方法稍有不同。Tensorbox 侧重于解决人群人物检测的问题。它们使用一个递归的 LSTM 层来生成边界框的序列,并定义了一种新的损失函数,这个损失函数作用于检测结果集合。

问题 14 - Magenta

项目链接:github.com/tensorflow/magenta

Magenta 是 Google Brain 团队的一个项目,专注于使用深度学习进行音乐和艺术生成。这是一个非常活跃的代码库,涵盖了许多有趣问题的实现,比如图像风格化、旋律生成或生成草图。你可以访问以下链接,获取 Magenta 的模型:

github.com/tensorflow/magenta/tree/master/magenta/models

问题 15 - Wavenet

项目链接:github.com/ibab/tensorflow-wavenet

WaveNet 是 Google Deep Mind 提出的用于音频生成的神经网络架构。WaveNet 被训练生成原始音频波形,并在语音合成和音频生成中取得了良好的结果。根据 Deep Mind 的说法,WaveNet 在英语和普通话的语音合成任务中,将传统方法和人类水平表现之间的差距缩小了超过 50%。

问题 16 - Deep Speech

项目链接:github.com/mozilla/DeepSpeech

Deep Speech 是一个开源的语音转文本引擎,基于百度的研究论文。语音转文本是一个非常有趣的问题,Deep Speech 是解决这一问题的最先进方法之一。通过 Mozilla 提供的 TensorFlow 实现,你甚至可以学习如何在多台机器上使用 TensorFlow。然而,仍然存在一个问题,即个人研究人员无法访问与大公司相同的大规模语音转文本数据集。因此,尽管我们可以使用 Deep Speech 或自己实现它,但仍然很难为生产环境提供一个良好的模型。

有趣的项目

在本节中,我们将向你展示一些在其他深度学习框架中实现的有趣项目。这些项目在解决非常困难的问题上取得了显著的成果。你可能会想挑战自己,将这些方法实现到 TensorFlow 中。

问题 17 - 交互式深度着色 - iDeepColor

项目链接:richzhang.github.io/ideepcolor/

互动深度图像着色是 Richard Zhang、Jun-Yan Zun 等人正在开展的研究,旨在实现用户引导的图像着色。在该系统中,用户可以为图像中的某些点提供几种颜色提示,网络将根据用户的输入以及从大规模数据中学到的语义信息传播颜色。这种着色可以通过单次前向传播实时执行。

问题 18 - 微小面部检测器

项目链接:github.com/peiyunh/tiny

本项目是由 Peiyun Hu 和 Deva Ramanan 提出的面部检测器,专注于识别图像中的小面部。虽然大多数面部检测器只关注图像中的大物体,但这一微小面部检测方法能够处理非常小的面部,并且与之前的方法相比,在 WIDER FACE 数据集上减少了两倍的错误率。

问题 19 - 人物搜索

项目链接:github.com/ShuangLI59/person_search

本项目是 Tong Xiao 等人提出的论文的实现,专注于人物检测和重新识别问题。该项目可用于视频监控。现有的人物重新识别方法主要假设人物已经被裁剪并对齐。然而,在现实场景中,人物检测算法可能无法完美地提取人物的裁剪区域,从而降低识别准确性。在本项目中,作者在受 Faster RCNN 启发的全新架构中联合解决检测和识别问题。当前项目使用 Caffe 深度学习框架实现。

问题 20 - 面部识别 - MobileID

项目链接:github.com/liuziwei7/mobile-id

本项目提供了一种极快的面部识别系统,能够以 250 帧每秒的速度运行,同时保持高准确性。该模型通过使用最先进的面部识别系统 DeepID 的输出进行训练。然而,移动 ID 模型的运算速度极快,适用于处理能力和内存受限的情况。

问题 21 - 问答系统 - DrQA

项目链接:github.com/facebookresearch/DrQA

DrQA 是 Facebook 推出的一个开放领域问答系统。DrQA 专注于解决机器阅读任务,在该任务中,模型会试图理解维基百科文档,并为用户的任何问题提供答案。当前项目是用 PyTorch 实现的。你可能会对在 TensorFlow 中实现我们自己的解决方案感兴趣。

Caffe 到 TensorFlow

在这一部分,我们将向你展示如何利用来自 Caffe Model Zoo 的多个预训练模型(github.com/BVLC/caffe/wiki/Model-Zoo)。这里有许多适用于不同任务的 Caffe 模型,涵盖各种架构。将这些模型转换为 TensorFlow 后,你可以将其作为你架构的一部分,或者可以针对不同任务微调我们的模型。使用这些预训练模型作为初始权重是训练的有效方法,而不是从头开始训练。我们将展示如何使用 Saumitro Dasgupta 提供的caffe-to-tensorflow方法,详见github.com/ethereon/caffe-tensorflow

然而,Caffe 和 TensorFlow 之间有很多差异。这项技术仅支持 Caffe 中一部分层类型。即便如此,仍有一些 Caffe 架构,如 ResNet、VGG 和 GoogLeNet,已被该项目的作者验证。

首先,我们需要使用git clone命令克隆caffe-tensorflow仓库:

ubuntu@ubuntu-PC:~/github$ git clone https://github.com/ethereon/caffe-tensorflow
Cloning into 'caffe-tensorflow'...
remote: Counting objects: 479, done.
remote: Total 479 (delta 0), reused 0 (delta 0), pack-reused 479
Receiving objects: 100% (510/510), 1.71 MiB | 380.00 KiB/s, done.
Resolving deltas: 100% (275/275), done.
Checking connectivity... done.

然后,我们需要切换到caffe-to-tensorflow目录,并运行转换的 Python 脚本,以查看一些帮助消息:

cd caffe-tensorflow
python convert.py -h
The resulting console will look like this:
usage: convert.py [-h] [--caffemodel CAFFEMODEL]
                  [--data-output-path DATA_OUTPUT_PATH]
                  [--code-output-path CODE_OUTPUT_PATH] [-p PHASE]
                  def_path

positional arguments:
def_path              Model definition (.prototxt) path

optional arguments:
  -h, --help            show this help message and exit
  --caffemodel CAFFEMODEL
                        Model data (.caffemodel) path
  --data-output-path DATA_OUTPUT_PATH
                        Converted data output path
  --code-output-path CODE_OUTPUT_PATH
                        Save generated source to this path
  -p PHASE, --phase PHASE
                        The phase to convert: test (default) or train

根据此帮助消息,我们可以了解convert.py脚本的参数。总结来说,我们将使用这个convert.py脚本,通过标志code-output-path在 TensorFlow 中创建网络架构,并通过标志data-output-path转换预训练权重。

在开始转换模型之前,我们需要从该项目的贡献者那里获取一些拉取请求。当前的主分支存在一些问题,我们无法使用最新版本的 TensorFlow(本文写作时为 1.3 版)和 python-protobuf(本文写作时为 3.4.0 版)。因此,我们将通过以下拉取请求获取代码:

github.com/ethereon/caffe-tensorflow/pull/105

github.com/ethereon/caffe-tensorflow/pull/133

你需要打开上述链接查看拉取请求是否已合并。如果它仍然处于open状态,你需要遵循下一部分。否则,你可以跳过已合并的pull请求。

首先,我们将获取拉取请求105中的代码:

ubuntu@ubuntu-PC:~/github$ git pull origin pull/105/head
remote: Counting objects: 33, done.
remote: Total 33 (delta 8), reused 8 (delta 8), pack-reused 25
Unpacking objects: 100% (33/33), done.
From https://github.com/ethereon/caffe-tensorflow
* branch            refs/pull/105/head -> FETCH_HEAD
Updating d870c51..ccd1a52
Fast-forward
.gitignore                               |  5 +++++
convert.py                               |  8 ++++++++
examples/save_model/.gitignore           | 11 ++++++++++
examples/save_model/READMD.md            | 17 ++++++++++++++++
examples/save_model/__init__.py          |  0
examples/save_model/save_model.py        | 51 ++++++++++++++++++++++++++++++++++++++++++++++
kaffe/caffe/{caffepb.py => caffe_pb2.py} |  0
kaffe/caffe/resolver.py                  |  4 ++--
kaffe/tensorflow/network.py              |  8 ++++----
9 files changed, 98 insertions(+), 6 deletions(-)
create mode 100644 examples/save_model/.gitignore
create mode 100644 examples/save_model/READMD.md
create mode 100644 examples/save_model/__init__.py
create mode 100755 examples/save_model/save_model.py
rename kaffe/caffe/{caffepb.py => caffe_pb2.py} (100%)

然后,从拉取请求133开始:

- git pull origin pull/133/head
remote: Counting objects: 31, done.
remote: Total 31 (delta 20), reused 20 (delta 20), pack-reused 11
Unpacking objects: 100% (31/31), done.
From https://github.com/ethereon/caffe-tensorflow
* branch            refs/pull/133/head -> FETCH_HEAD
Auto-merging kaffe/tensorflow/network.py
CONFLICT (content): Merge conflict in kaffe/tensorflow/network.py
Auto-merging .gitignore
CONFLICT (content): Merge conflict in .gitignore
Automatic merge failed; fix conflicts and then commit the result.

如你所见,kaffe/tensorflow/network.py文件中存在一些冲突。我们将向你展示如何解决这些冲突,如下所示。

首先,我们将在第 137 行解决冲突:

我们将从第 137 行到第 140 行删除 HEAD 部分。最终结果将如下所示:

接下来,我们将在第 185 行解决冲突:

我们还需要从第 185 行到第 187 行删除 HEAD 部分。最终结果将如下所示:

caffe-to-tensorflow目录中,有一个名为 examples 的目录,包含了 MNIST 和 ImageNet 挑战的代码和数据。我们将向您展示如何使用 MNIST 模型。ImageNet 挑战与之大致相同。

首先,我们将使用以下命令将 MNIST 架构从 Caffe 转换为 TensorFlow:

    ubuntu@ubuntu-PC:~/github$ python ./convert.py examples/mnist/lenet.prototxt --code-output-path=./mynet.py
    The result will look like this:

    ------------------------------------------------------------
        WARNING: PyCaffe not found!
        Falling back to a pure protocol buffer implementation.
        * Conversions will be drastically slower.
        * This backend is UNTESTED!
    ------------------------------------------------------------

    Type                 Name                                          Param               Output
    ----------------------------------------------------------------------------------------------
    Input                data                                             --      (64, 1, 28, 28)
    Convolution          conv1                                            --     (64, 20, 24, 24)
    Pooling              pool1                                            --     (64, 20, 12, 12)
    Convolution          conv2                                            --       (64, 50, 8, 8)
    Pooling              pool2                                            --       (64, 50, 4, 4)
    InnerProduct         ip1                                              --      (64, 500, 1, 1)
    InnerProduct         ip2                                              --       (64, 10, 1, 1)
    Softmax              prob                                             --       (64, 10, 1, 1)
    Converting data...
    Saving source...
    Done.

接下来,我们将使用以下命令将预训练的 MNIST Caffe 模型examples/mnist/lenet_iter_10000.caffemodel转换为 TensorFlow:

 ubuntu@ubuntu-PC:~/github$ python ./convert.py  
 examples/mnist/lenet.prototxt --caffemodel  
 examples/mnist/lenet_iter_10000.caffemodel --data-output- 
 path=./mynet.npy

结果如下所示:

    ------------------------------------------------------------
        WARNING: PyCaffe not found!
        Falling back to a pure protocol buffer implementation.
        * Conversions will be drastically slower.
        * This backend is UNTESTED!
    ------------------------------------------------------------

    Type                 Name                                          Param               Output
    ----------------------------------------------------------------------------------------------
    Input                data                                             --      (64, 1, 28, 28)
    Convolution          conv1                                 
(20, 1, 5, 5)     (64, 20, 24, 24)
    Pooling              pool1                                            --     (64, 20, 12, 12)
    Convolution          conv2                               
 (50, 20, 5, 5)       (64, 50, 8, 8)
    Pooling              pool2                                            --       (64, 50, 4, 4)
    InnerProduct         ip1                                   
   (500, 800)      (64, 500, 1, 1)
    InnerProduct         ip2                                      
 (10, 500)       (64, 10, 1, 1)
    Softmax              prob                                             --       (64, 10, 1, 1)
    Converting data...
    Saving data...
    Done.

如您所见,这些命令将在当前目录中创建一个名为mynet.py的 Python 文件和一个名为mynet.npynumpy文件。我们还需要将当前目录添加到PYTHONPATH,以便后续代码可以导入mynet.py

ubuntu@ubuntu-PC:~/github$ export PYTHONPATH=$PYTHONPATH:.
ubuntu@ubuntu-PC:~/github$ python examples/mnist/finetune_mnist.py
....
('Iteration: ', 900, 0.0087626642, 1.0)
('Iteration: ', 910, 0.018495116, 1.0)
('Iteration: ', 920, 0.0029206357, 1.0)
('Iteration: ', 930, 0.0010091728, 1.0)
('Iteration: ', 940, 0.071255416, 1.0)
('Iteration: ', 950, 0.045163739, 1.0)
('Iteration: ', 960, 0.005758767, 1.0)
('Iteration: ', 970, 0.012100354, 1.0)
('Iteration: ', 980, 0.12018739, 1.0)
('Iteration: ', 990, 0.079262167, 1.0)

每行中的最后两个数字是微调过程的损失和准确率。您可以看到,使用来自 Caffe 模型的预训练权重,微调过程可以轻松达到 100% 的准确率。

现在,我们将查看finetune_mnist.py文件,看看如何使用预训练权重。

首先,他们使用以下代码导入mynet python:

    from mynet import LeNet as MyNet  

然后,他们为imageslabels创建了一些占位符,并使用层ip2计算loss,如下所示:

 images = tf.placeholder(tf.float32, [None, 28, 28, 1]) 
 labels = tf.placeholder(tf.float32, [None, 10]) 
 net = MyNet({'data': images}) 

 ip2 = net.layers['ip2'] 
 pred = net.layers['prob'] 

 loss =  
 tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=ip2,  
 labels=labels), 0) 
 Finally, they load the numpy file into the graph, using the load  
 method in the network class. 
 with tf.Session() as sess: 
    # Load the data 
    sess.run(tf.global_variables_initializer()) 
    net.load('mynet.npy', sess) 

之后,微调过程将独立于 Caffe 框架进行。

TensorFlow-Slim

TensorFlow-Slim 是一个轻量级库,用于在 TensorFlow 中定义、训练和评估复杂模型。使用 TensorFlow-Slim 库,我们可以通过提供许多高级层、变量和正则化器,轻松构建、训练和评估模型。我们建议您查看以下链接中的 TensorFlow-Slim 库:github.com/tensorflow/tensorflow/tree/master/tensorflow/contrib/slim

还提供了许多使用 TensorFlow-Slim 的预训练模型。您可以通过以下链接利用高级 TensorFlow 层和模型:

github.com/tensorflow/tensorflow/tree/master/tensorflow/contrib/slim

总结

在本章中,我们提供了许多有趣的挑战和问题,您可以尝试解决并从中学习,以提升您的 TensorFlow 技能。在本章末尾,我们还指导您将 Caffe 模型转换为 TensorFlow,并向您介绍了高级 TensorFlow 库 TensorFlow-Slim。

第十二章:高级安装

深度学习涉及大量的矩阵乘法,图形处理单元GPU)是学习深度学习时非常重要的方面。没有 GPU,实验过程可能需要一天或更长时间。有了良好的 GPU,我们可以快速迭代深度学习网络和大型训练数据集,并在短时间内运行多个实验。使用 TensorFlow 时,我们可以轻松地在单个 GPU 或多个 GPU 上工作。然而,一旦涉及到 GPU,大多数机器学习平台的安装过程都非常复杂。

本章将讨论 GPU,并重点介绍逐步 CUDA 设置和基于 GPU 的 TensorFlow 安装。我们将从安装 Nvidia 驱动程序、CUDA 工具包和 cuDNN 库开始。然后,我们将使用pip安装支持 GPU 的 TensorFlow。最后,我们将展示如何使用 Anaconda 进一步简化安装过程。

安装

本章将以一台配备 Nvidia Titan X GPU 的 Ubuntu 16.06 计算机为例进行操作。

我们建议您使用 Ubuntu 14.04 或 16.06 版本,以避免出现其他问题。

GPU 的选择超出了本章的范围。然而,您必须选择一款具有高内存容量的 Nvidia 设备,以便与 CPU 相比充分发挥 GPU 的优势。目前,AMD 的 GPU 尚未被 TensorFlow 和大多数其他深度学习框架官方支持。在撰写本文时,Windows 可以在 Python 3.5 或 Python 3.6 上使用 TensorFlow 的 GPU 版本。然而,从 TensorFlow 1.2 版本开始,TensorFlow 不再支持 macOS 上的 GPU。如果您使用的是 Windows,我们建议您按照以下链接的官方教程进行操作:www.tensorflow.org/install/install_windows

安装 Nvidia 驱动程序

在 Ubuntu 中安装 Nvidia 驱动程序有多种方法。本节将展示使用专有 GPU 驱动程序 PPA 的最简单方法,PPA 提供稳定的 Nvidia 图形驱动程序更新。

首先,打开终端并运行以下命令,将PPA添加到 Ubuntu 中:

sudo add-apt-repository ppa:graphics-drivers/ppa
sudo apt update  

现在,我们需要选择一个 Nvidia 驱动程序版本进行安装。运行以下命令查看您机器上的最新版本:

sudo apt-cache search nvidia

上述命令的结果可能如下所示:

如你所见,我机器上的最新驱动程序是 375.66,它与文本中的 NVIDIA 二进制驱动程序行对齐。现在,我们可以使用以下命令安装 Nvidia 驱动程序版本 375.66:

sudo apt-get install nvidia-375 

上述命令的结果可能如下所示:

安装完成后,您应该会看到如下屏幕:

现在,我们将安装 Nvidia 的 CUDA 工具包。

安装 CUDA 工具包

首先,我们需要打开 Nvidia 网站以下载 CUDA 工具包。访问 developer.nvidia.com/cuda-downloads。你将看到如下屏幕:

然后,选择 Linux | x86_64 | Ubuntu | 16.04 | runfile(local),如下面的截图所示:

接下来,点击“下载 (1.4 GB)”按钮以下载安装程序。安装程序大小为 1.4 GB,下载可能需要一些时间。下载完成后,打开终端,切换到包含安装程序的文件夹,并运行以下命令:

sudo sh cuda_8.0.61_375.26_linux.run

在命令行提示符中,你将看到最终用户许可协议:

你可以使用方向键在协议中导航。否则,你可以按 :q 并看到以下屏幕:

现在,你可以输入 accept 来接受协议。之后,你需要回答一些问题,如下图所示:

你可能注意到,在此提示中我们不会安装 Nvidia 驱动程序,因为我们已经在上一节中安装了最新的驱动程序。当安装完成时,你将看到如下屏幕:

现在,打开你的 ~/.bashrc 文件,并在文件末尾添加以下行:

    export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/cuda/lib64/ 

我们已经成功将 CUDA 工具包安装到机器上。你可以尝试以下命令查看你的显卡信息:

nvidia-smi

我们机器上的结果如下所示:

安装 cuDNN

为了使用支持 GPU 的 TensorFlow,你需要安装一个名为 cuDNN 的 Nvidia 库。首先,你需要访问 Nvidia 网站,并从 developer.nvidia.com/cudnn 下载 cuDNN 库。

你可能需要注册一个新的 Nvidia 账户。在你登录 Nvidia 网站并打开 cuDNN 链接后,你将看到如下屏幕:

如你所见,cuDNN 有多个版本,我们将使用适用于 CUDA 8.0 的 cuDNN v5.1,这是 TensorFlow 所需的 cuDNN 版本。现在,你可以点击 cuDNN v5.1 Library for Linux 链接来下载该库:

你可以继续使用终端,并使用以下命令在你的机器上安装 cuDNN:

tar -xf cudnn-8.0-linux-x64-v5.1.tgz 
cd cuda
sudo cp -P include/cudnn.h /usr/include/
sudo cp -P lib64/libcudnn* /usr/lib/x86_64-linux-gnu/
sudo chmod a+r /usr/lib/x86_64-linux-gnu/libcudnn*

安装 TensorFlow

设置完成后,我们可以通过 pip 工具轻松安装支持 GPU 的 TensorFlow,具体命令如下:

sudo pip install tensorflow-gpu

命令的结果应该如下所示:

验证支持 GPU 的 TensorFlow

现在,您可以在命令行中输入 python,并输入以下 Python 命令来查看 TensorFlow 是否能识别您的 GPU:

    import tensorflow as tf 
    tf.Session()

结果应该如下图所示:

恭喜!TensorFlow 现在可以与您的 GPU 配合使用了。我们的 GPU 被识别为 GeForce GTX TITAN X,具有 11.92 GB 内存。在接下来的部分,我们将展示推荐的多版本 TensorFlow 和库(如 OpenCV)共存的方法。

使用 Anaconda 配合 TensorFlow

在工作中,您可能会遇到需要在同一台机器上使用多个版本的 TensorFlow 的情况,例如 TensorFlow 1.0 或 TensorFlow 1.2。我们可能需要在 Python 2.7 或 3.0 上使用 TensorFlow。通过之前的安装,我们已经成功将 TensorFlow 安装在系统的 Python 中。现在,我们将展示如何使用 Anaconda 在同一台机器上创建多个工作环境。使用 Anaconda,我们甚至可以使用不同版本的其他流行库,如 OpenCVNumPyscikit-learn

首先,我们需要从 conda.io/miniconda.html 下载并安装 miniconda。在我们的例子中,我们选择 Python 2.7 64 位 bash 安装包,因为我们希望将 Python 2.7 作为默认 Python。然而,我们稍后可以创建使用 Python 2.7 或 Python 3 的环境。我们需要运行以下命令来启动安装程序:

bash Miniconda3-latest-Linux-x86_64.sh

我们需要接受最终用户许可协议:

之后,我们可以继续安装。结果应该如下所示:

最后,我们需要加载 .bashrc 文件以启动 Anaconda:

source ~/.bashrc

在本章的源代码中,我们已经提供了一些环境配置,您可以使用这些配置来创建所需的环境。

这是一个使用 Python 2.7、OpenCV 3 和 TensorFlow 1.2.1(支持 GPU)的环境。配置文件名为 env2.yml

您可以轻松地将 python=2.7 更改为 python=3,将 opencv3 更改为 opencv,以分别使用 Python 3 和 OpenCV 2.4。

现在,让我们运行以下命令来创建环境:

conda env create -f env2.yml

结果应该如下所示:

接下来,您可以输入 source activate env2 来激活环境。

最后,我们需要像之前一样验证 TensorFlow:

您可能会注意到前面图片左上角的 (env2)。它显示了当前环境的名称。第二行的 Python 版本是 2.7.13,并且是由 conda-forge 打包的。

现在,您可以创建多个不同的环境以供工作流使用。以下是一个名为 env3 的环境示例,包含 Python 3 和 OpenCV 2.4:

总结

在本章中,我们讨论了在机器学习工作流中使用 GPU 的优势,特别是在深度学习中。接着,我们展示了 Nvidia 驱动程序、CUDA 工具包、cuDNN 和支持 GPU 的 TensorFlow 的逐步安装过程。我们还介绍了我们推荐的工作流,以便使用多个版本的 TensorFlow 和其他库。

posted @ 2025-07-10 11:38  绝不原创的飞龙  阅读(63)  评论(0)    收藏  举报