Skip to content

feat: add composition-encoder multitask model - #113

Open
RobbinBouwmeester wants to merge 1 commit into
mainfrom
feat/composition-multitask-model
Open

feat: add composition-encoder multitask model#113
RobbinBouwmeester wants to merge 1 commit into
mainfrom
feat/composition-multitask-model

Conversation

@RobbinBouwmeester

Copy link
Copy Markdown
Member

Stacked on #112, which this needs for the corrected features. Review that one first; this diff is against it.

What this adds

A second multitask architecture and its trained weights, alongside the existing model rather than replacing it. DEFAULT_MODEL is untouched.

Calibration is unchanged. The model returns one column per LC setup, so best-head selection and the existing 1-D calibrations work exactly as they do today. I benchmarked replacing best-head selection with a ridge over head outputs and it was not worth it: across 20 datasets it improved median relative MAE from 2.49% to 2.31% at 1,000 calibration peptides, but it was a wash at 200 and worse on several runs. Not a good enough trade to change a working default.

Architecture

deeplc/_composition_architecture.py:

  • Pointwise stem. A kernel-1 convolution decodes each position's six atom counts on its own, before any neighbour mixing. The kernel-5 convolutions would otherwise have to disentangle residue identity and sequence context simultaneously. Measured worth 0.20 min when no residue embedding is present, and it still helps when one is.
  • Low-rank read-out. Each LC setup is a 64-dimensional vector dotted with a projected trunk, rather than an independent head, so a new setup costs 66 parameters. Rank 128 gained nothing over 64.
  • matrix_sum is accepted and ignored. It equals matrix.reshape(30, 2, 6).sum(1) exactly, verified elementwise, so it carries no information the atom matrix lacks. Trained as the sole sequence input it was 2.8x worse.

Plumbing

The model needs the terminal-composition blocks from #112 and declares this via requires_terminal_composition.

  • DeepLCDataset gains a matching add_terminal_composition argument.
  • core.predict loads the model before building the dataset, so it can ask what encoding that model needs. The feature encoding is a property of the model, not of the call site.
  • load_model dispatches on the state dict (head.embedding present) and now accepts any nn.Module instance, not DeepLCModel alone. Architecture hyperparameters are inferred from tensor shapes, as the existing branch already does.

Performance

Trained on 1,025 LC setups and 23.6M observations, features from #112's extractor. Held-out split of 116,627 peptidoforms, 2.36M observations:

metric value
pooled MAE 0.5464 min
median absolute error 0.223 min
per-dataset median MAE 0.479 min
r 0.9987
within 1 min 88%

2.2M parameters.

How to test

pytest tests/

tests/test_composition_architecture.py covers the forward shape, that matrix_sum genuinely does not affect the output, that the packaged checkpoint round-trips through load_model and predict, and that an N-terminal acetyl no longer predicts identically to the unmodified peptide.

End to end through deeplc.core.predict on 1,500 held-out peptidoforms: prediction matrix (1500, 1025), pooled MAE 0.514 min over 30,671 observations.

Limits worth knowing

Unseen modifications work when the chemistry is close to training. On datasets/prosit_ptm_2020.csv, never trained on, calibrated from peptides carrying only known chemistry: Methyl 2.1, Dimethyl 2.1, Succinyl 2.9, Malonyl 3.1, Formyl 3.4, Trimethyl 4.6, Propionyl 4.9, Crotonyl 6.2, Nitro 6.5 min, against a 2.8 min control.

A modification introducing an element the corpus never contains will fail. Phosphorus enters this corpus only through phosphorylation; a model trained without phospho predicts phosphopeptides at 9.7 min. The same holds for an amino acid outside the training composition range: tryptophan held out entirely gives 14.1 min, and no encoding choice rescued it. Worth checking a novel modification's composition against the training distribution before trusting the prediction.

All results are single-seed; repeat runs of an identical configuration spread by about 0.045 min.

🤖 Generated with Claude Code

Adds an alternative multitask architecture and its trained weights, alongside
the existing model rather than replacing it. Calibration is unchanged: the
model returns one column per LC setup, so best-head selection and the existing
1-D calibrations work as they are.

Architecture, in deeplc/_composition_architecture.py:

* a kernel-1 pointwise stem decodes each position's six atom counts before any
  neighbour mixing, so the kernel-5 convolutions are not asked to disentangle
  residue identity and sequence context at once
* a low-rank read-out: each LC setup is a 64-dimensional vector dotted with a
  projected trunk, so a new setup costs 66 parameters
* matrix_sum is accepted for signature compatibility and ignored; it equals
  matrix.reshape(30, 2, 6).sum(1) exactly and carries no extra information

The model requires the terminal-composition blocks in matrix_global and says so
through requires_terminal_composition. DeepLCDataset gained a matching
add_terminal_composition argument, and core.predict loads the model first so it
can ask what encoding that model needs. load_model dispatches on the state dict
and now accepts any nn.Module instance rather than DeepLCModel alone.

Trained on 1,025 LC setups and 23.6M observations, with features from this
branch's extractor. On a held-out 116,627-peptidoform split: 0.5464 min pooled
MAE, 0.223 median absolute error, 0.479 per-dataset median MAE.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@RobbinBouwmeester
RobbinBouwmeester changed the base branch from feat/multitask-composition-model to main August 12, 2026 17:08
@RobbinBouwmeester
RobbinBouwmeester marked this pull request as ready for review August 12, 2026 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant