genomic-llm-embeddings
When to Use
- Turning raw DNA sequences into fixed-length numeric vectors for a downstream classifier or clustering task.
- Deciding between character-level, k-mer, or BPE tokenization before fine-tuning a genomic LM.
- Sanity-checking a new sequence embedding (from a pretrained model or your own encoder) against a cheap k-mer baseline before trusting it.
- Choosing between short-context transformers (DNABERT-2, Nucleotide Transformer) and long-context models (HyenaDNA, Evo) based on how far apart regulatory elements are.
- Building a synthetic probe task (e.g., promoter motif detection) to validate an embedding pipeline end-to-end.
Version Compatibility
- Python ≥3.10, NumPy ≥1.24
- transformers ≥4.40 (only if loading pretrained genomic LMs)
- Reference models: Nucleotide Transformer v2 (InstaDeepAI, 500M-multi-species), DNABERT-2 (117M), HyenaDNA (up to 1M bp context), Evo (prokaryotic/viral, StripedHyena backbone)
Prerequisites
pip install numpy (add transformers torch only for loading real pretrained models)
- Familiarity with basic sequence handling (
biopython skill) helps but is not required
- Related skill:
genomic-foundation-models for fine-tuning details of the models named above
Tokenization Strategy Selection
| Strategy |
Context |
Strength |
Limitation |
| Character (A/C/G/T/N) |
Long |
Max resolution |
Long token sequences, slow attention |
| k-mer (k=3..6) |
Short/medium |
Fast, interpretable baseline |
Loses positional/order info; k=6 already gives 4096-word vocab |
| DNABERT-2 (BPE) |
~512 bp windows |
Strong short-window tasks |
Limited context length |
| Nucleotide Transformer (6-mer, stride 1) |
kb-scale |
Good transfer embeddings |
~L/6 tokens, higher memory than k-mer counts |
| HyenaDNA (character-level, SSM) |
up to 1M bp |
Captures distal regulatory interactions |
Heavier training/inference than short-context models |
| Evo (character-level, StripedHyena) |
genome-scale |
Prokaryotic/viral generation & scoring |
Not trained on mammalian genomes — don't use for human regulatory tasks |
Goal: Turn a DNA sequence into a fixed-length vector without training anything.
Approach: Slide a window of length k across the sequence, count k-mer occurrences, and normalize into a frequency vector over the fixed 4**k vocabulary. This is the standard sanity-check baseline before reaching for a pretrained model.
import numpy as np
from collections import Counter
def kmers(seq: str, k: int = 6) -> list[str]:
"""Slide a window of length k across seq, returning overlapping k-mers."""
seq = seq.upper()
return [seq[i:i + k] for i in range(len(seq) - k + 1)]
def kmer_embedding(seq: str, vocab: list[str], k: int = 3) -> np.ndarray:
"""Normalized k-mer frequency vector over a fixed vocabulary (order-independent)."""
tokens = kmers(seq, k)
counts = Counter(tokens)
vec = np.array([counts[v] for v in vocab], dtype=float)
return vec / (vec.sum() + 1e-9)
alphabet = ["A", "C", "G", "T"]
vocab_3 = [a + b + c for a in alphabet for b in alphabet for c in alphabet] # 64-dim
example_vec = kmer_embedding("ATGATGATGCCC", vocab_3, k=3)
assert example_vec.shape[0] == 64
assert abs(example_vec.sum() - 1.0) < 1e-6
Goal: Check whether an embedding (k-mer or model-derived) actually separates two classes before trusting it downstream.
Approach: Train-free nearest-centroid probe — compute per-class centroids on train embeddings, classify test embeddings by nearest centroid. Cheap, has no hyperparameters, and exposes garbage embeddings immediately.
import numpy as np
def nearest_centroid_predict(X_train: np.ndarray, y_train: np.ndarray, X_test: np.ndarray) -> np.ndarray:
"""Binary nearest-centroid classifier: assign each test row to the closer class mean."""
c0 = X_train[y_train == 0].mean(axis=0)
c1 = X_train[y_train == 1].mean(axis=0)
d0 = ((X_test - c0) ** 2).sum(axis=1)
d1 = ((X_test - c1) ** 2).sum(axis=1)
return (d1 < d0).astype(int)
def random_dna(n: int, rng: np.random.Generator) -> str:
"""Generate a random ACGT sequence of length n."""
return "".join(rng.choice(list("ACGT"), size=n))
def inject_motif(seq: str, motif: str, pos: int) -> str:
"""Splice a motif into seq at position pos (overwrites in place, keeps length)."""
return seq[:pos] + motif + seq[pos + len(motif):]
def demo():
"""Synthetic promoter-vs-background probe: TATA box injected at pos 20 in half the sequences."""
rng = np.random.default_rng(7)
n_samples, length, motif = 120, 80, "TATAAA"
seqs, labels = [], []
for _ in range(n_samples):
s = random_dna(length, rng)
if rng.random() < 0.5:
s = inject_motif(s, motif, pos=20)
labels.append(1)
else:
labels.append(0)
seqs.append(s)
X = np.stack([kmer_embedding(s, vocab_3, k=3) for s in seqs])
y = np.array(labels)
X_train, y_train = X[:90], y[:90]
X_test, y_test = X[90:], y[90:]
pred = nearest_centroid_predict(X_train, y_train, X_test)
acc = (pred == y_test).mean()
assert acc > 0.8, f"probe accuracy too low: {acc}" # motif is easy; k-mer baseline should nail it
print(f"nearest-centroid probe accuracy: {acc:.3f}")
if __name__ == "__main__":
demo()
Goal: Get real embeddings from a pretrained genomic LM instead of a k-mer baseline.
Approach: Use transformers to load a genomic foundation model, tokenize, and mean-pool the last hidden state into a single vector per sequence.
def embed_with_nucleotide_transformer(seqs: list[str]) -> "np.ndarray":
"""Mean-pooled embeddings from Nucleotide Transformer v2 (500M-multi-species).
Requires: pip install transformers torch
"""
import torch
from transformers import AutoTokenizer, AutoModel
model_name = "InstaDeepAI/nucleotide-transformer-v2-500m-multi-species"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name).eval()
tokens = tokenizer(seqs, return_tensors="pt", padding=True, truncation=True)
with torch.no_grad():
out = model(**tokens)
mask = tokens["attention_mask"].unsqueeze(-1)
pooled = (out.last_hidden_state * mask).sum(1) / mask.sum(1)
return pooled.numpy()
Pitfalls
- Mixing tokenization schemes between train and inference silently destroys performance — a model trained on 6-mer tokens fed character-level input at inference will not error, just degrade quietly.
- Always compare against a k-mer baseline first; without one, overfitting or a broken embedding pipeline is very hard to detect.
- k-mer vocab explodes with k: 4^6 = 4096, 4^8 = 65,536 — keep explicit-count k-mers to k≤6, use BPE or learned embeddings beyond that.
- Match sequence window length across models being compared — truncating to a short window can flip labels that depend on distal motifs (see the
distal_interaction_label pattern: a motif near the end is silently dropped if you truncate to 200bp).
- Evo is trained on prokaryotic/viral genomes only — do not use it for human/mammalian regulatory tasks.
- Nearest-centroid probes only validate that classes separate at all; a low probe score means "fix the embedding," not "add a bigger downstream model."
See Also
genomic-foundation-models — fine-tuning and inference details for NT, DNABERT-2, HyenaDNA, Evo
protein-language-models — ESM2 embeddings for protein sequences (analogous workflow for proteins)
ai-science-epigenomic-sequence-models — regulatory-activity prediction (Enformer/AlphaGenome) from DNA
bio-sequence-manipulation-motif-search — motif scanning utilities used alongside embedding probes
1---2name: genomic-llm-embeddings3description: Build DNA embeddings via k-mer frequency vectors or genomic LMs (Nucleotide Transformer, DNABERT-2, HyenaDNA). Use when embedding DNA for ML, choosing k-mer/BPE tokenization, or probing embedding quality.4---56# genomic-llm-embeddings78## When to Use910- Turning raw DNA sequences into fixed-length numeric vectors for a downstream classifier or clustering task.11- Deciding between character-level, k-mer, or BPE tokenization before fine-tuning a genomic LM.12- Sanity-checking a new sequence embedding (from a pretrained model or your own encoder) against a cheap k-mer baseline before trusting it.13- Choosing between short-context transformers (DNABERT-2, Nucleotide Transformer) and long-context models (HyenaDNA, Evo) based on how far apart regulatory elements are.14- Building a synthetic probe task (e.g., promoter motif detection) to validate an embedding pipeline end-to-end.1516## Version Compatibility1718- Python ≥3.10, NumPy ≥1.2419- transformers ≥4.40 (only if loading pretrained genomic LMs)20- Reference models: Nucleotide Transformer v2 (InstaDeepAI, 500M-multi-species), DNABERT-2 (117M), HyenaDNA (up to 1M bp context), Evo (prokaryotic/viral, StripedHyena backbone)2122## Prerequisites2324- `pip install numpy` (add `transformers torch` only for loading real pretrained models)25- Familiarity with basic sequence handling (`biopython` skill) helps but is not required26- Related skill: `genomic-foundation-models` for fine-tuning details of the models named above2728## Tokenization Strategy Selection2930| Strategy | Context | Strength | Limitation |31|---|---:|---|---|32| Character (A/C/G/T/N) | Long | Max resolution | Long token sequences, slow attention |33| k-mer (k=3..6) | Short/medium | Fast, interpretable baseline | Loses positional/order info; k=6 already gives 4096-word vocab |34| DNABERT-2 (BPE) | ~512 bp windows | Strong short-window tasks | Limited context length |35| Nucleotide Transformer (6-mer, stride 1) | kb-scale | Good transfer embeddings | ~L/6 tokens, higher memory than k-mer counts |36| HyenaDNA (character-level, SSM) | up to 1M bp | Captures distal regulatory interactions | Heavier training/inference than short-context models |37| Evo (character-level, StripedHyena) | genome-scale | Prokaryotic/viral generation & scoring | Not trained on mammalian genomes — don't use for human regulatory tasks |3839**Goal:** Turn a DNA sequence into a fixed-length vector without training anything.40**Approach:** Slide a window of length `k` across the sequence, count k-mer occurrences, and normalize into a frequency vector over the fixed `4**k` vocabulary. This is the standard sanity-check baseline before reaching for a pretrained model.4142```python43import numpy as np44from collections import Counter4546def kmers(seq: str, k: int = 6) -> list[str]:47 """Slide a window of length k across seq, returning overlapping k-mers."""48 seq = seq.upper()49 return [seq[i:i + k] for i in range(len(seq) - k + 1)]5051def kmer_embedding(seq: str, vocab: list[str], k: int = 3) -> np.ndarray:52 """Normalized k-mer frequency vector over a fixed vocabulary (order-independent)."""53 tokens = kmers(seq, k)54 counts = Counter(tokens)55 vec = np.array([counts[v] for v in vocab], dtype=float)56 return vec / (vec.sum() + 1e-9)5758alphabet = ["A", "C", "G", "T"]59vocab_3 = [a + b + c for a in alphabet for b in alphabet for c in alphabet] # 64-dim6061example_vec = kmer_embedding("ATGATGATGCCC", vocab_3, k=3)62assert example_vec.shape[0] == 6463assert abs(example_vec.sum() - 1.0) < 1e-664```6566**Goal:** Check whether an embedding (k-mer or model-derived) actually separates two classes before trusting it downstream.67**Approach:** Train-free nearest-centroid probe — compute per-class centroids on train embeddings, classify test embeddings by nearest centroid. Cheap, has no hyperparameters, and exposes garbage embeddings immediately.6869```python70import numpy as np7172def nearest_centroid_predict(X_train: np.ndarray, y_train: np.ndarray, X_test: np.ndarray) -> np.ndarray:73 """Binary nearest-centroid classifier: assign each test row to the closer class mean."""74 c0 = X_train[y_train == 0].mean(axis=0)75 c1 = X_train[y_train == 1].mean(axis=0)76 d0 = ((X_test - c0) ** 2).sum(axis=1)77 d1 = ((X_test - c1) ** 2).sum(axis=1)78 return (d1 < d0).astype(int)7980def random_dna(n: int, rng: np.random.Generator) -> str:81 """Generate a random ACGT sequence of length n."""82 return "".join(rng.choice(list("ACGT"), size=n))8384def inject_motif(seq: str, motif: str, pos: int) -> str:85 """Splice a motif into seq at position pos (overwrites in place, keeps length)."""86 return seq[:pos] + motif + seq[pos + len(motif):]8788def demo():89 """Synthetic promoter-vs-background probe: TATA box injected at pos 20 in half the sequences."""90 rng = np.random.default_rng(7)91 n_samples, length, motif = 120, 80, "TATAAA"92 seqs, labels = [], []93 for _ in range(n_samples):94 s = random_dna(length, rng)95 if rng.random() < 0.5:96 s = inject_motif(s, motif, pos=20)97 labels.append(1)98 else:99 labels.append(0)100 seqs.append(s)101102 X = np.stack([kmer_embedding(s, vocab_3, k=3) for s in seqs])103 y = np.array(labels)104 X_train, y_train = X[:90], y[:90]105 X_test, y_test = X[90:], y[90:]106107 pred = nearest_centroid_predict(X_train, y_train, X_test)108 acc = (pred == y_test).mean()109 assert acc > 0.8, f"probe accuracy too low: {acc}" # motif is easy; k-mer baseline should nail it110 print(f"nearest-centroid probe accuracy: {acc:.3f}")111112if __name__ == "__main__":113 demo()114```115116**Goal:** Get real embeddings from a pretrained genomic LM instead of a k-mer baseline.117**Approach:** Use `transformers` to load a genomic foundation model, tokenize, and mean-pool the last hidden state into a single vector per sequence.118119```python120def embed_with_nucleotide_transformer(seqs: list[str]) -> "np.ndarray":121 """Mean-pooled embeddings from Nucleotide Transformer v2 (500M-multi-species).122 Requires: pip install transformers torch123 """124 import torch125 from transformers import AutoTokenizer, AutoModel126127 model_name = "InstaDeepAI/nucleotide-transformer-v2-500m-multi-species"128 tokenizer = AutoTokenizer.from_pretrained(model_name)129 model = AutoModel.from_pretrained(model_name).eval()130131 tokens = tokenizer(seqs, return_tensors="pt", padding=True, truncation=True)132 with torch.no_grad():133 out = model(**tokens)134 mask = tokens["attention_mask"].unsqueeze(-1)135 pooled = (out.last_hidden_state * mask).sum(1) / mask.sum(1)136 return pooled.numpy()137```138139## Pitfalls140141- Mixing tokenization schemes between train and inference silently destroys performance — a model trained on 6-mer tokens fed character-level input at inference will not error, just degrade quietly.142- Always compare against a k-mer baseline first; without one, overfitting or a broken embedding pipeline is very hard to detect.143- k-mer vocab explodes with k: 4^6 = 4096, 4^8 = 65,536 — keep explicit-count k-mers to k≤6, use BPE or learned embeddings beyond that.144- Match sequence window length across models being compared — truncating to a short window can flip labels that depend on distal motifs (see the `distal_interaction_label` pattern: a motif near the end is silently dropped if you truncate to 200bp).145- Evo is trained on prokaryotic/viral genomes only — do not use it for human/mammalian regulatory tasks.146- Nearest-centroid probes only validate that classes separate at all; a low probe score means "fix the embedding," not "add a bigger downstream model."147148## See Also149150- `genomic-foundation-models` — fine-tuning and inference details for NT, DNABERT-2, HyenaDNA, Evo151- `protein-language-models` — ESM2 embeddings for protein sequences (analogous workflow for proteins)152- `ai-science-epigenomic-sequence-models` — regulatory-activity prediction (Enformer/AlphaGenome) from DNA153- `bio-sequence-manipulation-motif-search` — motif scanning utilities used alongside embedding probes