Keras 高级深度学习(一)

原文:annas-archive.org/md5/946fd2e9c806f075bc9bc101ff92dd6c

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

近年来,深度学习在视觉、语音、自然语言处理与理解以及其他数据丰富领域的难题中取得了前所未有的成功。企业、大学、政府和研究机构对该领域的关注加速了技术的进步。本书涵盖了深度学习中的一些重要进展。通过介绍原理背景、深入挖掘概念背后的直觉、使用 Keras 实现方程和算法,并分析结果,本书对高级理论进行了详细讲解。

人工智能AI)现阶段仍然远未成为一个完全被理解的领域。作为 AI 子领域的深度学习,也处于同样的境地。虽然它远未成熟,但许多现实世界的应用,如基于视觉的检测与识别、产品推荐、语音识别与合成、节能、药物发现、金融和营销,已经在使用深度学习算法。未来还会发现并构建更多的应用。本书的目标是解释高级概念,提供示例实现,并让读者作为该领域的专家,识别出目标应用。

一个尚不完全成熟的领域是一把双刃剑。一方面,它为发现和开发提供了许多机会。深度学习中有很多尚未解决的问题,这为成为市场先行者——无论是在产品开发、出版还是行业认可上——提供了机会。另一方面,在任务关键型环境中,很难信任一个尚未完全理解的领域。我们可以放心地说,如果有人问,很少有机器学习工程师会选择乘坐由深度学习系统控制的自动驾驶飞机。要获得这种程度的信任,还需要做大量的工作。本书中讨论的高级概念很有可能在建立这种信任基础方面发挥重要作用。

每一本关于深度学习的书都不可能完全涵盖整个领域,本书也不例外。鉴于时间和篇幅的限制,我们本可以触及一些有趣的领域,如检测、分割与识别、视觉理解、概率推理、自然语言处理与理解、语音合成以及自动化机器学习。然而,本书相信,选取并讲解某些领域能够使读者能够进入未覆盖的其他领域。

当读者准备继续阅读本书时,需要牢记,他们选择了一个充满激动人心的挑战并且能够对社会产生巨大影响的领域。我们很幸运,拥有一份让我们每天早晨醒来都期待的工作。

本书适合谁阅读

本书面向希望深入理解深度学习高级主题的机器学习工程师和学生。每个讨论都附有 Keras 中的代码实现。本书适合那些希望了解如何将理论转化为在 Keras 中实现的工作代码的读者。除了理解理论外,代码实现通常是将机器学习应用于现实问题时最具挑战性的任务之一。

本书内容概览

第一章,使用 Keras 介绍高级深度学习,涵盖了深度学习的关键概念,如优化、正则化、损失函数、基础层和网络及其在 Keras 中的实现。本章还回顾了深度学习和 Keras 的使用,采用顺序 API。

第二章,深度神经网络,讨论了 Keras 的功能 API。探讨了两种广泛使用的深度网络架构,ResNet 和 DenseNet,并在 Keras 中通过功能 API 进行了实现。

第三章,自编码器,介绍了一个常见的网络结构——自编码器,用于发现输入数据的潜在表示。讨论并在 Keras 中实现了两个自编码器的应用示例:去噪和着色。

第四章,生成对抗网络(GANs),讨论了深度学习中的一项重要进展。GAN 用于生成看似真实的新合成数据。本章解释了 GAN 的原理。本文讨论并实现了两种 GAN 示例:DCGAN 和 CGAN,均在 Keras 中实现。

第五章,改进的 GANs,介绍了改进基础 GAN 的算法。这些算法解决了训练 GAN 时的难题,并提高了合成数据的感知质量。讨论并在 Keras 中实现了 WGAN、LSGAN 和 ACGAN。

第六章,解耦表示 GANs,讨论了如何控制 GAN 生成的合成数据的属性。如果潜在表示解耦,这些属性就可以被控制。介绍了两种解耦表示的技术:InfoGAN 和 StackedGAN,并在 Keras 中进行了实现。

第七章,跨域 GANs,涵盖了 GAN 的一个实际应用,即将图像从一个领域转移到另一个领域,通常称为跨域迁移。讨论了广泛使用的跨域 GAN——CycleGAN,并在 Keras 中实现。本章还展示了 CycleGAN 执行图像着色和风格迁移的过程。

第八章,变分自编码器(VAE),讨论了深度学习中的另一项重大进展。与 GAN 类似,VAE 是一种生成模型,用于生成合成数据。与 GAN 不同,VAE 侧重于可解码的连续潜在空间,适用于变分推断。VAE 及其变种 CVAE 和https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_08_121.jpg-VAE 在 Keras 中进行了实现。

第九章,深度强化学习,解释了强化学习和 Q 学习的原理。介绍了实现 Q 学习的两种技术,Q 表更新和深度 Q 网络(DQN)。在 OpenAI gym 环境中,演示了使用 Python 和 DQN 实现 Q 学习。

第十章,策略梯度方法,解释了如何使用神经网络学习强化学习中的决策策略。涵盖了四种方法,并在 Keras 和 OpenAI gym 环境中实现,分别是 REINFORCE、带基线的 REINFORCE、演员-评论家和优势演员-评论家。本章中的示例展示了在连续动作空间中应用策略梯度方法。

为了最大限度地从本书中受益

  • 深度学习与 Python:读者应具备基本的深度学习知识,并了解如何在 Python 中实现深度学习。虽然有使用 Keras 实现深度学习算法的经验会有所帮助,但并不是必须的。第一章,使用 Keras 介绍深度学习进阶,回顾了深度学习的概念及其在 Keras 中的实现。

  • 数学:本书中的讨论假设读者具备大学水平的微积分、线性代数、统计学和概率论基础知识。

  • GPU:本书中的大多数 Keras 实现需要 GPU。如果没有 GPU,执行许多代码示例将不可行,因为所需时间过长(可能需要几个小时甚至几天)。本书中的示例尽量使用合理的数据大小,以减少高性能计算机的使用。本书假设读者至少可以访问 NVIDIA GTX 1060。

  • 编辑:本书中的代码示例是在vim编辑器下,使用 Ubuntu Linux 16.04 LTS、Ubuntu Linux 17.04 和 macOS High Sierra 操作系统编辑的。任何支持 Python 的文本编辑器都可以使用。

  • TensorFlow:Keras 需要一个后端。本书中的代码示例使用 Keras 和 TensorFlow 后端编写。请确保正确安装了 GPU 驱动和tensorflow

  • GitHub:我们通过示例和实验来学习。请从本书的 GitHub 仓库中 git pullfork 代码包。获取代码后,检查它。运行它。修改它。再次运行它。通过调整代码示例进行所有创造性的实验。这是理解章节中所有理论的唯一方法。我们也非常感激你在书籍 GitHub 仓库上给予星标。

下载示例代码文件

本书的代码包托管在 GitHub 上,地址是:

github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras

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

下载彩色图片

我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图片。你可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781788629416_ColorImages.pdf

使用的约定

本书中的代码示例使用 Python 编写,具体来说是 python3。配色方案基于 vim 语法高亮。考虑以下示例:

def encoder_layer(inputs,
                  filters=16,
                  kernel_size=3,
                  strides=2,
                  activation='relu',
                  instance_norm=True):
    """Builds a generic encoder layer made of Conv2D-IN-LeakyReLU
    IN is optional, LeakyReLU may be replaced by ReLU

    """

    conv = Conv2D(filters=filters,
                  kernel_size=kernel_size,
                  strides=strides,
                  padding='same')

    x = inputs
    if instance_norm:
        x = InstanceNormalization()(x)
    if activation == 'relu':
        x = Activation('relu')(x)
    else:
        x = LeakyReLU(alpha=0.2)(x)
    x = conv(x)
    return x

尽可能包含文档字符串。至少使用文本注释来 最小化空间的使用。

所有命令行代码执行格式如下:

$ python3 dcgan-mnist-4.2.1.py

示例代码文件命名格式为:algorithm-dataset-chapter.section.number.py。命令行示例是第四章第二节的第一段代码,使用的是 DCGAN 算法和 MNIST 数据集。在某些情况下,执行命令行并未明确写出,但默认是:

$ python3 name-of-the-file-in-listing

The file name of the code example is included in the Listing caption.

联系我们

我们始终欢迎读者的反馈。

一般反馈:发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中注明书名。如果你对本书的任何部分有疑问,请通过 <questions@packtpub.com> 给我们发送邮件。

勘误:尽管我们已尽最大努力确保内容的准确性,但错误还是难免发生。如果你在本书中发现错误,请告知我们。请访问,www.packtpub.com/submit-errata,选择你的书籍,点击“勘误提交表单”链接,并填写相关信息。

盗版:如果你在互联网上发现任何形式的非法复制品,我们将非常感激你提供相关网址或网站名称。请通过 <copyright@packtpub.com> 联系我们,并附上相关材料的链接。

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

评论

请留下评论。当你阅读并使用过这本书后,为什么不在你购买它的站点上留下评论呢?潜在的读者可以看到并参考你公正的意见做出购买决策,我们在 Packt 能够了解你对我们产品的看法,作者也可以看到你对他们书籍的反馈。谢谢!

了解更多关于 Packt 的信息,请访问 packtpub.com

第一章。介绍使用 Keras 的高级深度学习

在本章中,我们将介绍本书中将使用的三种深度学习人工神经网络。这些深度学习模型是 MLP、CNN 和 RNN,它们是本书中所涵盖的高级深度学习主题(如自编码器和 GANs)的构建模块。

在本章中,我们将一起使用 Keras 库实现这些深度学习模型。我们将首先了解为什么 Keras 是我们使用的优秀工具。接下来,我们将深入探讨三种深度学习模型的安装和实现细节。

本章内容将包括:

  • 解释为什么 Keras 库是用于高级深度学习的绝佳选择。

  • 介绍 MLP、CNN 和 RNN —— 这是大多数高级深度学习模型的核心构建模块,我们将在本书中使用它们。

  • 提供如何使用 Keras 和 TensorFlow 实现 MLP、CNN 和 RNN 的示例。

  • 在过程中,我们将开始介绍一些重要的深度学习概念,包括优化、正则化和损失函数。

到本章结束时,我们将使用 Keras 实现基本的深度学习模型。在下一章中,我们将探讨基于这些基础的高级深度学习主题,如深度网络、自编码器和生成对抗网络(GANs)。

为什么 Keras 是完美的深度学习库?

Keras [Chollet, François. “Keras (2015).” (2017)] 是一个流行的深度学习库,写作时已有超过 250,000 名开发者使用,每年这个数字翻倍。超过 600 名贡献者积极维护它。本书中使用的一些示例已贡献至官方 Keras GitHub 仓库。谷歌的TensorFlow,一个流行的开源深度学习库,将 Keras 作为其高层 API。在工业界,Keras 被谷歌、Netflix、Uber 和 NVIDIA 等大公司广泛使用。本章中,我们将介绍如何使用 Keras Sequential API

我们选择 Keras 作为本书中的工具,因为 Keras 是一个致力于加速深度学习模型实现的库。这使得 Keras 成为我们在实践和动手操作时的理想选择,尤其是在探索本书中的高级深度学习概念时。由于 Keras 与深度学习紧密相连,在最大化使用 Keras 库之前,了解深度学习的关键概念是至关重要的。

注意

本书中的所有示例都可以在 GitHub 上找到,链接如下:github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras

Keras 是一个深度学习库,使我们能够高效地构建和训练模型。在该库中,层像乐高积木一样相互连接,生成的模型简洁且易于理解。模型训练非常直接,只需要数据、若干训练周期和用于监控的指标。最终结果是,大多数深度学习模型都可以通过显著更少的代码行数来实现。通过使用 Keras,我们能够提高生产力,通过节省在代码实现上的时间,将其投入到更重要的任务中,例如制定更好的深度学习算法。我们将 Keras 与深度学习结合使用,因为在引入本章接下来部分的三种深度学习网络时,它能提供更高的效率。

同样,Keras 非常适合快速实现深度学习模型,就像我们将在本书中使用的模型一样。典型的模型可以通过少量的代码行使用 Sequential Model API 构建。然而,不要被它的简洁性误导。Keras 也可以通过其 API 和 ModelLayer 类构建更先进和复杂的模型,这些模型可以定制以满足独特的需求。功能性 API 支持构建图形化的模型、层的重用以及表现像 Python 函数的模型。同时,ModelLayer 类提供了一个框架,用于实现不常见或实验性的深度学习模型和层。

安装 Keras 和 TensorFlow

Keras 不是一个独立的深度学习库。如 图 1.1.1 所示,它建立在其他深度学习库或后端之上。可以是 Google 的 TensorFlow,MILA 的 Theano 或 Microsoft 的 CNTK。对 Apache 的 MXNet 的支持几乎已经完成。我们将在本书中使用 TensorFlow 后端和 Python 3 进行测试。之所以选择 TensorFlow,是因为它的流行,使其成为一个常见的后端。

我们可以通过编辑 Linux 或 macOS 中的 Keras 配置文件 .keras/keras.json,轻松地在不同的后端之间切换。由于底层算法实现方式的不同,网络在不同的后端上可能会有不同的速度。

在硬件上,Keras 可以运行在 CPU、GPU 和 Google 的 TPU 上。在本书中,我们将使用 CPU 和 NVIDIA GPU(特别是 GTX 1060 和 GTX 1080Ti 型号)进行测试。

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_01.jpg

图 1.1.1:Keras 是一个高级库,建立在其他深度学习模型之上。Keras 支持在 CPU、GPU 和 TPU 上运行。

在继续阅读本书的其余内容之前,我们需要确保 Keras 和 TensorFlow 已正确安装。有多种方法可以进行安装;其中一种示例是使用 pip3 安装:

$ sudo pip3 install tensorflow

如果我们拥有支持的 NVIDIA GPU,并且已正确安装驱动程序,以及 NVIDIA 的 CUDA 工具包和 cuDNN 深度神经网络库,建议安装支持 GPU 的版本,因为它可以加速训练和预测:

$ sudo pip3 install tensorflow-gpu

我们接下来的步骤是安装 Keras:

$ sudo pip3 install keras

本书中的示例需要额外的包,如 pydotpydot_ngvizgraphpython3-tkmatplotlib。在继续进行本章后,请先安装这些包。

如果已安装 TensorFlow 和 Keras 及其依赖项,则以下内容不应产生任何错误:

$ python3
>>> import tensorflow as tf
>>> message = tf.constant('Hello world!')
>>> session = tf.Session()
>>> session.run(message)
b'Hello world!'
>>> import keras.backend as K
Using TensorFlow backend.
>>> print(K.epsilon())
1e-07

关于 SSE4.2 AVX AVX2 FMA 的警告信息(如下所示)可以安全忽略。要去除警告信息,你需要从 github.com/tensorflow/tensorflow 重新编译并安装 TensorFlow 源代码。

tensorflow/core/platform/cpu_feature_guard.cc:137] Your CPU supports instructions that this TensorFlow binary was not compiled to use: SSE4.2 AVX AVX2 FMA

本书不覆盖完整的 Keras API。我们只会介绍解释本书中高级深度学习主题所需的材料。如需更多信息,请参考官方 Keras 文档,网址为 keras.io

实现核心深度学习模型 - MLPs、CNNs 和 RNNs

我们已经提到过,我们将使用三种高级深度学习模型,它们是:

  • MLPs:多层感知机

  • RNNs:递归神经网络

  • CNNs:卷积神经网络

这是我们在本书中将使用的三种网络。尽管这三种网络是独立的,但你会发现它们常常会结合在一起,以便充分利用每种模型的优势。

在本章的接下来的部分中,我们将逐一详细讨论这些构建模块。在随后的部分中,MLPs 将与其他重要主题一起讨论,如损失函数、优化器和正则化器。之后,我们将覆盖 CNNs 和 RNNs。

MLPs、CNNs 和 RNNs 之间的区别

多层感知机(MLPs)是一个全连接网络。在一些文献中,你常常会看到它们被称为深度前馈网络或前馈神经网络。从已知的目标应用角度理解这些网络将帮助我们深入理解设计高级深度学习模型的基本原因。MLPs 在简单的逻辑回归和线性回归问题中很常见。然而,MLPs 并不适合处理序列数据和多维数据模式。由于设计上的原因,MLPs 很难记住序列数据中的模式,并且需要大量的参数来处理多维数据。

对于顺序数据输入,RNNs 因其内部设计允许网络发现对预测有用的历史数据依赖关系,因此非常受欢迎。对于多维数据,如图像和视频,CNN 在提取特征图以进行分类、分割、生成等方面表现出色。在某些情况下,CNN 也以一维卷积的形式用于具有顺序输入数据的网络。然而,在大多数深度学习模型中,MLPs、RNNs 和 CNNs 会结合使用,以充分发挥每个网络的优势。

MLPs、RNNs 和 CNNs 并没有完整展示深度网络的全貌。我们还需要识别一个目标损失函数优化器,以及一个正则化器。目标是在训练过程中减少损失函数的值,因为这可以作为模型正在学习的良好指引。为了最小化这个值,模型使用优化器。这是一个算法,用于确定每个训练步骤中权重和偏差应该如何调整。一个训练好的模型不仅要在训练数据上工作,还要在测试数据甚至是未曾预见的输入数据上有效。正则化器的作用是确保训练好的模型能够泛化到新数据。

多层感知机(MLPs)

我们将要查看的三种网络中的第一种被称为多层感知机(MLPs)。假设目标是创建一个神经网络,用于基于手写数字来识别数字。例如,当网络的输入是手写数字 8 的图像时,相应的预测也必须是数字 8。这是分类器网络的经典任务,可以通过逻辑回归进行训练。为了训练和验证一个分类器网络,必须有一个足够大的手写数字数据集。修改版国家标准与技术研究院数据集(简称 MNIST)通常被认为是深度学习的Hello World!,并且是一个适合手写数字分类的数据集。

在我们讨论多层感知机模型之前,必须先理解 MNIST 数据集。书中大量的示例使用了 MNIST 数据集。MNIST 被用来解释和验证深度学习理论,因为它包含的 70,000 个样本虽然不大,却包含了足够丰富的信息:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_02.jpg

图 1.3.1:MNIST 数据集中的示例图像。每张图像为 28 × 28 像素的灰度图。

MNIST 数据集

MNIST 是一个包含从 0 到 9 的手写数字集合。它有一个包含 60,000 张图像的训练集和 10,000 张测试图像,这些图像被分类到相应的类别或标签中。在一些文献中,目标真实标签一词也用来指代标签

在前面的图中可以看到 MNIST 数字的样本图像,每个图像的大小为 28×28 像素的灰度图。要在 Keras 中使用 MNIST 数据集,提供了一个 API 来自动下载和提取图像与标签。Listing 1.3.1展示了如何用一行代码加载 MNIST 数据集,使我们能够计算训练集和测试集的标签数量,并随机绘制数字图像。

Listing 1.3.1,mnist-sampler-1.3.1.py。Keras 代码展示了如何访问 MNIST 数据集,绘制 25 个随机样本,并计算训练集和测试集标签的数量:

import numpy as np
from keras.datasets import mnist
import matplotlib.pyplot as plt

# load dataset
(x_train, y_train), (x_test, y_test) = mnist.load_data()

# count the number of unique train labels
unique, counts = np.unique(y_train, return_counts=True)
print("Train labels: ", dict(zip(unique, counts)))

# count the number of unique test labels
unique, counts = np.unique(y_test, return_counts=True)
print("Test labels: ", dict(zip(unique, counts)))

# sample 25 mnist digits from train dataset
indexes = np.random.randint(0, x_train.shape[0], size=25)
images = x_train[indexes]
labels = y_train[indexes]

# plot the 25 mnist digits
plt.figure(figsize=(5,5))
for i in range(len(indexes)):
    plt.subplot(5, 5, i + 1)
    image = images[i]
    plt.imshow(image, cmap='gray')
    plt.axis('off')

plt.show()
plt.savefig("mnist-samples.png")
plt.close('all')

mnist.load_data()方法非常方便,因为不需要单独加载所有 70,000 张图像和标签并将其存储在数组中。在命令行中执行python3 mnist-sampler-1.3.1.py会打印出训练集和测试集标签的分布:

Train labels:  {0: 5923, 1: 6742, 2: 5958, 3: 6131, 4: 5842, 5: 5421, 6: 5918, 7: 6265, 8: 5851, 9: 5949}
Test labels:  {0: 980, 1: 1135, 2: 1032, 3: 1010, 4: 982, 5: 892, 6: 958, 7: 1028, 8: 974, 9: 1009}

然后,代码将绘制 25 个随机数字,如前面的图图 1.3.1所示。

在讨论多层感知机分类器模型之前,必须记住,虽然 MNIST 数据是二维张量,但应根据输入层的类型进行相应的重塑。下图展示了如何将一个 3×3 的灰度图像重塑为 MLP、CNN 和 RNN 的输入层:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_03.jpg

图 1.3.2:一个类似于 MNIST 数据的输入图像,根据输入层的类型进行重塑。为简化起见,展示了如何将一个 3×3 的灰度图像进行重塑。

MNIST 数字分类器模型

图 1.3.3所示,提出的 MLP 模型可用于 MNIST 数字分类。当单元或感知机被展示时,MLP 模型是一个完全连接的网络,如图 1.3.4所示。接下来还会展示如何根据输入计算感知机的输出,作为权重w[i]和偏置b[n](n 为第 n 个单元)的函数。相应的 Keras 实现请参见Listing 1.3.2

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_04.jpg

图 1.3.3:MLP MNIST 数字分类器模型

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_05.jpg

图 1.3.4:图 1.3.3 中的 MLP MNIST 数字分类器由全连接层组成。为简化起见,激活函数和 dropout 未显示。同时也展示了一个单元或感知机。

Listing 1.3.2,mlp-mnist-1.3.2.py展示了使用 MLP 的 MNIST 数字分类器模型的 Keras 实现:

import numpy as np
from keras.models import Sequential
from keras.layers import Dense, Activation, Dropout
from keras.utils import to_categorical, plot_model
from keras.datasets import mnist

# load mnist dataset
(x_train, y_train), (x_test, y_test) = mnist.load_data()

# compute the number of labels
num_labels = len(np.unique(y_train))

# convert to one-hot vector
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

# image dimensions (assumed square)
image_size = x_train.shape[1]
input_size = image_size * image_size

# resize and normalize
x_train = np.reshape(x_train, [-1, input_size])
x_train = x_train.astype('float32') / 255
x_test = np.reshape(x_test, [-1, input_size])
x_test = x_test.astype('float32') / 255

# network parameters
batch_size = 128
hidden_units = 256
dropout = 0.45

# model is a 3-layer MLP with ReLU and dropout after each layer
model = Sequential()
model.add(Dense(hidden_units, input_dim=input_size))
model.add(Activation('relu'))
model.add(Dropout(dropout))
model.add(Dense(hidden_units))
model.add(Activation('relu'))
model.add(Dropout(dropout))
model.add(Dense(num_labels))
# this is the output for one-hot vector
model.add(Activation('softmax'))
model.summary()
plot_model(model, to_file='mlp-mnist.png', show_shapes=True)

# loss function for one-hot vector
# use of adam optimizer
# accuracy is a good metric for classification tasks
model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])
# train the network
model.fit(x_train, y_train, epochs=20, batch_size=batch_size)

# validate the model on test dataset to determine generalization
loss, acc = model.evaluate(x_test, y_test, batch_size=batch_size)
print("\nTest accuracy: %.1f%%" % (100.0 * acc))

在讨论模型实现之前,数据必须是正确的形状和格式。加载 MNIST 数据集后,标签数量的计算方式如下:

# compute the number of labels
num_labels = len(np.unique(y_train))

硬编码num_labels = 10也是一种选择。但是,最好让计算机完成其工作。代码假设y_train包含标签 0 到 9。

此时,标签为数字格式,范围从 0 到 9。标签的稀疏标量表示法不适用于输出每类概率的神经网络预测层。更合适的格式是称为one-hot 向量的格式,这是一个 10 维向量,所有元素为 0,除了数字类的索引。例如,如果标签是 2,则等效的 one-hot 向量为[0,0,1,0,0,0,0,0,0,0]。第一个标签的索引为0

以下几行将每个标签转换为 one-hot 向量:

# convert to one-hot vector
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

在深度学习中,数据存储在张量中。张量一词适用于标量(0D 张量)、向量(1D 张量)、矩阵(2D 张量)和多维张量。从现在开始,除非标量、向量或矩阵能使解释更清晰,否则将使用张量一词。

剩余部分计算图像尺寸,第一层Denseinput_size,并将每个像素值从 0 到 255 的范围缩放到 0.0 到 1.0 之间。虽然可以直接使用原始像素值,但最好对输入数据进行归一化,以避免大梯度值,这可能会使训练变得困难。网络的输出也会进行归一化。训练完成后,有一个选项可以通过将输出张量乘以 255,将所有值恢复为整数像素值。

提出的模型基于 MLP 层。因此,输入预计为 1D 张量。因此,x_trainx_test分别被重塑为[60000, 28 * 28]和[10000, 28 * 28]。

# image dimensions (assumed square)
image_size = x_train.shape[1]
input_size = image_size * image_size

# resize and normalize
x_train = np.reshape(x_train, [-1, input_size])
x_train = x_train.astype('float32') / 255
x_test = np.reshape(x_test, [-1, input_size])
x_test = x_test.astype('float32') / 255

使用 MLP 和 Keras 构建模型

数据准备好后,接下来是构建模型。提议的模型由三层 MLP 构成。在 Keras 中,MLP 层被称为Dense,即密集连接层。第一层和第二层 MLP 在结构上完全相同,每层有 256 个单元,后跟relu激活和dropout。选择 256 个单元是因为 128、512 和 1,024 个单元的性能指标较低。在 128 个单元时,网络收敛较快,但测试准确率较低。增加 512 或 1,024 个单元并未显著提高测试准确率。

单元数是一个超参数。它控制网络的容量。容量是网络能够逼近的函数复杂度的度量。例如,对于多项式,次数就是超参数。随着次数的增加,函数的容量也随之增加。

如下所示的模型,分类器模型是使用 Keras 的顺序模型 API 实现的。如果模型只需要一个输入和一个输出,并通过一系列层进行处理,这种方法已经足够简单。为了简单起见,我们暂时使用这个方法,但在第二章《深度神经网络》中,将介绍 Keras 的函数式 API 来实现更复杂的深度学习模型。

# model is a 3-layer MLP with ReLU and dropout after each layer
model = Sequential()
model.add(Dense(hidden_units, input_dim=input_size))
model.add(Activation('relu'))
model.add(Dropout(dropout))
model.add(Dense(hidden_units))
model.add(Activation('relu'))
model.add(Dropout(dropout))
model.add(Dense(num_labels))
# this is the output for one-hot vector
model.add(Activation('softmax'))

由于 Dense 层是线性操作,若仅有一系列 Dense 层,它们只能逼近线性函数。问题在于,MNIST 手写数字分类本质上是一个非线性过程。在 Dense 层之间插入 relu 激活函数将使得多层感知机(MLP)能够建模非线性映射。relu修正线性单元 (ReLU) 是一个简单的非线性函数。它就像一个过滤器,允许正输入保持不变,而将其它输入压制为零。数学上,relu 可以通过以下公式表示,并在 图 1.3.5 中绘制:

relu(x) = max(0,x)

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_06.jpg

图 1.3.5:ReLU 函数的图像。ReLU 函数在神经网络中引入了非线性。

还有其他非线性函数可以使用,如 eluselusoftplussigmoidtanh。然而,relu 是行业中最常用的,并且由于其简单性,在计算上非常高效。sigmoidtanh 被用作输出层的激活函数,后文将详细描述。表 1.3.1 展示了这些激活函数的方程:

relurelu(x) = max(0,x)1.3.1
softplussoftplus(x) = log(1 + e x)1.3.2
eluhttps://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_001.jpg,其中 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_002.jpg,并且是一个可调超参数1.3.3
seluselu(x) = k × elu(x,a),其中 k = 1.0507009873554804934193349852946 和 a = 1.67326324235437728481704299167171.3.4

表 1.3.1:常见非线性激活函数的定义

正则化

神经网络有记忆训练数据的倾向,特别是当它的容量足够大时。在这种情况下,网络在面对测试数据时会发生灾难性的失败。这是网络无法泛化的经典案例。为了避免这种倾向,模型使用了正则化层或正则化函数。一个常见的正则化层被称为 dropout

dropout 的思想很简单。给定一个 dropout 率(这里设为 dropout=0.45),Dropout 层会随机移除该比例的神经元,使其不参与下一层的计算。例如,如果第一层有 256 个神经元,应用 dropout=0.45 后,只有 (1 - 0.45) * 256 = 140 个神经元会参与第二层的计算。Dropout 层使得神经网络能够应对不可预见的输入数据,因为网络经过训练后,即使部分神经元缺失,依然能够正确预测。值得注意的是,dropout 不会用于输出层,并且它只在训练过程中起作用,预测时不会使用 dropout。

除了 dropout 外,还有其他正则化器可以使用,如l1l2。在 Keras 中,可以对每个层的偏置、权重和激活输出进行正则化。l1l2通过添加惩罚函数来偏向较小的参数值。l1l2通过参数值的绝对值和平方的和的分数来强制实施惩罚。换句话说,惩罚函数强迫优化器找到较小的参数值。具有较小参数值的神经网络对输入数据中的噪声更不敏感。

例如,l2权重正则化器与fraction=0.001可以实现为:

from keras.regularizers import l2
model.add(Dense(hidden_units,
          kernel_regularizer=l2(0.001),
          input_dim=input_size))

如果使用了l1l2正则化,则不会添加额外的层。正则化会在Dense层内部强制执行。对于所提出的模型,dropout 仍然比l2正则化表现更好。

输出激活和损失函数

输出层有 10 个单元,并采用softmax激活函数。10 个单元对应着 10 个可能的标签、类别或分类。softmax激活函数可以通过以下方程表示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_003.jpg

(方程 1.3.5)

该方程适用于所有N = 10 个输出,x i for i = 0, 1 … 9 的最终预测。softmax的概念出奇地简单。它通过标准化预测,将输出压缩为概率。在这里,每个预测的输出是给定输入图像的正确标签的概率。所有输出的概率总和为 1.0。例如,当softmax层生成预测时,它将是一个 10 维的 1D 张量,输出可能类似于以下内容:

[  3.57351579e-11   7.08998016e-08   2.30154569e-07   6.35787558e-07
   5.57471187e-11   4.15353840e-09   3.55973775e-16   9.99995947e-01
   1.29531730e-09   3.06023480e-06]

预测输出张量表明,输入图像的标签应该是 7,因为其索引具有最高的概率。可以使用numpy.argmax()方法来确定具有最大值的元素的索引。

还有其他可以选择的输出激活层,例如linearsigmoidtanhlinear激活函数是一个恒等函数,它将输入直接复制到输出。sigmoid函数更具体地被称为逻辑 sigmoid。当预测张量的元素需要独立地映射到 0.0 到 1.0 之间时,将使用sigmoid。与softmax不同,预测张量中所有元素的总和不被限制为 1.0。例如,在情感预测(0.0 表示坏,1.0 表示好)或图像生成(0.0 表示 0,1.0 表示 255 像素值)中,sigmoid被用作最后一层。

tanh函数将其输入映射到-1.0 到 1.0 的范围。这在输出可以正负波动时非常重要。tanh函数更常用于循环神经网络的内部层,但也曾作为输出层激活函数使用。如果tanh替代sigmoid作为输出激活函数,所使用的数据必须进行适当的缩放。例如,代替在[0.0, 1.0]范围内缩放每个灰度像素

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_004.jpg

它被指定在范围[-1.0, 1.0]内

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_005.jpg

.

下图展示了sigmoidtanh函数。从数学上讲,sigmoid可以通过以下方程式表示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_006.jpg

(方程式 1.3.6)

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_07.jpg

图 1.3.6:sigmoidtanh函数图

预测张量与独热编码真实标签向量的差异被称为损失。一种损失函数是mean_squared_error均方误差),即目标值与预测值之间差异的平方的平均值。在当前示例中,我们使用的是categorical_crossentropy。它是目标与预测值的乘积与预测值对数的总和的负值。Keras 中还提供了其他损失函数,如mean_absolute_errorbinary_crossentropy。损失函数的选择并非随意,而应该是模型正在学习的标准。对于分类问题,categorical_crossentropymean_squared_errorsoftmax激活层之后的一个不错的选择。而binary_crossentropy损失函数通常在sigmoid激活层之后使用,mean_squared_error则是tanh输出的一个选择。

优化

在优化过程中,目标是最小化损失函数。其思想是,如果损失减少到可接受的水平,模型就间接地学习了将输入映射到输出的函数。性能指标用于确定模型是否学习到了潜在的数据分布。Keras 中的默认指标是损失。在训练、验证和测试过程中,还可以包括其他指标,如准确率。准确率是基于真实标签的正确预测的百分比或分数。在深度学习中,还有许多其他的性能指标。然而,这取决于模型的目标应用。在文献中,通常会报告训练模型在测试数据集上的性能指标,以便与其他深度学习模型进行比较。

在 Keras 中,有几种优化器可供选择。最常用的优化器包括;随机梯度下降SGD)、自适应矩估计Adam)和均方根传播RMSprop)。每个优化器都有可调参数,如学习率、动量和衰减。Adam 和 RMSprop 是 SGD 的变种,具有自适应学习率。在所提出的分类器网络中,使用了 Adam,因为它具有最高的测试准确率。

SGD 被认为是最基本的优化器。它是微积分中梯度下降的简化版本。在梯度下降GD)中,沿着函数曲线向下追踪找到最小值,就像在山谷中或沿着梯度的反方向走,直到到达底部。

GD 算法如图 1.3.7所示。假设 x 是正在调节的参数(例如,权重),目的是找到 y(例如,损失函数)的最小值。从 x = -0.5 的任意位置开始,梯度为

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_007.jpg

GD 算法要求 x 随后更新为

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_008.jpg

新的 x 值等于旧的值,加上梯度的相反方向并按比例缩放。

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_009.jpg

。这个小数字

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_010.jpg

指的是学习率。如果

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_011.jpg

,新值的 x = -0.48。

GD 通过迭代执行。在每一步中,y 将越来越接近其最小值。在 x = 0.5 时,

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_012.jpg

,GD 已经找到了 y = -1.25 的绝对最小值。梯度建议 x 不再改变。

学习率的选择至关重要。一个较大的值

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_013.jpg

可能无法找到最小值,因为搜索可能会在最小值附近来回摆动。另一方面,过小的值

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_014.jpg

可能需要进行大量的迭代才能找到最小值。在多重极小值的情况下,搜索可能会陷入局部最小值。

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_08.jpg

图 1.3.7:梯度下降类似于沿着函数曲线走下坡,直到到达最低点。在这个图中,全局最小值位于 x = 0.5。

多重极小值的例子可以在图 1.3.8中看到。如果出于某种原因搜索从图的左侧开始,且学习率非常小,则 GD 有很高的概率将 x = -1.51 作为 y 的最小值,而不会找到 x = 1.66 的全局最小值。一个足够的学习率将使梯度下降能够越过 x = 0.0 处的小山。在深度学习实践中,通常建议从较大的学习率(例如 0.1 到 0.001)开始,并随着损失接近最小值逐渐减小学习率。

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_09.jpg

图 1.3.8:具有两个最小值的函数图,x = -1.51 和 x = 1.66。图中还显示了该函数的导数。

梯度下降在深度神经网络中通常不使用,因为你经常会遇到需要训练的数百万个参数。执行完全的梯度下降在计算上效率低下。相反,使用了 SGD(随机梯度下降)。在 SGD 中,选择一个小批量样本来计算下降的近似值。参数(例如权重和偏置)通过以下公式进行调整:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_015.jpg

(公式 1.3.7)

在这个公式中,

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_016.jpg

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_017.jpg

分别是损失函数的参数和梯度张量。g 是通过损失函数的偏导数计算得到的。为了优化 GPU 性能,建议将小批量大小设置为 2 的幂。在所提议的网络中,batch_size=128

公式 1.3.7 计算了最后一层的参数更新。那么,如何调整前面层的参数呢?对于这种情况,应用微分的链式法则将导数传播到更低的层,并相应地计算梯度。这个算法在深度学习中被称为反向传播。反向传播的细节超出了本书的范围。不过,可以在neuralnetworksanddeeplearning.com找到一个很好的在线参考。

由于优化是基于微分的,因此损失函数的一个重要标准是它必须是平滑的或可微分的。当引入新的损失函数时,这个约束是非常重要的。

给定训练数据集、损失函数、优化器和正则化器的选择,现在可以通过调用 fit() 函数来训练模型:

# loss function for one-hot vector
# use of adam optimizer
# accuracy is a good metric for classification tasks
model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])
# train the network
model.fit(x_train, y_train, epochs=20, batch_size=batch_size)

这是 Keras 另一个有用的功能。只需提供 xy 数据、训练的 epoch 数量和批量大小,fit() 就会处理剩下的部分。在其他深度学习框架中,这需要执行多个任务,比如将输入和输出数据转换为正确的格式、加载、监控等。所有这些都必须在 for 循环内完成!在 Keras 中,所有工作都在一行代码中完成。

fit() 函数中,epoch 是对整个训练数据集的完整采样。batch_size 参数是每次训练步骤处理的输入样本数量。要完成一个 epoch,fit() 需要用训练数据集的大小除以批量大小,再加 1 以补偿任何小数部分。

性能评估

到目前为止,MNIST 数字分类器的模型已经完成。性能评估将是下一个关键步骤,以确定所提出的模型是否提供了令人满意的解决方案。训练模型 20 个 epoch 足以获得可比的性能指标。

以下表格 表 1.3.2 显示了不同网络配置和相应的性能指标。在 Layers 列中,展示了第 1 到第 3 层的单元数。对于每个优化器,使用了 Keras 中的默认参数。可以观察到改变正则化器、优化器和每层单元数的效果。在 表 1.3.2 中的另一个重要观察结果是,较大的网络不一定会带来更好的性能。

增加网络深度对于训练和测试数据集的准确性没有带来额外的好处。另一方面,像 128 这样的较小单元数也可能会降低测试和训练的准确率。当去除正则化器并使用每层 256 个单元时,获得了 99.93%的最佳训练准确率。然而,由于网络过拟合,测试准确率要低得多,为 98.0%。

最高的测试准确率是在使用 Adam 优化器和 Dropout(0.45) 时达到 98.5%。从技术上讲,仍然存在一定程度的过拟合,因为其训练准确率为 99.39%。对于 256-512-256、Dropout(0.45) 和 SGD,训练和测试准确率都是 98.2%。去除 RegularizerReLU 层会导致最差的性能。通常,我们会发现 Dropout 层的性能优于 l2

以下表格展示了在调优过程中典型的深度神经网络性能。该示例表明,网络架构需要改进。在接下来的部分中,另一个使用 CNN 的模型显示了测试准确率的显著提高:

层数正则化器优化器ReLU训练准确率,%测试准确率,%
256-256-256SGD93.6592.5
256-256-256L2(0.001)SGD99.3598.0
256-256-256L2(0.01)SGD96.9096.7
256-256-256SGD99.9398.0
256-256-256Dropout(0.4)SGD98.2398.1
256-256-256Dropout(0.45)SGD98.0798.1
256-256-256Dropout(0.5)SGD97.6898.1
256-256-256Dropout(0.6)SGD97.1197.9
256-512-256Dropout(0.45)SGD98.2198.2
512-512-512Dropout(0.2)SGD99.4598.3
512-512-512Dropout(0.4)SGD98.9598.3
512-1024-512Dropout(0.45)SGD98.9098.2
1024-1024-1024Dropout(0.4)SGD99.3798.3
256-256-256Dropout(0.6)Adam98.6498.2
256-256-256Dropout(0.55)Adam99.0298.3
256-256-256Dropout(0.45)Adam99.3998.5
256-256-256Dropout(0.45)RMSprop98.7598.1
128-128-128Dropout(0.45)Adam98.7097.7

模型总结

使用 Keras 库可以通过调用以下方法快速验证模型描述:

model.summary()

清单 1.3.2展示了提出的网络模型的总结。该模型需要总计 269,322 个参数。考虑到我们只是一个简单的 MNIST 数字分类任务,这个参数数量相当可观。MLP 模型并不高效。参数的数量可以通过图 1.3.4来计算,重点关注感知机如何计算输出。从输入到 Dense 层:784 × 256 + 256 = 200,960。第一个 Dense 到第二个 Dense:256 × 256 + 256 = 65,792。从第二个 Dense 到输出层:10 × 256 + 10 = 2,570。总数为 269,322。

清单 1.3.2 展示了 MLP MNIST 数字分类器模型的总结:

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_1 (Dense)              (None, 256)               200960    
_________________________________________________________________
activation_1 (Activation)    (None, 256)               0         
_________________________________________________________________
dropout_1 (Dropout)          (None, 256)               0         
_________________________________________________________________
dense_2 (Dense)              (None, 256)               65792     
_________________________________________________________________
activation_2 (Activation)    (None, 256)               0         
_________________________________________________________________
dropout_2 (Dropout)          (None, 256)               0         
_________________________________________________________________
dense_3 (Dense)              (None, 10)                2570      
_________________________________________________________________
activation_3 (Activation)    (None, 10)                0         
=================================================================
Total params: 269,322
Trainable params: 269,322
Non-trainable params: 0

另一种验证网络的方法是调用:

plot_model(model, to_file='mlp-mnist.png', show_shapes=True)

图 1.3.9展示了该图形结果。你会发现,这与summary()的结果类似,但以图形化的方式显示了每一层的相互连接和输入输出。

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_10.jpg

图 1.3.9:MLP MNIST 数字分类器的图形描述

卷积神经网络(CNNs)

现在我们将进入第二种人工神经网络——卷积神经网络CNNs)。在本节中,我们将解决相同的 MNIST 数字分类问题,不过这次我们将使用 CNN。

图 1.4.1展示了我们将用于 MNIST 数字分类的 CNN 模型,模型的实现细节则在清单 1.4.1中进行了说明。为了实现 CNN 模型,之前模型的一些部分需要做出调整。输入向量不再是原来的形式,而是新的维度(高度、宽度、通道数),对于灰度的 MNIST 图像来说,输入形状是(image_size, image_size, 1) = (28, 28, 1)。因此,训练和测试图像需要重新调整大小以符合这一输入形状要求。

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_11.jpg

图 1.4.1:用于 MNIST 数字分类的 CNN 模型

清单 1.4.1,cnn-mnist-1.4.1.py展示了使用 CNN 进行 MNIST 数字分类的 Keras 代码:

import numpy as np
from keras.models import Sequential
from keras.layers import Activation, Dense, Dropout
from keras.layers import Conv2D, MaxPooling2D, Flatten
from keras.utils import to_categorical, plot_model
from keras.datasets import mnist

# load mnist dataset
(x_train, y_train), (x_test, y_test) = mnist.load_data()

# compute the number of labels
num_labels = len(np.unique(y_train))

# convert to one-hot vector
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

# input image dimensions
image_size = x_train.shape[1]
# resize and normalize
x_train = np.reshape(x_train,[-1, image_size, image_size, 1])
x_test = np.reshape(x_test,[-1, image_size, image_size, 1])
x_train = x_train.astype('float32') / 255
x_test = x_test.astype('float32') / 255

# network parameters
# image is processed as is (square grayscale)
input_shape = (image_size, image_size, 1)
batch_size = 128
kernel_size = 3
pool_size = 2
filters = 64
dropout = 0.2

# model is a stack of CNN-ReLU-MaxPooling
model = Sequential()
model.add(Conv2D(filters=filters,
                 kernel_size=kernel_size,
                 activation='relu',
                 input_shape=input_shape))
model.add(MaxPooling2D(pool_size))
model.add(Conv2D(filters=filters,
                 kernel_size=kernel_size,
                 activation='relu'))
model.add(MaxPooling2D(pool_size))
model.add(Conv2D(filters=filters,
                 kernel_size=kernel_size,
                 activation='relu'))
model.add(Flatten())
# dropout added as regularizer
model.add(Dropout(dropout))
# output layer is 10-dim one-hot vector
model.add(Dense(num_labels))
model.add(Activation('softmax'))
model.summary()
plot_model(model, to_file='cnn-mnist.png', show_shapes=True)

# loss function for one-hot vector
# use of adam optimizer
# accuracy is good metric for classification tasks
model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])
# train the network
model.fit(x_train, y_train, epochs=10, batch_size=batch_size)

loss, acc = model.evaluate(x_test, y_test, batch_size=batch_size)
print("\nTest accuracy: %.1f%%" % (100.0 * acc))

这里的主要变化是使用了Conv2D层。relu激活函数已经是Conv2D的一个参数。使用批量归一化层时,relu函数可以作为一个Activation层单独使用。批量归一化在深度 CNN 中非常常见,可以使得在训练过程中使用较大的学习率,而不会引起不稳定。

卷积

如果在 MLP 模型中,单元的数量表示Dense层的特点,那么卷积核则代表 CNN 操作的特点。如图 1.4.2所示,卷积核可以视为一个矩形的图像块或窗口,它从左到右、从上到下滑过整个图像。这一操作被称为卷积。它将输入图像转化为特征图,特征图代表了卷积核从输入图像中学习到的内容。特征图随后会被转化为下一层的特征图,依此类推。每个Conv2D生成的特征图的数量由filters参数控制。

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_12.jpg

图 1.4.2:一个 3 × 3 的卷积核与 MNIST 数字图像进行卷积。卷积过程通过步骤 t[n]和 t[n+1]展示,其中卷积核向右移动了 1 个像素的步幅。

卷积过程中涉及的计算如图 1.4.3所示。为了简便起见,假设输入图像(或输入特征图)是一个 5 × 5 的矩阵,应用了一个 3 × 3 的卷积核。卷积后的特征图也在图中展示。图中特征图中的一个元素已被阴影标出。你会注意到,卷积后的特征图比原始输入图像要小,这是因为卷积只在有效元素上进行,卷积核不能越过图像的边界。如果希望输入和输出特征图的尺寸保持一致,可以将Conv2D的参数设置为padding='same'。输入图像的边界会被零填充,从而在卷积后保持尺寸不变:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_13.jpg

图 1.4.3:卷积操作展示了特征图中一个元素是如何被计算出来的

池化操作

最后的变化是添加了一个MaxPooling2D层,参数为pool_size=2MaxPooling2D会压缩每个特征图。每个大小为pool_size × pool_size的区域都会被缩减为一个像素,值等于该区域内的最大像素值。以下图展示了两个区域的MaxPooling2D操作:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_14.jpg

图 1.4.4:MaxPooling2D 操作。为了简便起见,输入特征图为 4 × 4,得到一个 2 × 2 的特征图。

MaxPooling2D的意义在于特征图大小的减少,从而实现了卷积核覆盖范围的增加。例如,在MaxPooling2D(2)之后,2 × 2 的卷积核大约是与一个 4 × 4 的区域进行卷积。CNN 学习到了新的特征图集,涵盖了不同的区域。

还有其他池化和压缩方式。例如,为了实现与MaxPooling2D(2)相同的 50%尺寸缩小,AveragePooling2D(2)通过取一个区域的平均值来代替找到最大值。步幅卷积Conv2D(strides=2,…)会在卷积过程中跳过每两个像素,仍然能达到相同的 50%尺寸缩小效果。不同的缩减技术在效果上有细微差别。

Conv2DMaxPooling2D中,pool_sizekernel都可以是非方形的。在这种情况下,必须同时指定行和列的大小。例如,pool_size=(1, 2)kernel=(3, 5)

最后一个MaxPooling2D层的输出是一个堆叠的特征图。Flatten的作用是将这些堆叠的特征图转换为适合DropoutDense层的向量格式,类似于 MLP 模型的输出层。

性能评估和模型总结

列表 1.4.2所示,与使用 MLP 层时的 269,322 相比,列表 1.4.1 中的 CNN 模型所需参数较少,为 80,226。conv2d_1 层有 640 个参数,因为每个卷积核有 3 × 3 = 9 个参数,且每个 64 个特征图都有一个卷积核和一个偏置参数。其他卷积层的参数数量可以通过类似的方式计算。图 1.4.5 显示了 CNN MNIST 数字分类器的图形表示。

表 1.4.1 显示了使用 Adam 优化器和 dropout=0.2 的 3 层网络,每层 64 个特征图时,最大测试准确率为 99.4%。CNN 在参数效率上更高,并且具有比 MLP 更高的准确性。同样,CNN 也适合用于从顺序数据、图像和视频中学习表示。

列表 1.4.2 显示了 CNN MNIST 数字分类器的总结:

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 26, 26, 64)        640       
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 13, 13, 64)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 11, 11, 64)        36928     
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 5, 5, 64)          0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 3, 3, 64)          36928     
_________________________________________________________________
flatten_1 (Flatten)          (None, 576)               0         
_________________________________________________________________
dropout_1 (Dropout)          (None, 576)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 10)                5770      
_________________________________________________________________
activation_1 (Activation)    (None, 10)                0         
=================================================================
Total params: 80,266
Trainable params: 80,266
Non-trainable params: 0

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_15.jpg

图 1.4.5:CNN MNIST 数字分类器的图形描述

层数优化器正则化器训练准确率,%测试准确率,%
64-64-64SGDDropout(0.2)97.7698.50
64-64-64RMSpropDropout(0.2)99.1199.00
64-64-64AdamDropout(0.2)99.7599.40
64-64-64AdamDropout(0.4)99.6499.30

递归神经网络(RNNs)

现在我们将看看我们三个人工神经网络中的最后一个——递归神经网络(RNNs)。

RNNs 是一类适用于学习顺序数据表示的网络,例如自然语言处理NLP)中的文本或仪器中的传感器数据流。虽然每个 MNIST 数据样本本身并非顺序数据,但不难想象,每个图像都可以被解释为一系列像素行或列的顺序。因此,基于 RNN 的模型可以将每个 MNIST 图像处理为 28 元素输入向量的序列,时间步等于 28。以下列表显示了图 1.5.1中 RNN 模型的代码:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_16.jpg

图 1.5.1:用于 MNIST 数字分类的 RNN 模型

在以下列表中,列表 1.5.1rnn-mnist-1.5.1.py 显示了使用 RNNs 进行 MNIST 数字分类的 Keras 代码:

import numpy as np
from keras.models import Sequential
from keras.layers import Dense, Activation, SimpleRNN
from keras.utils import to_categorical, plot_model
from keras.datasets import mnist

# load mnist dataset
(x_train, y_train), (x_test, y_test) = mnist.load_data()

# compute the number of labels
num_labels = len(np.unique(y_train))

# convert to one-hot vector
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

# resize and normalize
image_size = x_train.shape[1]
x_train = np.reshape(x_train,[-1, image_size, image_size])
x_test = np.reshape(x_test,[-1, image_size, image_size])
x_train = x_train.astype('float32') / 255
x_test = x_test.astype('float32') / 255

# network parameters
input_shape = (image_size, image_size)
batch_size = 128
units = 256
dropout = 0.2 

# model is RNN with 256 units, input is 28-dim vector 28 timesteps
model = Sequential()
model.add(SimpleRNN(units=units,
                    dropout=dropout,
                    input_shape=input_shape))
model.add(Dense(num_labels))
model.add(Activation('softmax'))
model.summary()
plot_model(model, to_file='rnn-mnist.png', show_shapes=True)

# loss function for one-hot vector
# use of sgd optimizer
# accuracy is good metric for classification tasks
model.compile(loss='categorical_crossentropy',
              optimizer='sgd',
              metrics=['accuracy'])
# train the network
model.fit(x_train, y_train, epochs=20, batch_size=batch_size)

loss, acc = model.evaluate(x_test, y_test, batch_size=batch_size)
print("\nTest accuracy: %.1f%%" % (100.0 * acc))

RNN 与前两种模型之间有两个主要区别。首先是input_shape = (image_size, image_size),实际上是input_shape = (timesteps, input_dim),即一个长度为timestepsinput_dim维度向量序列。其次是使用SimpleRNN层来表示一个具有units=256的 RNN 单元。units变量表示输出单元的数量。如果 CNN 的特点是卷积核在输入特征图上进行卷积,那么 RNN 的输出不仅是当前输入的函数,还与上一输出或隐藏状态有关。由于上一输出也是上一输入的函数,因此当前输出也是上一输出和输入的函数,依此类推。Keras 中的SimpleRNN层是 RNN 的简化版本。以下方程描述了 SimpleRNN 的输出:

ht = tanh(b + Wht-1 + Uxt) (1.5.1)

在此方程中,b是偏置项,WU分别被称为递归核(上一输出的权重)和核(当前输入的权重)。下标t用于表示序列中的位置。对于SimpleRNN层,units=256时,总参数数量为 256 + 256 × 256 + 256 × 28 = 72,960,分别对应bWU的贡献。

以下图展示了在 MNIST 数字分类中使用的 SimpleRNN 和 RNN 的示意图。SimpleRNN比 RNN 更简单的原因是缺少在 softmax 计算之前的输出值Ot = Vht + c

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_17.jpg

图 1.5.2:SimpleRNN 和 RNN 的示意图

与 MLP 或 CNN 相比,RNN 在初始阶段可能更难理解。在 MLP 中,感知机是基本单元。一旦理解了感知机的概念,MLP 就只是感知机的网络。在 CNN 中,卷积核是一个滑过特征图以生成另一个特征图的窗口。在 RNN 中,最重要的是自循环的概念。实际上,只有一个单元。

多个单元的错觉出现是因为每个时间步长都有一个单元,但实际上,除非网络被展开,否则它只是相同的单元被重复使用。RNN 的底层神经网络在单元之间是共享的。

列表 1.5.2中的总结指出,使用SimpleRNN需要较少的参数。图 1.5.3展示了 RNN MNIST 数字分类器的图形描述。该模型非常简洁。表 1.5.1显示,SimpleRNN在所展示的网络中具有最低的准确率。

列表 1.5.2,RNN MNIST 数字分类器总结:

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
simple_rnn_1 (SimpleRNN)     (None, 256)               72960     
_________________________________________________________________
dense_1 (Dense)              (None, 10)                2570      
_________________________________________________________________
activation_1 (Activation)    (None, 10)                0         
=================================================================
Total params: 75,530
Trainable params: 75,530
Non-trainable params: 0

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_18.jpg

图 1.5.3:RNN MNIST 数字分类器的图形描述

层数优化器正则化器训练准确率,%测试准确率,%
256SGDDropout(0.2)97.2698.00
256RMSpropDropout(0.2)96.7297.60
256AdamDropout(0.2)96.7997.40
512SGDDropout(0.2)97.8898.30

表 1.5.1:不同的 SimpleRNN 网络配置和性能测量

在许多深度神经网络中,RNN 家族的其他成员更常被使用。例如,长短期记忆LSTM)网络已广泛应用于机器翻译和问答问题。LSTM 网络解决了长期依赖问题,即将相关的过去信息记忆到当前输出。

与 RNN 或 SimpleRNN 不同,LSTM 单元的内部结构更为复杂。图 1.5.4展示了在 MNIST 数字分类上下文中的 LSTM 示意图。LSTM 不仅使用当前输入和过去的输出或隐藏状态,还引入了一个单元状态,st,用于将信息从一个单元传递到另一个单元。单元状态之间的信息流由三个门控控制,分别是ft、it 和qt。这三个门控的作用是决定哪些信息应被保留或替换,以及过去和当前输入中的哪些信息应对当前单元状态或输出做出贡献。本书中不讨论 LSTM 单元的内部结构的详细内容。但是,关于 LSTM 的直观指南可以参考:colah.github.io/posts/2015-08-Understanding-LSTMs

LSTM()层可以作为SimpleRNN()的直接替代。如果 LSTM 对当前任务而言过于复杂,可以使用一个更简单的版本,称为门控循环单元GRU)。GRU 通过将单元状态和隐藏状态结合来简化 LSTM。GRU 还通过减少一个门控来降低复杂度。GRU()函数也可以作为SimpleRNN()的直接替代。

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_01_19.jpg

图 1.5.4:LSTM 的示意图。为了清晰起见,参数未显示。

配置 RNN 的方式有很多种。一种方式是创建一个双向的 RNN 模型。默认情况下,RNN 是单向的,即当前输出仅受过去状态和当前输入的影响。而在双向 RNN 中,未来状态也可以通过允许信息反向流动来影响当前状态和过去状态。过去的输出会根据接收到的新信息进行更新。可以通过调用包装函数将 RNN 转为双向。例如,双向 LSTM 的实现是Bidirectional(LSTM())

对于所有类型的 RNN,增加单元数也会增加模型的容量。然而,增加容量的另一种方式是通过堆叠 RNN 层。不过,作为一般经验法则,只有在必要时才应增加模型容量。过多的容量可能导致过拟合,从而导致训练时间延长并在预测时出现较慢的性能。

结论

本章概述了三种深度学习模型——MLP、RNN 和 CNN——并介绍了 Keras,这是一个用于快速开发、训练和测试这些深度学习模型的库。本章还讨论了 Keras 的顺序 API。在下一章中,将介绍功能性 API,它将使我们能够构建更复杂的模型,特别是针对高级深度神经网络。

本章还回顾了深度学习中的重要概念,如优化、正则化和损失函数。为了便于理解,这些概念是在 MNIST 数字分类的背景下呈现的。我们还讨论了使用人工神经网络(特别是 MLP、CNN 和 RNN)解决 MNIST 数字分类的不同方法,并讨论了它们的性能评估。MLP、CNN 和 RNN 是深度神经网络的重要构建块。

通过对深度学习概念的理解,以及如何将 Keras 用作这些概念的工具,我们现在已经准备好分析高级深度学习模型。在下一章讨论功能性 API 后,我们将进入流行深度学习模型的实现。后续章节将讨论一些高级主题,如自编码器、GAN、VAE 和强化学习。配套的 Keras 代码实现将对理解这些主题起到重要作用。

参考文献

  1. LeCun, Yann, Corinna Cortes, 和 C. J. Burges. MNIST 手写数字数据库。AT&T 实验室 [在线]. 可用链接:yann. lecun. com/exdb/mnist 2 (2010)

第二章:深度神经网络

在本章中,我们将研究深度神经网络。这些网络在像 ImageNet、CIFAR10 和 CIFAR100 等更具挑战性和高级的数据集上的分类准确度表现优秀。为了简洁起见,我们将重点关注两个网络:ResNet [2][4] 和 DenseNet [5]。虽然我们会进行更详细的讲解,但在深入之前,有必要简要介绍这两个网络:

ResNet 引入了残差学习的概念,通过解决深度卷积网络中的梯度消失问题,使得它能够构建非常深的网络。

DenseNet 通过允许每个卷积层直接访问输入和较低层的特征图,进一步改进了 ResNet 技术。它还通过利用 瓶颈 层和 过渡 层,在深度网络中成功地保持了较低的参数数量。

为什么选择这两个模型,而不是其他模型?自从它们问世以来,已有无数模型(如 ResNeXt [6] 和 FractalNet [7])受到了这两个网络使用的技术的启发。同样,通过理解 ResNet 和 DenseNet,我们能够利用它们的设计指南构建自己的模型。通过迁移学习,这也将使我们能够利用预训练的 ResNet 和 DenseNet 模型来为我们自己的目标服务。这些原因,加上它们与 Keras 的兼容性,使得这两个模型非常适合本书中探讨的高级深度学习内容。

虽然本章的重点是深度神经网络;我们将从讨论 Keras 中一个重要特性 功能性 API 开始。这个 API 作为构建网络的替代方法,可以帮助我们构建比顺序模型更复杂的网络。我们之所以如此关注这个 API,是因为它将在构建深度网络时变得非常有用,尤其是本章所聚焦的两个网络。建议在继续阅读本章之前,先完成第一章,介绍 Keras 高级深度学习,因为我们将在本章中使用该章的入门级代码和概念,并将其提升到更高级的水平。

本章的目标是介绍:

  • Keras 中的功能性 API,并探索运行此 API 的网络实例

  • 深度残差网络(ResNet 版本 1 和 2)在 Keras 中的实现

  • 将密集连接卷积网络(DenseNet)实现到 Keras 中

  • 探索两个流行的深度学习模型:ResNetDenseNet

功能性 API

在我们在第一章中首先介绍的顺序模型中,《深入了解 Keras 中的高级深度学习》,层是堆叠在另一个层之上的。通常,模型将通过其输入层和输出层进行访问。我们还了解到,如果我们想在网络中间添加一个辅助输入,或者在倒数第二层之前提取一个辅助输出,就没有简单的机制。

那个模型也有其不足之处,例如,它不支持图形化模型或像 Python 函数一样行为的模型。此外,也很难在两个模型之间共享层。函数式 API 解决了这些限制,这也是它成为任何想从事深度学习模型工作的人不可或缺的工具的原因。

函数式 API 遵循以下两个概念:

  • 层是一个接受张量作为参数的实例。层的输出是另一个张量。为了构建模型,层实例是通过输入和输出张量相互链接的对象。这将产生与在顺序模型中堆叠多个层相似的最终结果。然而,使用层实例使得模型更容易具有辅助输入或多个输入输出,因为每个层的输入/输出都可以直接访问。

  • 模型是一个在一个或多个输入张量和输出张量之间的函数。在模型的输入和输出之间,张量是通过层输入和输出张量相互链接的层实例。因此,模型是一个或多个输入层和一个或多个输出层的函数。模型实例规范了计算图,描述了数据如何从输入流向输出。

完成函数式 API 模型构建后,训练和评估是通过与顺序模型相同的函数来执行的。举个例子,在函数式 API 中,具有 32 个滤波器的 2D 卷积层Conv2D,以x作为层的输入张量,以y作为层的输出张量,可以写成:

y = Conv2D(32)(x)

我们还能够堆叠多个层来构建我们的模型。例如,我们可以像下面的代码示例那样,重写在上章创建的 CNN on MNIST 代码:

你将会看到以下列表 2.1.1,cnn-functional-2.1.1.py,它展示了我们如何使用函数式 API 转换cnn-mnist-1.4.1.py代码:

import numpy as np
from keras.layers import Dense, Dropout, Input
from keras.layers import Conv2D, MaxPooling2D, Flatten
from keras.models import Model
from keras.datasets import mnist
from keras.utils import to_categorical

# compute the number of labels
num_labels = len(np.unique(y_train))

# convert to one-hot vector
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

# reshape and normalize input images
image_size = x_train.shape[1]
x_train = np.reshape(x_train,[-1, image_size, image_size, 1])
x_test = np.reshape(x_test,[-1, image_size, image_size, 1])
x_train = x_train.astype('float32') / 255
x_test = x_test.astype('float32') / 255

# network parameters
# image is processed as is (square grayscale)
input_shape = (image_size, image_size, 1)
batch_size = 128
kernel_size = 3
filters = 64 
dropout = 0.3 

# use functional API to build cnn layers
inputs = Input(shape=input_shape)
y = Conv2D(filters=filters,
           kernel_size=kernel_size,
           activation='relu')(inputs)
y = MaxPooling2D()(y)
y = Conv2D(filters=filters,
           kernel_size=kernel_size,
           activation='relu')(y)
y = MaxPooling2D()(y)
y = Conv2D(filters=filters,
           kernel_size=kernel_size,
           activation='relu')(y)
# image to vector before connecting to dense layer
y = Flatten()(y)
# dropout regularization
y = Dropout(dropout)(y)
outputs = Dense(num_labels, activation='softmax')(y)

# build the model by supplying inputs/outputs
model = Model(inputs=inputs, outputs=outputs)
# network model in text
model.summary()

# classifier loss, Adam optimizer, classifier accuracy
model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])

# train the model with input images and labels
model.fit(x_train,
          y_train,
          validation_data=(x_test, y_test),
          epochs=20,
          batch_size=batch_size)

# model accuracy on test dataset
score = model.evaluate(x_test, y_test, batch_size=batch_size)
print("\nTest accuracy: %.1f%%" % (100.0 * score[1]))

默认情况下,MaxPooling2D使用pool_size=2,因此该参数已被移除。

在前面的代码中,每个层都是张量的函数。它们每个都会生成一个张量作为输出,这个输出会成为下一个层的输入。为了创建这个模型,我们可以调用Model()并提供inputsoutputs张量,或者提供张量的列表。其他部分保持不变。

相同的列表也可以使用 fit()evaluate() 函数进行训练和评估,与顺序模型类似。sequential 类实际上是 Model 类的子类。我们需要记住在 fit() 函数中插入了 validation_data 参数以查看训练过程中验证准确度的进展。准确度在 20 个 epoch 中的范围从 99.3% 到 99.4%。

创建一个两输入一输出的模型

现在我们将要做一些非常激动人心的事情,创建一个具有两个输入和一个输出的高级模型。在我们开始之前,重要的是要知道,这对于顺序模型来说并不是一件简单的事情。

假设有一个新的 MNIST 数字分类模型被发明了,它被称为Y-Network,如 图 2.1.1 所示。Y-Network 使用相同的输入两次,分别在左右 CNN 分支上。网络使用 concatenate 层合并结果。合并操作 concatenate 类似于沿着连接轴堆叠两个形状相同的张量以形成一个张量。例如,沿着最后一个轴连接两个形状为 (3, 3, 16) 的张量将得到一个形状为 (3, 3, 32) 的张量。

concatenate 层之后的其他内容与之前的 CNN 模型保持不变,即 Flatten-Dropout-Dense

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_01.jpg

图 2.1.1: Y-Network 接受相同的输入两次,但在两个卷积网络分支中处理输入。分支的输出通过 concatenate 层组合。最后一层的预测将与之前的 CNN 示例类似。

要改进 列表 2.1.1 中模型的性能,我们可以提出几个改变。首先,Y-Network 的分支正在将滤波器数量加倍,以补偿 MaxPooling2D() 后特征映射大小的减半。例如,如果第一个卷积的输出为 (28, 28, 32),经过最大池化后新形状为 (14, 14, 32)。接下来的卷积将有 64 个滤波器大小,并且输出尺寸为 (14, 14, 64)。

其次,尽管两个分支的卷积核尺寸都为 3,右分支使用了扩张率为 2。图 2.1.2 展示了在大小为 3 的卷积核上不同扩张率的效果。这个想法是通过增加扩张率增加卷积核的覆盖范围,CNN 将使右分支能够学习不同的特征映射。我们将使用 padding='same' 选项确保在使用扩张 CNN 时不会出现负张量维度。通过使用 padding='same',我们将保持输入的尺寸与输出特征映射的尺寸相同。这是通过填充输入以确保输出具有相同大小来完成的:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_02.jpg

图 2.1.2: 通过增加扩张率从 1 开始,有效卷积核覆盖范围也会增加。

以下清单展示了 Y-网络的实现。两个分支是通过两个 for 循环创建的。两个分支期望相同的输入形状。这两个 for 循环将创建两个 3 层堆叠的 Conv2D-Dropout-MaxPooling2D。尽管我们使用了 concatenate 层来组合左右分支的输出,但我们也可以使用 Keras 的其他合并函数,例如 adddotmultiply。合并函数的选择并非完全任意,而是必须基于合理的模型设计决策。

在 Y-网络中,concatenate 不会丢弃特征图的任何部分。相反,我们会让 Dense 层来处理拼接后的特征图。清单 2.1.2,cnn-y-network-2.1.2.py 展示了使用功能性 API 实现的 Y-网络:

import numpy as np

from keras.layers import Dense, Dropout, Input
from keras.layers import Conv2D, MaxPooling2D, Flatten
from keras.models import Model
from keras.layers.merge import concatenate
from keras.datasets import mnist
from keras.utils import to_categorical
from keras.utils import plot_model

# load MNIST dataset
(x_train, y_train), (x_test, y_test) = mnist.load_data()

   # compute the number of labels
   num_labels = len(np.unique(y_train))

   # convert to one-hot vector
   y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

# reshape and normalize input images
image_size = x_train.shape[1]
x_train = np.reshape(x_train,[-1, image_size, image_size, 1])
x_test = np.reshape(x_test,[-1, image_size, image_size, 1])
x_train = x_train.astype('float32') / 255
x_test = x_test.astype('float32') / 255

# network parameters
input_shape = (image_size, image_size, 1)
batch_size = 32
kernel_size = 3
dropout = 0.4
n_filters = 32

# left branch of Y network
left_inputs = Input(shape=input_shape)
x = left_inputs
filters = n_filters
# 3 layers of Conv2D-Dropout-MaxPooling2D
# number of filters doubles after each layer (32-64-128)
for i in range(3):
    x = Conv2D(filters=filters,
               kernel_size=kernel_size,
               padding='same',
               activation='relu')(x)
    x = Dropout(dropout)(x)
    x = MaxPooling2D()(x)
    filters *= 2

# right branch of Y network
right_inputs = Input(shape=input_shape)
y = right_inputs
filters = n_filters
# 3 layers of Conv2D-Dropout-MaxPooling2D
# number of filters doubles after each layer (32-64-128)
for i in range(3):
    y = Conv2D(filters=filters,
               kernel_size=kernel_size,
               padding='same',
               activation='relu',
               dilation_rate=2)(y)
    y = Dropout(dropout)(y)
    y = MaxPooling2D()(y)
    filters *= 2

# merge left and right branches outputs
y = concatenate([x, y])
# feature maps to vector before connecting to Dense layer
y = Flatten()(y)
y = Dropout(dropout)(y)
outputs = Dense(num_labels, activation='softmax')(y)

# build the model in functional API
model = Model([left_inputs, right_inputs], outputs)
# verify the model using graph
plot_model(model, to_file='cnn-y-network.png', show_shapes=True)
# verify the model using layer text description
model.summary()

# classifier loss, Adam optimizer, classifier accuracy
model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])

# train the model with input images and labels
model.fit([x_train, x_train],
          y_train,
          validation_data=([x_test, x_test], y_test),
          epochs=20,
          batch_size=batch_size)

# model accuracy on test dataset
score = model.evaluate([x_test, x_test], y_test, batch_size=batch_size)
print("\nTest accuracy: %.1f%%" % (100.0 * score[1]))

值得注意的是,Y-网络需要两个输入进行训练和验证。这两个输入是相同的,因此提供了[x_train, x_train]

在 20 个周期中,Y-网络的准确率从 99.4% 上升到 99.5%。这相比于 3 层堆叠的 CNN(其准确率范围为 99.3% 至 99.4%)略有提高。然而,这也带来了更高的复杂性以及超过一倍的参数数量。以下图表,图 2.1.3,展示了 Keras 理解并通过 plot_model() 函数生成的 Y-网络架构:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_03.jpg

图 2.1.3:在清单 2.1.2 中实现的 CNN Y-网络

这就结束了我们对功能性 API 的介绍。我们应该记住,本章的重点是构建深度神经网络,特别是 ResNet 和 DenseNet。因此,我们仅覆盖了构建它们所需的功能性 API 内容,因为覆盖整个 API 会超出本书的范围。

注意

读者可以访问keras.io/以获取有关功能性 API 的更多信息。

深度残差网络(ResNet)

深度网络的一个关键优势是它们能够从输入和特征图中学习不同层次的表示。在分类、分割、检测以及许多其他计算机视觉问题中,学习不同层次的特征通常能带来更好的性能。

然而,您会发现训练深度网络并不容易,因为在反向传播过程中,浅层的梯度会随着深度的增加而消失(或爆炸)。图 2.2.1 说明了梯度消失的问题。网络参数通过从输出层到所有前一层的反向传播进行更新。由于反向传播是基于链式法则的,因此梯度在到达浅层时会逐渐减小。这是因为小数的乘积,特别是当误差和参数的绝对值很小时。

乘法操作的次数将与网络的深度成正比。还值得注意的是,如果梯度退化,参数将无法得到适当的更新。

因此,网络将无法提升其性能:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_04.jpg

图 2.2.1:深度网络中的一个常见问题是梯度在反向传播过程中传递到浅层时会消失。

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_05.jpg

图 2.2.2:典型 CNN 中的一个块与 ResNet 中的一个块的对比。为了防止反向传播过程中梯度退化,引入了快捷连接。

为了缓解深度网络中梯度的退化问题,ResNet 引入了深度残差学习框架的概念。让我们分析一下一个块,一个深度网络的小段。

上图显示了典型 CNN 块与 ResNet 残差块之间的对比。ResNet 的理念是,为了防止梯度退化,我们将让信息通过快捷连接流向浅层。

接下来,我们将进一步探讨这两个块之间差异的更多细节。图 2.2.3 显示了另一种常用深度网络 VGG[3] 和 ResNet 的 CNN 块的更多细节。我们将层特征图表示为 x。层 l 的特征图是

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_001.jpg

CNN 层中的操作是Conv2D-批量归一化BN)-ReLU

假设我们将这一组操作表示为 H() = Conv2D-批量归一化(BN)-ReLU,这将意味着:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_002.jpg

(方程 2.2.1)

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_003.jpg

(方程 2.2.2)

换句话说,l - 2 层的特征图被转换为

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_004.jpg

H() = Conv2D-批量归一化(BN)-ReLU。相同的一组操作应用于转换

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_005.jpg

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_006.jpg

换句话说,如果我们有一个 18 层的 VGG,那么在输入图像转化为第 18 层特征图之前,会进行 18 次 H() 操作。

一般来说,我们可以观察到,层 l 输出的特征图仅受前一层特征图的直接影响。同时,对于 ResNet:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_007.jpg

(方程 2.2.3)

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_008.jpg

(方程 2.2.4)

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_06.jpg

图 2.2.3:普通 CNN 块和残差块的详细层操作

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_009.jpg

Conv2D-BN构成,这也被称为残差映射。**+**符号表示快捷连接与输出的张量元素逐一相加

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_010.jpg

快捷连接不会增加额外的参数或计算复杂度。

在 Keras 中,add 操作可以通过add()合并函数来实现。然而,两者

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_011.jpg

方程式和x应该具有相同的维度。如果维度不同,例如在更改特征图大小时,我们应该对x进行线性投影,以匹配

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_012.jpg

在原始论文中,当特征图大小减半时,线性投影是通过一个 1 × 1 卷积核和strides=2Conv2D来实现的。

回到第一章,在介绍高级深度学习与 Keras时,我们讨论了stride > 1等价于在卷积过程中跳过像素。例如,如果strides=2,则在滑动卷积核时,每跳过一个像素。

前述的方程式 2.2.32.2.4都表示 ResNet 残差块的操作。它们暗示,如果更深层的网络可以训练得错误较少,那么浅层网络不应该有更多的错误。

了解了 ResNet 的基本构建块后,我们可以设计一个用于图像分类的深度残差网络。然而,这次我们将处理一个更具挑战性和高级的数据集。

在我们的示例中,我们将考虑 CIFAR10 数据集,这是原始论文验证过的其中一个数据集。在此示例中,Keras 提供了一个 API,可以方便地访问 CIFAR10 数据集,如下所示:

from keras.datasets import cifar10
(x_train, y_train), (x_test, y_test) = cifar10.load_data()

与 MNIST 类似,CIFAR10 数据集包含 10 个类别。该数据集由小型(32 × 32)RGB 真实世界图像组成,包含飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车,每个类别对应一个图像。图 2.2.4显示了 CIFAR10 的示例图像。

在该数据集中,有 50,000 个标记的训练图像和 10,000 个标记的测试图像用于验证:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_07.jpg

图 2.2.4:CIFAR10 数据集的示例图像。完整数据集包含 50,000 个标记的训练图像和 10,000 个标记的测试图像用于验证。

对于 CIFAR10 数据集,ResNet 可以使用不同的网络架构来构建,如表 2.2.1所示。n的值和 ResNet 的相应架构已在表 2.2.2中验证过。表 2.2.1表示我们有三组残差块。每组有2n层,对应于n个残差块。在 32 × 32 的输入图像中,额外的层是第一层。

卷积核大小为 3,除了两个不同大小特征图之间的过渡层,它实现了线性映射。例如,Conv2D 的卷积核大小为 1,strides=2。为了与 DenseNet 保持一致,我们将在连接两个不同大小的残差块时使用过渡层(Transition Layer)一词。

ResNet 使用 kernel_initializer='he_normal' 来帮助反向传播时的收敛[1]。最后一层由 AveragePooling2D-Flatten-Dense 组成。值得注意的是,ResNet 不使用 dropout。并且看起来 add 合并操作和 1 × 1 卷积具有自我正则化效果。图 2.2.4 展示了 CIFAR10 数据集的 ResNet 模型架构,如 表 2.2.1 中所述。

以下代码片段展示了 Keras 中的部分 ResNet 实现。该代码已被贡献到 Keras GitHub 仓库。从 表 2.2.2 我们还可以看到,通过修改 n 的值,我们能够增加网络的深度。例如,对于 n = 18,我们已经得到了 ResNet110,这是一种拥有 110 层的深度网络。为了构建 ResNet20,我们使用 n = 3

n = 3

# model version
# orig paper: version = 1 (ResNet v1), 
# Improved ResNet: version = 2 (ResNet v2)
version = 1

# computed depth from supplied model parameter n
if version == 1:
    depth = n * 6 + 2
elif version == 2:
    depth = n * 9 + 2if version == 2:
    model = resnet_v2(input_shape=input_shape, depth=depth)
else:
    model = resnet_v1(input_shape=input_shape, depth=depth)

resnet_v1() 方法是 ResNet 的模型构建器。它使用一个工具函数 resnet_layer() 来帮助构建 Conv2D-BN-ReLU 堆栈。

它被称为版本 1,正如我们在下一节中将看到的那样,提出了改进版的 ResNet,并称之为 ResNet 版本 2,或 v2。与 ResNet 相比,ResNet v2 在残差模块设计上有所改进,从而提高了性能。

层数输出大小卷积核大小操作
卷积32 × 3216https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_013.jpg
残差模块(1)32 × 32https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_014.jpg
过渡层(1)32 × 32https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_015.jpg
16 × 16
残差模块(2)16 × 1632https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_016.jpg
过渡层(2)16 × 16https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_017.jpg
8 × 8
残差模块(3)8 × 864https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_018.jpg
平均池化1 × 1https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_019.jpg

表 2.2.1:ResNet 网络架构配置

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_08.jpg

图 2.2.4:用于 CIFAR10 数据集分类的 ResNet 模型架构

# 层数nCIFAR10 精度(原论文)CIFAR10 精度(本书)
ResNet20391.2592.16
ResNet32592.4992.46
ResNet44792.8392.50
ResNet56993.0392.71
ResNet1101893.5792.65

表 2.2.2:用 CIFAR10 验证的 ResNet 架构

以下代码片段展示了 resnet-cifar10-2.2.1.py 的部分代码,这是 ResNet v1 的 Keras 实现:

def resnet_v1(input_shape, depth, num_classes=10):
    if (depth - 2) % 6 != 0:
        raise ValueError('depth should be 6n+2 (eg 20, 32, 44 in [a])')
    # Start model definition.
    num_filters = 16
    num_res_blocks = int((depth - 2) / 6)

    inputs = Input(shape=input_shape)
    x = resnet_layer(inputs=inputs)
    # Instantiate the stack of residual units
    for stack in range(3):
        for res_block in range(num_res_blocks):
            strides = 1
            if stack > 0 and res_block == 0:
                strides = 2  # downsample
            y = resnet_layer(inputs=x,
                             num_filters=num_filters,
                             strides=strides)
            y = resnet_layer(inputs=y,
                             num_filters=num_filters,
                             activation=None)
            if stack > 0 and res_block == 0
                # linear projection residual shortcut connection 
                # to match changed dims
                x = resnet_layer(inputs=x,
                                 num_filters=num_filters,
                                 kernel_size=1,
                                 strides=strides,
                                 activation=None,
                                 batch_normalization=False)
            x = add([x, y])
            x = Activation('relu')(x)
        num_filters *= 2

    # Add classifier on top.
    # v1 does not use BN after last shortcut connection-ReLU
    x = AveragePooling2D(pool_size=8)(x)
    y = Flatten()(x)
    outputs = Dense(num_classes,
                    activation='softmax',
                    kernel_initializer='he_normal')(y)

    # Instantiate model.
    model = Model(inputs=inputs, outputs=outputs)
    return model

与原始的 ResNet 实现相比,有一些小的差异。特别是,我们不使用 SGD,而是使用 Adam。这是因为使用 Adam 时,ResNet 更容易收敛。我们还将使用学习率(lr)调度器lr_schedule(),在 80、120、160 和 180 个 epoch 时从默认的 1e-3 开始逐步减少lrlr_schedule()函数将在训练的每个 epoch 后作为callbacks变量的一部分被调用。

另一个回调函数会在验证精度有进展时每次保存检查点。在训练深度网络时,保存模型或权重检查点是一个好习惯。因为训练深度网络需要大量时间。当你想使用你的网络时,只需要重新加载检查点,训练好的模型就会被恢复。这可以通过调用 Keras 的load_model()来实现。lr_reducer()函数也被包含在内。如果在调度减少学习率之前,验证损失没有改善,该回调函数将在patience=5个 epoch 后通过某个因子减少学习率。

当调用model.fit()方法时,会提供callbacks变量。与原始论文相似,Keras 实现使用数据增强ImageDataGenerator()来提供额外的训练数据,作为正则化方案的一部分。随着训练数据量的增加,泛化能力会有所提升。

例如,一个简单的数据增强是翻转狗的照片,如下图所示(horizontal_flip=True)。如果原图是狗的照片,那么翻转后的图像仍然是狗的照片。你也可以进行其他变换,如缩放、旋转、白化等,标签依然保持不变:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_09.jpg

图 2.2.5:一个简单的数据增强是翻转原始图像

精确复制原始论文中的实现通常是困难的,特别是在使用的优化器和数据增强方面,因为本书中 Keras 实现的 ResNet 模型与原始论文中的模型在性能上存在轻微差异。

ResNet v2

在发布了关于 ResNet 的第二篇论文[4]之后,前一部分中介绍的原始模型被称为 ResNet v1。改进版的 ResNet 通常称为 ResNet v2。该改进主要体现在残差块中层的排列,如下图所示。

ResNet v2 的显著变化包括:

  • 使用 1 × 1 - 3 × 3 - 1 × 1 BN-ReLU-Conv2D堆叠

  • 批量归一化和 ReLU 激活在 2D 卷积之前

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_10.jpg

图 2.3.1:ResNet v1 与 ResNet v2 之间残差块的对比

ResNet v2 也在与resnet-cifar10-2.2.1.py相同的代码中实现:

def resnet_v2(input_shape, depth, num_classes=10):
    if (depth - 2) % 9 != 0:
        raise ValueError('depth should be 9n+2 (eg 56 or 110 in [b])')
    # Start model definition.
    num_filters_in = 16
    num_res_blocks = int((depth - 2) / 9)

    inputs = Input(shape=input_shape)
    # v2 performs Conv2D with BN-ReLU on input 
    # before splitting into 2 paths
    x = resnet_layer(inputs=inputs,
                     num_filters=num_filters_in,
                     conv_first=True)

    # Instantiate the stack of residual units
    for stage in range(3):
        for res_block in range(num_res_blocks):
            activation = 'relu'
            batch_normalization = True
            strides = 1
            if stage == 0:
                num_filters_out = num_filters_in * 4
                if res_block == 0:  # first layer and first stage
                    activation = None
                    batch_normalization = False
            else:
                num_filters_out = num_filters_in * 2
                if res_block == 0:  # 1st layer but not 1st stage
                    strides = 2    # downsample

            # bottleneck residual unit
            y = resnet_layer(inputs=x,
                             num_filters=num_filters_in,
                             kernel_size=1,
                             strides=strides,
                             activation=activation,
                             batch_normalization=batch_normalization,
                             conv_first=False)
            y = resnet_layer(inputs=y,
                             num_filters=num_filters_in,
                             conv_first=False)
            y = resnet_layer(inputs=y,
                             num_filters=num_filters_out,
                             kernel_size=1,
                             conv_first=False)
            if res_block == 0:
                # linear projection residual shortcut connection 
                # to match changed dims
                x = resnet_layer(inputs=x,
                                 num_filters=num_filters_out,
                                 kernel_size=1,
                                 strides=strides,
                                 activation=None,
                                 batch_normalization=False)
            x = add([x, y])

        num_filters_in = num_filters_out

    # add classifier on top.
    # v2 has BN-ReLU before Pooling
    x = BatchNormalization()(x)
    x = Activation('relu')(x)
    x = AveragePooling2D(pool_size=8)(x)
    y = Flatten()(x)
    outputs = Dense(num_classes,
                    activation='softmax',
                    kernel_initializer='he_normal')(y)

    # instantiate model.
    model = Model(inputs=inputs, outputs=outputs)
    return model

ResNet v2 的模型构建器在以下代码中展示。例如,为了构建 ResNet110 v2,我们将使用 n = 12

n = 12

# model version
# orig paper: version = 1 (ResNet v1), Improved ResNet: version = 2 (ResNet v2)
version = 2

# computed depth from supplied model parameter n
if version == 1:
    depth = n * 6 + 2
elif version == 2:
    depth = n * 9 + 2if version == 2:
    model = resnet_v2(input_shape=input_shape, depth=depth)
else:
    model = resnet_v1(input_shape=input_shape, depth=depth)

ResNet v2 的准确度在以下表格中展示:

# 层数nCIFAR10 精度(原文)CIFAR10 精度(本书)
ResNet56993.01
ResNet1101893.6393.15

在 Keras 应用包中,ResNet50 也已经实现,并配有相应的检查点以便重用。这是一种替代实现,但绑定于 50 层 ResNet v1。

密集连接卷积网络 (DenseNet)

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_11.jpg

图 2.4.1:DenseNet 中的 4 层 Dense 块。每一层的输入由所有前面生成的特征图组成。

DenseNet 通过一种不同的方法解决了梯度消失问题。与使用快捷连接不同,所有之前的特征图将作为下一层的输入。前面的图展示了 Dense 块中的密集互连示例。

为简便起见,在此图中我们只显示了四层。请注意,第 l 层的输入是所有先前特征图的拼接。如果我们将 BN-ReLU-Conv2D 视为操作 H(x),则第 l 层的输出为:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_020.jpg

(方程 2.4.1)

Conv2D 使用 3×3 的卷积核。每层生成的特征图数量称为增长率,k。通常,k = 12,但在论文《Densely Connected Convolutional Networks》中,Huang 等人(2017)也使用 k = 24 [5]。因此,如果特征图数量为

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_021.jpg

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_022.jpg

,那么在 图 2.4.1 的 4 层 Dense 块结束时,特征图的总数将是

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_023.jpg

DenseNet 还建议 Dense 块前面应使用 BN-ReLU-Conv2D,并且特征图的数量应为增长率的两倍,

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_024.jpg

。因此,在 Dense 块结束时,特征图的总数将是 72。我们还将使用相同的卷积核大小,即 3。在输出层,DenseNet 建议我们在 Dense()softmax 分类器之前执行一次平均池化。如果未使用数据增强,必须在 Dense 块的 Conv2D 后添加一个 dropout 层:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_12.jpg

图 2.4.2:DenseNet 中一个 Dense 块的层,包含和不包含瓶颈层 BN-ReLU-Conv2D(1)。为清晰起见,我们将卷积核大小作为 Conv2D 的一个参数。

随着网络加深,两个新问题将出现。首先,由于每一层都贡献k个特征图,因此在层l的输入数量为

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_025.jpg

。因此,特征图在深层中会迅速增长,导致计算变得缓慢。例如,对于一个 101 层的网络,当k = 12 时,这个数值为 1200 + 24 = 1224。

其次,与 ResNet 类似,随着网络深度的增加,特征图的尺寸会被缩小,以增加卷积核的覆盖范围。如果 DenseNet 在合并操作中使用了拼接,它必须解决尺寸不匹配的问题。

为了防止特征图数量增加到计算效率低下的程度,DenseNet 引入了瓶颈层,如图 2.4.2所示。其思路是,每次拼接后,应用一个大小为 4k的 1 × 1 卷积。这种维度减少技术可以防止由Conv2D(3)处理的特征图数量迅速增加。

然后,瓶颈层将 DenseNet 层修改为BN-ReLU-Conv2D(1)-BN-ReLU-Conv2D(3),而不仅仅是BN-ReLU-Conv2D(3)。为了清晰起见,我们在Conv2D中包含了卷积核大小作为参数。使用瓶颈层后,每个Conv2D(3)只处理 4k个特征图,而不是

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_026.jpg

对于层l。例如,对于 101 层的网络,最后一个Conv2D(3)的输入仍然是 48 个特征图,当k = 12 时,而不是之前计算的 1224:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_13.jpg

图 2.4.3:两个 Dense 块之间的过渡层

为了解决特征图尺寸不匹配的问题,DenseNet 将深度网络划分为多个密集块,这些密集块通过过渡层连接,如前图所示。在每个密集块内,特征图的尺寸(即宽度和高度)保持不变。

过渡层的作用是过渡从一个特征图尺寸到另一个较小的特征图尺寸,位于两个密集块之间。尺寸的减小通常是原来的一半。这个过程是通过平均池化层完成的。例如,默认pool_size=2AveragePooling2D将尺寸从(64, 64, 256)减少到(32, 32, 256)。过渡层的输入是上一个密集块中最后一个拼接层的输出。

然而,在将特征图传递给平均池化之前,它们的数量将通过一定的压缩因子减少,

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_027.jpg

,使用Conv2D(1)。DenseNet 使用

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_028.jpg

在他们的实验中。例如,如果上一个密集块的最后一次连接的输出是(64, 64, 512),那么在Conv2D(1)后,特征图的新维度将是(64, 64, 256)。当压缩和维度降低结合在一起时,过渡层由BN-Conv2D(1)-AveragePooling2D层组成。在实际中,批量归一化位于卷积层之前。

构建一个 100 层 DenseNet-BC 用于 CIFAR10

我们现在将构建一个用于 CIFAR10 数据集的DenseNet-BC瓶颈压缩)模型,具有 100 层,使用我们之前讨论的设计原则。

下表展示了模型配置,图 2.4.3展示了模型架构。列表 2.4.1展示了 100 层 DenseNet-BC 的部分 Keras 实现。我们需要注意的是,我们使用RMSprop,因为它在使用 DenseNet 时比 SGD 或 Adam 收敛得更好。

输出大小DenseNet-100 BC
卷积32 x 32https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_029.jpg
密集块(1)32 x 32https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_030.jpg
过渡层(1)32 x 32https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_031.jpg
16 x 16
密集块(2)16 x 16https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_032.jpg
过渡层(2)16 x 16https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_033.jpg
8 x 8
密集块(3)8 x 8https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_034.jpg
平均池化1 x 1https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_035.jpg
分类层Flatten-Dense(10)-softmax

表 2.4.1:用于 CIFAR10 分类的 100 层 DenseNet-BC

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_02_14.jpg

图 2.4.3:用于 CIFAR10 分类的 100 层 DenseNet-BC 模型架构

列表 2.4.1,densenet-cifar10-2.4.1.py:如表 2.4.1所示,100 层 DenseNet-BC 的部分 Keras 实现:

# start model definition
# densenet CNNs (composite function) are made of BN-ReLU-Conv2D
inputs = Input(shape=input_shape)
x = BatchNormalization()(inputs)
x = Activation('relu')(x)
x = Conv2D(num_filters_bef_dense_block,
           kernel_size=3,
           padding='same',
           kernel_initializer='he_normal')(x)
x = concatenate([inputs, x])

# stack of dense blocks bridged by transition layers
for i in range(num_dense_blocks):
    # a dense block is a stack of bottleneck layers
    for j in range(num_bottleneck_layers):
        y = BatchNormalization()(x)
        y = Activation('relu')(y)
        y = Conv2D(4 * growth_rate,
                   kernel_size=1,
                   padding='same',
                   kernel_initializer='he_normal')(y)
        if not data_augmentation:
            y = Dropout(0.2)(y)
        y = BatchNormalization()(y)
        y = Activation('relu')(y)
        y = Conv2D(growth_rate,
                   kernel_size=3,
                   padding='same',
                   kernel_initializer='he_normal')(y)
        if not data_augmentation:
            y = Dropout(0.2)(y)
        x = concatenate([x, y])

    # no transition layer after the last dense block
    if i == num_dense_blocks - 1:
        continue

    # transition layer compresses num of feature maps and 
    # reduces the size by 2
    num_filters_bef_dense_block += num_bottleneck_layers * growth_rate
    num_filters_bef_dense_block = int(num_filters_bef_dense_block * compression_factor)
    y = BatchNormalization()(x)
    y = Conv2D(num_filters_bef_dense_block,
               kernel_size=1,
               padding='same',
               kernel_initializer='he_normal')(y)
    if not data_augmentation:
        y = Dropout(0.2)(y)
    x = AveragePooling2D()(y)

# add classifier on top
# after average pooling, size of feature map is 1 x 1
x = AveragePooling2D(pool_size=8)(x)
y = Flatten()(x)
outputs = Dense(num_classes,
                kernel_initializer='he_normal',
                activation='softmax')(y)

# instantiate and compile model
# orig paper uses SGD but RMSprop works better for DenseNet
model = Model(inputs=inputs, outputs=outputs)
model.compile(loss='categorical_crossentropy',
              optimizer=RMSprop(1e-3),
              metrics=['accuracy'])
model.summary()

列表 2.4.1中的 Keras 实现训练 200 个 epochs 后,准确率为 93.74%,而论文中报告的是 95.49%。使用了数据增强。我们在 DenseNet 中使用了与 ResNet v1/v2 相同的回调函数。

对于更深的层,需要使用 Python 代码中的表格更改growth_ratedepth变量。然而,训练一个深度为 250 或 190 的网络将需要相当长的时间,正如论文中所做的那样。为了给我们一个训练时间的概念,每个 epoch 大约需要在 1060Ti GPU 上运行一个小时。尽管 Keras 应用程序模块中也有 DenseNet 的实现,但它是在 ImageNet 上训练的。

结论

在本章中,我们介绍了作为构建复杂深度神经网络模型的高级方法——功能性 API,并展示了如何使用功能性 API 构建多输入单输出的 Y 型网络。与单一分支 CNN 网络相比,该网络的精度更高。接下来,本书中的其他章节,我们将发现功能性 API 在构建更复杂和高级模型时是不可或缺的。例如,在下一章,功能性 API 将帮助我们构建模块化的编码器、解码器和自编码器。

我们还花费了大量时间探索两个重要的深度网络——ResNet 和 DenseNet。这两个网络不仅在分类中应用广泛,还应用于其他领域,如分割、检测、跟踪、生成以及视觉/语义理解。我们需要记住,理解 ResNet 和 DenseNet 中的模型设计决策,比单纯跟随原始实现更为重要。通过这种方式,我们能够将 ResNet 和 DenseNet 的关键概念应用于我们的实际需求。

参考文献

  1. Kaiming He 和其他人. 深入探讨整流器:超越人类级别的 ImageNet 分类性能。IEEE 国际计算机视觉会议论文集,2015(www.cv-foundation.org/openaccess/content_iccv_2015/papers/He_Delving_Deep_into_ICCV_2015_paper.pdf?spm=5176.100239.blogcont55892.28.pm8zm1&file=He_Delving_Deep_into_ICCV_2015_paper.pdf)。

  2. Kaiming He 和其他人. 用于图像识别的深度残差学习。IEEE 计算机视觉与模式识别会议论文集,2016a(openaccess.thecvf.com/content_cvpr_2016/papers/He_Deep_Residual_Learning_CVPR_2016_paper.pdf)。

  3. Karen Simonyan 和 Andrew Zisserman. 用于大规模图像识别的非常深的卷积网络。ICLR,2015(arxiv.org/pdf/1409.1556/)。

  4. Kaiming He 和其他人. 深度残差网络中的身份映射。欧洲计算机视觉会议。Springer 国际出版,2016b(arxiv.org/pdf/1603.05027.pdf)。

  5. Gao Huang 和其他人. 密集连接卷积网络。IEEE 计算机视觉与模式识别会议论文集,2017(openaccess.thecvf.com/content_cvpr_2017/papers/Huang_Densely_Connected_Convolutional_CVPR_2017_paper.pdf)。

  6. 谢赛宁(Saining Xie)等人。深度神经网络的聚合残差变换。计算机视觉与模式识别(CVPR),2017 年 IEEE 会议。IEEE,2017(openaccess.thecvf.com/content_cvpr_2017/papers/Xie_Aggregated_Residual_Transformations_CVPR_2017_paper.pdf)。

  7. 古斯塔夫·拉尔松(Gustav Larsson)、迈克尔·梅尔(Michael Maire)和格雷戈里·沙赫纳罗维奇(Gregory Shakhnarovich)。Fractalnet: 无残差的超深神经网络。arXiv 预印本 arXiv:1605.07648,2016 年(arxiv.org/pdf/1605.07648.pdf)。

第三章 自编码器

在上一章,第二章,深度神经网络中,您已介绍了深度神经网络的概念。现在我们将继续研究自编码器,这是一种神经网络架构,旨在找到给定输入数据的压缩表示。

与前几章类似,输入数据可以是多种形式,包括语音、文本、图像或视频。自编码器将尝试找到一种表示或编码,以便对输入数据执行有用的变换。例如,在去噪自编码器中,神经网络将尝试找到一个可以将噪声数据转换为干净数据的编码。噪声数据可能是带有静态噪音的音频录音,然后将其转换为清晰的声音。自编码器将自动从数据中学习编码,无需人工标注。因此,自编码器可以归类为无监督学习算法。

在本书的后续章节中,我们将介绍生成对抗网络GANs)和变分自编码器VAEs),它们也是无监督学习算法的代表形式。这与我们在前几章讨论的监督学习算法不同,后者需要人工标注。

在最简单的形式中,自编码器将通过尝试将输入复制到输出的方式来学习表示或编码。然而,使用自编码器并不是简单地将输入复制到输出。否则,神经网络将无法揭示输入分布中的隐藏结构。

自编码器将输入分布编码成低维张量,通常表现为一个向量。这将近似于通常称为潜在表示、编码或向量的隐藏结构。这个过程构成了编码部分。然后,潜在向量将通过解码器部分被解码,以恢复原始输入。

由于潜在向量是输入分布的低维压缩表示,因此应当预期通过解码器恢复的输出只能近似输入。输入和输出之间的不相似度可以通过损失函数来度量。

那么,为什么我们要使用自编码器呢?简单来说,自编码器在其原始形式或作为更复杂神经网络的一部分都有实际应用。它们是理解深度学习高级主题的关键工具,因为它们提供了一个低维的潜在向量。此外,它可以高效处理,用于对输入数据执行结构性操作。常见的操作包括去噪、着色、特征级运算、检测、跟踪和分割,仅举几例。

总结来说,本章的目标是呈现:

  • 自编码器的原理

  • 如何将自编码器实现到 Keras 神经网络库中

  • 去噪和颜色化自编码器的主要特征

自编码器原理

在本节中,我们将介绍自编码器的原理。在本节中,我们将查看使用 MNIST 数据集的自编码器,这是我们在前几章中首次介绍的。

首先,我们需要意识到自编码器有两个操作符,它们是:

  • 编码器:它将输入 x 转换为低维潜在向量 z = f(x)。由于潜在向量维度较低,编码器被迫仅学习输入数据的最重要特征。例如,在 MNIST 数字的情况下,重要的特征可能包括书写风格、倾斜角度、笔画圆度、粗细等。本质上,这些是表示数字零到九所需的最重要信息。

  • 解码器:它尝试从潜在向量恢复输入,https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_001.jpg

    。尽管潜在向量维度较低,但它具有足够的大小,使解码器能够恢复输入数据。

解码器的目标是使

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_002.jpg

尽可能接近 x。通常,编码器和解码器都是非线性函数。z 的维度是它能表示的显著特征数量的度量。为了提高效率并限制潜在编码仅学习输入分布的最显著特性,维度通常远小于输入维度[1]。

当潜在编码的维度明显大于 x 时,自编码器倾向于记住输入。

适当的损失函数,

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_003.jpg

,是输入 x 和输出(即恢复的输入)之间不相似度的度量,

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_004.jpg

如下方的方程所示,均方误差MSE)是这种损失函数的一个例子:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_005.jpg

(方程 3.1.1)

在此示例中,m 是输出维度(例如,在 MNIST 中 m = 宽度 × 高度 × 通道 = 28 × 28 × 1 = 784)。

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_006.jpg

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_007.jpg

x 的元素,并且

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_008.jpg

分别。由于损失函数是输入和输出之间不相似度的度量,我们可以使用其他重建损失函数,如二元交叉熵或结构相似性指数SSIM)。

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_01.jpg

图 3.1.1:自编码器的框图

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_02.jpg

图 3.1.2:带有 MNIST 数字输入和输出的自编码器。潜在向量是 16 维的。

为了使自编码器具有上下文,x 可以是一个 MNIST 数字,其维度为 28 × 28 × 1 = 784。编码器将输入转换为一个低维度的 z,它可以是一个 16 维的潜在向量。解码器将尝试以以下形式恢复输入

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_010.jpg

z 中获取。直观地看,每个 MNIST 数字 x 将与

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_011.jpg

. 图 3.1.2 向我们展示了这个自编码过程。我们可以观察到,解码后的数字 7,虽然不是完全相同,但足够接近。

由于编码器和解码器都是非线性函数,我们可以使用神经网络来实现它们。例如,在 MNIST 数据集中,可以通过 MLP 或 CNN 来实现自编码器。自编码器可以通过最小化损失函数并通过反向传播进行训练。与其他神经网络类似,唯一的要求是损失函数必须是可微的。

如果我们将输入视为一个分布,我们可以将编码器解释为分布的编码器,

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_012.jpg

编码器和解码器作为分布的解码器,

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_013.jpg

. 自编码器的损失函数表示如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_014.jpg

(公式 3.1.2)

损失函数简单地意味着我们希望在给定潜在向量分布的情况下,最大化恢复输入分布的机会。如果假设解码器输出分布是高斯分布,那么损失函数就简化为 MSE,因为:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_015.jpg

(公式 3.1.3)

在这个例子中,

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_016.jpg

表示均值为的高斯分布

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_017.jpg

和方差为

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_018.jpg

. 假设常数方差。解码器输出

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_019.jpg

假设是独立的。m 是输出维度。

使用 Keras 构建自编码器

接下来我们将进行一个非常激动人心的内容,使用 Keras 库构建一个自编码器。为了简化起见,我们将使用 MNIST 数据集作为第一组示例。自编码器将从输入数据中生成一个潜在向量,并通过解码器恢复输入。第一个示例中的潜在向量是 16 维的。

首先,我们将通过构建编码器来实现自动编码器。清单 3.2.1 显示了编码器,它将 MNIST 数字压缩为一个 16 维的潜在向量。编码器是由两层 Conv2D 堆叠而成。最后一个阶段是一个有 16 个单元的 Dense 层,用于生成潜在向量。图 3.2.1 显示了 plot_model() 生成的架构模型图,它与 encoder.summary() 生成的文本版本相同。最后一个 Conv2D 层输出的形状被保存下来,用于计算解码器输入层的维度,以便轻松重建 MNIST 图像。

以下的代码清单 3.2.1 显示了 autoencoder-mnist-3.2.1.py。这是一个使用 Keras 实现的自动编码器。潜在向量是 16 维的:

from keras.layers import Dense, Input
from keras.layers import Conv2D, Flatten
from keras.layers import Reshape, Conv2DTranspose
from keras.models import Model
from keras.datasets import mnist
from keras.utils import plot_model
from keras import backend as K

import numpy as np
import matplotlib.pyplot as plt

# load MNIST dataset
(x_train, _), (x_test, _) = mnist.load_data()

# reshape to (28, 28, 1) and normalize input images
image_size = x_train.shape[1]
x_train = np.reshape(x_train, [-1, image_size, image_size, 1])
x_test = np.reshape(x_test, [-1, image_size, image_size, 1])
x_train = x_train.astype('float32') / 255
x_test = x_test.astype('float32') / 255

# network parameters
input_shape = (image_size, image_size, 1)
batch_size = 32
kernel_size = 3
latent_dim = 16
# encoder/decoder number of filters per CNN layer
layer_filters = [32, 64]

# build the autoencoder model
# first build the encoder model
inputs = Input(shape=input_shape, name='encoder_input')
x = inputs
# stack of Conv2D(32)-Conv2D(64)
for filters in layer_filters:
    x = Conv2D(filters=filters,
               kernel_size=kernel_size,
               activation='relu',
               strides=2,
               padding='same')(x)

# shape info needed to build decoder model 
# so we don't do hand computation
# the input to the decoder's first Conv2DTranspose 
# will have this shape
# shape is (7, 7, 64) which is processed by 
# the decoder back to (28, 28, 1)
shape = K.int_shape(x)

# generate latent vector
x = Flatten()(x)
latent = Dense(latent_dim, name='latent_vector')(x)

# instantiate encoder model
encoder = Model(inputs, latent, name='encoder')
encoder.summary()
plot_model(encoder, to_file='encoder.png', show_shapes=True)

# build the decoder model
latent_inputs = Input(shape=(latent_dim,), name='decoder_input')
# use the shape (7, 7, 64) that was earlier saved
x = Dense(shape[1] * shape[2] * shape[3])(latent_inputs)
# from vector to suitable shape for transposed conv
x = Reshape((shape[1], shape[2], shape[3]))(x)

# stack of Conv2DTranspose(64)-Conv2DTranspose(32)
for filters in layer_filters[::-1]:
    x = Conv2DTranspose(filters=filters,
                        kernel_size=kernel_size,
                        activation='relu',
                        strides=2,
                        padding='same')(x)

# reconstruct the input
outputs = Conv2DTranspose(filters=1,
                          kernel_size=kernel_size,
                          activation='sigmoid',
                          padding='same',
                          name='decoder_output')(x)

# instantiate decoder model
decoder = Model(latent_inputs, outputs, name='decoder')
decoder.summary()
plot_model(decoder, to_file='decoder.png', show_shapes=True)

# autoencoder = encoder + decoder
# instantiate autoencoder model
   autoencoder = Model(inputs,
                       decoder(encoder(inputs)),
                       name='autoencoder')
   autoencoder.summary()
   plot_model(autoencoder,
              to_file='autoencoder.png',
           show_shapes=True)

# Mean Square Error (MSE) loss funtion, Adam optimizer
autoencoder.compile(loss='mse', optimizer='adam')

# train the autoencoder
autoencoder.fit(x_train,
                x_train,
                validation_data=(x_test, x_test),
                epochs=1,
                batch_size=batch_size)

# predict the autoencoder output from test data
x_decoded = autoencoder.predict(x_test)

# display the 1st 8 test input and decoded images
imgs = np.concatenate([x_test[:8], x_decoded[:8]])
imgs = imgs.reshape((4, 4, image_size, image_size))
imgs = np.vstack([np.hstack(i) for i in imgs])
plt.figure()
plt.axis('off')
plt.title('Input: 1st 2 rows, Decoded: last 2 rows')
plt.imshow(imgs, interpolation='none', cmap='gray')
plt.savefig('input_and_decoded.png')
plt.show()

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_03.jpg

图 3.2.1:编码器模型由 Conv2D(32)-Conv2D(64)-Dense(16) 组成,用于生成低维潜在向量。

清单 3.2.1 中的解码器将潜在向量解压缩,以恢复 MNIST 数字。解码器的输入阶段是一个 Dense 层,用于接受潜在向量。单元的数量等于编码器中 Conv2D 输出维度的乘积。这样做是为了方便将 Dense 层的输出调整为 Conv2DTranspose 的输入,最终恢复原始的 MNIST 图像维度。

解码器由三层 Conv2DTranspose 堆叠而成。在我们的例子中,我们将使用 反向卷积神经网络(有时称为反卷积),这种结构在解码器中更为常见。我们可以把反向卷积神经网络(Conv2DTranspose)想象成卷积神经网络的反向过程。在一个简单的例子中,如果卷积神经网络将图像转换为特征图,反向卷积神经网络则会根据特征图生成图像。图 3.2.2 显示了解码器模型。

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_04.jpg

图 3.2.2:解码器模型由 Dense(16)-Conv2DTranspose(64)-Conv2DTranspose(32)-Conv2DTranspose(1) 组成。输入是潜在向量,被解码以恢复原始输入。

通过将编码器和解码器结合在一起,我们可以构建自动编码器。图 3.2.3 说明了自动编码器的模型图。编码器的张量输出也是解码器的输入,解码器生成自动编码器的输出。在这个示例中,我们将使用 MSE 损失函数和 Adam 优化器。在训练过程中,输入和输出相同,即 x_train。我们应该注意,在这个示例中,只有少数几层就足够使验证损失在一轮训练中降到 0.01。对于更复杂的数据集,可能需要更深的编码器、解码器以及更多的训练轮次。

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_05.jpg

图 3.2.3:自动编码器模型是通过将编码器模型和解码器模型结合在一起构建的。这个自动编码器有 178k 个参数。

在对自编码器训练一个周期后,验证损失为 0.01,我们能够验证它是否可以编码和解码之前未见过的 MNIST 数据。图 3.2.4展示了来自测试数据的八个样本及其对应的解码图像。除了图像中的轻微模糊外,我们可以轻松识别出自编码器能够以良好的质量恢复输入数据。随着训练轮数的增加,结果将会得到改善。

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_06.jpg

图 3.2.4:从测试数据中预测的自编码器输出。前两行是原始输入的测试数据,后两行是预测的数据。

此时,我们可能会想,如何可视化潜在向量空间呢?一种简单的可视化方法是强迫自编码器通过使用 2 维潜在向量来学习 MNIST 数字的特征。这样,我们就能够将这个潜在向量投影到 2D 空间中,从而看到 MNIST 编码的分布情况。通过在 autoencoder-mnist-3.2.1.py 代码中设置 latent_dim = 2,并使用 plot_results() 绘制 MNIST 数字与 2 维潜在向量的关系,图 3.2.5图 3.2.6 展示了 MNIST 数字在潜在编码上的分布情况。这些图形是在训练 20 个周期后生成的。为了方便起见,程序已保存为 autoencoder-2dim-mnist-3.2.2.py,其部分代码在代码清单 3.2.2中显示。

以下是代码清单 3.2.2,autoencoder-2dim-mnist-3.2.2.py,它展示了用于可视化 MNIST 数字在 2 维潜在编码上的分布的函数。其余代码实际上与代码清单 3.2.1类似,这里不再展示。

def plot_results(models,
                 data,
                 batch_size=32,
                 model_name="autoencoder_2dim"):
    """Plots 2-dim latent values as color gradient
        then, plot MNIST digits as function of 2-dim latent vector

    Arguments:
        models (list): encoder and decoder models
        data (list): test data and label
        batch_size (int): prediction batch size
        model_name (string): which model is using this function
    """

    encoder, decoder = models
    x_test, y_test = data
    os.makedirs(model_name, exist_ok=True)

    filename = os.path.join(model_name, "latent_2dim.png")
    # display a 2D plot of the digit classes in the latent space
    z = encoder.predict(x_test,
                        batch_size=batch_size)
    plt.figure(figsize=(12, 10))
    plt.scatter(z[:, 0], z[:, 1], c=y_test)
    plt.colorbar()
    plt.xlabel("z[0]")
    plt.ylabel("z[1]")
    plt.savefig(filename)
    plt.show()

    filename = os.path.join(model_name, "digits_over_latent.png")
    # display a 30x30 2D manifold of the digits
    n = 30
    digit_size = 28
    figure = np.zeros((digit_size * n, digit_size * n))
    # linearly spaced coordinates corresponding to the 2D plot
    # of digit classes in the latent space
    grid_x = np.linspace(-4, 4, n)
    grid_y = np.linspace(-4, 4, n)[::-1]

    for i, yi in enumerate(grid_y):
        for j, xi in enumerate(grid_x):
            z = np.array([[xi, yi]])
            x_decoded = decoder.predict(z)
            digit = x_decoded[0].reshape(digit_size, digit_size)
            figure[i * digit_size: (i + 1) * digit_size,
                   j * digit_size: (j + 1) * digit_size] = digit

    plt.figure(figsize=(10, 10))
    start_range = digit_size // 2
    end_range = n * digit_size + start_range + 1
    pixel_range = np.arange(start_range, end_range, digit_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[0]")
    plt.ylabel("z[1]")
    plt.imshow(figure, cmap='Greys_r')
    plt.savefig(filename)
    plt.show()

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_07.jpg

图 3.2.5:MNIST 数字分布与潜在编码维度 z[0] 和 z[1] 的关系。原始彩色照片可在书籍的 GitHub 仓库中找到,https://github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras/blob/master/chapter3-autoencoders/README.md。

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_08.jpg

图 3.2.6:当导航 2 维潜在向量空间时生成的数字

图 3.2.5中,我们能够看到特定数字的潜在编码在空间中的某个区域聚集。例如,数字 0 位于左下象限,而数字 1 位于右上象限。这种聚集在图 3.2.6中得到了呈现。事实上,同一图形展示了从潜在空间中导航或生成新数字的结果,正如在图 3.2.5中所示。

例如,从中心开始,并改变一个二维潜在向量的值朝向左下象限,显示出数字从 2 变为 0。这是可以预期的,因为从图 3.2.5,我们可以看到数字 2 的代码簇接近中心,而数字 0 的代码簇则位于左下象限。如图 3.2.6所示,我们只探索了每个潜在维度在-4.0 到+4.0 之间的区域。

如在图 3.2.5所示,潜在代码分布是不连续的,并且超出了

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_020.jpg

。理想情况下,它应该像一个圆形,那里每个地方都有有效的值。由于这种不连续性,存在一些区域,如果我们解码潜在向量,可能会生成几乎无法识别的数字。

去噪自编码器(DAE)

我们现在要构建一个具有实际应用的自编码器。首先,让我们画个图,假设 MNIST 数字图像被噪声污染,这样人类阅读起来会更困难。我们可以构建一个去噪自编码器DAE)来去除这些图像中的噪声。图 3.3.1展示了三组 MNIST 数字。每组的顶部行(例如,MNIST 数字 7、2、1、9、0、6、3、4、9)是原始图像。中间行显示了 DAE 的输入,即被噪声污染的原始图像。底部行显示了 DAE 的输出:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_09.jpg

图 3.3.1:原始 MNIST 数字(顶部行),被污染的原始图像(中间行)和去噪图像(底部行)

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_10.jpg

图 3.3.2:去噪自编码器的输入是被污染的图像,输出是干净的或去噪的图像。假设潜在向量为 16 维。

图 3.3.2所示,去噪自编码器的结构实际上与我们在前一部分中介绍的 MNIST 自编码器相同。输入定义为:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_021.jpg

(方程式 3.3.1)

在这个公式中,

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_022.jpg

表示被噪声污染的原始 MNIST 图像。

编码器的目标是发现如何生成潜在向量z,使得解码器能够恢复

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_023.jpg

通过最小化失真损失函数(如均方误差 MSE),如图所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_024.jpg

(方程式 3.3.2)

在这个例子中,m 是输出维度(例如,在 MNIST 中,m = 宽度 × 高度 × 通道 = 28 × 28 × 1 = 784)。

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_025.jpg

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_026.jpg

是…的元素

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_027.jpg

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_028.jpg

,分别。

为了实现 DAE,我们需要对上一节中的自编码器做一些修改。首先,训练输入数据应该是损坏的 MNIST 数字。训练输出数据与原始清晰的 MNIST 数字相同。这就像是告诉自编码器正确的图像应该是什么,或者要求它在给定损坏图像的情况下找出如何去除噪声。最后,我们必须在损坏的 MNIST 测试数据上验证自编码器。

图 3.3.2左侧展示的 MNIST 数字 7 是一个实际的损坏图像输入。右侧的是经过训练的去噪自编码器输出的清晰图像。

清单 3.3.1展示了已贡献至 Keras GitHub 仓库的去噪自编码器。使用相同的 MNIST 数据集,我们能够通过添加随机噪声来模拟损坏的图像。添加的噪声是一个高斯分布,均值为,

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_029.jpg

和标准差

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_030.jpg

。由于添加随机噪声可能会将像素数据推向无效值(小于 0 或大于 1),因此像素值会被裁剪到[0.1, 1.0]范围内。

其他部分将与上一节中的自编码器几乎相同。我们将使用相同的 MSE 损失函数和 Adam 优化器。然而,训练的 epoch 数增加到了 10。这是为了允许足够的参数优化。

图 3.3.1展示了实际的验证数据,包含损坏和去噪后的 MNIST 测试数字。我们甚至能够看到,人类会发现很难读取损坏的 MNIST 数字。图 3.3.3展示了 DAE 在噪声水平提高时的某种鲁棒性。

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_031.jpg

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_032.jpg

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_033.jpg

。在

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_034.jpg

,DAE 仍然能够恢复原始图像。然而,在

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_035.jpg

,在第二组和第三组中,像数字 4 和 5 这样的几个数字已经无法正确恢复。

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_11.jpg

图 3.3.3:去噪自编码器在噪声水平增加时的表现

如清单 3.3.1 所示,denoising-autoencoder-mnist-3.3.1.py向我们展示了一个去噪自编码器:

from keras.layers import Dense, Input
from keras.layers import Conv2D, Flatten
from keras.layers import Reshape, Conv2DTranspose
from keras.models import Model
from keras import backend as K
from keras.datasets import mnist
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image

np.random.seed(1337)

# load MNIST dataset
(x_train, _), (x_test, _) = mnist.load_data()

# reshape to (28, 28, 1) and normalize input images
image_size = x_train.shape[1]
x_train = np.reshape(x_train, [-1, image_size, image_size, 1])
x_test = np.reshape(x_test, [-1, image_size, image_size, 1])
x_train = x_train.astype('float32') / 255
x_test = x_test.astype('float32') / 255

# generate corrupted MNIST images by adding noise with normal dist
# centered at 0.5 and std=0.5
noise = np.random.normal(loc=0.5, scale=0.5, size=x_train.shape)
x_train_noisy = x_train + noise
noise = np.random.normal(loc=0.5, scale=0.5, size=x_test.shape)
x_test_noisy = x_test + noise

# adding noise may exceed normalized pixel values>1.0 or <0.0
# clip pixel values >1.0 to 1.0 and <0.0 to 0.0
x_train_noisy = np.clip(x_train_noisy, 0., 1.)
x_test_noisy = np.clip(x_test_noisy, 0., 1.)

# network parameters
input_shape = (image_size, image_size, 1)
batch_size = 32
kernel_size = 3
latent_dim = 16
# encoder/decoder number of CNN layers and filters per layer
layer_filters = [32, 64]

# build the autoencoder model
# first build the encoder model
inputs = Input(shape=input_shape, name='encoder_input')
x = inputs

# stack of Conv2D(32)-Conv2D(64)
for filters in layer_filters:
    x = Conv2D(filters=filters,
               kernel_size=kernel_size,
               strides=2,
               activation='relu',
               padding='same')(x)

# shape info needed to build decoder model 
# so we don't do hand computation
# the input to the decoder's first Conv2DTranspose 
# will have this shape
# shape is (7, 7, 64) which can be processed by 
# the decoder back to (28, 28, 1)
shape = K.int_shape(x)

# generate the latent vector
x = Flatten()(x)
latent = Dense(latent_dim, name='latent_vector')(x)

# instantiate encoder model
encoder = Model(inputs, latent, name='encoder')
encoder.summary()

# build the decoder model
latent_inputs = Input(shape=(latent_dim,), name='decoder_input')
# use the shape (7, 7, 64) that was earlier saved
x = Dense(shape[1] * shape[2] * shape[3])(latent_inputs)
# from vector to suitable shape for transposed conv
x = Reshape((shape[1], shape[2], shape[3]))(x)

# stack of Conv2DTranspose(64)-Conv2DTranspose(32)
for filters in layer_filters[::-1]:
    x = Conv2DTranspose(filters=filters,
                        kernel_size=kernel_size,
                        strides=2,
                        activation='relu',
                        padding='same')(x)

# reconstruct the denoised input
outputs = Conv2DTranspose(filters=1,
                          kernel_size=kernel_size,
                          padding='same',
                          activation='sigmoid',
                          name='decoder_output')(x)

# instantiate decoder model
decoder = Model(latent_inputs, outputs, name='decoder')
decoder.summary()

# autoencoder = encoder + decoder
# instantiate autoencoder model
autoencoder = Model(inputs, decoder(encoder(inputs)), name='autoencoder')
autoencoder.summary()

# Mean Square Error (MSE) loss function, Adam optimizer
autoencoder.compile(loss='mse', optimizer='adam')

# train the autoencoder
autoencoder.fit(x_train_noisy,
                x_train,
                validation_data=(x_test_noisy, x_test),
                epochs=10,
                batch_size=batch_size)

# predict the autoencoder output from corrupted test images
x_decoded = autoencoder.predict(x_test_noisy)

# 3 sets of images with 9 MNIST digits
# 1st rows - original images
# 2nd rows - images corrupted by noise
# 3rd rows - denoised images
rows, cols = 3, 9
num = rows * cols
imgs = np.concatenate([x_test[:num], x_test_noisy[:num], x_decoded[:num]])
imgs = imgs.reshape((rows * 3, cols, image_size, image_size))
imgs = np.vstack(np.split(imgs, rows, axis=1))
imgs = imgs.reshape((rows * 3, -1, image_size, image_size))
imgs = np.vstack([np.hstack(i) for i in imgs])
imgs = (imgs * 255).astype(np.uint8)
plt.figure()
plt.axis('off')
plt.title('Original images: top rows, '
          'Corrupted Input: middle rows, '
          'Denoised Input:  third rows')
plt.imshow(imgs, interpolation='none', cmap='gray')
Image.fromarray(imgs).save('corrupted_and_denoised.png')
plt.show()

自动着色自编码器

我们现在要进行自编码器的另一个实际应用。在这个案例中,我们假设有一张灰度照片,我们希望构建一个能够自动为其添加颜色的工具。我们希望模拟人类的能力,能够识别出大海和天空是蓝色的,草地和树木是绿色的,而云是白色的,等等。

图 3.4.1所示,如果我们得到一张前景为稻田、背景为火山、顶部为天空的灰度照片,我们能够为其添加适当的颜色。

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_12.jpg

图 3.4.1:为梅雁火山的灰度照片添加颜色。颜色化网络应该模仿人类的能力,通过给灰度照片添加颜色。左图是灰度图,右图是彩色图。原始的彩色照片可以在本书的 GitHub 仓库中找到,链接为 https://github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras/blob/master/chapter3-autoencoders/README.md。

一个简单的自动颜色化算法似乎是自编码器的合适问题。如果我们可以用足够多的灰度照片作为输入,并将对应的彩色照片作为输出进行训练,它可能会发现隐藏的结构并正确地应用颜色。大致来说,它是去噪的反向过程。问题是,自编码器能否为原始灰度图像添加颜色(良好的噪声)?

列表 3.4.1 显示了颜色化自编码器网络。该颜色化自编码器网络是我们之前用于 MNIST 数据集的去噪自编码器的修改版。首先,我们需要一个灰度到彩色照片的数据集。我们之前使用过的 CIFAR10 数据库包含 50,000 张训练照片和 10,000 张 32 × 32 的 RGB 测试照片,可以转换为灰度图像。如下所示,我们可以使用rgb2gray()函数,通过对 R、G 和 B 分量加权,将彩色图像转换为灰度图像。

列表 3.4.1,colorization-autoencoder-cifar10-3.4.1.py,展示了一个使用 CIFAR10 数据集的颜色化自编码器:

from keras.layers import Dense, Input
from keras.layers import Conv2D, Flatten
from keras.layers import Reshape, Conv2DTranspose
from keras.models import Model
from keras.callbacks import ReduceLROnPlateau, ModelCheckpoint
from keras.datasets import cifar10
from keras.utils import plot_model
from keras import backend as K

import numpy as np
import matplotlib.pyplot as plt
import os

# convert from color image (RGB) to grayscale
# source: opencv.org
# grayscale = 0.299*red + 0.587*green + 0.114*blue
def rgb2gray(rgb):
    return np.dot(rgb[...,:3], [0.299, 0.587, 0.114])

# load the CIFAR10 data
(x_train, _), (x_test, _) = cifar10.load_data()

# input image dimensions
# we assume data format "channels_last"
img_rows = x_train.shape[1]
img_cols = x_train.shape[2]
channels = x_train.shape[3]

# create saved_images folder
imgs_dir = 'saved_images'
save_dir = os.path.join(os.getcwd(), imgs_dir)
if not os.path.isdir(save_dir):
        os.makedirs(save_dir)

# display the 1st 100 input images (color and gray)
imgs = x_test[:100]
imgs = imgs.reshape((10, 10, img_rows, img_cols, channels))
imgs = np.vstack([np.hstack(i) for i in imgs])
plt.figure()
plt.axis('off')
plt.title('Test color images (Ground Truth)')
plt.imshow(imgs, interpolation='none')
plt.savefig('%s/test_color.png' % imgs_dir)
plt.show()

# convert color train and test images to gray
x_train_gray = rgb2gray(x_train)
x_test_gray = rgb2gray(x_test)

# display grayscale version of test images
imgs = x_test_gray[:100]
imgs = imgs.reshape((10, 10, img_rows, img_cols))
imgs = np.vstack([np.hstack(i) for i in imgs])
plt.figure()
plt.axis('off')
plt.title('Test gray images (Input)')
plt.imshow(imgs, interpolation='none', cmap='gray')
plt.savefig('%s/test_gray.png' % imgs_dir)
plt.show()

# normalize output train and test color images
x_train = x_train.astype('float32') / 255
x_test = x_test.astype('float32') / 255

# normalize input train and test grayscale images
x_train_gray = x_train_gray.astype('float32') / 255
x_test_gray = x_test_gray.astype('float32') / 255

# reshape images to row x col x channel for CNN output/validation
x_train = x_train.reshape(x_train.shape[0], img_rows, img_cols, channels)
x_test = x_test.reshape(x_test.shape[0], img_rows, img_cols, channels)

# reshape images to row x col x channel for CNN input
x_train_gray = x_train_gray.reshape(x_train_gray.shape[0], img_rows, img_cols, 1)
x_test_gray = x_test_gray.reshape(x_test_gray.shape[0], img_rows, img_cols, 1)

# network parameters
input_shape = (img_rows, img_cols, 1)
batch_size = 32
kernel_size = 3
latent_dim = 256
# encoder/decoder number of CNN layers and filters per layer
layer_filters = [64, 128, 256]

# build the autoencoder model
# first build the encoder model
inputs = Input(shape=input_shape, name='encoder_input')
x = inputs
# stack of Conv2D(64)-Conv2D(128)-Conv2D(256)
for filters in layer_filters:
    x = Conv2D(filters=filters,
               kernel_size=kernel_size,
               strides=2,
               activation='relu',
               padding='same')(x)

# shape info needed to build decoder model 
# so we don't do hand computation
# the input to the decoder's first Conv2DTranspose 
# will have this shape
# shape is (4, 4, 256) which is processed 
# by the decoder to (32, 32, 3)
shape = K.int_shape(x)

# generate a latent vector
x = Flatten()(x)
latent = Dense(latent_dim, name='latent_vector')(x)

# instantiate encoder model
encoder = Model(inputs, latent, name='encoder')
encoder.summary()

# build the decoder model
latent_inputs = Input(shape=(latent_dim,), name='decoder_input')
x = Dense(shape[1]*shape[2]*shape[3])(latent_inputs)
x = Reshape((shape[1], shape[2], shape[3]))(x)

# stack of Conv2DTranspose(256)-Conv2DTranspose(128)-
# Conv2DTranspose(64)
for filters in layer_filters[::-1]:
    x = Conv2DTranspose(filters=filters,
                        kernel_size=kernel_size,
                        strides=2,
                        activation='relu',
                        padding='same')(x)

outputs = Conv2DTranspose(filters=channels,
                          kernel_size=kernel_size,
                          activation='sigmoid',
                          padding='same',
                          name='decoder_output')(x)

# instantiate decoder model
decoder = Model(latent_inputs, outputs, name='decoder')
decoder.summary()

# autoencoder = encoder + decoder
# instantiate autoencoder model
autoencoder = Model(inputs, decoder(encoder(inputs)), name='autoencoder')
autoencoder.summary()

# prepare model saving directory.
save_dir = os.path.join(os.getcwd(), 'saved_models')
model_name = 'colorized_ae_model.{epoch:03d}.h5'
if not os.path.isdir(save_dir):
        os.makedirs(save_dir)
filepath = os.path.join(save_dir, model_name)

# reduce learning rate by sqrt(0.1) if the loss does not improve in 5 epochs
lr_reducer = ReduceLROnPlateau(factor=np.sqrt(0.1),
                               cooldown=0,
                               patience=5,
                               verbose=1,
                               min_lr=0.5e-6)

# save weights for future use 
# (e.g. reload parameters w/o training)
checkpoint = ModelCheckpoint(filepath=filepath,
                             monitor='val_loss',
                             verbose=1,
                             save_best_only=True)

# Mean Square Error (MSE) loss function, Adam optimizer
autoencoder.compile(loss='mse', optimizer='adam')

# called every epoch
callbacks = clr_reducer, checkpoint]

# train the autoencoder
autoencoder.fit(x_train_gray,
                x_train,
                validation_data=(x_test_gray, x_test),
                epochs=30,
                batch_size=batch_size,
                callbacks=callbacks)

# predict the autoencoder output from test data
x_decoded = autoencoder.predict(x_test_gray)

# display the 1st 100 colorized images
imgs = x_decoded[:100]
imgs = imgs.reshape((10, 10, img_rows, img_cols, channels))
imgs = np.vstack([np.hstack(i) for i in imgs])
plt.figure()
plt.axis('off')
plt.title('Colorized test images (Predicted)')
plt.imshow(imgs, interpolation='none')
plt.savefig('%s/colorized.png' % imgs_dir)
plt.show()

我们通过增加一个卷积和转置卷积块来增加自编码器的容量。我们还在每个 CNN 块中加倍了滤波器的数量。潜在向量现在是 256 维,以便增加它可以表示的显著属性的数量,如自编码器部分所讨论的那样。最后,输出滤波器的大小增加到 3,即与期望的彩色输出中的 RGB 通道数相等。

颜色化自编码器现在使用灰度图像作为输入,原始 RGB 图像作为输出进行训练。训练将需要更多的 epoch,并使用学习率调整器,在验证损失没有改善时缩小学习率。这可以通过在 Keras 的fit()函数中告诉callbacks参数调用lr_reducer()函数来轻松实现。

图 3.4.2 演示了 CIFAR10 测试数据集中的灰度图像颜色化。图 3.4.3 比较了地面真实值与颜色化自编码器的预测。自编码器完成了一个可接受的颜色化任务。海洋或天空被预测为蓝色,动物具有不同的棕色阴影,云是白色的,等等。

有一些明显的错误预测,比如红色车辆变成了蓝色,或者蓝色车辆变成了红色,偶尔绿色的田野被误判为蓝天,暗色或金色的天空被转换成了蓝色天空。

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_13.jpg

图 3.4.2:使用自编码器进行自动灰度到彩色图像转换。CIFAR10 测试灰度输入图像(左)和预测的彩色图像(右)。原始彩色照片可以在书籍的 GitHub 仓库中找到,https://github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras/blob/master/chapter3-autoencoders/README.md。

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_03_14.jpg

图 3.4.3:实地对比真实的彩色图像和预测的上色图像。原始彩色照片可以在书籍的 GitHub 仓库中找到,https://github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras/blob/master/chapter3-autoencoders/README.md。

结论

在本章中,我们介绍了自编码器,它是一种神经网络,将输入数据压缩成低维度的编码,以便高效地执行结构性转换,如去噪和上色。我们为更高级的话题——生成对抗网络(GANs)和变分自编码器(VAEs)奠定了基础,这些将在后续章节中介绍,同时仍然探讨自编码器如何利用 Keras。我们展示了如何从两个基本模型(编码器和解码器)实现自编码器。我们还学会了如何提取输入分布的隐藏结构,这是 AI 中的常见任务之一。

一旦潜在编码被揭示出来,就可以对原始输入分布执行许多结构性操作。为了更好地理解输入分布,可以通过低级嵌入方式(类似于我们在本章中所做的)或通过更复杂的降维技术,如 t-SNE 或 PCA,来可视化潜在向量形式的隐藏结构。

除了去噪和上色外,自编码器还用于将输入分布转换为低维度潜在编码,这些编码可以进一步处理用于其他任务,如分割、检测、跟踪、重建、视觉理解等。在第八章"),*变分自编码器(VAEs)*中,我们将讨论与自编码器结构相同,但通过具有可解释的潜在编码来区分的 VAEs,这些编码能够生成连续的潜在编码投影。在下一章,我们将介绍 AI 领域最近最重要的突破之一——生成对抗网络(GANs),在其中我们将学习 GANs 的核心优势及其合成看似真实的数据或信号的能力。

参考文献

  1. Ian Goodfellow 等人。深度学习。第 1 卷。剑桥:MIT 出版社,2016 年 (www.deeplearningbook.org/)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值