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 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/ ModernBertForMaskedLM presence/absence (β‰₯1 alt allele) AutoModel + subfolder="dominant" 0.785
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.

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

pip install "git+https://github.com/BioLab-D/SNPBERT.git"
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)

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

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.

License

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

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support