Conv2D

Описание

Info

Родительский класс: ConvND

Производные классы: -

Этот модуль выполняет операцию двумерной свертки. Для более подробной теоретической информации об операции свёртки см. ConvND.

Для входного тензора с размерами (N, C_{in}, H_{in}, W_{in}), выходного с размерами (N, C_{out}, H_{out}, W_{out}) и ядра свёртки размером (size_h, size_w) операция проводится следующим образом (рассматриваем i-й элемент батча и j-ую карту выходного тензора):

out_i(C_{out_j}) = bias(C_{out_j}) + \sum_{k=0}^{C_{in} - 1}weight(C_{out_j}, k) \star input_i(k)

где

N - размер батча;
C - количество карт в тензоре;
H - размер карты тензора по высоте;
W - размер карты тензора по ширине;
bias - тензор смещений слоя свёртки, имеет размеры (1, C_{out}, 1, 1);
weight - тензор весов слоя свёртки, имеет размеры (C_{out}, C_{in}, size_h, size_w);
\star - оператор взаимной корреляции.

Инициализация

def __init__(self, inmaps, outmaps, size, stride=1, pad=0, dilation=1, wscale=1.0, useBias=True, name=None, initscheme=None, empty=False, groups=1):

Параметры

Параметр Возможные типы Описание По умолчанию
inmaps int Количество карт во входном тензоре. -
outmaps int Количество карт в выходном тензоре. -
size int Размер ядра свёртки (ядро всегда равностороннее). -
stride Union[int, tuple] Шаг свёртки. 1
pad Union[int, tuple] Паддинг карт. 0
dilation Union[int, tuple] Разрежение окна свёртки. 1
wscale float Дисперсия случайных весов слоя. 1.0
useBias bool Использовать или нет вектор смещений. True
initscheme Union[tuple, str] Указывает схему инициализации весов слоя (см. createTensorWithScheme). None -> ("xavier_uniform", "in")
name str Имя слоя. None
empty bool Инициализировать ли матрицу весов и смещений. False
groups int На сколько групп разбиваются карты для раздельной обработки. 1

Пояснения

Info

Для вышерассмотренных входного (N, C_{in}, H_{in}, W_{in}) и выходного (N, C_{out}, H_{out}, W_{out}) тензоров существует зависимость между их размерами: \begin{equation} H_{out} = \frac{H_{in} + 2pad_h - dil_h(size_h - 1) - 1}{stride_h} + 1 \end{equation}

\begin{equation} W_{out} = \frac{W_{in} + 2pad_w - dil_w(size_w - 1) - 1}{stride_w} + 1 \end{equation}


size - фильтры всегда квадратные, т.е. (size_h, size_w), где size_h == size_w;


stride - возможна передача как единой величины шага свёртки по высоте и ширине, так и tuple вида (stride_h, stride_w), где stride_h - величина шага свёртки вдоль высоты картинки, и stride_w - вдоль ширины;


pad - возможна передача как единой величины отступа для всех сторон карт, так и tuple вида (pad_h, pad_w), где pad_h - величина отступа с каждой стороны вдоль высоты картинки, и pad_w - вдоль ширины. Возможности создания асимметричного паддинга (заполнение дополнительными элементами только с одной стороны тензора) для данного модуля не предусмотрено, используйте Pad2D;


dilation - возможна передача как единой величины разрежения для всех сторон ядра свёртки, так и tuple вида (dil_h, dil_w), где dil_h - величина разрежения фильтра вдоль высоты картинки, и dil_w - вдоль ширины;


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

Общее правило звучит так (при этом надо учитывать, что значения параметров inmaps и outmaps должны целочисленно делиться на значение параметра groups): из каждых \frac{inmaps}{groups} входных карт формируются \frac{outmaps}{groups} выходных карт. То есть, можно сказать, мы проводим groups независимых свёрток. Частные случаи:

  • если groups=1, то каждая выходная карта взаимодействует со всеми входными картами, то есть происходит обычная свёртка;
  • если inmaps == outmaps == groups, то происходит depthwise свёртка - из каждой входной карты формируется одна выходная (см. подробности в теории ConvND).

Таким образом, для получения полноценного Depthwise Separable Convolution блока необходимо разместить подряд два библиотечных слоя свёртки:

  • один depthwise с параметрами inmaps == outmaps == groups;
  • один pointwise с ядром размера 1.

Примеры


Базовый пример свёртки


Необходимые импорты.

>>> import numpy as np
>>> from PuzzleLib.Backend import gpuarray
>>> from PuzzleLib.Modules import Conv2D
>>> from PuzzleLib.Variable import Variable

Info

gpuarray необходим для правильного размещения тензора на GPU

Зададим параметры тензоров таким образом, чтобы можно было наглядно продемонстрировать работу модуля: установим количество входных и выходных карт равными 2 и 2 соответственно.

>>> batchsize, inmaps, h, w = 1, 2, 5, 5
>>> outsize = 2

Синтетический тензор:

>>> data = gpuarray.to_gpu(np.arange(batchsize * inmaps * h * w).reshape((batchsize, inmaps, h, w)).astype(np.float32))
>>> print(data)
[[[[ 0.  1.  2.  3.  4.]
   [ 5.  6.  7.  8.  9.]
   [10. 11. 12. 13. 14.]
   [15. 16. 17. 18. 19.]
   [20. 21. 22. 23. 24.]]]]

Размер фильтра зададим 2, остальные параметры свёртки оставим по умолчанию (stride=1, pad=0, dilation=1, groups=1). Использование смещений явно отключим (хоть по умолчанию их тензор будет нулевым и не будет оказывать влияния на итоговый результат):

>>> size = 2
>>> conv = Conv2D(inmaps=inmaps, outmaps=outsize, size=size, useBias=False)

В этом моменте проведён небольшой хак, чтобы задать веса явно. Так как выходных карт две и тензоры весов имеют размерности вида (C_{out}, C_{in}, size_h, size_w):

def customW(size):
  w1 = np.diag(np.full(size, 1)).reshape((1, 1, size, size))
  w2 = np.flip(np.diag(np.arange(1, size + 1) * (-1)), 1).reshape((1, 1, size, size))
  w = np.vstack([w1, w2]).astype(np.float32)

  return w
>>> w = customW(size)
>>> print(w)
[[[[ 1.  0.]
   [ 0.  1.]]]

 [[[ 0. -1.]
   [-2.  0.]]]]

Установим веса на модуль:

>>> conv.setVar("W", Variable(gpuarray.to_gpu(w)))

Important

Вводим условие, что веса модуля для всех примеров задаются функцией customW. Для краткости этот момент в примерах кода будет опущен.

Проведём операцию свёртки на синтетическом тензоре. Так как паддинг не был задан, то размер карт выходного тензора меньше, чем входного:

>>> conv(data)
>>> print(conv.data)
[[[[  6.   8.  10.  12.]
   [ 16.  18.  20.  22.]
   [ 26.  28.  30.  32.]
   [ 36.  38.  40.  42.]]

  [[-11. -14. -17. -20.]
   [-26. -29. -32. -35.]
   [-41. -44. -47. -50.]
   [-56. -59. -62. -65.]]]]


Параметр size


Используем всё то же самое, что и в предыдущем примере, но зададим другой размер фильтра:

>>> conv = Conv2D(inmaps=inmaps, outmaps=outsize, size=3, useBias=False)
>>> conv(data)
>>> print(conv.data)
[[[[  18.   21.   24.]
   [  33.   36.   39.]
   [  48.   51.   54.]]

  [[ -44.  -50.  -56.]
   [ -74.  -80.  -86.]
   [-104. -110. -116.]]]]


Параметр pad


Используем параметры с предыдущего примера, но, допустим, хотим сохранить размеры тензора. Учитывая, что размер фильтра равен 3, а шаг свёртки 1, то для сохранения размера 5х5 нам понадобится паддинг 1 с каждой стороны, т.е. исходный тензор будет выглядеть следующим образом:

[[[[ 0.  0.  0.  0.  0.  0.  0.]
   [ 0.  0.  1.  2.  3.  4.  0.]
   [ 0.  5.  6.  7.  8.  9.  0.]
   [ 0. 10. 11. 12. 13. 14.  0.]
   [ 0. 15. 16. 17. 18. 19.  0.]
   [ 0. 20. 21. 22. 23. 24.  0.]
   [ 0.  0.  0.  0.  0.  0.  0.]]]]

Переинициализируем свёртку:

>>> conv = Conv2D(inmaps=inmaps, outmaps=outsize, size=3, pad=1, useBias=False)
>>> conv(data)
>>> print(conv.data)
[[[[   6.    8.   10.   12.    4.]
   [  16.   18.   21.   24.   12.]
   [  26.   33.   36.   39.   22.]
   [  36.   48.   51.   54.   32.]
   [  20.   36.   38.   40.   42.]]

  [[   0.  -17.  -22.  -27.  -32.]
   [ -11.  -44.  -50.  -56.  -57.]
   [ -26.  -74.  -80.  -86.  -82.]
   [ -41. -104. -110. -116. -107.]
   [ -56.  -59.  -62.  -65.  -48.]]]]

Паддинг для высоты и ширины карты можно задать разным:

>>> conv = Conv2D(inmaps=inmaps, outmaps=outsize, size=3, pad=(1, 0), useBias=False)
>>> conv(data)
>>> print(conv.data)
[[[[   8.   10.   12.]
   [  18.   21.   24.]
   [  33.   36.   39.]
   [  48.   51.   54.]
   [  36.   38.   40.]]

  [[ -17.  -22.  -27.]
   [ -44.  -50.  -56.]
   [ -74.  -80.  -86.]
   [-104. -110. -116.]
   [ -59.  -62.  -65.]]]]


Параметр stride


Вернёмся к параметрам по умолчанию и фильтру размера 2х2, но изменим шаг свёртки:

>>> conv = Conv2D(inmaps=inmaps, outmaps=outsize, size=2, stride=2, useBias=False)
>>> conv(data)
>>> print(conv.data)
[[[[  6.  10.]
   [ 26.  30.]]

  [[-11. -17.]
   [-41. -47.]]]]

Чтобы сохранить размер исходного тензора, придётся выставлять паддинг, равный 3:

>>> conv = Conv2D(inmaps=inmaps, outmaps=outsize, size=2, stride=2, pad=3, useBias=False)
>>> conv(data)
>>> print(conv.data)
[[[[  0.   0.   0.   0.   0.]
   [  0.   0.   2.   4.   0.]
   [  0.  10.  18.  22.   0.]
   [  0.  20.  38.  42.   0.]
   [  0.   0.   0.   0.   0.]]

  [[  0.   0.   0.   0.   0.]
   [  0.   0.  -2.  -6.   0.]
   [  0.  -5. -29. -35.   0.]
   [  0. -15. -59. -65.   0.]
   [  0.   0.   0.   0.   0.]]]]

Как и параметр pad, параметр stride может быть задан разным для высоты и ширины:

>>> conv = Conv2D(inmaps=inmaps, outmaps=outsize, size=2, stride=(2, 4), pad=3, useBias=False)
>>> conv(data)
>>> print(conv.data)
[[[[  0.   0.   0.]
   [  0.   2.   0.]
   [  0.  18.   0.]
   [  0.  38.   0.]
   [  0.   0.   0.]]

  [[  0.   0.   0.]
   [  0.  -2.   0.]
   [  0. -29.   0.]
   [  0. -59.   0.]
   [  0.   0.   0.]]]]


Параметр dilation


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

>>> conv = Conv2D(inmaps=inmaps, outmaps=outsize, size=2, stride=1, pad=0, dilation=2, useBias=False)
>>> conv(data)
>>> print(conv.data)
[[[[ 12.  14.  16.]
   [ 22.  24.  26.]
   [ 32.  34.  36.]]

  [[-22. -25. -28.]
   [-37. -40. -43.]
   [-52. -55. -58.]]]]
Параметр dilation может быть задан различным для осей фильтра свёртки:
>>> conv = Conv2D(inmaps=inmaps, outmaps=outsize, size=2, stride=1, pad=0, dilation=(3, 1), useBias=False)
>>> conv(data)
[[[[ 16.  18.  20.  22.]
   [ 26.  28.  30.  32.]]

  [[-31. -34. -37. -40.]
   [-46. -49. -52. -55.]]]]


Параметр groups


Для данного примера вывод тензоров приведёт к очень громоздким конструкциям, поэтому опустим их.

В данном примере переинициализации весов функциией customW не происходит.

>>> batchsize, inmaps, h, w = 1, 16, 5, 5
>>> outmaps = 32
>>> groups = 1
>>> conv = Conv2D(inmaps, outmaps, size=size, initscheme="gaussian", groups=groups)
>>> print(conv.W.shape)
(32, 16, 3, 3)
Видно, что получилась обычная свёртка. Поменяем количество групп:
>>> groups = 4
>>> conv = Conv2D(inmaps, outmaps, size=size, initscheme="gaussian", groups=groups)
>>> print(conv.W.shape)
(32, 4, 3, 3)
Это может быть не очевидно из представленного кода, но теперь операция свёртки будет проходить следующим образом: из первых \frac{inmaps}{groups}=4 входных карт будут получены \frac{outmaps}{groups}=8 выходных карт, этот же принцип сохраняется и для оставшихся четвёрок.

Чтобы получить Depthwise Separable Convolution блок:

>>> from PuzzleLib.Containers import Sequential
>>> seq = Sequential()
>>> seq.append(Conv2D(inmaps, inmaps, size=size, initscheme="gaussian", groups=inmaps, name="depthwise"))
>>> seq.append(Conv2D(inmaps, outmaps, size=1, initscheme="gaussian", name="pointwise"))
>>> print(seq["depthwise"].W.shape)
>>> (3, 1, 4, 4)
>>> print(seq["pointwise"].W.shape)
>>> (32, 3, 1, 1)
>>> data = gpuarray.to_gpu(np.random.randn(batchsize, inmaps, h, w).astype(np.float32))
>>> seq(data)