tox21_deepberta / predict.py
Aayush Kothari
DeepBERTa fine-tuned on Tox21 Challenge data
0549373
Raw History Blame Contribute Delete
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}")