Egy rendelési késést modellező feladatban 1200 rendeléshez 122 numerikus feature-t kapunk: kettő valódi jelet hordoz, 120 pedig független standard normális zaj. A megfigyelési egység a rendelés, a célváltozó a 7 napot meghaladó kézbesítés indikátora. A feladat: válasszuk ki a 10 legjobbnak tűnő feature-t logisztikus regresszióhoz, és jelentsük a teszt ROC AUC-t. A kérdés az, hogy a kiválasztást hol végezzük el.
Előzetes becslés: a két valódi jel AUC-ja körülbelül 0.73 körül lehet, de ha a teljes adaton választunk, a zajfeature-ök véletlen asszociációi felfújják a teszt AUC-t, így 0.80 fölötti érték is megjelenhet. A pipeline-ba helyezett szelekció ennél alacsonyabb, de becsületes általánosítást mutat. A pontos érték seedfüggő, ezért a becslés csak nagyságrendi.
A filter (szűrő) a modelltanítás előtt,
feature-enként külön-külön pontoz. Az f_classif numerikus
feature és bináris cél esetén F-statisztikát számol, lényegében azt
vizsgálja, hogy a cél két csoportjában a feature átlaga mennyire tér el.
A SelectKBest(f_classif, k=10) a 10 legnagyobb pontszámút
tartja meg.
Előnye, hogy gyors és egyszerű; hátránya, hogy nem látja a feature-ök együttes hatását. Egy önmagában gyenge, de egy másikkal együtt erős jel kieshet. A filter pontszáma nem helyettesíti a modellbeli kapcsolatot, ahogyan az Eloszlás, kapcsolat és csoportkülönbség alfejezetben tárgyalt korreláció sem mér nemlineáris vagy feltételes összefüggést.
Az embedded (beágyazott) szelekció a modell tanítása közben történik. A logisztikus regresszió L1 büntetéssel:
ahol a cél, , a büntetés erőssége, pedig a -dimenziós együtthatóvektor. Az L1 tag a abszolútérték-összeg; a veszteség minimuma emiatt gyakran olyan pontba kerül, ahol sok pontosan nulla. Az L2 büntetés alakú, zsugorít, de ritkán nulláz.
Kis képi kontroll: ha a veszteségkontúr ovális, az L1 rombusz alakú
büntetési tartománya előbb érinti a kontúrt egy tengelyen, mint az L2
kör, ezért az L1 nullákat hoz. A
LogisticRegression(penalty="l1", solver="liblinear") ezt
valósítja meg; a C paraméter a büntetés inverze, kisebb
C erősebb büntetést és több nullát jelent. A
SelectFromModel a nemnulla együtthatókhoz tartozó
feature-öket adja vissza.
A permutation importance (permutációs fontosság) egy már betanított modellre és egy validációs mintára épül. Egy feature oszlopát véletlenszerűen összekeverjük, majd megnézzük, mennyit romlik a modell pontossága vagy AUC-ja. Ha a feature valódi jelet hordoz, a romlás nagy; ha zaj, a romlás kicsi vagy akár negatív is lehet. Ezt a mérőszámot a train-től független validációs részen számoljuk, nem a teszt halmazon, ha utána a teszt AUC-t is jelentjük, különben a teszt ismét befolyásolja a kiválasztást.
A sparse matrix (ritka mátrix) olyan mátrix,
amelyben a legtöbb elem nulla, és csak a nemnulla elemeket tároljuk. Az
egy-hot kódolás magas dimenziót hoz, de a mátrix ritka: minden sorban a
kategorikus változó oszlopai közül csak egy 1-es van. A
scipy.sparse többféle reprezentációt támogat: például a COO
forma (sor, oszlop, érték) hármasokat, a CSR forma pedig
sorindexelt tömör tárolást használ; a .shape a dimenziót,
az .nnz a nemnulla elemek számát adja.
Példa: 1200 sor és 100 kategória esetén a sűrű mátrix 120 000 számot
tárolna, a ritka változat nnz=1200. Az
OneHotEncoder alapértelmezésben
sparse_output=True ritka mátrixot ad, így a memóriaigény
nem robban fel magas kategorikus kardinalitásnál sem.
A dimensionality a feature-tér dimenziója, azaz . Ha közel van a mintamérethez -hez, a tér „üres”, és a modell könnyen talál véletlen mintázatot. A többszörös keresés ennek statisztikai oldala: ha független tesztet futtatunk szignifikanciaszinten, annak valószínűsége, hogy legalább egy hamis pozitív van:
Ha és , akkor ; szinte biztos, hogy lesz olyan zajfeature, amelyik szignifikánsnak tűnik. Ez a mechanizmus okozza a teljes adaton végzett szelekció optimizmusát.
Az alábbi kód a fenti szintetikus adatot állítja elő, majd összehasonlítja a hibás és a helyes utat. A kód nem futtatott; a kimenetre vonatkozó állítások szemléltető jellegűek.
import numpy as np
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
rng = np.random.default_rng(42)
n = 1200
p_noise = 118
# Két valós jel
X_signal = rng.normal(size=(n, 2))
logit = 0.7 * X_signal[:, 0] - 0.5 * X_signal[:, 1]
prob = 1 / (1 + np.exp(-logit))
y = rng.binomial(n=1, p=prob)
X = np.hstack([X_signal, rng.normal(size=(n, p_noise))])
names = [f"signal_{i}" for i in range(2)] + [f"noise_{i}" for i in range(p_noise)]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=42
)
# Hibás út: a teljes X, y alapján választunk
sel_all = SelectKBest(f_classif, k=10).fit(X, y)
X_tr_all = sel_all.transform(X_train)
X_te_all = sel_all.transform(X_test)
m_all = LogisticRegression(max_iter=1000)
m_all.fit(X_tr_all, y_train)
auc_all = roc_auc_score(y_test, m_all.predict_proba(X_te_all)[:, 1])
# Helyes út: minden a Pipeline-on belül, csak a train-en fit
pipe = Pipeline([
("select", SelectKBest(f_classif, k=10)),
("model", LogisticRegression(max_iter=1000)),
])
pipe.fit(X_train, y_train)
auc_pipe = roc_auc_score(y_test, pipe.predict_proba(X_test)[:, 1])
print("Teljes adaton választott teszt AUC:", round(auc_all, 3))
print("Pipeline teszt AUC:", round(auc_pipe, 3))
# Ellenőrzés: a pipeline kiválasztott feature-nevei
selected = np.array(names)[pipe.named_steps["select"].get_support()]
print("Pipeline-ban kiválasztott feature-ök:", list(selected[:5]), "...")A kimenet elméletileg várható jellege: a teljes adaton választott AUC magasabb, mert a teszt megfigyelései részt vettek a kiválasztásban, így a zajfeature-ök véletlen asszociációi a tesztben is érvényesülnek. A pipeline AUC-ja alacsonyabb, közel a valódi általánosításhoz. A pontos számok seed- és környezetfüggők, ezért csak a két érték viszonyát kezeljük tanulságként, nem a konkrét számokat.
A két út közötti különbség nem a modell jobb tudását mutatja, hanem a
szivárgást. A hibás úton a SelectKBest.fit(X, y) a teljes
adatot használja, tehát a tesztadat is beleszól abba, melyik 10 feature
marad. A kiválasztott zajfeature-ök a tesztbeli célvéletlennel
korrelálnak, ezért a teszt AUC felfelé torzít.
A pipeline-ban a SelectKBest.fit csak a train-en fut; a
tesztet a modell csak a predict_proba lépésben látja. A
train-en belül is bekerülhet néhány zajfeature, de ezek nem segítenek a
független teszten, mert a train-beli véletlen asszociáció nem ismétlődik
meg a tesztben. A pipeline AUC-ja így a kiválasztott konfiguráció valódi
általánosítását becsli.
Egy fontos árnyalat: a pipeline-eredmény a kiválasztott
k=10 konfigurációra érvényes, nem a teljes keresési
folyamatra. Ha a k értékét is a teszt alapján hangolnánk,
újra szivárgást okoznánk. A modellválasztási bizonytalanság kezelésére a
nested cross-validation a megfelelő eszköz, amit a Baseline és egyetlen teljes kísérlet alfejezetben
bemutatott kísérleti keret is megkövetel.
A teljes adaton elvégzett feature-szelekció a tesztinformációt használja fel; szelekció és hiperparaméter-hangolás a belső CV része. A véletlen adatokon kialakuló optimizmus mértéke nem előre rögzített AUC-küszöb.
Értelmezés. Tegyük fel, hogy 118 független zajfeature-t 5%-os szignifikanciaszinten tesztelünk. Számítsa ki, hány hamis pozitív várható, és mennyi a valószínűsége, hogy legalább egy hamis pozitív van. Miért okoz ez optimizmust, ha a kiválasztást a teljes adaton végezzük el?
Implementálás. Írjon
select_and_evaluate(X, y, k=10, test_size=0.25, seed=42)
függvényt, amely a fenti adaton belül train/test splitet készít, majd
egy Pipeline-t illeszt SelectKBest és
LogisticRegression elemekkel, és visszaadja a teszt AUC-t.
Futtassa 10 különböző seeddel, és jelentse az átlagot és szórást. A
szórást ne nevezze konfidenciaintervallumnak.
Hibakeresés. Egy kolléga a következő kódot írta:
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25)
selector = SelectKBest(f_classif, k=10).fit(X, y) # teljes X!
X_train_sel = selector.transform(X_train)
X_test_sel = selector.transform(X_test)
model = LogisticRegression(max_iter=1000).fit(X_train_sel, y_train)
print(roc_auc_score(y_test, model.predict_proba(X_test_sel)[:, 1]))Mi a hiba? Javítsa ki, és magyarázza meg, miért nem menti meg a
helyzetet, ha a selector.fit(X, y)-t a split után, de a
teljes X-re hívja.
mutual_info_classif-ra. A fenti 1200 soros mátrixon 5
különböző train-spliten válassza ki a 10 legjobb feature-t, és számítsa
ki a kiválasztott halmazok Jaccard-átfedését. Mit mond ez a kiválasztás
stabilitásáról? A Jaccard-átfedés két halmazra
.Várható hamis pozitív: . Legalább egy hamis pozitív valószínűsége: . A teljes adaton végzett szelekciónál a teszt is részt vesz a kiválasztásban, így a hamis pozitív asszociációk a tesztben is megjelennek, és a teszt AUC felfelé torzít.
Lehetséges megoldás:
def select_and_evaluate(X, y, k=10, test_size=0.25, seed=42):
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=test_size, random_state=seed
)
pipe = Pipeline([
("select", SelectKBest(f_classif, k=k)),
("model", LogisticRegression(max_iter=1000)),
])
pipe.fit(X_train, y_train)
return roc_auc_score(y_test, pipe.predict_proba(X_test)[:, 1])
scores = [select_and_evaluate(X, y, seed=s) for s in range(10)]
print(np.mean(scores), np.std(scores))Az átlag és szórás a seedek közötti bizonytalanságot mutatja; ez nem konfidenciaintervallum, mert nem veszi figyelembe a modellillesztés teljes varianciáját, csak egy szűk újramintázási forrást.
A selector.fit(X, y) a teljes adatra illeszti a
filtert, tehát a teszt megfigyelései befolyásolják a kiválasztást. A
javítás: selector.fit(X_train, y_train). Attól, hogy a
split előbb történt, a teszt még mindig a fit bemenetében
van, tehát a szivárgás megmarad. A helyes sorrend: a fit
kizárólag a train-en fut, a transform pedig a train-en és a
testen külön-külön, de a train-en tanult szelekcióval.
Lehetséges megoldás:
from sklearn.feature_selection import mutual_info_classif
def selected_names(X_train, y_train, k=10, seed=42):
rng = np.random.default_rng(seed)
idx = rng.choice(len(X_train), size=int(0.8 * len(X_train)), replace=False)
sel = SelectKBest(mutual_info_classif, k=k).fit(X_train[idx], y_train[idx])
return set(np.array(names)[sel.get_support()])
sets = [selected_names(X, y, seed=s) for s in range(5)]
overlaps = []
for i in range(5):
for j in range(i + 1, 5):
a, b = sets[i], sets[j]
overlaps.append(len(a & b) / len(a | b))
print(np.mean(overlaps))A Jaccard-átfedés alacsony lehet, mert a zajfeature-ök cserélődnek a split-ek között; a valódi jelek általában bekerülnek, de a zajok nem stabilak. Ez azt mutatja, hogy a kiválasztott halmaz bizonytalan, ezért a teszt AUC-t a kiválasztási folyamat figyelembevételével kellene értelmezni, például nested CV-vel.
A leggyakoribb érvelés: „A teszt AUC érintetlen, mert a modell illesztése csak a train-en történt; a feature-kiválasztás a teszt előtt volt.” Ez hamis. A kiválasztás maga is modellezési döntés, és ha a tesztet használja, a teszt már nem független bizonyíték. A fenti szimuláció az ellenpélda: a teljes adaton kiválasztott zajfeature-ök a teszt AUC-t felfelé tolják, pedig valódi jelük nincs.
Ez visszautal a Baseline és egyetlen teljes
kísérlet alfejezetben tanult alapelvre: a preprocessing, a baseline
és a scoring mind a train-en belül kell, hogy maradjon. A
feature-szelekció ugyanolyan preprocessing-lépés, mint a skálázás vagy a
kategóriakódolás, ezért a Pipeline-ben a helye.
SelectKBest.fit-et hívni a
pipeline-ban?nnz értéke 1200 sor és 100 kategória
one-hot kódolása esetén?A filter gyors előszűrő, az embedded módszer a modell tanítása közben
szelektál, az L1 büntetés pontosan nulláz, a permutációs fontosság pedig
a modellre jellemző fontosságot mér validációs adaton. Magas dimenzióban
a többszörös keresés miatt szinte biztos a hamis pozitív, ezért a teljes
adaton végzett szelekció optimista teszt AUC-t ad. A
Pipeline biztosítja, hogy a kiválasztás csak a train-en
történjen, így a teszt valóban független marad. A kiválasztott halmaz
stabilitását külön kell vizsgálni, mert a zajfeature-ök splitenként
cserélődhetnek. A következő alkalmazás, a Feature
store és adatkontraktus szükségessége, ezt a fegyelmet feature store
és adatkontraktus formájában intézményesíti.