O que o tf.nn.conv2d faz no fluxo tensor?


135

Eu estava olhando os documentos do tensorflow tf.nn.conv2d aqui . Mas não consigo entender o que faz ou o que está tentando alcançar. Diz nos documentos,

# 1: Nivela o filtro para uma matriz 2-D com forma

[filter_height * filter_width * in_channels, output_channels].

Agora o que isso faz? Isso é multiplicação por elementos ou simplesmente multiplicação por matriz? Também não consegui entender os outros dois pontos mencionados nos documentos. Eu os escrevi abaixo:

# 2: Extrai amostras de imagens do tensor de entrada para formar um tensor virtual de forma

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

Nº 3: para cada patch, multiplica à direita a matriz do filtro e o vetor de patch da imagem.

Seria realmente útil se alguém pudesse dar um exemplo, um pedaço de código (extremamente útil) talvez e explicar o que está acontecendo lá e por que a operação é assim.

Tentei codificar uma pequena parte e imprimir a forma da operação. Ainda assim, eu não consigo entender.

Eu tentei algo assim:

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)

Entendo pedaços de redes neurais convolucionais. Eu os estudei aqui . Mas a implementação no tensorflow não é o que eu esperava. Por isso, levantou a questão.

EDIT : Então, eu implementei um código muito mais simples. Mas não consigo descobrir o que está acontecendo. Quero dizer como os resultados são assim. Seria extremamente útil se alguém pudesse me dizer qual processo produz essa saída.

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)

resultado

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

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

  [[-0.00354624]
   [ 0.41650501]]]]

Na verdade, cudnn é ativado por padrão na GPU in tf.nn.conv2d(), portanto o método em questão não é usado quando usamos o TF com suporte à GPU, a menos que use_cudnn_on_gpu=Falseseja especificado explicitamente.
gkcn

Respostas:


59

A convolução 2D é calculada de maneira semelhante ao cálculo da convolução 1D : você desliza seu kernel sobre a entrada, calcula as multiplicações em elementos e as soma. Mas ao invés de seu kernel / input ser uma matriz, aqui estão elas.


No exemplo mais básico, não há preenchimento e passada = 1. Vamos assumir o seu inpute kernelsão: insira a descrição da imagem aqui

Ao usar seu kernel, você receberá a seguinte saída:, insira a descrição da imagem aquique é calculada da seguinte maneira:

  • 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

A função conv2d do TF calcula as convoluções em lotes e usa um formato ligeiramente diferente. Para uma entrada, é [batch, in_height, in_width, in_channels]para o kernel que é [filter_height, filter_width, in_channels, out_channels]. Portanto, precisamos fornecer os dados no formato correto:

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

Posteriormente, a convolução é calculada com:

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 será equivalente ao que calculamos à mão.


Para exemplos com preenchimento / passada, dê uma olhada aqui .


Bom exemplo, porém alguns links estão quebrados.
silgon

1
Infelizmente, isso é porque a SO decidiu não oferecer suporte ao recurso de documentação que eles criaram e anunciaram inicialmente.
Salvador Dali

161

Ok, acho que essa é a maneira mais simples de explicar tudo.


Seu exemplo é 1 imagem, tamanho 2x2, com 1 canal. Você tem 1 filtro, com tamanho 1x1 e 1 canal (tamanho é altura x largura x canais x número de filtros).

Nesse caso simples, a imagem 2x2, 1 canal resultante (tamanho 1x2x2x1, número de imagens x altura x largura xx canais) é o resultado da multiplicação do valor do filtro por cada pixel da imagem.


Agora vamos tentar mais canais:

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

Aqui, a imagem 3x3 e o filtro 1x1 possuem 5 canais. A imagem resultante será 3x3 com 1 canal (tamanho 1x3x3x1), em que o valor de cada pixel é o produto escalar através dos canais do filtro com o pixel correspondente na imagem de entrada.


Agora com um 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')

Aqui temos uma imagem 1x1, com 1 canal (tamanho 1x1x1x1). O valor é a soma dos produtos com 9 pontos e 5 elementos. Mas você pode chamar isso de um produto com 45 elementos.


Agora com uma imagem maior

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

A saída é uma imagem 3x3 de 1 canal (tamanho 1x3x3x1). Cada um desses valores é uma soma de 9 pontos com 5 elementos.

Cada saída é feita centralizando o filtro em um dos 9 pixels centrais da imagem de entrada, para que nenhum filtro se destaque. Os xs abaixo representam os centros de filtro para cada pixel de saída.

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

Agora com o preenchimento "MESMO":

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

Isso fornece uma imagem de saída 5x5 (tamanho 1x5x5x1). Isso é feito centralizando o filtro em cada posição da imagem.

Qualquer um dos produtos com 5 elementos onde o filtro ultrapassa a borda da imagem obtém um valor zero.

Portanto, os cantos são apenas somas de produtos com 4 pontos e 5 elementos.


Agora com vários 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')

Isso ainda fornece uma imagem de saída 5x5, mas com 7 canais (tamanho 1x5x5x7). Onde cada canal é produzido por um dos filtros no conjunto.


Agora com passos 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')

Agora, o resultado ainda tem 7 canais, mas é apenas 3x3 (tamanho 1x3x3x7).

Isso ocorre porque, em vez de centralizar os filtros em todos os pontos da imagem, os filtros são centralizados em todos os outros pontos da imagem, seguindo etapas (passos) da largura 2. Os xitens abaixo representam o centro do filtro para cada pixel de saída, em a imagem de entrada.

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

E, claro, a primeira dimensão da entrada é o número de imagens, para que você possa aplicá-lo em um lote de 10 imagens, por exemplo:

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

Isso realiza a mesma operação, para cada imagem independentemente, fornecendo uma pilha de 10 imagens como resultado (tamanho 10x3x3x7)


@ZijunLost Não, os médicos afirmam que o primeiro e último elemento deve 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

Esta implementação de convolução é baseada na matriz de Toeplitz ?
gkcn

Em relação a isso: "Isso ainda fornece uma imagem de saída 5x5, mas com 7 canais (tamanho 1x5x5x7). Onde cada canal é produzido por um dos filtros do conjunto.", Ainda tenho dificuldade em entender de onde são os 7 canais? o que você quer dizer com "filtros no conjunto"? Obrigado.
Derek

@mdaoust Oi, no seu segundo exemplo the 3x3 image and the 1x1 filter each have 5 channels, acho que o resultado é diferente do produto escalável calculado manualmente.
precisa saber é o seguinte

1
@ Derek Eu tenho a mesma pergunta, o "output_channel" é igual a "número de filtros" ??? Se sim, por que eles são nomeados "output_channel" nos documentos do tensorflow?
Wei

11

Apenas para adicionar às outras respostas, você deve pensar nos parâmetros em

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

como '5' correspondente ao número de canais em cada filtro. Cada filtro é um cubo 3d, com uma profundidade de 5. A profundidade do filtro deve corresponder à profundidade da imagem de entrada. O último parâmetro, 7, deve ser considerado como o número de filtros no lote. Apenas esqueça que isso é 4D e imagine que você tem um conjunto ou um lote de 7 filtros. O que você faz é criar 7 cubos de filtro com dimensões (3,3,5).

É muito mais fácil visualizar no domínio de Fourier, uma vez que a convolução se torna multiplicação por pontos. Para uma imagem de entrada de dimensões (100.100,3), você pode reescrever as dimensões do filtro como

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

Para obter um dos 7 mapas de recursos de saída, simplesmente executamos a multiplicação por pontos do cubo de filtro com o cubo de imagem e, em seguida, somamos os resultados através da dimensão de canais / profundidade (aqui é 3), colapsando em um 2d (100.100) apresentam mapa. Faça isso com cada cubo de filtro e você obterá 7 mapas de recursos 2D.


8

Eu tentei implementar o conv2d (para os meus estudos). Bem, eu escrevi isso:

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 ter feito isso corretamente. Verificado no MNIST, teve resultados muito próximos (mas essa implementação é mais lenta). Espero que isso ajude você.


0

Além de outras respostas, a operação conv2d está operando em c ++ (cpu) ou cuda para máquinas gpu que exigem nivelar e remodelar dados de certa maneira e usar a multiplicação da matriz gemmBLAS ou cuBLAS (cuda).


Portanto, na memória, a convolução está realmente sendo executada como uma multiplicação de matrizes, o que explica por que imagens maiores não são executadas necessariamente com um tempo de computação maior, mas com maior probabilidade de ocorrerem erros de OOM (falta de memória). Você pode me explicar por que a convolução 3D é mais ineficiente / eficiente da memória em comparação com a convolução 2D? Por exemplo, fazer conv 3D em [B, H, W, D, C] em comparação com conv 2D em [B * C, H, W, D]. Certamente, eles custam computacionalmente o mesmo?
SomePhysicsStudent
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.