TensorFlow 2 和 Keras 高级深度学习:1~5(2)https://developer.aliyun.com/article/1426945
ResNet 在n
的各种值上的表现显示在“表 2.2.2”中。
层 | n |
CIFAR10 的准确率百分比(原始论文) | CIFAR10 的准确率百分比(本书) |
ResNet20 | 3 | 91.25 | 92.16 |
ResNet32 | 5 | 92.49 | 92.46 |
ResNet44 | 7 | 92.83 | 92.50 |
ResNet56 | 9 | 93.03 | 92.71 |
ResNet110 | 18 | 93.57 | 92.65 |
表 2.2.2:针对不同的 n 值,使用 CIFAR10 验证的 ResNet 架构
与 ResNet 的原始实现有一些细微的差异。 特别是,我们不使用 SGD,而是使用 Adam。 这是因为 ResNet 更容易与 Adam 融合。 我们还将使用学习率调度器lr_schedule()
,以便将lr
的减少量从默认的1e-3
缩短为 80、120、160 和 180 个周期。 在训练期间的每个周期之后,都会将lr_schedule()
函数作为回调变量的一部分进行调用。
每当验证准确率方面取得进展时,另一个回调将保存检查点。 训练深层网络时,保存模型或权重检查点是一个好习惯。 这是因为训练深度网络需要大量时间。
当您想使用网络时,您只需要做的就是重新加载检查点,然后恢复经过训练的模型。 这可以通过调用tf.keras load_model()
来完成。 包含lr_reducer()
函数。 如果指标在排定的减少之前已稳定在上,则如果在patience = 5
周期之后验证损失没有改善,则此回调将以参数中提供的某个因子来降低学习率。
调用model.fit()
方法时,会提供回调变量。 与原始论文相似,tf.keras
实现使用数据扩充ImageDataGenerator()
来提供其他训练数据作为正则化方案的一部分。 随着训练数据数量的增加,概括性将会提高。
例如,简单的数据扩充就是翻转一条狗的照片,如图“图 2.2.6”(horizontal_flip = True
)所示。 如果它是狗的图像,则翻转的图像仍然是狗的图像。 您还可以执行其他变换,例如缩放,旋转,变白等等,并且标签将保持不变:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8GeAiXei-1681704179655)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_02_09.png)]
图 2.2.6:一个简单的数据扩充就是翻转原始图像
准确复制原始论文的实现通常很困难。 在本书中,我们使用了不同的优化器和数据扩充。 这可能会导致本书中所实现的tf.keras
ResNet 和原始模型中的表现略有不同。
在 ResNet [4]的第二篇论文发布之后,本节中介绍的原始模型为,称为 ResNet v1。 改进的 ResNet 通常称为 ResNet v2,我们将在下一部分讨论。
3. ResNet v2
ResNet v2 的改进主要体现在残块中各层的排列中,如图“图 2.3.1”所示。
ResNet v2 的主要变化是:
- 使用
1 x 1 – 3 x 3 – 1 × 1
的栈BN-ReLU-Conv2D
- 批量标准化和 ReLU 激活先于二维卷积
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hChC6wso-1681704179655)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_02_10.png)]
图 2.3.1:ResNet v1 和 ResNet v2 之间的剩余块比较
ResNet v2 也以与resnet-cifar10-2.2.1.py
相同的代码实现,如“列表 2.2.1”所示:
“列表 2.2.1”:resnet-cifar10-2.2.1.py
def resnet_v2(input_shape, depth, num_classes=10): """ResNet Version 2 Model builder [b]
Stacks of (1 x 1)-(3 x 3)-(1 x 1) BN-ReLU-Conv2D or also known as bottleneck layer. First shortcut connection per layer is 1 x 1 Conv2D. Second and onwards shortcut connection is identity. At the beginning of each stage, the feature map size is halved (downsampled) by a convolutional layer with strides=2, while the number of filter maps is doubled. Within each stage, the layers have the same number filters and the same filter map sizes. Features maps sizes: conv1 : 32x32, 16 stage 0: 32x32, 64 stage 1: 16x16, 128 stage 2: 8x8, 256
Arguments: input_shape (tensor): shape of input image tensor depth (int): number of core convolutional layers num_classes (int): number of classes (CIFAR10 has 10)
Returns: model (Model): Keras model instance """ if (depth - 2) % 9 != 0: raise ValueError('depth should be 9n+2 (eg 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 # first layer and first stage if res_block == 0: activation = None batch_normalization = False else: num_filters_out = num_filters_in * 2 # first layer but not first stage if res_block == 0: # downsample strides = 2
# 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
和version = 2
:
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 + 2
if version == 2: model = resnet_v2(input_shape=input_shape, depth=depth) else: model = resnet_v1(input_shape=input_shape, depth=depth)
ResNet v2 的准确率显示在下面的“表 2.3.1”中:
层 | n |
CIFAR10 的准确率百分比(原始论文) | CIFAR10 的准确率百分比(本书) |
ResNet56 | 9 | 不适用 | 93.01 |
ResNet110 | 18 | 93.63 | 93.15 |
表 2.3.1:在 CIFAR10 数据集上验证的 ResNet v2 架构
在 Keras 应用包中,已实现某些 ResNet v1 和 v2 模型(例如:50、101、152)。 这些是替代的实现方式,其中预训练的权重不清楚,可以轻松地重新用于迁移学习。 本书中使用的模型在层数方面提供了灵活性。
我们已经完成了对最常用的深度神经网络之一 ResNet v1 和 v2 的讨论。 在以下部分中,将介绍另一种流行的深度神经网络架构 DenseNet。
4. 紧密连接的卷积网络(DenseNet)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1UjcjqS7-1681704179655)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_02_11.png)]
图 2.4.1:DenseNet 中的一个 4 层Dense
块,每层的输入均由所有先前的特征映射组成。
DenseNet 使用另一种方法攻击梯度消失的问题。 代替使用快捷方式连接,所有先前的特征映射都将成为下一层的输入。 上图显示了一个Dense
块中密集互连的示例。
为简单起见,在此图中,我们仅显示四层。 注意,层l
的输入是所有先前特征映射的连接。 如果用操作H
表示BN-ReLU-Conv2D
(x
),则层l
的输出为:
x[l] = H(x[0], x[1], x[2], x[l-1])
(公式 2.4.1)
Conv2D
使用大小为 3 的核。每层生成的特征映射的数量称为增长率k
。 通常,在 Huang 等人的论文“密集连接卷积网络”中,也使用k = 12
,但是k = 24
[5]。 因此,如果特征映射x[0]
的数量为k[0]
,则“图 2.4.1”中,4 层Dense
块的末尾的特征映射总数为4 x k + k[0]
。
DenseNet 建议在Dense
块之前加上BN-ReLU-Conv2D
,以及许多是增长率两倍的特征映射k[0]
= 2 xk
。 在Dense
块的末尾,特征映射的总数将为4 x 12 + 2 x 12 = 72
。
在输出层,DenseNet 建议我们在具有softmax
层的Dense()
之前执行平均池化。 如果未使用数据扩充,则必须在Dense
块Conv2D
之后跟随一个丢弃层。
随着网络的深入,将出现两个新问题。 首先,由于每一层都贡献了k
特征映射,因此l
层的输入数量为(l – 1) x k + k[0]
。 特征映射可以在深层中快速增长,从而减慢了计算速度。 例如,对于 101 层网络,对于k = 12
,这将是1200 + 24 = 1224
。
其次,类似于 ResNet,随着网络的不断深入,特征映射的大小将减小,从而增加核的接收域大小。 如果 DenseNet 在合并操作中使用连接,则必须协调大小上的差异。
为了防止特征映射的数量增加到计算效率低的程度,DenseNet 引入了Bottleneck
层,如图“图 2.4.2”所示。 这个想法是,在每次连接之后,现在应用1 x 1
卷积,其过滤器大小等于4k
。 这种降维技术阻止了Conv2D(3)
处理的特征映射的数量快速增加。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CsKcAOS6-1681704179655)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_02_12.png)]
图 2.4.2:DenseNet 的 Dense 块中的一层,带有和不带有瓶颈层 BN-ReLU-Conv2D(1)。 为了清楚起见,我们将核大小作为 Conv2D 的参数。
然后Bottleneck
层将 DenseNet 层修改为BN-ReLU-Conv2D(1)-BN- ReLU-Conv2D(3)
,而不仅仅是BN-ReLU-Conv2D(3)
。 为了清楚起见,我们将核大小作为Conv2D
的参数。 在瓶颈层,每个Conv2D(3)
仅处理 4 个k
特征映射,而不是(l – 1) x k + k[0]
的,对于层l
。 例如,对于 101 层网络,最后一个Conv2D(3)
的输入仍然是k = 12
而不是先前计算的 1224 的 48 个特征映射。
为了解决特征映射大小不匹配的问题,DenseNet 将深度网络划分为多个 Dense 块,这些块通过过渡层连接在一起,如图“图 2.4.3”所示。 在每个Dense
块中,特征映射的大小(即宽度和高度)将保持不变。
过渡层的作用是在两个Dense
块之间从一个特征映射大小过渡到较小的特征映射大小。 尺寸通常减少一半。 这是通过平均池化层完成的。 例如,默认值为pool_size=2
的AveragePooling2D
会将大小从(64, 64, 256)
减小为(32, 32, 256)
。 过渡层的输入是前一个Dense
块中最后一个连接层的输出。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xY40DkQm-1681704179656)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_02_13.png)]
图 2.4.3:两个密集块之间的过渡层
但是,在将特征映射传递到平均池之前,使用Conv2D(1)
将其数量减少某个压缩因子0 < θ < 1
。DenseNet 在实验中使用θ = 0.5
。 例如,如果先前Dense
块的最后连接的输出是(64, 64, 512)
,则在Conv2D(1)
之后,特征映射的新尺寸将是(64, 64, 256)
。 当压缩和降维放在一起时,过渡层由BN-Conv2D(1)-AveragePooling2D
层组成。 实际上,批量归一化在卷积层之前。
现在,我们已经涵盖了 DenseNet 的重要概念。 接下来,我们将为tf.keras
中的 CIFAR10 数据集构建并验证 DenseNet-BC。
为 CIFAR10 构建 100 层 DenseNet-BC
现在,我们将要为 CIFAR10 数据集构建一个具有 100 层的 DenseNet-BC(瓶颈压缩), 我们在上面讨论过。
“表 2.4.1”显示了模型配置,而“图 2.4.4”显示了模型架构。 清单为我们展示了具有 100 层的 DenseNet-BC 的部分 Keras 实现。 我们需要注意的是,我们使用RMSprop
,因为在使用 DenseNet 时,它的收敛性优于 SGD 或 Adam。
层 | 输出大小 | DenseNet-100 BC |
卷积 | 32 x 32 |
3 x 3 Conv2D |
密集块(1) | 32 x 32 |
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GSLxggPQ-1681704179656)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_02_006.png)] |
过渡层(1) | 32 x 32 |
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aSnE423L-1681704179656)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_02_007.png)] |
16 x 16 |
||
密集块(2) | 16 x 16 |
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pCa5xcKQ-1681704179656)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_02_008.png)] |
过渡层(2) | 16 x 16 |
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NXVNyRdC-1681704179656)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_02_009.png)] |
8 x 8 |
||
密集块(3) | 8 x 8 |
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BFQ9QvPB-1681704179657)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_02_006.png)] |
平均池化 | 1 x 1 |
8 x 8 AveragePooling2D |
分类层 | Flatten-Dense(10)-softmax |
表 2.4.1:100 层的 DenseNet-BC 用于 CIFAR10 分类
将从配置移至架构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X3uNIYkK-1681704179657)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_02_14.png)]
图 2.4.4:用于 CIFAR10 分类的 100 个层的 DenseNet-BC 模型架构
下面“列表 2.4.1”是具有 100 层的 DenseNet-BC 的部分 Keras 实现,如“表 2.4.1”所示。
“列表 2.4.1”:densenet-cifar10-2.4.1.py
# 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()
训练 DenseNet 的tf.keras
实现 200 个周期,可以达到 93.74% 的准确率,而本文中报道的是 95.49%。 使用数据扩充。 我们在 ResNet v1 / v2 中为 DenseNet 使用了相同的回调函数。
对于更深的层,必须使用 Python 代码上的表来更改growth_rate
和depth
变量。 但是,如本文所述,以深度 190 或 250 训练网络将需要大量时间。 为了给我们一个训练时间的想法,每个周期在 1060Ti GPU 上运行大约一个小时。 与 ResNet 相似,Keras 应用包具有针对 DenseNet 121 及更高版本的预训练模型。
DenseNet 完成了我们对深度神经网络的讨论。 与 ResNet 一起,这两个网络已成为许多下游任务中不可或缺的特征提取器网络。
5. 总结
在本章中,我们介绍了函数式 API 作为使用tf.keras
构建复杂的深度神经网络模型的高级方法。 我们还演示了如何使用函数式 API 来构建多输入单输出 Y 网络。 与单分支 CNN 网络相比,该网络具有更高的准确率。 在本书的其余部分中,我们将发现在构建更复杂和更高级的模型时必不可少的函数式 API。 例如,在下一章中,函数式 API 将使我们能够构建模块化编码器,解码器和自编码器。
我们还花费了大量时间探索两个重要的深度网络 ResNet 和 DenseNet。 这两个网络不仅用于分类,而且还用于其他领域,例如分段,检测,跟踪,生成和视觉语义理解。 在“第 11 章”,“对象检测”和“第 12 章”,“语义分割”中,我们将使用 ResNet 进行对象检测和分割。 我们需要记住,与仅仅遵循原始实现相比,更仔细地了解 ResNet 和 DenseNet 中的模型设计决策至关重要。 这样,我们就可以将 ResNet 和 DenseNet 的关键概念用于我们的目的。
6. 参考
Kaiming He et al. Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification. Proceedings of the IEEE international conference on computer vision, 2015 (https://www.cv-foundation.org/openaccess/content_iccv_2015/papers/He_Delving_Deep_into_ICCV_2015_paper.pdfspm=5176.100239.blogcont55892.28.pm8zm1&file=He_Delving_Deep_into_ICCV_2015_paper.pdf).
Kaiming He et al. Deep Residual Learning for Image Recognition. Proceedings of the IEEE conference on computer vision and pattern recognition, 2016a (http://openaccess.thecvf.com/content_cvpr_2016/papers/He_Deep_Residual_Learning_CVPR_2016_paper.pdf).
Karen Simonyan and Andrew Zisserman. Very Deep Convolutional Networks for Large-Scale Image Recognition. ICLR, 2015 (https://arxiv.org/pdf/1409.1556/).
Kaiming He et al. Identity Mappings in Deep Residual Networks. European Conference on Computer Vision. Springer International Publishing, 2016b (https://arxiv.org/pdf/1603.05027.pdf).
Gao Huang et al. Densely Connected Convolutional Networks. Proceedings of the IEEE conference on computer vision and pattern recognition, 2017 (http://openaccess.thecvf.com/content_cvpr_2017/papers/Huang_Densely_Connected_Convolutional_CVPR_2017_paper.pdf).
Saining Xie et al. Aggregated Residual Transformations for Deep Neural Networks. Computer Vision and Pattern Recognition (CVPR), 2017 IEEE Conference on. IEEE, 2017 (http://openaccess.thecvf.com/content_cvpr_2017/papers/Xie_Aggregated_Residual_Transformations_CVPR_2017_paper.pdf).
Zagoruyko, Sergey, and Nikos Komodakis. "Wide residual networks." arXiv preprint arXiv:1605.07146 (2016).
三、自编码器
在上一章“第 2 章”,“深度神经网络”中,我们介绍了深度神经网络的概念。 现在,我们将继续研究自编码器,它是一种神经网络架构,试图找到给定输入数据的压缩表示形式。
与前面的章节相似,输入数据可以采用多种形式,包括语音,文本,图像或视频。 自编码器将尝试查找表示形式或一段代码,以便对输入数据执行有用的转换。 例如,当对自编码器进行降噪处理时,神经网络将尝试找到可用于将噪声数据转换为干净数据的代码。 嘈杂的数据可以是带有静态噪声的录音形式,然后将其转换为清晰的声音。 自编码器将自动从数据中自动学习代码,而无需人工标记。 这样,自编码器可以在无监督学习算法下分类为。
在本书的后续章节中,我们将研究生成对抗网络(GAN)和变分自编码器(VAE) 也是无监督学习算法的代表形式。 这与我们在前几章中讨论过的监督学习算法相反,后者需要人工标注。
总之,本章介绍:
- 自编码器的原理
- 如何使用
tf.keras
实现自编码器 - 去噪和着色自编码器的实际应用
让我们从了解自编码器是什么以及自编码器的原理开始。
1. 自编码器的原理
自编码器以最简单的形式通过尝试将输入复制到输出中来学习表示形式或代码。 但是,使用自编码器并不像将输入复制到输出那样简单。 否则,神经网络将无法发现输入分布中的隐藏结构。
自编码器将输入分布编码为低维张量,通常采用向量形式。 这将近似通常称为潜在表示,代码或向量的隐藏结构。 该处理构成编码部分。 然后,潜在向量将由解码器部分解码,以恢复原始输入。
由于潜向量是输入分布的低维压缩表示,因此应该期望解码器恢复的输出只能近似输入。 输入和输出之间的差异可以通过损失函数来衡量。
但是为什么我们要使用自编码器? 简而言之,自编码器在原始形式或更复杂的神经网络的一部分中都有实际应用。
它们是了解深度学习的高级主题的关键工具,因为它们为我们提供了适合密度估计的低维数据表示。 此外,可以有效地对其进行处理以对输入数据执行结构化操作。 常见的操作包括去噪,着色,特征级算术,检测,跟踪和分割,仅举几例。
在本节中,我们将介绍自编码器的原理。 我们将使用前几章介绍的带有 MNIST 数据集的自编码器。
首先,我们需要意识到自编码器具有两个运算符,它们是:
- 编码器:这会将输入
x
转换为低维潜向量z = f(x)
。 由于潜向量是低维的,编码器被迫仅学习输入数据的最重要特征。 例如,在 MNIST 数字的情况下,要学习的重要特征可能包括书写风格,倾斜角度,笔触圆度,厚度等。 从本质上讲,这些是代表数字 0 至 9 所需的最重要的信息位。 - 解码器:这尝试从潜在向量
g(z) = x
中恢复输入。
尽管潜向量的维数较小,但它的大小足以使解码器恢复输入数据。
解码器的目标是使x_tilde
尽可能接近x
。 通常,编码器和解码器都是非线性函数。z
的尺寸是可以表示的重要特征数量的度量。 该维数通常比输入维数小得多,以提高效率,并为了限制潜在代码仅学习输入分布的最显着属性[1]。
当潜码的维数明显大于x
时,自编码器倾向于记忆输入。
合适的损失函数L(x, x_tilde)
衡量输入x
与输出(即)恢复后的输入x_tilde
的相异程度。 如下式所示,均方误差(MSE)是此类损失函数的一个示例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iPCf6x6d-1681704179657)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_03_006.png)] (Equation 3.1.1)
在此示例中,m
是输出尺寸(例如,在 MNIST 中,m = width × height × channels = 28 × 28 × 1 = 784
)。x[i]
和x_tilde[i]
分别是x
和x_tilde
的元素。 由于损失函数是输入和输出之间差异的量度,因此我们可以使用替代的重建损失函数,例如二进制交叉熵或结构相似性指数(SSIM)。
与其他神经网络类似,自编码器会在训练过程中尝试使此误差或损失函数尽可能小。“图 3.1.1”显示了一个自编码器。 编码器是将输入x
压缩为低维潜向量z
的函数。 该潜向量代表输入分布的重要特征。 然后,解码器尝试以x_tilde
的形式从潜向量中恢复原始输入。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-99WX1WYh-1681704179657)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_03_01.png)]
图 3.1.1:自编码器的框图
为了将自编码器置于上下文中,x
可以是尺寸为28×28×1 = 784
的 MNIST 数字。编码器将输入转换为低维的z
,可以是 16 维潜在向量。 解码器将尝试从z
中以x_tilde
的形式恢复输入。
在视觉上,每个 MNIST 数字x
看起来都类似于x_tilde
。“图 3.1.2”向我们演示了此自编码过程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9D7aI91G-1681704179657)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_03_02.png)]
图 3.1.2:带有 MNIST 数字输入和输出的自编码器。 潜在向量为 16 角
我们可以看到,虽然解码后的数字 7 并不完全相同,但仍然足够接近。
由于编码器和解码器都是非线性函数,因此我们可以使用神经网络来实现两者。 例如,在 MNIST 数据集中,自编码器可以由 MLP 或 CNN 实现。 通过最小化通过反向传播的损失函数,可以训练自编码器。 与其他神经网络类似,反向传播的要求是损失函数必须是可微的。
如果将输入视为分布,则可以将编码器解释为分布的编码器,p(z | x)
,将解码器解释为分布的解码器p(x | z)
。 自编码器的损失函数表示为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gs6soqtL-1681704179658)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_03_012.png)] (Equation 3.1.2)
损失函数只是意味着我们要在给定潜在向量分布的情况下最大程度地恢复输入分布的机会。 如果假设解码器的输出分布为为高斯,则损失函数归结为 MSE,因为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gYiFUxwp-1681704179658)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_03_013.png)] (Equation 3.1.3)
在此示例中,N(x[i]; x_tilde[i], σ²
表示平均值为x_tilde[i]
且方差为σ²
的高斯分布。 假设恒定方差。 假定解码器输出x_tilde[i]
是独立的。m
是输出尺寸。
了解自编码器背后的原理将有助于我们执行代码。 在下一节中,我们将研究如何使用tf.keras
函数式 API 来构建编码器,解码器和自编码器。
2. 使用 Keras 构建自编码器
现在,我们要使用进行一些令人兴奋的事情,使用tf.keras
库构建一个自编码器。 为了简单起见,我们将使用 MNIST 数据集作为第一组示例。 然后,自编码器将根据输入数据生成潜向量,并使用解码器恢复输入。 在该第一示例中,潜向量是 16 维。
首先,我们将通过构建编码器来实现自编码器。
“列表 3.2.1”显示了将 MNIST 数字压缩为 16 维潜在向量的编码器。 编码器是两个Conv2D
的栈。 最后阶段是具有 16 个单元的Dense
层,以生成潜向量。
“列表 3.2.1”:autoencoder-mnist-3.2.1.py
from tensorflow.keras.layers import Dense, Input from tensorflow.keras.layers import Conv2D, Flatten from tensorflow.keras.layers import Reshape, Conv2DTranspose from tensorflow.keras.models import Model from tensorflow.keras.datasets import mnist from tensorflow.keras.utils import plot_model from tensorflow.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 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, 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 function, 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()
“图 3.2.1”显示了plot_model()
生成的架构模型图,与encoder.summary()
生成的文本版本相同。 保存最后一个Conv2D
的输出形状以计算解码器输入层的尺寸,以便轻松重建 MNIST 图像:shape = K.int_shape(x)
。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uOaR150i-1681704179658)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_03_03.png)]
图 3.2.1:编码器模型由Conv2D(32) - Conv2D(64) - Dense(16)
组成,以生成低维潜向量
列表 3.2.1 中的解码器对潜在向量进行解压缩,以恢复 MNIST 数字。 解码器输入级是Dense
层,它将接受潜在向量。 单元的数量等于从编码器保存的Conv2D
输出尺寸的乘积。 这样做是为了便于我们调整Dense
层Dense
层的输出大小,以最终恢复原始 MNIST 图像尺寸。
解码器由三个Conv2DTranspose
的栈组成。 在我们的案例中,我们将使用转置的 CNN(有时称为反卷积),它是解码器中常用的。 我们可以将转置的 CNN(Conv2DTranspose
)想象成 CNN 的逆过程。
在一个简单的示例中,如果 CNN 将图像转换为特征映射,则转置的 CNN 将生成给定特征映射的图像。“图 3.2.2”显示了解码器模型:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sZ9rl7Wl-1681704179658)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_03_04.png)]
图 3.2.2:解码器模型由Dense(16) - Conv2DTranspose(64) - Conv2DTranspose(32) - Conv2DTranspose(1)
组成。 输入是经过解码以恢复原始输入的潜向量
通过将编码器和解码器连接在一起,我们可以构建自编码器。“图 3.2.3”说明了自编码器的模型图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QAeXmj6n-1681704179658)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_03_05.png)]
图 3.2.3:通过将编码器模型和解码器模型结合在一起来构建自编码器模型。 此自编码器有 178 k 个参数
编码器的张量输出也是解码器的输入,该解码器生成自编码器的输出。 在此示例中,我们将使用 MSE 损失函数和 Adam 优化器。 在训练期间,输入与输出x_train
相同。 我们应该注意,在我们的示例中,只有几层足以将验证损失在一个周期内驱动到 0.01。 对于更复杂的数据集,我们可能需要更深的编码器和解码器,以及更多的训练时间。
在对自编码器进行了一个周期的验证损失为 0.01 的训练之后,我们能够验证它是否可以对以前从未见过的 MNIST 数据进行编码和解码。“图 3.2.4”向我们展示了来自测试数据和相应解码图像的八个样本:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kbH6VN6e-1681704179659)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_03_06.png)]
图 3.2.4:根据测试数据预测自编码器。 前两行是原始输入测试数据。 最后两行是预测数据
除了图像中的轻微模糊之外,我们能够轻松识别出自编码器能够以良好的质量恢复输入。 随着我们训练更多的周期,结果将有所改善。
在这一点上,我们可能想知道:我们如何可视化空间中的潜在向量? 一种简单的可视化方法是强制自编码器使用 2 维潜在向量来学习 MNIST 数字特征。 从那里,我们可以将该潜在向量投影到二维空间上,以查看 MNIST 潜在向量的分布方式。“图 3.2.5”和“图 3.2.6”显示了 MNIST 数字的分布与潜在代码尺寸的关系。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rqmX9jBD-1681704179659)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_03_07.png)]
图 3.2.5:MNIST 数字分布与潜在代码尺寸z[0]
和z[1]
的关系。 原始照片可以在本书的 GitHub 存储库中找到。
在“图 3.2.5”中,我们可以看到特定数字的潜向量聚集在空间的某个区域上。 例如,数字 0 在左下象限中,而数字 1 在右上象限中。 这种群集在图中得到了反映。 实际上,同一图显示了导航或从潜在空间生成新数字的结果,如图“图 3.2.5”所示。
例如,从中心开始,向右上象限改变 2 维潜向量的值,这表明数字从 9 变为 1。这是可以预期的,因为从“图 3.2.5”开始,我们可以看到数字 9 群集的潜在代码值在中心附近,数字 1 群集的潜在代码值在右上象限。
对于“图 3.2.5”和“图 3.2.6”,我们仅研究了每个潜在向量维在 -4.0 和 +4.0 之间的区域:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gn0mEi5A-1681704179659)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_03_08.png)]
图 3.2.6:导航 2 维潜在向量空间时生成的数字
从“图 3.2.5”中可以看出,潜在代码分布不是连续的。 理想情况下,应该看起来像一个圆圈,其中到处都有有效值。 由于这种不连续性,因此如果解码潜伏向量,则几乎不会产生任何可识别的数字。
“图 3.2.5”和“图 3.2.6”经过 20 个训练周期后生成。 通过设置latent_dim = 2
修改了autoencoder-mnist-3.2.1.py
代码。 plot_ results()
函数将 MNIST 数字绘制为 2 维潜在向量的函数。 为了方便起见,该程序另存为autoencoder-2dim-mnist-3.2.2.py
,其部分代码显示在“列表 3.2.2”中。 其余代码实际上类似于“列表 3.2.1”,在此不再显示。
“列表 3.2.2”:autoencoder-2dim-mnist-3.2.2.py
def plot_results(models, data, batch_size=32, model_name="autoencoder_2dim"): """Plots 2-dim latent values as scatter plot of digits 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 xmin = ymin = -4 xmax = ymax = +4 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))
# axes x and y ranges axes = plt.gca() axes.set_xlim([xmin,xmax]) axes.set_ylim([ymin,ymax])
# subsample to reduce density of points on the plot z = z[0::2] y_test = y_test[0::2] plt.scatter(z[:, 0], z[:, 1], marker="") for i, digit in enumerate(y_test): axes.annotate(digit, (z[i, 0], z[i, 1])) 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(xmin, xmax, n) grid_y = np.linspace(ymin, ymax, 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()
这样就完成了和自编码器的检查。 接下来的章节将重点介绍其实际应用。 我们将从去噪自编码器开始。
3. 去噪自编码器(DAE)
现在,我们将构建具有实际应用的自编码器。 首先,让我们画一幅画,然后想象 MNIST 的数字图像被噪声破坏了,从而使人类更难以阅读。 我们能够构建一个去噪自编码器(DAE),以消除这些图像中的噪声。“图 3.3.1”向我们展示了三组 MNIST 数字。 每组的顶部行(例如,MNIST 数字 7、2、1、9、0、6、3、4 和 9)是原始图像。 中间的行显示了 DAE 的输入,这些输入是被噪声破坏的原始图像。 作为人类,我们发现很难读取损坏的 MNIST 数字。 最后一行显示 DAE 的输出。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Civzq8O7-1681704179659)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_03_09.png)]
图 3.3.1:原始 MNIST 数字(顶部行),损坏的原始图像(中间行)和去噪图像(最后一行)
如图“图 3.3.2”所示,去噪自编码器的结构实际上与我们在上一节中介绍的 MNIST 的自编码器相同。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YwFkZaye-1681704179660)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_03_10.png)]
图 3.3.2:去噪自编码器的输入是损坏的图像。 输出是干净或去噪的图像。 假定潜向量为 16 维
“图 3.3.2”中的输入定义为:
x = x_ori + noise
(公式 3.3.1)
在该公式中,x_ori
表示被噪声破坏的原始 MNIST 图像。 编码器的目的是发现如何产生潜向量z
,这将使解码器能够恢复诸如 MSE,如下所示:x_ori
通过最小化相异损失函数:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xvdAmnOO-1681704179660)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_03_018.png)] (Equation 3.3.2)
在此示例中,m
是输出尺寸(例如,在 MNIST 中,m = width × height × channels = 28 × 28 × 1 = 784
)。 x_ori[i]
和x_tilde[i]
分别是x_ori
和x_tilde
的元素。
为了实现 DAE,我们将需要对上一节中介绍的自编码器进行一些更改。 首先,训练输入数据应损坏的 MNIST 数字。 训练输出数据是原始的原始 MNIST 数字相同。 这就像告诉自编码器应校正的图像是什么,或要求它找出在图像损坏的情况下如何消除噪声。 最后,我们必须在损坏的 MNIST 测试数据上验证自编码器。
“图 3.3.2"左侧所示的 MNIST 数字 7 是实际损坏的图像输入。 右边的是经过训练的降噪自编码器的干净图像输出。
“列表 3.3.1”:denoising-autoencoder-mnist-3.3.1.py
from tensorflow.keras.layers import Dense, Input from tensorflow.keras.layers import Conv2D, Flatten from tensorflow.keras.layers import Reshape, Conv2DTranspose from tensorflow.keras.models import Model from tensorflow.keras import backend as K from tensorflow.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.3.1”显示了去噪自编码器,该编码器已添加到官方 Keras GitHub 存储库中。 使用相同的 MNIST 数据集,我们可以通过添加随机噪声来模拟损坏的图像。 添加的噪声是高斯分布,平均值为μ = 0.5
,标准差为σ = 0.5
。 由于添加随机噪声可能会将像素数据推入小于 0 或大于 1 的无效值,因此像素值会被裁剪为[0.1, 1.0]
范围。
其他所有内容实际上都与上一节中的自编码器相同。 我们将使用相同的 MSE 损失函数和 Adam 优化器。 但是,训练的周期数已增加到 10。这是为了进行足够的参数优化。
“图 3.3.3”显示了 DAE 在某种程度上的鲁棒性,因为噪声级别从σ = 0.5
增至σ = 0.75
和σ = 1.0
。 在σ = 0.75
处,DAE 仍能够恢复原始图像。 但是,在σ = 1.0
处,一些数字,例如第二和第三组中的 4 和 5,将无法正确恢复。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WuDQ7VTO-1681704179660)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_03_11.png)]
图 3.3.3:降噪自编码器的表现随着噪声水平的提高而增加
我们已经完成去噪自编码器的讨论和实现。 尽管此概念已在 MNIST 数字上进行了演示,但该思想也适用于其他信号。 在下一节中,我们将介绍自编码器的另一种实际应用,称为着色自编码器。
4. 自动着色自编码器
现在,我们将致力于自编码器的另一个实际应用。 在这种情况下,我们将想象一下,我们有一张灰度照片,并且想要构建一个可以自动为其添加颜色的工具。 我们要复制人类的能力,以识别海洋和天空为蓝色,草地和树木为绿色,云层为白色,依此类推。
如图“图 3.4.1”所示,如果给我们前景的稻田,背景的火山和顶部的天空的灰度照片(左),我们可以添加适当的颜色(右)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fKiPB1zx-1681704179660)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_03_12.png)]
图 3.4.1:为 Mayon 火山的灰度照片添加颜色。 着色网络应通过向灰度照片添加颜色来复制人类的能力。 左照片是灰度的。 正确的照片是彩色的。 原始彩色照片可以在本书的 GitHub 存储库中找到。
对于自编码器,一种简单的自动着色算法似乎是一个合适的问题。 如果我们可以使用足够数量的灰度照片作为输入并使用相应的彩色照片作为输出来训练自编码器,则可能会在正确应用颜色时发现隐藏的结构。 大致上,这是去噪的反向过程。 问题是,自编码器能否在原始灰度图像上添加颜色(良好的噪点)?
“列表 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
from tensorflow.keras.layers import Dense, Input from tensorflow.keras.layers import Conv2D, Flatten from tensorflow.keras.layers import Reshape, Conv2DTranspose from tensorflow.keras.models import Model from tensorflow.keras.callbacks import ReduceLROnPlateau from tensorflow.keras.callbacks import ModelCheckpoint from tensorflow.keras.datasets import cifar10 from tensorflow.keras.utils import plot_model from tensorflow.keras import backend as K
import numpy as np import matplotlib.pyplot as plt import os
def rgb2gray(rgb): """Convert from color image (RGB) to grayscale. Source: opencv.org grayscale = 0.299*red + 0.587*green + 0.114*blue Argument: rgb (tensor): rgb image Return: (tensor): grayscale image """ 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 back 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 = [lr_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 维,以增加其可以表示的显着属性的数量,如自编码器部分所述。 最后,输出过滤器的大小已增加到三倍,或等于预期的彩色输出的 RGB 中的通道数。
现在使用灰度作为输入,原始 RGB 图像作为输出来训练着色自编码器。 训练将花费更多的时间,并在验证损失没有改善的情况下使用学习率降低器来缩小学习率。 通过告诉tf.keras fit()
函数中的 callbacks 参数调用lr_reducer()
函数,可以轻松完成此操作。
“图 3.4.2”演示了来自 CIFAR10 测试数据集的灰度图像的着色。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-44EXWVbZ-1681704179661)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_03_13.png)]
图 3.4.2:使用自编码器将灰度自动转换为彩色图像。 CIFAR10 测试灰度输入图像(左)和预测的彩色图像(右)。 原始彩色照片可以在本书的 GitHub 存储库中找到,网址为 https://github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras/blob/master/chapter3-autoencoders/README.md
“图 3.4.3”将基本事实与着色自编码器预测进行了比较:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l01pHkDX-1681704179661)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_03_14.png)]
图 3.4.3:地面真彩色图像与预测彩色图像的并排比较。 原始彩色照片可以在本书的 GitHub 存储库中找到,网址为 https://github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras/blob/master/chapter3-autoencoders/README.md
自编码器执行可接受的着色作业。 预计大海或天空为蓝色,动物的阴影为棕色,云为白色,依此类推。
有一些明显的错误预测,例如红色车辆变成蓝色或蓝色车辆变成红色,偶尔的绿色领域被误认为是蓝天,而黑暗或金色的天空被转换为蓝天。
这是关于自编码器的最后一部分。 在以下各章中,我们将重新讨论以一种或另一种形式进行编码和解码的概念。 表示学习的概念在深度学习中非常基础。
5. 总结
在本章中,我们已经介绍了自编码器,它们是将输入数据压缩为低维表示形式的神经网络,以便有效地执行结构转换,例如降噪和着色。 我们为 GAN 和 VAE 的更高级主题奠定了基础,我们将在后面的章节中介绍它们。 我们已经演示了如何从两个构建模块模型(编码器和解码器)实现自编码器。 我们还学习了如何提取输入分布的隐藏结构是 AI 的常见任务之一。
一旦学习了潜在代码,就可以对原始输入分布执行许多结构操作。 为了更好地了解输入分布,可以使用低级嵌入(类似于本章内容)或通过更复杂的降维技术(例如 t-SNE 或 PCA)来可视化潜在向量形式的隐藏结构。
除了去噪和着色外,自编码器还用于将输入分布转换为低维潜向量,可以针对其他任务(例如,分割,检测,跟踪,重建和视觉理解)进一步对其进行处理。 在“第 8 章”,“变分自编码器(VAE)”中,我们将讨论 VAE,它们在结构上与自编码器相同,但具有可解释的潜在代码,这些代码可以产生连续的潜在向量投影,因此有所不同。
在下一章中,我们将着手介绍 AI 最近最重要的突破之一,即 GAN。 在下一章中,我们将学习 GAN 的核心优势,即其综合看起来真实的数据的能力。
6. 参考
Ian Goodfellow et al.: Deep Learning. Vol. 1. Cambridge: MIT press, 2016 (http://www.deeplearningbook.org/).
四、生成对抗网络(GAN)
在本章中,我们将研究生成对抗网络(GAN)[1]。 GAN 属于生成模型家族。 但是,与自编码器不同,生成模型能够在给定任意编码的情况下创建新的有意义的输出。
在本章中,将讨论 GAN 的工作原理。 我们还将使用tf.keras
回顾几个早期 GAN 的实现,而在本章的后面,我们将演示实现稳定训练所需的技术。 本章的范围涵盖了 GAN 实现的两个流行示例,深度卷积 GAN(DCGAN)[2]和条件 GAN(CGAN)[3]。
总之,本章的目标是:
- GAN 的原理简介
- GAN 的早期工作实现之一的简介,称为 DCGAN
- 改进的 DCGAN,称为 CGAN,它使用条件
- 在
tf.keras
中实现 DCGAN 和 CGAN
让我们从 GAN 的概述开始。
1. GAN 概述
在进入 GAN 的更高级概念之前,让我们开始研究 GAN,并介绍它们背后的基本概念。 GAN 非常强大。 通过执行潜在空间插值,他们可以生成不是真实人的新人脸这一事实证明了这一简单的陈述。
可以在以下 YouTube 视频中看到 GAN 的高级功能:
展示如何利用 GAN 产生逼真的面部的视频演示了它们的功能。 这个主题比我们之前看过的任何内容都先进得多。 例如,上面的视频演示了自编码器无法轻松完成的事情,我们在“第 3 章”,“自编码器”中介绍了这些内容。
GAN 可以通过训练两个相互竞争(且相互配合)的网络(称为生成器和判别器(有时称为评论家)。 生成器的作用是继续弄清楚如何生成伪造数据或信号(包括音频和图像),使伪造者蒙上阴影。 同时,判别器被训练以区分假信号和真实信号。 随着训练的进行,判别器将不再能够看到合成生成的数据与真实数据之间的差异。 从那里,可以丢弃判别器,然后可以使用生成器来创建从未见过的新的真实数据。
GAN 的基本概念很简单。 但是,我们将发现的一件事是,最具挑战性的问题是我们如何实现对生成器-判别器网络的稳定训练? 为了使两个网络都能同时学习,生成器和判别器之间必须存在健康的竞争。 由于损失函数是根据判别器的输出计算得出的,因此其参数会快速更新。 当判别器收敛速度更快时,生成器不再为其参数接收到足够的梯度更新,并且无法收敛。 除了难以训练之外,GAN 还可能遭受部分或全部模态崩溃的影响,这种情况下,生成器针对不同的潜在编码生成几乎相似的输出。
GAN 的原理
如图“图 4.1.1”所示,GAN 类似于伪造者(生成器)-警察(判别器)场景。 在学院里,警察被教导如何确定美钞是真钞还是假钞。 来自银行的真实美钞样本和来自伪造者的伪钞样本被用来训练警察。 但是,伪造者会不时地假装他印制了真实的美元钞票。 最初,警方不会上当,并且会告诉造假者这笔钱是假的。 考虑到此反馈,造假者再次磨练他的技能,并尝试制作新的假美元钞票。 如预期的那样,警察将能够发现这笔钱是伪造的,并说明为什么美元钞票是伪造的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eq0c6b0t-1681704179661)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_04_01.png)]
图 4.1.1:GAN 的生成器和判别器类似于伪造者和警察。 造假者的目的是欺骗警察,使他们相信美元钞票是真实的
此过程无限期地继续,但是到了造假者已经掌握了伪造货币的程度,以至于伪造品与真实货币几乎无法区分-甚至对于最受执业的警察也是如此。 然后,伪造者可以无限次打印美元钞票,而不会被警方抓获,因为它们不再可识别为伪造的。
如图“图 4.1.2”所示,GAN 由两个网络组成,一个生成器和一个判别器:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ATdYgZbH-1681704179661)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_04_02.png)]
图 4.1.2:GAN 由两个网络组成,一个生成器和一个判别器。 判别器经过训练,可以区分真实信号和虚假信号或数据。 生成器的工作是生成伪造的信号或数据,这些伪造的信号或数据最终会欺骗判别器
生成器的输入是噪声,输出是合成数据。 同时,判别器的输入将是实数据或合成数据。 真实数据来自真实的采样数据,而虚假数据来自生成器。 所有有效数据均标记为 1.0(即 100% 为真实概率),而所有合成数据均标记为 0.0(即 0% 为真实概率)。 由于标记过程是自动化的,因此 GAN 仍被认为是深度学习中无监督学习方法的一部分。
判别器的目标是从此提供的数据集中学习如何区分真实数据与伪数据。 在 GAN 训练的这一部分中,仅判别器参数将被更新。 像典型的二元分类器一样,判别器经过训练,可以在 0.0 到 1.0 的范围内预测置信度值,以了解给定输入数据与真实数据的接近程度。 但是,这只是故事的一半。
生成器将以固定的时间间隔假装其输出是真实数据,并要求 GAN 将其标记为 1.0。 然后,当将伪造数据提供给判别器时,自然会将其分类为伪造,标签接近 0.0。
优化器根据显示的标签(即 1.0)计算生成器参数更新。 在对新数据进行训练时,它还会考虑自己的预测。 换句话说,判别器对其预测有一些疑问,因此,GAN 将其考虑在内。 这次,GAN 将让梯度从判别器的最后一层向下向下传播到生成器的第一层。 但是,在大多数实践中,在训练的此阶段,判别器参数会暂时冻结。 生成器将使用梯度来更新其参数并提高其合成伪数据的能力。
总体而言,整个过程类似于两个网络相互竞争,同时仍在合作。 当 GAN 训练收敛时,最终结果是生成器,可以合成看似真实的数据。 判别器认为该合成数据是真实数据或带有接近 1.0 的标签,这意味着该判别器可以被丢弃。 生成器部分将有助于从任意噪声输入中产生有意义的输出。
下面的“图 4.1.3”中概述了该过程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-awrADSDm-1681704179661)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_04_03.png)]
图 4.1.3:训练判别器类似于使用二进制交叉熵损失训练二分类器网络。 伪数据由生成器提供,而真实数据来自真实样本
如上图所示,可以通过最小化以下等式中的损失函数来训练判别器:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pNX2PMhP-1681704179662)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_04_001.png)] (Equation 4.1.1)
该方程只是标准的二进制交叉熵代价函数。 损失是正确识别真实数据1 - D(g(z))
的期望值与 1.0 正确识别合成数据1 - D(g(z))
的期望值之和。 日志不会更改本地最小值的位置。
TensorFlow 2 和 Keras 高级深度学习:1~5(4)https://developer.aliyun.com/article/1426947