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 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:
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 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:
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.
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:
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.
# !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,)
# 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
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}
# 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}
# 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.
# 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()