本文演示了如何在 TensorFlow 中建立卷积神经网络(Convolutional Neural Network,CNN)并将其用于 MNIST 训练和预测。
官方教程为 TensorFlow - A Guide to TF Layers ,本文有个人删减和添加。
基础函数
1 | from __future__ import absolute_import |
导入所需模块:
1 | import numpy as np |
设置 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_LEVEL
为0~3
,对应INFO~FATAL
:
1 | os.environ['TF_CPP_MIN_LOG_LEVEL'] = "0" # or any in {"1", "2", "3"} |
按照 TensorFlow 惯例,在末尾先加上如下代码(主代码放在在下面这段代码之前):
1 | if __name__ == "__main__": |
模型函数
1 | def cnn_model_fn(features, labels, mode): |
输入层
将输入张量变为 [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 | conv1 = tf.layers.conv2d( |
conv1
:对 28×28×1 的输入和 32 个 5×5 的卷积核进行卷积(same padding),并加入 ReLU 非线性激活函数,输出为 28×28×32 形状的张量。pool1
:使用最大池化,池化大小为 2×2 且步长为 2 表示长宽降为原来的一半,输出为 14×14×32 形状的张量。
第二层:卷积 + 池化
1 | conv2 = tf.layers.conv2d( |
conv2
:对 14×14×32 的输入和 64 个 5×5 的卷积核进行卷积(same padding),并加入 ReLU 非线性激活函数,输出为 14×14×64 形状的张量。pool2
:同上节,长宽降为原来的一半,输出为 7×7×64 形状的张量。
第三层:密集层
1 | pool2_flat = tf.reshape(pool2, [-1, 7 * 7 * 64]) |
pool2_flat
:将每个张量形状由三维展开至一维,具有长度 7×7×64=3136 。dense
:将 3136 个输入特征映射到 1024 个输出特征,并加入 ReLU 非线性激活函数。dropout
:随机丢弃单元的正则化方法,防止过拟合。
参数
training
采用布尔值,当且仅当其为True
的时候(即函数mode
为TRAIN
)才进行 dropout。
第四层:密集层(logits)
1 | logits = tf.layers.dense(inputs=dropout, units=10) |
dense
:将 1024 个输入特征映射到 10 个输出,每个输出分别代表手写数字为 0~9 的logits
。
logits
这个概念有点难以理解。其实际上为范围为 $[-\infty, +\infty]$ 在归一化之前的概率的数值表征。softmax 计算之后为归一化的概率。logits
越大,对应的 softmax 概率也越大。
生成预测
1 | predictions = { |
predictions
使用字典类型,包含最大概率的数字和每个数字的 softmax 概率。- 通过参数
name
将操作显式命名为softmax_tensor
,以便稍后通过LoggingTensorHook
引用。 - 返回
EstimatorSpec
对象:如果mode
为PREDICT
,程序到此返回,不运行后续的训练阶段。
计算损失函数
1 | onehot_labels = tf.one_hot(indices=tf.cast(labels, tf.int32), depth=10) |
onehot_labels
采用长度为 10 的独热编码(One Hot Encoding),即标签位所在置 1,其余位置 0。loss
为 softmax 交叉熵;计算的时候需要采用归一化前的概率表征logits
。
添加训练操作
如果 mode
为 TRAIN
,采用小批量随机梯度下降法进行训练操作,训练后返回 EstimatorSpec
对象:
1 | if mode == tf.estimator.ModeKeys.TRAIN: |
添加评估指标
计算模型的准确性:
1 | eval_metric_ops = { |
- 这里的
mode
为EVAL
(ModeKeys
只有三种mode
,其中PREDICT
和TRAIN
已在前文返回)。 eval_metric_ops
:字典中定义需要的指标,这里为准确率accuracy
。若有需要可以添加其他指标。- 返回
EstimatorSpec
对象:传递eval_metric_ops
,对模型进行评估。
main 函数
加载数据
1 | def main(unused_argv): |
创建估计器
1 | mnist_classifier = tf.estimator.Estimator( |
model_fn
参数指定用于训练,评估和预测的模型函数,这里为前文创建的cnn_model_fn
。model_dir
参数指定模型数据(.ckpt)的保存目录,可以自行更改。
关于 TensorFlow Estimator API 更深入的部分,请参见 在 tf.estimator 中创建估计器 。
设置 Logging Hook
1 | tensors_to_log = {"probabilities": "softmax_tensor"} |
tensors_to_log
字典:将字符串标签映射为张量或张量名称。这里映射为前文的名称softmax_tensor
。logging_hook
:传递需要打印日志的张量的字典tensors_to_log
,每训练 50 次进行一次打印。
模型训练
1 | # Train the model |
train_input_fn
:输入训练样本。传递训练集的数据和标签,每次采用 100 个小批量样本,迭代不停止,样本随机打乱。mnist_classifier.train
:传递train_input_fn
给上文创建的估计器,共进行 20000 次训练,并将[logging_hook]
传递给参数hooks
以便在训练过程中触发。
模型评估
1 | # Evaluate the model and print results |
eval_input_fn
:输入递评估样本。传递评估集的数据和标签,迭代一次即停止,不随机打乱样本以循环遍历。eval_results
:传递eval_input_fn
给上文创建的估计器,进行模型评估。
模型运行
运行 cnn_mnist.py
。
CNN 的训练过程需要大量时间,CPU 上可能会超过 1 小时。通过减少
mnist_classifier.train
的步数steps
可以加快训练,但是准确率会有所降低。
模型训练时,您会看到如下所示的日志输出:
1 | INFO:tensorflow:loss = 2.36026, step = 1 |
训练后的 CNN 模型在测试集可以达到大约 97.3% 的准确率。