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

Posted

技术标签:

【中文标题】pytorch 中的自适应池化如何工作?【英文标题】:How does adaptive pooling in pytorch work? 【发布时间】:2019-05-19 08:55:04 【问题描述】:

自适应池是一个很棒的功能,但它是如何工作的呢?它似乎是以一种看似有图案但相当随意的方式插入焊盘或缩小/扩大内核大小。我能找到的 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 的输出。它是在改变内核的大小吗?

我是否遗漏了一些关于其工作方式的明显内容?

【问题讨论】:

【参考方案1】:

一般来说,池化会减少维度。如果你想增加维度,你可能想看看interpolation。

无论如何,让我们来谈谈一般的自适应池。可以看源码here。一些人声称自适应池与标准池相同,其步幅和内核大小是根据输入和输出大小计算的。具体使用以下参数:

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

这些是从池 formula 中逆向工作的。虽然它们确实产生所需大小的输出,但其输出不一定与自适应池的输出相同。这是一个测试sn-p:

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 以了解实际实施情况。

【讨论】:

【参考方案2】:

正如 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 和填充的非自适应池化层来重现自适应池化层的效果。 (下面的示例。)

最后,当输入大小不是输出大小的整数倍时,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]

【讨论】:

你知道padding=0的原因吗?

以上是关于pytorch 中的自适应池化如何工作?的主要内容,如果未能解决你的问题,请参考以下文章

如何从 vgg19 中移除自适应平均池层?

(pytorch复现)基于深度强化学习(CNN+dueling network/DQN/DDQN/D3QN)的自适应车间调度(JSP)

(pytorch复现)基于深度强化学习(CNN+dueling network/DQN/DDQN/D3QN)的自适应车间调度(JSP)

自适应池化最大值池化和均值池化效率的比较分析

(pytorch复现)基于深度强化学习(CNN+dueling network/DQN/DDQN/D3QN/PER)的自适应车间调度(JSP)

iPad的自适应布局