正文
本文主要是把之前在知乎上的回答
对于上采样+卷积操作,就是一个最近邻或者双线插值上采样到想要的feature map 空间大小再接一层卷积。但是对于反卷积,相信有不少炼丹师并不了解其具体实现原理,即反卷积是如何实现增大feature map空间大小的,而本文主要内容就是把反卷积具体实现讲清楚。
卷积前后向传播实现细节
在讲解反卷积计算实现细节之前,首先来看下深度学习中的卷积是如何实现前后向传播的。
先来看下一般训练框架比如Caffe和MXNet卷积前向实现部分代码:
Caffe:
MXNet:
从实现上看,卷积的前向的实现方式都是
im2col 实现细节:
假设输入feature map 维度是
则
因为没有padding且步长为1,所以根据卷积输出大小计算公式可得:
所以
更一般的卷积前向计算:
则
我们接着来看卷积反向传播是如何实现的。
其实用不太严谨的方式来想,我们知道输入对应的梯度维度大小肯定是和输入大小一致的,而上一层传回来的梯度大小肯定是和输出一致的。而且既然是反向传播,计算过程肯定是卷积前向过程的逆过程。
所以是将权值转置之后左乘输出梯度,得到类似
这个
简单来说就是把中间buffer结果的每一列从一个
反卷积的两种实现方式
理解卷积实现细节之后,再来看下反卷积的两种实现方式,这里只讨论步长大于1,pad大于0的情况。
GEMM + col2im
其实从前面卷积的实现过程可以看到,如果卷积步长大于1的话,输出大小是小于输入的,但是反向传播的时候,输出梯度通过
下面给出反卷积前向过程示意图:
所以反卷积核的维度是
看caffe里面反卷积的实现确实也是调用的卷积的后向传播实现。
一般在用反卷积的时候都是需要输出大小是输入的两倍这样子,但是仔细回想一下卷积的输出大小计算公式:
如果根据这个公式反推,
假设
下面画个简单的计算流程图展示下卷积的反向传播和反卷积的前向传播过程,假设卷积和反卷积核大小都是3x3,步长为2,pad为1,卷积输入大小是4x4,则假设需要卷积输出或反卷积输入大小是2x2,则现在看下如何从2x2大小的输入反推输出4x4。
为了方便理解,假设卷积输出梯度或者反卷积输入都是1,输入和输出通道都是1:
为什么要center crop,可以这样想,原来卷积输入是4x4的,然后是pad了0再卷积得到输出2x2,在梯度回传的时候我们其实是只需要中间4x4部分的梯度,相当于把pad的部分去掉。
用MXNet
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import mxnet as mx import numpy as np data_shape = (1, 1, 2, 2) data = mx.nd.ones(data_shape) deconv_weight_shape = (1, 1, 3, 3) deconv_weight = mx.nd.ones(deconv_weight_shape) deconv_weight[:] = np.array([1,2,3,4,5,6,7,8,9]).reshape((1,1,3,3)) # deconvolution forward data_deconv = mx.nd.Deconvolution(data=data, weight=deconv_weight, kernel=(3, 3), pad=(1, 1), stride=(2, 2), adj=(1, 1), num_filter=1) print(data_deconv) # convolution backward data_sym = mx.sym.Variable('data') conv_sym = mx.sym.Convolution(data=data_sym, kernel=(3, 3), stride=(2, 2), pad=(1, 1), num_filter=1, no_bias=True, name='conv') executor = conv_sym.simple_bind(data=(1, 1, 4, 4), ctx=mx.cpu()) deconv_weight.copyto(executor.arg_dict['conv_weight']) executor.backward(mx.nd.ones((1, 1, 2, 2))) print(executor.grad_dict['data']) |
可以看到运行结果和手推是一样的:
输入插空补0+卷积
其实反卷积还有一种实现方式,就是输入插空补0再加一个卷积(这里需要注意,卷积的时候需要把反卷积核旋转180度,下面会详细讲)的方式,同上这里只讨论步长大于1,pad大于0的情况。
根据文章
然后假设反卷积前向,输入大小是
那实际插空补0是怎么做呢,这里直接给出结论,输入之间插入
下面看下文章
假设卷积输入是
假设卷积输入是
下面用实际例子来讲解下实际计算过程,假设反卷积核大小都是3x3,步长为2,pad为1,假设反卷积输入大小是2x2,则现在看下如何从2x2大小的输入反推输出4x4。
为了方便理解,假设反卷积输入都是1,输入和输出通道都是1:
可以看到结果和上面
一般看训练和推理框架的实现的方式都是
https://github.com/alibaba/MNN/blob/master/source/backend/metal/MetalDeconvolution.metal#L83
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | #define UP_DIV(x, y) (((x) + (y) - (1)) / (y)) #define ROUND_UP(x, y) (((x) + (y) - (1)) / (y) * (y)) kernel void deconv_depthwise(const device ftype4 *in [[buffer(0)]], device ftype4 *out [[buffer(1)]], constant deconv_constants& cst [[buffer(2)]], const device ftype4 *wt [[buffer(3)]], const device ftype4 *biasTerms [[buffer(4)]], ushort3 gid [[thread_position_in_grid]]) { if ((int)gid.x >= 4 || (int)gid.y >= 4) return; float4 result = float4(biasTerms[(short)gid.z]); short oy = (short)gid.y + 1; // 第一个输出:1,第6个输出:2 short ox = (short)gid.x + 1; // 第一个输出:1,第6个输出:2 short max_sy = min((2 - 1) * 2, oy / 2 * 2); // 第一个输出:0,第6个输出:2 short max_sx = min((2 - 1) * 2, ox / 2 * 2); // 第一个输出:0,第6个输出:2 short min_ky = UP_DIV(oy - max_sy, 1); // 第一个输出:1,第6个输出:0 short min_kx = UP_DIV(ox - max_sx, 1); // 第一个输出:1,第6个输出:0 if ((oy - min_ky * 1) % 2 == 0 && (ox - min_kx * 1) % 2 == 0) { short min_sy = max(0, ROUND_UP(oy + 1 - 3 * 1, 2)); // 第一个输出:0,第6个输出:0 short min_sx = max(0, ROUND_UP(ox + 1 - 3 * 1, 2)); // 第一个输出:0,第6个输出:0 short max_ky = (oy - min_sy) / 1; // 第一个输出:1,第6个输出:2 short max_kx = (ox - min_sx) / 1; // 第一个输出:1,第6个输出:2 short min_iy = (oy - max_ky * 1) / 2; // 第一个输出:0,第6个输出:0 short min_ix = (ox - max_kx * 1) / 2; // 第一个输出:0,第6个输出:0 for (auto ky = max_ky, iy = min_iy; ky >= min_ky; ky -= 2, iy += 2) { for (auto kx = max_kx, ix = min_ix; kx >= min_kx; kx -= 2, ix += 2) { auto wt4 = wt[ky * 3 + kx]; auto in4 = in[iy * 2 + ix]; result += float4(in4 * wt4); } } } } |
这里我把代码简化了,为了方便理解,同时也把一些参数都按照上面例子带入进去了。这里GPU实现的思路,简单来说开启的线程数是输出的大小,假设现在输出维度是
这里kernel实现的是计算一个输出点的代码,而且因为实际实现的时候,输入并没有真的去插空补0和Padding,反卷积核也没有真的去旋转180度,所以看到绝大部分代码在计算当前线程负责的输出点所对应的权值和输入的取值索引。
这里线程维度是3维的,所以gid.x表示输出宽索引,gid.y表示高索引,gid.z表示通道索引。
代码里面的注释是按照卷积顺序,计算第一个卷积输出点和第6个输出点,变量所对应的值,就是上面流程图的红框和蓝框。最后看到卷积循环,恰好就是对应各自输入和权值的取值点。
反卷积的缺点
分析完反卷积的运算过程,再来看下反卷积的缺点。
反卷积有一个最大的问题是,如果参数配置不当很容易出现输出feature map带有明显棋盘状的现象,原因就是在与
文献
而偶数kernel就不会:
而如果是多层堆叠反卷积的话而参数配置又不当,那么棋盘状的现象就会层层传递:
所以当使用反卷积的时候参数配置需要特别的小心。
下面就用简单的几句代码来复现使用反卷积可能会带来的的网格问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | import mxnet as mx batch_size = 1 in_channel = 1 height = 5 width = 5 data_shape = (batch_size, in_channel, height, width) data = mx.nd.ones(data_shape) out_channel = 1 kernel_size = 3 deconv_weight_shape = (in_channel, out_channel, kernel_size, kernel_size) deconv_weight = mx.nd.ones(deconv_weight_shape) stride = 2 up_scale = 2 data_deconv = mx.nd.Deconvolution(data=data, weight=deconv_weight, target_shape=(height * up_scale, width * up_scale), kernel=(kernel_size, kernel_size), stride=(stride, stride), num_filter=out_channel) print(data_deconv) data_upsample = mx.nd.contrib.BilinearResize2D(data=data, scale_height=up_scale, scale_width=up_scale) conv_weight_shape = (out_channel, in_channel, kernel_size, kernel_size) conv_weight = mx.nd.ones(conv_weight_shape) pad = (kernel_size - 1) / 2 data_conv = mx.nd.Convolution(data=data_upsample, weight=conv_weight, kernel=(kernel_size, kernel_size), pad=(pad, pad), num_filter=out_channel, no_bias=True) print(data_conv) |
这里为了简化,反卷积和卷积的权重都是设为1,而输入与输出 feature map 通道数都是1,输入 feature map 的值都是1,然后来看下反卷积和上采样+卷积的前向结果:
可以看到,kernel为3,步长为2的情况下,反卷积在不训练的情况下,输出就带有明显很规律的棋盘状。接着我们把kernel改为4看看:
可以看到棋盘状消失了。
所以在实际应用中对于一些像素级别的预测任务,比如分割,风格化,Gan这类的任务,对于视觉效果有要求的,在使用反卷积的时候需要注意参数的配置,或者直接换成上采样+卷积。
参考资料
-
[1] https://www.zhihu.com/question/328891283/answer/717113611
-
[2] https://www.zhihu.com/question/48279880/answer/838063090
-
[3] https://distill.pub/2016/deconv-checkerboard/
-
[4] https://blog.csdn.net/shwan_ma/article/details/78440394
-
[5] https://arxiv.org/pdf/1603.07285.pdf
-
[6] https://github.com/alibaba/MNN
-
[7] https://github.com/apache/incubator-mxnet
-
[8] https://www.zhihu.com/question/337513515/answer/768632471
公众号近期荐读:
-
GAN整整6年了!是时候要来捋捋了!
-
新手指南综述 | GAN模型太多,不知道选哪儿个?
-
数百篇GAN论文已下载好!配一份生成对抗网络最新综述!
-
图卷积网络GCN的理解与介绍
-
【CapsulesNet的解析】了解一下胶囊网络?
-
结合GAN的零次学习(zero-shot learning)
-
GAN的图像修复:多样化补全
-
CVPR2020之MSG-GAN:简单有效的SOTA
-
CVPR2020之姿势变换GAN:图像里谁都会劈叉?
-
CVPR2020之多码先验GAN:预训练模型如何使用?
-
两幅图像!这样能训练好GAN做图像转换吗?
-
单图训GAN!如何改进SinGAN?
-
有点夸张、有点扭曲!速览这些GAN如何夸张漫画化人脸!
-
见微知细之超分辨率GAN!附70多篇论文下载!
-
天降斯雨,于我却无!GAN用于去雨如何?
-
脸部转正!GAN能否让侧颜杀手、小猪佩奇真容无处遁形?
-
容颜渐失!GAN来预测?
-
强数据所难!SSL(半监督学习)结合GAN如何?
-
弱水三千,只取你标!AL(主动学习)结合GAN如何?
-
异常检测,GAN如何gan ?
-
虚拟换衣!速览这几篇最新论文咋做的!
-
脸部妆容迁移!速览几篇用GAN来做的论文
-
【1】GAN在医学图像上的生成,今如何?
-
01-GAN公式简明原理之铁甲小宝篇
GAN&CV交流群,无论小白还是大佬,诚挚邀您加入!
一起讨论交流!长按备注【进群】加入:
更多分享、长按关注本公众号: