Pytorch: [Solicitação de recurso] Implementar "mesmo" preenchimento para operações de convolução?

Criado em 25 nov. 2017  ·  59Comentários  ·  Fonte: pytorch/pytorch

A implementação seria fácil, mas poderia ajudar muitas pessoas que sofriam com a dor de cabeça de calcular a quantidade de preenchimento necessária.

cc @ezyang @gchanan @ zou3519 @albanD @mruberry

enhancement high priority convolution nn triaged

Comentários muito úteis

Existe algum plano de implementar uma API semelhante em Pytorch em um futuro próximo? Pessoas com experiência em tensorflow / keras certamente irão gostar.

Todos 59 comentários

Parece que vale a pena fazer isso. Qual é a interface que você está propondo? como nn.Conv2d(..., padding="same") ?

Observe que, se você estiver procurando pelo mesmo comportamento do TensorFlow, a implementação não será tão simples, porque o número de pixels a adicionar depende do tamanho da entrada. Consulte https://github.com/caffe2/caffe2/blob/master/caffe2/proto/caffe2_legacy.proto para referência

Obrigado por indicar o problema e a referência.
Para resolver o problema apontado por
Primeiro, como @soutmith mencionou, a primeira interface seria como nn.Conv*d(..., padding="same") , calculando o preenchimento a cada forward() chamada.
No entanto, seria uma forma ineficiente quando a forma de entrada fosse conhecida na fase de inicialização. Portanto, sugiro uma interface como nn.CalcPadConv*d(<almost same parameters as Conv*d>) . Usando-o, um usuário pode calcular o preenchimento usando largura e altura conhecidas na inicialização e passar a saída (a forma do preenchimento) para o parâmetro de preenchimento de nn.Conv2d(...)
Não tenho certeza se a segunda proposta poderia ser uma otimização prematura.
Como você pensa sobre isso? Existe alguma ideia de um nome melhor?

Acho que a maior fonte de ineficiência virá do fato de que precisaremos adicionar uma camada F.pad antes de cada outra convolução que requer o caso padding=same (porque a quantidade de preenchimento pode não ser o mesmo nos lados esquerdo e direito), veja por exemplo como o TensorFlow deve lidar com isso no caso cudnn . Isso significa que nn.CalcPadConv*d seria normalmente tão caro quanto nn.Conv*d(..., padding="same") .

Isso poderia ser mais eficiente se suportássemos diferentes preenchimentos para cada lado da convolução (como no Caffe2, então à esquerda, à direita, em cima, em baixo), mas o cudnn ainda não suporta isso, então precisaríamos de preenchimento extra nesses casos .

Além disso, acho que se adicionarmos padding="same" a nn.Conv*d , provavelmente deveríamos fazer o mesmo para nn.*Pool*d , certo?

Acho que o que me incomoda um pouco é que os usuários podem esperar que o comportamento de padding=same seja equivalente ao TF, mas eles podem não estar esperando uma queda de desempenho.

O que você acha?

Por que isso seria ineficiente? não poderíamos simplesmente calcular o enchimento a cada passo à frente? o custo deve ser mínimo, portanto, não há necessidade de otimizá-lo. Talvez eu não entenda totalmente a semântica, mas não consigo ver por que F.pad seria necessário.

tornar o preenchimento dependente do tamanho da entrada é muito ruim. Acabamos de ter uma discussão interna sobre isso, com @Yangqing descrevendo por que isso é uma má ideia por uma série de razões de serialização e eficiência.

@fmassa , o que eu pretendia era calcular a forma de preenchimento "constante" em __init__() usando nn.CalcPadConv*d() . Como você disse, essa maneira não funcionará apenas quando o preenchimento calculado for estranho. Portanto, é necessário que a camada F.pad seja adicionada ou o suporte de F.conv*d para preenchimentos estranhos deve ajudar.

EDIT: Então o que eu sugeri deveria ser uma função e colocado em, digamos, torch.nn.utils ou torch.utils.

Como resultado, o que sugiro é uma função de utilidade simples, como (pseudocódigo):

def calc_pad_conv1d(width, padding='same', check_symmetric=True, ... <params that conv1d has>):
    shape = <calculate padding>

    assert not check_symmetric or <shape is symmetric>, \
        'Calculated padding shape is asymmetric, which is not supported by conv1d. ' \ 
        'If you just want to get the value, consider using check_symmetric=False.'

    return shape


width = 100  # for example
padding = calc_pad_conv1d(width, ...)
m = nn.Conv1d(..., padding=padding)

Além disso, a função pode ser usada com F.pad em favor do usuário.

@ qbx2 talvez eu não entenda totalmente sua proposta, mas se quisermos replicar o comportamento do TensorFlow, não acho que isso seja suficiente.

Aqui está um trecho do que eu acho que imita o preenchimento do TensorFlow SAME (estou escrevendo na interface funcional, de modo que nn.Conv2d possa simplesmente chamar F.conv2d_same_padding ):

def conv2d_same_padding(input, weight, bias=None, stride=1, dilation=1, groups=1):
  input_rows = input.size(2)
  filter_rows = weight.size(2)
  effective_filter_size_rows = (filter_rows - 1) * dilation[0] + 1
  out_rows = (input_rows + stride[0] - 1) // stride[0]
  padding_needed =
          max(0, (out_rows - 1) * stride[0] + effective_filter_size_rows -
                  input_rows)
  padding_rows = max(0, (out_rows - 1) * stride[0] +
                        (filter_rows - 1) * dilation[0] + 1 - input_rows)
  rows_odd = (padding_rows % 2 != 0)
  # same for padding_cols

  if rows_odd or cols_odd:
    input = F.pad(input, [0, int(cols_odd), 0, int(rows_odd)])

  return F.conv2d(input, weight, bias, stride,
                  padding=(padding_rows // 2, padding_cols // 2),
                  dilation=dilation, groups=groups)

A maior parte foi copiado e colado do código TensorFlow aqui e aqui .

Como você pode ver, há muitas coisas ocultas acontecendo lá, e é por isso que acho que não vale a pena adicionar padding='same' . E eu acho que não replicar o comportamento SAME no TensorFlow também não é o ideal.

Pensamentos?

@fmassa Sim, você tem razão. Pode ser ineficiente calcular o preenchimento em cada forward() .

No entanto, minha proposta NÃO é calcular o preenchimento a cada forward() chamada. Um pesquisador (desenvolvedor) pode esperar que os tamanhos das imagens cheguem a nn.Conv2d antes do tempo de execução. E se ele / ela deseja o 'mesmo' preenchimento, ele pode usar a função para calcular o preenchimento necessário para imitar o 'MESMO'.

Por exemplo, imagine o caso de um pesquisador ter imagens com 200x200, 300x300, 400x400. Em seguida, ele pode calcular os preenchimentos para os três casos na fase de inicialização e apenas passar as imagens para F.pad() com o preenchimento correspondente. Ou ele / ela apenas altera o campo de preenchimento de nn.Conv2d antes da chamada forward() . Consulte isto:

>>> import torch
>>> import torch.nn as nn
>>> from torch.autograd import Variable
>>> m = nn.Conv2d(1,1,1)
>>> m(Variable(torch.randn(1,1,2,2))).shape
torch.Size([1, 1, 2, 2])
>>> m.padding = (1, 1)
>>> m(Variable(torch.randn(1,1,2,2))).shape
torch.Size([1, 1, 4, 4])

Sim, eu só quero adicionar a "função de utilidade de cálculo de preenchimento" no núcleo do pytorch.

Quando o pesquisador deseja um preenchimento dependente de cada tamanho de imagem de entrada, ele pode combinar a função com F.pad() antes de passar a imagem para nn.Conv2d . Quero deixar o autor do código decidir se preenche as entradas em cada chamada de forward() ou não.

Existe algum plano de implementar uma API semelhante em Pytorch em um futuro próximo? Pessoas com experiência em tensorflow / keras certamente irão gostar.

Portanto, uma estratégia básica de cálculo de preenchimento (que não fornece os mesmos resultados do TensorFlow, mas as formas são semelhantes) é necessária

def _get_padding(padding_type, kernel_size):
    assert padding_type in ['SAME', 'VALID']
    if padding_type == 'SAME':
        return tuple((k - 1) // 2 for k in kernel_size))
    return tuple(0 for _ in kernel_size)

É isso que você tem em mente @ im9uri ?

É parecido com o que eu tinha em mente, mas como você mencionou anteriormente, o cálculo fica complicado com o passo e a dilatação.

Além disso, ter tal api em outras operações de convolução, como ConvTranspose2d, seria ótimo.

Acho que todos os "operadores de janela deslizante" devem oferecer suporte ao preenchimento assimétrico.

Sobre o "mesmo" argumento ...
@soumith Você pode explicar por que fazer preenchimento dependendo do tamanho de entrada é ruim, por favor?
Se isso for um problema, de qualquer maneira, uma solução pragmática poderia ser exigir stride == 1 ao usar "mesmo". Para stride == 1 , o preenchimento não depende do tamanho da entrada e pode ser calculado uma única vez. O construtor deve levantar ValueError se o usuário tentar usar padding='same' com stride > 1 .

Eu sei, não é a solução mais limpa, mas a restrição parece razoável o suficiente para mim, visto que:

  1. a semântica original do rótulo "mesmo" foi introduzida para convoluções não strided e foi: a saída tem o _mesmo_ tamanho da entrada; é claro, isso não é verdade no tensorflow para stride > 1 e isso torna o uso da palavra "mesmo" um pouco enganador IMO;
  2. cobriria 99% dos casos que se deseja usar "mesmo"; Eu mal posso imaginar um caso em que alguém realmente precise do comportamento de tensorflow para stride > 1 , enquanto se dermos ao "mesmo" sua semântica original, bem, é claro que não faz sentido usar uma convolução strided se você quiser que a saída tenha o mesmo tamanho da entrada.

A documentação do

def _get_padding(size, kernel_size, stride, dilation):
    padding = ((size - 1) * (stride - 1) + dilation * (kernel_size - 1)) //2
    return padding

Uma vez que o mesmo preenchimento significa preenchimento = (kernel_size - stride) // 2, e se preenchimento = "mesmo" for introduzido de forma que, quando escrito, leia automaticamente o tamanho do kernel e o passo (como também é mencionado em nn.Conv2d) e aplique o preenchimento automaticamente em conformidade

Aqui está uma camada Conv2d muito simples com preenchimento same para referência. Suporta apenas grãos quadrados e passada = 1, dilatação = 1, grupos = 1.

class Conv2dSame(torch.nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size, bias=True, padding_layer=torch.nn.ReflectionPad2d):
        super().__init__()
        ka = kernel_size // 2
        kb = ka - 1 if kernel_size % 2 == 0 else ka
        self.net = torch.nn.Sequential(
            padding_layer((ka,kb,ka,kb)),
            torch.nn.Conv2d(in_channels, out_channels, kernel_size, bias=bias)
        )
    def forward(self, x):
        return self.net(x)

c = Conv2dSame(1,3,5)
print(c(torch.rand((16,1,10,10))).shape)

# torch.Size([16, 3, 10, 10])

Se isso ainda está sendo avaliado para ser adicionado ao PyTorch, em relação às compensações entre complexidade / ineficiência e facilidade de uso para desenvolvedores:

No caminho para a postagem do blog 1.0 , ele afirma:

O objetivo central do PyTorch é fornecer uma grande plataforma para pesquisa e hackeabilidade. Portanto, embora adicionemos todas essas otimizações [uso de produção], temos trabalhado com uma restrição de design rígida para nunca trocá-las pela usabilidade.

Curiosamente, venho de uma experiência de uso de Keras, bem como as APIs tf.layers / estimator originais. Todos têm suporte para preenchimento de same . Atualmente, estou reimplementando um convnet que escrevi originalmente em TF com PyTorch, e o fato de que eu mesmo tive que construir a aritmética para preenchimento de zero me custou cerca de meio dia.

Se o "objetivo central" realmente estiver focado na usabilidade, então eu argumentaria que mesmo se houver um impacto na eficiência do cálculo de preenchimento zero em cada passe para frente (como mencionado acima), o tempo economizado em termos de eficiência do desenvolvedor e facilidade de manutenção ( por exemplo, não ter que escrever código personalizado para calcular o preenchimento de zero) pode valer a pena. Pensamentos?

Eu usaria este recurso

Não faz sentido para mim por que uma API opcional de padding=SAME ser oferecida? Se alguém estiver disposto a incorrer no custo adicional de enchimento, deixe-o fazê-lo. Para muitos pesquisadores, a prototipagem rápida é um requisito.

Sim, se alguém puder adicionar e aprovar isso, seria ótimo.

Definitivamente, adicione isso, Conner quer.

O pytorch o suporta agora? Ele pode usar a mesma operação como primeiro no VGG, definir padding = (kernel_size-1) / 2?
A rede VGG pode fazer com que o tamanho da saída não mude no primeiro grupo. Então você pode usar o stride para redimensionar o mapa de recursos, parece certo?

Aqui está um exemplo para chamar padding mesmo conv2d de deepfakes:

# modify con2d function to use same padding
# code referd to <strong i="6">@famssa</strong> in 'https://github.com/pytorch/pytorch/issues/3867'
# and tensorflow source code

import torch.utils.data
from torch.nn import functional as F

import math
import torch
from torch.nn.parameter import Parameter
from torch.nn.functional import pad
from torch.nn.modules import Module
from torch.nn.modules.utils import _single, _pair, _triple


class _ConvNd(Module):

    def __init__(self, in_channels, out_channels, kernel_size, stride,
                 padding, dilation, transposed, output_padding, groups, bias):
        super(_ConvNd, self).__init__()
        if in_channels % groups != 0:
            raise ValueError('in_channels must be divisible by groups')
        if out_channels % groups != 0:
            raise ValueError('out_channels must be divisible by groups')
        self.in_channels = in_channels
        self.out_channels = out_channels
        self.kernel_size = kernel_size
        self.stride = stride
        self.padding = padding
        self.dilation = dilation
        self.transposed = transposed
        self.output_padding = output_padding
        self.groups = groups
        if transposed:
            self.weight = Parameter(torch.Tensor(
                in_channels, out_channels // groups, *kernel_size))
        else:
            self.weight = Parameter(torch.Tensor(
                out_channels, in_channels // groups, *kernel_size))
        if bias:
            self.bias = Parameter(torch.Tensor(out_channels))
        else:
            self.register_parameter('bias', None)
        self.reset_parameters()

    def reset_parameters(self):
        n = self.in_channels
        for k in self.kernel_size:
            n *= k
        stdv = 1. / math.sqrt(n)
        self.weight.data.uniform_(-stdv, stdv)
        if self.bias is not None:
            self.bias.data.uniform_(-stdv, stdv)

    def __repr__(self):
        s = ('{name}({in_channels}, {out_channels}, kernel_size={kernel_size}'
             ', stride={stride}')
        if self.padding != (0,) * len(self.padding):
            s += ', padding={padding}'
        if self.dilation != (1,) * len(self.dilation):
            s += ', dilation={dilation}'
        if self.output_padding != (0,) * len(self.output_padding):
            s += ', output_padding={output_padding}'
        if self.groups != 1:
            s += ', groups={groups}'
        if self.bias is None:
            s += ', bias=False'
        s += ')'
        return s.format(name=self.__class__.__name__, **self.__dict__)


class Conv2d(_ConvNd):

    def __init__(self, in_channels, out_channels, kernel_size, stride=1,
                 padding=0, dilation=1, groups=1, bias=True):
        kernel_size = _pair(kernel_size)
        stride = _pair(stride)
        padding = _pair(padding)
        dilation = _pair(dilation)
        super(Conv2d, self).__init__(
            in_channels, out_channels, kernel_size, stride, padding, dilation,
            False, _pair(0), groups, bias)

    def forward(self, input):
        return conv2d_same_padding(input, self.weight, self.bias, self.stride,
                        self.padding, self.dilation, self.groups)


# custom con2d, because pytorch don't have "padding='same'" option.
def conv2d_same_padding(input, weight, bias=None, stride=1, padding=1, dilation=1, groups=1):

    input_rows = input.size(2)
    filter_rows = weight.size(2)
    effective_filter_size_rows = (filter_rows - 1) * dilation[0] + 1
    out_rows = (input_rows + stride[0] - 1) // stride[0]
    padding_needed = max(0, (out_rows - 1) * stride[0] + effective_filter_size_rows -
                  input_rows)
    padding_rows = max(0, (out_rows - 1) * stride[0] +
                        (filter_rows - 1) * dilation[0] + 1 - input_rows)
    rows_odd = (padding_rows % 2 != 0)
    padding_cols = max(0, (out_rows - 1) * stride[0] +
                        (filter_rows - 1) * dilation[0] + 1 - input_rows)
    cols_odd = (padding_rows % 2 != 0)

    if rows_odd or cols_odd:
        input = pad(input, [0, int(cols_odd), 0, int(rows_odd)])

    return F.conv2d(input, weight, bias, stride,
                  padding=(padding_rows // 2, padding_cols // 2),
                  dilation=dilation, groups=groups)

Passei por aqui para dizer que também aprecio muito isso. Atualmente portando um modelo simples do tensorflow e os cálculos estão demorando muito para eu descobrir ...

Parece que este tópico morreu. Dado o número de polegares levantados aqui, seria realmente ótimo adicionar esse recurso para uma prototipagem mais rápida.

Vou escrever uma proposta para isso e podemos encontrar alguém para implementá-la.
Estou comparando isso com o marco v1.1.

Obrigado, você é demais! Também preenchi uma solicitação de recurso separada para fazer o argumento de preenchimento aceitar 4 tuplas. Isso permitiria um preenchimento assimétrico e simétrico, que também é uma boa rota de baixo custo para chegar lá.

@soumith Seria bom ter um modo de preenchimento MESMO no pytorch.

@soumith Que tal usar uma interface de tipo de compilação?

model=torch.compile(model,input_shape=(3,224,224))

Fiz um Conv2D com o mesmo preenchimento que suporta dilatação e passadas, com base em como o TensorFlow faz o deles. Este calcula em tempo real, se você quiser pré-calcular, basta mover o preenchimento para init () e ter um parâmetro de tamanho de entrada.

import torch as tr
import math

class Conv2dSame(tr.nn.Module):

    def __init__(self, in_channels, out_channels, kernel_size, stride=1, dilation=1):
        super(Conv2dSame, self).__init__()
        self.F = kernel_size
        self.S = stride
        self.D = dilation
        self.layer = tr.nn.Conv2d(in_channels, out_channels, kernel_size, stride, dilation=dilation)

    def forward(self, x_in):
        N, C, H, W = x_in.shape
        H2 = math.ceil(H / self.S)
        W2 = math.ceil(W / self.S)
        Pr = (H2 - 1) * self.S + (self.F - 1) * self.D + 1 - H
        Pc = (W2 - 1) * self.S + (self.F - 1) * self.D + 1 - W
        x_pad = tr.nn.ZeroPad2d((Pr//2, Pr - Pr//2, Pc//2, Pc - Pc//2))(x_in)
        x_out = self.layer(x_pad)
        return x_out

Ex1:
Forma de entrada: (1, 3, 96, 96)
Filtros: 64
Tamanho: 9x9

Conv2dSame(3, 64, 9)

Forma acolchoada: (1, 3, 104, 104)
Forma de saída: (1, 64, 96, 96)

Ex2:
O mesmo que antes, mas com passo = 2

Conv2dSame(3, 64, 9, 2)

Forma acolchoada = (1, 3, 103, 103)
Forma de saída = (1, 64, 48, 48)

@jpatts Eu acredito que o cálculo da forma de saída está errado, deveria ser ceil (input_dimension / stride). A divisão inteira em python é a divisão de chão - seu código deve ter um resultado diferente de tensorflow para, por exemplo, h=w=28, stride=3, kernel_size=1 .

Aqui está uma variante que faz o cálculo de antemão:

def pad_same(in_dim, ks, stride, dilation=1):
    """
    Refernces:
          https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/framework/common_shape_fns.h
          https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/framework/common_shape_fns.cc#L21
    """
    assert stride > 0
    assert dilation >= 1
    effective_ks = (ks - 1) * dilation + 1
    out_dim = (in_dim + stride - 1) // stride
    p = max(0, (out_dim - 1) * stride + effective_ks - in_dim)

    padding_before = p // 2
    padding_after = p - padding_before
    return padding_before, padding_after

Se a dimensão de entrada for conhecida e não calculada em tempo real, isso pode ser usado, por exemplo:

# Pass this to nn.Sequential
def conv2d_samepad(in_dim, in_ch, out_ch, ks, stride, dilation=1, bias=True):
    pad_before, pad_after = pad_same(in_dim, ks, stride, dilation)
    if pad_before == pad_after:
        return [nn.Conv2d(in_ch, out_ch, ks, stride, pad_after, dilation, bias=bias)]
    else:
        return [nn.ZeroPad2d((pad_before, pad_after, pad_before, pad_after)),
                nn.Conv2d(in_ch, out_ch, ks, stride, 0, dilation, bias=bias)]

No entanto, neste caso, alguma contabilidade precisa ser feita para a dimensão de entrada (este é o problema central), portanto, se você usar o acima, poderá ser útil:

def conv_outdim(in_dim, padding, ks, stride, dilation):
    if isinstance(padding, int) or isinstance(padding, tuple):
        return conv_outdim_general(in_dim, padding, ks, stride, dilation)
    elif isinstance(padding, str):
        assert padding in ['same', 'valid']
        if padding == 'same':
            return conv_outdim_samepad(in_dim, stride)
        else:
            return conv_outdim_general(in_dim, 0, ks, stride, dilation)
    else:
        raise TypeError('Padding can be int/tuple or str=same/valid')


def conv_outdim_general(in_dim, padding, ks, stride, dilation=1):
    # See https://arxiv.org/pdf/1603.07285.pdf, eq (15)
    return ((in_dim + 2 * padding - ks - (ks - 1) * (dilation - 1)) // stride) + 1


def conv_outdim_samepad(in_dim, stride):
    return (in_dim + stride - 1) // stride

@mirceamironenco obrigado por apontar isso, fiz isso rápido e sujo e nunca verifiquei. Atualizado para usar teto

@harritaylor Concordo, esse recurso definitivamente simplificaria a transferência dos modelos Keras / TF para o PyTorch. De vez em quando, ainda uso cálculos "manuais" de tamanho de preenchimento para construir minhas camadas com o mesmo preenchimento.

@kylemcdonald

Aqui está uma camada Conv2d muito simples com preenchimento same para referência. Suporta apenas grãos quadrados e passada = 1, dilatação = 1, grupos = 1.

class Conv2dSame(torch.nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size, bias=True, padding_layer=torch.nn.ReflectionPad2d):
        super().__init__()
        ka = kernel_size // 2
        kb = ka - 1 if kernel_size % 2 == 0 else ka
        self.net = torch.nn.Sequential(
            padding_layer((ka,kb,ka,kb)),
            torch.nn.Conv2d(in_channels, out_channels, kernel_size, bias=bias)
        )
    def forward(self, x):
        return self.net(x)

c = Conv2dSame(1,3,5)
print(c(torch.rand((16,1,10,10))).shape)

# torch.Size([16, 3, 10, 10])

Deve ser kb = ka - 1 if kernel_size % 2 else ka ou não?

Isso também se aplica a Conv1d?

Talvez adicionar um novo método de preenchimento à classe ConvND seja uma escolha elegante e, ao sobrecarregar o método, o cronograma de preenchimento poderia ser facilmente estendido.

Provavelmente posso aceitar isso se @soumith alguma vez escreveu essa proposta ou se alguém resumir o que precisa ser feito. Houve muita discussão acima e não tenho certeza do que decidimos. Estamos calculando o preenchimento dependendo dos dados de entrada ou não, precisamos implementar padding="same" para o pool também, etc.?

Eu gostaria de adicionar preenchimento causal também. e adicione também a conv1d.
Eu parei de seguir os comentários em algum momento, mas acho que esse recurso é muito bem feito no Keras. você deve segui-lo exatamente.

@Chillee, aqui está:

Alcance

Devemos adicionar preenchimento às seguintes camadas:

  • Conv * d
  • MaxPool * d
  • AvgPool * d

Para o primeiro PR, vamos mantê-lo simples e apenas nos limitar a Conv * d.

Complexidade e desvantagens

A complexidade discutida acima é em torno da camada se tornar dinâmica por natureza, depois que uma opção de preenchimento same é escrita. Ou seja, vai desde os parâmetros da camada sendo estaticamente conhecidos, o que é ótimo para exportação de modelo (por exemplo, exportação ONNX), até os parâmetros da camada ser dinâmica. Nesse caso, o parâmetro dinâmico é padding .
Embora pareça bastante inofensivo, a não-estática torna-se muito importante em tempos de execução limitados, como tempos de execução de hardware móvel ou exótico, onde, por exemplo, você deseja fazer análise e otimização de forma estática.

A outra desvantagem prática é que este padding calculado dinamicamente nem sempre é simétrico, porque dependendo do tamanho / passo do kernel, fator de dilatação e tamanho de entrada, o preenchimento pode ter que ser assimétrico (ou seja, diferente quantidade de preenchimento no lado esquerdo vs direito). Isso significaria que você não pode usar kernels CuDNN, por exemplo.

Projeto

Atualmente, a assinatura do Conv2d é:

torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode='zeros')

Aqui, apoiamos padding como sendo int ou tuple de ints (ou seja, para cada dimensão de altura / largura).
Devemos suportar uma sobrecarga adicional para padding que receberia uma string, com o valor same .

O preenchimento same deve preencher input de tal forma antes de dar a convolução que o tamanho de output seja igual ao tamanho de input .

Detalhes de implementação

Quando 'same' é dado a padding , temos que calcular a quantidade de preenchimento esquerdo e direito necessário em cada dimensão.

Há dois casos a serem considerados após o preenchimento obrigatório L (esquerdo) e R (direito) ser calculado:

  • L == R: neste caso, é um preenchimento simétrico. Pode-se simplesmente chamar F.conv2d com um padding igual a L
  • L! = R: Neste caso, o preenchimento é assimétrico e tem implicações significativas de desempenho e memória. Fazemos o seguinte:

    • chamamos input_padded = F.pad(input, ...) e enviamos input_padded para F.conv2d .

    • lançamos um aviso para este caso (pelo menos para o lançamento inicial e podemos revisitar se o aviso for necessário) sobre a implicação de desempenho.

    • Não me lembro dos detalhes da formulação e onde entramos neste caso, mas se bem me lembro, pode ser tão simples como ter um kernel de tamanho uniforme. Se for esse o caso, o aviso pode ter uma solução fácil para o usuário.

Não é preciso dizer que precisa ser testado para funcionar também no caminho JIT

@Chilee para referência, aqui está uma implementação potencial para obter inspiração em https://github.com/mlperf/inference/blob/master/others/edge/object_detection/ssd_mobilenet/pytorch/utils.py#L40

Ele correspondeu à implementação do TF para as configurações que foi testado, mas o teste não foi exaustivo

@soumith Algumas perguntas rápidas:

  1. Há algum motivo para não implementarmos isso por meio de functional.conv2d ? O design que você escreveu parece sugerir que não deveria. Não há nada sobre padding = "mesmo" que pareça que deveria ser específico para camadas. (EDIT: Nvm, não percebi que o F.conv2d impl que eu estava olhando era o quantizado).
  2. Acho que o modo de preenchimento valid do Tensorflow é simplesmente equivalente ao nosso com padding=0 , certo?

Além disso, não parece que haverá uma solução fácil para o usuário lidar com o preenchimento assimétrico. A regra completa para determinar a quantidade de preenchimento que precisa ocorrer é
(ceil(x/stride) -1)*stride + (filter-1)*dilation + 1 - x ao longo de uma dimensão. Em particular, precisaremos fazer preenchimento assimétrico quando este não for um múltiplo de 2. Como um contra-exemplo à sua esperança de que isso só aconteça com filtros de tamanhos iguais, pegue input = 10, stride=3, filter=3, dilation=1 . Não vejo regras simples para resolver as situações em que isso pode acontecer.

Além disso, não seremos capazes de determinar estaticamente o preenchimento, exceto no caso em que stride=1 , como então ceil(x/stride) = x , e tivermos preenchimento igual a (filter-1)*dilation .

@Chillee sobre (1), sem motivo, não havia pensado nas implicações - de desempenho ou não.

(2) Sim.

Além disso, não seremos capazes de determinar estaticamente o preenchimento, exceto no caso em que passo = 1, como então ceil (x / passo) = x, e temos preenchimento igual a (filtro-1) * dilatação

Sim, mas stride = 1 é comum o suficiente e os benefícios do preenchimento estático são bons o suficiente para que possamos lidar com isso de maneira especial.

Sobre preenchimento assimétrico, oh bem ...

Não faz sentido para mim por que uma API opcional de padding=SAME ser oferecida? Se alguém estiver disposto a incorrer no custo adicional de enchimento, deixe-o fazê-lo. Para muitos pesquisadores, a prototipagem rápida é um requisito.

Sim,

Não faz sentido para mim por que uma API opcional de padding=SAME ser oferecida? Se alguém estiver disposto a incorrer no custo adicional de enchimento, deixe-o fazê-lo. Para muitos pesquisadores, a prototipagem rápida é um requisito.

Aceita! Fiquei preso nessa porra de “acolchoamento” por 4 horas.

Temos alguma atualização sobre a solução para este problema?

Uau e aqui eu pensei que Pytorch seria mais fácil do que Keras / Tensorflow 2.0 ...

@zwep, há um pouco mais de esforço para começar. Você tem que escrever seu loop trianing, o que pode ser irritante, e você tem que escrever camadas de forma mais explícita. Depois de fazer isso (uma vez), você pode avançar muito mais na melhoria real além disso.

Minha regra é usar Keras se for algo que você fez um milhão de vezes / super padrão.
use o pytorch sempre que houver pesquisa e desenvolvimento envolvidos.

aqui está meu código para conversões 1d preenchidas

importar tocha
da importação da tocha nn
importar numpy como np
import tocch.functional as F

class Conv1dSamePad(nn.Module):
    def __init__(self, in_channels, out_channels, filter_len, stride=1, **kwargs):
        super(Conv1dSamePad, self).__init__()
        self.filter_len = filter_len
        self.conv = nn.Conv1d(in_channels, out_channels, filter_len, padding=(self.filter_len // 2), stride=stride,
                              **kwargs)
        nn.init.xavier_uniform_(self.conv.weight)
        # nn.init.constant_(self.conv.bias, 1 / out_channels)

    def forward(self, x):
        if self.filter_len % 2 == 1:
            return self.conv(x)
        else:
            return self.conv(x)[:, :, :-1]


class Conv1dCausalPad(nn.Module):
    def __init__(self, in_channels, out_channels, filter_len, **kwargs):
        super(Conv1dCausalPad, self).__init__()
        self.filter_len = filter_len
        self.conv = nn.Conv1d(in_channels, out_channels, filter_len, **kwargs)
        nn.init.xavier_uniform_(self.conv.weight)

    def forward(self, x):
        padding = (self.filter_len - 1, 0)
        return self.conv(F.pad(x, padding))


class Conv1dPad(nn.Module):
    def __init__(self, in_channels, out_channels, filter_len, padding="same", groups=1):
        super(Conv1dPad, self).__init__()
        if padding not in ["same", "causal"]:
            raise Exception("invalid padding type %s" % padding)
        self.conv = Conv1dCausalPad(in_channels, out_channels, filter_len, groups=groups) \
            if padding == "causal" else Conv1dSamePad(in_channels, out_channels, filter_len, groups=groups)

    def forward(self, x):
        return self.conv(x)

@danFromTelAviv He man, obrigado pelo código. Manterá essa filosofia pytorch em mente!

É 2020. Ainda sem padding='same' em Pytorch?

Esta é uma maneira de obter o mesmo preenchimento funcionando para qualquer tamanho de kernel, passo e dilatação (até mesmo tamanhos de kernel também funcionam).

class Conv1dSame(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size, stride=1, dilation=1):
        super().__init__()
        self.cut_last_element = (kernel_size % 2 == 0 and stride == 1 and dilation % 2 == 1)
        self.padding = math.ceil((1 - stride + dilation * (kernel_size-1))/2)
        self.conv = nn.Conv1d(in_channels, out_channels, kernel_size, padding=self.padding, stride=stride, dilation=dilation)

    def forward(self, x):
        if self.cut_last_element:
            return self.conv(x)[:, :, :-1]
        else:
            return self.conv(x)

Eu quero o mesmo recurso de preenchimento em nn.Conv2d também.

BTW, além das questões de desempenho / serialização discutidas acima, há razões de correção / precisão sobre porque o modo de preenchimento "mesmo" dependente do tamanho no TF não é um bom padrão. Eu discuti em https://github.com/tensorflow/tensorflow/issues/18213 e mostrei que, na verdade, o próprio código do Google usa um modo de preenchimento "mesmo" independente de tamanho.

Parece que não há trabalho em andamento agora sobre esse problema, mas se houver, espero que seja uma solução independente do tamanho.

Olá, @ppwwyyxx Yuxin, obrigado pela resposta.
Acho que a implementação de @ McHughes288 é boa e gostaria de saber sua opinião sobre a implementação dele.

Aqui está minha solução para o SAME padding Conv1D (só funciona corretamente quando dilation==1 & groups==1 , mais complicado quando você considera dilatação e grupos):

import torch.nn.functional as F
from torch import nn

class Conv1dSamePadding(nn.Conv1d):
    """Represents the "Same" padding functionality from Tensorflow.
    NOTE: Only work correctly when dilation == 1, groups == 1 !!!
    """
    def forward(self, input):
        size, kernel, stride = input.size(-1), self.weight.size(
            2), self.stride[0]
        padding = kernel - stride - size % stride
        while padding < 0:
            padding += stride
        if padding != 0:
            # pad left by padding // 2, pad right by padding - padding // 2
            # in Tensorflow, one more padding value(default: 0) is on the right when needed
            input = F.pad(input, (padding // 2, padding - padding // 2))
        return F.conv1d(input=input,
                        weight=self.weight,
                        bias=self.bias,
                        stride=stride,
                        dilation=1,
                        groups=1)

@Chillee , você pretendia continuar trabalhando neste recurso? Vou cancelar sua atribuição agora para que possamos acompanhar melhor o andamento desse problema. Sinta-se à vontade para reatribuir se você ainda estiver trabalhando nisso.

depois de ler o código de @wizcheu , crio outra versão de conv1d com padding = 'same'

class Conv1dPaddingSame(nn.Module):
    '''pytorch version of padding=='same'
    ============== ATTENTION ================
    Only work when dilation == 1, groups == 1
    =========================================
    '''
    def __init__(self, in_channels, out_channels, kernel_size, stride):
        super(Conv1dPaddingSame, self).__init__()
        self.kernel_size = kernel_size
        self.stride = stride
        self.weight = nn.Parameter(torch.rand((out_channels, 
                                                 in_channels, kernel_size)))
        # nn.Conv1d default set bias=True,so create this param
        self.bias = nn.Parameter(torch.rand(out_channels))

    def forward(self, x):
        batch_size, num_channels, length = x.shape
        if length % self.stride == 0:
            out_length = length // self.stride
        else:
            out_length = length // self.stride + 1

        pad = math.ceil((out_length * self.stride + 
                         self.kernel_size - length - self.stride) / 2)
        out = F.conv1d(input=x, 
                       weight = self.weight,
                       stride = self.stride, 
                       bias = self.bias,
                       padding=pad)
        return out

Existe alguma atualização sobre isso?

alguma atualização ??

@ peterbell10 vinculou um rascunho de RP que você pode seguir.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

miguelvr picture miguelvr  ·  3Comentários

SeparateReality picture SeparateReality  ·  3Comentários

ikostrikov picture ikostrikov  ·  3Comentários

kdexd picture kdexd  ·  3Comentários

cdluminate picture cdluminate  ·  3Comentários