pytorch手动实现滑动窗口操作,论fold和unfold函数的使用

在卷积网络中,经常会需要用到卷积核滑动窗口的操作,如下图所示。这个操作在大多数的深度学习框架中,都被封装的很好,以至于我们并不需要显式地调用便可以实现卷积网络的这个操作。
conv_slice_kernel
但是,大部分深度学习框架也是提供了显式地进行滑动窗口操作的API的,在pytorch中就是unfoldfold。接下来我们来探讨下这两个函数的使用。


pytorch中,和unfold有关的有:torch.nn.Unfold, torch.nn.functional.unfold,tensor.unfold,其中我们讨论的是第一个,也就是作为网络层的Unfold

torch.nn.Unfold按照官方的说法,就是从一个批次的输入样本中,提取出滑动的局部区域块,也即是实现所谓局部连接的滑动窗口操作。该类的构造器的参数有:

torch.nn.Unfold(kernel_size, dilation=1, padding=0, stride=1)

不难发现其参数和卷积nn.Conv2d有着很多相似的地方,首先都需要指定卷积核的尺寸,腐蚀大小,填充大小以及步进大小等,实际上,完全可以通过扩展Unfold实现Conv操作。(conv = unfold + matmul + fold

我们来看下unfold的输入和输出,其输入形状如: ( N , C , ) (N,C,*) ,其中 N N batch_size C C 是通道数, * 是空间上的拉直的结果,比如说是图片如 ( 300 , 400 ) (300,400) ,则这里的 = 300 × 400 = 120000 * = 300 \times 400 = 120000 。到这里为止都很好理解,我们看下其输出。

其输出形状如: ( N , C × ( k e r n e l _ s i z e ) , L ) (N, C \times \prod(\mathrm{kernel\_size}) , L) ,其中 L L 为根据设定的 k e r n e l _ s i z e \mathrm{kernel\_size} 进行输入数据的裁剪后,所输出的区块的数量,比如说,现有输入 ( 10 , 12 ) (10,12) ,若卷积核尺寸为 ( 4 , 5 ) (4,5) ,那么就会有 ( 10 4 + 1 ) × ( 12 5 + 1 ) = 56 (10-4+1) \times (12-5+1)=56 个区块,每个区块的大小为 C × k e r n e l _ s i z e [ 0 ] × k e r n e l _ s i z e [ 1 ] . . . C \times \mathrm{kernel\_size}[0] \times \mathrm{kernel\_size}[1] ... , 将其拉直变为了输出形状的第二维。

给个代码实例:

inp = torch.randn(1, 3, 10, 12)
inp_unf = torch.nn.functional.unfold(inp, (4, 5)) 
# 这里的unfold是函数,而Unfold是层,使用差不多
print(inp_unf.shape)

输出为:torch.Size([1, 60, 56])

注意到到目前为止只是对原输入进行了一种特殊的reshape操作而已, 并没有引入任何可学习的参数。
接下来如果是为了实现卷积操作,则还需要引入参数,主要是对每个block的拉直结果进行加权,如果同一个图片的每个block的参数都是相同的,那么称为参数共享,就是标准的卷积层;如果每个block的参数都不一样,那么就不是参数共享的,此时一般称为局部连接层(Local connected layer)。接下来以参数共享的例子作为示例:

# Convolution is equivalent with Unfold + Matrix Multiplication + Fold (or view to output shape)
inp = torch.randn(1, 3, 10, 12)
w = torch.randn(2, 3, 4, 5)
inp_unf = torch.nn.functional.unfold(inp, (4, 5))
out_unf = inp_unf.transpose(1, 2).matmul(w.view(w.size(0), -1).t()).transpose(1, 2)
out = torch.nn.functional.fold(out_unf, (7, 8), (1, 1))
# or equivalently (and avoiding a copy),
# out = out_unf.view(1, 2, 7, 8)
(torch.nn.functional.conv2d(inp, w) - out).abs().max()
tensor(1.9073e-06)

我们看到最后的根据间接实现的卷积操作和自带的卷积操作的相减结果最大不超过1e-6,可以视为是计算误差,因此是等价的。

这里我们刚才的例子中涉及到了fold,这个操作和unfold相反,是把一系列的滑动区块拼接成一个张量。其参数列表为:

torch.nn.Fold(output_size, kernel_size, dilation=1, padding=0, stride=1)

其中需要的输入形状为: ( N , C × ( k e r n e l _ s i z e ) , L ) (N, C \times \prod(\mathrm{kernel\_size}) , L) ,和之前的输出形状一样,其输出形状为: ( N , C , o u t p u t _ s i z e [ 0 ] , o u t p u t _ s i z e [ 1 ] . . . ) (N, C, \mathrm{output\_size[0]}, \mathrm{output\_size[1]} ...)

其中,不管是Unfold还是fold,其参数中的paddingdilation都是在进行reshape之前在原数据上操作的。

Q&A

  1. 这个函数在tensorflow中对应是tf.image.extract_image_patches,即是从图片中取块,后续要自行进行卷积操作,也就是手动实现了滑动窗口的操作。[2]
tf.image.extract_image_patches(
    images,
    ksizes=None,
    strides=None,
    rates=None,
    padding=None,
    name=None,
    sizes=None
)

Reference

[1]. https://stackoverflow.com/questions/53972159/how-does-pytorchs-fold-and-unfold-work
[2]. https://www.tensorflow.org/api_docs/python/tf/image/extract_image_patches

发布了111 篇原创文章 · 获赞 206 · 访问量 28万+

猜你喜欢

转载自blog.csdn.net/LoseInVain/article/details/88139435