Kihagyás

TF-IDF – A szófontosság értékelése

A következő leírás a TF-IDF algoritmusról és változatairól szól. Először vázoljuk a problémát, amit kezelni akarunk, kitérünk az egyéb elterjedt megoldások hiányosságaira, végül részletezzük magát az algoritmust, és annak keresésre való alkalmazását.

Amennyiben a szócikkben részletezett algoritmus jelenlegi implementációjára vagyunk kíváncsiak, látogassunk el a Keyword Search modul dokumentációjára.

Definíciók és háttér

A probléma

Tekintsük a szövegkeresés alapproblémáját:

Szövegkeresés

Adott: Szövegek egy \(\mathcal{D}\) halmaza, egy \(q\) kérdés és \(C\) kérdést kontextus.

Feladat: Keressük meg \(q\) és \(C\) segítségével azon \(\mathcal{D}\)-beli szövegeket, melyek tartalmazzák a pontos válaszadáshoz szükséges információt.

Mi most erre a feladatra fogunk koncentrálni, azaz az alapszituáció, hogy rendelkezésünkre áll egy szöveges egységekből álló halmaz, és mi ezek közül néhányat szeretnénk megtalálni, melyek relevánsak \((q, C)\)-hoz. Továbbá kezdetben feltesszük, hogy \(q\) önálló, azaz olyan, hogy \(C\) nem ad többletinformációt, vagy más szóval „\(q\) kontextus nélkül is ugyanúgy értelmezhető“. Ennek megfelelően \(C\)-t nem is használjuk, csak az erre vonatkozó szakaszban.

Minek van kontextus?

A jelenlegi dokumentációt és gondolati síkot erősen a RAG koncepció határozza meg. Ugyanakkor enélkül is hasznos ilyen általánosságban gondolni a kérdésre.

Egyéb megoldások és hiányosságaik

A feladat megoldására jelenleg több módszert alkalmazunk. Az egyik a szemantikus keresés, ami a következő, bejáratott módszer:

Szemantikus keresés

Vegyünk egy LLM-et, ennek beágyazás (embedding) függvénye legyen \(\text{emb}\). Mentsük el minden \(d\in D\)-re \(\text{emb}(d)\) vektort (legyen ezek halmaza \(E\)), majd \(q\) kérés érkezésekor hasonlítsuk össze \(\text{emb}(q)\)-t \(E\) minden elemével (a beágyazási módszer hasonlóság-függvénye segítségével).

Ekkor az első pár leghasonlóbb elemet véve kapjuk a keresés eredményét.

A beágyazásokról további információ található a Szemantikus beágyazás szócikkben, míg a konkrét, jelenleg használt megvalósításról a Semantic Search szócikkben.

A beágyazások nagyon erős eszközt biztosítanak szövegek szemantikus tartalmának leképezésére. Ugyanakkor két problémába futunk

  1. A jó minőségű beágyazás modellek első sorban angol nyelvűek

  2. Sok szövegben egy-egy szó megváltoztatása igencsak más jelentést kölcsönöz annak

Bár (2) ugyan önmagában is fennáll, (1) erősíti, hiszen a modellek kevésbé képesek megragadni a magyar nyelvben egy-egy szóváltozás valódi szemantikus hatását. Emellett (2) már csak azért is fennáll, mert a modell nem tud mindenről. Például általa ismeretlen tulajdonnevek esetén beágyazás szinten „mindegy“, melyiket írjuk egy mondatba; a változtatás nagyon kis behatással van arra, hogy hogy képezi le szemantikusan a mondatot.

Példa a banki dokumentumokkal

A dokumentumhalmazunk főleg banki jellegű. Az előbbi problémára jó példát biztosítanak a szolgáltatások: a következő két mondat szemantikusan igencsak hasonló:

  • Szeretnék igényelni Kamat Pluszt, mik ennek a lépései?

  • szeretnék igényelni KamatMax-ot, mik ennek a lépései?

Nyilván akinek fogalma sincs arról, hogy a bank kínálatában a Kamat Plusz és a KamatMax termékek léteznek-e, illetve milyen típusúak, az joggal gondolhatja, hogy a két kérdésre a válasz megegyezik, vagy legalább nagyon hasonló. Egy beágyazási modell is közel fogja elhelyezni a kettőt egymáshoz.

Az ElasticSearch

Voltak kísérletek ElasticSearch bevezetésének irányába is. Végső soron ez is egy kucsszavas keresés (alap konfigurációban), viszont a tapasztalat az volt vele kapcsolatban, hogy magyar nyelven nem képes megfelelően dokumentumot keresni ahhoz, hogy kiváltsuk vele az alábbi rendszert.

Érdemes megjegyezni, hogy egyéb beállítás hiányában az Elastic ugyanazt az algoritmust használja, amit mi.

Mi az a kulcsszavas keresés?

A kulcsszavas keresés olyan módszerek gyűjtőneve melyek a szövegkeresési feladatot úgy próbálják megoldani, hogy a hasonlóság nem valami koncepcionális elkódoláson alapszik, hanem valamiféle „egyszerű“ szöveghasonlóságon, ahol a szöveghasonlóságot szavak, szóelemek szerinti felbontás segítségével definiáljuk.

A koncepció központjában tehát egy olyan függvény áll, melyet a legjobban talán így tudunk megfogalmazni: „szövegek közötti (súlyozott) átfedési mérték“. Ezt a mértéket jelöljük \(m\)-mel, vagyis adott \(d\in D\)-re \(m(q, d)=\) „a \(q\) kérdés hasonlósága a \(d\) szöveghez“.

Ez a megfogalmazás igencsak bonyolult, a következő példa talán megvilágítja a definíció lényegét.

Egyszerű példa kulcsszavas keresésre

Legyen \(\mathcal{K}\) egy feketedoboz, ami képes nekünk megmondani, hogy tetszőleges szövegben mik a kulcsszavak (bármit is jelentsen ez). Azaz ha \(t\) egy szöveg, akkor a \(\mathcal{K}(t)\) halmaz az \(t\) kulcsszavait tartalmazza.

Definiáljuk \(m\)-et a következő módon:

\[ m(q,d) = |\mathcal{K}(q) \cap \mathcal{K}(t)| \]

Azaz \(m(q,d)\) az \(q\) és \(d\) közös kulcsszavainak száma. Ekkor tehát minnél több közös kulcsszava van a kérésnek a szöveggel, annál „közelebb“ vannak a mérték szerint.

A példa algoritmusa egy józan hozzáállásnak tekinthető a feladat szempontjából, ugyanakkor könnyen olyan problémákba futunk, hogy

  • Hogy határozzuk meg a kulcsszavakat, és mi az a kulcsszó egyáltalán?
  • Hogyan deduplikáljuk az azonos kulcsszavakat, melyek több formában jelennek meg (pl. rövidítések, elírások)
  • Milyen súlyokat adjunk a kulcsszavaknak (mert a fontosságuk általában eltér)?

Mindenesetre ezzel a kulcsszavas keresés alapötletét körülírtuk. A továbbiakban egy alternatív mértéket definiálunk, mely algoritmikus választ ad a leírt problémákra.

A szósúlyozási feladat és a TF-IDF algoritmus

Ezen szakasz a TF-IDF-típusú algoritmusok bemutatásáról szól. Az algoritmusok részletezése során egy \(T\) szövegre úgy gondolunk, mint szavak sorozata, és ennek megfelelően indexelhetjük is (\(T_i\) a \(T\) szöveg \(i.\) szava).

Ezen algoritmusok célja nem bonyolult, alapfeladatuk a következő:

A szósúlyozási feladat

Adott: szövegek egy \(\mathcal{T}\) halmaza

Feladat: Határozzuk meg az \(s\) szó súlyát minden \(T\in \mathcal{T}\) szövegre (ahol \(s\in T\))!

Innentől az \(s\) szó súlyát a \(T\) szövegben \(w_T(s)\)-sel jelöljük.

Ha van egy jó algoritmusunk a szósúlyozási feladatra, akkor abból algoritmust konstruálhatunk a szövegkeresési feladatra is. Valóban, Legyen \(S\) a szövegekben megjelenő összes szavak halmaza (azaz \(S:=\{s: s\in T\in \mathcal{T}\}\)). Ekkor minden szöveget elkódolhatunk egy súlyvektorral, ahol a \(T\)-nek megfelelő vektor (jele \(v^T\)) \(s\) koordinátáján a \(v^T_s := w_T(s)\) érték áll (illetve 0, ha \(s\notin T\)). Ekkor \(m(q, T) := v^q \cdot v^T \equiv \sum_{s\in S} v^q_s v^T_s\) definícióval megkaptuk az algoritmust a szövegkeresési feladatra.

Mi lehet szó?

Ezen a ponton fontos megjegyezni, hogy az algoritmusban egy „szó“ nem feltétlen kell, hogy szó legyen. Sőt, a szöveg sem kell, hogy szöveget jelentsen. Valójában csak arra van szükségünk, hogy ha a „szövegeink“ azok \(\mathcal{E}_1\) típusú entitások, és a szavaink pedig \(\mathcal{E}_2\) típusúak, akkor legyen egy „szófelbontás“ funkciót ellátó \(f:\mathcal{E}_1\rightarrow \mathcal{P}_m\left(\mathcal{E}_2\right)\) függvényünk.

Mi az a \(\mathcal{P}_m\)?

Itt \(\mathcal{P}_m\) a hatvány-multihalmaz, azaz \(\mathcal{P}_m(A)\) azon multihalmazok (halmazok, melyekben egy elem többször is lehet) halmaza, melyek \(A\) elemeiből állnak, a multiplicitások pedig tetszőlegesek lehetnek.

Jelen gondolatmenetben persze \(\mathcal{E}_1\equiv \mathcal{T}\) és \(\mathcal{E}_2 = \{\text{a magyar szavak halmaza}\}\), \(f\) pedig az a függvény, ami megmondja, hogy „a mondatban ezek a szavak ennyiszer szerepeltek“. Mint ezt később látni fogjuk, ennél jobb szó-szerepű entitásokat is tudunk találni, erre kitérünk a megfelelő szakaszban és a Keyword Search modul dokumentációján is.

Implementációs megfontolások

Implementáció szempontjából (mint ahogy általában szokás) \(S\) elemeit rendezzük, és indexeljük őket (a vektorok is így értelmzehetők \(\mathbb{R}^{|S|}\)-beli vektorokként).

Emellett míg \(v^T\) ismert, afelett átsiklottunk, hogy \(v^q\)-t valahogy ki kell számolnunk. De ez nem (akkora) probléma: a számolások során minden \(\mathcal{T}\)-től függő értéket fixáltnak tekinthetünk, és ezekkel számoljuk ki \(w_q(s)\)-t minden \(s\in S\)-re. Ekkor természetesen ha egy eddig ismeretlen szó szerepel \(q\)-ban, akkor az alapján azt a rendszer a keresésnél nem fogja figyelembe venni (mivel a szövegek súlyvektoraiban nincs megfelelő koordináta).

Ilyen módon gondolunk a TF-IDF algoritmusra. Ezt a dokumentáció szempontjából többé-kevésbé fekete doboznak tekintjük, amiről mindössze annyit kell tudni, hogy valamilyen módon kiszámolja ezeket a \(w_T(s)\) értékeket (méghozzá rögtön mátrix formában). Az algoritmus működéséről a következő megjegyzés tartalmaz egy kisebb leírást, pontos képletekért érdemes a vonatkozó Wikipédia szócikkeket felkeresni (TF-IDF, BM25)

Az algoritmusok működése

A TF-IDF algoritmus két összetevőt számol ki. Az egyik a szógyakoriság (TF = term frequency). \(tf(s, T)\)-hez először kiszámoljuk, hogy hányszor szerepel az \(s\) szó a \(T\) szövegben, majd normáljuk a szöveg hosszával. A másik az inverz szöveggyakoriság (IDF = inverse document frequency). \(idf(s)\)-hez kiszámoljuk, \(\mathcal{T}\) hány szövegben szerepel az \(s\) szó, és ezzel leosztjuk az összes dokumentumok számát, illetve ennek az értéknek a logaritmusát vesszük.

Képletekkel

\[ \text{tf}(s,T) = \frac{ |\{i:\ T_i = s\}| }{ |T| } \qquad\qquad \text{idf}(s) = \log \frac{ |\mathcal{T}| }{ |\{T\in \mathcal{T}:\ s\in T\}| } \]

A ekkor a TF-IDF szerint \(w_T(s) = \text{tf}(s,T) \cdot \text{idf}(s)\).

Heurisztikusan a következő történik. Egy szóra az \(\text{idf}\) értéke megmondja, mennyire ritka (pl. az „a“ szó minden szövegben szerepel, így az \(\text{idf}\) értéke 0. Míg a „KamatMax“ jóval kevesebb szövegben szerepel, erre a nagyon magas lesz az érték). Továbbá egy szóra és egy szövegre \(\text{tf}\) érték megmondja, mennyire gyakori a szövegben. Így tehát ha egy ritka szó sokszor megjelenik egy szövegben, az nagyon magas pontot kap, míg ha egy gyakori szó jelenik meg sokszor, az kevesebbet.

A BM25 algoritmus ennek egy változata. A TF-IDF súlyozás hibája, hogy bár a szövegek hosszával normál, a gykoriság nem feltétlen megfelelő súlyozása a „fontosságnak“. Ugyanis pl. egy személyről szóló Wikipédia szócikk egy a személy nevét lehet, csak néhányszor tartalmazza, ugyanakkor igencsak releváns.

Erre a BM25 algoritmus megoldása, hogy a szógyakoriság képletét egy \(b\) és egy \(k_1\) paraméterrel ruházza fel. A képlet pontos felírása és a paraméterek szemléltetése ezen dokumentáció keretein túlmutat. A komponensek definíciója és a jelentéstartalma az algoritmus(ok) Wikipédia oldalán megtalálható.

Annyit zárásként megjegyzünk, hogy az IDF komponenst sem pontosan így definiálják; konstansok hozzáadásával elkerülhetők a nullából adódó matematikai kivételek és nagyságrendi inkonzisztenciák. Az algoritmusban használt pontos képlet szintén megtalálható a megfelelő Wikipédia szócikkben.

Az értékek normálásáról

Felmerülhet a kérdés, hogy milyen nagyságrendűek a kapott hasonlóságok, illetve hogy nem lehet-e valahogyan meghatározni egy százalékos értéket, hogy „a \(q\) kérdés 93%-ban fontos a \(d\) szöveghez“. A válasz az, hogy nem kéne.

Amennyiben mégis akarunk, számos eszköz áll rendelkezésünkre, amelyek képesek az esetek egy részében értelmes eredményt adni. Néhány példa:

min-max

Vesszük az összes m(q, d) hasonlóságot, és legyen

\[ m'(q, d) := \frac{ m(q, d) - \min m(q, \cdot) }{ \max m(q, \cdot) - \min m(q, \cdot) } \]

Előnye, hogy midnen pontszám a \([0,1]\) intervallumba fog esni. Hátránya, hogy \(m'(q,d)\) erősen függ a többi dokumentum pontértékeitől. Például ha nincs releváns dokumentum, akkor az irrelevánsak is nagyon magas értéket fognak kapni. Mindenesetre a mi feladatunkra valószínűleg még ez a legkövetkezetesebb normálás.

normalizálás

Megpróbálunk egy normális eloszlást illeszteni a pontokra. Legyen \(\mu\) a pontok átlaga, \(\sigma\) a szórása, és legyen

\[ m'(q, d) := \frac{ m(q, d) - \mu }{ \sigma } \]

Előnye, hogy a pontszámok így \(0\) központú, \(1\) eloszlásúak lesznek. Hátránya, hogy nincs se alsó, se felső korlát az értékekre, megjelennek negatív értékek, és \(m'\) itt is hasonló módon függ a többi dokumentumtól

softmax

Megcsavarjuk a kérdést, nem azt kérdezzük, hogy adott dokumentum mennyire hasonló, hanem hogy a hasonlósági pontszám alapján adott dokumentumot mekkora valószínűséggel választanánk. Ez a softmax függvénnyel számolható. A mi feladatunk szempontjából ez a kérdés nem túl érdekes, illetve továbbra is dokumentumhalmazra relatív lesz \(m'\).

Tetszőlegesen sok függvényt lehet felírni, ami valamilyen módon normálja az eredményt, ugyanakkor ismét kiemelendő, hogy ezt nem kéne, az algoritmus nem ad ilyen módon egyértelműen interpretálható eredményt.

Pár szó a szóról

Az egyszerű kulcsszavas keresés felsorolt problémái közül eddig kettőt kezeltünk:

  • A kulcsszavak súlyozását úgy oldottunk meg, hogy minden szót súlyoztunk.
  • A kulcsszó definiálásának szükségét elkerültük azzal, hogy minden szót súlyoztunk azzal a feltevéssel, hogy a „fontosak“ majd úgyis magas súllyal fognak szerepelni a reprezentáló vektorban (ezeket akár a dokumentum kulcsszavainak is tekinthetjük, ha valamilyen módon korlátoljuk, meddig nézzük a top súlyokat).

Ugyanakkor maradt egy probléma: hogyan deduplikálunk?

Erre igazán jó megoldást nem ad az algoritmus, és a gyakorlatban is körülményes kérdés. Egy specifikus esetet érdemes viszont mindig kezelni: a különböző szóalakok esetét (toldalékolás). Erre a gyakorlatban kiterjedt NLP eszköztár áll rendelkezésre, és jelentősen javítja az algoritmus teljesítményét, hiszen azonos szóhoz ezzel nem 10-20-30 szóalak tartozik (mely mind különböző „term“-nek felel meg a TF-IDF-ben), hanem 1, vagy legalábbis csak néhány.

Agglutináló nyelveknél (mint a magyar is) ez a lépés kifejezetten fontos bármilyen szó-alapú megoldáshoz, hiszen jelentősen csökkenti a megjelenő szavak számát, és növeli egy-egy szó gyakoriságát.

Mikor működik jól az algoritmus?

Van egy paraméter, amiről eddig nem esett szó, pedig igencsak fontos: a szövegek mennyisége. Mivel a TF-IDF jellegű algoritmusok a szövegekben való megjelenést használják a fontosság megtippelésére, a keresés pontos működéséhez alapvetően egy nagy korpusz kell (hiszen vegyük észre, hogy pl. ha egy szövegünk van, az IDF mindenre 1). Kevés szöveg esetén a modell nem (feltétlen) lesz képes kitalálni, hogy mely szavak és szókapcsolatok „kulcsszó-jellegűek“, és melyek lényegtelenek.

Nem megfelelő szövegmennyiség esetén jelentősen javítható a teljesítmény, ha egy nagyobb, általános korpuszt rakunk a hozzáadott korpuszunk mellé, amit illesztés után egyszerűen elhagyunk (erre megfelelő lehet párszáz Wikipédia szócikk). Ennek az az előnye is megvan, hogy a korpusz-specifikus szakszavak, tulajdonnevek a hozzáadott szövegekben valószínűleg nem szerepelnek, így tovább emelik ezek IDF pontját.

Értelmezés információkeresésként

A szócikk során hasonlóan vezetjük be a fogalmakat, mint ahogy az az Információkeresés szócikkben megfigyelhető. Ugyanakkor hasznos, ha teljes egészében felírjuk a feladatot és módszert az ott definiált keretrendszer segítségével.

Fogalom Megoldásbeli megfelelő
\(\mathbb{I}\) Szöveghalmaz
\(\mathbb{i}\) Egy szöveg
\(\mathcal{I}_{\text{raw}}\) A szövegek halmaza, amiből a választ keressük
\(\mathfrak{p}\) Szövegek felbontása szavakra, majd a TF-IDF
értékek kiszámolása
\(\mathcal{I}\) A szövegekhez tartozó TF-IDF vektorok
\(\mathbb{S}\) Egy szöveges kérdés
\(\mathfrak{f}\) Felbontjuk a szöveget szavakra, elkészítjük a TF-IDF
vektorát, meghatározzuk a hasonlóságát az egyes
szövegekhez skaláris szorzás segítségével
\(\mathbb{f}\) Szövegek egy rendezett sora

Természetesen ezen módon való keresés egy rendező szövegkeresés. \(m\)-et már definiáltuk korábban, szabadszavasan valójában \(L_S^{\mathfrak{s}}(\mathcal{D})\) rendezett halmazt is.

Kiterjesztések

A TF-IDF-alapú megszokott formájukban szövegeken és szavakon operálnak. Ez több félén is kiterjeszthető, néhány triviálisabb, míg néhány bonyolult technikai módosításokat kíván. Ezek alapvetően vagy a kontextusból táplálkoznak, vagy a „szó“ és „szöveg“ fogalmát terjesztik ki.

Az egyik legegyszerűbb belenyúlás a címke alapú szűrés. Ilyenkor egy \(d\) alatt nem csak egy szöveget értünk, hanem egy szöveget és néhány címkét. Hasonlóan, \(C\) bemeneti kontextus is tartalmaz(hat) egy vagy több címkét. Ez alapján logikai kapuk segítségével megmondhatjuk, hogy \(\mathcal{D}\) mely elemeit kell figyelembe vennünk, mikor az első néhány elemet nézzük. Amennyiben a keresés jól működik, és logikus címkékre szűrünk, ez nem fogja nagyban befolyásolni egy ilyen algoritmus futási idejét, hiszen feltételezhetően eredetileg is magas pontszámmal rendelkeztek volna a megtalált szövegek.

Az is előfordulhat, hogy beszélgetési előzmény van megadva a kontextusban. Erre lehetséges megoldási módok a következők:

  • Definiálunk TF-IDF tokeneket („szavak“), melyek valamilyen módon a beszélgetés lényegét kódolják el. Ezeket megpróbáljuk szövegfeldolgozás közben dokumentumonként kitalálni, majd egy érkező kérdés esetén a beszélgetési előzményből is kitalálni. (Ez a módszer igen bonyolult, ilyenféle megoldással jelenleg nem dolgozunk.)

  • Újrafogalmazzuk a kérdést magát, hogy teljesen önálló legyen, azaz tartalmazza az összes információt, ami a beszélgetésből szükséges.

Végül ismét fontos kitérni arra, hogy amit az algoritmusban szövegnek és szónak tekintünk, az csak konvenció, és a számolás működésének szempontjából lényegtelen. Valójában csak egy jól definiált „szófelbontó“, azaz tokenizáló függvényre van szükségünk. Az, hogy ez milyen bemeneteken működik, és milyen tokeneket hoz létre, az nem számít, amíg ezek a tokenek értelmesek (azaz pl. nem mindegyik csak egy „szövegben“ jelenik meg).