Raaport - Rozpoznawanie talerzy Sara Kowalska

This commit is contained in:
Sara Kowalska 2020-05-04 11:28:59 +02:00
parent bfd41cd46f
commit 7d762e234f
4 changed files with 162 additions and 6 deletions

BIN
Raporty/plansza_menu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -3,9 +3,6 @@ from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Activation, Dropout, Flatten, Dense from keras.layers import Activation, Dropout, Flatten, Dense
from keras import backend as K from keras import backend as K
import json
import numpy as np
import matplotlib.pyplot as plt
img_width, img_height = 256, 256 img_width, img_height = 256, 256

View File

@ -3,14 +3,14 @@ import random
import time import time
import queue import queue
import math import math
from keras.models import load_model, Sequential, model_from_json from keras.models import load_model, Sequential
from keras.preprocessing.image import img_to_array, load_img from keras.preprocessing.image import img_to_array, load_img
from keras.layers import Conv2D, MaxPooling2D from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Activation, Dropout, Flatten, Dense from keras.layers import Activation, Dropout, Flatten, Dense
from keras import backend as K from keras import backend as K
from tkinter import messagebox, Label from tkinter import messagebox
import tkinter as tk import tkinter as tk
from PIL import Image, ImageTk from PIL import Image
root = tk.Tk() root = tk.Tk()
root.withdraw() #ukrycie okna tworzonego przez tkinter root.withdraw() #ukrycie okna tworzonego przez tkinter

View File

@ -0,0 +1,159 @@
##### 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, splot = 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 czemu 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 splot 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')
> ```
Zdjęcie!
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.