Freelance project
Drug–Drug Interaction
- Problem
- Someone holding a prescription with several drugs on it has no quick way to see whether two of them interact, or why.
- Approach
- OCR reads the drug names from a photo, each drug is turned into a molecular graph, and one shared graph attention encoder feeds two heads: does this pair interact, and through which of five mechanisms. Gemini then explains the result in plain language.
- Result
- On the held-out test set the interaction model reached AUC 0.910, F1 0.832 and accuracy 0.826.
- My role
- Freelance AI engineer (2024–2025). Built the system end to end for a client: data pipeline, model, training and app.
- Stack
- Python
- EasyOCR
- PubChem + RxNav APIs
- RDKit
- PyTorch Geometric (GATv2)
- scikit-learn
- Gemini
- Streamlit
- Links
Code private (client work)
01
Architecture
- Prescription photo: the user uploads a photo of a prescription in the Streamlit app.
- EasyOCR reads the text in the photo and the drug names are picked out of it.
- Drug lookup: each name is resolved through PubChem to a SMILES string (the molecule's structure) and through RxNav to its drug class.
- RDKit graphs: RDKit parses each SMILES into a molecular graph, with atoms as nodes (carrying features such as element, degree, charge and aromaticity) and bonds as edges.
- Shared GATv2 encoder: both drugs go through the same 3-layer GATv2 graph attention network (4 attention heads in the first two layers, one in the last), and mean pooling turns each molecule into one embedding.
- Two heads: the pair of embeddings feeds an interaction head (does this pair interact?) and a mechanism head that picks one of five types: pharmacokinetic, pharmacodynamic, CYP450 inhibition, protein binding or transporter effect.
- Gemini turns the prediction and the mechanism into a short explanation a non-specialist can read.
- Streamlit app: the results page shows the prediction, the mechanism and the explanation for each pair.
02
Key decisions
One shared GNN encoder for both drugs
Encoding drug A and drug B with the same weights means every molecule in the training set trains one encoder, and a drug gets the same embedding whichever side of the pair it is on. The cost is that the encoder cannot learn role-specific features by itself (for example, which drug inhibits the other's metabolism); that has to come from the heads.
Predict from molecular structure, not from a table of known pairs
Working from the molecular graph lets the model score a pair it never saw in training, as long as both structures can be looked up. The trade-off is a dependency on PubChem returning a SMILES string for the OCR'd name, and structure alone knows nothing about dose or the patient.
The LLM explains; it does not decide
Gemini only rephrases the model's output, so the yes/no and the mechanism always come from the trained model, which has measured test scores. The explanation reads well, but it can still sound more certain than the model is, so it is shown next to the prediction rather than instead of it.
03
Results
- Interaction prediction on the held-out test set: AUC 0.910, F1 0.832, accuracy 0.826.
- Covers the full path from a photo of a prescription to a readable explanation in one app.
- Five mechanism classes predicted alongside the interaction: pharmacokinetic, pharmacodynamic, CYP450 inhibition, protein binding and transporter effect.
04
What I'd do next
- Report the mechanism head's accuracy separately; the published test scores are for the interaction prediction.
- Add a confirmation step after OCR so the user can fix a misread drug name before it is looked up.
- Calibrate the interaction probability so the score shown to the user means what it says.