Чтобы добиться того же поведения, что и у плотного слоя с использованием слоя Conv1d, необходимо убедиться, что любой выходной нейрон из Conv1d подключен к каждому входному нейрону.
Для ввода размера [batch_size, L, K] ваш Conv1d должен иметь ядро размера L и столько фильтров, сколько вы хотите вывести нейронов.Чтобы понять почему, давайте вернемся к определению 1d свертки или временной свертки.
Параметры слоя Conv1d состоят из набора обучаемых фильтров.Каждый фильтр обычно мал по времени и распространяется на всю глубину входного объема.Например, в вашей задаче типичный фильтр может иметь размер 5xK (т. Е. 5 шагов вашей последовательности, и K, потому что ваш вход имеет глубину K).Во время прямого прохода мы скользим (точнее, сворачиваем) каждый фильтр по различным шагам последовательности входного тома и вычисляем точечные произведения между записями фильтра и входом в любой позиции.Сдвигая фильтр, мы создадим одномерную карту активации, которая дает ответы этого фильтра в каждой пространственной позиции.
Теперь, если ваши фильтры имеют размер LxK, вы можете легко увидеть, что вы будетеиметь только одну возможную пространственную позицию (поскольку фильтр имеет тот же размер, что и последовательность), который будет точечным произведением между полным входным объемом и весами LxK для каждого фильтра.Различные фильтры, составляющие ваш Conv1d, теперь ведут себя так же, как и блоки, составляющие плотный слой: они полностью подключены к вашему входу.
Это поведение можно проверить с помощью следующего кода:
import tensorflow as tf
import numpy as np
l = 10
k = 2
n = 5
x = tf.placeholder(tf.float32, [None, l, k])
c = tf.layers.conv1d(inputs=x, strides=1, filters=n, kernel_size=l, kernel_initializer=tf.ones_initializer())
d = tf.layers.dense(inputs=tf.reshape(x, [-1, l*k]), units=n, kernel_initializer=tf.ones_initializer())
batch_size = 10
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
r_conv, r_dense = sess.run([c, d], {x: np.random.normal(size=[batch_size, l, k])})
print(r_conv.shape, r_dense.shape)
#(10, 1, 5) (10, 5)
print(np.allclose(r_conv.reshape([batch_size, -1]), r_dense.reshape([batch_size, -1])))
#True
При той же инициализации выходные данные действительно равны.
Что касается скорости, я полагаю, что одна из главных причин, почему Conv1d был быстрее и потреблял больше VRAM, была из-за изменения формы: вы фактически увеличивали размер пакета, улучшая распараллеливание за счет памяти.
Редактируйте после уточнения:
Возможно, я неправильно понял ваш вопрос.Метод 1 и метод 2 одинаковы, но они не совпадают с применением плотного слоя к Input = [B, LxK].
Здесь ваши выходы подключены к полному измерению K, и затем одни и те же веса используются для каждого временного шага вашей последовательности, что означает, что оба метода только полностью связаны с кадром, но не с последовательностью.Это на самом деле эквивалентно плотному слою на [BxL, K].
Вы можете проверить это поведение с помощью следующего кода:
l = 10
k = 2
n = 5
x = tf.placeholder(tf.float32, [None, l, k])
c2 = tf.layers.conv1d(inputs=x, strides=1, filters=n, kernel_size=1, kernel_initializer=tf.ones_initializer())
c3 = tf.layers.conv1d(inputs=tf.reshape(x, [-1, k, 1]), strides=1, filters=n, kernel_size=k, kernel_initializer=tf.ones_initializer())
d2 = tf.layers.dense(inputs=tf.reshape(x, [-1, k]), units=n, kernel_initializer=tf.ones_initializer())
batch_size = 10
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
r_d2, r_c2, r_c3 = sess.run([d2, c2, c3], {x: np.random.normal(size=[batch_size, l, k])})
r_d2 = r_d2.reshape([10, 10, 5])
r_c3 = r_c3.reshape([10, 10, 5])
print(r_d2.shape, r_c2.shape, r_c3.shape)
#(10, 10, 5) (10, 10, 5) (10, 10, 5)
print(np.allclose(r_d2, r_c2))
#True
print(np.allclose(r_d2, r_c3))
#True
print(np.allclose(r_c2, r_c3))
#True
Что касается скорости, то это должно быть потому, что в методе 1 для вычисления результата есть только одно точечное произведение, тогда как у вас есть Lв методе 2 + другие операции.