---
title: "Wprowadzenie do generatywnych sieci z przeciwnikiem"
output: 
  html_notebook: 
    theme: cerulean
    highlight: textmate
---

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


## Schematyczna implementacja sieci GAN


W tej sekcji wyjaśnię implementację najprostszej formy sieci GAN w pakiecie Keras. Sieci GAN są zaawansowane, a więc zagłębianie się w techniczne szczegóły wykraczałoby poza zakres tematyczny tej książki. Zaprezentuję implementację głębokiej konwolucyjnej sieci GAN (DCGAN) — sieci GAN, w której generator i dyskryminator są głębokimi sieciami konwolucyjnymi. W praktyce zastosuję warstwę lay-er_conv_2d_transpose w celu zwiększenia próbkowania w generatorze.

Sieć GAN będzie trenowana na zbiorze CIFAR10 składającym się z 50 000 kolorowych obrazów o rozdzielczości 3232, podzielonych na 10 równych klas (w każdej z klas znajduje się 5000 obrazów). Dla ułatwienia będziemy korzystać tylko z obrazów należących do klasy „żaba”.

Schematycznie działanie tej sieci GAN można przedstawić w następujący sposób:

* 1.	Sieć generatora (generator) mapuje wektory o kształcie (latent_dim) na obrazy o kształcie (32, 32, 3).
* 2.	Sieć dyskryminatora (discriminator) mapuje obrazy o kształcie (32, 32, 3) na binarną wartość określającą prawdopodobieństwo tego, że obraz jest prawdziwy.
* 3.	Sieć gan tworzy łańcuch składający się z generatora i dyskryminatora: gan(x) <- discriminator(generator(x)). Sieć gan mapuje wektory niejawnej przestrzeni na oceny realizmu wystawiane przez dyskryminator.
* 4.	Trenujemy dyskryminator przy użyciu przykładów prawdziwych i sztucznych obrazów oznaczonych etykietami, tak jakbyśmy trenowali zwykły model klasyfikacji obrazów.
* 5.	W celu wytrenowania generatora korzystamy z gradien-tów wag generatora w odniesieniu do straty modelu gan. W związku z tym każdy krok trenowania ma modyfikować wagi generatora tak, aby zwiększyć prawdopodobieństwo zaklasyfikowania wygenerowanych obrazów jako prawdzi-wych. Innymi słowy, trenujemy generator tak, aby był w stanie oszukać dyskryminator.


## Zbiór przydatnych rozwiązań

Proces trenowania sieci GAN i dostrajania ich implementacji jest bardzo trudny. W związku z tym warto poznać rozwiązania ułatwiające pracę z tymi sieciami. Rozwiązania te mają naturę heurystyczną i nie są teoretycznymi wskazówkami — podobnie jak z większością zagadnień uczenia głębokiego, mamy do czynienia bardziej z alchemią niż fizyką. Mają one pomóc w zrozumieniu bieżącego problemu. Są sprawdzone w działaniu, ale nie nadają się do każdego kontekstu.

Oto kilka rozwiązań zastosowanych w zaprezentowanej w tym podrozdziale implementacji generatora i dyskryminatora GAN. Nie jest to lista wszystkich możliwych rozwiązań pomocni-czych. Jeżeli zainteresował Cię ten temat, to zajrzyj do książek poświęconych sieciom GAN.


*	Ostatnią warstwą aktywacji modelu jest tanh, a nie wa-stwa sigmoid spotykana w większości innych modeli.
*	Próbkowanie punktów z niejawnej przestrzeni dokonujemy przy użyciu rozkładu normalnego (rozkładu Gaussa), a nie rozkładu jednorodnego.
*	Stochastyczność przyczynia się do uzyskania bardziej solidnego modelu. Trenowanie sieci GAN przyczynia się do powstania dynamicznej równowagi, a więc proces ten może utknąć w wielu punktach. Wprowadzanie losowości do procesu trenowania pomaga temu zapobiec. Losowość wprowadzamy na dwa sposoby: korzystając z mechanizmu odrzucania zaimplementowanego w dyskryminatorze i po-przez dodanie losowego szumu do etykiet przetwarzanych przez dyskryminator.
*	Rzadkie gradienty mogą przeszkodzić w trenowaniu sieci GAN. W uczeniu głębokim rzadki charakter danych jest często czymś wręcz pożądanym, ale nie dotyczy to sieci GAN. Do powstawania rzadkich gradientów mogą przyczynić się dwie rzeczy: operacje maxpooling i aktywacje ReLU. Zamiast operacji maxpooling polecam stosowanie krokowych konwolucji w celu zmniejszenia objętości próbek, a zamiast aktywacji ReLU polecam stosowanie warstwy layer_activation_leaky_relu. Działa ona podobnie do warstwy ReLU, ale nie posiada tak dużych ograniczeń — pozwala na pojawianie się niewielkich ujemnych wartości aktywacji.
*	W wygenerowanych obrazach często pojawiają się art-fakty wyglądające jak szachownica (patrz rysunek 8.17). Powstają one w wyniku nierównego pokrycia przestrzeni pikseli w generatorze. W celu rozwiązania tego problemu, za każdym razem, gdy będziemy korzystać z kroku layer_conv_2d_transpose lub layer_conv_2d, zastosujemy rozmiar jądra podzielny przez rozmiar kroku (dotyczy to generatora i dyskryminatora).


## Generator

Zacznijmy od opracowania modelu generator zamieniającego wektor pochodzący z niejawnej przestrzeni (podczas trenowania będzie on próbkowany losowo) w obraz. Jednym z typowych problemów spotykanych podczas pracy z sieciami GAN jest stałe generowanie obrazów wyglądających jak szum. Można to rozwiązać, stosując technikę odrzucania w implementacjach dyskryminatora i generatora.

```{r}
library(keras)

latent_dim <- 32
height <- 32
width <- 32
channels <- 3

generator_input <- layer_input(shape = c(latent_dim))

generator_output <- generator_input %>% 
  
  # Zamiana obiektu wejściowego w 128-kanałową mapę cech o wymiarach 1616.
  layer_dense(units = 128 * 16 * 16) %>%
  layer_activation_leaky_relu() %>% 
  layer_reshape(target_shape = c(16, 16, 128)) %>% 
  
  # Warstwa konwolucyjna.
  layer_conv_2d(filters = 256, kernel_size = 5, 
                padding = "same") %>% 
  layer_activation_leaky_relu() %>% 
  
  # Zwiększenie rozmiaru do 3232.
  layer_conv_2d_transpose(filters = 256, kernel_size = 4, 
                          strides = 2, padding = "same") %>% 
  layer_activation_leaky_relu() %>% 
  
  # Kolejne warstwy konwolucyjne.
  layer_conv_2d(filters = 256, kernel_size = 5, 
                padding = "same") %>% 
  layer_activation_leaky_relu() %>% 
  layer_conv_2d(filters = 256, kernel_size = 5, 
                padding = "same") %>% 
  layer_activation_leaky_relu() %>% 
  
  # Generuje jednokanałową mapę cech o rozmiarze 3232 (rozmiar ten jest taki sam jak rozmiar obrazów wchodzących w skład zbioru CIFAR10).
  layer_conv_2d(filters = channels, kernel_size = 7,
                activation = "tanh", padding = "same")

generator <- keras_model(generator_input, generator_output)
summary(generator)
```

## Dyskryminator


Teraz możemy przystąpić do pracy nad modelem discriminator, który przyjmuje na swoim wejściu obraz (prawdziwy lub sztuczny) i klasyfikuje go do jednej z dwóch klas: „obraz wygenerowany” lub „obraz pochodzący z treningowego zbioru danych”.

```{r}
discriminator_input <- layer_input(shape = c(height, width, channels))

discriminator_output <- discriminator_input %>% 
  layer_conv_2d(filters = 128, kernel_size = 3) %>% 
  layer_activation_leaky_relu() %>% 
  layer_conv_2d(filters = 128, kernel_size = 4, strides = 2) %>% 
  layer_activation_leaky_relu() %>% 
  layer_conv_2d(filters = 128, kernel_size = 4, strides = 2) %>% 
  layer_activation_leaky_relu() %>% 
  layer_conv_2d(filters = 128, kernel_size = 4, strides = 2) %>% 
  layer_activation_leaky_relu() %>% 
  layer_flatten() %>%
  # Warstwa odrzucania. To bardzo ważne rozwiązanie.
  layer_dropout(rate = 0.4) %>%  
  # Warstwa klasyfikacji.
  layer_dense(units = 1, activation = "sigmoid")

discriminator <- keras_model(discriminator_input, discriminator_output)
summary(discriminator)

# Optymalizator korzysta z mechanizmu ucinania wartości gradientu.
# W celu uzyskania stabilnego przebiegu procesu trenowania korzystamy z parametru rozkładu współczynnika uczenia.
discriminator_optimizer <- optimizer_rmsprop( 
  lr = 0.0008, 
  clipvalue = 1.0,
  decay = 1e-8
)

discriminator %>% compile(
  optimizer = discriminator_optimizer,
  loss = "binary_crossentropy"
)
```

## Sieć z przeciwnikiem

Teraz czas skonfigurować sieć GAN, która łączy generator z dyskryminatorem. Po wytrenowaniu model ten pchnie gene-rator w kierunku usprawniającym oszukiwanie dyskryminatora. Model ten zamienia punkty niejawnej przestrzeni w etykiety klasyfikacji: „prawdziwy” lub „sztuczny” i ma być trenowany na etykietach zawsze wskazujących prawdziwość obrazu. W związku z tym trenowanie modelu gan doprowadzi do modyfikacji wartości wag generatora tak, aby zwiększyć prawdopodobieństwo orzeczenia przez dyskryminator analizujący sztuczne obrazy tego, że są one prawdziwe. Podczas trenowania dyskryminator powinien być zamrożony (nie należy go trenować) — w czasie trenowania modelu gam wagi dyskryminatora nie będą modyfikowane. Gdyby wagi dyskryminatora były modyfikowane podczas tego procesu, to trenowalibyśmy dyskryminator tak, aby zawsze przewidywał prawdziwość obrazu, a przecież nie tego chcemy!

```{r}
# Uniemożliwia trenowanie wag dyskryminatora 
# (tylko w modelu gan).
freeze_weights(discriminator) 

gan_input <- layer_input(shape = c(latent_dim))
gan_output <- discriminator(generator(gan_input))
gan <- keras_model(gan_input, gan_output)

gan_optimizer <- optimizer_rmsprop(
  lr = 0.0004, 
  clipvalue = 1.0, 
  decay = 1e-8
)

gan %>% compile(
  optimizer = gan_optimizer, 
  loss = "binary_crossentropy"
)
```

## Trenowanie sieci DCGAN

Teraz możemy rozpocząć proces trenowania. Oto lista czynności wykonywanych podczas każdej epoki trenowania (tak właśnie powinna działać pętla trenująca model):

* 1.	Wybierz losowe punkty z niejawnej przestrzeni (losowy szum).
* 2.	Użyj generatora w celu wygenerowania obrazów zawiera-jących losowy szum.
* 3.	Połącz wygenerowane obrazy z prawdziwymi.
* 4.	Wytrenuj dyskryminator przy użyciu wylosowanych obrazów z etykietami określającymi prawdziwość obrazów.
* 5.	Wybierz kolejne losowe punkty z niejawnej przestrzeni.
* 6.	Trenuj model gan w tym celu, aby wszystkie obrazy były uznawane przez dyskryminator za prawdziwe. W tym procesie zmodyfikowane zostaną wagi generatora (podczas trenowania modelu gan wagi dyskryminatora są zamrożone), tak aby zwiększyć prawdopodobieństwo tego, że wygenerowane obrazy zostaną uznane przez dyskryminator za prawdziwe — generator jest trenowany tak, żeby był w stanie oszukać dyskryminator.


Czas zaimplementować ten mechanizm.

```{r, echo=TRUE, results='hide'}
# Ładowanie zbioru danych CIFAR10.
cifar10 <- dataset_cifar10()
c(c(x_train, y_train), c(x_test, y_test)) %<-% cifar10

# Wybór obrazów żab (klasa numer 6).
x_train <- x_train[as.integer(y_train) == 6,,,] 
# Normalizacja danych.
x_train <- x_train / 255

iterations <- 10000
batch_size <- 20
save_dir <- "gan_images"
dir.create(save_dir)

# Początek pętli trenowania.
start <- 1

for (step in 1:iterations) {
  
  # Próbkowanie losowych punktów z niejawnej przestrzeni.
  random_latent_vectors <- matrix(rnorm(batch_size * latent_dim), 
                                  nrow = batch_size, ncol = latent_dim)
  
  # Dekodowanie punktów w celu wygenerowania sztucznych obrazów.
  generated_images <- generator %>% predict(random_latent_vectors)
  
  # Łączenie obrazów sztucznych z prawdziwymi.
  stop <- start + batch_size - 1 
  real_images <- x_train[start:stop,,,]
  rows <- nrow(real_images)
  combined_images <- array(0, dim = c(rows * 2, dim(real_images)[-1]))
  combined_images[1:rows,,,] <- generated_images
  combined_images[(rows+1):(rows*2),,,] <- real_images
 
  # Tworzenie etykiet umożliwiających odróżnienie obrazów prawdziwych od sztucznych.
  labels <- rbind(matrix(1, nrow = batch_size, ncol = 1),
                  matrix(0, nrow = batch_size, ncol = 1))
  
  # Ważny zabieg: wprowadzanie losowego szumu do etykiet.
  labels <- labels + (0.5 * array(runif(prod(dim(labels))),
                                  dim = dim(labels)))
  
  # Trenowanie dyskryminatora.
  d_loss <- discriminator %>% train_on_batch(combined_images, labels) 
  
  # Losowe próbkowanie punktów w niejawnej przestrzeni.
  random_latent_vectors <- matrix(rnorm(batch_size * latent_dim), 
                                  nrow = batch_size, ncol = latent_dim)
  
  # Tworzenie fałszywych etykiet stwierdzających oryginalność wszystkich obrazów.
  misleading_targets <- array(0, dim = c(batch_size, 1))
  
  # Trenowanie generatora przy użyciu modelu gan i zamrożeniu wag dyskryminatora.
  a_loss <- gan %>% train_on_batch( 
    random_latent_vectors, 
    misleading_targets
  )  
  
  start <- start + batch_size
  if (start > (nrow(x_train) - batch_size))
    start <- 1
  
  # Okazjonalny zapis obrazów.
  if (step %% 100 == 0) { 
    
    # Zapis wag modelu.
    save_model_weights_hdf5(gan, "gan.h5")
    
    # Wyświetlanie metryk.
    cat("strata dyskryminatora w kroku :", d_loss, "\n")
    cat("strata przeciwna:", a_loss, "\n")  
    
    # Zapis jednego wygenerowanego obrazu.
    image_array_save(
      generated_images[1,,,] * 255, 
      path = file.path(save_dir, paste0("generated_frog", step, ".png"))
    )
   
    # Zapis jednego prawdziwego obrazu w celach porównawczych.
    image_array_save(
      real_images[1,,,] * 255, 
      path = file.path(save_dir, paste0("real_frog", step, ".png"))
    )
  }
}
```



