从代码学AI——卷积神经网络(CNN)

今天和大家一起来看下基于TensorFlow实现CNN的代码示例,源码参见Convolutional_netWork。使用MNIST数据集进行训练和预测,下面开始代码注解。

'''
这里从源码的角度详细说明使用TensorFlow如何简单的实现卷积神经网络

使用MNIST数据集进行卷积神经网络的实现

使用MNIST数据集

数据集参见:http://yann.lecun.com/exdb/mnist/)

源码地址: 
    
    https://github.com/aymericdamien/TensorFlow-Examples/blob/master/examples/3_NeuralNetworks/convolutional_network.py

'''

导入相关方法和模块

from __future__ import print_function

import tensorflow as tf

# 导入 MNIST 数据
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)

基本参数设置

# 参数设置
#学习速率(也有称为步长的)
learning_rate = 0.001
#迭代的次数
training_iters = 200000
#每个批次的数据大小(这里表示每个批次使用128张图片)
batch_size = 128
#每10次进行一次结果输出
display_step = 10

#网络相关参数
#输入数据,每个待处理图片是28*28的矩阵,可以理解为将其拉平则变为一个长度为784的数组
n_input = 784 
#分类数量,最终是解决分类问题,这里分为10类,卷积神经网络也只是替代传统机器学习中特征提取的这个过程
n_classes = 10
#随机失活概率,也有叫留存率的,即在dropout层,以这个概率进行随机失活,进而减少过拟合
dropout = 0.75

基本输入的占位

# 图的输入参数定义,TensorFlow中会将各个Option编排成一个DAG(有向无环图)
# 这里定义输入,可以理解为采用占位符进行站位,待实际运行时进行具体填充,这里None表示运行时指定
# [参见]tf.placeholder使用
x = tf.placeholder(tf.float32, [None, n_input])
y = tf.placeholder(tf.float32, [None, n_classes])
#失活概率
keep_prob = tf.placeholder(tf.float32)

定义卷积神经网络

# 该函数实现卷基层的定义
# 在卷积神经网络中,往往存在多层卷基层,这里将卷积的定义当初抽出来
# 实现很简单,即采用卷积核(或者理解成滤波器)W对待处理数据x进行卷积计算(理解成在图像上平移过滤,当然也会处理深度的问题)
# 在每个窗口都可以理解成进行窗口内的WX+b这样的计算(这里的X是窗口内的数据,W是卷积核)
def conv2d(x, W, b, strides=1):
    #进行卷积计算,padding = 'SAME'表示处理后输出图像大小不变,如果valid则是变动的
    #一般在多层卷积中,往往会控制图像大小不变,否则处理起来比较困难,每层处理都要考虑图像大小
    #[参见]卷积计算
    x = tf.nn.conv2d(x, W, strides=[1, strides, strides, 1], padding='SAME')
    x = tf.nn.bias_add(x, b)
    #对结果进行非线性激活
    #[参见]激活函数
    return tf.nn.relu(x)

定义池化操作

# 进行池化操作,这里进行最大池化
# 池化可以达到降低数据量、利用更过的局部信息的目的
# 这里k默认为2,移动的步长是2,那么图像处理后相当于减小一半
# [参见]池化操作
def maxpool2d(x, k=2):
    return tf.nn.max_pool(x, ksize=[1, k, k, 1], strides=[1, k, k, 1],
                          padding='SAME')

构建卷积神经网络

# 进行卷积神经网络的构建
# 这里的网络层级关系为:卷积(Conv)->池化(Pooling)->卷积(Conv)->池化(Pooling)->全连接(包含非线性激活,FullConnect)->随机失活(Dropout)->(Class Pre)分类
def conv_net(x, weights, biases, dropout):
    
    #可以简单理解为,将输入数据还原为28*28的矩阵
    #输入x为[128,784] 经过reshape后变为 [128,28,28,1]的tensor(依次为[batchSize,width,height,channel])
    x = tf.reshape(x, shape=[-1, 28, 28, 1])

    #卷积层1
    #卷积核为[5,5,1,32]([width,height,input_channel,out_channel])即采用5*5的卷积核,输入图像的通道是1,输出通道是32
    #[128,28,28,1]的输入,采用[5,5,1,32]的filter,得到[128,28,28,32]输出
    conv1 = conv2d(x, weights['wc1'], biases['bc1'])
    #池化层 池化后得到[128,14,14,32]的输出
    conv1 = maxpool2d(conv1, k=2)

    #卷积层2
    #上面的结果是32通道的,所以这里的卷积核采用[5,5,32,64],输出通道是64
    #卷积计算后输出[128,14,14,64]
    conv2 = conv2d(conv1, weights['wc2'], biases['bc2'])

    #池化层 输出[128,7,7,64]
    conv2 = maxpool2d(conv2, k=2)

    # 全连接层
    # reshape后数据变成[128,7*7*64]
    fc1 = tf.reshape(conv2, [-1, weights['wd1'].get_shape().as_list()[0]])
    #矩阵乘法 [128,7*7*64] X [7*7*64,1024] -> [128,1024]
    fc1 = tf.add(tf.matmul(fc1, weights['wd1']), biases['bd1'])
    #非线性激活,这里采用relu
    fc1 = tf.nn.relu(fc1)

    #失活层
    #对结果进行随机失活
    fc1 = tf.nn.dropout(fc1, dropout)

    # 分类 实际上就是做WX+b 这个运算
    #[128,1024]X[1024,10] ->[128,10] 即得到每个图片在10个分类中的得分
    # 如某个图片分类结果是[0.1, 0.0, 0.01, 0.3, 0.8, 0.2, 0.1, 0.1, 0.0, 0.2],那么最大的0.8这一列表示的类别即为预测类别
    out = tf.add(tf.matmul(fc1, weights['out']), biases['out'])
    return out

权重等相关参数

weights = {
    # 第一层的过滤器,采用[5,5,1,32]的filter输出通道是32
    'wc1': tf.Variable(tf.random_normal([5, 5, 1, 32])),
    # 第二层的过滤器,输入通道是32,输出通道是64
    'wc2': tf.Variable(tf.random_normal([5, 5, 32, 64])),
    # 全连接层,输入数据是7*7*64,输出是1024
    'wd1': tf.Variable(tf.random_normal([7*7*64, 1024])),
    # 分类,权重矩阵是[1024,10],输出10个分类
    'out': tf.Variable(tf.random_normal([1024, n_classes]))
}

biases = {
    'bc1': tf.Variable(tf.random_normal([32])),
    'bc2': tf.Variable(tf.random_normal([64])),
    'bd1': tf.Variable(tf.random_normal([1024])),
    'out': tf.Variable(tf.random_normal([n_classes]))
}


定义优化(迭代)以及准确率相关

#构建网络
pred = conv_net(x, weights, biases, keep_prob)

#定义优化目标(损失函数,这里采用交叉熵)
#[参见] 交叉熵 https://www.tensorflow.org/api_docs/python/tf/nn/softmax_cross_entropy_with_logits
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=pred, labels=y))
#采用Adam方法进行优化
#[参见]优化方法
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost)

#进行效果评估
#看预测值和实际值是否一致
correct_pred = tf.equal(tf.argmax(pred, 1), tf.argmax(y, 1))

#计算准确率
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))

开始进行循环迭代

#下面开始构建计算图
# 首先,开始初始化所有的变量
init = tf.global_variables_initializer()
# 然后,开始准备构建图
with tf.Session() as sess:
    sess.run(init)
    step = 1
    while step * batch_size < training_iters:
        #获得批次内的训练数据
        batch_x, batch_y = mnist.train.next_batch(batch_size)

        # 进行batch优化
        sess.run(optimizer, feed_dict={x: batch_x, y: batch_y,
                                       keep_prob: dropout})
        #每10次输出一下结果
        if step % display_step == 0:
            # 计算当前批次内的损失值和准确率
            loss, acc = sess.run([cost, accuracy], feed_dict={x: batch_x,
                                                              y: batch_y,
                                                              keep_prob: 1.})
            print("迭代次数 " + str(step*batch_size) + ", 当前损失值= " + \
                  "{:.6f}".format(loss) + ",  Accuracy= " + \
                  "{:.5f}".format(acc))
        step += 1
    print("优化完成!")

    # 进行准确率测试,这里使用256张测试图片
    print("测试准确率:", \
        sess.run(accuracy, feed_dict={x: mnist.test.images[:256],
                                      y: mnist.test.labels[:256],
                                      keep_prob: 1.}))


看完整个代码,其实从结构和代码数量上,已经很简洁了,相比之前的基于TFLearn的LSTM麻烦了一点,也多出来了一些概念,这里会重点对卷积、池化、失活做重点解释,但不会太涉及公式相关,只做物理层面的解释和方法注释。


卷积

本节的重点在于:
1.理解相关的函数定义,不会重点阐释卷积的意义,而是重点对函数定义进行解释;
2.如何快速计算卷积后的shape

简单不负责任的对于卷积进行一个物理意义上的解释即:
一个函数(如:单位响应)在另一个函数(如:输入信号)上的加权叠加
即卷积的物理意义可以理解为:加权叠加

tensorflow的api_guide中对卷积操作的定义如下

The convolution ops sweep a 2-D filter over a batch of images, 
applying the filter to each window of each image of the appropriate size. 
The different ops trade off between generic vs. specific filters。

个人翻译如下:
卷积操作实际上是用卷积核在图像上进行扫描过滤,将卷积核应用到适当大小的图片的每个窗口。tensorflow提供了不同的卷积实现,不同的实现在通用性和特定功能之间做取舍。

TensorFlow中提供了conv2d,depthwiseconv2d,separableconv2d 三个Op:

a. conv2d: 可以将通道混合在一起的任意滤波器

b. depthwise_conv2d: 在每个通道上独立工作的滤波器

c. separable_conv2d: 深度方向的空间滤波器,后跟一个点滤波器。

在示例代码中使用了conv2d,所以这里只重点介绍conv2d

tf.nn.conv2d

conv2d(
    input,
    filter,
    strides,
    padding,
    use_cudnn_on_gpu=None,
    data_format=None,
    name=None
)

对4维的输入和卷积核进行二维的卷积操作。

input:给定输入tensor,输入数据的形式为[batch,in_height,in_width,in_channels]即[批次大小,图像高度,图像宽度,通道数量]。

filter:给定卷积核的tensor,形式为[filter_height,filter_width,in_channels,out_channels]即[卷积核的高度,卷积核的宽度,输入通道数,输出通道数量]

注意这里input的in_channels和filter中的in_channels是一样的。

进行二维卷积操作大致流程如下:

  1. 将卷积核的维度转换成一个二维的矩阵形状 [filter_height * filter_width * in_channels, output_channels]

  2. 对于每个批处理的图片,我们将输入张量转换成一个虚拟的数据维度 [batch, out_height, out_width, filter_height * filter_width * in_channels]

  3. 对于每个批处理的图片,我们右乘以卷积核,得到最后的输出结果

参数说明
input:输入Tensor,4维且数据类型必须为(half, float32, float64)中之一. tensor中每个维度表示的含义和data_format相关

filter: 代表卷积核的定义的tensor,数据类型和input一致。4维张量,每个维度表示的意义为[filter_height, filter_width, in_channels, out_channels]

strides: 一个长度是4的一维整数类型数组,每一维度对应的是 input 中每一维的对应移动间隔,比如,strides[1] 对应 input[1] 的移动间隔,通常每个维度的移动间隔相同且置为1.


padding: 一个取值为 "SAME"或"VALID"的字符串. 决定边缘填充的算法

use_cudnn_on_gpu: 可选参数,默认为True,表示启用GPU


data_format: 一个取值为 "NHWC"或 "NCHW"的可选变量,默认为"NHWC". 如果为"NHWC"则输入数据每个维度表示的意思为[batch, height, width, channels]. 否则表示为[batch, channels, height, width].

name: 可选变量,表示当前OP的名称.

实例说明

实例代码如下:

import numpy as np
import tensorflow as tf

input_data = tf.Variable( np.random.rand(1,6,6,3), dtype = np.float32 )
filter_data = tf.Variable( np.random.rand(3, 3, 3, 1), dtype = np.float32)

y = tf.nn.conv2d(input_data, filter_data, strides = [1, 1, 1, 1], padding = 'VALID')

with tf.Session() as sess:
    init = tf.initialize_all_variables();
    sess.run(init);
    print (sess.run(y));
    print (sess.run(tf.shape(y)));

输出为:

#卷积结果
[[[[ 6.2532773 ]
   [ 6.09991837]
   [ 4.29529333]
   [ 5.97482729]]

  [[ 5.49180269]
   [ 4.88662767]
   [ 4.4858675 ]
   [ 4.94966698]]

  [[ 5.19242811]
   [ 6.68614483]
   [ 6.02896595]
   [ 5.76866436]]

  [[ 6.8271122 ]
   [ 6.84668255]
   [ 6.29273748]
   [ 5.54653883]]]]

#卷积结果的shape
[1 4 4 1]

如果Padding改为SAME则结果为

[[[[ 3.13691545]
   [ 4.51995659]
   [ 4.12897968]
   [ 3.91766047]
   [ 4.65173292]
   [ 3.75577402]]

  [[ 4.15843153]
   [ 6.25889444]
   [ 6.30766678]
   [ 6.38891935]
   [ 7.19078541]
   [ 4.80262566]]

  [[ 4.68451309]
   [ 6.82779837]
   [ 6.46818972]
   [ 5.57897949]
   [ 6.88392735]
   [ 4.24736738]]

  [[ 4.93537664]
   [ 7.15243053]
   [ 5.66338301]
   [ 4.92590523]
   [ 5.60546398]
   [ 3.04639673]]

  [[ 4.68692017]
   [ 6.24736881]
   [ 5.82442379]
   [ 3.48279071]
   [ 3.68962264]
   [ 2.33093119]]

  [[ 2.54168987]
   [ 3.39097095]
   [ 3.19667697]
   [ 2.36519623]
   [ 2.3597281 ]
   [ 1.1976347 ]]]]

    [1 6 6 1]

从上面的例子可以看出当Padding参数为valid的时候,卷积核移动到边缘的时候,不采用填充算法,直接终止,那么输出结果的宽高为input_width - filter_width+1,input_height-filter_height+1(此时stride[1]=stride[2]=1)即(6-3+1,6-3+1) = (4,4)

当strides改为[1,2,2,1]时结果为

[[[[ 5.08089352]
   [ 5.5374465 ]]

  [[ 5.4308424 ]
   [ 6.23833847]]]]

[1 2 2 1]

所以输出的宽高(padding=valid时),计算公式为

(input_width - filter_width + 1)/strides[1],(input_height - filter_height + 1)/strides[2]

重点

这里需要强调2点:

a. 1x1的filter是有意义的,这个在以后,我会再解释一下

b.padding的意义在于不去处理图像size的问题,如果网络中处理的Image的size总是变来变去,那么网络在设计和处理时会很麻烦,所以一般会选择SAME


池化(Pooling)

按照TensorFlow的API文档解释,池操作对输入张量进行矩形窗口扫描并计算的缩减操作(即计算窗口内的平均值、最大值、带参数的最大值)。

一般来说池化层往往跟在卷积层后面,通过avg_pool或者max_pool的方法将之前卷基层得到的特征做一个聚合统计。假设前一层的卷积层得到的某一特征图是500 * 500,那么如果选择一个2 * 2的窗口做池化,那么池化层会输出250 * 250的图(窗口不重叠),这就达到了降低数据量的目的。

换一种解释,一般来说图像具有一种“静态性”的属性,这也就意味着在一个图像区域有用的特征极有可能在另一个区域同样适用。例如,卷积层输出的特征图中两个相连的点的特征通常会很相似,假设a[0,0],a[0,1],a[1,0],a[1,1]都表示颜色特征是红色,没有必要都保留作下一层的输入。池化层可以将这四个点做一个整合,输出红色这个特征。可以达到降低模型的规模,加速训练的目的。

TensorFlow中对pooling相关的函数有如下六个:

  1. tf.nn.avg_pool

  2. tf.nn.max_pool

  3. tf.nn.max_pool_with_argmax

  4. tf.nn.avg_pool3d

    进行3D平均池化,后面不详细介绍

  5. tf.nn.fractional_avg_pool

    在池化区域执行局部平均池化,目前只支持在row和column上进行局部平均池化,需要分别制定在row和column上的比例,在这两个方向上进行独立的平均池化操作,后面不详细介绍。

  6. tf.nn.fractional_max_pool

    在池化区域执行局部最大池化,目前只支持在row和column上进行局部平均池化,需要分别制定在row和column上的比例,在这两个方向上进行独立的最大池化操作,后面不详细介绍。

示例中使用了max_pool,所以这里只对最大池化进行函数解释。

max_pool
tf.nn.max_pool
    max_pool(
        value,
        ksize,
        strides,
        padding,
        data_format='NHWC',
        name=None
    )

这个函数的作用是计算池化区中元素的最大值

参数说明
value: 4-D 的输入 Tensor,shape为 [batch, height, width, channels] 数据类型是 float32, float64, qint8, quint8, 或者qint32.

ksize: 一个长度不小于4的整型数组,数组中每一个值对应于输入tensor中每一维的窗口大小.

strides: 一个长度不小于4的整型数组,数组中每个值对应输入tensor上每个维度滑动窗口的移动步长。

padding: 字符变量,取值为'VALID'或'SAME',表示移动到边界的处理方法    
data_format: 表示输入数据的格式,字符变量,取值为'NHWC'或'NCHW'.

name: 当期OP的名字.

使用实例:

# -*- coding: utf-8 -*-
import numpy as np
import tensorflow as tf

input_data = tf.Variable( np.random.rand(1,6,6,3), dtype = np.float32 )
filter_data = tf.Variable( np.random.rand(2, 2, 3, 10), dtype = np.float32)

y = tf.nn.conv2d(input_data, filter_data, strides = [1, 1, 1, 1], padding = 'VALID')
output = tf.nn.max_pool(value = y, ksize = [1, 2, 2, 1], strides = [1, 1, 1, 1], padding = 'VALID')

with tf.Session() as sess:
    init = tf.initialize_all_variables()
    sess.run(init)
    print (' --convResult--')
    print (sess.run(y))
    print (' --poolResult--')
    print (sess.run(output))
    print (sess.run(tf.shape(output)))

Dropout

tflearn.layers.core和tf.nn都包含dropout这个函数,功能相同,只差一个参数,这里合在一起解释

概念解释:

简单理解可以将Dropout理解为为了防止模型过拟合而采用的一种trick的方法。
进一步讲,Dropout是指在深度学习网络的训练过程中,对于神经网络单元,按照一定的概率将其暂时从网络中丢弃。注意是暂时,例如对于随机梯度下降来说,由于是随机丢弃,故而每一个mini-batch都在训练不同的网络。

再简明一些,Dropout说的简单一点就是我们让在前向传导的时候,让某个神经元的激活值以一定的概率p,让其停止工作。

总结而言,Dropout的思想是训练整体DNN,并平均整个集合的结果,而不是训练单个DNN模型,DNNs是以概率p舍弃部分神经元,其它神经元以概率q=1-p被保留,舍去的神经元的输出都被设置为零。

函数定义:

tflearn.layers.core.dropout (x, keep_prob, noise_shape=None, name='Dropout')
和 tf.nn.dropout

    dropout(
        x,
        keep_prob,
        noise_shape=None,
        seed=None,
        name=None
    )

参数说明:

x : 待处理的Tensor

keep_prob : 浮点数,每个元素被保留或放电的概率

noise_shape : 一个一维的Tensor,数据类型是int32。代表元素保留/失活是否独立的标志
    
name : OP的名称

seed :一个Python整数。 用于创建随机种子
方法说明

一个神经元将以概率keep_prob决定是否保留或放电,如果不保留或放电,那么该神经元的输出将是0,如果该神经元保留或放电,那么该神经元的输出值将被放大到原来的1/keep_prob倍。这里的放大操作是为了保持神经元输出总个数不变。例如,神经元的值为[2,1],keep_prob的值是0.5,并且是第一个神经元是保留,第二个神经元不保留,那么神经元输出的结果是[4,0],也就是相当于,第一个神经元被当做了1/keep_prob个输出,即2个。这样保证了总和2个神经元保持不变。

默认情况下,每个神经元是否放电是相互独立的。但是,如果noise_shape被修改了,那么这对于待处理的张量x就是一个广播形式,而且当且仅当 noise_shape[i] == shape(x)[i] ,x中的元素是相互独立的,否则要么保留,要么保持同步。这里举个例子说明:

如果 shape(x) = [k, l, m, n], noise_shape = [k, 1, 1, n] ,那么每个批和通道都是相互独立的(这里k表示批次,n表示通道数量),但是每行和每列(l,m表示行和列或者宽和高)的数据都是关联的,即要么为0,要么保持不变,即不是相互独立的。

为了感受一下noise_shape的作用,运行如下代码

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import tensorflow as tf

a = tf.constant([[-1.0, 2.0, 3.0, 4.0]])
with tf.Session() as sess:
    b = tf.nn.dropout(a, 0.5, noise_shape = [1,4])
    print (sess.run(b)) #[[-0.  0.  0.  8.]]
    b = tf.nn.dropout(a, 0.5, noise_shape = [1,1])
    print (sess.run(b)) #[[-2.  4.  6.  8.]]
    c = tf.nn.dropout(a, 0.5, noise_shape = [1,1])
    print (sess.run(c)) #[[-0.  0.  0.  0.]]
    d = tf.nn.dropout(a, 0.5, noise_shape = [1,1])
    print (sess.run(d)) #[[-0.  0.  0.  0.]]
    e = tf.nn.dropout(a, 0.5, noise_shape = [1,1])
    print (sess.run(e)) #[[-0.  0.  0.  0.]]

输出如下(代码中也已经标注了):

    [[-0.  0.  0.  8.]]
    [[-2.  4.  6.  8.]]
    [[-0.  0.  0.  0.]]
    [[-0.  0.  0.  0.]]
    [[-0.  0.  0.  0.]]

这就说明了如果noise_shape[i] == shape(x)[i],那么保持独立,否则不是相互独立的。

相关论文

JMLRdropout


总结

本文对如何使用TensorFlow实现CNN相关功能做了注解,对代码中引入的部分新的概念做了解释和说明,总体而言在实现和使用上是相对简单的,但还是需要理解其物理意义才能很好的使用CNN,让其发挥更多作用。一般而言其用来处理图像是首选,但目前越来越多的人使用其来处理自然语言问题,如长文本,感兴趣的可以对其进行更多的探索。

你可能感兴趣的:(从代码学AI——卷积神经网络(CNN))