Skip to content

v0.8.4 — Cyclic-Pipeline Guard & Live Tuning Charts

Choose a tag to compare

@github-actions github-actions released this 26 Aug 18:52
· 112 commits to master since this release
577078d

📦 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.