原文链接
TensorFlow 的layer模块提供了高层API来方便你构建一个神经网络。它提供了一些方法方便你提供Dense Layer(全连接层)和 Convolutiaonal layers(卷积层),以及增加激励函数,或者应用dropout 正则化。这个教程里,你将学会如何构建一个卷积神经网络模型去识别手写的MNIST数据集。

MNIST dataset集合了60000训练数据,以及10000测试数据。这些数据都是手写的0-9的数字,每个数字都是由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
tf.logging.set_verbosity(tf.logging.INFO)

# Our application logic will be added here

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

通过这个教程,你会添加代码去构建,训练和评估这个卷积神经网络。完整的最终的代码你可以在这里找到。

卷积神经网络介绍


卷积神经网络(Convolutional neural network)简称CNN, 是目前用于图像分类的最先进的模型。CNN用一系列的filter从原始像素的图片抽取和学习更高级别的features。最后模型用这些高级别的feature来进行识别。CNN包含3个模块:
1. 卷积层(Convolutional Layer),对图像应用一组卷积的filter,对图像的每一个小的区域,卷集成应用fitler进行一些数学计算,产生一个单一的output。卷积层一般使用ReLU作为激活函数来给模型引入非线性。
2. 池化层(Pooling Laye),用来对卷积层产生的output进一步按照一定规则降低采样,比如选取某个区域(比如2×2)的最大值来代表某个区域。从而减小了数据的维度,加快运行速度。
3. 全连接层(Dense Layer),全连接层一般放在CNN的最后,它负责从卷积层抽象和池化层降低采样后的数据里进行建模。在一个全连接层里,每个节点和它前边层的所有节点都有连接。

通常,一个CNN是由一组负责特征抽取的卷积模块构成。每个卷积模块包含一个(或多个)卷积层,后边跟着一个池化层。最后一个卷积模块后边跟着一个或者多个全连接层来进行对图片分类的工作。最后一个全连接层每一种可能的分类对应一个节点,用一个softmax激活函数对每一个节点生成一个0到1的值,所有节点的值加起来等于1。每个节点的值代表它是这种分类的可能性。

如果你想更深入理解卷积神经网络可以看我的博客中关于这方面的内容,或者这个

构建一个CNN MNIST 分类器


让我们构建一个用于对MNIST数据进行图片分类的CNN网络,它的结构如下:
1. 卷积层#1: 用32个 5×5的filter(提取5×5个像素区域),用ReLU激活函数。
2. 池化层#1:用最大池化,2×2的filter,步长为2(这样池化层就不会有重叠)
3. 卷积层#2:用64个 5×5的filter。用ReLU作为激活函数。
4. 池化层#2:同样的用2×2的filter,步长为2
5. 全连接层#1:1024个神经元,用dropout来进行正则化,rate为0.4(意味着40%的神经元在每次迭代时,会被随机失效)
6. 全连接层#2(logits Layer):10个神经元,每个对应一个数字0到9

tf.layers模块包含了对应上边3中不同层的方法:
– conv2d(). 创建一个二维的卷积层,参数包含:filter个数, filter kernel size,padding,激活函数。
– max_pooling2d(). 创建一个二维的池化层,使用最大池化层算法。用filter size和步长作为参数。
– dense(). 创建一个全连接层,用神经元数量和激活函数作为参数。
这些method的都接受一个tensor作为input,并返回一个转化后的tensor作为output。这样就可以让一层层连接起来。一个层的output可以作为下一层的input。

我们在cnn_mnist.py里增加下边的cnn_model_fn 方法,它构建了TensorFlow Estimator API期望的接口。cnn_minist.py用MNIST 的feature和label数据,并用model mode(TRAIN,EVAL,PREDICT)作为参数;配置CNN;返回预测值;loss;和一个training的操作:


def cnn_model_fn(features,labels,mode):
    """ Model function for CNN"""
    # Input Layer
    input_layer = tf.reshape(features["x"],[-1,28,28,1]) #-1表示根据其余维度指定值自动计算该维度的值。-1目前的维度代表的是训练的条目数。
   
    # 卷积层 #1
    conv1 = tf.layers.conv2d(
        inputs = input_layer,
        filters = 32,
        kernel_size = [5,5],
        padding = "same",
        activation = tf.nn.relu)
   
    #池化层 #1
    pool1 = tf.layers.max_pooling2d(inputs=conv1,pool_size=[2,2],strides=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)
   
    # 全连接层
    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,trainning= mode ==tf.estimator.ModeKeys.TRAIN)
       
    # Logits layer
    logits = tf.layers.dense(inputs=dropout,units=10)
   
    predictions = {
        # 为 PREDICT 和EVAL 模型生成prediction
        "classes":tf.argmax(input=logits,axis=1),
        # 给图上增加‘softmax_tensor’,预测和‘logging_hook’会使用
        "probabilites":tf.nn.softmax(logits,name="softmax_tensor")
    }
   
    if mode == tf.estimator.ModeKeys.PREDICT:
        return tf.estimator.EstimatorSpec(mode=mode,predictions=predictions)
       
    # 计算loss(TRAIN和EVAL都需要)
    loss = tf.losses.sparse_softmax_cross_entropy(labels=labels,logits=logits)
       
        # 配置Training的操作(只是为了TRAIN 模式)
        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)
       
        # 添加 evaluation 指标
        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每个层的代码。同时也会讲如何计算loss,配置training op,生成predictions。

Input Layer

在layers的模块里,创建卷积层和池化层的方法需要输入的tensor有如下的shape:[batch_size,image_width,image_height,channels],定义如下:
– batch_size,进行梯度下降时,每次选择的样例数
– image_width,图片宽
– image_height,图片高
– channels,颜色通道数,彩色图片有3个(R,G,B)。单色图片只有一个灰度。

这里,我们的MNIST数据集是一个28×28灰度值的图片。所以它的shape是[batch_size,28,28,1]
为了把我们的输入feature对应到这个shape,我们可以执行下边的reshape操作:


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

注意我们把batch size标为-1,这表示batch size会自动计算,计算原则是用features[“x”]的元素个数除以其他维度设置的大小。这样batch size 就成为一个我们可以调节的超参数了。比如,我们的batch size为5, features[“x”]就包含3920个元素,每个元素代表一个图片里的一个像素的灰度值。input layer的shape是[5,28,28,1]。 类似,如果batch size是100,那么features[“x”]就包含78400个元素,shape是[100, 28, 28, 1]。

Convolutional Layer #1

在我们的第一个卷积层,我们想对输入应用32个 5×5 的filter。并用一个ReLU激励函数。我们可以用layers 模块下的conv2d()方法来创建这一层:


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

inputs 参数指定了我们的输入tensor,他必须有这样的shape:[batch_size,image_width,image_height,channels]. 这里我们把我们的第一个卷积层连接到我们的input_layer上,它有着这样的shape:[batch_size,28,28,1]

conv2d() 会接受这样的shape:[channels,batch_size,image_width,image_height],如果我们设置data_format=channels_frist

filters 参数指定filter个数,kernel_size指定了filter计算的区域大小[width,height]

如果kernel_size 长和宽一样大,你可以这样指定 kernel_size=5

padding 参数只有两个可选值,大小写敏感:valid和same。valid是默认值。用来指定输出的tensor大小和输入大小的关系。如果你指定为same,会给原图周围添加0值来保证输出和输入tensor的长宽一样。如果valid就不进行填充。对于28×28的tensor用5×5的卷积,就会输出24×24的tensor。因为在28×28的格子上,放置5×5的块,只有24×24个有效的位置。

activator参数指定了对卷积output应用的激励函数。在这里我们用tf.nn.relu。

我们conv2d()方法产生的output有着这样的shape:[batchsize,28,28,32]和输入tensor有着同样的宽度和高度。但是现在我们有32个channel,来自32个filter。

Pooling Layer #1

下来,我们把我们的第一个池化层(Pooling layer)连接到上边我们刚创建的卷积层。我们可以用layers里的max_pooling2d()方法去构造一个最大化池,它用一个2×2的filter,步长为2.


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

同样,inputs指定了输入的tensor,它的shape是[batch_size,iamge_width,image_height,channels],这里我们的输入tensor是 conv1,第一个卷积层的输出。它的输出是[batch_size,28,28,32]

就像conv2d(), max_pooling2d()会接受这样的shape:[channels,batch_size,image_width,image_height],如果我们设置data_format=channels_frist

pool_size 参数制定了最大池化filter的大小[width,height]。如果长宽一样,可以这样指定pool_size=2。
strides指定了步长,它表示filter每次移动的像素个数。这里指定为2,表示长和宽方向都是2,如果长和宽方向不一样,可以这样指定:stride=[3,6]。如果strides和pool_size大小一样就保证了filter移动时,处理的像素不会有重叠。

经过max_pooling2d()后,我们output的shape为[batch_size,14,14,32]: 2×2 filter减少了一半的长和宽。

卷积层#2 池化层#2

我们可以像上边一样通过conv2d()和max_pooling2d()给我们的网络加上第二层。对于卷积层#2,我们用64个5×5的filter。和ReLU activation。对于池化层#2,我们用和池化层#1 一样的设置。


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用的same padding,所以不会改变tensor的长宽,因为是64个filters,所以,输出shape为[batch_size,14,14,64]

Pooling layer #2,用了conv2 作为input,它减少了一半的长宽,输出size为[batch_size,7,7,64]

全连接层

接下来,我们加上一个全连接层。它有1024个神经元和一个ReLU激励函数。它用来给我们之前通过卷积和池化层提取出来的feature来进行分类。在我们连接到全连接层之前,我们首先把我们pool2层的输出进行展平。它的shape是[batch_size,features],这时的tesnor只有两维。


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

上边的reshape操作,-1表示batch_size 维是根据我们输入data的样例数动态计算的。每个pool2的输入宽为7,高维7,channel为64,所以features 维度有7x7x64,3136个,所以pool2_flat的shape为[batch_size,3136]。

现在,我们可以用layers 的dense()方法连接:


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

inputs参数指定了input tensor,我们展平后的feature map:pool2_flat,units 指定了全连接层的神经元个数。activation参数指定了激活函数。这里我们还是用ReLU。

为了改善我们的模型,我们还用了dropout 正则化在全连接层:


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

同样,inputs指定了input tensor,这里就是全连接层的输出。rate指定了dropout的概率。这里0.4表示每次训练有40%的神经元会被随机失效。

training参数表示模型现在是否是训练模式。只有在training为true时,droupout才有效。

我们的dropout输出的tensor shape 为[batch_size,1024]

Logits 层

我们的神经网络最后一层是Logits 层。它会返回我们预测的原始数据。我们创建了一个10个神经元的全连接层,每个输出代表一个分类(0-9)。这里没有指定激活函数,所以激活函数是默认值:线性激活函数。


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

我们最后的logits层的shape为[batch_size,10]

生成预测

logits层以原始数据的形式返回我们的预测,预测时一个[batch_size,10]这样的一个tensor。我们可以把原始值转化为2中不同的格式作为我们的模型返回:
– 对每个样本的预测的分类:一个数字(0-9)
– 每个分类的概率。

对于一个给定的样本,我们预测它的分类是根据在Logits输出的原始值里最大的那个。我们可以用下边的方法找到这个值的index:


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

input指定了用来寻找最大值的tensor,axis指定了要找的axis。这里我们需要找到inde为1的这个维度的最大值。它对应的是我们的预测值。因为在这里的shape是[batch_size,10]。

我们可以用tf.nn.softmax来帮我们生成概率值。


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

这里我们通过name参数,显式指定了softmax操作的名字。这样我们后边就能引用它。我们可以为softmax values设置logging “set up a logging hook”

我们把我们的预测放到一个字典里,然后返回一个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,predicitons=predictions)

计算loss

对于Training和Evaluation,我们需要定义一个loss function。它叫做损失函数,它的值用来描述模型预测的值和实际的值之间的差距。对于多分类的问题,比如MNIST问题,交叉熵是一种典型的损失值计算的办法。下边的代码在Trainning和Evaluation的时候来计算模型的交叉熵。


onehot_labels = tf.one_hot(indices=tf.cast(labels,tf.int32),depth=10)
loss = tf.losses.softmax_corss_entropy(
    onehot_labels=onehot_labels,
    logits=logits)

我们看一下上边的代码干了什么。
我们的labels tensor包含了一组我们样例实际代表数字的值。比如[1,9…],为了计算交叉熵,我们需要把它进行one-hot encoding。


[[0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
 ...]

我们可以用tf.one_hot方法来进行这个转换。tf.one_hot()有两个参数:
– indices,需要进行one hot encoding的值在tensor里的位置。
– depth,分类的个数。

下边的代码为我们的label创建了one-hot tensor。


onehot_labels = tf.one_hot(indices=tf.cast(labels,tf.int32),depth=10)

因为labels包含了一个值的序列,每个值取值可以是0-9中的一个。indices就是我们的labels tensor把值转化成integers。depth是10,因为我们一共分为10类。

下来,我们计算onehot_labels和我们logits layer的预测值的softmax值的交叉熵。tf.losses.softmax_cross_entropy()接收onehot_labels和logits作为参数。对logits应用softmax激活函数。计算交叉熵,然后返回一个标量的tensor作为loss:


loss = tf.losses.softmax_cross_entropy(
    onehot_labels=onehot_labels, logits=logits)

配置Training Op

在上边我们定义了我们CNN的loss,他是label和我们预测值的softmax 交叉熵。我们配置我们的模型来在训练过程中优化这个loss值。 我们设置learning rate为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)

如果你想更详细的看如何给Estimator定义 training ops。可以看这篇文章

添加 Evluation的量度

为了给我们的模型增加准确性的量度,在EVAL模式下,我们像下边那样定义eval_metric_ops dict:


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)

发表评论

电子邮件地址不会被公开。 必填项已用*标注

%d 博主赞过: