Вы успешно освоили 1D-свертки, 2D-свертки и 3D-свертки. Вы тоже покорили каналы с множеством входов и выходов. Но в последнем сообщении в блоге серии сверток мы подошли к главному уровню: пониманию транспонированной свертки.

Итак, давайте начнем с названия и посмотрим, с чем мы имеем дело. transpose заставляет (два или более объекта) поменяться местами друг с другом. Когда мы транспонируем матрицы, мы меняем порядок их размеров, поэтому для 2D-матрицы мы, по сути, переворачиваем значения по диагонали. Мы не будем рассматривать это в данной серии статей, но можно представить операции (такие как вращения, сдвиги и свертки) в виде матриц. См. Раздел 4.1 Dumoulin & Visin, если вам интересно. Когда мы транспонируем свертки, мы меняем порядок измерений в этой матрице операций свертки, что дает некоторые интересные эффекты и приводит к другому поведению по сравнению с обычными свертками, о которых мы узнали до сих пор.

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

Зачем они нам нужны?

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

Перед сверточным автокодировщиком ставится задача воссоздать входное изображение после передачи промежуточных результатов через «узкое место» ограниченного размера. Использование автокодировщиков включает сжатие, удаление шума, окрашивание и окраску. Успех зависит от способности изучить сжатие, специфичное для набора данных, в ядрах свертки и декомпрессию для конкретного набора данных в транспонированных ядрах свертки. Зачем же останавливаться на достигнутом?

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

Семантическая сегментация - это пример использования транспонированных сверточных слоев для распаковки абстрактного представления в другой домен (из входного изображения RGB). Мы выводим класс для каждого пикселя входного изображения, а затем просто для целей визуализации мы визуализируем каждый класс как отдельный цвет (например, класс людей показан красным, автомобили темно-синим и т. Д.).

Есть недостатки?

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

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

Таблица рисует тысячу формул

В отличие от обычных сверток, где объяснения довольно последовательны, а диаграммы часто интуитивно понятны, мир транспонированных сверток может быть немного более устрашающим. Вы часто встретите разные (кажущиеся несвязанными) способы думать о вычислениях. Итак, в этом сообщении в блоге я возьму две ментальные модели транспонированных сверток и помогу вам соединить точки с помощью нашего верного друга… MS Excel. И мы будем кодировать вещи в Apache MXNet, потому что мы, вероятно, когда-нибудь захотим использовать их на практике!

Дополнительно: операция транспонированной свертки эквивалентна вычислению градиента для обычной свертки (т. Е. Обратного прохода обычной свертки). Наоборот. Учтите это при чтении следующего раздела.

Ментальная модель no 1: распределение ценностей

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

Давайте начнем с точки зрения одного значения во входных данных. Мы берем это значение и «распределяем» его по окрестностям точек на выходе. Ядро точно определяет, как мы это делаем, и для каждой выходной ячейки мы умножаем входное значение на соответствующий вес ядра. Мы повторяем этот процесс для каждого значения на входе и накапливаем значения в каждой выходной ячейке. На рисунке 4 показан пример этого накопления (с вводом модуля и ядром).

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

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

С Apache MXNet мы можем воспроизвести это, используя блоки Transpose. У нас есть два пространственных измерения, поэтому мы будем использовать Conv2DTranspose. Аналогично MXNet определяет Conv1DTranspose и Conv3DTranspose.

input_data = mx.nd.ones(shape=(4,4))
kernel = mx.nd.ones(shape=(3,3))
conv = mx.gluon.nn.Conv2DTranspose(channels=1, kernel_size=(3,3))
# see appendix for definition of `apply_conv`
output_data = apply_conv(input_data, kernel, conv)
print(output_data)
# [[[[1. 2. 3. 3. 2. 1.]
#    [2. 4. 6. 6. 4. 2.]
#    [3. 6. 9. 9. 6. 3.]
#    [3. 6. 9. 9. 6. 3.]
#    [2. 4. 6. 6. 4. 2.]
#    [1. 2. 3. 3. 2. 1.]]]]
# <NDArray 1x1x6x6 @cpu(0)>

Ментальная модель # 2: сбор ценностей

Другой способ думать о транспонированных свертках - с точки зрения ячейки на выходе, а не значения на входе, как мы делали с первой ментальной моделью. Когда мы это делаем, мы получаем что-то странно знакомое, что-то очень похожее на обычную свертку!

Шаг за шагом мы будем преобразовывать то, что мы уже знаем, в этот новый образ мышления. Начнем с анимации на рисунке 6. Она выделяет одну ячейку в выходных данных и смотрит на входные значения, которые распределяются в нее. Вы должны обратить пристальное внимание на веса ядра, используемые для каждого из входных значений.

Мы можем сделать это еще более очевидным на рисунке 7, закодировав входные значения по весу ядра, на который они умножаются перед накоплением. Вы должны заметить, как ядро ​​на входе «перевернулось» относительно центра; то есть темно-синий вес ядра был внизу справа при распределении, но он перемещается в верхний левый угол, когда мы думаем о сборе.

Мы только что создали свертку! Если вы мне не верите, посмотрите стоп-кадр на рисунке 8. Мы используем «перевернутое» ядро, которое, несмотря на название «транспонированная свертка», на самом деле не является транспонированием ядра дистрибутива.

Продвинутый: если вы внимательны, то, возможно, заметили, что применение такого Conv2D фактически приведет к выходу 2x2. Conv2DTranspose без заполнения эквивалентно заполнению 2x2 ((kernel_size + 1) / 2) теперь, когда мы сопоставили операцию с Conv2D, давая нам вывод 6x6, как и раньше.

Сбор значений с помощью 2D-сверток позволяет нам писать явные формулы для вывода: идеально для MS Excel, а также для реализации кода. Таким образом, у нас будет следующая формула для левой верхней ячейки вывода:

Мы можем подтвердить наши результаты с помощью кода Apache MXNet, который мы видели ранее:

# define input_data and kernel as above
# input_data.shape is (4, 4)
# kernel.shape is (3, 3)
conv = mx.gluon.nn.Conv2DTranspose(channels=1, kernel_size=(3,3))
output_data = apply_conv(input_data, kernel, conv)
print(output_data)
# [[[[ 1.  5. 11. 14.  8.  3.]
#    [ 1.  6. 15. 18. 12.  3.]
#    [ 4. 13. 21. 21. 15. 11.]
#    [ 5. 17. 28. 27. 25. 11.]
#    [ 4.  7.  9. 12.  8.  6.]
#    [ 6.  7. 14. 13.  9.  6.]]]]
# <NDArray 1x1x6x6 @cpu(0)>

ГНИДДАП!

Мы только что видели странный пример Conv2DTranspose без отступов (кажется, что имеет отступ 2x2, если думать о нем как о Conv2D), но все становится еще более загадочным, когда мы начинаем добавлять отступы.

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

for pad in range(3):
    conv = mx.gluon.nn.Conv2DTranspose(channels=1,
                                       kernel_size=(3,3),
                                       padding=(pad,pad))
    output_data = apply_conv(input_data, kernel, conv)
    print("With padding=({pad}, {pad}) the output shape is {shape}"
          .format(pad=pad, shape=output_data.shape))
# With padding=(0, 0) the output shape is (1, 1, 6, 6)
# With padding=(1, 1) the output shape is (1, 1, 4, 4)
# With padding=(2, 2) the output shape is (1, 1, 2, 2)

Мы можем рассматривать заполнение для транспонированных сверток как величину заполнения, которая включается в полный вывод. Придерживаясь нашего обычного примера (где полный вывод равен 6x6), когда мы определяем отступ 2x2, мы, по сути, говорим, что нам не важны внешние ячейки вывода (с шириной 2), потому что это было просто заполнение, оставив нам выход 2x2. Рассматривая транспонированные свертки как обычные свертки, мы удаляем заполнение из ввода на определенную величину. См. Пример с MS Excel на рисунке 11 и обратите внимание на то, что выходные данные идентичны центральным значениям выходных данных на рисунке 10 при отсутствии заполнения.

# define input_data and kernel as above
# input_data.shape is (4, 4)
# kernel.shape is (3, 3)
conv = mx.gluon.nn.Conv2DTranspose(channels=1,
                                   kernel_size=(3,3),
                                   padding=(2,2))
output_data = apply_conv(input_data, kernel, conv)
print(output_data)
# [[[[21. 21.]
#    [28. 27.]]]]
# <NDArray 1x1x2x2 @cpu(0)>

СЕДИРТЫ!

Шаги тоже меняются местами. При регулярной свертке мы шагаем по входу, в результате чего получаем меньший результат. Но когда мы думаем о транспонированных свертках с точки зрения распределения, мы опережаем вывод, что увеличивает размер вывода. Шаги отвечают за эффект масштабирования транспонированных сверток. См. Рисунок 12.

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

Хотя все ясно с точки зрения распределения выше, все становится немного странно, когда мы думаем о вещах с точки зрения коллекции и пытаемся реализовать это с помощью свертки. Шаг по выходу эквивалентен «дробному шагу» по входу, и отсюда происходит альтернативное название транспонированных извилин, называемое «извилины с дробным шагом». Шаг 2 по выходу будет эквивалентен шагу 1/2 по входу: дробный шаг. Мы реализуем это путем введения пустых пробелов между нашими входными значениями, величиной, пропорциональной шагу, а затем шагом на единицу. В результате мы применяем ядро ​​к области ввода, которая меньше самого ядра! См. Пример на Рисунке 13.

# define input_data and kernel as above
# input_data.shape is (2, 2)
# kernel.shape is (3, 3)
conv = mx.gluon.nn.Conv2DTranspose(channels=1,
                                   kernel_size=(3,3),
                                   strides=(2,2))
output_data = apply_conv(input_data, kernel, conv)
print(output_data)
# [[[[ 3.  6. 12.  6.  9.]
#    [ 0.  3.  0.  3.  0.]
#    [ 7.  5. 16.  5.  9.]
#    [ 0.  1.  0.  1.  0.]
#    [ 2.  1.  4.  1.  2.]]]]
# <NDArray 1x1x5x5 @cpu(0)>

Многоканальные транспонированные свертки

Как и в случае обычных сверток, каждый входной канал будет использовать отдельное ядро, и результаты для каждого канала будут суммироваться, чтобы получить единственный выходной канал. Мы повторяем этот процесс для каждого необходимого выходного канала, используя другой набор ядер. Все эти ядра хранятся в одном массиве ядра с формой:

(входные каналы, выходные каналы, высота ядра, ширина ядра)

Это отличается от формы массива ядра, используемой для обычной свертки:

(выходные каналы, входные каналы, высота ядра, ширина ядра)

# define input_data and kernel as above
# input_data.shape is (3, 5, 5)
# kernel.shape is (3, 3, 3)
kernel = kernel.expand_dims(0).transpose((1,0,2,3))
# kernel.shape is now (3, 1, 3, 3)
conv = mx.gluon.nn.Conv2DTranspose(channels=1, kernel_size=(3,3))
output_data = apply_conv(input_data, kernel, conv)
print(output_data)
# [[[[ 4.  2.  1.  5.  2.  2.  0.]
#    [ 9.  6.  7. 13.  9.  1.  4.]
#    [11. 14. 12. 14. 17. 11.  4.]
#    [ 5. 17. 19. 25. 18. 14.  6.]
#    [ 6. 13. 25. 21. 22.  6.  6.]
#    [ 1.  3. 20.  9. 17. 15.  0.]
#    [ 0.  3.  4. 11. 11.  5.  2.]]]]
# <NDArray 1x1x7x7 @cpu(0)>

Экспериментируйте

Все примеры, показанные в сообщениях этого блога, можно найти в этой Таблице Excel (а также Таблице Google). Щелкните ячейки вывода, чтобы проверить формулы и попробовать разные значения ядра, чтобы изменить результаты. После репликации ваших результатов в MXNet Gluon, я думаю, вы можете официально добавить мастер свертки в качестве заголовка в свой профиль LinkedIn!

Поздравляю!

Вы подошли к концу этой серии excel одолженных по сверткам. Надеюсь, вы узнали что-то полезное и теперь готовы применить эти методы к реальным проблемам с Apache MXNet. Любые вопросы? Просто оставьте комментарий ниже или посетите Дискуссионный форум MXNet. Акции и аплодисменты также были бы очень признательны. Большое спасибо!

Приложение