Preprilagođavanje modela (eng. Overfitting)¶

Kada model daje odlične rezultate na trening skupu, a na test skupu daje znatno lošije rezultate, kažemo da je došlo do preprilagođavanja. Ovo se može ilustrovati na primeru učenika koji određenu lekciju nauči napamet. Dakle, on tu lekciju zna potpuno precizno (trening skup), ali nije naučio i razumeo principe koji se odnose na tu oblast, pa će njegovo znanje na bilo kom sličnom gradivu (test skup) biti jako loše.

Do ove pojave može doći zbog neadekvatnog izbora modela, prevelikog broja parametara, nereprezentativnog skupa za obučavanje. Postavlja se pitanje zašto, na primer, u regresiji ne koristimo Lagranžov interpolacioni polinom, kada njime možemo dobiti tačnu predikciju svake tačke iz trening skupa. Razlog je to što savršeno prilagođavanje trening podacima ne garantuje dobre rezultate na novim podacima. Štaviše, često dovodi do velikih oscilacija u predikcijama za nove instance. Ovo je klasičan primer preprilagođavanja. Zbog toga je često bolje dozvoliti modelu da pravi male greške na trening skupu, kako bi bio stabilniji i uspešniji na novim podacima.

Ako je preciznost modela mala i na trening i na test skupu, tada govorimo o potprilagođavanju (eng. underfitting).

Da bismo ublažili problem preprilagođavanja, uvodimo validacioni skup, koji koristimo za izbor optimalnih vrednosti hiperparametara modela. Trening skup koristimo za učenje parametara modela, dok validacioni skup koristimo za podešavanje hiperparametara. Dodatni način borbe protiv preprilagođavanja jeste regularizacija.

slika1.png

Regularizacija¶

Kod parametarskih modela, kao što su linearna i logistička regresija, ocene parametara dobijamo traženjem ekstremuma odgovarajućih funkcija. U linearnoj regresiji tražimo minimum zbira kvadrata reziduala, dok u logističkoj regresiji tražimo maksimum funkcije verodostojnosti, odnosno češće maksimum njenog logaritma. Pošto je većina optimizacionih algoritama formulisana kao problem minimizacije, ocene parametara logističke regresije dobijaju se minimizovanjem negativnog logaritma funkcije verodostojnosti.

Uz oznake sa prethodnog časa, možemo reći da se ocene parametara nekog od ovih modela dobijaju rešavanjem problema:

$$ (\beta_0, \beta) = \arg\min_{(\beta_0,\beta)} \sum_{i=1}^{n} L(y_i,\beta_0+\beta^T x_i) $$

gde je β = (β₁,…,βₚ)ᵀ, a xᵢ = (xᵢ₁,…,xᵢₚ)ᵀ. Funkcija gubitka L zavisi od modela koji posmatramo.

U slučaju linearne regresije važi:

$$ L(y_i,\beta_0+\beta^T x_i) = (y_i-\beta_0-\beta^T x_i)^2 $$

dok u slučaju logističke regresije važi:

$$ L(y_i,\beta_0+\beta^T x_i) = -\left(y_i(\beta_0+\beta^T x_i) - \ln(1+e^{\beta_0+\beta^T x_i})\right) $$

Od preprilagođavanja se možemo delimično zaštititi uvođenjem nenegativnog hiperparametra λ i minimizovanjem funkcije oblika:

$$ \sum_{i=1}^{n} L(y_i,\beta_0+\beta^T x_i) + \lambda \|\beta\| $$

gde je norma koja se primenjuje na β najčešće l₁ ili l₂ norma. Uloga hiperparametra λ jeste da ograniči veličinu koeficijenata modela, odnosno da ne dozvoli da pojedini prediktori dobiju preveliku težinu. Na taj način model postaje stabilniji i manje sklon preprilagođavanju trening podacima.

Postavlja se pitanje kako izabrati λ tako da ne bude ni premalo ni preveliko. Ako je λ suviše malo, regularizacioni član postaje zanemarljiv u odnosu na osnovnu funkciju gubitka, pa model može biti sklon preprilagođavanju. Ako je, sa druge strane, λ suviše veliko, regularizacioni član dominira i model može postati previše jednostavan, što vodi potprilagođavanju.

Dakle, λ je hiperparametar modela i njegova vrednost se bira pomoću validacionog skupa. U praksi se uzima više kandidata za λ, za svaku od tih vrednosti trenira se model na trening skupu, a zatim se meri uspešnost na validacionom skupu. Kao optimalna vrednost bira se ona za koju model postiže najbolje rezultate na validacionom skupu.

Postavlja se i pitanje koju normu koristiti. Poznat teorijski rezultat kaže da se pri korišćenju l₁ norme može dogoditi da neki parametri modela budu jednaki nuli. Zato l₁ regularizacija, poznatija kao LASSO regularizacija, može služiti i za selekciju prediktora. Pri korišćenju l₂ norme, poznatije kao RIDGE regularizacija, koeficijenti se smanjuju, ali u opštem slučaju ne postaju jednaki nuli. Iako l₁ regularizacija ima ovu prednost, u nekim situacijama l₂ regularizacija daje bolje rezultate.

Važno je istaći da regularizacija nije uvek neophodna. Kada raspolažemo velikom i reprezentativnom bazom podataka, opasnost od preprilagođavanja je često manja, pa preterana regularizacija može dovesti do nepotrebnog smanjenja preciznosti modela.

Evaluacija modela¶

Pomenuli smo da je nakon izgradnje modela potrebno proveriti njegov kvalitet. Kako to možemo učiniti? Pozabavimo se najpre problemom klasifikacije. Često se dešava da za modele koji imaju visok procenat tačnih predviđanja pomislimo da su zaista dobri. Međutim, takva mera kvaliteta nije uvek dovoljno informativna.

Kada se jedna kategorija zavisne promenljive u skupu za obučavanje pojavljuje mnogo češće od ostalih, mnogi modeli teže tome da veliki broj instanci svrstavaju upravo u tu dominantnu kategoriju. U takvoj situaciji model nije nužno naučio da dobro razlikuje kategorije, već samo koristi činjenicu da je jedna od njih znatno zastupljenija od ostalih. Ako je ta ista kategorija dominantna i u test skupu, model može imati visok procenat tačnih predviđanja, iako ostale kategorije klasifikuje veoma loše. Drugim rečima, sam procenat tačnih predviđanja nekada može prikriti činjenicu da model uopšte nije dovoljno kvalitetan.

Opišimo sada jedan očigledan primer u kome je posebno važno koju meru kvaliteta koristimo. Pretpostavimo da klasifikujemo tumor u dve grupe: maligni i benigni. Da li je ista greška ako za benigni tumor kažemo da je maligni i ako za maligni tumor kažemo da je benigni? Greška u drugom slučaju može imati mnogo ozbiljnije posledice. Zbog toga postoji više različitih mera kvaliteta klasifikatora, a gotovo sve se mogu dobiti na osnovu matrice konfuzije.

slika2.png

Najčešće korišćene mere kvaliteta su:
accuracy = (TP + TN) / (TP + TN + FP + FN)
precision = TP / (TP + FP)
tpr (true positive rate) = TP / (TP + FN)
tnr (true negative rate) = TN / (TN + FP)

Svaka od ovih mera može biti pod uticajem nebalansiranosti kategorija. Zato se često uvodi i mera koja se zove F1 score i koja je jednaka harmonijskoj sredini precision i tpr, odnosno
F1 score = 2 · precision · tpr / (precision + tpr)

Još jedna značajna mera, koja je često pogodna i u slučaju nebalansiranih kategorija, jeste površina ispod ROC krive.

ROC kriva je skup tačaka oblika (1 - tnr(r), tpr(r)) za različite vrednosti praga r iz intervala [0,1]. Ta kriva počinje u tački (0,0), a završava se u tački (1,1). Kako se menja vrednost praga, posmatramo da li se ROC kriva izdiže ka gornjem levom uglu, jer to ukazuje na to da su i tpr(r) i tnr(r) bliski vrednosti 1.

slika4.png

Dakle, što je ROC kriva izraženije povijena ka gornjem levom uglu, to je klasifikator bolji u smislu razlikovanja obe kategorije. Zbog toga se često posmatra veličina AUC (area under the curve), koja predstavlja površinu ispod ROC krive. Što je AUC bliže jedinici, to je klasifikacija uspešnija. Ako je AUC blizu 0.5, to je znak da model veoma slabo razlikuje kategorije.

Postoji i veoma lepa interpretacija ove veličine: AUC je jednak verovatnoći da slučajno odabrani element iz pozitivne kategorije dobije veći skor od slučajno odabranog elementa iz negativne kategorije. Dakle, savršeni klasifikator dodeljuje veće skorove svim pozitivnim elementima nego negativnim, pa će njegov AUC biti jednak 1.

Metod k najbližih suseda (KNN)¶

KNN (k-nearest neighbors) je jedan od najjednostavnijih algoritama mašinskog učenja. Ideja algoritma je da se nova instanca klasifikuje na osnovu svojih najbližih suseda iz trening skupa.

Za datu instancu pronalazimo k najbližih instanci (u smislu izabrane metrike, najčešće euklidske), a zatim joj dodeljujemo klasu koja je najzastupljenija među tim susedima.

Važna karakteristika ovog algoritma jeste da ne vrši eksplicitno treniranje modela, već čuva ceo trening skup i odluku donosi tek prilikom predikcije.

Pošto se KNN oslanja na rastojanja, veoma je važno da atributi budu na istoj skali. Zbog toga je standardizacija podataka ključan korak pre primene ovog algoritma.

In [ ]:
import pandas as pd
import numpy as np

from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import (
    accuracy_score, precision_score, recall_score,
    f1_score, roc_auc_score)

df = pd.read_csv("CustomerChurn.csv")

df = df.drop(columns=["customerID"])

# TotalCharges je ucitan kao kategoricki atribut iako sadrzi brojeve,
# pa ga prevodimo u numericki tip. Vrednosti koje ne mogu da se prevedu
# postaju NaN, a zatim takve redove uklanjamo.
df["TotalCharges"] = pd.to_numeric(df["TotalCharges"], errors="coerce")
df = df.dropna().copy()

# Ciljna promenljiva
y = df["Churn"].map({"Yes": 1, "No": 0})

# Prediktori
X = df.drop(columns=["Churn"])

# Razdvajamo kategoricke i numericke kolone
cat_cols = X.select_dtypes(include=["object"]).columns.tolist()
num_cols = X.select_dtypes(include=[np.number]).columns.tolist()

# One-hot encoding kategorickih atributa
X = pd.get_dummies(X, columns=cat_cols, drop_first=True)

# Podela na trening i test skup
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# Standardizacija numerickih atributa
scaler = StandardScaler()
X_train.loc[:, num_cols] = scaler.fit_transform(X_train[num_cols])
X_test.loc[:, num_cols] = scaler.transform(X_test[num_cols])

# KNN model
knn = KNeighborsClassifier()

# Mreza hiperparametara koje isprobavamo
param_grid = {
    "n_neighbors": [3, 5, 7, 9, 11, 15, 21],
    "weights": ["uniform", "distance"],
    "metric": ["euclidean", "manhattan", "minkowski"]
}

# GridSearchCV vrši sistematsku pretragu kombinacija hiperparametara
# i koristi unakrsnu validaciju (cross-validation) za evaluaciju modela

grid = GridSearchCV(
    estimator=knn,          # model koji treniramo (ovde KNN klasifikator)

    param_grid=param_grid,  # skup hiperparametara i njihovih vrednosti
                            # koje želimo da isprobamo (n_neighbors, metric, weights)

    cv=5,                   # broj foldova za unakrsnu validaciju
                            # podaci se dele na 5 delova:
                            # 4 za trening, 1 za validaciju (rotira se)
                            # dakle, validacioni skup se formira od trening skupa
                            # u okviru funkcije GridSearchCV

    scoring="roc_auc",      # metrika po kojoj biramo najbolji model
                            # ovde koristimo ROC AUC (dobro za nebalansirane klase)

    n_jobs=-1               # koristi sva dostupna CPU jezgra
                            # ubrzava pretragu hiperparametara
)

grid.fit(X_train, y_train)

# Najbolji model
best_model = grid.best_estimator_

print("Najbolji parametri:")
print(grid.best_params_)
print("Najbolji AUC:", grid.best_score_)
Najbolji parametri:
{'metric': 'manhattan', 'n_neighbors': 21, 'weights': 'uniform'}
Najbolji AUC: 0.8259990930219375
In [ ]:
# Predikcije na test skupu
y_pred = best_model.predict(X_test)
y_prob = best_model.predict_proba(X_test)[:, 1]

# Metrike
accuracy = accuracy_score(y_test, y_pred)
precision = precision_score(y_test, y_pred)
tpr = recall_score(y_test, y_pred)                                     
f1 = f1_score(y_test, y_pred)
auc = roc_auc_score(y_test, y_prob)

print("\nRezultati na test skupu:")
print(f"Accuracy:  {accuracy:.4f}")
print(f"Precision: {precision:.4f}")
print(f"TPR:       {tpr:.4f}")
print(f"F1:        {f1:.4f}")
print(f"ROC AUC:   {auc:.4f}")
Rezultati na test skupu:
Accuracy:  0.7882
Precision: 0.6044
TPR:       0.5882
F1:        0.5962
ROC AUC:   0.8218

Stablo odlučivanja (Decision Tree)¶

Stablo odlučivanja je model koji odluku donosi kroz niz uzastopnih podela skupa podataka. U svakom unutrašnjem čvoru stabla proverava se neki uslov nad jednim atributom, a zatim se, u zavisnosti od ishoda tog uslova, prelazi u odgovarajuću granu. Na kraju se dolazi do lista stabla, koji predstavlja konačnu odluku, odnosno predikciju. U slučaju klasifikacije, list stabla određuje predikovanu klasu, dok u slučaju regresije daje numeričku vrednost. Kod klasifikacionih stabala cilj je da se što bolje razdvoje instance različitih klasa. Kod regresionih stabala cilj nije razdvajanje klasa, već formiranje grupa instanci koje imaju što sličnije vrednosti ciljne numeričke promenljive. U nastavku se fokusiramo na klasifikaciono stablo.

Jedna od najčešće korišćenih mera kvaliteta podele jeste entropija. Ako u nekom čvoru imamo klase sa verovatnoćama p1, p2, ..., pk, entropija se definiše formulom

$$ H = -\sum_{i=1}^{k} p_i \log_2 p_i. $$

Entropija je mala kada u čvoru preovlađuje jedna klasa, a velika kada su klase ravnomernije zastupljene. Zato prilikom izgradnje stabla težimo podelama koje što više smanjuju entropiju.

Druga često korišćena mera kvaliteta podele jeste Gini indeks, definisan formulom

$$ G = 1 - \sum_{i=1}^{k} p_i^2. $$

I Gini indeks meri koliko su klase izmešane u čvoru. Ako su sve instance u čvoru iz iste klase, tada je Gini indeks jednak 0. Što su klase ravnomernije raspoređene, Gini indeks je veći. U praksi se i entropija i Gini indeks koriste za izbor dobre podele, a razlike u rezultatima često nisu velike.

Za razliku od KNN algoritma, decision tree ne zahteva standardizaciju podataka, jer se ne oslanja na rastojanja između instanci. Jedna od glavnih prednosti ovog modela jeste interpretabilnost, pošto se odluke mogu lako pratiti kroz strukturu stabla. Sa druge strane, stablo može lako da se preprilagodi podacima, pa se u praksi često ograničavaju njegova dubina, minimalan broj instanci u listu ili neki drugi hiperparametri.

In [ ]:
import pandas as pd
import numpy as np

from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import (
    accuracy_score, precision_score,
    recall_score, f1_score, roc_auc_score)

df = pd.read_csv("CustomerChurn.csv")

df = df.drop(columns=["customerID"])

df["TotalCharges"] = pd.to_numeric(df["TotalCharges"], errors="coerce")
df = df.dropna().copy()

# Ciljna promenljiva
y = df["Churn"].map({"Yes": 1, "No": 0})

# Prediktori
X = df.drop(columns=["Churn"])

# Kategoricke kolone kodiramo pomocu one-hot encoding-a
cat_cols = X.select_dtypes(include=["object"]).columns.tolist()
X = pd.get_dummies(X, columns=cat_cols, drop_first=True)

# Podela na trening i test skup
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y)

# Osnovni model
tree = DecisionTreeClassifier(random_state=42)

# Definisemo mrezu hiperparametara koje zelimo da isprobamo.
# max_depth odredjuje maksimalnu dubinu stabla.

# min_samples_split predstavlja minimalan broj uzoraka koje cvor mora imati
# da bi uopste mogao da se deli na potomke.

# min_samples_leaf predstavlja minimalan broj uzoraka koji mora ostati u svakom listu.

# criterion odredjuje meru na osnovu koje biramo najbolju podelu u stablu.
param_grid = {
    "max_depth": [3, 5, 7, 10, None],
    "min_samples_split": [2, 5, 10, 20],
    "min_samples_leaf": [1, 2, 5, 10],
    "criterion": ["gini", "entropy"]
}

# Pretraga hiperparametara uz 5-fold cross-validation
grid = GridSearchCV(
    estimator=tree, param_grid=param_grid, cv=5,                   
    scoring="roc_auc", n_jobs=-1)

grid.fit(X_train, y_train)

# Najbolji model
best_model = grid.best_estimator_

print("Najbolji parametri:")
print(grid.best_params_)
print("Najbolji CV ROC AUC:", grid.best_score_)
Najbolji parametri:
{'criterion': 'entropy', 'max_depth': 5, 'min_samples_leaf': 10, 'min_samples_split': 2}
Najbolji CV ROC AUC: 0.8279705556050434
In [ ]:
# Predikcije na test skupu
y_pred = best_model.predict(X_test)
y_prob = best_model.predict_proba(X_test)[:, 1]

accuracy = accuracy_score(y_test, y_pred)
precision = precision_score(y_test, y_pred)
tpr = recall_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
auc = roc_auc_score(y_test, y_prob)

print("\nRezultati na test skupu:")
print(f"Accuracy:  {accuracy:.4f}")
print(f"Precision: {precision:.4f}")
print(f"TPR:       {tpr:.4f}")
print(f"F1:        {f1:.4f}")
print(f"ROC AUC:   {auc:.4f}")
Rezultati na test skupu:
Accuracy:  0.7754
Precision: 0.5980
TPR:       0.4733
F1:        0.5284
ROC AUC:   0.8159