7 Felügyelet nélküli tanulás

7.1 Fogalmi alapok

A felügyelet nélküli tanulási során az alkalmazott algoritmus a dokumentum tulajdonságait és a modell becsléseit felhasználva végez csoportosítást, azaz hoz létre különböző kategóriákat, melyekhez később hozzárendeli a szöveget. Jelen fejezetben a felügyelet nélküli módszerek közül a topikmodellezést tárgyaljuk részletesen, majd a következőkben bemutatjuk a szintén ide sorolható szóbeágyazást és a szövegskálázás wordfish módszerét is.

7.2 K-közép klaszterezés

A klaszterezés egy adathalmaz pontjainak, rekordjainak hasonlóság alapján való csoportosítása, ami szinte minden nagyméretű adathalmaz leíró modellezésére alkalmas. A klaszterezés során az adatpontokat diszjunkt halmazokba, azaz klaszterekbe soroljuk, hogy az elemeknek egy olyan partíciója jöjjön létre, amelyben a közös csoportokba kerülő elempárok lényegesen jobban hasonlítanak egymáshoz, mint azok a pontpárok, melyek két különböző csoportba sorolódtak. Klaszterezés során a megfelelő csoportok kialakítása nem egyértelmű feladat, mivel a különböző adatok eltérő jelentése és felhasználása miatt adathalmazonként más szempontokat kell figyelembe vennünk. Egy klaszterezési feladat megoldásához ismernünk kell a különböző algoritmusok alapvető tulajdonságait és mindig szükség van az eredményként kapott klaszterezés kiértékelésére. Mivel egy klaszterezés az adatpontok hasonlóságából indul ki, ezért az eljárás során az első fontos lépés az adatpontok páronkénti hasonlóságát a lehető legjobban megragadó hasonlósági függvény kiválasztása (Tan, Steinbach, and Kumar 2011). Számos klaszterezési eljárás létezik, melyek között az egyik leggyakoribb különbségtétel, hogy a klaszterek egymásba ágyazottak vagy sem. Ez alapján beszélhetünk hierarchikus és felosztó klaszterezésről.

A hierarchikus klaszterezés egymásba ágyazott klaszterek egy fába rendezett halmaza, azaz ahol a klaszterek alklaszterekkel rendelkeznek. A fa minden csúcsa (klasztere), a levélcsúcsokat kivéve, a gyermekei (alklaszterei) uniója, és a fa gyökere az összes objektumot tartalmazó klaszter. Felosztó (partitional) klaszterezés esetén az adathalmazt olyan, nem átfedő alcsoportokra bontjuk, ahol minden adatobjektum pontosan egy részhalmazba kerül (Tan, Steinbach, and Kumar 2011; Tikk 2007). A klaszterezési eljárások között aszerint is különbséget tehetünk, hogy azok egy objektumot csak egy vagy több klaszterbe is beilleszthetnek. Ez alapján beszélhetünk kizáró (exclusive), illetve nem-kizáró (non exclusive), vagy átfedő (overlapping) klaszterezésről. Az előbbi minden objektumot csak egyetlen klaszterhez rendel hozzá, az utóbbi esetén egy pont több klaszterbe is beleillik. Fuzzy klaszterezés esetén minden objektum minden klaszterbe beletartozik egy tagsági súly erejéig, melynek értéke 0 (egyáltalán nem tartozik bele) és 1 (teljesen beletartozik) közé esik. A klasztereknek is különböző típusai vannak, így beszélhetünk prototípus-alapú, gráf-alapú vagy sűrűség-alapú klaszterekről.

A prototípus-alapú klaszter olyan objektumokat tartalmazó halmaz, amelynek mindegyik objektuma jobban hasonlít a klasztert definiáló objektumhoz, mint bármelyik másik klasztert definiáló objektumhoz. A prototípus-alapú klaszterek közül a K-közép klaszter az egyik leggyakrabban alkalmazott. A K-közép klaszterezési módszer első lépése k darab kezdő középpont kijelölése, ahol k a klaszterek kívánt számával egyenlő. Ezután minden adatpontot a hozzá legközelebb eső középponthoz rendelünk. Az így képzett csoportok lesznek a kiinduló klaszterek. Ezután újra meghatározzuk mindegyik klaszter középpontját a klaszterhez rendelt pontok alapján. A hozzárendelési és frissítési lépéseket felváltva folytatjuk addig, amíg egyetlen pont sem vált klasztert, vagy ameddig a középpontok ugyanazok nem maradnak (Tan, Steinbach, and Kumar 2011).

A K-közép klaszterezés tehát a dokumentumokat alkotó szavak alapján keresi meg a felhasználó által megadott számú k klasztert, amelyeket a középpontjaik képviselnek, és így rendezi a dokumentumokat csoportokba. A klaszterezés vagy csoportosítás egy induktív kategorizálás, ami akkor hasznos, amikor nem állnak a kutató rendelkezésére előzetesen ismert csoportok, amelyek szerint a vizsgált dokumentumokat rendezni tudná. Hiszen ebben az esetben a korpusz elemeinek rendezéséhez nem határozunk meg előzetesen csoportokat, hanem az eljárás során olyan különálló csoportokat hozunk létre a dokumentumokból, amelynek tagjai valamilyen szempontból hasonlítanak egymásra. A csoportosítás legfőbb célja az, hogy az egy csoportba kerülő szövegek minél inkább hasonlítsanak egymásra, miközben a különböző csoportba kerülők minél inkább eltérjenek egymástól. Azaz klaszterezésnél nem egy-egy szöveg jellemzőire vagyunk kíváncsiak, hanem arra, hogy a szövegek egy-egy csoportja milyen hasonlóságokkal bír (Burtejin 2016; Tikk 2007). A gépi kódolással végzett klaszterezés egy felügyelet nélküli tanulás, mely a szöveg tulajdonságaiból tanul, anélkül, hogy előre meghatározott csoportokat ismerne. Alkalmazása során a dokumentum tulajdonságait és a modell becsléseit felhasználva jönnek létre a különböző kategóriák, melyekhez később hozzárendeli a szöveget (Grimmer and Stewart 2013). Az osztályozással ellentétben a csoportosítás esetén tehát nincs ismert „címkékkel” ellátott kategóriarendszer vagy olyan minta, mint az osztályozás esetében a tanítókörnyezet, amiből tanulva a modellt fel lehet építeni (Tikk 2007). A gépi kódolással végzett csoportosítás (klaszterezés) esetén a kutató feladata a megfelelő csoportosító mechanizmus kiválasztása, mely alapján egy program végzi el a szövegek különböző kategóriákba sorolását. Ezt követi a hasonló szövegeket tömörítő csoportok elnevezésének lépése. A több dokumentumból álló korpuszok esetében a gépi klaszterelemzés különösen eredményes és költséghatékony lehet, mivel egy nagy korpusz vizsgálata sok erőforrást igényel (Grimmer and Stewart 2013, 1.).

A klaszterezés bemutatásához a rendszerváltás utáni magyar miniszterelnökök egy-egy véletlenszerűen kiválasztott beszédét használjuk.

library(readr)
library(dplyr)
library(purrr)
library(stringr)
library(readtext)
library(quanteda)
library(quanteda.textstats)
library(tidytext)
library(ggplot2)
library(topicmodels)
library(factoextra)
library(stm)
library(igraph)
library(plotly)
library(HunMineR)

A beszédek szövege meglehetősen tiszta, ezért az egyszerűség kedvéért most kihagyjuk a szövegtisztítás lépéseit. Az elemzés első lépéseként a .csv fájlból beolvasott szövegeinkből a quanteda csomaggal korpuszt hozunk létre, majd abból egy dokumentum-kifejezés mátrixot készítünk a dfm() függvénnyel. Láthatjuk, hogy márixunk 7 megfigyelést és 4 változót tartalmaz.

beszedek <- HunMineR::data_miniszterelnokok

beszedek_corpus <- corpus(beszedek)

beszedek_dfm <- beszedek_corpus %>% 
  tokens() %>% 
  dfm()

A glimpse funkció segítségével ismét megtekinthetjük az adatainkat és láthatjuk, hogy a 4 változó a miniszterelnökök neve, az év, a dokumentum azonosító, amely az előző két változó kombinációja, valamint maguk a beszédek szövegei.

glimpse(data_miniszterelnokok)
#> Rows: 7
#> Columns: 4
#> $ doc_id <chr> "antall_jozsef_1990", "bajnai_gordon_2009", "gyurcsány_ferenc_2005", "horn_gyula_1994", "medgyessy_peter_2002", "or…
#> $ text   <chr> "Elnök Úr! Tisztelt Országgyulés! Hölgyeim és Uraim! Honfitársaim! Ünnepi pillanatban állok a magyar Országgyulés é…
#> $ year   <dbl> 1990, 2009, 2005, 1994, 2002, 1995, 2018
#> $ pm     <chr> "antall_jozsef", "bajnai_gordon", "gyurcsány_ferenc", "horn_gyula", "medgyessy_peter", "orban_viktor", "orban_vikto…

A beszédek klaszterekbe rendezését az R egyik alapfüggvénye, a kmeans() végzi. Első lépésben 2 klasztert készítünk. A table() függvénnyel megnézhetjük, hogy egy-egy csoportba hány dokumentum került.

beszedek_klaszter <- kmeans(beszedek_dfm, centers = 2)

table(beszedek_klaszter$cluster)
#> 
#> 1 2 
#> 2 5

A felügyelet nélküli klasszifikáció nagy kérdése, hány klasztert alakítsunk ki, hogy megközelítsük a valóságot, és ne csak mesterségesen kreáljunk csoportokat. Ez ugyanis azzal a kockázattal jár, hogy ténylegesen nem létező csoportok is létrejönnek. A klaszterek optimális számának meghatározására kvalitatív és kvantitatív lehetőségeink is vannak. A következőkben az utóbbira mutatunk példát, amihez a factoextra csomagot használjuk. A 7.1.-es ábra azt mutatja, hogy a klasztereken belüli négyzetösszegek hogyan változnak a k paraméter változásának függvényében. Minél kisebb a klasztereken belüli négyzetösszegek értéke, annál közelebbi pontok tartoznak össze, így a kisebb értékekkel definiált klasztereket kapunk. Az ábra alapján tehát az ideális k 4 vagy 2, attól fuggően, hogy milyen feltevésekkel élünk a kutatásunk során. A 2-es érték azért lehet jó, mert a \(k > 2\) értékek esetén a négyzetösszegek értéke nem csökken drasztikusan és a korpuszunk alapján a két („jobb-bal”) klaszter kvalitativ alapon is jól definiálható. A \(k = 4\) pedig azért lehet jó, mert utánna gyakorlatilag nem változik a kapott négyzetösszeg, ami azt jelzi, hogy a további klaszterek hozzáadásával nem lesz pontosabb a csoportosítás.

klas_df <- factoextra::fviz_nbclust(as.matrix(beszedek_dfm), kmeans, method = "wss", k.max = 5, linecolor = "black") +
labs(
  title = NULL,
  x = "Klaszterek száma",
  y = "Klasztereken belüli négyzetösszeg")

ggplotly(klas_df)

Ábra 7.1: A klaszterek optimális száma

A kialakított csoportokat vizuálisan is megjeleníthetjük (ld. 7.2. ábra).

min_besz_plot <- factoextra::fviz_cluster(
  beszedek_klaszter, 
  data = beszedek_dfm, 
  pointsize = 2,
  repel = TRUE,
  ggtheme = theme_minimal()
  ) +
  labs(
    title = "",
    x = "Első dimenzió",
    y = "Második dimenzió"
  ) +
  theme(legend.position = "none")

ggplotly(min_besz_plot)

Ábra 7.2: A miniszterelnöki beszédek klaszterei

Az így kapott ábrán láthatjuk nem csak a két klaszter középpontjainak egymáshoz viszonyított pozícióját, de a klaszterek elemeinek egymáshoz viszonyított pozícióját is. Ez alapján láthatjuk, hogy a 2 klaszterünk nem ugyanakkora elemszámból állnak, valamint a belső hasonlóságuk is eltérő. valamint a klaszter elemek neveiből láthatjuk, hogy nem vált be a hipotézisünk arra vonatkozóan, hogy két klaszter beállítása esetén azok a jobb és baloldaliságot fogják tükrözni, ez valószínűleg azért van így mert a korpuszokat ebben az esetben nem tisztítottuk, tehát stopszavazást sem végeztünk rajtuk, ebből kifolyólag pedig a politikai pozíciók mellett a szövegek hasonlósága és különbözősége sok más tényezőt is tükröz (pl: hogy mennyire összetetten vagy egyszerűen fogalmaz az illető, vagy a beszéd elhangzásának idején különös relevanciával bíró közpolitikákat)

7.3 LDA topikmodellek35

A topikmodellezés a dokumentumok téma klasztereinek meghatározására szolgáló valószínűség alapú eljárás, amely szógyakoriságot állapít meg minden témához, és minden dokumentumhoz hozzárendeli az adott témák valószínűségét. A topikmodellezés egy felügyelet nélküli tanulási módszer, amely során az alkalmazott algoritmus a dokumentum tulajdonságait és a modell becsléseit felhasználva hoz létre különböző kategóriákat, melyekhez később hozzárendeli a szöveget (Burtejin 2016; Grimmer and Stewart 2013; Tikk 2007). Az egyik leggyakrabban alkalmazott topikmodellezési eljárás, a Látens Dirichlet Allokáció (LDA) alapja az a feltételezés, hogy minden korpusz topikok/témák keverékéből áll, ezen témák pedig statisztikailag a korpusz szókészlete valószínűségi függvényeinek (eloszlásának) tekinthetőek (Blei, Ng, and Jordan 2003). Az LDA a korpusz dokumentumainak csoportosítása során az egyes dokumentumokhoz topik szavakat rendel, a topikok megbecsléséhez pedig a szavak együttes megjelenését vizsgálja a dokumentum egészében. Az LDA algoritmusnak előzetesen meg kell adni a keresett klaszterek (azaz a keresett topikok) számát, ezt követően a dokumentumhalmazban szereplő szavak eloszlása alapján az algoritmus azonosítja a kulcsszavakat, amelyek eloszlása kirajzolja a topikokat (Blei, Ng, and Jordan 2003; Burtejin 2016; Jacobi, Van Atteveldt, and Welbers 2015).

A következőkben a magyar törvények korpuszán szemléltetjük a topikmodellezés módszerét, hogy a mesterséges intelligencia segítségével feltárjuk a korpuszon belüli rejtett összefüggéseket. A korábban leírtak szerint tehát nincsenek előre meghatározott kategóriáink, dokumentumainkat a klaszterezés segítségével szeretnénk csoportosítani. Egy-egy dokumentumban keveredhetnek a témák és az azokat reprezentáló szavak. Mivel ugyanaz a szó több topikhoz is kapcsolódhat, így az eljárás komplex elemzési lehetőséget nyújt, az egy szövegen belüli témák és akár azok dokumentumon belüli súlyának azonosítására.

Az alábbiakban a 1998–2002-es és a 2002–2006-os parlamenti ciklus 1032 törvényszövegének topikmodellezését és a szükséges előkészítő, korpusztisztító lépéseket mutatjuk be. A HunMineR csomag segítségével beolvassuk az elemezni kívánt fájlokat.36

Töltsük be az elemezni kívánt csv fájlt, megadva az elérési útvonalát.

torvenyek <- HunMineR::data_lawtext_1998_2006
glimpse(torvenyek)
#> Rows: 1,032
#> Columns: 2
#> $ doc_id <chr> "1998L", "1998LI", "1998LII", "1998LIII", "1998LIV", "1998LIX", "1998LV", "1998LVI", "1998LVII", "1998LVIII", "1998…
#> $ text   <chr> "1998. évi L. törvény\n\naz Egyesült Nemzetek Szervezete keretében a kábítószerek és pszichotrop anyagok tiltott fo…

Láthatjuk, hogy ezek az objektum a dokumentum azonosítón kívül (amely a törvény évét és számát tartalmazza) nem rendelkezik egyéb metaadatokkal.

Az előző fejezetekben láthattuk, hogyan lehet használni a stringr csomagot a szövegtisztításra. A lépések a már megismert sztenderd folyamatot követik: számok, központozás, sortörések, extra szóközök eltávolítása, illetve a szöveg kisbetűsítése. Az eddigieket további szövegtisztító lépésekkel is kiegészíthetjük. Olyan elemek esetében, amelyek nem feltétlenül különálló szavak és el akarjuk távolítani őket a korpuszból, szintén a str_remove_all() a legegyszerűbb megoldás.

torvenyek_tiszta <- torvenyek %>%
  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),
    text = str_remove_all(string = text, pattern = "’"),
    text = str_remove_all(string = text, pattern = "…"),
    text = str_remove_all(string = text, pattern = "–"),
    text = str_remove_all(string = text, pattern = "“"),
    text = str_remove_all(string = text, pattern = "”"),
    text = str_remove_all(string = text, pattern = "„"),
    text = str_remove_all(string = text, pattern = "«"),
    text = str_remove_all(string = text, pattern = "»"),
    text = str_remove_all(string = text, pattern = "§"),
    text = str_remove_all(string = text, pattern = "°"),
    text = str_remove_all(string = text, pattern = "<U+25A1>"),
    text = str_remove_all(string = text, pattern = "@")
  )

A dokumentum változókat egy külön fájlból töltjük be, ami a törvények keletkezési évét tartalmazza, illetve azt, hogy melyik kormányzati ciklusban születtek. Mindkét adatbázisban egy közös egyedi azonosító jelöli az egyes törvényeket, így ki tudjuk használni a dplyr left_join() függvényét, ami hatékonyan és gyorsan kapcsol össze adatbázisokat közös egyedi azonosító mentén. Jelen esetben ez az egyedi azonosító a txt_filename oszlopból fog elkészülni, amely a törvények neveit tartalmazza. Első lépésben betöltjük a metaadatokat tartalmazó adattáblát, majd a .txt rész előtti törvényneveket tartjuk csak meg a létrehozott doc_id- oszlopban. A [^\\.]* regular expression itt a string elejétől indulva kijelöl mindent az elso . karakterig. A str_extract() pedig ezt a kijelölt string szakaszt (ami a törvények neve) menti át az új változónkba.

torveny_meta <- HunMineR::data_lawtext_meta

torveny_meta <- torveny_meta %>%
  mutate(doc_id = str_extract(txt_filename, "[^\\.]*")) %>%
  select(-txt_filename)

head(torveny_meta, 5)
#> # A tibble: 5 × 4
#>    year electoral_cycle majortopic doc_id     
#>   <dbl> <chr>                <dbl> <chr>      
#> 1  1998 1998-2002               13 1998XXXV   
#> 2  1998 1998-2002               20 1998XXXVI  
#> 3  1998 1998-2002                3 1998XXXVII 
#> 4  1998 1998-2002                6 1998XXXVIII
#> 5  1998 1998-2002               13 1998XXXIX

Végül összefűzzük a dokumentumokat és a metaadatokat tartalmazó data frame-eket.

torveny_final <- dplyr::left_join(torvenyek_tiszta, torveny_meta, by = "doc_id")

Majd létrehozzuk a korpuszt és ellenőrizzük azt.

torvenyek_corpus <- corpus(torveny_final)

head(summary(torvenyek_corpus), 5)
#>       Text Types Tokens Sentences year electoral_cycle majortopic
#> 1    1998L  2879   9628         1 1998       1998-2002          3
#> 2   1998LI   352    680         1 1998       1998-2002         20
#> 3  1998LII   446    992         1 1998       1998-2002          9
#> 4 1998LIII   126    221         1 1998       1998-2002          9
#> 5  1998LIV   835   2013         1 1998       1998-2002          9

Az RStudio environments fülén láthatjuk, hogy egy 1032 elemből álló korpusz jött létre, amelynek tartalmát a summary() paranccsal kiíratva, a console ablakban megjelenik a dokumentumok listája és a főbb leíró statisztikai adatok (egyedi szavak – types; szószám – tokens; mondatok – sentences). Az előbbi fejezettől eltérően most a tokenizálás során is végzünk még egy kis tisztítást: a felesleges stop szavakat kitöröljük a tokens_remove() és stopwords() kombinálásával. A quanteda tartalmaz egy beépített magyar stopszó szótárat. A második lépésben szótövesítjük a tokeneket a tokens_words() használatával, ami szintén képes a magyar nyelvű szövegeket kezelni.

Szükség esetén a beépített magyar nyelvű stopszó szótárat saját stopszavakkal is kiegészíthetjük. Példaként a HunMineR csomagban lévő kiegészítő stopszó data frame-t töltsük be.

custom_stopwords <- HunMineR::data_legal_stopwords

Mivel a korpusz ellenőrzése során találunk még olyan kifejezéseket, amelyeket el szeretnénk távolítani, ezeket is kiszűrjük.

custom_stopwords_egyeb <- c("lábjegyzet", "országgyűlés", "ülésnap")

Aztán pedig a korábban már megismert pipe operátor használatával elkészítjük a token objektumunkat. A szótövesített tokeneket egy külön objektumban tároljuk, mert gyakran előfordul, hogy később vissza kell térnünk az eredeti token objektumhoz, hogy egyéb műveleteket végezzünk el, például további stopszavakat távolítsunk el.

torvenyek_tokens <- tokens(torvenyek_corpus) %>%
  tokens_remove(stopwords("hungarian")) %>%
  tokens_remove(custom_stopwords) %>%
  tokens_remove(custom_stopwords_egyeb) %>%
  tokens_wordstem(language = "hun")

Végül eltávolítjuk a dokumentum-kifejezés mátrixból a túl gyakori kifejezéseket. A dfm_trim() függvénnyel a nagyon ritka és nagyon gyakori szavak megjelenését kontrollálhatjuk. Ha termfreq_type opció értéke “prop” (úgymint proportional) akkor 0 és 1.0 közötti értéket vehetnek fel a max_termfreq/docfreq és min_termfreq/docfreq paraméterek. A lenti példában azokat a tokeneket tartjuk meg, amelyek legalább egyszer előfordulnak ezer dokumentumonként (így kizárva a nagyon ritka kifejezéseket).

torvenyek_dfm <- dfm(torvenyek_tokens) %>%
  quanteda::dfm_trim(min_termfreq = 0.001, termfreq_type = "prop")

A szövegtisztító lépesek eredményét úgy ellenőrizhetjük, hogy szógyakorisági listát készítünk a korpuszban maradt kifejezésekről. Itt kihasználhatjuk a korpuszunkban lévő metaadatokat és megnézhetjük ciklus szerinti bontásban a szófrekvencia ábrát. Az ábránál figyeljünk arra, hogy a tidytext reorder_within() függvényét használjuk, ami egy nagyon hasznos megoldás a csoportosított sorrendbe rendezésre a ggplot2 ábránál (ld. 7.3. ábra).

top_tokens <- textstat_frequency(torvenyek_dfm, n = 15, groups = docvars(torvenyek_dfm,
    field = "electoral_cycle"))

tok_df <- ggplot(top_tokens, aes(reorder_within(feature, frequency, group), frequency)) +
    geom_point(aes(shape = group), size = 2) + coord_flip() + labs(x = NULL, y = "szófrekvenica") +
    facet_wrap(~group, nrow = 4, scales = "free") + theme(panel.spacing = unit(1,
    "lines")) + tidytext::scale_x_reordered() + theme(legend.position = "none")

ggplotly(tok_df, height = 1000, tooltip = "frequency")

Ábra 7.3: A 15 leggyakoribb token a korpuszban

A szövegtisztító lépéseket később újabbakkal is kiegészíthetjük, ha észrevesszük, hogy az elemzést zavaró tisztítási lépés maradt ki. Ilyen esetben tovább tisztíthatjuk a korpuszt, majd újra lefuttathatjuk az elemzést. Például, ha szükséges, további stopszavak eltávolítását is elvégezhetjük egy újabb stopszólista hozzáadásával. Ilyenkor ugyanúgy járunk el, mint az előző stopszólista esetén.

custom_stopwords2 <- HunMineR::data_legal_stopwords2

torvenyek_tokens_final <- torvenyek_tokens %>%
  tokens_remove(custom_stopwords2)

Ezután újra ellenőrizzük az eredményt.

torvenyek_dfm_final <- dfm(torvenyek_tokens_final) %>%
    dfm_trim(min_termfreq = 0.001, termfreq_type = "prop")

top_tokens_final <- textstat_frequency(torvenyek_dfm_final, n = 15, groups = docvars(torvenyek_dfm,
    field = "electoral_cycle"))

Ezt egy interaktív ábrán is megjelenítjük (ld. 7.4. ábra).

tokclean_df <- ggplot(top_tokens_final, aes(reorder_within(feature, frequency, group), frequency)) +
  geom_point(aes(shape = group), size = 2) +
  coord_flip() +
  labs(
    x = NULL,
    y = "szófrekvencia"
  ) +
  facet_wrap(~group, nrow = 2, scales = "free") +
  theme(panel.spacing = unit(1, "lines")) +
  tidytext::scale_x_reordered() +
  theme(legend.position = "none")

ggplotly(tokclean_df, height = 1000, tooltip = "frequency")

Ábra 7.4: A 15 leggyakoribb token a korpuszban, a bővített stop szó listával

A szövegtisztító és a korpusz előkészítő műveletek után következhet az LDA illesztése. Az alábbiakban az LDA illesztés két módszerét, a VEM-et és a Gibbs-et mutatjuk be. A modell mindkét módszer esetén ugyanaz, a különbség a következtetés módjában van. A VEM módszer variációs következtetés, míg a Gibbs mintavételen alapuló következtetés. (Blei, Ng, and Jordan 2003; Griffiths and Steyvers 2004; Phan, Nguyen, and Horiguchi 2008).

A két modell illesztése nagyon hasonló, meg kell adnunk az elemezni kívánt dfm nevét, majd a k értékét, ami egyenlő az általunk létrehozni kívánt topikok számával, ezt követően meg kell jelölnünk, hogy a VEM vagy a Gibbs módszert alkalmazzuk. A set.seed() funkció az R véletlen szám generátor magjának beállítására szolgál, ami ahhoz kell, hogy a kapott eredmények, ábrák stb. pontosan reprodukálhatóak legyenek. A set.seed() bármilyen tetszőleges egész szám lehet. Mivel az elemzésünk célja a két ciklus jogalkotásának összehasonlítása, a korpuszunkat két alkorpuszra bontjuk, ehhez a dokumentumok kormányzati ciklus azonosítóját használjuk fel. A dokumentum változók alapján a dfm_subset() parancs segítségével választjuk szét a már elkészült és a tisztított mátrixunkat.

dfm_98_02 <- dfm_subset(torvenyek_dfm_final, electoral_cycle == "1998-2002")

dfm_02_06 <- dfm_subset(torvenyek_dfm_final, electoral_cycle == "2002-2006")

7.3.1 A VEM módszer alkalmazása a magyar törvények korpuszán

Saját korpuszunkon először a VEM módszert alkalmazzuk, ahol k = 10, azaz a modell 10 témacsoportot alakít ki. Ahogyan korábban arról már volt szó, a k értékének meghatározása kutatói döntésen alapul, a modell futtatása során bevett gyakorlat a különböző k értékekkel való kísérletezés. Az elkészült modell kiértékelésére az elemzés elkészülte után a perplexity() függvény segítségével van lehetőségünk – ahol a theta az adott topikhoz való tartozás valószínűsége. A függvény a topikok által reprezentált elméleti szóeloszlásokat hasonlítja össze a szavak tényleges eloszlásával a dokumentumokban. A függvény értéke nem önmagában értelmezendő, hanem két modell összehasonlításában, ahol a legalacsonyabb perplexity (zavarosság) értékkel rendelkező modellt tekintik a legjobbnak.37 Az illusztráció kedvéért lefuttatunk 4 LDA modellt az 1998–2002-es kormányzati ciklushoz tartozó dfm-en. Az iterációhoz a purrr csomag map függvényét használtuk. Fontos megjegyezni, hogy minél nagyobb a korpuszunk, annál több számítási kapacitásra van szükség (és annál tovább tart a számítás).

k_topics <- c(5, 10, 15, 20)

lda_98_02 <- k_topics %>%
  purrr::map(topicmodels::LDA, x = dfm_98_02, control = list(seed = 1234))


perp_df <- dplyr::tibble(
  k = k_topics,
  perplexity = purrr::map_dbl(lda_98_02, topicmodels::perplexity)
)
perp_df <- ggplot(perp_df, aes(k, perplexity)) +
  geom_point() +
  geom_line() +
  labs(
    x = "Klaszterek száma",
    y = "Zavarosság"
  )

ggplotly(perp_df)

Ábra 7.5: Zavarosság változása a k függvényében

A zavarossági mutató alapján a 20 topikos modell szerepel a legjobban, de a megfelelő k kiválasztása a kutató kvalitatív döntésén múlik. Természetesen könnyen elképzelhető az is, hogy egy 40 topikos modellnél jelentősen kisebb zavarossági értéket kapjunk. Egy ilyen esetben mérlegelni kell, hogy a kapott csoportosítás kvalitatívan értelmezhető-e, illetve hogy tisztább-e annyival a kapott modell, mint amennyivel több idő és számítási energia a becslése. A zavarossági pontszám ehhez a kvalitatív döntéshez ad kvantitatív szempontokat, de érdemes általános sorvezetőként tekinteni rá, nem pedig mint egy áthághatatlan szabályra (ld. 7.5).38

A reprodukálhatóság és a futási sebesség érdekében a fejezet további részeiben a k paraméternek 10-es értéket adunk. Ezzel lefuttatunk egy-egy modellt a két ciklusra.

vem_98_02 <- LDA(dfm_98_02, k = 10, method = "VEM", control = list(seed = 1234))

vem_02_06 <- LDA(dfm_02_06, k = 10, method = "VEM", control = list(seed = 1234))

Ezt követően a modell által létrehozott topikokat tidy formátumba tesszük és egyesítjük egy adattáblában.39

topics_98_02 <- tidytext::tidy(vem_98_02, matrix = "beta") %>%
  mutate(electoral_cycle = "1998-2002")

topics_02_06 <- tidytext::tidy(vem_02_06, matrix = "beta") %>%
  mutate(electoral_cycle = "2002-2006")

lda_vem <- dplyr::bind_rows(topics_98_02, topics_02_06)

Ezután listázzuk az egyes topikokhoz tartozó leggyakoribb kifejezéseket.

top_terms <- lda_vem %>%
  group_by(electoral_cycle, topic) %>%
  top_n(5, beta) %>%
  top_n(5, term) %>%
  ungroup() %>%
  arrange(topic, -beta)

Végül a ggplot2 csomag segítségével ábrán is megjeleníthetjük az egyes topikok 10 legfontosabb kifejezését (ld. 7.6. és 7.7. ábra).

toptermsplot9802vem <- top_terms %>%
  filter(electoral_cycle == "1998-2002") %>%
  ggplot(aes(reorder_within(term, beta, topic), beta)) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~topic, scales = "free", ncol = 1) +
  theme(panel.spacing = unit(4, "lines")) +
  coord_flip() +
  labs(
    x = NULL,
    y = NULL
  ) +
  tidytext::scale_x_reordered()

ggplotly(toptermsplot9802vem, tooltip = "beta")

Ábra 7.6: 1998–2002-es ciklus: topikok és kifejezések (VEM mintavételezéssel)

toptermsplot0206vem <- top_terms %>%
  filter(electoral_cycle == "2002-2006") %>%
  ggplot(aes(reorder_within(term, beta, topic), beta)) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~topic, scales = "free", ncol = 1) +
  theme(panel.spacing = unit(4, "lines")) +
  coord_flip() +
  labs(
    x = NULL,
    y = NULL
  ) +
  tidytext::scale_x_reordered()

ggplotly(toptermsplot0206vem, tooltip = "beta")

Ábra 7.7: 2002–2006-os ciklus topikok és kifejezések (VEM mintavételezéssel)

7.3.2 Az LDA Gibbs módszer alkalmazása a magyar törvények korpuszán

A következőkben ugyanazon a korpuszon az LDA Gibbs módszert alkalmazzuk. A szövegelőkészítő és tisztító lépések ennél a módszernél is ugyanazok, mint a fentebb bemutatott VEM módszer esetében, így itt most csak a modell illesztését mutatjuk be.

gibbs_98_02 <- LDA(dfm_98_02, k = 10, method = "Gibbs", control = list(seed = 1234))

gibbs_02_06 <- LDA(dfm_02_06, k = 10, method = "Gibbs", control = list(seed = 1234))

Itt is elvégezzük a topikok tidy formátumra alakítását.

topics_g98_02 <- tidy(gibbs_98_02, matrix = "beta") %>%
  mutate(electoral_cycle = "1998-2002")

topics_g02_06 <- tidy(gibbs_02_06, matrix = "beta") %>%
  mutate(electoral_cycle = "2002-2006")

lda_gibbs <- bind_rows(topics_g98_02, topics_g02_06)

Majd listázzuk az egyes topikokhoz tartozó leggyakoribb kifejezéseket.

top_terms_gibbs <- lda_gibbs %>%
  group_by(electoral_cycle, topic) %>%
  top_n(5, beta) %>%
  top_n(5, term) %>%
  ungroup() %>%
  arrange(topic, -beta)

Ezután a ggplot2 csomag segítségével ábrán is megjeleníthetjük (ld. 7.8. és @ref(fig:Gibbs0206 ábra).

toptermsplot9802gibbs <- top_terms_gibbs %>%
  filter(electoral_cycle == "1998-2002") %>%
  ggplot(aes(reorder_within(term, beta, topic), beta)) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~topic, scales = "free", ncol = 1) +
  theme(panel.spacing = unit(4, "lines")) +
  coord_flip() +
  labs(
    title = ,
    x = NULL,
    y = NULL
  ) +
  tidytext::scale_x_reordered()

ggplotly(toptermsplot9802gibbs, tooltip = "beta")

Ábra 7.8: 1998–2002-es ciklus topikok és kifejezések (Gibbs mintavétellel)

toptermsplot0206gibbs <- top_terms_gibbs %>%
  filter(electoral_cycle == "2002-2006") %>%
  ggplot(aes(reorder_within(term, beta, topic), beta)) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~topic, scales = "free", ncol = 1) +
  theme(panel.spacing = unit(4, "lines")) +
  coord_flip() +
  labs(
    x = NULL,
    y = NULL
  ) +
  scale_x_reordered()

ggplotly(toptermsplot0206gibbs, tooltip = "beta")

Ábra 7.9: 2002–2006-os ciklus topikok és kifejezések (Gibbs mintavétellel)

7.4 Strukturális topikmodellek

A kvantitatív szövegelemzés elterjedésével együtt megjelentek a módszertani innovációk is. Roberts et al. (2014) kiváló cikkben mutatták be a strukturális topikmodelleket (structural topic models – stm), amelyek fő újítása, hogy a dokumentumok metaadatai kovariánsként40 tudják befolyásolni, hogy egy-egy kifejezés mekkora valószínűséggel lesz egy-egy téma része. A kovariánsok egyrészről megmagyarázhatják, hogy egy-egy dokumentum mennyire függ össze egy-egy témával (topical prevalence), illetve hogy egy-egy szó mennyire függ össze egy-egy témán belül (topical content).

Az stm modell becslése során mindkét típusú kovariánst használhatjuk, illetve ha nem adunk meg dokumentum metaadatot, akkor az stm csomag stm függvénye a Korrelált Topic Modell-t fogja becsülni.

Az stm modelleket az R-ben az stm csomaggal tudjuk kivitelezni. A csomag fejlesztői között van a módszer kidolgozója is, ami nem ritka az R csomagok esetében.

A lenti lépésekben a csomag dokumentációjában szereplő ajánlásokat követjük, habár a könyv írásakor a stm már képes volt a quanteda-ban létrehozott dfm-ek kezelésére is. A kiinduló adatbázisunk a törvény_final, amit a fejezet elején hoztunk létre a dokumentumokból és a metaadatokból. A javasolt munkafolyamat a textProcessor() függvény használatával indul, ami szintén tartalmazza az alap szöveg előkészítési lépéseket. Az egyszerűség és a futási sebesség érdekében itt most ezek többségétől eltekintünk, mivel a fejezet korábbi részeiben részletesen tárgyaltuk őket.

Az előkészítés utolsó szakaszában az out objektumban tároljuk el a dokumentumokat, az egyedi szavakat, illetve a metaadatokat (kovariánsokat).

data_stm <- torveny_final

processed_stm <- stm::textProcessor(
  torveny_final$text,
  metadata = torveny_final,
  lowercase = FALSE,
  removestopwords = FALSE,
  removenumbers = FALSE,
  removepunctuation = FALSE,
  ucp = FALSE,
  stem = TRUE,
  language = "hungarian",
  verbose = FALSE
)

out <- stm::prepDocuments(processed_stm$documents, processed_stm$vocab, processed_stm$meta)
#> Removing 96264 of 180243 terms (96264 of 1252793 tokens) due to frequency 
#> Your corpus now has 1032 documents, 83979 terms and 1156529 tokens.

A strukturális topikmodellünket az stm függvénnyel becsüljük és a kovariánsokat a prevalence opciónál tudjuk formulaként megadni. A lenti példában a Hungarian Comparative Agendas Project41 kategóriáit (például gazdaság, egészségügy stb.) és a kormányciklusokat használjuk. A futási idő kicsit hosszabb mint az LDA modellek esetében.

stm_fit <- stm::stm(
  out$documents,
  out$vocab,
  K = 10,
  prevalence = ~ majortopic + electoral_cycle,
  data = out$meta,
  init.type = "Spectral",
  seed = 1234,
  verbose = FALSE
)

Amennyiben a kutatási kérdés megkívánja, akkor megvizsgálhatjuk, hogy a kategorikus változóinknak milyen hatása volt az egyes topikok esetében. Ehhez az estimateEffect() függvénnyel lefuttatunk egy lineáris regressziót és a summary() használatával láthatjuk az egyes kovariánsok koefficienseit. Itt az első topikkal illusztráljuk az eredményt, ami azt mutatja, hogy (a kategórikus változóink első kategóriájához mérten) statisztikailag szignifikáns mind a téma, mind pedig a kormányzati ciklusok abban, hogy egyes dokumentumok milyen témákból épülnek fel.

out$meta$electoral_cycle <- as.factor(out$meta$electoral_cycle)
out$meta$majortopic <- as.factor(out$meta$majortopic)

cov_estimate <- stm::estimateEffect(1:10 ~ majortopic + electoral_cycle, stm_fit,
    meta = out$meta, uncertainty = "Global")

summary(cov_estimate, topics = 1)
#> 
#> Call:
#> stm::estimateEffect(formula = 1:10 ~ majortopic + electoral_cycle, 
#>     stmobj = stm_fit, metadata = out$meta, uncertainty = "Global")
#> 
#> 
#> Topic 1:
#> 
#> Coefficients:
#>                          Estimate Std. Error t value Pr(>|t|)    
#> (Intercept)                0.3035     0.0310    9.79  < 2e-16 ***
#> majortopic2               -0.2042     0.0676   -3.02  0.00260 ** 
#> majortopic3               -0.2046     0.0595   -3.44  0.00061 ***
#> majortopic4               -0.2213     0.0589   -3.76  0.00018 ***
#> majortopic5                0.1026     0.0471    2.18  0.02977 *  
#> majortopic6               -0.2232     0.0587   -3.80  0.00015 ***
#> majortopic7               -0.1558     0.0681   -2.29  0.02244 *  
#> majortopic8               -0.2158     0.0729   -2.96  0.00313 ** 
#> majortopic9                0.5252     0.0877    5.99  2.9e-09 ***
#> majortopic10              -0.1089     0.0549   -1.98  0.04745 *  
#> majortopic12              -0.1742     0.0406   -4.29  2.0e-05 ***
#> majortopic13              -0.1359     0.0560   -2.43  0.01534 *  
#> majortopic14              -0.2174     0.0755   -2.88  0.00408 ** 
#> majortopic15              -0.1476     0.0427   -3.46  0.00057 ***
#> majortopic16              -0.0960     0.0531   -1.81  0.07094 .  
#> majortopic17              -0.2246     0.0580   -3.87  0.00012 ***
#> majortopic18               0.2105     0.0572    3.68  0.00025 ***
#> majortopic19               0.0736     0.0513    1.44  0.15120    
#> majortopic20              -0.2107     0.0392   -5.37  9.8e-08 ***
#> majortopic21              -0.2250     0.0697   -3.23  0.00129 ** 
#> majortopic23              -0.1672     0.0939   -1.78  0.07546 .  
#> electoral_cycle2002-2006  -0.1036     0.0210   -4.94  9.0e-07 ***
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Az LDA modelleknél már bemutatott munkafolyamat az stm modellünk esetében is alkalmazható, hogy vizuálisan is megjelenítsük az eredményeinket. A tidy() függvény data frammé alakítja az stm objektumot, amit aztán a már ismerős dplyr csomagban lévő függvényekkel tudunk átalakítani és végül vizualizálni a ggplot2 csomaggal. A 7.10-es ábrán az egyes témákhoz tartozó 5 legvalószínűbb szót mutatjuk be.

tidy_stm <- tidytext::tidy(stm_fit)
topicplot <- tidy_stm %>%
  group_by(topic) %>%
  top_n(5, beta) %>%
  ungroup() %>%
  mutate(
    topic = paste0("Topic ", topic),
    term = reorder_within(term, beta, topic)
  ) %>%
  ggplot(aes(term, beta)) +
  geom_col() +
  facet_wrap(~topic, scales = "free_y", ncol = 1) +
  theme(panel.spacing = unit(4, "lines")) +
  coord_flip() +
  scale_x_reordered() +
  labs(
    x = NULL,
    y = NULL
  )

ggplotly(topicplot, tooltip = "beta")

Ábra 7.10: Topikonkénti legmagasabb valószínűségű szavak

Egy-egy topichoz tartozó meghatározó szavak annak függvényében változhatnak, hogy milyen algoritmust használunk. A labelTopics() függvény a már becsült stm modellünket alapul véve kínál négyféle alternatív opciót. Az egyes algoritmusok részletes magyarázatáért érdemes elolvasni a csomag részletes leírását.42

stm::labelTopics(stm_fit, c(1:2))
#> Topic 1 Top Words:
#>       Highest Prob: szerződő, vagi, egyezméni, fél, államban, nem, másik 
#>       FREX: megadóztatható, haszonhúzója, beruházóinak, segélycsapatok, adóztatást, jövedelemadók, kijelölések 
#>       Lift: felségterületén, haszonhúzója, abbottifalakó, abertiehető, abyssinicus, achátcsigákcamaenidaepapustyla, adalbertiibériai 
#>       Score: szerződő, államban, illetőségű, egyezméni, megadóztatható, adóztatható, cikka 
#> Topic 2 Top Words:
#>       Highest Prob: működési, célú, támogatások, költségvetésegyéb, felhalmozási, terhelő, beruházási 
#>       FREX: kiadásokfelújításegyéb, kiadásokintézményi, kiadásokközponti, költségvetésfelhalmozási, kiadásokkormányzati, felújításegyéb, rek 
#>       Lift: a+b+c, a+b+c+d, adago, adódóa, adósságállományából, adósságrendezésr, adótartozásának 
#>       Score: költségvetésegyéb, kiadásokfelhalmozási, költségvetésszemélyi, járulékokdolog, költségvetésintézményi, kiadásokegyéb, juttatásokmunkaadókat

A korpuszunkon belüli témák megoszlását a plot.STM()-el tudjuk ábrázolni. Jól látszik, hogy a Topic 6-ba tartozó szavak vannak jelen a legnagyobb arányban a dokumentumaink között.

stm::plot.STM(stm_fit,
         "summary", 
         main = "", 
         labeltype = "frex", 
         xlab = "Várható topic arányok",
         xlim=c(0,1)
         )

Ábra 7.11: Leggyakoribb témák és kifejezések

Leggyakoribb témák és kifejezések

Végezetül a témák közötti korrelációt a topicCorr() függvénnyel becsülhetjük és az igraph csomagot betöltve a plot() paranccsal tudjuk vizualizálni. Az eredmény egy hálózat lesz, amit gráfként ábrázolunk. A gráfok élei a témák közötti összefüggést (korrelációt) jelölik. A 7.12-es ábrán a 10 topikos modellünket látjuk, azonban ilyen kis k esetén nem látunk jelentős korrelációs kapcsolatot, csak a 4-es és 1-es témák között. A korrelációs gráfok jellemzően hasznosabbak, hogyha a nagy témaszám miatt (több száz, vagy akár több ezer) egy magasabb absztrakciós szinten szeretnénk vizsgálni az eredményeinket.

plot(stm::topicCorr(stm_fit))

Ábra 7.12: Témák közötti korreláció hálózat

Témák közötti korreláció hálózat

  1. A kód részben az alábbiakon alapul: tidytextmining.com/topicmodeling.html. Az általunk is használt topicmodels csomag interfészt biztosít az LDA modellek és a korrelált témamodellek (CTM) C kódjához, valamint az LDA modellek illesztéséhez szükséges C ++ kódhoz.↩︎

  2. A törvényeket és a metaadatokat tartalmazó adatbázisokat regisztációt követően a Hungarian Comparative Agendas Projekt honlapjáról https://cap.tk.hu/ lehet letölteni.↩︎

  3. Részletesebben lásd például: http://brooksandrew.github.io/simpleblog/articles/latent-dirichlet-allocation-under-the-hood/↩︎

  4. A ldatuning csomagban további indikátor implementációja található, ami a perplexityhez hasonlóan minimalizálásra (Arun et al. 2010; Cao et al. 2009), illetve maximalizálásra alapoz (Deveaud, SanJuan, and Bellot 2014; Griffiths and Steyvers 2004)↩︎

  5. A tidy formátumról bővebben: https://cran.r-project.org/web/packages/tidyr/vignettes/tidy-data.html↩︎

  6. A kovariancia megadja két egymástól különböző változó együttmozgását. Kis értékei gyenge, nagy értékei erős lineáris összefüggésre utalnak.↩︎

  7. A kódkönyv elérhető az alábbi linken: Comparative Agendas Project.↩︎

  8. Az stm csomaghoz tartozó leírás: https://cran.r-project.org/web/packages/stm/vignettes/stmVignette.pdf↩︎