BigSMILES and BigSmirk¶
BigSmirk tokenizes the BigSMILES encoding for macromolecules all the way down to their constituent elements.
Let’s see it in action!
🐍 Installation is easy with pre-build binaries on PyPI and GitHub. Just run: pip install smirk
Installing from source? See the developer guide for instructions.
!python -m pip install smirk transformers
First steps¶
🤗 smirk subclasses Hugging Face’s PreTrainedTokenizerBase for seamless compatibility and leverages Tokenizers for raw rust-powered speed. No need to learn another framework; everything works out of the box 🎁
from smirk import SmirkBigSmilesFast
# Just import and tokenize!
bigsmirk = SmirkBigSmilesFast()
bigsmirk("{[][$]CC[$],[$]CC(CC)[$][]}") # ethylene butene copolymer
{'input_ids': [159, 148, 150, 148, 2, 150, 45, 45, 148, 2, 150, 161, 148, 2, 150, 45, 45, 4, 45, 45, 5, 148, 2, 150, 148, 150, 160], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
# Batch Tokenization with Padding
batch = bigsmirk([
"[H]O{[>][<]C(=O)CCCCC(=O)[<],[>]NCCCCCCN[>][<]}[H]", # nylon-6,6
"{[][<]OCC[>][<]}{[>][<]OC(C)C[>][]}", # block copolymer
"{[][<]C(=O)c1ccc(cc1)C(=O)[<],[>]OCCO[>][]}", # alternation co-polymer
], padding="longest")
batch
{'input_ids': [[148, 71, 150, 102, 159, 148, 164, 150, 148, 163, 150, 45, 4, 22, 102, 5, 45, 45, 45, 45, 45, 4, 22, 102, 5, 148, 163, 150, 161, 148, 164, 150, 93, 45, 45, 45, 45, 45, 45, 93, 148, 164, 150, 148, 163, 150, 160, 148, 71, 150], [159, 148, 150, 148, 163, 150, 102, 45, 45, 148, 164, 150, 148, 163, 150, 160, 159, 148, 164, 150, 148, 163, 150, 102, 45, 4, 45, 5, 45, 148, 164, 150, 148, 150, 160, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168], [159, 148, 150, 148, 163, 150, 45, 4, 22, 102, 5, 153, 12, 153, 153, 153, 4, 153, 153, 12, 5, 45, 4, 22, 102, 5, 148, 163, 150, 161, 148, 164, 150, 102, 45, 45, 102, 148, 164, 150, 148, 150, 160, 168, 168, 168, 168, 168, 168, 168]], 'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]]}
# Back to polymers!
bigsmirk.batch_decode(batch["input_ids"], skip_special_tokens=True)
['[H]O{[>][<]C(=O)CCCCC(=O)[<],[>]NCCCCCCN[>][<]}[H]',
'{[][<]OCC[>][<]}{[>][<]OC(C)C[>][]}',
'{[][<]C(=O)c1ccc(cc1)C(=O)[<],[>]OCCO[>][]}']
Let’s visualize BigSMILES token boundaries for PVC (Polyvinyl chloride ) and sPP (Syndiotactic Polypropylene) by coloring each token in sequence.
# Polyvinyl chloride (PVC)
render_colored_tokens("{[][$]CC(Cl)[$][]}")
# Syndiotactic polypropylene (sPP)
render_colored_tokens("CC{[>][<]C[C@@H](C)C[C@H](C)[>];[<]C=CC,[<]C[C@H](C)C=CC[]}")
Handling the Fragment Name Definition Notation¶
The BigSMILES line notation allows some portions of the BigSMILES representations be replaced by more abstract but compact proxy fragment names, for example, the names of repeating units.
The smirk BigSMILES tokenizers handles this '[' + '#' + fragment_name + ']' syntax by replacing the fragment name with its definition and then tokenizing the expanded BigSMILES.
The definition should be provided within the BigSMILES as specified by the line notation i.e.:
BigSMILES_string + '[' + '#' + fragment_name + ']' + BigSMILES_string + '.' + '{' + '#' + fragment_name + '=' + BigSMILES_string + '}'
For example:
C([#Arm1])([#Arm2]).{#Arm1=CO}.{#Arm2=N} --> C(CO)(N) ---> 'C', '(', 'C', 'O', ')', '(', 'N', ')'
More examples of valid and invalid use of the fragment name definition notation are provided below.
abstract_label_examples = {
"defined [#label] placeholder": "C([#Arm])([#Arm])([#Arm])[#Arm].{#Arm=CO{[<][>]CCO[<][>]}}",
"multiple [#label] placeholders": "C([#Arm1])([#Arm2]).{#Arm1=CO}.{#Arm2=N}",
"[#label] in stochastic object": "{[][$]CC(C)([#Side])[$][]}.{#Side=C(=O)OCC}",
"undefined [#label] placeholder": "C([#Arm])([#Arm])",
"bare labels stay unknown": r"A([$1[<1]1])R(A'[$1[>1]1]).{#A=C}.{#R=C}",
}
for label, text in abstract_label_examples.items():
encoded = bigsmirk(text, add_special_tokens=False)
tokens = bigsmirk.convert_ids_to_tokens(encoded["input_ids"])
decoded = bigsmirk.decode(encoded["input_ids"], skip_special_tokens=True)
print(label)
print("input:", text)
print("tokens:", tokens)
print("unknowns:", tokens.count(bigsmirk.unk_token))
print("decoded:", decoded)
print()
defined [#label] placeholder
input: C([#Arm])([#Arm])([#Arm])[#Arm].{#Arm=CO{[<][>]CCO[<][>]}}
tokens: ['C', '(', 'C', 'O', '{', '[', '<', ']', '[', '>', ']', 'C', 'C', 'O', '[', '<', ']', '[', '>', ']', '}', ')', '(', 'C', 'O', '{', '[', '<', ']', '[', '>', ']', 'C', 'C', 'O', '[', '<', ']', '[', '>', ']', '}', ')', '(', 'C', 'O', '{', '[', '<', ']', '[', '>', ']', 'C', 'C', 'O', '[', '<', ']', '[', '>', ']', '}', ')', 'C', 'O', '{', '[', '<', ']', '[', '>', ']', 'C', 'C', 'O', '[', '<', ']', '[', '>', ']', '}']
unknowns: 0
decoded: C(CO{[<][>]CCO[<][>]})(CO{[<][>]CCO[<][>]})(CO{[<][>]CCO[<][>]})CO{[<][>]CCO[<][>]}
multiple [#label] placeholders
input: C([#Arm1])([#Arm2]).{#Arm1=CO}.{#Arm2=N}
tokens: ['C', '(', 'C', 'O', ')', '(', 'N', ')']
unknowns: 0
decoded: C(CO)(N)
[#label] in stochastic object
input: {[][$]CC(C)([#Side])[$][]}.{#Side=C(=O)OCC}
tokens: ['{', '[', ']', '[', '$', ']', 'C', 'C', '(', 'C', ')', '(', 'C', '(', '=', 'O', ')', 'O', 'C', 'C', ')', '[', '$', ']', '[', ']', '}']
unknowns: 0
decoded: {[][$]CC(C)(C(=O)OCC)[$][]}
undefined [#label] placeholder
input: C([#Arm])([#Arm])
tokens: ['C', '(', '[', '[UNK]', ']', ')', '(', '[', '[UNK]', ']', ')']
unknowns: 2
decoded: C([])([])
bare labels stay unknown
input: A([$1[<1]1])R(A'[$1[>1]1]).{#A=C}.{#R=C}
tokens: ['[UNK]', '(', '[', '$', '1', '[', '<', '1', ']', '1', ']', ')', '[UNK]', '(', '[UNK]', '[', '$', '1', '[', '>', '1', ']', '1', ']', ')']
unknowns: 3
decoded: ([$1[<1]1])([$1[>1]1])
Zero to Polymer Foundation Model with Smirk!¶
Let’s train a small RoBERTa model on polymers from S. Choi et al., 2024 using Hugging Face and smirk.
!python -m pip install accelerate datasets torch
Dataset Preprocessing¶
This tutorial uses the BigSMILES dataset generated by Choi et al and published on Figshare. The hidden setup cell below downloads and extracts the required CSV automatically.
💡 Hugging Face/ Tokenizers may raise a warning about being forked as we’ve already used our tokenizers (this isn’t a smirk issue). It’s harmless, but when actually training it’s best to avoid tokenization until after the fork to benefit from the rust-level parallelism
🎉 That’s it! We’ve tokenized all of the BigSMILES dataset using smirk!
dataset["train"].to_pandas().head()
| BigSMILES | input_ids | attention_mask | |
|---|---|---|---|
| 0 | {<c1ccc(n1CC)>} | [159, 163, 153, 12, 153, 153, 153, 4, 154, 12,... | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] |
| 1 | {<NC(CO)C(=O)>} | [159, 163, 93, 45, 4, 45, 102, 5, 45, 4, 22, 1... | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] |
| 2 | {<c1cccc(c1)N2C(=O)c3ccc(cc3C2=O)Oc4ccc5c(c4)C... | [159, 163, 153, 12, 153, 153, 153, 153, 4, 153... | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ... |
| 3 | {<CCCCCCCCCCOC(=O)C(=O)O>} | [159, 163, 45, 45, 45, 45, 45, 45, 45, 45, 45,... | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ... |
| 4 | {<CCOCCOCCOC(=O)c1ccc(cc1)C(=O)O>} | [159, 163, 45, 45, 102, 45, 45, 102, 45, 45, 1... | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ... |
Training¶
Once we’ve tokenized the dataset, training the model is just a matter of configuration.
from transformers import Trainer, RobertaForMaskedLM, RobertaConfig, DataCollatorForLanguageModeling
# A very small model for demonstrating training a molecular foundation model with smirk
config = RobertaConfig(
vocab_size=len(bigsmirk),
hidden_size=256,
intermediate_size=1024,
num_hidden_layers=4,
num_attention_heads=4,
)
model = RobertaForMaskedLM(config)
# Setup up the trainer to use our dataset
trainer = Trainer(
model=model,
train_dataset=dataset["train"],
eval_dataset=dataset["test"],
processing_class=bigsmirk,
data_collator=DataCollatorForLanguageModeling(bigsmirk), # The data collator needs to know about our tokenizer
)
trainer.train()
MLM Example: Predict a Masked Token¶
Mask one token in a BigSMILES string and ask the trained model for top predictions.
import torch
inference_model = trainer.model
inference_model.eval()
device = next(inference_model.parameters()).device
sample = dataset["test"][5]["BigSMILES"]
# Encode and choose a position to mask
encoded = bigsmirk(sample, add_special_tokens=False)
input_ids = encoded["input_ids"]
tokens = bigsmirk.convert_ids_to_tokens(input_ids)
mask_pos = len(tokens) // 2
masked_ids = input_ids.copy()
masked_ids[mask_pos] = bigsmirk.mask_token_id
masked_tokens = bigsmirk.convert_ids_to_tokens(masked_ids)
inputs = {
"input_ids": torch.tensor([masked_ids],device=device),
"attention_mask": torch.ones((1, len(masked_ids)), device=device),
}
with torch.no_grad():
logits = inference_model(**inputs).logits[0, mask_pos].detach().cpu()
probs = torch.softmax(logits, dim=-1)
top_k = 5
top_ids = torch.topk(probs, k=top_k).indices.tolist()
top_tokens = bigsmirk.convert_ids_to_tokens(top_ids)
print("Original:", sample)
print("Masked :", "".join(masked_tokens))
print(f"Masked token index: {mask_pos} (original token: {tokens[mask_pos]})")
print("\nTop predictions:")
for rank, (tok_id, tok) in enumerate(zip(top_ids, top_tokens), start=1):
candidate_ids = masked_ids.copy()
candidate_ids[mask_pos] = tok_id
candidate = bigsmirk.decode(candidate_ids, skip_special_tokens=True)
score = probs[tok_id].item()
print(f"{rank}. token={tok!r:>4} p={score:.4f} -> {candidate}")
Original: {<CCc1ccc(c2cccnc12)>}
Masked : {<CCc1ccc(c[MASK]cccnc12)>}
Masked token index: 11 (original token: 2)
Top predictions:
1. token='Zr' p=0.0127 -> {<CCc1ccc(cZrcccnc12)>}
2. token='Yb' p=0.0108 -> {<CCc1ccc(cYbcccnc12)>}
3. token= 'B' p=0.0103 -> {<CCc1ccc(cBcccnc12)>}
4. token='Sm' p=0.0103 -> {<CCc1ccc(cSmcccnc12)>}
5. token='Es' p=0.0100 -> {<CCc1ccc(cEscccnc12)>}