Pojačavanje (eng. boosting)¶

Osnovna ideja boosting algoritama jeste da se ansambl gradi postepeno, dodavanjem modela jedan po jedan, pri čemu se svaki novi model obučava tako da nadomesti slabosti trenutnog ansambla. Na taj način se sukcesivno poboljšava ukupna preciznost predviđanja.

Ako sa $F_{m-1}$ označimo ansambl izgrađen nakon $(m-1)$ koraka, tada se u $m$-tom koraku konstruše novi model koji treba da popravi greške postojećeg ansambla, nakon čega se formira novi ansambl $F_m$. Broj modela u ansamblu predstavlja važan hiperparametar, jer utiče i na preciznost modela i na rizik od preprilagođavanja.

AdaBoost¶

AdaBoost pravi ansambl slabih modela. Svaki sledeći model obraća više pažnje na podatke koje su prethodni modeli loše predvideli.

Kod klasifikacije, ideja se najlakše vidi preko težina opservacija. Klase označimo sa $y_i \in \{-1,1\}$. Na početku sve opservacije imaju istu težinu:

$$ w_i = \frac{1}{n}, \qquad i = 1,\ldots,n. $$

Nakon što istreniramo jedan slab model, računamo njegovu ponderisanu grešku:

$$ \varepsilon_m = \sum_{i=1}^n w_i I(y_i \neq f_m(x_i)). $$

Ovde je $f_m$ trenutni slabi model.

Ako je model dobar, njegova greška je mala i on dobija veću težinu u konačnom ansamblu:

$$ \alpha_m = \frac{1}{2}\log\left(\frac{1-\varepsilon_m}{\varepsilon_m}\right). $$

Nakon toga ažuriramo težine opservacija na sledeći način:

$$ w_i^{(m+1)} = \frac{ w_i^{(m)} e^{-\alpha_m y_i f_m(x_i)} }{ Z_m }. $$

Ovde je $Z_m$ normalizaciona konstanta, koja služi da zbir svih težina ostane jednak 1.

U ovoj formuli proizvod $y_i f_m(x_i)$ govori da li je model pogodio ili pogrešio:

  • ako je model pogodio, težina se smanjuje;
  • ako je model pogrešio, težina se povećava.

Zato naredni model više pažnje posvećuje primerima koje je prethodni model loše klasifikovao.

Konačna odluka dobija se kao ponderisano glasanje slabih modela:

$$ F_M(x) = \operatorname{sign}\left(\sum_{m=1}^M \alpha_m f_m(x)\right). $$

XGBoost¶

XGBoost takođe pravi ansambl dodavanjem jednog po jednog stabla, ali to radi na optimizacioni način.

Model posle M koraka možemo zapisati kao:

$$ F_M(x) = f_1(x) + f_2(x) + \cdots + f_M(x). $$

Svako novo stablo pokušava da popravi greške prethodnog ansambla.

Za razliku od AdaBoost-a, XGBoost eksplicitno minimizuje funkciju cilja:

$$ \sum_{i=1}^n l(y_i, \hat y_i) + \sum_{m=1}^M \Omega(f_m). $$

Prvi deo meri grešku modela, dok drugi deo kažnjava previše složena stabla.

U svakom koraku XGBoost posmatra kako se funkcija greške menja u odnosu na trenutnu predikciju. Zato za svaku opservaciju računa gradijent:

$$ g_i = \frac{\partial l(y_i, \hat y_i)}{\partial \hat y_i}, $$

i Hesijan, odnosno drugi izvod:

$$ h_i = \frac{\partial^2 l(y_i, \hat y_i)}{\partial \hat y_i^2}. $$

Zatim XGBoost pravi novo stablo koje pokušava da grupiše opservacije tako da ukupna korekcija predikcije bude što korisnija. Drugim rečima, novo stablo se ne trenira direktno da predvidi $y_i$, već da popravi trenutne predikcije modela.

U svakom listu stabla XGBoost računa vrednost korekcije koristeći gradijente i hesijane opservacija koje su upale u taj list. Ako je $I_j$ skup opservacija u j-tom listu, tada je vrednost lista približno:

$$ w_j = - \frac{ \sum_{i \in I_j} g_i }{ \sum_{i \in I_j} h_i + \lambda }. $$

Dakle:

  • gradijenti govore u kom smeru treba pomeriti predikcije;
  • hesijani i regularizacija kontrolišu koliko veliko to pomeranje treba da bude.

Novo stablo se zatim dodaje prethodnom modelu. U praksi se često koristi parametar learning_rate, koji se obično označava sa $\eta$:

$$ F_m(x) = F_{m-1}(x) + \eta f_m(x). $$

Ako je $\eta$ manje, svako novo stablo ima manji uticaj. Model tada uči sporije, ali često stabilnije.

Primer regresije¶

U nastavku ćemo se upoznati sa modelima XGBoost i AdaBoost, koje ćemo primeniti na zadatak predviđanja cena nekretnina korišćenjem skupa podataka ParisHousing.

Za rad sa XGBoost modelom potrebno je instalirati biblioteku xgboost. Sa druge strane, implementacija AdaBoost algoritma dostupna je u modulu sklearn.ensemble.

Za oba modela koristićemo:

  • isti trening skup,
  • isti validacioni skup,
  • isti test skup.

Najpre ćemo za svaki model isprobati nekoliko kombinacija hiperparametara i izabrati onu koja daje najbolje rezultate na validacionom skupu.
Zatim ćemo, sa izabranim hiperparametrima, ponovo obučiti model nad objedinjenim trening i validacionim skupom, a konačno poređenje izvršiti na test skupu.

Na taj način poređenje je fer, jer oba modela raspolažu istom količinom informacija i ocenjuju se pod istim uslovima.

In [ ]:
# !pip install xgboost

import warnings
warnings.filterwarnings("ignore")

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.model_selection import train_test_split, ParameterGrid
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
from sklearn.ensemble import AdaBoostRegressor
from sklearn.tree import DecisionTreeRegressor
from xgboost import XGBRegressor

df = pd.read_csv("ParisHousing.csv")
y = df["price"]
X = df.drop(columns="price")

# one-hot encoding
X = pd.get_dummies(X, drop_first=True)

print("\nBroj atributa nakon obrade:", X.shape[1])

# Prvo odvajamo test skup koji ostaje netaknut do samog kraja
# Zatim preostali deo delimo na trening i validacioni skup
X_trainval, X_test, y_trainval, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42)

X_train, X_val, y_train, y_val = train_test_split(
    X_trainval, y_trainval, test_size=0.25, random_state=42)

print("Dimenzije trening skupa:", X_train.shape, y_train.shape)
print("Dimenzije validacionog skupa:", X_val.shape, y_val.shape)
print("Dimenzije test skupa:", X_test.shape, y_test.shape)
Broj atributa nakon obrade: 17
Dimenzije trening skupa: (6000, 17) (6000,)
Dimenzije validacionog skupa: (2000, 17) (2000,)
Dimenzije test skupa: (2000, 17) (2000,)
In [ ]:
# Funkcija za izbor hiperparametara pomoću validacionog skupa.
# Za svaku kombinaciju hiperparametara:
# 1. trenira model na trening skupu,
# 2. računa MAE na validacionom skupu,
# 3. pamti kombinaciju sa najmanjim MAE.

def tune_model(model_class, param_grid, X_train, y_train, X_val, y_val, model_name):
    best_model = None
    best_params = None
    best_val_mae = np.inf
    all_params = []
    all_mae = []

    for params in ParameterGrid(param_grid):
        model = model_class(**params)
        model.fit(X_train, y_train)

        val_pred = model.predict(X_val)
        val_mae = mean_absolute_error(y_val, val_pred)

        all_params.append(params)
        all_mae.append(val_mae)

        if val_mae < best_val_mae:
            best_val_mae = val_mae
            best_model = model
            best_params = params


    print(f"\nNajbolji parametri za {model_name}:")
    print(best_params)
    print(f"Najbolji validation MAE: {best_val_mae:.6f}")

    return best_model, best_params
In [ ]:
ada_grid = {
    "estimator": [
        DecisionTreeRegressor(max_depth=2, random_state=42),
        DecisionTreeRegressor(max_depth=3, random_state=42),
        DecisionTreeRegressor(max_depth=4, random_state=42)
    ],
    "n_estimators": [100, 200, 300],
    "learning_rate": [0.03, 0.05, 0.1],
    "loss": ["linear"],
    "random_state": [42]
}

best_ada_model, best_ada_params = tune_model(
    AdaBoostRegressor, ada_grid, X_train, y_train, X_val, y_val, "AdaBoostRegressor")

print(best_ada_params)
Najbolji parametri za AdaBoostRegressor:
{'estimator': DecisionTreeRegressor(max_depth=4, random_state=42), 'learning_rate': 0.1, 'loss': 'linear', 'n_estimators': 100, 'random_state': 42}
Najbolji validation MAE: 115520.586877
{'estimator': DecisionTreeRegressor(max_depth=4, random_state=42), 'learning_rate': 0.1, 'loss': 'linear', 'n_estimators': 100, 'random_state': 42}
In [ ]:
# Definisemo mrezu hiperparametara za XGBoost
xgb_grid = {
    "n_estimators": [200, 400],
    "learning_rate": [0.03, 0.05, 0.1],
    "max_depth": [3, 5, 7],
    "subsample": [0.8],
    "colsample_bytree": [0.8],
    "objective": ["reg:squarederror"],
    "random_state": [42],
    "n_jobs": [-1]}

best_xgb_model, best_xgb_params = tune_model(
    XGBRegressor, xgb_grid, X_train, y_train, X_val, y_val, "XGBRegressor")

print(best_xgb_params)
Najbolji parametri za XGBRegressor:
{'colsample_bytree': 0.8, 'learning_rate': 0.05, 'max_depth': 3, 'n_estimators': 400, 'n_jobs': -1, 'objective': 'reg:squarederror', 'random_state': 42, 'subsample': 0.8}
Najbolji validation MAE: 13943.648426
{'colsample_bytree': 0.8, 'learning_rate': 0.05, 'max_depth': 3, 'n_estimators': 400, 'n_jobs': -1, 'objective': 'reg:squarederror', 'random_state': 42, 'subsample': 0.8}
In [ ]:
# Kada smo izabrali najbolje hiperparametre na validacionom skupu,
# sada spajamo trening i validacioni skup i ponovo treniramo oba modela.
# Tek nakon toga modele konacno ocenjujemo na test skupu.
X_final_train = pd.concat([X_train, X_val], axis=0)
y_final_train = pd.concat([y_train, y_val], axis=0)

best_ada_final = AdaBoostRegressor(
    n_estimators=best_ada_params["n_estimators"],
    learning_rate=best_ada_params["learning_rate"],
    loss=best_ada_params["loss"],
    random_state=best_ada_params["random_state"])
best_ada_final.fit(X_final_train, y_final_train)
ada_test_pred = best_ada_final.predict(X_test)

best_xgb_final = XGBRegressor(
    n_estimators=best_xgb_params["n_estimators"],
    max_depth=best_xgb_params["max_depth"],
    learning_rate=best_xgb_params["learning_rate"],
    subsample=best_xgb_params["subsample"],
    colsample_bytree=best_xgb_params["colsample_bytree"],
    random_state=best_xgb_params["random_state"],
    objective=best_xgb_params["objective"])
best_xgb_final.fit(X_final_train, y_final_train)
xgb_test_pred = best_xgb_final.predict(X_test)

ada_test_mae = mean_absolute_error(y_test, ada_test_pred)
xgb_test_mae = mean_absolute_error(y_test, xgb_test_pred)

print(ada_test_mae)
print(xgb_test_mae)
309155.3519539735
14355.056876171877

Dakle XGBoost daje bolje rezultate.

In [ ]:
# Racunamo feature importance za oba finalna modela.
ada_importance = pd.Series(best_ada_final.feature_importances_,
    index=X.columns).sort_values(ascending=False)

xgb_importance = pd.Series(best_xgb_final.feature_importances_,
    index=X.columns).sort_values(ascending=False)

plt.figure(figsize=(10, 6))
ada_importance.head(15).sort_values().plot(kind="barh")
plt.title("AdaBoostRegressor - najvazniji prediktori")
plt.xlabel("Feature importance")
plt.tight_layout()
plt.show()


plt.figure(figsize=(10, 6))
xgb_importance.head(15).sort_values().plot(kind="barh")
plt.title("XGBRegressor - najvazniji prediktori")
plt.xlabel("Feature importance")
plt.tight_layout()
plt.show()