Spaces:
Sleeping
Sleeping
Download predict.py from aakothari/tox21_deepberta: direct link, hf CLI and curl.
- Browser
- Download file 4.66 kB
-
https://huggingface.co/spaces/aakothari/tox21_deepberta/resolve/main/predict.py
- Command line
-
hf download hf://spaces/aakothari/tox21_deepberta/predict.py
-
curl -L -o predict.py https://huggingface.co/spaces/aakothari/tox21_deepberta/resolve/main/predict.py
4.66 kB
| """ | |
| Inference for the Tox21 leaderboard Space. | |
| Contract (from the leaderboard paper, Appendix A.1 and Section 4): | |
| - predict() takes a list of SMILES strings | |
| - returns {smiles: {task_name: score}} for ALL 12 tasks | |
| - scores are floats in [0, 1] | |
| - EVERY input molecule must get a prediction for EVERY task. The leaderboard | |
| validates all molecule-target pairs are present and errors out otherwise. | |
| Never drop a molecule, no matter what the preprocessing does. | |
| """ | |
| import os | |
| import torch | |
| import torch.nn as nn | |
| import deepsmiles | |
| from rdkit import Chem, RDLogger | |
| from transformers import AutoTokenizer, AutoModel | |
| RDLogger.DisableLog("rdApp.*") | |
| TASKS = [ | |
| "NR-AR", "NR-AR-LBD", "NR-AhR", "NR-Aromatase", "NR-ER", "NR-ER-LBD", | |
| "NR-PPAR-gamma", "SR-ARE", "SR-ATAD5", "SR-HSE", "SR-MMP", "SR-p53", | |
| ] | |
| CHECKPOINT = os.environ.get("CHECKPOINT_DIR", "checkpoints/deepberta-tox21") | |
| CONVERTER = deepsmiles.Converter(rings=True, branches=True) | |
| DEVICE = "cuda" if torch.cuda.is_available() else "cpu" | |
| # Neutral score for molecules we cannot encode. 0.5 is uninformative for | |
| # ROC-AUC rather than actively wrong. Any conversion failures MUST be | |
| # reported in the model card -- silently scoring 0.5 on part of the test | |
| # set is exactly the kind of thing reviewers need disclosed. | |
| FALLBACK_SCORE = 0.5 | |
| class DeepBERTaForTox21(nn.Module): | |
| def __init__(self, path, n_tasks=12, dropout=0.1): | |
| super().__init__() | |
| self.encoder = AutoModel.from_pretrained(path) | |
| self.dropout = nn.Dropout(dropout) | |
| self.classifier = nn.Linear(self.encoder.config.hidden_size, n_tasks) | |
| def forward(self, input_ids, attention_mask): | |
| out = self.encoder(input_ids=input_ids, attention_mask=attention_mask) | |
| mask = attention_mask.unsqueeze(-1).float() | |
| pooled = (out.last_hidden_state * mask).sum(1) / mask.sum(1).clamp(min=1e-9) | |
| return self.classifier(self.dropout(pooled)) | |
| _tokenizer = AutoTokenizer.from_pretrained(CHECKPOINT) | |
| _model = DeepBERTaForTox21(CHECKPOINT).to(DEVICE) | |
| _model.load_state_dict( | |
| torch.load(os.path.join(CHECKPOINT, "model.pt"), map_location=DEVICE, | |
| weights_only=True) | |
| ) | |
| _model.eval() | |
| def _encode(smiles): | |
| """SMILES -> DeepSMILES. Returns None if unconvertible.""" | |
| try: | |
| mol = Chem.MolFromSmiles(smiles) | |
| if mol is None: | |
| return None | |
| return CONVERTER.encode(Chem.MolToSmiles(mol)) | |
| except Exception: | |
| return None | |
| def predict(smiles_list, batch_size=32): | |
| results = {s: {t: FALLBACK_SCORE for t in TASKS} for s in smiles_list} | |
| encodable = [(s, _encode(s)) for s in smiles_list] | |
| usable = [(s, d) for s, d in encodable if d is not None] | |
| n_failed = len(smiles_list) - len(usable) | |
| if n_failed: | |
| print(f"WARNING: {n_failed}/{len(smiles_list)} molecules failed " | |
| f"DeepSMILES conversion; returning {FALLBACK_SCORE} for these.") | |
| with torch.no_grad(): | |
| for i in range(0, len(usable), batch_size): | |
| chunk = usable[i:i + batch_size] | |
| enc = _tokenizer( | |
| [d for _, d in chunk], padding=True, truncation=True, | |
| max_length=256, return_tensors="pt", | |
| ).to(DEVICE) | |
| # sigmoid: the leaderboard requires values in [0, 1], not logits | |
| probs = torch.sigmoid(_model(**enc)).cpu().numpy() | |
| for (smi, _), row in zip(chunk, probs): | |
| results[smi] = {t: float(p) for t, p in zip(TASKS, row)} | |
| # The leaderboard requires a prediction for every molecule-target pair it | |
| # sent. The test set is 647 molecules but only 645 unique structures, so | |
| # check against the unique inputs -- and verify none went missing. | |
| missing = set(smiles_list) - set(results) | |
| assert not missing, f"no prediction for {len(missing)} molecules: {list(missing)[:5]}" | |
| for s, d in results.items(): | |
| assert set(d) == set(TASKS), f"incomplete predictions for {s}" | |
| assert all(0.0 <= v <= 1.0 for v in d.values()), f"out of range for {s}" | |
| print(f"returned predictions for {len(results)} unique structures " | |
| f"from {len(smiles_list)} inputs, {len(TASKS)} tasks each") | |
| return results | |
| if __name__ == "__main__": | |
| # awkward cases worth checking before deploying: salts, stereochemistry, | |
| # charged species, very small molecules, and deliberate garbage | |
| tests = [ | |
| "CCO", | |
| "CC(=O)Oc1ccccc1C(=O)O", | |
| "C[C@H](N)C(=O)O", | |
| "[Na+].[Cl-]", | |
| "c1ccc2c(c1)ccc1ccccc12", | |
| "not_a_molecule", | |
| ] | |
| for smi, preds in predict(tests).items(): | |
| print(f"{smi:35s} {preds['NR-AR']:.3f} {preds['SR-MMP']:.3f}") |