---
title: "Kodowanie słów i znaków metodą gorącej jedynki"
output: 
  html_notebook: 
    theme: cerulean
    highlight: textmate
---

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



Kodowanie metodą gorącej jedynki jest najpopularniejszym podstawowym sposobem zamieniania tokena w wektor. Z metody tej korzystaliśmy w początkowych przykładach przetwarzania zbiorów IMDB i Agencji Reutera przedstawionych w rozdziale 3. (używaliśmy jej do kodowania słów). Polega ona na przypisaniu do każdego słowa unikatowego indeksu będącego wartością całkowitoliczbową i umieszczoną w wektorze binarnym o długości n (rozmiar słownika). Wektor przyjmuje same wartości zerowe poza i-tym elementem, który przyjmuje wartość 1.

Oczywiście metoda ta może być również użyta na poziomie znaków. W celu wyjaśnienia praktycznej implementacji metody kodowania z gorącą jedynką chciałbym zaprezentować dwa przykłady: pierwszy z nich przedstawia kodowanie słów, a drugi kodowanie znaków:

Prosty przykład kodowania słów metodą gorącej jedynki:

```{r}
# Początkowa forma danych: jeden element na próbkę (w tym przykładzie próbką jest zdanie, 
# ale może ona być również całym dokumentem).
samples <- c("The cat sat on the mat.", "The dog ate my homework.")
  
# Zbuduj indeks wszystkich tokenów danych.
token_index <- list()
for (sample in samples)
  # Tokenizacja próbek poprzez metodę podziału. 
  # Podczas pracy z prawdziwymi danymi podziału dokonuje się również na znakach interpunkcyjnych i specjalnych.
  for (word in strsplit(sample, " ")[[1]])
    if (!word %in% names(token_index))
      # Przypisywanie unikatowego indeksu do każdego unikatowego słowa. 
      # Zwróć uwagę na to, że indeks 0 nie jest przypisywany do żadnego słowa.
      token_index[[word]] <- length(token_index) + 2 

# Wektoryzacja próbek. 
# Bierzemy pod uwagę tylko max_length pierwszych słów każdej próbki.
max_length <- 10

# Tu przechowujemy wyniki operacji.
results <- array(0, dim = c(length(samples), 
                            max_length, 
                            max(as.integer(token_index))))

for (i in 1:length(samples)) {
  sample <- samples[[i]]
  words <- head(strsplit(sample, " ")[[1]], n = max_length)
  for (j in 1:length(words)) {
    index <- token_index[[words[[j]]]]
    results[[i, j, index]] <- 1
  }
}
```

Prosty przykład kodowania znaków metodą gorącej jedynki:

```{r}
samples <- c("The cat sat on the mat.", "The dog ate my homework.")

ascii_tokens <- c("", sapply(as.raw(c(32:126)), rawToChar))
token_index <- c(1:(length(ascii_tokens)))
names(token_index) <- ascii_tokens

max_length <- 50

results <- array(0, dim = c(length(samples), max_length, length(token_index)))

for (i in 1:length(samples)) {
  sample <- samples[[i]]
  characters <- strsplit(sample, "")[[1]]
  for (j in 1:length(characters)) {
    character <- characters[[j]]
    results[i, j, token_index[[character]]] <- 1
  }
}
```

Pakiet Keras jest wyposażony w narzędzia przeznaczone do kodowania znaków i słów metodą gorącej jedynki (narzędzia te potrafią przetwarzać surowy tekst). W praktyce warto z nich korzystać, ponieważ wykonują wiele ważnych operacji, takich jak usuwanie znaków specjalnych i branie pod uwagę tylko n słów najczęściej występujących w zbiorze (zwykle stosuje się takie ograniczenie w celu uniknięcia pracy z bardzo dużymi przestrzeniami wektora wejściowego).

Przykład kodowania słów metodą gorącej jedynki przy użyciu gotowych narzędzi pakietu Keras:

```{r}
library(keras)

samples <- c("The cat sat on the mat.", "The dog ate my homework.")

# Tworzy mechanizm tokenizacji skonfigurowany tak, aby brał pod uwagę tylko 1000 najczęściej występujących słów.
tokenizer <- text_tokenizer(num_words = 1000) %>%
  fit_text_tokenizer(samples)

# Zamienia łańcuchy na listy indeksów (wartości całkowitoliczbowe).
sequences <- texts_to_sequences(tokenizer, samples)

# Możliwe jest również uzyskanie bezpośredniej binarnej reprezentacji kodowania metodą gorącej jedynki.
# Ten generator tokenów obsługuje także inne tryby wektoryzacji.
one_hot_results <- texts_to_matrix(tokenizer, samples, mode = "binary")

# Przykład kodu pozwalającego na uzyskanie dostępu do indeksu słów.
word_index <- tokenizer$word_index

cat("Znaleziono", length(word_index), "unikatowych tokenów.\n")
```

Odmianą kodowania metodą gorącej jedynki, która może zostać użyta, gdy liczba unikatowych tokenów w słowniku jest zbyt duża, aby obsłużyć ją w sposób jawny, jest sztuczka haszowania z gorącą jedynką (ang. one-hot hashing trick). Zamiast jawnie przypisywać indeks do każdego ze słów i utrzymywać odwołania do tych indeksów w słowniku, można haszować słowa do formy wektorów o określonym rozmiarze. Zwykle robi się to za pomocą bardzo lekkiej funkcji haszującej. Główną zaletą tej metody jest brak konieczności utrzymywania jawnego indeksu słów, co pozwala oszczędzić przestrzeń pamięci i zakodować dane w locie (możliwe jest natychmiastowe wygenerowanie wektorów tokena, bez potrzeby przyglądania się całości dostępnych danych). Wadą tego rozwiązania jest możliwość wystąpienia konfliktów haszy (ang. hash collisions), polegających na przepisaniu tego samego hasza dwóm różnym słowom (w takim przypadku żaden model uczenia maszynowego analizujący uzyskane hasze nie będzie mógł odróżnić od siebie tych słów). Prawdopodobieństwo wystąpienia konfliktów haszy maleje, gdy przestrzeń haszowania jest o wiele większa od całkowitej liczby unikatowych haszowanych tokenów.

Prosty przykład sztuczki haszowania słów metodą gorącej jedynki:

```{r}
library(hashFunction)

samples <- c("The cat sat on the mat.", "The dog ate my homework.")

# Słowa są zapisywane w postaci wektorów o długości 1000. 
# Jeżeli przetworzymy przykład, w którym znajduje się około 1000 różnych słów, to zauważymy wiele konfliktów 
# haszy, które doprowadzą do pogorszenia dokładności tej metody kodowania.

dimensionality <- 1000
max_length <- 10

results <- array(0, dim = c(length(samples), max_length, dimensionality))

for (i in 1:length(samples)) {
  sample <- samples[[i]]
  words <- head(strsplit(sample, " ")[[1]], n = max_length)
  for (j in 1:length(words)) {
    # Słowom przypisywane są losowe wartości całkowite indeksu z zakresu od 0 do 1000.
    index <- abs(spooky.32(words[[i]])) %% dimensionality
    results[[i, j, index]] <- 1
  }
}
```


