diff --git a/_bookdown_files/_main_files/figure-html/OGYnevelem-1.png b/_bookdown_files/_main_files/figure-html/OGYnevelem-1.png index b5f5404..ddc2c00 100644 Binary files a/_bookdown_files/_main_files/figure-html/OGYnevelem-1.png and b/_bookdown_files/_main_files/figure-html/OGYnevelem-1.png differ diff --git a/_bookdown_files/_main_files/figure-html/csoportositas-1.png b/_bookdown_files/_main_files/figure-html/csoportositas-1.png index 75a49ff..9cc9231 100644 Binary files a/_bookdown_files/_main_files/figure-html/csoportositas-1.png and b/_bookdown_files/_main_files/figure-html/csoportositas-1.png differ diff --git a/_bookdown_files/_main_files/figure-html/eiffel-1.png b/_bookdown_files/_main_files/figure-html/eiffel-1.png index 2fc45c5..6a2f3e9 100644 Binary files a/_bookdown_files/_main_files/figure-html/eiffel-1.png and b/_bookdown_files/_main_files/figure-html/eiffel-1.png differ diff --git a/_bookdown_files/_main_files/figure-html/fontoskifejezes-1.png b/_bookdown_files/_main_files/figure-html/fontoskifejezes-1.png index 4c7d44c..334afe1 100644 Binary files a/_bookdown_files/_main_files/figure-html/fontoskifejezes-1.png and b/_bookdown_files/_main_files/figure-html/fontoskifejezes-1.png differ diff --git a/_bookdown_files/_main_files/figure-html/pozicio-1.png b/_bookdown_files/_main_files/figure-html/pozicio-1.png index 54cf92a..7f1a693 100644 Binary files a/_bookdown_files/_main_files/figure-html/pozicio-1.png and b/_bookdown_files/_main_files/figure-html/pozicio-1.png differ diff --git a/_bookdown_files/_main_files/figure-html/unnamed-chunk-16-1.png b/_bookdown_files/_main_files/figure-html/unnamed-chunk-16-1.png index c161f7c..123c04f 100644 Binary files a/_bookdown_files/_main_files/figure-html/unnamed-chunk-16-1.png and b/_bookdown_files/_main_files/figure-html/unnamed-chunk-16-1.png differ diff --git a/_bookdown_files/_main_files/figure-html/unnamed-chunk-17-1.png b/_bookdown_files/_main_files/figure-html/unnamed-chunk-17-1.png index ea371a3..5e2b7d1 100644 Binary files a/_bookdown_files/_main_files/figure-html/unnamed-chunk-17-1.png and b/_bookdown_files/_main_files/figure-html/unnamed-chunk-17-1.png differ diff --git a/_bookdown_files/_main_files/figure-html/unnamed-chunk-20-1.png b/_bookdown_files/_main_files/figure-html/unnamed-chunk-20-1.png index 9a67b83..28ae3af 100644 Binary files a/_bookdown_files/_main_files/figure-html/unnamed-chunk-20-1.png and b/_bookdown_files/_main_files/figure-html/unnamed-chunk-20-1.png differ diff --git a/_bookdown_files/_main_files/figure-html/unnamed-chunk-21-1.png b/_bookdown_files/_main_files/figure-html/unnamed-chunk-21-1.png index 20070a3..cfbcc3e 100644 Binary files a/_bookdown_files/_main_files/figure-html/unnamed-chunk-21-1.png and b/_bookdown_files/_main_files/figure-html/unnamed-chunk-21-1.png differ diff --git a/_bookdown_files/_main_files/figure-html/unnamed-chunk-251-1.png b/_bookdown_files/_main_files/figure-html/unnamed-chunk-251-1.png index 26fcc73..c3f1484 100644 Binary files a/_bookdown_files/_main_files/figure-html/unnamed-chunk-251-1.png and b/_bookdown_files/_main_files/figure-html/unnamed-chunk-251-1.png differ diff --git a/_bookdown_files/_main_files/figure-html/unnamed-chunk-266-1.png b/_bookdown_files/_main_files/figure-html/unnamed-chunk-266-1.png index 6df8f0e..d88dbf5 100644 Binary files a/_bookdown_files/_main_files/figure-html/unnamed-chunk-266-1.png and b/_bookdown_files/_main_files/figure-html/unnamed-chunk-266-1.png differ diff --git a/_bookdown_files/_main_files/figure-html/unnamed-chunk-267-1.png b/_bookdown_files/_main_files/figure-html/unnamed-chunk-267-1.png index 4c7980a..c2bdb36 100644 Binary files a/_bookdown_files/_main_files/figure-html/unnamed-chunk-267-1.png and b/_bookdown_files/_main_files/figure-html/unnamed-chunk-267-1.png differ diff --git a/_bookdown_files/_main_files/figure-html/unnamed-chunk-268-1.png b/_bookdown_files/_main_files/figure-html/unnamed-chunk-268-1.png index b6358a1..bf5c246 100644 Binary files a/_bookdown_files/_main_files/figure-html/unnamed-chunk-268-1.png and b/_bookdown_files/_main_files/figure-html/unnamed-chunk-268-1.png differ diff --git a/_bookdown_files/_main_files/figure-html/unnamed-chunk-269-1.png b/_bookdown_files/_main_files/figure-html/unnamed-chunk-269-1.png index c983891..3737050 100644 Binary files a/_bookdown_files/_main_files/figure-html/unnamed-chunk-269-1.png and b/_bookdown_files/_main_files/figure-html/unnamed-chunk-269-1.png differ diff --git a/_bookdown_files/_main_files/figure-html/unnamed-chunk-270-1.png b/_bookdown_files/_main_files/figure-html/unnamed-chunk-270-1.png index 4474e15..5963d5a 100644 Binary files a/_bookdown_files/_main_files/figure-html/unnamed-chunk-270-1.png and b/_bookdown_files/_main_files/figure-html/unnamed-chunk-270-1.png differ diff --git a/_bookdown_files/_main_files/figure-html/unnamed-chunk-271-1.png b/_bookdown_files/_main_files/figure-html/unnamed-chunk-271-1.png index 8efe949..3df3c73 100644 Binary files a/_bookdown_files/_main_files/figure-html/unnamed-chunk-271-1.png and b/_bookdown_files/_main_files/figure-html/unnamed-chunk-271-1.png differ diff --git a/_bookdown_files/_main_files/figure-html/unnamed-chunk-272-1.png b/_bookdown_files/_main_files/figure-html/unnamed-chunk-272-1.png index 93a36ed..fcfdf9a 100644 Binary files a/_bookdown_files/_main_files/figure-html/unnamed-chunk-272-1.png and b/_bookdown_files/_main_files/figure-html/unnamed-chunk-272-1.png differ diff --git a/_bookdown_files/_main_files/figure-html/unnamed-chunk-273-1.png b/_bookdown_files/_main_files/figure-html/unnamed-chunk-273-1.png index c30359a..81525e3 100644 Binary files a/_bookdown_files/_main_files/figure-html/unnamed-chunk-273-1.png and b/_bookdown_files/_main_files/figure-html/unnamed-chunk-273-1.png differ diff --git a/_bookdown_files/_main_files/figure-html/unnamed-chunk-274-1.png b/_bookdown_files/_main_files/figure-html/unnamed-chunk-274-1.png index f239e8f..f89e7fc 100644 Binary files a/_bookdown_files/_main_files/figure-html/unnamed-chunk-274-1.png and b/_bookdown_files/_main_files/figure-html/unnamed-chunk-274-1.png differ diff --git a/_bookdown_files/_main_files/figure-html/unnamed-chunk-275-1.png b/_bookdown_files/_main_files/figure-html/unnamed-chunk-275-1.png index 5f5689a..f5ae928 100644 Binary files a/_bookdown_files/_main_files/figure-html/unnamed-chunk-275-1.png and b/_bookdown_files/_main_files/figure-html/unnamed-chunk-275-1.png differ diff --git a/docs/404.html b/docs/404.html index 605d5f5..5101f8c 100644 --- a/docs/404.html +++ b/docs/404.html @@ -6,7 +6,7 @@ Page not found | Szövegbányászat és mesterséges intelligencia R-ben - + @@ -55,7 +55,7 @@ - + diff --git a/docs/_main_files/figure-html/OGYnevelem-1.png b/docs/_main_files/figure-html/OGYnevelem-1.png index b5f5404..ddc2c00 100644 Binary files a/docs/_main_files/figure-html/OGYnevelem-1.png and b/docs/_main_files/figure-html/OGYnevelem-1.png differ diff --git a/docs/_main_files/figure-html/csoportositas-1.png b/docs/_main_files/figure-html/csoportositas-1.png index 75a49ff..9cc9231 100644 Binary files a/docs/_main_files/figure-html/csoportositas-1.png and b/docs/_main_files/figure-html/csoportositas-1.png differ diff --git a/docs/_main_files/figure-html/eiffel-1.png b/docs/_main_files/figure-html/eiffel-1.png index 2fc45c5..6a2f3e9 100644 Binary files a/docs/_main_files/figure-html/eiffel-1.png and b/docs/_main_files/figure-html/eiffel-1.png differ diff --git a/docs/_main_files/figure-html/fontoskifejezes-1.png b/docs/_main_files/figure-html/fontoskifejezes-1.png index 4c7d44c..334afe1 100644 Binary files a/docs/_main_files/figure-html/fontoskifejezes-1.png and b/docs/_main_files/figure-html/fontoskifejezes-1.png differ diff --git a/docs/_main_files/figure-html/pozicio-1.png b/docs/_main_files/figure-html/pozicio-1.png index 54cf92a..7f1a693 100644 Binary files a/docs/_main_files/figure-html/pozicio-1.png and b/docs/_main_files/figure-html/pozicio-1.png differ diff --git a/docs/_main_files/figure-html/unnamed-chunk-16-1.png b/docs/_main_files/figure-html/unnamed-chunk-16-1.png index c161f7c..123c04f 100644 Binary files a/docs/_main_files/figure-html/unnamed-chunk-16-1.png and b/docs/_main_files/figure-html/unnamed-chunk-16-1.png differ diff --git a/docs/_main_files/figure-html/unnamed-chunk-17-1.png b/docs/_main_files/figure-html/unnamed-chunk-17-1.png index ea371a3..5e2b7d1 100644 Binary files a/docs/_main_files/figure-html/unnamed-chunk-17-1.png and b/docs/_main_files/figure-html/unnamed-chunk-17-1.png differ diff --git a/docs/_main_files/figure-html/unnamed-chunk-20-1.png b/docs/_main_files/figure-html/unnamed-chunk-20-1.png index 9a67b83..28ae3af 100644 Binary files a/docs/_main_files/figure-html/unnamed-chunk-20-1.png and b/docs/_main_files/figure-html/unnamed-chunk-20-1.png differ diff --git a/docs/_main_files/figure-html/unnamed-chunk-21-1.png b/docs/_main_files/figure-html/unnamed-chunk-21-1.png index 20070a3..cfbcc3e 100644 Binary files a/docs/_main_files/figure-html/unnamed-chunk-21-1.png and b/docs/_main_files/figure-html/unnamed-chunk-21-1.png differ diff --git a/docs/_main_files/figure-html/unnamed-chunk-251-1.png b/docs/_main_files/figure-html/unnamed-chunk-251-1.png index 26fcc73..c3f1484 100644 Binary files a/docs/_main_files/figure-html/unnamed-chunk-251-1.png and b/docs/_main_files/figure-html/unnamed-chunk-251-1.png differ diff --git a/docs/_main_files/figure-html/unnamed-chunk-266-1.png b/docs/_main_files/figure-html/unnamed-chunk-266-1.png index 6df8f0e..d88dbf5 100644 Binary files a/docs/_main_files/figure-html/unnamed-chunk-266-1.png and b/docs/_main_files/figure-html/unnamed-chunk-266-1.png differ diff --git a/docs/_main_files/figure-html/unnamed-chunk-267-1.png b/docs/_main_files/figure-html/unnamed-chunk-267-1.png index 4c7980a..c2bdb36 100644 Binary files a/docs/_main_files/figure-html/unnamed-chunk-267-1.png and b/docs/_main_files/figure-html/unnamed-chunk-267-1.png differ diff --git a/docs/_main_files/figure-html/unnamed-chunk-268-1.png b/docs/_main_files/figure-html/unnamed-chunk-268-1.png index b6358a1..bf5c246 100644 Binary files a/docs/_main_files/figure-html/unnamed-chunk-268-1.png and b/docs/_main_files/figure-html/unnamed-chunk-268-1.png differ diff --git a/docs/_main_files/figure-html/unnamed-chunk-269-1.png b/docs/_main_files/figure-html/unnamed-chunk-269-1.png index c983891..3737050 100644 Binary files a/docs/_main_files/figure-html/unnamed-chunk-269-1.png and b/docs/_main_files/figure-html/unnamed-chunk-269-1.png differ diff --git a/docs/_main_files/figure-html/unnamed-chunk-270-1.png b/docs/_main_files/figure-html/unnamed-chunk-270-1.png index 4474e15..5963d5a 100644 Binary files a/docs/_main_files/figure-html/unnamed-chunk-270-1.png and b/docs/_main_files/figure-html/unnamed-chunk-270-1.png differ diff --git a/docs/_main_files/figure-html/unnamed-chunk-271-1.png b/docs/_main_files/figure-html/unnamed-chunk-271-1.png index 8efe949..3df3c73 100644 Binary files a/docs/_main_files/figure-html/unnamed-chunk-271-1.png and b/docs/_main_files/figure-html/unnamed-chunk-271-1.png differ diff --git a/docs/_main_files/figure-html/unnamed-chunk-272-1.png b/docs/_main_files/figure-html/unnamed-chunk-272-1.png index 93a36ed..fcfdf9a 100644 Binary files a/docs/_main_files/figure-html/unnamed-chunk-272-1.png and b/docs/_main_files/figure-html/unnamed-chunk-272-1.png differ diff --git a/docs/_main_files/figure-html/unnamed-chunk-273-1.png b/docs/_main_files/figure-html/unnamed-chunk-273-1.png index c30359a..81525e3 100644 Binary files a/docs/_main_files/figure-html/unnamed-chunk-273-1.png and b/docs/_main_files/figure-html/unnamed-chunk-273-1.png differ diff --git a/docs/_main_files/figure-html/unnamed-chunk-274-1.png b/docs/_main_files/figure-html/unnamed-chunk-274-1.png index f239e8f..f89e7fc 100644 Binary files a/docs/_main_files/figure-html/unnamed-chunk-274-1.png and b/docs/_main_files/figure-html/unnamed-chunk-274-1.png differ diff --git a/docs/_main_files/figure-html/unnamed-chunk-275-1.png b/docs/_main_files/figure-html/unnamed-chunk-275-1.png index 5f5689a..f5ae928 100644 Binary files a/docs/_main_files/figure-html/unnamed-chunk-275-1.png and b/docs/_main_files/figure-html/unnamed-chunk-275-1.png differ diff --git a/docs/adatkezeles.html b/docs/adatkezeles.html index 46d8409..bf64d5c 100644 --- a/docs/adatkezeles.html +++ b/docs/adatkezeles.html @@ -6,7 +6,7 @@ 3 Adatkezelés R-ben | Szövegbányászat és mesterséges intelligencia R-ben - + @@ -55,7 +55,7 @@ - + diff --git a/docs/alapfogalmak.html b/docs/alapfogalmak.html index 4e2645d..f3dcb03 100644 --- a/docs/alapfogalmak.html +++ b/docs/alapfogalmak.html @@ -6,7 +6,7 @@ 2 Kulcsfogalmak | Szövegbányászat és mesterséges intelligencia R-ben - + @@ -55,7 +55,7 @@ - + diff --git a/docs/corpus_ch.html b/docs/corpus_ch.html index 4755f5d..ad7d3c1 100644 --- a/docs/corpus_ch.html +++ b/docs/corpus_ch.html @@ -6,7 +6,7 @@ 4 Korpuszépítés és szövegelőkészítés | Szövegbányászat és mesterséges intelligencia R-ben - + @@ -55,7 +55,7 @@ - + @@ -338,13 +338,13 @@

4.1 Szövegbeszerzésr #> {html_document} #> <html lang="hu" class="no-js"> -#> [1] <head>\n<meta http-equiv="Content-Type" content="text/html; charset ... -#> [2] <body class="index">\n\n\t<script>\n\t (function(i,s,o,g,r,a,m){i[ ... +#> [1] <head>\n<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">\n<meta charset="utf-8">\n<meta http-equiv="X-UA-C ... +#> [2] <body class="index">\n\n\t<script>\n\t (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){\n\t (i[ ...

Ezután a html_nodes() függvény argumentumaként meg kell adnunk azt a HTML címkét vagy CSS azonosítót, ami a legyűjteni kívánt elemeket azonosítja a weboldalon. [^websraping] Ezeket az azonosítókat az adott weboldal forráskódjának megtekintésével tudhatjuk meg, amire a különböző böngészők különböző lehetőségeket kínálnak. Majd a html_text() függvény segítségével megkapjuk azokat a szövegeket, amelyek az adott weblapon az adott azonosítóval rendelkeznek.

Példánkban a https://poltextlab.tk.hu/hu weboldalról azokat az információkat szeretnénk kigyűjteni, amelyek az <title> címke alatt szerepelnek.

title <- read_html("https://poltextlab.tk.hu/hu") %>%
-  rvest::html_nodes("title") %>%
-  rvest::html_text()
+    rvest::html_nodes("title") %>%
+    rvest::html_text()
 
 title
 #> [1] "MTA TK Political and Legal Text Mining and Artificial Intelligence Laboratory (poltextLAB)"
@@ -411,11 +411,9 @@

4.2.1 Tokenizálás, szótövezé toks1 #> Tokens consisting of 1 document. #> text1 : -#> [1] "Példa" "az" "előkészítés" -#> [4] "Az" "előkészítés" "a" -#> [7] "szövetisztítással" "kell" "megkezdenünk" -#> [10] "Az" "előkészítés" "korpuszon" -#> [ ... and 2 more ] +#> [1] "Példa" "az" "előkészítés" "Az" "előkészítés" "a" +#> [7] "szövetisztítással" "kell" "megkezdenünk" "Az" "előkészítés" "korpuszon" +#> [ ... and 2 more ]

A fenti text1 objektumban tárolt szöveg szótövezését az alábbiak szerint tudjuk elvégezni. Megvizsgálva az előkészítés különböző alakjainak lemmatizált és stemmelt változatát jól láthatjuk a két módszer közötti különbséget.

text1 <- "Példa az előkészítésre. Az előkészítést a szövetisztítással kell megkezdenünk. Az előkészített korpuszon elemzést végzünk"
 
@@ -426,11 +424,9 @@ 

4.2.1 Tokenizálás, szótövezé toks2 #> Tokens consisting of 1 document. #> text1 : -#> [1] "Példa" "az" "előkészítésr" -#> [4] "Az" "előkészítést" "a" -#> [7] "szövetisztításs" "kell" "megkezdenünk" -#> [10] "Az" "előkészített" "korpuszon" -#> [ ... and 2 more ]

+#> [1] "Példa" "az" "előkészítésr" "Az" "előkészítést" "a" "szövetisztításs" +#> [8] "kell" "megkezdenünk" "Az" "előkészített" "korpuszon" +#> [ ... and 2 more ]

4.2.2 Dokumentum kifejezés mátrix (dtm, dfm)

@@ -479,22 +475,18 @@

4.2.3 Súlyozás dfm()

A topfeatures() függvény segítségével megnézhetjük a mátrix leggyakoribb szavait, a függvény argumentumában megadva a dokumentum kifejezés mátrix nevét és a kívánt kifejezésszámot.

quanteda::topfeatures(lawtext_dfm, 15)
-#>         the    bekezdés          of         áll    szerződő rendelkezés 
-#>        7942        5877        5666        4267        3620        3403 
-#>     törvény          to        hely          an     személy          év 
-#>        3312        3290        3114        3068        3048        2975 
-#>      kiadás           b          ha 
-#>        2884        2831        2794
+#> the bekezdés of áll szerződő rendelkezés törvény to hely an személy +#> 7942 5877 5666 4267 3620 3403 3312 3290 3114 3068 3048 +#> év kiadás b ha +#> 2975 2884 2831 2794

Mivel látható, hogy a szövegekben sok angol kifejezés is volt egy következő lépcsőben az angol stopszavakat is eltávolítjuk.

lawtext_dfm_2 <- quanteda::dfm_remove(lawtext_dfm, pattern = stopwords("english"))

Ezután megnézzük a leggyakoribb 15 kifejezést.

topfeatures(lawtext_dfm_2, 15)
-#>     bekezdés          áll     szerződő  rendelkezés      törvény 
-#>         5877         4267         3620         3403         3312 
-#>         hely      személy           év       kiadás            b 
-#>         3114         3048         2975         2884         2831 
-#>           ha    következő költségvetés      működés         eset 
-#>         2794         2398         2376         2325         2070
+#> bekezdés áll szerződő rendelkezés törvény hely személy év kiadás b +#> 5877 4267 3620 3403 3312 3114 3048 2975 2884 2831 +#> ha következő költségvetés működés eset +#> 2794 2398 2376 2325 2070

A következő lépés, hogy TF-IDF súlyozású statisztikát készítünk, a dokumentum kifejezés mátrix alapján. Ehhez először létrehozzuk a lawtext_tfidf nevű objektumot, majd a textstat_frequency() függvény segítségével kilistázzuk annak első 10 elemét.

lawtext_tfidf <- quanteda::dfm_tfidf(lawtext_dfm_2)
 
diff --git a/docs/embedding.html b/docs/embedding.html
index c6fbb25..c019989 100644
--- a/docs/embedding.html
+++ b/docs/embedding.html
@@ -6,7 +6,7 @@
   
   8 Szóbeágyazások | Szövegbányászat és mesterséges intelligencia R-ben
   
-  
+  
 
   
   
@@ -55,7 +55,7 @@
 
 
 
-
+
 
 
 
@@ -359,13 +359,13 @@ 

8.2.1 GloVe használata magyar m
glimpse(mn)
 #> Rows: 35,021
 #> Columns: 2
-#> $ doc_id <chr> "mn_2002_05_27_00.txt", "mn_2002_05_27_01.txt", "mn_2002_…
-#> $ text   <chr> "Csere szerb módra\nNagy vihart kavart Szerbiában a kormá…
+#> $ 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_…
-#> $ text   <chr> "csere szerb módranagy vihart kavart szerbiában a kormány…
+#> $ 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)
 
@@ -378,21 +378,22 @@ 

8.2.1 GloVe használata magyar m 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)
+
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  [13:19:48.862] epoch 1, loss 0.2291
-#> INFO  [13:20:14.583] epoch 2, loss 0.0963
-#> INFO  [13:20:34.354] epoch 3, loss 0.0706
-#> INFO  [13:20:52.810] epoch 4, loss 0.0490
-#> INFO  [13:21:11.203] epoch 5, loss 0.0412
-#> INFO  [13:21:29.323] epoch 6, loss 0.0362
-#> INFO  [13:21:45.562] epoch 7, loss 0.0326
-#> INFO  [13:22:03.680] epoch 8, loss 0.0298
-#> INFO  [13:22:03.681] Success: early stopping. Improvement at iterartion 8 is less then convergence_tol
+#> 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
 
@@ -404,7 +405,7 @@ 

8.2.1 GloVe használata magyar m head(sort(cos_sim_rom[, 1], decreasing = TRUE), 5) #> polgármester mszps szocialista fideszes politikus -#> 1.000 0.519 0.508 0.465 0.409

+#> 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]
@@ -420,22 +421,22 @@ 

8.2.1 GloVe használata magyar m #> <chr> <dbl> #> 1 barack 1 #> 2 obama 0.726 -#> 3 amerikai 0.429 -#> 4 elnök 0.394 -#> 5 demokrata 0.389 -#> 6 republikánus 0.282 +#> 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.00 
-#> 2 viktor         0.932
-#> 3 miniszterelnök 0.764
-#> 4 mondta         0.699
-#> 5 kormányfő      0.686
-#> 6 fidesz         0.675
+#> 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] +
@@ -445,7 +446,7 @@ 

8.2.1 GloVe használata magyar m head(sort(cos_sim[, 1], decreasing = TRUE), 5) #> budapest németország német airport kancellár -#> 0.602 0.557 0.537 0.422 0.394

+#> 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")
@@ -469,8 +470,8 @@

8.2.1 GloVe használata magyar m ggplotly(embedded)
-
- +
+

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

diff --git a/docs/felugyelt.html b/docs/felugyelt.html index 754c385..7e942fa 100644 --- a/docs/felugyelt.html +++ b/docs/felugyelt.html @@ -6,7 +6,7 @@ 12 Osztályozás és felügyelt tanulás | Szövegbányászat és mesterséges intelligencia R-ben - + @@ -55,7 +55,7 @@ - + @@ -352,7 +352,7 @@

12.1 Fogalmi alapok

12.2 Osztályozás felügyelt tanulással

Az alábbi fejezetben a CAP magyar média gyűjteményéből a napilap címlapokat tartalmazó modult használjuk.57 Az induló adatbázis 71875 cikk szövegét és metaadatait (összesen öt változót: sorszám, fájlnév, a közpolitikai osztály kódja, szöveg, illetve a korpusz forrása – Magyar Nemzet vagy Népszabadság) tartalmazza. Az a célunk, hogy az egyes cikkekhez kézzel, jó minőségben (két, egymástól függetlenül dolgozó kódoló által) kiosztott és egyeztetett közpolitikai kódokat – ez a tanítóhalmaz – arra használjuk, hogy meghatározzuk egy kiválasztott cikkcsoport hasonló kódjait. Az osztályozási feladathoz a CAP közpolitikai kódrendszerét használjuk, mely 21 közpolitikai kategóriát határoz meg az oktatástól az egészségügyön át a honvédelemig. 58

-

Annak érdekében, hogy egyértelműen értékelhessük a gépi tanulás hatékonyságát, a kiválasztott cikkcsoport (azaz a teszthalmaz) esetében is ismerjük a kézi kódolás eredményét („éles“ kutatási helyzetben, ismeretlen kódok esetében ugyanakkor ezt gyakran szintén csak egy kisebb mintán tudjuk kézzel validálni). További fontos lépés, hogy az észszerű futási idő érdekében a gyakorlat során a teljes adatbázisból – és ezen belül is csak a Népszabadság részhalmazból – fogunk venni egy 4500 darabos mintát. Ezen a mintán pedigg a már korábban említett kétféle modellt fogjuk futtatni a NB-t és az SVM-t. Az ezekkel a módszerekkel létrehozott két modellünk hatékonyságát fogjuk összehasonlítani, valamint azt is megfogjuk nézni, hogy az eredmények megbízhatósága mennyiben közelíti meg a kézikódolási módszerre jellemző 80-90%-os pontosságot.

+

Annak érdekében, hogy egyértelműen értékelhessük a gépi tanulás hatékonyságát, a kiválasztott cikkcsoport (azaz a teszthalmaz) esetében is ismerjük a kézi kódolás eredményét („éles“ kutatási helyzetben, ismeretlen kódok esetében ugyanakkor ezt gyakran szintén csak egy kisebb mintán tudjuk kézzel validálni). További fontos lépés, hogy az észszerű futási idő érdekében a gyakorlat során a teljes adatbázisból – és ezen belül is csak a Népszabadság részhalmazból – fogunk venni egy 4500 darabos mintát. Ezen a mintán pedig a már korábban említett kétféle modellt fogjuk futtatni a NB-t és az SVM-t. Az ezekkel a módszerekkel létrehozott két modellünk hatékonyságát fogjuk összehasonlítani, valamint azt is megfogjuk nézni, hogy az eredmények megbízhatósága mennyiben közelíti meg a kézikódolási módszerre jellemző 80-90%-os pontosságot.

Először behívjuk a szükséges csomagokat. Majd a felügyelet nélküli tanulással foglalkozó fejezethez hasonlóan itt is alkalmazzuk a set.seed() funkciót, mivel anélkül nem egyeznének az eredményeink teljes mértékben egy a kódunk egy késöbbi újrafuttatása esetén.

library(stringr)
 library(dplyr)
@@ -367,12 +367,12 @@ 

12.2 Osztályozás felügyelt tan glimpse(Data_NOL_MNO) #> Rows: 71,875 #> Columns: 5 -#> $ row_number <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 1… -#> $ filename <chr> "nol_1990_01_02_01.txt", "nol_1990_01_02_02.txt"… -#> $ majortopic_code <dbl> 19, 19, 19, 1, 1, 19, 19, 1, 1, 19, 19, 1, 19, 1… -#> $ text <chr> "változás év választás év világ ünneplés köz kel… -#> $ corpus <chr> "NOL", "NOL", "NOL", "NOL", "NOL", "NOL", "NOL",…

-

Majd pár kissebb átalakítást hajtunk végre az adatainkon. Először is ehhez a modellhez most csak a Népszabadság cikkeket fogjuk alkalmazni a Magyar Nemzet cikkeit kiszedjük az adataink közül. Ezután leegyszerűsítjük a kódolást. Összesen 21 témakört lefed a cikkek kézi kódolása most viszont ezekből egy bináris változót készítünk, amelynek 1 lesz az értéke, ha a cikk makrogazdaságról szól, ha pedig bármilyne más témakörről, akkor 0 lesz az értéke. A két fajta cikkből összesen 10000 db-os mintat veszunk, amelyben a két típus egyenlő arányban van jelen. A modellek pedig az ide történő besorolálst próbálják majd megállapítani a cikkek szövege alapján vagyis, hogy makrogazdasági témáról szól-e az adott cikk, vagy valamilyen más témáról. Majd kiválasztjuk a táblából a két változó, amelyekre szükségünk lesz a cikkek szövegét text és a cikkekhez kézzel hozzárendelt téműt label a többit pedig elvetjük.

+#> $ row_number <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,… +#> $ filename <chr> "nol_1990_01_02_01.txt", "nol_1990_01_02_02.txt", "nol_1990_01_02_03.txt", "nol_1990_01_02_04.txt", "nol_1… +#> $ majortopic_code <dbl> 19, 19, 19, 1, 1, 19, 19, 1, 1, 19, 19, 1, 19, 1, 19, 19, 17, 19, 33, 4, 19, 19, 19, 19, 19, 1, 19, 19, 5,… +#> $ text <chr> "változás év választás év világ ünneplés köz keleteuróp figyel — kilencvenes évtiz szovjet—ameri közeledés… +#> $ corpus <chr> "NOL", "NOL", "NOL", "NOL", "NOL", "NOL", "NOL", "NOL", "NOL", "NOL", "NOL", "NOL", "NOL", "NOL", "NOL", "…

+

Majd pár kisebb átalakítást hajtunk végre az adatainkon. Először is ehhez a modellhez most csak a Népszabadság cikkeket fogjuk alkalmazni a Magyar Nemzet cikkeit kiszedjük az adataink közül. Ezután leegyszerűsítjük a kódolást. Összesen 21 témakört lefed a cikkek kézi kódolása most viszont ezekből egy bináris változót készítünk, amelynek 1 lesz az értéke, ha a cikk makrogazdaságról szól, ha pedig bármilyne más témakörről, akkor 0 lesz az értéke. A két fajta cikkből összesen 10000 db-os mintat veszunk, amelyben a két típus egyenlő arányban van jelen. A modellek pedig az ide történő besorolálst próbálják majd megállapítani a cikkek szövege alapján vagyis, hogy makrogazdasági témáról szól-e az adott cikk, vagy valamilyen más témáról. Majd kiválasztjuk a táblából a két változó, amelyekre szükségünk lesz a cikkek szövegét text és a cikkekhez kézzel hozzárendelt témát label a többit pedig elvetjük.

Data_NOL <- Data_NOL_MNO %>% 
   subset(corpus == "NOL" & is.na(text) == F) %>% 
   mutate(label = if_else(majortopic_code == 1, 1, 0)) %>% 
@@ -412,7 +412,7 @@ 

12.2.1 Naïve Bayes#> becsult_osztaly #> tenyleges_osztaly 0 1 #> 0 3310 463 -#> 1 794 2933

+#> 1 793 2934

Ezt létrehozhatjuk a caret csomag funkciójával is, amely a tévesztési tábla mellett sok más hasznos adatot is megad a számunkra.

confusionMatrix(eredmeny_tabla, mode = "everything")
 #> Confusion Matrix and Statistics
@@ -420,9 +420,9 @@ 

12.2.1 Naïve Bayes#> becsult_osztaly #> tenyleges_osztaly 0 1 #> 0 3310 463 -#> 1 794 2933 +#> 1 793 2934 #> -#> Accuracy : 0.832 +#> Accuracy : 0.833 #> 95% CI : (0.824, 0.841) #> No Information Rate : 0.547 #> P-Value [Acc > NIR] : <2e-16 @@ -437,7 +437,7 @@

12.2.1 Naïve Bayes#> Neg Pred Value : 0.787 #> Precision : 0.877 #> Recall : 0.807 -#> F1 : 0.840 +#> F1 : 0.841 #> Prevalence : 0.547 #> Detection Rate : 0.441 #> Detection Prevalence : 0.503 @@ -445,7 +445,7 @@

12.2.1 Naïve Bayes#> #> 'Positive' Class : 0 #>

-

Az eredményeken pedig láthatjuk, hogy még az egyszerűség kedvéért lecsökkentett méretű tanítási halmaz ellenére is egy kifejezett magas 83.25%-os pontossági értéket kapunk, amely többé-kevésbé megfeletethető egy kizárólag kézikódolással végzett vizsgálat pontosságának. Ezt követően a kapott eredményeket a mérésünk minőségéről egy táblázatba rendezzük, későbbi összehasonlítás céljából.

+

Az eredményeken pedig láthatjuk, hogy még az egyszerűség kedvéért lecsökkentett méretű tanítási halmaz ellenére is egy kifejezett magas 83.25%-os pontossági értéket kapunk, amely többé-kevésbé megfeleltethető egy kizárólag kézikódolással végzett vizsgálat pontosságának. Ezt követően a kapott eredményeket a mérésünk minőségéről egy táblázatba rendezzük, későbbi összehasonlítás céljából.

nb_eredmenyek <- data.frame(confusionMatrix(eredmeny_tabla, mode = "prec_recall")[4])
 nb_eredmenyek$meres <- row.names(nb_eredmenyek)
 nb_eredmenyek$modszer <- "Naïve Bayes"
@@ -463,39 +463,39 @@ 

12.2.2 Support-vector machineeredmeny_tabla_svm #> becsult_osztaly_svm #> tenyleges_osztaly_svm 0 1 -#> 0 3061 712 -#> 1 578 3149

+#> 0 3066 707 +#> 1 581 3146

Valamint jelen esetben is használhatjuk a caret csomagban található funkciót, hogy még több információt nyerjünk ki a modellünk működéséről.

confusionMatrix(eredmeny_tabla_svm, mode = "everything")
 #> Confusion Matrix and Statistics
 #> 
 #>                      becsult_osztaly_svm
 #> tenyleges_osztaly_svm    0    1
-#>                     0 3061  712
-#>                     1  578 3149
-#>                                         
-#>                Accuracy : 0.828         
-#>                  95% CI : (0.819, 0.836)
-#>     No Information Rate : 0.515         
-#>     P-Value [Acc > NIR] : < 2e-16       
-#>                                         
-#>                   Kappa : 0.656         
-#>                                         
-#>  Mcnemar's Test P-Value : 0.000213      
-#>                                         
-#>             Sensitivity : 0.841         
-#>             Specificity : 0.816         
-#>          Pos Pred Value : 0.811         
-#>          Neg Pred Value : 0.845         
-#>               Precision : 0.811         
-#>                  Recall : 0.841         
-#>                      F1 : 0.826         
-#>              Prevalence : 0.485         
-#>          Detection Rate : 0.408         
-#>    Detection Prevalence : 0.503         
-#>       Balanced Accuracy : 0.828         
-#>                                         
-#>        'Positive' Class : 0             
+#>                     0 3066  707
+#>                     1  581 3146
+#>                                        
+#>                Accuracy : 0.828        
+#>                  95% CI : (0.82, 0.837)
+#>     No Information Rate : 0.514        
+#>     P-Value [Acc > NIR] : < 2e-16      
+#>                                        
+#>                   Kappa : 0.657        
+#>                                        
+#>  Mcnemar's Test P-Value : 0.000496     
+#>                                        
+#>             Sensitivity : 0.841        
+#>             Specificity : 0.817        
+#>          Pos Pred Value : 0.813        
+#>          Neg Pred Value : 0.844        
+#>               Precision : 0.813        
+#>                  Recall : 0.841        
+#>                      F1 : 0.826        
+#>              Prevalence : 0.486        
+#>          Detection Rate : 0.409        
+#>    Detection Prevalence : 0.503        
+#>       Balanced Accuracy : 0.829        
+#>                                        
+#>        'Positive' Class : 0            
 #> 

Itt ismét azt találjuk, hogy a csökkentett méretű korpusz ellenére is egy kifejezetten magas pontossági értéket 82.83%-ot kapunk. Az eredményeinket ismét egy adattáblába rendezzük, amelyet végül összekötünk az első táblánnkkal, hogy a két módszert a korábban tárgyalt három mérőszám alapján is összehasonlítsuk.

svm_eredmenyek <- data.frame(confusionMatrix(eredmeny_tabla_svm, mode = "prec_recall")[4])
diff --git "a/docs/f\303\274ggel\303\251k.html" "b/docs/f\303\274ggel\303\251k.html"
index 109e138..44a85e9 100644
--- "a/docs/f\303\274ggel\303\251k.html"
+++ "b/docs/f\303\274ggel\303\251k.html"
@@ -6,7 +6,7 @@
   
   13 Függelék | Szövegbányászat és mesterséges intelligencia R-ben
   
-  
+  
 
   
   
@@ -55,7 +55,7 @@
 
 
 
-
+
 
 
 
@@ -441,8 +441,7 @@ 

13.1.8 Data frame#> Levels: Asia Europe North America South America orszag_adatok$orszag -#> [1] "Thailand" "Norway" "North Korea" "Canada" "Slovenia" -#> [6] "France" "Venezuela"

+#> [1] "Thailand" "Norway" "North Korea" "Canada" "Slovenia" "France" "Venezuela"
@@ -585,8 +584,8 @@

13.3 Vizualizáció geom_histogram()

Majd ennek az obejktumnak a nevét helyezzük be a ggplotly parancsba, és futtassuk azt.

ggplotly(ggplotabra)
-
- +
+ diff --git a/docs/index.html b/docs/index.html index 8457e38..e959972 100644 --- a/docs/index.html +++ b/docs/index.html @@ -6,7 +6,7 @@ Szövegbányászat és mesterséges intelligencia R-ben - + @@ -55,7 +55,7 @@ - + diff --git a/docs/intro.html b/docs/intro.html index cb940b5..ac269ff 100644 --- a/docs/intro.html +++ b/docs/intro.html @@ -6,7 +6,7 @@ 1 Bevezetés | Szövegbányászat és mesterséges intelligencia R-ben - + @@ -55,7 +55,7 @@ - + @@ -341,8 +341,8 @@

1.2 Használati utasításA könyvhöz tartozik egy HunMineR nevű R csomag is, amely tartalmazza az egyes fejezetekben használt összes adatbázist, így az adatbeviteli problémákat elkerülve lehet gyakorolni a szövegbányászatot. A könyv megjelenésekor a csomag még nem került be a központi R CRAN csomag repozitóriumába, hanem a poltextLAB GitHub repozitóriumából tölthető le.

A könyvben szereplő ábrák nagy része a ggplot2 csomaggal készült a theme_set(theme_light()) opció beállításával a háttérben. Ez azt jelenti, hogy az ábrákat előállító kódok a theme_light() sort nem tartalmazzák, de a tényleges ábrán már megjelennek a tematikus elemek.

Az egyes fejezetekben használt R csomagok listája és verziószáma a lenti táblázatban található. Fontos tudni, hogy a használt R csomagokat folyamatosan fejlesztik, ezért elképzelhető hogy eltérő verziószámú változatok esetén változhat a kód szintaxis.

- - + +
A könyvben használt R csomagok diff --git "a/docs/irodalomjegyz\303\251k.html" "b/docs/irodalomjegyz\303\251k.html" index 17f60f9..1051e85 100644 --- "a/docs/irodalomjegyz\303\251k.html" +++ "b/docs/irodalomjegyz\303\251k.html" @@ -6,7 +6,7 @@ Irodalomjegyzék | Szövegbányászat és mesterséges intelligencia R-ben - + @@ -55,7 +55,7 @@ - + diff --git a/docs/lda_ch.html b/docs/lda_ch.html index 802338a..d1a4cb1 100644 --- a/docs/lda_ch.html +++ b/docs/lda_ch.html @@ -6,7 +6,7 @@ 7 Felügyelet nélküli tanulás | Szövegbányászat és mesterséges intelligencia R-ben - + @@ -55,7 +55,7 @@ - + @@ -356,10 +356,10 @@

7.2 K-közép klaszterezés
glimpse(data_miniszterelnokok)
 #> Rows: 7
 #> Columns: 4
-#> $ doc_id <chr> "antall_jozsef_1990", "bajnai_gordon_2009", "gyurcsány_fe…
-#> $ text   <chr> "Elnök Úr! Tisztelt Országgyulés! Hölgyeim és Uraim! Honf…
+#> $ 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", "ho…
+#> $ 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)
 
@@ -376,8 +376,8 @@ 

7.2 K-közép klaszterezés ggplotly(klas_df)

-
- +
+

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

@@ -399,8 +399,8 @@

7.2 K-közép klaszterezés ggplotly(min_besz_plot)

-
- +
+

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

@@ -418,8 +418,8 @@

7.3 LDA topikmodellek
glimpse(torvenyek)
 #> Rows: 1,032
 #> Columns: 2
-#> $ doc_id <chr> "1998L", "1998LI", "1998LII", "1998LIII", "1998LIV", "199…
-#> $ text   <chr> "1998. évi L. törvény\n\naz Egyesült Nemzetek Szervezete …

+#> $ 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 %>%
@@ -486,28 +486,18 @@ 

7.3 LDA topikmodellek
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")
+
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

@@ -519,12 +509,10 @@

7.3 LDA topikmodellek 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")
+    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")
-                                       )
+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) +
@@ -540,8 +528,8 @@ 

7.3 LDA topikmodellek ggplotly(tokclean_df, height = 1000, tooltip = "frequency")

-
- +
+

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

@@ -574,8 +562,8 @@

7.3.1 A VEM módszer alkalmazása ggplotly(perp_df)

-
- +
+

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

@@ -616,8 +604,8 @@

7.3.1 A VEM módszer alkalmazása ggplotly(toptermsplot9802vem, tooltip = "beta")

-
- +
+

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

@@ -637,8 +625,8 @@

7.3.1 A VEM módszer alkalmazása ggplotly(toptermsplot0206vem, tooltip = "beta")

-
- +
+

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

@@ -682,8 +670,8 @@

7.3.2 Az LDA Gibbs módszer alkal ggplotly(toptermsplot9802gibbs, tooltip = "beta")

-
- +
+

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

@@ -703,8 +691,8 @@

7.3.2 Az LDA Gibbs módszer alkal ggplotly(toptermsplot0206gibbs, tooltip = "beta")

-
- +
+

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

@@ -751,43 +739,44 @@

7.4 Strukturális topikmodellek
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")
-#> 
+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.3034     0.0310    9.79  < 2e-16 ***
-#> majortopic2               -0.2040     0.0677   -3.01  0.00265 ** 
-#> majortopic3               -0.2044     0.0595   -3.43  0.00062 ***
-#> majortopic4               -0.2211     0.0589   -3.75  0.00018 ***
-#> majortopic5                0.1030     0.0472    2.18  0.02928 *  
-#> majortopic6               -0.2231     0.0587   -3.80  0.00015 ***
-#> majortopic7               -0.1557     0.0681   -2.29  0.02244 *  
-#> majortopic8               -0.2157     0.0729   -2.96  0.00315 ** 
-#> majortopic9                0.5250     0.0876    5.99  2.9e-09 ***
-#> majortopic10              -0.1087     0.0549   -1.98  0.04805 *  
-#> majortopic12              -0.1741     0.0407   -4.28  2.0e-05 ***
-#> majortopic13              -0.1358     0.0560   -2.43  0.01543 *  
-#> majortopic14              -0.2172     0.0755   -2.88  0.00411 ** 
-#> majortopic15              -0.1475     0.0427   -3.46  0.00057 ***
-#> majortopic16              -0.0959     0.0531   -1.81  0.07100 .  
-#> majortopic17              -0.2243     0.0580   -3.86  0.00012 ***
-#> majortopic18               0.2104     0.0573    3.67  0.00025 ***
-#> majortopic19               0.0738     0.0512    1.44  0.14966    
-#> majortopic20              -0.2105     0.0392   -5.37  1.0e-07 ***
-#> majortopic21              -0.2247     0.0698   -3.22  0.00131 ** 
-#> majortopic23              -0.1670     0.0939   -1.78  0.07566 .  
-#> electoral_cycle2002-2006  -0.1036     0.0210   -4.94  9.2e-07 ***
-#> ---
-#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

+#> +#> 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 %>%
@@ -811,8 +800,8 @@ 

7.4 Strukturális topikmodellek ggplotly(topicplot, tooltip = "beta")

-
- +
+

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

@@ -822,13 +811,13 @@

7.4 Strukturális topikmodellek#> 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: árucikkeket, átalányösszegben, átléphetik, átszállítást, beruházóikat, célországban, cikktanulók +#> 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, költségvetésszemélyi, kiadásokfelhalmozási, járulékokdolog, költségvetésintézményi, kiadásokegyéb, juttatásokmunkaadókat

+#> 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", 
diff --git a/docs/leiro_stat.html b/docs/leiro_stat.html
index bfc02aa..0e50ede 100644
--- a/docs/leiro_stat.html
+++ b/docs/leiro_stat.html
@@ -6,7 +6,7 @@
   
   5 Leíró statisztika | Szövegbányászat és mesterséges intelligencia R-ben
   
-  
+  
 
   
   
@@ -55,7 +55,7 @@
 
 
 
-
+
 
 
 
@@ -346,10 +346,10 @@ 

5.2 Leíró statisztikadplyr::glimpse(texts) #> Rows: 7 #> Columns: 4 -#> $ doc_id <chr> "antall_jozsef_1990", "bajnai_gordon_2009", "gyurcsány_fe… -#> $ text <chr> "Elnök Úr! Tisztelt Országgyulés! Hölgyeim és Uraim! Honf… +#> $ 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", "ho…

+#> $ pm <chr> "antall_jozsef", "bajnai_gordon", "gyurcsány_ferenc", "horn_gyula", "medgyessy_peter", "orban_viktor", "orban_vikto…

A glimpse funkció segítségével nem csak a sorok és oszlopok számát tekinthetjük meg, hanem az egyes oszlopok neveit is, amelyek alapján megállapíthatjuk, hogy milyen információkat tartalmaz ez az objektum. Az egyes beszédek dokumentum azonosítóját, azok szövegét, az évüket és végül a miniszterelnök nevét, aki elmondta az adott beszédet.

Ezt követően az Adatkezelés R-ben című fejezetben ismertetett mutate() függvény használatával két csoportra osztjuk a beszédeket. Ehhez a pm nevű változót alkalmazzuk, amely az egyes miniszterelnökök neveit tartalmazza. Kialakítjuk a két csoportot, azaz az if_else() segítségével meghatározzuk, hogy ha „antall_jozsef”, „boross_peter”, „orban_viktor” beszédeiről van szó azokat a jobb csoportba tegye, a maradékot pedig a bal csoportba.

Ezután a glimpse() függvény segítségével megtekintjük, hogy milyen változtatásokat végeztünk az adattáblánkon. Láthatjuk, hogy míg korábban 7 dokumentumunk és 4 változónk volt, az átalakítás eredményeként a 7 dokumentum mellett már 5 változót találunk. Ezzel a lépéssel tehát kialakítottuk azokat a változókat, amelyekre az elemzés során szükségünk lesz.

@@ -363,10 +363,10 @@

5.2 Leíró statisztikaglimpse(texts) #> Rows: 7 #> Columns: 5 -#> $ doc_id <chr> "antall_jozsef_1990", "bajnai_gordon_2009", "gyurcsány… -#> $ text <chr> "Elnök Úr! Tisztelt Országgyulés! Hölgyeim és Uraim! H… +#> $ doc_id <chr> "antall_jozsef_1990", "bajnai_gordon_2009", "gyurcsány_ferenc_2005", "horn_gyula_1994", "medgyessy_peter_2002", … +#> $ text <chr> "Elnök Úr! Tisztelt Országgyulés! Hölgyeim és Uraim! Honfitársaim! Ünnepi pillanatban állok a magyar Országgyulé… #> $ year <dbl> 1990, 2009, 2005, 1994, 2002, 1995, 2018 -#> $ pm <chr> "antall_jozsef", "bajnai_gordon", "gyurcsány_ferenc", … +#> $ pm <chr> "antall_jozsef", "bajnai_gordon", "gyurcsány_ferenc", "horn_gyula", "medgyessy_peter", "orban_viktor", "orban_vi… #> $ partoldal <chr> "jobb", "bal", "bal", "bal", "bal", "jobb", "jobb"

Ezt követően a további lépések elvégzéséhez létrehozzuk a quanteda korpuszt, majd a summary() függvény segítségével megtekinthetjük a korpusz alapvető statisztikai jellemzőit. Láthatjuk például, hogy az egyes dokumentumok hány tokenből vagy mondatból állnak.

corpus_mineln <- corpus(texts)
@@ -374,22 +374,14 @@ 

5.2 Leíró statisztikasummary(corpus_mineln) #> Corpus consisting of 7 documents, showing 7 documents: #> -#> Text Types Tokens Sentences year pm -#> antall_jozsef_1990 3745 9408 431 1990 antall_jozsef -#> bajnai_gordon_2009 1391 3277 201 2009 bajnai_gordon -#> gyurcsány_ferenc_2005 2963 10267 454 2005 gyurcsány_ferenc -#> horn_gyula_1994 1704 4372 226 1994 horn_gyula -#> medgyessy_peter_2002 1021 2362 82 2002 medgyessy_peter -#> orban_viktor_1998 1810 4287 212 1995 orban_viktor -#> orban_viktor_2018 933 1976 126 2018 orban_viktor -#> partoldal -#> jobb -#> bal -#> bal -#> bal -#> bal -#> jobb -#> jobb

+#> Text Types Tokens Sentences year pm partoldal +#> antall_jozsef_1990 3745 9408 431 1990 antall_jozsef jobb +#> bajnai_gordon_2009 1391 3277 201 2009 bajnai_gordon bal +#> gyurcsány_ferenc_2005 2963 10267 454 2005 gyurcsány_ferenc bal +#> horn_gyula_1994 1704 4372 226 1994 horn_gyula bal +#> medgyessy_peter_2002 1021 2362 82 2002 medgyessy_peter bal +#> orban_viktor_1998 1810 4287 212 1995 orban_viktor jobb +#> orban_viktor_2018 933 1976 126 2018 orban_viktor jobb

Mivel az elemzés során a korpuszon belül két csoportra osztva szeretnénk összehasonlításokat tenni, az alábbiakban két alkorpuszt alakítunk ki.

mineln_jobb <- quanteda::corpus_subset(corpus_mineln, pm %in% c("antall_jozsef", "boross_peter", 
     "orban_viktor"))
@@ -408,14 +400,10 @@ 

5.2 Leíró statisztikasummary(mineln_bal) #> Corpus consisting of 3 documents, showing 3 documents: #> -#> Text Types Tokens Sentences year pm -#> bajnai_gordon_2009 1391 3277 201 2009 bajnai_gordon -#> horn_gyula_1994 1704 4372 226 1994 horn_gyula -#> medgyessy_peter_2002 1021 2362 82 2002 medgyessy_peter -#> partoldal -#> bal -#> bal -#> bal

+#> Text Types Tokens Sentences year pm partoldal +#> bajnai_gordon_2009 1391 3277 201 2009 bajnai_gordon bal +#> horn_gyula_1994 1704 4372 226 1994 horn_gyula bal +#> medgyessy_peter_2002 1021 2362 82 2002 medgyessy_peter bal

A korábban létrehozott „jobb” és „bal” változó segítségével nem csak az egyes dokumentumokat, hanem a két csoportba sorolt beszédeket is összehasonlíthatjuk egymással.

summary(corpus_mineln) %>%
   group_by(partoldal) %>%
@@ -533,53 +521,32 @@ 

5.3 A szövegek lexikai diverzit

A fenti elemzést elvégezhetjük úgy is, hogy valamennyi indexálást egyben megkapjuk. Ehhez a textstat_lexdiv() függvény argumentumába a measure = "all" kifejezést kell megadnunk.

mineln_dfm %>%
   textstat_lexdiv(measure = "all")
-#>                document   TTR     C    R CTTR    U     S     K   I
-#> 1    antall_jozsef_1990 0.647 0.949 46.7 33.0 72.9 0.960  9.29 419
-#> 2    bajnai_gordon_2009 0.728 0.957 28.2 19.9 73.2 0.962  9.73 459
-#> 3 gyurcsány_ferenc_2005 0.556 0.930 37.0 26.1 52.2 0.944  9.60 292
-#> 4       horn_gyula_1994 0.714 0.956 31.5 22.3 74.0 0.962  7.45 572
-#> 5  medgyessy_peter_2002 0.711 0.951 23.8 16.8 62.8 0.955 17.54 251
-#> 6     orban_viktor_1998 0.722 0.957 33.0 23.4 78.1 0.964 11.72 400
-#> 7     orban_viktor_2018 0.753 0.958 23.0 16.2 71.5 0.961 15.44 313
-#>          D     Vm  Maas  lgV0 lgeV0
-#> 1 0.000930 0.0287 0.117 11.19  25.8
-#> 2 0.000974 0.0269 0.117 10.43  24.0
-#> 3 0.000960 0.0279 0.138  9.23  21.3
-#> 4 0.000745 0.0232 0.116 10.66  24.5
-#> 5 0.001755 0.0373 0.126  9.42  21.7
-#> 6 0.001172 0.0314 0.113 11.02  25.4
-#> 7 0.001545 0.0345 0.118  9.98  23.0
+#> document TTR C R CTTR U S K I D Vm Maas lgV0 lgeV0 +#> 1 antall_jozsef_1990 0.647 0.949 46.7 33.0 72.9 0.960 9.29 419 0.000930 0.0287 0.117 11.19 25.8 +#> 2 bajnai_gordon_2009 0.728 0.957 28.2 19.9 73.2 0.962 9.73 459 0.000974 0.0269 0.117 10.43 24.0 +#> 3 gyurcsány_ferenc_2005 0.556 0.930 37.0 26.1 52.2 0.944 9.60 292 0.000960 0.0279 0.138 9.23 21.3 +#> 4 horn_gyula_1994 0.714 0.956 31.5 22.3 74.0 0.962 7.45 572 0.000745 0.0232 0.116 10.66 24.5 +#> 5 medgyessy_peter_2002 0.711 0.951 23.8 16.8 62.8 0.955 17.54 251 0.001755 0.0373 0.126 9.42 21.7 +#> 6 orban_viktor_1998 0.722 0.957 33.0 23.4 78.1 0.964 11.72 400 0.001172 0.0314 0.113 11.02 25.4 +#> 7 orban_viktor_2018 0.753 0.958 23.0 16.2 71.5 0.961 15.44 313 0.001545 0.0345 0.118 9.98 23.0

Ha pedig arra vagyunk kíváncsiak, hogy a kapott értékek hogyan korrelálnak egymással, azt a cor() függvény segítésével számolhatjuk ki.

div_df <- mineln_dfm %>%
   textstat_lexdiv(measure = "all")
 
 cor(div_df[, 2:13])
-#>         TTR      C       R    CTTR       U       S       K      I       D
-#> TTR   1.000  0.970 -0.6532 -0.6532  0.7504  0.8470  0.3835  0.257  0.3839
-#> C     0.970  1.000 -0.4521 -0.4521  0.8838  0.9498  0.2257  0.402  0.2261
-#> R    -0.653 -0.452  1.0000  1.0000 -0.0157 -0.1587 -0.6552  0.260 -0.6556
-#> CTTR -0.653 -0.452  1.0000  1.0000 -0.0157 -0.1587 -0.6552  0.260 -0.6556
-#> U     0.750  0.884 -0.0157 -0.0157  1.0000  0.9835 -0.1556  0.636 -0.1554
-#> S     0.847  0.950 -0.1587 -0.1587  0.9835  1.0000 -0.0197  0.571 -0.0194
-#> K     0.383  0.226 -0.6552 -0.6552 -0.1556 -0.0197  1.0000 -0.772  1.0000
-#> I     0.257  0.402  0.2602  0.2602  0.6361  0.5714 -0.7725  1.000 -0.7722
-#> D     0.384  0.226 -0.6556 -0.6556 -0.1554 -0.0194  1.0000 -0.772  1.0000
-#> Vm    0.277  0.154 -0.4803 -0.4803 -0.1495 -0.0415  0.9722 -0.826  0.9721
-#> Maas -0.781 -0.908  0.0505  0.0505 -0.9969 -0.9932  0.1099 -0.617  0.1097
-#> lgV0  0.332  0.549  0.4784  0.4784  0.8692  0.7822 -0.4815  0.709 -0.4815
-#>           Vm    Maas   lgV0
-#> TTR   0.2773 -0.7812  0.332
-#> C     0.1545 -0.9076  0.549
-#> R    -0.4803  0.0505  0.478
-#> CTTR -0.4803  0.0505  0.478
-#> U    -0.1495 -0.9969  0.869
-#> S    -0.0415 -0.9932  0.782
-#> K     0.9722  0.1099 -0.482
-#> I    -0.8265 -0.6170  0.709
-#> D     0.9721  0.1097 -0.482
-#> Vm    1.0000  0.1122 -0.393
-#> Maas  0.1122  1.0000 -0.848
-#> lgV0 -0.3931 -0.8479  1.000
+#> TTR C R CTTR U S K I D Vm Maas lgV0 +#> TTR 1.000 0.970 -0.6532 -0.6532 0.7504 0.8470 0.3835 0.257 0.3839 0.2773 -0.7812 0.332 +#> C 0.970 1.000 -0.4521 -0.4521 0.8838 0.9498 0.2257 0.402 0.2261 0.1545 -0.9076 0.549 +#> R -0.653 -0.452 1.0000 1.0000 -0.0157 -0.1587 -0.6552 0.260 -0.6556 -0.4803 0.0505 0.478 +#> CTTR -0.653 -0.452 1.0000 1.0000 -0.0157 -0.1587 -0.6552 0.260 -0.6556 -0.4803 0.0505 0.478 +#> U 0.750 0.884 -0.0157 -0.0157 1.0000 0.9835 -0.1556 0.636 -0.1554 -0.1495 -0.9969 0.869 +#> S 0.847 0.950 -0.1587 -0.1587 0.9835 1.0000 -0.0197 0.571 -0.0194 -0.0415 -0.9932 0.782 +#> K 0.383 0.226 -0.6552 -0.6552 -0.1556 -0.0197 1.0000 -0.772 1.0000 0.9722 0.1099 -0.482 +#> I 0.257 0.402 0.2602 0.2602 0.6361 0.5714 -0.7725 1.000 -0.7722 -0.8265 -0.6170 0.709 +#> D 0.384 0.226 -0.6556 -0.6556 -0.1554 -0.0194 1.0000 -0.772 1.0000 0.9721 0.1097 -0.482 +#> Vm 0.277 0.154 -0.4803 -0.4803 -0.1495 -0.0415 0.9722 -0.826 0.9721 1.0000 0.1122 -0.393 +#> Maas -0.781 -0.908 0.0505 0.0505 -0.9969 -0.9932 0.1099 -0.617 0.1097 0.1122 1.0000 -0.848 +#> lgV0 0.332 0.549 0.4784 0.4784 0.8692 0.7822 -0.4815 0.709 -0.4815 -0.3931 -0.8479 1.000

A kapott értékeket a ggcorr() függvény segítségével ábrázolhatjuk is. Ha a függvény argumentumában a label = TRUE szerepel, a kapott ábrán a kiszámított értékek is láthatók (ld. 5.1. ábra).

GGally::ggcorr(div_df[, 2:13], label = TRUE)
@@ -600,17 +567,18 @@

5.3 A szövegek lexikai diverzit #> 6 orban_viktor_1998 13.0 #> 7 orban_viktor_2018 11.4

Ezután a kiszámított értékkel kiegészítjük a korpuszt.

-
docvars(corpus_mineln, "f_k") <- textstat_readability(corpus_mineln, measure = "Flesch.Kincaid")[, 2]
-
-docvars(corpus_mineln)
-#>   year               pm partoldal  f_k
-#> 1 1990    antall_jozsef      jobb 16.5
-#> 2 2009    bajnai_gordon       bal 10.9
-#> 3 2005 gyurcsány_ferenc       bal 13.6
-#> 4 1994       horn_gyula       bal 13.8
-#> 5 2002  medgyessy_peter       bal 15.8
-#> 6 1995     orban_viktor      jobb 13.0
-#> 7 2018     orban_viktor      jobb 11.4
+
docvars(corpus_mineln, "f_k") <- textstat_readability(corpus_mineln, measure = "Flesch.Kincaid")[,
+    2]
+
+docvars(corpus_mineln)
+#>   year               pm partoldal  f_k
+#> 1 1990    antall_jozsef      jobb 16.5
+#> 2 2009    bajnai_gordon       bal 10.9
+#> 3 2005 gyurcsány_ferenc       bal 13.6
+#> 4 1994       horn_gyula       bal 13.8
+#> 5 2002  medgyessy_peter       bal 15.8
+#> 6 1995     orban_viktor      jobb 13.0
+#> 7 2018     orban_viktor      jobb 11.4

Majd a ggplot2 segítségével vizualizálhatjuk az eredményt (ld. ??. ábra). Ehhez az olvashatósági pontszámmal kiegészített korpuszból egy adattáblát alakítunk ki, majd beállítjuk az ábrázolás paramétereit. Az ábra két tengelyén az év, illetve az olvashatósági pontszám szerepel, a jobb- és a baloldalt a vonal típusa különbözteti meg, az egyes dokumentumokat ponttal jelöljük, az ábrára pedig felíratjuk a miniszterelnökök neveit, valamint azt is beállítjuk, hogy az x tengely beosztása az egyes beszédek dátumához igazodjon. A theme_minimal() függvénnyel pedig azt határozzuk meg, hogy mindez fehér hátteret kapjon. Az így létrehozott ábránkat a ggplotly parancs segítségével pedig interaktívvá is tehetjük.

corpus_df <- docvars(corpus_mineln)
mineln_df <- ggplot(corpus_df, aes(year, f_k)) +
@@ -629,8 +597,8 @@ 

5.3 A szövegek lexikai diverzit ggplotly(mineln_df)

-
- +
+

Ábra 5.2: Az olvashatósági index alakulása

@@ -644,74 +612,42 @@

5.4 Összehasonlítás dfm_weight("prop") %>% quanteda.textstats::textstat_simil(margin = "documents", method = "jaccard") #> textstat_simil object; method = "jaccard" -#> antall_jozsef_1990 bajnai_gordon_2009 -#> antall_jozsef_1990 1.0000 0.0559 -#> bajnai_gordon_2009 0.0559 1.0000 -#> gyurcsány_ferenc_2005 0.0798 0.0850 -#> horn_gyula_1994 0.0694 0.0592 -#> medgyessy_peter_2002 0.0404 0.0690 -#> orban_viktor_1998 0.0778 0.0626 -#> orban_viktor_2018 0.0362 0.0617 -#> gyurcsány_ferenc_2005 horn_gyula_1994 -#> antall_jozsef_1990 0.0798 0.0694 -#> bajnai_gordon_2009 0.0850 0.0592 -#> gyurcsány_ferenc_2005 1.0000 0.0683 -#> horn_gyula_1994 0.0683 1.0000 -#> medgyessy_peter_2002 0.0684 0.0587 -#> orban_viktor_1998 0.0734 0.0621 -#> orban_viktor_2018 0.0503 0.0494 -#> medgyessy_peter_2002 orban_viktor_1998 -#> antall_jozsef_1990 0.0404 0.0778 -#> bajnai_gordon_2009 0.0690 0.0626 -#> gyurcsány_ferenc_2005 0.0684 0.0734 -#> horn_gyula_1994 0.0587 0.0621 -#> medgyessy_peter_2002 1.0000 0.0650 -#> orban_viktor_1998 0.0650 1.0000 -#> orban_viktor_2018 0.0504 0.0583 -#> orban_viktor_2018 -#> antall_jozsef_1990 0.0362 -#> bajnai_gordon_2009 0.0617 -#> gyurcsány_ferenc_2005 0.0503 -#> horn_gyula_1994 0.0494 -#> medgyessy_peter_2002 0.0504 -#> orban_viktor_1998 0.0583 -#> orban_viktor_2018 1.0000

+#> antall_jozsef_1990 bajnai_gordon_2009 gyurcsány_ferenc_2005 horn_gyula_1994 medgyessy_peter_2002 +#> antall_jozsef_1990 1.0000 0.0559 0.0798 0.0694 0.0404 +#> bajnai_gordon_2009 0.0559 1.0000 0.0850 0.0592 0.0690 +#> gyurcsány_ferenc_2005 0.0798 0.0850 1.0000 0.0683 0.0684 +#> horn_gyula_1994 0.0694 0.0592 0.0683 1.0000 0.0587 +#> medgyessy_peter_2002 0.0404 0.0690 0.0684 0.0587 1.0000 +#> orban_viktor_1998 0.0778 0.0626 0.0734 0.0621 0.0650 +#> orban_viktor_2018 0.0362 0.0617 0.0503 0.0494 0.0504 +#> orban_viktor_1998 orban_viktor_2018 +#> antall_jozsef_1990 0.0778 0.0362 +#> bajnai_gordon_2009 0.0626 0.0617 +#> gyurcsány_ferenc_2005 0.0734 0.0503 +#> horn_gyula_1994 0.0621 0.0494 +#> medgyessy_peter_2002 0.0650 0.0504 +#> orban_viktor_1998 1.0000 0.0583 +#> orban_viktor_2018 0.0583 1.0000

Majd a textstat_dist() függvény segítségével kiszámoljuk a dokumentumok egymástól való különbözőségét.

mineln_dfm %>%
   quanteda.textstats::textstat_dist(margin = "documents", method = "euclidean")
 #> textstat_dist object; method = "euclidean"
-#>                       antall_jozsef_1990 bajnai_gordon_2009
-#> antall_jozsef_1990                     0              162.8
-#> bajnai_gordon_2009                   163                  0
-#> gyurcsány_ferenc_2005                186              137.8
-#> horn_gyula_1994                      164               80.1
-#> medgyessy_peter_2002                 160               68.1
-#> orban_viktor_1998                    139               84.7
-#> orban_viktor_2018                    167               67.3
-#>                       gyurcsány_ferenc_2005 horn_gyula_1994
-#> antall_jozsef_1990                      186           163.6
-#> bajnai_gordon_2009                      138            80.1
-#> gyurcsány_ferenc_2005                     0           147.0
-#> horn_gyula_1994                         147               0
-#> medgyessy_peter_2002                    143            75.9
-#> orban_viktor_1998                       147            89.6
-#> orban_viktor_2018                       148            74.8
-#>                       medgyessy_peter_2002 orban_viktor_1998
-#> antall_jozsef_1990                   160.2             139.2
-#> bajnai_gordon_2009                    68.1              84.7
-#> gyurcsány_ferenc_2005                142.6             146.9
-#> horn_gyula_1994                       75.9              89.6
-#> medgyessy_peter_2002                     0              77.5
-#> orban_viktor_1998                     77.5                 0
-#> orban_viktor_2018                     60.7              83.6
-#>                       orban_viktor_2018
-#> antall_jozsef_1990                167.4
-#> bajnai_gordon_2009                 67.3
-#> gyurcsány_ferenc_2005             147.9
-#> horn_gyula_1994                    74.8
-#> medgyessy_peter_2002               60.7
-#> orban_viktor_1998                  83.6
-#> orban_viktor_2018                     0
+#> antall_jozsef_1990 bajnai_gordon_2009 gyurcsány_ferenc_2005 horn_gyula_1994 medgyessy_peter_2002 +#> antall_jozsef_1990 0 162.8 186 163.6 160.2 +#> bajnai_gordon_2009 163 0 138 80.1 68.1 +#> gyurcsány_ferenc_2005 186 137.8 0 147.0 142.6 +#> horn_gyula_1994 164 80.1 147 0 75.9 +#> medgyessy_peter_2002 160 68.1 143 75.9 0 +#> orban_viktor_1998 139 84.7 147 89.6 77.5 +#> orban_viktor_2018 167 67.3 148 74.8 60.7 +#> orban_viktor_1998 orban_viktor_2018 +#> antall_jozsef_1990 139.2 167.4 +#> bajnai_gordon_2009 84.7 67.3 +#> gyurcsány_ferenc_2005 146.9 147.9 +#> horn_gyula_1994 89.6 74.8 +#> medgyessy_peter_2002 77.5 60.7 +#> orban_viktor_1998 0 83.6 +#> orban_viktor_2018 83.6 0

Ezután vizualizálhatjuk is a dokumentumok egymástól való távolságát egy olyan dendogram21 segítségével, amely megmutatja nekünk a lehetséges dokumentumpárokat (ld. 5.3. ábra).

dist <- mineln_dfm %>%
   textstat_dist(margin = "documents", method = "euclidean")
@@ -786,8 +722,8 @@

5.4 Összehasonlítás ggplotly(data_df, height = 1000, tooltip = "frequency")
-
- +
+

Ábra 5.5: Leggyakoribb kifejezések a miniszterelnöki beszédekben

@@ -806,18 +742,12 @@

5.5 A kulcsszavak kontextusa case_insensitive = TRUE ) %>% head(5) -#> Keyword-in-context with 5 matches. -#> [antall_jozsef_1990, 1167] Átfogó és mély | -#> [antall_jozsef_1990, 1283] kell hárítanunk a | -#> [antall_jozsef_1990, 2772] és a lakásgazdálkodás | -#> [antall_jozsef_1990, 5226] gazdaság egészét juttatta | -#> [antall_jozsef_1990, 5286] gazdaság reménytelenül eladósodott | -#> -#> válságba | süllyedtünk a nyolcvanas -#> válságot | , de csakis -#> válságos | helyzetbe került. -#> válságba | , és amellyel -#> válsággócai | ellen. A

+#> Keyword-in-context with 5 matches. +#> [antall_jozsef_1990, 1167] Átfogó és mély | válságba | süllyedtünk a nyolcvanas +#> [antall_jozsef_1990, 1283] kell hárítanunk a | válságot | , de csakis +#> [antall_jozsef_1990, 2772] és a lakásgazdálkodás | válságos | helyzetbe került. +#> [antall_jozsef_1990, 5226] gazdaság egészét juttatta | válságba | , és amellyel +#> [antall_jozsef_1990, 5286] gazdaság reménytelenül eladósodott | válsággócai | ellen. A diff --git a/docs/libs/plotly-binding-4.10.2/plotly.js b/docs/libs/plotly-binding-4.10.2/plotly.js new file mode 100644 index 0000000..7a2a143 --- /dev/null +++ b/docs/libs/plotly-binding-4.10.2/plotly.js @@ -0,0 +1,941 @@ + +HTMLWidgets.widget({ + name: "plotly", + type: "output", + + initialize: function(el, width, height) { + return {}; + }, + + resize: function(el, width, height, instance) { + if (instance.autosize) { + var width = instance.width || width; + var height = instance.height || height; + Plotly.relayout(el.id, {width: width, height: height}); + } + }, + + renderValue: function(el, x, instance) { + + // Plotly.relayout() mutates the plot input object, so make sure to + // keep a reference to the user-supplied width/height *before* + // we call Plotly.plot(); + var lay = x.layout || {}; + instance.width = lay.width; + instance.height = lay.height; + instance.autosize = lay.autosize || true; + + /* + / 'inform the world' about highlighting options this is so other + / crosstalk libraries have a chance to respond to special settings + / such as persistent selection. + / AFAIK, leaflet is the only library with such intergration + / https://github.com/rstudio/leaflet/pull/346/files#diff-ad0c2d51ce5fdf8c90c7395b102f4265R154 + */ + var ctConfig = crosstalk.var('plotlyCrosstalkOpts').set(x.highlight); + + if (typeof(window) !== "undefined") { + // make sure plots don't get created outside the network (for on-prem) + window.PLOTLYENV = window.PLOTLYENV || {}; + window.PLOTLYENV.BASE_URL = x.base_url; + + // Enable persistent selection when shift key is down + // https://stackoverflow.com/questions/1828613/check-if-a-key-is-down + var persistOnShift = function(e) { + if (!e) window.event; + if (e.shiftKey) { + x.highlight.persistent = true; + x.highlight.persistentShift = true; + } else { + x.highlight.persistent = false; + x.highlight.persistentShift = false; + } + }; + + // Only relevant if we haven't forced persistent mode at command line + if (!x.highlight.persistent) { + window.onmousemove = persistOnShift; + } + } + + var graphDiv = document.getElementById(el.id); + + // TODO: move the control panel injection strategy inside here... + HTMLWidgets.addPostRenderHandler(function() { + + // lower the z-index of the modebar to prevent it from highjacking hover + // (TODO: do this via CSS?) + // https://github.com/ropensci/plotly/issues/956 + // https://www.w3schools.com/jsref/prop_style_zindex.asp + var modebars = document.querySelectorAll(".js-plotly-plot .plotly .modebar"); + for (var i = 0; i < modebars.length; i++) { + modebars[i].style.zIndex = 1; + } + }); + + // inject a "control panel" holding selectize/dynamic color widget(s) + if ((x.selectize || x.highlight.dynamic) && !instance.plotly) { + var flex = document.createElement("div"); + flex.class = "plotly-crosstalk-control-panel"; + flex.style = "display: flex; flex-wrap: wrap"; + + // inject the colourpicker HTML container into the flexbox + if (x.highlight.dynamic) { + var pickerDiv = document.createElement("div"); + + var pickerInput = document.createElement("input"); + pickerInput.id = el.id + "-colourpicker"; + pickerInput.placeholder = "asdasd"; + + var pickerLabel = document.createElement("label"); + pickerLabel.for = pickerInput.id; + pickerLabel.innerHTML = "Brush color  "; + + pickerDiv.appendChild(pickerLabel); + pickerDiv.appendChild(pickerInput); + flex.appendChild(pickerDiv); + } + + // inject selectize HTML containers (one for every crosstalk group) + if (x.selectize) { + var ids = Object.keys(x.selectize); + + for (var i = 0; i < ids.length; i++) { + var container = document.createElement("div"); + container.id = ids[i]; + container.style = "width: 80%; height: 10%"; + container.class = "form-group crosstalk-input-plotly-highlight"; + + var label = document.createElement("label"); + label.for = ids[i]; + label.innerHTML = x.selectize[ids[i]].group; + label.class = "control-label"; + + var selectDiv = document.createElement("div"); + var select = document.createElement("select"); + select.multiple = true; + + selectDiv.appendChild(select); + container.appendChild(label); + container.appendChild(selectDiv); + flex.appendChild(container); + } + } + + // finally, insert the flexbox inside the htmlwidget container, + // but before the plotly graph div + graphDiv.parentElement.insertBefore(flex, graphDiv); + + if (x.highlight.dynamic) { + var picker = $("#" + pickerInput.id); + var colors = x.highlight.color || []; + // TODO: let users specify options? + var opts = { + value: colors[0], + showColour: "both", + palette: "limited", + allowedCols: colors.join(" "), + width: "20%", + height: "10%" + }; + picker.colourpicker({changeDelay: 0}); + picker.colourpicker("settings", opts); + picker.colourpicker("value", opts.value); + // inform crosstalk about a change in the current selection colour + var grps = x.highlight.ctGroups || []; + for (var i = 0; i < grps.length; i++) { + crosstalk.group(grps[i]).var('plotlySelectionColour') + .set(picker.colourpicker('value')); + } + picker.on("change", function() { + for (var i = 0; i < grps.length; i++) { + crosstalk.group(grps[i]).var('plotlySelectionColour') + .set(picker.colourpicker('value')); + } + }); + } + } + + // if no plot exists yet, create one with a particular configuration + if (!instance.plotly) { + + var plot = Plotly.newPlot(graphDiv, x); + instance.plotly = true; + + } else if (x.layout.transition) { + + var plot = Plotly.react(graphDiv, x); + + } else { + + // this is essentially equivalent to Plotly.newPlot(), but avoids creating + // a new webgl context + // https://github.com/plotly/plotly.js/blob/2b24f9def901831e61282076cf3f835598d56f0e/src/plot_api/plot_api.js#L531-L532 + + // TODO: restore crosstalk selections? + Plotly.purge(graphDiv); + // TODO: why is this necessary to get crosstalk working? + graphDiv.data = undefined; + graphDiv.layout = undefined; + var plot = Plotly.newPlot(graphDiv, x); + } + + // Trigger plotly.js calls defined via `plotlyProxy()` + plot.then(function() { + if (HTMLWidgets.shinyMode) { + Shiny.addCustomMessageHandler("plotly-calls", function(msg) { + var gd = document.getElementById(msg.id); + if (!gd) { + throw new Error("Couldn't find plotly graph with id: " + msg.id); + } + // This isn't an official plotly.js method, but it's the only current way to + // change just the configuration of a plot + // https://community.plot.ly/t/update-config-function/9057 + if (msg.method == "reconfig") { + Plotly.react(gd, gd.data, gd.layout, msg.args); + return; + } + if (!Plotly[msg.method]) { + throw new Error("Unknown method " + msg.method); + } + var args = [gd].concat(msg.args); + Plotly[msg.method].apply(null, args); + }); + } + + // plotly's mapbox API doesn't currently support setting bounding boxes + // https://www.mapbox.com/mapbox-gl-js/example/fitbounds/ + // so we do this manually... + // TODO: make sure this triggers on a redraw and relayout as well as on initial draw + var mapboxIDs = graphDiv._fullLayout._subplots.mapbox || []; + for (var i = 0; i < mapboxIDs.length; i++) { + var id = mapboxIDs[i]; + var mapOpts = x.layout[id] || {}; + var args = mapOpts._fitBounds || {}; + if (!args) { + continue; + } + var mapObj = graphDiv._fullLayout[id]._subplot.map; + mapObj.fitBounds(args.bounds, args.options); + } + + }); + + // Attach attributes (e.g., "key", "z") to plotly event data + function eventDataWithKey(eventData) { + if (eventData === undefined || !eventData.hasOwnProperty("points")) { + return null; + } + return eventData.points.map(function(pt) { + var obj = { + curveNumber: pt.curveNumber, + pointNumber: pt.pointNumber, + x: pt.x, + y: pt.y + }; + + // If 'z' is reported with the event data, then use it! + if (pt.hasOwnProperty("z")) { + obj.z = pt.z; + } + + if (pt.hasOwnProperty("customdata")) { + obj.customdata = pt.customdata; + } + + /* + TL;DR: (I think) we have to select the graph div (again) to attach keys... + + Why? Remember that crosstalk will dynamically add/delete traces + (see traceManager.prototype.updateSelection() below) + For this reason, we can't simply grab keys from x.data (like we did previously) + Moreover, we can't use _fullData, since that doesn't include + unofficial attributes. It's true that click/hover events fire with + pt.data, but drag events don't... + */ + var gd = document.getElementById(el.id); + var trace = gd.data[pt.curveNumber]; + + if (!trace._isSimpleKey) { + var attrsToAttach = ["key"]; + } else { + // simple keys fire the whole key + obj.key = trace.key; + var attrsToAttach = []; + } + + for (var i = 0; i < attrsToAttach.length; i++) { + var attr = trace[attrsToAttach[i]]; + if (Array.isArray(attr)) { + if (typeof pt.pointNumber === "number") { + obj[attrsToAttach[i]] = attr[pt.pointNumber]; + } else if (Array.isArray(pt.pointNumber)) { + obj[attrsToAttach[i]] = attr[pt.pointNumber[0]][pt.pointNumber[1]]; + } else if (Array.isArray(pt.pointNumbers)) { + obj[attrsToAttach[i]] = pt.pointNumbers.map(function(idx) { return attr[idx]; }); + } + } + } + return obj; + }); + } + + + var legendEventData = function(d) { + // if legendgroup is not relevant just return the trace + var trace = d.data[d.curveNumber]; + if (!trace.legendgroup) return trace; + + // if legendgroup was specified, return all traces that match the group + var legendgrps = d.data.map(function(trace){ return trace.legendgroup; }); + var traces = []; + for (i = 0; i < legendgrps.length; i++) { + if (legendgrps[i] == trace.legendgroup) { + traces.push(d.data[i]); + } + } + + return traces; + }; + + + // send user input event data to shiny + if (HTMLWidgets.shinyMode && Shiny.setInputValue) { + + // Some events clear other input values + // TODO: always register these? + var eventClearMap = { + plotly_deselect: ["plotly_selected", "plotly_selecting", "plotly_brushed", "plotly_brushing", "plotly_click"], + plotly_unhover: ["plotly_hover"], + plotly_doubleclick: ["plotly_click"] + }; + + Object.keys(eventClearMap).map(function(evt) { + graphDiv.on(evt, function() { + var inputsToClear = eventClearMap[evt]; + inputsToClear.map(function(input) { + Shiny.setInputValue(input + "-" + x.source, null, {priority: "event"}); + }); + }); + }); + + var eventDataFunctionMap = { + plotly_click: eventDataWithKey, + plotly_sunburstclick: eventDataWithKey, + plotly_hover: eventDataWithKey, + plotly_unhover: eventDataWithKey, + // If 'plotly_selected' has already been fired, and you click + // on the plot afterwards, this event fires `undefined`?!? + // That might be considered a plotly.js bug, but it doesn't make + // sense for this input change to occur if `d` is falsy because, + // even in the empty selection case, `d` is truthy (an object), + // and the 'plotly_deselect' event will reset this input + plotly_selected: function(d) { if (d) { return eventDataWithKey(d); } }, + plotly_selecting: function(d) { if (d) { return eventDataWithKey(d); } }, + plotly_brushed: function(d) { + if (d) { return d.range ? d.range : d.lassoPoints; } + }, + plotly_brushing: function(d) { + if (d) { return d.range ? d.range : d.lassoPoints; } + }, + plotly_legendclick: legendEventData, + plotly_legenddoubleclick: legendEventData, + plotly_clickannotation: function(d) { return d.fullAnnotation } + }; + + var registerShinyValue = function(event) { + var eventDataPreProcessor = eventDataFunctionMap[event] || function(d) { return d ? d : el.id }; + // some events are unique to the R package + var plotlyJSevent = (event == "plotly_brushed") ? "plotly_selected" : (event == "plotly_brushing") ? "plotly_selecting" : event; + // register the event + graphDiv.on(plotlyJSevent, function(d) { + Shiny.setInputValue( + event + "-" + x.source, + JSON.stringify(eventDataPreProcessor(d)), + {priority: "event"} + ); + }); + } + + var shinyEvents = x.shinyEvents || []; + shinyEvents.map(registerShinyValue); + } + + // Given an array of {curveNumber: x, pointNumber: y} objects, + // return a hash of { + // set1: {value: [key1, key2, ...], _isSimpleKey: false}, + // set2: {value: [key3, key4, ...], _isSimpleKey: false} + // } + function pointsToKeys(points) { + var keysBySet = {}; + for (var i = 0; i < points.length; i++) { + + var trace = graphDiv.data[points[i].curveNumber]; + if (!trace.key || !trace.set) { + continue; + } + + // set defaults for this keySet + // note that we don't track the nested property (yet) since we always + // emit the union -- http://cpsievert.github.io/talks/20161212b/#21 + keysBySet[trace.set] = keysBySet[trace.set] || { + value: [], + _isSimpleKey: trace._isSimpleKey + }; + + // Use pointNumber by default, but aggregated traces should emit pointNumbers + var ptNum = points[i].pointNumber; + var hasPtNum = typeof ptNum === "number"; + var ptNum = hasPtNum ? ptNum : points[i].pointNumbers; + + // selecting a point of a "simple" trace means: select the + // entire key attached to this trace, which is useful for, + // say clicking on a fitted line to select corresponding observations + var key = trace._isSimpleKey ? trace.key : Array.isArray(ptNum) ? ptNum.map(function(idx) { return trace.key[idx]; }) : trace.key[ptNum]; + // http://stackoverflow.com/questions/10865025/merge-flatten-an-array-of-arrays-in-javascript + var keyFlat = trace._isNestedKey ? [].concat.apply([], key) : key; + + // TODO: better to only add new values? + keysBySet[trace.set].value = keysBySet[trace.set].value.concat(keyFlat); + } + + return keysBySet; + } + + + x.highlight.color = x.highlight.color || []; + // make sure highlight color is an array + if (!Array.isArray(x.highlight.color)) { + x.highlight.color = [x.highlight.color]; + } + + var traceManager = new TraceManager(graphDiv, x.highlight); + + // Gather all *unique* sets. + var allSets = []; + for (var curveIdx = 0; curveIdx < x.data.length; curveIdx++) { + var newSet = x.data[curveIdx].set; + if (newSet) { + if (allSets.indexOf(newSet) === -1) { + allSets.push(newSet); + } + } + } + + // register event listeners for all sets + for (var i = 0; i < allSets.length; i++) { + + var set = allSets[i]; + var selection = new crosstalk.SelectionHandle(set); + var filter = new crosstalk.FilterHandle(set); + + var filterChange = function(e) { + removeBrush(el); + traceManager.updateFilter(set, e.value); + }; + filter.on("change", filterChange); + + + var selectionChange = function(e) { + + // Workaround for 'plotly_selected' now firing previously selected + // points (in addition to new ones) when holding shift key. In our case, + // we just want the new keys + if (x.highlight.on === "plotly_selected" && x.highlight.persistentShift) { + // https://stackoverflow.com/questions/1187518/how-to-get-the-difference-between-two-arrays-in-javascript + Array.prototype.diff = function(a) { + return this.filter(function(i) {return a.indexOf(i) < 0;}); + }; + e.value = e.value.diff(e.oldValue); + } + + // array of "event objects" tracking the selection history + // this is used to avoid adding redundant selections + var selectionHistory = crosstalk.var("plotlySelectionHistory").get() || []; + + // Construct an event object "defining" the current event. + var event = { + receiverID: traceManager.gd.id, + plotlySelectionColour: crosstalk.group(set).var("plotlySelectionColour").get() + }; + event[set] = e.value; + // TODO: is there a smarter way to check object equality? + if (selectionHistory.length > 0) { + var ev = JSON.stringify(event); + for (var i = 0; i < selectionHistory.length; i++) { + var sel = JSON.stringify(selectionHistory[i]); + if (sel == ev) { + return; + } + } + } + + // accumulate history for persistent selection + if (!x.highlight.persistent) { + selectionHistory = [event]; + } else { + selectionHistory.push(event); + } + crosstalk.var("plotlySelectionHistory").set(selectionHistory); + + // do the actual updating of traces, frames, and the selectize widget + traceManager.updateSelection(set, e.value); + // https://github.com/selectize/selectize.js/blob/master/docs/api.md#methods_items + if (x.selectize) { + if (!x.highlight.persistent || e.value === null) { + selectize.clear(true); + } + selectize.addItems(e.value, true); + selectize.close(); + } + } + selection.on("change", selectionChange); + + // Set a crosstalk variable selection value, triggering an update + var turnOn = function(e) { + if (e) { + var selectedKeys = pointsToKeys(e.points); + // Keys are group names, values are array of selected keys from group. + for (var set in selectedKeys) { + if (selectedKeys.hasOwnProperty(set)) { + selection.set(selectedKeys[set].value, {sender: el}); + } + } + } + }; + if (x.highlight.debounce > 0) { + turnOn = debounce(turnOn, x.highlight.debounce); + } + graphDiv.on(x.highlight.on, turnOn); + + graphDiv.on(x.highlight.off, function turnOff(e) { + // remove any visual clues + removeBrush(el); + // remove any selection history + crosstalk.var("plotlySelectionHistory").set(null); + // trigger the actual removal of selection traces + selection.set(null, {sender: el}); + }); + + // register a callback for selectize so that there is bi-directional + // communication between the widget and direct manipulation events + if (x.selectize) { + var selectizeID = Object.keys(x.selectize)[i]; + var options = x.selectize[selectizeID]; + var first = [{value: "", label: "(All)"}]; + var opts = $.extend({ + options: first.concat(options.items), + searchField: "label", + valueField: "value", + labelField: "label", + maxItems: 50 + }, + options + ); + var select = $("#" + selectizeID).find("select")[0]; + var selectize = $(select).selectize(opts)[0].selectize; + // NOTE: this callback is triggered when *directly* altering + // dropdown items + selectize.on("change", function() { + var currentItems = traceManager.groupSelections[set] || []; + if (!x.highlight.persistent) { + removeBrush(el); + for (var i = 0; i < currentItems.length; i++) { + selectize.removeItem(currentItems[i], true); + } + } + var newItems = selectize.items.filter(function(idx) { + return currentItems.indexOf(idx) < 0; + }); + if (newItems.length > 0) { + traceManager.updateSelection(set, newItems); + } else { + // Item has been removed... + // TODO: this logic won't work for dynamically changing palette + traceManager.updateSelection(set, null); + traceManager.updateSelection(set, selectize.items); + } + }); + } + } // end of selectionChange + + } // end of renderValue +}); // end of widget definition + +/** + * @param graphDiv The Plotly graph div + * @param highlight An object with options for updating selection(s) + */ +function TraceManager(graphDiv, highlight) { + // The Plotly graph div + this.gd = graphDiv; + + // Preserve the original data. + // TODO: try using Lib.extendFlat() as done in + // https://github.com/plotly/plotly.js/pull/1136 + this.origData = JSON.parse(JSON.stringify(graphDiv.data)); + + // avoid doing this over and over + this.origOpacity = []; + for (var i = 0; i < this.origData.length; i++) { + this.origOpacity[i] = this.origData[i].opacity === 0 ? 0 : (this.origData[i].opacity || 1); + } + + // key: group name, value: null or array of keys representing the + // most recently received selection for that group. + this.groupSelections = {}; + + // selection parameters (e.g., transient versus persistent selection) + this.highlight = highlight; +} + +TraceManager.prototype.close = function() { + // TODO: Unhook all event handlers +}; + +TraceManager.prototype.updateFilter = function(group, keys) { + + if (typeof(keys) === "undefined" || keys === null) { + + this.gd.data = JSON.parse(JSON.stringify(this.origData)); + + } else { + + var traces = []; + for (var i = 0; i < this.origData.length; i++) { + var trace = this.origData[i]; + if (!trace.key || trace.set !== group) { + continue; + } + var matchFunc = getMatchFunc(trace); + var matches = matchFunc(trace.key, keys); + + if (matches.length > 0) { + if (!trace._isSimpleKey) { + // subsetArrayAttrs doesn't mutate trace (it makes a modified clone) + trace = subsetArrayAttrs(trace, matches); + } + traces.push(trace); + } + } + this.gd.data = traces; + } + + Plotly.redraw(this.gd); + + // NOTE: we purposely do _not_ restore selection(s), since on filter, + // axis likely will update, changing the pixel -> data mapping, leading + // to a likely mismatch in the brush outline and highlighted marks + +}; + +TraceManager.prototype.updateSelection = function(group, keys) { + + if (keys !== null && !Array.isArray(keys)) { + throw new Error("Invalid keys argument; null or array expected"); + } + + // if selection has been cleared, or if this is transient + // selection, delete the "selection traces" + var nNewTraces = this.gd.data.length - this.origData.length; + if (keys === null || !this.highlight.persistent && nNewTraces > 0) { + var tracesToRemove = []; + for (var i = 0; i < this.gd.data.length; i++) { + if (this.gd.data[i]._isCrosstalkTrace) tracesToRemove.push(i); + } + Plotly.deleteTraces(this.gd, tracesToRemove); + this.groupSelections[group] = keys; + } else { + // add to the groupSelection, rather than overwriting it + // TODO: can this be removed? + this.groupSelections[group] = this.groupSelections[group] || []; + for (var i = 0; i < keys.length; i++) { + var k = keys[i]; + if (this.groupSelections[group].indexOf(k) < 0) { + this.groupSelections[group].push(k); + } + } + } + + if (keys === null) { + + Plotly.restyle(this.gd, {"opacity": this.origOpacity}); + + } else if (keys.length >= 1) { + + // placeholder for new "selection traces" + var traces = []; + // this variable is set in R/highlight.R + var selectionColour = crosstalk.group(group).var("plotlySelectionColour").get() || + this.highlight.color[0]; + + for (var i = 0; i < this.origData.length; i++) { + // TODO: try using Lib.extendFlat() as done in + // https://github.com/plotly/plotly.js/pull/1136 + var trace = JSON.parse(JSON.stringify(this.gd.data[i])); + if (!trace.key || trace.set !== group) { + continue; + } + // Get sorted array of matching indices in trace.key + var matchFunc = getMatchFunc(trace); + var matches = matchFunc(trace.key, keys); + + if (matches.length > 0) { + // If this is a "simple" key, that means select the entire trace + if (!trace._isSimpleKey) { + trace = subsetArrayAttrs(trace, matches); + } + // reach into the full trace object so we can properly reflect the + // selection attributes in every view + var d = this.gd._fullData[i]; + + /* + / Recursively inherit selection attributes from various sources, + / in order of preference: + / (1) official plotly.js selected attribute + / (2) highlight(selected = attrs_selected(...)) + */ + // TODO: it would be neat to have a dropdown to dynamically specify these! + $.extend(true, trace, this.highlight.selected); + + // if it is defined, override color with the "dynamic brush color"" + if (d.marker) { + trace.marker = trace.marker || {}; + trace.marker.color = selectionColour || trace.marker.color || d.marker.color; + } + if (d.line) { + trace.line = trace.line || {}; + trace.line.color = selectionColour || trace.line.color || d.line.color; + } + if (d.textfont) { + trace.textfont = trace.textfont || {}; + trace.textfont.color = selectionColour || trace.textfont.color || d.textfont.color; + } + if (d.fillcolor) { + // TODO: should selectionColour inherit alpha from the existing fillcolor? + trace.fillcolor = selectionColour || trace.fillcolor || d.fillcolor; + } + // attach a sensible name/legendgroup + trace.name = trace.name || keys.join("
"); + trace.legendgroup = trace.legendgroup || keys.join("
"); + + // keep track of mapping between this new trace and the trace it targets + // (necessary for updating frames to reflect the selection traces) + trace._originalIndex = i; + trace._newIndex = this.gd._fullData.length + traces.length; + trace._isCrosstalkTrace = true; + traces.push(trace); + } + } + + if (traces.length > 0) { + + Plotly.addTraces(this.gd, traces).then(function(gd) { + // incrementally add selection traces to frames + // (this is heavily inspired by Plotly.Plots.modifyFrames() + // in src/plots/plots.js) + var _hash = gd._transitionData._frameHash; + var _frames = gd._transitionData._frames || []; + + for (var i = 0; i < _frames.length; i++) { + + // add to _frames[i].traces *if* this frame references selected trace(s) + var newIndices = []; + for (var j = 0; j < traces.length; j++) { + var tr = traces[j]; + if (_frames[i].traces.indexOf(tr._originalIndex) > -1) { + newIndices.push(tr._newIndex); + _frames[i].traces.push(tr._newIndex); + } + } + + // nothing to do... + if (newIndices.length === 0) { + continue; + } + + var ctr = 0; + var nFrameTraces = _frames[i].data.length; + + for (var j = 0; j < nFrameTraces; j++) { + var frameTrace = _frames[i].data[j]; + if (!frameTrace.key || frameTrace.set !== group) { + continue; + } + + var matchFunc = getMatchFunc(frameTrace); + var matches = matchFunc(frameTrace.key, keys); + + if (matches.length > 0) { + if (!trace._isSimpleKey) { + frameTrace = subsetArrayAttrs(frameTrace, matches); + } + var d = gd._fullData[newIndices[ctr]]; + if (d.marker) { + frameTrace.marker = d.marker; + } + if (d.line) { + frameTrace.line = d.line; + } + if (d.textfont) { + frameTrace.textfont = d.textfont; + } + ctr = ctr + 1; + _frames[i].data.push(frameTrace); + } + } + + // update gd._transitionData._frameHash + _hash[_frames[i].name] = _frames[i]; + } + + }); + + // dim traces that have a set matching the set of selection sets + var tracesToDim = [], + opacities = [], + sets = Object.keys(this.groupSelections), + n = this.origData.length; + + for (var i = 0; i < n; i++) { + var opacity = this.origOpacity[i] || 1; + // have we already dimmed this trace? Or is this even worth doing? + if (opacity !== this.gd._fullData[i].opacity || this.highlight.opacityDim === 1) { + continue; + } + // is this set an element of the set of selection sets? + var matches = findMatches(sets, [this.gd.data[i].set]); + if (matches.length) { + tracesToDim.push(i); + opacities.push(opacity * this.highlight.opacityDim); + } + } + + if (tracesToDim.length > 0) { + Plotly.restyle(this.gd, {"opacity": opacities}, tracesToDim); + // turn off the selected/unselected API + Plotly.restyle(this.gd, {"selectedpoints": null}); + } + + } + + } +}; + +/* +Note: in all of these match functions, we assume needleSet (i.e. the selected keys) +is a 1D (or flat) array. The real difference is the meaning of haystack. +findMatches() does the usual thing you'd expect for +linked brushing on a scatterplot matrix. findSimpleMatches() returns a match iff +haystack is a subset of the needleSet. findNestedMatches() returns +*/ + +function getMatchFunc(trace) { + return (trace._isNestedKey) ? findNestedMatches : + (trace._isSimpleKey) ? findSimpleMatches : findMatches; +} + +// find matches for "flat" keys +function findMatches(haystack, needleSet) { + var matches = []; + haystack.forEach(function(obj, i) { + if (obj === null || needleSet.indexOf(obj) >= 0) { + matches.push(i); + } + }); + return matches; +} + +// find matches for "simple" keys +function findSimpleMatches(haystack, needleSet) { + var match = haystack.every(function(val) { + return val === null || needleSet.indexOf(val) >= 0; + }); + // yes, this doesn't make much sense other than conforming + // to the output type of the other match functions + return (match) ? [0] : [] +} + +// find matches for a "nested" haystack (2D arrays) +function findNestedMatches(haystack, needleSet) { + var matches = []; + for (var i = 0; i < haystack.length; i++) { + var hay = haystack[i]; + var match = hay.every(function(val) { + return val === null || needleSet.indexOf(val) >= 0; + }); + if (match) { + matches.push(i); + } + } + return matches; +} + +function isPlainObject(obj) { + return ( + Object.prototype.toString.call(obj) === '[object Object]' && + Object.getPrototypeOf(obj) === Object.prototype + ); +} + +function subsetArrayAttrs(obj, indices) { + var newObj = {}; + Object.keys(obj).forEach(function(k) { + var val = obj[k]; + + if (k.charAt(0) === "_") { + newObj[k] = val; + } else if (k === "transforms" && Array.isArray(val)) { + newObj[k] = val.map(function(transform) { + return subsetArrayAttrs(transform, indices); + }); + } else if (k === "colorscale" && Array.isArray(val)) { + newObj[k] = val; + } else if (isPlainObject(val)) { + newObj[k] = subsetArrayAttrs(val, indices); + } else if (Array.isArray(val)) { + newObj[k] = subsetArray(val, indices); + } else { + newObj[k] = val; + } + }); + return newObj; +} + +function subsetArray(arr, indices) { + var result = []; + for (var i = 0; i < indices.length; i++) { + result.push(arr[indices[i]]); + } + return result; +} + +// Convenience function for removing plotly's brush +function removeBrush(el) { + var outlines = el.querySelectorAll(".select-outline"); + for (var i = 0; i < outlines.length; i++) { + outlines[i].remove(); + } +} + + +// https://davidwalsh.name/javascript-debounce-function + +// Returns a function, that, as long as it continues to be invoked, will not +// be triggered. The function will be called after it stops being called for +// N milliseconds. If `immediate` is passed, trigger the function on the +// leading edge, instead of the trailing. +function debounce(func, wait, immediate) { + var timeout; + return function() { + var context = this, args = arguments; + var later = function() { + timeout = null; + if (!immediate) func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) func.apply(context, args); + }; +}; diff --git a/docs/nlp_ch.html b/docs/nlp_ch.html index eccd320..ac43f34 100644 --- a/docs/nlp_ch.html +++ b/docs/nlp_ch.html @@ -6,7 +6,7 @@ 11 NLP és névelemfelismerés | Szövegbányászat és mesterséges intelligencia R-ben - + @@ -55,7 +55,7 @@ - + @@ -337,8 +337,7 @@

11.2 A spacyr haszn

A spaCy használatához Python környezet szükséges, az első használat előtt telepíteni kell a számítógépünkre egy Anaconda alkalmazást: https://www.anaconda.com/. Majd az RStudio/Tools/Global Options menüjében be kell állítanunk a Pyton interpretert, azaz meg kell adnunk, hogy a gépünkön hol található a feltelepített Anaconda. Ezt csak az első használat előtt kell megtennünk, a későbbiekben innen folytathatjuk a modell betöltését.

Ezt követően a már megszokott módon installálnunk kell a reticulate és a spacyr 55 csomagot és telepítenünk a magyar nyelvi modellt. A Pythonban készült spacy-t a spacyr::spacy_install() paranccsal kell telepíteni. A következő lépésben létre kell hoznunk egy conda környezetet, és a huggingface-ről be kell töltenünk a magyar modellt.

conda_install(envname = "spacyr", "https://huggingface.co/huspacy/hu_core_news_lg/resolve/v3.5.2/hu_core_news_lg-any-py3-none-any.whl" , pip = TRUE)
-
spacy_initialize(model = "hu_core_news_lg", condaenv="spacyr")
-#> NULL
+
spacy_initialize(model = "hu_core_news_lg", condaenv="spacyr")

11.2.1 Lemmatizálás, tokenizálás, szófaji egyértelműsítés

Ezután a spacy_parse() függvény segítségével lehetőségünk van a szövegek tokenizálására, szótári alakra hozására (lemmatizálására) és szófaji egyértelműsítésére.

@@ -408,28 +407,28 @@

11.2.1 Lemmatizálás, tokenizál parsedtxt_2 #> # A tibble: 21 × 3 #> # Groups: doc_id [2] -#> doc_id lemma text -#> <chr> <chr> <chr> -#> 1 d1 Budapest Budapest;süt;a;nap;. -#> 2 d1 süt Budapest;süt;a;nap;. -#> 3 d1 a Budapest;süt;a;nap;. -#> 4 d1 nap Budapest;süt;a;nap;. -#> 5 d1 . Budapest;süt;a;nap;. -#> 6 d2 tájékoztat tájékoztat;ön;,;hogy;az;ülés;vezetés;Hegedűs;Lorántné… +#> doc_id lemma text +#> <chr> <chr> <chr> +#> 1 d1 Budapest Budapest;süt;a;nap;. +#> 2 d1 süt Budapest;süt;a;nap;. +#> 3 d1 a Budapest;süt;a;nap;. +#> 4 d1 nap Budapest;süt;a;nap;. +#> 5 d1 . Budapest;süt;a;nap;. +#> 6 d2 tájékoztat tájékoztat;ön;,;hogy;az;ülés;vezetés;Hegedűs;Lorántné;és;Szűcs;Lajos;jegyző;lesz;segítség;. #> # ℹ 15 more rows

Mivel az eredeti táblában minden lemma az eredeti az azt tartalmazó dokumentum id-jét kapta meg, az így létrehozott táblánkban a szövegek annyiszor ismétlődnek, ahány lemmából álltak. Ezért egy következő lépésben ki kell törtölnünk a feleslegesen ismétlődő sorokat. Ehhez először töröljük a lemma oszlopot, hogy a sorok tökéletesen egyezzenek.

parsedtxt_2$lemma <- NULL
 parsedtxt_2
 #> # A tibble: 21 × 2
 #> # Groups:   doc_id [2]
-#>   doc_id text                                                             
-#>   <chr>  <chr>                                                            
-#> 1 d1     Budapest;süt;a;nap;.                                             
-#> 2 d1     Budapest;süt;a;nap;.                                             
-#> 3 d1     Budapest;süt;a;nap;.                                             
-#> 4 d1     Budapest;süt;a;nap;.                                             
-#> 5 d1     Budapest;süt;a;nap;.                                             
-#> 6 d2     tájékoztat;ön;,;hogy;az;ülés;vezetés;Hegedűs;Lorántné;és;Szűcs;L…
+#>   doc_id text                                                                                       
+#>   <chr>  <chr>                                                                                      
+#> 1 d1     Budapest;süt;a;nap;.                                                                       
+#> 2 d1     Budapest;süt;a;nap;.                                                                       
+#> 3 d1     Budapest;süt;a;nap;.                                                                       
+#> 4 d1     Budapest;süt;a;nap;.                                                                       
+#> 5 d1     Budapest;süt;a;nap;.                                                                       
+#> 6 d2     tájékoztat;ön;,;hogy;az;ülés;vezetés;Hegedűs;Lorántné;és;Szűcs;Lajos;jegyző;lesz;segítség;.
 #> # ℹ 15 more rows

Majd a következő lépésben a dplyr csomag distinct függvénye segítségével - amely mindig csak egy-egy egyedi sort tart meg az adattáblában - kitöröljük a felesleges sorokat.


@@ -438,10 +437,10 @@ 

11.2.1 Lemmatizálás, tokenizál parsedtxt_3 #> # A tibble: 2 × 2 #> # Groups: doc_id [2] -#> doc_id text -#> <chr> <chr> -#> 1 d1 Budapest;süt;a;nap;. -#> 2 d2 tájékoztat;ön;,;hogy;az;ülés;vezetés;Hegedűs;Lorántné;és;Szűcs;L…

+#> doc_id text +#> <chr> <chr> +#> 1 d1 Budapest;süt;a;nap;. +#> 2 d2 tájékoztat;ön;,;hogy;az;ülés;vezetés;Hegedűs;Lorántné;és;Szűcs;Lajos;jegyző;lesz;segítség;.

Az így létrejött adattáblában a text mezőben már nem az eredeti szöveg, hanem annak lemmái szerepelnek. Ha az adattáblát elmentjük, a lemmákon végezhetjük tovább az elemzéseket.

@@ -477,13 +476,13 @@

11.2.2 Saját .txt v head(parsed_df_3, 5) #> # A tibble: 5 × 2 #> # Groups: doc_id [5] -#> doc_id text -#> <chr> <chr> -#> 1 text1 "VONA GÁBOR ( Jobbik ): Tisztelt Elnök Úr ! tisztelt Országgyűlé… -#> 2 text2 "dr. SCHIFFER ANDRÁS ( LMP ): Köszön a szó , elnök úr . tisztelt… -#> 3 text3 "dr. SZÉL BERNADETT ( LMP ): Köszön a szó , elnök úr . tisztelt … -#> 4 text4 "TÓBIÁS JÓZSEF ( MSZP ): Köszön a szó , elnök úr . tisztelt Ház … -#> 5 text5 "SCHMUCK ERZSÉBET ( LMP ): Köszön a szó , elnök úr . tisztelt Or…

+#> doc_id text +#> <chr> <chr> +#> 1 text1 "VONA GÁBOR ( Jobbik ): Tisztelt Elnök Úr ! tisztelt Országgyűlés ! a tegnapi nap 11 hely tart időközi önkormányzati válas… +#> 2 text2 "dr. SCHIFFER ANDRÁS ( LMP ): Köszön a szó , elnök úr . tisztelt Országgyűlés ! múlt hét napvilág lát a hír , hogy az ille… +#> 3 text3 "dr. SZÉL BERNADETT ( LMP ): Köszön a szó , elnök úr . tisztelt Államtitkár Úr ! van egy ország Európa , amely pusztító te… +#> 4 text4 "TÓBIÁS JÓZSEF ( MSZP ): Köszön a szó , elnök úr . tisztelt Ház ! tisztelt Képviselőtárs ! az hisz , az mindannyi egyetért… +#> 5 text5 "SCHMUCK ERZSÉBET ( LMP ): Köszön a szó , elnök úr . tisztelt Országgyűlés ! hét , hónap óta rágódik az ország a Magyar Ne…

A nagyobb fájlok lemmatizálásának eredményét célszerű elmenteni a kötetben ismert módok egyikén például RDS vagy .csv fájlba.

diff --git a/docs/scaling.html b/docs/scaling.html index 1978ae5..bbfcafa 100644 --- a/docs/scaling.html +++ b/docs/scaling.html @@ -6,7 +6,7 @@ 9 Szövegskálázás | Szövegbányászat és mesterséges intelligencia R-ben - + @@ -55,7 +55,7 @@ - + @@ -347,45 +347,34 @@

9.1 Fogalmi alapok
glimpse(parl_beszedek)
 #> Rows: 10
 #> Columns: 4
-#> $ id         <chr> "20142018_024_0002_0002", "20142018_055_0002_0002", "…
-#> $ text       <chr> "VONA GÁBOR (Jobbik): Tisztelt Elnök Úr! Tisztelt Ors…
-#> $ felszolalo <chr> "Vona Gábor (Jobbik)", "Dr. Schiffer András (LMP)", "…
-#> $ part       <chr> "Jobbik", "LMP", "LMP", "MSZP", "LMP", "MSZP", "Jobbi…
+#> $ id         <chr> "20142018_024_0002_0002", "20142018_055_0002_0002", "20142018_064_0002_0002", "20142018_115_0002_0002", "201420…
+#> $ text       <chr> "VONA GÁBOR (Jobbik): Tisztelt Elnök Úr! Tisztelt Országgyűlés! A tegnapi napon 11 helyen tartottak időközi önk…
+#> $ felszolalo <chr> "Vona Gábor (Jobbik)", "Dr. Schiffer András (LMP)", "Dr. Szél Bernadett (LMP)", "Tóbiás József (MSZP)", "Schmuc…
+#> $ part       <chr> "Jobbik", "LMP", "LMP", "MSZP", "LMP", "MSZP", "Jobbik", "Fidesz", "KDNP", "Fidesz"
 glimpse(beszedek_tiszta)
 #> Rows: 10
 #> Columns: 4
-#> $ id         <chr> "20142018_024_0002_0002", "20142018_055_0002_0002", "…
-#> $ text       <chr> "vona gábor jobbik tisztelt elnök úr tisztelt országg…
-#> $ felszolalo <chr> "Vona Gábor (Jobbik)", "Dr. Schiffer András (LMP)", "…
-#> $ part       <chr> "Jobbik", "LMP", "LMP", "MSZP", "LMP", "MSZP", "Jobbi…

+#> $ id <chr> "20142018_024_0002_0002", "20142018_055_0002_0002", "20142018_064_0002_0002", "20142018_115_0002_0002", "201420… +#> $ text <chr> "vona gábor jobbik tisztelt elnök úr tisztelt országgyűlés a tegnapi napon helyen tartottak időközi önkormányza… +#> $ felszolalo <chr> "Vona Gábor (Jobbik)", "Dr. Schiffer András (LMP)", "Dr. Szél Bernadett (LMP)", "Tóbiás József (MSZP)", "Schmuc… +#> $ part <chr> "Jobbik", "LMP", "LMP", "MSZP", "LMP", "MSZP", "Jobbik", "Fidesz", "KDNP", "Fidesz"

A wordfish és wordscores algoritmus is ugyanazt a kiinduló korpuszt és dfm objektumot használja, amit a szokásos módon a quanteda csomag corpus() függvényével hozunk létre.

beszedek_corpus <- corpus(beszedek_tiszta)
 
 summary(beszedek_corpus)
 #> Corpus consisting of 10 documents, showing 10 documents:
 #> 
-#>    Text Types Tokens Sentences                     id
-#>   text1   442    819         1 20142018_024_0002_0002
-#>   text2   354    607         1 20142018_055_0002_0002
-#>   text3   426    736         1 20142018_064_0002_0002
-#>   text4   314    538         1 20142018_115_0002_0002
-#>   text5   354    589         1 20142018_158_0002_0002
-#>   text6   333    538         1 20142018_172_0002_0002
-#>   text7   344    559         1 20142018_206_0002_0002
-#>   text8   352    628         1 20142018_212_0002_0002
-#>   text9   317    492         1 20142018_236_0002_0002
-#>  text10   343    600         1 20142018_249_0002_0002
-#>                   felszolalo   part
-#>          Vona Gábor (Jobbik) Jobbik
-#>    Dr. Schiffer András (LMP)    LMP
-#>     Dr. Szél Bernadett (LMP)    LMP
-#>         Tóbiás József (MSZP)   MSZP
-#>       Schmuck Erzsébet (LMP)    LMP
-#>     Dr. Tóth Bertalan (MSZP)   MSZP
-#>        Volner János (Jobbik) Jobbik
-#>          Kósa Lajos (Fidesz) Fidesz
-#>         Harrach Péter (KDNP)   KDNP
-#>  Dr. Gulyás Gergely (Fidesz) Fidesz
+#> Text Types Tokens Sentences id felszolalo part +#> text1 442 819 1 20142018_024_0002_0002 Vona Gábor (Jobbik) Jobbik +#> text2 354 607 1 20142018_055_0002_0002 Dr. Schiffer András (LMP) LMP +#> text3 426 736 1 20142018_064_0002_0002 Dr. Szél Bernadett (LMP) LMP +#> text4 314 538 1 20142018_115_0002_0002 Tóbiás József (MSZP) MSZP +#> text5 354 589 1 20142018_158_0002_0002 Schmuck Erzsébet (LMP) LMP +#> text6 333 538 1 20142018_172_0002_0002 Dr. Tóth Bertalan (MSZP) MSZP +#> text7 344 559 1 20142018_206_0002_0002 Volner János (Jobbik) Jobbik +#> text8 352 628 1 20142018_212_0002_0002 Kósa Lajos (Fidesz) Fidesz +#> text9 317 492 1 20142018_236_0002_0002 Harrach Péter (KDNP) KDNP +#> text10 343 600 1 20142018_249_0002_0002 Dr. Gulyás Gergely (Fidesz) Fidesz

A leíró statisztikai táblázatban látszik, hogy a beszédek hosszúsága nem egységes, a leghosszabb 819, a legrövidebb pedig 492 szavas. Az átlagos dokumentum hossz az 611 szó. A korpusz szemléltető célú, alaposabb elemzéshez hosszabb és/vagy több dokumentummal érdemes dolgoznunk.

A korpusz létrehozása után elkészítjük a dfm mátrixot, amelyből eltávolítjuk a magyar stopszvakat a HunMineR beépített szótára segítségével.

stopszavak <- HunMineR::data_stopwords_extra
@@ -404,28 +393,17 @@ 

9.2 Wordscoresdocvars(beszedek_dfm, "referencia_pont")[10] <- 1 docvars(beszedek_dfm) -#> id felszolalo part -#> 1 20142018_024_0002_0002 Vona Gábor (Jobbik) Jobbik -#> 2 20142018_055_0002_0002 Dr. Schiffer András (LMP) LMP -#> 3 20142018_064_0002_0002 Dr. Szél Bernadett (LMP) LMP -#> 4 20142018_115_0002_0002 Tóbiás József (MSZP) MSZP -#> 5 20142018_158_0002_0002 Schmuck Erzsébet (LMP) LMP -#> 6 20142018_172_0002_0002 Dr. Tóth Bertalan (MSZP) MSZP -#> 7 20142018_206_0002_0002 Volner János (Jobbik) Jobbik -#> 8 20142018_212_0002_0002 Kósa Lajos (Fidesz) Fidesz -#> 9 20142018_236_0002_0002 Harrach Péter (KDNP) KDNP -#> 10 20142018_249_0002_0002 Dr. Gulyás Gergely (Fidesz) Fidesz -#> referencia_pont -#> 1 NA -#> 2 NA -#> 3 -1 -#> 4 NA -#> 5 NA -#> 6 NA -#> 7 NA -#> 8 NA -#> 9 NA -#> 10 1

+#> id felszolalo part referencia_pont +#> 1 20142018_024_0002_0002 Vona Gábor (Jobbik) Jobbik NA +#> 2 20142018_055_0002_0002 Dr. Schiffer András (LMP) LMP NA +#> 3 20142018_064_0002_0002 Dr. Szél Bernadett (LMP) LMP -1 +#> 4 20142018_115_0002_0002 Tóbiás József (MSZP) MSZP NA +#> 5 20142018_158_0002_0002 Schmuck Erzsébet (LMP) LMP NA +#> 6 20142018_172_0002_0002 Dr. Tóth Bertalan (MSZP) MSZP NA +#> 7 20142018_206_0002_0002 Volner János (Jobbik) Jobbik NA +#> 8 20142018_212_0002_0002 Kósa Lajos (Fidesz) Fidesz NA +#> 9 20142018_236_0002_0002 Harrach Péter (KDNP) KDNP NA +#> 10 20142018_249_0002_0002 Dr. Gulyás Gergely (Fidesz) Fidesz 1

A lenti wordscores-modell specifikáció követi a Laver, Benoit, and Garry (2003) tanulmányban leírtakat.

beszedek_ws <- textmodel_wordscores(
   x = beszedek_dfm,
@@ -455,10 +433,10 @@ 

9.2 Wordscores#> #> Wordscores: #> (showing first 10 elements) -#> tisztelt elnök országgyűlés ország nemhogy -#> -0.1027 0.3691 0.0408 -1.0000 -1.0000 -#> tette fidesz taps padsoraiban természetesen -#> -1.0000 1.0000 -1.0000 -1.0000 1.0000

+#> tisztelt elnök országgyűlés ország nemhogy tette fidesz taps padsoraiban +#> -0.1027 0.3691 0.0408 -1.0000 -1.0000 -1.0000 1.0000 -1.0000 -1.0000 +#> természetesen +#> 1.0000

Az illesztett wordscores modellünkkel ezek után már meg tudjuk becsülni a korpuszban lévő többi dokumentum pozícióját. Ehhez a predict() függvény megoldását használjuk. A kiegészítő opciókkal a konfidencia intervallum alsó és felső határát is meg tudjuk becsülni, ami jól jön akkor, ha szeretnénk ábrázolni az eredményt.

beszedek_ws_pred <- predict(
   beszedek_ws, 
@@ -511,8 +489,8 @@ 

9.2 Wordscores ggplotly(party_df, height = 1000, tooltip = "fit")

-
- +
+

Ábra 9.1: A párton belüli wordscores-alapú skála

@@ -542,21 +520,15 @@

9.3 Wordfish#> text10 1.6465 0.0426 #> #> Estimated Feature Scores: -#> vona gábor jobbik tisztelt elnök országgyűlés tegnapi napon -#> beta -0.961 0.201 0.292 -0.268 -0.0345 -0.837 -1.11 -0.257 -#> psi -2.877 -2.106 -0.413 0.263 -0.2174 -0.770 -2.22 -1.682 -#> helyen tartottak időközi önkormányzati választásokat érdekelt -#> beta -0.882 -0.961 -0.961 -0.961 -0.961 -0.961 -#> psi -1.249 -2.877 -2.877 -2.877 -2.877 -2.877 -#> recsken ózdon október nyertünk örömmel közlöm ország közvéleményével -#> beta -1.11 -1.16 -0.50 -0.961 -0.961 -0.961 -2.205 -0.961 -#> psi -2.22 -1.84 -1.41 -2.877 -2.877 -2.877 -0.785 -2.877 -#> amúgy tudnak mindkét jobbikos polgármester maradt tisztségében -#> beta -0.961 -0.404 -0.961 -0.961 -0.961 -0.961 -0.961 -#> psi -2.877 -1.690 -2.877 -2.877 -2.877 -2.877 -2.877 -#> nemhogy -#> beta -1.89 -#> psi -2.54

+#> vona gábor jobbik tisztelt elnök országgyűlés tegnapi napon helyen tartottak időközi önkormányzati választásokat +#> beta -0.961 0.201 0.292 -0.268 -0.0345 -0.837 -1.11 -0.257 -0.882 -0.961 -0.961 -0.961 -0.961 +#> psi -2.877 -2.106 -0.413 0.263 -0.2174 -0.770 -2.22 -1.682 -1.249 -2.877 -2.877 -2.877 -2.877 +#> érdekelt recsken ózdon október nyertünk örömmel közlöm ország közvéleményével amúgy tudnak mindkét jobbikos polgármester +#> beta -0.961 -1.11 -1.16 -0.50 -0.961 -0.961 -0.961 -2.205 -0.961 -0.961 -0.404 -0.961 -0.961 -0.961 +#> psi -2.877 -2.22 -1.84 -1.41 -2.877 -2.877 -2.877 -0.785 -2.877 -2.877 -1.690 -2.877 -2.877 -2.877 +#> maradt tisztségében nemhogy +#> beta -0.961 -0.961 -1.89 +#> psi -2.877 -2.877 -2.54

Amennyiben szeretnénk a szavak szintjén is megnézni a \(\beta\) (a szavakhoz társított súly, ami a relatív fontosságát mutatja) és \(\psi\) (a szó rögzített hatást (word fixed effects), ami az eltérő szófrekvencia kezeléséért felelős) koefficienseket, akkor a beszedek_wf objektumban tárolt értékeket egy data frame-be tudjuk bemásolni. A dokumentumok hosszát és a szófrekvenciát figyelembe véve, a negatív \(\beta\) értékű szavakat gyakrabban használják a negatív \(\theta\) koefficienssel rendelkező politikusok.

szavak_wf <- data.frame(
   word = beszedek_wf$features, 
@@ -660,8 +632,8 @@ 

9.3 Wordfish ggplotly(speech_df, height = 1000, tooltip = "theta")

-
- +
+

Ábra 9.4: Párton belüli pozíciók

diff --git a/docs/search_index.json b/docs/search_index.json index e94bfb3..7b49f3d 100644 --- a/docs/search_index.json +++ b/docs/search_index.json @@ -1 +1 @@ -[["index.html", "Szövegbányászat és mesterséges intelligencia R-ben Üdvözöljük! 2.0 - Online frissítések", " Szövegbányászat és mesterséges intelligencia R-ben Sebők Miklós, Ring Orsolya, Máté Ákos 2023 Üdvözöljük! Könyvünk bevezeti az érdeklődőket a szövegbányászat és a mesterséges intelligencia társadalomtudományi alkalmazásának speciális problémáiba. Támaszkodva a Sebők Miklós által szerkesztett Kvantitatív szövegelemzés és szövegbányászat a politikatudományban (L’Harmattan, 2016) című kötet elméleti bevezetésére, ezúttal a társadalomtudományi elemzések során használható kvantitatív szövegelemzés legfontosabb gyakorlati feladatait vesszük sorra. A szövegek adatként való értelmezése (text as data) és kvantitatív elemzése, avagy a szövegbányászat (text mining) a nemzetközi társadalomtudományi kutatások egyik leggyorsabban fejlődő irányzata. A szövegbányászat emellett a társadalomtudósok számára az egyik legnyilvánvalóbb belépési pont a mesterséges intelligenciát, ezen belül is gépi tanulást alkalmazó kutatások területére. A magyar tankönyvpiacon elsőként ismertetünk lépésről-lépésre a nemzetközi társadalomtudományban használatos olyan kvantitatív szövegelemzési eljárásokat, mint a névelemfelismerés, a véleményelemzés, a topikmodellezés, illetve a szövegek felügyelt tanulásra épülő osztályozása. A módszereink bemutatására szolgáló elemzéseket az egyik leggyakrabban használt programnyelv, az R segítségével végeztük el. A kötet anyaga akár minimális programozási ismerettel is elsajátítható, így teljesen kezdők számára is ajánlott. A hazai olvasók érdeklődését szem előtt tartva példáink döntő többsége új, magyar nyelvű korpuszokra épül, melyek alapján megismerhetők a magyar nyelvű kvantitatív szövegelemzés módozatai. A könyv megrendelhető a Typotex kiadó honlapján! 2.0 - Online frissítések A Szövegbányászat és mesterséges intelligencia R-ben szerkesztette Sebők Miklós, Ring Orsolya, és Máté Ákos frissített verziója már online elérhető, a könyv a MILAB projekt támogatásával került publikációra. Az eredeti kézirat több részét is kiegészítettük mind Szövegbányászati módszertannal, gyakorlati példákkal, és ábrázolási technikákkal. Az újítások teljes listája: A szövegbányászat egy részletesebb meghatározása és definíciója a könyv bevezetőjében. A gyakorlati szövegbányászat fő lépéseit egy egyszerű példa mutatja be az olvasó számára Ady Endre és Petőfi Sándor versein keresztül. A könyvben felhasznált adatokhoz és a könyvben előállított ábrákhoz külön leíró részeket helyeztünk a szövegbe. Egy új alfejezet segíti az olvasót a saját adatai használatához. A könyv statikus ábráit interaktív megfelelőikre cseréltük ki. Új információk az adatábrázolással foglalkozó alfejezetben interaktív ábrák előállítására vonatkozóan. Az Osztályozás és felügyelt tanulás című fejezet immár mind az SVM és a Naïve Bayes módszer R-n belüli alkalmazását is bemutatja. Az Osztályozás és felügyelt tanulás című fejezetet kiegészítettük az itt bemutatott módszerek működésének közérthető leírásával. A Munka karakter vektorokkal című alfejezetet kiegészítettük az n-grammok leírásával. Javasolt hivatkozás: Sebők Miklós, Ring Orsolya, és Máté Ákos. 2021. Szövegbányászat és Mesterséges Intelligencia R-ben. Budapest: Typotex. Bib formátumban: @book{sebokringmate2021szovegbanyaszat, address = {Budapest}, title = {Szövegbányászat és mesterséges intelligencia {R}-ben}, publisher = {Typotex}, author = {Sebők, Miklós and Ring, Orsolya and Máté, Ákos}, year = {2021} } A kötet alapjául szolgáló kutatást, amelyet a Társadalomtudományi Kutatóközpont valósított meg, az Innovációs és Technológiai Minisztérium és a Nemzeti Kutatási, Fejlesztési és Innovációs Hivatal támogatta a Mesterséges Intelligencia Nemzeti Laboratórium keretében. A kötet megjelenését az MTA Könyvkiadási Alapja, a Társadalomtudományi Kutatóközpont Könyvtámogatási Alapja, a Nemzeti Kutatási, Fejlesztési és Innovációs Hivatal (NKFIH FK 123907, NKFIH FK 129018), valamint az MTA Bolyai János Kutatási Ösztöndíja támogatta. "],["intro.html", "1 Bevezetés 1.1 A kötet témái 1.2 Használati utasítás 1.3 A HunMineR használata 1.4 Egy bevezető példa 1.5 Köszönetnyilvánítás", " 1 Bevezetés 1.1 A kötet témái A szövegek adatként való értelmezése (text as data) és kvantitatív elemzése (quantitative text analysis), avagy a szövegbányászat (text mining) a nemzetközi társadalomtudományi kutatások egyik leggyorsabban fejlődő irányzata. A szövegbányászat egy olyan folyamat, amely során nyers, strukturálatlan szövegeket (pl.: beszédek, felszólalások, újságcikkek) egy strukturált formátumba helyezünk, hogy így új, korábban ismeretlen információkhoz tudjunk hozzájutni, mint trendekhez, mintázatokhoz, összefüggésekhez stb. A szövegek és más kvalitatív adatok (filmek, képek) elemzése annyiban különbözik a mennyiségi (kvantitatív) adatokétól, hogy nyers formájukban még nem alkalmasak statisztikai, illetve ökonometriai elemzésre. Ezért van szükség az ezzel összefüggő módszertani problémák speciális tárgyalására. Jelen kötet bevezeti az érdeklődőket a szövegbányászat és a mesterséges intelligencia társadalomtudományi alkalmazásának ilyen speciális problémáiba, valamint ezek gyakorlati megoldásába. Közvetlen előzménynek tekinthető a témában a Sebők Miklós által szerkesztett Kvantitatív szövegelemzés és szövegbányászat a politikatudományban címmel megjelent könyv, amely a magyar tudományos diskurzusban kevésbé bevett alapfogalmakat és eljárásokat mutatta be (Sebők 2016). A hangsúly az elméleten volt, bár számos fejezet foglalkozott konkrét kódrészletek elemzésével. Míg az előző kötet az egyes kódolási eljárásokat, illetve ezek kutatásmódszertani előnyeit és hátrányait ismertette, ezúttal a társadalomtudományi elemzések során használható kvantitatív szövegelemzés legfontosabb gyakorlati feladatait vesszük sorra, ezek egymással való logikai kapcsolatátt illusztrálja a lenti ábra. A könyvben bemutatott kódok elsősorban a quanteda csomagot illetve egyes a quanteda-ra épülő csomagokat használnak (Benoit et al. 2018). A szövegbányászat könyvünkben bemutatott egyes módszerei A kötetünkben tárgyalt egyes módszereket három kategóriába sorolhatjuk, a működési elvük alapján. A szövegek statisztikai leírása egyszerre lehet két szöveg összehasonlítása például a Koszinusz vagy Jaccard hasonlóság alapján, melyekről a Szövegösszehasonlítás fejzetben írunk, valamint egy adott szöveg különböző statisztikai jellemzőinek leírása, az erre voantkozó módszerekkel Leíró statisztika című fejezet foglalkozik. A szótáralapú módszerekről és ezek érzelemelemzésre való használatát a Szótárak és érzelemelemzés című fejezet tárgyalja. Az ideológiai skálázás során politikai szereplők egymáshoz viszonyított realtív pozícióit azonosítjuk általában beszédek, illetve felszólalások alapján az erre vonatkozó módszertant a Szövegskálázás című fejezet részletezi. A szövegbányászat egyik legalapvetőbb problémája a dokumentumok egyes kategóriákba való csoportosítása. Az erre vonatkozó módszerek két alkategóriája közül is többet mutatunk be. Amikor ismert kategóriákba történő osztályzásról beszélünk, akkor a kutatónak kell meghatároznia előre a kategóriákat a modell számára, mielőtt az replikálja a besorolásokat. Mind a Naïve Bayes és a Support Vector Machine ezt a feladatot látja el ezek működéséről és alkalmazásáról az Osztályozás és felügyelt tanulás című fejezetben írunk. Ezzel szemben ismeretlen kategóriákba való csoportosítás esetén a kutató nem ad előzőleges utatsítást az algoritmus számára, a modell a csoportosítást a dokumentumok szövegében meglévő látens mintázatok alapján végzi el. Az ismeretlen kategóriákba történő csoportosítást a könyvben bemutatott több módszerrel is elvégezhetjók, mint a K-közép klaszterezés, Latent Dirichlet Allocation, és Struktúrális topikmodellek, amelyeket a Felügyelet nélküli tanulás - Topikmodellezés című fejezetben tárgyaljuk. A Végezetül pedig a szóbeágyazások ellentétben a korábbiakban említett szózsák logikát követő módszerekkel képesek egy dokumentum szóhasználatának belső összefüggéseit azonosítani, ezzel a kifejezetten rugalmas kutatási módszerrel, illetve alkalmazásával Szóbeágyazások fejezetben foglalkozunk. Könyvünk a magyar tankönyvpiacon elsőként ismerteti lépésről-lépésre a nemzetközi társadalomtudományban használatos kvantitatív szövegelemzési eljárásokat. A módszereink bemutatására szolgáló elemzéseket az R programnyelv segítségével végeztük el, mely a nemzetközi társadalomtudományi vizsgálatok során egyik leggyakrabban használt környezet a Python mellett. A kötetben igyekeztünk magyar szakkifejezéseket használni, de mivel a szövegbányászat nyelve az angol, mindig megadtuk, azok angol megfelelőjét is. Kivételt képeznek azok az esetek, ahol nincs használatban megfelelő magyar terminológia, ezeknél megtartottuk az angol kifejezéseket, de magyarázattal láttuk el azokat. Az olvasó a két kötet együttes használatával olyan ismeretek birtokába jut, melyek révén képes lesz alkalmazni a kvantitatív szövegelemzés és szövegbányászat legalapvetőbb eljárásait saját kutatásaiban. Deduktív vagy induktív felfedező logikája szerint dönthet az adatelemzés módjáról, és a felkínált menüből kiválaszthatja a kutatási tervéhez legjobban illeszkedő megoldásokat. A bemutatott konkrét példák segítségével pedig akár reprodukálhatja is ezen eljárásokat saját kutatásában. Mindezt a kötet fejezeteiben bőséggel tárgyalt R-scriptek (kódok) részletes leírása is segíti. Ennek alapján a kötet két fő célcsoportja a társadalomtudományi kutatói és felsőoktatási hallgatói-oktatói közösség. Az oktatási alkalmazást segítheti a fontosabb fogalmak magyar és angol nyelvű tárgymutatója, valamint több helyen a további olvasásra ajánlott szakirodalom felsorolása. A kötet honlapján (https://tankonyv.poltextlab.com) közvetlenül is elérhetőek a felhasznált adatbázisok és kódok. Kötetünk négy logikai egységből épül fel. Az első négy fejezet bemutatja azokat a fogalmakat és eljárásokat, amelyek elengedhetetlenek egy szövegbányászati kutatás során, valamint itt kerül sor a szöveges adatforrásokkal való munkafolyamat ismertetésére, a szövegelőkészítés és a korpuszépítés technikáinak bemutatására. A második blokkban az egyszerűbb elemzési módszereket tárgyaljuk, így a leíró statisztikák készítését, a szótár alapú elemzést, valamint érzelemelemzést. A kötet harmadik blokkját a mesterséges intelligencia alapú megközelítéseknek szenteljük, melynek során az olvasó a felügyelt és felügyelet nélküli tanulás fogalmával ismerkedhet meg. A felügyelet nélküli módszerek közül a topik-modellezést, szóbeágyazást és a szövegskálázás wordfish módszerét mutatjuk be, a felügyelt elemzések közül pedig az osztályozással foglalkozunk részletesebben. Végezetül kötetünket egy függelék zárja, melyben a kezdő RStudió felhasználóknak adunk gyakorlati iránymutatást a programfelülettel való megismerkedéshez, használatának elsajátításához. 1.2 Használati utasítás A könyv célja, hogy keresztmetszeti képet adjon a szövegbányászat R programnyelven használatos eszközeiről. A fejezetekben ezért a magyarázó szövegben maga az R kód is megtalálható, illetve láthatóak a lefuttatott kód eredményei. Az alábbi példában a sötét háttér az R környezetet jelöli, ahol az R kód betűtípusa is eltérő a főszövegétől. A kód eredményét pedig a #> kezdetű sorokba szedtük, ezzel szimulálva az R console ablakát. # példa R kód 1 + 1 #> [1] 2 Az egyes fejezetekben szereplő kódrészleteket egymás utáni sorrendben bemásolva és lefuttatva a saját R környezetünkben tudjuk reprodukálni a könyvben szereplő technikákat. A Függelékben részletesebben is foglalkozunk az R és az RStudio beállításaival, használatával. Az ajánlott R minimum verzió a 4.0.0, illetve az ajánlott minimum RStudio verzió az 1.4.0000.1 A könyvhöz tartozik egy HunMineR nevű R csomag is, amely tartalmazza az egyes fejezetekben használt összes adatbázist, így az adatbeviteli problémákat elkerülve lehet gyakorolni a szövegbányászatot. A könyv megjelenésekor a csomag még nem került be a központi R CRAN csomag repozitóriumába, hanem a poltextLAB GitHub repozitóriumából tölthető le. A könyvben szereplő ábrák nagy része a ggplot2 csomaggal készült a theme_set(theme_light()) opció beállításával a háttérben. Ez azt jelenti, hogy az ábrákat előállító kódok a theme_light() sort nem tartalmazzák, de a tényleges ábrán már megjelennek a tematikus elemek. Az egyes fejezetekben használt R csomagok listája és verziószáma a lenti táblázatban található. Fontos tudni, hogy a használt R csomagokat folyamatosan fejlesztik, ezért elképzelhető hogy eltérő verziószámú változatok esetén változhat a kód szintaxis. A könyvben használt R csomagok Csomagnév Verziószám dplyr 1.0.5 e1071 1.7.6 factoextra 1.0.7 gapminder 0.3.0 GGally 2.1.1 ggdendro 0.1.22 ggplot2 3.3.3 ggrepel 0.9.1 HunMineR 0.0.0.9000 igraph 1.2.6 kableExtra 1.3.4 knitr 1.33 lubridate 1.7.10 purrr 0.3.4 quanteda 3.0.0 quanteda.textmodels 0.9.4 quanteda.textplots 0.94 quanteda.textstats 0.94 readr 1.4.0 readtext 0.80 readxl 1.3.1 rvest 1.0.0 spacyr 1.2.1 stm 1.3.6 stringr 1.4.0 text2vec 0.6 tibble 3.1.1 tidyr 1.1.3 tidytext 0.3.1 topicmodels 0.2.12 1.3 A HunMineR használata A Windows rendszert használóknak először az installr csomagot kell telepíteni, majd annak segítségével letölteni az Rtools nevű programot (az OS X és Linux rendszerek esetében erre a lépésre nincs szükség). A lenti kód futtatásával ezek a lépések automatikusan megtörténnek. # az installr csomag letöltése és installálása install.packages("installr") # az Rtools.exe fájl letöltése és installálása installr::install.Rtools() Ezt követően a devtools csomagban található install_github paranccsal tudjuk telepíteni a HunMineR csomagot, a lenti kód lefuttatásával. # A devtools csomag letöltése és installálása install.packages("devtools") # A HunMineR csomag letöltése és installálása devtools::install_github("poltextlab/HunMineR") Ebben a fázisban a data függvénnyel tudjuk megnézni, hogy pontosan milyen adatbázisok szerepelnek a csomagban, illetve ugyanitt megtalálható az egyes adatbázisok részletes leírása. Ha egy adatbázisról szeretnénk többet megtudni, akkor a kiegészítő információkat ?adatbazis_neve megoldással tudjuk megnézni.2 # A HunMineR csomag betöltése library(HunMineR) # csomagban lévő adatok listázása data(package = "HunMineR") # A miniszterelnöki beszédek minta adatbázisának részletei ?data_miniszterelnokok 1.4 Egy bevezető példa A következőkben egy példát mutatunk be a szövegbányászat gyakorlati alkalmazására vonatkozóan, amely során Ady Endre és Petőfi Sándor összes verseinek szóhasználatát hasonlítjuk össze, az eredményekhez pedig szemléletes ábrázolást is készítünk. A jelen példában alkalmazott eljárásokat a későbbi fejezetekben részletesen is kifejtjük itt csupán szemléltetni kívánjuk velük, hogy milyen jellegű elemzéseket sajátíthat majd el az olvasó a könyv segítségével. Az R számos kiegészítő csomaggal rendelkezik, amelyek hasznos funkcióit úgy érhetjük el, ha telepítjük az őket tartalmazó csomagot. Ezt az install.packages paranccsal tehetjük meg, ha már korábban egyszer elvégeztük, akkor nem szükséges egy csomaggal megismételni. A lenti kódsorral a példánkhoz szükséges csomagokat telepíthetjük, de a könyvben használt többi csomagot is ezzel a módszerrel telepíthetjük. install.packages("readtext") install.packages("readr") install.packages("quanteda") install.packages("quanteda.textstats") install.packages("quanteda.textplots") install.packages("ggplot2") install.packages("dplyr") A library() paranccsal pedig betölteni tudjuk a már telepített csomagjainkat, ezt minden alkalommal el kell, hogy végezzük, hogyha ezek egy funkcióját kívánjuk használni. Itt láthatjuk, hogy a két csomag után a HunMineR betöltése is szükséges, viszont annak ellenére, hogy ugyanazzal a funkcióval hívjuk elő a HunMiner nem egy csomag, hanem egy repository (adattár) így a telepítése egy eltérő eljárással zajlik az előző alfejezetben leírtaknak megfelelően. library(readtext) library(readr) library(quanteda) library(quanteda.textstats) library(quanteda.textplots) library(ggplot2) library(dplyr) library(HunMineR) Ezt követően betöltjük a szükséges adatokat, jelen esetben a HunMiner adattárából. ady <- HunMineR::data_ady petofi <- HunMineR::data_petofi A betöltött adattáblák megjelennek az Rstudio environemnt fülében, ahol azt is láthatjuk, hogy 619 megfigyelésünk vagyis versünk van a Petőfi táblában, Ady esetében pedig 1309. Ahhoz, hogy adatokat használni tudjunk azokon először mindig úgynevezett tisztítási folyamatokat kell elvégeznünk, valamint át kell alakítanunk az adataink formátumát is. Az úgynevezett szövegtisztítási folyamatok a szövegeink előkészítését jelentik, ha kihagyjuk őket vagy nem végezzük el őket kellő alapossággal, akkor az eredményeink félrevezetőek lesznek. Az R-rel való kódolás során többféle formátumban is tárolhatjuk az adatainkat, illetve az adataink egyik formátumból másikba való átalakítására is van lehetőségünk. Az egyes formátumok az adatokat különböző elrendezésben tárolják, azért van szükségünk gyakran az adataink átalakítására, mivel az R-n belüli funkciókat, amelyekre szükségünk lesz csak specifikus formátumokon hajthatjuk végre. Első lépésként korpusz formátumba helyezzük az adatainkat, erre csupán azért van szükség mert a későbbiekben ebből a formátumból tudjuk majd őket, token formátumba helyezni. ady_corpus <- corpus(ady) petofi_corpus <- corpus(petofi) Ezt követően betölthetjük a HunMiner csomagból a stop szavainkat, ez egy szó lista, amelyet a szövegelőkészítés során elfogunk távolítani a korpuszból. stopszavak <- HunMineR::data_stopwords_extra A corpus formátumban lévő szövegeket nem csak token formátumba helyezzük a lenti kódsorokkal, hanem több szövegtisztító lépést is elvégzünk velük. A remove_punct funkció segítségével eltávolítjuk a mondatvégi írásjeleket, veszőket. A tokens_tolower() parancs segítségével minden szavunk betűjét kisbetűvé alakítjuk így a kódunk nem tesz különbséget ugyanazon szó két alternatív megjelenése között csupán azért, mert az egyik a mondat elején helyezkedik el. Végül pedig a tokens_remove() parancs segítségével eltávolíthatjuk a korábban betöltött stopszavakat a szövegeinkből. ady_tok <- tokens(ady_corpus, remove_punct = TRUE) %>% tokens_tolower() %>% tokens_remove(stopszavak) petofi_tok <- tokens(petofi_corpus, remove_punct = TRUE) %>% tokens_tolower() %>% tokens_remove(stopszavak) Ezt követően a token formátumban lévő adatainkat dfm formátumba helyezzük. ady_dfm <- dfm(ady_tok) petofi_dfm <- dfm(petofi_tok) Majd megnyírbáljuk a szavaink listáját: a dfm_trim() parancs segítségével beállíthatjuk, hogy milyen gyakorisággal megjelenő szavakat hagyjunk az adataink között. Vannak jó okok mind a túlságosan gyakran megjelenő és a túlságosan kevésszer megejelenő szavak eltávolítására is. Ha egy szó nagyon gyakran jelenik meg, akkor feltételezhető, hogy az nem segít a számunkra értelmezni az egyes dokumentumaink közötti különbséget, mivel nem különbözteti meg azokat. Ha pedig egy szó túlságosan kevésszer jelenik meg, akkor az lehet, hogy egy egyedülálló eset, amely nem adhat a dokumentumainkra általánosítható információt a számunkra. Jelen kód előírja, hogy ahhoz, hogy egy szó bekerüljün az összehasonlításunkba legalább 25-ször meg kell jelenie a szövegben. ady_dfm <- dfm_trim(ady_dfm, min_termfreq = 25) petofi_dfm <- dfm_trim(petofi_dfm, min_termfreq = 25) Végül pedig az R programon belül ábrázolhatjuk is az eredményeinket. Jelen esetben nem végeztünk el számítást, hanem a megtisztított szövegeink szavaiból generáltunk szófelhőket. A Petőfi versekből készült szófelhő: textplot_wordcloud(petofi_dfm, min_count = 25, color = "red") Ábra 1.1: Petőfi szófelhő Az Ady versekből készült szófelhő: textplot_wordcloud(ady_dfm, min_count = 25, color = "orange") Ábra 1.2: Ady szófelhő Az így kapott ábrákon az Ady és Petőfi versek szavait láthatjuk egy úgynevezett szófelhőben összegyűjtve. Annál nagyobb méretben jelenít meg az ábránk egy kifejezést, minél gyakrabban fordul az elő a szövegünkben, amely jelen esetben a két költő összes verse. A két szófelhőn látszódik, hogy jelentős az átfedés a felhasznált szavakkal, úgyhogy a folyamatot megismételhetjük, azzal a kiegészítéssel, hogy a szófelhő létrehozása előtt eltávolíjuk mindazokat a szavakat, amelyek jelen vannak a másik korpuszban, így jobban láthatjuk, hogy mely kifejezések különböztetik meg a költőket. Ehhez először kigyűjtjük a featnames() funkció segítségével a két dfm egyegedi szavait. ady_szavak <- featnames(ady_dfm) petofi_szavak <- featnames(petofi_dfm) Ezt követően pedig megismételjük a korábbi előkészítő műveleteket azzal a kiegészítéssel, hogy eltávolítjuk az előbb létrehozott két szólistát is. ady_tok <- tokens(ady_corpus, remove_punct = TRUE) %>% tokens_tolower() %>% tokens_remove(stopszavak) %>% tokens_remove(petofi_szavak) petofi_tok <- tokens(petofi_corpus, remove_punct = TRUE) %>% tokens_tolower() %>% tokens_remove(stopszavak) %>% tokens_remove(ady_szavak) ady_dfm <- dfm(ady_tok) %>% dfm_trim(min_termfreq = 25) petofi_dfm <- dfm(petofi_tok) %>% dfm_trim(min_termfreq = 25) Ha ezzel végeztünk, akkor ismét létrehozhatjuk a két szófelhőnket. textplot_wordcloud(petofi_dfm, min_count = 25, color = "red") Ábra 1.3: Petőfi szófelhő textplot_wordcloud(ady_dfm, min_count = 25, color = "orange") Ábra 1.4: Ady szófelhő Ugyan a szófelhő nem egy statisztikai számítás, amely alapján következtetéseket vonhatnánk le a vizsgált szövegekről, de egy hasznos és látványos ábrázolási módszer, illetve jelen esetben jól példázza az R egy jelentős előnyét, hogy a segítségével az eredményeinket gyorsan és egyszerűen tudjuk ábrázolni. 1.5 Köszönetnyilvánítás Jelen kötet az ELKH Társadalomtudományi Kutatóközpont poltextLAB szövegbányászati kutatócsoportja (http://poltextlab.com/) műhelyében készült. A kötet fejezetei Sebők Miklós, Ring Orsolya és Máté Ákos közös munkájának eredményei. Az Alapfogalmak, illetve a Szövegösszehasonlítás fejezetekben társszerző volt Székely Anna. A Bevezetésben, a Függelékben, és az Adatkezelés R-ben, az Osztályozás és felügyelt tanulás című fejezetekben Gelányi Péter hajtott végre nagyobb frissítéseket, valamint alakította ki a könyv interaktív ábráit. A kézirat a szerzők többéves oktatási gyakorlatára, a hallgatóktól kapott visszajelzésekre építve készült el. Köszönjük a Bibó Szakkollégiumban (2021), a Rajk Szakkollégiumban (2019–2021), valamint a Széchenyi Szakkollégiumban (2019) tartott féléves, valamint a Corvinus Egyetemen és a Társadalomtudományi Kutatóközpontban tartott rövidebb képzési alkalmak résztvevőinek visszajelzéseit. Köszönjük a projekt gyakornokainak, Czene-Joó Máténak, Kaló Eszternek, Meleg Andrásnak, Lovász Dorottyának, Nagy Orsolyának, valamint kutatás asszisztenseinek, Balázs Gergőnek, Gelányi Péternek és Lancsár Eszternek a kézirat végleges formába öntése sorn nyújtott segítséget. Külön köszönet illeti a Társadalomtudományi Kutatóközpont Comparative Agendas Project (https://cap.tk.hu/hu) kutatócsoportjának tagjait, kiemelten Boda Zsoltot, Molnár Csabát és Pokornyi Zsanettet a kötetben használt korpuszok sokéves előkészítéséért. Köszönettel tartozunk az egyes fejezetek alapjául szolgáló elemzések és publikációk társszerzőinek, Barczikay Tamásnak, Berki Tamásnak, Kacsuk Zoltánnak, Kubik Bálintnak, Molnár Csabának és Szabó Martina Katalinnak. Köszönjük Ballabás Dániel szakmai lektor hasznos megjegyzéseit, Fedinec Csilla nyelvi lektor alapos munkáját, valamint a Typotex Kiadó rugalmasságát és színvonalas közreműködését a könyv kiadásában! Végül, de nem utolsósorban hálásak vagyunk a kötet megvalósulásához támogatást nyújtó szervezeteknek és ösztöndíjaknak: az MTA Könyvkiadási Alapjának, a Társadalomtudományi Kutatóközpont Könyvtámogatási Alapjának, a Nemzeti Kutatási, Fejlesztési és Innovációs Hivatalnak (NKFIH FK 123907, NKFIH FK 129018), az MTA Bolyai János Kutatási Ösztöndíjának. A kötet alapjául szolgáló kutatást, amelyet a Társadalomtudományi Kutatóközpont valósított meg, az Innovációs és Technológiai Minisztérium és a Nemzeti Kutatási, Fejlesztési és Innovációs Hivatal támogatta a Mesterséges Intelligencia Nemzeti Laboratórium keretében. Az R Windows, OS X és Linux változatai itt érhetőek el: https://cloud.r-project.org/. Az RStudio pedig innen érhető el: https://www.rstudio.com/products/rstudio/download/.↩︎ Többek között az adat forrása, a változók részletes leírása, illetve az adatbázis mérete is megtalálható így.↩︎ "],["alapfogalmak.html", "2 Kulcsfogalmak 2.1 Big Data és társadalomtudomány 2.2 Fogalmi alapok 2.3 A szövegbányászat alapelvei", " 2 Kulcsfogalmak 2.1 Big Data és társadalomtudomány A szövegek géppel való feldolgozása és elemzése módszertanának számos megnevezése létezik. A szövegelemzés, kvantitatív szövegelemzés, szövegbányászat, természetes nyelvfeldolgozás, automatizált szövegelemzés, automatizált tartalomelemzés és hasonló fogalmak között nincs éles tartalami különbség. Ezek a kifejezések jellemzően ugyanarra az általánosabb kutatási irányra reflektálnak, csupán hangsúlybeli eltolódások vannak köztük, így gyakran szinonimaként is használják őket. A szövegek gépi feldolgozásával foglalkozó tudományág a Big Data forradalom részeként kezdett kialakulni, melyet az adatok egyre nagyobb és diverzebb tömegének elérhető és összegyűjthető jellege hívott életre. Ennek megfelelően az adattudomány számos különböző adatforrás, így képek, videók, hanganyagok, internetes keresési adatok, telefonok lokációs adatai és megannyi különböző információ feldolgozásával foglalkozik. A szöveg is egy az adatbányászat érdeklődési körébe tartozó számos adattípus közül, melynek elemzésére külön kutatási irány alakult ki. Mivel napjainkban minden másodpercben óriási mennyiségű szöveg keletkezik és válik hozzáférhetővé az interneten, egyre nagyobb az igény az ilyen jellegű források és az emberi nyelv automatizált feldolgozására. Ebből adódóan az elemzési eszköztár is egyre szélesebb körű és egyre szofisztikáltabb, így a tartalomelemzési és szövegbányászati ismeretekkel bíró elemzők számára rengeteg értékes információ kinyerhető. Ezért a szövegbányászat nemcsak a társadalomtudósok számára izgalmas kutatási irány, hanem gyakran hasznosítják üzleti célokra is. Gondoljunk például az online sajtótermékekre, az ezekhez kapcsolódó kommentekre vagy a politikusok beszédéire. Ezek mind-mind hatalmas mennyiségben rendelkezésre állnak, hasznosításukhoz azonban képesnek kell lenni ezeket a szövegeket összegyűjteni, megfelelő módon feldolgozni és kiértékelni. A könyv ebben is segítséget nyújt az Olvasónak. Mielőtt azonban az adatkezelés és az elemzés részleteire rátérnénk, érdemes végigvenni néhány elvi megfontolást, melyek nélkülözhetetlenek a leendő elemző számára az etikus, érvényes és eredményes szövegbányászati kutatások kivitelezéséhez. A nagy mennyiségben rendelkezésre álló szöveges források kiváló kutatási terepet kínálnak a társadalomtudósok számára megannyi vizsgálati kérdéshez, azonban fontos tisztában lenni azzal, hogy a mindenki által elérhető adatokat is meglehetősen körültekintően, etikai szempontok figyelembevételével kell használni. Egy másik szempont, amelyet érdemes szem előtt tartani, mielőtt az ember fejest ugrana az adatok végtelenjébe, a 3V elve: volume, velocity, variety vagyis az adatok mérete, a keletkezésük sebessége és azok változatossága (Brady 2019). Ezek mind olyan tulajdonságok, amelyek az adatelemzőt munkája során más (és sok esetben több vagy nagyobb) kihívások elé állítják, mint egy hagyományos statisztikai elemzés esetében. A szövegbányászati módszerek abban is eltérnek a hagyományos társadalomtudományi elemzésektől, hogy – az adattudományokba visszanyúló gyökerei miatt – jelentős teret nyit az induktív (empiricista) kutatások számára a deduktív szemlélettel szemben. A deduktív kutatásmódszertani megközelítés esetén a kutató előre meghatározza az alkalmazandó fogalomrendszert, és azokat az elvárásokat, amelyek teljesülése esetén sikeresnek tekinti az elemzést. Az adattudományban az ilyen megközelítés a felügyelt tanulási feladatokat jellemzi, vagyis azokat a feladatokat, ahol ismert az elvárt eredmény. Ilyen például egy osztályozási feladat, amikor újságcikkeket szeretnénk különböző témakörökbe besorolni. Ebben az esetben az adatok egy részét általában kézzel kategorizáljuk, és a gépi eljárás sikerességét ehhez viszonyítjuk. Mivel az ideális eredmény (osztálycímke) ismert, a gépi teljesítmény könnyen mérhető (például a pontosság, a gép által sikeresen kategorizált cikkek százalékában kifejezve). Az induktív megoldás esetében kevésbé egyértelmű a gépi eljárás teljesítményének mérése, hiszen a rejtett mintázatok feltárását várjuk az algoritmustól, emiatt nincsenek előre meghatározott eredmények sem, amelyekhez viszonyíthatjuk a teljesítményt. Az adattudományban az ilyen feladatokat hívják felügyelet nélküli tanulásnak. Ide tartozik a klaszterelemzés, vagy a topic modellezés, melynek esetén a kutató csak azt határozza meg, hány klasztert, hány témát szeretne kinyerni, a gép pedig létrehozza az egymáshoz leghasonlóbb csoportokat. Értelemszerűen itt a kutatói validálás jóval nagyobb hangsúlyt kap, mint a deduktív megközelítés esetében. Egy harmadik, középutas megoldás a megalapozott elmélet megközelítése, mely ötvözi az induktív és a deduktív módszer előnyeit. Ennek során a kutató kidolgoz egy laza elméleti keretet, melynek alapján elvégzi az elemzést, majd az eredményeket figyelembe véve finomít a fogalmi keretén, és újabb elemzést futtat, addig folytatva ezt az iterációt, amíg a kutatás eredményeit kielégítőnek nem találja. A szövegbányászati elemzéseket kategorizálhatjuk továbbá a gépi hozzájárulás mértéke szerint. Ennek megfelelően megkülönböztethetünk kézi, géppel támogatott és gépi eljárásokat. Mindhárom megközelítésnek megvan a maga előnye. A kézi megoldások esetén valószínűbb, hogy azt mérjük a szövegünkben, amit mérni szeretnénk (például bizonyos szakpolitikai tartalmat), ugyanakkor ez idő- és költségigényes. A gépi eljárások ezzel szemben költséghatékonyak és gyorsak, de fennáll a veszélye, hogy nem azt mérjük, amit eredetileg mérni szerettünk volna (ennek megállapításában ismét a validálás kap kulcsszerepet). Továbbá lehetséges kézzel támogatott gépi megoldások alkalmazása, ahol a humán és a gépi elemzés ideális arányának megtalálása jelenti a fő kihívást. 2.2 Fogalmi alapok Miután áttekintettük a szövegbányászatban használatos elméleti megközelítéseket, érdemes tisztázni a fogalmi alapokat is. A szövegbányászat szempontjából a szöveg is egy adat. Az elemzéshez használatos strukturált adathalmazt korpusznak nevezzük. A korpusz az összes szövegünket jelöli, ennek részegységei a dokumentumok. Ha például a Magyar Nemzet cikkeit kívánjuk elemezni, a kiválasztott időszak összes cikke lesz a teljes korpuszunk, az egyes cikkek pedig a dokumentumaink. Az elemzés mindig egy meghatározott tématerületre (domain-re) koncentrál. E tématerület utalhat a nyelvre, amelyen a szövegek íródtak, vagy a specifikus tartalomra, amelyet vizsgálunk, de mindenképpen meghatározza a szöveg szókészletével kapcsolatos várakozásainkat. Más lesz tehát a szóhasználat egy bulvárlap cikkeiben, mint egy tudományos szaklap cikkeiben, aminek elsősorban akkor van jelentősége, ha szótár alapú elemzéseket készítünk. A szótár alapú elemzések során olyan szószedeteket hozunk létre, amelyek segíthetnek a kutatásunk szempontjából érdekes témák vagy tartalmak azonosításában. Így például létrehozhatunk pozitív és negatív szótárakat, vagy a gazdasági és a külpolitikai témákhoz kapcsolódó szótárakat, melyek segíthetnek azonosítani, hogy adott dokumentum inkább gazdasági vagy inkább külpolitikai témákat tárgyal. Léteznek előre elkészített szótárak – angol nyelven például a Bing Liu által fejlesztett szótár egy jól ismert és széles körben alkalmazható példa (Liu 2010) –, azonban fontos fejben tartani, hogy a vizsgált téma specifikus nyelvezete jellemzően meghatározza azt, hogy egy-egy szótárba milyen kifejezéseknek kellene kerülniük. Már említettük, hogy egy szövegbányászati elemzés során a szöveg is adatként kezelendő. Tehát hasonló módon gondolhatunk az elemzendő szövegeinkre, mint egy statisztikai elemzésre szánt adatbázisra, annak csupán, reprezentációja tér el az utóbbitól. Tehát míg egy statisztikai elemzésre szánt táblázatban elsősorban számokat és adott esetben kategorikus változókat reprezentáló karakterláncokat (stringeket) – például „férfi”/„nő”, „falu”/„város” – találunk, addig a szöveges adatokban első ránézésre nem tűnik ki gépileg értelmezhető struktúra. Ahhoz, hogy a szövegeink a gépi elemzés számára feldolgozhatóvá váljanak, annak reprezentációját kell megváltoztatni, vagyis strukturálatlan adathalmazból strukturált adathalmazt kell létrehozni, melyet jellemzően a szövegek mátrixszá alakításával teszünk meg. A mátrixszá alakítás első hallásra bonyolult eljárás benyomását keltheti, azonban a gyakorlatban egy meglehetősen egyszerű transzformációról van szó, melynek eredményeként a szavakat számokkal reprezentáljuk. A könnyebb megértés érdekében vegyük az alábbi példát: tekintsük a három példamondatot a három elemzendő dokumentumnak, ezek összességét pedig a korpuszunknak. 1. Az Európai Unió 27 tagországának egyike Magyarország. 2. Magyarország 2004-ben csatlakozott az Európai Unióhoz. 3. Szlovákia, akárcsak Magyarország, 2004-ben lett ez Európai Unió tagja. A példamondatok dokumentum-kifejezés mátrixsza az alábbi táblázat szerint fog kinézni. Vegyük észre azt is, hogy több olyan kifejezés van, melyek csak ragozásukban térnek el: Unió, Unióhoz; tagja, tagjának. Ezeket a kifejezéseket a kutatói szándék függvényében azonos alakúra hozhatjuk, hogy egy egységként jelenjenek meg. Az elemzések többségében a szövegelőkészítés egyik kiinduló lépése a szótövesítés vagy a lemmatizálás, előbbi a szavak toldalékainak levágását jelöli, utóbbi a szavak szótári alakra való visszaalakítását. A ragozás eltávolítását illetően elöljáróban annyit érdemes megjegyezni, hogy az agglutináló, vagyis ragasztó nyelvek esetén, mint amilyen a magyar is, a toldalékok eltávolítása gyakran igen komoly kihívást jelent. Nem csak a toldalékok formája lehet igen sokféle, de az is előfordulhat, hogy a tőszó nem egyezik meg a toldalék levágásával keletkező szótővel. Ilyen például a vödröt kifejezés, melynek szótöve a „vödr”, de a nyelvtanilag helyes tőszó a „vödör”. Hasonlóan a majmok kifejezés esetén a szótő a „majm” lesz, míg a nyelvtanilag helyes tőszó a „majom”. Emiatt a toldalékok levágását a magyar nyelvű szövegek esetén megfelelő körültekintéssel kell végezni. Táblázat 2.1: Dokumentum-kifejezés mátrix a példamondatokból Dokumentum 27 2004-ben akárcsak az csatlakozott egyike Európai lett Magyarország Szlovákia tagja tagjának Unióhoz 1 1 0 0 1 0 1 1 0 1 0 0 1 0 2 0 1 0 1 1 0 1 0 1 0 0 0 1 3 0 1 1 1 0 0 1 1 1 1 1 0 0 A dokumentum-kifejezés mátrixban minden dokumentumot egy vektor (értsd: egy sor) reprezentál, az eltérő kifejezések pedig külön oszlopokat kapnak. Tehát a fenti példában minden dokumentumunk egy 14 elemű vektorként jelenik meg, amelynek elemei azt jelölik, hogy milyen gyakran szerepel az adott kifejezés a dokumentumban. A dokumentum-kifejezés mátrixok egy jellemző tulajdonsága, hogy igen nagy dimenziókkal rendelkezhetnek (értsd: sok sorral és sok oszloppal), hiszen minden kifejezést külön oszlopként reprezentálnak. Egy sok dokumentumból álló vagy egy témák tekintetében változatos korpusz esetében a kifejezés mátrix elemeinek jelentős része 0 lesz, hiszen számos olyan kifejezés fordul elő az egyes dokumentumokban, amelyek más dokumentumban nem szerepelnek. A sok nullát tartalmazó mátrixot hívjuk ritka mátrixnak. Az adatok jobb kezelhetősége érdekben a ritka mátrixot valamilyen dimenzióredukciós eljárással sűrű mátrixszá lehet alakítani (például a nagyon ritka kifejezések eltávolításával vagy valamilyen súlyozáson alapuló eljárással). 2.3 A szövegbányászat alapelvei A módszertani fogalmak tisztázásást követően néhány elméleti megfontolást osztanánk meg a Grimmer és Stewart (2013) által megfogalmazott alapelvek nyomán, melyek hasznos útravalóul szolgálhatnak a szövegbányászattal ismerkedő kutatók számára. 1. A szövegbányászat rossz, de hasznos Az emberi nyelv egy meglehetősen bonyolult rendszer, így egy szöveg jelentésének, érzelmi telítettségének különböző olvasók általi értelmezése meglehetősen eltérő lehet, így nem meglepő, hogy egy gép sok esetben csak korlátozott eredményeket képes felmutatni ezen feladatok teljesítésében. Ettől függetlenül nem elvitatható a szövegbányászati modellek hasznossága, hiszen olyan mennyiségű szöveg válik feldolgozhatóvá, ami gépi támogatás nélkül elképzelhetetlen lenne, mindemellett azonban nem lehet megfeledkezni a módszertan korlátairól sem. 2. A kvantitatív modellek kiegészítik az embert, nem helyettesítik azt A kvantitatív eszközökkel történő elemzés nem szünteti meg a szövegek elolvasásának szükségességét, hiszen egészen más információk kinyerését teszi lehetővé, mint egy kvalitatív megközelítés. Emiatt a kvantitatív szövegelemzés talán legfontosabb kihívása, hogy a kutató megtalálja a gépi és a humán erőforrások együttes hasznosításának legjobb módját. 3. Nincs legjobb modell Minden kutatáshoz meg kell találni a leginkább alkalmas modellt a kutatási kérdés, a rendelkezésre álló adatok és a kutatói szándék alapján. Gyakran különböző eljárások kombinálása vezethet egy specifikus probléma legjobb megoldásához. Azonban minden esetben az eredmények értékelésére kell támaszkodni, hogy megállapíthassuk egy modell teljesítményét adott problémára és szövegkorpuszra nézve. 4. Validálás, validálás, validálás! Mivel az automatizált szövegelemzés számos esetben jelentősen lecsökkenti az elemzéshez szükséges időt és energiát, csábító lehet a gondolat, hogy ezekhez a módszerekhez forduljon a kutató, ugyanakkor nem szabad elfelejteni, hogy az elemzés csupán a kezdeti lépés, hiszen a kutatónak validálnia kell az eredményeket ahhoz, hogy valóban megbízható következtetésekre jussunk. Az érvényesség-vizsgálat (validálás) lényege egy felügyelet nélküli modell esetén – ahol az elvárt eredmények nem ismertek, így a teljesítmény nem tesztelhető –, hogy meggyőződjünk: egy felügyelt modellel (olyan modellel, ahol az elvárt eredmény ismert, így ellenőrizhető) egyenértékű eredményeket hozzon. Ennek az elvárásnak a teljesítése gyakran nem egyszerű, azonban az eljárások alapos kiértékelést nélkülöző alkalmazása meglehetősen kétesélyes eredményekhez vezethet, emiatt érdemes megfelelő alapossággal eljárni az érvényesség-vizsgálat során. "],["adatkezeles.html", "3 Adatkezelés R-ben 3.1 Az adatok importálása 3.2 Az adatok exportálása 3.3 A pipe operátor 3.4 Műveletek adattáblákkal 3.5 Munka karakter vektorokkal6", " 3 Adatkezelés R-ben 3.1 Az adatok importálása Az adatok importálására az R alapfüggvénye mellett több csomag is megoldást kínál. Ezek közül a könyv írásakor a legnépszerűbbek a readr és a rio csomagok. A szövegek különböző karakterkódolásának problémáját tapasztalataink szerint a legjobban a readr csomag read_csv() függvénye kezeli, ezért legtöbbször ezt fogjuk használni a .csv állományok beolvasására. Amennyiben kihasználjuk az RStudio projekt opcióját (lásd a Függelékben) akkor elegendő csak az elérni kívánt adatok relatív elérési útját megadni (relative path). Ideális esetben az adataink egy csv fájlban vannak, ahol az egyes értékeket vesszők (vagy egyéb speciális karakterek) választják el. Ez esetben a read_delim() függvényt is használhatjuk. A beolvasásnál egyből el is tároljuk az adatokat egy objektumban. A sep = opcióval tudjuk a szeparátor karaktert beállítani, mert előfordulhat, hogy vessző helyett pontosvessző tagolja az adatainkat. library(readr) library(dplyr) library(gapminder) library(stringr) library(readtext) df <- readr::read_csv("data/adatfile.csv") Az R képes linkről letölteni fájlokat, elég megadnunk egy működő elérési útvonalat (lenti kódrészlet nem egy valódi linkre mutat, csupán egy példa). df_online <- read_csv("https://www.pelda_link.hu/adatok/pelda_file.csv") Az R csomag ökoszisztémája kellően változatos ahhoz, hogy gyakorlatilag bármilyen inputtal meg tudjon birkózni. Az Excel fájlokat a readxl csomagot használva tudjuk betölteni a read_excel() függvény használatával.lásd ehhez a Függeléket A leggyakoribb statisztikai programok formátumait pedig a haven csomag tudja kezelni (például Stata, Spss, SAS). A szintaxis itt is hasonló: read_stata(), read_spss(), read_sas(). A nagy mennyiségű szöveges dokumentum (a legyakrabban előforduló kiterjesztések: .txt, .doc, .pdf, .json, .csv, .xml, .rtf, .odt) betöltésére a legalkalmasabb a readtext csomag. Az alábbi példa azt mutatja be, hogyan tudjuk beolvasni egy adott mappából az összes .txt kiterjesztésű fájlt anélkül, hogy egyenként kellene megadnunk a fájlok neveit. A kódsorban szereplő * karakter ebben a környezetben azt jelenti, hogy bármilyen fájl az adott mappában, ami .txt-re végződik. Amennyiben a fájlok nevei tartalmaznak valamilyen metaadatot, akkor ezt is be tudjuk olvasni a betöltés során. Ilyen metaadat lehet például egy parlamenti felszólalásnál a felszólaló neve, a beszéd ideje, a felszólaló párttagsága (például: kovacsjanos_1994_fkgp.txt). df_text <- readtext::readtext( "data/*.txt", docvarsfrom = "filenames", dvsep = "_", docvarnames = c("nev", "ev", "part") ) 3.2 Az adatok exportálása Az adatainkat R-ből a write.csv()-vel exportálhatjuk a kívánt helyre, .csv formátumba. Az openxlsx csomaggal .xls és .xlsx Excel formátumokba is tudunk exportálni. Az R rendelkezik saját, .Rds és .Rda kiterjesztésű, tömörített fájlformátummal. Mivel ezeket csak az R-ben nyithatjuk meg, érdemes a köztes, hosszadalmas számítást igénylő lépések elmentésére használni, a saveRDS() és a save() parancsokkal. 3.3 A pipe operátor Az úgynevezett pipe operátor alapjaiban határozta meg a modern R fejlődését és a népszerű csomag ökoszisztéma, a tidyverse, egyik alapköve. Úgy gondoljuk, hogy a tidyverse és a pipe egyszerűbbé teszi az R használatának elsajátítását, ezért mi is erre helyezzük a hangsúlyt.3 Vizuálisan a pipe operátor így néz ki: %>% (a pipe operátor a Ctrl + Shift + M billentyúk kombinációjával könnyedén kiírható) és arra szolgál, hogy a kódban több egymáshoz kapcsolódó műveletet egybefűzzünk.4 Technikailag a pipe a bal oldali elemet adja meg a jobb oldali függvény első argumentumának. A lenti példa, amely nem tartalmaz igazi kódot, csupán a logikát kívánja szemlélteni ugyanazt a folyamatot írja le az alap R (base R), illetve a pipe használatával. 5 Miközben a kódot olvassuk, érdemes a pipe-ot „és aztán”-nak fordítani. reggeli(oltozkodes(felkeles(ebredes(en, idopont = "8:00"), oldal = "jobb"), nadrag = TRUE, ing = TRUE)) en %>% ebredes(idopont = "8:00") %>% felkeles(oldal = "jobb") %>% oltozkodes(nadrag = TRUE, ing = TRUE) %>% reggeli() A fenti példa is jól mutatja, hogy a pipe a bal oldali elemet fogja a jobb oldali függvény első elemének berakni. A pipe operátor működését még egy soron demonstrálhatjuk ez alkalommal valódi kódsorokkal és funkciókkal. Láthatjuk, hogy a pipe baloldalán elhelyezkedő kiindulópont egy számsor, ezt először a sum() funkcióba helyezzük, az így kapott eredmény 16 lesz, amelynek a gyökét vesszük az sqrt() az operátor alkalmazásával ezt a két műveletet egyetlen kódsorban könnyen olvasható módon végezzük el és megkapjuk a teljes folyamat eredeményét vagyis négyet. c(3, 5, 8) %>% sum() %>% sqrt() #> [1] 4 A fejezet további részeiben még bőven fogunk gyakorlati példát találni a pipe használatára. Mivel az itt bemutatott példák az alkalmazásoknak csak egy relatíve szűk körét mutatják be, érdemes átolvasni a csomagokhoz tartozó dokumentációt, illetve ha van, akkor tanulmányozni a működést demonstráló bemutató oldalakat is. 3.4 Műveletek adattáblákkal Az adattábla (data frame) az egyik leghasznosabb és leggyakrabban használt adattárolási mód az R-ben (a részletesebb leírás a Függelékben található). Ebben az alfejezetben azt mutatjuk be a dplyr és gapminder csomagok segítségével, hogyan lehet vele hatékonyan dolgozni. A dplyr az egyik legnépszerűbb R csomag, a tidyverse része. A gapminder csomag pedig a példa adatbázisunkat tartalmazza, amiben a világ országainak különböző gazdasági és társadalmi mutatói találhatók. A sorok (megfigyelések) szűréséhez a dplyr csomag filter() parancsát használva lehetőségünk van arra, hogy egy vagy több kritérium alapján szűkítsük az adatbázisunkat. A lenti példában azokat a megfigyeléseket tartjuk meg, ahol az év 1962 és a várható élettartam több mint 72 év. gapminder %>% dplyr::filter(year == 1962, lifeExp > 72) #> # A tibble: 5 × 6 #> country continent year lifeExp pop gdpPercap #> <fct> <fct> <int> <dbl> <int> <dbl> #> 1 Denmark Europe 1962 72.4 4646899 13583. #> 2 Iceland Europe 1962 73.7 182053 10350. #> 3 Netherlands Europe 1962 73.2 11805689 12791. #> 4 Norway Europe 1962 73.5 3638919 13450. #> 5 Sweden Europe 1962 73.4 7561588 12329. Ugyanígy leválogathatjuk az adattáblából az adatokat akkor is, ha egy karakter változó alapján szeretnénk szűrni. gapminder %>% filter(country == "Sweden", year > 1990) #> # A tibble: 4 × 6 #> country continent year lifeExp pop gdpPercap #> <fct> <fct> <int> <dbl> <int> <dbl> #> 1 Sweden Europe 1992 78.2 8718867 23880. #> 2 Sweden Europe 1997 79.4 8897619 25267. #> 3 Sweden Europe 2002 80.0 8954175 29342. #> 4 Sweden Europe 2007 80.9 9031088 33860. Itt tehát az adattábla azon sorait szeretnénk látni, ahol az ország megegyezik a „Sweden” karakterlánccal, az év pedig 1990 utáni. A select() függvény segítségével válogathatunk oszlopokat a data frame-ből. A változók kiválasztására több megoldás is van. A dplyr csomag tartalmaz apróbb kisegítő függvényeket, amik megkönnyítik a nagy adatbázisok esetén a változók kiválogatását a nevük alapján. Ezek a függvények a contains(), starts_with(), ends_with(), matches(), és beszédesen arra szolgálnak, hogy bizonyos nevű változókat ne kelljen egyenként felsorolni. A select()-en belüli változó sorrend egyben az eredmény data frame változójának sorrendjét is megadja. A negatív kiválasztás is lehetséges, ebben az esetben egy - jelet kell tennünk a nem kívánt változó(k) elé (pl.: select(df, year, country, -continent). gapminder %>% dplyr::select(dplyr::contains("ea"), dplyr::starts_with("co"), pop) #> # A tibble: 1,704 × 4 #> year country continent pop #> <int> <fct> <fct> <int> #> 1 1952 Afghanistan Asia 8425333 #> 2 1957 Afghanistan Asia 9240934 #> 3 1962 Afghanistan Asia 10267083 #> 4 1967 Afghanistan Asia 11537966 #> 5 1972 Afghanistan Asia 13079460 #> 6 1977 Afghanistan Asia 14880372 #> # ℹ 1,698 more rows Az így kiválogatott változókból létrehozhatunk és objektumként eltárolhatunk egy új adattáblát az objektumok részletesebb leírása a függelékben található Függelékben, amivel azután tovább dolgozhatunk, vagy kiírathatjuk például .csv fájlba, vagy elmenthetjük a saveRDS segítségével. gapminder_select <- gapminder %>% select(contains("ea"), starts_with("co"), pop) readr::write_csv(gapminder_select, "gapminder_select.csv") saveRDS(gapminder_select, "gapminder_select.Rds") A saveRDS segítségével elmentett fájlt később a readRDS() függvénnyel olvashatjuk be, majd onnan folytathatjuk a munkát, ahol korábban abbahagytuk. readRDS("gapminder_select.Rds") Az elemzési munkafolyamat elkerülhetetlen része, hogy új változókat hozzunk létre, vagy a meglévőket módosítsuk. Ezt a mutate()-el tehetjük meg, ahol a szintaxis a következő: mutate(data frame, uj valtozo = ertekek). Példaként kiszámoljuk a svéd GDP-t (milliárd dollárban) 1992-től kezdve. A mutate() alkalmazását részletesebben is bemutatjuk a szövegek előkészítésével foglalkozó fejezetben. gapminder %>% filter(country == "Sweden", year >= 1992) %>% dplyr::mutate(gdp = (gdpPercap * pop) / 10^9) #> # A tibble: 4 × 7 #> country continent year lifeExp pop gdpPercap gdp #> <fct> <fct> <int> <dbl> <int> <dbl> <dbl> #> 1 Sweden Europe 1992 78.2 8718867 23880. 208. #> 2 Sweden Europe 1997 79.4 8897619 25267. 225. #> 3 Sweden Europe 2002 80.0 8954175 29342. 263. #> 4 Sweden Europe 2007 80.9 9031088 33860. 306. Az adataink részletesebb és alaposabb megismerésében segítenek a különböző szintű leíró statisztikai adatok. A szintek megadására a group_by() használható, a csoportokon belüli számításokhoz pedig a summarize(). A lenti példa azt illusztrálja, hogy ha kontinensenként csoportosítjuk a gapminder adattáblát, akkor a summarise() használatával megkaphatjuk a megfigyelések számát, illetve az átlagos per capita GDP-t. A summarise() a mutate() közeli rokona, hasonló szintaxissal és logikával használható. Ezt a függvénypárost fogjuk majd használni a szöveges adataink leíró statisztikáinál is az 5. fejezetben. gapminder %>% dplyr::group_by(continent) %>% dplyr::summarise(megfigyelesek = n(), atlag_gdp = mean(gdpPercap)) #> # A tibble: 5 × 3 #> continent megfigyelesek atlag_gdp #> <fct> <int> <dbl> #> 1 Africa 624 2194. #> 2 Americas 300 7136. #> 3 Asia 396 7902. #> 4 Europe 360 14469. #> 5 Oceania 24 18622. 3.5 Munka karakter vektorokkal6 A szöveges adatokkal (karakter stringekkel) való munka elkerülhetetlen velejárója, a vektorokról, köztük a karakter vektorokról részletesebben a Függelékben írunk. A felesleges szövegelemeket, karaktereket el kell távolítanunk, hogy javuljon az elemzésünk hatásfoka. Erre a célra a stringr csomagot fogjuk használni, kombinálva a korábban bemutatott mutate()-el. A stringr függvények az str_ előtaggal kezdődnek és eléggé beszédes nevekkel rendelkeznek. Egy gyakran előforduló probléma, hogy extra szóközök maradnak a szövegben, vagy bizonyos szavakról, karakterkombinációkról tudjuk, hogy nem kellenek az elemzésünkhöz. Ebben az esetben egy vagy több reguláris kifejezés (regular expression, regex) használatával tudjuk pontosan kijelölni, hogy a karakter sornak melyik részét akarjuk módosítani.7 A legegyszerűbb formája a regexeknek, ha pontosan tudjuk milyen szöveget akarunk megtalálni. A kísérletezésre az str_view()-t használjuk, ami megjeleníti, hogy a megadott regex mintánk pontosan mit jelöl. A függvény match = TRUE paramétere lehetővé teszi, hogy csak a releváns találatokat kapjuk vissza. szoveg <- c("gitar", "ukulele", "nagybogo") stringr::str_view(szoveg, pattern = "ar") #> [1] │ git<ar> Reguláris kifejezésekkel rákereshetünk nem csak egyes elemekre (pl.: szavak, szótagok, betűk) hanem olyan konkrét esetekre is, amikor ezek egymás után fordulnak elő. Ilyenkor úgynevezett ngrammokat használunk, amelyek egy karakterláncban szereplő n-számú elem szekvenciája. A lenti példban ezeke működését egy úgynevezett bigrammal tehát egy n=2 értékű ngrammal mutatjuk be. Az ngrammok segítenek kezelni olyan eseteket, amikor két egymást követő elem eltérő jelentéssel bír, egymás mellett, mint külön-külön. szoveg <- c("a fehér ház fehérebb mint bármely más ház") stringr::str_view(szoveg, pattern = "fehér ház") #> [1] │ a <fehér ház> fehérebb mint bármely más ház Az ún. „horgonyokkal“, (anchor) azt lehet megadni, hogy a karakter string elején vagy végén szeretnénk-e egyezést találni. A string eleji anchor a ^, a string végi pedig a $. str_view("Dr. Doktor Dr.", pattern = "^Dr.") #> [1] │ <Dr.> Doktor Dr. str_view("Dr. Doktor Dr.", pattern = "Dr.$") #> [1] │ Dr. Doktor <Dr.> Továbbá azt is meghatározhatjuk, hogy egy adott karakter, vagy karakter kombinációt, valamint a mellett elhelyezkedő karaktereket is szeretnénk kijelölni. str_view("Dr. Doktor Dr.", pattern = ".k.") #> [1] │ Dr. D<okt>or Dr. Egy másik jellemző probléma, hogy olyan speciális karaktert akarunk leírni a regex kifejezésünkkel, ami amúgy a regex szintaxisban használt. Ilyen eset például a ., ami mint írásjel sokszor csak zaj, ám a regex kontextusban a „bármilyen karakter„ megfelelője. Ahhoz, hogy magát az írásjelet jelöljük, a \\\\ -t kell elé rakni, ennek egy alternatívája, hogy a keresett szóláncot a fixed() funkcióba helyezzük, így pedig a szóláncban lévő karakterek különleges jelentéseit figyelmen kívül hagyja és csak a magára az egzakt szóláncra keres rá. Ha rákeresünk a . karakterre, akkor látható, hogy a szólánc mindegyik karakterét kijelöltük, hiszen a . jelentése “bármilyen karakter”. str_view("Dr. Doktor Dr.", pattern = ".") #> [1] │ <D><r><.>< ><D><o><k><t><o><r>< ><D><r><.> A \\\\ jel segítségével már csak a szövegben lévő tényleges pontokra keresünk rá. str_view("Dr. Doktor Dr.", pattern = "\\\\.") #> [1] │ Dr<.> Doktor Dr<.> Ugyanúgy a fixed() funkció segítségével szintén csak a tényleges pontokra keresünk rá. str_view("Dr. Doktor Dr.", pattern = fixed(".")) #> [1] │ Dr<.> Doktor Dr<.> Néhány hasznos regex kifejezés: [:digit:] - számok (123) [:alpha:] - betűk (abc ABC) [:lower:] - kisbetűk (abc) [:upper:] - nagybetűk (ABC) [:alnum:] - betűk és számok (123 abc ABC) [:punct:] - központozás (.!?\\(){}) [:graph:] - betűk, számok és központozás (123 abc ABC .!?\\(){}) [:space:] - szóköz ( ) [:blank:] - szóköz és tabulálás [:cntrl:] - kontrol karakterek (\\n, \\r, stb.) * - bármi A tidyverse megközelítés miatt a kötetben szereplő R kód követi a “The tidyverse style guide” dokumentációt (https://style.tidyverse.org/)↩︎ Az RStudio-ban a pipe operátor billentyű kombinációja a Ctrl + Shift + M↩︎ Köszönjük Andrew Heissnek a kitűnő példát.↩︎ A könyv terjedelme miatt ezt a témát itt csak bemutatni tudjuk, de minden részletre kiterjedően nem tudunk elmélyülni benne. A témában nagyon jól használható online anyagok találhatóak az RStudio GitHub tárhelyén (https://github.com/rstudio/cheatsheets/raw/master/strings.pdf), illetve Wickham and Grolemund (2016) 14. fejezetében.↩︎ A reguláris kifejezés egy olyan, meghatározott szintaktikai szabályok szerint leírt karakterlánc (string), amivel meghatározható stringek egy adott halmaza. Az ilyen kifejezés valamilyen minta szerinti szöveg keresésére, cseréjére, illetve a szöveges adatok ellenőrzésére használható. További információ: http://www.regular-expressions.info/↩︎ "],["corpus_ch.html", "4 Korpuszépítés és szövegelőkészítés 4.1 Szövegbeszerzés 4.2 Szövegelőkészítés", " 4 Korpuszépítés és szövegelőkészítés 4.1 Szövegbeszerzés A szövegbányászati elemzések egyik első lépése az elemzés alapjául szolgáló korpusz megépítése. A korpuszt alkotó szövegek beszerzésének egyik módja a webscarping, melynek során weboldalakról történik az információ kinyerése. A scrapelést végezhetjük R-ben az rvest csomag segítségével. Fejezetünkben a scrapelésnek csupán néhány alaplépését mutatjuk meg.8 library(rvest) library(readr) library(dplyr) library(lubridate) library(stringr) library(quanteda) library(quanteda.textmodels) library(quanteda.textstats) library(HunMineR) A szükséges csomagok 9 beolvasása után a read_html() függvény segítségével az adott weboldal adatait kérjük le a szerverről. A read_html() függvény argumentuma az adott weblap URL-je. Ha például a poltextLAB projekt honlapjáról szeretnénk adatokat gyűjteni, azt az alábbi módon tehetjük meg: r <- rvest::read_html("https://poltextlab.tk.hu/hu") r #> {html_document} #> <html lang="hu" class="no-js"> #> [1] <head>\\n<meta http-equiv="Content-Type" content="text/html; charset ... #> [2] <body class="index">\\n\\n\\t<script>\\n\\t (function(i,s,o,g,r,a,m){i[ ... Ezután a html_nodes() függvény argumentumaként meg kell adnunk azt a HTML címkét vagy CSS azonosítót, ami a legyűjteni kívánt elemeket azonosítja a weboldalon. [^websraping] Ezeket az azonosítókat az adott weboldal forráskódjának megtekintésével tudhatjuk meg, amire a különböző böngészők különböző lehetőségeket kínálnak. Majd a html_text() függvény segítségével megkapjuk azokat a szövegeket, amelyek az adott weblapon az adott azonosítóval rendelkeznek. Példánkban a https://poltextlab.tk.hu/hu weboldalról azokat az információkat szeretnénk kigyűjteni, amelyek az <title> címke alatt szerepelnek. title <- read_html("https://poltextlab.tk.hu/hu") %>% rvest::html_nodes("title") %>% rvest::html_text() title #> [1] "MTA TK Political and Legal Text Mining and Artificial Intelligence Laboratory (poltextLAB)" Ezután a kigyűjtött információkat kiírhatjuk egy csv fájlba. write_csv(title, "title.csv") A webscraping során az egyik nehézség, ha a weboldal letiltja az automatikus letöltést, ezt kivédhetjük például különböző böngészőbővítmények segítségével, illetve a fejléc (header) vagy a hálózati kliens (user agent) megváltoztatásával. De segíthet véletlenszerű kiszolgáló (proxy) vagy VPN szolgáltatás10 használata is, valamint ha az egyes kérések között időt hagyunk. Mielőtt egy weboldal tartalmának scrapelését elkezdenénk, fontos tájékozódni a hatályos szerzői jogi szabályozásokról. A weboldalakon legtöbbször a legyűjtött szövegekhez tartozó különböző metaadatok is szerepelnek (például egy parlamenti beszéd dátuma, az azt elmondó képviselő neve), melyeket érdemes a scarpelés során szintén összegyűjteni. A scrapelés során fontos figyelnünk arra, hogy később jól használható formában mentsük el az adatokat, például .csv,.json vagy .txt kiterjesztésekkel. A karakterkódolási problémák elkerülése érdekében érdemes UTF-8 vagy UTF-16-os kódolást alkalmazni, mivel ezek tartalmazzák a magyar nyelv ékezetes karaktereit is.11 Arra is van lehetőség, hogy az elemezni kívánt korpuszt papíron keletkezett, majd szkennelt és szükség szerint optikai karakterfelismerés (Optical Character Recognition – OCR) segítségével feldolgozott szövegekből építsük fel. Mivel azonban ezeket a feladatokat nem R-ben végezzük, ezekről itt nem szólunk bővebben. Az így beszerzett és .txt vagy .csv fájllá alakított szövegekből való korpuszépítés a következő lépésekben megegyezik a weboldalakról gyűjtött szövegekével. 4.2 Szövegelőkészítés Az elemzéshez vezető következő lépés a szövegelőkészítés, amit a szöveg tisztításával kell kezdenünk. A szövegtisztításnál mindig járjunk el körültekintően és az egyes lépéseket a kutatási kérdésünknek megfelelően tervezzük meg, a folyamat során pedig időnként végezzünk ellenőrzést, ezzel elkerülhetjük a kutatásunkhoz szükséges információk elvesztését. Miután az elemezni kívánt szövegeinket beszereztük, majd a Az adatok importálása alfejezetben leírtak szerint importáltuk, következhetnek az alapvető előfeldolgozási lépések, ezek közé tartozik például a scrapelés során a korpuszba került html címkék, számok és egyéb zajok (például a speciális karakterek, írásjelek) eltávolítása, valamint a kisbetűsítés, a tokenizálás, a szótövezés és a tiltólistás szavak eltávolítása, azaz stopszavazás. A stringr csomag segítségével először eltávolíthatjuk a felesleges html címkéket a korpuszból.12 Ehhez először létrehozzuk a text1 nevű objektumot, ami egy karaktervektorból áll. text1 <- c("MTA TK", "<font size='6'> Political and Legal Text Mining and Artificial Intelligence Laboratory (poltextLAB)") text1 #> [1] "MTA TK" #> [2] "<font size='6'> Political and Legal Text Mining and Artificial Intelligence Laboratory (poltextLAB)" Majd a str_replace_all()függvény segítségével eltávolítjuk két html címke közötti szövegrészt. Ehhez a függvény argumentumában létrehozunk egy regex kifejezést, aminek segítségével a függvény minden < > közötti szövegrészt üres karakterekre cserél. Ezután a str_to_lower()mindent kisbetűvé konvertál, majd a str_trim() eltávolítja a szóközöket a karakterláncok elejéről és végéről. text1 %>% stringr::str_replace_all(pattern = "<.*?>", replacement = "") %>% stringr::str_to_lower() %>% stringr::str_trim() #> [1] "mta tk" #> [2] "political and legal text mining and artificial intelligence laboratory (poltextlab)" 4.2.1 Tokenizálás, szótövezés, kisbetűsítés és a tiltólistás szavak eltávolítása Az előkészítés következő lépésében tokenizáljuk, azaz egységeire bontjuk az elemezni kívánt szöveget, így a tokenek az egyes szavakat vagy kifejezéseket fogják jelölni. Ennek eredményeként kapjuk meg az n-gramokat, amik a vizsgált egységek (számok, betűk, szavak, kifejezések) n-elemű sorozatát alkotják. A következőkben a „Példa az előkészítésre” mondatot bontjuk először tokenekre a tokens() függvénnyel, majd a tokeneket a tokens_tolower() segítségével kisbetűsítjük, a tokens_wordstem() függvénnyel pedig szótövezzük. Végezetül a quanteda csomagban található magyar nyelvű stopszótár segítségével, elvégezzük a tiltólistás szavak eltávolítását. Ehhez először létrehozzuk az sw elnevezésű karaktervektort a magyar stopszavakból. A head() függvény segítségével belenézhetünk a szótárba, és a console-ra kiírathatjuk a szótár első hat szavát. Végül a tokens_remove()segítségével eltávolítjuk a stopszavakat. text <- "Példa az elokészítésre" toks <- quanteda::tokens(text) toks <- quanteda::tokens_tolower(toks) toks <- quanteda::tokens_wordstem(toks) toks #> Tokens consisting of 1 document. #> text1 : #> [1] "példa" "az" "elokészítésr" sw <- quanteda::stopwords("hungarian") head(sw) #> [1] "a" "ahogy" "ahol" "aki" "akik" "akkor" quanteda::tokens_remove(toks, sw) #> Tokens consisting of 1 document. #> text1 : #> [1] "példa" "elokészítésr" Ezt követi a szótövezés (stemmelés) lépése, melynek során az alkalmazott szótövező algoritmus egyszerűen levágja a szavak összes toldalékát, a képzőket, a jelzőket és a ragokat. Szótövezés helyett alkalmazhatunk szótári alakra hozást is (lemmatizálás). A két eljárás közötti különbség abban rejlik, hogy a szótövezés során csupán eltávolítjuk a szavak toldalékként azonosított végződéseit, hogy ugyanannak a szónak különböző megjelenési formáit közös törzsre redukáljuk, míg a lemmatizálás esetében rögtön az értelmes, szótári formát kapjuk vissza. A két módszer közötti választás a kutatási kérdés alapján meghozott kutatói döntésen alapul (Grimmer and Stewart 2013). Az alábbi példában egyetlen szó különböző alakjainak szótári alakra hozásával szemléltetjük a lemmatizálás működését. Ehhez először a text1 nevű objektumban tároljuk a szótári alakra hozni kívánt szöveget, majd tokenizáljuk és eltávolítjuk a központozást. Ezután definiáljuk a megfelelő szótövet és azt, hogy mely szavak alakjait szeretnénk erre a szótőre egységesíteni, majd a rep() függvény segítségével a korábban zárójelben megadott kifejeztéseket az “elokeszites” lemmával helyettesítjük, azaz a korábban definiált szólakokat az általunk megadott szótári alakkal helyettesítjük. Hosszabb szövegek lemmatizálásához előre létrehozott szótárakat használhatunk, ilyen például a WordNet, ami magyar nyelven is elérhető.13 text1 <- "Példa az előkészítésre. Az előkészítést a szövetisztítással kell megkezdenünk. Az előkészített korpuszon elemzést végzünk" toks1 <- tokens(text1, remove_punct = TRUE) elokeszites <- c("előkészítésre", "előkészítést", "előkészített") lemma <- rep("előkészítés", length(elokeszites)) toks1 <- quanteda::tokens_replace(toks1, elokeszites, lemma, valuetype = "fixed") toks1 #> Tokens consisting of 1 document. #> text1 : #> [1] "Példa" "az" "előkészítés" #> [4] "Az" "előkészítés" "a" #> [7] "szövetisztítással" "kell" "megkezdenünk" #> [10] "Az" "előkészítés" "korpuszon" #> [ ... and 2 more ] A fenti text1 objektumban tárolt szöveg szótövezését az alábbiak szerint tudjuk elvégezni. Megvizsgálva az előkészítés különböző alakjainak lemmatizált és stemmelt változatát jól láthatjuk a két módszer közötti különbséget. text1 <- "Példa az előkészítésre. Az előkészítést a szövetisztítással kell megkezdenünk. Az előkészített korpuszon elemzést végzünk" toks2 <- tokens(text1, remove_punct = TRUE) toks2 <- tokens_wordstem(toks2) toks2 #> Tokens consisting of 1 document. #> text1 : #> [1] "Példa" "az" "előkészítésr" #> [4] "Az" "előkészítést" "a" #> [7] "szövetisztításs" "kell" "megkezdenünk" #> [10] "Az" "előkészített" "korpuszon" #> [ ... and 2 more ] 4.2.2 Dokumentum kifejezés mátrix (dtm, dfm) A szövegbányászati elemzések nagy részéhez szükségünk van arra, hogy a szövegeinkből dokumentum kifejezés mátrix-ot (Document Term Matrix – dtm vagy Document Feature Matrix – dfm) hozzunk létre.14 Ezzel a lépéssel alakítjuk a szövegeinket számokká, ami lehetővé teszi, hogy utána különböző statisztikai műveleteket végezzünk velük. A dokumentum kifejezés mátrix minden sora egy dokumentum, minden oszlopa egy kifejezés, az oszlopokban szereplő változók pedig megmutatják az egyes kifejezések számát az egyes dokumentumokban. A legtöbb dokumentum kifejezés mátrix ritka mátrix, mivel a legtöbb dokumentum és kifejezés párosítása nem történik meg: a kifejezések nagy része csak néhány dokumentumban szerepel, ezek értéke nulla lesz. Az alábbi példában három, egy-egy mondatos dokumentumon szemléltetjük a fentieket. A korábban megismert módon előkészítjük, azaz kisbetűsítjük, szótövezzük a dokumentumokat, eltávolítjuk a tiltólistás szavakat, majd létrehozzuk belőlük a dokumentum kifejezés mátrixot.15 text <- c( d1 = "Ez egy példa az elofeldolgozásra", d2 = "Egy másik lehetséges példa", d3 = "Ez pedig egy harmadik példa" ) dfm <- text %>% tokens %>% tokens_remove(pattern = stopwords("hungarian")) %>% tokens_tolower() %>% tokens_wordstem(language = "hungarian") %>% dfm() dfm #> Document-feature matrix of: 3 documents, 4 features (50.00% sparse) and 0 docvars. #> features #> docs péld elofeldolgozás lehetséges harmad #> d1 1 1 0 0 #> d2 1 0 1 0 #> d3 1 0 0 1 4.2.3 Súlyozás A dokumentum kifejezés mátrix lehet egy egyszerű bináris mátrix, ami csak azt az információt tartalmazza, hogy egy adott szó előfordul-e egy adott dokumentumban. Míg az egyszerű bináris mátrixban ugyanakkora súlya van egy szónak ha egyszer és ha tízszer szerepel, készíthetünk olyan mátrixot is, ahol egy szónak annál nagyobb a súlya egy dokumentumban, minél többször fordul elő. A szógyakoriság (term frequency – TF) szerint súlyozott mátrixnál azt is figyelembe vesszük, hogy az adott szó hány dokumentumban szerepel. Minél több dokumentumban szerepel egy szó, annál kisebb a jelentősége. Ilyen szavak például a névelők, amelyek sok dokumentumban előfordulnak ugyan, de nem sok tartalmi jelentőséggel bírnak. Két szó közül általában az a fontosabb, amelyik koncentráltan, kevés dokumentumban, de azokon belül nagy gyakorisággal fordul elő. A dokumentum gyakorisági érték (document frequency – DF) egy szó gyakoriságát jellemzi egy korpuszon belül. A súlyozási sémákban általában a dokumentum gyakorisági érték inverzével számolnak (inverse document frequency - IDF), ez a leggyakrabban használt TF-IDF súlyozás (term frequency & inverse document frequency - TF-IDF). Az így súlyozott TF mátrix egy-egy cellájában található érték azt mutatja, hogy egy adott szónak mekkora a jelentősége egy adott dokumentumban. A TF-IDF súlyozás értéke tehát magas azon szavak esetén, amelyek az adott dokumentumban gyakran fordulnak elő, míg a teljes korpuszban ritkán; alacsonyabb azon szavak esetén, amelyek az adott dokumentumban ritkábban, vagy a korpuszban gyakrabban fordulnak elő; és kicsi azon szavaknál, amelyek a korpusz lényegében összes dokumentumában előfordulnak (Tikk 2007, 33–37 o.) Az alábbiakban az 1999-es törvényszövegeken szemléltetjük, hogy egy 125 dokumentumból létrehozott mátrix segítségével milyen alapvető statisztikai műveleteket végezhetünk.16 A HunMineR csomagból tudjuk importálni a törvényeket. lawtext_df <- HunMineR::data_lawtext_1999 Majd az importált adatokból létrehozzuk a korpuszt lawtext_corpus néven. Ezt követi a dokumentum kifejezés mátrix kialakítása (mivel a quanteda csomaggal dolgozunk, dfm mátrixot hozunk létre), és ezzel egy lépésben elvégezzük az alapvető szövegtisztító lépéseket is. lawtext_corpus <- quanteda::corpus(lawtext_df) lawtext_dfm <- lawtext_corpus %>% tokens( remove_punct = TRUE, remove_symbols = TRUE, remove_numbers = TRUE ) %>% tokens_tolower() %>% tokens_remove(pattern = stopwords("hungarian")) %>% tokens_wordstem(language = "hungarian") %>% dfm() A topfeatures() függvény segítségével megnézhetjük a mátrix leggyakoribb szavait, a függvény argumentumában megadva a dokumentum kifejezés mátrix nevét és a kívánt kifejezésszámot. quanteda::topfeatures(lawtext_dfm, 15) #> the bekezdés of áll szerződő rendelkezés #> 7942 5877 5666 4267 3620 3403 #> törvény to hely an személy év #> 3312 3290 3114 3068 3048 2975 #> kiadás b ha #> 2884 2831 2794 Mivel látható, hogy a szövegekben sok angol kifejezés is volt egy következő lépcsőben az angol stopszavakat is eltávolítjuk. lawtext_dfm_2 <- quanteda::dfm_remove(lawtext_dfm, pattern = stopwords("english")) Ezután megnézzük a leggyakoribb 15 kifejezést. topfeatures(lawtext_dfm_2, 15) #> bekezdés áll szerződő rendelkezés törvény #> 5877 4267 3620 3403 3312 #> hely személy év kiadás b #> 3114 3048 2975 2884 2831 #> ha következő költségvetés működés eset #> 2794 2398 2376 2325 2070 A következő lépés, hogy TF-IDF súlyozású statisztikát készítünk, a dokumentum kifejezés mátrix alapján. Ehhez először létrehozzuk a lawtext_tfidf nevű objektumot, majd a textstat_frequency() függvény segítségével kilistázzuk annak első 10 elemét. lawtext_tfidf <- quanteda::dfm_tfidf(lawtext_dfm_2) quanteda.textstats::textstat_frequency(lawtext_tfidf, force = TRUE, n = 10) #> feature frequency rank docfreq group #> 1 felhalmozás 1452 1 7 all #> 2 szerződő 1349 2 53 all #> 3 shall 1291 3 14 all #> 4 kiadás 1280 4 45 all #> 5 költségvetés 1101 5 43 all #> 6 támogatás 1035 6 38 all #> 7 beruházás 942 7 22 all #> 8 contracting 894 8 7 all #> 9 articl 884 9 14 all #> 10 működés 741 10 60 all A folyamatról bővebb információ található például az alábbi oldalakon: https://cran.r-project.org/web/packages/rvest/rvest.pdf, https://rvest.tidyverse.org.↩︎ A quanteda csomagnak több kiegészítő csomagja van, amelyeknek a tartalmára a nevük utal. A könyvben a quanteda.textmodels, quanteda.textplots, quanteda.textstats csomagokat használjuk még, amelyek statisztikai és vizualizaciós függvényeket tartalmaznak.↩︎ A VPN (Virtual Private Network, azaz a virtuális magánhálózat) azt teszi lehetővé, hogy a felhasználók egy megosztott vagy nyilvános hálózaton keresztül úgy küldjenek és fogadjanak adatokat, mintha számítógépeik közvetlenül kapcsolódnának a helyi hálózathoz.↩︎ A karakterkódolással kapcsolatosan további hasznos információk találhatóak az alábbi oldalon: http://www.cs.bme.hu/~egmont/utf8.↩︎ A stringr csomag jól használható eszköztárat kínál a különböző karakterláncokkezeléséhez. Részletesebb leírása megtalálható: https://stringr.tidyverse.org/. A karakterláncokról bővebben: https://r4ds.had.co.nz/strings.html↩︎ WordNet: https://github.com/mmihaltz/huwn. A magyar nyelvű szövegek lemmatizálását elvégezhetjük a szövegek R-be való beolvasása előtt is a magyarlanc nyelvi elemző segítségével, melyről a Természetes-nyelv feldolgozás (NLP) és névelemfelismerés című fejezetben szólunk részletesebben.↩︎ A két mátrix csak a nevében különbözik, tartalmilag nem. A használni kívánt csomag leírásában mindig megtalálható, hogy dtm vagy dfm mátrix segítségével dolgozik-e. Mivel a könyvben általunk használt quanteda csomag dfm mátrixot használ, mi is ezt használjuk.↩︎ Bár a lenti mátrixok, amelyekben csak 0 és 1 szerepel, a bináris mátrix látszatát keltik, de valójában nem azok, ha egy-egy kifejezésből több is szerepelne a mondatban, a mátrixban 2, 3, 4 stb. szám lenne.↩︎ Az itt használt kódok az alábbiakon alapulnak: https://rdrr.io/cran/quanteda/man/dfm_weight.html, https://rdrr.io/cran/quanteda/man/dfm_tfidf.html. A példaként használt korpusz a Hungarian Comparative Agendas Project keretében készült adatbázis része: https://cap.tk.hu/torveny↩︎ "],["leiro_stat.html", "5 Leíró statisztika 5.1 Szövegek a vektortérben 5.2 Leíró statisztika 5.3 A szövegek lexikai diverzitása 5.4 Összehasonlítás20 5.5 A kulcsszavak kontextusa", " 5 Leíró statisztika 5.1 Szövegek a vektortérben A szövegbányászati feladatok két altípusa a keresés és a kinyerés. A keresés során olyan szövegeket keresünk, amelyekben egy adott kifejezés előfordul. A webes keresőprogramok egyik jellemző tevékenysége, az információ-visszakeresés (information retrieval) során például az a cél, hogy a korpuszból visszakeressük a kereső információigénye szempontjából releváns információkat, mely keresés alapulhat metaadatokon vagy teljes szöveges indexelésen (Russel and Norvig 2005, 742.o; Tikk 2007). Az információkinyerés (information extraction) esetén a cél, hogy a strukturálatlan szövegekből strukturált adatokat állítsunk elő. Azaz az információkinyerés során nem a felhasználó által keresett információt keressük meg és lokalizáljuk, hanem az adott kérdés szempontjából releváns információkat gyűjtjük ki a dokumentumokból. Az információkinyerés alternatív megoldása segítségével már képesek lehetünk a kifejezések közötti kapcsolatok elemzésére, tendenciák és minták felismerésére és az információk összekapcsolása révén új információk létrehozására, azaz a segítségével strukturálatlan szövegekből is előállíthatunk strukturált információkat (Kwartler 2017; Schütze, Manning, and Raghavan 2008; Tikk 2007, 63–81.o). A szövegbányászati vizsgálatok során folyó szövegek, azaz strukturálatlan vagy részben strukturált dokumentumok elemzésére kerül sor. Ezekből a kutatási kérdéseink szempontjából releváns, látens összefüggéseket nyerünk ki, amelyek már strukturált szerkezetűek. A dokumentumok reprezentálásának három legelterjedtebb módja a halmazelmélet alapú, az algebrai és a valószínűségi modell. A halmazelméleti modellek a dokumentumok hasonlóságát halmazelmélet, a valószínűségi modellek pedig feltételes valószínűségi becslés alapján határozzák meg. Az algebrai modellek a dokumentumokat vektorként vagy mátrixként ábrázolják és algebrai műveletek segítségével hasonlítják össze. A vektortérmodell sokdimenziós vektortérben ábrázolja a dokumentumokat, úgy, hogy a dokumentumokat vektorokkal reprezentálja, a vektortér dimenziói pedig a dokumentumok összességében előforduló egyedi szavak. A modell alkalmazása során azok a dokumentumok hasonlítanak egymásra, amelyeknek a szókészlete átfedi egymást, és a hasonlóság mértéke az átfedéssel arányos. A vektortérmodellben a dokumentumgyűjteményt a dokumentum-kifejezés mátrixszal reprezentáljuk, a mátrixban a sorok száma megegyezik a dokumentumok számával, az oszlopokat pedig a korpusz egyedi kifejezési alkotják. Az egyedi szavak összességét szótárnak nevezzük. Mivel mátrixban az egyedi szavak száma általában igen nagy, ezért a mátrix hatékony kezeléséhez annak mérete különböző eljárásokkal csökkenthető. Fontos tudni, hogy a dokumentumok vektortér reprezentációjában a szavak szövegen belüli sorrendjére és pozíciójára vonatkozó információ nem található meg (Russel and Norvig 2005, 742–44 o.; Kwartler 2017; Welbers, Van Atteveldt, and Benoit 2017). A vektortérmodellt szózsák (bag of words) modellnek is nevezzük, melynek segítségével a fent leírtak szerint az egyes szavak gyakoriságát vizsgálhatjuk meg egy adott korpuszon belül. 5.2 Leíró statisztika Fejezetünkben nyolc véletlenszerűen kiválasztott magyar miniszterelnöki beszéd vizsgálatát végezzük el,17 amihez az alábbi csomagokat használjuk: library(HunMineR) library(readtext) library(dplyr) library(lubridate) library(stringr) library(ggplot2) library(quanteda) library(quanteda.textstats) library(quanteda.textplots) library(GGally) library(ggdendro) library(tidytext) library(plotly) Első lépésben a Bevezetőben már ismertetett módon a HunMineR csomagból betöltjük a beszédeket. A glimpse() függvénnyel egy gyors pilltast vethetünk a betöltött adatokra. texts <- HunMineR::data_miniszterelnokok_raw dplyr::glimpse(texts) #> Rows: 7 #> Columns: 4 #> $ doc_id <chr> "antall_jozsef_1990", "bajnai_gordon_2009", "gyurcsány_fe… #> $ text <chr> "Elnök Úr! Tisztelt Országgyulés! Hölgyeim és Uraim! Honf… #> $ year <dbl> 1990, 2009, 2005, 1994, 2002, 1995, 2018 #> $ pm <chr> "antall_jozsef", "bajnai_gordon", "gyurcsány_ferenc", "ho… A glimpse funkció segítségével nem csak a sorok és oszlopok számát tekinthetjük meg, hanem az egyes oszlopok neveit is, amelyek alapján megállapíthatjuk, hogy milyen információkat tartalmaz ez az objektum. Az egyes beszédek dokumentum azonosítóját, azok szövegét, az évüket és végül a miniszterelnök nevét, aki elmondta az adott beszédet. Ezt követően az Adatkezelés R-ben című fejezetben ismertetett mutate() függvény használatával két csoportra osztjuk a beszédeket. Ehhez a pm nevű változót alkalmazzuk, amely az egyes miniszterelnökök neveit tartalmazza. Kialakítjuk a két csoportot, azaz az if_else() segítségével meghatározzuk, hogy ha „antall_jozsef”, „boross_peter”, „orban_viktor” beszédeiről van szó azokat a jobb csoportba tegye, a maradékot pedig a bal csoportba. Ezután a glimpse() függvény segítségével megtekintjük, hogy milyen változtatásokat végeztünk az adattáblánkon. Láthatjuk, hogy míg korábban 7 dokumentumunk és 4 változónk volt, az átalakítás eredményeként a 7 dokumentum mellett már 5 változót találunk. Ezzel a lépéssel tehát kialakítottuk azokat a változókat, amelyekre az elemzés során szükségünk lesz. jobboldali_miniszterelnokok <- c("antall_jozsef", "boross_peter", "orban_viktor") texts <- texts %>% mutate( partoldal = dplyr::if_else(pm %in% jobboldali_miniszterelnokok, "jobb", "bal") ) glimpse(texts) #> Rows: 7 #> Columns: 5 #> $ doc_id <chr> "antall_jozsef_1990", "bajnai_gordon_2009", "gyurcsány… #> $ text <chr> "Elnök Úr! Tisztelt Országgyulés! Hölgyeim és Uraim! H… #> $ year <dbl> 1990, 2009, 2005, 1994, 2002, 1995, 2018 #> $ pm <chr> "antall_jozsef", "bajnai_gordon", "gyurcsány_ferenc", … #> $ partoldal <chr> "jobb", "bal", "bal", "bal", "bal", "jobb", "jobb" Ezt követően a további lépések elvégzéséhez létrehozzuk a quanteda korpuszt, majd a summary() függvény segítségével megtekinthetjük a korpusz alapvető statisztikai jellemzőit. Láthatjuk például, hogy az egyes dokumentumok hány tokenből vagy mondatból állnak. corpus_mineln <- corpus(texts) summary(corpus_mineln) #> Corpus consisting of 7 documents, showing 7 documents: #> #> Text Types Tokens Sentences year pm #> antall_jozsef_1990 3745 9408 431 1990 antall_jozsef #> bajnai_gordon_2009 1391 3277 201 2009 bajnai_gordon #> gyurcsány_ferenc_2005 2963 10267 454 2005 gyurcsány_ferenc #> horn_gyula_1994 1704 4372 226 1994 horn_gyula #> medgyessy_peter_2002 1021 2362 82 2002 medgyessy_peter #> orban_viktor_1998 1810 4287 212 1995 orban_viktor #> orban_viktor_2018 933 1976 126 2018 orban_viktor #> partoldal #> jobb #> bal #> bal #> bal #> bal #> jobb #> jobb Mivel az elemzés során a korpuszon belül két csoportra osztva szeretnénk összehasonlításokat tenni, az alábbiakban két alkorpuszt alakítunk ki. mineln_jobb <- quanteda::corpus_subset(corpus_mineln, pm %in% c("antall_jozsef", "boross_peter", "orban_viktor")) mineln_bal <- quanteda::corpus_subset(corpus_mineln, pm %in% c("horn_gyula", "gyurcsany_ferenc", "medgyessy_peter", "bajnai_gordon")) summary(mineln_jobb) #> Corpus consisting of 3 documents, showing 3 documents: #> #> Text Types Tokens Sentences year pm partoldal #> antall_jozsef_1990 3745 9408 431 1990 antall_jozsef jobb #> orban_viktor_1998 1810 4287 212 1995 orban_viktor jobb #> orban_viktor_2018 933 1976 126 2018 orban_viktor jobb summary(mineln_bal) #> Corpus consisting of 3 documents, showing 3 documents: #> #> Text Types Tokens Sentences year pm #> bajnai_gordon_2009 1391 3277 201 2009 bajnai_gordon #> horn_gyula_1994 1704 4372 226 1994 horn_gyula #> medgyessy_peter_2002 1021 2362 82 2002 medgyessy_peter #> partoldal #> bal #> bal #> bal A korábban létrehozott „jobb” és „bal” változó segítségével nem csak az egyes dokumentumokat, hanem a két csoportba sorolt beszédeket is összehasonlíthatjuk egymással. summary(corpus_mineln) %>% group_by(partoldal) %>% summarise( mean_wordcount = mean(Tokens), std_dev = sd(Tokens), min_wordc = min(Tokens), max_wordc = max(Tokens) ) #> # A tibble: 2 × 5 #> partoldal mean_wordcount std_dev min_wordc max_wordc #> <chr> <dbl> <dbl> <int> <int> #> 1 bal 5070. 3561. 2362 10267 #> 2 jobb 4710. 3271. 1976 9408 A textstat_collocations() függvény segítségével szókapcsolatokat kereshetünk. A függvény argumentumai közül a size a szókapcsolatok hossza, a min_count pedig a minimális előfordulásuk száma. Miután a szókapcsolatokat megkerestük, közülük a korábban már megismert head() függvény segítségével tetszőleges számút megnézhetünk.18 corpus_mineln %>% quanteda.textstats::textstat_collocations( size = 3, min_count = 6 ) %>% head(n = 10) #> collocation count count_nested length lambda z #> 1 a kormány a 21 0 3 1.510 2.996 #> 2 az új kormány 10 0 3 4.092 2.571 #> 3 az a politika 6 0 3 3.849 2.418 #> 4 a száz lépés 9 0 3 3.245 1.438 #> 5 a magyar gazdaság 12 0 3 2.199 1.374 #> 6 tisztelt hölgyeim és 31 0 3 3.290 1.266 #> 7 ez a program 8 0 3 1.683 1.107 #> 8 hogy ez a 10 0 3 0.530 1.024 #> 9 hogy a magyar 18 0 3 0.756 0.855 #> 10 az ellenzéki pártok 6 0 3 1.434 0.784 A szókapcsolatok listázásánál is láthattuk, hogy a korpuszunk még minden szót tartalmaz, ezért találtunk például „hogy ez a” összetételt. A következőkben eltávolítjuk az ilyen funkció nélküli stopszavakat a korpuszból, amihez saját stopszólistát használunk. Először a HunMineR csomagból beolvassuk és egy custom_stopwords nevű objektumban tároljuk a stopszavakat, majd a tokens() függvény segítségével tokenizáljuk a korpuszt és a tokens_select() használatával eltávolítjuk a stopszavakat. Ha ezután újra megnézzük a kollokációkat, jól látható a stopszavak eltávolításának eredménye: custom_stopwords <- HunMineR::data_stopwords_extra corpus_mineln %>% tokens() %>% tokens_select(pattern = custom_stopwords, selection = "remove") %>% textstat_collocations( size = 3, min_count = 6 ) %>% head(n = 10) #> collocation count count_nested length lambda z #> 1 taps MSZP soraiból 7 0 3 -1.72 -0.932 #> 2 taps kormánypártok soraiban 13 0 3 -1.75 -1.025 #> 3 tisztelt hölgyeim uraim 31 0 3 -3.14 -1.062 #> 4 közbeszólás fidesz soraiból 12 0 3 -4.24 -1.891 #> 5 taps MSZP soraiban 9 0 3 -4.58 -2.702 A korpusz további elemzése előtt fontos, hogy ne csak a stopszavakat távolítsuk el, hanem az egyéb alapvető szövegtisztító lépéseket is elvégezzük. Azaz a tokens_select() segítségével eltávolítsuk a számokat, a központozást, az elválasztó karaktereket, mint például a szóközöket, tabulátorokat, sortöréseket. Ezután a tokens_ngrams() segítségével n-gramokat (n elemű karakterláncokat) hozunk létre a tokenekből, majd kialakítjuk a dokumentum kifejezés mátrixot (dfm) és elvégezzük a tf-idf szerinti súlyozást. A dfm_tfidf() függvény kiszámolja a dokumentum gyakoriság inverz súlyozását, azok a szavak, amelyek egy dokumentumban gyakran jelennek meg nagyobb súly kapnak, de ha a korpusz egészében jelennek meg gyakran, tehát több dokumentumban is jelen van nagy számban, akkor egy kissebb súlyt kap. A függvény alapértelmezés szerint a normalizált kifejezések gyakoriságát használja a dokumentumon belüli relatív kifejezés gyakoriság helyett, ezt írjuk felül a schem_tf = \"prop\" használatával. Végül a textstat_frequency() segítségével gyakorisági statisztikát készíthetünk a korábban meghatározott (példánkban két és három tagú) n-gramokról. corpus_mineln %>% tokens( remove_numbers = TRUE, remove_punct = TRUE, remove_separators = TRUE ) %>% tokens_select(pattern = custom_stopwords, selection = "remove") %>% quanteda::tokens_ngrams(n = 2:3) %>% dfm() %>% dfm_tfidf(scheme_tf = "prop") %>% quanteda.textstats::textstat_frequency(n = 10, force = TRUE) #> feature frequency rank docfreq group #> 1 fordítsanak_hátat 0.00228 1 1 all #> 2 tisztelt_hölgyeim 0.00225 2 4 all #> 3 tisztelt_hölgyeim_uraim 0.00225 2 4 all #> 4 fidesz_soraiból 0.00201 4 1 all #> 5 taps_mszp 0.00159 5 2 all #> 6 magyarország_európa 0.00136 6 1 all #> 7 tisztelt_képviselőtársaim 0.00130 7 2 all #> 8 kormánypártok_soraiban 0.00129 8 2 all #> 9 taps_kormánypártok 0.00117 9 2 all #> 10 taps_kormánypártok_soraiban 0.00117 9 2 all 5.3 A szövegek lexikai diverzitása Az alábbiakban a korpuszunkat alkotó szövegek lexikai diverzitását vizsgáljuk. Ehhez a quanteda csomag textstat_lexdiv() függvényét használjuk. Először a corpus_mineln nevű korpuszunkból létrehozzuk a mineln_dfm nevű dokumentum-kifejezés mátrixot, amelyen elvégezzük a korábban már megismert alapvető tisztító lépéseket. A textstat_lexdiv() függvény eredménye szintén egy dfm, így azt arrange() parancs argumentumában a desc megadásával csökkenő sorba is rendezhetjük. Atextstat_lexdiv() függvény segítségével 13 különböző mérőszámot alkalmazhatunk, amelyek mind az egyes szövegek lexikai különbözőségét írják le.19 mineln_dfm <- corpus_mineln %>% tokens( remove_punct = TRUE, remove_separators = TRUE, split_hyphens = TRUE ) %>% dfm() %>% quanteda::dfm_remove(pattern = custom_stopwords) mineln_dfm %>% quanteda.textstats::textstat_lexdiv(measure = "CTTR") %>% dplyr::arrange(dplyr::desc(CTTR)) #> document CTTR #> 1 antall_jozsef_1990 33.0 #> 2 gyurcsány_ferenc_2005 26.1 #> 3 orban_viktor_1998 23.4 #> 4 horn_gyula_1994 22.3 #> 5 bajnai_gordon_2009 19.9 #> 6 medgyessy_peter_2002 16.8 #> 7 orban_viktor_2018 16.2 A megkapott értékeket hozzáadhatjuk a dfm-hez is. A lenti kód egy dfm_lexdiv nevű adattáblát hoz létre, amely tartalmazza a mineln_dfm adattábla sorait, valamint a lexikai diverzitás értékeket. dfm_lexdiv <- mineln_dfm cttr_score <- unlist(textstat_lexdiv(dfm_lexdiv, measure = "CTTR")[, 2]) quanteda::docvars(dfm_lexdiv, "cttr") <- cttr_score docvars(dfm_lexdiv) #> year pm partoldal cttr #> 1 1990 antall_jozsef jobb 33.0 #> 2 2009 bajnai_gordon bal 19.9 #> 3 2005 gyurcsány_ferenc bal 26.1 #> 4 1994 horn_gyula bal 22.3 #> 5 2002 medgyessy_peter bal 16.8 #> 6 1995 orban_viktor jobb 23.4 #> 7 2018 orban_viktor jobb 16.2 A fenti elemzést elvégezhetjük úgy is, hogy valamennyi indexálást egyben megkapjuk. Ehhez a textstat_lexdiv() függvény argumentumába a measure = \"all\" kifejezést kell megadnunk. mineln_dfm %>% textstat_lexdiv(measure = "all") #> document TTR C R CTTR U S K I #> 1 antall_jozsef_1990 0.647 0.949 46.7 33.0 72.9 0.960 9.29 419 #> 2 bajnai_gordon_2009 0.728 0.957 28.2 19.9 73.2 0.962 9.73 459 #> 3 gyurcsány_ferenc_2005 0.556 0.930 37.0 26.1 52.2 0.944 9.60 292 #> 4 horn_gyula_1994 0.714 0.956 31.5 22.3 74.0 0.962 7.45 572 #> 5 medgyessy_peter_2002 0.711 0.951 23.8 16.8 62.8 0.955 17.54 251 #> 6 orban_viktor_1998 0.722 0.957 33.0 23.4 78.1 0.964 11.72 400 #> 7 orban_viktor_2018 0.753 0.958 23.0 16.2 71.5 0.961 15.44 313 #> D Vm Maas lgV0 lgeV0 #> 1 0.000930 0.0287 0.117 11.19 25.8 #> 2 0.000974 0.0269 0.117 10.43 24.0 #> 3 0.000960 0.0279 0.138 9.23 21.3 #> 4 0.000745 0.0232 0.116 10.66 24.5 #> 5 0.001755 0.0373 0.126 9.42 21.7 #> 6 0.001172 0.0314 0.113 11.02 25.4 #> 7 0.001545 0.0345 0.118 9.98 23.0 Ha pedig arra vagyunk kíváncsiak, hogy a kapott értékek hogyan korrelálnak egymással, azt a cor() függvény segítésével számolhatjuk ki. div_df <- mineln_dfm %>% textstat_lexdiv(measure = "all") cor(div_df[, 2:13]) #> TTR C R CTTR U S K I D #> TTR 1.000 0.970 -0.6532 -0.6532 0.7504 0.8470 0.3835 0.257 0.3839 #> C 0.970 1.000 -0.4521 -0.4521 0.8838 0.9498 0.2257 0.402 0.2261 #> R -0.653 -0.452 1.0000 1.0000 -0.0157 -0.1587 -0.6552 0.260 -0.6556 #> CTTR -0.653 -0.452 1.0000 1.0000 -0.0157 -0.1587 -0.6552 0.260 -0.6556 #> U 0.750 0.884 -0.0157 -0.0157 1.0000 0.9835 -0.1556 0.636 -0.1554 #> S 0.847 0.950 -0.1587 -0.1587 0.9835 1.0000 -0.0197 0.571 -0.0194 #> K 0.383 0.226 -0.6552 -0.6552 -0.1556 -0.0197 1.0000 -0.772 1.0000 #> I 0.257 0.402 0.2602 0.2602 0.6361 0.5714 -0.7725 1.000 -0.7722 #> D 0.384 0.226 -0.6556 -0.6556 -0.1554 -0.0194 1.0000 -0.772 1.0000 #> Vm 0.277 0.154 -0.4803 -0.4803 -0.1495 -0.0415 0.9722 -0.826 0.9721 #> Maas -0.781 -0.908 0.0505 0.0505 -0.9969 -0.9932 0.1099 -0.617 0.1097 #> lgV0 0.332 0.549 0.4784 0.4784 0.8692 0.7822 -0.4815 0.709 -0.4815 #> Vm Maas lgV0 #> TTR 0.2773 -0.7812 0.332 #> C 0.1545 -0.9076 0.549 #> R -0.4803 0.0505 0.478 #> CTTR -0.4803 0.0505 0.478 #> U -0.1495 -0.9969 0.869 #> S -0.0415 -0.9932 0.782 #> K 0.9722 0.1099 -0.482 #> I -0.8265 -0.6170 0.709 #> D 0.9721 0.1097 -0.482 #> Vm 1.0000 0.1122 -0.393 #> Maas 0.1122 1.0000 -0.848 #> lgV0 -0.3931 -0.8479 1.000 A kapott értékeket a ggcorr() függvény segítségével ábrázolhatjuk is. Ha a függvény argumentumában a label = TRUE szerepel, a kapott ábrán a kiszámított értékek is láthatók (ld. 5.1. ábra). GGally::ggcorr(div_df[, 2:13], label = TRUE) Ábra 5.1: Korrelációs hőtérkép Az így kapott ábránk egy korrelációs hőtérkép, az oszlopok tetején elhelyezkedő rövidítések az egyes mérőszámokat jelentik, amelyekkel a beszédeket vizsgáltuk ezek képlete megtalálható a textstat lexdiv funkció oldalán. Ezek keresztmetszetében a számok az ábrázolják, hogy az egyes mérőszámok eredményei milyen kapcsolatban állnak egymással. Ahogy az ábra melletti skála is jelzi a piros négyzetben lévő számok pozitív korrelációt jeleznek, a kékben lévők pedig negatívat, minél halványabb egy adott négyzet színezése a korreláció mértéke annál kisebb. Ezt követően azt is megvizsgálhatjuk, hogy a korpusz szövegei mennyire könnyen olvashatóak. Ehhez a Flesch.Kincaid pontszámot használjuk, ami a szavak és a mondatok hossza alapján határozza meg a szöveg olvashatóságát. Ehhez a textstat_readability() függvényt használjuk, mely a korpuszunkat elemzi. quanteda.textstats::textstat_readability(x = corpus_mineln, measure = "Flesch.Kincaid") #> document Flesch.Kincaid #> 1 antall_jozsef_1990 16.5 #> 2 bajnai_gordon_2009 10.9 #> 3 gyurcsány_ferenc_2005 13.6 #> 4 horn_gyula_1994 13.8 #> 5 medgyessy_peter_2002 15.8 #> 6 orban_viktor_1998 13.0 #> 7 orban_viktor_2018 11.4 Ezután a kiszámított értékkel kiegészítjük a korpuszt. docvars(corpus_mineln, "f_k") <- textstat_readability(corpus_mineln, measure = "Flesch.Kincaid")[, 2] docvars(corpus_mineln) #> year pm partoldal f_k #> 1 1990 antall_jozsef jobb 16.5 #> 2 2009 bajnai_gordon bal 10.9 #> 3 2005 gyurcsány_ferenc bal 13.6 #> 4 1994 horn_gyula bal 13.8 #> 5 2002 medgyessy_peter bal 15.8 #> 6 1995 orban_viktor jobb 13.0 #> 7 2018 orban_viktor jobb 11.4 Majd a ggplot2 segítségével vizualizálhatjuk az eredményt (ld. ??. ábra). Ehhez az olvashatósági pontszámmal kiegészített korpuszból egy adattáblát alakítunk ki, majd beállítjuk az ábrázolás paramétereit. Az ábra két tengelyén az év, illetve az olvashatósági pontszám szerepel, a jobb- és a baloldalt a vonal típusa különbözteti meg, az egyes dokumentumokat ponttal jelöljük, az ábrára pedig felíratjuk a miniszterelnökök neveit, valamint azt is beállítjuk, hogy az x tengely beosztása az egyes beszédek dátumához igazodjon. A theme_minimal() függvénnyel pedig azt határozzuk meg, hogy mindez fehér hátteret kapjon. Az így létrehozott ábránkat a ggplotly parancs segítségével pedig interaktívvá is tehetjük. corpus_df <- docvars(corpus_mineln) mineln_df <- ggplot(corpus_df, aes(year, f_k)) + geom_point(size = 2) + geom_line(aes(linetype = partoldal), size = 1) + geom_text(aes(label = pm), color = "black", nudge_y = 0.15) + scale_x_continuous(limits = c(1988, 2020)) + labs( x = NULL, y = "Flesch-Kincaid index", color = NULL, linetype = NULL ) + theme_minimal() + theme(legend.position = "bottom") ggplotly(mineln_df) Ábra 5.2: Az olvashatósági index alakulása 5.4 Összehasonlítás20 A fentiekben láthattuk az eltéréseket a jobb és a bal oldali beszédeken belül, sőt ugyanahhoz a miniszterelnökhöz tartozó két beszéd között is. A következőkben textstat_dist() és textstat_simil() függvények segítségével megvizsgáljuk, valójában mennyire hasonlítanak vagy különböznek ezek a beszédek. Mindkét függvény bemenete dmf, melyből először egy súlyozott dfm-et készítünk, majd elvégezzük az összehasonlítást először a jaccard-féle hasonlóság alapján. mineln_dfm %>% dfm_weight("prop") %>% quanteda.textstats::textstat_simil(margin = "documents", method = "jaccard") #> textstat_simil object; method = "jaccard" #> antall_jozsef_1990 bajnai_gordon_2009 #> antall_jozsef_1990 1.0000 0.0559 #> bajnai_gordon_2009 0.0559 1.0000 #> gyurcsány_ferenc_2005 0.0798 0.0850 #> horn_gyula_1994 0.0694 0.0592 #> medgyessy_peter_2002 0.0404 0.0690 #> orban_viktor_1998 0.0778 0.0626 #> orban_viktor_2018 0.0362 0.0617 #> gyurcsány_ferenc_2005 horn_gyula_1994 #> antall_jozsef_1990 0.0798 0.0694 #> bajnai_gordon_2009 0.0850 0.0592 #> gyurcsány_ferenc_2005 1.0000 0.0683 #> horn_gyula_1994 0.0683 1.0000 #> medgyessy_peter_2002 0.0684 0.0587 #> orban_viktor_1998 0.0734 0.0621 #> orban_viktor_2018 0.0503 0.0494 #> medgyessy_peter_2002 orban_viktor_1998 #> antall_jozsef_1990 0.0404 0.0778 #> bajnai_gordon_2009 0.0690 0.0626 #> gyurcsány_ferenc_2005 0.0684 0.0734 #> horn_gyula_1994 0.0587 0.0621 #> medgyessy_peter_2002 1.0000 0.0650 #> orban_viktor_1998 0.0650 1.0000 #> orban_viktor_2018 0.0504 0.0583 #> orban_viktor_2018 #> antall_jozsef_1990 0.0362 #> bajnai_gordon_2009 0.0617 #> gyurcsány_ferenc_2005 0.0503 #> horn_gyula_1994 0.0494 #> medgyessy_peter_2002 0.0504 #> orban_viktor_1998 0.0583 #> orban_viktor_2018 1.0000 Majd a textstat_dist() függvény segítségével kiszámoljuk a dokumentumok egymástól való különbözőségét. mineln_dfm %>% quanteda.textstats::textstat_dist(margin = "documents", method = "euclidean") #> textstat_dist object; method = "euclidean" #> antall_jozsef_1990 bajnai_gordon_2009 #> antall_jozsef_1990 0 162.8 #> bajnai_gordon_2009 163 0 #> gyurcsány_ferenc_2005 186 137.8 #> horn_gyula_1994 164 80.1 #> medgyessy_peter_2002 160 68.1 #> orban_viktor_1998 139 84.7 #> orban_viktor_2018 167 67.3 #> gyurcsány_ferenc_2005 horn_gyula_1994 #> antall_jozsef_1990 186 163.6 #> bajnai_gordon_2009 138 80.1 #> gyurcsány_ferenc_2005 0 147.0 #> horn_gyula_1994 147 0 #> medgyessy_peter_2002 143 75.9 #> orban_viktor_1998 147 89.6 #> orban_viktor_2018 148 74.8 #> medgyessy_peter_2002 orban_viktor_1998 #> antall_jozsef_1990 160.2 139.2 #> bajnai_gordon_2009 68.1 84.7 #> gyurcsány_ferenc_2005 142.6 146.9 #> horn_gyula_1994 75.9 89.6 #> medgyessy_peter_2002 0 77.5 #> orban_viktor_1998 77.5 0 #> orban_viktor_2018 60.7 83.6 #> orban_viktor_2018 #> antall_jozsef_1990 167.4 #> bajnai_gordon_2009 67.3 #> gyurcsány_ferenc_2005 147.9 #> horn_gyula_1994 74.8 #> medgyessy_peter_2002 60.7 #> orban_viktor_1998 83.6 #> orban_viktor_2018 0 Ezután vizualizálhatjuk is a dokumentumok egymástól való távolságát egy olyan dendogram21 segítségével, amely megmutatja nekünk a lehetséges dokumentumpárokat (ld. 5.3. ábra). dist <- mineln_dfm %>% textstat_dist(margin = "documents", method = "euclidean") hierarchikus_klaszter <- hclust(as.dist(dist)) ggdendro::ggdendrogram(hierarchikus_klaszter) Ábra 5.3: A dokumentumok csoportosítása a távolságuk alapján A textstat_simil funkció segítségével azt is meg tudjuk vizsgálni, hogy egy adott kifejezés milyen egyéb kifejezésekkel korrelál. mineln_dfm %>% textstat_simil(y = mineln_dfm[, c("kormány")], margin = "features", method = "correlation") %>% head(n = 10) #> kormány #> elnök -0.125 #> tisztelt -0.508 #> országgyulés 0.804 #> hölgyeim -0.298 #> uraim -0.298 #> honfitársaim 0.926 #> ünnepi 0.959 #> pillanatban 0.959 #> állok 0.683 #> magyar 0.861 Arra is van lehetőségünk, hogy a két alkorpuszt hasonlítsuk össze egymással. Ehhez a textstat_keyness() függvényt használjuk, melynek a bemenete a dfm. A függvény argumentumában a target = után kell megadni, hogy mely alkorpusz a viszonyítási alap. Az összehasonlítás eredményét a textplot_keyness() függvény segítségével ábrázolhatjuk, ami megjeleníti a két alkorpusz leggyakoribb kifejezéseit (ld. 5.4. ábra). dfm_keyness <- corpus_mineln %>% tokens(remove_punct = TRUE) %>% tokens_remove(pattern = custom_stopwords) %>% dfm() %>% quanteda::dfm_group(partoldal) result_keyness <- quanteda.textstats::textstat_keyness(dfm_keyness, target = "jobb") quanteda.textplots::textplot_keyness(result_keyness, color = c("#484848", "#D0D0D0")) + xlim(c(-65, 65)) + theme(legend.position = c(0.9,0.1)) Ábra 5.4: A korpuszok legfontosabb kifejezései Ha az egyes miniszterelnökök beszédeinek leggyakoribb kifejezéseit szeretnénk összehasonlítani, azt a textstat_frequency() függvény segítségével tehetjük meg, melynek bemenete a megtisztított és súlyozott dfm. Az összehasonlítás eredményét pedig a ggplot2 segítségével ábrázolhatjuk is (ld. 5.5. ábra). Majd ábránkat a plotly segítségével interaktívvá tehetjük. dfm_weighted <- corpus_mineln %>% tokens( remove_punct = TRUE, remove_symbols = TRUE, remove_numbers = TRUE ) %>% tokens_tolower() %>% tokens_wordstem(language = "hungarian") %>% tokens_remove(pattern = custom_stopwords) %>% dfm() %>% dfm_weight(scheme = "prop") freq_weight <- textstat_frequency(dfm_weighted, n = 5, groups = pm) data_df <- ggplot(data = freq_weight, aes(x = nrow(freq_weight):1, y = frequency)) + geom_point() + facet_wrap(~ group, scales = "free", ncol = 1) + theme(panel.spacing = unit(1, "lines"))+ coord_flip() + scale_x_continuous( breaks = nrow(freq_weight):1, labels = freq_weight$feature ) + labs( x = NULL, y = "Relatív szófrekvencia" ) ggplotly(data_df, height = 1000, tooltip = "frequency") Ábra 5.5: Leggyakoribb kifejezések a miniszterelnöki beszédekben Mivel a szövegösszehasonlítás egy komplex kutatási feladat, a témával bőbben is foglalkozunk a Szövegösszhasonlítás fejezetben. 5.5 A kulcsszavak kontextusa Arra is lehetőségünk van, hogy egyes kulcszavakat a korpuszon belül szövegkörnyezetükben vizsgáljunk meg. Ehhez a kwic() függvényt használjuk, az argumentumok között a pattern = kifejezés után megadva azt a szót, amelyet vizsgálni szeretnénk, a window = után pedig megadhatjuk, hogy az adott szó hány szavas környezetére vagyunk kíváncsiak. corpus_mineln %>% tokens() %>% quanteda::kwic( pattern = "válság*", valuetype = "glob", window = 3, case_insensitive = TRUE ) %>% head(5) #> Keyword-in-context with 5 matches. #> [antall_jozsef_1990, 1167] Átfogó és mély | #> [antall_jozsef_1990, 1283] kell hárítanunk a | #> [antall_jozsef_1990, 2772] és a lakásgazdálkodás | #> [antall_jozsef_1990, 5226] gazdaság egészét juttatta | #> [antall_jozsef_1990, 5286] gazdaság reménytelenül eladósodott | #> #> válságba | süllyedtünk a nyolcvanas #> válságot | , de csakis #> válságos | helyzetbe került. #> válságba | , és amellyel #> válsággócai | ellen. A A beszédeket a Hungarian Comparative Agendas Project miniszterelnöki beszéd korpuszából válogattuk: https://cap.tk.hu/vegrehajto↩︎ A lambda leírása megtalálható itt: https://quanteda.io/reference/textstat_collocations.html↩︎ A különböző indexek leírása és képlete megtalálható az alábbi linken: https://quanteda.io/reference/textstat_lexdiv.html↩︎ (Schütze, Manning, and Raghavan 2008)↩︎ Olyan ábra, amely hasonlóságaik vagy különbségeik alapján csoportosított objektumok összefüggéseit mutatja meg.↩︎ "],["sentiment.html", "6 Szótárak és érzelemelemzés 6.1 Fogalmi alapok 6.2 Szótárak az R-ben 6.3 A Magyar Nemzet elemzése 6.4 MNB sajtóközlemények", " 6 Szótárak és érzelemelemzés 6.1 Fogalmi alapok A szentiment- vagy vélemény-, illetve érzelemelemzés a számítógépes nyelvészet részterülete, melynek célja az egyes szövegek tartalmából kinyerni azokat az információkat, amelyek értékelést fejeznek ki.22 A véleményelemzés a szövegeket három szinten osztályozza. A legáltalánosabb a dokumentumszintű osztályozás, amikor egy hosszabb szövegegység egészét vizsgáljuk, míg a mondatszintű osztályozásnál a vizsgálat alapegysége a mondat. A legrészletesebb adatokat akkor nyerjük, amikor az elemzést target-szinten végezzük, azaz meghatározzuk azt is, hogy egy-egy érzelem a szövegen belül mire vonatkozik. Mindhárom szinten azonos a feladat: egyrészt meg kell állapítani, hogy az adott egységben van-e értékelés, vélemény vagy érzelem, és ha igen, akkor pedig meg kell határozni, hogy milyen azok érzelmi tartalma. A pozitív-negatív-semleges skálán mozgó szentimentelemzés mellett az elmúlt két évtizedben jelentős lépések történtek a szövegek emóciótartalmának automatikus vizsgálatára is. A módszer hasonló a szentimentelemzéshez, tartalmilag azonban más skálán mozog. Az emócióelemzés esetén ugyanis nem csak azt kell meghatározni, hogy egy kifejezés pozitív vagy negatív töltettel rendelkezik, hanem azt is, hogy milyen érzelmet (öröm, bánat, undor stb.) hordoz. A szótár alapú szentiment- vagy emócióelemzés alapja az az egyszerű ötlet, hogy ha tudjuk, hogy egyes szavak milyen érzelmeket, érzéseket hordoznak, akkor ezeket a szavakat egy szövegben megszámolva képet kaphatunk az adott dokumentum érzelmi tartalmáról. Mivel a szótár alapú elemzés az adott kategórián belüli kulcsszavak gyakoriságán alapul, ezért van, aki nem tekinti statisztikai elemzésnek (lásd például Young and Soroka (2012)). A tágabb kvantitatív szövegelemzési kontextusban az osztályozáson (classification) belül a felügyelt módszerekhez hasonlóan itt is ismert kategóriákkal dolgozunk, azaz előre meghatározzuk, hogy egy-egy adott szó pozitív vagy negatív tértékű, vagy továbbmenve, milyen érzelmet hordoz, csak egyszerűbb módszertannal (Grimmer and Stewart 2013). A kulcsszavakra építés miatt a módszer a kvalitatív és a kvantitatív kutatási vonalak találkozásának is tekinthető, hiszen egy-egy szónak az érzelmi töltete nem mindig ítélhető meg objektíven. Mint minden módszer esetében, itt is kiemelten fontos ellenőrni, hogy a használt szótár kategóriák és kulcsszavak fedik-e a valóságot. Más szavakkal: validálás, validálás, validálás. A módszer előnyei: Tökéletesen megbízható: a számításoknak nincs probabilisztikus (azaz valószínűségre épülő) eleme, mint például a Support Vector alapú osztályozásnak, illetve az emberi szövegkódolásnál előforduló problémákat is elkerüljük (például azt, hogy két kódoló, vagy ugyanazon kódoló két különböző időpontban nem azonosan értékeli ugyanazt a kifejezést). Általa képesek vagyunk mérni a szöveg látens dimenzióit. Széles körben alkalmazható, egyszerűen számolható. A politikatudományon és a számítógépes nyelvészeten belül nagyon sok kész szótár elérhető, amelyek különböző módszerekkel készültek és különböző területet fednek le (például populizmus, pártprogramok policy tartalma, érzelmek, gazdasági tartalom). Relatíve könnyen adaptálható egyik nyelvi környezetből a másikba, bár szótárfordítások esetén külön hangsúlyt kell fektetni a validálásra.23 A módszer lehetséges hátrányai: A szótár hatékonysága és validitása azon múlik, hogy mennyire egyezik a szótár és a vizsgálni kívánt dokumentum területe. Nem mindegy például, hogy a szótárunkkal tőzsdei jelentések alapján a gazdasági bizonytalanságot vagy nézők filmekre adott értékeléseit szeretnénk-e vizsgálni. Léteznek általános szentimentszótárak, ezek hatékonysága azonban általában alulmúlja a terület-specifikus szótárakét. A terület-specifikus szótár építése kvalitatív folyamat, éppen ezért idő- és emberi erőforrás igényes. A szózsák alapú elemzéseknél a kontextus elvész. Gondoljunk például a tagadásra: a „nem vagyok boldog” kifejezés esetén egy általános szentiment szótár a tagadás miatt félreosztályozná a mondat érzelmi töltését, hiszen a boldog szó önmagában a pozitív kategóriába tartozik. Természetesen az automatikus tagadás kezelésére is vannak lehetőségek, de a kérdés komplexitása miatt ezek bemutatásától most eltekintünk. A legnagyobb méretű általános szentimentszótár az angol nyelvű SentiWordNet (SWN), ami kb. 150 000 szót tartalmaz, amelyek mindegyike a három szentimentérték – pozitív, negatív, semleges – közül kapott egyet.24(Baccianella, Esuli, and Sebastiani 2010) Az R-ben végzett szentimentelemzés során az angol nyelvű szövegekhez több beépített általános szentimentszótár is a rendelkezésünkre áll.25 A teljesség igénye nélkül említhetjük az AFINN,26 a bing27 és az nrc28 szótárakat. Az elemzés sikere több faktortól is függ. Fontos, hogy a korpuszban lévő dokumentumokat körültekintően tisztítsuk meg az elemzés elején (lásd a Korpuszépítés és előkészítés fejezetet). A következő lépésben meg kell bizonyosodnunk arról, hogy a kiválasztott szentiment szótár alkalmazható a korpuszunkra. Amennyiben nem találunk alkalmas szótárt, akkor a saját szótár validálására kell figyelni. A negyedik fejezetben leírtak itt is érvényesek, a dokumentum-kifejezés mátrixot érdemes valamilyen módon súlyozni. 6.2 Szótárak az R-ben A szótár alapú elemzéshez a quanteda csomagot fogjuk használni, illetve a 3. fejezetben már megismert readr, stringr, dplyr tidyverse csomagokat.29 library(stringr) library(dplyr) library(tidyr) library(ggplot2) library(quanteda) library(HunMineR) library(plotly) Mielőtt két esettanulmányt bemutatnánk, vizsgáljuk meg, hogyan néz ki egy szentimentszótár az R-ben. A szótárt kézzel úgy tudjuk elkészíteni, hogy egy listán belül létrehozzuk karaktervektorként a kategóriákat és a kulcsszavakat, és ezt a listát a quanteda dictionary függvényével eltároljuk. szentiment_szotar <- dictionary( list( pozitiv = c("jó", "boldog", "öröm"), negativ = c("rossz", "szomorú", "lehangoló") ) ) szentiment_szotar #> Dictionary object with 2 key entries. #> - [pozitiv]: #> - jó, boldog, öröm #> - [negativ]: #> - rossz, szomorú, lehangoló A quanteda, quanteda.corpora és tidytext R csomagok több széles körben használt szentiment szótárat tartalmaznak, így nem kell kézzel replikálni minden egyes szótárat, amit használni szeretnénk. A szentiment elemzési munkafolyamat, amit ebben a részfejezetben bemutatunk, a következő lépésekből áll: dokumentumok betöltése, szöveg előkészítése, a korpusz létrehozása, dokumentum-kifejezés mátrix létrehozása, szótár betöltése, a dokumentum-kifejezés mátrix szűrése a szótárban lévő kulcsszavakkal, az eredmény vizualizálása, további felhasználása. A fejezetben két különböző korpuszt fogunk elemezni: a 2006-os Magyar Nemzet címlapjainak egy 252 cikkből álló mintáját vizsgáljuk egy magyar szentiment szótárral.30 A második korpusz a Magyar Nemzeti Bank angol nyelvű sajtóközleményeiből áll, amin egy széles körben használt gazdasági szótár használatát mutatjuk be.31 6.3 A Magyar Nemzet elemzése mn_minta <- HunMineR::data_magyar_nemzet_small A HunMineR csomag segítségével beolvassuk a Magyar Nemzet adatbázis egy kisebb részét, ami az esetünkben a 2006-os címlapokon szereplő híreket jelenti. A summary() parancs, ahogy a neve is mutatja, gyors áttekintést nyújt a betöltött adatbázisról. Látjuk, hogy 2834 sorból (megfigyelés) és 3 oszlopból (változó) áll. Első ránézésre látszik, hogy a text változónk tartalmazza a szövegeket, és hogy azok tisztításra szorulnak. glimpse(mn_minta) #> Rows: 2,834 #> Columns: 3 #> $ doc_id <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, … #> $ text <chr> "Hat fővárosi képviselő öt percnél is kevesebbet beszél… #> $ doc_date <date> 2006-01-02, 2006-01-02, 2006-01-02, 2006-01-02, 2006-0… A glimpse függvény segítségével belepillanthatunk a használt korpuszba és láthatjuk, hogy az 3 oszlopból áll a dokumentum azonosítójából, amely csak egy sorszám, a dokumentum szövegéből, és a dokumentumhoz tartozó azonosítóból. Az első szöveget megnézve látjuk, hogy a standard előkészítési lépések mellett a sortörést (\\n) is ki kell törölnünk. mn_minta$text[1] #> [1] "Hat fővárosi képviselő öt percnél is kevesebbet beszélt egy év alatt a közgyűlésben.\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n" Habár a quanteda is lehetőséget ad néhány előkészítő lépésre, érdemes ezt olyan céleszközzel tenni, ami nagyobb rugalmasságot ad a kezünkbe. Mi erre a célra a stringr csomagot használjuk. Első lépésben kitöröljük a sortöréseket (\\n), a központozást, a számokat és kisbetűsítünk minden szót. Előfordulhat, hogy (számunkra nehezen látható) extra szóközök maradnak a szövegben. Ezeket az str_squish()függvénnyel tüntetjük el. A szöveg eleji és végi extra szóközöket (leading vagy trailing white space) az str_trim() függvény vágja le. mn_tiszta <- mn_minta %>% mutate( text = stringr::str_remove_all(string = text, pattern = "\\n"), text = stringr::str_remove_all(string = text, pattern = "[:punct:]"), text = stringr::str_remove_all(string = text, pattern = "[:digit:]"), text = stringr::str_to_lower(text), text = stringr::str_trim(text), text = stringr::str_squish(text) ) A szöveg sokkal jobban néz ki, habár észrevehetjük, hogy maradhattak benne problémás részek, főleg a sortörés miatt, ami sajnos hol egyes szavak közepén van (a jobbik eset), vagy pedig pont szóhatáron, ez esetben a két szó sajnos összevonódik. Az egyszerűség kedvéért feltételezzük, hogy ez kellően ritkán fordul elő ahhoz, hogy ne befolyásolja az elemzésünk eredményét. mn_tiszta$text[1] #> [1] "hat fővárosi képviselő öt percnél is kevesebbet beszélt egy év alatt a közgyűlésben" Miután kész a tisztá(bb) szövegünk, korpuszt hozunk létre a quanteda corpus() függvényével. A korpusz objektum a szöveg mellett egyéb dokumentum meta adatokat is tud tárolni (dátum, író, hely, stb.) Ezeket mi is hozzáadhatjuk (erre majd látunk példát), illetve amikor létrehozzuk a korpuszt a data frame-ünkből, automatikusan metaadatokként tárolódnak a változóink. Jelen esetben az egyetlen dokumentum változónk a szöveg mellett a dátum lesz. A korpusz dokumentum változóihoz a docvars() függvény segítségével tudunk hozzáférni. mn_corpus <- corpus(mn_tiszta) head(docvars(mn_corpus), 5) #> doc_date #> 1 2006-01-02 #> 2 2006-01-02 #> 3 2006-01-02 #> 4 2006-01-02 #> 5 2006-01-02 A következő lépés a dokumentum-kifejezés mátrix létrehozása a dfm() függvénnyel. Először tokenekre bontjuk a szövegeket a tokens() paranccsal, és aztán ezt a tokenizált szózsákot kapja meg a dfm inputnak. A sor a végén a létrehozott mátrixunkat TF-IDF módszerrel súlyozzuk a dfm_tfidf() függvény használatával. mn_dfm <- mn_corpus %>% tokens(what = "word") %>% dfm() %>% dfm_tfidf() A cikkek szentimentjét egy magyar szótárral fogjuk becsülni, amit a Társadalomtudományi Kutatóközpont kutatói a Mesterséges Intelligencia Nemzeti Laboratórium projekt keretében készítettek.32 Két dimenziót tarlamaz (pozitív és negatív), 2614 pozitív és 2654 negatív kulcsszóval. Ez nem számít kirívóan nagynak a szótárak között, mivel az adott kategóriák minél teljesebb lefedése a cél. poltext_szotar <- HunMineR::dictionary_poltext poltext_szotar #> Dictionary object with 2 key entries. #> - [positive]: #> - abszolút, ad, adaptív, adekvát, adócsökkentés, adókedvezmény, adomány, adományoz, adóreform, adottság, adottságú, áfacsökkentés, agilis, agytröszt, áhított, ajándék, ajándékoz, ajánl, ajánlott, akadálytalan [ ... and 2,279 more ] #> - [negative]: #> - aberrált, abnormális, abnormalitás, abszurd, abszurditás, ádáz, adócsalás, adócsaló, adós, adósság, áfacsalás, áfacsaló, affér, aggasztó, aggodalom, aggódik, aggódás, agresszió, agresszíven, agresszivitás [ ... and 2,568 more ] Az egyes dokumentumok szentimentjét a dfm_lookup() becsüli, ahol az előző lépésben létrehozott súlyozott dfm az input és a magyar szentimentszótár a dictionary. Egy gyors pillantás az eredményre és látjuk hogy minden dokumentumhoz készült egy pozitív és egy negatív érték. A TF-IDF súlyozás miatt nem látunk egész számokat (a súlyozás nélkül a sima szófrekvenciát kapnánk). mn_szentiment <- quanteda::dfm_lookup(mn_dfm, dictionary = poltext_szotar) head(mn_szentiment, 5) #> Document-feature matrix of: 5 documents, 2 features (40.00% sparse) and 1 docvar. #> features #> docs positive negative #> 1 0 0 #> 2 0.838 12.50 #> 3 0 0 #> 4 21.104 6.45 #> 5 11.036 8.13 Ahhoz, hogy fel tudjuk használni a kapott eredményt, érdemes dokumentumváltozóként eltárolni a korpuszban. Ezt a fent már használt docvars() függvény segítségével tudjuk megtenni, ahol a második argumentumként az új változó nevét adjuk meg. docvars(mn_corpus, "pos") <- as.numeric(mn_szentiment[, 1]) docvars(mn_corpus, "neg") <- as.numeric(mn_szentiment[, 2]) head(docvars(mn_corpus), 5) #> doc_date pos neg #> 1 2006-01-02 0.000 0.00 #> 2 2006-01-02 0.838 12.50 #> 3 2006-01-02 0.000 0.00 #> 4 2006-01-02 21.104 6.45 #> 5 2006-01-02 11.036 8.13 Végül a kapott korpuszt a kiszámolt szentimentértékekkel a quanteda-ban lévő convert() függvénnyel adattáblává alakítjuk. Aconvert() függvény dokumentációját érdemes elolvasni, mert ennek segítségével tudjuk a quanteda-ban elkészült objektumainkat átalakítani úgy, hogy azt más csomagok is tudják használni. mn_df <- quanteda::convert(mn_corpus, to = "data.frame") Mielőtt vizualizálnánk az eredményt érdemes a napi szintre aggregálni a szentimentértéket és egy nettó értéket kalkulálni (ld. 6.1. ábra).33 mn_df <- mn_df %>% group_by(doc_date) %>% summarise( daily_pos = sum(pos), daily_neg = sum(neg), net_daily = daily_pos - daily_neg ) Az így kapott plot y tengelyén az adott cikkek időpontját láthatjuk, míg az x tengelyén a szentiment értékeiket. Ebben több kiugrást is tapasztalhatunk. Természetesen messzemenő következtetéseket egy ilyen kis korpusz alapján nem vonhatunk le, de a kiugrásokhoz tartozó cikkek kvalitatív vizsgálatával megállapíthatjuk, hogy az áprilisi kiugrást a választásokhoz kötődő cikkek pozitív hangulata, míg az októberi negatív kilengést az öszödi beszéd nyilvánosságra kerüléséhez köthető cikkek negatív szentimentje okozza. mncim_df <- ggplot(mn_df, aes(doc_date, net_daily)) + geom_line() + labs( y = "Szentiment", x = NULL, caption = "Adatforrás: https://cap.tk.hu/" ) ggplotly(mncim_df) Ábra 6.1: Magyar Nemzet címlap szentimentje 6.4 MNB sajtóközlemények A második esettanulmányban a kontextuális szótárelemzést mutatjuk be egy angol nyelvű korpusz és specializált szótár segítségével. A korpusz az MNB kamatdöntéseit kísérő nemzetközi sajtóközleményei, a szótár pedig a Loughran and McDonald (2011) pénzügyi szentimentszótár.34 penzugy_szentiment <- HunMineR::dictionary_LoughranMcDonald penzugy_szentiment #> Dictionary object with 9 key entries. #> - [NEGATIVE]: #> - abandon, abandoned, abandoning, abandonment, abandonments, abandons, abdicated, abdicates, abdicating, abdication, abdications, aberrant, aberration, aberrational, aberrations, abetting, abnormal, abnormalities, abnormality, abnormally [ ... and 2,335 more ] #> - [POSITIVE]: #> - able, abundance, abundant, acclaimed, accomplish, accomplished, accomplishes, accomplishing, accomplishment, accomplishments, achieve, achieved, achievement, achievements, achieves, achieving, adequately, advancement, advancements, advances [ ... and 334 more ] #> - [UNCERTAINTY]: #> - abeyance, abeyances, almost, alteration, alterations, ambiguities, ambiguity, ambiguous, anomalies, anomalous, anomalously, anomaly, anticipate, anticipated, anticipates, anticipating, anticipation, anticipations, apparent, apparently [ ... and 277 more ] #> - [LITIGIOUS]: #> - abovementioned, abrogate, abrogated, abrogates, abrogating, abrogation, abrogations, absolve, absolved, absolves, absolving, accession, accessions, acquirees, acquirors, acquit, acquits, acquittal, acquittals, acquittance [ ... and 883 more ] #> - [CONSTRAINING]: #> - abide, abiding, bound, bounded, commit, commitment, commitments, commits, committed, committing, compel, compelled, compelling, compels, comply, compulsion, compulsory, confine, confined, confinement [ ... and 164 more ] #> - [SUPERFLUOUS]: #> - aegis, amorphous, anticipatory, appertaining, assimilate, assimilating, assimilation, bifurcated, bifurcation, cessions, cognizable, concomitant, correlative, deconsolidation, delineation, demonstrable, demonstrably, derecognized, derecognizes, derivatively [ ... and 36 more ] #> [ reached max_nkey ... 3 more keys ] A szentimentszótár 9 kategóriából áll. A legtöbb kulcsszó a negatív dimenzióhoz van (2355). A munkamenet hasonló az előző példához: adat betöltés, szövegtisztítás, korpusz létrehozás, tokenizálás, kulcs kontextuális tokenek szűrése, dfm előállítás és szentiment számítás, az eredmény vizualizálása, további felhasználása. mnb_pr <- HunMineR::data_mnb_pr Adatbázisunk 180 megfigyelésből és 4 változóból áll. Az egyetlen lényeges dokumentum metaadat itt is a szövegek megjelenési ideje, de a glimpse függvénnyel itt is ellenőrizhetjük hogyan néz ki a korpusz felépítése és milyen metaadatokat tartalmaz pontosan. glimpse(mnb_pr) #> Rows: 180 #> Columns: 4 #> $ date <date> 2005-01-24, 2005-02-21, 2005-03-29, 2005-04-25, 2005-05-23… #> $ text <chr> "At its meeting on January the Monetary Council considered … #> $ id <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, … #> $ year <dbl> 2005, 2005, 2005, 2005, 2005, 2005, 2005, 2005, 2005, 2005,… Ez alapján pedig láthatjuk, hogy a korpusz a tényleges szövegek mellett tartalmaz még id sorszámot, pontos dátumot és évet is. A szövegeket ugyanazokkal a standard eszközökkel kezeljük, mint a Magyar Nemzet esetében. Érdemes minden esetben ellenőrizni, hogy az R-kód, amit használunk, tényleg azt csinálja-e, amit szeretnénk. Ez hatványozottan igaz abban az esetben, amikor szövegekkel és reguláris kifejezésekkel dolgozunk. mnb_tiszta <- mnb_pr %>% 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) ) Miután rendelkezésre állnak a tiszta dokumentumaink, egy karaktervektorba gyűjtjük azokat a kulcsszavakat, amelyek környékén szeretnénk megfigyelni a szentiment alakulását. A példa kedvéért mi az unemp*, growth, gdp, inflation* szótöveket és szavakat választottuk. A tokens_keep() megtartja a kulcsszavainkat és egy általunk megadott +/- n tokenes környezetüket (jelen esetben 10). A szentimentelemzést pedig már ezen a jóval kisebb mátrixon fogjuk lefuttatni. A phrase() segítségével több szóból álló kifejezéséket is vizsgálhatunk. Ilyen szókapcsolat például az „Európai Unió” is, ahol lényeges, hogy egyben kezeljük a két szót. mnb_corpus <- corpus(mnb_tiszta) gazdasag <- c("unemp*", "growth", "gdp", "inflation*", "inflation expectation*") mnb_token <- tokens(mnb_corpus) %>% tokens_keep(pattern = phrase(gazdasag), window = 10) A szentimentet most is egy súlyozott dfm-ből számoljuk. A kész eredményt hozzáadjuk a korpuszhoz, majd adattáblát hozunk létre belőle. A 7 kategóriából 5-öt használunk csak, amelyeknek jegybanki környezetben értelmezhető tartalma van. mnb_szentiment <- tokens_lookup(mnb_token, dictionary = penzugy_szentiment) %>% dfm() %>% dfm_tfidf() docvars(mnb_corpus, "negative") <- as.numeric(mnb_szentiment[, "negative"]) docvars(mnb_corpus, "positive") <- as.numeric(mnb_szentiment[, "positive"]) docvars(mnb_corpus, "uncertainty") <- as.numeric(mnb_szentiment[, "uncertainty"]) docvars(mnb_corpus, "constraining") <- as.numeric(mnb_szentiment[, "constraining"]) docvars(mnb_corpus, "superfluous") <- as.numeric(mnb_szentiment[, "superfluous"]) mnb_df <- convert(mnb_corpus, to = "data.frame") A célunk, hogy szentiment kategóriánkénti bontásban mutassuk be az elemzésünk eredményét, de előtte egy kicsit alakítani kell az adattáblán, hogy a korábban már tárgyalt tidy formára hozzuk. A különböző szentiment értékeket tartalmazó oszlopokat fogjuk átrendezni úgy, hogy kreálunk egy „sent_type” változót, ahol a kategória nevet fogjuk eltárolni és egy „sent_score” változót, ahol a szentiment értéket. Ehhez a tidyr-ben található pivot_longer() föggvényt használjuk. mnb_df <- mnb_df %>% tidyr::pivot_longer( cols = negative:superfluous, names_to = "sent_type", values_to = "sent_score" ) Az átalakítás után már könnyedén tudjuk kategóriákra bontva megjeleníteni az MNB közlemények különböző látens dimenzióit. Fontos emlékezni arra, hogy ez az eredmény a kulcsszavaink +/- 10 tokenes környezetében lévő szavak szentimentjét méri. Az így kapott ábránk a három alkalmazott szentiment kategória időbeli előfordulását mutatja be. Ami érdekes eredmény, hogy a felesleges „töltelék” (superfluous) szövegek szinte soha nem fordulnak elő a kulcsszavaink körül. A többi érték is nagyjából megfelel a várakozásainknak, habár a 2008-as gazdasági válság nem tűnik kiugró pontnak. Azonban a 2010 utáni európai válság már láthatóan megjelenik az idősorainkban (ld. 6.2. ábra). Az általunk használt szótár alapvetően az Egyesült Államokban a tőzsdén kereskedő cégek publikus beszámolóiból készült, így elképzelhető, hogy egyes jegybanki környezetben sokat használt kifejezések nincsenek benne. A kapott eredmények validálása ezért is nagyon fontos, illetve érdemes azzal is tisztában lenni, hogy a szótáras módszer nem tökéletes (ahogy az emberi vagy más gépi kódolás sem). mnsent_df <- ggplot(mnb_df, aes(date, sent_score)) + geom_line() + labs( y = NULL, x = NULL ) + facet_wrap(~sent_type, ncol = 1)+ theme(panel.spacing = unit(2, "lines")) ggplotly(mnsent_df) Ábra 6.2: Magyar Nemzeti Bank közleményeinek szentimentje Bővebben lásd például: (Liu 2010)↩︎ A lehetséges, területspecifikus szótáralkotási módszerekről részletesebben ezekben a tanulmányokban lehet olvasni: Laver and Garry (2000); Young and Soroka (2012); Loughran and McDonald (2011); Máté, Sebők, and Barczikay (2021)↩︎ A szótár és dokumentációja elérhető az alábbi linken: https://github.com/aesuli/SentiWordNet↩︎ A quanteda.dictionaries csomag leírása és a benne található szótárak az alábbi github linken érhetőek el: https://github.com/kbenoit/quanteda.dictionaries↩︎ A szótár és dokumentációja elérhető itt: http://www2.imm.dtu.dk/pubdb/pubs/6010-full.html↩︎ A szótár és dokumentációja elérhető itt: https://www.cs.uic.edu/~liub/FBS/sentiment-analysis.html↩︎ A szótár és dokumentációja elérhető itt: http://saifmohammad.com/WebPages/NRC-Emotion-Lexicon.htm↩︎ A szentimentelemzéshez gyakran használt csomag még a tidytext. A szerzők online is szabadon elérhető könyvük Silge and Robinson (2017) 2. fejezetében részletesen is bemutatják a tidytext munkafolyamatot: (https://www.tidytextmining.com/sentiment.html).↩︎ A korpusz a Hungarian Compartive Agendas Project keretében készült és regisztáció után, kutatási célra elérhető az alábbi linken: https://cap.tk.hu/a-media-es-a-kozvelemeny-napirendje.↩︎ A korpusz, a szótár és az elemzés teljes dokumentációja elérhető az alábbi github linken: https://github.com/poltextlab/central_bank_communication, a teljes elemzés (Máté, Sebők, and Barczikay 2021) elérhető: https://doi.org/10.1371/journal.pone.0245515↩︎ ELKH TK MILAB: https://milab.tk.hu/hu A szótár és a hozzátartozó dokumentáció elérhető az alábbi github oldalon: https://github.com/poltextlab/sentiment_hun↩︎ A csoportosított adatokkal való munka bővebb bemutatását lásd a Függelékben.↩︎ A témával részletesen foglalkozó tanulmányban egy saját monetáris szentimentszótárat mutatunk be: Az implementáció és a hozzá tartozó R forráskód nyilvános: https://doi.org/10.6084/m9.figshare.13526156.v1↩︎ "],["lda_ch.html", "7 Felügyelet nélküli tanulás 7.1 Fogalmi alapok 7.2 K-közép klaszterezés 7.3 LDA topikmodellek35 7.4 Strukturális topikmodellek", " 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_fe… #> $ text <chr> "Elnök Úr! Tisztelt Országgyulés! Hölgyeim és Uraim! Honf… #> $ year <dbl> 1990, 2009, 2005, 1994, 2002, 1995, 2018 #> $ pm <chr> "antall_jozsef", "bajnai_gordon", "gyurcsány_ferenc", "ho… 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 2016). 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", "199… #> $ text <chr> "1998. évi L. törvény\\n\\naz Egyesült Nemzetek Szervezete … 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.3034 0.0310 9.79 < 2e-16 *** #> majortopic2 -0.2040 0.0677 -3.01 0.00265 ** #> majortopic3 -0.2044 0.0595 -3.43 0.00062 *** #> majortopic4 -0.2211 0.0589 -3.75 0.00018 *** #> majortopic5 0.1030 0.0472 2.18 0.02928 * #> majortopic6 -0.2231 0.0587 -3.80 0.00015 *** #> majortopic7 -0.1557 0.0681 -2.29 0.02244 * #> majortopic8 -0.2157 0.0729 -2.96 0.00315 ** #> majortopic9 0.5250 0.0876 5.99 2.9e-09 *** #> majortopic10 -0.1087 0.0549 -1.98 0.04805 * #> majortopic12 -0.1741 0.0407 -4.28 2.0e-05 *** #> majortopic13 -0.1358 0.0560 -2.43 0.01543 * #> majortopic14 -0.2172 0.0755 -2.88 0.00411 ** #> majortopic15 -0.1475 0.0427 -3.46 0.00057 *** #> majortopic16 -0.0959 0.0531 -1.81 0.07100 . #> majortopic17 -0.2243 0.0580 -3.86 0.00012 *** #> majortopic18 0.2104 0.0573 3.67 0.00025 *** #> majortopic19 0.0738 0.0512 1.44 0.14966 #> majortopic20 -0.2105 0.0392 -5.37 1.0e-07 *** #> majortopic21 -0.2247 0.0698 -3.22 0.00131 ** #> majortopic23 -0.1670 0.0939 -1.78 0.07566 . #> electoral_cycle2002-2006 -0.1036 0.0210 -4.94 9.2e-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: árucikkeket, átalányösszegben, átléphetik, átszállítást, beruházóikat, célországban, cikktanulók #> 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, költségvetésszemélyi, kiadásokfelhalmozási, 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 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 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.↩︎ 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.↩︎ Részletesebben lásd például: http://brooksandrew.github.io/simpleblog/articles/latent-dirichlet-allocation-under-the-hood/↩︎ 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)↩︎ A tidy formátumról bővebben: https://cran.r-project.org/web/packages/tidyr/vignettes/tidy-data.html↩︎ 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.↩︎ A kódkönyv elérhető az alábbi linken: Comparative Agendas Project.↩︎ Az stm csomaghoz tartozó leírás: https://cran.r-project.org/web/packages/stm/vignettes/stmVignette.pdf↩︎ "],["embedding.html", "8 Szóbeágyazások 8.1 A szóbeágyazás célja 8.2 Word2Vec és GloVe", " 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_… #> $ text <chr> "Csere szerb módra\\nNagy vihart kavart Szerbiában a kormá… 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_… #> $ text <chr> "csere szerb módranagy vihart kavart szerbiában a kormány… 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 [13:19:48.862] epoch 1, loss 0.2291 #> INFO [13:20:14.583] epoch 2, loss 0.0963 #> INFO [13:20:34.354] epoch 3, loss 0.0706 #> INFO [13:20:52.810] epoch 4, loss 0.0490 #> INFO [13:21:11.203] epoch 5, loss 0.0412 #> INFO [13:21:29.323] epoch 6, loss 0.0362 #> INFO [13:21:45.562] epoch 7, loss 0.0326 #> INFO [13:22:03.680] epoch 8, loss 0.0298 #> INFO [13:22:03.681] 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.519 0.508 0.465 0.409 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.429 #> 4 elnök 0.394 #> 5 demokrata 0.389 #> 6 republikánus 0.282 #> # ℹ 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.00 #> 2 viktor 0.932 #> 3 miniszterelnök 0.764 #> 4 mondta 0.699 #> 5 kormányfő 0.686 #> 6 fidesz 0.675 #> # ℹ 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.537 0.422 0.394 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 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.↩︎ 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↩︎ A futtatásra használt PC konfiguráció: CPU: Intel Core i5-4460 (3.2GHz); RAM: 16GB↩︎ 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).↩︎ "],["scaling.html", "9 Szövegskálázás 9.1 Fogalmi alapok 9.2 Wordscores 9.3 Wordfish", " 9 Szövegskálázás 9.1 Fogalmi alapok A politikatudomány egyik izgalmas kérdése a szereplők ideológiai, vagy közpolitikai skálákon való elhelyezése. Ezt jellemzően pártprogramok vagy különböző ügyekkel kapcsolatos álláspontpontok alapján szokták meghatározni, de a politikusok beszédei is alkalmasak arra, hogy meghatározzuk a beszélő ideológiai hovatartozását. A szövegbányászat területén jellemzően a wordfish és a wordscores módszert alkalmazzák erre a feladatra. Míg előbbi a felügyelet nélküli módszerek sorába tartozik, utóbbi felügyelt módszerek közé. A wordscores a szótári módszerekhez hasonlóan a szövegeket a bennük található szavak alapján helyezi el a politikai térben oly módon, hogy az ún. referenciadokumentumok szövegét használja tanító halmazként. A wordscores kiindulópontja, hogy pozíció pontszámokat kell rendelni referencia szövegekhez. A modell számításba veszi a szövegek szavainak súlyozott gyakoriságát és a pozíciópontszám, valamint a szógyakoriság alapján becsüli meg a korpuszban lévő többi dokumentum pozícióját (Laver, Benoit, and Garry 2003) A felügyelet nélküli wordfish módszer a skálázás során nem a referencia dokumentumokra támaszkodik, hanem olyan kifejezéseket keres a szövegben, amelyek megkülönböztetik egymástól a politikai spektrum különböző pontjain elhelyezkedő beszélőket. Az IRT-n (item response theory) alapuló módszer azt feltételezi, hogy a politikusok egy kevés dimenziós politikai térben mozognak, amely tér leírható az i politikus \\(\\theta_1\\) paraméterével. Egy politikus (vagy párt) ebben a térben elfoglalt helyzete pedig befolyásolja a szavak szövegekben történő használatát. A módszer erőssége, hogy kevés erőforrás-befektetéssel megbízható becsléseket ad, ha a szövegek valóban az ideológiák mentén különböznek, tehát ha a szereplők erősen ideológiai tartalamú diskurzust folytatnak. Alkalmazásakor azonban tudnunk kell: a módszer nem képes kezelni, hogy a szövegek között nem csak ideológiai különbség lehet, hanem például stílusból és témából eredő eltérések is. Mivel a modell nem felügyelt, ezért nehéz garantálni, hogy valóban megbízhatóan azonosítja a szereplők elhelyezkedését a politikai térben, így az eredményeket mindenképpen körültekintően és alaposan kell validálni (Grimmer and Stewart 2013; Hjorth et al. 2015; Slapin and Proksch 2008). library(readr) library(dplyr) library(stringr) library(ggplot2) library(ggrepel) library(quanteda) library(quanteda.textmodels) library(plotly) library(HunMineR) A skálázási algoritmusokat egy kis korpuszon mutatjuk be. A minta dokumentumok a 2014–2018-as parlamenti ciklusban az Országgyűlésben frakcióvezető politikusok egy-egy véletlenszerűen kiválasztott napirend előtti felszólalásai. Ebben a ciklusban összesen 11 frakcióvezetője volt a két kormánypárti és öt ellenzéki frakciónak. 47 A dokumentumokon először elvégeztük a szokásos előkészítési lépéseket. parl_beszedek <- HunMineR::data_parlspeakers_small beszedek_tiszta <- parl_beszedek %>% 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 ismét megtekinthetjük, mint az eredeti szöveget és a tisztított is, ezzel nem csak azt tudjuk ellenőrizni, hogy a tisztítás sikeres volt-e, hanem a metaadatokat is megnézhetjük, amelyek jelenesetben a felszólalás azonosító száma a felszólaló neve, valamint a pártja. glimpse(parl_beszedek) #> Rows: 10 #> Columns: 4 #> $ id <chr> "20142018_024_0002_0002", "20142018_055_0002_0002", "… #> $ text <chr> "VONA GÁBOR (Jobbik): Tisztelt Elnök Úr! Tisztelt Ors… #> $ felszolalo <chr> "Vona Gábor (Jobbik)", "Dr. Schiffer András (LMP)", "… #> $ part <chr> "Jobbik", "LMP", "LMP", "MSZP", "LMP", "MSZP", "Jobbi… glimpse(beszedek_tiszta) #> Rows: 10 #> Columns: 4 #> $ id <chr> "20142018_024_0002_0002", "20142018_055_0002_0002", "… #> $ text <chr> "vona gábor jobbik tisztelt elnök úr tisztelt országg… #> $ felszolalo <chr> "Vona Gábor (Jobbik)", "Dr. Schiffer András (LMP)", "… #> $ part <chr> "Jobbik", "LMP", "LMP", "MSZP", "LMP", "MSZP", "Jobbi… A wordfish és wordscores algoritmus is ugyanazt a kiinduló korpuszt és dfm objektumot használja, amit a szokásos módon a quanteda csomag corpus() függvényével hozunk létre. beszedek_corpus <- corpus(beszedek_tiszta) summary(beszedek_corpus) #> Corpus consisting of 10 documents, showing 10 documents: #> #> Text Types Tokens Sentences id #> text1 442 819 1 20142018_024_0002_0002 #> text2 354 607 1 20142018_055_0002_0002 #> text3 426 736 1 20142018_064_0002_0002 #> text4 314 538 1 20142018_115_0002_0002 #> text5 354 589 1 20142018_158_0002_0002 #> text6 333 538 1 20142018_172_0002_0002 #> text7 344 559 1 20142018_206_0002_0002 #> text8 352 628 1 20142018_212_0002_0002 #> text9 317 492 1 20142018_236_0002_0002 #> text10 343 600 1 20142018_249_0002_0002 #> felszolalo part #> Vona Gábor (Jobbik) Jobbik #> Dr. Schiffer András (LMP) LMP #> Dr. Szél Bernadett (LMP) LMP #> Tóbiás József (MSZP) MSZP #> Schmuck Erzsébet (LMP) LMP #> Dr. Tóth Bertalan (MSZP) MSZP #> Volner János (Jobbik) Jobbik #> Kósa Lajos (Fidesz) Fidesz #> Harrach Péter (KDNP) KDNP #> Dr. Gulyás Gergely (Fidesz) Fidesz A leíró statisztikai táblázatban látszik, hogy a beszédek hosszúsága nem egységes, a leghosszabb 819, a legrövidebb pedig 492 szavas. Az átlagos dokumentum hossz az 611 szó. A korpusz szemléltető célú, alaposabb elemzéshez hosszabb és/vagy több dokumentummal érdemes dolgoznunk. A korpusz létrehozása után elkészítjük a dfm mátrixot, amelyből eltávolítjuk a magyar stopszvakat a HunMineR beépített szótára segítségével. stopszavak <- HunMineR::data_stopwords_extra beszedek_dfm <- beszedek_corpus %>% tokens() %>% tokens_remove(stopszavak) %>% dfm() 9.2 Wordscores A modell illesztést a wordfish-hez hasonlóan a quanteda.textmodels csomagban található textmodel_wordscores() függvény végzi. A kiinduló dfm ugyanaz, mint amit a fejezet elején elkészítettünk, a beszedek_dfm. A referencia pontokat dokumentumváltozóként hozzáadjuk a dfm-hez (a refrencia_pont oszlopot, ami NA értéket kap alapértelmezetten). A kiválasztott referencia dokumentumoknál pedig egyenként hozzáadjuk az értékeket. Erre több megoldás is van, az egyszerűbb út, hogy az egyik és a másik végletet a -1; 1 intervallummal jelöljük. Ennek a lehetséges alternatívája, hogy egy külső, már validált forrást használunk. Pártok esetén ilyen lehet a Chapel Hill szakértői kérdőívének a pontszámai, a Manifesto projekt által kódolt jobb-bal (rile) dimenzió. A lenti példánál mi maradunk az egyszerűbb bináris kódolásnál (ld. 9.4. ábra). A wordfish eredményt alapul véve a két referencia pont Gulyás Gergely és Szél Bernadett beszédei lesznek.48 Ezek a 3. és a 10. dokumentumok. Miután a referencia pontokat hozzárendeltünk az adattáblához szintén a docvars funkcióval meg is tekinthetjük azt és láthatjuk, hogy a referenci_pont már a metaadatok között szerepel. docvars(beszedek_dfm, "referencia_pont") <- NA docvars(beszedek_dfm, "referencia_pont")[3] <- -1 docvars(beszedek_dfm, "referencia_pont")[10] <- 1 docvars(beszedek_dfm) #> id felszolalo part #> 1 20142018_024_0002_0002 Vona Gábor (Jobbik) Jobbik #> 2 20142018_055_0002_0002 Dr. Schiffer András (LMP) LMP #> 3 20142018_064_0002_0002 Dr. Szél Bernadett (LMP) LMP #> 4 20142018_115_0002_0002 Tóbiás József (MSZP) MSZP #> 5 20142018_158_0002_0002 Schmuck Erzsébet (LMP) LMP #> 6 20142018_172_0002_0002 Dr. Tóth Bertalan (MSZP) MSZP #> 7 20142018_206_0002_0002 Volner János (Jobbik) Jobbik #> 8 20142018_212_0002_0002 Kósa Lajos (Fidesz) Fidesz #> 9 20142018_236_0002_0002 Harrach Péter (KDNP) KDNP #> 10 20142018_249_0002_0002 Dr. Gulyás Gergely (Fidesz) Fidesz #> referencia_pont #> 1 NA #> 2 NA #> 3 -1 #> 4 NA #> 5 NA #> 6 NA #> 7 NA #> 8 NA #> 9 NA #> 10 1 A lenti wordscores-modell specifikáció követi a Laver, Benoit, and Garry (2003) tanulmányban leírtakat. beszedek_ws <- textmodel_wordscores( x = beszedek_dfm, y = docvars(beszedek_dfm, "referencia_pont"), scale = "linear", smooth = 0 ) summary(beszedek_ws, 10) #> #> Call: #> textmodel_wordscores.dfm(x = beszedek_dfm, y = docvars(beszedek_dfm, #> "referencia_pont"), scale = "linear", smooth = 0) #> #> Reference Document Statistics: #> score total min max mean median #> text1 NA 411 0 6 0.181 0 #> text2 NA 341 0 5 0.150 0 #> text3 -1 383 0 6 0.168 0 #> text4 NA 289 0 5 0.127 0 #> text5 NA 321 0 8 0.141 0 #> text6 NA 298 0 5 0.131 0 #> text7 NA 307 0 5 0.135 0 #> text8 NA 354 0 8 0.156 0 #> text9 NA 250 0 3 0.110 0 #> text10 1 353 0 8 0.155 0 #> #> Wordscores: #> (showing first 10 elements) #> tisztelt elnök országgyűlés ország nemhogy #> -0.1027 0.3691 0.0408 -1.0000 -1.0000 #> tette fidesz taps padsoraiban természetesen #> -1.0000 1.0000 -1.0000 -1.0000 1.0000 Az illesztett wordscores modellünkkel ezek után már meg tudjuk becsülni a korpuszban lévő többi dokumentum pozícióját. Ehhez a predict() függvény megoldását használjuk. A kiegészítő opciókkal a konfidencia intervallum alsó és felső határát is meg tudjuk becsülni, ami jól jön akkor, ha szeretnénk ábrázolni az eredményt. beszedek_ws_pred <- predict( beszedek_ws, newdata = beszedek_dfm, interval = "confidence") beszedek_ws_pred <- as.data.frame(beszedek_ws_pred$fit) beszedek_ws_pred #> fit lwr upr #> text1 -0.4585 -0.6883 -0.2287 #> text2 -0.2664 -0.4912 -0.0416 #> text3 -0.9448 -0.9660 -0.9236 #> text4 -0.3741 -0.6106 -0.1375 #> text5 -0.3197 -0.5520 -0.0874 #> text6 -0.0379 -0.3474 0.2715 #> text7 0.2343 -0.0316 0.5002 #> text8 -0.0575 -0.3298 0.2149 #> text9 0.0627 -0.2399 0.3653 #> text10 0.9448 0.9194 0.9703 A kapott modellünket a wordfish-hez hasonlóan tudjuk ábrázolni, miután a beszedek_ws_pred objektumból adattáblát csinálunk és a ggplot2-vel elkészítjük a vizualizációt. A dokumentumok_ws két részből áll össze. Először a wordscores modell objektumunkból a frakcióvezetők neveit és pártjaikat emeljük ki (kicsit körülményes a dolog, mert egy komplexebb objektumban tárolja őket a quanteda, de az str() függvény tud segíteni ilyen esetekben). A dokumentumok becsült pontszámait pedig a beszedek_ws_pred objektumból készített data frame hozzácsatolásával adjuk hozzá a már elkészült data frame-hez. Ehhez a dplyr csomag bind_cols függvényét használjuk. Fontos, hogy itt teljesen biztosnak kell lennünk abban, hogy a sorok a két data frame esetében ugyanarra a dokumentumra vonatkoznak. dokumentumok_ws <- data.frame( speaker = beszedek_ws$x@docvars$felszolalo, part = beszedek_ws$x@docvars$part ) dokumentumok_ws <- bind_cols(dokumentumok_ws, beszedek_ws_pred) dokumentumok_ws #> speaker part fit lwr upr #> text1 Vona Gábor (Jobbik) Jobbik -0.4585 -0.6883 -0.2287 #> text2 Dr. Schiffer András (LMP) LMP -0.2664 -0.4912 -0.0416 #> text3 Dr. Szél Bernadett (LMP) LMP -0.9448 -0.9660 -0.9236 #> text4 Tóbiás József (MSZP) MSZP -0.3741 -0.6106 -0.1375 #> text5 Schmuck Erzsébet (LMP) LMP -0.3197 -0.5520 -0.0874 #> text6 Dr. Tóth Bertalan (MSZP) MSZP -0.0379 -0.3474 0.2715 #> text7 Volner János (Jobbik) Jobbik 0.2343 -0.0316 0.5002 #> text8 Kósa Lajos (Fidesz) Fidesz -0.0575 -0.3298 0.2149 #> text9 Harrach Péter (KDNP) KDNP 0.0627 -0.2399 0.3653 #> text10 Dr. Gulyás Gergely (Fidesz) Fidesz 0.9448 0.9194 0.9703 A 9.4-es ábrán a párton belüli bontást illusztráljuk a facet_wrap() segítségével. party_df <- ggplot(dokumentumok_ws, aes(fit, reorder(speaker, fit))) + geom_point() + geom_errorbarh(aes(xmin = lwr, xmax = upr), height = 0) + labs( y = NULL, x = "wordscores" ) + facet_wrap(~part, ncol = 1, scales = "free_y") ggplotly(party_df, height = 1000, tooltip = "fit") Ábra 9.1: A párton belüli wordscores-alapú skála 9.3 Wordfish A wordfish felügyelet nélküli skálázást a quanteda.textmodels csomagban implementált textmodel_wordfish() függvény fogja végezni. A megadott dir = c(1, 2) paraméterrel a két dokumentum relatív \\(\\theta\\) értékét tudjuk rögzíteni, mégpedig úgy hogy \\(\\theta_{dir1} < \\theta_{dir2}\\). Alapbeállításként az algoritmus az első és az utolsó dokumentumot teszi be ide. A lenti példánál mi a pártpozíciók alapján a Jobbikos Vona Gábor és az LMP-s Schiffer András egy-egy beszédét használtuk. A summary() használható az illesztett modellel, és a dokumentumonkénti \\(\\theta\\) koefficienst tudjuk így megnézni. beszedek_wf <- quanteda.textmodels::textmodel_wordfish(beszedek_dfm, dir = c(2, 1)) summary(beszedek_wf) #> #> Call: #> textmodel_wordfish.dfm(x = beszedek_dfm, dir = c(2, 1)) #> #> Estimated Document Positions: #> theta se #> text1 -0.3336 0.0334 #> text2 -1.8334 0.0518 #> text3 -0.9245 0.0356 #> text4 0.0332 0.0390 #> text5 -0.7420 0.0386 #> text6 0.6071 0.0373 #> text7 0.7474 0.0364 #> text8 0.1139 0.0351 #> text9 0.6854 0.0405 #> text10 1.6465 0.0426 #> #> Estimated Feature Scores: #> vona gábor jobbik tisztelt elnök országgyűlés tegnapi napon #> beta -0.961 0.201 0.292 -0.268 -0.0345 -0.837 -1.11 -0.257 #> psi -2.877 -2.106 -0.413 0.263 -0.2174 -0.770 -2.22 -1.682 #> helyen tartottak időközi önkormányzati választásokat érdekelt #> beta -0.882 -0.961 -0.961 -0.961 -0.961 -0.961 #> psi -1.249 -2.877 -2.877 -2.877 -2.877 -2.877 #> recsken ózdon október nyertünk örömmel közlöm ország közvéleményével #> beta -1.11 -1.16 -0.50 -0.961 -0.961 -0.961 -2.205 -0.961 #> psi -2.22 -1.84 -1.41 -2.877 -2.877 -2.877 -0.785 -2.877 #> amúgy tudnak mindkét jobbikos polgármester maradt tisztségében #> beta -0.961 -0.404 -0.961 -0.961 -0.961 -0.961 -0.961 #> psi -2.877 -1.690 -2.877 -2.877 -2.877 -2.877 -2.877 #> nemhogy #> beta -1.89 #> psi -2.54 Amennyiben szeretnénk a szavak szintjén is megnézni a \\(\\beta\\) (a szavakhoz társított súly, ami a relatív fontosságát mutatja) és \\(\\psi\\) (a szó rögzített hatást (word fixed effects), ami az eltérő szófrekvencia kezeléséért felelős) koefficienseket, akkor a beszedek_wf objektumban tárolt értékeket egy data frame-be tudjuk bemásolni. A dokumentumok hosszát és a szófrekvenciát figyelembe véve, a negatív \\(\\beta\\) értékű szavakat gyakrabban használják a negatív \\(\\theta\\) koefficienssel rendelkező politikusok. szavak_wf <- data.frame( word = beszedek_wf$features, beta = beszedek_wf$beta, psi = beszedek_wf$psi ) szavak_wf %>% arrange(beta) %>% head(n = 15) #> word beta psi #> 1 függőség -5.93 -6.41 #> 2 paks -5.44 -6.11 #> 3 árnak -5.44 -6.11 #> 4 biztonsági -5.44 -6.11 #> 5 brüsszeli -5.04 -5.88 #> 6 helyzet -5.04 -5.88 #> 7 alsó -5.04 -5.88 #> 8 hangon -5.04 -5.88 #> 9 áram -5.04 -5.88 #> 10 ára -5.04 -5.88 #> 11 feltételezzük -5.04 -5.88 #> 12 ár -5.04 -5.88 #> 13 extra -5.04 -5.88 #> 14 tervezett -5.04 -5.88 #> 15 veszélybe -5.04 -5.88 Ez a pozitív értékekre is igaz. szavak_wf %>% arrange(desc(beta)) %>% head(n = 15) #> word beta psi #> 1 czeglédy 6.21 -6.21 #> 2 csaba 6.08 -6.14 #> 3 human 5.76 -5.99 #> 4 operator 5.76 -5.99 #> 5 zrt 5.54 -5.89 #> 6 fizette 5.26 -5.76 #> 7 gyanú 5.26 -5.76 #> 8 szocialista 5.26 -5.76 #> 9 elkövetett 4.85 -5.58 #> 10 tárgya 4.85 -5.58 #> 11 céghálózat 4.85 -5.58 #> 12 diákok 4.85 -5.58 #> 13 májusi 4.85 -5.58 #> 14 júniusi 4.85 -5.58 #> 15 büntetőeljárás 4.85 -5.58 Az eredményeinket mind a szavak, mind a dokumentumok szintjén tudjuk vizualizálni. Elsőként a klasszikus „Eiffel-torony” ábrát reprodukáljuk, ami a szavak gyakoriságának és a skálára gyakorolt befolyásának az illusztrálására szolgál. Ehhez a már elkészült szavak_wf data framet-et és a ggplot2 csomagot fogjuk használni. Mivel a korpuszunk nagyon kicsi, ezért csak 2273 kifejezést fogunk ábrázolni. Ennek ellenére a lényeg kirajzolódik a lenti ábrán is.49 Kihasználhatjuk, hogy a ggplot ábra definiálása közben a felhasznált bemeneti data frame-et különböző szempontok alapján lehet szűrni. Így ábrázolni tudjuk a gyakran használt, ám semleges szavakat (magas \\(\\psi\\), alacsony \\(\\beta\\)), illetve a ritkább, de meghatározóbb szavakat (magas \\(\\beta\\), alacsony \\(\\psi\\)). ggplot(szavak_wf, aes(x = beta, y = psi)) + geom_point(color = "grey") + geom_text_repel( data = filter(szavak_wf, beta > 4.5 | beta < -5 | psi > 0), aes(beta, psi, label = word), alpha = 0.7 ) + labs( x = expression(beta), y = expression(psi) ) Ábra 9.2: A wordfish ‘Eiffel-torony’ Az így kapott ábrán az egyes pontok mind egy szót reprezentálnak, láthatjuk, hogy tipikusan minél magasabb a \\(\\psi\\) értékük annál inkább középen helyezkednek el hiszen a leggyakoribb szavak azok, amelyeket mindenki használ politikai spektrumon való elhelyezkedésüktől függetlenül. Az ábra két szélén lévő szavak azok, amelyek specifikusan a skála egy-egy végpontjához kötődnek. Jelen esetben ezek kevésbé beszédések, mivel a korpusz kifejezetten kis méretű és láthatóan további stopszavazás is szükséges. A dokumentumok szintjén is érdemes megvizsgálni az eredményeket. Ehhez a dokumentum szintű paramétereket fogjuk egy data frame-be gyűjteni: a \\(\\theta\\) ideológiai pozíciót, illetve a beszélő nevét. A vizualizáció kedvéért a párttagságot is hozzáadjuk. A data frame összerakása után az alsó és a felső határát is kiszámoljuk a konfidencia intervallumnak és azt is ábrázoljuk (ld. 9.2. ábra). dokumentumok_wf <- data.frame( speaker = beszedek_wf$x@docvars$felszolalo, part = beszedek_wf$x@docvars$part, theta = beszedek_wf$theta, theta_se = beszedek_wf$se.theta ) %>% mutate( lower = theta - 1.96 * theta_se, upper = theta + 1.96 * theta_se ) ggplot(dokumentumok_wf, aes(theta, reorder(speaker, theta))) + geom_point() + geom_errorbarh(aes(xmin = lower, xmax = upper), height = 0) + labs( y = NULL, x = expression(theta) ) Ábra 9.3: A beszédek egymáshoz viszonyított pozíciója A párt metaadattal összehasonlíthatjuk az egy párthoz tartozó frakcióvezetők értékeit a facet_wrap() használatával. Figyeljünk arra, hogy az y tengelyen szabadon változhasson az egyes rész ábrák között, a scales = \"free\" opcióval (ld. 9.3. ábra). speech_df <- ggplot(dokumentumok_wf, aes(theta, reorder(speaker, theta))) + geom_point() + geom_errorbarh(aes(xmin = lower, xmax = upper), height = 0) + labs( y = NULL, x = "wordscores" ) + facet_wrap(~part, ncol = 1, scales = "free_y") ggplotly(speech_df, height = 1000, tooltip = "theta") Ábra 9.4: Párton belüli pozíciók A mintába nem került be Rogán Antal, akinek csak egy darab napirend előtti felszólalása volt.↩︎ Azért nem Vona Gábor beszédét választottuk, mert az gyaníthatóan egy kiugró érték, ami nem reprezentálja megfelelően a sokaságot.↩︎ A quanteda.textplots csomag több megoldást is kínál az ábrák elkészítésére. Mivel ezek a megoldások kifejezetten a quanteda elemzések ábrázolására készültek, ezért rövid egysoros függvényekkel tudunk gyorsan ábrákat készíteni. A hátrányuk, hogy kevésbé tudjuk „személyre szabni” az ábráinkat, mint a ggplot2 példák esetében. A quanteda.textplots megoldásokat ezen a linken demonstrálják a csomag készítői: https://quanteda.io/articles/pkgdown/examples/plotting.html.↩︎ "],["similarity.html", "10 Szövegösszehasonlítás 10.1 A szövegösszehasonlítás különböző megközelítései 10.2 Lexikális hasonlóság 10.3 Szemantikai hasonlóság 10.4 Hasonlóságszámítás 10.5 Szövegtisztítás 10.6 A Jaccard-hasonlóság számítása 10.7 A koszinusz-hasonlóság számítása 10.8 Az eredmények vizualizációja", " 10 Szövegösszehasonlítás 10.1 A szövegösszehasonlítás különböző megközelítései A gépi szövegösszehasonlítás a mindennapi életünk számos területén megjelenő szövegbányászati technika, bár az emberek többsége nincs ennek tudatában. Ezen a módszeren alapulnak a böngészők kereső mechanizmusai, vagy a kérdés-felelet (Q&A) fórumok algoritmusai, melyek ellenőrzik, hogy szerepel-e már a feltenni kívánt kérdés a fórumon (Sieg 2018). Alkalmazzák továbbá a szövegösszehasonlítást a gépi szövegfordításban és az automatikus kérdésmegválaszolási feladatok esetén is (Wang and Dong 2020), de akár automatizált esszéértékelésre vagy plágiumellenőrzésre is hasznosítható az eljárás (Bar, Zesch, and Gurevych 2011). A szövegösszehasonlítás hétköznapi életben előforduló rejtett alkalmazásain túl a társadalomtudományok művelői is számos esetben hasznosítják az eljárást. A politikatudomány területén többek között használhatjuk arra, hogy eldöntsük, mennyire különböznek egymástól a benyújtott törvényjavaslatok és az elfogadott törvények szövegei, ezzel fontos információhoz jutva arról, hogy milyen szerepe van a parlamenti vitának a végleges törvények kialakításában. Egy másik példa a szakpolitikai prioritásokban és alapelvekben végbemenő változások elemzése, melyet például szakpolitikai javaslatok vagy ilyen témájú viták leiratainak elemzésével is megtehetünk. A könyv korábbi fejezeteiben bemutatott eljárások között több olyat találunk, melyek alkalmasak arra, hogy a szövegek hasonlóságából valamilyen információt nyerjünk. Ugyanakkor vannak módszerek, melyek segítségével számszerűsíthetjük a szövegek közötti különbségeket. Ez a fejezet ezekről nyújt rövid áttekintést. Mindenekelőtt azonban azt kell tisztáznunk, hogy miként értelmezzük a hasonlóságot. A hasonlóságelemzéseket jellemzően két nagy kategóriába szoktuk sorolni a mérni kívánt hasonlóság típusa szerint. Ez alapján beszélhetünk lexikális (formai) és szemantikai hasonlóságról. 10.2 Lexikális hasonlóság A lexikális hasonlóság a gépi szövegfeldolgozás egy egyszerűbb megközelítése, amikor nem várjuk el az elemzésünktől, hogy „értse” a szöveget, csupán a formai hasonlóságot figyeljük. A megközelítés előnye, hogy számítási szempontból jelentősen egyszerűbb, mint a szemantikai hasonlóságra irányuló elemzések, hátránya azonban, hogy az egyszerűség könnyen tévútra vihet szofisztikáltabb elemzések esetén. Így például a lexikális hasonlóság szempontjából az alábbi két példamondat azonosnak tekinthető, hiszen formailag (kifejezések szintjén) megegyeznek. 1. „A boszorkány megsüti Jancsit és Juliskát.” 2. „Jancsi és Juliska megsüti a boszorkányt.” Két dokumentum közötti lexikális hasonlóságot a szöveg számos szintjén mérhetjük: karakterláncok (stringek), szóalakok (tokenek), n-gramok (n egységből álló karakterláncok), szózsákok (bag of words) között, de akár a dokumentum nagyobb egységei, így szövegrészletek és dokumentumok között is. Bevett megközelítés továbbá a szókészlet összehasonlítása, melyet lexikális és szemantikai hasonlóság feltárására egyaránt használhatunk. A hasonlóság számítására számos metrika létezik. Ezek jelentős része valamilyen távolságszámításon alapul, mint például a koszinus-távolság. Ez a metrika két szövegvektor (a két dokumentum-kifejezés mátrix) által bezárt szög alapján határozza meg a hasonlóságot (Wang and Dong 2020). Mindezt az alábbi képlet szerint: \\[ cos(X,Y)=\\frac{X \\cdot Y}{\\|X\\| \\|Y\\|} \\] vagyis kiszámoljuk a két vektor skaláris szorzatát, amelyet elosztunk a vektorok Euklidészi normáinak (gyakran hívják L2 normának is, és ennek segítségével kapjuk meg a vektorok hosszát) szorzatával. Vegyük az alábbi két példamondatot a koszinusz távolság számításának szemléltetésére: 1. Jancsi és Juliska megsüti a boszorkányt. 2. A pék megsüti a kenyeret. A két példamondat (vagyis a dokumentumaink) dokumentum-kifejezés mátrixsza az alábbi táblázat szerint fog kinézni. Az X vektor reprezentálja az 1. példamondatot, az Y vektor pedig a második példamondatot. Táblázat 10.1: Dokumentum-kifejezés mátrix két példamondattal Vektor_név jancsi és juliska megsüti a boszokrkányt pék kenyeret X 1 1 1 1 1 1 0 0 Y 0 0 0 1 2 0 1 1 A két mondat közötti távolság értékét a képlet szerint a következő módon számítjuk ki: \\[ \\frac{x_{1}*y_{1}+x_{2}*y_{2}+x_{3}*y_{3}+x_{4}*y_{4}+x_{5}*y_{5}+x_{6}*y_{6}+x_{7}*y_{7}+x_{8}*y_{8}} {\\sqrt{x_{1}^2+x_{2}^2+x_{3}^2+x_{4}^2+x_{5}^2+x_{6}^2+x_{7}^2+x_{8}^2}* \\sqrt{y_{1}^2+y_{2}^2+y_{3}^2+y_{4}^2+y_{5}^2+y_{6}^2+y_{7}^2+y_{8}^2}} \\] A két példamondat koszinusz-távolságának értéke ennek megfelelően 0,463. \\[ \\frac{1*0+1*0+1*0+1*1+1*2+1*0+0*1+0*1} {\\sqrt{1^2+1^2+1^2+1^2+1^2+1^2+0^2+0^2}* \\sqrt{0^2+0^2+0^2+1^2+2^2+0^2+1^2+1^2}} = \\frac{3}{\\sqrt{6}*\\sqrt{7}}\\approx 0,463 \\] A koszinusz-hasonlóság 0 és 1 közötti értékeket vehet fel. 0-ás értéket akkor kapunk, ha a dokumentumok egyáltalán nem hasonlítanak egymásra. Geometriai értelmeben ebben az esetben a két szövegvektor 90 fokos szöget zár be, hiszen cos(90) = 0 (Ladd 2020). Egy másik széles körben alkalmazott dokumentumhasonlósági metrika a Jaccard-hasonlóság, melynek számítása egy egyszerű eljáráson alapul: a két dokumentumban egyező szavak számát elosztja a két dokumentumban szereplő szavak számának uniójával (vagyis a két dokumentumban szereplő szavak számának összegével, melyből kivonja az egyező szavak számának összegét). A Jaccard-hasonlóság tehát azt képes megmutatni, hogy a két dokumentum teljes szószámához képest mekkora az azonos kifejezések aránya (Niwattanakul et al. 2013, 2.o). Ahogy a koszinusz-hasonlóságnál is, itt is 0 és 1 közötti értéket kapunk, ahol a magasabb érték nagyobb hasonlóságra utal. \\[ Jaccard(doc_{1}, doc_{2}) = \\frac{|doc_{1}\\,\\cap \\, doc_{2}|}{|doc1 \\, \\cup \\, doc2|} = \\frac{|doc_{1} \\, \\cap \\, doc_{2}|}{|doc_{1}| + |doc_{2}| - |doc_{1} \\, \\cap \\, doc_{2} |} \\] 10.3 Szemantikai hasonlóság A szemantikai hasonlóság a lexikai hasonlósággal szemben egy komplexebb számítás, melynek során az algoritmus a szavak tartalmát is képes elemezni. Így például formai szempontból hiába nem azonos az alábbi két példamondat, a szemantikai hasonlóságvizsgálatnak észlelnie kell a tartalmi azonosságot. 1. „A diákok jegyzetelnek, amíg a professzor előadást tart.” 2. „A nebulók írnak, amikor az oktató beszél.” A jelentésbeli hasonlóság kimutatására számos megközelítés létezik. Többek között alkalmazható a témamodellezés (topikmodellezés), melyet a Felügyelet nélküli tanulás fejezetben tárgyaltunk bővebben, ezen belül pedig az LDA Látens Dirichlet-Allokáció (Latent Dirichlet Allocation), valamint az LSA látens érzelemelemzés (Latent Sentiment Analysis) is nagyszerű lehetőséget kínál arra, hogy az egyes dokumentumainkat tartalmi hasonlóságok alapján csoportosítsuk. Az LSA-nél és az LDA-nél azonban egy fokkal komplexebb megközelítés a szóbeágyazás, melyet a Szóbeágyazások című fejezetben mutattunk be. Ez a módszertan a témamodellezéshez képest a szöveg mélyebb szemantikai tartalmait is képes feltárni, hiszen a beágyazásnak köszönhetően képes formailag különböző, de jelentésükben azonos kifejezések azonosságát megmutatni. A jelentésbeli hasonlóság megállapítható a beágyazás során létrehozott vektorreprezentációkból (emlékezzünk: a hasonló vektorreprezentáció hasonló szemantikai tartalomra utal). Kimutathatjuk a szemantikai közelséget például a király – férfi – lovag kifejezések között, de olyan mesterségesen létrehozott jelentésbeli azonosságokat is feltárhatunk, mint az irányítószámok és az általuk jelölt városnevek kapcsolata. Abban az esetben, ha a szóbeágyazást kimondottan a szöveghasonlóság megállapítására szeretnénk használni, a WMD (Word Mover’s Distance) metrikát érdemes használni, mely a vektortérben elhelyezkedő szóvektorok közötti távolság által számszerűsíti a szövegek hasonlóságát (Kusner et al. 2015). 10.4 Hasonlóságszámítás 10.4.1 Adatbázis-importálás és előkészítés A fejezet második felében a lexikai hasonlóság vizsgálatára, ezen belül a Jaccard-hasonlóság és a koszinusz-hasonlóság számítására mutatunk be egy-egy példát a törvényjavaslatok és az elfogadott törvények szövegeinek összehasonlításával. Az alábbiakban bemutatott elemzés a (Sebők et al. 2021) megjelenés előtt álló tanulmányból meríti elemzési fókuszát. Az eredeti cikk által megvalósított elemzést a svájci korpusz elemzése nélkül, a magyar korpusz egy részhalmazán replikáljuk az alábbiakban. A kutatási kérdés arra irányul, hogy mennyiben változik meg a törvényjavaslatok szövege a parlamenti vita folyamán, amíg a javaslat elfogadásra kerül. Az elemzés során a különböző kormányzati ciklusok közötti eltérésekre világítunk rá. Az elemzés megkezdése előtt a már ismert módon betöltjük a szükséges csomagokat. library(stringr) library(dplyr) library(tidyr) library(quanteda) library(quanteda.textstats) library(readtext) library(ggplot2) library(plotly) library(HunMineR) Ezt követően betöltjük azokat az adatbázisokat, amelyeken a szövegösszehasonlítást fogjuk végezni: az elfogadott törvények szövegét tartalmazó korpuszt, a törvényjavaslatok szövegét tartalmazó korpuszt, valamint az ezek összekapcsolását segítő adatbázist, melyben az összetartozó törvényjavaslatok és törvények azonosítóját (id-ját) tároltuk el. Ahogy behívjuk a három adattáblát, érdemes rögtön lekérni az oszlopneveket colnames() és a táblázat dimenzióit dim(), hogy lássuk, milyen adatok állnak a rendelkezésünkre, és mekkora táblákkal fogunk dolgozni. A dim() függvény első értéke a sorok száma, a második pedig az oszlopok száma lesz az adott táblázatban. torvenyek <- HunMineR::data_lawtext_sample colnames(torvenyek) #> [1] "tv_id" "torveny_szoveg" "korm_ciklus" "ev" #> [5] "korm_ell" dim(torvenyek) #> [1] 600 5 tv_javaslatok <- HunMineR::data_lawprop_sample colnames(tv_javaslatok) #> [1] "tvjav_id" "tvjav_szoveg" dim(tv_javaslatok) #> [1] 600 2 parok <- HunMineR::data_lawsample_match colnames(parok) #> [1] "tv_id" "tvjav_id" dim(parok) #> [1] 600 2 Az importált adatbázisok megfigyeléseinek száma egységesen 600. Ez a több mint háromezer megfigyelést tartalmazó eredeti korpusz egy részhalmaza, mely gyorsabb és egyszerűbb elemzést tesz lehetővé. Az oszlopnevek lekérésével láthatjuk, hogy a törvénykorpuszban van néhány metaadat, amelyet az elemzés során felhasználhatunk: ezek a kormányzati ciklusra, a törvény elfogadásának évére, valamint a benyújtó kormánypárti vagy ellenzéki pártállására vonatkoznak. Ezenkívül rendelkezésre állnak a törvényeket és a törvényjavaslatokat azonosító kódok (tv_id és tvjav_id), melyek segítségével majd tudjuk párosítani az összetartozó törvényjavaslatok és törvények szövegeit. Ezt a left_join() függvénnyel tesszük meg. Elsőként a törvényeket tartalmazó adatbázishoz kapcsoljuk hozzá a törvény–törvényjavaslat párokat tartalmazó adatbázist a törvények azonosítója (tv_id) alapján. A colnames() függvény használatával ellenőrizhetjük, hogy sikeres volt-e a művelet, és az új táblában szerepelnek-e a kívánt oszlopok. tv_tvjavid_osszekapcs <- left_join(torvenyek, parok) colnames(tv_tvjavid_osszekapcs) #> [1] "tv_id" "torveny_szoveg" "korm_ciklus" "ev" #> [5] "korm_ell" "tvjav_id" dim(tv_tvjavid_osszekapcs) #> [1] 600 6 Második lépésben a törvényjavaslatokat tartalmazó adatbázist rendeljük hozzá az előzőekben már összekapcsolt két adatbázishoz. tv_tvjav_minta <- left_join(tv_tvjavid_osszekapcs, tv_javaslatok) colnames(tv_tvjav_minta) #> [1] "tv_id" "torveny_szoveg" "korm_ciklus" "ev" #> [5] "korm_ell" "tvjav_id" "tvjav_szoveg" dim(tv_tvjav_minta) #> [1] 600 7 Ha jól végeztük a dolgunkat az adatbázisok összekapcsolása során, az eljárás végére 7 oszlopunk és 600 sorunk van, vagyis az újonnan létrehozott adatbázisba bekerült az összes változó (oszlop). A korpuszaink egy adattáblában való kezelése azért hasznos, mert így nem kell párhuzamosan elvégezni az azonos műveleteket a két korpusz, a törvények és a törvényjavaslatok tisztításához, hanem párhuzamosan tudunk dolgozni a kettővel. Kicsit közelebbről megvizsgálva az adatbázist, megtekinthetjük a metaadatainkat, valamit azt is láthatjuk a count funkció segítségével, hogy minden adatbázisunkban szereplő kormányzati ciklusra 100 megfigyelés áll rendelkezésünkre: tv_tvjav_minta %>% count(korm_ciklus). summary(tv_tvjav_minta) #> tv_id torveny_szoveg korm_ciklus ev #> Length:600 Length:600 Length:600 Min. :1994 #> Class :character Class :character Class :character 1st Qu.:2000 #> Mode :character Mode :character Mode :character Median :2006 #> Mean :2006 #> 3rd Qu.:2012 #> Max. :2018 #> korm_ell tvjav_id tvjav_szoveg #> Min. : 0 Length:600 Length:600 #> 1st Qu.:900 Class :character Class :character #> Median :900 Mode :character Mode :character #> Mean :741 #> 3rd Qu.:900 #> Max. :901 tv_tvjav_minta %>% count(korm_ciklus) #> # A tibble: 6 × 2 #> korm_ciklus n #> <chr> <int> #> 1 1994-1998 100 #> 2 1998-2002 100 #> 3 2002-2006 100 #> 4 2006-2010 100 #> 5 2010-2014 100 #> 6 2014-2018 100 Hasonlóan ellenőrizhetjük az egyes évekre eső megfigyelések számát is. tv_tvjav_minta %>% count(ev) #> # A tibble: 24 × 2 #> ev n #> <dbl> <int> #> 1 1994 11 #> 2 1995 31 #> 3 1996 23 #> 4 1997 31 #> 5 1998 17 #> 6 1999 20 #> # ℹ 18 more rows 10.5 Szövegtisztítás Mivel az elemzés során két különböző korpusszal dolgozunk – két oszlopnyi szöveggel –, egyszerűbb, ha a szövegtisztítás lépéseiből létrehozunk egy külön függvényt, amely magában foglalja a művelet egyes lépéseit, és lehetővé teszi, hogy ne kelljen minden szövegtisztítási lépést külön definiálni az egyes korpuszok esetén. A függvény neve jelen esetben szovegtisztitas lesz, és a már ismert lépéseket foglalja magában: kontrollkarakterek szóközzé alakítása, központozás és a számok eltávolítása. Kisbetűsítés, ismétlődő stringek és a stringek előtt található szóközök eltávolítása. Továbbá a str_remove_all() függvénnyel eltávolítjuk azokat az írásjeleket, amelyek előfordulnak a szövegben, de számunkra nem hasznosak. A függvény definiálását az alábbi szintaxissal tehetjük meg. fuggveny <- function(bemenet) { elvegzendo_lepesek return(kimenet) } A bemenet helyen azt jelöljük, hogy milyen objektumon fogjuk végrehajtani a műveleteket, a kimenetet pedig a return() függvénnyel definiáljuk, ez lesz a függvényünk úgynevezett visszatérési értéke, vagyis az elvégzendő lépések szerint átalakított objektum. A szövegtisztító függvény bemeneti és kimeneti értéke is text lesz, mivel ebbe a változóba mentettük az elvégzendő változtatásokat. szovegtisztitas <- function(text) { text = str_replace(text, "[: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) return(text) } Miután létrehoztuk a szövegtisztításra alkalmas függvényünket, az adatbázis két oszlopára fogjuk alkalmazni: a törvények szövegét és a törvényjavaslatok szövegét tartalmazó oszlopra, amiben a mapply() függvény lesz a segítségünkre. A mapply() függvényen belül megadjuk megadjuk az adattáblát és hivatkozunk annak releváns oszlopaira tv_tvjav_minta[ ,c(\"torveny_szoveg\",\"tvjav_szoveg\")]. Az alkalmazni kívánt függvényt a FUN argumentumaként adhatjuk meg – értelemszerűen ez esetünkben az előzőekben létrehozott szovegtisztitas függvény lesz. Végezetül pedig a függvényünk által megtisztított új oszlopokkal felülírjuk az előző adatbázisunk vonatkozó oszlopait, vagyis a torveny_szoveg és a tvjav_szoveg oszlopokat: tv_tvjav_minta[, c(\"torveny_szoveg\",\"tvjav_szoveg\")]. Amennyiben számítunk rá, hogy még változhatnak a szövegeket tartalmazó oszlopok, akkor érdemes előre definiálni a szöveges oszlopok neveit, hogy később csak egy helyen kelljen változtatni a kódon. szovegek <- c("torveny_szoveg", "tvjav_szoveg") tv_tvjav_minta[, szovegek] <- mapply(tv_tvjav_minta[, szovegek], FUN = szovegtisztitas) A szövegtisztítás következő lépése a tiltólistás szavak meghatározása és kiszűrése a szövegből. Itt a quanteda csomagban elérhető magyar nyelvű stopszavakat, valamint a 7. fejezetben meghatározott speciális jogi stopszavak listáját használjuk. legal_stopwords <- HunMineR::data_legal_stopwords A stopszavak beimportálását követően korpusszá alakítjuk a szövegeinket és tokenizáljuk azokat. Ezt már külön-külön végezzük el a törvények és a törvényjavaslatok szövegeire, azonos lépésekben haladva. A létrehozott objektumokat itt is ellenőrizhetjük, például a summary(torvenyek_coprus) paranccsal, vagy a torvenyek_tokens[1:3] paranccsal, mely az első 3 dokumentum tokenjeit fogja megmutatni. torvenyek_corpus <- corpus(tv_tvjav_minta$torveny_szoveg) tv_javaslatok_corpus <- corpus(tv_tvjav_minta$tvjav_szoveg) torvenyek_tokens <- tokens(torvenyek_corpus) %>% tokens_remove(stopwords("hungarian")) %>% tokens_remove(legal_stopwords) %>% tokens_wordstem(language = "hun") tv_javaslatok_tokens <- tokens(tv_javaslatok_corpus) %>% tokens_remove(stopwords("hungarian")) %>% tokens_remove(legal_stopwords) %>% tokens_wordstem(language = "hun") A szövegek tokenizálásával és a tiltólistás szavak eltávolításával a szövegtisztítás végére értünk, így megkezdhetjük az elemzést. 10.6 A Jaccard-hasonlóság számítása A Jaccard-hasonlóság kiszámításához a quanteda.textstats textstat_simil() függvényét fogjuk alkalmazni. Mivel a textstat_simil() függvény dokumentum-kifejezés mátrixot vár bemenetként, elsőként alakítsuk át ennek megfelelően a korpuszainkat. Az előző fejezetekhez hasonlóan itt is a TF-IDF súlyozást választottuk a mátrix létrehozásakor. torvenyek_dfm <- dfm(torvenyek_tokens) %>% dfm_tfidf() tv_javaslatok_dfm <- dfm(tv_javaslatok_tokens) %>% dfm_tfidf() Miután létrehoztuk a dokumentum-kifejezés mátrixokat, érdemes a leggyakoribb tokeneket ellenőrizni a textstat_frequency() függvénnyel, hogy biztosak lehessünk abban, hogy a megfelelő eredményt értük el a szövegtisztítás során. (Amennyiben nem vagyunk elégedettek, érdemes visszatérni a stopszavakhoz és újabb kifejezéseket hozzárendelni a stopszólistához.) tv_toptokens <- textstat_frequency(torvenyek_dfm, n = 10, force = TRUE) tv_toptokens #> feature frequency rank docfreq group #> 1 an 6992 1 131 all #> 2 szerződő 4795 2 150 all #> 3 felhalmozás 3618 3 28 all #> 4 articl 3426 4 74 all #> 5 kiadás 3328 5 224 all #> 6 for 3305 6 100 all #> 7 contracting 3295 7 39 all #> 8 költségvetés 3130 8 206 all #> 9 befektetés 3130 9 73 all #> 10 szanálás 2629 10 13 all tvjav_toptokens <- textstat_frequency(tv_javaslatok_dfm, n = 10, force = TRUE) tvjav_toptokens #> feature frequency rank docfreq group #> 1 an 6051 1 160 all #> 2 szerződő 4533 2 148 all #> 3 articl 3490 3 75 all #> 4 befektetés 3447 4 90 all #> 5 bűncselekmény 3274 5 96 all #> 6 contracting 3266 6 40 all #> 7 szanálás 3235 7 12 all #> 8 bíróság 3226 8 301 all #> 9 egyezmény 3045 9 231 all #> 10 for 3045 10 109 all A létrehozott dokumentum-kifejezés mátrixokon elvégezhetjük a dokumentumhasonlóság-vizsgálatot. A Jaccard-hasonlóság-metrika, illetve a quanteda textstat_simil() függvénye alkalmazható egy korpuszra is. Egy korpuszra végezve az elemzést, a függvény a korpusz dokumentumai közötti hasonlóságot számítja ki, míg két korpuszra mindkét korpusz összes dokumentuma közötti hasonlóságot. Érdemes továbbá azt is megjegyezni, hogy a textstat_simil() method argumentumaként megadható számos más hasonlósági metrika is, melyekkel további érdekes számítások végezhetők. Bővebben a textstat_simil() függény használatáról és argumentumairól a quanteda hivatalos honlapján olvashatunk.50 A textstat_simil() függvény kapcsán azt is érdemes figyelembe venni, hogy mivel nemcsak a dokumentum párokra, hanem az összes bemenetként megadott dokumentumra külön kiszámítja a Jaccard-indexet, a korpusz(ok) méretének növelésével a számítás kapacitás- és időigényessége exponenciálisan növekszik. Két 600 dokumentumból álló korpusz esetén kb. 4–5 perc a számítási idő, míg 360 dokumentum esetén csupán 1–2 perc. jaccard_hasonlosag <- textstat_simil(torvenyek_dfm, tv_javaslatok_dfm, method = "jaccard") Mivel az eredménymátrixunk meglehetősen terjedelmes, nem érdemes az egészet egyben megtekinteni, egyszerűbb az első néhány dokumentum közötti hasonlóságra szűrni, melyet a szögletes zárójelben való indexeléssel tudunk megtenni. Az [1:5, 1:5] kifejezéssel specifikálhatjuk a sorokat és az oszlopkat az elsőtől az ötödikig. jaccard_hasonlosag[1:5, 1:5] #> 5 x 5 Matrix of class "dgeMatrix" #> text1 text2 text3 text4 text5 #> text1 0.5541 0.126 0.1144 0.1250 0.1526 #> text2 0.0983 0.572 0.1278 0.1649 0.0943 #> text3 0.0880 0.124 0.7288 0.1022 0.0764 #> text4 0.1089 0.189 0.1182 0.5720 0.1134 #> text5 0.1504 0.094 0.0881 0.0984 0.6273 A mátrix főátlójában jelennek meg az összetartozó törvényekre és törvényszövegekre vonatkozó értékek, minden más érték nem összetartozó törvény- és törvényjavaslat-szövegek hasonlóságára vonatkozik, vagyis a vizsgálatunk szempontjából irreleváns. A mátrix főátlóját a diag() függvény segítségével nyerhetjük ki. Ha jól dolgoztunk, a létrehozott jaccard_diag első öt eleme (jaccard_diag[1:5]) megegyezik a fent megjelenített 5 × 5-ös mátrix főátlójában elhelyezkedő értékekkel, hossza pedig (length()) a mátrix bármelyik dimenziójával. jaccard_diag <- diag(as.matrix(jaccard_hasonlosag)) jaccard_diag[1:5] #> text1 text2 text3 text4 text5 #> 0.554 0.572 0.729 0.572 0.627 Miután sikerült kinyerni az egyes törvény–törvényjavaslat párokra vonatkozó Jaccard-értéket, érdemes a számításainkat hozzárendelni az eredeti adattáblánkhoz, hogy a meglévő metaadatok fényében tudjuk kiértékelni az egyes dokumentumok közötti hasonlóságot. A hozzárendeléshez egyszerűen definiálunk egy új oszlopot a meglévő adatbázisban tv_tvjav_minta$jaccard_index, melyhez hozzárendeljük a kinyert értékeket. tv_tvjav_minta$jaccard_index <- jaccard_diag Érdemes megnézni a végeredményt, ellenőrizni a Jaccard-hasonlóság legmagasabb vagy legalacsonyabb értékeit. A top_n() függvény használatával ki tudjuk válogatni a legmagasabb és a legalacsonyabb értékeket. A top_n() függvény első argumentuma a változó lesz, ami alapján a legalacsonyabb és a legmagasabb értékeket keressük, a második argumentum pedig azt specifikálja, hogy a legmagasabb és a legalacsonyabb értékek közül hányat szeretnénk látni. Az n=5 értékkel a legmagasabb, az n=-5 értékkel a legalacsonyabb 5 Jaccard-indexszel rendelkező sort tudjuk kiszűrni. Emellett érdemes arra is odafigyelni, hogy a szövegeket tartalmazó oszlopainkat ne próbáljuk meg kiíratni, hiszen ez jelentősen lelassítja az RStudio működését és csökkenti a kiírt eredmények áttekinthetőségét. tvjav_oszlopok <- c( "tv_id", "korm_ciklus", "tvjav_id", "jaccard_index" ) tv_tvjav_minta[, tvjav_oszlopok] %>% top_n(jaccard_index, n = 5) #> # A tibble: 5 × 4 #> tv_id korm_ciklus tvjav_id jaccard_index #> <chr> <chr> <chr> <dbl> #> 1 1994XCV 1994-1998 1994-1998_T0276 0.991 #> 2 1995LXXXI 1994-1998 1994-1998_T1296 0.987 #> 3 1999XXXV 1998-2002 1998-2002_T0807 0.985 #> 4 2013XLII 2010-2014 2010-2014_T10219 0.981 #> 5 2014VIII 2010-2014 2010-2014_T13631 0.980 tv_tvjav_minta[, tvjav_oszlopok] %>% top_n(jaccard_index, n = -5) #> # A tibble: 5 × 4 #> tv_id korm_ciklus tvjav_id jaccard_index #> <chr> <chr> <chr> <dbl> #> 1 1998I 1994-1998 1994-1998_T4328 0.0513 #> 2 2005LII 2002-2006 2002-2006_T16291 0.0532 #> 3 2007CLXVIII 2006-2010 2006-2010_T04678 0.0270 #> 4 2010CLV 2010-2014 2010-2014_T01809 0.0273 #> 5 2012CXCV 2010-2014 2010-2014_T09103 0.00895 10.7 A koszinusz-hasonlóság számítása A Jaccard-hasonlóság számítása után a koszinusz-távolság számítása már nem jelent nagy kihívást, hiszen a textat_simil() függvénnyel ezt is kiszámíthatjuk, csupán a metrika paramétereként (method =) megadhatjuk a koszinuszt is. Ahogy az előbbiekben, itt is a dokumentum-kifejezés mátrixokat adjuk meg bemeneti értékként. koszinusz_hasonlosag <- textstat_simil(x = torvenyek_dfm, y = tv_javaslatok_dfm, method = "cosine") Érdemes itt is megtekinteni a mátrix első néhány sorába és oszlopába eső értékeket. koszinusz_hasonlosag[0:5, 0:5] #> 5 x 5 Matrix of class "dgeMatrix" #> text1 text2 text3 text4 text5 #> text1 0.6238 0.01731 0.01716 0.01699 0.05780 #> text2 0.0118 0.92878 0.00877 0.04633 0.00753 #> text3 0.0155 0.01226 0.98497 0.00662 0.00203 #> text4 0.0202 0.05076 0.00612 0.96063 0.01878 #> text5 0.0763 0.00743 0.00318 0.01804 0.75334 Ebben az esetben is csak a mátrix átlójára van szükségünk, melyet a fent ismertetett módon nyerünk ki a mátrixból. koszinusz_diag <- diag(as.matrix(koszinusz_hasonlosag)) koszinusz_diag[1:5] #> text1 text2 text3 text4 text5 #> 0.624 0.929 0.985 0.961 0.753 Végezetül pedig az átlóból kinyert koszinusz értékeket is hozzárendeljük az adatbázisunkhoz. tv_tvjav_minta$koszinusz <- koszinusz_diag A kibővített adatbázis oszlopneveit a colnames() függvénnyel ellenőrizhetjük, hogy lássuk, valóban sikerült-e hozzárendelnünk a koszinusz értékeket a táblához. colnames(tv_tvjav_minta) #> [1] "tv_id" "torveny_szoveg" "korm_ciklus" "ev" #> [5] "korm_ell" "tvjav_id" "tvjav_szoveg" "jaccard_index" #> [9] "koszinusz" 10.8 Az eredmények vizualizációja A hasonlósági metrikák vizulizációjára gyakran alkalmazott megoldás a hőtérkép (heatmap), mellyel korrelációs mátrixokat ábrázolhatunk. Ebben az esetben a mátrix értékeit egy színskálán vizualizáljuk, ahol a világosabb színek a magasabb, a sötétebb színek az alacsonyabb értékeket jelölik. A Jaccard-hasonlóság számításakor és a koszinusz-hasonlóság számításakor kapott mátrixok esetén is ábrázolhatjuk az értékeinket ilyen módon. Mivel azonban mindkét mátrix 600 × 600-as, nem érdemes a teljes mátrixot megjeleníteni, mert ilyen nagy mennyiségű adatnál már értelmezhetetlenné válik az ábra, így csak az utolsó 100 elemet, vagyis a 2014–2018-as időszakra vonatkozó értékeket jelenítjük meg. Ezt a koszinusz_hasonlóság nevű objektumunk feldarabolásával tesszük meg, szögletes zárójelben jelölve, hogy a mátrix mely sorait és mely oszlopait szeretnénk használni: koszinusz_hasonlosag[501:600, 501:600]. A koszinusz_hasonlosag objektumból egy data frame-et készítünk, ahol a dokumentumok közötti hasonlóság szerepel. A mátrix formátumból a tidyr csomag pivot_longer() függvényét használva tudjuk a kívánt formátumot elérni. koszinusz_df <- as.matrix(koszinusz_hasonlosag[501:600, 501:600]) %>% as.data.frame() %>% rownames_to_column("docs1") %>% pivot_longer( "text501":"text600", names_to = "docs2", values_to = "similarity" ) glimpse(koszinusz_df) #> Rows: 10,000 #> Columns: 3 #> $ docs1 <chr> "text501", "text501", "text501", "text501", "text501"… #> $ docs2 <chr> "text501", "text502", "text503", "text504", "text505"… #> $ similarity <dbl> 0.95618, 0.02592, 0.04451, 0.03346, 0.01169, 0.04536,… Ezt követően pedig a ggplot függvényt használva a geom_tile segítségével tudjuk elkészíteni a hőtérképet, ami a hasonlósági mátrixot ábrázolja (ld. 10.1. ábra). koszinusz_plot <- ggplot(koszinusz_df, aes(docs1, docs2, fill = similarity)) + geom_tile() + scale_fill_gradient(high = "#2c3e50", low = "#bdc3c7") + labs( x = NULL, y = NULL, fill = "Koszinusz-hasonlóság" ) + theme( axis.text.x=element_blank(), axis.ticks.x=element_blank(), axis.text.y=element_blank(), axis.ticks.y=element_blank() ) ggplotly(koszinusz_plot, tooltip = "similarity") Ábra 10.1: Koszinusz-hasonlóság hőtérképen ábrázolva A Jaccard-hasonlósági hőtérképet ugyanezzel a módszerrel tudjuk elkészíteni (ld. 10.2. ábra). # adatok átalakítása jaccard_df <- as.matrix(jaccard_hasonlosag[501:600, 501:600]) %>% as.data.frame() %>% rownames_to_column("docs1") %>% pivot_longer( "text501":"text600", names_to = "docs2", values_to = "similarity" ) # a ggplot ábra jaccard_plot <-ggplot(jaccard_df, aes(docs1, docs2, fill = similarity)) + geom_tile() + scale_fill_gradient(high = "#2c3e50", low = "#bdc3c7") + labs( x = NULL, y = NULL, fill = "Jaccard hasonlóság" ) + theme( axis.text.x=element_blank(), axis.ticks.x=element_blank(), axis.text.y=element_blank(), axis.ticks.y=element_blank() ) ggplotly(jaccard_plot, tooltip = "similarity") Ábra 10.2: Jaccard hasonlóság hőtérképen ábrázolva A két ábra összehasonlításánál láthatjuk, hogy a koszinusz-hasonlóság általában magasabb hasonlósági értékeket mutat. A mátrix főátlójában kiugró világos csík azt mutatja meg, hogy a legnagyobb hasonlóság az összetartozó törvény- és törvényjavaslat-szövegek között mutatkozik meg, eddig tehát az adataink az elvárásaink szerinti képet mutatják. Amennyiben a világos csíkot nem látnánk, az egyértelmű visszajelzés volna arról, hogy elrontottunk valamit a szövegelőkészítés eddigi lépései során, vagy a várakozásásaink voltak teljesen rosszak. A koszinusz- és a Jaccard-hasonlóság értékét ábrázolhatjuk közös pont-diagramon a geom_jitter() segítségével (ld. 10.3. ábra). Ehhez először egy kicsit átalakítjuk a data frame-et a tidyr csomag pivot_longer() függvényével, hogy a két hasonlósági érték egy oszlopban legyen. Ez azért szükséges, hogy a ggplot ábránkat könnyebben tudjuk létrehozni. tv_tvjav_tidy <- tv_tvjav_minta %>% pivot_longer( "jaccard_index":"koszinusz", values_to = "hasonlosag", names_to = "hasonlosag_tipus" ) glimpse(tv_tvjav_tidy) #> Rows: 1,200 #> Columns: 9 #> $ tv_id <chr> "1994LXXXII", "1994LXXXII", "1996CXXXI", "1996C… #> $ torveny_szoveg <chr> "évi lxxxii törvény a magánszemélyek jövedelema… #> $ korm_ciklus <chr> "1994-1998", "1994-1998", "1994-1998", "1994-19… #> $ ev <dbl> 1994, 1994, 1996, 1996, 1995, 1995, 1995, 1995,… #> $ korm_ell <dbl> 900, 900, 900, 900, 900, 900, 900, 900, 900, 90… #> $ tvjav_id <chr> "1994-1998_T0233", "1994-1998_T0233", "1994-199… #> $ tvjav_szoveg <chr> "magyar köztársaság kormánya t számú törvényjav… #> $ hasonlosag_tipus <chr> "jaccard_index", "koszinusz", "jaccard_index", … #> $ hasonlosag <dbl> 0.554, 0.624, 0.572, 0.929, 0.729, 0.985, 0.572… jaccard_koszinusz_plot <- ggplot(tv_tvjav_tidy, aes(ev, hasonlosag)) + geom_jitter(aes(shape = hasonlosag_tipus,color = hasonlosag_tipus), width = 0.1, alpha = 0.45) + scale_x_continuous(breaks = seq(1994, 2018, by = 2)) + labs( y = "Jaccard és koszinusz-hasonlóság", shape = NULL, color = NULL, x = NULL ) + theme(legend.position = "bottom", panel.grid.major.x = element_blank(), panel.grid.minor.x = element_blank() ) ggplotly(jaccard_koszinusz_plot) Ábra 10.3: Koszinusz- és Jaccard hasonlóság értékeinek alakulása A pont-diagramm ebben a formájában keveset árul el a számunkra, láthatjuk, hogy a koszinusz értékek általában magasabbak, de azt nem tudjuk meg mondani az egyes pontok alapján, hogy évről évre hogyan alakultak a jaccard és koszinusz értékek. Ezért a hasonlósági értékek évenkénti alakulásának megértése érdekében érdemes átlagot számolni a mutatókra. Ezt a group_by() és a summarize() függvények együttes alkalmazásával tehetjük meg. Megadjuk, hogy évenkénti bontásban szeretnénk a számításainkat elvégezni group_by(ev), és azt, hogy átlagszámítást szeretnénk végezni mean(). evenkenti_atlag <- tv_tvjav_tidy %>% group_by(ev, hasonlosag_tipus) %>% summarize(atl_hasonlosag = mean(hasonlosag)) head(evenkenti_atlag) #> # A tibble: 6 × 3 #> # Groups: ev [3] #> ev hasonlosag_tipus atl_hasonlosag #> <dbl> <chr> <dbl> #> 1 1994 jaccard_index 0.556 #> 2 1994 koszinusz 0.758 #> 3 1995 jaccard_index 0.499 #> 4 1995 koszinusz 0.777 #> 5 1996 jaccard_index 0.675 #> 6 1996 koszinusz 0.887 Az évenkénti átlagot tartalmazó adattáblánkra ezt követően vonal diagramot illesztünk. A 10.4.-es ábrán az látható, hogy a Jaccard- és koszinusz-hasonlóság értéke nagyjából együtt mozog, leszámítva a 2002-es, 2003-as éveket, mivel azonban itt csak néhány adatpont áll rendelkezésre, ettől az inkonzisztenciától eltekinthetünk. jac_cos_ave_df <- ggplot(evenkenti_atlag, aes(ev, atl_hasonlosag)) + geom_line(aes(linetype = hasonlosag_tipus)) + labs(y = "Átlagos Jaccard- és koszinusz-hasonlóság", linetype = NULL, x = NULL) + theme( legend.position = "bottom", panel.grid.major.x = element_blank(), panel.grid.minor.x = element_blank() ) ggplotly(jac_cos_ave_df) Ábra 10.4: Koszinusz- és Jaccard hasonlóság vonal diagramon ábrázolva Ahhoz, hogy valamivel pontosabb képet kapjunk a Jaccard-index értékének alakulásáról, az eredményeinket érdemes vizualizálni a ggplot2 segítségével. Elsőként évenkénti bontásban, boxplotokkal ábrázoljuk a Jaccard-index értékének alakulását (ld. 10.5. ábra). Így már nem csak azt láthatjuk, hogy évente mennyi volt az átlagos jaccard érték, hanem azt is, hogy az átlagtól mekkora eltérések voltak jelen, ez utóbbit természetesen azt is befolyásolja, hogy hány törvényt fogadtak el az adott évben. jac_cos_box_df <- ggplot(tv_tvjav_minta, aes(as.factor(ev), jaccard_index)) + geom_boxplot() + labs( x = NULL, y = "Jaccard-hasonlóság" ) + theme( axis.text.x = element_text(angle = 45), legend.position = "none" ) ggplotly(jac_cos_box_df) Ábra 10.5: Jaccard hasonlóság boxplotokkal ábrázolva A legalacsonyabb Jaccard-hasonlóság talán az 1994–1998-as időszakra jellemző, míg a 2014–2018-as időszakra szembetűnően magas Jaccard-értékeket látunk az első ábrázolt ciklushoz képest. Összességében nehéz trendet látni az ábrán, de érdemes azt is megjegyezni, hogy a negatív irányba kiugró adatpontok a 2014–2018-as ciklusban jelentősen nagyobb arányban tűnnek fel, mint a korábbi kormányzati ciklusok alatt. Végezetül pedig ábrázolhatjuk a Jaccard-hasonlóságot a benyújtó személye alapján is a korm_ell változónk alapján. A változók értékei a következők a CAP kódkönyve alapján: 0 - Ellenzéki benyújtó 1 - Kormánypárti benyújtó 2 - Kormánypárti és ellenzéki benyújtó közösen 3 - Benyújtók legalább két ellenzéki pártból 4 - Benyújtók legalább két kormánypártból 900 - Nem releváns – a benyújtó a kabinet tagja 901 - Nem releváns – a benyújtó a bizottság tagja 902 - Nem releváns – a benyújtó sem a parlamentnek, sem a kabinetnek, sem a bizottságnak nem tagja Mivel nincs túl sok adatpontunk, és ezek többsége a 900-as adatpont alá esik (lásd tv_tvjav_minta %>% count(korm_ell)), érdemes összevonni a 0-ás és a 3-as változót, valamint az 1-es és a 4-es változót egy-egy értékbe, hogy jobban elemezhetőek legyenek az eredményeink. Ehhez a korm_ell változó értékei alapján definiálunk egy új korm_ell2 változót. Az új változó definiálását és az értékadásokat a dplyr case_when() függvényével fogjuk megtenni. A függvényen belül a bal oldalra kerül, hogy milyen értékek alapján szeretnénk az új értéket meghatározni, a tilde (~) után pedig az, hogy mi legyen az újonnan létrehozott oszlop értéke. Tehát a case_when()-en belül lévő első sor azt fejezi ki, hogy amennyiben a korm_ell egyenlő 0-val, vagy (|) a korm_ell egyenlő 3-mal, legyen a korm_ell2 értéke 0. tv_tvjav_minta <- tv_tvjav_minta %>% mutate( korm_ell2 = case_when( korm_ell == 0 | korm_ell == 3 ~ "Ellenzéki képviselő", korm_ell == 1 | korm_ell == 4 ~ "Kormánypárti képviselő", korm_ell == 2 ~ "Kormánypárti és ellenzéki képviselők közösen", korm_ell == 900 ~ "Kabinet tagja", korm_ell == 901 ~ "Parlamenti bizottság", korm_ell == 902 ~ "Egyik sem" ), korm_ell2 = as.factor(korm_ell2) ) Miután létrehoztuk az új oszlopot, létrehozhatjuk a vizualizációt is annak alapján. Itt egy speciális pontdiagramot fogunk használni: geom_jitter(). Ez annyiban különbözik a pontdiagramtól, hogy kicsit szórtabban ábrázolja a diszkrét értékekre (évekre) eső pontokat, hogy az egy helyen sűrűsödő értékek ne takarják ki egymást. A facet_wrap segítségével tudjuk kategóriánként ábrázolni az évenkénti hasonlóságot (ld. 10.6. ábra). jaccard_be_plot <- ggplot(tv_tvjav_minta, aes(ev, jaccard_index)) + geom_jitter(width = 0.1, alpha = 0.5) + scale_x_continuous(breaks = seq(1994, 2018, by = 2)) + facet_wrap(~ korm_ell2, ncol = 1) + theme(panel.spacing = unit(2, "lines"))+ labs( y = NULL, x = NULL) ggplotly(jaccard_be_plot, height = 1000) Ábra 10.6: Jaccard hasonlóság a benyújtó személye alapján Mivel a törvényjavaslatok túlnyomó többségét a kabinet tagjai nyújtják be, nem igazán tudunk érdemi következtetéseket levonni arra vonatkozóan, hogy az ellenzéki vagy a kormánypárti képviselők által benyújtott javaslatok módosulnak-e többet a vita folyamán. Amennyiben ezzel a kérdéssel alaposabban is szeretnénk foglalkozni, érdemes csak azokat a sorokat kiválasztani a hasonlóságszámításhoz, amelyekben a számunkra releváns megfigyelések szerepelnek. Ha azonban ezt az eljárást választjuk, mindenképpen fontos odafigyelni arra is, hogy az elemzésben használandó megfigyelések kiválogatása nehogy szelektív legyen valamely nem megfigyelt változó szempontjából, ezzel befolyásolva a kutatás eredményeit. https://quanteda.io/reference/textstat_simil.html↩︎ "],["nlp_ch.html", "11 NLP és névelemfelismerés 11.1 Fogalmi alapok 11.2 A spacyr használata", " 11 NLP és névelemfelismerés 11.1 Fogalmi alapok A természetes-nyelv feldolgozása (Natural Language Processing – NLP) a nyelvészet és a mesterséges intelligencia közös területe, amely a számítógépes módszerek segítségével elemzi az emberek által használt (természetes) nyelveket. Azaz képes feldolgozni különböző szöveges dokumentumok tartalmát, kinyerni a bennük található információkat, kategorizálni és rendszerezni azokat. A névelem-felismerés többféle módon is megoldható, így például felügyelt tanulással, szótár alapú módszerekkel vagy kollokációk elemzésével. A névelem-felismerés körében két alapvető módszer alkalmazására van lehetőség. A szabályalapú módszer alkalmazása során előre megadott adatok alapján kerül kinyerésre az információ (ilyen szabály például a mondatközi nagybetű mint a tulajdonnév kezdete). A másik módszer a statisztikai tanulás, amikor a gép alkot szabályokat a kutató előzetes mintakódolása alapján. A névelemfelismerés során nehézséget okozhat a különböző névelemosztályok közötti gyakori átfedés, így például ha egy adott szó településnév és vezetéknév is lehet. Fontos különbséget tenni a névelem-felismerés és a tulajdonnév-felismerés között. A névelem-felismerésbe beletartozik minden olyan kifejezés, amely a világ valamely entitására egyedi módon (unikálisan) referál. Ezzel szemben a tulajdonnév-felismerés, kizárólag a tulajdonnevekre koncentrál.(Üveges 2019; Vincze 2019) A fejezetben részletesen foglalkozunk a lemmatizálással, ami a magyar nyelvű szövegek szövegelőkészítésnek fontos eleme (erről lásd bővebben a Korpuszépítés és szövegelőkészítés fejezetet), és a névelem-felismeréssel (Named Entity Recognition – NER). Névelemnek azokat a tokensorozatokat nevezzük, amelyek valamely entitást egyedi módon jelölnek. A névelem-felismerés az infomációkinyerés részterülete, melynek lényege, hogy automatikusan felismerjük a strukturálátlan szövegben szereplő tulajdonneveket, majd azokat kigyűjtsük, és típusonként (például személynév, földrajzi név, márkanév, stb.) csoportosítsuk. Bár a tulajdonnevek mellett névelemnek tekinthetők még például a telefonszámok vagy az e-mail címek is, a névelem-felismerés leginkább mégis a tulajdonnevek felismerésére irányul. A névelem-felismerés a számítógépes nyelvészetben a korai 1990-es évektől kezdve fontos feladatnak és megoldandó problémának számít. A magyar nyelvű szövegekben a tulajdonnevek automatikus felismerésére jelen kötetben a huSpaCy 51 elemző használatát mutatjuk be, amely képes mondatok teljes nyelvi elemzésére (szótő, szófajok, stb.) illetve névelemek (például személynevek, helységek) azonosítására is folyó szövegben, emellett alkalmas a magyar nyelvű szövegek lemmatizálására, ami amint azt korábban bemutattuk, a magyar nyelvű szövegek előfeldolgozásának fontos lépése. Bár magyar nyelven más elemzők 52 is képesek a nyers szövegek mondatra és szavakra bontására és szófaji elemzésére, azaz POS-taggelésére (Part of Speech-tagging) továbbá a mondatok függőségi elemzésére, két okból döntöttünk a huSpaCy használata mellett. Egyrészt ez a illeszkedik a nemzetközi akadémia és ipari szférában legszélesebb körben használt SpaCy 53 keretrendszerbe, másrészt a reticulate 54 csomag segítségével viszonylag egyszerűen használható R környezetben is, és nagyon jól együttműködik a kötetben rendszeresen használt quanteda csomaggal. 11.2 A spacyr használata library(reticulate) library(spacyr) library(dplyr) library(stringr) library(quanteda) library(quanteda.textplots) library(HunMineR) A spaCy használatához Python környezet szükséges, az első használat előtt telepíteni kell a számítógépünkre egy Anaconda alkalmazást: https://www.anaconda.com/. Majd az RStudio/Tools/Global Options menüjében be kell állítanunk a Pyton interpretert, azaz meg kell adnunk, hogy a gépünkön hol található a feltelepített Anaconda. Ezt csak az első használat előtt kell megtennünk, a későbbiekben innen folytathatjuk a modell betöltését. Ezt követően a már megszokott módon installálnunk kell a reticulate és a spacyr 55 csomagot és telepítenünk a magyar nyelvi modellt. A Pythonban készült spacy-t a spacyr::spacy_install() paranccsal kell telepíteni. A következő lépésben létre kell hoznunk egy conda környezetet, és a huggingface-ről be kell töltenünk a magyar modellt. conda_install(envname = "spacyr", "https://huggingface.co/huspacy/hu_core_news_lg/resolve/v3.5.2/hu_core_news_lg-any-py3-none-any.whl" , pip = TRUE) spacy_initialize(model = "hu_core_news_lg", condaenv="spacyr") #> NULL 11.2.1 Lemmatizálás, tokenizálás, szófaji egyértelműsítés Ezután a spacy_parse() függvény segítségével lehetőségünk van a szövegek tokenizálására, szótári alakra hozására (lemmatizálására) és szófaji egyértelműsítésére. txt <- c(d1 = "Budapesten süt a nap.", d2 = "Tájékoztatom önöket, hogy az ülés vezetésében Hegedűs Lorántné és Szűcs Lajos jegyzők lesznek segítségemre.") parsedtxt <- spacy_parse(txt) print(parsedtxt) #> doc_id sentence_id token_id token lemma pos entity #> 1 d1 1 1 Budapesten Budapest PROPN LOC_B #> 2 d1 1 2 süt süt VERB #> 3 d1 1 3 a a DET #> 4 d1 1 4 nap nap NOUN #> 5 d1 1 5 . . PUNCT #> 6 d2 1 1 Tájékoztatom tájékoztat VERB #> 7 d2 1 2 önöket ön NOUN #> 8 d2 1 3 , , PUNCT #> 9 d2 1 4 hogy hogy SCONJ #> 10 d2 1 5 az az DET #> 11 d2 1 6 ülés ülés NOUN #> 12 d2 1 7 vezetésében vezetés NOUN #> 13 d2 1 8 Hegedűs Hegedűs PROPN PER_B #> 14 d2 1 9 Lorántné Lorántné PROPN PER_I #> 15 d2 1 10 és és CCONJ #> 16 d2 1 11 Szűcs Szűcs PROPN PER_B #> 17 d2 1 12 Lajos Lajos PROPN PER_I #> 18 d2 1 13 jegyzők jegyző NOUN #> 19 d2 1 14 lesznek lesz AUX #> 20 d2 1 15 segítségemre segítség NOUN #> 21 d2 1 16 . . PUNCT Láthatjuk, hogy az eredmény egy olyan tábla, amely soronként tartalmazza a lemmákat. Mivel az elemzések során legtöbbször arra van szükségünk, hogy egy teljes szöveg lemmáit egy egységként kezeljük, a kapott táblán el kell végeznünk néhány átlakítást. Mivel nekünk a lemmákra van szükségünk, először is töröljük az összes oszlopot a doc_id és a lemma kivételével. parsedtxt$sentence_id <- NULL parsedtxt$token_id <- NULL parsedtxt$token <- NULL parsedtxt$pos <- NULL parsedtxt$entity <- NULL parsedtxt #> doc_id lemma #> 1 d1 Budapest #> 2 d1 süt #> 3 d1 a #> 4 d1 nap #> 5 d1 . #> 6 d2 tájékoztat #> 7 d2 ön #> 8 d2 , #> 9 d2 hogy #> 10 d2 az #> 11 d2 ülés #> 12 d2 vezetés #> 13 d2 Hegedűs #> 14 d2 Lorántné #> 15 d2 és #> 16 d2 Szűcs #> 17 d2 Lajos #> 18 d2 jegyző #> 19 d2 lesz #> 20 d2 segítség #> 21 d2 . Majd a doc_id segítségével összakapcsoljuk azokat a lemmákat, amelyek egy dokumentumhoz tartoznak és az egyes lemmákat ; segítsével elválasztjuk elmástól. parsedtxt_2<- parsedtxt %>% group_by(doc_id) %>% mutate(text = str_c(lemma, collapse = ";")) parsedtxt_2 #> # A tibble: 21 × 3 #> # Groups: doc_id [2] #> doc_id lemma text #> <chr> <chr> <chr> #> 1 d1 Budapest Budapest;süt;a;nap;. #> 2 d1 süt Budapest;süt;a;nap;. #> 3 d1 a Budapest;süt;a;nap;. #> 4 d1 nap Budapest;süt;a;nap;. #> 5 d1 . Budapest;süt;a;nap;. #> 6 d2 tájékoztat tájékoztat;ön;,;hogy;az;ülés;vezetés;Hegedűs;Lorántné… #> # ℹ 15 more rows Mivel az eredeti táblában minden lemma az eredeti az azt tartalmazó dokumentum id-jét kapta meg, az így létrehozott táblánkban a szövegek annyiszor ismétlődnek, ahány lemmából álltak. Ezért egy következő lépésben ki kell törtölnünk a feleslegesen ismétlődő sorokat. Ehhez először töröljük a lemma oszlopot, hogy a sorok tökéletesen egyezzenek. parsedtxt_2$lemma <- NULL parsedtxt_2 #> # A tibble: 21 × 2 #> # Groups: doc_id [2] #> doc_id text #> <chr> <chr> #> 1 d1 Budapest;süt;a;nap;. #> 2 d1 Budapest;süt;a;nap;. #> 3 d1 Budapest;süt;a;nap;. #> 4 d1 Budapest;süt;a;nap;. #> 5 d1 Budapest;süt;a;nap;. #> 6 d2 tájékoztat;ön;,;hogy;az;ülés;vezetés;Hegedűs;Lorántné;és;Szűcs;L… #> # ℹ 15 more rows Majd a következő lépésben a dplyr csomag distinct függvénye segítségével - amely mindig csak egy-egy egyedi sort tart meg az adattáblában - kitöröljük a felesleges sorokat. parsedtxt_3 <-distinct(parsedtxt_2) parsedtxt_3 #> # A tibble: 2 × 2 #> # Groups: doc_id [2] #> doc_id text #> <chr> <chr> #> 1 d1 Budapest;süt;a;nap;. #> 2 d2 tájékoztat;ön;,;hogy;az;ülés;vezetés;Hegedűs;Lorántné;és;Szűcs;L… Az így létrejött adattáblában a text mezőben már nem az eredeti szöveg, hanem annak lemmái szerepelnek. Ha az adattáblát elmentjük, a lemmákon végezhetjük tovább az elemzéseket. 11.2.2 Saját .txt vagy .csv fájlokban elmentett szövegek lemmatizálása Saját .txt vagy .csv fájlok lemmatizálásához a fájlokat a kötetben bemutatott módon olvassuk be egy adattáblába. (ehhez lásd a Függelék, Munka saját adatokkal alfejezetét) Az alábbi példában egy, a HunMineR csomagban lévő kisebb korpuszon mutatjuk be az ilyen fájlok lemmatizálását. Fontos kiemelni, hogy a nagyobb fájlok feldolgozása elég sok időt (akár több órát) is igénybe vehet. df <- HunMineR::data_parlspeakers_small A beolvasott szövegeket először quanteda korpusszá alakítjuk. Majd a spacy_parsed() függvény segítségével a fentebb bemutatottak szerint elvégezzük a lemmatizálást. df_corpus <- corpus(df) parsed_df <- spacy_parse(df_corpus) head(parsed_df, 5) #> doc_id sentence_id token_id token lemma pos entity #> 1 text1 1 1 VONA VONA PROPN #> 2 text1 1 2 GÁBOR GÁBOR PROPN #> 3 text1 1 3 ( ( PUNCT #> 4 text1 1 4 Jobbik Jobbik PROPN PER_B #> 5 text1 1 5 ): ): PUNCT parsed_df$sentence_id <- NULL parsed_df$token_id <- NULL parsed_df$token <- NULL parsed_df$pos <- NULL parsed_df$entity <- NULL parsed_df_2<- parsed_df %>% group_by(doc_id) %>% mutate( text = str_c(lemma, collapse = " ")) parsed_df_2$lemma <- NULL parsed_df_3 <-distinct(parsed_df_2) head(parsed_df_3, 5) #> # A tibble: 5 × 2 #> # Groups: doc_id [5] #> doc_id text #> <chr> <chr> #> 1 text1 "VONA GÁBOR ( Jobbik ): Tisztelt Elnök Úr ! tisztelt Országgyűlé… #> 2 text2 "dr. SCHIFFER ANDRÁS ( LMP ): Köszön a szó , elnök úr . tisztelt… #> 3 text3 "dr. SZÉL BERNADETT ( LMP ): Köszön a szó , elnök úr . tisztelt … #> 4 text4 "TÓBIÁS JÓZSEF ( MSZP ): Köszön a szó , elnök úr . tisztelt Ház … #> 5 text5 "SCHMUCK ERZSÉBET ( LMP ): Köszön a szó , elnök úr . tisztelt Or… A nagyobb fájlok lemmatizálásának eredményét célszerű elmenteni a kötetben ismert módok egyikén például RDS vagy .csv fájlba. 11.2.3 Névelemfelismerés és eredményeinek vizualizálása A szövegekből történő névelemfelismeréshez ugyancsak egy adattáblára és egy belőle kialakított quanteda korpuszra van szükségünk. A következő példában mi az előzőleg léterhozott lemmatizált adattáblával dolgozunk, de a névelemfelismerés működik nyers szövegeken is. A léterhozott korpuszon a spacy_parse() függvény argumentumában kell jeleznünk, hogy az entitások felismerését szeretnénk elvégezni (entity = TRUE). Az eredménytáblában láthatjuk, hogy egy új oszlopban minden névelem mellett megkaptuk annak típusát (PER = személynév, LOC = helynév, ORG = szervezet, MISC = egyéb). A corpus() függvény egyedi dokumentum neveket vár, ezért átnevezzük először a doc_id értékeit. parsed_df_3 <- parsed_df_3 %>% mutate(doc_id = paste(doc_id, row_number(), sep = "-")) lemma_corpus <- corpus(parsed_df_3) parsedtxt <- spacy_parse(lemma_corpus, entity = TRUE) entity_extract(parsedtxt, type = "all") #> doc_id sentence_id entity entity_type #> 1 text1-1 1 Jobbik PER #> 2 text1-1 1 Úr PER #> 3 text1-1 1 Országgyűlés PER #> 4 text1-1 3 Jobbik ORG #> 5 text1-1 3 Recsken LOC #> 6 text1-1 3 Ózd LOC #> 7 text1-1 7 Recsken LOC #> 8 text1-1 7 Fidesz ORG #> 9 text1-1 8 Ózd_Janiczak_Dávid PER #> 10 text1-1 9 Jobbik ORG #> 11 text1-1 11 Janiczak_Dávid PER #> 12 text1-1 11 Jobbik ORG #> 13 text1-1 12 Ózd LOC #> 14 text1-1 14 Vida_Ildikó PER #> 15 text1-1 15 Dr._Répássy_Róbert PER #> 16 text1-1 21 Oroszország LOC #> 17 text1-1 21 Egyesült_Államok LOC #> 18 text1-1 21 Ukrajna LOC #> 19 text1-1 22 Gorbacsov PER #> 20 text1-1 23 Magyarország ORG #> 21 text1-1 23 Magyarország LOC #> 22 text1-1 25 Gyurcsány_Ferenc PER #> 23 text1-1 26 Dr._Rétvári_Bence PER #> 24 text1-1 27 Jobbik ORG #> 25 text1-1 29 Fidesz-KDNP ORG #> 26 text1-1 29 Vida_Ildikó PER #> 27 text1-1 31 Kovács_Béla PER #> 28 text1-1 32 Kovács_Béla PER #> 29 text1-1 34 Isten PER #> 30 text1-1 36 Magyarország LOC #> 31 text1-1 37 Magyarország LOC #> 32 text1-1 42 Magyarország ORG #> 33 text1-1 44 Magyarország LOC #> 34 text1-1 48 Jobbik ORG #> 35 text2-1 1 dr._SCHIFFER_ANDRÁS ORG #> 36 text2-1 1 LMP ORG #> 37 text2-1 2 Országgyűlés PER #> 38 text2-1 2 Eurat MISC #> 39 text2-1 2 Orbán-Putyin-paktum MISC #> 40 text2-1 3 L._Simon_László PER #> 41 text2-1 5 Paks LOC #> 42 text2-1 11 Paks ORG #> 43 text2-1 12 Magyarország LOC #> 44 text2-1 13 Westinghouse MISC #> 45 text2-1 14 Paksi_Atomerőműbe ORG #> 46 text2-1 15 Ukrajna LOC #> 47 text2-1 17 Magyarország LOC #> 48 text2-1 17 Európa-USA ORG #> 49 text2-1 18 Magyarország LOC #> 50 text2-1 18 Magyarország LOC #> 51 text2-1 19 Magyarország LOC #> 52 text2-1 20 Magyarország LOC #> 53 text2-1 21 Magyarország LOC #> 54 text2-1 23 Magyarország LOC #> 55 text2-1 25 Európai_Unió ORG #> 56 text2-1 25 Amerika LOC #> 57 text2-1 29 LMP ORG #> 58 text3-1 1 dr._SZÉL_BERNADETT ORG #> 59 text3-1 1 LMP PER #> 60 text3-1 2 Államtitkár_Úr PER #> 61 text3-1 2 Európa LOC #> 62 text3-1 3 Nyugat-Európa LOC #> 63 text3-1 9 Magyarország LOC #> 64 text3-1 16 Németország LOC #> 65 text3-1 20 Magyarország LOC #> 66 text3-1 30 LMP ORG #> 67 text3-1 35 LMP ORG #> 68 text3-1 36 LMP ORG #> 69 text3-1 41 LMP ORG #> 70 text3-1 41 Országgyűlés ORG #> 71 text3-1 45 Ház ORG #> 72 text3-1 46 LMP ORG #> 73 text4-1 1 TÓBIÁS_JÓZSEF PER #> 74 text4-1 1 MSZP PER #> 75 text4-1 2 Ház PER #> 76 text4-1 2 Képviselőtárs PER #> 77 text4-1 4 Magyarország LOC #> 78 text4-1 5 Magyar_Tudományos_Akadémia ORG #> 79 text4-1 5 Magyarország LOC #> 80 text4-1 10 Ázsia LOC #> 81 text4-1 10 Európa LOC #> 82 text4-1 10 Európa LOC #> 83 text4-1 12 Balog_Zoltán PER #> 84 text4-1 13 Magyarország LOC #> 85 text4-1 16 Magyarország LOC #> 86 text4-1 19 Országgyűlés ORG #> 87 text4-1 23 MSZP ORG #> 88 text5-1 1 LMP PER #> 89 text5-1 2 Országgyűlés PER #> 90 text5-1 3 Magyar_Nemzeti_Bank ORG #> 91 text5-1 3 Matolcsy_György PER #> 92 text5-1 4 Matolcsy PER #> 93 text5-1 9 Magyarország LOC #> 94 text5-1 12 Fidesz ORG #> 95 text5-1 12 Matolcsy_György PER #> 96 text5-1 14 Fidesz ORG #> 97 text5-1 15 Fidesz ORG #> 98 text5-1 20 Magyarország LOC #> 99 text5-1 21 Országgyűlés PER #> 100 text5-1 21 Matolcsy_György PER #> 101 text5-1 21 Nemzeti_Bank ORG #> 102 text5-1 21 Magyarország LOC #> 103 text5-1 22 Matolcsy_György PER #> 104 text5-1 23 Matolcsy_György PER #> 105 text5-1 24 Matolcsy_György PER #> 106 text5-1 25 Magyarország LOC #> 107 text5-1 26 Magyarország LOC #> 108 text5-1 31 Magyarország LOC #> 109 text6-1 1 dr._TÓTH_BERTALAN PER #> 110 text6-1 1 MSZP PER #> 111 text6-1 2 Képviselőtárs PER #> 112 text6-1 2 Fidesz ORG #> 113 text6-1 3 Fidesz ORG #> 114 text6-1 3 Fidesz ORG #> 115 text6-1 3 Jobbik ORG #> 116 text6-1 12 Navracsics_Tibor PER #> 117 text6-1 13 MSZP ORG #> 118 text6-1 14 Alaptörvény MISC #> 119 text6-1 15 MSZP ORG #> 120 text6-1 15 Népszabadság ORG #> 121 text6-1 15 Alaptörvény MISC #> 122 text6-1 18 Kunhalmi_Ágnes PER #> 123 text6-1 18 Fidesz ORG #> 124 text6-1 18 Parlament ORG #> 125 text6-1 19 MSZP ORG #> 126 text6-1 19 Origo MISC #> 127 text6-1 19 vs.hu ORG #> 128 text6-1 19 TV2 ORG #> 129 text6-1 19 Népszabadság ORG #> 130 text6-1 20 Soltész_Miklós PER #> 131 text6-1 20 Népszava MISC #> 132 text6-1 20 Fidesz ORG #> 133 text6-1 23 Fidesz ORG #> 134 text6-1 24 Rogán_Antal PER #> 135 text6-1 24 Orbán_Viktor PER #> 136 text6-1 24 Habony_Árpád PER #> 137 text6-1 24 Orbán_Viktor PER #> 138 text6-1 24 Orbán_Viktor PER #> 139 text6-1 26 Orbán_Viktor PER #> 140 text6-1 26 Németh_Szilárd_István PER #> 141 text6-1 27 Országgyűlés ORG #> 142 text6-1 28 Országgyűlés PER #> 143 text6-1 28 Magyarország ORG #> 144 text6-1 28 Magyarország LOC #> 145 text6-1 30 Orbán_Viktor PER #> 146 text6-1 30 Fidesz ORG #> 147 text6-1 32 MSZP ORG #> 148 text6-1 33 Fodor_Gábor PER #> 149 text7-1 1 VOLNER_János PER #> 150 text7-1 1 Jobbik PER #> 151 text7-1 2 Képviselőtárs PER #> 152 text7-1 2 Jobbik ORG #> 153 text7-1 3 Viktor PER #> 154 text7-1 5 Magyarország LOC #> 155 text7-1 6 Legfőbb_Ügyészség ORG #> 156 text7-1 6 Állami_Számvevőszék ORG #> 157 text7-1 6 Fidesz ORG #> 158 text7-1 7 Fidesz ORG #> 159 text7-1 7 Állami_Számvevőszék ORG #> 160 text7-1 7 Magyarország LOC #> 161 text7-1 8 Fidesz ORG #> 162 text7-1 8 MSZP ORG #> 163 text7-1 10 Magyarország LOC #> 164 text7-1 18 off­shore MISC #> 165 text7-1 19 Jobbik ORG #> 166 text7-1 20 Jobbik ORG #> 167 text8-1 1 KÓSA_LAJOS ORG #> 168 text8-1 1 Fidesz ORG #> 169 text8-1 1 Ház PER #> 170 text8-1 1 Európa LOC #> 171 text8-1 1 Stockholm LOC #> 172 text8-1 1 Egyiptom LOC #> 173 text8-1 1 Európa LOC #> 174 text8-1 2 Soros_György PER #> 175 text8-1 2 Európa LOC #> 176 text8-1 3 Európa LOC #> 177 text8-1 3 Németország LOC #> 178 text8-1 4 Németország LOC #> 179 text8-1 6 Magyarország LOC #> 180 text8-1 6 Délnyugat-Balkán LOC #> 181 text8-1 6 Magyarország LOC #> 182 text8-1 6 Magyarország LOC #> 183 text8-1 7 Soros PER #> 184 text8-1 7 Korózs_Lajos PER #> 185 text8-1 9 MSZP ORG #> 186 text8-1 11 Kunhalmi_Ágnes PER #> 187 text8-1 12 Korózs_Lajos PER #> 188 text8-1 12 Fidesz ORG #> 189 text8-1 14 Korózs_Lajos PER #> 190 text8-1 14 Bangóné_Borbély_Ildikó PER #> 191 text8-1 15 Közép-európai_Egyetem ORG #> 192 text8-1 15 Kunhalmi_Ágnes PER #> 193 text8-1 16 Ház PER #> 194 text8-1 16 Soros_György PER #> 195 text8-1 17 Soros_György PER #> 196 text8-1 17 Soros_György PER #> 197 text8-1 17 Soros_György PER #> 198 text8-1 17 Soros PER #> 199 text8-1 17 Soros PER #> 200 text8-1 17 Magyarország LOC #> 201 text8-1 17 Magyarország ORG #> 202 text8-1 19 Magyarország ORG #> 203 text9-1 1 KDNP PER #> 204 text9-1 1 Úr PER #> 205 text9-1 1 Ház PER #> 206 text9-1 2 dr._Rubovszky_György PER #> 207 text9-1 5 Gyuri PER #> 208 text9-1 8 Képviselőtárs PER #> 209 text9-1 9 Szabó_Szabolcs PER #> 210 text9-1 9 Fabiny_Tamás PER #> 211 text9-1 11 Szabó_Szabolcs PER #> 212 text9-1 11 Fabiny_Tamás PER #> 213 text9-1 13 Fidesszel ORG #> 214 text9-1 16 Hölvényi_György PER #> 215 text9-1 18 Közel-Kelet LOC #> 216 text9-1 19 Kereszténydemokrata_Néppárt ORG #> 217 text9-1 24 Ferenc PER #> 218 text9-1 27 Szabó_Szabolcs PER #> 219 text9-1 27 Észak-Afrika LOC #> 220 text9-1 27 Közel-Kelet LOC #> 221 text9-1 30 Gőgös_Zoltán PER #> 222 text10-1 1 dr._GULYÁS PER #> 223 text10-1 1 Fidesz ORG #> 224 text10-1 2 Úr PER #> 225 text10-1 2 Országgyűlés PER #> 226 text10-1 2 Államtitkár_Úr PER #> 227 text10-1 3 Czeglédy_Csaba PER #> 228 text10-1 3 Human_Operator_Zrt. ORG #> 229 text10-1 3 Czeglédy_Csaba PER #> 230 text10-1 4 Czeglédy_Csaba PER #> 231 text10-1 4 Human_Operator_Zrt. ORG #> 232 text10-1 5 Human_Operator_Zrt. ORG #> 233 text10-1 7 Országgyűlés ORG #> 234 text10-1 8 Human_Operator_Zrt. ORG #> 235 text10-1 9 Bangóné_Borbély_Ildikó PER #> 236 text10-1 9 Eszosz-ügy MISC #> 237 text10-1 9 Bangóné_Borbély_Ildikó PER #> 238 text10-1 9 Human_Operator_Zrt. ORG #> 239 text10-1 12 Szocialista_Párt ORG #> 240 text10-1 12 Czeglédy_Csaba PER #> 241 text10-1 12 Demokratikus_Koalíció ORG #> 242 text10-1 12 Szocialista_Párt ORG #> 243 text10-1 12 Magyar_Szocialista_Párt ORG #> 244 text10-1 13 Czeglédy_Csaba PER #> 245 text10-1 13 Czeglédy_Csaba PER #> 246 text10-1 15 Bangóné_Borbély_Ildikó PER #> 247 text10-1 15 Eszosz-ügy MISC #> 248 text10-1 16 Czeglédy_Csabá PER #> 249 text10-1 16 Czeglédy_Csaba PER #> 250 text10-1 18 Heringes_Anita PER #> [ reached 'max' / getOption("max.print") -- omitted 2 rows ] A következőkben a névelemfelismerés eredményeinek vizualizálásra mutatunk be egy példát, amihez az előzőekben elkészített lemmákat tartalmazó adattáblát használjuk fel, úgy hogy első lépésként korpuszt készítünk belőle. df <- parsed_df_3 lemma_corpus <-corpus(df) Ezután a spacy_extract_entity() függvénye segítségévek elvégezzük a névelemfelismerést. Az elemzés eredményét itt nem adattáblában, hanem listában kérjük vissza. A névelemek tokenjeit ezután a jobb áttekinthetőség érdekében megritkítottuk, és csak azokat hagytuk meg, amelyek legalább három alkalommal szerepeltek a korpuszban. lemma_ner <- spacy_extract_entity( lemma_corpus, output = c("list"), multithread = TRUE) ner_tokens <- tokens(lemma_ner) features <- dfm(ner_tokens) %>% dfm_trim(min_termfreq = 3) %>% featnames() ner_tokens <- tokens_select(ner_tokens, features, padding = TRUE) Ezután a különböző alakban előforduló, de ugyanarra az entitásra vonatkozó névelemeket összevontuk. soros <- c("Soros", "Soros György") lemma <- rep("Soros György", length(soros)) ner_tokens <- tokens_replace(ner_tokens, soros, lemma, valuetype = "fixed") ogy <- c("Országgyűlés", "Ház") lemma <- rep("Országgyűlés", length(ogy)) ner_tokens <- tokens_replace(ner_tokens, ogy, lemma, valuetype = "fixed") Majd elkészítettük a szóbeágyazás fejezetben már megismert fcm-et, végezetül pedig egy együttes előfordulási mátrixot készítettünk a kinyert entitásokból és a ggplot() segítségével ábrázoltuk (ld 11.1. ábra).56 Az így kapott ábránk láthatóvá teszi, hogy mely szavak fordulnak elő jellemzően együtt, valamint a vonalvastagsággal azt is egmutatja, hogy ez relatív értelemben milyen gyakran történik. ner_fcm <- fcm(ner_tokens, context = "window", count = "weighted", weights = 1 / (1:5), tri = TRUE) feat <- names(topfeatures(ner_fcm, 80)) ner_fcm_select <- fcm_select(ner_fcm, pattern = feat, selection = "keep") dim(ner_fcm_select) #> [1] 20 20 size <- log(colSums(dfm_select(ner_fcm, feat, selection = "keep"))) set.seed(144) textplot_network(ner_fcm_select, min_freq = 0.7, vertex_size = size / max(size) * 3) Ábra 11.1: Az országgyúlési beszédek névelemeinek együttelőfordulási mátrixa https://github.com/huspacy/huspacy↩︎ Például az UDPipe: http://lindat.mff.cuni.cz/services/udpipe, a magyarlanc: https://rgai.inf.u-szeged.hu/magyarlanc és az e-magyar: https://e-magyar.hu/hu/. Magyar nyelvű szövegek NLP elemzésére használható eszközök részletes listája: https://github.com/oroszgy/awesome-hungarian-nlp↩︎ Részletes leírása: https://spacy.io/↩︎ Részletes leírása: https://cran.r-project.org/web/packages/reticulate/index.html↩︎ Részletes leírása: https://spacyr.quanteda.io/articles/using_spacyr.html↩︎ Részletes leírását lásd: https://tutorials.quanteda.io/basic-operations/fcm/fcm/↩︎ "],["felugyelt.html", "12 Osztályozás és felügyelt tanulás 12.1 Fogalmi alapok 12.2 Osztályozás felügyelt tanulással", " 12 Osztályozás és felügyelt tanulás 12.1 Fogalmi alapok A mesterséges intelligencia két fontos társadalomtudományi alkalmazási területe a felügyelet nélküli és a felügyelt tanulás. Míg az első esetben – ahogy azt a Felügyelet nélküli tanulás fejezetben bemutattuk – az emberi beavatkozás néhány kulcsparaméter megadására (így pl. a kívánt topikok számának meghatározására) szorítkozik, addig a felügyelt tanulás esetében a kutatónak nagyobb mozgástere van “tanítani” a gépet. Ennyiben a felügyelt tanulás alkalmasabb hipotézisek tesztelésére, mint az adatok rejtett mintázatait felfedező felügyelet nélküli tanulás. A felügyelt tanulási feladat megoldása egy úgynevezett tanító halmaz (training set) meghatározásával kezdődik, melynek során a kutatók saját maguk végzik el kézzel azt a feladatot melyet a továbbiakban gépi közreműködéssel szeretnének nagyobb nagyságrendben, de egyben érvényesen (validity) és megbízhatóan (reliability) kivitelezni. Eredményeinket az ugyanúgy eredetileg kézzel lekódolt, de a modell-építés során félretett teszthalmazunkon (test set) értékelhetjük. Ennek során négy kategóriába rendezzük modellünk előrejelzéseit. Egy, a politikusi beszédeket a pozitív hangulatuk alapján osztályozó példát véve ezek a következők: azok a beszédek amelyeket a modell helyesen sorolt be pozitívba (valódi pozitív), vagy negatívba (valódi negatív), illetve azok, amelyek hibásan szerepelnek a pozitív (hamis-pozitív), vagy a negatív kategóriában (hamis-negatív). Mindezek együttesen egy ún. tévesztési táblát (confusion matrix) adnak, melynek további elemzésével ítéletet alkothatunk modellépítésünk eredményességéről. A felügyelt tanulás számos kutatási feladat megoldására alkalmazhatjuk, melyek közül a leggyakoribbak a különböző osztályozási (classification) feladatok. Miközben ezek – így pl. a véleményelemzés – szótáralapú módszertannal is megoldhatóak (lásd a Szótárak és érzelemelemzés fejezetet), a felügyelt tanulás a nagyobb előkészítési igényt rendszerint jobb eredményekkel és rugalmasabb felhasználhatósággal hálálja meg (gondoljunk csak a szótárak domain-függőségére). A felügyelt tanulás egyben a mesterséges intelligencia kutatásának gyorsan fejlődő területe, mely az e fejezetben tárgyalt algoritmus-központú gépi tanuláson túl az ún. mélytanulás (deep learning) és a neurális hálók területén is zajlik egyre látványosabb sikerekkel. Az egyes modellek pontosságának kiértékelésére általában az F értéket használjuk. Az F érték két másik mérőszám keresztezésével jön létre a precision és a recall, előbbi a helyes találatok száma az összes találat között. Az utóbbi pedig a megtalált helyes értékek aránya. A precision érték képlete A recall érték képlete Ebből a két mérőszámból áll az F érték vagy F1, amely a két mérőszám harmonikus átlaga. Az F1 érték képlete Jelen fejezetben két modell fajtát mutatunk be egyazon korpuszon az Support Vector Machine (SVM) és a Naïve Bayes (NB). Mindkettő a fentiekben leírt klasszifikációs feladatot végzi el, viszont eltérő módon működnek. Az SVM a tanító halmazunk dokumentumait vektorként reprezentálja, ami annyit jelent, hogy hozzájuk rendel egy számsort, amely egy közös térben betöltött pozíciójukat reprezentálja. A teszthalmaz dokumentumai egy közös térben reprezentálva Ezt követően pedig a különféle képpen képpen felcímkézett dokumentumok között egy vonalat (hyperplane) húz meg, amely a lehető legnagyobb távolságra van minden egyes dokumentumtól. A minta osztályozása Innentől kezdve pedig a modellnek nincs más dolga, mint a tanító halmazon kívül eső dokumentumok vektor értékeit is megállapítani, elhelyezni őket ebben a közös térben, és attól függően, hogy a hyperplane mely oldalára kerülnek besorolni őket. Ezzel szemben az NB a felcímkézett tanító halmazunk szavaihoz egy valószínűségi értéket rendel annak függvényében, hogy az adott szó adott kategóriába tartozó dokumentumokban hányszor jelenik meg, az adott kategória dokumentumainak teljes szószámához képest. Miután a teszt halmazunk dokumentumaiban minden szóhoz hozzárendeltük ezeket a valószínűségi értékeket nincs más dolgunk, mint a teszt halmazunkon kívül eső dokumentumokban felkeresni ugyanezen szavakat, a hozzájuk rendelt valószínűségi értékeket aggregálni és ez alapján minden dokumentumhoz tudunk rendelni több valószínűségi értéket is, amelyek megadják, hogy mekkora eséllyel tartozik a dokumentum a teszt halmazunk egyes kategóriáiba. 12.2 Osztályozás felügyelt tanulással Az alábbi fejezetben a CAP magyar média gyűjteményéből a napilap címlapokat tartalmazó modult használjuk.57 Az induló adatbázis 71875 cikk szövegét és metaadatait (összesen öt változót: sorszám, fájlnév, a közpolitikai osztály kódja, szöveg, illetve a korpusz forrása – Magyar Nemzet vagy Népszabadság) tartalmazza. Az a célunk, hogy az egyes cikkekhez kézzel, jó minőségben (két, egymástól függetlenül dolgozó kódoló által) kiosztott és egyeztetett közpolitikai kódokat – ez a tanítóhalmaz – arra használjuk, hogy meghatározzuk egy kiválasztott cikkcsoport hasonló kódjait. Az osztályozási feladathoz a CAP közpolitikai kódrendszerét használjuk, mely 21 közpolitikai kategóriát határoz meg az oktatástól az egészségügyön át a honvédelemig. 58 Annak érdekében, hogy egyértelműen értékelhessük a gépi tanulás hatékonyságát, a kiválasztott cikkcsoport (azaz a teszthalmaz) esetében is ismerjük a kézi kódolás eredményét („éles“ kutatási helyzetben, ismeretlen kódok esetében ugyanakkor ezt gyakran szintén csak egy kisebb mintán tudjuk kézzel validálni). További fontos lépés, hogy az észszerű futási idő érdekében a gyakorlat során a teljes adatbázisból – és ezen belül is csak a Népszabadság részhalmazból – fogunk venni egy 4500 darabos mintát. Ezen a mintán pedigg a már korábban említett kétféle modellt fogjuk futtatni a NB-t és az SVM-t. Az ezekkel a módszerekkel létrehozott két modellünk hatékonyságát fogjuk összehasonlítani, valamint azt is megfogjuk nézni, hogy az eredmények megbízhatósága mennyiben közelíti meg a kézikódolási módszerre jellemző 80-90%-os pontosságot. Először behívjuk a szükséges csomagokat. Majd a felügyelet nélküli tanulással foglalkozó fejezethez hasonlóan itt is alkalmazzuk a set.seed() funkciót, mivel anélkül nem egyeznének az eredményeink teljes mértékben egy a kódunk egy késöbbi újrafuttatása esetén. library(stringr) library(dplyr) library(quanteda) library(caret) library(quanteda.textmodels) library(HunMineR) set.seed(1234) Ezt követően betöltjük a HunMineR-ből az adatainkat. Jelen esetben is vethetünk egy pillantást az objektum tartalmára a glimpse funkció segítségével, és láthatjuk, hogy az öt változónk a cikkek tényleges szövege, a sorok száma, a fájlok neve, az újság, ahol a cikk megjelent, valamint a cikk adott témába való besorolása, amely kézi kódolással került hozzárendelésre. Data_NOL_MNO <- HunMineR::data_nol_mno_clean glimpse(Data_NOL_MNO) #> Rows: 71,875 #> Columns: 5 #> $ row_number <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 1… #> $ filename <chr> "nol_1990_01_02_01.txt", "nol_1990_01_02_02.txt"… #> $ majortopic_code <dbl> 19, 19, 19, 1, 1, 19, 19, 1, 1, 19, 19, 1, 19, 1… #> $ text <chr> "változás év választás év világ ünneplés köz kel… #> $ corpus <chr> "NOL", "NOL", "NOL", "NOL", "NOL", "NOL", "NOL",… Majd pár kissebb átalakítást hajtunk végre az adatainkon. Először is ehhez a modellhez most csak a Népszabadság cikkeket fogjuk alkalmazni a Magyar Nemzet cikkeit kiszedjük az adataink közül. Ezután leegyszerűsítjük a kódolást. Összesen 21 témakört lefed a cikkek kézi kódolása most viszont ezekből egy bináris változót készítünk, amelynek 1 lesz az értéke, ha a cikk makrogazdaságról szól, ha pedig bármilyne más témakörről, akkor 0 lesz az értéke. A két fajta cikkből összesen 10000 db-os mintat veszunk, amelyben a két típus egyenlő arányban van jelen. A modellek pedig az ide történő besorolálst próbálják majd megállapítani a cikkek szövege alapján vagyis, hogy makrogazdasági témáról szól-e az adott cikk, vagy valamilyen más témáról. Majd kiválasztjuk a táblából a két változó, amelyekre szükségünk lesz a cikkek szövegét text és a cikkekhez kézzel hozzárendelt téműt label a többit pedig elvetjük. Data_NOL <- Data_NOL_MNO %>% subset(corpus == "NOL" & is.na(text) == F) %>% mutate(label = if_else(majortopic_code == 1, 1, 0)) %>% group_by(label) %>% sample_n(5000) %>% ungroup() %>% select(text, label) Ezt követően egy korpusszá alakítjuk az objektumunkat. nol_corpus <- corpus(Data_NOL) Majd létrehozunk egy új objektumot id_train néven amely 1 és 4500 között 3600 különböző a sample() funkció segítségével véletlenszerűen kiválogatott számot tartalmaz. Utána pedig a korpuszunkhoz hozzá adunk egy új id_numeric elnevezésű változót, amely csupán a megszámozza az egyes cikkeket. Ezeket a késöbbiekben arra fogjuk felhasználni, hogy kialakítsuk a tanító és teszt halmazokat id_train <- sample(1:10000, 2500, replace = FALSE) nol_corpus$id_numeric <- 1:ndoc(nol_corpus) Ezt követően a korpuszunkat egy tokenekké alakítjuk és ezzel egyidejűleg eltávolítjuk a HunMiner beépített magyar stopszó szótárát, valamint szótövesítést is végrehajtunk. stopszavak <- HunMineR::data_stopwords_extra nol_toks <- tokens(nol_corpus, remove_punct = TRUE, remove_number = TRUE) %>% tokens_remove(pattern = stopszavak) %>% tokens_wordstem() A tokeneket pedig egy dfm-é alakítjuk. nol_dfm <- dfm(nol_toks) Ezt követően kialakítjunk a tanító és teszt halmazokat a korábban létrehozott változók segítségével. A lenti kód elsősora egy subsetet alakít ki az adattáblánk mindazon soraiból, amelyek id_numeric változója szerepel az id_train számsorban ez lesz a tanító alhalmaz. Majd ennek megfelelően egy másik alhalmazt alakítunk ki a teszteléshez, amely minden olyan sorát fogja tartalmazni adattáblánknak, amely id_numeric változója nem szerepel az id_train számsorban. train_dfm <- dfm_subset(nol_dfm, id_numeric %in% id_train) test_dfm <- dfm_subset(nol_dfm, !id_numeric %in% id_train) Azáltal pedig, hogy kialakítottuk a két alhalmazt el is végeztük az utolsó előkészítési folyamatot is, az így előkészített adatokon tudjuk majd futtatni a következőkben, mind az NB és az SVM modellünket. 12.2.1 Naïve Bayes A fejezet Naïve Bayesre vonatkozó kódjai quanteda tutoriál menetét és kódjait követik.59 Először létrehozzuk a modellünket, amely számára meghatározzuk, hogy a label változóhoz kell egy predikciót adnia, majd ezt alakalmazzuk az adatainkra. A dfm_match parancs segítségével eltávolítjuk a vizsgált dokumentumaink szavai közül mindazokat, amelyek nem szerepeltek a teszt halmazunkba. Erre azért van szükség, mivel az NB csak azokat a szavakat képes kezelni, amelyekhez már hozzárendelt egy valószínűségi értéket, tehát csak azokat, amelyek a teszt halmazban is megtalálhatóak. nol_nb <- textmodel_nb(train_dfm, train_dfm$label) dfm_kozos <- dfm_match(test_dfm, features = featnames(train_dfm)) A következő objektumban eltároljuk a kézikódolás eredményeit, amelyeket már ismerünk. tenyleges_osztaly <- dfm_kozos$label Majd eltároljuk egy másik objektumban azokat az eredményeket, amelyeket a modellünk generált. becsult_osztaly <- predict(nol_nb, newdata = dfm_kozos) A két fenti adat segítségével pedig létrehozhatjuk a korábban is említett tévesztési táblát. eredmeny_tabla <- table(tenyleges_osztaly, becsult_osztaly) eredmeny_tabla #> becsult_osztaly #> tenyleges_osztaly 0 1 #> 0 3310 463 #> 1 794 2933 Ezt létrehozhatjuk a caret csomag funkciójával is, amely a tévesztési tábla mellett sok más hasznos adatot is megad a számunkra. confusionMatrix(eredmeny_tabla, mode = "everything") #> Confusion Matrix and Statistics #> #> becsult_osztaly #> tenyleges_osztaly 0 1 #> 0 3310 463 #> 1 794 2933 #> #> Accuracy : 0.832 #> 95% CI : (0.824, 0.841) #> No Information Rate : 0.547 #> P-Value [Acc > NIR] : <2e-16 #> #> Kappa : 0.665 #> #> Mcnemar's Test P-Value : <2e-16 #> #> Sensitivity : 0.807 #> Specificity : 0.864 #> Pos Pred Value : 0.877 #> Neg Pred Value : 0.787 #> Precision : 0.877 #> Recall : 0.807 #> F1 : 0.840 #> Prevalence : 0.547 #> Detection Rate : 0.441 #> Detection Prevalence : 0.503 #> Balanced Accuracy : 0.835 #> #> 'Positive' Class : 0 #> Az eredményeken pedig láthatjuk, hogy még az egyszerűség kedvéért lecsökkentett méretű tanítási halmaz ellenére is egy kifejezett magas 83.25%-os pontossági értéket kapunk, amely többé-kevésbé megfeletethető egy kizárólag kézikódolással végzett vizsgálat pontosságának. Ezt követően a kapott eredményeket a mérésünk minőségéről egy táblázatba rendezzük, későbbi összehasonlítás céljából. nb_eredmenyek <- data.frame(confusionMatrix(eredmeny_tabla, mode = "prec_recall")[4]) nb_eredmenyek$meres <- row.names(nb_eredmenyek) nb_eredmenyek$modszer <- "Naïve Bayes" row.names(nb_eredmenyek) <- 1:nrow(nb_eredmenyek) 12.2.2 Support-vector machine A következő kódsorral alkalmazzuk az SVM modellünket, hogy az adatainkon belül a label változóra vonatkozóan készítsen predikciót. nol_svm <- textmodel_svm(train_dfm, y = train_dfm$label) Ismét eltároljuk a kézikódolás eredményeit, amelyeket már ismerünk. Valamint az eredményeket, amelyeket az SVM modellünk generált szintén eltároljuk egy objektumba. tenyleges_osztaly_svm <- dfm_kozos$label becsult_osztaly_svm <- predict(nol_svm, newdata = dfm_kozos) Ismét létrehozhatunk egy egyszerű tévesztési táblát. eredmeny_tabla_svm <- table(tenyleges_osztaly_svm, becsult_osztaly_svm) eredmeny_tabla_svm #> becsult_osztaly_svm #> tenyleges_osztaly_svm 0 1 #> 0 3061 712 #> 1 578 3149 Valamint jelen esetben is használhatjuk a caret csomagban található funkciót, hogy még több információt nyerjünk ki a modellünk működéséről. confusionMatrix(eredmeny_tabla_svm, mode = "everything") #> Confusion Matrix and Statistics #> #> becsult_osztaly_svm #> tenyleges_osztaly_svm 0 1 #> 0 3061 712 #> 1 578 3149 #> #> Accuracy : 0.828 #> 95% CI : (0.819, 0.836) #> No Information Rate : 0.515 #> P-Value [Acc > NIR] : < 2e-16 #> #> Kappa : 0.656 #> #> Mcnemar's Test P-Value : 0.000213 #> #> Sensitivity : 0.841 #> Specificity : 0.816 #> Pos Pred Value : 0.811 #> Neg Pred Value : 0.845 #> Precision : 0.811 #> Recall : 0.841 #> F1 : 0.826 #> Prevalence : 0.485 #> Detection Rate : 0.408 #> Detection Prevalence : 0.503 #> Balanced Accuracy : 0.828 #> #> 'Positive' Class : 0 #> Itt ismét azt találjuk, hogy a csökkentett méretű korpusz ellenére is egy kifejezetten magas pontossági értéket 82.83%-ot kapunk. Az eredményeinket ismét egy adattáblába rendezzük, amelyet végül összekötünk az első táblánnkkal, hogy a két módszert a korábban tárgyalt három mérőszám alapján is összehasonlítsuk. svm_eredmenyek <- data.frame(confusionMatrix(eredmeny_tabla_svm, mode = "prec_recall")[4]) svm_eredmenyek$meres <- row.names(svm_eredmenyek) svm_eredmenyek$modszer <- "Support-vector machine" row.names(svm_eredmenyek) <- 1:nrow(svm_eredmenyek) eredmenyek <- rbind(svm_eredmenyek, nb_eredmenyek) %>% subset(meres %in% c("Precision", "Recall", "F1")) names(eredmenyek)[1] <- "ertek" Végül egy ábrával szemléltetjük a kapott eredményeinket, amelyen látható, hogy jelen esetben a két módszer közötti különbség minimális. Jelen esetben a Naïve Bayes jobban teljesített, mint a Support-vector machine az F1 és Precision tekintetében, viszont utóbbi a Recall tekintetében. ggplot(eredmenyek, aes(x = modszer, y = ertek, fill = modszer)) + geom_bar(stat = "identity", width = 0.1) + facet_wrap(~meres, ncol=1) A korpusz regisztációt követően elérhető az alábbi linken: https://cap.tk.hu/a-media-es-a-kozvelemeny-napirendje↩︎ A kódkönyv regisztrációt követően elérhető az alábbi linken: https://cap.tk.hu/kozpolitikai-cap↩︎ https://tutorials.quanteda.io/machine-learning/nb/↩︎ "],["függelék.html", "13 Függelék 13.1 Az R és az RStudio használata 13.2 Munka saját adatokkal 13.3 Vizualizáció", " 13 Függelék 13.1 Az R és az RStudio használata Az R egy programozási nyelv, amely alkalmas statisztikai számítások elvégzésére és ezek eredményeinek grafikus megjelenítésére. Az R ingyenes, nyílt forráskódú szoftver, mely telepíthető mind Windows, mind Linux, mind MacOS operációs rendszerek alatt az alábbi oldalról: https://cran.r-project.org/ Az RStudio az R integrált fejlesztői környezete (integrated development environment, IDE), mely egy olyan felhasználóbarát felületet biztosít, ami egyszerűbb és átláthatóbb munkát tesz lehetővé. Az RStudio az alábbi oldalról tölthető le: https://rstudio.com/products/rstudio/download/ A „point and click” szoftverekkel szemben az R használata során scripteket kell írni, ami bizonyos programozási jártasságot feltételez, de a későbbiekben lehetővé teszi azt adott kutatási kérdéshez maximálisan illeszkedő kódok összeállítását, melyek segítségével az elemzések mások számára is megbízhatóan reprodukálhatók lesznek. Ugyancsak az R használata mellett szól, hogy komoly fejlesztői és felhasználói közösséggel rendelkezik, így a használat során felmerülő problémákra általában gyorsan megoldást találhatunk. 13.1.1 Az RStudio kezdőfelülete Az RStudio kezdőfelülete négy panelből, eszközsorból és menüsorból áll: Ábra 13.1: RStudio felhasználói felület Az (1) editor ablak szolgál a kód beírására, futtatására és mentésére. A (2) console ablakban jelenik meg a lefuttatott kód és az eredmények. A jobb felső ablak (3) environment fülén láthatóak a memóriában tárolt adatállományok, változók és felhasználói függvények. A history fül mutatja a korábban lefuttatott utasításokat. A jobb alsó ablak (4) files fülén az aktuális munkakönyvtárban tárolt mappákat és fájlok találjuk, míg a plot fülön az elemzéseink során elkészített ábrák jelennek meg. A packages fülön frissíthetjük a meglévő r csomagokat és telepíthetünk újakat. A help fülön a különböző függvények, parancsok leírását, és használatát találjuk meg. A Tools -> Global Options menüpont végezhetjük el az RStudio testreszabását. Így például beállíthatjuk az ablaktér elrendezését (Pane layout), vagy a színvilágot (Appearance), illetve azt hogy a kódok ne fussanak ki az ablakból (Code -> Editing -> Soft wrap R source files). 13.1.2 A projektalapú munka Bár nem kötelező, de javasolt, hogy az RStudio-ban projekt alapon dolgozzunk, mivel így az összes – az adott projekttel kapcsolatos fájlt – egy mappában tárolhatjuk. Új projekt beállítását a File->New Project menüben tehetjük meg, ahol a saját gépünk egy könyvtárát kell kiválasztani, ahová az R a scripteket, az adat- és előzményfájlokat menti. Ezenkívül a Tools->Global Options->General menüpont alatt le kell tiltani a „Restore most recently opened project at startup” és a „Restore .RData ino workspace at startup” beállítást, valamint „Save workspace to .RData on exit” legördülő menüjében be kell állítani a „Never” értéket. Ábra 13.2: RStudio projekt beállítások A szükséges beállítások után a File -> New Project menüben hozhatjuk létre a projektet. Itt arra is lehetőségünk van, hogy kiválasszuk, hogy a projektünket egy teljesen új könyvtárba, vagy egy meglévőbe kívánjuk menteni, esetleg egy meglévő projekt új verzióját szeretnénk létrehozni. Ha sikeresen létrehoztuk a projektet, az RStudio jobb felső sarkában látnunk kell annak nevét. 13.1.3 Scriptek szerkesztése, függvények használata Új script a File -> New -> File -> R Script menüpontban hozható létre, mentésére a File->Save menüpontban egy korábbi script megnyitására File -> Open menüpontban van lehetőségünk. Script bármilyen szövegszerkesztővel írható, majd beilleszthető az editor ablakba. A scripteket érdemes magyarázatokkal (kommentekkel) ellátni, hogy a későbbiekben pontosan követhető legyen, hogy melyik parancs segítségével pontosan milyen lépéseket hajtottunk végre. A magyarázatokat vagy más néven kommenteket kettőskereszt (#) karakterrel vezetjük be. A scriptbeli utasítások az azokat tartalmazó sorokra állva vagy több sort kijelölve a Run feliratra kattintva vagy a Ctrl+Enter billentyűparanccsal futtathatók le. A lefuttatott parancsok és azok eredményei ezután a bal alsó sarokban lévő console ablakban jelennek meg és ugyanitt kapunk hibaüzenetet is, ha valamilyen hibát vétettünk a script írása közben. A munkafolyamat során létrehozott állományok (ábrák, fájlok) az ún. munkakönyvtárba (working directory) mentődnek. Az aktuális munkakönyvtár neve, elérési útja a getwd() utasítással jeleníthető meg. A könyvtárban található állományok listázására a list.files() utasítással van lehetőségünk. Ha a korábbiaktól eltérő munkakönyvtárat akarunk megadni, azt a setwd() függvénnyel tehetjük meg, ahol a ()-ben az adott mappa elérési útját kell megadnunk. Az elérési útban a meghajtó azonosítóját, majd a mappák, almappák nevét vagy egy normál irányú perjel (/), vagy két fordított perjel (\\\\) választja el, mivel az elérési út karakterlánc, ezért azt idézőjelek vagy aposztrófok közé kell tennünk. Az aktuális munkakönyvtárba beléphetünk a jobb alsó ablak file lapján a „More -> Go To Working Directory” segítségével. Ugyanitt a „Set Working Directory”-val munkakönyvtárnak állíthatjuk be az a mappát, amelyben épp benne vagyunk. Ábra 13.3: Working directory beállítások A munkafolyamat befejezésére a q() vagy quit() függvénnyel van lehetőségünk. Az R-ben objektumokkal dolgozunk, amik a teljesség igénye nélkül lehetnek például egyszerű szám vektortok, vagy akár komplex listák, illetve függvények, ábrák. A munkafolyamat során létrehozott objektumokat az RStudio jobb felső ablakának environment fülén jelennek meg. A mentett objektumokat a fent látható seprű ikonra kattintva törölhetjük a memóriából. Az environment ablakra érdemes úgy gondolni hogy ott jelennek meg a memóriában tárolt értékek. Az RStudio jobb alsó ablakának plots fülén láthatjuk azon parancsok eredményét, melyek kimenete valamilyen ábra. A packages fülnél a már telepített és a letölthető kiegészítő csomagokat jeleníthetjük meg. A help fülön a korábban említettek szerint a súgó érhető el. Az RStudio-ban használható billentyűparancsok teljes listáját Alt+Shift+K billentyűkombinációval tekinthetjük meg. Néhány gyakrabban használt, hasznos billentyűparancs: Ctrl+Enter: futtassa a kódot az aktuális sorban Ctrl+Alt+B: futtassa a kódot az elejétől az aktuális sorig Ctrl+Alt+E: futtassa a kódot az aktuális sortól a forrásfájl végéig Ctrl+D: törölje az aktuális sort Az R-ben beépített függvények (function) állnak rendelkezésünkre a számítások végrehajtására, emellett több csomag (package) is letölthető, amelyek különböző függvényeket tartalmaznak. A függvények a következőképpen épülnek fel: függvénynév(paraméter). Például tartalom képernyőre való kiíratását a print() függvénnyel tehetjük, amelynek gömbölyű zárójelekkel határolt részébe írhatjuk a megjelenítendő szöveget. A citation() függvénnyel lekérdezhetjük az egyes beépített csomagokra való hivatkozást is: a citation(quanteda) függvény a quanteda csomag hivatkozását adja meg. Az R súgórendszere a help.start() utasítással indítható el. Egy adott függvényre vonatkozó súgórészlet a függvények neve elé kérdőjel írásával, vagy a help() argumentumába a kérdéses függvény nevének beírásával jeleníthető meg (pl.: help(sum)). 13.1.4 R csomagok Az R-ben telepíthetők kiegészítő csomagok (packages), amelyek alapértelmezetten el nem érhető algoritmusokat, függvényeket tartalmaznak. A csomagok saját dokumentációval rendelkeznek, amelyeket fel kell tüntetni a használatukkal készült publikációink hivatkozáslistájában. A csomagok telepítésre több lehetőségünk is van: használhatjuk a menüsor Tools -> Install Packages menüpontját, vagy a jobb alsó ablak packages fül Install menüpontját, illetve az editor ablakban az install.packages() parancsot futtatva, ahol a ()-be a telepíteni kívánt csomag nevét kell beírnunk (pl.: install.packages(\"dplyr\")). Ahhoz, hogy egy csomag funkcióit használjuk azt be kell töltetnünk a library() parancs segítségével, itt megintcsak a használni kívánt csomag nevét kell a zárójelek közé helyeznünk, viszont ebben az esetben nem szükséges idézőjelek közé helyeznünk zat (pl.: library(dplyr)) ameddig ezt a parancsot nem futattjuk le az adott csomag funkció nem lesznek elérhetőek számunkra. Ábra 13.4: Packages fül 13.1.5 Objektumok tárolása, értékadás Az objektumok lehetnek például vektorok, mátrixok, tömbök (array), adat táblák (data frame). Értékadás nélkül az R csak megjeleníti a műveletek eredményét, de nem tárolja el azokat. Az eredmények eltárolásához azokat egy objektumba kell elmentenünk. Ehhez meg kell adnunk az objektum nevét majd az <- után adjuk meg annak értékét: a <- 12 + 3.Futtatás után az environments fülön megjelenik az a objektum, melynek értéke 15. Az objektumok elnevezésénél figyelnünk kell arra, hogy az R különbséget tesz a kis és nagybetűk között, valamint, hogy az ugyanolyan nevű objektumokat kérdés nélkül felülírja és ezt a felülírást nem lehet visszavonni. 13.1.6 Vektorok Az R-ben kétféle típusú vektort különböztetünk meg: atomi vektor (atomic vector) lista (list) Az atomi vektornak hat típusa van, logikai (logical), egész szám (integer), természetes szám (double), karakter (character), komplex szám (complex) és nyers adat (raw). A leggyakrabban valamilyen numerikus, logikai vagy karakter vektorral használjuk. Az egyedüli vektorok onnan kapták a nevüket hogy csak egy féle adattípust tudnak tárolni. A listák ezzel szemben gyakorlatilag bármit tudnak tárolni, akár több listát is egybeágyazhatunk. A vektorok és listák azok az építőelemek amikből felépülnek az R objektumaink. Több érték vagy azonos típusú objektum összefűzését a c() függvénnyel végezhetjük el. A lenti példában három különböző objektumot kreálunk, egy numerikusat, egy karaktert és egy logikait. A karakter vektorban az elemeket időzőjellel és vesszővel szeparáljuk. A logikai vektor csak TRUE, illetve FALSE értékeket tartalmazhat. numerikus <- c(1,2,3,4,5) karakter <- c("kutya","macska","ló") logikai <- c(TRUE, TRUE, FALSE) A létrehozott vektorokkal különböző műveleteket végezhetünk el, például összeadhatjuk numerikus vektorainkat. Ebben az esetben az első vektor első eleme a második vektor első eleméhez adódik. c(1:4) + c(10,20,30,40) #> [1] 11 22 33 44 A karaktervektorokat össze is fűzhetjük egymással. Példánkban egy új objektumot is létrehoztunk, ezért a jobb felső ablakban, az environment fülön láthatjuk, hogy a létrejött karakter_kombinalt objektum egy négy elemű (hosszúságú) karaktervektor (chr [1:4]), melynek elemei a \"kutya\",\"macska\",\"ló\",\"nyúl\". Az objektumként tárolt vektorok tartalmát az adott sort lefuttatva írathatjuk ki a console ablakba. Ugyanezt megtehetjük print() függvény segítségével is, ahol a függvény arrgumentumában () az adott objektum nevét kell szerepeltetnünk. karakter1 <- c("kutya","macska","ló") karakter2 <-c("nyúl") karakter_kombinalt <-c(karakter1, karakter2) karakter_kombinalt #> [1] "kutya" "macska" "ló" "nyúl" Ha egy vektorról szeretnénk megtudni, hogy milyen típusú azt a typeof() vagy a class() paranccsal tehetjük meg, ahol ()-ben az adott objektumként tárolt vektor nevét kell megadnunk: typeof(karakter1). A vektor hosszúságát (benne tárolt elemek száma vektorok esetén) a lenght() függvénnyel tudhatjuk meg. typeof(karakter1) #> [1] "character" length(karakter1) #> [1] 3 13.1.7 Faktorok A faktorok a kategórikus adatok tárolására szolgálnak. Faktor típusú változó a factor() függvénnyel hozható létre. A faktor szintjeit (igen, semleges, nem), a levels() függvénnyel kaphatjuk meg míg az adatok címkéit (tehát a kapott válaszok számát), a labels() paranccsal érhetjük el. survey_response <- factor(c("igen", "semleges", "nem", "semleges", "nem", "nem", "igen"), ordered = TRUE) levels(survey_response) #> [1] "igen" "nem" "semleges" labels(survey_response) #> [1] "1" "2" "3" "4" "5" "6" "7" 13.1.8 Data frame Az adattábla (data frame) a statisztikai és adatelemzési folyamatok egyik leggyakrabban használt adattárolási formája. Egy data frame többféle típusú adatot tartalmazhat. A data frame-k különféle oszlopokból állhatnak, amelyek különféle típusú adatokat tartalmazhatnak, de egy oszlop csak egy típusú adatból állhat. Az itt bemutatott data frame 7 megfigyelést és 4 féle változót tartalmaz (id, country, pop, continent). #> id orszag nepesseg kontinens #> 1 1 Thailand 68.7 Asia #> 2 2 Norway 5.2 Europe #> 3 3 North Korea 24.0 Asia #> 4 4 Canada 47.8 North America #> 5 5 Slovenia 2.0 Europe #> 6 6 France 63.6 Europe #> 7 7 Venezuela 31.6 South America A data frame-be rendezett adatokhoz különböző módon férhetünk hozzá, például a data frame nevének majd []-ben a kívánt sor megadásával, kiírathatjuk a console ablakba annak tetszőleges sorát ás oszlopát: orszag_adatok[1, 1]. Az R több különböző módot kínál a data frame sorainak és oszlopainak eléréséhez. A [ általános használata: data_frame[sor, oszlop]. Egy másik megoldás a $ haszálata: data_frame$oszlop. orszag_adatok[1, 4] #> [1] Asia #> Levels: Asia Europe North America South America orszag_adatok$orszag #> [1] "Thailand" "Norway" "North Korea" "Canada" "Slovenia" #> [6] "France" "Venezuela" 13.2 Munka saját adatokkal Saját adatainkat legegyszerűbben a munkakönyvtárból (working directory) hívhatjuk be. A munkakönyvtár egy olyan mappa a számítógépünkön, amely közvetlenül kapcsolatban van az éppen megnyitott R scripttel vagy projekttel. Amennyiben nem határozzuk meg , hogy adatokat honnan szeretnénk behívni, akkor azokat mindig innen fogja megpróbálni betölteni az R. A getwd() parancs segítségével bármikor megtekinthetjük az aktuális munkakönyvtárunk helyét a számítógépünkön. Amennyiben szeretnénk beállítani egy új helyet a munkakönyvtárunknak akkor azt megtehetjük a setwd() paranccsal (pl.: setwd(C:/ User /Documents) illetve a menü rendszeren keresztül is van rá lehetőségünk Session -> Set Working Directory -> Choose Direcotry…. Az R-ben praktikus úgynevezett projektalapú munkával dolgozni. Létrehozhatunk egy új projektet a menü rendszerben File -> New Project… itt meg kell határoznunk a projekt fájl helyét. A projekt alapú munka előnye, hogy a a munkakönyvtár mindig ugyanabban a mappában található, ahol az R projekt fájl is, amely megkönnyíti a saját adatokkal való munkát. Az egyes fájl formátumokat különböző parancsokkal tudjuk beolvasni. Egy txt esetében használhatjuk a read.txt() parancsot, ehhez a funkcióhoz nem kell csomagot betöltenünk, mivel az R alapverziója tartalmazza.Areadtext csomagon található readtext() parancs pedig nem csak txt-k esetében, hanem minden elterjedt szöveges fájl formátum esetében működik, mint docx és pdf. A Akárhányszor adatokat olvasunk be meg kell, hogy határozzuk az objektum nevét, amely tartalmazni fogja az adott fájl adatait (pl.: proba<-read.txt (proba.txt)). A csv (comma separated values) formátumú fájlok esetében használhatjuk a read.csv parancsot, amelyet szintén tartalmaz az R alapverziója, illetve használhatjuk a read_csv parancsot is, amelyet a readr csomag tartalmaz (pl.: proba <- read.csv(proba.csv)). Utóbbi használata ajánlott magyar nyelvű szöveget tartalmazó adatok esetében, mivel tapasztalataink szerint ez a parancs kezeli legjobban a különböző kódolási problémákat. Amennyiben Excel táblázatokkal dolgozunk érdemes azokat csv formátumban elmenteni és azután betölteni, viszont van lehetőségünk a tidyverse csomag read_excel() parancsának segítségével is Excel fájlokat betölteni. A read_excel parancs működik, mind az xlsx és az xls formátumú fájlok esetében is. Mivel az excel fájlok több munkalappal is rendelkeznek ezért a read_excel használatokar azt is meghatározhatjuk, hogy melyik munkalapot szeretnénk betölteni, amennyiben nem határozzuk meg az első lapot használja alpértelmezett módon (pl.: proba <- read_excel(proba.xlsx, Sheet = 2). A saját adatok beolvasása során gyakran felmerülő hiba az úgynevezet karakter kódoláshoz kötődik, amely a számítógépek számára azt mutatja meg, hogy hogyan fordítsák a digitális adatokat szimbólumokká, vagyis karakterekké, mivel az egyes nyelvek eltérő karakter készlettel rendekeznek, ezért többféle karakter kódolás is létezik. A hiba akkor jelentkezik, ha adatokat akarunk betölteni amelyek egy addott karakter kódolással rendelkeznek, de a kódunk egy másikkal tölti be őket, ilyenkor a szövegünk beolvasása hibás lesz. Szerencsére a fentebb említett beolvasási módok mint lehetővé teszik, hogy a felhasználó explicite meghatározza, hogy milyen karakter kódolással legyenek az adatok betöltve pl.: proba <- readtext(\"proba.txt\", encoding = \"UTF-8\"). A legtöbb esetben a magyar nyelvű szövegek UTF-8 karakter kódolással rendelkeznek, amennyiben ezt meghatározzuk, de az R-be töltött szövegeink továbbra sem néznek ki úgy, mint a szöveges dokumentumainkban, akkor használhatjuk a readtext csomag encoding() parancsát, amely egy szóláncról vagy egy szóláncokról álló vektorról meg tudja nekünk mondani, hogy milyen a karakter kódolásuk. 13.3 Vizualizáció library(ggplot2) library(gapminder) library(plotly) Az elemzéseinkhez használt data frame adatai alapján a ggplot2 csomag segítségével lehetőségünk van különböző vizualizációk készítésére is. A ggplot2 használata során különböző témákat alkalmazhatunk, melyek részletes leírása megtalálható: https://ggplot2.tidyverse.org/reference/ggtheme.html Abban az esetben, ha nem választunk témát, a ggplot2 a következő ábrán is látható alaptémát használja. Ha például a szürke helyett fehér hátteret szeretnénk, alkalmazhatjuk a theme_minmal()parancsot. Szintén gyakran alkalmazott ábra alap a thema_bw(), ami az előzőtől az ábra keretezésében különbözik. Ha fehér alapon, de a beosztások vonalait feketén szeretnénk megjeleníteni, alkalmazhatjuk a theme_linedraw() függvényt, a theme_void() segítségével pedig egy fehér alapon, beosztásoktól mentes alapot kapunk, a theme_dark() pedig sötét hátteret eredményez. A theme_classic() segítségével az x és y tengelyt jeleníthetjük meg fehér alapon. Egy ábra készítésének alapja mindig a használni kívánt adatkészlet beolvasása, illetve az ábrázolni kívánt változót vagy változók megadása. Ezt követi a megfelelő alakzat kiválasztása, attól függően például, hogy eloszlást, változást, adatok közötti kapcsolatot, vagy eltéréseket akarunk ábrázolni. A geom az a geometriai objektum, a mit a diagram az adatok megjelenítésére használ. Agglpot2 több mint 40 féle alakzat alkalmazására ad lehetőséget, ezek közül néhány gyakoribbat mutatunk be az alábbiakban. Az alakzatokról részletes leírása található például az alábbi linken: https://r4ds.had.co.nz/data-visualisation.html A következőkben a gapminder csomagban található adatok segítségével szemléltetjük az adatok vizualizálásának alapjait. Először egyszerű alapbeállítások mellett egy histogram típusú vizualizációt készítünk. ggplot( data = gapminder, mapping = aes(x = gdpPercap) ) + geom_histogram() Lehetőségünk van arra, hogy az alakzat színét megváltoztassuk. A használható színek és színkódok megtalálhatóak a ggplot2 leírásában: https://ggplot2-book.org/scale-colour.html ggplot( data = gapminder, mapping = aes(x = gdpPercap) ) + geom_histogram(fill = "yellow", colour = "green") Meghatározhatjuk külön-külön a histogram x és y tengelyén ábrázolni kívánt adatokat és választhatjuk azok pontszerű ábrázolását is. ggplot( data = gapminder, mapping = aes( x = gdpPercap, y = lifeExp ) ) + geom_point() Ahogy az előzőekben, itt is megváltoztathatjuk az ábra színét. ggplot( data = gapminder, mapping = aes( x = gdpPercap, y = lifeExp ) ) + geom_point(colour = "blue") Az fenti script kibővítésével az egyes kontinensek adatait különböző színnel ábrázolhatjuk, az x és y tengelyt elnevezhetjük, a histogramnak címet és alcímet adhatunk, illetve az adataink forrását is feltüntethetjük az alábbi módon: ggplot( data = gapminder, mapping = aes( x = gdpPercap, y = lifeExp, color = continent ) ) + geom_point() + labs( x = "GDP per capita (log $)", y = "Life expectancy", title = "Connection between GDP and Life expectancy", subtitle = "Points are country-years", caption = "Source: Gapminder dataset" ) Az ábrán található feliratok méretének, betűtípusának és betűszínének megválasztásra is lehetőségünk van. ggplot( data = gapminder, mapping = aes( x = gdpPercap, y = lifeExp, color = continent ) ) + geom_point() + labs( x = "GDP per capita (log $)", y = "Life expectancy", title = "Connection between GDP and Life expectancy", subtitle = "Points are country-years", caption = "Source: Gapminder dataset" ) + theme(plot.title = element_text( size = 12, colour = "red" )) Készíthetünk oszlopdiagramot is, amit a ggplot2 diamonds adatkészletén személtetünk ggplot(data = diamonds) + geom_bar(mapping = aes(x = cut)) Itt is lehetőségünk van arra, hogy a diagram színét megváltoztassuk. ggplot(data = diamonds) + geom_bar(mapping = aes(x = cut), fill = "darkgreen") De arra is lehetőségünk van, hogy az egyes oszlopok eltérő színűek legyenek. ggplot(data = diamonds) + geom_bar(mapping = aes(x = cut, fill = cut)) Arra is van lehetőségünk, hogy egyszerre több változót is ábrázoljunk. ggplot(data = diamonds) + geom_bar(mapping = aes(x = cut, fill = clarity)) Arra ggplot2 segítségével arra is lehetőségünk van, hogy csv-ből beolvasott adatainkat vizualizáljuk. plot_cap_1 <- read.csv("data/plot_cap_1.csv", head = TRUE, sep = ";") ggplot(plot_cap_1, aes(Year, fill = Subtopic)) + scale_x_discrete(limits = c(1957, 1958, 1959, 1960, 1961, 1962, 1963)) + geom_bar(position = "dodge") + labs( x = NULL, y = NULL, title = "A Magyar Közlönyben kihirdetett agrárpolitikai jogszabályok", subtitle = "N=445" ) + coord_flip() + # az ábra tipusa theme_minimal() + theme(plot.title = element_text(size = 12)) A csv-ből belolvasott adatainkból kördiagramot is készíthetünk pie <- read.csv("data/pie.csv", head = TRUE, sep = ";") ggplot(pie, aes(x = "", y = value, fill = Type)) + geom_bar(stat = "identity", width = 1) + coord_polar("y", start = 0) + scale_fill_brewer(palette = "GnBu") + labs( title = "A Magyar Közlönyben megjelent jogszabályok típusai", subtitle = "N = 445" ) + theme_void() Továbbá minden ábránkat, amelyet a ggplot segítségével létrehozunk lehetőségünk van interaktívvá tenni a plotly csomag ggplotly parancsának segítségével. Ehhez egyszerűen csak az ábrát egy objektumba kell, hogy létrehozzuk. ggplotabra <- ggplot( data = gapminder, mapping = aes(x = gdpPercap) ) + geom_histogram() Majd ennek az obejktumnak a nevét helyezzük be a ggplotly parancsba, és futtassuk azt. ggplotly(ggplotabra) "],["irodalomjegyzék.html", "Irodalomjegyzék", " Irodalomjegyzék "],["404.html", "Page not found", " Page not found The page you requested cannot be found (perhaps it was moved or renamed). You may want to try searching to find the page's new location, or use the table of contents to find the page you are looking for. "]] +[["index.html", "Szövegbányászat és mesterséges intelligencia R-ben Üdvözöljük! 2.0 - Online frissítések", " Szövegbányászat és mesterséges intelligencia R-ben Sebők Miklós, Ring Orsolya, Máté Ákos 2023 Üdvözöljük! Könyvünk bevezeti az érdeklődőket a szövegbányászat és a mesterséges intelligencia társadalomtudományi alkalmazásának speciális problémáiba. Támaszkodva a Sebők Miklós által szerkesztett Kvantitatív szövegelemzés és szövegbányászat a politikatudományban (L’Harmattan, 2016) című kötet elméleti bevezetésére, ezúttal a társadalomtudományi elemzések során használható kvantitatív szövegelemzés legfontosabb gyakorlati feladatait vesszük sorra. A szövegek adatként való értelmezése (text as data) és kvantitatív elemzése, avagy a szövegbányászat (text mining) a nemzetközi társadalomtudományi kutatások egyik leggyorsabban fejlődő irányzata. A szövegbányászat emellett a társadalomtudósok számára az egyik legnyilvánvalóbb belépési pont a mesterséges intelligenciát, ezen belül is gépi tanulást alkalmazó kutatások területére. A magyar tankönyvpiacon elsőként ismertetünk lépésről-lépésre a nemzetközi társadalomtudományban használatos olyan kvantitatív szövegelemzési eljárásokat, mint a névelemfelismerés, a véleményelemzés, a topikmodellezés, illetve a szövegek felügyelt tanulásra épülő osztályozása. A módszereink bemutatására szolgáló elemzéseket az egyik leggyakrabban használt programnyelv, az R segítségével végeztük el. A kötet anyaga akár minimális programozási ismerettel is elsajátítható, így teljesen kezdők számára is ajánlott. A hazai olvasók érdeklődését szem előtt tartva példáink döntő többsége új, magyar nyelvű korpuszokra épül, melyek alapján megismerhetők a magyar nyelvű kvantitatív szövegelemzés módozatai. A könyv megrendelhető a Typotex kiadó honlapján! 2.0 - Online frissítések A Szövegbányászat és mesterséges intelligencia R-ben szerkesztette Sebők Miklós, Ring Orsolya, és Máté Ákos frissített verziója már online elérhető, a könyv a MILAB projekt támogatásával került publikációra. Az eredeti kézirat több részét is kiegészítettük mind Szövegbányászati módszertannal, gyakorlati példákkal, és ábrázolási technikákkal. Az újítások teljes listája: A szövegbányászat egy részletesebb meghatározása és definíciója a könyv bevezetőjében. A gyakorlati szövegbányászat fő lépéseit egy egyszerű példa mutatja be az olvasó számára Ady Endre és Petőfi Sándor versein keresztül. A könyvben felhasznált adatokhoz és a könyvben előállított ábrákhoz külön leíró részeket helyeztünk a szövegbe. Egy új alfejezet segíti az olvasót a saját adatai használatához. A könyv statikus ábráit interaktív megfelelőikre cseréltük ki. Új információk az adatábrázolással foglalkozó alfejezetben interaktív ábrák előállítására vonatkozóan. Az Osztályozás és felügyelt tanulás című fejezet immár mind az SVM és a Naïve Bayes módszer R-n belüli alkalmazását is bemutatja. Az Osztályozás és felügyelt tanulás című fejezetet kiegészítettük az itt bemutatott módszerek működésének közérthető leírásával. A Munka karakter vektorokkal című alfejezetet kiegészítettük az n-grammok leírásával. Javasolt hivatkozás: Sebők Miklós, Ring Orsolya, és Máté Ákos. 2021. Szövegbányászat és Mesterséges Intelligencia R-ben. Budapest: Typotex. Bib formátumban: @book{sebokringmate2021szovegbanyaszat, address = {Budapest}, title = {Szövegbányászat és mesterséges intelligencia {R}-ben}, publisher = {Typotex}, author = {Sebők, Miklós and Ring, Orsolya and Máté, Ákos}, year = {2021} } A kötet alapjául szolgáló kutatást, amelyet a Társadalomtudományi Kutatóközpont valósított meg, az Innovációs és Technológiai Minisztérium és a Nemzeti Kutatási, Fejlesztési és Innovációs Hivatal támogatta a Mesterséges Intelligencia Nemzeti Laboratórium keretében. A kötet megjelenését az MTA Könyvkiadási Alapja, a Társadalomtudományi Kutatóközpont Könyvtámogatási Alapja, a Nemzeti Kutatási, Fejlesztési és Innovációs Hivatal (NKFIH FK 123907, NKFIH FK 129018), valamint az MTA Bolyai János Kutatási Ösztöndíja támogatta. "],["intro.html", "1 Bevezetés 1.1 A kötet témái 1.2 Használati utasítás 1.3 A HunMineR használata 1.4 Egy bevezető példa 1.5 Köszönetnyilvánítás", " 1 Bevezetés 1.1 A kötet témái A szövegek adatként való értelmezése (text as data) és kvantitatív elemzése (quantitative text analysis), avagy a szövegbányászat (text mining) a nemzetközi társadalomtudományi kutatások egyik leggyorsabban fejlődő irányzata. A szövegbányászat egy olyan folyamat, amely során nyers, strukturálatlan szövegeket (pl.: beszédek, felszólalások, újságcikkek) egy strukturált formátumba helyezünk, hogy így új, korábban ismeretlen információkhoz tudjunk hozzájutni, mint trendekhez, mintázatokhoz, összefüggésekhez stb. A szövegek és más kvalitatív adatok (filmek, képek) elemzése annyiban különbözik a mennyiségi (kvantitatív) adatokétól, hogy nyers formájukban még nem alkalmasak statisztikai, illetve ökonometriai elemzésre. Ezért van szükség az ezzel összefüggő módszertani problémák speciális tárgyalására. Jelen kötet bevezeti az érdeklődőket a szövegbányászat és a mesterséges intelligencia társadalomtudományi alkalmazásának ilyen speciális problémáiba, valamint ezek gyakorlati megoldásába. Közvetlen előzménynek tekinthető a témában a Sebők Miklós által szerkesztett Kvantitatív szövegelemzés és szövegbányászat a politikatudományban címmel megjelent könyv, amely a magyar tudományos diskurzusban kevésbé bevett alapfogalmakat és eljárásokat mutatta be (Sebők 2016). A hangsúly az elméleten volt, bár számos fejezet foglalkozott konkrét kódrészletek elemzésével. Míg az előző kötet az egyes kódolási eljárásokat, illetve ezek kutatásmódszertani előnyeit és hátrányait ismertette, ezúttal a társadalomtudományi elemzések során használható kvantitatív szövegelemzés legfontosabb gyakorlati feladatait vesszük sorra, ezek egymással való logikai kapcsolatátt illusztrálja a lenti ábra. A könyvben bemutatott kódok elsősorban a quanteda csomagot illetve egyes a quanteda-ra épülő csomagokat használnak (Benoit et al. 2018). A szövegbányászat könyvünkben bemutatott egyes módszerei A kötetünkben tárgyalt egyes módszereket három kategóriába sorolhatjuk, a működési elvük alapján. A szövegek statisztikai leírása egyszerre lehet két szöveg összehasonlítása például a Koszinusz vagy Jaccard hasonlóság alapján, melyekről a Szövegösszehasonlítás fejzetben írunk, valamint egy adott szöveg különböző statisztikai jellemzőinek leírása, az erre voantkozó módszerekkel Leíró statisztika című fejezet foglalkozik. A szótáralapú módszerekről és ezek érzelemelemzésre való használatát a Szótárak és érzelemelemzés című fejezet tárgyalja. Az ideológiai skálázás során politikai szereplők egymáshoz viszonyított realtív pozícióit azonosítjuk általában beszédek, illetve felszólalások alapján az erre vonatkozó módszertant a Szövegskálázás című fejezet részletezi. A szövegbányászat egyik legalapvetőbb problémája a dokumentumok egyes kategóriákba való csoportosítása. Az erre vonatkozó módszerek két alkategóriája közül is többet mutatunk be. Amikor ismert kategóriákba történő osztályzásról beszélünk, akkor a kutatónak kell meghatároznia előre a kategóriákat a modell számára, mielőtt az replikálja a besorolásokat. Mind a Naïve Bayes és a Support Vector Machine ezt a feladatot látja el ezek működéséről és alkalmazásáról az Osztályozás és felügyelt tanulás című fejezetben írunk. Ezzel szemben ismeretlen kategóriákba való csoportosítás esetén a kutató nem ad előzőleges utatsítást az algoritmus számára, a modell a csoportosítást a dokumentumok szövegében meglévő látens mintázatok alapján végzi el. Az ismeretlen kategóriákba történő csoportosítást a könyvben bemutatott több módszerrel is elvégezhetjók, mint a K-közép klaszterezés, Latent Dirichlet Allocation, és Struktúrális topikmodellek, amelyeket a Felügyelet nélküli tanulás - Topikmodellezés című fejezetben tárgyaljuk. A Végezetül pedig a szóbeágyazások ellentétben a korábbiakban említett szózsák logikát követő módszerekkel képesek egy dokumentum szóhasználatának belső összefüggéseit azonosítani, ezzel a kifejezetten rugalmas kutatási módszerrel, illetve alkalmazásával Szóbeágyazások fejezetben foglalkozunk. Könyvünk a magyar tankönyvpiacon elsőként ismerteti lépésről-lépésre a nemzetközi társadalomtudományban használatos kvantitatív szövegelemzési eljárásokat. A módszereink bemutatására szolgáló elemzéseket az R programnyelv segítségével végeztük el, mely a nemzetközi társadalomtudományi vizsgálatok során egyik leggyakrabban használt környezet a Python mellett. A kötetben igyekeztünk magyar szakkifejezéseket használni, de mivel a szövegbányászat nyelve az angol, mindig megadtuk, azok angol megfelelőjét is. Kivételt képeznek azok az esetek, ahol nincs használatban megfelelő magyar terminológia, ezeknél megtartottuk az angol kifejezéseket, de magyarázattal láttuk el azokat. Az olvasó a két kötet együttes használatával olyan ismeretek birtokába jut, melyek révén képes lesz alkalmazni a kvantitatív szövegelemzés és szövegbányászat legalapvetőbb eljárásait saját kutatásaiban. Deduktív vagy induktív felfedező logikája szerint dönthet az adatelemzés módjáról, és a felkínált menüből kiválaszthatja a kutatási tervéhez legjobban illeszkedő megoldásokat. A bemutatott konkrét példák segítségével pedig akár reprodukálhatja is ezen eljárásokat saját kutatásában. Mindezt a kötet fejezeteiben bőséggel tárgyalt R-scriptek (kódok) részletes leírása is segíti. Ennek alapján a kötet két fő célcsoportja a társadalomtudományi kutatói és felsőoktatási hallgatói-oktatói közösség. Az oktatási alkalmazást segítheti a fontosabb fogalmak magyar és angol nyelvű tárgymutatója, valamint több helyen a további olvasásra ajánlott szakirodalom felsorolása. A kötet honlapján (https://tankonyv.poltextlab.com) közvetlenül is elérhetőek a felhasznált adatbázisok és kódok. Kötetünk négy logikai egységből épül fel. Az első négy fejezet bemutatja azokat a fogalmakat és eljárásokat, amelyek elengedhetetlenek egy szövegbányászati kutatás során, valamint itt kerül sor a szöveges adatforrásokkal való munkafolyamat ismertetésére, a szövegelőkészítés és a korpuszépítés technikáinak bemutatására. A második blokkban az egyszerűbb elemzési módszereket tárgyaljuk, így a leíró statisztikák készítését, a szótár alapú elemzést, valamint érzelemelemzést. A kötet harmadik blokkját a mesterséges intelligencia alapú megközelítéseknek szenteljük, melynek során az olvasó a felügyelt és felügyelet nélküli tanulás fogalmával ismerkedhet meg. A felügyelet nélküli módszerek közül a topik-modellezést, szóbeágyazást és a szövegskálázás wordfish módszerét mutatjuk be, a felügyelt elemzések közül pedig az osztályozással foglalkozunk részletesebben. Végezetül kötetünket egy függelék zárja, melyben a kezdő RStudió felhasználóknak adunk gyakorlati iránymutatást a programfelülettel való megismerkedéshez, használatának elsajátításához. 1.2 Használati utasítás A könyv célja, hogy keresztmetszeti képet adjon a szövegbányászat R programnyelven használatos eszközeiről. A fejezetekben ezért a magyarázó szövegben maga az R kód is megtalálható, illetve láthatóak a lefuttatott kód eredményei. Az alábbi példában a sötét háttér az R környezetet jelöli, ahol az R kód betűtípusa is eltérő a főszövegétől. A kód eredményét pedig a #> kezdetű sorokba szedtük, ezzel szimulálva az R console ablakát. # példa R kód 1 + 1 #> [1] 2 Az egyes fejezetekben szereplő kódrészleteket egymás utáni sorrendben bemásolva és lefuttatva a saját R környezetünkben tudjuk reprodukálni a könyvben szereplő technikákat. A Függelékben részletesebben is foglalkozunk az R és az RStudio beállításaival, használatával. Az ajánlott R minimum verzió a 4.0.0, illetve az ajánlott minimum RStudio verzió az 1.4.0000.1 A könyvhöz tartozik egy HunMineR nevű R csomag is, amely tartalmazza az egyes fejezetekben használt összes adatbázist, így az adatbeviteli problémákat elkerülve lehet gyakorolni a szövegbányászatot. A könyv megjelenésekor a csomag még nem került be a központi R CRAN csomag repozitóriumába, hanem a poltextLAB GitHub repozitóriumából tölthető le. A könyvben szereplő ábrák nagy része a ggplot2 csomaggal készült a theme_set(theme_light()) opció beállításával a háttérben. Ez azt jelenti, hogy az ábrákat előállító kódok a theme_light() sort nem tartalmazzák, de a tényleges ábrán már megjelennek a tematikus elemek. Az egyes fejezetekben használt R csomagok listája és verziószáma a lenti táblázatban található. Fontos tudni, hogy a használt R csomagokat folyamatosan fejlesztik, ezért elképzelhető hogy eltérő verziószámú változatok esetén változhat a kód szintaxis. A könyvben használt R csomagok Csomagnév Verziószám dplyr 1.0.5 e1071 1.7.6 factoextra 1.0.7 gapminder 0.3.0 GGally 2.1.1 ggdendro 0.1.22 ggplot2 3.3.3 ggrepel 0.9.1 HunMineR 0.0.0.9000 igraph 1.2.6 kableExtra 1.3.4 knitr 1.33 lubridate 1.7.10 purrr 0.3.4 quanteda 3.0.0 quanteda.textmodels 0.9.4 quanteda.textplots 0.94 quanteda.textstats 0.94 readr 1.4.0 readtext 0.80 readxl 1.3.1 rvest 1.0.0 spacyr 1.2.1 stm 1.3.6 stringr 1.4.0 text2vec 0.6 tibble 3.1.1 tidyr 1.1.3 tidytext 0.3.1 topicmodels 0.2.12 1.3 A HunMineR használata A Windows rendszert használóknak először az installr csomagot kell telepíteni, majd annak segítségével letölteni az Rtools nevű programot (az OS X és Linux rendszerek esetében erre a lépésre nincs szükség). A lenti kód futtatásával ezek a lépések automatikusan megtörténnek. # az installr csomag letöltése és installálása install.packages("installr") # az Rtools.exe fájl letöltése és installálása installr::install.Rtools() Ezt követően a devtools csomagban található install_github paranccsal tudjuk telepíteni a HunMineR csomagot, a lenti kód lefuttatásával. # A devtools csomag letöltése és installálása install.packages("devtools") # A HunMineR csomag letöltése és installálása devtools::install_github("poltextlab/HunMineR") Ebben a fázisban a data függvénnyel tudjuk megnézni, hogy pontosan milyen adatbázisok szerepelnek a csomagban, illetve ugyanitt megtalálható az egyes adatbázisok részletes leírása. Ha egy adatbázisról szeretnénk többet megtudni, akkor a kiegészítő információkat ?adatbazis_neve megoldással tudjuk megnézni.2 # A HunMineR csomag betöltése library(HunMineR) # csomagban lévő adatok listázása data(package = "HunMineR") # A miniszterelnöki beszédek minta adatbázisának részletei ?data_miniszterelnokok 1.4 Egy bevezető példa A következőkben egy példát mutatunk be a szövegbányászat gyakorlati alkalmazására vonatkozóan, amely során Ady Endre és Petőfi Sándor összes verseinek szóhasználatát hasonlítjuk össze, az eredményekhez pedig szemléletes ábrázolást is készítünk. A jelen példában alkalmazott eljárásokat a későbbi fejezetekben részletesen is kifejtjük itt csupán szemléltetni kívánjuk velük, hogy milyen jellegű elemzéseket sajátíthat majd el az olvasó a könyv segítségével. Az R számos kiegészítő csomaggal rendelkezik, amelyek hasznos funkcióit úgy érhetjük el, ha telepítjük az őket tartalmazó csomagot. Ezt az install.packages paranccsal tehetjük meg, ha már korábban egyszer elvégeztük, akkor nem szükséges egy csomaggal megismételni. A lenti kódsorral a példánkhoz szükséges csomagokat telepíthetjük, de a könyvben használt többi csomagot is ezzel a módszerrel telepíthetjük. install.packages("readtext") install.packages("readr") install.packages("quanteda") install.packages("quanteda.textstats") install.packages("quanteda.textplots") install.packages("ggplot2") install.packages("dplyr") A library() paranccsal pedig betölteni tudjuk a már telepített csomagjainkat, ezt minden alkalommal el kell, hogy végezzük, hogyha ezek egy funkcióját kívánjuk használni. Itt láthatjuk, hogy a két csomag után a HunMineR betöltése is szükséges, viszont annak ellenére, hogy ugyanazzal a funkcióval hívjuk elő a HunMiner nem egy csomag, hanem egy repository (adattár) így a telepítése egy eltérő eljárással zajlik az előző alfejezetben leírtaknak megfelelően. library(readtext) library(readr) library(quanteda) library(quanteda.textstats) library(quanteda.textplots) library(ggplot2) library(dplyr) library(HunMineR) Ezt követően betöltjük a szükséges adatokat, jelen esetben a HunMiner adattárából. ady <- HunMineR::data_ady petofi <- HunMineR::data_petofi A betöltött adattáblák megjelennek az Rstudio environemnt fülében, ahol azt is láthatjuk, hogy 619 megfigyelésünk vagyis versünk van a Petőfi táblában, Ady esetében pedig 1309. Ahhoz, hogy adatokat használni tudjunk azokon először mindig úgynevezett tisztítási folyamatokat kell elvégeznünk, valamint át kell alakítanunk az adataink formátumát is. Az úgynevezett szövegtisztítási folyamatok a szövegeink előkészítését jelentik, ha kihagyjuk őket vagy nem végezzük el őket kellő alapossággal, akkor az eredményeink félrevezetőek lesznek. Az R-rel való kódolás során többféle formátumban is tárolhatjuk az adatainkat, illetve az adataink egyik formátumból másikba való átalakítására is van lehetőségünk. Az egyes formátumok az adatokat különböző elrendezésben tárolják, azért van szükségünk gyakran az adataink átalakítására, mivel az R-n belüli funkciókat, amelyekre szükségünk lesz csak specifikus formátumokon hajthatjuk végre. Első lépésként korpusz formátumba helyezzük az adatainkat, erre csupán azért van szükség mert a későbbiekben ebből a formátumból tudjuk majd őket, token formátumba helyezni. ady_corpus <- corpus(ady) petofi_corpus <- corpus(petofi) Ezt követően betölthetjük a HunMiner csomagból a stop szavainkat, ez egy szó lista, amelyet a szövegelőkészítés során elfogunk távolítani a korpuszból. stopszavak <- HunMineR::data_stopwords_extra A corpus formátumban lévő szövegeket nem csak token formátumba helyezzük a lenti kódsorokkal, hanem több szövegtisztító lépést is elvégzünk velük. A remove_punct funkció segítségével eltávolítjuk a mondatvégi írásjeleket, veszőket. A tokens_tolower() parancs segítségével minden szavunk betűjét kisbetűvé alakítjuk így a kódunk nem tesz különbséget ugyanazon szó két alternatív megjelenése között csupán azért, mert az egyik a mondat elején helyezkedik el. Végül pedig a tokens_remove() parancs segítségével eltávolíthatjuk a korábban betöltött stopszavakat a szövegeinkből. ady_tok <- tokens(ady_corpus, remove_punct = TRUE) %>% tokens_tolower() %>% tokens_remove(stopszavak) petofi_tok <- tokens(petofi_corpus, remove_punct = TRUE) %>% tokens_tolower() %>% tokens_remove(stopszavak) Ezt követően a token formátumban lévő adatainkat dfm formátumba helyezzük. ady_dfm <- dfm(ady_tok) petofi_dfm <- dfm(petofi_tok) Majd megnyírbáljuk a szavaink listáját: a dfm_trim() parancs segítségével beállíthatjuk, hogy milyen gyakorisággal megjelenő szavakat hagyjunk az adataink között. Vannak jó okok mind a túlságosan gyakran megjelenő és a túlságosan kevésszer megejelenő szavak eltávolítására is. Ha egy szó nagyon gyakran jelenik meg, akkor feltételezhető, hogy az nem segít a számunkra értelmezni az egyes dokumentumaink közötti különbséget, mivel nem különbözteti meg azokat. Ha pedig egy szó túlságosan kevésszer jelenik meg, akkor az lehet, hogy egy egyedülálló eset, amely nem adhat a dokumentumainkra általánosítható információt a számunkra. Jelen kód előírja, hogy ahhoz, hogy egy szó bekerüljün az összehasonlításunkba legalább 25-ször meg kell jelenie a szövegben. ady_dfm <- dfm_trim(ady_dfm, min_termfreq = 25) petofi_dfm <- dfm_trim(petofi_dfm, min_termfreq = 25) Végül pedig az R programon belül ábrázolhatjuk is az eredményeinket. Jelen esetben nem végeztünk el számítást, hanem a megtisztított szövegeink szavaiból generáltunk szófelhőket. A Petőfi versekből készült szófelhő: textplot_wordcloud(petofi_dfm, min_count = 25, color = "red") Ábra 1.1: Petőfi szófelhő Az Ady versekből készült szófelhő: textplot_wordcloud(ady_dfm, min_count = 25, color = "orange") Ábra 1.2: Ady szófelhő Az így kapott ábrákon az Ady és Petőfi versek szavait láthatjuk egy úgynevezett szófelhőben összegyűjtve. Annál nagyobb méretben jelenít meg az ábránk egy kifejezést, minél gyakrabban fordul az elő a szövegünkben, amely jelen esetben a két költő összes verse. A két szófelhőn látszódik, hogy jelentős az átfedés a felhasznált szavakkal, úgyhogy a folyamatot megismételhetjük, azzal a kiegészítéssel, hogy a szófelhő létrehozása előtt eltávolíjuk mindazokat a szavakat, amelyek jelen vannak a másik korpuszban, így jobban láthatjuk, hogy mely kifejezések különböztetik meg a költőket. Ehhez először kigyűjtjük a featnames() funkció segítségével a két dfm egyegedi szavait. ady_szavak <- featnames(ady_dfm) petofi_szavak <- featnames(petofi_dfm) Ezt követően pedig megismételjük a korábbi előkészítő műveleteket azzal a kiegészítéssel, hogy eltávolítjuk az előbb létrehozott két szólistát is. ady_tok <- tokens(ady_corpus, remove_punct = TRUE) %>% tokens_tolower() %>% tokens_remove(stopszavak) %>% tokens_remove(petofi_szavak) petofi_tok <- tokens(petofi_corpus, remove_punct = TRUE) %>% tokens_tolower() %>% tokens_remove(stopszavak) %>% tokens_remove(ady_szavak) ady_dfm <- dfm(ady_tok) %>% dfm_trim(min_termfreq = 25) petofi_dfm <- dfm(petofi_tok) %>% dfm_trim(min_termfreq = 25) Ha ezzel végeztünk, akkor ismét létrehozhatjuk a két szófelhőnket. textplot_wordcloud(petofi_dfm, min_count = 25, color = "red") Ábra 1.3: Petőfi szófelhő textplot_wordcloud(ady_dfm, min_count = 25, color = "orange") Ábra 1.4: Ady szófelhő Ugyan a szófelhő nem egy statisztikai számítás, amely alapján következtetéseket vonhatnánk le a vizsgált szövegekről, de egy hasznos és látványos ábrázolási módszer, illetve jelen esetben jól példázza az R egy jelentős előnyét, hogy a segítségével az eredményeinket gyorsan és egyszerűen tudjuk ábrázolni. 1.5 Köszönetnyilvánítás Jelen kötet az ELKH Társadalomtudományi Kutatóközpont poltextLAB szövegbányászati kutatócsoportja (http://poltextlab.com/) műhelyében készült. A kötet fejezetei Sebők Miklós, Ring Orsolya és Máté Ákos közös munkájának eredményei. Az Alapfogalmak, illetve a Szövegösszehasonlítás fejezetekben társszerző volt Székely Anna. A Bevezetésben, a Függelékben, és az Adatkezelés R-ben, az Osztályozás és felügyelt tanulás című fejezetekben Gelányi Péter hajtott végre nagyobb frissítéseket, valamint alakította ki a könyv interaktív ábráit. A kézirat a szerzők többéves oktatási gyakorlatára, a hallgatóktól kapott visszajelzésekre építve készült el. Köszönjük a Bibó Szakkollégiumban (2021), a Rajk Szakkollégiumban (2019–2021), valamint a Széchenyi Szakkollégiumban (2019) tartott féléves, valamint a Corvinus Egyetemen és a Társadalomtudományi Kutatóközpontban tartott rövidebb képzési alkalmak résztvevőinek visszajelzéseit. Köszönjük a projekt gyakornokainak, Czene-Joó Máténak, Kaló Eszternek, Meleg Andrásnak, Lovász Dorottyának, Nagy Orsolyának, valamint kutatás asszisztenseinek, Balázs Gergőnek, Gelányi Péternek és Lancsár Eszternek a kézirat végleges formába öntése sorn nyújtott segítséget. Külön köszönet illeti a Társadalomtudományi Kutatóközpont Comparative Agendas Project (https://cap.tk.hu/hu) kutatócsoportjának tagjait, kiemelten Boda Zsoltot, Molnár Csabát és Pokornyi Zsanettet a kötetben használt korpuszok sokéves előkészítéséért. Köszönettel tartozunk az egyes fejezetek alapjául szolgáló elemzések és publikációk társszerzőinek, Barczikay Tamásnak, Berki Tamásnak, Kacsuk Zoltánnak, Kubik Bálintnak, Molnár Csabának és Szabó Martina Katalinnak. Köszönjük Ballabás Dániel szakmai lektor hasznos megjegyzéseit, Fedinec Csilla nyelvi lektor alapos munkáját, valamint a Typotex Kiadó rugalmasságát és színvonalas közreműködését a könyv kiadásában! Végül, de nem utolsósorban hálásak vagyunk a kötet megvalósulásához támogatást nyújtó szervezeteknek és ösztöndíjaknak: az MTA Könyvkiadási Alapjának, a Társadalomtudományi Kutatóközpont Könyvtámogatási Alapjának, a Nemzeti Kutatási, Fejlesztési és Innovációs Hivatalnak (NKFIH FK 123907, NKFIH FK 129018), az MTA Bolyai János Kutatási Ösztöndíjának. A kötet alapjául szolgáló kutatást, amelyet a Társadalomtudományi Kutatóközpont valósított meg, az Innovációs és Technológiai Minisztérium és a Nemzeti Kutatási, Fejlesztési és Innovációs Hivatal támogatta a Mesterséges Intelligencia Nemzeti Laboratórium keretében. Az R Windows, OS X és Linux változatai itt érhetőek el: https://cloud.r-project.org/. Az RStudio pedig innen érhető el: https://www.rstudio.com/products/rstudio/download/.↩︎ Többek között az adat forrása, a változók részletes leírása, illetve az adatbázis mérete is megtalálható így.↩︎ "],["alapfogalmak.html", "2 Kulcsfogalmak 2.1 Big Data és társadalomtudomány 2.2 Fogalmi alapok 2.3 A szövegbányászat alapelvei", " 2 Kulcsfogalmak 2.1 Big Data és társadalomtudomány A szövegek géppel való feldolgozása és elemzése módszertanának számos megnevezése létezik. A szövegelemzés, kvantitatív szövegelemzés, szövegbányászat, természetes nyelvfeldolgozás, automatizált szövegelemzés, automatizált tartalomelemzés és hasonló fogalmak között nincs éles tartalami különbség. Ezek a kifejezések jellemzően ugyanarra az általánosabb kutatási irányra reflektálnak, csupán hangsúlybeli eltolódások vannak köztük, így gyakran szinonimaként is használják őket. A szövegek gépi feldolgozásával foglalkozó tudományág a Big Data forradalom részeként kezdett kialakulni, melyet az adatok egyre nagyobb és diverzebb tömegének elérhető és összegyűjthető jellege hívott életre. Ennek megfelelően az adattudomány számos különböző adatforrás, így képek, videók, hanganyagok, internetes keresési adatok, telefonok lokációs adatai és megannyi különböző információ feldolgozásával foglalkozik. A szöveg is egy az adatbányászat érdeklődési körébe tartozó számos adattípus közül, melynek elemzésére külön kutatási irány alakult ki. Mivel napjainkban minden másodpercben óriási mennyiségű szöveg keletkezik és válik hozzáférhetővé az interneten, egyre nagyobb az igény az ilyen jellegű források és az emberi nyelv automatizált feldolgozására. Ebből adódóan az elemzési eszköztár is egyre szélesebb körű és egyre szofisztikáltabb, így a tartalomelemzési és szövegbányászati ismeretekkel bíró elemzők számára rengeteg értékes információ kinyerhető. Ezért a szövegbányászat nemcsak a társadalomtudósok számára izgalmas kutatási irány, hanem gyakran hasznosítják üzleti célokra is. Gondoljunk például az online sajtótermékekre, az ezekhez kapcsolódó kommentekre vagy a politikusok beszédéire. Ezek mind-mind hatalmas mennyiségben rendelkezésre állnak, hasznosításukhoz azonban képesnek kell lenni ezeket a szövegeket összegyűjteni, megfelelő módon feldolgozni és kiértékelni. A könyv ebben is segítséget nyújt az Olvasónak. Mielőtt azonban az adatkezelés és az elemzés részleteire rátérnénk, érdemes végigvenni néhány elvi megfontolást, melyek nélkülözhetetlenek a leendő elemző számára az etikus, érvényes és eredményes szövegbányászati kutatások kivitelezéséhez. A nagy mennyiségben rendelkezésre álló szöveges források kiváló kutatási terepet kínálnak a társadalomtudósok számára megannyi vizsgálati kérdéshez, azonban fontos tisztában lenni azzal, hogy a mindenki által elérhető adatokat is meglehetősen körültekintően, etikai szempontok figyelembevételével kell használni. Egy másik szempont, amelyet érdemes szem előtt tartani, mielőtt az ember fejest ugrana az adatok végtelenjébe, a 3V elve: volume, velocity, variety vagyis az adatok mérete, a keletkezésük sebessége és azok változatossága (Brady 2019). Ezek mind olyan tulajdonságok, amelyek az adatelemzőt munkája során más (és sok esetben több vagy nagyobb) kihívások elé állítják, mint egy hagyományos statisztikai elemzés esetében. A szövegbányászati módszerek abban is eltérnek a hagyományos társadalomtudományi elemzésektől, hogy – az adattudományokba visszanyúló gyökerei miatt – jelentős teret nyit az induktív (empiricista) kutatások számára a deduktív szemlélettel szemben. A deduktív kutatásmódszertani megközelítés esetén a kutató előre meghatározza az alkalmazandó fogalomrendszert, és azokat az elvárásokat, amelyek teljesülése esetén sikeresnek tekinti az elemzést. Az adattudományban az ilyen megközelítés a felügyelt tanulási feladatokat jellemzi, vagyis azokat a feladatokat, ahol ismert az elvárt eredmény. Ilyen például egy osztályozási feladat, amikor újságcikkeket szeretnénk különböző témakörökbe besorolni. Ebben az esetben az adatok egy részét általában kézzel kategorizáljuk, és a gépi eljárás sikerességét ehhez viszonyítjuk. Mivel az ideális eredmény (osztálycímke) ismert, a gépi teljesítmény könnyen mérhető (például a pontosság, a gép által sikeresen kategorizált cikkek százalékában kifejezve). Az induktív megoldás esetében kevésbé egyértelmű a gépi eljárás teljesítményének mérése, hiszen a rejtett mintázatok feltárását várjuk az algoritmustól, emiatt nincsenek előre meghatározott eredmények sem, amelyekhez viszonyíthatjuk a teljesítményt. Az adattudományban az ilyen feladatokat hívják felügyelet nélküli tanulásnak. Ide tartozik a klaszterelemzés, vagy a topic modellezés, melynek esetén a kutató csak azt határozza meg, hány klasztert, hány témát szeretne kinyerni, a gép pedig létrehozza az egymáshoz leghasonlóbb csoportokat. Értelemszerűen itt a kutatói validálás jóval nagyobb hangsúlyt kap, mint a deduktív megközelítés esetében. Egy harmadik, középutas megoldás a megalapozott elmélet megközelítése, mely ötvözi az induktív és a deduktív módszer előnyeit. Ennek során a kutató kidolgoz egy laza elméleti keretet, melynek alapján elvégzi az elemzést, majd az eredményeket figyelembe véve finomít a fogalmi keretén, és újabb elemzést futtat, addig folytatva ezt az iterációt, amíg a kutatás eredményeit kielégítőnek nem találja. A szövegbányászati elemzéseket kategorizálhatjuk továbbá a gépi hozzájárulás mértéke szerint. Ennek megfelelően megkülönböztethetünk kézi, géppel támogatott és gépi eljárásokat. Mindhárom megközelítésnek megvan a maga előnye. A kézi megoldások esetén valószínűbb, hogy azt mérjük a szövegünkben, amit mérni szeretnénk (például bizonyos szakpolitikai tartalmat), ugyanakkor ez idő- és költségigényes. A gépi eljárások ezzel szemben költséghatékonyak és gyorsak, de fennáll a veszélye, hogy nem azt mérjük, amit eredetileg mérni szerettünk volna (ennek megállapításában ismét a validálás kap kulcsszerepet). Továbbá lehetséges kézzel támogatott gépi megoldások alkalmazása, ahol a humán és a gépi elemzés ideális arányának megtalálása jelenti a fő kihívást. 2.2 Fogalmi alapok Miután áttekintettük a szövegbányászatban használatos elméleti megközelítéseket, érdemes tisztázni a fogalmi alapokat is. A szövegbányászat szempontjából a szöveg is egy adat. Az elemzéshez használatos strukturált adathalmazt korpusznak nevezzük. A korpusz az összes szövegünket jelöli, ennek részegységei a dokumentumok. Ha például a Magyar Nemzet cikkeit kívánjuk elemezni, a kiválasztott időszak összes cikke lesz a teljes korpuszunk, az egyes cikkek pedig a dokumentumaink. Az elemzés mindig egy meghatározott tématerületre (domain-re) koncentrál. E tématerület utalhat a nyelvre, amelyen a szövegek íródtak, vagy a specifikus tartalomra, amelyet vizsgálunk, de mindenképpen meghatározza a szöveg szókészletével kapcsolatos várakozásainkat. Más lesz tehát a szóhasználat egy bulvárlap cikkeiben, mint egy tudományos szaklap cikkeiben, aminek elsősorban akkor van jelentősége, ha szótár alapú elemzéseket készítünk. A szótár alapú elemzések során olyan szószedeteket hozunk létre, amelyek segíthetnek a kutatásunk szempontjából érdekes témák vagy tartalmak azonosításában. Így például létrehozhatunk pozitív és negatív szótárakat, vagy a gazdasági és a külpolitikai témákhoz kapcsolódó szótárakat, melyek segíthetnek azonosítani, hogy adott dokumentum inkább gazdasági vagy inkább külpolitikai témákat tárgyal. Léteznek előre elkészített szótárak – angol nyelven például a Bing Liu által fejlesztett szótár egy jól ismert és széles körben alkalmazható példa (Liu 2010) –, azonban fontos fejben tartani, hogy a vizsgált téma specifikus nyelvezete jellemzően meghatározza azt, hogy egy-egy szótárba milyen kifejezéseknek kellene kerülniük. Már említettük, hogy egy szövegbányászati elemzés során a szöveg is adatként kezelendő. Tehát hasonló módon gondolhatunk az elemzendő szövegeinkre, mint egy statisztikai elemzésre szánt adatbázisra, annak csupán, reprezentációja tér el az utóbbitól. Tehát míg egy statisztikai elemzésre szánt táblázatban elsősorban számokat és adott esetben kategorikus változókat reprezentáló karakterláncokat (stringeket) – például „férfi”/„nő”, „falu”/„város” – találunk, addig a szöveges adatokban első ránézésre nem tűnik ki gépileg értelmezhető struktúra. Ahhoz, hogy a szövegeink a gépi elemzés számára feldolgozhatóvá váljanak, annak reprezentációját kell megváltoztatni, vagyis strukturálatlan adathalmazból strukturált adathalmazt kell létrehozni, melyet jellemzően a szövegek mátrixszá alakításával teszünk meg. A mátrixszá alakítás első hallásra bonyolult eljárás benyomását keltheti, azonban a gyakorlatban egy meglehetősen egyszerű transzformációról van szó, melynek eredményeként a szavakat számokkal reprezentáljuk. A könnyebb megértés érdekében vegyük az alábbi példát: tekintsük a három példamondatot a három elemzendő dokumentumnak, ezek összességét pedig a korpuszunknak. 1. Az Európai Unió 27 tagországának egyike Magyarország. 2. Magyarország 2004-ben csatlakozott az Európai Unióhoz. 3. Szlovákia, akárcsak Magyarország, 2004-ben lett ez Európai Unió tagja. A példamondatok dokumentum-kifejezés mátrixsza az alábbi táblázat szerint fog kinézni. Vegyük észre azt is, hogy több olyan kifejezés van, melyek csak ragozásukban térnek el: Unió, Unióhoz; tagja, tagjának. Ezeket a kifejezéseket a kutatói szándék függvényében azonos alakúra hozhatjuk, hogy egy egységként jelenjenek meg. Az elemzések többségében a szövegelőkészítés egyik kiinduló lépése a szótövesítés vagy a lemmatizálás, előbbi a szavak toldalékainak levágását jelöli, utóbbi a szavak szótári alakra való visszaalakítását. A ragozás eltávolítását illetően elöljáróban annyit érdemes megjegyezni, hogy az agglutináló, vagyis ragasztó nyelvek esetén, mint amilyen a magyar is, a toldalékok eltávolítása gyakran igen komoly kihívást jelent. Nem csak a toldalékok formája lehet igen sokféle, de az is előfordulhat, hogy a tőszó nem egyezik meg a toldalék levágásával keletkező szótővel. Ilyen például a vödröt kifejezés, melynek szótöve a „vödr”, de a nyelvtanilag helyes tőszó a „vödör”. Hasonlóan a majmok kifejezés esetén a szótő a „majm” lesz, míg a nyelvtanilag helyes tőszó a „majom”. Emiatt a toldalékok levágását a magyar nyelvű szövegek esetén megfelelő körültekintéssel kell végezni. Táblázat 2.1: Dokumentum-kifejezés mátrix a példamondatokból Dokumentum 27 2004-ben akárcsak az csatlakozott egyike Európai lett Magyarország Szlovákia tagja tagjának Unióhoz 1 1 0 0 1 0 1 1 0 1 0 0 1 0 2 0 1 0 1 1 0 1 0 1 0 0 0 1 3 0 1 1 1 0 0 1 1 1 1 1 0 0 A dokumentum-kifejezés mátrixban minden dokumentumot egy vektor (értsd: egy sor) reprezentál, az eltérő kifejezések pedig külön oszlopokat kapnak. Tehát a fenti példában minden dokumentumunk egy 14 elemű vektorként jelenik meg, amelynek elemei azt jelölik, hogy milyen gyakran szerepel az adott kifejezés a dokumentumban. A dokumentum-kifejezés mátrixok egy jellemző tulajdonsága, hogy igen nagy dimenziókkal rendelkezhetnek (értsd: sok sorral és sok oszloppal), hiszen minden kifejezést külön oszlopként reprezentálnak. Egy sok dokumentumból álló vagy egy témák tekintetében változatos korpusz esetében a kifejezés mátrix elemeinek jelentős része 0 lesz, hiszen számos olyan kifejezés fordul elő az egyes dokumentumokban, amelyek más dokumentumban nem szerepelnek. A sok nullát tartalmazó mátrixot hívjuk ritka mátrixnak. Az adatok jobb kezelhetősége érdekben a ritka mátrixot valamilyen dimenzióredukciós eljárással sűrű mátrixszá lehet alakítani (például a nagyon ritka kifejezések eltávolításával vagy valamilyen súlyozáson alapuló eljárással). 2.3 A szövegbányászat alapelvei A módszertani fogalmak tisztázásást követően néhány elméleti megfontolást osztanánk meg a Grimmer és Stewart (2013) által megfogalmazott alapelvek nyomán, melyek hasznos útravalóul szolgálhatnak a szövegbányászattal ismerkedő kutatók számára. 1. A szövegbányászat rossz, de hasznos Az emberi nyelv egy meglehetősen bonyolult rendszer, így egy szöveg jelentésének, érzelmi telítettségének különböző olvasók általi értelmezése meglehetősen eltérő lehet, így nem meglepő, hogy egy gép sok esetben csak korlátozott eredményeket képes felmutatni ezen feladatok teljesítésében. Ettől függetlenül nem elvitatható a szövegbányászati modellek hasznossága, hiszen olyan mennyiségű szöveg válik feldolgozhatóvá, ami gépi támogatás nélkül elképzelhetetlen lenne, mindemellett azonban nem lehet megfeledkezni a módszertan korlátairól sem. 2. A kvantitatív modellek kiegészítik az embert, nem helyettesítik azt A kvantitatív eszközökkel történő elemzés nem szünteti meg a szövegek elolvasásának szükségességét, hiszen egészen más információk kinyerését teszi lehetővé, mint egy kvalitatív megközelítés. Emiatt a kvantitatív szövegelemzés talán legfontosabb kihívása, hogy a kutató megtalálja a gépi és a humán erőforrások együttes hasznosításának legjobb módját. 3. Nincs legjobb modell Minden kutatáshoz meg kell találni a leginkább alkalmas modellt a kutatási kérdés, a rendelkezésre álló adatok és a kutatói szándék alapján. Gyakran különböző eljárások kombinálása vezethet egy specifikus probléma legjobb megoldásához. Azonban minden esetben az eredmények értékelésére kell támaszkodni, hogy megállapíthassuk egy modell teljesítményét adott problémára és szövegkorpuszra nézve. 4. Validálás, validálás, validálás! Mivel az automatizált szövegelemzés számos esetben jelentősen lecsökkenti az elemzéshez szükséges időt és energiát, csábító lehet a gondolat, hogy ezekhez a módszerekhez forduljon a kutató, ugyanakkor nem szabad elfelejteni, hogy az elemzés csupán a kezdeti lépés, hiszen a kutatónak validálnia kell az eredményeket ahhoz, hogy valóban megbízható következtetésekre jussunk. Az érvényesség-vizsgálat (validálás) lényege egy felügyelet nélküli modell esetén – ahol az elvárt eredmények nem ismertek, így a teljesítmény nem tesztelhető –, hogy meggyőződjünk: egy felügyelt modellel (olyan modellel, ahol az elvárt eredmény ismert, így ellenőrizhető) egyenértékű eredményeket hozzon. Ennek az elvárásnak a teljesítése gyakran nem egyszerű, azonban az eljárások alapos kiértékelést nélkülöző alkalmazása meglehetősen kétesélyes eredményekhez vezethet, emiatt érdemes megfelelő alapossággal eljárni az érvényesség-vizsgálat során. "],["adatkezeles.html", "3 Adatkezelés R-ben 3.1 Az adatok importálása 3.2 Az adatok exportálása 3.3 A pipe operátor 3.4 Műveletek adattáblákkal 3.5 Munka karakter vektorokkal6", " 3 Adatkezelés R-ben 3.1 Az adatok importálása Az adatok importálására az R alapfüggvénye mellett több csomag is megoldást kínál. Ezek közül a könyv írásakor a legnépszerűbbek a readr és a rio csomagok. A szövegek különböző karakterkódolásának problémáját tapasztalataink szerint a legjobban a readr csomag read_csv() függvénye kezeli, ezért legtöbbször ezt fogjuk használni a .csv állományok beolvasására. Amennyiben kihasználjuk az RStudio projekt opcióját (lásd a Függelékben) akkor elegendő csak az elérni kívánt adatok relatív elérési útját megadni (relative path). Ideális esetben az adataink egy csv fájlban vannak, ahol az egyes értékeket vesszők (vagy egyéb speciális karakterek) választják el. Ez esetben a read_delim() függvényt is használhatjuk. A beolvasásnál egyből el is tároljuk az adatokat egy objektumban. A sep = opcióval tudjuk a szeparátor karaktert beállítani, mert előfordulhat, hogy vessző helyett pontosvessző tagolja az adatainkat. library(readr) library(dplyr) library(gapminder) library(stringr) library(readtext) df <- readr::read_csv("data/adatfile.csv") Az R képes linkről letölteni fájlokat, elég megadnunk egy működő elérési útvonalat (lenti kódrészlet nem egy valódi linkre mutat, csupán egy példa). df_online <- read_csv("https://www.pelda_link.hu/adatok/pelda_file.csv") Az R csomag ökoszisztémája kellően változatos ahhoz, hogy gyakorlatilag bármilyen inputtal meg tudjon birkózni. Az Excel fájlokat a readxl csomagot használva tudjuk betölteni a read_excel() függvény használatával.lásd ehhez a Függeléket A leggyakoribb statisztikai programok formátumait pedig a haven csomag tudja kezelni (például Stata, Spss, SAS). A szintaxis itt is hasonló: read_stata(), read_spss(), read_sas(). A nagy mennyiségű szöveges dokumentum (a legyakrabban előforduló kiterjesztések: .txt, .doc, .pdf, .json, .csv, .xml, .rtf, .odt) betöltésére a legalkalmasabb a readtext csomag. Az alábbi példa azt mutatja be, hogyan tudjuk beolvasni egy adott mappából az összes .txt kiterjesztésű fájlt anélkül, hogy egyenként kellene megadnunk a fájlok neveit. A kódsorban szereplő * karakter ebben a környezetben azt jelenti, hogy bármilyen fájl az adott mappában, ami .txt-re végződik. Amennyiben a fájlok nevei tartalmaznak valamilyen metaadatot, akkor ezt is be tudjuk olvasni a betöltés során. Ilyen metaadat lehet például egy parlamenti felszólalásnál a felszólaló neve, a beszéd ideje, a felszólaló párttagsága (például: kovacsjanos_1994_fkgp.txt). df_text <- readtext::readtext( "data/*.txt", docvarsfrom = "filenames", dvsep = "_", docvarnames = c("nev", "ev", "part") ) 3.2 Az adatok exportálása Az adatainkat R-ből a write.csv()-vel exportálhatjuk a kívánt helyre, .csv formátumba. Az openxlsx csomaggal .xls és .xlsx Excel formátumokba is tudunk exportálni. Az R rendelkezik saját, .Rds és .Rda kiterjesztésű, tömörített fájlformátummal. Mivel ezeket csak az R-ben nyithatjuk meg, érdemes a köztes, hosszadalmas számítást igénylő lépések elmentésére használni, a saveRDS() és a save() parancsokkal. 3.3 A pipe operátor Az úgynevezett pipe operátor alapjaiban határozta meg a modern R fejlődését és a népszerű csomag ökoszisztéma, a tidyverse, egyik alapköve. Úgy gondoljuk, hogy a tidyverse és a pipe egyszerűbbé teszi az R használatának elsajátítását, ezért mi is erre helyezzük a hangsúlyt.3 Vizuálisan a pipe operátor így néz ki: %>% (a pipe operátor a Ctrl + Shift + M billentyúk kombinációjával könnyedén kiírható) és arra szolgál, hogy a kódban több egymáshoz kapcsolódó műveletet egybefűzzünk.4 Technikailag a pipe a bal oldali elemet adja meg a jobb oldali függvény első argumentumának. A lenti példa, amely nem tartalmaz igazi kódot, csupán a logikát kívánja szemlélteni ugyanazt a folyamatot írja le az alap R (base R), illetve a pipe használatával. 5 Miközben a kódot olvassuk, érdemes a pipe-ot „és aztán”-nak fordítani. reggeli(oltozkodes(felkeles(ebredes(en, idopont = "8:00"), oldal = "jobb"), nadrag = TRUE, ing = TRUE)) en %>% ebredes(idopont = "8:00") %>% felkeles(oldal = "jobb") %>% oltozkodes(nadrag = TRUE, ing = TRUE) %>% reggeli() A fenti példa is jól mutatja, hogy a pipe a bal oldali elemet fogja a jobb oldali függvény első elemének berakni. A pipe operátor működését még egy soron demonstrálhatjuk ez alkalommal valódi kódsorokkal és funkciókkal. Láthatjuk, hogy a pipe baloldalán elhelyezkedő kiindulópont egy számsor, ezt először a sum() funkcióba helyezzük, az így kapott eredmény 16 lesz, amelynek a gyökét vesszük az sqrt() az operátor alkalmazásával ezt a két műveletet egyetlen kódsorban könnyen olvasható módon végezzük el és megkapjuk a teljes folyamat eredeményét vagyis négyet. c(3, 5, 8) %>% sum() %>% sqrt() #> [1] 4 A fejezet további részeiben még bőven fogunk gyakorlati példát találni a pipe használatára. Mivel az itt bemutatott példák az alkalmazásoknak csak egy relatíve szűk körét mutatják be, érdemes átolvasni a csomagokhoz tartozó dokumentációt, illetve ha van, akkor tanulmányozni a működést demonstráló bemutató oldalakat is. 3.4 Műveletek adattáblákkal Az adattábla (data frame) az egyik leghasznosabb és leggyakrabban használt adattárolási mód az R-ben (a részletesebb leírás a Függelékben található). Ebben az alfejezetben azt mutatjuk be a dplyr és gapminder csomagok segítségével, hogyan lehet vele hatékonyan dolgozni. A dplyr az egyik legnépszerűbb R csomag, a tidyverse része. A gapminder csomag pedig a példa adatbázisunkat tartalmazza, amiben a világ országainak különböző gazdasági és társadalmi mutatói találhatók. A sorok (megfigyelések) szűréséhez a dplyr csomag filter() parancsát használva lehetőségünk van arra, hogy egy vagy több kritérium alapján szűkítsük az adatbázisunkat. A lenti példában azokat a megfigyeléseket tartjuk meg, ahol az év 1962 és a várható élettartam több mint 72 év. gapminder %>% dplyr::filter(year == 1962, lifeExp > 72) #> # A tibble: 5 × 6 #> country continent year lifeExp pop gdpPercap #> <fct> <fct> <int> <dbl> <int> <dbl> #> 1 Denmark Europe 1962 72.4 4646899 13583. #> 2 Iceland Europe 1962 73.7 182053 10350. #> 3 Netherlands Europe 1962 73.2 11805689 12791. #> 4 Norway Europe 1962 73.5 3638919 13450. #> 5 Sweden Europe 1962 73.4 7561588 12329. Ugyanígy leválogathatjuk az adattáblából az adatokat akkor is, ha egy karakter változó alapján szeretnénk szűrni. gapminder %>% filter(country == "Sweden", year > 1990) #> # A tibble: 4 × 6 #> country continent year lifeExp pop gdpPercap #> <fct> <fct> <int> <dbl> <int> <dbl> #> 1 Sweden Europe 1992 78.2 8718867 23880. #> 2 Sweden Europe 1997 79.4 8897619 25267. #> 3 Sweden Europe 2002 80.0 8954175 29342. #> 4 Sweden Europe 2007 80.9 9031088 33860. Itt tehát az adattábla azon sorait szeretnénk látni, ahol az ország megegyezik a „Sweden” karakterlánccal, az év pedig 1990 utáni. A select() függvény segítségével válogathatunk oszlopokat a data frame-ből. A változók kiválasztására több megoldás is van. A dplyr csomag tartalmaz apróbb kisegítő függvényeket, amik megkönnyítik a nagy adatbázisok esetén a változók kiválogatását a nevük alapján. Ezek a függvények a contains(), starts_with(), ends_with(), matches(), és beszédesen arra szolgálnak, hogy bizonyos nevű változókat ne kelljen egyenként felsorolni. A select()-en belüli változó sorrend egyben az eredmény data frame változójának sorrendjét is megadja. A negatív kiválasztás is lehetséges, ebben az esetben egy - jelet kell tennünk a nem kívánt változó(k) elé (pl.: select(df, year, country, -continent). gapminder %>% dplyr::select(dplyr::contains("ea"), dplyr::starts_with("co"), pop) #> # A tibble: 1,704 × 4 #> year country continent pop #> <int> <fct> <fct> <int> #> 1 1952 Afghanistan Asia 8425333 #> 2 1957 Afghanistan Asia 9240934 #> 3 1962 Afghanistan Asia 10267083 #> 4 1967 Afghanistan Asia 11537966 #> 5 1972 Afghanistan Asia 13079460 #> 6 1977 Afghanistan Asia 14880372 #> # ℹ 1,698 more rows Az így kiválogatott változókból létrehozhatunk és objektumként eltárolhatunk egy új adattáblát az objektumok részletesebb leírása a függelékben található Függelékben, amivel azután tovább dolgozhatunk, vagy kiírathatjuk például .csv fájlba, vagy elmenthetjük a saveRDS segítségével. gapminder_select <- gapminder %>% select(contains("ea"), starts_with("co"), pop) readr::write_csv(gapminder_select, "gapminder_select.csv") saveRDS(gapminder_select, "gapminder_select.Rds") A saveRDS segítségével elmentett fájlt később a readRDS() függvénnyel olvashatjuk be, majd onnan folytathatjuk a munkát, ahol korábban abbahagytuk. readRDS("gapminder_select.Rds") Az elemzési munkafolyamat elkerülhetetlen része, hogy új változókat hozzunk létre, vagy a meglévőket módosítsuk. Ezt a mutate()-el tehetjük meg, ahol a szintaxis a következő: mutate(data frame, uj valtozo = ertekek). Példaként kiszámoljuk a svéd GDP-t (milliárd dollárban) 1992-től kezdve. A mutate() alkalmazását részletesebben is bemutatjuk a szövegek előkészítésével foglalkozó fejezetben. gapminder %>% filter(country == "Sweden", year >= 1992) %>% dplyr::mutate(gdp = (gdpPercap * pop) / 10^9) #> # A tibble: 4 × 7 #> country continent year lifeExp pop gdpPercap gdp #> <fct> <fct> <int> <dbl> <int> <dbl> <dbl> #> 1 Sweden Europe 1992 78.2 8718867 23880. 208. #> 2 Sweden Europe 1997 79.4 8897619 25267. 225. #> 3 Sweden Europe 2002 80.0 8954175 29342. 263. #> 4 Sweden Europe 2007 80.9 9031088 33860. 306. Az adataink részletesebb és alaposabb megismerésében segítenek a különböző szintű leíró statisztikai adatok. A szintek megadására a group_by() használható, a csoportokon belüli számításokhoz pedig a summarize(). A lenti példa azt illusztrálja, hogy ha kontinensenként csoportosítjuk a gapminder adattáblát, akkor a summarise() használatával megkaphatjuk a megfigyelések számát, illetve az átlagos per capita GDP-t. A summarise() a mutate() közeli rokona, hasonló szintaxissal és logikával használható. Ezt a függvénypárost fogjuk majd használni a szöveges adataink leíró statisztikáinál is az 5. fejezetben. gapminder %>% dplyr::group_by(continent) %>% dplyr::summarise(megfigyelesek = n(), atlag_gdp = mean(gdpPercap)) #> # A tibble: 5 × 3 #> continent megfigyelesek atlag_gdp #> <fct> <int> <dbl> #> 1 Africa 624 2194. #> 2 Americas 300 7136. #> 3 Asia 396 7902. #> 4 Europe 360 14469. #> 5 Oceania 24 18622. 3.5 Munka karakter vektorokkal6 A szöveges adatokkal (karakter stringekkel) való munka elkerülhetetlen velejárója, a vektorokról, köztük a karakter vektorokról részletesebben a Függelékben írunk. A felesleges szövegelemeket, karaktereket el kell távolítanunk, hogy javuljon az elemzésünk hatásfoka. Erre a célra a stringr csomagot fogjuk használni, kombinálva a korábban bemutatott mutate()-el. A stringr függvények az str_ előtaggal kezdődnek és eléggé beszédes nevekkel rendelkeznek. Egy gyakran előforduló probléma, hogy extra szóközök maradnak a szövegben, vagy bizonyos szavakról, karakterkombinációkról tudjuk, hogy nem kellenek az elemzésünkhöz. Ebben az esetben egy vagy több reguláris kifejezés (regular expression, regex) használatával tudjuk pontosan kijelölni, hogy a karakter sornak melyik részét akarjuk módosítani.7 A legegyszerűbb formája a regexeknek, ha pontosan tudjuk milyen szöveget akarunk megtalálni. A kísérletezésre az str_view()-t használjuk, ami megjeleníti, hogy a megadott regex mintánk pontosan mit jelöl. A függvény match = TRUE paramétere lehetővé teszi, hogy csak a releváns találatokat kapjuk vissza. szoveg <- c("gitar", "ukulele", "nagybogo") stringr::str_view(szoveg, pattern = "ar") #> [1] │ git<ar> Reguláris kifejezésekkel rákereshetünk nem csak egyes elemekre (pl.: szavak, szótagok, betűk) hanem olyan konkrét esetekre is, amikor ezek egymás után fordulnak elő. Ilyenkor úgynevezett ngrammokat használunk, amelyek egy karakterláncban szereplő n-számú elem szekvenciája. A lenti példban ezeke működését egy úgynevezett bigrammal tehát egy n=2 értékű ngrammal mutatjuk be. Az ngrammok segítenek kezelni olyan eseteket, amikor két egymást követő elem eltérő jelentéssel bír, egymás mellett, mint külön-külön. szoveg <- c("a fehér ház fehérebb mint bármely más ház") stringr::str_view(szoveg, pattern = "fehér ház") #> [1] │ a <fehér ház> fehérebb mint bármely más ház Az ún. „horgonyokkal“, (anchor) azt lehet megadni, hogy a karakter string elején vagy végén szeretnénk-e egyezést találni. A string eleji anchor a ^, a string végi pedig a $. str_view("Dr. Doktor Dr.", pattern = "^Dr.") #> [1] │ <Dr.> Doktor Dr. str_view("Dr. Doktor Dr.", pattern = "Dr.$") #> [1] │ Dr. Doktor <Dr.> Továbbá azt is meghatározhatjuk, hogy egy adott karakter, vagy karakter kombinációt, valamint a mellett elhelyezkedő karaktereket is szeretnénk kijelölni. str_view("Dr. Doktor Dr.", pattern = ".k.") #> [1] │ Dr. D<okt>or Dr. Egy másik jellemző probléma, hogy olyan speciális karaktert akarunk leírni a regex kifejezésünkkel, ami amúgy a regex szintaxisban használt. Ilyen eset például a ., ami mint írásjel sokszor csak zaj, ám a regex kontextusban a „bármilyen karakter„ megfelelője. Ahhoz, hogy magát az írásjelet jelöljük, a \\\\ -t kell elé rakni, ennek egy alternatívája, hogy a keresett szóláncot a fixed() funkcióba helyezzük, így pedig a szóláncban lévő karakterek különleges jelentéseit figyelmen kívül hagyja és csak a magára az egzakt szóláncra keres rá. Ha rákeresünk a . karakterre, akkor látható, hogy a szólánc mindegyik karakterét kijelöltük, hiszen a . jelentése “bármilyen karakter”. str_view("Dr. Doktor Dr.", pattern = ".") #> [1] │ <D><r><.>< ><D><o><k><t><o><r>< ><D><r><.> A \\\\ jel segítségével már csak a szövegben lévő tényleges pontokra keresünk rá. str_view("Dr. Doktor Dr.", pattern = "\\\\.") #> [1] │ Dr<.> Doktor Dr<.> Ugyanúgy a fixed() funkció segítségével szintén csak a tényleges pontokra keresünk rá. str_view("Dr. Doktor Dr.", pattern = fixed(".")) #> [1] │ Dr<.> Doktor Dr<.> Néhány hasznos regex kifejezés: [:digit:] - számok (123) [:alpha:] - betűk (abc ABC) [:lower:] - kisbetűk (abc) [:upper:] - nagybetűk (ABC) [:alnum:] - betűk és számok (123 abc ABC) [:punct:] - központozás (.!?\\(){}) [:graph:] - betűk, számok és központozás (123 abc ABC .!?\\(){}) [:space:] - szóköz ( ) [:blank:] - szóköz és tabulálás [:cntrl:] - kontrol karakterek (\\n, \\r, stb.) * - bármi A tidyverse megközelítés miatt a kötetben szereplő R kód követi a “The tidyverse style guide” dokumentációt (https://style.tidyverse.org/)↩︎ Az RStudio-ban a pipe operátor billentyű kombinációja a Ctrl + Shift + M↩︎ Köszönjük Andrew Heissnek a kitűnő példát.↩︎ A könyv terjedelme miatt ezt a témát itt csak bemutatni tudjuk, de minden részletre kiterjedően nem tudunk elmélyülni benne. A témában nagyon jól használható online anyagok találhatóak az RStudio GitHub tárhelyén (https://github.com/rstudio/cheatsheets/raw/master/strings.pdf), illetve Wickham and Grolemund (2016) 14. fejezetében.↩︎ A reguláris kifejezés egy olyan, meghatározott szintaktikai szabályok szerint leírt karakterlánc (string), amivel meghatározható stringek egy adott halmaza. Az ilyen kifejezés valamilyen minta szerinti szöveg keresésére, cseréjére, illetve a szöveges adatok ellenőrzésére használható. További információ: http://www.regular-expressions.info/↩︎ "],["corpus_ch.html", "4 Korpuszépítés és szövegelőkészítés 4.1 Szövegbeszerzés 4.2 Szövegelőkészítés", " 4 Korpuszépítés és szövegelőkészítés 4.1 Szövegbeszerzés A szövegbányászati elemzések egyik első lépése az elemzés alapjául szolgáló korpusz megépítése. A korpuszt alkotó szövegek beszerzésének egyik módja a webscarping, melynek során weboldalakról történik az információ kinyerése. A scrapelést végezhetjük R-ben az rvest csomag segítségével. Fejezetünkben a scrapelésnek csupán néhány alaplépését mutatjuk meg.8 library(rvest) library(readr) library(dplyr) library(lubridate) library(stringr) library(quanteda) library(quanteda.textmodels) library(quanteda.textstats) library(HunMineR) A szükséges csomagok 9 beolvasása után a read_html() függvény segítségével az adott weboldal adatait kérjük le a szerverről. A read_html() függvény argumentuma az adott weblap URL-je. Ha például a poltextLAB projekt honlapjáról szeretnénk adatokat gyűjteni, azt az alábbi módon tehetjük meg: r <- rvest::read_html("https://poltextlab.tk.hu/hu") r #> {html_document} #> <html lang="hu" class="no-js"> #> [1] <head>\\n<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">\\n<meta charset="utf-8">\\n<meta http-equiv="X-UA-C ... #> [2] <body class="index">\\n\\n\\t<script>\\n\\t (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){\\n\\t (i[ ... Ezután a html_nodes() függvény argumentumaként meg kell adnunk azt a HTML címkét vagy CSS azonosítót, ami a legyűjteni kívánt elemeket azonosítja a weboldalon. [^websraping] Ezeket az azonosítókat az adott weboldal forráskódjának megtekintésével tudhatjuk meg, amire a különböző böngészők különböző lehetőségeket kínálnak. Majd a html_text() függvény segítségével megkapjuk azokat a szövegeket, amelyek az adott weblapon az adott azonosítóval rendelkeznek. Példánkban a https://poltextlab.tk.hu/hu weboldalról azokat az információkat szeretnénk kigyűjteni, amelyek az <title> címke alatt szerepelnek. title <- read_html("https://poltextlab.tk.hu/hu") %>% rvest::html_nodes("title") %>% rvest::html_text() title #> [1] "MTA TK Political and Legal Text Mining and Artificial Intelligence Laboratory (poltextLAB)" Ezután a kigyűjtött információkat kiírhatjuk egy csv fájlba. write_csv(title, "title.csv") A webscraping során az egyik nehézség, ha a weboldal letiltja az automatikus letöltést, ezt kivédhetjük például különböző böngészőbővítmények segítségével, illetve a fejléc (header) vagy a hálózati kliens (user agent) megváltoztatásával. De segíthet véletlenszerű kiszolgáló (proxy) vagy VPN szolgáltatás10 használata is, valamint ha az egyes kérések között időt hagyunk. Mielőtt egy weboldal tartalmának scrapelését elkezdenénk, fontos tájékozódni a hatályos szerzői jogi szabályozásokról. A weboldalakon legtöbbször a legyűjtött szövegekhez tartozó különböző metaadatok is szerepelnek (például egy parlamenti beszéd dátuma, az azt elmondó képviselő neve), melyeket érdemes a scarpelés során szintén összegyűjteni. A scrapelés során fontos figyelnünk arra, hogy később jól használható formában mentsük el az adatokat, például .csv,.json vagy .txt kiterjesztésekkel. A karakterkódolási problémák elkerülése érdekében érdemes UTF-8 vagy UTF-16-os kódolást alkalmazni, mivel ezek tartalmazzák a magyar nyelv ékezetes karaktereit is.11 Arra is van lehetőség, hogy az elemezni kívánt korpuszt papíron keletkezett, majd szkennelt és szükség szerint optikai karakterfelismerés (Optical Character Recognition – OCR) segítségével feldolgozott szövegekből építsük fel. Mivel azonban ezeket a feladatokat nem R-ben végezzük, ezekről itt nem szólunk bővebben. Az így beszerzett és .txt vagy .csv fájllá alakított szövegekből való korpuszépítés a következő lépésekben megegyezik a weboldalakról gyűjtött szövegekével. 4.2 Szövegelőkészítés Az elemzéshez vezető következő lépés a szövegelőkészítés, amit a szöveg tisztításával kell kezdenünk. A szövegtisztításnál mindig járjunk el körültekintően és az egyes lépéseket a kutatási kérdésünknek megfelelően tervezzük meg, a folyamat során pedig időnként végezzünk ellenőrzést, ezzel elkerülhetjük a kutatásunkhoz szükséges információk elvesztését. Miután az elemezni kívánt szövegeinket beszereztük, majd a Az adatok importálása alfejezetben leírtak szerint importáltuk, következhetnek az alapvető előfeldolgozási lépések, ezek közé tartozik például a scrapelés során a korpuszba került html címkék, számok és egyéb zajok (például a speciális karakterek, írásjelek) eltávolítása, valamint a kisbetűsítés, a tokenizálás, a szótövezés és a tiltólistás szavak eltávolítása, azaz stopszavazás. A stringr csomag segítségével először eltávolíthatjuk a felesleges html címkéket a korpuszból.12 Ehhez először létrehozzuk a text1 nevű objektumot, ami egy karaktervektorból áll. text1 <- c("MTA TK", "<font size='6'> Political and Legal Text Mining and Artificial Intelligence Laboratory (poltextLAB)") text1 #> [1] "MTA TK" #> [2] "<font size='6'> Political and Legal Text Mining and Artificial Intelligence Laboratory (poltextLAB)" Majd a str_replace_all()függvény segítségével eltávolítjuk két html címke közötti szövegrészt. Ehhez a függvény argumentumában létrehozunk egy regex kifejezést, aminek segítségével a függvény minden < > közötti szövegrészt üres karakterekre cserél. Ezután a str_to_lower()mindent kisbetűvé konvertál, majd a str_trim() eltávolítja a szóközöket a karakterláncok elejéről és végéről. text1 %>% stringr::str_replace_all(pattern = "<.*?>", replacement = "") %>% stringr::str_to_lower() %>% stringr::str_trim() #> [1] "mta tk" #> [2] "political and legal text mining and artificial intelligence laboratory (poltextlab)" 4.2.1 Tokenizálás, szótövezés, kisbetűsítés és a tiltólistás szavak eltávolítása Az előkészítés következő lépésében tokenizáljuk, azaz egységeire bontjuk az elemezni kívánt szöveget, így a tokenek az egyes szavakat vagy kifejezéseket fogják jelölni. Ennek eredményeként kapjuk meg az n-gramokat, amik a vizsgált egységek (számok, betűk, szavak, kifejezések) n-elemű sorozatát alkotják. A következőkben a „Példa az előkészítésre” mondatot bontjuk először tokenekre a tokens() függvénnyel, majd a tokeneket a tokens_tolower() segítségével kisbetűsítjük, a tokens_wordstem() függvénnyel pedig szótövezzük. Végezetül a quanteda csomagban található magyar nyelvű stopszótár segítségével, elvégezzük a tiltólistás szavak eltávolítását. Ehhez először létrehozzuk az sw elnevezésű karaktervektort a magyar stopszavakból. A head() függvény segítségével belenézhetünk a szótárba, és a console-ra kiírathatjuk a szótár első hat szavát. Végül a tokens_remove()segítségével eltávolítjuk a stopszavakat. text <- "Példa az elokészítésre" toks <- quanteda::tokens(text) toks <- quanteda::tokens_tolower(toks) toks <- quanteda::tokens_wordstem(toks) toks #> Tokens consisting of 1 document. #> text1 : #> [1] "példa" "az" "elokészítésr" sw <- quanteda::stopwords("hungarian") head(sw) #> [1] "a" "ahogy" "ahol" "aki" "akik" "akkor" quanteda::tokens_remove(toks, sw) #> Tokens consisting of 1 document. #> text1 : #> [1] "példa" "elokészítésr" Ezt követi a szótövezés (stemmelés) lépése, melynek során az alkalmazott szótövező algoritmus egyszerűen levágja a szavak összes toldalékát, a képzőket, a jelzőket és a ragokat. Szótövezés helyett alkalmazhatunk szótári alakra hozást is (lemmatizálás). A két eljárás közötti különbség abban rejlik, hogy a szótövezés során csupán eltávolítjuk a szavak toldalékként azonosított végződéseit, hogy ugyanannak a szónak különböző megjelenési formáit közös törzsre redukáljuk, míg a lemmatizálás esetében rögtön az értelmes, szótári formát kapjuk vissza. A két módszer közötti választás a kutatási kérdés alapján meghozott kutatói döntésen alapul (Grimmer and Stewart 2013). Az alábbi példában egyetlen szó különböző alakjainak szótári alakra hozásával szemléltetjük a lemmatizálás működését. Ehhez először a text1 nevű objektumban tároljuk a szótári alakra hozni kívánt szöveget, majd tokenizáljuk és eltávolítjuk a központozást. Ezután definiáljuk a megfelelő szótövet és azt, hogy mely szavak alakjait szeretnénk erre a szótőre egységesíteni, majd a rep() függvény segítségével a korábban zárójelben megadott kifejeztéseket az “elokeszites” lemmával helyettesítjük, azaz a korábban definiált szólakokat az általunk megadott szótári alakkal helyettesítjük. Hosszabb szövegek lemmatizálásához előre létrehozott szótárakat használhatunk, ilyen például a WordNet, ami magyar nyelven is elérhető.13 text1 <- "Példa az előkészítésre. Az előkészítést a szövetisztítással kell megkezdenünk. Az előkészített korpuszon elemzést végzünk" toks1 <- tokens(text1, remove_punct = TRUE) elokeszites <- c("előkészítésre", "előkészítést", "előkészített") lemma <- rep("előkészítés", length(elokeszites)) toks1 <- quanteda::tokens_replace(toks1, elokeszites, lemma, valuetype = "fixed") toks1 #> Tokens consisting of 1 document. #> text1 : #> [1] "Példa" "az" "előkészítés" "Az" "előkészítés" "a" #> [7] "szövetisztítással" "kell" "megkezdenünk" "Az" "előkészítés" "korpuszon" #> [ ... and 2 more ] A fenti text1 objektumban tárolt szöveg szótövezését az alábbiak szerint tudjuk elvégezni. Megvizsgálva az előkészítés különböző alakjainak lemmatizált és stemmelt változatát jól láthatjuk a két módszer közötti különbséget. text1 <- "Példa az előkészítésre. Az előkészítést a szövetisztítással kell megkezdenünk. Az előkészített korpuszon elemzést végzünk" toks2 <- tokens(text1, remove_punct = TRUE) toks2 <- tokens_wordstem(toks2) toks2 #> Tokens consisting of 1 document. #> text1 : #> [1] "Példa" "az" "előkészítésr" "Az" "előkészítést" "a" "szövetisztításs" #> [8] "kell" "megkezdenünk" "Az" "előkészített" "korpuszon" #> [ ... and 2 more ] 4.2.2 Dokumentum kifejezés mátrix (dtm, dfm) A szövegbányászati elemzések nagy részéhez szükségünk van arra, hogy a szövegeinkből dokumentum kifejezés mátrix-ot (Document Term Matrix – dtm vagy Document Feature Matrix – dfm) hozzunk létre.14 Ezzel a lépéssel alakítjuk a szövegeinket számokká, ami lehetővé teszi, hogy utána különböző statisztikai műveleteket végezzünk velük. A dokumentum kifejezés mátrix minden sora egy dokumentum, minden oszlopa egy kifejezés, az oszlopokban szereplő változók pedig megmutatják az egyes kifejezések számát az egyes dokumentumokban. A legtöbb dokumentum kifejezés mátrix ritka mátrix, mivel a legtöbb dokumentum és kifejezés párosítása nem történik meg: a kifejezések nagy része csak néhány dokumentumban szerepel, ezek értéke nulla lesz. Az alábbi példában három, egy-egy mondatos dokumentumon szemléltetjük a fentieket. A korábban megismert módon előkészítjük, azaz kisbetűsítjük, szótövezzük a dokumentumokat, eltávolítjuk a tiltólistás szavakat, majd létrehozzuk belőlük a dokumentum kifejezés mátrixot.15 text <- c( d1 = "Ez egy példa az elofeldolgozásra", d2 = "Egy másik lehetséges példa", d3 = "Ez pedig egy harmadik példa" ) dfm <- text %>% tokens %>% tokens_remove(pattern = stopwords("hungarian")) %>% tokens_tolower() %>% tokens_wordstem(language = "hungarian") %>% dfm() dfm #> Document-feature matrix of: 3 documents, 4 features (50.00% sparse) and 0 docvars. #> features #> docs péld elofeldolgozás lehetséges harmad #> d1 1 1 0 0 #> d2 1 0 1 0 #> d3 1 0 0 1 4.2.3 Súlyozás A dokumentum kifejezés mátrix lehet egy egyszerű bináris mátrix, ami csak azt az információt tartalmazza, hogy egy adott szó előfordul-e egy adott dokumentumban. Míg az egyszerű bináris mátrixban ugyanakkora súlya van egy szónak ha egyszer és ha tízszer szerepel, készíthetünk olyan mátrixot is, ahol egy szónak annál nagyobb a súlya egy dokumentumban, minél többször fordul elő. A szógyakoriság (term frequency – TF) szerint súlyozott mátrixnál azt is figyelembe vesszük, hogy az adott szó hány dokumentumban szerepel. Minél több dokumentumban szerepel egy szó, annál kisebb a jelentősége. Ilyen szavak például a névelők, amelyek sok dokumentumban előfordulnak ugyan, de nem sok tartalmi jelentőséggel bírnak. Két szó közül általában az a fontosabb, amelyik koncentráltan, kevés dokumentumban, de azokon belül nagy gyakorisággal fordul elő. A dokumentum gyakorisági érték (document frequency – DF) egy szó gyakoriságát jellemzi egy korpuszon belül. A súlyozási sémákban általában a dokumentum gyakorisági érték inverzével számolnak (inverse document frequency - IDF), ez a leggyakrabban használt TF-IDF súlyozás (term frequency & inverse document frequency - TF-IDF). Az így súlyozott TF mátrix egy-egy cellájában található érték azt mutatja, hogy egy adott szónak mekkora a jelentősége egy adott dokumentumban. A TF-IDF súlyozás értéke tehát magas azon szavak esetén, amelyek az adott dokumentumban gyakran fordulnak elő, míg a teljes korpuszban ritkán; alacsonyabb azon szavak esetén, amelyek az adott dokumentumban ritkábban, vagy a korpuszban gyakrabban fordulnak elő; és kicsi azon szavaknál, amelyek a korpusz lényegében összes dokumentumában előfordulnak (Tikk 2007, 33–37 o.) Az alábbiakban az 1999-es törvényszövegeken szemléltetjük, hogy egy 125 dokumentumból létrehozott mátrix segítségével milyen alapvető statisztikai műveleteket végezhetünk.16 A HunMineR csomagból tudjuk importálni a törvényeket. lawtext_df <- HunMineR::data_lawtext_1999 Majd az importált adatokból létrehozzuk a korpuszt lawtext_corpus néven. Ezt követi a dokumentum kifejezés mátrix kialakítása (mivel a quanteda csomaggal dolgozunk, dfm mátrixot hozunk létre), és ezzel egy lépésben elvégezzük az alapvető szövegtisztító lépéseket is. lawtext_corpus <- quanteda::corpus(lawtext_df) lawtext_dfm <- lawtext_corpus %>% tokens( remove_punct = TRUE, remove_symbols = TRUE, remove_numbers = TRUE ) %>% tokens_tolower() %>% tokens_remove(pattern = stopwords("hungarian")) %>% tokens_wordstem(language = "hungarian") %>% dfm() A topfeatures() függvény segítségével megnézhetjük a mátrix leggyakoribb szavait, a függvény argumentumában megadva a dokumentum kifejezés mátrix nevét és a kívánt kifejezésszámot. quanteda::topfeatures(lawtext_dfm, 15) #> the bekezdés of áll szerződő rendelkezés törvény to hely an személy #> 7942 5877 5666 4267 3620 3403 3312 3290 3114 3068 3048 #> év kiadás b ha #> 2975 2884 2831 2794 Mivel látható, hogy a szövegekben sok angol kifejezés is volt egy következő lépcsőben az angol stopszavakat is eltávolítjuk. lawtext_dfm_2 <- quanteda::dfm_remove(lawtext_dfm, pattern = stopwords("english")) Ezután megnézzük a leggyakoribb 15 kifejezést. topfeatures(lawtext_dfm_2, 15) #> bekezdés áll szerződő rendelkezés törvény hely személy év kiadás b #> 5877 4267 3620 3403 3312 3114 3048 2975 2884 2831 #> ha következő költségvetés működés eset #> 2794 2398 2376 2325 2070 A következő lépés, hogy TF-IDF súlyozású statisztikát készítünk, a dokumentum kifejezés mátrix alapján. Ehhez először létrehozzuk a lawtext_tfidf nevű objektumot, majd a textstat_frequency() függvény segítségével kilistázzuk annak első 10 elemét. lawtext_tfidf <- quanteda::dfm_tfidf(lawtext_dfm_2) quanteda.textstats::textstat_frequency(lawtext_tfidf, force = TRUE, n = 10) #> feature frequency rank docfreq group #> 1 felhalmozás 1452 1 7 all #> 2 szerződő 1349 2 53 all #> 3 shall 1291 3 14 all #> 4 kiadás 1280 4 45 all #> 5 költségvetés 1101 5 43 all #> 6 támogatás 1035 6 38 all #> 7 beruházás 942 7 22 all #> 8 contracting 894 8 7 all #> 9 articl 884 9 14 all #> 10 működés 741 10 60 all A folyamatról bővebb információ található például az alábbi oldalakon: https://cran.r-project.org/web/packages/rvest/rvest.pdf, https://rvest.tidyverse.org.↩︎ A quanteda csomagnak több kiegészítő csomagja van, amelyeknek a tartalmára a nevük utal. A könyvben a quanteda.textmodels, quanteda.textplots, quanteda.textstats csomagokat használjuk még, amelyek statisztikai és vizualizaciós függvényeket tartalmaznak.↩︎ A VPN (Virtual Private Network, azaz a virtuális magánhálózat) azt teszi lehetővé, hogy a felhasználók egy megosztott vagy nyilvános hálózaton keresztül úgy küldjenek és fogadjanak adatokat, mintha számítógépeik közvetlenül kapcsolódnának a helyi hálózathoz.↩︎ A karakterkódolással kapcsolatosan további hasznos információk találhatóak az alábbi oldalon: http://www.cs.bme.hu/~egmont/utf8.↩︎ A stringr csomag jól használható eszköztárat kínál a különböző karakterláncokkezeléséhez. Részletesebb leírása megtalálható: https://stringr.tidyverse.org/. A karakterláncokról bővebben: https://r4ds.had.co.nz/strings.html↩︎ WordNet: https://github.com/mmihaltz/huwn. A magyar nyelvű szövegek lemmatizálását elvégezhetjük a szövegek R-be való beolvasása előtt is a magyarlanc nyelvi elemző segítségével, melyről a Természetes-nyelv feldolgozás (NLP) és névelemfelismerés című fejezetben szólunk részletesebben.↩︎ A két mátrix csak a nevében különbözik, tartalmilag nem. A használni kívánt csomag leírásában mindig megtalálható, hogy dtm vagy dfm mátrix segítségével dolgozik-e. Mivel a könyvben általunk használt quanteda csomag dfm mátrixot használ, mi is ezt használjuk.↩︎ Bár a lenti mátrixok, amelyekben csak 0 és 1 szerepel, a bináris mátrix látszatát keltik, de valójában nem azok, ha egy-egy kifejezésből több is szerepelne a mondatban, a mátrixban 2, 3, 4 stb. szám lenne.↩︎ Az itt használt kódok az alábbiakon alapulnak: https://rdrr.io/cran/quanteda/man/dfm_weight.html, https://rdrr.io/cran/quanteda/man/dfm_tfidf.html. A példaként használt korpusz a Hungarian Comparative Agendas Project keretében készült adatbázis része: https://cap.tk.hu/torveny↩︎ "],["leiro_stat.html", "5 Leíró statisztika 5.1 Szövegek a vektortérben 5.2 Leíró statisztika 5.3 A szövegek lexikai diverzitása 5.4 Összehasonlítás20 5.5 A kulcsszavak kontextusa", " 5 Leíró statisztika 5.1 Szövegek a vektortérben A szövegbányászati feladatok két altípusa a keresés és a kinyerés. A keresés során olyan szövegeket keresünk, amelyekben egy adott kifejezés előfordul. A webes keresőprogramok egyik jellemző tevékenysége, az információ-visszakeresés (information retrieval) során például az a cél, hogy a korpuszból visszakeressük a kereső információigénye szempontjából releváns információkat, mely keresés alapulhat metaadatokon vagy teljes szöveges indexelésen (Russel and Norvig 2005, 742.o; Tikk 2007). Az információkinyerés (information extraction) esetén a cél, hogy a strukturálatlan szövegekből strukturált adatokat állítsunk elő. Azaz az információkinyerés során nem a felhasználó által keresett információt keressük meg és lokalizáljuk, hanem az adott kérdés szempontjából releváns információkat gyűjtjük ki a dokumentumokból. Az információkinyerés alternatív megoldása segítségével már képesek lehetünk a kifejezések közötti kapcsolatok elemzésére, tendenciák és minták felismerésére és az információk összekapcsolása révén új információk létrehozására, azaz a segítségével strukturálatlan szövegekből is előállíthatunk strukturált információkat (Kwartler 2017; Schütze, Manning, and Raghavan 2008; Tikk 2007, 63–81.o). A szövegbányászati vizsgálatok során folyó szövegek, azaz strukturálatlan vagy részben strukturált dokumentumok elemzésére kerül sor. Ezekből a kutatási kérdéseink szempontjából releváns, látens összefüggéseket nyerünk ki, amelyek már strukturált szerkezetűek. A dokumentumok reprezentálásának három legelterjedtebb módja a halmazelmélet alapú, az algebrai és a valószínűségi modell. A halmazelméleti modellek a dokumentumok hasonlóságát halmazelmélet, a valószínűségi modellek pedig feltételes valószínűségi becslés alapján határozzák meg. Az algebrai modellek a dokumentumokat vektorként vagy mátrixként ábrázolják és algebrai műveletek segítségével hasonlítják össze. A vektortérmodell sokdimenziós vektortérben ábrázolja a dokumentumokat, úgy, hogy a dokumentumokat vektorokkal reprezentálja, a vektortér dimenziói pedig a dokumentumok összességében előforduló egyedi szavak. A modell alkalmazása során azok a dokumentumok hasonlítanak egymásra, amelyeknek a szókészlete átfedi egymást, és a hasonlóság mértéke az átfedéssel arányos. A vektortérmodellben a dokumentumgyűjteményt a dokumentum-kifejezés mátrixszal reprezentáljuk, a mátrixban a sorok száma megegyezik a dokumentumok számával, az oszlopokat pedig a korpusz egyedi kifejezési alkotják. Az egyedi szavak összességét szótárnak nevezzük. Mivel mátrixban az egyedi szavak száma általában igen nagy, ezért a mátrix hatékony kezeléséhez annak mérete különböző eljárásokkal csökkenthető. Fontos tudni, hogy a dokumentumok vektortér reprezentációjában a szavak szövegen belüli sorrendjére és pozíciójára vonatkozó információ nem található meg (Russel and Norvig 2005, 742–44 o.; Kwartler 2017; Welbers, Van Atteveldt, and Benoit 2017). A vektortérmodellt szózsák (bag of words) modellnek is nevezzük, melynek segítségével a fent leírtak szerint az egyes szavak gyakoriságát vizsgálhatjuk meg egy adott korpuszon belül. 5.2 Leíró statisztika Fejezetünkben nyolc véletlenszerűen kiválasztott magyar miniszterelnöki beszéd vizsgálatát végezzük el,17 amihez az alábbi csomagokat használjuk: library(HunMineR) library(readtext) library(dplyr) library(lubridate) library(stringr) library(ggplot2) library(quanteda) library(quanteda.textstats) library(quanteda.textplots) library(GGally) library(ggdendro) library(tidytext) library(plotly) Első lépésben a Bevezetőben már ismertetett módon a HunMineR csomagból betöltjük a beszédeket. A glimpse() függvénnyel egy gyors pilltast vethetünk a betöltött adatokra. texts <- HunMineR::data_miniszterelnokok_raw dplyr::glimpse(texts) #> 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 glimpse funkció segítségével nem csak a sorok és oszlopok számát tekinthetjük meg, hanem az egyes oszlopok neveit is, amelyek alapján megállapíthatjuk, hogy milyen információkat tartalmaz ez az objektum. Az egyes beszédek dokumentum azonosítóját, azok szövegét, az évüket és végül a miniszterelnök nevét, aki elmondta az adott beszédet. Ezt követően az Adatkezelés R-ben című fejezetben ismertetett mutate() függvény használatával két csoportra osztjuk a beszédeket. Ehhez a pm nevű változót alkalmazzuk, amely az egyes miniszterelnökök neveit tartalmazza. Kialakítjuk a két csoportot, azaz az if_else() segítségével meghatározzuk, hogy ha „antall_jozsef”, „boross_peter”, „orban_viktor” beszédeiről van szó azokat a jobb csoportba tegye, a maradékot pedig a bal csoportba. Ezután a glimpse() függvény segítségével megtekintjük, hogy milyen változtatásokat végeztünk az adattáblánkon. Láthatjuk, hogy míg korábban 7 dokumentumunk és 4 változónk volt, az átalakítás eredményeként a 7 dokumentum mellett már 5 változót találunk. Ezzel a lépéssel tehát kialakítottuk azokat a változókat, amelyekre az elemzés során szükségünk lesz. jobboldali_miniszterelnokok <- c("antall_jozsef", "boross_peter", "orban_viktor") texts <- texts %>% mutate( partoldal = dplyr::if_else(pm %in% jobboldali_miniszterelnokok, "jobb", "bal") ) glimpse(texts) #> Rows: 7 #> Columns: 5 #> $ doc_id <chr> "antall_jozsef_1990", "bajnai_gordon_2009", "gyurcsány_ferenc_2005", "horn_gyula_1994", "medgyessy_peter_2002", … #> $ text <chr> "Elnök Úr! Tisztelt Országgyulés! Hölgyeim és Uraim! Honfitársaim! Ünnepi pillanatban állok a magyar Országgyulé… #> $ 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_vi… #> $ partoldal <chr> "jobb", "bal", "bal", "bal", "bal", "jobb", "jobb" Ezt követően a további lépések elvégzéséhez létrehozzuk a quanteda korpuszt, majd a summary() függvény segítségével megtekinthetjük a korpusz alapvető statisztikai jellemzőit. Láthatjuk például, hogy az egyes dokumentumok hány tokenből vagy mondatból állnak. corpus_mineln <- corpus(texts) summary(corpus_mineln) #> Corpus consisting of 7 documents, showing 7 documents: #> #> Text Types Tokens Sentences year pm partoldal #> antall_jozsef_1990 3745 9408 431 1990 antall_jozsef jobb #> bajnai_gordon_2009 1391 3277 201 2009 bajnai_gordon bal #> gyurcsány_ferenc_2005 2963 10267 454 2005 gyurcsány_ferenc bal #> horn_gyula_1994 1704 4372 226 1994 horn_gyula bal #> medgyessy_peter_2002 1021 2362 82 2002 medgyessy_peter bal #> orban_viktor_1998 1810 4287 212 1995 orban_viktor jobb #> orban_viktor_2018 933 1976 126 2018 orban_viktor jobb Mivel az elemzés során a korpuszon belül két csoportra osztva szeretnénk összehasonlításokat tenni, az alábbiakban két alkorpuszt alakítunk ki. mineln_jobb <- quanteda::corpus_subset(corpus_mineln, pm %in% c("antall_jozsef", "boross_peter", "orban_viktor")) mineln_bal <- quanteda::corpus_subset(corpus_mineln, pm %in% c("horn_gyula", "gyurcsany_ferenc", "medgyessy_peter", "bajnai_gordon")) summary(mineln_jobb) #> Corpus consisting of 3 documents, showing 3 documents: #> #> Text Types Tokens Sentences year pm partoldal #> antall_jozsef_1990 3745 9408 431 1990 antall_jozsef jobb #> orban_viktor_1998 1810 4287 212 1995 orban_viktor jobb #> orban_viktor_2018 933 1976 126 2018 orban_viktor jobb summary(mineln_bal) #> Corpus consisting of 3 documents, showing 3 documents: #> #> Text Types Tokens Sentences year pm partoldal #> bajnai_gordon_2009 1391 3277 201 2009 bajnai_gordon bal #> horn_gyula_1994 1704 4372 226 1994 horn_gyula bal #> medgyessy_peter_2002 1021 2362 82 2002 medgyessy_peter bal A korábban létrehozott „jobb” és „bal” változó segítségével nem csak az egyes dokumentumokat, hanem a két csoportba sorolt beszédeket is összehasonlíthatjuk egymással. summary(corpus_mineln) %>% group_by(partoldal) %>% summarise( mean_wordcount = mean(Tokens), std_dev = sd(Tokens), min_wordc = min(Tokens), max_wordc = max(Tokens) ) #> # A tibble: 2 × 5 #> partoldal mean_wordcount std_dev min_wordc max_wordc #> <chr> <dbl> <dbl> <int> <int> #> 1 bal 5070. 3561. 2362 10267 #> 2 jobb 4710. 3271. 1976 9408 A textstat_collocations() függvény segítségével szókapcsolatokat kereshetünk. A függvény argumentumai közül a size a szókapcsolatok hossza, a min_count pedig a minimális előfordulásuk száma. Miután a szókapcsolatokat megkerestük, közülük a korábban már megismert head() függvény segítségével tetszőleges számút megnézhetünk.18 corpus_mineln %>% quanteda.textstats::textstat_collocations( size = 3, min_count = 6 ) %>% head(n = 10) #> collocation count count_nested length lambda z #> 1 a kormány a 21 0 3 1.510 2.996 #> 2 az új kormány 10 0 3 4.092 2.571 #> 3 az a politika 6 0 3 3.849 2.418 #> 4 a száz lépés 9 0 3 3.245 1.438 #> 5 a magyar gazdaság 12 0 3 2.199 1.374 #> 6 tisztelt hölgyeim és 31 0 3 3.290 1.266 #> 7 ez a program 8 0 3 1.683 1.107 #> 8 hogy ez a 10 0 3 0.530 1.024 #> 9 hogy a magyar 18 0 3 0.756 0.855 #> 10 az ellenzéki pártok 6 0 3 1.434 0.784 A szókapcsolatok listázásánál is láthattuk, hogy a korpuszunk még minden szót tartalmaz, ezért találtunk például „hogy ez a” összetételt. A következőkben eltávolítjuk az ilyen funkció nélküli stopszavakat a korpuszból, amihez saját stopszólistát használunk. Először a HunMineR csomagból beolvassuk és egy custom_stopwords nevű objektumban tároljuk a stopszavakat, majd a tokens() függvény segítségével tokenizáljuk a korpuszt és a tokens_select() használatával eltávolítjuk a stopszavakat. Ha ezután újra megnézzük a kollokációkat, jól látható a stopszavak eltávolításának eredménye: custom_stopwords <- HunMineR::data_stopwords_extra corpus_mineln %>% tokens() %>% tokens_select(pattern = custom_stopwords, selection = "remove") %>% textstat_collocations( size = 3, min_count = 6 ) %>% head(n = 10) #> collocation count count_nested length lambda z #> 1 taps MSZP soraiból 7 0 3 -1.72 -0.932 #> 2 taps kormánypártok soraiban 13 0 3 -1.75 -1.025 #> 3 tisztelt hölgyeim uraim 31 0 3 -3.14 -1.062 #> 4 közbeszólás fidesz soraiból 12 0 3 -4.24 -1.891 #> 5 taps MSZP soraiban 9 0 3 -4.58 -2.702 A korpusz további elemzése előtt fontos, hogy ne csak a stopszavakat távolítsuk el, hanem az egyéb alapvető szövegtisztító lépéseket is elvégezzük. Azaz a tokens_select() segítségével eltávolítsuk a számokat, a központozást, az elválasztó karaktereket, mint például a szóközöket, tabulátorokat, sortöréseket. Ezután a tokens_ngrams() segítségével n-gramokat (n elemű karakterláncokat) hozunk létre a tokenekből, majd kialakítjuk a dokumentum kifejezés mátrixot (dfm) és elvégezzük a tf-idf szerinti súlyozást. A dfm_tfidf() függvény kiszámolja a dokumentum gyakoriság inverz súlyozását, azok a szavak, amelyek egy dokumentumban gyakran jelennek meg nagyobb súly kapnak, de ha a korpusz egészében jelennek meg gyakran, tehát több dokumentumban is jelen van nagy számban, akkor egy kissebb súlyt kap. A függvény alapértelmezés szerint a normalizált kifejezések gyakoriságát használja a dokumentumon belüli relatív kifejezés gyakoriság helyett, ezt írjuk felül a schem_tf = \"prop\" használatával. Végül a textstat_frequency() segítségével gyakorisági statisztikát készíthetünk a korábban meghatározott (példánkban két és három tagú) n-gramokról. corpus_mineln %>% tokens( remove_numbers = TRUE, remove_punct = TRUE, remove_separators = TRUE ) %>% tokens_select(pattern = custom_stopwords, selection = "remove") %>% quanteda::tokens_ngrams(n = 2:3) %>% dfm() %>% dfm_tfidf(scheme_tf = "prop") %>% quanteda.textstats::textstat_frequency(n = 10, force = TRUE) #> feature frequency rank docfreq group #> 1 fordítsanak_hátat 0.00228 1 1 all #> 2 tisztelt_hölgyeim 0.00225 2 4 all #> 3 tisztelt_hölgyeim_uraim 0.00225 2 4 all #> 4 fidesz_soraiból 0.00201 4 1 all #> 5 taps_mszp 0.00159 5 2 all #> 6 magyarország_európa 0.00136 6 1 all #> 7 tisztelt_képviselőtársaim 0.00130 7 2 all #> 8 kormánypártok_soraiban 0.00129 8 2 all #> 9 taps_kormánypártok 0.00117 9 2 all #> 10 taps_kormánypártok_soraiban 0.00117 9 2 all 5.3 A szövegek lexikai diverzitása Az alábbiakban a korpuszunkat alkotó szövegek lexikai diverzitását vizsgáljuk. Ehhez a quanteda csomag textstat_lexdiv() függvényét használjuk. Először a corpus_mineln nevű korpuszunkból létrehozzuk a mineln_dfm nevű dokumentum-kifejezés mátrixot, amelyen elvégezzük a korábban már megismert alapvető tisztító lépéseket. A textstat_lexdiv() függvény eredménye szintén egy dfm, így azt arrange() parancs argumentumában a desc megadásával csökkenő sorba is rendezhetjük. Atextstat_lexdiv() függvény segítségével 13 különböző mérőszámot alkalmazhatunk, amelyek mind az egyes szövegek lexikai különbözőségét írják le.19 mineln_dfm <- corpus_mineln %>% tokens( remove_punct = TRUE, remove_separators = TRUE, split_hyphens = TRUE ) %>% dfm() %>% quanteda::dfm_remove(pattern = custom_stopwords) mineln_dfm %>% quanteda.textstats::textstat_lexdiv(measure = "CTTR") %>% dplyr::arrange(dplyr::desc(CTTR)) #> document CTTR #> 1 antall_jozsef_1990 33.0 #> 2 gyurcsány_ferenc_2005 26.1 #> 3 orban_viktor_1998 23.4 #> 4 horn_gyula_1994 22.3 #> 5 bajnai_gordon_2009 19.9 #> 6 medgyessy_peter_2002 16.8 #> 7 orban_viktor_2018 16.2 A megkapott értékeket hozzáadhatjuk a dfm-hez is. A lenti kód egy dfm_lexdiv nevű adattáblát hoz létre, amely tartalmazza a mineln_dfm adattábla sorait, valamint a lexikai diverzitás értékeket. dfm_lexdiv <- mineln_dfm cttr_score <- unlist(textstat_lexdiv(dfm_lexdiv, measure = "CTTR")[, 2]) quanteda::docvars(dfm_lexdiv, "cttr") <- cttr_score docvars(dfm_lexdiv) #> year pm partoldal cttr #> 1 1990 antall_jozsef jobb 33.0 #> 2 2009 bajnai_gordon bal 19.9 #> 3 2005 gyurcsány_ferenc bal 26.1 #> 4 1994 horn_gyula bal 22.3 #> 5 2002 medgyessy_peter bal 16.8 #> 6 1995 orban_viktor jobb 23.4 #> 7 2018 orban_viktor jobb 16.2 A fenti elemzést elvégezhetjük úgy is, hogy valamennyi indexálást egyben megkapjuk. Ehhez a textstat_lexdiv() függvény argumentumába a measure = \"all\" kifejezést kell megadnunk. mineln_dfm %>% textstat_lexdiv(measure = "all") #> document TTR C R CTTR U S K I D Vm Maas lgV0 lgeV0 #> 1 antall_jozsef_1990 0.647 0.949 46.7 33.0 72.9 0.960 9.29 419 0.000930 0.0287 0.117 11.19 25.8 #> 2 bajnai_gordon_2009 0.728 0.957 28.2 19.9 73.2 0.962 9.73 459 0.000974 0.0269 0.117 10.43 24.0 #> 3 gyurcsány_ferenc_2005 0.556 0.930 37.0 26.1 52.2 0.944 9.60 292 0.000960 0.0279 0.138 9.23 21.3 #> 4 horn_gyula_1994 0.714 0.956 31.5 22.3 74.0 0.962 7.45 572 0.000745 0.0232 0.116 10.66 24.5 #> 5 medgyessy_peter_2002 0.711 0.951 23.8 16.8 62.8 0.955 17.54 251 0.001755 0.0373 0.126 9.42 21.7 #> 6 orban_viktor_1998 0.722 0.957 33.0 23.4 78.1 0.964 11.72 400 0.001172 0.0314 0.113 11.02 25.4 #> 7 orban_viktor_2018 0.753 0.958 23.0 16.2 71.5 0.961 15.44 313 0.001545 0.0345 0.118 9.98 23.0 Ha pedig arra vagyunk kíváncsiak, hogy a kapott értékek hogyan korrelálnak egymással, azt a cor() függvény segítésével számolhatjuk ki. div_df <- mineln_dfm %>% textstat_lexdiv(measure = "all") cor(div_df[, 2:13]) #> TTR C R CTTR U S K I D Vm Maas lgV0 #> TTR 1.000 0.970 -0.6532 -0.6532 0.7504 0.8470 0.3835 0.257 0.3839 0.2773 -0.7812 0.332 #> C 0.970 1.000 -0.4521 -0.4521 0.8838 0.9498 0.2257 0.402 0.2261 0.1545 -0.9076 0.549 #> R -0.653 -0.452 1.0000 1.0000 -0.0157 -0.1587 -0.6552 0.260 -0.6556 -0.4803 0.0505 0.478 #> CTTR -0.653 -0.452 1.0000 1.0000 -0.0157 -0.1587 -0.6552 0.260 -0.6556 -0.4803 0.0505 0.478 #> U 0.750 0.884 -0.0157 -0.0157 1.0000 0.9835 -0.1556 0.636 -0.1554 -0.1495 -0.9969 0.869 #> S 0.847 0.950 -0.1587 -0.1587 0.9835 1.0000 -0.0197 0.571 -0.0194 -0.0415 -0.9932 0.782 #> K 0.383 0.226 -0.6552 -0.6552 -0.1556 -0.0197 1.0000 -0.772 1.0000 0.9722 0.1099 -0.482 #> I 0.257 0.402 0.2602 0.2602 0.6361 0.5714 -0.7725 1.000 -0.7722 -0.8265 -0.6170 0.709 #> D 0.384 0.226 -0.6556 -0.6556 -0.1554 -0.0194 1.0000 -0.772 1.0000 0.9721 0.1097 -0.482 #> Vm 0.277 0.154 -0.4803 -0.4803 -0.1495 -0.0415 0.9722 -0.826 0.9721 1.0000 0.1122 -0.393 #> Maas -0.781 -0.908 0.0505 0.0505 -0.9969 -0.9932 0.1099 -0.617 0.1097 0.1122 1.0000 -0.848 #> lgV0 0.332 0.549 0.4784 0.4784 0.8692 0.7822 -0.4815 0.709 -0.4815 -0.3931 -0.8479 1.000 A kapott értékeket a ggcorr() függvény segítségével ábrázolhatjuk is. Ha a függvény argumentumában a label = TRUE szerepel, a kapott ábrán a kiszámított értékek is láthatók (ld. 5.1. ábra). GGally::ggcorr(div_df[, 2:13], label = TRUE) Ábra 5.1: Korrelációs hőtérkép Az így kapott ábránk egy korrelációs hőtérkép, az oszlopok tetején elhelyezkedő rövidítések az egyes mérőszámokat jelentik, amelyekkel a beszédeket vizsgáltuk ezek képlete megtalálható a textstat lexdiv funkció oldalán. Ezek keresztmetszetében a számok az ábrázolják, hogy az egyes mérőszámok eredményei milyen kapcsolatban állnak egymással. Ahogy az ábra melletti skála is jelzi a piros négyzetben lévő számok pozitív korrelációt jeleznek, a kékben lévők pedig negatívat, minél halványabb egy adott négyzet színezése a korreláció mértéke annál kisebb. Ezt követően azt is megvizsgálhatjuk, hogy a korpusz szövegei mennyire könnyen olvashatóak. Ehhez a Flesch.Kincaid pontszámot használjuk, ami a szavak és a mondatok hossza alapján határozza meg a szöveg olvashatóságát. Ehhez a textstat_readability() függvényt használjuk, mely a korpuszunkat elemzi. quanteda.textstats::textstat_readability(x = corpus_mineln, measure = "Flesch.Kincaid") #> document Flesch.Kincaid #> 1 antall_jozsef_1990 16.5 #> 2 bajnai_gordon_2009 10.9 #> 3 gyurcsány_ferenc_2005 13.6 #> 4 horn_gyula_1994 13.8 #> 5 medgyessy_peter_2002 15.8 #> 6 orban_viktor_1998 13.0 #> 7 orban_viktor_2018 11.4 Ezután a kiszámított értékkel kiegészítjük a korpuszt. docvars(corpus_mineln, "f_k") <- textstat_readability(corpus_mineln, measure = "Flesch.Kincaid")[, 2] docvars(corpus_mineln) #> year pm partoldal f_k #> 1 1990 antall_jozsef jobb 16.5 #> 2 2009 bajnai_gordon bal 10.9 #> 3 2005 gyurcsány_ferenc bal 13.6 #> 4 1994 horn_gyula bal 13.8 #> 5 2002 medgyessy_peter bal 15.8 #> 6 1995 orban_viktor jobb 13.0 #> 7 2018 orban_viktor jobb 11.4 Majd a ggplot2 segítségével vizualizálhatjuk az eredményt (ld. ??. ábra). Ehhez az olvashatósági pontszámmal kiegészített korpuszból egy adattáblát alakítunk ki, majd beállítjuk az ábrázolás paramétereit. Az ábra két tengelyén az év, illetve az olvashatósági pontszám szerepel, a jobb- és a baloldalt a vonal típusa különbözteti meg, az egyes dokumentumokat ponttal jelöljük, az ábrára pedig felíratjuk a miniszterelnökök neveit, valamint azt is beállítjuk, hogy az x tengely beosztása az egyes beszédek dátumához igazodjon. A theme_minimal() függvénnyel pedig azt határozzuk meg, hogy mindez fehér hátteret kapjon. Az így létrehozott ábránkat a ggplotly parancs segítségével pedig interaktívvá is tehetjük. corpus_df <- docvars(corpus_mineln) mineln_df <- ggplot(corpus_df, aes(year, f_k)) + geom_point(size = 2) + geom_line(aes(linetype = partoldal), size = 1) + geom_text(aes(label = pm), color = "black", nudge_y = 0.15) + scale_x_continuous(limits = c(1988, 2020)) + labs( x = NULL, y = "Flesch-Kincaid index", color = NULL, linetype = NULL ) + theme_minimal() + theme(legend.position = "bottom") ggplotly(mineln_df) Ábra 5.2: Az olvashatósági index alakulása 5.4 Összehasonlítás20 A fentiekben láthattuk az eltéréseket a jobb és a bal oldali beszédeken belül, sőt ugyanahhoz a miniszterelnökhöz tartozó két beszéd között is. A következőkben textstat_dist() és textstat_simil() függvények segítségével megvizsgáljuk, valójában mennyire hasonlítanak vagy különböznek ezek a beszédek. Mindkét függvény bemenete dmf, melyből először egy súlyozott dfm-et készítünk, majd elvégezzük az összehasonlítást először a jaccard-féle hasonlóság alapján. mineln_dfm %>% dfm_weight("prop") %>% quanteda.textstats::textstat_simil(margin = "documents", method = "jaccard") #> textstat_simil object; method = "jaccard" #> antall_jozsef_1990 bajnai_gordon_2009 gyurcsány_ferenc_2005 horn_gyula_1994 medgyessy_peter_2002 #> antall_jozsef_1990 1.0000 0.0559 0.0798 0.0694 0.0404 #> bajnai_gordon_2009 0.0559 1.0000 0.0850 0.0592 0.0690 #> gyurcsány_ferenc_2005 0.0798 0.0850 1.0000 0.0683 0.0684 #> horn_gyula_1994 0.0694 0.0592 0.0683 1.0000 0.0587 #> medgyessy_peter_2002 0.0404 0.0690 0.0684 0.0587 1.0000 #> orban_viktor_1998 0.0778 0.0626 0.0734 0.0621 0.0650 #> orban_viktor_2018 0.0362 0.0617 0.0503 0.0494 0.0504 #> orban_viktor_1998 orban_viktor_2018 #> antall_jozsef_1990 0.0778 0.0362 #> bajnai_gordon_2009 0.0626 0.0617 #> gyurcsány_ferenc_2005 0.0734 0.0503 #> horn_gyula_1994 0.0621 0.0494 #> medgyessy_peter_2002 0.0650 0.0504 #> orban_viktor_1998 1.0000 0.0583 #> orban_viktor_2018 0.0583 1.0000 Majd a textstat_dist() függvény segítségével kiszámoljuk a dokumentumok egymástól való különbözőségét. mineln_dfm %>% quanteda.textstats::textstat_dist(margin = "documents", method = "euclidean") #> textstat_dist object; method = "euclidean" #> antall_jozsef_1990 bajnai_gordon_2009 gyurcsány_ferenc_2005 horn_gyula_1994 medgyessy_peter_2002 #> antall_jozsef_1990 0 162.8 186 163.6 160.2 #> bajnai_gordon_2009 163 0 138 80.1 68.1 #> gyurcsány_ferenc_2005 186 137.8 0 147.0 142.6 #> horn_gyula_1994 164 80.1 147 0 75.9 #> medgyessy_peter_2002 160 68.1 143 75.9 0 #> orban_viktor_1998 139 84.7 147 89.6 77.5 #> orban_viktor_2018 167 67.3 148 74.8 60.7 #> orban_viktor_1998 orban_viktor_2018 #> antall_jozsef_1990 139.2 167.4 #> bajnai_gordon_2009 84.7 67.3 #> gyurcsány_ferenc_2005 146.9 147.9 #> horn_gyula_1994 89.6 74.8 #> medgyessy_peter_2002 77.5 60.7 #> orban_viktor_1998 0 83.6 #> orban_viktor_2018 83.6 0 Ezután vizualizálhatjuk is a dokumentumok egymástól való távolságát egy olyan dendogram21 segítségével, amely megmutatja nekünk a lehetséges dokumentumpárokat (ld. 5.3. ábra). dist <- mineln_dfm %>% textstat_dist(margin = "documents", method = "euclidean") hierarchikus_klaszter <- hclust(as.dist(dist)) ggdendro::ggdendrogram(hierarchikus_klaszter) Ábra 5.3: A dokumentumok csoportosítása a távolságuk alapján A textstat_simil funkció segítségével azt is meg tudjuk vizsgálni, hogy egy adott kifejezés milyen egyéb kifejezésekkel korrelál. mineln_dfm %>% textstat_simil(y = mineln_dfm[, c("kormány")], margin = "features", method = "correlation") %>% head(n = 10) #> kormány #> elnök -0.125 #> tisztelt -0.508 #> országgyulés 0.804 #> hölgyeim -0.298 #> uraim -0.298 #> honfitársaim 0.926 #> ünnepi 0.959 #> pillanatban 0.959 #> állok 0.683 #> magyar 0.861 Arra is van lehetőségünk, hogy a két alkorpuszt hasonlítsuk össze egymással. Ehhez a textstat_keyness() függvényt használjuk, melynek a bemenete a dfm. A függvény argumentumában a target = után kell megadni, hogy mely alkorpusz a viszonyítási alap. Az összehasonlítás eredményét a textplot_keyness() függvény segítségével ábrázolhatjuk, ami megjeleníti a két alkorpusz leggyakoribb kifejezéseit (ld. 5.4. ábra). dfm_keyness <- corpus_mineln %>% tokens(remove_punct = TRUE) %>% tokens_remove(pattern = custom_stopwords) %>% dfm() %>% quanteda::dfm_group(partoldal) result_keyness <- quanteda.textstats::textstat_keyness(dfm_keyness, target = "jobb") quanteda.textplots::textplot_keyness(result_keyness, color = c("#484848", "#D0D0D0")) + xlim(c(-65, 65)) + theme(legend.position = c(0.9,0.1)) Ábra 5.4: A korpuszok legfontosabb kifejezései Ha az egyes miniszterelnökök beszédeinek leggyakoribb kifejezéseit szeretnénk összehasonlítani, azt a textstat_frequency() függvény segítségével tehetjük meg, melynek bemenete a megtisztított és súlyozott dfm. Az összehasonlítás eredményét pedig a ggplot2 segítségével ábrázolhatjuk is (ld. 5.5. ábra). Majd ábránkat a plotly segítségével interaktívvá tehetjük. dfm_weighted <- corpus_mineln %>% tokens( remove_punct = TRUE, remove_symbols = TRUE, remove_numbers = TRUE ) %>% tokens_tolower() %>% tokens_wordstem(language = "hungarian") %>% tokens_remove(pattern = custom_stopwords) %>% dfm() %>% dfm_weight(scheme = "prop") freq_weight <- textstat_frequency(dfm_weighted, n = 5, groups = pm) data_df <- ggplot(data = freq_weight, aes(x = nrow(freq_weight):1, y = frequency)) + geom_point() + facet_wrap(~ group, scales = "free", ncol = 1) + theme(panel.spacing = unit(1, "lines"))+ coord_flip() + scale_x_continuous( breaks = nrow(freq_weight):1, labels = freq_weight$feature ) + labs( x = NULL, y = "Relatív szófrekvencia" ) ggplotly(data_df, height = 1000, tooltip = "frequency") Ábra 5.5: Leggyakoribb kifejezések a miniszterelnöki beszédekben Mivel a szövegösszehasonlítás egy komplex kutatási feladat, a témával bőbben is foglalkozunk a Szövegösszhasonlítás fejezetben. 5.5 A kulcsszavak kontextusa Arra is lehetőségünk van, hogy egyes kulcszavakat a korpuszon belül szövegkörnyezetükben vizsgáljunk meg. Ehhez a kwic() függvényt használjuk, az argumentumok között a pattern = kifejezés után megadva azt a szót, amelyet vizsgálni szeretnénk, a window = után pedig megadhatjuk, hogy az adott szó hány szavas környezetére vagyunk kíváncsiak. corpus_mineln %>% tokens() %>% quanteda::kwic( pattern = "válság*", valuetype = "glob", window = 3, case_insensitive = TRUE ) %>% head(5) #> Keyword-in-context with 5 matches. #> [antall_jozsef_1990, 1167] Átfogó és mély | válságba | süllyedtünk a nyolcvanas #> [antall_jozsef_1990, 1283] kell hárítanunk a | válságot | , de csakis #> [antall_jozsef_1990, 2772] és a lakásgazdálkodás | válságos | helyzetbe került. #> [antall_jozsef_1990, 5226] gazdaság egészét juttatta | válságba | , és amellyel #> [antall_jozsef_1990, 5286] gazdaság reménytelenül eladósodott | válsággócai | ellen. A A beszédeket a Hungarian Comparative Agendas Project miniszterelnöki beszéd korpuszából válogattuk: https://cap.tk.hu/vegrehajto↩︎ A lambda leírása megtalálható itt: https://quanteda.io/reference/textstat_collocations.html↩︎ A különböző indexek leírása és képlete megtalálható az alábbi linken: https://quanteda.io/reference/textstat_lexdiv.html↩︎ (Schütze, Manning, and Raghavan 2008)↩︎ Olyan ábra, amely hasonlóságaik vagy különbségeik alapján csoportosított objektumok összefüggéseit mutatja meg.↩︎ "],["sentiment.html", "6 Szótárak és érzelemelemzés 6.1 Fogalmi alapok 6.2 Szótárak az R-ben 6.3 A Magyar Nemzet elemzése 6.4 MNB sajtóközlemények", " 6 Szótárak és érzelemelemzés 6.1 Fogalmi alapok A szentiment- vagy vélemény-, illetve érzelemelemzés a számítógépes nyelvészet részterülete, melynek célja az egyes szövegek tartalmából kinyerni azokat az információkat, amelyek értékelést fejeznek ki.22 A véleményelemzés a szövegeket három szinten osztályozza. A legáltalánosabb a dokumentumszintű osztályozás, amikor egy hosszabb szövegegység egészét vizsgáljuk, míg a mondatszintű osztályozásnál a vizsgálat alapegysége a mondat. A legrészletesebb adatokat akkor nyerjük, amikor az elemzést target-szinten végezzük, azaz meghatározzuk azt is, hogy egy-egy érzelem a szövegen belül mire vonatkozik. Mindhárom szinten azonos a feladat: egyrészt meg kell állapítani, hogy az adott egységben van-e értékelés, vélemény vagy érzelem, és ha igen, akkor pedig meg kell határozni, hogy milyen azok érzelmi tartalma. A pozitív-negatív-semleges skálán mozgó szentimentelemzés mellett az elmúlt két évtizedben jelentős lépések történtek a szövegek emóciótartalmának automatikus vizsgálatára is. A módszer hasonló a szentimentelemzéshez, tartalmilag azonban más skálán mozog. Az emócióelemzés esetén ugyanis nem csak azt kell meghatározni, hogy egy kifejezés pozitív vagy negatív töltettel rendelkezik, hanem azt is, hogy milyen érzelmet (öröm, bánat, undor stb.) hordoz. A szótár alapú szentiment- vagy emócióelemzés alapja az az egyszerű ötlet, hogy ha tudjuk, hogy egyes szavak milyen érzelmeket, érzéseket hordoznak, akkor ezeket a szavakat egy szövegben megszámolva képet kaphatunk az adott dokumentum érzelmi tartalmáról. Mivel a szótár alapú elemzés az adott kategórián belüli kulcsszavak gyakoriságán alapul, ezért van, aki nem tekinti statisztikai elemzésnek (lásd például Young and Soroka (2012)). A tágabb kvantitatív szövegelemzési kontextusban az osztályozáson (classification) belül a felügyelt módszerekhez hasonlóan itt is ismert kategóriákkal dolgozunk, azaz előre meghatározzuk, hogy egy-egy adott szó pozitív vagy negatív tértékű, vagy továbbmenve, milyen érzelmet hordoz, csak egyszerűbb módszertannal (Grimmer and Stewart 2013). A kulcsszavakra építés miatt a módszer a kvalitatív és a kvantitatív kutatási vonalak találkozásának is tekinthető, hiszen egy-egy szónak az érzelmi töltete nem mindig ítélhető meg objektíven. Mint minden módszer esetében, itt is kiemelten fontos ellenőrni, hogy a használt szótár kategóriák és kulcsszavak fedik-e a valóságot. Más szavakkal: validálás, validálás, validálás. A módszer előnyei: Tökéletesen megbízható: a számításoknak nincs probabilisztikus (azaz valószínűségre épülő) eleme, mint például a Support Vector alapú osztályozásnak, illetve az emberi szövegkódolásnál előforduló problémákat is elkerüljük (például azt, hogy két kódoló, vagy ugyanazon kódoló két különböző időpontban nem azonosan értékeli ugyanazt a kifejezést). Általa képesek vagyunk mérni a szöveg látens dimenzióit. Széles körben alkalmazható, egyszerűen számolható. A politikatudományon és a számítógépes nyelvészeten belül nagyon sok kész szótár elérhető, amelyek különböző módszerekkel készültek és különböző területet fednek le (például populizmus, pártprogramok policy tartalma, érzelmek, gazdasági tartalom). Relatíve könnyen adaptálható egyik nyelvi környezetből a másikba, bár szótárfordítások esetén külön hangsúlyt kell fektetni a validálásra.23 A módszer lehetséges hátrányai: A szótár hatékonysága és validitása azon múlik, hogy mennyire egyezik a szótár és a vizsgálni kívánt dokumentum területe. Nem mindegy például, hogy a szótárunkkal tőzsdei jelentések alapján a gazdasági bizonytalanságot vagy nézők filmekre adott értékeléseit szeretnénk-e vizsgálni. Léteznek általános szentimentszótárak, ezek hatékonysága azonban általában alulmúlja a terület-specifikus szótárakét. A terület-specifikus szótár építése kvalitatív folyamat, éppen ezért idő- és emberi erőforrás igényes. A szózsák alapú elemzéseknél a kontextus elvész. Gondoljunk például a tagadásra: a „nem vagyok boldog” kifejezés esetén egy általános szentiment szótár a tagadás miatt félreosztályozná a mondat érzelmi töltését, hiszen a boldog szó önmagában a pozitív kategóriába tartozik. Természetesen az automatikus tagadás kezelésére is vannak lehetőségek, de a kérdés komplexitása miatt ezek bemutatásától most eltekintünk. A legnagyobb méretű általános szentimentszótár az angol nyelvű SentiWordNet (SWN), ami kb. 150 000 szót tartalmaz, amelyek mindegyike a három szentimentérték – pozitív, negatív, semleges – közül kapott egyet.24(Baccianella, Esuli, and Sebastiani 2010) Az R-ben végzett szentimentelemzés során az angol nyelvű szövegekhez több beépített általános szentimentszótár is a rendelkezésünkre áll.25 A teljesség igénye nélkül említhetjük az AFINN,26 a bing27 és az nrc28 szótárakat. Az elemzés sikere több faktortól is függ. Fontos, hogy a korpuszban lévő dokumentumokat körültekintően tisztítsuk meg az elemzés elején (lásd a Korpuszépítés és előkészítés fejezetet). A következő lépésben meg kell bizonyosodnunk arról, hogy a kiválasztott szentiment szótár alkalmazható a korpuszunkra. Amennyiben nem találunk alkalmas szótárt, akkor a saját szótár validálására kell figyelni. A negyedik fejezetben leírtak itt is érvényesek, a dokumentum-kifejezés mátrixot érdemes valamilyen módon súlyozni. 6.2 Szótárak az R-ben A szótár alapú elemzéshez a quanteda csomagot fogjuk használni, illetve a 3. fejezetben már megismert readr, stringr, dplyr tidyverse csomagokat.29 library(stringr) library(dplyr) library(tidyr) library(ggplot2) library(quanteda) library(HunMineR) library(plotly) Mielőtt két esettanulmányt bemutatnánk, vizsgáljuk meg, hogyan néz ki egy szentimentszótár az R-ben. A szótárt kézzel úgy tudjuk elkészíteni, hogy egy listán belül létrehozzuk karaktervektorként a kategóriákat és a kulcsszavakat, és ezt a listát a quanteda dictionary függvényével eltároljuk. szentiment_szotar <- dictionary( list( pozitiv = c("jó", "boldog", "öröm"), negativ = c("rossz", "szomorú", "lehangoló") ) ) szentiment_szotar #> Dictionary object with 2 key entries. #> - [pozitiv]: #> - jó, boldog, öröm #> - [negativ]: #> - rossz, szomorú, lehangoló A quanteda, quanteda.corpora és tidytext R csomagok több széles körben használt szentiment szótárat tartalmaznak, így nem kell kézzel replikálni minden egyes szótárat, amit használni szeretnénk. A szentiment elemzési munkafolyamat, amit ebben a részfejezetben bemutatunk, a következő lépésekből áll: dokumentumok betöltése, szöveg előkészítése, a korpusz létrehozása, dokumentum-kifejezés mátrix létrehozása, szótár betöltése, a dokumentum-kifejezés mátrix szűrése a szótárban lévő kulcsszavakkal, az eredmény vizualizálása, további felhasználása. A fejezetben két különböző korpuszt fogunk elemezni: a 2006-os Magyar Nemzet címlapjainak egy 252 cikkből álló mintáját vizsgáljuk egy magyar szentiment szótárral.30 A második korpusz a Magyar Nemzeti Bank angol nyelvű sajtóközleményeiből áll, amin egy széles körben használt gazdasági szótár használatát mutatjuk be.31 6.3 A Magyar Nemzet elemzése mn_minta <- HunMineR::data_magyar_nemzet_small A HunMineR csomag segítségével beolvassuk a Magyar Nemzet adatbázis egy kisebb részét, ami az esetünkben a 2006-os címlapokon szereplő híreket jelenti. A summary() parancs, ahogy a neve is mutatja, gyors áttekintést nyújt a betöltött adatbázisról. Látjuk, hogy 2834 sorból (megfigyelés) és 3 oszlopból (változó) áll. Első ránézésre látszik, hogy a text változónk tartalmazza a szövegeket, és hogy azok tisztításra szorulnak. glimpse(mn_minta) #> Rows: 2,834 #> Columns: 3 #> $ doc_id <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31… #> $ text <chr> "Hat fővárosi képviselő öt percnél is kevesebbet beszélt egy év alatt a közgyűlésben.\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n", "Mo… #> $ doc_date <date> 2006-01-02, 2006-01-02, 2006-01-02, 2006-01-02, 2006-01-02, 2006-01-02, 2006-01-02, 2006-01-02, 2006-01-02, 2006… A glimpse függvény segítségével belepillanthatunk a használt korpuszba és láthatjuk, hogy az 3 oszlopból áll a dokumentum azonosítójából, amely csak egy sorszám, a dokumentum szövegéből, és a dokumentumhoz tartozó azonosítóból. Az első szöveget megnézve látjuk, hogy a standard előkészítési lépések mellett a sortörést (\\n) is ki kell törölnünk. mn_minta$text[1] #> [1] "Hat fővárosi képviselő öt percnél is kevesebbet beszélt egy év alatt a közgyűlésben.\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n" Habár a quanteda is lehetőséget ad néhány előkészítő lépésre, érdemes ezt olyan céleszközzel tenni, ami nagyobb rugalmasságot ad a kezünkbe. Mi erre a célra a stringr csomagot használjuk. Első lépésben kitöröljük a sortöréseket (\\n), a központozást, a számokat és kisbetűsítünk minden szót. Előfordulhat, hogy (számunkra nehezen látható) extra szóközök maradnak a szövegben. Ezeket az str_squish()függvénnyel tüntetjük el. A szöveg eleji és végi extra szóközöket (leading vagy trailing white space) az str_trim() függvény vágja le. mn_tiszta <- mn_minta %>% mutate( text = stringr::str_remove_all(string = text, pattern = "\\n"), text = stringr::str_remove_all(string = text, pattern = "[:punct:]"), text = stringr::str_remove_all(string = text, pattern = "[:digit:]"), text = stringr::str_to_lower(text), text = stringr::str_trim(text), text = stringr::str_squish(text) ) A szöveg sokkal jobban néz ki, habár észrevehetjük, hogy maradhattak benne problémás részek, főleg a sortörés miatt, ami sajnos hol egyes szavak közepén van (a jobbik eset), vagy pedig pont szóhatáron, ez esetben a két szó sajnos összevonódik. Az egyszerűség kedvéért feltételezzük, hogy ez kellően ritkán fordul elő ahhoz, hogy ne befolyásolja az elemzésünk eredményét. mn_tiszta$text[1] #> [1] "hat fővárosi képviselő öt percnél is kevesebbet beszélt egy év alatt a közgyűlésben" Miután kész a tisztá(bb) szövegünk, korpuszt hozunk létre a quanteda corpus() függvényével. A korpusz objektum a szöveg mellett egyéb dokumentum meta adatokat is tud tárolni (dátum, író, hely, stb.) Ezeket mi is hozzáadhatjuk (erre majd látunk példát), illetve amikor létrehozzuk a korpuszt a data frame-ünkből, automatikusan metaadatokként tárolódnak a változóink. Jelen esetben az egyetlen dokumentum változónk a szöveg mellett a dátum lesz. A korpusz dokumentum változóihoz a docvars() függvény segítségével tudunk hozzáférni. mn_corpus <- corpus(mn_tiszta) head(docvars(mn_corpus), 5) #> doc_date #> 1 2006-01-02 #> 2 2006-01-02 #> 3 2006-01-02 #> 4 2006-01-02 #> 5 2006-01-02 A következő lépés a dokumentum-kifejezés mátrix létrehozása a dfm() függvénnyel. Először tokenekre bontjuk a szövegeket a tokens() paranccsal, és aztán ezt a tokenizált szózsákot kapja meg a dfm inputnak. A sor a végén a létrehozott mátrixunkat TF-IDF módszerrel súlyozzuk a dfm_tfidf() függvény használatával. mn_dfm <- mn_corpus %>% tokens(what = "word") %>% dfm() %>% dfm_tfidf() A cikkek szentimentjét egy magyar szótárral fogjuk becsülni, amit a Társadalomtudományi Kutatóközpont kutatói a Mesterséges Intelligencia Nemzeti Laboratórium projekt keretében készítettek.32 Két dimenziót tarlamaz (pozitív és negatív), 2614 pozitív és 2654 negatív kulcsszóval. Ez nem számít kirívóan nagynak a szótárak között, mivel az adott kategóriák minél teljesebb lefedése a cél. poltext_szotar <- HunMineR::dictionary_poltext poltext_szotar #> Dictionary object with 2 key entries. #> - [positive]: #> - abszolút, ad, adaptív, adekvát, adócsökkentés, adókedvezmény, adomány, adományoz, adóreform, adottság, adottságú, áfacsökkentés, agilis, agytröszt, áhított, ajándék, ajándékoz, ajánl, ajánlott, akadálytalan [ ... and 2,279 more ] #> - [negative]: #> - aberrált, abnormális, abnormalitás, abszurd, abszurditás, ádáz, adócsalás, adócsaló, adós, adósság, áfacsalás, áfacsaló, affér, aggasztó, aggodalom, aggódik, aggódás, agresszió, agresszíven, agresszivitás [ ... and 2,568 more ] Az egyes dokumentumok szentimentjét a dfm_lookup() becsüli, ahol az előző lépésben létrehozott súlyozott dfm az input és a magyar szentimentszótár a dictionary. Egy gyors pillantás az eredményre és látjuk hogy minden dokumentumhoz készült egy pozitív és egy negatív érték. A TF-IDF súlyozás miatt nem látunk egész számokat (a súlyozás nélkül a sima szófrekvenciát kapnánk). mn_szentiment <- quanteda::dfm_lookup(mn_dfm, dictionary = poltext_szotar) head(mn_szentiment, 5) #> Document-feature matrix of: 5 documents, 2 features (40.00% sparse) and 1 docvar. #> features #> docs positive negative #> 1 0 0 #> 2 0.838 12.50 #> 3 0 0 #> 4 21.104 6.45 #> 5 11.036 8.13 Ahhoz, hogy fel tudjuk használni a kapott eredményt, érdemes dokumentumváltozóként eltárolni a korpuszban. Ezt a fent már használt docvars() függvény segítségével tudjuk megtenni, ahol a második argumentumként az új változó nevét adjuk meg. docvars(mn_corpus, "pos") <- as.numeric(mn_szentiment[, 1]) docvars(mn_corpus, "neg") <- as.numeric(mn_szentiment[, 2]) head(docvars(mn_corpus), 5) #> doc_date pos neg #> 1 2006-01-02 0.000 0.00 #> 2 2006-01-02 0.838 12.50 #> 3 2006-01-02 0.000 0.00 #> 4 2006-01-02 21.104 6.45 #> 5 2006-01-02 11.036 8.13 Végül a kapott korpuszt a kiszámolt szentimentértékekkel a quanteda-ban lévő convert() függvénnyel adattáblává alakítjuk. Aconvert() függvény dokumentációját érdemes elolvasni, mert ennek segítségével tudjuk a quanteda-ban elkészült objektumainkat átalakítani úgy, hogy azt más csomagok is tudják használni. mn_df <- quanteda::convert(mn_corpus, to = "data.frame") Mielőtt vizualizálnánk az eredményt érdemes a napi szintre aggregálni a szentimentértéket és egy nettó értéket kalkulálni (ld. 6.1. ábra).33 mn_df <- mn_df %>% group_by(doc_date) %>% summarise( daily_pos = sum(pos), daily_neg = sum(neg), net_daily = daily_pos - daily_neg ) Az így kapott plot y tengelyén az adott cikkek időpontját láthatjuk, míg az x tengelyén a szentiment értékeiket. Ebben több kiugrást is tapasztalhatunk. Természetesen messzemenő következtetéseket egy ilyen kis korpusz alapján nem vonhatunk le, de a kiugrásokhoz tartozó cikkek kvalitatív vizsgálatával megállapíthatjuk, hogy az áprilisi kiugrást a választásokhoz kötődő cikkek pozitív hangulata, míg az októberi negatív kilengést az öszödi beszéd nyilvánosságra kerüléséhez köthető cikkek negatív szentimentje okozza. mncim_df <- ggplot(mn_df, aes(doc_date, net_daily)) + geom_line() + labs(y = "Szentiment", x = NULL, caption = "Adatforrás: https://cap.tk.hu/") ggplotly(mncim_df) Ábra 6.1: Magyar Nemzet címlap szentimentje 6.4 MNB sajtóközlemények A második esettanulmányban a kontextuális szótárelemzést mutatjuk be egy angol nyelvű korpusz és specializált szótár segítségével. A korpusz az MNB kamatdöntéseit kísérő nemzetközi sajtóközleményei, a szótár pedig a Loughran and McDonald (2011) pénzügyi szentimentszótár.34 penzugy_szentiment <- HunMineR::dictionary_LoughranMcDonald penzugy_szentiment #> Dictionary object with 9 key entries. #> - [NEGATIVE]: #> - abandon, abandoned, abandoning, abandonment, abandonments, abandons, abdicated, abdicates, abdicating, abdication, abdications, aberrant, aberration, aberrational, aberrations, abetting, abnormal, abnormalities, abnormality, abnormally [ ... and 2,335 more ] #> - [POSITIVE]: #> - able, abundance, abundant, acclaimed, accomplish, accomplished, accomplishes, accomplishing, accomplishment, accomplishments, achieve, achieved, achievement, achievements, achieves, achieving, adequately, advancement, advancements, advances [ ... and 334 more ] #> - [UNCERTAINTY]: #> - abeyance, abeyances, almost, alteration, alterations, ambiguities, ambiguity, ambiguous, anomalies, anomalous, anomalously, anomaly, anticipate, anticipated, anticipates, anticipating, anticipation, anticipations, apparent, apparently [ ... and 277 more ] #> - [LITIGIOUS]: #> - abovementioned, abrogate, abrogated, abrogates, abrogating, abrogation, abrogations, absolve, absolved, absolves, absolving, accession, accessions, acquirees, acquirors, acquit, acquits, acquittal, acquittals, acquittance [ ... and 883 more ] #> - [CONSTRAINING]: #> - abide, abiding, bound, bounded, commit, commitment, commitments, commits, committed, committing, compel, compelled, compelling, compels, comply, compulsion, compulsory, confine, confined, confinement [ ... and 164 more ] #> - [SUPERFLUOUS]: #> - aegis, amorphous, anticipatory, appertaining, assimilate, assimilating, assimilation, bifurcated, bifurcation, cessions, cognizable, concomitant, correlative, deconsolidation, delineation, demonstrable, demonstrably, derecognized, derecognizes, derivatively [ ... and 36 more ] #> [ reached max_nkey ... 3 more keys ] A szentimentszótár 9 kategóriából áll. A legtöbb kulcsszó a negatív dimenzióhoz van (2355). A munkamenet hasonló az előző példához: adat betöltés, szövegtisztítás, korpusz létrehozás, tokenizálás, kulcs kontextuális tokenek szűrése, dfm előállítás és szentiment számítás, az eredmény vizualizálása, további felhasználása. mnb_pr <- HunMineR::data_mnb_pr Adatbázisunk 180 megfigyelésből és 4 változóból áll. Az egyetlen lényeges dokumentum metaadat itt is a szövegek megjelenési ideje, de a glimpse függvénnyel itt is ellenőrizhetjük hogyan néz ki a korpusz felépítése és milyen metaadatokat tartalmaz pontosan. glimpse(mnb_pr) #> Rows: 180 #> Columns: 4 #> $ date <date> 2005-01-24, 2005-02-21, 2005-03-29, 2005-04-25, 2005-05-23, 2005-06-20, 2005-07-18, 2005-08-22, 2005-09-19, 2005-10-… #> $ text <chr> "At its meeting on January the Monetary Council considered the latest economic and financial developments and decided… #> $ id <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32… #> $ year <dbl> 2005, 2005, 2005, 2005, 2005, 2005, 2005, 2005, 2005, 2005, 2005, 2005, 2006, 2006, 2006, 2006, 2006, 2006, 2006, 200… Ez alapján pedig láthatjuk, hogy a korpusz a tényleges szövegek mellett tartalmaz még id sorszámot, pontos dátumot és évet is. A szövegeket ugyanazokkal a standard eszközökkel kezeljük, mint a Magyar Nemzet esetében. Érdemes minden esetben ellenőrizni, hogy az R-kód, amit használunk, tényleg azt csinálja-e, amit szeretnénk. Ez hatványozottan igaz abban az esetben, amikor szövegekkel és reguláris kifejezésekkel dolgozunk. mnb_tiszta <- mnb_pr %>% 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) ) Miután rendelkezésre állnak a tiszta dokumentumaink, egy karaktervektorba gyűjtjük azokat a kulcsszavakat, amelyek környékén szeretnénk megfigyelni a szentiment alakulását. A példa kedvéért mi az unemp*, growth, gdp, inflation* szótöveket és szavakat választottuk. A tokens_keep() megtartja a kulcsszavainkat és egy általunk megadott +/- n tokenes környezetüket (jelen esetben 10). A szentimentelemzést pedig már ezen a jóval kisebb mátrixon fogjuk lefuttatni. A phrase() segítségével több szóból álló kifejezéséket is vizsgálhatunk. Ilyen szókapcsolat például az „Európai Unió” is, ahol lényeges, hogy egyben kezeljük a két szót. mnb_corpus <- corpus(mnb_tiszta) gazdasag <- c("unemp*", "growth", "gdp", "inflation*", "inflation expectation*") mnb_token <- tokens(mnb_corpus) %>% tokens_keep(pattern = phrase(gazdasag), window = 10) A szentimentet most is egy súlyozott dfm-ből számoljuk. A kész eredményt hozzáadjuk a korpuszhoz, majd adattáblát hozunk létre belőle. A 7 kategóriából 5-öt használunk csak, amelyeknek jegybanki környezetben értelmezhető tartalma van. mnb_szentiment <- tokens_lookup(mnb_token, dictionary = penzugy_szentiment) %>% dfm() %>% dfm_tfidf() docvars(mnb_corpus, "negative") <- as.numeric(mnb_szentiment[, "negative"]) docvars(mnb_corpus, "positive") <- as.numeric(mnb_szentiment[, "positive"]) docvars(mnb_corpus, "uncertainty") <- as.numeric(mnb_szentiment[, "uncertainty"]) docvars(mnb_corpus, "constraining") <- as.numeric(mnb_szentiment[, "constraining"]) docvars(mnb_corpus, "superfluous") <- as.numeric(mnb_szentiment[, "superfluous"]) mnb_df <- convert(mnb_corpus, to = "data.frame") A célunk, hogy szentiment kategóriánkénti bontásban mutassuk be az elemzésünk eredményét, de előtte egy kicsit alakítani kell az adattáblán, hogy a korábban már tárgyalt tidy formára hozzuk. A különböző szentiment értékeket tartalmazó oszlopokat fogjuk átrendezni úgy, hogy kreálunk egy „sent_type” változót, ahol a kategória nevet fogjuk eltárolni és egy „sent_score” változót, ahol a szentiment értéket. Ehhez a tidyr-ben található pivot_longer() föggvényt használjuk. mnb_df <- mnb_df %>% tidyr::pivot_longer( cols = negative:superfluous, names_to = "sent_type", values_to = "sent_score" ) Az átalakítás után már könnyedén tudjuk kategóriákra bontva megjeleníteni az MNB közlemények különböző látens dimenzióit. Fontos emlékezni arra, hogy ez az eredmény a kulcsszavaink +/- 10 tokenes környezetében lévő szavak szentimentjét méri. Az így kapott ábránk a három alkalmazott szentiment kategória időbeli előfordulását mutatja be. Ami érdekes eredmény, hogy a felesleges „töltelék” (superfluous) szövegek szinte soha nem fordulnak elő a kulcsszavaink körül. A többi érték is nagyjából megfelel a várakozásainknak, habár a 2008-as gazdasági válság nem tűnik kiugró pontnak. Azonban a 2010 utáni európai válság már láthatóan megjelenik az idősorainkban (ld. 6.2. ábra). Az általunk használt szótár alapvetően az Egyesült Államokban a tőzsdén kereskedő cégek publikus beszámolóiból készült, így elképzelhető, hogy egyes jegybanki környezetben sokat használt kifejezések nincsenek benne. A kapott eredmények validálása ezért is nagyon fontos, illetve érdemes azzal is tisztában lenni, hogy a szótáras módszer nem tökéletes (ahogy az emberi vagy más gépi kódolás sem). mnsent_df <- ggplot(mnb_df, aes(date, sent_score)) + geom_line() + labs( y = NULL, x = NULL ) + facet_wrap(~sent_type, ncol = 1)+ theme(panel.spacing = unit(2, "lines")) ggplotly(mnsent_df) Ábra 6.2: Magyar Nemzeti Bank közleményeinek szentimentje Bővebben lásd például: (Liu 2010)↩︎ A lehetséges, területspecifikus szótáralkotási módszerekről részletesebben ezekben a tanulmányokban lehet olvasni: Laver and Garry (2000); Young and Soroka (2012); Loughran and McDonald (2011); Máté, Sebők, and Barczikay (2021)↩︎ A szótár és dokumentációja elérhető az alábbi linken: https://github.com/aesuli/SentiWordNet↩︎ A quanteda.dictionaries csomag leírása és a benne található szótárak az alábbi github linken érhetőek el: https://github.com/kbenoit/quanteda.dictionaries↩︎ A szótár és dokumentációja elérhető itt: http://www2.imm.dtu.dk/pubdb/pubs/6010-full.html↩︎ A szótár és dokumentációja elérhető itt: https://www.cs.uic.edu/~liub/FBS/sentiment-analysis.html↩︎ A szótár és dokumentációja elérhető itt: http://saifmohammad.com/WebPages/NRC-Emotion-Lexicon.htm↩︎ A szentimentelemzéshez gyakran használt csomag még a tidytext. A szerzők online is szabadon elérhető könyvük Silge and Robinson (2017) 2. fejezetében részletesen is bemutatják a tidytext munkafolyamatot: (https://www.tidytextmining.com/sentiment.html).↩︎ A korpusz a Hungarian Compartive Agendas Project keretében készült és regisztáció után, kutatási célra elérhető az alábbi linken: https://cap.tk.hu/a-media-es-a-kozvelemeny-napirendje.↩︎ A korpusz, a szótár és az elemzés teljes dokumentációja elérhető az alábbi github linken: https://github.com/poltextlab/central_bank_communication, a teljes elemzés (Máté, Sebők, and Barczikay 2021) elérhető: https://doi.org/10.1371/journal.pone.0245515↩︎ ELKH TK MILAB: https://milab.tk.hu/hu A szótár és a hozzátartozó dokumentáció elérhető az alábbi github oldalon: https://github.com/poltextlab/sentiment_hun↩︎ A csoportosított adatokkal való munka bővebb bemutatását lásd a Függelékben.↩︎ A témával részletesen foglalkozó tanulmányban egy saját monetáris szentimentszótárat mutatunk be: Az implementáció és a hozzá tartozó R forráskód nyilvános: https://doi.org/10.6084/m9.figshare.13526156.v1↩︎ "],["lda_ch.html", "7 Felügyelet nélküli tanulás 7.1 Fogalmi alapok 7.2 K-közép klaszterezés 7.3 LDA topikmodellek35 7.4 Strukturális topikmodellek", " 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 2016). 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 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 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.↩︎ 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.↩︎ Részletesebben lásd például: http://brooksandrew.github.io/simpleblog/articles/latent-dirichlet-allocation-under-the-hood/↩︎ 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)↩︎ A tidy formátumról bővebben: https://cran.r-project.org/web/packages/tidyr/vignettes/tidy-data.html↩︎ 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.↩︎ A kódkönyv elérhető az alábbi linken: Comparative Agendas Project.↩︎ Az stm csomaghoz tartozó leírás: https://cran.r-project.org/web/packages/stm/vignettes/stmVignette.pdf↩︎ "],["embedding.html", "8 Szóbeágyazások 8.1 A szóbeágyazás célja 8.2 Word2Vec és GloVe", " 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 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.↩︎ 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↩︎ A futtatásra használt PC konfiguráció: CPU: Intel Core i5-4460 (3.2GHz); RAM: 16GB↩︎ 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).↩︎ "],["scaling.html", "9 Szövegskálázás 9.1 Fogalmi alapok 9.2 Wordscores 9.3 Wordfish", " 9 Szövegskálázás 9.1 Fogalmi alapok A politikatudomány egyik izgalmas kérdése a szereplők ideológiai, vagy közpolitikai skálákon való elhelyezése. Ezt jellemzően pártprogramok vagy különböző ügyekkel kapcsolatos álláspontpontok alapján szokták meghatározni, de a politikusok beszédei is alkalmasak arra, hogy meghatározzuk a beszélő ideológiai hovatartozását. A szövegbányászat területén jellemzően a wordfish és a wordscores módszert alkalmazzák erre a feladatra. Míg előbbi a felügyelet nélküli módszerek sorába tartozik, utóbbi felügyelt módszerek közé. A wordscores a szótári módszerekhez hasonlóan a szövegeket a bennük található szavak alapján helyezi el a politikai térben oly módon, hogy az ún. referenciadokumentumok szövegét használja tanító halmazként. A wordscores kiindulópontja, hogy pozíció pontszámokat kell rendelni referencia szövegekhez. A modell számításba veszi a szövegek szavainak súlyozott gyakoriságát és a pozíciópontszám, valamint a szógyakoriság alapján becsüli meg a korpuszban lévő többi dokumentum pozícióját (Laver, Benoit, and Garry 2003) A felügyelet nélküli wordfish módszer a skálázás során nem a referencia dokumentumokra támaszkodik, hanem olyan kifejezéseket keres a szövegben, amelyek megkülönböztetik egymástól a politikai spektrum különböző pontjain elhelyezkedő beszélőket. Az IRT-n (item response theory) alapuló módszer azt feltételezi, hogy a politikusok egy kevés dimenziós politikai térben mozognak, amely tér leírható az i politikus \\(\\theta_1\\) paraméterével. Egy politikus (vagy párt) ebben a térben elfoglalt helyzete pedig befolyásolja a szavak szövegekben történő használatát. A módszer erőssége, hogy kevés erőforrás-befektetéssel megbízható becsléseket ad, ha a szövegek valóban az ideológiák mentén különböznek, tehát ha a szereplők erősen ideológiai tartalamú diskurzust folytatnak. Alkalmazásakor azonban tudnunk kell: a módszer nem képes kezelni, hogy a szövegek között nem csak ideológiai különbség lehet, hanem például stílusból és témából eredő eltérések is. Mivel a modell nem felügyelt, ezért nehéz garantálni, hogy valóban megbízhatóan azonosítja a szereplők elhelyezkedését a politikai térben, így az eredményeket mindenképpen körültekintően és alaposan kell validálni (Grimmer and Stewart 2013; Hjorth et al. 2015; Slapin and Proksch 2008). library(readr) library(dplyr) library(stringr) library(ggplot2) library(ggrepel) library(quanteda) library(quanteda.textmodels) library(plotly) library(HunMineR) A skálázási algoritmusokat egy kis korpuszon mutatjuk be. A minta dokumentumok a 2014–2018-as parlamenti ciklusban az Országgyűlésben frakcióvezető politikusok egy-egy véletlenszerűen kiválasztott napirend előtti felszólalásai. Ebben a ciklusban összesen 11 frakcióvezetője volt a két kormánypárti és öt ellenzéki frakciónak. 47 A dokumentumokon először elvégeztük a szokásos előkészítési lépéseket. parl_beszedek <- HunMineR::data_parlspeakers_small beszedek_tiszta <- parl_beszedek %>% 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 ismét megtekinthetjük, mint az eredeti szöveget és a tisztított is, ezzel nem csak azt tudjuk ellenőrizni, hogy a tisztítás sikeres volt-e, hanem a metaadatokat is megnézhetjük, amelyek jelenesetben a felszólalás azonosító száma a felszólaló neve, valamint a pártja. glimpse(parl_beszedek) #> Rows: 10 #> Columns: 4 #> $ id <chr> "20142018_024_0002_0002", "20142018_055_0002_0002", "20142018_064_0002_0002", "20142018_115_0002_0002", "201420… #> $ text <chr> "VONA GÁBOR (Jobbik): Tisztelt Elnök Úr! Tisztelt Országgyűlés! A tegnapi napon 11 helyen tartottak időközi önk… #> $ felszolalo <chr> "Vona Gábor (Jobbik)", "Dr. Schiffer András (LMP)", "Dr. Szél Bernadett (LMP)", "Tóbiás József (MSZP)", "Schmuc… #> $ part <chr> "Jobbik", "LMP", "LMP", "MSZP", "LMP", "MSZP", "Jobbik", "Fidesz", "KDNP", "Fidesz" glimpse(beszedek_tiszta) #> Rows: 10 #> Columns: 4 #> $ id <chr> "20142018_024_0002_0002", "20142018_055_0002_0002", "20142018_064_0002_0002", "20142018_115_0002_0002", "201420… #> $ text <chr> "vona gábor jobbik tisztelt elnök úr tisztelt országgyűlés a tegnapi napon helyen tartottak időközi önkormányza… #> $ felszolalo <chr> "Vona Gábor (Jobbik)", "Dr. Schiffer András (LMP)", "Dr. Szél Bernadett (LMP)", "Tóbiás József (MSZP)", "Schmuc… #> $ part <chr> "Jobbik", "LMP", "LMP", "MSZP", "LMP", "MSZP", "Jobbik", "Fidesz", "KDNP", "Fidesz" A wordfish és wordscores algoritmus is ugyanazt a kiinduló korpuszt és dfm objektumot használja, amit a szokásos módon a quanteda csomag corpus() függvényével hozunk létre. beszedek_corpus <- corpus(beszedek_tiszta) summary(beszedek_corpus) #> Corpus consisting of 10 documents, showing 10 documents: #> #> Text Types Tokens Sentences id felszolalo part #> text1 442 819 1 20142018_024_0002_0002 Vona Gábor (Jobbik) Jobbik #> text2 354 607 1 20142018_055_0002_0002 Dr. Schiffer András (LMP) LMP #> text3 426 736 1 20142018_064_0002_0002 Dr. Szél Bernadett (LMP) LMP #> text4 314 538 1 20142018_115_0002_0002 Tóbiás József (MSZP) MSZP #> text5 354 589 1 20142018_158_0002_0002 Schmuck Erzsébet (LMP) LMP #> text6 333 538 1 20142018_172_0002_0002 Dr. Tóth Bertalan (MSZP) MSZP #> text7 344 559 1 20142018_206_0002_0002 Volner János (Jobbik) Jobbik #> text8 352 628 1 20142018_212_0002_0002 Kósa Lajos (Fidesz) Fidesz #> text9 317 492 1 20142018_236_0002_0002 Harrach Péter (KDNP) KDNP #> text10 343 600 1 20142018_249_0002_0002 Dr. Gulyás Gergely (Fidesz) Fidesz A leíró statisztikai táblázatban látszik, hogy a beszédek hosszúsága nem egységes, a leghosszabb 819, a legrövidebb pedig 492 szavas. Az átlagos dokumentum hossz az 611 szó. A korpusz szemléltető célú, alaposabb elemzéshez hosszabb és/vagy több dokumentummal érdemes dolgoznunk. A korpusz létrehozása után elkészítjük a dfm mátrixot, amelyből eltávolítjuk a magyar stopszvakat a HunMineR beépített szótára segítségével. stopszavak <- HunMineR::data_stopwords_extra beszedek_dfm <- beszedek_corpus %>% tokens() %>% tokens_remove(stopszavak) %>% dfm() 9.2 Wordscores A modell illesztést a wordfish-hez hasonlóan a quanteda.textmodels csomagban található textmodel_wordscores() függvény végzi. A kiinduló dfm ugyanaz, mint amit a fejezet elején elkészítettünk, a beszedek_dfm. A referencia pontokat dokumentumváltozóként hozzáadjuk a dfm-hez (a refrencia_pont oszlopot, ami NA értéket kap alapértelmezetten). A kiválasztott referencia dokumentumoknál pedig egyenként hozzáadjuk az értékeket. Erre több megoldás is van, az egyszerűbb út, hogy az egyik és a másik végletet a -1; 1 intervallummal jelöljük. Ennek a lehetséges alternatívája, hogy egy külső, már validált forrást használunk. Pártok esetén ilyen lehet a Chapel Hill szakértői kérdőívének a pontszámai, a Manifesto projekt által kódolt jobb-bal (rile) dimenzió. A lenti példánál mi maradunk az egyszerűbb bináris kódolásnál (ld. 9.4. ábra). A wordfish eredményt alapul véve a két referencia pont Gulyás Gergely és Szél Bernadett beszédei lesznek.48 Ezek a 3. és a 10. dokumentumok. Miután a referencia pontokat hozzárendeltünk az adattáblához szintén a docvars funkcióval meg is tekinthetjük azt és láthatjuk, hogy a referenci_pont már a metaadatok között szerepel. docvars(beszedek_dfm, "referencia_pont") <- NA docvars(beszedek_dfm, "referencia_pont")[3] <- -1 docvars(beszedek_dfm, "referencia_pont")[10] <- 1 docvars(beszedek_dfm) #> id felszolalo part referencia_pont #> 1 20142018_024_0002_0002 Vona Gábor (Jobbik) Jobbik NA #> 2 20142018_055_0002_0002 Dr. Schiffer András (LMP) LMP NA #> 3 20142018_064_0002_0002 Dr. Szél Bernadett (LMP) LMP -1 #> 4 20142018_115_0002_0002 Tóbiás József (MSZP) MSZP NA #> 5 20142018_158_0002_0002 Schmuck Erzsébet (LMP) LMP NA #> 6 20142018_172_0002_0002 Dr. Tóth Bertalan (MSZP) MSZP NA #> 7 20142018_206_0002_0002 Volner János (Jobbik) Jobbik NA #> 8 20142018_212_0002_0002 Kósa Lajos (Fidesz) Fidesz NA #> 9 20142018_236_0002_0002 Harrach Péter (KDNP) KDNP NA #> 10 20142018_249_0002_0002 Dr. Gulyás Gergely (Fidesz) Fidesz 1 A lenti wordscores-modell specifikáció követi a Laver, Benoit, and Garry (2003) tanulmányban leírtakat. beszedek_ws <- textmodel_wordscores( x = beszedek_dfm, y = docvars(beszedek_dfm, "referencia_pont"), scale = "linear", smooth = 0 ) summary(beszedek_ws, 10) #> #> Call: #> textmodel_wordscores.dfm(x = beszedek_dfm, y = docvars(beszedek_dfm, #> "referencia_pont"), scale = "linear", smooth = 0) #> #> Reference Document Statistics: #> score total min max mean median #> text1 NA 411 0 6 0.181 0 #> text2 NA 341 0 5 0.150 0 #> text3 -1 383 0 6 0.168 0 #> text4 NA 289 0 5 0.127 0 #> text5 NA 321 0 8 0.141 0 #> text6 NA 298 0 5 0.131 0 #> text7 NA 307 0 5 0.135 0 #> text8 NA 354 0 8 0.156 0 #> text9 NA 250 0 3 0.110 0 #> text10 1 353 0 8 0.155 0 #> #> Wordscores: #> (showing first 10 elements) #> tisztelt elnök országgyűlés ország nemhogy tette fidesz taps padsoraiban #> -0.1027 0.3691 0.0408 -1.0000 -1.0000 -1.0000 1.0000 -1.0000 -1.0000 #> természetesen #> 1.0000 Az illesztett wordscores modellünkkel ezek után már meg tudjuk becsülni a korpuszban lévő többi dokumentum pozícióját. Ehhez a predict() függvény megoldását használjuk. A kiegészítő opciókkal a konfidencia intervallum alsó és felső határát is meg tudjuk becsülni, ami jól jön akkor, ha szeretnénk ábrázolni az eredményt. beszedek_ws_pred <- predict( beszedek_ws, newdata = beszedek_dfm, interval = "confidence") beszedek_ws_pred <- as.data.frame(beszedek_ws_pred$fit) beszedek_ws_pred #> fit lwr upr #> text1 -0.4585 -0.6883 -0.2287 #> text2 -0.2664 -0.4912 -0.0416 #> text3 -0.9448 -0.9660 -0.9236 #> text4 -0.3741 -0.6106 -0.1375 #> text5 -0.3197 -0.5520 -0.0874 #> text6 -0.0379 -0.3474 0.2715 #> text7 0.2343 -0.0316 0.5002 #> text8 -0.0575 -0.3298 0.2149 #> text9 0.0627 -0.2399 0.3653 #> text10 0.9448 0.9194 0.9703 A kapott modellünket a wordfish-hez hasonlóan tudjuk ábrázolni, miután a beszedek_ws_pred objektumból adattáblát csinálunk és a ggplot2-vel elkészítjük a vizualizációt. A dokumentumok_ws két részből áll össze. Először a wordscores modell objektumunkból a frakcióvezetők neveit és pártjaikat emeljük ki (kicsit körülményes a dolog, mert egy komplexebb objektumban tárolja őket a quanteda, de az str() függvény tud segíteni ilyen esetekben). A dokumentumok becsült pontszámait pedig a beszedek_ws_pred objektumból készített data frame hozzácsatolásával adjuk hozzá a már elkészült data frame-hez. Ehhez a dplyr csomag bind_cols függvényét használjuk. Fontos, hogy itt teljesen biztosnak kell lennünk abban, hogy a sorok a két data frame esetében ugyanarra a dokumentumra vonatkoznak. dokumentumok_ws <- data.frame( speaker = beszedek_ws$x@docvars$felszolalo, part = beszedek_ws$x@docvars$part ) dokumentumok_ws <- bind_cols(dokumentumok_ws, beszedek_ws_pred) dokumentumok_ws #> speaker part fit lwr upr #> text1 Vona Gábor (Jobbik) Jobbik -0.4585 -0.6883 -0.2287 #> text2 Dr. Schiffer András (LMP) LMP -0.2664 -0.4912 -0.0416 #> text3 Dr. Szél Bernadett (LMP) LMP -0.9448 -0.9660 -0.9236 #> text4 Tóbiás József (MSZP) MSZP -0.3741 -0.6106 -0.1375 #> text5 Schmuck Erzsébet (LMP) LMP -0.3197 -0.5520 -0.0874 #> text6 Dr. Tóth Bertalan (MSZP) MSZP -0.0379 -0.3474 0.2715 #> text7 Volner János (Jobbik) Jobbik 0.2343 -0.0316 0.5002 #> text8 Kósa Lajos (Fidesz) Fidesz -0.0575 -0.3298 0.2149 #> text9 Harrach Péter (KDNP) KDNP 0.0627 -0.2399 0.3653 #> text10 Dr. Gulyás Gergely (Fidesz) Fidesz 0.9448 0.9194 0.9703 A 9.4-es ábrán a párton belüli bontást illusztráljuk a facet_wrap() segítségével. party_df <- ggplot(dokumentumok_ws, aes(fit, reorder(speaker, fit))) + geom_point() + geom_errorbarh(aes(xmin = lwr, xmax = upr), height = 0) + labs( y = NULL, x = "wordscores" ) + facet_wrap(~part, ncol = 1, scales = "free_y") ggplotly(party_df, height = 1000, tooltip = "fit") Ábra 9.1: A párton belüli wordscores-alapú skála 9.3 Wordfish A wordfish felügyelet nélküli skálázást a quanteda.textmodels csomagban implementált textmodel_wordfish() függvény fogja végezni. A megadott dir = c(1, 2) paraméterrel a két dokumentum relatív \\(\\theta\\) értékét tudjuk rögzíteni, mégpedig úgy hogy \\(\\theta_{dir1} < \\theta_{dir2}\\). Alapbeállításként az algoritmus az első és az utolsó dokumentumot teszi be ide. A lenti példánál mi a pártpozíciók alapján a Jobbikos Vona Gábor és az LMP-s Schiffer András egy-egy beszédét használtuk. A summary() használható az illesztett modellel, és a dokumentumonkénti \\(\\theta\\) koefficienst tudjuk így megnézni. beszedek_wf <- quanteda.textmodels::textmodel_wordfish(beszedek_dfm, dir = c(2, 1)) summary(beszedek_wf) #> #> Call: #> textmodel_wordfish.dfm(x = beszedek_dfm, dir = c(2, 1)) #> #> Estimated Document Positions: #> theta se #> text1 -0.3336 0.0334 #> text2 -1.8334 0.0518 #> text3 -0.9245 0.0356 #> text4 0.0332 0.0390 #> text5 -0.7420 0.0386 #> text6 0.6071 0.0373 #> text7 0.7474 0.0364 #> text8 0.1139 0.0351 #> text9 0.6854 0.0405 #> text10 1.6465 0.0426 #> #> Estimated Feature Scores: #> vona gábor jobbik tisztelt elnök országgyűlés tegnapi napon helyen tartottak időközi önkormányzati választásokat #> beta -0.961 0.201 0.292 -0.268 -0.0345 -0.837 -1.11 -0.257 -0.882 -0.961 -0.961 -0.961 -0.961 #> psi -2.877 -2.106 -0.413 0.263 -0.2174 -0.770 -2.22 -1.682 -1.249 -2.877 -2.877 -2.877 -2.877 #> érdekelt recsken ózdon október nyertünk örömmel közlöm ország közvéleményével amúgy tudnak mindkét jobbikos polgármester #> beta -0.961 -1.11 -1.16 -0.50 -0.961 -0.961 -0.961 -2.205 -0.961 -0.961 -0.404 -0.961 -0.961 -0.961 #> psi -2.877 -2.22 -1.84 -1.41 -2.877 -2.877 -2.877 -0.785 -2.877 -2.877 -1.690 -2.877 -2.877 -2.877 #> maradt tisztségében nemhogy #> beta -0.961 -0.961 -1.89 #> psi -2.877 -2.877 -2.54 Amennyiben szeretnénk a szavak szintjén is megnézni a \\(\\beta\\) (a szavakhoz társított súly, ami a relatív fontosságát mutatja) és \\(\\psi\\) (a szó rögzített hatást (word fixed effects), ami az eltérő szófrekvencia kezeléséért felelős) koefficienseket, akkor a beszedek_wf objektumban tárolt értékeket egy data frame-be tudjuk bemásolni. A dokumentumok hosszát és a szófrekvenciát figyelembe véve, a negatív \\(\\beta\\) értékű szavakat gyakrabban használják a negatív \\(\\theta\\) koefficienssel rendelkező politikusok. szavak_wf <- data.frame( word = beszedek_wf$features, beta = beszedek_wf$beta, psi = beszedek_wf$psi ) szavak_wf %>% arrange(beta) %>% head(n = 15) #> word beta psi #> 1 függőség -5.93 -6.41 #> 2 paks -5.44 -6.11 #> 3 árnak -5.44 -6.11 #> 4 biztonsági -5.44 -6.11 #> 5 brüsszeli -5.04 -5.88 #> 6 helyzet -5.04 -5.88 #> 7 alsó -5.04 -5.88 #> 8 hangon -5.04 -5.88 #> 9 áram -5.04 -5.88 #> 10 ára -5.04 -5.88 #> 11 feltételezzük -5.04 -5.88 #> 12 ár -5.04 -5.88 #> 13 extra -5.04 -5.88 #> 14 tervezett -5.04 -5.88 #> 15 veszélybe -5.04 -5.88 Ez a pozitív értékekre is igaz. szavak_wf %>% arrange(desc(beta)) %>% head(n = 15) #> word beta psi #> 1 czeglédy 6.21 -6.21 #> 2 csaba 6.08 -6.14 #> 3 human 5.76 -5.99 #> 4 operator 5.76 -5.99 #> 5 zrt 5.54 -5.89 #> 6 fizette 5.26 -5.76 #> 7 gyanú 5.26 -5.76 #> 8 szocialista 5.26 -5.76 #> 9 elkövetett 4.85 -5.58 #> 10 tárgya 4.85 -5.58 #> 11 céghálózat 4.85 -5.58 #> 12 diákok 4.85 -5.58 #> 13 májusi 4.85 -5.58 #> 14 júniusi 4.85 -5.58 #> 15 büntetőeljárás 4.85 -5.58 Az eredményeinket mind a szavak, mind a dokumentumok szintjén tudjuk vizualizálni. Elsőként a klasszikus „Eiffel-torony” ábrát reprodukáljuk, ami a szavak gyakoriságának és a skálára gyakorolt befolyásának az illusztrálására szolgál. Ehhez a már elkészült szavak_wf data framet-et és a ggplot2 csomagot fogjuk használni. Mivel a korpuszunk nagyon kicsi, ezért csak 2273 kifejezést fogunk ábrázolni. Ennek ellenére a lényeg kirajzolódik a lenti ábrán is.49 Kihasználhatjuk, hogy a ggplot ábra definiálása közben a felhasznált bemeneti data frame-et különböző szempontok alapján lehet szűrni. Így ábrázolni tudjuk a gyakran használt, ám semleges szavakat (magas \\(\\psi\\), alacsony \\(\\beta\\)), illetve a ritkább, de meghatározóbb szavakat (magas \\(\\beta\\), alacsony \\(\\psi\\)). ggplot(szavak_wf, aes(x = beta, y = psi)) + geom_point(color = "grey") + geom_text_repel( data = filter(szavak_wf, beta > 4.5 | beta < -5 | psi > 0), aes(beta, psi, label = word), alpha = 0.7 ) + labs( x = expression(beta), y = expression(psi) ) Ábra 9.2: A wordfish ‘Eiffel-torony’ Az így kapott ábrán az egyes pontok mind egy szót reprezentálnak, láthatjuk, hogy tipikusan minél magasabb a \\(\\psi\\) értékük annál inkább középen helyezkednek el hiszen a leggyakoribb szavak azok, amelyeket mindenki használ politikai spektrumon való elhelyezkedésüktől függetlenül. Az ábra két szélén lévő szavak azok, amelyek specifikusan a skála egy-egy végpontjához kötődnek. Jelen esetben ezek kevésbé beszédések, mivel a korpusz kifejezetten kis méretű és láthatóan további stopszavazás is szükséges. A dokumentumok szintjén is érdemes megvizsgálni az eredményeket. Ehhez a dokumentum szintű paramétereket fogjuk egy data frame-be gyűjteni: a \\(\\theta\\) ideológiai pozíciót, illetve a beszélő nevét. A vizualizáció kedvéért a párttagságot is hozzáadjuk. A data frame összerakása után az alsó és a felső határát is kiszámoljuk a konfidencia intervallumnak és azt is ábrázoljuk (ld. 9.2. ábra). dokumentumok_wf <- data.frame( speaker = beszedek_wf$x@docvars$felszolalo, part = beszedek_wf$x@docvars$part, theta = beszedek_wf$theta, theta_se = beszedek_wf$se.theta ) %>% mutate( lower = theta - 1.96 * theta_se, upper = theta + 1.96 * theta_se ) ggplot(dokumentumok_wf, aes(theta, reorder(speaker, theta))) + geom_point() + geom_errorbarh(aes(xmin = lower, xmax = upper), height = 0) + labs( y = NULL, x = expression(theta) ) Ábra 9.3: A beszédek egymáshoz viszonyított pozíciója A párt metaadattal összehasonlíthatjuk az egy párthoz tartozó frakcióvezetők értékeit a facet_wrap() használatával. Figyeljünk arra, hogy az y tengelyen szabadon változhasson az egyes rész ábrák között, a scales = \"free\" opcióval (ld. 9.3. ábra). speech_df <- ggplot(dokumentumok_wf, aes(theta, reorder(speaker, theta))) + geom_point() + geom_errorbarh(aes(xmin = lower, xmax = upper), height = 0) + labs( y = NULL, x = "wordscores" ) + facet_wrap(~part, ncol = 1, scales = "free_y") ggplotly(speech_df, height = 1000, tooltip = "theta") Ábra 9.4: Párton belüli pozíciók A mintába nem került be Rogán Antal, akinek csak egy darab napirend előtti felszólalása volt.↩︎ Azért nem Vona Gábor beszédét választottuk, mert az gyaníthatóan egy kiugró érték, ami nem reprezentálja megfelelően a sokaságot.↩︎ A quanteda.textplots csomag több megoldást is kínál az ábrák elkészítésére. Mivel ezek a megoldások kifejezetten a quanteda elemzések ábrázolására készültek, ezért rövid egysoros függvényekkel tudunk gyorsan ábrákat készíteni. A hátrányuk, hogy kevésbé tudjuk „személyre szabni” az ábráinkat, mint a ggplot2 példák esetében. A quanteda.textplots megoldásokat ezen a linken demonstrálják a csomag készítői: https://quanteda.io/articles/pkgdown/examples/plotting.html.↩︎ "],["similarity.html", "10 Szövegösszehasonlítás 10.1 A szövegösszehasonlítás különböző megközelítései 10.2 Lexikális hasonlóság 10.3 Szemantikai hasonlóság 10.4 Hasonlóságszámítás 10.5 Szövegtisztítás 10.6 A Jaccard-hasonlóság számítása 10.7 A koszinusz-hasonlóság számítása 10.8 Az eredmények vizualizációja", " 10 Szövegösszehasonlítás 10.1 A szövegösszehasonlítás különböző megközelítései A gépi szövegösszehasonlítás a mindennapi életünk számos területén megjelenő szövegbányászati technika, bár az emberek többsége nincs ennek tudatában. Ezen a módszeren alapulnak a böngészők kereső mechanizmusai, vagy a kérdés-felelet (Q&A) fórumok algoritmusai, melyek ellenőrzik, hogy szerepel-e már a feltenni kívánt kérdés a fórumon (Sieg 2018). Alkalmazzák továbbá a szövegösszehasonlítást a gépi szövegfordításban és az automatikus kérdésmegválaszolási feladatok esetén is (Wang and Dong 2020), de akár automatizált esszéértékelésre vagy plágiumellenőrzésre is hasznosítható az eljárás (Bar, Zesch, and Gurevych 2011). A szövegösszehasonlítás hétköznapi életben előforduló rejtett alkalmazásain túl a társadalomtudományok művelői is számos esetben hasznosítják az eljárást. A politikatudomány területén többek között használhatjuk arra, hogy eldöntsük, mennyire különböznek egymástól a benyújtott törvényjavaslatok és az elfogadott törvények szövegei, ezzel fontos információhoz jutva arról, hogy milyen szerepe van a parlamenti vitának a végleges törvények kialakításában. Egy másik példa a szakpolitikai prioritásokban és alapelvekben végbemenő változások elemzése, melyet például szakpolitikai javaslatok vagy ilyen témájú viták leiratainak elemzésével is megtehetünk. A könyv korábbi fejezeteiben bemutatott eljárások között több olyat találunk, melyek alkalmasak arra, hogy a szövegek hasonlóságából valamilyen információt nyerjünk. Ugyanakkor vannak módszerek, melyek segítségével számszerűsíthetjük a szövegek közötti különbségeket. Ez a fejezet ezekről nyújt rövid áttekintést. Mindenekelőtt azonban azt kell tisztáznunk, hogy miként értelmezzük a hasonlóságot. A hasonlóságelemzéseket jellemzően két nagy kategóriába szoktuk sorolni a mérni kívánt hasonlóság típusa szerint. Ez alapján beszélhetünk lexikális (formai) és szemantikai hasonlóságról. 10.2 Lexikális hasonlóság A lexikális hasonlóság a gépi szövegfeldolgozás egy egyszerűbb megközelítése, amikor nem várjuk el az elemzésünktől, hogy „értse” a szöveget, csupán a formai hasonlóságot figyeljük. A megközelítés előnye, hogy számítási szempontból jelentősen egyszerűbb, mint a szemantikai hasonlóságra irányuló elemzések, hátránya azonban, hogy az egyszerűség könnyen tévútra vihet szofisztikáltabb elemzések esetén. Így például a lexikális hasonlóság szempontjából az alábbi két példamondat azonosnak tekinthető, hiszen formailag (kifejezések szintjén) megegyeznek. 1. „A boszorkány megsüti Jancsit és Juliskát.” 2. „Jancsi és Juliska megsüti a boszorkányt.” Két dokumentum közötti lexikális hasonlóságot a szöveg számos szintjén mérhetjük: karakterláncok (stringek), szóalakok (tokenek), n-gramok (n egységből álló karakterláncok), szózsákok (bag of words) között, de akár a dokumentum nagyobb egységei, így szövegrészletek és dokumentumok között is. Bevett megközelítés továbbá a szókészlet összehasonlítása, melyet lexikális és szemantikai hasonlóság feltárására egyaránt használhatunk. A hasonlóság számítására számos metrika létezik. Ezek jelentős része valamilyen távolságszámításon alapul, mint például a koszinus-távolság. Ez a metrika két szövegvektor (a két dokumentum-kifejezés mátrix) által bezárt szög alapján határozza meg a hasonlóságot (Wang and Dong 2020). Mindezt az alábbi képlet szerint: \\[ cos(X,Y)=\\frac{X \\cdot Y}{\\|X\\| \\|Y\\|} \\] vagyis kiszámoljuk a két vektor skaláris szorzatát, amelyet elosztunk a vektorok Euklidészi normáinak (gyakran hívják L2 normának is, és ennek segítségével kapjuk meg a vektorok hosszát) szorzatával. Vegyük az alábbi két példamondatot a koszinusz távolság számításának szemléltetésére: 1. Jancsi és Juliska megsüti a boszorkányt. 2. A pék megsüti a kenyeret. A két példamondat (vagyis a dokumentumaink) dokumentum-kifejezés mátrixsza az alábbi táblázat szerint fog kinézni. Az X vektor reprezentálja az 1. példamondatot, az Y vektor pedig a második példamondatot. Táblázat 10.1: Dokumentum-kifejezés mátrix két példamondattal Vektor_név jancsi és juliska megsüti a boszokrkányt pék kenyeret X 1 1 1 1 1 1 0 0 Y 0 0 0 1 2 0 1 1 A két mondat közötti távolság értékét a képlet szerint a következő módon számítjuk ki: \\[ \\frac{x_{1}*y_{1}+x_{2}*y_{2}+x_{3}*y_{3}+x_{4}*y_{4}+x_{5}*y_{5}+x_{6}*y_{6}+x_{7}*y_{7}+x_{8}*y_{8}} {\\sqrt{x_{1}^2+x_{2}^2+x_{3}^2+x_{4}^2+x_{5}^2+x_{6}^2+x_{7}^2+x_{8}^2}* \\sqrt{y_{1}^2+y_{2}^2+y_{3}^2+y_{4}^2+y_{5}^2+y_{6}^2+y_{7}^2+y_{8}^2}} \\] A két példamondat koszinusz-távolságának értéke ennek megfelelően 0,463. \\[ \\frac{1*0+1*0+1*0+1*1+1*2+1*0+0*1+0*1} {\\sqrt{1^2+1^2+1^2+1^2+1^2+1^2+0^2+0^2}* \\sqrt{0^2+0^2+0^2+1^2+2^2+0^2+1^2+1^2}} = \\frac{3}{\\sqrt{6}*\\sqrt{7}}\\approx 0,463 \\] A koszinusz-hasonlóság 0 és 1 közötti értékeket vehet fel. 0-ás értéket akkor kapunk, ha a dokumentumok egyáltalán nem hasonlítanak egymásra. Geometriai értelmeben ebben az esetben a két szövegvektor 90 fokos szöget zár be, hiszen cos(90) = 0 (Ladd 2020). Egy másik széles körben alkalmazott dokumentumhasonlósági metrika a Jaccard-hasonlóság, melynek számítása egy egyszerű eljáráson alapul: a két dokumentumban egyező szavak számát elosztja a két dokumentumban szereplő szavak számának uniójával (vagyis a két dokumentumban szereplő szavak számának összegével, melyből kivonja az egyező szavak számának összegét). A Jaccard-hasonlóság tehát azt képes megmutatni, hogy a két dokumentum teljes szószámához képest mekkora az azonos kifejezések aránya (Niwattanakul et al. 2013, 2.o). Ahogy a koszinusz-hasonlóságnál is, itt is 0 és 1 közötti értéket kapunk, ahol a magasabb érték nagyobb hasonlóságra utal. \\[ Jaccard(doc_{1}, doc_{2}) = \\frac{|doc_{1}\\,\\cap \\, doc_{2}|}{|doc1 \\, \\cup \\, doc2|} = \\frac{|doc_{1} \\, \\cap \\, doc_{2}|}{|doc_{1}| + |doc_{2}| - |doc_{1} \\, \\cap \\, doc_{2} |} \\] 10.3 Szemantikai hasonlóság A szemantikai hasonlóság a lexikai hasonlósággal szemben egy komplexebb számítás, melynek során az algoritmus a szavak tartalmát is képes elemezni. Így például formai szempontból hiába nem azonos az alábbi két példamondat, a szemantikai hasonlóságvizsgálatnak észlelnie kell a tartalmi azonosságot. 1. „A diákok jegyzetelnek, amíg a professzor előadást tart.” 2. „A nebulók írnak, amikor az oktató beszél.” A jelentésbeli hasonlóság kimutatására számos megközelítés létezik. Többek között alkalmazható a témamodellezés (topikmodellezés), melyet a Felügyelet nélküli tanulás fejezetben tárgyaltunk bővebben, ezen belül pedig az LDA Látens Dirichlet-Allokáció (Latent Dirichlet Allocation), valamint az LSA látens érzelemelemzés (Latent Sentiment Analysis) is nagyszerű lehetőséget kínál arra, hogy az egyes dokumentumainkat tartalmi hasonlóságok alapján csoportosítsuk. Az LSA-nél és az LDA-nél azonban egy fokkal komplexebb megközelítés a szóbeágyazás, melyet a Szóbeágyazások című fejezetben mutattunk be. Ez a módszertan a témamodellezéshez képest a szöveg mélyebb szemantikai tartalmait is képes feltárni, hiszen a beágyazásnak köszönhetően képes formailag különböző, de jelentésükben azonos kifejezések azonosságát megmutatni. A jelentésbeli hasonlóság megállapítható a beágyazás során létrehozott vektorreprezentációkból (emlékezzünk: a hasonló vektorreprezentáció hasonló szemantikai tartalomra utal). Kimutathatjuk a szemantikai közelséget például a király – férfi – lovag kifejezések között, de olyan mesterségesen létrehozott jelentésbeli azonosságokat is feltárhatunk, mint az irányítószámok és az általuk jelölt városnevek kapcsolata. Abban az esetben, ha a szóbeágyazást kimondottan a szöveghasonlóság megállapítására szeretnénk használni, a WMD (Word Mover’s Distance) metrikát érdemes használni, mely a vektortérben elhelyezkedő szóvektorok közötti távolság által számszerűsíti a szövegek hasonlóságát (Kusner et al. 2015). 10.4 Hasonlóságszámítás 10.4.1 Adatbázis-importálás és előkészítés A fejezet második felében a lexikai hasonlóság vizsgálatára, ezen belül a Jaccard-hasonlóság és a koszinusz-hasonlóság számítására mutatunk be egy-egy példát a törvényjavaslatok és az elfogadott törvények szövegeinek összehasonlításával. Az alábbiakban bemutatott elemzés a (Sebők et al. 2021) megjelenés előtt álló tanulmányból meríti elemzési fókuszát. Az eredeti cikk által megvalósított elemzést a svájci korpusz elemzése nélkül, a magyar korpusz egy részhalmazán replikáljuk az alábbiakban. A kutatási kérdés arra irányul, hogy mennyiben változik meg a törvényjavaslatok szövege a parlamenti vita folyamán, amíg a javaslat elfogadásra kerül. Az elemzés során a különböző kormányzati ciklusok közötti eltérésekre világítunk rá. Az elemzés megkezdése előtt a már ismert módon betöltjük a szükséges csomagokat. library(stringr) library(dplyr) library(tidyr) library(quanteda) library(quanteda.textstats) library(readtext) library(ggplot2) library(plotly) library(HunMineR) Ezt követően betöltjük azokat az adatbázisokat, amelyeken a szövegösszehasonlítást fogjuk végezni: az elfogadott törvények szövegét tartalmazó korpuszt, a törvényjavaslatok szövegét tartalmazó korpuszt, valamint az ezek összekapcsolását segítő adatbázist, melyben az összetartozó törvényjavaslatok és törvények azonosítóját (id-ját) tároltuk el. Ahogy behívjuk a három adattáblát, érdemes rögtön lekérni az oszlopneveket colnames() és a táblázat dimenzióit dim(), hogy lássuk, milyen adatok állnak a rendelkezésünkre, és mekkora táblákkal fogunk dolgozni. A dim() függvény első értéke a sorok száma, a második pedig az oszlopok száma lesz az adott táblázatban. torvenyek <- HunMineR::data_lawtext_sample colnames(torvenyek) #> [1] "tv_id" "torveny_szoveg" "korm_ciklus" "ev" "korm_ell" dim(torvenyek) #> [1] 600 5 tv_javaslatok <- HunMineR::data_lawprop_sample colnames(tv_javaslatok) #> [1] "tvjav_id" "tvjav_szoveg" dim(tv_javaslatok) #> [1] 600 2 parok <- HunMineR::data_lawsample_match colnames(parok) #> [1] "tv_id" "tvjav_id" dim(parok) #> [1] 600 2 Az importált adatbázisok megfigyeléseinek száma egységesen 600. Ez a több mint háromezer megfigyelést tartalmazó eredeti korpusz egy részhalmaza, mely gyorsabb és egyszerűbb elemzést tesz lehetővé. Az oszlopnevek lekérésével láthatjuk, hogy a törvénykorpuszban van néhány metaadat, amelyet az elemzés során felhasználhatunk: ezek a kormányzati ciklusra, a törvény elfogadásának évére, valamint a benyújtó kormánypárti vagy ellenzéki pártállására vonatkoznak. Ezenkívül rendelkezésre állnak a törvényeket és a törvényjavaslatokat azonosító kódok (tv_id és tvjav_id), melyek segítségével majd tudjuk párosítani az összetartozó törvényjavaslatok és törvények szövegeit. Ezt a left_join() függvénnyel tesszük meg. Elsőként a törvényeket tartalmazó adatbázishoz kapcsoljuk hozzá a törvény–törvényjavaslat párokat tartalmazó adatbázist a törvények azonosítója (tv_id) alapján. A colnames() függvény használatával ellenőrizhetjük, hogy sikeres volt-e a művelet, és az új táblában szerepelnek-e a kívánt oszlopok. tv_tvjavid_osszekapcs <- left_join(torvenyek, parok) colnames(tv_tvjavid_osszekapcs) #> [1] "tv_id" "torveny_szoveg" "korm_ciklus" "ev" "korm_ell" "tvjav_id" dim(tv_tvjavid_osszekapcs) #> [1] 600 6 Második lépésben a törvényjavaslatokat tartalmazó adatbázist rendeljük hozzá az előzőekben már összekapcsolt két adatbázishoz. tv_tvjav_minta <- left_join(tv_tvjavid_osszekapcs, tv_javaslatok) colnames(tv_tvjav_minta) #> [1] "tv_id" "torveny_szoveg" "korm_ciklus" "ev" "korm_ell" "tvjav_id" "tvjav_szoveg" dim(tv_tvjav_minta) #> [1] 600 7 Ha jól végeztük a dolgunkat az adatbázisok összekapcsolása során, az eljárás végére 7 oszlopunk és 600 sorunk van, vagyis az újonnan létrehozott adatbázisba bekerült az összes változó (oszlop). A korpuszaink egy adattáblában való kezelése azért hasznos, mert így nem kell párhuzamosan elvégezni az azonos műveleteket a két korpusz, a törvények és a törvényjavaslatok tisztításához, hanem párhuzamosan tudunk dolgozni a kettővel. Kicsit közelebbről megvizsgálva az adatbázist, megtekinthetjük a metaadatainkat, valamit azt is láthatjuk a count funkció segítségével, hogy minden adatbázisunkban szereplő kormányzati ciklusra 100 megfigyelés áll rendelkezésünkre: tv_tvjav_minta %>% count(korm_ciklus). summary(tv_tvjav_minta) #> tv_id torveny_szoveg korm_ciklus ev korm_ell tvjav_id tvjav_szoveg #> Length:600 Length:600 Length:600 Min. :1994 Min. : 0 Length:600 Length:600 #> Class :character Class :character Class :character 1st Qu.:2000 1st Qu.:900 Class :character Class :character #> Mode :character Mode :character Mode :character Median :2006 Median :900 Mode :character Mode :character #> Mean :2006 Mean :741 #> 3rd Qu.:2012 3rd Qu.:900 #> Max. :2018 Max. :901 tv_tvjav_minta %>% count(korm_ciklus) #> # A tibble: 6 × 2 #> korm_ciklus n #> <chr> <int> #> 1 1994-1998 100 #> 2 1998-2002 100 #> 3 2002-2006 100 #> 4 2006-2010 100 #> 5 2010-2014 100 #> 6 2014-2018 100 Hasonlóan ellenőrizhetjük az egyes évekre eső megfigyelések számát is. tv_tvjav_minta %>% count(ev) #> # A tibble: 24 × 2 #> ev n #> <dbl> <int> #> 1 1994 11 #> 2 1995 31 #> 3 1996 23 #> 4 1997 31 #> 5 1998 17 #> 6 1999 20 #> # ℹ 18 more rows 10.5 Szövegtisztítás Mivel az elemzés során két különböző korpusszal dolgozunk – két oszlopnyi szöveggel –, egyszerűbb, ha a szövegtisztítás lépéseiből létrehozunk egy külön függvényt, amely magában foglalja a művelet egyes lépéseit, és lehetővé teszi, hogy ne kelljen minden szövegtisztítási lépést külön definiálni az egyes korpuszok esetén. A függvény neve jelen esetben szovegtisztitas lesz, és a már ismert lépéseket foglalja magában: kontrollkarakterek szóközzé alakítása, központozás és a számok eltávolítása. Kisbetűsítés, ismétlődő stringek és a stringek előtt található szóközök eltávolítása. Továbbá a str_remove_all() függvénnyel eltávolítjuk azokat az írásjeleket, amelyek előfordulnak a szövegben, de számunkra nem hasznosak. A függvény definiálását az alábbi szintaxissal tehetjük meg. fuggveny <- function(bemenet) { elvegzendo_lepesek return(kimenet) } A bemenet helyen azt jelöljük, hogy milyen objektumon fogjuk végrehajtani a műveleteket, a kimenetet pedig a return() függvénnyel definiáljuk, ez lesz a függvényünk úgynevezett visszatérési értéke, vagyis az elvégzendő lépések szerint átalakított objektum. A szövegtisztító függvény bemeneti és kimeneti értéke is text lesz, mivel ebbe a változóba mentettük az elvégzendő változtatásokat. szovegtisztitas <- function(text) { text = str_replace(text, "[: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) return(text) } Miután létrehoztuk a szövegtisztításra alkalmas függvényünket, az adatbázis két oszlopára fogjuk alkalmazni: a törvények szövegét és a törvényjavaslatok szövegét tartalmazó oszlopra, amiben a mapply() függvény lesz a segítségünkre. A mapply() függvényen belül megadjuk megadjuk az adattáblát és hivatkozunk annak releváns oszlopaira tv_tvjav_minta[ ,c(\"torveny_szoveg\",\"tvjav_szoveg\")]. Az alkalmazni kívánt függvényt a FUN argumentumaként adhatjuk meg – értelemszerűen ez esetünkben az előzőekben létrehozott szovegtisztitas függvény lesz. Végezetül pedig a függvényünk által megtisztított új oszlopokkal felülírjuk az előző adatbázisunk vonatkozó oszlopait, vagyis a torveny_szoveg és a tvjav_szoveg oszlopokat: tv_tvjav_minta[, c(\"torveny_szoveg\",\"tvjav_szoveg\")]. Amennyiben számítunk rá, hogy még változhatnak a szövegeket tartalmazó oszlopok, akkor érdemes előre definiálni a szöveges oszlopok neveit, hogy később csak egy helyen kelljen változtatni a kódon. szovegek <- c("torveny_szoveg", "tvjav_szoveg") tv_tvjav_minta[, szovegek] <- mapply(tv_tvjav_minta[, szovegek], FUN = szovegtisztitas) A szövegtisztítás következő lépése a tiltólistás szavak meghatározása és kiszűrése a szövegből. Itt a quanteda csomagban elérhető magyar nyelvű stopszavakat, valamint a 7. fejezetben meghatározott speciális jogi stopszavak listáját használjuk. legal_stopwords <- HunMineR::data_legal_stopwords A stopszavak beimportálását követően korpusszá alakítjuk a szövegeinket és tokenizáljuk azokat. Ezt már külön-külön végezzük el a törvények és a törvényjavaslatok szövegeire, azonos lépésekben haladva. A létrehozott objektumokat itt is ellenőrizhetjük, például a summary(torvenyek_coprus) paranccsal, vagy a torvenyek_tokens[1:3] paranccsal, mely az első 3 dokumentum tokenjeit fogja megmutatni. torvenyek_corpus <- corpus(tv_tvjav_minta$torveny_szoveg) tv_javaslatok_corpus <- corpus(tv_tvjav_minta$tvjav_szoveg) torvenyek_tokens <- tokens(torvenyek_corpus) %>% tokens_remove(stopwords("hungarian")) %>% tokens_remove(legal_stopwords) %>% tokens_wordstem(language = "hun") tv_javaslatok_tokens <- tokens(tv_javaslatok_corpus) %>% tokens_remove(stopwords("hungarian")) %>% tokens_remove(legal_stopwords) %>% tokens_wordstem(language = "hun") A szövegek tokenizálásával és a tiltólistás szavak eltávolításával a szövegtisztítás végére értünk, így megkezdhetjük az elemzést. 10.6 A Jaccard-hasonlóság számítása A Jaccard-hasonlóság kiszámításához a quanteda.textstats textstat_simil() függvényét fogjuk alkalmazni. Mivel a textstat_simil() függvény dokumentum-kifejezés mátrixot vár bemenetként, elsőként alakítsuk át ennek megfelelően a korpuszainkat. Az előző fejezetekhez hasonlóan itt is a TF-IDF súlyozást választottuk a mátrix létrehozásakor. torvenyek_dfm <- dfm(torvenyek_tokens) %>% dfm_tfidf() tv_javaslatok_dfm <- dfm(tv_javaslatok_tokens) %>% dfm_tfidf() Miután létrehoztuk a dokumentum-kifejezés mátrixokat, érdemes a leggyakoribb tokeneket ellenőrizni a textstat_frequency() függvénnyel, hogy biztosak lehessünk abban, hogy a megfelelő eredményt értük el a szövegtisztítás során. (Amennyiben nem vagyunk elégedettek, érdemes visszatérni a stopszavakhoz és újabb kifejezéseket hozzárendelni a stopszólistához.) tv_toptokens <- textstat_frequency(torvenyek_dfm, n = 10, force = TRUE) tv_toptokens #> feature frequency rank docfreq group #> 1 an 6992 1 131 all #> 2 szerződő 4795 2 150 all #> 3 felhalmozás 3618 3 28 all #> 4 articl 3426 4 74 all #> 5 kiadás 3328 5 224 all #> 6 for 3305 6 100 all #> 7 contracting 3295 7 39 all #> 8 költségvetés 3130 8 206 all #> 9 befektetés 3130 9 73 all #> 10 szanálás 2629 10 13 all tvjav_toptokens <- textstat_frequency(tv_javaslatok_dfm, n = 10, force = TRUE) tvjav_toptokens #> feature frequency rank docfreq group #> 1 an 6051 1 160 all #> 2 szerződő 4533 2 148 all #> 3 articl 3490 3 75 all #> 4 befektetés 3447 4 90 all #> 5 bűncselekmény 3274 5 96 all #> 6 contracting 3266 6 40 all #> 7 szanálás 3235 7 12 all #> 8 bíróság 3226 8 301 all #> 9 egyezmény 3045 9 231 all #> 10 for 3045 10 109 all A létrehozott dokumentum-kifejezés mátrixokon elvégezhetjük a dokumentumhasonlóság-vizsgálatot. A Jaccard-hasonlóság-metrika, illetve a quanteda textstat_simil() függvénye alkalmazható egy korpuszra is. Egy korpuszra végezve az elemzést, a függvény a korpusz dokumentumai közötti hasonlóságot számítja ki, míg két korpuszra mindkét korpusz összes dokumentuma közötti hasonlóságot. Érdemes továbbá azt is megjegyezni, hogy a textstat_simil() method argumentumaként megadható számos más hasonlósági metrika is, melyekkel további érdekes számítások végezhetők. Bővebben a textstat_simil() függény használatáról és argumentumairól a quanteda hivatalos honlapján olvashatunk.50 A textstat_simil() függvény kapcsán azt is érdemes figyelembe venni, hogy mivel nemcsak a dokumentum párokra, hanem az összes bemenetként megadott dokumentumra külön kiszámítja a Jaccard-indexet, a korpusz(ok) méretének növelésével a számítás kapacitás- és időigényessége exponenciálisan növekszik. Két 600 dokumentumból álló korpusz esetén kb. 4–5 perc a számítási idő, míg 360 dokumentum esetén csupán 1–2 perc. jaccard_hasonlosag <- textstat_simil(torvenyek_dfm, tv_javaslatok_dfm, method = "jaccard") Mivel az eredménymátrixunk meglehetősen terjedelmes, nem érdemes az egészet egyben megtekinteni, egyszerűbb az első néhány dokumentum közötti hasonlóságra szűrni, melyet a szögletes zárójelben való indexeléssel tudunk megtenni. Az [1:5, 1:5] kifejezéssel specifikálhatjuk a sorokat és az oszlopkat az elsőtől az ötödikig. jaccard_hasonlosag[1:5, 1:5] #> 5 x 5 Matrix of class "dgeMatrix" #> text1 text2 text3 text4 text5 #> text1 0.5541 0.126 0.1144 0.1250 0.1526 #> text2 0.0983 0.572 0.1278 0.1649 0.0943 #> text3 0.0880 0.124 0.7288 0.1022 0.0764 #> text4 0.1089 0.189 0.1182 0.5720 0.1134 #> text5 0.1504 0.094 0.0881 0.0984 0.6273 A mátrix főátlójában jelennek meg az összetartozó törvényekre és törvényszövegekre vonatkozó értékek, minden más érték nem összetartozó törvény- és törvényjavaslat-szövegek hasonlóságára vonatkozik, vagyis a vizsgálatunk szempontjából irreleváns. A mátrix főátlóját a diag() függvény segítségével nyerhetjük ki. Ha jól dolgoztunk, a létrehozott jaccard_diag első öt eleme (jaccard_diag[1:5]) megegyezik a fent megjelenített 5 × 5-ös mátrix főátlójában elhelyezkedő értékekkel, hossza pedig (length()) a mátrix bármelyik dimenziójával. jaccard_diag <- diag(as.matrix(jaccard_hasonlosag)) jaccard_diag[1:5] #> text1 text2 text3 text4 text5 #> 0.554 0.572 0.729 0.572 0.627 Miután sikerült kinyerni az egyes törvény–törvényjavaslat párokra vonatkozó Jaccard-értéket, érdemes a számításainkat hozzárendelni az eredeti adattáblánkhoz, hogy a meglévő metaadatok fényében tudjuk kiértékelni az egyes dokumentumok közötti hasonlóságot. A hozzárendeléshez egyszerűen definiálunk egy új oszlopot a meglévő adatbázisban tv_tvjav_minta$jaccard_index, melyhez hozzárendeljük a kinyert értékeket. tv_tvjav_minta$jaccard_index <- jaccard_diag Érdemes megnézni a végeredményt, ellenőrizni a Jaccard-hasonlóság legmagasabb vagy legalacsonyabb értékeit. A top_n() függvény használatával ki tudjuk válogatni a legmagasabb és a legalacsonyabb értékeket. A top_n() függvény első argumentuma a változó lesz, ami alapján a legalacsonyabb és a legmagasabb értékeket keressük, a második argumentum pedig azt specifikálja, hogy a legmagasabb és a legalacsonyabb értékek közül hányat szeretnénk látni. Az n=5 értékkel a legmagasabb, az n=-5 értékkel a legalacsonyabb 5 Jaccard-indexszel rendelkező sort tudjuk kiszűrni. Emellett érdemes arra is odafigyelni, hogy a szövegeket tartalmazó oszlopainkat ne próbáljuk meg kiíratni, hiszen ez jelentősen lelassítja az RStudio működését és csökkenti a kiírt eredmények áttekinthetőségét. tvjav_oszlopok <- c( "tv_id", "korm_ciklus", "tvjav_id", "jaccard_index" ) tv_tvjav_minta[, tvjav_oszlopok] %>% top_n(jaccard_index, n = 5) #> # A tibble: 5 × 4 #> tv_id korm_ciklus tvjav_id jaccard_index #> <chr> <chr> <chr> <dbl> #> 1 1994XCV 1994-1998 1994-1998_T0276 0.991 #> 2 1995LXXXI 1994-1998 1994-1998_T1296 0.987 #> 3 1999XXXV 1998-2002 1998-2002_T0807 0.985 #> 4 2013XLII 2010-2014 2010-2014_T10219 0.981 #> 5 2014VIII 2010-2014 2010-2014_T13631 0.980 tv_tvjav_minta[, tvjav_oszlopok] %>% top_n(jaccard_index, n = -5) #> # A tibble: 5 × 4 #> tv_id korm_ciklus tvjav_id jaccard_index #> <chr> <chr> <chr> <dbl> #> 1 1998I 1994-1998 1994-1998_T4328 0.0513 #> 2 2005LII 2002-2006 2002-2006_T16291 0.0532 #> 3 2007CLXVIII 2006-2010 2006-2010_T04678 0.0270 #> 4 2010CLV 2010-2014 2010-2014_T01809 0.0273 #> 5 2012CXCV 2010-2014 2010-2014_T09103 0.00895 10.7 A koszinusz-hasonlóság számítása A Jaccard-hasonlóság számítása után a koszinusz-távolság számítása már nem jelent nagy kihívást, hiszen a textat_simil() függvénnyel ezt is kiszámíthatjuk, csupán a metrika paramétereként (method =) megadhatjuk a koszinuszt is. Ahogy az előbbiekben, itt is a dokumentum-kifejezés mátrixokat adjuk meg bemeneti értékként. koszinusz_hasonlosag <- textstat_simil(x = torvenyek_dfm, y = tv_javaslatok_dfm, method = "cosine") Érdemes itt is megtekinteni a mátrix első néhány sorába és oszlopába eső értékeket. koszinusz_hasonlosag[0:5, 0:5] #> 5 x 5 Matrix of class "dgeMatrix" #> text1 text2 text3 text4 text5 #> text1 0.6238 0.01731 0.01716 0.01699 0.05780 #> text2 0.0118 0.92878 0.00877 0.04633 0.00753 #> text3 0.0155 0.01226 0.98497 0.00662 0.00203 #> text4 0.0202 0.05076 0.00612 0.96063 0.01878 #> text5 0.0763 0.00743 0.00318 0.01804 0.75334 Ebben az esetben is csak a mátrix átlójára van szükségünk, melyet a fent ismertetett módon nyerünk ki a mátrixból. koszinusz_diag <- diag(as.matrix(koszinusz_hasonlosag)) koszinusz_diag[1:5] #> text1 text2 text3 text4 text5 #> 0.624 0.929 0.985 0.961 0.753 Végezetül pedig az átlóból kinyert koszinusz értékeket is hozzárendeljük az adatbázisunkhoz. tv_tvjav_minta$koszinusz <- koszinusz_diag A kibővített adatbázis oszlopneveit a colnames() függvénnyel ellenőrizhetjük, hogy lássuk, valóban sikerült-e hozzárendelnünk a koszinusz értékeket a táblához. colnames(tv_tvjav_minta) #> [1] "tv_id" "torveny_szoveg" "korm_ciklus" "ev" "korm_ell" "tvjav_id" "tvjav_szoveg" #> [8] "jaccard_index" "koszinusz" 10.8 Az eredmények vizualizációja A hasonlósági metrikák vizulizációjára gyakran alkalmazott megoldás a hőtérkép (heatmap), mellyel korrelációs mátrixokat ábrázolhatunk. Ebben az esetben a mátrix értékeit egy színskálán vizualizáljuk, ahol a világosabb színek a magasabb, a sötétebb színek az alacsonyabb értékeket jelölik. A Jaccard-hasonlóság számításakor és a koszinusz-hasonlóság számításakor kapott mátrixok esetén is ábrázolhatjuk az értékeinket ilyen módon. Mivel azonban mindkét mátrix 600 × 600-as, nem érdemes a teljes mátrixot megjeleníteni, mert ilyen nagy mennyiségű adatnál már értelmezhetetlenné válik az ábra, így csak az utolsó 100 elemet, vagyis a 2014–2018-as időszakra vonatkozó értékeket jelenítjük meg. Ezt a koszinusz_hasonlóság nevű objektumunk feldarabolásával tesszük meg, szögletes zárójelben jelölve, hogy a mátrix mely sorait és mely oszlopait szeretnénk használni: koszinusz_hasonlosag[501:600, 501:600]. A koszinusz_hasonlosag objektumból egy data frame-et készítünk, ahol a dokumentumok közötti hasonlóság szerepel. A mátrix formátumból a tidyr csomag pivot_longer() függvényét használva tudjuk a kívánt formátumot elérni. koszinusz_df <- as.matrix(koszinusz_hasonlosag[501:600, 501:600]) %>% as.data.frame() %>% rownames_to_column("docs1") %>% pivot_longer( "text501":"text600", names_to = "docs2", values_to = "similarity" ) glimpse(koszinusz_df) #> Rows: 10,000 #> Columns: 3 #> $ docs1 <chr> "text501", "text501", "text501", "text501", "text501", "text501", "text501", "text501", "text501", "text501", "… #> $ docs2 <chr> "text501", "text502", "text503", "text504", "text505", "text506", "text507", "text508", "text509", "text510", "… #> $ similarity <dbl> 0.95618, 0.02592, 0.04451, 0.03346, 0.01169, 0.04536, 0.07091, 0.27322, 0.04593, 0.00791, 0.02199, 0.11017, 0.0… Ezt követően pedig a ggplot függvényt használva a geom_tile segítségével tudjuk elkészíteni a hőtérképet, ami a hasonlósági mátrixot ábrázolja (ld. 10.1. ábra). koszinusz_plot <- ggplot(koszinusz_df, aes(docs1, docs2, fill = similarity)) + geom_tile() + scale_fill_gradient(high = "#2c3e50", low = "#bdc3c7") + labs( x = NULL, y = NULL, fill = "Koszinusz-hasonlóság" ) + theme( axis.text.x=element_blank(), axis.ticks.x=element_blank(), axis.text.y=element_blank(), axis.ticks.y=element_blank() ) ggplotly(koszinusz_plot, tooltip = "similarity") Ábra 10.1: Koszinusz-hasonlóság hőtérképen ábrázolva A Jaccard-hasonlósági hőtérképet ugyanezzel a módszerrel tudjuk elkészíteni (ld. 10.2. ábra). # adatok átalakítása jaccard_df <- as.matrix(jaccard_hasonlosag[501:600, 501:600]) %>% as.data.frame() %>% rownames_to_column("docs1") %>% pivot_longer( "text501":"text600", names_to = "docs2", values_to = "similarity" ) # a ggplot ábra jaccard_plot <-ggplot(jaccard_df, aes(docs1, docs2, fill = similarity)) + geom_tile() + scale_fill_gradient(high = "#2c3e50", low = "#bdc3c7") + labs( x = NULL, y = NULL, fill = "Jaccard hasonlóság" ) + theme( axis.text.x=element_blank(), axis.ticks.x=element_blank(), axis.text.y=element_blank(), axis.ticks.y=element_blank() ) ggplotly(jaccard_plot, tooltip = "similarity") Ábra 10.2: Jaccard hasonlóság hőtérképen ábrázolva A két ábra összehasonlításánál láthatjuk, hogy a koszinusz-hasonlóság általában magasabb hasonlósági értékeket mutat. A mátrix főátlójában kiugró világos csík azt mutatja meg, hogy a legnagyobb hasonlóság az összetartozó törvény- és törvényjavaslat-szövegek között mutatkozik meg, eddig tehát az adataink az elvárásaink szerinti képet mutatják. Amennyiben a világos csíkot nem látnánk, az egyértelmű visszajelzés volna arról, hogy elrontottunk valamit a szövegelőkészítés eddigi lépései során, vagy a várakozásásaink voltak teljesen rosszak. A koszinusz- és a Jaccard-hasonlóság értékét ábrázolhatjuk közös pont-diagramon a geom_jitter() segítségével (ld. 10.3. ábra). Ehhez először egy kicsit átalakítjuk a data frame-et a tidyr csomag pivot_longer() függvényével, hogy a két hasonlósági érték egy oszlopban legyen. Ez azért szükséges, hogy a ggplot ábránkat könnyebben tudjuk létrehozni. tv_tvjav_tidy <- tv_tvjav_minta %>% pivot_longer( "jaccard_index":"koszinusz", values_to = "hasonlosag", names_to = "hasonlosag_tipus" ) glimpse(tv_tvjav_tidy) #> Rows: 1,200 #> Columns: 9 #> $ tv_id <chr> "1994LXXXII", "1994LXXXII", "1996CXXXI", "1996CXXXI", "1995CXIV", "1995CXIV", "1995XC", "1995XC", "1996LX… #> $ torveny_szoveg <chr> "évi lxxxii törvény a magánszemélyek jövedelemadójáról szóló évi xc törvény módosításáról lábjegyzet a ma… #> $ korm_ciklus <chr> "1994-1998", "1994-1998", "1994-1998", "1994-1998", "1994-1998", "1994-1998", "1994-1998", "1994-1998", "… #> $ ev <dbl> 1994, 1994, 1996, 1996, 1995, 1995, 1995, 1995, 1996, 1996, 1997, 1997, 1994, 1994, 1997, 1997, 1996, 199… #> $ korm_ell <dbl> 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, … #> $ tvjav_id <chr> "1994-1998_T0233", "1994-1998_T0233", "1994-1998_T3167", "1994-1998_T3167", "1994-1998_T1538", "1994-1998… #> $ tvjav_szoveg <chr> "magyar köztársaság kormánya t számú törvényjavaslat a magánszemélyek jövedelemadójáról szóló törvény mód… #> $ hasonlosag_tipus <chr> "jaccard_index", "koszinusz", "jaccard_index", "koszinusz", "jaccard_index", "koszinusz", "jaccard_index"… #> $ hasonlosag <dbl> 0.554, 0.624, 0.572, 0.929, 0.729, 0.985, 0.572, 0.961, 0.627, 0.753, 0.698, 0.978, 0.520, 0.695, 0.711, … jaccard_koszinusz_plot <- ggplot(tv_tvjav_tidy, aes(ev, hasonlosag)) + geom_jitter(aes(shape = hasonlosag_tipus,color = hasonlosag_tipus), width = 0.1, alpha = 0.45) + scale_x_continuous(breaks = seq(1994, 2018, by = 2)) + labs( y = "Jaccard és koszinusz-hasonlóság", shape = NULL, color = NULL, x = NULL ) + theme(legend.position = "bottom", panel.grid.major.x = element_blank(), panel.grid.minor.x = element_blank() ) ggplotly(jaccard_koszinusz_plot) Ábra 10.3: Koszinusz- és Jaccard hasonlóság értékeinek alakulása A pont-diagramm ebben a formájában keveset árul el a számunkra, láthatjuk, hogy a koszinusz értékek általában magasabbak, de azt nem tudjuk meg mondani az egyes pontok alapján, hogy évről évre hogyan alakultak a jaccard és koszinusz értékek. Ezért a hasonlósági értékek évenkénti alakulásának megértése érdekében érdemes átlagot számolni a mutatókra. Ezt a group_by() és a summarize() függvények együttes alkalmazásával tehetjük meg. Megadjuk, hogy évenkénti bontásban szeretnénk a számításainkat elvégezni group_by(ev), és azt, hogy átlagszámítást szeretnénk végezni mean(). evenkenti_atlag <- tv_tvjav_tidy %>% group_by(ev, hasonlosag_tipus) %>% summarize(atl_hasonlosag = mean(hasonlosag)) head(evenkenti_atlag) #> # A tibble: 6 × 3 #> # Groups: ev [3] #> ev hasonlosag_tipus atl_hasonlosag #> <dbl> <chr> <dbl> #> 1 1994 jaccard_index 0.556 #> 2 1994 koszinusz 0.758 #> 3 1995 jaccard_index 0.499 #> 4 1995 koszinusz 0.777 #> 5 1996 jaccard_index 0.675 #> 6 1996 koszinusz 0.887 Az évenkénti átlagot tartalmazó adattáblánkra ezt követően vonal diagramot illesztünk. A 10.4.-es ábrán az látható, hogy a Jaccard- és koszinusz-hasonlóság értéke nagyjából együtt mozog, leszámítva a 2002-es, 2003-as éveket, mivel azonban itt csak néhány adatpont áll rendelkezésre, ettől az inkonzisztenciától eltekinthetünk. jac_cos_ave_df <- ggplot(evenkenti_atlag, aes(ev, atl_hasonlosag)) + geom_line(aes(linetype = hasonlosag_tipus)) + labs(y = "Átlagos Jaccard- és koszinusz-hasonlóság", linetype = NULL, x = NULL) + theme( legend.position = "bottom", panel.grid.major.x = element_blank(), panel.grid.minor.x = element_blank() ) ggplotly(jac_cos_ave_df) Ábra 10.4: Koszinusz- és Jaccard hasonlóság vonal diagramon ábrázolva Ahhoz, hogy valamivel pontosabb képet kapjunk a Jaccard-index értékének alakulásáról, az eredményeinket érdemes vizualizálni a ggplot2 segítségével. Elsőként évenkénti bontásban, boxplotokkal ábrázoljuk a Jaccard-index értékének alakulását (ld. 10.5. ábra). Így már nem csak azt láthatjuk, hogy évente mennyi volt az átlagos jaccard érték, hanem azt is, hogy az átlagtól mekkora eltérések voltak jelen, ez utóbbit természetesen azt is befolyásolja, hogy hány törvényt fogadtak el az adott évben. jac_cos_box_df <- ggplot(tv_tvjav_minta, aes(as.factor(ev), jaccard_index)) + geom_boxplot() + labs( x = NULL, y = "Jaccard-hasonlóság" ) + theme( axis.text.x = element_text(angle = 45), legend.position = "none" ) ggplotly(jac_cos_box_df) Ábra 10.5: Jaccard hasonlóság boxplotokkal ábrázolva A legalacsonyabb Jaccard-hasonlóság talán az 1994–1998-as időszakra jellemző, míg a 2014–2018-as időszakra szembetűnően magas Jaccard-értékeket látunk az első ábrázolt ciklushoz képest. Összességében nehéz trendet látni az ábrán, de érdemes azt is megjegyezni, hogy a negatív irányba kiugró adatpontok a 2014–2018-as ciklusban jelentősen nagyobb arányban tűnnek fel, mint a korábbi kormányzati ciklusok alatt. Végezetül pedig ábrázolhatjuk a Jaccard-hasonlóságot a benyújtó személye alapján is a korm_ell változónk alapján. A változók értékei a következők a CAP kódkönyve alapján: 0 - Ellenzéki benyújtó 1 - Kormánypárti benyújtó 2 - Kormánypárti és ellenzéki benyújtó közösen 3 - Benyújtók legalább két ellenzéki pártból 4 - Benyújtók legalább két kormánypártból 900 - Nem releváns – a benyújtó a kabinet tagja 901 - Nem releváns – a benyújtó a bizottság tagja 902 - Nem releváns – a benyújtó sem a parlamentnek, sem a kabinetnek, sem a bizottságnak nem tagja Mivel nincs túl sok adatpontunk, és ezek többsége a 900-as adatpont alá esik (lásd tv_tvjav_minta %>% count(korm_ell)), érdemes összevonni a 0-ás és a 3-as változót, valamint az 1-es és a 4-es változót egy-egy értékbe, hogy jobban elemezhetőek legyenek az eredményeink. Ehhez a korm_ell változó értékei alapján definiálunk egy új korm_ell2 változót. Az új változó definiálását és az értékadásokat a dplyr case_when() függvényével fogjuk megtenni. A függvényen belül a bal oldalra kerül, hogy milyen értékek alapján szeretnénk az új értéket meghatározni, a tilde (~) után pedig az, hogy mi legyen az újonnan létrehozott oszlop értéke. Tehát a case_when()-en belül lévő első sor azt fejezi ki, hogy amennyiben a korm_ell egyenlő 0-val, vagy (|) a korm_ell egyenlő 3-mal, legyen a korm_ell2 értéke 0. tv_tvjav_minta <- tv_tvjav_minta %>% mutate( korm_ell2 = case_when( korm_ell == 0 | korm_ell == 3 ~ "Ellenzéki képviselő", korm_ell == 1 | korm_ell == 4 ~ "Kormánypárti képviselő", korm_ell == 2 ~ "Kormánypárti és ellenzéki képviselők közösen", korm_ell == 900 ~ "Kabinet tagja", korm_ell == 901 ~ "Parlamenti bizottság", korm_ell == 902 ~ "Egyik sem" ), korm_ell2 = as.factor(korm_ell2) ) Miután létrehoztuk az új oszlopot, létrehozhatjuk a vizualizációt is annak alapján. Itt egy speciális pontdiagramot fogunk használni: geom_jitter(). Ez annyiban különbözik a pontdiagramtól, hogy kicsit szórtabban ábrázolja a diszkrét értékekre (évekre) eső pontokat, hogy az egy helyen sűrűsödő értékek ne takarják ki egymást. A facet_wrap segítségével tudjuk kategóriánként ábrázolni az évenkénti hasonlóságot (ld. 10.6. ábra). jaccard_be_plot <- ggplot(tv_tvjav_minta, aes(ev, jaccard_index)) + geom_jitter(width = 0.1, alpha = 0.5) + scale_x_continuous(breaks = seq(1994, 2018, by = 2)) + facet_wrap(~ korm_ell2, ncol = 1) + theme(panel.spacing = unit(2, "lines"))+ labs( y = NULL, x = NULL) ggplotly(jaccard_be_plot, height = 1000) Ábra 10.6: Jaccard hasonlóság a benyújtó személye alapján Mivel a törvényjavaslatok túlnyomó többségét a kabinet tagjai nyújtják be, nem igazán tudunk érdemi következtetéseket levonni arra vonatkozóan, hogy az ellenzéki vagy a kormánypárti képviselők által benyújtott javaslatok módosulnak-e többet a vita folyamán. Amennyiben ezzel a kérdéssel alaposabban is szeretnénk foglalkozni, érdemes csak azokat a sorokat kiválasztani a hasonlóságszámításhoz, amelyekben a számunkra releváns megfigyelések szerepelnek. Ha azonban ezt az eljárást választjuk, mindenképpen fontos odafigyelni arra is, hogy az elemzésben használandó megfigyelések kiválogatása nehogy szelektív legyen valamely nem megfigyelt változó szempontjából, ezzel befolyásolva a kutatás eredményeit. https://quanteda.io/reference/textstat_simil.html↩︎ "],["nlp_ch.html", "11 NLP és névelemfelismerés 11.1 Fogalmi alapok 11.2 A spacyr használata", " 11 NLP és névelemfelismerés 11.1 Fogalmi alapok A természetes-nyelv feldolgozása (Natural Language Processing – NLP) a nyelvészet és a mesterséges intelligencia közös területe, amely a számítógépes módszerek segítségével elemzi az emberek által használt (természetes) nyelveket. Azaz képes feldolgozni különböző szöveges dokumentumok tartalmát, kinyerni a bennük található információkat, kategorizálni és rendszerezni azokat. A névelem-felismerés többféle módon is megoldható, így például felügyelt tanulással, szótár alapú módszerekkel vagy kollokációk elemzésével. A névelem-felismerés körében két alapvető módszer alkalmazására van lehetőség. A szabályalapú módszer alkalmazása során előre megadott adatok alapján kerül kinyerésre az információ (ilyen szabály például a mondatközi nagybetű mint a tulajdonnév kezdete). A másik módszer a statisztikai tanulás, amikor a gép alkot szabályokat a kutató előzetes mintakódolása alapján. A névelemfelismerés során nehézséget okozhat a különböző névelemosztályok közötti gyakori átfedés, így például ha egy adott szó településnév és vezetéknév is lehet. Fontos különbséget tenni a névelem-felismerés és a tulajdonnév-felismerés között. A névelem-felismerésbe beletartozik minden olyan kifejezés, amely a világ valamely entitására egyedi módon (unikálisan) referál. Ezzel szemben a tulajdonnév-felismerés, kizárólag a tulajdonnevekre koncentrál.(Üveges 2019; Vincze 2019) A fejezetben részletesen foglalkozunk a lemmatizálással, ami a magyar nyelvű szövegek szövegelőkészítésnek fontos eleme (erről lásd bővebben a Korpuszépítés és szövegelőkészítés fejezetet), és a névelem-felismeréssel (Named Entity Recognition – NER). Névelemnek azokat a tokensorozatokat nevezzük, amelyek valamely entitást egyedi módon jelölnek. A névelem-felismerés az infomációkinyerés részterülete, melynek lényege, hogy automatikusan felismerjük a strukturálátlan szövegben szereplő tulajdonneveket, majd azokat kigyűjtsük, és típusonként (például személynév, földrajzi név, márkanév, stb.) csoportosítsuk. Bár a tulajdonnevek mellett névelemnek tekinthetők még például a telefonszámok vagy az e-mail címek is, a névelem-felismerés leginkább mégis a tulajdonnevek felismerésére irányul. A névelem-felismerés a számítógépes nyelvészetben a korai 1990-es évektől kezdve fontos feladatnak és megoldandó problémának számít. A magyar nyelvű szövegekben a tulajdonnevek automatikus felismerésére jelen kötetben a huSpaCy 51 elemző használatát mutatjuk be, amely képes mondatok teljes nyelvi elemzésére (szótő, szófajok, stb.) illetve névelemek (például személynevek, helységek) azonosítására is folyó szövegben, emellett alkalmas a magyar nyelvű szövegek lemmatizálására, ami amint azt korábban bemutattuk, a magyar nyelvű szövegek előfeldolgozásának fontos lépése. Bár magyar nyelven más elemzők 52 is képesek a nyers szövegek mondatra és szavakra bontására és szófaji elemzésére, azaz POS-taggelésére (Part of Speech-tagging) továbbá a mondatok függőségi elemzésére, két okból döntöttünk a huSpaCy használata mellett. Egyrészt ez a illeszkedik a nemzetközi akadémia és ipari szférában legszélesebb körben használt SpaCy 53 keretrendszerbe, másrészt a reticulate 54 csomag segítségével viszonylag egyszerűen használható R környezetben is, és nagyon jól együttműködik a kötetben rendszeresen használt quanteda csomaggal. 11.2 A spacyr használata library(reticulate) library(spacyr) library(dplyr) library(stringr) library(quanteda) library(quanteda.textplots) library(HunMineR) A spaCy használatához Python környezet szükséges, az első használat előtt telepíteni kell a számítógépünkre egy Anaconda alkalmazást: https://www.anaconda.com/. Majd az RStudio/Tools/Global Options menüjében be kell állítanunk a Pyton interpretert, azaz meg kell adnunk, hogy a gépünkön hol található a feltelepített Anaconda. Ezt csak az első használat előtt kell megtennünk, a későbbiekben innen folytathatjuk a modell betöltését. Ezt követően a már megszokott módon installálnunk kell a reticulate és a spacyr 55 csomagot és telepítenünk a magyar nyelvi modellt. A Pythonban készült spacy-t a spacyr::spacy_install() paranccsal kell telepíteni. A következő lépésben létre kell hoznunk egy conda környezetet, és a huggingface-ről be kell töltenünk a magyar modellt. conda_install(envname = "spacyr", "https://huggingface.co/huspacy/hu_core_news_lg/resolve/v3.5.2/hu_core_news_lg-any-py3-none-any.whl" , pip = TRUE) spacy_initialize(model = "hu_core_news_lg", condaenv="spacyr") 11.2.1 Lemmatizálás, tokenizálás, szófaji egyértelműsítés Ezután a spacy_parse() függvény segítségével lehetőségünk van a szövegek tokenizálására, szótári alakra hozására (lemmatizálására) és szófaji egyértelműsítésére. txt <- c(d1 = "Budapesten süt a nap.", d2 = "Tájékoztatom önöket, hogy az ülés vezetésében Hegedűs Lorántné és Szűcs Lajos jegyzők lesznek segítségemre.") parsedtxt <- spacy_parse(txt) print(parsedtxt) #> doc_id sentence_id token_id token lemma pos entity #> 1 d1 1 1 Budapesten Budapest PROPN LOC_B #> 2 d1 1 2 süt süt VERB #> 3 d1 1 3 a a DET #> 4 d1 1 4 nap nap NOUN #> 5 d1 1 5 . . PUNCT #> 6 d2 1 1 Tájékoztatom tájékoztat VERB #> 7 d2 1 2 önöket ön NOUN #> 8 d2 1 3 , , PUNCT #> 9 d2 1 4 hogy hogy SCONJ #> 10 d2 1 5 az az DET #> 11 d2 1 6 ülés ülés NOUN #> 12 d2 1 7 vezetésében vezetés NOUN #> 13 d2 1 8 Hegedűs Hegedűs PROPN PER_B #> 14 d2 1 9 Lorántné Lorántné PROPN PER_I #> 15 d2 1 10 és és CCONJ #> 16 d2 1 11 Szűcs Szűcs PROPN PER_B #> 17 d2 1 12 Lajos Lajos PROPN PER_I #> 18 d2 1 13 jegyzők jegyző NOUN #> 19 d2 1 14 lesznek lesz AUX #> 20 d2 1 15 segítségemre segítség NOUN #> 21 d2 1 16 . . PUNCT Láthatjuk, hogy az eredmény egy olyan tábla, amely soronként tartalmazza a lemmákat. Mivel az elemzések során legtöbbször arra van szükségünk, hogy egy teljes szöveg lemmáit egy egységként kezeljük, a kapott táblán el kell végeznünk néhány átlakítást. Mivel nekünk a lemmákra van szükségünk, először is töröljük az összes oszlopot a doc_id és a lemma kivételével. parsedtxt$sentence_id <- NULL parsedtxt$token_id <- NULL parsedtxt$token <- NULL parsedtxt$pos <- NULL parsedtxt$entity <- NULL parsedtxt #> doc_id lemma #> 1 d1 Budapest #> 2 d1 süt #> 3 d1 a #> 4 d1 nap #> 5 d1 . #> 6 d2 tájékoztat #> 7 d2 ön #> 8 d2 , #> 9 d2 hogy #> 10 d2 az #> 11 d2 ülés #> 12 d2 vezetés #> 13 d2 Hegedűs #> 14 d2 Lorántné #> 15 d2 és #> 16 d2 Szűcs #> 17 d2 Lajos #> 18 d2 jegyző #> 19 d2 lesz #> 20 d2 segítség #> 21 d2 . Majd a doc_id segítségével összakapcsoljuk azokat a lemmákat, amelyek egy dokumentumhoz tartoznak és az egyes lemmákat ; segítsével elválasztjuk elmástól. parsedtxt_2<- parsedtxt %>% group_by(doc_id) %>% mutate(text = str_c(lemma, collapse = ";")) parsedtxt_2 #> # A tibble: 21 × 3 #> # Groups: doc_id [2] #> doc_id lemma text #> <chr> <chr> <chr> #> 1 d1 Budapest Budapest;süt;a;nap;. #> 2 d1 süt Budapest;süt;a;nap;. #> 3 d1 a Budapest;süt;a;nap;. #> 4 d1 nap Budapest;süt;a;nap;. #> 5 d1 . Budapest;süt;a;nap;. #> 6 d2 tájékoztat tájékoztat;ön;,;hogy;az;ülés;vezetés;Hegedűs;Lorántné;és;Szűcs;Lajos;jegyző;lesz;segítség;. #> # ℹ 15 more rows Mivel az eredeti táblában minden lemma az eredeti az azt tartalmazó dokumentum id-jét kapta meg, az így létrehozott táblánkban a szövegek annyiszor ismétlődnek, ahány lemmából álltak. Ezért egy következő lépésben ki kell törtölnünk a feleslegesen ismétlődő sorokat. Ehhez először töröljük a lemma oszlopot, hogy a sorok tökéletesen egyezzenek. parsedtxt_2$lemma <- NULL parsedtxt_2 #> # A tibble: 21 × 2 #> # Groups: doc_id [2] #> doc_id text #> <chr> <chr> #> 1 d1 Budapest;süt;a;nap;. #> 2 d1 Budapest;süt;a;nap;. #> 3 d1 Budapest;süt;a;nap;. #> 4 d1 Budapest;süt;a;nap;. #> 5 d1 Budapest;süt;a;nap;. #> 6 d2 tájékoztat;ön;,;hogy;az;ülés;vezetés;Hegedűs;Lorántné;és;Szűcs;Lajos;jegyző;lesz;segítség;. #> # ℹ 15 more rows Majd a következő lépésben a dplyr csomag distinct függvénye segítségével - amely mindig csak egy-egy egyedi sort tart meg az adattáblában - kitöröljük a felesleges sorokat. parsedtxt_3 <-distinct(parsedtxt_2) parsedtxt_3 #> # A tibble: 2 × 2 #> # Groups: doc_id [2] #> doc_id text #> <chr> <chr> #> 1 d1 Budapest;süt;a;nap;. #> 2 d2 tájékoztat;ön;,;hogy;az;ülés;vezetés;Hegedűs;Lorántné;és;Szűcs;Lajos;jegyző;lesz;segítség;. Az így létrejött adattáblában a text mezőben már nem az eredeti szöveg, hanem annak lemmái szerepelnek. Ha az adattáblát elmentjük, a lemmákon végezhetjük tovább az elemzéseket. 11.2.2 Saját .txt vagy .csv fájlokban elmentett szövegek lemmatizálása Saját .txt vagy .csv fájlok lemmatizálásához a fájlokat a kötetben bemutatott módon olvassuk be egy adattáblába. (ehhez lásd a Függelék, Munka saját adatokkal alfejezetét) Az alábbi példában egy, a HunMineR csomagban lévő kisebb korpuszon mutatjuk be az ilyen fájlok lemmatizálását. Fontos kiemelni, hogy a nagyobb fájlok feldolgozása elég sok időt (akár több órát) is igénybe vehet. df <- HunMineR::data_parlspeakers_small A beolvasott szövegeket először quanteda korpusszá alakítjuk. Majd a spacy_parsed() függvény segítségével a fentebb bemutatottak szerint elvégezzük a lemmatizálást. df_corpus <- corpus(df) parsed_df <- spacy_parse(df_corpus) head(parsed_df, 5) #> doc_id sentence_id token_id token lemma pos entity #> 1 text1 1 1 VONA VONA PROPN #> 2 text1 1 2 GÁBOR GÁBOR PROPN #> 3 text1 1 3 ( ( PUNCT #> 4 text1 1 4 Jobbik Jobbik PROPN PER_B #> 5 text1 1 5 ): ): PUNCT parsed_df$sentence_id <- NULL parsed_df$token_id <- NULL parsed_df$token <- NULL parsed_df$pos <- NULL parsed_df$entity <- NULL parsed_df_2<- parsed_df %>% group_by(doc_id) %>% mutate( text = str_c(lemma, collapse = " ")) parsed_df_2$lemma <- NULL parsed_df_3 <-distinct(parsed_df_2) head(parsed_df_3, 5) #> # A tibble: 5 × 2 #> # Groups: doc_id [5] #> doc_id text #> <chr> <chr> #> 1 text1 "VONA GÁBOR ( Jobbik ): Tisztelt Elnök Úr ! tisztelt Országgyűlés ! a tegnapi nap 11 hely tart időközi önkormányzati válas… #> 2 text2 "dr. SCHIFFER ANDRÁS ( LMP ): Köszön a szó , elnök úr . tisztelt Országgyűlés ! múlt hét napvilág lát a hír , hogy az ille… #> 3 text3 "dr. SZÉL BERNADETT ( LMP ): Köszön a szó , elnök úr . tisztelt Államtitkár Úr ! van egy ország Európa , amely pusztító te… #> 4 text4 "TÓBIÁS JÓZSEF ( MSZP ): Köszön a szó , elnök úr . tisztelt Ház ! tisztelt Képviselőtárs ! az hisz , az mindannyi egyetért… #> 5 text5 "SCHMUCK ERZSÉBET ( LMP ): Köszön a szó , elnök úr . tisztelt Országgyűlés ! hét , hónap óta rágódik az ország a Magyar Ne… A nagyobb fájlok lemmatizálásának eredményét célszerű elmenteni a kötetben ismert módok egyikén például RDS vagy .csv fájlba. 11.2.3 Névelemfelismerés és eredményeinek vizualizálása A szövegekből történő névelemfelismeréshez ugyancsak egy adattáblára és egy belőle kialakított quanteda korpuszra van szükségünk. A következő példában mi az előzőleg léterhozott lemmatizált adattáblával dolgozunk, de a névelemfelismerés működik nyers szövegeken is. A léterhozott korpuszon a spacy_parse() függvény argumentumában kell jeleznünk, hogy az entitások felismerését szeretnénk elvégezni (entity = TRUE). Az eredménytáblában láthatjuk, hogy egy új oszlopban minden névelem mellett megkaptuk annak típusát (PER = személynév, LOC = helynév, ORG = szervezet, MISC = egyéb). A corpus() függvény egyedi dokumentum neveket vár, ezért átnevezzük először a doc_id értékeit. parsed_df_3 <- parsed_df_3 %>% mutate(doc_id = paste(doc_id, row_number(), sep = "-")) lemma_corpus <- corpus(parsed_df_3) parsedtxt <- spacy_parse(lemma_corpus, entity = TRUE) entity_extract(parsedtxt, type = "all") #> doc_id sentence_id entity entity_type #> 1 text1-1 1 Jobbik PER #> 2 text1-1 1 Úr PER #> 3 text1-1 1 Országgyűlés PER #> 4 text1-1 3 Jobbik ORG #> 5 text1-1 3 Recsken LOC #> 6 text1-1 3 Ózd LOC #> 7 text1-1 7 Recsken LOC #> 8 text1-1 7 Fidesz ORG #> 9 text1-1 8 Ózd_Janiczak_Dávid PER #> 10 text1-1 9 Jobbik ORG #> 11 text1-1 11 Janiczak_Dávid PER #> 12 text1-1 11 Jobbik ORG #> 13 text1-1 12 Ózd LOC #> 14 text1-1 14 Vida_Ildikó PER #> 15 text1-1 15 Dr._Répássy_Róbert PER #> 16 text1-1 21 Oroszország LOC #> 17 text1-1 21 Egyesült_Államok LOC #> 18 text1-1 21 Ukrajna LOC #> 19 text1-1 22 Gorbacsov PER #> 20 text1-1 23 Magyarország ORG #> 21 text1-1 23 Magyarország LOC #> 22 text1-1 25 Gyurcsány_Ferenc PER #> 23 text1-1 26 Dr._Rétvári_Bence PER #> 24 text1-1 27 Jobbik ORG #> 25 text1-1 29 Fidesz-KDNP ORG #> 26 text1-1 29 Vida_Ildikó PER #> 27 text1-1 31 Kovács_Béla PER #> 28 text1-1 32 Kovács_Béla PER #> 29 text1-1 34 Isten PER #> 30 text1-1 36 Magyarország LOC #> 31 text1-1 37 Magyarország LOC #> 32 text1-1 42 Magyarország ORG #> 33 text1-1 44 Magyarország LOC #> 34 text1-1 48 Jobbik ORG #> 35 text2-1 1 dr._SCHIFFER_ANDRÁS ORG #> 36 text2-1 1 LMP ORG #> 37 text2-1 2 Országgyűlés PER #> 38 text2-1 2 Eurat MISC #> 39 text2-1 2 Orbán-Putyin-paktum MISC #> 40 text2-1 3 L._Simon_László PER #> 41 text2-1 5 Paks LOC #> 42 text2-1 11 Paks ORG #> 43 text2-1 12 Magyarország LOC #> 44 text2-1 13 Westinghouse MISC #> 45 text2-1 14 Paksi_Atomerőműbe ORG #> 46 text2-1 15 Ukrajna LOC #> 47 text2-1 17 Magyarország LOC #> 48 text2-1 17 Európa-USA ORG #> 49 text2-1 18 Magyarország LOC #> 50 text2-1 18 Magyarország LOC #> 51 text2-1 19 Magyarország LOC #> 52 text2-1 20 Magyarország LOC #> 53 text2-1 21 Magyarország LOC #> 54 text2-1 23 Magyarország LOC #> 55 text2-1 25 Európai_Unió ORG #> 56 text2-1 25 Amerika LOC #> 57 text2-1 29 LMP ORG #> 58 text3-1 1 dr._SZÉL_BERNADETT ORG #> 59 text3-1 1 LMP PER #> 60 text3-1 2 Államtitkár_Úr PER #> 61 text3-1 2 Európa LOC #> 62 text3-1 3 Nyugat-Európa LOC #> 63 text3-1 9 Magyarország LOC #> 64 text3-1 16 Németország LOC #> 65 text3-1 20 Magyarország LOC #> 66 text3-1 30 LMP ORG #> 67 text3-1 35 LMP ORG #> 68 text3-1 36 LMP ORG #> 69 text3-1 41 LMP ORG #> 70 text3-1 41 Országgyűlés ORG #> 71 text3-1 45 Ház ORG #> 72 text3-1 46 LMP ORG #> 73 text4-1 1 TÓBIÁS_JÓZSEF PER #> 74 text4-1 1 MSZP PER #> 75 text4-1 2 Ház PER #> 76 text4-1 2 Képviselőtárs PER #> 77 text4-1 4 Magyarország LOC #> 78 text4-1 5 Magyar_Tudományos_Akadémia ORG #> 79 text4-1 5 Magyarország LOC #> 80 text4-1 10 Ázsia LOC #> 81 text4-1 10 Európa LOC #> 82 text4-1 10 Európa LOC #> 83 text4-1 12 Balog_Zoltán PER #> 84 text4-1 13 Magyarország LOC #> 85 text4-1 16 Magyarország LOC #> 86 text4-1 19 Országgyűlés ORG #> 87 text4-1 23 MSZP ORG #> 88 text5-1 1 LMP PER #> 89 text5-1 2 Országgyűlés PER #> 90 text5-1 3 Magyar_Nemzeti_Bank ORG #> 91 text5-1 3 Matolcsy_György PER #> 92 text5-1 4 Matolcsy PER #> 93 text5-1 9 Magyarország LOC #> 94 text5-1 12 Fidesz ORG #> 95 text5-1 12 Matolcsy_György PER #> 96 text5-1 14 Fidesz ORG #> 97 text5-1 15 Fidesz ORG #> 98 text5-1 20 Magyarország LOC #> 99 text5-1 21 Országgyűlés PER #> 100 text5-1 21 Matolcsy_György PER #> 101 text5-1 21 Nemzeti_Bank ORG #> 102 text5-1 21 Magyarország LOC #> 103 text5-1 22 Matolcsy_György PER #> 104 text5-1 23 Matolcsy_György PER #> 105 text5-1 24 Matolcsy_György PER #> 106 text5-1 25 Magyarország LOC #> 107 text5-1 26 Magyarország LOC #> 108 text5-1 31 Magyarország LOC #> 109 text6-1 1 dr._TÓTH_BERTALAN PER #> 110 text6-1 1 MSZP PER #> 111 text6-1 2 Képviselőtárs PER #> 112 text6-1 2 Fidesz ORG #> 113 text6-1 3 Fidesz ORG #> 114 text6-1 3 Fidesz ORG #> 115 text6-1 3 Jobbik ORG #> 116 text6-1 12 Navracsics_Tibor PER #> 117 text6-1 13 MSZP ORG #> 118 text6-1 14 Alaptörvény MISC #> 119 text6-1 15 MSZP ORG #> 120 text6-1 15 Népszabadság ORG #> 121 text6-1 15 Alaptörvény MISC #> 122 text6-1 18 Kunhalmi_Ágnes PER #> 123 text6-1 18 Fidesz ORG #> 124 text6-1 18 Parlament ORG #> 125 text6-1 19 MSZP ORG #> 126 text6-1 19 Origo MISC #> 127 text6-1 19 vs.hu ORG #> 128 text6-1 19 TV2 ORG #> 129 text6-1 19 Népszabadság ORG #> 130 text6-1 20 Soltész_Miklós PER #> 131 text6-1 20 Népszava MISC #> 132 text6-1 20 Fidesz ORG #> 133 text6-1 23 Fidesz ORG #> 134 text6-1 24 Rogán_Antal PER #> 135 text6-1 24 Orbán_Viktor PER #> 136 text6-1 24 Habony_Árpád PER #> 137 text6-1 24 Orbán_Viktor PER #> 138 text6-1 24 Orbán_Viktor PER #> 139 text6-1 26 Orbán_Viktor PER #> 140 text6-1 26 Németh_Szilárd_István PER #> 141 text6-1 27 Országgyűlés ORG #> 142 text6-1 28 Országgyűlés PER #> 143 text6-1 28 Magyarország ORG #> 144 text6-1 28 Magyarország LOC #> 145 text6-1 30 Orbán_Viktor PER #> 146 text6-1 30 Fidesz ORG #> 147 text6-1 32 MSZP ORG #> 148 text6-1 33 Fodor_Gábor PER #> 149 text7-1 1 VOLNER_János PER #> 150 text7-1 1 Jobbik PER #> 151 text7-1 2 Képviselőtárs PER #> 152 text7-1 2 Jobbik ORG #> 153 text7-1 3 Viktor PER #> 154 text7-1 5 Magyarország LOC #> 155 text7-1 6 Legfőbb_Ügyészség ORG #> 156 text7-1 6 Állami_Számvevőszék ORG #> 157 text7-1 6 Fidesz ORG #> 158 text7-1 7 Fidesz ORG #> 159 text7-1 7 Állami_Számvevőszék ORG #> 160 text7-1 7 Magyarország LOC #> 161 text7-1 8 Fidesz ORG #> 162 text7-1 8 MSZP ORG #> 163 text7-1 10 Magyarország LOC #> 164 text7-1 18 off­shore MISC #> 165 text7-1 19 Jobbik ORG #> 166 text7-1 20 Jobbik ORG #> 167 text8-1 1 KÓSA_LAJOS ORG #> 168 text8-1 1 Fidesz ORG #> 169 text8-1 1 Ház PER #> 170 text8-1 1 Európa LOC #> 171 text8-1 1 Stockholm LOC #> 172 text8-1 1 Egyiptom LOC #> 173 text8-1 1 Európa LOC #> 174 text8-1 2 Soros_György PER #> 175 text8-1 2 Európa LOC #> 176 text8-1 3 Európa LOC #> 177 text8-1 3 Németország LOC #> 178 text8-1 4 Németország LOC #> 179 text8-1 6 Magyarország LOC #> 180 text8-1 6 Délnyugat-Balkán LOC #> 181 text8-1 6 Magyarország LOC #> 182 text8-1 6 Magyarország LOC #> 183 text8-1 7 Soros PER #> 184 text8-1 7 Korózs_Lajos PER #> 185 text8-1 9 MSZP ORG #> 186 text8-1 11 Kunhalmi_Ágnes PER #> 187 text8-1 12 Korózs_Lajos PER #> 188 text8-1 12 Fidesz ORG #> 189 text8-1 14 Korózs_Lajos PER #> 190 text8-1 14 Bangóné_Borbély_Ildikó PER #> 191 text8-1 15 Közép-európai_Egyetem ORG #> 192 text8-1 15 Kunhalmi_Ágnes PER #> 193 text8-1 16 Ház PER #> 194 text8-1 16 Soros_György PER #> 195 text8-1 17 Soros_György PER #> 196 text8-1 17 Soros_György PER #> 197 text8-1 17 Soros_György PER #> 198 text8-1 17 Soros PER #> 199 text8-1 17 Soros PER #> 200 text8-1 17 Magyarország LOC #> 201 text8-1 17 Magyarország ORG #> 202 text8-1 19 Magyarország ORG #> 203 text9-1 1 KDNP PER #> 204 text9-1 1 Úr PER #> 205 text9-1 1 Ház PER #> 206 text9-1 2 dr._Rubovszky_György PER #> 207 text9-1 5 Gyuri PER #> 208 text9-1 8 Képviselőtárs PER #> 209 text9-1 9 Szabó_Szabolcs PER #> 210 text9-1 9 Fabiny_Tamás PER #> 211 text9-1 11 Szabó_Szabolcs PER #> 212 text9-1 11 Fabiny_Tamás PER #> 213 text9-1 13 Fidesszel ORG #> 214 text9-1 16 Hölvényi_György PER #> 215 text9-1 18 Közel-Kelet LOC #> 216 text9-1 19 Kereszténydemokrata_Néppárt ORG #> 217 text9-1 24 Ferenc PER #> 218 text9-1 27 Szabó_Szabolcs PER #> 219 text9-1 27 Észak-Afrika LOC #> 220 text9-1 27 Közel-Kelet LOC #> 221 text9-1 30 Gőgös_Zoltán PER #> 222 text10-1 1 dr._GULYÁS PER #> 223 text10-1 1 Fidesz ORG #> 224 text10-1 2 Úr PER #> 225 text10-1 2 Országgyűlés PER #> 226 text10-1 2 Államtitkár_Úr PER #> 227 text10-1 3 Czeglédy_Csaba PER #> 228 text10-1 3 Human_Operator_Zrt. ORG #> 229 text10-1 3 Czeglédy_Csaba PER #> 230 text10-1 4 Czeglédy_Csaba PER #> 231 text10-1 4 Human_Operator_Zrt. ORG #> 232 text10-1 5 Human_Operator_Zrt. ORG #> 233 text10-1 7 Országgyűlés ORG #> 234 text10-1 8 Human_Operator_Zrt. ORG #> 235 text10-1 9 Bangóné_Borbély_Ildikó PER #> 236 text10-1 9 Eszosz-ügy MISC #> 237 text10-1 9 Bangóné_Borbély_Ildikó PER #> 238 text10-1 9 Human_Operator_Zrt. ORG #> 239 text10-1 12 Szocialista_Párt ORG #> 240 text10-1 12 Czeglédy_Csaba PER #> 241 text10-1 12 Demokratikus_Koalíció ORG #> 242 text10-1 12 Szocialista_Párt ORG #> 243 text10-1 12 Magyar_Szocialista_Párt ORG #> 244 text10-1 13 Czeglédy_Csaba PER #> 245 text10-1 13 Czeglédy_Csaba PER #> 246 text10-1 15 Bangóné_Borbély_Ildikó PER #> 247 text10-1 15 Eszosz-ügy MISC #> 248 text10-1 16 Czeglédy_Csabá PER #> 249 text10-1 16 Czeglédy_Csaba PER #> 250 text10-1 18 Heringes_Anita PER #> [ reached 'max' / getOption("max.print") -- omitted 2 rows ] A következőkben a névelemfelismerés eredményeinek vizualizálásra mutatunk be egy példát, amihez az előzőekben elkészített lemmákat tartalmazó adattáblát használjuk fel, úgy hogy első lépésként korpuszt készítünk belőle. df <- parsed_df_3 lemma_corpus <-corpus(df) Ezután a spacy_extract_entity() függvénye segítségévek elvégezzük a névelemfelismerést. Az elemzés eredményét itt nem adattáblában, hanem listában kérjük vissza. A névelemek tokenjeit ezután a jobb áttekinthetőség érdekében megritkítottuk, és csak azokat hagytuk meg, amelyek legalább három alkalommal szerepeltek a korpuszban. lemma_ner <- spacy_extract_entity( lemma_corpus, output = c("list"), multithread = TRUE) ner_tokens <- tokens(lemma_ner) features <- dfm(ner_tokens) %>% dfm_trim(min_termfreq = 3) %>% featnames() ner_tokens <- tokens_select(ner_tokens, features, padding = TRUE) Ezután a különböző alakban előforduló, de ugyanarra az entitásra vonatkozó névelemeket összevontuk. soros <- c("Soros", "Soros György") lemma <- rep("Soros György", length(soros)) ner_tokens <- tokens_replace(ner_tokens, soros, lemma, valuetype = "fixed") ogy <- c("Országgyűlés", "Ház") lemma <- rep("Országgyűlés", length(ogy)) ner_tokens <- tokens_replace(ner_tokens, ogy, lemma, valuetype = "fixed") Majd elkészítettük a szóbeágyazás fejezetben már megismert fcm-et, végezetül pedig egy együttes előfordulási mátrixot készítettünk a kinyert entitásokból és a ggplot() segítségével ábrázoltuk (ld 11.1. ábra).56 Az így kapott ábránk láthatóvá teszi, hogy mely szavak fordulnak elő jellemzően együtt, valamint a vonalvastagsággal azt is egmutatja, hogy ez relatív értelemben milyen gyakran történik. ner_fcm <- fcm(ner_tokens, context = "window", count = "weighted", weights = 1 / (1:5), tri = TRUE) feat <- names(topfeatures(ner_fcm, 80)) ner_fcm_select <- fcm_select(ner_fcm, pattern = feat, selection = "keep") dim(ner_fcm_select) #> [1] 20 20 size <- log(colSums(dfm_select(ner_fcm, feat, selection = "keep"))) set.seed(144) textplot_network(ner_fcm_select, min_freq = 0.7, vertex_size = size / max(size) * 3) Ábra 11.1: Az országgyúlési beszédek névelemeinek együttelőfordulási mátrixa https://github.com/huspacy/huspacy↩︎ Például az UDPipe: http://lindat.mff.cuni.cz/services/udpipe, a magyarlanc: https://rgai.inf.u-szeged.hu/magyarlanc és az e-magyar: https://e-magyar.hu/hu/. Magyar nyelvű szövegek NLP elemzésére használható eszközök részletes listája: https://github.com/oroszgy/awesome-hungarian-nlp↩︎ Részletes leírása: https://spacy.io/↩︎ Részletes leírása: https://cran.r-project.org/web/packages/reticulate/index.html↩︎ Részletes leírása: https://spacyr.quanteda.io/articles/using_spacyr.html↩︎ Részletes leírását lásd: https://tutorials.quanteda.io/basic-operations/fcm/fcm/↩︎ "],["felugyelt.html", "12 Osztályozás és felügyelt tanulás 12.1 Fogalmi alapok 12.2 Osztályozás felügyelt tanulással", " 12 Osztályozás és felügyelt tanulás 12.1 Fogalmi alapok A mesterséges intelligencia két fontos társadalomtudományi alkalmazási területe a felügyelet nélküli és a felügyelt tanulás. Míg az első esetben – ahogy azt a Felügyelet nélküli tanulás fejezetben bemutattuk – az emberi beavatkozás néhány kulcsparaméter megadására (így pl. a kívánt topikok számának meghatározására) szorítkozik, addig a felügyelt tanulás esetében a kutatónak nagyobb mozgástere van “tanítani” a gépet. Ennyiben a felügyelt tanulás alkalmasabb hipotézisek tesztelésére, mint az adatok rejtett mintázatait felfedező felügyelet nélküli tanulás. A felügyelt tanulási feladat megoldása egy úgynevezett tanító halmaz (training set) meghatározásával kezdődik, melynek során a kutatók saját maguk végzik el kézzel azt a feladatot melyet a továbbiakban gépi közreműködéssel szeretnének nagyobb nagyságrendben, de egyben érvényesen (validity) és megbízhatóan (reliability) kivitelezni. Eredményeinket az ugyanúgy eredetileg kézzel lekódolt, de a modell-építés során félretett teszthalmazunkon (test set) értékelhetjük. Ennek során négy kategóriába rendezzük modellünk előrejelzéseit. Egy, a politikusi beszédeket a pozitív hangulatuk alapján osztályozó példát véve ezek a következők: azok a beszédek amelyeket a modell helyesen sorolt be pozitívba (valódi pozitív), vagy negatívba (valódi negatív), illetve azok, amelyek hibásan szerepelnek a pozitív (hamis-pozitív), vagy a negatív kategóriában (hamis-negatív). Mindezek együttesen egy ún. tévesztési táblát (confusion matrix) adnak, melynek további elemzésével ítéletet alkothatunk modellépítésünk eredményességéről. A felügyelt tanulás számos kutatási feladat megoldására alkalmazhatjuk, melyek közül a leggyakoribbak a különböző osztályozási (classification) feladatok. Miközben ezek – így pl. a véleményelemzés – szótáralapú módszertannal is megoldhatóak (lásd a Szótárak és érzelemelemzés fejezetet), a felügyelt tanulás a nagyobb előkészítési igényt rendszerint jobb eredményekkel és rugalmasabb felhasználhatósággal hálálja meg (gondoljunk csak a szótárak domain-függőségére). A felügyelt tanulás egyben a mesterséges intelligencia kutatásának gyorsan fejlődő területe, mely az e fejezetben tárgyalt algoritmus-központú gépi tanuláson túl az ún. mélytanulás (deep learning) és a neurális hálók területén is zajlik egyre látványosabb sikerekkel. Az egyes modellek pontosságának kiértékelésére általában az F értéket használjuk. Az F érték két másik mérőszám keresztezésével jön létre a precision és a recall, előbbi a helyes találatok száma az összes találat között. Az utóbbi pedig a megtalált helyes értékek aránya. A precision érték képlete A recall érték képlete Ebből a két mérőszámból áll az F érték vagy F1, amely a két mérőszám harmonikus átlaga. Az F1 érték képlete Jelen fejezetben két modell fajtát mutatunk be egyazon korpuszon az Support Vector Machine (SVM) és a Naïve Bayes (NB). Mindkettő a fentiekben leírt klasszifikációs feladatot végzi el, viszont eltérő módon működnek. Az SVM a tanító halmazunk dokumentumait vektorként reprezentálja, ami annyit jelent, hogy hozzájuk rendel egy számsort, amely egy közös térben betöltött pozíciójukat reprezentálja. A teszthalmaz dokumentumai egy közös térben reprezentálva Ezt követően pedig a különféle képpen képpen felcímkézett dokumentumok között egy vonalat (hyperplane) húz meg, amely a lehető legnagyobb távolságra van minden egyes dokumentumtól. A minta osztályozása Innentől kezdve pedig a modellnek nincs más dolga, mint a tanító halmazon kívül eső dokumentumok vektor értékeit is megállapítani, elhelyezni őket ebben a közös térben, és attól függően, hogy a hyperplane mely oldalára kerülnek besorolni őket. Ezzel szemben az NB a felcímkézett tanító halmazunk szavaihoz egy valószínűségi értéket rendel annak függvényében, hogy az adott szó adott kategóriába tartozó dokumentumokban hányszor jelenik meg, az adott kategória dokumentumainak teljes szószámához képest. Miután a teszt halmazunk dokumentumaiban minden szóhoz hozzárendeltük ezeket a valószínűségi értékeket nincs más dolgunk, mint a teszt halmazunkon kívül eső dokumentumokban felkeresni ugyanezen szavakat, a hozzájuk rendelt valószínűségi értékeket aggregálni és ez alapján minden dokumentumhoz tudunk rendelni több valószínűségi értéket is, amelyek megadják, hogy mekkora eséllyel tartozik a dokumentum a teszt halmazunk egyes kategóriáiba. 12.2 Osztályozás felügyelt tanulással Az alábbi fejezetben a CAP magyar média gyűjteményéből a napilap címlapokat tartalmazó modult használjuk.57 Az induló adatbázis 71875 cikk szövegét és metaadatait (összesen öt változót: sorszám, fájlnév, a közpolitikai osztály kódja, szöveg, illetve a korpusz forrása – Magyar Nemzet vagy Népszabadság) tartalmazza. Az a célunk, hogy az egyes cikkekhez kézzel, jó minőségben (két, egymástól függetlenül dolgozó kódoló által) kiosztott és egyeztetett közpolitikai kódokat – ez a tanítóhalmaz – arra használjuk, hogy meghatározzuk egy kiválasztott cikkcsoport hasonló kódjait. Az osztályozási feladathoz a CAP közpolitikai kódrendszerét használjuk, mely 21 közpolitikai kategóriát határoz meg az oktatástól az egészségügyön át a honvédelemig. 58 Annak érdekében, hogy egyértelműen értékelhessük a gépi tanulás hatékonyságát, a kiválasztott cikkcsoport (azaz a teszthalmaz) esetében is ismerjük a kézi kódolás eredményét („éles“ kutatási helyzetben, ismeretlen kódok esetében ugyanakkor ezt gyakran szintén csak egy kisebb mintán tudjuk kézzel validálni). További fontos lépés, hogy az észszerű futási idő érdekében a gyakorlat során a teljes adatbázisból – és ezen belül is csak a Népszabadság részhalmazból – fogunk venni egy 4500 darabos mintát. Ezen a mintán pedig a már korábban említett kétféle modellt fogjuk futtatni a NB-t és az SVM-t. Az ezekkel a módszerekkel létrehozott két modellünk hatékonyságát fogjuk összehasonlítani, valamint azt is megfogjuk nézni, hogy az eredmények megbízhatósága mennyiben közelíti meg a kézikódolási módszerre jellemző 80-90%-os pontosságot. Először behívjuk a szükséges csomagokat. Majd a felügyelet nélküli tanulással foglalkozó fejezethez hasonlóan itt is alkalmazzuk a set.seed() funkciót, mivel anélkül nem egyeznének az eredményeink teljes mértékben egy a kódunk egy késöbbi újrafuttatása esetén. library(stringr) library(dplyr) library(quanteda) library(caret) library(quanteda.textmodels) library(HunMineR) set.seed(1234) Ezt követően betöltjük a HunMineR-ből az adatainkat. Jelen esetben is vethetünk egy pillantást az objektum tartalmára a glimpse funkció segítségével, és láthatjuk, hogy az öt változónk a cikkek tényleges szövege, a sorok száma, a fájlok neve, az újság, ahol a cikk megjelent, valamint a cikk adott témába való besorolása, amely kézi kódolással került hozzárendelésre. Data_NOL_MNO <- HunMineR::data_nol_mno_clean glimpse(Data_NOL_MNO) #> Rows: 71,875 #> Columns: 5 #> $ row_number <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,… #> $ filename <chr> "nol_1990_01_02_01.txt", "nol_1990_01_02_02.txt", "nol_1990_01_02_03.txt", "nol_1990_01_02_04.txt", "nol_1… #> $ majortopic_code <dbl> 19, 19, 19, 1, 1, 19, 19, 1, 1, 19, 19, 1, 19, 1, 19, 19, 17, 19, 33, 4, 19, 19, 19, 19, 19, 1, 19, 19, 5,… #> $ text <chr> "változás év választás év világ ünneplés köz keleteuróp figyel — kilencvenes évtiz szovjet—ameri közeledés… #> $ corpus <chr> "NOL", "NOL", "NOL", "NOL", "NOL", "NOL", "NOL", "NOL", "NOL", "NOL", "NOL", "NOL", "NOL", "NOL", "NOL", "… Majd pár kisebb átalakítást hajtunk végre az adatainkon. Először is ehhez a modellhez most csak a Népszabadság cikkeket fogjuk alkalmazni a Magyar Nemzet cikkeit kiszedjük az adataink közül. Ezután leegyszerűsítjük a kódolást. Összesen 21 témakört lefed a cikkek kézi kódolása most viszont ezekből egy bináris változót készítünk, amelynek 1 lesz az értéke, ha a cikk makrogazdaságról szól, ha pedig bármilyne más témakörről, akkor 0 lesz az értéke. A két fajta cikkből összesen 10000 db-os mintat veszunk, amelyben a két típus egyenlő arányban van jelen. A modellek pedig az ide történő besorolálst próbálják majd megállapítani a cikkek szövege alapján vagyis, hogy makrogazdasági témáról szól-e az adott cikk, vagy valamilyen más témáról. Majd kiválasztjuk a táblából a két változó, amelyekre szükségünk lesz a cikkek szövegét text és a cikkekhez kézzel hozzárendelt témát label a többit pedig elvetjük. Data_NOL <- Data_NOL_MNO %>% subset(corpus == "NOL" & is.na(text) == F) %>% mutate(label = if_else(majortopic_code == 1, 1, 0)) %>% group_by(label) %>% sample_n(5000) %>% ungroup() %>% select(text, label) Ezt követően egy korpusszá alakítjuk az objektumunkat. nol_corpus <- corpus(Data_NOL) Majd létrehozunk egy új objektumot id_train néven amely 1 és 4500 között 3600 különböző a sample() funkció segítségével véletlenszerűen kiválogatott számot tartalmaz. Utána pedig a korpuszunkhoz hozzá adunk egy új id_numeric elnevezésű változót, amely csupán a megszámozza az egyes cikkeket. Ezeket a késöbbiekben arra fogjuk felhasználni, hogy kialakítsuk a tanító és teszt halmazokat id_train <- sample(1:10000, 2500, replace = FALSE) nol_corpus$id_numeric <- 1:ndoc(nol_corpus) Ezt követően a korpuszunkat egy tokenekké alakítjuk és ezzel egyidejűleg eltávolítjuk a HunMiner beépített magyar stopszó szótárát, valamint szótövesítést is végrehajtunk. stopszavak <- HunMineR::data_stopwords_extra nol_toks <- tokens(nol_corpus, remove_punct = TRUE, remove_number = TRUE) %>% tokens_remove(pattern = stopszavak) %>% tokens_wordstem() A tokeneket pedig egy dfm-é alakítjuk. nol_dfm <- dfm(nol_toks) Ezt követően kialakítjunk a tanító és teszt halmazokat a korábban létrehozott változók segítségével. A lenti kód elsősora egy subsetet alakít ki az adattáblánk mindazon soraiból, amelyek id_numeric változója szerepel az id_train számsorban ez lesz a tanító alhalmaz. Majd ennek megfelelően egy másik alhalmazt alakítunk ki a teszteléshez, amely minden olyan sorát fogja tartalmazni adattáblánknak, amely id_numeric változója nem szerepel az id_train számsorban. train_dfm <- dfm_subset(nol_dfm, id_numeric %in% id_train) test_dfm <- dfm_subset(nol_dfm, !id_numeric %in% id_train) Azáltal pedig, hogy kialakítottuk a két alhalmazt el is végeztük az utolsó előkészítési folyamatot is, az így előkészített adatokon tudjuk majd futtatni a következőkben, mind az NB és az SVM modellünket. 12.2.1 Naïve Bayes A fejezet Naïve Bayesre vonatkozó kódjai quanteda tutoriál menetét és kódjait követik.59 Először létrehozzuk a modellünket, amely számára meghatározzuk, hogy a label változóhoz kell egy predikciót adnia, majd ezt alakalmazzuk az adatainkra. A dfm_match parancs segítségével eltávolítjuk a vizsgált dokumentumaink szavai közül mindazokat, amelyek nem szerepeltek a teszt halmazunkba. Erre azért van szükség, mivel az NB csak azokat a szavakat képes kezelni, amelyekhez már hozzárendelt egy valószínűségi értéket, tehát csak azokat, amelyek a teszt halmazban is megtalálhatóak. nol_nb <- textmodel_nb(train_dfm, train_dfm$label) dfm_kozos <- dfm_match(test_dfm, features = featnames(train_dfm)) A következő objektumban eltároljuk a kézikódolás eredményeit, amelyeket már ismerünk. tenyleges_osztaly <- dfm_kozos$label Majd eltároljuk egy másik objektumban azokat az eredményeket, amelyeket a modellünk generált. becsult_osztaly <- predict(nol_nb, newdata = dfm_kozos) A két fenti adat segítségével pedig létrehozhatjuk a korábban is említett tévesztési táblát. eredmeny_tabla <- table(tenyleges_osztaly, becsult_osztaly) eredmeny_tabla #> becsult_osztaly #> tenyleges_osztaly 0 1 #> 0 3310 463 #> 1 793 2934 Ezt létrehozhatjuk a caret csomag funkciójával is, amely a tévesztési tábla mellett sok más hasznos adatot is megad a számunkra. confusionMatrix(eredmeny_tabla, mode = "everything") #> Confusion Matrix and Statistics #> #> becsult_osztaly #> tenyleges_osztaly 0 1 #> 0 3310 463 #> 1 793 2934 #> #> Accuracy : 0.833 #> 95% CI : (0.824, 0.841) #> No Information Rate : 0.547 #> P-Value [Acc > NIR] : <2e-16 #> #> Kappa : 0.665 #> #> Mcnemar's Test P-Value : <2e-16 #> #> Sensitivity : 0.807 #> Specificity : 0.864 #> Pos Pred Value : 0.877 #> Neg Pred Value : 0.787 #> Precision : 0.877 #> Recall : 0.807 #> F1 : 0.841 #> Prevalence : 0.547 #> Detection Rate : 0.441 #> Detection Prevalence : 0.503 #> Balanced Accuracy : 0.835 #> #> 'Positive' Class : 0 #> Az eredményeken pedig láthatjuk, hogy még az egyszerűség kedvéért lecsökkentett méretű tanítási halmaz ellenére is egy kifejezett magas 83.25%-os pontossági értéket kapunk, amely többé-kevésbé megfeleltethető egy kizárólag kézikódolással végzett vizsgálat pontosságának. Ezt követően a kapott eredményeket a mérésünk minőségéről egy táblázatba rendezzük, későbbi összehasonlítás céljából. nb_eredmenyek <- data.frame(confusionMatrix(eredmeny_tabla, mode = "prec_recall")[4]) nb_eredmenyek$meres <- row.names(nb_eredmenyek) nb_eredmenyek$modszer <- "Naïve Bayes" row.names(nb_eredmenyek) <- 1:nrow(nb_eredmenyek) 12.2.2 Support-vector machine A következő kódsorral alkalmazzuk az SVM modellünket, hogy az adatainkon belül a label változóra vonatkozóan készítsen predikciót. nol_svm <- textmodel_svm(train_dfm, y = train_dfm$label) Ismét eltároljuk a kézikódolás eredményeit, amelyeket már ismerünk. Valamint az eredményeket, amelyeket az SVM modellünk generált szintén eltároljuk egy objektumba. tenyleges_osztaly_svm <- dfm_kozos$label becsult_osztaly_svm <- predict(nol_svm, newdata = dfm_kozos) Ismét létrehozhatunk egy egyszerű tévesztési táblát. eredmeny_tabla_svm <- table(tenyleges_osztaly_svm, becsult_osztaly_svm) eredmeny_tabla_svm #> becsult_osztaly_svm #> tenyleges_osztaly_svm 0 1 #> 0 3066 707 #> 1 581 3146 Valamint jelen esetben is használhatjuk a caret csomagban található funkciót, hogy még több információt nyerjünk ki a modellünk működéséről. confusionMatrix(eredmeny_tabla_svm, mode = "everything") #> Confusion Matrix and Statistics #> #> becsult_osztaly_svm #> tenyleges_osztaly_svm 0 1 #> 0 3066 707 #> 1 581 3146 #> #> Accuracy : 0.828 #> 95% CI : (0.82, 0.837) #> No Information Rate : 0.514 #> P-Value [Acc > NIR] : < 2e-16 #> #> Kappa : 0.657 #> #> Mcnemar's Test P-Value : 0.000496 #> #> Sensitivity : 0.841 #> Specificity : 0.817 #> Pos Pred Value : 0.813 #> Neg Pred Value : 0.844 #> Precision : 0.813 #> Recall : 0.841 #> F1 : 0.826 #> Prevalence : 0.486 #> Detection Rate : 0.409 #> Detection Prevalence : 0.503 #> Balanced Accuracy : 0.829 #> #> 'Positive' Class : 0 #> Itt ismét azt találjuk, hogy a csökkentett méretű korpusz ellenére is egy kifejezetten magas pontossági értéket 82.83%-ot kapunk. Az eredményeinket ismét egy adattáblába rendezzük, amelyet végül összekötünk az első táblánnkkal, hogy a két módszert a korábban tárgyalt három mérőszám alapján is összehasonlítsuk. svm_eredmenyek <- data.frame(confusionMatrix(eredmeny_tabla_svm, mode = "prec_recall")[4]) svm_eredmenyek$meres <- row.names(svm_eredmenyek) svm_eredmenyek$modszer <- "Support-vector machine" row.names(svm_eredmenyek) <- 1:nrow(svm_eredmenyek) eredmenyek <- rbind(svm_eredmenyek, nb_eredmenyek) %>% subset(meres %in% c("Precision", "Recall", "F1")) names(eredmenyek)[1] <- "ertek" Végül egy ábrával szemléltetjük a kapott eredményeinket, amelyen látható, hogy jelen esetben a két módszer közötti különbség minimális. Jelen esetben a Naïve Bayes jobban teljesített, mint a Support-vector machine az F1 és Precision tekintetében, viszont utóbbi a Recall tekintetében. ggplot(eredmenyek, aes(x = modszer, y = ertek, fill = modszer)) + geom_bar(stat = "identity", width = 0.1) + facet_wrap(~meres, ncol=1) A korpusz regisztációt követően elérhető az alábbi linken: https://cap.tk.hu/a-media-es-a-kozvelemeny-napirendje↩︎ A kódkönyv regisztrációt követően elérhető az alábbi linken: https://cap.tk.hu/kozpolitikai-cap↩︎ https://tutorials.quanteda.io/machine-learning/nb/↩︎ "],["függelék.html", "13 Függelék 13.1 Az R és az RStudio használata 13.2 Munka saját adatokkal 13.3 Vizualizáció", " 13 Függelék 13.1 Az R és az RStudio használata Az R egy programozási nyelv, amely alkalmas statisztikai számítások elvégzésére és ezek eredményeinek grafikus megjelenítésére. Az R ingyenes, nyílt forráskódú szoftver, mely telepíthető mind Windows, mind Linux, mind MacOS operációs rendszerek alatt az alábbi oldalról: https://cran.r-project.org/ Az RStudio az R integrált fejlesztői környezete (integrated development environment, IDE), mely egy olyan felhasználóbarát felületet biztosít, ami egyszerűbb és átláthatóbb munkát tesz lehetővé. Az RStudio az alábbi oldalról tölthető le: https://rstudio.com/products/rstudio/download/ A „point and click” szoftverekkel szemben az R használata során scripteket kell írni, ami bizonyos programozási jártasságot feltételez, de a későbbiekben lehetővé teszi azt adott kutatási kérdéshez maximálisan illeszkedő kódok összeállítását, melyek segítségével az elemzések mások számára is megbízhatóan reprodukálhatók lesznek. Ugyancsak az R használata mellett szól, hogy komoly fejlesztői és felhasználói közösséggel rendelkezik, így a használat során felmerülő problémákra általában gyorsan megoldást találhatunk. 13.1.1 Az RStudio kezdőfelülete Az RStudio kezdőfelülete négy panelből, eszközsorból és menüsorból áll: Ábra 13.1: RStudio felhasználói felület Az (1) editor ablak szolgál a kód beírására, futtatására és mentésére. A (2) console ablakban jelenik meg a lefuttatott kód és az eredmények. A jobb felső ablak (3) environment fülén láthatóak a memóriában tárolt adatállományok, változók és felhasználói függvények. A history fül mutatja a korábban lefuttatott utasításokat. A jobb alsó ablak (4) files fülén az aktuális munkakönyvtárban tárolt mappákat és fájlok találjuk, míg a plot fülön az elemzéseink során elkészített ábrák jelennek meg. A packages fülön frissíthetjük a meglévő r csomagokat és telepíthetünk újakat. A help fülön a különböző függvények, parancsok leírását, és használatát találjuk meg. A Tools -> Global Options menüpont végezhetjük el az RStudio testreszabását. Így például beállíthatjuk az ablaktér elrendezését (Pane layout), vagy a színvilágot (Appearance), illetve azt hogy a kódok ne fussanak ki az ablakból (Code -> Editing -> Soft wrap R source files). 13.1.2 A projektalapú munka Bár nem kötelező, de javasolt, hogy az RStudio-ban projekt alapon dolgozzunk, mivel így az összes – az adott projekttel kapcsolatos fájlt – egy mappában tárolhatjuk. Új projekt beállítását a File->New Project menüben tehetjük meg, ahol a saját gépünk egy könyvtárát kell kiválasztani, ahová az R a scripteket, az adat- és előzményfájlokat menti. Ezenkívül a Tools->Global Options->General menüpont alatt le kell tiltani a „Restore most recently opened project at startup” és a „Restore .RData ino workspace at startup” beállítást, valamint „Save workspace to .RData on exit” legördülő menüjében be kell állítani a „Never” értéket. Ábra 13.2: RStudio projekt beállítások A szükséges beállítások után a File -> New Project menüben hozhatjuk létre a projektet. Itt arra is lehetőségünk van, hogy kiválasszuk, hogy a projektünket egy teljesen új könyvtárba, vagy egy meglévőbe kívánjuk menteni, esetleg egy meglévő projekt új verzióját szeretnénk létrehozni. Ha sikeresen létrehoztuk a projektet, az RStudio jobb felső sarkában látnunk kell annak nevét. 13.1.3 Scriptek szerkesztése, függvények használata Új script a File -> New -> File -> R Script menüpontban hozható létre, mentésére a File->Save menüpontban egy korábbi script megnyitására File -> Open menüpontban van lehetőségünk. Script bármilyen szövegszerkesztővel írható, majd beilleszthető az editor ablakba. A scripteket érdemes magyarázatokkal (kommentekkel) ellátni, hogy a későbbiekben pontosan követhető legyen, hogy melyik parancs segítségével pontosan milyen lépéseket hajtottunk végre. A magyarázatokat vagy más néven kommenteket kettőskereszt (#) karakterrel vezetjük be. A scriptbeli utasítások az azokat tartalmazó sorokra állva vagy több sort kijelölve a Run feliratra kattintva vagy a Ctrl+Enter billentyűparanccsal futtathatók le. A lefuttatott parancsok és azok eredményei ezután a bal alsó sarokban lévő console ablakban jelennek meg és ugyanitt kapunk hibaüzenetet is, ha valamilyen hibát vétettünk a script írása közben. A munkafolyamat során létrehozott állományok (ábrák, fájlok) az ún. munkakönyvtárba (working directory) mentődnek. Az aktuális munkakönyvtár neve, elérési útja a getwd() utasítással jeleníthető meg. A könyvtárban található állományok listázására a list.files() utasítással van lehetőségünk. Ha a korábbiaktól eltérő munkakönyvtárat akarunk megadni, azt a setwd() függvénnyel tehetjük meg, ahol a ()-ben az adott mappa elérési útját kell megadnunk. Az elérési útban a meghajtó azonosítóját, majd a mappák, almappák nevét vagy egy normál irányú perjel (/), vagy két fordított perjel (\\\\) választja el, mivel az elérési út karakterlánc, ezért azt idézőjelek vagy aposztrófok közé kell tennünk. Az aktuális munkakönyvtárba beléphetünk a jobb alsó ablak file lapján a „More -> Go To Working Directory” segítségével. Ugyanitt a „Set Working Directory”-val munkakönyvtárnak állíthatjuk be az a mappát, amelyben épp benne vagyunk. Ábra 13.3: Working directory beállítások A munkafolyamat befejezésére a q() vagy quit() függvénnyel van lehetőségünk. Az R-ben objektumokkal dolgozunk, amik a teljesség igénye nélkül lehetnek például egyszerű szám vektortok, vagy akár komplex listák, illetve függvények, ábrák. A munkafolyamat során létrehozott objektumokat az RStudio jobb felső ablakának environment fülén jelennek meg. A mentett objektumokat a fent látható seprű ikonra kattintva törölhetjük a memóriából. Az environment ablakra érdemes úgy gondolni hogy ott jelennek meg a memóriában tárolt értékek. Az RStudio jobb alsó ablakának plots fülén láthatjuk azon parancsok eredményét, melyek kimenete valamilyen ábra. A packages fülnél a már telepített és a letölthető kiegészítő csomagokat jeleníthetjük meg. A help fülön a korábban említettek szerint a súgó érhető el. Az RStudio-ban használható billentyűparancsok teljes listáját Alt+Shift+K billentyűkombinációval tekinthetjük meg. Néhány gyakrabban használt, hasznos billentyűparancs: Ctrl+Enter: futtassa a kódot az aktuális sorban Ctrl+Alt+B: futtassa a kódot az elejétől az aktuális sorig Ctrl+Alt+E: futtassa a kódot az aktuális sortól a forrásfájl végéig Ctrl+D: törölje az aktuális sort Az R-ben beépített függvények (function) állnak rendelkezésünkre a számítások végrehajtására, emellett több csomag (package) is letölthető, amelyek különböző függvényeket tartalmaznak. A függvények a következőképpen épülnek fel: függvénynév(paraméter). Például tartalom képernyőre való kiíratását a print() függvénnyel tehetjük, amelynek gömbölyű zárójelekkel határolt részébe írhatjuk a megjelenítendő szöveget. A citation() függvénnyel lekérdezhetjük az egyes beépített csomagokra való hivatkozást is: a citation(quanteda) függvény a quanteda csomag hivatkozását adja meg. Az R súgórendszere a help.start() utasítással indítható el. Egy adott függvényre vonatkozó súgórészlet a függvények neve elé kérdőjel írásával, vagy a help() argumentumába a kérdéses függvény nevének beírásával jeleníthető meg (pl.: help(sum)). 13.1.4 R csomagok Az R-ben telepíthetők kiegészítő csomagok (packages), amelyek alapértelmezetten el nem érhető algoritmusokat, függvényeket tartalmaznak. A csomagok saját dokumentációval rendelkeznek, amelyeket fel kell tüntetni a használatukkal készült publikációink hivatkozáslistájában. A csomagok telepítésre több lehetőségünk is van: használhatjuk a menüsor Tools -> Install Packages menüpontját, vagy a jobb alsó ablak packages fül Install menüpontját, illetve az editor ablakban az install.packages() parancsot futtatva, ahol a ()-be a telepíteni kívánt csomag nevét kell beírnunk (pl.: install.packages(\"dplyr\")). Ahhoz, hogy egy csomag funkcióit használjuk azt be kell töltetnünk a library() parancs segítségével, itt megintcsak a használni kívánt csomag nevét kell a zárójelek közé helyeznünk, viszont ebben az esetben nem szükséges idézőjelek közé helyeznünk zat (pl.: library(dplyr)) ameddig ezt a parancsot nem futattjuk le az adott csomag funkció nem lesznek elérhetőek számunkra. Ábra 13.4: Packages fül 13.1.5 Objektumok tárolása, értékadás Az objektumok lehetnek például vektorok, mátrixok, tömbök (array), adat táblák (data frame). Értékadás nélkül az R csak megjeleníti a műveletek eredményét, de nem tárolja el azokat. Az eredmények eltárolásához azokat egy objektumba kell elmentenünk. Ehhez meg kell adnunk az objektum nevét majd az <- után adjuk meg annak értékét: a <- 12 + 3.Futtatás után az environments fülön megjelenik az a objektum, melynek értéke 15. Az objektumok elnevezésénél figyelnünk kell arra, hogy az R különbséget tesz a kis és nagybetűk között, valamint, hogy az ugyanolyan nevű objektumokat kérdés nélkül felülírja és ezt a felülírást nem lehet visszavonni. 13.1.6 Vektorok Az R-ben kétféle típusú vektort különböztetünk meg: atomi vektor (atomic vector) lista (list) Az atomi vektornak hat típusa van, logikai (logical), egész szám (integer), természetes szám (double), karakter (character), komplex szám (complex) és nyers adat (raw). A leggyakrabban valamilyen numerikus, logikai vagy karakter vektorral használjuk. Az egyedüli vektorok onnan kapták a nevüket hogy csak egy féle adattípust tudnak tárolni. A listák ezzel szemben gyakorlatilag bármit tudnak tárolni, akár több listát is egybeágyazhatunk. A vektorok és listák azok az építőelemek amikből felépülnek az R objektumaink. Több érték vagy azonos típusú objektum összefűzését a c() függvénnyel végezhetjük el. A lenti példában három különböző objektumot kreálunk, egy numerikusat, egy karaktert és egy logikait. A karakter vektorban az elemeket időzőjellel és vesszővel szeparáljuk. A logikai vektor csak TRUE, illetve FALSE értékeket tartalmazhat. numerikus <- c(1,2,3,4,5) karakter <- c("kutya","macska","ló") logikai <- c(TRUE, TRUE, FALSE) A létrehozott vektorokkal különböző műveleteket végezhetünk el, például összeadhatjuk numerikus vektorainkat. Ebben az esetben az első vektor első eleme a második vektor első eleméhez adódik. c(1:4) + c(10,20,30,40) #> [1] 11 22 33 44 A karaktervektorokat össze is fűzhetjük egymással. Példánkban egy új objektumot is létrehoztunk, ezért a jobb felső ablakban, az environment fülön láthatjuk, hogy a létrejött karakter_kombinalt objektum egy négy elemű (hosszúságú) karaktervektor (chr [1:4]), melynek elemei a \"kutya\",\"macska\",\"ló\",\"nyúl\". Az objektumként tárolt vektorok tartalmát az adott sort lefuttatva írathatjuk ki a console ablakba. Ugyanezt megtehetjük print() függvény segítségével is, ahol a függvény arrgumentumában () az adott objektum nevét kell szerepeltetnünk. karakter1 <- c("kutya","macska","ló") karakter2 <-c("nyúl") karakter_kombinalt <-c(karakter1, karakter2) karakter_kombinalt #> [1] "kutya" "macska" "ló" "nyúl" Ha egy vektorról szeretnénk megtudni, hogy milyen típusú azt a typeof() vagy a class() paranccsal tehetjük meg, ahol ()-ben az adott objektumként tárolt vektor nevét kell megadnunk: typeof(karakter1). A vektor hosszúságát (benne tárolt elemek száma vektorok esetén) a lenght() függvénnyel tudhatjuk meg. typeof(karakter1) #> [1] "character" length(karakter1) #> [1] 3 13.1.7 Faktorok A faktorok a kategórikus adatok tárolására szolgálnak. Faktor típusú változó a factor() függvénnyel hozható létre. A faktor szintjeit (igen, semleges, nem), a levels() függvénnyel kaphatjuk meg míg az adatok címkéit (tehát a kapott válaszok számát), a labels() paranccsal érhetjük el. survey_response <- factor(c("igen", "semleges", "nem", "semleges", "nem", "nem", "igen"), ordered = TRUE) levels(survey_response) #> [1] "igen" "nem" "semleges" labels(survey_response) #> [1] "1" "2" "3" "4" "5" "6" "7" 13.1.8 Data frame Az adattábla (data frame) a statisztikai és adatelemzési folyamatok egyik leggyakrabban használt adattárolási formája. Egy data frame többféle típusú adatot tartalmazhat. A data frame-k különféle oszlopokból állhatnak, amelyek különféle típusú adatokat tartalmazhatnak, de egy oszlop csak egy típusú adatból állhat. Az itt bemutatott data frame 7 megfigyelést és 4 féle változót tartalmaz (id, country, pop, continent). #> id orszag nepesseg kontinens #> 1 1 Thailand 68.7 Asia #> 2 2 Norway 5.2 Europe #> 3 3 North Korea 24.0 Asia #> 4 4 Canada 47.8 North America #> 5 5 Slovenia 2.0 Europe #> 6 6 France 63.6 Europe #> 7 7 Venezuela 31.6 South America A data frame-be rendezett adatokhoz különböző módon férhetünk hozzá, például a data frame nevének majd []-ben a kívánt sor megadásával, kiírathatjuk a console ablakba annak tetszőleges sorát ás oszlopát: orszag_adatok[1, 1]. Az R több különböző módot kínál a data frame sorainak és oszlopainak eléréséhez. A [ általános használata: data_frame[sor, oszlop]. Egy másik megoldás a $ haszálata: data_frame$oszlop. orszag_adatok[1, 4] #> [1] Asia #> Levels: Asia Europe North America South America orszag_adatok$orszag #> [1] "Thailand" "Norway" "North Korea" "Canada" "Slovenia" "France" "Venezuela" 13.2 Munka saját adatokkal Saját adatainkat legegyszerűbben a munkakönyvtárból (working directory) hívhatjuk be. A munkakönyvtár egy olyan mappa a számítógépünkön, amely közvetlenül kapcsolatban van az éppen megnyitott R scripttel vagy projekttel. Amennyiben nem határozzuk meg , hogy adatokat honnan szeretnénk behívni, akkor azokat mindig innen fogja megpróbálni betölteni az R. A getwd() parancs segítségével bármikor megtekinthetjük az aktuális munkakönyvtárunk helyét a számítógépünkön. Amennyiben szeretnénk beállítani egy új helyet a munkakönyvtárunknak akkor azt megtehetjük a setwd() paranccsal (pl.: setwd(C:/ User /Documents) illetve a menü rendszeren keresztül is van rá lehetőségünk Session -> Set Working Directory -> Choose Direcotry…. Az R-ben praktikus úgynevezett projektalapú munkával dolgozni. Létrehozhatunk egy új projektet a menü rendszerben File -> New Project… itt meg kell határoznunk a projekt fájl helyét. A projekt alapú munka előnye, hogy a a munkakönyvtár mindig ugyanabban a mappában található, ahol az R projekt fájl is, amely megkönnyíti a saját adatokkal való munkát. Az egyes fájl formátumokat különböző parancsokkal tudjuk beolvasni. Egy txt esetében használhatjuk a read.txt() parancsot, ehhez a funkcióhoz nem kell csomagot betöltenünk, mivel az R alapverziója tartalmazza.Areadtext csomagon található readtext() parancs pedig nem csak txt-k esetében, hanem minden elterjedt szöveges fájl formátum esetében működik, mint docx és pdf. A Akárhányszor adatokat olvasunk be meg kell, hogy határozzuk az objektum nevét, amely tartalmazni fogja az adott fájl adatait (pl.: proba<-read.txt (proba.txt)). A csv (comma separated values) formátumú fájlok esetében használhatjuk a read.csv parancsot, amelyet szintén tartalmaz az R alapverziója, illetve használhatjuk a read_csv parancsot is, amelyet a readr csomag tartalmaz (pl.: proba <- read.csv(proba.csv)). Utóbbi használata ajánlott magyar nyelvű szöveget tartalmazó adatok esetében, mivel tapasztalataink szerint ez a parancs kezeli legjobban a különböző kódolási problémákat. Amennyiben Excel táblázatokkal dolgozunk érdemes azokat csv formátumban elmenteni és azután betölteni, viszont van lehetőségünk a tidyverse csomag read_excel() parancsának segítségével is Excel fájlokat betölteni. A read_excel parancs működik, mind az xlsx és az xls formátumú fájlok esetében is. Mivel az excel fájlok több munkalappal is rendelkeznek ezért a read_excel használatokar azt is meghatározhatjuk, hogy melyik munkalapot szeretnénk betölteni, amennyiben nem határozzuk meg az első lapot használja alpértelmezett módon (pl.: proba <- read_excel(proba.xlsx, Sheet = 2). A saját adatok beolvasása során gyakran felmerülő hiba az úgynevezet karakter kódoláshoz kötődik, amely a számítógépek számára azt mutatja meg, hogy hogyan fordítsák a digitális adatokat szimbólumokká, vagyis karakterekké, mivel az egyes nyelvek eltérő karakter készlettel rendekeznek, ezért többféle karakter kódolás is létezik. A hiba akkor jelentkezik, ha adatokat akarunk betölteni amelyek egy addott karakter kódolással rendelkeznek, de a kódunk egy másikkal tölti be őket, ilyenkor a szövegünk beolvasása hibás lesz. Szerencsére a fentebb említett beolvasási módok mint lehetővé teszik, hogy a felhasználó explicite meghatározza, hogy milyen karakter kódolással legyenek az adatok betöltve pl.: proba <- readtext(\"proba.txt\", encoding = \"UTF-8\"). A legtöbb esetben a magyar nyelvű szövegek UTF-8 karakter kódolással rendelkeznek, amennyiben ezt meghatározzuk, de az R-be töltött szövegeink továbbra sem néznek ki úgy, mint a szöveges dokumentumainkban, akkor használhatjuk a readtext csomag encoding() parancsát, amely egy szóláncról vagy egy szóláncokról álló vektorról meg tudja nekünk mondani, hogy milyen a karakter kódolásuk. 13.3 Vizualizáció library(ggplot2) library(gapminder) library(plotly) Az elemzéseinkhez használt data frame adatai alapján a ggplot2 csomag segítségével lehetőségünk van különböző vizualizációk készítésére is. A ggplot2 használata során különböző témákat alkalmazhatunk, melyek részletes leírása megtalálható: https://ggplot2.tidyverse.org/reference/ggtheme.html Abban az esetben, ha nem választunk témát, a ggplot2 a következő ábrán is látható alaptémát használja. Ha például a szürke helyett fehér hátteret szeretnénk, alkalmazhatjuk a theme_minmal()parancsot. Szintén gyakran alkalmazott ábra alap a thema_bw(), ami az előzőtől az ábra keretezésében különbözik. Ha fehér alapon, de a beosztások vonalait feketén szeretnénk megjeleníteni, alkalmazhatjuk a theme_linedraw() függvényt, a theme_void() segítségével pedig egy fehér alapon, beosztásoktól mentes alapot kapunk, a theme_dark() pedig sötét hátteret eredményez. A theme_classic() segítségével az x és y tengelyt jeleníthetjük meg fehér alapon. Egy ábra készítésének alapja mindig a használni kívánt adatkészlet beolvasása, illetve az ábrázolni kívánt változót vagy változók megadása. Ezt követi a megfelelő alakzat kiválasztása, attól függően például, hogy eloszlást, változást, adatok közötti kapcsolatot, vagy eltéréseket akarunk ábrázolni. A geom az a geometriai objektum, a mit a diagram az adatok megjelenítésére használ. Agglpot2 több mint 40 féle alakzat alkalmazására ad lehetőséget, ezek közül néhány gyakoribbat mutatunk be az alábbiakban. Az alakzatokról részletes leírása található például az alábbi linken: https://r4ds.had.co.nz/data-visualisation.html A következőkben a gapminder csomagban található adatok segítségével szemléltetjük az adatok vizualizálásának alapjait. Először egyszerű alapbeállítások mellett egy histogram típusú vizualizációt készítünk. ggplot( data = gapminder, mapping = aes(x = gdpPercap) ) + geom_histogram() Lehetőségünk van arra, hogy az alakzat színét megváltoztassuk. A használható színek és színkódok megtalálhatóak a ggplot2 leírásában: https://ggplot2-book.org/scale-colour.html ggplot( data = gapminder, mapping = aes(x = gdpPercap) ) + geom_histogram(fill = "yellow", colour = "green") Meghatározhatjuk külön-külön a histogram x és y tengelyén ábrázolni kívánt adatokat és választhatjuk azok pontszerű ábrázolását is. ggplot( data = gapminder, mapping = aes( x = gdpPercap, y = lifeExp ) ) + geom_point() Ahogy az előzőekben, itt is megváltoztathatjuk az ábra színét. ggplot( data = gapminder, mapping = aes( x = gdpPercap, y = lifeExp ) ) + geom_point(colour = "blue") Az fenti script kibővítésével az egyes kontinensek adatait különböző színnel ábrázolhatjuk, az x és y tengelyt elnevezhetjük, a histogramnak címet és alcímet adhatunk, illetve az adataink forrását is feltüntethetjük az alábbi módon: ggplot( data = gapminder, mapping = aes( x = gdpPercap, y = lifeExp, color = continent ) ) + geom_point() + labs( x = "GDP per capita (log $)", y = "Life expectancy", title = "Connection between GDP and Life expectancy", subtitle = "Points are country-years", caption = "Source: Gapminder dataset" ) Az ábrán található feliratok méretének, betűtípusának és betűszínének megválasztásra is lehetőségünk van. ggplot( data = gapminder, mapping = aes( x = gdpPercap, y = lifeExp, color = continent ) ) + geom_point() + labs( x = "GDP per capita (log $)", y = "Life expectancy", title = "Connection between GDP and Life expectancy", subtitle = "Points are country-years", caption = "Source: Gapminder dataset" ) + theme(plot.title = element_text( size = 12, colour = "red" )) Készíthetünk oszlopdiagramot is, amit a ggplot2 diamonds adatkészletén személtetünk ggplot(data = diamonds) + geom_bar(mapping = aes(x = cut)) Itt is lehetőségünk van arra, hogy a diagram színét megváltoztassuk. ggplot(data = diamonds) + geom_bar(mapping = aes(x = cut), fill = "darkgreen") De arra is lehetőségünk van, hogy az egyes oszlopok eltérő színűek legyenek. ggplot(data = diamonds) + geom_bar(mapping = aes(x = cut, fill = cut)) Arra is van lehetőségünk, hogy egyszerre több változót is ábrázoljunk. ggplot(data = diamonds) + geom_bar(mapping = aes(x = cut, fill = clarity)) Arra ggplot2 segítségével arra is lehetőségünk van, hogy csv-ből beolvasott adatainkat vizualizáljuk. plot_cap_1 <- read.csv("data/plot_cap_1.csv", head = TRUE, sep = ";") ggplot(plot_cap_1, aes(Year, fill = Subtopic)) + scale_x_discrete(limits = c(1957, 1958, 1959, 1960, 1961, 1962, 1963)) + geom_bar(position = "dodge") + labs( x = NULL, y = NULL, title = "A Magyar Közlönyben kihirdetett agrárpolitikai jogszabályok", subtitle = "N=445" ) + coord_flip() + # az ábra tipusa theme_minimal() + theme(plot.title = element_text(size = 12)) A csv-ből belolvasott adatainkból kördiagramot is készíthetünk pie <- read.csv("data/pie.csv", head = TRUE, sep = ";") ggplot(pie, aes(x = "", y = value, fill = Type)) + geom_bar(stat = "identity", width = 1) + coord_polar("y", start = 0) + scale_fill_brewer(palette = "GnBu") + labs( title = "A Magyar Közlönyben megjelent jogszabályok típusai", subtitle = "N = 445" ) + theme_void() Továbbá minden ábránkat, amelyet a ggplot segítségével létrehozunk lehetőségünk van interaktívvá tenni a plotly csomag ggplotly parancsának segítségével. Ehhez egyszerűen csak az ábrát egy objektumba kell, hogy létrehozzuk. ggplotabra <- ggplot( data = gapminder, mapping = aes(x = gdpPercap) ) + geom_histogram() Majd ennek az obejktumnak a nevét helyezzük be a ggplotly parancsba, és futtassuk azt. ggplotly(ggplotabra) "],["irodalomjegyzék.html", "Irodalomjegyzék", " Irodalomjegyzék "],["404.html", "Page not found", " Page not found The page you requested cannot be found (perhaps it was moved or renamed). You may want to try searching to find the page's new location, or use the table of contents to find the page you are looking for. "]] diff --git a/docs/sentiment.html b/docs/sentiment.html index ab16fb9..1d03049 100644 --- a/docs/sentiment.html +++ b/docs/sentiment.html @@ -6,7 +6,7 @@ 6 Szótárak és érzelemelemzés | Szövegbányászat és mesterséges intelligencia R-ben - + @@ -55,7 +55,7 @@ - + @@ -383,9 +383,9 @@

6.3 A Magyar Nemzet elem
glimpse(mn_minta)
 #> Rows: 2,834
 #> Columns: 3
-#> $ doc_id   <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, …
-#> $ text     <chr> "Hat fővárosi képviselő öt percnél is kevesebbet beszél…
-#> $ doc_date <date> 2006-01-02, 2006-01-02, 2006-01-02, 2006-01-02, 2006-0…
+#> $ doc_id <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31… +#> $ text <chr> "Hat fővárosi képviselő öt percnél is kevesebbet beszélt egy év alatt a közgyűlésben.\n\n\n\n\n\n\n\n\n\n\n", "Mo… +#> $ doc_date <date> 2006-01-02, 2006-01-02, 2006-01-02, 2006-01-02, 2006-01-02, 2006-01-02, 2006-01-02, 2006-01-02, 2006-01-02, 2006…

A glimpse függvény segítségével belepillanthatunk a használt korpuszba és láthatjuk, hogy az 3 oszlopból áll a dokumentum azonosítójából, amely csak egy sorszám, a dokumentum szövegéből, és a dokumentumhoz tartozó azonosítóból.

Az első szöveget megnézve látjuk, hogy a standard előkészítési lépések mellett a sortörést (\n) is ki kell törölnünk.

mn_minta$text[1]
@@ -461,18 +461,13 @@ 

6.3 A Magyar Nemzet elem net_daily = daily_pos - daily_neg )

Az így kapott plot y tengelyén az adott cikkek időpontját láthatjuk, míg az x tengelyén a szentiment értékeiket. Ebben több kiugrást is tapasztalhatunk. Természetesen messzemenő következtetéseket egy ilyen kis korpusz alapján nem vonhatunk le, de a kiugrásokhoz tartozó cikkek kvalitatív vizsgálatával megállapíthatjuk, hogy az áprilisi kiugrást a választásokhoz kötődő cikkek pozitív hangulata, míg az októberi negatív kilengést az öszödi beszéd nyilvánosságra kerüléséhez köthető cikkek negatív szentimentje okozza.

-
mncim_df <- ggplot(mn_df, aes(doc_date, net_daily)) +
-  geom_line() +
-  labs(
-    y = "Szentiment",
-    x = NULL,
-    caption = "Adatforrás: https://cap.tk.hu/"
-  )
-
-ggplotly(mncim_df)
+
mncim_df <- ggplot(mn_df, aes(doc_date, net_daily)) + geom_line() + labs(y = "Szentiment",
+    x = NULL, caption = "Adatforrás: https://cap.tk.hu/")
+
+ggplotly(mncim_df)
-
- +
+

Ábra 6.1: Magyar Nemzet címlap szentimentje

@@ -513,10 +508,10 @@

6.4 MNB sajtóközlemények
glimpse(mnb_pr)
 #> Rows: 180
 #> Columns: 4
-#> $ date <date> 2005-01-24, 2005-02-21, 2005-03-29, 2005-04-25, 2005-05-23…
-#> $ text <chr> "At its meeting on January the Monetary Council considered …
-#> $ id   <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, …
-#> $ year <dbl> 2005, 2005, 2005, 2005, 2005, 2005, 2005, 2005, 2005, 2005,…

+#> $ date <date> 2005-01-24, 2005-02-21, 2005-03-29, 2005-04-25, 2005-05-23, 2005-06-20, 2005-07-18, 2005-08-22, 2005-09-19, 2005-10-… +#> $ text <chr> "At its meeting on January the Monetary Council considered the latest economic and financial developments and decided… +#> $ id <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32… +#> $ year <dbl> 2005, 2005, 2005, 2005, 2005, 2005, 2005, 2005, 2005, 2005, 2005, 2005, 2006, 2006, 2006, 2006, 2006, 2006, 2006, 200…

Ez alapján pedig láthatjuk, hogy a korpusz a tényleges szövegek mellett tartalmaz még id sorszámot, pontos dátumot és évet is.

A szövegeket ugyanazokkal a standard eszközökkel kezeljük, mint a Magyar Nemzet esetében. Érdemes minden esetben ellenőrizni, hogy az R-kód, amit használunk, tényleg azt csinálja-e, amit szeretnénk. Ez hatványozottan igaz abban az esetben, amikor szövegekkel és reguláris kifejezésekkel dolgozunk.

mnb_tiszta <- mnb_pr %>%
@@ -567,8 +562,8 @@ 

6.4 MNB sajtóközlemények ggplotly(mnsent_df)

-
- +
+

Ábra 6.2: Magyar Nemzeti Bank közleményeinek szentimentje

diff --git a/docs/similarity.html b/docs/similarity.html index 1e2e741..b88ca95 100644 --- a/docs/similarity.html +++ b/docs/similarity.html @@ -6,7 +6,7 @@ 10 Szövegösszehasonlítás | Szövegbányászat és mesterséges intelligencia R-ben - + @@ -55,7 +55,7 @@ - + @@ -476,11 +476,10 @@

10.4.1 Adatbázis-importálás é
torvenyek <- HunMineR::data_lawtext_sample
 
 colnames(torvenyek)
-#> [1] "tv_id"          "torveny_szoveg" "korm_ciklus"    "ev"            
-#> [5] "korm_ell"
-
-dim(torvenyek)
-#> [1] 600   5
+#> [1] "tv_id" "torveny_szoveg" "korm_ciklus" "ev" "korm_ell" + +dim(torvenyek) +#> [1] 600 5

tv_javaslatok <- HunMineR::data_lawprop_sample
 
 colnames(tv_javaslatok)
@@ -499,48 +498,39 @@ 

10.4.1 Adatbázis-importálás é
tv_tvjavid_osszekapcs <- left_join(torvenyek, parok)
 
 colnames(tv_tvjavid_osszekapcs)
-#> [1] "tv_id"          "torveny_szoveg" "korm_ciklus"    "ev"            
-#> [5] "korm_ell"       "tvjav_id"
-
-dim(tv_tvjavid_osszekapcs)
-#> [1] 600   6
+#> [1] "tv_id" "torveny_szoveg" "korm_ciklus" "ev" "korm_ell" "tvjav_id" + +dim(tv_tvjavid_osszekapcs) +#> [1] 600 6

Második lépésben a törvényjavaslatokat tartalmazó adatbázist rendeljük hozzá az előzőekben már összekapcsolt két adatbázishoz.

tv_tvjav_minta <- left_join(tv_tvjavid_osszekapcs, tv_javaslatok)
 
 colnames(tv_tvjav_minta)
-#> [1] "tv_id"          "torveny_szoveg" "korm_ciklus"    "ev"            
-#> [5] "korm_ell"       "tvjav_id"       "tvjav_szoveg"
-
-dim(tv_tvjav_minta)
-#> [1] 600   7
+#> [1] "tv_id" "torveny_szoveg" "korm_ciklus" "ev" "korm_ell" "tvjav_id" "tvjav_szoveg" + +dim(tv_tvjav_minta) +#> [1] 600 7

Ha jól végeztük a dolgunkat az adatbázisok összekapcsolása során, az eljárás végére 7 oszlopunk és 600 sorunk van, vagyis az újonnan létrehozott adatbázisba bekerült az összes változó (oszlop). A korpuszaink egy adattáblában való kezelése azért hasznos, mert így nem kell párhuzamosan elvégezni az azonos műveleteket a két korpusz, a törvények és a törvényjavaslatok tisztításához, hanem párhuzamosan tudunk dolgozni a kettővel. Kicsit közelebbről megvizsgálva az adatbázist, megtekinthetjük a metaadatainkat, valamit azt is láthatjuk a count funkció segítségével, hogy minden adatbázisunkban szereplő kormányzati ciklusra 100 megfigyelés áll rendelkezésünkre: tv_tvjav_minta %>% count(korm_ciklus).

summary(tv_tvjav_minta)
-#>     tv_id           torveny_szoveg     korm_ciklus              ev      
-#>  Length:600         Length:600         Length:600         Min.   :1994  
-#>  Class :character   Class :character   Class :character   1st Qu.:2000  
-#>  Mode  :character   Mode  :character   Mode  :character   Median :2006  
-#>                                                           Mean   :2006  
-#>                                                           3rd Qu.:2012  
-#>                                                           Max.   :2018  
-#>     korm_ell     tvjav_id         tvjav_szoveg      
-#>  Min.   :  0   Length:600         Length:600        
-#>  1st Qu.:900   Class :character   Class :character  
-#>  Median :900   Mode  :character   Mode  :character  
-#>  Mean   :741                                        
-#>  3rd Qu.:900                                        
-#>  Max.   :901
-
-tv_tvjav_minta %>% 
-  count(korm_ciklus)
-#> # A tibble: 6 × 2
-#>   korm_ciklus     n
-#>   <chr>       <int>
-#> 1 1994-1998     100
-#> 2 1998-2002     100
-#> 3 2002-2006     100
-#> 4 2006-2010     100
-#> 5 2010-2014     100
-#> 6 2014-2018     100
+#> tv_id torveny_szoveg korm_ciklus ev korm_ell tvjav_id tvjav_szoveg +#> Length:600 Length:600 Length:600 Min. :1994 Min. : 0 Length:600 Length:600 +#> Class :character Class :character Class :character 1st Qu.:2000 1st Qu.:900 Class :character Class :character +#> Mode :character Mode :character Mode :character Median :2006 Median :900 Mode :character Mode :character +#> Mean :2006 Mean :741 +#> 3rd Qu.:2012 3rd Qu.:900 +#> Max. :2018 Max. :901 + +tv_tvjav_minta %>% + count(korm_ciklus) +#> # A tibble: 6 × 2 +#> korm_ciklus n +#> <chr> <int> +#> 1 1994-1998 100 +#> 2 1998-2002 100 +#> 3 2002-2006 100 +#> 4 2006-2010 100 +#> 5 2010-2014 100 +#> 6 2014-2018 100

Hasonlóan ellenőrizhetjük az egyes évekre eső megfigyelések számát is.

tv_tvjav_minta %>%
   count(ev)
@@ -706,9 +696,8 @@ 

10.7 A koszinusz-hasonlóság sz
tv_tvjav_minta$koszinusz <- koszinusz_diag

A kibővített adatbázis oszlopneveit a colnames() függvénnyel ellenőrizhetjük, hogy lássuk, valóban sikerült-e hozzárendelnünk a koszinusz értékeket a táblához.

colnames(tv_tvjav_minta)
-#> [1] "tv_id"          "torveny_szoveg" "korm_ciklus"    "ev"            
-#> [5] "korm_ell"       "tvjav_id"       "tvjav_szoveg"   "jaccard_index" 
-#> [9] "koszinusz"
+#> [1] "tv_id" "torveny_szoveg" "korm_ciklus" "ev" "korm_ell" "tvjav_id" "tvjav_szoveg" +#> [8] "jaccard_index" "koszinusz"

10.8 Az eredmények vizualizációja

@@ -725,9 +714,9 @@

10.8 Az eredmények vizualizáci glimpse(koszinusz_df) #> Rows: 10,000 #> Columns: 3 -#> $ docs1 <chr> "text501", "text501", "text501", "text501", "text501"… -#> $ docs2 <chr> "text501", "text502", "text503", "text504", "text505"… -#> $ similarity <dbl> 0.95618, 0.02592, 0.04451, 0.03346, 0.01169, 0.04536,…

+#> $ docs1 <chr> "text501", "text501", "text501", "text501", "text501", "text501", "text501", "text501", "text501", "text501", "… +#> $ docs2 <chr> "text501", "text502", "text503", "text504", "text505", "text506", "text507", "text508", "text509", "text510", "… +#> $ similarity <dbl> 0.95618, 0.02592, 0.04451, 0.03346, 0.01169, 0.04536, 0.07091, 0.27322, 0.04593, 0.00791, 0.02199, 0.11017, 0.0…

Ezt követően pedig a ggplot függvényt használva a geom_tile segítségével tudjuk elkészíteni a hőtérképet, ami a hasonlósági mátrixot ábrázolja (ld. 10.1. ábra).

koszinusz_plot <- ggplot(koszinusz_df, aes(docs1, docs2, fill = similarity)) +
   geom_tile() +
@@ -746,8 +735,8 @@ 

10.8 Az eredmények vizualizáci ggplotly(koszinusz_plot, tooltip = "similarity")

-
- +
+

Ábra 10.1: Koszinusz-hasonlóság hőtérképen ábrázolva

@@ -781,8 +770,8 @@

10.8 Az eredmények vizualizáci ggplotly(jaccard_plot, tooltip = "similarity")

-
- +
+

Ábra 10.2: Jaccard hasonlóság hőtérképen ábrázolva

@@ -800,15 +789,15 @@

10.8 Az eredmények vizualizáci glimpse(tv_tvjav_tidy) #> Rows: 1,200 #> Columns: 9 -#> $ tv_id <chr> "1994LXXXII", "1994LXXXII", "1996CXXXI", "1996C… -#> $ torveny_szoveg <chr> "évi lxxxii törvény a magánszemélyek jövedelema… -#> $ korm_ciklus <chr> "1994-1998", "1994-1998", "1994-1998", "1994-19… -#> $ ev <dbl> 1994, 1994, 1996, 1996, 1995, 1995, 1995, 1995,… -#> $ korm_ell <dbl> 900, 900, 900, 900, 900, 900, 900, 900, 900, 90… -#> $ tvjav_id <chr> "1994-1998_T0233", "1994-1998_T0233", "1994-199… -#> $ tvjav_szoveg <chr> "magyar köztársaság kormánya t számú törvényjav… -#> $ hasonlosag_tipus <chr> "jaccard_index", "koszinusz", "jaccard_index", … -#> $ hasonlosag <dbl> 0.554, 0.624, 0.572, 0.929, 0.729, 0.985, 0.572…

+#> $ tv_id <chr> "1994LXXXII", "1994LXXXII", "1996CXXXI", "1996CXXXI", "1995CXIV", "1995CXIV", "1995XC", "1995XC", "1996LX… +#> $ torveny_szoveg <chr> "évi lxxxii törvény a magánszemélyek jövedelemadójáról szóló évi xc törvény módosításáról lábjegyzet a ma… +#> $ korm_ciklus <chr> "1994-1998", "1994-1998", "1994-1998", "1994-1998", "1994-1998", "1994-1998", "1994-1998", "1994-1998", "… +#> $ ev <dbl> 1994, 1994, 1996, 1996, 1995, 1995, 1995, 1995, 1996, 1996, 1997, 1997, 1994, 1994, 1997, 1997, 1996, 199… +#> $ korm_ell <dbl> 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, 900, … +#> $ tvjav_id <chr> "1994-1998_T0233", "1994-1998_T0233", "1994-1998_T3167", "1994-1998_T3167", "1994-1998_T1538", "1994-1998… +#> $ tvjav_szoveg <chr> "magyar köztársaság kormánya t számú törvényjavaslat a magánszemélyek jövedelemadójáról szóló törvény mód… +#> $ hasonlosag_tipus <chr> "jaccard_index", "koszinusz", "jaccard_index", "koszinusz", "jaccard_index", "koszinusz", "jaccard_index"… +#> $ hasonlosag <dbl> 0.554, 0.624, 0.572, 0.929, 0.729, 0.985, 0.572, 0.961, 0.627, 0.753, 0.698, 0.978, 0.520, 0.695, 0.711, …
jaccard_koszinusz_plot <- ggplot(tv_tvjav_tidy, aes(ev, hasonlosag)) +
   geom_jitter(aes(shape = hasonlosag_tipus,color = hasonlosag_tipus), width = 0.1, alpha = 0.45) +
   scale_x_continuous(breaks = seq(1994, 2018, by = 2)) + 
@@ -824,8 +813,8 @@ 

10.8 Az eredmények vizualizáci ggplotly(jaccard_koszinusz_plot)

-
- +
+

Ábra 10.3: Koszinusz- és Jaccard hasonlóság értékeinek alakulása

@@ -860,8 +849,8 @@

10.8 Az eredmények vizualizáci ggplotly(jac_cos_ave_df)

-
- +
+

Ábra 10.4: Koszinusz- és Jaccard hasonlóság vonal diagramon ábrázolva

@@ -879,8 +868,8 @@

10.8 Az eredmények vizualizáci ggplotly(jac_cos_box_df)

-
- +
+

Ábra 10.5: Jaccard hasonlóság boxplotokkal ábrázolva

@@ -920,8 +909,8 @@

10.8 Az eredmények vizualizáci ggplotly(jaccard_be_plot, height = 1000)

-
- +
+

Ábra 10.6: Jaccard hasonlóság a benyújtó személye alapján