前言
这篇文章介绍<Is the deconvolution layer the same as a convolutional layer?>论文中提出的一种结合上升采样upsample和卷积操作的的一种方法,称之为Sub-piexl convolution。这种方法作用于低分率图像得然后通过其操作得到高分辨率图像,其实说白了就是上升采样,我们之前做过的maxpooling、average-pooling和transposed-convolution都有类似的效果,也就是将一个尺寸较小的图像通过算法使之变为尺寸较大的图像,如下图。
(cs231n课程课件截图)
相比于其他的上升采样操作,本文介绍的这个算法在相同运算速度下,不仅具有比maxpooling和averagepooling有对位置的记忆功能,而且相比Transposed convolution具有精度更高的优点。
思想描述
文章将这个算法描述为:LR network,即低分辨卷积网络。文章拿来与之作对比的是HR network,高分辨卷积网络,一般HR network是现将低分辨力的图像进行二次插值变换后然后对变换后的图像再进行卷积网络。像HR network是先将图像进行upsample后才进行卷积,而文中的这个算法操作则是在upsample的过程中对图像就进行了卷积。
这个算法的实现流程如上图,举个例子,实现的功能就是将一个1×1的image通过Sub-pixel操作将其变为的高分辨率图像,但是这个实现过程不是直接产生这个高分辨率图像而是先得到个通道特征图然后通过周期筛选(periodic shuffing)得到这个高分辨率的图像,其中r为upscaling factor,也就是图像扩大倍率。
知识铺垫
在说明这个具体的算法流程之前,我们先对几个知识回顾一下:
Transposed convolution and sub-pixel convolution layers
上图是一个简单的1D的卷积网络,x为输入y为输出,通过滤波器f来实现这个过程,x的size为8,f为4,y为5,x中灰色的方块表示用0进行padding。在f权重中的灰色方块代表f中某些值与x中的0进行了相乘。这个图就是1D卷积的过程,从x到y。
上图左面是一个转置卷积(stride为2),右面是sub-pixel convolution(stride为1/2),作用在1D上。
通过观察可以很容易的看到左边x为5,y为8,f为4,y中灰色部分代表被截去的部分,而右边通过对x进行部分 取值达到 过滤效果。通过比较两个卷积方法可以发现:如果把右边的fliter进行reverse(倒转),会发现两边得到的y值相同(自己可以试着加一下)。也就是说在参数(filter中权重值)可以学习的情况下,右边的操作和左边的操作完全相同。
Deconvolution layer vs Convolution in LR
接下来我们展示的是:在LR space中用一个简单的卷积参数为(o* r* r, i,k,k)-(输出通道,输入通道,kernel width,kernel height)的卷积操作,和deconvolution,其中卷积参数为((o, i ,k * r,k * r),k为正数)的作用是相同的。
我们用2D来进行演示,input为(1,4,4),kernel为(1,1,4,4),假设我们upsample的比率为2,我们要得到(1,8,8)的输出。
如上图,我们首先通过fractional indices从原input中创建一个sub-pixel图像,其中白色的像素就是原input中的像素(在LR sapce中),灰色是通过zero padding而来的。
用一个(1,1,4,4)的卷积核来和刚才生成的sub-pixel图像进行卷积,首先发现卷积核和sub-pixel图像中非零的像素进行了第一次有效卷积(图中紫色像素代表被激活的权重),然后我们将sub-pixels整体向右移动一格,让卷积核在进行一次卷积,会发现卷积核中蓝色的像素权重被激活,同理绿色和红色(注意这里是中间的那个8×8的sub-pixel图像中的白色像素点进行移动,而每次卷积的方式都相同)。
最后我们输出得到HR图像,HR图像和sub-pixel图像的大小是一致的,我们将其涂上颜色,颜色代表这个卷积核中权重和sub-pixel图像中哪个像素点进行了卷积(也就是哪个权重对对应的像素进行了贡献)。
需要注意的是,这是一个(1,1,4,4)的卷积核,上面每个权重皆为独立地被激活,也就是说我们可以轻易地将其分成(4,1,2,2)的卷积核(如上图)。这个操作是可逆的因为每个卷及权重在操作过程中都是独立的。
这样,这里不适用(1,1,4,4)的卷积核,而是使用(4,1,2,2)的卷积核,,对sub-pixel图像直接在LR space中进行卷及操作(如上图)。再将得到的(4,4,4)的输出进行周期筛选(periodic shuffling)将其reshape为HR image。
这个图片展示了将左面图像上方的(9,32,3,3)卷积层转化为底部的(1,32,9,9)卷积层,转化过程正如右方所示。也就是说,上方的卷积层对一个32通道的图像进行卷积然后周期筛选后得到的图像和使用下方的卷积层进行反卷积(deconvolution)得到的结果相同。
这个卷积操作是很灵活的,看一下之前的1D的卷积操作,可以将滤波器f=(1,2,3,4)更换为f1=(2,4)以及f2=(1,3),然后产生y1 = f1 * x和y2 = f2 * 2其中代表卷积操作。通过对y1和y2进行组合得到最终的结果y。还记得上面谈说过:
“在LR space中用一个简单的卷积参数为(o r* r,i,k,k)-(输出通道,输入通道,kernel width,kernel height)的卷积操作,和deconvolution,其中卷积参数为((o,i,k * r,k * r),k为正数)的作用是相同的。”
这里的f我们一般都以(k* r)来表示,但其实这个f可以为任意值,加入f=(1,2,3),我们就可以分成f=(2),f=(1,3),同样,得到结果组合起来即可。
核心思想以及结论
现在回到最开始说的那个操作,上图是这个算法的整个过程。通过上面的一系列论述,我们可以得到deconvolition layer的操作和在LR中输出的convolution操作得到的结果是一样的(d代表维度)。也就是说,得到个通道的这个卷积过程和在它之前得到个特征图的操作是一样的。
在上面的Hidden convolutional layers中,看倒数第二个layer,有n个feature maps。我们现在可以知道,在LR space中,通过一个放大倍率为2的upsampling去学习表示n feature maps和在HR space中,通过一个n/4个feature maps去表示的效果是一样的。想象在相同运行速度下,一个n=32(LR space),一个n/4=8个feature maps(HR space),在LR 下的network比在HR network下的表示能力是很强大的。
举个实际的例子,LR network(32 x 32 x 3 x 3 x W/2 x H/2)的复杂度和HR中的(8 x 8 x 6 x 6 x W x H)的复杂度是一样的(W和H代表image的宽和高)。特征图中存留的信息也是一样的:LR(l x 32 x w/2 x h/2)和HR(l x 8 x w x h),其中l代表layer的数量。接受域来说,在原始input LR space中都是一样的,但是LR network的参数(l x 32 x 32 x 3 x 3)比HR network(l x 8 x 8 x 6 x 6)的多,也就是说,LR中,network的卷积比HR中先对input进行upsample的卷积有很强的表示学习能力。
所以有时候先对原始input进行upsample然后再进行卷积并不好,至少在表示学习能力并没有sub-pixel convolution效果好。
实现代码(摘自Deepfake-keras实现版本)
class PixelShuffler(Layer): def __init__(self, size=(2, 2), data_format=None, **kwargs): super(PixelShuffler, self).__init__(**kwargs) self.data_format = conv_utils.normalize_data_format(data_format) self.size = conv_utils.normalize_tuple(size, 2, 'size') # (2,2) def call(self, inputs): input_shape = K.int_shape(inputs) if len(input_shape) != 4: raise ValueError('Inputs should have rank ' + str(4) + '; Received input shape:', str(input_shape)) if self.data_format == 'channels_first': batch_size, c, h, w = input_shape if batch_size is None: batch_size = -1 rh, rw = self.size oh, ow = h * rh, w * rw oc = c // (rh * rw) out = K.reshape(inputs, (batch_size, rh, rw, oc, h, w)) out = K.permute_dimensions(out, (0, 3, 4, 1, 5, 2)) out = K.reshape(out, (batch_size, oc, oh, ow)) return out elif self.data_format == 'channels_last': batch_size, h, w, c = input_shape if batch_size is None: batch_size = -1 rh, rw = self.size oh, ow = h * rh, w * rw oc = c // (rh * rw) out = K.reshape(inputs, (batch_size, h, w, rh, rw, oc)) out = K.permute_dimensions(out, (0, 1, 3, 2, 4, 5)) out = K.reshape(out, (batch_size, oh, ow, oc)) return out def compute_output_shape(self, input_shape): if len(input_shape) != 4: raise ValueError('Inputs should have rank ' + str(4) + '; Received input shape:', str(input_shape)) if self.data_format == 'channels_first': height = input_shape[2] * self.size[0] if input_shape[2] is not None else None width = input_shape[3] * self.size[1] if input_shape[3] is not None else None channels = input_shape[1] // self.size[0] // self.size[1] if channels * self.size[0] * self.size[1] != input_shape[1]: raise ValueError('channels of input and size are incompatible') return (input_shape[0], channels, height, width) elif self.data_format == 'channels_last': height = input_shape[1] * self.size[0] if input_shape[1] is not None else None width = input_shape[2] * self.size[1] if input_shape[2] is not None else None channels = input_shape[3] // self.size[0] // self.size[1] if channels * self.size[0] * self.size[1] != input_shape[3]: raise ValueError('channels of input and size are incompatible') return (input_shape[0], height, width, channels)
参考资料:
https://arxiv.org/pdf/1603.07285v1.pdf
https://arxiv.org/abs/1609.07009