Pytorch: [Solicitud de función] ¿Implementar "el mismo" relleno para las operaciones de convolución?

Creado en 25 nov. 2017  ·  59Comentarios  ·  Fuente: pytorch/pytorch

La implementación sería fácil, pero podría ayudar a muchas personas que sufren el dolor de cabeza de calcular cuántos acolchados necesitan.

cc @ezyang @gchanan @ zou3519 @albanD @mruberry

enhancement high priority convolution nn triaged

Comentario más útil

¿Existe algún plan para implementar una API similar en Pytorch en un futuro próximo? Las personas que provienen de un trasfondo de tensorflow / keras sin duda lo apreciarán.

Todos 59 comentarios

Parece que vale la pena hacerlo. ¿Cuál es la interfaz que propones? como nn.Conv2d(..., padding="same") ?

Tenga en cuenta que si está buscando el mismo comportamiento de TensorFlow, la implementación no será tan sencilla, porque la cantidad de píxeles para agregar depende del tamaño de entrada. Consulte https://github.com/caffe2/caffe2/blob/master/caffe2/proto/caffe2_legacy.proto como referencia

Gracias por indicar el problema y la referencia.
Para resolver el problema planteado por @fmassa , propongo dos interfaces.
Primero, como mencionó @soutmith , la primera interfaz sería como nn.Conv*d(..., padding="same") , calculando el relleno cada llamada forward() .
Sin embargo, sería una forma ineficaz cuando se conoce la forma de entrada en la fase de inicialización. Por lo tanto, sugiero una interfaz como nn.CalcPadConv*d(<almost same parameters as Conv*d>) . Al usarlo, un usuario puede calcular el relleno utilizando el ancho y la altura conocidos en la inicialización, y pasar la salida (la forma del relleno) al parámetro de relleno de nn.Conv2d(...)
No estoy seguro de si la segunda propuesta podría ser una optimización prematura.
¿Qué opinas de estos? ¿Existe alguna idea de un nombre mejor?

Creo que la mayor fuente de ineficiencia vendrá del hecho de que tendremos que agregar una capa F.pad antes de cada otra convolución que requiera el caso padding=same (porque la cantidad de relleno podría no ser la misma en los lados izquierdo y derecho), vea, por ejemplo, cómo TensorFlow tiene que manejar eso en el caso cudnn . Eso significa que el nn.CalcPadConv*d normalmente sería tan caro como un nn.Conv*d(..., padding="same") .

Esto podría hacerse más eficiente si admitiéramos diferentes rellenos para cada lado de la convolución (como en Caffe2, por lo tanto, izquierda, derecha, arriba, abajo), pero cudnn todavía no lo admite, por lo que necesitaríamos el relleno adicional en esos casos. .

Además, creo que si sumamos padding="same" a nn.Conv*d , probablemente deberíamos hacer lo mismo con nn.*Pool*d , ¿verdad?

Creo que lo que me molesta un poco es que los usuarios pueden esperar que el comportamiento de padding=same sea ​​equivalente a TF, pero es posible que no esperen una caída en el rendimiento.

¿Qué piensas?

¿Por qué sería eso ineficiente? ¿No podríamos simplemente calcular el relleno en cada paso hacia adelante? el costo debe ser mínimo, por lo que no es necesario optimizarlo. Tal vez no entiendo completamente la semántica, pero no veo por qué se necesitaría F.pad .

hacer que el relleno dependa del tamaño de entrada es bastante malo. Acabamos de tener una discusión interna sobre esto, con @Yangq describiendo por qué es una mala idea por una variedad de razones de serialización y eficiencia.

@fmassa , lo que pretendía era calcular la forma de relleno "constante" en __init__() usando nn.CalcPadConv*d() . Como dijiste, esta forma no solo funcionará cuando el relleno calculado sea extraño. Por lo tanto, es necesario agregar la capa F.pad , o el soporte de F.conv*d para rellenos impares debería ayudar.

EDITAR: Entonces lo que sugerí debería ser una función y colocarla en, digamos, torch.nn.utils o torch.utils.

Como resultado, lo que sugiero es una función de utilidad simple, 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)

Además, la función podría usarse con F.pad a favor del usuario.

@ qbx2 tal vez no entiendo completamente su propuesta, pero si queremos replicar el comportamiento de TensorFlow, no creo que esto sea suficiente.

Aquí hay un fragmento de lo que creo que imita el relleno de TensorFlow SAME (lo estoy escribiendo en la interfaz funcional, de modo que nn.Conv2d pueda simplemente llamar a 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)

Se copió y pegó principalmente del código de TensorFlow aquí y aquí .

Como puede ver, hay muchas cosas ocultas allí, y por eso creo que no vale la pena agregar un padding='same' . Y creo que tampoco es ideal replicar el comportamiento de SAME en TensorFlow.

¿Pensamientos?

@fmassa Sí, tienes razón. Puede resultar ineficaz calcular el relleno en cada forward() .

Sin embargo, mi propuesta NO es calcular el relleno cada llamada forward() . Un investigador (desarrollador) puede esperar que los tamaños de las imágenes sean nn.Conv2d antes del tiempo de ejecución. Y si quiere el "mismo" relleno, puede usar la función para calcular el relleno necesario para imitar "MISMO".

Por ejemplo, piense en el caso de que un investigador tenga imágenes con 200x200, 300x300, 400x400. Luego puede calcular los paddings para los tres casos en la fase de inicialización y simplemente pasar las imágenes a F.pad() con el padding correspondiente. O simplemente cambia el campo de relleno de nn.Conv2d antes de la llamada forward() . Refiérase a esto:

>>> 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])

Sí, solo quiero agregar la "función de utilidad de cálculo de relleno" en el núcleo de Pytorch.

Cuando el investigador desea un relleno dependiente en cada tamaño de imagen de entrada, puede combinar la función con F.pad() antes de pasar la imagen a nn.Conv2d . Quiero dejar que el escritor de código decida si rellenar las entradas en cada llamada forward() o no.

¿Existe algún plan para implementar una API similar en Pytorch en un futuro próximo? Las personas que provienen de un trasfondo de tensorflow / keras sin duda lo apreciarán.

Entonces, una estrategia básica de cálculo de relleno (que no da los mismos resultados que TensorFlow, pero las formas son similares) es tener

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)

¿Es eso lo que tienes en mente @ im9uri ?

Es similar a lo que tenía en mente, pero como mencionaste anteriormente, el cálculo se complica con el paso y la dilatación.

También sería genial tener una API de este tipo en otras operaciones de convolución como ConvTranspose2d.

Creo que los "operadores de ventana deslizante" deberían admitir el relleno asimétrico.

Sobre el "mismo" argumento ...
@soumith ¿Puede explicar por qué hacer un relleno en función del tamaño de entrada es malo, por favor?
Si eso es un problema, de todos modos, una solución pragmática podría ser requerir stride == 1 cuando se usa "mismo". Para stride == 1 , el relleno no depende del tamaño de entrada y se puede calcular una sola vez. El constructor debería generar un ValueError si el usuario intenta usar padding='same' con stride > 1 .

Lo sé, no es la solución más limpia, pero la restricción me parece lo suficientemente razonable dado que:

  1. la semántica original de la etiqueta "igual" se introdujo para convoluciones no escalonadas y era: la salida tiene el _mismo_ tamaño de la entrada; por supuesto, esto no es cierto en tensorflow para stride > 1 y eso hace que el uso de la palabra "mismo" sea un poco engañoso en mi opinión;
  2. cubriría el 99% de los casos en los que se quiere usar "mismo"; Apenas puedo imaginar un caso en el que alguien realmente necesite el comportamiento de tensorflow por stride > 1 , mientras que si le damos a "mismo" su semántica original, bueno, por supuesto que no tiene ningún sentido usar una convolución escalonada si desea que la salida tenga el mismo tamaño que la entrada.

La documentación de

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

Dado que el mismo relleno significa padding = (kernel_size - stride) // 2, ¿qué pasa si padding = "mismo" se introduce de tal manera que cuando se escribe, lee automáticamente el tamaño del kernel y la zancada (como eso también se menciona en nn.Conv2d) y aplica el relleno? automáticamente en consecuencia

Aquí hay una capa Conv2d muy simple con relleno same como referencia. Solo admite granos cuadrados y zancada = 1, dilatación = 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])

Si esto aún se está evaluando para agregarlo a PyTorch, entonces con respecto a las compensaciones entre complejidad / ineficiencia y facilidad de uso para los desarrolladores:

En el camino a la publicación de blog 1.0 , dice:

El objetivo central de PyTorch es proporcionar una gran plataforma para la investigación y la piratería. Entonces, mientras agregamos todas estas optimizaciones [producción-uso], hemos estado trabajando con una restricción de diseño estricta para nunca intercambiarlas con la usabilidad.

Como anécdota, vengo de un historial de uso de Keras, así como las API de estimador tf.layers / originales. Todos tienen soporte para relleno de same . Actualmente estoy reimplementando un convnet que había escrito originalmente en TF con PyTorch, y el hecho de que tuve que construir en la aritmética para el relleno de ceros me ha costado alrededor de medio día.

Si el "objetivo central" realmente se centra en la usabilidad, entonces yo diría que incluso si hay un impacto de eficiencia en el cálculo del relleno cero en cada pasada hacia adelante (como se mencionó anteriormente), el tiempo ahorrado en términos de eficiencia y mantenibilidad del desarrollador ( por ejemplo, no tener que escribir código personalizado para calcular el relleno de ceros) puede valer la pena. ¿Pensamientos?

Usaría esta característica

No tiene sentido para mí ¿por qué no se puede ofrecer una API opcional de padding=SAME ? Si alguien está dispuesto a incurrir en el costo adicional del relleno, déjelo hacerlo. Para muchos investigadores, la creación rápida de prototipos es un requisito.

Sí, si alguien puede agregar y aprobar esto, sería genial.

Definitivamente agregue esto, Conner lo quiere.

¿Pytorch lo admite ahora? ¿Puede usar la misma operación que la primera en VGG, establecer padding = (kernel_size-1) / 2?
La red VGG puede hacer que el tamaño de salida no cambie en el primer grupo. Entonces puede usar Stride para cambiar el tamaño del mapa de características, ¿suena bien?

Aquí hay un ejemplo para llamar a padding same conv2d desde 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)

Pasando por aquí para decirle que también lo agradecería mucho. Actualmente transfiero un modelo simple desde tensorflow y los cálculos me están tomando mucho tiempo para darme cuenta ...

Parece que este hilo acaba de extinguirse. Dada la cantidad de aprobados aquí, sería genial agregar esta función para una creación de prototipos más rápida.

Escribiré una propuesta para esto y podemos encontrar a alguien que la implemente.
Estoy poniendo esto en contra del hito v1.1.

¡Gracias, eres increíble! También presenté una solicitud de función separada para hacer que el argumento de relleno acepte 4 tuplas. Esto permitiría un acolchado asimétrico y simétrico, que también es una buena ruta de bajo costo para llegar a la mitad del camino.

@soumith Sería bueno tener un modo de relleno MISMO en el pytorch.

@soumith ¿Qué tal usar una interfaz de tipo de compilación?

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

Hice un Conv2D con el mismo relleno que admite dilatación y zancadas, en función de cómo TensorFlow hace los suyos. Sin embargo, este lo calcula en tiempo real, si desea precalcularlo, simplemente mueva el relleno a init () y tenga un parámetro de tamaño 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

Ej1:
Forma de entrada: (1, 3, 96, 96)
Filtros: 64
Tamaño: 9x9

Conv2dSame(3, 64, 9)

Forma acolchada: (1, 3, 104, 104)
Forma de salida: (1, 64, 96, 96)

Ej2:
Igual que antes, pero con zancada = 2

Conv2dSame(3, 64, 9, 2)

Forma acolchada = (1, 3, 103, 103)
Forma de salida = (1, 64, 48, 48)

@jpatts Creo que el cálculo de la forma de salida es incorrecto, debería ser ceil (input_dimension / stride). La división de enteros en Python es la división de piso: su código debe tener un resultado diferente al de tensorflow para, por ejemplo, h=w=28, stride=3, kernel_size=1 .

Aquí hay una variante que hace el cálculo de antemano:

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

Si se conoce la dimensión de entrada y no se calcula sobre la marcha, se puede utilizar, por ejemplo:

# 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)]

Sin embargo, en este caso, es necesario llevar un registro de la dimensión de entrada (este es el problema principal), por lo que si usa lo anterior, puede resultarle ú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 gracias por señalar eso, hice esto rápido y sucio y nunca lo revisé. Actualizado para usar techo en su lugar

@harritaylor De acuerdo, esta característica definitivamente simplificaría la migración de modelos Keras / TF a PyTorch. De vez en cuando, sigo usando cálculos "manuales" de tamaño de relleno para construir mis capas con el mismo relleno.

@kylemcdonald

Aquí hay una capa Conv2d muy simple con relleno same como referencia. Solo admite granos cuadrados y zancada = 1, dilatación = 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])

¿Debería ser kb = ka - 1 if kernel_size % 2 else ka o no?

¿Esto también se aplicará a Conv1d?

Tal vez agregar un nuevo método de relleno a la clase ConvND sería una elección elegante y, al sobrecargar el método, el programa de relleno podría extenderse fácilmente.

Probablemente pueda aceptar esto si @soumith alguna vez escribió esa propuesta o si alguien resume lo que hay que hacer. Ha habido mucha discusión arriba y no estoy seguro de en qué nos hemos decidido. ¿Estamos calculando el relleno en función de los datos de entrada o no? ¿Necesitamos implementar padding="same" para el grupo, etc.?

También me gustaría agregar un relleno causal. y agregue también esto a conv1d.
Dejé de seguir los comentarios en algún momento, pero creo que esta función está muy bien hecha en keras. deberías seguirlo exactamente.

@Chillee aquí tienes:

Alcance

Deberíamos agregar relleno a las siguientes capas:

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

Para el primer PR, hagámoslo simple y limítese a Conv * d.

Complejidad y desventajas

La complejidad discutida anteriormente se basa en que la capa se vuelve de naturaleza dinámica, después de que se escribe una opción de relleno same . Es decir, va desde que los parámetros de la capa se conocen estáticamente, lo cual es excelente para la exportación de modelos (por ejemplo, la exportación ONNX), hasta que los parámetros de la capa son dinámicos. En este caso, el parámetro dinámico es padding .
Si bien esto parece bastante inofensivo, la falta de estática se vuelve bastante importante en tiempos de ejecución limitados, como tiempos de ejecución móviles o de hardware exótico, donde, por ejemplo, desea realizar análisis y optimización de formas estáticas.

La otra desventaja práctica es que este padding calculado dinámicamente ya no es siempre simétrico, porque dependiendo del tamaño / zancada del kernel, el factor de dilatación y el tamaño de entrada, el relleno podría tener que ser asimétrico (es decir, diferente cantidad de relleno en el lado izquierdo frente al derecho). Significaría que no puede usar núcleos CuDNN, por ejemplo.

Diseño

Actualmente, la firma de Conv2d es:

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

Aquí, admitimos que padding sea ​​un int o tuple de ints (es decir, para cada dimensión de alto / ancho).
Deberíamos admitir una sobrecarga adicional para padding que tomaría una cadena, con valor same .

El relleno same debe rellenar el input de tal manera antes de darle a la convolución que el tamaño output sea ​​el mismo que el tamaño input .

Detalles de implementacion

Cuando se da 'same' a padding , tenemos que calcular la cantidad de relleno izquierdo y derecho necesario en cada dimensión.

Hay dos casos a considerar después de que se calcule el relleno requerido L (izquierda) y R (derecha):

  • L == R: en este caso es un acolchado simétrico. Uno puede simplemente llamar F.conv2d con un padding igual a L
  • L! = R: En este caso, el relleno es asimétrico y tiene implicaciones significativas en el rendimiento y la memoria. Hacemos lo siguiente:

    • llamamos input_padded = F.pad(input, ...) y enviamos el input_padded al F.conv2d .

    • lanzamos una advertencia para este caso (al menos para la versión inicial, y podemos volver a visitarla si la advertencia es necesaria) sobre la implicación del rendimiento.

    • No recuerdo los detalles de la formulación y dónde entramos en este caso, pero si lo recuerdo, podría ser tan simple como tener un kernel de tamaño uniforme. Si ese es el caso, la advertencia puede tener una solución fácil para el usuario.

No hace falta decir que debe probarse para que también funcione en la ruta JIT

@Chilee como referencia, aquí hay una implementación potencial para inspirarse en https://github.com/mlperf/inference/blob/master/others/edge/object_detection/ssd_mobilenet/pytorch/utils.py#L40

Coincidió con la implementación de TF para las configuraciones que se probó, pero la prueba no fue exhaustiva

@soumith Algunas preguntas rápidas:

  1. ¿Hay alguna razón por la que no deberíamos implementar esto a través de functional.conv2d ? El diseño que escribiste parece implicar que no debería. No hay nada acerca de padding = "mismo" que parezca que debería ser específico para las capas. (EDITAR: Nvm, no me di cuenta de que la implícita F.conv2d que estaba viendo era la cuantificada).
  2. Creo que el modo de relleno valid Tensorflow es simplemente equivalente al nuestro con padding=0 , ¿verdad?

Además, no parece que haya una solución fácil para que el usuario lidie con el relleno asimétrico. La regla completa para determinar la cantidad de relleno que debe producirse es
(ceil(x/stride) -1)*stride + (filter-1)*dilation + 1 - x largo de una dimensión. En particular, necesitaremos hacer un relleno asimétrico cuando este no sea un múltiplo de 2. Como contraejemplo a su esperanza de que esto solo suceda con filtros de tamaño uniforme, tome input = 10, stride=3, filter=3, dilation=1 . No veo reglas simples para resolver las situaciones en las que esto puede suceder.

Además, no podremos determinar estáticamente el relleno excepto en el caso de stride=1 , como entonces ceil(x/stride) = x , y tenemos un relleno igual a (filter-1)*dilation .

@Chillee sobre (1), no hay razón, no había pensado en las implicaciones, perf o de otra manera.

(2) Sí.

Además, no podremos determinar estáticamente el padding excepto en el caso de stride = 1, ya que ceil (x / stride) = x, y tenemos padding igual a (filter-1) * dilation

Sí, pero stride = 1 es lo suficientemente común y los beneficios del relleno estático lo suficientemente buenos como para que definitivamente deberíamos manejarlo de manera especial.

Sobre el acolchado asimétrico, oh bien ...

No tiene sentido para mí ¿por qué no se puede ofrecer una API opcional de padding=SAME ? Si alguien está dispuesto a incurrir en el costo adicional del relleno, déjelo hacerlo. Para muchos investigadores, la creación rápida de prototipos es un requisito.

Sí,

No tiene sentido para mí ¿por qué no se puede ofrecer una API opcional de padding=SAME ? Si alguien está dispuesto a incurrir en el costo adicional del relleno, déjelo hacerlo. Para muchos investigadores, la creación rápida de prototipos es un requisito.

¡De acuerdo! Me quedé atrapado en este maldito "relleno" durante 4 horas.

¿Tenemos alguna actualización sobre la solución para este problema?

Vaya y aquí pensé que Pytorch sería más fácil que Keras / Tensorflow 2.0 ...

@zwep, hay un poco más de esfuerzo para comenzar. Tienes que escribir tu ciclo de trianing que puede ser molesto y tienes que escribir capas de manera más explícita. Una vez que lo haya hecho (una vez), puede avanzar mucho más en la mejora real más allá de eso.

Mi regla general es usar Keras si es algo que ha hecho un millón de veces / súper estándar.
use pytorch siempre que haya investigación y desarrollo involucrados.

aquí está mi código para convs 1d acolchadas

antorcha de importación
de la importación de la antorcha nn
importar numpy como np
importar antorcha funcional como 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 Hombre, gracias por el código. ¡Tendremos en mente esa filosofía de Pytorch!

Es 2020. ¿Aún no hay padding='same' en Pytorch?

Esta es una forma de hacer que el mismo relleno funcione para cualquier tamaño de kernel, zancada y dilatación (incluso los tamaños de kernel también funcionan).

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)

También quiero la función de "mismo relleno" en nn.Conv2d .

Por cierto, además de las preocupaciones de perf / serialización discutidas anteriormente, existen razones de corrección / precisión de por qué el modo de relleno "mismo" dependiente del tamaño en TF no es un buen valor predeterminado. Lo discutí en https://github.com/tensorflow/tensorflow/issues/18213 y mostré que en realidad muchos códigos de Google usan un "mismo" modo de relleno independiente del tamaño.

Parece que no hay trabajo en curso en este momento sobre este problema, pero si lo hay, espero que sea una solución independiente del tamaño.

Hola, @ppwwyyxx Yuxin, gracias por la respuesta.
Creo que la implementación de @ McHughes288 es buena y me pregunto cuál es su opinión sobre su implementación.

Aquí está mi solución para el relleno Conv1D SAME (solo funciona correctamente cuando dilation==1 & groups==1 , más complicado cuando se considera la dilatación y los 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, ¿tenías la intención de seguir trabajando en esta función? Voy a anular la asignación por ahora para que podamos realizar un mejor seguimiento del progreso de este problema. No dude en reasignar si todavía está trabajando en ello.

después de leer el código de @wizcheu , creo otra versión de conv1d con 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

¿Hay alguna actualización sobre esto?

¿¿alguna actualización??

@ peterbell10 ha vinculado un borrador de relaciones públicas que puedes seguir.

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

mishraswapnil picture mishraswapnil  ·  3Comentarios

eliabruni picture eliabruni  ·  3Comentarios

ikostrikov picture ikostrikov  ·  3Comentarios

bartolsthoorn picture bartolsthoorn  ·  3Comentarios

miguelvr picture miguelvr  ·  3Comentarios