Skip to content

fix(classifier): encode class labels to 0..K-1 for training - #434

Open
ChrisW09 wants to merge 1 commit into
mainfrom
fix/classifier-label-encoding
Open

fix(classifier): encode class labels to 0..K-1 for training#434
ChrisW09 wants to merge 1 commit into
mainfrom
fix/classifier-label-encoding

Conversation

@ChrisW09

Copy link
Copy Markdown
Collaborator

Fixes #409

Problem

SklearnBaseClassifier.fit sets self.classes_ = np.unique(y) and predict() maps argmax indices back through it — but nothing ever converts y into those indices. The raw values flow through _build_modelTabularDataModule.preprocess_datatorch.tensor(y_train, dtype=torch.long) and are used directly as CrossEntropy indices / BCE targets. There is no LabelEncoder, return_inverse, or searchsorted anywhere in the package.

Three reproduced failure modes:

Labels Result before this PR
{"yes","no"} ValueError: could not convert string to float in the preprocessor
{10,20,30} IndexError: Target 10 is out of bounds on CPU; silently trains on garbage on MPS (no bounds check)
{5,7} Silent. BCE trains against targets 5.0/7.0 — perfectly separable data reached 47.75% accuracy vs 100% with {0,1}

Only y already coded as 0..K-1 worked, which is why the suite never caught it.

Fix

A small _encode_labels helper maps labels to contiguous indices via the sorted classes_ array (np.searchsorted), applied in both fit() and build_model(). y_val is encoded with the same mapping and raises a clear ValueError listing any validation labels unseen during fit.

Class-weight resolution deliberately still runs on the raw labels, so class_weight={label: weight} mappings keep working — compute_class_weights orders its output by the same sorted classes_, so the weight vector already matches the encoded indices.

Tests

New tests/test_label_encoding.py: binary and multiclass string labels round-trip through predict/predict_proba, non-contiguous integers work, {5,7} on separable data now exceeds 90% accuracy, unseen y_val labels raise, and plain 0..K-1 labels are unaffected. tests/test_class_imbalance.py and tests/test_models.py pass unchanged (219 passed).

🤖 Generated with Claude Code

Raw label values were passed straight into the loss. classes_ is set to
np.unique(y) and predict() maps model output indices back through it,
but nothing ever produced those indices: no LabelEncoder, no
return_inverse, no searchsorted anywhere in the package.

Three failure modes, all reproduced:
- String labels crash in the preprocessor (PLE's decision-tree binning
  calls float(y)), so sklearn-style string labels were unusable.
- Non-contiguous integer labels (e.g. {10,20,30}) send out-of-range
  targets to CrossEntropy: IndexError on CPU, and on MPS -- which has no
  bounds check -- training silently proceeds on garbage.
- Binary labels other than {0,1} silently train BCEWithLogitsLoss
  against targets like 5.0/7.0; a perfectly separable dataset with
  labels {5,7} reached 47.75% accuracy versus 100% with {0,1}.

fit() and build_model() now encode y (and y_val, raising a clear error
for validation labels unseen during fit) through the sorted classes_
array, completing the round trip predict() already assumed. Class-weight
resolution still sees the raw labels, so {label: weight} mappings keep
working and the resulting weight vector order matches the encoded
indices.

Fixes #409

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Classifier labels are never encoded to 0..K-1 — crashes on string labels, silently wrong on non-contiguous integers

1 participant