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 webscraping, 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

  1. 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.↩︎

  2. 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.↩︎

  3. 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.↩︎

  4. 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.↩︎

  5. 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↩︎

  6. 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.↩︎

  7. 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.↩︎

  8. 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.↩︎

  9. 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↩︎