Match Carbon FNS BpLogitsProcessor decoding
Browse files- daisychain.py +36 -21
daisychain.py
CHANGED
|
@@ -173,19 +173,28 @@ class DaisyChain:
|
|
| 173 |
return out
|
| 174 |
|
| 175 |
# ---- FNS base-pair-level decode + score (Carbon's factorized-nucleotide approach) ----
|
| 176 |
-
def _bp_marginals(self, logits):
|
| 177 |
-
"""Marginalize a 6-mer logit vector into six 4-way per-position base distributions [6,4].
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
bp = torch.zeros(6, 4, device=logits.device)
|
| 180 |
for pos in range(6):
|
| 181 |
bp[pos].scatter_add_(0, self._base_at[pos], p)
|
| 182 |
return bp
|
| 183 |
|
| 184 |
@torch.no_grad()
|
| 185 |
-
def generate_baselevel_stream(self, domain, length=180, temperature=1.0, top_p=0.9,
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
|
|
|
| 189 |
m = self.models[domain]
|
| 190 |
p = clean(prompt) if prompt else ""
|
| 191 |
p = p[-1020:]; p = p[len(p) % 6:]
|
|
@@ -194,16 +203,11 @@ class DaisyChain:
|
|
| 194 |
out = ""
|
| 195 |
while len(out) < length:
|
| 196 |
logits = m(input_ids=t).logits[0, -1].float() / max(temperature, 1e-6)
|
| 197 |
-
bp = self._bp_marginals(logits)
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
rm = torch.cumsum(s, -1) > top_p
|
| 203 |
-
rm[1:] = rm[:-1].clone(); rm[0] = False
|
| 204 |
-
q = probs.clone(); q[si[rm]] = 0; q = q / q.sum()
|
| 205 |
-
six.append(self._idx2base[int(torch.multinomial(q, 1))])
|
| 206 |
-
six = "".join(six)
|
| 207 |
t = torch.cat([t, torch.tensor([[self._kmer_id_of[six]]], device=self.dev)], dim=1)
|
| 208 |
out += six
|
| 209 |
yield out[:length]
|
|
@@ -211,10 +215,15 @@ class DaisyChain:
|
|
| 211 |
@torch.no_grad()
|
| 212 |
def score(self, domain, seq):
|
| 213 |
"""Carbon `score_sequence` equivalent: mean per-base log-prob of the observed sequence under
|
| 214 |
-
the base-level (marginalized) distribution. Higher = more likely. bits/base = -score/ln2.
|
|
|
|
| 215 |
m = self.models[domain]
|
| 216 |
-
s = clean(seq)
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
return float("nan")
|
| 219 |
ids = [self.bos] + self.tok.encode(s, add_special_tokens=False)
|
| 220 |
t = torch.tensor([ids], device=self.dev)
|
|
@@ -222,8 +231,14 @@ class DaisyChain:
|
|
| 222 |
b2i = {"A": 0, "T": 1, "C": 2, "G": 3}
|
| 223 |
tot, n = 0.0, 0
|
| 224 |
for i in range(len(ids) - 1):
|
|
|
|
|
|
|
| 225 |
bp = self._bp_marginals(logits[i].float()) # [6,4] predicted bases of next 6-mer
|
| 226 |
nxt = s[i * 6:(i + 1) * 6]
|
| 227 |
for pos, ch in enumerate(nxt):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
tot += math.log(max(float(bp[pos, b2i[ch]]), 1e-12)); n += 1
|
| 229 |
-
return tot / n
|
|
|
|
| 173 |
return out
|
| 174 |
|
| 175 |
# ---- FNS base-pair-level decode + score (Carbon's factorized-nucleotide approach) ----
|
| 176 |
+
def _bp_marginals(self, logits, top_p=1.0):
|
| 177 |
+
"""Marginalize a 6-mer logit vector into six 4-way per-position base distributions [6,4].
|
| 178 |
+
Matching Carbon's _BPLogitsProcessor: any top-p filtering happens at the 6-MER level
|
| 179 |
+
(before marginalizing), not per-base."""
|
| 180 |
+
kl = logits[self._kmer_ids] # [4096] 6-mer logits
|
| 181 |
+
if top_p < 1.0: # nucleus on the 6-mers first
|
| 182 |
+
sk, si = torch.sort(kl, descending=True)
|
| 183 |
+
rm = torch.cumsum(F.softmax(sk, -1), -1) > top_p
|
| 184 |
+
rm[1:] = rm[:-1].clone(); rm[0] = False
|
| 185 |
+
kl = kl.clone(); kl[si[rm]] = float("-inf")
|
| 186 |
+
p = F.softmax(kl, dim=-1)
|
| 187 |
bp = torch.zeros(6, 4, device=logits.device)
|
| 188 |
for pos in range(6):
|
| 189 |
bp[pos].scatter_add_(0, self._base_at[pos], p)
|
| 190 |
return bp
|
| 191 |
|
| 192 |
@torch.no_grad()
|
| 193 |
+
def generate_baselevel_stream(self, domain, length=180, temperature=1.0, top_p=0.9,
|
| 194 |
+
prompt="", greedy=False):
|
| 195 |
+
"""Base-pair generation exactly as Carbon's FNS BpLogitsProcessor: apply temperature +
|
| 196 |
+
top-p at the 6-mer level, marginalize the filtered 6-mer softmax to six 4-way base
|
| 197 |
+
distributions, then pick each base by multinomial (sampling) or argmax (greedy)."""
|
| 198 |
m = self.models[domain]
|
| 199 |
p = clean(prompt) if prompt else ""
|
| 200 |
p = p[-1020:]; p = p[len(p) % 6:]
|
|
|
|
| 203 |
out = ""
|
| 204 |
while len(out) < length:
|
| 205 |
logits = m(input_ids=t).logits[0, -1].float() / max(temperature, 1e-6)
|
| 206 |
+
bp = self._bp_marginals(logits, top_p=(1.0 if greedy else top_p)) # [6,4]
|
| 207 |
+
if greedy:
|
| 208 |
+
six = "".join(self._idx2base[int(bp[pos].argmax())] for pos in range(6))
|
| 209 |
+
else:
|
| 210 |
+
six = "".join(self._idx2base[int(torch.multinomial(bp[pos], 1))] for pos in range(6))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
t = torch.cat([t, torch.tensor([[self._kmer_id_of[six]]], device=self.dev)], dim=1)
|
| 212 |
out += six
|
| 213 |
yield out[:length]
|
|
|
|
| 215 |
@torch.no_grad()
|
| 216 |
def score(self, domain, seq):
|
| 217 |
"""Carbon `score_sequence` equivalent: mean per-base log-prob of the observed sequence under
|
| 218 |
+
the base-level (marginalized) distribution. Higher = more likely. bits/base = -score/ln2.
|
| 219 |
+
Right-pads to a multiple of 6 with 'A' (Carbon's convention) and scores only real bases."""
|
| 220 |
m = self.models[domain]
|
| 221 |
+
s = clean(seq)
|
| 222 |
+
orig = len(s)
|
| 223 |
+
r = len(s) % 6
|
| 224 |
+
if r:
|
| 225 |
+
s = s + "A" * (6 - r) # right-pad like score_sequence
|
| 226 |
+
if orig < 12:
|
| 227 |
return float("nan")
|
| 228 |
ids = [self.bos] + self.tok.encode(s, add_special_tokens=False)
|
| 229 |
t = torch.tensor([ids], device=self.dev)
|
|
|
|
| 231 |
b2i = {"A": 0, "T": 1, "C": 2, "G": 3}
|
| 232 |
tot, n = 0.0, 0
|
| 233 |
for i in range(len(ids) - 1):
|
| 234 |
+
if i * 6 >= orig: # don't score the padding
|
| 235 |
+
break
|
| 236 |
bp = self._bp_marginals(logits[i].float()) # [6,4] predicted bases of next 6-mer
|
| 237 |
nxt = s[i * 6:(i + 1) * 6]
|
| 238 |
for pos, ch in enumerate(nxt):
|
| 239 |
+
if i * 6 + pos >= orig: # padding base — don't score
|
| 240 |
+
break
|
| 241 |
+
if ch not in b2i: # skip N / ambiguous bases
|
| 242 |
+
continue
|
| 243 |
tot += math.log(max(float(bp[pos, b2i[ch]]), 1e-12)); n += 1
|
| 244 |
+
return tot / max(n, 1) # mean per-base logp
|