CoolFace
Apppublic

aakothari/tox21_deepberta

sourceHugging Facemitupdated 6d agoView on Hugging Face
0likes
predict.py121 linesDownload Raw Back to root
1"""2Inference for the Tox21 leaderboard Space.3 4Contract (from the leaderboard paper, Appendix A.1 and Section 4):5  - predict() takes a list of SMILES strings6  - returns {smiles: {task_name: score}} for ALL 12 tasks7  - scores are floats in [0, 1]8  - EVERY input molecule must get a prediction for EVERY task. The leaderboard9    validates all molecule-target pairs are present and errors out otherwise.10    Never drop a molecule, no matter what the preprocessing does.11"""12 13import os14import torch15import torch.nn as nn16import deepsmiles17from rdkit import Chem, RDLogger18from transformers import AutoTokenizer, AutoModel19 20RDLogger.DisableLog("rdApp.*")21 22TASKS = [23    "NR-AR", "NR-AR-LBD", "NR-AhR", "NR-Aromatase", "NR-ER", "NR-ER-LBD",24    "NR-PPAR-gamma", "SR-ARE", "SR-ATAD5", "SR-HSE", "SR-MMP", "SR-p53",25]26 27CHECKPOINT = os.environ.get("CHECKPOINT_DIR", "checkpoints/deepberta-tox21")28CONVERTER = deepsmiles.Converter(rings=True, branches=True)29DEVICE = "cuda" if torch.cuda.is_available() else "cpu"30 31# Neutral score for molecules we cannot encode. 0.5 is uninformative for32# ROC-AUC rather than actively wrong. Any conversion failures MUST be33# reported in the model card -- silently scoring 0.5 on part of the test34# set is exactly the kind of thing reviewers need disclosed.35FALLBACK_SCORE = 0.536 37 38class DeepBERTaForTox21(nn.Module):39    def __init__(self, path, n_tasks=12, dropout=0.1):40        super().__init__()41        self.encoder = AutoModel.from_pretrained(path)42        self.dropout = nn.Dropout(dropout)43        self.classifier = nn.Linear(self.encoder.config.hidden_size, n_tasks)44 45    def forward(self, input_ids, attention_mask):46        out = self.encoder(input_ids=input_ids, attention_mask=attention_mask)47        mask = attention_mask.unsqueeze(-1).float()48        pooled = (out.last_hidden_state * mask).sum(1) / mask.sum(1).clamp(min=1e-9)49        return self.classifier(self.dropout(pooled))50 51 52_tokenizer = AutoTokenizer.from_pretrained(CHECKPOINT)53_model = DeepBERTaForTox21(CHECKPOINT).to(DEVICE)54_model.load_state_dict(55    torch.load(os.path.join(CHECKPOINT, "model.pt"), map_location=DEVICE,56               weights_only=True)57)58_model.eval()59 60 61def _encode(smiles):62    """SMILES -> DeepSMILES. Returns None if unconvertible."""63    try:64        mol = Chem.MolFromSmiles(smiles)65        if mol is None:66            return None67        return CONVERTER.encode(Chem.MolToSmiles(mol))68    except Exception:69        return None70 71 72def predict(smiles_list, batch_size=32):73    results = {s: {t: FALLBACK_SCORE for t in TASKS} for s in smiles_list}74 75    encodable = [(s, _encode(s)) for s in smiles_list]76    usable = [(s, d) for s, d in encodable if d is not None]77    n_failed = len(smiles_list) - len(usable)78    if n_failed:79        print(f"WARNING: {n_failed}/{len(smiles_list)} molecules failed "80              f"DeepSMILES conversion; returning {FALLBACK_SCORE} for these.")81 82    with torch.no_grad():83        for i in range(0, len(usable), batch_size):84            chunk = usable[i:i + batch_size]85            enc = _tokenizer(86                [d for _, d in chunk], padding=True, truncation=True,87                max_length=256, return_tensors="pt",88            ).to(DEVICE)89            # sigmoid: the leaderboard requires values in [0, 1], not logits90            probs = torch.sigmoid(_model(**enc)).cpu().numpy()91            for (smi, _), row in zip(chunk, probs):92                results[smi] = {t: float(p) for t, p in zip(TASKS, row)}93 94    # The leaderboard requires a prediction for every molecule-target pair it95    # sent. The test set is 647 molecules but only 645 unique structures, so96    # check against the unique inputs -- and verify none went missing.97    missing = set(smiles_list) - set(results)98    assert not missing, f"no prediction for {len(missing)} molecules: {list(missing)[:5]}"99    for s, d in results.items():100        assert set(d) == set(TASKS), f"incomplete predictions for {s}"101        assert all(0.0 <= v <= 1.0 for v in d.values()), f"out of range for {s}"102 103    print(f"returned predictions for {len(results)} unique structures "104          f"from {len(smiles_list)} inputs, {len(TASKS)} tasks each")105 106    return results107 108 109if __name__ == "__main__":110    # awkward cases worth checking before deploying: salts, stereochemistry,111    # charged species, very small molecules, and deliberate garbage112    tests = [113        "CCO",114        "CC(=O)Oc1ccccc1C(=O)O",115        "C[C@H](N)C(=O)O",116        "[Na+].[Cl-]",117        "c1ccc2c(c1)ccc1ccccc12",118        "not_a_molecule",119    ]120    for smi, preds in predict(tests).items():121        print(f"{smi:35s} {preds['NR-AR']:.3f}  {preds['SR-MMP']:.3f}")