卷积神经网络CNN
CNN是一种至少包含一层卷积层的神经网络模型,卷积层的目的是将卷积核应用到图像张量的所有点上,并通过将卷积核在输入张量上滑动生成经过滤波处理的张量.简单的CNN架构通常会包含卷积层,池化层,非线性变换层,全连接层.通过这些层网络会被填充大量的信息,因此模型便可以进行复杂的模式匹配.
本文主要是通过TF1.5这个工具,展示CNN中各个层对图像信息的加工过程.
主要内容:
- 张量在TF中的表示TensorFlow基础知识
- 卷积层
- 激活函数
- 池化层
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的重要组成,通常也是网络的第一层,下面主要介绍卷积层对图像的一些操作
# 在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
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()
原图尺寸
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()
图像锐化效果
通过卷积核增加卷积核中心位置的像素灰度,降低周围像素的灰度.
# 锐化卷积核
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中模型通过大量的训练能够自动学习到更复杂的卷积核,不但能够匹配边缘,而且还能匹配更加复杂的模式.卷积核的初始值通常随即设定,它会随着训练的迭代自动更新.当完成一轮训练后,模型会继续'阅读'一副图像,并将其与卷积核进行卷积等等一系列计算,根据得到的结果与图像真实的标签对比.推卷积核进行调整.直到模型收敛(或达到理想的准确率)对于图像识别和分类任务,通常是使用不同的层支持某个卷基层,不是单一使用一个卷积层.这样有助于减少过拟合,加速训练过程减少内存占用率.
激活函数
激活函数通常搭配卷积层和全连接层使用,对它们输出的特征(张量)进行'激活',其实就是对运算的结果进行平滑处理.目的是为网络引入非线性.非线性意味着输入输出直接的关系是一条曲线.曲线能够反映输入中更复杂的变化,比如:在 大部分点值很小,但在某个单点会周期性地出现极值的输入.引入非线性可以使网络对数据中发现更复杂的模式进行训练.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()
# sigmoid
plt.plot(sess.run(features), sess.run(tf.sigmoid(features)))
plt.grid(True)
plt.title("Sigmoid")
plt.show()
# tanh
plt.plot(sess.run(features), sess.run(tf.tanh(features)))
plt.title("Tanh")
plt.grid(True)
plt.show()
# drop
plt.scatter(sess.run(features), sess.run(tf.nn.dropout(features, keep_prob=0.5)))
plt.title("Dropout")
plt.grid(True)
plt.show()
池化层
池化层通过对输入张量的尺寸进行压缩,来对输入进行降采样,保留重要的特征,也能有效减少过拟合,在卷积的过程中'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的接受域,四个像素中选出值最大的保留输出,张量尺寸也缩小了一半:
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()
图像尺寸(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()
池化后图像大小
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()
增加滑动步长和接受域
# 平均池化层输入张量为浮点型数据
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中有不止一个卷积层和池化层,对图像的主要特征进行多轮的处理,最后通常连接一个全连接层.跟普通的BP神经网络一样.