Que fait tf.nn.conv2d dans Tensorflow?


135

Je regardais la documentation de tensorflow tf.nn.conv2d ici . Mais je ne peux pas comprendre ce qu'il fait ou ce qu'il essaie de réaliser. Il dit sur les documents,

# 1: Aplatit le filtre en une matrice 2D avec forme

[filter_height * filter_width * in_channels, output_channels].

Maintenant qu'est-ce que cela fait? Est-ce une multiplication élémentaire ou simplement une multiplication matricielle? Je ne pouvais pas non plus comprendre les deux autres points mentionnés dans la documentation. Je les ai écrits ci-dessous:

# 2: Extrait des patchs d'image du tenseur d'entrée pour former un tenseur virtuel de forme

[batch, out_height, out_width, filter_height * filter_width * in_channels].

# 3: Pour chaque patch, multiplie à droite la matrice de filtre et le vecteur de patch d'image.

Ce serait vraiment utile si quelqu'un pouvait donner un exemple, un morceau de code (extrêmement utile) peut-être et expliquer ce qui se passe là-bas et pourquoi l'opération est comme ça.

J'ai essayé de coder une petite partie et d'imprimer la forme de l'opération. Pourtant, je ne comprends pas.

J'ai essayé quelque chose comme ça:

op = tf.shape(tf.nn.conv2d(tf.random_normal([1,10,10,10]), 
              tf.random_normal([2,10,10,10]), 
              strides=[1, 2, 2, 1], padding='SAME'))

with tf.Session() as sess:
    result = sess.run(op)
    print(result)

Je comprends des morceaux de réseaux de neurones convolutifs. Je les ai étudiés ici . Mais l'implémentation sur tensorflow n'est pas ce à quoi je m'attendais. Cela a donc soulevé la question.

EDIT : Donc, j'ai implémenté un code beaucoup plus simple. Mais je ne peux pas comprendre ce qui se passe. Je veux dire comment les résultats sont comme ça. Il serait extrêmement utile que quelqu'un puisse me dire quel processus produit ce résultat.

input = tf.Variable(tf.random_normal([1,2,2,1]))
filter = tf.Variable(tf.random_normal([1,1,1,1]))

op = tf.nn.conv2d(input, filter, strides=[1, 1, 1, 1], padding='SAME')
init = tf.initialize_all_variables()
with tf.Session() as sess:
    sess.run(init)

    print("input")
    print(input.eval())
    print("filter")
    print(filter.eval())
    print("result")
    result = sess.run(op)
    print(result)

production

input
[[[[ 1.60314465]
   [-0.55022103]]

  [[ 0.00595062]
   [-0.69889867]]]]
filter
[[[[-0.59594476]]]]
result
[[[[-0.95538563]
   [ 0.32790133]]

  [[-0.00354624]
   [ 0.41650501]]]]

En fait, cudnn est activé par défaut sur le GPU dans tf.nn.conv2d(), donc la méthode en question n'est pas du tout utilisée lorsque nous utilisons TF avec le support GPU, sauf si cela use_cudnn_on_gpu=Falseest spécifié explicitement.
gkcn

Réponses:


59

La convolution 2D est calculée de la même manière que l'on calculerait la convolution 1D : vous faites glisser votre noyau sur l'entrée, calculez les multiplications par élément et additionnez-les. Mais au lieu que votre noyau / entrée soit un tableau, ce sont ici des matrices.


Dans l'exemple le plus élémentaire, il n'y a pas de rembourrage et stride = 1. Supposons que vous inputet kernelêtes: entrez la description de l'image ici

Lorsque vous utilisez votre noyau, vous recevrez la sortie suivante:, entrez la description de l'image iciqui est calculée de la manière suivante:

  • 14 = 4 * 1 + 3 * 0 + 1 * 1 + 2 * 2 + 1 * 1 + 0 * 0 + 1 * 0 + 2 * 0 + 4 * 1
  • 6 = 3 * 1 + 1 * 0 + 0 * 1 + 1 * 2 + 0 * 1 + 1 * 0 + 2 * 0 + 4 * 0 + 1 * 1
  • 6 = 2 * 1 + 1 * 0 + 0 * 1 + 1 * 2 + 2 * 1 + 4 * 0 + 3 * 0 + 1 * 0 + 0 * 1
  • 12 = 1 * 1 + 0 * 0 + 1 * 1 + 2 * 2 + 4 * 1 + 1 * 0 + 1 * 0 + 0 * 0 + 2 * 1

La fonction conv2d de TF calcule les convolutions par lots et utilise un format légèrement différent. Pour une entrée, c'est [batch, in_height, in_width, in_channels]pour le noyau [filter_height, filter_width, in_channels, out_channels]. Nous devons donc fournir les données dans le bon format:

import tensorflow as tf
k = tf.constant([
    [1, 0, 1],
    [2, 1, 0],
    [0, 0, 1]
], dtype=tf.float32, name='k')
i = tf.constant([
    [4, 3, 1, 0],
    [2, 1, 0, 1],
    [1, 2, 4, 1],
    [3, 1, 0, 2]
], dtype=tf.float32, name='i')
kernel = tf.reshape(k, [3, 3, 1, 1], name='kernel')
image  = tf.reshape(i, [1, 4, 4, 1], name='image')

Ensuite, la convolution est calculée avec:

res = tf.squeeze(tf.nn.conv2d(image, kernel, [1, 1, 1, 1], "VALID"))
# VALID means no padding
with tf.Session() as sess:
   print sess.run(res)

Et sera équivalent à celui que nous avons calculé à la main.


Pour des exemples de rembourrage / foulées, jetez un œil ici .


Bel exemple, cependant certains liens sont rompus.
silgon

1
@silgon, malheureusement, c'est parce que SO a décidé de ne pas prendre en charge la fonctionnalité de documentation qu'ils ont créée et annoncée au début.
Salvador Dali

161

Ok, je pense que c'est la façon la plus simple de tout expliquer.


Votre exemple est 1 image, taille 2x2, avec 1 canal. Vous avez 1 filtre, de taille 1x1, et 1 canal (la taille est hauteur x largeur x canaux x nombre de filtres).

Pour ce cas simple, l'image résultante 2x2, 1 canal (taille 1x2x2x1, nombre d'images x hauteur x largeur xx canaux) est le résultat de la multiplication de la valeur de filtre par chaque pixel de l'image.


Essayons maintenant plus de canaux:

input = tf.Variable(tf.random_normal([1,3,3,5]))
filter = tf.Variable(tf.random_normal([1,1,5,1]))

op = tf.nn.conv2d(input, filter, strides=[1, 1, 1, 1], padding='VALID')

Ici, l'image 3x3 et le filtre 1x1 ont chacun 5 canaux. L'image résultante sera 3x3 avec 1 canal (taille 1x3x3x1), où la valeur de chaque pixel est le produit scalaire sur les canaux du filtre avec le pixel correspondant dans l'image d'entrée.


Maintenant avec un filtre 3x3

input = tf.Variable(tf.random_normal([1,3,3,5]))
filter = tf.Variable(tf.random_normal([3,3,5,1]))

op = tf.nn.conv2d(input, filter, strides=[1, 1, 1, 1], padding='VALID')

Ici, nous obtenons une image 1x1, avec 1 canal (taille 1x1x1x1). La valeur est la somme des 9 produits scalaires à 5 éléments. Mais vous pouvez simplement appeler cela un produit scalaire à 45 éléments.


Maintenant avec une image plus grande

input = tf.Variable(tf.random_normal([1,5,5,5]))
filter = tf.Variable(tf.random_normal([3,3,5,1]))

op = tf.nn.conv2d(input, filter, strides=[1, 1, 1, 1], padding='VALID')

La sortie est une image 3x3 à 1 canal (taille 1x3x3x1). Chacune de ces valeurs est une somme de 9 produits scalaires à 5 éléments.

Chaque sortie est effectuée en centrant le filtre sur l'un des 9 pixels centraux de l'image d'entrée, de sorte qu'aucun filtre ne dépasse. Les xs ci-dessous représentent les centres de filtre pour chaque pixel de sortie.

.....
.xxx.
.xxx.
.xxx.
.....

Maintenant avec le rembourrage "SAME":

input = tf.Variable(tf.random_normal([1,5,5,5]))
filter = tf.Variable(tf.random_normal([3,3,5,1]))

op = tf.nn.conv2d(input, filter, strides=[1, 1, 1, 1], padding='SAME')

Cela donne une image de sortie 5x5 (taille 1x5x5x1). Cela se fait en centrant le filtre à chaque position sur l'image.

Tous les produits scalaires à 5 éléments où le filtre dépasse le bord de l'image obtiennent une valeur de zéro.

Les coins ne sont donc que des sommes de produits scalaires à 4 ou 5 éléments.


Maintenant avec plusieurs filtres.

input = tf.Variable(tf.random_normal([1,5,5,5]))
filter = tf.Variable(tf.random_normal([3,3,5,7]))

op = tf.nn.conv2d(input, filter, strides=[1, 1, 1, 1], padding='SAME')

Cela donne toujours une image de sortie 5x5, mais avec 7 canaux (taille 1x5x5x7). Où chaque canal est produit par l'un des filtres de l'ensemble.


Maintenant avec des foulées 2,2:

input = tf.Variable(tf.random_normal([1,5,5,5]))
filter = tf.Variable(tf.random_normal([3,3,5,7]))

op = tf.nn.conv2d(input, filter, strides=[1, 2, 2, 1], padding='SAME')

Maintenant, le résultat a toujours 7 canaux, mais n'est que de 3x3 (taille 1x3x3x7).

En effet, au lieu de centrer les filtres en chaque point de l'image, les filtres sont centrés en un autre point de l'image, en effectuant des pas (enjambées) de largeur 2. Les xs ci-dessous représentent le centre du filtre pour chaque pixel de sortie, sur l'image d'entrée.

x.x.x
.....
x.x.x
.....
x.x.x

Et bien sûr, la première dimension de l'entrée est le nombre d'images afin que vous puissiez l'appliquer sur un lot de 10 images, par exemple:

input = tf.Variable(tf.random_normal([10,5,5,5]))
filter = tf.Variable(tf.random_normal([3,3,5,7]))

op = tf.nn.conv2d(input, filter, strides=[1, 2, 2, 1], padding='SAME')

Cela effectue la même opération, pour chaque image indépendamment, donnant une pile de 10 images comme résultat (taille 10x3x3x7)


@ZijunLost Non, la documentation indique que le premier et le dernier élément doivent être 1.Must have strides[0] = strides[3] = 1. For the most common case of the same horizontal and vertices strides, strides = [1, stride, stride, 1].
JohnAllen

Cette implémentation de la convolution est-elle basée sur la matrice Toeplitz ?
gkcn

A ce propos: "Cela donne toujours une image de sortie 5x5, mais avec 7 canaux (taille 1x5x5x7). Où chaque canal est produit par l'un des filtres de l'ensemble.", J'ai encore du mal à comprendre d'où viennent les 7 canaux? que voulez-vous dire "filtres dans l'ensemble"? Merci.
derek

@mdaoust Bonjour, concernant votre deuxième exemple où the 3x3 image and the 1x1 filter each have 5 channels, je trouve que le résultat est différent du produit scalaire calculé manuellement.
Tgn Yang

1
@derek J'ai la même question, est-ce que le "canal de sortie" est identique au "nombre de filtres" ??? si oui, pourquoi sont-ils nommés "output_channel" dans la documentation tensorflow?
Wei

11

Juste pour ajouter aux autres réponses, vous devriez penser aux paramètres dans

filter = tf.Variable(tf.random_normal([3,3,5,7]))

comme «5» correspondant au nombre de canaux dans chaque filtre. Chaque filtre est un cube 3D, d'une profondeur de 5. La profondeur de votre filtre doit correspondre à la profondeur de votre image d'entrée. Le dernier paramètre, 7, doit être considéré comme le nombre de filtres dans le lot. Oubliez simplement qu'il s'agit de 4D, et imaginez à la place que vous avez un ensemble ou un lot de 7 filtres. Ce que vous faites est de créer 7 cubes de filtre avec des dimensions (3,3,5).

Il est beaucoup plus facile de visualiser dans le domaine de Fourier puisque la convolution devient une multiplication ponctuelle. Pour une image d'entrée de dimensions (100,100,3), vous pouvez réécrire les dimensions du filtre comme

filter = tf.Variable(tf.random_normal([100,100,3,7]))

Afin d'obtenir l'une des 7 cartes d'entités en sortie, nous effectuons simplement la multiplication ponctuelle du cube de filtre avec le cube d'image, puis nous additionnons les résultats à travers la dimension canaux / profondeur (ici c'est 3), en nous effondrant en 2d (100,100) carte des caractéristiques. Faites cela avec chaque cube de filtre et vous obtenez 7 cartes d'entités 2D.


8

J'ai essayé d'implémenter conv2d (pour mes études). Eh bien, j'ai écrit ça:

def conv(ix, w):
   # filter shape: [filter_height, filter_width, in_channels, out_channels]
   # flatten filters
   filter_height = int(w.shape[0])
   filter_width = int(w.shape[1])
   in_channels = int(w.shape[2])
   out_channels = int(w.shape[3])
   ix_height = int(ix.shape[1])
   ix_width = int(ix.shape[2])
   ix_channels = int(ix.shape[3])
   filter_shape = [filter_height, filter_width, in_channels, out_channels]
   flat_w = tf.reshape(w, [filter_height * filter_width * in_channels, out_channels])
   patches = tf.extract_image_patches(
       ix,
       ksizes=[1, filter_height, filter_width, 1],
       strides=[1, 1, 1, 1],
       rates=[1, 1, 1, 1],
       padding='SAME'
   )
   patches_reshaped = tf.reshape(patches, [-1, ix_height, ix_width, filter_height * filter_width * ix_channels])
   feature_maps = []
   for i in range(out_channels):
       feature_map = tf.reduce_sum(tf.multiply(flat_w[:, i], patches_reshaped), axis=3, keep_dims=True)
       feature_maps.append(feature_map)
   features = tf.concat(feature_maps, axis=3)
   return features

J'espère que je l'ai fait correctement. Vérifié sur MNIST, a eu des résultats très proches (mais cette mise en œuvre est plus lente). J'espère que ceci vous aide.


0

En plus d'autres réponses, l'opération conv2d fonctionne en c ++ (cpu) ou cuda pour les machines gpu qui nécessitent d'aplatir et de remodeler les données d'une certaine manière et d'utiliser la multiplication de matrice gemmBLAS ou cuBLAS (cuda).


Ainsi, dans la mémoire, la convolution est en fait exécutée comme une multiplication matricielle, ce qui explique pourquoi les images plus grandes ne fonctionnent pas nécessairement avec un temps de calcul plus long, mais sont plutôt plus susceptibles de générer une erreur OOM (mémoire insuffisante). Pouvez-vous m'expliquer pourquoi la convolution 3D est plus inefficace / efficace en mémoire que la convolution 2D? Par exemple, faire une convocation 3D sur [B, H, W, D, C] par rapport à une convocation 2D sur [B * C, H, W, D]. Sûrement, ils coûtent le même prix en calcul?
SomePhysicsStudent le
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.