ai-internet-diagnostic-model / model /train_classifier.py
WolfDavid's picture
feat(02-01): feature matrix assembly + calibrated LightGBM classifier
10cb234
Raw
History Blame Contribute Delete
2.64 kB
"""LightGBM 10-class classifier with isotonic calibration (D-CAL-01..03).
Per OQ-2 resolution / D-CAL-02: pass LGBMClassifier directly to
CalibratedClassifierCV — sklearn 1.8 applies OvR internally for
method='isotonic' on multi-class. An explicit one-vs-rest wrapper around
the base estimator would cause double-OvR (Pitfall 4).
"""
from __future__ import annotations
import numpy as np
from lightgbm import LGBMClassifier
from sklearn.calibration import CalibratedClassifierCV
from sklearn.model_selection import StratifiedKFold
# D-PUB-07 fixed defaults at v1, no HPO. D-REPRO-01 determinism trio.
LGBM_PARAMS: dict = {
"n_estimators": 500,
"learning_rate": 0.05,
"num_leaves": 63,
"max_depth": -1,
"class_weight": "balanced", # carries forward Phase 1 D-07
"deterministic": True, # Pitfall 1 — required for 1e-4 reproducibility
"force_col_wise": True, # required when deterministic=True (LightGBM docs)
"n_jobs": -1,
"verbose": -1, # silence LightGBM stdout for clean CI logs
}
def train_calibrated_classifier(
X: np.ndarray,
y: np.ndarray,
*,
classifier_seed: int,
cv_seed: int,
) -> CalibratedClassifierCV:
"""Fit LightGBM + isotonic calibration with stratified 5-fold CV.
Single-wrap CalibratedClassifierCV(LGBMClassifier, method='isotonic', cv=5).
Returns a fitted CalibratedClassifierCV with len(.calibrated_classifiers_) == 5
(one per fold; ensemble='auto' resolves to True since base is not FrozenEstimator).
"""
base = LGBMClassifier(random_state=classifier_seed, **LGBM_PARAMS)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=cv_seed)
calibrated = CalibratedClassifierCV(
estimator=base,
method="isotonic", # D-CAL-01
cv=cv, # D-CAL-03 — explicit StratifiedKFold for reproducibility
ensemble="auto", # resolves to True; keeps 5 (base + isotonic) pairs
n_jobs=-1,
)
calibrated.fit(X, y)
return calibrated
def train_raw_classifier(
X: np.ndarray, y: np.ndarray, *, classifier_seed: int
) -> LGBMClassifier:
"""Fit a non-calibrated LGBMClassifier on the same train data + seed.
Needed by the orchestrator (Pattern 6 / 02-02) to produce raw_proba for
the dual reliability grid (D-CAL-06). CalibratedClassifierCV does not
expose the underlying full-train softmax — fold-internal estimators are
on 4/5 of data; we want the matched-seed 5/5 baseline for the raw plot.
"""
clf = LGBMClassifier(random_state=classifier_seed, **LGBM_PARAMS)
clf.fit(X, y)
return clf