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.
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, Berki, and Bolonyai 2021) megjelenés előtt álló kéziratból meríti elemzési fókuszát. Az eredeti kézirat által megvalósított elemzést 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.
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.
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.
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.
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.
É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.
É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.
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.
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")