---
title: "Wizualizacja efektów uczenia konwolucyjnych sieci neuronowych"
output: 
  html_notebook: 
    theme: cerulean
    highlight: textmate
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(warning = FALSE, message = FALSE)
dir.create("images")
```


Często słyszy się, że modele uczenia głębokiego są jak „czarne skrzynki”: uczą się reprezentacji, których ekstrakcja jest trudna do wykonania, i trudno jest je przedstawić w formie czytelnej z punktu widzenia człowieka. Stwierdzenie to dla niektórych modeli uczenia głębokiego można uznać za częściowo prawdziwe, ale z pewnością nie jest ono prawdziwe w przypadku pracy z konwolucyjnymi sieciami neuronowymi. Reprezentacje uczone przez konwolucyjne sieci neuronowe można z łatwością przedstawić w sposób graficzny. Wynika to w dużej mierze z tego, że są to reprezentacje koncepcji graficznych. Od 2013 r. rozwinięto wiele technik wizualizacji i interpretowania takich reprezentacji. Nie będę opisywał ich wszystkich, ale przedstawię Ci trzy najprostsze rozwiązania tego typu:

*	Wizualizacja pośrednich danych wyjściowych (pośrednich aktywacji) — technika przydatna do zrozumienia tego, jak kolejne warstwy sieci konwolucyjnej przekształcają dane wejściowe, i tego, co robią poszczególne filtry sieci.
*	Wizualizacja filtrów sieci konwolucyjnej — technika umożliwiająca dokładne zrozumienie graficznego wzorca lub graficznej koncepcji, na którą reaguje każdy z filtrów sieci konwolucyjnej.
*	Wizualizacja map ciepła aktywacji klas obrazu — technika przydatna do określenia części obrazu zidentyfikowanych jako należące do danej klasy — pozwala zlokalizować obiekty widoczne na obrazach.

Działanie pierwszej techniki — wizualizacji aktywacji — sprawdzimy na przykładzie małej sieci konwolucyjnej wytrenowanej od podstaw podczas podejmowania próby rozwiązania klasyfikacji obrazów psów i kotów w podrozdziale 5.2. Działanie dwóch kolejnych technik sprawdzimy na modelu VGG16 z podrozdziału 5.3.


## Wizualizacja pośrednich aktywacji

Wizualizacja pośrednich aktywacji polega na wyświetlaniu map cech, które są generowane przez różne warstwy konwolucyjne i łączące sieci na podstawie określonych danych wejściowych (dane wyjściowe warstwy są często określane mianem aktywacji, wynikiem zwracanym przez funkcję aktywacji). Umożliwia ona podgląd tego, jak dane wejściowe są rozkładane przez różne filtry wyuczone przez sieć. Chcemy dokonać wizualizacji map cech w przestrzeni trzech wymiarów: wysokości, szerokości i głębi (kanały). Każdy kanał koduje względnie niezależne cechy, a więc w celu wykonania poprawnej wizualizacji map tych cech należy przedstawić zawartość każdego kanału w formie niezależnego dwuwymiarowego obrazu. Zacznijmy od załadowania modelu zapisanego podczas rozwiązywania problemu z podrozdziału 5.2.

```{r}
library(keras)

model <- load_model_hdf5("cats_and_dogs_small_2.h5")
summary(model)  # W celu przypomnienia
```

Teraz uzyskamy obraz wejściowy — zdjęcie kota niewchodzące w skład treningowego zbioru danych.

```{r}
img_path <- "~/Downloads/cats_and_dogs_small/test/cats/cat.1700.jpg"

# Wstępnie przetwarzamy obraz tak, aby uzyskał formę czterowymiarowego tensora.
img <- image_load(img_path, target_size = c(150, 150))
img_tensor <- image_to_array(img)
img_tensor <- array_reshape(img_tensor, c(1, 150, 150, 3))

# Pamiętaj o tym, że model był trenowany na danych przetworzonych właśnie w taki sposób.
img_tensor <- img_tensor / 255

dim(img_tensor)
```

Czas wyświetlić zdjęcie:
```{r}
plot(as.raster(img_tensor[1,,,]))
```

W celu wyciągnięcia map cech, którym chcemy się przyjrzeć, utworzymy model Keras, który będzie przyjmował na wejściu wsady składające się z obrazów i będzie generował na swoim wyjściu aktywacje wszystkich warstw konwolucyjnych i warstw łączących. W tym celu skorzystamy z funkcji keras_model przyjmującej dwa argumenty: tensor wejściowy (lub listę tensorów wejściowych) i tensor wyjściowy (lub listę tensorów wyjściowych). Utworzona w ten sposób klasa jest modelem Keras, który podobnie do opisanych wcześniej modeli sekwencyjnych (funkcja keras_sequential_model()) mapuje określone dane wejściowe na określone dane wyjściowe. Model ten od modelu keras_sequential_model odróżnia to, że umożliwia on obsługę wielu wyjść. Więcej informacji na temat funkcji keras_model znajdziesz w podrozdziale 7.1.

```{r}
# Wyciąga dane wyjściowe ośmiu górnych warstw.
layer_outputs <- lapply(model$layers[1:8], function(layer) layer$output)
# Tworzy model zwracający dane wyjściowe generowane w wyniku przekazania określonych danych wejściowych.
activation_model <- keras_model(inputs = model$input, outputs = layer_outputs)
```

Model ten po skierowaniu do jego wejścia obrazu zwraca wartości aktywacji warstwy oryginalnego modelu. To pierwszy przykład modelu zwracającego wiele danych wyjściowych, który został zaprezentowany w tej książce. Dotychczas przedstawiałem modele, które przyjmowały dokładnie jeden obiekt wejściowy i zwracały jeden obiekt wyjściowy, ale ogólnie rzecz biorąc, model może przyjmować dowolną liczbę obiektów wejściowych i generować dowolną liczbę obiektów wyjściowych. Opisywany obecnie model przyjmuje jeden obiekt wejściowy i generuje osiem obiektów wyjściowych (po jednym dla każdej warstwy aktywacji).

```{r}
# Zwraca listę pięciu tablic: po jednej dla każdej warstwy aktywacji.
activations <- activation_model %>% predict(img_tensor)
```

Oto przykład aktywacji pierwszej warstwy konwolucyjnej wygenerowanej na podstawie wybranego przez nas zdjęcia kota:

```{r}
first_layer_activation <- activations[[1]]
dim(first_layer_activation)
```

Jest to mapa cech o rozmiarach 148148 składająca się z 32 kanałów. Przedstawmy wybrane kanały w formie graficznej. Zacznijmy od zdefiniowania funkcji R generującej wykres danych kanału:

```{r}
plot_channel <- function(channel) {
  rotate <- function(x) t(apply(x, 2, rev))
  image(rotate(channel), axes = FALSE, asp = 1, 
        col = terrain.colors(12))
}
```

Wizualizacja piątego kanału:

```{r}
plot_channel(first_layer_activation[1,,,5])
```


Kanał ten wydaje się zawierać informacje o wykryciu krawędzi. W przypadku Twojej sieci krawędzie mogą zostać wykryte przez inny kanał.

```{r}
plot_channel(first_layer_activation[1,,,7])
```

Kanał ten zawiera zupełnie inne informacje — wydaje się, że wykrywa kocie oczy. Za chwilę wygenerujemy kompletną wizualizację wszystkich warstw aktywacji naszej sieci. Wyciągniemy dane każdego kanału ośmiu map aktywacji i przedstawimy je na wykresie mającym formę jednego dużego zbioru obrazów.

```{r}
dir.create("cat_activations")
image_size <- 58
images_per_row <- 16

for (i in 1:8) {
  
  layer_activation <- activations[[i]]
  layer_name <- model$layers[[i]]$name
 
  n_features <- dim(layer_activation)[[4]]
  n_cols <- n_features %/% images_per_row
 
  png(paste0("cat_activations/", i, "_", layer_name, ".png"), 
      width = image_size * images_per_row, 
      height = image_size * n_cols)
  op <- par(mfrow = c(n_cols, images_per_row), mai = rep_len(0.02, 4))
  
  for (col in 0:(n_cols-1)) {
    for (row in 0:(images_per_row-1)) {
      channel_image <- layer_activation[1,,,(col*images_per_row) + row + 1]
      plot_channel(channel_image)
    }
  }
  
  par(op)
  dev.off()
}
```

***

![](cat_activations/1_conv2d_21.png)

***

![](cat_activations/3_conv2d_22.png)

***

![](cat_activations/5_conv2d_23.png)

***

![](cat_activations/7_conv2d_24.png)

***

Warto zauważyć tu kilka rzeczy:

*	Pierwsza warstwa wykrywa różne krawędzie. Na tym etapie aktywacje zachowują prawie wszystkie informacje obecne w początkowym obrazie.
*	Kolejne warstwy aktywacji stają się coraz bardziej abstrakcyjne i trudniej je zinterpretować graficznie. Mogą one zaczynać kodować wysokopoziomowe koncepcje takie jak „kocie ucho” i „kocie oko”. Wyższe reprezentacje danych zawierają coraz mnie informacji dotyczących wizualnej zawartości obrazu i jednocześnie coraz więcej informacji związanych z klasą obrazu.
*	Wraz z głębią warstw maleje ich gęstość: wszystkie filtry pierwszej warstwy są aktywowane obrazem wejściowym, ale w kolejnych warstwach coraz więcej filtrów jest pustych. Oznacza to w obrazie brak wzorca zakodowanego przez dany filtr.

Właśnie odkryliśmy ważną cechę reprezentacji uczonych przez głębokie sieci neuronowe: wraz ze wzrostem głębokości warstwy rozpoznawane przez nią cechy stają się coraz bardziej abstrakcyjne. Aktywacje wyższych warstw zawierają coraz mniej informacji o obserwacji wejściowej, jednocześnie zawierają coraz więcej informacji o celu (klasie, do której należy obraz). Głęboka sieć neuronowa pełni funkcję potoku oczyszczającego informację — na wejściu tego potoku podawany jest obraz (w tym przypadku jest to obraz w formacie RGB), który w wyniku powtarzania przekształceń jest odzierany ze zbędnych informacji (np. wizualnych parametrów danego zdjęcia), co prowadzi do wzmocnienia przydatnych informacji, na podstawie których można ustalić klasę, do której należy obraz.

W taki sam sposób świat postrzegają ludzie i zwierzęta: w wyniku obserwowania jakiejś sceny przez kilka sekund człowiek zapamiętuje abstrakcyjne obiekty, które były w niej obecne (takie jak rower lub drzewo), ale nie pamięta konkretnego wyglądu tych obiektów. Spróbuj narysować rower z pamięci. Najprawdopodobniej będzie to problematyczne pomimo tego, że z pewnością zdarzyło Ci się w życiu widzieć rowery wielokrotnie (patrz rysunek 5.24). Spróbuj to zrobić teraz. Nasz mózg naprawdę działa, ucząc się abstrakcji bodźców wizualnych i odzierając je ze zbędnych szczegółów. Przez to zjawisko dość trudno jest zapamiętać detale wyglądu otaczających nas rzeczy.


## Wizualizacja filtrów konwolucyjnych sieci neuronowych


Innym łatwym sposobem na przyjrzenie się działaniu filtrów konwolucyjnej sieci neuronowej jest wyświetlenie graficznego wzorca, na który reaguje każdy z filtrów. Można to zrobić poprzez zwiększanie gradientu w przestrzeni wejściowej: zastosowanie algorytmu spadku gradientowego na wartości obrazu wejściowego konwolucyjnej sieci neuronowej w celu zmaksymalizowania odpowiedzi wybranego filtra, zaczynając od pustego obrazu wejściowego. Technika ta pozwala na uzyskanie obrazu wejściowego, na który wybrany filtr reaguje w sposób maksymalny.

Proces ten jest prosty: wystarczy zbudować funkcję straty maksymalizującą wartość wybranego filtra w wybranej warstwie konwolucyjnej, a następnie skorzystać z algorytmu stochastycznego spadku gradientowego w celu zmodyfikowania obrazu wejściowego, tak aby uzyskać maksymalną wartość aktywacji. Oto przykład funkcji straty aktywacji filtra 1 warstwy block3_conv1 sieci VGG16 wytrenowanej na zbiorze obrazów ImageNet:


```{r}
library(keras)

model <- application_vgg16(
  weights = "imagenet", 
  include_top = FALSE
)

layer_name <- "block3_conv1"
filter_index <- 1

layer_output <- get_layer(model, layer_name)$output
loss <- k_mean(layer_output[,,,filter_index])
```

W celu zaimplementowania algorytmu spadku gradientowego będziemy musieli ustalić gradient straty z uwzględnieniem danych wejściowych modelu. W związku z tym skorzystamy z funkcji k_gradients.

```{r}
# Wywołanie funkcji k_gradients zwraca listę tensorów (w tym przypadku ma ona rozmiar 1).  
# W związku z tym zapisujemy tylko pierwszy element tej listy, który jest potrzebnym nam tensorem.
grads <- k_gradients(loss, model$input)[[1]] 
```

Niezbyt oczywistym rozwiązaniem wspomagającym algorytm spadku gradientowego jest normalizacja tensora gradientu poprzez podzielenie go przez jego normę L2 (pierwiastek kwadratowy średniej kwadratów wartości tensora). Zapewni to stały zakres modyfikacji obrazu wejściowego.

```{r}
# Przed dzieleniem dodawana jest wartość 1e–5 w celu zapobiegnięcia przypadkowemu dzieleniu przez zero.
grads <- grads / (k_sqrt(k_mean(k_square(grads))) + 1e-5)
```

Teraz musimy jakoś obliczyć wartość tensora straty i tensora gradientu. W tym celu możemy zdefiniować wewnętrzną funkcję pakietu Keras: funkcja iterate przyjmuje tensor Numpy (w formie listy tensorów o rozmiarze 1) i zwraca listę dwóch tensorów Numpy: wartość straty i wartość gradientu.

```{r}
iterate <- k_function(list(model$input), list(loss, grads))

# Przetestujmy to.
c(loss_value, grads_value) %<-%
    iterate(list(array(0, dim = c(1, 150, 150, 3))))
```

Teraz mo?emy zdefiniowa? p?tl? implementuj?c? stochastyczny spadek wzd?u? gradientu:

```{r}
# Zaczynamy od szarego obrazu z szumem.
input_img_data <-
  array(runif(150 * 150 * 3), dim = c(1, 150, 150, 3)) * 20 + 128 

step <- 1  # Waga każdej aktualizacji gradientu.
for (i in 1:40) { 
  # Obliczamy wartość straty i wartość gradientu.
  c(loss_value, grads_value) %<-% iterate(list(input_img_data))
  # Modyfikuje obraz wejściowy tak, aby uzyskać jak najwyższą wartość straty.
  input_img_data <- input_img_data + (grads_value * step)
}
```

Uzyskany w ten sposób tensor obrazu jest tensorem liczb zmiennoprzecinkowych o kształcie (1, 150, 150, 3). Znajdują się w nim wartości z zakresu od 0 do 255, które nie muszą być liczbami całkowitymi. W związku z tym w celu wyświetlenia obrazu musimy jeszcze przetworzyć ten tensor. Zrobimy to za pomocą prostej funkcji narzędziowej.

```{r}
deprocess_image <- function(x) {
 
  dms <- dim(x)
  
  # Normalizacja tensora: wyśrodkowanie w punkcie 0, zapewnienie odchylenia standardowego równego 0,1.
  x <- x - mean(x) 
  x <- x / (sd(x) + 1e-5)
  x <- x * 0.1 
  
  # Ograniczenie do zakresu [0, 1].
  x <- x + 0.5 
  x <- pmax(0, pmin(x, 1))
  
  # Zwrócenie obrazu o początkowych wymiarach.
  array(x, dim = dms)
}
```

Mamy już wszystkie elementy, których potrzebujemy. Połączmy je w celu utworzenia funkcji przyjmującej w charakterze parametru wejściowego nazwę warstwy i indeks filtra. Funkcja ta powinna zwracać gotowy obraz wzorca maksymalizującego aktywację wybranego filtra.

```{r}
generate_pattern <- function(layer_name, filter_index, size = 150) {
  
  # Buduje funkcję straty maksymalizującą aktywację n-tego filtra analizowanej warstwy.
  layer_output <- model$get_layer(layer_name)$output
  loss <- k_mean(layer_output[,,,filter_index]) 
  
  # Oblicza gradient obrazu wejściowego z uwzględnieniem wartości straty.
  grads <- k_gradients(loss, model$input)[[1]]
  
  # Sztuczka normalizująca gradient.
  grads <- grads / (k_sqrt(k_mean(k_square(grads))) + 1e-5)
  
  # Funkcja zwracająca stratę i gradient dla danego obrazu wejściowego..
  iterate <- k_function(list(model$input), list(loss, grads))
  
  # Zaczynamy od szarego obrazu z szumem.
  input_img_data <- 
    array(runif(size * size * 3), dim = c(1, size, size, 3)) * 20 + 128
  
  # Wykonuje 40 kroków algorytmu zwiększania gradientu.
  step <- 1
  for (i in 1:40) {
    c(loss_value, grads_value) %<-% iterate(list(input_img_data))
    input_img_data <- input_img_data + (grads_value * step) 
  }
  
  img <- input_img_data[1,,,]
  deprocess_image(img) 
}
```

Wypróbujmy działanie tej funkcji :

```{r}
library(grid)
grid.raster(generate_pattern("block3_conv1", 1))
```

![](images/polka_dots-r.png)

<br/>

Wygląda na to, że filtr nr 1 warstwy block3_conv1 reaguje na wzór kropek. 

Teraz możemy zacząć zabawę i przyjrzeć się wizualizacjom wszystkich filtrów wchodzących w skład wszystkich warstw. Dla uproszczenia przyjrzymy się 64 pierwszym filtrom każdej warstwy i będziemy brać pod uwagę tylko pierwszą warstwę każdego bloku konwolucji (block1_conv1, block2_conv1, block3_conv1, block4_conv1 i block5_conv1). Wygenerowane wartości umieścimy w siatce o wymiarach 88 składającej się z wzorców filtrów o rozdzielczości 64x64, a między wzorcami umieścimy czarne linie rozgraniczające .

```{r}
library(grid)
library(gridExtra)
dir.create("vgg_filters")
for (layer_name in c("block1_conv1", "block2_conv1", 
                     "block3_conv1", "block4_conv1")) {
  size <- 140
  
  png(paste0("vgg_filters/", layer_name, ".png"),
      width = 8 * size, height = 8 * size)
  
  grobs <- list()
  for (i in 0:7) {
    for (j in 0:7) {
      pattern <- generate_pattern(layer_name, i + (j*8) + 1, size = size)
      grob <- rasterGrob(pattern, 
                         width = unit(0.9, "npc"), 
                         height = unit(0.9, "npc"))
      grobs[[length(grobs)+1]] <- grob
    }  
  }
  
  grid.arrange(grobs = grobs, ncol = 8)
  dev.off()
}
```

***

![block1_conv1](vgg_filters/block1_conv1.png)

***

![block2_conv1](vgg_filters/block2_conv1.png)

***

![block3_conv1](vgg_filters/block3_conv1.png)

***

![block4_conv1](vgg_filters/block4_conv1.png)

***

Na podstawie tych wizualizacji można dowiedzieć się, jak warstwy sieci neuronowej widzą obrazy: każda warstwa konwolucyjnej sieci neuronowej uczy się zbioru filtrów umożliwiających przedstawienie danych wejściowych za pomocą kombinacji tych filtrów. Rozwiązanie to działa podobnie do transformacji Fouriera rozkładającej sygnał na zbiór funkcji cosinusowych. Filtry tej konwolucyjnej sieci neuronowej stają się coraz bardziej złożone i lepsze wraz ze zwiększaniem się ich pozycji w modelu:

*	Filtry pierwszej warstwy modelu (block1_conv1) kodują proste kierunkowe krawędzi i koloru (w niektórych przepadkach są to kolorowe krawędzie).
*	Filtry warstwy block2_conv1 kodują proste tekstury składające się z kombinacji krawędzi i kolorów.
*	Filtry wyższych warstw zaczynają przypominać tekstury naturalnych obrazów: pióra, oczy, liście itp.


## Wizualizacja map ciepła aktywacji klas

Chciałbym przedstawić jeszcze jedną technikę wizualizacji. Przydaje się ona w celu określenia części obrazu, które doprowadziły sieć neuronową do podjęcia decyzji o zaklasyfikowaniu go do danej klasy. Rozwiązanie to przydaje się podczas debugowania procesu decyzyjnego konwolucyjnej sieci neuronowej w przypadku podejmowania przez nią błędnych decyzji. Ponadto pozwala ono na zlokalizowanie określonego obiektu na obrazie.

Tę ogólną kategorię technik określamy mianem wizualizacji map aktywacji klas (map CAM). Rozwiązania te polegają na generowaniu map ciepła aktywacji klas naniesionych na obrazy wejściowe. Mapa ciepła aktywacji klas jest dwuwymiarową siatką wyników związanych z określoną klasą wyjściową. Wyniki te są obliczane dla każdego punktu obrazu wejściowego. Określają one to, jak ważny jest dany punkt z uwagi na wybraną klasę. W przypadku przykładowego obrazu przekazanego do sieci klasyfikującej obrazy na zdjęcia psów i zdjęcia kotów wizualizacja map aktywacji klasy pozwala na wygenerowanie mapy ciepła aktywacji klasy „kot” określającej to, jak bardzo poszczególne części obrazu swym wyglądem przypominają kota. Możemy wygenerować również mapę ciepła klasy „pies” określającą to, jak bardzo poszczególne części obrazu przypominają psa.

Będziemy korzystać z implementacji opisanej w artykule Grad-CAM: Visual Explanations from Deep Networks via Gradient-based Localization . To bardzo prosta implementacja polegająca na przechwytywaniu wyjściowej mapy cech warstwy konwolucyjnej powstałej w wyniku przetwarzania danego obrazu. Następnie określane są wagi poszczególnych kanałów tej mapy (używany jest do tego algorytm spadku gradientowego klasy z uwzględnieniem kanału). Technikę tę można zrozumieć jako ważenie przestrzennych map „intensywności aktywowania różnych kanałów przez obraz wejściowy” i „istotności każdego kanału z punktu widzenia wybranej klasy” — generowana mapa określa „intensywność aktywacji klasy przez obraz wejściowy”.

Przyjrzymy się tej technice, ponownie korzystając z wytrenowanej sieci VGG16.


```{r}
# Czyszczenie sesji.
k_clear_session()

# Zwróć uwagę na to, że dołączamy gęsto połączony klasyfikator. 
# We wcześniejszych przykładach nie korzystaliśmy z niego.
model <- application_vgg16(weights = "imagenet")
```

Przyjrzyjmy się zdjęciu afrykańskich słoni udostępnionemu na mocy licencji Creative Commons. Prawdopodobnie jest to matka ze słoniątkiem podczas spaceru po sawannie.

![elephants](img\5_4_3.jpg)

Zamieńmy ten obraz na format, który może zostać przetworzony przez model VGG16. Model ten został wytrenowany na obrazach o rozdzielczości 224244, przygotowanych zgodnie z wytycznymi mającymi postać funkcji imagenet_preprocess_input(). W związku z tym musimy wczytać obraz, zmienić jego rozdzielczość na 224224 i wykonać pozostałe operacje wstępnej obróbki obrazu.

```{r}
# Lokalna ścieżka obrazu..
img_path <- "~/Downloads/creative_commons_elephant.jpg"
  
# Obraz o rozdzielczości 224x224,
img <- image_load(img_path, target_size = c(224, 224)) %>% 
  # Tablica o kształcie (224, 224, 3).
  image_to_array() %>% 
  # Operacja dodawania wymiaru w celu wykonania transformacji tablicy do postaci wsadu o rozmiarze (1, 224, 224, 3).
  array_reshape(dim = c(1, 224, 224, 3)) %>% 
  # Wstępna obróbka wsadu (normalizacja kanałów poszczególnych kolorów).
  imagenet_preprocess_input()
```


Teraz możemy uruchomić wytrenowaną sieć, przepuścić przez nią obraz i odczytać przewidywane etykiety klas:

```{r}
preds <- model %>% predict(img)
imagenet_decode_predictions(preds, top = 3)[[1]]
```

Oto trzy najbardziej prawdopodobne klasy tego obrazu:
*	słoń afrykański (prawdopodobieństwo 92,5%),
*	dorosły słoń (prawdopodobieństwo 7%),
*	słoń indyjski (prawdopodobieństwo 0,4%).


Sieć doszła do wniosku, że na obrazie znajduje się niewiadoma liczba słoni afrykańskich. Element wektora predykcji, który uzyskał maksymalną wartość aktywacji, odpowiada klasie „słoń afrykański” (African_elephant) o numerze indeksu 387:

```{r}
which.max(preds[1,])
```

W celu określenia elementów obrazu, które najbardziej świadczą o tym, że znajduje się na nim słoń afrykański, skonfigurujmy proces Grad-CAM.

```{r}
# Wartość wektora predykcji odwołująca się do klasy „słoń afrykański”.
african_elephant_output <- model$output[, 387]

# Wyjściowa mapa cech warstwy block5_conv3 — ostatniej warstwy konwolucyjnej modelu VGG16.
last_conv_layer <- model %>% get_layer("block5_conv3")

# Gradient klasy „słoń afrykanski” uwzględniający wyjściową mapę cech warstwy block5_conv3.
grads <- k_gradients(african_elephant_output, last_conv_layer$output)[[1]]

# Wektor o kształcie (512). 
# Każda wartość tego wektora jest średnią intensywności gradientu przy określonym kanale mapy cech.
pooled_grads <- k_mean(grads, axis = c(1, 2, 3))

# Pozwala na uzyskanie dostępu do zdefiniowanych przed chwilą wartości 
# pooled_grads i wyjściowej mapy cech block5_conv3.
iterate <- k_function(list(model$input),
                      list(pooled_grads, last_conv_layer$output[1,,,])) 

# Wartości dwóch niewiadomych w formie tablic 
# (wartości te powstały w wyniku przetwarzania obrazu dwóch słoni).
c(pooled_grads_value, conv_layer_output_value) %<-% iterate(list(img))

# Mnożenie każdego kanału tablicy mapy cech przez wartość określającą 
# „jego wpływ na to, że obraz przedstawia słonia”.
for (i in 1:512) {
  conv_layer_output_value[,,i] <- 
    conv_layer_output_value[,,i] * pooled_grads_value[[i]] 
}

# Średnia kanałów wyjściowej mapy cech jest mapą ciepła aktywacji klasy.
heatmap <- apply(conv_layer_output_value, c(1,2), mean)
```

Chcemy wygenerować wizualizację, a więc przeprowadzimy normalizację wartości map ciepła do zakresu 0 – 1. 

```{r}
heatmap <- pmax(heatmap, 0) 
heatmap <- heatmap / max(heatmap)

write_heatmap <- function(heatmap, filename, width = 224, height = 224,
                          bg = "white", col = terrain.colors(12)) {
  png(filename, width = width, height = height, bg = bg)
  op = par(mar = c(0,0,0,0))
  on.exit({par(op); dev.off()}, add = TRUE)
  rotate <- function(x) t(apply(x, 2, rev))
  image(rotate(heatmap), axes = FALSE, asp = 1, col = col)
}

write_heatmap(heatmap, "images/elephant_heatmap.png") 
```


![](images/elephant_heatmap.png)

<br/>

Na koniec skorzystamy z pakietu magic w celu nałożenia wygenerowanej mapy ciepła na oryginalny obraz :

```{r}
library(magick) 
library(viridis) 

# Wczytujemy oryginalny obraz.
image <- image_read(img_path)
info <- image_info(image) 
geometry <- sprintf("%dx%d!", info$width, info$height) 

# Tworzymy półprzezroczystą mapę aktywacji.
pal <- col2rgb(viridis(20), alpha = TRUE) 
alpha <- floor(seq(0, 255, length = ncol(pal))) 
pal_col <- rgb(t(pal), alpha = alpha, maxColorValue = 255)
write_heatmap(heatmap, "images/elephant_overlay.png", 
              width = 14, height = 14, bg = NA, col = pal_col) 

# Nakładamy mapę cieplną na oryginalny obraz.
image_read("images/elephant_overlay.png") %>% 
  image_resize(geometry, filter = "quadratic") %>% 
  image_composite(image, operator = "blend", compose_args = "20") %>%
  plot() 
```


Korzystając z tej techniki, możemy odpowiedzieć na dwa pytania:

*	Dlaczego sieć uważa, że na tym zdjęciu znajduje się słoń afrykański?
*	W którym miejscu fotografii znajduje się słoń afrykański?

Ciekawą obserwacją jest to, że uszy małego słonia wywołują największą aktywację sieci. Ten szczegół zdjęcia pokazuje prawdopodobnie element, po którym sieć odróżnia słonia afrykańskiego od słonia indyjskiego.

