Co robi tf.nn.conv2d w tensorflow?

135

Patrzyłem na dokumentację tensorflow o tf.nn.conv2d tutaj . Ale nie mogę zrozumieć, co to robi ani co próbuje osiągnąć. W dokumentach jest napisane,

# 1: spłaszcza filtr do matrycy 2-D z kształtem

[filter_height * filter_width * in_channels, output_channels].

Co to teraz robi? Czy jest to mnożenie elementarne, czy po prostu zwykłe mnożenie macierzy? Też nie mogłem zrozumieć pozostałych dwóch punktów wymienionych w dokumentach. Napisałem je poniżej:

# 2: Wyodrębnia łaty obrazu z wejściowego tensora, aby utworzyć wirtualny tensor kształtu

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

# 3: Dla każdego obszaru mnoży się w prawo macierz filtra i wektor wstawki obrazu.

Byłoby naprawdę pomocne, gdyby ktoś mógł podać przykład, może fragment kodu (niezwykle pomocny) i wyjaśnić, co się tam dzieje i dlaczego tak się dzieje.

Próbowałem zakodować małą część i wydrukować kształt operacji. Wciąż nie mogę tego zrozumieć.

Próbowałem czegoś takiego:

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)

Rozumiem bity i fragmenty konwolucyjnych sieci neuronowych. Studiowałem je tutaj . Ale implementacja na tensorflow nie jest tym, czego się spodziewałem. Więc to wywołało pytanie.

EDYCJA : Więc zaimplementowałem znacznie prostszy kod. Ale nie mogę dowiedzieć się, co się dzieje. Mam na myśli, jakie są takie wyniki. Byłoby niezwykle pomocne, gdyby ktokolwiek mógł mi powiedzieć, jaki proces daje taki wynik.

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)

wynik

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

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

  [[-0.00354624]
   [ 0.41650501]]]]
Shubhashis
źródło
W rzeczywistości cudnn jest domyślnie włączony na GPU w tf.nn.conv2d(), więc omawiana metoda nie jest w ogóle używana, gdy używamy TF z obsługą GPU, chyba że zostanie use_cudnn_on_gpu=Falseto wyraźnie określone.
gkcn

Odpowiedzi:

59

Splot 2D jest obliczany w podobny sposób, jak można obliczyć splot 1D : przesuwasz jądro po wejściu, obliczasz mnożenia według elementów i sumujesz je. Ale zamiast jądra / wejścia będącego tablicą, tutaj są to macierze.


W najbardziej podstawowym przykładzie nie ma dopełnienia, a krok = 1. Załóżmy, że jesteś inputi kerneljesteś: wprowadź opis obrazu tutaj

Kiedy używasz jądra, otrzymasz następujący wynik:, wprowadź opis obrazu tutajktóry jest obliczany w następujący sposób:

  • 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

Funkcja conv2d TF oblicza zwoje w partiach i używa nieco innego formatu. Dla wejścia jest to [batch, in_height, in_width, in_channels]dla jądra [filter_height, filter_width, in_channels, out_channels]. Dlatego musimy podać dane w odpowiednim formacie:

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

Następnie splot jest obliczany ze wzoru:

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)

I będzie równa tej, którą obliczyliśmy ręcznie.


Na przykładach z wyściółką / krokami, warto tu zapoznać .

Salvador Dali
źródło
Fajny przykład, jednak niektóre linki są zepsute.
silgon
1
@silgon niestety dzieje się tak, ponieważ SO zdecydował się nie wspierać funkcji dokumentacji, którą utworzyli i reklamowali na początku.
Salvador Dali
161

Ok, myślę, że to najprostszy sposób, aby to wszystko wyjaśnić.


Twój przykład to 1 obraz, rozmiar 2x2, z 1 kanałem. Masz 1 filtr o rozmiarze 1x1 i 1 kanał (rozmiar to wysokość x szerokość x kanały x liczba filtrów).

W tym prostym przypadku wynikowy obraz 2x2,1 kanału (rozmiar 1x2x2x1, liczba obrazów x wysokość x szerokość xx kanałów) jest wynikiem pomnożenia wartości filtru przez każdy piksel obrazu.


Teraz wypróbujmy więcej kanałów:

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

Tutaj obraz 3x3 i filtr 1x1 mają po 5 kanałów. Wynikowy obraz będzie 3x3 z 1 kanałem (rozmiar 1x3x3x1), gdzie wartość każdego piksela jest iloczynem skalarnym w kanałach filtru z odpowiadającym mu pikselem w obrazie wejściowym.


Teraz z filtrem 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')

Tutaj otrzymujemy obraz 1x1, z 1 kanałem (rozmiar 1x1x1x1). Wartość jest sumą 9, 5-elementowych iloczynów skalarnych. Ale możesz po prostu nazwać to 45-elementowym iloczynem skalarnym.


Teraz z większym obrazem

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

Na wyjściu jest 1-kanałowy obraz 3x3 (rozmiar 1x3x3x1). Każda z tych wartości jest sumą 9, 5-elementowych iloczynów skalarnych.

Każde wyjście jest tworzone przez wyśrodkowanie filtra na jednym z 9 środkowych pikseli obrazu wejściowego, tak aby żaden filtr nie odstał. Poniższe xsymbole przedstawiają środki filtrów dla każdego piksela wyjściowego.

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

Teraz z dopełnieniem „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')

Daje to obraz wyjściowy 5x5 (rozmiar 1x5x5x1). Odbywa się to poprzez wyśrodkowanie filtra w każdej pozycji obrazu.

Każdy z 5-elementowych iloczynów skalarnych, w których filtr wystaje poza krawędź obrazu, otrzymuje wartość zero.

Zatem rogi są tylko sumami 4, 5-elementowych iloczynów skalarnych.


Teraz z wieloma filtrami.

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

To nadal daje obraz wyjściowy 5x5, ale z 7 kanałami (rozmiar 1x5x5x7). Gdzie każdy kanał jest wytwarzany przez jeden z filtrów w zestawie.


Teraz z krokami 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')

Teraz wynik nadal ma 7 kanałów, ale ma tylko 3x3 (rozmiar 1x3x3x7).

Dzieje się tak, ponieważ zamiast wyśrodkować filtry w każdym punkcie obrazu, filtry są wyśrodkowane w każdym innym punkcie obrazu, wykonując kroki (kroki) o szerokości 2. Poniższe xponiżej przedstawiają środek filtru dla każdego piksela wyjściowego, na obraz wejściowy.

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

I oczywiście pierwszym wymiarem danych wejściowych jest liczba obrazów, więc można je zastosować do partii 10 obrazów, na przykład:

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

Wykonuje tę samą operację niezależnie dla każdego obrazu, dając w rezultacie stos 10 obrazów (rozmiar 10x3x3x7)

mdaoust
źródło
@ZijunLost Nie, dokumentacja stwierdza, że ​​pierwszy i ostatni element musi być 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
Czy jest to implementacja splotu oparta na macierzy Toeplitza ?
gkcn
W związku z tym: „To nadal daje obraz wyjściowy 5x5, ale z 7 kanałami (rozmiar 1x5x5x7). Gdzie każdy kanał jest wytwarzany przez jeden z filtrów w zestawie.”, Nadal mam trudności ze zrozumieniem, skąd pochodzi te 7 kanałów? co masz na myśli "filtry w zestawie"? Dzięki.
derek
@mdaoust Cześć, jeśli chodzi o twój drugi przykład, w którym the 3x3 image and the 1x1 filter each have 5 channelsuważam, że wynik różni się od obliczonego ręcznie iloczynu skalarnego .
Tgn Yang
1
@derek Mam to samo pytanie, czy „kanał_wyjściowy” jest taki sam jak „liczba filtrów” ??? jeśli tak, to dlaczego w dokumentach tensorflow są nazywane „kanał_wyjściowy”?
Wei
11

Aby dodać do innych odpowiedzi, powinieneś pomyśleć o parametrach w

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

jako „5”, co odpowiada liczbie kanałów w każdym filtrze. Każdy filtr to sześcian 3D o głębokości 5. Głębokość filtra musi odpowiadać głębi obrazu wejściowego. Ostatni parametr, 7, należy traktować jako liczbę filtrów w partii. Po prostu zapomnij o tym, że jest to 4D i zamiast tego wyobraź sobie, że masz zestaw lub partię 7 filtrów. Tworzysz 7 kostek filtracyjnych o wymiarach (3,3,5).

Dużo łatwiej jest wizualizować w domenie Fouriera, ponieważ splot staje się mnożeniem punktowym. W przypadku obrazu wejściowego o wymiarach (100,100,3) można przepisać wymiary filtra jako

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

Aby uzyskać jedną z 7 map cech wyjściowych, po prostu wykonujemy punktowe pomnożenie kostki filtra przez kostkę obrazu, a następnie sumujemy wyniki w wymiarze kanałów / głębokości (tutaj jest to 3), zwinięte do 2d (100,100) mapa funkcji. Zrób to z każdą kostką filtra, a otrzymasz 7 map funkcji 2D.

Val9265
źródło
8

Próbowałem zaimplementować conv2d (do nauki). Cóż, napisałem, że:

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

Mam nadzieję, że zrobiłem to poprawnie. Sprawdzone na MNIST, dało bardzo zbliżone wyniki (ale ta implementacja jest wolniejsza). Mam nadzieję, że to Ci pomoże.

Artem Yaschenko
źródło
0

Oprócz innych odpowiedzi, operacja conv2d działa w języku c ++ (cpu) lub cuda dla maszyn z procesorem graficznym, które wymagają spłaszczenia i zmiany kształtu danych w określony sposób oraz użycia mnożenia macierzy gemmBLAS lub cuBLAS (cuda).

karaspd
źródło
Tak więc w pamięci splot jest faktycznie wykonywany jako mnożenie macierzy, co wyjaśnia, dlaczego większe obrazy niekoniecznie działają w dłuższym czasie obliczeniowym, ale zamiast tego częściej napotykają na błąd OOM (brak pamięci). Czy możesz mi wyjaśnić, dlaczego splot 3D jest bardziej nieefektywny / wydajny pamięci w porównaniu do splotu 2D? Na przykład konw. 3D w [B, H, W, D, C] w porównaniu do konw. 2D w [B * C, H, W, D]. Z pewnością kosztują obliczeniowo tyle samo?
SomePhysicsStudent