Pytorch: [Demande de fonctionnalité] Implémenter le "même" remplissage pour les opérations de convolution ?

Créé le 25 nov. 2017  ·  59Commentaires  ·  Source: pytorch/pytorch

La mise en œuvre serait facile, mais pourrait aider de nombreuses personnes souffrant du casse-tête de calculer le nombre de rembourrage dont elles ont besoin.

cc @ezyang @gchanan @zou3519 @albanD @mruberry

enhancement high priority convolution nn triaged

Commentaire le plus utile

Existe-t-il un projet de mise en œuvre d'une API similaire dans Pytorch dans un avenir proche ? Les personnes venant d'un milieu tensorflow / keras l'apprécieront certainement.

Tous les 59 commentaires

Cela semble valoir la peine d'être fait. Quelle est l'interface que vous proposez ? comme nn.Conv2d(..., padding="same") ?

Notez que si vous recherchez le même comportement que TensorFlow, l'implémentation ne sera pas aussi simple, car le nombre de pixels à ajouter dépend de la taille de l'entrée. Voir https://github.com/caffe2/caffe2/blob/master/caffe2/proto/caffe2_legacy.proto pour référence

Merci d'avoir indiqué le problème et la référence.
Pour résoudre le problème signalé par @fmassa , je propose deux interfaces.
Tout d'abord, comme @soutmith l'a mentionné, la première interface serait comme nn.Conv*d(..., padding="same") , calculant le remplissage à chaque appel forward() .
Cependant, ce serait un moyen inefficace lorsque la forme d'entrée est connue dans la phase d'initialisation. Par conséquent, je suggère une interface comme nn.CalcPadConv*d(<almost same parameters as Conv*d>) . En l'utilisant, un utilisateur peut calculer le rembourrage en utilisant la largeur et la hauteur connues lors de l'initialisation, et passer la sortie (la forme du rembourrage) au paramètre de rembourrage de nn.Conv2d(...)
Je ne sais pas si la deuxième proposition pourrait être une optimisation prématurée.
Comment pensez-vous de ceux-ci? Y a-t-il une idée d'un meilleur nom?

Je pense que la plus grande source d'inefficacité viendra du fait que nous devrons ajouter une couche F.pad avant chaque autre convolution qui nécessite le cas padding=same (car la quantité de rembourrage peut ne pas être la même sur les côtés gauche et droit), voyez par exemple comment TensorFlow doit gérer cela dans le cas cudnn . Cela signifie donc que le nn.CalcPadConv*d serait normalement aussi cher qu'un nn.Conv*d(..., padding="same") .

Cela pourrait être rendu plus efficace si nous prenions en charge différents rembourrages pour chaque côté de la convolution (comme dans Caffe2, donc gauche, droite, haut, bas), mais cudnn ne prend toujours pas en charge cela, nous aurions donc besoin d'un rembourrage supplémentaire dans ces cas .

De plus, je pense que si nous ajoutons padding="same" à nn.Conv*d , nous devrions probablement faire la même chose pour nn.*Pool*d , n'est-ce pas ?

Je pense que ce qui me dérange un peu, c'est que les utilisateurs peuvent s'attendre à ce que le comportement de padding=same soit équivalent à TF, mais ils pourraient ne pas s'attendre à une baisse des performances.

Qu'est-ce que tu penses?

Pourquoi serait-ce inefficace? ne pourrions-nous pas simplement calculer le rembourrage à chaque pas en avant ? le coût devrait être minime, il n'est donc pas nécessaire d'optimiser cela. Peut-être que je ne comprends pas complètement la sémantique, mais je ne vois pas pourquoi F.pad serait nécessaire.

rendre le remplissage dépendant de la taille de l'entrée est assez mauvais. Nous venons d'avoir une discussion interne à ce sujet, avec @Yangqing expliquant pourquoi c'est une mauvaise idée pour diverses raisons de sérialisation et d'efficacité.

@fmassa , ce que je voulais était de calculer la forme de remplissage "constante" dans __init__() utilisant nn.CalcPadConv*d() . Comme vous l'avez dit, cette méthode ne fonctionnera pas uniquement lorsque le remplissage calculé est impair. Par conséquent, il est nécessaire d'ajouter la couche F.pad , ou la prise en charge de F.conv*d pour les rembourrages impairs devrait aider.

EDIT: Ensuite, ce que j'ai suggéré devrait être une fonction et placé, disons, torch.nn.utils ou torch.utils.

En conséquence, ce que je suggère est une fonction utilitaire simple, comme (pseudocode):

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)

En outre, la fonction peut être utilisée avec F.pad en faveur de l'utilisateur.

@ qbx2 peut-être que je ne comprends pas complètement votre proposition, mais si nous voulons reproduire le comportement de TensorFlow, je ne pense pas que cela suffise.

Voici un extrait de ce que je pense imite le remplissage SAME TensorFlow nn.Conv2d puisse simplement appeler 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)

Il s'agissait principalement de copier-coller à partir du code TensorFlow ici et ici .

Comme vous pouvez le voir, il se passe beaucoup de choses cachées là-bas, et c'est pourquoi je pense que cela ne vaut peut-être pas la peine d'ajouter un padding='same' . Et je pense que ne pas reproduire le comportement SAME dans TensorFlow n'est pas idéal non plus.

Les pensées?

@fmassa Oui, vous avez raison. Il peut être inefficace de calculer le rembourrage sur chaque forward() .

Cependant, ma proposition n'est PAS de calculer le rembourrage à chaque appel forward() . Un chercheur (développeur) peut s'attendre à ce que les tailles des images atteignent nn.Conv2d avant l'exécution. Et s'il/elle veut le « même » rembourrage, il/elle peut utiliser la fonction pour calculer le rembourrage requis pour imiter « MÊME ».

Par exemple, pensez au cas où un chercheur a des images avec 200x200, 300x300, 400x400. Ensuite, il peut calculer les rembourrages pour les trois cas dans la phase d'initialisation et simplement passer les images à F.pad() avec le rembourrage correspondant. Ou il/elle change simplement le champ de remplissage de nn.Conv2d avant l'appel forward() , soit. Référez-vous à ceci :

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

Oui, je veux juste ajouter la "fonction utilitaire de calcul de remplissage" dans le noyau pytorch.

Lorsque le chercheur souhaite un remplissage dépendant de chaque taille d'image d'entrée, il peut combiner la fonction avec F.pad() avant de passer l'image à nn.Conv2d . Je veux laisser le rédacteur de code décider s'il faut ou non remplir les entrées à chaque appel forward() .

Existe-t-il un projet de mise en œuvre d'une API similaire dans Pytorch dans un avenir proche ? Les personnes venant d'un milieu tensorflow / keras l'apprécieront certainement.

Ainsi, une stratégie de calcul de remplissage de base (qui ne donne pas les mêmes résultats que TensorFlow, mais les formes sont similaires) consiste à avoir

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)

C'est ce que tu as en tête @im9uri ?

C'est similaire à ce que j'avais en tête, mais comme vous l'avez mentionné précédemment, le calcul se complique avec la foulée et la dilatation.

Avoir également une telle API dans d'autres opérations de convolution telles que ConvTranspose2d serait formidable.

Je pense que les "opérateurs de fenêtre coulissante" devraient tous prendre en charge le remplissage asymétrique.

A propos du "même" argument...
@soumith Pouvez-vous expliquer pourquoi faire du remplissage en fonction de la taille de l'entrée est mauvais, s'il vous plaît?
Si c'est un problème, de toute façon, une solution pragmatique pourrait être d'exiger stride == 1 lors de l'utilisation de "même". Pour stride == 1 , le remplissage ne dépend pas de la taille de l'entrée et peut être calculé une seule fois. Le constructeur doit lever un ValueError si l'utilisateur essaie d'utiliser padding='same' avec stride > 1 .

Je sais, ce n'est pas la solution la plus propre mais la contrainte me semble assez raisonnable étant donné que :

  1. la sémantique originale de l'étiquette « même » a été introduite pour les convolutions non enjambées et était : la sortie a la même taille que l'entrée ; bien sûr, ce n'est pas vrai dans tensorflow pour stride > 1 et cela rend l'utilisation du mot "même" un peu trompeuse IMO;
  2. il couvrirait 99% des cas que l'on souhaite utiliser "même" ; Je peux à peine imaginer un cas où quelqu'un a vraiment besoin du comportement de tensorflow pour stride > 1 , alors que si nous donnons à "same" sa sémantique d'origine, eh bien, bien sûr, cela n'a aucun sens d'utiliser une convolution strié si vous voulez que la sortie ait la même taille que l'entrée.

La documentation de

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

Étant donné que le même remplissage signifie padding = (kernel_size - stride)//2, que se passe-t-il si padding = "same" est introduit de telle sorte que lorsqu'il est écrit, il lit automatiquement la taille du noyau et la foulée (comme cela est également mentionné dans nn.Conv2d) et applique le remplissage automatiquement en conséquence

Voici une couche Conv2d très simple avec same rembourrage

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 cela est toujours en cours d'évaluation pour être ajouté à PyTorch, alors en ce qui concerne les compromis entre complexité / inefficacité et facilité d'utilisation pour les développeurs :

Dans la route vers la version 1.0 du blog , il est indiqué :

L'objectif central de PyTorch est de fournir une excellente plate-forme pour la recherche et la piratage. Ainsi, alors que nous ajoutons toutes ces optimisations [production-utilisation], nous avons travaillé avec une contrainte de conception stricte pour ne jamais les échanger contre la convivialité.

Pour l'anecdote, je viens d'avoir utilisé Keras ainsi que les API d'origine tf.layers / estimateur. Tous prennent en charge le rembourrage same . Je suis actuellement en train de réimplémenter un convnet que j'avais initialement écrit en TF avec PyTorch, et le fait que j'ai dû construire moi-même l'arithmétique pour le zéro-remplissage m'a coûté environ une demi-journée de temps.

Si « l'objectif central » est vraiment axé sur la convivialité, je dirais que même s'il y a un impact sur l'efficacité du calcul du zéro remplissage à chaque passe avant (comme mentionné ci-dessus), le temps gagné en termes d'efficacité et de maintenabilité du développeur ( par exemple, ne pas avoir à écrire de code personnalisé pour calculer le remplissage nul) peut valoir le compromis. Les pensées?

j'utiliserais cette fonction

Cela n'a pas de sens pour moi pourquoi une API optionnelle de padding=SAME ne peut-elle pas être proposée ? Si quelqu'un est prêt à assumer le coût supplémentaire du rembourrage, laissez-le le faire. Pour de nombreux chercheurs, le prototypage rapide est une exigence.

Oui, si quelqu'un peut s'il vous plaît ajouter et approuver cela, ce serait génial.

Ajoutez certainement ceci, Conner le veut.

Pytorch le supporte-t-il maintenant? Peut-il utiliser la même opération que d'abord dans VGG, définir padding = (kernel_size-1)/2 ?
Le réseau VGG peut faire en sorte que la taille de sortie ne change pas dans le premier groupe. Ensuite, vous pouvez utiliser stride pour redimensionner la carte des caractéristiques, cela vous semble-t-il correct ?

Voici un exemple pour appeler le remplissage même conv2d à partir 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)

Je passe juste pour dire que j'apprécierais aussi beaucoup cela. Je porte actuellement un modèle simple à partir de tensorflow et les calculs me prennent beaucoup de temps à comprendre...

On dirait que ce fil vient de disparaître. Compte tenu du nombre d'approbations ici, ce serait vraiment génial d'ajouter cette fonctionnalité pour un prototypage plus rapide.

J'écrirai une proposition pour cela et nous pourrons trouver quelqu'un pour la mettre en œuvre.
Je mets cela contre le jalon v1.1.

Merci, vous êtes génial ! J'ai également déposé une demande de fonctionnalité distincte pour que l'argument de remplissage accepte 4 tuples. Cela permettrait un rembourrage asymétrique et symétrique, ce qui est également un bon itinéraire à faible coût pour y arriver à mi-chemin.

@soumith Ce serait bien d'avoir un mode de remplissage SAME dans le pytorch.

@soumith Et si vous utilisiez une interface de type compilation ?

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

J'ai créé un Conv2D avec le même rembourrage qui prend en charge la dilatation et les foulées, en fonction de la façon dont TensorFlow fait le leur. Celui-ci le calcule en temps réel cependant, si vous souhaitez le précalculer, déplacez simplement le remplissage vers init() et disposez d'un paramètre de taille d'entrée.

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 :
Forme d'entrée : (1, 3, 96, 96)
Filtres : 64
Taille: 9x9

Conv2dSame(3, 64, 9)

Forme rembourrée : (1, 3, 104, 104)
Forme de sortie : (1, 64, 96, 96)

Ex2 :
Idem que précédemment, mais avec foulée=2

Conv2dSame(3, 64, 9, 2)

Forme rembourrée = (1, 3, 103, 103)
Forme de sortie = (1, 64, 48, 48)

@jpatts Je pense que votre calcul de forme de sortie est faux, il devrait être ceil(input_dimension / stride). La division entière en python est une division par étage - votre code devrait avoir un résultat différent de tensorflow pour par exemple h=w=28, stride=3, kernel_size=1 .

Voici une variante qui fait le calcul au préalable :

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 la dimension d'entrée est connue et n'est pas calculée à la volée, cela peut être utilisé par exemple :

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

Cependant, dans ce cas, une comptabilité doit être effectuée pour la dimension d'entrée (c'est le problème central), donc si vous utilisez ce qui précède, vous pouvez trouver utile :

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 merci de l'avoir signalé, j'ai fait cela rapidement et

@harritaylor D'accord, cette fonctionnalité simplifierait définitivement le portage des modèles Keras/TF dans PyTorch. De temps en temps, j'utilise toujours des calculs "manuels" de la taille du rembourrage pour créer mes calques avec le même rembourrage.

@kylemcdonald

Voici une couche Conv2d très simple avec same rembourrage

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

Est-ce que ça doit être kb = ka - 1 if kernel_size % 2 else ka ou pas ?

Cela s'appliquera-t-il également à Conv1d ?

Peut-être que l'ajout d'une nouvelle méthode de remplissage à la classe ConvND serait un choix élégant, et en surchargeant la méthode, le calendrier de remplissage pourrait facilement être étendu.

Je peux probablement accepter cela si @soumith a déjà écrit cette proposition ou si quelqu'un résume ce qui doit être fait. Il y a eu beaucoup de discussions ci-dessus et je ne sais pas sur quoi nous avons décidé. Calculons-nous le remplissage en fonction des données d'entrée ou non, devons-nous également implémenter padding="same" pour le pool, etc. ?

J'aimerais également ajouter un rembourrage causal. et veuillez également ajouter ceci à conv1d.
J'ai arrêté de suivre les commentaires à un moment donné, mais je pense que cette fonctionnalité est très bien faite dans Keras. vous devriez le suivre exactement.

@Chillee et voilà :

Portée

Nous devrions ajouter un rembourrage aux couches suivantes :

  • Conv*d
  • MaxPool*d
  • MoyPool*d

Pour le premier PR, restons simple et restons-en à Conv*d.

Complexité et inconvénients

La complexité discutée ci-dessus concerne le fait que la couche devient de nature dynamique, après l'écriture d'une option de remplissage same . C'est-à-dire que cela va des paramètres de la couche connus de manière statique, ce qui est idéal pour l'exportation de modèles (par exemple, l'exportation ONNX), aux paramètres de la couche étant dynamiques. Dans ce cas, le paramètre dynamique est padding .
Bien que cela semble assez inoffensif, la non-statique devient assez importante dans les temps d'exécution limités, comme les temps d'exécution de matériel mobile ou exotique, où, par exemple, vous souhaitez effectuer une analyse et une optimisation de forme statique.

L'autre inconvénient pratique est que ce padding calculé dynamiquement n'est plus toujours symétrique, car en fonction de la taille / foulée du noyau, du facteur de dilatation et de la taille d'entrée, le remplissage peut devoir être asymétrique (c'est-à-dire différent quantité de rembourrage sur le côté gauche par rapport à droite). Cela signifierait que vous ne pouvez pas utiliser les noyaux CuDNN par exemple.

Concevoir

Actuellement, la signature de Conv2d est :

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

Ici, nous soutenons que padding soit un int ou tuple d'entiers (c'est-à-dire pour chaque dimension de hauteur/largeur).
Nous devrions supporter une surcharge supplémentaire pour padding qui prendrait une chaîne, avec la valeur same .

Le rembourrage same doit remplir le input de telle manière avant de le donner à la convolution que la taille output soit la même que la taille input .

Détails d'implémentation

Lorsque 'same' est attribué à padding , nous devons calculer la quantité de rembourrage gauche et droite nécessaire dans chaque dimension.

Il y a deux cas à considérer après le calcul des rembourrages L (gauche) et R (droit) requis :

  • L == R : dans ce cas il s'agit d'un remplissage symétrique. On peut simplement appeler F.conv2d avec une padding égale à L
  • L != R : Dans ce cas, le remplissage est asymétrique, et il a des implications importantes en termes de performances et de mémoire. Nous procédons comme suit :

    • nous appelons input_padded = F.pad(input, ...) et envoyons le input_padded dans le F.conv2d .

    • nous lançons un avertissement pour ce cas (au moins pour la version initiale, et nous pouvons revoir si l'avertissement est nécessaire) sur l'implication des performances.

    • Je ne me souviens pas des détails de la formulation et de l'endroit où nous entrons dans ce cas, mais si je me souviens bien, cela pourrait être aussi simple que d'avoir un noyau de taille égale. Si tel est le cas, l'avertissement peut avoir une solution facile du côté de l'utilisateur.

Inutile de dire qu'il doit être testé pour fonctionner également sur le chemin JIT

@Chilee pour référence, voici une implémentation potentielle pour s'inspirer de https://github.com/mlperf/inference/blob/master/others/edge/object_detection/ssd_mobilenet/pytorch/utils.py#L40

Il correspondait à l'implémentation de TF pour les configurations testées, mais les tests n'étaient pas exhaustifs

@soumith Quelques questions rapides :

  1. Y a-t-il une raison pour laquelle nous ne devrions pas implémenter cela via functional.conv2d ? Le design que vous avez écrit semble impliquer que cela ne devrait pas. Il n'y a rien à propos de padding = "same" qui semble être spécifique aux calques. (EDIT: Nvm, je n'avais pas réalisé que l'impl F.conv2d je regardais était celui quantifié).
  2. Je pense que le mode de remplissage valid Tensorflow est simplement équivalent au nôtre avec padding=0 , n'est-ce pas ?

De plus, il ne semble pas qu'il y ait une solution facile pour l'utilisateur face au rembourrage asymétrique. La règle complète pour déterminer la quantité de rembourrage qui doit se produire est
(ceil(x/stride) -1)*stride + (filter-1)*dilation + 1 - x long d'une dimension. En particulier, nous devrons faire un remplissage asymétrique lorsque ce n'est pas un multiple de 2. Comme contre-exemple à votre espoir que cela ne se produise qu'avec des filtres de taille égale, prenez input = 10, stride=3, filter=3, dilation=1 . Je ne vois pas de règles simples pour résoudre les situations dans lesquelles cela peut arriver.

De plus, nous ne pourrons pas déterminer statiquement le rembourrage sauf dans le cas où stride=1 , comme alors ceil(x/stride) = x , et nous avons un rembourrage égal à (filter-1)*dilation .

@Chillee à propos de (1), aucune raison, je n'avais pas réfléchi aux implications - perf ou autre.

(2) Oui.

De plus, nous ne pourrons pas déterminer statiquement le rembourrage sauf dans le cas où foulée=1, car alors ceil(x/stride) = x, et nous avons un rembourrage égal à (filtre-1)*dilatation

Oui, mais stride=1 est assez courant et les avantages du rembourrage statique sont assez bons pour que nous devions certainement le gérer spécialement.

A propos du rembourrage asymétrique, eh bien.....

Cela n'a pas de sens pour moi pourquoi une API optionnelle de padding=SAME ne peut-elle pas être proposée ? Si quelqu'un est prêt à assumer le coût supplémentaire du rembourrage, laissez-le le faire. Pour de nombreux chercheurs, le prototypage rapide est une exigence.

Oui,

Cela n'a pas de sens pour moi pourquoi une API optionnelle de padding=SAME ne peut-elle pas être proposée ? Si quelqu'un est prêt à assumer le coût supplémentaire du rembourrage, laissez-le le faire. Pour de nombreux chercheurs, le prototypage rapide est une exigence.

Se mettre d'accord! Je suis resté coincé dans ce putain de "rembourrage" pendant 4 heures.

Avons-nous des informations sur la solution à ce problème ?

Wow et là je pensais que Pytorch serait plus simple que Keras/Tensorflow 2.0...

@zwep, il y a un peu plus d'efforts pour commencer. Vous devez écrire votre boucle d'essai, ce qui peut être ennuyeux et vous devez écrire les calques de manière plus explicite. Une fois que vous avez fait cela (une fois), vous pouvez avancer beaucoup plus loin dans l'amélioration réelle au-delà de cela.

Ma règle d'or est d'utiliser Keras si c'est quelque chose que vous avez fait un million de fois/super standard.
utilisez pytorch chaque fois qu'il y a de la recherche et du développement.

voici mon code pour les convs 1d rembourrés

torche d'importation
de la torche import nn
importer numpy en tant que np
importer torch.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 mec, merci pour le code. Gardera cette philosophie Pytorch à l'esprit !

Nous sommes en 2020. Toujours pas de padding='same' à Pytorch ?

C'est une façon d'obtenir le même rembourrage pour n'importe quelle taille de noyau, foulée et dilatation (même les tailles de noyau fonctionnent aussi).

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)

Je veux aussi la fonction "même rembourrage" dans nn.Conv2d .

BTW, en plus des problèmes de performances/sérialisation discutés ci-dessus, il existe des raisons d'exactitude/précision pour lesquelles le mode de remplissage "même" dépendant de la taille dans TF n'est pas une bonne valeur par défaut. J'ai discuté dans https://github.com/tensorflow/tensorflow/issues/18213 et j'ai montré qu'en fait, de nombreux codes de Google utilisent à la place un mode de remplissage "même" indépendant de la taille.

Il semble qu'il n'y ait actuellement aucun travail en cours sur ce problème, mais s'il y en a, j'espère que c'est une solution indépendante de la taille.

Salut, @ppwwyyxx Yuxin, merci pour la réponse.
Je pense que l'implémentation de @McHughes288 est bonne, et je m'interroge sur votre opinion sur son implémentation.

Voici ma solution pour le rembourrage Conv1D SAME (ne fonctionne correctement que lorsque dilation==1 & groups==1 , plus compliqué si l'on considère la dilatation et les groupes):

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 aviez -vous l'intention de continuer à travailler sur cette fonctionnalité ? Je vais vous désaffecter pour le moment afin que nous puissions mieux suivre l'évolution de ce problème, n'hésitez pas à réaffecter si vous travaillez toujours dessus.

après avoir lu le code de @wizcheu , je crée une autre version de conv1d avec 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

Y a-t-il une mise à jour à ce sujet ?

les mises à jour??

@ peterbell10 a lié un projet de relations publiques que vous pouvez suivre.

Cette page vous a été utile?
0 / 5 - 0 notes