---
title: "Neuronowy transfer stylu"
output: 
  html_notebook: 
    theme: cerulean
    highlight: textmate
---

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


Kolejnym ważnym rozwiązaniem modyfikującym obrazy, opartym na technologii uczenia głębokiego, jest neuronowy transfer stylu opracowany latem 2015 r. przez zespół kierowany przez Leona Gatysego . Algorytm neuronowego transferu stylu od tego czasu był wielokrotnie usprawniany i modyfikowany. Zastosowano go w wielu aplikacjach pozwalających na edycję zdjęć przy użyciu smartfona. Dla uproszczenia skupimy się na oryginalnej wersji tego algorytmu.

Neuronowy transfer stylu polega na zastosowaniu stylu obrazu referencyjnego w celu przetworzenia innego obrazu z zacho-waniem jego zawartości:
![style transfer](img\8_3.png)

Pojęcie stylu odnosi się do tekstur, kolorów i sposobu przedstawiania rzeczy widocznych na obrazie. Treścią określamy wysokopoziomową makrostrukturę obrazu. Niebieskie i żółte linie narysowane pędzlem widoczne na rysunku 8.7 (obraz Gwiaździsta noc, namalowany przez Vincenta van Gogha) charakteryzują styl, a budynki widoczne na zdjęciu Tübingen są treścią.

Idea transferu stylu powiązana z generowaniem tekstur była znana w środowisku osób zajmujących się przetwarzaniem obrazu na długo przed pojawieniem się w 2015 r. neuronowego transfe-ru stylu, ale transfer stylu oparty na technikach uczenia głębokiego okazał się dawać o wiele lepsze rezultaty od tych, które uzyskiwano z zastosowaniem klasycznych technik przetwa-rzania obrazu. Ta nowatorska technika zyskała wiele kreatyw-nych zastosowań.

Implementacja transferu stylu jest oparta na tych samych rozwiązaniach, co wszystkie algorytmy uczenia głębokiego — definiujemy w niej funkcję straty i staramy się ją zminimali-zować. Celem algorytmu jest zachowanie treści oryginalnego obrazu przy jednoczesnym przyjęciu stylu obrazu referencyjne-go. Gdybyśmy mogli matematycznie zdefiniować treść (content) i styl (style), to wówczas funkcja straty (loss) miałaby nastę-pującą postać:

```
loss <- distance(style(reference_image) - style(generated_image)) +
        distance(content(original_image) - content(generated_image))
```

Distance (odległość) jest funkcją normy takiej jak norma L2, content jest funkcją przyjmującą obraz i generującą reprezen-tację jego treści, a style — funkcją przyjmującą obraz i obl-czającą reprezentację jego stylu. Minimalizacja straty spra-wia, że wartość zwracana przez funkcję style(generated_image) zbliża się do wartości zwracanej przez style(reference_image), a content(generated_image) zbliża się do content(original_image), co prowadzi do zdefiniowanego wcześniej transferu stylu.

Głównym spostrzeżeniem Gatysa i jego zespołu było to, że głębokie konwolucyjne sieci neuronowe umożliwiają matematycz-ne zdefiniowanie funkcji style i content. Sprawdźmy, jak do tego dochodzi.

## Strata treści

Przypominam, że aktywacje wcześniejszych warstw sieci zawierają lokalne informacje o obrazie, a aktywacje wyższych warstw zawierają coraz bardziej globalne i abstrakcyjne informacje. W związku z tym można przyjąć, że aktywacje różnych warstw konwolucyjnej sieci zawierają rozkład treści obrazu przeprowadzony według różnych przestrzennych skal, a więc treść obrazu, która jest bardziej globalna i abstrakcyjna, powinna być opisywana przez reprezentacje górnych warstw sieci konwolucyjnej.

Dobrym kandydatem na funkcję straty treści jest norma L2 pomiędzy aktywacjami górnej warstwy uprzednio wytrenowanej sieci neuronowej, obliczona przy użyciu przetwarzanego obrazu i aktywacji tej samej warstwy określonych z zastosowaniem wy-generowanego obrazu. Rozwiązanie takie gwarantuje to, że z punktu widzenia górnej warstwy wygenerowany obraz będzie wy-glądał podobnie do oryginalnego obrazu, oczywiście przy zało-żeniu, że górne warstwy konwolucyjnej sieci neuronowej na-prawdę „widzą” treść obrazów wejściowych. Wówczas rozwiązanie takie pozwoli na zachowanie treści obrazu.

## Strata stylu


Mechanizm obliczający stratę treści korzysta tylko z jednej górnej warstwy, a mechanizm obliczający stratę stylu według Gatysa korzysta z wielu warstw sieci konwolucyjnej — próbujemy wziąć pod uwagę styl referencyjnego obrazu, który jest rozsiany po wszystkich przestrzennych skalach sieci konwolucyjnej. Gates, określając stratę stylu, korzysta z macierzy Grama składającej się z aktywacji warstw — iloczynu skalarnego map cech danej warstwy. Iloczyn skalarny może być rozumiany jako reprezentacja mapy korelacji między cechami warstwy. Korelacje cech określają parametry statystyczne wzorców poszczególnych skal przestrzennych, co empirycznie odpowiada wyglądowi tekstur skal.

Mechanizm obliczający stratę stylu próbuje zachować podobne do siebie wewnętrzne korelacje wewnątrz aktywacji różnych warstw między stylem obrazu referencyjnego a stylem obrazu wygenerowanego. Rozwiązanie to sprawia, że tekstury znalezione w różnych przestrzennych skalach obrazu referencyjnego mają podobny styl do tych, które występują w wygenerowanym obrazie.



## W skrócie


Ogólnie rzecz biorąc, możemy skorzystać z wytrenowanej wcześniej konwolucyjnej sieci neuronowej w celu zdefiniowania funkcji straty, która:

*	Zachowa treść poprzez utrzymanie podobnych wysokopozi-mowych warstw aktywacji treści przetwarzanego obrazu i wygenerowanego obrazu. Sieć konwolucyjna powinna „po-strzegać” oba te obrazy tak, jakby przedstawiały to samo.
*	Zachowa styl, utrzymując podobne korelacje aktywacji warstw niskiego poziomu, a także warstw wysokiego poziomu. Korelacje cech odwzorowują tekstury — wygenerowany obraz i obraz referencyjny powinny charakteryzować się takimi samymi teksturami na różnych przestrzennych ska-lach.


Przyjrzyjmy się implementacji w Keras oryginalnego algorytmu neuronowego transferu stylu opracowanego w 2015 r. Rozwiązanie to jest pod wieloma względami podobne do implementacji algorytmu DeepDream zaprezentowanej w poprzednim podrozdziale.

## Implementacja neuronowego transferu stylu przy użyciu pakietu Keras



Neuronowy transfer stylu może zostać zaimplementowany przy użyciu dowolnej wytrenowanej wcześniej konwolucyjnej sieci neuronowej. Skorzystamy z sieci VGG19 — tej samej, z której korzystał również Gates. VGG19 jest prostą wersją sieci VGG16 opisanej w rozdziale 5. Sieć ta zawiera trzy dodatkowe warstwy konwolucyjne.

Oto czynności, które musisz wykonać:

* 1.	Skonfiguruj sieć obliczającą jednocześnie aktywacje warstw VGG19 obrazu referencyjnego, obrazu przetwarzanego i obrazu wyjściowego.
* 2.	Skorzystaj z obliczonych aktywacji warstw wszystkich trzech obrazów w celu zdefiniowania opisanej wcześniej funkcji straty, którą będziesz minimalizować w celu osiągnięcia transferu stylu.
* 3.	Skonfiguruj algorytm spadku gradientowego w celu zmini-malizowania funkcji straty.

Zacznijmy od zdefiniowania ścieżek obrazu referencyjnego i obrazu, który ma zostać przetworzony. Transfer przebiega łatwiej w przypadku obrazów o zbliżonych rozmiarach, a więc później zmienimy rozmiary obrazów tak, aby wszystkie miały wysokość 400 pikseli.

```{r}
library(keras)

# Ścieżka obrazu, który ma zostać zmodyfikowany.
target_image_path <- "style_transfer/portrait.png" 

# Ścieżka obrazu referencyjnego.
style_reference_image_path <- "style_transfer/transfer_style_reference.png"

# Wymiary wygenerowanego obrazu..
img <- image_load(target_image_path)
width <- img$size[[1]]
height <- img$size[[2]]
img_nrows <- 400
img_ncols <- as.integer(width * img_nrows / height)  
```

Potrzebujemy jeszcze funkcji pomocniczych służących do łado-wania, przetwarzania wstępnego i przetwarzania końcowego obrazów wejściowych i wyjściowych sieci konwolucyjnej VGG19.

```{r}
preprocess_image <- function(path) {
  img <- image_load(path, target_size = c(img_nrows, img_ncols)) %>%
    image_to_array() %>%
    array_reshape(c(1, dim(.)))
  imagenet_preprocess_input(img)
}

deprocess_image <- function(x) {
  x <- x[1,,,]
  # Wyśrodkowywanie w punkcie zerowym.
  x[,,1] <- x[,,1] + 103.939
  x[,,2] <- x[,,2] + 116.779
  x[,,3] <- x[,,3] + 123.68
  # 'BGR'->'RGB'
  x <- x[,,c(3,2,1)]
  x[x > 255] <- 255
  x[x < 0] <- 0
  x[] <- as.integer(x)/255
  x
}
```

Czas skonfigurować sieć VGG19. Na wejściu przyjmuje ona wsad składający się z trzech obrazów: obrazu referencyjnego, obra-zu przetwarzanego i obrazu zastępczego, który zostanie wypełniony wygenerowaną grafiką. Obraz zastępczy jest symbolicznym tensorem, którego wartości są ustalane z zewnątrz przy użyciu tablic. Obraz referencyjny i obraz przetwarzany mają charakter statyczny, a więc definiuje się je przy użyciu polecenia k_constant. Dane obrazu zastępczego będą z czasem wypełniane danymi obrazu generowanego.

```{r}
target_image <- k_constant(preprocess_image(target_image_path))
style_reference_image <- k_constant(
  preprocess_image(style_reference_image_path)
)

# Obiekt zastępczy, który zostanie wypełniony danymi wygenerowanego obrazu.
combination_image <- k_placeholder(c(1, img_nrows, img_ncols, 3)) 

# Wsad składający się z trzech obrazów.
input_tensor <- k_concatenate(list(target_image, style_reference_image, 
                                   combination_image), axis = 1)

# Budowanie sieci VGG19 z danymi wejściowymi w postaci wsadu składającego się z trzech obrazów. 
# Model zostanie załadowany z wagami wytrenowanymi wcześniej na podstawie zbioru ImageNet.
model <- application_vgg19(input_tensor = input_tensor, 
                           weights = "imagenet", 
                           include_top = FALSE)

cat("Model został załadowany.\n")
```

Czas zdefiniować stratę treści, dzięki której górna warstwa sieci konwolucyjnej VGG19 będzie postrzegać w podobny sposób obraz przetwarzany i obraz generowany.

```{r}
content_loss <- function(base, combination) {
  k_sum(k_square(combination - base))
}
```

Teraz możemy zdefiniować stratę stylu. Podczas jej obliczania korzystamy z funkcji pomocniczej tworzącej macierz Grama — mapę korelacji cech początkowej macierzy.

```{r}
gram_matrix <- function(x) {
  features <- k_batch_flatten(k_permute_dimensions(x, c(3, 1, 2)))
  gram <- k_dot(features, k_transpose(features))
  gram
}

style_loss <- function(style, combination){
  S <- gram_matrix(style)
  C <- gram_matrix(combination)
  channels <- 3
  size <- img_nrows*img_ncols
  k_sum(k_square(S - C)) / (4 * channels^2  * size^2)
}
```

Do tych dwóch elementów straty należy dodać trzeci — całkowitą stratę wariacji. Parametr ten odwołuje się do pikseli wygenerowanego obrazu i ułatwia uzyskanie przestrzennej ciągłości tego obrazu, zapobiegając jego całkowitemu rozpikselowaniu. Można go traktować jako stratę regularyzacji.

```{r}
total_variation_loss <- function(x) {
  y_ij  <- x[,1:(img_nrows - 1L), 1:(img_ncols - 1L),]
  y_i1j <- x[,2:(img_nrows), 1:(img_ncols - 1L),]
  y_ij1 <- x[,1:(img_nrows - 1L), 2:(img_ncols),]
  a <- k_square(y_ij - y_i1j)
  b <- k_square(y_ij - y_ij1)
  k_sum(k_pow(a + b, 1.25))
}
```

Minimalizowana strata będzie średnią ważoną tych trzech strat. W celu obliczenia straty treści musimy odwołać się tylko do jednej górnej warstwy modelu (warstwy block5_conv2), ale podczas obliczania strat stylu musimy korzystać z listy warstw, na której znajdują się warstwy niskiego oraz wysokiego poziomu. Na koniec tego procesu należy pamiętać o dodaniu całkowitej straty wariacji.

Najprawdopodobniej podczas samodzielnej pracy odczujesz chęć dostrojenia współczynnika content_weight pod kątem wybranych obrazów (referencyjnego i przetwarzanego). Współczynnik ten określa wpływ straty treści na całkowitą wartość straty. Wyższa wartość content_weight sprawi, że w wygenerowanym obra-zie łatwiej będzie dostrzec zawartość przetwarzanego obrazu.

```{r}
# Lista przypisująca nazwy warstw do tensorów aktywacji.
outputs_dict <- lapply(model$layers, `[[`, "output")
names(outputs_dict) <- lapply(model$layers, `[[`, "name")

# Warstwa używana podczas obliczania straty treści.
content_layer <- "block5_conv2" 

# Warstwy używane podczas obliczania straty stylu.
style_layers = c("block1_conv1", "block2_conv1",
                 "block3_conv1", "block4_conv1",
                 "block5_conv1")

# Wagi używane podczas obliczania średniej ważonej (całkowitej wartości straty).
total_variation_weight <- 1e-4
style_weight <- 1.0
content_weight <- 0.025

# Define the loss by adding all components to a `loss` variable
loss <- k_variable(0.0) 
layer_features <- outputs_dict[[content_layer]] 
target_image_features <- layer_features[1,,,]
combination_features <- layer_features[3,,,]

loss <- loss + content_weight * content_loss(target_image_features,
                                             combination_features)

for (layer_name in style_layers){
  layer_features <- outputs_dict[[layer_name]]
  style_reference_features <- layer_features[2,,,]
  combination_features <- layer_features[3,,,]
  sl <- style_loss(style_reference_features, combination_features)
  loss <- loss + ((style_weight / length(style_layers)) * sl)
}

loss <- loss + 
  (total_variation_weight * total_variation_loss(combination_image))
```

Na koniec musimy skonfigurować algorytm spadku gradientowego. W pracy Gatysa optymalizacja jest przeprowadzana przy użyciu algorytmu L-BFGS. Skorzystamy z tego samego rozwiązania. To jedna z ważniejszych różnic między tą implementacją neuronowego transferu stylu a implementacją techniki DeepDream opisaną w poprzednim podrozdziale. Implementacja algorytmu L-BFGS jest w postaci funkcji optim(), ale charakteryzuje się dwoma ograniczeniami:

*	Wymaga przekazania wartości funkcji straty i wartości gradientów w formie dwóch oddzielnych funkcji.
*	Można jej używać tylko do przetwarzania płaskich wekt-rów, a my mamy do przetworzenia trójwymiarową tablicę reprezentującą obraz.


Obliczanie wartości funkcji straty i wartości gradientów w sposób niezależny byłoby niewydajne, ponieważ wiązałoby się z koniecznością wykonywania wielu zbędnych obliczeń — taki proces przebiegałby praktycznie dwukrotnie wolniej od procesu jednoczesnego obliczania tych wartości. W celu obejścia tego ograniczenia skorzystamy z klasy R6 o nazwie Evaluator, która jednocześnie oblicza wartość straty i wartości gradientów, a następnie przy pierwszym wywołaniu zwraca wartość straty, a przy drugim — wartości gradientów.

```{r}
# Ustalanie wartości gradientów wygenerowanego obrazu.
grads <- k_gradients(loss, combination_image)[[1]] 

# Funkcja przechwytująca bieżące wartości straty i gradientów.
fetch_loss_and_grads <- k_function(list(combination_image), list(loss, grads))

eval_loss_and_grads <- function(image) {
  image <- array_reshape(image, c(1, img_nrows, img_ncols, 3))
  outs <- fetch_loss_and_grads(list(image))
  list(
    loss_value = outs[[1]],
    grad_values = array_reshape(outs[[2]], dim = length(outs[[2]]))
  )
}

library(R6)
Evaluator <- R6Class("Evaluator",
  public = list(
    
    loss_value = NULL,
    grad_values = NULL,
    
    initialize = function() {
      self$loss_value <- NULL
      self$grad_values <- NULL
    },
    
    loss = function(x){
      loss_and_grad <- eval_loss_and_grads(x)
      self$loss_value <- loss_and_grad$loss_value
      self$grad_values <- loss_and_grad$grad_values
      self$loss_value
    },
    
    grads = function(x){
      grad_values <- self$grad_values
      self$loss_value <- NULL
      self$grad_values <- NULL
      grad_values
    }
  )
)

evaluator <- Evaluator$new()
```

Na koniec uruchamiamy proces wzrostu gradientowego. Korzystamy z algorytmu L-BFGS. W każdej iteracji algorytmu zapisujemy aktualną wersję generowanego obrazu (pojedyncza iteracja jest reprezentacją 20 kroków algorytmu wzrostu gradientu).

```{r}
iterations <- 20

dms <- c(1, img_nrows, img_ncols, 3)

# Stan początkowy: obraz docelowy.
x <- preprocess_image(target_image_path)
# Spłaszczamy obraz, ponieważ optymalizator może przetwarzać tylko płaskie wektory.
x <- array_reshape(x, dim = length(x))  

for (i in 1:iterations) { 
  
  # Optymalizacja L-BFGS przetwarza piksele wygenerowanego obrazu w celu zminimalizowania straty.
  opt <- optim(
    array_reshape(x, dim = length(x)), 
    fn = evaluator$loss, 
    gr = evaluator$grads, 
    method = "L-BFGS-B",
    control = list(maxit = 15)
  )
  
  cat("Strata:", opt$value, "\n")
  
  image <- x <- opt$par
  image <- array_reshape(image, dms)
  
  im <- deprocess_image(image)
  plot(as.raster(im))
}
```


![](style_transfer/portrait_styled.png)

Pamiętaj o tym, że technika ta przeprowadza transformację polegającą na zmianie tekstur lub ich przeniesieniu. Najlepiej sprawdza się z obrazami referencyjnymi wypełnionymi wyraźnymi teksturami i obrazami źródłowymi, które nie wymagają dużej ilości szczegółów do bycia rozpoznawalnymi. Zwykle nie da się w ten sposób wykonać operacji zmiany stylu portretu. Algorytm ten jest bliższy klasycznym technikom przetwarzania sygnału niż sztucznej inteligencji, a więc nie należy od niego oczekiwać cudów!

Działanie tego algorytmu jest dość powolne, ale transformacja ta jest na tyle prosta, że można jej dokonać również za pomocą małej szybkiej jednokierunkowej sieci konwolucyjnej (wymogiem jest dysponowanie odpowiednim zbiorem danych treningowych). Szybki transfer stylu może być osiągnięty poprzez wygenerowanie wejściowych i wyjściowych obrazów treningowych utrzymanych w jednym stylu za pomocą techniki zaprezentowanej w tym podrozdziale. Po wykonaniu tej operacji należy wytrenować prostą sieć konwolucyjną pod kątem wykonywania transformacji zgodnej z określonym stylem. Teraz zmiana stylu obrazu będzie przeprowadzana praktycznie natychmiastowo w wyniku jednej iteracji algorytmu małej konwolucyjnej sieci neuronowej.


## Wnioski

*	Transfer stylu polega na utworzeniu nowego obrazu, na którym zachowana zostanie treść przetwarzanego obrazu, ale zostanie ona przedstawiona w stylu obrazu referencyjnego.
*	Treść może zostać rozpoznana przez aktywacje wysokopo-ziomowych warstw konwolucyjnej sieci neuronowej.
*	Styl może zostać rozpoznany przez wewnętrzne korelacje aktywacji różnych warstw konwolucyjnej sieci neuronowej.
*	Uczenie głębokie pozwala na utworzenie mechanizmu trans-feru stylu w formie procesu optymalizacji korzystającego z funkcji straty zdefiniowanej przy użyciu wytrenowanej wcześniej konwolucyjnej sieci neuronowej.
*	Tę prostą ideę można rozbudowywać i modyfikować.

