La documentation de l'opération conv2d_transpose()
n'explique pas clairement ce qu'elle fait:
La transposition de conv2d.
Cette opération est parfois appelée "déconvolution" après Réseaux déconvolutionnels , mais est en fait la transposition (gradient) de conv2d plutôt qu'une déconvolution réelle.
J'ai parcouru le papier que le doc indique, mais cela n'a pas aidé.
Que fait cette opération et quels sont les exemples de pourquoi vous voudriez l’utiliser?
C’est la meilleure explication que j’ai vue en ligne sur la façon dont la transposition par convolution fonctionne est ici .
Je vais donner ma propre courte description. Il applique la convolution avec une foulée fractionnelle. En d'autres termes, espacer les valeurs d'entrée (avec des zéros) pour appliquer le filtre sur une région potentiellement plus petite que la taille du filtre.
Quant à la raison pour laquelle on voudrait l'utiliser. Il peut être utilisé comme une sorte de suréchantillonnage avec des poids appris par opposition à une interpolation bilinéaire ou à une autre forme fixe de suréchantillonnage.
Voici un autre point de vue de la perspective "gradients", c'est-à-dire pourquoi la documentation de TensorFlow indique que conv2d_transpose()
est "en fait la transposition (gradient) de conv2d plutôt qu'une déconvolution réelle". Pour plus de détails sur le calcul effectué dans conv2d_transpose
, je recommanderais vivement cet article , à partir de la page 19.
Dans tf.nn
, il existe 4 fonctions étroitement liées et plutôt déroutantes pour la convolution 2d:
tf.nn.conv2d
tf.nn.conv2d_backprop_filter
tf.nn.conv2d_backprop_input
tf.nn.conv2d_transpose
Résumé en une phrase: elles ne sont que des 2èmes convolutions. Leurs différences sont dans l'ordre des arguments d'entrée, la rotation ou la transposition d'entrée, les foulées (y compris la taille de foulée fractionnelle), les bourrages, etc. Avec tf.nn.conv2d
, vous pouvez implémenter les 3 autres opérations en transformant les entrées et en modifiant les arguments conv2d
.
# forward
out = conv2d(x, w)
# backward, given d_out
=> find d_x?
=> find d_w?
Dans le calcul en avant, nous calculons la convolution de l'image d'entrée x
avec le filtre w
et le résultat est out
. Dans le calcul en arrière, supposons que nous recevons d_out
, qui est le gradient w.r.t. out
. Notre objectif est de trouver d_x
et d_w
, qui sont le dégradé w.r.t. x
et w
respectivement.
Pour faciliter la discussion, supposons:
1
in_channels
et out_channels
sont 1
VALID
Conceptuellement, avec les hypothèses ci-dessus, nous avons les relations suivantes:
out = conv2d(x, w, padding='VALID')
d_x = conv2d(d_out, rot180(w), padding='FULL')
d_w = conv2d(x, d_out, padding='VALID')
Où rot180
est une matrice 2D pivotée à 180 degrés (une inversion gauche-droite et une inversion vers le haut), FULL
signifie "appliquer un filtre chaque fois qu'il chevauche partiellement l'entrée" (voir theeano docs ). Note que ceci n'est valide qu'avec les hypothèses précédentes, cependant, on peut changer les arguments de conv2d pour le généraliser.
Les points à retenir:
d_x
est la convolution du gradient de sortie d_out
et du poids w
, avec quelques modifications.d_w
est la convolution de l'entrée x
et du gradient de sortie d_out
, avec quelques modifications.Maintenant, donnons un exemple de code de travail réel montrant comment utiliser les 4 fonctions ci-dessus pour calculer d_x
et d_w
étant donné d_out
. Cela montre comment conv2d
, conv2d_backprop_filter
, conv2d_backprop_input
, et conv2d_transpose
sont liés l'un à l'autre. Veuillez trouver les scripts complets ici .
Computing d_x
de 4 manières différentes:
# Method 1: TF's autodiff
d_x = tf.gradients(f, x)[0]
# Method 2: manually using conv2d
d_x_manual = tf.nn.conv2d(input=tf_pad_to_full_conv2d(d_out, w_size),
filter=tf_rot180(w),
strides=strides,
padding='VALID')
# Method 3: conv2d_backprop_input
d_x_backprop_input = tf.nn.conv2d_backprop_input(input_sizes=x_shape,
filter=w,
out_backprop=d_out,
strides=strides,
padding='VALID')
# Method 4: conv2d_transpose
d_x_transpose = tf.nn.conv2d_transpose(value=d_out,
filter=w,
output_shape=x_shape,
strides=strides,
padding='VALID')
Computing d_w
de 3 manières différentes:
# Method 1: TF's autodiff
d_w = tf.gradients(f, w)[0]
# Method 2: manually using conv2d
d_w_manual = tf_NHWC_to_HWIO(tf.nn.conv2d(input=x,
filter=tf_NHWC_to_HWIO(d_out),
strides=strides,
padding='VALID'))
# Method 3: conv2d_backprop_filter
d_w_backprop_filter = tf.nn.conv2d_backprop_filter(input=x,
filter_sizes=w_shape,
out_backprop=d_out,
strides=strides,
padding='VALID')
Veuillez consulter les scripts complets pour l’implémentation de tf_rot180
, tf_pad_to_full_conv2d
, tf_NHWC_to_HWIO
. Dans les scripts, nous vérifions que les valeurs de sortie finales de différentes méthodes sont les mêmes; une implémentation numpy est également disponible.
conv2d_transpose () transpose simplement les poids et les retourne à 180 degrés. Ensuite, il applique la norme conv2d (). "Transpose" signifie pratiquement qu'il change l'ordre des "colonnes" dans le tenseur de poids. Veuillez vérifier l'exemple ci-dessous.
Voici un exemple qui utilise des convolutions avec stride = 1 et padding = 'SAME'. C'est un cas simple, mais le même raisonnement pourrait être appliqué aux autres cas.
Disons que nous avons:
Si nous effectuons une convolution de l'entrée, les activations du auront alors une forme: [1,28,28,32].
activations = sess.run(h_conv1,feed_dict={x:np.reshape(image,[1,784])})
Où:
W_conv1 = weight_variable([7, 7, 1, 32])
b_conv1 = bias_variable([32])
h_conv1 = conv2d(x, W_conv1, strides=[1, 1, 1, 1], padding='SAME') + b_conv1
Pour obtenir la "déconvolution" ou la "convolution transposée", vous pouvez utiliser conv2d_transpose () sur les activations de convolution de la manière suivante:
deconv = conv2d_transpose(activations,W_conv1, output_shape=[1,28,28,1],padding='SAME')
OU en utilisant conv2d (), nous devons transposer et retourner les poids:
transposed_weights = tf.transpose(W_conv1, perm=[0, 1, 3, 2])
Ici nous changeons l'ordre des "colonnes" de [0,1,2,3] à [0,1,3,2]. Alors de [7, 7, 1, 32] nous obtiendrons un tenseur de forme = [7,7,32,1]. Ensuite, nous retournons les poids:
for i in range(n_filters):
# Flip the weights by 180 degrees
transposed_and_flipped_weights[:,:,i,0] = sess.run(tf.reverse(transposed_weights[:,:,i,0], axis=[0, 1]))
Ensuite, nous pouvons calculer la convolution avec conv2d () comme suit:
strides = [1,1,1,1]
deconv = conv2d(activations,transposed_and_flipped_weights,strides=strides,padding='SAME')
Et nous obtiendrons le même résultat qu'avant. De plus, le même résultat peut être obtenu avec conv2d_backprop_input () en utilisant:
deconv = conv2d_backprop_input([1,28,28,1],W_conv1,activations, strides=strides, padding='SAME')
Les résultats sont affichés ici:
Test de conv2d (), conv2d_tranposed () et conv2d_backprop_input ()
Nous pouvons voir que les résultats sont les mêmes. Pour mieux le voir, veuillez vérifier mon code sur:
https://github.com/simo23/conv2d_transpose
Ici, je réplique la sortie de la fonction conv2d_transpose () en utilisant la norme conv2d ().
Une application pour conv2d_transpose est la conversion ascendante. Voici un exemple qui explique son fonctionnement
a = np.array([[0, 0, 1.5],
[0, 1, 0],
[0, 0, 0]]).reshape(1,3,3,1)
filt = np.array([[1, 2],
[3, 4.0]]).reshape(2,2,1,1)
b = tf.nn.conv2d_transpose(a,
filt,
output_shape=[1,6,6,1],
strides=[1,2,2,1],
padding='SAME')
print(tf.squeeze(b))
tf.Tensor(
[[0. 0. 0. 0. 1.5 3. ]
[0. 0. 0. 0. 4.5 6. ]
[0. 0. 1. 2. 0. 0. ]
[0. 0. 3. 4. 0. 0. ]
[0. 0. 0. 0. 0. 0. ]
[0. 0. 0. 0. 0. 0. ]], shape=(6, 6), dtype=float64)