2012. március 11.

Kulcsszavak kinyerése szöveges dokumentumból

Bár ismeretes, hogy már létező megoldást újra megírni nem igazán érdemes, talán alapvető eljárások esetében mégis más a helyzet: tanulni és megérteni szerintem sok dolgot így lehet a legjobban. Tehát, egy klasszikus keyword-extraction eljárással és egy hasznos python könyvtárral való ismerkedésem dokumentálása következik.


1. A probléma: szeretnénk kideríteni, hogy egy rakás szöveges dokumentum, mondjuk egy weblap oldalai körülbelül miről szólnak. Építhetünk tag-felhőt vagy minden oldalhoz odarendelhetjük a legjellemzőbb három taget (a Számítógépes Nyelvészeten is van ilyen, a cikk alján látható!). Ki szeretnénk nyerni a kulcsszavakat, keyword extractiont -t csinálni.

2. A legjellemzőbb kifejezések nyilván gyakran fordulnak elő -- elég megszámolni a szavak előfordulását. Sajnos a természetes nyelv alattommos, és hatalmas túlsúlyba kerülnek a semmire sem használható segédszavak. Ismert a stopword-szűrés technika, de ez sem megoldás mindenre.

3. Kifinomultabb a TF-IDF módszer (term frequency / inverse document freqency), mint a keyword kinyerés elterjedt eszköze (leírás itt és itt). Az eljárás lényege, hogy egy dokumentum jellemző kulcsszavait nem csak a dokumentumhoz képest, hanem más (lehetőleg hasonló) dokumentumokhoz képest is vizsgáljuk: ha az index.hu újságírói szívesen használnak bizonyos kifejezéseket, akkor ezt figyelembe kell vennünk a kulcsszavak keresése közben, és az ilyen "általában gyakori" kifejezéseket "le kell pontoznunk".

4. A TF-IDF modell gyakorlati megvalósításához érdemes röviden áttekinteni a dokumentum-vektortér modellt
Minden dokumentumot vektorként ábrázolunk, amely vektornak az egyes elemei az egyes kifejezések előfordulásainak számát tartalmazzák.

Nyilvánvaló, hogy ehhez szükségünk van egy lexikonra is. Pl, ha a lexikonunk így néz ki:

1: szép
2: csúnya
3: kutya

akkor a "szép kutya" szöveget tartalmazó dokumentumot a következő vektor reprezentálja:

(1,0,1)

A valóságban természetesen több ezer dimenziós vektorokkal lesz dolgunk. A vektorokká alakítást elsősorban rendelkezésre álló matematikai eszköztár indokolja -- ezt fogjuk mi is kihasználni a python kód megírásakor.

5. Minden dokumentumot vektoronként ábrázolunk az n-dimenziós vektorterünkben, majd ezekből a vektorokból d x n kiterjedésű mátrixot írunk fel. Nevezzük ezt TF-mátrixnak. Valahogy így

1. dokumentum: 1 3 5 0 1
2. dokumentum: 5 1 1 0 0
3. dokumentum: 8 5 3 1 0

A mátrix oszlopai az adott dokumentumban az adott kifejezés előfordulási gyakoriságát tartalmazzák.

6. Létrehozunk egy document freqency vektort, amely azt fogja tartalmazni, hogy melyik kifejezés hány dokumentumban fordul elő. Vagyis, azt derítjük ki, hogy mennyire szokványos az adott kifejezés az egész szettre nézve. Ezt például megoldhatjuk úgy, hogy a TF mátrixból csinálunk egy másik ugyanolyan kiterjedésű mátrixot; az új mátrix elemei 0 értéket tartalmaznak, ha a TF mátrixban 0 található az adott helyen, 1-et, ha nem 0. Ezek után oszloponként összeadjuk az elemeket, és meg is kaptuk a document freqency vektort. A példában pl.

(3, 3, 3, 1, 1)

7. Az inverse document frequency értéke wikipediás definíció szerint 
log(dokumentumok száma/kifejezés előfordulása) 

Amennyiben nincs hatalmas mennyiségű adatunk, a logaritmus befejezése nem biztos, hogy szükséges (hiszen itt a nagyságrendi viszonyok kezdenek számítani), számolhatunk egyszerű reciprokkal is. A lényeg: az inverz érték azt jelenti, hogy minél gyakoribb egy kifejezés, annál kisebb lesz a kapott érték.

8. Ezek után nincs más hátra, mint elvégezni a TF * IDF műveletet: minden dokumentum minden egyes kifejezésének előfordulási értékét megszorozni az adott kifejezés IDF értékével. Ily módon a sok dokumentumban előforduló kifejezések lejjebb csúsznak a ranglistán, míg a csak az adott dokumentumra jellemző kifejezések jobb pontszámot érnek el.

9. A mátrixxá alakítás itt mutatja meg az igazi előnyét: a fenti művelet egyetlen mátrix-szorzás, nevezetesen a TF mátrixot szozzuk meg egy másikkal. Ez a másik mátrix nem más, mint az a diagonális mátrix, amelyet az IDF vektor elemeiből állítunk elő. Az eredményként kapott mátrix elemei az egyes dokumentumok kifejezéseinek relatív fontosságát fogják tartalmazni.

(mint ismeretes, diagonális mátrixxal szorozni gyakorlatilag annyit tesz, mint minden egyes oszlop elemeit egyenként végigszorozni a diagonális mátrix adott oszlopának egyes elemével).

Felmerülhet a kérdés, hogy miért kell diagonális mátrixxal bohóckodni, miért nem lehet oszloponként szorozni a TF mátrixot IDF vektor soron következő elemével (skalárszorzatként). Lehet, sőt, érdemes is, mert gyorsabb, az eredmény pedig ugyanaz. Csak az első megoldás papíron jobban néz ki;)

10. A python numpy könyvtára bevezet egy array nevű adattípust -- ezek n-dimenziós mátrixok. Az adattípus rendkívül rugalmasan kezelhető, teljesen illeszkedik a python sztenderd típusaihoz, minden mátrixalgebrai művelet elvégezhető rajta, és nem utolsósorban nagyon gyors adatkezelést tesz lehetővé (az ezért felelős részek C-ben íródtak). Ízelítő itt.



A lexikon elkészítése nem túl izgalmas: a dokumentumszett minden előforduló új kifejezését betesszük a lexikon nevű listába. A TF mátrix létrehozása is rendkívül egyszerű (nullákkal töltjük fel):

tf=zeros((len(doks), len(lexikon)), dtype=int16)

Ezek után újra végigszaladunk az összes dokumentumon, és összeszámoljuk az előforduló kifejezéseket.

A DF vektort így hozzuk létre:

f=lambda x: 0 if x==0 else 1
df=array([f(x) for x in tf.flat]).reshape(tf.shape).sum(axis=0)
idf=[1/x for x in df]

A sum(axis=0) oszloponként számolja össze az egyeseket, amelyeket az f=lambda x: 0 if x==0 else 1 mini-fügvénnyel állítunk elő a TF függvény elemeiből. Hogy ne kelljen két egymásba ágyazott ciklust használni, a tf.flat kifejezéssel egyszerűen "kiterítjük" az elemeit, majd a reshape(tf.shape) kifejezéssel újra összerakjuk. Az IDF vektor előállításához a triviális reciprok-formulát (1/x) alkalmazzuk, majd a TF mátrixot oszloponként szorozzuk az IDF elemeivel:

tfidf=tf*idf

Figyelem! Ez nem mátix-szorzás! (az a dot() fügvénnyel csinálnánk)

10+1. A TF vektort normalizálhatnánk is. Ez azt jelenti, hogy a vektor hosszát 1 egység hosszúságúra nyessük -- ennek az volna az értelme, hogy a különböző hosszúságú dokumentumok ne okozzanak kavarodást a rendszerünkben.


Nincsenek megjegyzések: