25.3 Feature-szelekció és magas dimenzió

25.3.1 Helyzet és előzetes becslés

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.

25.3.2 Fogalmi magyarázat

25.3.2.1 Filter: egyváltozós előszűrés

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.

25.3.2.2 Embedded és regularization

Az embedded (beágyazott) szelekció a modell tanítása közben történik. A logisztikus regresszió L1 büntetéssel:

J(β)=1n∑i=1n[−yilnp̂i−(1−yi)ln(1−p̂i)]+λ∑j=1p|βj|, J(\beta) = \frac{1}{n}\sum_{i=1}^{n} \left[-y_i \ln \hat p_i - (1-y_i)\ln(1-\hat p_i)\right] + \lambda \sum_{j=1}^{p} |\beta_j|,

ahol yiy_i a cél, p̂i=σ(xi⊤β)\hat p_i = \sigma(x_i^\top \beta), λ\lambda a büntetés erőssége, β\beta pedig a pp-dimenziós együtthatóvektor. Az L1 tag a ∑|βj|\sum |\beta_j| abszolútérték-összeg; a veszteség minimuma emiatt gyakran olyan pontba kerül, ahol sok βj\beta_j pontosan nulla. Az L2 büntetés λ∑j=1pβj2\lambda \sum_{j=1}^p \beta_j^2 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.

25.3.2.3 Permutation importance

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.

25.3.2.4 Sparse matrix

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.

25.3.2.5 Dimensionality és többszörös keresés

A dimensionality a feature-tér dimenziója, azaz pp. Ha pp közel van a mintamérethez nn-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 pp független tesztet futtatunk α\alpha szignifikanciaszinten, annak valószínűsége, hogy legalább egy hamis pozitív van:

P(legalább egy hamis pozitív)=1−(1−α)p. P(\text{legalább egy hamis pozitív}) = 1 - (1-\alpha)^p.

Ha α=0.05\alpha=0.05 és p=118p=118, akkor 1−0.95118≈0.99761 - 0.95^{118} \approx 0.9976; szinte biztos, hogy lesz olyan zajfeature, amelyik szignifikánsnak tűnik. Ez a mechanizmus okozza a teljes adaton végzett szelekció optimizmusát.

25.3.3 Teljes minimális példa: zajfeature-ök és a két ú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.

25.3.4 Eredmény értelmezése

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.

25.3.5 Feladatok

  1. É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?

  2. 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.

  3. 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.

  1. Megváltozott feltétel. Cserélje ki a filtert 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 J(A,B)=|A∩B|/|A∪B|J(A,B)=|A\cap B|/|A\cup B|.

25.3.6 Megoldások és indoklás

  1. Várható hamis pozitív: 118⋅0.05=5.9118 \cdot 0.05 = 5.9. Legalább egy hamis pozitív valószínűsége: 1−0.95118≈0.99761 - 0.95^{118} \approx 0.9976. 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.

  2. 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.

  1. 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.

  2. 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.

25.3.7 Gyakori tévedés ellenpéldával

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.

25.3.8 Önellenőrzés

25.3.9 Rövid összefoglaló

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.