Sieć konwolucyjna do rozpoznawania ciągu cyfr z obrazów

Post ten rozpoczyna serię, w której chciałbym przybliżyć tworzenie sieci neuronowych z wykorzystaniem biblioteki TensorFlow. Na początek przeanalizujemy architekturę sieci konwolucyjnej (ang. Convolutional Network) oraz przyjrzymy się warunkom w jakich taka sieć będzie w stanie wyuczyć się rozpoznawania wzorca, jako przykład posłuży nam problem rozpoznawania ciągu cyfr z obrazu.

Zachęcam do zapoznania się z drugą częścią artykułu:

Rozpoznawanie ciągu cyfr – definicja problemu i generowanie danych

Zazwyczaj wszelkie tutoriale dokonujące wprowadzenia do głębokich sieci neuronowych bazują na dwóch popularnych zbiorach MNIST lub CIFAR-10, ale jest ich i tak już wiele w sieci, więc nie będę ich powielał, zainteresowanych odsyłam do dokumentacji TensorFlow:

Ja postanowiłem was przeprowadzić przez proces tworzenia i uczenia sieci od początku do końca, a z mojego punktu widzenia ważne jest aby wyrobić sobie umiejętność definiowania tego co ma być na wyjściu sieci oraz określeniu funkcji straty. W przypadku powyższych dwóch zbiorów, sytuacja jest bardzo prosta. W obu zadanie polega na przydzieleniu obrazka do jednej z 10 klas, w przypadku MNIST jest to cyfra od 0-9 a w przypadku CIFAR-10 to zestaw dziesięciu pojęć (airplane, automobile, bird, cat, deer, dog, frog, horse, ship, truck).

My za zadanie postawimy sobie rozpoznanie ciągu wygenerowanych cyfr na obrazie, na początku w tym wpisie 1 cyfry, a w kolejnym zwiększymy ich ilość do 2 i 6. Pozwoli to nam prześledzić złożoność procesu uczenia sieci oraz kolejno będzie wymagało przyłożenia większej wagi do odpowiedniej inicjalizacji parametrów.

Generowanie danych

Ciągi cyfr wygenerujemy na podstawie skryptu, który opisałem w poprzednim poście, przypomnę tylko, że pozwalał on wygenerować zestaw obrazów z ciągiem losowo ułożonych cyfr narysowanych różnym krojem czcionek. W zależności od ilości cyfr w skrypcie należy ustawić różne parametry, ich zestawienie umieściłem w tabeli:

Data set name Font used Font size Image size (h,w) Images count
Digit_1f1  OpenSans-Regular.ttf  26  (32,28) 1000
Digit_2f1  OpenSans-Regular.ttf  26  (32,56)  1000
Digit_6f3  OpenSans-Regular.ttf, Mothproof_Script.ttf, Calligraffiti.ttf  26  (32,160)  3×1000

Czcionki można ściągnąć z OpenSans-Regular.ttf, Mothproof_Script.ttf, Calligraffiti.ttf. Powyższe zbiory można ściągnąć lub wygenerować. W drugim przypadku z uwagi na losowość wyniki mogą się nieznacznie różnić. Wygenerowane pliki zostały nazwane w formacie ‚00000_timestamp.png’, więc chcąc odczytać jakie cyfry znajdują się na obrazie wystarczy wziąć n-znaków do podkreślenia z nazwy pliku.

Architektura sieci do rozpoznawania cyfr

Postawione zadanie postaramy się rozwiązać z pomocą sieci neuronowej składającej się z:

  • dwóch warstw konwolucyjnych (conv1, conv2)
  • dwóch warstw dokonujących max-pool’ing
  • wykorzystującej technikę dropout
  • jednej warstwy w pełni połączonej (full connect – fc)
  • jednej warstwie wyjściowej (output)

Całość zrealizujemy z wykorzystaniem biblioteki TensorFlow.

One-hot encoding – jak określić wyjście sieci

Na początek zajmijmy się naszym najprostszym przypadkiem, a mianowicie rozpoznawaniem tylko 1 cyfry (zbiór Digit_1f1).
Zacznijmy od końca, czyli określenia tego co powinno być na wyjściu sieci. Każdy z obrazów może przynależeć do jednej z 10 kategorii (cyfry 0-9), dokonajmy kodowania każdej klasy do 10-wymiarowego wektora tzw. one hot-enconding. Polega ono na tym, że na pozycji określającej daną klasę stoi jedynka, a na pozostałych pozycjach zera.

  • cyfra ‚0’ zostanie zakodowana jako [1,0,0,0,0,0,0,0,0,0]
  • cyfra ‚1’  zostanie zakodowana jako [0,1,0,0,0,0,0,0,0,0]
  • cyfra ‚9’ zostanie zakodowana jako [0,0,0,0,0,0,0,0,0,1]

Kodowanie to zastosujemy w naszym zbiorze danych kodując w ten sposób etykiety wygenerowanych cyfr. Na taką reprezentację można patrzeć jak na rozkład prawdopodobieństwa tzn. z jakim prawdopodobieństwem cyfra na obrazie jest podobna do tych określonych przez nie zerową wartość na poszczególnych pozycjach. I takie spojrzenie będzie wygodne przy uczeniu naszej sieci, gdyż zazwyczaj na wyjściu sieci otrzymamy wektor, który nie będzie składał się z ‚0’i ‚1’, tylko miał wartości pośrednie. Wektor o współrzędnych [0,0,0,0.7,0,0,0.05,0,0.2,0.05] wskazuje, że najbardziej prawdopodobną cyfrą na obrazie jest ‚3’ (p-stwo 0.7) a następnie cyfra ‚8’ (p-stwo 0.2).

W ostatniej warstwie wyjściowej otrzymamy 10-wymiarowy wektor liczb, które jeszcze nam nie określą rozkładu prawdopodobieństwa. Liczby te powinny sumować się do 1, aby tak było powinniśmy te wartości znormalizować, zazwyczaj do tego celu używa się funkcji softmax.

Uczenie na podstawie funkcji straty

Uczenie nadzorowane w machine learning zazwyczaj przebiega w następujący sposób. Podajemy przykłady uczące do algorytmu, który na bazie parametrów (macierzy wag) przewiduje do jakiej klasy należą przykłady, następnie obliczamy błąd pomiędzy wartością zwróconą przez algorytm, a rzeczywistą wartością pochodzącą z zbioru danych. Obliczenie błędu wykonywane jest przy określonej przez nas ‚loss function’ (funkcję straty). Idea polega na zmianie parametrów modelu, tak aby zminimalizować wartość loss function na zbiorze treningowym.

W naszym przykładzie wykorzystamy jedną z gotowych w TensorFlow funkcji tf.nn.softmax_cross_entropy_with_logits (dobre wytłumaczenie tej funkcji można znaleźć na SO).

Kod sieci rozpoznającej jedną cyfrę

Na listingu poniżej znajduje się kod sieci rozpoznającej jedną cyfrę, cały kod dostępny jest na githubie (projekt „Numbers recoginiton”).
Na początku importujemy niezbędne biblioteki, najważniejsze dla nas to ‚tensorflow, numpy i matplotlib’. Biblioteka ‚data_helpers’ jest to własny skrypt znajdujący się w bieżącym folderze, którego zadaniem jest ułatwienie wczytywania danych oraz ich kodowania.

Wczytujemy dane do zmiennych X oraz Y, są to tablice z numpy. X – ma wymiary 1000×1024, gdzie w każdym wierszu znajduje się jeden obraz (32x32px=1024). Natomiast zmienna ma wymiary 1000×10, w każdym wierszu znajduje się 10-wymiarowy wektor kodujący cyfrę, która znajduje się na obrazie.

Następnie ustalamy podstawowe parametry uczące:

  • learning_rate – parametr uczący podczas optymalizacji
  •  batch_size – określa ile naraz będziemy przetwarzać obrazów (zazwyczaj 32,64,128)
  • trainning_iters – ile iteracji treningowych ma wykonać nasz algorytm
  • display_step – co ile iteracji ma zostać obliczona wartość funkcji straty oraz treningowe accuracy, przydaje się do testów
  • n_input – o jakich wymiarach jest wejście sieci (wymiary obrazu)
  • n_classes – ile mamy klas, o jakich wymiarach jest wyjście sieci

Budowa sieci w TensorFlow

Idea TensorFlow polega na zbudowaniu grafu obliczeniowego, będącego ciągiem kolejnych operacji na danych wejściowych. Następnie graf ten wewnętrzne jest kompilowany przez bibliotekę, aby w ramach sesji uruchomić obliczenia.

Dane do stworzonego grafu przekazywane są poprzez ‚placehoder’, można to rozumieć jako alokację pamięci dla danych wejściowych sieci. Dla każdego placeholdera należy podać wymiary oraz typ, w naszym skrypcie zadeklarowaliśmy trzy tego typu zmienne ‚x,y, keep_prob’. Jak łatwo się domyśleć zmienne te posłużą nam do przechowywania obrazów (x) oraz etykiet (y). Zmienna keep_prob – przechowuje wartość prawdopodobieństwa dla operacji dropout [1].

W celu uproszczenie procesu tworzenia sieci warstwa po warstwie, zdefiniowane zostały trzy funkcje:

  • conv2d – tworzy jedną warstwę konwolucyjną sieci, dokonuje konwolucji wykorzystując funkcję tf.nn.conv2d pomiędzy obrazem img a wagami w, parametr strides określa o ile okno konwolucji będzie się przesuwało, u nas o 1 w każdym wymiarze. Następnie dodajemy bias i stosujemy nielinowość w postaci funkcji relu
  • max_pool – dokonuje operacji pooling’u
  • funkcja conv_net wykorzystuje poprzednie funkcje w celu zbudowania pełnej sieci, warstwa po warstwie wraz z operacjami max_pool oraz dropout

Funkcje tworzące architekturę sieci

W funkcji conv_net na początku dokonujemy zmiany rozmiaru danych i tworzymy tensor o wymiarach [-1,32,32,1], pierwszy (-1) oznacza nasz batch_size (ilość obrazów), podanie -1 informuje o tym, że wymiar ten zostanie dopasowany do przekazanych danych. Następnie 32,32 oznaczają wysokość i szerokość obrazu, a ostatni 1 ilość kanałów w obrazie. My przetwarzamy obrazy w odcieniu szarości więc mamy tylko 1, w przypadku RGB ustawilibyśmy na 3.

Tworzymy kolejno dwie warstwy konwolucyjne (conv1,conv2) aby na końcu warstwę conv2 połączyć z pełną warstwą o 1024 neuronach. Wymaga to przeorganizowania wymiarów Tensora conv2 stąd ponowne użycie operacji reshape. Ostatecznie dokonujemy przemnożenia macierzy, czyli obliczenia aktywacji warstwy dense1, stosujemy relu i dropout. Warstwa dense1 połączona jest z warstwą wyjściową out.

Zmienne – jako parametry modelu w TensorFlow

Pod funkcją zdefiniowane są wartości wag i bias, należy tu być bardzo uważnym i zwracać uwagę na wymiary poszczególnych warstw. Każdą zmienną modelu, która będzie uaktualniana podczas procesu optymalizacji należy zdefiniować jako tf.Variable. W naszym przypadku definiujemy słownik takich zmiennych, aby łatwiej się nam nimi operowało. Pierwsza warstwa konwolucyjna ‚wc1’ ma wymiary 3x3x32, czyli ma 32 filtry, każdy o wymiarach 3×3, wartość 1 oznacza, że wejście do tej sieci ma głębokość 1(nasz obraz jest w odcieniach szarości, ma jeden kanał) . Druga warstwa konwolucyjna otrzymuje jako wejście macierz o głębokości równej poprzedniej warstwie czyli 32 (trzeci wymiar), rozmiar kernela jest także 3×3 a wyjście tej warstwy składa się z 64 filtrów. Mogą zastanowić wymiary wag w warstwie wd1, otóż po każdej warstwie stosujemy max_pooling, który za każdym razem zmniejszył wymiary obrazu o 2, czyli z 32×32 ostatecznie mamy 8×8 i to należy przemnożyć przez głębokość poprzedniej warstwy czyli 64.

Pozostaje nam już tylko zdefiniowane wywołanie funkcji conv_net aby zbudować nasz computation graph  i zapamiętać go w zmiennej  pred. Następnie definiujemy funkcję straty i tworzymy obiekt optymalizatora. Do optymalizacji wybraliśmy AdamOptimizer (obecnie zalecany). Mając zdefiniowany graf oraz funkcję straty nasz optymalizator potrafi automatycznie przejść po węzłach w grafie sam dokonać wstecznej propagacji błędu. To jest właśnie wielka zaleta TensorFlow, że wystarczy dobrze zbudować graf, a część związaną z uczeniem mamy już za darmo z biblioteki.

Uruchamianie obliczeń w sesji

Ostatnim krokiem jest uruchomienie obliczeń. TensorFlow w tym celu posługuje się pojęciem sesji, gdyż ten sam kod możemy uruchomić na CPU jak i GPU. Tutaj w pętli while wykonujemy kolejne kroki optymalizacji, wczytujemy daną porcję danych do zmiennych batch_xs, batch_ys, tą porcją danych „karmimy” nasz optymalizator. Dokonuje on zmian wag sieci na bazie przekazanych danych, zauważcie że każdą operację uruchamiamy poprzez sess.run().

Warunek if służy do sprawdzenia czy nasz model się uczy, w trakcie działania pętli co display_step iteracji obliczamy wartość loss function oraz liczymy dokładność klasyfikacji na danych treningowych, aby wyświetlić te dane w konsoli. Przy okazji dodajemy do list losses i accuracies wyliczone wartości, aby na końcu móc je zwizualizować.

Wygenerowanie wykresów

Na sam koniec z użyciem matplotlib wyświetlamy dwa wykresy, przedstawiające jak w trakcie uczenia zmieniały się loss function oraz accuracy

W wyniku działania skryptu powinniśmy otrzymać wykres podobny do tego:

convnet_one_digit_recognition

Z wykresu można wyczytać, że proces uczenia sieci przebiegał szybko i nasz problem rozpoznania cyfry jest prostym problem. Już po 100 iteracjach dokładność klasyfikacji wynosi 100%.

Podsumowanie

Wpis ten przedstawia budowę niedużej sieci konwolucyjnej służącej do rozpoznawania cyfr na obrazie. Jego głównym zadaniem jest wprowadzenia do biblioteki TensorFlow na działającym kompletnym przykładzie. W kolejnym cyklu przedstawię modyfikację skryptu, dzięki której będziemy mogli rozpoznawać nie jedną a kilka cyfr. Będzie wiązało się to zmianą architektury sieci, zmianą funkcji straty oraz zmianą sposobu kodowania wektora wyjściowego.

Post main image by: Maurice Peemen

 

[1] N. Srivastava, G. Hinton, A. Krizhevsky, I. Sutskever, and R. Salakhutdinov, „Dropout: a simple way to prevent neural networks from overfitting,” J. mach. learn. res., vol. 15, iss. 1, pp. 1929-1958, 2014.
[Bibtex]
@article{Srivastava2014,
author = {Srivastava, Nitish and Hinton, Geoffrey and Krizhevsky, Alex and Sutskever, Ilya and Salakhutdinov, Ruslan},
title = {Dropout: A Simple Way to Prevent Neural Networks from Overfitting},
journal = {J. Mach. Learn. Res.},
issue_date = {January 2014},
volume = {15},
number = {1},
month = jan,
year = {2014},
issn = {1532-4435},
pages = {1929--1958},
numpages = {30},
url = {http://dl.acm.org/citation.cfm?id=2627435.2670313},
acmid = {2670313},
publisher = {JMLR.org},
keywords = {deep learning, model combination, neural networks, regularization},
}

10 Comments Sieć konwolucyjna do rozpoznawania ciągu cyfr z obrazów

  1. Pingback: Sieć konwolucyjna do rozpoznawania ciągu cyfr część 2 - About Data

  2. Nickhodem

    Witam,
    Bardzo ciekawy artykuł, pomaga zrozumieć i poznać tensorflow. Dużo z niego korzystam 🙂
    Pojawiło się jedno issue, które nie pozwalało mi odtworzyć sesji. Sprawdzenie:
    if os.path.isfile(model_file):
    saver.restore(sess, model_file)
    nie działa, ponieważ nie ma takiego pliku 🙂 Oczywiście u siebie poprawiłem, ale ktoś może też mieć podobne problemy, tak więc ostrzegam.
    Powodzenia w dalszej pracy!

    Reply
  3. PawelPawel

    Dzień dobry,

    bardzo przydatny artykuł, dziękuję, rozjasniło mi to trochę jak to działa, ale wciąż nasuwają się pytania. Rozumiem, że nie każda linia będzie komentowana itd. ale przy tworzeniu sieci mamy

    # Output, class prediction
    out = tf.add(tf.matmul(dense1, _weights[‚out’]), _biases[‚out’])
    #out = tf.nn.softmax(out)

    Zakomentowana linia z uzyciem funkcji softmax nigdzie nie jest wspomniana ani użyta. Czy nie powinno to być odkomentowane w związku z „(…)aby tak było powinniśmy te wartości znormalizować, zazwyczaj do tego celu używa się funkcji softmax.”?

    Reply
    1. ksopyla

      Niestety są to pozostałości po moich różnych testach, softmax o którym piszesz powinien pozostać zakomentowany gdyż kilka lini dalej mamy:

      loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(pred, y))
      optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(loss)

      a tam wykorzystanie funkcji tf.nn.softmax_cross_entropy_with_logits, która już wewnętrznie oblicza softmax.

      Reply
  4. Piotrek

    Witaj, zacząłem czytać Twojego bloga kilka dni temu i bardzo mi się podoba, że ktoś w końcu poruszył temat tensorflow i uczenia maszynowego 🙂 Nie jestem informatykiem, ale postanowiłem zająć się tym, tematem w celu stworzenia projektu na studia, który wykorzystywałby sztuczne sieci neuronowe. Jednak, ciężko jest mi znaleźć zrozumiałe źródło tłumaczące zagadnienie wizualizacji. Jak zamienić model klasyfikujący, na taki, który coś generuje? Pomyślałem, że fajnie byłoby zrobić model, bazujący na rozwiązaniach z modelu inception-v3 od google. Chciałem wykorzystać bazę kompozycji graficznych(statyczna, dynamiczna, symetryczna, wertykalna, horyzontalna itp.), aby wyuczyć model, zdolności rozpoznawania kompozycji. Co do modelu inception-v3, są tutoriale do fine-tunningu sieci, na rozpoznawanie. Jednak aby osiągnąć wizualizację, np. deepdreamy – tutorial ze strony tensorflow, wykorzystuje inną wersję modelu inception, której nie można nauczyć tym samym skryptem, co model inception-v3(chyba nie zgadza się nazwa i ilość warstw). Chciałbym móc wybrać sobie, żeby model wygenerował czarno-białą kompozycję dynamiczną np. na obrazie 640x800px.
    Widziałem, że da się przykładowo wygenerować liczby z MNIST, albo rzeczy odpowiadające odpowiednim klasom nauczonego modelu inception – deepdreamy. Ciekawi mnie również zagadnienie neural-style-transfer, czyli przerzucania cech jednego obrazu, na drugi, dzięki wyuczonej sieci neuronowej. Fajnie by było, kiedyś poczytać coś o tym jak to działa, w przystępny sposób 🙂 Pozdrawiam!

    Reply
    1. ksopyla

      Dzięki za komentarz. Co do generowania obrazów to sam się powoli do tego przymierzam, lecz na razie jeszcze mi daleko do czegoś konkretnego. Temat jest bardzo ciekawy.
      Obecnie jedną z najczęściej wykorzystywanych technik jest „Generative adversarial networks (GAN)” i możesz na to spojrzeć, jak się poszuka na githubie to da radę znaleźć przykładowy kod, który może uda się tobie dostosować to twoich potrzeb (choć z doświadczenie wiem że nie jest to często takie proste).
      Na youtubie znajdziesz także kilka prezentacji prowadzonych przez Iana Goodfellow, jednego z twórców GAN.
      Życzę powodzenia.

      Reply
  5. Rafal

    Bardzo ciekawy tutorial. Dziekuje.
    W skrypcie generate_digits.py usunalem komentarz w linii 119, zeby zapisywac obrazy generowanej liczby (cyfry) oraz odkomentowalem linie 74 random_digits=1. W ten sposob otrzymalem 10 obrazkow z wygenrowanymi pojedynczymi cyframi zapisanymi w katalogu \shared\Digits_2f1. Moze ten krok robie niepotrzebnie?
    Pozniej uruchomilem skrypt conv_net_one.py zeby wytrenowac siec.
    Najpierw otrzymuje ostrzezenie:
    WARNING:tensorflow:From conv_net_one_digit.py:105 in .: initialize_all_v
    ariables (from tensorflow.python.ops.variables) is deprecated and will be remove
    d after 2017-03-02.

    a pozniej blad:

    Traceback (most recent call last):
    File „conv_net_one_digit.py”, line 130, in
    batch_xs, batch_ys, idx = dh.random_batch(X, Y, batch_size)
    File „C:\Users\BRM738\Documents\numbers_recognition\data_helpers.py”, line 113
    , in random_batch
    raise ValueError(‚Batch cant be larger then X has rows’)
    ValueError: Batch cant be larger then X has rows

    Prosze o pomoc, jak ustawic parametry, zeby nie bylo bledu
    pozdrawiam
    Rafal

    Reply
    1. ksopyla

      Cześć, dzięki za dobre słowo.

      Co do pierwszego ostrzeżenia to z tego co pamiętam to projekt robiłem w tensorflow 0.12 teraz jest wersja 1.0 i funkcja intialize_all_variables została zmieniona na global_variables_initializer

      Natomiast co do błędu to wynika on z tego że masz za mało wygenerowanych obrazów w zbiorze treningowym (10 obrazów) w stosunku do ‚batch’, który wynosi 64. Mój błąd, zmień linie 93 w generate_digits na liczbę większą niż 64, proponuję na początek na 128, później można spróbować na większej ilości (kilka tysięcy) (https://github.com/ksopyla/numbers_recognition/blob/master/generate_digits.py#L93)

      Reply
      1. Rafal

        Dziekuje.
        Moj blad wystapil rowniez gdy uzywalem TF012. Dzisiaj upgradowalem do TF1.0. Poniewaz pracuje na Win7/64 wiec musialem najpierw downgradowac Python’a z 3.6 do 3.5.2.
        Zmienilem inicjalizacje zgodnie z Twoja uwaga. Teraz mam:
        # Initializing the variables
        init = tf.global_variables_initializer()

        Parametry, ktore mam w connv_net_one_digit:

        # Parameters
        learning_rate = 0.001
        batch_size = 64
        training_iters =500
        display_step = 50

        Ustawilem w generate_digit wartosci wieksze od 64. Zaczalem od 128 teraz mam 640.

        folder=’shared/Digits_2f1′
        #how many images with one type of font, final dataset has size number_of_images*number_of_fonts
        number_of_images=640
        dispaly_count=1

        Niestety ciagle ten blad:
        ValueError: Batch cant be larger then X has rows

        File „conv_net_one_digit.py”, line 130, in
        batch_xs, batch_ys, idx = dh.random_batch(X, Y, batch_size)
        File „C:\Users\Public\Documents\Python Scripts\numbers_recognition\data_helper
        s.py”, line 113, in random_batch
        raise ValueError(‚Batch cant ValueError: Batch cant be larger then X has rows
        be larger then X has rows’)

Dodaj komentarz

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