---
title: "Deep Dream"
output: 
  html_notebook: 
    theme: cerulean
    highlight: textmate
---

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



## Implementacja algorytmu DeepDream w pakiecie Keras

Zaczniemy od konwolucyjnej sieci neuronowej wytrenowanej na zbiorze obrazów ImageNet. Pakiet Keras zawiera wiele takich sieci. Są to między innymi: VGG16, VGG19, Xception i Res-Net50. Algorytm DeepDream może zostać zaimplementowany przy użyciu każdej z tych sieci, ale wybór sieci będzie miał oczy-wiście wpływ na generowane wizualizacje. Wynika to z tego, że różne architektury sieci konwolucyjnych uczą się różnych cech. W oryginalnym algorytmie DeepDream zastosowano model Inception. Wykorzystanie tego modelu pozwala na wygenerowanie ładnie wyglądających grafik, a więc skorzystamy z modelu In-ception V3 dołączonego do pakietu Keras.

```{r}
library(keras)

# Nie będziemy trenować modelu. 
# Polecenie to wyłącza wszystkie operacje używane tylko podczas trenowania.
k_set_learning_phase(0)

# Sieć Inception V3 jest budowana bez swojej konwolucyjnej bazy. 
# Model zostanie załadowany z wagami wytrenowanymi na zbiorze ImageNet.
model <- application_inception_v3(
  weights = "imagenet", 
  include_top = FALSE,
)
```

Następnie musimy zająć się obliczaniem straty — wartości, którą będziemy starali się maksymalizować w procesie wzrostu gradientu. W rozdziale 5. podczas filtrowania wizualizacji staraliśmy się maksymalizować wartość określonego filtra wybranej warstwy sieci. Tym razem będziemy jednocześnie maksymalizować aktywacje wszystkich filtrów wielu warstw, a konkretnie rzecz biorąc, będziemy maksymalizować sumę normy L2 aktywacji zbioru warstw wysokiego poziomu. Wybór warstw (a także dokładanie się poszczególnych warstw do finalnej wartości straty) ma największy wpływ na generowane wizualizacje. W związku z tym chcemy, aby parametry te można było z łatwością modyfikować. Niższe warstwy odpowiadają za wzorce geometryczne, a wyższe warstwy odpowiadają za elementy obrazu pozwalające na rozpoznawanie klas zbioru ImageNet (np. ptaków lub psów). Zaczniemy od niezbyt optymalnej konfiguracji czterech warstw, ale z pewnością warto wypróbować później działanie wielu innych konfiguracji.

```{r}
# Lista z nazwami przypisująca nazwy warstw do współczynników wpływu aktywacji warstw na 
# wartość straty, którą chcemy maksymalizować. 
# Zauważ, że nazwy warstw są wprowadzone na stałe w wbudowanej aplikacji Inception V3.
# Listę nazw wszystkich warstw modelu można wyświetlić za pomocą polecenia summary(model).
layer_contributions <- list(
  mixed2 = 0.2,
  mixed3 = 3,
  mixed4 = 2,
  mixed5 = 1.5
)
```

Teraz czas zdefiniować tensor zawierający wartość straty: ważoną sumę normy L2 aktywacji warstw z poprzedniego listingu.

```{r}
# Tworzy listę przypisującą nazwy warstw do instancji warstw.
layer_dict <- model$layers
names(layer_dict) <- lapply(layer_dict, function(layer) layer$name) 

# Strata będzie definiowana przez dodanie wartości charakteryzujących wpływ poszczególnych warstw na stratę.
loss <- k_variable(0) 
for (layer_name in names(layer_contributions)) {
  # Dodaje normę L2 cech warstwy do straty.
  coeff <- layer_contributions[[layer_name]]
  activation <- layer_dict[[layer_name]]$output
  scaling <- k_prod(k_cast(k_shape(activation), "float32"))
  loss <- loss + (coeff * k_sum(k_square(activation)) / scaling)
}
```

Teraz możemy uruchomić proces wzrostu gradientu.

```{r}
# W tym tensorze znajduje się wygenerowany obraz (wizja).
dream <- model$input

# Normalizuje gradienty (to ważny zabieg).
grads <- k_gradients(loss, dream)[[1]]
grads <- grads / k_maximum(k_mean(k_abs(grads)), 1e-7)

# Konfiguruje funkcję Keras służącą do uzyskiwania wartości straty i gradientów 
# na podstawie obrazu wejściowego.
outputs <- list(loss, grads)
fetch_loss_and_grads <- k_function(list(dream), outputs)

eval_loss_and_grads <- function(x) {
  outs <- fetch_loss_and_grads(list(x))
  loss_value <- outs[[1]]
  grad_values <- outs[[2]]
  list(loss_value, grad_values)
}

gradient_ascent <- function(x, iterations, step, max_loss = NULL) {
  for (i in 1:iterations) {
    c(loss_value, grad_values) %<-% eval_loss_and_grads(x)
    if (!is.null(max_loss) && loss_value > max_loss)
      break
    cat("...Wartość straty", i, ":", loss_value, "\n")
    x <- x + (step * grad_values)
  }
  x
}
```

Na koniec możemy zająć się właściwym algorytmem DeepDream. 

Na początku definiowana jest lista skal (nazywanych również ok-tawami), które są używane podczas przetwarzania obrazów. Każda kolejna skala jest większa od poprzedniej o współczynnik równy 1,4 (jest o 40% większa) — zaczynamy od przetwarzania małego obrazu, a następnie zwiększamy jego skalę (patrz rysu-nek 8.4).

![deep dream process](img\8_2.png)

Po każdej kolejnej operacji skalowania (od najmniejszej do największej) uruchamiany jest algorytm wzrostu gradientu w celu maksymalizacji zdefiniowanej wcześniej straty przy danej skali. Po każdym zakończeniu pracy tego algorytmu skala obrazu jest zwiększana o 40%.

W celu uniknięcia utraty dużej ilości szczegółów obrazu po każdej operacji skalowania (w wyniku tych operacji otrzymywany jest coraz bardziej rozmyty i rozpikselowany obraz) możemy wykonać prosty zabieg polegający na ponownym dodaniu utraconych szczegółów do obrazu. Jest to możliwe do wykonania, ponieważ wiemy, jak powinien wyglądać oryginalny obraz w większej rozdzielczości. Dysponując obrazem S o małym rozmiarze i obrazem L o większym rozmiarze, możemy przekształcić obraz L do rozmiaru obrazu S i określić różnice między tymi obrazami — różnica ta będzie wskazywać utracone szczegóły.

```{r}
resize_img <- function(img, size) {
  image_array_resize(img, size[[1]], size[[2]])
}

save_img <- function(img, fname) {
  img <- deprocess_image(img)
  image_array_save(img, fname)
}

# Funkcja narzędziowa  zamieniająca obrazy w tensory.
preprocess_image <- function(image_path) {
  image_load(image_path) %>% 
    image_to_array() %>% 
    array_reshape(dim = c(1, dim(.))) %>% 
    inception_v3_preprocess_input()
}

# Funkcja pomocnicza zmieniająca tensory w obrazy
deprocess_image <- function(img) {
  img <- array_reshape(img, dim = c(dim(img)[[2]], dim(img)[[3]], 3))
  img <- img / 2
  img <- img + 0.5
  img <- img * 255
  
  dims <- dim(img)
  img <- pmax(0, pmin(img, 255))
  dim(img) <- dims
  img
}
```

```{r}
# Modyfikacja tych parametrów pozwala na uzyskanie innych efektów wizualnych.

step <- 0.01          # Rozmiar kroku algorytmu wzrostu gradientu.
num_octave <- 3       # Liczba operacji skalowania, przy których należy uruchomić algorytm wzrostu gradientu.
octave_scale <- 1.4   # Różnica między rozmiarami kolejnych wersji obrazu.
iterations <- 20      # Liczba kroków wzrostu wykonywanych przy każdej operacji skalowania.

# Jeżeli strata przekroczy wartość równą 10, to proces wzrostu gradientu 
# zostanie przerwany w celu zapobiegnięcia powstawania brzydkich artefaktów.
max_loss <- 10  

# Tu należy umieścić ścieżkę obrazu, który chcemy przetwarzać.
dir.create("dream")
base_image_path <- "~/Downloads/creative_commons_elephant.jpg"

# Ładowanie obrazu do tablicy (funkcję tę zdefiniowano w listingu 8.13).
img <- preprocess_image(base_image_path)

# Przygotowywanie listy kształtów definiujących skalowania,
# przy których uruchomiony zostanie algorytm wzrostu gradientu.
original_shape <- dim(img)[-1]
successive_shapes <- list(original_shape)
for (i in 1:num_octave) { 
  shape <- as.integer(original_shape / (octave_scale ^ i))
  successive_shapes[[length(successive_shapes) + 1]] <- shape 
}

# Odwracanie listy kształtów tak, aby znalazły się w kolejności rosnącej.

successive_shapes <- rev(successive_shapes) 

# Zmiana rozmiaru tablicy obrazu w celu zmniejszenia jego skali.
original_img <- img 
shrunk_original_img <- resize_img(img, successive_shapes[[1]])

for (shape in successive_shapes) {
  cat("Zmiana kształtu obrazu", shape, "\n")
  img <- resize_img(img, shape)
  img <- gradient_ascent(img,
                         iterations = iterations,
                         step = step,
                         max_loss = max_loss)
  upscaled_shrunk_original_img <- resize_img(shrunk_original_img, shape)
  same_size_original <- resize_img(original_img, shape)
  lost_detail <- same_size_original - upscaled_shrunk_original_img
  
  img <- img + lost_detail
  shrunk_original_img <- resize_img(original_img, shape)
  save_img(img, fname = sprintf("dream/at_scale_%s.png",
                                paste(shape, collapse = "x")))
}

save_img(img, fname = "dream/final_dream.png")
```

```{r}
plot(as.raster(deprocess_image(img) / 255))
```