---
title: "Generowanie tekstu za pomocą sieci LSTM"
output: 
  html_notebook: 
    theme: cerulean
    highlight: textmate
---

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


## Implementacja algorytmu LSTM generującego tekst na poziomie liter


Czas skorzystać z pakietu Keras i zastosować teorię w praktyce. Na początek będziemy potrzebować dużo danych tekstowych do trenowania modelu języka. Możemy skorzystać z dowolnego wystarczająco rozbudowanego zestawu plików tekstowych — Wikipedii, Władcy pierścieni itd. W zaprezentowanym przykładzie posłużymy się wybranymi dziełami Friedricha Nietzschego — niemieckiego filozofa żyjącego w XIX w. — przetłumaczonymi na język angielski. W związku z tym wytrenujemy model odwzorowujący specyficzny styl pisania Nietzschego. Ponadto model ten będzie generował teksty tylko na wybrane tematy — nie będzie to ogólny model języka angielskiego.

## Przygotowywanie danych

Zacznijmy od pobrania korpusu i zapisania go przy użyciu tyl-ko małych liter.

```{r}
library(keras)
library(stringr)

path <- get_file(
  "nietzsche.txt",
  origin = "https://s3.amazonaws.com/text-datasets/nietzsche.txt"
)
text <- tolower(readChar(path, file.info(path)$size))
cat("Długość korpusu:", nchar(text), "\n")
```

Teraz dokonamy wyodrębnienia częściowo zachodzących na siebie sekwencji o długości maxlen, zakodujemy je techniką kodowania z gorącą jedynką, a następnie umieścimy je w trójwymiarowej tablicy o kształcie x (sequences, maxlen, unique_characters). Jednocześnie przygotujemy tablicę y zawierającą „wartości do-celowe”, które w tym przypadku są po prostu literami umieszczanymi po każdej z wyodrębnionych sekwencji. Wartości te zostaną zapisane przy użyciu techniki kodowania z gorącą jedynką.


```{r}
maxlen <- 60  # Wyodrębniamy sekwencje składające się z 60 znaków.

step <- 3  # Nowa sekwencja jest próbkowana co 3 znaki.
  
text_indexes <- seq(1, nchar(text) - maxlen, by = step)

# Zmienna, w której zapisywane będą wyodrębnione sekwencje.
sentences <- str_sub(text, text_indexes, text_indexes + maxlen - 1)

# Zmienna, w której zapisywane będą kolejne znaki (cele).
next_chars <- str_sub(text, text_indexes + maxlen, text_indexes + maxlen)

cat("Liczba sekwencji: ", length(sentences), "\n")

# Lista unikatowych znaków wchodzących w skład korpusu.
chars <- unique(sort(strsplit(text, "")[[1]]))
cat("Liczba unikatowych znaków:", length(chars), "\n")

# Słownik przypisujący unikatowe znaki do ich indeksów.
char_indices <- 1:length(chars) 
names(char_indices) <- chars

# Znaki są zapisywane w formie tablic binarnych przy użyciu kodowania z gorącą jedynką.
cat("Tworzenie wektorów...\n") 
x <- array(0L, dim = c(length(sentences), maxlen, length(chars)))
y <- array(0L, dim = c(length(sentences), length(chars)))
for (i in 1:length(sentences)) {
  sentence <- strsplit(sentences[[i]], "")[[1]]
  for (t in 1:length(sentence)) {
    char <- sentence[[t]]
    x[i, t, char_indices[[char]]] <- 1
  }
  next_char <- next_chars[[i]]
  y[i, char_indices[[next_char]]] <- 1
}
```

## BUDOWANIE SIECI

Sieć składa się z pojedynczej warstwy layer_lstm i klasyfikatora dense z funkcją aktywacji softmax. Pamiętajmy o tym, że generowanie danych sekwencyjnych nie musi być przeprowadzane przy użyciu rekurencyjnych sieci neuronowych. Ostatnio coraz częściej stosuje się w tym celu jednowymiarowe sieci konwolucyjne.


```{r}
model <- keras_model_sequential() %>% 
  layer_lstm(units = 128, input_shape = c(maxlen, length(chars))) %>% 
  layer_dense(units = length(chars), activation = "softmax")
```

Wartości docelowe (znaki) są zakodowane przy użyciu techniki gorącej jedynki, a więc funkcją straty trenowanego modelu będzie categorical_crossentropy.

```{r}
optimizer <- optimizer_rmsprop(lr = 0.01)

model %>% compile(
  loss = "categorical_crossentropy", 
  optimizer = optimizer
)   
```

## TRENOWANIE MODELU JĘZYKA I PRÓBKOWANIE Z NIEGO


Dysponując wytrenowanym modelem i kawałkiem początkowego tekstu, możemy wygenerować nowy tekst. W tym celu należy powta-rzać następujące operacje:

* 1.	Użyj modelu w celu wygenerowania rozkładu prawdopodobieństwa następnego znaku kontynuującego obecny tekst.
* 2.	Zmodyfikuj rozkład, korzystając z określonej wartości parametru temperature.
* 3.	Przeprowadź operację losowego próbkowania następnego znaku na podstawie zmodyfikowanego rozkładu.
* 4.	Dodaj nowy znak na końcu obecnego tekstu.

Oto kod używany do zmiany wag rozkładu prawdopodobieństwa wygenerowanego przez model. Kod ten tworzy funkcję próbkującą, która również określa indeks znaku.


```{r}
sample_next_char <- function(preds, temperature = 1.0) {
  preds <- as.numeric(preds)
  preds <- log(preds) / temperature
  exp_preds <- exp(preds)
  preds <- exp_preds / sum(exp_preds)
  which.max(t(rmultinom(1, 1, preds)))
}
```

Na koniec poniższa pętla wykonuje operację trenowania modelu i generowania tekstu. Wygenerujemy teksty przy różnych wartościach parametru temperature (wartości te będą zmieniane przy rozpoczęciu kolejnych epok procesu trenowania). Pozwoli to nam zobaczyć, jak zmienia się tekst wraz z udoskonalaniem modelu, a także to, jak parametr temperature wpływa na strategię próbkowania.

```{r}
for (epoch in 1:60) {
  
  cat("epoch", epoch, "\n")
  
  # Model będzie trenowany przez 60 epok.
  model %>% fit(x, y, batch_size = 128, epochs = 1) 
  
  # Losowanie tekstu początkowego.
  start_index <- sample(1:(nchar(text) - maxlen - 1), 1)  
  seed_text <- str_sub(text, start_index, start_index + maxlen - 1)
  
  cat("--- Generowanie przy użyciu tekstu początkowego:", seed_text, "\n\n")
  
  for (temperature in c(0.2, 0.5, 1.0, 1.2)) {
    
    cat("------ Wartość parametru temperature:", temperature, "\n")
    cat(seed_text, "\n")
    
    generated_text <- seed_text
    
     # Generowanie 400 znaków (proces rozpoczyna się od wylosowanego tekstu początkowego).
    for (i in 1:400) {
      
      sampled <- array(0, dim = c(1, maxlen, length(chars)))
      generated_chars <- strsplit(generated_text, "")[[1]]
      for (t in 1:length(generated_chars)) {
        char <- generated_chars[[t]]
        sampled[1, t, char_indices[[char]]] <- 1
      }
        
      preds <- model %>% predict(sampled, verbose = 0)
      next_index <- sample_next_char(preds[1,], temperature)
      next_char <- chars[[next_index]]
      
      generated_text <- paste0(generated_text, next_char)
      generated_text <- substring(generated_text, 2)
      
      cat(next_char)
    }
    cat("\n\n")
  }
}
```

Oto tekst wygenerowany przy użyciu wylosowanej frazy new faculty, and the jubilation reached its climax when kant. Został on uzyskany po 20 epokach trenowania algorytmu — na długo przed osiągnięciem końca procesu trenowania — przy parametrze temperature przyjmującym wartość równą 0,2:

```
new faculty, and the jubilation reached its climax when kant and such a man
in the same time the spirit of the surely and the such the such 
as a man is the sunligh and subject the present to the superiority of the 
special pain the most man and strange the subjection of the 
special conscience the special and nature and such men the subjection of the
special men, the most surely the subjection of the special 
intellect of the subjection of the same things and
```

Oto wynik przy parametrze temperature przyjmującym wartość równą 0,5:
```
new faculty, and the jubilation reached its climax when kant in the eterned 
and such man as it's also become himself the condition of the 
experience of off the basis the superiory and the special morty of the 
strength, in the langus, as which the same time life and "even who 
discless the mankind, with a subject and fact all you have to be the stand
and lave no comes a troveration of the man and surely the 
conscience the superiority, and when one must be w
```

A to wynik przy parametrze temperature przyjmującym wartość 1,0:

```
new faculty, and the jubilation reached its climax when kant, as a 
periliting of manner to all definites and transpects it it so 
hicable and ont him artiar resull
too such as if ever the proping to makes as cnecience. to been juden, 
all every could coldiciousnike hother aw passife, the plies like 
which might thiod was account, indifferent germin, that everythery 
certain destrution, intellect into the deteriorablen origin of moralian, 
and a lessority o
```

Po 60 epokach proces trenowania można uznać za praktycznie zakończony — tekst generowany przez model zaczyna wyglądać na coraz bardziej składny. Oto tekst wygenerowany przy parametrze temperature przyjmującym wartość 0,2:

```
cheerfulness, friendliness and kindness of a heart are the sense of the 
spirit is a man with the sense of the sense of the world of the 
self-end and self-concerning the subjection of the strengthorixes--the 
subjection of the subjection of the subjection of the 
self-concerning the feelings in the superiority in the subjection of the 
subjection of the spirit isn't to be a man of the sense of the 
subjection and said to the strength of the sense of the
```

A to tekst wygenerowany przy parametrze temperature przyjmującym wartość 0,5:

```
cheerfulness, friendliness and kindness of a heart are the part of the soul
who have been the art of the philosophers, and which the one 
won't say, which is it the higher the and with religion of the frences. 
the life of the spirit among the most continuess of the 
strengther of the sense the conscience of men of precisely before enough 
presumption, and can mankind, and something the conceptions, the 
subjection of the sense and suffering and the
```

Przy parametrze temperature przyjmującym wartość 1,0 wygenerowany został następujący tekst:

```
cheerfulness, friendliness and kindness of a heart are spiritual by the 
ciuture for the
entalled is, he astraged, or errors to our you idstood--and it needs, 
to think by spars to whole the amvives of the newoatly, prefectly 
raals! it was
name, for example but voludd atu-especity"--or rank onee, or even all 
"solett increessic of the world and
implussional tragedy experience, transf, or insiderar,--must hast
if desires of the strubction is be stronges
```

Jak widać, niska wartość parametru temperature prowadzi do uzyskania tekstu, który charakteryzuje się dużą przewidywalnością i powtarzalnością, ale jego lokalna struktura jest bardzo realistyczna — wszystkie wygenerowane słowa (słowo jest lokalnym wzorcem składającym się ze znaków) występują w języku angielskim. Przy wyższych wartościach parametru tempe-rature wygenerowany tekst staje się bardziej interesujący, zaskakujący, a nawet kreatywny — algorytm czasami wymyśla nawet nowe słowa, które brzmią tak, jakby były naprawdę istniejący-mi słowami (są to np. eterned i troveration), ale lokalna struktura tekstu zaczyna się załamywać i większość słów wy-gląda tak, jakby była prawie losowym zbiorem znaków. Bez wątpienia najciekawsze efekty w przypadku tego generowania tek-stu uzyskuje się przy parametrze temperature równym 0,5. Zawsze warto eksperymentować z różnymi strategiami próbkowania! Dobra równowaga między wytrenowaną strukturą a losowością sprawi, że wygenerowany tekst będzie interesujący.

Trenując model dłużej, tworząc większy model i stosując większy zbiór danych, można generować próbki, które wyglądają o wiele składniej i bardziej realistycznie. Oczywiście nie należy oczekiwać od modelu wygenerowania tekstu, który będzie miał jakiś większy sens — mechanizm generujący tekst tylko próbkuje litery z modelu statystycznego określającego ich kolejność. Język jest kanałem komunikacji, a rozmowy dotyczące różnych tematów charakteryzują się inną strukturą statystyczną. Tezę tę można udowodnić, odpowiadając sobie na pytanie: co, jeżeli język ludzki zostałby skompresowany tak, jak kompresowana jest większość cyfrowej komunikacji między komputerami? Wówczas język przenosiłby tyle samo informacji, ale nie charakteryzowałby się żadną ukrytą strukturą statystyczną, co uniemożliwiłoby wytrenowanie modelu języka w sposób, w jaki zrobiliśmy to przed chwilą.


## Wnioski

*	Dyskretna sekwencja danych może zostać wygenerowana p-przez trenowanie modelu pod kątem przewidywania kolejnych elementów tekstu na podstawie wcześniejszego ciągu znaków.
*	Model trenowany na zbiorze danych tekstowych określany jest mianem modelu języka. Może on być oparty na słowach lub literach.
*	Próbkowanie zbioru elementów tekstu wymaga kompromisu między bezkrytycznym przyjmowaniem przewidywań modelu a losowością.
*	Można to zrobić przy użyciu parametru temperature funkcji softmax. Wybór właściwej wartości tego parametru powinien zostać dokonany na drodze eksperymentów.


