Klasyfikacja cyfr z MNIST przy pomocy sieci neuronowej w Tensorflow

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..

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.

MNIST i SVM klasyfikacja ręcznie pisanych cyfr

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 \\
\vdots  &\vdots & \ddots &\vdots \\
\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, tak więc na początku budujemy taki graf obliczeń, tensorflow go sobie wewnętrznie 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.

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

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

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.

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

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:

1 Comment Klasyfikacja cyfr z MNIST przy pomocy sieci neuronowej w Tensorflow

  1. Pingback: Wielowarstwowa sieć neuronowa w Tensorflow do klasyfikacji cyfr z MNIST - About Data

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *