生成式深度学习(第二版)-译文-第二章-深度学习

章节目标

  • 了解可用深度学习建模的不同类型非结构化数据。
  • 定义一个深度神经网络,理解它如何对复杂数据库建模。
  • 构建一个多层感知机来预测图像的内容。
  • 通过引入卷积层,dropout,batch normalization 来提升模型的性能。

我们先从深度学习的基础定义开始:

深度学习是一类机器学习算法,它使用多层堆叠的处理单元 来学习 非结构化数据 的高层表示。

为了更充分的理解深度学习,我们需要更深入理解上面的定义。第一,我们要看看深度学习可以建模的不同类型非结构化数据,然后我们将深入构建多层堆叠处理单元以解决问题的机制。这将后续章节中深度学习在生成式任务重的应用打下基础。

深度学习数据

很多类型的机器学习算法要求结构化,表格式数据作为输入,即利用一列列的特征来刻画每个观察。例如,一个人的年龄,收入,上月网站访问次数,这些都是有利于预测该人是否下月会订阅特定在线服务的特征。我们可以使用这些特征的一个结构化表格来训练一个 logistic regression,随机森林,或者XGBoost模型来预测二元响应变量—此人 订阅(1) 或者 不订阅(0)? 这里,每个个体特征包括观察样本某个方面的信息,模型负责学习这些特征如何聚合作用影响最终响应。

非结构化数据 指代 那些不能被整理为一列列特征的数据,例如图像,声音,文本。当然了,图像有空间结构,声音有时间结构,文本有段落结构,视频数据既有空间又有时间结构,但是由于这些数据不能用一列列特征来表示,因此通常意义上被认为是 非结构的,如图2-1所示。
在这里插入图片描述
当我们的数据是非结构化时,单个像素,频率或者字符几乎是完全无信息量的。例如,知道图像的234个像素为泥泞黄并不能帮助我们识别出图像中到底是一栋房子,还是一只狗。类似的,知道一个句子中第24个字符是e 并无助于预测文本是关于足球的,还是关于政治的。

像素或者字符实际上只是”画布上的凹痕“, 用以嵌入更高层信息的特征,比如图像中的烟囱 或是 文本中 “前锋” 这个词。如果图像中的烟囱放置在屋子的另一侧,那么这幅图仍然包含有烟囱,但是这个信息却是由完全不同的像素集合承载。如果词语”前锋“在文本的稍前或者稍后出现,文本仍然是关于足球的,但是由不同的字符位置来提供这一信息。数据的粒度,和高度空间依赖性的结合,破坏了像素或是字符本身作为信息特征的概念。

因此,如果我们要在裸像素基础上训练logistic回归,随机森林,或者XGBoost模型,所训练的模型通常只能在最简单的分类任务上表现良好。这些模型过分依赖于输入特征是富含信息的,而非空间依赖。相反,深度学习模型,则可以从非结构化数据出发,学会自己构建高层信息特征。

深度学习可以应用于结构化数据,但是它真正的能力,尤其是涉及生成式建模,则来自于它处理非结构化数据的能力。大多数时候,我们想生成非结构化数据,例如新的图像,或者文本串,这也是为何深度学习在生成式建模领域有巨大影响的原因。

深度神经网络

大多数深度学习系统都是多隐层堆叠的人工神经网络(ANNs, artificial neural networks, 通常用 neural networks作为简写)。因此,深度学习某种意义上和深度神经网络同义。但是,任何系统,只要采用多层来学习输入数据的高层表示,都可以看作是深度学习的一种形式(如 深度信念网络).

让我们从分解神经网络开始,一起看看它们怎么从非结构化数据学习高层特征。

什么是神经网络?

神经网络包含一系列的堆叠层。每个层包含一些单元,与前层的单元通过一组权重连接。我们将看到,存在很多不同类型的层,但一种最常见的层是 全连接层,它将当前层的所有单元与前面层的所有单元进行直接的密集连接。

所有邻近层都保持全连接的神经网络叫做多层感知机(multilayer perceptrons, MLPs)。这是我们将研究的第一种神经网络类型。MLP的一个示例如下图2-2所示。在这里插入图片描述
输入(例如图像)将在网络中被逐层变换,这一过程通常被称为 前向(forward pass), 直到碰到输出层为止。特别的,每个单元对其输入的加权和施加一个非线性变换,并把输出送到下一层。最后的输出层是整个过程的高潮: 单一的单元输出一个概率值,表示原始输入属于特定类别(如微笑)的概率。

深度神经网络的魔力在于找到每层的权重集,使得预测最精准。寻找这些权重的过程我们称作网络的训练。

在网络的训练过程中,一批图像传入网络,预测的输出和真实值进行对比。例如,对于一张某人微笑的图像,网络可能输出概率80%;而对于一张某人没有微笑的图像,网络输出概率23%。对这两个例子,完美的预测是希望输出100%和0%,因此存在少量的误差。这个预测误差被网络反向传播回来,并对权重集进行微量调整,调整以最显著提升预测性能的方向进行。这一过程被称为后向传播。逐渐的,每一单元逐渐具备识别特定特征的能力,并逐渐帮助整个网络做出更好的预测。

高层特征的学习

使得神经网络如此强大的关键原因在于他们从输入数据学习特征的能力,并且完全无需人工的引导。换句话说,我们不需要做任何特征工程,这就是为啥神经网络这么有用。我们可以让模型自主决定它要怎么安排权重,唯一的导引就是要最小化其预测误差。

例如,让我们再过一遍图2-2所示的网络,假定它已经被训练好,能够精确估计一张输入脸是否在微笑:

  1. 单元A接收一个像素值作为单一通道;
  2. 单元B聚合其输入值,使得某一特定底层特征(如边缘)出现时其响应最大;
  3. 单元C聚合底层特征,使得某个高层特征(如牙齿)出现时其响应最大;
  4. 单元D聚合高层特征,使得图片中人物微笑时其响应最大。

从原始输入开始,每个后续层中的单元都可以表达逐渐复杂的概念,这是通过聚合前层的低层特征来实现的。令人惊奇的是,这是伴着训练过程自然出现的—我们压根不需要告诉每个单元它负责什么,或者它是否肩负寻找高层/低层特征的职责。

输入和输出层之间的层被称为 隐层。尽管我们的例子中仅有两层隐层,深度神经网络通常可以有更多。叠加大量的层可以使得神经网络逐渐从前面层中的低层特征抽取信息,渐进式学习到高层次特征。例如,为图像识别而设计的ResNet,就包含了152层。

接下来,我们将直接跳到深度学习的实践,并且通过TensorFlow和Keras来构建自己的神经网络。

TensorFlow和Keras

TensorFlow 是一个开源的Python机器学习库,由Google开发。TensorFlow是构建深度学习解决方案使用最多的框架之一,它尤为强调张量的处理 (这也是它得名的原因)。它提供了训练神经网络所需的底层功能,例如计算任意可微表达式的梯度计算,并能高效的进行张量操作。

Keras 是构建神经网络的高层API,基于TensorFlow构建 (如图2-3所示)。它非常灵活,友好,这使得它成为深度学习入门的绝佳选择。另外,Keras还提供了丰富的构建模组,可以被插入式的集成起来以通过其功能API创建非常复杂的深度学习架构。
在这里插入图片描述
如果你仅仅只是想进入深度学习的世界,我高度推荐你使用TensorFlow和Keras。这个组合可以让你在生产环境中构建任何网络,也能够为你提供易于学习的API,使得新想法和概念的快速原型开发成为可能。让我们一起来看看通过Keras构建一个多层感知机有多么容易吧!

多层感知机 (MLP)

在本节中,我们将训练一个MLP来使用监督学习来训练一个图像分类器。监督学习是一类在有标签样本库上训练的计算机算法。也就是说,用来训练的数据集既包含输入数据,又
包含对应的输出标签。算法的目标是学习一个输入数据和输出标签间的映射,使得该映射可以在新的、没见过的数据上做出预测。

运行本例的代码
本例子的代码可以在Jupyter Notebook中找到,具体目录在本书代码仓库 notebooks/02_deeplearning/01_mlp/mlp/ipynb

数据准备

本例中,我们将使用CIFAR-10 数据集,它包含6万张 大小为32x32的彩色图像,在Keras有预置。如图2-4所示, 每张图像都可进行10类划分。
在这里插入图片描述
默认的,图像数据在每个像素通道上的取值都在0和255之间。我们需要对图像进行预处理,将其值归一化到0和1,因为神经网络在其每个输入的绝对值<1时能取得最好的效果。

我们也需要把图像的整数标签转化成 独热向量 (one-hot encoded vectors),因为神经网络的输出是图像属于每类的概率。如果某图像的类别整数标签为 i, 那么其独热向量是一个长度为10(即 类别数) 的向量 ,该向量除了第 i 个元素为1,其余的位置都是0。这一过程在下面的代码中给出。

import numpy as np
from tensorflow.keras import datasets, utils

# 加载CIFAR-1O数据库
# x_train 和 x_test 都是numpy矩阵,形状分别为 [50000,32,32,3] 和 [10000,32,32,3].
# y_train 和 y_test 则分别是形状为 [50000,1]和[10000,1]的numpy矩阵,其中的元素是每幅图像的[0,9]整数类别标签。
(x_train, y_train), (x_test, y_test) = datasets.cifar10.load_data()

NUM_CLASS = 10

# 对图像进行尺度放缩,使得像素通道的值在[0,1]之间
x_train = x_train.astype('float32') / 255.0
x_test = x_test.astype('float32') / 255.0

# 独热编码,新的 y_train 和 y_test 形状分别是 [50000,10] 和 [10000,10]。
y_train = utils.to_categorical(y_train, NUM_CLASS)
y_test = utils.to_categorical(y_test, NUM_CLASS)

我们可以看到: 训练图像数据 (x_train) 在张量 [50000, 32, 32, 3] 中存储。这个数据集中并无 列 或者 行。而是四维度的张量。一个张量就是一个多维矩阵 — 它是矩阵向更高维的自然扩展。这个张量的第一维指代图片在数据集中的索引,第二维和第三维与图像的尺寸相关,最后一维 是通道 (即,红,绿,或 蓝,因为它们是 RGB 图像)。

例如,下面的例子2-2展示了我们怎么取得图像在特定像素的通道值。

例子2-2. 第54张图像(12,13)位置所在像素对应的绿色通道(1)值
x_train[54, 12, 13, 1] #0.36862746

在这里插入图片描述

搭建模型

在keras中,你可以把神经网络定义为一个序列模型,或者直接使用功能API。

序列模型在快速定义层级的线性堆叠(即,一层直接跟着前面的层,不引入任何分支)。如下例子2-3所示我们可以用 Sequential 类 定义MLP模型。

from tensorflow.keras import layers, models

model = models.Sequential([
	layers.Flatten(input_shape=(32,32,3)), 
	layers.Dense(200,activation = 'relu'),
	layers.Dense(150,activation = 'relu'),
	layers.Dense(10,activation = 'softmax'),
])

本书中大多数模型要求一层的输出被传递给多个后续层,或者反过来说,要求一层接受多个前序层的输入。对于这些模型,Sequential类并不合适,我们需要使用更为灵活的功能API。

小贴士
即使你只是刚刚开始使用Keras构建线性模型, 我仍然建议你使用功能API而非Sequential模型,因为长远来看前者的功能更为强大,尤其是随着你的神经网络变得越来越复杂。功能API会给你完全的自由来设计自己的深度神经网络。

样例2-4 给出了用功能API实现的相同MLP。当使用功能API时,我们使用 Model类来定义整体的输入输出层。

from tensorflow.keras import layers, models

input_layer = layers.Input(shape=(32,32,3))
x = layers.Flatten()(input_layer)
x = layers.Dense(units=200, activation = 'relu')(x)
x = layers.Dense(units=150, activation = 'relu')(x)
output_layer = layers.Dense(units=10, activation = 'softmax')(x)
model = models.Model(input_layer, output_layer)

上面两个模型给出的模型完全相同,它们的架构图如下2-5所示。
在这里插入图片描述
接下来,让我们一起更细节的讨论MLP中不同的层和激活函数。

为了构建 MLP,我们用了三类不同的层: Input, Flatten, Dense.

输入层是整个网络的入口,我们以元组的方式告诉网络每个数据元素的形状。注意:我们没有特定指出batch size,这不是必须的,因为我们可以一次性向网络的输入层传入任何数目的图像。因此,我们无需显式的在网络输入层定义中描述其batch size。

其次,我们使用Flatten层将输入拉平为一个向量。这将产生一个长度为3072 (= 32 x 32 x 3) 的向量。这么做的原因是因为后续的 Dense层要求输入是拉平的,而不是多维数组。接下来我们也将看到,还有其他的层类型要求多维矩阵作为输入,所以我们需要充分理解每层的必要输入输出形状,以判断是否有必要使用Flatten。

Dense层是神经网络的基础构建模块之一。它包含给定数目的单元,其与层面的层保持密集的链接 — 也即,层中的每一个单元与前层中的每一个单元都有连接,这个连接是单一的,且每个连接都对应一个权重值(可正可负)。某个给定单元的输出是其从前层接收输入的加权和,该和进一步通过一个非线性激活函数然后送到下一层。激活函数在网络中非常关键,因为它可以保证网络学到复杂的函数,而非仅仅只是输出其输入的线性组合。

激活函数

存在很多不同类型的激活函数,但三种最重要的类型是 ReLU,sigmoid,softmax。

ReLU(rectified linear unit)激活函数定义如下: 当输入为负时,其输出为0;当输入为正时,输出等于输入。另有一个LeakyReLU激活函数与ReLU非常相似,仅有一点差别: ReLU对于小于0的输入返回0,而LeakyReLU则返回一个正比于输入的小负数。ReLU单元某些情况下可能会”死“: 如果其持续输出0,这可能来自于激活之前对负值的大偏好。在这种情况下,梯度是0,因此该单元持续没有误差反传。LeakyReLU激活正时针对这种情况提出,它能够始终保证梯度非零。基于ReLU的函数是深度网络中常用的最可靠的激活函数,它通常能保证训练的稳定。

如果你想把某层的输出放缩到[0,1],那么Sigmoid激活函数很有用。例如,只有一个输出单元的二类分类问题,或者一个多标签分类问题(每个观察都可以属于多个类别)。图2-6给出了ReLU,LeakyReLU,以及Sigmoid函数,并进行了直观的比较。
在这里插入图片描述
如果你想某层的输出规范化到和为1,那么Softmax激活函数正合适。例如,多类分类问题(每个观察只属于多类中的某一类)。它的定义如下:

y i = e x i ∑ j = 1 J    e x j y_i = \frac{e^{x_i}}{\sum_{j=1}^J \; e^{x_j} } yi=j=1Jexjexi

其中, J J J 是该层总的单元数。在我们的神经网络中,我们在最后一层用softmax激活函数来保证10个概率的和为1,这可以被理解成图像属于各类别的似然。

在Keras中,激活函数可以在层中定义,也可以单独作为一个层来定义。如下示例2-5和示例2-6 所示:

x = layers.Dense(units=200, activation = 'relu')(x)
x = layers.Dense(units=200)(x)
x = layers.Activation('relu')(x)

在我们的例子中,我们将输入传入两个Dense层,第一个有200个单元,第二个有150个单元,两个层都有ReLU激活函数。

模型检查

我们可以用 model.summary() 方法来检查网络中每层的形状,如下表2-1所示

层(类型)输出形状参数数目
InputLayer(None, 32, 32, 3)0
Flatten(None, 3072)0
Dense(None, 200)61400
Dense(None,150)30150
Dense(None,10)1510
所有参数646260
可训练参数646260
非可训练参数0

注意我们的输入层是如何与x_train的形状匹配的,同时,我们的Dense输出层如何与 y_train的形状匹配。Keras使用 None 作为第一个维度的标记,以此表明 它还不知道要送入网络的观察数目。事实上,它也不需要,我们送入1个观察和送入1000个观察同样轻松。这是因为张量运算可以利用线性代数在所有的观察上同时操作—这个问题已经被TensorFlow部分解决。这也是为何你使用GPU训练深度网络会获得比CPU更大的性能提升: GPU面向大规模张量运行进行了优化,因为这些运算也是复杂图形处理所必备的。

summary 方法也给出了每一层训练的参数(权重)数目。如果你发现模型训练太慢,可以检查下summary,核实一下是否有某个层包含了巨量的权重。如果是,你应该考虑该层的单元数是否可以减少以加速训练。

小贴士
确保你需要理解,每层的参数数量是如何计算出来的。需要记住,一个给定层中的每个单元也与一个额外的bias单元(始终输出1)相连。这确保了即使前层所有的输入都是0,输出都可以不是0。因此,200单元的Dense 层,其参数数量为 200x(3072+1) = 614600 .

编译模型

在这个步骤里,我们用一个优化器和损失函数编译模型,如下示例2-7所示。

from tensorflow.keras import optimizer

opt = optimizer.Adam(learning_rate=0.0005)
model.compile(loss='categorical_crossentropy',optimizer=opt,metrics=['accuracy'])

现在,让我们进一步深入细节,看看到底什么是所谓的 损失函数 和 优化器。

损失函数

损失函数 是神经网络用来将其预测输出和真值进行比较的。它对每个观察返回一个数值,数值越大,则网络对此观察的表现越差。

Keras 提供了很多可供选择的内置损失函数,你也可以构建自己的损失函数。三个最常见的损失函数是 均方误差损失 (mean square error),类别互熵损失(categorical cross-entropy),以及 二类互熵损失(binary cross-entropy)。理解什么时候使用哪种损失函数比较重要。

如果你的网络想要解决回归问题(也即,输出是连续的),那么你可能需要使用均方误差损失函数,它是真值 y i y_i yi 和 预测值 P i P_i Pi 的均方差,其中平均值是在所有 n n n 个输出单元上求取的:
M S E = 1 n ∑ i = 1 n    ( y i − p i ) 2 MSE = \frac{1}{n} \sum_{i=1}^n \;(y_i - p_i)^2 MSE=n1i=1n(yipi)2

如果你试图解决一个多类分类问题,其中每个观察属于一个类别,则类别互熵损失就是恰当的选择。它的定义如下:
C C E = − ∑ i = 1 n y i l o g ( p i ) CCE = -\sum_{i=1}^{n}y_i log (p_i) CCE=i=1nyilog(pi)

最后,如果你试图解决一个二类分类问题,或者一个多标签问题(每个观察可以同时属于多个类别),你应该用二类互熵损失:
B C E = − 1 n ∑ i = 1 n ( y i l o g ( p i ) + ( 1 − y i ) l o g ( 1 − p i ) ) BCE = -\frac{1}{n}\sum_{i=1}^{n} (y_i log (p_i) + (1-y_i)log(1-p_i)) BCE=n1i=1n(yilog(pi)+(1yi)log(1pi))

译者注: 作者多次提到 多分类 和 多标签分类,其差别在这篇博文中讲解的比较清楚,可供参考。

优化器

优化器是一个算法,它利用损失函数的梯度来对神经网络的权重进行更新。一类经常使用的稳定优化器是Adam (Adaptive Moment Estimation)。在大多数情况下,除了学习率以外,你都无需调整Adam优化器的默认参数。学习率越大,每步训练对权重的调整越大。尽管使用比较大的学习率会让训练在开始阶段比较快,但是副作用是它会带来不稳定的训练,并会导致无法找到损失函数的全局最小。因此,学习率可能是你在训练时想要调整的。

另一个常见的优化器是 RMSProp (均方根误差扩散)。同样的,你也无需进行太多的调整。但是,最好能够仔细查阅 Keras 文档,以便仔细理解各个参数的意义。

我们将损失函数和优化器都传递给模型的 compile 方法,同时传递 metrics 参数(用以表明我们在训练时希望报告的额外度量,如准确率)。

模型训练

目前为止,我们没给模型看任何数据。我们只是设置了结构,并用损失函数和优化器编译了模型。

为了在数据上进行模型训练,我们只需要调用fit方法,如下例2-8所示。

model.fit(x_train  				# 裸的图像数据
		  , y_train				# 类别标签的独热编码
		  , batch_size = 32     # batch_size 决定了在训练过程的每步中传递多少观察
		  , epochs = 10			# epochs 决定了网络看多少次全部训练数据
		  , shuffle = True)     # 如果shuffle = True, 在每个训练步骤中,我们都会基于无放回采样进行随机抽取

这会开始训练一个神经网络,它将预测CIFAR-10数据集中一张图像的类别。训练过程如下:
首先,网络的权重用一组小的随机值进行初始化。然后网络开始一系列的训练步骤。在每个训练步骤中,一批图像被传递给网络,预测误差通过网络反向传播并进行权重的更新。batch_size决定了每个批训练步骤中使用多少图像。batch_size越大,梯度的计算越稳定,但是每个训练步骤越慢。

小贴士
在每个训练步骤中,如果我们都采用整个数据集来进行梯度计算,毫无疑问是无比耗时和计算复杂的,因此,我们一般设定batch_size为32或者256。现在,也经常推荐随着训练过程的进展增大batch_size。

这一过程一直持续,直到数据库中所有样本都已被遍历一轮。我们成为第一个epoch。接着,在第二个epoch中,数据继续以批的方式传递。整个过程持续到设定的epochs达到为止。

在训练过程中,Keras会输出过程的进展,如图2-7所示。我们可以看到训练数据库被划分为1563个批次(每批包含32张图像),并被网络遍历了10轮(也即,epochs数为10),训练的效率大概是每批次2毫秒。类别互熵值从1.8377一路下降到1.3696,对应的精度从33.69%(第一个epoch)一路攀升到51.67%(第10个epoch)。
在这里插入图片描述

模型评估

我们知道,模型在训练集上取得了51.9%的精度,但是在从未见过的数据上到底性能如何呢?

为了回答这一问题,我们可以用Keras中提供的evaluate方法,如下样例2-9所示。

model.evaluate(x_test, y_test)

图2-8展示了该方法的输出。
在这里插入图片描述
输出是我们监控度量的一个列表:类别互熵和准确率。我们可以看到,即使面对的是之前从未见过的图片,模型的准确率仍然能够保持在49%。注意,如果模型是在做随机猜测,那么它大概只能得到10%左右的准确率(因为有10类),因此,考虑到我们只用了一个非常基础的神经网络,49%是一个好的结果。

我们可以用 predict 方法在测试集上看到一些预测,如下示例2-10所示。

CLASSES = np.array(['airplane', 'automobile', 'bird', 'cat', 'frog'
					, 'horse', 'ship', 'struck'])
preds = model.predict(x_test) # 大小为[10000,10]的矩阵,每个观察对应10维类别向量
preds_single = CLASSES[np.argmax(preds, axis=-1)] # 使用numpy的argmax函数,这里axis=-1告诉函数最后一维坍塌,因此preds_single的形状是[10000,1]
actual_single = CLASSES[np.argmax(y_test, axis=-1)]

在样例2-11中,我们可以看到一些图像,和它们对应的label 以及 预测。如预期,大概一半是正确的。

n_to_show = 10
indices = np.random.choice(range(len(x_test)),n_to_show)

fig = plt.figure(figsize=(15,3))
fig.subplots_adjust(hspace=0.4,wspace=0.4)

for i, idx in enumerate(indices):
	img = x_test[idx]
	ax = fig.add_subplot(1,n_to_show,i+1)
	ax.axis('off')
	ax.text(0.5,-0.35,'pred = ' + str(preds_single[idx]),
	 		fontsize = 10, ha = 'center', transform=ax.transAxes)
	ax.text(0.5,-0.7,'act = ' + str(actual_single[idx]),
	 		fontsize = 10, ha = 'center', transform=ax.transAxes)
	ax.imshow(img)

图2-9随机展示了模型做出的一组预测,边上标注了真实的标签。
在这里插入图片描述
恭喜!你刚刚用 Keras构建了一个多层感知机,并用它在新数据上做出了预测。尽管这是一个有监督学习问题,当我们在未来的章节构建生成式模型时,很多核心的idea依赖源自本章(例如,损失函数,激活函数,以及对于层形状的理解),这些思想依然重要。接下来,让我们一起看看,通过进一步引入新的层,有哪些方式可以进一步提升模型。

卷积神经网络(CNN)

我们的网络目前的性能还不够好的一个原因在于: 前面设计的网络中并没有任何地方考虑了图像本身的空间结构。实际上,我们的第一步就是把图像拉平为单一向量,以便于我们可以将其传递给第一个Dense层。要实现这点,我们需要使用一个卷积层。

卷积层

首先,我们要理解在深度学习的语境下卷积是什么。
图2-10展示了一幅灰度图像两个不同的 3 x 3 x 1 3 x 3 x 1 3x3x1 小块,分别与一个 3 x 3 x 1 3 x 3 x 1 3x3x1 滤波器(或者称作 核) 进行卷积。卷积的操作是将滤波器与图像的小块进行逐像素相乘,并把乘积加起来。如果图像小块跟滤波器匹配的很好,那么卷积的输出会正的多。相反,如果图像小块与滤波器正好相反,那么卷积的输出负的更多。顶部的示例与滤波器谐振的更好,因此产生了大的正向输出,底下的示例与滤波器无谐振,因为产生的值接近于0。

在这里插入图片描述
如果我们在整张图像上自左到右移动滤波器,根据卷积输出,我们将得到一个新的矩阵,它能根据滤波器的数值检出输入的某个特征。例如,图2-11展示了两个不同的滤波器,分布侧重水平和垂直边缘。
在这里插入图片描述

运行本例代码
你可以根据以下路径 (notebooks/02_deeplearning/02_cnn/convolutions.ipynb),在jupyter notebook中手工运行卷积过程代码。

一个卷积层知识滤波器的聚合,滤波器中储存的数值是网络训练过程中学习到的权重。最开始,一切权重都是随机的,但是逐渐的,滤波器开始将各自的权重调整,以拾取图像中有趣的特征,例如边缘,或者特定的颜色组合。

在Keras中,Conv2D层对一个空间维度为2的输入张量(例如图像)应用卷积。例如,下面样例2-12中的代码构建了一个包含2个滤波器的卷积层,与图2-11匹配。

from tensorflow.keras import layers

input_layer = layers.Input(shape=(64,64,1))
conv_layer_1 = layers.Conv2D(
				filters = 2,
				kernel_size = (3,3),
				strides = 1,
				padding = 'same'
				)(input_layer)

接下来,让我们进一步深入Conv2D—进一步了解 strides 和 padding的细节。

Stride

strides 参数是卷积层用以在输入上移动滤波器的步长。增长步长意味着减少输出张量的尺寸。例如,当strides=2,那么输出张量的高和宽将只有输入张量的一半。这一操作有利于在张量通过网络的过程中减少张量的空间尺寸,同时增加通道的数目。

Padding

输入参数 padding = ”same” 将输入数据的边缘按0填充,以使得该层的输出尺寸恰好与输入尺寸在strides = 1时相同。

图2-12 给出了一个 3x3 的核,我们将其输入到5x5的卷积图像,其中 padding = “same”,strides = 1。那么卷积层的输出将是5x5,因为padding允许核沿着图像边缘扩展,因此该核在两个方向维度上都计算5次。不使用padding的话,kernel在每个方向上都只适配3次,那么输出尺度只有3x3。
在这里插入图片描述
将padding设置为“same”是一个很好的方式来保证你可以在网络有很多卷积层的情况下仍能很容易追踪张量的尺寸。在卷积层 padding = “same” 情况下,卷积网络的输出是:
( i n p u t h e i g h t s t r i d e , i n p u t w i d t h s t r i d e , f i l t e r s ) (\frac{input height}{stride}, \frac{input_width}{stride}, filters) (strideinputheight,strideinputwidth,filters)

卷积层堆叠

Conv2D层的输出是另一个4维张量,形状为(batch_size,height,width,filters),因此,我们可以堆叠Conv2D层来增加网络的深度,并使得整个网络更强大。为了展示这一点,让我们设想一下,我们将Conv2D层应用到CIFAR-10数据库,并希望以此预测一幅给定图像的标签。注意,这次我们输入的通道不是1(灰度)而是3 (RGB三通道)。

from tensorflow.keras import layers, models

input_layer = layers.Input(shape=(32,32,3))
conv_layer_1 = layers.Conv2D(
				filters = 10,
				kernel_size = (4,4),
				strides = 2,
				padding = 'same'
				)(input_layer) # (16,16,10)
conv_layer_2 = layers.Conv2D(
				filters = 20,
				kernel_size = (3,3),
				strides = 2,
				padding = 'same'
				)(conv_layer_1) # (8,8,20)
flatten_layer = layers.Flatten()(conv_layer_2)  # 1280
output_layer = layers.Dense(units=10, activation = 'softmax')(flatten_layer)
model = models.Model(input_layer, output_layer) # 10

这一代码对应下图2-13的框图。
在这里插入图片描述
注意,我们在彩色图像上工作,第一个卷积层的每个滤波器其深度是3 而不是 1 (也即,每个滤波器的形状为 4x4x3,而非 4x4x1)。这是为了匹配输入的RGB三通道。相同的思想同样在第二个卷积层存在,即它的深度是10,以匹配第一个卷积层的10通道输出。

小贴士
一般的,一层滤波器的深度总等于前一层输出的通道数。

模型审视

随着数据在网络中从一个卷积层流动到另一个,张量的形状如何发生变化这一点很重要。我们可以使用 model.summary() 方法来检查张量的形状(表2-2)。

层(类型)输出形状参数数目
InputLayer(None, 32,32,3)0
Conv2D(None, 16,16,10)490
Conv2D(None, 8, 8, 20)1820
Flatten(None, 1280)0
Dense(None, 10)12810
所有参数15120
可训练参数15120
非可训练参数0

让我们跟着网络流逐层逐层的看,同时注意对应的张量形状:

  1. 输入形状是(None, 32, 32, 3) — Keras使用 None来表示我们可以同时给网络传入任何数目的图像。因为网络是使用张量代数进行计算,因此我们无需逐个逐个的向网络中传图,而是可以批量的在一个batch中一起传递。
  2. 第一个卷积层中10个滤波器的形状为4x4x3,这是因为我们将每个滤波器的高和宽设定为4 (kernel_size=(4,4)), 而前一层有三个通道(红、绿、蓝)。因此,这一层中参数(或权重)为(4x4x3+1)x10 = 490,其中+1是为每一个滤波器引入一个偏置项。每个滤波器的输出都是滤波器权重与图像中4x4x3小块的像素乘。因为strides=2,padding=“same”,则输出的宽和高都将减半到16,因为一共有10个滤波器,则第一层的输出是一批形状为[16,16,10]的张量。
  3. 在第二个卷积层中,我们选择了 3x3 的滤波器,它们的深度这个时候是10,以匹配前一层输出的通道数。因为本层有20个滤波器,因此总共的参数(权重)为 (3x3x10+1) x 20 = 1820。同样的,我们这里设定 strides = 2,padding = “same”,则输出的高和宽都将减半。这意味着输出的形状为 (None, 8, 8 ,20)。
  4. 我们接下来用Keras中的Flatten层将张量拉直。这产生了一组 8x8x20 = 1280 的单元。注意在Flatten层中并无参数需要学习,因为这一算子仅仅是张量的结构重排。
  5. 最后,我们把这些单元与一个10单元的Dense层进行连接,激活函数设为softmax,代表10类分类任务重每一类的概率。这产生了额外的 (1280+1)x 10 = 12810 参数(权重)。

这个示例展示了我们如何把卷积层链接起来以创造卷积神经网络。在我们查看其与密集链接网络的准确率对比时,我们将检查两个技术,以进一步增强技术:批归一化 和 dropout。

批归一化 (batch normalization)

训练神经网络时,一个常见的问题是保证网络的权重始终在合理的范围区间—如果权重开始变的过大,它意味着网络正遭受 梯度爆炸问题。随着误差在网络中反传,开始几层的梯度计算有时候会变得异常大,从而导致权重的波动。

警告
如果损失函数开始返回 NaN,很可能你的权重已经过大,以至于产生了溢出错误。

上面的情况不一定在你开始训练时马上发生。有时候很可能训练开始几个小时候才开始返回NaN。这种情况真是令人烦恼。为了阻止这种情况发生,我们需要理解梯度爆炸问题的根源。

Covariate shift

将网络的输入数据进行放缩,其中一个理由是保证在训练开始的最初几次迭代中有个稳定的开始。因为网络的权重是随机初始化的,未放缩的输入有可能产生巨大的响应值,直接导致梯度爆炸。例如,我们并不经常直接把[0,255]的像素值直接传递给输入层,经常会将其放缩到[-1,1]。

因为输入是放缩的,因此,我们可以期待所有未来层的激活也必然是相对放缩良好的。刚开始,这一点可能成立,但是随着网络训练的进行,权重越来越偏离其初始值,这一假设可能被打破。这种现象称作 covariate shift。

covariate shift类比
假设你在搬动一大堆书,这时你遇到了风。你把书向着逆风的方向移动以补偿,但是随着你这么做,一些书会偏移,因此整个书塔将比之前更不稳定。刚开始,这没什么,但是随着风每次的冲击,书堆越来越不稳定,直到这些书偏移过大导致书堆坍塌。这就是covariate shift
将之与神经网络联系起来,每层就像堆中的一本书。要维持稳定,当网络更新权重时,每层隐性的假设该层输入的分布在迭代中是近似一致的。但是,因为事实上并没有任何东西可以阻止激活分布从某个分布中显著偏移,这有时候会导致权重值飞起,整个网络坍塌。

使用batch normalization训练

Batch normalization是一种可以显著减少上述问题的技术。整个解决方案令人吃惊的简单,在训练中,对于整个batch中的每一个输入通道,一个批处理层计算均值和标准差,并通过减去均值并除以标准差进行归一化。因此,对每个通道来讲,有两个需要学习的参数: 尺度(gamma) 和 偏移(beta)。输出就是通过gamma放缩并通过beta偏移的规一化输入。图2-14展示了整个过程。

输入: mini-batch中的 x x x值: B = x 1 ⋯ m B={x_{1\cdots m}} B=x1m;
需要学习的参数: γ \gamma γ, β \beta β
输出: { y i = B N γ , β ( x i ) y_i = BN_{\gamma, \beta}(x_i) yi=BNγ,β(xi)}
μ B ← 1 m ∑ i = 1 m x i \mu_{B} \leftarrow \frac{1}{m}\sum_{i=1}^{m} x_i μBm1i=1mxi # mini-batch均值
σ B 2 ← 1 m ∑ i = 1 m ( x i − μ B ) 2 \sigma^2_{B} \leftarrow \frac{1}{m}\sum_{i=1}^m(x_i - \mu_B)^2 σB2m1i=1m(xiμB)2 # mini-batch方差
x i ^ ← x i − μ B σ B 2 + ϵ \hat{x_i} \leftarrow \frac{x_i - \mu_B}{\sqrt{\sigma_B^2+\epsilon}} xi^σB2+ϵ xiμB # 归一化
y i ← γ x i ^ + β ≡ B N γ , β ( x i ) y_i \leftarrow \gamma \hat{x_i} + \beta \equiv BN_{\gamma, \beta}(x_i) yiγxi^+βBNγ,β(xi)

我们可以把BN层放在dense或卷积层后面来归一化输出。

小贴士
根据我们前面的例子,本书中提到的层有点类似一小组可调的弹簧,随着时间的推移,它们在各自的位置上都不至于产生过大的偏移。

使用BN进行预测

你可能疑惑: BN层在预测时怎么工作的。当它用于预测时,我们可能只希望预测单一的观察,因此并没有batch存在用以计算均值和标准差。为了解决这个问题,在训练一个BN层时,我们也计算了各个通道上均值和标准差的滑动平均(moving average), 并把它们的值作为层的一部分存起来,以便在测试时使用。

那么,一个BN层包含多少参数呢? 对于前一层的每一通道,我们都要学习两个权重: 尺度( γ \gamma γ)和 偏移 ( β \beta β)。这些是可训练参数,均值和标准差的滑动平均也是针对每个通道进行计算,但是因为它们是从通过该层的数据得到,而不是通过反向传播训练得到,因此它们是非训练参数。合起来,对于前层的每一通道,我们都计算了四个参数,其中2个是可训练的,两个是非训练的。
在Keras中,BatchNormalization 层实现了批归一化的功能,如例2-14所示。

from tensorflow.keras import layers
layers.BatchNormalization(momentum = 0.9)

其中 momentum 参数是权重,用以计算均值和方差的滑动平均。

Dropout

在面向考试学习时,很常见的一个操作是,利用过去的试卷和例题来提升知识。一些学生尝试记住问题的答案,但往往在考试中卡壳,因为它们并没有真的理解问题。最有效的学生使用联系材料来进一步深化他们的理解,因此他们面对之前从未见过的新问题时也能正确回答。

类似的原则存在于机器学习中,任何成功的机器学习算法必须确保它可以泛化到未知数据,而不是简单的记住训练集。如果一个算法在训练集中表现良好,但是在测试集中很拉垮,我们通常称其遭遇了过拟合。为了解决这一问题,我们使用正则化技术,来保证模型在开始过拟合时会受到惩罚。

对于一个机器学习模型,有很多可以施加正则化的方式,但是对于深度学习,一个最常见的方式是使用 dropout 层。这一技术由Hinton等在2012年提出,并在2014年的一个论文中展现。

Dropout层非常简单。在训练时,每个dropout层从前层中选取一个随机单元集合,并将它的值设为0,如图2-15所示。

不可思议的是,这个简单的附加操作显著的减少了过拟合,因为它强制网络不可过于依赖特定的单元组(也就是说,只是记住了训练集中的某些观察)。如果我们使用dropout层,那么网络不会过分依赖某个单元,因此,知识在整个网络中得到更均匀的扩散。
在这里插入图片描述
这将使得模型更好的泛化到未知数据上,因为网络已经被训练成即使在不熟悉的条件下(例如那些由随机单元丢弃导致的)依然能产生准确的输出。在dropout层中,没什么权重是需要学习的,因为丢弃的单元是随机决定的,在预测时,dropout层不再丢弃任何单元,从而使得整个网络都可以被用来进行预测。

DropOut类比
回到我们的类比中,有点象一个数学学生,在联系过去的卷子时,随机的遮挡一部分关键公式。通过这种方式,他们能够学会如何通过对关键原则的理解来解答问题,而不是总是在书本的同样位置查找公式。当进入测试时,他们将发现回答从未见过的问题时更简单,因为他们具备从训练材料泛化的能力。
Keras中,Dropout层实现了这一功能,其中rate参数为从前序层丢弃单元的比例,如下例2-15所示。
from tensorflow.keras import layers
layers.Dropout(rate=0.25)

Dropout层通常接在dense层后面(当然你也可以接在卷积层后面),因为dense层权重数量过多,通常是最容易过拟合的。

小贴士
BN也可以防止过拟合,因此很多现代深度学习架构压根就不用dropout了,仅仅只依赖BN做正则化。对大部分深度学习原则来说,并没有任何情况下都适用的黄金法则—唯一确定最好架构的办法就是在一小撮数据上进行测试。

构建CNN

现在你已经学过了Keras上的三种新层:Conv2D,BatchNormalization,Dropout。让我们把这些组合成一个CNN模型,并看看它在CIFAR-10数据集上表现如何吧!

运行示例代码
你可以在Jupyter notebook上运行本例代码,其路径为“notebooks/02_deeplearning/02_cnn/cnn.ipynb”

我们将要测试的模型架构如下样例2-16所示。

# 样例2-16. 使用Keras构建CNN模型之代码
from tensorflow.keras import layers, models

input_layer = layers.Input((32,32,3))

x = layers.Conv2D(filters = 32, kernel_size = 3,
			      strides = 1, padding = 'same')(input_layer)
x = layers.BatchNormalization()(x)
x = layers.LeakyReLU()(x)

x = layers.Conv2D(filters = 32, kernel_size = 3,
			      strides = 2, padding = 'same')(x)
x = layers.BatchNormalization()(x)
x = layers.LeakyReLU()(x)

x = layers.Conv2D(filters = 64, kernel_size = 3,
			      strides = 1, padding = 'same')(x)
x = layers.BatchNormalization()(x)
x = layers.LeakyReLU()(x)

x = layers.Conv2D(filters = 64, kernel_size = 3,
			      strides = 2, padding = 'same')(x)
x = layers.BatchNormalization()(x)
x = layers.LeakyReLU()(x)

x = layers.Flatten()(x)

x = layers.Dense(128)(x)
x = layers.BatchNormalization()(x)
x = layers.LeakyReLU()(x)
x = layers.Dropout(rate = 0.5)(x)

output_layer = layers.Dense(10, activation = 'softmax')(x)

model = models.Model(input_layer, output_layer)

我们使用了四个堆叠的CNN层,每个CNN后面紧接BatchNormalization和LeakyReLU层。在把输出张量拉直后,我们把数据输入一个大小为128的Dense层,同样的,该Dense层也紧接BatchNormalization和LeakyReLU层。然后我们再接了一个Dropout层进行正则化,网络的最后以大小为10的Dense层进行输出。

小贴士
使用BN和激活层的先后顺序是一种偏好。通常BN层放在激活层的签名,但是一些成功的架构也会反过来用。如果你选择BN放在激活之前,你可以用一个缩写进行简单的记忆: BAD (batch normalization, activation, dropout)。

模型在表格2-3中进行了总结。

层(类型)输出形状参数数目
InputLayer(None, 32, 32, 3)0
Conv2D(None, 32, 32, 32)896
BatchNormalization(None, 32, 32, 32)128
LeakyReLU(None,32, 32, 32)0
Conv2D(None, 16, 16, 32)9248
BatchNormalization(None, 16, 16, 32)128
LeakyReLU(None, 16, 16, 32)0
Conv2D(None, 16, 16, 64)18496
BatchNormalization(None, 16, 16, 64)256
LeakyReLU(None,16, 16, 64)0
Conv2D(None, 8, 8, 64)36928
BatchNormalization(None, 8, 8, 64)256
LeakyReLU(None,8, 8, 64)0
Flatten(None, 4096)0
Dense(None, 128)524416
BatchNormalization(None, 128)512
LeakyReLU(None,128)0
Dropout(None,128)0
Dense(None,10)1290
所有参数592554
可训练参数591914
非可训练参数640
小贴士
在进一步之前,确保你能够计算每层输出形状和参数数目。这是一个很好的练习,可以帮助你深刻理解每层是怎么构建的,以及它是如何和前面的层链接的。别忘记Conv2D和Dense层中的偏置权重。

训练并评估CNN

我们编译并训练模型,方式与之前相同。接着,我们调用 evaluate 方法来确定其在测试集上的准确率(图2-16)。
在这里插入图片描述
可以看到,模型现在可以取得72.76%的准确率,而之前的准确率仅为49%。好太多!图2-17给出了我们新模型的一些预测例。
在这里插入图片描述
性能提升是通过改变模型架构,引入卷积,BN及dropout层得到。注意,在新模型中,参数两实际上比原始模型要小,即使层数大很多。这个实验展示了模型设计的重要性,以及如何利用不同的层类型来提升性能。当构建生成式模型时,理解你模型内部的工作机理就更为重要,因为在整个网络中,事实上是中间层捕获了我们最感兴趣的高层次特征。

小结

本章介绍了深度学习的核心概念,对于你开始构建深度生成式模型比较重要。一开始,我们利用Keras构建了一个多层感知机,并在CIFAR-10数据集上训练模型,以预测给定图像的类别。接着,我们在这个架构上做了改进,进一步引入卷积层,BN层 及 dropout层,并构建了卷积神经网络。

本章很重要的一个点在于: 深度神经网络在设计上是完全灵活的,在模型架构的层次上并无定规。事实上,存在一些引导和好的实践,但是你仍然可以在层和顺序上自由实验。不要把自己局限在本书(或任何地方)读到的架构上。就像拿到一大堆积木的孩子那样,唯一限制你所设计神经网络的,恰恰是你自己的想象!

在下一章中,你可以看到,我们如果使用这些构建单元来生成图像。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值