Pytorch: [Funktionsanfrage] "gleiches" Padding für Faltungsoperationen implementieren?

Erstellt am 25. Nov. 2017  ·  59Kommentare  ·  Quelle: pytorch/pytorch

Die Implementierung wäre einfach, könnte aber vielen Menschen helfen, die unter Kopfschmerzen leiden, wenn sie berechnen müssen, wie viele Polster sie benötigen.

cc @ezyang @gchanan @zou3519 @albanD @mruberry

enhancement high priority convolution nn triaged

Hilfreichster Kommentar

Gibt es einen Plan, in naher Zukunft eine ähnliche API in pytorch zu implementieren? Leute, die einen Tensorflow / Keras-Hintergrund haben, werden es sicherlich zu schätzen wissen.

Alle 59 Kommentare

Dies scheint es wert zu sein. Welche Schnittstelle schlagen Sie vor? wie nn.Conv2d(..., padding="same") ?

Beachten Sie, dass die Implementierung nicht so einfach ist, wenn Sie nach dem gleichen Verhalten von TensorFlow suchen, da die Anzahl der hinzuzufügenden Pixel von der Eingabegröße abhängt. Siehe https://github.com/caffe2/caffe2/blob/master/caffe2/proto/caffe2_legacy.proto als Referenz

Vielen Dank für die Angabe des Problems und der Referenz.
Um das von @fmassa angegebene schlage ich zwei Schnittstellen vor.
Erstens, wie @soutmith erwähnt hat, wäre die erste Schnittstelle nn.Conv*d(..., padding="same") , die die Auffüllung bei jedem forward() Aufruf berechnet.
Es wäre jedoch ein ineffizienter Weg, wenn die Eingabeform in der Initialisierungsphase bekannt ist. Daher schlage ich eine Schnittstelle wie nn.CalcPadConv*d(<almost same parameters as Conv*d>) . Damit kann ein Benutzer die Auffüllung mit bekannter Breite und Höhe bei der Initialisierung berechnen und die Ausgabe (die Form der Auffüllung) an den Auffüllungsparameter von nn.Conv2d(...)
Ich bin mir nicht sicher, ob der zweite Vorschlag eine verfrühte Optimierung sein könnte.
Wie denkst du über diese? Gibt es einen besseren Namen?

Ich denke, die größte Quelle der Ineffizienz wird die Tatsache sein, dass wir vor jeder zweiten Faltung, die den Fall padding=same erfordert, eine F.pad Schicht hinzufügen müssen (weil die Menge an Auffüllung möglicherweise nicht dieselbe ist) auf der linken und rechten Seite), siehe zum Beispiel, wie TensorFlow das im Fall cudnn handhaben muss . Das bedeutet also, dass das nn.CalcPadConv*d normalerweise so teuer wäre wie ein nn.Conv*d(..., padding="same") .

Dies könnte effizienter gemacht werden, wenn wir für jede Seite der Faltung unterschiedliche Paddings unterstützen (wie in Caffe2, also links, rechts, oben, unten), aber cudnn unterstützt dies immer noch nicht, sodass wir in diesen Fällen die zusätzliche Polsterung benötigen würden .

Außerdem denke ich, wenn wir padding="same" zu nn.Conv*d hinzufügen, sollten wir wahrscheinlich dasselbe für nn.*Pool*d tun, oder?

Ich denke, was mich ein wenig stört, ist, dass Benutzer möglicherweise erwarten, dass das Verhalten von padding=same dem von TF entspricht, aber sie erwarten möglicherweise keinen Leistungsabfall.

Was denken Sie?

Warum sollte das ineffizient sein? Könnten wir nicht einfach die Auffüllung bei jedem Vorwärtsschritt berechnen? die Kosten sollten gering sein, also besteht keine Notwendigkeit, dies zu optimieren. Vielleicht verstehe ich die Semantik nicht ganz, aber ich verstehe nicht, warum F.pad benötigt wird.

Padding von der Eingabegröße abhängig zu machen ist ziemlich schlecht. Wir hatten gerade eine interne Diskussion darüber, in der @Yangqing darlegte , warum dies aus verschiedenen Serialisierungs- und Effizienzgründen eine schlechte Idee ist.

@fmassa , ich wollte die "konstante" Füllform in __init__() mit nn.CalcPadConv*d() berechnen. Wie Sie sagten, funktioniert dieser Weg nicht nur, wenn die berechnete Auffüllung ungerade ist. Daher ist es erforderlich, dass die Ebene F.pad hinzugefügt wird, oder die Unterstützung von F.conv*d für ungerade Auffüllungen sollte helfen.

BEARBEITEN: Dann sollte das, was ich vorschlug, eine Funktion sein und beispielsweise in "fackel.nn.utils" oder "fackel.utils" platziert werden.

Als Ergebnis schlage ich eine einfache Hilfsfunktion vor, wie (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)

Außerdem könnte die Funktion mit F.pad zugunsten des Benutzers verwendet werden.

@qbx2 vielleicht verstehe ich Ihren Vorschlag nicht ganz, aber wenn wir das TensorFlow-Verhalten replizieren möchten, denke ich, dass dies nicht ausreicht.

Hier ist ein Ausschnitt von dem, was meiner Meinung nach das TensorFlow SAME Padding nachahmt (ich schreibe es in die funktionale Schnittstelle, damit nn.Conv2d einfach in F.conv2d_same_padding aufrufen kann):

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)

Es wurde hauptsächlich aus dem TensorFlow-Code hier und hier kopiert.

Wie Sie sehen können, gibt es dort viele versteckte Dinge, und deshalb denke ich, dass es sich nicht lohnen könnte, ein padding='same' hinzuzufügen. Und ich denke, es ist auch nicht ideal, das Verhalten von SAME in TensorFlow nicht zu replizieren.

Die Gedanken?

@fmassa Ja, du hast recht. Es kann ineffizient sein, die Auffüllung für alle forward() zu berechnen.

Mein Vorschlag ist jedoch, die Auffüllung NICHT bei jedem forward() Aufruf zu berechnen. Ein Forscher (Entwickler) kann vor der Laufzeit mit einer Bildgröße von nn.Conv2d rechnen. Und wenn er/sie die „gleiche“ Auffüllung möchte, kann er/sie die Funktion verwenden, um die erforderliche Auffüllung zu berechnen, um „GLEICHE“ nachzuahmen.

Stellen Sie sich beispielsweise den Fall vor, dass ein Forscher Bilder mit 200 x 200, 300 x 300, 400 x 400 hat. Dann kann er/sie in der Initialisierungsphase Paddings für die drei Fälle berechnen und die Bilder einfach mit dem entsprechenden Padding an F.pad() . Oder er/sie ändert einfach das Füllfeld von nn.Conv2d vor dem forward() Aufruf. Siehe dazu:

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

Ja, ich möchte nur die "Padding-Rechenhilfsfunktion" in pytorch core hinzufügen.

Wenn der Forscher abhängiges Auffüllen von jeder Eingabebildgröße wünscht, kann er/sie die Funktion mit F.pad() kombinieren, bevor er das Bild an nn.Conv2d übergibt. Ich möchte den Codeschreiber entscheiden lassen, ob die Eingaben bei jedem forward() Aufruf aufgefüllt werden oder nicht.

Gibt es einen Plan, in naher Zukunft eine ähnliche API in pytorch zu implementieren? Leute, die einen Tensorflow / Keras-Hintergrund haben, werden es sicherlich zu schätzen wissen.

Also, eine grundlegende Padding-Berechnungsstrategie (die nicht die gleichen Ergebnisse wie TensorFlow liefert, aber die Formen sind ähnlich) ist zu haben

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)

Ist das das, was Sie im Sinn haben @im9uri ?

Es ist ähnlich wie das, was ich mir vorgestellt habe, aber wie Sie bereits erwähnt haben, wird die Berechnung mit Schritt und Dilatation kompliziert.

Auch eine solche API in anderen Faltungsoperationen wie ConvTranspose2d wäre großartig.

Ich denke, dass "Schiebefenster-Operatoren" alle asymmetrisches Auffüllen unterstützen sollten.

Über das "gleiche" Argument...
@soumith Können Sie bitte erklären, warum es schlecht ist, Padding in Abhängigkeit von der Eingabegröße zu
Wenn dies ein Problem ist, könnte eine pragmatische Lösung darin bestehen, stride == 1 zu verlangen, wenn "same" verwendet wird. Für stride == 1 hängt die Auffüllung nicht von der Eingabegröße ab und kann einmalig berechnet werden. Der Konstruktor sollte ValueError wenn der Benutzer versucht, padding='same' mit stride > 1 .

Ich weiß, es ist nicht die sauberste Lösung, aber die Einschränkung klingt für mich vernünftig genug, da:

  1. die ursprüngliche Semantik des Labels "same" wurde für nicht geschrittene Faltungen eingeführt und lautete: die Ausgabe hat die _gleiche_ Größe der Eingabe; natürlich gilt dies in Tensorflow für stride > 1 und das macht die Verwendung des Wortes "gleich" IMO etwas irreführend;
  2. es würde 99% der Fälle abdecken, in denen man "gleich" verwenden möchte; Ich kann mir kaum einen Fall vorstellen, in dem jemand wirklich das Verhalten von tensorflow für stride > 1 , während wenn wir "same" seine ursprüngliche Semantik geben, natürlich macht es keinen Sinn, eine gestaffelte Faltung zu verwenden Wenn Sie möchten, hat die Ausgabe die gleiche Größe wie die Eingabe.

Die conv2d- Dokumentation enthält die expliziten Formeln für Ausgabegrößen. Durch die Gleichsetzung von zB Hout mit Hin kann man nach der Auffüllung auflösen:

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

Da gleiches Padding bedeutet padding = (kernel_size - stride)//2, was ist, wenn padding = "same" eingeführt wird, so dass beim Schreiben automatisch die Kernelgröße und der stride gelesen werden (wie dies auch in nn.Conv2d erwähnt wird) und padding anwendet automatisch entsprechend

Hier ist eine sehr einfache Conv2d-Ebene mit same Padding als Referenz. Es unterstützt nur quadratische Kernel und Stride=1, Dilation=1, Groups=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])

Wenn dies noch evaluiert wird, um PyTorch hinzugefügt zu werden, dann in Bezug auf die Kompromisse zwischen Komplexität / Ineffizienz vs. Benutzerfreundlichkeit für Entwickler:

Im Weg zum 1.0-Blogpost heißt es:

Das zentrale Ziel von PyTorch ist es, eine großartige Plattform für Forschung und Hackbarkeit bereitzustellen. Während wir also all diese Optimierungen für die Produktion hinzufügen, haben wir mit einer harten Designeinschränkung gearbeitet, um diese niemals gegen die Benutzerfreundlichkeit einzutauschen.

Anekdotisch komme ich aus dem Hintergrund der Verwendung von Keras sowie der ursprünglichen tf.layers / Estimator-APIs. Alle haben Unterstützung für same Padding. Ich bin gerade dabei, ein Convnet, das ich ursprünglich in TF geschrieben hatte, mit PyTorch neu zu implementieren, und die Tatsache, dass ich die Arithmetik für das Zero-Padding selbst einbauen musste, hat mich ungefähr einen halben Tag Zeit gekostet.

Wenn das "zentrale Ziel" wirklich auf die Benutzerfreundlichkeit ausgerichtet ist, würde ich argumentieren, dass selbst wenn die Berechnung von Zero-Padding bei jedem Vorwärtsdurchlauf (wie oben erwähnt) einen Effizienzverlust aufweist, die Zeit in Bezug auf Entwicklereffizienz und Wartbarkeit eingespart wird ( B. keinen benutzerdefinierten Code schreiben zu müssen, um das Auffüllen von Nullen zu berechnen) kann den Kompromiss wert sein. Die Gedanken?

Ich würde diese Funktion nutzen

Es macht für mich keinen Sinn, warum eine optionale API von padding=SAME nicht angeboten werden kann? Wenn jemand bereit ist, die zusätzlichen Kosten für die Polsterung zu tragen, dann lassen Sie dies dies tun. Für viele Forscher ist schnelles Prototyping eine Voraussetzung.

Ja, wenn jemand dies bitte hinzufügen und genehmigen kann, wäre das großartig.

Auf jeden Fall hinzufügen, Conner will es.

Unterstützt pytorch es jetzt? Kann es die gleiche Operation wie zuerst in VGG verwenden, set padding = (kernel_size-1)/2 ?
Das VGG-Netzwerk kann die Ausgabegröße in der ersten Gruppe nicht ändern. Dann können Sie mit Stride die Größe der Featuremap ändern. Klingt das in Ordnung?

Hier ist ein Beispiel, um das Auffüllen derselben conv2d von Deepfakes aufzurufen:

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

Komme nur vorbei, um zu sagen, dass ich das auch sehr schätzen würde. Momentan portiere ich ein einfaches Modell von tensorflow und die Berechnungen dauern sehr lange, bis ich es herausbekomme ...

Anscheinend ist dieser Thread gerade ausgestorben. Angesichts der Anzahl der Daumen nach oben wäre es wirklich großartig, diese Funktion für ein schnelleres Prototyping hinzuzufügen.

Ich werde dafür einen Vorschlag schreiben und wir können jemanden finden, der ihn umsetzt.
Ich setze dies gegen den Meilenstein v1.1.

Danke, du bist großartig! Ich habe auch eine separate Feature-Anfrage eingereicht, damit das Padding-Argument 4-Tupel akzeptiert. Dies würde sowohl asymmetrische als auch symmetrische Polsterung ermöglichen, was auch eine gute kostengünstige Route ist, um auf halbem Weg dorthin zu gelangen.

@soumith Es wäre schön, einen Auffüllmodus GLEICHEN in der pytorch zu haben.

@soumith Wie wäre es mit einer Compile-Typ-Schnittstelle?

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

Ich habe eine Conv2D mit derselben Polsterung erstellt, die Dilatation und Schritte unterstützt, basierend darauf, wie TensorFlow ihre macht. Dieser berechnet es jedoch in Echtzeit, wenn Sie es vorberechnen möchten, verschieben Sie einfach die Auffüllung auf init() und haben einen Eingabegrößenparameter.

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

Bsp1:
Eingabeform: (1, 3, 96, 96)
Filter: 64
Größe: 9x9

Conv2dSame(3, 64, 9)

Gepolsterte Form: (1, 3, 104, 104)
Ausgabeform: (1, 64, 96, 96)

Bsp2:
Wie zuvor, jedoch mit Schrittweite=2

Conv2dSame(3, 64, 9, 2)

Gepolsterte Form = (1, 3, 103, 103)
Ausgabeform = (1, 64, 48, 48)

@jpatts Ich glaube, Ihre Berechnung der Ausgabeform ist falsch, sie sollte ceil(input_dimension / stride) sein. Integer Division in Python ist Floor Division - Ihr Code sollte ein anderes Ergebnis als Tensorflow haben, zB h=w=28, stride=3, kernel_size=1 .

Hier ist eine Variante, die die Berechnung im Voraus durchführt:

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

Wenn die Eingabedimension bekannt ist und nicht im laufenden Betrieb berechnet wird, kann dies z. B. verwendet werden:

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

In diesem Fall muss jedoch eine gewisse Buchhaltung für die Eingabedimension durchgeführt werden (dies ist der Kernpunkt).

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 danke für den Hinweis, ich habe das schnell und schmutzig gemacht und nie überprüft. Aktualisiert, um stattdessen die Decke zu verwenden

@harritaylor Zustimmen , diese Funktion würde die Portierung von Keras/TF-Modellen in PyTorch definitiv vereinfachen. Hin und wieder verwende ich immer noch "manuelle" Berechnungen der Füllgröße, um meine gleich gepolsterten Schichten zu erstellen.

@kylemcdonald

Hier ist eine sehr einfache Conv2d-Ebene mit same Padding als Referenz. Es unterstützt nur quadratische Kernel und Stride=1, Dilation=1, Groups=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])

Soll es kb = ka - 1 if kernel_size % 2 else ka oder nicht?

Gilt das auch für Conv1d?

Vielleicht wäre das Hinzufügen einer neuen Auffüllmethode zur Klasse ConvND eine elegante Wahl, und durch Überladen der Methode könnte der Auffüllzeitplan leicht erweitert werden.

Ich kann das wahrscheinlich annehmen, wenn @soumith diesen Vorschlag jemals geschrieben hat oder wenn jemand zusammenfasst, was zu tun ist. Oben wurde viel diskutiert, und ich bin mir nicht sicher, worauf wir uns geeinigt haben. Berechnen wir Padding abhängig von Eingabedaten oder nicht, müssen wir padding="same" für Pool usw. implementieren?

Ich möchte auch kausale Polsterung hinzufügen. und fügen Sie dies bitte auch zu conv1d hinzu.
Ich habe irgendwann aufgehört, den Kommentaren zu folgen, aber ich denke, diese Funktion ist in Keras sehr gut gemacht. du solltest es genau befolgen.

@Chillee hier gehts:

Umfang

Wir sollten den folgenden Ebenen Polsterung hinzufügen:

  • Conv*d
  • MaxPool*d
  • Durchschn.Pool*d

Für die erste PR halten wir es einfach und bleiben einfach bei Conv*d.

Komplexität und Nachteile

Die oben besprochene Komplexität besteht darin, dass die Ebene dynamisch wird, nachdem eine same geschrieben wurde. Das heißt, dass die Parameter des Layers statisch bekannt sind, was sich hervorragend für den Modellexport (z. B. ONNX-Export) eignet, bis hin zu dynamischen Parametern des Layers. In diesem Fall ist der dynamische Parameter padding .
Während dies ziemlich harmlos aussieht, wird die Nicht-Statik in begrenzten Laufzeiten ziemlich wichtig, wie z.

Der andere praktische Nachteil ist, dass dieses dynamisch berechnete padding nicht mehr immer symmetrisch ist, da je nach Größe / Schrittweite des Kernels, Dilatationsfaktor und Eingabegröße das Padding möglicherweise asymmetrisch (dh unterschiedlich) sein muss Füllmenge links vs. rechts). Dies würde bedeuten, dass Sie beispielsweise keine CuDNN-Kernel verwenden können.

Entwurf

Derzeit ist die Signatur von Conv2d:

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

Hier unterstützen wir padding als int oder tuple ints (dh für jede Dimension von Höhe/Breite).
Wir sollten eine zusätzliche Überladung für padding , die einen String mit dem Wert same annehmen würde.

Die same Auffüllung sollte die input so auffüllen, dass die output Größe der input Größe entspricht, bevor sie der Faltung übergeben wird.

Implementierungsdetails

Wenn 'same' an padding , müssen wir die erforderliche linke und rechte Auffüllung in jeder Dimension berechnen.

Nachdem die erforderliche L- (links) und R- (rechts) Auffüllung berechnet wurde, sind zwei Fälle zu berücksichtigen:

  • L == R: In diesem Fall handelt es sich um eine symmetrische Auffüllung. Man kann einfach F.conv2d mit einem padding Wert von L aufrufen
  • L != R: In diesem Fall ist die Auffüllung asymmetrisch und hat erhebliche Auswirkungen auf Leistung und Speicher. Wir machen folgendes:

    • wir rufen input_padded = F.pad(input, ...) und schicken die input_padded in die F.conv2d .

    • Wir geben für diesen Fall eine Warnung (zumindest für die erste Veröffentlichung, und wir können die Warnung erneut aufrufen, wenn die Warnung erforderlich ist) über die Auswirkungen auf die Leistung.

    • Ich erinnere mich nicht an die Details der Formulierung und wo wir diesen Fall eingeben, aber wenn ich mich erinnere, könnte es so einfach sein, einen gleich großen Kernel zu haben. Wenn dies der Fall ist, kann die Warnung auf der Benutzerseite leicht behoben werden.

Unnötig zu erwähnen, dass es getestet werden muss, um auch auf dem JIT-Pfad zu funktionieren

@Chilee als Referenz, hier ist eine mögliche Implementierung, um sich von https://github.com/mlperf/inference/blob/master/others/edge/object_detection/ssd_mobilenet/pytorch/utils.py#L40 inspirieren zu lassen

Es entsprach der TF-Implementierung für die getesteten Konfigurationen, aber die Tests waren nicht erschöpfend

@soumith Einige

  1. Gibt es einen Grund, warum wir dies nicht über functional.conv2d implementieren sollten? Das von Ihnen geschriebene Design scheint zu implizieren, dass dies nicht der Fall sein sollte. Es gibt nichts an padding = "gleiches", das so aussieht, als ob es spezifisch für Ebenen sein sollte. (EDIT: Nvm, wusste nicht, dass das F.conv2d Impl, das ich mir ansah, das quantisierte war).
  2. Ich denke, der Padding-Modus von Tensorflow valid entspricht einfach unserem mit padding=0 , oder?

Außerdem scheint es für den Benutzer keine einfache Lösung zu geben, mit asymmetrischer Polsterung umzugehen. Die vollständige Regel zur Bestimmung der erforderlichen Auffüllung lautet:
(ceil(x/stride) -1)*stride + (filter-1)*dilation + 1 - x entlang einer Dimension. Insbesondere müssen wir asymmetrisches Padding durchführen, wenn dies kein Vielfaches von 2 ist. Als Gegenbeispiel zu Ihrer Hoffnung, dass dies nur bei Filtern mit gerader Größe passiert, nehmen Sie input = 10, stride=3, filter=3, dilation=1 . Ich sehe keine einfachen Regeln, um die Situationen zu lösen, in denen dies passieren kann.

Außerdem können wir die Auffüllung nicht statisch bestimmen, außer wenn stride=1 , wie dann ceil(x/stride) = x , und wir haben eine Auffüllung von (filter-1)*dilation .

@ Chillee zu (1), kein Grund, ich hatte die Auswirkungen nicht durchdacht - perfekt oder nicht.

(2) Ja.

Außerdem können wir die Auffüllung nicht statisch bestimmen, außer wenn stride=1 ist, da dann ceil(x/stride) = x und wir haben eine Auffüllung gleich (filter-1)*dilation

Ja, aber stride=1 ist üblich genug und die Vorteile des statischen Paddings sind gut genug, dass wir es auf jeden Fall speziell behandeln sollten.

Über asymmetrische Polsterung, na ja.....

Es macht für mich keinen Sinn, warum eine optionale API von padding=SAME nicht angeboten werden kann? Wenn jemand bereit ist, die zusätzlichen Kosten für die Polsterung zu tragen, dann lassen Sie dies dies tun. Für viele Forscher ist schnelles Prototyping eine Voraussetzung.

Jawohl,

Es macht für mich keinen Sinn, warum eine optionale API von padding=SAME nicht angeboten werden kann? Wenn jemand bereit ist, die zusätzlichen Kosten für die Polsterung zu tragen, dann lassen Sie dies dies tun. Für viele Forscher ist schnelles Prototyping eine Voraussetzung.

Zustimmen! Ich steckte 4 Stunden in diesem verdammten „Polster“ fest.

Haben wir ein Update zur Lösung dieses Problems?

Wow und da dachte ich mir, dass Pytorch einfacher wäre als Keras/Tensorflow 2.0...

@zwep der Einstieg ist etwas

Meine Faustregel lautet, Keras zu verwenden, wenn Sie es millionenfach gemacht haben / Superstandard.
Verwenden Sie pytorch immer dann, wenn Forschung und Entwicklung beteiligt sind.

Hier ist mein Code für aufgefüllte 1D-Convs

Taschenlampe importieren
aus Fackelimport nn
importiere numpy als np
Taschenlampe importieren.funktional als 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 Mann, danke für den Code. Werde diese Pytorch-Philosophie im Hinterkopf behalten!

Es ist 2020. Immer noch kein padding='same' in Pytorch?

Dies ist eine Möglichkeit, das gleiche Padding für jede Kernelgröße, jeden Schritt und jede Dilatation zum Laufen zu bringen (sogar Kernelgrößen funktionieren auch).

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)

Ich möchte die Funktion "gleiches Auffüllen" auch in nn.Conv2d .

Übrigens, zusätzlich zu den oben erörterten Perf-/Serialisierungsproblemen gibt es Korrektheits-/Genauigkeitsgründe dafür, warum der größenabhängige "gleiche" Padding-Modus in TF kein guter Standard ist. Ich habe in https://github.com/tensorflow/tensorflow/issues/18213 diskutiert und gezeigt, dass tatsächlich viel Googles eigener Code stattdessen einen größenunabhängigen "gleichen" Padding-Modus verwendet.

Anscheinend gibt es derzeit keine laufenden Arbeiten zu diesem Problem, aber wenn ja, hoffe ich, dass es sich um eine größenunabhängige Lösung handelt.

Hallo @ppwwyyxx Yuxin , danke für die Antwort.
Ich finde die Implementierung von @McHughes288 gut und frage mich, wie du zu seiner Implementierung

Hier ist meine Lösung für Conv1D SAME padding (funktioniert nur korrekt, wenn dilation==1 & groups==1 , komplizierter, wenn Sie Dilatation und Gruppen berücksichtigen):

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 hast du vor, an dieser Funktion weiterzuarbeiten? Ich werde Sie vorerst aufheben, damit wir den Fortschritt dieses Problems besser verfolgen können. Wenn Sie noch daran arbeiten, können Sie es gerne neu zuweisen.

Nachdem ich den Code von @wizcheu gelesen

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

Gibt es dazu ein Update?

irgendwelche Updates??

@peterbell10 hat einen PR-Entwurf verlinkt, dem Sie folgen können.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen