dl-tf-2e-zh-merge-1

49 阅读55分钟

TensorFlow 深度学习中文第二版(二)

原文:Deep Learning with TensorFlow Second Edition

协议:CC BY-NC-SA 4.0

四、CNN 实战

以前面提到的5×5输入矩阵为例,CNN 的输入层由 25 个神经元(5×5)组成,其任务是获取与每个像素对应的输入值并将其转移到下一层。

在多层网络中,输入层中所有神经元的输出将连接到隐藏层(完全连接层)中的每个神经元。然而,在 CNN 网络中,上面定义的连接方案和我们要描述的卷积层是显着不同的。正如您可能猜到的,这是层的主要类型:在 CNN 中使用这些层中的一个或多个是必不可少的。

在卷积层中,每个神经元连接到输入区域的某个区域,称为感受野。例如,使用3×3内核滤波器,每个神经元将具有偏置并且 9 个权重(3×3)连接到单个感受野。为了有效地识别图像,我们需要将各种不同的内核过滤器应用于相同的感受野,因为每个过滤器应该识别来自图像的不同特征。识别相同特征的神经元集定义了单个特征映射。

下图显示了运行中的 CNN 架构:28×28输入图像将由一个由28x28x32特征映射组成的卷积层进行分析。该图还显示了一个感受野和一个3×3内核过滤器:

CNNs in action

图 5:CNN 正在运行中

CNN 可以由级联连接的若干卷积层组成。每个卷积层的输出是一组特征映射(每个都由单个内核过滤器生成)。这些矩阵中的每一个都定义了将由下一层使用的新输入。

通常,在 CNN 中,每个神经元产生高达激活阈值的输出,该激活阈值与输入成比例并且不受限制。

CNN 还使用位于卷积层之后的池化层。池化层将卷积区域划分为子区域。然后,池化层选择单个代表值(最大池化或平均池化)以减少后续层的计算时间并增加特征相对于其空间位置的稳健性。卷积网络的最后一层通常是完全连接的网络,具有用于输出层的 softmax 激活函数。在接下来的几节中,将详细分析最重要的 CNN 的架构。

LeNet5

LeNet5 CNN 架构由 Yann LeCun 于 1998 年发明,是第一个 CNN。它是一个多层前馈网络,专门用于对手写数字进行分类。它被用于 LeCun 的实验,由七层组成,包含可训练的权重。 LeNet5 架构如下所示:

LeNet5

图 6:LeNet5 网络

LeNet5 架构由三个卷积层和两个交替序列池化层组成。最后两层对应于传统的完全连接的神经网络,即完全连接的层,后面是输出层。输出层的主要功能是计算输入向量和参数向量之间的欧几里德距离。输出函数识别输入模式和我们模型的测量值之间的差异。输出保持最小,以实现最佳模型。因此,完全连接的层被配置为使得输入模式和我们的模型的测量之间的差异最小化。虽然它在 MNIST 数据集上表现良好,但是在具有更高分辨率和更多类别的更多图像的数据集上表现下降。

注意

有关 LeNet 系列模型的基本参考,请参见此链接

逐步实现 LeNet-5

在本节中,我们将学习如何构建 LeNet-5 架构来对 MNIST 数据集中的图像进行分类。下图显示了数据如何在前两个卷积层中流动:使用滤波器权重在第一个卷积层中处理输入图像。这导致 32 个新图像,一个用于卷积层中的每个滤波器。图像也通过合并操作进行下采样,因此图像分辨率从28×28降低到14×14。然后在第二卷积层中处理这 32 个较小的图像。我们需要为这 32 个图像中的每一个再次使用滤波器权重,并且我们需要该层的每个输出通道的滤波器权重。通过合并操作再次对图像进行下采样,使得图像分辨率从14×14减小到7×7。此卷积层的特征总数为 64。

Implementing a LeNet-5 step by step

图 7:前两个卷积层的数据流

通过(3×3)第三卷积层再次过滤 64 个结果图像。没有对该层应用池操作。第三卷积层的输出是128×7×7像素图像。然后将这些图像展平为单个向量,长度为4×4×128 = 2048,其用作完全连接层的输入。

LeNet-5 的输出层由 625 个神经元作为输入(即完全连接层的输出)和 10 个神经元作为输出,用于确定图像的类别,该数字在图片。

Implementing a LeNet-5 step by step

图 8:最后三个卷积层的数据流

卷积滤波器最初是随机选择的。输入图像的预测类和实际类之间的差异被称为成本函数,并且这使我们的网络超出训练数据。然后,优化器会自动通过 CNN 传播此成本函数,并更新过滤器权重以改善分类误差。这反复进行数千次,直到分类误差足够低。

现在让我们详细看看如何编写我们的第一个 CNN。让我们首先导入我们实现所需的 TensorFlow 库:

import tensorflow as tf
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data

设置以下参数。它们表示在训练阶段(128)和测试阶段(256)使用的样本数量:

batch_size = 128
test_size = 256

当我们定义以下参数时,该值为28,因为 MNIST 图像的高度和宽度为28像素:

img_size = 28

对于类的数量,值10意味着我们将为每个 0 到 9 位数设置一个类:

num_classes = 10

为输入图像定义占位符变量X。该张量的数据类型设置为float32,形状设置为[None, img_size, img_size, 1],其中None表示张量可以保存任意数量的图像:

X = tf.placeholder("float", [None, img_size, img_size, 1])

然后我们为占位符变量X中的输入图像正确关联的标签设置另一个占位符变量Y。此占位符变量的形状为[None, num_classes],这意味着它可以包含任意数量的标签。每个标签都是长度为num_classes的向量,在这种情况下为10

Y = tf.placeholder("float", [None, num_classes])

我们收集MNIST数据,这些数据将被复制到数据文件夹中:

mnist = input_data.read_data_sets("MNIST-data", one_hot=True)

我们构建训练数据集(trXtrY)和测试网络(teXteY)

trX, trY, teX, teY = mnist.train.images, \
                     mnist.train.labels, \
                     mnist.test.images,  \
                     mnist.test.labels

必须重新整形trXteX图像集以匹配输入形状:

trX = trX.reshape(-1, img_size, img_size, 1)
teX = teX.reshape(-1, img_size, img_size, 1)

我们现在开始定义网络的weights

init_weights函数在提供的形状中构建新变量,并使用随机值初始化网络权重:

def init_weights(shape):
    return tf.Variable(tf.random_normal(shape, stddev=0.01))

第一卷积层的每个神经元被卷积为输入张量的小子集,尺寸为3×3×1。值32只是我们为第一层考虑的特征图的数量。然后定义权重w

w = init_weights([3, 3, 1, 32])

然后输入的数量增加到32,这意味着第二卷积层中的每个神经元被卷积到第一卷积层的3×3×32个神经元。w2权重如下:

w2 = init_weights([3, 3, 32, 64])

64表示获得的输出特征的数量。第三个卷积层被卷积为前一层的3x3x64个神经元,而128是结果特征。

w3 = init_weights([3, 3, 64, 128])

第四层完全连接并接收128x4x4输入,而输出等于625

w4 = init_weights([128 * 4 * 4, 625])

输出层接收625输入,输出是类的数量:

w_o = init_weights([625, num_classes])

请注意,这些初始化实际上并未在此时完成。它们仅在 TensorFlow 图中定义。

p_keep_conv = tf.placeholder("float")
p_keep_hidden = tf.placeholder("float")

是时候定义网络模型了。就像网络的权重定义一样,它将是一个函数。它接收X张量,权重张量和丢弃参数作为卷积和完全连接层的输入:

def model(X, w, w2, w3, w4, w_o, p_keep_conv, p_keep_hidden):

tf.nn.conv2d()执行 TensorFlow 操作进行卷积。请注意,所有尺寸的strides都设置为 1。实际上,第一步和最后一步必须始终为 1,因为第一步是图像编号,最后一步是输入通道。padding参数设置为'SAME',这意味着输入图像用零填充,因此输出的大小相同:

conv1 = tf.nn.conv2d(X, w,strides=[1, 1, 1, 1],\
                         padding='SAME')

然后我们将conv1层传递给 ReLU 层。它为每个输入像素x计算max(x, 0)函数,为公式添加一些非线性,并允许我们学习更复杂的函数:

    conv1_a = tf.nn.relu(conv1)

然后由tf.nn.max_pool运算符合并生成的层:

    conv1 = tf.nn.max_pool(conv1_a, ksize=[1, 2, 2, 1]\
                           ,strides=[1, 2, 2, 1],\
                           padding='SAME')

这是一个2×2最大池,这意味着我们正在检查2×2窗口并在每个窗口中选择最大值。然后我们将 2 个像素移动到下一个窗口。我们尝试通过tf.nn.dropout()函数减少过拟合,我们传递conv1层和p_keep_conv概率值:

    conv1 = tf.nn.dropout(conv1, p_keep_conv)

如您所见,接下来的两个卷积层conv2conv3的定义方式与conv1相同:

    conv2 = tf.nn.conv2d(conv1, w2,\
                         strides=[1, 1, 1, 1],\
                         padding='SAME')
    conv2_a = tf.nn.relu(conv2)
    conv2 = tf.nn.max_pool(conv2, ksize=[1, 2, 2, 1],\
                        strides=[1, 2, 2, 1],\
                        padding='SAME')
    conv2 = tf.nn.dropout(conv2, p_keep_conv)

    conv3=tf.nn.conv2d(conv2, w3,\
                       strides=[1, 1, 1, 1]\
                       ,padding='SAME')

    conv3 = tf.nn.relu(conv3)

完全连接的层将添加到网络中。第一个FC_layer的输入是前一个卷积的卷积层:

    FC_layer = tf.nn.max_pool(conv3, ksize=[1, 2, 2, 1],\
                        strides=[1, 2, 2, 1],\
                        padding='SAME')

    FC_layer = tf.reshape(FC_layer,\
                          [-1, w4.get_shape().as_list()[0]])

dropout函数再次用于减少过拟合:

    FC_layer = tf.nn.dropout(FC_layer, p_keep_conv)

输出层接收FC_layerw4权重张量作为输入。应用 ReLU 和丢弃运算符:

    output_layer = tf.nn.relu(tf.matmul(FC_layer, w4))
    output_layer = tf.nn.dropout(output_layer, p_keep_hidden)

结果是一个长度为 10 的向量。这用于确定图像所属的 10 个输入类中的哪一个:

    result = tf.matmul(output_layer, w_o)
    return result

交叉熵是我们在此分类器中使用的表现指标。交叉熵是一个连续的函数,它总是正的,如果预测的输出与期望的输出完全匹配,则等于零。因此,这种优化的目标是通过改变网络层中的变量来最小化交叉熵,使其尽可能接近零。 TensorFlow 具有用于计算交叉熵的内置函数。请注意,该函数在内部计算 softmax,因此我们必须直接使用py_x的输出:

py_x = model(X, w, w2, w3, w4, w_o, p_keep_conv, p_keep_hidden)
   Y_ = tf.nn.softmax_cross_entropy_with_logits_v2\
        (labels=Y,logits=py_x)

现在我们已经为每个分类图像定义了交叉熵,我们可以衡量模型在每个图像上的表现。我们需要一个标量值来使用交叉熵来优化网络变量,因此我们只需要对所有分类图像求平均交叉熵:

cost = tf.reduce_mean(Y_)

为了最小化评估的cost,我们必须定义一个优化器。在这种情况下,我们将使用RMSPropOptimizer,它是 GD 的高级形式。RMSPropOptimizer实现了 RMSProp 算法,这是一种未发表的自适应学习率方法,由 Geoff Hinton 在他的 Coursera 课程的第 6 讲中提出。

注意

您可以在此链接找到 Geoff Hinton 的课程。

RMSPropOptimizer还将学习率除以梯度平方的指数衰减均值。 Hinton 建议将衰减参数设置为0.9,而学习率的良好默认值为0.001

optimizer = tf.train.RMSPropOptimizer(0.001, 0.9).minimize(cost)

基本上,通用 SGD 算法存在一个问题,即学习率必须以1 / T(其中T是迭代次数)进行缩放以实现收敛。 RMSProp 尝试通过自动调整步长来解决这个问题,以使步长与梯度相同。随着平均梯度变小,SGD 更新中的系数变得更大以进行补偿。

注意

有关此算法的有趣参考可在此处找到

最后,我们定义predict_op,它是模式输出中尺寸最大值的索引:

predict_op = tf.argmax(py_x, 1)

请注意,此时不执行优化。什么都没有计算,因为我们只是将优化器对象添加到 TensorFlow 图中以便以后执行。

我们现在来定义网络的运行会话。训练集中有 55,000 个图像,因此使用所有这些图像计算模型的梯度需要很长时间。因此,我们将在优化器的每次迭代中使用一小批图像。如果您的计算机崩溃或由于 RAM 耗尽而变得非常慢,那么您可以减少此数量,但您可能需要执行更多优化迭代。

现在我们可以继续实现 TensorFlow 会话:

with tf.Session() as sess:
    tf.global_variables_initializer().run()
    for i in range(100):

我们得到了一批训练样例,training_batch张量现在包含图像的子集和相应的标签:

        training_batch =  zip(range(0, len(trX), batch_size),\
                                    range(batch_size, \
                                    len(trX)+1, \
                                    batch_size))

将批量放入feed_dict中,并在图中为占位符变量指定相应的名称。我们现在可以使用这批训练数据运行优化器。 TensorFlow 将馈送中的变量分配给占位符变量,然后运行优化程序:

        for start, end in training_batch:
            sess.run(optimizer, feed_dict={X: trX[start:end],\
                                          Y: trY[start:end],\
                                          p_keep_conv: 0.8,\
                                          p_keep_hidden: 0.5})

同时,我们得到了打乱的一批测试样本:

        test_indices = np.arange(len(teX)) 
        np.random.shuffle(test_indices)
        test_indices = test_indices[0:test_size]

对于每次迭代,我们显示批次的评估accuracy

        print(i, np.mean(np.argmax(teY[test_indices], axis=1) ==\
                         sess.run\
                         (predict_op,\
                          feed_dict={X: teX[test_indices],\
                                     Y: teY[test_indices], \
                                     p_keep_conv: 1.0,\
                                     p_keep_hidden: 1.0})))

根据所使用的计算资源,训练网络可能需要几个小时。我机器上的结果如下:

Successfully downloaded train-images-idx3-ubyte.gz 9912422 bytes.
Successfully extracted to train-images-idx3-ubyte.mnist 9912422 bytes.
Loading ata/train-images-idx3-ubyte.mnist
Successfully downloaded train-labels-idx1-ubyte.gz 28881 bytes.
Successfully extracted to train-labels-idx1-ubyte.mnist 28881 bytes.
Loading ata/train-labels-idx1-ubyte.mnist
Successfully downloaded t10k-images-idx3-ubyte.gz 1648877 bytes.
Successfully extracted to t10k-images-idx3-ubyte.mnist 1648877 bytes.
Loading ata/t10k-images-idx3-ubyte.mnist
Successfully downloaded t10k-labels-idx1-ubyte.gz 4542 bytes.
Successfully extracted to t10k-labels-idx1-ubyte.mnist 4542 bytes.
Loading ata/t10k-labels-idx1-ubyte.mnist
(0, 0.95703125)
(1, 0.98046875)
(2, 0.9921875)
(3, 0.99609375)
(4, 0.99609375)
(5, 0.98828125)
(6, 0.99609375)
(7, 0.99609375)
(8, 0.98828125)
(9, 0.98046875)
(10, 0.99609375)
.
.
.
..
.
(90, 1.0)
(91, 0.9921875)
(92, 0.9921875)
(93, 0.99609375)
(94, 1.0)
(95, 0.98828125)
(96, 0.98828125)
(97, 0.99609375)
(98, 1.0)
(99, 0.99609375)

经过 10,000 次迭代后, 模型的准确率为 99.60%,这还不错!

AlexNet

AlexNet 神经网络是首批实现巨大成功的 CNN 之一。作为 2012 年 ILSVRC 的获胜者,这个神经网络是第一个使用 LeNet-5 网络之前定义的神经网络的标准结构在 ImageNet 等非常复杂的数据集上获得良好结果。

注意

ImageNet 项目是一个大型视觉数据库,设计用于视觉对象识别软件研究。截至 2016 年,ImageNet 手工标注了超过一千万个图像 URL,以指示图像中的对象。在至少一百万个图像中,还提供了边界框。第三方图像 URL 的标注数据库可直接从 ImageNet 免费获得。

AlexNet 的架构如下图所示:

AlexNet

图 9:AlexNet 网络

在 AlexNet 架构中,有八层具有可训练参数:一系列五个连续卷积层,后面是三个完全连接的层。每个卷积层之后是 ReLU 层,并且可选地还有最大池层,尤其是在网络的开始处,以便减少网络占用的空间量。

所有池层都有3x3扩展区域,步长率为 2:这意味着您始终使用重叠池。这是因为与没有重叠的普通池相比,这种类型的池提供了稍好的网络表现。在网络的开始,在池化层和下一个卷积层之间,总是使用几个 LRN 标准化层:经过一些测试,可以看出它们倾向于减少网络误差。

前两个完全连接的层拥有 4,096 个神经元,而最后一个拥有 1,000 个单元,对应于 ImageNet 数据集中的类数。考虑到完全连接层中的大量连接,在每对完全连接的层之间添加了比率为 0.5 的丢弃层,即,每次忽略一半的神经元激活。在这种情况下已经注意到,使用丢弃技术不仅加速了单次迭代的处理,而且还很好地防止了过拟合。没有丢弃层,网络制造商声称原始网络过拟合。

迁移学习

迁移学习包括建立已经构建的网络,并对各个层的参数进行适当的更改,以便它可以适应另一个数据集。例如,您可以在大型数据集(如 ImageNet)上使用预先测试的网络,并在较小的数据集上再次训练它。如果我们的数据集在内容上与原始数据集没有明显不同,那么预先训练的模型已经具有与我们自己的分类问题相关的学习特征。

如果我们的数据集与预训练模型训练的数据集没有太大差异,我们可以使用微调技术。 已在大型不同数据集上进行预训练的模型可能会捕捉到早期层中的曲线和边缘等通用特征,这些特征在大多数分类问题中都是相关且有用的。但是,如果我们的数据集来自一个非常特定的域,并且找不到该域中预先训练好的网络,我们应该考虑从头开始训练网络。

预训练的 AlexNet

我们会对预先训练好的 AlexNet 进行微调,以区分狗和猫。 AlexNet 在 ImageNet 数据集上经过预先训练。

要执行这个例子,你还需要安装 scipy(参见此链接)和 PIL(Pillow),这是 scipy 使用的读取图像:pip install Pillowpip3 install Pillow

然后,您需要下载以下文件:

  • myalexnet_forward.py:2017 版 TensorFlow 的 AlexNet 实现和测试代码(Python 3.5)
  • bvlc_alexnet.npy:权重,需要在工作目录中
  • caffe_classes.py:类,与网络输出的顺序相同
  • poodle.pnglaska.pngdog.pngdog2.pngquail227.JPEG:测试图像(图像应为227×227×3

从此链接下载这些文件,或从本书的代码库中下载。

首先,我们将在之前下载的图像上测试网络。为此,只需从 Python GUI 运行myalexnet_forward.py即可。

通过简单地检查源代码可以看到(参见下面的代码片段),将调用预先训练好的网络对以下两个图像进行分类,laska.pngpoodle.png,这些图像之前已下载过:

im1 = (imread("laska.png")[:,:,:3]).astype(float32)
im1 = im1 - mean(im1)
im1[:, :, 0], im1[:, :, 2] = im1[:, :, 2], im1[:, :, 0]

im2 = (imread("poodle.png")[:,:,:3]).astype(float32)
im2[:, :, 0], im2[:, :, 2] = im2[:, :, 2], im2[:, :, 0]

Pretrained AlexNet

图 10:要分类的图像

的权重和偏置由以下语句加载:

net_data = load(open("bvlc_alexnet.npy", "rb"), encoding="latin1").item()

网络是一组卷积和池化层,后面是三个完全连接的状态。该模型的输出是 softmax 函数:

prob = tf.nn.softmax(fc8)

softmax 函数的输出是分类等级,因为它们表示网络认为输入图像属于caffe_classes.py文件中定义的类的强度。

如果我们运行代码,我们应该得到以下结果:

Image 0
weasel 0.503177
black-footed ferret, ferret, Mustela nigripes 0.263265
polecat, fitch, foulmart, foumart, Mustela putorius 0.147746
mink 0.0649517
otter 0.00771955
Image 1
clumber, clumber spaniel 0.258953
komondor 0.165846
miniature poodle 0.149518
toy poodle 0.0984719
kuvasz 0.0848062
0.40007972717285156
>>>

在前面的例子中,AlexNet 给鼬鼠的分数约为 50%。这意味着该模型非常有信心图像显示黄鼠狼,其余分数可视为噪音。

数据集准备

我们的任务是建立一个区分狗和猫的图像分类器。我们从 Kaggle 那里得到一些帮助,我们可以从中轻松下载数据集

在此数据集中,训练集包含 20,000 个标记图像,测试和验证集包含 2,500 个图像。

要使用数据集,必须将每个图像重新整形为227×227×3。为此,您可以使用prep_images.py中的 Python 代码。否则,您可以使用本书仓库中的trainDir.rartestDir.rar文件。它们包含 6,000 个用于训练的犬和猫的重塑图像,以及 100 个重新成形的图像用于测试。

以下部分中描述的以下微调实现在alexnet_finetune.py中实现,可以在本书的代码库中下载。

微调的实现

我们的分类任务包含两个类别,因此网络的新 softmax 层将包含 2 个类别而不是 1,000 个类别。这是输入张量,它是一个227×227×3图像,以及等级 2 的输出张量:

n_classes = 2
train_x = zeros((1, 227,227,3)).astype(float32)
train_y = zeros((1, n_classes))

微调实现包括截断预训练网络的最后一层(softmax 层),并将其替换为与我们的问题相关的新 softmax 层。

例如,ImageNet 上预先训练好的网络带有一个包含 1,000 个类别的 softmax 层。

以下代码片段定义了新的 softmax 层fc8

fc8W = tf.Variable(tf.random_normal\
                   ([4096, n_classes]),\
                   trainable=True, name="fc8w")
fc8b = tf.Variable(tf.random_normal\
                   ([n_classes]),\
                   trainable=True, name="fc8b")
fc8 = tf.nn.xw_plus_b(fc7, fc8W, fc8b)
prob = tf.nn.softmax(fc8)

损失是用于分类的表现指标。它是一个始终为正的连续函数,如果模型的预测输出与期望的输出完全匹配,则交叉熵等于零。因此,优化的目标是通过改变模型的权重和偏置来最小化交叉熵,因此它尽可能接近零。

TensorFlow 具有用于计算交叉熵的内置函数。为了使用交叉熵来优化模型的变量,我们需要一个标量值,因此我们只需要对所有图像分类采用交叉熵的平均值:

loss = tf.reduce_mean\
       (tf.nn.softmax_cross_entropy_with_logits_v2\
        (logits =prob, labels=y))
opt_vars = [v for v in tf.trainable_variables()\
            if (v.name.startswith("fc8"))]

既然我们必须最小化成本度量,那么我们可以创建optimizer

optimizer = tf.train.AdamOptimizer\
            (learning_rate=learning_rate).minimize\
            (loss, var_list = opt_vars)
correct_pred = tf.equal(tf.argmax(prob, 1), tf.argmax(y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))

在这种情况下,我们使用步长为0.5AdamOptimizer。请注意,此时不执行优化。事实上,根本没有计算任何东西,我们只需将优化器对象添加到 TensorFlow 图中以便以后执行。然后我们在网络上运行反向传播以微调预训练的权重:

batch_size = 100
training_iters = 6000
display_step = 1
dropout = 0.85 # Dropout, probability to keep units

init = tf.global_variables_initializer()
with tf.Session() as sess:
    sess.run(init)
    step = 1

继续训练,直到达到最大迭代次数:

    while step * batch_size < training_iters:
        batch_x, batch_y = \
                 next(next_batch(batch_size)) #.next()

运行优化操作(反向传播):

        sess.run(optimizer, \
                 feed_dict={x: batch_x, \
                            y: batch_y, \
                            keep_prob: dropout})

        if step % display_step == 0:

计算批次损失和准确率:

            cost, acc = sess.run([loss, accuracy],\
                                 feed_dict={x: batch_x, \
                                            y: batch_y, \
                                            keep_prob: 1.})
            print ("Iter " + str(step*batch_size) \
                   + ", Minibatch Loss= " + \
                  "{:.6f}".format(cost) + \
                   ", Training Accuracy= " + \
                  "{:.5f}".format(acc))              

        step += 1
    print ("Optimization Finished!")

网络训练产生以下结果:

Iter 100, Minibatch Loss= 0.555294, Training Accuracy= 0.76000
Iter 200, Minibatch Loss= 0.584999, Training Accuracy= 0.73000
Iter 300, Minibatch Loss= 0.582527, Training Accuracy= 0.73000
Iter 400, Minibatch Loss= 0.610702, Training Accuracy= 0.70000
Iter 500, Minibatch Loss= 0.583640, Training Accuracy= 0.73000
Iter 600, Minibatch Loss= 0.583523, Training Accuracy= 0.73000
…………………………………………………………………
…………………………………………………………………
Iter 5400, Minibatch Loss= 0.361158, Training Accuracy= 0.95000
Iter 5500, Minibatch Loss= 0.403371, Training Accuracy= 0.91000
Iter 5600, Minibatch Loss= 0.404287, Training Accuracy= 0.91000
Iter 5700, Minibatch Loss= 0.413305, Training Accuracy= 0.90000
Iter 5800, Minibatch Loss= 0.413816, Training Accuracy= 0.89000
Iter 5900, Minibatch Loss= 0.413476, Training Accuracy= 0.90000
Optimization Finished!

要测试我们的模型,我们将预测与标签集(cat = 0dog = 1)进行比较:

    output = sess.run(prob, feed_dict = {x:imlist, keep_prob: 1.})
    result = np.argmax(output,1)
    testResult = [1,1,1,1,0,0,0,0,0,0,\
                  0,1,0,0,0,0,1,1,0,0,\
                  1,0,1,1,0,1,1,0,0,1,\
                  1,1,1,0,0,0,0,0,1,0,\
                  1,1,1,1,0,1,0,1,1,0,\
                  1,0,0,1,0,0,1,1,1,0,\
                  1,1,1,1,1,0,0,0,0,0,\
                  0,1,1,1,0,1,1,1,1,0,\
                  0,0,1,0,1,1,1,1,0,0,\
                  0,0,0,1,1,0,1,1,0,0]
    count = 0
    for i in range(0,99):
        if result[i] == testResult[i]:
            count=count+1

    print("Testing Accuracy = " + str(count) +"%")

最后,我们有我们模型的准确率:

Testing Accuracy = 82%

VGG

VGG 是在 2014 年 ILSVRC 期间发明神经网络的人的名字。我们谈论的是复数网络,因为创建了同一网络的多个版本,每个拥有不同数量的层。根据层数n,这些网络中的一个具有的权重,它们中的每一个通常称为 VGG-n。所有这些网络都比 AlexNet 更深。这意味着它们由多个层组成,其参数比 AlexNet 更多,在这种情况下,总共有 11 到 19 个训练层。通常,只考虑可行的层,因为它们会影响模型的处理和大小,如前一段所示。然而,整体结构仍然非常相似:总是有一系列初始卷积层和最后一系列完全连接的层,后者与 AlexNet 完全相同。因此,使用的卷积层的数量,当然还有它们的参数有什么变化。下表显示了 VGG 团队构建的所有变体。

每一列,从左侧开始,向右侧,显示一个特定的 VGG 网络,从最深到最浅。粗体项表示与先前版本相比,每个版本中添加的内容。 ReLU 层未在表中显示,但在网络中它存在于每个卷积层之后。所有卷积层使用 1 的步幅:

VGG

表:VGG 网络架构

请注意, AlexNet 没有具有相当大的感受野的卷积层:这里,所有感受野都是3×3,除了 VGG-16 中有几个具有1×1感受野的卷积层。回想一下,具有 1 步梯度的凸层不会改变输入空间大小,同时修改深度值,该深度值与使用的内核数量相同。因此,VGG 卷积层不会影响输入体积的宽度和高度;只有池化层才能这样做。使用具有较小感受野的一系列卷积层的想法最终总体上模拟具有较大感受野的单个卷积层,这是由于这样的事实,即以这种方式使用多个 ReLU 层而不是单独使用一个,从而增加激活函数的非线性,从而使其更具区别性。它还用于减少使用的参数数量。这些网络被认为是 AlexNet 的演变,因为总体而言,使用相同的数据集,它们的表现优于 AlexNet。 VGG 网络演示的主要概念是拥塞神经网络越来越深刻,其表现也越来越高。但是, 必须拥有越来越强大的硬件,否则网络训练就会成为问题。

对于 VGG,使用了四个 NVIDIA Titan Blacks,每个都有 6 GB 的内存。因此,VGG 具有更好的表现,但需要大量的硬件用于训练,并且还使用大量参数:例如,VGG-19 模型大约为 550MB(是 AlexNet 的两倍)。较小的 VGG 网络仍然具有大约 507MB 的模型。

用 VGG-19 学习艺术风格

在这个项目中,我们使用预训练的 VGG-19 来学习艺术家创建的样式和模式,并将它们转移到图像中(项目文件是style_transfer.py,在本书的 GitHub 仓库中)。这种技术被称为artistic style learning参见 Gatys 等人的文章 A Art Algorithm of Artistic Style)。根据学术文献,艺术风格学习定义如下:给定两个图像作为输入,合成具有第一图像的语义内容和第二图像的纹理/风格的第三图像。

为了使其正常工作,我们需要训练一个深度卷积神经网络来构建以下内容:

  • 用于确定图像 A 内容的内容提取器

  • 用于确定图像 B 样式的样式提取器

  • 合并器将一些任意内容与另一个任意样式合并,以获得最终结果

    Artistic style learning with VGG-19

    图 11:艺术风格学习操作模式

输入图像

输入图像,每个都是478×478像素,是您在本书的代码库中也可以找到的以下图像(cat.jpgmosaic.jpg):

Input images

图 12:艺术风格学习中的输入图像

为了通过 VGG 模型分析 ,需要对这些图像进行预处理:

  1. 添加额外的维度

  2. 从输入图像中减去MEAN_VALUES

    MEAN_VALUES = np.array([123.68, 116.779, 103.939]).reshape((1,1,1,3))
    content_image = preprocess('cat.jpg')
    style_image = preprocess('mosaic.jpg') 
    
    def preprocess(path):
        image = plt.imread(path)
        image = image[np.newaxis]
        image = image - MEAN_VALUES
        return image
    

内容提取器和损失

为了隔离图像的语义内容,我们使用预先训练好的 VGG-19 神经网络,对权重进行了一些微调,以适应这个问题,然后使用其中一个隐藏层的输出作为内容提取器。下图显示了用于此问题的 CNN:

Content extractor and loss

图 13:用于艺术风格学习的 VGG-19

使用以下代码加载预训练的 VGG:

import scipy.io
vgg = scipy.io.loadmat('imagenet-vgg-verydeep-19.mat')

imagenet-vgg-verydeep-19.mat模型应从此链接下载。

该模型有 43 层,其中 19 层是卷积层。其余的是最大池/激活/完全连接的层。

我们可以检查每个卷积层的形状:

 [print (vgg_layers[0][i][0][0][2][0][0].shape,\
        vgg_layers[0][i][0][0][0][0]) for i in range(43) 
 if 'conv' in vgg_layers[0][i][0][0][0][0] \
 or 'fc' in vgg_layers[0][i][0][0][0][0]]

上述代码的结果如下:

(3, 3, 3, 64) conv1_1
(3, 3, 64, 64) conv1_2
(3, 3, 64, 128) conv2_1
(3, 3, 128, 128) conv2_2
(3, 3, 128, 256) conv3_1
(3, 3, 256, 256) conv3_2
(3, 3, 256, 256) conv3_3
(3, 3, 256, 256) conv3_4
(3, 3, 256, 512) conv4_1
(3, 3, 512, 512) conv4_2
(3, 3, 512, 512) conv4_3
(3, 3, 512, 512) conv4_4
(3, 3, 512, 512) conv5_1
(3, 3, 512, 512) conv5_2
(3, 3, 512, 512) conv5_3
(3, 3, 512, 512) conv5_4
(7, 7, 512, 4096) fc6
(1, 1, 4096, 4096) fc7
(1, 1, 4096, 1000) fc8

每种形状以下列方式表示:[kernel height, kernel width, number of input channels, number of output channels]

第一层有 3 个输入通道,因为输入是 RGB 图像,而卷积层的输出通道数从 64 到 512,所有内核都是3x3矩阵。

然后我们应用转移学习技术,以使 VGG-19 网络适应我们的问题:

  1. 不需要完全连接的层,因为它们用于对象识别。

  2. 最大池层代替平均池层,以获得更好的结果。平均池层的工作方式与卷积层中的内核相同。

    IMAGE_WIDTH = 478
    IMAGE_HEIGHT = 478
    INPUT_CHANNELS = 3
    model = {}
    model['input'] = tf.Variable(np.zeros((1, IMAGE_HEIGHT,\
                                     IMAGE_WIDTH,\
                                     INPUT_CHANNELS)),\
                                   dtype = 'float32')
    
    model['conv1_1']  = conv2d_relu(model['input'], 0, 'conv1_1')
    model['conv1_2']  = conv2d_relu(model['conv1_1'], 2, 'conv1_2')
    model['avgpool1'] = avgpool(model['conv1_2'])
    
    model['conv2_1']  = conv2d_relu(model['avgpool1'], 5, 'conv2_1')
    model['conv2_2']  = conv2d_relu(model['conv2_1'], 7, 'conv2_2')
    model['avgpool2'] = avgpool(model['conv2_2'])
    
    model['conv3_1']  = conv2d_relu(model['avgpool2'], 10, 'conv3_1')
    model['conv3_2']  = conv2d_relu(model['conv3_1'], 12, 'conv3_2')
    model['conv3_3']  = conv2d_relu(model['conv3_2'], 14, 'conv3_3')
    model['conv3_4']  = conv2d_relu(model['conv3_3'], 16, 'conv3_4')
    model['avgpool3'] = avgpool(model['conv3_4'])
    
    model['conv4_1']  = conv2d_relu(model['avgpool3'], 19,'conv4_1')
    model['conv4_2']  = conv2d_relu(model['conv4_1'], 21, 'conv4_2')
    model['conv4_3']  = conv2d_relu(model['conv4_2'], 23, 'conv4_3')
    model['conv4_4']  = conv2d_relu(model['conv4_3'], 25,'conv4_4')
    model['avgpool4'] = avgpool(model['conv4_4'])
    
    model['conv5_1']  = conv2d_relu(model['avgpool4'], 28, 'conv5_1')
    model['conv5_2']  = conv2d_relu(model['conv5_1'], 30, 'conv5_2')
    model['conv5_3']  = conv2d_relu(model['conv5_2'], 32, 'conv5_3')
    model['conv5_4']  = conv2d_relu(model['conv5_3'], 34, 'conv5_4')
    model['avgpool5'] = avgpool(model['conv5_4'])
    

这里我们定义了contentloss函数来测量两个图像px之间的内容差异:

def contentloss(p, x):
    size = np.prod(p.shape[1:])
    loss = (1./(2*size)) * tf.reduce_sum(tf.pow((x - p),2))
    return loss

当输入图像在内容方面彼此非常接近并且随着其内容偏离而增长时,该函数倾向于为 0。

我们将在conv5_4层上使用contentloss。这是输出层,其输出将是预测,因此我们需要使用contentloss函数将此预测与实际预测进行比较:

content_loss = contentloss\
               (sess.run(model['conv5_4']), model['conv5_4'])

最小化content_loss意味着混合图像在给定层中具有与内容图像的激活非常相似的特征激活。

样式提取器和损失

样式提取器使用过滤器的 Gram 矩阵作为给定的隐藏层。简单来说,使用这个矩阵,我们可以破坏图像的语义,保留其基本组件并使其成为一个好的纹理提取器:

def gram_matrix(F, N, M):
    Ft = tf.reshape(F, (M, N))
    return tf.matmul(tf.transpose(Ft), Ft)

style_loss测量两个图像彼此之间的接近程度。此函数是样式图像和输入noise_image生成的 Gram 矩阵元素的平方差的总和:

noise_image = np.random.uniform\
              (-20, 20,\
               (1, IMAGE_HEIGHT, \
                IMAGE_WIDTH,\
                INPUT_CHANNELS)).astype('float32')

def style_loss(a, x):
    N = a.shape[3]
    M = a.shape[1] * a.shape[2]
    A = gram_matrix(a, N, M)
    G = gram_matrix(x, N, M)
    result = (1/(4 * N**2 * M**2))* tf.reduce_sum(tf.pow(G-A,2))
    return result

style_loss生长 ,因为它的两个输入图像(ax)倾向于偏离风格。

合并和总损失

我们可以合并内容和样式损失,以便训练输入noise_image来输出(在层中)与样式图像类似的样式,其特征相似于内容图像:

alpha = 1
beta = 100
total_loss = alpha * content_loss + beta * styleloss

训练

最小化网络中的损失,以便样式损失(输出图像的样式和样式图像的样式之间的损失),内容损失(内容图像和输出图像之间的损失),以及总变异损失尽可能低:

train_step = tf.train.AdamOptimizer(1.5).minimize(total_loss)

从这样的网络生成的输出图像应该类似于输入图像并且具有样式图像的造型师属性。

最后,我们可以准备网络进行训练:

sess.run(tf.global_variables_initializer())
sess.run(model['input'].assign(input_noise))
for it in range(2001):
    sess.run(train_step)
    if it%100 == 0:
        mixed_image = sess.run(model['input'])
        print('iteration:',it,'cost: ', sess.run(total_loss))
        filename = 'out2/%d.png' % (it)
        deprocess(filename, mixed_image)

训练时间可能非常耗时,但结果可能非常有趣:

iteration: 0 cost:  8.14037e+11
iteration: 100 cost:  1.65584e+10
iteration: 200 cost:  5.22747e+09
iteration: 300 cost:  2.72995e+09
iteration: 400 cost:  1.8309e+09
iteration: 500 cost:  1.36818e+09
iteration: 600 cost:  1.0804e+09
iteration: 700 cost:  8.83103e+08
iteration: 800 cost:  7.38783e+08
iteration: 900 cost:  6.28652e+08
iteration: 1000 cost:  5.41755e+08

经过 1000 次迭代后,我们创建了一个新的拼接:

Training

图 14:艺术风格学习中的输出图像

真是太棒了!你终于可以训练你的神经网络像毕加索一样画画......玩得开心!

Inception-v3

Szegedy 和其他人在 2014 年的论文“Going Deeper with Convolutions”中首次介绍了 Inception 微架构:

Inception-v3

图 15:GoogLeNet 中使用的 Original Inception 模块

初始模块的目标是通过在网络的同一模块内计算1×13×35×5卷积来充当多级特征提取器 - 这些滤波器的输出然后,在被馈送到网络中的下一层之前,沿着信道维度堆叠。这种架构的原始版本称为 GoogLeNet,但后续形式简称为 InceptionVN,其中 N 表示 Google 推出的版本号。

您可能想知道为什么我们在同一输入上使用不同类型的卷积。答案是,只要仔细研究了它的参数,就不可能总是获得足够的有用特征来用单个卷积进行精确分类。事实上,使用一些输入它可以更好地使用卷积小内核,而其他输入可以使用其他类型的内核获得更好的结果。可能由于这个原因,GoogLeNet 团队想要在他们自己的网络中考虑一些替代方案。如前所述,为此目的,GoogLeNet 在同一网络级别(即它们并行)使用三种类型的卷积层:1×1层,3×3层和5×5层。

这个 3 层并行局部结构的结果是它们所有输出值的组合,链接到单个向量输出,它将是下一层的输入。这是通过使用连接层完成的。除了三个并行卷积层之外,在相同的本地结构中还添加了一个池化层,因为池化操作对于 CNN 的成功至关重要。

使用 TensorFlow 探索初始化

此链接,你应该能够下载相应的模型库。

然后键入以下命令:

cd models/tutorials/image/imagenet python classify_image.py

当程序第一次运行时,classify_image.pytensorflow.org 下载经过训练的模型。您的硬盘上需要大约 200MB 的可用空间。

上面的命令将对提供的熊猫图像进行分类。如果模型正确运行,脚本将生成以下输出:

giant panda, panda, panda bear, coon bear, Ailuropoda melanoleuca (score = 0.88493)
indri, indris, Indri indri, Indri brevicaudatus (score = 0.00878)
lesser panda, red panda, panda, bear cat, cat bear, Ailurus fulgens (score = 0.00317)
custard apple (score = 0.00149)
earthstar (score = 0.00127)

如果您想提供其他 JPEG 图像,可以通过编辑来完成:

image_file argument:
python classify_image.py --image=image.jpg

您可以通过从互联网下载图像并查看其产生的结果来测试初始阶段。

例如,您可以尝试从此链接获取以下图像(我们将其重命名为inception_image.jpg):

Exploring Inception with TensorFlow

图 16:使用 Inception-v3 进行分类的输入图像

结果如下:

python classify_image.py --image=inception_example.jpg
strawberry (score = 0.91541)
crayfish, crawfish, crawdad, crawdaddy (score = 0.01208)
chocolate sauce, chocolate syrup (score = 0.00628)
cockroach, roach (score = 0.00572)
grocery store, grocery, food market, market (score = 0.00264)

听起来不错!

CNN 的情感识别

深度学习中难以解决的一个问题与神经网络无关:它是以正确格式获取正确数据。但是,Kaggle 平台提供了新的问题,并且需要研究新的数据集。

Kaggle 成立于 2010 年,作为预测建模和分析竞赛的平台,公司和研究人员发布他们的数据,来自世界各地的统计人员和数据挖掘者竞争生产最佳模型。在本节中,我们将展示如何使用面部图像制作 CNN 以进行情感检测。此示例的训练和测试集可以从此链接下载。

Emotion recognition with CNNs

图 17:Kaggle 比赛页面

训练组由 3,761 个灰度图像组成,尺寸为48×48像素,3,761 个标签,每个图像有 7 个元素。

每个元素编码一个情感,0:愤怒,1:厌恶,2:恐惧,3:幸福,4:悲伤,5:惊讶,6:中立。

在经典 Kaggle 比赛中,必须由平台评估从测试集获得的标签集。在这个例子中,我们将训练一个来自训练组的神经网络,之后我们将在单个图像上评估模型。

在开始 CNN 实现之前,我们将通过实现一个简单的过程(文件download_and_display_images.py)来查看下载的数据。

导入库:

import tensorflow as tf
import numpy as np
from matplotlib import pyplot as plt
import EmotionUtils

read_data函数允许构建所有数据集,从下载的数据开始,您可以在本书的代码库中的EmotionUtils库中找到它们:

FLAGS = tf.flags.FLAGS
tf.flags.DEFINE_string("data_dir",\
                       "EmotionDetector/",\
                       "Path to data files")
images = []
images = EmotionUtils.read_data(FLAGS.data_dir)

train_images = images[0]
train_labels = images[1]
valid_images = images[2]
valid_labels = images[3]
test_images  = images[4]

然后打印训练的形状并测试图像:

print ("train images shape = ",train_images.shape)
print ("test labels shape = ",test_images.shape)

显示训练组的第一个图像及其正确的标签:

image_0 = train_images[0]
label_0 = train_labels[0]
print ("image_0 shape = ",image_0.shape)
print ("label set = ",label_0)
image_0 = np.resize(image_0,(48,48))

plt.imshow(image_0, cmap='Greys_r')
plt.show()

有 3,761 个48×48像素的灰度图像:

train images shape =  (3761, 48, 48, 1)

有 3,761 个类标签,每个类包含七个元素:

train labels shape =  (3761, 7)

测试集由 1,312 个48x48像素灰度图像组成:

test labels shape =  (1312, 48, 48, 1)

单个图像具有以下形状:

image_0 shape =  (48, 48, 1)

第一张图片的标签设置如下:

label set =  [ 0\.  0\.  0\.  1\.  0\.  0\.  0.]

此标签对应于快乐,图像在以下 matplot 图中可视化:

Emotion recognition with CNNs

图 18:来自情感检测面部数据集的第一图像

我们现在转向 CNN 架构。

下图显示了数据将如何在 CNN 中流动:

Emotion recognition with CNNs

图 19:实现的 CNN 的前两个卷积层

该网络具有两个卷积层,两个完全连接的层,最后是 softmax 分类层。使用5×5卷积核在第一卷积层中处理输入图像(48×48像素)。这导致 32 个图像,每个使用的滤波器一个。通过最大合并操作对图像进行下采样,以将图像从48×48减小到24×24像素。然后,这些 32 个较小的图像由第二卷积层处理;这导致 64 个新图像(见上图)。通过第二次池化操作,将得到的图像再次下采样到12×12像素。

第二合并层的输出是64×12×12像素的图像。然后将它们展平为长度为12×12×64 = 9,126的单个向量,其用作具有 256 个神经元的完全连接层的输入。这将进入另一个具有 10 个神经元的完全连接的层,每个类对应一个类,用于确定图像的类别,即图像中描绘的情感。

Emotion recognition with CNNs

图 20:实现的 CNN 的最后两层

让我们继续讨论权重和偏置定义。以下数据结构表示网络权重的定义,并总结了到目前为止我们所描述的内容:

weights = {
    'wc1': weight_variable([5, 5, 1, 32], name="W_conv1"),
    'wc2': weight_variable([3, 3, 32, 64],name="W_conv2"),
    'wf1': weight_variable([(IMAGE_SIZE // 4) * (IMAGE_SIZE // 4)
                                         \* 64,256],name="W_fc1"),
    'wf2': weight_variable([256, NUM_LABELS], name="W_fc2")
}

注意卷积滤波器是随机初始化的,所以分类是随机完成的:

def weight_variable(shape, stddev=0.02, name=None):
    initial = tf.truncated_normal(shape, stddev=stddev)
    if name is None:
        return tf.Variable(initial)
    else:
        return tf.get_variable(name, initializer=initial)

以相似方式,我们已经定义了偏差:

biases = {
    'bc1': bias_variable([32], name="b_conv1"),
    'bc2': bias_variable([64], name="b_conv2"),
    'bf1': bias_variable([256], name="b_fc1"),
    'bf2': bias_variable([NUM_LABELS], name="b_fc2")
}

def bias_variable(shape, name=None):
    initial = tf.constant(0.0, shape=shape)
    if name is None:
        return tf.Variable(initial)
    else:
        return tf.get_variable(name, initializer=initial)

优化器必须使用区分链规则通过 CNN 传播误差,并更新过滤器权重以改善分类误差。输入图像的预测类和真实类之间的差异由loss函数测量。它将pred模型的预测输出和所需输出label作为输入:

def loss(pred, label):
    cross_entropy_loss =\
    tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2\
                   (logits=pred, labels=label))
    tf.summary.scalar('Entropy', cross_entropy_loss)
    reg_losses = tf.add_n(tf.get_collection("losses"))
    tf.summary.scalar('Reg_loss', reg_losses)
       return cross_entropy_loss + REGULARIZATION * reg_losses

tf.nn.softmax_cross_entropy_with_logits_v2(pred, label)函数在应用 softmax 函数后计算结果的cross_entropy_loss(但它以数学上仔细的方式一起完成)。这就像是以下结果:

a = tf.nn.softmax(x)
b = cross_entropy(a)

我们计算每个分类图像的cross_entropy_loss,因此我们将测量模型在每个图像上的单独表现。

我们计算分类图像的交叉熵平均值:

cross_entropy_loss =    tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2 (logits=pred, labels=label))

为了防止过拟合,我们将使用 L2 正则化,其中包括向cross_entropy_loss插入一个附加项:

reg_losses = tf.add_n(tf.get_collection("losses"))
return cross_entropy_loss + REGULARIZATION * reg_losses

哪里:

def add_to_regularization_loss(W, b):
    tf.add_to_collection("losses", tf.nn.l2_loss(W))
    tf.add_to_collection("losses", tf.nn.l2_loss(b))

注意

有关详细信息,请参阅此链接

我们已经构建了网络的权重和偏置以及优化过程。但是,与所有已实现的网络一样,我们必须通过导入所有必需的库来启动实现:

import tensorflow as tf
import numpy as np
from datetime import datetime
import EmotionUtils
import os, sys, inspect
from tensorflow.python.framework import ops
import warnings

warnings.filterwarnings("ignore")
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
ops.reset_default_graph()

然后,我们在您的计算机上设置存储数据集的路径,以及网络参数:

FLAGS = tf.flags.FLAGS
tf.flags.DEFINE_string("data_dir",\
                       "EmotionDetector/",\
                       "Path to data files")
tf.flags.DEFINE_string("logs_dir",\
                       "logs/EmotionDetector_logs/",\
                       "Path to where log files are to be saved")
tf.flags.DEFINE_string("mode",\
                       "train",\
                       "mode: train (Default)/ test")
BATCH_SIZE = 128
LEARNING_RATE = 1e-3
MAX_ITERATIONS = 1001
REGULARIZATION = 1e-2
IMAGE_SIZE = 48
NUM_LABELS = 7
VALIDATION_PERCENT = 0.1

emotion_cnn函数实现我们的模型:

def emotion_cnn(dataset):
    with tf.name_scope("conv1") as scope:
        tf.summary.histogram("W_conv1", weights['wc1'])
        tf.summary.histogram("b_conv1", biases['bc1'])
        conv_1 = tf.nn.conv2d(dataset, weights['wc1'],\
                              strides=[1, 1, 1, 1],\
                              padding="SAME")

        h_conv1 = tf.nn.bias_add(conv_1, biases['bc1'])
        h_1 = tf.nn.relu(h_conv1)
        h_pool1 = max_pool_2x2(h_1)
        add_to_regularization_loss(weights['wc1'], biases['bc1'])

    with tf.name_scope("conv2") as scope:
        tf.summary.histogram("W_conv2", weights['wc2'])
        tf.summary.histogram("b_conv2", biases['bc2'])
        conv_2 = tf.nn.conv2d(h_pool1, weights['wc2'],\
                              strides=[1, 1, 1, 1], \ 
                              padding="SAME")
        h_conv2 = tf.nn.bias_add(conv_2, biases['bc2'])
        h_2 = tf.nn.relu(h_conv2)
        h_pool2 = max_pool_2x2(h_2)
        add_to_regularization_loss(weights['wc2'], biases['bc2'])

    with tf.name_scope("fc_1") as scope:
        prob=0.5
        image_size = IMAGE_SIZE // 4
        h_flat = tf.reshape(h_pool2,[-1,image_size*image_size*64])
        tf.summary.histogram("W_fc1", weights['wf1'])
        tf.summary.histogram("b_fc1", biases['bf1'])
        h_fc1 = tf.nn.relu(tf.matmul\
                     (h_flat, weights['wf1']) + biases['bf1'])
        h_fc1_dropout = tf.nn.dropout(h_fc1, prob)

    with tf.name_scope("fc_2") as scope:
        tf.summary.histogram("W_fc2", weights['wf2'])
        tf.summary.histogram("b_fc2", biases['bf2'])
        pred = tf.matmul(h_fc1_dropout, weights['wf2']) +\
               biases['bf2']
    return pred

然后定义一个main函数,我们将在其中定义数据集,输入和输出占位符变量以及主会话,以便启动训练过程:

def main(argv=None):

此函数中的第一个操作是加载数据集以进行训练和验证。我们将使用训练集来教授分类器识别待预测的标签,我们将使用验证集来评估分类器的表现:

train_images,\
train_labels,\
valid_images,\
valid_labels,\ test_images=EmotionUtils.read_data(FLAGS.data_dir)
print("Train size: %s" % train_images.shape[0])
print('Validation size: %s' % valid_images.shape[0])
print("Test size: %s" % test_images.shape[0])

我们为输入图像定义占位符变量。这允许我们更改输入到 TensorFlow 图的图像。数据类型设置为float32,形状设置为[None, img_size, img_size, 1](其中None表示张量可以保存任意数量的图像,每个图像为img_size像素高和img_size像素宽),和1是颜色通道的数量:

    input_dataset = tf.placeholder(tf.float32, \
                                   [None, \
                                    IMAGE_SIZE, \
                                    IMAGE_SIZE, 1],name="input")

接下来,我们为与占位符变量input_dataset中输入的图像正确关联的标签提供占位符变量。这个占位符变量的形状是[None, NUM_LABELS],这意味着它可以包含任意数量的标签,每个标签是长度为NUM_LABELS的向量,在这种情况下为 7:

    input_labels = tf.placeholder(tf.float32,\
                                  [None, NUM_LABELS])

global_step保持跟踪到目前为止执行的优化迭代数量。我们希望在检查点中使用所有其他 TensorFlow 变量保存此变量。请注意trainable=False,这意味着 TensorFlow 不会尝试优化此变量:

    global_step = tf.Variable(0, trainable=False)

跟随变量dropout_prob,用于丢弃优化:

    dropout_prob = tf.placeholder(tf.float32)

现在为测试阶段创建神经网络。emotion_cnn()函数返回input_dataset的预测类标签pred

    pred = emotion_cnn(input_dataset)

output_pred是测试和验证的预测,我们将在运行会话中计算:

    output_pred = tf.nn.softmax(pred,name="output")

loss_val包含输入图像的预测类(pred)与实际类别(input_labels)之间的差异:

    loss_val = loss(pred, input_labels)

train_op定义用于最小化成本函数的优化器。在这种情况下,我们再次使用AdamOptimizer

    train_op = tf.train.AdamOptimizer\
                    (LEARNING_RATE).minimize\
                              (loss_val, global_step)

summary_op是用于 TensorBoard 可视化的 :

summary_op = tf.summary.merge_all()

创建图后,我们必须创建一个 TensorFlow 会话,用于执行图:

    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        summary_writer = tf.summary.FileWriter(FLAGS.logs_dir, sess.graph)

我们定义saver来恢复模型:

        saver = tf.train.Saver()
        ckpt = tf.train.get_checkpoint_state(FLAGS.logs_dir)
        if ckpt and ckpt.model_checkpoint_path:
            saver.restore(sess, ckpt.model_checkpoint_path)
            print ("Model Restored!")

接下来我们需要获得一批训练示例。batch_image现在拥有一批图像,batch_label包含这些图像的正确标签:

        for step in xrange(MAX_ITERATIONS):
            batch_image, batch_label = get_next_batch(train_images,\
                                    train_labels,\
                                    step)

我们将批次放入dict中,其中包含 TensorFlow 图中占位符变量的正确名称:

            feed_dict = {input_dataset: batch_image, \
                         input_labels: batch_label}

我们使用这批训练数据运行优化器。 TensorFlow 将feed_dict_train中的变量分配给占位符变量,然后运行优化程序:

            sess.run(train_op, feed_dict=feed_dict)
            if step % 10 == 0:
                train_loss,\
                             summary_str =\ 
                                 sess.run([loss_val,summary_op],\
                                              feed_dict=feed_dict)
                summary_writer.add_summary(summary_str,\
                                           global_step=step)
                print ("Training Loss: %f" % train_loss)

当运行步长是 100 的倍数时,我们在验证集上运行训练模型:

            if step % 100 == 0:
                valid_loss = \
                           sess.run(loss_val, \
                                feed_dict={input_dataset: valid_images, input_labels: valid_labels})

然后我们打印掉损失值:

                print ("%s Validation Loss: %f" \
                      % (datetime.now(), valid_loss))

在训练过程结束时,模型将被保存:

                saver.save(sess, FLAGS.logs_dir\
                           + 'model.ckpt', \
                           global_step=step)

if __name__ == "__main__":
    tf.app.run()

这是输出。如您所见,在模拟过程中损失函数减少:

Reading train.csv ...
(4178, 48, 48, 1)
(4178, 7)
Reading test.csv ...
Picking ...
Train size: 3761
Validation size: 417
Test size: 1312
2018-02-24 15:17:45.421344 Validation Loss: 1.962773
2018-02-24 15:19:09.568140 Validation Loss: 1.796418
2018-02-24 15:20:35.122450 Validation Loss: 1.328313
2018-02-24 15:21:58.200816 Validation Loss: 1.120482
2018-02-24 15:23:24.024985 Validation Loss: 1.066049
2018-02-24 15:24:38.838554 Validation Loss: 0.965881
2018-02-24 15:25:54.761599 Validation Loss: 0.953470
2018-02-24 15:27:15.592093 Validation Loss: 0.897236
2018-02-24 15:28:39.881676 Validation Loss: 0.838831
2018-02-24 15:29:53.012461 Validation Loss: 0.910777
2018-02-24 15:31:14.416664 Validation Loss: 0.888537
>>>

然而,模型可以通过改变超参数或架构来改进。

在下一节中,我们将了解如何在您自己的图像上有效地测试模型。

在您自己的图像上测试模型

我们使用的数据集是标准化的。所有面部都指向相机,表情在某些情况下被夸大甚至滑稽。现在让我们看看如果我们使用更自然的图像会发生什么。确保脸部没有文字覆盖,情感可识别,脸部主要指向相机。

我从这个 JPEG 图像开始(它是一个彩色图像,你可以从书的代码库下载):

Testing the model on your own image

图 21:输入图像

使用 Matplotlib 和其他 NumPy Python 库,我们将输入颜色图像转换为网络的有效输入,即灰度图像:

img = mpimg.imread('author_image.jpg')
gray = rgb2gray(img)

转换函数如下:

def rgb2gray(rgb):
    return np.dot(rgb[...,:3], [0.299, 0.587, 0.114])

结果如下图所示:

Testing the model on your own image

图 22:灰度输入图像

最后,我们可以使用此图像为网络提供信息,但首先我们必须定义一个正在运行的 TensorFlow 会话:

sess = tf.InteractiveSession()

然后我们可以回想起之前保存的模型:

new_saver = tf.train.\
import_meta_graph('logs/EmotionDetector_logs/model.ckpt-1000.meta')
new_saver.restore(sess,'logs/EmotionDetector_logs/model.ckpt-1000')
tf.get_default_graph().as_graph_def()
x = sess.graph.get_tensor_by_name("input:0")
y_conv = sess.graph.get_tensor_by_name("output:0")

要测试图像,我们必须将其重新整形为网络的有效48×48×1格式:

image_test = np.resize(gray,(1,48,48,1))

我们多次评估相同的图片(1000),以便在输入图像中获得一系列可能的情感:

tResult = testResult()
num_evaluations = 1000
for i in range(0,num_evaluations):
    result = sess.run(y_conv, feed_dict={x:image_test})
    label = sess.run(tf.argmax(result, 1))
    label = label[0]
    label = int(label)
    tResult.evaluate(label)

tResult.display_result(num_evaluations)

在几秒后,会出现如下结果:

>>>
anger = 0.1%
disgust = 0.1%
fear = 29.1%
happy = 50.3%
sad = 0.1%
surprise = 20.0%
neutral = 0.3%
>>>

最高的百分比证实(happy = 50.3%)我们走在正确的轨道上。当然,这并不意味着我们的模型是准确的。可以通过更多和更多样化的训练集,更改网络参数或修改网络架构来实现可能的改进。

源代码

这里列出了实现的分类器的第二部分:

from scipy import misc
import numpy as np
import matplotlib.cm as cm
import tensorflow as tf
from matplotlib import pyplot as plt
import matplotlib.image as mpimg
import EmotionUtils
from EmotionUtils import testResult

def rgb2gray(rgb):
    return np.dot(rgb[...,:3], [0.299, 0.587, 0.114])

img = mpimg.imread('author_image.jpg')     
gray = rgb2gray(img)
plt.imshow(gray, cmap = plt.get_cmap('gray'))
plt.show()

sess = tf.InteractiveSession()
new_saver = tf.train.import_meta_graph('logs/model.ckpt-1000.meta')
new_saver.restore(sess, 'logs/model.ckpt-1000')
tf.get_default_graph().as_graph_def()
x = sess.graph.get_tensor_by_name("input:0")
y_conv = sess.graph.get_tensor_by_name("output:0")

image_test = np.resize(gray,(1,48,48,1))
tResult = testResult()
num_evaluations = 1000
for i in range(0,num_evaluations):
    result = sess.run(y_conv, feed_dict={x:image_test})
    label = sess.run(tf.argmax(result, 1))
    label = label[0]
    label = int(label)
    tResult.evaluate(label)

tResult.display_result(num_evaluations)

我们实现testResult Python 类来显示结果百分比。它可以在EmotionUtils文件中找到。

以下是此类的实现:

class testResult:

    def __init__(self):
        self.anger = 0
        self.disgust = 0
        self.fear = 0
        self.happy = 0
        self.sad = 0
        self.surprise = 0
        self.neutral = 0

    def evaluate(self,label):

        if (0 == label):
            self.anger = self.anger+1
        if (1 == label):
            self.disgust = self.disgust+1
        if (2 == label):
            self.fear = self.fear+1
        if (3 == label):
            self.happy = self.happy+1
        if (4 == label):
            self.sad = self.sad+1
        if (5 == label):
            self.surprise = self.surprise+1
        if (6 == label):
            self.neutral = self.neutral+1

    def display_result(self,evaluations):
        print("anger = "    +\
              str((self.anger/float(evaluations))*100)    + "%")
        print("disgust = "  +\
              str((self.disgust/float(evaluations))*100)  + "%")
        print("fear = "     +\
              str((self.fear/float(evaluations))*100)     + "%")
        print("happy = "    +\
              str((self.happy/float(evaluations))*100)    + "%")
        print("sad = "      +\
              str((self.sad/float(evaluations))*100)      + "%")
        print("surprise = " +\
              str((self.surprise/float(evaluations))*100) + "%")
        print("neutral = "  +\
              str((self.neutral/float(evaluations))*100)  + "%")

总结

在本章中,我们介绍了 CNN。我们已经看到 CNN 适用于图像分类问题,使训练阶段更快,测试阶段更准确。

最常见的 CNN 架构已经被描述:LeNet-5 模型,专为手写和机器打印字符识别而设计; AlexNet,2012 年参加 ILSVRC; VGG 模型在 ImageNet 中实现了 92.7% 的前 5 个测试精度(属于 1,000 个类别的超过 1400 万个图像的数据集);最后是 Inception-v3 模型,该模型负责在 2014 年 ILSVRC 中设置分类和检测标准。

每个 CNN 架构的描述后面都是一个代码示例。此外,AlexNet 网络和 VGG 示例有助于解释传输和样式学习技术的概念。

最后,我们建立了一个 CNN 来对图像数据集中的情感进行分类;我们在单个图像上测试了网络,并评估了模型的限制和质量。

下一章将介绍自编码器:这些算法可用于降维,分类,回归,协同过滤,特征学习和主题建模。我们将使用自编码器进行进一步的数据分析,并使用图像数据集测量分类表现。

五、使用 TensorFlow 实现自编码器

训练自编码器是一个简单的过程。它是一个 NN,其输出与其输入相同。有一个输入层,后面是几个隐藏层,然后在一定深度之后,隐藏层遵循反向架构,直到我们到达最终层与输入层相同的点。我们将数据传递到我们希望学习嵌入的网络中。

在此示例中,我们使用来自 MNIST 数据集的图像作为输入。我们通过导入所有主库来开始实现:

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt

然后我们准备 MNIST 数据集。我们使用 TensorFlow 中的内置input_data类来加载和设置数据。此类确保下载和预处理数据以供自编码器使用。因此,基本上,我们根本不需要进行任何特征工程:

from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/",one_hot=True)

在前面的代码块中,one_hot=True参数确保所有特征都是热编码的。单热编码是一种技术,通过该技术将分类变量转换为可以馈入 ML 算法的形式。

接下来,我们配置网络参数:

learning_rate = 0.01
training_epochs = 20
batch_size = 256
display_step = 1
examples_to_show = 20

输入图像的大小如下:

n_input = 784

隐藏层的大小如下:

n_hidden_1 = 256
n_hidden_2 = 128

最终尺寸对应于28×28 = 784像素。

我们需要为输入图像定义占位符变量。该张量的数据类型设置为float,因为mnist值的比例为[0,1],形状设置为[None, n_input]。定义None参数意味着张量可以包含任意数量的图像:

X = tf.placeholder("float", [None, n_input])

然后我们可以定义网络的权重和偏置。weights数据结构包含编码器和解码器的权重定义。请注意,使用tf.random_normal选择权重,它返回具有正态分布的随机值:

weights = {
    'encoder_h1': tf.Variable\
    (tf.random_normal([n_input, n_hidden_1])),
    'encoder_h2': tf.Variable\
    (tf.random_normal([n_hidden_1, n_hidden_2])),
    'decoder_h1': tf.Variable\
    (tf.random_normal([n_hidden_2, n_hidden_1])),
    'decoder_h2': tf.Variable\
    (tf.random_normal([n_hidden_1, n_input])),
}

同样,我们定义了网络的偏置:

biases = {
    'encoder_b1': tf.Variable\
    (tf.random_normal([n_hidden_1])),
    'encoder_b2': tf.Variable\
    (tf.random_normal([n_hidden_2])),
    'decoder_b1': tf.Variable\
    (tf.random_normal([n_hidden_1])),
    'decoder_b2': tf.Variable\
    (tf.random_normal([n_input])),
}

我们将网络建模分为两个互补的完全连接的网络:编码器和解码器。编码器对数据进行编码;它从 MNIST 数据集中输入图像X,并执行数据编码:

encoder_in = tf.nn.sigmoid(tf.add\
                           (tf.matmul(X, \
                                      weights['encoder_h1']),\
                            biases['encoder_b1']))

输入数据编码只是矩阵乘法运算。使用矩阵乘法将维度 784 的输入数据X减少到较低维度 256:

Implementing autoencoders with TensorFlow

这里,W是权重张量,encoder_h1b是偏置张量,encoder_b1。通过这个操作,我们将初始图像编码为自编码器的有用输入。编码过程的第二步包括数据压缩。输入encoder_in张量表示的数据通过第二个矩阵乘法运算减小到较小的大小:

encoder_out = tf.nn.sigmoid(tf.add\
                            (tf.matmul(encoder_in,\
                                       weights['encoder_h2']),\
                            biases['encoder_b2']))

然后将尺寸 256 的输入数据encoder_in压缩到 128 的较小张量:

Implementing autoencoders with TensorFlow

这里,W代表权重张量encoder_h2,而b代表偏差张量,encoder_b2。请注意,我们使用 sigmoid 作为编码器阶段的激活函数。

解码器执行编码器的逆操作。它解压缩输入以获得相同大小的网络输入的输出。该过程的第一步是将大小为 128 的encoder_out张量转换为 256 大小的中间表示的张量:

decoder_in = tf.nn.sigmoid(tf.add\
                           (tf.matmul(encoder_out,\
                                      weights['decoder_h1']),\
                            biases['decoder_b1']))

在公式中,它意味着:

Implementing autoencoders with TensorFlow

这里,W是权重张量,decoder_h1,大小256×128b是偏置张量,decoder_b1,大小 256。最终解码操作是将数据从其中间表示(大小为 256)解压缩到最终表示(维度 784),这是原始数据的大小:

decoder_out = tf.nn.sigmoid(tf.add\
                            (tf.matmul(decoder_in,\
                                       weights['decoder_h2']),\
                             biases['decoder_b2']))

y_pred参数设置为decoder_out

y_pred = decoder_out

网络将了解输入数据X是否等于解码数据,因此我们定义以下内容:

y_true = X

自编码器的要点是创建一个擅长重建原始数据的缩减矩阵。因此,我们希望最小化cost函数。然后我们将cost函数定义为y_truey_pred之间的均方误差:

cost = tf.reduce_mean(tf.pow(y_true - y_pred, 2))

为了优化cost函数,我们使用以下RMSPropOptimizer类:

optimizer = tf.train.RMSPropOptimizer(learning_rate).minimize(cost)

然后我们准备启动会话:

init = tf.global_variables_initializer()
with tf.Session() as sess:
    sess.run(init)

我们需要设置批量图像的大小来训练网络:

    total_batch = int(mnist.train.num_examples/batch_size)

从训练周期开始(training_epochs的数量设置为10):

    for epoch in range(training_epochs):

循环遍历所有批次:

        for i in range(total_batch):
            batch_xs, batch_ys =\
                      mnist.train.next_batch(batch_size)

然后我们运行优化程序,用批量集batch_xs提供执行图:

            _, c = sess.run([optimizer, cost],\
                            feed_dict={X: batch_xs})

接下来,我们显示每个epoch步骤的结果:

        if epoch % display_step == 0:
            print(„Epoch:", ‚%04d' % (epoch+1),
                  „cost=", „{:.9f}".format(c))
    print("Optimization Finished!")

最后,我们使用编码或解码程序测试模型 。我们为模型提供图像子集,其中example_to_show的值设置为4

    encode_decode = sess.run(
        y_pred, feed_dict=\
        {X: mnist.test.images[:examples_to_show]})

我们使用 Matplotlib 比较原始图像和它们的重建:

    f, a = plt.subplots(2, 10, figsize=(10, 2))
    for i in range(examples_to_show):
        a[0][i].imshow(np.reshape(mnist.test.images[i], (28, 28)))
        a[1][i].imshow(np.reshape(encode_decode[i], (28, 28)))
    f.show()
    plt.draw()
    plt.show()

当我们运行会话时,我们应该有这样的输出:

Extracting MNIST_data/train-images-idx3-ubyte.gz
Extracting MNIST_data/train-labels-idx1-ubyte.gz
Extracting MNIST_data/t10k-images-idx3-ubyte.gz
Extracting MNIST_data/t10k-labels-idx1-ubyte.gz
Epoch: 0001 cost= 0.208461761
Epoch: 0002 cost= 0.172908291
Epoch: 0003 cost= 0.153524384
Epoch: 0004 cost= 0.144243762
Epoch: 0005 cost= 0.137013704
Epoch: 0006 cost= 0.127291277
Epoch: 0007 cost= 0.125370100
Epoch: 0008 cost= 0.121299766
Epoch: 0009 cost= 0.111687921
Epoch: 0010 cost= 0.108801551
Epoch: 0011 cost= 0.105516203
Epoch: 0012 cost= 0.104304880
Epoch: 0013 cost= 0.103362709
Epoch: 0014 cost= 0.101118311
Epoch: 0015 cost= 0.098779991
Epoch: 0016 cost= 0.095374011
Epoch: 0017 cost= 0.095469855
Epoch: 0018 cost= 0.094381645
Epoch: 0019 cost= 0.090281256
Epoch: 0020 cost= 0.092290156
Optimization Finished!

然后我们显示结果。第一行是原始图像,第二行是解码图像:

Implementing autoencoders with TensorFlow

图 4:原始和解码的 MNIST 图像

如你所见,第二个与原来的不同(它似乎仍然是数字二,就像三个一样)。我们可以增加周期数或更改网络参数以改善结果。

提高自编码器的鲁棒性

我们可以用来改善模型稳健性的成功策略,是在编码阶段引入噪声。我们将去噪自编码器称为自编码器的随机版本;在去噪自编码器中,输入被随机破坏,但相同输入的未破坏版本被用作解码阶段的目标。

直觉上, 去噪自编码器做了两件事:首先,它试图对输入进行编码,保留相关信息;然后,它试图消除应用于同一输入的腐败过程的影响。在下一节中,我们将展示一个去噪自编码器的实现。

实现去噪自编码器

网络架构非常简单。 784 像素的输入图像被随机破坏,然后通过编码网络层进行尺寸缩减。图像尺寸从 784 减少到 256 像素。

在解码阶段,我们准备网络输出,将图像大小返回到 784 像素。像往常一样,我们开始将所有必要的库加载到我们的实现中:

import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from tensorflow.examples.tutorials.mnist import input_data

然后我们设置基本的网络参数:

n_input    = 784
n_hidden_1 = 1024
n_hidden_2 = 2048
n_output   = 784

我们设置会话的参数:

epochs     = 100
batch_size = 100
disp_step  = 10

我们构建训练和测试集。我们再次使用从tensorflow.examples.tutorials.mnist导入的input_data函数:

print ("PACKAGES LOADED")
mnist = input_data.read_data_sets('data/', one_hot=True)
trainimg   = mnist.train.images
trainlabel = mnist.train.labels
testimg    = mnist.test.images
testlabel  = mnist.test.labels
print ("MNIST LOADED")

让我们为输入图像定义一个占位符变量。数据类型设置为float,形状设置为[None, n_input]None参数表示张量可以保持任意数量的图像,每个图像的大小为n_input

x = tf.placeholder("float", [None, n_input])

接下来,我们有一个占位符变量,用于与在占位符变量x中输入的图像相关联的真实标签。这个占位符变量的形状是[None, n_output],这意味着它可以包含任意数量的标签,并且每个标签都是长度为n_output的向量,在这种情况下为10

y = tf.placeholder("float", [None, n_output])

为了减少过拟合,我们在编码和解码过程之前应用一个丢弃,因此我们必须定义一个占位符,以便在丢弃期间保持神经元输出的概率:

dropout_keep_prob = tf.placeholder("float")

在这些定义中,我们修正了权重和网络偏差:

weights = {
    'h1': tf.Variable(tf.random_normal([n_input, n_hidden_1])),
    'h2': tf.Variable(tf.random_normal([n_hidden_1, n_hidden_2])),
    'out': tf.Variable(tf.random_normal([n_hidden_2, n_output]))
}
biases = {
    'b1': tf.Variable(tf.random_normal([n_hidden_1])),
    'b2': tf.Variable(tf.random_normal([n_hidden_2])),
    'out': tf.Variable(tf.random_normal([n_output]))
}

使用tf.random_normal选择weightsbiases值,它返回具有正态分布的随机值。编码阶段将来自 MNIST 数据集的图像作为输入,然后通过应用矩阵乘法运算来执行数据压缩:

encode_in = tf.nn.sigmoid\
          (tf.add(tf.matmul\
                  (x, weights['h1']),\
                  biases['b1']))
encode_out = tf.nn.dropout\
             (encode_in, dropout_keep_prob)

在解码阶段,我们应用相同的过程:

decode_in = tf.nn.sigmoid\
          (tf.add(tf.matmul\
                  (encode_out, weights['h2']),\
                  biases['b2']))

过拟合的减少是通过丢弃程序来完成的:

decode_out = tf.nn.dropout(decode_in,\
                           dropout_keep_prob)

最后,我们准备构建预测张量,y_pred

y_pred = tf.nn.sigmoid\
         (tf.matmul(decode_out,\
                    weights['out']) +\
          biases['out'])

然后我们定义一个成本度量, 用于指导变量优化过程:

cost = tf.reduce_mean(tf.pow(y_pred - y, 2))

我们将使用RMSPropOptimizer类最小化cost函数:

optimizer = tf.train.RMSPropOptimizer(0.01).minimize(cost)

最后,我们可以按如下方式初始化已定义的变量:

init = tf.global_variables_initializer()

然后我们设置 TensorFlow 的运行会话:

with tf.Session() as sess:
    sess.run(init)
    print ("Start Training")
    for epoch in range(epochs):
        num_batch  = int(mnist.train.num_examples/batch_size)
        total_cost = 0.
        for i in range(num_batch):

对于每个训练周期,我们从训练数据集中选择一个较小的批次集:

            batch_xs, batch_ys = \
                      mnist.train.next_batch(batch_size)

这是焦点。我们使用之前导入的 NumPy 包中的randn函数随机破坏batch_xs集:

            batch_xs_noisy = batch_xs + \
                             0.3*np.random.randn(batch_size, 784)

我们使用这些集来提供执行图,然后运行会话(sess.run):

            feeds = {x: batch_xs_noisy,\
                     y: batch_xs, \
                     dropout_keep_prob: 0.8}
            sess.run(optimizer, feed_dict=feeds)
            total_cost += sess.run(cost, feed_dict=feeds)

每十个周期,将显示平均成本值:

        if epoch % disp_step == 0:
            print("Epoch %02d/%02d average cost: %.6f"
                   % (epoch, epochs, total_cost/num_batch))

最后,我们开始测试训练有素的模型:

            print("Start Test")

为此,我们从测试集中随机选择一个图像:

            randidx   = np.random.randint\
                               (testimg.shape[0], size=1)
            orgvec    = testimg[randidx, :]
            testvec   = testimg[randidx, :]
            label     = np.argmax(testlabel[randidx, :], 1)
            print("Test label is %d" % (label))
            noisyvec = testvec + 0.3*np.random.randn(1, 784)

然后我们在选定的图像上运行训练模型:

            outvec = sess.run(y_pred,\
                              feed_dict={x: noisyvec,\
                                         dropout_keep_prob: 1})

正如我们将看到的,以下plotresult函数将显示原始图像,噪声图像和预测图像:

            plotresult(orgvec,noisyvec,outvec)
            print("restart Training")

当我们运行会话时,我们应该看到如下结果:

PACKAGES LOADED
Extracting data/train-images-idx3-ubyte.gz
Extracting data/train-labels-idx1-ubyte.gz
Extracting data/t10k-images-idx3-ubyte.gz
Extracting data/t10k-labels-idx1-ubyte.gz
MNIST LOADED
Start Training

为简洁起见,我们仅报告了 100 个周期后的结果:

Epoch 100/100 average cost: 0.212313
Start Test
Test label is 6

这些是原始图像和噪声图像(如您所见,数字 6):

Implementing a denoising autoencoder

图 5:原始图像和噪声图像

这是一个严格重建的图像:

Implementing a denoising autoencoder

图 6:严重重建的图像

在 100 个周期之后,我们有了更好的结果:

Epoch 100/100 average cost: 0.018221
Start Test
Test label is 5

这是原版和噪声图像:

Implementing a denoising autoencoder

图 7:原始图像和噪声图像

这是一个很好的重建图像:

Implementing a denoising autoencoder

图 8:良好的重建图像

实现卷积自编码器

到目前为止,我们已经看到自编码器输入是图像。因此,有必要问一下卷积架构是否可以在我们之前展示的自编码器架构上更好地工作。我们将分析编码器和解码器在卷积自编码器中的工作原理。

编码器

编码器由三个卷积层组成。特征数量从输入数据 1 变为第一卷积层的 16;然后,第二层从 16 到 32;最后,从最后一个卷积层的 32 到 64。从卷积层移动到另一个层时,形状经历图像压缩:

Encoder

图 9:编码阶段的数据流

解码器

解码器由三个依次排列的反卷积层组成。对于每个反卷积操作,我们减少特征的数量以获得必须与原始图像大小相同的图像。除了减少特征数量外,反卷积还可以转换图像的形状:

Decoder

图 10:解码阶段的数据流

我们已经准备好了解如何实现卷积自编码器;第一个实现步骤是加载基本库:

import matplotlib.pyplot as plt
import numpy as np
import math
import tensorflow as tf
import tensorflow.examples.tutorials.mnist.input_data as input_data

然后我们构建训练和测试集:

mnist = input_data.read_data_sets("data/", one_hot=True)
trainings   = mnist.train.images
trainlabels = mnist.train.labels
testings    = mnist.test.images
testlabels  = mnist.test.labels
ntrain      = trainings.shape[0]
ntest       = testings.shape[0]
dim         = trainings.shape[1]
nout        = trainlabels.shape[1]

我们需要为输入图像定义占位符变量:

x = tf.placeholder(tf.float32, [None, dim])

数据类型设置为float32,形状设置为[None, dim],其中None表示张量可以保持任意数量的图像,每个图像是长度为dim的向量。接下来,我们为输出图像提供占位符变量。此变量的形状设置为[None, dim]与输入形状相同:

y = tf.placeholder(tf.float32, [None, dim])

然后我们定义keepprob变量,用于配置在网络训练期间使用的丢弃率

keepprob = tf.placeholder(tf.float32)

此外,我们必须定义每个网络层中的节点数:

n1 = 16
n2 = 32
n3 = 64
ksize = 5

网络总共包含六层。前三层是卷积的,属于编码阶段,而后三层是解卷积的,是解码阶段的一部分:

weights = {
    'ce1': tf.Variable(tf.random_normal\
                       ([ksize, ksize, 1, n1],stddev=0.1)),
    'ce2': tf.Variable(tf.random_normal\
                       ([ksize, ksize, n1, n2],stddev=0.1)),
    'ce3': tf.Variable(tf.random_normal\
                       ([ksize, ksize, n2, n3],stddev=0.1)),
    'cd3': tf.Variable(tf.random_normal\
                       ([ksize, ksize, n2, n3],stddev=0.1)),
    'cd2': tf.Variable(tf.random_normal\
                       ([ksize, ksize, n1, n2],stddev=0.1)),
    'cd1': tf.Variable(tf.random_normal\
                       ([ksize, ksize, 1, n1],stddev=0.1))
}

biases = {
    'be1': tf.Variable\
    (tf.random_normal([n1], stddev=0.1)),
    'be2': tf.Variable\
    (tf.random_normal([n2], stddev=0.1)),
    'be3': tf.Variable\
    (tf.random_normal([n3], stddev=0.1)),
    'bd3': tf.Variable\
    (tf.random_normal([n2], stddev=0.1)),
    'bd2': tf.Variable\
    (tf.random_normal([n1], stddev=0.1)),
    'bd1': tf.Variable\
    (tf.random_normal([1],  stddev=0.1))
}

以下函数cae构建卷积自编码器:传递的输入是图像,_X;数据结构权重和偏置,_W_b;和_keepprob参数:

def cae(_X, _W, _b, _keepprob):

最初的 784 像素图像必须重新整形为28×28矩阵,随后由下一个卷积层处理:

    _input_r = tf.reshape(_X, shape=[-1, 28, 28, 1])

第一个卷积层是_ce1。相对于输入图像,它有_input_r张量作为输入:

    _ce1 = tf.nn.sigmoid\
           (tf.add(tf.nn.conv2d\
                   (_input_r, _W['ce1'],\
                    strides=[1, 2, 2, 1],\
                    padding='SAME'),\
                   _b['be1']))

在移动到第二个卷积层之前,我们应用了丢弃操作:

    _ce1 = tf.nn.dropout(_ce1, _keepprob)

在下面的两个编码层中,我们应用相同的卷积和丢弃操作:

    _ce2 = tf.nn.sigmoid\
           (tf.add(tf.nn.conv2d\
                   (_ce1, _W['ce2'],\
                    strides=[1, 2, 2, 1],\
                    padding='SAME'),\
                   _b['be2']))
    _ce2 = tf.nn.dropout(_ce2, _keepprob)
    _ce3 = tf.nn.sigmoid\
           (tf.add(tf.nn.conv2d\
                   (_ce2, _W['ce3'],\
                    strides=[1, 2, 2, 1],\
                    padding='SAME'),\
                   _b['be3']))
    _ce3 = tf.nn.dropout(_ce3, _keepprob)

特征数量从 1(输入图像)增加到 64,而原始形状图像已减少到28×287×7。在解码阶段,压缩(或编码)和重新成形的图像必须为尽可能与原始图像相似。为实现这一目标,我们在接下来的三个层中使用了conv2d_transpose TensorFlow 函数:

tf.nn.conv2d_transpose(value, filter, output_shape, strides, padding='SAME')

这种操作有时是 ,称为反卷积;它只是conv2d的梯度。该函数的参数如下:

  • valuefloat型和形状[batch, height, width, in_channels]的 4D 张量。
  • filter:与value和形状[height, width, output_channels, in_channels]具有相同类型的 4D 张量。in_channels维度必须与值匹配。
  • output_shape:表示去卷积操作的输出形状的 1D 张量。
  • strides:整数列表。输入张量的每个维度的滑动窗口的步幅。
  • padding:一个有效的字符串或SAME

conv2d_transpose函数将返回与value参数类型相同的张量。第一个去卷积层_cd3具有卷积层_ce3作为输入。它返回_cd3张量,其形状为(1,7,7,32)

    _cd3 = tf.nn.sigmoid\
           (tf.add(tf.nn.conv2d_transpose\
                   (_ce3, _W['cd3'],\
                    tf.stack([tf.shape(_X)[0], 7, 7, n2]),\
                    strides=[1, 2, 2, 1],\
                    padding='SAME'),\
                   _b['bd3']))
    _cd3 = tf.nn.dropout(_cd3, _keepprob)

对于第二个去卷积层_cd2,我们将反卷积层_cd3作为输入传递。它返回_cd2张量,其形状为(1,14,14,16)

    _cd2 = tf.nn.sigmoid\
           (tf.add(tf.nn.conv2d_transpose\
                   (_cd3, _W['cd2'],\
                    tf.stack([tf.shape(_X)[0], 14, 14, n1]),\
                    strides=[1, 2, 2, 1],\
                    padding='SAME'),\
                   _b['bd2']))
    _cd2 = tf.nn.dropout(_cd2, _keepprob)

第三个也是最后一个反卷积层_cd1_cd2层作为输入传递。它返回结果_out张量,其形状为(1,28,28,1),与输入图像相同:

    _cd1 = tf.nn.sigmoid\
           (tf.add(tf.nn.conv2d_transpose\
                   (_cd2, _W['cd1'],\
                    tf.stack([tf.shape(_X)[0], 28, 28, 1]),\
                    strides=[1, 2, 2, 1],\
                    padding='SAME'),\
                   _b['bd1']))
    _cd1 = tf.nn.dropout(_cd1, _keepprob)
    _out = _cd1
    return _out

然后我们将成本函数定义为ypred之间的均方误差:

pred = cae(x, weights, biases, keepprob)
cost = tf.reduce_sum\
       (tf.square(cae(x, weights, biases, keepprob)\
                  - tf.reshape(y, shape=[-1, 28, 28, 1])))
learning_rate = 0.001

为了优化成本,我们将使用AdamOptimizer

optm = tf.train.AdamOptimizer(learning_rate).minimize(cost)

在下一步中,我们配置我们的网络来运行会话:

init = tf.global_variables_initializer()
print ("Functions ready")
sess = tf.Session()
sess.run(init)
mean_img = np.zeros((784))

批次的大小设置为128

batch_size = 128

周期数是50

n_epochs   = 50

然后我们开始循环会话:

for epoch_i in range(n_epochs):

对于每个周期,我们得到一个批量集trainbatch

    for batch_i in range(mnist.train.num_examples // batch_size):
        batch_xs, _ = mnist.train.next_batch(batch_size)
        trainbatch = np.array([img - mean_img for img in batch_xs])

我们应用随机噪声,就像去噪自编码器一样,来改善学习:

        trainbatch_noisy = trainbatch + 0.3*np.random.randn(\
            trainbatch.shape[0], 784)
        sess.run(optm, feed_dict={x: trainbatch_noisy \
                                  , y: trainbatch, keepprob: 0.7})
        print ("[%02d/%02d] cost: %.4f" % (epoch_i, n_epochs \
        , sess.run(cost, feed_dict={x: trainbatch_noisy \
                                   , y: trainbatch, keepprob: 1.})))

对于每个训练周期,我们随机抽取五个训练样例:

    if (epoch_i % 10) == 0:
        n_examples = 5
        test_xs, _ = mnist.test.next_batch(n_examples)
        test_xs_noisy = test_xs + 0.3*np.random.randn(
            test_xs.shape[0], 784)

然后我们在一个小子集上测试训练模型:

        recon = sess.run(pred, feed_dict={x: test_xs_noisy,\
                                                   keepprob: 1.})
        fig, axs = plt.subplots(2, n_examples, figsize=(15, 4))
        for example_i in range(n_examples):
            axs[0][example_i].matshow(np.reshape(
                test_xs_noisy[example_i, :], (28, 28))
                , cmap=plt.get_cmap('gray'))

最后,我们可以使用 Matplotlib 显示输入和学习集:

            axs[1][example_i].matshow(np.reshape(
                np.reshape(recon[example_i, ...], (784,))
                + mean_img, (28, 28)), cmap=plt.get_cmap('gray'))
            plt.show()

执行将产生以下输出:

>>>
Extracting data/train-images-idx3-ubyte.gz
Extracting data/train-labels-idx1-ubyte.gz
Extracting data/t10k-images-idx3-ubyte.gz
Extracting data/t10k-labels-idx1-ubyte.gz
Packages loaded
Network ready
Functions ready
Start training..
[00/05] cost: 8049.0332
[01/05] cost: 3706.8667
[02/05] cost: 2839.9155
[03/05] cost: 2462.7021
[04/05] cost: 2391.9460
>>>

请注意,对于每个周期,我们将可视化输入集和先前显示的相应学习集。正如您在第一个周期所看到的,我们不知道哪些图像已被学习:

Decoder

图 11:第一个周期图像

在第二个周期, 的想法变得更加清晰 :

Decoder

图 12:第二周期图像

这是第三个周期:

Decoder

图 13:第三周期图像

在第四个周期再好一点:

Decoder

图 14:第四周期图像

我们可能已经停止在上一个周期,但这是第五个也是最后一个周期:

Decoder

图 15:第五周期图像

到目前为止,我们已经看到了自编码器的不同实现以及改进版本。但是,将此技术应用于 MNIST 数据集并不能说明其真正的力量。因此,现在是时候看到一个更现实的问题,我们可以应用自编码器技术。

自编码器和欺诈分析

银行,保险公司和信用合作社等金融公司的欺诈检测和预防是一项重要任务。到目前为止,我们已经看到如何以及在何处使用深度神经网络(DNN) 和卷积神经网络(CNN)。

现在是时候使用其他无监督学习算法,如自编码器。在本节中,我们将探索信用卡交易的数据集,并尝试构建一个无监督的机器学习模型,该模型能够判断特定交易是欺诈性的还是真实的。

更具体地说,我们将使用自编码器预先训练分类模型并应用异常检测技术来预测可能的欺诈。在开始之前,我们需要知道数据集。

数据集的描述

对于这个例子,我们将使用来自 Kaggle 的信用卡欺诈检测数据集。数据集可以从此链接下载。由于我使用的是数据集,因此引用以下出版物时,最好是透明的:

Andrea Dal Pozzolo,Olivier Caelen,Reid A. Johnson 和 Gianluca Bontempi。用不平衡分类的欠采样校准概率。在计算智能和数据挖掘研讨会(CIDM),IEEE,2015 年。

该数据集包含 2013 年 9 月欧洲信用卡持有人在两天内进行的交易。共有 285,299 笔交易,只有 492 笔欺诈,这意味着数据集非常不平衡。正类(欺诈)占所有交易的 0.172%。

数据集包含数字输入变量,这些变量是 PCA 转换的结果。遗憾的是,由于机密性问题,我们无法提供有关数据的原始特征和更多背景信息。有 28 个特征,即V1V2,... V27,它们是使用 PCA 获得的主要成分,除了TimeAmount特征。Class特征是响应变量,在欺诈情况下取值1,否则取0

还有两个附加特征,TimeAmountTime列表示每笔交易与第一笔交易之间的时间(以秒为单位),而Amount列表示此交易中转账的金额。那么,让我们看一下输入数据(仅显示V1V2V26V27),如图 16 所示:

Description of the dataset

图 16:信用卡欺诈检测数据集的快照

问题描述

对于这个例子,我们将使用自编码器作为无监督的特征学习算法,该算法学习和概括训练数据共享的公共模式。在重建阶段,对于具有异常模式的数据点,RMSE 将更高。因此,这些数据点是异常值或异常值。我们的假设是,异常也等于我们所追求的欺诈性交易。

现在,在评估步骤中,我们可以根据验证数据选择 RMSE 的阈值,并将 RMSE 高于阈值的所有数据标记为欺诈。或者,如果我们认为 0.1% 的交易都是欺诈性的,我们也可以根据每个数据点(即 RMSE)的重建误差对数据进行排名,然后选择前 0.1% 为欺诈性交易。

给定类不平衡比,建议使用精确回忆曲线下面积(AUPRC)测量精度 ,因为混淆矩阵精度在不平衡分类中没有意义。在这种情况下,使用线性机器学习模型,例如随机森林,逻辑回归或支持向量机,通过应用上下采样技术,将是一个更好的主意。或者,我们可以尝试查找数据中的异常,因为我们假设数据集中只有少数欺诈案例,即异常。

在处理如此严重的响应标签不平衡时,我们在测量模型表现时也需要小心。只有少数欺诈性实例,因此将一切预测为非欺诈的模型将达到 99% 以上的准确率。然而,尽管它们具有高精度,但线性 ML 模型(甚至是树组合)并不一定能帮助我们找到欺诈性案例。

对于这个例子,我们将构建一个无监督的模型:该模型将接受正面和负面数据(欺诈和非欺诈)的训练,但不提供标签。由于我们有比欺诈更多的正常交易,我们应该期望该模型在训练后学习和记忆正常交易的模式,并且该模型应该能够为任何异常交易给出分数。

这种无监督的训练对于此目的非常有用,因为我们没有足够的标记数据。那么,让我们开始吧。

探索性数据分析

在我们实现模型之前,探索数据集将提供一些见解。我们首先导入所需的包和模块(包括此示例所需的其他包)和模块:

import pandas as pd
import numpy as np
import tensorflow as tf
import os
from datetime import datetime
from sklearn.metrics import roc_auc_score as auc
import seaborn as sns # for statistical data visualization
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec

提示

安装 seaborn

您可以通过多种方式安装seaborn,这是一个用于统计数据可视化的 Python 模块:

$ sudo pip install seaborn # for Python 2.7
$ sudo pip3 install seaborn # for Python 3.x
$ sudo conda install seaborn # using conda
# Directly from GitHub (use pip for Python 2.7)
$ pip3 install git+https://github.com/mwaskom/seaborn.git

现在,我假设您已经从上述 URL 下载了数据集。下载附带一个名为creditcard.csv的 CSV 文件。

接下来,让我们阅读数据集并创建一个 pandas DataFrame

df = pd.read_csv('creditcard.csv')
print(df.shape)
>>>
(284807, 31)

因此,数据集具有关于 300,000 个事务,30 个特征和两个二元标签(即 0/1)。现在让我们看一下列名及其数据类型:

print(df.columns)
>>>
Index(['Time', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6', 'V7', 'V8', 'V9', 'V10', 'V11', 'V12', 'V13', 'V14', 'V15', 'V16', 'V17', 'V18', 'V19', 'V20', 'V21', 'V22', 'V23', 'V24', 'V25', 'V26', 'V27', 'V28', 'Amount', 'Class'],
  dtype='object')

print(df.dtypes)
>>>
Time      float64
V1        float64
V2        float64
V3        float64
…
V25       float64
V26       float64
V27       float64
V28       float64
Amount    float64
  Class     int64

现在让我们来看看数据集:

print(df.head())
>>>

Exploratory data analysis

图 17:数据集的快照

现在让我们看看所有交易的时间跨度:

print("Total time spanning: {:.1f} days".format(df['Time'].max() / (3600 * 24.0)))
>>>
Total time spanning: 2.0 days

现在让我们看看这些类的统计信息:

print("{:.3f} % of all transactions are fraud. ".format(np.sum(df['Class']) / df.shape[0] * 100))
>>>
0.173 % of all transactions are fraud.

因此,我们只有少数欺诈交易。这在文献中也被称为罕见事件检测,并且意味着数据集高度不平衡。现在,让我们绘制前五个特征的直方图:

plt.figure(figsize=(12,5*4))
gs = gridspec.GridSpec(5, 1)
for i, cn in enumerate(df.columns[:5]):
    ax = plt.subplot(gs[i])
    sns.distplot(df[cn][df.Class == 1], bins=50)
    sns.distplot(df[cn][df.Class == 0], bins=50)
    ax.set_xlabel('')
    ax.set_title('histogram of feature: ' + str(cn))
plt.show()
>>>

Exploratory data analysis

图 18:显示前五个特征的直方图

在前面的屏幕截图中,可以看出所有特征都是正偏差或负偏斜。此外,数据集没有很多特征,因此修剪尾部会丢失重要的信息。所以,暂时,让我们尽量不这样做,并使用所有特征。

训练,验证和测试集准备

让我们通过将数据分成训练,开发(也称为验证)和测试集来开始训练。我们首先使用 80% 的数据作为训练和验证集。剩余的 20% 将用作测试集:

TEST_RATIO = 0.20
df.sort_values('Time', inplace = True)
TRA_INDEX = int((1-TEST_RATIO) * df.shape[0])
train_x = df.iloc[:TRA_INDEX, 1:-2].values
train_y = df.iloc[:TRA_INDEX, -1].values
test_x = df.iloc[TRA_INDEX:, 1:-2].values
test_y = df.iloc[TRA_INDEX:, -1].values

现在,让我们对前面的分裂进行统计:

print("Total train examples: {}, total fraud cases: {}, equal to {:.5f} % of total cases. ".format(train_x.shape[0], np.sum(train_y), (np.sum(train_y)/train_x.shape[0])*100))

print("Total test examples: {}, total fraud cases: {}, equal to {:.5f} % of total cases. ".format(test_x.shape[0], np.sum(test_y), (np.sum(test_y)/test_y.shape[0])*100))

>>>
Total train examples: 227845, total fraud cases: 417, equal to 0.18302 % of total cases.
Total test examples: 56962, total fraud cases: 75, equal to 0.13167 % of total cases.

归一化

为了获得更好的预测准确率,我们可以考虑两种类型的标准化:z 得分和 min-max 缩放:

  • Z 得分:这将每列归一化为零均值并将其标准化。这特别适用于激活函数,例如 tanh,其输出零两侧的值。其次,这将留下极端值,因此在正常化之后会有一些极端。在这种情况下,这可能对检测异常值很有用。
  • 最小 - 最大缩放:这确保所有值都在 0 和 1 之间,即正数。如果我们使用 sigmoid 作为输出激活,这是默认方法。

我们使用验证集来决定数据标准化方法和激活函数。根据实验,我们发现当与 z 得分标准化一起使用时,tanh 的表现略好于 sigmoid。因此,我们选择了 tanh,然后是 z 得分:

cols_mean = []
cols_std = []

for c in range(train_x.shape[1]):
    cols_mean.append(train_x[:,c].mean())
    cols_std.append(train_x[:,c].std())
    train_x[:, c] = (train_x[:, c] - cols_mean[-1]) / cols_std[-1]
    test_x[:, c] =  (test_x[:, c] - cols_mean[-1]) / cols_std[-1]

作为无监督特征学习算法的自编码器

在本小节中,我们将看到如何使用自编码器作为无监督的特征学习算法。首先,让我们初始化网络超参数:

learning_rate = 0.001
training_epochs = 1000
batch_size = 256
display_step = 10
n_hidden_1 = 15 # number of neurons is the num features
n_input = train_x.shape[1]

由于第一层和第二层分别包含 15 和 5 个神经元,我们正在构建这样的架构网络:28(输入)-> 15 -> 5 -> 15 -> 28(输出)。那么让我们构建我们的自编码器网络。

让我们创建一个 TensorFlow 占位符来保存输入:

X = tf.placeholder("float", [None, n_input])

现在我们必须使用随机初始化创建偏差和权重向量:

weights = {
    'encoder_h1': tf.Variable\
                  (tf.random_normal([n_input, n_hidden_1])),
    'decoder_h1': tf.Variable\
                  (tf.random_normal([n_hidden_1, n_input])),
}
biases = {
    'encoder_b1': tf.Variable(tf.random_normal([n_hidden_1])),
    'decoder_b1': tf.Variable(tf.random_normal([n_input])),
}

现在,我们构建一个简单的自编码器。这里我们有encoder()函数,它构造了编码器。我们使用tanh函数对隐藏层进行编码,如下所示:

def encoder(x):
    layer_1 = tf.nn.tanh(tf.add\
              (tf.matmul(x, weights['encoder_h1']),\
              biases['encoder_b1']))
    return layer_1

这是decoder()函数,它构造了解码器。我们使用tanh函数解码隐藏层,如下所示:

def decoder(x):
    layer_1 = tf.nn.tanh(tf.add\
              (tf.matmul(x, weights['decoder_h1']),\
              biases['decoder_b1']))
    return layer_1

之后,我们通过传递输入数据的 TensorFlow 占位符来构建模型。权重和偏置(NN 的Wb)包含我们将学习优化的网络的所有参数,如下所示:

encoder_op = encoder(X)
decoder_op = decoder(encoder_op)

一旦我们构建了自编码器网络,就可以进行预测,其中目标是输入数据:

y_pred = decoder_op
y_true = X

现在我们已经进行了预测,现在是时候定义batch_mse来评估表现:

batch_mse = tf.reduce_mean(tf.pow(y_true - y_pred, 2), 1)

注意

未观测值的均方误差(MSE)是平方误差偏差平均值。从统计学的角度来看, 它是估计量质量的度量(它总是非负的,接近于零的值更好)。

如果Y^是 n 个预测的向量,并且Y是被预测变量的观测值的向量,则预测变量的样本内 MSE 计算如下:

Autoencoder as an unsupervised feature learning algorithm

因此,MSE 是误差平方(Y[i] - Y^[i])^2的平均值(1/n ∑[i])

我们在这里有另一个batch_mse将返回批量中所有输入数据的 RMSE,这是一个长度等于输入数据中行数的向量。如果您想要输入(无论是训练,验证还是测试数据),这些将是预测值或欺诈分数,我们可以在预测后提取出来。然后我们定义损失和优化器,并最小化平方误差:

cost_op = tf.reduce_mean(tf.pow(y_true - y_pred, 2))
optimizer = tf.train.RMSPropOptimizer(learning_rate).minimize(cost_op)

每层所用的激活函数是tanh。这里的目标函数或成本测量一批中预测和输入数组的总 RMSE,这意味着它是一个标量。然后,每次我们想要进行批量更新时,我们都会运行优化器。

太棒了!我们准备开始训练了。但是,在此之前,让我们定义保存训练模型的路径:

save_model = os.path.join(data_dir, 'autoencoder_model.ckpt')
saver = tf.train.Saver()

到目前为止,我们已经定义了许多变量以及超参数,因此我们必须初始化变量:

init_op = tf.global_variables_initializer()

最后,我们开始训练。我们在训练周期中循环所有批次。然后我们运行优化操作和成本操作来获得损失值。然后我们显示每个周期步骤的日志。最后,我们保存训练有素的模型:

epoch_list = []
loss_list = []
train_auc_list = []
data_dir = 'Training_logs/'
with tf.Session() as sess:
    now = datetime.now()
    sess.run(init_op)
    total_batch = int(train_x.shape[0]/batch_size)

    # Training cycle
    for epoch in range(training_epochs):
        # Loop over all batches
        for i in range(total_batch):
            batch_idx = np.random.choice(train_x.shape[0],\
                        batch_size)
            batch_xs = train_x[batch_idx]

            # Run optimization op (backprop) and
            # cost op (to get loss value)
            _, c = sess.run([optimizer, cost_op],\
                feed_dict={X: batch_xs})

        # Display logs per epoch step
        if epoch % display_step == 0:
            train_batch_mse = sess.run(batch_mse,\
                feed_dict={X: train_x})
            epoch_list.append(epoch+1)
            loss_list.append(c)
            train_auc_list.append(auc(train_y, train_batch_mse))
            print("Epoch:", '%04d,' % (epoch+1),
                  "cost=", "{:.9f},".format(c),
                  "Train auc=", "{:.6f},".format(auc(train_y, \
                  train_batch_mse)),
    print("Optimization Finished!")
    save_path = saver.save(sess, save_model)
    print("Model saved in: %s" % save_path)

save_model = os.path.join(data_dir, autoencoder_model_1L.ckpt')
saver = tf.train.Saver()

前面的代码段很简单。每次,我们从train_x中随机抽取 256 个小批量的小批量,作为X的输入将其输入模型,并运行优化器通过随机梯度下降更新参数 SGD):

>>>
Epoch: 0001, cost= 0.938937187, Train auc= 0.951383
Epoch: 0011, cost= 0.491790086, Train auc= 0.954131
…
Epoch: 0981, cost= 0.323749095, Train auc= 0.953185
Epoch: 0991, cost= 0.255667418, Train auc= 0.953107
Optimization Finished!
Model saved in: Training_logs/autoencoder_model.ckpt
Test auc score: 0.947296

我们在train_x上的估值得出的 AUC 得分约为 0.95。然而,从前面的日志中,很难理解训练的进展情况:

# Plot Training AUC over time
plt.plot(epoch_list, train_auc_list, 'k--', label='Training AUC', linewidth=1.0)
plt.title('Training AUC per iteration')
plt.xlabel('Iteration')
plt.ylabel('Training AUC')
plt.legend(loc='upper right')
plt.grid(True)

# Plot train loss over time
plt.plot(epoch_list, loss_list, 'r--', label='Training loss', linewidth=1.0)
plt.title('Training loss')
plt.xlabel('Iteration')
plt.ylabel('Loss')
plt.legend(loc='upper right')
plt.grid(True)
plt.show()
>>>

Autoencoder as an unsupervised feature learning algorithm

图 19:每次迭代的训练损失和 AUC

在上图中,我们可以看到训练误差有点颠簸,但训练 AUC 几乎保持稳定,约为 95%。这可能听起来很可疑。您还可以看到我们使用相同的数据进行训练和验证。这可能听起来很混乱,但等等!

由于我们正在进行无监督的训练,并且模型在训练期间从未看到标签,因此不会导致过拟合。此附加验证用于监视早期停止以及超参数调整。

评估模型

在训练完我们的自编码器模型和超参数后,我们可以在 20% 测试数据集上评估其表现,如下所示:

save_model = os.path.join(data_dir, autoencoder_model.ckpt')
saver = tf.train.Saver()

# Initializing the variables
init = tf.global_variables_initializer()

with tf.Session() as sess:
    now = datetime.now()
    saver.restore(sess, save_model)
    test_batch_mse = sess.run(batch_mse, feed_dict={X: test_x})

    print("Test auc score: {:.6f}".format(auc(test_y, \
    test_batch_mse)))

在此代码中,我们重用了之前制作的训练模型。test_batch_mse是我们测试数据的欺诈分数:

>>>
Test auc score: 0.948843

太棒了!我们训练有素的模型被证明是一个高度准确的模型,显示 AUC 约为 95%。现在我们已经看到了评估,一些可视化分析会很棒。你们觉得怎么样?让我们绘制非欺诈案件的欺诈分数(MSE)分布图。以下代码段执行此操作:

plt.hist(test_batch_mse[test_y == 0.0], bins = 100)
plt.title("Fraud score (mse) distribution for non-fraud cases")
plt.xlabel("Fraud score (mse)") 
plt.show()
>>>

Evaluating the model

图 20:非欺诈案件的 MSE 欺诈评分

前面的屏幕截图是不可理解的,所以让我们将其缩放到(0, 30)范围并再次绘制图形:

# Zoom into (0, 30) range
plt.hist(test_batch_mse[(test_y == 0.0) & (test_batch_mse < 30)], bins = 100)
plt.title("Fraud score (mse) distribution for non-fraud cases")
plt.xlabel("Fraud score (mse)")
plt.show()
>>>

Evaluating the model

图 21:非欺诈案件的 MSE 欺诈评分,放大到(0, 30)范围

现在让我们只显示欺诈类:

# Display only fraud classes
plt.hist(test_batch_mse[test_y == 1.0], bins = 100)plt.title("Fraud score (mse) distribution for fraud cases")
plt.xlabel("Fraud score (mse)")
plt.show()
>>>

Evaluating the model

图 22:欺诈案件的 MSE 欺诈评分

最后,让我们看一下一些相关统计数据。例如,我们使用10作为检测阈值。现在我们可以计算高于阈值的检测到的病例数,高于阈值的阳性病例数,高于阈值的准确率百分比(即精确度),并将其与测试集中欺诈的平均百分比进行比较:

threshold = 10
print("Number of detected cases above threshold: {}, \n\
Number of pos cases only above threshold: {}, \n\
The percentage of accuracy above threshold (Precision): {:0.2f}%. \n\
Compared to the average percentage of fraud in test set: 0.132%".format( \
np.sum(test_batch_mse > threshold), \
np.sum(test_y[test_batch_mse > threshold]), \
np.sum(test_y[test_batch_mse > threshold]) / np.sum(test_batch_mse > threshold) * 100))
>>>
Number of detected cases above threshold: 198,
Number of positive cases only above threshold: 18,
The percentage of accuracy above threshold (Precision): 9.09%.
Compared to the average percentage of fraud in test set: 0.132%

总而言之,对于我们的案例,只有一个隐藏层的自编码器足够(至少用于训练)。但是,您仍然可以尝试采用其他变体,例如解卷积自编码器和去噪自编码器来解决同样的问题。

总结

在本章中,我们实现了一些称为自编码器的优化网络。自编码器基本上是数据压缩网络模型。它用于将给定输入编码为较小维度的表示,然后可以使用解码器从编码版本重建输入。我们实现的所有自编码器都包含编码和解码部分。

我们还看到了如何通过在网络训练期间引入噪声和构建去噪自编码器来提高自编码器的表现。最后,我们应用第 4 章中介绍的 CNN 概念,卷积神经网络上的 TensorFlow 和卷积自编码器的实现。

即使隐藏单元的数量很大,我们仍然可以通过在网络上施加其他约束来使用自编码器发现数据集的有趣和隐藏结构。换句话说,如果我们对隐藏单元施加稀疏约束,那么即使隐藏单元的数量很大,自编码器仍将在数据中发现有趣的结构。为了证明这一观点,我们看到了一个真实的例子,即信用卡欺诈分析,我们成功应用了自编码器。

循环神经网络(RNN)是一类人工神经网络,其中单元之间的连接形成有向循环。 RNN 利用过去的信息,如时间序列预测。这样,他们就可以对具有高时间依赖性的数据进行预测。这创建了网络的内部状态,允许它展示动态时间行为。

在下一章中,我们将研究 RNN。我们将首先描述这些网络的基本原理,然后我们将实现这些架构的一些有趣示例。