卷积神经网络(CNN)及其实践

卷积神经网络(CNN)及其实践

一、CNN 的基础概念先行

1.1 CNN 的基本结构简介

  • 首先,我们应该明确 CNN 是被成功应用的 DNN 模型之一,它们并不独立。特别是针对图片类数据集的时候,我们发现针对一张 28 x 28 (784)像素的图片喂给 全连接网络需要优化的参数就有 397510 个参数(近40万)。如下图,故如果我们将真实生活中的高分辨率的彩色图像直接喂给全连接网络,则待优化的参数就更多了。然而待优化的参数过多,就容易出现过拟合现象。实际中,我们会先对原始图像进行特征提取,把提取到的特征喂给全连接网络,再让全连接网络计算出分类的评估值,而卷积便是一种有效的特征提取方法,故 CNN 结构也是这样设计的,由卷积层 + 池化层,不断组合最后在经过一层或者多层全连接网络,进行识别、分类、或者检测操作。 故我们便可以把卷积认为是一种有效的特征提取方法。

这里写图片描述

  • 接下来,我们来看看 CNN 的基本结构,图如下:

这里写图片描述

  • 图中是一个识别的CNN模型。最左边的图片船是输入层(对计算机来说,即二维矩阵),然后是卷积层(Convolution Layer),它是CNN特有的,卷积层的激活函数使用 ReLU。即 ReLU(x)=max(0,x)。在卷积层后面便是池化层(Pooling layer),它也是CNN特有,注意:池化层没有激活函数。

  • 卷积层+池化层的组合可以在隐藏层出现很多次,上图中循环出现了两次。而实际上这个次数是根据模型的需要而来的。常见的CNN都是若干卷积层+池化层的组合,如上图。当然我们可以尝试 卷积 和 池化的各种组合,如1212,2121,121121等等排列。

  • 在若干卷积层+池化层后面是全连接层(Fully Connected Layer, 简称FC),全连接层其实就是我们前面讲的DNN结构,只是输出层使用了Softmax激活函数来做图像识别的分类。

1.2、认识卷积

  • 首先,在去学卷积层的模型原理前,我们需要了解什么是卷积,以及CNN中的卷积是什么样子的。
  • 大家学习数学时都有学过卷积的知识,微积分中卷积的表达式为:
    S ( t ) = x ( t a ) w ( a ) d a
  • 离散形式是: s ( t ) = a x ( t a ) w ( a )

  • 它用矩阵表示可以为: s ( t ) = ( X W ) ( t ) ,其中星号表示卷积。以上都是以为卷积,而二维卷积最为典型的例子便是图像卷积,先说说数学中的二维卷积,公式如下:


    s ( i , j ) = ( X W ) ( i , j ) = m n x ( i m , j n ) w ( m , n )
  • 在CNN中,虽然我们也是说卷积,但是我们的卷积公式和严格意义数学中的定义稍有不同,比如对于二维的卷积,定义为:


    s ( i , j ) = ( X W ) ( i , j ) = m n x ( i + m , j + n ) w ( m , n )
  • 其中,我们叫W为我们的卷积核,而X则为我们的输入。如果X是一个二维输入的矩阵,而W也是一个二维的矩阵。但是如果X是多维张量,那么W也是一个多维的张量。

1.3、CNN 中的卷积层

  • 有了卷积的基本知识,我们现在来看看CNN中的卷积,假如是对图像卷积,回想我们的上一节的卷积公式,其实就是对输入的图像的不同局部的矩阵和卷积核矩阵各个位置的元素相乘,然后相加,加上偏置项后得到。
  • 接着,举一个例子如下,下面这个绿色的 5x5x1 输入矩阵,卷积核是一个下面这个黄色的 3x3x1 的矩阵。卷积的步幅是一个像素。则卷积的过程如下面的动图。卷积的结果是一个 3x3x1 的矩阵。

这里写图片描述

  • 注意,这里的输入图像并没有填充(Padding),对于没有填充的情况,我们可以用下列公式计算输出。
    = + 1 /

    ,上图中为:(5 - 3 + 1)/ 1 = 3,即输出图片为 3x3 分辨率,用了 1 个卷积核,故输出深度为 1(等于卷积核个数),故最后输出的是 3x3x1 的图片。其中,一般卷积核的第 3 个维度一般与输入的第三个维度有关,比如,如果输入为 rgb 三通道的如 5x5x3 则卷积核第 3 个维度也为3 ,如 3x3x3。

  • 有时,我们可能对输入做全零填充,或者其他,如果是全零填充,就可以保持图片输入维度和输出维度保持一样,针对 5x5x1 的图片,我们可以按照
    = ( + 2 x ) / + 1

    来计算单侧边填充的数目,如输入 32x32x3,核为 5x5x3,不用全零填充,则输出 (32-5+1)/ 1 = 28 ,如果我们希望输出也是 32 像素的,则可以用上面公式计算 32 = (32 -5 + 2P) / 1 + 1, 得出 P= 2,则应该填充2层 0 。

这里写图片描述

  • tensorflow 中卷积对于填充 Padding 给出的标志位有 SAME(填充)、VALID(不全 0 填充),输出维度计算公式如下图:

这里写图片描述

  • 上面举的例子都是二维的输入,卷积的过程比较简单,那么如果输入是多维的呢?如输入的是对应RGB的彩色图像,即是三个分布对应R,G和B的矩阵呢?
  • 在斯坦福大学的cs231n的课程上,有一个动态的例子,cs231n图示。建议大家对照着例子中的动图看下面的讲解。


    • 这里面输入是3个7x7的矩阵。实际上原输入是3个5x5的矩阵。只是在原来的输入周围加上了1的padding,即将周围都填充一圈的0,变成了3个7x7的矩阵(或者说是 7x7x3 的张量)。
    • 例子里面使用了两个卷积核(或者我们说卷积的深度是 2,而输出的深度与这里保持一致),我们先关注于卷积核W0。由于输入是7x7x3的张量,则我们对应的卷积核W0也必须最后一维是3的张量,这里卷积核W0的单个子矩阵维度为3x3。那么卷积核W0实际上是一个3x3x3的张量。这里的步幅为2,也就是每次卷积后会移动2个像素的位置。
    • 卷积过程,即两个矩阵对应位置的元素相乘后相加。这里是张量的卷积,即两个张量的3个子矩阵卷积后,再把卷积的结果相加后再加上偏倚b。
    • 7x7x3 的张量和 3x3x3的卷积核张量W0卷积的结果是一个3x3的矩阵。由于我们有两个卷积核W0和W1,因此最后卷积的结果是两个3x3的矩阵。或者说卷积的结果是一个3x3x2的张量。(故这里输出的最后一个维度,又称深度,等于卷积核的个数)
    • 对于卷积后的输出,一般会通过ReLU激活函数,将输出的张量中的小于0的位置对应的元素值都变为0。

1.4、CNN 中的池化层

  • 所谓的池化,即对输入张量的各个子矩阵进行压缩,用于减少特征数量。是降采样(subsampling)的一种方法,它们是降低信息的数量和复杂性的同时保存重要信息。假如是2x2的池化核,那么就将子矩阵的每2x2个元素变成一个元素,如果是3x3核的池化,那么就将子矩阵的每3x3个元素变成一个元素,这样输入矩阵的维度就变小了。
  • 池化标准有2个,MAX或者是Average。即取对应区域的最大值或者平均值作为池化后的元素值。
  • 下面这个例子采用取最大值的池化方法。同时采用的是2x2的池化。步幅为2。
  • 首先对红色2x2区域进行池化,由于此2x2区域的最大值为6.那么对应的池化输出位置的值为6,由于步幅为2,此时移动到绿色的位置去进行池化,输出的最大值为8.同样的方法,可以得到黄色区域和蓝色区域的输出值。最终,我们的输入4x4的矩阵在池化后变成了2x2的矩阵。进行了压缩。

  • 最大值池化可提取图片的纹理,均值池化可保留背景特征

这里写图片描述

  • 这里在提一下,训练时候可能会采用的操作 Dropout(舍弃),在神经网络的训练过程中,为了进一步减少过多的参数会使用 dropout,将一部分神经元按照一定概率从神经网络中舍弃在使用神经网络预测时,又恢复过来。Dropout 可以有效的减少过拟合现象。

这里写图片描述

二、在 TensorFlow 中使用卷积的相关函数简介

2.1 常用基础函数

1、tf.get_collection(”) 函数表示从 collection 集合中取出全部变量生成一个列表。它常用与损失函数收集和运行计算。以下以 L2_norm 为例。

regularizer = tf.contrib.layers.l2_regularizer(Regularization_Rate)      # 创建regularizer对象,Regularization_Rate表示正则项在loss function中所占的比重
loss_W = regularizer(W)      # 计算权重矩阵W所对应的L2 Norm
  • 但是,在神经网络中往往存在着多个权重矩阵W1,W2…Wn,因此最终计算得到的正则项是所有这些权重矩阵所对应的正则项之和,即:
loss = regularizer(W1)+regularizer(W2)+...+regularizer(Wn)
  • 这样计算loss的代码过于冗长的问题,而且当权重矩阵很多的时候容易出现遗漏。我们希望在每次创建权重矩阵后能立即计算出它所对应的正则项。故就到了 tf.get_collection()出场了
W1 = tf.get_variable('weights_1',shape,tf.random_normal_initializer())      # 创建权重矩阵W1
tf.add_to_collection('losses',regularizer(W1))      # 将权重矩阵W1对应的正则项加入集合losses
W2 = tf.get_variable('weights_2',shape,tf.random_normal_initializer())      # 创建权重矩阵W2
tf.add_to_collection('losses',regularizer(W2))      # 将权重矩阵W2对应的正则项加入集合losses
...
Wn = tf.get_variable('weights_n',shape,tf.random_normal_initializer())      # 创建权重矩阵Wn
tf.add_to_collection('losses',regularizer(Wn))      # 将权重矩阵Wn对应的正则项加入集合losses

losses_collection = tf.get_collection('losses')      # 以列表的形式获取集合losses中的值,每一个值都作为列表中的一个元素
loss = tf.add_n(losses_collection,name='loss')      # 计算列表中所有元素之和,得到的结果就是正则项的值

2、tf.add() 函数将参数列表对应元素相加。
3、tf.cast(x,dtype) 函数表示将参数 x 转换为指定数据类型。如下:

A = tf.convert_to_tensor(np.array([[1,1,2,4], [3,4,8,5]]))
print A.dtype
b = tf.cast(A, float32)
print b.dtype

结果输出:
<dtype: 'int64'>
<dtype: 'float32'>

4、tf.equal() 函数表示对比两个矩阵或者向量的元素。若对应元素相等,则返回 True;若对应元素不相等,则返回 False

A = [[1,3,4,5,6]]
B = [[1,3,4,3,2]]
with tf.Session( ) as sess:
    print(sess.run(tf.equal(A, B)))

输出结果: [[ True True True False False]]

5、tf.argmax(x,axis) 函数表示返回指定维度 axis 下,参数 x 中最大值索引号。

tf.argmax ([1,0,0],1) 函数中,axis为 1,参数 x[1 ,0,0] ,表示在参数 x 的第一个维度取最大值对应的索引号 ,故返回 0。

6、os.path.join() 函数表示把参数字符串按照路径命名规则拼接。

import os
os.path.join('/hello/','good/boy/','doiido')
输出结果: '/hello/good/boy/doiido'

7、字符串.split( )函数表示按照指定 “ 拆分符” 对字符串拆分, 返回拆分列表。例如:

'./model/mnist_model-1001'.split('/')[-1].split('-')[-1]

在该例子中,共进行两次拆分。第一个拆分符为‘ /’,返回拆分列表,并提取
列表中索引为-1 的元素即倒数第一个元素;第二个拆分符为‘ -’,返回拆分列
表,并提取列表中索引为-1 的元素即倒数第一个元素,故函数返回值为 1001

8、tf.Graph( ).as_default( )函数表示将当前图设置成为默认图,并返回一个上下文管理器。 该函数一般与 with 关键字搭配使用,一般应用于 将已经定义好的神经网络在计算图中复现

with tf.Graph().as_default() as g,表示将在 Graph()内定义的节点加入到计算图 g 中。  

9、神经网络模型的保存,在反向传播过程中,一般会间隔一定轮数保存一次神经网络模型, 并产生三个文件(保存当前图结构的.meta 文件、保存当前参数名的.index 文件、保存当前参数的.data 文件),在 Tensorflow 中如下表示:

saver = tf.train.Saver()
with tf.Session() as sess:
    for i in range(STEPS):
        if i % 轮数 == 0:
            saver.save(sess, os.path.join(MODEL_SAVE_PATH,
                      MODEL_NAME), global_step=global_step)

其中,tf.train.Saver()用来实例化 saver 对象。上述代码表示,神经网络每循环规定的轮数, 将神经网络模型中所有的参数等信息保存到指定的路径中,并在存放网络模型的文件夹名称中注明保存模型时的训练轮数。

10、神经网络模型的加载,在测试网络效果时,需要将训练好的神经网络模型加载,在 Tensorflow 中这样表示如下:

with tf.Session() as sess:
    ckpt = tf.train.get_checkpoint_state(存储路径)
    if ckpt and ckpt.model_checkpoint_path:
        saver.restore(sess, ckpt.model_checkpoint_path)

在 with 结构中进行加载保存的神经网络模型,若 ckpt 和保存的模型在指定路径中存在,则将保存的神经网络模型加载到当前会话中。

11、加载模型中参数的滑动平均值在保存模型时,若模型中采用滑动平均,则参数的滑动平均值会保存在相应文件中。 通过实例化 saver 对象, 实现参数滑动平均值的加载,在 Tensorflow 中如下表示:

ema = tf.train.ExponentialMovingAverage(滑动平均基数)
ema_restore = ema.variables_to_restore()
saver = tf.train.Saver(ema_restore)

12、神经网络模型准确率评估方法在网络评估时,一般通过计算在一组数据上的识别准确率, 评估神经网络的效果。在 Tensorflow 中这样表示:

correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))  

在上述中,y 表示在一组数据(即 batch_size 个数据)上神经网络模型的预测结果,y 的形状为[batch_size,10],每一行表示一张图片的识别结果。 通过tf.argmax()函数取出每张图片对应向量中最大值元素对应的索引值,组成长度为输入数据 batch_size 个的一维数组。通过 tf.equal()函数判断预测结果张量和实际标签张量的每个维度是否相等,若相等则返回 True,不相等则返回 False。通过 tf.cast() 函数将得到的布尔型数值转化为实数型,再 通过tf.reduce_mean()函数求平均值,最终得到神经网络模型在本组数据上的准确率。

13、实现断点续训, 在运行run 训练train_op 之前加上 ckpt (get_checkpoint_state)操作即可。

ckpt = tf.train.get_checkpoint_state(MODEL_SAVE_PATH)
if ckpt and ckpt.model_checkpoint_path:
    saver.restore(sess, ckpt.model_checkpoint_path)

14.tf.train.get_checkpoint_state(checkpoint_dir,latest_filename=None)该函数表示如果断点文件夹中包含有效断点状态文件,则返回该文件。

参数说明: checkpoint_dir: 表示存储断点文件的目录
latest_filename=None:断点文件的可选名称,默认为“ checkpoint”

15、saver.restore(sess, ckpt.model_checkpoint_path)该函数表示恢复当前会话,将 ckpt 中的值赋给 w 和 b。

参数说明: 
sess:表示当前会话,之前保存的结果将被加载入这个会话
ckpt.model_checkpoint_path:表示模型存储的位置,不需要提供模
型的名字,它会去查看 checkpoint 文件,看看最新的是谁,叫做什么。

16、tf.control_dependencies(control_inputs ) 它可以用来指定某些操作执行的依赖关系,它会返回一个控制依赖的上下文管理器,使用with关键字可以让在这个上下文环境中的操作都在control_inputs 之后执行。

 with g.control_dependencies([a, b, c]):
  # `d` and `e` will only run after `a`, `b`, and `c` have executed.
  d = ...
  e = ...
  • 注意: 控制依赖只对那些在上下文环境中建立的操作有效,仅仅在context中使用一个操作或张量是没用的,如下:
# Wrong
def my_func(pred, tensor):
  t = tf.matmul(tensor, tensor)
  with tf.control_dependencies([pred]):
    # The matmul op is created outside the context, so no control
    # dependency will be added.
    return t

# Right
def my_func(pred, tensor):
  with tf.control_dependencies([pred]):
    # The matmul op is created in the context, so a control dependency
    # will be added.
    return tf.matmul(tensor, tensor)
  • 在训练模型时我们每步训练可能要执行两种操作,op a, b 这时我们就可以使用如下代码:
with tf.control_dependencies([a, b]):
    c= tf.no_op(name='train')    # tf.no_op;没有实际的操作,仅仅作为一个名字叫 train的占位符
sess.run(c)

# 如果只是这样简单的要求,则可以将上面代码替换为:
c= tf.group([a, b])
sess.run(c)

2.2 卷积相关函数

下面是一些跟卷积相关的函数说明。
17、tf.nn.conv2d(·,·,·,·) ,是Tensorflow 给出的计算卷积的函数,函数中要给出四个信息:对输入图片的描述、对卷积核的描述、对卷积核滑动步长的描述以及是否使用 padding。

这里写图片描述

  • 对输入图片的描述: 用 batch 给出一次喂入多少张图片,每张图片的分辨率大小,比如 5 行 5 列,以及这些图片包含几个通道的信息, 如果是灰度图则为单通道,参数写 1,如果是彩色图则为红绿蓝三通道,参数写 3。

  • 对卷积核的描述:要给出卷积核的行分辨率和列分辨率、通道数以及用了几个卷积核。比如上图描述,表示卷积核行列分辨率分别为 3 行和 3 列,且是1 通道的,一共有 16 个这样的卷积核,卷积核的通道数是由输入图片的通道数
    决定的,卷积核的通道数等于输入图片的通道数,所以卷积核的通道数也是 1。
    一共有 16 个这样的卷积核,说明卷积操作后输出图片的深度是 16,也就是输出为 16 通道。

  • 对卷积核滑动步长的描述:上图第二个参数表示横向滑动步长,第三个参数表示纵向滑动步长。第一个 1 和最后一个 1 这里固定的。这句表示横向纵向都以 1 为步长。
  • 是否使用 padding:用的是 VALID。注意这里以字符串的形式给出VALID


    18、池化,最大池化用tf.nn.max_pool 函数,平均池化tf.nn.avg_pool 函数。函数中要给出四个信息,对输入的描述、对池化核的描述、对池化核滑动步长的描述和是否使用 padding。

这里写图片描述

  • 对输入的描述: 给出一次输入 batch 张图片、 行列分辨率、 输入通道的个数。
  • 对池化核的描述: 只描述行分辨率和列分辨率, 第一个和最后一个参数固定是 1。
  • 对池化核滑动步长的描述: 只描述横向滑动步长和纵向滑动步长, 第一个和最后一个参数固定是 1。
  • 是否使用 padding: padding 可以是使用零填充 SAME 或者不使用零填充VALID

2.3 对于卷积和池化操作的直观代码理解

  • 对于卷积的理解
import tensorflow as tf

#Generate the filename queue, and read the gif files contents
filename_queue = tf.train.string_input_producer(tf.train.match_filenames_once("data/test.gif"))
reader = tf.WholeFileReader()
key, value = reader.read(filename_queue)
image=tf.image.decode_gif(value)

#Define the kernel parameters
kernel=tf.constant(
        [
         [[[-1.]],[[-1.]],[[-1.]]],
         [[[-1.]],[[8.]],[[-1.]]],
         [[[-1.]],[[-1.]],[[-1.]]]
         ]            
    )

#Define the train coordinator
coord = tf.train.Coordinator()

with tf.Session() as sess:
    tf.initialize_all_variables().run()
    threads = tf.train.start_queue_runners(coord=coord)
    #Get first image
    image_tensor = tf.image.rgb_to_grayscale(sess.run([image])[0])
    #apply convolution, preserving the image size
    imagen_convoluted_tensor=tf.nn.conv2d(tf.cast(image_tensor, tf.float32),kernel,[1,1,1,1],"SAME")
    #Prepare to save the convolution option
    file=open ("blur2.png", "wb+")
    #Cast to uint8 (0..255), previous scalation, because the convolution could alter the scale of the final image
    out=tf.image.encode_png(tf.reshape(tf.cast(imagen_convoluted_tensor/tf.reduce_max(imagen_convoluted_tensor)*255.,tf.uint8), tf.shape(imagen_convoluted_tensor.eval()[0]).eval()))
    file.write(out.eval())
    file.close()
    coord.request_stop()
coord.join(threads)
  • 对于池化(下采样)的理解
import tensorflow as tf

#Generate the filename queue, and read the gif files contents
filename_queue = tf.train.string_input_producer(tf.train.match_filenames_once("data/test.gif"))
reader = tf.WholeFileReader()
key, value = reader.read(filename_queue)
image=tf.image.decode_gif(value)

#Define the  coordinator
coord = tf.train.Coordinator()

def normalize_and_encode (img_tensor):
    image_dimensions = tf.shape(img_tensor.eval()[0]).eval()
    return tf.image.encode_jpeg(tf.reshape(tf.cast(img_tensor, tf.uint8), image_dimensions))

with tf.Session() as sess:
    maxfile=open ("maxpool.jpeg", "wb+")
    avgfile=open ("avgpool.jpeg", "wb+")
    tf.initialize_all_variables().run()
    threads = tf.train.start_queue_runners(coord=coord)

    image_tensor = tf.image.rgb_to_grayscale(sess.run([image])[0])

    maxed_tensor=tf.nn.avg_pool(tf.cast(image_tensor, tf.float32),[1,2,2,1],[1,2,2,1],"SAME")
    averaged_tensor=tf.nn.avg_pool(tf.cast(image_tensor, tf.float32),[1,2,2,1],[1,2,2,1],"SAME")

    maxfile.write(normalize_and_encode(maxed_tensor).eval())
    avgfile.write(normalize_and_encode(averaged_tensor).eval())
    coord.request_stop()
    maxfile.close()
    avgfile.close()
coord.join(threads)

三、代码 CNN 实战

请听下回下章节分解 ~ ~ ~ ~

感谢一下博主分享的文章,让我学到了很多:

http://www.cnblogs.com/pinard/p/6483207.html

猜你喜欢

转载自blog.csdn.net/smilejiasmile/article/details/80752889
今日推荐