新认知机 (Fukushima, 1980年) 受哺乳动物视 觉系统的结构启发,引入了一个处理图片的强大模型架构,它后来成为了现代卷积网络的基础 (LeCun et al., 1998年)。
卷积神经网络是深度学习中最经典的模型之一。它可以解决全连接网络中参数过多的问题,它可以使用很少的权重参数达到全连接网络实现不了的效果。下面我们通过简单的计算,看一看全连接网络的带来的问题。
假设在对MNIST数据集(28x28)进行训练时,使用的网络有两个隐藏层,每层256个节点,输出层10个节点。
则训练中所需参数: (权重w参数 28*28*256+256*256+256*10) + (偏置值b 256+256+10), 这就不少了吧。那么如果图片是 1000x1000呢?仅仅一层就需要 1000*1000*256 大约2亿左右个参数,再如果图片是RGB的呢??再乘以3,就是6亿个参数。这仅仅是一层,如果想要效果更好,再添加几个隐藏层,那么参数就惊人了,这么多参数要训练需要耗费更多的时间和资源。
而且对于高维度的数据,用全连接网络的话,只能增加节点数和更多的层数,这就会带来参数过多的问题。而且层数太多的话还会使反向传播的修正值越来越小,导致网络无法训练。
卷积神经网络使用参数共享的方式,换一个角度解决问题,不仅准确率大大提升,参数也更少了。
卷积神经网络结构
一个卷积神经网络包括多个部件:输入层、多个(卷积层、池化层)、全局平均池化层、输出层
先对每一个部件有个简单的认识:
输入层:不多说
卷积层:通过卷积核(有人也叫 滤波器、sobel算子)进项卷积操作,用于抽取局部特征
池化层:主要是将卷积操结果降维,用于抽象和容错
全局平均池化层:对生成feature map 取全局平均值。以前的大部分网络在这一层都是用 1~3 层的全连接层,全连接层计算量大,效果和全局平均池化层一样,而且效率没有全局平均池化层高。
输出层:需要分成几类就会有几个输出节点
下图大致就是卷积网络的过程(看主要过程的就好):
该图以及后面几个图来自魏秀参的<解析卷积神经网络 ——深度学习实践手册>,有兴趣的童鞋可以下载看一下。
链接:https://pan.baidu.com/s/1WJOHxuWbYqrC_EeMNYW6XQ 密码:owlm
卷积过程
用下面的 卷积核3x3 和 输入数据 5x5来演示过程:
在这之前先说一个比较重要的概念:步长(stride) 表示卷积核在输入数据上每次向右、向下移动的格数
下面是 步长 stride=1 时候的操作:
上面的过程还有一个名字叫做 VALID卷积(窄卷积) 顾名思义 卷积后的数据尺寸比原数据变小了,它的步长可变。
假设:
输入的尺寸中高和宽:in_height, in_width
卷积核的高和宽:filter_height, filter_width
输出的尺寸中高和宽:output_height, output_width
步长的高宽方向:strides_height, strides_width
则VALID卷积时,输出的尺寸:
output_height=(in_height-filter_height+1)/strides_height (结果向上取整)
output_width=(in_width-filter_width+1)/strides_height (结果向上取整)
有窄卷积,会不会有宽卷积呀??那肯定是。。。没有的。
不过倒是有 SAME卷积(同卷积)、full卷积(全卷积、反卷积)。
SAME卷积(同卷积) 再顾名思义一下,就是卷积后的数据尺寸与原始数据尺寸一样大,SAME卷积的步长固定为1,一般操作时还会使用padding技术(就是在原来的数据周围补一大堆0,以确保生成的尺寸不变)。
SAME卷积时,输出的尺寸:
输出的宽和高与卷积核无关
output_height=in_height/strides_height (结果向上取整)
output_width=in_width/strides_height (结果向上取整)
padding的补0规则:
pad_height = max( (out_height-1) x strides_height + filter_height -in_height, 0) # 高度方向要填充的0的行数
pad_width = max( (out_width-1) x strides_height + filter_width -in_width, 0) # 宽度方向要填充的0的列数
pad_top = pad_height / 2 # 上
pad_bottom = pad_height - pad_top # 下
pad_left = pad_width / 2 # 左
pad_bottom = pad_width - pad_left # 右
full卷积这里不讲。其多用在反卷积神经网络中。
卷积实例
还是先看一下 tensorflow 中的卷积函数:
tf.nn.conv2d(input, filter, strides, padding, use_cudnn_on_gpu=True, data_format="NHWC", dilations=[1, 1, 1, 1], name=None)
input:
指需要做卷积的输入图像,它要求是一个tensor,具有[batch, in_height, in_width, in_channels]这样的shape,含义是['训练时一个batch的图像数量',
'图片高度', '图片宽度', '图像通道数'],这是一个4维的Tensor, 要求类型为float32、float64之一
filter:相当于CNN中的卷积核,它要求是一个Tensor,shape为[filter_height, filter_width, in_channels, out_channels],含义是['卷积核的高度',
'卷积核的宽度','图像通道数','滤波器个数'],要求类型与参数input相同, 其in_channels就是参数input的第四维
strides: 卷积时在图像每一维的步长,这是一个一维向量,长度为4
padding: 定义元素边框与元素内容之间的空间。string类型的量,只能是SAME和VALID其中之一,这个值决定了不同的卷积方式,padding的值为VALID时,表示
边缘不填充,为SAME时,表示填充到滤波器可以达到图像边缘
use_cudnn_on_gpu:bool类型,是否使用cudnn加速,默认为True
返回值:是一个Tensor,这个输出就是常说的feature map
import tensorflow as tf
'''
卷积函数的用法
手动生成一个5 x 5的矩阵模拟图片,定义2 x 2 的卷积核
'''
# 定义三个输入变量模拟输入数据
# [batch, in_height, in_width, in_channels]
# ['训练时一个batch的图像数量','图片高度', '图片宽度', '图像通道数']
input1 = tf.Variable(tf.constant(1., shape=[1, 5, 5, 1]))
# [
# [
# [[1] [1] [1] [1] [1]]
# [[1] [1] [1] [1] [1]]
# [[1] [1] [1] [1] [1]]
# [[1] [1] [1] [1] [1]]
# [[1] [1] [1] [1] [1]]
# ]
# ]
input2 = tf.Variable(tf.constant(1., shape=[1, 5, 5, 2]))
# [
# [
# [[1. 1.] [1. 1.] [1. 1.] [1. 1.] [1. 1.]]
# [[1. 1.] [1. 1.] [1. 1.] [1. 1.] [1. 1.]]
# [[1. 1.] [1. 1.] [1. 1.] [1. 1.] [1. 1.]]
# [[1. 1.] [1. 1.] [1. 1.] [1. 1.] [1. 1.]]
# [[1. 1.] [1. 1.] [1. 1.] [1. 1.] [1. 1.]]
# ]
# ]
input3 = tf.Variable(tf.constant(1., shape=[1, 4, 4, 1]))
# [
# [
# [[1.] [1.] [1.] [1.]]
# [[1.] [1.] [1.] [1.]]
# [[1.] [1.] [1.] [1.]]
# [[1.] [1.] [1.] [1.]]
# ]
# ]
# 定义卷积核
'''
定义5个卷积核,每个卷积核都是 2 x 2的矩阵,只是输入、输出的通道数有差别
分别为 1ch输入、1ch输出、1ch输入、2ch输出, 1ch输入、3ch输出,
2ch输入、2ch输出、2ch输入、1ch输出,并分别在里面填入指定的值
'''
# [filter_height, filter_width, in_channels, out_channels]
# ['卷积核的高度','卷积核的宽度','图像通道数','滤波器个数']
filter1 = tf.Variable(tf.constant([-1., 0, 0, -1], shape=[2, 2, 1, 1]))
# [
# [
# [[-1.]] [[ 0.]]
# ]
# [
# [[ 0.]] [[-1.]]
# ]
# ]
filter2 = tf.Variable(tf.constant([-1., 0, 0, -1,
-1., 0, 0, -1], shape=[2, 2, 1, 2]))
# [
# [
# [[-1. 0.]]
# [[0. -1.]]
# ]
# [
# [[-1. 0.]]
# [[0. -1.]]
# ]
# ]
filter3 = tf.Variable(tf.constant([-1., 0, 0, -1,
-1., 0, 0, -1,
-1., 0, 0, -1], shape=[2, 2, 1, 3]))
# [
# [
# [[-1. 0. 0.]]
# [[-1. -1. 0.]]
# ]
# [
# [[0. -1. -1.]]
# [[0. 0. -1.]]
# ]
# ]
filter4 = tf.Variable(tf.constant([-1., 0, 0, -1,
-1., 0, 0, -1,
-1., 0, 0, -1,
-1., 0, 0, -1], shape=[2, 2, 2, 2])) # 2*2*2*2 = 16
# [
# [
# [[-1. 0.] [ 0. -1.]]
# [[-1. 0.] [ 0. -1.]]
# ]
# [
# [[-1. 0.] [ 0. -1.]]
# [[-1. 0.] [ 0. -1.]]
# ]
# ]
filter5 = tf.Variable(tf.constant([-1., 0, 0, -1,
-1., 0, 0, -1], shape=[2, 2, 2, 1]))
# [
# [
# [[-1.] [ 0.]]
# [[ 0.] [-1.]]
# ]
# [
# [[-1.] [ 0.]]
# [[ 0.] [-1.]]
# ]
# ]
# 定义卷积操作
# 将上面的两步组合起来,建立8个卷积操作
# padding 值为 'VALID' 的,表示边缘不填充
# padding 值为 'SAME' 的,表示便于边缘填充到卷积核可以达到图像的边缘
# 1 个通道输入,1个 feature map 生成
op1 = tf.nn.conv2d(input1, filter1, strides=[1, 2, 2, 1], padding='SAME')
# 1 个通道输入,2个 feature map 生成
op2 = tf.nn.conv2d(input1, filter2, strides=[1, 2, 2, 1], padding='SAME')
# 1 个通道输入,3个 feature map 生成
op3 = tf.nn.conv2d(input1, filter3, strides=[1, 2, 2, 1], padding='SAME')
# 2 个通道输入,2个 feature map 生成
op4 = tf.nn.conv2d(input2, filter4, strides=[1, 2, 2, 1], padding='SAME')
# 2 个通道输入,1个 feature map 生成
op5 = tf.nn.conv2d(input2, filter5, strides=[1, 2, 2, 1], padding='SAME')
# 5 * 5对于padding不同而不同
vop1 = tf.nn.conv2d(input1, filter1, strides=[1, 2, 2, 1], padding="VALID")
op6 = tf.nn.conv2d(input3, filter1, strides=[1, 2, 2, 1], padding="SAME")
# 4 * 4 与 padding 无关
vop6 = tf.nn.conv2d(input3, filter1, strides=[1, 2, 2, 1], padding="VALID")
# 进行卷积操作
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
print('op1:\n', sess.run([op1, filter1])) # 1-1 后补0
print('---------------------------------------------')
print('op2:\n', sess.run([op2, filter2])) # 1-2 多卷积核, 按列取
print('op3:\n', sess.run([op3, filter3])) # 1-3 一个输入, 3个输出
print('---------------------------------------------')
print('op4:\n', sess.run([op4, filter4])) # 2-2 通道叠加
print('op5:\n', sess.run([op5, filter5])) # 2-1 两个输入,一个输出
print('---------------------------------------------')
print('op1:\n', sess.run([op1, filter1])) # 1-1 一个输出,一个输入
print('vop1:\n', sess.run([vop1, filter1])) #
print('op6:\n', sess.run([op6, filter1])) #
print('vop6:\n', sess.run([vop6, filter1])) #
结果输出就自己实验吧,这里就不贴了。重点讲一下里面的几个。
1)先看一下padding的例子,op1用的 padding= 'SAME',则其需要补0操作,通过上面的公式计算如下:
output_height=in_height/strides_height (结果向上取整) = 5 / 2 = 3
output_width=in_width/strides_height (结果向上取整) = 5 / 2 = 3
pad_height = max( (out_height-1) x strides_height + filter_height -in_height, 0) = max((3-1)*2+2-5, 0) = 1
pad_width = max( (out_width-1) x strides_height + filter_width -in_width,0) = max((3-1)*2+2-5, 0) = 1
pad_top = pad_height / 2 = 1 / 2 = 0
pad_bottom = pad_height - pad_top = 1 - 0 = 1
pad_left = pad_width / 2 = 1 / 2 = 0
pad_bottom = pad_width - pad_left 1 - 0 = 1
结果就是下面的样子:
2)结果中 op2 的输出如下:
# array([[
# [[-2., -2.],[-2., -2.],[-2., 0.]],
# [[-2., -2.],[-2., -2.],[-2., 0.]],
# [[-1., -1.],[-1., -1.],[-1., 0.]]
# ]], dtype=float32)
op2 有2个卷积核,所以结果是个多通道的输出,按列排列(每个feature map为一列),过程如下:
3)看一看 op5,其输入图片是2通道,1个卷积核,结果就是两个通道的feature map叠加
好了,卷积操作就说到这。下面说一下池化操作(pooling).
池化层
池化层,也同样有人称为 汇合层。这一层不需要学习参数,但需要指出其池化类型,核大小,步长
池化分两种类型:
1)最大池化: 取核内的最大值
2)平均池化: 将核内的值相加后,取平均值
就看一下最大池化吧,类型:最大池化,核大小:2x2,步长 stride=1
看一看 tensorflow中的池化函数:
def max_pool(value, ksize, strides, padding, name=None):
def avg_pool(value, ksize, strides, padding, name=None):
value: 需要池化的输入,一般池化层在卷积层的后面,所以输入通常是feature map,依然是[batch, height, width, channels]这样的shape
ksize: 池化窗口的大小,去一个4维向量,一般是[1, height, width, 1], 因为我们不想在 batch 和 channels 上做池化,所以这两个维度设为了1
strides:和卷积参数含义类似,窗口在每一个维度上滑动测步长,一般也是[1, stride, stride, 1]
padding:和卷积参数一样,也是取VALID或者SAME
返回一个Tensor,类型不变,shape仍然是[batch, height, width, channels]
例子:
import tensorflow as tf
'''
定义输入变量
定义一个输入变量用来模拟输入图片、4x4大小的2通道矩阵,并将其赋予指定的值。
2个通道分别为:4个 0 0 0 0到3 3 3 3 组成矩阵,4个 4 4 4 4到7 7 7 7组成的矩阵
'''
img = tf.constant([
[[0., 4.], [0., 4.], [0., 4.], [0., 4.]],
[[1., 5.], [1., 5.], [1., 5.], [1., 5.]],
[[2., 6.], [2., 6.], [2., 6.], [2., 6.]],
[[3., 7.], [3., 7.], [3., 7.], [3., 7.]]
])
img = tf.reshape(img, [1, 4, 4, 2])
'''
定义池化操作
定义了 4 个池化操作和一个取均值操作
'''
pooling0 = tf.nn.max_pool(img, [1, 2, 2, 1], [1, 2, 2, 1], padding='VALID')
pooling1 = tf.nn.max_pool(img, [1, 2, 2, 1], [1, 1, 1, 1], padding='VALID')
pooling2 = tf.nn.avg_pool(img, [1, 4, 4, 1], [1, 1, 1, 1], padding='SAME')
pooling3 = tf.nn.avg_pool(img, [1, 4, 4, 1], [1, 4, 4, 1], padding='SAME')
nt_hpool2_flat = tf.reshape(tf.transpose(img), [-1, 16])
print('tf.transpose(img): ', tf.transpose(img).shape)
pooling4 = tf.reduce_mean(nt_hpool2_flat, 1) # 按行求均值
# 运行池化操作
with tf.Session() as sess:
print('image: ', sess.run(img))
print('result0: \n', sess.run(pooling0))
print('result1: \n', sess.run(pooling1))
print('result2: \n', sess.run(pooling2))
print('result3: \n', sess.run(pooling3))
print('result4: \n', sess.run(pooling4))
print('flat: \n', sess.run(nt_hpool2_flat))
卷积操作的作用
卷积操作的一个作用的就是用于边缘检测。边缘检测又可以分为横向检测和纵向检测。
下面就直接拿魏老师书中的一个例子,简单讲一下。
将上面的图分别作用于整体边缘滤波器,横向边缘滤波器,纵向边缘滤波器,三种滤波器如下:
下面是作用后的结果对比图:
若原图像素(x,y)出可能存在物体边缘,则其周围(x-1, y)、(x+1,y)、(x, y-1)、(x, y+1)处像素值应与(x,y)处有显著差异。此时如果作用以整体边缘滤波器Ke,可消除四周像素值差异小的图像区域,而保留显著差异区域,以此检测出物体边缘信息。同理,类似Kh和Kv的横向、纵向边缘滤波器可分别保留横向、纵向的边缘信息。
事实上,卷积网络中的卷积核参数是通过训练出来的,除了可以学习到类似横向、纵向边缘滤波器,还可以学到任意角度的边缘滤波器。当然不仅如此,检测颜色、形状、纹理等等众多基本模式的滤波器都可以包含在一个足够复杂的深层卷积神经网络中。
下面是一个例子,可以猜猜用的哪种滤波器:
实验中的图片是漂亮的丫丫姐姐的图片,丫丫姐姐就是漂亮。。。
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
import matplotlib.image as im_img
my_img = im_img.imread('./static/yaya.jpeg') # 目录结构自己决定,图片自己放
# plt.imshow(my_img)
# plt.axis('off') # 不显示坐标轴
# plt.show()
# print(my_img.shape) # 输出图片的大小及通道数
# 定义占位符、卷积核、卷积op
full = np.reshape(my_img, [1, 1633, 1200, 3])
input_full = tf.Variable(tf.constant(1., shape=[1, 1633, 1200, 3]))
sobel = tf.Variable(tf.constant([[-1., -1., -1.], [0, 0, 0], [1., 1., 1.],
[-2., -2., -2.], [0, 0, 0], [2., 2., 2.],
[-1., -1., -1.], [0, 0, 0], [1., 1., 1.]
], shape=[3, 3, 3, 1]))
# 3 个通道输入, 生成 1 个feature map
# 卷积的不长 1x1, padding为 SAME 表明这是个同卷积操作
op = tf.nn.conv2d(input_full, sobel, strides=[1, 1, 1, 1], padding='SAME')
'''
sobel算子处理过的图片不保证每个像素都在255之间, 所以要做一次归一化操作,让生成的值都在【0, 1】之间,然后再乘以255
'''
o = tf.cast(((op - tf.reduce_min(op))/(tf.reduce_max(op) - tf.reduce_min(op)))*255, tf.uint8)
# 运行卷积操作并显示
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
print(sess.run(sobel))
t, f = sess.run([o, sobel], feed_dict={ input_full: full })
t = np.reshape(t, [1633, 1200])
plt.imshow(t, cmap='Greys_r')
plt.axis('off')
plt.show()
# [
# [
# [[-1.] [-1.] [-1.]]
# [[ 0.] [ 0.] [ 0.]]
# [[ 1.] [ 1.] [ 1.]]
# ]
# [
# [[-2.] [-2.] [-2.]]
# [[ 0.] [ 0.] [ 0.]]
# [[ 2.] [ 2.] [ 2.]]
# ]
# [
# [[-1.] [-1.] [-1.]]
# [[ 0.] [ 0.] [ 0.]]
# [[ 1.] [ 1.] [ 1.]]
# ]
# ]
实验结果:
这次就到这、、、、、