Sieć konwolucyjna w Tensorflow do klasyfikacji cyfr z MNIST

Trzeci wpis z serii związanej tworzeniem sieci neuronowych w Tensorflow, tym razem budujemy sieć konwolucyjną do klasyfikacji cyfr z MNIST. Omawiam idee operacji konwolucji dla sieci neuronowych oraz jak ją poprawnie zaimplementować w Tensorflow. W stosunku do poprzednich wpisów z serii sieć ta osiąga najlepszą dokładność klasyfikacji równą 0.9880.

Wpis ten jest kontynuację serii tekstów o klasyfikacji cyfr z MNIST z wykorzystaniem Tensorflow:

  • zaczęliśmy z jednowarstwową siecią neuronową, która osiągnęła 0.9237 dokładności, służy ona jako baseline dla dalszych naszych modeli
  • w drugim wpisie zbudowaliśmy pięć wariantów wielowarstwowej sieci neuronowej, które w zależności od architektury osiągają dokładność od 0.9541 do 0.9817
  • w tym poście zbudujemy sieć konwolucyjną osiągającą dokładnośc na poziomie 0.9880

Artykuł ten ma na celu wprowadzić się w świat sieci konwolucyjnych oraz ich implementacji w Tensorflow. Jestem zwolennikiem nie tylko praktyki, ale lubię także wpleść trochę teorii. Daje ona intuicję i pozwala na zastosowanie praktyki w innych kontekstach. Na początku przeczytasz o samej zasadzie działania operacji konwolucji, dzięki temu mam nadzieję będziesz lepiej rozumiał i umiał poprawnie zastosować.

[bibshow file=https://ksopyla.com/wp-content/uploads/2017/12/mnist_conv_net_2017.bib show_links=1 group=year group_order=desc]

Operacja konwolucji w sieciach neuronowych

Operacja konwolucji jest matematyczą operacją znaną już od 1754 roku, natomiast ostatnio została wykorzystana z ogromnym sukcesem w sieciach neuronowych. Znalazła zastosowanie przy klasyfikacji obrazów oraz w technikach z NLP.  W ogromnym skrócie jej idea polega na przefiltrowaniu sygnału poruszając się po nim w ramach mniejszego okna, tak aby uchwycić cechy z sąsiedztwa. W przypadku obrazu filtrem jest np. macierz 3×3 z wagami, którą przesuwamy po obrazie, a w przypadku słów jest to wektor, który przesuwamy po sąsiedztwie dwóch poprzedających i następnych słów. Jeżeli chcesz dobrze zrozumieć zasadę działania konwolucji to odsyłam do dosyć szczegółowego wpisu na temat operacji konwolucji(splotu), podaję tam jej formalną matematyczną definicję z licznymi przykładami z przetwarzania obrazów.

Konwolucja jako filtr

W kontekście sieci neuronowych intuicję stojącą za operacją konwolucji doscyć dobrze oddaje następujący przykład:

Image convolution example
Operacja konwolucji na obrazie. Duża macierz to obraz, a mała pomarańczowa to filtr konwolucyjny.

Duża macierz to jest nasz obraz wraz z wartościami poszczególnych pikseli. Mała pomarańczowa macierz to jeden z przykładowych filtrów(ang. kernel, jądro konwolucji). Przemieszczająć nasz filtr wzdłuż obrazu wykonujemy operację mnożenia wartości fitru i odpowiadajacych wartości pikseli, następnie wszystko to dodajemy do siebie. W ten sposób otrzymamy wartość nowego piksela przefiltrowanego obrazu.

Do czego służą filtry

No dobra, ale po co nam te filtry, do czego one służą? Młody adepcie sztuki konwolucji, otóż fitlry pozwalają uwypuklić różne cechy na obrazie, w zależności od dobranych wag pozwalają na:

  • wydobycie krawędzi na obrazie,
  • rozmycie lub wyostrzenie obrazu,
  • wykrycie charakterystycznych układów lini np. trójkątów, lini równoległych, owali itp.
  • i wiele inych

Aaaa, już rozumiem, stosując jednocześnie wiele filtrów na obrazie mogę wykrywać różne charaktrystyczne prymitywne cechy lub obiekty. Czyli jeden filtr wykryje mi linie równoległe, inny prostopadłe, jeszcze inny trójkąty i składając tą wiedzę razem mogę wnioskować co się na obrazie znajduje. Czy w takim razie nie jest to coś w rodzaju odkrywania cech obrazu (ang. feature enginniering)? Brawo, właśnie to robi konwolucja.

Ok, rozumiem do czego filtry służą, ale skąd wziąć wartości do poszczególnych filtrów? Mnie na początku mojej przygody z operacją konwolucji takze to nie dawało spokoju. Tu mam dla ciebie dwie odpowiedzi, w klasycznym przetwarzaniu obrazów te wartości są już eksperymentalnie opracowane. Zaglądasz do książki i tam masz napisane, że filtr do wykrywania krawędzi to ma takie a nie inne wagi. W kontekście uczenia maszynowego i sieci, odpowiedź jest jeszcze prostsza – Sieć sama wyucza się tych wag! Dzieje się to na tej samej zasadzie jak w tradycyjnej sieci neuronowej, w trakcie treningu macierz „W” jest aktualizowana, tak tutaj wartości poszczególnych filtrów są także aktualizowane.

Zastosowanie wielu filtrów na raz – macierz konwolucji

W poprzednim przykładzie mieliśmy uproszczoną sytuacje, po pierwsze obraz składał się tylko z jednego kanału, w odróżnieniu od obrazu kolorowego składającego się z trzech kanałów RGB. Po drugie stosowaliśmy tylko jeden filtr na raz. Poniżej mamy bardziej realistyczny przykład. Obraz składa się z trzech warstw (kanały czerwony, zielony, niebieski), stąd macierz wejściowa (obraz) jest trójwymiarowa, ponadto mamy dwa filtry \(W_1\) i \(W_2\), które obejmują 4 sąsiednie piksele (4×4). Biorąc to wszystko pod uwagę nasza warstwa konwolucyjna jest macierzą o wymiarach \( W=[4,4,3,2] \). Dwa pierwsze wymiary oznaczają szerekość i wysokość filtrów (4×4), kolejny wymiar głębokość warstwy wejściowej (3), a ostatni głębokość warstwy wyjściowej (co jest równoważne ilości filtrów).

Credits to Martin Gorner @martin_gorner https://codelabs.developers.google.com/codelabs/cloud-tensorflow-mnist/#0

Obliczajać wartości dla warstwy wyjściowej, postępujemy podobnie jak w poprzednim przykładzie, odpowiadające sobie komórki macierzy przemnażamy oraz dodajemy do siebie. W tym przypadku aby obliczyć jedną wartość wyjściową będziemy musieli dokonać 4x4x3 mnożenia oraz wyniki te zsumować. Następnie przechodząć do następnego położenia operację powtarzamy otrzymująć kolejną wartość wyjściową, całą procedurę powtarzamy dla kolejnych filtrów. Miejcie na uwadzę, że operacja ta w Tensorflow jest dużo bardziej zoptymalizowana i wiele z tych obliczeń dzieje się równolegle.

Sieć konwolucyjna w Tensorflow – omówienie kodu

Kod dla całej serii wpisów znajduje się na moim githubie „Tensorflow MNIST Convolutional Network Tutorial” a obecnie interesował nas będzie plik mnist_3.0_3layer_convnet.py. Do jego uruchomienie będziecie potrzebowali python3 oraz zainstalowanej biblioteki Tensorflow w wersji min. 1.3.

Nie będę się rozpisywał o zbiorze MNIST bo już pojawił się w poprzednich postach z serii, więc szybko przejdę do meritum.

Architektura sieci konwolucyjnej

Zbudujemy pięcio warstwową sieć, w której zastosujemy 3 warstwy konwolucyjne wraz z operacją „max pooling”. Przedostatnią warstwą będzie warstwa w pełni połączona (ang. fully connected) i ostatnia warstwa wyjściowa z 10 neuronami wyjściowymi okręslającymi prawdopodobieństwo bycia jedną z 10 cyfr.

# Network architecture:
# 5 layer neural network with 3 convolution layers, input layer 28x28x1, output 10 (10 digits)
# Output labels uses one-hot encoding

# input layer               - X[batch, 28, 28]
# 1 conv. layer             - W1[5,5,,1,4] + b1[4]
#                             Y1[batch, 28, 28, C1]
# 2 conv. layer             - W2[3, 3, 4, 8] + b2[8]
# 2.1 max pooling filter 2x2, stride 2 - down sample the input (rescale input by 2) 28x28-> 14x14
#                             Y2[batch, 14,14,8] 
# 3 conv. layer             - W3[3, 3, 8, 16]  + b3[16]
# 3.1 max pooling filter 2x2, stride 2 - down sample the input (rescale input by 2) 14x14-> 7x7
#                             Y3[batch, 7, 7, 16] 
# 4 fully connecteed layer  - W4[7*7*16, 256]   + b4[256]
#                             Y4[batch, 256] 
# 5 output layer            - W5[256, 10]   + b5[10]
# One-hot encoded labels      Y5[batch, 10]

Na wejściu podajemy obrazki z zbioru MNIST mają one wymiary 28x28x1, cyfra 1 na końcu wskazuje że mamy tylko jeden kanał czyli obrazy są w odcieniach szarości. Całość podajemy do sieci w paczkach o rozmiarze ’batch’.

Dane z warstwy wejściowej przepuszczamy przez pierwszą warstwę konwolucyjną o wymiarach \( W_1=[5,5,1,4] \), czyli mamy 4 filtry o rozmiarze 5×5. Warstwą wejściową jest obraz o głębokości 1, stąd na trzeciej pozycji mamy jedynkę.

Zaraz po pierwszej warstwie konwolucyjnej dodaliśmy drugą o wymiarach \( W_2=[3,3,4,8] \), tym razem mamy 8 filtrów o rozmiarze 3×3, a dane wejściowe z warstwy powyżej mają głębokość 4 (tyle ile filtrów w poprzedniej warstwie). Po tych operacjach stosujemy max_pooling zmieniający rozdzielczość naszego obrazu o połowę z 28×28 pikseli do 14×14.

W tym momecie muszę się usprawiedliwić, bo pominąłem w opisie całą arytmetykę związaną z wielkością filtra, o ile chcemy go przesuwać (ang. strides) oraz jaki wpływ ma 'max_pooling’. Wszystkie te trzy rzeczy mają wpływ na wielkość docelowego 'obrazu’ wyjściowego. Dla mnie jest to najtrudniejsza część całego procesu składania warstw ze sobą, tak aby poszczególne wymiary w kolejnych warstwach się zgadzały. W tym celu polecam do przeczytania publikację „A guide to convolution arithmetic for deep learning” [bibcite key=convnet_arithmetic]

Trzecia warstwa konwolucyjna składa się z 16 filtrów \( W_3=[3,3,8,16] \), rozmiar filtra 3×3 a dane wejściowe z warstwy powyżej mają głębokość 8. Po tej operacji stosujemy powtórnie „max_pooling” zmniejszając rozdzielczość dwukrotnie z 14×14 do 7×7 pikseli.

Obraz nasz przechodząc powyższe przekształcenia ma teraz wymiary [7,7,16], przed ostateczną klasyfikacją dodajemy jeszcze jedną warstwę w pełni połączoną o wymiarach 256 neuronów, na na koniec warstwę z ’softmax’ z 10 wyjściowymi neuronami.

Proszę nie pytajcie mnie skąd wziąłem wymiary poszczególnych warstw. Nie ma tutaj jakiejś jednej przyjętej teorii, ilość neuronów w warstwach dobrałem eksperymentalnie, tak aby wyniki były dobre ale jednocześnie nie było ich za dużo.

Stworzenie odpowiednich zmiennych w Tensorflow

Przejdźmy zatem do kodu, który zbuduje nam model. W tym celu musimy zdefiniować wejście sieci 'placeholdery’ oraz poszczególne macierze o odpowiednich wymiarach.

Musimy stworzyć dwa pudełka, które pozwolą na wrzucenie danych do sieci, pierwszy ’X’ o wymiarach [None, 28,28,1], jedyne co może zastanawiać jak pierwszy wymiar może być równy None. Określa on ’batch_size’ ilość obrazków, które naraz chcemy przetważać. Otóż jest to pewien tric z Tensorflow polegający na tym, że biblioteka sama sobie wyliczy ile tych obrazów my przekazaliśmy, pozwala to na napisanie elastycznego kodu. Podony placeholder (Y_)  tworzymy dla rzeczywistych etykiet.

X = tf.placeholder(tf.float32, [None, 28, 28, 1])
# correct answers will go here
Y_ = tf.placeholder(tf.float32, [None, 10])
# Probability of keeping a node during dropout = 1.0 at test time (no dropout) and 0.75 at training time
pkeep = tf.placeholder(tf.float32)

Następnie dałem trzy zmienne określający rozmiar poszczególnych warstw konwolucyjnych (C1,C2,C3) oraz ilość neuronow w warstwie w pełni połączonej (FC4).

# layers sizes
C1 = 4  # first convolutional layer output depth
C2 = 8  # second convolutional layer output depth
C3 = 16 # third convolutional layer output depth

FC4 = 256  # fully connected layer

Sama definicja modelu wymaga określenia zmiennych (w sensie TF), lepiej nazywać je parametrrami modelu. W kodzie są to poszczególne macierze W1, W2, W3, W4, W5. Wszystkie są od razu zainicjalizowane losowymi wartościami z rozkładu normalnego. Przestroga na przyszłość nie inicjalizujcie parametrów modelu zerami, to nie zadziała 🙁

Dodatkowo prócz macierzy inicjalizowane są bias b1, b2, b3, b4, b5 (te już mogą być inicjalizowane zerami)

# weights - initialized with random values from normal distribution mean=0, stddev=0.1

# 5x5 conv. window, 1 input channel (gray images), C1 - outputs
W1 = tf.Variable(tf.truncated_normal([5, 5, 1, C1], stddev=0.1))
b1 = tf.Variable(tf.truncated_normal([C1], stddev=0.1))
# 3x3 conv. window, C1 input channels(output from previous conv. layer ), C2 - outputs
W2 = tf.Variable(tf.truncated_normal([3, 3, C1, C2], stddev=0.1))
b2 = tf.Variable(tf.truncated_normal([C2], stddev=0.1))
# 3x3 conv. window, C2 input channels(output from previous conv. layer ), C3 - outputs
W3 = tf.Variable(tf.truncated_normal([3, 3, C2, C3], stddev=0.1))
b3 = tf.Variable(tf.truncated_normal([C3], stddev=0.1))
# fully connected layer, we have to reshpe previous output to one dim, 
# we have two max pool operation in our network design, so our initial size 28x28 will be reduced 2*2=4
# each max poll will reduce size by factor of 2
W4 = tf.Variable(tf.truncated_normal([7*7*C3, FC4], stddev=0.1))
b4 = tf.Variable(tf.truncated_normal([FC4], stddev=0.1))

# output softmax layer (10 digits)
W5 = tf.Variable(tf.truncated_normal([FC4, 10], stddev=0.1))
b5 = tf.Variable(tf.truncated_normal([10], stddev=0.1))

Łączymy warstwy w sieć

Mając określone parametry modelu (zmienne z TF – variables) powinniśmy połączyć je ze sobą, tak aby dane swobodnie przepływały (ang. Flow – łapiecie skąd już nazwa TensorFlow) z jednej warstwy do drugiej. Każdą warstwę można rozumieć jako transformacje danych.

Tworzymy pierwszą warstwę, na danych z naszego placholdera X stosujemy operację konwolucji (tf.nn.conv2d) z macierzą W1 a następnie wszystko przepuszczamy przez funkcję aktywacji ReLu. W ten sposób otrzymujemy wyjście pierwszej warstwy Y1. Powtarzamy operację, lecz już jako wejście do drugiej warstwy stosujemy wyjście z pierwszej Y1. Wykonujemy konwolucję Y1 z macierzą W2 i stosujemy na wyniku funkcję aktywacji ReLu. Następnie stosujemy operację ’max_pool’, która z kwadrata 2×2 piksele wybiera największą wartość. Pozwala to na zmniejszenie wrażliwości na przesunięcia obiektów na obrazie (choć najnowsze publikacje z 2017 roku podają to w wątpliwość) oraz skutkuje zmniejszeniem wymiaru z 28×28 do 14×14.

Wyjście z warstwy drugiej Y2 stosujemy jako wejście do warstwy trzeciej, poddając je operacji konwolucji z macierzą W3, przepuszczamy przez funkcję aktywacji i stosujemy poling.

XX = tf.reshape(X, [-1, 784])

# Define the model

stride = 1  # output is 28x28
Y1 = tf.nn.relu(tf.nn.conv2d(X, W1, strides=[1, stride, stride, 1], padding='SAME') + b1)

k = 2 # max pool filter size and stride, will reduce input by factor of 2
Y2 = tf.nn.relu(tf.nn.conv2d(Y1, W2, strides=[1, stride, stride, 1], padding='SAME') + b2)
Y2 = tf.nn.max_pool(Y2, ksize=[1, k, k, 1], strides=[1, k, k, 1], padding='SAME')

Y3 = tf.nn.relu(tf.nn.conv2d(Y2, W3, strides=[1, stride, stride, 1], padding='SAME') + b3)
Y3 = tf.nn.max_pool(Y3, ksize=[1, k, k, 1], strides=[1, k, k, 1], padding='SAME')

Następnie musimy „rozwinąć” wyjście z warstwy Y3, aby móc połączyć z warstwą czwartą już w pełni połączoną. W tym celu stosujemy funkcję tf.reshape i otrzymujemy macierz 2 wymiarową, gdzie pierwszy wymiar określa numer obrazka w paczce (’batch’) a drugi to rozciągnięty wektor z danymi. Obliczamy wyjście warstwy Y4 poprzez zwykłe mnożenie macierzy YY i W4.

# reshape the output from the third convolution for the fully connected layer
YY = tf.reshape(Y3, shape=[-1, 7 * 7 * C3])

Y4 = tf.nn.relu(tf.matmul(YY, W4) + b4)
#Y4 = tf.nn.dropout(Y4, pkeep)
Ylogits = tf.matmul(Y4, W5) + b5
Y = tf.nn.softmax(Ylogits)

Na koniec obliczamy wyjście sieci Y, mnożąc Y4 poprzez macierz W5. W ten sposób otrzymujemy „surowe wartości” Ylogits (zapamiętujemy je bo się jeszcze przydadzą). W celu otrzymania prawdopodobieństw stosujemy tf.nn.softmax

Mając połączone warstwy z sobą przejdźmy do sposobu uczenia sieci.

Uczenie – wybór funkcji straty i algorytm optymalizacji

W tym przypadku sytuacja jest raczej standardowa. Podobnie jak w poprzednich przypadkach stosujemy funkcję starty ’cross entropy’. Funkcja ta porównuje wartości obliczone przez z sieć (Ylogits) z rzeczywistymi etykietami (Y_). Gdy wartości zgadzają się to nie nalicza błędu, a jezeli nie to dodaje do całkowitej sumy błędów błędy cząstkowe.

Cały proces uczenia polega na minimalizacji wartości funkcji straty poprzez odpowiednie dostosowywanie naszych zmiennych (ang. variables). Dzieje się to poprzez modyfikację wag w naszych macierzach W1 do W5. Drogi czytelniku wybacz za ogromny skrót myślowy, ale nie chcę wchodzić w szczegóły alg. wstecznej propagacji błędu oraz optymalizację.

cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=Ylogits, labels=Y_)
cross_entropy = tf.reduce_mean(cross_entropy)*100

                                                          
# accuracy of the trained model, between 0 (worst) and 1 (best)
correct_prediction = tf.equal(tf.argmax(Y, 1), tf.argmax(Y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

# training, 
learning_rate = 0.003
train_step = tf.train.AdamOptimizer(learning_rate).minimize(cross_entropy)

Na końcu określamy algorytm, który zastosujemy do minimalizacji naszej funkcji straty. Wybraliśmy ’AdamOptimizer’, który magicznie będzie wiedział jak dokonać minimalizacji naszej funkcji straty. Przechodząc kolejno po warstwach w górę sieci i modyfikując wagi w sieci. Zwrócicie uwagę, że minimalizacja jest zapisana pod zmienną ’train_step’. Na razie jeszcze niczego nie uruchomiliśmy i nie ruszyły żadne obliczenia, dopiero poniżej uruchomimy uczenie sieci.

Pętla ucząca

Mamy już wszystko zdefiniowane, wystarczy tylko uruchomić całą naszą maszynerię. Bo jak pamiętacie, idea Tensorflow polega na tym, że najpierw tworzymy graf obliczeń. Jest to swoisty przepis łączący poszczególne elementy sieci oraz procesu uczenia ze sobą. Na koniec w ramach sesji (ang. Session) uruchamiamy poszczególne kroki, dla nas najważniejsza jest linia ostatnia:

sess.run(train_step, feed_dict={X: batch_X, Y_: batch_Y, pkeep: 0.75}) 

Uruchamia ona krok algorytmu optymalizacji jednocześnie poprzez słownik feed_dict przekazuje część danych, na których proces uczenia ma się odbyć.
Zapętlając to N-razy i za każdym razem losując paczkę (ang. batch) danych

batch_X, batch_Y = mnist.train.next_batch(BATCH)

dokonujemy optymalizacji wag, czyli uczenia sieci.

with tf.Session() as sess:
    sess.run(init)

    for i in range(NUM_ITERS+1):
        # training on batches of 100 images with 100 labels
        batch_X, batch_Y = mnist.train.next_batch(BATCH)
        
        if i%DISPLAY_STEP ==0:
            # compute training values for visualisation
            acc_trn, loss_trn, w, b = sess.run([accuracy, cross_entropy, allweights, allbiases], feed_dict={X: batch_X, Y_: batch_Y, pkeep: 1.0})
            
            acc_tst, loss_tst = sess.run([accuracy, cross_entropy], feed_dict={X: mnist.test.images, Y_: mnist.test.labels, pkeep: 1.0})
            
            print("#{} Trn acc={} , Trn loss={} Tst acc={} , Tst loss={}".format(i,acc_trn,loss_trn,acc_tst,loss_tst))

            train_losses.append(loss_trn)
            train_acc.append(acc_trn)
            test_losses.append(loss_trn)
            test_acc.append(acc_tst)

        # the backpropagation training step
        sess.run(train_step, 
                 feed_dict={X: batch_X, Y_: batch_Y, pkeep: 0.75})
#

Pozostała część pętli służy tylko wyświetleniu statystyk co DISPLAY_STEP kroków, tak aby można w konsoli śledzić proces uczenia. Jak zmienia się funkcja starty na danych treningowych i testowych.

Wyniki

Uruchamiając skrypt powinniście otrymać dokładność powyżej 0.9880, przynajmniej wcześniej taką zawsze otrzymywałem. Po aktualizacji do TF do wersji 1.4 accuracy podskoczyło do 0.9910 nie wiem dlaczego. Skrypt powinien wygenerować wam także następujący wykres z wartościami loss function i accuracy.

Sieć konwolucyjna W Tensorflow wykresy loss i accuracy
Wartości accuracy i loss dla zbioru treningowego i testowego dla sieci konwolucyjnej

Podsumowanie

Wpis ten kończy serię artykułów związanych z budową sieci neuronowych w Tensorflow. W trakcie całej serii zbudowaliśmy najpierw prostą sieć jednowarstwową, następnie 5 warstwową a na końcu konwolucyjną. Chciałem wam pokazać jak krok po kroku można zbudować coraz to wydajniejsze modele służące do klasyfikacji obrazów. Oczywiście pozostało jeszcze wiele kwestii nie poruszonych takich jak: batch normalization, residual connections, inception modules itp. To pozostawiam sobie i wam na przyszłość.

Jeżeli byście chcieli wykorzystać materiały do zbudowania swojego własnego klasyfikatora to zwróćcie uwagę na kilka kwestii:

  • Rozmiar placeholdera X zależy od rozmiaru waszych danych. Nie tylko rozdzielczość (28×28) należało by zmienić ilość kanałów jeżeli przetwarzacie obrazy kolorowe RGB
  • Rozmiar warstwy wyjściowej zależny jest od ilości klas, my w tych przykładach mieliśmy ich 10 u was może ich być znacznie więcej np. 1000
  • Architektura tej sieci jest raczej prosta i może nie dawać zadowalających rezultatów na waszych danych, tak więc polecam eksperymentowanie
    • z ilością filtrów, można zwiększyć wartości C1, C2, C3
    • a także z dodawaniem nowych warstw, wymagać to będzie od was dodanie nowych zmiennych W oraz odpowiednie ich połączenie

Jeżeli uważasz ten wpis za wartościowy to Zasubskrybuj bloga. Dostaniesz informacje o nowych artykułach.

Join 100 other subscribers

11 Comments Sieć konwolucyjna w Tensorflow do klasyfikacji cyfr z MNIST

    1. ksopyla

      Powoli zbieram się do tego, ale jest na mojej liście TODO, ale dzięki twojemu komentarzowi daję temu wyższy priorytet 🙂

  1. Bartosz

    Trafiłem na tego bloga przez ten artykuł i zostanę tu na dłużej! Czeka mnie dużo tematów do nadrobienia i z niecierpliwością czekam na kolejne 😀

    Reply

Ciekawe, wartościowe, podziel się proszę opinią!

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.