通常,自编码器可被视为一种无监督学习技术,因为您不需要显式标签来训练模型。训练自编码器所需的只是原始输入数据。
在本教程中,您将了解深度学习中的自编码器,并在 Python 与 Keras 中实现卷积自编码器和去噪自编码器。您将以 NotMNIST 字母数据集为示例进行实践。
简而言之,今天的教程将涵盖以下主题:
首先,您将进一步了解自编码器:它是什么、与降维技术的比较,以及这种算法的不同类型;
接着,您将关注卷积自编码器:先了解其作用与构建方法,然后亲手实现一个!您将学习如何在数据为 ubyte gzip 格式时进行加载、探索数据、预处理数据、将数据拟合到模型中、可视化训练与验证损失曲线,最后在测试集上进行预测。
随后,您将认识去噪自编码器并实现一个:学习如何为图像添加噪声,将这些图像输入深度学习模型并进行训练,最后对含噪测试图像进行预测。
自编码器
如引言所述,自编码器是一种无监督机器学习算法,它以图像为输入,并尝试使用瓶颈(也称潜在空间)中的更少比特进行重建。图像在瓶颈处被大幅压缩。自编码器中的压缩是通过训练网络一段时间实现的,随着学习推进,它会尝试在瓶颈处尽可能好地表示输入图像。普通的图像压缩算法(如 JPEG 与无损 JPEG 压缩)无需任何训练就能压缩图像,并且表现相当不错。
自编码器与主成分分析(PCA)等降维技术类似。它们通过线性变换将数据从高维投射到低维,并在去除非关键部分的同时尽量保留数据的重要特征。
然而,自编码器与 PCA 的主要差异在于变换方式:如前所述,PCA 使用线性变换,而自编码器使用非线性变换。
现在您对自编码器已有初步了解,让我们拆解这个概念,进一步建立直观感受!
上图是一个具有单个隐藏层的两层基础自编码器。在深度学习术语中,统计架构层数时通常不计输入层。架构中的总层数只包含隐藏层数量与输出层。
如上图所示,输入层与输出层具有相同数量的神经元。
举个例子。您将仅含五个像素值的图像输入自编码器,编码器在瓶颈(中间层)或潜在空间中将其压缩为三个像素值。解码器使用这三个值尝试重建五个像素值,或者说重建您输入到网络中的原始图像。
实际中,输入与输出之间往往有更多隐藏层。
自编码器可分为三个部分
编码器(Encoder):网络的这一部分将输入压缩或下采样为更少的比特。由更少比特表示的空间通常称为潜在空间 或瓶颈 。瓶颈也被称为“最大压缩点”,因为此处的压缩程度最高。代表原始输入的这些压缩比特合称为输入的“编码”。
解码器(Decoder):网络的这一部分尝试仅使用输入的编码来重建输入。当解码器能够精确重建输入时,您可以认为编码器为输入生成了最佳编码,从而使解码器能够很好地重建!
自编码器有多种类型,如卷积自编码器、去噪自编码器、变分自编码器以及稀疏自编码器。然而,正如引言所述,本教程将仅聚焦于卷积与去噪这两类。使用 Keras 在 Python 中实现卷积自编码器
由于您的输入数据是图像,使用卷积自编码器是个不错的主意。它不是自编码器的全新变体,而是用卷积层堆叠出的传统自编码器:基本上用卷积层替换全连接层。卷积层配合最大池化层,将输入从“宽”(28 x 28 的图像)且“薄”(单通道或灰度)变为“小”(潜在空间中的 7 x 7 图像)且“厚”(128 个通道)。
如果您尚未完全理解上述思想也不必担心!本教程的第二部分将聚焦于上述内容的实现,希望能澄清您可能存在的所有疑惑。
提示 :如果您想进一步了解卷积神经网络,请查看这篇教程 。
这有助于网络从图像中提取视觉特征,从而获得更准确的潜在空间表示。重建过程使用上采样与卷积,即解码器;下采样即将图像压缩到低维的过程,也称为编码器。
需要注意的是,编码器主要压缩输入图像,例如:如果您的输入图像尺寸为 176 x 176 x 1(约 30976),那么最大压缩点的尺寸可能为 22 x 22 x 512(约 247808)。因此,在该情况下,您从一个尺寸为 176 x 176 的灰度图像出发,经过若干卷积层以及恰好三个最大池化层,下采样到 22 x 22 的尺寸,但通道数从 1 增至 512。正如上文所述,您的输入从“宽”(176 x 176)且“薄”(1)变为“小”(22 x 22)且“厚”(512)。
加载数据
notMNIST 数据集是一个字母 A 到 J 的字体字形图像识别数据集。它与经典的 MNIST 数据集相当相似,后者包含手写数字 0 到 9 的图像:在这里,您会发现 NotMNIST 数据集由 70,000 张 A - J 的 28x28 灰度字母图像组成,共 10 个类别,每个类别 6,000 张图像。
提示 :如果您想学习如何使用 MNIST 数据集实现用于分类任务的多层感知机(MLP),请查看这篇教程 。
NotMNIST 数据集并未在 Keras 或 TensorFlow 框架中预置,因此您需要从此来源 下载数据。数据将以 ubyte.gzip 格式下载,但无需担心!您很快就会学会如何读取字节流格式并将其转换为 NumPy 数组。那就开始吧!
网络将在 Nvidia Tesla K40 上训练,因此如果您在 GPU 上训练并使用 Jupyter Notebook,您需要添加三行代码,通过 os 模块指定 CUDA 设备顺序与可见设备。
在下面的代码中,您基本是在 Notebook 中使用 os.environ 设置环境变量。最好在初始化 Keras 之前执行以下操作,以限制 Keras 后端 TensorFlow 使用第一个 GPU。如果您所用机器的 GPU 在 0,请确保使用 0 而不是 1。您可以在终端运行简单命令检查,例如 nvidia-smi
import os
os. environ[ "CUDA_DEVICE_ORDER" ] = "PCI_BUS_ID"
os. environ[ "CUDA_VISIBLE_DEVICES" ] = "1"
技术支持
接下来,导入所有所需模块,如 numpy、matplotlib,以及最重要的 keras,因为本教程将使用该框架!
import keras
from matplotlib import pyplot as plt
import numpy as np
import gzip
% matplotlib inline
from keras. layers import Input, Conv2D, MaxPooling2D, UpSampling2D
from keras. models import Model
from keras. optimizers import RMSprop
技术支持
Using TensorFlow backend.
技术支持
这里,您定义了一个函数用于打开 gzip 文件,并用 bytestream.read() 读取文件。您将图像尺寸与图像总数传给该函数。然后使用 np.frombuffer() 将存储在变量 buf 中的字符串转换为 float32 类型的 NumPy 数组。
接着,将数组重塑为三维数组或张量,其中第一维为图像数量,第二与第三维为图像尺寸。最后返回 NumPy 数组 data。
def extract_data ( filename, num_images) :
with gzip. open ( filename) as bytestream:
bytestream. read( 16 )
buf = bytestream. read( 28 * 28 * num_images)
data = np. frombuffer( buf, dtype= np. uint8) . astype( np. float32)
data = data. reshape( num_images, 28 , 28 )
return data
技术支持
现在,通过传入训练与测试文件及其对应的图像数量来调用 extract_data() 函数:
train_data = extract_data( 'train-images-idx3-ubyte.gz' , 60000 )
test_data = extract_data( 't10k-images-idx3-ubyte.gz' , 10000 )
技术支持
类似地,您定义一个提取标签的函数,打开 gzip 文件并使用 bytestream.read() 读取文件,向其传入标签维度(1)与图像总数。随后,使用 np.frombuffer() 将变量 buf 中的字符串转换为 int64 类型的 NumPy 数组。
这次无需重塑数组,因为变量 labels 将返回维度为 60,000 x 1 的列向量。最后返回 NumPy 数组 labels。
def extract_labels ( filename, num_images) :
with gzip. open ( filename) as bytestream:
bytestream. read( 8 )
buf = bytestream. read( 1 * num_images)
labels = np. frombuffer( buf, dtype= np. uint8) . astype( np. int64)
return labels
技术支持
现在通过传入训练与测试标签文件及其对应的图像数量来调用提取标签函数:
train_labels = extract_labels( 'train-labels-idx1-ubyte.gz' , 60000 )
test_labels = extract_labels( 't10k-labels-idx1-ubyte.gz' , 10000 )
技术支持
加载了训练与测试数据后,便可着手分析数据,以便对今天将要处理的数据集建立一些直觉!
数据探索
现在让我们分析数据集中图像的外观,并借助 NumPy 数组属性 .shape 查看图像的维度:
print ( "Training set (images) shape: {shape}" . format ( shape= train_data. shape) )
print ( "Test set (images) shape: {shape}" . format ( shape= test_data. shape) )
技术支持
Training set ( images) shape: ( 60000 , 28 , 28 )
Test set ( images) shape: ( 10000 , 28 , 28 )
技术支持
从上述输出可见,训练数据形状为 60000 x 28 x 28,因为有 60,000 个训练样本,每个为 28 x 28 的矩阵。类似地,测试数据形状为 10000 x 28 x 28,因为有 10,000 个测试样本。
注意 ,在本任务中,您不会使用训练与测试标签。当前任务仅处理训练与测试图像。不过,出于探索目的(有助于更好地理解数据),您将使用这些标签。
让我们创建一个字典,包含类别名称及其对应的类别标签:
label_dict = {
0 : 'A' ,
1 : 'B' ,
2 : 'C' ,
3 : 'D' ,
4 : 'E' ,
5 : 'F' ,
6 : 'G' ,
7 : 'H' ,
8 : 'I' ,
9 : 'J' ,
}
技术支持
现在,让我们看看数据集中几张图像:
plt. figure( figsize= [ 5 , 5 ] )
plt. subplot( 121 )
curr_img = np. reshape( train_data[ 0 ] , ( 28 , 28 ) )
curr_lbl = train_labels[ 0 ]
plt. imshow( curr_img, cmap= 'gray' )
plt. title( "(Label: " + str ( label_dict[ curr_lbl] ) + ")" )
plt. subplot( 122 )
curr_img = np. reshape( test_data[ 0 ] , ( 28 , 28 ) )
curr_lbl = test_labels[ 0 ]
plt. imshow( curr_img, cmap= 'gray' )
plt. title( "(Label: " + str ( label_dict[ curr_lbl] ) + ")" )
技术支持
< matplotlib. text. Text at 0x7f3ec73db240 >
技术支持
上述两幅图的输出分别是来自训练与测试数据的样本图像,这些图像被分别赋予 5 或 F,以及 3 或 D 的类别标签。类似地,其他字母会有不同标签,而相同字母会有相同标签。这意味着所有 6,000 张类别为 F 的图像都将拥有类别标签 5。
数据预处理
该数据集中的图像确为灰度图像,像素值范围为 0 到 255,尺寸为 28 x 28,因此在将数据输入模型之前进行预处理非常重要。您将首先把训练与测试集的每一张 28 x 28 图像转换为 28 x 28 x 1 的矩阵,以便输入网络:
train_data = train_data. reshape( - 1 , 28 , 28 , 1 )
test_data = test_data. reshape( - 1 , 28 , 28 , 1 )
train_data. shape, test_data. shape
技术支持
( ( 60000 , 28 , 28 , 1 ) , ( 10000 , 28 , 28 , 1 ) )
技术支持
接下来,您需要检查训练与测试 NumPy 数组的数据类型,应为 float32;若不是,需要进行转换。但由于您在读取数据时已完成转换,因此无需再次处理。同时,您还需要将像素值缩放到 0 - 1(含)范围内。我们来做这一步!
别忘了验证训练与测试数据类型:
train_data. dtype, test_data. dtype
技术支持
( dtype ( 'float32' ) , dtype ( 'float32' ) )
技术支持
接下来,使用训练与测试数据的最大像素值对其进行缩放:
np. max ( train_data) , np. max ( test_data)
技术支持
( 255.0 , 255.0 )
技术支持
train_data = train_data / np. max ( train_data)
test_data = test_data / np. max ( test_data)
技术支持
验证缩放后的训练与测试数据的最大值应为 1.0!
np. max ( train_data) , np. max ( test_data)
技术支持
( 1.0 , 1.0 )
技术支持
完成上述步骤后,划分数据也很重要。为了使模型具有良好的泛化能力,您将训练数据划分为两部分:训练集与验证集。您将在 80% 的数据上训练模型,并在剩余 20% 的训练数据上进行验证。
这也有助于降低过拟合的可能性,因为您会在训练阶段未见过的数据上验证模型。
您可以使用 scikit-learn 的 train_test_split 模块来正确划分数据:
from sklearn. model_selection import train_test_split
train_X, valid_X, train_ground, valid_ground = train_test_split( train_data,
train_data,
test_size= 0.2 ,
random_state= 13 )
技术支持
注意 ,在本任务中您不需要训练与测试标签。这就是为什么会将训练图像传入两次。训练图像既作为输入也作为类似分类任务中标签的“真值”。
现在,您已经准备好定义网络并将数据输入网络。事不宜迟,我们进入下一步!
卷积自编码器
图像大小为 28 x 28 x 1,或 784 维向量。您将图像矩阵转换为数组,将其缩放到 0 到 1 之间,重塑为 28 x 28 x 1 的尺寸,并将其作为输入馈入网络。
此外,您将使用批大小 128;使用更大的批大小 256 或 512 也可取,具体取决于训练所用系统。批大小对学习参数的确定与预测精度影响很大。您将训练网络 50 个 epoch。
batch_size = 128
epochs = 50
inChannel = 1
x, y = 28 , 28
input_img = Input( shape = ( x, y, inChannel) )
技术支持
如前所述,自编码器分为两个部分:编码器与解码器。
编码器
第一层包含 32 个 3 x 3 卷积核,后接一次下采样(最大池化)层,
第二层包含 64 个 3 x 3 卷积核,后接另一层下采样,
编码器的最后一层包含 128 个 3 x 3 卷积核。
解码器
第一层包含 128 个 3 x 3 卷积核,后接一次上采样层,/li>
第二层包含 64 个 3 x 3 卷积核,后接另一层上采样,
编码器的最后一层包含 1 个 3 x 3 卷积核。
最大池化层每次使用时会将输入下采样为原来的一半,而上采样层每次使用时会将输入上采样为原来的两倍。
注意 :卷积核数量、卷积核大小、层数、训练轮数等都是超参数,应基于您的直觉来决定,您可以自由地通过调整这些超参数来尝试新实验并评估模型性能。通过这种方式,您将逐步掌握深度学习的“技艺”!
def autoencoder ( input_img) :
conv1 = Conv2D( 32 , ( 3 , 3 ) , activation= 'relu' , padding= 'same' ) ( input_img)
pool1 = MaxPooling2D( pool_size= ( 2 , 2 ) ) ( conv1)
conv2 = Conv2D( 64 , ( 3 , 3 ) , activation= 'relu' , padding= 'same' ) ( pool1)
pool2 = MaxPooling2D( pool_size= ( 2 , 2 ) ) ( conv2)
conv3 = Conv2D( 128 , ( 3 , 3 ) , activation= 'relu' , padding= 'same' ) ( pool2)
conv4 = Conv2D( 128 , ( 3 , 3 ) , activation= 'relu' , padding= 'same' ) ( conv3)
up1 = UpSampling2D( ( 2 , 2 ) ) ( conv4)
conv5 = Conv2D( 64 , ( 3 , 3 ) , activation= 'relu' , padding= 'same' ) ( up1)
up2 = UpSampling2D( ( 2 , 2 ) ) ( conv5)
decoded = Conv2D( 1 , ( 3 , 3 ) , activation= 'sigmoid' , padding= 'same' ) ( up2)
return decoded
技术支持
创建模型后,需要进行编译,并将优化器设为 RMSProp。
提示 :如果想进一步了解 RMSProp,请观看 Andrew Ng 的这个教程 。
注意,您还需要通过 loss 参数指定损失类型。在本例中使用均方误差,因为每个批次后的损失将基于预测输出与真值之间的逐像素均方误差计算:
autoencoder = Model( input_img, autoencoder( input_img) )
autoencoder. compile ( loss= 'mean_squared_error' , optimizer = RMSprop( ) )
技术支持
让我们使用 summary 函数可视化上一步中创建的各层,它将显示每层的参数数量(权重与偏置)以及模型的总参数量。
autoencoder. summary( )
技术支持
_________________________________________________________________
Layer ( type) Output Shape Param #
=== === === === === === === === === === === === === === === === === === === === === ==
input_6 ( InputLayer) ( None, 28 , 28 , 1 ) 0
_________________________________________________________________
conv2d_25 ( Conv2D) ( None, 28 , 28 , 32 ) 320
_________________________________________________________________
max_pooling2d_9 ( MaxPooling2 ( None, 14 , 14 , 32 ) 0
_________________________________________________________________
conv2d_26 ( Conv2D) ( None, 14 , 14 , 64 ) 18496
_________________________________________________________________
max_pooling2d_10 ( MaxPooling ( None, 7 , 7 , 64 ) 0
_________________________________________________________________
conv2d_27 ( Conv2D) ( None, 7 , 7 , 128 ) 73856
_________________________________________________________________
conv2d_28 ( Conv2D) ( None, 7 , 7 , 128 ) 147584
_________________________________________________________________
up_sampling2d_9 ( UpSampling2 ( None, 14 , 14 , 128 ) 0
_________________________________________________________________
conv2d_29 ( Conv2D) ( None, 14 , 14 , 64 ) 73792
_________________________________________________________________
up_sampling2d_10 ( UpSampling ( None, 28 , 28 , 64 ) 0
_________________________________________________________________
conv2d_30 ( Conv2D) ( None, 28 , 28 , 1 ) 577
=== === === === === === === === === === === === === === === === === === === === === ==
Total params: 314 , 625
Trainable params: 314 , 625
Non- trainable params: 0
_________________________________________________________________
技术支持
终于可以用 Keras 的 fit() 函数训练模型了!模型将训练 50 个 epoch。fit() 会返回一个 history 对象;将其结果存储在 fashion_train(此处为 autoencoder_train)后,您可以用它来绘制训练与验证损失曲线,从而直观分析模型表现。
训练模型
autoencoder_train = autoencoder. fit( train_X, train_ground, batch_size= batch_size, epochs= epochs, verbose= 1 , validation_data= ( valid_X, valid_ground) )
技术支持
Train on 48000 samples, validate on 12000 samples
Epoch 1 / 50
48000 / 48000 [ === === === === === === === === === === ] - 16s - loss: 0.0368 - val_loss: 0.0132
Epoch 2 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0101 - val_loss: 0.0085
Epoch 3 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0071 - val_loss: 0.0081
Epoch 4 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0057 - val_loss: 0.0056
Epoch 5 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0048 - val_loss: 0.0051
Epoch 6 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0043 - val_loss: 0.0039
Epoch 7 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0038 - val_loss: 0.0039
Epoch 8 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0035 - val_loss: 0.0039
Epoch 9 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0032 - val_loss: 0.0030
Epoch 10 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0030 - val_loss: 0.0029
Epoch 11 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0029 - val_loss: 0.0026
Epoch 12 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0027 - val_loss: 0.0025
Epoch 13 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0026 - val_loss: 0.0028
Epoch 14 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0025 - val_loss: 0.0022
Epoch 15 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0024 - val_loss: 0.0024
Epoch 16 / 50
48000 / 48000 [ === === === === === === === === === === ] - 16s - loss: 0.0023 - val_loss: 0.0027
Epoch 17 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0023 - val_loss: 0.0022
Epoch 18 / 50
48000 / 48000 [ === === === === === === === === === === ] - 16s - loss: 0.0022 - val_loss: 0.0025
Epoch 19 / 50
48000 / 48000 [ === === === === === === === === === === ] - 16s - loss: 0.0022 - val_loss: 0.0022
Epoch 20 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0021 - val_loss: 0.0022
Epoch 21 / 50
48000 / 48000 [ === === === === === === === === === === ] - 16s - loss: 0.0021 - val_loss: 0.0020
Epoch 22 / 50
48000 / 48000 [ === === === === === === === === === === ] - 16s - loss: 0.0020 - val_loss: 0.0019
Epoch 23 / 50
48000 / 48000 [ === === === === === === === === === === ] - 16s - loss: 0.0020 - val_loss: 0.0021
Epoch 24 / 50
48000 / 48000 [ === === === === === === === === === === ] - 16s - loss: 0.0020 - val_loss: 0.0018
Epoch 25 / 50
48000 / 48000 [ === === === === === === === === === === ] - 16s - loss: 0.0019 - val_loss: 0.0020
Epoch 26 / 50
48000 / 48000 [ === === === === === === === === === === ] - 16s - loss: 0.0019 - val_loss: 0.0020
Epoch 27 / 50
48000 / 48000 [ === === === === === === === === === === ] - 16s - loss: 0.0019 - val_loss: 0.0017
Epoch 28 / 50
48000 / 48000 [ === === === === === === === === === === ] - 16s - loss: 0.0018 - val_loss: 0.0018
Epoch 29 / 50
48000 / 48000 [ === === === === === === === === === === ] - 16s - loss: 0.0018 - val_loss: 0.0019
Epoch 30 / 50
48000 / 48000 [ === === === === === === === === === === ] - 16s - loss: 0.0018 - val_loss: 0.0017
Epoch 31 / 50
48000 / 48000 [ === === === === === === === === === === ] - 16s - loss: 0.0018 - val_loss: 0.0019
Epoch 32 / 50
48000 / 48000 [ === === === === === === === === === === ] - 16s - loss: 0.0017 - val_loss: 0.0018
Epoch 33 / 50
48000 / 48000 [ === === === === === === === === === === ] - 16s - loss: 0.0017 - val_loss: 0.0017
Epoch 34 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0017 - val_loss: 0.0018
Epoch 35 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0017 - val_loss: 0.0019
Epoch 36 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0017 - val_loss: 0.0016
Epoch 37 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0017 - val_loss: 0.0017
Epoch 38 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0016 - val_loss: 0.0019
Epoch 39 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0016 - val_loss: 0.0015
Epoch 40 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0016 - val_loss: 0.0017
Epoch 41 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0016 - val_loss: 0.0017
Epoch 42 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0016 - val_loss: 0.0014
Epoch 43 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0016 - val_loss: 0.0018
Epoch 44 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0016 - val_loss: 0.0015
Epoch 45 / 50
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0016 - val_loss: 0.0014
Epoch 46 / 50
48000 / 48000 [ === === === === === === === === === === ] - 16s - loss: 0.0015 - val_loss: 0.0016
Epoch 47 / 50
48000 / 48000 [ === === === === === === === === === === ] - 16s - loss: 0.0015 - val_loss: 0.0017
Epoch 48 / 50
48000 / 48000 [ === === === === === === === === === === ] - 16s - loss: 0.0015 - val_loss: 0.0015
Epoch 49 / 50
48000 / 48000 [ === === === === === === === === === === ] - 16s - loss: 0.0015 - val_loss: 0.0016
Epoch 50 / 50
48000 / 48000 [ === === === === === === === === === === ] - 16s - loss: 0.0015 - val_loss: 0.0020
技术支持
太好了!您已在 Not-MNIST 数据上训练了模型 50 个 epoch。现在,让我们绘制训练与验证数据之间的损失曲线,以可视化模型性能。
训练与验证损失曲线
loss = autoencoder_train. history[ 'loss' ]
val_loss = autoencoder_train. history[ 'val_loss' ]
epochs = range ( epochs)
plt. figure( )
plt. plot( epochs, loss, 'bo' , label= 'Training loss' )
plt. plot( epochs, val_loss, 'b' , label= 'Validation loss' )
plt. title( 'Training and validation loss' )
plt. legend( )
plt. show( )
技术支持
可以看到,验证损失与训练损失基本保持同步。这表明模型没有过拟合:验证损失在下降而非上升,且训练与验证损失之间几乎没有间隙。
因此,可以认为模型的泛化能力良好。
最后,是时候使用 Keras 的 predict() 函数重建测试图像,看看模型在测试数据上的重建效果如何。
在测试数据上预测
您将对全部 10,000 张测试图像进行预测,并绘制部分重建图像,以可视化模型对测试图像的重建质量。
pred = autoencoder. predict( test_data)
技术支持
pred. shape
技术支持
( 10000 , 28 , 28 , 1 )
技术支持
plt. figure( figsize= ( 20 , 4 ) )
print ( "Test Images" )
for i in range ( 10 ) :
plt. subplot( 2 , 10 , i+ 1 )
plt. imshow( test_data[ i, . . . , 0 ] , cmap= 'gray' )
curr_lbl = test_labels[ i]
plt. title( "(Label: " + str ( label_dict[ curr_lbl] ) + ")" )
plt. show( )
plt. figure( figsize= ( 20 , 4 ) )
print ( "Reconstruction of Test Images" )
for i in range ( 10 ) :
plt. subplot( 2 , 10 , i+ 1 )
plt. imshow( pred[ i, . . . , 0 ] , cmap= 'gray' )
plt. show( )
技术支持
Test Images
技术支持
Reconstruction of Test Images
技术支持
从上述图中可以观察到,模型在重建您使用模型预测的测试图像方面表现出色。至少从视觉上看,测试图像与重建图像几乎完全相同。
去噪自编码器
去噪自编码器尝试学习一种对噪声具有鲁棒性的表示(潜在空间或瓶颈)。您为图像添加噪声,然后将含噪图像作为输入馈入网络的编码器部分。自编码器的编码器部分将图像变换到一个尽量保留字母信息、但去除噪声的空间。
但它究竟如何去除噪声呢?
在训练过程中,您会定义一个损失函数,类似于之前卷积自编码器中定义的均方根误差。在训练的每次迭代中,网络会计算解码器输出的含噪图像与真值(无噪图像)之间的损失,并尝试最小化重建图像与原始无噪图像之间的差异。换言之,网络将学习一个 7 x 7 x 128 的空间,该空间是您用于训练数据的无噪编码!
去噪自编码器的实现
现在,为了在 Python 中看看它如何工作,您将继续使用本教程第一部分中的 NotMNIST 数据集。这意味着无需再次进行数据预处理,因为之前已经完成!不过,本部分一个重要的预处理步骤是为训练、验证与测试图像添加噪声。我们先快速完成这一步!
为图像添加噪声
先定义一个噪声因子,这是一个超参数。噪声因子会与一个均值为 0.0、标准差为 1.0 的随机矩阵相乘。该矩阵将从正态(高斯)分布中采样。随机正态数组的形状将与您要添加噪声的数据形状相同。
为便于理解,举个例子:变量 train_X 的形状为 48000 x 28 x 28 x 1。因此,随机正态数组的形状也应与 train_X 相同,只有这样才能将两个数组相加,因为它们的维度一致。
noise_factor = 0.5
x_train_noisy = train_X + noise_factor * np. random. normal( loc= 0.0 , scale= 1.0 , size= train_X. shape)
x_valid_noisy = valid_X + noise_factor * np. random. normal( loc= 0.0 , scale= 1.0 , size= valid_X. shape)
x_test_noisy = test_data + noise_factor * np. random. normal( loc= 0.0 , scale= 1.0 , size= test_data. shape)
x_train_noisy = np. clip( x_train_noisy, 0. , 1. )
x_valid_noisy = np. clip( x_valid_noisy, 0. , 1. )
x_test_noisy = np. clip( x_test_noisy, 0. , 1. )
技术支持
np.clip() 会将所有负值阈值化为 0,将所有大于 1 的值阈值化为 1。因为您希望像素值位于 0 与 1 之间。向数据注入噪声后像素值范围可能会有所改变,因此为安全起见,对像素值进行裁剪是个好习惯。
可视化含噪图像
plt. figure( figsize= [ 5 , 5 ] )
plt. subplot( 121 )
curr_img = np. reshape( x_train_noisy[ 1 ] , ( 28 , 28 ) )
plt. imshow( curr_img, cmap= 'gray' )
plt. subplot( 122 )
curr_img = np. reshape( x_test_noisy[ 1 ] , ( 28 , 28 ) )
plt. imshow( curr_img, cmap= 'gray' )
技术支持
< matplotlib. image. AxesImage at 0x7f3ec6b20e48 >
技术支持
现在您已有含噪数据,可以将其输入网络,看看图像中的噪声是如何被“神奇地”去除的!
去噪自编码器网络
如前所述,自编码器分为两部分:编码器与解码器。您将构建的架构如下:
编码器
第一层为 32 个 3 x 3 卷积核,后接一次下采样(最大池化)层,
第二层为 64 个 3 x 3 卷积核,后接另一层下采样,
编码器的最后一层为 128 个 3 x 3 卷积核。
解码器
第一层为 128 个 3 x 3 卷积核,后接一次上采样层,
第二层为 64 个 3 x 3 卷积核,后接另一层上采样,
编码器的最后一层为 1 个 3 x 3 卷积核。
batch_size = 128
epochs = 20
inChannel = 1
x, y = 28 , 28
input_img = Input( shape = ( x, y, inChannel) )
技术支持
def autoencoder ( input_img) :
conv1 = Conv2D( 32 , ( 3 , 3 ) , activation= 'relu' , padding= 'same' ) ( input_img)
pool1 = MaxPooling2D( pool_size= ( 2 , 2 ) ) ( conv1)
conv2 = Conv2D( 64 , ( 3 , 3 ) , activation= 'relu' , padding= 'same' ) ( pool1)
pool2 = MaxPooling2D( pool_size= ( 2 , 2 ) ) ( conv2)
conv3 = Conv2D( 128 , ( 3 , 3 ) , activation= 'relu' , padding= 'same' ) ( pool2)
conv4 = Conv2D( 128 , ( 3 , 3 ) , activation= 'relu' , padding= 'same' ) ( conv3)
up1 = UpSampling2D( ( 2 , 2 ) ) ( conv4)
conv5 = Conv2D( 64 , ( 3 , 3 ) , activation= 'relu' , padding= 'same' ) ( up1)
up2 = UpSampling2D( ( 2 , 2 ) ) ( conv5)
decoded = Conv2D( 1 , ( 3 , 3 ) , activation= 'sigmoid' , padding= 'same' ) ( up2)
return decoded
技术支持
autoencoder = Model( input_img, autoencoder( input_img) )
autoencoder. compile ( loss= 'mean_squared_error' , optimizer = RMSprop( ) )
技术支持
训练
如果您还记得在训练卷积自编码器时,您将训练图像输入了两次,因为输入与真值相同。然而,在去噪自编码器中,您将含噪图像作为输入,而真值仍为您施加噪声之前的无噪图像。只有这样,网络才能在含噪图像与真实无噪图像之间计算损失。
autoencoder_train = autoencoder. fit( x_train_noisy, train_X, batch_size= batch_size, epochs= epochs, verbose= 1 , validation_data= ( x_valid_noisy, valid_X) )
技术支持
Train on 48000 samples, validate on 12000 samples
Epoch 1 / 20
48000 / 48000 [ === === === === === === === === === === ] - 15s - loss: 0.0531 - val_loss: 0.0268
Epoch 2 / 20
48000 / 48000 [ === === === === === === === === === === ] - 14s - loss: 0.0243 - val_loss: 0.0217
Epoch 3 / 20
48000 / 48000 [ === === === === === === === === === === ] - 14s - loss: 0.0207 - val_loss: 0.0200
Epoch 4 / 20
48000 / 48000 [ === === === === === === === === === === ] - 14s - loss: 0.0190 - val_loss: 0.0184
Epoch 5 / 20
48000 / 48000 [ === === === === === === === === === === ] - 14s - loss: 0.0179 - val_loss: 0.0183
Epoch 6 / 20
48000 / 48000 [ === === === === === === === === === === ] - 14s - loss: 0.0171 - val_loss: 0.0178
Epoch 7 / 20
48000 / 48000 [ === === === === === === === === === === ] - 14s - loss: 0.0165 - val_loss: 0.0161
Epoch 8 / 20
48000 / 48000 [ === === === === === === === === === === ] - 14s - loss: 0.0160 - val_loss: 0.0166
Epoch 9 / 20
48000 / 48000 [ === === === === === === === === === === ] - 14s - loss: 0.0157 - val_loss: 0.0157
Epoch 10 / 20
48000 / 48000 [ === === === === === === === === === === ] - 14s - loss: 0.0153 - val_loss: 0.0159
Epoch 11 / 20
48000 / 48000 [ === === === === === === === === === === ] - 14s - loss: 0.0151 - val_loss: 0.0153
Epoch 12 / 20
48000 / 48000 [ === === === === === === === === === === ] - 14s - loss: 0.0148 - val_loss: 0.0154
Epoch 13 / 20
48000 / 48000 [ === === === === === === === === === === ] - 14s - loss: 0.0146 - val_loss: 0.0151
Epoch 14 / 20
48000 / 48000 [ === === === === === === === === === === ] - 14s - loss: 0.0145 - val_loss: 0.0152
Epoch 15 / 20
48000 / 48000 [ === === === === === === === === === === ] - 14s - loss: 0.0143 - val_loss: 0.0163
Epoch 16 / 20
48000 / 48000 [ === === === === === === === === === === ] - 14s - loss: 0.0141 - val_loss: 0.0152
Epoch 17 / 20
48000 / 48000 [ === === === === === === === === === === ] - 14s - loss: 0.0140 - val_loss: 0.0149
Epoch 18 / 20
48000 / 48000 [ === === === === === === === === === === ] - 14s - loss: 0.0139 - val_loss: 0.0153
Epoch 19 / 20
48000 / 48000 [ === === === === === === === === === === ] - 14s - loss: 0.0138 - val_loss: 0.0152
Epoch 20 / 20
48000 / 48000 [ === === === === === === === === === === ] - 14s - loss: 0.0137 - val_loss: 0.0150
技术支持
训练与验证损失曲线
loss = autoencoder_train. history[ 'loss' ]
val_loss = autoencoder_train. history[ 'val_loss' ]
epochs = range ( epochs)
plt. figure( )
plt. plot( epochs, loss, 'bo' , label= 'Training loss' )
plt. plot( epochs, val_loss, 'b' , label= 'Validation loss' )
plt. title( 'Training and validation loss' )
plt. legend( )
plt. show( )
技术支持
从上述曲线可以直观判断,模型在某些 epoch 会出现过拟合,而大多数时候保持同步。您完全可以通过引入一些复杂度来提升模型表现,以进一步降低损失,或者尝试训练更多 epoch 后再做判断。
在测试数据集上预测
pred = autoencoder. predict( x_test_noisy)
技术支持
plt. figure( figsize= ( 20 , 4 ) )
print ( "Test Images" )
for i in range ( 10 , 20 , 1 ) :
plt. subplot( 2 , 10 , i+ 1 )
plt. imshow( test_data[ i, . . . , 0 ] , cmap= 'gray' )
curr_lbl = test_labels[ i]
plt. title( "(Label: " + str ( label_dict[ curr_lbl] ) + ")" )
plt. show( )
plt. figure( figsize= ( 20 , 4 ) )
print ( "Test Images with Noise" )
for i in range ( 10 , 20 , 1 ) :
plt. subplot( 2 , 10 , i+ 1 )
plt. imshow( x_test_noisy[ i, . . . , 0 ] , cmap= 'gray' )
plt. show( )
plt. figure( figsize= ( 20 , 4 ) )
print ( "Reconstruction of Noisy Test Images" )
for i in range ( 10 , 20 , 1 ) :
plt. subplot( 2 , 10 , i+ 1 )
plt. imshow( pred[ i, . . . , 0 ] , cmap= 'gray' )
plt. show( )
技术支持
Test Images
技术支持
Test Images with Noise
技术支持
Reconstruction of Noisy Test Images
技术支持
看起来,仅用 20 个 epoch,一个相当简单的网络架构就能很好地去除测试图像中的噪声,不是吗?
更进一步!
本教程在理论与实践层面为理解自编码器提供了一个良好起点。如果您能轻松或稍加努力就完成了学习,做得很棒!查看 DataCamp 的Keras 教程:Python 深度学习 以及 Introduction to Deep Learning in Python 课程。
在下一篇教程中,您将学习如何从零开始读取图像,进行分析、预处理,并使用指纹数据集将其输入到模型中。您还将学习如何读取 T-1 模态的医学图像,并使用自编码器对其进行重建!
仍有许多内容值得探索,不妨参加 DataCamp 的Deep Learning in Python 课程?如果您还没有学习过。您将从基础入门,逐步走向掌握深度学习领域;在学习如何使用 Python 构建卷积神经网络、检测人脸与物体等任务时,这将无疑成为一份不可或缺的资源。
通常,自编码器可被视为一种无监督学习技术,因为您不需要显式标签来训练模型。训练自编码器所需的只是原始输入数据。
在本教程中,您将了解深度学习中的自编码器,并在 Python 与 Keras 中实现卷积自编码器和去噪自编码器。您将以 NotMNIST 字母数据集为示例进行实践。
简而言之,今天的教程将涵盖以下主题:
自编码器
如引言所述,自编码器是一种无监督机器学习算法,它以图像为输入,并尝试使用瓶颈(也称潜在空间)中的更少比特进行重建。图像在瓶颈处被大幅压缩。自编码器中的压缩是通过训练网络一段时间实现的,随着学习推进,它会尝试在瓶颈处尽可能好地表示输入图像。普通的图像压缩算法(如 JPEG 与无损 JPEG 压缩)无需任何训练就能压缩图像,并且表现相当不错。
自编码器与主成分分析(PCA)等降维技术类似。它们通过线性变换将数据从高维投射到低维,并在去除非关键部分的同时尽量保留数据的重要特征。
然而,自编码器与 PCA 的主要差异在于变换方式:如前所述,PCA 使用线性变换,而自编码器使用非线性变换。
现在您对自编码器已有初步了解,让我们拆解这个概念,进一步建立直观感受!
上图是一个具有单个隐藏层的两层基础自编码器。在深度学习术语中,统计架构层数时通常不计输入层。架构中的总层数只包含隐藏层数量与输出层。
如上图所示,输入层与输出层具有相同数量的神经元。
举个例子。您将仅含五个像素值的图像输入自编码器,编码器在瓶颈(中间层)或潜在空间中将其压缩为三个像素值。解码器使用这三个值尝试重建五个像素值,或者说重建您输入到网络中的原始图像。
实际中,输入与输出之间往往有更多隐藏层。
自编码器可分为三个部分
自编码器有多种类型,如卷积自编码器、去噪自编码器、变分自编码器以及稀疏自编码器。然而,正如引言所述,本教程将仅聚焦于卷积与去噪这两类。使用 Keras 在 Python 中实现卷积自编码器
由于您的输入数据是图像,使用卷积自编码器是个不错的主意。它不是自编码器的全新变体,而是用卷积层堆叠出的传统自编码器:基本上用卷积层替换全连接层。卷积层配合最大池化层,将输入从“宽”(28 x 28 的图像)且“薄”(单通道或灰度)变为“小”(潜在空间中的 7 x 7 图像)且“厚”(128 个通道)。
如果您尚未完全理解上述思想也不必担心!本教程的第二部分将聚焦于上述内容的实现,希望能澄清您可能存在的所有疑惑。
提示:如果您想进一步了解卷积神经网络,请查看这篇教程。
这有助于网络从图像中提取视觉特征,从而获得更准确的潜在空间表示。重建过程使用上采样与卷积,即解码器;下采样即将图像压缩到低维的过程,也称为编码器。
需要注意的是,编码器主要压缩输入图像,例如:如果您的输入图像尺寸为 176 x 176 x 1(约 30976),那么最大压缩点的尺寸可能为 22 x 22 x 512(约 247808)。因此,在该情况下,您从一个尺寸为 176 x 176 的灰度图像出发,经过若干卷积层以及恰好三个最大池化层,下采样到 22 x 22 的尺寸,但通道数从 1 增至 512。正如上文所述,您的输入从“宽”(176 x 176)且“薄”(1)变为“小”(22 x 22)且“厚”(512)。
加载数据
notMNIST 数据集是一个字母 A 到 J 的字体字形图像识别数据集。它与经典的MNIST 数据集相当相似,后者包含手写数字 0 到 9 的图像:在这里,您会发现 NotMNIST 数据集由 70,000 张 A - J 的 28x28 灰度字母图像组成,共 10 个类别,每个类别 6,000 张图像。
提示:如果您想学习如何使用 MNIST 数据集实现用于分类任务的多层感知机(MLP),请查看这篇教程。
NotMNIST 数据集并未在 Keras 或 TensorFlow 框架中预置,因此您需要从此来源下载数据。数据将以
ubyte.gzip格式下载,但无需担心!您很快就会学会如何读取字节流格式并将其转换为 NumPy 数组。那就开始吧!网络将在 Nvidia Tesla K40 上训练,因此如果您在 GPU 上训练并使用 Jupyter Notebook,您需要添加三行代码,通过
os模块指定 CUDA 设备顺序与可见设备。在下面的代码中,您基本是在 Notebook 中使用
os.environ设置环境变量。最好在初始化 Keras 之前执行以下操作,以限制 Keras 后端 TensorFlow 使用第一个 GPU。如果您所用机器的 GPU 在0,请确保使用0而不是1。您可以在终端运行简单命令检查,例如nvidia-smi接下来,导入所有所需模块,如
numpy、matplotlib,以及最重要的keras,因为本教程将使用该框架!这里,您定义了一个函数用于打开
gzip文件,并用bytestream.read()读取文件。您将图像尺寸与图像总数传给该函数。然后使用np.frombuffer()将存储在变量buf中的字符串转换为float32类型的 NumPy 数组。接着,将数组重塑为三维数组或张量,其中第一维为图像数量,第二与第三维为图像尺寸。最后返回 NumPy 数组
data。现在,通过传入训练与测试文件及其对应的图像数量来调用
extract_data()函数:类似地,您定义一个提取标签的函数,打开
gzip文件并使用bytestream.read()读取文件,向其传入标签维度(1)与图像总数。随后,使用np.frombuffer()将变量buf中的字符串转换为int64类型的 NumPy 数组。这次无需重塑数组,因为变量
labels将返回维度为 60,000 x 1 的列向量。最后返回 NumPy 数组labels。现在通过传入训练与测试标签文件及其对应的图像数量来调用提取标签函数:
加载了训练与测试数据后,便可着手分析数据,以便对今天将要处理的数据集建立一些直觉!
数据探索
现在让我们分析数据集中图像的外观,并借助 NumPy 数组属性
.shape查看图像的维度:从上述输出可见,训练数据形状为 60000 x 28 x 28,因为有 60,000 个训练样本,每个为 28 x 28 的矩阵。类似地,测试数据形状为 10000 x 28 x 28,因为有 10,000 个测试样本。
注意,在本任务中,您不会使用训练与测试标签。当前任务仅处理训练与测试图像。不过,出于探索目的(有助于更好地理解数据),您将使用这些标签。
让我们创建一个字典,包含类别名称及其对应的类别标签:
现在,让我们看看数据集中几张图像:
上述两幅图的输出分别是来自训练与测试数据的样本图像,这些图像被分别赋予
5或F,以及3或D的类别标签。类似地,其他字母会有不同标签,而相同字母会有相同标签。这意味着所有 6,000 张类别为F的图像都将拥有类别标签5。数据预处理
该数据集中的图像确为灰度图像,像素值范围为 0 到 255,尺寸为 28 x 28,因此在将数据输入模型之前进行预处理非常重要。您将首先把训练与测试集的每一张 28 x 28 图像转换为 28 x 28 x 1 的矩阵,以便输入网络:
接下来,您需要检查训练与测试 NumPy 数组的数据类型,应为 float32;若不是,需要进行转换。但由于您在读取数据时已完成转换,因此无需再次处理。同时,您还需要将像素值缩放到 0 - 1(含)范围内。我们来做这一步!
别忘了验证训练与测试数据类型:
接下来,使用训练与测试数据的最大像素值对其进行缩放:
验证缩放后的训练与测试数据的最大值应为 1.0!
完成上述步骤后,划分数据也很重要。为了使模型具有良好的泛化能力,您将训练数据划分为两部分:训练集与验证集。您将在 80% 的数据上训练模型,并在剩余 20% 的训练数据上进行验证。
这也有助于降低过拟合的可能性,因为您会在训练阶段未见过的数据上验证模型。
您可以使用 scikit-learn 的
train_test_split模块来正确划分数据:注意,在本任务中您不需要训练与测试标签。这就是为什么会将训练图像传入两次。训练图像既作为输入也作为类似分类任务中标签的“真值”。
现在,您已经准备好定义网络并将数据输入网络。事不宜迟,我们进入下一步!
卷积自编码器
图像大小为 28 x 28 x 1,或 784 维向量。您将图像矩阵转换为数组,将其缩放到 0 到 1 之间,重塑为 28 x 28 x 1 的尺寸,并将其作为输入馈入网络。
此外,您将使用批大小 128;使用更大的批大小 256 或 512 也可取,具体取决于训练所用系统。批大小对学习参数的确定与预测精度影响很大。您将训练网络 50 个 epoch。
如前所述,自编码器分为两个部分:编码器与解码器。
编码器
解码器
最大池化层每次使用时会将输入下采样为原来的一半,而上采样层每次使用时会将输入上采样为原来的两倍。
注意:卷积核数量、卷积核大小、层数、训练轮数等都是超参数,应基于您的直觉来决定,您可以自由地通过调整这些超参数来尝试新实验并评估模型性能。通过这种方式,您将逐步掌握深度学习的“技艺”!
创建模型后,需要进行编译,并将优化器设为 RMSProp。
提示:如果想进一步了解 RMSProp,请观看 Andrew Ng 的这个教程。
注意,您还需要通过
loss参数指定损失类型。在本例中使用均方误差,因为每个批次后的损失将基于预测输出与真值之间的逐像素均方误差计算:让我们使用 summary 函数可视化上一步中创建的各层,它将显示每层的参数数量(权重与偏置)以及模型的总参数量。
终于可以用 Keras 的
fit()函数训练模型了!模型将训练 50 个 epoch。fit()会返回一个 history 对象;将其结果存储在 fashion_train(此处为 autoencoder_train)后,您可以用它来绘制训练与验证损失曲线,从而直观分析模型表现。训练模型
太好了!您已在 Not-MNIST 数据上训练了模型 50 个 epoch。现在,让我们绘制训练与验证数据之间的损失曲线,以可视化模型性能。
训练与验证损失曲线
可以看到,验证损失与训练损失基本保持同步。这表明模型没有过拟合:验证损失在下降而非上升,且训练与验证损失之间几乎没有间隙。
因此,可以认为模型的泛化能力良好。
最后,是时候使用 Keras 的
predict()函数重建测试图像,看看模型在测试数据上的重建效果如何。在测试数据上预测
您将对全部 10,000 张测试图像进行预测,并绘制部分重建图像,以可视化模型对测试图像的重建质量。
从上述图中可以观察到,模型在重建您使用模型预测的测试图像方面表现出色。至少从视觉上看,测试图像与重建图像几乎完全相同。
去噪自编码器
去噪自编码器尝试学习一种对噪声具有鲁棒性的表示(潜在空间或瓶颈)。您为图像添加噪声,然后将含噪图像作为输入馈入网络的编码器部分。自编码器的编码器部分将图像变换到一个尽量保留字母信息、但去除噪声的空间。
但它究竟如何去除噪声呢?
在训练过程中,您会定义一个损失函数,类似于之前卷积自编码器中定义的均方根误差。在训练的每次迭代中,网络会计算解码器输出的含噪图像与真值(无噪图像)之间的损失,并尝试最小化重建图像与原始无噪图像之间的差异。换言之,网络将学习一个 7 x 7 x 128 的空间,该空间是您用于训练数据的无噪编码!
去噪自编码器的实现
现在,为了在 Python 中看看它如何工作,您将继续使用本教程第一部分中的 NotMNIST 数据集。这意味着无需再次进行数据预处理,因为之前已经完成!不过,本部分一个重要的预处理步骤是为训练、验证与测试图像添加噪声。我们先快速完成这一步!
为图像添加噪声
先定义一个噪声因子,这是一个超参数。噪声因子会与一个均值为 0.0、标准差为 1.0 的随机矩阵相乘。该矩阵将从正态(高斯)分布中采样。随机正态数组的形状将与您要添加噪声的数据形状相同。
为便于理解,举个例子:变量
train_X的形状为 48000 x 28 x 28 x 1。因此,随机正态数组的形状也应与train_X相同,只有这样才能将两个数组相加,因为它们的维度一致。np.clip()会将所有负值阈值化为 0,将所有大于 1 的值阈值化为 1。因为您希望像素值位于 0 与 1 之间。向数据注入噪声后像素值范围可能会有所改变,因此为安全起见,对像素值进行裁剪是个好习惯。可视化含噪图像
现在您已有含噪数据,可以将其输入网络,看看图像中的噪声是如何被“神奇地”去除的!
去噪自编码器网络
如前所述,自编码器分为两部分:编码器与解码器。您将构建的架构如下:
编码器
解码器
训练
如果您还记得在训练卷积自编码器时,您将训练图像输入了两次,因为输入与真值相同。然而,在去噪自编码器中,您将含噪图像作为输入,而真值仍为您施加噪声之前的无噪图像。只有这样,网络才能在含噪图像与真实无噪图像之间计算损失。
训练与验证损失曲线
从上述曲线可以直观判断,模型在某些 epoch 会出现过拟合,而大多数时候保持同步。您完全可以通过引入一些复杂度来提升模型表现,以进一步降低损失,或者尝试训练更多 epoch 后再做判断。
在测试数据集上预测
看起来,仅用 20 个 epoch,一个相当简单的网络架构就能很好地去除测试图像中的噪声,不是吗?
更进一步!
本教程在理论与实践层面为理解自编码器提供了一个良好起点。如果您能轻松或稍加努力就完成了学习,做得很棒!查看 DataCamp 的Keras 教程:Python 深度学习 以及 Introduction to Deep Learning in Python 课程。
在下一篇教程中,您将学习如何从零开始读取图像,进行分析、预处理,并使用指纹数据集将其输入到模型中。您还将学习如何读取 T-1 模态的医学图像,并使用自编码器对其进行重建!
仍有许多内容值得探索,不妨参加 DataCamp 的Deep Learning in Python 课程?如果您还没有学习过。您将从基础入门,逐步走向掌握深度学习领域;在学习如何使用 Python 构建卷积神经网络、检测人脸与物体等任务时,这将无疑成为一份不可或缺的资源。