Szózsákmodell (Bag-of-words)

Ebben a bejegyzésben átnézzük a legalapvetőbb számítógépes nyelvészeti (Natural Language Processing, NLP) eljárást, az úgynevezett szózsákmodellt (bag-of-words).


Kezdjük talán azzal, mi is a számítógépes nyelvészet! Mivel korábban erről nem volt szó a blogon. Lényegében minden olyan számítógépes tevékenység, ami természetes nyelvek feldolgozásával foglalkozik.

Az NLP azt szeretné elérni, hogy feldolgozhassunk természetes szövegeket. Például szeretnénk megtudni, hogy miről szól a szöveg, két szöveg hasonló-e egymásra, mi lesz a következő szó, amit a felhasználó begépel etc. Ezek a kérdések elég összetettek, és csak több lépésben válaszolhatók meg. A szózságmodell[1] az egyik ilyen lépés lesz, nem maga a válasz.

Az alapvető problémánk természetes nyelvek feldolgozásánál: hogy miképpen ültessük át olyan formátumra, ami a számítógépnek is értelmezhető és lehetővé teszi, hogy matematikai formában felírjuk a problémánkat. Erre a kérdésre válaszol a bag-of-words.

Maga az ötlet nagyon egyszerű: készítsünk egy szótárt, amiben minden egyes ismert szó benne van. A feldolgozandó szövegekben pedig számoljuk meg, hogy a szótár szavai hányszor fordulnak elő. A fenti szótárt felfoghatjuk úgy is, mint egy dimenzióteret, és a szövegben előforduló szavak pedig az szöveg adott dimenzióban felvett értékei. Elméletileg ez lehetővé teszi számunkra, hogy mérjük a szövegek közötti távolságot.

Nézzünk egy példát! Legyen két szövegünk:

  1. „János szeret moziba járni. Éva is szeret moziba járni.”
  2. „Éva kirándulni is szeret.”

Mi lesz a szótárunk? Mi lesz ennek a szózsák modellje:

Szótár 1. szöveg 2. szöveg
János 1 0
szeret 2 1
moziba 2 0
járni 2 0
Éva 1 1
kirándulni 0 1
is 1 1

Táblázat: Szózsákmodell

Szózsákmodell

Most, hogy tudjuk, hogy néz ki a modell, készítsük el Pythonban:

szovegek = [ 
            "János szeret moziba járni. Éva is szeret moziba járni.", 
            "Éva kirándulni is szeret." 
           ]
# minimális szöveg előkészítés
for szidx in range(len(szovegek)):
    # különleges karakterek
    pattern = r"[{}]".format("(),.;:\-%\"") 
    szovegek[szidx] = re.sub(pattern, "", szovegek[szidx]) 
     
# szózsákmodell készítése
from sklearn.feature_extraction.text import CountVectorizer
BoW_Vector = CountVectorizer(min_df = 0., max_df = 1.)
BoW_Matrix = BoW_Vector.fit_transform(szovegek)
 
# a szózsákmodell a blog bejegyzé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)

Ami az elvárt eredményt nyomtatja ki:

            0  1
is          1  1
jános       1  0
járni       2  0
kirándulni  0  1
moziba      2  0
szeret      2  1
éva         1  1

Lényegében ennyi a szórósan vett szózsákmodell.

Most nézzünk meg néhány kérdést és problémát ezzel a modellel kapcsolatban.

Távolságmérés

Mint fentebb említettem, az alapvető cél az NLP-ben, hogy matematikailag tudjuk mérni a szövegeket és megfogalmazni a kérdéseinket. A fenti két szöveg az „Éva” és az „is” dimenzióban hasonlít a legjobban egymásra, ahol mindkettő értéke 1. A legnagyobb távolságuk pedig a „moziba” és a „járni” dimenzióban van. Ez eddig egyszerű, minden egyes tengely mentén látjuk, hogy mekkor a két szöveg távolsága. De hogyan lehetne ezt egyetlen számban kifejezni?

Két vektor közötti távolságot több módon lehet mérni. A két legelterjedtebb az un. Cosine hasonlóság és az euklideszi távolság. A következő ábra szemléti, hogy mit is jelentenek ezek:

Euklideszi távolság és Cosine hasonlóság

Az ábrán az A és B pont közti távolságot mérjük. Látható. hogy az euklideszi távolság, E-vel jelölve, a két pontot összekötő egyenes hossza.

A Cosine hasonlóság a fenti ábrán \theta-val van jelölve. Lényegében ez nem más, mint a két vektor által bezárt szög. Pontosabban ennek a szögnek a koszinusza. Miért koszinusz? Ez  dinamikusabban változik 0-hoz közel, mint egy lineáris függvény. Vagyis segít abban, hogy kisebb különbségeket is észrevegyünk amikor a vektorok amúgy nagyon hasonlóak egymáshoz.

A konkrét számítás így néz ki:

 \cos(\theta) = {\mathbf{A} \cdot \mathbf{B} \over |\mathbf{A}| |\mathbf{B}|} = \frac{ \sum\limits_{i=1}^{n}{A_i B_i} }{ \sqrt{\sum\limits_{i=1}^{n}{A_i^2}} \sqrt{\sum\limits_{i=1}^{n}{B_i^2}} }

Ahol:

  • \cos(\theta) — a Cosine hasonlóság
  • \mathbf{A} — az A vektor
  • \mathbf{B} — a B vektor
  • |\mathbf{A}| — az A vektor hossza
  • |\mathbf{B}| — a B vektor hossza
  • n — a dimenziók száma, esetünkben a szótár szavainak száma
  • A_i — az A vektor értéke az i dimenzióban, esetünkben az egyes szavak száma a szövegben
  • B_i — a B vektor értéke az i dimenzióban

Rendben, akkor számoljuk ki:

import numpy as np
# János szeret moziba járni. Éva is szeret moziba járni.
elsoszoveg = np.array([1,2,2,2,1,0,1])
# Éva kirándulni is szeret.
masodikszoveg = np.array([0,1,0,0,1,1,1])
 
# euklideszi távolság
dist = np.linalg.norm(elsoszoveg -masodikszoveg )
 
# cosine hasonlóság
cos_sim = np.dot(elsoszoveg , masodikszoveg )/ \
          ( \
            np.linalg.norm(elsoszoveg )* \
            np.linalg.norm(masodikszoveg ) \
          )

Ami a fenti példában kb. 0,516 lesz. Mit is jelent ez a szám? Ha a két szöveg teljesen megegyezik, akkor az általuk bezárt szög 0°, aminek a koszinusza 1. Tehát 1 a maximális értéke a Cosine hasonlóságnak. Hasonló logika alapján, a legnagyobb távolság akkor van, ha a vektorok 90° szöget zárnak be, amikor is 0 a Cosine hasonlóság. A mi esetünkben körülbelül 60° ez a szög, ami 0,5.

Ez a 60° magában nem mond semmit. Viszont ha van egy harmadik szövegünk, akkor már tudjuk mérni, hogy melyik két szöveg hasonlít egymásra a legjobban. Tegyük ezt! Az új, 3. szövegünk legyen:

  1. „Éva is szeret moziba járni.”

Ez azért érdekes, mert, ugye, ez az 1. szöveg egy része, ugyanakkor szerepelnek benne „Éva”, „szeret” és „is” szavak, ami miatt a szavai 3/4 részben[2] megegyeznek a 2. szöveggel.

Az új szöveg vektorformában:

Szótár 3. szöveg
János 0
szeret 1
moziba 1
járni 1
Éva 1
kirándulni 0
is 1

Táblázat: 3. szöveg szózsákmodellje

Ha elvégezzük a fenti számításokat, akkor a következő eredményt fogjuk kapni a Cosine hasonlóságra:

1. szöveg 2. szöveg 3. szöveg
1. szöveg 1 0,5163977794943222 0,9237604307034011
2. szöveg 1 0,6708203932499369
3.szöveg 1

Táblázat: Példa szövegek Cosine hasonlósága

Amint az fentebb látható, a Cosine hasonlóság alapján az 1. és a 3. szöveg hasonlít legjobban egymásra. Ez nagyjából várható volt, mivel a 3. az 1. része.

Most nézzük az euklideszi távolságokat:

1. szöveg 2. szöveg 3. szöveg
1. szöveg 0 3.3166247903554 2.0
2. szöveg 0 1.7320508075688772
3. szöveg 0

Táblázat: példaszövegek euklideszi távolsága

Hoppá! Itt viszont a 2. és a 3. szöveg között a legkisebb a távolság, tehát ezek hasonítanak a leginkább egymásra. Ez várható volt olyan szempontból, hogy a két szöveg szókincse 75%-ban megegyezik.

A fenti különbség szemlélteti, hogy miért szoktak az NLP területén inkább a Cosine hasonlóságot számolni. Természetes szövegek feldolgozásánál jobb ötletnek vélhető nem csupán a szókincset figyelembe venni a hasonlóság számítása esetén, hanem a szövegek hosszát is. Ez az, amit a Cosine hasonlóság megtesz az euklideszi távolság számítással ellentétben.

Talán egy kicsit zavaró lehet a fentiekben, hogy míg az euklideszi távolságnál a kisebb szám jelenti azt, hogy a vektorok közelebb vannak egymáshoz, addig a Cosine hasonlóságnál a nagyobb. Ezért néha Cosine távolságot is szoktak használni, ami 1-\cos(\theta). Pythonban ha scikit-et használunk, akkor ezeket a következő módon számolhatjuk:

szovegek = np.array([[1,2,2,2,1,0,1],[0,1,0,0,1,1,1]])
 
# Cosine távolság
from sklearn.metrics import pairwise_distances
cosine_tavolsag = pairwise_distances(szovegek, metric = "cosine",n_jobs = -1 )
 
# Cosine hasonlóság
from sklearn.metrics.pairwise import cosine_similarity
cosine_hasonlosag = cosine_similarity(szovegek)
 
# Ellenőrzés
print(cosine_tavolsag+cosine_hasonlosag)
 
# A legnagyobb távolság indexe
indices = np.where(cosine_tavolsag  == cosine_tavolsag.max())
for index in indices:
    print("A legnagyobb távolság indexe: ", index)

Problémák a szózsákmodellel

Ez eddig mind szép és jó, de van itt néhány probléma.

Először is a szórendet nem vesszük figyelembe. Ez a magyar nyelvben talán nem akkora probléma, mivel nálunk nincs kötött szórend, de több más nyelv esetében, például az angolban annál inkább jelentős a szórend.

Másodszor, ahogy egyre több szövegünk van, a szótárunk egyre nagyobb, ezzel párhuzamosan az egyes szövegek vektorjaiban egyre kisebb a hasznos információ aránya. Képzeljük el, mi történik, ha a fenti szótárt kibővítjük száz szóval. Az 1. szövegben ebben az esetben is csak hat egyedi szó lesz. De a vektorterünk 107 dimenziós lesz, vagyis 101 darab 0 lesz az 1. szöveget ábrázoló vektorban. Ennek a megnövekedett vektortérnek a kezelése pedig sokkal nagyobb erőforrást igényel.

Harmadszor, mi van a hasonló jelentésű szavakkal? Például ha „szeret” helyett „kedvel” szerepel. A modell szerint ez a kettő nem hasonlít jobban egymásra mint a „szeret” és „utál” szavak. Ez a probléma a ragozó nyelveket, mint amilyen a magyar, hatványozottan érinti. A „moziban” és a „moziból” szoros rokonságban vannak. Sajnos ezt a szózsákmodell nem képes megragadni.

A fenti problémákra válaszként több irányba próbálták továbbfejleszteni a szózsákmodellt. Ezek közül ma csak egyet fogunk megnézni: az n-gram modellt. Miért? Mert ez közvetlen leszármazottja a szózsákmodellnek.

n-gram változat

Az első probléma megoldására született a szózsákmodell ezen változata. Az ötlet nagyjából annyi, hogy amennyiben a szórendet nem akarjuk elveszteni, akkor ne csupán az egyes szavak legyenek a szótárban, hanem a szókapcsolatok is. Az n netű azt jelenti, hogy menyi szót kapcsoljunk össze. Például egy 2-gram szózsákmodell esetén két szó hosszasságúak az egységek.

Maradva a fenti példánál ebben az esetben a 2-gram szótár így fog alakulni:

Szótár 1. szöveg 2. szöveg 3. szöveg
János 1 0 0
szeret 2 1 1
János szeret 1 0 0
moziba 2 0 1
szeret moziba 2 0 1
járni 2 0 1
moziba járni 2 0 1
Éva 1 1 1
is 1 1 1
Éva is 1 0 1
is szeret 1 1 1
kirándulni 0 1 0
Éva kirándulni 0 1 0
kirándulni is 0 1 0

Táblázat: 2-gram Szózsákmodell

Rendben. Feltételezem, hogy mindenki látja az alapötletet. Vajon a problémákat is? Először is, döntsük el, hogy mennyi legyen az „n”. Másodszor lássuk be, hogy a szavak nem csupán a közvetlen szomszédjukra hathatnak. Harmadszor pedig a fentebb már említett második problémát nem oldottuk meg, mert a hasznos információ arányát túlzottan csökkentettük.

Végezetül

A szózsákmodell nagyjelentőségű volt az 50-es években, amikor kidolgozták, de mára nyilvánvalóvá váltak a korlátai. A jövőbeli bejegyzésekben majd igyekszem bemutatni, hogy milyen alternatíváink vannak napjainkban.

Irodalom

  • Jason Brownlee: A Gentle Introduction to the Bag-of-Words Model
  • Selva Prabhakaran: Cosine Similarity – Understanding the math and how it works (with python codes)

Végjegyzetek

  1. A magyarítás forrása: VI. Alkalmazott Nyelvészeti Doktorandusz Konferencia. De több helyen is találkoztam vele. ↩︎
  2. Csak a „kirándulni” a különbség. ↩︎
Hírdetés

Szózsákmodell (Bag-of-words)” bejegyzéshez egy hozzászólás

Vélemény, hozzászólás?

Adatok megadása vagy bejelentkezés valamelyik ikonnal:

WordPress.com Logo

Hozzászólhat a WordPress.com felhasználói fiók használatával. Kilépés /  Módosítás )

Twitter kép

Hozzászólhat a Twitter felhasználói fiók használatával. Kilépés /  Módosítás )

Facebook kép

Hozzászólhat a Facebook felhasználói fiók használatával. Kilépés /  Módosítás )

Kapcsolódás: %s