Pytorch: [Запрос на функцию] Реализовать "то же самое" заполнение для операций свертки?

Созданный на 25 нояб. 2017  ·  59Комментарии  ·  Источник: pytorch/pytorch

Реализация была бы простой, но могла бы помочь многим людям, страдающим от головной боли при подсчете необходимого количества отступов.

копи @ezyang @gchanan @ zou3519 @albanD @mruberry

enhancement high priority convolution nn triaged

Самый полезный комментарий

Есть ли план в ближайшем будущем реализовать аналогичный api в pytorch? Люди, работающие с тензорным потоком / керасом, безусловно, оценят это.

Все 59 Комментарий

Кажется, это стоит того. Какой интерфейс вы предлагаете? как nn.Conv2d(..., padding="same") ?

Обратите внимание: если вы ищете такое же поведение TensorFlow, реализация не будет такой простой, потому что количество добавляемых пикселей зависит от размера ввода. См. Https://github.com/caffe2/caffe2/blob/master/caffe2/proto/caffe2_legacy.proto для справки.

Спасибо, что указали на проблему и ссылку.
Чтобы решить проблему, заявленную @fmassa , я предлагаю два интерфейса.
Во-первых, как упоминал @soutmith , первый интерфейс будет иметь вид nn.Conv*d(..., padding="same") , вычисляя отступы при каждом вызове forward() .
Однако это будет неэффективным способом, если форма ввода известна на этапе инициализации. Поэтому я предлагаю такой интерфейс, как nn.CalcPadConv*d(<almost same parameters as Conv*d>) . Используя его, пользователь может рассчитать заполнение, используя известную ширину и высоту при инициализации, и передать результат (форму заполнения) параметру заполнения nn.Conv2d(...)
Я не уверен, что второе предложение могло быть преждевременной оптимизацией.
Что вы думаете об этом? Есть ли какое-нибудь название лучше?

Я думаю, что самый большой источник неэффективности проистекает из того факта, что нам нужно будет добавлять слой F.pad перед каждой другой сверткой, для которой требуется случай padding=same (поскольку количество отступов может не совпадать слева и справа), посмотрите, например, как TensorFlow справляется с этим в случае cudnn . Это означает, что nn.CalcPadConv*d обычно будет таким же дорогим, как nn.Conv*d(..., padding="same") .

Это можно было бы сделать более эффективным, если бы мы поддерживали разные отступы для каждой стороны свертки (как в Caffe2, так left, right, top, bottom), но cudnn по-прежнему не поддерживает это, поэтому нам потребовались бы дополнительные отступы в этих случаях. .

Кроме того, я думаю, что если мы добавим padding="same" к nn.Conv*d , нам, вероятно, следует сделать то же самое для nn.*Pool*d , верно?

Я думаю, что меня немного беспокоит то, что пользователи могут ожидать, что поведение padding=same будет эквивалентно TF, но они могут не ожидать падения производительности.

Что вы думаете?

Почему это было бы неэффективно? не могли бы мы просто вычислить отступы на каждом шаге вперед? стоимость должна быть крошечной, поэтому нет необходимости ее оптимизировать. Возможно, я не совсем понимаю семантику, но я не понимаю, зачем нужен F.pad .

делать отступы зависимыми от размера ввода довольно плохо. У нас только что было внутреннее обсуждение этого вопроса, и @Yangqing объяснил, почему это плохая идея по ряду причин сериализации и эффективности.

@fmassa , я намеревался вычислить "постоянную" форму заполнения в __init__() используя nn.CalcPadConv*d() . Как вы сказали, этот способ не будет работать только тогда, когда вычисленное заполнение нечетное. Следовательно, необходимо добавить слой F.pad , или должна помочь поддержка F.conv*d для нечетных отступов.

РЕДАКТИРОВАТЬ: Тогда то, что я предложил, должно быть функцией и помещено, скажем, в torch.nn.utils или torch.utils.

В результате я предлагаю простую служебную функцию, например (псевдокод):

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)

Кроме того, функция может использоваться с F.pad в пользу пользователя.

@ qbx2, возможно, я не полностью понимаю ваше предложение, но если мы хотим воспроизвести поведение TensorFlow, я не думаю, что этого достаточно.

Вот фрагмент того, что, как мне кажется, имитирует заполнение TensorFlow SAME (я записываю его в функциональный интерфейс, чтобы nn.Conv2d мог просто вызывать 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)

В основном это было скопировано из кода TensorFlow сюда и сюда .

Как видите, там происходит много скрытых вещей, и поэтому я думаю, что, возможно, не стоит добавлять padding='same' . И я думаю, что отказ от репликации поведения SAME в TensorFlow тоже не идеален.

Мысли?

@fmassa Да, ты прав. Расчет заполнения каждого forward() может быть неэффективным.

Однако мое предложение НЕ состоит в том, чтобы рассчитывать заполнение при каждом вызове forward() . Исследователь (разработчик) может ожидать, что размеры изображений будут nn.Conv2d перед запуском. И если он / она хочет «такое же» заполнение, он / она может использовать эту функцию для вычисления необходимого заполнения, чтобы имитировать «ТАКЖЕ».

Например, представьте, что у исследователя есть изображения размером 200x200, 300x300, 400x400. Затем он / она может вычислить отступы для трех случаев на этапе инициализации и просто передать изображения в F.pad() с соответствующими отступами. Или он / она просто меняет поле заполнения nn.Conv2d перед вызовом forward() . Обратитесь к этому:

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

Да, я просто хочу добавить «вспомогательную функцию вычисления отступов» в ядро ​​pytorch.

Когда исследователь хочет зависимого заполнения для каждого размера входного изображения, он / она может объединить функцию с F.pad() перед передачей изображения в nn.Conv2d . Я хочу, чтобы автор кода решал, добавлять ли входы при каждом вызове forward() или нет.

Есть ли план в ближайшем будущем реализовать аналогичный api в pytorch? Люди, работающие с тензорным потоком / керасом, безусловно, оценят это.

Итак, базовая стратегия вычисления отступов (которая не дает таких же результатов, как TensorFlow, но формы аналогичны) состоит в том, чтобы иметь

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)

Это то, что ты имел в виду @ im9uri ?

Это похоже на то, что я имел в виду, но, как вы упомянули ранее, расчет усложняется из-за шага и расширения.

Также было бы здорово иметь такой api в других операциях свертки, таких как ConvTranspose2d.

Я думаю, что все «операторы скользящего окна» должны поддерживать асимметричное заполнение.

Насчет «того же» аргумента ...
@soumith Не могли бы вы объяснить, почему делать отступы в зависимости от размера ввода - это плохо?
В любом случае, если это проблема, прагматическим решением может быть требование stride == 1 при использовании "same". Для stride == 1 заполнение не зависит от размера ввода и может быть вычислено за один раз. Конструктор должен вызвать ValueError если пользователь пытается использовать padding='same' с stride > 1 .

Я знаю, это не самое чистое решение, но ограничение кажется мне достаточно разумным, учитывая, что:

  1. исходная семантика метки «такой же» была введена для сверток без разбиения и была следующей: выходные данные имеют _ тот же_ размер, что и входные; конечно, это неверно в тензорном потоке для stride > 1 и это делает использование слова «то же самое» немного вводящим в заблуждение ИМО;
  2. это охватывало бы 99% случаев, когда кто-то хочет использовать «то же самое»; Я с трудом могу представить себе случай, когда кому-то действительно нужно поведение тензорного потока для stride > 1 , тогда как если мы дадим "тому же" его исходную семантику, ну, конечно, нет никакого смысла использовать последовательную свертку если вы хотите, чтобы вывод имел тот же размер, что и ввод.

Документация conv2d дает явные формулы для размеров вывода. Приравнивая, например, Hout к Hin, можно решить для заполнения:

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

Поскольку одно и то же заполнение означает padding = (kernel_size - stride) // 2, что, если padding = "same" вводится таким образом, что при записи он автоматически считывает размер ядра и шаг (как это также упоминается в nn.Conv2d) и применяет заполнение автоматически соответственно

Вот очень простой слой Conv2d с заполнением same для справки. Он поддерживает только квадратные ядра и шаг = 1, расширение = 1, группы = 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])

Если это все еще оценивается на предмет добавления в PyTorch, то относительно компромисса между сложностью / неэффективностью и простотой использования для разработчиков:

По пути к сообщению в блоге 1.0 он гласит:

Основная цель PyTorch - предоставить отличную платформу для исследований и взлома. Итак, пока мы добавляем все эти оптимизации [производственного использования], мы работали с жесткими конструктивными ограничениями, чтобы никогда не обменивать их на удобство использования.

Как ни странно, я исхожу из опыта использования Keras, а также исходных API tf.layers / Estimator. У всех есть поддержка same padding. В настоящее время я заново реализую сверточную сеть, которую я изначально написал на TF, с помощью PyTorch, и тот факт, что мне пришлось самостоятельно встроить арифметику для заполнения нулями, стоил мне примерно полдня времени.

Если «центральная цель» действительно сосредоточена на удобстве использования, то я бы сказал, что даже если есть снижение эффективности при вычислении нулевого заполнения на каждом прямом проходе (как упоминалось выше), время, сэкономленное с точки зрения эффективности разработчика и ремонтопригодности ( например, отсутствие необходимости писать собственный код для вычисления нулевого заполнения) может стоить компромисса. Мысли?

Я бы использовал эту функцию

Для меня это не имеет смысла, почему нельзя предложить дополнительный API padding=SAME ? Если кто-то готов взять на себя дополнительные расходы на обивку, позвольте ему это сделать. Для многих исследователей быстрое прототипирование является требованием.

Да, если бы кто-то мог добавить и одобрить это, было бы здорово.

Определенно добавьте это, Коннер этого хочет.

Поддерживает ли это pytorch сейчас? Может ли он, используя ту же операцию, что и сначала в VGG, установить padding = (kernel_size-1) / 2?
Сеть VGG может сделать так, чтобы размер вывода не изменялся в первой группе. Затем вы можете использовать шаг, чтобы изменить размер карты характеристик, звучит нормально?

Вот один пример вызова заполнения того же conv2d из 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)

Просто заглянул, чтобы сказать, что я тоже очень признателен за это. В настоящее время я портирую простую модель из tensorflow, и мне нужно очень много времени, чтобы разобраться в вычислениях ...

Похоже, эта ветка просто вымерла. Учитывая количество положительных отзывов, было бы здорово добавить эту функцию для более быстрого прототипирования.

Я напишу предложение по этому поводу, и мы найдем кого-нибудь, кто его реализует.
Я ставлю это на отметку v1.1.

Спасибо, вы молодцы! Я также подал отдельный запрос функции, чтобы аргумент заполнения принимал 4-кортеж. Это позволит использовать как асимметричное, так и симметричное заполнение, что также является хорошим недорогим маршрутом, чтобы добраться до середины.

@soumith Было бы неплохо иметь ТАКЖЕ режим заполнения в pytorch.

@soumith Как насчет использования интерфейса типа компиляции?

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

Я сделал Conv2D с таким же заполнением, которое поддерживает расширение и шаги, в зависимости от того, как TensorFlow делает их. Этот вычисляет его в реальном времени, однако, если вы хотите предварительно вычислить его, просто переместите заполнение в init () и получите параметр размера ввода.

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:
Форма ввода: (1, 3, 96, 96)
Фильтры: 64
Размер: 9x9

Conv2dSame(3, 64, 9)

Мягкая форма: (1, 3, 104, 104)
Форма вывода: (1, 64, 96, 96)

Пример 2:
То же, что и раньше, но с шагом = 2

Conv2dSame(3, 64, 9, 2)

Мягкая форма = (1, 3, 103, 103)
Форма вывода = (1, 64, 48, 48)

@jpatts Я считаю, что ваш расчет выходной формы неверен, это должно быть ceil (input_dimension / stride). Целочисленное деление в python - это деление по полу - ваш код должен иметь результат, отличный от тензорного потока, например, для h=w=28, stride=3, kernel_size=1 .

Вот вариант, который производит расчет заранее:

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

Если входной размер известен и не рассчитывается на лету, его можно использовать, например:

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

Однако в этом случае необходимо провести некоторую бухгалтерскую отчетность для входного измерения (это основная проблема), поэтому, если вы используете вышеуказанное, вы можете найти полезным:

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, спасибо, что указали на это, я сделал это быстро и грязно и никогда не проверял. Обновлено для использования потолка.

@harritaylor Согласитесь, эта функция определенно упростит перенос моделей Keras / TF в PyTorch. Время от времени я все еще использую «ручные» вычисления размера отступов для создания моих слоев с такими же заполнениями.

@kylemcdonald

Вот очень простой слой Conv2d с заполнением same для справки. Он поддерживает только квадратные ядра и шаг = 1, расширение = 1, группы = 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])

Должно быть kb = ka - 1 if kernel_size % 2 else ka или нет?

Будет ли это относиться также к Conv1d?

Возможно, добавление нового метода заполнения к классу ConvND было бы элегантным выбором, и, перегрузив метод, можно было бы легко расширить расписание заполнения.

Я, вероятно, приму это, если @soumith когда-либо написал это предложение или если кто-то резюмирует, что нужно сделать. Выше было много обсуждений, и я не уверен, на чем мы остановились. Мы вычисляем отступы в зависимости от входных данных или нет, нужно ли нам реализовать padding="same" для пула и т. Д.?

Я также хотел бы добавить причинное дополнение. и, пожалуйста, также добавьте это в conv1d.
В какой-то момент я перестал следить за комментариями, но я думаю, что эта функция очень хорошо реализована в keras. вы должны точно следовать ему.

@ Чилли, держи :

Сфера

Мы должны добавить отступы к следующим слоям:

  • Конв * д
  • MaxPool * d
  • AvgPool * d

Что касается первого PR, давайте оставим его простым и будем придерживаться Conv * d.

Сложность и недостатки

Сложность, описанная выше, связана с тем, что уровень становится динамическим по своей природе после того, как записана опция same padding. То есть, он идет от параметров слоя, который является статически известным, что отлично подходит для экспорта модели (например, экспорт ONNX), к параметрам динамического слоя. В этом случае динамический параметр - padding .
Хотя это выглядит довольно безобидным, нестатичность становится очень важной в ограниченных средах выполнения, таких как среда выполнения на мобильных устройствах или экзотическом оборудовании, где, например, вы хотите выполнить анализ и оптимизацию статической формы.

Другой практический недостаток заключается в том, что этот динамически вычисляемый padding больше не всегда симметричен, потому что в зависимости от размера / шага ядра, коэффициента расширения и размера ввода, заполнение может быть ассиметричным (т. Е. Другим количество отступов слева и справа). Это означало бы, что вы не можете использовать, например, ядра CuDNN.

Дизайн

В настоящее время подпись Conv2d:

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

Здесь мы поддерживаем padding как int или tuple целых чисел (то есть для каждого измерения высоты / ширины).
Мы должны поддерживать дополнительную перегрузку для padding которая будет принимать строку со значением same .

Заполнение same должно дополнять input таким образом, прежде чем передавать его свертке, чтобы размер output таким же, как размер input .

Детали реализации

Когда 'same' присваивается padding , мы должны вычислить количество левого и правого отступа, необходимого в каждом измерении.

После вычисления необходимого заполнения L (слева) и R (справа) необходимо рассмотреть два случая:

  • L == R: в данном случае это симметричный отступ. Можно просто вызвать F.conv2d со значением padding равным L
  • L! = R: в этом случае заполнение асимметрично, и это имеет значительные последствия для производительности и памяти. Мы делаем следующее:

    • мы вызываем input_padded = F.pad(input, ...) и отправляем input_padded в F.conv2d .

    • мы выдаем предупреждение для этого случая (по крайней мере, для первоначального выпуска, и мы можем вернуться, если предупреждение необходимо) о последствиях для производительности.

    • Я не помню деталей формулировки и того, где мы входим в этот случай, но если я помню, это может быть так же просто, как иметь ядро ​​одинакового размера. В этом случае предупреждение можно легко исправить на стороне пользователя.

Излишне говорить, что он должен быть протестирован, чтобы он также работал на пути JIT.

@Chilee для справки, вот потенциальная реализация, которая может вдохновить https://github.com/mlperf/inference/blob/master/others/edge/object_detection/ssd_mobilenet/pytorch/utils.py#L40

Он соответствовал реализации TF для тестируемых конфигураций, но тестирование не было исчерпывающим.

@soumith Несколько быстрых вопросов:

  1. Есть ли причина, по которой мы не должны реализовывать это через functional.conv2d ? Написанный вами дизайн, кажется, подразумевает, что этого не должно быть. В padding = "same" нет ничего, что могло бы быть специфичным для слоев. (РЕДАКТИРОВАТЬ: Nvm, не понимал, что F.conv2d impl, на который я смотрел, был квантованным).
  2. Я думаю, что режим заполнения valid Tensorflow просто эквивалентен нашему с padding=0 , верно?

Кроме того, не похоже, что у пользователя будет простое решение проблемы асимметричного заполнения. Полное правило для определения количества необходимых отступов:
(ceil(x/stride) -1)*stride + (filter-1)*dilation + 1 - x по измерению. В частности, нам нужно будет выполнить асимметричное заполнение, когда оно не кратно 2. В качестве контрпримера вашей надежде, что это происходит только с фильтрами четного размера, возьмите input = 10, stride=3, filter=3, dilation=1 . Я не вижу простых правил разрешения ситуаций, в которых это может произойти.

Кроме того, мы не сможем статически определять отступы, за исключением случая, когда stride=1 , поскольку тогда ceil(x/stride) = x , и у нас есть заполнение, равное (filter-1)*dilation .

@Chillee о (1), без причины, я не продумал последствия - перфоманс или что-то еще.

(2) Да.

Кроме того, мы не сможем статически определять отступы, за исключением случая, когда stride = 1, поскольку тогда ceil (x / stride) = x, и у нас есть padding, равный (filter-1) * dilation

Да, но stride = 1 является достаточно распространенным явлением, и преимущества статического заполнения достаточно хороши, поэтому мы определенно должны обрабатывать его специально.

Насчет асимметричной прокладки, ну ладно ...

Для меня это не имеет смысла, почему нельзя предложить дополнительный API padding=SAME ? Если кто-то готов взять на себя дополнительные расходы на обивку, позвольте ему это сделать. Для многих исследователей быстрое прототипирование является требованием.

Да,

Для меня это не имеет смысла, почему нельзя предложить дополнительный API padding=SAME ? Если кто-то готов взять на себя дополнительные расходы на обивку, позвольте ему это сделать. Для многих исследователей быстрое прототипирование является требованием.

Согласен! Я застрял в этой гребаной «прокладке» на 4 часа.

Есть ли у нас обновления для решения этой проблемы?

Ого, и тут я подумал, что Pytorch будет проще, чем Keras / Tensorflow 2.0 ...

@zwep, для начала нужно приложить немного больше усилий. Вы должны написать свой цикл трианирования, который может раздражать, и вам придется писать слои более явно. Как только вы это сделаете (один раз), вы сможете значительно продвинуться дальше в реальном улучшении.

Мое практическое правило - используйте Keras, если это то, что вы делали миллион раз / суперстандарт.
используйте pytorch в любое время, когда проводятся исследования и разработки.

вот мой код для однодневных конвенций с добавлением

импортный фонарик
из факела импорт нн
импортировать numpy как np
импортировать torch.functional как 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 Мужик , спасибо за код. Я буду иметь в виду эту философию pytorch!

Наступил 2020 год. По-прежнему нет padding='same' в Pytorch?

Это один из способов заставить одинаковое заполнение работать для любого размера ядра, шага и расширения (даже размеры ядра тоже работают).

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)

Мне тоже нужна функция «одинакового заполнения» в nn.Conv2d .

Кстати, в дополнение к проблемам перфоманса / сериализации, обсуждавшимся выше, существуют причины правильности / точности того, почему зависящий от размера «одинаковый» режим заполнения в TF не является хорошим значением по умолчанию. Я обсуждал в https://github.com/tensorflow/tensorflow/issues/18213 и показал, что на самом деле многие собственные коды Google вместо этого используют независимый от размера «тот же» режим заполнения.

Кажется, что сейчас над этой проблемой не ведется постоянной работы, но если она есть, я надеюсь, что это решение независимо от размера.

Привет, @ppwwyyxx Yuxin , спасибо за ответ.
Я считаю, что реализация @ McHughes288 хороша, и мне интересно ваше мнение о его реализации.

Вот мое решение для заполнения Conv1D SAME (работает правильно только при dilation==1 & groups==1 , более сложное, если вы рассматриваете расширение и группы):

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 , вы собирались продолжить работу над этой функцией? Я собираюсь отменить вас на данный момент, чтобы мы могли лучше отслеживать прогресс в решении этой проблемы. Если вы все еще работаете над этим, можете переназначить вас.

прочитав код @wizcheu , я создаю другую версию conv1d с 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

Есть какие-нибудь обновления по этому поводу?

любые обновления??

@ peterbell10 связал черновик PR, за которым вы можете следить.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги