ultrams 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ultrams-0.3.0/CITATION.cff +15 -0
- ultrams-0.3.0/MANIFEST.in +4 -0
- {ultrams-0.2.0/src/ultrams.egg-info → ultrams-0.3.0}/PKG-INFO +11 -2
- {ultrams-0.2.0 → ultrams-0.3.0}/README.md +10 -1
- ultrams-0.3.0/cookbook/tutorials/MASSSPECGYM_CONTRASTIVE.md +22 -0
- ultrams-0.3.0/cookbook/tutorials/README.md +13 -0
- ultrams-0.3.0/cookbook/tutorials/embed.ipynb +83 -0
- ultrams-0.3.0/cookbook/tutorials/massspecgym_contrastive.py +301 -0
- ultrams-0.3.0/cookbook/tutorials/pytorch_finetune.ipynb +97 -0
- ultrams-0.3.0/cookbook/tutorials/spectrum_search.ipynb +69 -0
- {ultrams-0.2.0 → ultrams-0.3.0}/docs/data-format.md +15 -0
- {ultrams-0.2.0 → ultrams-0.3.0}/examples/README.md +13 -0
- {ultrams-0.2.0 → ultrams-0.3.0}/examples/pytorch_finetune.py +3 -0
- {ultrams-0.2.0 → ultrams-0.3.0}/pyproject.toml +1 -1
- {ultrams-0.2.0 → ultrams-0.3.0}/src/ultrams/inference.py +18 -1
- {ultrams-0.2.0 → ultrams-0.3.0/src/ultrams.egg-info}/PKG-INFO +11 -2
- {ultrams-0.2.0 → ultrams-0.3.0}/src/ultrams.egg-info/SOURCES.txt +7 -0
- {ultrams-0.2.0 → ultrams-0.3.0}/tests/test_model_api.py +15 -0
- ultrams-0.2.0/MANIFEST.in +0 -2
- {ultrams-0.2.0 → ultrams-0.3.0}/LICENSE +0 -0
- {ultrams-0.2.0 → ultrams-0.3.0}/docs/model-selection.md +0 -0
- {ultrams-0.2.0 → ultrams-0.3.0}/examples/batch_embeddings.py +0 -0
- {ultrams-0.2.0 → ultrams-0.3.0}/examples/data/NOTICE.md +0 -0
- {ultrams-0.2.0 → ultrams-0.3.0}/examples/data/example_5_spectra.mgf +0 -0
- {ultrams-0.2.0 → ultrams-0.3.0}/examples/spectrum_search.py +0 -0
- {ultrams-0.2.0 → ultrams-0.3.0}/setup.cfg +0 -0
- {ultrams-0.2.0 → ultrams-0.3.0}/src/ultrams/__init__.py +0 -0
- {ultrams-0.2.0 → ultrams-0.3.0}/src/ultrams/_architecture.py +0 -0
- {ultrams-0.2.0 → ultrams-0.3.0}/src/ultrams/cli.py +0 -0
- {ultrams-0.2.0 → ultrams-0.3.0}/src/ultrams/finetune.py +0 -0
- {ultrams-0.2.0 → ultrams-0.3.0}/src/ultrams/spectra.py +0 -0
- {ultrams-0.2.0 → ultrams-0.3.0}/src/ultrams.egg-info/dependency_links.txt +0 -0
- {ultrams-0.2.0 → ultrams-0.3.0}/src/ultrams.egg-info/entry_points.txt +0 -0
- {ultrams-0.2.0 → ultrams-0.3.0}/src/ultrams.egg-info/requires.txt +0 -0
- {ultrams-0.2.0 → ultrams-0.3.0}/src/ultrams.egg-info/top_level.txt +0 -0
- {ultrams-0.2.0 → ultrams-0.3.0}/tests/test_spectra.py +0 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use UltraMS in your research, please cite this software."
|
|
3
|
+
type: software
|
|
4
|
+
title: UltraMS
|
|
5
|
+
authors:
|
|
6
|
+
- family-names: Li
|
|
7
|
+
given-names: Yifan
|
|
8
|
+
- family-names: Zhong
|
|
9
|
+
given-names: Yunhua
|
|
10
|
+
- family-names: Liu
|
|
11
|
+
given-names: Pan
|
|
12
|
+
- family-names: Xia
|
|
13
|
+
given-names: Jun
|
|
14
|
+
repository-code: https://github.com/Dsadd4/UltraMS
|
|
15
|
+
license: Apache-2.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ultrams
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Pretrained UltraMS models for mass spectrometry
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://github.com/Dsadd4/UltraMS
|
|
@@ -30,7 +30,8 @@ python -m pip install ultrams
|
|
|
30
30
|
| Embed an MGF or mzML file | [File command](#embed-a-spectrum-file) |
|
|
31
31
|
| Train on my labelled spectra | [PyTorch example](#fine-tune-with-pytorch) · [complete MGF example](https://github.com/Dsadd4/UltraMS/blob/main/examples/pytorch_finetune.py) |
|
|
32
32
|
| Compare spectra | [Search example](https://github.com/Dsadd4/UltraMS/blob/main/examples/spectrum_search.py) |
|
|
33
|
-
|
|
|
33
|
+
| Learn in a notebook | [UltraMS tutorials](https://github.com/Dsadd4/UltraMS/tree/main/cookbook/tutorials) |
|
|
34
|
+
| Inspect or run pretraining | [Training code](https://github.com/Dsadd4/UltraMS/blob/main/training/README.md) |
|
|
34
35
|
|
|
35
36
|
## Embed a spectrum
|
|
36
37
|
|
|
@@ -48,6 +49,8 @@ print(embedding.shape) # (1024,)
|
|
|
48
49
|
|
|
49
50
|
The first call downloads the selected checkpoint from Hugging Face. Use `device="cuda"` or `device="mps"` with `from_pretrained` to run on an available accelerator.
|
|
50
51
|
|
|
52
|
+
The three checkpoints are collected on [Hugging Face](https://huggingface.co/collections/dsadd4/ultrams-6ab4cfaa860cbbd1f9a6b9b6).
|
|
53
|
+
|
|
51
54
|
## Embed a spectrum file
|
|
52
55
|
|
|
53
56
|
```bash
|
|
@@ -87,6 +90,8 @@ for batch in loader:
|
|
|
87
90
|
|
|
88
91
|
The [complete fine-tuning example](https://github.com/Dsadd4/UltraMS/blob/main/examples/pytorch_finetune.py) reads an MGF file and saves training history. For a shorter route, use `model.finetune(labelled_spectra, task="regression")` with a `label` field; it saves weights, configuration, and losses. See [spectrum input](https://github.com/Dsadd4/UltraMS/blob/main/docs/data-format.md) for the input format.
|
|
89
92
|
|
|
93
|
+
For measured molecular labels, the [MassSpecGym contrastive tutorial](https://github.com/Dsadd4/UltraMS/blob/main/cookbook/tutorials/MASSSPECGYM_CONTRASTIVE.md) downloads the dataset, keeps its train/validation/test folds, fine-tunes the encoder, and reloads the selected checkpoint.
|
|
94
|
+
|
|
90
95
|
## Choose a model
|
|
91
96
|
|
|
92
97
|
| Model name | Representation | Dimension | Spectral peak limit | Weights |
|
|
@@ -100,3 +105,7 @@ Load any of them with `UltraMS.from_pretrained("name")`, or load a downloaded `m
|
|
|
100
105
|
## Pretraining
|
|
101
106
|
|
|
102
107
|
The [UltraMSdata pretraining package](https://github.com/Dsadd4/UltraMS/blob/main/training/README.md) contains the current model architecture, training entry points, configuration and history outputs. It installs separately from the inference package.
|
|
108
|
+
|
|
109
|
+
## Cite
|
|
110
|
+
|
|
111
|
+
Citation metadata for this software is in [CITATION.cff](https://github.com/Dsadd4/UltraMS/blob/main/CITATION.cff).
|
|
@@ -12,7 +12,8 @@ python -m pip install ultrams
|
|
|
12
12
|
| Embed an MGF or mzML file | [File command](#embed-a-spectrum-file) |
|
|
13
13
|
| Train on my labelled spectra | [PyTorch example](#fine-tune-with-pytorch) · [complete MGF example](https://github.com/Dsadd4/UltraMS/blob/main/examples/pytorch_finetune.py) |
|
|
14
14
|
| Compare spectra | [Search example](https://github.com/Dsadd4/UltraMS/blob/main/examples/spectrum_search.py) |
|
|
15
|
-
|
|
|
15
|
+
| Learn in a notebook | [UltraMS tutorials](https://github.com/Dsadd4/UltraMS/tree/main/cookbook/tutorials) |
|
|
16
|
+
| Inspect or run pretraining | [Training code](https://github.com/Dsadd4/UltraMS/blob/main/training/README.md) |
|
|
16
17
|
|
|
17
18
|
## Embed a spectrum
|
|
18
19
|
|
|
@@ -30,6 +31,8 @@ print(embedding.shape) # (1024,)
|
|
|
30
31
|
|
|
31
32
|
The first call downloads the selected checkpoint from Hugging Face. Use `device="cuda"` or `device="mps"` with `from_pretrained` to run on an available accelerator.
|
|
32
33
|
|
|
34
|
+
The three checkpoints are collected on [Hugging Face](https://huggingface.co/collections/dsadd4/ultrams-6ab4cfaa860cbbd1f9a6b9b6).
|
|
35
|
+
|
|
33
36
|
## Embed a spectrum file
|
|
34
37
|
|
|
35
38
|
```bash
|
|
@@ -69,6 +72,8 @@ for batch in loader:
|
|
|
69
72
|
|
|
70
73
|
The [complete fine-tuning example](https://github.com/Dsadd4/UltraMS/blob/main/examples/pytorch_finetune.py) reads an MGF file and saves training history. For a shorter route, use `model.finetune(labelled_spectra, task="regression")` with a `label` field; it saves weights, configuration, and losses. See [spectrum input](https://github.com/Dsadd4/UltraMS/blob/main/docs/data-format.md) for the input format.
|
|
71
74
|
|
|
75
|
+
For measured molecular labels, the [MassSpecGym contrastive tutorial](https://github.com/Dsadd4/UltraMS/blob/main/cookbook/tutorials/MASSSPECGYM_CONTRASTIVE.md) downloads the dataset, keeps its train/validation/test folds, fine-tunes the encoder, and reloads the selected checkpoint.
|
|
76
|
+
|
|
72
77
|
## Choose a model
|
|
73
78
|
|
|
74
79
|
| Model name | Representation | Dimension | Spectral peak limit | Weights |
|
|
@@ -82,3 +87,7 @@ Load any of them with `UltraMS.from_pretrained("name")`, or load a downloaded `m
|
|
|
82
87
|
## Pretraining
|
|
83
88
|
|
|
84
89
|
The [UltraMSdata pretraining package](https://github.com/Dsadd4/UltraMS/blob/main/training/README.md) contains the current model architecture, training entry points, configuration and history outputs. It installs separately from the inference package.
|
|
90
|
+
|
|
91
|
+
## Cite
|
|
92
|
+
|
|
93
|
+
Citation metadata for this software is in [CITATION.cff](https://github.com/Dsadd4/UltraMS/blob/main/CITATION.cff).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Contrastive fine-tuning with MassSpecGym
|
|
2
|
+
|
|
3
|
+
This tutorial fine-tunes the **UltraMS Unsupervised** encoder on MassSpecGym. Two distinct MS/MS spectra with the same `inchikey` are a positive pair; spectra from different molecules in a batch are negatives. The dataset's `train`, `val`, and `test` folds are retained, and the script checks that no molecular key appears in more than one fold. It keeps spectra with at least three positive-intensity, positive-m/z peaks and records the source and selected row and molecule counts in `data_audit.json`.
|
|
4
|
+
|
|
5
|
+
Install UltraMS, then run the small teaching subset from the repository root:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python -m pip install -e .
|
|
9
|
+
python cookbook/tutorials/massspecgym_contrastive.py --output-dir /path/to/my_run
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The default selects up to 64 train molecules and 24 molecules from each evaluation fold, with two distinct spectra per molecule. Selection is deterministic. To use every training molecule with at least two distinct spectra and all valid evaluation spectra, after removing exact spectral duplicates:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
python cookbook/tutorials/massspecgym_contrastive.py --full --output-dir /path/to/full_run
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The full run can take substantially longer. `--data-file /path/to/MassSpecGym.tsv` reuses a local copy; otherwise the script downloads the pinned file through Hugging Face Hub. In both cases it checks SHA-256 before reading. The source is [`roman-bushuiev/MassSpecGym`](https://huggingface.co/datasets/roman-bushuiev/MassSpecGym), revision `d2e86d0c3bd905a6d578c0dd6053ed2bd41f9c2a`, file `data/MassSpecGym.tsv`, SHA-256 `0c9cc50450def3f0d4fe2dc09dea1105fc15e635db8c6656bc3e3be37a3bcd95`.
|
|
19
|
+
|
|
20
|
+
The script writes `best.pt`, `config.json`, `data_audit.json`, `history.json`, and `test.json`. Validation selects the checkpoint; the held-out test fold is evaluated once afterward. Top1 means nearest *other spectrum* within the same fold has the same `inchikey`; singleton molecules are candidates but cannot be queries. This is a **tutorial retrieval task**, not an official MassSpecGym benchmark or a reported UltraMS result. The label is molecular identity from the dataset, not measured retention time or logP.
|
|
21
|
+
|
|
22
|
+
For the original dataset and citation, see [MassSpecGym](https://github.com/pluskal-lab/MassSpecGym).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# UltraMS tutorials
|
|
2
|
+
|
|
3
|
+
Each notebook runs from top to bottom on the [five-spectrum MGF example](../../examples/data/NOTICE.md). It installs UltraMS from PyPI and downloads the selected model from Hugging Face on first use.
|
|
4
|
+
|
|
5
|
+
| Tutorial | Notebook | Colab |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| Read MGF and obtain embeddings | [Get embeddings](embed.ipynb) | [Open in Colab](https://colab.research.google.com/github/Dsadd4/UltraMS/blob/main/cookbook/tutorials/embed.ipynb) |
|
|
8
|
+
| Fine-tune in a PyTorch loop | [PyTorch fine-tuning](pytorch_finetune.ipynb) | [Open in Colab](https://colab.research.google.com/github/Dsadd4/UltraMS/blob/main/cookbook/tutorials/pytorch_finetune.ipynb) |
|
|
9
|
+
| Rank spectra with the Search model | [Spectrum search](spectrum_search.ipynb) | [Open in Colab](https://colab.research.google.com/github/Dsadd4/UltraMS/blob/main/cookbook/tutorials/spectrum_search.ipynb) |
|
|
10
|
+
|
|
11
|
+
The fine-tuning labels are placeholders for learning the API. They do not represent measured properties or a model performance result.
|
|
12
|
+
|
|
13
|
+
For a complete fine-tuning example with real molecular labels and the original dataset folds, follow the [MassSpecGym contrastive tutorial](MASSSPECGYM_CONTRASTIVE.md).
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cells": [
|
|
3
|
+
{
|
|
4
|
+
"cell_type": "markdown",
|
|
5
|
+
"id": "introduction",
|
|
6
|
+
"metadata": {},
|
|
7
|
+
"source": "# Get MS/MS embeddings\n\n[Open in Colab](https://colab.research.google.com/github/Dsadd4/UltraMS/blob/main/cookbook/tutorials/embed.ipynb)\n\nRead a real MGF file and obtain one UltraMS embedding per spectrum. The input file is the [five-spectrum DreaMS example](../../examples/data/NOTICE.md). Run cells from top to bottom."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"cell_type": "markdown",
|
|
11
|
+
"id": "installation",
|
|
12
|
+
"metadata": {},
|
|
13
|
+
"source": "## Install"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"cell_type": "code",
|
|
17
|
+
"id": "install-ultrams",
|
|
18
|
+
"metadata": {},
|
|
19
|
+
"execution_count": null,
|
|
20
|
+
"outputs": [],
|
|
21
|
+
"source": [
|
|
22
|
+
"%pip install -q ultrams\n"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"cell_type": "markdown",
|
|
27
|
+
"id": "read-spectra",
|
|
28
|
+
"metadata": {},
|
|
29
|
+
"source": "## Read spectra"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"cell_type": "code",
|
|
33
|
+
"id": "load-mgf",
|
|
34
|
+
"metadata": {},
|
|
35
|
+
"execution_count": null,
|
|
36
|
+
"outputs": [],
|
|
37
|
+
"source": "from pathlib import Path\nfrom urllib.request import urlretrieve\nimport torch\nfrom ultrams import UltraMS, read_spectra\n\nsample = Path(\"examples/data/example_5_spectra.mgf\")\nif not sample.exists():\n sample = Path(\"example_5_spectra.mgf\")\n if not sample.exists():\n urlretrieve(\n \"https://raw.githubusercontent.com/Dsadd4/UltraMS/main/examples/data/example_5_spectra.mgf\",\n sample,\n )\nspectra = list(read_spectra(sample))\nassert len(spectra) == 5\nprint(f\"Loaded {len(spectra)} MS/MS spectra\")"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"cell_type": "markdown",
|
|
41
|
+
"id": "encode-heading",
|
|
42
|
+
"metadata": {},
|
|
43
|
+
"source": "## Encode in batches"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"cell_type": "code",
|
|
47
|
+
"id": "encode-batch",
|
|
48
|
+
"metadata": {},
|
|
49
|
+
"execution_count": null,
|
|
50
|
+
"outputs": [],
|
|
51
|
+
"source": "device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nmodel = UltraMS.from_pretrained(\"unsupervised\", device=device)\nembeddings = model.encode_batch(spectra, batch_size=5)\nassert embeddings.shape == (len(spectra), model.embedding_dim)\nprint(\"Embedding matrix:\", embeddings.shape)\nprint(\"First spectrum:\", spectra[0][\"id\"])"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"cell_type": "markdown",
|
|
55
|
+
"id": "save-heading",
|
|
56
|
+
"metadata": {},
|
|
57
|
+
"source": "## Save embeddings"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"cell_type": "code",
|
|
61
|
+
"id": "save-embeddings",
|
|
62
|
+
"metadata": {},
|
|
63
|
+
"execution_count": null,
|
|
64
|
+
"outputs": [],
|
|
65
|
+
"source": "import numpy as np\n\noutput = Path(\"ultrams_embeddings.npz\")\nnp.savez_compressed(output, ids=[spectrum[\"id\"] for spectrum in spectra], embeddings=embeddings)\nprint(output.resolve())"
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
"metadata": {
|
|
69
|
+
"kernelspec": {
|
|
70
|
+
"display_name": "Python 3",
|
|
71
|
+
"language": "python",
|
|
72
|
+
"name": "python3"
|
|
73
|
+
},
|
|
74
|
+
"language_info": {
|
|
75
|
+
"name": "python"
|
|
76
|
+
},
|
|
77
|
+
"colab": {
|
|
78
|
+
"provenance": []
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"nbformat": 4,
|
|
82
|
+
"nbformat_minor": 5
|
|
83
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Fine-tune UltraMS on MassSpecGym spectra sharing a molecular InChIKey."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import csv
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import random
|
|
10
|
+
import tempfile
|
|
11
|
+
from collections import Counter, defaultdict
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import torch
|
|
16
|
+
import torch.nn.functional as F
|
|
17
|
+
from huggingface_hub import hf_hub_download
|
|
18
|
+
|
|
19
|
+
from ultrams import UltraMS
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
DATASET = "roman-bushuiev/MassSpecGym"
|
|
23
|
+
REVISION = "d2e86d0c3bd905a6d578c0dd6053ed2bd41f9c2a"
|
|
24
|
+
FILENAME = "data/MassSpecGym.tsv"
|
|
25
|
+
SHA256 = "0c9cc50450def3f0d4fe2dc09dea1105fc15e635db8c6656bc3e3be37a3bcd95"
|
|
26
|
+
FOLDS = ("train", "val", "test")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def checked_data_file(path: Path | None) -> Path:
|
|
30
|
+
if path is None:
|
|
31
|
+
path = Path(hf_hub_download(repo_id=DATASET, repo_type="dataset", filename=FILENAME, revision=REVISION))
|
|
32
|
+
digest = hashlib.sha256()
|
|
33
|
+
with path.open("rb") as handle:
|
|
34
|
+
for chunk in iter(lambda: handle.read(8 * 1024 * 1024), b""):
|
|
35
|
+
digest.update(chunk)
|
|
36
|
+
if digest.hexdigest() != SHA256:
|
|
37
|
+
raise ValueError(f"MassSpecGym SHA-256 mismatch for {path}")
|
|
38
|
+
return path
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def rows_in(path: Path):
|
|
42
|
+
with path.open(newline="", encoding="utf-8") as handle:
|
|
43
|
+
reader = csv.DictReader(handle, delimiter="\t")
|
|
44
|
+
needed = {"identifier", "mzs", "intensities", "inchikey", "precursor_mz", "fold"}
|
|
45
|
+
missing = needed - set(reader.fieldnames or [])
|
|
46
|
+
if missing:
|
|
47
|
+
raise ValueError(f"MassSpecGym is missing columns: {sorted(missing)}")
|
|
48
|
+
yield from reader
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_spectrum(row: dict[str, str]):
|
|
52
|
+
key = row["inchikey"].strip()
|
|
53
|
+
if not key:
|
|
54
|
+
return None
|
|
55
|
+
try:
|
|
56
|
+
mz = np.fromstring(row["mzs"], sep=",", dtype=np.float32)
|
|
57
|
+
intensity = np.fromstring(row["intensities"], sep=",", dtype=np.float32)
|
|
58
|
+
precursor = float(row["precursor_mz"])
|
|
59
|
+
except ValueError:
|
|
60
|
+
return None
|
|
61
|
+
if mz.size != intensity.size or not np.isfinite(precursor) or precursor <= 0:
|
|
62
|
+
return None
|
|
63
|
+
valid = np.isfinite(mz) & np.isfinite(intensity) & (mz > 0)
|
|
64
|
+
effective = valid & (intensity > 0)
|
|
65
|
+
if int(effective.sum()) < 3:
|
|
66
|
+
return None
|
|
67
|
+
# Keep zero-intensity peaks, as UltraMS inference does.
|
|
68
|
+
mz, intensity = mz[valid], intensity[valid]
|
|
69
|
+
fingerprint = hashlib.blake2b(
|
|
70
|
+
mz.tobytes() + intensity.tobytes() + np.float32(precursor).tobytes(), digest_size=16
|
|
71
|
+
).hexdigest()
|
|
72
|
+
return {
|
|
73
|
+
"id": row["identifier"],
|
|
74
|
+
"inchikey": key,
|
|
75
|
+
"mz": mz,
|
|
76
|
+
"intensity": intensity,
|
|
77
|
+
"precursor_mz": precursor,
|
|
78
|
+
"fingerprint": fingerprint,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def selected_keys(groups: dict[str, set[str]], fold: str, seed: int, limit: int | None):
|
|
83
|
+
keys = sorted(groups, key=lambda key: hashlib.sha256(f"{seed}:{fold}:{key}".encode()).digest())
|
|
84
|
+
return set(keys if limit is None else keys[:limit])
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def prepare(path: Path, *, seed: int, full: bool, limits: dict[str, int]):
|
|
88
|
+
raw = Counter()
|
|
89
|
+
valid = Counter()
|
|
90
|
+
fingerprints = {fold: defaultdict(set) for fold in FOLDS}
|
|
91
|
+
for row in rows_in(path):
|
|
92
|
+
fold = row["fold"]
|
|
93
|
+
if fold not in FOLDS:
|
|
94
|
+
raise ValueError(f"unexpected MassSpecGym fold: {fold!r}")
|
|
95
|
+
raw[fold] += 1
|
|
96
|
+
spectrum = parse_spectrum(row)
|
|
97
|
+
if spectrum is None:
|
|
98
|
+
continue
|
|
99
|
+
valid[fold] += 1
|
|
100
|
+
fingerprints[fold][spectrum["inchikey"]].add(spectrum["fingerprint"])
|
|
101
|
+
|
|
102
|
+
keys = {fold: set(fingerprints[fold]) for fold in FOLDS}
|
|
103
|
+
for i, left in enumerate(FOLDS):
|
|
104
|
+
for right in FOLDS[i + 1 :]:
|
|
105
|
+
if keys[left] & keys[right]:
|
|
106
|
+
raise ValueError(f"molecular overlap between {left} and {right}")
|
|
107
|
+
|
|
108
|
+
eligible = {
|
|
109
|
+
fold: {key: values for key, values in fingerprints[fold].items() if len(values) >= 2}
|
|
110
|
+
for fold in FOLDS
|
|
111
|
+
}
|
|
112
|
+
chosen = {
|
|
113
|
+
"train": selected_keys(eligible["train"], "train", seed, None if full else limits["train"]),
|
|
114
|
+
"val": selected_keys(fingerprints["val"] if full else eligible["val"], "val", seed, None if full else limits["val"]),
|
|
115
|
+
"test": selected_keys(fingerprints["test"] if full else eligible["test"], "test", seed, None if full else limits["test"]),
|
|
116
|
+
}
|
|
117
|
+
if any(not chosen[fold] for fold in FOLDS):
|
|
118
|
+
raise ValueError("at least one fold has no selected molecules with valid spectra")
|
|
119
|
+
|
|
120
|
+
spectra = {fold: defaultdict(list) for fold in FOLDS}
|
|
121
|
+
seen = {fold: defaultdict(set) for fold in FOLDS}
|
|
122
|
+
duplicates = Counter()
|
|
123
|
+
for row in rows_in(path):
|
|
124
|
+
fold, key = row["fold"], row["inchikey"].strip()
|
|
125
|
+
if key not in chosen[fold]:
|
|
126
|
+
continue
|
|
127
|
+
spectrum = parse_spectrum(row)
|
|
128
|
+
if spectrum is None:
|
|
129
|
+
continue
|
|
130
|
+
fingerprint = spectrum.pop("fingerprint")
|
|
131
|
+
if fingerprint in seen[fold][key]:
|
|
132
|
+
duplicates[fold] += 1
|
|
133
|
+
continue
|
|
134
|
+
seen[fold][key].add(fingerprint)
|
|
135
|
+
if not full and len(spectra[fold][key]) >= 2:
|
|
136
|
+
continue
|
|
137
|
+
spectra[fold][key].append(spectrum)
|
|
138
|
+
|
|
139
|
+
for fold in FOLDS:
|
|
140
|
+
if fold == "train" or not full:
|
|
141
|
+
if any(len(group) < 2 for group in spectra[fold].values()):
|
|
142
|
+
raise ValueError(f"{fold} contains a selected molecule without two distinct spectra")
|
|
143
|
+
|
|
144
|
+
audit = {
|
|
145
|
+
"source": {"dataset": DATASET, "revision": REVISION, "file": FILENAME, "sha256": SHA256},
|
|
146
|
+
"mode": "full" if full else "teaching subset",
|
|
147
|
+
"selection_seed": seed,
|
|
148
|
+
"folds": {
|
|
149
|
+
fold: {
|
|
150
|
+
"source_rows": raw[fold],
|
|
151
|
+
"valid_rows_at_least_three_peaks": valid[fold],
|
|
152
|
+
"source_molecules_after_filter": len(fingerprints[fold]),
|
|
153
|
+
"molecules_with_two_distinct_spectra": len(eligible[fold]),
|
|
154
|
+
"selected_rows": sum(map(len, spectra[fold].values())),
|
|
155
|
+
"selected_molecules": len(spectra[fold]),
|
|
156
|
+
"selected_molecules_with_two_spectra": sum(len(group) >= 2 for group in spectra[fold].values()),
|
|
157
|
+
"retrieval_queries_with_positive": sum(
|
|
158
|
+
len(group) for group in spectra[fold].values() if len(group) >= 2
|
|
159
|
+
),
|
|
160
|
+
"exact_duplicate_spectra_removed": duplicates[fold],
|
|
161
|
+
}
|
|
162
|
+
for fold in FOLDS
|
|
163
|
+
},
|
|
164
|
+
"molecule_overlap_between_folds": 0,
|
|
165
|
+
}
|
|
166
|
+
return spectra, audit
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def pairs_for_epoch(groups, seed: int, epoch: int):
|
|
170
|
+
rng = random.Random(seed + epoch)
|
|
171
|
+
keys = list(groups)
|
|
172
|
+
rng.shuffle(keys)
|
|
173
|
+
return [rng.sample(groups[key], 2) for key in keys]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def pair_batches(pairs, batch_size: int):
|
|
177
|
+
batches = [pairs[start : start + batch_size] for start in range(0, len(pairs), batch_size)]
|
|
178
|
+
if len(batches) > 1 and len(batches[-1]) == 1:
|
|
179
|
+
batches[-2].extend(batches.pop())
|
|
180
|
+
return batches
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def top1(model: UltraMS, groups, *, batch_size: int, device: torch.device):
|
|
184
|
+
records = [record for molecule in groups.values() for record in molecule]
|
|
185
|
+
labels = [record["inchikey"] for record in records]
|
|
186
|
+
counts = Counter(labels)
|
|
187
|
+
embeddings = model.encode_batch(records, batch_size=batch_size)
|
|
188
|
+
vectors = F.normalize(torch.from_numpy(embeddings).to(device), dim=-1)
|
|
189
|
+
correct = queries = 0
|
|
190
|
+
for start in range(0, len(records), 256):
|
|
191
|
+
end = min(start + 256, len(records))
|
|
192
|
+
scores = vectors[start:end] @ vectors.T
|
|
193
|
+
scores[torch.arange(end - start, device=device), torch.arange(start, end, device=device)] = -float("inf")
|
|
194
|
+
nearest = scores.argmax(dim=1).cpu().tolist()
|
|
195
|
+
for offset, neighbour in enumerate(nearest):
|
|
196
|
+
index = start + offset
|
|
197
|
+
if counts[labels[index]] >= 2:
|
|
198
|
+
queries += 1
|
|
199
|
+
correct += labels[index] == labels[neighbour]
|
|
200
|
+
return {"top1": correct / queries, "correct": correct, "queries": queries, "candidates": len(records)}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def write_json(path: Path, content) -> None:
|
|
204
|
+
path.write_text(json.dumps(content, indent=2) + "\n")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def main() -> None:
|
|
208
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
209
|
+
parser.add_argument("--data-file", type=Path, help="Use an already downloaded copy of the pinned TSV")
|
|
210
|
+
parser.add_argument("--output-dir", type=Path, help="Keep run files here; otherwise use a temporary directory")
|
|
211
|
+
parser.add_argument("--full", action="store_true", help="Use all valid spectra, not the teaching subset")
|
|
212
|
+
parser.add_argument("--train-molecules", type=int, default=64)
|
|
213
|
+
parser.add_argument("--val-molecules", type=int, default=24)
|
|
214
|
+
parser.add_argument("--test-molecules", type=int, default=24)
|
|
215
|
+
parser.add_argument("--epochs", type=int, default=2)
|
|
216
|
+
parser.add_argument("--batch-size", type=int, default=8, help="Number of positive pairs per training batch")
|
|
217
|
+
parser.add_argument("--lr", type=float, default=1e-5)
|
|
218
|
+
parser.add_argument("--temperature", type=float, default=0.07)
|
|
219
|
+
parser.add_argument("--seed", type=int, default=42)
|
|
220
|
+
parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
|
|
221
|
+
parser.add_argument("--prepare-only", action="store_true", help="Audit and select data without loading the model")
|
|
222
|
+
args = parser.parse_args()
|
|
223
|
+
if any(value < 1 for value in (args.train_molecules, args.val_molecules, args.test_molecules, args.epochs)) or args.batch_size < 2:
|
|
224
|
+
parser.error("molecule limits and epochs must be positive; batch size needs at least two positive pairs")
|
|
225
|
+
if args.lr <= 0 or args.temperature <= 0:
|
|
226
|
+
parser.error("learning rate and temperature must be positive")
|
|
227
|
+
|
|
228
|
+
output = args.output_dir or Path(tempfile.mkdtemp(prefix="ultrams-massspecgym-"))
|
|
229
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
230
|
+
path = checked_data_file(args.data_file)
|
|
231
|
+
groups, audit = prepare(
|
|
232
|
+
path,
|
|
233
|
+
seed=args.seed,
|
|
234
|
+
full=args.full,
|
|
235
|
+
limits={"train": args.train_molecules, "val": args.val_molecules, "test": args.test_molecules},
|
|
236
|
+
)
|
|
237
|
+
if len(groups["train"]) < 2:
|
|
238
|
+
raise ValueError("contrastive training needs at least two distinct training molecules")
|
|
239
|
+
write_json(output / "data_audit.json", audit)
|
|
240
|
+
config = {
|
|
241
|
+
"pretrained_model": "unsupervised",
|
|
242
|
+
"objective": "in-batch contrastive learning; positives are two distinct spectra with the same MassSpecGym inchikey",
|
|
243
|
+
"evaluation": "same-fold spectrum retrieval Top1, self-match excluded; singleton queries excluded",
|
|
244
|
+
"dataset": audit["source"],
|
|
245
|
+
"mode": audit["mode"],
|
|
246
|
+
"seed": args.seed,
|
|
247
|
+
"epochs": args.epochs,
|
|
248
|
+
"batch_size_in_positive_pairs": args.batch_size,
|
|
249
|
+
"learning_rate": args.lr,
|
|
250
|
+
"temperature": args.temperature,
|
|
251
|
+
"device": args.device,
|
|
252
|
+
"output_dir": str(output.resolve()),
|
|
253
|
+
}
|
|
254
|
+
write_json(output / "config.json", config)
|
|
255
|
+
print(json.dumps(audit["folds"], indent=2), flush=True)
|
|
256
|
+
print(f"Run directory: {output.resolve()}", flush=True)
|
|
257
|
+
if args.prepare_only:
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
random.seed(args.seed)
|
|
261
|
+
np.random.seed(args.seed)
|
|
262
|
+
torch.manual_seed(args.seed)
|
|
263
|
+
device = torch.device(args.device)
|
|
264
|
+
model = UltraMS.from_pretrained("unsupervised", device=device)
|
|
265
|
+
optimizer = torch.optim.AdamW(model.parameters(), lr=args.lr)
|
|
266
|
+
history = []
|
|
267
|
+
best_top1 = -1.0
|
|
268
|
+
for epoch in range(1, args.epochs + 1):
|
|
269
|
+
model.train()
|
|
270
|
+
pairs = pairs_for_epoch(groups["train"], args.seed, epoch)
|
|
271
|
+
sum_loss = 0.0
|
|
272
|
+
for mini in pair_batches(pairs, args.batch_size):
|
|
273
|
+
batch = model.collate([spectrum for pair in mini for spectrum in pair])
|
|
274
|
+
embeddings = model(
|
|
275
|
+
batch["peaks"].to(device), batch["attention_mask"].to(device), batch["precursor_mz"].to(device)
|
|
276
|
+
)
|
|
277
|
+
logits = embeddings @ embeddings.T / args.temperature
|
|
278
|
+
logits = logits.masked_fill(torch.eye(len(embeddings), device=device, dtype=torch.bool), -float("inf"))
|
|
279
|
+
target = torch.arange(len(embeddings), device=device) ^ 1
|
|
280
|
+
loss = F.cross_entropy(logits, target)
|
|
281
|
+
optimizer.zero_grad()
|
|
282
|
+
loss.backward()
|
|
283
|
+
optimizer.step()
|
|
284
|
+
sum_loss += float(loss.detach()) * len(mini)
|
|
285
|
+
validation = top1(model, groups["val"], batch_size=args.batch_size * 2, device=device)
|
|
286
|
+
result = {"epoch": epoch, "train_loss": sum_loss / len(pairs), "validation": validation}
|
|
287
|
+
history.append(result)
|
|
288
|
+
write_json(output / "history.json", history)
|
|
289
|
+
print(json.dumps(result), flush=True)
|
|
290
|
+
if validation["top1"] > best_top1:
|
|
291
|
+
best_top1 = validation["top1"]
|
|
292
|
+
torch.save({"model_state_dict": model.model.state_dict(), "config": model.config}, output / "best.pt")
|
|
293
|
+
|
|
294
|
+
best = UltraMS.from_checkpoint(output / "best.pt", device=device)
|
|
295
|
+
test = top1(best, groups["test"], batch_size=args.batch_size * 2, device=device)
|
|
296
|
+
write_json(output / "test.json", test)
|
|
297
|
+
print("Test on the best validation checkpoint:", json.dumps(test), flush=True)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
if __name__ == "__main__":
|
|
301
|
+
main()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cells": [
|
|
3
|
+
{
|
|
4
|
+
"cell_type": "markdown",
|
|
5
|
+
"id": "introduction",
|
|
6
|
+
"metadata": {},
|
|
7
|
+
"source": "# Fine-tune with PyTorch\n\n[Open in Colab](https://colab.research.google.com/github/Dsadd4/UltraMS/blob/main/cookbook/tutorials/pytorch_finetune.ipynb)\n\nUse UltraMS as a standard `torch.nn.Module` in a training loop. The input file is the [five-spectrum DreaMS example](../../examples/data/NOTICE.md). Run cells from top to bottom."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"cell_type": "markdown",
|
|
11
|
+
"id": "installation",
|
|
12
|
+
"metadata": {},
|
|
13
|
+
"source": "## Install"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"cell_type": "code",
|
|
17
|
+
"id": "install-ultrams",
|
|
18
|
+
"metadata": {},
|
|
19
|
+
"execution_count": null,
|
|
20
|
+
"outputs": [],
|
|
21
|
+
"source": [
|
|
22
|
+
"%pip install -q ultrams\n"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"cell_type": "markdown",
|
|
27
|
+
"id": "read-spectra",
|
|
28
|
+
"metadata": {},
|
|
29
|
+
"source": "## Read spectra"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"cell_type": "code",
|
|
33
|
+
"id": "load-mgf",
|
|
34
|
+
"metadata": {},
|
|
35
|
+
"execution_count": null,
|
|
36
|
+
"outputs": [],
|
|
37
|
+
"source": "from pathlib import Path\nfrom urllib.request import urlretrieve\nimport torch\nfrom ultrams import UltraMS, read_spectra\n\nsample = Path(\"examples/data/example_5_spectra.mgf\")\nif not sample.exists():\n sample = Path(\"example_5_spectra.mgf\")\n if not sample.exists():\n urlretrieve(\n \"https://raw.githubusercontent.com/Dsadd4/UltraMS/main/examples/data/example_5_spectra.mgf\",\n sample,\n )\nspectra = list(read_spectra(sample))\nassert len(spectra) == 5\nprint(f\"Loaded {len(spectra)} MS/MS spectra\")"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"cell_type": "markdown",
|
|
41
|
+
"id": "labels-heading",
|
|
42
|
+
"metadata": {},
|
|
43
|
+
"source": "## Add demonstration labels\n\nThe five targets below are **placeholders made from file order**. They are not measured properties or research labels; replace them with your own labels for a scientific task."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"cell_type": "code",
|
|
47
|
+
"id": "demo-labels",
|
|
48
|
+
"metadata": {},
|
|
49
|
+
"execution_count": null,
|
|
50
|
+
"outputs": [],
|
|
51
|
+
"source": "from torch.utils.data import DataLoader\n\nrecords = [\n {**spectrum, \"target\": index / (len(spectra) - 1)}\n for index, spectrum in enumerate(spectra)\n]\nprint([record[\"target\"] for record in records])"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"cell_type": "markdown",
|
|
55
|
+
"id": "train-heading",
|
|
56
|
+
"metadata": {},
|
|
57
|
+
"source": "## Train encoder and task head"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"cell_type": "code",
|
|
61
|
+
"id": "native-training",
|
|
62
|
+
"metadata": {},
|
|
63
|
+
"execution_count": null,
|
|
64
|
+
"outputs": [],
|
|
65
|
+
"source": "device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nmodel = UltraMS.from_pretrained(\"unsupervised\", device=device).train()\nhead = torch.nn.Linear(model.embedding_dim, 1).to(device)\nloader = DataLoader(records, batch_size=2, collate_fn=model.batch_converter())\noptimizer = torch.optim.AdamW([*model.parameters(), *head.parameters()], lr=1e-5)\n\nlosses = []\nfor batch in loader:\n batch = {name: value.to(device) for name, value in batch.items()}\n prediction = head(model(batch[\"peaks\"], batch[\"attention_mask\"], batch[\"precursor_mz\"]))\n loss = torch.nn.functional.mse_loss(prediction.squeeze(-1), batch[\"target\"])\n optimizer.zero_grad()\n loss.backward()\n optimizer.step()\n losses.append(float(loss.detach()))\nprint(\"Batch losses (demonstration only):\", losses)"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"cell_type": "markdown",
|
|
69
|
+
"id": "save-heading",
|
|
70
|
+
"metadata": {},
|
|
71
|
+
"source": "## Save the run configuration and history\n\nThe weights trained on placeholder labels are only a code demonstration."
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"cell_type": "code",
|
|
75
|
+
"id": "save-history",
|
|
76
|
+
"metadata": {},
|
|
77
|
+
"execution_count": null,
|
|
78
|
+
"outputs": [],
|
|
79
|
+
"source": "import json\n\noutput = Path(\"ultrams_finetune_demo.json\")\noutput.write_text(json.dumps({\n \"model\": \"unsupervised\",\n \"target\": \"placeholder labels from file order\",\n \"batch_size\": 2,\n \"learning_rate\": 1e-5,\n \"loss_per_batch\": losses,\n}, indent=2) + \"\\n\")\nprint(output.resolve())"
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
"metadata": {
|
|
83
|
+
"kernelspec": {
|
|
84
|
+
"display_name": "Python 3",
|
|
85
|
+
"language": "python",
|
|
86
|
+
"name": "python3"
|
|
87
|
+
},
|
|
88
|
+
"language_info": {
|
|
89
|
+
"name": "python"
|
|
90
|
+
},
|
|
91
|
+
"colab": {
|
|
92
|
+
"provenance": []
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"nbformat": 4,
|
|
96
|
+
"nbformat_minor": 5
|
|
97
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cells": [
|
|
3
|
+
{
|
|
4
|
+
"cell_type": "markdown",
|
|
5
|
+
"id": "introduction",
|
|
6
|
+
"metadata": {},
|
|
7
|
+
"source": "# Search similar MS/MS spectra\n\n[Open in Colab](https://colab.research.google.com/github/Dsadd4/UltraMS/blob/main/cookbook/tutorials/spectrum_search.ipynb)\n\nRank the other spectra in the same example file by cosine similarity using the UltraMS Search model. The input file is the [five-spectrum DreaMS example](../../examples/data/NOTICE.md). Run cells from top to bottom."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"cell_type": "markdown",
|
|
11
|
+
"id": "installation",
|
|
12
|
+
"metadata": {},
|
|
13
|
+
"source": "## Install"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"cell_type": "code",
|
|
17
|
+
"id": "install-ultrams",
|
|
18
|
+
"metadata": {},
|
|
19
|
+
"execution_count": null,
|
|
20
|
+
"outputs": [],
|
|
21
|
+
"source": [
|
|
22
|
+
"%pip install -q ultrams\n"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"cell_type": "markdown",
|
|
27
|
+
"id": "read-spectra",
|
|
28
|
+
"metadata": {},
|
|
29
|
+
"source": "## Read spectra"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"cell_type": "code",
|
|
33
|
+
"id": "load-mgf",
|
|
34
|
+
"metadata": {},
|
|
35
|
+
"execution_count": null,
|
|
36
|
+
"outputs": [],
|
|
37
|
+
"source": "from pathlib import Path\nfrom urllib.request import urlretrieve\nimport torch\nfrom ultrams import UltraMS, read_spectra\n\nsample = Path(\"examples/data/example_5_spectra.mgf\")\nif not sample.exists():\n sample = Path(\"example_5_spectra.mgf\")\n if not sample.exists():\n urlretrieve(\n \"https://raw.githubusercontent.com/Dsadd4/UltraMS/main/examples/data/example_5_spectra.mgf\",\n sample,\n )\nspectra = list(read_spectra(sample))\nassert len(spectra) == 5\nprint(f\"Loaded {len(spectra)} MS/MS spectra\")"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"cell_type": "markdown",
|
|
41
|
+
"id": "rank-heading",
|
|
42
|
+
"metadata": {},
|
|
43
|
+
"source": "## Encode and rank\n\nThis small in-file ranking illustrates the Search representation. It is not an UltraAtlas search or a retrieval benchmark."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"cell_type": "code",
|
|
47
|
+
"id": "cosine-ranking",
|
|
48
|
+
"metadata": {},
|
|
49
|
+
"execution_count": null,
|
|
50
|
+
"outputs": [],
|
|
51
|
+
"source": "import numpy as np\n\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nmodel = UltraMS.from_pretrained(\"search\", device=device)\nembeddings = model.encode_batch(spectra, batch_size=5)\nunit = embeddings / np.maximum(np.linalg.norm(embeddings, axis=1, keepdims=True), 1e-12)\nquery_index = 0\nscores = unit @ unit[query_index]\nranked = [index for index in np.argsort(-scores) if index != query_index]\nprint(\"Query:\", spectra[query_index][\"id\"])\nfor rank, index in enumerate(ranked, start=1):\n print(rank, spectra[index][\"id\"], float(scores[index]))"
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"metadata": {
|
|
55
|
+
"kernelspec": {
|
|
56
|
+
"display_name": "Python 3",
|
|
57
|
+
"language": "python",
|
|
58
|
+
"name": "python3"
|
|
59
|
+
},
|
|
60
|
+
"language_info": {
|
|
61
|
+
"name": "python"
|
|
62
|
+
},
|
|
63
|
+
"colab": {
|
|
64
|
+
"provenance": []
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"nbformat": 4,
|
|
68
|
+
"nbformat_minor": 5
|
|
69
|
+
}
|
|
@@ -42,6 +42,21 @@ The `target` field is a numeric training value used by `model.batch_converter()`
|
|
|
42
42
|
|
|
43
43
|
`model.encode(...).embedding` is a NumPy vector for inference. `model(peaks, attention_mask, precursor_mz)` returns a `[batch, embedding_dim]` PyTorch tensor with gradients for training. See [model selection](model-selection.md) for the representation returned by each checkpoint.
|
|
44
44
|
|
|
45
|
+
To inspect representations for individual spectral peaks, pass `return_peaks=True`:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
result = model.encode(
|
|
49
|
+
mz=[150.0, 100.1, 121.1],
|
|
50
|
+
intensity=[35, 20, 100],
|
|
51
|
+
precursor_mz=301.2,
|
|
52
|
+
return_peaks=True,
|
|
53
|
+
)
|
|
54
|
+
print(result.peak_mz) # processed peak m/z, sorted ascending
|
|
55
|
+
print(result.peak_embeddings.shape) # (3, 1024) for the Unsupervised model
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`peak_embeddings[i]` is the encoder's final contextual representation for `peak_mz[i]` and `peak_intensity[i]`. These arrays reflect the peak selection, intensity normalization and sorting described above. They are returned only when requested.
|
|
59
|
+
|
|
45
60
|
## Spectrum files
|
|
46
61
|
|
|
47
62
|
For MGF or `.mgf.gz`, UltraMS reads each `BEGIN IONS` block, uses `PEPMASS` for precursor-ion `m/z`, and takes the spectrum ID from `TITLE`, `NAME`, or `SCANS` when available. Extra columns after a spectral peak's `m/z` and intensity are ignored. The optional `ultrams[io]` installation adds mzML input; only MS2 spectra are read.
|
|
@@ -13,3 +13,16 @@ python -m pip install -e .
|
|
|
13
13
|
| Rank spectra by cosine similarity | `python examples/spectrum_search.py` |
|
|
14
14
|
|
|
15
15
|
Each command downloads its pretrained checkpoint from Hugging Face on first use. `batch_embeddings.py` saves an `.npz` containing spectrum IDs and one embedding per input spectrum. `spectrum_search.py` saves a CSV of ranked matches; its scores illustrate spectrum similarity and are not a benchmark. `pytorch_finetune.py` uses **placeholder regression targets based on file order** solely to show how to train the encoder and a task head. Replace these targets with measured labels for research. By default, output files go to a newly created temporary directory printed by each command; use `--output` or `--output-dir` to choose a persistent destination.
|
|
16
|
+
|
|
17
|
+
The PyTorch example saves both the encoder and task head in `model.pt`. Load them for prediction with:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import torch
|
|
21
|
+
from ultrams import UltraMS
|
|
22
|
+
|
|
23
|
+
weights = torch.load("/path/to/model.pt", map_location="cpu", weights_only=True)
|
|
24
|
+
model = UltraMS.from_pretrained("unsupervised").eval()
|
|
25
|
+
head = torch.nn.Linear(model.embedding_dim, 1).eval()
|
|
26
|
+
model.load_state_dict(weights["encoder"])
|
|
27
|
+
head.load_state_dict(weights["head"])
|
|
28
|
+
```
|
|
@@ -47,6 +47,8 @@ def main() -> None:
|
|
|
47
47
|
|
|
48
48
|
output_dir = args.output_dir or Path(tempfile.mkdtemp(prefix="ultrams-finetune-"))
|
|
49
49
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
checkpoint = output_dir / "model.pt"
|
|
51
|
+
torch.save({"encoder": model.state_dict(), "head": head.state_dict()}, checkpoint)
|
|
50
52
|
record = {
|
|
51
53
|
"model": "unsupervised",
|
|
52
54
|
"target": "demonstration labels from input order, scaled to [0, 1]",
|
|
@@ -56,6 +58,7 @@ def main() -> None:
|
|
|
56
58
|
}
|
|
57
59
|
(output_dir / "training.json").write_text(json.dumps(record, indent=2) + "\n")
|
|
58
60
|
print(f"Trained on {len(spectra)} spectra with demonstration targets; final loss: {history[-1]:.4f}")
|
|
61
|
+
print(checkpoint)
|
|
59
62
|
print(output_dir / "training.json")
|
|
60
63
|
|
|
61
64
|
|
|
@@ -29,6 +29,9 @@ class SpectrumEmbedding:
|
|
|
29
29
|
fusion: np.ndarray
|
|
30
30
|
projection: np.ndarray | None = None
|
|
31
31
|
rt_seconds: float | None = None
|
|
32
|
+
peak_embeddings: np.ndarray | None = None
|
|
33
|
+
peak_mz: np.ndarray | None = None
|
|
34
|
+
peak_intensity: np.ndarray | None = None
|
|
32
35
|
|
|
33
36
|
@property
|
|
34
37
|
def embedding(self) -> np.ndarray:
|
|
@@ -323,7 +326,18 @@ class UltraMS(torch.nn.Module):
|
|
|
323
326
|
|
|
324
327
|
@torch.inference_mode()
|
|
325
328
|
def encode(
|
|
326
|
-
self, mz: Any, intensity: Any, *, precursor_mz: float
|
|
329
|
+
self, mz: Any, intensity: Any, *, precursor_mz: float, return_peaks: bool = False
|
|
330
|
+
) -> SpectrumEmbedding:
|
|
331
|
+
"""Encode one MS/MS spectrum; optionally return aligned peak embeddings."""
|
|
332
|
+
was_training = self.training
|
|
333
|
+
self.eval()
|
|
334
|
+
try:
|
|
335
|
+
return self._encode_one(mz, intensity, precursor_mz, return_peaks)
|
|
336
|
+
finally:
|
|
337
|
+
self.train(was_training)
|
|
338
|
+
|
|
339
|
+
def _encode_one(
|
|
340
|
+
self, mz: Any, intensity: Any, precursor_mz: float, return_peaks: bool
|
|
327
341
|
) -> SpectrumEmbedding:
|
|
328
342
|
peaks, attention = _prepare_spectrum(mz, intensity, self.input_max_peaks)
|
|
329
343
|
if not np.isfinite(precursor_mz) or precursor_mz <= 0:
|
|
@@ -356,4 +370,7 @@ class UltraMS(torch.nn.Module):
|
|
|
356
370
|
fusion=fusion[0].cpu().numpy(),
|
|
357
371
|
projection=projected[0].cpu().numpy() if projected is not None else None,
|
|
358
372
|
rt_seconds=rt_seconds,
|
|
373
|
+
peak_embeddings=peak_hidden[0].float().cpu().numpy() if return_peaks else None,
|
|
374
|
+
peak_mz=peaks[:, 0].copy() if return_peaks else None,
|
|
375
|
+
peak_intensity=peaks[:, 1].copy() if return_peaks else None,
|
|
359
376
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ultrams
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Pretrained UltraMS models for mass spectrometry
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://github.com/Dsadd4/UltraMS
|
|
@@ -30,7 +30,8 @@ python -m pip install ultrams
|
|
|
30
30
|
| Embed an MGF or mzML file | [File command](#embed-a-spectrum-file) |
|
|
31
31
|
| Train on my labelled spectra | [PyTorch example](#fine-tune-with-pytorch) · [complete MGF example](https://github.com/Dsadd4/UltraMS/blob/main/examples/pytorch_finetune.py) |
|
|
32
32
|
| Compare spectra | [Search example](https://github.com/Dsadd4/UltraMS/blob/main/examples/spectrum_search.py) |
|
|
33
|
-
|
|
|
33
|
+
| Learn in a notebook | [UltraMS tutorials](https://github.com/Dsadd4/UltraMS/tree/main/cookbook/tutorials) |
|
|
34
|
+
| Inspect or run pretraining | [Training code](https://github.com/Dsadd4/UltraMS/blob/main/training/README.md) |
|
|
34
35
|
|
|
35
36
|
## Embed a spectrum
|
|
36
37
|
|
|
@@ -48,6 +49,8 @@ print(embedding.shape) # (1024,)
|
|
|
48
49
|
|
|
49
50
|
The first call downloads the selected checkpoint from Hugging Face. Use `device="cuda"` or `device="mps"` with `from_pretrained` to run on an available accelerator.
|
|
50
51
|
|
|
52
|
+
The three checkpoints are collected on [Hugging Face](https://huggingface.co/collections/dsadd4/ultrams-6ab4cfaa860cbbd1f9a6b9b6).
|
|
53
|
+
|
|
51
54
|
## Embed a spectrum file
|
|
52
55
|
|
|
53
56
|
```bash
|
|
@@ -87,6 +90,8 @@ for batch in loader:
|
|
|
87
90
|
|
|
88
91
|
The [complete fine-tuning example](https://github.com/Dsadd4/UltraMS/blob/main/examples/pytorch_finetune.py) reads an MGF file and saves training history. For a shorter route, use `model.finetune(labelled_spectra, task="regression")` with a `label` field; it saves weights, configuration, and losses. See [spectrum input](https://github.com/Dsadd4/UltraMS/blob/main/docs/data-format.md) for the input format.
|
|
89
92
|
|
|
93
|
+
For measured molecular labels, the [MassSpecGym contrastive tutorial](https://github.com/Dsadd4/UltraMS/blob/main/cookbook/tutorials/MASSSPECGYM_CONTRASTIVE.md) downloads the dataset, keeps its train/validation/test folds, fine-tunes the encoder, and reloads the selected checkpoint.
|
|
94
|
+
|
|
90
95
|
## Choose a model
|
|
91
96
|
|
|
92
97
|
| Model name | Representation | Dimension | Spectral peak limit | Weights |
|
|
@@ -100,3 +105,7 @@ Load any of them with `UltraMS.from_pretrained("name")`, or load a downloaded `m
|
|
|
100
105
|
## Pretraining
|
|
101
106
|
|
|
102
107
|
The [UltraMSdata pretraining package](https://github.com/Dsadd4/UltraMS/blob/main/training/README.md) contains the current model architecture, training entry points, configuration and history outputs. It installs separately from the inference package.
|
|
108
|
+
|
|
109
|
+
## Cite
|
|
110
|
+
|
|
111
|
+
Citation metadata for this software is in [CITATION.cff](https://github.com/Dsadd4/UltraMS/blob/main/CITATION.cff).
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
CITATION.cff
|
|
1
2
|
LICENSE
|
|
2
3
|
MANIFEST.in
|
|
3
4
|
README.md
|
|
4
5
|
pyproject.toml
|
|
6
|
+
cookbook/tutorials/MASSSPECGYM_CONTRASTIVE.md
|
|
7
|
+
cookbook/tutorials/README.md
|
|
8
|
+
cookbook/tutorials/embed.ipynb
|
|
9
|
+
cookbook/tutorials/massspecgym_contrastive.py
|
|
10
|
+
cookbook/tutorials/pytorch_finetune.ipynb
|
|
11
|
+
cookbook/tutorials/spectrum_search.ipynb
|
|
5
12
|
docs/data-format.md
|
|
6
13
|
docs/model-selection.md
|
|
7
14
|
examples/README.md
|
|
@@ -53,6 +53,21 @@ class ModelAPITests(unittest.TestCase):
|
|
|
53
53
|
actual = self.model.encode_batch([], batch_size=2)
|
|
54
54
|
self.assertEqual(actual.shape, (0, self.model.embedding_dim))
|
|
55
55
|
|
|
56
|
+
def test_peak_embeddings_align_with_selected_sorted_peaks(self) -> None:
|
|
57
|
+
mz = [180, 100, 150, 110, 140, 120, 170, 130, 190, 160]
|
|
58
|
+
intensity = [8, 1, 5, 2, 4, 3, 7, 6, 10, 9]
|
|
59
|
+
result = self.model.encode(mz, intensity, precursor_mz=301.2, return_peaks=True)
|
|
60
|
+
self.assertEqual(result.peak_embeddings.shape, (8, 16))
|
|
61
|
+
np.testing.assert_array_equal(
|
|
62
|
+
result.peak_mz,
|
|
63
|
+
np.asarray([120, 130, 140, 150, 160, 170, 180, 190], dtype=np.float32),
|
|
64
|
+
)
|
|
65
|
+
np.testing.assert_array_equal(
|
|
66
|
+
result.peak_intensity,
|
|
67
|
+
np.asarray([3, 6, 4, 5, 9, 7, 8, 10], dtype=np.float32) / 10,
|
|
68
|
+
)
|
|
69
|
+
self.assertIsNone(self.model.encode(mz, intensity, precursor_mz=301.2).peak_embeddings)
|
|
70
|
+
|
|
56
71
|
def test_real_mgf_to_npz_with_local_checkpoint(self) -> None:
|
|
57
72
|
source = Path(__file__).resolve().parents[1] / "examples/data/example_5_spectra.mgf"
|
|
58
73
|
with tempfile.TemporaryDirectory() as directory:
|
ultrams-0.2.0/MANIFEST.in
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|