---
title: "Osadzanie słów"
output: 
  html_notebook: 
    theme: cerulean
    highlight: textmate
---

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




Kolejnym popularnym sposobem łączenia wektora ze słowem jest zastosowanie gęstych wektorów słów, określanych również mianem osadzeń słów (ang. word embeddings). Wektory uzyskane przy użyciu techniki kodowania z gorącą jedynką są binarne, rzadkie (zawierają głównie zera) i wysokowymiarowe (liczba ich wymiarów jest równa liczbie słów wchodzących w skład słownika). Osadzenia słów charakteryzują się niską liczbą wymiarów. Są to gęste wektory zmiennoprzecinkowe (patrz rysunek 6.2). Osadzenia słów, w przeciwieństwie do wektorów uzyskanych za pomocą techniki kodowania z gorącą jedynką, są uczone z danych. Osadzenia słów mają zwykle 256, 512 wymiarów, a w przypadku pracy z bardzo dużymi słownikami nawet 1024 wymiary. Praca z wektorami słów zakodowanymi metodą gorącej jedynki zwykle prowadzi do wygenerowania wektorów mających 20 000 lub więcej wymiarów (wektor mający 20 000 wymiarów może być użyty do zakodowania 20 000 tokenów). Jak widać, osadzenia słów pozwalają zakodować więcej informacji w mniejszej liczbie wymiarów.

![word embeddings vs. one hot encoding](img\6_1_2.png)

Osadzenia słów można tworzyć na dwa sposoby:

*	Mogą one być uczone wraz z głównym zadaniem (np. klasyfikacją dokumentu lub przewidywaniem sentymentu). W takim przypadku pracę zaczyna się od losowych wektorów słów, a następnie zawartość tych wektorów jest uczona w taki sam sposób jak wagi sieci neuronowej.
*	Do modelu można załadować osadzenia słów utworzone podczas pracy nad innym problemem uczenia maszynowego. Takie osadzenia określamy mianem uprzednio wytrenowanych osadzeń słów.

Przyjrzyjmy się obu rozwiązaniom.


## Uczenie osadzeń słów przy użyciu warstwy osadzającej


Najprostszym sposobem powiązania gęstego wektora ze słowem jest wybranie losowego wektora. Problemem z takim rozwiązanie jest to, że wynikowa przestrzeń osadzeń nie ma struktury — np. słowa piękny i śliczny mogą skończyć w zupełnie różnych osadzeniach pomimo tego, że w większości zdań są to synonimy. Sieć neuronowa może mieć problem z uporządkowaniem tak zaszumianej i pozbawionej struktury przestrzeni osadzeń.

Przejdźmy na poziom pewnej abstrakcji. Geometryczna zależność między wektorami słów powinna odzwierciedlać semantyczną zależność między tymi słowami. Osadzenia słów mają odzwierciedlać język w przestrzeni geometrycznej. W poprawnej przestrzeni osadzeń synonimy powinny być osadzone w podobnych wektorach słów. Ogólnie rzecz biorąc, odległość geometryczna (np. odległość L2) między dowolnymi dwoma słowami powinna odzwierciedlać różnicę semantyczną między nimi. W przestrzeni osadzeń nie tylko odległość ma znaczenie — również kierunki powinny mieć określone znaczenie. Wyjaśnijmy to na konkretnym przykładzie.

W rzeczywistości przestrzenie osadzania pozwalają na wykonanie transformacji zmieniających płeć lub uzyskanie liczby mnogiej. Dodając wektor „rodzaju żeńskiego” do wektora „król”, uzyskamy wektor „królowa”. Dodając do wektora „król” wektor „liczba mnoga”, uzyskamy wektor „królowie”. Przestrzenie osadzeń słów zwykle zawierają tysiące wektorów, które można zinterpretować i z których można potencjalnie skorzystać.

Czy istnieje doskonała przestrzeń osadzania słów, która idealnie oddałaby zależności między słowami używanymi przez ludzi? Być może, ale nikt jej jeszcze nie uzyskał. Nie ma czegoś takiego jak język ludzki — ludzie posługują się wieloma różnymi językami, które nie są izometryczne (język stanowi odzwierciedlenie kultury i określonego kontekstu). W praktyce to, czy dana przestrzeń osadzeń jest dobra, zależy głównie od problemu, który chcemy rozwiązać. Idealna przestrzeń osadzeń słów języka angielskiego modelu analizy sentymentu recenzji filmów wygląda inaczej od idealnej przestrzeni osadzeń słów języka angielskiego modelu klasyfikującego dokumenty prawne. Wynika to z tego, że w każdym z tych problemów ważne są inne zależności semantyczne.


W związku z tym warto trenować nową przestrzeń osadzeń przy okazji rozwiązywania nowego problemu. Na szczęście proces ten ułatwia algorytm propagacji wstecznej oraz pakiet Keras. Wszystko sprowadza się do wyuczenia wartości wag warstwy osadzania layer_embedding.

```{r}
library(keras)

# Warstwa embedding przyjmuje przynajmniej dwa argumenty:
# liczbę tokenów (tutaj 1000: 1 + maksymalny indeks słowa) i liczbę wymiarów osadzeń (tutaj 64).
embedding_layer <- layer_embedding(input_dim = 1000, output_dim = 64) 
```

Warstwę embedding najlepiej jest rozumieć jako słownik mapujący całkowitoliczbowe indeksy oznaczające określone słowa na gęste wektory. Przyjmuje ona na wejściu wartości całkowitoliczbowe i wyszukuje je w wewnętrznym słowniku, zwracając związane z nimi wektory. Na rysunku 6.4 przedstawiono schemat operacji wyszukiwania w słowniku.

Warstwa embedding przyjmuje na wejściu dwuwymiarowy tensor o kształcie (próbki, długość_sekwencji). Każdy element tego tensora jest sekwencją liczb całkowitych. Warstwa ta może osadzać sekwencje o zmiennej długości: do zaprezentowanej w poprzednim przykładzie warstwy Embedding możliwe jest kierowanie wsadów o kształcie (32, 10) (32 sekwencje o długości równej 10) lub wsadów o kształcie (64, 15) (wsad 64 sekwencji o długości 15). Wszystkie sekwencje wchodzące w skład wsadu muszą mieć tę samą długość (wynika to z faktu umieszczania ich w tym samym tensorze), a więc sekwencje, które są krótsze od innych, należy dopełnić zerami, a sekwencje, które są dłuższe, należy uciąć.

Warstwa ta zwraca trójwymiarowy tensor zmiennoprzecinkowy o kształcie (próbki, długość_sekwencji, liczba_wymiarów_osadzenia). Taki trójwymiarowy tensor może zostać przetworzony przez warstwę RNN lub jednowymiarową warstwę konwolucyjną (oba rozwiązania opiszę w kolejnych sekcjach)
.
Podczas tworzenia instancji warstwy embedding jej wagi (wewnętrzny słownik wektorów tokenów) są inicjowane losowo (tak jak w przypadku każdej innej warstwy). Podczas trenowania wektory słów są stopniowo dostrajane przy użyciu algorytmu propagacji wstecznej, co prowadzi do uzyskania przestrzeni, z której może korzystać model. Po pełnym wytrenowaniu przestrzeń osadzeń będzie miała charakter struktury wyspecjalizowanej pod kątem rozwiązywania problemu, do którego trenowany jest model.

Zastosujmy to rozwiązanie w modelu przewidującym sentyment recenzji filmu wchodzącej w skład zbioru IMDB (z zadaniem tym próbowaliśmy się zmierzyć już wcześniej). Zacznijmy od szybkiego przygotowania danych. Ograniczymy zawartość recenzji do 10 000 najczęściej pojawiających się w nich słów (zabieg taki stosowaliśmy również podczas pierwszego podejścia do tego problemu), a następnie utniemy recenzje po zaledwie 20 słowach. Sieć będzie uczyć się ośmiowymiarowych osadzeń każdego z 10 000 słów, zamieni wejściową sekwencję wartości całkowitoliczbowych (dwuwymiarowy tensor całkowitoliczbowy) na sekwencje osadzone (trójwymiarowy tensor zmiennoprzecinkowy), spłaszczy ten tensor do dwóch wymiarów i wytrenuje pojedynczą warstwę Dense znajdującą się na końcu klasyfikatora.


```{r}
# Liczba słów analizowanych w charakterze wag.
max_features <- 10000
# Ucina recenzje, w których występuje ta liczba słów 
# (słów zaliczanych do zbioru max_features najczęściej występujących słów).
maxlen <- 20

# Ładuje dane w formie list wartości całkowitoliczbowych.
imdb <- dataset_imdb(num_words = max_features)
c(c(x_train, y_train), c(x_test, y_test)) %<-% imdb

# Zamieniamy listy liczb całkowitych na dwuwymiarowy tensor wartości całkowitoliczbowych o kształcie (próbki, maxlen).
x_train <- pad_sequences(x_train, maxlen = maxlen)
x_test <- pad_sequences(x_test, maxlen = maxlen)
```

```{r, echo=TRUE, results='hide'}
model <- keras_model_sequential() %>% 
  # Określamy maksymalną długość danych wejściowych warstwy embedding,
  # co umożliwi późniejsze spłaszczenie osadzonych danych wejściowych.
  layer_embedding(input_dim = 10000, output_dim = 8, 
                  input_length = maxlen) %>% 
  # Spłaszczanie trójwymiarowego tensora osadzeń 
  # w celu uzyskania dwuwymiarowego tensora o kształcie (próbki, maxlen * 8).
  layer_flatten() %>% 
  # Dodawanie ostatniej warstwy klasyfikatora.
  layer_dense(units = 1, activation = "sigmoid") 

model %>% compile(
  optimizer = "rmsprop",
  loss = "binary_crossentropy",
  metrics = c("acc")
)

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

Podczas walidacji uzyskujemy dokładność na poziomie ~75%. Jest to wynik dość dobry, biorąc pod uwagę to, że analizujemy tylko 20 pierwszych słów każdej recenzji. Zwróć uwagę na to, że spłaszczenie osadzonych sekwencji i trenowanie jednej górnej warstwy dense prowadzi do uzyskania modelu traktującego niezależnie każde słowo sekwencji wejściowej. Model nie analizuje zależności między słowami i struktury zdania (potraktuje np. zdania this movie is a bomb i this movie is the bomb jako należące do recenzji negatywnych). O wiele lepszym rozwiązaniem jest dodanie do osadzonej sekwencji rekurencyjnych warstw lub jednowymiarowych warstw konwolucyjnych — pozwoli to na uczenie się cech biorących pod uwagę całość sekwencji. Rozwiązania te opiszę w kolejnych sekcjach.

## Używanie trenowanych wcześniej osadzeń słów


Czasami dysponuje się tak małą ilością dostępnych danych, że niemożliwe jest korzystanie z samych tych danych w celu wytrenowania osadzenia słów właściwego dla problemu. Co można zrobić w takiej sytuacji?

Zamiast trenować osadzenia słów łącznie z rozwiązywanym problemem, można w takim przypadku załadować osadzające wektory z utworzonej wcześniej przestrzeni osadzania, o której wiadomo, że ma odpowiednią strukturę i właściwości — ujmuje ogólne aspekty struktury języka. Sens używania wytrenowanych wcześniej osadzeń słów podczas przetwarzania języka naturalnego jest taki sam jak sens używania wytrenowanych wcześniej konwolucyjnych sieci neuronowych podczas klasyfikacji obrazów — jeżeli nie dysponujemy wystarczającą ilością danych, aby model mógł wytrenować samodzielnie praktyczne cechy, a cechy, których wytrenowania oczekujemy, mają charakter ogólny, to w przypadku cech wizualnych i semantycznych możemy używać cech wyuczonych przez modele pracujące nad innymi problemami.

Tego typu osadzenia słów tworzy się zwykle, korzystając ze statystyk występowania słów (obserwacji tego, jakie słowa występują obok siebie w zdaniach lub dokumentach). Robi się to za pomocą wielu technik. Niektóre z nich są oparte na sieciach neuronowych. Pomysł tworzenia gęstej niskowymiarowej osadzającej przestrzeni słów tworzonej w sposób nienadzorowany był początkowo analizowany przez Bengio i innych na początku lat dwutysięcznych , ale zyskał popularność w zastosowaniach praktycznych dopiero po opublikowaniu najsłynniejszego algorytmu osadzania słów — algorytmu Word2vec. Algorytm ten został opracowany w 2013 r. przez pracownika firmy Google Tomasa Mikolova. Wymiary algorytmu Word2vec odzwierciedlają określone właściwości semantyczne, takie jak rodzaj (płeć). 

Istnieje wiele gotowych baz osadzeń słów, które można pobrać i zaimportować do warstwy Embedding pakietu Keras. Jednym z takich rozwiązań jest właśnie Word2vec. Innym popularnym projektem jest Global Vectors for Word Representation (GloVe). Projekt ten został opracowany w 2014 r. przez badaczy z Uniwersytetu Stanforda. Ta technika osadzania korzysta z faktoryzacji macierzy wartości statystycznych określających wspólne występowanie słów. Jej autorzy udostępnili gotowe osadzenia milionów angielskich słów utworzone na podstawie danych z serwisów Wikipedia i Common Crawl.

Przyjrzyjmy się stosowaniu osadzeń GloVe w modelach Keras. W takim sam sposób można korzystać z osadzeń Word2vec i innych baz osadzeń słów. Pracując nad tym przykładem, przypomnimy również sobie opisane wcześniej techniki tokenizacji tekstu — pracę zaczniemy od surowego tekstu.

## Łączenie wszystkich technik: od surowego tekstu do osadzenia słów


Będziemy korzystać z modelu podobnego do tego, który został przed chwilą opisany — zamienimy zdania na sekwencje wektorów, spłaszczymy je i będziemy trenować górną warstwę dense naszego modelu. Tym razem będziemy jednak korzystać z gotowych osadzeń słów. Zamiast wykorzystywać poddane tokenizacji dane zbioru IMDB dołączonego do pakietu Keras, zaczniemy od podstaw — pobierzemy dane w postaci surowego tekstu.

### Pobieranie danych zbioru IMDB w postaci surowego tekstu


Na początku musimy wejść na stronę http://ai.stanford.edu/~amaas/data/sentiment pobrać archiwum z surowym zbiorem danych IMDB i je rozpakować.

Następnie należy zebrać poszczególne recenzje tworzące treningowy zbiór danych i przedstawić je w formie listy łańcuchów — każda recenzja powinna tworzyć oddzielny łańcuch. Etykiety recenzji (określenia ich tonu) umieścimy na liście labels.

```{r}
imdb_dir <- "~/Downloads/aclImdb"
train_dir <- file.path(imdb_dir, "train")

labels <- c()
texts <- c()

for (label_type in c("neg", "pos")) {
  label <- switch(label_type, neg = 0, pos = 1)
  dir_name <- file.path(train_dir, label_type)
  for (fname in list.files(dir_name, pattern = glob2rx("*.txt"), 
                           full.names = TRUE)) {
    texts <- c(texts, readChar(fname, file.info(fname)$size))
    labels <- c(labels, label)
  }
}
```

### Tokenizacja danych


Dokonajmy konwersji danych tekstowych na wektory i przygotujmy zbiór treningowy i zbiór walidacyjny, korzystając z przedstawionych wcześniej koncepcji. Trenowane wcześniej osadzenia słów są szczególnie przydatne podczas pracy, gdy ma się dostęp do małej ilości danych treningowych (w przeciwnym razie osadzenia zoptymalizowane pod kątem danego problemu sprawdzą się o wiele lepiej), a więc ograniczmy treningowy zbiór danych do 200 pierwszych próbek. Będziemy trenować klasyfikator recenzji filmów na zaledwie 200 przykładach.

```{r}
library(keras)

maxlen <- 100                 # Skraca recenzję do 100 słów.
training_samples <- 200       # Trenowanie na 200 próbkach.
validation_samples <- 10000   # Walidacja na 10000 próbek.
max_words <- 10000            # Bierzemy pod uwagę tylko 10 000 słów najczęściej występujących w zbiorze.

tokenizer <- text_tokenizer(num_words = max_words) %>% 
  fit_text_tokenizer(texts)

sequences <- texts_to_sequences(tokenizer, texts)

word_index = tokenizer$word_index
cat("Znaleziono", length(word_index), "unikatowych tokenów.\n")

data <- pad_sequences(sequences, maxlen = maxlen)

labels <- as.array(labels)
cat("Kształt tensora danych:", dim(data), "\n")
cat('Kształt tensora etykiet:', dim(labels), "\n")

# Dzieli dane na zbiór treningowy i zbiór walidacyjny, 
# ale najpierw dane są ustawiane w losowej kolejności 
# (obecnie próbki są ustawione w kolejności od recenzji negatywnych do recenzji pozytywnych).
indices <- sample(1:nrow(data))
training_indices <- indices[1:training_samples]
validation_indices <- indices[(training_samples + 1): 
                              (training_samples + validation_samples)]

x_train <- data[training_indices,]
y_train <- labels[training_indices]

x_val <- data[validation_indices,]
y_val <- labels[validation_indices]
```

### Pobieranie osadzeń słów GloVe


Ze strony  https://nlp.stanford.edu/projects/glove/ możemy pobrać gotowe osadzenia słów wygenerowane w 2014 r. na podstawie artykułów w serwisie Wikipedia. Jest to archiwum ZIP zajmujące 822 MB (plik glove.6B.zip). Zawiera ono stuwymiarowe wektory osadzeń 400 000 słów (można je już określać mianem tokenów). Rozpakujmy to archiwum.

### Wstępne przetwarzanie osadzeń


Przeprowadźmy operację parsowania rozpakowanego pliku tekstowego w celu zbudowania indeksu przypisującego słowa (w formie łańcuchów) do reprezentacji wektorowych (wektorów liczb).

```{r}
glove_dir = '~/Downloads/glove.6B'
lines <- readLines(file.path(glove_dir, "glove.6B.100d.txt"))

embeddings_index <- new.env(hash = TRUE, parent = emptyenv())
for (i in 1:length(lines)) {
  line <- lines[[i]]
  values <- strsplit(line, " ")[[1]]
  word <- values[[1]]
  embeddings_index[[word]] <- as.double(values[-1])
}

cat("Znaleziono", length(embeddings_index), "wektorów słów.\n")
```

Następnie będziemy budować macierz osadzeń, którą można załadować do warstwy embedding. Macierz ta musi mieć kształt (max_words, embedding_dim), a element i ma zawierać wektor embedding_dim-wymiarowy dla słowa o indeksie i zbudowanym podczas tokenizacji. Zwrócimy uwagę na to, że indeks 0 nie ma odpowiadać żadnemu słowu ani tokenowi — jest to po prostu wypełniacz miejsca.

```{r}
embedding_dim <- 100

embedding_matrix <- array(0, c(max_words, embedding_dim))

for (word in names(word_index)) {
  index <- word_index[[word]]
  if (index < max_words) {
    embedding_vector <- embeddings_index[[word]]
    if (!is.null(embedding_vector))
      # Słowa nieznalezione w osadzanym indeksie zostaną zastąpione zerami
      embedding_matrix[index+1,] <- embedding_vector
  }
}
```

### Definiowanie modelu

Będziemy korzystać z tej samej architektury co we wcześniejszym modelu:

```{r}
model <- keras_model_sequential() %>% 
  layer_embedding(input_dim = max_words, output_dim = embedding_dim, 
                  input_length = maxlen) %>% 
  layer_flatten() %>% 
  layer_dense(units = 32, activation = "relu") %>% 
  layer_dense(units = 1, activation = "sigmoid")

summary(model)
```

### Ładowanie osadzeń GloVe do modelu


Warstwa embedding ma pojedynczą macierz wag: dwuwymiarową macierz wartości zmiennoprzecinkowych, w której każdy element i jest wektorem słowa skojarzonym z indeksem i. Musimy po prostu załadować przygotowaną macierz GloVe do warstwy embedding, będącej pierwszą warstwą modelu.

```{r}
get_layer(model, index = 1) %>% 
  set_weights(list(embedding_matrix)) %>% 
  freeze_weights()
```

Dodatkowo zamrozimy warstwę embedding. Sensowność tego rozwiązania wyjaśniałem już przy okazji korzystania z wytrenowanych wcześniej cech konwolucyjnych sieci neuronowych — gdy niektóre elementy modelu (takie jak warstwa embedding) zostały wytrenowane wcześniej, a inne elementy (takie jak klasyfikator) są inicjowane liczbami losowymi, nie powinno dochodzić do modyfikowania wytrenowanych wcześniej elementów modelu, ponieważ doprowadzi to do zniknięcia zapisanej w nich wiedzy. Duże wartości aktualizacji gradientu wywołane losowym charakterem niektórych elementów modelu spowodują wprowadzenie dużych zmian w wytrenowanych cechach.

### Trenowanie i ewaluacja modelu

Skompilujmy model i go wytrenujmy.

```{r, echo=TRUE, results='hide'}
model %>% compile(
  optimizer = "rmsprop",
  loss = "binary_crossentropy",
  metrics = c("acc")
)

history <- model %>% fit(
  x_train, y_train,
  epochs = 20,
  batch_size = 32,
  validation_data = list(x_val, y_val)
)

save_model_weights_hdf5(model, "pre_trained_glove_model.h5")
```

Teraz możemy wygenerować wykresy ilustrujące zmiany wydajności modelu na przestrzeni czasu :

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

Model zaczyna szybko ulegać nadmiernemu dopasowaniu, co nie jest niczym zaskakującym przy tak małej liczbie próbek wchodzących w skład treningowego zbioru danych. Z tego samego powodu dokładność walidacji charakteryzuje się dużą zmiennością, ale wydaje się uzyskiwać szczytową wartość na poziomie przekraczającym 50%.

Przy tak małej liczbie próbek wchodzących w skład treningowego zbioru danych charakterystyka modelu w dużej mierze zależy od tego, które 200 próbek zostanie wybranych do treningowego zbioru danych (próbki te są wybierane w sposób losowy). Jeżeli Twój model uzyskuje wyraźnie gorsze parametry, to spróbuj wylosować inny zestaw 200 próbek (to tylko ćwiczenie, podczas pracy nad prawdziwym problemem nie dysponuje się możliwością wybrania danych treningowych).

Możemy również wytrenować ten sam model bez ładowania wytrenowanych osadzeń słów i bez zamrażania warstwy osadzeń. W takim przypadku model będzie trenować osadzenia tokenów wejściowych właściwe dla naszego problemu. Rozwiązanie to, ogólnie rzecz biorąc, sprawdza się o wiele lepiej od stosowania gotowych osadzeń słów w przypadku problemów, w których dysponuje się obszernym zbiorem danych. Dysponujemy zbiorem tylko 200 próbek treningowych, ale pomimo to wypróbujmy to rozwiązanie (patrz rysunek 6.6).

```{r, echo=TRUE, results='hide'}
model <- keras_model_sequential() %>% 
  layer_embedding(input_dim = max_words, output_dim = embedding_dim, 
                  input_length = maxlen) %>% 
  layer_flatten() %>% 
  layer_dense(units = 32, activation = "relu") %>% 
  layer_dense(units = 1, activation = "sigmoid")

model %>% compile(
  optimizer = "rmsprop",
  loss = "binary_crossentropy",
  metrics = c("acc")
)

history <- model %>% fit(
  x_train, y_train,
  epochs = 20,
  batch_size = 32,
  validation_data = list(x_val, y_val)
)
```

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

Dokładność walidacji utrzymuje się na poziomie zbliżonym do 50%. W tym przypadku korzystanie z gotowych osadzeń słów pozwoliło na uzyskanie lepszych wyników. Jeżeli zwiększymy liczbę próbek wchodzących w skład treningowego zbioru danych dość szybko uzyskamy lepszą wydajność od tej, którą uzyskaliśmy, korzystając z gotowych osadzeń słów. Spróbuj zrobić to samodzielnie w ramach ćwiczeń.

Na koniec możemy sprawdzić wydajność modelu podczas przetwarzania danych testowych. W tym celu musimy zamienić dane testowe na tokeny:


```{r}
test_dir <- file.path(imdb_dir, "test")

labels <- c()
texts <- c()

for (label_type in c("neg", "pos")) {
  label <- switch(label_type, neg = 0, pos = 1)
  dir_name <- file.path(test_dir, label_type)
  for (fname in list.files(dir_name, pattern = glob2rx("*.txt"), 
                           full.names = TRUE)) {
    texts <- c(texts, readChar(fname, file.info(fname)$size))
    labels <- c(labels, label)
  }
}

sequences <- texts_to_sequences(tokenizer, texts)
x_test <- pad_sequences(sequences, maxlen = maxlen)
y_test <- as.array(labels)
```

Teraz możemy załadować pierwszy model i ocenić efekty jego pracy:

```{r}
model %>% 
  load_model_weights_hdf5("pre_trained_glove_model.h5") %>% 
  evaluate(x_test, y_test, verbose = 0)
```

Uzyskujemy dokładność testową na poziomie 56%. Praca z małą liczbą próbek treningowych jest naprawdę trudna!
