File size: 7,081 Bytes
08840d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
---
license: mit
library_name: transformers
inference: false
tags:
  - genomics
  - human-genetics
  - genotype
  - snp
  - modernbert
  - masked-language-modeling
  - uk-biobank
  - allele-dosage
---

# SNPBERT

Self-supervised transformer that learns individual-level representations from unlabeled
genotype data. Each person is encoded as an ordered sequence of **rsID tokens** β€” one per
locus carrying at least one alternate allele β€” and the model is pretrained by masked
language modeling (masked rsID prediction) on **485,923 UK Biobank participants across
13,615 SNPs**. Built on the [ModernBERT](https://huggingface.co/docs/transformers/model_doc/modernbert)
architecture; its alternating local/global attention captures both short-range linkage
disequilibrium and genome-wide structure in a single forward pass.

**Code / reproduction:** https://github.com/BioLab-D/SNPBERT

## Two backbones in this repo

This repo ships **two pretrained backbones** as subfolders:

| Subfolder | Model | Genotype encoding | Load with | Masked-SNP acc. |
|---|---|---|---|---|
| [`dominant/`](./dominant) | `ModernBertForMaskedLM` | presence/absence (β‰₯1 alt allele) | `AutoModel` + `subfolder="dominant"` | 0.785 |
| [`dosage/`](./dosage) | `ModernBertWithDosage` (base + dosage embedding) | het (1) vs. hom-alt (2) | **custom class** + `subfolder="dosage"` | 0.787 |

The **dosage** backbone adds an additive per-token dosage embedding on top of the dominant
architecture, so it distinguishes heterozygous from homozygous-alternate genotypes.

- **Architecture:** ModernBERT β€” 22 layers, hidden 768, 12 heads, intermediate 1152,
  global attention every 3rd layer, max position 9936, vocab 13,620.
- **Precision:** bfloat16.

> **Precision note:** weights are bfloat16 (trained on GPU). On **CPU**, cast to float32
> (`.float()`) β€” CPU LayerNorm does not support bfloat16. On GPU, use
> `torch_dtype=torch.bfloat16`.

## Tokenizer conventions (shared by both)

The vocabulary reserves the **first 5 ids** for special tokens, then SNP column *i* maps
to token id `i + 5`:

```
0=[UNK]  1=[SEP]  2=[PAD]  3=[CLS]  4=[MASK]   5..=SNP tokens (e.g. rs11260596_T)
```

Each SNP token is `rsID_allele`. An individual is a sequence
`[CLS] <SNP tokens for loci with β‰₯1 alt allele> [SEP]`.

---

## Usage β€” dominant backbone

Standard `ModernBertForMaskedLM`; loads directly with `AutoModel`.

```python
import torch
from transformers import AutoModel, AutoTokenizer

device = "cuda" if torch.cuda.is_available() else "cpu"
tok = AutoTokenizer.from_pretrained("ccjsj/SNPBERT", subfolder="dominant")
model = AutoModel.from_pretrained("ccjsj/SNPBERT", subfolder="dominant",
                                  attn_implementation="sdpa").to(device)
model = model.bfloat16() if device == "cuda" else model.float()
model.eval()

# One individual: [CLS] + SNP tokens (loci with >=1 alt allele) + [SEP]
snps = ["rs11260596_T", "rs6603782_T", "rs11523819_A"]
ids = [tok.cls_token_id] + tok.convert_tokens_to_ids(snps) + [tok.sep_token_id]
input_ids = torch.tensor([ids], device=device)
attention_mask = torch.ones_like(input_ids)

with torch.no_grad():
    out = model(input_ids=input_ids, attention_mask=attention_mask)
cls_embedding = out.last_hidden_state[:, 0]   # (1, 768)
```

These `[CLS]` embeddings feed the paper's downstream tasks (population-structure
clustering, disease-risk AMIL, variant identification).

---

## Usage β€” dosage backbone

### ⚠️ Not a plain `AutoModel` β€” custom class required

The `dosage/` checkpoint is **not** a standard `ModernBertForMaskedLM`. Its flat state
dict is:

```
base.*              # a ModernBertForMaskedLM  (138 tensors)
dosage_emb.weight   # nn.Embedding(3, 768)     (the dosage embedding)
```

`config.json` still declares `architectures: ["ModernBertForMaskedLM"]`, so loading with
`AutoModel.from_pretrained(...)` will **silently drop `dosage_emb.weight`**. You must
rebuild the `ModernBertWithDosage` wrapper. Its forward adds the dosage embedding to the
token embeddings **before** the encoder:

```
inputs_embeds = base.get_input_embeddings()(input_ids) + dosage_emb(dosage_ids)
```

`dosage_ids` is per-token in `{0, 1, 2}` β€” `0` at special positions, `1` (heterozygous) /
`2` (homozygous-alternate) at SNP positions.

### Option A β€” install the SNPBERT package

```bash
pip install "git+https://github.com/BioLab-D/SNPBERT.git"
```

```python
import torch
from huggingface_hub import snapshot_download
from snpbert.model import load_dosage_model

root = snapshot_download("ccjsj/SNPBERT")     # downloads both subfolders
ckpt = f"{root}/dosage"
device = "cuda" if torch.cuda.is_available() else "cpu"

model = load_dosage_model(ckpt, ckpt, num_dosage=3, attn_impl="sdpa").to(device)
model = model.bfloat16() if device == "cuda" else model.float()  # CPU needs fp32
model.eval()
```

### Option B β€” self-contained (no repo install)

```python
import torch, torch.nn as nn
from transformers import AutoConfig, AutoModelForMaskedLM, AutoTokenizer
from safetensors.torch import load_file
from huggingface_hub import hf_hub_download

class ModernBertWithDosage(nn.Module):
    def __init__(self, base, num_dosage=3):
        super().__init__()
        self.base = base
        self.dosage_emb = nn.Embedding(num_dosage, base.config.hidden_size)
    def forward(self, input_ids, dosage_ids=None, attention_mask=None, labels=None):
        emb = self.base.get_input_embeddings()(input_ids)
        if dosage_ids is not None:
            emb = emb + self.dosage_emb(dosage_ids)
        return self.base(inputs_embeds=emb, attention_mask=attention_mask, labels=labels)

repo = "ccjsj/SNPBERT"
cfg = AutoConfig.from_pretrained(repo, subfolder="dosage")
cfg._attn_implementation = "sdpa"
model = ModernBertWithDosage(AutoModelForMaskedLM.from_config(cfg), num_dosage=3)
sd = load_file(hf_hub_download(repo, "model.safetensors", subfolder="dosage"))
model.load_state_dict(sd, strict=True)        # keys: base.* + dosage_emb.weight
model.float().eval()
```

### Forward with dosage

```python
tok = AutoTokenizer.from_pretrained("ccjsj/SNPBERT", subfolder="dosage")
snps = ["rs11260596_T", "rs6603782_T", "rs11523819_A"]
ids  = [tok.cls_token_id] + tok.convert_tokens_to_ids(snps) + [tok.sep_token_id]
input_ids  = torch.tensor([ids])
attn       = torch.ones_like(input_ids)
# per-token dosage: 0 at [CLS]/[SEP]; 1=het, 2=hom-alt at SNP positions
dosage_ids = torch.tensor([[0, 1, 2, 1, 0]])

with torch.no_grad():
    out = model(input_ids=input_ids, dosage_ids=dosage_ids, attention_mask=attn)
logits = out.logits              # (1, seq_len, 13620)
```

---

## Downstream use

The frozen backbone feeds three analyses in the paper: population-structure clustering
(matching PCA), disease-risk modeling with a sparse-gated attention-MIL head, and
attention-based variant prioritization. Pipelines and a runnable synthetic-data demo are
in the [GitHub repo](https://github.com/BioLab-D/SNPBERT).

## License

MIT. Trained on UK Biobank data under approved access; use of derived representations
must comply with UK Biobank terms.