MNIST dataset – sieci neuronowe, część 2 (Keras)

Tym razem przygotujemy środowisko do pracy z Keras i rozpoznamy kilka liczb (ponownie jak poprzednio).

Instalacja Keras

Zaczniemy od instalacji. Potrzebujemy zainstalowanego Pythona na maszynie. W unixach (w tym odcinku korzystam z serwera EC2 w AWS) nie jest to problem, pod Windowsem można zainstalować go na przykład instalując Anacondę.

Kolejny krok to przygotowanie R do pracy z Keras. Najpierw instalujemy bibliotekę keras:

a następnie sam silnik:

Ta ostatnia funkcja ma trochę parametrów – między innymi odpowiedzialnych z zainstalowanie TensorFlow skompilowanego pod korzystanie z GPU (wykorzystanie kart graficznych do obliczeń – znacznie przyspiesza to cały proces, ale potrzebny jest stosowny sprzęt). Odpowiedni ich opis znajdziecie w dokumentacji. Szczegółową dokumentację Keras znajdziecie na stronie Keras.io.

Proces instalacji nieco trwa za pierwszym razem, później jest już z górki. Przy instalacji na Windowsie może się coś pogryźć w Anacondzie (mi powstało drugie środowisko i trochę z tym trzeba powalczyć). Na maszynie EC2 (szczególnie t2.micro) może być okazać, że zabraknie pamięci (tak się stało dla sieci opisywanych poniżej). Rozwiązaniem jest zwiększenie wielkości tzw. swapu czyli wirtualnej pamięci, która tak na prawdę jest miejscem na dysku. Znalazłem odpowiednie obejście na niezastąpionym Stackoverflow. Stosujemy więc zwiększenie swapu, po resecie maszyny wszystko wraca do normy (chyba, że potrzebujemy pozostać przy powiększonym miejscu – odpowiednia zmiana opisana jest też na SO).

Przygotowanie danych

Pierwszy krok to przygotowanie danych – podobnie jak poprzednio wykorzystamy pliki CSV z Kaggle.

Mamy dane. Dla każdego wiersza danych pierwsza kolumna opisuje nam co widzimy w danych zapisanych w kolejnych 784 kolumnach. Widzimy liczbę od 0 do 9. Keras potrzebuje tej informacji w formie binarnej macierzy: każdy wiersz to jedna liczba (ta z obrazka), a w odpowiedniej kolumnie mamy 1 jeśli to właśnie ta liczba lub zero jeśli to nie ona. Czyli dla czwórki w kolumnie 5 (liczymy od 0) będzie 1, a w pozostałych zera – widać to w czwartym wierszu poniższej tabeli. To one-hot-encoding – rozsmarowanie cech kategorycznych (czyli takich, które można wylistować czy też zesłownikować) na kolumny i umieszczenie w odpowiednich kolumnach jedynki.

Spójrzmy zresztą na wynik:

0 1 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0
0 1 0 0 0 0 0 0 0 0
0 0 0 0 1 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0

Pozostałe dane to cechy (784 punktów dla każdej ręcznie pisanej liczby). Przekształcamy data frame jaki mamy z wczytania CSV na macierz (Keras/TensorFlow operuje na macierzach; właściwie cały deep learning to operacje na macierzach).

Mamy więc macierz odpowiedzi (10 kolumn z 1 lub 0 w 42000 wierszach – tyle ile liczb w zbiorze treningowym) oraz 784 cech (macierz 28×28 pikseli rozciągniętych na wektor).

Tutaj ważna sprawa: podobnie jak poprzednio dane opisujące liczbę traktujemy jako wektor czyli ciąg 784 wartości, a nie jak obrazek (macierz). To ważne założenie i ma wpływ na sposób budowania sieci neuronowych. W kolejnym odcinku obraz potraktujemy już jak obraz.

Zbudujemy pierwszy model – 10 neuronów w jednej warstwie. W poprzednim odcinku dla pełnych danych z użyciem sieci zbudowanej przy pomocy pakietu nnet udało się uzyskać dokładność rozpoznania liczb na poziomie 63.4% – to będzie nasz punkt odniesienia.

Model 1

Jak wygląda budowanie modelu w Keras? Bardzo prosto (a operator %>% pipe jeszcze bardziej to upraszcza). Pierwszy element to rodzaj modelu – w naszym przypadku będzie to model sekwencyjny (jedna warstwa za drugą). Do takiej informacji dodajemy kolejne warstwy.

W pierwszej warstwie musimy określić rozmiar danych wejściowych (długość wektora, wielkość macierzy i liczbę jej wymiarów itd), w ostatniej – liczbę wyjść. Tutaj mamy 10 wyjść, bo tyle mamy klas.

Po zbudowaniu architektury musimy ją skompilować (czyli wysłać do Kerasa w Pythonie, bo wszystko dzieje się tak na prawdę w Pythonie, R jest tutaj opakowaniem).

Widzimy, że nasz prosty model składa się z jednej warstwy w której powstaje 7850 parametrów. Dlaczego tyle? Mamy 10 neuronów i 784 cechy. To już po przemnożeniu daje 7840, gdzie brakujące 10? To wolne parametry, niezwiązane z wejściem (bias) dla każdej z klas.

Mając architekturę – czas na wytrenowanie modelu. To właśnie tutaj dzieją się obliczenia polegające na znalezieniu odpowiednich parametrów:

Wykresy obrazują uzyskane współczynniki określające jakość modelu w kolejnych krokach epoch. Co oznaczają te współczynniki?

  • loss – błąd uczenia sieci, im mniejszy tym lepiej
  • acc – poprawność modelu w przypisaniu klas na danych treningowych
  • val_loss oraz val_acc są analogiczne do powyższych, z tą zasadniczą różnicą, że val_ to miara na części sprawdzającej danych treningowych, czyli tej która nie była wykorzystana do uczenia (Keras sam sobie to dzieli, nie musimy robić tego wprost).

Której miary użyć? Jeżeli chcemy sprawdzić poprawność działania modelu na nowych danych (po to robi się modele: uczymy na czymś co znamy, a nieznane wrzucamy do modelu i oczekujemy wyników) powinniśmy patrzyć na wartość val_acc.

Po co zatem acc i val_acc? Ano po to, ze w sytuacji kiedy acc rośnie, a val_acc maleje mamy do czynienia z overfittingiem czyli nadmiernym dopasowaniem modelu do danych, co w dużym uproszczeniu można wytłumaczyć tym że sieć zapamiętuje odpowiedzi a nie uczy się.

W powyższym modelu widzimy, że po 10 epochach wyniki stabilizują się i acc podąża mniej więcej równo z val_acc. Otrzymany wynik to 55.52% dokładności rozpoznania liczby co już zachęca do dalszych działań (to ponad 10 punktów procentowych lepiej niż wynik otrzymany z użyciem nnet).

Model 2

W drugim modelu dodamy jedną warstwę z 5 neuronami. Ścieżka jest taka sama, zatem:

Architektura sieci:

Trening modelu:

Tutaj z kolei widać, że 15 epochów to jeszcze trochę mało (krzywe nie wypłaszczyły się w całości, widać potencjał dla kolejnych kilku epochów). Osiągnęliśmy tym razem 41.65% dokładności. Wynik jest gorszy, ale zwróćcie uwagę że mniejsza była liczba możliwych do obliczenia parametrów.

Model 3

Dodajemy zatem jeszcze jedną warstwę i zwiększamy znacznie liczbę neuronów:

Tym razem widzimy ciekawostkę – krzywe acc od początku są na podobnym poziomie, kolejne iteracje nie przynoszą widocznej poprawy wyniku. Sam wynik też jest mało zadowalający – 44.61%. Mamy do czynienia z sytuacją opisaną wyżej (overfitting): acc rośnie, val_acc maleje. Trzeba zatem coś zrobić z siecią.

Dodamy warstwy dropout, które przepuszczają tylko część danych (wskazany procent). Jest to dobry sposób na unikniecie przeuczenia sieci. Czy zadziała w naszej architekturze?

Model 4

Trzy warstwy neuronów, przeplatane warstwami dropout:

Po 15 epochach trudno powiedzieć, czy pomogło – osiągnęliśmy jeszcze gorszy wynik (13.96%), ale widać że krzywe się nie wypłaszczyły. Co ciekawe – zarówno krzywe acc jaki i loss są od siebie sporo oddalone, co nie jest zdrowe. Mamy ponad 111 tysięcy parametrów, którymi można kręcić i na nic to… Jak krew w piach.

Spróbujmy jednak czego innego – z wartości opisujących każdy punkt z zakresu 0-255 przejdźmy na zakres 0-1. Zwykłe przeskalowanie danych źródłowych. W zasadzie nie powinno niczego zmienić, prawda?

Model 4b

Dane znormalizowane do przedziału [0, 1], architektura sieci jak w modelu 4.

Architektura pozostała bez zmian, zmienione zostały (tylko przeskalowane!) wartości cech. Uzysk jest jak widać olbrzymi, mamy najlepszy dotychczasowy wynik – val_acc rzędu 95.81%, acc 92.07%. Szok i niedowierzanie!

Zwiększmy zatem dwukrotnie wielkość wszystkich warstw:

Już pierwsze epoch-i dały wynik ponad 90% dokładności. Po 15 skończyliśmy na prawie 96.95% val_acc i 94.91% acc. Można tak jeszcze trochę, na przykład dodając kolejne warstwy i zwiększając liczbę neuronów w nich, a przede wszystkim trenując sieć dłużej (dopóki rośnie val_acc). Nie da się tak jednak w nieskończoność, gdzieś jest granica (dla tego typu sieci).

Model 5

W ostatnim modelu dodajmy jeszcze jedną warstwę i dwukrornie zwiększmy liczbę neuronów w warstwach znanych z modelu 4b oraz wydłużmy dwukrotnie czas treningu. Usuniemy też jedną warstwę dropout:

Przedobrzyliśmy. Żarło, żarło, ale zdechło. 20.61% val_acc i 13.51% acc. Może to wina zrezygnowania z jednej warstwy dropout, może sieć miała za dużo neuronów? To niestety trzeba sprawdzać empirycznie.

Warto też pokręcić parametrami compile() – przede wszystkim dobierając inny optimizer.

W kolejnych krokach użyjemy modelu 4b (dał najlepsze wyniki).

Wykorzystanie modelu

Dobrze, mamy wytrenowany model, a jak teraz mając nowe dane użyć modelu do (w tym przypadku) rozpoznawania liczb pisanych ręcznie? Dość prosto:

Mając przygotowane dane testowe (nieznane) możemy przepuścić je przez model i sprawdzić co dostaniemy na wyjściu. A dostać możemy albo konkretną klasę albo prawdopodobieństwo przypisania do niej:

Zobaczmy jak wygląda prawdopodobieństwo dla kolejnych liczb (danyych testowych):

0.0000000 0.00e+00 1.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000
1.0000000 0.00e+00 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000
0.0000000 0.00e+00 0.0000000 0.0000647 0.0043258 0.0000055 0.0000000 0.0005651 0.0000617 0.9949772
0.0006655 6.46e-05 0.0005053 0.0011611 0.3349387 0.0009262 0.0001943 0.0225850 0.0026711 0.6362880
0.0000000 1.00e-07 0.0000257 0.9998727 0.0000000 0.0000119 0.0000000 0.0000005 0.0000882 0.0000007
0.0000000 1.00e-07 0.0000332 0.0000402 0.0000003 0.0000000 0.0000000 0.9899057 0.0000031 0.0100174

No dużo liczb. Wykres poproszę! Na przykład dla szóstej w kolejności liczby:

Wygląda na to, że w 98.99% jest to siódemka, a w 1.00% dziewiątka. Sama liczba wygląda tak (ta funkcja już była):

Świetnie! Jest to siódemka, ale kreseczka przecinająca nóżkę (jak to inaczej nazwać?) może zasugerować, że to niedokończona dziewiątka.

Predykcja klasy

Zamiast liczenia prawdopodobieństwa przypisania do każdej z klas możemy po prostu wybrać najbardziej prawdopodobną klasę:

Jaki jest wynik dla poprzednio badanej liczby?

Spróbujmy poszukać liczb, z którymi był największy problem. Możemy to zrobić tylko w danych treningowych – w testowych nie mamy odpowiedzi.

y_train 0 1 2 3 4 5 6 7 8 9
0 4096 0 1 1 3 1 18 0 9 3
1 0 4625 20 3 8 1 3 7 14 3
2 8 2 4083 12 8 0 7 22 30 5
3 5 0 37 4162 0 29 2 21 76 19
4 4 5 3 0 3985 2 18 2 8 45
5 10 2 3 29 4 3672 30 0 30 15
6 14 2 0 0 5 8 4102 0 6 0
7 7 11 22 2 10 1 0 4274 6 68
8 7 16 11 11 3 11 17 2 3970 15
9 9 3 1 16 42 8 1 26 11 4071

Usuńmy dobre wyniki (zaciemnią nam obraz) i zobaczmy na diagramie które liczby są ze sobą najbardziej mylone:

Dużo pomyłek jest pomiędzy 4 (prawdziwa liczba) i 9 (liczba rozpoznana przez sieć). Zobaczmy więc różne czwórki:

Jak widać kilka z czwórek można pomylić z dziewiątkami… wszystko się zgadza!

Podsumowanie

  • zainstalowaliśmy bibliotekę Keras, która z poziomu R pozwala na budowanie sieci neuronowych opartych o TensorFlow
  • zbudowaliśmy kilka modeli sieci – z różną liczbą warstw oraz neuronów w tych warstwach
  • dowiedzieliśmy się co oznaczają wskaźniki acc, val_acc, loss oraz val_loss i jak na nie patrzyć
  • udało nam się dość szybko zbudować sieć dającą dokładność rozpoznawania liczb na poziomie około 97%

W następnej części spróbujemy podejść do tego samego problemu traktując liczby jako obrazki (czyli macierz 28×28) a nie jako wektor liczb. Za punkt wyjścia weźmiemy 97% dokładności – rekord to zdaje się około 99.7% (co oznaczałoby jakieś 120 źle rozpoznanych liczb z naszego zbioru treningowego liczącego 42 tysiące…).

Dodaj komentarz