File size: 16,002 Bytes
9e753c9
53f9a35
 
 
 
 
 
 
 
 
 
 
 
9e753c9
53f9a35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
---
license: cc-by-nc-4.0
base_model: neuralbioinfo/prokbert-mini-long
library_name: pytorch
pipeline_tag: feature-extraction
tags:
  - biology
  - genomics
  - bacterial-genomics
  - streptococcus-pneumoniae
  - serotyping
  - novelty-detection
  - out-of-distribution-detection
---

# ALLCAPS — pneumococcal *cps* locus serotyping and novel-serotype detection

ALLCAPS takes a *Streptococcus pneumoniae* capsular biosynthetic locus (CBL, *cps*) and predicts

1. whether the sequence is a *cps* locus at all,
2. its serotype, and
3. whether that serotype is **novel** — unlike anything seen in training.

Code, pipeline and issue tracker: <https://github.com/bacpop/pneumococcal-serotyping>

---

## ⚠️ License — please read before use

**These weights are released under [CC-BY-NC-4.0](https://creativecommons.org/licenses/by-nc/4.0/): non-commercial use only.**

ALLCAPS is a classification head that runs on top of
[ProkBERT-mini-long](https://huggingface.co/neuralbioinfo/prokbert-mini-long), which is itself
distributed under CC-BY-NC-4.0. ProkBERT is required at inference time — every query sequence is
embedded by ProkBERT before ALLCAPS sees it — so **the non-commercial restriction applies to the
pipeline as a whole**, and we pass it through rather than advertise a freedom we cannot grant.

Note the two licenses that apply to different parts of this project:

| Component | License |
|---|---|
| ALLCAPS **source code** (the GitHub repository, and `modeling_allcaps.py` here) | MIT |
| ALLCAPS **weights** (this repository) | CC-BY-NC-4.0 (inherited from ProkBERT) |
| ProkBERT-mini-long weights (downloaded separately at runtime) | CC-BY-NC-4.0 |
| ProkBERT source code | MIT |

If you need commercial use, you must take it up with the ProkBERT authors — our code license
cannot lift their restriction. Attribution to both ALLCAPS and ProkBERT is required under CC-BY-NC;
see [Citation](#citation).

> The `base_model` field above records a *dependency*, not a fine-tune. ALLCAPS does not modify,
> contain or redistribute any ProkBERT weights — ProkBERT is loaded frozen, in `eval()` mode, and
> used purely as a feature extractor. This repository holds only ALLCAPS' own parameters.

---

## Model description

Input DNA is split into **4 kbp chunks with 50% overlap** and embedded with frozen ProkBERT-mini-long
(384-d per chunk). Those chunk embeddings are then processed by ALLCAPS' own encoder:

```
chunk embeddings (L, 384)
  → + learned positional embeddings
  → TransformerEncoder            (1 layer, nhead=4)
  → masked mean-pool              (padding-aware)
  → projection MLP → L2-normalised 128-d locus embedding  z
  → three linear heads:
        cbl_classifier        (2 classes: capsule y/n)
        serotype_classifier   (82 serotypes)
        genogroup_classifier  (53 genogroups — NOT TRAINED, see below)
```

Model class `TransformerTriHeadLR`. A single locus embedding `z` serves both classification and
novelty detection.

| | |
|---|---|
| Base encoder | `neuralbioinfo/prokbert-mini-long` (frozen, not included here) |
| Chunk size / stride | 4000 bp / 50% overlap |
| Max input length | 30,000 bp |
| Base embedding dim | 384 |
| Locus embedding dim | 128 (L2-normalised) |
| Trainable parameters | 2,013,833 |
| Serotype classes | 82 |
| Genogroup classes | 53 (head untrained — see below) |

### ⚠️ The genogroup head is not trained

The architecture has three heads; the released model trains two. The genogroup head was found to
add nothing to serotype or novelty performance, so it was trained with loss weight 0. It still
emits logits, and **those logits are meaningless** — the head sits at **0.8% accuracy over 53
classes**, below the 1.9% you would get by guessing.

Do not read a genogroup off this model. If you want the genogroup of a call, derive it from the
predicted serotype, or use the `nn_serotype` the novelty detector returns (which comes from the
training data, not from this head).

### Novel-serotype detection

After a query is classified as a *cps* locus, its pooled embedding `z` is compared by **cosine
distance** to every training *cps* embedding. The locus is called **novel** when the distance to
its **nearest** training neighbour (k = 1) exceeds

```
T = 0.014824147932735014
```

which is the **95th percentile of the training leave-one-out 1-NN distances** — a threshold
derived without ever looking at novel data. The index shipped here holds **54,102** training locus
embeddings with their serotype labels, so the report also names the closest known serotype: a novel
locus is placed in a neighbourhood rather than merely rejected.

An **energy** score (`E = −T·logsumexp(logits/T)`) is available as a reference baseline
(`energy_summary.json` carries its calibrated percentiles). Across 98 leave-one-serotype-out folds,
kNN beat energy on every fold-level metric, so kNN is the deployed detector.

> **Distances must be computed in float64.** In float32, `1 − x·y` cancels below machine epsilon for
> the many near-identical loci in this dataset, quantising ~73% of in-distribution distances toward
> zero and costing ~0.015 AUROC. `modeling_allcaps.py` already does this; if you reimplement the
> detector, don't lose it.

---

## Files in this repository

| File | Contents |
|---|---|
| `model.safetensors` | The 2.0M ALLCAPS parameters |
| `config.json` | Architecture + the chunking parameters inference must match |
| `labels.json` | `serotype_to_idx` (82) and `genogroup_to_idx` (53) |
| `knn_index.npz` | Novelty index: 54,102 × 128 training embeddings, their sample keys and serotypes, and the leave-one-out distance distribution |
| `knn_config.json` | The deployed operating point: k=1, cosine, p95, and the resolved threshold |
| `energy_summary.json` | Calibrated percentiles for the energy baseline |
| `modeling_allcaps.py` | Self-contained inference — architecture, kNN detector, FASTA → calls. Loads `model.safetensors`, falling back to `transformer_model.pth` |
| `transformer_model.pth` | The same weights as a `torch.save` checkpoint — self-describing (`model_config`, `serotype_to_idx`), and the file the `ALLCAPS` CLI and the pipeline load |
| `assets/dexB_aliA_ATCC700669.fasta` | The flanking genes used to cut a *cps* locus out of an assembly |
| `eval/` | Classification reports, confusion matrices, CV summary, and the 98-fold novelty report |

The `knn_index.npz` carries training contig identifiers (`cbl|Public_ID#Contig_ID`, the
pipeline's own record keys) and their serotypes. These are public GPS/ENA accessions.

## How to use

### The `ALLCAPS` CLI (recommended)

The CLI takes **whole assemblies**, cuts the *cps* locus out for you, and downloads
everything in this repository on first run (cached under `~/.cache/allcaps`, override with
`ALLCAPS_CACHE`):

```bash
git clone https://github.com/bacpop/pneumococcal-serotyping && cd ALLCAPS
mamba env create -f environment-cpu.yml      # or environment-gpu.yml on linux-64 + CUDA
conda activate allcaps && pip install .

ALLCAPS predict --input samples.txt --extract align --output results/
```

`samples.txt` lists one assembly FASTA per line (gzip is fine); bare paths work too —
`ALLCAPS predict a.fasta b.fasta --extract align`. The run writes `results/predictions.tsv`
and prints the same table, one row per contig, carrying `serotype`,
`serotype_confidence`, `is_novel_knn`, `nn_serotype`, `nn_distance` and a plain-language
`note`.

`--extract align` cuts between the `dexB`/`aliA` flanking genes with minimap2 — this is how
the model was trained, and what you want. `--extract scan` embeds rolling windows across the
whole assembly instead; it needs no flanking genes but is orders of magnitude slower.

To pin this repository explicitly, or to use a model you trained yourself:

```bash
ALLCAPS predict --input samples.txt --extract align --output results/ \
    --hf-repo bacpop/ALLCAPS --revision main        # or:
ALLCAPS predict --input samples.txt --extract align --output results/ \
    --model transformer_model.pth \
    --knn-index knn_index.npz \
    --energy-summary energy_summary.json \
    --offline
```

The three `--model` / `--knn-index` / `--energy-summary` files are exactly the ones in this
repository, and exactly what `ALLCAPS train` writes into its run directory.

### Standalone, without the repository

`modeling_allcaps.py` here is self-contained — architecture, kNN detector and FASTA → calls
in one file, needing only torch, transformers, numpy, safetensors and biopython. It takes
**already-cut loci**, not whole genomes.

```bash
pip install torch 'transformers<5' numpy biopython safetensors
```

```python
from huggingface_hub import snapshot_download
from modeling_allcaps import ALLCAPS          # from this repository

path  = snapshot_download("bacpop/ALLCAPS")
model = ALLCAPS.from_pretrained(path, device="cpu")

for call in model.predict_fasta("my_loci.fasta"):
    print(call.record_id, call.serotype, call.confidence,
          "NOVEL" if call.is_novel else call.nn_serotype)
```

Or from the command line:

```bash
python modeling_allcaps.py my_loci.fasta --model_dir . --output calls.csv
```

ProkBERT is fetched from the Hub automatically on first use.

**Input must be cut loci, not whole genomes.** Extract the region between the flanking
`dexB`/`aliA` genes first — `assets/dexB_aliA_ATCC700669.fasta` is the query file for that,
and `allcaps.data_locus_cutter` in the GitHub repository does the cutting. The `ALLCAPS`
CLI above does it for you.

### The pipeline modules directly

Useful when you want the intermediate files. Run as modules from `src/`:

```bash
cd src

python -m allcaps.trihead.process_trihead_query \
    --query my_loci.fasta --model_path transformer_model.pth \
    --energy_summary energy_summary.json --output_dir out/ --device cpu

python -m allcaps.knn_ood predict \
    --input_type query --embeddings out/query_embeddings.npz \
    --knn_index knn_index.npz --threshold_percentile 95.0 --max_k 5 \
    --output out/knn_query_distances.csv
```

| Output | Contents |
|---|---|
| `query_results.csv` | Serotype calls, confidence, `is_novel_energy` (the baseline) |
| `knn_query_distances.csv` | The deployed novelty call: `is_novel_knn`, distance, nearest known serotype |

`ALLCAPS predict` runs exactly these two stages and joins their output into one table.

`modeling_allcaps.py` and the pipeline agree to within 1e-15 on distances and exactly on
novelty flags. They can disagree on *which* neighbour is reported when several training loci
sit at identical distance (~0.4% of queries, all exact ties) — the tie-break order differs,
the distance does not.

---

## Intended use and limitations

**Intended for** research and public-health genomic surveillance of *S. pneumoniae*: in-silico
serotyping of assembled genomes, and triage of candidate novel capsular types for follow-up.

**Not intended for** clinical diagnosis or treatment decisions. This model is research software; it
has not been validated as a diagnostic device, and the non-commercial license precludes that use in
any case.

**Limitations**

- Trained on *S. pneumoniae* only — behaviour on other streptococci or other species is undefined,
  though the capsule y/n head is designed to reject non-*cps* input.
- Serotypes with fewer than 5 distinct samples were not kept as resolvable classes, so rare types
  may surface as "novel" rather than as themselves. 21 of the 98 serotype labels in the novelty
  index are represented by fewer than 20 contigs.
- Class support is very uneven: the largest serotype contributes 5,429 training contigs, the
  smallest 1.
- A *cps* locus is frequently split across two contigs in a draft assembly. Metrics here are **per
  contig**, not per sample.
- Novelty is a distance threshold, not proof. A "novel" call is a prompt to look, not a discovery.
- Training data reflects the geographic and temporal sampling of the GPS/Monocle collection.
- The genogroup head is untrained (see above).

## Training data

*S. pneumoniae* assemblies from the **GPS (Global Pneumococcal Sequencing) / Monocle** collection.

| | |
|---|---|
| Training contigs | 119,909, of which 54,102 capsulated |
| Training assemblies with a *cps* locus | 34,125 |
| Held-out test contigs | 13,337 (6,014 capsulated) across 7,598 assemblies |
| Serotype labels present | 98 (82 resolvable as classes) |
| Collection version / date | <!-- TODO: Monocle metadata snapshot date and accession list --> |

Train/test splitting is grouped **by sample, never by contig**, so sibling contigs of one assembly
never straddle the boundary; a runtime assertion enforces it. Serotypes with `< 5` distinct samples
go entirely to train, so every test serotype is also represented in training.

Trained for 100 epochs, batch size 128, AdamW at lr 1e-3, with 5-fold stratified CV followed by a
refit on all data. Full hyperparameters: see [TRAINING.md](https://github.com/bacpop/pneumococcal-serotyping/blob/main/TRAINING.md) in the GitHub repository, or train your own with `ALLCAPS train --input manifest.csv --output run/`.

## Evaluation

**Capsule detection** (training set, per contig, n = 106,909): precision, recall and F1 all 1.00;
weighted F1 0.9997.

**Closed-set serotyping**

| Split | Accuracy | Macro F1 | Weighted F1 | n |
|---|---|---|---|---|
| 5-fold CV (mean ± sd) | 0.952 ± 0.003 | — | — | 23,982/fold |
| Held-out GPS test | 0.96 | 0.81 | 0.95 | 5,833 |
| UK benchmark (external) | 0.89 | 0.69 | 0.88 | 3,012 |

The macro/weighted gap is the long tail: rare serotypes carry most of the errors.

**Novel-serotype detection**, 98 leave-one-serotype-out folds — each fold retrains with one
serotype removed, so that serotype is genuinely unseen:

| Detector | AUROC mean | AUROC median | TPR@5%FPR mean | TPR@5%FPR median | folds won |
|---|---|---|---|---|---|
| **kNN (deployed)** | **0.945** | **0.967** | **0.635** | **0.777** | 95/98 |
| energy (baseline) | 0.836 | 0.884 | 0.394 | 0.310 | 3/98 |

95% CI on mean AUROC: kNN [0.936, 0.955], energy [0.808, 0.864] — non-overlapping.

**Threshold calibration.** The p95 threshold is set on training leave-one-out distances, and it
transfers: 5.00% of training loci are flagged (by construction) and **4.89%** of *held-out*
capsulated test loci — all of which are in-distribution, so that is the false-positive rate. Of
held-out non-capsulated contigs, 99.9% are flagged, which is the correct behaviour: non-*cps*
sequence is far from every *cps* embedding.

Per-fold and per-genogroup breakdowns are in `eval/novel_detection_loo_report.md`.

## Citation

If you use ALLCAPS, please cite both ALLCAPS and ProkBERT.

```bibtex
@article{allcaps,
  title   = {ALLCAPS: a scalable and accurate deep-learning method for in silico pneumococcal serotyping},
  author  = {Tajmirriahi, Alireza and Mateeva, Teodora and Russell, Matthew and Lorenz, Oliver
             and Kim, Soo-Jeong and Lo, Stephanie and Bentley, Stephen and Lees, John
             and Horsfield, Sam},
  year    = {2026},
  journal = {bioRxiv},
  doi     = {<!-- TODO: DOI on preprint -->}
}
```

```bibtex
@article{ligeti2024prokbert,
  title   = {ProkBERT family: genomic language models for microbiome applications},
  author  = {Ligeti, Bal{\'a}zs and Szepesi-Nagy, Istv{\'a}n and Bodn{\'a}r, Bal{\'a}zs
             and Ligeti-Nagy, No{\'e}mi and Juh{\'a}sz, J{\'a}nos},
  journal = {Frontiers in Microbiology},
  volume  = {14},
  pages   = {1331233},
  year    = {2024},
  doi     = {10.3389/fmicb.2023.1331233}
}
```

## Acknowledgements

Built on [ProkBERT](https://github.com/nbrg-ppcu/prokbert) by the Neural Bioinformatics Research
Group, Pázmány Péter Catholic University. Training data from the Global Pneumococcal Sequencing
project.

## Contact

[Alireza Tajmirriahi](https://github.com/AlirezaT99) and
[Sam Horsfield](https://github.com/samhorsfield96) — please open an issue on
[the repository](https://github.com/bacpop/pneumococcal-serotyping/issues).