[不定期更新长文]卷积神经网络工作原理研究

本人只是出于兴趣研究这个话题,本人并非学者,或者从业者,所以可能在写作中出现技术上的硬伤,请各位指正。
本文可能根据业界最新进展进行更新, 欢迎来评论中进行交流和研究。
本文所引用的资料将严格标明出处和版权。本文首发于简书平台,博客园和本人公众号,请勿转载

卷积神经网络在图像识别领域无限风光,通过一张图片,算法可以知道图片上的物体是什么,着实令人震惊,但是很多人和我一样,对于其背后的原理,都非常好奇,卷积神经网络是如何进行图像识别的呢

图像识别

如果你的英文主够好的话,可以阅读这篇论文:
Visualizing and Understanding Convolutional Networks

猫应该长成什么样子

看过女神李飞飞的ImageNet演讲的人,都对于下面两张图片印象深刻。
原文请阅读:
ImageNet缔造者:让冰冷的机器读懂照片背后的故事

(从薛定谔开始,猫就一直被各种科学家拿出来说事情,当然汪星人也时不时出镜)

人类是如何识别猫咪的?借用知乎的一个回答:

现在假设要做一个图像的分类问题,比如辨别一个图像里是否有一只猫,我们可以先判断是否有猫的头,猫的尾巴,猫的身子等等,如果这些特征都具备,那么我就判定这应该是一只猫。当然,如果图像是下面这样一只老实本分的猫咪,则一切都好办了。

正常的猫

但是喵星人不但品种不同,颜色繁多,各种销魂的动作也层出不穷,所以,机器识别猫还是很困难的。

扭曲的猫

这样,我们必须要让机器知道,猫,到底应该长成什么样子。

想象中的图形识别原理

第一次考虑怎么处理这个问题,一个很自然的想法浮想在脑海里面:
将所有猫咪的图片放在一起,提取出猫咪的共同特征,做成一个识别猫的模型。然后对于每张图片,使用模型,看一下是猫的概率为多少。但是如果真的这样做的话,可能每种物体都必须要有一个专门的模型了,这样可能是不行的,计算量可能也是一个问题。特别对于扭曲的猫,这样子的例子非常难处理,我们不太可能穷举出所有猫的正常和非正常形态。(毛色,眼神,是否有物体和猫进行交互)

当然,可以考虑,将猫进行分解,就如知乎网友所说,猫头,猫尾巴,猫爪子,独立进行识别。这样不管猫怎么扭曲,都无所谓了。当然,如果你是资深猫奴,你可以很高兴的说出猫的组成特征,但是,这样本质上还是加入了太多的领域专家的干涉。如果要识别大型粒子加速器,这个是不是要请物理学家参与呢?所以,机器应该完全屏蔽领域知识才可以做到泛用。

图像处理矩阵

虽然不是科班出身,但是以前或多或少看过一些图像处理的书籍。
一般的图像处理都是通过矩阵操作完成的。

  • 位置和形状 : 例如图像的拉伸,缩放,旋转(当然可以是各种效果的组合)。
  • 颜色: 灰度调整,透明度,滤镜

具体的颜色矩阵文章:
C++图像处理 -- 颜色矩阵变换

图像处理

其实我认为卷积核这个概念,应该是从图像处理矩阵这个概念来的。通过不同的图像处理矩阵,可以突出图像的某些特征,屏蔽掉某些细节。

图像滤镜处理算法:灰度、黑白、底片、浮雕

原图

处理后的图片,屏蔽了颜色,突出了轮廓特征。(猫的轮廓特征保留下来了,颜色特征暂时消失了)

黑白图

当然,实际处理的时候,可能使用的卷积核可能更加复杂。不过,如果真的看一下卷积核的工作方式,一般来说,卷积用来进行特征的提取,而不是进行图像的预处理的(或者说,是将图像针对特征进行压缩的一个过程)。

卷积核

主流图像识别

上面所说的都大半是猜测,无论如何也应该看一下真实的算法到底是怎么样的。图像识别上最有名气的算法大概就是Inception模型。整个算法的架构大概是这样的,深度也是叹为观止。(当前ResNet神经网络已经152层了,计算量相当相当相当可怕)
inception_v3_architecture.png

原始图像经过了深深的流水线之后,最后在Softmax层进行分类。这个过程中到底发生了什么事情,图像在Softmax层变成了什么,这个可能是所有人都关心的问题。本文也想通过长期的研究,能或多或少搞清楚里面的奥秘。这个过程应该是极其艰苦的,非常困难的。但是对于机器学习的思考却非常有帮助。

Inception V3源代码(Slim实现)

整体架构

Google的Tensorflow已经在Github上开源了,找到了这样的一个源代码,由于非科班出身,所以也无法断定是否这个就是inception的源代码了。暂时就以这个作为对象进行研究了
https://github.com/tensorflow/models/tree/master/inception
然后按照ReadMe的指示看到以下的工程
https://github.com/tensorflow/models/tree/master/slim
最新的V3代码在以下链接里面
https://github.com/tensorflow/models/blob/master/slim/nets/inception_v3.py

分析源代码的时候,可以将上节的图和代码一起观看。(暂时没有找到V4的图片,所以,只能研究V3了。如果大家有兴趣也可以研究最牛逼的ResNet深度残差网络)

从代码上看,整个深度网络的结构体系可能是这样子的。从输入端开始,先有3个卷积层,然后是1个pool层。然后又是2个卷积层,一个pool层。这个和上面那张神经网络构造图是完全一致的。前3个是卷积层(黄色),然后是1个MaxPool(绿色),然后是2个卷积层,1个Maxpool。
后面的11个混合层(Mixed)具体的代码还需要进一步检查。

Here is a mapping from the old_names to the new names:
  Old name          | New name
  =======================================
  conv0             | Conv2d_1a_3x3
  conv1             | Conv2d_2a_3x3
  conv2             | Conv2d_2b_3x3
  pool1             | MaxPool_3a_3x3
  conv3             | Conv2d_3b_1x1
  conv4             | Conv2d_4a_3x3
  pool2             | MaxPool_5a_3x3
  mixed_35x35x256a  | Mixed_5b
  mixed_35x35x288a  | Mixed_5c
  mixed_35x35x288b  | Mixed_5d
  mixed_17x17x768a  | Mixed_6a
  mixed_17x17x768b  | Mixed_6b
  mixed_17x17x768c  | Mixed_6c
  mixed_17x17x768d  | Mixed_6d
  mixed_17x17x768e  | Mixed_6e
  mixed_8x8x1280a   | Mixed_7a
  mixed_8x8x2048a   | Mixed_7b
  mixed_8x8x2048b   | Mixed_7c

TF-Slim

先看一下最前面的第1个卷积层,在继续阅读代码之前,想去网络上找一下关于slim的API资料,可惜暂时没有太多的资料。
TensorFlow-Slim@github
slim操作的源代码

TF-Slimを使ってTensorFlowを簡潔に書く
从下面这个例子可以看到,slim的conv2d构造的是一个激活函数为Relu的卷积神经网络。(其实slim估计和keras一样,是一套高级的API函数,语法糖)

//使用TensorFlow的代码
W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
//使用slim的代码
h_conv1 = slim.conv2d(x_image, 32, [5, 5])

第一个卷积层的输入参数 299 x 299 x 3 :

      # 299 x 299 x 3
      end_point = 'Conv2d_1a_3x3'
      net = slim.conv2d(inputs, depth(32), [3, 3], stride=2, scope=end_point)
      end_points[end_point] = net
      if end_point == final_endpoint: return net, end_points

前面的299 x 299 代表的含义,在源代码中可以看到,是图片的默认尺寸。(The default image size used to train this network is 299x299.)
后面一个3 表示深度Depth,原始的JPEG图片的每个像素具有RGB 3个不同的数值,在卷积层中则设置了3个通道。(这里只是我的主观推测而已)

然后看一下第一个卷基层自身的参数:
表示有32个不同的Filter(32套不同的参数,最终形成32个FeatureMap)。卷积核是 3 * 3 ,步长为2。
(每个Filter的深度也应该是3,如果要表示这个Filter的张量,应该是 3 x 3 x 3,高度,宽度,深度都是3)

卷积前后关系

在上面两个公式中,W2是卷积后Feature Map的宽度;W1是卷积前图像的宽度;F是filter的宽度;P是Zero Padding数量,Zero Padding是指在原始图像周围补几圈0,如果的值是1,那么就补1圈0;S是步幅;H2是卷积后Feature Map的高度;H1是卷积前图像的高度。

按照公式可以推导出卷积之后的Feature Map 为 149 x 149
W2 = (299 - 3 + 2 * 0)/ 2 + 1 = 149

第一层的卷积输出就是第二层的卷积输入,所以第二层的第一行表示输入的注释是这样的:

      # 149 x 149 x 32
      end_point = 'Conv2d_2a_3x3'
      net = slim.conv2d(net, depth(32), [3, 3], scope=end_point)
      end_points[end_point] = net
      if end_point == final_endpoint: return net, end_points

149 x 149 x 32 :卷积前的特征图(FeatureMap)的大小是149 x 149 ,一共有32个特征图。

关于padding的细节

如果再往下看代码,会看到一个padding的参数设定

      # 147 x 147 x 32
      end_point = 'Conv2d_2b_3x3'
      net = slim.conv2d(net, depth(64), [3, 3], padding='SAME', scope=end_point)
      end_points[end_point] = net
      if end_point == final_endpoint: return net, end_points

padding有两种参数可以设定,分别是SAME和VALID:
What is the difference between 'SAME' and 'VALID' padding in tf.nn.max_pool of tensorflow?

If you like ascii art:

padding
In this example:

Input width = 13
Filter width = 6
Stride = 5
Notes:

"VALID" only ever drops the right-most columns (or bottom-most rows).
"SAME" tries to pad evenly left and right, but if the amount of columns to be added is odd, it will add the extra column to the right, as is the case in this example (the same logic applies vertically: there may be an extra row of zeros at the bottom).

这个例子很清楚的解释了两个参数的含义。如果Input的宽度是13,卷积核宽度是6,步长是5的情况下,VALID将只做2次卷积(1-6,6-11),第三次由于宽度不够(11-16,但是14,15,16缺失),就被舍弃了。SAME的情况下,则自动在外层补零(Zero Padding),保证所有的元素都能够被卷积使用到。
注意:如果conv2d方法没有特别设定padding,则需要看一下arg_scope是否标明了padding。

前三层卷积的总结

      with slim.arg_scope([slim.conv2d, slim.max_pool2d, slim.avg_pool2d],
                        stride=1, padding='VALID'):
      # 299 x 299 x 3
      end_point = 'Conv2d_1a_3x3'
      net = slim.conv2d(inputs, depth(32), [3, 3], stride=2, scope=end_point)
      end_points[end_point] = net
      if end_point == final_endpoint: return net, end_points
      # 149 x 149 x 32
      end_point = 'Conv2d_2a_3x3'
      net = slim.conv2d(net, depth(32), [3, 3], scope=end_point)
      end_points[end_point] = net
      if end_point == final_endpoint: return net, end_points
      # 147 x 147 x 32
      end_point = 'Conv2d_2b_3x3'
      net = slim.conv2d(net, depth(64), [3, 3], padding='SAME', scope=end_point)
      end_points[end_point] = net
      if end_point == final_endpoint: return net, end_points

注意:前三层默认是步长为1,padding为VALID。

以下文字,需要业内人士帮忙看一下是否正确:
输入的时候,原始图像大小是 299 x 299 的。
在图像预处理的时候,根据 R G B 三个通道,将图像分为了3个深度。
这样的话,输入层是 高度299 宽度 299 深度3

卷积神经元

第一个卷积层,由于Depth是32,则认为一共有32个深度为3,高度和宽度为3的Filter。步长为2
卷积之后,结果为32个特征图,高度和宽度为149.

前面我们已经讲了深度为1的卷积层的计算方法,如果深度大于1怎么计算呢?其实也是类似的。如果卷积前的图像深度为D,那么相应的filter的深度也必须为D。我们扩展一下式1,得到了深度大于1的卷积计算公式:
卷积深度
说明

不管深度为多少,经过一个Filter,最后都通过上面的公式变成一个深度为1的特征图。

下面的例子中,输入层是高度和宽度是 7 x 7 ,深度是3.
两个Filter的,每个Filter的高度和宽度是 3 x 3 ,深度因为要和输入层保持一致,所以也必须是 3
最左边的输入层(Input Volume)和Filter W0 进行计算(输入的第一层和Filter的第一层进行运算,第二层和第二层进行运算,第三层和第三层进行运算,最后三层结果累加起来),获得了 Output Volume 的第一个结果(绿色的上面一个矩阵);和Filter W1 进行计算,获得了 Output Volume 的第二个结果(绿色的下面一个矩阵)。

访问 //upload-images.jianshu.io/upload_images/2256672-958f31b01695b085.gif 观看动态图片
Filter

MaxPool

Pool是一个将卷积参数进行减少的过程,这里是将 3 x 3 的区域进行步长为2的Max的下采样。
这里同样可以使用步长和宽度的计算公式,获得输出层的高度和宽度。
W2 = (147 - 3 + 2 * 0)/ 2 + 1 = 73
和卷积层相比,这里就没有什么深度计算了。这里只是单纯的进行特征图的压缩而已。
对于深度为D的Feature Map,各层独立做Pooling,因此Pooling后的深度仍然为D。

Max

      # 147 x 147 x 64
      end_point = 'MaxPool_3a_3x3'
      net = slim.max_pool2d(net, [3, 3], stride=2, scope=end_point)
      end_points[end_point] = net
      if end_point == final_endpoint: return net, end_points
      # 73 x 73 x 64
      end_point = 'Conv2d_3b_1x1'
      net = slim.conv2d(net, depth(80), [1, 1], scope=end_point)
      end_points[end_point] = net
      if end_point == final_endpoint: return net, end_points

按照这个思路整理Inception V3的Mixed Layer之前的代码,应该没有什么问题了。

      # 299 x 299 x 3
      end_point = 'Conv2d_1a_3x3'
      net = slim.conv2d(inputs, depth(32), [3, 3], stride=2, scope=end_point)
      end_points[end_point] = net
      if end_point == final_endpoint: return net, end_points
      # 149 x 149 x 32
      end_point = 'Conv2d_2a_3x3'
      net = slim.conv2d(net, depth(32), [3, 3], scope=end_point)
      end_points[end_point] = net
      if end_point == final_endpoint: return net, end_points
      # 147 x 147 x 32
      end_point = 'Conv2d_2b_3x3'
      net = slim.conv2d(net, depth(64), [3, 3], padding='SAME', scope=end_point)
      end_points[end_point] = net
      if end_point == final_endpoint: return net, end_points
      # 147 x 147 x 64
      end_point = 'MaxPool_3a_3x3'
      net = slim.max_pool2d(net, [3, 3], stride=2, scope=end_point)
      end_points[end_point] = net
      if end_point == final_endpoint: return net, end_points
      # 73 x 73 x 64
      end_point = 'Conv2d_3b_1x1'
      net = slim.conv2d(net, depth(80), [1, 1], scope=end_point)
      end_points[end_point] = net
      if end_point == final_endpoint: return net, end_points
      # 73 x 73 x 80.
      end_point = 'Conv2d_4a_3x3'
      net = slim.conv2d(net, depth(192), [3, 3], scope=end_point)
      end_points[end_point] = net
      if end_point == final_endpoint: return net, end_points
      # 71 x 71 x 192.
      end_point = 'MaxPool_5a_3x3'
      net = slim.max_pool2d(net, [3, 3], stride=2, scope=end_point)
      end_points[end_point] = net
      if end_point == final_endpoint: return net, end_points
      # 35 x 35 x 192.

原始的图片大小是299 x 299 ,由于有三元色,则深度为 3.
经过一系列处理之后,尺寸变成了 35 * 35 ,深度则上升为 192.
卷积使用的激活函数是Relu。Pooling使用的是 Max Pooling。
Relu

深入Mixed层

      # mixed: 35 x 35 x 256.
      end_point = 'Mixed_5b'
      with tf.variable_scope(end_point):
        with tf.variable_scope('Branch_0'):
          branch_0 = slim.conv2d(net, depth(64), [1, 1], scope='Conv2d_0a_1x1')
        with tf.variable_scope('Branch_1'):
          branch_1 = slim.conv2d(net, depth(48), [1, 1], scope='Conv2d_0a_1x1')
          branch_1 = slim.conv2d(branch_1, depth(64), [5, 5],
                                 scope='Conv2d_0b_5x5')
        with tf.variable_scope('Branch_2'):
          branch_2 = slim.conv2d(net, depth(64), [1, 1], scope='Conv2d_0a_1x1')
          branch_2 = slim.conv2d(branch_2, depth(96), [3, 3],
                                 scope='Conv2d_0b_3x3')
          branch_2 = slim.conv2d(branch_2, depth(96), [3, 3],
                                 scope='Conv2d_0c_3x3')
        with tf.variable_scope('Branch_3'):
          branch_3 = slim.avg_pool2d(net, [3, 3], scope='AvgPool_0a_3x3')
          branch_3 = slim.conv2d(branch_3, depth(32), [1, 1],
                                 scope='Conv2d_0b_1x1')
        net = tf.concat(axis=3, values=[branch_0, branch_1, branch_2, branch_3])
      end_points[end_point] = net
      if end_point == final_endpoint: return net, end_points

avgPooling 和 tf.concat

一般做Pooling的时候,使用的是maxPooling,在Mixed层出现了avgPooling。

除了Max Pooing之外,常用的还有Mean Pooling——取各样本的平均值。

可能avgpooling就是mean pooling吧。

tf.concat函数也是一个重点知识:这里使用concat将张量进行连接。但是具体的连接形状还需要进一步考证。

concat

Inception V4架构图

v3的架构图,对于mixed的细节并不是很清晰,所以这里找了一张v4的架构图来看一下。

Inception v4

看一下右下角,除了参数之外和我们的Mixed模型很像了。(V4比V3更加的深)

filter

Mixed 层(inception结构)分析

inception结构具有3种不同的形式,(Mixed_5x,Mixed_6x,Mixed_7x),下面就是这3种形式的示例图。
仔细观察,这里有两个特点:
这里使用了很多 1 x 1的卷积核。

Googlenet 中1*1 卷积核分析

一种简单的解释是用来降维。
For example, an image of 200200 with 50 features on convolution with 20 filters of 11 would result in size of 20020020.
但是,1*1卷积核的作用不仅仅于此。

  • 特征变换
    11卷积是在Network in Network 中第一次被提出来,作者的目的是为了得到一个深的网络,但作者并不想直接垂直的堆砌一些layer。作者用了一些11,33的卷积层,因此
    最后的网络架构其实是going wide. 在googlenet中,1
    1卷积核有两个作用:
    1 为了使得网络更深,作者引入了类似Network in Network 中的"inception module"
    2 为了降低维度
    3 为了增加更多的非线性变换(eg. RELU)
  • 其他作用
    1 11卷积核可以结合max pooling
    2 1
    1卷积核可以设置大的步长,这样可以在丢失非常少的信息下降低数据的维度
    3 取代fc层。

或者将 n x n 的卷积核改写为 n x 1 和 1 x n 。

v3一个最重要的改进是分解(Factorization),将7x7分解成两个一维的卷积(1x7,7x1),3x3也是一样(1x3,3x1),这样的好处,既可以加速计算(多余的计算能力可以用来加深网络),又可以将1个conv拆成2个conv,使得网络深度进一步增加,增加了网络的非线性,还有值得注意的地方是网络输入从224x224变为了299x299,更加精细设计了35x35/17x17/8x8的模块;
作者:无话可说
链接:https://www.zhihu.com/question/50370954/answer/138938524
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

inception结构

辅助块

在整个Mixed层的中间,可以看到有一个分支块。这个分支包含一个AvgPool层,两个Conv层,和一个Fully Connect层,一个Softmax层。
这个层是用来干什么的呢?从代码的注释看:

Auxiliary Head logits 如果直译的话:辅助用头部洛基特几率。
这个东西的用法,在模型里面无法找到答案,那么我们看一下测试用代码里面是不是有答案。

https://github.com/tensorflow/models/blob/master/slim/nets/inception_v3_test.py

  def testBuildEndPoints(self):
    batch_size = 5
    height, width = 299, 299
    num_classes = 1000
    ...
    ...
    self.assertTrue('AuxLogits' in end_points)
    aux_logits = end_points['AuxLogits']
    self.assertListEqual(aux_logits.get_shape().as_list(),
                         [batch_size, num_classes])

这个看上去应该是用来做检证的,看一下张量的形状是不是和我们预期的一样。并没有什么特别的意义。

最后3层

最后3层的理解应该是比较容易的。

    def inception_v3(inputs,
                 num_classes=1000,
                 is_training=True,
                 dropout_keep_prob=0.8,
                 min_depth=16,
                 depth_multiplier=1.0,
                 prediction_fn=slim.softmax,
                 spatial_squeeze=True,
                 reuse=None,
                 scope='InceptionV3'):

      # Final pooling and prediction
      with tf.variable_scope('Logits'):
        kernel_size = _reduced_kernel_size_for_small_input(net, [8, 8])
        net = slim.avg_pool2d(net, kernel_size, padding='VALID',
                              scope='AvgPool_1a_{}x{}'.format(*kernel_size))
        # 1 x 1 x 2048
        net = slim.dropout(net, keep_prob=dropout_keep_prob, scope='Dropout_1b')
        end_points['PreLogits'] = net
        # 2048
        logits = slim.conv2d(net, num_classes, [1, 1], activation_fn=None,
                             normalizer_fn=None, scope='Conv2d_1c_1x1')
        if spatial_squeeze:
          logits = tf.squeeze(logits, [1, 2], name='SpatialSqueeze')
        # 1000
      end_points['Logits'] = logits
      end_points['Predictions'] = prediction_fn(logits, scope='Predictions')

Dropout层:

这个层的作用是随机除去一些神经元,使得整个模型不至于过拟合。
至于为什么这样做能够防止过拟合,网络上有很多说明文档,这里就不再啰嗦了。
这里一般选择keep_prob = 0.8 (这个参数值在代码中定义,可以修改),保留80%的神经元。至于为什么是0.8,这个应该是很多实验得出的结果。
理解dropout

Dropout

FullConnect

全连接层,在整个过程的最后,才使用全连接,训练出权重。
(仅仅这里进行训练权重?还是filter也需要训练?)

FullConnect

Softmax

这个神经网络的最后是softmax层。softmax层也就是分类专用的层,使用一个概率来表示待分类对象有多大概率属于某个类。
softmax
最后的概率矩阵看上去应该是这个样子的。
概率矩阵

关于Filter的意义

本章节参照了zhihu.com的内容。所以我完整引用,不进行任何修改。版权归原作者所有

Paste_Image.png
你眼睛真实看到的图像其实是上图的下半部分。而后经过大脑的层层映射后才出现了你脑中所“看见”的图像。CNN的卷积层部分可以理解成是学习你的“眼球结构”。

Paste_Image.png

同一个filter内部的权重是相同的,因为它用一个“抓取方式”去侦测特征。比如说“边缘侦测”。 你也注意到了,我们的眼睛不只观看一次,等到扫描完该特征后,另一个filter可以改变“抓取方式”去侦测另一个特征。所权重在同一个filter内是共享的理解是该filter对整个图片进行了某个特征的扫描。

Paste_Image.png

提取若干个特征后,就可以靠这些特征来判断图片是什么了。

疑问

1.filter的选择问题,各个filter是怎么确定内部的值的?
2.训练到底只是训练最后的全连接层,还是整个神经网络?

未完待续

参考资料

卷积神经网络工作原理直观的解释?
[透析] 卷积神经网络CNN究竟是怎样一步一步工作的?
TF-Slimを使ってTensorFlowを簡潔に書く
深入浅出——网络模型中Inception的作用与结构全解析
零基础入门深度学习(4) - 卷积神经网络
cs231n学习笔记-CNN-目标检测、定位、分割
A Note to Techniques in Convolutional Neural Networks and Their Influences III (paper summary)
理解dropout
CNN卷积神经网络架构综述

posted @ 2017-04-05 11:38  数据可视化俱乐部  阅读(...)  评论(...编辑  收藏