Jednowarstwowa sieć neuronowa w Tensorflow do klasyfikacji cyfr z MNIST

Rozpoznawanie cyfr MNIST jest jednym z najbardziej popularnych problemów w świecie uczenia maszynowego. Chciałbym wam krok po kroku pokazać jak zbudować wielowarstwową sztuczną sieć neuronową, która będzie rozpoznawać ręcznie pisane cyfry z dokładnością ponad 98%.

Pierwotnie w poście tym chciałem przedstawić od razu sieć konwolucyjną, ale doszedłem do wniosku że lepiej będzie zacząć od podstaw i w kolejnych postach opisywać coraz bardziej zaawansowane modele. W tym wpisie skupimy się na stworzeniu „normalnej” jednowarstwowej sieci neuronowej, będzie to także stanowiło solidną podstawę do dalszej pracy. Tak więc wpis ten jest częścią większego projektu, w którym kolejno będziemy wspólnie budować coraz lepsze modele, skończywszy na sieci konwolucyjnej wraz z tymi wszystkimi bajerami jak: dropout, nowe funkcje aktywacji (relu, elu), batch normalization etc.

Wpis ten jest pierwszą części serii tekstów o klasyfikacji cyfr z MNIST z wykorzystaniem Tensorflow:

  • w tym poście zbudujemy jednowarstwową siecią neuronową, która osiągnie 0.9237 dokładności, służy ona jako baseline dla dalszych naszych modeli
  • w drugim wpisie zbudujemy 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 trzeciej części zbudujemy sieć konwolucyjną, która osiągnie dokładnośc klasyfikacji na poziomie 0.9880

MNIST Dataset

Zbirór MNIST i problem z nim związany czyli klasyfikacja odręcznie pisanych cyfr jest już kultowym zadaniem w dziedzinie uczenia maszynowego. Pierwsze prace Yana Lecun pojawiły się w roku 1998, później systematycznie od tego czasu kolejni badacze usiłowali pobić poprzedni rekord w rozpoznawaniu cyfr. Obecny rekord wynosi 99.79% (MNIST Test Error 0.21%). Ja sam zaczynałem swoją przygodę z uczeniem maszynowym właśnie na pisaniu różnego rodzaju klasyfikatorów dla tego zbioru.

W poprzednim wpisie na blogu opisałem sposób klasyfikacji zbioru MNIST z wykorzystaniem algorytmu SVM i biblioteki scikit-learn. Znajduje się tam także szczegółowy opis danych oraz ich formatu, zachęcam do przeczytania jako dobre wprowadzenie do dzisiejszej tematyki.

Tak dla przypomnienia to cyfry zapisane są w postaci obrazków o wymiarach 28x28px i wyglądają następująco:

MNIST digits neural network

Etapy budowy sztucznej sieci neuronowej

Moim celem jest przedstawienie kolejnych kroków budowaniu sieci, która będzie miała dokładność na poziomie ponda 98%. Zaczniemy od prostej jednowarstwowej sieci, następnie dodamy kolejne warstwy ukryte, dostosujemy architekturę tej sieci, aby na koniec stworzyć sieć konwolucyjną. W kolejnych postach będę opisywał kolejne etapy.

  • Jednowarstwowa sieć neuronowa
  • Sieć z pięcioma warstwami (4 ukryte+1wyjściowa)
  • Sieć z trzema warstwami konwolucyjnymi
  • Ulepszona wersja sieci konwolucyjnej (relu, elu, dropout, batch normalization)

Jednowarstwowa sieć neuronowa

Jest to stosunkowo prosta siec zawierająca wejście i wyjście. Na wejściu podajemy jeden obrazek 28x28px, rozciągnięty wiersz po wierszu jako wektor o 784 wymiarach (28*28=784), każdemu obrazkowi będzie odpowiadała etykieta 0…9, zakodowana jako 10-elementowy wektor (one-hot encoding). Stąd też wyjściem sieci jest 10-cio elementowy wektor zawierający prawdopodobieństwa określające na ile rozpatrywana cyfra na obrazie podobna jest do poszczególnych cyfr. Na i-tej pozycji mamy prawdopodobieństwo, że na wejściu podaliśmy liczbę i, czyli na pozycji 0 mamy prawdopodobieństwo tego, że podany obraz zawiera cyfrę zero, na pozycji 1 prawdopodobieństwo, że podana cyfra to 1 i tak dalej.

Jednowarstwowa sieć neuronowa MNIST
Przykład jednowarstwowej sieci neuronowej dla MNIST. Obraz pochodzi z https://ml4a.github.io/demos/f_mnist_1layer/

Sieć neuronowa jako operacje macierzowe

Wybaczcie, ale od razu przejdę do budowy sieci zakładając, że budowa neuronu jest wam znana. Zauważcie, że do każdego neuronu wyjściowego dochodzi 784 połączeń, każde takie połączenie ma stowarzyszoną wagę (liczbę rzeczywistą). Przemnażając 784 wartości z wejścia przez 784 wag, połączonych z wybranym węzłem wyjściowym, a następnie sumując je otrzymujemy jedną liczbę. Wartość ta, w węźle przepuszczana jest przez nieliniowy filtr (funkcję aktywacji), abyśmy na koniec otrzymali liczbę informującą nas na ile dane wejście przypomina cyfrę z wybranej pozycji.

A teraz proszę was o skupienie, zamknijcie facebooka, przyciszcie muzykę z youtuba, bo spróbujmy to zapisać matematycznie. Tak więc do dzieła!

Co mamy dane? Po pierwsze zbiór cyfr, w postaci obrazów, z których każda to wektor \(x_i \in R^{784}\). Po drugie, posiadamy zestaw wag. Na jeden z dziesięciu neuronów wyjściowych przypadają 784 wagi, w celu usystematyzowania pracy z nimi ułóżmy je w macierz \(W\) :

$$W=\left[
\begin{array}{cccc}
w_{1,1} & w_{1,2} & \ldots & w_{1,10} \\
w_{2,1} & w_{2,2} & \ldots & w_{2,10} \\
\vdots  &\vdots & \ddots &\vdots \\
w_{784,1} & w_{784,2} & \ldots & w_{784,10} \\
\end{array}\right]$$

Mamy 10 kolumn w macierzy, czyli tyle ile klas. Kolumna o numerze i  przechowuje współczynniki (połączenia) dla i-tego neuronu wyjściowego. Chcąc obliczyć wartości na wyjściu sieci wystarczy dokonać mnożenia macierzy, przemnożyć wektor \(x_i\) przez macierz \(W\).

$$\left[
\begin{array}{cccc}
x_{1,1} & x_{1,2} & \ldots & x_{1,784}
\end{array}\right] *
\left[
\begin{array}{cccc}
w_{1,1} & w_{1,2} & \ldots & w_{1,10} \\
w_{2,1} & w_{2,2} & \ldots & w_{2,10} \\
\vdots  &\vdots & \ddots &\vdots \\
w_{784,1} & w_{784,2} & \ldots & w_{784,10} \\
\end{array}\right]= \\
=\left[\begin{array}{cccc}
out_{1,1} & out_{1,2} & \ldots & out_{1,10}
\end{array}\right] $$

Podejście wsadowe, przetwarzamy wektory w paczkach (batch)

Powyższe podejście możemy jeszcze trochę ulepszyć. Zamiast w danym kroku rozpatrywać tylko jeden wektor wejściowy to można wziąć ich całą paczkę (batch) np. 100 i przemnożyć przez macierz \(W\) dzięki temu zabiegowi od razu otrzymamy przykładowe odpowiedzi sieci dla 100 obrazów. Pozwala to lepiej wykorzystać zasoby obliczeniowe i dokonać wielu optymalizacji podczas mnożenia. A także uczynić proces uczenia bardziej stabilnym (głównie chodzi o obliczenia gradientów w algorytmie wstecznej propagacji błędów).

$$\left[
\begin{array}{cccc}
x_{1,1} & x_{1,2} & \ldots & x_{1,784} \\
x_{2,1} & x_{2,2} & \ldots & x_{2,784} \\
\vdots  &\vdots & \ddots &\vdots \\
x_{100,1} & x_{100,2} & \ldots & x_{100,784} \\
\end{array}\right] *
\left[
\begin{array}{cccc}
w_{1,1} & w_{1,2} & \ldots & w_{1,10} \\
w_{2,1} & w_{2,2} & \ldots & w_{2,10} \\
\vdots  &\vdots & \ddots &\vdots \\
w_{784,1} & w_{784,2} & \ldots & w_{784,10} \\
\end{array}\right]= \\
=\left[\begin{array}{cccc}
out_{1,1} & out_{1,2} & \ldots & out_{1,10} \\
out_{2,1} & out_{2,2} & \ldots & out_{2,10} \\
\vdots  &\vdots & \ddots &\vdots \\
out_{100,1} & out_{100,2} & \ldots & out_{100,10} \\
\end{array}\right] $$

Wyjściowe wartości \(out\) przepuszczane są przez funkcję aktywacji, a następnie cały wiersz jest normalizowany, tak aby określał prawdopodobieństwo. Suma wartości z wiersza powinna wynosić 1.

Trening sieci neuronowej

Do tej pory omówiliśmy tylko samą strukturę sieci, przyszedł czas na samą procedurę treningu takiej sieci.
Drogi czytelniku, tutaj cię rozczaruję. Temat uczenia sieci i wykorzystywanych algorytmów jest na tyle obszerny, że pozwolisz podsumuję go tylko kilkoma słowami i zestawem linków do materiałów.

W naszym przykładzie, sieć będzie uczona z wykorzystaniem algorytmu wstecznej propagacji błędów wraz z wykorzystaniem algorytmu Gradient Descent do optymalizacji. Tak naprawdę to całością zajmie się Tensorflow, my skorzystamy z gotowych funkcji.

Tworzymy Sieć w Tensorflow

Po tych wszystkich wstępach możemy przejść do kodu 🙂
Projekt został przygotowany w Python 3.5 i Tensorflow 1.0. Przypominam, że w Tensorflow operujemy na grafie obliczeniowym. Zaczynamy od zdefiniowania grafu odwzorowującego nasze obliczenia. Tensorflow wewnętrznie go kompiluje(może lepiej użyć słowa przetwarza), aby później  móc podstawić odpowiednie wartości i przeliczyć poszczególne węzły. Dobrym wstępem może być film.

Importy, stałe oraz ściągnięcie zbioru MNIST

Na początku importujemy własną bibliotekę z kilkoma funkcjami do wizualizacji (znajduje się ona w udostępnionym projekcie), tensorflow oraz funkcję do ściągnięcia zbioru MNIST.
Ustawiamy kilka stałych:

  • NUM_ITERS=5000 – liczba iteracji algorytmu, ile mini-batch’y ma przetworzyć, zwiększając dostaniemy lepsze wyniki, ale i dłużej się będzie liczył model
  • DISPLAY_STEP=100 – co ile iteracji obliczamy statysytki, które pomogą nam zwizualizować postęp uczenia (test error, loss function etc)
  • BATCH=100 – rozmiar paczki, ile obrazków bierzemy naraz pod uwagę (zazwyczaj 64,100,128, 200)

Następnie ściągamy zbiór MNIST, tak naprawdę ściągnięcie nastąpi przy pierwszym uruchominiu, później skrypt będzie korzystał z już sciągniętej wersji.

import visualizations as vis
import tensorflow as tf
from tensorflow.contrib.learn.python.learn.datasets.mnist import read_data_sets


NUM_ITERS=5000
DISPLAY_STEP=100
BATCH=100
tf.set_random_seed(0)

# Download images and labels 
mnist = read_data_sets("MNISTdata", one_hot=True, reshape=False, validation_size=0)
# mnist.test (10K images+labels) -> mnist.test.images, mnist.test.labels
# mnist.train (60K images+labels) -> mnist.train.images, mnist.test.labels

Placeholders, variables – przygotowujemy pojemniki

Przygotujmy teraz zmienne oraz pojemniki(placeholders) na dane niezbędne dla grafu Tensorflow.
Określamy, że zmienna X będzie pojemnikiem na dane o rozmiarach [ilość obrazów w paczce, szerokość obrazu, wysokość obrazu, ilość kanałów]. Komentarza wymagają tylko pierwszy i ostatni wymiar, None informuje TF, że ma się sam dostosować do ilości danych, a 1 na końcu określa, że nasz obraz jest w odcieniach szarości. Gdybyśmy przetwarzali obraz kolorowy RGB to byśmy operowali 3 kanałami. Podobnie do X tworzymy pojemnik Y_ przechowujący etykiety związane z obrazami, Y_ zawiera wektory o 10 elementach zakodowane zgodnie z one-hot encoding.

Istotnym elementem jest macierz W,  zauważcie że jest ona typu tf.Variable.  Jest to specjalny typ w tensorflow służący do przechowywania parametrów modelu, które mają być optymalizowane. TF sam będzie uaktualniał ich wartości w trakcie uczenia (wiem, it’s magic). XX – jest naszym zbiorem X, z rozwiniętymi wierszami.

Na samym końcu definiujemy nasz model, przemnażamy (tf.matmul) wartości wejściowe z XX przez wagi z macierzy dodajemy przesunięcie i wszystko normalizujemy poprzez wykorzystnie funkcji tf.softmax

# All the data will be stored in X - tensor, 4 dimensional matrix
# The first dimension (None) will index the images in the mini-batch
X = tf.placeholder(tf.float32, [None, 28, 28, 1])

# correct answers will go here
Y_ = tf.placeholder(tf.float32, [None, 10])

# weights W[784, 10] - initialized with random values from normal distribution mean=0, stddev=0.1
W = tf.Variable(tf.truncated_normal([784, 10],stddev=0.1))

# biases b[10]
b = tf.Variable(tf.zeros([10]))

# matplotlib visualisation
allweights = tf.reshape(W, [-1])
allbiases = tf.reshape(b, [-1])

# flatten the images, unrole eacha image row by row, create vector[784] 
# -1 in the shape definition means compute automatically the size of this dimension
XX = tf.reshape(X, [-1, 784])

# Define model
Y = tf.nn.softmax(tf.matmul(XX, W) + b)

Określamy kryteria jakości naszego modelu

Mając zdefiniowany model, potrzebujemy funkcji oceny naszego modelu. Najczęściej nazywa się ją funkcją straty (loss function). Jest to funkcja dzięki, dzięki której wiemy czy model jest dobry czy zły, na jej podstawie dokonujemy optymalizacji. Informuje ona o ilości popełnianych błędów przez nasz model, jeżeli wartość jest wysoka to znaczy, że nasz model wymaga jeszcze uczenia, czyli dostosowania wag z macierzy W. W naszym przykładzie wykorzystam cross entropy.

Dodatkowo w celach pomocniczych definiujemy w jaki sposób będziemy obliczać accuracy. Na koniec tworzymy instancję klasy GradientDescentOptimizer i do jej metody minimize przekazujemy „przepis” na obliczenie cross_entropy

# cross-entropy
# log takes the log of each element, * multiplies the tensors element by element
# reduce_mean will add all the components in the tensor
# so here we end up with the total cross-entropy for all images in the batch
cross_entropy = -tf.reduce_mean(Y_ * tf.log(Y)) * 1000.0  # normalized for batches of 100 images,
                                                          # *10 because  "mean" included an unwanted division by 10

# 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.005
train_step = tf.train.GradientDescentOptimizer(0.005).minimize(cross_entropy)

Główna pętla obliczeniowa

Ostatnim etapem jest uruchomienie obliczeń w pętli. Najpier inicjalizujemy wszystkie zmienne i parametry z sieci, tworzymy pomocnicze listy na wyniki cząstkowe do wizualizacji. Tworzymy sesję tensorflow w pętli for kolejno uruchamiamy obliczenia przy pomocy sess.run. Funkcja ta jest wyjątkowa, to dzięki niej dzieje się cała magia, ona wprawia w ruch maszynerię działającą pod spodem. Wszelkie niezbędne dane do obliczeń przekazujemy z wykorzystaniem uprzednio stworzonych placeholders poprzez słownik feed_dict. Z punktu widzenia modelu najważniejsze jest wywołanie sess.run(train_step, feed_dict={X: batch_X, Y_: batch_Y}) to ono dokonuje wykonania kroku optymalizacyjnego, poprzednie wywołania służą tylko obliczeniu statystyk podczas uczenia takich jak (accuracy_trn, accuracy_tst, loss itp).

Ostatnia linia ma zadanie narysować wykresy przedstawiające jak wyglądał trening.

# Initializing the variables
init = tf.global_variables_initializer()

train_losses = list()
train_acc = list()
test_losses = list()
test_acc = list()

saver = tf.train.Saver()

# Launch the graph
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})
            
            
            acc_tst, loss_tst = sess.run([accuracy, cross_entropy], feed_dict={X: mnist.test.images, Y_: mnist.test.labels})
            
            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 backpropagationn training step
        sess.run(train_step, feed_dict={X: batch_X, Y_: batch_Y})



title = "MNIST 1 layer"
vis.losses_accuracies_plots(train_losses,train_acc,test_losses, test_acc,title,DISPLAY_STEP)

W wyniku działania skryptu dla 5k iteracji powinniście otrzymać wynik w konsoli:

#0 Trn acc=0.10000000149011612 , Trn loss=257.5960998535156 Tst acc=0.09359999746084213 , Tst loss=262.1988525390625
#100 Trn acc=0.8799999952316284 , Trn loss=42.37456512451172 Tst acc=0.879800021648407 , Tst loss=41.94294357299805
#200 Trn acc=0.8299999833106995 , Trn loss=50.943721771240234 Tst acc=0.883899986743927 , Tst loss=39.74076843261719
#300 Trn acc=0.8500000238418579 , Trn loss=39.26817321777344 Tst acc=0.9021999835968018 , Tst loss=33.9247932434082
# ....
#4800 Trn acc=0.9100000262260437 , Trn loss=32.18285369873047 Tst acc=0.9223999977111816 , Tst loss=27.490428924560547
#4900 Trn acc=0.9399999976158142 , Trn loss=19.267147064208984 Tst acc=0.9240999817848206 , Tst loss=27.271032333374023
#5000 Trn acc=0.949999988079071 , Trn loss=14.251474380493164 Tst acc=0.923799991607666 , Tst loss=27.61037826538086

Dodatkowo zostaną wygenerowane wykresy:

Accuracy i loss dla jednowarstwowej sieci neuronowej

Podsumowanie

W tym wpisie stworzyliśmy prostą jednowarstwową sieć neuronową w Tensorflow klasyfikującą obrazy z bazy MNIST. Udało się nam osiągnąć ponad 0.923 dokładność klasyfikacji. Podejście to należy rozpatrywać jako fundament pod przyszłe bardziej złożone modele, które będę opisywał w kolejnych odcinkach.

Jak zwykle kod źródłowy jest otwarty i ogólnie dostępny można go ściągnąc z Githuba w ramach projektu tensorflow-mnist-convnets

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

Join 99 other subscribers

10 Comments Jednowarstwowa sieć neuronowa w Tensorflow do klasyfikacji cyfr z MNIST

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

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