TensorFlow 2.0 Beta is available Learn more

使用 Estimator 构建卷积神经网络

tf.layers 模块提供一个可用于轻松构建神经网络的高级 API,还提供了一些有助于创建密集(全连接)层和卷积层、添加激活函数以及应用 dropout 正规化的方法。在本教程中,您将了解如何使用 layers 构建一个卷积神经网络模型来识别 MNIST 数据集中的手写数字。

MNIST 数据集中从 0 到 9 的手写数字

MNIST 数据集包含 60000 个训练样本和 10000 个测试样本,这些样本均为 0-9 的手写数字,格式为 28x28 像素的单色图像。

开始构建

我们来设置 TensorFlow 程序的框架。创建一个名为 cnn_mnist.py 的文件,并添加以下代码:

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

# Imports
import numpy as np
import tensorflow as tf

tf.logging.set_verbosity(tf.logging.INFO)

# Our application logic will be added here

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

在学习本教程的过程中,您将添加构建、训练和评估卷积神经网络的代码。可在此处找到完整的最终代码。

卷积神经网络简介

卷积神经网络 (CNN) 是目前用于图像分类任务的先进模型架构。CNN 将一系列过滤器应用于图像的原始像素数据,以提取和学习更高级别的特征,然后模型可以使用这些特征进行分类。CNN 包含下列 3 个组成部分:

  • 卷积层:将指定数量的卷积过滤器应用于图像。对于每个子区域,该层会执行一组数学运算,以在输出特征图中生成单个值。然后,卷积层通常会向输出应用 ReLU 激活函数,以便在模型中引入非线性规律。

  • 池化层:对卷积层提取的图像数据进行下采样,以降低特征图的维度,从而缩短处理时间。常用的池化算法是最大池化,它会提取特征图的子区域(例如 2x2 像素区块),保留子区域的最大值,并舍弃其他所有值。

  • 密集(全连接)层:对由卷积层提取并由池化层下采样的特征进行分类。密集层中的每个节点都连接到前一层中的所有节点。

通常,CNN 包括多个执行特征提取的卷积模块。每个模块都由一个卷积层后跟一个池化层组成。最后一个卷积模块后跟一个或多个执行分类的密集层。在 CNN 的最终密集层中,模型中的每个目标类别(模型会预测的所有可能类别)都对应一个节点,并应用 softmax 激活函数,该函数针对每个节点生成一个介于 0 到 1 之间的值(所有这些 softmax 值的总和等于 1)。我们可以将某张给定图像的 softmax 值解析为相对测量值,表示该图像属于每个目标类别的概率。

构建 CNN MNIST 分类器

我们使用以下 CNN 架构构建一个模型,用于对 MNIST 数据集中的图像进行分类:

  1. 卷积层 1:应用 32 个 5x5 过滤器(提取 5x5 像素的子区域),并应用 ReLU 激活函数
  2. 池化层 1:使用 2x2 过滤器和步长 2(指定不重叠的池化区域)执行最大池化运算
  3. 卷积层 2:应用 64 个 5x5 过滤器,并应用 ReLU 激活函数
  4. 池化层 2:同样,使用 2x2 过滤器和步长 2 执行最大池化运算
  5. 密集层 1:包含 1024 个神经元,其中丢弃正则化率为 0.4(任何指定元素在训练期间被丢弃的概率为 0.4)
  6. 密集层 2(对数层):包含 10 个神经元,每个数字目标类别 (0–9) 对应一个神经元。

tf.layers 模块包含用于创建上述 3 种层的方法:

  • conv2d()。构建一个二维卷积层。接受的参数为过滤器数量、过滤器核大小、填充和激活函数。
  • max_pooling2d()。构建一个使用最大池化算法的二维池化层。接受的参数为池化过滤器大小和步长。
  • dense()。构建密集层。接受的参数为神经元数量和激活函数。

上述这些方法都接受张量作为输入,并返回转换后的张量作为输出。这样可轻松地将一个层连接到另一个层:只需从一个层创建方法中获取输出,并将其作为输入提供给另一个层即可。

打开 cnn_mnist.py 并添加以下 cnn_model_fn 函数,该函数符合 TensorFlow Estimator API 预期接口的要求(之后的创建 Estimator 部分对此进行了详细介绍)。cnn_mnist.py 接受的参数为 MNIST 特征数据、标签和模式(来自 tf.estimator.ModeKeysTRAINEVALPREDICT);配置 CNN,然后返回预测、损失和训练操作:

def cnn_model_fn(features, labels, mode):
  """Model function for CNN."""
  # Input Layer
  input_layer = tf.reshape(features["x"], [-1, 28, 28, 1])

  # Convolutional Layer #1
  conv1 = tf.layers.conv2d(
      inputs=input_layer,
      filters=32,
      kernel_size=[5, 5],
      padding="same",
      activation=tf.nn.relu)

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

  # Convolutional Layer #2 and Pooling Layer #2
  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)

  # Dense Layer
  pool2_flat = tf.reshape(pool2, [-1, 7 * 7 * 64])
  dense = tf.layers.dense(inputs=pool2_flat, units=1024, activation=tf.nn.relu)
  dropout = tf.layers.dropout(
      inputs=dense, rate=0.4, training=mode == tf.estimator.ModeKeys.TRAIN)

  # Logits Layer
  logits = tf.layers.dense(inputs=dropout, units=10)

  predictions = {
      # Generate predictions (for PREDICT and EVAL mode)
      "classes": tf.argmax(input=logits, axis=1),
      # Add `softmax_tensor` to the graph. It is used for PREDICT and by the
      # `logging_hook`.
      "probabilities": tf.nn.softmax(logits, name="softmax_tensor")
  }

  if mode == tf.estimator.ModeKeys.PREDICT:
    return tf.estimator.EstimatorSpec(mode=mode, predictions=predictions)

  # Calculate Loss (for both TRAIN and EVAL modes)
  loss = tf.losses.sparse_softmax_cross_entropy(labels=labels, logits=logits)

  # Configure the Training Op (for TRAIN mode)
  if mode == tf.estimator.ModeKeys.TRAIN:
    optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001)
    train_op = optimizer.minimize(
        loss=loss,
        global_step=tf.train.get_global_step())
    return tf.estimator.EstimatorSpec(mode=mode, loss=loss, train_op=train_op)

  # Add evaluation metrics (for EVAL mode)
  eval_metric_ops = {
      "accuracy": tf.metrics.accuracy(
          labels=labels, predictions=predictions["classes"])}
  return tf.estimator.EstimatorSpec(
      mode=mode, loss=loss, eval_metric_ops=eval_metric_ops)

下面的部分(标题对应上述各个代码块)详细介绍了用于创建每个层的 tf.layers 代码,以及如何计算损失、配置训练操作和生成预测。如果您已经熟悉 CNN 和 TensorFlow Estimator,并认为上述代码直观明了,则可以简单浏览这些部分,或者直接跳转到训练和评估 CNN MNIST 分类器

输入层

默认情况下,对于 layers 模块中用于为二维图像数据创建卷积层和池化层的方法,输入张量的形状应该为 [batch_size, image_height, image_width, channels]。可以使用 data_format 参数更改这种行为;具体定义如下:

  • batch_size。在训练期间执行梯度下降法时使用的样本子集的大小。
  • image_height。样本图像的高度。
  • image_width。样本图像的宽度。
  • channels。样本图像中颜色通道的数量。彩色图像有 3 个通道(红色、绿色、蓝色)。单色图像只有 1 个通道(黑色)。
  • data_format。一个字符串,channels_last(默认)或 channels_first 之一。 channels_last 对应于形状为 (batch, ..., channels) 的输入,而 channels_first 对应于形状为 (batch, channels, ...) 的输入。

在此教程中,我们的 MNIST 数据集由 28x28 像素的单色图像组成,因此输入层的形状应该为 [batch_size, 28, 28, 1]

要将我们的输入特征图 (features) 转换为此形状,我们可以执行以下 reshape 操作:

input_layer = tf.reshape(features["x"], [-1, 28, 28, 1])

请注意,我们已经指明批次大小为 -1,表示应根据 features["x"] 中输入值的数量动态计算此维度,同时使所有其他维度的大小保持不变。这样一来,我们就可以将 batch_size 视为可调整的超参数。例如,如果我们按照批次大小 5 将样本馈送到模型中,则 features["x"] 将包含 3920 个值(每个图像中的每个像素对应一个值),并且 input_layer 的形状将为 [5, 28, 28, 1]。同样,如果我们按照批次大小 100 馈送样本,则 features["x"] 将包含 78400 个值,并且 input_layer 的形状将为 [100, 28, 28, 1]

卷积层 1

在我们的第一个卷积层中,我们需要将 32 个 5x5 过滤器应用到输入层,并应用 ReLU 激活函数。我们可以使用 layers 模块中的 conv2d() 方法创建该层,如下所示:

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

inputs 参数指定输入张量,该张量的形状必须为 [batch_size, image_height, image_width, channels]。在此教程中,我们要将第一个卷积层连接到形状为 [batch_size, 28, 28, 1]input_layer

filters 参数指定要应用的过滤器数量(在此教程中为 32),kernel_size 将过滤器的维度指定为 [height, width](在此教程中为 [5, 5])。

提示:如果过滤器高度和宽度的值相同,则可以为 kernel_size 指定单个整数(例如 kernel_size=5)。

padding 参数指定以下两个枚举值之一(不区分大小写):valid(默认值)或 same。要指定输出张量与输入张量具有相同的高度和宽度值,我们在此教程中设置 padding=same,它指示 TensorFlow 向输入张量的边缘添加 0 值,使高度和宽度均保持为 28(没有填充的话,在 28x28 张量上进行 5x5 卷积运算将生成一个 24x24 张量,因为在 28x28 网格中,可以从 24x24 个位置提取出一个 5x5 图块)。

activation 参数指定要应用于卷积输出的激活函数。在此教程中,我们使用 tf.nn.relu 指定 ReLU 激活函数。

conv2d() 生成的输出张量的形状为 [batch_size, 28, 28, 32]:高度和宽度维度与输入相同,但现在有 32 个通道,用于保存每个过滤器的输出。

池化层 1

接下来,我们将第一个池化层连接到刚刚创建的卷积层。我们可以使用 layers 中的 max_pooling2d() 方法构建一个层,该层使用 2x2 过滤器和步长 2 执行最大池化运算:

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

同样,inputs 指定输入张量,形状为 [batch_size, image_height, image_width, channels]。在此教程中,输入张量是 conv1,即第一个卷积层的输出,形状为 [batch_size, 28, 28, 32]

pool_size 参数将最大池化过滤器的大小指定为 [height, width](在此教程中为 [2, 2])。如果两个维度的值相同,则您可以改为指定单个整数(例如 pool_size=2)。

strides 参数指定步长的大小。在此教程中,我们将步长设置为 2,表示过滤器提取的子区域在高度和宽度维度方面均应以 2 个像素分隔(对于一个 2x2 过滤器而言,这意味着提取的任何区域都不会重叠)。如果您要为高度和宽度设置不同的步长值,则可以改为指定元组或列表(例如 stride=[3, 6])。

max_pooling2d() (pool1) 生成的输出张量的形状为 [batch_size, 14, 14, 32]:2x2 过滤器将高度和宽度各减少 50%。

卷积层 2 和池化层 2

像之前一样,我们可以使用 conv2d()max_pooling2d() 将第二个卷积层和池化层连接到 CNN。对于卷积层 2,我们配置 64 个 5x5 过滤器,并应用 ReLU 激活函数;对于池化层 2,我们使用与池化层 1 相同的规格(一个 2x2 最大池化过滤器,步长为 2):

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)

请注意,卷积层 2 接受的输入为第一个池化层 (pool1) 的输出张量,并生成输出张量 conv2conv2 的形状为 [batch_size, 14, 14, 64],高度和宽度与 pool1 相同(因为 padding="same"),并有 64 个通道,对应于应用的 64 个过滤器。

池化层 2 接受输入 conv2,并生成输出 pool2pool2 的形状为 [batch_size, 7, 7, 64](将 conv2 的高度和宽度各减少 50%)。

密集层

接下来,我们需要向 CNN 添加密集层(具有 1024 个神经元和 ReLU 激活函数),以对卷积/池化层提取的特征执行分类。不过,在我们连接该层之前,我们会先扁平化特征图 (pool2),以将其变形为 [batch_size, features],使张量只有两个维度:

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

在上面的 reshape() 操作中,-1 表示 batch_size 维度将根据输入数据中的样本数量动态计算。每个样本都具有 7(pool2 高度)* 7(pool2 宽度)* 64(pool2 通道)个特征,因此我们希望 features 维度的值为 7 * 7 * 64(总计为 3136)。输出张量 pool2_flat 的形状为 [batch_size, 3136]

现在,我们可以使用 layers 中的 dense() 方法连接密集层,如下所示:

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

inputs 参数指定输入张量:扁平化后的特征图 pool2_flatunits 参数指定密集层中的神经元数量 (1024)。activation 参数会接受激活函数;同样,我们会使用 tf.nn.relu 添加 ReLU 激活函数。

为了改善模型的结果,我们还会使用 layers 中的 dropout 方法,向密集层应用丢弃正则化:

dropout = tf.layers.dropout(
    inputs=dense, rate=0.4, training=mode == tf.estimator.ModeKeys.TRAIN)

同样,inputs 指定输入张量,即密集层 (dense) 的输出张量。

rate 参数指定丢弃率;在此教程中,我们使用 0.4,该值表示 40% 的元素会在训练期间被随机丢弃。

training 参数采用布尔值,表示模型目前是否在训练模式下运行;只有在 trainingTrue 的情况下才会执行丢弃操作。在此教程中,我们检查传递到模型函数 cnn_model_fnmode 是否为 TRAIN 模式。

输出张量 dropout 的形状为 [batch_size, 1024]

对数层

我们的神经网络中的最后一层是对数层,该层返回预测的原始值。我们创建一个具有 10 个神经元(介于 0 到 9 之间的每个目标类别对应一个神经元)的密集层,并应用线性激活函数(默认函数):

logits = tf.layers.dense(inputs=dropout, units=10)

CNN 的最终输出张量 logits 的形状为 [batch_size, 10]

生成预测

模型的对数层以 [batch_size, 10] 维张量中原始值的形式返回预测。我们将这些原始值转换成模型函数可以返回的两种不同格式:

  • 每个样本的预测类别:一个介于 0 到 9 之间的数字。
  • 每个样本属于每个可能的目标类别的概率:样本属于以下类别的概率:0、1、2 等。

对于某个给定的样本,预测的类别是对数张量中具有最高原始值的行对应的元素。我们可以使用 tf.argmax 函数查找该元素的索引:

tf.argmax(input=logits, axis=1)

input 参数指定要从其中提取最大值的张量,在此教程中为 logitsaxis 参数指定要沿着 input 张量的哪个轴查找最大值。在此教程中,我们需要沿着索引为 1 的维度查找最大值,该维度对应于预测结果(已经知道对数张量的形状为 [batch_size, 10])。

我们可以使用 tf.nn.softmax 应用 softmax 激活函数,以从对数层中得出概率:

tf.nn.softmax(logits, name="softmax_tensor")

我们将预测编译为字典,并返回 EstimatorSpec 对象:

predictions = {
    "classes": tf.argmax(input=logits, axis=1),
    "probabilities": tf.nn.softmax(logits, name="softmax_tensor")
}
if mode == tf.estimator.ModeKeys.PREDICT:
  return tf.estimator.EstimatorSpec(mode=mode, predictions=predictions)

计算损失

对于训练和评估,我们需要定义损失函数来衡量模型的预测结果与目标类别之间的匹配程度。对于像 MNIST 这样的多类别分类问题,通常将交叉熵用作损失指标。以下代码计算模型在 TRAINEVAL 模式下运行时的交叉熵:

loss = tf.losses.sparse_softmax_cross_entropy(labels=labels, logits=logits)

我们来详细了解一下上述代码。

labels 张量包含样本的预测索引列表,例如 [1, 9, ...]logits 包含最后一层的线性输出。

tf.losses.sparse_softmax_cross_entropy 以高效的数值稳定方式计算以上两个输入的 softmax 交叉熵(又名:类别交叉熵、负对数似然率)。

配置训练操作

在上一部分中,我们将 CNN 的损失定义为对数层和标签之间的 softmax 交叉熵。下面我们配置模型以在训练期间优化该损失值。我们将学习速率设为 0.001,并将优化算法设为随机梯度下降法

if mode == tf.estimator.ModeKeys.TRAIN:
  optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001)
  train_op = optimizer.minimize(
      loss=loss,
      global_step=tf.train.get_global_step())
  return tf.estimator.EstimatorSpec(mode=mode, loss=loss, train_op=train_op)

添加评估指标

要在模型中添加准确率指标,我们在评估模式下定义 eval_metric_ops 字典,如下所示:

eval_metric_ops = {
    "accuracy": tf.metrics.accuracy(
        labels=labels, predictions=predictions["classes"])}
return tf.estimator.EstimatorSpec(
    mode=mode, loss=loss, eval_metric_ops=eval_metric_ops)

训练和评估 CNN MNIST 分类器

我们已经编写了 MNIST CNN 模型函数;现在可以开始训练和评估了。

加载训练和测试数据

首先,我们加载训练和测试数据。使用以下代码将 main() 函数添加到 cnn_mnist.py 中:

def main(unused_argv):
  # Load training and eval data
  mnist = tf.contrib.learn.datasets.load_dataset("mnist")
  train_data = mnist.train.images # Returns np.array
  train_labels = np.asarray(mnist.train.labels, dtype=np.int32)
  eval_data = mnist.test.images # Returns np.array
  eval_labels = np.asarray(mnist.test.labels, dtype=np.int32)

我们在 train_datatrain_labels 中将训练特征数据(55000 张手写数字图像的原始像素值)和训练标签(每张图像在 0 到 9 之间的对应值)分别存储为 Numpy 数组。同样,我们将评估特征数据(10000 张图像)和评估标签分别存储在 eval_dataeval_labels 中。

创建 Estimator

接下来,我们为模型创建一个 Estimator(一种用于执行高级模型训练、评估和推理的 TensorFlow 类)。将以下代码添加到 main() 中:

# Create the Estimator
mnist_classifier = tf.estimator.Estimator(
    model_fn=cnn_model_fn, model_dir="/tmp/mnist_convnet_model")

model_fn 参数指定用于训练、评估和预测的模型函数;我们将在构建 CNN MNIST 分类器部分创建的 cnn_model_fn 传递到该参数。model_dir 参数指定要用于保存模型数据(检查点)的目录(在此教程中,我们指定临时目录 /tmp/mnist_convnet_model,不过您可以选择更改为其他目录)。

设置日志记录钩子

由于 CNN 可能需要一段时间才能完成训练,因此我们设置一些日志记录,以在训练期间跟踪进度。我们可以使用 TensorFlow 的 tf.train.SessionRunHook 创建 tf.train.LoggingTensorHook,它将记录 CNN 的 softmax 层的概率值。将以下代码添加到 main() 中:

# Set up logging for predictions
tensors_to_log = {"probabilities": "softmax_tensor"}
logging_hook = tf.train.LoggingTensorHook(
    tensors=tensors_to_log, every_n_iter=50)

我们将要记录的张量字典存储到 tensors_to_log 中。每个键都是我们选择的将会显示在日志输出中的标签,而相应标签是 TensorFlow 图中 Tensor 的名称。在此教程中,我们可以在 softmax_tensor(我们之前在 cnn_model_fn 中生成概率时为 softmax 操作指定的名称)中找到 probabilities

接下来,我们创建 LoggingTensorHook,将 tensors_to_log 传递到 tensors 参数。我们设置 every_n_iter=50,指定每完成 50 个训练步之后应记录概率。

训练模型

现在,我们可以训练模型了,可以通过创建 train_input_fn 并在 mnist_classifier 上调用 train() 来完成该操作。将以下代码添加到 main() 中:

# Train the model
train_input_fn = tf.estimator.inputs.numpy_input_fn(
    x={"x": train_data},
    y=train_labels,
    batch_size=100,
    num_epochs=None,
    shuffle=True)
mnist_classifier.train(
    input_fn=train_input_fn,
    steps=20000,
    hooks=[logging_hook])

numpy_input_fn 调用中,我们将训练特征数据和标签分别传递到 x(作为字典)和 y。我们将 batch_size 设置为 100(这意味着模型会在每一步训练 100 个小批次样本)。 num_epochs=None 表示模型会一直训练,直到达到指定的训练步数。我们还设置 shuffle=True,以随机化处理训练数据。在 train 调用中,我们设置 steps=20000(这意味着模型总共要训练 20000 步)。为了在训练期间触发 logging_hook,我们将其传递到 hooks 参数。

评估模型

训练完成后,我们需要评估模型以确定其在 MNIST 测试集上的准确率。我们调用 evaluate 方法,该方法将评估我们在 model_fneval_metric_ops 参数中指定的指标。将以下代码添加到 main() 中:

# Evaluate the model and print results
eval_input_fn = tf.estimator.inputs.numpy_input_fn(
    x={"x": eval_data},
    y=eval_labels,
    num_epochs=1,
    shuffle=False)
eval_results = mnist_classifier.evaluate(input_fn=eval_input_fn)
print(eval_results)

要创建 eval_input_fn,我们设置 num_epochs=1,以便模型评估一个数据周期的指标,并返回结果。我们还设置 shuffle=False 以按顺序遍历数据。

运行模型

我们已经编写了 CNN 模型函数 Estimator 和训练/评估逻辑;现在我们来看看结果。运行 cnn_mnist.py

在模型训练期间,您将看到如下所示的日志输出:

INFO:tensorflow:loss = 2.36026, step = 1
INFO:tensorflow:probabilities = [[ 0.07722801  0.08618255  0.09256398, ...]]
...
INFO:tensorflow:loss = 2.13119, step = 101
INFO:tensorflow:global_step/sec: 5.44132
...
INFO:tensorflow:Loss for final step: 0.553216.

INFO:tensorflow:Restored model from /tmp/mnist_convnet_model
INFO:tensorflow:Eval steps [0,inf) for training step 20000.
INFO:tensorflow:Input iterator is exhausted.
INFO:tensorflow:Saving evaluation summary for step 20000: accuracy = 0.9733, loss = 0.0902271
{'loss': 0.090227105, 'global_step': 20000, 'accuracy': 0.97329998}

在此教程中,我们在测试数据集上的准确率达到了 97.3%。

其他资源

要详细了解 TensorFlow 中的 TensorFlow Estimator 和 CNN,请参阅以下资源:

  • 在 tf.estimator 中创建 Estimator 介绍了 TensorFlow Estimator API,其中详细说明了如何配置 Estimator、编写模型函数、计算损失以及定义训练操作。
  • 高级卷积神经网络详细介绍了如何使用低阶 TensorFlow 操作构建一个没有 Estimator 的 MNIST CNN 分类模型。