Cosa fa tf.nn.conv2d in tensorflow?


135

Stavo guardando i documenti di Tensorflow tf.nn.conv2d qui intorno . Ma non riesco a capire cosa fa o cosa cerca di raggiungere. Dice sui documenti,

# 1: appiattisce il filtro su una matrice 2D con forma

[filter_height * filter_width * in_channels, output_channels].

Ora che cosa fa? È una moltiplicazione saggia o semplicemente una moltiplicazione a matrice? Inoltre non riuscivo a capire gli altri due punti menzionati nei documenti. Le ho scritte di seguito:

# 2: estrae le patch di immagine dal tensore di input per formare un tensore virtuale di forma

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

# 3: per ogni patch, moltiplica a destra la matrice del filtro e il vettore della patch dell'immagine.

Sarebbe davvero utile se qualcuno potesse dare un esempio, un pezzo di codice (estremamente utile) forse e spiegare cosa sta succedendo lì e perché l'operazione è così.

Ho provato a codificare una piccola porzione e a stampare la forma dell'operazione. Tuttavia, non riesco a capire.

Ho provato qualcosa del genere:

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)

Capisco frammenti di reti neurali convoluzionali. Li ho studiati qui . Ma l'implementazione su tensorflow non è quella che mi aspettavo. Quindi ha sollevato la domanda.

EDIT : Quindi, ho implementato un codice molto più semplice. Ma non riesco a capire cosa sta succedendo. Voglio dire come i risultati sono così. Sarebbe estremamente utile se qualcuno potesse dirmi quale processo produce questo output.

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)

produzione

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

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

  [[-0.00354624]
   [ 0.41650501]]]]

In realtà cudnn è abilitato di default su GPU in tf.nn.conv2d(), quindi il metodo in questione non è affatto usato quando usiamo TF con supporto GPU, a meno che non use_cudnn_on_gpu=Falsesia specificato esplicitamente.
gkcn,

Risposte:


59

La convoluzione 2D viene calcolata in un modo simile per calcolare la convoluzione 1D : si fa scorrere il kernel sull'input, si calcolano le moltiplicazioni in termini di elementi e si sommano. Ma invece che il tuo kernel / input sia un array, qui sono delle matrici.


Nell'esempio più semplice non c'è imbottitura e stride = 1. Supponiamo che tu sia inpute kernel: inserisci qui la descrizione dell'immagine

Quando usi il tuo kernel riceverai il seguente output:, inserisci qui la descrizione dell'immagineche viene calcolato nel modo seguente:

  • 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 funzione conv2d di TF calcola le convoluzioni in lotti e utilizza un formato leggermente diverso. Per un input è [batch, in_height, in_width, in_channels]per il kernel che è [filter_height, filter_width, in_channels, out_channels]. Quindi dobbiamo fornire i dati nel formato corretto:

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')

Successivamente la convoluzione viene calcolata con:

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)

E sarà equivalente a quello che abbiamo calcolato a mano.


Per esempi con imbottitura / falcate, dai un'occhiata qui .


Bell'esempio, tuttavia alcuni collegamenti sono interrotti.
silgon,

1
@silgon è tristemente perché SO ha deciso di non supportare la funzione di documentazione che ha creato e pubblicizzato all'inizio.
Salvador Dali,

161

Ok, penso che questo sia il modo più semplice per spiegare tutto.


Il tuo esempio è 1 immagine, dimensione 2x2, con 1 canale. Hai 1 filtro, con dimensioni 1x1 e 1 canale (la dimensione è altezza x larghezza x canali x numero di filtri).

Per questo semplice caso l'immagine risultante 2x2, 1 canale (dimensioni 1x2x2x1, numero di immagini x altezza x larghezza xx canali) è il risultato della moltiplicazione del valore del filtro per ciascun pixel dell'immagine.


Ora proviamo più canali:

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')

Qui l'immagine 3x3 e il filtro 1x1 hanno 5 canali ciascuno. L'immagine risultante sarà 3x3 con 1 canale (dimensione 1x3x3x1), dove il valore di ciascun pixel è il prodotto punto attraverso i canali del filtro con il pixel corrispondente nell'immagine di input.


Ora con un filtro 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')

Qui otteniamo un'immagine 1x1, con 1 canale (dimensione 1x1x1x1). Il valore è la somma dei prodotti punto 9, 5 elementi. Ma potresti semplicemente chiamarlo un prodotto a punti di 45 elementi.


Ora con un'immagine più 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')

L'output è un'immagine a 3 canali 3x3 (dimensioni 1x3x3x1). Ognuno di questi valori è una somma di 9, punti con 5 elementi.

Ogni output viene realizzato centrando il filtro su uno dei 9 pixel centrali dell'immagine di input, in modo che nessuno dei filtri sporga. Le xs seguenti rappresentano i centri di filtro per ciascun pixel di output.

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

Ora con imbottitura "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')

Ciò fornisce un'immagine in uscita 5x5 (dimensioni 1x5x5x1). Questo viene fatto centrando il filtro in ciascuna posizione sull'immagine.

Qualsiasi prodotto a punti a 5 elementi in cui il filtro sporge oltre il bordo dell'immagine ottiene un valore pari a zero.

Quindi gli angoli sono solo somme di prodotti a punti a 4, 5 elementi.


Ora con più filtri.

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')

Ciò fornisce comunque un'immagine in uscita 5x5, ma con 7 canali (dimensioni 1x5x5x7). Dove ogni canale è prodotto da uno dei filtri nel set.


Ora con passi 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')

Ora il risultato ha ancora 7 canali, ma è solo 3x3 (dimensione 1x3x3x7).

Questo perché invece di centrare i filtri in ogni punto dell'immagine, i filtri sono centrati in ogni altro punto sull'immagine, facendo passi (passi) di larghezza 2. I xseguenti rappresentano il centro del filtro per ogni pixel di output, su l'immagine di input.

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

E ovviamente la prima dimensione dell'input è il numero di immagini in modo da poterlo applicare su un batch di 10 immagini, ad esempio:

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')

Ciò esegue la stessa operazione, per ogni immagine in modo indipendente, dando come risultato una pila di 10 immagini (dimensioni 10x3x3x7)


@ZijunLost No, i documenti affermano che il primo e l'ultimo elemento devono essere 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,

Questa implementazione della convoluzione basata sulla matrice di Toeplitz è ?
gkcn,

Riguardo a questo: "Questo dà ancora un'immagine in uscita 5x5, ma con 7 canali (dimensione 1x5x5x7). Dove ogni canale è prodotto da uno dei filtri nel set.", Ho ancora difficoltà a capire da dove provengono i 7 canali? cosa intendi con "filtri nel set"? Grazie.
Derek,

@mdaoust Ciao, per quanto riguarda il tuo secondo esempio in cui the 3x3 image and the 1x1 filter each have 5 channels, trovo che il risultato sia diverso dal prodotto punto calcolato manualmente.
Tgn Yang,

1
@derek Ho la stessa domanda, "output_channel" equivale a "numero di filtri" ??? in tal caso, perché sono chiamati "output_channel" nei documenti di Tensorflow?
Wei

11

Solo per aggiungere alle altre risposte, dovresti pensare ai parametri in

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

come '5' corrispondente al numero di canali in ciascun filtro. Ogni filtro è un cubo 3d, con una profondità di 5. La profondità del filtro deve corrispondere alla profondità dell'immagine di input. L'ultimo parametro, 7, dovrebbe essere considerato come il numero di filtri nel batch. Dimentica che questo è 4D e immagina invece di avere un set o un batch di 7 filtri. Quello che fai è creare 7 cubi filtro con dimensioni (3,3,5).

È molto più facile da visualizzare nel dominio di Fourier poiché la convoluzione diventa una moltiplicazione puntuale. Per un'immagine di input di dimensioni (100.100,3) è possibile riscrivere le dimensioni del filtro come

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

Per ottenere una delle 7 mappe delle caratteristiche di output, eseguiamo semplicemente la moltiplicazione puntuale del cubo filtro con il cubo immagine, quindi sommiamo i risultati attraverso i canali / dimensione della profondità (qui è 3), crollando a 2d (100.100) mappa delle caratteristiche. Fallo con ciascun cubo filtro e otterrai 7 mappe caratteristiche 2D.


8

Ho provato a implementare conv2d (per il mio studio). Bene, ho scritto che:

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

Spero di averlo fatto correttamente. Controllato su MNIST, ha avuto risultati molto vicini (ma questa implementazione è più lenta). Spero che questo ti aiuta.


0

Oltre ad altre risposte, l'operazione conv2d sta funzionando in c ++ (cpu) o cuda per macchine gpu che richiede di appiattire e rimodellare i dati in un certo modo e utilizzare la moltiplicazione della matrice gemmBLAS o cuBLAS (cuda).


Quindi, nella memoria, la convoluzione viene effettivamente eseguita come una moltiplicazione matriciale, il che spiega perché le immagini più grandi non vengono eseguite necessariamente in tempi di calcolo maggiori ma invece hanno maggiori probabilità di incorrere in errori OOM (memoria insufficiente). Potete spiegarmi perché la convoluzione 3D è più memoria inefficiente / efficiente rispetto alla convoluzione 2D? Ad esempio facendo conv 3D su [B, H, W, D, C] rispetto a conv 2D su [B * C, H, W, D]. Sicuramente, computazionalmente costano lo stesso?
SomePhysicsStudent,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.