¿Qué hace tf.nn.conv2d en tensorflow?

135

Estaba mirando los documentos de Tensorflow por tf.nn.conv2d aquí . Pero no puedo entender lo que hace o lo que está tratando de lograr. Dice en los documentos,

# 1: aplana el filtro a una matriz 2D con forma

[filter_height * filter_width * in_channels, output_channels].

¿Y ahora qué hace eso? ¿Es esa multiplicación por elementos o simplemente multiplicación de matriz simple? Tampoco pude entender los otros dos puntos mencionados en los documentos. Los he escrito a continuación:

# 2: extrae parches de imagen del tensor de entrada para formar un tensor de forma virtual

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

# 3: Para cada parche, multiplica a la derecha la matriz del filtro y el vector del parche de imagen.

Sería realmente útil si alguien pudiera dar un ejemplo, un fragmento de código (extremadamente útil) tal vez y explicar qué está sucediendo allí y por qué la operación es así.

He intentado codificar una pequeña porción e imprimir la forma de la operación. Aún así, no puedo entender.

Intenté algo como esto:

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)

Entiendo partes de redes neuronales convolucionales. Los estudié aquí . Pero la implementación en tensorflow no es lo que esperaba. Entonces planteó la pregunta.

EDITAR : Entonces, implementé un código mucho más simple. Pero no puedo entender qué está pasando. Me refiero a cómo son los resultados así. Sería extremadamente útil si alguien pudiera decirme qué proceso produce este resultado.

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)

salida

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

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

  [[-0.00354624]
   [ 0.41650501]]]]
Shubhashis
fuente
En realidad, cudnn está habilitado de forma predeterminada en GPU in tf.nn.conv2d(), por lo que el método en cuestión no se usa en absoluto cuando usamos TF con soporte de GPU, a menos que use_cudnn_on_gpu=Falsese especifique explícitamente.
gkcn

Respuestas:

59

La convolución 2D se calcula de forma similar a la que se calcularía la convolución 1D : desliza su núcleo sobre la entrada, calcula las multiplicaciones por elementos y las suma. Pero en lugar de que su núcleo / entrada sea una matriz, aquí están las matrices.


En el ejemplo más básico no hay relleno y zancada = 1. Asumamos tu inputy kernelson: ingrese la descripción de la imagen aquí

Cuando use su núcleo, recibirá el siguiente resultado: ingrese la descripción de la imagen aquíque se calcula de la siguiente manera:

  • 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 función conv2d de TF calcula las convoluciones en lotes y utiliza un formato ligeramente diferente. Para una entrada es [batch, in_height, in_width, in_channels]para el núcleo que es [filter_height, filter_width, in_channels, out_channels]. Por lo tanto, debemos proporcionar los datos en el formato correcto:

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

Luego, la convolución se calcula 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)

Y será equivalente al que calculamos a mano.


Para ejemplos con relleno / zancadas, eche un vistazo aquí .

Salvador Dalí
fuente
Buen ejemplo, sin embargo, algunos enlaces están rotos.
silgon
1
@silgon lamentablemente esto se debe a que SO decidió no admitir la función de documentación que crearon y anunciaron al principio.
Salvador Dali
161

Ok, creo que esta es la forma más sencilla de explicarlo todo.


Su ejemplo es 1 imagen, tamaño 2x2, con 1 canal. Tiene 1 filtro, con tamaño 1x1, y 1 canal (el tamaño es alto x ancho x canales x número de filtros).

Para este caso simple, la imagen resultante de 2x2 y 1 canal (tamaño 1x2x2x1, número de imágenes x alto x ancho xx canales) es el resultado de multiplicar el valor del filtro por cada píxel de la imagen.


Ahora intentemos más canales:

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

Aquí la imagen 3x3 y el filtro 1x1 tienen 5 canales cada uno. La imagen resultante será 3x3 con 1 canal (tamaño 1x3x3x1), donde el valor de cada píxel es el producto escalar a través de los canales del filtro con el píxel correspondiente en la imagen de entrada.


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

Aquí obtenemos una imagen 1x1, con 1 canal (tamaño 1x1x1x1). El valor es la suma de los 9 productos de punto de 5 elementos. Pero podría llamarlo un producto de punto de 45 elementos.


Ahora con una imagen más 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 salida es una imagen 3x3 de 1 canal (tamaño 1x3x3x1). Cada uno de estos valores es una suma de 9 productos de puntos de 5 elementos.

Cada salida se realiza centrando el filtro en uno de los 9 píxeles centrales de la imagen de entrada, de modo que ninguno de los filtros sobresalga. Las xsiguientes s representan los centros de filtro para cada píxel de salida.

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

Ahora con el relleno "MISMO":

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

Esto proporciona una imagen de salida de 5x5 (tamaño 1x5x5x1). Esto se hace centrando el filtro en cada posición de la imagen.

Cualquiera de los productos de puntos de 5 elementos donde el filtro sobresale más allá del borde de la imagen obtiene un valor de cero.

Por lo tanto, las esquinas son solo sumas de 4 productos de punto de 5 elementos.


Ahora con múltiples filtros.

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

Esto todavía da una imagen de salida de 5x5, pero con 7 canales (tamaño 1x5x5x7). Donde cada canal es producido por uno de los filtros en el conjunto.


Ahora con zancadas 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')

Ahora el resultado todavía tiene 7 canales, pero es solo 3x3 (tamaño 1x3x3x7).

Esto se debe a que en lugar de centrar los filtros en cada punto de la imagen, los filtros se centran en cualquier otro punto de la imagen, tomando pasos (zancadas) de ancho 2. Los xsiguientes representan el centro del filtro para cada píxel de salida, en La imagen de entrada.

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

Y, por supuesto, la primera dimensión de la entrada es la cantidad de imágenes para que pueda aplicarla en un lote de 10 imágenes, por ejemplo:

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

Esto realiza la misma operación, para cada imagen de forma independiente, dando como resultado una pila de 10 imágenes (tamaño 10x3x3x7)

mdaoust
fuente
@ZijunLost No, los documentos indican que el primer y último elemento debe ser 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
¿Es esta la implementación de convolución basada en matriz de Toeplitz ?
gkcn
Con respecto a esto: "Esto todavía da una imagen de salida de 5x5, pero con 7 canales (tamaño 1x5x5x7). Donde cada canal es producido por uno de los filtros en el conjunto", ¿todavía tengo dificultades para entender de dónde provienen los 7 canales? ¿Qué quieres decir con "filtros en el conjunto"? Gracias.
Derek
@mdaoust Hola, con respecto a su segundo ejemplo donde the 3x3 image and the 1x1 filter each have 5 channels, encuentro que el resultado es diferente del producto de punto calculado manualmente.
Tgn Yang
1
@derek Tengo la misma pregunta, ¿el "output_channel" es lo mismo que "número de filtros"? Si es así, ¿por qué se denominan "output_channel" en los documentos de Tensorflow?
Wei
11

Solo para agregar a las otras respuestas, debe pensar en los parámetros en

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

como '5' correspondiente al número de canales en cada filtro. Cada filtro es un cubo 3D, con una profundidad de 5. La profundidad de su filtro debe corresponder con la profundidad de su imagen de entrada. El último parámetro, 7, debe considerarse como el número de filtros en el lote. Solo olvídate de que esto es 4D y, en cambio, imagina que tienes un conjunto o un lote de 7 filtros. Lo que debe hacer es crear 7 cubos de filtro con dimensiones (3,3,5).

Es mucho más fácil visualizar en el dominio de Fourier ya que la convolución se convierte en multiplicación puntual. Para una imagen de entrada de dimensiones (100,100,3) puede reescribir las dimensiones del filtro como

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

Para obtener uno de los 7 mapas de características de salida, simplemente realizamos la multiplicación puntual del cubo del filtro con el cubo de la imagen, luego sumamos los resultados a través de los canales / dimensión de profundidad (aquí es 3), colapsando a 2d (100,100) mapa de características. Haga esto con cada cubo de filtro, y obtendrá 7 mapas de características 2D.

Val9265
fuente
8

Traté de implementar conv2d (para mis estudios). Bueno, escribí eso:

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

Espero haberlo hecho bien. Comprobado en MNIST, tuvo resultados muy cercanos (pero esta implementación es más lenta). Espero que esto te ayude.

Artem Yaschenko
fuente
0

Además de otras respuestas, la operación conv2d está operando en c ++ (cpu) o cuda para máquinas gpu que requiere aplanar y remodelar datos de cierta manera y usar la multiplicación de matrices gemmBLAS o cuBLAS (cuda).

karaspd
fuente
Entonces, en la memoria, la convolución en realidad se realiza como una multiplicación de matriz que explica por qué las imágenes más grandes no necesariamente se ejecutan en un tiempo de cálculo mayor, sino que es más probable que se encuentren con un error OOM (sin memoria). ¿Puede explicarme por qué la convolución 3D es más ineficiente / eficiente en comparación con la convolución 2D? Por ejemplo, hacer conv 3D en [B, H, W, D, C] en comparación con conv 2D en [B * C, H, W, D]. ¿Seguramente, cuestan computacionalmente lo mismo?
SomePhysicsStudent