DSZI_2020_Projekt/Rozpoznawanie talerzy Sara Kowalska.md

158 lines
8.6 KiB
Markdown

##### Raport przygotowała: Sara Kowalska
##### Raportowany okres: 27 kwietnia - 3 maja 2020
##### Niniejszy raport poświęcony jest przekazaniu informacji na temat stanu mini-projektu indywidualnego w ramach projektu grupowego realizowanego na przedmiot Sztuczna Inteligencja w roku akademickim 2019/2020.
Tematem realizowanego projektu indywidualnego jest stworzenie sztucznej inteligencji rozpoznającej ze zdjęcia czy na talerzu jest jedzenie, czy też jest on brudny (po jedzeniu). Do tego celu wykorzystałam sieci neuronowe. Wykorzystane biblioteki:
- Keras (backend: Tensorflow),
- Pillow (obsługa zdjęć).
## Uczenie modelu
#### Dane wejściowe:
Do utworzenia modelu przygotowałam zdjęcia talerzy podzielonych na dwie kategorie:
- food - zdjęcia talerzy z jedzeniem,
- dirty - zdjęcia talerzy "brudnych".
Zestaw treningowy (train) zawiera po 70 zdjęć z każdej kategorii. Oprócz tego przygotowałam zestaw testowy (validation), składający się z 40 zdjęć (po 20 z kategorii).
Wszystkie obrazy są normalizowane do rozmiaru 256x256 px.
#### Proces uczenia:
Na początek inicjalizuję sieć neuronową, w moim przypadku jest to sieć sekwencyjna (zatem wykonująca instrukcje po kolei).
> ```python
> model = Sequential()
> model.add(Conv2D(32, (2, 2), input_shape=input_shape))
> model.add(Activation('relu'))
> model.add(MaxPooling2D(pool_size=(2, 2)))
>
> model.add(Conv2D(32, (2, 2)))
> model.add(Activation('relu'))
> model.add(MaxPooling2D(pool_size=(2, 2)))
>
> model.add(Conv2D(64, (2, 2)))
> model.add(Activation('relu'))
> model.add(MaxPooling2D(pool_size=(2, 2)))
> ```
Jako pierwszą do modelu dołączam warstwę **operacji splotu** (**konwolucji**), która polega na przesuwaniu filtru (mnożenie przez macierz, tutaj 2x2, liczba filtrów, których dana warstwa się uczy = 32) wzdłuż obrazu, aby dla każdego jego fragmentu obliczyć splot między nim a filtrem (ekstrakcja cech). Z tą warstwą dodany jest również format wejścia (dodany raz, odczytywany jest przez wszystkie kolejne warstwy).
Następnie dodana zostaje funkcja aktywacji - **ReLU**: f(x) = x+ = max(0, x), x - dane wejściowe, która zeruje negatywne wartości (korzystamy z niej, ponieważ w wyniku spodziewamy się uzyskać prawdopodobieństwo, które nie może być ujemne).
Kolejna zostaje załadowana warstwa **MaxPooling2D**, która zmienia rozdzielczość obrazka. Jest ona podobna do zastosowania filtru, z tym że tutaj nie stosujemy mnożenia, a wyciągamy największą wartość z wycinka obrazka. Dodatkowo tu "okno" operacji przesuwa się o swoją szerokość, a nie jak w Conv2D o 1. punkt. Dzięki temu zmniejszamy rozmiar danych w sieci oraz liczbę trenowanych cech.
W ten sam sposób załadowane zostają kolejne dwie sekwencje warstw (w 3. zwiększamy liczbę filtrów na 64).
> ```python
> model.add(Flatten())
> model.add(Dense(64))
> model.add(Activation('relu'))
> model.add(Dropout(0.5))
> model.add(Dense(1))
> model.add(Activation('sigmoid'))
> ```
Kolejną dodaną warstwą jest **Flatten()**, która spłaszcza macierze do wektorów oraz warstwa **Dense(64)** - jest to warstwa z 64 neuronami. Po raz kolejny dodajemy funkcję **ReLU**, w celu wyzerowania wartości negatywnych.
Następnie poprzez dodanie warstwy **Dropout(0.5)** losowo odrzucamy 50% danych, aby uniknąć przeuczenia sieci.
Następnie dodajemy warstwę **Dense(1)** z 1. neuronem - standardowo na wyjściu liczba neuronów powinna odpowiadać liczbie klas (wektor zer i jedynek wskazujący, który z neuronów powinien zostać aktywowany), jednak ze względu na to, iż w naszym przypadku mamy tylko dwie klasy (food oraz dirty), możemy zastosować podejście z jednym neuronem - 1. elementowe wyjście (0 dla klasy dirty, 1 dla klasy food).
Na końcu dodajemy funkcję aktywacyjną **sigmoid**, która "upycha" wartości w przedział [0, 1], co pozwala na generowanie wartości, które możemy zinterpretować jako prawdopodobieństwo.
> ```python
> model.compile(loss='binary_crossentropy',
> optimizer='rmsprop',
> metrics=['accuracy'])
> ```
Następnym krokiem jest kompilacja modelu. Ponieważ mamy do czynienia z problemem klasyfikacji binarnej (tylko dwie klasy), wybraną funkcją straty jest **binary_crosentropy** (binarna entropia krzyżowa). Wykorzystałam optymalizator **rmsprop** oraz przy pomocy parametru *metrics* dodałam monitorowanie dokładności.
> ```python
> train_datagen = ImageDataGenerator(
> rotation_range=45,
> width_shift_range=0.3,
> height_shift_range=0.3,
> rescale=1./255,
> shear_range=0.25,
> zoom_range=0.1,
> horizontal_flip=True)
>
> validation_datagen = ImageDataGenerator(rescale=1. / 255)
> ```
Następnie przygotowuję generatory, odpowiednio **train_datagen** dla danych uczących oraz **validation_datagen** dla danych testowych. Dla dobrego uczenia na wszystkich zdjęciach musimy zastosować parametr *rescale=1. / 255*. Dodatkowo, ponieważ głębokie uczenie najlepiej sprawdza się dla dużych zbiorów danych (a mój zbiór nie jest bardzo liczny), korzystam ze strategii augumentacji danych - na każdym zdjęciu treningowym generator losowo stosuje dostępne parametry, dzięki temu w każdej epoce uczenia (epoka - iteracja na wszystkich próbkach) w zestawie treningowym dostajemy nowe dane, zbliżone do oryginalnych, jednak przez sieć traktowane jak nowy, inny zbiór.
> ```python
> train_generator = train_datagen.flow_from_directory(
> train_data_dir,
> target_size=(img_width, img_height),
> batch_size=batch_size,
> class_mode='binary')
>
> validation_generator = validation_datagen.flow_from_directory(
> validation_data_dir,
> target_size=(img_width, img_height),
> batch_size=batch_size,
> class_mode='binary')
>
> model.fit_generator(
> train_generator,
> steps_per_epoch=nb_train_samples // batch_size,
> epochs=epochs,
> validation_data=validation_generator,
> validation_steps=nb_validation_samples // batch_size)
>
> model.save_weights('model_food_dirty.h5')
> ```
Ostatnim krokiem jest załadowanie danych z odpowiednich folderów do generatora - **train_generator** i **validation_generator** (ładujemy zdjęcia na ustalonym rozmiarze w trybie binarnym - 2 klasy) oraz uruchomienie uczenie modelu **model.fit_generator** (batch_size - rozmiar pojedynczej partii danych = 16; 20 epok, liczba kroków w ramach jednej epoki obliczana jest przez ilość danych treningowych/testowych przez rozmiar partii).
Na końcu zapisuję wagi modelu w pliku *model_food_dirty.h5*.
## Integracja z projektem
W funkcji *main()* tworzę model sekwencyjny o parametrach uczonego przeze mnie wcześniej modelu oraz ładuję wagi nauczonego modelu.
> ```python
> img_classify = Sequential()
> makeImgClassificator(img_classify)
> img_classify.load_weights('Sara/model_food_dirty.h5')
> ```
<img src="https://git.wmi.amu.edu.pl/s444412/DSZI_2020_Projekt/raw/master/Raporty/plansza_menu.png" align="right" width="350"/>
Po wybraniu na ekranie głównym opcji *Rozpoznawanie talerzy*, uruchomiona zostaje funkcja *Classify()*, która inicjuje przykładowy początkowy stan restauracji (dodanie kilku klientów, przypisanie im stołów i talerzy) oraz nakazuje agentowi podejście do każdego z klientów i sprawdzenie czy posiadany przez niego talerz jest pusty czy pełny poprzez funkcję *predictAndShowImg()*:
> ```python
> def predictAndShowImg(plate):
> img_width, img_height = 256, 256
> plate.draw()
> test_image = load_img(plate.img, target_size=(img_width, img_height))
> test_image = img_to_array(test_image)
> test_image = test_image.reshape((1,) + test_image.shape)
>
> result = img_classify.predict(test_image)
> print(result)
> if (result == [1.]):
> messagebox.showinfo("Rozpoznanie", "Talerz pełny.")
> return 1
> else:
> messagebox.showinfo("Rozpoznanie", "Talerz pusty.")
> return 0
> ```
Funkcja ta przyjmuje jako argument obiekt talerz (plate) danego klienta, wyświetla odpowiednie zdjęcie (w domyślnej przeglądarce systemowej) i za pomocą **img_classify.predict(test_image)** przekazuje wytrenowanej sieci zdjęcie do rozpoznania. W zależności od werdyktu wyświetlane jest okno z odpowiednią informacją.
Dodatkowo, jeżeli sieć rozpozna pusty talerz, agent zostaje wysłany do kuchni przed odwiedzeniem kolejnego stolika (imitacja odnoszenia brudnego talerza).
Ponieważ na chwilę obecną program nie posiada innych dostępnych projektów do uruchomienia, po zakończeniu trasy wyświetlany jest stosowny komunikat, a aplikacja zostaje wyłączona.