Download predict.py from Da24b048/enzyme-retrieval-bert: direct link, hf CLI and curl.
- Browser
- Download file 6.15 kB
-
https://huggingface.co/Da24b048/enzyme-retrieval-bert/resolve/main/predict.py
- Command line
-
hf download hf://Da24b048/enzyme-retrieval-bert/predict.py
-
curl -L -o predict.py https://huggingface.co/Da24b048/enzyme-retrieval-bert/resolve/main/predict.py
6.15 kB
| """Offline CPU inference: ensemble of fine-tuned reaction BERTs (int8) blended with a TF-IDF reaction kNN. | |
| Output: top-5 distinct ECs per unique input, best first (only the first is scored).""" | |
| import argparse | |
| import json | |
| import re | |
| from pathlib import Path | |
| import numpy as np | |
| import pandas as pd | |
| import scipy.sparse as sp | |
| import torch | |
| import torch.nn as nn | |
| from transformers import BertConfig, BertModel | |
| import featurize as F | |
| from inputs import prepare_inputs | |
| SMI_RE = re.compile(r"(\[[^\]]+]|Br?|Cl?|N|O|S|P|F|I|b|c|n|o|s|p|\(|\)|\.|=|#|-|\+|\\|/|:|~|@|\?|>>?|\*|\$|%[0-9]{2}|[0-9])") | |
| MAX_LEN = 256 | |
| TOP = 5 | |
| class Net(nn.Module): | |
| def __init__(self, config, n_cls, n_aux): | |
| super().__init__() | |
| self.bert = BertModel(config, add_pooling_layer=False) | |
| self.head = nn.Linear(config.hidden_size, n_cls) | |
| self.aux = nn.ModuleList([nn.Linear(config.hidden_size, n) for n in n_aux]) | |
| def forward(self, ids, mask): | |
| x = self.bert(input_ids=ids, attention_mask=mask).last_hidden_state | |
| m = mask.unsqueeze(-1).to(x.dtype) | |
| return self.head((x * m).sum(1) / m.sum(1)) | |
| def bert_probs(wdir, rxns, all_idx, quantize): | |
| """Softmax probabilities over the shared class list (all_idx maps this model's classes into it).""" | |
| classes = json.loads((wdir / 'classes.json').read_text()) | |
| tokens = (wdir / 'vocab.txt').read_text().splitlines() | |
| idx = {t: i for i, t in enumerate(tokens)} | |
| state = torch.load(wdir / 'best.pt', map_location='cpu', weights_only=True) | |
| n_aux = [state[f'aux.{i}.weight'].shape[0] for i in range(3)] if 'aux.0.weight' in state else [] | |
| net = Net(BertConfig.from_pretrained(wdir), len(classes), n_aux) | |
| net.load_state_dict(state, strict=False) | |
| net.eval() | |
| if quantize: | |
| net = torch.quantization.quantize_dynamic(net, {nn.Linear}, dtype=torch.qint8) | |
| enc = [[idx['[CLS]']] + [idx.get(t, 1) for t in SMI_RE.findall(r)][:MAX_LEN - 2] + [idx['[SEP]']] for r in rxns] | |
| order = sorted(range(len(enc)), key=lambda i: len(enc[i])) | |
| out = np.zeros((len(rxns), len(all_idx[1])), np.float32) | |
| with torch.inference_mode(): | |
| for a in range(0, len(order), 64): | |
| b = order[a:a + 64] | |
| n = max(len(enc[i]) for i in b) | |
| ids = torch.zeros(len(b), n, dtype=torch.long) | |
| mask = torch.zeros(len(b), n, dtype=torch.long) | |
| for j, i in enumerate(b): | |
| ids[j, :len(enc[i])] = torch.tensor(enc[i]); mask[j, :len(enc[i])] = 1 | |
| p = torch.softmax(net(ids, mask).float(), -1).numpy() | |
| for j, i in enumerate(b): | |
| out[i, all_idx[0]] = p[j] | |
| return out | |
| def knn_probs(kdir, Q, n, all_cls, k=10, power=8): | |
| X = sp.load_npz(kdir / 'X.npz').tocsr() | |
| Y = sp.load_npz(kdir / 'Y.npz').tocsr() | |
| kc = json.loads((kdir / 'classes.json').read_text()) | |
| XT = X.T.tocsr() | |
| out = np.zeros((n, len(kc)), np.float32) | |
| for a in range(0, n, 250): | |
| S = (Q[a:a + 250] @ XT).toarray() | |
| ik = np.argpartition(-S, k, axis=1)[:, :k] | |
| sk = np.maximum(np.take_along_axis(S, ik, 1), 0) ** power | |
| for j in range(k): | |
| out[a:a + 250] += sk[:, j:j + 1] * Y[ik[:, j]].toarray() | |
| out /= np.maximum(out.sum(1, keepdims=True), 1e-9) | |
| pos = {e: i for i, e in enumerate(all_cls)} | |
| full = np.zeros((n, len(all_cls)), np.float32) | |
| for i, e in enumerate(kc): | |
| if e in pos: | |
| full[:, pos[e]] = out[:, i] | |
| return full | |
| def mlp_probs(mdir, Q, all_cls): | |
| """Fingerprint MLP (Linear-ReLU-Linear) on the same TF-IDF features as the kNN.""" | |
| st = torch.load(mdir / 'mlp.pt', map_location='cpu', weights_only=True) | |
| cl = json.loads((mdir / 'classes.json').read_text()) | |
| net = nn.Sequential(nn.Linear(st['0.weight'].shape[1], st['0.weight'].shape[0]), nn.ReLU(), nn.Dropout(0.0), | |
| nn.Linear(st['3.weight'].shape[1], st['3.weight'].shape[0])) | |
| net.load_state_dict({k: v.float() for k, v in st.items()}) | |
| net.eval() | |
| pos = {e: i for i, e in enumerate(all_cls)} | |
| idx = [pos[e] for e in cl] | |
| full = np.zeros((Q.shape[0], len(all_cls)), np.float32) | |
| with torch.inference_mode(): | |
| for a in range(0, Q.shape[0], 512): | |
| full[a:a + 512][:, idx] = torch.softmax(net(torch.from_numpy(Q[a:a + 512].toarray())), -1).numpy() | |
| return full | |
| def predict_file(input_path, output_path): | |
| torch.set_num_threads(2) | |
| root = Path(__file__).parent | |
| w = root / 'weights' | |
| cfg = json.loads((w / 'blend.json').read_text()) | |
| frame = prepare_inputs(pd.read_parquet(input_path)) | |
| rxns = frame.rxn_smiles.tolist() | |
| valid = set(json.loads((root / 'ec_vocabulary.json').read_text())) | |
| kdir = w / 'knn' | |
| all_cls = json.loads((kdir / 'classes.json').read_text()) | |
| pos = {e: i for i, e in enumerate(all_cls)} | |
| probs = 0 | |
| models = sorted(d for d in w.iterdir() if d.is_dir() and (d / 'best.pt').exists()) | |
| for d in models: | |
| cl = json.loads((d / 'classes.json').read_text()) | |
| probs = probs + bert_probs(d, rxns, ([pos[e] for e in cl], all_cls), cfg['quantize']) | |
| probs = probs / len(models) | |
| kw, mw = cfg['knn_weight'], cfg.get('mlp_weight', 0.0) | |
| idf = np.load(kdir / 'idf.npz') | |
| whole, diff = F.raw_features(rxns) | |
| Q = F.combine(whole, diff, idf['whole'], idf['diff']) | |
| blend = (1 - kw - mw) * probs + kw * knn_probs(kdir, Q, len(rxns), all_cls) | |
| if mw: | |
| blend = blend + mw * mlp_probs(w / 'mlp', Q, all_cls) | |
| probs = blend | |
| top = np.argsort(-probs, axis=1)[:, :TOP] | |
| with Path(output_path).open('w') as out: | |
| for key, row in zip(frame.id, top): | |
| p = [all_cls[t] for t in row if all_cls[t] in valid] | |
| out.write(json.dumps({'id': key, 'prediction': p or [all_cls[0]]}) + '\n') | |
| if __name__ == '__main__': | |
| ap = argparse.ArgumentParser(description=__doc__) | |
| ap.add_argument('--input', required=True) | |
| ap.add_argument('--output', required=True) | |
| a = ap.parse_args() | |
| predict_file(a.input, a.output) | |