Egy belső support-eszköz rövid szöveges üzeneteket kap. A döntés:
valódi hibabejelentés-e az üzenet (support), vagy általános
megjegyzés, visszajelzés (nem_support). A bemenet egy-egy
rövid szöveges dokumentum, a megfigyelési egység az üzenet, a kimenet
pedig egy bináris címke. Ebben az alfejezetben egy olyan szöveges
baseline-t építünk, amely elkülönített tanító- és tesztkorpusszal
dolgozik, és egy ritka TF-IDF-mátrixból lineáris osztályozóval dönt. A
tanulási cél nem a nagy pontosság, hanem egy ellenőrizhető,
reprodukálható alapvonal.
Előzetes becslés: a példaként használt, mesterségesen egyszerű magyar
mondatokon a kulcsszavak (frissítés, driver,
leáll) alapján a támogatási jegyek valószínűleg jól
elkülöníthetők. A minta azonban olyan kicsi, hogy az eredményt inkább
folyamat-ellenőrzésként, mintsem általánosítható modellminőségként
érdemes értelmezni.
A tokenizálás (tokenization) során a nyers szöveget
kisebb egységekre, úgynevezett tokenekre bontjuk. A legegyszerűbb
változat a szóközök és írásjelek mentén való felosztás. A scikit-learn
TfidfVectorizer alapból hozzávetőleg a
(?u)\b\w\w+\b reguláris kifejezést használja: Unicode
szóhatárokat keres, és eldobja az egyetlen karakterből álló tokeneket. A
magyar toldalékok miatt a frissítés,
frissítése, frissült külön tokenként jelenik
meg; ez a korlátozás a későbbi hibaelemzéshez tartozik.
Az n-gram a szomszédos tokenek rögzített hosszúságú
sorozata. Az ngram_range=(1,2) beállítás unigramokat és
bigramokat is létrehoz: a driver frissítése szövegből
például driver, frissítése,
driver frissítése lesz. A bigramok segítenek megőrizni a
szórendből fakadó jelentést, például a nem indul
kifejezést.
A TF-IDF (term frequency–inverse document frequency) egy dokumentumot valós vektorrá alakít. Legyen a tanítókorpusz dokumentumainak száma, a token előfordulási gyakorisága a dokumentumban, pedig azoknak a dokumentumoknak a száma, amelyekben legalább egyszer szerepel. A scikit-learn simított inverz dokumentumgyakorisága:
[ \mathrm{idf}(t) = \log\frac{N+1}{\mathrm{df}(t)+1} + 1, ]
a súly pedig:
[ \mathrm{tfidf}(t,d) = \mathrm{tf}(t,d)\cdot \mathrm{idf}(t). ]
A vektort a scikit-learn soronként L2-normára normalizálja, így a dokumentum hosszának nyers hatása csökken. A TF-IDF logikai szerepe: a ritka token nagyobb súlyt kap, a minden dokumentumban előforduló token pedig közelítőleg 1-et; a logaritmus megfékezi a nagy gyakoriságkülönbségeket.
Kézi ellenőrző példa. Legyen három dokumentum:
driver frissítés leálldriver frissítésnyomtató hibaA tokenhalmaz: driver, frissítés,
leáll, nyomtató, hiba, tehát
.
A dokumentumgyakoriságok:
,
,
a többié 1. Ezért:
[ \mathrm{idf}(\text{driver})=\log\frac{4}{3}+1\approx 1.288, \qquad \mathrm{idf}(\text{leáll})=\log\frac{4}{2}+1\approx 1.693. ]
A
vektora (1.288, 1.288, 1.693, 0, 0), L2-normája körülbelül
,
így a normalizált vektor körülbelül
(0.518, 0.518, 0.681, 0, 0). A
normalizált vektora (0, 0, 0, 0.707, 0.707). A
skalárszorzat
és
között nulla, míg
és
között körülbelül
.
A ritka reprezentáció ezt a nagy, de nagyrészt nullákat tartalmazó
vektorteret használja.
A sparse, azaz ritka mátrix
(scipy.sparse) csak a nemnulla elemeket tárolja. Ha
több ezer vagy több százezer, akkor egy sűrű
(dokumentumok, tokenek) mátrix memóriaigénye lehetetlenül
nagy lenne. A ritka tárolásnál a műveletek hatékonysága is jobb, mert a
skalárszorzatot csak a nemnulla elemeken kell kiszámítani. A tanult
vektorizáló a fit hívás után rögzíti a tanítószókincset; a
transform minden új dokumentumot ugyanezen dimenziójú ritka
vektorrá képez.
A lineáris osztályozó (linear classifier) a dokumentumvektor és egy súlyvektor belső szorzatát számolja:
[ s(d) = w_0 + \sum_{j=1}^{V} w_j x_j. ]
A logisztikus regresszió ebből a sigmoid függvénnyel
valószínűséget készít:
.
A súlyok előjele és nagysága a Globális és lokális
magyarázat szerint lokálisan magyarázható; a nagy abszolút értékű
azt jelzi, hogy az adott token felfelé vagy lefelé tolja a döntést. A
ritka szöveges mátrixokon a lineáris modellek gyakran értelmes első
baseline-t adnak, mert a döntési felület egyszerű és gyorsan
tanítható.
A korpuszfelosztás (corpus split) a szöveges
feladatoknál is ugyanazt a szivárgásmentes elvet követi, mint a táblás
adatoknál: a tanító- és tesztkorpust a vektorizáló illesztése előtt kell
szétválasztani. Ha egy dokumentum két példányban szerepel, akkor a
tanító- és tesztkészlet közötti ismétlődés optimista becslést adhat; a
felosztásnál használt véletlen mag (random_state) pedig
reprodukálhatóvá teszi a munkamenetet.
Az alábbi példa tizenkét rövid, szintetikus magyar mondatot használ.
A cél a support címke előrejelzése. A kód a
tanítóvektorizálót csak a tanítószövegen illeszti, a tesztet pedig csak
transzformálja.
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.metrics import accuracy_score, classification_report
train_texts = [
"Szerver leállt az éjszakai frissítés után.",
"Nem tudok belépni a VPN-be a jelszófrissítés óta.",
"A nyomtató hibát jelez a driver frissítése miatt.",
"Frissítés után nem indul el az alkalmazás.",
"A heti riport elkészült és megosztva.",
"Köszönöm a gyors segítséget, minden rendben.",
"A csapatmegbeszélés jövő héten lesz.",
"Megkaptam a számlát, köszönöm.",
]
train_labels = [1, 1, 1, 1, 0, 0, 0, 0] # 1: support
test_texts = [
"A levelező nem indul a mai frissítés után.",
"Kérem a következő találkozó időpontját.",
"Driver-frissítés óta nem nyomtat a nyomtató.",
"A prezentáció diái elkészültek.",
]
test_labels = [1, 0, 1, 0]
pipeline = Pipeline([
("tfidf", TfidfVectorizer(ngram_range=(1, 2), min_df=1)),
("clf", LogisticRegression(max_iter=1000, random_state=42)),
])
pipeline.fit(train_texts, train_labels)
pred = pipeline.predict(test_texts)
print("Tanítószókincs nagysága:", len(pipeline.named_steps["tfidf"].vocabulary_))
print("accuracy (szemléltető kimenet):", accuracy_score(test_labels, pred))
print(classification_report(test_labels, pred, target_names=["nem_support", "support"]))A kód a projektgyökérből Linux/macOS alatt például így futtatható, ha
a scikit-learn már a projekt függőségei között van:
uv run python s105_baseline.pyA kód automatikusan még nem ellenőrzött, ezért a kiírt pontosságot itt nem közöljük mért értékként. A szöveg szerkezete alapján elméletileg várható, hogy a teszt három támogatási kulcsszavas mondata közül kettő erős jelet ad, de a pontos szám a tokenizálás és a véletlen súlyok függvénye. A fontos ellenőrző invariáns a dimenzió: a tanítókorpuszon illesztett vektorizáló a tesztet ugyanannyi oszlopú mátrixszá alakítja, mint a tanítót. Ha a két mátrix oszlopszáma eltér, akkor a vektorizálót véletlenül a tesztre is újraillesztettük, ami szerződésszegés.
A kézi ellenőrzéshez kérdezzük le a vektorizálóból a szókincset:
vec = pipeline.named_steps["tfidf"]
print(vec.get_feature_names_out())A várt listában a tanítószövegekből származó tokenek szerepelnek,
például frissítés, driver, nem,
indul; a bigramok közül például nem indul,
driver frissítése. A transform(test_texts)
során a tesztben előforduló, de a tanítószókincsben nem szereplő tokenek
csendben kimaradnak. Ez nem hiba, hanem a predikciós szerződés része: a
modell csak azt a jeltípust használhatja, amelyet a tanításkor
ismert.
A TF–IDF szókincsét és IDF-súlyait kizárólag a tanítókorpuszon illesszük. Azonos vagy közel azonos dokumentumok tesztbe kerülése külön szivárgási út.
Értelmezés. Miért kell a
TfidfVectorizer illesztését kizárólag a tanítókorpuszon
elvégezni? Fogalmazz meg legalább két konkrét következményt, ha valaki a
tanító- és tesztkorpust összefűzve illeszti a vektorizálót.
Megvalósítás. Változtasd meg a példában a
vektorizálót min_df=2 értékre, és futtasd újra a tanítást.
Mely tokenek tűnhetnek el a szókincsből? Hogyan változhat a tesztelés,
ha egy tesztmondat csak ilyen, egyszer előforduló tokent
tartalmaz?
Hibakeresés. Az alábbi vázlat hibás:
vec = TfidfVectorizer()
X_all = vec.fit_transform(train_texts + test_texts)
X_train = X_all[:8]
X_test = X_all[8:]
clf = LogisticRegression(max_iter=1000, random_state=42)
clf.fit(X_train, train_labels)
print(clf.score(X_test, test_labels))Nevezd meg pontosan, milyen szivárgást okoz ez a minta, és írd át a kódot úgy, hogy a vektorizáló csak a tanítószövegeket lássa.
"A VPN frissítés után nem indul." Hogyan kell helyesen
predikálni a betanított pipeline-nal? Mi történik akkor, ha a mondatban
szereplő összes token ismeretlen a tanítószókincsben? Hogyan lehet ezt a
helyzetet a Predikciós szerződés és hibateszt
predikciós szerződéshez hasonlóan észlelni?A vektorizáló illesztése két dolgot határoz meg: a tokenek
szókincsét és az idf-értékeket. Ha a teszt is részt vesz a
fit lépésben, akkor a modell olyan tokeneket is megismer,
amelyeket egy valódi éles rendszerben nem ismerhetne, és a tesztből
becsült dokumentumgyakoriság módosítja a súlyokat. Ennek következménye
optimista értékelés: a teszt nem a jövőbeli, ismeretlen szókincsű
üzenetek nehézségét méri. Másrészt a szivárgó vektorizálóval kapott
feature-tér nem azonos azzal, amit egy későbbi, csak tanítóra illesztett
változat állítana elő, így a kísérlet nem reprodukálható a termelési
szerződéssel.
A min_df=2 beállítás csak azokat a tokeneket és
n-gramokat tartja meg, amelyek legalább két tanítódokumentumban
előfordulnak. A példában például a nyomtató csak egy
tanítómondatban szerepel, ezért eltűnne a szókincsből. Ha egy
tesztmondat csak ilyen ritka tokenből állna, akkor a vektorizálás üres
sort adna; a modell csak az interceptből, azaz az osztályok tanítóbeli
arányából tudna dönteni. Ez kis, szintetikus korpuszon különösen élesen
mutatja a csekély adat korlátját.
A hiba a fit_transform(train_texts + test_texts)
hívásban van: a vektorizáló a teljes, tesztet is tartalmazó
szöveghalmazon tanulja meg a szókincset és az idf-et. A logisztikus
regresszió ugyan csak a train_labels címkéket látja, de a
bemeneti reprezentáció már tesztinformációt tartalmaz, ezért a
kiértékelés torz. A javított változat:
vec = TfidfVectorizer()
vec.fit(train_texts)
X_train = vec.transform(train_texts)
X_test = vec.transform(test_texts)
clf = LogisticRegression(max_iter=1000, random_state=42)
clf.fit(X_train, train_labels)
print(clf.score(X_test, test_labels))Az ellenőrző invariáns:
X_train.shape[1] == X_test.shape[1] és
len(vec.vocabulary_) == X_train.shape[1].
pipeline.predict(["A VPN frissítés után nem indul."])
hívással kell kiértékelni. A pipeline először a tanított vektorizálóval
transzformál, így az ismeretlen tokenek kimaradnak, az ismert tokenek
pedig a megszokott súlyokat kapják. Ha az üzenetben egyetlen
tanítószókincsbeli token sincs, a ritka sor minden eleme nulla; ilyenkor
a logisztikus regresszió csak a w_0 interceptet használja,
és a tanítóbeli gyakoriságnak megfelelő alapvalószínűséget ad. Az ilyen
eseteket érdemes explicit ellenőrzéssel kezelni: a
transform után meg kell nézni, hogy a létrejött ritka sor
nnz értéke nulla-e. Ez a séma a Predikciós szerződés és hibateszt predikciós
szerződés szöveges megfelelője: a vektorizáló dimenziója és a kimenő
feature-nevek stabilak, az ismeretlen token nem okoz összeomlást, de a
bizalom alacsony.A leggyakoribb hiba az, hogy a vektorizálót a tanító- és
tesztkorpussal együtt illesztik, mert a szövegfeldolgozást
„előfeldolgozó lépésnek” tekintik, amely még nem gépi tanulás. A TF-IDF
azonban tanulási lépés: a szókincs és az idf a korpuszból becsült
paraméter. Ellenpélda: ha a teszt tartalmazza az router
szót, és ezt a szivárgó vektorizáló beépíti a szókincsbe, a kísérleti
pontosság magasabb lehet, mint egy olyan rendszeré, amely éles üzem
közben találkozik először az ismeretlen router tokennal. A
teszt célja éppen ezt a jövőbeli ismeretlenséget kell modellezze; ezért
a fit hívás soha nem láthatja a tesztet.
Tedd fel magadnak a következő kérdéseket a saját futtatásod után:
Látta-e a vektorizáló a tesztszövegeket bármelyik lépésben? Egyezik-e a
tanító- és tesztmátrix oszlopszáma? A tesztkorpusszal nem optimalizáltam
sem küszöböt, sem min_df értéket? Ha bármelyik válasz
nemleges, akkor a kísérlet szivárgásmentesnek tekinthető ezen a
szinten.
A szöveges baseline egy ritka TF-IDF-mátrixot és egy lineáris osztályozót kapcsol össze. A tokenizálás a szöveget szószintű és bigram elemekre bontja; a TF-IDF a dokumentumgyakorisággal súlyozva emeli ki a ritka, megkülönböztető tokeneket. A korpuszfelosztásnak a vektorizáló illesztése előtt meg kell történnie, különben a tesztből származó szókincs szivárgást okoz. A példa kis, szintetikus magyar mondatokon ellenőrzi a dimenzióinvarianciát, de nem ad általánosítható pontosságot. A következő alkalmazásként az Embedding és hasonlósági keresés embedding-fejezetben összehasonlíthatjuk, hogy a rögzített vektoros reprezentáció hogyan módosítja ugyanennek a támogatásjegy-feladatnak az értékelését.