Conv2D¶
Описание¶
Этот модуль выполняет операцию двумерной свертки. Для более подробной теоретической информации об операции свёртки см. ConvND.
Для входного тензора с размерами (N, C_{in}, H_{in}, W_{in}), выходного с размерами (N, C_{out}, H_{out}, W_{out}) и ядра свёртки размером (size_h, size_w) операция проводится следующим образом (рассматриваем i-й элемент батча и j-ую карту выходного тензора):
где
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)
Чтобы получить 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)