v17
Browse files- README.md +4 -1
- predict.py +30 -1
- rerank2.py +67 -0
- weights/mode.json +1 -1
- weights/ranker_v2.npz +3 -0
README.md
CHANGED
|
@@ -31,7 +31,9 @@ The course permits external data provided it is declared. This model uses three
|
|
| 31 |
| + transcript-level TrEMBL as a fast second pass, weight 0.1 (v16, shipped) | **0.8467** |
|
| 32 |
| train + full Swiss-Prot (v13) | 0.9634 |
|
| 33 |
|
| 34 |
-
|
|
|
|
|
|
|
| 35 |
|
| 36 |
No external data was used for training any learned component. ESM-2 and other pretrained protein language models were used only in experiments and are not part of this submission.
|
| 37 |
|
|
@@ -40,6 +42,7 @@ No external data was used for training any learned component. ESM-2 and other pr
|
|
| 40 |
2. **Second pass.** Inputs with no hit, or a best hit of 30% identity or less, are searched again with `--ultra-sensitive` when the time budget clearly allows it.
|
| 41 |
3. **Extra reference pass (v16).** All inputs are also searched against the transcript-level TrEMBL database in DIAMOND's `--fast` mode, about 25 s. This runs only if less than 330 s have passed. It only adds hits and is skipped on any error. For the hard validation proteins not in Swiss-Prot, it doubles the number with a relative at 80% identity or more (53 to 103, with validation sequences excluded).
|
| 42 |
4. **Vote.** Each EC gets `sum over the top 50 hits of (bitscore / best bitscore)^12 x w_source`, split evenly across a hit's ECs. `w_source` is 1 for train and Swiss-Prot references, 0.2 for protein-level TrEMBL references and 0.1 for transcript-level TrEMBL references, whose automatic annotations are noisier. The highest-scoring EC is predicted, with ties broken by training frequency. Settings are in `weights/mode.json`. They were tuned on the validation inputs that have Swiss-Prot labels, with every validation sequence removed from the external reference. Tuning used alternate halves: the best setting chosen on one half improved the other half by the same margin (+0.5 points over k=20, power 8, unweighted).
|
|
|
|
| 43 |
|
| 44 |
## Robustness
|
| 45 |
- The evaluator caps every written file at 16 MiB (RLIMIT_FSIZE) and allows only 256 open files (RLIMIT_NOFILE). DIAMOND runs with `--bin 128` (derived from RLIMIT_NOFILE) and batches of at most 8,000 queries. It halves the bin count on "Too many open files" and splits a batch on SIGXFSZ.
|
|
|
|
| 31 |
| + transcript-level TrEMBL as a fast second pass, weight 0.1 (v16, shipped) | **0.8467** |
|
| 32 |
| train + full Swiss-Prot (v13) | 0.9634 |
|
| 33 |
|
| 34 |
+
On **official labels**, the same idea gives a larger honest gain. On the 13,440 hard training proteins above, the vote scores 0.732 with the training reference only and **0.801** with the external references added. Training sequences are never part of the external sets, so every hit is a different protein.
|
| 35 |
+
|
| 36 |
+
The labels for the validation check come from Swiss-Prot. They agree with the official scoring: our train-only model scores L1 0.962 here and 0.960 on the leaderboard.
|
| 37 |
|
| 38 |
No external data was used for training any learned component. ESM-2 and other pretrained protein language models were used only in experiments and are not part of this submission.
|
| 39 |
|
|
|
|
| 42 |
2. **Second pass.** Inputs with no hit, or a best hit of 30% identity or less, are searched again with `--ultra-sensitive` when the time budget clearly allows it.
|
| 43 |
3. **Extra reference pass (v16).** All inputs are also searched against the transcript-level TrEMBL database in DIAMOND's `--fast` mode, about 25 s. This runs only if less than 330 s have passed. It only adds hits and is skipped on any error. For the hard validation proteins not in Swiss-Prot, it doubles the number with a relative at 80% identity or more (53 to 103, with validation sequences excluded).
|
| 44 |
4. **Vote.** Each EC gets `sum over the top 50 hits of (bitscore / best bitscore)^12 x w_source`, split evenly across a hit's ECs. `w_source` is 1 for train and Swiss-Prot references, 0.2 for protein-level TrEMBL references and 0.1 for transcript-level TrEMBL references, whose automatic annotations are noisier. The highest-scoring EC is predicted, with ties broken by training frequency. Settings are in `weights/mode.json`. They were tuned on the validation inputs that have Swiss-Prot labels, with every validation sequence removed from the external reference. Tuning used alternate halves: the best setting chosen on one half improved the other half by the same margin (+0.5 points over k=20, power 8, unweighted).
|
| 45 |
+
5. **Learned re-ranker (v17).** For inputs whose best hit is below 80% identity, a gradient-boosted tree model (scikit-learn HistGradientBoosting, exported to pure numpy in `weights/ranker_v2.npz`) re-orders the vote's top 10 ECs. It uses 29 features per candidate (`rerank2.py`): vote shares at several powers, share of the vote from each source (train / Swiss-Prot / TrEMBL), best-hit identity, coverage and rank, support counts, EC-hierarchy agreement, EC frequency and query difficulty. It was trained on **official training labels**: 13,440 hard training proteins (one per 50%-identity cluster, best training hit below 58% identity), searched against the other cluster folds plus the external references. In grouped 5-fold CV on that set it improves L4 by about 0.3 points (vote 0.7982 / 0.8031 → re-ranker 0.8011 / 0.8064 on the two halves). The gain comes below 80% identity; above that it is neutral, hence the cutoff.
|
| 46 |
|
| 47 |
## Robustness
|
| 48 |
- The evaluator caps every written file at 16 MiB (RLIMIT_FSIZE) and allows only 256 open files (RLIMIT_NOFILE). DIAMOND runs with `--bin 128` (derived from RLIMIT_NOFILE) and batches of at most 8,000 queries. It halves the bin count on "Too many open files" and splits a batch on SIGXFSZ.
|
predict.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""Offline CPU inference: DIAMOND homology search against train + UniProt references, bitscore vote (mode.json).
|
| 2 |
|
| 3 |
1. Search every unique input against the training set (diamond blastp
|
| 4 |
--very-sensitive, 50 hits).
|
|
@@ -328,6 +328,35 @@ def homology_predictions(seqs, labels, freq):
|
|
| 328 |
votes[e] = votes.get(e, 0.0) + w / len(ecs)
|
| 329 |
if votes:
|
| 330 |
preds[i] = sorted(votes, key=lambda e: (-votes[e], -freq.get(e, 0), e))[:10]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
return preds
|
| 332 |
ranker = TreeRanker(ROOT / 'weights' / 'ranker.npz')
|
| 333 |
rel = EcReliability(ROOT / 'weights' / 'te_tables.json.gz')
|
|
|
|
| 1 |
+
"""Offline CPU inference: DIAMOND homology search against train + UniProt references, bitscore vote + learned re-ranker (mode.json).
|
| 2 |
|
| 3 |
1. Search every unique input against the training set (diamond blastp
|
| 4 |
--very-sensitive, 50 hits).
|
|
|
|
| 328 |
votes[e] = votes.get(e, 0.0) + w / len(ecs)
|
| 329 |
if votes:
|
| 330 |
preds[i] = sorted(votes, key=lambda e: (-votes[e], -freq.get(e, 0), e))[:10]
|
| 331 |
+
# v17: learned re-ranker over the vote's top 10 candidates, used only when the best hit is below
|
| 332 |
+
# rerank_max_pid identity (it was trained and validated on official-label hard proteins there).
|
| 333 |
+
rr_path = ROOT / 'weights' / 'ranker_v2.npz'
|
| 334 |
+
if mode_cfg.get('reranker') and rr_path.exists():
|
| 335 |
+
try:
|
| 336 |
+
from rerank2 import candidate_features
|
| 337 |
+
rr = TreeRanker(rr_path)
|
| 338 |
+
srcw = mode_cfg.get('source_weights', {})
|
| 339 |
+
qs, blocks, cand_of = [], [], {}
|
| 340 |
+
for i in sorted(hits):
|
| 341 |
+
if not hits[i] or max(x[1] for x in hits[i]) >= float(mode_cfg.get('rerank_max_pid', 80)):
|
| 342 |
+
continue
|
| 343 |
+
cands, X = candidate_features(hits[i], labels, freq, srcw, int(mode_cfg['vote_k']),
|
| 344 |
+
float(mode_cfg['vote_power']))
|
| 345 |
+
if len(cands) > 1:
|
| 346 |
+
qs.append(i)
|
| 347 |
+
cand_of[i] = cands
|
| 348 |
+
blocks.append(np.asarray(X, dtype=np.float64))
|
| 349 |
+
if blocks:
|
| 350 |
+
scores = rr.score(np.vstack(blocks))
|
| 351 |
+
pos = 0
|
| 352 |
+
for i in qs:
|
| 353 |
+
c = cand_of[i]
|
| 354 |
+
sc = scores[pos:pos + len(c)]
|
| 355 |
+
pos += len(c)
|
| 356 |
+
preds[i] = [c[j] for j in np.argsort(-sc, kind='stable')]
|
| 357 |
+
log(f're-ranker v2 applied to {len(qs)} inputs')
|
| 358 |
+
except Exception as exc:
|
| 359 |
+
log(f're-ranker v2 skipped, using the vote: {exc!r}')
|
| 360 |
return preds
|
| 361 |
ranker = TreeRanker(ROOT / 'weights' / 'ranker.npz')
|
| 362 |
rel = EcReliability(ROOT / 'weights' / 'te_tables.json.gz')
|
rerank2.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Re-ranker v2: candidate-EC features from the merged hit list (train + UniProt references).
|
| 2 |
+
|
| 3 |
+
The same function builds the training rows (official-label proxy, see experiments/external) and the
|
| 4 |
+
inference rows, so features cannot drift. Candidates are the top 10 ECs of the weighted vote.
|
| 5 |
+
"""
|
| 6 |
+
import math
|
| 7 |
+
|
| 8 |
+
FEATURES = ['v12w', 'v12', 'v8', 'v4', 'v1', 'rank', 's_tr', 's_sp', 's_un', 's_u2', 'best_bits_rel', 'best_pid',
|
| 9 |
+
'best_rank', 'best_src', 'qcov', 'scov', 'n10', 'n50', 'l3', 'l2', 'lfreq', 'top_pid', 'top_bits',
|
| 10 |
+
'top_src', 'n_hits', 'n_cand', 'margin', 'pid_gap', 'nec_best']
|
| 11 |
+
SRC = {'sp_': 1, 'un_': 2, 'u2_': 3}
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def source(s):
|
| 15 |
+
"""0 = training protein (ids tr_<digits>), 1 = Swiss-Prot, 2 = TrEMBL protein-level, 3 = TrEMBL transcript-level."""
|
| 16 |
+
return 0 if s[3:].isdigit() else SRC.get(s[:3], 0)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def candidate_features(hits, labels, freq, weights, k=50, power=12):
|
| 20 |
+
"""hits: (sseqid, pid, qlen, slen, evalue, bits, qstart, qend, sstart, send) tuples, any order.
|
| 21 |
+
Returns (candidate ECs ordered by weighted vote, feature rows)."""
|
| 22 |
+
h = sorted(hits, key=lambda x: -x[5])[:k]
|
| 23 |
+
if not h:
|
| 24 |
+
return [], []
|
| 25 |
+
top = h[0][5]
|
| 26 |
+
st, l3, l2 = {}, {}, {}
|
| 27 |
+
tot = {'w': 0.0, '12': 0.0, '8': 0.0, '4': 0.0, '1': 0.0}
|
| 28 |
+
for r, (s, pid, ql, sl, ev, b, qs, qe, ss, se) in enumerate(h):
|
| 29 |
+
ecs = labels[s]
|
| 30 |
+
n = len(ecs)
|
| 31 |
+
rb = b / top
|
| 32 |
+
src = source(s)
|
| 33 |
+
w = rb ** power * (1.0 if src == 0 else weights.get(s[:3], 1.0))
|
| 34 |
+
tot['w'] += w
|
| 35 |
+
tot['12'] += rb ** 12
|
| 36 |
+
tot['8'] += rb ** 8
|
| 37 |
+
tot['4'] += rb ** 4
|
| 38 |
+
tot['1'] += rb
|
| 39 |
+
for e in ecs:
|
| 40 |
+
d = st.get(e)
|
| 41 |
+
if d is None:
|
| 42 |
+
d = st[e] = dict(w=0.0, p12=0.0, p8=0.0, p4=0.0, p1=0.0, src=[0.0, 0.0, 0.0, 0.0], bb=b, bp=pid, br=r,
|
| 43 |
+
bs=src, qc=(qe - qs + 1) / ql, sc=(se - ss + 1) / sl, n10=0, n50=0, nec=n)
|
| 44 |
+
d['w'] += w / n
|
| 45 |
+
d['p12'] += rb ** 12 / n
|
| 46 |
+
d['p8'] += rb ** 8 / n
|
| 47 |
+
d['p4'] += rb ** 4 / n
|
| 48 |
+
d['p1'] += rb / n
|
| 49 |
+
d['src'][src] += w / n
|
| 50 |
+
d['n10'] += r < 10
|
| 51 |
+
d['n50'] += 1
|
| 52 |
+
p3 = e.rsplit('.', 1)[0]
|
| 53 |
+
p2 = '.'.join(e.split('.')[:2])
|
| 54 |
+
l3[p3] = l3.get(p3, 0.0) + w / n
|
| 55 |
+
l2[p2] = l2.get(p2, 0.0) + w / n
|
| 56 |
+
cands = sorted(st, key=lambda e: (-st[e]['w'], -freq.get(e, 0), e))[:10]
|
| 57 |
+
ws = [st[e]['w'] for e in cands]
|
| 58 |
+
margin = (ws[0] - (ws[1] if len(ws) > 1 else 0.0)) / tot['w']
|
| 59 |
+
rows = []
|
| 60 |
+
for i, e in enumerate(cands):
|
| 61 |
+
d = st[e]
|
| 62 |
+
rows.append([d['w'] / tot['w'], d['p12'] / tot['12'], d['p8'] / tot['8'], d['p4'] / tot['4'], d['p1'] / tot['1'], i,
|
| 63 |
+
*[x / tot['w'] for x in d['src']], d['bb'] / top, d['bp'], d['br'], d['bs'], d['qc'], d['sc'],
|
| 64 |
+
d['n10'], d['n50'], l3[e.rsplit('.', 1)[0]] / tot['w'], l2['.'.join(e.split('.')[:2])] / tot['w'],
|
| 65 |
+
math.log1p(freq.get(e, 0)), h[0][1], top, source(h[0][0]), len(h), len(st), margin,
|
| 66 |
+
h[0][1] - d['bp'], d['nec']])
|
| 67 |
+
return cands, rows
|
weights/mode.json
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
{"scoring": "vote", "vote_k": 50, "vote_power": 12, "source_weights": {"sp_": 1.0, "un_": 0.2, "u2_": 0.1}, "extra_db": "trembl_ev2.dmnd", "extra_mode": "fast", "extra_guard": 330}
|
|
|
|
| 1 |
+
{"scoring": "vote", "vote_k": 50, "vote_power": 12, "source_weights": {"sp_": 1.0, "un_": 0.2, "u2_": 0.1}, "extra_db": "trembl_ev2.dmnd", "extra_mode": "fast", "extra_guard": 330, "reranker": true, "rerank_max_pid": 80}
|
weights/ranker_v2.npz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8909167aa4c5233416f390f70f93ac1cce2627aaaf0cb6a14535e7d3bc9b3875
|
| 3 |
+
size 193919
|