使用TensorFlow的层来构建深度学习神经网络
使用TensorFlow构建多层卷积网络的分步教程。
编者注:更多人工智能内容请关注2018年4月10-13日人工智能北京大会

深度学习已经在许多领域(例如计算机视觉、自然语言处理、文本翻译或语音到文本等)证明了其有效性。它的名字来源于大量用于构建执行机器学习任务的神经网络的深度层次。尽管有多种类型的层和多种整体神经网络的架构,但一般规则认为,网络层次越深,其可以表征的复杂性就越高。本文将解释神经网络的层的基本概念,并会带大家一步一步地探索使用TensorFlow创建多种类型(的网络)。

TensorFlow是针对大众用户的人工智能(AI)的平台。它是一个拥有大量的社区和强力支持的开源库。TensorFlow提供了一整套用于构建神经网络体系架构的工具,并能训练和发布模型服务。它提供了不同的抽象级别,因此你可以把它用于高级别的剪贴式机器学习过程,或者自己进行更深层的计算的编写。

TensorFlow在tf.layers包中提供了多种类型的层。该模块使得在深度学习模型中创建一个层变得非常容易,且无需考虑许多细节。目前它主要支持用于卷积网络的层的类型。对于其他类型的网络,例如RNN,你可能需要查看tf.contrib.rnntf.nn。最基本的层的类型是全连接层。要实现它,只需要在Dense类中设置输入和大小。其他类型的层可能需要更多的参数,但它们的实现方式可以支持默认行为并节省开发人员的时间。

关于什么是层和什么不是还存在一些分歧。一种观点认为,层必须存储训练过的参数(如权重和偏置)。例如,这意味着应用激活函数不是一层。事实上,tf.layers模块通过使用激活参数来实现这样的函数。尽管如此,此模块中引入的层并不总是严格遵循此规则。你可以在其中找到很多种类型的层:全连接、卷积、池化、扁平、批量归一化、部分丢弃和卷积转置。看起来扁平层和最大化池层不存储在学习过程中训练的任何参数。尽管如此,它们却在执行比激活函数更复杂的操作。因此模块的作者决定将它们设置为独立的类。在文章的后面,我们将讨论如何使用其中的一些来构建深度卷积网络。

一个典型的卷积网络是一系列卷积加池化的组,然后是一点全连接的层。一个卷积就像一个小型的神经网络,对它的输入的每个位置重复使用一次卷积计算。结果,网络层会变得小很多但深度增加了。池化操作通常是减小输入图像大小的操作。最大池化是最常用的池化算法,并且已被证明在许多计算机视觉任务中是很有效的。

在本文中,我将展示如何使用TensorFlow将卷积网络应用于图像处理。我们会使用TensorFlow示例中的MNIST数据集。这个任务是识别手写的0到9的数字。

首先,TensorFlow具有加载数据的功能。你所需要做的就是使用input_data模块:

from tensorflow.examples.tutorials.mnist import input_data

mnist = input_data.read_data_sets(folder_path, one_hot=True)

我们现在要构建一个多层架构。在描述学习的过程之后,我会带着你创建不同类型的层并将其用于MNIST分类任务。

训练是优化损失函数的过程。损失函数测量网络预测与实际标注值之间的差别。深度学习通常使用称为交叉熵的技术来定义损失函数。

TensorFlow提供了称为tf.losses.softmax_cross_entropy的函数,该函数在内部应用softmax算法,对模型的非标准化预测进行计算,并把对所有类的预测结果求和。在我们的例子中,我们使用由tf.train API提供的Adam优化器。标注结果将在训练和测试过程中被使用,代表了基本事实。网络的输出表示神经网络预测的结果,会在下一节构建网络时被定义。

loss = tf.losses.softmax_cross_entropy(labels, output)

train_step = tf.train.AdamOptimizer(1e-4).minimize(loss)

为了评估训练过程的性能,我们希望将输出与实际标注进行比较并计算准确度:

correct_prediction = tf.equal(tf.argmax(output, 1), tf.argmax(labels, 1))

accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

现在,我们将使用批次方法、固定数量的步数和学习速率定义一个简单的训练过程。对于MNIST数据集,next_batch函数会调用mnist.train.next_batch方法。网络训练完成后,我们可以检查模型在测试数据上的表现。

# Open the session

sess = tf.InteractiveSession()

sess.run(tf.global_variables_initializer())

for i in range(steps):

# Get the next batch

input_batch, labels_batch = next_batch(100)

feed_dict = {x_input: input_batch, y_labels: labels_batch}

# Print the current batch accuracy every 100 steps

if i%100 == 0:

train_accuracy = accuracy.eval(feed_dict=feed_dict)

print(“Step %d, training batch accuracy %g”%(i, train_accuracy))

# Run the optimization step

train_step.run(feed_dict=feed_dict)

# Print the test accuracy once the training is over

print(“Test accuracy: %g”%accuracy.eval(feed_dict={x_input: test_images, y_labels: test_labels}))

对于实际的训练过程,让我们从简单的开始,创建只有一个输出层的网络。我们首先为输入数据和标注定义占位符。在训练阶段,它们将存储来自MNIST数据集的数据。由于数据被扁平化了,输入层只有一个维度。输出层的大小对应于标注种类的数量。输入和标注都将额外的维度设置为None,这就能应对可变数量的样本数据。

input = tf.placeholder(tf.float32, [None, image_size*image_size])

labels = tf.placeholder(tf.float32, [None, labels_size])

现在是构建令人兴奋的部分的时候了:输出层。背后的魔力非常简单明了。其中的每个神经元都有权重和偏置量的参数,从每个输入维度获取数据并进行一些计算。这是使它成为一个完全连接的层。

TensorFlow的tf.layers包让你只用一行代码就可以完成所有这些工作。你需要提供的就只是输入和层的大小。

output = tf.layers.dense(inputs=input, units=labels_size)

我们的第一个网络在准确性方面并不令人印象深刻。但它很简单,所以它运行速度非常快。

我们将尝试通过在输入和输出之间添加更多的层来改进我们的网络。这些层被称为隐藏层。首先,我们添加另一个全连接层进去。

上面的体系结构需要进行一些小的更改。首先,还有另一个参数被用来表示隐藏层的神经元数量。隐藏层本身会获取输入数据并连接到输出层:

hidden = tf.layers.dense(inputs=input, units=1024, activation=tf.nn.relu)

output = tf.layers.dense(inputs=hidden, units=labels_size)

注意,这次我们使用了一个激活参数。它对从神经元经过的数据都会使用激活函数(我们这里用的是ReLU)运算。该算法已被证明在深度架构下会工作得非常好。

你应该看到我们的性能略有下降。我们的网络正在变得更加深,这意味着它需要调整更多的参数,这使得训练过程变长。另一方面,这显著地提高了准确度,达到94%。

接下来我们要添加的两层是卷积网络的组成部分。它们的工作方式与全连接型不同,并且对具有两个或更多维度(如图像)的输入表现尤其出色。卷积层的参数是卷积窗口的大小和滤波器的数量。对填充设置为same表明生成的特征层具有相同的大小。在这一步之后,我们应用最大池化。

使用卷积可以让我们利用输入数据的二维表示。当我们把数字图片打平并将结果输入全连接层时,这些二维信息就丢失了。要变回到原始结构,我们可以使用tf.reshape函数。

input2d = tf.reshape(input, [-1,image_size,image_size,1])

下面是进行卷积和最大池化的代码。请注意,为了和下一个全连接层进行连接,这一层的输出必须再次被打平。

conv1 = tf.layers.conv2d(inputs=input2d, filters=32, kernel_size=[5, 5], padding=”same”, activation=tf.nn.relu)

pool1 = tf.layers.max_pooling2d(inputs=conv1, pool_size=[2, 2], strides=2)

pool_flat = tf.reshape(pool1, [-1, 14 * 14 * 32])

hidden = tf.layers.dense(inputs= pool_flat, units=1024, activation=tf.nn.relu)

output = tf.layers.dense(inputs=hidden, units=labels_size)

向图片应用卷积可以提高准确度(达到97%),但会显著地减慢训练的过程。要充分利用这一模型,我们应该继续加入另一层。再次使用2D输入,但只打平第二层的输出。现在第一层的输出不需要打平了,因为后面卷积可以工作在更高的维度上。

conv2 = tf.layers.conv2d(inputs=pool1, filters=64, kernel_size=[5, 5], padding=”same”, activation=tf.nn.relu)

pool2 = tf.layers.max_pooling2d(inputs=conv2, pool_size=[2, 2], strides=2)

pool_flat = tf.reshape(pool2, [-1, 7 * 7 * 64])

此时,你在运行代码时需要非常耐心。网络的复杂性增加了很多开销,但我们获得了更好的准确性。

现在我们会介绍另一种可以改善网络性能并避免过拟合的技术。它被称为Dropout,我们把它用到隐藏层的全连接层。Dropout的方式是单个神经节要么关闭,要么按一定的概率被保留。它被用于训练阶段,因此请记住需要在评估网络性能时关闭它。

要使用Dropout,我们需要稍微更改代码。首先,我们需要一个占位符来在训练和测试阶段保存Dropout的概率。

should_drop = tf.placeholder(tf.bool)

其次,我们需要定义drouput并将其连接到输出层。架构的其余部分保持不变。

hidden = tf.layers.dense(inputs=pool_flat, units=1024, activation=tf.nn.relu)

dropout = tf.layers.dropout(inputs=hidden, rate=0.5, training=should_drop)

output = tf.layers.dense(inputs=dropout, units=labels_size)

在本文中,我们首先介绍了深度学习的概念,并使用TensorFlow构建了一个多层卷积网络。该代码可以重新用于图像识别任务以及其他任何数据集。但是,更复杂的图像需要修改更深的层数以及更复杂的架构,例如Inception或ResNets。

本练习的关键在于你不需要掌握统计技术或编写复杂的矩阵乘法代码来创建人工智能模型。TensorFlow就可以处理这些问题。但是,你需要知道哪些算法适合你自己的数据和应用,并确定最佳的超参数,例如网络体系架构、层的深度、批处理的大小、学习速率等。请注意,TensorFlow提供给你的各种选择需要你自己承担很多责任。

这篇博文是O’ReillyTensorFlow的合作产物。请阅读我们的编辑独立声明

Barbara Fusinska

Barbara是一名软件开发人员、架构师和团队负责人,拥有超过10年的从业经验。从快节奏的创业公司到国际大型公司她都曾工作过。Barbara喜欢使用最佳实践和具有大量常识的现代模式构建漂亮的系统架构。这种热情与对团队合作的坚定信念和为人们创造最佳环境以发挥人类的潜力相伴随。你可以用@BasiaFusinska在Twitter上或通过她的博客http://barbarafusinska.com找到她。

Samples from the MNIST test data set (source: Josef Steppan on Wikimedia Commons)