pytorch中的自适应池如何工作?

时间:2018-12-18 21:42:06

标签: pytorch

自适应池是一个很棒的功能,但是它如何工作?似乎是以似乎是but啪作响但相当武断的方式插入填充或缩小/扩展内核大小。我可以找到的pytorch文档比“在此处输入所需的输出大小”更具描述性。有谁知道它是如何工作的,或者可以指出它的解释之处?

在1x1x6张量(1,2,3,4,5,6)上的一些测试代码,自适应输出大小为8:

import torch
import torch.nn as nn

class TestNet(nn.Module):
    def __init__(self):
        super(TestNet, self).__init__()
        self.avgpool = nn.AdaptiveAvgPool1d(8)

    def forward(self,x):
        print(x)
        x = self.avgpool(x)
        print(x)
        return x

def test():
    x = torch.Tensor([[[1,2,3,4,5,6]]])
    net = TestNet()
    y = net(x)
    return y

test()

输出:

tensor([[[ 1.,  2.,  3.,  4.,  5.,  6.]]])
tensor([[[ 1.0000,  1.5000,  2.5000,  3.0000,  4.0000,  4.5000,  5.5000,
       6.0000]]])

如果它左右镜像(在(1,1,2,3,4,5,6,6)上操作),并且内核为2,则除以下位置以外的所有位置的输出4和5是有意义的,当然输出不是正确的大小。是否也在内部填充3和4?如果是这样,它将在(1,1,2,3,3,4,4,5,6,6)上运行,如果使用大小为2的内核,则会产生错误的输出大小,并且还会丢失3.5输出。它会改变内核的大小吗?

我是否缺少一些明显的工作方式?

2 个答案:

答案 0 :(得分:9)

正如hkchengrex的答案所指出的那样,PyTorch文档没有解释自适应池化层使用什么规则来确定池化内核的大小和位置。 (实际上,有一个fixme in the PyTorch code表示文档需要改进。)

但是,内核大小和位置is implemented by this cpp function以及关键逻辑的计算实际上是在对函数start_indexend_index的调用中,这些函数定义了变量的位置和偏移量。内核。

我相信这段Python代码会重新实现该代码,并演示如何计算内核:

from typing import List
import math
def kernels(ind,outd) -> List:
    """Returns a List [(kernel_offset_start,kernel_length)] defining all the pooling kernels for a 1-D adaptive pooling layer that takes an input of dimension `ind` and yields an output of dimension `outd`"""
    def start_index(a,b,c):
        return math.floor((float(a) * float(c)) / b)
    def end_index(a,b,c):
        return math.ceil((float(a + 1) * float(c)) / b)
    results = []
    for ow in range(outd):
        start = start_index(ow,outd,ind)
        end = end_index(ow,outd,ind)
        sz = end - start
        results.append((start,sz))
    return results

def kernel_indexes(ind,out) -> List:
    """Returns a List [[*ind]] containing the indexes of the pooling kernels"""
    startsLengths = kernels(ind,out)
    return [list(range(start,start+length)) for (start,length) in startsLengths]

这是需要注意的重点。

首先,输入尺寸(ind)是否是输出尺寸(outd)的整数倍至关重要。

第二,在这种情况下,自适应层的内核大小相等且不重叠,并且正是根据以下规则定义内核和步幅所产生的结果:

stride = ind // outd
kernel_size = ind - (outd-1)*stride
padding = 0

换句话说,在这种情况下,可以通过使用以合适的步幅,kernel_size和padding定义的非自适应池化层来复制自适应池化层的效果。 (下面的示例。)

最后,当输入大小不是输出大小的整数倍时,PyTorch的自适应池化规则将生成重叠且大小可变的内核。

由于非自适应池化API不允许使用大小可变的内核,因此在我看来,没有办法通过将合适的值输入非自适应池来重现自适应池的效果层

这是显示两种情况的示例。这个辅助函数使我们可以比较自适应平均池化层和使用固定步幅和内核的普通平均池化层的情况:

import torch
import torch.nn as nn

def compare1DAdaptivity(ind,outd,inputpattern):
    c = 1
    padding = 0

    input = torch.Tensor(inputpattern).view(1,c,ind)

    stride = ind // outd
    kernel_size = (ind - (outd-1)*stride)
    avg_pool = nn.AvgPool1d(stride=stride,kernel_size=kernel_size,padding=padding)
    avg_out = avg_pool(input)

    adap_avg_pool = torch.nn.AdaptiveAvgPool1d(outd)
    adap_avg_out = adap_avg_pool(input)
    
    try:
        equal_output = torch.allclose(avg_out,adap_avg_out)
    except:
        equal_output = False

    print("input.shape: {}".format(input.shape))
    print("in_dims: {}".format(ind))
    print("out_dims: {}".format(outd))
    print("")
    print("AAL strides: {}".format(stride))
    print("AAL kernel_sizes: {}".format(kernel_size))
    print("AAL pad: {}".format(padding))
    print("")
    print("outputs equal: {}".format(equal_output))
    print("")
    print("AAL input -> output: {} -> {}".format(input,avg_out))
    print("adap input -> output: {} -> {}".format(input,adap_avg_out))
    return equal_output

因此,举第一个例子为例,其中输入维数是输出维数的倍数,我们可以从6变为3。我们可以看到近似自适应层和真实自适应层给出相同的值输出:

compare1DAdaptivity(6,3,[1,0,0,0,0]) # => Tue
AAL input -> output: tensor([[[1., 0., 0., 0., 0., 0.]]]) -> tensor([[[0.5000, 0.0000, 0.0000]]])
adap input -> output: tensor([[[1., 0., 0., 0., 0., 0.]]]) -> tensor([[[0.5000, 0.0000, 0.0000]]])

但是,如果我们从5转到3,则此方法不再起作用。

compare1DAdaptivity(5,3,[1,0,0,0,0]) # => False
AAL input -> output: tensor([[[1., 0., 0., 0., 0.]]]) -> tensor([[[0.3333, 0.0000, 0.0000]]])
adap input -> output: tensor([[[1., 0., 0., 0., 0.]]]) -> tensor([[[0.5000, 0.0000, 0.0000]]])

但是我们可以通过手动计算索引来再现自适应层的结果:

t = [1,0,0,0,0]; [sum( [t[x] for x in xs] ) / len(xs) for xs in kernel_indexes(5,3)]
# => [0.5,0.0,0.0]

答案 1 :(得分:0)

通常,合并可减小尺寸。如果要增加尺寸,则可能需要查看interpolation

无论如何,让我们大致讨论自适应池。您可以查看源代码here。有人声称自适应池与标准池相同,后者的步幅和内核大小由输入和输出大小计算得出。具体来说,使用以下参数:

  1. 步幅= (input_size//output_size)
  2. 内核大小= input_size - (output_size-1)*stride
  3. 填充= 0

这些是从池formula中反向进行的。尽管它们 DO 产生所需大小的输出,但其输出不一定与自适应池的输出相同。这是一个测试代码段:

import torch
import torch.nn as nn

in_length = 5
out_length = 3

x = torch.arange(0, in_length).view(1, 1, -1).float()
print(x)

stride = (in_length//out_length)
avg_pool = nn.AvgPool1d(
        stride=stride,
        kernel_size=(in_length-(out_length-1)*stride),
        padding=0,
    )
adaptive_pool = nn.AdaptiveAvgPool1d(out_length)

print(avg_pool.stride, avg_pool.kernel_size)

y_avg = avg_pool(x)
y_ada = adaptive_pool(x)

print(y_avg)
print(y_ada)

输出:

tensor([[[0., 1., 2., 3., 4.]]])
(1,) (3,)
tensor([[[1., 2., 3.]]])
tensor([[[0.5000, 2.0000, 3.5000]]])
Error:  1.0

来自元素(0、1、2),(1、2、3)和(2、3、4)的平均池。

来自元素(0、1),(1、2、3)和(3、4)的自适应池。 (稍微更改一下代码,以查看它不是仅从(2)开始合并)

  • 您可以告诉自适应池尝试在池中减少重叠
  • 可以使用count_include_pad=True填充来缓解这种差异,但是总的来说,我认为对于2D或更高的所有输入/输出尺寸,它们不可能完全相同。我可以想象对左/右使用不同的填充。目前,池层不支持此功能。
  • 从实用的角度来看,应该没有多大关系。
  • 检查code的实际实现。