CNN是如何一层一层'理解'图像信息的

时间:2021-11-24 19:44:58

卷积神经网络CNN

  CNN是一种至少包含一层卷积层的神经网络模型,卷积层的目的是将卷积核应用到图像张量的所有点上,并通过将卷积核在输入张量上滑动生成经过滤波处理的张量.简单的CNN架构通常会包含卷积层,池化层,非线性变换层,全连接层.通过这些层网络会被填充大量的信息,因此模型便可以进行复杂的模式匹配.
本文主要是通过TF1.5这个工具,展示CNN中各个层对图像信息的加工过程.
主要内容:

import tensorflow as tf
import matplotlib.pyplot as plt

1.tensor(张量)

# 图像经过解码后是一张量的形式表示
# 定义一个包含两个张量[图像]的常量
image_batch = tf.constant([
    [
        [[0, 255, 0], [0, 255, 0], [0, 255, 0]],
        [[0, 255, 0], [0, 255, 0], [0, 255, 0]]
    ],
    [
        [[0, 0, 255], [0, 0, 255], [0, 0, 255]],
        [[0, 0, 255], [0, 0, 255], [0, 0, 255]]
    ]
])

查看张量尺寸信息

image_batch.get_shape()
TensorShape([Dimension(2), Dimension(2), Dimension(3), Dimension(3)])
TensorShape信息:两个张量,高为2个像素,宽为3个像素,颜色空间为RGB
# 访问第一副图像的第一个像素点
sess = tf.Session()
sess.run(image_batch)[0][0][0]
array([  0, 255,   0], dtype=int32)
sess = tf.Session()

TF加载图像并解码

# 加载一张彩色图像
image_filename = 'bcd.jpg'
# 此处可能会出现编码错误,因为系统默认uhf-8编码,文件可能不是该编码方式-'rb'
image_file = tf.gfile.FastGFile(image_filename,'rb').read()
# 解码
image = tf.image.decode_jpeg(image_file)
sess.run(image)

图像解码后是一个三阶的张量,每个元素为图像像素点的值

array([[[243, 245, 224],
        [242, 244, 223],
        [242, 243, 225],
        ...,
        [225, 227, 222],
        [225, 227, 222],
        [225, 227, 222]],

       [[242, 244, 223],
        [242, 244, 223],
        [242, 243, 225],
        ...,
        [225, 227, 222],
        [225, 227, 222],
        [225, 227, 222]],

       [[241, 243, 222],
        [241, 243, 222],
        [240, 241, 223],
        ...,
        [225, 227, 222],
        [225, 227, 222],
        [225, 227, 222]],
    ...,
       ...,
       ...,
        [217, 183, 146],
        [217, 183, 146],
        [216, 182, 145]]], dtype=uint8)
plt.imshow(sess.run(image))
plt.show()

     重绘图像

         CNN是如何一层一层'理解'图像信息的CNN是如何一层一层'理解'图像信息的


卷积层

卷积是CNN的重要组成,通常也是网络的第一层,下面主要介绍卷积层对图像的一些操作

# 在TensorFlow中卷积运算通过tf.nn.conv2d()完成.同时TF还提供其特定的卷积运算.
# 定义一个张量,简单实现卷积运算
tensor_1 = tf.constant([
    [
        [[0.0],[1.0]],
        [[2.0],[3.0]]
    ],
])

# 卷积核
kernel_1 = tf.constant([
    [
        [[1.0, 2.0]]
    ]
])
# 张量的shape
tensor_1.get_shape()
# TensorShape[张量数量,张量高度,张量宽度,通道数]
TensorShape([Dimension(1), Dimension(2), Dimension(2), Dimension(1)])
# kernel_1
kernel_1.get_shape()
TensorShape([Dimension(1), Dimension(1), Dimension(1), Dimension(2)])
# 卷积后,得到一个新张量(特征图)
conv2d_1 = tf.nn.conv2d(tensor_1, kernel_1 ,strides=[1, 1, 1, 1],
                                                  padding='SAME')
sess.run(conv2d_1)
array([
    [
        [[0.0, 0.0], [1.0, 2.0]],
        [[2.0, 4.0], [3.0, 6.0]]
    ]
])

卷积后得到一个和原张量大小相同,通道数为2,与卷积核相同

conv2d_1.get_shape()
TensorShape([Dimension(1), Dimension(2), Dimension(2), Dimension(2)])

访问tensor_1卷积前后相同的位置像素值,了解下卷积运算时,像素值是如何变化的

sess.run(tensor_1)[0][1][1],sess.run(conv2d_1)[0][1][1]
(array([3.], dtype=float32), array([3., 6.], dtype=float32))

 
  tensor_1中的每个像素值,分别与卷积核不同通道(维度)的值相乘得到新的像素值,然后映射到特征图的相应的通道(维度)中:3.0*1.0, 3.0*2.0,这个例子仍不能直观感受到卷积是如何如何,且往下看.
 

strides(跨度)

  卷积的价值在对图像数据的降维能力,有利于CNN模型减少在图像学习上消耗的时间,降维是通过修改卷积核的strides参数来实现,简单来说就是,通过改变卷积核在原始图像上移动的步长,来减少网络扫描图像的时间.在tf.nn.conv2d()中可以通过修改strides参数,来改变卷积核'滑动'的方式,使得卷积核可以跳过某些像素.
比如,一个图像的高度为5(像素),宽度为5,通道为1(1x5x5x1),卷积核的大小是(3x3x1)

# tensor_2 [1x5x5x1]
tensor_2 = tf.constant([
    [
        [[2.0], [1.0], [0.0], [2.0], [3.0]],
        [[9.0], [5.0], [4.0], [2.0], [0.0]],
        [[2.0], [3.0], [4.0], [5.0], [6.0]],
        [[1.0], [2.0], [3.0], [1.0], [0.0]],
        [[0.0], [4.0], [4.0], [2.0], [8.0]],
    ],
])

# 卷积核[3x3x1x1]
kernel_2 = tf.constant([
    [
        [[-1.0]], [[0.0]], [[1.0]]
    ],
    [
        [[-1.0]], [[0.0]], [[1.0]]
    ],
    [
        [[-1.0]], [[0.0]], [[1.0]]
    ]
])

注意!在卷积核中,张量的四个维度表示的数值,代表:卷积核的高,宽,通道和数量,其实可以这样理解:是用3个高为3,宽为1,通道为1的张量.表示一个,高为3,宽为3,通道为1,数量为1的卷积核

tensor_2.get_shape()
TensorShape([Dimension(1), Dimension(5), Dimension(5), Dimension(1)])
kernel_2.get_shape()
TensorShape([Dimension(3), Dimension(3), Dimension(1), Dimension(1)])
# 卷积,strides,padding(下面会介绍)
# padding = 'VALID',输入会比输入尺寸小
conv2d_2 = tf.nn.conv2d(tensor_2, kernel_2, strides =[1,1,1,1],
                        padding='VALID')
sess.run(conv2d_2)

卷积后得到的特征张量

array([
    [
     [[-5.],[ 0.],[ 1.]],
     [[-1.],[-2.],[-5.]],
     [[ 8.],[-1.],[ 3.]]
    ],
], dtype=float32)

kernel_2每次移动时,都会与tensor_2位置重叠的部分对应的值相乘,然后将乘积相加得到卷积的结果.卷积就是通过这中逐点相乘的方式将两个输入整合在一起.下图更直观展示卷积.
-1*1 + 0*0 + 1*2+
-1*5 + 0*4 + 1*2+
-1*3 + 0*4 + 1*5 = 0

    CNN是如何一层一层'理解'图像信息的

strides,padding

strides:

参数的格式和tensor_2张量相对应
image_batch_size_stirde,image_height_stride,image_width_stride,image_channels_stirde
一般通过修改中间两个参数,改变卷积核在tensor高和宽两个方向上的移动步长,在卷积过程中跳过一些像素点

padding:’SAME’OR’VALID’

在卷积过程中会遇到这种情况:卷积核未到达图像的边界时,下一步的移动会越过图像的边界.
针对这种情况,tensorflow提供的措施是对超出图像边界的部分进行填充,即边界填充.
SAME:当卷积核超出图像边界时,会进行0填充,卷积的输出与输入相同
VALID:考虑卷积核的尺寸,尽量不越过图像边界.卷积输出小于输入

张量的数据格式

  tf.nn.conv2d()输入的张量的格式不是固定的可以自定义,通过修改data_format,默认参数是’NHWC’,N:batch_size; H:张量高度; W:宽;C:通道数
 
继续讨论卷积:

  在计算机视觉中,卷积常用于识别图像中的重要特征,下面使用一个专为边缘检测的卷积核,来突出图像中的边缘.不同的卷积核会突出图像中的不同模式,得到不同的结果.
 

# 边缘检测卷积核
kernel_3 = tf.constant([
    [
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]],
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]],
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]]
    ],
    [
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]],
        [[ 8., 0., 0.], [0.,  8., 0.], [0., 0.,  8.]],
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]]
    ],
    [
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]],
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]],
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]]
    ]
])

原图

# 卷积前图像
plt.imshow(sess.run(image))
plt.show()

      CNN是如何一层一层'理解'图像信息的CNN是如何一层一层'理解'图像信息的

原图尺寸

sess.run(image).shape
(393, 700, 3)

边缘检测

# uint8转化为浮点型
image = tf.to_float(image)
# 转化为一个四维张量
conv2d_3 = tf.nn.conv2d(tf.reshape(image,[1,393,700,3]), kernel_3,
                        [1,1,1,1], padding='SAME')

卷积后图像的大小

conv2d_3.get_shape()
TensorShape([Dimension(1), Dimension(393), Dimension(700), Dimension(3)])

卷积后的图像

# tf.minimum()和tf.nn.relu()是将卷积后的像素值保持在RGB颜色值的合法范围内[0,255]
conved_image = tf.minimum(tf.nn.relu(conv2d_3), 255)
plt.imshow(sess.run(conved_image).reshape(393,700,3))
plt.show()

       CNN是如何一层一层'理解'图像信息的CNN是如何一层一层'理解'图像信息的

 
图像锐化效果
 通过卷积核增加卷积核中心位置的像素灰度,降低周围像素的灰度.

# 锐化卷积核
kernel_4 = tf.constant([
    [
        [[ 0., 0., 0.], [0.,  0., 0.], [0., 0.,  0.]],
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]],
        [[ 0., 0., 0.], [0.,  0., 0.], [0., 0.,  0.]]
    ],
    [
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]],
        [[ 5., 0., 0.], [0.,  5., 0.], [0., 0.,  5.]],
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]]
    ],
    [
        [[ 0., 0., 0.], [0.,  0., 0.], [0., 0.,  0.]],
        [[-1., 0., 0.], [0., -1., 0.], [0., 0., -1.]],
        [[ 0., 0., 0.], [0.,  0., 0.], [0., 0.,  0.]]
    ]
])

# 卷积
conv2d_4 = tf.nn.conv2d(tf.reshape(image,[1,393,700,3]), kernel_4,
              [1,1,1,1], padding='SAME')
sharpen_image = tf.minimum(tf.nn.relu(conv2d_4), 255)
# 通过索引访问第一个张量,就不用对图像张量进行resize操作了
plt.imshow(sess.run(sharpen_image)[0])
plt.show()

        CNN是如何一层一层'理解'图像信息的CNN是如何一层一层'理解'图像信息的

 
  特定的卷积核能够匹配特定的像素模式,在CNN中模型通过大量的训练能够自动学习到更复杂的卷积核,不但能够匹配边缘,而且还能匹配更加复杂的模式.卷积核的初始值通常随即设定,它会随着训练的迭代自动更新.当完成一轮训练后,模型会继续'阅读'一副图像,并将其与卷积核进行卷积等等一系列计算,根据得到的结果与图像真实的标签对比.推卷积核进行调整.直到模型收敛(或达到理想的准确率)对于图像识别和分类任务,通常是使用不同的层支持某个卷基层,不是单一使用一个卷积层.这样有助于减少过拟合,加速训练过程减少内存占用率.
 


激活函数

  激活函数通常搭配卷积层和全连接层使用,对它们输出的特征(张量)进行'激活',其实就是对运算的结果进行平滑处理.目的是为网络引入非线性.非线性意味着输入输出直接的关系是一条曲线.曲线能够反映输入中更复杂的变化,比如:在 大部分点值很小,但在某个单点会周期性地出现极值的输入.引入非线性可以使网络对数据中发现更复杂的模式进行训练.TensorFlow提供了多种激活函数.他们的共同点是:单调,可微分.只要满足这两点就可以作为激活函数

1.tf.nn.relu()

  ReLU是分段线性的,输入为正时,输出=输入,输入为负时,输出=0,取值范围[0,+oo]优点:不受梯度消失的影响;缺点:学习效率较大时,易受饱和神经元的影响, 梯度消失:每一层神经元对上一层的输出的偏导乘上权重结果都小于1的话即使这个结果是0.99,在经过足够多层传播之后,误差对输入层的偏导会趋于0

2.tf.sigmoid()

  sigmoid函数的返回值在[0.0, 1.0]之间,输入的数值较大时,返回的结果就越接近1.0,较小的数值返回结果就越接近0.0, 对于真实输出位于[0,1]之间的数据来说使用sigmoid()将输出保持在很小范围很有用,但是对于输入接近饱和或者变化剧烈的数据上使用sigmoid()压缩输出范围往往会有不利的影响,比如:梯度消失

3.tf.tanh()

  与sigmoid()接近,区别在于tanh将输出的值映射在[-1,1]之间.

4.tf.nn.dropout()

  dropout函数其实不太算是激活函数,它并不对输入的值进行非线性变换,只是根据一个概率参数将部分的输出值置为0.0,这样做的目的是为训练中的模型引入少量的随机噪声,防止模型在学习过程中出现过拟合(仅在训练阶段)

函数图像

features = tf.to_float(tf.range(-20,20))
# Relu
plt.plot(sess.run(features), sess.run(tf.nn.relu(features)))
plt.grid(True)
plt.title('Relu')
plt.show()

        CNN是如何一层一层'理解'图像信息的CNN是如何一层一层'理解'图像信息的

# sigmoid
plt.plot(sess.run(features), sess.run(tf.sigmoid(features)))
plt.grid(True)
plt.title("Sigmoid")
plt.show()

        CNN是如何一层一层'理解'图像信息的CNN是如何一层一层'理解'图像信息的

# tanh
plt.plot(sess.run(features), sess.run(tf.tanh(features)))
plt.title("Tanh")
plt.grid(True)
plt.show()

       CNN是如何一层一层'理解'图像信息的CNN是如何一层一层'理解'图像信息的

# drop
plt.scatter(sess.run(features), sess.run(tf.nn.dropout(features, keep_prob=0.5)))
plt.title("Dropout")
plt.grid(True)
plt.show()

        CNN是如何一层一层'理解'图像信息的CNN是如何一层一层'理解'图像信息的


池化层

  池化层通过对输入张量的尺寸进行压缩,来对输入进行降采样,保留重要的特征,也能有效减少过拟合,在卷积的过程中'VALID'模式下也能减小输入张量的尺寸,但是池化效果更明显.池化层的输入通常是卷积层的输出.
  池化分两种: 1. 最大池化, 2. 平均池化
 
max_pool()
   tf.nn.max_pool()除了不进行卷积运算,寻找最大像素点的过程和卷积是很相似的,不过kernel不是一个张量,而是一个接受域的范围[batch_size,imput_height,input_width,channels],即在这个范围内寻找最大的值输出.

# 输入张量
input_tensor = tf.constant([
    [
        [[1.0], [1.0], [2.0], [4.0]],
        [[5.0], [6.0], [7.0], [8.0]],
        [[3.0], [2.0], [1.0], [0.0]],
        [[1.0], [2.0], [3.0], [4.0]]
    ]
])
kernel_pool = [1, 2, 2, 1]
strides = [1, 2, 2, 1] # 跟卷积作用相同
max_pool = tf.nn.max_pool(input_tensor, kernel_pool, strides, 'VALID')
sess.run(max_pool)

最大池化结果

array([
    [
        [[6.],[8.]],
        [[3.],[4.]]
    ]
], dtype=float32)

从2x2的接受域,四个像素中选出值最大的保留输出,张量尺寸也缩小了一半:
          CNN是如何一层一层'理解'图像信息的
          
avg_pool()

# 平均池化
avg_pool = tf.nn.avg_pool(input_tensor, kernel_pool, strides, 'VALID')
sess.run(avg_pool)

平均池化输出

array([
    [
        [[3.25],[5.25]],
        [[2.],[2.]]
    ]
], dtype=float32)

 从2x2的接受域中,计算四个像素平均值保留输出:3.25 = (1.0+1.0+5.0+6.0)/4
 


下面在彩色图像上进行池化操作,观察对图像输出的影响,一般在CNN是不会直接对图像进行池化操作的.

# 下面分别对这副图像进行最大和平均池化
image_file2 = tf.gfile.FastGFile('123.jpg','rb').read()
image_2 = tf.image.decode_jpeg(image_file2)
plt.imshow(sess.run(image_2))
plt.show()

         CNN是如何一层一层'理解'图像信息的

图像尺寸(873, 1070, 3)
最大池化

max_pool_image = tf.nn.max_pool(tf.reshape(image_2,[1,873,1070,3]),
                       [1,6,6,1],[1,5,5,1],'VALID')
plt.imshow(sess.run(max_pool_image)[0])
plt.show()

         CNN是如何一层一层'理解'图像信息的CNN是如何一层一层'理解'图像信息的

池化后图像大小

max_pool_image
<tf.Tensor 'MaxPool_8:0' shape=(1, 174, 214, 3) dtype=uint8>

平均池化

# 平均池化层输入张量为浮点型数据
avg_pool_image = tf.nn.avg_pool(tf.to_float(tf.reshape(image_2,[1,873,1070,3])),
                                [1,1,2,1],[1,1,1,1],'VALID')
plt.imshow(sess.run(avg_pool_image)[0])
plt.show()

        CNN是如何一层一层'理解'图像信息的CNN是如何一层一层'理解'图像信息的

增加滑动步长和接受域

# 平均池化层输入张量为浮点型数据
avg_pool_image_1 = tf.nn.avg_pool(tf.to_float(tf.reshape(image_2,[1,873,1070,3])),
                                [1,2,2,1],[1,2,2,1],'VALID')
plt.imshow(sess.run(avg_pool_image_1)[0])
plt.show()

        CNN是如何一层一层'理解'图像信息的CNN是如何一层一层'理解'图像信息的

不管是最大池化还是平均池化,图像的轮廓都会随着接受域和滑动'步长'的增加越来越模糊,平均池化图像轮廓消失的最快.
 
  在CNN中有不止一个卷积层和池化层,对图像的主要特征进行多轮的处理,最后通常连接一个全连接层.跟普通的BP神经网络一样.