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, 1, 5, 5
outmaps = 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=outmaps, 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=outmaps, 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=outmaps, 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=outmaps, 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=outmaps, 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=outmaps, 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=outmaps, 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=outmaps, 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=outmaps, size=2, stride=1, pad=0, dilation=(3, 1), useBias=False)
print(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=2, initscheme="gaussian", groups=groups)
print(conv.W.shape)
(32, 16, 2, 2)
Видно, что получилась обычная свёртка. Поменяем количество групп:
groups = 4
conv = Conv2D(inmaps, outmaps, size=2, initscheme="gaussian", groups=groups)
print(conv.W.shape)
(32, 4, 2, 2)
Это может быть не очевидно из представленного кода, но теперь операция свёртки будет проходить следующим образом: из первых \frac{inmaps}{groups}=4 входных карт будут получены \frac{outmaps}{groups}=8 выходных карт, этот же принцип сохраняется и для оставшихся четвёрок.

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

from PuzzleLib.Containers import Sequential
batchsize, inmaps, h, w = 1, 3, 5, 5
outmaps = 32
seq = Sequential()
seq.append(Conv2D(inmaps, inmaps, size=4, 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)