Короче говоря, это из-за пакетирования. Первый аргумент torch.conv2d
интерпретируется как [batch, channel, height, width]
, второй - [out_channel, in_channel, height, width]
, а вывод - [batch, channel, height, width]
. Поэтому, если вы звоните conv2d(a, conv2d(b, c))
, вы рассматриваете ведущее измерение b
как пакет, а если вы звоните conv2d(conv2d(a, b), c)
, вы воспринимаете его как out_channels
.
Как говорится, у меня сложилось впечатление, что вы спрашиваете о математике здесь, поэтому позвольте мне расширить. Ваша идея в теории верна: свертки являются линейными операторами и должны быть ассоциативными. Однако, поскольку мы предоставляем им ядер , а не фактические матрицы, представляющие линейные операторы, существует некоторое «преобразование», которое должно происходить за кулисами, чтобы ядра правильно интерпретировались как матрицы. Классически это может быть сделано путем построения соответствующих циркулянтных матриц (без учета граничных условий). Если мы обозначим ядра с помощью a
, b
, c
и оператор создания циркулянтной матрицы с помощью M
, мы получим M(a) @ [M(b) @ M(c)] = [M(a) @ M(b)] @ M(c)
, где @
обозначает умножение матрицы на матрицу.
Реализации Convolution возвращают изображение (вектор, ядро, как вы его называете), а не связанную циркулянтную матрицу, которая смехотворно избыточна и в большинстве случаев не помещается в память. Поэтому нам также нужен некоторый оператор циркулянта к вектору V(matrix)
, который возвращает первый столбец matrix
и поэтому является обратным M
. В абстрактных математических терминах такие функции, как scipy.signal.convolve
(на самом деле correlate
, поскольку свертка требует дополнительного переворота одного из входов, который я пропускаю для ясности), реализованы как convolve = lambda a, b: V(M(a) @ M(b))
и, таким образом,
convolve(a, convolve(b, c)) =
= V(M(a) @ M(V[M(b) @ M(c)])
= V(M(a) @ M(b) @ M(c))
= V(M(V[M(a) @ M(b)]) @ M(c))
= convolve(convolve(a, b), c)
Я надеюсь, что не потерял вас, это просто преобразование одного в другое, используя тот факт, что V
является обратным к M
и ассоциативность умножения матриц для перемещения скобок. Обратите внимание, что средняя линия в основном «необработанная» ABC
. Мы можем проверить с помощью следующего кода:
import numpy as np
import scipy.signal as sig
c2d = sig.convolve2d
a = np.random.randn(7, 7)
b = np.random.randn(3, 3)
c = np.random.randn(3, 3)
ab = c2d(a, b)
ab_c = c2d(ab, c)
bc = c2d(b, c)
a_bc = c2d(a, bc)
print((a_bc - ab_c).max())
Проблема с PyTorch заключается в том, что первый вход интерпретируется как [batch, channel, height, width]
, а второй - как [out_channels, in_channels, height, width]
. Это означает, что оператор «преобразования» M
отличается для первого аргумента и второго аргумента. Назовем их M
и N
соответственно. Поскольку имеется только один выход, есть только один V
, и он может быть обратным либо M
, либо N
, но не обоим (так как они разные). Если вы перепишете вышеприведенное уравнение, стараясь различить M
и N
, вы увидите, что в зависимости от вашего выбора, инвертирует ли V
одну или другую, вы не можете записать равенство между строками 2 и 3 или 3 и 4.
На практике существует также дополнительная проблема измерения channel
, которого нет в классическом определении сверток, однако я предполагаю, что он мог бы иметь дело с одним оператором подъема M
для обоих операнды, в отличие от пакетирования.