Skip to content

Move label semantics onto LabelProcessor - #1363

Draft
Logiquo wants to merge 2 commits into
feat/keyed-processor-schemafrom
feat/metric-2
Draft

Logiquo wants to merge 2 commits into
feat/keyed-processor-schemafrom
feat/metric-2

Conversation

@Logiquo

@Logiquo Logiquo commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #1361.

Summary

BaseModel decided how to train on a label from a mode string: it found the mode by walking each processor class's MRO against registry names, then switched on it in get_loss_function and prepare_y_prob. Label semantics lived in the model rather than in the processor that defines the label.

This PR moves those semantics onto the label processors:

  • LabelProcessor mixin (pyhealth.processors.LabelProcessor): mode, is_timeseries, supports_pos_weight, activation(logits) and loss(logits, y_true, pos_weight=None).

    Processor mode activation loss pos_weight
    BinaryLabelProcessor binary sigmoid BCE with logits ✓
    MultiClassLabelProcessor multiclass softmax cross-entropy
    MultiLabelProcessor multilabel sigmoid BCE with logits ✓
    RegressionLabelProcessor regression identity MSE
    TimeseriesBinaryLabelProcessor timeseries_binary as binary binary, per label record
    TimeseriesRegressionLabelProcessor timeseries_regression as regression regression, per label record

    The time-series labels delegate to their static counterpart (base_cls) and keep the per-record shape check and the zero loss for a batch without label records (formerly _label_record_loss). Their fit is FeatureProcessor's no-op: SequenceProcessorV2.fit read every sample and found no tokens or statistics to learn.

  • BaseModel delegates. get_loss_function() returns the label processor's loss with the model's pos_weight, and prepare_y_prob() its activation. The results are unchanged. _resolve_mode reads LabelProcessor.mode instead of walking the registry. set_pos_weight checks supports_pos_weight. Without a fitted label processor (e.g. a dataset whose processors are not fitted), the processor registered under the mode is used.

  • Optional labels on MultiClassLabelProcessor and MultiLabelProcessor: the label set in index order, or its size K for labels already 0..K-1. When given, the vocabulary (and size()) is fixed, and fit only checks that every label is in it. A class missing from the training split still gets its output. When omitted, the label set is learned as before, which code-prediction tasks such as drug recommendation and medical coding rely on. Pass it as ("multiclass", {"labels": [...]}).

  • autoimprove resolves the label mode from LabelProcessor.mode too.

  • Own module. The time-series label processors move from timeseries_processor.py (the legacy TimeseriesProcessor input processor) to timeseries_label_processor.py. They are unreleased, so no import path or pickled cache depends on the old location; from pyhealth.processors import ... is unchanged.

Breaking changes

  • get_loss_function() returns a functools.partial of the processor's loss, so it has no __name__. The loss values are unchanged.

Tests

  • New tests/core/test_label_processor.py covers:
    • activation and loss of all six processors against the torch functions they replace, including pos_weight, its rejection where unsupported, the per-record shape check, the zero loss on an empty batch, and the time-series fit not reading the samples;
    • only label processors are labels (MultiHotProcessor is not);
    • labels: fixed indices and size, K meaning range(K), unknown and invalid labels, omitted labels learned as before, pickling (including an instance pickled before labels existed), and labels passed through a task schema;
    • BaseModel delegation.
  • test_legacy_mode_resolution compares loss values rather than function names.
  • Ran locally (unittest): the test modules for models, processors, interpretability, calibration, trainer and autoimprove, plus doctests of the changed modules. All pass, except two that fail for environment reasons: test_survival_mdn imports pytest, which is not installed, and test_transformers_model.TestChunking.test_windows_need_a_fast_tokenizer also fails on master here.

@Logiquo
Logiquo added this pull request to stack #1364 November 1, 2026 23:45
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Test Coverage

Total: 75.27% (21568 / 28656 lines)

Modified file Coverage Lines
pyhealth/autoimprove/improver.py 95.62% 371 / 388
pyhealth/models/base_model.py 88.99% 97 / 109
pyhealth/processors/__init__.py 94.44% 34 / 36
pyhealth/processors/label_processor.py 92.11% 175 / 190
pyhealth/processors/timeseries_label_processor.py 100.00% 53 / 53
pyhealth/processors/timeseries_processor.py 84.42% 65 / 77

Workflow run · 0c28e2f

@Logiquo
Logiquo force-pushed the feat/metric-2 branch 2 times, most recently from 8fca165 to 01f45dd Compare November 2, 2026 00:48
Label processors now say what kind of label they are and how to train on
it; BaseModel delegates instead of switching on a mode string.

- LabelProcessor mixin: mode, is_timeseries, supports_pos_weight,
  activation() and loss(). Binary, MultiClass, MultiLabel and Regression
  implement them; the time-series labels apply their static counterpart
  per label record (formerly _label_record_loss) and skip
  SequenceProcessorV2's pass over the samples in fit().
- BaseModel.get_loss_function() and prepare_y_prob() use the label
  processor; _resolve_mode reads LabelProcessor.mode instead of walking
  the registry; set_pos_weight checks supports_pos_weight.
- MultiClassLabelProcessor and MultiLabelProcessor take an optional
  labels (the label set, or its size) that fixes the vocabulary; fit then
  only checks the labels. Omitted, the label set is learned as before.
TimeseriesLabelProcessor and its binary and regression subclasses are
labels built on SequenceProcessorV2; they now live in
timeseries_label_processor.py instead of next to the legacy
TimeseriesProcessor feature processor. They have not been released, so
no import path or pickled cache depends on the old location.

This branch has not been deployed

No deployments
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.

1 participant