---
title: "Sieci konwolucyjne i małe zbiory danych"
output: 
  html_notebook: 
    theme: cerulean
    highlight: textmate
---

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



## Trenowanie konwolucyjnej sieci neuronowej na małym zbiorze danych

Konieczność trenowania modelu klasyfikacji obrazów na bardzo małej ilości danych jest często spotykaną sytuacją podczas prywatnej pracy nad problemami analizy obrazu. „Mała liczba” próbek może oznaczać różną liczbę — od kilkuset do kilkudziesięciu tysięcy obrazów. W tym podrozdziale zajmiemy się praktycznym przykładem klasyfikacji zdjęć przedstawiających psy i koty. Nasz zbiór będzie składał się z 4000 obrazów (2000 z nich będzie przedstawiać koty, a pozostałe 2000 — psy). Podczas testowania będziemy korzystać z 2000 zdjęć, 1000 przyda nam się do walidacji, a kolejny 1000 zostanie użyty do testowania.

W tym podrozdziale zajmiemy się jedną strategią rozwiązywania tego problemu — będziemy trenować nowy model od podstaw, korzystając przy tym tylko z dostępnych danych. Zaczniemy od naiwnego trenowania konwolucyjnej sieci neuronowej na 2000 próbek bez stosowania mechanizmu regularyzacji. Utworzymy w ten sposób punkt odniesienia do dalszej pracy — nasz klasyfikator uzyska dokładność na poziomie 71%. Naszym głównym problemem będzie nadmierne dopasowanie modelu do danych treningowych. Wprowadzimy technikę augmentacji danych, która pozwala na zmniejszenie skutków zbytniego dopasowania modelu w przypadku problemów dotyczących przetwarzania obrazu. Technika ta umożliwi zwiększenie dokładności modelu do 82%.

W dalszej części tego rozdziału opiszę dwie kolejne techniki przydatne podczas stosowania uczenia głębokiego na małych zbiorach danych: ekstrakcję cech przy uprzednio trenowanej sieci (rozwiązanie to pozwala na uzyskanie dokładności sięgającej 90 – 96%) i dostrajanie uprzednio trenowanej sieci (technika ta umożliwia uzyskanie dokładności na poziomie 97%). Te trzy techniki (trenowanie małego modelu od podstaw, ekstrakcja cech przy uprzednio trenowanej sieci i dostrajanie uprzednio trenowanej sieci) pozwolą Ci na późniejszą samodzielną pracę nad problemami klasyfikacji obrazów przy dysponowaniu małą ilością danych.


## Stosowanie uczenia głębokiego w problemach małych zbiorów danych

Niektórzy twierdzą, że uczenie głębokie działa tylko wtedy, gdy możliwe jest uzyskanie dostępu do dużej ilości danych. Stwierdzenie to jest częściowo prawdziwe: główną cechą uczenia głębokiego jest to, że algorytmy tego uczenia mogą samodzielnie wybrać przydatne cechy z treningowego zbioru danych, ale wymagają do tego licznego treningowego zbioru danych. Dotyczy to szczególnie pracy z próbkami o bardzo dużej liczbie wymiarów (przykładem takich próbek są obrazy).

Pojęcie licznego treningowego zbioru danych jest względne. Liczba danych potrzebnych do wytrenowania sieci zależy np. od jej rozmiaru i głębokości. Konwolucyjnej sieci neuronowej nie można wytrenować w celu rozwiązania skomplikowanego problemu na zaledwie kilkudziesięciu przykładach, ale zbiór kilkuset przykładów może okazać się wystarczający, jeżeli model będzie mały i poddany regularyzacji, a zadanie będzie proste. Konwolucyjne sieci neuronowe uczą się lokalnych cech niewrażliwych na przesunięcie, a więc charakteryzują się dużą wydajnością analizy danych w przypadku problemów percepcyjnych. Trenowanie konwolucyjnej sieci neuronowej od podstaw na bardzo małym zbiorze obrazów może dać całkiem sensowne efekty pomimo relatywnego braku danych (bez potrzeby przeprowadzania specjalnej inżynierii cech). Przekonasz się o tym podczas lektury tego podrozdziału.

Ponadto modele uczenia głębokiego mają naturę umożliwiającą stosowanie ich w wielu celach — model klasyfikacji obrazu lub dokonujący konwersji mowy na tekst pisany, który to model został wytrenowany na dużym zbiorze danych, może zostać użyty w celu rozwiązania innego problemu przy niewielkiej ilości zmian. Szczególnie w przypadku przetwarzania obrazu wiele uprzednio wytrenowanych modeli (zwykle modele te trenuje się na zbiorze danych ImageNet) może zostać pobranych z internetu i zastosowanych podczas pracy z małą ilością danych — zabieg ten pozwala na uzyskanie doskonałych wyników. Przykład zastosowania tej techniki przedstawię w dalszej części tego rozdziału. Zacznijmy pracę nad naszym modelem od wczytania danych.


## Pobieranie danych

Będziemy korzystać ze zbioru danych „Dogs vs. Cats”, który nie jest dołączony do pakietu Keras. Został on udostępniony w serwisie Kaggle w ramach konkursu analizy obrazu pod koniec 2013 r. (wówczas sieci konwolucyjne nie były jeszcze popularne). Możesz go pobrać ze strony  https://www.kaggle.com/c/dogs-vs-cats/data (musisz posiadać konto w serwisie Kaggle, ale jeżeli go jeszcze nie masz, to założenie go nie będzie stanowić żadnego problemu).

Zdjęcia wchodzące w skład zbioru są kolorowymi obrazami JPEG o średniej rozdzielczości:

![cats_vs_dogs_samples](img\5_2_2.jpg)

Oczywiście konkurs z 2013 r., z którego pochodzi ten zbiór danych, wygrały osoby, które użyły konwolucyjnych sieci neuronowych. Najlepsze rozwiązania uzyskały dokładność na poziomie 95%. W tym przykładzie (w kolejnej sekcji) zbliżysz się do tej wartości pomimo tego, że Twój model będzie trenowany na próbce mniej niż 10% danych udostępnionych uczestnikom konkursu.

Pełny zbiór danych zawiera 25 000 zdjęć psów i kotów (po 12 500 zdjęć należących do każdej z klas) i po skompresowaniu zajmuje 543 MB. Po pobraniu go i rozpakowaniu utworzymy nowy zbiór składający się z trzech podzbiorów: zbioru treningowego zawierającego po 1000 próbek każdej z klas, zbioru walidacyjnego zawierającego po 500 próbek każdej z klas i zbioru testowego zawierającego po 500 próbek każdej z klas.


Oto kod, który wykonuje te operacje:

```{r, echo=TRUE, results='hide'}
original_dataset_dir <- "~/Downloads/kaggle_original_data"

base_dir <- "~/Downloads/cats_and_dogs_small"
dir.create(base_dir)

train_dir <- file.path(base_dir, "train")
dir.create(train_dir)
validation_dir <- file.path(base_dir, "validation")
dir.create(validation_dir)
test_dir <- file.path(base_dir, "test")
dir.create(test_dir)

train_cats_dir <- file.path(train_dir, "cats")
dir.create(train_cats_dir)

train_dogs_dir <- file.path(train_dir, "dogs")
dir.create(train_dogs_dir)

validation_cats_dir <- file.path(validation_dir, "cats")
dir.create(validation_cats_dir)

validation_dogs_dir <- file.path(validation_dir, "dogs")
dir.create(validation_dogs_dir)

test_cats_dir <- file.path(test_dir, "cats")
dir.create(test_cats_dir)

test_dogs_dir <- file.path(test_dir, "dogs")
dir.create(test_dogs_dir)

fnames <- paste0("cat.", 1:1000, ".jpg")
file.copy(file.path(original_dataset_dir, fnames), 
          file.path(train_cats_dir)) 

fnames <- paste0("cat.", 1001:1500, ".jpg")
file.copy(file.path(original_dataset_dir, fnames), 
          file.path(validation_cats_dir))

fnames <- paste0("cat.", 1501:2000, ".jpg")
file.copy(file.path(original_dataset_dir, fnames),
          file.path(test_cats_dir))

fnames <- paste0("dog.", 1:1000, ".jpg")
file.copy(file.path(original_dataset_dir, fnames),
          file.path(train_dogs_dir))

fnames <- paste0("dog.", 1001:1500, ".jpg")
file.copy(file.path(original_dataset_dir, fnames),
          file.path(validation_dogs_dir)) 

fnames <- paste0("dog.", 1501:2000, ".jpg")
file.copy(file.path(original_dataset_dir, fnames),
          file.path(test_dogs_dir))
```

Sprawd?my, ile zdj?? mamy w poszczeg?lnych podzbiorach:

```{r}
cat("liczba obrazów treningowych kotów:", length(list.files(train_cats_dir)), "\n")
```

```{r}
cat("liczba obrazów treningowych psów:", length(list.files(train_dogs_dir)), "\n")
```

```{r}
cat("liczba obrazów walidacyjnych kotów:", length(list.files(validation_cats_dir)), "\n")
```

```{r}
cat("liczba obrazów walidacyjnych psów:", length(list.files(validation_dogs_dir)), "\n")
```

```{r}
cat("liczba obrazów testowych kotów:", length(list.files(test_cats_dir)), "\n")
```

```{r}
 cat("liczba obrazów testowych psów:", length(list.files(test_dogs_dir)), "\n")
```

Uzyskaliśmy zbiór treningowy składający się z 2000 zdjęć, zbiór walidacyjny składający się z 1000 zdjęć i zbiór testowy również zawierający 1000 zdjęć. Każdy zbiór zawiera równą liczbę zdjęć każdej z klas — pracujemy nad problemem wyważonej klasyfikacji binarnej, a więc dokładność klasyfikacji jest miarą sukcesu pracy modelu.

## Budowa sieci neuronowej

W poprzednim przykładzie tworzyliśmy konwolucyjną sieć neuronową przetwarzającą zbiór danych MNIST, a więc tworzenie takiej sieci nie jest dla Ciebie niczym nowym. Ponownie zastosujemy strukturę tej sieci: nasza sieć będzie stosem naprzemiennych warstw layer_conv_2d (z funkcją aktywacji relu) i layer_max_pooling_2d.

Tym razem pracujemy z większymi obrazami i bardziej złożonym problemem, a więc musimy dostosować do niego konstrukcję sieci — dodamy do niej jeszcze jedną fazę layer_conv_2d + layer_max_pooling_2d. Rozwiązanie to zmodyfikuje pojemność sieci i zredukuje rozmiar map cech tak, aby nie były one zbyt duże po osiągnięciu warstwy spłaszczania layer_flatten. Zaczynamy od map wejściowych o rozmiarach 150x150 (to wybrana przeze mnie dowolna wartość), a tuż przed warstwą layer_flatten kończymy na mapach o rozmiarze 7x7.


Zauważ, że w tej sieci głębokość map cech wzrasta w sposób progresywny (od 32 do 128), a ich wymiary maleją (od 148x148 do x77). Sytuacja taka ma miejsce w prawie wszystkich konwolucyjnych sieciach neuronowych.

Próbujemy rozwiązać problem klasyfikacji binarnej, a więc na końcu sieci umieszczamy jedną jednostkę (warstwę dense o rozmiarze równym 1) i funkcję aktywacji sigmoid. Jednostka ta będzie generować wartości prawdopodobieństwa tego, że analizowany obraz należy do jednej z klas.


```{r}
library(keras)

model <- keras_model_sequential() %>% 
  layer_conv_2d(filters = 32, kernel_size = c(3, 3), activation = "relu",
                input_shape = c(150, 150, 3)) %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>% 
  layer_conv_2d(filters = 64, kernel_size = c(3, 3), activation = "relu") %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>% 
  layer_conv_2d(filters = 128, kernel_size = c(3, 3), activation = "relu") %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>% 
  layer_conv_2d(filters = 128, kernel_size = c(3, 3), activation = "relu") %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>% 
  layer_flatten() %>% 
  layer_dense(units = 512, activation = "relu") %>% 
  layer_dense(units = 1, activation = "sigmoid")
```

Przyjrzyjmy się zmianom liczby wymiarów przyszłych map w kolejnych warstwach sieci:

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

Na etapie kompilacji skorzystamy jak zwykle z optymalizatora RMSprop. Zakończyliśmy sieć pojedynczą jednostką sigmoid, a więc posłużymy się funkcją straty w postaci binarnej entropii krzyżowej (informacje na temat doboru funkcji straty do różnych sytuacji znajdziesz w tabeli 4.1).

```{r}
model %>% compile(
  loss = "binary_crossentropy",
  optimizer = optimizer_rmsprop(lr = 1e-4),
  metrics = c("acc")
)
```

## Wstępna obróbka danych

Przypominam, że dane przed przekazaniem do wejść sieci należy odpowiednio sformatować — przedstawić w formie tensorów wartości zmiennoprzecinkowych. Obecnie dane zapisane na dysku mają formę plików JPG, a w celu przystosowania ich do przetwarzania przez sieć należy:

* Wczytać pliki obrazów.
* Zdekodować format JPEG do siatki pikseli w formacie RGB.
* Zapisać dane w formie tensorów liczb zmiennoprzecinkowych.
* Przeskalować wartości pikseli z zakresu 0 - 255 do zakresu [0, 1], ponieważ sieci neuronowe lepiej pracują z małymi wartościami wejściowymi.

Może się wydawać to dość pracochłonne, ale pakiet Keras jest wyposażony w narzędzia umożliwiające automatyczne wykonanie procesu konwersji. Pakiet Keras obsługuje funkcję image_data_generator() pozwalającą szybko zamienić obrazy zapisane na dysku w tensory przygotowane do skierowania do sieci neuronowej. Skorzystamy z tego gotowego rozwiązania:

```{r}
# Przeskalowuje wszystkie obrazy o współczynnik 1/255.
train_datagen <- image_data_generator(rescale = 1/255)
validation_datagen <- image_data_generator(rescale = 1/255)

train_generator <- flow_images_from_directory(
  # Katalog docelowy.
  train_dir,
  # Generator danych treningowych.
  train_datagen,
  # Zmienia rozdzielczość wszystkich obrazów na 150x150.
  target_size = c(150, 150),
  batch_size = 20,
  # Używamy funkcji binary_crossentropy w charakterze funkcji straty, a więc potrzebujemy binarnych etykiet.
  class_mode = "binary"
)

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

Przyjrzyjmy się wartościom wygenerowanym przez jeden z generatorów: zwraca on wsad obrazów RGB o wymiarach 150x150 (o kształcie (20, 150, 150, 3)) i binarne etykiety (kształt (20, )). W każdym wsadzie znajduje się 20 próbek. Zauważ, że generator zwraca wsady w nieskończoność (wykonuje nieskończoną pętlę, przetwarzając obrazy umieszczone w folderze docelowym).

```{r}
batch <- generator_next(train_generator)
str(batch)
```

Dopasujmy model do danych przy użyciu generatora. W tym celu należy skorzystać z funkcji fit_generator — jest to odpowiednik funkcji fit stosowany w przypadku generatorów danych. Metoda ta oczekuje zdefiniowania w pierwszym argumencie generatora, który w nieskończoność będzie zwracał wsady danych wejściowych i ich etykiet. Dane są generowane w nieskończoność, a więc model Keras musi wiedzieć, ile próbek ma pobrać z generatora przed zakończeniem epoki. Służy do tego argument steps_per_epoch: po pobraniu liczby wsadów określanej przez wartość tego argumentu (tj. po wykonaniu odpowiedniej liczby kroków spadku gradientu) proces dopasowywania modelu przejdzie do kolejnej epoki. W naszym przypadku wsady składają się z 20 próbek, a więc musimy wygenerować 100 wsadów w celu wytrenowania modelu na 2000 próbek.

Korzystając z funkcji fit_generator, możemy — podobnie jak w przypadku funkcji fit — przekazać argument validation_data. Argument ten może być generatorem danych, a także krotką tablic. W przypadku przekazania generatora w argumencie validation_data oczekuje się, że generator ten będzie zwracał wsady danych walidacyjnych w nieskończoność. W związku z tym należy zdefiniować wartość argumentu validation_steps określającą liczbę wsadów, która ma zostać pobrana z generatora danych walidacyjnych w celu przeprowadzenia walidacji.


```{r, echo=TRUE, results='hide'}
history <- model %>% fit_generator(
  train_generator,
  steps_per_epoch = 100,
  epochs = 30,
  validation_data = validation_generator,
  validation_steps = 50
)
```

Dobrą praktyką jest zapisywanie wszystkich wytrenowanych modeli.

```{r}
model %>% save_model_hdf5("cats_and_dogs_small_1.h5")
```

Utwórzmy wykresy straty i dokładności pracy modelu podczas przetwarzania danych treningowych i walidacyjnych:

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

Na wykresach tych wyraźnie widać nadmierne dopasowanie. Dokładność trenowania wzrasta liniowo wraz z upływem czasu aż do osiągnięcia wartości równej niemalże 100%, a dokładność walidacji nie przekracza poziomu 71 – 75%. Strata walidacji osiąga minimalną wartość po zaledwie pięciu epokach, a następnie stabilizuje się, a strata treningowa maleje liniowo aż do osiągnięcia wartości zbliżonych do 0.

Dysponujemy względnie niewielką liczbą próbek treningowych (2000), a więc nadmierne dopasowanie będzie naszym głównym problemem. Znasz już kilka technik rozwiązywania tego problemu, takich jak porzucanie i rozkład wag (regularyzacja L2). Teraz poznasz nową technikę przeciwdziałania nadmiernemu dopasowaniu, która sprawdza się podczas analizy obrazu i jest używana w praktycznie wszystkich modelach uczenia głębokiego przetwarzających obrazy: augmentację danych.

## Stosowanie techniki augmentacji danych

Nadmierne dopasowanie wynika ze zbyt małej liczby próbek, na których model może się uczyć. Model nie może w takiej sytuacji utworzyć uogólnień, które sprawdzą się podczas przetwarzania nowych danych. Gdybyśmy dysponowali nieskończenie wielkim zbiorem danych treningowych, to na model działałby każdy możliwy aspekt rozkładu danych — nigdy nie uległby przeuczeniu. Augmentacja danych to technika generowania większej liczby elementów treningowego zbioru danych poprzez augmentację próbek na drodze losowych przekształceń zwracających obrazy, które wyglądają wiarygodnie. Celem tego rozwiązania jest to, aby trenowany model nigdy nie zobaczył dwukrotnie tego samego zdjęcia. Dzięki temu model może zauważyć więcej aspektów przetwarzanych danych i utworzyć lepsze uogólnienia.

W pakiecie Keras z techniki tej można skorzystać, konfigurując losowe przekształcenia obrazów wczytywanych przez funkcję image_data_generator. Zacznijmy od przeanalizowania przykładu:

```{r}
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"
)
```

To tylko kilka z dostępnych opcji (informacje na temat pozostałych znajdziesz w dokumentacji pakietu Keras). Przeanalizujmy zaprezentowany kod:
*	Wartość rotation_range określa stopnie (0 – 180) — zakres kątów, o które zostanie wykonany losowy obrót obrazów.
*	Zakresy width_shift i height_shift określają ułamki całkowitej szerokości i wysokości obrazów; zakresy te wskazują ramy, w obrębie których przeprowadza się losowe pionowe i poziome przekształcenia obrazów.
*	Parametr shear_range określa zakres losowego przycinania obrazu.
*	Parametr zoom_range określa zakres losowego przybliżania fragmentów obrazów.
*	Operacja horizontal_flip polega na losowym odbiciu połowy obrazu w płaszczyźnie poziomej — z przekształcenia tego warto korzystać wtedy, gdy nie ma założeń o horyzontalnej asymetrii obrazu (np. w przypadku prawdziwych zdjęć).
*	Tryb fill_mode jest strategią wypełniania nowo utworzonych pikseli, które mogą powstać w wyniku obrotu lub przesunięcia.


Przyjrzyjmy się zmodyfikowanym obrazom:

```{r}
# Wybieramy obraz do zmodyfikowania.
fnames <- list.files(train_cats_dir, full.names = TRUE)
img_path <- fnames[[2]]

# Zamieniamy obraz w tablicę o kształcie (150, 150, 3).
img <- image_load(img_path, target_size = c(150, 150))
img_array <- image_to_array(img)
img_array <- array_reshape(img_array, c(1, 150, 150, 3))

# Generujemy wsady obrazów zmodyfikowanych w sposób losowy. 
augmentation_generator <- flow_images_from_data(
  img_array, 
  generator = datagen, 
  batch_size = 1 
)

# Generujemy wykres zawierajacy zmodyfikowane obrazy.
op <- par(mfrow = c(2, 2), pty = "s", mar = c(1, 0, 1, 0))
for (i in 1:4) {
  batch <- generator_next(augmentation_generator)
  plot(as.raster(batch[1,,,]))
}
par(op)
```

Jeżeli użyjemy tak skonfigurowanego mechanizmu modyfikującego obrazy, to nasza sieć nigdy nie będzie przetwarzać dwukrotnie tego samego obrazu, ale przetwarzane przez nią obrazy będą wciąż bardzo podobne do siebie, ponieważ będziemy generować je na bazie małej liczby oryginalnych obrazów — nie możemy wygenerować nowych informacji, lecz tylko przedstawiać w nowej formie informacje, którymi dysponujemy. W związku z tym być może nie uda nam się zupełnie wyeliminować nadmiernego dopasowania. Dlatego później dodamy do sieci warstwę dropout (umieścimy ją bezpośrednio przed gęsto połączonym klasyfikatorem).

```{r}
model <- keras_model_sequential() %>% 
  layer_conv_2d(filters = 32, kernel_size = c(3, 3), activation = "relu",
                input_shape = c(150, 150, 3)) %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>% 
  layer_conv_2d(filters = 64, kernel_size = c(3, 3), activation = "relu") %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>% 
  layer_conv_2d(filters = 128, kernel_size = c(3, 3), activation = "relu") %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>% 
  layer_conv_2d(filters = 128, kernel_size = c(3, 3), activation = "relu") %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>% 
  layer_flatten() %>% 
  layer_dropout(rate = 0.5) %>% 
  layer_dense(units = 512, activation = "relu") %>% 
  layer_dense(units = 1, activation = "sigmoid")  
  
model %>% compile(
  loss = "binary_crossentropy",
  optimizer = optimizer_rmsprop(lr = 1e-4),
  metrics = c("acc")
)
```

Przeprowadźmy proces trenowania sieci przy użyciu technik augmentacji danych i odrzucania.

```{r, echo=TRUE, results='hide'}
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
)

test_datagen <- image_data_generator(rescale = 1/255)

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

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

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

Zapiszmy utworzony model — będziemy z niego korzystać ponownie w podrozdziale 5.4.

```{r}
model %>% save_model_hdf5("cats_and_dogs_small_2.h5")
```

Utw?rzmy nowe wykresy:

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

Dzięki zastosowaniu technik augmentacji danych i odrzucania nie mamy już problemu nadmiernego dopasowania (patrz rysunek 5.11). Krzywe trenowania i walidacji mają podobny przebieg. Dokładność osiągnęła poziom 82%, a więc w skali względnej uległa poprawie o 15% w stosunku do początkowej wersji modelu.

Poprzez dalsze stosowanie technik regularyzacji i dostrajanie parametrów sieci, takich jak liczba filtrów poszczególnych warstw konwolucji lub liczba warstw sieci, możesz zbliżyć się do dokładności na poziomie 86 – 87%. Uzyskanie wyższej dokładności w wyniku trenowania własnej sieci konwolucyjnej od podstaw byłoby trudne, ponieważ dysponujemy małą ilością danych. W celu dalszego zwiększania dokładności musimy skorzystać z wytrenowanego wcześniej modelu. Technika ta będzie tematem przewodnim dwóch kolejnych podrozdziałów.
