8 Szóbeágyazások

8.1 A szóbeágyazás célja

Az eddigi fejezetekben elsősorban a szózsák (bag of words) alapú módszerek voltak előtérben. A szózsák alapú módszerekkel szemben, amelyek alkalmazása során elveszik a kontextuális tartalom, a szóbeágyazáson (word embedding) alapuló modellek kimondottan a kontextuális információt ragadják meg. A szóbeágyazás a topikmodellekhez hasonlóan a felügyelet nélküli tanulás módszerére épül, azonban itt a dokumentum domináns kifejezéseinek és témáinak feltárása helyett a szavak közötti szemantikai kapcsolat megértése a cél. Vagyis a modellnek képesnek kell lennie az egyes szavak esetén szinonimáik és ellentétpárjaik megtalálására.

A hagyományos topikmodellezés esetén a modell a szavak dokumentumokon belüli együttes megjelenési statisztikái alapján becsül dokumentum-topik, illetve topik-szó eloszlásokat, azzal a céllal, hogy koherens téma-csoportokat képezzen. Ezzel szemben a szóbeágyazás legújabb iskolája már neurális halókon alapul. A neurális háló a tanítási folyamata során az egyes szavak vektorreprezentációját állítja elő. A vektorok jellemzően 100–300 dimenzióból állnak, a távolságuk alapján pedig megállapítható, hogy az egyes kifejezések milyen szemantikai kapcsolatban állnak egymással.

A szóbeágyazás célja tehát a szemantikai relációk feltárása. A szavak vektorizálásának köszönhetően bármely (a korpuszunkban szereplő) tetszőleges számú szóról eldönthetjük, hogy azok milyen szemantikai kapcsolatban állnak egymással, azaz szinonimaként vagy ellentétes fogalompárként szerepelnek. A szóvektorokon dimenziócsökkentő eljárást alkalmazva, s a multidimenzionális (100–300 dimenziós) teret 2 dimenziósra szűkítve könnyen vizualizálhatjuk is a korpuszunk kifejezései között fennálló szemantikai távolságot, és ahogy a lenti ábrákon láthatjuk, azt, hogy az egyes kifejezések milyen relációban állnak egymással – a szemantikailag hasonló tartalmú kifejezések egymáshoz közel, míg a távolabbi jelentéstartalmú kifejezések egymástól távolabb foglalnak helyet. A klasszikus példa, amivel jól lehet szemléltetni a szóvektorok közötti összefüggést: king - man + woman = queen.

8.2 Word2Vec és GloVe

A társadalomtudományokban szóbeágyazásra a két legnépszerűbb algoritmus – a Word2Vec és a GloVe – a kontextuális szövegeloszláson (distributional similarity based representations) alapul, vagyis abból a feltevésből indul ki, hogy a hasonló kifejezések hasonló kontextusban fordulnak elő, emellett mindkettő sekély neurális hálón (2 rejtett réteg) alapuló modell.43 A Word2Vec-nek két verziója van: Continuous Bag-of-words (CBOW) és SkipGram (SG). Előbbi a kontextuális szavakból jelzi előre (predicting) a kontextushoz legszorosabban kapcsolódó kifejezést, míg utóbbi adott kifejezésből jelzi előre a kontextust Mikolov et al. (2013). A GloVe (Global Vectors for Word Representation) a Word2Vec-hez hasonlóan neurális hálón alapuló, szóvektorok előállítását célzó modell, a Word2Vec-kel szemben azonban nem a meghatározott kontextus-ablakban (context window) megjelenő kifejezések közti kapcsolatokat tárja fel, hanem a szöveg globális jellemzőit igyekszik megragadni az egész szöveget jellemző együttes előfordulási gyakoriságok (co-occurrance) meghatározásával Pennington, Socher, and Manning (2014). Míg a Word2Vec modell prediktív jellegű, addig a GloVe egy statisztikai alapú (count-based) modell, melyek gyakorlati hasznosításukat tekintve nagyon hasonlóak.

A szóvektor modellek között érdemes megemlíteni a fastText-et is, mely 157 nyelvre (köztük a magyarra is) kínál a szóbeágyazás módszerén alapuló, előre tanított szóvektorokat, melyet tovább lehet tanítani speciális szövegkorpuszokra, ezzel jelentősen lerövidítve a modell tanításához szükséges idő- és kapacitásszükségletet (Mikolov et al. (2018)). Habár a GloVe és Word2Vec skip-gram módszerek hasonlóságát a szakirodalom adottnak veszi, a tényleges kép ennél árnyaltabb. A GloVe esetében a ritkán előforduló szavak kisebb súlyt kapnak a szóvektorok számításánál, míg a Word2Vec alulsúlyozza a nagy frekvenciájú szavakat. Ennek a következménye, hogy a Word2Vec esetében gyakori, hogy a szemantikailag legközelebbi szó az egy elütés, nem pedig valid találat. Ennek ellenére a két módszer (amennyiben a Word2Vec algoritmusnál a kisfrekvenciájú tokeneket kiszűrjük) az emberi validálás során nagyon hasonló eredményeket hozott (Spirling and Rodriguez 2021).

A fejezetben a gyakorlati példa során a GloVe algoritmust használjuk majd, mivel véleményünk szerint jobb és könnyebben követhető a dokumentációja az implementációt tartalmazó R csomagnak, mint a többi alternatívának.

8.2.1 GloVe használata magyar média korpuszon

Az elemzéshez a text2vec csomagot használjuk, ami a GloVe implementációt tartalmazza (Selivanov, Bickel, and Wang 2020). A lenti kód a csomag dokumentáción alapul és a Társadalomtudományi Kutatóközpont által a Hungarian Comparative Agendas Project (CAP) adatbázisában tárolt Magyar Nemzet korpuszt használja.44

library(text2vec)
library(quanteda)
library(quanteda.textstats)
library(readtext)
library(readr)
library(dplyr)
library(tibble)
library(stringr)
library(ggplot2)
library(plotly)
library(HunMineR)

A lenti kód blokk azt mutatja be, hogyan kell a betöltött korpuszt tokenizálni és mátrix formátumba alakítani. A korpusz a Magyar Nemzet 2004 és 2014 közötti címlapos cikkeit tartalmazza. Az eddigi előkészítő lépéseket most is megtesszük: kitöröljük a központozást, a számokat, a magyar töltelékszavakat, illetve kisbetűsítünk és eltávolítjuk a felesleges szóközöket és töréseket.

mn <- HunMineR::data_magyar_nemzet_large

mn_clean <- mn %>%
  mutate(
    text = str_remove_all(string = text, pattern = "[:cntrl:]"),
    text = str_remove_all(string = text, pattern = "[:punct:]"),
    text = str_remove_all(string = text, pattern = "[:digit:]"),
    text = str_to_lower(text),
    text = str_trim(text),
    text = str_squish(text)
  )

A glimpse funkció segítségével belepillanthatunk mind a két korpuszba és láthatjuk, hogy sikeres volt a tisztítása, valamint azt is, hogy jelenleg egyetlen metaadatunk a dokumentumok azonosítója.

glimpse(mn)
#> Rows: 35,021
#> Columns: 2
#> $ doc_id <chr> "mn_2002_05_27_00.txt", "mn_2002_05_27_01.txt", "mn_2002_05_27_02.txt", "mn_2002_05_27_03.txt", "mn_2002_05_27_04.t…
#> $ text   <chr> "Csere szerb módra\nNagy vihart kavart Szerbiában a kormánykoalíció \nvezetőségének azon döntése, hogy lecseréli az…
glimpse(mn_clean)
#> Rows: 35,021
#> Columns: 2
#> $ doc_id <chr> "mn_2002_05_27_00.txt", "mn_2002_05_27_01.txt", "mn_2002_05_27_02.txt", "mn_2002_05_27_03.txt", "mn_2002_05_27_04.t…
#> $ text   <chr> "csere szerb módranagy vihart kavart szerbiában a kormánykoalíció vezetőségének azon döntése hogy lecseréli azokat …

Fontos különbség, hogy az eddigi munkafolyamatokkal ellentétben a GloVe algoritmus nem egy dokumentum-kifejezés mátrixon dolgozik, hanem egy kifejezések együttes előfordulását tartalmazó mátrixot (feature co-occurence matrix) kell készíteni inputként. Ezt a quanteda fcm() függvényével tudjuk előállítani, ami a tokenekből készíti el a mátrixot. A tokenek sorrendiségét úgy tudjuk megőrizni, hogy egy dfm objektumból csak a kifejezéseket tartjuk meg a featnames() függvény segítségével, majd a teljes token halmazból a tokens_select() függvénnyel kiválasztjuk őket.

mn_corpus <- corpus(mn_clean)

mn_tokens <- tokens(mn_corpus) %>%
  tokens_remove(stopwords(language = "hungarian"))

features <- dfm(mn_tokens) %>%
  dfm_trim(min_termfreq = 5) %>%
  quanteda::featnames()

mn_tokens <- tokens_select(mn_tokens, features, padding = TRUE)

Az fcm megalkotása során a célkifejezéstől való távolság függvényében súlyozzuk a tokeneket.

mn_fcm <- quanteda::fcm(mn_tokens, context = "window", count = "weighted", weights = 1/(1:5),
    tri = TRUE)

A tényleges szóbeágyazás a text2vec csomaggal történik. A GlobalVector egy új „környezetet” (environment) hoz létre. Itt adhatjuk meg az alapvető paramétereket. A rank a vektor dimenziót adja meg (a szakirodalomban a 300–500 dimenzió a megszokott). A többi paraméterrel is lehet kísérletezni, hogy mennyire változtatja meg a kapott szóbeágyazásokat. A fit_transform pedig a tényleges becslést végzi. Itt az iterációk számát (a gépi tanulásos irodalomban epoch-nak is hívják a tanulási köröket) és a korai leállás (early stopping) kritériumát a convergence_tol megadásával állíthatjuk be. Minél több dimenziót szeretnénk és minél több iterációt, annál tovább fog tartani a szóbeágyazás futtatása.

Az egyszerűség és a gyorsaság miatt a lenti kód 10 körös tanulást ad meg, ami a relatíve kicsi Magyar Nemzet korpuszon ~3 perc alatt fut le.45 Természetesen minél nagyobb korpuszon, minél több iterációt futtatunk, annál pontosabb eredményt fogunk kapni. A text2vec csomag képes a számítások párhuzamosítására, így alapbeállításként a rendelkezésre álló összes CPU magot teljesen kihasználja a számításhoz. Ennek ellenére egy százezres, milliós korpusz esetén több óra is lehet a tanítás.

glove <- GlobalVectors$new(rank = 300, x_max = 10, learning_rate = 0.1)

mn_main <- glove$fit_transform(mn_fcm, n_iter = 10, convergence_tol = 0.1)
#> INFO  [14:20:07.100] epoch 1, loss 0.2295
#> INFO  [14:20:20.771] epoch 2, loss 0.0963
#> INFO  [14:20:33.842] epoch 3, loss 0.0706
#> INFO  [14:20:47.761] epoch 4, loss 0.0490
#> INFO  [14:21:03.564] epoch 5, loss 0.0411
#> INFO  [14:21:16.841] epoch 6, loss 0.0361
#> INFO  [14:21:39.893] epoch 7, loss 0.0326
#> INFO  [14:21:56.744] epoch 8, loss 0.0298
#> INFO  [14:21:56.746] Success: early stopping. Improvement at iterartion 8 is less then convergence_tol

A végleges szóvektorokat a becslés során elkészült két mátrix összegeként kapjuk.

mn_context <- glove$components

mn_word_vectors <- mn_main + t(mn_context)

Az egyes szavakhoz legközelebb álló szavakat a koszinusz hasonlóság alapján kapjuk, a sim2() függvénnyel. A lenti példában „l2” normalizálást alkalmazunk, majd a kapott hasonlósági vektort csökkenő sorrendbe rendezzük. Példaként a „polgármester” szónak a környezetét nézzük meg. Mivel a korpuszunk egy politikai napilap, ezért nem meglepő, hogy a legközelebbi szavak a politikához kapcsolódnak.

teszt <- mn_word_vectors["polgármester", , drop = F]

cos_sim_rom <- text2vec::sim2(x = mn_word_vectors, y = teszt, method = "cosine", norm = "l2")

head(sort(cos_sim_rom[, 1], decreasing = TRUE), 5)
#> polgármester        mszps  szocialista     fideszes    politikus 
#>        1.000        0.517        0.507        0.464        0.408

A lenti show_vector() függvényt definiálva a kapott eredmény egy data frame lesz, és az n változtatásával a kapcsolódó szavak számát is könnyen változtathatjuk.

show_vector <- function(vectors, pattern, n = 5) {
  term <- mn_word_vectors[pattern, , drop = F]
  cos_sim <- sim2(x = vectors, y = term, method = "cosine", norm = "l2")
  cos_sim_head <- head(sort(cos_sim[, 1], decreasing = TRUE), n)
  output <- enframe(cos_sim_head, name = "term", value = "dist")
  return(output)
}

Példánkban láthatjuk, hogy a „barack” szó beágyazásának eredménye nem gyümölcsöt fog adni, hanem az Egyesült Államok elnökét és a hozzá kapcsolódó szavakat.

show_vector(mn_word_vectors, "barack", 10)
#> # A tibble: 10 × 2
#>   term          dist
#>   <chr>        <dbl>
#> 1 barack       1    
#> 2 obama        0.726
#> 3 amerikai     0.428
#> 4 elnök        0.393
#> 5 demokrata    0.386
#> 6 republikánus 0.281
#> # ℹ 4 more rows

Ugyanez működik magyar vezetőkkel is.

show_vector(mn_word_vectors, "orbán", 10)
#> # A tibble: 10 × 2
#>   term            dist
#>   <chr>          <dbl>
#> 1 orbán          1    
#> 2 viktor         0.931
#> 3 miniszterelnök 0.763
#> 4 mondta         0.698
#> 5 kormányfő      0.685
#> 6 fidesz         0.678
#> # ℹ 4 more rows

A szakirodalomban klasszikus vektorműveletes példákat is reprokuálni tudjuk a Magyar Nemzet korpuszon készített szóbeágyazásainkkal. A budapest - magyarország + német + németország eredményét úgy kapjuk meg, hogy az egyes szavakhoz tartozó vektorokat kivonjuk egymásból, illetve hozzáadjuk őket, ezután pedig a kapott mátrixon a quanteda csomag textstat_simil() függvényével kiszámítjuk az új hasonlósági értékeket.

budapest <- mn_word_vectors["budapest", , drop = FALSE] - mn_word_vectors["magyarország", , drop = FALSE] + mn_word_vectors["német", , drop = FALSE] +
  + mn_word_vectors["németország", , drop = FALSE]

cos_sim <- textstat_simil(x = as.dfm(mn_word_vectors), y = as.dfm(budapest), method = "cosine")

head(sort(cos_sim[, 1], decreasing = TRUE), 5)
#>    budapest németország       német     airport   kancellár 
#>       0.602       0.557       0.542       0.424       0.395

A szavak egymástól való távolságát vizuálisan is tudjuk ábrázolni. Az egyik ezzel kapcsolatban felmerülő probléma, hogy egy 2 dimenziós ábrán akarunk egy 3–500 dimenziós mátrixot ábrázolni. Több lehetséges megoldás is van, mi ezek közül a lehető legegyszerűbbet mutatjuk be.46 Első lépésben egy data frame-et készítünk a szóbeágyazás eredményeként kapott mátrixból, megtartva a szavakat az első oszlopban a tibble csomag rownames_to_column() függvényével. Mivel csak 2 dimenziót tudunk ábrázolni egy tradícionális statikus ábrán, ezért a V1 és V2 oszlopokat tartjuk csak meg, amik az első és második dimenziót reprezentálják.

mn_embedding_df <- as.data.frame(mn_word_vectors[, c(1:2)]) %>% 
  tibble::rownames_to_column(var = "words")

Ezután pedig a ggplot() függvényt felhasználva definiálunk egy új, embedding_plot() nevű, függvényt, ami az elkészült data frame alapján bármilyen kulcsszó kombinációt képes ábrázolni.

embedding_plot <- function(data, keywords) {
  data %>% 
    filter(words %in% keywords) %>% 
    ggplot(aes(V1, V2, label = words)) +
    labs(
      x = "Első dimenzió",
      y = "Második dimenzió"
    ) +
    geom_text() +
    xlim(-1, 1) +
    ylim(-1, 1)
}

Példaként néhány településnevet megvizsgálva, azt látjuk, hogy a megadott szavak, jelen esetben “budapest”, “debrecen”, “washington”, “moszkva” milyen közel vagy távol vannak egymástól, vagyis milyen gyakorisággal fordulnak elő ugyanazon szavak társaságában. A magyar városok közel helyezkednek el egymáshoz, ám “washington” és “moszkva” távolsága nagyobb. Ennek az oka az lehet hogy a két magyar nagyváros gyakrabban szerepel hasonló kontextusban a belföldi hírekben, míg a két külföldi főváros valószínűleg eltérő külpolitikai környezetben jelenik meg.

words_selected <- c("moszkva", "debrecen", "budapest", "washington")

embedded <- embedding_plot(data = mn_embedding_df, keywords = words_selected)

ggplotly(embedded)

Ábra 8.1: Kiválasztott szavak két dimenzós térben


  1. Egy kiváló tanulmányban Spirling and Rodriguez (2021) (könyvünk írásakor még nem jelent meg) összehasonlítják a Word2Vec és GloVe módszereket, különböző paraméterekkel, adatbázisokkal. Azoknak, akiket komolyabban érdekelnek a szóbeágyazás gyakorlati alkalmazásának a részletei, mindenképp ajánljuk elolvasásra.↩︎

  2. A Magyar CAP Project által kezelt adatbázisok regisztrációt követően elérhetőek az elábbi linken: https://cap.tk.hu/adatbazisok. A text2vec csomag dokumentációja: https://cran.r-project.org/web/packages/text2vec/vignettes/glove.html↩︎

  3. A futtatásra használt PC konfiguráció: CPU: Intel Core i5-4460 (3.2GHz); RAM: 16GB↩︎

  4. Az egyik legelterjedtebb dimenzionalitás csökkentő eljárás a szakirodalomban a főkomponens-analízis (principal component analysis), illetve szintén gyakran használt az irodalomban az úgynevezett t-SNE (t-distributed stochastic neighbor embedding).↩︎