CellTriage / configs /models.yaml
Sarvarbek13's picture
CellTriage QC operator console - inference only, CPU-bound classical ML
749bffa verified
Raw
History Blame Contribute Delete
10 kB
# ---------------------------------------------------------------------------
# Model spaces, cross-validation protocol, and seeds.
#
# HARD CONSTRAINT: classical supervised ML only. NO NEURAL NETWORKS of any
# kind. This is a deliberate methodological choice, not a limitation -- the
# seminal result in this field was a regularized linear model, feature
# engineering rather than estimator capacity is the bottleneck, and TreeSHAP
# gives exact attributions, which matters when a process engineer must audit a
# scrap decision.
#
# The `constraints.allow_neural_networks: false` flag below is validated by
# src/utils/config.py, which REJECTS the config if it is ever set true.
# ---------------------------------------------------------------------------
constraints:
allow_neural_networks: false
target:
name: "cycle_life"
# Modelling target is log10(cycle life). WHY: cycle life spans 150-2300
# cycles and its error distribution is multiplicative rather than additive;
# the source literature models it in log space, so this also keeps the
# reproduction comparable.
transform: "log10"
# ---------------------------------------------------------------------------
# Repeated grouped nested cross-validation. Grouping is by physical cell in
# every scheme -- a cell must never appear on both sides of a split.
# ---------------------------------------------------------------------------
cv:
group_key: "cell_id"
outer:
n_splits: 5
# Ten repeats with different seeds. WHY: with n ~ 124-169 cells, a single
# 5-fold partition has enough sampling noise that two seeds can disagree
# by more than the difference between models being compared.
n_repeats: 10
inner:
n_splits: 5
n_repeats: 1
# Hyperparameter search and feature selection happen HERE ONLY.
purpose: "hyperparameter_search_and_selection_only"
splits:
# Phase 5 produces three schemes; see src/data/splitter.py.
cell_grouped:
enabled: true
role: "primary evaluation"
batch_holdout:
enabled: true
train_batches: [batch1, batch2]
test_batches: [batch3]
role: "RQ4 out-of-distribution: qualify on one campaign, deploy on the next"
recipe_holdout:
enabled: true
# Hold out entire charging-policy families, analogous to introducing a new
# process recipe. Family definition is derived in Phase 4 from the parsed
# policy string and recorded in docs/02_feature_engineering.md.
n_folds: 5
role: "RQ4 process-recipe shift"
seeds:
# Seed everything. Every artifact records the seed that produced it.
global: 42
numpy: 42
# One seed per outer-CV repeat, fixed here so that a re-run reproduces the
# same ten partitions exactly.
outer_repeats: [101, 202, 303, 404, 505, 606, 707, 808, 909, 1010]
bootstrap: 7
# ---------------------------------------------------------------------------
# REPORTING RULE, NON-NEGOTIABLE: every headline number is mean +/- std across
# outer folds, with a bootstrap 95% CI. A single test-set number is grounds for
# rejection.
# ---------------------------------------------------------------------------
bootstrap:
n_resamples: 10000
confidence_level: 0.95
method: "percentile"
metrics:
# Both families are reported everywhere. Reporting only the statistical
# family would contradict the thesis of the project.
statistical: [rmse_log10, mae_log10, mape_raw, spearman]
manufacturing: [escape_rate, overkill_rate, yield, chamber_cycles_per_cell, expected_cost_per_cell]
primary: "expected_cost_per_cell"
hyperparameter_search:
engine: "optuna"
n_trials: 50
timeout_seconds: 600
# Optuna runs inside the INNER loop only. Tuning against outer-fold
# performance would be tuning on the test set.
scope: "inner_fold_only"
sampler: "tpe"
seed: 42
# ---------------------------------------------------------------------------
# Model spaces. Search ranges live here so that no magic number sits in code.
# ---------------------------------------------------------------------------
models:
dummy_mean:
enabled: true
estimator: "sklearn.dummy.DummyRegressor"
role: "floor"
params: {strategy: "mean"}
dummy_median:
enabled: true
estimator: "sklearn.dummy.DummyRegressor"
role: "floor"
params: {strategy: "median"}
elastic_net:
enabled: true
estimator: "sklearn.linear_model.ElasticNet"
role: "severson_reproduction_and_baseline"
search_space:
alpha: {type: "float", low: 1.0e-4, high: 1.0e+1, log: true}
l1_ratio: {type: "float", low: 0.01, high: 1.0, log: false}
ridge:
enabled: true
estimator: "sklearn.linear_model.Ridge"
search_space:
alpha: {type: "float", low: 1.0e-4, high: 1.0e+3, log: true}
lasso:
enabled: true
estimator: "sklearn.linear_model.Lasso"
search_space:
alpha: {type: "float", low: 1.0e-5, high: 1.0e+1, log: true}
huber:
enabled: true
estimator: "sklearn.linear_model.HuberRegressor"
# Robust to the handful of very-long-lived cells that otherwise dominate
# squared-error loss.
search_space:
epsilon: {type: "float", low: 1.05, high: 3.0, log: false}
alpha: {type: "float", low: 1.0e-5, high: 1.0e+0, log: true}
random_forest:
enabled: true
estimator: "sklearn.ensemble.RandomForestRegressor"
search_space:
n_estimators: {type: "int", low: 200, high: 1000}
max_depth: {type: "int", low: 2, high: 12}
min_samples_leaf: {type: "int", low: 1, high: 8}
max_features: {type: "float", low: 0.2, high: 1.0}
extra_trees:
enabled: true
estimator: "sklearn.ensemble.ExtraTreesRegressor"
search_space:
n_estimators: {type: "int", low: 200, high: 1000}
max_depth: {type: "int", low: 2, high: 12}
min_samples_leaf: {type: "int", low: 1, high: 8}
lightgbm:
enabled: true
estimator: "lightgbm.LGBMRegressor"
search_space:
n_estimators: {type: "int", low: 100, high: 1500}
learning_rate: {type: "float", low: 0.005, high: 0.3, log: true}
# Shallow trees and small leaves throughout: with ~124 training cells a
# deep GBDT memorises the training set within a few dozen rounds.
num_leaves: {type: "int", low: 4, high: 32}
max_depth: {type: "int", low: 2, high: 6}
min_child_samples: {type: "int", low: 2, high: 15}
subsample: {type: "float", low: 0.6, high: 1.0}
colsample_bytree: {type: "float", low: 0.4, high: 1.0}
reg_alpha: {type: "float", low: 1.0e-8, high: 10.0, log: true}
reg_lambda: {type: "float", low: 1.0e-8, high: 10.0, log: true}
xgboost:
enabled: true
estimator: "xgboost.XGBRegressor"
search_space:
n_estimators: {type: "int", low: 100, high: 1500}
learning_rate: {type: "float", low: 0.005, high: 0.3, log: true}
max_depth: {type: "int", low: 2, high: 6}
min_child_weight: {type: "float", low: 1.0, high: 10.0}
subsample: {type: "float", low: 0.6, high: 1.0}
colsample_bytree: {type: "float", low: 0.4, high: 1.0}
reg_alpha: {type: "float", low: 1.0e-8, high: 10.0, log: true}
reg_lambda: {type: "float", low: 1.0e-8, high: 10.0, log: true}
catboost:
enabled: true
estimator: "catboost.CatBoostRegressor"
search_space:
iterations: {type: "int", low: 100, high: 1500}
learning_rate: {type: "float", low: 0.005, high: 0.3, log: true}
depth: {type: "int", low: 2, high: 6}
l2_leaf_reg: {type: "float", low: 1.0, high: 30.0, log: true}
quantile:
# Feeds conformalized quantile regression in Phase 7.
enabled: true
estimator: "lightgbm.LGBMRegressor"
objective: "quantile"
quantiles: [0.05, 0.10, 0.25, 0.50, 0.75, 0.90, 0.95]
grading_route:
# ---------------------------------------------------------------------
# REVIEWED DECISION (after Phase 3): grade by ORDINAL REGRESSION from
# predicted cycle life, not by direct three-class classification.
#
# WHY. Phase 3 recomputed the realised class balance from data:
# A = 11 cells (8.9%), B = 70 (56.5%), C = 43 (34.7%). Eleven grade-A cells
# is roughly two per fold under 5-fold grouped CV. A direct three-class
# classifier would be estimating a decision boundary for a class it sees
# twice per fold, and its per-fold grade-A recall would be dominated by
# sampling noise rather than by signal.
#
# Predicting log10(cycle life) and thresholding at the grade boundaries
# avoids the small-class problem STRUCTURALLY rather than compensating for
# it: every cell contributes to a single continuous target regardless of
# which side of a boundary it falls, so the 11 grade-A cells inform the fit
# without having to support a class of their own. The grade boundaries are
# ordered and known, which is exactly the situation ordinal treatment is for.
#
# It also preserves what the decision layer needs: the CONTINUE action in
# Phase 8 requires a continuous predictive distribution over cycle life to
# compute value of information. A three-class posterior cannot supply that.
primary: "ordinal_from_regression"
# Direct three-class classification is retained as a SECONDARY,
# INTERPRETABILITY-ONLY view and must be labelled as such wherever reported.
# It is not the headline grading route and its grade-A metrics carry an
# explicit small-class caveat.
secondary:
enabled: true
role: "secondary interpretability view only - not the headline result"
estimators: ["lightgbm.LGBMClassifier", "sklearn.linear_model.LogisticRegression"]
class_weight: "balanced"
min_class_count_warning: 20 # A has 11; reporting must carry the caveat
ensemble:
enabled: true
# Linear meta-learner on OUT-OF-FOLD predictions only. Stacking on in-fold
# predictions leaks the base models' training performance into the meta-model.
meta_learner: "sklearn.linear_model.RidgeCV"
use_out_of_fold_only: true
registry:
directory: "outputs/models"
# Every persisted artifact records the full config and package versions.
record_provenance: true