You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
📦 Release
Version sync to 0.8.4 (app + frontend + core): root pyproject.toml, frontend/ml-canvas/package.json / package-lock.json, and skyulf-core/setup.py bumped to 0.8.4.
🧠 Core — leakage-free searcher tuning
optuna/halving_* tuning now refits preprocessing per fold for every
chain — including the ones the old Pipeline wrap refused: resampling
(SMOTE/over-/undersampling), row drops/outlier removal, and target
re-encoding (LabelEncoder). The transformer-step wrap could only hand X
forward, so such chains fell back to applying the chain once on the full
training set — leaking synthetic rows / fitted target stats into the
validation folds (measured: optuna 0.978 vs per-fold random 0.914 on the
same SMOTE data).
New fit-time meta-estimator FoldAwareModelStep
(skyulf/modeling/_tuning/fold_pipeline.py): the searcher wraps
preprocessing + model in one estimator (fit runs the chain on the fold's
training rows only, then fits the model), so the searcher's internal CV
drives a true per-fold refit with no sklearn patching. When the chain
re-encodes the target, predictions and classes_ are mapped back to the
original label space (built from paired uniques over the whole fold — no
sampling risk for rare classes), and predict_proba columns stay aligned
for roc_auc-style scorers; regressors pass through untouched.
The runtime alignment probe and the one-shot "apply once to the full
split" fallback are retired — the constraint they guarded no longer
exists. The fallback narrows to frameless SDK calls (numpy-only, no named
frames to run the chain on), which keep scoring the raw payload with an
explicit log. Backend runs always have frames, so the app path is always
per-fold now; grid/random (already per-fold) and fork-join
merged-branch screening are unchanged.
Holdout tuning with a validation split is leakage-free now — the last
remaining optimistic path. Before, the backend skipped per-fold refit
entirely whenever data.validation existed and the engine refused preprocessing + validation_data together, so candidates trained on the
already-preprocessed full train frame (SMOTE/WOE statistics fitted with
held-out rows included). Now all five strategies refit the chain on the
train rows only and score candidates against the untouched validation
split: the pre-transform train and validation frames are concatenated into
one search frame with a single PredefinedSplit fold (train masked -1,
validation = the one scoring fold). The backend reconstructs the
pre-transform validation payload alongside the train one and threads it
through fit_predict(..., preprocessing_validation=...) → TuningCalculator.fit/tune(..., validation_frames=...); post-tuning CV
gets the adapter too instead of scoring raw train. Note: on
leakage-dominated graphs (e.g. target-aware encoders) reported holdout best_scores will drop — same dynamic as the v0.8.2 note; the drop is the
bias leaving. SDK callers without named validation frames keep today's
logged raw-payload fallback.
Fold-aware wrap dtype fix: polars payloads handed to the searcher now
convert through to_pandas() inside FoldAwareModelStep; the old np.asarray rebuild collapsed mixed-type frames to object dtype and
silently disabled numeric steps (a SimpleImputer + WOE chain left NaNs
untouched and crashed the candidate fits).
Search-space routing moved to model__estimator__<param>; best_params
and trial params are stripped back to the caller's original keys.
Red-green coverage: meta-estimator unit tests (label-space round trip,
SMOTE-style row shaping, deepcopy isolation for n_jobs > 1, regressor
passthrough, array-input frame rebuild, f1/roc_auc through a real
searcher), a fold-leakage proof (spy adapter asserts every fit_transform sees only its fold's training rows and every validation
fold is scored through transform) for optuna and halving_random, and an
end-to-end XGBoost + string-target optuna run matching the grid score on
the same folds.
🔧 App — cyclic graphs fail fast at both layers
Connect-time checks already blocked cycles one edge at a time, but bulk
graph loads (saved projects) applied nodes/edges verbatim and the backend
never validated topology — a loop died late mid-run with a cryptic
"Artifact not found". Both layers now reject whole-graph cycles up front:
Backend: _cycle_validation.py runs Kahn's algorithm over node inputs,
prunes the stuck set down to the exact loop members (nodes merely
downstream of the loop are not blamed), and raises PipelineCycleError
naming the loop. Called in engine.run() before the leakage check,
whose descendant-map build bails out on cycles.
Frontend: pipelineCycleValidation.ts mirrors the guard and feeds a new 'cycle' category into collectGraphValidationIssues, so the validation
panel flags loops from bulk-loaded graphs instantly and preview/training
submission stays blocked until the loop is removed.
Red-green coverage: 8 backend unit tests, 1 engine integration test
(nothing runs, no artifacts written), 8 frontend tests.
📈 App — live tuning trial chart
Tuning jobs now draw their trial progress while they run: the training
runner publishes a structured trial job event per completed trial
(grid/random and Optuna paths) over the existing /ws/jobs channel, and
the job details view renders an updating per-trial score + best-so-far
chart (useTuningTrials + TuningTrialsChart) with a live Trial x/y
progress tile.
Completed jobs redraw the same chart from the already-persisted metrics.trials — no new persistence. Fixed runs (single trial) and
callback-less strategies (halving) degrade honestly: no live curve,
completed halving jobs still redraw from their recorded trials.
Privacy-safe by construction: the channel broadcasts to every client
without auth, so trial events carry aggregate scalars only (trial index,
total, score, metric) — never hyperparameters or data. Emission failures
are swallowed; training is never impacted.
Late openers are backfilled: the WebSocket only reaches already-connected
clients, so the runner also records each trial in a bounded in-memory
per-job buffer (LRU-evicted, capped) served by GET /api/pipeline/jobs/{id}/trials; opening a running job fetches that
snapshot and the curve starts at trial 1, with live events merged on top
(deduped by trial number). Once a job turns terminal the complete
persisted metrics.trials list always wins over the partial live tail,
so watching a job to completion no longer leaves a truncated curve.
📈 App — live boosting iteration chart
Fixed boosting runs (XGBoost / LightGBM classifiers and regressors) now
stream a per-iteration live chart exactly like tuning runs stream their
trial curve: the calculators attach a native callback
(XgboostIterationAdapter / LightGBMIterationAdapter in core) and an eval_set=[(X_train, X_train)] used purely for display — no early
stopping is configured, and a test verifies predict_proba is identical
with and without the callback. XGBoost 3.x dropped fit(callbacks=...),
so the adapter is set as the estimator's callbacks attribute and
detached again after fit so pickled artifacts never carry live closures.
Per explicit decision, the final refit pass of multi-trial tuning runs
streams its iterations too, after the trial curve completes; candidate
fits inside the search stay silent. Fixed runs route through the same
engine (n_trials=1), so both paths share one code path.
New iteration job event carries iteration_direction
("minimize"/"maximize") because boosting scores are usually losses — the
chart's best-so-far runs a running min for minimize metrics (LightGBM's is_higher_better is used verbatim; XGBoost metric names fall back to a
lower-is-better heuristic).
The runner persists metrics["iterations"] (plus iteration_metric / iteration_direction) post-fit, mirroring metrics["trials"], so
completed jobs redraw without new storage; the live backfill buffer and GET /jobs/{id}/trials snapshot grew a parallel iterations field.
Scope halved on purpose: XGBoost + LightGBM only. CatBoost is not
registered anywhere and HistGradientBoosting has no callback API; both
remain non-goals alongside DL epoch curves.
📈 App — Trials/Iterations tabs for dual-series jobs
A tuned boosting job runs two series — the search's trials, then the
refit's boosting iterations — and used to collapse to one chart
(iterations outranked trials). The job details view now keeps both: useTuningTrials returns two independent slices plus the actively
streaming activeKind, and the view renders a small Trials / Iterations
tab row whenever both series have chartable points.
The view auto-follows the streaming series (trials during the search,
passing to iterations when the refit starts) until the user clicks a tab,
which pins that series; the pin resets when switching jobs. The progress
tile (Trial x/y vs Iteration x/y) tracks the visible series.
Single-series jobs are unchanged: no tabs, chart behaves as before
(trials for any tuned job; iterations for fixed boosting runs).
Coverage: trials appear for every tuned job regardless of problem type
or algorithm; iterations only for xgboost_* / lgbm_* (the only
calculators with native callback APIs); segmentation never tunes, so it
shows neither.
🧠 Core — console trial progress for SDK-only use
TuningConfig(progress=True) gives core-only users a tidy console read of
a tuning run without hand-writing a progress_callback: on a TTY a single
self-updating line (Tuning trial 12/60 | score 0.8530 | best 0.8710 (#8)),
and on completion a compact summary (best score + params, top-3 trials).
Piped stdout (CI/logs) skips per-trial lines and prints only the summary;
output is ASCII-only so it can never raise on cp1252 Windows consoles.
Default off, and the backend always supplies its own callback, so server
behavior is unchanged.
🐛 Core — optuna/halving tuning no longer crashes on target-encoding graphs
With per-fold refit (F-15), the tuner receives the pre-transform
payload. The grid/random loop re-applies the chain per fold, but the
searcher-backed strategies (optuna, halving_*) refuse the Pipeline
wrap when the chain changes the row count or the target — and the old
fallback then searched the raw frames, handing XGBoost/LightGBM a
still-string target. Every trial produced a NaN score and the job died
with "All trials failed". The fallback now applies the chain once to the
full training set before the search (the pre-F-15 behaviour, logged as
such), so e.g. XGBoost + Optuna + CV + a LabelEncoder node tunes cleanly.
Failed optuna trials no longer swallow their cause: the per-trial error
Optuna logs is now forwarded into the job log (visible in the frontend
Error Log), and when all trials fail the raised error carries the first
trial's message (First trial error: ...) instead of only the generic
NaN hint.
Regression tests: the fallback must hand the searcher the encoded target,
and a failing-trials run must surface the error in both the log callback
and the raised message.