TensorFlow-2-0-计算机视觉秘籍-全-

TensorFlow 2.0 计算机视觉秘籍(全)

原文:annas-archive.org/md5/cf3ce16c27a13f4ce55f8e29a1bf85e1

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

2019 年 TensorFlow 2.x 的发布是深度学习和人工智能领域最盛大且最受期待的事件之一,因为它带来了对这个流行且相关框架的久违改进,主要集中在简化和易用性上。

Keras 被作为官方 TensorFlow 高级 API,支持在急切执行和基于图执行之间来回切换(得益于tf.function),以及通过tf.data创建复杂数据管道,这些只是 TensorFlow 2.x 带来的一些重要新功能。

在本书中,你将发现大量实用的示例,帮助你在计算机视觉领域的深度学习应用中,利用这些进步。我们将涵盖广泛的应用,从图像分类到更具挑战性的任务,如目标检测、图像分割以及自动化机器学习AutoML)。

到本书结束时,你将准备好并有足够的信心应对任何计算机视觉问题,得益于 TensorFlow 2.x 的无价帮助!

本书适用对象

本书适用于计算机视觉开发者、计算机视觉工程师和深度学习从业者,尤其是那些寻找计算机视觉中常见问题解决方案的人。你将学习如何使用现代机器学习技术和深度学习架构来完成各种计算机视觉任务。要求具备 Python 编程和计算机视觉的基础知识。

本书涵盖内容

第一章TensorFlow 2.x 计算机视觉入门,概述了基本的深度学习概念,同时也首次介绍了一些重要的 TensorFlow 2.x 特性,如 Keras 和tf.data.Dataset API。它还教你如何完成一些常见且必要的任务,如保存和加载模型,以及可视化网络架构。最后,章节通过实现一个简单的图像分类器作结。

第二章执行图像分类,深入探讨了深度神经网络在计算机视觉中最常见的应用——图像分类。它探索了常见的分类种类,如二分类和多分类,然后转向多标签分类的示例,并介绍了使用迁移学习和 TensorFlow Hub 的现成解决方案。

第三章利用迁移学习发挥预训练网络的优势,重点介绍迁移学习,这是一种强大的技术,可以重复利用在庞大数据集上预训练的网络,从而提高开发生产力和深度学习驱动的计算机视觉应用的性能。本章首先让你使用预训练的网络作为特征提取器。接着,你将学习如何通过一种叫做增量学习的过程,将深度学习与传统机器学习算法结合。最后,本章通过两个微调示例作结:第一个使用 Keras API,第二个依赖于 TensorFlow Hub。

第四章利用 DeepDream、神经风格迁移和图像超分辨率增强和美化图像,聚焦于计算机视觉中深度神经网络的一些有趣和不那么传统的应用,特别是 DeepDream、神经风格迁移和图像超分辨率。

第五章使用自编码器减少噪声,讲解了自编码器,这是一种在图像修复、逆向图像搜索索引和图像去噪等领域广泛应用的复合架构。它首先介绍了自编码器的密集型和卷积变体,然后解释了若干应用,如逆向图像搜索引擎和异常值检测。

第六章生成模型与对抗攻击,向你介绍了许多生成对抗网络GANs)的示例和应用。本章以一个示例结束,展示如何对卷积神经网络执行对抗攻击。

第七章使用 CNN 和 RNN 为图像生成描述,重点介绍了如何结合卷积神经网络和循环神经网络,生成图像的文字描述。

第八章通过图像分割实现图像的细粒度理解,重点讲解图像分割,这是图像分类的细粒度版本,作用于像素级别。它涵盖了开创性的分割架构,如 U-Net 和 Mask-RCNN。

第九章通过目标检测定位图像中的元素,涵盖了复杂而常见的目标检测任务。它介绍了基于图像金字塔和滑动窗口的传统方法,以及更现代的解决方案,如 YOLO。章节中还详细解释了如何利用 TensorFlow 目标检测 API,在自定义数据集上训练最先进的模型。

第十章将深度学习的力量应用于视频,扩展了深度神经网络在视频中的应用。在这里,你将看到如何检测情绪、识别动作和生成视频帧的示例。

第十一章通过 AutoML 简化网络实现,探讨了使用 Autokeras 这一基于 TensorFlow 2.x 的实验性库,开启了 AutoML 这一令人兴奋的子领域。Autokeras 使用神经网络架构搜索NAS)来为给定问题找到最佳模型。本章首先探讨了 Autokeras 的基本功能,最后通过 AutoML 创建一个年龄和性别预测工具。

第十二章提升性能,详细介绍了许多可以用来提升网络性能的不同技术,从简单但强大的方法,如使用集成方法,到更先进的技术,如使用 GradientTape 来根据项目的具体需求定制训练过程。

为了充分利用本书的内容

你需要安装 TensorFlow 2 的版本。本书中的所有示例都已经在 macOS X 和 Ubuntu 20.04 上使用 TensorFlow 2.3 进行实现和测试,但它们应该也适用于未来的稳定版本。请注意,Windows 系统不受支持。

虽然并非绝对必要,但强烈建议访问支持 GPU 的机器,无论是在本地还是云端,因为这样可以大幅缩短示例的运行时间。

如果你正在使用本书的数字版本,建议你自己输入代码或通过 GitHub 仓库(下节中提供的链接)访问代码。这样做将有助于避免与复制粘贴代码相关的潜在错误

因为这是一本以实践为导向的书,专注于解决各种实际问题,我鼓励你在任何特别的食谱中,拓展你对任何感兴趣的主题的知识。在每个食谱的另见部分,你会找到链接、参考资料以及推荐阅读或扩展点,它们将巩固你对示例中解释的技术的理解。

下载示例代码文件

你可以从 GitHub 下载本书的示例代码文件,地址是github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook。如果代码有更新,将会在现有的 GitHub 仓库中进行更新。

我们还在github.com/PacktPublishing/提供了其他代码包,来自我们丰富的书籍和视频目录。快去看看吧!

《实战代码》

本书的《实战代码》视频可以在bit.ly/2NmdZ5G观看。

下载彩色图像

我们还提供了一份包含本书中使用的截图/图表的彩色图像的 PDF 文件。你可以在此下载:static.packt-cdn.com/downloads/9781838829131_ColorImages.pdf

使用的约定

本书中使用了多种文本约定。

正文中的代码:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。例如:“使用image_generator,我们将直接从存储图像的目录中选择并显示一批随机的 10 张图像。”

代码块如下所示:

iterator = (image_generator
           .flow_from_directory(directory=data_directory, 
                                 batch_size=10))
for batch, _ in iterator:
plt.figure(figsize=(5, 5))
for index, image in enumerate(batch, start=1):
ax = plt.subplot(5, 5, index)
plt.imshow(image)
plt.axis(‘off’)
plt.show()
break

当我们希望特别引起你对代码块中某一部分的注意时,相关的行或项会被加粗显示:

[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)

任何命令行输入或输出都如下所示:

$ pip install tensorflow-hub Pillow
$ pip install tensorflow-datasets tqdm

粗体:表示一个新术语、一个重要的词汇或在屏幕上看到的词汇。例如,菜单或对话框中的词汇会以这种方式出现在文本中。以下是一个例子:“从管理面板中选择系统信息。”

提示或重要说明

在本章节的后续食谱中,我们将继续使用我们刚刚处理过的修改版斯坦福汽车数据集。

章节

在本书中,你会发现几个经常出现的标题(准备工作如何操作…工作原理…更多内容…另见)。

为了给出清晰的操作指南,请按照以下方式使用这些部分:

准备工作

本节告诉你在该食谱中会遇到什么内容,并描述如何设置任何所需的软硬件或前期设置。

如何操作…

本部分包含遵循食谱所需的步骤。

工作原理…

本部分通常包含对上一节所发生内容的详细说明。

更多内容…

本部分包含关于食谱的附加信息,帮助你更好地理解食谱。

另见

本部分提供了指向其他有用信息的链接,帮助你更好地完成食谱。

联系我们

我们欢迎读者提供反馈意见。

customercare@packtpub.com

勘误表:尽管我们已尽最大努力确保内容的准确性,但错误仍然可能发生。如果你在本书中发现错误,我们将不胜感激你能向我们报告。请访问 www.packtpub.com/support/errata,选择你的书籍,点击“勘误表提交表格”链接,并填写相关细节。

copyright@packt.com,并提供相关链接。

如果你有意成为作者:如果你在某个领域有专长,并且有兴趣写作或为书籍贡献内容,请访问 authors.packtpub.com

书评

请留下评论。在您阅读并使用本书后,为什么不在您购买该书的网站上留下评论呢?潜在的读者可以根据您的客观评价做出购买决定,我们在 Packt 可以了解您对我们产品的看法,而我们的作者也能看到您对他们书籍的反馈。谢谢!

如需了解有关 Packt 的更多信息,请访问packt.com

第一章:第一章:使用 TensorFlow 2.x 进行计算机视觉入门

TensorFlow 2.x 最棒的特点之一是,它终于将 Keras 纳入了高层 API。这为什么如此重要呢?虽然 Keras 和 TensorFlow 之间已经有很好的兼容性,但它们依然是独立的库,拥有不同的开发周期,这导致了频繁的兼容性问题。如今,这两个极为流行的工具关系正式确立,它们将朝着同一个方向发展,遵循同一条路线图,实现无缝的互操作性。最终,Keras 就是 TensorFlow,TensorFlow 也就是 Keras。

或许这次合并最大的优势就是,通过使用 Keras 的高层功能,我们在性能上丝毫没有妥协。简单来说,Keras 代码已经准备好用于生产!

除非某个项目的具体要求另有规定,否则在本书的大多数配方中,我们将依赖 TensorFlow 的 Keras API。

这一决策背后的原因有两个:

  • Keras 更易于理解和使用。

  • 它是使用 TensorFlow 2.x 开发的推荐方式。

在本章中,我们将涵盖以下配方:

  • 使用 Keras API 的基本构建模块

  • 使用 Keras API 加载图像

  • 使用 tf.data.Dataset API 加载图像

  • 保存和加载模型

  • 可视化模型架构

  • 创建一个基本的图像分类器

我们开始吧!

技术要求

对于本章内容,您需要安装并配置好 TensorFlow 2.x。如果能访问到 GPU,无论是物理 GPU 还是通过云服务提供商提供的 GPU,您的体验将更加愉快。在每个配方的 准备工作 部分,您都能找到完成它所需的具体前期步骤和依赖项。最后,本章中所有的代码都可以在本书的 GitHub 仓库中找到,地址为 github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch1

请查看以下链接,观看“代码实战”视频:

bit.ly/39wkpGN

使用 Keras API 的基本构建模块

Keras 是 TensorFlow 2.x 的官方高层 API,并且强烈建议在实验性和生产级代码中使用。因此,在本配方中,我们将通过创建一个非常简单的全连接神经网络来回顾 Keras 的基本构建模块。

准备好了吗?我们开始吧!

准备工作

在最基本的层面上,安装好 TensorFlow 2.x 就能满足所有需求。

如何操作…

在接下来的章节中,我们将逐步讲解完成此配方所需的步骤。我们开始吧:

  1. 从 Keras API 导入所需的库:

    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import LabelBinarizer
    from tensorflow.keras import Input
    from tensorflow.keras.datasets import mnist
    from tensorflow.keras.layers import Dense
    from tensorflow.keras.models import Model
    from tensorflow.keras.models import Sequential
    
  2. 使用 Sequential API 创建模型,通过将层列表传递给 Sequential 构造函数。每一层中的数字对应其包含的神经元或单元数:

    layers = [Dense(256, input_shape=(28 * 28 * 1,), 
                    activation='sigmoid'),
              Dense(128, activation='sigmoid'),
              Dense(10, activation='softmax')]
    sequential_model_list = Sequential(layers)
    
  3. 使用 add() 方法逐层添加模型。每一层中的数字对应其包含的神经元或单元数:

    sequential_model = Sequential()
    sequential_model.add(Dense(256, 
                         input_shape=(28 * 28 * 1,), 
                         activation='sigmoid'))
    sequential_model.add(Dense(128, activation='sigmoid'))
    sequential_model.add(Dense(10, activation='softmax'))
    
  4. 使用功能 API 创建模型。每一层中的数字对应其包含的神经元或单元数:

    input_layer = Input(shape=(28 * 28 * 1,))
    dense_1 = Dense(256, activation='sigmoid')(input_layer)
    dense_2 = Dense(128, activation='sigmoid')(dense_1)
    predictions = Dense(10, activation='softmax')(dense_2)
    functional_model = Model(inputs=input_layer, 
                             outputs=predictions)
    
  5. 使用面向对象的方法,通过子类化 tensorflow.keras.models.Model 来创建模型。每一层中的数字对应其包含的神经元或单元数:

    class ClassModel(Model):
        def __init__(self):
            super(ClassModel, self).__init__()
            self.dense_1 = Dense(256, activation='sigmoid')
            self.dense_2 = Dense(256, activation='sigmoid')
            self.predictions = Dense(10,activation='softmax')
        def call(self, inputs, **kwargs):
            x = self.dense_1(inputs)
            x = self.dense_2(x)
      return self.predictions(x)
    class_model = ClassModel()
    
  6. 准备数据,以便我们可以训练之前定义的所有模型。我们必须将图像重塑为向量格式,因为这是全连接网络所期望的格式:

    (X_train, y_train), (X_test, y_test) = mnist.load_data()
    X_train = X_train.reshape((X_train.shape[0], 28 * 28 * 
                               1))
    X_test = X_test.reshape((X_test.shape[0], 28 * 28 * 
                              1))
    X_train = X_train.astype('float32') / 255.0
    X_test = X_test.astype('float32') / 255.0
    
  7. 对标签进行独热编码,以消除任何不必要的排序偏差:

    label_binarizer = LabelBinarizer()
    y_train = label_binarizer.fit_transform(y_train)
    y_test = label_binarizer.fit_transform(y_test)
    
  8. 将 20% 的数据用于验证:

    X_train, X_valid, y_train, y_valid = train_test_split(X_train, y_train, train_size=0.8)
    
  9. 编译、训练这些模型 50 个 epochs,并在测试集上进行评估:

    models = {
        'sequential_model': sequential_model,
        'sequential_model_list': sequential_model_list,
        'functional_model': functional_model,
        'class_model': class_model
    }
    for name, model in models.items():
        print(f'Compiling model: {name}')
        model.compile(loss='categorical_crossentropy', 
                      optimizer='adam', 
                      metrics=['accuracy'])
        print(f'Training model: {name}')
        model.fit(X_train, y_train,
                  validation_data=(X_valid, y_valid),
                  epochs=50,
                  batch_size=256,
                  verbose=0)
        _, accuracy = model.evaluate(X_test, y_test, 
                                     verbose=0)
        print(f'Testing model: {name}. \nAccuracy: 
              {accuracy}')
        print('---')
    

在 50 个 epochs 后,所有三个模型应该在测试集上达到约 98% 的准确率。

工作原理如下…

在前一节中,我们讨论了构建大多数基于 TensorFlow 2.x 的深度学习计算机视觉项目所需的基本构建模块。

首先,我们导入了 Keras API,这是 TensorFlow 第二版的高级接口。我们了解到所有与 Keras 相关的功能都位于 tensorflow 包内。

接下来,我们发现 TensorFlow 2.x 在定义模型时提供了很大的灵活性。特别是,我们有两个主要的 API 可以用来构建模型:

  • 符号化:也被称为声明式 API,它允许我们将模型定义为有向无环图(DAG),其中每一层构成一个节点,层之间的交互或连接是边缘。这个 API 的优点是可以通过绘图或打印其架构来检查模型;框架会运行兼容性检查,减少运行时错误的概率;如果模型编译通过,它就会运行。另一方面,其主要缺点是不适合非 DAG 架构(具有循环的网络),例如 Tree-LSTMs。

  • 命令式:也称为模型子类化 API,这种 API 是一种更符合 Python 编程习惯、对开发者友好的指定模型方式。与其符号化的对应方式相比,它也允许在前向传播中具有更多的灵活性。该 API 的优点是,开发模型与其他面向对象任务没有区别,这加快了尝试新想法的速度;使用 Python 的内置结构指定控制流很容易;它适用于非 DAG 架构,如树形 RNN。缺点是,由于架构隐藏在类内部,复用性降低;几乎不进行层间兼容性检查,因此将大部分调试责任从框架转移到开发者身上;并且由于层之间的相互联系信息不可用,透明性降低。

我们使用了 Sequential 和 Functional API 定义了相同的架构,分别对应符号化或声明式的网络实现方式,并且还使用命令式方法定义了第三种方式。

为了明确说明,无论我们采用哪种方法,最终这三种网络是相同的,我们在著名的MNIST数据集上进行了训练和评估,最终在测试集上达到了 98%的不错准确率。

另见

如果你对 Tree-LSTM(树形 LSTM)感兴趣,可以阅读首次介绍它们的论文,点击这里查看:https://nlp.stanford.edu/pubs/tai-socher-manning-acl2015.pdf。

使用 Keras API 加载图像

在这个教程中,我们将学习如何使用 Keras API 加载图像,这在计算机视觉中是一个非常重要的任务,因为我们总是需要处理视觉数据。具体来说,我们将学习如何打开、浏览和可视化单张图像,以及一批图像。此外,我们还将学习如何通过编程下载数据集。

准备工作

Keras 依赖于Pillow库来处理图像。你可以通过pip轻松安装它:

$> pip install Pillow

开始吧!

如何实现…

现在,让我们开始本教程:

  1. 导入必要的包:

    import glob
    import os
    import tarfile
    import matplotlib.pyplot as plt
    from tensorflow.keras.preprocessing.image import ImageDataGenerator
    from tensorflow.keras.preprocessing.image 
    import load_img, img_to_array
    from tensorflow.keras.utils import get_file
    
  2. 定义CINIC-10数据集的 URL 和目标路径,这是著名CIFAR-10数据集的替代品:

    DATASET_URL = 'https://datashare.is.ed.ac.uk/bitstream/handle/10283/3192/CINIC-10.tar.gz?sequence=4&isAllowed=y'
    DATA_NAME = 'cinic10'
    FILE_EXTENSION = 'tar.gz'
    FILE_NAME = '.'.join([DATA_NAME, FILE_EXTENSION])
    
  3. 下载并解压数据。默认情况下,它将存储在~/.keras/datasets/<FILE_NAME>中:

    downloaded_file_location = get_file(origin=DATASET_URL, fname=FILE_NAME, extract=False)
    # Build the path to the data directory based on the location of the downloaded file.
    data_directory, _ = downloaded_file_location.rsplit(os.path.sep, maxsplit=1)
    data_directory = os.path.sep.join([data_directory, 
                                       DATA_NAME])
    # Only extract the data if it hasn't been extracted already
    if not os.path.exists(data_directory):
        tar = tarfile.open(downloaded_file_location)
        tar.extractall(data_directory)
    
  4. 加载所有图像路径并打印找到的图像数量:

    data_pattern = os.path.sep.join([data_directory, 
                                     '*/*/*.png'])
    image_paths = list(glob.glob(data_pattern))
    print(f'There are {len(image_paths):,} images in the 
          dataset')
    

    输出应该如下所示:

    There are 270,000 images in the dataset
    
  5. 从数据集中加载单张图像并打印其元数据:

    sample_image = load_img(image_paths[0])
    print(f'Image type: {type(sample_image)}')
    print(f'Image format: {sample_image.format}')
    print(f'Image mode: {sample_image.mode}')
    print(f'Image size: {sample_image.size}')
    

    输出应该如下所示:

    Image type: <class 'PIL.PngImagePlugin.PngImageFile'>
    Image format: PNG
    Image mode: RGB
    Image size: (32, 32)
    
  6. 将图像转换为NumPy数组:

    sample_image_array = img_to_array(sample_image)
    print(f'Image type: {type(sample_image_array)}')
    print(f'Image array shape: {sample_image_array.shape}')
    

    这是输出:

    Image type: <class 'numpy.ndarray'>
    Image array shape: (32, 32, 3)
    
  7. 使用matplotlib显示图像:

    plt.imshow(sample_image_array / 255.0)
    

    这给我们带来了以下图像:

    图 1.1 – 示例图像

    图 1.1 – 示例图像

  8. 使用ImageDataGenerator加载一批图像。与前一步一样,每张图像将被重新缩放到[0, 1]的范围内:

    image_generator = ImageDataGenerator(horizontal_flip=True, rescale=1.0 / 255.0)
    
  9. 使用image_generator,我们将从存储图像的目录中直接挑选并显示一批随机的 10 张图像:

    iterator = (image_generator
            .flow_from_directory(directory=data_directory, 
                                     batch_size=10))
    for batch, _ in iterator:
        plt.figure(figsize=(5, 5))
        for index, image in enumerate(batch, start=1):
            ax = plt.subplot(5, 5, index)
            plt.imshow(image)
            plt.axis('off')
        plt.show()
        break
    

    显示的批次如下所示:

图 1.2 – 图像批次

图 1.2 – 图像批次

让我们看看这一切是如何工作的。

它是如何工作的……

首先,我们借助get_file()函数下载了一个视觉数据集,该函数默认将文件存储在我们选择的文件名下,并保存在~/.keras/datasets目录中。如果文件已存在于此位置,get_file()会智能地避免重新下载。

接下来,我们使用untar解压了CINIC-10数据集。尽管这些步骤并非加载图像所必需(我们可以手动下载和解压数据集),但通常将尽可能多的步骤自动化是个好主意。

然后我们用load_img()加载了一张图像到内存中,这个函数底层使用了Pillow库。由于该函数返回的结果格式不是神经网络可以理解的格式,我们使用img_to_array()将其转换为NumPy数组。

最后,为了批量加载图像而不是一个一个地加载,我们使用了ImageDataGenerator,它已被配置为同时对每张图像进行归一化处理。ImageDataGenerator能够做更多的事情,我们通常会在需要实现数据增强时使用它,但在这个示例中,我们只使用它一次性从磁盘加载 10 张图像,得益于flow_from_directory()方法。最后需要说明的是,尽管这个方法返回的是一批图像和标签,但我们忽略了标签,因为我们只对图像感兴趣。

另见

要了解更多关于使用 Keras 处理图像的信息,请参考官方文档:https://www.tensorflow.org/api_docs/python/tf/keras/preprocessing/image。有关CINIC-10数据集的更多信息,请访问此链接:datashare.is.ed.ac.uk/handle/10283/3192

使用 tf.data.Dataset API 加载图像

在本示例中,我们将学习如何使用tf.data.Dataset API 加载图像,这是 TensorFlow 2.x 带来的最重要创新之一。其函数式接口以及高效的优化,使其在大规模项目中成为比传统 Keras API 更好的选择,尤其是在效率和性能至关重要的场景下。

具体来说,我们将学习如何打开、探索和可视化单张图像,以及一批图像。此外,我们还将学习如何通过编程下载数据集。

如何做到这一点……

让我们开始这个示例:

  1. 首先,我们需要导入本示例所需的所有包:

    import os
    import tarfile
    import matplotlib.pyplot as plt
    import numpy as np
    import tensorflow as tf
    from tensorflow.keras.utils import get_file
    
  2. 定义CINIC-10数据集的 URL 和存储路径,这是CIFAR-10数据集的一个替代品:

    DATASET_URL = 'https://datashare.is.ed.ac.uk/bitstream/handle/10283/3192/CINIC-10.tar.gz?sequence=4&isAllowed=y'
    DATA_NAME = 'cinic10'
    FILE_EXTENSION = 'tar.gz'
    FILE_NAME = '.'.join([DATA_NAME, FILE_EXTENSION])
    
  3. 下载并解压数据。默认情况下,它将存储在~/keras/dataset/<FILE_NAME>路径下:

    downloaded_file_location = get_file(origin=DATASET_URL, fname=FILE_NAME, extract=False)
    # Build the path to the data directory based on the location of the downloaded file.
    data_directory, _ = downloaded_file_location.rsplit(os.path.sep, maxsplit=1)
    data_directory = os.path.sep.join([data_directory, 
                                      DATA_NAME])
    # Only extract the data if it hasn't been extracted already
    if not os.path.exists(data_directory):
        tar = tarfile.open(downloaded_file_location)
        tar.extractall(data_directory)
    
  4. 使用类似 glob 模式的方式创建图像路径的数据集:

    data_pattern = os.path.sep.join([data_directory, '*/*/*.png'])
    image_dataset = tf.data.Dataset.list_files(data_pattern)
    
  5. 从数据集中获取一个路径,并用它读取相应的图像:

    for file_path in image_dataset.take(1):
        sample_path = file_path.numpy()
    sample_image = tf.io.read_file(sample_path)
    
  6. 尽管图像现在已经加载到内存中,我们仍然需要将其转换为神经网络可以处理的格式。为此,我们必须将其从 PNG 格式解码为NumPy数组,如下所示:

    sample_image = tf.image.decode_png(sample_image, 
                                       channels=3)
    sample_image = sample_image.numpy()
    
  7. 使用matplotlib显示图像:

    plt.imshow(sample_image / 255.0)
    

    这是结果:

    图 1.3 – 示例图像

    图 1.3 – 示例图像

    图 1.3 – 示例图像

  8. image_dataset的前 10 个元素,解码并归一化它们,然后使用matplotlib进行显示:

    plt.figure(figsize=(5, 5))
    for index, image_path in enumerate(image_dataset.take(10), start=1):
        image = tf.io.read_file(image_path)
        image = tf.image.decode_png(image, channels=3)
        image = tf.image.convert_image_dtype(image, 
                                             np.float32)
        ax = plt.subplot(5, 5, index)
        plt.imshow(image)
        plt.axis('off')
    plt.show()
    plt.close()
    

    这是输出结果:

图 1.4 – 图像批次

图 1.4 – 图像批次

让我们更详细地解释一下。

工作原理……

首先,我们使用get_file()辅助函数下载了CINIC-10数据集,该函数默认将获取的文件保存在~/.keras/datasets目录下,并使用我们指定的文件名。如果文件已经下载过,get_files()将不会再次下载。

由于 CINIC-10 是压缩文件,我们使用untar提取了它的内容。当然,每次加载图像时并不需要执行这些步骤,因为我们可以手动下载并解压数据集,但将尽可能多的步骤自动化是一个良好的实践。

为了将图像加载到内存中,我们创建了一个包含其文件路径的数据集,这使得我们几乎可以使用相同的流程来显示单张或多张图像。我们通过路径加载图像到内存,然后将其从源格式(本教程中是 PNG)解码,转换为NumPy数组,并根据需要进行预处理。

最后,我们取了数据集中前 10 张图像,并使用matplotlib进行了显示。

另见

如果你想了解更多关于tf.data.Dataset API 的内容,请参阅官方文档:www.tensorflow.org/api_docs/python/tf/data/Dataset。关于 CINIC-10 数据集的更多信息,请访问此链接:https://datashare.is.ed.ac.uk/handle/10283/3192。

保存和加载模型

训练神经网络是一个艰难且耗时的工作。因此,每次都重新训练模型并不实际。好消息是,我们可以将网络保存到磁盘,并在需要时加载,无论是通过更多训练提升其性能,还是用它来对新数据进行预测。在本教程中,我们将学习不同的保存模型的方法。

让我们开始吧!

如何做到……

在这个教程中,我们将训练一个mnist模型,仅用于说明我们的观点。让我们开始:

  1. 导入我们需要的所有内容:

    import json
    import numpy as np
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import LabelBinarizer
    from tensorflow.keras import Model
    from tensorflow.keras.datasets import mnist
    from tensorflow.keras.layers import BatchNormalization
    from tensorflow.keras.layers import Conv2D
    from tensorflow.keras.layers import Dense
    from tensorflow.keras.layers import Dropout
    from tensorflow.keras.layers import Flatten
    from tensorflow.keras.layers import Input
    from tensorflow.keras.layers import MaxPooling2D
    from tensorflow.keras.layers import ReLU
    from tensorflow.keras.layers import Softmax
    from tensorflow.keras.models import load_model
    from tensorflow.keras.models import model_from_json
    
  2. 定义一个函数来下载并准备数据,方法是对训练集和测试集进行归一化,并进行标签的独热编码:

    def load_data():
     (X_train, y_train), (X_test, y_test) = mnist.load_data()
        # Normalize data.
        X_train = X_train.astype('float32') / 255.0
        X_test = X_test.astype('float32') / 255.0
        # Reshape grayscale to include channel dimension.
        X_train = np.expand_dims(X_train, axis=3)
        X_test = np.expand_dims(X_test, axis=3)
        # Process labels.
        label_binarizer = LabelBinarizer()
        y_train = label_binarizer.fit_transform(y_train)
        y_test = label_binarizer.fit_transform(y_test)
        return X_train, y_train, X_test, y_test
    
  3. 定义一个用于构建网络的函数。该架构包含一个卷积层和两个全连接层:

    def build_network():
        input_layer = Input(shape=(28, 28, 1))
        convolution_1 = Conv2D(kernel_size=(2, 2),
                               padding='same',
                               strides=(2, 2),
                               filters=32)(input_layer)
        activation_1 = ReLU()(convolution_1)
        batch_normalization_1 = BatchNormalization()    
                                (activation_1)
        pooling_1 = MaxPooling2D(pool_size=(2, 2),
                                  strides=(1, 1),
           padding='same')(batch_normalization_1)
        dropout = Dropout(rate=0.5)(pooling_1)
        flatten = Flatten()(dropout)
        dense_1 = Dense(units=128)(flatten)
        activation_2 = ReLU()(dense_1)
        dense_2 = Dense(units=10)(activation_2)
        output = Softmax()(dense_2)
        network = Model(inputs=input_layer, outputs=output)
        return network
    
  4. 实现一个函数,用来使用测试集评估网络:

    def evaluate(model, X_test, y_test):
        _, accuracy = model.evaluate(X_test, y_test, 
                                     verbose=0)
        print(f'Accuracy: {accuracy}')
    
  5. 准备数据,创建验证集,并实例化神经网络:

    X_train, y_train, X_test, y_test = load_data()
    X_train, X_valid, y_train, y_valid = train_test_split(X_train, y_train, train_size=0.8)
    model = build_network()
    
  6. 编译并训练模型 50 个 epoch,批次大小为1024。根据你机器的性能调整这些值:

    model.compile(loss='categorical_crossentropy', 
                  optimizer='adam', 
                  metrics=['accuracy'])
    model.fit(X_train, y_train, 
              validation_data=(X_valid, y_valid), 
              epochs=50, 
              batch_size=1024, 
              verbose=0)
    
  7. 使用save()方法将模型及其权重以 HDF5 格式保存。然后,使用load_model()加载保存的模型,并评估该网络在测试集上的表现:

    # Saving model and weights as HDF5.
    model.save('model_and_weights.hdf5')
    # Loading model and weights as HDF5.
    loaded_model = load_model('model_and_weights.hdf5')
    # Predicting using loaded model.
    evaluate(loaded_model, X_test, y_test)
    

    输出如下:

    Accuracy: 0.9836000204086304
    

在这里,我们可以看到加载的模型在测试集上达到了 98.36%的准确率。让我们更详细地看看这个结果。

它是如何工作的…

我们刚刚学会了如何使用 TensorFlow 的 2.0 Keras API 将模型持久化到磁盘并再加载到内存中,这包括通过单一的save()方法保存模型及其权重。虽然还有其他方法可以实现同样的目标,但这是最常用和推荐的方法,因为我们可以通过load_model()函数简单地恢复网络的保存状态,然后继续训练或用于推断。

还有更多…

你也可以将模型与权重分开存储——分别使用to_json()save_weights()。这种方法的优势在于,我们可以通过model_from_json()函数从头开始复制一个具有相同架构的网络。然而,缺点是我们需要更多的函数调用,这种做法很少值得采用。

可视化模型架构

由于神经网络的复杂性,调试神经网络最有效的方式之一就是可视化它的架构。在本教程中,我们将学习两种不同的方式来展示模型的架构:

  • 使用文本摘要

  • 使用视觉图示

准备工作

我们需要Pillowpydot来生成网络架构的视觉表示。我们可以通过以下方式使用 pip 安装这两个库:

$> pip install Pillow pydot

如何操作…

可视化模型的架构非常简单,正如我们将在接下来的步骤中所学到的:

  1. 导入所有必需的库:

    from PIL import Image
    from tensorflow.keras import Model
    from tensorflow.keras.layers import BatchNormalization
    from tensorflow.keras.layers import Conv2D
    from tensorflow.keras.layers import Dense
    from tensorflow.keras.layers import Dropout
    from tensorflow.keras.layers import Flatten
    from tensorflow.keras.layers import Input
    from tensorflow.keras.layers import LeakyReLU
    from tensorflow.keras.layers import MaxPooling2D
    from tensorflow.keras.layers import Softmax
    from tensorflow.keras.utils import plot_model
    
  2. 使用我们在前一步中导入的所有层来实现一个模型。请注意,我们为方便后续引用给每个层命名。首先,让我们定义输入:

    input_layer = Input(shape=(64, 64, 3), 
                        name='input_layer')
    

    这是第一个卷积块:

    convolution_1 = Conv2D(kernel_size=(2, 2),
                           padding='same',
                           strides=(2, 2),
                           filters=32,
                           name='convolution_1')(input_layer)
    activation_1 = LeakyReLU(name='activation_1')(convolution_1)
    batch_normalization_1 = BatchNormalization(name='batch_normalization_1')(activation_1)
    pooling_1 = MaxPooling2D(pool_size=(2, 2),
                             strides=(1, 1),
                             padding='same',
                             name='pooling_1')(batch_
                             normalization_1)
    

    这是第二个卷积块:

    convolution_2 = Conv2D(kernel_size=(2, 2),
                           padding='same',
                           strides=(2, 2),
                           filters=64,
                           name='convolution_2')(pooling_1)
    activation_2 = LeakyReLU(name='activation_2')(convolution_2)
    batch_normalization_2 = BatchNormalization(name='batch_normalization_2')(activation_2)
    pooling_2 = MaxPooling2D(pool_size=(2, 2),
                             strides=(1, 1),
                             padding='same',
                             name='pooling_2')
                            (batch_normalization_2)
    dropout = Dropout(rate=0.5, name='dropout')(pooling_2)
    

    最后,我们将定义密集层和模型本身:

    flatten = Flatten(name='flatten')(dropout)
    dense_1 = Dense(units=256, name='dense_1')(flatten)
    activation_3 = LeakyReLU(name='activation_3')(dense_1)
    dense_2 = Dense(units=128, name='dense_2')(activation_3)
    activation_4 = LeakyReLU(name='activation_4')(dense_2)
    dense_3 = Dense(units=3, name='dense_3')(activation_4)
    output = Softmax(name='output')(dense_3)
    model = Model(inputs=input_layer, outputs=output, 
                  name='my_model')
    
  3. 通过打印其架构的文本表示来总结模型,如下所示:

    print(model.summary())
    

    这是摘要。输出形状列中的数字描述了该层生成的体积的维度,而参数数量列中的数字则表示该层中的参数数量:

    图 1.5 – 网络的文本表示

    图 1.5 – 网络的文本表示

    最后几行总结了可训练和不可训练参数的数量。模型的参数越多,训练起来就越困难和缓慢。

  4. 绘制网络架构的图示:

    plot_model(model, 
               show_shapes=True, 
               show_layer_names=True, 
               to_file='my_model.jpg')
    model_diagram = Image.open('my_model.jpg')
    

    这会产生以下输出:

图 1.6 – 网络的视觉表示

图 1.6 – 网络的视觉表示

现在,让我们了解这一切是如何工作的。

它是如何工作的…

可视化一个模型就像对包含模型的变量调用plot_model()一样简单。然而,为了使其正常工作,我们必须确保已安装所需的依赖项;例如,pydot。尽管如此,如果我们希望更详细地总结网络各层的参数数量,我们必须调用summarize()方法。

最后,为每一层命名是一个良好的约定。这使得架构更具可读性,并且在将来更容易重用,因为我们可以通过名称简单地检索某一层。这一特性的一个显著应用是神经风格迁移

创建一个基本的图像分类器

我们将通过在Fashion-MNIST数据集上实现一个图像分类器来结束本章,Fashion-MNISTmnist的一个流行替代品。这将帮助我们巩固从前面教程中学到的知识。如果在某个步骤中你需要更多的细节,请参考之前的教程。

准备工作

我鼓励你在处理本教程之前先完成前面五个教程,因为我们的目标是通过本章学到的知识形成一个完整的循环。此外,确保你的系统中已经安装了Pillowpydot。你可以使用 pip 安装它们:

$> pip install Pillow pydot

最后,我们将使用tensorflow_docs包来绘制模型的损失和准确率曲线。你可以通过以下命令安装这个库:

$> pip install git+https://github.com/tensorflow/docs

如何操作…

按照以下步骤完成本教程:

  1. 导入必要的包:

    import matplotlib.pyplot as plt
    import numpy as np
    import tensorflow as tf
    import tensorflow_docs as tfdocs
    import tensorflow_docs.plots
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import LabelBinarizer
    from tensorflow.keras import Model
    from tensorflow.keras.datasets import fashion_mnist as fm
    from tensorflow.keras.layers import BatchNormalization
    from tensorflow.keras.layers import Conv2D
    from tensorflow.keras.layers import Dense
    from tensorflow.keras.layers import Dropout
    from tensorflow.keras.layers import ELU
    from tensorflow.keras.layers import Flatten
    from tensorflow.keras.layers import Input
    from tensorflow.keras.layers import MaxPooling2D
    from tensorflow.keras.layers import Softmax
    from tensorflow.keras.models import load_model
    from tensorflow.keras.utils import plot_model
    
  2. 定义一个函数来加载并准备数据集。它将对数据进行归一化处理,对标签进行独热编码,取部分训练集用于验证,并将三个数据子集包装成三个独立的tf.data.Dataset实例,以通过from_tensor_slices()提高性能:

    def load_dataset():
        (X_train, y_train), (X_test, y_test) = fm.load_data()
        X_train = X_train.astype('float32') / 255.0
        X_test = X_test.astype('float32') / 255.0
        # Reshape grayscale to include channel dimension.
        X_train = np.expand_dims(X_train, axis=3)
        X_test = np.expand_dims(X_test, axis=3)
        label_binarizer = LabelBinarizer()
        y_train = label_binarizer.fit_transform(y_train)
        y_test = label_binarizer.fit_transform(y_test)
        (X_train, X_val,
         y_train, y_val) = train_test_split(X_train, y_train, 
    
                            train_size=0.8)
        train_ds = (tf.data.Dataset
                    .from_tensor_slices((X_train, 
                                         y_train)))
        val_ds = (tf.data.Dataset
                  .from_tensor_slices((X_val, y_val)))
        test_ds = (tf.data.Dataset
                   .from_tensor_slices((X_test, y_test)))
    
  3. 实现一个函数,构建一个类似于BatchNormalization的网络,我们将使用它来使网络更快、更稳定;同时也实现Dropout层,帮助我们应对过拟合问题,这是由于方差过大导致网络失去泛化能力的情况。

    def build_network():
        input_layer = Input(shape=(28, 28, 1))
        x = Conv2D(filters=20, 
                   kernel_size=(5, 5),
                   padding='same', 
                   strides=(1, 1))(input_layer)
        x = ELU()(x)
        x = BatchNormalization()(x)
        x = MaxPooling2D(pool_size=(2, 2), 
                         strides=(2, 2))(x)
        x = Dropout(0.5)(x)
        x = Conv2D(filters=50, 
                   kernel_size=(5, 5), 
                   padding='same', 
                   strides=(1, 1))(x)
        x = ELU()(x)
        x = BatchNormalization()(x)
        x = MaxPooling2D(pool_size=(2, 2), 
                         strides=(2, 2))(x)
        x = Dropout(0.5)(x)
        x = Flatten()(x)
        x = Dense(units=500)(x)
        x = ELU()(x)
        x = Dropout(0.5)(x)
        x = Dense(10)(x)
        output = Softmax()(x)
        model = Model(inputs=input_layer, outputs=output)
        return model
    
  4. 定义一个函数,该函数接收模型的训练历史记录以及一个感兴趣的度量标准,用于创建对应于训练和验证曲线的图:

    def plot_model_history(model_history, metric, ylim=None):
        plt.style.use('seaborn-darkgrid')
        plotter = tfdocs.plots.HistoryPlotter()
        plotter.plot({'Model': model_history}, metric=metric)
        plt.title(f'{metric.upper()}')
        if ylim is None:
            plt.ylim([0, 1])
        else:
            plt.ylim(ylim)
        plt.savefig(f'{metric}.png')
        plt.close()
    
  5. 以每次 256 张图片的批次来消费训练和验证数据集。prefetch()方法会启动一个后台线程,填充大小为1024的缓冲区,缓存图像批次:

    BATCH_SIZE = 256
    BUFFER_SIZE = 1024
    train_dataset, val_dataset, test_dataset = load_dataset()
    train_dataset = (train_dataset
                     .shuffle(buffer_size=BUFFER_SIZE)
                     .batch(BATCH_SIZE)
                     .prefetch(buffer_size=BUFFER_SIZE))
    val_dataset = (val_dataset
                   .batch(BATCH_SIZE)
                   .prefetch(buffer_size=BUFFER_SIZE))
    test_dataset = test_dataset.batch(BATCH_SIZE)
    
  6. 构建并训练网络:

    EPOCHS = 100
    model = build_network()
    model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
    model_history = model.fit(train_dataset, validation_data=validation_dataset, epochs=EPOCHS, verbose=0)
    
  7. 绘制训练和验证的损失与准确率:

    plot_model_history(model_history, 'loss', [0., 2.0])
    plot_model_history(model_history, 'accuracy')
    

    第一个图对应训练集和验证集的损失曲线:

    图 1.7 – 损失图

    图 1.7 – 损失图

    第二个图显示了训练和验证集的准确率曲线:

    图 1.8 – 准确率图

    图 1.8 – 准确率图

  8. 可视化模型的架构:

    plot_model(model, show_shapes=True, show_layer_names=True, to_file='model.png')
    

    以下是我们模型的示意图:

    图 1.9 – 模型架构

    图 1.9 – 模型架构

  9. 保存模型:

    model.save('model.hdf5')
    
  10. 加载并评估模型:

    loaded_model = load_model('model.hdf5')
    results = loaded_model.evaluate(test_dataset, verbose=0)
    print(f'Loss: {results[0]}, Accuracy: {results[1]}')
    

    输出如下:

    Loss: 0.2943768735975027, Accuracy: 0.9132000207901001
    

这完成了本章的最终步骤。让我们回顾一下它的全部运作。

它是如何工作的…

在这个步骤中,我们运用了本章中学到的所有教训。我们首先下载了Fashion-MNIST,并使用tf.data.Dataset API 加载其图像,以便将它们馈送到我们使用声明式功能高级 Keras API 实现的网络中。在将我们的模型拟合到数据之后,我们通过检查在训练和验证集上的损失和准确率曲线来审视其性能,借助matplotlibtensorflow_docs。为了更好地理解网络,我们使用plot_model()可视化其架构,然后将其以便捷的 HDF5 格式保存到磁盘上,包括其权重。最后,我们使用load_model()加载模型以在新的、未见过的数据上评估它 —— 即测试集 —— 获得了令人满意的 91.3% 的准确率评分。

另请参阅

欲深入了解Fashion-MNIST,请访问此网站:github.com/zalandoresearch/fashion-mnist。TensorFlow 文档的 GitHub 仓库在此处可用:github.com/tensorflow/docs

第二章:第二章:执行图像分类

计算机视觉是一个广泛的领域,借鉴了许多地方的灵感。当然,这意味着它的应用也非常广泛和多样。然而,过去十年里,特别是在深度学习应用于视觉任务的背景下,最大的突破出现在一个特定领域,即图像分类

顾名思义,图像分类的过程是根据图像的视觉内容来识别图像中的内容。这个图像中有狗还是猫?这张图片中显示的数字是什么?这张照片中的人是微笑还是不微笑?

由于图像分类是计算机视觉领域深度学习应用中的一项重要而广泛的任务,本章中的食谱将重点介绍使用 TensorFlow 2.x 进行图像分类的所有细节。

我们将涵盖以下食谱:

  • 创建一个二分类器来检测微笑

  • 创建一个多类别分类器来玩石头剪刀布

  • 创建一个多标签分类器来标记手表

  • 从零开始实现 ResNet

  • 使用 Keras API 通过预训练网络进行图像分类

  • 使用预训练网络通过 TensorFlow Hub 进行图像分类

  • 使用数据增强来提高 Keras API 的性能

  • 使用 tf.data 和 tf.image API 进行数据增强以提高性能

技术要求

除了正确安装 TensorFlow 2.x 外,强烈建议使用 GPU,因为某些食谱非常消耗资源,使用 CPU 将无法完成任务。在每个食谱中,你将找到完成它所需的步骤和依赖项,这些内容位于准备工作部分。最后,本章中展示的代码可以在这里完整查看:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch2

查看以下链接,观看《代码实践》视频:

bit.ly/3bOjqnU

创建一个二分类器来检测微笑

在最基本的形式下,图像分类是区分两种类别,或者判断某种特征是否存在。 在本食谱中,我们将实现一个二分类器,用于判断照片中的人是否在微笑。

那么我们开始吧,好吗?

准备工作

你需要安装Pillow,使用pip安装非常简单:

$> pip install Pillow

我们将使用SMILEs数据集,位于这里:github.com/hromi/SMILEsmileD。克隆或下载该仓库的压缩版本到你喜欢的目录。在本食谱中,我们假设数据位于~/.keras/datasets目录下,文件夹名为SMILEsmileD-master

图 2.1 – 正例(左)和反例(右)

图 2.1 – 正例(左)和反例(右)

开始吧!

如何实现…

按照以下步骤,从头开始训练一个基于SMILEs数据集的微笑分类器:

  1. 导入所有必要的包:

    import os
    import pathlib
    import glob
    import numpy as np
    from sklearn.model_selection import train_test_split
    from tensorflow.keras import Model
    from tensorflow.keras.layers import *
    from tensorflow.keras.preprocessing.image import *
    
  2. 定义一个函数从文件路径列表中加载图像和标签:

    def load_images_and_labels(image_paths):
        images = []
        labels = []
        for image_path in image_paths:
            image = load_img(image_path, target_size=(32,32), 
                             color_mode='grayscale')
            image = img_to_array(image)
            label = image_path.split(os.path.sep)[-2]
            label = 'positive' in label
            label = float(label)
            images.append(image)
            labels.append(label)
        return np.array(images), np.array(labels)
    

    注意,我们正在以灰度图像的形式加载图像,并通过检查图像路径中是否包含单词positive来对标签进行编码。

  3. 定义一个函数来构建神经网络。该模型的结构基于LeNet(你可以在另见部分找到 LeNet 论文的链接):

    def build_network():
        input_layer = Input(shape=(32, 32, 1))
        x = Conv2D(filters=20,
                   kernel_size=(5, 5),
                   padding='same',
                   strides=(1, 1))(input_layer)
        x = ELU()(x)
        x = BatchNormalization()(x)
        x = MaxPooling2D(pool_size=(2, 2),
                         strides=(2, 2))(x)
        x = Dropout(0.4)(x)
        x = Conv2D(filters=50,
                   kernel_size=(5, 5),
                   padding='same',
                   strides=(1, 1))(x)
        x = ELU()(x)
        x = BatchNormalization()(x)
        x = MaxPooling2D(pool_size=(2, 2),
                         strides=(2, 2))(x)
        x = Dropout(0.4)(x)
        x = Flatten()(x)
        x = Dense(units=500)(x)
        x = ELU()(x)
        x = Dropout(0.4)(x)
        output = Dense(1, activation='sigmoid')(x)
        model = Model(inputs=input_layer, outputs=output)
        return model
    

    由于这是一个二分类问题,输出层只需要一个 Sigmoid 激活的神经元。

  4. 将图像路径加载到列表中:

    files_pattern = (pathlib.Path.home() / '.keras' / 
                     'datasets' /
                     'SMILEsmileD-master' / 'SMILEs' / '*' 
                        / '*' / 
                     '*.jpg')
    files_pattern = str(files_pattern)
    dataset_paths = [*glob.glob(files_pattern)]
    
  5. 使用之前定义的load_images_and_labels()函数将数据集加载到内存中:

    X, y = load_images_and_labels(dataset_paths)
    
  6. 对图像进行归一化处理,并计算数据集中的正例、负例和总例数:

    X /= 255.0
    total = len(y)
    total_positive = np.sum(y)
    total_negative = total - total_positive
    
  7. 创建训练、测试和验证数据子集:

    (X_train, X_test,
     y_train, y_test) = train_test_split(X, y,
                                         test_size=0.2,
                                         stratify=y,
                                         random_state=999)
    (X_train, X_val,
     y_train, y_val) = train_test_split(X_train, y_train,
                                        test_size=0.2,
                                        stratify=y_train,
                                        random_state=999)
    
  8. 实例化模型并编译它:

    model = build_network()
    model.compile(loss='binary_crossentropy',
                  optimizer='rmsprop',
                  metrics=['accuracy'])
    
  9. 训练模型。由于数据集不平衡,我们为每个类别分配了与数据集中正负图像数量成比例的权重:

    BATCH_SIZE = 32
    EPOCHS = 20
    model.fit(X_train, y_train,
              validation_data=(X_val, y_val),
              epochs=EPOCHS,
              batch_size=BATCH_SIZE,
              class_weight={
                  1.0: total / total_positive,
                  0.0: total / total_negative
              })
    
  10. 在测试集上评估模型:

    test_loss, test_accuracy = model.evaluate(X_test, 
                                              y_test)
    

在经过 20 个周期后,网络应该能够在测试集上达到约 90%的准确率。在接下来的部分中,我们将解释前述步骤。

工作原理…

我们刚刚训练了一个网络来判断一个人是否在图片中微笑。我们的第一个大任务是将数据集中的图像加载成适合神经网络的格式。具体来说,load_image_and_labels()函数负责加载灰度图像,调整其大小为 32x32x1,然后将其转换为numpy数组。为了提取标签,我们查看了每个图像所在的文件夹:如果文件夹名称中包含单词 positive,我们就将标签编码为 1;否则,将其编码为 0(这里我们用的是将布尔值转换为浮点数的技巧,像这样:float(label))。

接下来,我们构建了神经网络,它受到 LeNet 架构的启发。这里的最大收获是,由于这是一个二分类问题,我们可以使用一个 Sigmoid 激活的神经元来区分两个类别。

然后,我们将 20%的图像用作测试集,从剩下的 80%中再取 20%用作验证集。设置好这三个子集后,我们开始在 20 个周期内训练网络,使用binary_crossentropy作为损失函数,rmsprop作为优化器。

为了处理数据集中的类别不平衡问题(在 13,165 张图像中,只有 3,690 张包含微笑的人,而其余 9,475 张不包含),我们传递了一个class_weight字典,其中我们为每个类别分配了与类别实例数成反比的权重,从而有效地迫使模型更多关注 1.0 类,即微笑类别。

最终,我们在测试集上取得了大约 90.5%的准确率。

另见

有关SMILEs数据集的更多信息,可以访问官方 GitHub 仓库:github.com/hromi/SMILEsmileD。你也可以阅读 LeNet 论文(虽然很长):yann.lecun.com/exdb/publis/pdf/lecun-98.pdf

创建一个多类别分类器来玩石头剪刀布

更多时候,我们关注的是将图像分类为两个以上的类别。正如我们在这个示例中看到的,实现一个神经网络来区分多个类别是相对简单的,最好的展示方法是什么呢?那就是训练一个能够玩著名的“石头剪刀布”游戏的模型。

你准备好了吗?让我们深入了解!

准备就绪

我们将使用Rock-Paper-Scissors Images数据集,该数据集托管在 Kaggle 上,位置如下:www.kaggle.com/drgfreeman/rockpaperscissors。要下载它,你需要一个 Kaggle 账户,请登录或注册。然后,在你选择的位置解压缩数据集。在这个示例中,我们假设解压缩后的文件夹位于~/.keras/datasets目录下,名为rockpaperscissors

下面是一些示例图像:

图 2.2 – 石头(左)、布(中)和剪刀(右)的示例图像

图 2.2 – 石头(左)、布(中)和剪刀(右)的示例图像

让我们开始实现吧。

如何做……

以下步骤说明如何训练一个多类别卷积神经网络CNN)来区分石头剪刀布游戏的三种类别:

  1. 导入所需的包:

    import os
    import pathlib
    import glob
    import numpy as np
    import tensorflow as tf
    from sklearn.model_selection import train_test_split
    from tensorflow.keras import Model
    from tensorflow.keras.layers import *
    from tensorflow.keras.losses import CategoricalCrossentropy
    
  2. 定义一个包含三个类别的列表,并为tf.data.experimental.AUTOTUNE定义一个别名,我们稍后将使用它:

    CLASSES = ['rock', 'paper', 'scissors']
    AUTOTUNE = tf.data.experimental.AUTOTUNE
    

    CLASSES中的值与每个类别的图像所在目录的名称匹配。

  3. 定义一个函数来加载图像及其标签,给定图像的文件路径:

    def load_image_and_label(image_path, target_size=(32, 32)):
        image = tf.io.read_file(image_path)
        image = tf.image.decode_jpeg(image, channels=3)
        image = tf.image.rgb_to_grayscale(image)
        image = tf.image.convert_image_dtype(image, 
                                             np.float32)
        image = tf.image.resize(image, target_size)
        label = tf.strings.split(image_path,os.path.sep)[-2]
        label = (label == CLASSES)  # One-hot encode.
        label = tf.dtypes.cast(label, tf.float32)
        return image, label
    

    请注意,我们通过将包含图像的文件夹名称(从image_path提取)与CLASSES列表进行比较来进行 one-hot 编码。

  4. 定义一个函数来构建网络架构。在这个例子中,这是一个非常简单和浅层的架构,对于我们要解决的问题足够了:

    def build_network():
        input_layer = Input(shape=(32, 32, 1))
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same',
                   strides=(1, 1))(input_layer)
        x = ReLU()(x)
        x = Dropout(rate=0.5)(x)
        x = Flatten()(x)
        x = Dense(units=3)(x)
        output = Softmax()(x)
        return Model(inputs=input_layer, outputs=output)
    
  5. 定义一个函数,给定数据集路径,返回一个tf.data.Dataset实例,其中包含图像和标签,按批次并可选地进行洗牌:

    def prepare_dataset(dataset_path,
                        buffer_size,
                        batch_size,
                        shuffle=True):
        dataset = (tf.data.Dataset
                   .from_tensor_slices(dataset_path)
                   .map(load_image_and_label,
                        num_parallel_calls=AUTOTUNE))
        if shuffle:
            dataset.shuffle(buffer_size=buffer_size)
        dataset = (dataset
                   .batch(batch_size=batch_size)
                   .prefetch(buffer_size=buffer_size))
        return dataset
    
  6. 将图像路径加载到列表中:

    file_patten = (pathlib.Path.home() / '.keras' / 
                   'datasets' /
                   'rockpaperscissors' / 'rps-cv-images' / 
                     '*' /
                   '*.png')
    file_pattern = str(file_patten)
    dataset_paths = [*glob.glob(file_pattern)]
    
  7. 创建训练、测试和验证数据集的图像路径:

    train_paths, test_paths = train_test_split(dataset_paths,
                                              test_size=0.2,
                                            random_state=999)
    train_paths, val_paths = train_test_split(train_paths,
                                          test_size=0.2,
                                         random_state=999)
    
  8. 准备训练、测试和验证数据集:

    BATCH_SIZE = 1024
    BUFFER_SIZE = 1024
    train_dataset = prepare_dataset(train_paths,
                                  buffer_size=BUFFER_SIZE,
                                    batch_size=BATCH_SIZE)
    validation_dataset = prepare_dataset(val_paths,
                                  buffer_size=BUFFER_SIZE,
                                   batch_size=BATCH_SIZE,
                                    shuffle=False)
    test_dataset = prepare_dataset(test_paths,
                                  buffer_size=BUFFER_SIZE,
                                   batch_size=BATCH_SIZE,
                                   shuffle=False)
    
  9. 实例化并编译模型:

    model = build_network()
    model.compile(loss=CategoricalCrossentropy
                 (from_logits=True),
                  optimizer='adam',
                  metrics=['accuracy'])
    
  10. 将模型拟合250个 epoch:

    EPOCHS = 250
    model.fit(train_dataset,
              validation_data=validation_dataset,
              epochs=EPOCHS)
    
  11. 在测试集上评估模型:

    test_loss, test_accuracy = model.evaluate(test_dataset)
    

在经过 250 轮训练后,我们的网络在测试集上达到了大约 93.5%的准确率。让我们来理解一下我们刚才做了什么。

它是如何工作的……

我们首先定义了CLASSES列表,这使得我们可以快速根据每张图片所在目录的名称,对其标签进行独热编码,正如我们在load_image_and_label()函数中观察到的那样。在同一个函数中,我们从磁盘读取图片,解码 JPEG 格式,将其转换为灰度图(此问题不需要颜色信息),然后将其调整为 32x32x1 的尺寸。

build_network()创建了一个非常简单且浅的卷积神经网络(CNN),包含一个卷积层,使用ReLU()激活函数,后接一个输出层,包含三个神经元,分别对应数据集中类别的数量。由于这是一个多类分类任务,我们使用Softmax()来激活输出层。

prepare_dataset()利用之前定义的load_image_and_label()函数,将文件路径转换为图像张量批次和独热编码标签。

使用此处解释的三个功能,我们准备了三个数据子集,目的是训练、验证和测试神经网络。我们训练模型 250 个 epoch,使用adam优化器和CategoricalCrossentropy(from_logits=True)作为损失函数(from_logits=True提供了更高的数值稳定性)。

最终,我们在测试集上达到了约 93.5%的准确率。根据这些结果,你可以将这个网络作为石头剪刀布游戏的一个组件,用来识别玩家的手势并做出相应的反应。

另见

关于Rock-Paper-Scissors Images数据集的更多信息,请参考它的 Kaggle 官方页面:www.kaggle.com/drgfreeman/rockpaperscissors

创建一个多标签分类器来标记手表

神经网络不仅仅局限于建模单一变量的分布。实际上,它可以轻松处理每张图片关联多个标签的情况。在这个配方中,我们将实现一个卷积神经网络(CNN),用来分类手表的性别和风格/用途。

让我们开始吧。

准备工作

首先,我们必须安装Pillow

$> pip install Pillow

接下来,我们将使用 Kaggle 上托管的Fashion Product Images (Small)数据集,登录后可以在这里下载:www.kaggle.com/paramaggarwal/fashion-product-images-small。在本配方中,我们假设数据位于~/.keras/datasets目录下,文件夹名为fashion-product-images-small。我们只会使用该数据集的一个子集,专注于手表部分,这将在如何操作……部分通过编程方式构建。

这里是一些示例图片:

图 2.3 – 示例图片

图 2.3 – 示例图片

让我们开始这个配方吧。

如何操作……

让我们回顾一下完成这个配方的步骤:

  1. 导入必要的包:

    import os
    import pathlib
    from csv import DictReader
    import glob
    import numpy as np
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import MultiLabelBinarizer
    from tensorflow.keras.layers import *
    from tensorflow.keras.models import Model
    from tensorflow.keras.preprocessing.image import *
    
  2. 定义一个函数来构建网络架构。首先,实现卷积模块:

    def build_network(width, height, depth, classes):
        input_layer = Input(shape=(width, height, depth))
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same')(input_layer)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
    

    接下来,添加全卷积层:

        x = Flatten()(x)
        x = Dense(units=512)(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Dropout(rate=0.5)(x)
        x = Dense(units=classes)(x)
        output = Activation('sigmoid')(x)
        return Model(input_layer, output)
    
  3. 定义一个函数,根据图像路径列表和与每个图像关联的元数据字典,加载所有图像和标签(性别和使用场景):

    def load_images_and_labels(image_paths, styles, 
                               target_size):
        images = []
        labels = []
        for image_path in image_paths:
            image = load_img(image_path, 
                             target_size=target_size)
            image = img_to_array(image)
            image_id = image_path.split(os.path.sep)[-
                                             1][:-4]
            image_style = styles[image_id]
            label = (image_style['gender'], 
                     image_style['usage'])
            images.append(image)
            labels.append(label)
        return np.array(images), np.array(labels)
    
  4. 设置随机种子以确保结果可复现:

    SEED = 999
    np.random.seed(SEED)
    
  5. 定义图像路径和styles.csv元数据文件的路径:

    base_path = (pathlib.Path.home() / '.keras' / 
                 'datasets' /
                 'fashion-product-images-small')
    styles_path = str(base_path / 'styles.csv')
    images_path_pattern = str(base_path / 'images/*.jpg')
    image_paths = glob.glob(images_path_pattern)
    
  6. 仅保留适用于CasualSmart CasualFormal使用场景的Watches图像,适合MenWomen

    with open(styles_path, 'r') as f:
        dict_reader = DictReader(f)
        STYLES = [*dict_reader]
        article_type = 'Watches'
        genders = {'Men', 'Women'}
        usages = {'Casual', 'Smart Casual', 'Formal'}
        STYLES = {style['id']: style
                  for style in STYLES
                  if (style['articleType'] == article_type 
                                               and
                      style['gender'] in genders and
                      style['usage'] in usages)}
    image_paths = [*filter(lambda p: 
                   p.split(os.path.sep)[-1][:-4]
                                     in STYLES.keys(),
                           image_paths)]
    
  7. 加载图像和标签,将图像调整为 64x64x3 的形状:

    X, y = load_images_and_labels(image_paths, STYLES, 
                                  (64, 64))
    
  8. 对图像进行归一化,并对标签进行多热编码:

    X = X.astype('float') / 255.0
    mlb = MultiLabelBinarizer()
    y = mlb.fit_transform(y)
    
  9. 创建训练集、验证集和测试集的划分:

    (X_train, X_test,
     y_train, y_test) = train_test_split(X, y,
                                         stratify=y,
                                         test_size=0.2,
    
                                        random_state=SEED)
    (X_train, X_valid,
     y_train, y_valid) = train_test_split(X_train, y_train,
                                        stratify=y_train,
                                          test_size=0.2,
                                       random_state=SEED)
    
  10. 构建并编译网络:

    model = build_network(width=64,
                          height=64,
                          depth=3,
                          classes=len(mlb.classes_))
    model.compile(loss='binary_crossentropy',
                  optimizer='rmsprop',
                  metrics=['accuracy'])
    
  11. 训练模型20个 epoch,每次批处理64张图像:

    BATCH_SIZE = 64
    EPOCHS = 20
    model.fit(X_train, y_train,
              validation_data=(X_valid, y_valid),
              batch_size=BATCH_SIZE,
              epochs=EPOCHS)
    
  12. 在测试集上评估模型:

    result = model.evaluate(X_test, y_test, 
                           batch_size=BATCH_SIZE)
    print(f'Test accuracy: {result[1]}')
    

    该块打印如下内容:

    Test accuracy: 0.90233546
    
  13. 使用模型对测试图像进行预测,显示每个标签的概率:

    test_image = np.expand_dims(X_test[0], axis=0)
    probabilities = model.predict(test_image)[0]
    for label, p in zip(mlb.classes_, probabilities):
        print(f'{label}: {p * 100:.2f}%')
    

    打印出如下内容:

    Casual: 100.00%
    Formal: 0.00%
    Men: 1.08%
    Smart Casual: 0.01%
    Women: 99.16%
    
  14. 比较真实标签与网络预测的结果:

    ground_truth_labels = np.expand_dims(y_test[0], 
                                         axis=0)
    ground_truth_labels = mlb.inverse_transform(ground_truth_labels)
    print(f'Ground truth labels: {ground_truth_labels}')
    

    输出如下:

    Ground truth labels: [('Casual', 'Women')]
    

接下来,我们将看到所有内容如何工作。

它是如何工作的…

我们实现了一个较小版本的与每个手表相关联的genderusage元数据。换句话说,我们同时建模了两个二分类问题:一个是gender(性别),另一个是usage(使用场景)。这就是为什么我们使用 Sigmoid 激活网络输出,而不是 Softmax,并且使用binary_crossentropy损失函数而不是categorical_crossentropy的原因。

我们在上述网络上训练了 20 个 epoch,每次使用 64 张图像的批处理,最终在测试集上获得了 90%的准确率。最后,我们在一个未见过的测试集图像上进行了预测,并验证了网络以高度确信(Casual的 100%确信度,Women的 99.16%确信度)输出的标签与真实标签CasualWomen完全一致。

另见

更多关于Fashion Product Images (Small)数据集的信息,请参考官方 Kaggle 页面:www.kaggle.com/paramaggarwal/fashion-product-images-small。我建议你阅读介绍VGG架构的论文:arxiv.org/abs/1409.1556

从零实现 ResNet

残差网络ResNet的简称)是深度学习中最具突破性的进展之一。该架构依赖于一种叫做残差模块的组件,它使得我们能够将深度网络组合起来,构建出几年前无法想象的深度网络。某些ResNet变种的层数超过 100 层,且性能没有任何下降!

在此配方中,我们将实现CIFAR-10CINIC-10

准备工作

我们不会深入解释ResNet,因此如果你对细节感兴趣,最好先了解一下该架构。你可以在这里阅读原始论文:arxiv.org/abs/1512.03385

如何实现……

按照以下步骤从头开始实现ResNet

  1. 导入所有必要的模块:

    import os
    import numpy as np
    import tarfile
    import tensorflow as tf
    from tensorflow.keras.callbacks import ModelCheckpoint
    from tensorflow.keras.layers import *
    from tensorflow.keras.models import *
    from tensorflow.keras.regularizers import l2
    from tensorflow.keras.utils import get_file
    
  2. 定义一个别名给tf.data.experimental.AUTOTUNE选项,稍后我们会使用它:

    AUTOTUNE = tf.data.experimental.AUTOTUNE
    
  3. 定义一个函数,在reduce=True时创建一个残差模块,我们应用 1x1 卷积:

        if reduce:
            shortcut = Conv2D(filters=filters,
                              kernel_size=(1, 1),
                              strides=stride,
                              use_bias=False,
                        kernel_regularizer=l2(reg))(act_1)
    

    最后,我们将跳跃连接和第三个块合并成一个单一的层,并将其作为输出返回:

        x = Add()([conv_3, shortcut])
        return x
    
  4. 定义一个函数来构建自定义ResNet网络:

    def build_resnet(input_shape,
                     classes,
                     stages,
                     filters,
                     reg=1e-3,
                     bn_eps=2e-5,
                     bn_momentum=0.9):
        inputs = Input(shape=input_shape)
        x = BatchNormalization(axis=-1,
                               epsilon=bn_eps,
    
                             momentum=bn_momentum)(inputs)
        x = Conv2D(filters[0], (3, 3),
                   use_bias=False,
                   padding='same',
                   kernel_regularizer=l2(reg))(x)
        for i in range(len(stages)):
            stride = (1, 1) if i == 0 else (2, 2)
            x = residual_module(data=x,
                                filters=filters[i + 1],
                                stride=stride,
                                reduce=True,
                                bn_eps=bn_eps,
                                bn_momentum=bn_momentum)
            for j in range(stages[i] - 1):
                x = residual_module(data=x,
                                    filters=filters[i + 
                                                   1],
                                    stride=(1, 1),
                                    bn_eps=bn_eps,
    
                                bn_momentum=bn_momentum)
        x = BatchNormalization(axis=-1,
                               epsilon=bn_eps,
                               momentum=bn_momentum)(x)
        x = ReLU()(x)
        x = AveragePooling2D((8, 8))(x)
        x = Flatten()(x)
        x = Dense(classes, kernel_regularizer=l2(reg))(x)
        x = Softmax()(x)
        return Model(inputs, x, name='resnet')
    
  5. 定义一个函数来加载图像及其一热编码标签,基于其文件路径:

    def load_image_and_label(image_path, target_size=(32, 32)):
        image = tf.io.read_file(image_path)
        image = tf.image.decode_png(image, channels=3)
        image = tf.image.convert_image_dtype(image, 
                                            np.float32)
        image -= CINIC_MEAN_RGB  # Mean normalize
        image = tf.image.resize(image, target_size)
        label = tf.strings.split(image_path, os.path.sep)[-2]
        label = (label == CINIC_10_CLASSES)  # One-hot encode.
        label = tf.dtypes.cast(label, tf.float32)
        return image, label
    
  6. 定义一个函数,通过类似 glob 的模式创建tf.data.Dataset实例,模式指向图像所在的文件夹:

    def prepare_dataset(data_pattern, shuffle=False):
        dataset = (tf.data.Dataset
                   .list_files(data_pattern)
                   .map(load_image_and_label,
                        num_parallel_calls=AUTOTUNE)
                   .batch(BATCH_SIZE))
    
        if shuffle:
            dataset = dataset.shuffle(BUFFER_SIZE)
    
        return dataset.prefetch(BATCH_SIZE)
    
  7. 定义CINIC-10数据集的平均 RGB 值,这些值将在load_image_and_label()函数中用于图像的均值归一化(该信息可在官方CINIC-10网站上找到):

    CINIC_MEAN_RGB = np.array([0.47889522, 0.47227842, 0.43047404])
    
  8. 定义CINIC-10数据集的类别:

    CINIC_10_CLASSES = ['airplane', 'automobile', 'bird', 'cat',
                        'deer', 'dog', 'frog', 'horse',    'ship',
                        'truck']
    
  9. 下载并解压CINIC-10数据集到~/.keras/datasets目录:

    DATASET_URL = ('https://datashare.is.ed.ac.uk/bitstream/handle/'
                   '10283/3192/CINIC-10.tar.gz?'
                   'sequence=4&isAllowed=y')
    DATA_NAME = 'cinic10'
    FILE_EXTENSION = 'tar.gz'
    FILE_NAME = '.'.join([DATA_NAME, FILE_EXTENSION])
    downloaded_file_location = get_file(origin=DATASET_URL,
                                        fname=FILE_NAME,
                                        extract=False)
    data_directory, _ = (downloaded_file_location
                         .rsplit(os.path.sep, maxsplit=1))
    data_directory = os.path.sep.join([data_directory, 
                                      DATA_NAME])
    tar = tarfile.open(downloaded_file_location)
    if not os.path.exists(data_directory):
        tar.extractall(data_directory)
    
  10. 定义类似 glob 的模式来表示训练、测试和验证子集:

    train_pattern = os.path.sep.join(
        [data_directory, 'train/*/*.png'])
    test_pattern = os.path.sep.join(
        [data_directory, 'test/*/*.png'])
    valid_pattern = os.path.sep.join(
        [data_directory, 'valid/*/*.png'])
    
  11. 准备数据集:

    BATCH_SIZE = 128
    BUFFER_SIZE = 1024
    train_dataset = prepare_dataset(train_pattern, 
                                    shuffle=True)
    test_dataset = prepare_dataset(test_pattern)
    valid_dataset = prepare_dataset(valid_pattern)
    
  12. 构建、编译并训练ModelCheckpoint()回调:

    model = build_resnet(input_shape=(32, 32, 3),
                         classes=10,
                         stages=(9, 9, 9),
                         filters=(64, 64, 128, 256),
                         reg=5e-3)
    model.compile(loss='categorical_crossentropy',
                  optimizer='rmsprop',
                  metrics=['accuracy'])
    model_checkpoint_callback = ModelCheckpoint(
        filepath='./model.{epoch:02d}-{val_accuracy:.2f}.hdf5',
        save_weights_only=False,
        monitor='val_accuracy')
    EPOCHS = 100
    model.fit(train_dataset,
              validation_data=valid_dataset,
              epochs=EPOCHS,
              callbacks=[model_checkpoint_callback])
    
  13. 加载最佳模型(在此例中为model.38-0.72.hdf5)并在测试集上进行评估:

    model = load_model('model.38-0.72.hdf5')
    result = model.evaluate(test_dataset)
    print(f'Test accuracy: {result[1]}')
    

    这将打印以下内容:

    Test accuracy: 0.71956664
    

在下一部分我们将学习它是如何工作的。

它是如何工作的……

residual_module()函数的关键接收输入数据(data)、滤波器数量(filters)、卷积块的步幅(stride)、reduce标志用于指示是否通过应用 1x1 卷积来减少跳跃分支的空间大小(该技术用于减少滤波器输出的维度),以及调整不同层的正则化(reg)和批量归一化(bn_epsbn_momentum)的参数。

一个残差模块由两条分支组成:第一条是跳跃连接,也叫捷径分支,基本上和输入相同。第二条或主分支由三个卷积块组成:一个 1x1 的卷积块,使用四分之一的滤波器,一个 3x3 的卷积块,同样使用四分之一的滤波器,最后是另一个 1x1 的卷积块,使用所有的滤波器。跳跃分支和主分支最终通过Add()层连接在一起。

build_network() 允许我们指定使用的阶段数量,以及每个阶段的过滤器数量。我们首先对输入应用一个 3x3 的卷积(在进行批量归一化后)。然后我们开始创建各个阶段。一个阶段是由一系列残差模块相互连接组成的。stages 列表的长度控制要创建的阶段数,每个元素则控制该阶段中层数的数量。filters 参数包含每个阶段中每个残差块要使用的过滤器数量。最后,我们在这些阶段的基础上构建了一个全连接网络,并使用 Softmax 激活函数,其单位数等于数据集中类别的数量(在此情况下为 10)。

由于 CINIC-10 数据集并不容易,而且我们没有应用任何数据增强或迁移学习。

参见

欲了解更多关于 CINIC-10 数据集的信息,请访问此链接:datashare.is.ed.ac.uk/handle/10283/3192

使用 Keras API 和预训练网络进行图像分类

我们并不总是需要从头开始训练一个分类器,特别是当我们想要分类的图像与其他网络训练过的图像相似时。在这些情况下,我们可以简单地重用已训练的模型,从而节省大量时间。在这个教程中,我们将使用一个在 ImageNet 上预训练的网络来对自定义图像进行分类。

我们开始吧!

正在准备中

我们将需要 Pillow。可以通过以下方式安装:

$> pip install Pillow

你可以自由使用你自己的图像,也可以从这个链接下载一张:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch2/recipe5/dog.jpg

这是我们将传递给分类器的图像:

图 2.4 – 输入到预训练分类器的图像

图 2.4 – 输入到预训练分类器的图像

如何操作…

正如我们在这一节中将看到的,重用一个预训练的分类器非常简单!

  1. 导入所需的包,包括用于分类的预训练网络,以及一些预处理图像的辅助函数:

    import matplotlib.pyplot as plt
    import numpy as np
    from tensorflow.keras.applications import imagenet_utils
    from tensorflow.keras.applications.inception_v3 import *
    from tensorflow.keras.preprocessing.image import *
    
  2. 实例化一个在 ImageNet 上预训练的 InceptionV3 网络:

    model = InceptionV3(weights='imagenet')
    
  3. 加载要分类的图像。InceptionV3 接受一个 299x299x3 的图像,因此我们必须相应地调整其大小:

    image = load_img('dog.jpg', target_size=(299, 299))
    
  4. 将图像转换为 numpy 数组,并将其包装成一个单例批次:

    image = img_to_array(image)
    image = np.expand_dims(image, axis=0)
    
  5. 按照 InceptionV3 的方式预处理图像:

    image = preprocess_input(image)
    
  6. 使用模型对图像进行预测,然后将预测解码为矩阵:

    predictions = model.predict(image)
    prediction_matrix = (imagenet_utils
                         .decode_predictions(predictions))
    
  7. 查看前 5 个预测及其概率:

    for i in range(5):
        _, label, probability = prediction_matrix[0][i]
        print(f'{i + 1}. {label}: {probability * 100:.3f}%')
    

    这将产生以下输出:

    1\. pug: 85.538%
    2\. French_bulldog: 0.585%
    3\. Brabancon_griffon: 0.543%
    4\. Boston_bull: 0.218%
    5\. bull_mastiff: 0.125%
    
  8. 绘制原始图像及其最可能的标签:

    _, label, _ = prediction_matrix[0][0]
    plt.figure()
    plt.title(f'Label: {label}.')
    original = load_img('dog.jpg')
    original = img_to_array(original)
    plt.imshow(original / 255.0)
    plt.show()
    

    这个块生成了以下图像:

图 2.5 – 正确分类的图像

图 2.5 – 正确分类的图像

让我们在下一节看看它是如何工作的。

如何操作…

如此所示,为了轻松分类图像,只需使用在 ImageNet 上预训练的网络,我们只需要用正确的权重实例化适当的模型,像这样:InceptionV3(weights='imagenet')。如果这是第一次使用它,它将下载架构和权重;否则,这些文件的版本将被缓存到我们的系统中。

然后,我们加载了我们想要分类的图像,将其调整为与InceptionV3兼容的尺寸(299x299x3),通过np.expand_dims(image, axis=0)将其转换为单例批次,并以InceptionV3训练时相同的方式进行预处理,使用preprocess_input(image)

接下来,我们从模型中获取预测结果,我们需要借助imagenet_utils.decode_predictions(predictions)将其转换为预测矩阵。这个矩阵在第 0 行包含了标签和概率,我们检查了它以获取最有可能的五个类别。

另见

你可以在这里阅读更多关于 Keras 预训练模型的内容:www.tensorflow.org/api_docs/python/tf/keras/applications

使用 TensorFlow Hub 通过预训练网络对图像进行分类

TensorFlow Hub (TFHub) 是一个包含数百个机器学习模型的仓库,受到了围绕 TensorFlow 的庞大社区的贡献。在这里,我们可以找到多种不同任务的模型,不仅限于计算机视觉,还包括许多其他领域的应用,如自然语言处理 (NLP)和强化学习。

在这个食谱中,我们将使用一个在 ImageNet 上训练的模型,该模型托管在 TFHub 上,用来对自定义图像进行预测。让我们开始吧!

准备工作

我们需要tensorflow-hubPillow包,它们可以通过pip轻松安装,如下所示:

$> pip install tensorflow-hub Pillow

如果你想使用我们在这个食谱中使用的相同图像,可以在这里下载:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch2/recipe6/beetle.jpg

这是我们将要分类的图像:

图 2.6 – 待分类的图像

图 2.6 – 待分类的图像

我们进入下一节。

如何操作…

让我们继续食谱步骤:

  1. 导入必要的包:

    import matplotlib.pyplot as plt
    import numpy as np
    import tensorflow_hub as hub
    from tensorflow.keras import Sequential
    from tensorflow.keras.preprocessing.image import *
    from tensorflow.keras.utils import get_file
    
  2. 定义预训练的ResNetV2152分类器的 URL 地址,托管在TFHub上:

    classifier_url = ('https://tfhub.dev/google/imagenet/'
                      'resnet_v2_152/classification/4')
    
  3. 下载并实例化托管在 TFHub 上的分类器:

    model = Sequential([
        hub.KerasLayer(classifier_url, input_shape=(224, 
                                                  224, 3))])
    
  4. 加载我们要分类的图像,将其转换为numpy数组,对其进行归一化,并将其包装为单例批次:

    image = load_img('beetle.jpg', target_size=(224, 224))
    image = img_to_array(image)
    image = image / 255.0
    image = np.expand_dims(image, axis=0)
    
  5. 使用预训练模型对图像进行分类:

    predictions = model.predict(image)
    
  6. 提取最可能的预测的索引:

    predicted_index = np.argmax(predictions[0], axis=-1)
    
  7. 下载名为ImageNetLabels.txt的 ImageNet 标签文件:

    file_name = 'ImageNetLabels.txt'
    file_url = ('https://storage.googleapis.com/'
        'download.tensorflow.org/data/ImageNetLabels.txt')
             labels_path = get_file(file_name, file_url)
    
  8. 将标签读取到numpy数组中:

    with open(labels_path) as f:
        imagenet_labels = np.array(f.read().splitlines())
    
  9. 提取与最可能的预测索引对应的类别名称:

    predicted_class = imagenet_labels[predicted_index]
    
  10. 绘制原始图像及其最可能的标签:

    plt.figure()
    plt.title(f'Label: {predicted_class}.')
    original = load_img('beetle.jpg')
    original = img_to_array(original)
    plt.imshow(original / 255.0)
    plt.show()
    

    这将生成以下结果:

图 2.7 – 正确分类的图像

图 2.7 – 正确分类的图像

让我们看看这一切是如何工作的。

它是如何工作的…

在导入相关包后,我们继续定义我们想要用来分类输入图像的模型 URL。为了下载并将这个网络转换为 Keras 模型,我们在步骤 3 中使用了方便的 hub.KerasLayer 类。然后,在步骤 4 中,我们将想要分类的图像加载到内存中,确保它的维度与网络所期望的相匹配:224x224x3。

步骤 56 分别执行分类和提取最可能的类别。然而,为了使这个预测对人类可读,我们在步骤 7 中下载了一个包含所有 ImageNet 标签的纯文本文件,然后使用 numpy 解析它,这样我们就可以使用最可能类别的索引来获取对应的标签,最终在步骤 10 中与输入图像一起显示出来。

另见

你可以在这里了解更多关于我们使用的预训练模型:tfhub.dev/google/imagenet/resnet_v2_152/classification/4

使用数据增强通过 Keras API 提高性能

通常情况下,我们可以通过为模型提供更多的数据来提高效果。但数据是昂贵且稀缺的。有办法绕过这一限制吗?是的,有办法!我们可以通过对已有数据进行一些小的修改,如随机旋转、随机裁剪和水平翻转等,来合成新的训练样本。在本食谱中,我们将学习如何使用 Keras API 进行数据增强来提升性能。

让我们开始吧。

准备开始

我们必须安装 Pillowtensorflow_docs

$> pip install Pillow git+https://github.com/tensorflow/docs

在这个食谱中,我们将使用 Caltech 101 数据集,可以在这里找到:www.vision.caltech.edu/Image_Datasets/Caltech101/。下载并解压 101_ObjectCategories.tar.gz 到你选择的位置。从现在开始,我们假设数据位于 ~/.keras/datasets 目录下,文件名为 101_ObjectCategories

这里是来自Caltech 101的数据集的示例图像:

图 2.8 – Caltech 101 示例图像

图 2.8 – Caltech 101 示例图像

让我们实现吧!

如何实现…

这里列出的步骤是完成该食谱所必需的。让我们开始吧!

  1. 导入所需的模块:

    import os
    import pathlib
    import matplotlib.pyplot as plt
    import numpy as np
    import tensorflow_docs as tfdocs
    import tensorflow_docs.plots
    from glob import glob
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import LabelBinarizer
    from tensorflow.keras.layers import *
    from tensorflow.keras.models import Model
    from tensorflow.keras.preprocessing.image import *
    
  2. 定义一个函数,根据文件路径加载数据集中的所有图像及其标签:

    def load_images_and_labels(image_paths, target_size=(64, 64)):
        images = []
        labels = []
        for image_path in image_paths:
            image = load_img(image_path, 
                             target_size=target_size)
            image = img_to_array(image)
            label = image_path.split(os.path.sep)[-2]
            images.append(image)
            labels.append(label)
        return np.array(images), np.array(labels)
    
  3. 定义一个函数来构建一个更小版本的VGG

    def build_network(width, height, depth, classes):
        input_layer = Input(shape=(width, height, depth))
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same')(input_layer)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
        x = Flatten()(x)
        x = Dense(units=512)(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Dropout(rate=0.25)(x)
        x = Dense(units=classes)(x)
        output = Softmax()(x)
        return Model(input_layer, output)
    
  4. 定义一个函数来绘制并保存模型的训练曲线:

    def plot_model_history(model_history, metric, 
                           plot_name):
        plt.style.use('seaborn-darkgrid')
        plotter = tfdocs.plots.HistoryPlotter()
        plotter.plot({'Model': model_history}, 
                      metric=metric)
        plt.title(f'{metric.upper()}')
        plt.ylim([0, 1])
        plt.savefig(f'{plot_name}.png')
        plt.close()
    
  5. 设置随机种子:

    SEED = 999
    np.random.seed(SEED)
    
  6. 加载数据集中所有图像的路径,除了 BACKGROUND_Google 类别的图像:

    base_path = (pathlib.Path.home() / '.keras' / 
                 'datasets' /
                 '101_ObjectCategories')
    images_pattern = str(base_path / '*' / '*.jpg')
    image_paths = [*glob(images_pattern)]
    image_paths = [p for p in image_paths if
                   p.split(os.path.sep)[-2] !=
                  'BACKGROUND_Google']
    
  7. 计算数据集中的类别集合:

    classes = {p.split(os.path.sep)[-2] for p in 
              image_paths}
    
  8. 将数据集加载到内存中,对图像进行归一化处理并进行 one-hot 编码标签:

    X, y = load_images_and_labels(image_paths)
    X = X.astype('float') / 255.0
    y = LabelBinarizer().fit_transform(y)
    
  9. 创建训练和测试子集:

    (X_train, X_test,
     y_train, y_test) = train_test_split(X, y,
                                         test_size=0.2,
                                        random_state=SEED)
    
  10. 构建、编译、训练并评估一个没有数据增强的神经网络:

    EPOCHS = 40
    BATCH_SIZE = 64
    model = build_network(64, 64, 3, len(classes))
    model.compile(loss='categorical_crossentropy',
                  optimizer='rmsprop',
                  metrics=['accuracy'])
    history = model.fit(X_train, y_train,
                        validation_data=(X_test, y_test),
                        epochs=EPOCHS,
                        batch_size=BATCH_SIZE)
    result = model.evaluate(X_test, y_test)
    print(f'Test accuracy: {result[1]}')
    plot_model_history(history, 'accuracy', 'normal')
    

    测试集上的准确率如下:

    Test accuracy: 0.61347926
    

    这里是准确率曲线:

    图 2.9 – 没有数据增强的网络训练和验证准确率

    图 2.9 – 没有数据增强的网络训练和验证准确率

  11. 构建、编译、训练并评估相同的网络,这次使用数据增强:

    model = build_network(64, 64, 3, len(classes))
    model.compile(loss='categorical_crossentropy',
                  optimizer='rmsprop',
                  metrics=['accuracy'])
    augmenter = ImageDataGenerator(horizontal_flip=True,
                                   rotation_range=30,
                                   width_shift_range=0.1,
                                   height_shift_range=0.1,
                                   shear_range=0.2,
                                   zoom_range=0.2,
                                   fill_mode='nearest')
    train_generator = augmenter.flow(X_train, y_train, 
                                      BATCH_SIZE)
    hist = model.fit(train_generator,
                     steps_per_epoch=len(X_train) // 
                     BATCH_SIZE,
                     validation_data=(X_test, y_test),
                     epochs=EPOCHS)
    result = model.evaluate(X_test, y_test)
    print(f'Test accuracy: {result[1]}')
    plot_model_history(hist, 'accuracy', 'augmented')
    

    使用数据增强时,我们在测试集上的准确率如下:

    Test accuracy: 0.65207374
    

    准确率曲线如下所示:

图 2.10 – 使用数据增强的网络训练和验证准确率

图 2.10 – 使用数据增强的网络训练和验证准确率

比较步骤 10步骤 11,我们观察到通过使用数据增强,性能有了显著提升。让我们在下一节中更好地理解我们做了什么。

它是如何工作的……

在本示例中,我们实现了一个简化版的Caltech 101数据集。首先,我们只用原始数据训练一个网络,然后使用数据增强。第一个网络(见步骤 10)在测试集上的准确率为 61.3%,并且明显表现出过拟合的迹象,因为训练和验证准确率曲线之间的差距非常大。另一方面,通过应用一系列随机扰动,使用ImageDataGenerator(),例如水平翻转、旋转、宽度和高度平移等(见步骤 11),我们将测试集上的准确率提高到了 65.2%。此外,这次训练和验证准确率曲线之间的差距明显缩小,这表明数据增强的应用产生了正则化效应。

另见

您可以在这里了解更多关于Caltech 101的信息:www.vision.caltech.edu/Image_Datasets/Caltech101/

使用数据增强来提高性能,使用 tf.data 和 tf.image API

数据增强是一个强大的技术,我们可以通过创建稍作修改的图像副本,人工增加数据集的大小。在本示例中,我们将利用tf.datatf.image API 来提高在具有挑战性的Caltech 101数据集上训练的 CNN 性能。

准备工作

我们必须安装tensorflow_docs

$> pip install git+https://github.com/tensorflow/docs

在本示例中,我们将使用Caltech 101数据集,您可以在这里找到:www.vision.caltech.edu/Image_Datasets/Caltech101/。下载并解压101_ObjectCategories.tar.gz到您喜欢的位置。从现在开始,我们假设数据位于~/.keras/datasets目录下的名为101_ObjectCategories的文件夹中。

这里是Caltech 101的一些示例图像:

图 2.11 – Caltech 101 示例图像

图 2.11 – Caltech 101 示例图像

让我们进入下一节。

如何操作……

让我们回顾一下完成此任务所需的步骤。

  1. 导入必要的依赖项:

    import os
    import pathlib
    import matplotlib.pyplot as plt
    import numpy as np
    import tensorflow as tf
    import tensorflow_docs as tfdocs
    import tensorflow_docs.plots
    from glob import glob
    from sklearn.model_selection import train_test_split
    from tensorflow.keras.layers import *
    from tensorflow.keras.models import Model
    
  2. tf.data.experimental.AUTOTUNE标志创建一个别名,稍后我们将使用它:

    AUTOTUNE = tf.data.experimental.AUTOTUNE
    
  3. 定义一个函数来创建一个更小版本的VGG。首先创建输入层和第一组具有 32 个滤波器的两个卷积层:

    def build_network(width, height, depth, classes):
        input_layer = Input(shape=(width, height, depth))
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same')(input_layer)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
    
  4. 继续进行第二个包含两个卷积层的模块,这次每个模块有 64 个卷积核:

        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
    
  5. 定义架构的最后部分,其中包括一系列全连接层:

        x = Flatten()(x)
        x = Dense(units=512)(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Dropout(rate=0.5)(x)
        x = Dense(units=classes)(x)
        output = Softmax()(x)
        return Model(input_layer, output)
    
  6. 定义一个函数,根据模型的训练历史绘制并保存训练曲线:

    def plot_model_history(model_history, metric, 
                           plot_name):
        plt.style.use('seaborn-darkgrid')
        plotter = tfdocs.plots.HistoryPlotter()
        plotter.plot({'Model': model_history}, 
                      metric=metric)
        plt.title(f'{metric.upper()}')
        plt.ylim([0, 1])
        plt.savefig(f'{plot_name}.png')
        plt.close()
    
  7. 定义一个函数来加载图像并进行独热编码标签,基于图像的文件路径:

    def load_image_and_label(image_path, target_size=(64, 
                                                     64)):
        image = tf.io.read_file(image_path)
        image = tf.image.decode_jpeg(image, channels=3)
        image = tf.image.convert_image_dtype(image, 
                                             np.float32)
        image = tf.image.resize(image, target_size)
        label = tf.strings.split(image_path, os.path.sep)[-2]
        label = (label == CLASSES)  # One-hot encode.
        label = tf.dtypes.cast(label, tf.float32)
        return image, label
    
  8. 定义一个函数,通过对图像执行随机变换来增强图像:

    def augment(image, label):
        image = tf.image.resize_with_crop_or_pad(image, 
                                                 74, 74)
        image = tf.image.random_crop(image, size=(64, 64, 3))
        image = tf.image.random_flip_left_right(image)
        image = tf.image.random_brightness(image, 0.2)
        return image, label
    
  9. 定义一个函数来准备基于类似 glob 模式的tf.data.Dataset图像集,该模式指向图像所在的文件夹:

    def prepare_dataset(data_pattern):
        return (tf.data.Dataset
                .from_tensor_slices(data_pattern)
                .map(load_image_and_label,
                     num_parallel_calls=AUTOTUNE))
    
  10. 设置随机种子:

    SEED = 999
    np.random.seed(SEED)
    
  11. 加载数据集中所有图像的路径,排除BACKGROUND_Google类别的图像:

    base_path = (pathlib.Path.home() / '.keras' / 
                 'datasets' /
                 '101_ObjectCategories')
    images_pattern = str(base_path / '*' / '*.jpg')
    image_paths = [*glob(images_pattern)]
    image_paths = [p for p in image_paths if
                   p.split(os.path.sep)[-2] !=
                  'BACKGROUND_Google']
    
  12. 计算数据集中的唯一类别:

    CLASSES = np.unique([p.split(os.path.sep)[-2]
                         for p in image_paths])
    
  13. 将图像路径分割成训练集和测试集:

    train_paths, test_paths = train_test_split(image_paths,
                                              test_size=0.2,
                                          random_state=SEED)
    
  14. 准备训练和测试数据集,不进行数据增强:

    BATCH_SIZE = 64
    BUFFER_SIZE = 1024
    train_dataset = (prepare_dataset(train_paths)
                     .batch(BATCH_SIZE)
                     .shuffle(buffer_size=BUFFER_SIZE)
                     .prefetch(buffer_size=BUFFER_SIZE))
    test_dataset = (prepare_dataset(test_paths)
                    .batch(BATCH_SIZE)
                    .prefetch(buffer_size=BUFFER_SIZE))
    
  15. 实例化、编译、训练并评估网络:

    EPOCHS = 40
    model = build_network(64, 64, 3, len(CLASSES))
    model.compile(loss='categorical_crossentropy',
                  optimizer='rmsprop',
                  metrics=['accuracy'])
    history = model.fit(train_dataset,
                        validation_data=test_dataset,
                        epochs=EPOCHS)
    result = model.evaluate(test_dataset)
    print(f'Test accuracy: {result[1]}')
    plot_model_history(history, 'accuracy', 'normal')
    

    在测试集上的精度是:

    Test accuracy: 0.6532258
    

    这是精度曲线:

    图 2.12 – 没有数据增强的网络的训练和验证精度

    图 2.12 – 没有数据增强的网络的训练和验证精度

  16. 准备训练和测试集,这次对训练集应用数据增强:

    train_dataset = (prepare_dataset(train_paths)
                     .map(augment, 
                         num_parallel_calls=AUTOTUNE)
                     .batch(BATCH_SIZE)
                     .shuffle(buffer_size=BUFFER_SIZE)
                     .prefetch(buffer_size=BUFFER_SIZE))
    test_dataset = (prepare_dataset(test_paths)
                    .batch(BATCH_SIZE)
                    .prefetch(buffer_size=BUFFER_SIZE))
    
  17. 实例化、编译、训练并在增强数据上评估网络:

    model = build_network(64, 64, 3, len(CLASSES))
    model.compile(loss='categorical_crossentropy',
                  optimizer='rmsprop',
                  metrics=['accuracy'])
    history = model.fit(train_dataset,
                        validation_data=test_dataset,
                        epochs=EPOCHS)
    result = model.evaluate(test_dataset)
    print(f'Test accuracy: {result[1]}')
    plot_model_history(history, 'accuracy', 'augmented')
    

    使用数据增强时,测试集上的精度如下所示:

    Test accuracy: 0.74711984
    

    精度曲线如下所示:

图 2.13 – 具有数据增强的网络的训练和验证精度

图 2.13 – 具有数据增强的网络的训练和验证精度

让我们在下一节中了解我们刚刚做了什么。

它是如何工作的……

我们刚刚实现了著名的Caltech 101数据集的精简版本。为了更好地理解数据增强的优势,我们在原始数据上拟合了第一个版本,未进行任何修改,在测试集上的准确率为 65.32%。该模型表现出过拟合的迹象,因为训练和验证精度曲线之间的差距在训练初期就开始变宽。

接下来,我们在增强数据集上训练了相同的网络(参见步骤 15),使用之前定义的augment()函数。这极大地提升了模型性能,在测试集上达到了 74.19%的准确率。此外,训练和验证精度曲线之间的差距明显缩小,这表明数据增强的应用具有正则化效果。

另见

你可以在这里了解更多关于Caltech 101的信息:www.vision.caltech.edu/Image_Datasets/Caltech101/

第三章:第三章:利用预训练网络的迁移学习威力

尽管深度神经网络为计算机视觉带来了不可否认的强大力量,但它们在调整、训练和提高性能方面非常复杂。这种难度来自三个主要来源:

  • 深度神经网络通常在数据充足时才会发挥作用,但往往并非如此。此外,数据既昂贵又有时难以扩展。

  • 深度神经网络包含许多需要调节的参数,这些参数会影响模型的整体表现。

  • 深度学习在时间、硬件和精力方面是非常资源密集型的。

别灰心!通过迁移学习,我们可以通过利用在庞大数据集(如 ImageNet)上预训练的经典架构中丰富的知识,节省大量时间和精力。而最棒的部分是?除了它是如此强大且有用的工具,迁移学习还很容易应用。在本章中,我们将学习如何做到这一点。

在本章中,我们将覆盖以下食谱:

  • 使用预训练网络实现特征提取器

  • 在提取的特征上训练一个简单的分类器

  • 检查提取器和分类器的效果

  • 使用增量学习训练分类器

  • 使用 Keras API 微调网络

  • 使用 TFHub 微调网络

我们开始吧!

技术要求

强烈建议你拥有 GPU 访问权限,因为迁移学习通常计算密集型。在每个食谱的准备工作部分,你将收到有关如何安装该食谱所需依赖项的具体说明。如果需要,你可以在这里找到本章的所有代码:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch3

查看以下链接,观看 Code in Action 视频:

bit.ly/39wR6DT

使用预训练网络实现特征提取器

利用迁移学习的最简单方法之一是将预训练模型用作特征提取器。这样,我们可以将深度学习和机器学习相结合,而这通常是我们做不到的,因为传统的机器学习算法无法处理原始图像。在这个示例中,我们将实现一个可重用的FeatureExtractor类,从一组输入图像中生成特征向量数据集,并将其保存在极速的 HDF5 格式中。

你准备好了吗?我们开始吧!

准备工作

你需要安装Pillowtqdm(我们将用它来显示一个漂亮的进度条)。幸运的是,使用pip安装非常容易:

$> pip install Pillow tqdm

我们将使用Stanford Cars数据集,你可以在这里下载:imagenet.stanford.edu/internal/car196/car_ims.tgz。将数据解压到你选择的位置。在本配方中,我们假设数据位于~/.keras/datasets目录下,名为car_ims

以下是数据集中的一些示例图像:

图 3.1 – 示例图像

图 3.1 – 示例图像

我们将把提取的特征以 HDF5 格式存储,HDF5 是一种用于在磁盘上存储非常大的数值数据集的二进制分层协议,同时保持易于访问和按行级别计算。你可以在这里了解更多关于 HDF5 的内容:portal.hdfgroup.org/display/HDF5/HDF5

如何做到这一点…

按照以下步骤完成此配方:

  1. 导入所有必要的包:

    import glob
    import os
    import pathlib
    import h5py
    import numpy as np
    import sklearn.utils as skutils
    from sklearn.preprocessing import LabelEncoder
    from tensorflow.keras.applications import imagenet_utils
    from tensorflow.keras.applications.vgg16 import VGG16
    from tensorflow.keras.preprocessing.image import *
    from tqdm import tqdm
    
  2. 定义FeatureExtractor类及其构造函数:

    class FeatureExtractor(object):
        def __init__(self,
                     model,
                     input_size,
                     label_encoder,
                     num_instances,
                     feature_size,
                     output_path,
                     features_key='features',
                     buffer_size=1000):
    
  3. 我们需要确保输出路径是可写的:

            if os.path.exists(output_path):
                error_msg = (f'{output_path} already 
                               exists. '
                             f'Please delete it and try 
                              again.')
                raise FileExistsError(error_msg)
    
  4. 现在,让我们将输入参数存储为对象成员:

            self.model = model
            self.input_size = input_size
            self.le = label_encoder
            self.feature_size = feature_size
            self.buffer_size = buffer_size
            self.buffer = {'features': [], 'labels': []}
            self.current_index = 0
    
  5. self.buffer将包含实例和标签的缓冲区,而self.current_index将指向 HDF5 数据库内数据集中的下一个空闲位置。我们现在将创建它:

            self.db = h5py.File(output_path, 'w')
            self.features = self.db.create_dataset(features_      key,
                                           (num_instances,
                                             feature_size),
                                             dtype='float')
    
    self.labels = self.db.create_dataset('labels',
                                      (num_instances,),
                                      dtype='int')
    
  6. 定义一个方法,从图像路径列表中提取特征和标签,并将它们存储到HDF5数据库中:

        def extract_features(self,
                           image_paths,
                           labels,
                           batch_size=64,
                           shuffle=True):
            if shuffle:
                image_paths, labels = 
                skutils.shuffle(image_paths,
                               labels)
            encoded_labels = self.le.fit_transform(labels)
            self._store_class_labels(self.le.classes_)
    
  7. 在对图像路径及其标签进行洗牌,并对标签进行编码和存储后,我们将遍历图像的批次,将它们传递通过预训练的网络。一旦完成,我们将把结果特征保存到 HDF5 数据库中(我们在这里使用的辅助方法稍后会定义):

            for i in tqdm(range(0, len(image_paths), 
                                batch_size)):
                batch_paths = image_paths[i: i + 
                                          batch_size]
                batch_labels = encoded_labels[i:i + 
                                             batch_size]
                batch_images = []
                for image_path in batch_paths:
                    image = load_img(image_path,
    
                            target_size=self.input_size)
                    image = img_to_array(image)
                    image = np.expand_dims(image, axis=0)
                    image = 
                   imagenet_utils.preprocess_input(image)
                    batch_images.append(image)
                batch_images = np.vstack(batch_images)
                feats = self.model.predict(batch_images,
                                    batch_size=batch_size)
                new_shape = (feats.shape[0], 
                            self.feature_size)
                feats = feats.reshape(new_shape)
                self._add(feats, batch_labels)
            self._close()
    
  8. 定义一个私有方法,将特征和标签添加到相应的数据集:

        def _add(self, rows, labels):
            self.buffer['features'].extend(rows)
            self.buffer['labels'].extend(labels)
            if len(self.buffer['features']) >= 
                                   self.buffer_size:
                self._flush()
    
  9. 定义一个私有方法,将缓冲区刷新到磁盘:

        def _flush(self):
            next_index = (self.current_index +
                          len(self.buffer['features']))
            buffer_slice = slice(self.current_index, 
                                next_index)
            self.features[buffer_slice] = 
                           self.buffer['features']
            self.labels[buffer_slice] = self.buffer['labels']
            self.current_index = next_index
            self.buffer = {'features': [], 'labels': []}
    
  10. 定义一个私有方法,将类别标签存储到 HDF5 数据库中:

        def _store_class_labels(self, class_labels):
            data_type = h5py.special_dtype(vlen=str)
            shape = (len(class_labels),)
            label_ds = self.db.create_dataset('label_names',
                          shape,
                        dtype=data_type)
            label_ds[:] = class_labels
    
  11. 定义一个私有方法,将关闭 HDF5 数据集:

        def _close(self):
            if len(self.buffer['features']) > 0:
                self._flush()
            self.db.close()
    
  12. 加载数据集中图像的路径:

    files_pattern = (pathlib.Path.home() / '.keras' / 
                    'datasets' /'car_ims' / '*.jpg')
    files_pattern = str(files_pattern)
    input_paths = [*glob.glob(files_pattern)]
    
  13. 创建输出目录。我们将创建一个旋转车图像的数据集,以便潜在的分类器可以学习如何正确地将照片恢复到原始方向,通过正确预测旋转角度:

    output_path = (pathlib.Path.home() / '.keras' / 
                  'datasets' /
                   'car_ims_rotated')
    if not os.path.exists(str(output_path)):
        os.mkdir(str(output_path))
    
  14. 创建数据集的副本,对图像进行随机旋转:

    labels = []
    output_paths = []
    for index in tqdm(range(len(input_paths))):
        image_path = input_paths[index]
        image = load_img(image_path)
        rotation_angle = np.random.choice([0, 90, 180, 270])
        rotated_image = image.rotate(rotation_angle)
        rotated_image_path = str(output_path / 
                              f'{index}.jpg')
        rotated_image.save(rotated_image_path, 'JPEG')
        output_paths.append(rotated_image_path)
        labels.append(rotation_angle)
        image.close()
        rotated_image.close()
    
  15. 实例化FeatureExtractor,并使用预训练的VGG16网络从数据集中的图像提取特征:

    features_path = str(output_path / 'features.hdf5')
    model = VGG16(weights='imagenet', include_top=False)
    fe = FeatureExtractor(model=model,
                          input_size=(224, 224, 3),
                          label_encoder=LabelEncoder(),
                          num_instances=len(input_paths),
                          feature_size=512 * 7 * 7,
                          output_path=features_path)
    
  16. 提取特征和标签:

    fe.extract_features(image_paths=output_paths, 
                        labels=labels)
    

几分钟后,应该会在~/.keras/datasets/car_ims_rotated中生成一个名为features.hdf5的文件。

它是如何工作的…

在本配方中,我们实现了一个可重用的组件,以便在 ImageNet 上使用预训练网络,如逻辑回归支持向量机

由于图像数据集通常过大,无法全部载入内存,我们选择了高性能且用户友好的 HDF5 格式,这种格式非常适合将大规模数值数据存储在磁盘上,同时保留了NumPy典型的易访问性。这意味着我们可以像操作常规NumPy数组一样与 HDF5 数据集进行交互,使其与整个SciPy生态系统兼容。

FeatureExtractor的结果是一个分层的 HDF5 文件(可以将其视为文件系统中的一个文件夹),包含三个数据集:features,包含特征向量;labels,存储编码后的标签;以及label_names,保存编码前的人类可读标签。

最后,我们使用FeatureExtractor创建了一个二进制表示的数据集,数据集包含了旋转了 0º、90º、180º或 270º的汽车图像。

提示

我们将在本章的后续教程中继续使用刚刚处理过的修改版Stanford Cars数据集。

另见

关于Stanford Cars数据集的更多信息,您可以访问官方页面:ai.stanford.edu/~jkrause/cars/car_dataset.html。要了解更多关于 HDF5 的信息,请访问 HDF Group 的官方网站:www.hdfgroup.org/

在提取特征后训练一个简单的分类器

机器学习算法并不适合直接处理张量,因此它们无法直接从图像中学习。然而,通过使用预训练的网络作为特征提取器,我们弥补了这一差距,使我们能够利用广泛流行且经过实战验证的算法,如逻辑回归决策树支持向量机

在本教程中,我们将使用在前一个教程中生成的特征(以 HDF5 格式)来训练一个图像方向检测器,以修正图像的旋转角度,将其恢复到原始状态。

准备工作

正如我们在本教程的介绍中提到的,我们将使用在前一个教程中生成的features.hdf5数据集,该数据集包含了来自Stanford Cars数据集的旋转图像的编码信息。我们假设该数据集位于以下位置:~/.keras/datasets/car_ims_rotated/features.hdf5

以下是一些旋转的样本:

图 3.2 – 一辆旋转了 180º的汽车(左),以及另一辆旋转了 90º的汽车(右)

图 3.2 – 一辆旋转了 180º的汽车(左),以及另一辆旋转了 90º的汽车(右)

让我们开始吧!

如何操作…

按照以下步骤完成本教程:

  1. 导入所需的包:

    import pathlib
    import h5py
    from sklearn.linear_model import LogisticRegressionCV
    from sklearn.metrics import classification_report
    
  2. 加载 HDF5 格式的数据集:

    dataset_path = str(pathlib.Path.home()/'.keras'/'datasets'/'car_ims_rotated'/'features.hdf5')
    db = h5py.File(dataset_path, 'r')
    
  3. 由于数据集太大,我们只处理 50%的数据。以下代码将特征和标签分成两半:

    SUBSET_INDEX = int(db['labels'].shape[0] * 0.5)
    features = db['features'][:SUBSET_INDEX]
    labels = db['labels'][:SUBSET_INDEX]
    
  4. 取数据的前 80%来训练模型,其余 20%用于之后的评估:

    TRAIN_PROPORTION = 0.8
    SPLIT_INDEX = int(len(labels) * TRAIN_PROPORTION)
    X_train, y_train = (features[:SPLIT_INDEX],
                        labels[:SPLIT_INDEX])
    X_test, y_test = (features[SPLIT_INDEX:],
                      labels[SPLIT_INDEX:])
    
  5. 训练一个交叉验证的LogisticRegressionCV,通过交叉验证找到最佳的C参数:

    model = LogisticRegressionCV(n_jobs=-1)
    model.fit(X_train, y_train)
    

    请注意,n_jobs=-1意味着我们将使用所有可用的核心来并行寻找最佳模型。您可以根据硬件的性能调整此值。

  6. 在测试集上评估模型。我们将计算分类报告,以获得模型性能的细节:

    predictions = model.predict(X_test)
    report = classification_report(y_test, predictions,
                           target_names=db['label_names'])
    print(report)
    

    这将打印以下报告:

                  precision    recall  f1-score   support
               0       1.00      1.00      1.00       404
              90       0.98      0.99      0.99       373
             180       0.99      1.00      1.00       409
             270       1.00      0.98      0.99       433
        accuracy                           0.99      1619
       macro avg       0.99      0.99      0.99      1619
    weighted avg       0.99      0.99      0.99      1619
    

    该模型在区分四个类别方面表现良好,在测试集上达到了 99%的整体准确率!

  7. 最后,关闭 HDF5 文件以释放任何资源:

    db.close()                    
    

我们将在下一节中了解这一切如何工作。

它是如何工作的…

我们刚刚训练了一个非常简单的逻辑回归模型,用于检测图像的旋转角度。为了实现这一点,我们利用了使用预训练VGG16网络在 ImageNet 上提取的丰富且富有表现力的特征(若需要更详细的解释,请参考本章的第一个食谱)。

由于数据量过大,而scikit-learn的机器学习算法一次性处理所有数据(更具体来说,大多数算法无法批处理数据),我们只使用了 50%的特征和标签,因内存限制。

几分钟后,我们在测试集上获得了惊人的 99%的表现。此外,通过分析分类报告,我们可以看到模型对其预测非常有信心,在所有四个类别中 F1 分数至少达到了 0.99。

另见

有关如何从预训练网络中提取特征的更多信息,请参阅本章的使用预训练网络实现特征提取器一节。

快速检查提取器和分类器

在处理一个新项目时,我们常常成为选择悖论的受害者:由于有太多选择,我们不知道从哪里或如何开始。哪个特征提取器最好?我们能训练出最具性能的模型吗?我们应该如何预处理数据?

在本食谱中,我们将实现一个框架,自动快速检查特征提取器和分类器。目标不是立即获得最好的模型,而是缩小选择范围,以便在后期专注于最有前景的选项。

准备工作

首先,我们需要安装Pillowtqdm

$> pip install Pillow tqdm

我们将使用一个名为17 Category Flower Dataset的数据集,下载地址:www.robots.ox.ac.uk/~vgg/data/flowers/17。不过,也可以下载一个整理好的版本,该版本按照类别组织成子文件夹,下载地址:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch3/recipe3/flowers17.zip。请将其解压到您喜欢的位置。在本食谱中,我们假设数据位于~/.keras/datasets目录下,名称为flowers17

最后,我们将重用在本章开头的使用预训练网络实现特征提取器食谱中定义的FeatureExtractor()类。如果你想了解更多,可以参考它。

以下是来自本食谱数据集17 类别花卉数据集的一些示例图像:

图 3.3 – 示例图像

图 3.3 – 示例图像

准备工作完成后,让我们开始吧!

它是如何实现的……

以下步骤将帮助我们对几种特征提取器和机器学习算法的组合进行抽查。按照以下步骤完成本食谱:

  1. 导入必要的包:

    import json
    import os
    import pathlib
    from glob import glob
    import h5py
    from sklearn.ensemble import *
    from sklearn.linear_model import *
    from sklearn.metrics import accuracy_score
    from sklearn.neighbors import KNeighborsClassifier
    from sklearn.preprocessing import LabelEncoder
    from sklearn.svm import LinearSVC
    from sklearn.tree import *
    from tensorflow.keras.applications import *
    from tqdm import tqdm
    from ch3.recipe1.feature_extractor import FeatureExtractor
    
  2. 定义所有特征提取器的输入大小:

    INPUT_SIZE = (224, 224, 3)
    
  3. 定义一个函数,用于获取预训练网络的元组列表,以及它们输出的向量的维度:

    def get_pretrained_networks():
        return [
            (VGG16(input_shape=INPUT_SIZE,
                   weights='imagenet',
                   include_top=False),
             7 * 7 * 512),
            (VGG19(input_shape=INPUT_SIZE,
                   weights='imagenet',
                   include_top=False),
             7 * 7 * 512),
            (Xception(input_shape=INPUT_SIZE,
                      weights='imagenet',
                      include_top=False),
             7 * 7 * 2048),
            (ResNet152V2(input_shape=INPUT_SIZE,
                         weights='imagenet',
                         include_top=False),
             7 * 7 * 2048),
            (InceptionResNetV2(input_shape=INPUT_SIZE,
                               weights='imagenet',
                               include_top=False),
             5 * 5 * 1536)
        ]
    
  4. 定义一个返回机器学习模型dict以进行抽查的函数:

    def get_classifiers():
        models = {}
        models['LogisticRegression'] = 
                                LogisticRegression()
        models['SGDClf'] = SGDClassifier()
        models['PAClf'] = PassiveAggressiveClassifier()
        models['DecisionTreeClf'] = 
                             DecisionTreeClassifier()
        models['ExtraTreeClf'] = ExtraTreeClassifier()
        n_trees = 100
        models[f'AdaBoostClf-{n_trees}'] = \
            AdaBoostClassifier(n_estimators=n_trees)
        models[f'BaggingClf-{n_trees}'] = \
            BaggingClassifier(n_estimators=n_trees)
        models[f'RandomForestClf-{n_trees}'] = \
            RandomForestClassifier(n_estimators=n_trees)
        models[f'ExtraTreesClf-{n_trees}'] = \
            ExtraTreesClassifier(n_estimators=n_trees)
        models[f'GradientBoostingClf-{n_trees}'] = \
            GradientBoostingClassifier(n_estimators=n_trees)
        number_of_neighbors = range(3, 25)
        for n in number_of_neighbors:
            models[f'KNeighborsClf-{n}'] = \
                KNeighborsClassifier(n_neighbors=n)
        reg = [1e-3, 1e-2, 1, 10]
        for r in reg:
            models[f'LinearSVC-{r}'] = LinearSVC(C=r)
            models[f'RidgeClf-{r}'] = 
                  RidgeClassifier(alpha=r)
        print(f'Defined {len(models)} models.')
        return models
    
  5. 定义数据集的路径,以及所有图像路径的列表:

    dataset_path = (pathlib.Path.home() / '.keras' / 
                   'datasets' 'flowers17')
    files_pattern = (dataset_path / 'images' / '*' / '*.jpg')
    images_path = [*glob(str(files_pattern))]
    
  6. 将标签加载到内存中:

    labels = []
    for index in tqdm(range(len(images_path))):
        image_path = images_path[index]
        label = image_path.split(os.path.sep)[-2]
        labels.append(label)
    
  7. 定义一些变量以便跟踪抽查过程。final_report将包含每个分类器的准确率,分类器是在不同预训练网络提取的特征上训练的。best_modelbest_accuracybest_features将分别包含最佳模型的名称、准确率和生成特征的预训练网络的名称:

    final_report = {}
    best_model = None
    best_accuracy = -1
    best_features = None
    
  8. 遍历每个预训练网络,使用它从数据集中的图像提取特征:

    for model, feature_size in get_pretrained_networks():
        output_path = dataset_path / f'{model.name}_features.hdf5'
        output_path = str(output_path)
        fe = FeatureExtractor(model=model,
                              input_size=INPUT_SIZE,
                              label_encoder=LabelEncoder(),
                              num_instances=len(images_path),
                              feature_size=feature_size,
                              output_path=output_path)
        fe.extract_features(image_paths=images_path,
                            labels=labels)
    
  9. 使用 80%的数据进行训练,20%的数据进行测试:

        db = h5py.File(output_path, 'r')
        TRAIN_PROPORTION = 0.8
        SPLIT_INDEX = int(len(labels) * TRAIN_PROPORTION)
        X_train, y_train = (db['features'][:SPLIT_INDEX],
                            db['labels'][:SPLIT_INDEX])
        X_test, y_test = (db['features'][SPLIT_INDEX:],
                          db['labels'][SPLIT_INDEX:])
        classifiers_report = {
            'extractor': model.name
        }
        print(f'Spot-checking with features from 
              {model.name}')
    
  10. 使用当前迭代中提取的特征,遍历所有机器学习模型,使用训练集进行训练,并在测试集上进行评估:

        for clf_name, clf in get_classifiers().items():
            try:
                clf.fit(X_train, y_train)
            except Exception as e:
                print(f'\t{clf_name}: {e}')
                continue
            predictions = clf.predict(X_test)
            accuracy = accuracy_score(y_test, predictions)
            print(f'\t{clf_name}: {accuracy}')
            classifiers_report[clf_name] = accuracy
    
  11. 检查是否有新的最佳模型。如果是,请更新相应的变量:

            if accuracy > best_accuracy:
                best_accuracy = accuracy
                best_model = clf_name
                best_features = model.name
    
  12. 将本次迭代的结果存储在final_report中,并释放 HDF5 文件的资源:

        final_report[output_path] = classifiers_report
        db.close()
    
  13. 更新final_report,并写入最佳模型的信息。最后,将其写入磁盘:

    final_report['best_model'] = best_model
    final_report['best_accuracy'] = best_accuracy
    final_report['best_features'] = best_features
    with open('final_report.json', 'w') as f:
        json.dump(final_report, f)
    

检查final_report.json文件,我们可以看到最好的模型是PAClfPassiveAggressiveClassifier),它在测试集上的准确率为 0.934(93.4%),并且是在我们从VGG19网络提取的特征上训练的。你可以在这里查看完整的输出:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch3/recipe3/final_report.json。让我们进入下一部分,详细研究一下我们在本食谱中完成的项目。

它是如何工作的……

在本配方中,我们开发了一个框架,使我们能够自动抽查 40 种不同的机器学习算法,使用由五种不同的预训练网络生成的特征,最终进行了 200 次实验。通过这种方法的结果,我们发现,对于这个特定问题,最佳的模型组合是使用VGG19网络生成的向量训练PassiveAggressiveClassifier

请注意,我们并没有专注于实现最大性能,而是基于充分的证据做出明智的决策,决定在优化此数据集的分类器时如何合理地分配时间和资源。现在,我们知道微调被动攻击性分类器最有可能带来回报。那么,我们多久才能得出这个结论呢?几个小时,甚至几天。

让计算机完成繁重工作的好处是,我们不必猜测,同时可以将时间自由地用于其他任务。这是不是很棒?

使用增量学习训练分类器

传统机器学习库的一个问题,如scikit-learn,是它们很少提供在大规模数据上训练模型的可能性,而这恰好是深度神经网络最适合处理的数据。拥有大量数据又能有什么用,如果我们不能使用它呢?

幸运的是,有一种方法可以绕过这个限制,叫做creme,它可以在数据集过大无法加载到内存时训练分类器。

准备工作

在本配方中,我们将利用creme,这是一个专门设计用于在无法加载到内存的大数据集上训练机器学习模型的实验性库。要安装creme,请执行以下命令:

$> pip install creme==0.5.1

我们将在本章的使用预训练网络实现特征提取器配方中使用我们生成的features.hdf5数据集,该数据集包含来自Stanford Cars数据集中旋转图像的编码信息。我们假设数据集位于以下位置:~/.keras/datasets/car_ims_rotated/features.hdf5

以下是该数据集中的一些示例图像:

图 3.4 – 旋转 90º的汽车示例(左),和旋转 0º的另一辆汽车(右)

图 3.4 – 旋转 90º的汽车示例(左),和旋转 0º的另一辆汽车(右)

让我们开始吧!

如何做……

以下步骤将指导我们如何在大数据上逐步训练分类器:

  1. 导入所有必要的软件包:

    import pathlib
    import h5py
    from creme import stream
    from creme.linear_model import LogisticRegression
    from creme.metrics import Accuracy
    from creme.multiclass import OneVsRestClassifier
    from creme.preprocessing import StandardScaler
    
  2. 定义一个函数,将数据集保存为 CSV 文件:

    def write_dataset(output_path, feats, labels, 
                      batch_size):
        feature_size = feats.shape[1]
        csv_columns = ['class'] + [f'feature_{i}'
                                   for i in range(feature_        size)]
    
  3. 我们将为每个特征的类别设置一列,每个特征向量中的元素将设置多列。接下来,我们将批量写入 CSV 文件的内容,从头部开始:

        dataset_size = labels.shape[0]
        with open(output_path, 'w') as f:
            f.write(f'{“,”.join(csv_columns)}\n')
    
  4. 提取本次迭代中的批次:

            for batch_number, index in \
                    enumerate(range(0, dataset_size, 
                              batch_size)):
                print(f'Processing batch {batch_number + 
                                          1} of '
                      f'{int(dataset_size / 
                      float(batch_size))}')
                batch_feats = feats[index: index + 
                                     batch_size]
                batch_labels = labels[index: index + 
                                      batch_size]
    
  5. 现在,写入批次中的所有行:

                for label, vector in \
                        zip(batch_labels, batch_feats):
                    vector = ','.join([str(v) for v in 
                                       vector])
                    f.write(f'{label},{vector}\n')
    
  6. 加载 HDF5 格式的数据集:

    dataset_path = str(pathlib.Path.home()/'.keras'/'datasets'/'car_ims_rotated'/'features.hdf5')
    db = h5py.File(dataset_path, 'r')
    
  7. 定义分割索引,将数据分为训练集(80%)和测试集(20%):

    TRAIN_PROPORTION = 0.8
    SPLIT_INDEX = int(db['labels'].shape[0] * 
                      TRAIN_PROPORTION) 
    
  8. 将训练集和测试集子集写入磁盘,保存为 CSV 文件:

    BATCH_SIZE = 256
    write_dataset('train.csv',
                  db['features'][:SPLIT_INDEX],
                  db['labels'][:SPLIT_INDEX],
                  BATCH_SIZE)
    write_dataset('test.csv',
                  db['features'][SPLIT_INDEX:],
                  db['labels'][SPLIT_INDEX:],
                  BATCH_SIZE)
    
  9. creme要求我们将 CSV 文件中每一列的类型指定为dict实例。以下代码块指定了class应该编码为int类型,而其余列(对应特征)应该为float类型:

    FEATURE_SIZE = db['features'].shape[1]
    types = {f'feature_{i}': float for i in range(FEATURE_SIZE)}
    types['class'] = int
    
  10. 在以下代码中,我们定义了一个creme管道,每个输入在传递给分类器之前都会进行标准化。由于这是一个多类别问题,我们需要将LogisticRegressionOneVsRestClassifier包装在一起:

    model = StandardScaler()
    model |= OneVsRestClassifier(LogisticRegression())
    
  11. Accuracy定义为目标指标,并创建一个针对train.csv数据集的迭代器:

    metric = Accuracy()
    dataset = stream.iter_csv('train.csv',
                              target_name='class',
                              converters=types)
    
  12. 一次训练一个样本的分类器。每训练 100 个样本时,打印当前准确率:

    print('Training started...')
    for i, (X, y) in enumerate(dataset):
        predictions = model.predict_one(X)
        model = model.fit_one(X, y)
        metric = metric.update(y, predictions)
        if i % 100 == 0:
            print(f'Update {i} - {metric}')
    print(f'Final - {metric}')
    
  13. 创建一个针对test.csv文件的迭代器:

    metric = Accuracy()
    test_dataset = stream.iter_csv('test.csv',
                                   target_name='class',
                                   converters=types)
    
  14. 再次在测试集上评估模型,一次处理一个样本:

    print('Testing model...')
    for i, (X, y) in enumerate(test_dataset):
        predictions = model.predict_one(X)
        metric = metric.update(y, predictions)
        if i % 1000 == 0:
            print(f'(TEST) Update {i} - {metric}')
    print(f'(TEST) Final - {metric}')
    

几分钟后,我们应该会得到一个在测试集上准确率约为 99% 的模型。我们将在下一部分详细查看这个过程。

它是如何工作的…

通常情况下,尽管我们有大量的数据可用,但由于硬件或软件限制,我们无法使用所有数据(在在提取特征上训练简单分类器这一食谱中,我们只使用了 50% 的数据,因为无法将其全部保存在内存中)。然而,通过增量学习(也称为在线学习),我们可以像训练神经网络一样,以批处理的方式训练传统的机器学习模型。

在这个食谱中,为了捕获我们Stanford Cars数据集的特征向量的全部信息,我们不得不将训练集和测试集写入 CSV 文件。接下来,我们训练了LogisticRegression并将其包装在OneVsRestClassifier中,后者学习了如何检测图像特征向量中的旋转角度。最后,我们在测试集上达到了非常满意的 99% 准确率。

使用 Keras API 微调网络

或许迁移学习的最大优势之一就是能够利用预训练网络中所编码的知识带来的顺风。在这些网络中,只需交换较浅的层,我们就能在新的、无关的数据集上获得出色的表现,即使我们的数据量很小。为什么?因为底层的信息几乎是普遍适用的:它编码了适用于几乎所有计算机视觉问题的基本形式和形状。

在这个食谱中,我们将对一个小型数据集微调预训练的VGG16网络,从而实现一个原本不太可能得到的高准确率。

准备工作

我们需要Pillow来实现此食谱。可以按如下方式安装:

$> pip install Pillow

我们将使用一个名为17 Category Flower Dataset的数据集,可以通过以下链接访问:www.robots.ox.ac.uk/~vgg/data/flowers/17。该数据集的一个版本已经按照每个类的子文件夹进行组织,可以在此链接找到:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch3/recipe3/flowers17.zip。下载并解压到您选择的位置。从现在起,我们假设数据存储在~/.keras/datasets/flowers17目录中。

以下是来自该数据集的一些示例图像:

图 3.5 – 示例图像

图 3.5 – 示例图像

让我们开始吧!

如何实现…

微调很简单!按照以下步骤完成这个食谱:

  1. 导入必要的依赖项:

    import os
    import pathlib
    from glob import glob
    import numpy as np
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import LabelBinarizer
    from tensorflow.keras import Model
    from tensorflow.keras.applications import VGG16
    from tensorflow.keras.layers import *
    from tensorflow.keras.optimizers import *
    from tensorflow.keras.preprocessing.image import *
    
  2. 设置随机种子:

    SEED = 999
    
  3. 定义一个函数,从预训练模型构建一个新的网络,其中顶部的全连接层将是全新的,并且针对当前问题进行了调整:

    def build_network(base_model, classes):
        x = Flatten()(base_model.output)
        x = Dense(units=256)(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Dropout(rate=0.5)(x)
        x = Dense(units=classes)(x)
        output = Softmax()(x)
        return output
    
  4. 定义一个函数,将数据集中的图像和标签加载为NumPy数组:

    def load_images_and_labels(image_paths,
                               target_size=(256, 256)):
        images = []
        labels = []
        for image_path in image_paths:
            image = load_img(image_path, 
                             target_size=target_size)
            image = img_to_array(image)
            label = image_path.split(os.path.sep)[-2]
            images.append(image)
            labels.append(label)
        return np.array(images), np.array(labels)
    
  5. 加载图像路径并从中提取类集合:

    dataset_path = (pathlib.Path.home() / '.keras' / 
                   'datasets' /'flowers17')
    files_pattern = (dataset_path / 'images' / '*' / '*.jpg')
    image_paths = [*glob(str(files_pattern))]
    CLASSES = {p.split(os.path.sep)[-2] for p in 
               image_paths}
    
  6. 加载图像并对其进行归一化,使用LabelBinarizer()进行一热编码,并将数据拆分为训练集(80%)和测试集(20%):

    X, y = load_images_and_labels(image_paths)
    X = X.astype('float') / 255.0
    y = LabelBinarizer().fit_transform(y)
    (X_train, X_test,
     y_train, y_test) = train_test_split(X, y,
                                         test_size=0.2,
    
                                      random_state=SEED)
    
  7. 实例化一个预训练的VGG16模型,去除顶部的全连接层。指定输入形状为 256x256x3:

    base_model = VGG16(weights='imagenet',
                       include_top=False,
                       input_tensor=Input(shape=(256, 256, 
                                                  3)))
    

    冻结基础模型中的所有层。我们这样做是因为我们不希望重新训练它们,而是使用它们已有的知识:

    for layer in base_model.layers:
        layer.trainable = False
    
  8. 使用build_network()(在步骤 3中定义)构建一个完整的网络,并在其上添加一组新层:

    model = build_network(base_model, len(CLASSES))
    model = Model(base_model.input, model)
    
  9. 定义批处理大小和一组要通过ImageDataGenerator()应用的增强方法:

    BATCH_SIZE = 64
    augmenter = ImageDataGenerator(rotation_range=30,
                                   horizontal_flip=True,
                                   width_shift_range=0.1,
                                   height_shift_range=0.1,
                                   shear_range=0.2,
                                   zoom_range=0.2,
                                   fill_mode='nearest')
    train_generator = augmenter.flow(X_train, y_train, 
                                     BATCH_SIZE)
    
  10. 预热网络。这意味着我们将只训练新添加的层(其余部分被冻结),训练 20 个周期,使用RMSProp优化器,学习率为 0.001。最后,我们将在测试集上评估网络:

    WARMING_EPOCHS = 20
    model.compile(loss='categorical_crossentropy',
                  optimizer=RMSprop(lr=1e-3),
                  metrics=['accuracy'])
    model.fit(train_generator,
              steps_per_epoch=len(X_train) // BATCH_SIZE,
              validation_data=(X_test, y_test),
              epochs=WARMING_EPOCHS)
    result = model.evaluate(X_test, y_test)
    print(f'Test accuracy: {result[1]}')
    
  11. 现在,网络已经预热完毕,我们将微调基础模型的最终层,特别是从第 16 层开始(记住,索引从零开始),以及全连接层,训练 50 个周期,使用SGD优化器,学习率为 0.001:

    for layer in base_model.layers[15:]:
        layer.trainable = True
    EPOCHS = 50
    model.compile(loss='categorical_crossentropy',
                  optimizer=SGD(lr=1e-3),
                  metrics=['accuracy'])
    model.fit(train_generator,
              steps_per_epoch=len(X_train) // BATCH_SIZE,
              validation_data=(X_test, y_test),
              epochs=EPOCHS)
    result = model.evaluate(X_test, y_test)
    print(f'Test accuracy: {result[1]}')
    

在预热后,网络在测试集上的准确率达到了 81.6%。然后,当我们进行了微调后,经过 50 个周期,测试集上的准确率提高到了 94.5%。我们将在下一节看到这一过程的具体细节。

它是如何工作的…

我们成功地利用了在庞大的 ImageNet 数据库上预训练的VGG16模型的知识。通过替换顶部的全连接层,这些层负责实际分类(其余部分充当特征提取器),我们使用自己的一组深度层来适应当前问题,从而在测试集上获得了超过 94.5%的不错准确率。

这个结果展示了迁移学习的强大能力,特别是考虑到数据集中每个类别只有 81 张图片(81x17=1,377 张图片),这对于从头开始训练一个表现良好的深度学习模型来说显然是不足够的。

提示

虽然并非总是必需的,但在微调网络时,最好先热身一下头部(顶部的全连接层),让它们有时间适应来自预训练网络的特征。

另见

你可以在这里阅读更多关于 Keras 预训练模型的内容:https://www.tensorflow.org/api_docs/python/tf/keras/applications。

使用 TFHub 微调网络

微调网络的最简单方法之一是依赖于TensorFlow HubTFHub)中丰富的预训练模型。在这个任务中,我们将微调一个ResNetV1152特征提取器,以便从一个非常小的数据集中分类花卉。

准备就绪

我们需要tensorflow-hubPillow来完成这个任务。两者都可以很容易地安装,方法如下:

$> pip install tensorflow-hub Pillow

我们将使用一个名为17 类花卉数据集的数据集,您可以通过www.robots.ox.ac.uk/~vgg/data/flowers/17访问。建议你在这里获取数据的重组织版本:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch3/recipe3/flowers17.zip。下载并解压到您选择的位置。从现在开始,我们假设数据存储在~/.keras/datasets/flowers17

以下是该数据集的一些示例图片:

图 3.6 – 示例图片

图 3.6 – 示例图片

让我们开始吧!

如何做……

按照以下步骤成功完成这个任务:

  1. 导入所需的包:

    import os
    import pathlib
    from glob import glob
    import numpy as np
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import LabelBinarizer
    from tensorflow.keras import Sequential
    from tensorflow.keras.layers import *
    from tensorflow.keras.optimizers import RMSprop
    from tensorflow.keras.preprocessing.image import *
    from tensorflow_hub import KerasLayer
    
  2. 设置随机种子:

    SEED = 999
    
  3. 定义一个函数,从预训练模型构建一个新的网络,其中顶部的全连接层将是全新的,并且会根据我们数据中的类别数量进行调整:

    def build_network(base_model, classes):
        return Sequential([
            base_model,
            Dense(classes),
            Softmax()
        ])
    
  4. 定义一个函数,将数据集中的图片和标签加载为NumPy数组:

    def load_images_and_labels(image_paths,
                               target_size=(256, 256)):
        images = []
        labels = []
        for image_path in image_paths:
            image = load_img(image_path, 
                             target_size=target_size)
            image = img_to_array(image)
            label = image_path.split(os.path.sep)[-2]
            images.append(image)
            labels.append(label)
        return np.array(images), np.array(labels)
    
  5. 加载图片路径并从中提取类别集合:

    dataset_path = (pathlib.Path.home() / '.keras' / 
                     'datasets' /'flowers17')
    files_pattern = (dataset_path / 'images' / '*' / '*.jpg')
    image_paths = [*glob(str(files_pattern))]
    CLASSES = {p.split(os.path.sep)[-2] for p in image_paths}
    
  6. 加载图片并进行归一化,使用LabelBinarizer()进行独热编码标签,然后将数据拆分为训练集(80%)和测试集(20%):

    X, y = load_images_and_labels(image_paths)
    X = X.astype('float') / 255.0
    y = LabelBinarizer().fit_transform(y)
    (X_train, X_test,
     y_train, y_test) = train_test_split(X, y,
                                         test_size=0.2,
                                       random_state=SEED)
    
  7. 实例化一个预训练的KerasLayer()类,指定输入形状为 256x256x3:

    model_url = ('https://tfhub.dev/google/imagenet/'
                 'resnet_v1_152/feature_vector/4')
    base_model = KerasLayer(model_url, input_shape=(256, 
                                                  256, 3))
    

    使基础模型不可训练:

    base_model.trainable = False
    
  8. 在使用基础模型作为起点的基础上,构建完整的网络:

    model = build_network(base_model, len(CLASSES))
    
  9. 定义批量大小以及通过ImageDataGenerator()应用的一组数据增强操作:

    BATCH_SIZE = 32
    augmenter = ImageDataGenerator(rotation_range=30,
                                   horizontal_flip=True,
                                   width_shift_range=0.1,
                                   height_shift_range=0.1,
                                   shear_range=0.2,
                                   zoom_range=0.2,
                                   fill_mode='nearest')
    train_generator = augmenter.flow(X_train, y_train, 
                                     BATCH_SIZE)
    
  10. 训练整个模型 20 个 epoch,并评估其在测试集上的性能:

    EPOCHS = 20
    model.compile(loss='categorical_crossentropy',
                  optimizer=RMSprop(lr=1e-3),
                  metrics=['accuracy'])
    model.fit(train_generator,
              steps_per_epoch=len(X_train) // BATCH_SIZE,
              validation_data=(X_test, y_test),
              epochs=EPOCHS)
    result = model.evaluate(X_test, y_test)
    print(f'Test accuracy: {result[1]}')
    

只需几分钟,我们就在测试集上获得了大约 95.22%的准确率。太棒了,你不觉得吗?现在,让我们深入了解一下。

它是如何工作的……

我们利用了预训练的17 类花卉数据集中编码的知识。

通过简单地更换顶层,我们在测试集上达到了令人印象深刻的 95.22%的准确率,考虑到所有的约束,这可是一个不小的成就。

使用 Keras API 微调网络的做法不同,这次我们没有对模型的头部进行预热。再次强调,这不是硬性规定,而是我们工具箱中的另一种方法,我们应该根据具体项目尝试使用。

另见

你可以在这里阅读更多关于我们在本教程中使用的预训练模型的信息:tfhub.dev/google/imagenet/resnet_v1_152/feature_vector/4

第四章:第四章:通过 DeepDream、神经风格迁移和图像超分辨率增强和美化图像

虽然深度神经网络在传统计算机视觉任务中表现出色,尤其是在纯粹的实际应用中,但它们也有有趣的一面!正如我们将在本章中发现的那样,我们可以借助一点聪明才智和数学的帮助,解锁深度学习的艺术面。

本章将从介绍DeepDream开始,这是一种使神经网络生成梦幻般图像的算法。接下来,我们将利用迁移学习的力量,将著名画作的风格应用到我们自己的图像上(这就是神经风格迁移)。最后,我们将结束于图像超分辨率,这是一种用于提升图像质量的深度学习方法。

本章将涵盖以下食谱:

  • 实现 DeepDream

  • 生成你自己的梦幻图像

  • 实现神经风格迁移

  • 将风格迁移应用到自定义图像

  • 使用 TFHub 应用风格迁移

  • 利用深度学习提高图像分辨率

让我们开始吧!

技术要求

在进行深度学习时,一般建议适用:如果可能,使用 GPU,因为它可以大大提高效率并减少计算时间。在每个食谱中,你会在准备工作部分找到具体的准备说明(如有必要)。你可以在这里找到本章的所有代码:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch4

查看以下链接,观看代码实战视频:

bit.ly/3bDns2A

实现 DeepDream

DeepDream是一个实验的产物,旨在可视化神经网络学习到的内部模式。为了实现这一目标,我们可以将一张图像传入网络,计算它关于特定层激活的梯度,然后修改图像,以增强这些激活的幅度,从而放大模式。结果?迷幻、超现实的照片!

尽管由于DeepDream的性质,本食谱稍显复杂,但我们将一步一步来,别担心。

让我们开始吧。

准备工作

本食谱无需额外安装任何内容。不过,我们不会深入讨论DeepDream的细节,但如果你对这个话题感兴趣,可以在这里阅读 Google 的原始博客文章:ai.googleblog.com/2015/06/inceptionism-going-deeper-into-neural.html

如何操作……

按照以下步骤操作,你很快就能拥有自己的深度梦幻生成器:

  1. 导入所有必要的包:

    import numpy as np
    import tensorflow as tf
    from tensorflow.keras import Model
    from tensorflow.keras.applications.inception_v3 import *
    
  2. 定义DeepDreamer类及其构造函数:

    class DeepDreamer(object):
        def __init__(self,
                     octave_scale=1.30,
                     octave_power_factors=None,
                     layers=None):
    
  3. 构造函数参数指定了我们将如何按比例增大图像的尺寸(octave_scale),以及将应用于尺度的因子(octave_power_factors)。layers包含将用于生成梦境的目标层。接下来,我们将这些参数存储为对象成员:

            self.octave_scale = octave_scale
            if octave_power_factors is None:
                self.octave_power_factors = [*range(-2, 3)]
            else:
                self.octave_power_factors = 
                           octave_power_factors
            if layers is None:
                self.layers = ['mixed3', 'mixed5']
            else:
                self.layers = layers
    
  4. 如果某些输入是None,我们使用默认值。如果不是,我们使用输入值。最后,通过从预训练的InceptionV3网络中提取layers来创建梦境生成模型:

            self.base_model = InceptionV3(weights='imagenet',
                                         include_top=False)
            outputs = [self.base_model.get_layer(name).output
                      for name in self.layers]
            self.dreamer_model = Model(self.base_model.input,
                                       outputs)
    
  5. 定义一个私有方法来计算损失:

        def _calculate_loss(self, image):
            image_batch = tf.expand_dims(image, axis=0)
            activations = self.dreamer_model(image_batch)
            if len(activations) == 1:
                activations = [activations]
            losses = []
            for activation in activations:
                loss = tf.math.reduce_mean(activation)
                losses.append(loss)
            total_loss = tf.reduce_sum(losses)
            return total_loss
    
  6. 定义一个私有方法来执行梯度上升(记住,我们希望放大图像中的图案)。为了提高性能,我们可以将此函数封装在tf.function中:

        @tf.function
        def _gradient_ascent(self, image, steps, step_size):
            loss = tf.constant(0.0)
            for _ in range(steps):
                with tf.GradientTape() as tape:
                    tape.watch(image)
                    loss = self._calculate_loss(image)
                gradients = tape.gradient(loss, image)
                gradients /= tf.math.reduce_std(gradients) 
                                              + 1e-8
                image = image + gradients * step_size
                image = tf.clip_by_value(image, -1, 1)
            return loss, image
    
  7. 定义一个私有方法,将梦境生成器产生的图像张量转换回NumPy数组:

        def _deprocess(self, image):
            image = 255 * (image + 1.0) / 2.0
            image = tf.cast(image, tf.uint8)
            image = np.array(image)
            return image
    
  8. 定义一个私有方法,通过执行_gradient_ascent()一定步数来生成梦幻图像:

        def _dream(self, image, steps, step_size):
            image = preprocess_input(image)
            image = tf.convert_to_tensor(image)
            step_size = tf.convert_to_tensor(step_size)
            step_size = tf.constant(step_size)
            steps_remaining = steps
            current_step = 0
            while steps_remaining > 0:
                if steps_remaining > 100:
                    run_steps = tf.constant(100)
                else:
                    run_steps = 
                         tf.constant(steps_remaining)
                steps_remaining -= run_steps
                current_step += run_steps
                loss, image = self._gradient_ascent(image,
                                               run_steps,
                                               step_size)
            result = self._deprocess(image)
            return result
    
  9. 定义一个公共方法来生成梦幻图像。这与_dream()(在第 6 步中定义,并在此内部使用)之间的主要区别是,我们将使用不同的图像尺寸(称为self.octave_scale,每个self.octave_power_factors中的幂次):

        def dream(self, image, steps=100, step_size=0.01):
            image = tf.constant(np.array(image))
            base_shape = tf.shape(image)[:-1]
            base_shape = tf.cast(base_shape, tf.float32)
            for factor in self.octave_power_factors:
                new_shape = tf.cast(
                    base_shape * (self.octave_scale ** 
                                   factor),
                                   tf.int32)
                image = tf.image.resize(image, 
                                       new_shape).numpy()
                image = self._dream(image, steps=steps,
                                    step_size=step_size)
            base_shape = tf.cast(base_shape, tf.int32)
            image = tf.image.resize(image, base_shape)
            image = tf.image.convert_image_dtype(image / 
                                                  255.0,
                                           dtype=tf.uint8)
            image = np.array(image)
            return np.array(image)
    

DeepDreamer()类可以重用,用于生成我们提供的任何图像的梦幻版本。我们将在下一节中看到这个如何工作。

它是如何工作的……

我们刚刚实现了一个实用类,方便地应用DeepDream。该算法通过计算一组层的激活值的梯度,然后使用这些梯度来增强网络所见的图案。

在我们的DeepDreamer()类中,之前描述的过程已在_gradient_ascent()方法中实现(在第 4 步中定义),我们计算了梯度并将其添加到原始图像中,通过多个步骤得到结果。最终结果是一个激活图,其中在每个后续步骤中,目标层中某些神经元的兴奋度被放大。

生成梦境的过程包括多次应用梯度上升,我们在_dream()方法中基本上已经做了这个(第 6 步)。

应用梯度上升于相同尺度的一个问题是,结果看起来噪声较大,分辨率低。而且,图案似乎发生在相同的粒度层级,这会导致结果的均匀性,从而减少我们想要的梦幻效果。为了解决这些问题,主要方法dream()在不同的尺度上应用梯度上升(称为八度音阶),其中一个八度的梦幻输出作为下一次迭代的输入,并且在更高的尺度上进行处理。

另见

要查看将不同参数组合传递给DeepDreamer()后的梦幻效果,请参阅下一篇食谱,生成你自己的梦幻图像

生成你自己的梦幻图像

深度学习有一个有趣的方面。DeepDream 是一个应用程序,旨在通过激活特定层的某些激活点来理解深度神经网络的内部工作原理。然而,除了实验的调查意图外,它还产生了迷幻、梦幻般有趣的图像。

在这个配方中,我们将尝试几种DeepDream的配置,看看它们如何影响结果。

准备开始

我们将使用本章第一个配方中的DeepDreamer()实现(实现 DeepDream)。虽然我鼓励你尝试用自己的图像进行测试,但如果你想尽量跟随这个配方,你可以在这里下载示例图像:https://github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch4/recipe2/road.jpg。

让我们来看一下示例图像:

图 4.1 – 示例图像

图 4.1 – 示例图像

让我们开始吧。

如何实现……

按照以下步骤制作你自己的梦幻照片:

  1. 让我们从导入所需的包开始。请注意,我们正在从之前的配方中导入DeepDreamer()实现 DeepDream

    import matplotlib.pyplot as plt
    from tensorflow.keras.preprocessing.image import *
    from ch4.recipe1.deepdream import DeepDreamer
    
  2. 定义load_image()函数,从磁盘加载图像到内存中,作为NumPy数组:

    def load_image(image_path):
        image = load_img(image_path)
        image = img_to_array(image)
        return image
    
  3. 定义一个函数,使用matplotlib显示图像(以NumPy数组表示):

    def show_image(image):
        plt.imshow(image)
        plt.show()
    
  4. 加载原始图像并显示:

    original_image = load_image('road.jpg')
    show_image(original_image / 255.0)
    

    这里,我们可以看到显示的原始图像:

    图 4.2 – 我们将很快修改的原始图像

    图 4.2 – 我们将很快修改的原始图像

    如我们所见,这只是穿过森林的一条道路。

  5. 使用默认参数生成图像的梦幻版,并显示结果:

    dreamy_image = DeepDreamer().dream(original_image)
    show_image(dreamy_image)
    

    这是结果:

    图 4.3 – 使用默认参数的 DeepDream 结果

    图 4.3 – 使用默认参数的 DeepDream 结果

    结果保留了原始照片的整体主题,但在其上添加了大量失真,形成了圆形、曲线和其他基本图案。酷——又有点怪异!

  6. 使用三层。靠近顶部的层(例如,'mixed7')编码更高层次的模式:

    dreamy_image = (DeepDreamer(layers=['mixed2',
                                        'mixed5',
                                        'mixed7'])
                    .dream(original_image))
    show_image(dreamy_image)
    

    这是使用三层后的结果:

    图 4.4 – 使用更多更高层次层的 DeepDream 结果

    图 4.4 – 使用更多更高层次层的 DeepDream 结果

    更多层的加入让生成的梦幻效果变得更柔和。我们可以看到,图案比以前更平滑,这很可能是因为'mixed7'层编码了更多的抽象信息,因为它离网络架构的末端更远。我们记得,网络中的前几层学习基本的模式,如线条和形状,而靠近输出的层则将这些基本模式组合起来,学习更复杂、更抽象的信息。

  7. 最后,让我们使用更多的八度音阶。我们期望的结果是图像中噪声较少,且具有更多异质模式:

    dreamy_image = (DeepDreamer(octave_power_factors=[-3, -1,
                                                      0, 3])
                    .dream(original_image))
    show_image(dreamy_image)                
    

    这是使用更多八度后得到的结果图像:

图 4.5 – 使用更多八度的 DeepDream 效果

图 4.5 – 使用更多八度的 DeepDream 效果

生成的梦境包含了一种令人满意的高低层次模式的混合,并且比步骤 4中生成的梦境具有更好的色彩分布。

让我们进入下一部分,了解我们刚刚做了什么。

它是如何工作的……

在这个食谱中,我们利用了在实现 DeepDream食谱中所做的工作,生成了几种我们输入图像(森林中的一条道路)的梦幻版本。通过结合不同的参数,我们发现结果可以有很大的变化。使用更高层次的抽象信息,我们获得了噪音更少、模式更精细的图片。

如果我们选择使用更多八度,这意味着网络将处理更多的图像,且在不同的尺度上进行处理。这种方法生成的图像饱和度较低,同时保留了卷积神经网络前几层中典型的更原始、更基本的模式。

最后,通过仅使用一张图片和一点创造力,我们可以获得非常有趣的结果!

深度学习的另一个更具娱乐性的应用是神经风格迁移,我们将在下一个食谱中讲解。

实现神经风格迁移

创造力和艺术表现并不是我们通常将其与深度神经网络和人工智能相联系的特征。然而,你知道吗,通过正确的调整,我们可以将预训练网络转变为令人印象深刻的艺术家,能够将像莫奈、毕加索和梵高这样的著名画家的独特风格应用到我们的平凡照片中?

这正是神经风格迁移的工作原理。通过这个食谱的学习,最终我们将掌握任何画家的艺术造诣!

正在准备中

我们不需要安装任何库或引入额外的资源来实现神经风格迁移。然而,由于这是一个实践性的食谱,我们不会详细描述解决方案的内部工作原理。如果你对神经风格迁移的细节感兴趣,建议阅读原始论文:arxiv.org/abs/1508.06576

我希望你已经准备好,因为我们马上就要开始了!

如何操作……

按照这些步骤实现你自己的可重用神经风格迁移器:

  1. 导入必要的包(注意,在我们的实现中,我们使用了一个预训练的VGG19网络):

    import numpy as np
    import tensorflow as tf
    from tensorflow.keras import Model
    from tensorflow.keras.applications.vgg19 import *
    
  2. 定义StyleTransferrer()类及其构造函数:

    class StyleTransferrer(object):
        def __init__(self,
                     content_layers=None,
                     style_layers=None):
    
  3. 唯一相关的参数是内容和风格生成的两个可选层列表。如果它们是None,我们将在内部使用默认值(稍后我们会看到)。接下来,加载预训练的VGG19并将其冻结:

            self.model = VGG19(weights='imagenet',
                               include_top=False)
            self.model.trainable = False
    
  4. 设置风格和内容损失的权重(重要性)(稍后我们会使用这些参数)。另外,存储内容和风格层(如果需要的话,可以使用默认设置):

            self.style_weight = 1e-2
            self.content_weight = 1e4
            if content_layers is None:
                self.content_layers = ['block5_conv2']
            else:
                self.content_layers = content_layers
            if style_layers is None:
                self.style_layers = ['block1_conv1',
                                     'block2_conv1',
                                     'block3_conv1',
                                     'block4_conv1',
                                     'block5_conv1']
            else:
                self.style_layers = style_layers
    
  5. 定义并存储样式迁移模型,该模型以VGG19输入层为输入,输出所有内容层和样式层(请注意,我们可以使用任何模型,但通常使用 VGG19 或 InceptionV3 能获得最佳效果):

            outputs = [self.model.get_layer(name).output
                       for name in
                       (self.style_layers + 
                       self.content_layers)]
            self.style_model = Model([self.model.input], 
                                    outputs)
    
  6. 定义一个私有方法,用于计算Gram 矩阵,它用于计算图像的样式。它由一个矩阵表示,其中包含输入张量中不同特征图之间的均值和相关性(例如,特定层中的权重),被称为Gram 矩阵。有关Gram 矩阵的更多信息,请参阅本配方中的另请参阅部分:

        def _gram_matrix(self, input_tensor):
            result = tf.linalg.einsum('bijc,bijd->bcd',
                                      input_tensor,
                                      input_tensor)
            input_shape = tf.shape(input_tensor)
            num_locations = np.prod(input_shape[1:3])
            num_locations = tf.cast(num_locations,tf.float32)
            result = result / num_locations
            return result
    
  7. 接下来,定义一个私有方法,用于计算输出(内容和样式)。该私有方法的作用是将输入传递给模型,然后计算所有样式层的Gram 矩阵以及内容层的身份,返回映射每个层名称到处理后值的字典:

        def _calc_outputs(self, inputs):
            inputs = inputs * 255
            preprocessed_input = preprocess_input(inputs)
            outputs = self.style_model(preprocessed_input)
            style_outputs = outputs[:len(self.style_layers)]
            content_outputs = 
                        outputs[len(self.style_layers):]
            style_outputs = 
           [self._gram_matrix(style_output)
                             for style_output in 
                                style_outputs]
            content_dict = {content_name: value
                            for (content_name, value)
                            in zip(self.content_layers,
                                   content_outputs)}
            style_dict = {style_name: value
                          for (style_name, value)
                          in zip(self.style_layers,
                                 style_outputs)}
            return {'content': content_dict,
                    'style': style_dict}
    
  8. 定义一个静态辅助私有方法,用于将值限制在01之间:

        @staticmethod
        def _clip_0_1(image):
            return tf.clip_by_value(image,
                                    clip_value_min=0.0,
                                    clip_value_max=1.0)
    
  9. 定义一个静态辅助私有方法,用于计算一对输出和目标之间的损失:

        @staticmethod
        def _compute_loss(outputs, targets):
            return tf.add_n([
                tf.reduce_mean((outputs[key] - 
                               targets[key]) ** 2)
                for key in outputs.keys()
            ])
    
  10. 定义一个私有方法,用于计算总损失,该损失是通过分别计算样式损失和内容损失,乘以各自权重并分配到相应层,再加总得到的:

        def _calc_total_loss(self,
                             outputs,
                             style_targets,
                             content_targets):
            style_outputs = outputs['style']
            content_outputs = outputs['content']
            n_style_layers = len(self.style_layers)
            s_loss = self._compute_loss(style_outputs,
                                        style_targets)
            s_loss *= self.style_weight / n_style_layers
            n_content_layers = len(self.content_layers)
            c_loss = self._compute_loss(content_outputs,
                                        content_targets)
            c_loss *= self.content_weight / n_content_layers
            return s_loss + c_loss
    
  11. 接下来,定义一个私有方法,用于训练模型。在一定数量的 epochs 和每个 epoch 的指定步数下,我们将计算输出(样式和内容),计算总损失,并获取并应用梯度到生成的图像,同时使用Adam作为优化器:

        @tf.function()
        def _train(self,
                   image,
                   s_targets,
                   c_targets,
                   epochs,
                   steps_per_epoch):
            optimizer = 
                  tf.optimizers.Adam(learning_rate=2e-2,
                                           beta_1=0.99,
                                           epsilon=0.1)
            for _ in range(epochs):
                for _ in range(steps_per_epoch):
                    with tf.GradientTape() as tape:
                        outputs = 
                             self._calc_outputs(image)
                        loss = 
                          self._calc_total_loss(outputs,
                                               s_targets,
                                              c_targets)
                    gradient = tape.gradient(loss, image)
                    optimizer.apply_gradients([(gradient, 
                                                image)])
                    image.assign(self._clip_0_1(image))
            return image 
    
  12. 定义一个静态辅助私有方法,用于将张量转换为NumPy图像:

        @staticmethod
        def _tensor_to_image(tensor):
            tensor = tensor * 255
            tensor = np.array(tensor, dtype=np.uint8)
            if np.ndim(tensor) > 3:
                tensor = tensor[0]
            return tensor
    
  13. 最后,定义一个公共的transfer()方法,该方法接受一张样式图像和一张内容图像,生成一张新图像。它应该尽可能保留内容,同时应用样式图像的样式:

        def transfer(self, s_image, c_image, epochs=10,
                     steps_per_epoch=100):
            s_targets = self._calc_outputs(s_image)['style']
            c_targets = 
              self._calc_outputs(c_image)['content']
            image = tf.Variable(c_image)
            image = self._train(image,
                                s_targets,
                                c_targets,
                                epochs,
                                steps_per_epoch)
            return self._tensor_to_image(image)
    

这可真是费了一番功夫!我们将在下一部分深入探讨。

它是如何工作的…

在本配方中,我们学到,神经风格迁移是通过优化两个损失而不是一个来工作的。一方面,我们希望尽可能保留内容,另一方面,我们希望让这个内容看起来像是使用样式图像的风格生成的。

内容量化是通过使用内容层实现的,正如我们在图像分类中通常会做的那样。那么,如何量化样式呢?这时,Gram 矩阵发挥了至关重要的作用,因为它计算了样式层的特征图(更准确地说,是输出)之间的相关性。

我们如何告诉网络内容比风格更重要呢?通过在计算组合损失时使用权重。默认情况下,内容权重是10,000,而风格权重仅为0.01。这告诉网络它的大部分努力应该集中在重现内容上,但也要稍微优化一下风格。

最终,我们获得了一张图像,它保留了原始图像的连贯性,但却拥有了风格参考图像的视觉吸引力,这是通过优化输出,使其匹配两个输入图像的统计特征所得到的结果。

另见

如果你想了解更多关于StyleTransferrer()运作背后的数学原理,请参见下一个配方,将风格迁移应用于自定义图像

将风格迁移应用于自定义图像

你是否曾经想过,如果你最喜欢的艺术家画了你的小狗 Fluffy 的照片会是什么样子?如果你车子的照片与最喜爱的画作的魔力结合,会变成什么样?好吧,你再也不需要想象了!通过神经风格迁移,我们可以轻松地将我们最喜欢的图像变成美丽的艺术作品!

在这个配方中,我们将使用我们在实现神经风格迁移配方中实现的StyleTransferrer()类来为我们自己的图像添加风格。

准备中

在这个配方中,我们将使用上一个配方中的StyleTransferrer()实现。为了最大化您从这个配方中获得的乐趣,您可以在这里找到示例图像以及许多不同的画作(您可以用作风格参考):

github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch4/recipe4

以下是我们将使用的示例图像:

图 4.6 – 示例内容图像

图 4.6 – 示例内容图像

让我们开始吧!

如何操作…

以下步骤将教您如何将著名画作的风格转移到您自己的图像上:

  1. 导入必要的包:

    import matplotlib.pyplot as plt
    import tensorflow as tf
    from chapter4.recipe3.styletransfer import StyleTransferrer
    

    请注意,我们正在导入在实现神经风格迁移配方中实现的StyleTransferrer()

  2. 告诉 TensorFlow 我们希望以急切模式运行,因为否则,它会尝试在图模式下运行StyleTransferrer()中的tf.function装饰器函数,这将导致其无法正常工作:

    tf.config.experimental_run_functions_eagerly(True)
    
  3. 定义一个函数,将图像加载为 TensorFlow 张量。请注意,我们正在将其重新缩放到一个合理的大小。我们这样做是因为神经风格迁移是一个资源密集型的过程,因此处理大图像可能需要很长时间:

    def load_image(image_path):
        dimension = 512
        image = tf.io.read_file(image_path)
        image = tf.image.decode_jpeg(image, channels=3)
        image = tf.image.convert_image_dtype(image, 
                                             tf.float32)
        shape = tf.cast(tf.shape(image)[:-1], tf.float32)
        longest_dimension = max(shape)
        scale = dimension / longest_dimension
        new_shape = tf.cast(shape * scale, tf.int32)
        image = tf.image.resize(image, new_shape)
        return image[tf.newaxis, :]
    
  4. 定义一个函数,用于通过matplotlib显示图像:

    def show_image(image):
        if len(image.shape) > 3:
            image = tf.squeeze(image, axis=0)
        plt.imshow(image)
        plt.show()
    
  5. 加载内容图像并显示它:

    content = load_image('bmw.jpg')
    show_image(content)
    

    这是内容图像:

    图 4.7 – 一辆车的内容图像

    图 4.7 – 一辆车的内容图像

    我们将把一幅画作的风格应用到这张图像上。

  6. 加载并显示风格图像:

    style = load_image(art.jpg')
    show_image(style)
    

    这是风格图像:

    图 4.8 – 风格图像

    ](https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/tf20-cv-cb/img/B14768_04_009.jpg)

    图 4.8 – 风格图像

    你能想象如果这幅画的艺术家为我们的车绘制图像,它会是什么样子吗?

  7. 使用 StyleTransferrer() 将画作的风格应用到我们的 BMW 图像上。然后,展示结果:

    stylized_image = StyleTransferrer().transfer(style, 
                                                 content)
    show_image(stylized_image)
    

    这是结果:

    图 4.9 – 将画作的风格应用到内容图像的结果

    ](https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/tf20-cv-cb/img/B14768_04_009.jpg)

    图 4.9 – 将画作的风格应用到内容图像的结果

    惊艳吧,是不是?

  8. 重复这个过程,这次进行 100 个训练周期:

    stylized_image = StyleTransferrer().transfer(style, 
                                                 content,
                                               epochs=100)
    show_image(stylized_image)
    

    这是结果:

图 4.10 – 对内容图像应用画作风格的结果(100 个训练周期)

](https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/tf20-cv-cb/img/B14768_04_008.jpg)

图 4.10 – 对内容图像应用画作风格的结果(100 个训练周期)

这次,结果更为锐利。然而,我们不得不等一段时间才能完成这个过程。时间和质量之间的权衡非常明显。

让我们继续进入下一部分。

它是如何工作的…

在这个食谱中,我们利用了在实现神经风格迁移食谱中所做的辛勤工作。我们取了一张汽车的图像,并将一幅酷炫迷人的艺术作品风格应用到其中。正如我们所看到的,结果非常吸引人。

然而,我们必须意识到这个过程的负担,因为在 CPU 上完成它需要很长时间——即使是在 GPU 上也是如此。因此,我们需要在用于精细化结果的训练周期或迭代次数与最终输出质量之间进行权衡。

参见也

我鼓励你尝试使用自己的图片和风格来应用这个食谱。作为起点,你可以使用以下仓库中的图像来快速入门:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch4/recipe4。在那里,你将找到来自沃霍尔、马蒂斯、莫奈等人的著名艺术作品。

使用 TFHub 应用风格迁移

从零开始实现神经风格迁移是一项艰巨的任务。幸运的是,我们可以使用TensorFlow HubTFHub)中的现成解决方案。

在这个食谱中,我们只需几行代码,就能通过 TFHub 提供的工具和便捷性,快速为自己的图像添加风格。

准备工作

我们必须安装 tensorflow-hub。我们只需一个简单的 pip 命令即可完成:

$> pip install tensorflow-hub

如果你想访问不同的示例内容和风格图像,请访问这个链接:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch4/recipe5

让我们来看一下示例图像:

图 4.11 – 内容图像

](https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/tf20-cv-cb/img/B14768_04_011.jpg)

图 4.11 – 内容图像

让我们开始吧!

如何操作…

使用 TFHub 进行神经风格迁移非常简单!按照以下步骤完成此食谱:

  1. 导入必要的依赖项:

    import matplotlib.pyplot as plt
    import numpy as np
    import tensorflow as tf
    from tensorflow_hub import load
    
  2. 定义一个将图像加载为 TensorFlow 张量的函数。由于神经风格迁移是一个计算密集型的过程,因此我们需要对图像进行重缩放,以节省时间和资源,因为处理大图像可能会花费很长时间:

    def load_image(image_path):
        dimension = 512
        image = tf.io.read_file(image_path)
        image = tf.image.decode_jpeg(image, channels=3)
        image = tf.image.convert_image_dtype(image, 
                                             tf.float32)
        shape = tf.cast(tf.shape(image)[:-1], tf.float32)
        longest_dimension = max(shape)
        scale = dimension / longest_dimension
        new_shape = tf.cast(shape * scale, tf.int32)
        image = tf.image.resize(image, new_shape)
        return image[tf.newaxis, :]
    
  3. 定义一个将张量转换为图像的函数:

    def tensor_to_image(tensor):
        tensor = tensor * 255
        tensor = np.array(tensor, dtype=np.uint8)
        if np.ndim(tensor) > 3:
            tensor = tensor[0]
        return tensor
    
  4. 定义一个使用matplotlib显示图像的函数:

    def show_image(image):
        if len(image.shape) > 3:
            image = tf.squeeze(image, axis=0)
        plt.imshow(image)
        plt.show()
    
  5. 定义风格迁移实现的路径,并加载模型:

    module_url = ('https://tfhub.dev/google/magenta/'
                  'arbitrary-image-stylization-v1-256/2')
    hub_module = load(module_url)
    
  6. 加载内容图像,然后显示它:

    image = load_image('bmw.jpg')
    show_image(image)
    

    就是这个:

    图 4.12 – 一辆车的内容图像

    图 4.12 – 一辆车的内容图像

    我们将在下一步应用风格迁移到这张照片上。

  7. 加载并显示风格图像:

    style_image = load_image('art4.jpg')
    show_image(style_image)
    

    在这里,你可以看到风格图像:

    图 4.13 – 这是我们选择的风格图像

    图 4.13 – 这是我们选择的风格图像

    我们将这个和内容图像传递给我们最近创建的 TFHub 模块,并等待结果。

  8. 使用我们从 TFHub 下载的模型应用神经风格迁移,并显示结果:

    results = hub_module(tf.constant(image),
                         tf.constant(style_image))
    stylized_image = tensor_to_image(results[0])
    show_image(stylized_image)
    

    这是使用 TFHub 应用神经风格迁移的结果:

图 4.14 – 使用 TFHub 应用风格迁移的结果

图 4.14 – 使用 TFHub 应用风格迁移的结果

瞧! 结果看起来相当不错,你不觉得吗?我们将在下一节深入探讨。

它是如何工作的……

在这个食谱中,我们学到,使用 TFHub 进行图像风格化比从头实现算法要容易得多。然而,它给了我们较少的控制,因为它像一个黑盒子。

无论哪种方式,结果都相当令人满意,因为它保持了原始场景的连贯性和意义,同时将风格图像的艺术特征叠加在上面。

最重要的部分是从 TFHub 下载正确的模块,然后使用 load() 函数加载它。

为了让预打包模块正常工作,我们必须将内容和风格图像都作为 tf.constant 常量传递。

最后,由于我们接收到的是一个张量,为了正确地在屏幕上显示结果,我们使用了自定义函数 tensor_to_image(),将其转化为可以通过 matplotlib 容易绘制的 NumPy 数组。

另见

你可以在此链接阅读更多关于我们使用的 TFHub 模块:https://tfhub.dev/google/magenta/arbitrary-image-stylization-v1-256/2。

另外,为什么不尝试一下你自己的图像和其他风格呢?你可以使用这里的资源作为起点:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch4/recipe5

使用深度学习提高图像分辨率

卷积神经网络(CNN) 也可以用来提高低质量图像的分辨率。历史上,我们可以通过使用插值技术、基于示例的方法,或需要学习的低到高分辨率映射来实现这一点。

正如我们在这个步骤中将看到的,通过使用基于端到端深度学习的方法,我们可以更快地获得更好的结果。

听起来有趣吗?那我们就开始吧!

准备工作

在这个步骤中,我们需要使用Pillow,你可以通过以下命令安装:

$> pip install Pillow

在这个步骤中,我们使用的是Dog and Cat Detection数据集,该数据集托管在 Kaggle 上:www.kaggle.com/andrewmvd/dog-and-cat-detection。要下载它,你需要在网站上登录或注册。一旦登录,将其保存到你喜欢的地方,命名为dogscats.zip。然后,将其解压到一个名为dogscats的文件夹中。从现在开始,我们假设数据存储在~/.keras/datasets/dogscats目录下。

以下是数据集中两个类别的示例:

图 4.15 – 示例图像

图 4.15 – 示例图像

让我们开始吧!

如何做……

按照以下步骤实现一个全卷积网络,以执行图像超分辨率:

  1. 导入所有必要的模块:

    import pathlib
    from glob import glob
    import matplotlib.pyplot as plt
    import numpy as np
    from PIL import Image
    from tensorflow.keras import Model
    from tensorflow.keras.layers import *
    from tensorflow.keras.optimizers import Adam
    from tensorflow.keras.preprocessing.image import *
    
  2. 定义一个函数,构建网络架构。请注意,这是一个全卷积网络,这意味着它仅由卷积层(除了激活层)组成,包括输出层:

    def build_srcnn(height, width, depth):
        input = Input(shape=(height, width, depth))
        x = Conv2D(filters=64, kernel_size=(9, 9),
                   kernel_initializer='he_normal')(input)
        x = ReLU()(x)
        x = Conv2D(filters=32, kernel_size=(1, 1),
                   kernel_initializer='he_normal')(x)
        x = ReLU()(x)
        output = Conv2D(filters=depth, kernel_size=(5, 5),
                        kernel_initializer='he_normal')(x)
        return Model(input, output)
    
  3. 定义一个函数,根据缩放因子调整图像的大小。需要考虑的是,它接收的是一个表示图像的NumPy数组:

    def resize_image(image_array, factor):
        original_image = Image.fromarray(image_array)
        new_size = np.array(original_image.size) * factor
        new_size = new_size.astype(np.int32)
        new_size = tuple(new_size)
        resized = original_image.resize(new_size)
        resized = img_to_array(resized)
        resized = resized.astype(np.uint8)
        return resized
    
  4. 定义一个函数,紧密裁剪图像。我们这样做是因为当我们稍后应用滑动窗口提取补丁时,希望图像能恰当地适应。SCALE是我们希望网络学习如何放大图像的因子:

    def tight_crop_image(image):
        height, width = image.shape[:2]
        width -= int(width % SCALE)
        height -= int(height % SCALE)
        return image[:height, :width]
    
  5. 定义一个函数,故意通过缩小图像然后再放大它来降低图像分辨率:

    def downsize_upsize_image(image):
        scaled = resize_image(image, 1.0 / SCALE)
        scaled = resize_image(scaled, SCALE / 1.0)
        return scaled
    
  6. 定义一个函数,用于从输入图像中裁剪补丁。INPUT_DIM是我们输入到网络中的图像的高度和宽度:

    def crop_input(image, x, y):
        y_slice = slice(y, y + INPUT_DIM)
        x_slice = slice(x, x + INPUT_DIM)
        return image[y_slice, x_slice]
    
  7. 定义一个函数,用于裁剪输出图像的区域。LABEL_SIZE是网络输出图像的高度和宽度。另一方面,PAD是用于填充的像素数,确保我们正确裁剪感兴趣的区域:

    def crop_output(image, x, y):
        y_slice = slice(y + PAD, y + PAD + LABEL_SIZE)
        x_slice = slice(x + PAD, x + PAD + LABEL_SIZE)
        return image[y_slice, x_slice]
    
  8. 设置随机种子:

    SEED = 999
    np.random.seed(SEED)
    
  9. 加载数据集中所有图像的路径:

    file_patten = (pathlib.Path.home() / '.keras' / 
                   'datasets' /
                   'dogscats' / 'images' / '*.png')
    file_pattern = str(file_patten)
    dataset_paths = [*glob(file_pattern)]
    
  10. 因为数据集非常庞大,而我们并不需要其中所有的图像来实现我们的目标,所以让我们随机挑选其中 1,500 张:

    SUBSET_SIZE = 1500
    dataset_paths = np.random.choice(dataset_paths, 
                                     SUBSET_SIZE)
    
  11. 定义将用于创建低分辨率补丁数据集(作为输入)和高分辨率补丁(作为标签)数据集的参数。除了STRIDE参数外,所有这些参数都在前面的步骤中定义过。STRIDE是我们在水平和垂直轴上滑动提取补丁时使用的像素数:

    SCALE = 2.0
    INPUT_DIM = 33
    LABEL_SIZE = 21
    PAD = int((INPUT_DIM - LABEL_SIZE) / 2.0)
    STRIDE = 14
    
  12. 构建数据集。输入将是从图像中提取的低分辨率补丁,这些补丁是通过缩小和放大处理过的。标签将是来自未改变图像的补丁:

    data = []
    labels = []
    for image_path in dataset_paths:
        image = load_img(image_path)
        image = img_to_array(image)
        image = image.astype(np.uint8)
        image = tight_crop_image(image)
        scaled = downsize_upsize_image(image)
        height, width = image.shape[:2]
        for y in range(0, height - INPUT_DIM + 1, STRIDE):
            for x in range(0, width - INPUT_DIM + 1, STRIDE):
                crop = crop_input(scaled, x, y)
                target = crop_output(image, x, y)
                data.append(crop)
                labels.append(target)
    data = np.array(data)
    labels = np.array(labels)
    
  13. 实例化网络,我们将在 12 个周期内进行训练,并使用Adam()作为优化器,同时进行学习率衰减。损失函数是'mse'。为什么?因为我们的目标不是实现高准确率,而是学习一组过滤器,正确地将低分辨率图像块映射到高分辨率:

    EPOCHS = 12
    optimizer = Adam(lr=1e-3, decay=1e-3 / EPOCHS)
    model = build_srcnn(INPUT_DIM, INPUT_DIM, 3)
    model.compile(loss='mse', optimizer=optimizer)
    
  14. 训练网络:

    BATCH_SIZE = 64
    model.fit(data, labels, batch_size=BATCH_SIZE, 
              epochs=EPOCHS)
    
  15. 现在,为了评估我们的解决方案,我们将加载一张测试图像,将其转换为NumPy数组,并降低其分辨率:

    image = load_img('dogs.jpg')
    image = img_to_array(image)
    image = image.astype(np.uint8)
    image = tight_crop_image(image)
    scaled = downsize_upsize_image(image)
    
  16. 显示低分辨率图像:

    plt.title('Low resolution image (Downsize + Upsize)')
    plt.imshow(scaled)
    plt.show()
    

    让我们看看结果:

    图 4.16 – 低分辨率测试图像

    图 4.16 – 低分辨率测试图像

    现在,我们想要创建这张照片的更清晰版本。

  17. 创建一个与输入图像相同尺寸的画布。这是我们存储网络生成的高分辨率图像块的地方:

    output = np.zeros(scaled.shape)
    height, width = output.shape[:2]
    
  18. 提取低分辨率图像块,将它们传入网络以获得高分辨率的对应图像块,并将它们放置在输出画布的正确位置:

    for y in range(0, height - INPUT_DIM + 1, LABEL_SIZE):
        for x in range(0, width - INPUT_DIM + 1, LABEL_SIZE):
            crop = crop_input(scaled, x, y)
            image_batch = np.expand_dims(crop, axis=0)
            prediction = model.predict(image_batch)
            new_shape = (LABEL_SIZE, LABEL_SIZE, 3)
            prediction = prediction.reshape(new_shape)
            output_y_slice = slice(y + PAD, y + PAD + 
                                   LABEL_SIZE)
            output_x_slice = slice(x + PAD, x + PAD + 
                                  LABEL_SIZE)
            output[output_y_slice, output_x_slice] = 
                                     prediction
    
  19. 最后,显示高分辨率结果:

    plt.title('Super resolution result (SRCNN output)')
    plt.imshow(output / 255)
    plt.show()
    

    这是超分辨率输出:

图 4.17 – 高分辨率测试图像

图 4.17 – 高分辨率测试图像

与低分辨率图像相比,这张照片更好地展示了狗和整个场景的细节,你不觉得吗?

提示

我建议你在 PDF 或照片查看器中同时打开低分辨率和高分辨率的图像。这将帮助你仔细检查它们之间的差异,并让你确信网络完成了它的工作。在本书的打印版本中,可能很难判断这种区别。

它是如何工作的……

在这个教程中,我们创建了一个能够提高模糊或低分辨率图像分辨率的模型。这个实现的最大收获是它由完全卷积神经网络驱动,意味着它只包含卷积层及其激活。

这是一个回归问题,输出中的每个像素都是我们想要学习的特征。

然而,我们的目标不是优化准确性,而是训练模型,使特征图能够编码必要的信息,从低分辨率图像生成高分辨率图像块。

现在,我们必须问自己:为什么是图像块?我们不想学习图像中的内容。相反,我们希望网络弄清楚如何从低分辨率到高分辨率。图像块足够适合这个目的,因为它们包含了局部的模式,更容易理解。

你可能已经注意到我们并没有训练很多周期(只有 12 个)。这是经过设计的,因为研究表明,训练过长实际上会损害网络的性能。

最后需要注意的是,由于该网络是在狗和猫的图像上进行训练的,因此它的专长在于放大这些动物的照片。尽管如此,通过更换数据集,我们可以轻松地创建一个专门处理其他类型数据的超分辨率网络。

另请参见

我们的实现基于董等人的重要工作,有关该主题的论文可以在这里阅读:arxiv.org/abs/1501.00092

第五章:第五章:使用自编码器减少噪声

在深度神经网络家族中,最有趣的家族之一就是自编码器家族。正如其名称所示,它们的唯一目的就是处理输入数据,然后将其重建为原始形状。换句话说,自编码器学习将输入复制到输出。为什么?因为这一过程的副作用就是我们所追求的目标:不是生成标签或分类,而是学习输入到自编码器的图像的高效、高质量表示。这种表示的名称是编码

它们是如何实现这一点的呢?通过同时训练两个网络:一个编码器,它接受图像并生成编码,另一个是解码器,它接受编码并尝试从中重建输入数据。

在本章中,我们将从基础开始,首先实现一个简单的全连接自编码器。之后,我们将创建一个更常见且多功能的卷积自编码器。我们还将学习如何在更实际的应用场景中使用自编码器,比如去噪图像、检测数据集中的异常值和创建逆向图像搜索索引。听起来有趣吗?

在本章中,我们将涵盖以下食谱:

  • 创建一个简单的全连接自编码器

  • 创建一个卷积自编码器

  • 使用自编码器去噪图像

  • 使用自编码器检测异常值

  • 使用深度学习创建逆向图像搜索索引

  • 实现一个变分自编码器

让我们开始吧!

技术要求

尽管使用 GPU 始终是一个好主意,但其中一些示例(特别是创建一个简单的全连接自编码器)在中端 CPU(如 Intel i5 或 i7)上运行良好。如果某个特定的示例依赖外部资源或需要预备步骤,您将在准备工作部分找到详细的准备说明。您可以随时访问本章的所有代码:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch5

请查看以下链接,观看《代码实践》视频:

bit.ly/3qrHYaF

创建一个简单的全连接自编码器

自编码器在设计和功能上都很独特。这也是为什么掌握自编码器基本原理,尤其是实现可能是最简单版本的自编码器——全连接自编码器,是一个好主意。

在本示例中,我们将实现一个全连接自编码器,来重建Fashion-MNIST中的图像,这是一个标准数据集,几乎不需要预处理,允许我们专注于自编码器本身。

你准备好了吗?让我们开始吧!

准备工作

幸运的是,Fashion-MNIST已经与 TensorFlow 一起打包,因此我们无需自己下载。

我们将使用OpenCV,一个著名的计算机视觉库,来创建一个马赛克,以便我们能够将原始图像与自编码器重建的图像进行对比。你可以通过pip轻松安装OpenCV

$> pip install opencv-contrib-python

现在,所有准备工作都已完成,来看看具体步骤吧!

如何操作…

按照以下步骤,来实现一个简单而有效的自编码器:

  1. 导入必要的包来实现全连接自编码器:

    import cv2
    import numpy as np
    from tensorflow.keras import Model
    from tensorflow.keras.datasets import fashion_mnist
    from tensorflow.keras.layers import *
    
  2. 定义一个函数来构建自编码器的架构。默认情况下,编码或潜在向量的维度是128,但163264也是不错的选择:

    def build_autoencoder(input_shape=784, encoding_dim=128):
        input_layer = Input(shape=(input_shape,))
        encoded = Dense(units=512)(input_layer)
        encoded = ReLU()(encoded)
        encoded = Dense(units=256)(encoded)
        encoded = ReLU()(encoded)
        encoded = Dense(encoding_dim)(encoded)
        encoding = ReLU()(encoded)
        decoded = Dense(units=256)(encoding)
        decoded = ReLU()(decoded)
        decoded = Dense(units=512)(decoded)
        decoded = ReLU()(decoded)
        decoded = Dense(units=input_shape)(decoded)
        decoded = Activation('sigmoid')(decoded)
        return Model(input_layer, decoded)
    
  3. 定义一个函数,用于将一组常规图像与其原始对应图像进行对比绘制,以便直观评估自编码器的性能:

    def plot_original_vs_generated(original, generated):
        num_images = 15
        sample = np.random.randint(0, len(original), 
                                   num_images)
    
  4. 前一个代码块选择了 15 个随机索引,我们将用它们从originalgenerated批次中挑选相同的样本图像。接下来,定义一个内部函数,这样我们就可以将 15 张图像按 3x5 网格排列:

        def stack(data):
            images = data[sample]
            return np.vstack([np.hstack(images[:5]),
                              np.hstack(images[5:10]),
                              np.hstack(images[10:15])])
    
  5. 现在,定义另一个内部函数,以便我们可以在图像上添加文字。这将有助于区分生成的图像和原始图像,稍后我们将看到如何操作:

        def add_text(image, text, position):
            pt1 = position
            pt2 = (pt1[0] + 10 + (len(text) * 22),
                   pt1[1] - 45)
            cv2.rectangle(image,
                          pt1,
                          pt2,
                          (255, 255, 255),
                          -1)
            cv2.putText(image, text,
                        position,
                        fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                        fontScale=1.3,
                        color=(0, 0, 0),
                        thickness=4)
    
  6. 完成该函数,通过从原始和生成的图像组中选择相同的图像。然后,将这两组图像堆叠在一起形成马赛克,调整其大小为 860x860,使用add_text()在马赛克中标注原始图像和生成图像,并显示结果:

        original = stack(original)
        generated = stack(generated)
        mosaic = np.vstack([original,
                            generated])
        mosaic = cv2.resize(mosaic, (860, 860), 
                            interpolation=cv2.INTER_AREA)
        mosaic = cv2.cvtColor(mosaic, cv2.COLOR_GRAY2BGR)
        add_text(mosaic, 'Original', (50, 100))
        add_text(mosaic, 'Generated', (50, 520))
        cv2.imshow('Mosaic', mosaic)
        cv2.waitKey(0)
    
  7. 下载(或加载缓存的)Fashion-MNIST。由于这不是一个分类问题,我们只保留图像,而不保留标签:

    (X_train, _), (X_test, _) = fashion_mnist.load_data()
    
  8. 对图像进行归一化:

    X_train = X_train.astype('float32') / 255.0
    X_test = X_test.astype('float32') / 255.0
    
  9. 将图像重塑为向量:

    X_train = X_train.reshape((X_train.shape[0], -1))
    X_test = X_test.reshape((X_test.shape[0], -1))
    
  10. 构建自编码器并进行编译。我们将使用'adam'作为优化器,均方误差('mse')作为损失函数。为什么?我们不关心分类是否正确,而是尽可能准确地重建输入,这意味着要最小化总体误差:

    autoencoder = build_autoencoder()
    autoencoder.compile(optimizer='adam', loss='mse')
    
  11. 在 300 个 epoch 上拟合自编码器,这是一个足够大的数字,能够让网络学习到输入的良好表示。为了加快训练过程,我们每次传入1024个向量的批次(可以根据硬件能力自由调整批次大小)。请注意,输入特征也是标签或目标:

    EPOCHS = 300
    BATCH_SIZE = 1024
    autoencoder.fit(X_train, X_train,
                    epochs=EPOCHS,
                    batch_size=BATCH_SIZE,
                    shuffle=True,
                    validation_data=(X_test, X_test))
    
  12. 对测试集进行预测(基本上就是生成测试向量的副本):

    predictions = autoencoder.predict(X_test)
    
  13. 将预测值和测试向量重新调整为 28x28x1 尺寸的灰度图像:

    original_shape = (X_test.shape[0], 28, 28)
    predictions = predictions.reshape(original_shape)
    X_test = X_test.reshape(original_shape)
    
  14. 生成原始图像与自编码器生成图像的对比图:

    plot_original_vs_generated(X_test, predictions)
    

    这是结果:

图 5.1 – 原始图像(前三行)与生成的图像(底部三行)

图 5.1 – 原始图像(前三行)与生成的图像(底部三行)

根据结果来看,我们的自编码器做得相当不错。在所有情况下,服装的形状都得到了很好的保留。然而,它在重建内部细节时并不如预期准确,如第六行第四列中的 T 恤所示,原图中的横条纹在生成的副本中缺失了。

它是如何工作的……

在这个食谱中,我们了解到自编码器是通过将两个网络结合成一个来工作的:编码器和解码器。在 build_autoencoder() 函数中,我们实现了一个完全连接的自编码架构,其中编码器部分接受一个 784 元素的向量并输出一个包含 128 个数字的编码。然后,解码器接收这个编码并通过几个堆叠的全连接层进行扩展,最后一个层生成一个 784 元素的向量(与输入的维度相同)。

训练过程因此包括最小化编码器接收的输入与解码器产生的输出之间的距离或误差。实现这一目标的唯一方法是学习能在压缩输入时最小化信息损失的编码。

尽管损失函数(在此情况下为 MSE)是衡量自编码器学习进展的好方法,但对于这些特定的网络,视觉验证同样重要,甚至可能更为关键。这就是我们实现 plot_original_vs_generated() 函数的原因:检查副本是否看起来像它们的原始对应物。

你为什么不试试改变编码大小呢?它是如何影响副本质量的?

另见

如果你想知道为什么 Fashion-MNIST 会存在,可以查看这里的官方仓库:github.com/zalandoresearch/fashion-mnist

创建卷积自编码器

与常规神经网络一样,处理图像时,使用卷积通常是最好的选择。在自编码器的情况下,这也是一样的。在这个食谱中,我们将实现一个卷积自编码器,用于重建 Fashion-MNIST 中的图像。

区别在于,在解码器中,我们将使用反向或转置卷积,它可以放大体积,而不是缩小它们。这是传统卷积层中发生的情况。

这是一个有趣的食谱。你准备好开始了吗?

准备工作

因为 TensorFlow 提供了方便的函数来下载 Fashion-MNIST,所以我们不需要在数据端做任何手动准备。然而,我们必须安装 OpenCV,以便我们可以可视化自编码器的输出。可以使用以下命令来完成:

$> pip install opencv-contrib-python

事不宜迟,让我们开始吧。

如何做……

按照以下步骤实现一个完全功能的卷积自编码器:

  1. 让我们导入必要的依赖:

    import cv2
    import numpy as np
    from tensorflow.keras import Model
    from tensorflow.keras.datasets import fashion_mnist
    from tensorflow.keras.layers import * 
    
  2. 定义build_autoencoder()函数,该函数内部构建自编码器架构,并返回编码器、解码器以及自编码器本身。首先定义输入层和第一组 32 个卷积过滤器:

    def build_autoencoder(input_shape=(28, 28, 1),
                          encoding_size=32,
                          alpha=0.2):
        inputs = Input(shape=input_shape)
        encoder = Conv2D(filters=32,
                         kernel_size=(3, 3),
                         strides=2,
                         padding='same')(inputs)
        encoder = LeakyReLU(alpha=alpha)(encoder)
        encoder = BatchNormalization()(encoder)
    

    定义第二组卷积层(这次是 64 个卷积核):

        encoder = Conv2D(filters=64,
                         kernel_size=(3, 3),
                         strides=2,
                         padding='same')(encoder)
        encoder = LeakyReLU(alpha=alpha)(encoder)
        encoder = BatchNormalization()(encoder)
    

    定义编码器的输出层:

        encoder_output_shape = encoder.shape
        encoder = Flatten()(encoder)
        encoder_output = Dense(units=encoding_size)(encoder)
        encoder_model = Model(inputs, encoder_output)
    
  3. 步骤 2中,我们定义了编码器模型,这是一个常规的卷积神经网络。下一块代码定义了解码器模型,从输入和 64 个反卷积过滤器开始:

        decoder_input = Input(shape=(encoding_size,))
        target_shape = tuple(encoder_output_shape[1:])
        decoder = Dense(np.prod(target_shape))(decoder_input)
        decoder = Reshape(target_shape)(decoder)
        decoder = Conv2DTranspose(filters=64,
                                  kernel_size=(3, 3),
                                  strides=2,
                                  padding='same')(decoder)
        decoder = LeakyReLU(alpha=alpha)(decoder)
        decoder = BatchNormalization()(decoder)
    

    定义第二组反卷积层(这次是 32 个卷积核):

        decoder = Conv2DTranspose(filters=32,
                                  kernel_size=(3, 3),
                                  strides=2,
                                  padding='same')(decoder)
        decoder = LeakyReLU(alpha=alpha)(decoder)
        decoder = BatchNormalization()(decoder)
    

    定义解码器的输出层:

        decoder = Conv2DTranspose(filters=1,
                                  kernel_size=(3, 3),
                                  padding='same')(decoder)
        outputs = Activation('sigmoid')(decoder)
        decoder_model = Model(decoder_input, outputs)
    
  4. 解码器使用Conv2DTranspose层,该层将输入扩展以生成更大的输出体积。注意,我们进入解码器的层数越多,Conv2DTranspose层使用的过滤器就越少。最后,定义自编码器:

        encoder_model_output = encoder_model(inputs)
        decoder_model_output = 
           decoder_model(encoder_model_output)
        autoencoder_model = Model(inputs, 
           decoder_model_output)
      return encoder_model, decoder_model, autoencoder_model
    

    自编码器是端到端的架构。它从输入层开始,进入编码器,最后通过解码器输出层,得出结果。

  5. 定义一个函数,将一般图像样本与其原始图像进行对比绘制。这将帮助我们直观评估自编码器的性能。(这是我们在前一个示例中定义的相同函数。有关更完整的解释,请参考本章的创建简单的全连接自编码器一节。)请看以下代码:

    def plot_original_vs_generated(original, generated):
        num_images = 15
        sample = np.random.randint(0, len(original), 
                                   num_images)
    
  6. 定义一个内部辅助函数,用于将图像样本堆叠成一个 3x5 的网格:

        def stack(data):
            images = data[sample]
            return np.vstack([np.hstack(images[:5]),
                              np.hstack(images[5:10]),
                              np.hstack(images[10:15])])
    
  7. 接下来,定义一个函数,将文本放置到图像的指定位置:

    def add_text(image, text, position):
            pt1 = position
            pt2 = (pt1[0] + 10 + (len(text) * 22),
                   pt1[1] - 45)
            cv2.rectangle(image,
                          pt1,
                          pt2,
                          (255, 255, 255),
                          -1)
            cv2.putText(image, text,
                        position,
                        fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                        fontScale=1.3,
                        color=(0, 0, 0),
                        thickness=4)
    
  8. 最后,创建一个包含原始图像和生成图像的马赛克:

        original = stack(original)
        generated = stack(generated)
        mosaic = np.vstack([original,
                            generated])
        mosaic = cv2.resize(mosaic, (860, 860),
                            interpolation=cv2.INTER_AREA)
        mosaic = cv2.cvtColor(mosaic, cv2.COLOR_GRAY2BGR)
        add_text(mosaic, 'Original', (50, 100))
        add_text(mosaic, 'Generated', (50, 520))
        cv2.imshow('Mosaic', mosaic)
        cv2.waitKey(0)
    
  9. 下载(或加载,如果已缓存)Fashion-MNIST。我们只关心图像,因此可以丢弃标签:

    (X_train, _), (X_test, _) = fashion_mnist.load_data()
    
  10. 对图像进行归一化并添加通道维度:

    X_train = X_train.astype('float32') / 255.0
    X_test = X_test.astype('float32') / 255.0
    X_train = np.expand_dims(X_train, axis=-1)
    X_test = np.expand_dims(X_test, axis=-1)
    
  11. 这里,我们只关心自编码器,因此会忽略build_autoencoder()函数的其他两个返回值。然而,在不同的情况下,我们可能需要保留它们。我们将使用'adam'优化器训练模型,并使用'mse'作为损失函数,因为我们希望减少误差,而不是优化分类准确性:

    _, _, autoencoder = build_autoencoder(encoding_size=256)
    autoencoder.compile(optimizer='adam', loss='mse')
    
  12. 在 300 个训练周期中训练自编码器,每次批处理 512 张图像。注意,输入图像也是标签:

    EPOCHS = 300
    BATCH_SIZE = 512
    autoencoder.fit(X_train, X_train,
                    epochs=EPOCHS,
                    batch_size=BATCH_SIZE,
                    shuffle=True,
                    validation_data=(X_test, X_test),
                    verbose=1)
    
  13. 复制测试集:

    predictions = autoencoder.predict(X_test)
    
  14. 将预测结果和测试图像的形状调整回 28x28(无通道维度):

    original_shape = (X_test.shape[0], 28, 28)
    predictions = predictions.reshape(original_shape)
    X_test = X_test.reshape(original_shape)
    predictions = (predictions * 255.0).astype('uint8')
    X_test = (X_test * 255.0).astype('uint8')
    
  15. 生成原始图像与自编码器输出的复制图像的对比马赛克:

    plot_original_vs_generated(X_test, predictions)
    

    让我们看看结果:

图 5.2 – 原始图像的马赛克(前三行),与卷积自编码器生成的图像(后三行)

图 5.2 – 原始图像的马赛克(前三行),与卷积自编码器生成的图像(后三行)进行对比

正如我们所见,自编码器已经学到了一个很好的编码,它使得它能够以最小的细节损失重建输入图像。让我们进入下一个部分,了解它是如何工作的!

它是如何工作的…

在这个教程中,我们了解到卷积自编码器是这一系列神经网络中最常见且最强大的成员之一。该架构的编码器部分是一个常规的卷积神经网络,依赖卷积和密集层来缩小输出并生成向量表示。解码器则是最有趣的部分,因为它必须处理相反的问题:根据合成的特征向量(即编码)重建输入。

它是如何做到的呢?通过使用转置卷积(Conv2DTranspose)。与传统的Conv2D层不同,转置卷积产生的是较浅的体积(较少的过滤器),但是它们更宽更高。结果是输出层只有一个过滤器,并且是 28x28 的维度,这与输入的形状相同。很有趣,不是吗?

训练过程包括最小化输出(生成的副本)和输入(原始图像)之间的误差。因此,均方误差(MSE)是一个合适的损失函数,因为它为我们提供了这个信息。

最后,我们通过目视检查一组测试图像及其合成的对照图像来评估自编码器的性能。

提示

在自编码器中,编码的大小至关重要,以确保解码器有足够的信息来重建输入。

另见

这里有一个关于转置卷积的很好的解释:towardsdatascience.com/transposed-convolution-demystified-84ca81b4baba

使用自编码器去噪图像

使用图像重建输入是很棒的,但有没有更有用的方式来应用自编码器呢?当然有!其中之一就是图像去噪。如其名所示,这就是通过用合理的值替换损坏的像素和区域来恢复损坏的图像。

在这个教程中,我们将故意损坏Fashion-MNIST中的图像,然后训练一个自编码器去噪它们。

准备就绪

Fashion-MNIST可以通过 TensorFlow 提供的便利函数轻松访问,因此我们不需要手动下载数据集。另一方面,因为我们将使用OpenCV来创建一些可视化效果,所以我们必须安装它,方法如下:

$> pip install opencv-contrib-python

让我们开始吧!

如何做…

按照以下步骤实现一个能够恢复损坏图像的卷积自编码器:

  1. 导入所需的包:

    import cv2
    import numpy as np
    from tensorflow.keras import Model
    from tensorflow.keras.datasets import fashion_mnist
    from tensorflow.keras.layers import *
    
  2. 定义build_autoencoder()函数,它创建相应的神经网络架构。请注意,这是我们在前一个教程中实现的相同架构;因此,我们在这里不再详细讲解。有关详细解释,请参见创建卷积自编码器教程:

    def build_autoencoder(input_shape=(28, 28, 1),
                          encoding_size=128,
                          alpha=0.2):
        inputs = Input(shape=input_shape)
        encoder = Conv2D(filters=32,
                         kernel_size=(3, 3),
                         strides=2,
                         padding='same')(inputs)
        encoder = LeakyReLU(alpha=alpha)(encoder)
        encoder = BatchNormalization()(encoder)
        encoder = Conv2D(filters=64,
                         kernel_size=(3, 3),
                         strides=2,
                         padding='same')(encoder)
        encoder = LeakyReLU(alpha=alpha)(encoder)
        encoder = BatchNormalization()(encoder)
        encoder_output_shape = encoder.shape
        encoder = Flatten()(encoder)
        encoder_output = 
          Dense(units=encoding_size)(encoder)
        encoder_model = Model(inputs, encoder_output)
    
  3. 现在我们已经创建了编码器模型,接下来创建解码器:

        decoder_input = Input(shape=(encoding_size,))
        target_shape = tuple(encoder_output_shape[1:])
        decoder = 
        Dense(np.prod(target_shape))(decoder_input)
        decoder = Reshape(target_shape)(decoder)
        decoder = Conv2DTranspose(filters=64,
                                  kernel_size=(3, 3),
                                  strides=2,
                                  padding='same')(decoder)
        decoder = LeakyReLU(alpha=alpha)(decoder)
        decoder = BatchNormalization()(decoder)
        decoder = Conv2DTranspose(filters=32,
                                  kernel_size=(3, 3),
                                  strides=2,
                                  padding='same')(decoder)
        decoder = LeakyReLU(alpha=alpha)(decoder)
        decoder = BatchNormalization()(decoder)
        decoder = Conv2DTranspose(filters=1,
                                  kernel_size=(3, 3),
                                  padding='same')(decoder)
        outputs = Activation('sigmoid')(decoder)
        decoder_model = Model(decoder_input, outputs)
    
  4. 最后,定义自编码器本身并返回三个模型:

        encoder_model_output = encoder_model(inputs)
        decoder_model_output = 
        decoder_model(encoder_model_output)
        autoencoder_model = Model(inputs, 
                                  decoder_model_output)
        return encoder_model, decoder_model, autoencoder_model
    
  5. 定义 plot_original_vs_generated() 函数,该函数创建原始图像与生成图像的比较拼图。我们稍后将使用此函数来显示噪声图像及其恢复后的图像。与 build_autoencoder() 类似,该函数的工作方式与我们在创建一个简单的全连接自编码器食谱中定义的相同,因此如果您需要详细解释,请查阅该食谱:

    def plot_original_vs_generated(original, generated):
        num_images = 15
        sample = np.random.randint(0, len(original), 
                                  num_images)
    
  6. 定义一个内部辅助函数,将一组图像按 3x5 网格堆叠:

        def stack(data):
            images = data[sample]
            return np.vstack([np.hstack(images[:5]),
                              np.hstack(images[5:10]),
                              np.hstack(images[10:15])])
    
  7. 定义一个函数,将自定义文本放置在图像上的特定位置:

    def add_text(image, text, position):
            pt1 = position
            pt2 = (pt1[0] + 10 + (len(text) * 22),
                   pt1[1] - 45)
            cv2.rectangle(image,
                          pt1,
                          pt2,
                          (255, 255, 255),
                          -1)
            cv2.putText(image, text,
                        position,
                        fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                        fontScale=1.3,
                        color=(0, 0, 0),
                        thickness=4)
    
  8. 创建包含原始图像和生成图像的拼图,标记每个子网格并显示结果:

        original = stack(original)
        generated = stack(generated)
        mosaic = np.vstack([original,
                            generated])
        mosaic = cv2.resize(mosaic, (860, 860),
                            interpolation=cv2.INTER_AREA)
        mosaic = cv2.cvtColor(mosaic, cv2.COLOR_GRAY2BGR)
        add_text(mosaic, 'Original', (50, 100))
        add_text(mosaic, 'Generated', (50, 520))
        cv2.imshow('Mosaic', mosaic)
        cv2.waitKey(0)
    
  9. 使用 TensorFlow 的便捷函数加载 Fashion-MNIST。我们将只保留图像,因为标签不需要:

    (X_train, _), (X_test, _) = fashion_mnist.load_data()
    
  10. 对图像进行归一化,并使用 np.expand_dims() 为其添加单一颜色通道:

    X_train = X_train.astype('float32') / 255.0
    X_test = X_test.astype('float32') / 255.0
    X_train = np.expand_dims(X_train, axis=-1)
    X_test = np.expand_dims(X_test, axis=-1)
    
  11. 生成两个与 X_trainX_test 相同维度的张量。它们将对应于随机的 0.5

    train_noise = np.random.normal(loc=0.5, scale=0.5,
                                   size=X_train.shape)
    test_noise = np.random.normal(loc=0.5, scale=0.5,
                                  size=X_test.shape)
    
  12. 通过分别向 X_trainX_test 添加 train_noisetest_noise 来故意损坏这两个数据集。确保使用 np.clip() 将值保持在 01 之间:

    X_train_noisy = np.clip(X_train + train_noise, 0, 1)
    X_test_noisy = np.clip(X_test + test_noise, 0, 1)
    
  13. 创建自编码器并编译它。我们将使用'adam'作为优化器,'mse'作为损失函数,因为我们更关心减少误差,而不是提高准确率:

    _, _, autoencoder = build_autoencoder(encoding_size=128)
    autoencoder.compile(optimizer='adam', loss='mse')
    
  14. 将模型训练 300 个周期,每次批量处理 1024 张噪声图像。注意,特征是噪声图像,而标签或目标是原始图像,即未经损坏的图像:

    EPOCHS = 300
    BATCH_SIZE = 1024
    autoencoder.fit(X_train_noisy, X_train,
                    epochs=EPOCHS,
                    batch_size=BATCH_SIZE,
                    shuffle=True,
                    validation_data=(X_test_noisy,X_test))
    
  15. 使用训练好的模型进行预测。将噪声图像和生成图像都重新调整为 28x28,并将它们缩放到[0, 255]范围内:

    predictions = autoencoder.predict(X_test)
    original_shape = (X_test_noisy.shape[0], 28, 28)
    predictions = predictions.reshape(original_shape)
    X_test_noisy = X_test_noisy.reshape(original_shape)
    predictions = (predictions * 255.0).astype('uint8')
    X_test_noisy = (X_test_noisy * 255.0).astype('uint8')
    
  16. 最后,显示噪声图像与恢复图像的拼图:

    plot_original_vs_generated(X_test_noisy, predictions)
    

    这是结果:

图 5.3 – 噪声图像(顶部)与网络恢复的图像(底部)拼图

图 5.3 – 噪声图像(顶部)与网络恢复的图像(底部)拼图

看看顶部的图像有多么受损!好消息是,在大多数情况下,自编码器成功地恢复了它们。然而,它无法正确去除拼图边缘部分的噪声,这表明可以进行更多实验以提高性能(公平地说,这些坏例子即使对人类来说也很难辨别)。

它是如何工作的…

本食谱的新颖之处在于实际应用卷积自编码器。网络和其他构建模块在前两个食谱中已被详细讨论,因此我们将重点关注去噪问题本身。

为了重现实际中损坏图像的场景,我们在Fashion-MNIST数据集的训练集和测试集中加入了大量的高斯噪声。这种噪声被称为“盐和胡椒”,因为损坏后的图像看起来像是洒满了这些调料。

为了教会我们的自编码器图像原本的样子,我们将带噪声的图像作为特征,将原始图像作为目标或标签。这样,经过 300 个 epoch 后,网络学会了一个编码,可以在许多情况下将带盐和胡椒噪声的实例映射到令人满意的恢复版本。

然而,模型并不完美,正如我们在拼图中看到的那样,网络无法恢复网格边缘的图像。这证明了修复损坏图像的困难性。

使用自编码器检测异常值

自编码器的另一个重要应用是异常检测。这个应用场景的理念是,自编码器会对数据集中最常见的类别学习出一个误差非常小的编码,而对于稀有类别(异常值)的重建能力则会误差较大。

基于这个前提,在本教程中,我们将依赖卷积自编码器来检测Fashion-MNIST子集中的异常值。

让我们开始吧!

准备中

要安装OpenCV,请使用以下pip命令:

$> pip install opencv-contrib-python

我们将依赖 TensorFlow 内建的便捷函数来加载Fashion-MNIST数据集。

如何实现…

按照以下步骤完成这个教程:

  1. 导入所需的包:

    import cv2
    import numpy as np
    from sklearn.model_selection import train_test_split
    from tensorflow.keras import Model
    from tensorflow.keras.datasets import fashion_mnist as fmnist
    from tensorflow.keras.layers import *
    
  2. 设置随机种子以保证可重复性:

    SEED = 84
    np.random.seed(SEED)
    
  3. 定义一个函数来构建自编码器架构。这个函数遵循我们在创建卷积自编码器教程中学习的结构,如果你想了解更深入的解释,请回到那个教程。让我们从创建编码器模型开始:

    def build_autoencoder(input_shape=(28, 28, 1),
                          encoding_size=96,
                          alpha=0.2):
        inputs = Input(shape=input_shape)
        encoder = Conv2D(filters=32,
                         kernel_size=(3, 3),
                         strides=2,
                         padding='same')(inputs)
        encoder = LeakyReLU(alpha=alpha)(encoder)
        encoder = BatchNormalization()(encoder)
        encoder = Conv2D(filters=64,
                         kernel_size=(3, 3),
                         strides=2,
                         padding='same')(encoder)
        encoder = LeakyReLU(alpha=alpha)(encoder)
        encoder = BatchNormalization()(encoder)
        encoder_output_shape = encoder.shape
        encoder = Flatten()(encoder)
        encoder_output = Dense(encoding_size)(encoder)
        encoder_model = Model(inputs, encoder_output)
    
  4. 接下来,构建解码器:

        decoder_input = Input(shape=(encoding_size,))
        target_shape = tuple(encoder_output_shape[1:])
        decoder = Dense(np.prod(target_shape))(decoder_input)
        decoder = Reshape(target_shape)(decoder)
        decoder = Conv2DTranspose(filters=64,
                                  kernel_size=(3, 3),
                                  strides=2,
                                  padding='same')(decoder)
        decoder = LeakyReLU(alpha=alpha)(decoder)
        decoder = BatchNormalization()(decoder)
        decoder = Conv2DTranspose(filters=32,
                                  kernel_size=(3, 3),
                                  strides=2,
                                  padding='same')(decoder)
        decoder = LeakyReLU(alpha=alpha)(decoder)
        decoder = BatchNormalization()(decoder)
        decoder = Conv2DTranspose(filters=1,
                                  kernel_size=(3, 3),
                                  padding='same')(decoder)
        outputs = Activation('sigmoid')(decoder)
        decoder_model = Model(decoder_input, outputs)
    
  5. 最后,构建自编码器并返回三个模型:

        encoder_model_output = encoder_model(inputs)
        decoder_model_output = 
        decoder_model(encoder_model_output)
        autoencoder_model = Model(inputs, 
                                  decoder_model_output)
        return encoder_model, decoder_model, autoencoder_model
    
  6. 然后,定义一个函数来构建一个包含两个类别的数据集,其中一个类别表示异常或离群点。首先选择与这两个类别相关的实例,然后将它们打乱,以打破可能存在的顺序偏差:

    def create_anomalous_dataset(features,
                                 labels,
                                 regular_label,
                                 anomaly_label,
                                 corruption_proportion=0.01):
        regular_data_idx = np.where(labels == 
                                    regular_label)[0]
        anomalous_data_idx = np.where(labels == 
                                      anomaly_label)[0]
        np.random.shuffle(regular_data_idx)
        np.random.shuffle(anomalous_data_idx)
    
  7. 接下来,从异常类别中选择与corruption_proportion成比例的实例。最后,通过将常规实例与离群点合并来创建最终的数据集:

        num_anomalies = int(len(regular_data_idx) *
                            corruption_proportion)
        anomalous_data_idx = 
                anomalous_data_idx[:num_anomalies]
        data = np.vstack([features[regular_data_idx],
                          features[anomalous_data_idx]])
        np.random.shuffle(data)
        return data
    
  8. 加载Fashion-MNIST。将训练集和测试集合并为一个数据集:

    (X_train, y_train), (X_test, y_test) = fmnist.load_data()
    X = np.vstack([X_train, X_test])
    y = np.hstack([y_train, y_test])
    
  9. 定义常规标签和异常标签,然后创建异常数据集:

    REGULAR_LABEL = 5  # Sandal
    ANOMALY_LABEL = 0  # T-shirt/top
    data = create_anomalous_dataset(X, y,
                                    REGULAR_LABEL,
                                    ANOMALY_LABEL)
    
  10. 向数据集中添加一个通道维度,进行归一化,并将数据集分为 80%作为训练集,20%作为测试集:

    data = np.expand_dims(data, axis=-1)
    data = data.astype('float32') / 255.0
    X_train, X_test = train_test_split(data,
                                       train_size=0.8,
                                       random_state=SEED)
    
  11. 构建自编码器并编译它。我们将使用'adam'作为优化器,'mse'作为损失函数,因为这可以很好地衡量模型的误差:

    _, _, autoencoder = build_autoencoder(encoding_size=256)
    autoencoder.compile(optimizer='adam', loss='mse')
    
  12. 将自编码器训练 300 个 epoch,每次处理1024张图像:

    EPOCHS = 300
    BATCH_SIZE = 1024
    autoencoder.fit(X_train, X_train,
                    epochs=EPOCHS,
                    batch_size=BATCH_SIZE,
                    validation_data=(X_test, X_test))
    
  13. 对数据进行预测以找出异常值。我们将计算原始图像与自动编码器生成图像之间的均方误差:

    decoded = autoencoder.predict(data)
    mses = []
    for original, generated in zip(data, decoded):
        mse = np.mean((original - generated) ** 2)
        mses.append(mse)
    
  14. 选择误差大于 99.9%分位数的图像索引。这些将是我们的异常值:

    threshold = np.quantile(mses, 0.999)
    outlier_idx = np.where(np.array(mses) >= threshold)[0]
    print(f'Number of outliers: {len(outlier_idx)}')
    
  15. 为每个异常值保存原始图像与生成图像的比较图像:

    decoded = (decoded * 255.0).astype('uint8')
    data = (data * 255.0).astype('uint8')
    for i in outlier_idx:
        image = np.hstack([data[i].reshape(28, 28),
                           decoded[i].reshape(28, 28)])
        cv2.imwrite(f'{i}.jpg', image)
    

    这是一个异常值的示例:

图 5.4 – 左:原始异常值。右:重建图像。

图 5.4 – 左:原始异常值。右:重建图像。

正如我们所看到的,我们可以利用自动编码器学习的编码知识轻松检测数据集中的异常或不常见图像。我们将在下一节中更详细地讨论这一点。

它是如何工作的…

本配方背后的思想非常简单:根据定义,异常值是数据集中事件或类别的稀有发生。因此,当我们在包含异常值的数据集上训练自动编码器时,它将没有足够的时间或示例来学习它们的适当表示。

通过利用网络在重建异常图像(在此示例中为 T 恤)时表现出的低置信度(换句话说,高误差),我们可以选择最差的副本来发现异常值。

然而,为了使此技术有效,自动编码器必须擅长重建常规类别(例如,凉鞋);否则,误报率将太高。

使用深度学习创建逆图像搜索索引

因为自动编码器的核心目的是学习图像集合的编码或低维表示,它们是非常优秀的特征提取器。此外,正如我们将在本配方中发现的那样,我们可以将它们作为图像搜索索引的完美构建模块。

准备就绪

让我们使用pip安装OpenCV。我们将用它来可视化自动编码器的输出,从而直观地评估图像搜索索引的有效性:

$> pip install opencv-python

我们将在下一节开始实现这个配方。

如何实现…

按照以下步骤创建您自己的图像搜索索引:

  1. 导入必要的库:

    import cv2
    import numpy as np
    from tensorflow.keras import Model
    from tensorflow.keras.datasets import fashion_mnist
    from tensorflow.keras.layers import *
    
  2. 定义build_autoencoder(),该函数实例化自动编码器。首先,让我们组装编码器部分:

    def build_autoencoder(input_shape=(28, 28, 1),
                          encoding_size=32,
                          alpha=0.2):
        inputs = Input(shape=input_shape)
        encoder = Conv2D(filters=32,
                         kernel_size=(3, 3),
                         strides=2,
                         padding='same')(inputs)
        encoder = LeakyReLU(alpha=alpha)(encoder)
        encoder = BatchNormalization()(encoder)
        encoder = Conv2D(filters=64,
                         kernel_size=(3, 3),
                         strides=2,
                         padding='same')(encoder)
        encoder = LeakyReLU(alpha=alpha)(encoder)
        encoder = BatchNormalization()(encoder)
        encoder_output_shape = encoder.shape
        encoder = Flatten()(encoder)
        encoder_output = Dense(units=encoding_size,
                               name='encoder_output')(encoder)
    
  3. 下一步是定义解码器部分:

        target_shape = tuple(encoder_output_shape[1:])
        decoder = Dense(np.prod(target_shape))(encoder _output)
        decoder = Reshape(target_shape)(decoder)
        decoder = Conv2DTranspose(filters=64,
                                  kernel_size=(3, 3),
                                  strides=2,
                                  padding='same')(decoder)
        decoder = LeakyReLU(alpha=alpha)(decoder)
        decoder = BatchNormalization()(decoder)
        decoder = Conv2DTranspose(filters=32,
                                  kernel_size=(3, 3),
                                  strides=2,
                                  padding='same')(decoder)
        decoder = LeakyReLU(alpha=alpha)(decoder)
        decoder = BatchNormalization()(decoder)
        decoder = Conv2DTranspose(filters=1,
                                  kernel_size=(3, 3),
                                  padding='same')(decoder)
        outputs = Activation(activation='sigmoid',
    
                         name='decoder_output')(decoder)
    
  4. 最后,构建自动编码器并返回它:

        autoencoder_model = Model(inputs, outputs)
        return autoencoder_model
    
  5. 定义一个计算两个向量之间欧几里得距离的函数:

    def euclidean_dist(x, y):
        return np.linalg.norm(x - y)
    
  6. 定义search()函数,该函数使用搜索索引(一个将特征向量与相应图像配对的字典)来检索与查询向量最相似的结果:

    def search(query_vector, search_index, 
               max_results=16):
        vectors = search_index['features']
        results = []
        for i in range(len(vectors)):
            distance = euclidean_dist(query_vector, 
                                       vectors[i])
            results.append((distance, 
                           search_index['images'][i]))
        results = sorted(results, 
                         key=lambda p: p[0])[:max_results]
        return results
    
  7. 加载Fashion-MNIST数据集。仅保留以下图像:

    (X_train, _), (X_test, _) = fashion_mnist.load_data()     
    
  8. 对图像进行归一化并添加颜色通道维度:

    X_train = X_train.astype('float32') / 255.0
    X_test = X_test.astype('float32') / 255.0
    X_train = np.expand_dims(X_train, axis=-1)
    X_test = np.expand_dims(X_test, axis=-1)
    
  9. 构建自动编码器并进行编译。我们将使用'adam'作为优化器,'mse'作为损失函数,因为这样可以很好地衡量模型的误差:

    autoencoder = build_autoencoder()
    autoencoder.compile(optimizer='adam', loss='mse')
    
  10. 训练自动编码器 10 个周期,每次批处理512张图像:

    EPOCHS = 50
    BATCH_SIZE = 512
    autoencoder.fit(X_train, X_train,
                    epochs=EPOCHS,
                    batch_size=BATCH_SIZE,
                    shuffle=True,
                    validation_data=(X_test, X_test))
    
  11. 创建一个新模型,我们将用它作为特征提取器。它将接收与自编码器相同的输入,并输出自编码器学到的编码。实质上,我们是使用自编码器的编码器部分将图像转换为向量:

    fe_input = autoencoder.input
    fe_output = autoencoder.get_layer('encoder_output').output
    feature_extractor = Model(inputs=fe_input, 
                             outputs=fe_output)
    
  12. 创建搜索索引,由X_train的特征向量和原始图像组成(原始图像必须重新调整为 28x28 并重新缩放到[0, 255]的范围):

    train_vectors = feature_extractor.predict(X_train)
    X_train = (X_train * 255.0).astype('uint8')
    X_train = X_train.reshape((X_train.shape[0], 28, 28))
    search_index = {
        'features': train_vectors,
        'images': X_train
    }
    
  13. 计算X_test的特征向量,我们将把它用作查询图像的样本。并将X_test调整为 28x28 的形状,并将其值重新缩放到[0, 255]的范围:

    test_vectors = feature_extractor.predict(X_test)
    X_test = (X_test * 255.0).astype('uint8')
    X_test = X_test.reshape((X_test.shape[0], 28, 28))
    
  14. 选择 16 个随机测试图像(以及其对应的特征向量)作为查询:

    sample_indices = np.random.randint(0, X_test.shape[0],16)
    sample_images = X_test[sample_indices]
    sample_queries = test_vectors[sample_indices]
    
  15. 对测试样本中的每个图像进行搜索,并保存查询图像与从索引中提取的结果之间的并排视觉对比(记住,索引是由训练数据组成的):

    for i, (vector, image) in \
            enumerate(zip(sample_queries, sample_images)):
        results = search(vector, search_index)
        results = [r[1] for r in results]
        query_image = cv2.resize(image, (28 * 4, 28 * 4),
                              interpolation=cv2.INTER_AREA)
        results_mosaic = 
                 np.vstack([np.hstack(results[0:4]),
                            np.hstack(results[4:8]),
                            np.hstack(results[8:12]),
                            np.hstack(results[12:16])])
        result_image = np.hstack([query_image, 
                                 results_mosaic])
        cv2.imwrite(f'{i}.jpg', result_image)
    

    下面是一个搜索结果的示例:

图 5.5 – 左:鞋子的查询图像。右:最佳的 16 个搜索结果,所有结果也都包含鞋子

图 5.5 – 左:鞋子的查询图像。右:最佳的 16 个搜索结果,所有结果也都包含鞋子

正如前面的图像所示,我们的图像搜索索引是成功的!我们将在下一部分看到它是如何工作的。

它是如何工作的…

在本食谱中,我们学习了如何利用自编码器的独特特征——学习一个大大压缩输入图像信息的编码,从而实现最小的信息损失。然后,我们使用卷积自编码器的编码器部分提取时尚物品照片的特征,并构建了一个图像搜索索引。

通过这样做,使用这个索引作为搜索引擎就像计算查询向量(对应于查询图像)与索引中所有图像之间的欧几里得距离一样简单,只选择那些最接近查询的图像。

我们解决方案中最重要的方面是训练一个足够优秀的自编码器,以生成高质量的向量,因为它们决定了搜索引擎的成败。

另见

该实现基于 Dong 等人的出色工作,论文可在此阅读:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch5/recipe5

实现变分自编码器

自编码器的一些最现代且复杂的应用场景是变分自编码器VAE)。它们与其他自编码器的不同之处在于,变分自编码器并不是学习一个任意的函数,而是学习输入图像的概率分布。我们可以从这个分布中采样,以生成新的、未见过的数据点。

VAE实际上是一个生成模型,在这个食谱中,我们将实现一个。

准备就绪

我们不需要为这个食谱做任何特别的准备,所以让我们立即开始吧!

如何操作…

按照以下步骤学习如何实现和训练VAE

  1. 导入必要的包:

    import matplotlib.pyplot as plt
    import numpy as np
    import tensorflow as tf
    from tensorflow.keras import Model
    from tensorflow.keras import backend as K
    from tensorflow.keras.datasets import fashion_mnist
    from tensorflow.keras.layers import *
    from tensorflow.keras.losses import mse
    from tensorflow.keras.optimizers import Adam
    
  2. 因为我们很快会使用tf.function注解,所以我们必须告诉 TensorFlow 以急切执行(eager execution)的方式运行函数:

    tf.config.experimental_run_functions_eagerly(True)
    
  3. 定义一个类,封装我们实现self.z_log_varself.z_mean的功能,它们是我们将学习的潜在高斯分布的参数:

            self.z_log_var = None
            self.z_mean = None
    
  4. 定义一些成员变量,用于存储encoderdecodervae的输入和输出:

            self.inputs = None
            self.outputs = None
            self.encoder = None
            self.decoder = None
            self.vae = None
    
  5. 定义build_vae()方法,该方法构建变分自编码器架构(请注意,我们使用的是全连接层而不是卷积层):

        def build_vae(self):
            self.inputs = Input(shape=(self.original_dimension,))
            x = Dense(self.encoding_dimension)(self.inputs)
            x = ReLU()(x)
            self.z_mean = Dense(self.latent_dimension)(x)
            self.z_log_var = Dense(self.latent_dimension)(x)
            z = Lambda(sampling)([self.z_mean, 
                                 self.z_log_var])
            self.encoder = Model(self.inputs,
                                 [self.z_mean, 
                                 self.z_log_var, z])
    

    请注意,编码器只是一个完全连接的网络,它产生三个输出:self.z_mean,这是我们训练建模的高斯分布的均值;self.z_log_var,这是该分布的对数方差;以及z,这是该概率空间中的一个样本点。为了简单地生成z,我们必须在Lambda层中包装一个自定义函数sampling()(在第 5 步中实现)。

  6. 接下来,定义解码器:

            latent_inputs = Input(shape=(self.latent_dimension,))
            x = Dense(self.encoding_dimension)(latent_inputs)
            x = ReLU()(x)
            self.outputs = Dense(self.original_dimension)(x)
            self.outputs = Activation('sigmoid')(self.outputs)
            self.decoder = Model(latent_inputs, 
                                 self.outputs)
    
  7. 解码器只是另一个完全连接的网络。解码器将从潜在维度中取样,以重构输入。最后,将编码器和解码器连接起来,创建VAE模型:

            self.outputs = self.encoder(self.inputs)[2]
            self.outputs = self.decoder(self.outputs)
            self.vae = Model(self.inputs, self.outputs)
    
  8. 定义train()方法,该方法训练变分自编码器。因此,它接收训练和测试数据,以及迭代次数和批次大小:

        @tf.function
        def train(self, X_train,
                  X_test, 
                  epochs=50, 
                  batch_size=64):
    
  9. 将重建损失定义为输入和输出之间的均方误差(MSE):

            reconstruction_loss = mse(self.inputs, 
                                      self.outputs)
            reconstruction_loss *= self.original_dimension
    

    kl_lossreconstruction_loss

            kl_loss = (1 + self.z_log_var -
                       K.square(self.z_mean) -
                       K.exp(self.z_log_var))
            kl_loss = K.sum(kl_loss, axis=-1)
            kl_loss *= -0.5
            vae_loss = K.mean(reconstruction_loss + kl_loss)
    
  10. 配置self.vae模型,使其使用vae_lossAdam()作为优化器(学习率为 0.003)。然后,在指定的迭代次数内拟合网络。最后,返回三个模型:

            self.vae.add_loss(vae_loss)
            self.vae.compile(optimizer=Adam(lr=1e-3))
            self.vae.fit(X_train,
                         epochs=epochs,
                         batch_size=batch_size,
                         validation_data=(X_test, None))
            return self.encoder, self.decoder, self.vae
    
  11. 定义一个函数,该函数将在给定两个相关参数(通过arguments数组传递)时生成潜在空间中的随机样本或点;即,z_meanz_log_var

    def sampling(arguments):
        z_mean, z_log_var = arguments
        batch = K.shape(z_mean)[0]
        dimension = K.int_shape(z_mean)[1]
        epsilon = K.random_normal(shape=(batch, dimension))
        return z_mean + K.exp(0.5 * z_log_var) * epsilon
    

    请注意,epsilon是一个随机的高斯向量。

  12. 定义一个函数,该函数将生成并绘制从潜在空间生成的图像。这将帮助我们了解靠近分布的形状,以及接近曲线尾部的形状:

    def generate_and_plot(decoder, grid_size=5):
        cell_size = 28
        figure_shape = (grid_size * cell_size,
                        grid_size * cell_size)
        figure = np.zeros(figure_shape)
    
  13. 创建一个值的范围,X 轴和 Y 轴的值从-4 到 4。我们将使用这些值在每个位置生成和可视化样本:

        grid_x = np.linspace(-4, 4, grid_size)
        grid_y = np.linspace(-4, 4, grid_size)[::-1]
    
  14. 使用解码器为每个z_meanz_log_var的组合生成新的样本:

        for i, z_log_var in enumerate(grid_y):
            for j, z_mean in enumerate(grid_x):
                z_sample = np.array([[z_mean, z_log_var]])
                generated = decoder.predict(z_sample)[0]
    
  15. 重塑样本,并将其放置在网格中的相应单元格中:

                fashion_item = 
                       generated.reshape(cell_size,
                                        cell_size)
                y_slice = slice(i * cell_size, 
                                (i + 1) * cell_size)
                x_slice = slice(j * cell_size, 
                                (j + 1) * cell_size)
                figure[y_slice, x_slice] = fashion_item
    
  16. 添加刻度和坐标轴标签,然后显示图形:

        plt.figure(figsize=(10, 10))
        start = cell_size // 2
        end = (grid_size - 2) * cell_size + start + 1
        pixel_range = np.arange(start, end, cell_size)
        sample_range_x = np.round(grid_x, 1)
        sample_range_y = np.round(grid_y, 1)
        plt.xticks(pixel_range, sample_range_x)
        plt.yticks(pixel_range, sample_range_y)
        plt.xlabel('z_mean')
        plt.ylabel('z_log_var')
        plt.imshow(figure)
        plt.show()
    
  17. 加载Fashion-MNIST数据集。对图像进行归一化,并添加颜色通道:

    (X_train, _), (X_test, _) = fashion_mnist.load_data()
    X_train = X_train.astype('float32') / 255.0
    X_test = X_test.astype('float32') / 255.0
    X_train = X_train.reshape((X_train.shape[0], -1))
    X_test = X_test.reshape((X_test.shape[0], -1))
    
  18. 实例化并构建变分自编码器

    vae = VAE(original_dimension=784,
              encoding_dimension=512,
              latent_dimension=2)
    vae.build_vae()
    
  19. 训练模型 100 个周期:

    _, decoder_model, vae_model = vae.train(X_train, X_test, 
                                            epochs=100)
    
  20. 使用解码器生成新图像并绘制结果:

    generate_and_plot(decoder_model, grid_size=7)
    

    这是结果:

图 5.6 – VAE 学习的潜在空间的可视化

图 5.6 – VAE 学习的潜在空间可视化

在这里,我们可以看到构成潜在空间的点集,以及这些点对应的服装项目。 这是网络学习的概率分布的一个表示,其中分布中心的项目类似于 T 恤,而边缘的项目则更像裤子、毛衣和鞋子。

让我们继续进入下一节。

它是如何工作的……

在这个示例中,我们了解到,变分自编码器是一种更先进、更复杂的自编码器,它不像学习一个任意的、简单的函数来将输入映射到输出,而是学习输入的概率分布。这样,它就能够生成新的、未见过的图像,成为更现代生成模型的前驱,例如生成对抗网络GANs)。

这个架构与我们在本章中学习的其他自编码器并没有太大不同。理解z的关键在于,我们通过sampling()函数在 Lambda 层中生成z

这意味着,在每次迭代中,整个网络都在优化z_meanz_log_var参数,使其与输入的概率分布尽可能接近。这样做是因为,只有这样,随机样本(z)的质量才足够高,解码器才能生成更好、更逼真的输出。

另见

我们可以用来调节VAE的一个关键组件是Kullback-Leibler散度,您可以在这里阅读更多内容:en.wikipedia.org/wiki/Kullback%E2%80%93Leibler_divergence

请注意,VAE是生成模型的完美开端,我们将在下一章深入讨论它!

第六章:第六章:生成模型与对抗攻击

能够区分两个或更多类别无疑是令人印象深刻的,且是深度神经网络确实在学习的健康信号。

但如果传统的分类任务令人印象深刻,那么生成新内容则令人叹为观止!这绝对需要对领域有更高的理解。那么,有没有神经网络能够做到这一点呢?当然有!

在本章中,我们将研究神经网络中最迷人且最有前景的一种类型:生成对抗网络GANs)。顾名思义,这些网络实际上是由两个子网络组成的系统:生成器和判别器。生成器的任务是生成足够优秀的图像,使它们看起来像是来自原始分布(但实际上并非如此;它们是从零开始生成的),从而欺骗判别器,而判别器的任务是分辨真假图像。

GANs 在半监督学习和图像到图像的翻译等领域处于尖端位置,这两个主题我们将在本章中讨论。作为补充,本章最后的食谱将教我们如何使用快速梯度符号方法FGSM)对网络进行对抗攻击。

本章我们将涉及的食谱如下:

  • 实现一个深度卷积 GAN

  • 使用 DCGAN 进行半监督学习

  • 使用 Pix2Pix 进行图像翻译

  • 使用 CycleGAN 翻译未配对的图像

  • 使用快速梯度符号方法实现对抗攻击

技术要求

GANs 很棒,但在计算能力方面非常消耗资源。因此,GPU 是必不可少的,才能在这些食谱上进行操作(即使如此,大多数情况仍需运行几个小时)。在准备工作部分,你会发现每个食谱所需的准备工作(如果有的话)。本章的代码可以在这里找到:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch6

查看以下链接,观看《代码实践》视频:bit.ly/35Z8IYn

实现一个深度卷积 GAN

一个 seed,它只是一个高斯噪声的向量。

在本食谱中,我们将实现一个 EMNIST 数据集,它是在原有的 MNIST 数据集的基础上,加入了大写和小写的手写字母,并涵盖了从 0 到 9 的数字。

让我们开始吧!

准备工作

我们需要安装 tensorflow-datasets 来更方便地访问 EMNIST。另外,为了在训练 GAN 时显示漂亮的进度条,我们将使用 tqdm

这两个依赖项可以按如下方式安装:

$> pip install tensorflow-datasets tqdm

我们可以开始了!

如何实现…

执行以下步骤来在 EMNIST 上实现 DCGAN:

  1. 导入必要的依赖项:

    import matplotlib.pyplot as plt
    import tensorflow as tf
    import tensorflow_datasets as tfds
    from tensorflow.keras.layers import *
    from tensorflow.keras.losses import BinaryCrossentropy
    from tensorflow.keras.models import Model
    from tensorflow.keras.optimizers import Adam
    from tqdm import tqdm
    
  2. 定义 AUTOTUNE 设置的别名,我们将在后续处理中使用它来确定处理数据集时的并行调用数量:

    AUTOTUNE = tf.data.experimental.AUTOTUNE
    
  3. 定义一个 DCGAN() 类来封装我们的实现。构造函数创建判别器、生成器、损失函数以及两个子网络各自的优化器:

    class DCGAN(object):
        def __init__(self):
            self.loss = BinaryCrossentropy(from_logits=True)
            self.generator = self.create_generator()
            self.discriminator = self.create_discriminator()
            self.generator_opt = Adam(learning_rate=1e-4)
            self.discriminator_opt = Adam(learning_rate=1e-4)
    
  4. 定义一个静态方法来创建生成器网络。它从一个 100 元素的输入张量重建一个 28x28x1 的图像。注意,使用了转置卷积(Conv2DTranspose)来扩展输出体积,随着网络的深入,卷积层数量也增多。同时,注意激活函数为 'tanh',这意味着输出将处于 [-1, 1] 的范围内:

       @staticmethod
        def create_generator(alpha=0.2):
            input = Input(shape=(100,))
            x = Dense(units=7 * 7 * 256, 
                     use_bias=False)(input)
            x = LeakyReLU(alpha=alpha)(x)
            x = BatchNormalization()(x)
            x = Reshape((7, 7, 256))(x)
    
  5. 添加第一个转置卷积块,具有 128 个滤波器:

            x = Conv2DTranspose(filters=128,
                                strides=(1, 1),
                                kernel_size=(5, 5),
                                padding='same',
                                use_bias=False)(x)
            x = LeakyReLU(alpha=alpha)(x)
            x = BatchNormalization()(x)
    
  6. 创建第二个转置卷积块,具有 64 个滤波器:

            x = Conv2DTranspose(filters=64,
                                strides=(2, 2),
                                kernel_size=(5, 5),
                                padding='same',
                                use_bias=False)(x)
            x = LeakyReLU(alpha=alpha)(x)
            x = BatchNormalization()(x)
    
  7. 添加最后一个转置卷积块,只有一个滤波器,对应于网络的输出:

            x = Conv2DTranspose(filters=1,
                                strides=(2, 2),
                                kernel_size=(5, 5),
                                padding='same',
                                use_bias=False)(x)
            output = Activation('tanh')(x)
            return Model(input, output)
    
  8. 定义一个静态方法来创建判别器。该架构是一个常规的 CNN:

        @staticmethod
        def create_discriminator(alpha=0.2, dropout=0.3):
            input = Input(shape=(28, 28, 1))
            x = Conv2D(filters=64,
                       kernel_size=(5, 5),
                       strides=(2, 2),
                       padding='same')(input)
            x = LeakyReLU(alpha=alpha)(x)
            x = Dropout(rate=dropout)(x)
            x = Conv2D(filters=128,
                       kernel_size=(5, 5),
                       strides=(2, 2),
                       padding='same')(x)
            x = LeakyReLU(alpha=alpha)(x)
            x = Dropout(rate=dropout)(x)
            x = Flatten()(x)
            output = Dense(units=1)(x)
            return Model(input, output)
    
  9. 定义一个方法来计算判别器的损失,它是实际损失和假损失的总和:

        def discriminator_loss(self, real, fake):
            real_loss = self.loss(tf.ones_like(real), real)
            fake_loss = self.loss(tf.zeros_like(fake), fake)
            return real_loss + fake_loss
    
  10. 定义一个方法来计算生成器的损失:

        def generator_loss(self, fake):
            return self.loss(tf.ones_like(fake), fake)
    
  11. 定义一个方法来执行单次训练步骤。我们将从生成一个随机高斯噪声向量开始:

        @tf.function
        def train_step(self, images, batch_size):
            noise = tf.random.normal((batch_size,noise_dimension))
    
  12. 接下来,将随机噪声传递给生成器以生成假图像:

            with tf.GradientTape() as gen_tape, \
                    tf.GradientTape() as dis_tape:
                generated_images = self.generator(noise,
                                            training=True)
    
  13. 将真实图像和假图像传递给判别器,并计算两个子网络的损失:

                real = self.discriminator(images, 
                                          training=True)
                fake = self.discriminator(generated_images,
                                          training=True)
                gen_loss = self.generator_loss(fake)
                disc_loss = self.discriminator_loss(real, 
                                                   fake)
    
  14. 计算梯度:

            generator_grad = gen_tape \
                .gradient(gen_loss,
                          self.generator.trainable_variables)
            discriminator_grad = dis_tape \
                .gradient(disc_loss,
                    self.discriminator.trainable_       variables)
    
  15. 接下来,使用各自的优化器应用梯度:

            opt_args = zip(generator_grad,
                          self.generator.trainable_variables)
            self.generator_opt.apply_gradients(opt_args)
            opt_args = zip(discriminator_grad,
    
                   self.discriminator.trainable_variables)
            self.discriminator_opt.apply_gradients(opt_args)
    
  16. 最后,定义一个方法来训练整个架构。每训练 10 个周期,我们将绘制生成器生成的图像,以便直观地评估它们的质量:

        def train(self, dataset, test_seed, epochs, 
                   batch_size):
            for epoch in tqdm(range(epochs)):
                for image_batch in dataset:
                    self.train_step(image_batch, 
                                     batch_size)
                if epoch == 0 or epoch % 10 == 0:
    
               generate_and_save_images(self.generator,
                                             epoch,
                                             test_seed)
    
  17. 定义一个函数来生成新图像,然后将它们的 4x4 马赛克保存到磁盘:

    def generate_and_save_images(model, epoch, test_input):
        predictions = model(test_input, training=False)
        plt.figure(figsize=(4, 4))
        for i in range(predictions.shape[0]):
            plt.subplot(4, 4, i + 1)
            image = predictions[i, :, :, 0] * 127.5 + 127.5
            image = tf.cast(image, tf.uint8)
            plt.imshow(image, cmap='gray')
            plt.axis('off')
        plt.savefig(f'{epoch}.png')
        plt.show()
    
  18. 定义一个函数来将来自 EMNIST 数据集的图像缩放到 [-1, 1] 区间:

    def process_image(input):
        image = tf.cast(input['image'], tf.float32)
        image = (image - 127.5) / 127.5
        return image
    
  19. 使用 tfds 加载 EMNIST 数据集。我们只使用 'train' 数据集,其中包含超过 60 万张图像。我们还会确保将每张图像缩放到 'tanh' 范围内:

    BUFFER_SIZE = 1000
    BATCH_SIZE = 512
    train_dataset = (tfds
                     .load('emnist', split='train')
                     .map(process_image,
                          num_parallel_calls=AUTOTUNE)
                     .shuffle(BUFFER_SIZE)
                     .batch(BATCH_SIZE))
    
  20. 创建一个测试种子,在整个 DCGAN 训练过程中用于生成图像:

    noise_dimension = 100
    num_examples_to_generate = 16
    seed_shape = (num_examples_to_generate, 
                  noise_dimension)
    test_seed = tf.random.normal(seed_shape)
    
  21. 最后,实例化并训练一个 DCGAN() 实例,训练 200 个周期:

    EPOCHS = 200
    dcgan = DCGAN()
    dcgan.train(train_dataset, test_seed, EPOCHS, BATCH_SIZE)
    

    由 GAN 生成的第一张图像将类似于这个,只是一些没有形状的斑点:

图 6.1 – 在第 0 个周期生成的图像

图 6.1 – 在第 0 个周期生成的图像

在训练过程结束时,结果要好得多:

图 6.2 – 在第 200 个周期生成的图像

图 6.2 – 在第 200 个周期生成的图像

图 6.2 中,我们可以辨认出熟悉的字母和数字,包括 Ad9XB。然而,在第一行中,我们注意到几个模糊的形状,这表明生成器还有改进的空间。

让我们在下一节中看看它是如何工作的。

它是如何工作的……

在这个配方中,我们学到 GAN 是协同工作的,不像自编码器那样相互配合,它们是相互对抗的(因此名字中有 对抗 二字)。当我们专注于生成器时,判别器只是一个训练生成器的工具,正如本例中的情况一样。这意味着训练后,判别器会被丢弃。

我们的生成器实际上是一个解码器,它接收一个包含 100 个元素的随机高斯向量,并生成 28x28x1 的图像,接着这些图像被传递给判别器,一个常规的 CNN,判别器需要判断它们是真实的还是伪造的。

因为我们的目标是创造最好的生成器,所以判别器尝试解决的分类问题与 EMNIST 中的实际类别无关。因此,我们不会事先明确标记图像为真实或伪造,但在 discriminator_loss() 方法中,我们知道所有来自 real 的图像都来自 EMNIST,因此我们对一个全为 1 的张量(tf.ones_like(real))计算损失,类似地,所有来自 fake 的图像是合成的,我们对一个全为 0 的张量(tf.zeros_like(fake))计算损失。

另一方面,生成器在计算其损失时会考虑来自判别器的反馈,以改进其输出。

必须注意的是,这里的目标是实现平衡,而不是最小化损失。因此,视觉检查至关重要,这也是我们每隔 10 个周期保存生成器输出的图像的原因。

最终,我们从第 0 个周期的随机、无形的块,到了第 200 个周期时,生成了可识别的数字和字母,尽管网络仍然可以进一步改进。

另见

你可以在这里阅读更多关于 EMNIST 的内容:arxiv.org/abs/1702.05373v1

使用 DCGAN 进行半监督学习

数据是开发任何深度学习模型中最重要的部分。然而,好的数据通常稀缺且获取成本高。好消息是,GAN 可以在这些情况下提供帮助,通过人工生成新颖的训练示例,这个过程被称为 半监督学习

在这个配方中,我们将开发一个特殊的 DCGAN 架构,在 Fashion-MNIST 的一个非常小的子集上训练分类器,并仍然达到不错的性能。

让我们开始吧,怎么样?

准备工作

我们不需要额外的东西来访问 Fashion-MNIST,因为它与 TensorFlow 一起捆绑提供。为了显示一个好看的进度条,让我们安装 tqdm

$> pip install tqdm

现在让我们进入下一部分,开始实现这个配方。

如何操作……

执行以下步骤以完成配方:

  1. 让我们开始导入所需的包:

    import numpy as np
    from numpy.random import *
    from tensorflow.keras import backend as K
    from tensorflow.keras.datasets import fashion_mnist as fmnist
    from tensorflow.keras.layers import *
    from tensorflow.keras.models import Model
    from tensorflow.keras.optimizers import Adam
    from tqdm import tqdm
    
  2. 定义 pick_supervised_subset() 函数来选择数据的子集。这将帮助我们模拟数据稀缺的情况,非常适合半监督学习。

    def pick_supervised_subset(feats,
                               labels,
                               n_samples=1000,
                               n_classes=10):
        samples_per_class = int(n_samples / n_classes)
        X = []
        y = []
        for i in range(n_classes):
            class_feats = feats[labels == i]
            class_sample_idx = randint(low=0,
    
                                   high=len(class_feats),
                                  size=samples_per_class)
            X.extend([class_feats[j] for j in 
                      class_sample_idx])
            y.extend([i] * samples_per_class)
        return np.array(X), np.array(y)
    
  3. 现在,定义一个函数来选择一个随机数据样本用于分类。这意味着我们将使用原始数据集中的标签:

    def pick_samples_for_classification(feats, labels, 
                                         n_samples):
        sample_idx = randint(low=0,
                             high=feats.shape[0],
                             size=n_samples)
        X = np.array([feats[i] for i in sample_idx])
        y = np.array([labels[i] for i in sample_idx])
        return X, y
    
  4. 定义 pick_samples_for_discrimination() 函数以选择一个随机样本用于判别。与上一个函数的主要区别在于这里的标签都是 1,表示所有的图像都是真实的,这清楚地表明该样本是为判别器准备的:

    def pick_samples_for_discrimination(feats, n_samples):
        sample_idx = randint(low=0,
                             high=feats.shape[0],
                             size=n_samples)
        X = np.array([feats[i] for i in sample_idx])
        y = np.ones((n_samples, 1))
        return X, y
    
  5. 实现 generate_fake_samples() 函数来生成一批潜在点,换句话说,就是一组随机噪声向量,生成器将利用这些向量生成假图像:

    def generate_fake_samples(model, latent_size, 
                              n_samples):
        z_input = generate_latent_points(latent_size, 
                                          n_samples)
        images = model.predict(z_input)
        y = np.zeros((n_samples, 1))
        return images, y
    
  6. 创建 generate_fake_samples() 函数,用生成器生成假数据:

    def generate_fake_samples(model, latent_size, 
                              n_samples):
        z_input = generate_latent_points(latent_size, 
                                          n_samples)
        images = model.predict(z_input)
        y = np.zeros((n_samples, 1))
        return images, y
    
  7. 我们已经准备好定义我们的半监督式 DCGAN,接下来将它封装在此处定义的 SSGAN() 类中。我们将从构造函数开始:

    class SSGAN(object):
        def __init__(self,
                     latent_size=100,
                     input_shape=(28, 28, 1),
                     alpha=0.2):
            self.latent_size = latent_size
            self.input_shape = input_shape
            self.alpha = alpha
    
  8. 在将参数作为成员存储后,让我们实例化判别器:

            (self.classifier,
             self.discriminator) = self._create_discriminators()
    
  9. 现在,编译分类器和判别器模型:

            clf_opt = Adam(learning_rate=2e-4, beta_1=0.5)
            self.classifier.compile(
                loss='sparse_categorical_crossentropy',
                optimizer=clf_opt,
                metrics=['accuracy'])
            dis_opt = Adam(learning_rate=2e-4, beta_1=0.5)
            self.discriminator.compile(loss='binary_crossentropy',
                                       optimizer=dis_opt)
    
  10. 创建生成器:

            self.generator = self._create_generator()
    
  11. 创建 GAN 并进行编译:

            self.gan = self._create_gan()
            gan_opt = Adam(learning_rate=2e-4, beta_1=0.5)
            self.gan.compile(loss='binary_crossentropy',
                             optimizer=gan_opt)
    
  12. 定义私有的 _create_discriminators() 方法来创建判别器。内部的 custom_activation() 函数用于激活分类器模型的输出,生成一个介于 0 和 1 之间的值,用于判断图像是真实的还是假的:

        def _create_discriminators(self, num_classes=10):
            def custom_activation(x):
                log_exp_sum = K.sum(K.exp(x), axis=-1,
                                    keepdims=True)
                return log_exp_sum / (log_exp_sum + 1.0)
    
  13. 定义分类器架构,它只是一个常规的 softmax 激活的 CNN:

            input = Input(shape=self.input_shape)
            x = input
            for _ in range(3):
                x = Conv2D(filters=128,
                           kernel_size=(3, 3),
                           strides=2,
                           padding='same')(x)
                x = LeakyReLU(alpha=self.alpha)(x)
            x = Flatten()(x)
            x = Dropout(rate=0.4)(x)
            x = Dense(units=num_classes)(x)
            clf_output = Softmax()(x)
            clf_model = Model(input, clf_output)
    
  14. 判别器与分类器共享权重,但不同的是,它不再使用 softmax 激活输出,而是使用之前定义的 custom_activation() 函数:

            dis_output = Lambda(custom_activation)(x)
            discriminator_model = Model(input, dis_output)
    
  15. 返回分类器和判别器:

            return clf_model, discriminator_model
    
  16. 创建私有的 _create_generator() 方法来实现生成器架构,实际上它只是一个解码器,正如本章第一节中所解释的那样:

        def _create_generator(self):
            input = Input(shape=(self.latent_size,))
            x = Dense(units=128 * 7 * 7)(input)
            x = LeakyReLU(alpha=self.alpha)(x)
            x = Reshape((7, 7, 128))(x)
            for _ in range(2):
                x = Conv2DTranspose(filters=128,
                                    kernel_size=(4, 4),
                                    strides=2,
                                    padding='same')(x)
                x = LeakyReLU(alpha=self.alpha)(x)
            x = Conv2D(filters=1,
                       kernel_size=(7, 7),
                       padding='same')(x)
            output = Activation('tanh')(x)
            return Model(input, output)
    
  17. 定义私有的 _create_gan() 方法来创建 GAN 本身,实际上它只是生成器和判别器之间的连接:

        def _create_gan(self):
            self.discriminator.trainable = False
            output = 
                  self.discriminator(self.generator.output)
            return Model(self.generator.input, output)
    
  18. 最后,定义 train() 函数来训练整个系统。我们将从选择将要训练的 Fashion-MNIST 子集开始,然后定义所需的批次和训练步骤数量来适配架构:

        def train(self, X, y, epochs=20, num_batches=100):
            X_sup, y_sup = pick_supervised_subset(X, y)
            batches_per_epoch = int(X.shape[0] / num_batches)
            num_steps = batches_per_epoch * epochs
            num_samples = int(num_batches / 2)
    
  19. 选择用于分类的样本,并使用这些样本来训练分类器:

            for _ in tqdm(range(num_steps)):
                X_sup_real, y_sup_real = \
                    pick_samples_for_classification(X_sup,
                                                    y_sup,
                                              num_samples)
                self.classifier.train_on_batch(X_sup_real,
                                               y_sup_real)
    
  20. 选择真实样本进行判别,并使用这些样本来训练判别器:

                X_real, y_real = \
                    pick_samples_for_discrimination(X,
                                              num_samples)
            self.discriminator.train_on_batch(X_real, y_real)
    
  21. 使用生成器生成假数据,并用这些数据来训练判别器:

                X_fake, y_fake = \
                    generate_fake_samples(self.generator,
                                        self.latent_size,
                                          num_samples)
                self.discriminator.train_on_batch(X_fake, 
                                                 y_fake)
    
  22. 生成潜在点,并利用这些点训练 GAN:

                X_gan = generate_latent_points(self.latent_size,
                          num_batches)
                y_gan = np.ones((num_batches, 1))
                self.gan.train_on_batch(X_gan, y_gan)
    
  23. 加载 Fashion-MNIST 数据集并对训练集和测试集进行归一化处理:

    (X_train, y_train), (X_test, y_test) = fmnist.load_data()
    X_train = np.expand_dims(X_train, axis=-1)
    X_train = (X_train.astype(np.float32) - 127.5) / 127.5
    X_test = np.expand_dims(X_test, axis=-1)
    X_test = (X_test.astype(np.float32) - 127.5) / 127.5
    
  24. 实例化一个 SSCGAN() 并训练 30 个 epoch:

    ssgan = SSGAN()
    ssgan.train(X_train, y_train, epochs=30)
    
  25. 报告分类器在训练集和测试集上的准确率:

    train_acc = ssgan.classifier.evaluate(X_train, 
                                          y_train)[1]
    train_acc *= 100
    print(f'Train accuracy: {train_acc:.2f}%')
    test_acc = ssgan.classifier.evaluate(X_test, y_test)[1]
    test_acc *= 100
    print(f'Test accuracy: {test_acc:.2f}%')
    

训练完成后,训练集和测试集的准确率应该都在 83% 左右,如果考虑到我们只使用了 50,000 个样本中的 1,000 个,这个结果是相当令人满意的!

它的工作原理…

在本食谱中,我们实现了一个与本章开头的 实现深度卷积 GAN 食谱中实现的架构非常相似。主要的区别在于我们有两个判别器:第一个实际上是一个分类器,训练时使用我们手头的少量标记数据的子集;另一个是常规判别器,其唯一任务是不要被生成器欺骗。

分类器如何在如此少的数据下取得如此出色的性能?答案是共享权重。分类器和判别器共享相同的特征提取层,唯一的区别在于最终的输出层,分类器使用普通的 softmax 函数进行激活,而判别器则使用一个 Lambda() 层包裹我们的 custom_activation() 函数进行激活。

这意味着每次分类器在一批标记数据上训练时,这些共享权重都会被更新,同时判别器在真实和假图像上训练时也会更新。最终,我们借助生成器解决了数据稀缺问题。

很厉害吧?

参见

你可以通过阅读最初提出这种方法的论文来巩固对本食谱中使用的半监督训练方法的理解:arxiv.org/abs/1606.03498

使用 Pix2Pix 翻译图像

GAN 最有趣的应用之一是图像到图像的翻译,顾名思义,它包括将一个图像领域的内容翻译到另一个领域(例如,素描到照片,黑白图像到 RGB,Google Maps 到卫星视图等)。

在本食谱中,我们将实现一个相当复杂的条件对抗网络,称为 Pix2Pix。我们将专注于解决方案的实际应用,如果你想了解更多文献,可以查看食谱末尾的 参见 部分。

准备工作

我们将使用 cityscapes 数据集,它可以在此处找到:https://people.eecs.berkeley.edu/~tinghuiz/projects/pix2pix/datasets/cityscapes.tar.gz。下载并解压到你选择的位置。为了本教程的目的,我们假设它被放置在 ~/.keras/datasets 目录下,命名为 cityscapes。为了在训练过程中显示进度条,安装 tqdm

$> pip install tqdm

在本节结束时,我们将学会如何使用 Pix2Pix 从右侧的图像生成左侧的图像:

图 6.3 – 我们将使用右侧的分割图像生成像左侧那样的真实世界图像

图 6.3 – 我们将使用右侧的分割图像生成像左侧那样的真实世界图像

让我们开始吧!

如何实现…

完成这些步骤后,你将从头实现 Pix2Pix!

  1. 导入依赖项:

    import pathlib
    import cv2
    import numpy as np
    import tensorflow as tf
    import tqdm
    from tensorflow.keras.layers import *
    from tensorflow.keras.losses import BinaryCrossentropy
    from tensorflow.keras.models import *
    from tensorflow.keras.optimizers import Adam
    
  2. 定义 TensorFlow 的自动调优和调整大小选项的常量,以及图像尺寸。我们将调整数据集中的所有图像:

    AUTOTUNE = tf.data.experimental.AUTOTUNE
    NEAREST_NEIGHBOR = tf.image.ResizeMethod.NEAREST_NEIGHBOR
    IMAGE_WIDTH = 256
    IMAGE_HEIGHT = 256
    
  3. 数据集中的每张图像由输入和目标组成,因此在处理完图像后,我们需要将它们拆分成单独的图像。load_image() 函数实现了这一点:

    def load_image(image_path):
        image = tf.io.read_file(image_path)
        image = tf.image.decode_jpeg(image)
        width = tf.shape(image)[1]
        width = width // 2
        real_image = image[:, :width, :]
        input_image = image[:, width:, :]
        input_image = tf.cast(input_image, tf.float32)
        real_image = tf.cast(real_image, tf.float32)
        return input_image, real_image
    
  4. 让我们创建 resize() 函数来调整输入图像和目标图像的大小:

     def resize(input_image, real_image, height, width):
        input_image = tf.image.resize(input_image,
                                  size=(height,width),
                                 method=NEAREST_NEIGHBOR)
        real_image = tf.image.resize(real_image,
                                     size=(height, width),
                                  method=NEAREST_NEIGHBOR)
        return input_image, real_image
    
  5. 现在,实施 random_crop() 函数,对图像进行随机裁剪:

    def random_crop(input_image, real_image):
        stacked_image = tf.stack([input_image, 
                                 real_image],axis=0)
        size = (2, IMAGE_HEIGHT, IMAGE_WIDTH, 3)
        cropped_image = tf.image.random_crop(stacked_image,
                                             size=size)
        input_image = cropped_image[0]
        real_image = cropped_image[1]
        return input_image, real_image
    
  6. 接下来,编写 normalize() 函数,将图像归一化到 [-1, 1] 范围内:

    def normalize(input_image, real_image):
        input_image = (input_image / 127.5) - 1
        real_image = (real_image / 127.5) - 1
        return input_image, real_image
    
  7. 定义 random_jitter() 函数,对输入图像进行随机抖动(注意它使用了 第 4 步第 5 步 中定义的函数):

    @tf.function
    def random_jitter(input_image, real_image):
        input_image, real_image = resize(input_image, 
                                         real_image,
                                         width=286, 
                                          height=286)
        input_image, real_image = random_crop(input_image,
                                              real_image)
        if np.random.uniform() > 0.5:
            input_image = \
                  tf.image.flip_left_right(input_image)
            real_image = \
                 tf.image.flip_left_right(real_image)
        return input_image, real_image
    
  8. 创建 load_training_image() 函数,用于加载和增强训练图像:

    def load_training_image(image_path):
        input_image, real_image = load_image(image_path)
        input_image, real_image = \
            random_jitter(input_image, real_image)
        input_image, real_image = \
            normalize(input_image, real_image)
        return input_image, real_image
    
  9. 现在,让我们实现 load_test_image() 函数,顾名思义,它将用于加载测试图像:

    def load_test_image(image_path):
        input_image, real_image = load_image(image_path)
        input_image, real_image = resize(input_image, 
                                         real_image,
                                       width=IMAGE_WIDTH,
                                     height=IMAGE_HEIGHT)
        input_image, real_image = \
            normalize(input_image, real_image)
        return input_image, real_image
    
  10. 现在,让我们继续创建 generate_and_save_images() 函数,来存储生成器模型生成的合成图像。结果图像将是 inputtargetprediction 的拼接:

    def generate_and_save_images(model, input, target,epoch):
        prediction = model(input, training=True)
        display_list = [input[0], target[0], prediction[0]]
        image = np.hstack(display_list)
        image *= 0.5
        image += 0.5
        image *= 255.0
        image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
        cv2.imwrite(f'{epoch + 1}.jpg', image)
    
  11. 接下来,定义 Pix2Pix() 类,封装此架构的实现。首先是构造函数:

    class Pix2Pix(object):
        def __init__(self, output_channels=3, 
                     lambda_value=100):
            self.loss = BinaryCrossentropy(from_logits=True)
            self.output_channels = output_channels
            self._lambda = lambda_value
            self.generator = self.create_generator()
            self.discriminator = self.create_discriminator()
            self.gen_opt = Adam(learning_rate=2e-4, 
                                 beta_1=0.5)
            self.dis_opt = Adam(learning_rate=2e-4, 
                                 beta_1=0.5)
    
  12. 第 11 步 中实现的构造函数定义了要使用的损失函数(二元交叉熵)、lambda 值(用于 第 18 步),并实例化了生成器和判别器及其各自的优化器。我们的生成器是一个修改过的 U-Net,它是一个 U 形网络,由下采样和上采样块组成。现在,让我们创建一个静态方法来生成下采样块:

        @staticmethod
        def downsample(filters, size, batch_norm=True):
           initializer = tf.random_normal_initializer(0.0, 0.02)
            layers = Sequential()
            layers.add(Conv2D(filters=filters,
                              kernel_size=size,
                              strides=2,
                              padding='same',
    
                          kernel_initializer=initializer,
                              use_bias=False))
            if batch_norm:
                layers.add(BatchNormalization())
            layers.add(LeakyReLU())
            return layers
    
  13. 下采样块是一个卷积块,可选地进行批归一化,并激活 LeakyReLU()。现在,让我们实现一个静态方法来创建上采样块:

        @staticmethod
        def upsample(filters, size, dropout=False):
            init = tf.random_normal_initializer(0.0, 0.02)
            layers = Sequential()
            layers.add(Conv2DTranspose(filters=filters,
                                       kernel_size=size,
                                       strides=2,
                                       padding='same',
                                 kernel_initializer=init,
                                       use_bias=False))
            layers.add(BatchNormalization())
            if dropout:
                layers.add(Dropout(rate=0.5))
            layers.add(ReLU())
            return layers
    
  14. 上采样块是一个转置卷积,后面可选地跟随 dropout,并激活 ReLU()。现在,让我们使用这两个便捷方法来实现 U-Net 生成器:

        def create_generator(self, input_shape=(256, 256,3)):
            down_stack = [self.downsample(64,4,batch_norm=False)]
            for filters in (128, 256, 512, 512, 512, 512, 
                             512):
                down_block = self.downsample(filters, 4)
                down_stack.append(down_block)
    
  15. 在定义了下采样堆栈后,让我们对上采样层做同样的事情:

            up_stack = []
            for _ in range(3):
                up_block = self.upsample(512, 4,dropout=True)
                up_stack.append(up_block)
            for filters in (512, 256, 128, 64):
                up_block = self.upsample(filters, 4)
                up_stack.append(up_block)
    
  16. 将输入通过下采样和上采样堆栈,同时添加跳跃连接,以防止网络的深度妨碍其学习:

            inputs = Input(shape=input_shape)
            x = inputs
            skip_layers = []
            for down in down_stack:
                x = down(x)
                skip_layers.append(x)
            skip_layers = reversed(skip_layers[:-1])
            for up, skip_connection in zip(up_stack, 
                                           skip_layers):
                x = up(x)
                x = Concatenate()([x, skip_connection])
    
  17. 输出层是一个转置卷积,激活函数为 'tanh'

            init = tf.random_normal_initializer(0.0, 0.02)
            output = Conv2DTranspose(
                filters=self.output_channels,
                kernel_size=4,
                strides=2,
                padding='same',
                kernel_initializer=init,
                activation='tanh')(x)
            return Model(inputs, outputs=output)
    
  18. 定义一个方法来计算生成器的损失,正如 Pix2Pix 的作者所推荐的那样。注意 self._lambda 常量的使用:

        def generator_loss(self,
                           discriminator_generated_output,
                           generator_output,
                           target):
            gan_loss = self.loss(
                tf.ones_like(discriminator_generated_output),
                discriminator_generated_output)
            # MAE
            error = target - generator_output
            l1_loss = tf.reduce_mean(tf.abs(error))
            total_gen_loss = gan_loss + (self._lambda * 
                                          l1_loss)
            return total_gen_loss, gan_loss, l1_loss
    
  19. 本步骤中定义的判别器接收两张图像;输入图像和目标图像:

        def create_discriminator(self):
            input = Input(shape=(256, 256, 3))
            target = Input(shape=(256, 256, 3))
            x = Concatenate()([input, target])
            x = self.downsample(64, 4, False)(x)
            x = self.downsample(128, 4)(x)
            x = self.downsample(256, 4)(x)
            x = ZeroPadding2D()(x)
    
  20. 注意,最后几层是卷积层,而不是 Dense() 层。这是因为判别器一次处理的是图像的一个小块,并判断每个小块是真实的还是假的:

            init = tf.random_normal_initializer(0.0, 0.02)
            x = Conv2D(filters=512,
                       kernel_size=4,
                       strides=1,
                       kernel_initializer=init,
                       use_bias=False)(x)
            x = BatchNormalization()(x)
            x = LeakyReLU()(x)
            x = ZeroPadding2D()(x)
            output = Conv2D(filters=1,
                            kernel_size=4,
                            strides=1,
                            kernel_initializer=init)(x)
            return Model(inputs=[input, target], 
                        outputs=output)
    
  21. 定义判别器的损失:

        def discriminator_loss(self,
                               discriminator_real_output,
                             discriminator_generated_output):
            real_loss = self.loss(
                tf.ones_like(discriminator_real_output),
                discriminator_real_output)
            fake_loss = self.loss(
                tf.zeros_like(discriminator_generated_output),
                discriminator_generated_output)
            return real_loss + fake_loss
    
  22. 定义一个执行单个训练步骤的函数,命名为train_step(),该函数包括:将输入图像传入生成器,然后使用判别器对输入图像与原始目标图像配对,再对输入图像与生成器输出的假图像配对进行处理:

        @tf.function
        def train_step(self, input_image, target):
            with tf.GradientTape() as gen_tape, \
                    tf.GradientTape() as dis_tape:
                gen_output = self.generator(input_image,
                                            training=True)
                dis_real_output = self.discriminator(
                    [input_image, target], training=True)
                dis_gen_output = self.discriminator(
                    [input_image, gen_output], 
                            training=True)
    
  23. 接下来,计算损失值以及梯度:

                (gen_total_loss, gen_gan_loss,   
                   gen_l1_loss) = \
                    self.generator_loss(dis_gen_output,
                                        gen_output,
                                        target)
                dis_loss = \
                    self.discriminator_loss(dis_real_output,
    
                            dis_gen_output)
            gen_grads = gen_tape. \
                gradient(gen_total_loss,
                         self.generator.trainable_variables)
            dis_grads = dis_tape. \
                gradient(dis_loss,
                         self.discriminator.trainable_variables)
    
  24. 使用梯度通过相应的优化器更新模型:

            opt_args = zip(gen_grads,
                           self.generator.trainable_variables)
            self.gen_opt.apply_gradients(opt_args)
            opt_args = zip(dis_grads,
                           self.discriminator.trainable_variables)
            self.dis_opt.apply_gradients(opt_args)
    
  25. 实现fit()方法来训练整个架构。对于每一轮,我们将生成的图像保存到磁盘,以便通过视觉方式评估模型的性能:

        def fit(self, train, epochs, test):
            for epoch in tqdm.tqdm(range(epochs)):
                for example_input, example_target in 
                                  test.take(1):
                    generate_and_save_images(self.generator,
                                           example_input,
                                           example_target,
                                             epoch)
                for input_image, target in train:
                    self.train_step(input_image, target)
    
  26. 组装训练集和测试集数据的路径:

    dataset_path = (pathlib.Path.home() / '.keras' / 
                    'datasets' /'cityscapes')
    train_dataset_pattern = str(dataset_path / 'train' / 
                                 '*.jpg')
    test_dataset_pattern = str(dataset_path / 'val' / 
                               '*.jpg')
    
  27. 定义训练集和测试集数据:

    BUFFER_SIZE = 400
    BATCH_SIZE = 1
    train_ds = (tf.data.Dataset
                .list_files(train_dataset_pattern)
                .map(load_training_image,
                     num_parallel_calls=AUTOTUNE)
                .shuffle(BUFFER_SIZE)
                .batch(BATCH_SIZE))
    test_ds = (tf.data.Dataset
               .list_files(test_dataset_pattern)
               .map(load_test_image)
               .batch(BATCH_SIZE))
    
  28. 实例化Pix2Pix()并训练 150 轮:

    pix2pix = Pix2Pix()
    pix2pix.fit(train_ds, epochs=150, test=test_ds)
    

    这是第 1 轮生成的图像:

图 6.4 – 最初,生成器只会产生噪声

图 6.4 – 最初,生成器只会产生噪声

这是第 150 轮的结果:

图 6.5 – 在训练结束时,生成器能够产生合理的结果

图 6.5 – 在训练结束时,生成器能够产生合理的结果

当训练结束时,我们的 Pix2Pix 架构能够将分割后的图像转换为真实场景,如图 6.5所示,其中第一张是输入图像,第二张是目标图像,最右边的是生成的图像。

接下来我们将在下一部分连接这些点。

它是如何工作的…

在本示例中,我们实现了一个稍微复杂的架构,但它基于与所有 GAN 相同的思路。主要的区别是,这次判别器工作在图像块上,而不是整个图像。更具体地说,判别器一次查看原始图像和假图像的图像块,并判断这些图像块是否属于真实图像或合成图像。

由于图像到图像的转换是一种图像分割形式,我们的生成器是一个经过修改的 U-Net,U-Net 是一种首次用于生物医学图像分割的突破性 CNN 架构。

因为 Pix2Pix 是一个如此复杂且深度的网络,训练过程需要几个小时才能完成,但最终,我们在将分割后的城市景观内容转换为真实感预测方面取得了非常好的结果。令人印象深刻!

如果你想查看其他生成的图像以及生成器和判别器的图形表示,请查阅官方仓库:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch6/recipe3

另见

我建议你阅读Pix2Pix的原始论文,作者为 Phillip Isola、Jun-Yan Zhu、Tinghui Zhou 和 Alexei A. Efros,论文链接在此:arxiv.org/abs/1611.07004。我们使用了 U-Net 作为生成器,你可以在这里了解更多:arxiv.org/abs/1505.04597

使用 CycleGAN 翻译未配对的图像

使用 Pix2Pix 翻译图像的配方中,我们探索了如何将图像从一个领域转移到另一个领域。然而,最终这仍然是监督学习,需要输入和目标图像的配对,以便 Pix2Pix 学习正确的映射。如果我们能够绕过这个配对条件,让网络自己找出如何将一个领域的特征翻译到另一个领域,同时保持图像的一致性,那该多好?

好吧,这正是CycleGAN的作用,在这个配方中,我们将从头开始实现一个,将夏季拍摄的优胜美地国家公园的照片转换为冬季版本!

开始吧。

准备工作

在这个配方中,我们将使用OpenCVtqdmtensorflow-datasets

使用pip同时安装这些:

$> pip install opencv-contrib-python tqdm tensorflow-datasets

通过 TensorFlow 数据集,我们将访问cyclegan/summer2winter_yosemite数据集。

以下是该数据集的一些示例图像:

图 6.6 – 左:夏季的优胜美地;右:冬季的优胜美地

图 6.6 – 左:夏季的优胜美地;右:冬季的优胜美地

提示

CycleGAN 的实现与 Pix2Pix 非常相似。因此,我们不会详细解释其中的大部分内容。相反,我建议你先完成使用 Pix2Pix 翻译图像的配方,然后再来挑战这个。

如何操作…

执行以下步骤来完成这个配方:

  1. 导入必要的依赖项:

    import cv2
    import numpy as np
    import tensorflow as tf
    import tensorflow_datasets as tfds
    from tensorflow.keras.layers import *
    from tensorflow.keras.losses import BinaryCrossentropy
    from tensorflow.keras.models import *
    from tensorflow.keras.optimizers import Adam
    from tqdm import tqdm
    
  2. 定义tf.data.experimental.AUTOTUNE的别名:

    AUTOTUNE = tf.data.experimental.AUTOTUNE
    
  3. 定义一个函数来执行图像的随机裁剪:

    def random_crop(image):
        return tf.image.random_crop(image, size=(256, 256, 
                                                   3))
    
  4. 定义一个函数,将图像归一化到[-1, 1]的范围:

    def normalize(image):
        image = tf.cast(image, tf.float32)
        image = (image / 127.5) - 1
        return image
    
  5. 定义一个函数,执行图像的随机抖动:

    def random_jitter(image):
        method = tf.image.ResizeMethod.NEAREST_NEIGHBOR
        image = tf.image.resize(image, (286, 286), 
                                method=method)
        image = random_crop(image)
        image = tf.image.random_flip_left_right(image)
        return image
    
  6. 定义一个函数来预处理并增强训练图像:

    def preprocess_training_image(image, _):
        image = random_jitter(image)
        image = normalize(image)
        return image
    
  7. 定义一个函数来预处理测试图像:

    def preprocess_test_image(image, _):
        image = normalize(image)
        return image
    
  8. 定义一个函数,使用生成器模型生成并保存图像。生成的图像将是输入图像与预测结果的拼接:

    def generate_images(model, test_input, epoch):
        prediction = model(test_input)
        image = np.hstack([test_input[0], prediction[0]])
        image *= 0.5
        image += 0.5
        image *= 255.0
        image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
        cv2.imwrite(f'{epoch + 1}.jpg', image)
    
  9. 定义一个自定义实例归一化层,从构造函数开始:

    class InstanceNormalization(Layer):
        def __init__(self, epsilon=1e-5):
            super(InstanceNormalization, self).__init__()
            self.epsilon = epsilon
    
  10. 现在,定义build()方法,它创建InstanceNormalization()类的内部组件:

        def build(self, input_shape):
            init = tf.random_normal_initializer(1.0, 0.02)
            self.scale = self.add_weight(name='scale',
                                   shape=input_shape[-1:],
                                         initializer=init,
                                         trainable=True)
            self.offset = self.add_weight(name='offset',
                                   shape=input_shape[-1:],
                                      initializer='zeros',
                                          trainable=True)
    
  11. 创建call()方法,该方法实现实例归一化输入张量x的逻辑:

        def call(self, x):
            mean, variance = tf.nn.moments(x,
                                           axes=(1, 2),
                                           keepdims=True)
            inv = tf.math.rsqrt(variance + self.epsilon)
            normalized = (x - mean) * inv
            return self.scale * normalized + self.offset
    
  12. 定义一个类来封装 CycleGAN 的实现。首先是构造函数:

    class CycleGAN(object):
        def __init__(self, output_channels=3, 
                     lambda_value=10):
            self.output_channels = output_channels
            self._lambda = lambda_value
            self.loss = BinaryCrossentropy(from_logits=True)
            self.gen_g = self.create_generator()
            self.gen_f = self.create_generator()
            self.dis_x = self.create_discriminator()
            self.dis_y = self.create_discriminator()
            self.gen_g_opt = Adam(learning_rate=2e-4, 
                                   beta_1=0.5)
            self.gen_f_opt = Adam(learning_rate=2e-4, 
                                  beta_1=0.5)
            self.dis_x_opt = Adam(learning_rate=2e-4, 
                                  beta_1=0.5)
            self.dis_y_opt = Adam(learning_rate=2e-4, 
                                  beta_1=0.5)
    

    与 Pix2Pix 的主要区别在于我们有两个生成器(gen_ggen_f)和两个鉴别器(dis_xdis_y)。gen_g学习如何将图像 X 转换为图像 Y,而gen_f学习如何将图像 Y 转换为图像 X。类似地,dis_x学习区分真实的图像 X 和gen_f生成的图像,而dis_y学习区分真实的图像 Y 和gen_g生成的图像。

  13. 现在,让我们创建一个静态方法来生成下采样块(这与上一个示例相同,只是这次我们使用实例化而不是批处理归一化):

        @staticmethod
        def downsample(filters, size, norm=True):
            initializer = tf.random_normal_initializer(0.0, 0.02)
            layers = Sequential()
            layers.add(Conv2D(filters=filters,
                              kernel_size=size,
                              strides=2,
                              padding='same',
    
                         kernel_initializer=initializer,
                              use_bias=False))
            if norm:
                layers.add(InstanceNormalization())
            layers.add(LeakyReLU())
            return layers
    
  14. 现在,定义一个静态方法来生成上采样块(这与上一个示例相同,只是这次我们使用实例化而不是批处理归一化):

        @staticmethod
        def upsample(filters, size, dropout=False):
            init = tf.random_normal_initializer(0.0, 0.02)
            layers = Sequential()
            layers.add(Conv2DTranspose(filters=filters,
                                       kernel_size=size,
                                       strides=2,
                                       padding='same',
    
                                 kernel_initializer=init,
                                       use_bias=False))
            layers.add(InstanceNormalization())
            if dropout:
                layers.add(Dropout(rate=0.5))
            layers.add(ReLU())
            return layers
    
  15. 定义一个方法来构建生成器。首先创建下采样层:

        def create_generator(self):
            down_stack = [
                self.downsample(64, 4, norm=False),
                self.downsample(128, 4),
                self.downsample(256, 4)]
            for _ in range(5):
                down_block = self.downsample(512, 4)
                down_stack.append(down_block)
    
  16. 现在,创建上采样层:

            for _ in range(3):
                up_block = self.upsample(512, 4, 
                                       dropout=True)
                up_stack.append(up_block)
            for filters in (512, 256, 128, 64):
                up_block = self.upsample(filters, 4)
                up_stack.append(up_block)
    
  17. 将输入通过下采样和上采样层。添加跳跃连接以避免梯度消失问题:

    inputs = Input(shape=(None, None, 3))
            x = inputs
            skips = []
            for down in down_stack:
                x = down(x)
                skips.append(x)
            skips = reversed(skips[:-1])
            for up, skip in zip(up_stack, skips):
                x = up(x)
                x = Concatenate()([x, skip])
    
  18. 输出层是一个激活函数为'tanh'的转置卷积层:

            init = tf.random_normal_initializer(0.0, 0.02)
            output = Conv2DTranspose(
                filters=self.output_channels,
                kernel_size=4,
                strides=2,
                padding='same',
                kernel_initializer=init,
                activation='tanh')(x)
            return Model(inputs, outputs=output)
    
  19. 定义一个方法来计算生成器损失:

        def generator_loss(self, generated):
            return self.loss(tf.ones_like(generated), 
                             generated)
    
  20. 定义一个方法来创建鉴别器:

        def create_discriminator(self):
            input = Input(shape=(None, None, 3))
            x = input
            x = self.downsample(64, 4, False)(x)
            x = self.downsample(128, 4)(x)
            x = self.downsample(256, 4)(x)
            x = ZeroPadding2D()(x)
    
  21. 添加最后几层卷积层:

            init = tf.random_normal_initializer(0.0, 0.02)
            x = Conv2D(filters=512,
                       kernel_size=4,
                       strides=1,
                       kernel_initializer=init,
                       use_bias=False)(x)
            x = InstanceNormalization()(x)
            x = LeakyReLU()(x)
            x = ZeroPadding2D()(x)
            output = Conv2D(filters=1,
                            kernel_size=4,
                            strides=1,
                            kernel_initializer=init)(x)
            return Model(inputs=input, outputs=output)
    
  22. 定义一个方法来计算鉴别器损失:

        def discriminator_loss(self, real, generated):
            real_loss = self.loss(tf.ones_like(real), 
                                         real)
            generated_loss = 
                  self.loss(tf.zeros_like(generated),
                                       generated)
            total_discriminator_loss = real_loss + generated_loss
            return total_discriminator_loss * 0.5
    
  23. 定义一个方法来计算真实图像和循环图像之间的损失。这个损失用于量化循环一致性,即如果你将图像 X 翻译为 Y,然后再将 Y 翻译为 X,结果应该是 X,或者接近 X:

        def calculate_cycle_loss(self, real_image, 
                                 cycled_image):
            error = real_image - cycled_image
            loss1 = tf.reduce_mean(tf.abs(error))
            return self._lambda * loss1
    
  24. 定义一个方法来计算身份损失。这个损失确保如果你将图像 Y 通过gen_g传递,我们应该得到真实的图像 Y 或接近它(gen_f也同样适用):

        def identity_loss(self, real_image, same_image):
            error = real_image - same_image
            loss = tf.reduce_mean(tf.abs(error))
            return self._lambda * 0.5 * loss
    
  25. 定义一个方法来执行单步训练。它接收来自不同领域的图像 X 和 Y。然后,使用gen_g将 X 转换为 Y,并使用gen_f将 Y 转换为 X:

        @tf.function
        def train_step(self, real_x, real_y):
            with tf.GradientTape(persistent=True) as tape:
                fake_y = self.gen_g(real_x, training=True)
                cycled_x = self.gen_f(fake_y, 
                                     training=True)
                fake_x = self.gen_f(real_y, training=True)
                cycled_y = self.gen_g(fake_x, 
                                       training=True)
    
  26. 现在,将 X 通过gen_f传递,将 Y 通过gen_y传递,以便稍后计算身份损失:

                same_x = self.gen_f(real_x, training=True)
                same_y = self.gen_g(real_y, training=True)
    
  27. 将真实的 X 和伪造的 X 传递给dis_x,将真实的 Y 以及生成的 Y 传递给dis_y

                dis_real_x = self.dis_x(real_x, 
                                        training=True)
                dis_real_y = self.dis_y(real_y, 
                                        training=True)
                dis_fake_x = self.dis_x(fake_x,training=True)
                dis_fake_y = self.dis_y(fake_y, 
                                       training=True)
    
  28. 计算生成器的损失:

                gen_g_loss = self.generator_loss(dis_fake_y)
                gen_f_loss = self.generator_loss(dis_fake_x)
    
  29. 计算循环损失:

                cycle_x_loss = \
                    self.calculate_cycle_loss(real_x, 
                                              cycled_x)
                cycle_y_loss = \
                    self.calculate_cycle_loss(real_y, 
                                             cycled_y)
                total_cycle_loss = cycle_x_loss + 
                                       cycle_y_loss
    
  30. 计算身份损失和总生成器 G 的损失:

                identity_y_loss = \
                    self.identity_loss(real_y, same_y)
                total_generator_g_loss = (gen_g_loss +
                                          total_cycle_loss +
                                          identity_y_loss)
    
  31. 对生成器 F 重复此过程:

                identity_x_loss = \
                    self.identity_loss(real_x, same_x)
                total_generator_f_loss = (gen_f_loss +
                                          total_cycle_loss +
                                          identity_x_loss)
    
  32. 计算鉴别器的损失:

             dis_x_loss = \
               self.discriminator_loss(dis_real_x,dis_fake_x)
             dis_y_loss = \
               self.discriminator_loss(dis_real_y,dis_fake_y)
    
  33. 计算生成器的梯度:

            gen_g_grads = tape.gradient(
                total_generator_g_loss,
                self.gen_g.trainable_variables)
            gen_f_grads = tape.gradient(
                total_generator_f_loss,
                self.gen_f.trainable_variables)
    
  34. 计算鉴别器的梯度:

            dis_x_grads = tape.gradient(
                dis_x_loss,
                self.dis_x.trainable_variables)
            dis_y_grads = tape.gradient(
                dis_y_loss,
                self.dis_y.trainable_variables)
    
  35. 使用相应的优化器将梯度应用到每个生成器:

            gen_g_opt_params = zip(gen_g_grads,
                             self.gen_g.trainable_variables)
            self.gen_g_opt.apply_gradients(gen_g_opt_params)
            gen_f_opt_params = zip(gen_f_grads,
                                   self.gen_f.trainable_variables)
            self.gen_f_opt.apply_gradients(gen_f_opt_params)
    
  36. 使用相应的优化器将梯度应用到每个鉴别器:

            dis_x_opt_params = zip(dis_x_grads,
                              self.dis_x.trainable_variables)
            self.dis_x_opt.apply_gradients(dis_x_opt_params)
            dis_y_opt_params = zip(dis_y_grads,
                              self.dis_y.trainable_variables)
            self.dis_y_opt.apply_gradients(dis_y_opt_params)
    
  37. 定义一个方法来拟合整个架构。它将在每个 epoch 之后将生成器 G 生成的图像保存到磁盘:

        def fit(self, train, epochs, test):
            for epoch in tqdm(range(epochs)):
                for image_x, image_y in train:
                    self.train_step(image_x, image_y)
                test_image = next(iter(test))
                generate_images(self.gen_g, test_image, 
                                   epoch)
    
  38. 加载数据集:

    dataset, _ = tfds.load('cycle_gan/summer2winter_  yosemite',
                           with_info=True,
                           as_supervised=True)
    
  39. 解包训练和测试集:

    train_summer = dataset['trainA']
    train_winter = dataset['trainB']
    test_summer = dataset['testA']
    test_winter = dataset['testB']
    
  40. 定义训练集的数据处理管道:

    BUFFER_SIZE = 400
    BATCH_SIZE = 1
    train_summer = (train_summer
                    .map(preprocess_training_image,
                         num_parallel_calls=AUTOTUNE)
                    .cache()
                    .shuffle(BUFFER_SIZE)
                    .batch(BATCH_SIZE))
    train_winter = (train_winter
                    .map(preprocess_training_image,
                         num_parallel_calls=AUTOTUNE)
                    .cache()
                    .shuffle(BUFFER_SIZE)
                    .batch(BATCH_SIZE))
    
  41. 定义测试集的数据处理管道:

    test_summer = (test_summer
                   .map(preprocess_test_image,
                        num_parallel_calls=AUTOTUNE)
                   .cache()
                   .shuffle(BUFFER_SIZE)
                   .batch(BATCH_SIZE))
    test_winter = (test_winter
                   .map(preprocess_test_image,
                        num_parallel_calls=AUTOTUNE)
                   .cache()
                   .shuffle(BUFFER_SIZE)
                   .batch(BATCH_SIZE))
    
  42. 创建一个CycleGAN()实例并训练 40 个 epoch:

    cycle_gan = CycleGAN()
    train_ds = tf.data.Dataset.zip((train_summer, 
                                    train_winter))
    cycle_gan.fit(train=train_ds,
                  epochs=40,
                  test=test_summer)
    

    在第 1 个 epoch 时,我们会注意到网络尚未学到很多东西:

图 6.7 – 左:夏季的原始图像;右:翻译后的图像(冬季)

图 6.7 – 左:夏季的原始图像;右:翻译后的图像(冬季)

然而,在第 40 个周期时,结果更加令人鼓舞:

图 6.8 – 左:夏季的原始图像;右:翻译后的图像(冬季)

图 6.8 – 左:夏季的原始图像;右:翻译后的图像(冬季)

如前图所示,我们的 CycleGAN() 在某些区域(如小道和树木)添加了更多的白色,使得翻译后的图像看起来像是冬季拍摄的。当然,训练更多的周期可能会带来更好的结果,我鼓励你这么做,以加深你对 CycleGAN 的理解!

它是如何工作的……

在本教程中,我们了解到,CycleGAN 的工作方式与 Pix2Pix 非常相似。然而,最大优势是 CycleGAN 不需要配对图像数据集就能实现目标。相反,它依赖于两组生成器和判别器,实际上,这些生成器和判别器形成了一个学习循环,因此得名。

特别地,CycleGAN 的工作方式如下:

  • 生成器 G 必须学习从图像 X 到图像 Y 的映射。

  • 生成器 F 必须学习从图像 Y 到图像 X 的映射。

  • 判别器 D(X) 必须区分真实图像 X 和由 G 生成的假图像。

  • 判别器 D(Y) 必须区分真实图像 Y 和由 F 生成的假图像。

有两个条件确保翻译在两个领域中保持含义(就像我们从英语翻译成西班牙语时,希望保留词语的含义,反之亦然):

  • 循环一致性:从 X 到 Y,再从 Y 到 X 应该产生原始的 X 或与 X 非常相似的东西。Y 也是如此。

  • 身份一致性:将 X 输入 G 应该产生相同的 X 或与 X 非常相似的东西。Y 也是如此。

使用这四个组件,CycleGAN 试图在翻译中保持循环和身份一致性,从而在无需监督、配对数据的情况下生成非常令人满意的结果。

另见

你可以在这里阅读关于 CycleGAN 的原始论文:arxiv.org/abs/1703.10593。此外,这里有一个非常有趣的讨论,帮助你理解实例归一化与批归一化之间的区别:intellipaat.com/community/1869/instance-normalisation-vs-batch-normalisation

使用快速梯度符号方法实现对抗性攻击

我们通常认为高度准确的深度神经网络是强大的模型,但由 GAN 之父伊恩·古德费洛(Ian Goodfellow)提出的快速梯度符号方法FGSM)却证明了相反的观点。在这个示例中,我们将对一个预训练模型执行 FGSM 攻击,看看如何通过引入看似无法察觉的变化,完全欺骗一个网络。

准备工作

让我们用pip安装OpenCV

我们将使用它来使用 FGSM 方法保存扰动后的图像:

$> pip install opencv-contrib-python

让我们开始吧。

如何操作

完成以下步骤后,您将成功执行一次对抗性攻击:

  1. 导入依赖项:

    import cv2
    import tensorflow as tf
    from tensorflow.keras.applications.nasnet import *
    from tensorflow.keras.losses import CategoricalCrossentropy
    
  2. 定义一个函数来预处理图像,这包括调整图像大小并应用与我们将要使用的预训练网络相同的处理(在这个例子中是NASNetMobile):

    def preprocess(image, target_shape):
        image = tf.cast(image, tf.float32)
        image = tf.image.resize(image, target_shape)
        image = preprocess_input(image)
        image = image[None, :, :, :]
        return image
    
  3. 定义一个函数来根据一组概率获取人类可读的图像:

    def get_imagenet_label(probabilities):
        return decode_predictions(probabilities, top=1)[0][0]
    
  4. 定义一个函数来保存图像。这个函数将使用预训练模型来获取正确的标签,并将其作为图像文件名的一部分,文件名中还包含预测的置信度百分比。在将图像存储到磁盘之前,它会确保图像在预期的[0, 255]范围内,并且处于 BGR 空间中,这是 OpenCV 使用的颜色空间:

    def save_image(image, model, description):
        prediction = model.predict(image)
        _, label, conf = get_imagenet_label(prediction)
        image = image.numpy()[0] * 0.5 + 0.5
        image = (image * 255).astype('uint8')
        image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
        conf *= 100
        img_name = f'{description}, {label} ({conf:.2f}%).jpg'
        cv2.imwrite(img_name, image)
    
  5. 定义一个函数来创建对抗性模式,该模式将在后续用于执行实际的 FGSM 攻击:

    def generate_adv_pattern(model,
                             input_image,
                             input_label,
                             loss_function):
        with tf.GradientTape() as tape:
            tape.watch(input_image)
            prediction = model(input_image)
            loss = loss_function(input_label, prediction)
        gradient = tape.gradient(loss, input_image)
        signed_gradient = tf.sign(gradient)
        return signed_gradient
    

    这个模式非常简单:它由一个张量组成,其中每个元素表示梯度的符号。更具体地说,signed_gradient将包含一个-1表示梯度值小于01表示梯度值大于0,而当梯度为0时,则是0

  6. 实例化预训练的NASNetMobile()模型并冻结其权重:

    pretrained_model = NASNetMobile(include_top=True,
                                    weights='imagenet')
    pretrained_model.trainable = False
    
  7. 加载测试图像并通过网络传递:

    image = tf.io.read_file('dog.jpg')
    image = tf.image.decode_jpeg(image)
    image = preprocess(image, pretrained_model.input.shape[1:-1])
    image_probabilities = pretrained_model.predict(image)    
    
  8. 对原始图像的地面真值标签进行独热编码,并用它生成对抗性模式:

    cce_loss = CategoricalCrossentropy()
    pug_index = 254
    label = tf.one_hot(pug_index, image_probabilities.shape[-1])
    label = tf.reshape(label, (1, image_probabilities.shape[-1]))
    disturbances = generate_adv_pattern(pretrained_model,
                                        image,
                                        label,
                                        cce_loss)
    
  9. 执行一系列对抗性攻击,使用逐渐增大但仍然较小的epsilon值,这些值将在梯度方向上应用,利用disturbances中的模式:

    for epsilon in [0, 0.005, 0.01, 0.1, 0.15, 0.2]:
        corrupted_image = image + epsilon * disturbances
        corrupted_image = tf.clip_by_value(corrupted_image, -1, 1)
        save_image(corrupted_image,
                   pretrained_model,
                   f'Epsilon = {epsilon:.3f}')
    

    对于 epsilon = 0(无攻击),图像如下,标签为pug,置信度为 80%:

图 6.9 – 原始图像。标签:pug(80.23% 置信度)

](https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/tf20-cv-cb/img/B14768_06_009.jpg)

图 6.9 – 原始图像。标签:pug(80.23% 置信度)

当 epsilon = 0.005(一个非常小的扰动)时,标签变为Brabancon_griffon,置信度为 43.03%:

图 6.10 – 在梯度方向上应用 epsilon = 0.005。标签:Brabancon_griffon(43.03% 置信度)

图 6.10 – 在梯度方向上应用 epsilon = 0.005。标签:Brabancon_griffon(43.03% 置信度)

如前图所示,像素值的微小变化会导致网络产生截然不同的响应。然而,随着ε(epsilon)值增大的情况变得更糟。有关完整的结果列表,请参阅github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch6/recipe5

它是如何工作的……

在这个食谱中,我们实现了一个基于 Ian Goodfellow 提出的 FGSM(快速梯度符号法)的简单攻击方法,主要通过确定每个位置的梯度方向(符号)并利用该信息创建对抗性图案。其基本原理是该技术在每个像素值上最大化损失。

接下来,我们使用此图案来对图像中的每个像素进行加减微小的扰动,然后将其传递给网络。

尽管这些变化通常人眼难以察觉,但它们能够完全扰乱网络,导致荒谬的预测,正如在本食谱的最后一步所展示的那样。

另请参见

幸运的是,针对这种类型攻击(以及更复杂攻击)的防御措施已经出现。你可以阅读一篇相当有趣的对抗攻击与防御的综述,内容在这里:arxiv.org/abs/1810.00069

第七章:第七章:使用 CNN 和 RNN 给图像加上字幕

赋予神经网络描述视觉场景的能力以人类可读的方式,必定是深度学习中最有趣但也最具挑战性的应用之一。主要困难在于,这个问题结合了人工智能的两个主要子领域:计算机视觉CV)和自然语言处理NLP)。

大多数图像字幕网络的架构使用卷积神经网络CNN)来将图像编码为数字格式,以便解码器消费,解码器通常是递归神经网络RNN)。这是一种专门用于学习序列数据(如时间序列、视频和文本)的网络。

正如我们在这一章中将看到的,构建具有这些能力的系统的挑战从准备数据开始,我们将在第一个实例中讨论这一点。然后,我们将从头开始实现一个图像字幕解决方案。在第三个实例中,我们将使用这个模型为我们自己的图片生成字幕。最后,在第四个实例中,我们将学习如何在我们的架构中包含注意力机制,以便我们可以理解网络在生成输出字幕中每个单词时看到图像的哪些部分。

相当有趣,你同意吗?

具体来说,在本章中我们将涵盖以下实例:

  • 实现可重复使用的图像字幕特征提取器

  • 实现图像字幕网络

  • 为您自己的照片生成字幕

  • 在 COCO 上实现带注意力的图像字幕网络

  • 让我们开始吧!

技术要求

图像字幕是一个需要大量内存、存储和计算资源的问题。我建议您使用像 AWS 或 FloydHub 这样的云解决方案来运行本章中的实例,除非您有足够强大的硬件。如预期的那样,GPU 对于完成本章中的实例至关重要。在每个实例的“准备就绪”部分,您将找到所需准备的内容。本章的代码在此处可用:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch7

点击以下链接查看“代码实战”视频:

bit.ly/3qmpVme

实现可重复使用的图像字幕特征提取器

创建基于深度学习的图像字幕解决方案的第一步是将数据转换为可以被某些网络使用的格式。这意味着我们必须将图像编码为向量或张量,将文本编码为嵌入,即句子的向量表示。

在本食谱中,我们将实现一个可自定义和可重用的组件,允许我们提前预处理实现图像标题生成器所需的数据,从而节省后续过程中大量时间。

让我们开始吧!

准备就绪

我们需要的依赖是tqdm(用于显示漂亮的进度条)和Pillow(用于使用 TensorFlow 的内置函数加载和处理图像):

$> pip install Pillow tqdm

我们将使用Flickr8k数据集,该数据集位于~/.keras/datasets/flickr8k文件夹中。

这里是一些示例图像:

图 7.1 – 来自 Flickr8k 的示例图像

图 7.1 – 来自 Flickr8k 的示例图像

有了这些,我们就可以开始了!

如何实现……

按照以下步骤创建一个可重用的特征提取器,用于图像标题问题:

  1. 导入所有必要的依赖项:

    import glob
    import os
    import pathlib
    import pickle
    from string import punctuation
    import numpy as np
    import tqdm
    from tensorflow.keras.applications.vgg16 import *
    from tensorflow.keras.layers import *
    from tensorflow.keras.preprocessing.image import *
    from tensorflow.keras.preprocessing.sequence import \
        pad_sequences
    from tensorflow.keras.preprocessing.text import Tokenizer
    from tensorflow.keras.utils import to_categorical
    from tqdm import tqdm
    
  2. 定义ImageCaptionFeatureExtractor类及其构造函数:

    class ImageCaptionFeatureExtractor(object):
        def __init__(self,
                     output_path,
                     start_token='beginsequence',
                     end_token='endsequence',
                     feature_extractor=None,
                     input_shape=(224, 224, 3)):
    
  3. 接下来,我们必须接收输出存储路径,以及我们将用于分隔文本序列起始和结束的标记。我们还必须将特征提取器的输入形状作为参数。接下来,让我们将这些值存储为成员:

            self.input_shape = input_shape
            if feature_extractor is None:
                input = Input(shape=input_shape)
                self.feature_extractor = VGG16(input_ 
                                         tensor=input,
                                       weights='imagenet',
                                       include_top=False)
            else:
                self.feature_extractor = feature_extractor
            self.output_path = output_path
            self.start_token = start_token
            self.end_token = end_token
            self.tokenizer = Tokenizer()
            self.max_seq_length = None
    
  4. 如果没有接收到任何feature_extractor,我们将默认使用VGG16。接下来,定义一个公共方法,该方法根据图像路径提取图像的特征:

        def extract_image_features(self, image_path):
            image = load_img(image_path,
                           target_size=self.input_shape[:2])
            image = img_to_array(image)
            image = np.expand_dims(image, axis=0)
            image = preprocess_input(image)
            return self.feature_extractor.predict(image)[0]
    
  5. 为了清理标题,我们必须去除所有标点符号和单个字母的单词(如a)。_clean_captions()方法执行了这个任务,并且还添加了特殊标记,也就是self.start_tokenself.end_token

        def _clean_captions(self, captions):
            def remove_punctuation(word):
                translation = str.maketrans('', '',
                                            punctuation)
                return word.translate(translation)
            def is_valid_word(word):
                return len(word) > 1 and word.isalpha()
            cleaned_captions = []
            for caption in captions:
                caption = caption.lower().split(' ')
                caption = map(remove_punctuation, caption)
                caption = filter(is_valid_word, caption)
                cleaned_caption = f'{self.start_token} ' \
                                  f'{“ “.join(caption)} ' \
                                  f'{self.end_token}'
                cleaned_captions.append(cleaned_caption)
            return cleaned_captions
    
  6. 我们还需要计算最长标题的长度,可以通过_get_max_seq_length()方法来实现。方法定义如下:

        def _get_max_seq_length(self, captions):
            max_sequence_length = -1
            for caption in captions:
                caption_length = len(caption.split(' '))
                max_sequence_length = 
                                max(max_sequence_length,
                                          caption_length)
            return max_sequence_length
    
  7. 定义一个公共方法extract_features(),它接收一个包含图像路径和标题的列表,并利用这些数据从图像和文本序列中提取特征:

        def extract_features(self, images_path, captions):
            assert len(images_path) == len(captions)
    
  8. 请注意,两个列表必须具有相同的大小。接下来的步骤是清理标题,计算最大序列长度,并为所有标题适配一个分词器:

            captions = self._clean_captions(captions)
            self.max_seq_length=self._get_max_seq_ 
                                       length(captions) 
            self.tokenizer.fit_on_texts(captions)
    
  9. 我们将遍历每一对图像路径和标题,从图像中提取特征。然后,我们将在data_mappingdict中保存一个条目,将图像 ID(存在于image_path中)与相应的视觉特征和清理后的标题相关联:

            data_mapping = {}
            print('\nExtracting features...')
            for i in tqdm(range(len(images_path))):
                image_path = images_path[i]
                caption = captions[i]
             feats = self.extract_image_features(image_ path)
                image_id = image_path.split(os.path.sep)[-1]
                image_id = image_id.split('.')[0]
                data_mapping[image_id] = {
                    'features': feats,
                    'caption': caption
                }
    
  10. 我们将把这个data_mapping保存到磁盘,以 pickle 格式存储:

            out_path = f'{self.output_path}/data_mapping.pickle'
            with open(out_path, 'wb') as f:
                pickle.dump(data_mapping, f, protocol=4)
    
  11. 我们将通过创建和存储将在未来输入图像标题网络的序列来完成此方法:

            self._create_sequences(data_mapping)
    
  12. 以下方法创建了用于训练图像标题模型的输入和输出序列(详细说明请见如何实现……部分)。我们将从确定输出类别数开始,这个数值是词汇大小加一(以便考虑超出词汇表的标记)。我们还必须定义存储序列的列表:

        def _create_sequences(self, mapping):
            num_classes = len(self.tokenizer.word_index) + 1
            in_feats = []
            in_seqs = []
            out_seqs = []
    
  13. 接下来,我们将迭代每个特征-标题对。我们将把标题从字符串转换为表示句子中单词的数字序列:

            print('\nCreating sequences...')
            for _, data in tqdm(mapping.items()):
                feature = data['features']
                caption = data['caption']
                seq = self.tokenizer.texts_to_
                           sequences([caption])
                seq = seq[0]
    
  14. 接下来,我们将生成与标题中单词数量相同的输入序列。每个输入序列将用于生成序列中的下一个单词。因此,对于给定的索引i,输入序列将是到i-1的所有元素,而相应的输出序列或标签将是在i处的独热编码元素(即下一个单词)。为了确保所有输入序列的长度相同,我们必须对它们进行填充:

                for i in range(1, len(seq)):
                    input_seq = seq[:i]
                    input_seq, = 
                       pad_sequences([input_seq],
    
                         self.max_seq_length)
                    out_seq = seq[i]
                    out_seq = to_categorical([out_seq],
    
                                           num_classes)[0]
    
  15. 然后,我们将视觉特征向量、输入序列和输出序列添加到相应的列表中:

                    in_feats.append(feature)
                    in_seqs.append(input_seq)
                    out_seqs.append(out_seq)
    
  16. 最后,我们必须将序列以 pickle 格式写入磁盘:

            file_paths = [
                f'{self.output_path}/input_features.pickle',
                f'{self.output_path}/input_sequences.pickle',
                f'{self.output_path}/output_sequences.
                                                     pickle']
            sequences = [in_feats,
                         in_seqs,
                         out_seqs]
            for path, seq in zip(file_paths, sequences):
                with open(path, 'wb') as f:
                    pickle.dump(np.array(seq), f, 
                                protocol=4)
    
  17. 让我们定义Flickr8k图像和标题的路径:

    BASE_PATH = (pathlib.Path.home() / '.keras' / 'datasets'       
                                          /'flickr8k')
    IMAGES_PATH = str(BASE_PATH / 'Images')
    CAPTIONS_PATH = str(BASE_PATH / 'captions.txt')
    
  18. 创建我们刚刚实现的特征提取器类的实例:

    extractor = ImageCaptionFeatureExtractor(output_path='.')
    
  19. 列出Flickr8k数据集中的所有图像文件:

    image_paths = list(glob.glob(f'{IMAGES_PATH}/*.jpg'))
    
  20. 读取标题文件的内容:

    with open(CAPTIONS_PATH, 'r') as f:
        text = f.read()
        lines = text.split('\n')
    
  21. 现在,我们必须创建一个映射,将每个图像与多个标题关联起来。键是图像 ID,而值是与该图像相关的所有标题的列表:

    mapping = {}
    for line in lines:
        if '.jpg' not in line:
            continue
        tokens = line.split(',', maxsplit=1)
        if len(line) < 2:
            continue
        image_id, image_caption = tokens
        image_id = image_id.split('.')[0]
        captions_per_image = mapping.get(image_id, [])
        captions_per_image.append(image_caption)
        mapping[image_id] = captions_per_image
    
  22. 我们将仅保留每个图像的一个标题:

    captions = []
    for image_path in image_paths:
        image_id = image_path.split('/')[-1].split('.')[0]
        captions.append(mapping[image_id][0])
    
  23. 最后,我们必须使用我们的提取器生成数据映射和相应的输入序列:

    extractor.extract_features(image_paths, captions)
    

    这个过程可能需要一些时间。几分钟后,我们应该在输出路径中看到以下文件:

    data_mapping.pickle     input_features.pickle   input_sequences.pickle  output_sequences.pickle
    

接下来的部分将详细介绍这一切是如何工作的。

工作原理如下...

在这个示例中,我们学到了创建良好的图像字幕系统的关键之一是将数据放入适当的格式中。这使得网络能够学习如何用文本描述视觉场景中发生的事情。

有许多方法可以构建图像字幕问题,但最流行和有效的方法是使用每个单词来生成标题中的下一个单词。这样,我们将逐词构造句子,通过每个中间输出作为下一个周期的输入传递。 (这就是RNNs的工作原理。要了解更多信息,请参阅参考部分。)

你可能想知道如何将视觉信息传递给网络。这就是特征提取步骤至关重要的地方,因为我们将数据集中的每个图像转换为一个数值向量,该向量总结了每张图片中的空间信息。然后,在训练网络时,我们通过每个输入序列传递相同的特征向量。这样,网络将学会将标题中的所有单词与同一图像关联起来。

如果我们不小心,可能会陷入无限循环的单词生成中。我们如何防止这种情况发生?通过使用一个特殊的标记来信号化序列的结束(这意味着网络在遇到这样的标记时应停止生成单词)。在我们的情况下,默认的标记是endsequence

一个类似的问题是如何启动一个序列。我们应该使用哪个词?在这种情况下,我们也必须使用一个特殊的标记(我们的默认值是beginsequence)。这个标记充当一个种子,网络将基于它开始生成字幕。

这一切现在听起来可能有点令人困惑,这是因为我们只专注于数据预处理阶段。在本章的剩余食谱中,我们将利用在这里所做的工作来训练许多不同的图像字幕生成器,一切都会变得明了!

另请参见

这是一个很好的关于RNNs如何工作的解释:www.youtube.com/watch?v=UNmqTiOnRfg

实现图像字幕生成网络

一个图像字幕生成架构由编码器和解码器组成。编码器是一个CNN(通常是一个预训练的模型),它将输入图像转换为数值向量。然后,这些向量与文本序列一起传递给解码器,解码器是一个RNN,它将基于这些值学习如何逐步生成对应字幕中的每个单词。

在这个食谱中,我们将实现一个已在Flickr8k数据集上训练的图像字幕生成器。我们将利用在实现可重用的图像字幕特征提取器食谱中实现的特征提取器。

我们开始吧,好吗?

准备工作

在这个食谱中,我们将使用的外部依赖是Pillownltktqdm。你可以通过以下命令一次性安装它们:

$> pip install Pillow nltk tqdm

我们将使用Flickr8k数据集,您可以从~/.keras/datasets/flickr8k目录中获取它。

以下是一些来自Flickr8k数据集的示例图像:

图 7.2 – 来自 Flickr8k 的示例图像

图 7.2 – 来自 Flickr8k 的示例图像

让我们进入下一部分,开始本食谱的实现。

如何实现……

按照以下步骤实现基于深度学习的图像字幕生成系统:

  1. 首先,我们必须导入所有必需的包:

    import glob
    import pathlib
    import pickle
    import numpy as np
    from nltk.translate.bleu_score import corpus_bleu
    from sklearn.model_selection import train_test_split
    from tensorflow.keras.applications.vgg16 import *
    from tensorflow.keras.callbacks import ModelCheckpoint
    from tensorflow.keras.layers import *
    from tensorflow.keras.models import *
    from tensorflow.keras.preprocessing.sequence import \
        pad_sequences
    from ch7.recipe1.extractor import ImageCaptionFeatureExtractor
    
  2. 定义图像和字幕的路径,以及输出路径,这将是我们存储在本食谱中创建的工件的位置:

    BASE_PATH = (pathlib.Path.home() / '.keras' / 'datasets'     
                 /'flickr8k')
    IMAGES_PATH = str(BASE_PATH / 'Images')
    CAPTIONS_PATH = str(BASE_PATH / 'captions.txt')
    OUTPUT_PATH = '.'
    
  3. 定义一个函数,该函数将加载图像路径及其对应的字幕列表。此实现类似于步骤 2022,来自实现可重用的图像字幕特征提取器食谱:

    def load_paths_and_captions():
        image_paths = list(glob.glob(f'{IMAGES_PATH}/*.jpg'))
        with open(f'{CAPTIONS_PATH}', 'r') as f:
            text = f.read()
            lines = text.split('\n')
        mapping = {}
        for line in lines:
            if '.jpg' not in line:
                continue
            tokens = line.split(',', maxsplit=1)
            if len(line) < 2:
                continue
            image_id, image_caption = tokens
            image_id = image_id.split('.')[0]
            captions_per_image = mapping.get(image_id, [])
            captions_per_image.append(image_caption)
            mapping[image_id] = captions_per_image
    
  4. 编译所有字幕:

        all_captions = []
        for image_path in image_paths:
            image_id = image_path.split('/')[-
                       1].split('.')[0]
            all_captions.append(mapping[image_id][0])
        return image_paths, all_captions
    
  5. 定义一个函数,该函数将构建网络的架构,接收词汇表大小、最大序列长度以及编码器的输入形状:

    def build_network(vocabulary_size,
                      max_sequence_length,
                      input_shape=(4096,)):
    
  6. 网络的第一部分接收特征向量并将其通过一个全连接的ReLU激活层:

        x = Dropout(rate=0.5)(feature_inputs)
        x = Dense(units=256)(x)
        feature_output = ReLU()(x)
    
  7. 层的第二部分接收文本序列,这些文本序列被转换为数值向量,并训练一个包含 256 个元素的嵌入层。然后,它将该嵌入传递给LSTM层:

        sequence_inputs = 
                Input(shape=(max_sequence_length,))
        y = Embedding(input_dim=vocabulary_size,
                      output_dim=256,
                      mask_zero=True)(sequence_inputs)
        y = Dropout(rate=0.5)(y)
        sequence_output = LSTM(units=256)(y)
    
  8. 我们将这两部分的输出连接起来,并通过一个全连接网络传递,输出层的单元数量与词汇表中的单词数相同。通过对该输出进行Softmax激活,我们得到一个对应词汇表中某个单词的 one-hot 编码向量:

        z = Add()([feature_output, sequence_output])
        z = Dense(units=256)(z)
        z = ReLU()(z)
        z = Dense(units=vocabulary_size)(z)
        outputs = Softmax()(z)
    
  9. 最后,我们构建模型,传入图像特征和文本序列作为输入,并输出 one-hot 编码向量:

        return Model(inputs=[feature_inputs, 
                      sequence_inputs],
                     outputs=outputs)
    
  10. 定义一个函数,通过使用分词器的内部映射将整数索引转换为单词:

    def get_word_from_index(tokenizer, index):
        return tokenizer.index_word.get(index, None)
    
  11. 定义一个函数来生成标题。它将从将beginsequence标记输入到网络开始,网络会迭代构建句子,直到达到最大序列长度或遇到endsequence标记:

    def produce_caption(model,
                        tokenizer,
                        image,
                        max_sequence_length):
        text = 'beginsequence'
        for _ in range(max_sequence_length):
           sequence = tokenizer.texts_to_sequences([text])[0]
            sequence = pad_sequences([sequence],
                   maxlen=max_sequence_length)
            prediction = model.predict([[image], sequence])
            index = np.argmax(prediction)
            word = get_word_from_index(tokenizer, index)
            if word is None:
                break
            text += f' {word}'
            if word == 'endsequence':
                break
        return text
    
  12. 定义一个函数来评估模型的表现。首先,我们将为测试数据集中每个图像的特征生成一个标题:

    def evaluate_model(model, features, captions, 
                         tokenizer,
                       max_seq_length):
        actual = []
        predicted = []
        for feature, caption in zip(features, captions):
            generated_caption = produce_caption(model,
                                                tokenizer,
                                                feature,
                                         max_seq_length)
            actual.append([caption.split(' ')])
            predicted.append(generated_caption.split(' '))
    
  13. 接下来,我们将使用不同的权重计算BLEU分数。虽然BLEU分数超出了本教程的范围,但你可以在另见部分找到一篇详细解释的优秀文章。你需要知道的是,它用于衡量生成的标题与一组参考标题的相似度:

        for index, weights in enumerate([(1, 0, 0, 0),
                                         (.5, .5, 0, 0),
                                         (.3, .3, .3, 0),
                                         (.25, .25, .25, 
                                            .25)],
                                        start=1):
            b_score = corpus_bleu(actual, predicted, weights)
            print(f'BLEU-{index}: {b_score}')
    
  14. 加载图像路径和标题:

    image_paths, all_captions = load_paths_and_captions()
    
  15. 创建图像提取模型:

    extractor_model = VGG16(weights='imagenet')
    inputs = extractor_model.inputs
    outputs = extractor_model.layers[-2].output
    extractor_model = Model(inputs=inputs, outputs=outputs)
    
  16. 创建图像标题特征提取器(传入我们在步骤 15中创建的常规图像提取器),并用它从数据中提取序列:

    extractor = ImageCaptionFeatureExtractor(
        feature_extractor=extractor_model,
        output_path=OUTPUT_PATH)
    extractor.extract_features(image_paths, all_captions)
    
  17. 加载我们在步骤 16中创建的已序列化输入和输出序列:

    pickled_data = []
    for p in [f'{OUTPUT_PATH}/input_features.pickle',
              f'{OUTPUT_PATH}/input_sequences.pickle',
              f'{OUTPUT_PATH}/output_sequences.pickle']:
        with open(p, 'rb') as f:
            pickled_data.append(pickle.load(f))
    input_feats, input_seqs, output_seqs = pickled_data
    
  18. 使用 80% 的数据进行训练,20% 用于测试:

    (train_input_feats, test_input_feats,
     train_input_seqs, test_input_seqs,
     train_output_seqs,
     test_output_seqs) = train_test_split(input_feats,
                                          input_seqs,
                                          output_seqs,
                                          train_size=0.8,
                                          random_state=9)
    
  19. 实例化并编译模型。因为最终这是一个多类分类问题,我们将使用categorical_crossentropy作为损失函数:

    vocabulary_size = len(extractor.tokenizer.word_index) + 1
    model = build_network(vocabulary_size,
                          extractor.max_seq_length)
    model.compile(loss='categorical_crossentropy',
                  optimizer='adam')
    
  20. 由于训练过程非常消耗资源,并且网络通常在早期就能给出最佳结果,因此我们创建了一个ModelCheckpoint回调,它将存储具有最低验证损失的模型:

    checkpoint_path = ('model-ep{epoch:03d}-
                         loss{loss:.3f}-'
                       'val_loss{val_loss:.3f}.h5')
    checkpoint = ModelCheckpoint(checkpoint_path,
                                 monitor='val_loss',
                                 verbose=1,
                                 save_best_only=True,
                                 mode='min')
    
  21. 在 30 个训练周期内拟合模型。请注意,我们必须传入两组输入或特征,但只有一组标签:

    EPOCHS = 30
    model.fit(x=[train_input_feats, train_input_seqs],
              y=train_output_seqs,
              epochs=EPOCHS,
              callbacks=[checkpoint],
              validation_data=([test_input_feats,test_input_
                                                     seqs],
                                           test_output_seqs))
    
  22. 加载最佳模型。这个模型可能会因运行而异,但在本教程中,它存储在model-ep003-loss3.847-val_loss4.328.h5文件中:

    model = load_model('model-ep003-loss3.847-
                       val_loss4.328.h5')
    
  23. 加载数据映射,其中包含所有特征与真实标题的配对。将特征和映射提取到不同的集合中:

    with open(f'{OUTPUT_PATH}/data_mapping.pickle', 'rb') as f:
        data_mapping = pickle.load(f)
    feats = [v['features'] for v in data_mapping.values()]
    captions = [v['caption'] for v in data_mapping.values()]
    
  24. 评估模型:

    evaluate_model(model,
                   features=feats,
                   captions=captions,
                   tokenizer=extractor.tokenizer,
                   max_seq_length=extractor.max_seq_length)
    

    这个步骤可能需要一些时间。最终,你会看到类似这样的输出:

    BLEU-1: 0.35674398077995173
    BLEU-2: 0.17030332240763874
    BLEU-3: 0.12170338107914261
    BLEU-4: 0.05493477725774873
    

训练图像标题生成器并不是一项简单的任务。然而,通过按正确的顺序执行合适的步骤,我们成功创建了一个表现不错的模型,并且在测试集上表现良好,基于前面代码块中显示的BLEU分数。继续阅读下一部分,了解它是如何工作的!

它是如何工作的……

在这个教程中,我们从零开始实现了一个图像描述生成网络。尽管一开始看起来可能有些复杂,但我们必须记住,这只是一个编码器-解码器架构的变种,类似于我们在第五章《使用自编码器减少噪声》和第六章《生成模型与对抗攻击》中研究过的架构。

在这种情况下,编码器只是一个完全连接的浅层网络,将我们从 ImageNet 的预训练模型中提取的特征映射到一个包含 256 个元素的向量。

另一方面,解码器并不是使用转置卷积,而是使用一个RNN,它接收文本序列(映射为数字向量)和图像特征,将它们连接成一个由 512 个元素组成的长序列。

网络的训练目标是根据前面时间步生成的所有词,预测句子中的下一个词。注意,在每次迭代中,我们传递的是与图像对应的相同特征向量,因此网络会学习按特定顺序映射某些词,以描述编码在该向量中的视觉数据。

网络的输出是独热编码,这意味着只有与网络认为应该出现在句子中的下一个词对应的位置包含 1,其余位置包含 0。

为了生成描述,我们遵循类似的过程。当然,我们需要某种方式告诉模型开始生成词汇。为此,我们将beginsequence标记传递给网络,并不断迭代,直到达到最大序列长度,或模型输出endsequence标记。记住,我们将每次迭代的输出作为下一次迭代的输入。

一开始这可能看起来有些困惑和繁琐,但现在你已经掌握了解决任何图像描述问题所需的构建块!

参见

如果你希望全面理解BLEU分数,可以参考这篇精彩的文章:machinelearningmastery.com/calculate-bleu-score-for-text-python/

为你的照片生成描述

训练一个优秀的图像描述生成系统只是整个过程的一部分。为了实际使用它,我们必须执行一系列的步骤,类似于我们在训练阶段执行的操作。

在这个教程中,我们将使用一个训练好的图像描述生成网络来生成新图像的文字描述。

让我们开始吧!

准备工作

虽然在这个特定的教程中我们不需要外部依赖,但我们需要访问一个训练好的图像描述生成网络,并且需要清理过的描述文本来对其进行训练。强烈建议你在开始这个教程之前,先完成实现可复用的图像描述特征提取器实现图像描述生成网络的教程。

你准备好了吗?让我们开始描述吧!

如何做……

按照以下步骤生成自己图像的标题:

  1. 和往常一样,让我们首先导入必要的依赖项:

    import glob
    import pickle
    import matplotlib.pyplot as plt
    import numpy as np
    from tensorflow.keras.applications.vgg16 import *
    from tensorflow.keras.models import *
    from tensorflow.keras.preprocessing.sequence import \
        pad_sequences
    from tensorflow.keras.preprocessing.text import Tokenizer
    from ch7.recipe1.extractor import ImageCaptionFeatureExtractor
    
  2. 定义一个函数,将整数索引转换为分词器映射中的对应单词:

    def get_word_from_index(tokenizer, index):
        return tokenizer.index_word.get(index, None)
    
  3. 定义produce_caption()函数,该函数接受标题生成模型、分词器、要描述的图像以及生成文本描述所需的最大序列长度:

    def produce_caption(model,
                        tokenizer,
                        image,
                        max_sequence_length):
        text = 'beginsequence'
        for _ in range(max_sequence_length):
           sequence = tokenizer.texts_to_sequences([text])[0]
           sequence = pad_sequences([sequence],
                                 maxlen=max_sequence_length)
            prediction = model.predict([[image], sequence])
            index = np.argmax(prediction)
            word = get_word_from_index(tokenizer, index)
            if word is None:
                break
            text += f' {word}'
            if word == 'endsequence':
                break
        return text
    

    注意,我们必须持续生成单词,直到遇到endsequence标记或达到最大序列长度。

  4. 定义一个预训练的VGG16网络,我们将其用作图像特征提取器:

    extractor_model = VGG16(weights='imagenet')
    inputs = extractor_model.inputs
    outputs = extractor_model.layers[-2].output
    extractor_model = Model(inputs=inputs, outputs=outputs)
    
  5. 将图像提取器传递给ImageCaptionFeatureExtractor()的一个实例:

    extractor = ImageCaptionFeatureExtractor(
        feature_extractor=extractor_model)
    
  6. 加载我们用于训练模型的清理过的标题。我们需要它们来拟合步骤 7中的分词器:

    with open('data_mapping.pickle', 'rb') as f:
        data_mapping = pickle.load(f)
    captions = [v['caption'] for v in 
                data_mapping.values()]
    
  7. 实例化Tokenizer()并将其拟合到所有标题。还需计算最大序列长度:

    tokenizer = Tokenizer()
    tokenizer.fit_on_texts(captions)
    max_seq_length = extractor._get_max_seq_length(captions)
    
  8. 加载训练好的网络(在本例中,网络名称为model-ep003-loss3.847-val_loss4.328.h5):

    model = load_model('model-ep003-loss3.847-
                         val_loss4.328.h5')
    
  9. 遍历当前目录中的所有测试图像,提取相应的数字特征:

    for idx, image_path in enumerate(glob.glob('*.jpg'), 
                                       start=1):
        img_feats = (extractor
                     .extract_image_features(image_path))
    
  10. 生成标题并移除beginsequenceendsequence特殊标记:

        description = produce_caption(model,
                                      tokenizer,
                                      img_feats,
                                      max_seq_length)
        description = (description
                       .replace('beginsequence', '')
                       .replace('endsequence', ''))
    
  11. 打开图像,将生成的标题作为其标题并保存:

        image = plt.imread(image_path)
        plt.imshow(image)
        plt.title(description)
        plt.savefig(f'{idx}.jpg')
    

    这是一个图像,网络在生成适当的标题方面表现得非常好:

图 7.3 – 我们可以看到,标题非常接近实际发生的情况

图 7.3 – 我们可以看到,标题非常接近实际发生的情况:

这是另一个例子,尽管网络在技术上是正确的,但它的准确性可以更高:

图 7.4 – 一名穿红色制服的足球运动员确实在空中,但还发生了更多的事情

图 7.4 – 一名穿红色制服的足球运动员确实在空中,但还发生了更多的事情

最后,这里有一个网络完全无能为力的例子:

图 7.5 – 网络无法描述这一场景

图 7.5 – 网络无法描述这一场景

这样,我们已经看到模型在一些图像上的表现不错,但仍有提升空间。我们将在下一节深入探讨。

它是如何工作的……

在本次配方中,我们了解到图像标题生成是一个困难的问题,且严重依赖于许多因素。以下是一些因素:

  • 一个训练良好的CNN用于提取高质量的视觉特征:

  • 为每个图像提供一组丰富的描述性标题:

  • 具有足够容量的嵌入,能够以最小的损失编码词汇的表现力:

  • 一个强大的RNN来学习如何将这一切组合在一起:

尽管存在这些明显的挑战,在这个教程中,我们使用了一个在 Flickr8k 数据集上训练的网络来生成新图像的标注。我们遵循的过程与我们训练系统时实施的过程类似:首先,我们必须将图像转换为特征向量。然后,我们需要对词汇表进行分词器拟合,获取适当的机制,以便能够将序列转换为人类可读的单词。最后,我们逐字拼接标注,同时传递图像特征和我们已经构建的序列。那么,我们如何知道何时停止呢?我们有两个停止标准:

  • 标注达到了最大序列长度。

  • 网络遇到了 endsequence 标记。

最后,我们在多张图片上测试了我们的解决方案,结果不一。在某些情况下,网络能够生成非常精确的描述,而在其他情况下,生成的标注则稍显模糊。在最后一个示例中,网络完全没有达成预期,这清楚地表明了仍有很大的改进空间。

如果你想查看其他带标注的图像,请查阅官方仓库:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch7/recipe3

在 COCO 上实现带注意力机制的图像标注网络

理解图像标注网络如何生成描述的一个好方法是向架构中添加一个注意力组件。这使我们能够看到在生成每个单词时,网络注视图像的哪些部分。

在本教程中,我们将在更具挑战性的 常见物体上下文 (COCO) 数据集上训练一个端到端的图像标注系统。我们还将为网络配备注意力机制,以提高其性能,并帮助我们理解其内部推理过程。

这是一个较长且复杂的教程,但不用担心!我们将逐步进行。如果你想深入了解支撑该实现的理论,请查看 另请参阅 部分。

准备就绪

尽管我们将使用 COCO 数据集,但你无需提前做任何准备,因为我们将在教程中下载它(不过,你可以在这里了解更多关于这个开创性数据集的信息:https://cocodataset.org/#home)。

以下是 COCO 数据集中的一个示例:

图 7.6 – 来自 COCO 的示例图片

](https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/tf20-cv-cb/img/B14768_07_006.jpg)

图 7.6 – 来自 COCO 的示例图片

让我们开始工作吧!

如何实现…

按照以下步骤完成这个教程:

  1. 导入所有必要的依赖项:

    import json
    import os
    import time
    import matplotlib.pyplot as plt
    import numpy as np
    import tensorflow as tf
    from sklearn.model_selection import train_test_split
    from sklearn.utils import shuffle
    from tensorflow.keras.applications.inception_v3 import *
    from tensorflow.keras.layers import *
    from tensorflow.keras.losses import \
        SparseCategoricalCrossentropy
    from tensorflow.keras.models import Model
    from tensorflow.keras.optimizers import Adam
    from tensorflow.keras.preprocessing.sequence import \
        pad_sequences
    from tensorflow.keras.preprocessing.text import Tokenizer
    from tensorflow.keras.utils import get_file
    
  2. tf.data.experimental.AUTOTUNE 定义一个别名:

    AUTOTUNE = tf.data.experimental.AUTOTUNE
    
  3. 定义一个函数来加载图像。它必须返回图像及其路径:

    def load_image(image_path):
        image = tf.io.read_file(image_path)
        image = tf.image.decode_jpeg(image, channels=3)
        image = tf.image.resize(image, (299, 299))
        image = preprocess_input(image)
        return image, image_path
    
  4. 定义一个函数来获取最大序列长度。这将在稍后使用:

    def get_max_length(tensor):
        return max(len(t) for t in tensor)
    
  5. 为图像标注网络定义一个函数,从磁盘加载图像(存储为 NumPy 格式):

    def load_image_and_caption(image_name, caption):
        image_name = image_name.decode('utf-8').split('/')
                                          [-1]
        image_tensor = np.load(f'./{image_name}.npy')
        return image_tensor, caption
    
  6. 使用模型子类化实现巴赫达努的注意力机制

    class BahdanauAttention(Model):
        def __init__(self, units):
            super(BahdanauAttention, self).__init__()
            self.W1 = Dense(units)
            self.W2 = Dense(units)
            self.V = Dense(1)
    
  7. 前面的代码块定义了网络层。现在,我们在call()方法中定义前向传播:

        def call(self, features, hidden):
            hidden_with_time_axis = tf.expand_dims(hidden, 
                                                     1)
            score = tf.nn.tanh(self.W1(features) +
                            self.W2(hidden_with_time_axis))
            attention_w = tf.nn.softmax(self.V(score), 
                                         axis=1)
            ctx_vector = attention_w * features
            ctx_vector = tf.reduce_sum(ctx_vector, axis=1)
            return ctx_vector, attention_w
    
  8. 定义图像编码器。这只是一个ReLU

    class CNNEncoder(Model):
        def __init__(self, embedding_dim):
            super(CNNEncoder, self).__init__()
            self.fc = Dense(embedding_dim)
        def call(self, x):
            x = self.fc(x)
            x = tf.nn.relu(x)
            return x
    
  9. 定义解码器。它是一个GRU和注意力机制,学习如何从视觉特征向量和文本输入序列中生成标题:

    class RNNDecoder(Model):
        def __init__(self, embedding_size, units, 
                         vocab_size):
            super(RNNDecoder, self).__init__()
            self.units = units
            self.embedding = Embedding(vocab_size, 
                                        embedding_size)
            self.gru = GRU(self.units,
                           return_sequences=True,
                           return_state=True,
                           recurrent_initializer='glorot_
                           uniform')
            self.fc1 = Dense(self.units)
            self.fc2 = Dense(vocab_size)
            self.attention = BahdanauAttention(self.units)
    
  10. 现在我们已经定义了RNN架构中的各个层,接下来实现前向传播。首先,我们必须通过注意力子网络传递输入:

        def call(self, x, features, hidden):
            context_vector, attention_weights = \
                self.attention(features, hidden)
    
  11. 然后,我们必须将输入序列(x)通过嵌入层,并将其与从注意力机制中获得的上下文向量进行连接:

            x = self.embedding(x)
            expanded_context = tf.expand_dims(context_vector, 
                                               1)
            x = Concatenate(axis=-1)([expanded_context, x])
    
  12. 接下来,我们必须将合并后的张量传递给GRU层,然后通过全连接层。这样返回的是输出序列、状态和注意力权重:

            output, state = self.gru(x)
            x = self.fc1(output)
            x = tf.reshape(x, (-1, x.shape[2]))
            x = self.fc2(x)
    
  13. 最后,我们必须定义一个方法来重置隐藏状态:

        def reset_state(self, batch_size):
            return tf.zeros((batch_size, self.units))
    
  14. 定义ImageCaptionerClass。构造函数实例化基本组件,包括编码器、解码器、分词器、以及训练整个系统所需的优化器和损失函数:

    class ImageCaptioner(object):
        def __init__(self, embedding_size, units, 
                     vocab_size,
                     tokenizer):
            self.tokenizer = tokenizer
            self.encoder = CNNEncoder(embedding_size)
            self.decoder = RNNDecoder(embedding_size, 
                                        units,
                                      vocab_size)
            self.optimizer = Adam()
            self.loss = SparseCategoricalCrossentropy(
                from_logits=True,
                reduction='none')
    
  15. 创建一个方法来计算损失函数:

        def loss_function(self, real, predicted):
            mask = tf.math.logical_not(tf.math.equal(real, 
                                                   0))
            _loss = self.loss(real, predicted)
            mask = tf.cast(mask, dtype=_loss.dtype)
            _loss *= mask
            return tf.reduce_mean(_loss)
    
  16. 接下来,定义一个函数来执行单个训练步骤。我们将从创建隐藏状态和输入开始,输入仅是包含<start>标记索引的单一序列批次,<start>是一个特殊元素,用于指示句子的开始:

        @tf.function
        def train_step(self, image_tensor, target):
            loss = 0
            hidden = 
           self.decoder.reset_state(target.shape[0])
            start_token_idx = 
           self.tokenizer.word_index['<start>']
            init_batch = [start_token_idx] * 
            target.shape[0]
            decoder_input = tf.expand_dims(init_batch, 1)
    
  17. 现在,我们必须编码图像张量。然后,我们将反复将结果特征传递给解码器,连同到目前为止的输出序列和隐藏状态。关于RNNs如何工作的更深层次解释,请参考另见部分:

          with tf.GradientTape() as tape:
                features = self.encoder(image_tensor)
                for i in range(1, target.shape[1]):
                    preds, hidden, _ = 
                    self.decoder(decoder_input,
                                features,
                                 hidden)
                    loss += self.loss_function(target[:, i],
                                               preds)
                    decoder_input = 
                           tf.expand_dims(target[:, i],1)
    
  18. 请注意,在前面的代码块中我们在每个时间步计算了损失。为了获得总损失,我们必须计算平均值。为了让网络真正学习,我们必须通过反向传播计算梯度,并通过优化器应用这些梯度:

            total_loss = loss / int(target.shape[1])
            trainable_vars = (self.encoder.trainable_
                                variables +
                              self.decoder.trainable_
                                 variables)
            gradients = tape.gradient(loss, trainable_vars)
            self.optimizer.apply_gradients(zip(gradients,
                                          trainable_vars))
            return loss, total_loss
    
  19. 本类中的最后一个方法负责训练系统:

        def train(self, dataset, epochs, num_steps):
            for epoch in range(epochs):
                start = time.time()
                total_loss = 0
                for batch, (image_tensor, target) \
                        in enumerate(dataset):
                    batch_loss, step_loss = \
                        self.train_step(image_tensor, target)
                    total_loss += step_loss
    
  20. 每经过 100 个 epoch,我们将打印损失。在每个 epoch 结束时,我们还将打印该 epoch 的损失和已用时间:

                    if batch % 100 == 0:
                        loss = batch_loss.numpy()
                        loss = loss / int(target.shape[1])
                        print(f'Epoch {epoch + 1}, batch 
                                                 {batch},'
                              f' loss {loss:.4f}')
                print(f'Epoch {epoch + 1},'
                      f' loss {total_loss / 
                               num_steps:.6f}')
                epoch_time = time.time() - start
                print(f'Time taken: {epoch_time} seconds. 
                       \n')
    
  21. 下载并解压COCO数据集的注释文件。如果它们已经在系统中,只需存储文件路径:

    INPUT_DIR = os.path.abspath('.')
    annots_folder = '/annotations/'
    if not os.path.exists(INPUT_DIR + annots_folder):
        origin_url = ('http://images.cocodataset.org/
                annotations''/annotations_trainval2014.zip')
        cache_subdir = os.path.abspath('.')
        annots_zip = get_file('all_captions.zip',
                              cache_subdir=cache_subdir,
                              origin=origin_url,
                              extract=True)
        annots_file = (os.path.dirname(annots_zip) +
                      '/annotations/captions_train2014.json')
        os.remove(annots_zip)
    else:
        annots_file = (INPUT_DIR +
                      '/annotations/captions_train2014.json')
    
  22. 下载并解压COCO数据集的图像文件。如果它们已经在系统中,只需存储文件路径:

    image_folder = '/train2014/'
    if not os.path.exists(INPUT_DIR + image_folder):
        origin_url = ('http://images.cocodataset.org/zips/'
                      'train2014.zip')
        cache_subdir = os.path.abspath('.')
        image_zip = get_file('train2014.zip',
                             cache_subdir=cache_subdir,
                             origin=origin_url,
                             extract=True)
        PATH = os.path.dirname(image_zip) + image_folder
        os.remove(image_zip)
    else:
        PATH = INPUT_DIR + image_folder
    
  23. 加载图像路径和标题。我们必须将特殊的<start><end>标记添加到每个标题中,以便它们包含在我们的词汇表中。这些特殊标记使我们能够分别指定序列的开始和结束位置:

    with open(annots_file, 'r') as f:
        annotations = json.load(f)
    captions = []
    image_paths = []
    for annotation in annotations['annotations']:
        caption = '<start>' + annotation['caption'] + ' <end>'
        image_id = annotation['image_id']
        image_path = f'{PATH}COCO_train2014_{image_id:012d}.jpg'
        image_paths.append(image_path)
        captions.append(caption)
    
  24. 由于COCO数据集庞大,训练一个模型需要很长时间,我们将选择 30,000 张图像及其对应的标题作为随机样本:

    train_captions, train_image_paths = shuffle(captions,
                                            image_paths, 
                                          random_state=42)
    SAMPLE_SIZE = 30000
    train_captions = train_captions[:SAMPLE_SIZE]
    train_image_paths = train_image_paths[:SAMPLE_SIZE]
    train_images = sorted(set(train_image_paths))
    
  25. 我们使用InceptionV3的预训练实例作为我们的图像特征提取器:

    feature_extractor = InceptionV3(include_top=False,
                                    weights='imagenet')
    feature_extractor = Model(feature_extractor.input,
                              feature_extractor.layers[-
                                            1].output)
    
  26. 创建一个 tf.data.Dataset,将图像路径映射到张量。使用它遍历我们样本中的所有图像,将它们转换为特征向量,并将其保存为 NumPy 数组。这将帮助我们在将来节省内存:

    BATCH_SIZE = 8
    image_dataset = (tf.data.Dataset
                     .from_tensor_slices(train_images)
                     .map(load_image, 
                         num_parallel_calls=AUTOTUNE)
                     .batch(BATCH_SIZE))
    for image, path in image_dataset:
        batch_features = feature_extractor.predict(image)
        batch_features = tf.reshape(batch_features,
                                 (batch_features.shape[0],
                                     -1,
                                batch_features.shape[3]))
        for batch_feature, p in zip(batch_features, path):
            feature_path = p.numpy().decode('UTF-8')
            image_name = feature_path.split('/')[-1]
            np.save(f'./{image_name}', batch_feature.numpy())
    
  27. 在我们标题中的前 5,000 个单词上训练一个分词器。然后,将每个文本转换为数字序列,并进行填充,使它们的大小一致。同时,计算最大序列长度:

    top_k = 5000
    filters = '!”#$%&()*+.,-/:;=?@[\]^_`{|}~ '
    tokenizer = Tokenizer(num_words=top_k,
                          oov_token='<unk>',
                          filters=filters)
    tokenizer.fit_on_texts(train_captions)
    tokenizer.word_index['<pad>'] = 0
    tokenizer.index_word[0] = '<pad>'
    train_seqs = tokenizer.texts_to_sequences(train_captions)
    captions_seqs = pad_sequences(train_seqs, 
                                  padding='post')
    max_length = get_max_length(train_seqs)
    
  28. 我们将使用 20% 的数据来测试模型,其余 80% 用于训练:

    (images_train, images_val, caption_train, caption_val) = \
        train_test_split(train_img_paths,
                         captions_seqs,
                         test_size=0.2,
                         random_state=42)
    
  29. 我们将一次加载 64 张图像的批次(以及它们的标题)。请注意,我们使用的是 第 5 步 中定义的 load_image_and_caption() 函数,它读取与图像对应的特征向量,这些向量以 NumPy 格式存储。此外,由于该函数在 NumPy 层面工作,我们必须通过 tf.numpy_function 将其包装,以便它能作为有效的 TensorFlow 函数在 map() 方法中使用:

    BATCH_SIZE = 64
    BUFFER_SIZE = 1000
    dataset = (tf.data.Dataset
               .from_tensor_slices((images_train, 
                                    caption_train))
               .map(lambda i1, i2:
                    tf.numpy_function(
                        load_image_and_caption,
                        [i1, i2],
                        [tf.float32, tf.int32]),
                    num_parallel_calls=AUTOTUNE)
               .shuffle(BUFFER_SIZE)
               .batch(BATCH_SIZE)
               .prefetch(buffer_size=AUTOTUNE))
    
  30. 让我们实例化一个 ImageCaptioner。嵌入层将包含 256 个元素,解码器和注意力模型的单元数将是 512。词汇表大小为 5,001。最后,我们必须传入 第 27 步 中拟合的分词器:

    image_captioner = ImageCaptioner(embedding_size=256,
                                     units=512,
                                     vocab_size=top_k + 1,
                                     tokenizer=tokenizer)
    EPOCHS = 30
    num_steps = len(images_train) // BATCH_SIZE
    image_captioner.train(dataset, EPOCHS, num_steps)
    
  31. 定义一个函数,用于在图像上评估图像标题生成器。它必须接收编码器、解码器、分词器、待描述的图像、最大序列长度以及注意力向量的形状。我们将从创建一个占位符数组开始,这里将存储构成注意力图的子图:

    def evaluate(encoder, decoder, tokenizer, image, 
                  max_length,
                 attention_shape):
        attention_plot = np.zeros((max_length,
                                   attention_shape))
    
  32. 接下来,我们必须初始化隐藏状态,提取输入图像的特征,并将其传递给编码器。我们还必须通过创建一个包含 <start> 标记索引的单一序列来初始化解码器输入:

        hidden = decoder.reset_state(batch_size=1)
        temp_input = tf.expand_dims(load_image(image)[0], 
                                        0)
        image_tensor_val = feature_extractor(temp_input)
        image_tensor_val = tf.reshape(image_tensor_val,
                               (image_tensor_val.shape[0],
                                       -1,
                              image_tensor_val.shape[3]))
        feats = encoder(image_tensor_val)
        start_token_idx = tokenizer.word_index['<start>']
        dec_input = tf.expand_dims([start_token_idx], 0)
        result = []
    
  33. 现在,让我们构建标题,直到达到最大序列长度或遇到 <end> 标记:

        for i in range(max_length):
            (preds, hidden, attention_w) = \
                decoder(dec_input, feats, hidden)
            attention_plot[i] = tf.reshape(attention_w,
                                           (-1,)).numpy()
            pred_id = tf.random.categorical(preds,
                                         1)[0][0].numpy()
            result.append(tokenizer.index_word[pred_id])
            if tokenizer.index_word[pred_id] == '<end>':
                return result, attention_plot
            dec_input = tf.expand_dims([pred_id], 0)
        attention_plot = attention_plot[:len(result), :]
        return result, attention_plot
    
  34. 请注意,对于每个单词,我们都会更新 attention_plot,并返回解码器的权重。

  35. 让我们定义一个函数,用于绘制网络对每个单词的注意力。它接收图像、构成标题的单个单词列表(result)、由 evaluate() 返回的 attention_plot,以及我们将存储图形的输出路径:

    def plot_attention(image, result,
                       attention_plot, output_path):
        tmp_image = np.array(load_image(image)[0])
        fig = plt.figure(figsize=(10, 10))
    
  36. 我们将遍历每个单词,创建相应注意力图的子图,并以其链接的特定单词为标题:

        for l in range(len(result)):
            temp_att = np.resize(attention_plot[l], (8, 8))
            ax = fig.add_subplot(len(result) // 2,
                                 len(result) // 2,
                                 l + 1)
            ax.set_title(result[l])
            image = ax.imshow(tmp_image)
            ax.imshow(temp_att,
                      cmap='gray',
                      alpha=0.6,
                      extent=image.get_extent())
    
  37. 最后,我们可以保存完整的图:

        plt.tight_layout()
        plt.show()
        plt.savefig(output_path)
    
  38. 在验证集上评估网络的随机图像:

    attention_features_shape = 64
    random_id = np.random.randint(0, len(images_val))
    image = images_val[random_id]
    
  39. 构建并清理实际(真实标签)标题:

    actual_caption = ' '.join([tokenizer.index_word[i]
                             for i in caption_val[random_id]
                               if i != 0])
    actual_caption = (actual_caption
                      .replace('<start>', '')
                      .replace('<end>', ''))
    
  40. 为验证图像生成标题:

    result, attention_plot = evaluate(image_captioner               
                                    encoder,
                       image_captioner.decoder,
                                      tokenizer,
                                      image,
                                      max_length,
                              attention_feats_shape)
    
  41. 构建并清理预测的标题:

    predicted_caption = (' '.join(result)
                         .replace('<start>', '')
                         .replace('<end>', '')) 
    
  42. 打印真实标签和生成的标题,然后将注意力图保存到磁盘:

    print(f'Actual caption: {actual_caption}')
    print(f'Predicted caption: {predicted_caption}')
    output_path = './attention_plot.png'
    plot_attention(image, result, attention_plot, output_path)
    
  43. 在以下代码块中,我们可以欣赏到真实标题与模型输出标题之间的相似性:

    Actual caption: a lone giraffe stands in the midst of a grassy area
    Predicted caption: giraffe standing in a dry grass near trees
    

    现在,让我们来看一下注意力图:

图 7.7 – 注意力图

图 7.7 – 注意力图

注意在生成每个单词时,网络关注的区域。较浅的方块表示更多的关注被放在这些像素上。例如,要生成单词giraffe,网络关注了照片中长颈鹿的周围环境。此外,我们可以看到,当网络生成单词grass时,它关注了长颈鹿腿部的草地部分。难道这不令人惊讶吗?

我们将在它是如何工作的...部分中详细讨论这个问题。

它是如何工作的…

在这个食谱中,我们实现了一个更完整的图像描述系统,这一次使用了挑战更大的COCO数据集,该数据集不仅比Flickr8k大几个数量级,而且更加多样化,因此网络理解起来更为困难。

然而,我们通过为网络提供一个注意力机制,使其拥有优势,这一机制灵感来自 Dzmitry Bahdanau 提出的令人印象深刻的突破(更多细节请参见另见部分)。这个功能赋予模型进行软搜索的能力,查找与预测目标词相关的源描述部分,简而言之,就是在输出句子中生成最佳的下一个词。这种注意力机制相对于传统方法具有优势,传统方法是使用固定长度的向量(如我们在实现图像描述网络食谱中所做的那样),解码器从中生成输出句子。这样表示的问题在于,当提高性能时,它往往会成为瓶颈。

此外,注意力机制使我们能够以更直观的方式理解网络生成描述时的思考过程。

因为神经网络是复杂的软件(通常像一个黑箱),使用视觉技术来检查其内部工作原理是我们可以利用的一种很好的工具,有助于我们在训练、微调和优化过程中。

另见

在这个食谱中,我们使用模型子类化模式实现了我们的架构,你可以在这里阅读更多内容:www.tensorflow.org/guide/keras/custom_layers_and_models

请查看以下链接,复习一下RNN的内容:www.youtube.com/watch?v=UNmqTiOnRfg

最后,我强烈鼓励你阅读 Dzmitry Bahdanau 关于我们刚刚实现和使用的注意力机制的论文:arxiv.org/abs/1409.0473

第八章:第八章:通过分割实现对图像的精细理解

图像分割是计算机视觉研究领域中最大的领域之一。它通过将共享一个或多个定义特征(如位置、颜色或纹理)的像素组合在一起,简化图像的视觉内容。与计算机视觉的许多其他子领域一样,图像分割也得到了深度神经网络的极大推动,特别是在医学和自动驾驶等行业。

虽然对图像的内容进行分类非常重要,但往往仅仅分类是不够的。假如我们想知道一个物体具体在哪里呢?如果我们对它的形状感兴趣呢?如果我们需要它的轮廓呢?这些精细的需求是传统分类技术无法满足的。然而,正如我们将在本章中发现的那样,我们可以用一种非常类似于常规分类项目的方式来框定图像分割问题。怎么做?我们不是给整张图像标注标签,而是给每个像素标注!这就是图像分割,也是本章食谱的核心内容。

在本章中,我们将涵盖以下食谱:

  • 创建一个用于图像分割的全卷积网络

  • 从头开始实现 U-Net

  • 使用迁移学习实现 U-Net

  • 使用 Mask-RCNN 和 TensorFlow Hub 进行图像分割

让我们开始吧!

技术要求

为了实现和实验本章的食谱,建议你拥有一台 GPU。如果你有访问基于云的服务提供商,如 AWS 或 FloydHub,那就太好了,但请注意相关费用,因为如果不小心的话,费用可能会飙升!在每个食谱的准备工作部分,你会找到你需要为接下来的内容做准备的所有信息。本章的代码可以在这里找到:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch8

查看以下链接,观看《代码实践》视频:

bit.ly/2Na77IF

创建一个用于图像分割的全卷积网络

如果你在知道图像分割本质上就是像素级分类的情况下,创建你的第一个图像分割网络,你会怎么做?你可能会选择一个经过验证的架构,并将最后的层(通常是全连接层)替换为卷积层,以便生成一个输出体积,而不是输出向量。

好的,这正是我们在本食谱中要做的,基于著名的VGG16网络构建一个全卷积网络FCN)来进行图像分割。

让我们开始吧!

准备工作

我们需要安装几个外部库,首先是 tensorflow_docs

$> pip install git+https://github.com/tensorflow/docs

接下来,我们需要安装 TensorFlow Datasets、PillowOpenCV

$> pip install tensorflow-datasets Pillow opencv-contrib-python

关于数据,我们将从the Oxford-IIIT Pet数据集中分割图像。好消息是,我们将通过tensorflow-datasets来访问它,所以在这方面我们实际上不需要做任何事情。该数据集中的每个像素将被分类如下:

  • 1: 像素属于宠物(猫或狗)。

  • 2: 像素属于宠物的轮廓。

  • 3: 像素属于周围环境。

这里是数据集中的一些示例图像:

图 8.1 – 来自 Oxford-IIIT Pet 数据集的示例图像

](https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/tf20-cv-cb/img/B14768_08_001.jpg)

图 8.1 – 来自 Oxford-IIIT Pet 数据集的示例图像

让我们开始实现吧!

如何实现……

按照以下步骤完成此配方:

  1. 导入所有必需的包:

    import pathlib
    import cv2
    import matplotlib.pyplot as plt
    import numpy as np
    import tensorflow as tf
    import tensorflow_datasets as tfds
    import tensorflow_docs as tfdocs
    import tensorflow_docs.plots
    from tensorflow.keras.layers import *
    from tensorflow.keras.losses import \
        SparseCategoricalCrossentropy
    from tensorflow.keras.models import Model
    from tensorflow.keras.optimizers import RMSprop
    
  2. tf.data.experimental.AUTOTUNE定义一个别名:

    AUTOTUNE = tf.data.experimental.AUTOTUNE 
    
  3. 定义一个函数,用于将数据集中的图像归一化到[0, 1]范围。为了保持一致性,我们将从掩膜中的每个像素减去 1,这样它们的范围就从 0 扩展到 2:

    def normalize(input_image, input_mask):
       input_image = tf.cast(input_image, tf.float32) / 255.0
        input_mask -= 1
        return input_image, input_mask
    
  4. 定义load_image()函数,给定一个 TensorFlow 数据集元素,该函数加载图像及其掩膜。我们将借此机会将图像调整为256x256。另外,如果train标志设置为True,我们可以通过随机镜像图像及其掩膜来进行一些数据增强。最后,我们必须对输入进行归一化:

    @tf.function
    def load_image(dataset_element, train=True):
      input_image = tf.image.resize(dataset_element['image'],
                                      (256, 256))
        input_mask = tf.image.resize(
            dataset_element['segmentation_mask'], (256, 256))
        if train and np.random.uniform() > 0.5:
            input_image = 
                  tf.image.flip_left_right(input_image)
            input_mask = tf.image.flip_left_right(input_mask)
        input_image, input_mask = normalize(input_image,
                                            input_mask)
        return input_image, input_mask
    
  5. 实现FCN()类,该类封装了构建、训练和评估所需的所有逻辑,使用RMSProp作为优化器,SparseCategoricalCrossentropy作为损失函数。请注意,output_channels默认值为 3,因为每个像素可以被分类为三类之一。还请注意,我们正在定义基于VGG16的预训练模型的权重路径。我们将使用这些权重在训练时为网络提供一个良好的起点。

  6. 现在,是时候定义模型的架构了:

    def _create_model(self):
            input = Input(shape=self.input_shape)
            x = Conv2D(filters=64,
                       kernel_size=(3, 3),
                       activation='relu',
                       padding='same',
                       name='block1_conv1')(input)
            x = Conv2D(filters=64,
                       kernel_size=(3, 3),
                       activation='relu',
                       padding='same',
                       name='block1_conv2')(x)
            x = MaxPooling2D(pool_size=(2, 2),
                             strides=2,
                             name='block1_pool')(x)
    
  7. 我们首先定义了输入和第一块卷积层以及最大池化层。现在,定义第二块卷积层,这次每个卷积使用 128 个滤波器:

            x = Conv2D(filters=128,
                       kernel_size=(3, 3),
                       activation='relu',
                       padding='same',
                       name='block2_conv1')(x)
            x = Conv2D(filters=128,
                       kernel_size=(3, 3),
                       activation='relu',
                       padding='same',
                       name='block2_conv2')(x)
            x = MaxPooling2D(pool_size=(2, 2),
                             strides=2,
    
               name='block2_pool')(x)
    
  8. 第三块包含 256 个滤波器的卷积:

            x = Conv2D(filters=256,
                       kernel_size=(3, 3),
                       activation='relu',
                       padding='same',
                       name='block3_conv1')(x)
            x = Conv2D(filters=256,
                       kernel_size=(3, 3),
                       activation='relu',
                       padding='same',
                       name='block3_conv2')(x)
            x = Conv2D(filters=256,
                       kernel_size=(3, 3),
                       activation='relu',
                       padding='same',
                       name='block3_conv3')(x)
            x = MaxPooling2D(pool_size=(2, 2),
                             strides=2,
                             name='block3_pool')(x)
            block3_pool = x
    
  9. 第四块使用了 512 个滤波器的卷积:

            x = Conv2D(filters=512,
                       kernel_size=(3, 3),
                       activation='relu',
                       padding='same',
                       name='block4_conv1')(x)
            x = Conv2D(filters=512,
                       kernel_size=(3, 3),
                       activation='relu',
                       padding='same',
                       name='block4_conv2')(x)
            x = Conv2D(filters=512,
                       kernel_size=(3, 3),
                       activation='relu',
                       padding='same',
                       name='block4_conv3')(x)
            block4_pool = MaxPooling2D(pool_size=(2, 2),
                                       strides=2,
                                 name='block4_pool')(x)
    
  10. 第五块是第四块的重复,同样使用 512 个滤波器的卷积:

            x = Conv2D(filters=512,
                       kernel_size=(3, 3),
                       activation='relu',
                       padding='same',
                       name='block5_conv1')(block4_pool)
            x = Conv2D(filters=512,
                       kernel_size=(3, 3),
                       activation='relu',
                       padding='same',
                       name='block5_conv2')(x)
            x = Conv2D(filters=512,
                       kernel_size=(3, 3),
                       activation='relu',
                       padding='same',
                       name='block5_conv3')(x)
            block5_pool = MaxPooling2D(pool_size=(2, 2),
                                       strides=2,
                                   name='block5_pool')(x)
    
  11. 我们到目前为止命名层的原因是,为了在接下来导入预训练权重时能够与它们匹配(请注意by_name=True):

            model = Model(input, block5_pool)
            model.load_weights(self.vgg_weights_path,
                               by_name=True)
    
  12. 在传统的VGG16架构中,output由全连接层组成。然而,我们将用反卷积层替换它们。请注意,我们正在将这些层连接到第五块的输出:

            output = Conv2D(filters=self.output_channels,
                            kernel_size=(7, 7),
                            activation='relu',
                            padding='same',
                            name='conv6')(block5_pool)
            conv6_4 = Conv2DTranspose(
                filters=self.output_channels,
                kernel_size=(4, 4),
                strides=4,
                use_bias=False)(output)
    
  13. 创建一个 1x1 卷积层,接着是一个反卷积层,并将其连接到第四块的输出(这实际上是一个跳跃连接):

            pool4_n = Conv2D(filters=self.output_channels,
                             kernel_size=(1, 1),
                             activation='relu',
                             padding='same',
                             name='pool4_n')(block4_pool)
            pool4_n_2 = Conv2DTranspose(
                filters=self.output_channels,
                kernel_size=(2, 2),
                strides=2,
                use_bias=False)(pool4_n)
    
  14. 将第三块的输出通过一个 1x1 卷积层。然后,将这三条路径合并成一条,传递通过最后一个反卷积层。这将通过Softmax激活。这个输出即为模型预测的分割掩膜:

            pool3_n = Conv2D(filters=self.output_channels,
                             kernel_size=(1, 1),
                             activation='relu',
                             padding='same',
                             name='pool3_n')(block3_pool)
            output = Add(name='add')([pool4_n_2,
                                      pool3_n,
                                      conv6_4])
            output = Conv2DTranspose
                           (filters=self.output_channels,
                                     kernel_size=(8, 8),
                                     strides=8,
                                 use_bias=False)(output)
            output = Softmax()(output)
            return Model(input, output)
    
  15. 现在,让我们创建一个私有辅助方法来绘制相关的训练曲线:

        @staticmethod
        def _plot_model_history(model_history, metric, 
                                            ylim=None):
            plt.style.use('seaborn-darkgrid')
            plotter = tfdocs.plots.HistoryPlotter()
            plotter.plot({'Model': model_history}, 
                                metric=metric)
            plt.title(f'{metric.upper()}')
            if ylim is None:
                plt.ylim([0, 1])
            else:
                plt.ylim(ylim)
            plt.savefig(f'{metric}.png')
            plt.close()
    
  16. train() 方法接受训练和验证数据集,以及执行的周期数和训练、验证步骤数,用于拟合模型。它还将损失和准确率图保存到磁盘,以便后续分析:

        def train(self, train_dataset, epochs, 
                         steps_per_epoch,
                  validation_dataset, validation_steps):
            hist = \
                self.model.fit(train_dataset,
                               epochs=epochs,
                       steps_per_epoch=steps_per_epoch,
                      validation_steps=validation_steps,
                      validation_data=validation_dataset)
            self._plot_model_history(hist, 'loss', [0., 2.0])
            self._plot_model_history(hist, 'accuracy')
    
  17. 实现 _process_mask(),用于使分割掩膜与 OpenCV 兼容。这个函数的作用是创建一个三通道版本的灰度掩膜,并将类值上采样到 [0, 255] 范围:

        @staticmethod
        def _process_mask(mask):
            mask = (mask.numpy() * 127.5).astype('uint8')
            mask = cv2.cvtColor(mask, cv2.COLOR_GRAY2RGB)
            return mask
    
  18. _save_image_and_masks() 辅助方法创建了原始图像、真实标签掩膜和预测分割掩膜的马赛克图像,并将其保存到磁盘以便后续修订:

        def _save_image_and_masks(self, image,
                                  ground_truth_mask,
                                  prediction_mask,
                                  image_id):
            image = (image.numpy() * 255.0).astype('uint8')
            gt_mask = self._process_mask(ground_truth_mask)
            pred_mask = self._process_mask(prediction_mask)
            mosaic = np.hstack([image, gt_mask, pred_mask])
            mosaic = cv2.cvtColor(mosaic, cv2.COLOR_RGB2BGR)
            cv2.imwrite(f'mosaic_{image_id}.jpg', mosaic)
    
  19. 为了将网络输出的体积转换为有效的分割掩膜,我们必须在每个像素位置选择值最高的索引。这对应于该像素最可能的类别。_create_mask() 方法执行此操作:

        @staticmethod
        def _create_mask(prediction_mask):
            prediction_mask = tf.argmax(prediction_mask, 
                                         axis=-1)
            prediction_mask = prediction_mask[..., 
                                              tf.newaxis]
            return prediction_mask[0]
    
  20. _save_predictions() 方法使用了我们在 步骤 18 中定义的 _save_image_and_mask() 辅助方法:

        def _save_predictions(self, dataset, 
                               sample_size=1):
            for id, (image, mask) in \
                    enumerate(dataset.take(sample_size), 
                                            start=1):
                pred_mask = self.model.predict(image)
                pred_mask = self._create_mask(pred_mask)
                image = image[0]
                ground_truth_mask = mask[0]
                self._save_image_and_masks(image,
                                      ground_truth_mask,
                                           pred_mask,
                                           image_id=id)
    
  21. evaluate() 方法计算 FCN 在测试集上的准确率,并为一部分图像生成预测结果,然后将其保存到磁盘:

        def evaluate(self, test_dataset, sample_size=5):
            result = self.model.evaluate(test_dataset)
            print(f'Accuracy: {result[1] * 100:.2f}%')
            self._save_predictions(test_dataset, 
                                    sample_size)
    
  22. 使用 TensorFlow Datasets 下载(或加载,如果已缓存)Oxford IIIT Pet Dataset 及其元数据:

    dataset, info = tfdata.load('oxford_iiit_pet', 
                                 with_info=True)
    
  23. 使用元数据定义网络在训练和验证数据集上的步数。此外,还要定义批处理和缓冲区大小:

    TRAIN_SIZE = info.splits['train'].num_examples
    VALIDATION_SIZE = info.splits['test'].num_examples
    BATCH_SIZE = 32
    STEPS_PER_EPOCH = TRAIN_SIZE // BATCH_SIZE
    VALIDATION_SUBSPLITS = 5
    VALIDATION_STEPS = VALIDATION_SIZE // BATCH_SIZE
    VALIDATION_STEPS //= VALIDATION_SUBSPLITS
    BUFFER_SIZE = 1000
    
  24. 定义训练和测试数据集的管道:

    train_dataset = (dataset['train']
                     .map(load_image, num_parallel_
                     calls=AUTOTUNE)
                     .cache()
                     .shuffle(BUFFER_SIZE)
                     .batch(BATCH_SIZE)
                     .repeat()
                     .prefetch(buffer_size=AUTOTUNE))
    test_dataset = (dataset['test']
                    .map(lambda d: load_image(d,train=False),
                         num_parallel_calls=AUTOTUNE)
                    .batch(BATCH_SIZE))
    
  25. 实例化 FCN 并训练 120 个周期:

    fcn = FCN(output_channels=3)
    fcn.train(train_dataset,
              epochs=120,
              steps_per_epoch=STEPS_PER_EPOCH,
              validation_steps=VALIDATION_STEPS,
              validation_dataset=test_dataset)
    
  26. 最后,在测试数据集上评估网络:

    unet.evaluate(test_dataset)
    

    如下图所示,测试集上的准确率应约为 84%(具体来说,我得到的是 84.47%):

图 8.2 – 训练和验证准确率曲线

图 8.2 – 训练和验证准确率曲线

训练曲线显示出健康的行为,意味着网络确实学到了东西。然而,真正的考验是通过视觉评估结果:

图 8.3 – 原始图像(左),真实标签掩膜(中),预测掩膜(右)

图 8.3 – 原始图像(左),真实标签掩膜(中),预测掩膜(右)

在前面的图像中,我们可以看到,网络生成的掩膜跟真实标签分割的形状相符。然而,分割部分存在令人不满意的像素化效果,并且右上角有噪点。让我们看看另一个例子:

图 8.4 – 原始图像(左),真实标签掩膜(中),预测掩膜(右)

图 8.4 – 原始图像(左),真实标签掩膜(中),预测掩膜(右)

在上面的图像中,我们可以看到一个非常不完整、斑点状且总体质量较差的掩膜,这证明网络仍需要大量改进。这可以通过更多的微调和实验来实现。然而,在下一个食谱中,我们将发现一个更适合执行图像分割并能以更少的努力产生真正好掩膜的网络。

我们将在它是如何工作的…部分讨论我们刚刚做的事情。

它是如何工作的…

在本食谱中,我们实现了一个FCN用于图像分割。尽管我们将一个广为人知的架构VGG16进行了调整以适应我们的需求,实际上,FCN有许多不同的变种,它们扩展或修改了其他重要的架构,如ResNet50DenseNet以及其他VGG的变体。

我们需要记住的是,UpSampling2D()配合双线性插值或ConvTranspose2D()的使用。最终的结果是,我们不再用一个输出的概率向量来对整个图像进行分类,而是生成一个与输入图像相同尺寸的输出体积,其中每个像素包含它可能属于的各个类别的概率分布。这种像素级的预测分割掩膜就被称为预测分割掩膜。

另见

你可以了解更多关于Oxford-IIIT Pet Dataset的信息,访问官方站点:www.robots.ox.ac.uk/~vgg/data/pets/

从零开始实现 U-Net

要谈论图像分割,不能不提到U-Net,它是像素级分类的经典架构之一。

U-Net是一个由编码器和解码器组成的复合网络,正如其名,网络层以 U 形排列。它旨在快速且精确地进行分割,在本食谱中,我们将从零开始实现一个。

让我们开始吧,怎么样?

准备工作

在本例中,我们将依赖几个外部库,如 TensorFlow Datasets、TensorFlow Docs、PillowOpenCV。好消息是,我们可以通过pip轻松安装它们。首先,安装tensorflow_docs,如下所示:

$> pip install git+https://github.com/tensorflow/docs

接下来,安装其余的库:

$> pip install tensorflow-datasets Pillow opencv-contrib-python

在本食谱中,我们将使用Oxford-IIIT Pet Dataset。不过,现阶段我们不需要做任何事情,因为我们将通过tensorflow-datasets下载并操作它。在这个数据集中,分割掩膜(一个图像,每个位置包含原始图像中相应像素的类别)包含分类为三类的像素:

  • 1: 像素属于宠物(猫或狗)。

  • 2: 像素属于宠物的轮廓。

  • 3: 像素属于周围环境。

以下是数据集中的一些示例图像:

图 8.5 – 来自 Oxford-IIIT Pet 数据集的示例图像

图 8.5 – 来自 Oxford-IIIT Pet 数据集的示例图像

太好了!让我们开始实现吧!

如何实现…

按照以下步骤实现您自己的U-Net,这样您就可以对自己宠物的图像进行分割:

  1. 让我们导入所有必需的依赖项:

    import cv2
    import matplotlib.pyplot as plt
    import numpy as np
    import tensorflow as tf
    import tensorflow_datasets as tfdata
    import tensorflow_docs as tfdocs
    import tensorflow_docs.plots
    from tensorflow.keras.layers import *
    from tensorflow.keras.losses import \
        SparseCategoricalCrossentropy
    from tensorflow.keras.models import *
    from tensorflow.keras.optimizers import RMSprop
    
  2. 定义tf.data.experimental.AUTOTUNE的别名:

    AUTOTUNE = tf.data.experimental.AUTOTUNE 
    
  3. 定义一个函数,用于归一化数据集中的图像。我们还需要归一化掩膜,使得类别编号从 0 到 2,而不是从 1 到 3:

    def normalize(input_image, input_mask):
        input_image = tf.cast(input_image, tf.float32) / 255.0
        input_mask -= 1
        return input_image, input_mask
    
  4. 定义一个函数,根据 TensorFlow 数据集结构中的元素加载图像。请注意,我们将图像和掩膜的大小调整为256x256。此外,如果train标志设置为True,我们会通过随机镜像图像及其掩膜来进行数据增强。最后,我们对输入进行归一化处理:

    @tf.function
    def load_image(dataset_element, train=True):
      input_image = tf.image.resize(dataset element['image'],
                                      (256, 256))
        input_mask = tf.image.resize(
            dataset_element['segmentation_mask'],(256, 256))
        if train and np.random.uniform() > 0.5:
          input_image = tf.image.flip_left_right(input_image)
            input_mask = tf.image.flip_left_right(input_mask)
        input_image, input_mask = normalize(input_image,
                                            input_mask)
        return input_image, input_mask
    
  5. 现在,让我们定义一个UNet()类,它将包含构建、训练和评估所需的所有逻辑,使用RMSProp作为优化器,SparseCategoricalCrossentropy作为损失函数。请注意,output_channels默认为3,因为每个像素可以被分类为三类之一。

  6. 现在,让我们定义_downsample()助手方法,用于构建下采样块。它是一个卷积层,可以(可选地)进行批量归一化,并通过LeakyReLU激活:

        @staticmethod
        def _downsample(filters, size, batch_norm=True):
        initializer = tf.random_normal_initializer(0.0, 0.02)
            layers = Sequential()
            layers.add(Conv2D(filters=filters,
                              kernel_size=size,
                              strides=2,
                              padding='same',
                              kernel_initializer=initializer,
                              use_bias=False))
            if batch_norm:
                layers.add(BatchNormalization())
            layers.add(LeakyReLU())
            return layers
    
  7. 相反,_upsample()助手方法通过转置卷积扩展其输入,该卷积也进行批量归一化,并通过ReLU激活(可选地,我们可以添加一个 dropout 层来防止过拟合):

        def _upsample(filters, size, dropout=False):
            init = tf.random_normal_initializer(0.0, 0.02)
            layers = Sequential()
            layers.add(Conv2DTranspose(filters=filters,
                                       kernel_size=size,
                                       strides=2,
                                       padding='same',
                                 kernel_initializer=init,
                                       use_bias=False))
            layers.add(BatchNormalization())
            if dropout:
                layers.add(Dropout(rate=0.5))
            layers.add(ReLU())
            return layers
    
  8. 凭借_downsample()_upsample(),我们可以迭代地构建完整的U-Net架构。网络的编码部分只是一个下采样块的堆叠,而解码部分则如预期那样,由一系列上采样块组成:

        def _create_model(self):
            down_stack = [self._downsample(64, 4,
                                      batch_norm=False)]
            for filters in (128, 256, 512, 512, 512, 512, 
                              512):
                down_block = self._downsample(filters, 4)
                down_stack.append(down_block)
            up_stack = []
            for _ in range(3):
                up_block = self._upsample(512, 4, 
                                          dropout=True)
                up_stack.append(up_block)
            for filters in (512, 256, 128, 64):
                up_block = self._upsample(filters, 4)
                up_stack.append(up_block)
    
  9. 为了防止网络出现梯度消失问题(即深度网络遗忘已学内容的现象),我们必须在每个层级添加跳跃连接:

            inputs = Input(shape=self.input_size)
            x = inputs
            skip_layers = []
            for down in down_stack:
                x = down(x)
                skip_layers.append(x)
            skip_layers = reversed(skip_layers[:-1])
            for up, skip_connection in zip(up_stack, 
                                           skip_layers):
                x = up(x)
                x = Concatenate()([x, skip_connection])
    

    U-Net的输出层是一个转置卷积,其尺寸与输入图像相同,但它的通道数与分割掩膜中的类别数相同:

            init = tf.random_normal_initializer(0.0, 0.02)
            output = Conv2DTranspose(
                filters=self.output_channels,
                kernel_size=3,
                strides=2,
                padding='same',
                kernel_initializer=init)(x)
            return Model(inputs, outputs=output)
    
  10. 让我们定义一个helper方法,用于绘制相关的训练曲线:

        @staticmethod
        def _plot_model_history(model_history, metric, 
                                ylim=None):
            plt.style.use('seaborn-darkgrid')
            plotter = tfdocs.plots.HistoryPlotter()
            plotter.plot({'Model': model_history}, 
                               metric=metric)
            plt.title(f'{metric.upper()}')
            if ylim is None:
                plt.ylim([0, 1])
            else:
                plt.ylim(ylim)
            plt.savefig(f'{metric}.png')
            plt.close()
    
  11. train()方法接受训练和验证数据集,以及进行训练所需的轮次、训练和验证步数。它还会将损失和准确率图保存到磁盘,以供后续分析:

        def train(self, train_dataset, epochs, 
                         steps_per_epoch,
                  validation_dataset, validation_steps):
            hist = \
                self.model.fit(train_dataset,
                               epochs=epochs,
                        steps_per_epoch=steps_per_epoch,
                       validation_steps=validation_steps,
                     validation_data=validation_dataset)
            self._plot_model_history(hist, 'loss', [0., 2.0])
            self._plot_model_history(hist, 'accuracy')
    
  12. 定义一个名为_process_mask()的助手方法,用于将分割掩膜与 OpenCV 兼容。此函数的作用是创建一个三通道的灰度掩膜版本,并将类别值扩大到[0, 255]的范围:

        @staticmethod
        def _process_mask(mask):
            mask = (mask.numpy() * 127.5).astype('uint8')
            mask = cv2.cvtColor(mask, cv2.COLOR_GRAY2RGB)
            return mask
    
  13. _save_image_and_masks()助手方法会创建一个由原始图像、真实掩膜和预测分割掩膜组成的马赛克,并将其保存到磁盘,供以后修订:

        def _save_image_and_masks(self, image,
                                  ground_truth_mask,
                                  prediction_mask,
                                  image_id):
            image = (image.numpy() * 
                     255.0).astype('uint8')
            gt_mask = self._process_mask(ground_truth_mask)
            pred_mask = self._process_mask(prediction_mask)
            mosaic = np.hstack([image, gt_mask, pred_mask])
            mosaic = cv2.cvtColor(mosaic, cv2.COLOR_RGB2BGR)
            cv2.imwrite(f'mosaic_{image_id}.jpg', mosaic)
    
  14. 为了将网络产生的输出体积传递到有效的分割掩膜,我们必须获取每个像素位置上最高值的索引,这对应于该像素最可能的类别。_create_mask()方法执行了这个操作:

        @staticmethod
        def _create_mask(prediction_mask):
            prediction_mask = tf.argmax(prediction_mask, 
                                           axis=-1)
            prediction_mask = prediction_mask[...,tf.newaxis]
            return prediction_mask[0]
    

    _save_predictions()方法使用了我们在步骤 13中定义的_save_image_and_mask()辅助方法:

        def _save_predictions(self, dataset, 
                                sample_size=1):
            for id, (image, mask) in \
                    enumerate(dataset.take(sample_size), 
                                           start=1):
                pred_mask = self.model.predict(image)
                pred_mask = self._create_mask(pred_mask)
                image = image[0]
                ground_truth_mask = mask[0]
                self._save_image_and_masks(image,
                                      ground_truth_mask,
                                           pred_mask,
                                           image_id=id)
    
  15. evaluate()方法计算U-Net在测试集上的准确度,并为一些图像样本生成预测,之后将其存储到磁盘上:

        def evaluate(self, test_dataset, sample_size=5):
            result = self.model.evaluate(test_dataset)
            print(f'Accuracy: {result[1] * 100:.2f}%')
            self._save_predictions(test_dataset, 
                                   sample_size)
    
  16. 使用 TensorFlow Datasets 下载(或加载,如果已缓存)Oxford IIIT Pet Dataset及其元数据:

    dataset, info = tfdata.load('oxford_iiit_pet',
                                with_info=True)
    
  17. 使用元数据来定义网络在训练和验证数据集上将进行的相应步数。还需定义批量和缓冲区大小:

    TRAIN_SIZE = info.splits['train'].num_examples
    VALIDATION_SIZE = info.splits['test'].num_examples
    BATCH_SIZE = 64
    STEPS_PER_EPOCH = TRAIN_SIZE // BATCH_SIZE
    VALIDATION_SUBSPLITS = 5
    VALIDATION_STEPS = VALIDATION_SIZE // BATCH_SIZE
    VALIDATION_STEPS //= VALIDATION_SUBSPLITS
    BUFFER_SIZE = 1000
    
  18. 定义训练和测试数据集的管道:

    train_dataset = (dataset['train']
                     .map(load_image, num_parallel_
                      calls=AUTOTUNE)
                     .cache()
                     .shuffle(BUFFER_SIZE)
                     .batch(BATCH_SIZE)
                     .repeat()
                     .prefetch(buffer_size=AUTOTUNE))
    test_dataset = (dataset['test']
                    .map(lambda d: load_image(d, 
                      train=False),
                         num_parallel_calls=AUTOTUNE)
                    .batch(BATCH_SIZE))
    
  19. 实例化U-Net并训练 50 个 epoch:

    unet = UNet()
    unet.train(train_dataset,
               epochs=50,
               steps_per_epoch=STEPS_PER_EPOCH,
               validation_steps=VALIDATION_STEPS,
               validation_dataset=test_dataset)
    
  20. 最后,在测试数据集上评估网络:

    unet.evaluate(test_dataset)
    

    测试集上的准确度应该在 83%左右(在我的情况下,我得到了 83.49%):

图 8.6 – 训练和验证准确度曲线

图 8.6 – 训练和验证准确度曲线

在这里,我们可以看到,大约在第 12 个 epoch 之后,训练准确度曲线和验证准确度曲线之间的差距开始慢慢扩大。这不是过拟合的表现,而是表明我们可以做得更好。那么,这个准确度是如何转化为实际图像的呢?

看一下下面的图片,展示了原始图像、地面真实掩膜和生成的掩膜:

图 8.7 – 原始图像(左侧)、地面真实掩膜(中间)和预测掩膜(右侧)

图 8.7 – 原始图像(左侧)、地面真实掩膜(中间)和预测掩膜(右侧)

在这里,我们可以看到,地面真实掩膜(中间)和预测掩膜(右侧)之间有很好的相似性,尽管存在一些噪声,比如小的白色区域和狗轮廓下半部分明显的隆起,这些噪声通过更多的训练可以清理掉:

图 8.8 – 原始图像(左侧)、地面真实掩膜(中间)和预测掩膜(右侧)

图 8.8 – 原始图像(左侧)、地面真实掩膜(中间)和预测掩膜(右侧)

前面的图片清楚地表明,网络可以进行更多的训练或微调。这是因为尽管它正确地获取了狗的整体形状和位置,但掩膜中仍有太多噪声,导致其无法在实际应用中使用。

让我们前往它是如何工作的……部分,进一步连结各个环节。

它是如何工作的……

在这个示例中,我们从头开始实现并训练了一个U-Net,用于分割家庭宠物的身体和轮廓。正如我们所看到的,网络确实学到了东西,但仍然有改进的空间。

在多个领域中,语义分割图像内容的能力至关重要,例如在医学中,比知道是否存在病症(如恶性肿瘤)更重要的是确定病变的实际位置、形状和面积。U-Net首次亮相于生物医学领域。2015 年,它在使用远少于数据的情况下,超越了传统的分割方法,例如滑动窗口卷积网络。

U-Net是如何取得如此好结果的呢?正如我们在本食谱中所学到的,关键在于其端到端的结构,其中编码器和解码器都由卷积组成,形成一个收缩路径,其任务是捕捉上下文信息,还有一个对称的扩展路径,从而实现精确的定位。

上述两条路径的深度可以根据数据集的性质进行调整。这种深度定制之所以可行,是因为跳跃连接的存在,它允许梯度在网络中进一步流动,从而防止梯度消失问题(这与ResNet所做的类似,正如我们在第二章中学到的,执行图像分类)。

在下一个食谱中,我们将结合这个强大的概念与Oxford IIIT Pet Dataset的实现:迁移学习。

另见

一个很好的方法来熟悉Oxford IIIT Pet Dataset,可以访问官方网站:www.robots.ox.ac.uk/~vgg/data/pets/

在本食谱中,我们提到过梯度消失问题几次,因此,阅读这篇文章以了解这一概念是个好主意:en.wikipedia.org/wiki/Vanishing_gradient_problem

实现带迁移学习的 U-Net

从头开始训练一个U-Net是创建高性能图像分割系统的一个非常好的第一步。然而,深度学习在计算机视觉中的最大超能力之一就是能够在其他网络的知识基础上构建解决方案,这通常会带来更快且更好的结果。

图像分割也不例外,在本食谱中,我们将使用迁移学习来实现一个更好的分割网络。

让我们开始吧。

准备就绪

本食谱与前一个食谱(从头开始实现 U-Net)非常相似,因此我们只会深入讨论不同的部分。为了更深入的理解,我建议在尝试本食谱之前,先完成从头开始实现 U-Net的食谱。正如预期的那样,我们需要的库与之前相同,都可以通过pip安装。让我们首先安装tensorflow_docs,如下所示:

$> pip install git+https://github.com/tensorflow/docs

现在,让我们设置剩余的依赖项:

$> pip install tensorflow-datasets Pillow opencv-contrib-python

我们将再次使用Oxford-IIIT Pet Dataset,可以通过tensorflow-datasets访问。该数据集中的每个像素都属于以下类别之一:

  • 1:该像素属于宠物(猫或狗)。

  • 2:该像素属于宠物的轮廓。

  • 3:该像素属于周围环境。

以下图片展示了数据集中的两张样本图像:

图 8.9 – 来自 Oxford-IIIT Pet 数据集的样本图像

](https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/tf20-cv-cb/img/B14768_08_009.jpg)

图 8.9 – 来自 Oxford-IIIT Pet 数据集的样本图像

这样,我们就可以开始了!

如何实现……

完成以下步骤以实现一个基于转移学习的U-Net

  1. 导入所有需要的包:

    import cv2
    import matplotlib.pyplot as plt
    import numpy as np
    import tensorflow as tf
    import tensorflow_datasets as tfdata
    import tensorflow_docs as tfdocs
    import tensorflow_docs.plots
    from tensorflow.keras.applications import MobileNetV2
    from tensorflow.keras.layers import *
    from tensorflow.keras.losses import \
        SparseCategoricalCrossentropy
    from tensorflow.keras.models import *
    from tensorflow.keras.optimizers import RMSprop
    
  2. tf.data.experimental.AUTOTUNE定义一个别名:

    AUTOTUNE = tf.data.experimental.AUTOTUNE 
    
  3. 定义一个函数,用于对数据集中的图像和掩码进行归一化处理:

    def normalize(input_image, input_mask):
        input_image = tf.cast(input_image, tf.float32) / 
                                       255.0
        input_mask -= 1
        return input_image, input_mask
    
  4. 定义一个函数,根据 TensorFlow Datasets 数据结构中的一个元素加载图像及其对应的掩码。可选地,函数可以对训练图像执行图像镜像操作:

    @tf.function
    def load_image(dataset_element, train=True):
        input_image = tf.image.resize(dataset
                               element['image'],(256, 256))
        input_mask = tf.image.resize(
            dataset_element['segmentation_mask'], (256,256))
        if train and np.random.uniform() > 0.5:
            input_image = tf.image.flip_left_right(input_
                                                  image)
            input_mask = 
               tf.image.flip_left_right(input_mask)
        input_image, input_mask = normalize(input_image,
                                            input_mask)
        return input_image, input_mask
    
  5. 定义UNet(),这是一个容器类,包含了构建、训练和评估我们的转移学习辅助的RMSProp优化器和SparseCategoricalCrossentropy损失函数所需的逻辑。注意,output_channels默认值为3,因为每个像素可以归类为三种类别之一。编码器将使用预训练的MobileNetV2,但我们只会使用其中一部分层,这些层在self.target_layers中定义。

  6. 现在,我们来定义_upsample()辅助方法,构建一个上采样模块:

        @staticmethod
        def _upsample(filters, size, dropout=False):
            init = tf.random_normal_initializer(0.0, 0.02)
            layers = Sequential()
            layers.add(Conv2DTranspose(filters=filters,
                                       kernel_size=size,
                                       strides=2,
                                       padding='same',
                               kernel_initializer=init,
                                       use_bias=False))
            layers.add(BatchNormalization())
            if dropout:
                layers.add(Dropout(rate=0.5))
            layers.add(ReLU())
            return layers
    
  7. 利用我们预训练的MobileNetV2_upsample()方法,我们可以逐步构建完整的self.target_layers,这些层被冻结(down_stack.trainable = False),这意味着我们只训练解码器或上采样模块:

        def _create_model(self):
          layers = [self.pretrained_model.get_layer(l).output
                      for l in self.target_layers]
        down_stack = Model(inputs=self.pretrained_model.
                                  input, outputs=layers)
            down_stack.trainable = False
            up_stack = []
            for filters in (512, 256, 128, 64):
                up_block = self._upsample(filters, 4)
                up_stack.append(up_block)
    
  8. 现在,我们可以添加跳跃连接,以促进梯度在网络中的流动:

            inputs = Input(shape=self.input_size)
            x = inputs
            skip_layers = down_stack(x)
            x = skip_layers[-1]
            skip_layers = reversed(skip_layers[:-1])
            for up, skip_connection in zip(up_stack, 
                                           skip_layers):
                x = up(x)
                x = Concatenate()([x, skip_connection])
    
  9. U-Net的输出层是一个转置卷积,其尺寸与输入图像相同,但通道数与分割掩码中的类别数相同:

            init = tf.random_normal_initializer(0.0, 0.02)
            output = Conv2DTranspose(
                filters=self.output_channels,
                kernel_size=3,
                strides=2,
                padding='same',
                kernel_initializer=init)(x)
            return Model(inputs, outputs=output)
    
  10. 定义_plot_model_history(),一个辅助方法,用于绘制相关的训练曲线:

        @staticmethod
        def _plot_model_history(model_history, metric, 
                                 ylim=None):
            plt.style.use('seaborn-darkgrid')
            plotter = tfdocs.plots.HistoryPlotter()
            plotter.plot({'Model': model_history}, 
                            metric=metric)
            plt.title(f'{metric.upper()}')
            if ylim is None:
                plt.ylim([0, 1])
            else:
                plt.ylim(ylim)
            plt.savefig(f'{metric}.png')
            plt.close()
    
  11. 定义train()方法,负责拟合模型:

        def train(self, train_dataset, epochs, 
                      steps_per_epoch,
                  validation_dataset, validation_steps):
            hist = \
                self.model.fit(train_dataset,
                             epochs=epochs,
                            steps_per_epoch=steps_per_epoch,
                           validation_steps=validation_steps,
                          validation_data=validation_dataset)
            self._plot_model_history(hist, 'loss', [0., 2.0])
            self._plot_model_history(hist, 'accuracy')
    
  12. 定义_process_mask(),一个辅助方法,使分割掩码与 OpenCV 兼容:

        @staticmethod
        def _process_mask(mask):
            mask = (mask.numpy() * 127.5).astype('uint8')
            mask = cv2.cvtColor(mask, cv2.COLOR_GRAY2RGB)
            return mask
    
  13. 定义_save_image_and_masks()辅助方法,用于创建原始图像的可视化,以及真实和预测的掩码:

        def _save_image_and_masks(self, image,
                                  ground_truth_mask,
                                  prediction_mask,
                                  image_id):
            image = (image.numpy() * 255.0).astype('uint8')
            gt_mask = self._process_mask(ground_truth_mask)
            pred_mask = self._process_mask(prediction_mask)
            mosaic = np.hstack([image, gt_mask, pred_mask])
            mosaic = cv2.cvtColor(mosaic, cv2.COLOR_RGB2BGR)
            cv2.imwrite(f'mosaic_{image_id}.jpg', mosaic)
    
  14. 定义_create_mask(),该方法根据网络的预测生成有效的分割掩码:

        @staticmethod
        def _create_mask(prediction_mask):
            prediction_mask = tf.argmax(prediction_mask, 
                                       axis=-1)
            prediction_mask = prediction_mask[..., 
                                          tf.newaxis]
            return prediction_mask[0]
    
  15. _save_predictions()方法使用了我们在第 13 步中定义的_save_image_and_mask()辅助方法:

        def _save_predictions(self, dataset, 
                              sample_size=1):
            for id, (image, mask) in \
                    enumerate(dataset.take(sample_size), 
                                start=1):
                pred_mask = self.model.predict(image)
                pred_mask = self._create_mask(pred_mask)
                image = image[0]
                ground_truth_mask = mask[0]
                self._save_image_and_masks(image,
                                      ground_truth_mask,
                                           pred_mask,
                                           image_id=id)
    
  16. evaluate()方法计算U-Net在测试集上的准确度,并生成一组图像样本的预测。预测结果随后会被存储到磁盘上:

        def evaluate(self, test_dataset, sample_size=5):
            result = self.model.evaluate(test_dataset)
            print(f'Accuracy: {result[1] * 100:.2f}%')
            self._save_predictions(test_dataset, sample_size)
    
  17. 使用 TensorFlow Datasets 下载(或加载缓存的)Oxford IIIT Pet Dataset及其元数据:

    dataset, info = tfdata.load('oxford_iiit_pet',
                                with_info=True)
    
  18. 使用元数据定义网络在训练和验证数据集上将执行的步骤数。还要定义批量大小和缓存大小:

    TRAIN_SIZE = info.splits['train'].num_examples
    VALIDATION_SIZE = info.splits['test'].num_examples
    BATCH_SIZE = 64
    STEPS_PER_EPOCH = TRAIN_SIZE // BATCH_SIZE
    VALIDATION_SUBSPLITS = 5
    VALIDATION_STEPS = VALIDATION_SIZE // BATCH_SIZE
    VALIDATION_STEPS //= VALIDATION_SUBSPLITS
    BUFFER_SIZE = 1000
    
  19. 定义训练和测试数据集的管道:

    train_dataset = (dataset['train']
                     .map(load_image, num_parallel_
                      calls=AUTOTUNE)
                     .cache()
                     .shuffle(BUFFER_SIZE)
                     .batch(BATCH_SIZE)
                     .repeat()
                     .prefetch(buffer_size=AUTOTUNE))
    test_dataset = (dataset['test']
                    .map(lambda d: load_image(d, 
                         train=False),
                         num_parallel_calls=AUTOTUNE)
                    .batch(BATCH_SIZE))
    
  20. 实例化U-Net并训练它 30 个周期:

    unet = UNet()
    unet.train(train_dataset,
               epochs=50,
               steps_per_epoch=STEPS_PER_EPOCH,
               validation_steps=VALIDATION_STEPS,
               validation_dataset=test_dataset)
    
  21. 在测试数据集上评估网络:

    unet.evaluate(test_dataset)
    

    在测试集上的准确率应该接近 90%(在我的案例中,我得到了 90.78%的准确率):

图 8.10 – 训练和验证准确率曲线

图 8.10 – 训练和验证准确率曲线

准确率曲线显示,网络没有发生过拟合,因为训练和验证图表遵循相同的轨迹,且差距非常小。这也确认了模型所获得的知识是可迁移的,并且可以用于未见过的数据。

让我们看一下网络的一些输出,从以下图像开始:

图 8.11 – 原始图像(左)、真实标签掩码(中)和预测掩码(右)

图 8.11 – 原始图像(左)、真实标签掩码(中)和预测掩码(右)

图 8.7从头实现 U-Net一节中的结果相比,在前面的图像中,我们可以看到U-Net产生了一个更干净的结果,背景(灰色像素)、轮廓(白色像素)和宠物(黑色像素)被清晰地分开,并且几乎与真实标签掩码(中)完全相同:

图 8.12 – 原始图像(左)、真实标签掩码(中)和预测掩码(右)

图 8.12 – 原始图像(左)、真实标签掩码(中)和预测掩码(右)

从头实现 U-Net一节中的图 8.8相比,前面的图像是一个显著的改进。这次,预测掩码(右),虽然不是完美的,但呈现出更少的噪声,并且更接近实际的分割掩码(中)。

我们将在它是如何工作的…一节中深入探讨。

它是如何工作的……

在这个例子中,我们对在庞大的ImageNet数据集上训练的MobileNetV2做了一个小但重要的改动。

迁移学习在这个场景中效果如此出色的原因是,ImageNet中有成百上千的类别,专注于不同品种的猫和狗,这意味着与Oxford IIIT Pet数据集的重叠非常大。然而,如果情况并非如此,这并不意味着我们应该完全放弃迁移学习!我们在这种情况下应该做的是通过使编码器的某些(或全部)层可训练,来微调它。

通过利用MobileNetV2中编码的知识,我们将测试集上的准确率从 83%提升到了 90%,这是一项令人印象深刻的提升,带来了更好、更清晰的预测掩码,即使是在具有挑战性的例子上。

另见

你可以阅读原始的Oxford IIIT Pet Dataset,请访问www.robots.ox.ac.uk/~vgg/data/pets/。想了解如何解决梯度消失问题,请阅读这篇文章:en.wikipedia.org/wiki/Vanishing_gradient_problem

使用 Mask-RCNN 和 TensorFlow Hub 进行图像分割

COCO数据集。这将帮助我们进行开箱即用的物体检测和图像分割。

准备工作

首先,我们必须安装PillowTFHub,如下所示:

$> pip install Pillow tensorflow-hub

我们还需要将cd安装到你选择的位置,并克隆tensorflow/models仓库:

$> git clone –-depth 1 https://github.com/tensorflow/models

接下来,安装TensorFlow 对象检测 API,方法如下:

$> sudo apt install -y protobuf-compiler
$> cd models/research
$> protoc object_detection/protos/*.proto --python_out=.
$> cp object_detection/packages/tf2/setup.py .
$> python -m pip install -q . 

就这样!让我们开始吧。

如何做到这一点…

按照以下步骤学习如何使用Mask-RCNN进行图像分割:

  1. 导入必要的包:

    import glob
    from io import BytesIO
    import matplotlib.pyplot as plt
    import numpy as np
    import tensorflow as tf
    import tensorflow_hub as hub
    from PIL import Image
    from object_detection.utils import ops
    from object_detection.utils import visualization_utils as viz
    from object_detection.utils.label_map_util import \
        create_category_index_from_labelmap
    
  2. 定义一个函数,将图像加载到 NumPy 数组中:

    def load_image(path):
        image_data = tf.io.gfile.GFile(path, 'rb').read()
        image = Image.open(BytesIO(image_data))
        width, height = image.size
        shape = (1, height, width, 3)
        image = np.array(image.getdata())
        image = image.reshape(shape).astype('uint8')
        return image
    
  3. 定义一个函数,使用Mask-RCNN进行预测,并将结果保存到磁盘。首先加载图像并将其输入到模型中:

    def get_and_save_predictions(model, image_path):
        image = load_image(image_path)
        results = model(image)
    
  4. 将结果转换为NumPy数组:

    model_output = {k: v.numpy() for k, v in 
                    results.items()}
    
  5. 从模型输出中提取检测掩膜和框,并将它们转换为张量:

      detection_masks = model_output['detection_masks'][0]
     detection_masks = tf.convert_to_tensor(detection_masks)
     detection_boxes = model_output['detection_boxes'][0]
     detection_boxes = tf.convert_to_tensor(detection_boxes)
    
  6. 将框掩膜转换为图像掩膜:

        detection_masks_reframed = \
         ops.reframe_box_masks_to_image_masks(detection_
                                    masks,detection_boxes,
                                     image.shape[1],
                                   image.shape[2])
        detection_masks_reframed = \
            tf.cast(detection_masks_reframed > 0.5, 
                     tf.uint8)
        model_output['detection_masks_reframed'] = \
            detection_masks_reframed.numpy()
    
  7. 创建一个可视化图,显示检测结果及其框、得分、类别和掩膜:

        boxes = model_output['detection_boxes'][0]
        classes = \
           model_output['detection_classes'][0].astype('int')
        scores = model_output['detection_scores'][0]
        masks = model_output['detection_masks_reframed']
        image_with_mask = image.copy()
        viz.visualize_boxes_and_labels_on_image_array(
            image=image_with_mask[0],
            boxes=boxes,
            classes=classes,
            scores=scores,
            category_index=CATEGORY_IDX,
            use_normalized_coordinates=True,
            max_boxes_to_draw=200,
            min_score_thresh=0.30,
            agnostic_mode=False,
            instance_masks=masks,
            line_thickness=5
        )
    
  8. 将结果保存到磁盘:

        plt.figure(figsize=(24, 32))
        plt.imshow(image_with_mask[0])
        plt.savefig(f'output/{image_path.split("/")[-1]}')
    
  9. 加载COCO数据集的类别索引:

    labels_path = 'resources/mscoco_label_map.pbtxt'
    CATEGORY_IDX =create_category_index_from_labelmap(labels_path)
    
  10. TFHub加载Mask-RCNN

    MODEL_PATH = ('https://tfhub.dev/tensorflow/mask_rcnn/'
                  'inception_resnet_v2_1024x1024/1')
    mask_rcnn = hub.load(MODEL_PATH)
    
  11. 运行output文件夹。让我们回顾一个简单的例子:

图 8.13– 单实例分割

图 8.13– 单实例分割

在这里,我们可以看到网络正确地检测并分割出了狗,准确率为 100%!我们来尝试一个更具挑战性的例子:

图 8.14 – 多实例分割

图 8.14 – 多实例分割

这张图片比之前的更拥挤,尽管如此,网络仍然正确地识别出了场景中的大部分物体(如汽车、人、卡车等)——即使是被遮挡的物体!然而,模型在某些情况下仍然失败,如下图所示:

图 8.15 – 带有错误和冗余的分割

图 8.15 – 带有错误和冗余的分割

这次,网络正确地识别了我和我的狗,还有咖啡杯和沙发,但它却出现了重复和荒谬的检测结果,比如我的腿被识别为一个人。这是因为我抱着我的狗,照片中我的身体部分被分离,导致了不正确或置信度低的分割。

让我们继续下一个部分。

它是如何工作的…

在这篇教程中,我们学习了如何使用现有最强大的神经网络之一:Mask-RCNN,来检测物体并执行图像分割。训练这样的模型并非易事,更不用说从零开始实现了!幸运的是,得益于TensorFlow Hub,我们能够通过仅几行代码使用它的全部预测能力。

我们必须考虑到,这个预训练模型在包含网络已训练过的物体的图像上表现最好。更具体地说,我们传递给COCO的图像越多,结果越好。不过,为了实现最佳检测效果,仍然需要一定程度的调整和实验,因为正如我们在前面的例子中看到的,虽然网络非常强大,但并不完美。

另见

您可以在这里了解我们使用的模型:tfhub.dev/tensorflow/mask_rcnn/inception_resnet_v2_1024x1024/1。此外,阅读Mask-RCNN的论文也是一个明智的决定:arxiv.org/abs/1703.06870

第九章:第九章:通过目标检测在图像中定位元素

目标检测是计算机视觉中最常见但最具挑战性的任务之一。它是图像分类的自然演变,我们的目标是识别图像中的内容。另一方面,目标检测不仅关注图像的内容,还关注数字图像中感兴趣元素的位置。

与计算机视觉中的许多其他知名任务一样,目标检测已经通过各种技术得到解决,从简单的解决方案(如目标匹配)到基于机器学习的解决方案(如 Haar 级联)。尽管如此,如今最有效的检测器都由深度学习驱动。

从零开始实现最先进的目标检测器(如YOLO(一次看全)YOLO)和快速区域卷积神经网络Fast R-CNN))是一个非常具有挑战性的任务。然而,我们可以利用许多预训练的解决方案,不仅可以进行预测,还可以从零开始训练我们自己的模型,正如本章所介绍的那样。

这里列出了我们将要快速处理的配方:

  • 使用图像金字塔和滑动窗口创建目标检测器

  • 使用 YOLOv3 进行目标检测

  • 使用 TensorFlow 的目标检测应用程序编程接口API)训练你自己的目标检测器

  • 使用TensorFlow HubTFHub)进行目标检测

技术要求

鉴于目标检测器的复杂性,拥有图形处理单元GPU)是个不错的选择。你可以使用许多云服务商来运行本章中的配方,我个人最喜欢的是 FloydHub,但你可以使用你最喜欢的任何服务!当然,如果你不想有意外费用,记得关注费用问题!在准备工作部分,你将找到每个配方的准备步骤。本章的代码可以在github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch9上找到。

查看以下链接,观看代码实战视频:

bit.ly/39wInla

使用图像金字塔和滑动窗口创建目标检测器

传统上,目标检测器通过一种迭代算法工作,该算法将窗口以不同的尺度滑过图像,以检测每个位置和视角下的潜在目标。尽管这种方法因其明显的缺点(我们将在工作原理...部分中进一步讨论)而已过时,但它的一个重要优点是它对我们使用的图像分类器类型没有偏见,这意味着我们可以将其作为一个框架,将任何分类器转变为目标检测器。这正是我们在第一个配方中所做的!

让我们开始吧。

准备工作

我们需要安装一些外部库,比如OpenCVPillowimutils,可以通过以下命令轻松完成:

$> pip install opencv-contrib-python Pillow imutils

我们将使用一个预训练模型来为我们的物体检测器提供支持,因此我们不需要为此食谱提供任何数据。

如何实现…

按照以下步骤完成食谱:

  1. 导入必要的依赖项:

    import cv2
    import imutils
    import numpy as np
    from tensorflow.keras.applications import imagenet_utils
    from tensorflow.keras.applications.inception_resnet_v2 \
        import *
    from tensorflow.keras.preprocessing.image import img_to_array
    
  2. 接下来,定义我们的ObjectDetector()类,从构造函数开始:

    class ObjectDetector(object):
        def __init__(self, 
                     classifier,
                     preprocess_fn=lambda x: x,
                     input_size=(299, 299),
                     confidence=0.98,
                     window_step_size=16,
                     pyramid_scale=1.5,
                     roi_size=(200, 150),
                     nms_threshold=0.3):
            self.classifier = classifier
            self.preprocess_fn = preprocess_fn
            self.input_size = input_size
            self.confidence = confidence
            self.window_step_size = window_step_size
            self.pyramid_scale = pyramid_scale
            self.roi_size = roi_size
            self.nms_threshold = nms_threshold
    

    classifier只是一个经过训练的网络,我们将用它来分类每个窗口,而preprocess_fn是用于处理每个窗口的函数,在将其传递给分类器之前进行处理。confidence是我们允许检测结果的最低概率,只有达到这个概率才能认为检测结果有效。剩余的参数将在下一步中解释。

  3. 现在,我们定义一个sliding_window()方法,该方法提取输入图像的部分区域,尺寸等于self.roi_size。它将在图像上水平和垂直滑动,每次移动self.window_step_size像素(注意使用了yield而不是return——这是因为它是一个生成器):

        def sliding_window(self, image):
            for y in range(0,
                           image.shape[0],
                           self.window_step_size):
                for x in range(0,
                               image.shape[1],
                               self.window_step_size):
                  y_slice = slice(y, y + self.roi_size[1], 1)
                  x_slice = slice(x, x + self.roi_size[0], 1)
                    yield x, y, image[y_slice, x_slice]
    
  4. 接下来,定义pyramid()方法,该方法会生成输入图像的越来越小的副本,直到达到最小尺寸(类似于金字塔的各个层级):

        def pyramid(self, image):
            yield image
            while True:
                width = int(image.shape[1] / 
                         self.pyramid_scale)
                image = imutils.resize(image, width=width)
                if (image.shape[0] < self.roi_size[1] or
                        image.shape[1] < 
                      self.roi_size[0]):
                    break
                yield image
    
  5. 因为在不同尺度上滑动窗口会很容易产生与同一物体相关的多个检测结果,我们需要一种方法来将重复项保持在最低限度。这就是我们下一个方法non_max_suppression()的作用:

        def non_max_suppression(self, boxes, probabilities):
            if len(boxes) == 0:
                return []
            if boxes.dtype.kind == 'i':
                boxes = boxes.astype(np.float)
            pick = []
            x_1 = boxes[:, 0]
            y_1 = boxes[:, 1]
            x_2 = boxes[:, 2]
            y_2 = boxes[:, 3]
            area = (x_2 - x_1 + 1) * (y_2 - y_1 + 1)
            indexes = np.argsort(probabilities)
    
  6. 我们首先计算所有边界框的面积,并按概率升序对它们进行排序。接下来,我们将选择具有最高概率的边界框的索引,并将其添加到最终选择中(pick),直到剩下indexes个边界框需要进行修剪:

            while len(indexes) > 0:
                last = len(indexes) - 1
                i = indexes[last]
                pick.append(i)
    
  7. 我们计算选中的边界框与其他边界框之间的重叠部分,然后剔除那些重叠部分超过self.nms_threshold的框,这意味着它们很可能指的是同一个物体:

                xx_1 = np.maximum(x_1[i],x_1[indexes[:last]])
                yy_1 = np.maximum(y_1[i],y_1[indexes[:last]])
                xx_2 = np.maximum(x_2[i],x_2[indexes[:last]])
                yy_2 = np.maximum(y_2[i],y_2[indexes[:last]])
                width = np.maximum(0, xx_2 - xx_1 + 1)
                height = np.maximum(0, yy_2 - yy_1 + 1)
                overlap = (width * height) / 
                          area[indexes[:last]]
                redundant_boxes = \
                    np.where(overlap > 
                            self.nms_threshold)[0]
                to_delete = np.concatenate(
                    ([last], redundant_boxes))
                indexes = np.delete(indexes, to_delete)
    
  8. 返回选中的边界框:

            return boxes[pick].astype(np.int)
    
  9. detect()方法将物体检测算法串联在一起。我们首先定义一个rois列表及其对应的locations(在原始图像中的坐标):

        def detect(self, image):
            rois = []
            locations = []
    
  10. 接下来,我们将使用pyramid()生成器在多个尺度上生成输入图像的不同副本,并在每个层级上,我们将通过sliding_windows()生成器滑动窗口,提取所有可能的 ROI:

            for img in self.pyramid(image):
                scale = image.shape[1] / 
                        float(img.shape[1])
                for x, y, roi_original in \
                        self.sliding_window(img):
                    x = int(x * scale)
                    y = int(y * scale)
                    w = int(self.roi_size[0] * scale)
                    h = int(self.roi_size[1] * scale)
                    roi = cv2.resize(roi_original, 
                                     self.input_size)
                    roi = img_to_array(roi)
                    roi = self.preprocess_fn(roi)
                    rois.append(roi)
                    locations.append((x, y, x + w, y + h))
            rois = np.array(rois, dtype=np.float32)
    
  11. 一次性通过分类器传递所有的 ROI:

            predictions = self.classifier.predict(rois)
            predictions = \
           imagenet_utils.decode_predictions(predictions, 
                                                  top=1)
    
  12. 构建一个dict来将分类器生成的每个标签映射到所有的边界框及其概率(注意我们只保留那些概率至少为self.confidence的边界框):

            labels = {}
            for i, pred in enumerate(predictions):
                _, label, proba = pred[0]
                if proba >= self.confidence:
                    box = locations[i]
                    label_detections = labels.get(label, [])
                    label_detections.append({'box': box,
                                             'proba': 
                                              proba})
                    labels[label] = label_detections
            return labels
    
  13. 实例化一个在 ImageNet 上训练的InceptionResnetV2网络,作为我们的分类器,并将其传递给新的ObjectDetector。注意,我们还将preprocess_function作为输入传递:

    model = InceptionResNetV2(weights='imagenet',
                              include_top=True)
    object_detector = ObjectDetector(model, preprocess_input)
    
  14. 加载输入图像,将其最大宽度调整为 600 像素(高度将相应计算以保持宽高比),并通过物体检测器进行处理:

    image = cv2.imread('dog.jpg')
    image = imutils.resize(image, width=600)
    labels = object_detector.detect(image)
    
  15. 遍历所有对应每个标签的检测结果,首先绘制所有边界框:

    GREEN = (0, 255, 0)
    for i, label in enumerate(labels.keys()):
        clone = image.copy()
        for detection in labels[label]:
            box = detection['box']
            probability = detection['proba']
            x_start, y_start, x_end, y_end = box
            cv2.rectangle(clone, (x_start, y_start),
                          (x_end, y_end), (0, 255, 0), 2)
        cv2.imwrite(f'Before_{i}.jpg', clone)
    

    然后,使用非最大抑制NMS)去除重复项,并绘制剩余的边界框:

        clone = image.copy()
        boxes = np.array([d['box'] for d in 
                       labels[label]])
        probas = np.array([d['proba'] for d in 
                        labels[label]])
        boxes = object_detector.non_max_suppression(boxes,
                                                  probas)
        for x_start, y_start, x_end, y_end in boxes:
            cv2.rectangle(clone, (x_start, y_start),
                          (x_end, y_end), GREEN, 2)
    
            if y_start - 10 > 10:
                y = y_start - 10
            else:
                y = y_start + 10
    
            cv2.putText(clone, label, (x_start, y),
                        cv2.FONT_HERSHEY_SIMPLEX, .45,
                        GREEN, 2)
        cv2.imwrite(f'After_{i}.jpg', clone)
    

    这是没有应用 NMS 的结果:

图 9.1 – 同一只狗的重叠检测

图 9.1 – 同一只狗的重叠检测

这是应用 NMS 后的结果:

图 9.2 – 使用 NMS 后,我们去除了冗余的检测

图 9.2 – 使用 NMS 后,我们去除了冗余的检测

尽管我们在前面的照片中成功检测到了狗,但我们注意到边界框并没有像我们预期的那样紧密包裹住物体。让我们在接下来的章节中讨论这个问题以及传统物体检测方法的其他问题。

它是如何工作的…

在这个方案中,我们实现了一个可重用的类,利用迭代方法在不同的视角层次(图像金字塔)提取 ROI(滑动窗口),并将其传递给图像分类器,从而确定照片中物体的位置和类别。我们还使用了非最大抑制(NMS)来减少这种策略所特有的冗余和重复检测。

尽管这是创建对象检测器的一个很好的初步尝试,但它仍然存在一些缺陷:

  • 它非常慢,这使得它在实时场景中不可用。

  • 边界框的准确性很大程度上取决于图像金字塔、滑动窗口和 ROI 大小的参数选择。

  • 该架构不是端到端可训练的,这意味着边界框预测中的误差不会通过网络反向传播,以便通过更新权重来产生更好、更准确的检测结果。相反,我们只能使用预训练模型,这些模型仅限于推断,而无法学习,因为框架不允许它们学习。

然而,别急着排除这种方法!如果你处理的图像在尺寸和视角上变化很小,且你的应用程序绝对不在实时环境中运行,那么本方案中实现的策略可能会对你的项目大有裨益!

另见

你可以在这里阅读更多关于 NMS 的内容:

towardsdatascience.com/non-maximum-suppression-nms-93ce178e177c

使用 YOLOv3 检测物体

使用图像金字塔和滑动窗口创建物体检测器的实例中,我们学会了如何通过将任何图像分类器嵌入到依赖于图像金字塔和滑动窗口的传统框架中,来将其转变为物体检测器。然而,我们也学到,这种方法并不理想,因为它无法让网络从错误中学习。

深度学习之所以在物体检测领域占据主导地位,是因为它的端到端方法。网络不仅能弄清楚如何对物体进行分类,还能发现如何生成最佳的边界框来定位图像中的每个元素。

基于这个端到端的策略,网络可以在一次遍历中检测到无数个物体!当然,这也使得这样的物体检测器极为高效!

YOLO 是开创性的端到端物体检测器之一,在这个实例中,我们将学习如何使用预训练的 YOLOv3 模型进行物体检测。

我们开始吧!

准备工作

首先安装tqdm,如下所示:

$> pip install tqdm

我们的实现深受精彩的keras-yolo3库的启发,该库由Huynh Ngoc Anh(GitHub 上的 experiencor)实现,你可以在这里查看:

github.com/experiencor/keras-yolo3

因为我们将使用预训练的 YOLO 模型,所以需要下载权重文件。它们可以在这里获取:pjreddie.com/media/files/yolov3.weights。在本教程中,我们假设这些权重文件位于伴随代码库中的ch9/recipe2/resources文件夹内,名为yolov3.weights。这些权重与 YOLO 的原作者使用的是相同的。更多关于 YOLO 的内容,请参考另见部分。

一切准备就绪!

如何操作…

按照以下步骤完成该实例:

  1. 首先导入相关的依赖:

    import glob
    import json
    import struct
    import matplotlib.pyplot as plt
    import numpy as np
    import tqdm
    from matplotlib.patches import Rectangle
    from tensorflow.keras.layers import *
    from tensorflow.keras.models import *
    from tensorflow.keras.preprocessing.image import *
    
  2. 定义一个WeightReader()类,自动加载 YOLO 的权重,无论原作者使用了什么格式。请注意,这是一个非常底层的解决方案,但我们不需要完全理解它就可以加以利用。让我们从构造函数开始:

    class WeightReader:
        def __init__(self, weight_file):
            with open(weight_file, 'rb') as w_f:
                major, = struct.unpack('i', w_f.read(4))
                minor, = struct.unpack('i', w_f.read(4))
                revision, = struct.unpack('i', w_f.read(4))
                if (major * 10 + minor) >= 2 and \
                        major < 1000 and \
                        minor < 1000:
                    w_f.read(8)
                else:
                    w_f.read(4)
                binary = w_f.read()
            self.offset = 0
            self.all_weights = np.frombuffer(binary,
                                         dtype='float32')
    
  3. 接下来,定义一个方法来从weights文件中读取指定数量的字节:

        def read_bytes(self, size):
            self.offset = self.offset + size
            return self.all_weights[self.offset-
                                   size:self.offset]
    
  4. load_weights()方法加载了组成 YOLO 架构的 106 层每一层的权重:

        def load_weights(self, model):
            for i in tqdm.tqdm(range(106)):
                try:
                    conv_layer = model.get_layer(f'conv_{i}')
                    if i not in [81, 93, 105]:
                        norm_layer = 
                 model.get_layer(f'bnorm_{i}')
                        size = np.prod(norm_layer.
    
                                  get_weights()[0].shape)
                        bias = self.read_bytes(size)
                        scale = self.read_bytes(size)
                        mean = self.read_bytes(size)
                        var = self.read_bytes(size)
                        norm_layer.set_weights([scale, 
                                                bias, mean, 
                                                var])
    
  5. 加载卷积层的权重:

                    if len(conv_layer.get_weights()) > 1:
                        bias = self.read_bytes(np.prod(
                       conv_layer.get_weights()[1].shape))
                        kernel = self.read_bytes(np.prod(
                       conv_layer.get_weights()[0].shape))
                        kernel = 
                      kernel.reshape(list(reversed(
                    conv_layer.get_weights()[0].shape)))
                        kernel = kernel.transpose([2, 3, 
                                                   1, 0])
                        conv_layer.set_weights([kernel, 
                                                bias])
                    else:
                        kernel = self.read_bytes(np.prod(
                      conv_layer.get_weights()[0].shape))
                        kernel = 
                   kernel.reshape(list(reversed(
    
                conv_layer.get_weights()[0].shape)))
                      kernel = kernel.transpose([2, 3, 1, 0])
                        conv_layer.set_weights([kernel])
                except ValueError:
                    pass
    
  6. 定义一个方法来重置偏移量:

        def reset(self):
            self.offset = 0
    
  7. 定义一个BoundBox()类,封装边界框的顶点,以及该框中元素为物体的置信度(objness):

    class BoundBox(object):
        def __init__(self, x_min, y_min, x_max, y_max,
                     objness=None,
                     classes=None):
            self.xmin = x_min
            self.ymin = y_min
            self.xmax = x_max
            self.ymax = y_max
            self.objness = objness
            self.classes = classes
            self.label = -1
            self.score = -1
        def get_label(self):
            if self.label == -1:
                self.label = np.argmax(self.classes)
            return self.label
        def get_score(self):
            if self.score == -1:
                self.score = self.classes[self.get_label()]
            return self.score
    
  8. 定义一个YOLO()类,封装网络的构建和检测逻辑。让我们从构造函数开始:

    class YOLO(object):
        def __init__(self, weights_path,
                     anchors_path='resources/anchors.json',
                     labels_path='resources/coco_labels.txt',
                     class_threshold=0.65):
            self.weights_path = weights_path
            self.model = self._load_yolo()
            self.labels = []
            with open(labels_path, 'r') as f:
                for l in f:
                    self.labels.append(l.strip())
            with open(anchors_path, 'r') as f:
                self.anchors = json.load(f)
            self.class_threshold = class_threshold
    

    YOLO 的输出是一组在锚框上下文中定义的编码边界框,这些锚框是由 YOLO 的作者精心挑选的。这是基于对 COCO 数据集中物体大小的分析。因此,我们将锚框存储在 self.anchors 中,COCO 的标签存储在 self.labels 中。此外,我们依赖于 self._load_yolo() 方法(稍后定义)来构建模型。

  9. YOLO 由一系列卷积块和可选的跳跃连接组成。 _conv_block() 辅助方法允许我们轻松地实例化这些块:

        def _conv_block(self, input, convolutions, 
                       skip=True):
            x = input
            count = 0
            for conv in convolutions:
                if count == (len(convolutions) - 2) and 
                    skip:
                    skip_connection = x
                count += 1
                if conv['stride'] > 1:
                    x = ZeroPadding2D(((1, 0), (1, 0)))(x)
                x = Conv2D(conv['filter'],
                           conv['kernel'],
                           strides=conv['stride'],
                           padding=('valid' if 
                           conv['stride'] > 1
                                    else 'same'),
    
                 name=f'conv_{conv["layer_idx"]}',
                           use_bias=(False if 
                               conv['bnorm']
                                     else True))(x)
    
  10. 检查是否需要添加批量归一化、leaky ReLU 激活和跳跃连接:

                if conv['bnorm']:
                    name = f'bnorm_{conv["layer_idx"]}'
                    x = BatchNormalization(epsilon=1e-3,
                                           name=name)(x)
                if conv['leaky']:
                    name = f'leaky_{conv["layer_idx"]}'
                    x = LeakyReLU(alpha=0.1, name=name)(x)
            return Add()([skip_connection, x]) if skip else x
    
  11. _make_yolov3_architecture() 方法,如下所示,通过堆叠一系列卷积块来构建 YOLO 网络,使用先前定义的 _conv_block() 方法:

        def _make_yolov3_architecture(self):
            input_image = Input(shape=(None, None, 3))
            # Layer  0 => 4
            x = self._conv_block(input_image, [
                {'filter': 32, 'kernel': 3, 'stride': 1,
                 'bnorm': True,
                 'leaky': True, 'layer_idx': 0},
                {'filter': 64, 'kernel': 3, 'stride': 2,
                 'bnorm': True,
                 'leaky': True, 'layer_idx': 1},
                {'filter': 32, 'kernel': 1, 'stride': 1,
                 'bnorm': True,
                 'leaky': True, 'layer_idx': 2},
                {'filter': 64, 'kernel': 3, 'stride': 1,
                 'bnorm': True,
                 'leaky': True, 'layer_idx': 3}])
    ...
    

    因为这个方法比较大,请参考附带的代码库获取完整实现。

  12. _load_yolo() 方法创建架构、加载权重,并实例化一个 TensorFlow 可理解的训练过的 YOLO 模型:

        def _load_yolo(self):
            model = self._make_yolov3_architecture()
            weight_reader = WeightReader(self.weights_path)
            weight_reader.load_weights(model)
            model.save('model.h5')
            model = load_model('model.h5')
            return model
    
  13. 定义一个静态方法来计算张量的 Sigmoid 值:

        @staticmethod
        def _sigmoid(x):
            return 1.0 / (1.0 + np.exp(-x))
    
  14. _decode_net_output() 方法解码 YOLO 产生的候选边界框和类别预测:

        def _decode_net_output(self, 
                               network_output,
                               anchors,
                               obj_thresh,
                               network_height,
                               network_width):
          grid_height, grid_width = network_output.shape[:2]
            nb_box = 3
            network_output = network_output.reshape(
                (grid_height, grid_width, nb_box, -1))
            boxes = []
            network_output[..., :2] = \
                self._sigmoid(network_output[..., :2])
            network_output[..., 4:] = \
                self._sigmoid(network_output[..., 4:])
            network_output[..., 5:] = \
                (network_output[..., 4][..., np.newaxis] *
                 network_output[..., 5:])
            network_output[..., 5:] *= \
                network_output[..., 5:] > obj_thresh
            for i in range(grid_height * grid_width):
                r = i / grid_width
                c = i % grid_width
    
  15. 我们跳过那些不能自信地描述物体的边界框:

                for b in range(nb_box):
                    objectness = \
                        network_output[int(r)][int(c)][b][4]
                    if objectness.all() <= obj_thresh:
                        continue
    
  16. 我们从网络输出中提取坐标和类别,并使用它们来创建 BoundBox() 实例:

                    x, y, w, h = \
                        network_output[int(r)][int(c)][b][:4]
                    x = (c + x) / grid_width
                    y = (r + y) / grid_height
                    w = (anchors[2 * b] * np.exp(w) /
                         network_width)
                    h = (anchors[2 * b + 1] * np.exp(h) /
                         network_height)
                   classes = network_output[int(r)][c][b][5:]
                    box = BoundBox(x_min=x - w / 2,
                                   y_min=y - h / 2,
                                   x_max=x + w / 2,
                                   y_max=y + h / 2,
                                   objness=objectness,
                                   classes=classes)
                    boxes.append(box)
            return boxes
    
  17. _correct_yolo_boxes() 方法将边界框调整为原始图像的尺寸:

        @staticmethod
        def _correct_yolo_boxes(boxes,
                                image_height,
                                image_width,
                                network_height,
                                network_width):
            new_w, new_h = network_width, network_height
            for i in range(len(boxes)):
                x_offset = (network_width - new_w) / 2.0
                x_offset /= network_width
                x_scale = float(new_w) / network_width
                y_offset = (network_height - new_h) / 2.0
                y_offset /= network_height
                y_scale = float(new_h) / network_height
                boxes[i].xmin = int((boxes[i].xmin - x_     
                                        offset) /
                                    x_scale * image_width)
                boxes[i].xmax = int((boxes[i].xmax - x_
                                 offset) /x_scale * image_
                                            width)
                boxes[i].ymin = int((boxes[i].ymin - y_
                                    offset) /
                                    y_scale * image_height)
                boxes[i].ymax = int((boxes[i].ymax - y_
                                     offset) /
                                    y_scale * image_height)
    
  18. 我们稍后会执行 NMS,以减少冗余的检测。为此,我们需要一种计算两个区间重叠量的方法:

        @staticmethod
        def _interval_overlap(interval_a, interval_b):
            x1, x2 = interval_a
            x3, x4 = interval_b
            if x3 < x1:
                if x4 < x1:
                    return 0
                else:
                    return min(x2, x4) - x1
            else:
                if x2 < x3:
                    return 0
                else:
                    return min(x2, x4) - x3
    
  19. 接下来,我们可以计算前面定义的 _interval_overlap() 方法:

        def _bbox_iou(self, box1, box2):
            intersect_w = self._interval_overlap(
                [box1.xmin, box1.xmax],
                [box2.xmin, box2.xmax])
            intersect_h = self._interval_overlap(
                [box1.ymin, box1.ymax],
                [box2.ymin, box2.ymax])
            intersect = intersect_w * intersect_h
            w1, h1 = box1.xmax - box1.xmin, box1.ymax - box1.ymin
            w2, h2 = box2.xmax - box2.xmin, box2.ymax - box2.ymin
            union = w1 * h1 + w2 * h2 - intersect
            return float(intersect) / union
    
  20. 有了这些方法,我们可以对边界框应用 NMS,从而将重复检测的数量降到最低:

        def _non_max_suppression(self, boxes, nms_thresh):
            if len(boxes) > 0:
                nb_class = len(boxes[0].classes)
            else:
                return
            for c in range(nb_class):
                sorted_indices = np.argsort(
                    [-box.classes[c] for box in boxes])
                for i in range(len(sorted_indices)):
                    index_i = sorted_indices[i]
                    if boxes[index_i].classes[c] == 0:
                        continue
                    for j in range(i + 1, 
                    len(sorted_indices)):
                        index_j = sorted_indices[j]
                        iou = self._bbox_iou(boxes[index_i],
    
                        boxes[index_j])
                        if iou >= nms_thresh:
                            boxes[index_j].classes[c] = 0
    
  21. _get_boxes() 方法仅保留那些置信度高于构造函数中定义的 self.class_threshold 方法(默认值为 0.6 或 60%)的框:

        def _get_boxes(self, boxes):
            v_boxes, v_labels, v_scores = [], [], []
            for box in boxes:
                for i in range(len(self.labels)):
                    if box.classes[i] > 
                   self.class_threshold:
                        v_boxes.append(box)
                        v_labels.append(self.labels[i])
                        v_scores.append(box.classes[i] * 
                                          100)
            return v_boxes, v_labels, v_scores
    
  22. _draw_boxes() 在输入图像中绘制最自信的检测结果,这意味着每个边界框都会显示其类别标签及其置信度:

        @staticmethod
        def _draw_boxes(filename, v_boxes, v_labels, 
                        v_scores):
            data = plt.imread(filename)
            plt.imshow(data)
            ax = plt.gca()
            for i in range(len(v_boxes)):
                box = v_boxes[i]
                y1, x1, y2, x2 = \
                    box.ymin, box.xmin, box.ymax, box.xmax
                width = x2 - x1
                height = y2 - y1
                rectangle = Rectangle((x1, y1), width, 
                                     height,
                                      fill=False, 
                                   color='white')
                ax.add_patch(rectangle)
                label = f'{v_labels[i]} ({v_scores[i]:.3f})'
                plt.text(x1, y1, label, color='green')
            plt.show()
    
  23. YOLO() 类中的唯一公共方法是 detect(),它实现了端到端的逻辑,用于检测输入图像中的物体。首先,它将图像传入模型:

        def detect(self, image, width, height):
            image = np.expand_dims(image, axis=0)
            preds = self.model.predict(image)
            boxes = []
    
  24. 然后,它解码网络的输出:

            for i in range(len(preds)):
                boxes.extend(
                    self._decode_net_output(preds[i][0],
                                        self.anchors[i],
                                    self.class_threshold,
                                            416,
                                            416))
    
  25. 接下来,它修正这些框,使它们与输入图像的比例正确。它还应用 NMS 来去除冗余的检测结果:

            self._correct_yolo_boxes(boxes, height, width, 
                                     416,
                                     416)
            self._non_max_suppression(boxes, .5)
    
  26. 最后,它获取有效的边界框,并将其绘制到输入图像中:

            valid_boxes, valid_labels, valid_scores = \
                self._get_boxes(boxes)
            for i in range(len(valid_boxes)):
                print(valid_labels[i], valid_scores[i])
            self._draw_boxes(image_path,
                             valid_boxes,
                             valid_labels,
                             valid_scores)
    
  27. 定义了 YOLO() 类后,我们可以按如下方式实例化它:

    model = YOLO(weights_path='resources/yolov3.weights')
    
  28. 最后一步是遍历所有测试图像,并在其上运行模型:

    for image_path in glob.glob('test_images/*.jpg'):
        image = load_img(image_path, target_size=(416, 
                                                  416))
        image = img_to_array(image)
        image = image.astype('float32') / 255.0
        original_image = load_img(image_path)
        width, height = original_image.size
        model.detect(image, width, height)
    

    这是第一个示例:

![图 9.3 – YOLO 检测到狗,具有非常高的置信度]

](https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/tf20-cv-cb/img/B14768_09_003.jpg)

图 9.3 – YOLO 以非常高的置信度检测到了这只狗

我们可以观察到,YOLO 非常自信地检测到了我的狗,并且置信度高达 94.5%!太棒了!接下来看看第二张测试图像:

图 9.4 – YOLO 在一次处理过程中检测到了不同尺度的多个物体

图 9.4 – YOLO 在一次处理过程中检测到了不同尺度的多个物体

尽管结果很拥挤,但快速一瞥便能看出,网络成功识别了前景中的两辆车,以及背景中的人。这是一个有趣的例子,因为它展示了 YOLO 作为端到端物体检测器的强大能力,它能够在一次处理过程中对许多不同的物体进行分类和定位,且尺度各异。是不是很令人印象深刻?

我们前往如何工作...部分,来连接这些点。

如何工作…

在这个配方中,我们发现了端到端物体检测器的巨大威力——特别是其中最著名和最令人印象深刻的一个:YOLO。

尽管 YOLO 最初是用 C++ 实现的,但我们利用了Huynh Ngoc Anh 的精彩 Python 适配,使用这个架构的预训练版本(特别是第 3 版)在开创性的 COCO 数据集上进行物体检测。

正如你可能已经注意到的那样,YOLO 和许多其他端到端物体检测器都是非常复杂的网络,但它们相对于传统方法(如图像金字塔和滑动窗口)有明显的优势。结果不仅更好,而且得益于 YOLO 能够一次性查看输入图像并产生所有相关检测的能力,处理速度也更快。

但如果你想在自己的数据上训练一个端到端的物体检测器呢?难道你只能依赖现成的解决方案吗?你需要花费数小时去解读难懂的论文,才能实现这些网络吗?

好吧,那是一个选项,但还有另一个,我们将在下一个配方中探讨,它涉及 TensorFlow 物体检测 API,这是一个实验性仓库,汇集了最先进的架构,能够简化并提升你的物体检测工作!

另见

YOLO 是深度学习和物体检测领域的一个里程碑,因此阅读这篇论文是一个非常明智的时间投资。你可以在这里找到它:

arxiv.org/abs/1506.02640

你可以直接从作者的网站了解更多关于 YOLO 的信息,网址如下:

pjreddie.com/darknet/yolo/

如果你有兴趣了解我们基于其实现的keras-yolo3工具,可以参考这个链接:

github.com/experiencor/keras-yolo3

使用 TensorFlow 的物体检测 API 训练你自己的物体检测器

现代物体检测器无疑是实现和调试最复杂、最具挑战性的架构之一!然而,这并不意味着我们不能利用这个领域的最新进展,在我们自己的数据集上训练物体检测器。怎么做?你问。那就让我们来了解一下 TensorFlow 的物体检测 API!

在这个食谱中,我们将安装这个 API,准备一个自定义数据集进行训练,调整几个配置文件,并使用训练好的模型在测试图像上定位物体。这个食谱与你之前做过的有所不同,因为我们将在 Python 和命令行之间来回切换。

你准备好了吗?那就让我们开始吧。

准备工作

有几个依赖项需要安装才能使这个食谱工作。让我们从最重要的开始:TensorFlow 物体检测 API。首先,cd到你喜欢的位置并克隆 tensorflow/models 仓库:

$> git clone –-depth 1 https://github.com/tensorflow/models

接下来,像这样安装 TensorFlow 物体检测 API:

$> sudo apt install -y protobuf-compiler
$> cd models/research
$> protoc object_detection/protos/*.proto –-python_out=.
$> cp object_detection/packages/tf2/setup.py .
$> python -m pip install -q . 

就本食谱而言,我们假设它与ch9文件夹位于同一层级(https://github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch9)。现在,我们必须安装pandasPillow

$> pip install pandas Pillow

我们将使用的数据集是Fruit Images for Object Detection,托管在 Kaggle 上,你可以通过以下链接访问:www.kaggle.com/mbkinaci/fruit-images-for-object-detection。登录或注册后,下载数据并保存到你喜欢的位置,文件名为fruits.zip(数据可以在本书配套仓库的ch9/recipe3文件夹中找到)。最后,解压缩它:

图 9.5 – 数据集中三类样本图像:苹果、橙子和香蕉

图 9.5 – 数据集中三类样本图像:苹果、橙子和香蕉

这个数据集的标签采用Pascal VOC格式,其中VOC代表视觉物体类别。请参考另见…部分了解更多信息。

现在,我们准备好了!让我们开始实现。

如何进行操作……

完成这些步骤后,你将使用 TensorFlow 物体检测 API 训练出你自己的最先进物体检测器:

  1. 在这个食谱中,我们将处理两个文件:第一个用于准备数据(你可以在仓库中找到它,名为prepare.py),第二个用于使用物体检测器进行推理(在仓库中为inference.py)。打开prepare.py并导入所有需要的包:

    import glob
    import io
    import os
    from collections import namedtuple
    from xml.etree import ElementTree as tree
    import pandas as pd
    import tensorflow.compat.v1 as tf
    from PIL import Image
    from object_detection.utils import dataset_util
    
  2. 定义encode_class()函数,将文本标签映射到它们的整数表示:

    def encode_class(row_label):
        class_mapping = {'apple': 1, 'orange': 2, 
                         'banana': 3}
        return class_mapping.get(row_label, None)
    
  3. 定义一个函数,将标签的数据框(我们稍后会创建)拆分成组:

    def split(df, group):
        Data = namedtuple('data', ['filename', 'object'])
        groups = df.groupby(group)
        return [Data(filename, groups.get_group(x))
                for filename, x
                in zip(groups.groups.keys(), 
              groups.groups)]
    
  4. TensorFlow 目标检测 API 使用一种名为tf.train.Example的数据结构。下一个函数接收图像的路径及其标签(即包含的所有对象的边界框集和真实类别),并创建相应的tf.train.Example。首先,加载图像及其属性:

    def create_tf_example(group, path):
        groups_path = os.path.join(path, f'{group.filename}')
        with tf.gfile.GFile(groups_path, 'rb') as f:
            encoded_jpg = f.read()
        image = Image.open(io.BytesIO(encoded_jpg))
        width, height = image.size
        filename = group.filename.encode('utf8')
        image_format = b'jpg'
    
  5. 现在,存储边界框的维度以及图像中每个对象的类别:

        xmins = []
        xmaxs = []
        ymins = []
        ymaxs = []
        classes_text = []
        classes = []
        for index, row in group.object.iterrows():
            xmins.append(row['xmin'] / width)
            xmaxs.append(row['xmax'] / width)
            ymins.append(row['ymin'] / height)
            ymaxs.append(row['ymax'] / height)
            classes_text.append(row['class'].encode('utf8'))
            classes.append(encode_class(row['class']))
    
  6. 创建一个tf.train.Features对象,包含图像及其对象的相关信息:

        features = tf.train.Features(feature={
            'image/height':
                dataset_util.int64_feature(height),
            'image/width':
                dataset_util.int64_feature(width),
            'image/filename':
                dataset_util.bytes_feature(filename),
            'image/source_id':
                dataset_util.bytes_feature(filename),
            'image/encoded':
                dataset_util.bytes_feature(encoded_jpg),
            'image/format':
                dataset_util.bytes_feature(image_format),
            'image/object/bbox/xmin':
                dataset_util.float_list_feature(xmins),
            'image/object/bbox/xmax':
                dataset_util.float_list_feature(xmaxs),
            'image/object/bbox/ymin':
                dataset_util.float_list_feature(ymins),
            'image/object/bbox/ymax':
                dataset_util.float_list_feature(ymaxs),
            'image/object/class/text':
               dataset_util.bytes_list_feature(classes_text),
            'image/object/class/label':
                dataset_util.int64_list_feature(classes)
        })
    
  7. 返回一个用先前创建的特征初始化的tf.train.Example结构:

        return tf.train.Example(features=features)
    
  8. 定义一个函数,将包含图像边界框信息的可扩展标记语言XML)文件转换为等效的逗号分隔值CSV)格式文件:

    def bboxes_to_csv(path):
        xml_list = []
        bboxes_pattern = os.path.sep.join([path, '*.xml'])
        for xml_file in glob.glob(bboxes_pattern):
            t = tree.parse(xml_file)
            root = t.getroot()
            for member in root.findall('object'):
                value = (root.find('filename').text,
                         int(root.find('size')[0].text),
                         int(root.find('size')[1].text),
                         member[0].text,
                         int(member[4][0].text),
                         int(member[4][1].text),
                         int(member[4][2].text),
                         int(member[4][3].text))
                xml_list.append(value)
        column_names = ['filename', 'width', 'height', 
                'class','xmin', 'ymin', 'xmax', 'ymax']
        df = pd.DataFrame(xml_list, columns=column_names)
        return df
    
  9. 遍历fruits文件夹中的testtrain子集,将标签从 CSV 转换为 XML:

    base = 'fruits'
    for subset in ['test', 'train']:
        folder = os.path.sep.join([base, f'{subset}_zip', 
                                   subset])
        labels_path = os.path.sep.join([base,f'{subset}_
                                           labels.           
                                           csv'])
        bboxes_df = bboxes_to_csv(folder)
        bboxes_df.to_csv(labels_path, index=None)
    
  10. 然后,使用相同的标签生成与当前正在处理的数据子集对应的tf.train.Examples

        writer = (tf.python_io.
                TFRecordWriter(f'resources/{subset}.record'))
        examples = pd.read_csv(f'fruits/{subset}_labels.csv')
        grouped = split(examples, 'filename')
        path = os.path.join(f'fruits/{subset}_zip/{subset}')
        for group in grouped:
            tf_example = create_tf_example(group, path)
            writer.write(tf_example.SerializeToString())
        writer.close()
    
  11. 在运行第 1 步第 10 步中实现的prepare.py脚本后,你将获得适合 TensorFlow 目标检测 API 训练的数据形状。下一步是下载EfficientDet的权重,这是我们将要微调的最先进架构。从Desktop文件夹下载权重。

  12. 创建一个文件,将类别映射到整数。命名为label_map.txt并将其放在ch9/recipe3/resources中:

    item {
        id: 1
        name: 'apple'
    }
    item {
        id: 2
        name: 'orange'
    }
    item {
        id: 3
        name: 'banana'
    }
    
  13. 接下来,我们必须更改该网络的配置文件,以使其适应我们的数据集。你可以将其放置在models/research/object_detection/configs/tf2/ssd_efficientdet_d0_512x512_coco17_tpu-8.config(假设你已将 TensorFlow 目标检测 API 安装在与ch9文件夹同一级别的伴随库中),或者直接从以下网址下载:github.com/tensorflow/models/blob/master/research/object_detection/configs/tf2/ssd_efficientdet_d0_512x512_coco17_tpu-8.config。无论你选择哪种方式,请将文件复制到ch9/recipe3/resources中,并修改第 13 行,以反映我们数据集中类别的数量:

    num_classes: 3
    

    然后,修改第 140 行,使其指向我们在第 7 步中下载的EfficientDet权重:

    fine_tune_checkpoint: "/home/jesus/Desktop/efficientdet_d0_coco17_tpu-32/checkpoint/ckpt-0"
    

    第 143 行fine_tune_checkpoint_typeclassification改为detection

    fine_tune_checkpoint_type: "detection"
    

    修改第 180 行,使其指向第 8 步中创建的label_map.txt文件:

    label_map_path: "/home/jesus/Desktop/tensorflow-computer-vision/ch9/recipe3/resources/label_map.txt"
    

    修改第 182 行,使其指向第 11 步中创建的train.record文件,该文件对应于已准备好的训练数据:

    input_path: "/home/jesus/Desktop/tensorflow-computer-vision/ch9/recipe3/resources/train.record"
    

    修改第 193 行,使其指向第 12 步中创建的label_map.txt文件:

    label_map_path: "/home/jesus/Desktop/tensorflow-computer-vision/ch9/recipe3/resources/label_map.txt"
    

    修改第 197 行,使其指向第 11 步中创建的test.record文件,该文件对应于已准备好的测试数据:

    input_path: "/home/jesus/Desktop/tensorflow-computer-vision/ch9/recipe3/resources/test.record"
    
  14. 到了训练模型的时候!首先,假设你在配套仓库的根目录下,cd进入 TensorFlow 对象检测 API 中的object_detection文件夹:

    $> cd models/research/object_detection
    

    然后,使用以下命令训练模型:

    $> python model_main_tf2.py --pipeline_config_path=../../../ch9/recipe3/resources/ssd_efficientdet_d0_512x512_coco17_tpu-8.config --model_dir=../../../ch9/recipe3/training --num_train_steps=10000
    

    在这里,我们正在训练模型进行10000步训练。此外,我们将把结果保存在ch9/recipe3中的training文件夹内。最后,我们通过--pipeline_config_path选项指定配置文件的位置。这个步骤将持续几个小时。

  15. 一旦网络进行了精调,我们必须将其导出为冻结图,以便用于推理。为此,再次cd进入 TensorFlow 对象检测 API 中的object_detection文件夹:

    $> cd models/research/object_detection
    

    现在,执行以下命令:

    $> python exporter_main_v2.py --trained_checkpoint_dir=../../../ch9/recipe3/training/ --pipeline_config_path=../../../ch9/recipe3/resources/ssd_efficientdet_d0_512x512_coco17_tpu-8.config --output_directory=../../../ch9/recipe3/resources/inference_graph
    

    trained_checkpoint_dir参数用于指定训练好的模型所在的位置,而pipeline_config_path则指向模型的配置文件。最后,冻结的推理图将保存在ch9/recipe3/resources/inference_graph文件夹中,正如output_directory标志所指定的那样。

  16. 打开一个名为inference.py的文件,并导入所有相关的依赖项:

    import glob
    import random
    from io import BytesIO
    import matplotlib.pyplot as plt
    import numpy as np
    import tensorflow as tf
    from PIL import Image
    from object_detection.utils import ops
    from object_detection.utils import visualization_utils as viz
    from object_detection.utils.label_map_util import \
        create_category_index_from_labelmap
    
  17. 定义一个函数,从磁盘加载图像并将其转换为 NumPy 数组:

    def load_image(path):
        image_data = tf.io.gfile.GFile(path, 'rb').read()
        image = Image.open(BytesIO(image_data))
        width, height = image.size
        shape = (height, width, 3)
        image = np.array(image.getdata())
        image = image.reshape(shape).astype('uint8')
        return image
    
  18. 定义一个函数,在单张图像上运行模型。首先,将图像转换为张量:

    def infer_image(net, image):
        image = np.asarray(image)
        input_tensor = tf.convert_to_tensor(image)
        input_tensor = input_tensor[tf.newaxis, ...]
    
  19. 将张量传递给网络,提取检测的数量,并在结果字典中保留与检测数量相等的值:

        num_detections = int(result.pop('num_detections'))
        result = {key: value[0, :num_detections].numpy()
                  for key, value in result.items()}
        result['num_detections'] = num_detections
        result['detection_classes'] = \
            result['detection_classes'].astype('int64')
    
  20. 如果有检测掩膜存在,将它们重框为图像掩膜并返回结果:

        if 'detection_masks' in result:
            detection_masks_reframed = \
                ops.reframe_box_masks_to_image_masks(
                    result['detection_masks'],
                    result['detection_boxes'],
                    image.shape[0],
                    image.shape[1])
            detection_masks_reframed = \
                tf.cast(detection_masks_reframed > 0.5, 
                        tf.uint8)
            result['detection_masks_reframed'] = \
                detection_masks_reframed.numpy()
        return result
    
  21. 从我们在步骤 12中创建的label_map.txt文件中创建类别索引,同时从步骤 15中生成的冻结推理图中加载模型:

    labels_path = 'resources/label_map.txt'
    CATEGORY_IDX = \
        create_category_index_from_labelmap(labels_path,
                                      use_display_name=True)
    model_path = 'resources/inference_graph/saved_model'
    model = tf.saved_model.load(model_path)
    
  22. 随机选择三张测试图像:

    test_images = list(glob.glob('fruits/test_zip/test/*.jpg'))
    random.shuffle(test_images)
    test_images = test_images[:3]
    
  23. 在样本图像上运行模型,并保存结果检测:

    for image_path in test_images:
        image = load_image(image_path)
        result = infer_image(model, image)
        masks = result.get('detection_masks_reframed', 
                            None)
        viz.visualize_boxes_and_labels_on_image_array(
            image,
            result['detection_boxes'],
            result['detection_classes'],
            result['detection_scores'],
            CATEGORY_IDX,
            instance_masks=masks,
            use_normalized_coordinates=True,
            line_thickness=5)
        plt.figure(figsize=(24, 32))
        plt.imshow(image)
        plt.savefig(f'detections_{image_path.split("/")[-1]}')
    

    我们可以在图 9.6中看到结果:

图 9.6 – EfficientDet 在随机样本测试图像上的检测结果

图 9.6 – EfficientDet 在随机样本测试图像上的检测结果

我们可以在图 9.6中看到,我们精调后的网络产生了相当准确且自信的检测结果。考虑到我们仅关注数据准备和推理,并且在架构方面我们只是根据需要调整了配置文件,结果相当令人印象深刻!

让我们继续阅读如何工作...部分。

如何工作...

在本食谱中,我们发现训练一个物体检测器是一个艰难且富有挑战的任务。然而,好消息是,我们可以使用 TensorFlow 对象检测 API 来训练各种前沿网络。

由于 TensorFlow 物体检测 API 是一个实验性工具,它使用与常规 TensorFlow 不同的约定,因此,为了使用它,我们需要对输入数据进行一些处理,将其转化为 API 可以理解的格式。这是通过将Fruits for Object Detection数据集中的标签(最初是 XML 格式)转换为 CSV,再转为序列化的tf.train.Example对象来完成的。

然后,为了使用训练好的模型,我们通过exporter_main_v2.py脚本将其导出为推理图,并利用 API 中的一些可视化工具显示样本测试图像上的检测结果。

那么,训练呢?可以说这是最简单的部分,包含三个主要步骤:

  • 创建从文本标签到整数的映射(步骤 12

  • 修改与模型对应的配置文件,以便在所有相关位置进行微调(步骤 13

  • 运行model_main_tf2.py文件来训练网络,并传递正确的参数(步骤 14

这个方案为你提供了一个模板,你可以对其进行调整和适应,以便在任何你选择的数据集上训练几乎所有现代物体检测器(API 支持的)。相当酷,对吧?

另见

你可以在这里了解更多关于 TensorFlow 物体检测 API 的信息:

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

此外,我鼓励你阅读这篇精彩的文章,了解更多关于EfficientDet的信息:

towardsdatascience.com/a-thorough-breakdown-of-efficientdet-for-object-detection-dc6a15788b73

如果你想深入了解Pascal VOC格式,那么你一定要观看这个视频:

www.youtube.com/watch?v=-f6TJpHcAeM

使用 TFHub 进行物体检测

TFHub 是物体检测领域的一个丰富宝库,充满了最先进的模型。正如我们在这个方案中将发现的那样,使用它们来识别图像中的感兴趣元素是一项相当直接的任务,尤其是考虑到它们已经在庞大的COCO数据集上进行了训练,这使得它们成为现成物体检测的绝佳选择。

准备工作

首先,我们必须安装Pillow和 TFHub,步骤如下:

$> pip install Pillow tensorflow-hub

此外,由于我们将使用的一些可视化工具位于 TensorFlow 物体检测 API 中,我们必须先安装它。首先,cd到你喜欢的位置,并克隆tensorflow/models仓库:

$> git clone –-depth 1 https://github.com/tensorflow/models

接下来,安装 TensorFlow 物体检测 API,像这样:

$> sudo apt install -y protobuf-compiler
$> cd models/research
$> protoc object_detection/protos/*.proto –-python_out=.
$> cp object_detection/packages/tf2/setup.py .
$> python -m pip install -q . 

就是这样!让我们开始吧。

如何操作……

按照以下步骤学习如何使用 TFHub 检测你自己照片中的物体:

  1. 导入我们需要的包:

    import glob
    from io import BytesIO
    import matplotlib.pyplot as plt
    import numpy as np
    import tensorflow as tf
    import tensorflow_hub as hub
    from PIL import Image
    from object_detection.utils import visualization_utils as viz
    from object_detection.utils.label_map_util import \
        create_category_index_from_labelmap
    
  2. 定义一个函数,将图像加载到 NumPy 数组中:

    def load_image(path):
        image_data = tf.io.gfile.GFile(path, 'rb').read()
        image = Image.open(BytesIO(image_data))
        width, height = image.size
        shape = (1, height, width, 3)
        image = np.array(image.getdata())
        image = image.reshape(shape).astype('uint8')
        return image
    
  3. 定义一个函数,通过模型进行预测,并将结果保存到磁盘。首先加载图像并将其传入模型:

    def get_and_save_predictions(model, image_path):
        image = load_image(image_path)
        results = model(image)
    
  4. 将结果转换为 NumPy 数组:

    model_output = {k: v.numpy() for k, v in results.items()}
    
  5. 创建一个包含检测框、得分和类别的可视化结果:

        boxes = model_output['detection_boxes'][0]
        classes = \
           model_output['detection_classes'][0].astype('int')
        scores = model_output['detection_scores'][0]
    
        clone = image.copy()
        viz.visualize_boxes_and_labels_on_image_array(
            image=clone[0],
            boxes=boxes,
            classes=classes,
            scores=scores,
            category_index=CATEGORY_IDX,
            use_normalized_coordinates=True,
            max_boxes_to_draw=200,
            min_score_thresh=0.30,
            agnostic_mode=False,
            line_thickness=5
        )
    
  6. 将结果保存到磁盘:

        plt.figure(figsize=(24, 32))
        plt.imshow(image_with_mask[0])
        plt.savefig(f'output/{image_path.split("/")[-1]}')
    
  7. 加载COCO的类别索引:

    labels_path = 'resources/mscoco_label_map.pbtxt'
    CATEGORY_IDX =create_category_index_from_labelmap(labels_path)
    
  8. 从 TFHub 加载 Faster R-CNN:

    MODEL_PATH = ('https://tfhub.dev/tensorflow/faster_rcnn/'
                  'inception_resnet_v2_1024x1024/1')
    model = hub.load(MODEL_PATH)
    
  9. 在所有测试图像上运行 Faster R-CNN:

    test_images_paths = glob.glob('test_images/*')
    for image_path in test_images_paths:
        get_and_save_predictions(model, image_path)
    

    一段时间后,标注过的图像应该会出现在output文件夹中。第一个示例展示了网络的强大能力,它以 100%的信心检测到了照片中的两只大象:

图 9.7 – 两只大象被检测到,且得分完美

图 9.7 – 两只大象被检测到,且得分完美

然而,也有模型出现一些错误的情况,像这样:

图 9.8 – 网络错误地将桌布中的一个人检测出来

图 9.8 – 网络错误地将桌布中的一个人检测出来

在这个例子中,网络将桌布中的一个人检测出来,置信度为 42%,虽然它正确识别了我的狗是巴哥犬,准确率为 100%。通过提高传递给visualize_boxes_and_labels_on_image_array()方法的min_score_thresh值,可以防止这种误报和其他假阳性。

让我们继续进入下一部分。

它是如何工作的…

在这个示例中,我们利用了 TFHub 中强大模型的易用性,进行开箱即用的物体检测,并取得了相当不错的结果。

为什么我们应该将 TFHub 视为满足物体检测需求的可行选择呢?好吧,那里绝大多数模型在从零开始时实现起来非常具有挑战性,更不用说训练它们以达到可接受的结果了。除此之外,这些复杂的架构是在COCO上训练的,COCO是一个庞大的图像数据集,专门用于物体检测和图像分割任务。然而,我们必须牢记,无法重新训练这些网络,因此它们最适用于包含COCO中已有物体的图像。如果我们需要创建自定义物体检测器,本章中介绍的其他策略应该足够了。

参见

您可以在此访问 TFHub 中所有可用物体检测器的列表:

tfhub.dev/tensorflow/collections/object_detection/1

第十章:第十章:将深度学习的力量应用到视频中

计算机视觉关注的是视觉数据的理解。当然,这也包括视频,视频本质上是图像的序列,这意味着我们可以利用我们关于图像处理的深度学习知识,应用到视频中并获得很好的结果。

在本章中,我们将开始训练卷积神经网络,检测人脸中的情感,然后学习如何在实时上下文中使用我们的摄像头应用它。

然后,在接下来的食谱中,我们将使用TensorFlow HubTFHub)托管的非常先进的架构,专门用于解决与视频相关的有趣问题,如动作识别、帧生成和文本到视频的检索。

这里是我们将要覆盖的食谱内容:

  • 实时检测情感

  • 使用 TensorFlow Hub 识别动作

  • 使用 TensorFlow Hub 生成视频的中间帧

  • 使用 TensorFlow Hub 进行文本到视频的检索

技术要求

和往常一样,拥有 GPU 是一个很大的优势,特别是在第一个食谱中,我们将从零开始实现一个网络。因为本章剩余部分利用了 TFHub 中的模型,所以即使是 CPU 也应该足够,尽管 GPU 能显著提高速度!在准备就绪部分,你可以找到每个食谱的准备步骤。你可以在这里找到本章的代码:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch10

查看以下链接,观看代码实际演示视频:

bit.ly/3qkTJ2l

实时检测情感

从最基本的形式来看,视频仅仅是图像序列。通过利用这一看似简单或微不足道的事实,我们可以将图像分类的知识应用到视频处理上,从而创建出由深度学习驱动的非常有趣的视频处理管道。

在本食谱中,我们将构建一个算法,实时检测情感(来自摄像头流或视频文件)。非常有趣,对吧?

让我们开始吧。

准备就绪

首先,我们需要安装一些外部库,如OpenCVimutils。执行以下命令安装它们:

$> pip install opencv-contrib-python imutils

为了训练情感分类器网络,我们将使用来自 Kaggle 比赛的数据集(~/.keras/datasets文件夹),将其提取为emotion_recognition,然后解压fer2013.tar.gz文件。

这里是一些示例图像:

图 10.1 – 示例图像。情感从左到右:悲伤、生气、害怕、惊讶、开心和中立

图 10.1 – 示例图像。情感从左到右:悲伤、生气、害怕、惊讶、开心和中立

让我们开始吧!

如何实现……

在本食谱结束时,你将拥有自己的情感检测器!

  1. 导入所有依赖项:

    import csv
    import glob
    import pathlib
    import cv2
    import imutils
    import numpy as np
    from tensorflow.keras.callbacks import ModelCheckpoint
    from tensorflow.keras.layers import *
    from tensorflow.keras.models import *
    from tensorflow.keras.optimizers import Adam
    from tensorflow.keras.preprocessing.image import *
    from tensorflow.keras.utils import to_categorical
    
  2. 定义数据集中所有可能情感的列表,并为每个情感指定一个颜色:

    EMOTIONS = ['angry', 'scared', 'happy', 'sad', 
              'surprised','neutral']
    COLORS = {'angry': (0, 0, 255),
        'scared': (0, 128, 255),
        'happy': (0, 255, 255),
        'sad': (255, 0, 0),
        'surprised': (178, 255, 102),
        'neutral': (160, 160, 160)
    }
    
  3. 定义一个方法来构建情感分类器的架构。它接收输入形状和数据集中的类别数量:

    def build_network(input_shape, classes):
        input = Input(shape=input_shape)
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same',
                   kernel_initializer='he_normal')(input)
        x = ELU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   kernel_initializer='he_normal',
                   padding='same')(x)
        x = ELU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x) 
    
  4. 网络中的每个块由两个 ELU 激活、批量归一化的卷积层组成,接着是一个最大池化层,最后是一个丢弃层。前面定义的块每个卷积层有 32 个滤波器,而后面的块每个卷积层有 64 个滤波器:

        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   kernel_initializer='he_normal',
                   padding='same')(x)
        x = ELU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   kernel_initializer='he_normal',
                   padding='same')(x)
        x = ELU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
    
  5. 第三个块每个卷积层有 128 个滤波器:

        x = Conv2D(filters=128,
                   kernel_size=(3, 3),
                   kernel_initializer='he_normal',
                   padding='same')(x)
        x = ELU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=128,
                   kernel_size=(3, 3),
                   kernel_initializer='he_normal',
                   padding='same')(x)
        x = ELU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
    
  6. 接下来,我们有两个密集层,ELU 激活、批量归一化,后面也跟着一个丢弃层,每个层有 64 个单元:

        x = Flatten()(x)
        x = Dense(units=64,
                  kernel_initializer='he_normal')(x)
        x = ELU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Dropout(rate=0.5)(x)
        x = Dense(units=64,
                  kernel_initializer='he_normal')(x)
        x = ELU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Dropout(rate=0.5)(x)
    
  7. 最后,我们遇到输出层,神经元数量与数据集中的类别数量相同,当然,采用 softmax 激活函数:

        x = Dense(units=classes,
                  kernel_initializer='he_normal')(x)
        output = Softmax()(x)
        return Model(input, output)
    
  8. load_dataset()加载训练集、验证集和测试集的图像和标签:

    def load_dataset(dataset_path, classes):
        train_images = []
        train_labels = []
        val_images = []
        val_labels = []
        test_images = []
        test_labels = []
    
  9. 这个数据集中的数据存储在一个 CSV 文件中,分为emotionpixelsUsage三列。我们首先解析emotion列。尽管数据集包含七类面部表情,我们将厌恶愤怒(分别编码为01)合并,因为它们共享大多数面部特征,合并后会得到更好的结果:

        with open(dataset_path, 'r') as f:
            reader = csv.DictReader(f)
            for line in reader:
                label = int(line['emotion'])
                if label <= 1:
                  label = 0  # This merges classes 1 and 0.
                if label > 0:
                  label -= 1  # All classes start from 0.
    
  10. 接下来,我们解析pixels列,它包含 2,034 个空格分隔的整数,代表图像的灰度像素(48x48=2034):

                image = np.array(line['pixels'].split
                                        (' '),
                                 dtype='uint8')
                image = image.reshape((48, 48))
                image = img_to_array(image)
    
  11. 现在,为了弄清楚这张图像和标签属于哪个子集,我们需要查看Usage列:

                if line['Usage'] == 'Training':
                    train_images.append(image)
                    train_labels.append(label)
                elif line['Usage'] == 'PrivateTest':
                    val_images.append(image)
                    val_labels.append(label)
                else:
                    test_images.append(image)
                    test_labels.append(label)
    
  12. 将所有的图像转换为 NumPy 数组:

        train_images = np.array(train_images)
        val_images = np.array(val_images)
        test_images = np.array(test_images)
    
  13. 然后,对所有标签进行独热编码:

        train_labels = 
        to_categorical(np.array(train_labels),
                                      classes)
        val_labels = to_categorical(np.array(val_labels), 
                                     classes)
        test_labels = to_categorical(np.array(test_labels),
                                     classes)
    
  14. 返回所有的图像和标签:

        return (train_images, train_labels), \
               (val_images, val_labels), \
               (test_images, test_labels)
    
  15. 定义一个计算矩形区域面积的函数。稍后我们将用它来获取最大的面部检测结果:

    def rectangle_area(r):
        return (r[2] - r[0]) * (r[3] - r[1])
    
  16. 现在,我们将创建一个条形图来显示每一帧中检测到的情感的概率分布。以下函数用于绘制每个条形图,代表某一特定情感:

    def plot_emotion(emotions_plot, emotion, probability, 
                     index):
        w = int(probability * emotions_plot.shape[1])
        cv2.rectangle(emotions_plot,
                      (5, (index * 35) + 5),
                      (w, (index * 35) + 35),
                      color=COLORS[emotion],
                      thickness=-1)
        white = (255, 255, 255)
        text = f'{emotion}: {probability * 100:.2f}%'
        cv2.putText(emotions_plot,
                    text,
                    (10, (index * 35) + 23),
                    fontFace=cv2.FONT_HERSHEY_COMPLEX,
                    fontScale=0.45,
                    color=white,
                    thickness=2)
        return emotions_plot
    
  17. 我们还会在检测到的面部周围画一个边界框,并标注上识别出的情感:

    def plot_face(image, emotion, detection):
        frame_x, frame_y, frame_width, frame_height = detection
        cv2.rectangle(image,
                      (frame_x, frame_y),
                      (frame_x + frame_width,
                       frame_y + frame_height),
                      color=COLORS[emotion],
                      thickness=2)
        cv2.putText(image,
                    emotion,
                    (frame_x, frame_y - 10),
                    fontFace=cv2.FONT_HERSHEY_COMPLEX,
                    fontScale=0.45,
                    color=COLORS[emotion],
                    thickness=2)
        return image
    
  18. 定义predict_emotion()函数,该函数接收情感分类器和输入图像,并返回模型输出的预测结果:

    def predict_emotion(model, roi):
        roi = cv2.resize(roi, (48, 48))
        roi = roi.astype('float') / 255.0
        roi = img_to_array(roi)
        roi = np.expand_dims(roi, axis=0)
        predictions = model.predict(roi)[0]
        return predictions
    
  19. 如果有保存的模型,则加载它:

    checkpoints = sorted(list(glob.glob('./*.h5')), reverse=True)
    if len(checkpoints) > 0:
        model = load_model(checkpoints[0])
    
  20. 否则,从头开始训练模型。首先,构建 CSV 文件的路径,然后计算数据集中的类别数量:

    else:
        base_path = (pathlib.Path.home() / '.keras' / 
                     'datasets' /
                     'emotion_recognition' / 'fer2013')
        input_path = str(base_path / 'fer2013.csv')
        classes = len(EMOTIONS)
    
  21. 然后,加载每个数据子集:

        (train_images, train_labels), \
        (val_images, val_labels), \
        (test_images, test_labels) = load_dataset(input_path,
                                                  classes)
    
  22. 构建网络并编译它。同时,定义一个ModelCheckpoint回调函数来保存最佳表现的模型(基于验证损失):

        model = build_network((48, 48, 1), classes)
        model.compile(loss='categorical_crossentropy',
                      optimizer=Adam(lr=0.003),
                      metrics=['accuracy'])
        checkpoint_pattern = ('model-ep{epoch:03d}-
                              loss{loss:.3f}'
                              '-val_loss{val_loss:.3f}.h5')
        checkpoint = ModelCheckpoint(checkpoint_pattern,
                                     monitor='val_loss',
                                     verbose=1,
                                     save_best_only=True,
                                     mode='min')
    
  23. 定义训练集和验证集的增强器和生成器。注意,我们仅增强训练集,而验证集中的图像只是进行重缩放:

        BATCH_SIZE = 128
        train_augmenter = ImageDataGenerator(rotation_
                                range=10,zoom_range=0.1,
                                  horizontal_flip=True,
                                        rescale=1\. / 255.,
                                    fill_mode='nearest')
        train_gen = train_augmenter.flow(train_images,
                                         train_labels,
                                     batch_size=BATCH_SIZE)
        train_steps = len(train_images) // BATCH_SIZE
        val_augmenter = ImageDataGenerator(rescale=1\. / 255.)
        val_gen = val_augmenter.flow(val_images,val_labels,
                             batch_size=BATCH_SIZE)
    
  24. 训练模型 300 个周期,然后在测试集上评估模型(我们只对该子集中的图像进行重缩放):

        EPOCHS = 300
        model.fit(train_gen,
                  steps_per_epoch=train_steps,
                  validation_data=val_gen,
                  epochs=EPOCHS,
                  verbose=1,
                  callbacks=[checkpoint])
       test_augmenter = ImageDataGenerator(rescale=1\. / 255.)
        test_gen = test_augmenter.flow(test_images,
                                       test_labels,
                                       batch_size=BATCH_SIZE)
        test_steps = len(test_images) // BATCH_SIZE
        _, accuracy = model.evaluate(test_gen, 
                                     steps=test_steps)
        print(f'Accuracy: {accuracy * 100}%')
    
  25. 实例化一个cv2.VideoCapture()对象来获取测试视频中的帧。如果你想使用你的网络摄像头,将video_path替换为0

    video_path = 'emotions.mp4'
    camera = cv2.VideoCapture(video_path)  # Pass 0 to use webcam
    
  26. 创建一个Haar 级联人脸检测器(这是本书范围之外的内容。如果你想了解更多关于 Haar 级联的内容,请参考本配方中的另见部分):

    cascade_file = 'resources/haarcascade_frontalface_default.xml'
    det = cv2.CascadeClassifier(cascade_file)
    
  27. 遍历视频中的每一帧(或网络摄像头流),只有在没有更多帧可以读取,或用户按下 Q 键时才退出:

    while True:
        frame_exists, frame = camera.read()
        if not frame_exists:
            break
    
  28. 将帧调整为宽度为 380 像素(高度会自动计算以保持宽高比)。同时,创建一个画布,用于绘制情感条形图,并创建一个输入帧的副本,用于绘制检测到的人脸:

        frame = imutils.resize(frame, width=380)
        emotions_plot = np.zeros_like(frame, 
                                      dtype='uint8')
        copy = frame.copy()
    
  29. 由于 Haar 级联方法是在灰度图像上工作的,我们必须将输入帧转换为黑白图像。然后,我们在其上运行人脸检测器:

        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        detections = \
            det.detectMultiScale(gray,scaleFactor=1.1,
                                 minNeighbors=5,
                                 minSize=(35, 35),
    
                            flags=cv2.CASCADE_SCALE_IMAGE)
    
  30. 验证是否有任何检测,并获取面积最大的那个:

        if len(detections) > 0:
            detections = sorted(detections,
                                key=rectangle_area)
            best_detection = detections[-1]
    
  31. 提取与检测到的面部表情对应的感兴趣区域(roi),并从中提取情感:

            (frame_x, frame_y,
             frame_width, frame_height) = best_detection
            roi = gray[frame_y:frame_y + frame_height,
                       frame_x:frame_x + frame_width]
            predictions = predict_emotion(model, roi)
            label = EMOTIONS[predictions.argmax()]
    
  32. 创建情感分布图:

            for i, (emotion, probability) in \
                    enumerate(zip(EMOTIONS, predictions)):
                emotions_plot = plot_emotion(emotions_plot,
                                             emotion,
                                             probability,
                                             i)
    
  33. 绘制检测到的面部表情及其所展示的情感:

            clone = plot_face(copy, label, best_detection)
    
  34. 显示结果:

        cv2.imshow('Face & emotions',
                   np.hstack([copy, emotions_plot]))
    
  35. 检查用户是否按下了 Q 键,如果按下了,则退出循环:

        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    
  36. 最后,释放资源:

    camera.release()
    cv2.destroyAllWindows()
    

    在 300 个周期后,我获得了 65.74%的测试准确率。在这里,你可以看到一些测试视频中检测到的情感快照:

图 10.2 – 在两个不同快照中检测到的情感

图 10.2 – 在两个不同快照中检测到的情感

我们可以看到网络正确地识别出了顶部帧中的悲伤表情,底部帧中识别出了幸福表情。让我们来看一个另一个例子:

图 10.3 – 在三个不同快照中检测到的情感

图 10.3 – 在三个不同快照中检测到的情感

在第一帧中,女孩显然呈现出中性表情,网络正确地识别出来了。第二帧中,她的面部表情显示出愤怒,分类器也检测到这一点。第三帧更有趣,因为她的表情显示出惊讶,但也可以被解读为恐惧。我们的检测器似乎在这两种情感之间有所犹豫。

让我们前往下一部分,好吗?

它是如何工作的……

在本配方中,我们实现了一个相当强大的情感检测器,用于视频流,无论是来自内建的网络摄像头,还是存储的视频文件。我们首先解析了FER 2013数据集,它与大多数其他图像数据集不同,是 CSV 格式的。然后,我们在其图像上训练了一个情感分类器,在测试集上达到了 65.74%的准确率。

我们必须考虑到面部表情的解读非常复杂,甚至对于人类来说也是如此。在某一时刻,我们可能会展示混合情感。此外,还有许多表情具有相似特征,比如愤怒厌恶,以及恐惧惊讶,等等。

本食谱中的最后一步是将输入视频流中的每一帧传递给 Haar Cascade 人脸检测器,然后使用训练好的分类器从检测到的人脸区域获取情感。

尽管这种方法对这个特定问题有效,但我们必须考虑到我们忽略了一个关键假设:每一帧都是独立的。简单来说,我们将视频中的每一帧当作一个独立的图像处理,但实际上,处理视频时并非如此,因为存在时间维度,如果考虑到这一点,将会得到更稳定、更好的结果。

另请参见

这是一个很好的资源,用于理解 Haar Cascade 分类器:docs.opencv.org/3.4/db/d28/tutorial_cascade_classifier.html

使用 TensorFlow Hub 识别动作

深度学习在视频处理中的一个非常有趣的应用是动作识别。这是一个具有挑战性的问题,因为它不仅涉及到图像分类中通常遇到的困难,还包括了时间维度。视频中的一个动作可能会根据帧呈现的顺序而有所不同。

好消息是,存在一个非常适合这种问题的架构,称为膨胀 3D 卷积网络I3D),在本食谱中,我们将使用 TFHub 上托管的训练版本来识别一组多样化视频中的动作!

开始吧。

准备工作

我们需要安装几个补充库,如OpenCVTFHubimageio。执行以下命令:

$> pip install opencv-contrib-python tensorflow-hub imageio

就是这样!让我们开始实现吧。

如何做…

执行以下步骤以完成本食谱:

  1. 导入所有所需的依赖项:

    import os
    import random
    import re
    import ssl
    import tempfile
    from urllib import request
    import cv2
    import imageio
    import numpy as np
    import tensorflow as tf
    import tensorflow_hub as tfhub
    from tensorflow_docs.vis import embed
    
  2. 定义UCF101 – 动作识别数据集的路径,从中获取我们稍后将传递给模型的测试视频:

    UCF_ROOT = 'https://www.crcv.ucf.edu/THUMOS14/UCF101/UCF101/'
    
  3. 定义Kinetics数据集的标签文件路径,后者用于训练我们将很快使用的 3D 卷积网络:

    KINETICS_URL = ('https://raw.githubusercontent.com/deepmind/'
                    'kinetics-i3d/master/data/label_map.txt')
    
  4. 创建一个临时目录,用于缓存下载的资源:

    CACHE_DIR = tempfile.mkdtemp()
    
  5. 创建一个未经验证的 SSL 上下文。我们需要这个以便能够从 UCF 的网站下载数据(在编写本书时,似乎他们的证书已过期):

    UNVERIFIED_CONTEXT = ssl._create_unverified_context()
    
  6. 定义fetch_ucf_videos()函数,该函数下载我们将从中选择的测试视频列表,以测试我们的动作识别器:

    def fetch_ucf_videos():
        index = \
            (request
             .urlopen(UCF_ROOT, 
                      context=UNVERIFIED_CONTEXT)
             .read()
             .decode('utf-8'))
        videos = re.findall('(v_[\w]+\.avi)', index)
        return sorted(set(videos))
    
  7. 定义fetch_kinetics_labels()函数,用于下载并解析Kinetics数据集的标签:

    def fetch_kinetics_labels():
        with request.urlopen(KINETICS_URL) as f:
            labels = [line.decode('utf-8').strip()
                      for line in f.readlines()]
        return labels
    
  8. 定义fetch_random_video()函数,该函数从我们的UCF101视频列表中选择一个随机视频,并将其下载到第 4 步中创建的临时目录中:

    def fetch_random_video(videos_list):
        video_name = random.choice(videos_list)
        cache_path = os.path.join(CACHE_DIR, video_name)
        if not os.path.exists(cache_path):
            url = request.urljoin(UCF_ROOT, video_name)
            response = (request
                        .urlopen(url,
    
                         context=UNVERIFIED_CONTEXT)
                        .read())
            with open(cache_path, 'wb') as f:
                f.write(response)
        return cache_path
    
  9. 定义crop_center()函数,该函数接受一张图片并裁剪出对应于接收帧中心的正方形区域:

    def crop_center(frame):
        height, width = frame.shape[:2]
        smallest_dimension = min(width, height)
        x_start = (width // 2) - (smallest_dimension // 2)
        x_end = x_start + smallest_dimension
        y_start = (height // 2) - (smallest_dimension // 2)
        y_end = y_start + smallest_dimension
        roi = frame[y_start:y_end, x_start:x_end]
        return roi
    
  10. 定义 read_video() 函数,它从我们的缓存中读取最多 max_frames 帧,并返回所有读取的帧列表。它还会裁剪每帧的中心,将其调整为 224x224x3 的大小(网络期望的输入形状),并进行归一化处理:

    def read_video(path, max_frames=32, resize=(224, 224)):
        capture = cv2.VideoCapture(path)
        frames = []
        while len(frames) <= max_frames:
            frame_read, frame = capture.read()
            if not frame_read:
                break
            frame = crop_center(frame)
            frame = cv2.resize(frame, resize)
            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            frames.append(frame)
        capture.release()
        frames = np.array(frames)
        return frames / 255.
    
  11. 定义 predict() 函数,用于获取模型在输入视频中识别的前五个最可能的动作:

    def predict(model, labels, sample_video):
        model_input = tf.constant(sample_video,
                                  dtype=tf.float32)
        model_input = model_input[tf.newaxis, ...]
        logits = model(model_input)['default'][0]
        probabilities = tf.nn.softmax(logits)
        print('Top 5 actions:')
        for i in np.argsort(probabilities)[::-1][:5]:
            print(f'{labels[i]}:  {probabilities[i] * 100:5.2f}%')
    
  12. 定义 save_as_gif() 函数,它接收一个包含视频帧的列表,并用它们创建 GIF 格式的表示:

    def save_as_gif(images, video_name):
        converted_images = np.clip(images * 255, 0, 255)
        converted_images = converted_images.astype(np.uint8)
        imageio.mimsave(f'./{video_name}.gif',
                        converted_images,
                        fps=25)
    
  13. 获取视频和标签:

    VIDEO_LIST = fetch_ucf_videos()
    LABELS = fetch_kinetics_labels()
    
  14. 获取一个随机视频并读取其帧:

    video_path = fetch_random_video(VIDEO_LIST)
    sample_video = read_video(video_path)
    
  15. 从 TFHub 加载 I3D:

    model_path = 'https://tfhub.dev/deepmind/i3d-kinetics-400/1'
    model = tfhub.load(model_path)
    model = model.signatures['default']
    
  16. 最后,将视频传递给网络以获得预测结果,然后将视频保存为 GIF 格式:

    predict(model, LABELS, sample_video)
    video_name = video_path.rsplit('/', maxsplit=1)[1][:-4]
    save_as_gif(sample_video, video_name)
    

    这是我获得的随机视频的第一帧:

图 10.4 – 随机 UCF101 视频的帧

图 10.4 – 随机 UCF101 视频的帧

这是模型生成的前五个预测:

Top 5 actions:
mopping floor:  75.29%
cleaning floor:  21.11%
sanding floor:   0.85%
spraying:   0.69%
sweeping floor:   0.64%

看起来网络理解视频中呈现的动作与地板有关,因为五个预测中有四个与此相关。然而,mopping floor才是正确的预测。

现在让我们进入 它是如何工作的…… 部分。

它是如何工作的……

在这个方案中,我们利用了 3D 卷积网络的强大功能来识别视频中的动作。顾名思义,3D 卷积是二维卷积的自然扩展,它可以在两个方向上进行操作。3D 卷积不仅考虑了宽度和高度,还考虑了深度,因此它非常适合某些特殊类型的图像,如磁共振成像(MRI),或者在本例中是视频,视频实际上就是一系列叠加在一起的图像。

我们首先从 UCF101 数据集中获取了一系列视频,并从 Kinetics 数据集中获取了一组动作标签。需要记住的是,我们从 TFHub 下载的 I3D 是在 Kinetics 数据集上训练的。因此,我们传递给它的视频是未见过的。

接下来,我们实现了一系列辅助函数,用于获取、预处理并调整每个输入视频的格式,以符合 I3D 的预期。然后,我们从 TFHub 加载了上述网络,并用它来显示视频中识别到的前五个动作。

你可以对这个解决方案进行一个有趣的扩展,即从文件系统中读取自定义视频,或者更好的是,将来自摄像头的图像流传递给网络,看看它的表现如何!

另请参见

I3D 是一种用于视频处理的突破性架构,因此我强烈建议你阅读原始论文:arxiv.org/abs/1705.07750。这里有一篇相当有趣的文章,解释了 1D、2D 和 3D 卷积的区别:towardsdatascience.com/understanding-1d-and-3d-convolution-neural-network-keras-9d8f76e29610。你可以在这里了解更多关于UCF101数据集的信息:https://www.crcv.ucf.edu/data/UCF101.php。如果你对Kinetics数据集感兴趣,可以访问这个链接:https://deepmind.com/research/open-source/kinetics。最后,你可以在这里找到我们使用的 I3D 实现的更多细节:tfhub.dev/deepmind/i3d-kinetics-400/1

使用 TensorFlow Hub 生成视频的中间帧

深度学习在视频中的另一个有趣应用涉及帧生成。这个技术的一个有趣且实用的例子是慢动作,其中一个网络根据上下文决定如何创建插入帧,从而扩展视频长度,并制造出用高速摄像机拍摄的假象(如果你想了解更多内容,可以参考另见…部分)。

在这个食谱中,我们将使用 3D 卷积网络来生成视频的中间帧,给定视频的第一帧和最后一帧。

为此,我们将依赖 TFHub。

让我们开始这个食谱。

准备就绪

我们必须安装 TFHub 和 TensorFlow Datasets

$> pip install tensorflow-hub tensorflow-datasets

我们将使用的模型是在 BAIR Robot Pushing Videos 数据集上训练的,该数据集可在 TensorFlow Datasets 中获得。然而,如果我们通过库访问它,我们将下载远超过我们这个食谱所需的数据。因此,我们将使用测试集的一个较小子集。执行以下命令来下载它并将其放入 ~/.keras/datasets/bair_robot_pushing 文件夹中:

$> wget -nv https://storage.googleapis.com/download.tensorflow.org/data/bair_test_traj_0_to_255.tfrecords -O ~/.keras/datasets/bair_robot_pushing/traj_0_to_255.tfrecords

现在一切准备就绪!让我们开始实施。

如何实现…

执行以下步骤,学习如何通过托管在 TFHub 中的模型生成中间帧,使用 直接 3D 卷积

  1. 导入依赖库:

    import pathlib
    import matplotlib.pyplot as plt
    import numpy as np
    import tensorflow as tf
    import tensorflow_hub as tfhub
    from tensorflow_datasets.core import SplitGenerator
    from tensorflow_datasets.video.bair_robot_pushing import \
        BairRobotPushingSmall
    
  2. 定义 plot_first_and_last_for_sample() 函数,该函数绘制四个视频样本的第一帧和最后一帧的图像:

    def plot_first_and_last_for_sample(frames, batch_size):
        for i in range(4):
            plt.subplot(batch_size, 2, 1 + 2 * i)
            plt.imshow(frames[i, 0] / 255.)
            plt.title(f'Video {i}: first frame')
            plt.axis('off')
            plt.subplot(batch_size, 2, 2 + 2 * i)
            plt.imshow(frames[i, 1] / 255.)
            plt.title(f'Video {i}: last frame')
            plt.axis('off')
    
  3. 定义 plot_generated_frames_for_sample() 函数,该函数绘制为四个视频样本生成的中间帧:

    def plot_generated_frames_for_sample(gen_videos):
        for video_id in range(4):
            fig = plt.figure(figsize=(10 * 2, 2))
            for frame_id in range(1, 16):
                ax = fig.add_axes(
                    [frame_id / 16., 0, (frame_id + 1) / 
                           16., 1],
                    xmargin=0, ymargin=0)
                ax.imshow(gen_videos[video_id, frame_id])
                ax.axis('off')
    
  4. 我们需要修补 BarRobotPushingSmall()(参见步骤 6)数据集构建器,只期望测试集可用,而不是同时包含训练集和测试集。因此,我们必须创建一个自定义的 SplitGenerator()

    def split_gen_func(data_path):
        return [SplitGenerator(name='test',
                               gen_kwargs={'filedir': 
                                           data_path})]
    
  5. 定义数据路径:

    DATA_PATH = str(pathlib.Path.home() / '.keras' / 
                       'datasets' /
                    'bair_robot_pushing')
    
  6. 创建一个 BarRobotPushingSmall() 构建器,将其传递给步骤 4中创建的自定义拆分生成器,然后准备数据集:

    builder = BairRobotPushingSmall()
    builder._split_generators = lambda _:split_gen_func(DATA_PATH)
    builder.download_and_prepare()
    
  7. 获取第一批视频:

    BATCH_SIZE = 16
    dataset = builder.as_dataset(split='test')
    test_videos = dataset.batch(BATCH_SIZE)
    for video in test_videos:
        first_batch = video
        break
    
  8. 保留每个视频批次中的第一帧和最后一帧:

    input_frames = first_batch['image_aux1'][:, ::15]
    input_frames = tf.cast(input_frames, tf.float32)
    
  9. 从 TFHub 加载生成器模型:

    model_path = 'https://tfhub.dev/google/tweening_conv3d_bair/1'
    model = tfhub.load(model_path)
    model = model.signatures['default']
    
  10. 将视频批次传递到模型中,生成中间帧:

    middle_frames = model(input_frames)['default']
    middle_frames = middle_frames / 255.0
    
  11. 将每个视频批次的首尾帧与网络在步骤 10中生成的相应中间帧进行连接:

    generated_videos = np.concatenate(
        [input_frames[:, :1] / 255.0,  # All first frames
         middle_frames,  # All inbetween frames
         input_frames[:, 1:] / 255.0],  # All last frames
        axis=1)
    
  12. 最后,绘制首尾帧,以及中间帧:

    plt.figure(figsize=(4, 2 * BATCH_SIZE))
    plot_first_and_last_for_sample(input_frames, 
                                    BATCH_SIZE)
    plot_generated_frames_for_sample(generated_videos)
    plt.show()
    

    图 10.5中,我们可以观察到我们四个示例视频中每个视频的首尾帧:

图 10.5 – 每个视频的首尾帧

图 10.5 – 每个视频的首尾帧

图 10.6中,我们观察到模型为每个视频生成的 14 帧中间帧。仔细检查可以发现,它们与传递给网络的首尾真实帧是一致的:

图 10.6 – 模型为每个示例视频生成的中间帧

图 10.6 – 模型为每个示例视频生成的中间帧

让我们进入它是如何工作的…部分,回顾我们所做的工作。

它是如何工作的…

在本节中,我们学习了深度学习在视频中的另一个有趣且有用的应用,特别是在生成模型的背景下,3D 卷积网络的应用。

我们使用了一个在 BAIR Robot Pushing Videos 数据集上训练的最先进架构,该数据集托管在 TFHub 上,并用它生成了一个全新的视频序列,仅以视频的首尾帧作为种子。

由于下载整个 30 GB 的 BAIR 数据集会显得过于冗余,考虑到我们只需要一个小得多的子集来测试我们的解决方案,我们无法直接依赖 TensorFlow 数据集的 load() 方法。因此,我们下载了测试视频的一个子集,并对 BairRobotPushingSmall() 构建器进行了必要的调整,以加载和准备示例视频。

必须提到的是,这个模型是在一个非常特定的数据集上训练的,但它确实展示了这个架构强大的生成能力。我鼓励你查看另见部分,其中列出了如果你想在自己的数据上实现视频生成网络时可能有帮助的有用资源。

另见

你可以在此了解更多关于 BAIR Robot Pushing Videos 数据集的信息:arxiv.org/abs/1710.05268。我鼓励你阅读题为 视频中间插帧使用直接 3D 卷积 的论文,这篇论文提出了我们在本节中使用的网络:https://arxiv.org/abs/1905.10240. 你可以在以下链接找到我们依赖的 TFHub 模型:https://tfhub.dev/google/tweening_conv3d_bair/1. 最后,以下是关于将普通视频转化为慢动作的 AI 的一篇有趣文章:petapixel.com/2020/09/08/this-ai-can-transform-regular-footage-into-slow-motion-with-no-artifacts/.

使用 TensorFlow Hub 进行文本到视频检索

深度学习在视频中的应用不仅限于分类、分类或生成。神经网络的最大资源之一是它们对数据特征的内部表示。一个网络在某一任务上越优秀,它们的内部数学模型就越好。我们可以利用最先进模型的内部工作原理,构建有趣的应用。

在这个步骤中,我们将基于由S3D模型生成的嵌入创建一个小型搜索引擎,该模型已经在 TFHub 上训练并准备好使用。

你准备好了吗?让我们开始吧!

准备就绪

首先,我们必须安装OpenCV和 TFHub,方法如下:

$> pip install opencv-contrib-python tensorflow-hub

这就是我们需要的,开始这个步骤吧!

如何做到这一点……

执行以下步骤,学习如何使用 TFHub 进行文本到视频的检索:

  1. 第一步是导入我们将使用的所有依赖项:

    import math
    import os
    import uuid
    import cv2
    import numpy as np
    import tensorflow as tf
    import tensorflow_hub as tfhub
    from tensorflow.keras.utils import get_file
    
  2. 定义一个函数,使用 S3D 实例生成文本和视频嵌入:

    def produce_embeddings(model, input_frames, input_words):
        frames = tf.cast(input_frames, dtype=tf.float32)
        frames = tf.constant(frames)
        video_model = model.signatures['video']
        video_embedding = video_model(frames)
        video_embedding = video_embedding['video_embedding']
        words = tf.constant(input_words)
        text_model = model.signatures['text']
        text_embedding = text_model(words)
        text_embedding = text_embedding['text_embedding']
        return video_embedding, text_embedding
    
  3. 定义crop_center()函数,该函数接收一张图像并裁剪出与接收到的帧中心相对应的正方形区域:

    def crop_center(frame):
        height, width = frame.shape[:2]
        smallest_dimension = min(width, height)
        x_start = (width // 2) - (smallest_dimension // 2)
        x_end = x_start + smallest_dimension
        y_start = (height // 2) - (smallest_dimension // 
                                        2)
        y_end = y_start + smallest_dimension
        roi = frame[y_start:y_end, x_start:x_end]
        return roi
    
  4. 定义fetch_and_read_video()函数,顾名思义,该函数下载视频并读取它。在最后一步,我们使用 OpenCV。首先,从给定的 URL 获取视频:

    def fetch_and_read_video(video_url,
                             max_frames=32,
                             resize=(224, 224)):
        extension = video_url.rsplit(os.path.sep,
                                     maxsplit=1)[-1]
        path = get_file(f'{str(uuid.uuid4())}.{extension}',
                        video_url,
                        cache_dir='.',
                        cache_subdir='.')
    

    我们从 URL 中提取视频格式。然后,我们将视频保存在当前文件夹中,文件名为一个随机生成的 UUID。

  5. 接下来,我们将加载这个获取的视频的max_frames

        capture = cv2.VideoCapture(path)
        frames = []
        while len(frames) <= max_frames:
            frame_read, frame = capture.read()
            if not frame_read:
                break
            frame = crop_center(frame)
            frame = cv2.resize(frame, resize)
            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            frames.append(frame)
        capture.release()
        frames = np.array(frames)
    
  6. 如果视频的帧数不足,我们将重复此过程,直到达到所需的容量:

        if len(frames) < max_frames:
            repetitions = math.ceil(float(max_frames) /        
                                    len(frames))
            repetitions = int(repetitions)
            frames = frames.repeat(repetitions, axis=0)
    
  7. 返回归一化后的帧:

        frames = frames[:max_frames]
        return frames / 255.0
    
  8. 定义视频的 URL:

    URLS = [
        ('https://media.giphy.com/media/'
         'WWYSFIZo4fsLC/source.gif'),
        ('https://media.giphy.com/media/'
         'fwhIy2QQtu5vObfjrs/source.gif'),
        ('https://media.giphy.com/media/'
         'W307DdkjIsRHVWvoFE/source.gif'),
        ('https://media.giphy.com/media/'
         'FOcbaDiNEaqqY/source.gif'),
        ('https://media.giphy.com/media/'
         'VJwck53yG6y8s2H3Og/source.gif')]
    
  9. 获取并读取每个视频:

    VIDEOS = [fetch_and_read_video(url) for url in URLS]
    
  10. 定义与每个视频相关联的查询(标题)。请注意,它们必须按正确的顺序排列:

    QUERIES = ['beach', 'playing drums', 'airplane taking 
                  off',
               'biking', 'dog catching frisbee']
    
  11. 从 TFHub 加载 S3D:

    model = tfhub.load
    ('https://tfhub.dev/deepmind/mil-nce/s3d/1')
    
  12. 获取文本和视频嵌入:

    video_emb, text_emb = produce_embeddings(model,
                                  np.stack(VIDEOS, axis=0),
                                             np.array(QUERIES))
    
  13. 计算文本和视频嵌入之间的相似度得分:

    scores = np.dot(text_emb, tf.transpose(video_emb))
    
  14. 获取每个视频的第一帧,将其重新缩放回[0, 255],然后转换为 BGR 空间,以便我们可以使用 OpenCV 显示它。我们这样做是为了展示实验结果:

    first_frames = [v[0] for v in VIDEOS]
    first_frames = [cv2.cvtColor((f * 255.0).astype('uint8'),
                                 cv2.COLOR_RGB2BGR) for f 
                                    in  first_frames]
    
  15. 遍历每个(查询,视频,得分)三元组,并显示每个查询的最相似视频:

    for query, video, query_scores in zip(QUERIES,VIDEOS,scores):
        sorted_results = sorted(list(zip(QUERIES,
                                         first_frames,
                                         query_scores)),
                                key=lambda p: p[-1],
                                reverse=True)
        annotated_frames = []
        for i, (q, f, s) in enumerate(sorted_results, 
                                     start=1):
            frame = f.copy()
            cv2.putText(frame,
                        f'#{i} - Score: {s:.2f}',
                        (8, 15),
                        fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                        fontScale=0.6,
                        color=(0, 0, 255),
                        thickness=2)
            annotated_frames.append(frame)
        cv2.imshow(f'Results for query “{query}”',
                   np.hstack(annotated_frames))
        cv2.waitKey(0)
    

    首先,我们来看一下海滩查询的结果:

图 10.7 – 针对“海滩”查询的排名结果

图 10.7 – 针对“海滩”查询的排名结果

正如预期的那样,第一个结果(得分最高)是一张海滩的图片。现在,让我们试试打鼓

图 10.8 – 针对“打鼓”查询的排名结果

图 10.8 – 针对“打鼓”查询的排名结果

太棒了!看来这个实例中查询文本和图像之间的相似度更强。接下来是一个更具挑战性的查询:

图 10.9 – 针对“飞机起飞”查询的排名结果

图 10.9 – 针对“飞机起飞”查询的排名结果

尽管飞机起飞是一个稍微复杂一点的查询,但我们的解决方案毫无问题地产生了正确的结果。现在让我们试试biking

图 10.10 – BIKING 查询的排名结果

图 10.10 – BIKING 查询的排名结果

又一个匹配!那狗抓飞盘呢?

图 10.11 – DOG CATCHING FRISBEE 查询的排名结果

图 10.11 – DOG CATCHING FRISBEE 查询的排名结果

一点问题都没有!我们所见到的令人满意的结果,归功于 S3D 在将图像与最能描述它们的文字进行匹配方面所做的出色工作。如果你已经阅读了介绍 S3D 的论文,你不会对这一事实感到惊讶,因为它是在大量数据上进行训练的。

现在让我们继续下一部分。

它是如何工作的……

在这个方法中,我们利用了 S3D 模型生成嵌入的能力,既针对文本也针对视频,创建了一个小型数据库,并将其用作一个玩具搜索引擎的基础。通过这种方式,我们展示了拥有一个能够在图像和文本之间生成丰富的信息向量双向映射的网络的实用性。

参见

我强烈推荐你阅读我们在这个方法中使用的模型所发表的论文,内容非常有趣!这是链接:https://arxiv.org/pdf/1912.06430.pdf。说到这个模型,你可以在这里找到它:https://tfhub.dev/deepmind/mil-nce/s3d/1。

第十一章:第十一章:使用 AutoML 简化网络实现

计算机视觉,特别是与深度学习结合时,是一个不适合胆小者的领域!在传统的计算机编程中,我们有有限的调试和实验选项,而在机器学习中,情况则不同。

当然,机器学习本身的随机性在使得创建足够好的解决方案变得困难的过程中起着一定作用,但我们需要调整的无数参数、变量、控制和设置同样是挑战所在,只有将它们调整正确,才能释放神经网络在特定问题上的真正力量。

选择合适的架构只是开始,因为我们还需要考虑预处理技术、学习率、优化器、损失函数、数据拆分等众多因素。

我的观点是,深度学习很难!从哪里开始呢?如果我们能有一种方法来减轻在如此多的组合中寻找的负担,那该多好!

嗯,它确实存在!它被称为自动机器学习AutoML),在本章中,我们将学习如何利用这一领域最有前景的工具之一,它是建立在 TensorFlow 之上的,叫做AutoKeras

在本章中,我们将涵盖以下配方:

  • 使用 AutoKeras 创建一个简单的图像分类器

  • 使用 AutoKeras 创建一个简单的图像回归器

  • 在 AutoKeras 中导出和导入模型

  • 使用 AutoKeras 的 AutoModel 控制架构生成

  • 使用 AutoKeras 预测年龄和性别

让我们开始吧!

技术要求

你会首先注意到的是,AutoML非常消耗资源,因此如果你想复制并扩展我们将在本章中讨论的配方,访问GPU是必须的。此外,由于我们将在所有提供的示例中使用AutoKeras,请按以下方式安装它:

$> pip install git+https://github.com/keras-team/keras-tuner.git@1.0.2rc2 autokeras pydot graphviz

本章我们将使用的AutoKeras版本仅支持 TensorFlow 2.3,因此请确保已安装该版本(如果愿意,你也可以创建一个全新的环境)。在每个配方的准备工作部分,你会找到任何需要的准备信息。和往常一样,本章中的代码可以在github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch11获取。

查看以下链接,观看 Code in Action 视频:

bit.ly/2Na6XRz

使用 AutoKeras 创建一个简单的图像分类器

图像分类无疑是神经网络在计算机视觉中的事实性应用。然而,正如我们所知道的,根据数据集的复杂性、信息的可用性以及无数其他因素,创建一个合适的图像分类器的过程有时可能相当繁琐。

在这个教程中,我们将借助AutoML的魔力轻松实现一个图像分类器。不信吗?那就开始吧,一起看看!

如何实现…

在本教程结束时,你将能用不超过十几行代码实现一个图像分类器!让我们开始吧:

  1. 导入所有需要的模块:

    from autokeras import ImageClassifier
    from tensorflow.keras.datasets import fashion_mnist as fm
    

    为了简化,我们将使用著名的Fashion-MNIST数据集,它是著名的MNIST的一个更具挑战性的版本。

  2. 加载训练和测试数据:

    (X_train, y_train), (X_test, y_test) = fm.load_data()
    
  3. 将图像归一化到[0, 1]的范围:

    X_train = X_train.astype('float32') / 255.0
    X_test = X_test.astype('float32') / 255.0
    
  4. 定义我们允许每个可能的网络(称为一次试验)训练的轮次数:

    EPOCHS = 10
    
  5. 这就是魔法发生的地方。定义一个ImageClassifier()实例:

    classifier = ImageClassifier(seed=9, max_trials=10)
    

    注意,我们将分类器的种子设为 9,并允许它找到一个合适的网络 10 次。我们这样做是为了让神经架构搜索NAS)过程在合理的时间内终止(要了解更多关于NAS的信息,请参考参见部分)。

  6. 在测试数据上对分类器进行 10 个轮次的训练(每次试验):

    classifier.fit(X_train, y_train, epochs=EPOCHS)
    
  7. 最后,在测试集上评估最佳分类器并打印准确率:

    print(classifier.evaluate(X_test, y_test))
    

    过一段时间后(别忘了库正在训练 10 个具有不同复杂度的模型),我们应该能得到大约 93%的准确率。考虑到我们甚至没有写 10 行代码,这个结果还不错!

我们将在工作原理…部分进一步讨论我们所做的工作。

工作原理…

在这个教程中,我们创建了最轻松的图像分类器!我们将所有主要决策都交给了AutoML工具——AutoKeras。从选择架构到选择使用哪种优化器,所有这些决策都由框架做出。

你可能已经注意到,我们通过指定最多 10 次试验和每次试验最多 10 个轮次来限制搜索空间。我们这样做是为了让程序在合理的时间内终止,但正如你可能猜到的,这些参数也可以交给AutoKeras来处理。

尽管AutoML具有很高的自主性,我们仍然可以根据需要指导框架。正如其名所示,AutoML提供了一种自动化寻找针对特定问题足够好组合的方法。然而,这并不意味着不需要人类的专业知识和先前的经验。事实上,通常情况下,一个经过精心设计的网络(通常是通过深入研究数据所得)往往比AutoML在没有任何先前信息的情况下找到的网络效果更好。

最终,AutoML是一个工具,应该用来增强我们对深度学习的掌握,而不是取而代之——因为它做不到这一点。

参见

你可以在这里了解更多关于NAS的信息:en.wikipedia.org/wiki/Neural_architecture_search

使用 AutoKeras 创建一个简单的图像回归器

AutoKeras的强大功能不仅限于图像分类。尽管不如图像分类流行,图像回归是一个类似的问题,我们希望根据图像中的空间信息预测一个连续的量。

在本配方中,我们将训练一个图像回归器,预测人们的年龄,同时使用AutoML

让我们开始吧。

准备工作

在本配方中,我们将使用APPA-REAL数据集,该数据集包含 7,591 张图像,标注了广泛对象的真实年龄和表观年龄。您可以在chalearnlap.cvc.uab.es/dataset/26/description/#查看更多有关该数据集的信息并下载它。将数据解压到您选择的目录中。为了配方的目的,我们假设数据集位于~/.keras/datasets/appa-real-release文件夹中。

以下是一些示例图像:

图 11.1 – 来自 APPA-REAL 数据集的示例图像

](https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/tf20-cv-cb/img/B14768_11_001.jpg)

图 11.1 – 来自 APPA-REAL 数据集的示例图像

让我们实现这个配方!

如何操作……

按照以下步骤完成此配方:

  1. 导入我们将要使用的模块:

    import csv
    import pathlib
    import numpy as np
    from autokeras import ImageRegressor
    from tensorflow.keras.preprocessing.image import *
    
  2. 数据集的每个子集(训练集、测试集和验证集)都在一个 CSV 文件中定义。在这个文件中,除了许多其他列外,我们还有图像路径和照片中人物的实际年龄。在此步骤中,我们将定义load_mapping()函数,该函数将从图像路径创建一个映射,用于加载实际数据到内存中:

    def load_mapping(csv_path, faces_path):
        mapping = {}
        with open(csv_path, 'r') as f:
            reader = csv.DictReader(f)
            for line in reader:
                file_name = line["file_name"].rsplit(".")[0]
               key = f'{faces_path}/{file_name}.jpg_face.jpg'
                mapping[key] = int(line['real_age'])
        return mapping
    
  3. 定义get_image_and_labels()函数,该函数接收load_mapping()函数生成的映射,并返回一个图像数组(归一化到[-1, 1]范围内)和一个相应年龄的数组:

    def get_images_and_labels(mapping):
        images = []
        labels = []
        for image_path, label in mapping.items():
            try:
                image = load_img(image_path, target_size=(64, 
                                                        64))
                image = img_to_array(image)
                images.append(image)
                labels.append(label)
            except FileNotFoundError:
                continue
        return (np.array(images) - 127.5) / 127.5, \
               np.array(labels).astype('float32')
    

    请注意,每张图像都已调整大小,确保其尺寸为 64x64x3。这是必要的,因为数据集中的图像尺寸不统一。

  4. 定义 CSV 文件的路径,以创建每个子集的数据映射:

    base_path = (pathlib.Path.home() / '.keras' / 'datasets' 
                 /'appa-real-release')
    train_csv_path = str(base_path / 'gt_train.csv')
    test_csv_path = str(base_path / 'gt_test.csv')
    val_csv_path = str(base_path / 'gt_valid.csv')
    
  5. 定义每个子集的图像所在目录的路径:

    train_faces_path = str(base_path / 'train')
    test_faces_path = str(base_path / 'test')
    val_faces_path = str(base_path / 'valid')
    
  6. 为每个子集创建映射:

    train_mapping = load_mapping(train_csv_path, 
                                train_faces_path)
    test_mapping = load_mapping(test_csv_path, 
                               test_faces_path)
    val_mapping = load_mapping(val_csv_path, 
                               val_faces_path)
    
  7. 获取每个子集的图像和标签:

    X_train, y_train = get_images_and_labels(train_mapping)
    X_test, y_test = get_images_and_labels(test_mapping)
    X_val, y_val = get_images_and_labels(val_mapping)
    
  8. 我们将在每次试验中训练每个网络,最多训练 15 个 epoch:

    EPOCHS = 15
    
  9. 我们实例化一个ImageRegressor()对象,它封装了adam优化器:

    regressor = ImageRegressor(seed=9,
                               max_trials=10,
                               optimizer='adam')
    
  10. 拟合回归器。请注意,我们传递了自己的验证集。如果我们不这样做,AutoKeras默认会取 20%的训练数据来验证它的实验:

    regressor.fit(X_train, y_train,
                  epochs=EPOCHS,
                  validation_data=(X_val, y_val))
    
  11. 最后,我们必须在测试数据上评估最佳回归器并打印其性能指标:

    print(regressor.evaluate(X_test, y_test))
    

    一段时间后,我们应该获得 241.248 的测试损失,考虑到我们的工作主要是加载数据集,这个结果还不错。

让我们进入它是如何工作的……部分。

它是如何工作的……

在这个食谱中,我们将模型的创建委托给了AutoML框架,类似于我们在使用 AutoKeras 创建简单图像分类器食谱中的做法。不过,这次,我们的目标是解决回归问题,即根据人脸照片预测一个人的年龄,而不是分类问题。

这一次,因为我们使用了一个真实世界的数据集,我们不得不实现几个辅助函数来加载数据,并将其转化为AutoKeras可以使用的正确形状。不过,在做完这些之后,我们让框架接管,利用它内建的NAS算法,在 15 次迭代中找到最佳模型。

我们在测试集上获得了一个相当不错的 241.248 的损失值。预测一个人的年龄并不是一件简单的任务,尽管乍一看它似乎很简单。我邀请你仔细查看APPA-REAL的 CSV 文件,这样你就能看到人类对年龄估算的偏差!

另见

你可以在这里了解更多关于NAS的信息:en.wikipedia.org/wiki/Neural_architecture_search

在 AutoKeras 中导出和导入模型

在使用AutoML时,我们可能会担心其黑盒性质。我们能控制生成的模型吗?我们可以扩展它们吗?理解它们吗?重新使用它们吗?

当然可以!AutoKeras的一个优点是它构建在 TensorFlow 之上,因此尽管它非常复杂,但在底层,训练的模型只是 TensorFlow 图,我们可以在以后导出、调整和优化这些模型(如果需要的话)。

在这个食谱中,我们将学习如何导出一个在AutoKeras上训练的模型,然后将其作为一个普通的 TensorFlow 网络导入。

你准备好了吗?让我们开始吧。

如何做到…

按照以下步骤完成本食谱:

  1. 导入必要的依赖项:

    from autokeras import *
    from tensorflow.keras.datasets import fashion_mnist as fm
    from tensorflow.keras.models import load_model
    from tensorflow.keras.utils import plot_model
    
  2. 加载Fashion-MNIST数据集的训练和测试集:

    (X_train, y_train), (X_test, y_test) = fm.load_data()
    
  3. 将数据归一化到[0, 1]区间:

    X_train = X_train.astype('float32') / 255.0
    X_test = X_test.astype('float32') / 255.0
    
  4. 定义我们将为每个网络训练的周期数:

    EPOCHS = 10
    
  5. 创建一个ImageClassifier(),它将尝试在 20 次试验中找到最佳分类器,每次训练 10 个周期。我们将指定adam作为优化器,并为可重复性设置ImageClassifier()的种子:

    classifier = ImageClassifier(seed=9,
                                 max_trials=20,
                                 optimizer='adam')
    
  6. 训练分类器。我们将允许AutoKeras自动选择 20%的训练数据作为验证集:

    classifier.fit(X_train, y_train, epochs=EPOCHS)
    
  7. 导出最佳模型并将其保存到磁盘:

    model = classifier.export_model()
    model.save('model.h5')
    
  8. 将模型重新加载到内存中:

    model = load_model('model.h5',
                       custom_objects=CUSTOM_OBJECTS)
    
  9. 在测试集上评估训练模型:

    print(classifier.evaluate(X_test, y_test))
    
  10. 打印最佳模型的文本摘要:

    print(model.summary())
    
  11. 最后,生成AutoKeras找到的最佳模型的架构图:

    plot_model(model,
               show_shapes=True,
               show_layer_names=True,
               to_file='model.png')
    

    在 20 次试验之后,AutoKeras创建的最佳模型在测试集上的准确率达到了 91.5%。以下截图展示了模型的摘要:

图 11.2 – **AutoKeras**最佳模型摘要

图 11.2 – AutoKeras最佳模型摘要

以下图表展示了模型的架构:

图 11.3 – AutoKeras 的最佳模型架构

图 11.3 – AutoKeras 的最佳模型架构

图 11.2中,我们可以看到AutoKeras被认为是最适合Fashion-MNIST的网络,至少在我们设定的范围内是这样。你可以在配套的 GitHub 仓库中更详细地查看完整架构。

让我们继续到下一节。

它是如何工作的……

在这个配方中,我们展示了AutoML如何作为我们解决新计算机视觉问题时的一个很好的起点。如何做?我们可以利用它快速生成表现良好的模型,然后基于我们对当前数据集的领域知识进行扩展。

实现这一点的公式很简单:让AutoML做一段时间的繁重工作,然后导出最佳网络并将其导入 TensorFlow 的框架中,这样你就可以在其上构建你的解决方案。

这不仅展示了像AutoKeras这样的工具的可用性,还让我们得以窥见幕后,理解由NAS生成的模型的构建块。

另见

AutoKeras的基础是NAS。你可以在这里阅读更多相关内容(非常有趣!):en.wikipedia.org/wiki/Neural_architecture_search

使用 AutoKeras 的 AutoModel 控制架构生成

AutoKeras自动决定哪个架构最适合是很好的,但这可能会非常耗时——有时是不可接受的。

我们能否施加更多控制?我们能否提示哪些选项最适合我们的特定问题?我们能否通过提供一组必须遵循的指导方针,使AutoML在我们事先的知识或偏好基础上进行实验,同时又给它足够的自由度进行试探?

是的,我们可以,在这个配方中,你将通过利用AutoKeras中的一个特别功能——AutoModel 来学习如何操作!

如何操作……

按照这些步骤学习如何自定义AutoModel的搜索空间:

  1. 我们需要做的第一件事是导入所有必需的依赖项:

    from autokeras import *
    from tensorflow.keras.datasets import fashion_mnist as fm
    from tensorflow.keras.models import load_model
    from tensorflow.keras.utils import *
    
  2. 因为我们将在Fashion-MNIST上训练我们的自定义模型,所以我们必须分别加载训练和测试拆分数据:

    (X_train, y_train), (X_test, y_test) = fm.load_data()
    
  3. 为了避免数值不稳定问题,让我们将两个拆分的图像归一化到[0, 1]范围内:

    X_train = X_train.astype('float32')
    X_test = X_test.astype('float32')
    
  4. 定义create_automodel()函数,该函数定义了底层Block的自定义搜索空间,负责定义的任务,如图像增强、归一化、图像处理或分类。首先,我们必须定义输入块,它将通过Normalization()ImageAugmentation()块分别进行归一化和增强:

    def create_automodel(max_trials=10):
        input = ImageInput()
        x = Normalization()(input)
        x = ImageAugmentation(horizontal_flip=False,
                              vertical_flip=False)(x)
    

    注意,我们在ImageAugmentation()块中禁用了水平和垂直翻转。这是因为这些操作会改变Fashion-MNIST中图像的类别。

  5. 现在,我们将图表分叉。左侧分支使用ConvBlock()搜索普通卷积层。右侧分支,我们将探索更复杂的类似 Xception 的架构(有关Xception架构的更多信息,请参阅另见部分):

        left = ConvBlock()(x)
        right = XceptionBlock(pretrained=True)(x)
    

    在前面的代码片段中,我们指示AutoKeras只探索在 ImageNet 上预训练的Xception架构。

  6. 我们将合并左右两个分支,展开它们,然后通过DenseBlock()传递结果,正如它的名字所示,它会搜索完全连接的层组合:

        x = Merge()([left, right])
        x = SpatialReduction(reduction_type='flatten')(x)
        x = DenseBlock()(x)
    
  7. 这个图表的输出将是一个ClassificationHead()。这是因为我们处理的是一个分类问题。请注意,我们没有指定类别的数量。这是因为AutoKeras会从数据中推断出这些信息:

        output = ClassificationHead()(x)
    
  8. 我们可以通过构建并返回一个AutoModel()实例来结束create_automodel()。我们必须指定输入和输出,以及要执行的最大尝试次数:

        return AutoModel(inputs=input,
                         outputs=output,
                         overwrite=True,
                         max_trials=max_trials)
    
  9. 让我们训练每个试验模型 10 个周期:

    EPOCHS = 10
    
  10. 创建AutoModel并进行拟合:

    model = create_automodel()
    model.fit(X_train, y_train, epochs=EPOCHS)
    
  11. 让我们导出最佳模型:

    model = model.export_model()
    
  12. 在测试集上评估模型:

    print(model.evaluate(X_test, to_categorical(y_test)))
    
  13. 绘制最佳模型的架构:

    plot_model(model,
               show_shapes=True,
               show_layer_names=True,
               to_file='automodel.png')
    

    我最终得到的架构在测试集上的准确率达到了 90%,尽管你的结果可能有所不同。更有趣的是生成的模型的结构:

图 11.4 – AutoKeras 的最佳模型架构

图 11.4 – AutoKeras 的最佳模型架构

图 11.4 – AutoKeras 的最佳模型架构

上面的图表揭示了AutoModel根据我们在create_automodel()中设计的蓝图生成了一个网络。

现在,让我们进入它是如何工作的……部分。

它是如何工作的……

在这个示例中,我们利用了AutoModel模块来缩小搜索空间。当我们大致知道最终模型应该是什么样时,这个特性非常有用。因为我们不会让AutoKeras浪费时间尝试无效、无用的组合,所以可以节省大量时间。一个这样的无效组合的例子可以在第 4 步中看到,我们告诉AutoKeras不要将图像翻转作为图像增强的一部分。因为根据我们问题的特点,这个操作会改变Fashion-MNIST中数字的类别。

证明我们引导了create_automodel()函数。

很令人印象深刻,对吧?

另见

这里我们没有做的一件事是实现我们自己的Block,这在AutoKeras中是可能的。为什么不尝试一下呢?你可以从阅读这里的文档开始:autokeras.com/tutorial/customized/。要查看所有可用的模块,可以访问autokeras.com/block/。在这个示例中,我们使用了类似 Xception 的层。要了解更多关于 Xception 的信息,可以阅读原始论文:arxiv.org/abs/1610.02357

使用 AutoKeras 预测年龄和性别

在这个方案中,我们将学习 AutoML 的一个实际应用,可以作为模板来创建原型、MVP,或者仅仅借助 AutoML 来解决现实世界中的问题。

更具体地说,我们将创建一个年龄和性别分类程序,其中有一个特别的地方:性别和年龄分类器的架构将由AutoKeras负责。我们将负责获取和整理数据,并创建框架来在我们自己的图像上测试解决方案。

希望你准备好了,因为我们马上开始!

准备好了吗

我们需要几个外部库,比如 OpenCV、scikit-learnimutils。所有这些依赖项可以一次性安装,方法如下:

$> pip install opencv-contrib-python scikit-learn imutils

在数据方面,我们将使用Adience数据集,其中包含 26,580 张 2,284 个主体的图像,并附有其性别和年龄。要下载数据,请访问 talhassner.github.io/home/projects/Adience/Adience-data.html

接下来,你需要进入下载部分并输入你的姓名和电子邮件,如下图所示:

图 11.5 – 输入你的信息以接收存储数据的 FTP 服务器凭证

图 11.5 – 输入你的信息以接收存储数据的 FTP 服务器凭证

一旦你点击提交按钮,你将获得访问 FTP 服务器所需的凭证,该服务器存储着数据。你可以在这里访问:www.cslab.openu.ac.il/download/

确保点击第一个链接,标签为Adience OUI 未滤镜的人脸用于性别和年龄分类

图 11.6 – 进入高亮链接

图 11.6 – 进入高亮链接

输入你之前收到的凭证并访问第二个链接,名称为AdienceBenchmarkOfUnfilteredFacesForGenderAndAgeClassification

图 11.7 – 点击高亮链接

图 11.7 – 点击高亮链接

最后,下载 aligned.tar.gzfold_frontal_0_data.txtfold_frontal_1_data.txtfold_frontal_2_data.txtfold_frontal_3_data.txtfold_frontal_4_data.txt

图 11.8 – 下载 aligned.tar.gz 和所有 fold_frontal_*_data.txt 文件

图 11.8 – 下载 aligned.tar.gz 和所有 fold_frontal_*_data.txt 文件

解压 aligned.tar.gz 到你选择的目录中,命名为 adience。在该目录内,创建一个名为 folds 的子目录,并将所有 fold_frontal_*_data.txt 文件移动到其中。为了本方案的方便,我们假设数据集位于 ~/.keras/datasets/adience

下面是一些示例图像:

图 11.9 – 来自 Adience 数据集的示例图像

图 11.9 – 来自 Adience 数据集的示例图像

让我们实现这个方案吧!

如何操作…

完成以下步骤以实现一个年龄和性别分类器,使用AutoML

  1. 我们需要做的第一件事是导入所有必要的依赖:

    import csv
    import os
    import pathlib
    from glob import glob
    import cv2
    import imutils
    import numpy as np
    from autokeras import *
    from sklearn.preprocessing import LabelEncoder
    from tensorflow.keras.models import load_model
    from tensorflow.keras.preprocessing.image import *
    
  2. 定义Adience数据集的基本路径,以及包含图像与其受试者年龄和性别关系的折叠(CSV 格式):

    base_path = (pathlib.Path.home() / '.keras' / 'datasets' 
                     /'adience')
    folds_path = str(base_path / 'folds')
    
  3. Adience中的年龄以区间、组别或括号的形式表示。在这里,我们将定义一个数组,用于将折叠中的报告年龄映射到正确的区间:

    AGE_BINS = [(0, 2), (4, 6), (8, 13), (15, 20), (25, 32), 
                (38, 43), (48, 53), (60, 99)]
    
  4. 定义age_to_bin()函数,该函数接收一个输入(如折叠 CSV 行中的值),并将其映射到相应的区间。例如,如果输入是(27, 29),输出将是25_32

    def age_to_bin(age):
        age = age.replace('(', '').replace(')', '').
                                        split(',')
        lower, upper = [int(x.strip()) for x in age]
        for bin_low, bin_up in AGE_BINS:
            if lower >= bin_low and upper <= bin_up:
                label = f'{bin_low}_{bin_up}'
                return label
    
  5. 定义一个函数来计算矩形的面积。我们稍后将用它来获取最大的面部检测区域:

    def rectangle_area(r):
        return (r[2] - r[0]) * (r[3] - r[1])
    
  6. 我们还将绘制一个边框框住检测到的人脸,并附上识别出的年龄和性别:

    def plot_face(image, age_gender, detection):
        frame_x, frame_y, frame_width, frame_height = detection
        cv2.rectangle(image,
                      (frame_x, frame_y),
                      (frame_x + frame_width,
                       frame_y + frame_height),
                      color=(0, 255, 0),
                      thickness=2)
        cv2.putText(image,
                    age_gender,
                    (frame_x, frame_y - 10),
                    fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                    fontScale=0.45,
                    color=(0, 255, 0),
                    thickness=2)
        return image
    
  7. 定义predict()函数,我们将用它来预测传入roi参数的人的年龄和性别(取决于model):

    def predict(model, roi):
        roi = cv2.resize(roi, (64, 64))
        roi = roi.astype('float32') / 255.0
        roi = img_to_array(roi)
        roi = np.expand_dims(roi, axis=0)
        predictions = model.predict(roi)[0]
        return predictions
    
  8. 定义存储数据集中所有图像、年龄和性别的列表:

    images = []
    ages = []
    genders = []
    
  9. 遍历每个折叠文件。这些文件将是 CSV 格式:

    folds_pattern = os.path.sep.join([folds_path, '*.txt'])
    for fold_path in glob(folds_pattern):
        with open(fold_path, 'r') as f:
            reader = csv.DictReader(f, delimiter='\t')
    
  10. 如果年龄或性别字段不明确,跳过当前行:

            for line in reader:
                if ((line['age'][0] != '(') or
                        (line['gender'] not in {'m', 'f'})):
                    Continue
    
  11. 将年龄映射到一个有效的区间。如果从age_to_bin()返回None,这意味着年龄不对应我们定义的任何类别,因此必须跳过此记录:

                age_label = age_to_bin(line['age'])
                if age_label is None:
                    continue
    
  12. 加载图像:

                aligned_face_file = 
                               (f'landmark_aligned_face.'
                                     f'{line["face_id"]}.'
                              f'{line["original_image"]}')
                image_path = os.path.sep.join(
                                 [str(base_path),
                                 line["user_id"],
                               aligned_face_file])
                image = load_img(image_path, 
                                 target_size=(64, 64))
                image = img_to_array(image)
    
  13. 将图像、年龄和性别添加到相应的集合中:

                images.append(image)
                ages.append(age_label)
                genders.append(line['gender'])
    
  14. 为每个问题(年龄分类和性别预测)创建两份图像副本:

    age_images = np.array(images).astype('float32') / 255.0
    gender_images = np.copy(images)
    
  15. 编码年龄和性别:

    gender_enc = LabelEncoder()
    age_enc = LabelEncoder()
    gender_labels = gender_enc.fit_transform(genders)
    age_labels = age_enc.fit_transform(ages)
    
  16. 定义每次试验的次数和每次试验的周期。这些参数会影响两个模型:

    EPOCHS = 100
    MAX_TRIALS = 10
    
  17. 如果有训练好的年龄分类器,加载它;否则,从头开始训练一个ImageClassifier()并保存到磁盘:

    if os.path.exists('age_model.h5'):
        age_model = load_model('age_model.h5')
    else:
        age_clf = ImageClassifier(seed=9,
                                  max_trials=MAX_TRIALS,
                                  project_name='age_clf',
                                  overwrite=True)
        age_clf.fit(age_images, age_labels, epochs=EPOCHS)
        age_model = age_clf.export_model()
        age_model.save('age_model.h5')
    
  18. 如果有训练好的性别分类器,加载它;否则,从头开始训练一个ImageClassifier()并保存到磁盘:

    if os.path.exists('gender_model.h5'):
        gender_model = load_model('gender_model.h5')
    else:
        gender_clf = ImageClassifier(seed=9,
    
                                   max_trials=MAX_TRIALS,
                                project_name='gender_clf',
                                     overwrite=True)
        gender_clf.fit(gender_images, gender_labels,
                       epochs=EPOCHS)
        gender_model = gender_clf.export_model()
        gender_model.save('gender_model.h5')
    
  19. 从磁盘读取测试图像:

    image = cv2.imread('woman.jpg')
    
  20. 创建一个Haar Cascades人脸检测器。(这是本书范围之外的主题。如果你想了解更多关于 Haar Cascades 的内容,请参阅本配方的另见部分。)使用以下代码来完成:

    cascade_file = 'resources/haarcascade_frontalface_default.xml'
    det = cv2.CascadeClassifier(cascade_file)
    
  21. 调整图像大小,使其宽度为 380 像素。得益于imutils.resize()函数,我们可以放心结果会保持纵横比。因为该函数会自动计算高度以确保这一条件:

    image = imutils.resize(image, width=380)
    
  22. 创建原始图像的副本,以便我们可以在其上绘制检测结果:

    copy = image.copy()
    
  23. 将图像转换为灰度,并通过人脸检测器:

    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    detections = \
        det.detectMultiScale(gray,
                             scaleFactor=1.1,
                             minNeighbors=5,
                             minSize=(35, 35),
                          flags=cv2.CASCADE_SCALE_IMAGE)
    
  24. 验证是否有检测到的对象,并获取具有最大面积的一个:

    if len(detections) > 0:
        detections = sorted(detections, key=rectangle_area)
        best_detection = detections[-1]
    
  25. 提取与检测到的人脸相对应的兴趣区域(roi),并提取其年龄和性别:

        (frame_x, frame_y,
         frame_width, frame_height) = best_detection
        roi = image[frame_y:frame_y + frame_height,
                    frame_x:frame_x + frame_width]
        age_pred = predict(age_model, roi).argmax()
        age = age_enc.inverse_transform([age_pred])[0]
        gender_pred = predict(gender_model, roi).argmax()
        gender = gender_enc.inverse_transform([gender_pred])[0]
    

    注意,我们使用每个编码器来还原为人类可读的标签,用于预测的年龄和性别。

  26. 将预测的年龄和性别标注在原始图像上,并显示结果:

        clone = plot_face(copy,
                          f'Gender: {gender} - Age: 
                           {age}',
                          best_detection)
        cv2.imshow('Result', copy)
        cv2.waitKey(0)
    

    重要提示

    第一次执行此脚本时,您需要等待很长时间——可能超过 24 小时(取决于您的硬件)。这是因为每个模型都经过大量的试验和轮次训练。然而,之后的运行会更快,因为程序将加载已经训练好的分类器。

    我们可以在以下截图中看到一个成功预测年龄和性别的例子:

图 11.10 – 我们的模型判定照片中的人是女性,年龄在 25 至 32 岁之间。看起来差不多吧?

图 11.10 – 我们的模型判定照片中的人是女性,年龄在 25 至 32 岁之间。看起来差不多吧?

难道不是真正令人惊叹吗?所有的繁重工作都是由AutoKeras完成的!我们正在生活在未来!

它是如何工作的……

在这个示例中,我们实现了一个实际的解决方案,来应对一个令人意外的挑战性问题:年龄和性别预测。

为什么这很具挑战性?一个人的表面年龄可能会有所不同,取决于多种因素,如种族、性别、健康状况和其他生活条件。我们人类在仅凭外貌特征来估算一个人(男性或女性)的年龄时,并没有我们想象中的那么准确。

例如,一个大致健康的 25 岁的人与另一个 25 岁的重度饮酒和吸烟者看起来会有很大的不同。

无论如何,我们相信AutoML的力量,找到了两个模型:一个用于性别分类,另一个用于年龄预测。我们必须强调,在这种情况下,我们将年龄预测视为分类问题,而不是回归问题。这是因为这样更容易选择一个年龄范围,而不是给出一个精确的数值。

经过长时间的等待(我们对每个模型进行了超过 100 轮的训练),我们得到了两个合格的网络,并将它们整合到一个框架中,该框架可以自动识别照片中的人脸,并使用这些模型标注出预测的年龄和性别。

如您所见,我们依赖于ImageClassifier(),这意味着我们将网络创建过程的 100%控制权交给了AutoModel,以缩小搜索空间,从而在更短的时间内获得潜在的更好解决方案。为什么不试试看呢?

另见

阅读以下论文,了解Adience数据集的作者如何解决这个问题:talhassner.github.io/home/projects/cnn_agegender/CVPR2015_CNN_AgeGenderEstimation.pdf。要了解更多关于我们之前使用的 Haar Cascade 分类器的信息,请阅读这个教程:docs.opencv.org/3.4/db/d28/tutorial_cascade_classifier.html

第十二章:第十二章:提升性能

更多时候,从好到优秀的跃升并不涉及剧烈的变化,而是细微的调整和微调。

人们常说,20%的努力可以带来 80%的成果(这就是帕累托原则)。但是 80%和 100%之间的差距呢?我们需要做什么才能超越预期,改进我们的解决方案,最大限度地提升计算机视觉算法的性能?

嗯,和所有深度学习相关的事情一样,答案是艺术与科学的结合。好消息是,本章将专注于一些简单的工具,你可以用它们来提升神经网络的性能!

本章将涵盖以下方法:

  • 使用卷积神经网络集成来提高准确性

  • 使用测试时数据增强来提高准确性

  • 使用排名-N 准确率来评估性能

  • 使用标签平滑提高性能

  • 检查点模型

  • 使用tf.GradientTape自定义训练过程

  • 可视化类激活图以更好地理解你的网络

让我们开始吧!

技术要求

如往常一样,如果你能使用 GPU,你将能从这些方法中获得最大收益,因为本章中的某些示例对资源的需求相当高。此外,如果有任何你需要执行的准备工作以完成某个方法,你会在准备工作部分找到相关内容。最后,关于本章的代码可以在 GitHub 的附带仓库中找到:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch12

查看以下链接,观看代码实战视频:

bit.ly/2Ko3H3K

使用卷积神经网络集成来提高准确性

在机器学习中,最强大的分类器之一,实际上是一个元分类器,叫做集成。集成由所谓的弱分类器组成,弱分类器是指比随机猜测稍微好一点的预测模型。然而,当它们结合在一起时,结果是一个相当强大的算法,特别是在面对高方差(过拟合)时。我们可能遇到的一些最著名的集成方法包括随机森林和梯度提升机。

好消息是,当涉及到神经网络时,我们可以利用相同的原理,从而创造出一个整体效果,超越单独部分的总和。你想知道怎么做吗?继续阅读吧!

准备工作

本方法依赖于Pillowtensorflow_docs,可以通过以下方式轻松安装:

$> pip install Pillow git+https://github.com/tensorflow/docs

我们还将使用著名的Caltech 101数据集,可以在这里找到:www.vision.caltech.edu/Image_Datasets/Caltech101/。下载并解压101_ObjectCategories.tar.gz到你选择的位置。为了这个配方,我们将它放在~/.keras/datasets/101_ObjectCategories中。

以下是一些示例图像:

图 12.1 – Caltech 101 样本图像

](https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/tf20-cv-cb/img/B14768_12_001.jpg)

图 12.1 – Caltech 101 样本图像

让我们开始这个配方吧?

如何操作…

按照以下步骤创建卷积神经网络CNN)的集成:

  1. 导入所有必需的模块:

    import os
    import pathlib
    from glob import glob
    import numpy as np
    from sklearn.metrics import accuracy_score
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import LabelBinarizer
    from tensorflow.keras import Model
    from tensorflow.keras.layers import *
    from tensorflow.keras.preprocessing.image import *
    
  2. 定义load_images_and_labels()函数,读取Caltech 101数据集中的图像和类别,并将它们作为 NumPy 数组返回:

    def load_images_and_labels(image_paths, 
                               target_size=(64, 64)):
        images = []
        labels = []
        for image_path in image_paths:
            image = load_img(image_path, 
                              target_size=target_size)
            image = img_to_array(image)
            label = image_path.split(os.path.sep)[-2]
            images.append(image)
            labels.append(label)
        return np.array(images), np.array(labels)
    
  3. 定义build_model()函数,负责构建一个类似 VGG 的卷积神经网络:

    def build_network(width, height, depth, classes):
        input_layer = Input(shape=(width, height, depth))
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same')(input_layer)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
    

    现在,构建网络的全连接部分:

        x = Flatten()(x)
        x = Dense(units=512)(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Dropout(rate=0.25)(x)
        x = Dense(units=classes)(x)
        output = Softmax()(x)
        return Model(input_layer, output)
    
  4. 定义plot_model_history()函数,我们将用它来绘制集成中各个网络的训练和验证曲线:

    def plot_model_history(model_history, metric, 
                           plot_name):
        plt.style.use('seaborn-darkgrid')
        plotter = tfdocs.plots.HistoryPlotter()
        plotter.plot({'Model': model_history}, 
                      metric=metric)
        plt.title(f'{metric.upper()}')
        plt.ylim([0, 1])
        plt.savefig(f'{plot_name}.png')
        plt.close()
    
  5. 为了提高可复现性,设置一个随机种子:

    SEED = 999
    np.random.seed(SEED)
    
  6. 编译Caltech 101图像的路径以及类别:

    base_path = (pathlib.Path.home() / '.keras' / 
                  'datasets' /
                 '101_ObjectCategories')
    images_pattern = str(base_path / '*' / '*.jpg')
    image_paths = [*glob(images_pattern)]
    image_paths = [p for p in image_paths if
                   p.split(os.path.sep)[-2] !='BACKGROUND_Google']
    CLASSES = {p.split(os.path.sep)[-2] for p in image_paths}
    
  7. 加载图像和标签,同时对图像进行归一化,并对标签进行独热编码:

    X, y = load_images_and_labels(image_paths)
    X = X.astype('float') / 255.0
    y = LabelBinarizer().fit_transform(y)
    
  8. 保留 20%的数据用于测试,其余用于训练模型:

    (X_train, X_test,
     y_train, y_test) = train_test_split(X, y,
                                         test_size=0.2,
                                      random_state=SEED)
    
  9. 定义批次大小、训练轮次以及每个轮次的批次数:

    BATCH_SIZE = 64
    STEPS_PER_EPOCH = len(X_train) // BATCH_SIZE
    EPOCHS = 40
    
  10. 我们将在这里使用数据增强,执行一系列随机变换,如水平翻转、旋转和缩放:

    augmenter = ImageDataGenerator(horizontal_flip=True,
                                   rotation_range=30,
                                   width_shift_range=0.1,
                                   height_shift_range=0.1,
                                   shear_range=0.2,
                                   zoom_range=0.2,
                                   fill_mode='nearest')
    
  11. 我们的集成将包含5个模型。我们会将每个网络在集成中的预测保存到ensemble_preds列表中:

    NUM_MODELS = 5
    ensemble_preds = []
    
  12. 我们将以类似的方式训练每个模型。首先创建并编译网络本身:

    for n in range(NUM_MODELS):
        print(f'Training model {n + 1}/{NUM_MODELS}')
        model = build_network(64, 64, 3, len(CLASSES))
        model.compile(loss='categorical_crossentropy',
                      optimizer='rmsprop',
                      metrics=['accuracy'])
    
  13. 然后,我们将使用数据增强来拟合模型:

        train_generator = augmenter.flow(X_train, y_train,
                                         BATCH_SIZE)
        hist = model.fit(train_generator,
                         steps_per_epoch=STEPS_PER_EPOCH,
                         validation_data=(X_test, y_test),
                         epochs=EPOCHS,
                         verbose=2)
    
  14. 计算模型在测试集上的准确率,绘制训练和验证准确率曲线,并将其预测结果存储在ensemble_preds中:

        predictions = model.predict(X_test, 
                                   batch_size=BATCH_SIZE)
        accuracy = accuracy_score(y_test.argmax(axis=1),
                                  predictions.argmax(axis=1))
        print(f'Test accuracy (Model #{n + 1}): {accuracy}')
        plot_model_history(hist, 'accuracy', f'model_{n +1}')
        ensemble_preds.append(predictions)
    
  15. 最后一步是对每个集成成员的预测进行平均,从而有效地为整个元分类器产生联合预测,然后计算测试集上的准确率:

    ensemble_preds = np.average(ensemble_preds, axis=0)
    ensemble_acc = accuracy_score(y_test.argmax(axis=1),
                             ensemble_preds.argmax(axis=1))
    print(f'Test accuracy (ensemble): {ensemble_acc}')
    

    因为我们训练的是五个网络,所以这个程序可能需要一段时间才能完成。完成后,你应该能看到每个集成网络成员的准确率类似于以下内容:

    Test accuracy (Model #1): 0.6658986175115207
    Test accuracy (Model #2): 0.6751152073732719
    Test accuracy (Model #3): 0.673963133640553
    Test accuracy (Model #4): 0.6491935483870968
    Test accuracy (Model #5): 0.6756912442396313
    

    在这里,我们可以看到准确率在 65%到 67.5%之间。下图展示了模型 1 到 5 的训练和验证曲线(从左到右,上排为模型 1、2、3,下排为模型 4、5):

图 12.2 – 五个模型在集成中的训练和验证准确率曲线

](https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/tf20-cv-cb/img/B14768_12_002.jpg)

图 12.2 – 五个模型在集成中的训练和验证准确率曲线

然而,最有趣的结果是集成模型的准确性,这是通过平均每个模型的预测结果得出的:

Test accuracy (ensemble): 0.7223502304147466

真是令人印象深刻!仅仅通过结合五个网络的预测,我们就把准确率提升到了 72.2%,并且是在一个非常具有挑战性的数据集——Caltech 101上!我们将在下一部分进一步讨论这一点。

它是如何工作的…

在本教程中,我们通过在具有挑战性的Caltech 101数据集上训练五个神经网络,利用了集成的力量。必须指出的是,我们的过程相当简单而不起眼。我们首先加载并整理数据,使其适合训练,然后使用相同的模板训练多个 VGG 风格的架构副本。

为了创建更强大的分类器,我们使用了数据增强,并对每个网络进行了 40 个周期的训练。除了这些细节之外,我们没有改变网络的架构,也没有调整每个特定成员。结果是,每个模型在测试集上的准确率介于 65%和 67%之间。然而,当它们结合起来时,达到了一个不错的 72%!

那么,为什么会发生这种情况呢?集成学习的原理是每个模型在训练过程中都会形成自己的偏差,这是深度学习随机性质的结果。然而,通过投票过程(基本上就是平均它们的预测结果)结合它们的决策时,这些差异会被平滑掉,从而给出更强健的结果。

当然,训练多个模型是一项资源密集型任务,根据问题的规模和复杂性,这可能完全不可能实现。然而,这仍然是一个非常有用的工具,通过创建并结合多个相同网络的副本,可以提高预测能力。

不错吧?

另见

如果你想了解集成背后的数学原理,可以阅读这篇关于詹森不等式的文章:en.wikipedia.org/wiki/Jensen%27s_inequality

使用测试时增强提高准确性

大多数情况下,当我们测试一个网络的预测能力时,我们会使用一个测试集。这个测试集包含了模型从未见过的图像。然后,我们将它们呈现给模型,并询问模型每个图像属于哪个类别。问题是……我们只做了一次

如果我们更宽容一点,给模型多次机会去做这个任务呢?它的准确性会提高吗?嗯,往往会提高!

这种技术被称为测试时增强TTA),它是本教程的重点。

准备工作

为了加载数据集中的图像,我们需要Pillow。可以使用以下命令安装:

$> pip install Pillow

然后,下载Caltech 101数据集,地址如下:www.vision.caltech.edu/Image_Datasets/Caltech101/。下载并解压101_ObjectCategories.tar.gz到你选择的位置。在本食谱的其余部分中,我们将假设数据集位于~/.keras/datasets/101_ObjectCategories

这是Caltech 101中可以找到的一个示例:

![图 12.3 – Caltech 101 样本图像]

](https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/tf20-cv-cb/img/B14768_12_003.jpg)

图 12.3 – Caltech 101 样本图像

我们准备开始了!

如何操作……

按照这些步骤学习 TTA 的好处:

  1. 导入我们需要的依赖项:

    import os
    import pathlib
    from glob import glob
    import numpy as np
    from sklearn.metrics import accuracy_score
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import LabelBinarizer
    from tensorflow.keras import Model
    from tensorflow.keras.layers import *
    from tensorflow.keras.preprocessing.image import *
    
  2. 定义load_images_and_labels()函数,以便从Caltech 101(NumPy 格式)中读取数据:

    def load_images_and_labels(image_paths, 
                               target_size=(64, 64)):
        images = []
        labels = []
        for image_path in image_paths:
            image = load_img(image_path, 
                            target_size=target_size)
            image = img_to_array(image)
            label = image_path.split(os.path.sep)[-2]
            images.append(image)
            labels.append(label)
        return np.array(images), np.array(labels)
    
  3. 定义build_model()函数,该函数基于著名的VGG架构返回一个网络:

    def build_network(width, height, depth, classes):
        input_layer = Input(shape=(width, height, depth))
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same')(input_layer)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
    

    现在,构建网络的全连接部分:

        x = Flatten()(x)
        x = Dense(units=512)(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Dropout(rate=0.25)(x)
        x = Dense(units=classes)(x)
        output = Softmax()(x)
        return Model(input_layer, output)
    
  4. flip_augment()函数是我们TTA方案的基础。它接收一张图像,并生成其副本,这些副本可以随机水平翻转(50%的概率):

    def flip_augment(image, num_test=10):
        augmented = []
        for i in range(num_test):
            should_flip = np.random.randint(0, 2)
            if should_flip:
                flipped = np.fliplr(image.copy())
                augmented.append(flipped)
            else:
                augmented.append(image.copy())
        return np.array(augmented)
    
  5. 为了确保可重复性,请设置随机种子:

    SEED = 84
    np.random.seed(SEED)
    
  6. 编译Caltech 101图像的路径及其类别:

    base_path = (pathlib.Path.home() / '.keras' / 
                 'datasets' /'101_ObjectCategories')
    images_pattern = str(base_path / '*' / '*.jpg')
    image_paths = [*glob(images_pattern)]
    image_paths = [p for p in image_paths if
                   p.split(os.path.sep)[-2] 
                   !='BACKGROUND_Google']
    CLASSES = {p.split(os.path.sep)[-2] for p in 
               image_paths}
    
  7. 加载图像和标签,同时对图像进行标准化,并对标签进行独热编码:

    X, y = load_images_and_labels(image_paths)
    X = X.astype('float') / 255.0
    y = LabelBinarizer().fit_transform(y)
    
  8. 使用 20%的数据进行测试,剩余部分用于训练模型:

    (X_train, X_test,
     y_train, y_test) = train_test_split(X, y, test_size=0.2,
                                      random_state=SEED)
    
  9. 定义批次大小和周期数:

    BATCH_SIZE = 64
    EPOCHS = 40
    
  10. 我们将随机地水平翻转训练集中的图像:

    augmenter = ImageDataGenerator(horizontal_flip=True)
    
  11. 构建并编译网络:

    model = build_network(64, 64, 3, len(CLASSES))
    model.compile(loss='categorical_crossentropy',
                  optimizer='adam',
                  metrics=['accuracy'])
    
  12. 拟合模型:

    train_generator = augmenter.flow(X_train, y_train,
                                     BATCH_SIZE)
    model.fit(train_generator,
              steps_per_epoch=len(X_train) // BATCH_SIZE,
              validation_data=(X_test, y_test),
              epochs=EPOCHS,
              verbose=2)
    
  13. 对测试集进行预测,并利用预测结果计算模型的准确性:

    predictions = model.predict(X_test,
                                batch_size=BATCH_SIZE)
    accuracy = accuracy_score(y_test.argmax(axis=1), 
                              predictions.argmax(axis=1))
    print(f'Accuracy, without TTA: {accuracy}')
    
  14. 现在,我们将在测试集上使用TTA。我们将把每个图像副本的预测结果存储在预测列表中。我们将创建每个图像的 10 个副本:

    predictions = []
    NUM_TEST = 10
    
  15. 接下来,我们将对测试集中的每个图像进行迭代,创建其副本的批次,并将其传递通过模型:

    for index in range(len(X_test)):
        batch = flip_augment(X_test[index], NUM_TEST)
        sample_predictions = model.predict(batch)
    
  16. 每张图像的最终预测将是该批次中预测最多的类别:

        sample_predictions = np.argmax(
            np.sum(sample_predictions, axis=0))
        predictions.append(sample_predictions)
    
  17. 最后,我们将使用 TTA 计算模型预测的准确性:

    accuracy = accuracy_score(y_test.argmax(axis=1), 
                              predictions)
    print(f'Accuracy with TTA: {accuracy}')
    

    稍等片刻,我们将看到类似于这些的结果:

    Accuracy, without TTA: 0.6440092165898618
    Accuracy with TTA: 0.6532258064516129
    

网络在没有 TTA 的情况下达到 64.4%的准确率,而如果我们给模型更多机会生成正确预测,准确率将提高到 65.3%。很酷,对吧?

让我们进入如何工作……部分。

如何工作……

在这个示例中,我们学习到测试时增强是一种简单的技术,一旦网络训练完成,只需要做少量修改。其背后的原因是,如果我们在测试集中给网络呈现一些与训练时图像相似的变化图像,网络的表现会更好。

然而,关键在于,这些变换应当在评估阶段进行,且应该与训练期间的变换相匹配;否则,我们将向模型输入不一致的数据!

但是有一个警告:TTA 实际上非常非常慢!毕竟,我们是在将测试集的大小乘以增强因子,在我们的例子中是 10。这意味着网络不再一次处理一张图像,而是必须处理 10 张图像。

当然,TTA 不适合实时或速度受限的应用,但当时间或速度不是问题时,它仍然可以非常有用。

使用 rank-N 准确度来评估性能

大多数时候,当我们训练基于深度学习的图像分类器时,我们关心的是准确度,它是模型性能的一个二元度量,基于模型的预测与真实标签之间的一对一比较。当模型说照片中有一只 时,照片中真的有 吗?换句话说,我们衡量的是模型的 精确度

然而,对于更复杂的数据集,这种评估网络学习的方法可能适得其反,甚至不公平,因为它过于限制。假如模型没有将图片中的猫科动物识别为 ,而是误识别为 老虎 呢?更重要的是,如果第二可能性最大的类别确实是 呢?这意味着模型还需要进一步学习,但它正在逐步接近目标!这很有价值!

这就是 rank-N 准确度 的原理,它是一种更宽容、更公平的评估预测模型性能的方法,当真实标签出现在模型输出的前 N 个最可能类别中时,该预测会被视为正确。在本教程中,我们将学习如何实现并使用这种方法。

让我们开始吧。

准备工作

安装 Pillow

$> pip install Pillow

接下来,下载并解压 Caltech 101 数据集,数据集可以在这里找到:www.vision.caltech.edu/Image_Datasets/Caltech101/。确保点击下载 101_ObjectCategories.tar.gz 文件。下载后,将其放在你选择的位置。在本教程的后续部分,我们将假设数据集位于 ~/.keras/datasets/101_ObjectCategories 目录下。

这是 Caltech 101 的一个样本:

图 12.4 – Caltech 101 样本图像

图 12.4 – Caltech 101 样本图像

让我们开始实现这个教程吧!

如何实现…

按照以下步骤实现并使用 rank-N 准确度

  1. 导入必要的模块:

    import os
    import pathlib
    from glob import glob
    import numpy as np
    from sklearn.metrics import accuracy_score
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import LabelBinarizer
    from tensorflow.keras import Model
    from tensorflow.keras.layers import *
    from tensorflow.keras.preprocessing.image import *
    
  2. 定义 load_images_and_labels() 函数,用于从 Caltech 101 读取数据:

    def load_images_and_labels(image_paths, 
                               target_size=(64, 64)):
        images = []
        labels = []
        for image_path in image_paths:
            image = load_img(image_path, 
                             target_size=target_size)
            image = img_to_array(image)
            label = image_path.split(os.path.sep)[-2]
            images.append(image)
            labels.append(label)
        return np.array(images), np.array(labels)
    
  3. 定义 build_model() 函数,创建一个 VGG 风格的网络:

    def build_network(width, height, depth, classes):
        input_layer = Input(shape=(width, height, depth))
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same')(input_layer)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
    

    现在,构建网络的全连接部分:

        x = Flatten()(x)
        x = Dense(units=512)(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Dropout(rate=0.25)(x)
        x = Dense(units=classes)(x)
        output = Softmax()(x)
        return Model(input_layer, output)
    
  4. 定义 rank_n() 函数,它根据预测结果和真实标签计算 rank-N 准确度。请注意,它会输出一个介于 0 和 1 之间的值,当真实标签出现在模型输出的前 N 个最可能的类别中时,才会算作“命中”或正确预测:

    def rank_n(predictions, labels, n):
        score = 0.0
        for prediction, actual in zip(predictions, labels):
            prediction = np.argsort(prediction)[::-1]
            if actual in prediction[:n]:
                score += 1
        return score / float(len(predictions))
    
  5. 为了可复现性,设置随机种子:

    SEED = 42
    np.random.seed(SEED)
    
  6. 编译 Caltech 101 的图像路径,以及它的类别:

    base_path = (pathlib.Path.home() / '.keras' / 'datasets' /
                 '101_ObjectCategories')
    images_pattern = str(base_path / '*' / '*.jpg')
    image_paths = [*glob(images_pattern)]
    image_paths = [p for p in image_paths if
            p.split(os.path.sep)[-2] !='BACKGROUND_Google']
    CLASSES = {p.split(os.path.sep)[-2] for p in image_paths}
    
  7. 加载图像和标签,同时对图像进行归一化处理并对标签进行独热编码:

    X, y = load_images_and_labels(image_paths)
    X = X.astype('float') / 255.0
    y = LabelBinarizer().fit_transform(y)
    
  8. 将 20%的数据用于测试,剩下的用于训练模型:

    (X_train, X_test,
     y_train, y_test) = train_test_split(X, y,
                                         test_size=0.2,
                                      random_state=SEED)
    
  9. 定义批次大小和训练轮数:

    BATCH_SIZE = 64
    EPOCHS = 40
    
  10. 定义一个ImageDataGenerator(),以随机翻转、旋转和其他转换来增强训练集中的图像:

    augmenter = ImageDataGenerator(horizontal_flip=True,
                                   rotation_range=30,
                                   width_shift_range=0.1,
                                   height_shift_range=0.1,
                                   shear_range=0.2,
                                   zoom_range=0.2,
                                   fill_mode='nearest')
    
  11. 构建并编译网络:

    model = build_network(64, 64, 3, len(CLASSES))
    model.compile(loss='categorical_crossentropy',
                  optimizer='adam',
                  metrics=['accuracy'])
    
  12. 拟合模型:

    train_generator = augmenter.flow(X_train, y_train,
                                     BATCH_SIZE)
    model.fit(train_generator,
              steps_per_epoch=len(X_train) // BATCH_SIZE,
              validation_data=(X_test, y_test),
              epochs=EPOCHS,
              verbose=2)
    
  13. 在测试集上进行预测:

    predictions = model.predict(X_test, 
                                batch_size=BATCH_SIZE)
    
  14. 计算排名-1(常规准确率)、排名-3、排名-5 和排名-10 的准确率:

    y_test = y_test.argmax(axis=1)
    for n in [1, 3, 5, 10]:
        rank_n_accuracy = rank_n(predictions, y_test, n=n) * 100
        print(f'Rank-{n}: {rank_n_accuracy:.2f}%')
    

    以下是结果:

    Rank-1: 64.29%
    Rank-3: 78.05%
    Rank-5: 83.01%
    Rank-10: 89.69%
    

在这里,我们可以观察到,64.29%的时间,网络产生了完全匹配的结果。然而,78.05%的时间,正确的预测出现在前 3 名,83.01%的时间出现在前 5 名,几乎 90%的时间出现在前 10 名。这些结果相当有趣且令人鼓舞,考虑到我们的数据集包含了 101 个彼此差异很大的类别。

我们将在如何工作...部分深入探讨。

如何工作…

在本教程中,我们了解了排名-N 准确率的存在和实用性。我们还通过一个简单的函数rank_n()实现了它,并在一个已经训练过具有挑战性的Caltech-101数据集的网络上进行了测试。

排名-N,尤其是排名-1 和排名-5 的准确率,在训练过大型且具有挑战性数据集的网络文献中很常见,例如 COCO 或 ImageNet,这些数据集即使人类也很难区分不同类别。它在我们有细粒度类别且这些类别共享一个共同的父类或祖先时尤为有用,例如巴哥犬金毛猎犬,它们都是的品种。

排名-N 之所以有意义,是因为一个训练良好的模型,能够真正学会泛化,将在其前 N 个预测中产生语境上相似的类别(通常是前 5 名)。

当然,我们也可以把排名-N 准确率使用得过头,直到它失去意义和实用性。例如,在MNIST数据集上进行排名-5 准确率评估,该数据集仅包含 10 个类别,这几乎是没有意义的。

另见

想看到排名-N 在实际应用中的效果吗?请查看这篇论文的结果部分:arxiv.org/pdf/1610.02357.pdf

使用标签平滑提高性能

机器学习中我们必须不断应对的一个常见问题是过拟合。我们可以使用多种技术来防止模型丧失泛化能力,例如 dropout、L1 和 L2 正则化,甚至数据增强。最近加入这个组的一个新技术是标签平滑,它是独热编码的一个更宽容的替代方案。

而在独热编码中,我们通过二进制向量表示每个类别,其中唯一非零元素对应被编码的类别,使用标签平滑时,我们将每个标签表示为一个概率分布,其中所有元素都有非零的概率。最高概率对应的类别,当然是与编码类别相符的。

例如,平滑后的* [0, 1, 0] 向量会变成 [0.01, 0.98, 0.01] *。

在这个教程中,我们将学习如何使用标签平滑。继续阅读!

准备就绪

安装Pillow,我们需要它来处理数据集中的图像:

$> pip install Pillow

访问Caltech 101官网:www.vision.caltech.edu/Image_Datasets/Caltech101/。下载并解压名为101_ObjectCategories.tar.gz的文件到你喜欢的目录。从现在开始,我们假设数据位于~/.keras/datasets/101_ObjectCategories

这里是Caltech 101的一个样本:

图 12.5 – Caltech 101 样本图像

图 12.5 – Caltech 101 样本图像

让我们开始吧!

如何操作…

按照以下步骤完成这个教程:

  1. 导入必要的依赖项:

    import os
    import pathlib
    from glob import glob
    import numpy as np
    from sklearn.metrics import accuracy_score
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import LabelBinarizer
    from tensorflow.keras import Model
    from tensorflow.keras.layers import *
    from tensorflow.keras.losses import CategoricalCrossentropy
    from tensorflow.keras.preprocessing.image import *
    
  2. 创建load_images_and_labels()函数,用来从Caltech 101读取数据:

    def load_images_and_labels(image_paths, 
                               target_size=(64, 64)):
        images = []
        labels = []
        for image_path in image_paths:
            image = load_img(image_path, 
                             target_size=target_size)
            image = img_to_array(image)
            label = image_path.split(os.path.sep)[-2]
            images.append(image)
            labels.append(label)
        return np.array(images), np.array(labels)
    
  3. 实现build_model()函数,创建一个基于VGG的网络:

    def build_network(width, height, depth, classes):
        input_layer = Input(shape=(width, height, depth))
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same')(input_layer)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
    

    现在,构建网络的全连接部分:

        x = Flatten()(x)
        x = Dense(units=512)(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Dropout(rate=0.25)(x)
        x = Dense(units=classes)(x)
        output = Softmax()(x)
        return Model(input_layer, output)
    
  4. 设置一个随机种子以增强可复现性:

    SEED = 9
    np.random.seed(SEED)
    
  5. 编译Caltech 101的图像路径以及其类别:

    base_path = (pathlib.Path.home() / '.keras' / 'datasets'         
                  /'101_ObjectCategories')
    images_pattern = str(base_path / '*' / '*.jpg')
    image_paths = [*glob(images_pattern)]
    image_paths = [p for p in image_paths if
            p.split(os.path.sep)[-2] !='BACKGROUND_Google']
    CLASSES = {p.split(os.path.sep)[-2] for p in image_paths}
    
  6. 加载图像和标签,同时对图像进行归一化,并对标签进行独热编码:

    X, y = load_images_and_labels(image_paths)
    X = X.astype('float') / 255.0
    y = LabelBinarizer().fit_transform(y)
    
  7. 使用 20%的数据作为测试数据,其余数据用于训练模型:

    (X_train, X_test,
     y_train, y_test) = train_test_split(X, y,
                                         test_size=0.2,
                                       random_state=SEED)
    
  8. 定义批次大小和训练轮次:

    BATCH_SIZE = 128
    EPOCHS = 40
    
  9. 定义一个ImageDataGenerator()来通过随机翻转、旋转和其他变换增强训练集中的图像:

    augmenter = ImageDataGenerator(horizontal_flip=True,
                                   rotation_range=30,
                                   width_shift_range=0.1,
                                   height_shift_range=0.1,
                                   shear_range=0.2,
                                   zoom_range=0.2,
                                   fill_mode='nearest')
    
  10. 我们将训练两个模型:一个使用标签平滑,另一个不使用。这将让我们比较它们的表现,评估标签平滑是否对性能有影响。两个案例的逻辑几乎相同,从模型创建过程开始:

    for with_label_smoothing in [False, True]:
        model = build_network(64, 64, 3, len(CLASSES))
    
  11. 如果with_label_smoothingTrue,则我们将平滑因子设置为 0.1。否则,平滑因子为 0,这意味着我们将使用常规的独热编码:

        if with_label_smoothing:
            factor = 0.1
        else:
            factor = 0
    
  12. 我们应用损失函数——在这种情况下是CategoricalCrossentropy()

        loss = CategoricalCrossentropy(label_smoothing=factor)
    
  13. 编译并训练模型:

        model.compile(loss=loss,
                      optimizer='rmsprop',
                      metrics=['accuracy'])
        train_generator = augmenter.flow(X_train, y_train,
                                         BATCH_SIZE)
        model.fit(train_generator,
                  steps_per_epoch=len(X_train) // 
                  BATCH_SIZE,
                  validation_data=(X_test, y_test),
                  epochs=EPOCHS,
                  verbose=2)
    
  14. 在测试集上进行预测并计算准确率:

        predictions = model.predict(X_test, 
                                   batch_size=BATCH_SIZE)
        accuracy = accuracy_score(y_test.argmax(axis=1),
                                  predictions.argmax(axis=1))
        print(f'Test accuracy '
              f'{"with" if with_label_smoothing else 
               "without"} '
              f'label smoothing: {accuracy * 100:.2f}%')
    

    脚本将训练两个模型:一个不使用损失函数。以下是结果:

    Test accuracy without label smoothing: 65.09%
    Test accuracy with label smoothing: 65.78%
    

仅仅通过使用标签平滑,我们提高了测试分数接近 0.7%,这是一个不可忽视的提升,考虑到我们的数据集大小和复杂性。在下一部分我们将深入探讨。

它是如何工作的……

在这个教程中,我们学习了如何应用CategoricalCrossentropy()损失函数,用于衡量网络的学习效果。

那么,为什么标签平滑有效呢?尽管在许多深度学习领域中被广泛应用,包括自然语言处理NLP)和当然的计算机视觉标签平滑仍然没有被完全理解。然而,许多人(包括我们在这个示例中的研究者)观察到,通过软化目标,网络的泛化能力和学习速度通常显著提高,防止其过于自信,从而保护我们免受过拟合的负面影响。

要了解有关标签平滑的有趣见解,请阅读另见部分提到的论文。

另见

这篇论文探讨了标签平滑有效的原因,以及它何时无效。值得一读!你可以在这里下载:arxiv.org/abs/1906.02629

检查点模型

训练一个深度神经网络是一个耗时且消耗存储和资源的过程。每次我们想要使用网络时重新训练它是不合理且不切实际的。好消息是,我们可以使用一种机制,在训练过程中自动保存网络的最佳版本。

在这个教程中,我们将讨论一种机制,称为检查点。

如何做……

按照以下步骤了解您在 TensorFlow 中可以使用的不同检查点方式:

  1. 导入我们将使用的模块:

    import numpy as np
    import tensorflow as tf
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import LabelBinarizer
    from tensorflow.keras.callbacks import ModelCheckpoint
    from tensorflow.keras.datasets import fashion_mnist as fm
    from tensorflow.keras.layers import *
    from tensorflow.keras.models import *
    
  2. 定义一个函数,将Fashion-MNIST加载到tf.data.Datasets中:

    def load_dataset():
        (X_train, y_train), (X_test, y_test) = fm.load_data()
        X_train = X_train.astype('float32') / 255.0
        X_test = X_test.astype('float32') / 255.0
        X_train = np.expand_dims(X_train, axis=3)
        X_test = np.expand_dims(X_test, axis=3)
        label_binarizer = LabelBinarizer()
        y_train = label_binarizer.fit_transform(y_train)
        y_test = label_binarizer.fit_transform(y_test)
    
  3. 使用 20% 的训练数据来验证数据集:

        (X_train, X_val,
         y_train, y_val) = train_test_split(X_train, y_train,
                                            train_size=0.8)
    
  4. 将训练集、测试集和验证集转换为tf.data.Datasets

        train_ds = (tf.data.Dataset
                    .from_tensor_slices((X_train, 
                                         y_train)))
        val_ds = (tf.data.Dataset
                  .from_tensor_slices((X_val, y_val)))
        test_ds = (tf.data.Dataset
                   .from_tensor_slices((X_test, y_test)))
        train_ds = (train_ds.shuffle(buffer_size=BUFFER_SIZE)
                    .batch(BATCH_SIZE)
                    .prefetch(buffer_size=BUFFER_SIZE))
        val_ds = (val_ds
                  .batch(BATCH_SIZE)
                  .prefetch(buffer_size=BUFFER_SIZE))
        test_ds = test_ds.batch(BATCH_SIZE)
        return train_ds, val_ds, test_ds
    
  5. 定义build_network()方法,顾名思义,它创建我们将在Fashion-MNIST上训练的模型:

    def build_network():
        input_layer = Input(shape=(28, 28, 1))
        x = Conv2D(filters=20,
                   kernel_size=(5, 5),
                   padding='same',
                   strides=(1, 1))(input_layer)
        x = ELU()(x)
        x = BatchNormalization()(x)
        x = MaxPooling2D(pool_size=(2, 2),
                         strides=(2, 2))(x)
        x = Dropout(0.5)(x)
        x = Conv2D(filters=50,
                   kernel_size=(5, 5),
                   padding='same',
                   strides=(1, 1))(x)
        x = ELU()(x)
        x = BatchNormalization()(x)
        x = MaxPooling2D(pool_size=(2, 2),
                         strides=(2, 2))(x)
        x = Dropout(0.5)(x)
    

    现在,构建网络的全连接部分:

        x = Flatten()(x)
        x = Dense(units=500)(x)
        x = ELU()(x)
        x = Dropout(0.5)(x)
        x = Dense(10)(x)
        output = Softmax()(x)
        return Model(inputs=input_layer, outputs=output)
    
  6. 定义train_and_checkpoint()函数,它加载数据集,然后根据checkpointer参数中设定的逻辑构建、编译并训练网络,同时保存检查点。

    def train_and_checkpoint(checkpointer):
        train_dataset, val_dataset, test_dataset = load_dataset()
    
        model = build_network()
        model.compile(loss='categorical_crossentropy',
                      optimizer='adam',
                      metrics=['accuracy'])
        model.fit(train_dataset,
                  validation_data=val_dataset,
                  epochs=EPOCHS,
                  callbacks=[checkpointer])
    
  7. 定义批量大小、训练模型的轮次数以及每个数据子集的缓冲区大小:

    BATCH_SIZE = 256
    BUFFER_SIZE = 1024
    EPOCHS = 100
    
  8. 生成检查点的第一种方式是每次迭代后保存一个不同的模型。为此,我们必须将save_best_only=False传递给ModelCheckpoint()

    checkpoint_pattern = (
        'save_all/model-ep{epoch:03d}-loss{loss:.3f}'
        '-val_loss{val_loss:.3f}.h5')
    checkpoint = ModelCheckpoint(checkpoint_pattern,
                                 monitor='val_loss',
                                 verbose=1,
                                 save_best_only=False,
                                 mode='min')
    train_and_checkpoint(checkpoint)
    

    注意,我们将所有的检查点保存在save_all文件夹中,检查点模型名称中包含了轮次、损失和验证损失。

  9. 一种更高效的检查点方式是仅保存迄今为止最好的模型。我们可以通过在ModelCheckpoint()中将save_best_only设置为True来实现:

    checkpoint_pattern = (
        'best_only/model-ep{epoch:03d}-loss{loss:.3f}'
        '-val_loss{val_loss:.3f}.h5')
    checkpoint = ModelCheckpoint(checkpoint_pattern,
                                 monitor='val_loss',
                                 verbose=1,
                                 save_best_only=True,
                                 mode='min')
    train_and_checkpoint(checkpoint)
    

    我们将把结果保存在best_only目录中。

  10. 一种更简洁的生成检查点的方式是只保存一个与当前最佳模型相对应的检查点,而不是存储每个逐步改进的模型。为了实现这一点,我们可以从检查点名称中删除任何参数:

    checkpoint_pattern = 'overwrite/model.h5'
    checkpoint = ModelCheckpoint(checkpoint_pattern,
                                 monitor='val_loss',
                                 verbose=1,
                                 save_best_only=True,
                                 mode='min')
    train_and_checkpoint(checkpoint)
    

    运行这三个实验后,我们可以检查每个输出文件夹,看看生成了多少个检查点。在第一个实验中,我们在每个 epoch 后保存了一个模型,如下图所示:

图 12.6 – 实验 1 结果

图 12.6 – 实验 1 结果

这种方法的缺点是,我们会得到很多无用的快照。优点是,如果需要的话,我们可以通过加载相应的 epoch 恢复训练。更好的方法是只保存到目前为止最好的模型,正如以下截图所示,这样生成的模型会更少。通过检查检查点名称,我们可以看到每个检查点的验证损失都低于前一个:

图 12.7 – 实验 2 结果

图 12.7 – 实验 2 结果

最后,我们可以只保存最好的模型,如下图所示:

图 12.8 – 实验 3 结果

图 12.8 – 实验 3 结果

让我们继续进入下一部分。

它是如何工作的……

在这个教程中,我们学习了如何进行模型检查点保存,这为我们节省了大量时间,因为我们不需要从头开始重新训练模型。检查点保存非常棒,因为我们可以根据自己的标准保存最好的模型,例如验证损失、训练准确度或任何其他度量标准。

通过利用 ModelCheckpoint() 回调,我们可以在每个完成的 epoch 后保存网络的快照,从而仅保留最好的模型或训练过程中产生的最佳模型历史。

每种策略都有其优缺点。例如,在每个 epoch 后生成模型的好处是我们可以从任何 epoch 恢复训练,但这会占用大量磁盘空间,而仅保存最好的模型则能节省空间,但会降低我们的实验灵活性。

你将在下一个项目中使用什么策略?

使用 tf.GradientTape 自定义训练过程

TensorFlow 的最大竞争对手之一是另一个著名框架:PyTorch。直到 TensorFlow 2.x 发布之前,PyTorch 的吸引力在于它给用户提供的控制程度,尤其是在训练神经网络时。

如果我们正在处理一些传统的神经网络以解决常见问题,如图像分类,我们不需要对如何训练模型进行过多控制,因此可以依赖 TensorFlow(或 Keras API)的内置功能、损失函数和优化器,而没有问题。

但是,如果我们是那些探索新方法、新架构以及解决挑战性问题的新策略的研究人员呢?过去,正因为 PyTorch 在自定义训练模型方面比 TensorFlow 1.x 容易得多,我们才不得不使用它,但现在情况已经不同了!TensorFlow 2.x 的tf.GradientTape使得我们能够更加轻松地为 Keras 和低级 TensorFlow 实现的模型创建自定义训练循环,在本章节中,我们将学习如何使用它。

如何做到…

按照以下步骤完成本章节:

  1. 导入我们将使用的模块:

    import time
    import numpy as np
    import tensorflow as tf
    from tensorflow.keras.datasets import fashion_mnist as fm
    from tensorflow.keras.layers import *
    from tensorflow.keras.losses import categorical_crossentropy
    from tensorflow.keras.models import Model
    from tensorflow.keras.optimizers import RMSprop
    from tensorflow.keras.utils import to_categorical
    
  2. 定义一个函数来加载和准备Fashion-MNIST

    def load_dataset():
        (X_train, y_train), (X_test, y_test) = fm.load_data()
        X_train = X_train.astype('float32') / 255.0
        X_test = X_test.astype('float32') / 255.0
        # Reshape grayscale to include channel dimension.
        X_train = np.expand_dims(X_train, axis=-1)
        X_test = np.expand_dims(X_test, axis=-1)
        y_train = to_categorical(y_train)
        y_test = to_categorical(y_test)
        return (X_train, y_train), (X_test, y_test)
    
  3. 定义build_network()方法,顾名思义,它创建我们将在Fashion-MNIST上训练的模型:

    def build_network():
        input_layer = Input(shape=(28, 28, 1))
        x = Conv2D(filters=20,
                   kernel_size=(5, 5),
                   padding='same',
                   strides=(1, 1))(input_layer)
        x = ELU()(x)
        x = BatchNormalization()(x)
        x = MaxPooling2D(pool_size=(2, 2),
                         strides=(2, 2))(x)
        x = Dropout(0.5)(x)
        x = Conv2D(filters=50,
                   kernel_size=(5, 5),
                   padding='same',
                   strides=(1, 1))(x)
        x = ELU()(x)
        x = BatchNormalization()(x)
        x = MaxPooling2D(pool_size=(2, 2),
                         strides=(2, 2))(x)
        x = Dropout(0.5)(x)
    

    现在,构建网络的全连接部分:

        x = Flatten()(x)
        x = Dense(units=500)(x)
        x = ELU()(x)
        x = Dropout(0.5)(x)
        x = Dense(10)(x)
        output = Softmax()(x)
        return Model(inputs=input_layer, outputs=output)
    
  4. 为了演示如何使用tf.GradientTape,我们将实现training_step()函数,该函数获取一批数据的梯度,然后通过优化器进行反向传播:

    def training_step(X, y, model, optimizer):
        with tf.GradientTape() as tape:
            predictions = model(X)
            loss = categorical_crossentropy(y, predictions)
        gradients = tape.gradient(loss, 
                              model.trainable_variables)
        optimizer.apply_gradients(zip(gradients,
                              model.trainable_variables))
    
  5. 定义批次大小和训练模型的轮次:

    BATCH_SIZE = 256
    EPOCHS = 100
    
  6. 加载数据集:

    (X_train, y_train), (X_test, y_test) = load_dataset()
    
  7. 创建优化器和网络:

    optimizer = RMSprop()
    model = build_network()
    
  8. 现在,我们将创建自定义训练循环。首先,我们将遍历每个轮次,衡量完成的时间:

    for epoch in range(EPOCHS):
        print(f'Epoch {epoch + 1}/{EPOCHS}')
        start = time.time()
    
  9. 现在,我们将遍历每个数据批次,并将它们与网络和优化器一起传递给training_step()函数:

        for i in range(int(len(X_train) / BATCH_SIZE)):
            X_batch = X_train[i * BATCH_SIZE:
                              i * BATCH_SIZE + BATCH_SIZE]
            y_batch = y_train[i * BATCH_SIZE:
                              i * BATCH_SIZE + BATCH_SIZE]
            training_step(X_batch, y_batch, model, 
                          optimizer)
    
  10. 然后,我们将打印当前轮次的时间:

        elapsed = time.time() - start
        print(f'\tElapsed time: {elapsed:.2f} seconds.')
    
  11. 最后,在测试集上评估网络,确保它没有出现任何问题:

    model.compile(loss=categorical_crossentropy,
                  optimizer=optimizer,
                  metrics=['accuracy'])
    results = model.evaluate(X_test, y_test)
    print(f'Loss: {results[0]}, Accuracy: {results[1]}')
    

    以下是结果:

    Loss: 1.7750033140182495, Accuracy: 0.9083999991416931
    

让我们继续下一个部分。

它是如何工作的…

在本章节中,我们学习了如何创建自己的自定义训练循环。虽然我们在这个实例中没有做任何特别有趣的事情,但我们重点介绍了如何使用tf.GradientTape来“烹饪”一个自定义深度学习训练循环的组件(或者说是食材):

  • 网络架构本身

  • 用于计算模型损失的损失函数

  • 用于根据梯度更新模型权重的优化器

  • 步骤函数,它实现了前向传播(计算梯度)和反向传播(通过优化器应用梯度)

如果你想研究tf.GradientTape的更真实和吸引人的应用,可以参考第六章生成模型与对抗攻击第七章用 CNN 和 RNN 进行图像描述;以及第八章通过分割实现细粒度图像理解。不过,你也可以直接阅读下一篇章节,在那里我们将学习如何可视化类别激活图来调试深度神经网络!

可视化类别激活图,以更好地理解你的网络

尽管深度神经网络具有无可争议的强大能力和实用性,但关于它们的最大抱怨之一就是它们的神秘性。大多数时候,我们将它们视为黑箱,知道它们能工作,但不知道为什么。

特别是,真正具有挑战性的是解释为什么一个网络会得到特定的结果,哪些神经元被激活了,为什么会被激活,或者网络在看哪里,以确定图像中物体的类别或性质。

换句话说,我们如何信任我们不理解的东西?如果它坏了,我们又该如何改进或修复它?

幸运的是,在这个配方中,我们将研究一种新方法,来揭示这些话题,称为梯度加权类激活映射,或简称Grad-CAM

准备好了吗?我们开始吧!

准备工作

对于这个配方,我们需要OpenCVPillowimutils。你可以像这样一次性安装它们:

$> pip install Pillow opencv-python imutils

现在,我们准备好实现这个配方了。

如何实现…

按照这些步骤完成这个配方:

  1. 导入我们将要使用的模块:

    import cv2
    import imutils 
    import numpy as np
    import tensorflow as tf
    from tensorflow.keras.applications import *
    from tensorflow.keras.models import Model
    from tensorflow.keras.preprocessing.image import *
    
  2. 定义GradCAM类,它将封装Grad-CAM算法,使我们能够生成给定层的激活图热力图。让我们从定义构造函数开始:

    class GradGAM(object):
        def __init__(self, model, class_index, 
                     layer_name=None):
            self.class_index = class_index
            if layer_name is None:
                for layer in reversed(model.layers):
                    if len(layer.output_shape) == 4:
                        layer_name = layer.name
                        break
            self.grad_model = 
                      self._create_grad_model(model,
    
                                           layer_name)
    
  3. 在这里,我们接收的是我们想要检查的类的class_index,以及我们希望可视化其激活的层的layer_name。如果我们没有接收到layer_name,我们将默认使用模型的最外层输出层。最后,我们通过调用这里定义的_create_grad_model()方法创建grad_model

        def _create_grad_model(self, model, layer_name):
            return Model(inputs=[model.inputs],
                         outputs=[
                           model.get_layer(layer_name).
                              output,model.output])
    

    这个模型与model的输入相同,但输出既包含兴趣层的激活,也包含model本身的预测。

  4. 接下来,我们必须定义compute_heatmap()方法。首先,我们需要将输入图像传递给grad_model,以获取兴趣层的激活图和预测:

        def compute_heatmap(self, image, epsilon=1e-8):
            with tf.GradientTape() as tape:
                inputs = tf.cast(image, tf.float32)
                conv_outputs, preds = self.grad_model(inputs)
                loss = preds[:, self.class_index]
    
  5. 我们可以根据与class_index对应的损失来计算梯度:

    grads = tape.gradient(loss, conv_outputs)
    
  6. 我们可以通过基本上在float_conv_outputsfloat_grads中找到正值并将其与梯度相乘来计算引导梯度,这样我们就可以可视化哪些神经元正在激活:

            guided_grads = (tf.cast(conv_outputs > 0, 
                          'float32') *
                            tf.cast(grads > 0, 'float32') *
                            grads)
    
  7. 现在,我们可以通过平均引导梯度来计算梯度权重,然后使用这些权重将加权映射添加到我们的Grad-CAM可视化中:

            conv_outputs = conv_outputs[0]
            guided_grads = guided_grads[0]
            weights = tf.reduce_mean(guided_grads, 
                                     axis=(0, 1))
            cam = tf.reduce_sum(
                tf.multiply(weights, conv_outputs),
                axis=-1)
    
  8. 然后,我们将Grad-CAM可视化结果调整为输入图像的尺寸,进行最小-最大归一化后再返回:

            height, width = image.shape[1:3]
            heatmap = cv2.resize(cam.numpy(), (width, 
                                  height))
            min = heatmap.min()
            max = heatmap.max()
            heatmap = (heatmap - min) / ((max - min) + 
                                                   epsilon)
            heatmap = (heatmap * 255.0).astype('uint8')
            return heatmap
    
  9. GradCAM类的最后一个方法将热力图叠加到原始图像上。这使我们能够更好地了解网络在做出预测时注视的视觉线索:

        def overlay_heatmap(self,
                            heatmap,
                            image, alpha=0.5,
                            colormap=cv2.COLORMAP_VIRIDIS):
            heatmap = cv2.applyColorMap(heatmap, colormap)
            output = cv2.addWeighted(image,
                                     alpha,
                                     heatmap,
                                     1 - alpha,
                                     0)
            return heatmap, output
    
  10. 让我们实例化一个在 ImageNet 上训练过的ResNet50模型:

    model = ResNet50(weights='imagenet')
    
  11. 加载输入图像,将其调整为 ResNet50 所期望的尺寸,将其转换为 NumPy 数组,并进行预处理:

    image = load_img('dog.jpg', target_size=(224, 224))
    image = img_to_array(image)
    image = np.expand_dims(image, axis=0)
    image = imagenet_utils.preprocess_input(image)
    
  12. 将图像通过模型并提取最可能类别的索引:

    predictions = model.predict(image)
    i = np.argmax(predictions[0])
    
  13. 实例化一个GradCAM对象并计算热力图:

    cam = GradGAM(model, i)
    heatmap = cam.compute_heatmap(image)
    
  14. 将热力图叠加在原始图像上:

    original_image = cv2.imread('dog.jpg')
    heatmap = cv2.resize(heatmap, (original_image.shape[1],
                                   original_image.shape[0]))
    heatmap, output = cam.overlay_heatmap(heatmap, 
                                          original_image,
                                          alpha=0.5)
    
  15. 解码预测结果,使其可供人类读取:

    decoded = imagenet_utils.decode_predictions(predictions)
    _, label, probability = decoded[0][0]
    
  16. 用类别及其关联的概率标注覆盖的热力图:

    cv2.rectangle(output, (0, 0), (340, 40), (0, 0, 0), -1)
    cv2.putText(output, f'{label}: {probability * 100:.2f}%',
                (10, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.8,
                (255, 255, 255), 2)
    
  17. 最后,将原始图像、热力图和标注覆盖层合并为一张图像并保存到磁盘:

    output = np.hstack([original_image, heatmap, output])
    output = imutils.resize(output, height=700)
    cv2.imwrite('output.jpg', output)
    

这是结果:

图 12.9 – Grad-CAM 可视化

图 12.9 – Grad-CAM 可视化

如我们所见,网络将我的狗分类为巴哥犬,这是正确的,置信度为 85.03%。此外,热力图显示网络在我的狗的鼻子和眼睛周围激活,这意味着这些是重要特征,模型的表现符合预期。

它是如何工作的……

在这个案例中,我们学习并实现了Grad-CAM,这是一种非常有用的算法,用于可视化检查神经网络的激活情况。这是调试网络行为的有效方法,它确保网络关注图像的正确部分。

这是一个非常重要的工具,因为我们模型的高准确率或性能可能与实际学习的关系不大,而与一些未考虑到的因素有更多关系。例如,如果我们正在开发一个宠物分类器来区分狗和猫,我们应该使用Grad-CAM来验证网络是否关注这些动物固有的特征,以便正确分类,而不是关注周围环境、背景噪音或图像中的次要元素。

另见

你可以通过阅读以下论文来扩展你对Grad-CAM的知识:arxiv.org/abs/1610.02391

第十三章:你可能会喜欢的其他书籍

如果你喜欢这本书,你可能对 Packt 出版的其他书籍感兴趣:

掌握 TensorFlow 2.x 的计算机视觉.png)

掌握 TensorFlow 2.x 的计算机视觉

Krishnendu Kar

ISBN:978-1-83882-706-9

  • 探索特征提取和图像检索方法,并可视化神经网络模型的不同层

  • 使用 TensorFlow 进行各种实际场景的视觉搜索方法

  • 构建神经网络或调整参数以优化模型的性能

  • 理解 TensorFlow DeepLab 以对图像进行语义分割,并使用 DCGAN 进行图像修复

  • 评估你的模型,并优化它并将其集成到你的应用中以便大规模运行

  • 迅速掌握手动和自动图像标注的技术

应用深度学习与计算机视觉在自动驾驶汽车中的应用

应用深度学习与计算机视觉在自动驾驶汽车中的应用

Sumit Ranjan,Dr. S. Senthamilarasu

ISBN:978-1-83864-630-1

  • 使用 Keras 库从零开始实现深度神经网络

  • 理解深度学习在自动驾驶汽车中的重要性

  • 使用 OpenCV 库掌握图像处理中的特征提取技术

  • 设计一个软件管道,检测视频中的车道线

  • 实现一个卷积神经网络(CNN)图像分类器,用于交通信号标志识别

  • 在虚拟模拟器中驾驶汽车,训练并测试神经网络进行行为克隆

  • 发现各种最先进的语义分割和目标检测架构

留下评论 - 告诉其他读者你的想法

请通过在你购买书籍的网站上留下评论与他人分享你对这本书的看法。如果你是从 Amazon 购买的,请在本书的 Amazon 页面上留下诚实的评论。这对其他潜在读者非常重要,因为他们可以看到并使用你无偏见的意见来做出购买决策,我们也能了解客户对我们产品的看法,作者也可以看到你对他们与 Packt 合作创作的书籍的反馈。只需要几分钟的时间,但对其他潜在客户、我们的作者和 Packt 来说都很有价值。谢谢!

posted @ 2025-07-08 21:22  绝不原创的飞龙  阅读(11)  评论(0)    收藏  举报