infernogod commited on
Commit
0c025bc
·
verified ·
1 Parent(s): cb7d5d8
Files changed (5) hide show
  1. README.md +4 -1
  2. predict.py +30 -1
  3. rerank2.py +67 -0
  4. weights/mode.json +1 -1
  5. 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
- The labels for this 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.
 
 
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