pejmantheory's picture
Publish the Covertype XGBoost classifier
426b48a verified
Raw History Blame Contribute Delete
6.32 kB
"""Train and score the Bleu.js forest-cover XGBoost classifier.
Protocol, fixed before the test set is scored:
- Data: sklearn.datasets.fetch_covtype (UCI Covertype, full table)
- Labels shifted from 1..7 to 0..6
- Outer split: stratified 80/20, random_state=42. The 20% is scored once.
- Inner split: stratified 90/10 of the training rows, random_state=42,
used only to pick n_estimators with early stopping.
- Final fit: all training rows, n_estimators = best_iteration + 1,
sample_weight = balanced class weights from the training rows.
- Baselines, same test rows, no retuning: majority class; a depth-3
XGBoost with 50 trees and no class weights.
"""
import json
from pathlib import Path
import numpy as np
import xgboost as xgb
from sklearn.datasets import fetch_covtype
from sklearn.metrics import (
accuracy_score,
classification_report,
f1_score,
log_loss,
)
from sklearn.model_selection import train_test_split
from sklearn.utils.class_weight import compute_sample_weight
OUT = Path(__file__).resolve().parent
RANDOM_STATE = 42
CLASS_NAMES = [
"Spruce/Fir",
"Lodgepole Pine",
"Ponderosa Pine",
"Cottonwood/Willow",
"Aspen",
"Douglas-fir",
"Krummholz",
]
FEATURE_NAMES = [
"Elevation",
"Aspect",
"Slope",
"Horizontal_Distance_To_Hydrology",
"Vertical_Distance_To_Hydrology",
"Horizontal_Distance_To_Roadways",
"Hillshade_9am",
"Hillshade_Noon",
"Hillshade_3pm",
"Horizontal_Distance_To_Fire_Points",
*[f"Wilderness_Area_{i}" for i in range(4)],
*[f"Soil_Type_{i}" for i in range(40)],
]
def scores(y_true, pred, proba):
return {
"accuracy": float(accuracy_score(y_true, pred)),
"macro_f1": float(f1_score(y_true, pred, average="macro")),
"weighted_f1": float(f1_score(y_true, pred, average="weighted")),
"log_loss": float(log_loss(y_true, proba, labels=list(range(7)))),
}
def main():
bunch = fetch_covtype(as_frame=False)
X = np.asarray(bunch.data, dtype=np.float32)
y = np.asarray(bunch.target, dtype=np.int32) - 1
if X.shape[1] != len(FEATURE_NAMES) or y.min() != 0 or y.max() != 6:
raise SystemExit(f"Unexpected covertype shape {X.shape} labels {y.min()}..{y.max()}")
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=RANDOM_STATE, stratify=y
)
X_fit, X_val, y_fit, y_val = train_test_split(
X_train, y_train, test_size=0.1, random_state=RANDOM_STATE, stratify=y_train
)
common = dict(
max_depth=8,
learning_rate=0.1,
subsample=0.8,
colsample_bytree=0.8,
min_child_weight=5,
reg_lambda=1.0,
tree_method="hist",
objective="multi:softprob",
num_class=7,
random_state=RANDOM_STATE,
n_jobs=-1,
)
search = xgb.XGBClassifier(n_estimators=400, early_stopping_rounds=20, **common)
search.fit(
X_fit,
y_fit,
sample_weight=compute_sample_weight("balanced", y_fit),
eval_set=[(X_val, y_val)],
verbose=False,
)
n_trees = int(search.best_iteration) + 1
model = xgb.XGBClassifier(n_estimators=n_trees, **common)
model.fit(
X_train,
y_train,
sample_weight=compute_sample_weight("balanced", y_train),
verbose=False,
)
simple = xgb.XGBClassifier(
n_estimators=50,
max_depth=3,
learning_rate=0.1,
tree_method="hist",
objective="multi:softprob",
num_class=7,
random_state=RANDOM_STATE,
n_jobs=-1,
)
simple.fit(X_train, y_train, verbose=False)
proba = model.predict_proba(X_test)
pred = proba.argmax(axis=1)
simple_proba = simple.predict_proba(X_test)
simple_pred = simple_proba.argmax(axis=1)
majority = int(np.bincount(y_train).argmax())
majority_pred = np.full_like(y_test, majority)
majority_proba = np.zeros_like(proba)
majority_proba[:, majority] = 1.0
report = classification_report(
y_test, pred, target_names=CLASS_NAMES, output_dict=True, digits=4
)
gain = model.get_booster().get_score(importance_type="gain")
importance = sorted(
(
{"feature": FEATURE_NAMES[int(k[1:])], "gain": float(v)}
for k, v in gain.items()
),
key=lambda row: row["gain"],
reverse=True,
)
payload = {
"dataset": "UCI Covertype",
"source": "sklearn.datasets.fetch_covtype",
"citation": "Blackard, Jock A. Covertype. UCI Machine Learning Repository.",
"n_rows": int(X.shape[0]),
"n_features": int(X.shape[1]),
"class_names": CLASS_NAMES,
"label_encoding": "model class i is UCI cover type i+1",
"split": {
"test_size": 0.2,
"inner_val_size": 0.1,
"random_state": RANDOM_STATE,
"stratify": True,
},
"n_train": int(len(y_train)),
"n_test": int(len(y_test)),
"n_estimators_selected_on_inner_val": n_trees,
"hyperparameters": {**common, "n_estimators": n_trees, "sample_weight": "balanced"},
"xgboost_version": xgb.__version__,
"trainer_note": (
"XGBClassifier. bleujs.ml.HybridTrainer targets binary classification "
"with binary:logistic, so this multiclass model is trained with the "
"same library and a documented multiclass objective."
),
"test_metrics": scores(y_test, pred, proba),
"simple_xgboost_baseline": scores(y_test, simple_pred, simple_proba),
"majority_baseline": scores(y_test, majority_pred, majority_proba),
"per_class_f1": {name: float(report[name]["f1-score"]) for name in CLASS_NAMES},
"per_class_support": {name: int(report[name]["support"]) for name in CLASS_NAMES},
"feature_importance_gain": importance,
}
model.save_model(OUT / "model.json")
(OUT / "metrics.json").write_text(json.dumps(payload, indent=2))
print(json.dumps({
"n_trees": n_trees,
"test": payload["test_metrics"],
"simple": payload["simple_xgboost_baseline"],
"majority": payload["majority_baseline"],
"per_class_f1": payload["per_class_f1"],
}, indent=2))
if __name__ == "__main__":
main()