Data: inputs, labels, and splits

Everything below is produced by projects/grounding-multimodal/scripts/prepare_data.py, which writes a single table to data/processed/deeploc_annotated.parquet, one row per protein. Raw downloads and processed tables are gitignored; the script regenerates them. Counts on this page come from the build of 2026-07-29.

Labels: where the ground truth comes from

Labels are the subcellular compartment assignments from DeepLoc 1.0, a published benchmark whose labels derive from curated UniProt subcellular location annotations. Using an existing benchmark rather than re-deriving labels from UniProt means the label definitions and the test partition are someone else's published choices, which makes the numbers comparable to prior work.

Source: https://services.healthtech.dtu.dk/services/DeepLoc-1.0/deeploc_data.fasta, downloaded 2026-07-29. It needs no license form.

Each FASTA header encodes the accession, the compartment, a membrane or soluble code, and whether the protein belongs to the official test partition:

>Q9H400 Cell.membrane-M test
>Q5I0E9 Cytoplasm-S

The ten compartments and their frequencies after preprocessing:

Compartment Proteins Share
Nucleus 4,043 29.2%
Cytoplasm 2,542 18.3%
Extracellular 1,973 14.2%
Mitochondrion 1,510 10.9%
Cell membrane 1,340 9.7%
Endoplasmic reticulum 862 6.2%
Plastid 757 5.5%
Golgi apparatus 356 2.6%
Lysosome/Vacuole 321 2.3%
Peroxisome 154 1.1%

The imbalance is roughly 26-fold between the largest and smallest class, which is why macro-F1 rather than accuracy carries the weight in evaluation.

One preprocessing decision worth stating

Of 14,004 proteins in the file, 146 are annotated in both cytoplasm and nucleus (Cytoplasm-Nucleus). These are dropped, leaving 13,858 single-label proteins. Dropping them keeps the task honestly single-label rather than quietly forcing dual-localized proteins into one compartment. It costs 1.0% of the data, and the count is reported by the build script rather than absorbed silently. Treating localization as multi-label is a legitimate alternative and a possible follow-up.

Inputs

Each protein has one sequence input and several text inputs. All encoders are frozen: they are used as fixed feature extractors, and only a small head is trained. This keeps every experiment cheap enough to iterate on a laptop and means differences between arms come from the inputs, not from different amounts of training.

Sequence

The amino-acid sequence, from the same DeepLoc FASTA, encoded with ESM-2 (esm2_t12_35M_UR50D, 480-dimensional) and mean-pooled over residues into one vector per protein.

Two details that affect correctness:

  • Pooling covers residue positions only. The BOS and EOS tokens and any batch padding are excluded, so a short protein embedded alongside long ones gets the same vector as it would alone. This is verified in the test suite to within float32 noise.
  • Batches are formed by sequence length rather than in dataset order, which is what keeps the padding cost down (see results.md), and the rows are scattered back afterwards so row i is always protein i. The test suite pins that ordering, because a permuted embedding matrix would train happily and report plausible metrics against the wrong proteins.
  • Sequences longer than 1,022 residues are truncated, that being ESM-2's 1,024-position limit minus the two special tokens. Truncation keeps the N-terminus, which is where signal and transit peptides sit, so the most localization-relevant region survives.

The 35M checkpoint is a deliberate choice for iteration speed rather than peak accuracy. Larger checkpoints are a later step, and because embeddings are cached on disk, swapping one in does not change any downstream code.

Text

Text comes from the UniProt REST API (rest.uniprot.org/uniprotkb/search), queried 2026-07-29 for the fields cc_function, go_c, go_p, go_f, and keyword. UniProt served 13,973 of the 14,004 requested accessions; the remainder are entries it no longer serves.

Two text conditions are used, and they behave very differently:

Free-text function description (cc_function), a curator-written prose summary. This is the condition the research question is really about. A complete example, exactly as UniProt returns it (P93004):

FUNCTION: Water channel required to facilitate the transport of water across cell membrane. May be involved in the osmoregulation in plants under high osmotic stress such as under a high salt condition.

Structured annotation terms, GO terms plus keywords. These are controlled vocabulary rather than prose, and they frequently contain the label verbatim. For Q9H400, the keywords include Cell membrane and Membrane, and the GO cellular-component field includes extracellular space. This condition is included precisely because it is leaky: it bounds how well a model can do by reading the answer, which is the reference the free-text arm is interpreted against.

Both are encoded with all-MiniLM-L6-v2 (384-dimensional) and combined with the sequence embedding by concatenation.

A resolved preprocessing question: evidence codes

The example above shows something that affects 96.0% of the function descriptions: UniProt appends provenance markers such as {ECO:0000269|PubMed:10102577} to the prose. They average 14% of each field's character content.

These codes are database bookkeeping, not biology. They say which experiment supports a claim, and no two proteins share them in a way a sentence encoder could use. They are also not harmless filler: all-MiniLM-L6-v2 truncates its input, so evidence codes displace real text on longer descriptions. Every description also begins with the literal prefix FUNCTION:, which is constant across all 12,626 proteins and therefore carries no information.

This has now been measured rather than argued. Stripping the codes, the inline (PubMed:...) citations and the FUNCTION: prefix removes 22% of the corpus by character count, and it moves no arm materially: the cleaned free-text arm scores 0.743 macro-F1 against the unfiltered 0.740, inside one standard deviation. The prediction that evidence codes displace real text under the encoder's token limit was reasonable and turned out not to bite, because most descriptions are short enough not to be truncated.

The pipeline therefore still feeds the headline arm text exactly as UniProt returns it, which keeps the committed baseline comparable with earlier runs, and carries the cleaned variant as its own arm so the choice stays visible. Cleaning also runs as the first step of the localization ablation, where it matters for a different reason: an evidence code following a sentence-final period leaves an orphan . that the sentence splitter would otherwise count as a claim. See Ablation filter.

Coverage, and why it matters

Field Non-empty Share
GO cellular component 13,824 99.8%
Keywords 13,843 99.9%
Free-text function 12,626 91.1%

The gap matters. 1,232 proteins have no curated function text, so the free-text arm has nothing to ground on for 8.9% of the data, while the structured arm has near-complete coverage. Missingness is not random: proteins without function text are generally less-studied ones.

Those proteins are given an explicit zero vector rather than an encoding of the empty string. Encoding "" would hand every un-annotated protein the same distinctive non-zero vector, which a head can learn as a "this protein is obscure" flag. That would be a confound dressed up as grounding.

This leaves an open question, recorded rather than resolved: whether to report the headline comparison on the 91.1% with annotations, on all proteins with zeros, or both. The gap between those two numbers is itself informative about how much the grounded arm depends on annotation availability.

Isoforms

68 accessions are isoforms, such as P22462-2. Queried directly, UniProt returns these with empty annotation fields, because the text lives on the parent entry. They inherit the parent entry's text, and the column annotation_from_parent_entry marks them so they can be excluded in one filter. The caveat: two isoforms of one gene can have different localization labels but identical inherited text, giving the text arm contradictory supervision for those rows. At 0.5% of the data this is noted rather than fixed.

Splits

Split Proteins Source
Test 2,773 DeepLoc's official test partition
Train + validation pool 11,085 The remainder

Test. DeepLoc's authors partitioned their data by homology rather than at random, so a test protein is not a close relative of a training protein. This matters more than it might seem: protein families are large and internally similar, so a random split lets a model recognize a family member it has already seen and score well without generalizing. Because the partition is inherited from the benchmark, the property is built in rather than something asserted here. The exact clustering procedure and identity threshold are described in the DeepLoc publication.

The test set is used once, for final reported numbers, and never for model selection.

Train and validation. Carved from the remaining 11,085 proteins.

A known limitation, stated plainly: this train/validation boundary is not yet family-grouped. Related proteins can therefore land on both sides of it, which makes validation numbers optimistic and unsuitable for claims about generalization. They are used only for model selection, and reported numbers come from the homology-partitioned test set, which is unaffected. Grouping the train/validation split by sequence-similarity cluster is a planned follow-up.

Output table

One row per protein, with the columns each arm needs:

Column Description
accession UniProt accession, including isoform suffix where present
sequence Amino-acid sequence
localization One of the ten compartments, the classification target
solubility M, S, or U; carried along, unused by this task
is_test Whether the protein is in DeepLoc's official test partition
function_text Free-text function description
go_cellular_component, go_biological_process, go_molecular_function GO terms
keywords UniProt keywords
entry_accession Parent accession used for the annotation lookup
annotation_from_parent_entry Whether text was inherited from a parent entry
has_function_text Whether free-text function is present

Reproducing

mamba activate biollm
python projects/grounding-multimodal/scripts/prepare_data.py

The DeepLoc download is cached after the first run; pass --force-download to refetch, or --skip-uniprot to parse the FASTA without querying UniProt. The script prints every count on this page, so a rebuild that disagrees with these numbers is visible immediately. UniProt is updated continuously, so annotation coverage will drift over time even though the DeepLoc labels are fixed.