Zadanie: zastosowanie konwolucyjnej sieci neuronowej do wykrywania obiektów na obrazach

Celem zadania jest przećwiczenie różnych podejść do budowania klasyfikatorów obrazów za pomocą konwolucyjnej sieci neuronowej. Dany jest zbiór obrazów, i zadaniem jest wytrenowanie CNN do rozpoznania obrazów przedstawiających samochody. CNN generalnie potrzebują dużych ilości danych i przy użyciu zadanego zbioru danych może nie udać się osiągnięcie bardzo dobrych wyników z uczenia CNN, więc alternatywnie zbadamy możliwość wykorzystania wcześniej wytrenowanego modelu CNN dla wykonania innego zadania klasyfikacji obrazu.

Przygotowanie do wykonania ćwiczenia

Przejrzyj materiały dotyczące konwolucyjnych sieci neuronowych.

Zainstaluj Keras na komputerze, na którym będziesz wykonywać zadanie. Przy użyciu systemu Anaconda użyj conda install tensorflow (Keras powinien być dołączony do biblioteki TensorFlow). Alternatywnie można wykorzystać pip, i zainstalować również h5py aby umożliwić zapisywanie wyuczonych modeli. Zainstaluj również bibliotekę przetwarzania obrazów Pillow, która jest potrzebna funkcjom Keras, które ładują obrazy. (conda install pillow lub pip3 install Pillow)

Niektóre elementy ćwiczenia będą wykonywały się bardzo długo. Rozważ wykonywanie zadania na wydajnym komputerze, do którego możesz uzyskać dostęp (zapytaj wykładowcy). W szczególności korzystne może być użycie karty GPU.

Wykonanie ćwiczenia - wstęp: ładowanie obrazów z katalogu

Rozpakuj pakiet zawierający obrazy. Pochodzą one ze zbioru danych PASCAL Visual Object Classes Challenge 2010 (VOC2010). Obrazy zostały podzielone na zbiór uczący i walidacyjny, a każdy podzbiór zawiera oddzielny katalog dla każdej z dwóch klas (car i other).

Do wczytywania pojedynczych obrazów można wykorzystać funkcję Kerasa load_img. Alternatywnie można utworzyć obiekt ImageDataGenerator do konwersji obrazów na dane NumPy. Parametr rescale oznacza, że skalowanie wartości kolorów w zakresie od 0 do 1, zamiast od 0 do 255.

from tensorflow.keras.preprocessing.image import ImageDataGenerator
data_gen = ImageDataGenerator(rescale=1.0/255)

Teraz można wczytać obrazy zbioru uczącego wywołując flow_from_directory co powoduje przekształcenie przez ImageDataGenerator obrazów z katalogu.

Parametry:

Można zmniejszyć rozmiar obrazów do wartości mniejszej niż 64x64. Spowoduje to mniejszą dokładność klasyfikacji, ale zwiększy wydajność. Z drugiej strony, jeśli masz do dyspozycji bardzo wydajny komputer, możesz rozważyć zwiększenie rozmiaru obrazu.

imgdir = 'images'
img_size = 64
batch_size = 32
train_generator = data_gen.flow_from_directory(
        imgdir + '/train',
        target_size=(img_size, img_size),
        batch_size=batch_size,
        class_mode='binary',
        classes=['other', 'car'],
        seed=12345,
        shuffle=True)

Spróbujmy odczytać jedną partię obrazów z generatora:

Xbatch, Ybatch = train_generator.next()

Przyjrzyjmy się zwróconemu obiektowi NumPy. Jest to czterowymiarowa tablica NumPy (formalnie czterowymiarowy tensor): wielkość wsadu (32 obrazy), wysokość i szerokość każdego obrazu (po 64), i wartość koloru (3 - R/G/B) w zakresie od 0 do 1.

Xbatch.shape

Przyjrzymy się pierwszej instancji. Najpierw sprawdźmy, czy to samochód (etykieta 1), czy nie (0). Ponieważ zadaliśmy shuffle=True obrazy pojawiają się w przypadkowej kolejności.

Ybatch[4]

Następnie wyświetl obraz za pomocą plt.imshow (zakładając, że matplotlib.pyplot został zaimportowany jako plt). Aby zobaczyć obraz należy wywołać plt.show() lub plt.savefig(). Obejrzenie obrazu powinno potwierdzić poprawność etykiety (pod warunkiem, że uda się rozpoznać obiekt na obrazku 64x64).

from matplotlib import pyplot as plt
plt.imshow(Xbatch[4]);

Wykonanie ćwiczenia - część 1: trenowanie konwolucyjnej sieci neuronowej

Napisz funkcję make_convnet która zbuduje konwolucyjną sieć neuronową. Kształt wejściowy powinien być taki (img_size, img_size, 3), gdzie img_size jest rozmiarem obrazu, który zdefiniowaliśmy powyżej (64), a 3 to liczba kanałów obrazu. Należy określić odpowiednio warstwę wyjściową, funkcję straty (loss), oraz optymalizator.

Przed uruchomieniem procesu uczenia trzeba utworzyć drugi generator danych do generowania obrazów walidacyjnych, ponownie wykorzystując data_gen.flow_from_directory dla katalogu walidacji.

Następnie wywołaj fit aby wytrenować model. Zwykle trwa to kilka minut. Po zakończeniu uczenia można wywołać cnn.save_weights(nazwa_pliku) w celu zapisania wyuczonego modelu sieci do pliku. Można go potem załadować przez cnn.load_weights(nazwa_pliku), dzięki czemu można wielokrotnie uruchamiać CNN bez konieczności ponownego szkolenia.

Przyjrzyj się otrzymanej dokładności. Ponieważ zbiór danych jest zbalansowany, więc bazowa dokładność klasy większościowej wynosi 0.5. Dobry wynik uczenia sieci powinien dać istotnie lepszą dokładność.

Pouczające jest również przeanalizowanie przebiegu wartości dokładności i straty dla każdej epoki. Wygeneruj wykres dokładności zbioru uczącego i walidacyjnego, oraz oddzielnie wykres straty obu tych zbiorów.

Wykreśl stratę szkolenia i walidacji dla każdej epoki. Przedstaw również dokładności szkolenia i walidacji na innym wykresie.

Chcąc uruchomić model na zbiorze danych, do którego uzyskujesz dostęp za pośrednictwem generatora, możesz użyć evaluate. Obliczona zostanie funkcja straty i dokładność. Te wartości powinny być takie same, jak w ostatniej epoce podczas trenowania modelu.

Na tych wykresach można się spodziewać typowych objawów przeuczenia.

Wykonanie ćwiczenia - część 2: rozszerzanie danych

Jednym ze sposobów zmniejszenia nadmiernego dopasowania do zbioru uczącego jest zwiększenia rozmiaru zbioru danych. W braku rzeczywistych dodatkowych danych, można zastosować różne typy modyfikacji istniejących obrazów treningowych w celu znaczącego zwiększenia zbioru.

Zapoznaj się z dokumentacją ImageDataGenerator. Opcjonalne parametry umożliwiają różne typy rozszerzania danych, takie jak obrót, odbicie lustrzane, i zmiana kolorów. Utwórz nowy ImageDataGenerator który stosuje rozszerzenie danych do obrazów treningowych, i następnie powtórz uczenie sieci. (Uwaga: generator do odczytu danych walidacyjnych nie powinien generować rozszerzonych danych. Dlaczego?)

Ponownie przeanalizuj otrzymane wyniki, włącznie z wykresami dokładności i straty dla kolejnych epok, i oceń je krytycznie.

Wskazówka: jeśli będziesz eksperymentować z różnymi typami rozszerzania danych, zauważysz, że istnieje kompromis między niedopasowaniem a nadmiernym dopasowaniem. Zazwyczaj, jeśli ostrożnie dostroisz swoje ulepszenia, zauważysz niewielką poprawę, ale będzie to oczywiście zależało od sposobu konstrukcji CNN. Pamiętaj też, że posiadany zestaw walidacyjny jest mały, więc możemy zobaczyć sporo fluktuacji w ocenach. Więc nawet jeśli rozszerzenie danych faktycznie poprawiło klasyfikator, ten efekt może być trudny do obiektywnego wykazania.

Wykonanie ćwiczenia - część 3: zastosowanie wcześniej wytrenowanej sieci

Jedna z najbardziej znanych baz danych obrazów używanych w wizji komputerowej nosi nazwę ImageNet. Zawiera bardzo dużą liczbę kategorii i jeszcze większą liczbę przykładowych zdjęć dla każdej kategorii. Kilka sieci CNN, które zostały wytrenowane na zbiorze ImageNet, zostało upublicznionych, a Keras zawiera kilka z nich. W tym ćwiczeniu użyjemy modelu o nazwie VGG-16, który został opracowany przez grupę z uniwersytetu w Oksfordzie (patrz odnośnik w sekcji Literatura).

Używamy funkcji wbudowanej w Keras do budowy modelu VGG-16 i wczytywania wag. Tutaj weights='imagenet' oznacza, że używamy wstępnie wytrenowanych wag, a include_top=True że używamy pełnego modelu klasyfikacji.

Przy pierwszym uruchomieniu tego kodu zostanie pobrany model VGG-16. Zostanie on następnie zapisany na Twoim dysku do wykorzystania w przyszłości.

from tensorflow.keras import applications
from tensorflow.keras.preprocessing.image import load_img, img_to_array
from tensorflow.keras.applications.vgg16 import decode_predictions, preprocess_input
vggmodel = applications.VGG16(weights='imagenet', include_top=True)

Aby zademonstrować model VGG-16 jako klasyfikator, teraz sklasyfikujemy przykładowy obraz. Wykonaj następujące kroki:

Oblicz prawdopodobieństwa klas dla tego obrazu, używając vggmodel.predict. Spowoduje to powstanie macierzy 1x1000. Istnieje 1000 kolumn, ponieważ klasyfikator VGG-16 używa 1000 klas ImageNet.

Zastosuj funkcję decode_prediction do danych wyjściowych z poprzedniego kroku. Spowoduje to przekształcenie macierzy prawdopodobieństwa w czytelne dla człowieka etykiety klas ImageNet. Oceń otrzymany wynik.

Następnie powtórz te kroki dla kilku losowo wybranych obrazów, i na tej podstawie spróbuje sformułować wniosek ogólny: czy ta predykcja ma sens?

Wykonanie ćwiczenia - część 4: używanie VGG-16 jako ekstraktora cech

Ponieważ mamy tylko 1600 obrazów treningowych, CNN, które trenowaliśmy wcześniej, nie daje rewelacyjnych wyników. Przyjmiemy teraz inne podejście: użyjemy modelu VGG-16 jako ekstraktora cech. Zastosujemy splotową część modelu VGG-16 do naszych obrazów i wykorzystamy te dane wyjściowe jako dane wejściowe naszego własnego klasyfikatora. To rozwiązanie jest przykładem modelu uczenia się transfer learning, co oznacza, że wykorzystujemy wiedzę zgromadzoną we wcześniej wytrenowanym modelu i stosujemy ją do naszego konkretnego zadania.

Aby rozpocząć, ponownie ładujemy model VGG-16. Zwróć uwagę, że tym razem parametry są nieco inne:

feature_extractor = applications.VGG16(include_top=False, weights='imagenet',
                                       input_shape=(img_size, img_size, 3))

Przed wczytaniem obrazów musimy utworzyć nowy, ImageDataGenerator, który wywołuje preprocess_input (przetwarzanie wstępne wymagane dla VGG-16) do obrazów, które przetwarza.

vgg_data_gen = ImageDataGenerator(preprocessing_function=preprocess_input)

Teraz napisz funkcję, create_vgg16_features która wykona następujące kroki:

Wywołaj tę funkcję dwukrotnie, raz dla części treningowej i raz dla walidacyjnej. Ten proces powinien zająć kilka minut.

Na koniec napisz funkcję, train_on_cnnfeatures która odczytuje dwa utworzone pliki, trenuje klasyfikator na zbiorze uczącym i ocenia na walidacyjnym. Aby odczytać pliki, możesz napisać:

with open(SOME_FILE_NAME, 'rb') as f:
    the_data = np.load(f)

W tym wypadku 'rb'oznacza czytanie w formacie binarnym.

Aby wytrenować ten klasyfikator można użyć Keras lub scikit-learn. Ale:

Dla uproszczenia, do tworzenia etykiet wyjściowych można użyć następującej funkcji pomocniczej, zakładając, że zbiór danych ma rozmiar n. Ta funkcja zakłada, że wszystkie instancje other znajdują się przed instancjami car. (To jest powód, dla którego ustawiliśmy shuffle=False w generatorze powyżej.)

def get_labels(n):
    return np.array([0]*(n//2) + [1]*(n//2))

Jaka jest Twoja tym razem dokładność? Jeśli dokładność jest inna niż podczas treningu poprzedniego CNN, jaka może być przyczyna tej różnicy?

Wskazówka: poprawne wykonanie tego kroku powinno przynieść dokładność znacznie wyższą niż podczas wcześniejszego trenowania samodzielnej CNN.

Wykonanie ćwiczenia - część 5: wizualizacja wyuczonych cech

Pierwsza warstwa splotowa reprezentuje wzorce najniższego poziomu, których model szuka na obrazach. Teraz zwizualizujemy te cechy w modelu VGG-16. (Możesz również powtórzyć ćwiczenie, używając własnego oryginalnego modelu, ale ponieważ danych jest mało, funkcje w tym modelu mogą wyglądać na mniej interpretowalne wzorce).

Wagi otrzymujemy w tej warstwie splotowej. To jest czterowymiarowy tensor NumPy: szerokość x wysokość x kolory x liczba konwolucji.

first_layer_weights = vggmodel.get_weights()[0]
first_layer_weights.shape

Tworzymy funkcję pomocniczą, która pomoże nam zwizualizować wzorce wyodrębnione przez pierwszą warstwę konwolucyjną.

Ta funkcja przyjmuje trzy parametry wejściowe: tensor wagi warstwy (z powyższego kroku), liczbę określającą, który filtr konwolucyjny rozważamy (0-63), i wreszcie wartość logiczną (True/False), która mówi, czy chcemy zobaczyć pozytywną czy negatywna część tego filtra. (To znaczy, jeśli positive=True widzimy wzorce, które włączają tę „cechę”; jeśli tak False, widzimy wzorce, które ją wyłączają).

def kernel_image(weights, i, positive):
    # extract the convolutional kernel at position i
    k = weights[:,:,:,i].copy()
    if not positive:
        k = -k
    # clip the values: if we're looking for positive
    # values, just keep the positive part; vice versa
    # for the negative values.
    k *= k > 0
    # rescale the colors, to make the images less dark
    m = k.max()
    if m > 1e-3:
        k /= m 
    return k

Na koniec zwizualizuj wybrane wzorce używane przez pierwszą warstwę konwolucyjną. Przydatne może być pokazanie tych obrazów w parach, tak aby dla każdego filtru splotowego można było zobaczyć wzorzec pozytywny i negatywny.

Czy można sformułować jakieś obserwacje albo wnioski na temat uzyskanych wyników?

Oddanie zadania

W celu oddania zadania należy wysłać w systemie eportalu:

  1. zwięzły raport opisujący wykonane elementy zadania, wyniki otrzymane w poszczególnych etapach, krótkie podsumowanie, wnioski, oraz znalezione przydatne i wykorzystane pozycje literatury (tutoriale, artykuły naukowe),
  2. pakiet uruchomieniowy (pliki źródłowe Pythona), pomocnicze skrypty, i plik Readme opisujący kroki niezbędne do uruchomienia programu/ów w środowisku Linux i odtworzenia najlepszych uzyskanych wyników.

Ponieważ wiele zagadnień pojawiających się w tym ćwiczeniu będzie zalewie powierzchownie omawiane na wykładzie do przedmiotu, opisz w raporcie główne problemy jakie pojawiły się w realizacji zadania.

Przed wysłaniem przetestuj ponowne wykonanie tych kroków w czystym środowisku, aby upewnić, się, że są one kompletne i dadzą się wykonać bez błędów.

W pakiecie uruchomieniowym proszę nie przesyłać oryginalnych obrazów. Należy założyć, że katalog images zawierający komplet obrazów zostanie zadany w postaci argumentu (alternatywnie można przyjąć, że ten katalog ma jakąś ustaloną nazwę, np. Obrazy).

Literatura

Francois Chollet: Deep learning. Praca z językiem Python i biblioteką Keras, Helion, 2019

Kurs na www.udemy.com - A Complete Guide on TensorFlow 2.0 using Keras API

http://www.robots.ox.ac.uk/%7Evgg/research/very_deep/

https://www.tensorflow.org/api_docs/python/tf