Multi-Target Regression in mlframe¶
This document describes the design and rollout plan for
MULTI_TARGET_REGRESSION support: K independent continuous targets
trained jointly through a shared model trunk.
Target shape: y of shape (N, K) float, K >= 2.
Output shape: (N, K) float — column k is the point prediction for
target k.
Status (2026-05-31)¶
-
MLP estimator — ✅ Native, landed in commit
0a6e60ea7.PytorchLightningRegressorauto-detects(N, K>=2)floatyin_fit_commonand routesnum_classes = Ktogenerate_mlp. MSE between(N, K)preds and(N, K)labels works without loss-shape gymnastics. The_is_multi_target_regressionflag is set on the estimator. Verified atR^2 >= 0.985per column on K=3 synthetic data. -
TargetTypes.MULTI_TARGET_REGRESSIONenum value — ✅ landed in commitf03a86ef. New helpersis_multi_target_regressionandis_any_regressionexpose the predicate.is_multi_outputnow includes MTR alongside QR and multiclass / multilabel. -
Strategy dispatch foundation — ✅ Landed in commit
d73bb763+ smoke tests in2241468f.ModelPipelineStrategygained: supports_native_multi_target: bool(default False)get_multi_target_objective_kwargs() -> dict(default{})wrap_multi_target(estimator)— identity for native, MultiOutputRegressor wrap for non-native Per-strategy overrides:- CatBoostStrategy: native, returns
{"loss_function": "MultiRMSE"} - XGBoostStrategy: native, returns
{"multi_strategy": "multi_output_tree", "tree_method": "hist"}(XGBoost ≥ 2.0) - NeuralNetStrategy: native (MLP-side already done in F-24)
- LinearModelStrategy: native (sklearn handles
(N, K)natively) -
TreeModelStrategy (LightGBM) / HGBStrategy: non-native →
MultiOutputRegressorwrap All 5 backends fit + predict(N, K)correctly pertest_multi_target_regression_smoke.py. -
Auto-route helper — ✅ Landed in
d73bb763. Newmultilabel_strategy="multi_target_regression"option onCompositeTargetDiscoveryConfig: instead of expanding(N, K)regression targets to K independent 1-D targets (the legacy"per_target"default), keeps them joint underTargetTypes.MULTI_TARGET_REGRESSION. Pick this for correlated targets that benefit from a shared trunk / boosting ensemble. -
Per-target body wiring — ✅ Landed in commit
d48245de(D1)._phase_train_one_target_body.pyaround line 651: when target_type is MULTI_TARGET_REGRESSION, the cloned model hasget_multi_target_objective_kwargs()injected viaset_params(CatBoostloss_function="MultiRMSE", XGBoostmulti_strategy="multi_output_tree"), then non-native strategies are wrapped viawrap_multi_target()(LightGBM / HGB → MultiOutputRegressor). MLP / Linear / Ridge auto-handle (N, K) at fit-time so the block is a no-op for them. Fallback to setattr if set_params rejects a kwarg. -
Metrics registry — ✅ Landed in commit
d48245de(D3). Seven MTR metrics registered:rmse_macro/rmse_micro/rmse_max/mae_macro/mae_max(lower-is-better),r2_macro/r2_min(higher-is-better). macro = mean per-target column, micro = pooled across (N*K), max/min = worst-case per-target (catches degenerate target columns the mean masks).metric_name_higher_is_better()resolves these via the registry-fallback path automatically. -
Regression reporting — ✅ Fully landed in commits
d48245de(D4) +c9d5067f0(E1).report_regression_model_perfdetects (N, K≥2) targets/preds and: - stamps aggregated MTR metrics (rmse_macro / r2_macro / ...)
into the metrics dict via
iter_extra_metrics(MULTI_TARGET_REGRESSION, ...) -
when
plot_outputs+plot_fileare set, renders ONE chart file per target column via the existingbuild_regression_panel_spec→render_and_savepipeline. Output names:{plot_file_base}_target{k}(the renderer appends the format extension from the DSL). Each per-target chart shows the same scatter + histogram + residual-audit overlay as the single-target report. Still skipped on the MTR path: fairness subgroup, MASE, prediction-envelope clip — all 1-D-only. -
CT_ENSEMBLE for MTR — ✅ Per-column equal-mean ensemble landed in commit
c9d5067f0(E2)._build_cross_target_ensemble_for_targetdispatcher routes MTR targets to a new_build_mtr_per_column_ensemblehelper that: - collects components from
models[target_type][target_name] - builds
MTRPerColumnEqualMeanEnsemble(a thin wrapper that stacks K trained component models, averages their (N, K) predictions across the component axis) -
appends the ensemble as a new entry with
ct_ensemble=True + mtr_ensemble=True + ensemble_strategy="per_column_equal_mean"so downstream save / report layers treat it like any other ensemble. Single-component pools skip with INFO. Future PR: swap equal-weight averaging for per-column honest-OOF optimal weights without changing the public predict() contract. -
Composite targets (
CompositeTargetEstimator/ discovery) — ⏳ NATURALLY SKIPPED. Composite discovery at_phase_composite_discovery.py:81,187already filters toTargetTypes.REGRESSIONonly — MTR targets are not iterated, so no composite generation runs for them. Users mixing the auto-route with composite discovery on the same target name should NOT see duplicate work; the auto-route happens BEFORE discovery and moves the target out of the REGRESSION bucket.
Per-strategy target dispatch (post-rollout)¶
| Strategy | Native MTR? | Mechanism | Notes |
|---|---|---|---|
| CatBoost | ✅ | loss_function="MultiRMSE" (or "MultiRMSEWithMissingValues") |
Single ensemble, native (N, K) output. Preferred path. |
| XGBoost | ✅ (≥2.0) | multi_strategy="multi_output_tree" + tree_method="hist" |
Treats targets jointly; faster than wrapper for K small. |
| LightGBM | ❌ | sklearn.multioutput.MultiOutputRegressor(LGBMRegressor) |
LGB has no native MTR; K independent fits. |
| HistGradientBoosting | ❌ | MultiOutputRegressor wrapper |
sklearn HGB doesn't support MTR natively. |
| Linear (Ridge / LinearRegression / Lasso) | ✅ | sklearn handles (N, K) natively |
MultiTaskLasso / MultiTaskElasticNet add joint L1 across targets. |
| Random Forest / Extra Trees | ✅ | sklearn handles (N, K) natively |
Per-target MSE summed. |
| MLP (ours) | ✅ | PytorchLightningRegressor (F-24) |
K output heads sharing trunk; MSE per column. |
| Recurrent (LSTM / GRU / Transformer) | ⏳ | TBD — likely K-head extension of MLPHead |
Lower priority; separate session. |
| NGB (NGBoost) | ❌ | MultiOutputRegressor wrapper |
NGBoost is single-output by design (probabilistic forecast). |
Suite-integration roadmap¶
Dependency-ordered outline of the work to wire multi-target regression (MTR) end-to-end:
- Detection & target-type wiring — auto-detect MTR (
y.ndim == 2 and y.shape[1] >= 2 and y.dtype.kind == "f") into aMULTI_TARGET_REGRESSIONtarget type, propagate it through the per-model dispatch, and add per-strategy allow/skip-lists (unsupported strategies raise a clearNotImplementedError, same pattern LTR uses). - Per-strategy native paths — CatBoost
loss_function="MultiRMSE", XGBoostmulti_strategy="multi_output_tree"; linear / sklearn-native / MLP pass(N, K)y through unchanged. - Wrappers for non-native strategies — wrap LightGBM / HistGradientBoosting / NGB in
sklearn.multioutput.MultiOutputRegressor, mirroring theMultilabelDispatchConfign_jobs convention. - Metrics & reporting — per-target metric variants (
R2_mean,R2_min,RMSE_mean,RMSE_max, ...) and per-target reporting tables / feature importances. Composite targets stay 1-D (explicitNotImplementedErrorfor MTR). - Ensembling, TTR/scaling, dummy baselines — per-target ensembling stacked across K (joint multi-target average as an opt-in), verify
TransformedTargetRegressorpasses(N, K)through, per-targetDummyRegressor(strategy="mean")baselines. - Docs & tests — MTR example in the suite docstring plus an end-to-end biz_value test (synthetic K=3 → all strategies → per-target metrics → ensemble aggregation).
Cross-cutting risks¶
- Target naming:
target_nameis currently a single string. For MTR with K=3 named ["price", "volume", "spread"], reporting must reference each by name. Passtarget_names: List[str]alongsidetarget_nameor use a delimiter convention. - Per-target sample weights: most strategies accept a single 1-D
sample_weightof shape(N,). Per-target weighting (e.g. K-column(N, K)weight matrix) is NOT supported by sklearn'sMultiOutputRegressoror by native CatBoost MultiRMSE; document the limitation. - Mixed dtype targets: MTR assumes all K targets are continuous (float). Mixed int + float (e.g. one regression + one classification) is NOT this target type — that's joint multi-task learning, a separate feature.
References¶
- F-24 audit entry in
project_mlp_audit_progress.md - Commit
0a6e60ea7(MLP-side native MTR) - Sibling doc:
MULTI_OUTPUT.md(multilabel + multiclass classification) - sklearn
MultiOutputRegressor: https://scikit-learn.org/stable/modules/generated/sklearn.multioutput.MultiOutputRegressor.html - CatBoost
MultiRMSE: https://catboost.ai/en/docs/concepts/loss-functions-multiregression - XGBoost multi-output trees: https://xgboost.readthedocs.io/en/stable/tutorials/multioutput.html