---
title: "Korzystanie z wcześniej wytrenowanej konwolucyjnej sieci neuronowej"
output: 
  html_notebook: 
    theme: cerulean
    highlight: textmate
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(warning = FALSE, message = FALSE)
```


Korzystanie z uprzednio wytrenowanej sieci jest częstą praktyką uczenia głębokiego, która charakteryzuje się dużą skutecznością w przypadku uczenia się na małych zbiorach obrazów. Wytrenowana uprzednio sieć jest zapisaną siecią, która została wcześniej wytrenowana na dużym zbiorze danych (zwykle trenowanie to miało miejsce podczas wykonywania zadania klasyfikacji dużego zbioru obrazów). Jeżeli oryginalny zbiór danych był na tyle duży i na tyle ogólny, to przestrzenna hierarchia cech wyuczona przez wytrenowany wcześniej moduł może skutecznie odgrywać rolę ogólnego modelu przetwarzania obrazu — cechy tej sieci mogą przydawać się podczas rozwiązywania różnych problemów przetwarzania obrazu pomimo tego, że nowe problemy wymagają rozpoznawania zupełnie innych klas, niż miało to miejsce w przypadku oryginalnego zadania. Sieć może zostać wytrenowana np. na zbiorze ImageNet (klasami są w nim głównie zwierzęta i przedmioty codziennego użytku), a następnie zastosowana w celu wykonania zadania takiego jak identyfikacja mebli widocznych na zupełnie innych zdjęciach. Możliwość stosowania wytrenowanych sieci do rozwiązywania różnych problemów jest ogromną przewagą uczenia głębokiego nad starszymi algorytmami uczenia płytkiego. Dzięki tej technice uczenie głębokie może byś stosowane skutecznie w pracy z małymi zbiorami danych.

Chcemy skorzystać z dużej sieci konwolucyjnej wytrenowanej na zbiorze ImageNet (1,4 miliona obrazów podzielonych na 1000 klas). Zbiór ImageNet obejmuje wiele klas zwierząt, w tym różne rasy psów i kotów, a więc wytrenowany na nim model prawdopodobnie sprawdzi się dobrze w przypadku problemu klasyfikacji zdjęć na zdjęcia psów i zdjęcia kotów.

Skorzystamy z architektury VGG16 opracowanej przez Karen Simonyan i Andrew Zissermana w 2014 r. To prosta i często stosowana architektura sieci uczących się na zbiorze ImageNet . Co prawda jest to dość stary model, który odbiega dość mocno od najnowszych i najbardziej zaawansowanych modeli, ale znamy już jego architekturę i jesteśmy w stanie zrozumieć jego działanie bez wprowadzania żadnych nowych koncepcji. W przyszłości z pewnością będziesz korzystać z modeli o nazwach pokroju VGG, ResNet, Inception, Inception-ResNet, Xception itd. Człony te występują w nazwach wielu architektur stosowanych w przetwarzaniu obrazu.

Istnieją dwie techniki stosowania wytrenowanej wcześniej sieci: ekstrakcja cech i dostrajanie. Opiszę oba rozwiązania, ale zacznę od ekstrakcji cech.


## Ekstrakcja cech

Ekstrakcja cech polega na korzystaniu z reprezentacji wyuczonej przez sieć wcześniej w celu dokonania ekstrakcji interesujących nas cech z nowych próbek. Cechy te są następnie przepuszczane przez nowy klasyfikator trenowany od podstaw.

Przypominam, że konwolucyjne sieci neuronowe stosowane w problemach klasyfikacji obrazów składają się z dwóch elementów: serii warstw łączących (warstw pooling) i warstw konwolucyjnych. Na końcu sieci znajduje się gęsto połączony klasyfikator. Pierwsza część sieci określana jest mianem konwolucyjnej bazy modelu. W przypadku sieci konwolucyjnej ekstrakcja cech polega na przyjrzeniu się konwolucyjnej bazie wcześniej wytrenowanej sieci, przepuszczeniu przez nią nowych danych i wytrenowaniu nowego klasyfikatora na bazie wyjścia tej sieci.

![swapping FC classifiers](img\5_3a.png)

Dlaczego korzystamy ponownie tylko z konwolucyjnej bazy? Czy moglibyśmy skorzystać znów z gęsto połączonego klasyfikatora? Zwykle należy tego unikać. Wynika to z tego, że reprezentacje wyuczone przez konwolucyjną bazę są zwykle bardziej ogólne, a więc można z nich skorzystać również podczas pracy nad innym problemem: mapy cech konwolucyjnej sieci neuronowej przedstawiają ogólne koncepcje, które nadają się do rozwiązywania różnych problemów związanych z przetwarzaniem obrazu. Reprezentacje wyuczone przez klasyfikator są specyficzne dla danego zestawu klas, na których trenowano model — będą zawierać tylko informacje pozwalające na określenie prawdopodobieństwa tego, czy dana klasa jest widoczna na obrazie. Ponadto reprezentacje gęsto połączonych warstw nie zawierają żadnych informacji o miejscu umieszczenia rozpoznawanych obiektów na obrazie wejściowym — warstwy te nie polegają na przestrzeni, a informacje o położeniu obiektu znajdują się tylko w konwolucyjnej mapie cech. W przypadku problemów, w których położenie obiektu ma znaczenie praktyczne, gęsto połączone cechy są bezużyteczne.

Zauważ, że poziom ogólności (możliwości ponownego zastosowania) reprezentacji wyciągniętych przez określone warstwy konwolucyjne zależy od głębokości warstwy modelu. Warstwy występujące w modelu wcześniej dokonują ekstrakcji lokalnych wysoce ogólnych map cech (np. krawędzi, kolorów i tekstur), a warstwy wyższe dokonują ekstrakcji bardziej abstrakcyjnych koncepcji (rozpoznają np. „ucho kota” lub „oko psa”). Jeżeli nowy zbiór danych różni się bardzo od zbioru, na którym model był początkowo trenowany, to prawdopodobnie lepiej będzie, jak skorzystasz z kilku pierwszych warstw tego modelu w celu przeprowadzenia ekstrakcji cech i nie skorzystasz z całej konwolucyjnej bazy.

W naszym przypadku w zbiorze ImageNet wyodrębniono wiele klas zdjęć psów i kotów, a prawdopodobnie warto jest skorzystać z informacji umieszczonych w gęsto połączonych warstwach oryginalnego modelu, ale nie zrobimy tego w celu zaimplementowania bardziej ogólnego rozwiązania, w którym zestaw klas nowego problemu nie nakłada się na zestaw klas oryginalnego modelu. Zróbmy to w praktyce, korzystając z konwolucyjnej bazy sieci VGG16, wytrenowanej na zbiorze ImageNet, w celu ekstrakcji cech ze zdjęć psów i kotów, a następnie wytrenujmy klasyfikator dzielący obrazy na zdjęcia psów i zdjęcia kotów.

Model VGG16 jest jednym z modeli dołączonych do pakietu Keras. Oto lista modeli klasyfikujących obrazy dostępnych w tym module (wszystkie te modele zostały wytrenowane na zbiorze ImageNet):

* Xception
* InceptionV3
* ResNet50
* VGG16
* VGG19
* MobileNet

Utwórzmy instancję modelu VGG16.

```{r, echo=TRUE, results='hide'}
library(keras)

conv_base <- application_vgg16(
  weights = "imagenet",
  include_top = FALSE,
  input_shape = c(150, 150, 3)
)
```

Do funkcji przekazaliśmy trzy argumenty:

*	Argument weights określa punkt kontrolny wag, z którego inicjowany jest model.
*	Argument include_top określa to, czy do górnej części sieci dołączony zostanie gęsto połączony klasyfikator. Domyślnie klasyfikator ten odpowiada za 1000 klas zbioru ImageNet. Mamy zamiar korzystać z własnego klasyfikatora rozpoznającego dwie klasy (psy i koty), a więc nie chcemy dołączać wytrenowanego wcześniej klasyfikatora.
*	Argument input_shape określa kształt tensorów obrazów, które będą kierowane do wejścia sieci. Argument ten nie musi być definiowany — jeżeli go nie określisz, to sieć będzie w stanie przetworzyć tensory wejściowe o dowolnym kształcie.


Oto szczegółowe informacje na temat architektury bazy konwolucyjnej modelu VGG16. Przypomina ona architekturę przedstawionych wcześniej prostych sieci konwolucyjnych:

```{r}
summary(conv_base)
```

Finalna mapa cech ma kształt (4, 4, 512). Z tej mapy cech korzystać będzie klasyfikator.


Kolejne czynności możemy wykonywać na dwa sposoby:

*	Możemy uruchomić konwolucyjną bazę na naszym zbiorze danych, zapisać wygenerowane przez nią wartości na dysku w postaci tablicy Numpy, a następnie użyć tej tablicy w charakterze danych wejściowych samodzielnego, gęsto połączonego klasyfikatora podobnego do tego, który został opisany w części I tej książki. Rozwiązanie to jest szybkie i łatwo je uruchomić, ponieważ wymaga tylko jednokrotnego uruchomienia konwolucyjnej bazy dla każdego obrazu wejściowego, a konwolucyjna baza jest najbardziej kosztownym obliczeniowo ogniwem potoku przetwarzania danych. Niestety wadą tego rozwiązania jest brak możliwości skorzystania z techniki augmentacji danych.
*	Możemy rozszerzyć model, który posiadamy (conv_base), dodając do niego warstwy dense i używając całej tej konstrukcji w celu przetworzenia danych wejściowych. Rozwiązanie to umożliwia korzystanie z augmentacji danych, ponieważ każdy obraz wejściowy przechodzi przez konwolucyjną bazę za każdym razem, gdy jest analizowany przez model. Niestety sprawia to, że technika ta wymaga przeprowadzania bardziej kosztownych obliczeń.



Opiszę oba rozwiązania. Na razie skupmy się na kodzie wymaganym w celu zaimplementowania pierwszego z nich — kodzie zapisującym dane wyjściowe bazy conv_base i pozwalającym na użycie ich w charakterze danych wejściowych nowego modelu.

Zaczniemy od uruchomienia instancji wprowadzonego wcześniej generatora image_data_generator w celu przeprowadzenia ekstrakcji obrazów jako tablic Numpy. Ponadto dokonamy ekstrakcji etykiet obrazów. Ekstrakcję cech obrazów przeprowadzimy, wywołując na modelu metodę predict.

```{r}
base_dir <- "~/Downloads/cats_and_dogs_small"
train_dir <- file.path(base_dir, "train")
validation_dir <- file.path(base_dir, "validation")
test_dir <- file.path(base_dir, "test")

datagen <- image_data_generator(rescale = 1/255)
batch_size <- 20

extract_features <- function(directory, sample_count) {
  
  features <- array(0, dim = c(sample_count, 4, 4, 512))  
  labels <- array(0, dim = c(sample_count))
  
  generator <- flow_images_from_directory(
    directory = directory,
    generator = datagen,
    target_size = c(150, 150),
    batch_size = batch_size,
    class_mode = "binary"
  )
  
  i <- 0
  while(TRUE) {
    batch <- generator_next(generator)
    inputs_batch <- batch[[1]]
    labels_batch <- batch[[2]]
    features_batch <- conv_base %>% predict(inputs_batch)
    
    index_range <- ((i * batch_size)+1):((i + 1) * batch_size)
    features[index_range,,,] <- features_batch
    labels[index_range] <- labels_batch
    
    i <- i + 1
    if (i * batch_size >= sample_count)
      # Generator zwraca dane w nieskończoność, 
      # a więc pętla musi zostać przerwana, gdy każdy z obrazów zostanie przeanalizowany jednokrotnie.
      break
  }
  
  list(
    features = features, 
    labels = labels
  )
}

train <- extract_features(train_dir, 2000)
validation <- extract_features(validation_dir, 1000)
test <- extract_features(test_dir, 1000)
```

Wyciągnięte cechy mają obecnie kształt (próbki, 4, 4, 512). Będziemy kierować je do gęsto połączonego klasyfikatora, a więc cechy musimy spłaszczyć do kształtu (próbki, 8192):

```{r}
reshape_features <- function(features) {
  array_reshape(features, dim = c(nrow(features), 4 * 4 * 512))
}
train$features <- reshape_features(train$features)
validation$features <- reshape_features(validation$features)
test$features <- reshape_features(test$features)
```

Teraz możemy zdefiniować gęsto połączony klasyfikator (zwróć uwagę na zastosowanie techniki regularyzacji — porzucania) i wytrenować go na zapisanych przed chwilą danych i etykietach.

```{r, echo=TRUE, results='hide'}
model <- keras_model_sequential() %>% 
  layer_dense(units = 256, activation = "relu", 
              input_shape = 4 * 4 * 512) %>% 
  layer_dropout(rate = 0.5) %>% 
  layer_dense(units = 1, activation = "sigmoid")

model %>% compile(
  optimizer = optimizer_rmsprop(lr = 2e-5),
  loss = "binary_crossentropy",
  metrics = c("accuracy")
)

history <- model %>% fit(
  train$features, train$labels,
  epochs = 30,
  batch_size = 20,
  validation_data = list(validation$features, validation$labels)
)
```

Trenowanie przebiega bardzo szybko, ponieważ mamy tylko dwie warstwy dense — przetworzenie epoki algorytmu zajmuje mniej niż sekundę nawet w przypadku korzystania z CPU.

Przeanalizujmy wykresy straty i dokładności trenowania :

```{r}
plot(history)
```

Uzyskaliśmy dokładność na poziomie zbliżonym do 90% — to wynik o wiele lepszy od tego, który uzyskaliśmy w poprzednim podrozdziale, gdy trenowaliśmy model od podstaw. Z wykresów wynika jednak również to, że model niemalże od początku ulega nadmiernemu dopasowaniu pomimo przyjęcia dość dużej wartości współczynnika odrzutu. Bierze się to stąd, że nie korzystamy z techniki augmentacji danych, która jest niezbędna, by zapobiegnąć przeuczeniu modelu podczas przetwarzania małych zbiorów obrazów.

Teraz przyjrzyjmy się drugiej technice ekstrakcji cech, która jest o wiele wolniejsza i wymaga wykonywania o wiele bardziej skomplikowanych obliczeń, ale umożliwia korzystanie z augmentacji danych podczas trenowania — dokonamy rozbudowy modelu conv_base i przetworzymy dane od początku do końca przy użyciu tego właśnie modelu. Technika ta wymaga na tyle dużych zasobów obliczeniowych, że możesz spróbować z niej skorzystać tylko, jeżeli posiadasz wydajny układ GPU — nie ma żadnego sensu korzystać z niej w pracy na procesorze CPU. Jeżeli nie możesz uruchomić kodu na procesorze GPU, to możesz poprzestać na technice opisanej przed chwilą.

Modele mają właściwości warstw, a więc model conv_base może zostać dodany do modelu sekwencyjnego (sequential), tak jakbyśmy łączyli ze sobą kolejne warstwy sieci.

```{r}
model <- keras_model_sequential() %>% 
  conv_base %>% 
  layer_flatten() %>% 
  layer_dense(units = 256, activation = "relu") %>% 
  layer_dense(units = 1, activation = "sigmoid")
```

Teraz nasz model charakteryzuje się następującą budową:

```{r}
summary(model)
```

Konwolucyjna baza sieci VGG16 ma 14 714 688 parametrów (jest to bardzo duża liczba). Klasyfikator dodawany do tej bazy ma 2 miliony parametrów.

Zanim skompilujemy i wytrenujemy model, musimy zamrozić bazę konwolucyjną. Zamrażanie warstwy lub zestawu warstw polega na zapobieganiu aktualizacji ich wag w procesie trenowania. Jeżeli tego nie zrobimy, to reprezentacje wyuczone wcześniej przez bazę konwolucyjną zostaną zmodyfikowane podczas trenowania. Warstwy dense znajdujące się u góry są inicjowane w sposób losowy, co sprawia, że dochodziłoby do dużych zmian wszystkich parametrów sieci, a to skutecznie zniszczyłoby wyuczone wcześniej reprezentacje.

W pakiecie Keras sieć zamraża się za pomocą funkcji freeze_weights():


```{r}
cat("Liczba wag poddawanych trenowaniu ",
    "przed zamrożeniem bazy:", length(model$trainable_weights), "\n")
```

```{r}
freeze_weights(conv_base)
```

```{r}
cat("Liczba wag poddawanych trenowaniu ",
    "po zamrożeniu bazy:", length(model$trainable_weights), "\n")
```

Przy takiej konfiguracji trenowane będą tylko wagi z dwóch warstw dense. Będą to w sumie cztery tensory wag: po dwa tensory na warstwę (główna macierz wag i wektor wartości progowych). Pamiętaj o tym, że w celu wprowadzenia zmian należy najpierw skompilować model. Jeżeli zmienisz możliwość trenowania wag po skompilowaniu modelu, będziesz musiał go skompilować jeszcze raz, w przeciwnym razie wprowadzone zmiany zostaną zignorowane.

Teraz możemy rozpocząć trenowanie modelu przy takiej samej konfiguracji techniki augmentacji danych, z jakiej korzystaliśmy w poprzednim przykładzie.

```{r, echo=TRUE, results='hide'}
train_datagen = image_data_generator(
  rescale = 1/255,
  rotation_range = 40,
  width_shift_range = 0.2,
  height_shift_range = 0.2,
  shear_range = 0.2,
  zoom_range = 0.2,
  horizontal_flip = TRUE,
  fill_mode = "nearest"
)

test_datagen <- image_data_generator(rescale = 1/255)

train_generator <- flow_images_from_directory(
  train_dir,
  train_datagen,
  target_size = c(150, 150),
  batch_size = 20,
  class_mode = "binary"
)

validation_generator <- flow_images_from_directory(
  validation_dir,
  test_datagen,
  target_size = c(150, 150),
  batch_size = 20,
  class_mode = "binary"
)

model %>% compile(
  loss = "binary_crossentropy",
  optimizer = optimizer_rmsprop(lr = 2e-5),
  metrics = c("accuracy")
)

history <- model %>% fit_generator(
  train_generator,
  steps_per_epoch = 100,
  epochs = 30,
  validation_data = validation_generator,
  validation_steps = 50
)
```

```{r}
save_model_hdf5(model, "cats_and_dogs_small_3.h5")
```

Ponownie przedstawmy wyniki pracy algorytmu na wykresach:

```{r}
plot(history)
```

Jak widać, uzyskaliśmy dokładność walidacji sięgającą około 90%. To o wiele lepszy wynik od tego, który uzyskaliśmy przy użyciu małej sieci konwolucyjnej trenowanej od podstaw.

## Dostrajanie

Dostrajanie jest techniką ponownego stosowania modeli uzupełniającą ekstrakcję cech. Polega ona na odmrażaniu kilku górnych warstw zamrożonej bazy modelu używanej do ekstrakcji cech i trenowaniu jej łącznie z nową częścią modelu (w naszym przypadku tą częścią modelu jest w pełni połączony klasyfikator). Proces ten określamy mianem dostrajania, ponieważ modyfikuje częściowo wcześniej wytrenowane bardziej abstrakcyjne reprezentacje modelu w celu dostosowania ich do bieżącego problemu.

![fine-tuning VGG16](img\5_3b.png)

Stwierdziłem, że musimy zamrozić bazę konwolucji modelu VGG16, aby wytrenować losowo zainicjowany klasyfikator kończący sieć. Z tego samego powodu możliwe jest tylko dostrajanie górnych warstw konwolucyjnej bazy po wytrenowaniu klasyfikatora. Jeżeli klasyfikator nie byłby jeszcze wytrenowany, to sygnał błędu przepływający przez sieć w trakcie trenowania byłby zbyt duży i doprowadziłby do zniszczenia reprezentacji wyuczonych wcześniej przez dostrajane warstwy. W związku z tym dostrajanie sieci należy przeprowadzać w następujący sposób:
* 1.	Dodaj samodzielnie zaprojektowaną sieć do końca bazy wytrenowanego już modelu.
* 2.	Zamróź bazę sieci.
* 3.	Wytrenuj nową część sieci.
* 4.	Odmroź niektóre warstwy bazy sieci.
* 5.	Wytrenuj razem te warstwy oraz nową część sieci.

Pierwsze trzy kroki wykonaliśmy podczas ekstrakcji cech. Zajmijmy się krokiem 4. Odmrozimy bazę conv_base, a następnie zamrozimy jej wybrane warstwy.

Przypominam, że nasza sieć charakteryzuje się następującą architekturą:


```{r}
summary(conv_base)
```

Dostroimy ostatnie warstwy zaczynając od block3_conv1. Dlaczego nie będziemy dostrajać większej liczby warstw? Dlaczego nie dostroić całej bazy konwolucyjnej? Moglibyśmy to zrobić, ale musimy pamiętać o następujących rzeczach:

*	Wcześniejsze warstwy bazy konwolucyjnej kodują bardziej ogólne, uniwersalne cechy, a wyższe warstwy kodują bardziej wyspecjalizowane cechy. Lepiej jest dostrajać bardziej wyspecjalizowane cechy, ponieważ to od nich zależy przydatność modelu w nowym zastosowaniu. Podczas dostrajania niższych warstw dochodziłoby do szybkiego spadku zwracanych wartości.
*	Im więcej parametrów jest trenowanych, tym większe jest ryzyko nadmiernego dopasowania. Baza konwolucyjna ma 15 milionów parametrów, a trenowanie tak dużej liczby parametrów na niewielkim zbiorze danych, którym dysponujemy, byłoby ryzykowne.

W związku z tym w naszym przypadku dobrą strategią jest dostrajanie tylko wybranych warstw bazy konwolucyjnej. Zróbmy to, zaczynając od miejsca, w którym skończyliśmy w poprzednim przykładzie.


```{r}
unfreeze_weights(conv_base, from = "block3_conv1")
```

Teraz możemy rozpocząć dostrajanie sieci. Zrobimy to za pomocą algorytmu optymalizującego RMSProp przy bardzo niskiej wartości parametru uczenia. Wybranie niskiej wartości wynika z tego, że chcemy minimalizować modyfikacje reprezentacji trzech dostrajanych warstw. Zbyt duże zmiany tych wartości mogłyby zaszkodzić reprezentacjom danych.

Czas dostroić parametry modelu:

```{r, echo=TRUE, results='hide'}
model %>% compile(
  loss = "binary_crossentropy",
  optimizer = optimizer_rmsprop(lr = 1e-5),
  metrics = c("accuracy")
)

history <- model %>% fit_generator(
  train_generator,
  steps_per_epoch = 100,
  epochs = 100,
  validation_data = validation_generator,
  validation_steps = 50
)
```

```{r}
save_model_hdf5(model, "cats_and_dogs_small_4.h5")
```

Ponownie wygenerujmy wykresy:

```{r}
plot(history)
```

W skali bezwzględnej dokładność poprawiła się o 6% (wzrosła z około 90% do ponad 96%).

Zauważ, że krzywa straty nie wykazuje żadnej realnej poprawy (tak naprawdę wynika z niej pogorszenie parametru pracy modelu). Dlaczego dokładność jest stabilna, a nawet poprawiła się, jeżeli wartości straty nie spadają? Odpowiedź jest prosta: widzimy średnią punktowych wartości straty, a dokładność zależy od rozkładu wartości straty (nie ich średniej). Dzieje się tak, ponieważ dokładność jest binarną wartością progową prawdopodobieństwa klas przewidywanych przez model. Model może działać lepiej pomimo tego, że nie odzwierciedlają tego średnie wartości straty.

Na koniec możemy sprawdzić działanie modelu na danych testowych:

```{r}
test_generator <- flow_images_from_directory(
  test_dir,
  test_datagen,
  target_size = c(150, 150),
  batch_size = 20,
  class_mode = "binary"
)

model %>% evaluate_generator(test_generator, steps = 50)
```

Uzyskamy testową dokładność na poziomie 96,5%. Gdybyśmy wzięli udział w konkursie Kaggle dotyczącym tego zbioru danych, to uzyskalibyśmy jeden z najwyższych wyników. Dzięki nowoczesnym technikom uczenia głębokiego udało nam się uzyskać taki wynik, dysponując tylko niewielką częścią treningowego zbioru danych (około 10% danych). Możliwość trenowania modelu na zbiorze 20 000 próbek to coś zupełnie innego niż trenowanie modelu na 2000 próbek!

## Wnioski

Oto wnioski, które należy wynieść z dwóch ostatnich podrozdziałów:

*	Konwolucyjne sieci neuronowe są najlepszymi modelami uczenia maszynowego do zadań związanych z przetwarzaniem obrazu. Można je trenować od podstaw nawet na małych zbiorach danych i uzyskiwać przy tym sensowne wyniki.
*	Głównym problemem podczas pracy z małymi zbiorami danych jest nadmierne dopasowanie modelu. Problem ten podczas pracy z danymi będącymi obrazami można rozwiązać za pomocą techniki augmentacji danych.
*	Ekstrakcja cech umożliwia łatwe użycie utworzonej wcześniej konwolucyjnej sieci neuronowej w celu rozwiązania nowego problemu. Technika ta jest szczególnie przydatna podczas pracy z małymi zbiorami obrazów.
*	Efekty ekstrakcji cech można wzmocnić techniką dostrajania, która polega na przystosowywaniu wyuczonej wcześniej reprezentacji danych do nowego problemu. Zabieg ten zwiększa nieco skuteczność pracy modelu.

Teraz dysponujesz już solidnym zestawem narzędzi przeznaczonych do rozwiązywania problemów klasyfikacji obrazów, które to narzędzia sprawdzają się szczególnie dobrze podczas pracy z małymi zbiorami danych.

