Egy korábbi bejegyzésben már átnéztük a szózsák modellt (bag-of-words), és annak problémáit. A mai bejegyzésben megnézzük, mit lehet tenni azért, hogy a szótár méretét csökkentsük és javítsuk a vektorizálás teljesítményét.
Szöveg előkészítés
Van néhány dolog, amit minden egyes szöveg esetén el szoktam végezni. A kisbetűvé alakítást, a speciális karakterek és számok eltávolítást és a szavakra vágást. Ezek a műveletek annyira egyértelműek, hogy nem is ragoznám tovább. A példa kedvéért a következő szöveget fogjuk feldolgozni:
Nem tudhatom, hogy másnak e tájék mit jelent,
nekem szülőhazám itt e lángoktól ölelt
kis ország, messzeringó gyerekkorom világa.
Belőle nőttem én, mint fatörzsből gyönge ága
s remélem, testem is majd e földbe süpped el.
Itthon vagyok. S ha néha lábamhoz térdepel
egy-egy bokor, nevét is, virágát is tudom,
tudom, hogy merre mennek, kik mennek az uton,
s tudom, hogy mit jelenthet egy nyári alkonyon
a házfalakról csorgó, vöröslő fájdalom. 12 3 : (), %,\"
Most lássunk egy Python megvalósítást:
# A példaszöveg
# részlet a kedvenc versemből, plusz néhány extra jel
text = """Nem tudhatom, hogy másnak e tájék mit jelent,
nekem szülőhazám itt e lángoktól ölelt
kis ország, messzeringó gyerekkorom világa.
Belőle nőttem én, mint fatörzsből gyönge ága
s remélem, testem is majd e földbe süpped el.
Itthon vagyok. S ha néha lábamhoz térdepel
egy-egy bokor, nevét is, virágát is tudom,
tudom, hogy merre mennek, kik mennek az uton,
s tudom, hogy mit jelenthet egy nyári alkonyon
a házfalakról csorgó, vöröslő fájdalom. 12 3 : (), %,\""""
# számok eltávolítása
import re
text = re.sub(" \d+", " ", text)
# különleges karakterek
pattern = r"[{}]".format("(),.;:%\"")
text = re.sub(pattern, "", text)
# kisbetű
text = text.lower()
# felesleges üres mezők törlése
text = text.strip()
# szavakra vágás
from nltk.tokenize import WordPunctTokenizer
WPT = WordPunctTokenizer()
tokens = WPT.tokenize(text)
Stop szavak
Szótárunk méretének csökkentése érdekében az első lépés általában a nem fontos szavak eltávolítása. Ezeket angolul “stop-words”-nek nevezik, és ilyen például az angol nyelvben az: “the”, “is”, “at”, “which” és “on”. Ezek a szöveg egyediségén nemigen válioztató gyakori szavak. Eltávolításukhoz én Pythonban erre az nltk könyvtárat használom, ami stop szavakként magyar nyelven jelenleg az alábbiakat definiálja:
['a', 'ahogy', 'ahol', 'aki', 'akik', 'akkor', 'alatt', 'által', 'általában', 'amely', 'amelyek', 'amelyekben', 'amelyeket', 'amelyet', 'amelynek', 'ami', 'amit', 'amolyan', 'amíg', 'amikor', 'át', 'abban', 'ahhoz', 'annak', 'arra', 'arról', 'az', 'azok', 'azon', 'azt', 'azzal', 'azért', 'aztán', 'azután', 'azonban', 'bár', 'be', 'belül', 'benne', 'cikk', 'cikkek', 'cikkeket', 'csak', 'de', 'e', 'eddig', 'egész', 'egy', 'egyes', 'egyetlen', 'egyéb', 'egyik', 'egyre', 'ekkor', 'el', 'elég', 'ellen', 'elő', 'először', 'előtt', 'első', 'én', 'éppen', 'ebben', 'ehhez', 'emilyen', 'ennek', 'erre', 'ez', 'ezt', 'ezek', 'ezen', 'ezzel', 'ezért', 'és', 'fel', 'felé', 'hanem', 'hiszen', 'hogy', 'hogyan', 'igen', 'így', 'illetve', 'ill.', 'ill', 'ilyen', 'ilyenkor', 'ison', 'ismét', 'itt', 'jó', 'jól', 'jobban', 'kell', 'kellett', 'keresztül', 'keressünk', 'ki', 'kívül', 'között', 'közül', 'legalább', 'lehet', 'lehetett', 'legyen', 'lenne', 'lenni', 'lesz', 'lett', 'maga', 'magát', 'majd', 'majd', 'már', 'más', 'másik', 'meg', 'még', 'mellett', 'mert', 'mely', 'melyek', 'mi', 'mit', 'míg', 'miért', 'milyen', 'mikor', 'minden', 'mindent', 'mindenki', 'mindig', 'mint', 'mintha', 'mivel', 'most', 'nagy', 'nagyobb', 'nagyon', 'ne', 'néha', 'nekem', 'neki', 'nem', 'néhány', 'nélkül', 'nincs', 'olyan', 'ott', 'össze', 'ő', 'ők', 'őket', 'pedig', 'persze', 'rá', 's', 'saját', 'sem', 'semmi', 'sok', 'sokat', 'sokkal', 'számára', 'szemben', 'szerint', 'szinte', 'talán', 'tehát', 'teljes', 'tovább', 'továbbá', 'több', 'úgy', 'ugyanis', 'új', 'újabb', 'újra', 'után', 'utána', 'utolsó', 'vagy', 'vagyis', 'valaki', 'valami', 'valamint', 'való', 'vagyok', 'van', 'vannak', 'volt', 'voltam', 'voltak', 'voltunk', 'vissza', 'vele', 'viszont', 'volna']
Nézzük a teljes Python kódot:
# stop szavak
from nltk.corpus import stopwords
stop_word_list = stopwords.words('hungarian')
# foltozás mivel az ő úgy szerepel mint õ
stop_word_list = [ re.sub("õ", "ő", sw) for sw in stop_word_list ]
# néhány extra stop szót hozzáadunk
stop_word_list = stop_word_list + [ "is"]
# stop szavak eltávolítása
filtered_tokens = [token for token in tokens
if token not in stop_word_list]
# szöveg újraegyesítése a stop szavak nélkül
text = ' '.join(filtered_tokens)
A stop szavak eltávolításának eredmény a fenti Raddnóti-versrészletben:
tudhatom másnak tájék jelent szülőhazám lángoktól ölelt kis ország messzeringó gyerekkorom világa belőle nőttem fatörzsből gyönge ága remélem testem földbe süpped itthon ha lábamhoz térdepel – bokor nevét virágát tudom tudom merre mennek kik mennek uton tudom jelenthet nyári alkonyon házfalakról csorgó vöröslő fájdalom
Stemming és Lemmatize
Minden természetes szövegben ugyanaz a szó több alakban előfordulhat, pl: “tudhat”, tudhatom”, “tudhatod” etc. A szótár csökkentése érdekében jó ötletnek tekinthető ezeknek az alakoknak az összevonása. Ezt a célt szolgálja a stemming és a lemmatizing is. A különbség a kettő között, hogy a szóvégek egy részét a stemming a nyelvtani szabályok figyelembe vétele nélkül vágja le, a lemmatizing pedig nyelvtani szabályok alapján próbálja megtenni ugyanezt.
Gondolom, nem árulok el titkot, hogy a stemming egyszerűbb és gyorsabb, a lemmatizing pedig jobb eredményt tud produkálni, ha jók az alkalmazott szabályok.
Stemming
A magyar nyelvben annyi szerencsénk van, hogy mivel ragozunk, általában a stemming elegendő számunkra. Erre a hunspell-t szoktam használni:
# hunspell
import hunspell
hobj = hunspell.HunSpell('/usr/share/myspell/hu_HU.dic',
'/usr/share/myspell/hu_HU.aff')
steamed = []
# végiglépkedünk a szavakon és levágjuk a ragokat
for szo in filtered_tokens:
s = hobj.stem(szo)
if len(s) > 0:
# automatikusan az első megoldást válasszuk,
# persze ez nem mindig lesz jó, de általában igen
steamed.append( s[0].decode('utf-8') )
else:
steamed.append( szo.lower() )
text = ' '.join(steamed)
print(text)
Maradva a fenti versnél a szöveg így fog átalakulni:
tud más tájék jelent szülőhazám láng ölel kis ország messzeringó gyerekkorom világ belőle nő fatörzs gyönge ág remél test föld süpped itthon ha láb térdepel egyegy bokor név virág tud tud merre megy ki megy uton tud jelent nyári alkony házfal csorgó vöröslő fájdalom
Csak a kíváncsiság véget lássuk hogy alakította át a hunspell a szöveget:
tudhatom --> tud
másnak --> más
tájék --> tájék
jelent --> jelent
szülőhazám --> szülőhazám
lángoktól --> láng
ölelt --> ölel
kis --> kis
ország --> ország
messzeringó --> messzeringó
gyerekkorom --> gyerekkorom
világa --> világ
belőle --> belőle
nőttem --> nő
fatörzsből --> fatörzs
gyönge --> gyönge
ága --> ág
remélem --> remél
testem --> test
földbe --> föld
süpped --> süpped
itthon --> itthon
ha --> ha
lábamhoz --> láb
térdepel --> térdepel
egyegy --> egyegy
bokor --> bokor
nevét --> név
virágát --> virág
tudom --> tud
merre --> merre
mennek --> megy
kik --> ki
mennek --> megy
uton --> uton
jelenthet --> jelent
nyári --> nyári
alkonyon --> alkony
házfalakról --> házfal
csorgó --> csorgó
vöröslő --> vöröslő
fájdalom --> fájdalom
Lemmatize
Ahogy említettem a lemmatizing nem csak a szótöveket állítja elő, hanem sokkal összetettebb munkát végez. Például a hasonló jelenésű szavakat egyformára alakítja. Például az angol “am”, “are” és “is” mind “be” lesz. Ez nagyon jó, sajnos azonban a kisebb nyelvekre, mint a magyar, nem nagyon áll rendelkezésre megfelelő szabályrendszer.
Mivel a magyar nyelvet támogató általánosan elterjedd megoldás[1] még nem létezik ezért, most kicsit félreteszem a fenti példát és a lemmatizer működését egy angol szövegen fogom szemléltetni. A feladathoz a nltk könyvtár WordNetLemmatizer osztályát fogom használni:
import nltk
nltk.download('punkt')
nltk.download('wordnet')
from nltk.stem import WordNetLemmatizer
wordnet_lemmatizer = WordNetLemmatizer()
# példa angol szöveg
sentence = "He was running and eating at same time. \
He has bad habit of swimming after playing long hours in the Sun."
# különleges karakterek
pattern = r"[{}]".format("(),.;:\-%\"")
sentence = re.sub(pattern, "", sentence)
# szavakra vágás
from nltk.tokenize import WordPunctTokenizer
WPT = WordPunctTokenizer()
sentence_words = WPT.tokenize(sentence)
print("{0:20}\t \t{1:20}".format("Eredeti szó","Lemma"))
for word in sentence_words:
print ("{0:20}\t-->\t{1:20}".format(word,
wordnet_lemmatizer.lemmatize(word,
pos="v"))
)
Ami eredménye a következő:
Eredeti szó Lemma
He --> He
was --> be
running --> run
and --> and
eating --> eat
at --> at
same --> same
time --> time
He --> He
has --> have
bad --> bad
habit --> habit
of --> of
swimming --> swim
after --> after
playing --> play
long --> long
hours --> hours
in --> in
the --> the
Sun --> Sun
Szózsákmodell
Itt az ideje, hogy elkészítsük a szózsák modellünk. Erre több Python implementáció létezik, most tegyük ezt az sklearn könyvtár segítségével.
# szózsákmodell készítése
from sklearn.feature_extraction.text import CountVectorizer
BoW_Vector = CountVectorizer(min_df = 0., max_df = 1.)
# listává az elvárt bemenet
BoW_Matrix = BoW_Vector.fit_transform([text])
# a szózsák modell az előző blogbejegyzésnek megfelelően formázva
features = BoW_Vector.get_feature_names()
import pandas as pd
BoW_df = pd.DataFrame(BoW_Matrix.toarray(), columns = features)
print(BoW_df.T)
A formázás eredménye:
0
alkony 1
belőle 1
bokor 1
csorgó 1
egyegy 1
fatörzs 1
fájdalom 1
föld 1
gyerekkorom 1
gyönge 1
ha 1
házfal 1
itthon 1
jelent 2
ki 1
kis 1
láb 1
láng 1
megy 2
merre 1
messzeringó 1
más 1
nyári 1
név 1
nő 1
ország 1
remél 1
szülőhazám 1
süpped 1
test 1
tud 4
tájék 1
térdepel 1
uton 1
világ 1
virág 1
vöröslő 1
ág 1
ölel 1
Rendben. Van egy szózsákmodellünk, és tettünk néhány kis lépést a szótár csökkentése irányába. Most nézzünk még egy-két trükköt, amivel a vektorok értékét módosítjuk annak érdekében, hogy a hasonló szövegek közelebb kerüljenek egymáshoz.
tf-idf
Az egyik probléma a szózsák modellel, hogy minden benne szerepeltetett szó ugyanolyan fontos. Pedig a valós életben ez nincs így. Egyrészt vannak szavak, amik sok dokumentumban előfordulnak és így kevésbé segítenek azok elválasztásában. Másrészt ha egy szöveg egy speciális területtel foglalkozik, sokszor szerepelhet benne olyan kifejezés, ami ritka más dokumentumokban.
Ezt a problémát hivatott megoldani a tf-idf modell. Ez a mágikus rövidítés a “term frequency–inverse document frequency” angol kifejezést takarja. Ami nem más, mint a szavak súlyozása annak megfelelően, hogy milyen gyakran fordulnak elő általában és a konkrét dokumentumban.
Kezdjük a tf résszel! Ez a rész azt fejezi ki, hogy az adott kifejezés (szó), mekkora része az adott szövegnek. Több módszer használható erre, mi nézzük meg itt azt, ami figyelembe veszi a dokumentum hosszát. Ez lényegében csak egy egyszerű osztás:
Ahol:
— dokumentum, az adott szöveg
— term, az adott kifejezés (szó)
— a számított tf érték az adott dokumentumra és kifejezésre nézve
— menyiszer fordul elő az adott kifejezés az adott dokumentumban
— a kifejezések összege
Ez elég egyértelmű, de nézzünk egy példát: a „tud” kifejezést a fenti dokumentumban. Ez a szó négyszer fordul elő a szövegben, ez lesz a . A szöveg kifejezéseinek összege, a
, pedig 44. Vagyis a
ebben az esetben.
Most nézzük a idf részt! Ez kifejezi, hogy menyi információt hordoz a kifejezés az összes dokumentumot figyelembe véve. A számítása:
Ahol:
— az összes dokumentum
— a kifejezés fontossága az összes dokumentumra vetítve
— a dokumentumok száma
— menyi dokumentumban fordul elő a kifejezés
Esetünkben csak egy dokumentum van, így mindegyik idf értéke nulla lesz.
Rendben, most, hogy ismerjük a két részt, számítsuk ki a tf-idf-et. Ez csak a részek szorzata:
Mint látható a tf-idf lényegében egy súlyozása a kifejezéseknek. Most nézzük meg, hogy számítjuk Pythonban[2]:
# tf-idf átalakitás
# a TfidfVectorizer: egy lépésben szózsák modell és tf-idf átalakitás
from sklearn.feature_extraction.text import TfidfVectorizer
Tfidf_Vector = TfidfVectorizer(norm = None, sublinear_tf=True)
Tfidf_Matrix = Tfidf_Vector.fit_transform([text])
Tfidf_Matrix = Tfidf_Matrix.toarray()
# eredmény nyomtatása a blognak megfelelően
features = Tfidf_Vector.get_feature_names()
Tfidf_df = pd.DataFrame(np.round(Tfidf_Matrix, 3), columns = features)
Tfidf_df.T
Ennek eredménye:
0
alkony 1.000
belőle 1.000
bokor 1.000
csorgó 1.000
egyegy 1.000
fatörzs 1.000
fájdalom 1.000
föld 1.000
gyerekkorom 1.000
gyönge 1.000
ha 1.000
házfal 1.000
itthon 1.000
jelent 1.693
ki 1.000
kis 1.000
láb 1.000
láng 1.000
megy 1.693
merre 1.000
messzeringó 1.000
más 1.000
nyári 1.000
név 1.000
nő 1.000
ország 1.000
remél 1.000
szülőhazám 1.000
süpped 1.000
test 1.000
tud 2.386
tájék 1.000
térdepel 1.000
uton 1.000
világ 1.000
virág 1.000
vöröslő 1.000
ág 1.000
ölel 1.000
Ez nem igazán az, amit az 1 képlete alapján vártunk. Igen, nem. Mégpedig azért mert a sublinear_tf=True argumentum hatására más tf számítási módszert használ a sklearn. Mégpedig a következőt:
Végezetül
Fentebb átnéztünk egy jó adag optimalizációs lépést a szózsákmodellre. Egészen a 2010-es évek elejéig ezek az eljárások képezték az NLP alkalmazások alapját. A következő bejegyzésben a doc2vec modellt fogjuk megismerni, ami Neurális Hálózatosokon alapuló alternatívát kínál ezekkel a modellekkel szemben.
Irodalom
- Deniz KILINÇ — Text Processing 1 — Old Fashioned Methods (Bag of Words and TFxIDF)
- Christopher D. Manning, Prabhakar Raghavan & Hinrich Schütze — Introduction to Information Retrieval: Stemming and lemmatization
- Hafsa Jabeen — Stemming and Lemmatization in Python
- Gábor Berend, Richárd Farkas — Opinion Mining in Hungarian based on textual and graphical clues
“Szózsák modell normalizálása” bejegyzéshez egy hozzászólás