TensorFlow 官方教程:搭建卷积神经网络

本文演示了如何在 TensorFlow 中建立卷积神经网络(Convolutional Neural Network,CNN)并将其用于 MNIST 训练和预测。

官方教程为 TensorFlow - A Guide to TF Layers ,本文有个人删减和添加。

基础函数

Python 2.x 和 3.x 代码兼容

1
2
3
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

导入所需模块:

1
2
import numpy as np
import tensorflow as tf

设置 TensorFlow log 的显示等级(默认等级也是 INFO):

1
tf.logging.set_verbosity(tf.logging.INFO)
  • tf.logging 包含 DEBUG / INFO / WARN / ERROR / FATAL 五个等级,对应的数字为 10 / 20 / 30 / 40 / 50

  • 另一种方式为 设置环境变量 TF_CPP_MIN_LOG_LEVEL0~3,对应 INFO~FATAL

1
os.environ['TF_CPP_MIN_LOG_LEVEL'] = "0"  # or any in {"1", "2", "3"}

按照 TensorFlow 惯例,在末尾先加上如下代码(主代码放在在下面这段代码之前):

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

模型函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def cnn_model_fn(features, labels, mode):
input_layer = tf.reshape(features["x"], [-1, 28, 28, 1])

conv1 = tf.layers.conv2d(
inputs=input_layer,
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)
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)
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 = 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)

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

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 = {
"accuracy": tf.metrics.accuracy(
labels=labels, predictions=predictions["classes"])}
return tf.estimator.EstimatorSpec(
mode=mode, loss=loss, eval_metric_ops=eval_metric_ops)

输入层

将输入张量变为 [batch_size, image_width, image_height, channels] 的形状:

  • batch_size:代表 MNIST 图片数量,使用占位符 -1
  • image_width & image_height:分别代表图片的长度和宽度的像素值。
  • channels:代表图像的通道数,彩色 RGB 图片为 3,黑白图片为 1
1
input_layer = tf.reshape(features["x"], [-1, 28, 28, 1])

输入层为一定数量的图片,每张图片具有 28×28×1 的形状。

神经网络架构

目前流行的 CNN 网络架构中,基本思想是卷积层(Convonlutional Layer)和池化层(Pooling Layer)反复交叠,然后将最后一层展开,并连接两三个密集层(Dense Layer)(也称为全连接层(Fully-connected Layer))。

下面将说明如何将张量 [batch_size, 28, 28, 1] 映射为张量 [batch_size, 10],即对每张 MNIST 图片预测其标签(0~9 其中之一)。

官方文档分为 6 层,这里分为 4 层。分层思想和 Andrew Ng 保持一致x,不将池化层作为单独一层,因为其没有参数需要训练。

第一层:卷积 + 池化

1
2
3
4
5
6
7
conv1 = tf.layers.conv2d(
inputs=input_layer,
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)
  • conv1:对 28×28×1 的输入和 32 个 5×5 的卷积核进行卷积(same padding),并加入 ReLU 非线性激活函数,输出为 28×28×32 形状的张量。
  • pool1:使用最大池化,池化大小为 2×2 且步长为 2 表示长宽降为原来的一半,输出为 14×14×32 形状的张量。

第二层:卷积 + 池化

1
2
3
4
5
6
7
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)
  • conv2:对 14×14×32 的输入和 64 个 5×5 的卷积核进行卷积(same padding),并加入 ReLU 非线性激活函数,输出为 14×14×64 形状的张量。
  • pool2:同上节,长宽降为原来的一半,输出为 7×7×64 形状的张量。

第三层:密集层

1
2
3
4
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)
  • pool2_flat:将每个张量形状由三维展开至一维,具有长度 7×7×64=3136 。
  • dense:将 3136 个输入特征映射到 1024 个输出特征,并加入 ReLU 非线性激活函数。
  • dropout:随机丢弃单元的正则化方法,防止过拟合。

参数 training 采用布尔值,当且仅当其为 True 的时候(即函数 modeTRAIN)才进行 dropout。

第四层:密集层(logits)

1
logits = tf.layers.dense(inputs=dropout, units=10)
  • dense:将 1024 个输入特征映射到 10 个输出,每个输出分别代表手写数字为 0~9 的 logits

logits 这个概念有点难以理解。其实际上为范围为 $[-\infty, +\infty]$ 在归一化之前的概率的数值表征。softmax 计算之后为归一化的概率。logits 越大,对应的 softmax 概率也越大。

生成预测

1
2
3
4
5
6
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)
  • predictions 使用字典类型,包含最大概率的数字和每个数字的 softmax 概率。
  • 通过参数 name 将操作显式命名为 softmax_tensor,以便稍后通过 LoggingTensorHook 引用。
  • 返回 EstimatorSpec 对象:如果 modePREDICT,程序到此返回,不运行后续的训练阶段。

计算损失函数

1
2
3
onehot_labels = tf.one_hot(indices=tf.cast(labels, tf.int32), depth=10)
loss = tf.losses.softmax_cross_entropy(
onehot_labels=onehot_labels, logits=logits)
  • onehot_labels 采用长度为 10 的独热编码(One Hot Encoding),即标签位所在置 1,其余位置 0。
  • loss 为 softmax 交叉熵;计算的时候需要采用归一化前的概率表征 logits

添加训练操作

如果 modeTRAIN,采用小批量随机梯度下降法进行训练操作,训练后返回 EstimatorSpec 对象:

1
2
3
4
5
6
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)

添加评估指标

计算模型的准确性:

1
2
3
4
5
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)
  • 这里的 modeEVALModeKeys 只有三种 mode,其中 PREDICTTRAIN 已在前文返回)。
  • eval_metric_ops:字典中定义需要的指标,这里为准确率 accuracy。若有需要可以添加其他指标。
  • 返回 EstimatorSpec 对象:传递 eval_metric_ops,对模型进行评估。

main 函数

加载数据

1
2
3
4
5
6
def main(unused_argv):
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)

创建估计器

1
2
mnist_classifier = tf.estimator.Estimator(
model_fn=cnn_model_fn, model_dir="/tmp/mnist_convnet_model")
  • model_fn 参数指定用于训练,评估和预测的模型函数,这里为前文创建的 cnn_model_fn
  • model_dir参数指定模型数据(.ckpt)的保存目录,可以自行更改。

关于 TensorFlow Estimator API 更深入的部分,请参见 在 tf.estimator 中创建估计器

设置 Logging Hook

1
2
3
tensors_to_log = {"probabilities": "softmax_tensor"}
logging_hook = tf.train.LoggingTensorHook(
tensors=tensors_to_log, every_n_iter=50)
  • tensors_to_log 字典:将字符串标签映射为张量或张量名称。这里映射为前文的名称 softmax_tensor
  • logging_hook:传递需要打印日志的张量的字典 tensors_to_log,每训练 50 次进行一次打印。

模型训练

1
2
3
4
5
6
7
8
9
10
11
# 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])
  • train_input_fn:输入训练样本。传递训练集的数据和标签,每次采用 100 个小批量样本,迭代不停止,样本随机打乱。
  • mnist_classifier.train:传递 train_input_fn 给上文创建的估计器,共进行 20000 次训练,并将 [logging_hook] 传递给参数 hooks 以便在训练过程中触发。

模型评估

1
2
3
4
5
6
7
8
# 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:输入递评估样本。传递评估集的数据和标签,迭代一次即停止,不随机打乱样本以循环遍历。
  • eval_results:传递 eval_input_fn 给上文创建的估计器,进行模型评估。

模型运行

运行 cnn_mnist.py

CNN 的训练过程需要大量时间,CPU 上可能会超过 1 小时。通过减少 mnist_classifier.train 的步数 steps 可以加快训练,但是准确率会有所降低。

模型训练时,您会看到如下所示的日志输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
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}

训练后的 CNN 模型在测试集可以达到大约 97.3% 的准确率。