最近在读Joint Face Detection and Alignment using Multi-task Cascaded Convolutional Networks这篇论文时发现其中的卷积神经网络结构中有池化操作时无法整除步长的情况,由于之前也思考过类似的问题,这里特地探究了一下tensorflow中是如何处理这种情况的,也顺带总结了一点Keras库的知识。
Tensorflow中卷积和池化层输出维数的准确计算
这里先来看看上面提到的论文中的情况,输入图片尺寸是12×12×3,经过一个包含10个卷积核大小为3×3、步长为1的卷积层,得到10×10×10的volume,再经过filter size为3×3,步长为2的池化层,得到5×5×10的volume。卷积层没有问题,到池化层的时候,如果用输出维度计算公式——(n+2*p-f)/s+1——来计算就会遇到问题,这里n=10(输入的尺寸),f=3(filter size),s=2(步长),如果没有p(padding),(10-3)/2就无法整除了,如果要满足(n+2p-f) / s+1=5,2p就要等于1,意味着在池化操作的时候要对输入进行边框的填充,而且是不对称的填充,也就是在“左”、“右”中的一边和“上”、“下”中的一边填充一像素的0。
那么在Tensorflow框架中真的是这样操作的吗?
口说无凭,我们还是用代码来实践一下:
import numpy as np
import keras
from keras import backend as K # 导入后端模块
from keras.models import Model
from keras.layers import Input, Conv2D, MaxPooling2D, ZeroPadding2D, AveragePooling2D
先引入需要的包和模块。
这里我用到了Keras库,Keras 是一个用 Python 编写的高级神经网络 API,高度模块化,能够简化神经网络代码的编写。
Keras 的核心数据结构是 model,一种组织网络层(包括全连接层、激活层、卷积层、池化层和输出层等等的各种层)的方式。最简单的模型是 Sequential 顺序模型,它是由多个网络层线性堆叠的栈。对于更复杂的结构,你应该使用 Keras 函数式 API,它允许构建任意的神经网络图。
Sequential模型比较简单,阅读Keras中文文档就能理解,函数式API的意思是把网络层的实例看作一个函数,它以张量为参数,并返回一个张量,定义一个Model,以最初输入层(Input)的输出张量和最终层的输出张量为参数,这个Model的实例和Sequential Model的实例一样可以被训练(compile、fit和evaluate等)。后面由于我要看卷积层和池化层输出volume的各个维度值,因此要用到函数式API每层都返回计算结果张量的特性。
inputs = Input(shape=(12,12,3)) # 注意shape参数不包括batch_size
x = Conv2D(10, (3, 3), strides = (1, 1))(inputs)
print(x.shape) # (?, 10, 10, 10)
x = MaxPooling2D((3, 3), strides = (2, 2))(x)
print(x.shape) # (?, 4, 4, 10),如果加上padding = 'same',(?, 5, 5, 10)
上面的代码在注释中给出了输出张量的形状,卷积层不用看,只看最大池化层,在Keras的池化函数里,有一个padding
参数,默认值是valid
,可选值是same
,从上面代码的注释可以看出,同样形状的输入,在池化层padding
参数选两种不同值的时候输出是不同的,为什么会这样呢,我在网上找到一些博客文章,说是:
“SAME”表示超过边界用0填充,使得输出保持和输入相同的大小,“VALID”表示不超过边界,通常会丢失一些信息。
吴恩达的课程里也是这么说的,但具体到底是怎么处理的感觉还是没有说清楚,于是我去看了Keras中MaxPooling2D
部分的源码,在MaxPooling2D
类里找到了如下代码:
def _pooling_function(self, inputs, pool_size, strides,
padding, data_format):
output = K.pool2d(inputs, pool_size, strides,
padding, data_format,
pool_mode='max')
return output
上面的K就是Tensorflow后端,也就是程序开头引入的backend模块,后端的具体概念后面会讲到,这里我们再到backend模块里去找pool2d
方法,发现:
if pool_mode == 'max':
x = tf.nn.max_pool(x, pool_size, strides,
padding=padding,
data_format=tf_data_format)
上面的这段代码说明,Keras中的最大池化其实是通过Tensorflow最大池化方法实现的,查询Tensorflow文档可以看到Tensorflow中卷积和池化的padding处理相同:
If padding == “SAME”: output_spatial_shape[i] = ceil(input_spatial_shape[i] / strides[i])
If padding == “VALID”: output_spatial_shape[i] = ceil((input_spatial_shape[i] - (spatial_filter_shape[i]-1) * dilation_rate[i]) / strides[i]).
上面的公式给出了输出维数的计算方法,具体到前文提到的论文中的例子,池化层的input_spatial_shape
就是10,stride
是2,spatial_filter_shape
是3,dilation_rate
是1,用上面的公式计算一下就能发现和前面程序中输出的维数吻合。
Tensorflow中padding具体填充的是什么?
本来到这里,池化输出的维度问题基本已经解决了,但是我还在想,在不对称填充的情况下,究竟是在左边填充还是在右边填充,在上面填充还是在下面填充呢?这时,只知道每层输出的张量的维数就不行了,还要知道张量里每个元素的具体值,这就要用到Keras后端了。
Keras侧重于快速建模,不处理诸如张量乘积和卷积等低级操作,它依赖于一个专门的、优化的张量操作库来完成这个操作,它可以作为 Keras 的「后端引擎」,Tensorflow就是后端引擎之一。
可以通过Keras后端实例化一个张量变量,并把一个numpy多维数组的值赋给它,然后再输入到卷积层和池化层里,再看输出张量的具体值:
val = np.array([[[[-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],[-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],[-1,-1,-1],[-1,-1,-1],[-1,-1,-1],[-1,-1,-1],[-1,-1,-1]]]])
print(val.shape) # (1, 6, 6, 3)
x = K.variable(value = val)
x = MaxPooling2D((3, 3), strides = 1, padding = 'same')(x)
print(x.shape) # (1, 3, 3, 3)
print(K.eval(x))
上面的代码里,我实例化了一个形状为(1,6,6,3)的张量,里面每个元素的值为-1,在池化层padding
为same
的条件下,应该需要1像素的不对称填充,也就是会在上(或下)和左(或右)填充1像素的0,那么按照最大池化的计算,输出的张量中应该有0值的元素,可是我运行上面代码后实际的结果却是:
[[[[-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,这是为什么呢?这个问题当时也困扰了我半天,我还做了一些其他的实验看结果,还是没能搞懂。
还好有万能的Google和Stackoverflow,帮我找到了答案:原来在Tensorflow中池化(包括平均池化和最大池化)时如果有填充像素,是不考虑填充像素的,也就是说最大池化的时候如果有填充,实际上填充的是-inf(负无穷),-1当然比负无穷大咯。
总结
- Tensorflow中卷积和池化层输出维数的计算公式:
If padding == "SAME": output_spatial_shape[i] = ceil(input_spatial_shape[i] / strides[i])
If padding == "VALID": output_spatial_shape[i] = ceil((input_spatial_shape[i] - (spatial_filter_shape[i]-1) * dilation_rate[i]) / strides[i]).
- Tensorflow中池化层如果有填充,不考虑填充像素的数值。
参考资料
- Joint Face Detection and Alignment using Multi-task Cascaded Convolutional Networks
- Keras中文文档
- MaxPooling2D源码——Keras
- tf.nn.convolution——Tensorflow API
- python 里 np.array 的shape (2,)与(2,1)的分别是什么意思,区别是什么?——百度知道
- Tensorflow: tf.nn.avg_pool() with ‘SAME’ padding does not average over padded pixels——Stackoverflow
本作品采用知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。要查看该许可协议,可访问 http://creativecommons.org/licenses/by-nc-sa/4.0/ 或者写信到 Creative Commons, PO Box 1866, Mountain View, CA 94042, USA。