---
title: "Konwolucyjne sieci neuronowe i przetwarzanie sekwencji"
output: 
  html_notebook: 
    theme: cerulean
    highlight: textmate
---

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



## Implementing a 1D convnet
  
W pakiecie Keras jednowymiarowe sieci konwolucyjne zostały zaimplementowane w postaci warstwy layer_conv_1d, której interfejs działa podobnie do interfejsu warstwy layer_conv_2d. Na wejściu warstwa ta przyjmuje trójwymiarowe tensory o kształcie (próbki, czas, cechy) i zwraca trójwymiarowe tensory o takim samym kształcie. Okno konwolucyjne jest jednowymiarowym oknem poruszającym się wzdłuż osi czasu (drugiej osi tensora wejściowego).

Zbudujmy prostą jednowymiarową sieć konwolucyjną i spróbujmy użyć jej do analizy sentymentu recenzji wchodzących w skład zbioru IMDB. Przypominam kod importujący i przetwarzający wstępnie dane tego zbioru:


```{r}
library(keras)

max_features <- 10000
max_len <- 500

cat("Ładowanie danych...\n")
imdb <- dataset_imdb(num_words = max_features)
c(c(x_train, y_train), c(x_test, y_test)) %<-% imdb 
cat(length(x_train), "sekwencje treningowe\n")
cat(length(x_test), "sekwencje testowe")

cat("Sekwencje (próbki x czas)\n")
x_train <- pad_sequences(x_train, maxlen = max_len)
x_test <- pad_sequences(x_test, maxlen = max_len)
cat("Kształt obiektu x_train:", dim(x_train), "\n")
cat("Kształt obiektu x_test:", dim(x_test), "\n")
```

Jednowymiarowe sieci konwolucyjne mają taką samą strukturę jak ich dwuwymiarowe odpowiedniki, z których korzystaliśmy w rozdziale 5.: składają się ze stosu warstw layer_conv_1d i layer_max_pooling_1d, a na ich końcu znajduje się globalna warstwa łącząca lub warstwa spłaszczająca (layer_flatten) zamieniająca trójwymiarowe obiekty wyjściowe w obiekty dwuwymiarowe, co pozwala na dodanie do modelu klasyfikacji lub regresji przynajmniej jednej warstwy dense.

Jedną z różnic między tymi typami sieci jest to, że w przypadku jednowymiarowych sieci konwolucyjnych możemy pozwolić sobie na użycie większych okien konwolucji. W przypadku dwuwymiarowej warstwy konwolucji okno konwolucji o wymiarach 33 zawiera 3  3 = 9 wektorów cech, a w przypadku warstwy jednowymiarowej konwolucji okno konwolucji o rozmiarze równym 3 składa się tylko z 3 wektorów cech. W związku z tym możemy swobodnie pozwolić sobie na korzystanie z jednowymiarowych okien konwolucji o rozmiarze równym 7 lub 9.

Oto przykładowa jednowymiarowa sieć konwolucyjna przetwarzająca zbiór danych IMDB.


```{r}
model <- keras_model_sequential() %>% 
  layer_embedding(input_dim = max_features, output_dim = 128,
                  input_length = max_len) %>% 
  layer_conv_1d(filters = 32, kernel_size = 7, activation = "relu") %>% 
  layer_max_pooling_1d(pool_size = 5) %>% 
  layer_conv_1d(filters = 32, kernel_size = 7, activation = "relu") %>% 
  layer_global_max_pooling_1d() %>% 
  layer_dense(units = 1)

summary(model)
```

```{r, echo=FALSE, results='hide'}

model %>% compile(
  optimizer = optimizer_rmsprop(lr = 1e-4),
  loss = "binary_crossentropy",
  metrics = c("acc")
)

history <- model %>% fit(
  x_train, y_train,
  epochs = 10,
  batch_size = 128,
  validation_split = 0.2
)
```

Oto wyniki procesu trenowania i walidacji modelu. Dokładność walidacji jest nieco mniejsza od tej, którą uzyskał model sieci layer_lstm, ale trenowanie bieżącego modelu trwało znacznie szybciej (dotyczy to pracy na układzie CPU, a także układzie GPU). Oczywiście dokładny przyrost prędkości wykonywania obliczeń zależy od konfiguracji sprzętowej. Możemy zapisać ten model, ograniczając czas jego wykonywania do ośmiu epok, a następnie sprawdzić jego działanie na zbiorze testowym. To z pewnością przekonujący dowód na to, że jednowymiarowa sieć konwolucyjna jest szybszą i tańszą alternatywą stosowania sieci rekurencyjnej w przypadku problemu analizy sentymentu na poziomie słów.

```{r}
plot(history)
```

## Łączenie sieci konwolucyjnych i rekurencyjnych w celu przetworzenia długich sekwencji


Jednowymiarowe sieci konwolucyjne przetwarzają sekwencje wejściowe w sposób niezależny. W przeciwieństwie do sieci rekurencyjnych nie są wrażliwe na kolejność obserwacji (poza skalą lokalną określaną przez rozmiar okna konwolucji). Oczywiście w celu rozpoznania bardziej rozciągniętych prawidłowości możemy utworzyć stos wielu warstw konwolucyjnych i warstw łączących. W ten sposób górne warstwy będą analizować dłuższe fragmenty danych wejściowych, ale jest to słaby sposób na wprowadzenie wrażliwości modelu na kolejność obserwacji. Sprawdźmy w praktyce tę słabość modelu podczas prognozowania temperatury (w tym problemie kolejność jest bardzo ważna dla otrzymania prawidłowych prognoz). W poniższym przykładzie korzystamy ponownie ze zdefiniowanych wcześniej zmiennych float_data, train_gen, val_gen i val_steps:

```{r}
library(tibble)
library(readr)

data_dir <- "~/Downloads/jena_climate"
fname <- file.path(data_dir, "jena_climate_2009_2016.csv")
data <- read_csv(fname)

data <- data.matrix(data[,-1])

train_data <- data[1:200000,]
mean <- apply(train_data, 2, mean)
std <- apply(train_data, 2, sd)
data <- scale(data, center = mean, scale = std)

generator <- function(data, lookback, delay, min_index, max_index,
                      shuffle = FALSE, batch_size = 128, step = 6) {
  if (is.null(max_index))
    max_index <- nrow(data) - delay - 1
  i <- min_index + lookback
  function() {
    if (shuffle) {
      rows <- sample(c((min_index+lookback):max_index), size = batch_size)
    } else {
      if (i + batch_size >= max_index)
        i <<- min_index + lookback
      rows <- c(i:min(i+batch_size, max_index))
      i <<- i + length(rows)
    }
    
    samples <- array(0, dim = c(length(rows), 
                                lookback / step,
                                dim(data)[[-1]]))
    targets <- array(0, dim = c(length(rows)))
                     
    for (j in 1:length(rows)) {
      indices <- seq(rows[[j]] - lookback, rows[[j]], 
                     length.out = dim(samples)[[2]])
      samples[j,,] <- data[indices,]
      targets[[j]] <- data[rows[[j]] + delay,2]
    }            
    
    list(samples, targets)
  }
}

lookback <- 1440
step <- 6
delay <- 144
batch_size <- 128

train_gen <- generator(
  data,
  lookback = lookback,
  delay = delay,
  min_index = 1,
  max_index = 200000,
  shuffle = TRUE,
  step = step, 
  batch_size = batch_size
)

val_gen = generator(
  data,
  lookback = lookback,
  delay = delay,
  min_index = 200001,
  max_index = 300000,
  step = step,
  batch_size = batch_size
)

test_gen <- generator(
  data,
  lookback = lookback,
  delay = delay,
  min_index = 300001,
  max_index = NULL,
  step = step,
  batch_size = batch_size
)

# Liczba kroków pobierania danych z obiektu val_gen zapewniających 
# przetworzenie całego walidacyjnego zbioru danych.
val_steps <- (300000 - 200001 - lookback) / batch_size

  # Liczba kroków pobierania danych z obiektu test_gen, 
  # przy której przetworzony zostanie cały testowy zbiór danych.
test_steps <- (nrow(data) - 300001 - lookback) / batch_size
```

```{r, echo=TRUE, results='hide'}
model <- keras_model_sequential() %>% 
  layer_conv_1d(filters = 32, kernel_size = 5, activation = "relu",
                input_shape = list(NULL, dim(data)[[-1]])) %>% 
  layer_max_pooling_1d(pool_size = 3) %>% 
  layer_conv_1d(filters = 32, kernel_size = 5, activation = "relu") %>% 
  layer_max_pooling_1d(pool_size = 3) %>% 
  layer_conv_1d(filters = 32, kernel_size = 5, activation = "relu") %>% 
  layer_global_max_pooling_1d() %>% 
  layer_dense(units = 1)


model %>% compile(
  optimizer = optimizer_rmsprop(),
  loss = "mae"
)

history <- model %>% fit_generator(
  train_gen,
  steps_per_epoch = 500,
  epochs = 20,
  validation_data = val_gen,
  validation_steps = val_steps
)
```

Oto wykres średniego bezwzględnego błędu trenowania i walidacji.

```{r}
plot(history)
```

Średni bezwzględny błąd walidacji utrzymuje się na poziomie około 40%, a więc model ten nie jest w stanie pokonać nawet zdefiniowanego wcześniej punktu odniesienia. Dzieje się tak, ponieważ sieć konwolucyjna poszukuje zależności w całych danych szeregu czasowego bez wiedzy o tym, w jakim czasie zaobserwowano przetwarzane wartości (nie zwraca uwagi na to, czy są one zbliżone bardziej do początku, czy do końca analizowanego zbioru). W tym problemie nowsze dane powinny być interpretowane w inny sposób od danych z dalszej przeszłości, a więc sieć konwolucyjna nie jest w stanie wygenerować sensownych wyników. To ograniczenie sieci konwolucyjnych nie jest problematyczne w przypadku zbioru IMDB, ponieważ słowa świadczące o tym, że dana recenzja jest pochlebna lub negatywna, mogą znajdować się w dowolnym miejscu analizowanych zdań.

Sposobem na połączenie szybkości i lekkości sieci konwolucyjnych z braniem pod uwagę kolejności przez sieci rekurencyjne jest zastosowanie jednowymiarowej sieci konwolucyjnej w roli mechanizmu przetwarzającego wstępnie dane kierowane do sieci rekurencyjnej (patrz rysunek 6.25). Rozwiązanie takie sprawdza się szczególnie dobrze podczas pracy z długimi sekwencjami, których długość uniemożliwia ich sensowne przetworzenie za pomocą samych sieci rekurencyjnych (piszę tu o sekwencjach składających się nawet z tysięcy kroków). Sieć konwolucyjna zamieni długą sekwencję wejściową w krótsze sekwencje cech wyższego poziomu, które można następnie skierować do warstwy rekurencyjnej.


Technika ta jest na tyle mało znana, że rzadko spotyka się ją w artykułach naukowych i praktycznych zastosowaniach. Jest jednak na tyle skuteczna, że powinna być bardziej popularna. Spróbujmy użyć jej w celu rozwiązania problemu prognozowania temperatury. Rozwiązanie to pozwala na przetwarzanie o wiele dłuższych sekwencji, a więc możemy przyglądać się danym z dalszej przeszłości poprzez zwiększenie wartości parametru lookback generatora danych lub przyglądać się szeregom czasowym o wyższej rozdzielczości poprzez zmniejszenie wartości parametru step generatora danych. W zaprezentowanym przykładzie zdecydowałem się na zmniejszenie o połowę parametru step, co wydłużyło dwukrotnie analizowane szeregi czasowe (pracujemy z próbkami temperatury odczytywanej w odstępie 30 minut). Ponownie korzystam ze zdefiniowanej wcześniej funkcji generator.


```{r}
# Wcześniej parametr ten przyjmował wartość równą 6 (1 obserwacja na godzinę). 
# obserwacja na 30 minut).
step <- 3 
lookback <- 720  # Bez zmian.
delay <- 144  # Bez zmian.
  
train_gen <- generator(
  data,
  lookback = lookback,
  delay = delay,
  min_index = 1,
  max_index = 200000,
  shuffle = TRUE,
  step = step
)

val_gen <- generator(
  data,
  lookback = lookback,
  delay = delay,
  min_index = 200001,
  max_index = 300000,
  step = step
)

test_gen <- generator(
  data,
  lookback = lookback,
  delay = delay,
  min_index = 300001,
  max_index = NULL,
  step = step
)

val_steps <- (300000 - 200001 - lookback) / 128
test_steps <- (nrow(data) - 300001 - lookback) / 128
```

Teraz możemy stworzyć model. Połączymy dwie warstwy layer_conv_1d z warstwą layer_gru.

```{r}
model <- keras_model_sequential() %>% 
  layer_conv_1d(filters = 32, kernel_size = 5, activation = "relu",
                input_shape = list(NULL, dim(data)[[-1]])) %>% 
  layer_max_pooling_1d(pool_size = 3) %>% 
  layer_conv_1d(filters = 32, kernel_size = 5, activation = "relu") %>% 
  layer_gru(units = 32, dropout = 0.1, recurrent_dropout = 0.5) %>% 
  layer_dense(units = 1)

summary(model)
```

```{r, echo=TRUE, results='hide'}
model %>% compile(
  optimizer = optimizer_rmsprop(),
  loss = "mae"
)

history <- model %>% fit_generator(
  train_gen,
  steps_per_epoch = 500,
  epochs = 20,
  validation_data = val_gen,
  validation_steps = val_steps
)
```

```{r}
plot(history)
```

Przyglądając się wykresowi straty walidacyjnej, możemy stwierdzić, że taka konfiguracja modelu jest słabsza od samej warstwy layer_gru poddanej regularyzacji, ale działa znacznie szybciej. Analizuje dwa razy więcej danych, co w tym przypadku nie jest zabiegiem szczególnie przydatnym, ale w innych zbiorach danych może przynieść znaczną poprawę pracy algorytmu.

## Wnioski

Oto wnioski, które należy wynieść z tego podrozdziału:

*	Jednowymiarowe konwolucyjne sieci neuronowe doskonale nadają się do przetwarzania danych szeregów czasowych, tak jak dwuwymiarowe sieci konwolucyjne doskonale nadają się do przetwarzania wzorców zakodowanych w płaszczyznach dwuwymiarowych obrazów. W przypadku niektórych problemów sieci te stanowią szybszą alternatywę sieci rekurencyjnych. Dotyczy to szczególnie zadań związanych z przetwarzaniem języka naturalnego.
*	Zwykle jednowymiarowe sieci konwolucyjne mają strukturę przypominającą strukturę ich dwuwymiarowych odpowiedników stosowanych do przetwarzania obrazu: składają się ze stosu warstw layer_conv_1d i layer_max_pooling_1d (na ich końcu stosowane są warstwy wykonujące operacje globalnego sumowania lub spłaszczania).
*	Używanie rekurencyjnych sieci neuronowych do przetwarzania długich sekwencji wiąże się z dużym wydatkiem obliczeniowym, a jednowymiarowe sieci konwolucyjne wymagają o wiele prostszych obliczeń. W związku z tym warto używać jednowymiarowych sieci konwolucyjnych w roli mechanizmów wstępnie przetwarzających dane przed skierowaniem ich do warstw sieci rekurencyjnej. Takie rozwiązanie skraca analizowaną sekwencję, a warstwy rekurencyjne zajmują się tylko przydatnymi reprezentacjami danych.
