DSZI_2020_Projekt/Rozpoznawanie talerzy Sara Kowalska.md

8.6 KiB

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

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

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.

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.

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.

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.

img_classify = Sequential()
makeImgClassificator(img_classify)
img_classify.load_weights('Sara/model_food_dirty.h5')

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():

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.