TensorFlow Layers module 为容易的创建一个神经网络提供了高水平的API接口。它提供了很多方法帮助创建dense(全连接)层和卷积层,增加激活函数和应用dropout做归一化。在这个教程中,你会学到如何用layers
构建一个卷积神经网络用于识别手写体数字,基于MNIST数据集。
MNIST 数据集包括手写体数字0~9的6万个训练数据和1万个测试数据,并格式化为28*28像素单色图片。
开端
让我们先创建一个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
from tensorflow.contrib import learn
from tensorflow.contrib.learn.python.learn.estimators import model_fn as model_fn_lib
tf.logging.set_verbosity(tf.logging.INFO)
# Our application logic will be added here
if __name__ == "__main__":
tf.app.run()
阅读完这段教程,学习相应代码就能完成构建,训练和验证一个卷积神经网络,完成代码可以从这里获取
卷积神经网络介绍
卷积神经网络(CNNs)介绍是现在图片分类任务的一个理想模型。CNN通过一系列的过滤器从图片的原始像素数据上抽取和学习更高层次的特征,这些特征用于模型进行分类。CNN包含三部分:
- 卷积层(Convolutional layers):这层会应用一定数量的卷积器到图片上。对于每个子区域,该层用一套数学运算产生一个单独的值在输出特征映射中。卷积层特别的应用了一个ReLU activation function 到输出数据上用于引用非线性到模型中。
- 池化层(Pooling layers):这层下采样图片数据抽取操作,来减少特征维度以减少处理时间。池算法中最大池算法通常被使用,最大池算法抽取特征图的子区域(例如:2*2像素块)
- 全连接层(Dense (fully connected) layers):Fully Connected 也叫 Dense,因为全连接权重密度很大。其实就是个卷积核宽高等于输入数据宽高的特殊卷积层。卷积层和全连接层可以等效转换。该层用于前面卷积层和池化层的下采样进行 的特征抽取后的分类任务。这层的每个节点都被前一层的每个节点连接。
通常,一个CNN由一系列的卷积模块组成,这些模块完成每步的特征抽取。每个模块由一个卷积层和紧跟的一个池化层组成,最后一个卷积模块由一个或多个全连接层组成,完成分类任务。在CNN的最后一个全连接层对模型的的每个目标类别(模型预测的所有可能类别)都包含着一个单独的节点,这个全连接层有一个softmax激活函数用于为每个节点产生一个0-1之间的值(所有这些softmax值的总和等于1)。我们能够把softmax值对应到一个给定的图片,而这些图片都是对应每个目标类别。CNN网络示例如下:
- 想更全面的了解CNN的架构,可以看斯坦福的CNN用于视觉识别课程材料
构建CNN的MNIST分类器
让我们用下面的CNN架构构建一个模型用于对MNIST数据集图片进行分类。
1. 卷积层1:用32个 的5*5的卷积核(抽取5*5像素的子区域),使用ReLu激活函数。
2. 池化层1:用一个2*2的过滤器进行最大池化,步长为2(保证池化区域不重叠)。
3. 卷积层2:用于64个 的5*5的卷积核,使用ReLu激活函数。
4. 池化层2:再次用一个2*2的过滤器进行最大池化,步长为2
5. 全连接层1:1024个神经元节点,每个节点有0.4的的概率会正则化丢弃(每个给定的元素点在训练时有0.4的概率被丢弃,防止过拟合)。
6. 全连接层2:10个神经元节点,每一个对应一个数字目标类(0-9)
tf.layers
模块包含了创建上面三种层的方法:
- conv2d():构建一个二维的卷积层。参数有:卷积核数(filters)、卷积和尺寸、填充(padding)、激活函数。
- max_pooling2d():构建一个二维的池化层,使用最大池算法。参数有:池化核尺寸,步长。
- dense():构建一个全连接层。参数有:神经元节点数、激活函数。
上面的每一个函数都接收一个张量(tensor)作为输入,并且返回一个变换过的张量作为输出。这样能够容易的连接一层到另一层:某一层的输出作为下一层的输入。
将下面的cnn_model_fn
函数添加到cnn_mnist.py
中,这符合TensorFlow’s Estimator API期待的接口。cnn_mnist.py
将MNIST特征数据,标签集合模型模式(TRAIN, EVAL, INFER)作为参数。配置CNN网络,并且返回预测值,损失值和训练操作。
def cnn_model_fn(features, labels, mode):
"""Model function for CNN."""
# Input Layer
input_layer = tf.reshape(features, [-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 == learn.ModeKeys.TRAIN)
# Logits Layer
logits = tf.layers.dense(inputs=dropout, units=10)
loss = None
train_op = None
# Calculate Loss (for both TRAIN and EVAL modes)
if mode != learn.ModeKeys.INFER:
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)
# Configure the Training Op (for TRAIN mode)
if mode == learn.ModeKeys.TRAIN:
train_op = tf.contrib.layers.optimize_loss(
loss=loss,
global_step=tf.contrib.framework.get_global_step(),
learning_rate=0.001,
optimizer="SGD")
# Generate Predictions
predictions = {
"classes": tf.argmax(
input=logits, axis=1),
"probabilities": tf.nn.softmax(
logits, name="softmax_tensor")
}
# Return a ModelFnOps object
return model_fn_lib.ModelFnOps(
mode=mode, predictions=predictions, loss=loss, train_op=train_op)
下面的部分(对应上面每块代码块的头部)深入到tf.layers
代码中,了解如何创建每层网络结构、如何计算损失(loss)、配置训练操作过程和进行预测。如果你已经对CNNs和TensorFlow Estimators非常熟悉了,能够直观的明白上述代码,你可以跳过这一部分或直接跳到"Training and Evaluating the CNN MNIST Classifier"
输入层(Input Layer)
在layers
模块中用于创建用于二维图片数据的卷积层和池化层的方法期待的张量(tensors)有一个结构(shape):[batch_size, image_width, image_height, channels]
,定义如下:
- batch_size
:子集的个数,例如在训练时,当执行梯度下降(gradient descent)时被用到。
- image_width
:样例图片的宽度。
- image_height
:样例图片的高度。
- channels
:用例图片的颜色通道数。对于彩色图片,通道数为3(红,绿,蓝),对于单色图片,仅有一个通道(黑)
这里,我们的MNIST数据集由单色28*28像素的图片组成,因此对于我们的输入层理想的结构是[batch_size, 28, 28, 1]
。
为了修正我们的输入特征映射到这个结构,我们可以执行下面的reshape
运算:
input_layer = tf.reshape(features, [-1, 28, 28, 1])
注意到这里我们指定batch size为-1,这是个特殊值,指这个维度应该基于在特征集features
中输入值的数量,保持所有其它维度尺寸固定。
卷积层1(Convolutional Layer1):
在我们第一个卷积层,我们应用32个5*5像素的过滤器到输入层,用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_width, image_height, channels]
结构。filters
参数指定是应用的过滤器个数(这里是32),kernel_size
只每个过滤器的维度[width, height]
(这里[5, 5]
)。
说明:如果过滤器的宽度和高度有相同的值,可以指定一个单独的整数,如:kernel_size=5
padding
参数为两个枚举类型中的一个:valid
(默认值)或者same
。如果我们想输出张量的宽度和高度跟输入一致,我们可以设padding=same
,这会指示TensorFlow在输出张量的边界加0值,来满足28的宽和高度(如果没有padding,一个5*5的卷积作用在28*28的张量上将会产生一个24*24的张量,有24*24个位置去提取5*5的区域从28*28的格子上)。activation
参数指定应用到卷积输出的的激活函数,这里指定ReLU激活函数tf.nn.relu
。
这里通过conv2d()
产生的输出张量有一个结构[batch_size, 28, 28, 32]
:与输入有相同的宽度和高度维度,但是每一个过滤器输出有32个通道。
池化层1(Pooling Layer1):
下面,我们连接第一个池化层到我们刚刚创建的卷积层。我们能够用layers里的max_pooling2d()
方法构建一个层,这个层用2*2的过滤器和步长2来进行最大池化:
pool1 = tf.layers.max_pooling2d(inputs=conv1, pool_size=[2, 2], strides=2)
inputs
参数指定结构是[batch_size, image_width, image_height, channels]
的输入张量。这里我们的输入张量是conv1
,第一个卷积层的输出,输出张量有结构[batch_size, 28, 28, 32]
。pool_size
参数指定最大池化过滤器的尺寸[width, height]
(这里是,[2,2]),如果两个维度有相同的值,可以用一个单独的整数值代替(pool_size=2
)。strides
参数指定步长的值,这里为2,如果想在两个维度指定不同 的步长值,可以设如:stride=[3, 6]
max_pooling2d()
函数产生的输出张量(pool1)
结构为:[batch_size, 14, 14, 32]
:2*2的过滤器会减少宽和高的50%。
卷积层2和池化层2
我们能够像前面一样,用conv2d()
和max_pooling2d()
函数连接第二个卷积和池化层到CNN网络,对于卷积层2,我们配置64个5*5的过滤器和ReLu激活函数,并且对于池化层2,我们使用相同的空间,如池化层1(一个2*2的最大池化过滤器和步长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)作为输入,并且产生输出张量conv2
,架构为:[batch_size, 14, 14, 64]
,与pool1有相同的宽度和高度,并且64个通道对应应用的64个过滤器。
池化层2把conv2
作为输入,产生pool2
作为输出,pool2结构为[batch_size, 7, 7, 64]
全连接层(Dense Layer):
下面,我们增加一个全连接层(1024个神经单元和Relu激活函数)到CNN网络上,来通过卷积层和池化层抽取的特征执行分类任务。然而,在连接到网络上前,我们需要格式化我们的特征映射到结构[batch_size, features]
,如此,我们的张量就只有二维了:
pool2_flat = tf.reshape(pool2, [-1, 7 * 7 * 64])
在上面的reshape()
运算,-1指batch_size
维度将被动态计算,根据我们输入数据样例的数量。每个样例都有7(pool2宽)*7(pool2高)*64(pool2通道)个特征,因此特征维度为: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_flat
.
- units
参数为全连接层神经元节点的个数(1024)。
- activation
,我们依然使用ReLU激活函数。
为了提高模型的结果,我们应用dropout把我们的全连接层归一化:
dropout = tf.layers.dropout(
inputs=dense, rate=0.4, training=mode == learn.ModeKeys.TRAIN)
inputs
参数,输入张量,为我们全连接层的输出张量。rate
参数指定丢弃率,我们指定0.4,意思为在训练期间,将会随机丢弃40%的元素。training
参数是一个布尔值,指模型现在是否在训练模式。dropout将只有training
是True
时才执行。
我们的输出张量dropout
结构为:[batch_size, 1024]
。
逻辑层(Logits Layer)
我们神经网络的最后一层是逻辑层,这层会返回我们的预测原始结果值。我们创建一个10个神经元(每一个对应0-9的每个目标类)的全连接层,使用线性激活函数(默认):
logits = tf.layers.dense(inputs=dropout, units=10)
CNN的最后的输出张量,logits结构为:[batch_size, 10]
计算损失率(Loss)
对于训练和验证,我们都需要定义一个损失函数来测量预测结果与目标类的匹配度。对于像MNIST这样的多分类问题,交叉熵(cross entropy)通常被用在损失测量中。下面代码计算当运行模式为TRAIN
或EVAL
时的交叉熵。
loss = None
train_op = None
# Calculate loss for both TRAIN and EVAL modes
if mode != learn.ModeKeys.INFER:
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)
我们的labels
张量包含了我们样例的预测列表,例如:[1, 9, ...]
,为了计算交叉熵,首先,我们需要转换labels
为相应的one-hot
编码。
[[0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
...]
我们使用tf.one_hot
函数执行转换操作,有两个必须的参数:
- indices
:在one-hot张量中有值的位置。–例如上述1值的位置。
- depth
:one-hot张量的深度。–例如目标类的数量,这里我们是10。
下面的代码创建一个one-hot张量为我们的labels
onehot_labels = tf.one_hot(indices=tf.cast(labels, tf.int32), depth=10)
tf.losses.softmax_cross_entropy()
把onehot_labels
和logits
作为参数,在logits
上执行softmax激活,计算交叉熵,并且返回我们的损失,为一个scalar张量:
loss = tf.losses.softmax_cross_entropy(
onehot_labels=onehot_labels, logits=logits)
配置训练参数
在上面部分,我们定义CNN的损失为logits层和标签(Labels)的softmax交叉熵。让我们配置我们的模型在训练期间让损失值可选,使用tf.contrib.layers
中的tf.contrib.layers.optimize_loss
函数,我们将用一个0.001的学习率和随机梯度下降(stochastic gradient descent ) 作为选择算法:
# Configure the Training Op (for TRAIN mode)
if mode == learn.ModeKeys.TRAIN:
train_op = tf.contrib.layers.optimize_loss(
loss=loss,
global_step=tf.contrib.framework.get_global_step(),
learning_rate=0.001,
optimizer="SGD")
产生预测结果
我们模型的逻辑层( logits layer)返回预测结果原始值张量,结构为:[batch_size, 10]
。让我们转换这些原始结果为两种我们的模型函数能够返回的不同的格式:
- 每个样例的预测类别:0-9间的一个数字
- 每个样例的每个可能的目标类的概率。
对于每一个给定的样例,我们的预测类别是在logits张量相应行的最高原始值的元素。我们能够知道这个元素的索引,用如下的函数:
tf.argmax(input=logits, axis=1)
input
参数指从张量中抽取的最大值,这里是logits
。axis
参数指沿着input
张量找到的最大值的轴,这里我们最大值的维度是1,这对应于我们的预测
我们能够从我们的逻辑层导出概率值,通过应用softmax 激活 tf.nn.softmax:
tf.nn.softmax(logits, name="softmax_tensor")
我们在字典中编译我们的预测,如下:
predictions = {
"classes": tf.argmax(
input=logits, axis=1),
"probabilities": tf.nn.softmax(
logits, name="softmax_tensor")
}
最后,我们可以返回我们的预测值、损失、和训练执行,mode参数在tf.contrib.learn.ModelFnOps对象:
# Return a ModelFnOps object
return model_fn_lib.ModelFnOps(
mode=mode, predictions=predictions, loss=loss, train_op=train_op)
训练和验证CNN MNIST 分类器
我们已经编写好我们的MNIST CNN模型函数,现在我们可以训练并验证它。
加载训练和测试数据
首先我们加载训练和测试数据,增加main()
函数到cnn_mnist.py
:
def main(unused_argv):
# Load training and eval data
mnist = 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)
我们存储训练特征数据(55000张图片的手写数字的原像素值)和训练标签(每张图片对应0-9中的一个值)作为numpy arrays在train_data
和train_labels
中。同样,我们存储验证特征数据(1000张图片)和验证标签ineval_data
和eval_labels
。
创建Estimator
让我们创建一个Estimator(一个TensorFlow的类,用于执行高维模型训练,验证,和接口),对于我们的模型,增加如下代码到main():
# Create the Estimator
mnist_classifier = learn.Estimator(
model_fn=cnn_model_fn, model_dir="/tmp/mnist_convnet_model")
model_fn
参数指用于训练、验证、接口的模型函数,我们传递在”Building the CNN MNIST Classifier”中创建的cnn_model_fn
给它。model_dir
参数指定模型数据(checkpoints)将被保存的目录(根据需要更换自己的目录)
设置日志
CNNs需要花费一段时间来训练,在训练时我们可以设置一些日志用于我们跟踪程序运行。我们可以用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
。每一个key都是我们选择的标签,这些标签将被打印在log输出里。并且相应的标签是TensorFlow图中一个张量的名字。这里,我们的probabilities
能够被找到在softmax_tensor
中,我们给softmax运算的名字要在我们生成概率之前。
接着,我们创建LoggingTensorHook
,传递tensors_to_log
给tensors
参数。我们设置every_n_iter=50
,这个值指训练每50步记录一下概率值。
训练模型
下面调用fit函数来训练我们的模型,添加如下代码到main:
# Train the model
mnist_classifier.fit(
x=train_data,
y=train_labels,
batch_size=100,
steps=20000,
monitors=[logging_hook])
在fit调用中,我们传递训练特征数据给标签x和y。
- batch_size=100
:指模型每步将会在100个样例的小集合上训练
- steps=20000
:模型总共训练20000步。
- monitors
:我们传递logging_hook
给它,以便在训练时能够触发。
验证模型
训练完成,我们想验证我们模型在测试集上的准确率,我们需要创建一个指标词典,用tf.contrib.learn.MetricSpec
,它能计算准确率,增加下面代码到main:
# Configure the accuracy metric for evaluation
metrics = {
"accuracy":
learn.MetricSpec(
metric_fn=tf.metrics.accuracy, prediction_key="classes"),
}
metric_fn
参数,计算并返回指标的函数。这里我们用在tf.metrics
模块中的accuracy
函数。prediction_key
参数,张量的key,这个张量是模型函数返回的预测值,这里,我们用前面创建的分类模型的预测keyclasses
。
下面我们可以验证我们的模型了,增加如下代码,会验证并打印结果:
# Evaluate the model and print results
eval_results = mnist_classifier.evaluate(
x=eval_data, y=eval_labels, metrics=metrics)
print(eval_results)
运行模型
我们已经编写完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 Estimators and CNNs
,可以看如下链接:
- Creating Estimators in tf.contrib.learn.
: TensorFlow Estimator API的引言,我们会学习到如何配置一个Estimator,写一个模型函数,计算loss,定义一个训练过程。
- Deep MNIST for Experts: Building a Multilayer CNN.:学习如何构建一个MNIST CNN分类模型,没有layers ,使用低水平的TensorFlow 运算。