Scikit-learn
Joblib
dom_ml
mass-spectrometry
molecular-formula
dissolved-organic-matter
machine-learning
scikit-learn
custom_code
Instructions to use SaeedLab/dom-formula-assignment-using-ml with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Scikit-learn
How to use SaeedLab/dom-formula-assignment-using-ml with Scikit-learn:
from huggingface_hub import hf_hub_download import joblib model = joblib.load( hf_hub_download("SaeedLab/dom-formula-assignment-using-ml", "sklearn_model.joblib") ) # only load pickle files from sources you trust # read more about it here https://skops.readthedocs.io/en/stable/persistence.html - Notebooks
- Google Colab
- Kaggle
Upload folder using huggingface_hub
Browse files- .gitattributes +1 -0
- DecisionTree.joblib +3 -0
- README.md +140 -34
- RandomForest.joblib +3 -0
- architecture.png +3 -0
- config.json +15 -0
- configuration_dom_ml.py +19 -0
- modeling_dom_ml.py +354 -0
- requirements.txt +7 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
architecture.png filter=lfs diff=lfs merge=lfs -text
|
DecisionTree.joblib
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a2ec37a3f117ef6cd281d12ffbf2a91779513a4c80b230ea7def4552628e57df
|
| 3 |
+
size 4984593
|
README.md
CHANGED
|
@@ -14,7 +14,7 @@ datasets:
|
|
| 14 |
# DOM Formula Assignment using Machine Learning
|
| 15 |
|
| 16 |
|
| 17 |
-

|
| 19 |

|
| 20 |
[](https://github.com/pcdslab/dom-formula-assignment-using-ml)
|
|
@@ -30,47 +30,153 @@ datasets:
|
|
| 30 |
## Abstract
|
| 31 |
A machine learning approach to molecular formula assignment is crucial for unlocking the full potential of ultra-high resolution mass spectrometry (UHRMS) when analyzing complex mixtures. By combining data-driven models with rigorous benchmarking, the accuracy, consistency, and speed in identifying plausible molecular formulas from vast spectral datasets can be improved. Compared with traditional de novo methods that rely heavily on rule-based heuristics, and manual parameter tuning, machine learning approaches can capture complex patterns in data and adapt more readily to diverse sample types. In this paper, we describe the application of a machine learning methods using the k-nearest neighbors (KNN) algorithm trained on curated chemical formula datasets of UHRMS analysis of dissolved organic matter (DOM) covering the saline river continuum and tropical wet/dry season variability. The influence of the mass accuracy (training set with 0.15-1ppm) was evaluated on a blind test set of DOMs of different geographical origins. A Decision Tree Regressor (DTR) and Random Forest Regressor (RFR) based on mass accuracy (<1ppm) was used. Results from our ML models exhibit 43% more formulas annotated than traditional methods (5796 vs 4047), Model-Synthetic achieved 99.9% assignment rate and annotated/assigned 2x more formulas (8,268 vs 4047). DTR and RFR achieved formula-level accuracies (FA) of 86.5% and 60.4%, respectively. Overall, results show an increase in formula assignment when compared with traditional methods. This ultimately enables more reliable characterization of complex natural and engineered systems, supporting advances in fields such as environmental science, metabolomics, and petroleomics. Furthermore, the novel data set produced for this study is made publicly available, establishing an initial benchmark for molecular formula assignment in UHRMS using machine learning. The dataset and code are publicly available at: https://github.com/pcdslab/dom-formula-assignment-using-ml
|
| 32 |
|
| 33 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
This repository contains pre-trained KNN models used for molecular formula assignment. The models vary by:
|
| 36 |
-
- **Dataset**: L1, L3, L1-L3, Synthetic
|
| 37 |
- **Neighbors (K)**: 1, 3
|
| 38 |
- **Distance Metric**: Euclidean, Manhattan
|
| 39 |
-
- **Field Strength/Type**: 7T, 21T, SYN (Synthetic)
|
| 40 |
|
| 41 |
### Model List
|
| 42 |
|
| 43 |
-
| Model
|
| 44 |
|---|---|
|
| 45 |
-
| `
|
| 46 |
-
| `
|
| 47 |
-
| `
|
| 48 |
-
| `
|
| 49 |
-
| `
|
| 50 |
-
| `
|
| 51 |
-
| `
|
| 52 |
-
| `
|
| 53 |
-
| `
|
| 54 |
-
| `
|
| 55 |
-
| `
|
| 56 |
-
| `
|
| 57 |
-
| `
|
| 58 |
-
| `
|
| 59 |
-
| `
|
| 60 |
-
| `
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
|
| 65 |
-
|
|
| 66 |
-
| `
|
| 67 |
-
| `
|
| 68 |
-
| `knn_model_Model-Synthetic_K3_Euclidean_7T.joblib` | Synthetic, K=3, Euclidean, 7T |
|
| 69 |
-
| `knn_model_Model-Synthetic_K3_Euclidean_SYN.joblib` | Synthetic, K=3, Euclidean, SYN |
|
| 70 |
-
| `knn_model_Model-Synthetic_K3_Manhattan_21T.joblib` | Synthetic, K=3, Manhattan, 21T |
|
| 71 |
-
| `knn_model_Model-Synthetic_K3_Manhattan_7T.joblib` | Synthetic, K=3, Manhattan, 7T |
|
| 72 |
-
| `knn_model_Model-Synthetic_K3_Manhattan_SYN.joblib` | Synthetic, K=3, Manhattan, SYN |
|
| 73 |
-
|
| 74 |
|
| 75 |
## License
|
| 76 |
|
|
|
|
| 14 |
# DOM Formula Assignment using Machine Learning
|
| 15 |
|
| 16 |
|
| 17 |
+

|
| 18 |

|
| 19 |

|
| 20 |
[](https://github.com/pcdslab/dom-formula-assignment-using-ml)
|
|
|
|
| 30 |
## Abstract
|
| 31 |
A machine learning approach to molecular formula assignment is crucial for unlocking the full potential of ultra-high resolution mass spectrometry (UHRMS) when analyzing complex mixtures. By combining data-driven models with rigorous benchmarking, the accuracy, consistency, and speed in identifying plausible molecular formulas from vast spectral datasets can be improved. Compared with traditional de novo methods that rely heavily on rule-based heuristics, and manual parameter tuning, machine learning approaches can capture complex patterns in data and adapt more readily to diverse sample types. In this paper, we describe the application of a machine learning methods using the k-nearest neighbors (KNN) algorithm trained on curated chemical formula datasets of UHRMS analysis of dissolved organic matter (DOM) covering the saline river continuum and tropical wet/dry season variability. The influence of the mass accuracy (training set with 0.15-1ppm) was evaluated on a blind test set of DOMs of different geographical origins. A Decision Tree Regressor (DTR) and Random Forest Regressor (RFR) based on mass accuracy (<1ppm) was used. Results from our ML models exhibit 43% more formulas annotated than traditional methods (5796 vs 4047), Model-Synthetic achieved 99.9% assignment rate and annotated/assigned 2x more formulas (8,268 vs 4047). DTR and RFR achieved formula-level accuracies (FA) of 86.5% and 60.4%, respectively. Overall, results show an increase in formula assignment when compared with traditional methods. This ultimately enables more reliable characterization of complex natural and engineered systems, supporting advances in fields such as environmental science, metabolomics, and petroleomics. Furthermore, the novel data set produced for this study is made publicly available, establishing an initial benchmark for molecular formula assignment in UHRMS using machine learning. The dataset and code are publicly available at: https://github.com/pcdslab/dom-formula-assignment-using-ml
|
| 32 |
|
| 33 |
+
## Architecture
|
| 34 |
+
|
| 35 |
+

|
| 36 |
+
(a) KNN Pipeline for Formula Assignment/Annotation. For each train set, the first KNN model is fitted, and then formula assignment/annotation is performed using the closest peaks in the training set. (b) Decision Tree Regressor (DTR) and Random Forest Regressor (RFR) are trained to predict element counts for CHONS using squared-error criterion.
|
| 37 |
+
## Usage
|
| 38 |
+
|
| 39 |
+
Install the dependencies:
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
pip install "numpy==2.4.1" "scikit-learn==1.8.0" joblib huggingface_hub transformers torch pandas
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
### KNN Formula Assignment
|
| 47 |
+
|
| 48 |
+
Load the default KNN model:
|
| 49 |
+
|
| 50 |
+
```python
|
| 51 |
+
from transformers import AutoModel
|
| 52 |
+
import torch
|
| 53 |
+
|
| 54 |
+
model = AutoModel.from_pretrained(
|
| 55 |
+
"SaeedLab/dom-formula-assignment-using-ml",
|
| 56 |
+
trust_remote_code=True,
|
| 57 |
+
).eval()
|
| 58 |
+
|
| 59 |
+
# Mass-only input. Each value is one mass to assign.
|
| 60 |
+
masses = torch.tensor([350.123456, 421.987654], dtype=torch.float64)
|
| 61 |
+
|
| 62 |
+
with torch.no_grad():
|
| 63 |
+
outputs = model(features=masses, return_neighbors=True, n_neighbors=3)
|
| 64 |
+
|
| 65 |
+
print("Predicted formulas:", outputs.predictions)
|
| 66 |
+
print("Neighbor indices:", outputs.indices)
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
Load a specific KNN model by passing `model_name`:
|
| 70 |
+
|
| 71 |
+
```python
|
| 72 |
+
from transformers import AutoModel
|
| 73 |
+
|
| 74 |
+
model = AutoModel.from_pretrained(
|
| 75 |
+
"SaeedLab/dom-formula-assignment-using-ml",
|
| 76 |
+
trust_remote_code=True,
|
| 77 |
+
model_name="L1-L3_K3_Manhattan_Ensemble",
|
| 78 |
+
).eval()
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
Use the `Model Name` value from the table below. For example, `Synthetic_K3_Euclidean_Ensemble` selects the default synthetic KNN ensemble.
|
| 82 |
+
|
| 83 |
+
There is no tokenizer step for these KNN models. Inputs are 1D numeric vectors of mass values.
|
| 84 |
+
|
| 85 |
+
### Get nearest neighbors
|
| 86 |
+
|
| 87 |
+
KNN models can also return the closest training examples for each input sample:
|
| 88 |
+
|
| 89 |
+
```python
|
| 90 |
+
neighbor_indices = model.neighbor_indices(masses, n_neighbors=3)
|
| 91 |
+
|
| 92 |
+
print("Neighbor indices:")
|
| 93 |
+
print(neighbor_indices)
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
Use the model variant that matches your experiment setup:
|
| 97 |
+
|
| 98 |
+
- `L1`, `L3`, `L1-L3`, or `Synthetic` for the training dataset.
|
| 99 |
+
- `K1` or `K3` for the number of neighbors.
|
| 100 |
+
- `Euclidean` or `Manhattan` for the distance metric.
|
| 101 |
+
- `Ensemble`, `7T`, `21T`, or `SYN` for combined, field-strength, or synthetic backing models.
|
| 102 |
+
|
| 103 |
+
### Decision Tree and Random Forest Formula Regressors
|
| 104 |
+
|
| 105 |
+
The Decision Tree and Random Forest models predict CHONS element counts from mass and ion mobility features. Inputs must be numeric rows in this order:
|
| 106 |
+
|
| 107 |
+
```text
|
| 108 |
+
[mz, inv_k0, ccs]
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
Here, `inv_k0` is `1/k0`, and both `inv_k0` and `ccs` are mobility-derived values.
|
| 112 |
+
|
| 113 |
+
```python
|
| 114 |
+
from transformers import AutoModel
|
| 115 |
+
import torch
|
| 116 |
+
|
| 117 |
+
model = AutoModel.from_pretrained(
|
| 118 |
+
"SaeedLab/dom-formula-assignment-using-ml",
|
| 119 |
+
trust_remote_code=True,
|
| 120 |
+
model_name="RandomForest",
|
| 121 |
+
).eval()
|
| 122 |
+
|
| 123 |
+
features = torch.tensor(
|
| 124 |
+
[
|
| 125 |
+
[191.03498, 0.6808667247437136, 146.2048497276655],
|
| 126 |
+
[191.071388, 0.7087887538442335, 152.19879035313514],
|
| 127 |
+
[191.10775, 0.6935974811483341, 148.93494377261592],
|
| 128 |
+
],
|
| 129 |
+
dtype=torch.float64,
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
with torch.no_grad():
|
| 133 |
+
outputs = model(features=features)
|
| 134 |
+
|
| 135 |
+
print("Predicted formulas:", outputs.formulas)
|
| 136 |
+
print("Predicted CHONS counts:", outputs.formula_counts)
|
| 137 |
+
```
|
| 138 |
+
|
| 139 |
+
The CHONS output columns are ordered as:
|
| 140 |
+
|
| 141 |
+
```text
|
| 142 |
+
[C, H, O, N, S]
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
## KNN Models
|
| 146 |
|
| 147 |
This repository contains pre-trained KNN models used for molecular formula assignment. The models vary by:
|
| 148 |
+
- **Dataset**: L1 (7T), L3 (21T), L1-L3, Synthetic
|
| 149 |
- **Neighbors (K)**: 1, 3
|
| 150 |
- **Distance Metric**: Euclidean, Manhattan
|
| 151 |
+
- **Field Strength/Type**: Ensemble, 7T, 21T, SYN (Synthetic)
|
| 152 |
|
| 153 |
### Model List
|
| 154 |
|
| 155 |
+
| Model Name | Description |
|
| 156 |
|---|---|
|
| 157 |
+
| `L1_K1_Euclidean` | L1 (7T) KNN model with 1 neighbor and Euclidean distance |
|
| 158 |
+
| `L1_K1_Manhattan` | L1 (7T) KNN model with 1 neighbor and Manhattan distance |
|
| 159 |
+
| `L1_K3_Euclidean` | L1 (7T) KNN model with 3 neighbors and Euclidean distance |
|
| 160 |
+
| `L1_K3_Manhattan` | L1 (7T) KNN model with 3 neighbors and Manhattan distance |
|
| 161 |
+
| `L3_K1_Euclidean` | L3 (21T) KNN model with 1 neighbor and Euclidean distance |
|
| 162 |
+
| `L3_K1_Manhattan` | L3 (21T) KNN model with 1 neighbor and Manhattan distance |
|
| 163 |
+
| `L3_K3_Euclidean` | L3 (21T) KNN model with 3 neighbors and Euclidean distance |
|
| 164 |
+
| `L3_K3_Manhattan` | L3 (21T) KNN model with 3 neighbors and Manhattan distance |
|
| 165 |
+
| `L1-L3_K1_Euclidean_Ensemble` | Ensemble of L1 (7T) and L3 (21T) KNN models with 1 neighbor and Euclidean distance |
|
| 166 |
+
| `L1-L3_K1_Manhattan_Ensemble` | Ensemble of L1 (7T) and L3 (21T) KNN models with 1 neighbor and Manhattan distance |
|
| 167 |
+
| `L1-L3_K3_Euclidean_Ensemble` | Ensemble of L1 (7T) and L3 (21T) KNN models with 3 neighbors and Euclidean distance |
|
| 168 |
+
| `L1-L3_K3_Manhattan_Ensemble` | Ensemble of L1 (7T) and L3 (21T) KNN models with 3 neighbors and Manhattan distance |
|
| 169 |
+
| `Synthetic_K1_Euclidean_Ensemble` | Ensemble of Synthetic 7T, 21T, and SYN KNN models with 1 neighbor and Euclidean distance |
|
| 170 |
+
| `Synthetic_K1_Manhattan_Ensemble` | Ensemble of Synthetic 7T, 21T, and SYN KNN models with 1 neighbor and Manhattan distance |
|
| 171 |
+
| `Synthetic_K3_Euclidean_Ensemble` | Ensemble of Synthetic 7T, 21T, and SYN KNN models with 3 neighbors and Euclidean distance |
|
| 172 |
+
| `Synthetic_K3_Manhattan_Ensemble` | Ensemble of Synthetic 7T, 21T, and SYN KNN models with 3 neighbors and Manhattan distance |
|
| 173 |
+
|
| 174 |
+
## Decision Tree and Random Forest Models
|
| 175 |
+
|
| 176 |
+
| Model Name | Description |
|
| 177 |
+
|---|---|
|
| 178 |
+
| `DecisionTree` | Predicts CHONS counts from `mz`, `inv_k0`, and `ccs` |
|
| 179 |
+
| `RandomForest` | Predicts CHONS counts from `mz`, `inv_k0`, and `ccs` |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
## License
|
| 182 |
|
RandomForest.joblib
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b6b93e850427cebb857a42e7714b25ae672425ee644ffae03a7d3d52b97adcfb
|
| 3 |
+
size 233828641
|
architecture.png
ADDED
|
Git LFS Details
|
config.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"DomMLModel"
|
| 4 |
+
],
|
| 5 |
+
"auto_map": {
|
| 6 |
+
"AutoConfig": "configuration_dom_ml.DomMLConfig",
|
| 7 |
+
"AutoModel": "modeling_dom_ml.DomMLModel"
|
| 8 |
+
},
|
| 9 |
+
"feature_names": [
|
| 10 |
+
"mass"
|
| 11 |
+
],
|
| 12 |
+
"library_name": "transformers",
|
| 13 |
+
"model_name": "Synthetic_K3_Euclidean_Ensemble",
|
| 14 |
+
"model_type": "dom_ml"
|
| 15 |
+
}
|
configuration_dom_ml.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from transformers import PretrainedConfig
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class DomMLConfig(PretrainedConfig):
|
| 5 |
+
model_type = "dom_ml"
|
| 6 |
+
|
| 7 |
+
def __init__(
|
| 8 |
+
self,
|
| 9 |
+
model_name="Synthetic_K3_Euclidean_Ensemble",
|
| 10 |
+
model_file=None,
|
| 11 |
+
feature_names=None,
|
| 12 |
+
model_kind=None,
|
| 13 |
+
**kwargs,
|
| 14 |
+
):
|
| 15 |
+
super().__init__(**kwargs)
|
| 16 |
+
self.model_name = model_name
|
| 17 |
+
self.model_file = model_file
|
| 18 |
+
self.model_kind = model_kind
|
| 19 |
+
self.feature_names = feature_names or ["mass"]
|
modeling_dom_ml.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import torch
|
| 3 |
+
from huggingface_hub import hf_hub_download
|
| 4 |
+
from joblib import load
|
| 5 |
+
from transformers import PreTrainedModel
|
| 6 |
+
from transformers.utils import ModelOutput
|
| 7 |
+
import os
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
from typing import Any, Optional
|
| 10 |
+
|
| 11 |
+
from .configuration_dom_ml import DomMLConfig
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
ELEMENTS = ("C", "H", "O", "N", "S")
|
| 15 |
+
FORMULA_REGRESSOR_NAMES = {"DecisionTree", "RandomForest"}
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@dataclass
|
| 19 |
+
class DomMLOutput(ModelOutput):
|
| 20 |
+
predictions: Any = None
|
| 21 |
+
formula_counts: Any = None
|
| 22 |
+
formulas: Any = None
|
| 23 |
+
distances: Optional[Any] = None
|
| 24 |
+
indices: Optional[Any] = None
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class DomMLModel(PreTrainedModel):
|
| 28 |
+
config_class = DomMLConfig
|
| 29 |
+
base_model_prefix = "dom_ml"
|
| 30 |
+
main_input_name = "features"
|
| 31 |
+
|
| 32 |
+
def __init__(self, config, estimator=None, estimators=None):
|
| 33 |
+
super().__init__(config)
|
| 34 |
+
self.estimators = estimators or ([estimator] if estimator is not None else [])
|
| 35 |
+
self.estimator = self.estimators[0] if self.estimators else None
|
| 36 |
+
|
| 37 |
+
@staticmethod
|
| 38 |
+
def _model_name_to_file(model_name):
|
| 39 |
+
if not model_name:
|
| 40 |
+
return None
|
| 41 |
+
if model_name in FORMULA_REGRESSOR_NAMES:
|
| 42 |
+
return f"{model_name}.joblib"
|
| 43 |
+
if model_name.endswith(".joblib"):
|
| 44 |
+
return model_name
|
| 45 |
+
if model_name.startswith("knn_model_"):
|
| 46 |
+
return f"{model_name}.joblib"
|
| 47 |
+
if model_name.startswith("Model-"):
|
| 48 |
+
return f"knn_model_{model_name}.joblib"
|
| 49 |
+
return f"knn_model_Model-{model_name}.joblib"
|
| 50 |
+
|
| 51 |
+
@classmethod
|
| 52 |
+
def _model_name_to_files(cls, model_name):
|
| 53 |
+
if not model_name:
|
| 54 |
+
return None
|
| 55 |
+
if model_name.startswith("L1-L3_") and model_name.endswith("_Ensemble"):
|
| 56 |
+
base_name = model_name[: -len("_Ensemble")]
|
| 57 |
+
return [
|
| 58 |
+
cls._model_name_to_file(f"{base_name}_7T"),
|
| 59 |
+
cls._model_name_to_file(f"{base_name}_21T"),
|
| 60 |
+
]
|
| 61 |
+
if model_name.startswith("Synthetic_") and model_name.endswith("_Ensemble"):
|
| 62 |
+
base_name = model_name[: -len("_Ensemble")]
|
| 63 |
+
return [
|
| 64 |
+
cls._model_name_to_file(f"{base_name}_7T"),
|
| 65 |
+
cls._model_name_to_file(f"{base_name}_21T"),
|
| 66 |
+
cls._model_name_to_file(f"{base_name}_SYN"),
|
| 67 |
+
]
|
| 68 |
+
return [cls._model_name_to_file(model_name)]
|
| 69 |
+
|
| 70 |
+
@staticmethod
|
| 71 |
+
def _infer_model_kind(model_name, model_files):
|
| 72 |
+
if model_name in FORMULA_REGRESSOR_NAMES:
|
| 73 |
+
return "formula_regressor"
|
| 74 |
+
if model_files and all(
|
| 75 |
+
os.path.basename(model_file) in {"DecisionTree.joblib", "RandomForest.joblib"}
|
| 76 |
+
for model_file in model_files
|
| 77 |
+
):
|
| 78 |
+
return "formula_regressor"
|
| 79 |
+
return "knn"
|
| 80 |
+
|
| 81 |
+
@classmethod
|
| 82 |
+
def from_pretrained(cls, pretrained_model_name_or_path, *model_args, **kwargs):
|
| 83 |
+
config = kwargs.pop("config", None)
|
| 84 |
+
model_name = kwargs.pop("model_name", None)
|
| 85 |
+
model_file = kwargs.pop("model_file", None)
|
| 86 |
+
cache_dir = kwargs.pop("cache_dir", None)
|
| 87 |
+
force_download = kwargs.pop("force_download", False)
|
| 88 |
+
local_files_only = kwargs.pop("local_files_only", False)
|
| 89 |
+
token = kwargs.pop("token", None)
|
| 90 |
+
revision = kwargs.pop("revision", None)
|
| 91 |
+
subfolder = kwargs.pop("subfolder", "")
|
| 92 |
+
|
| 93 |
+
kwargs.pop("trust_remote_code", None)
|
| 94 |
+
kwargs.pop("code_revision", None)
|
| 95 |
+
kwargs.pop("_commit_hash", None)
|
| 96 |
+
|
| 97 |
+
if config is None:
|
| 98 |
+
config = DomMLConfig.from_pretrained(
|
| 99 |
+
pretrained_model_name_or_path,
|
| 100 |
+
cache_dir=cache_dir,
|
| 101 |
+
force_download=force_download,
|
| 102 |
+
local_files_only=local_files_only,
|
| 103 |
+
token=token,
|
| 104 |
+
revision=revision,
|
| 105 |
+
subfolder=subfolder,
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
model_files = None
|
| 109 |
+
if model_file is not None:
|
| 110 |
+
model_files = [model_file]
|
| 111 |
+
else:
|
| 112 |
+
model_name = model_name or getattr(config, "model_name", None)
|
| 113 |
+
model_files = cls._model_name_to_files(model_name)
|
| 114 |
+
if model_files is None:
|
| 115 |
+
configured_model_file = getattr(config, "model_file", None)
|
| 116 |
+
if configured_model_file is not None:
|
| 117 |
+
model_files = [configured_model_file]
|
| 118 |
+
if model_files is None:
|
| 119 |
+
raise ValueError("Pass model_name=... to select one of the available models.")
|
| 120 |
+
|
| 121 |
+
config.model_name = model_name
|
| 122 |
+
config.model_file = model_files[0]
|
| 123 |
+
config.model_files = model_files
|
| 124 |
+
config.model_kind = cls._infer_model_kind(model_name, model_files)
|
| 125 |
+
if config.model_kind == "formula_regressor":
|
| 126 |
+
config.feature_names = ["mz", "inv_k0", "ccs"]
|
| 127 |
+
|
| 128 |
+
model_paths = []
|
| 129 |
+
for current_model_file in model_files:
|
| 130 |
+
if os.path.isdir(pretrained_model_name_or_path):
|
| 131 |
+
model_path = os.path.join(
|
| 132 |
+
pretrained_model_name_or_path,
|
| 133 |
+
subfolder,
|
| 134 |
+
current_model_file,
|
| 135 |
+
)
|
| 136 |
+
else:
|
| 137 |
+
model_path = hf_hub_download(
|
| 138 |
+
repo_id=pretrained_model_name_or_path,
|
| 139 |
+
filename=current_model_file,
|
| 140 |
+
cache_dir=cache_dir,
|
| 141 |
+
force_download=force_download,
|
| 142 |
+
local_files_only=local_files_only,
|
| 143 |
+
token=token,
|
| 144 |
+
revision=revision,
|
| 145 |
+
subfolder=subfolder,
|
| 146 |
+
)
|
| 147 |
+
model_paths.append(model_path)
|
| 148 |
+
|
| 149 |
+
estimators = [load(model_path) for model_path in model_paths]
|
| 150 |
+
model = cls(config, estimators=estimators)
|
| 151 |
+
model.eval()
|
| 152 |
+
return model
|
| 153 |
+
|
| 154 |
+
def _to_numpy(self, features):
|
| 155 |
+
if isinstance(features, torch.Tensor):
|
| 156 |
+
values = features.detach().cpu().numpy()
|
| 157 |
+
elif hasattr(features, "loc") and hasattr(features, "columns"):
|
| 158 |
+
feature_names = getattr(self.config, "feature_names", None)
|
| 159 |
+
if feature_names and all(name in features.columns for name in feature_names):
|
| 160 |
+
values = features.loc[:, feature_names].to_numpy(dtype=np.float64)
|
| 161 |
+
else:
|
| 162 |
+
values = features.to_numpy(dtype=np.float64)
|
| 163 |
+
elif hasattr(features, "to_numpy"):
|
| 164 |
+
values = features.to_numpy(dtype=np.float64)
|
| 165 |
+
else:
|
| 166 |
+
values = np.asarray(features, dtype=np.float64)
|
| 167 |
+
|
| 168 |
+
if getattr(self.config, "model_kind", None) == "formula_regressor":
|
| 169 |
+
if values.ndim == 1:
|
| 170 |
+
if values.size != 3:
|
| 171 |
+
raise ValueError(
|
| 172 |
+
"DecisionTree and RandomForest inputs must use [mz, inv_k0, ccs]."
|
| 173 |
+
)
|
| 174 |
+
return values.reshape(1, 3)
|
| 175 |
+
return values
|
| 176 |
+
|
| 177 |
+
if values.ndim == 0:
|
| 178 |
+
return values.reshape(1, 1)
|
| 179 |
+
if values.ndim == 1:
|
| 180 |
+
return values.reshape(-1, 1)
|
| 181 |
+
return values
|
| 182 |
+
|
| 183 |
+
@staticmethod
|
| 184 |
+
def _maybe_tensor(array, as_numpy, dtype=None):
|
| 185 |
+
if as_numpy or array is None:
|
| 186 |
+
return array
|
| 187 |
+
return torch.as_tensor(array, dtype=dtype)
|
| 188 |
+
|
| 189 |
+
@staticmethod
|
| 190 |
+
def _counts_to_formulas(counts):
|
| 191 |
+
formulas = []
|
| 192 |
+
for row in np.asarray(counts, dtype=int):
|
| 193 |
+
formula = ""
|
| 194 |
+
for element, count in zip(ELEMENTS, row):
|
| 195 |
+
if count > 0:
|
| 196 |
+
formula += element
|
| 197 |
+
if count != 1:
|
| 198 |
+
formula += str(int(count))
|
| 199 |
+
formulas.append(formula)
|
| 200 |
+
return np.asarray(formulas)
|
| 201 |
+
|
| 202 |
+
def predict_counts(self, features, as_numpy=True):
|
| 203 |
+
if not self.estimators:
|
| 204 |
+
raise ValueError("No model is loaded.")
|
| 205 |
+
if getattr(self.config, "model_kind", None) != "formula_regressor":
|
| 206 |
+
raise ValueError("predict_counts is only available for DecisionTree and RandomForest.")
|
| 207 |
+
|
| 208 |
+
values = self._to_numpy(features)
|
| 209 |
+
raw_counts = self.estimators[0].predict(values)
|
| 210 |
+
counts = np.rint(raw_counts).astype(int)
|
| 211 |
+
counts = np.clip(counts, 0, None)
|
| 212 |
+
if as_numpy:
|
| 213 |
+
return counts
|
| 214 |
+
return torch.as_tensor(counts, dtype=torch.long)
|
| 215 |
+
|
| 216 |
+
def predict(self, features):
|
| 217 |
+
if not self.estimators:
|
| 218 |
+
raise ValueError("No model is loaded.")
|
| 219 |
+
if getattr(self.config, "model_kind", None) == "formula_regressor":
|
| 220 |
+
return self._counts_to_formulas(self.predict_counts(features))
|
| 221 |
+
|
| 222 |
+
values = self._to_numpy(features)
|
| 223 |
+
predictions = [model.predict(values) for model in self.estimators]
|
| 224 |
+
if len(predictions) == 1:
|
| 225 |
+
return predictions[0]
|
| 226 |
+
|
| 227 |
+
stacked = np.vstack(predictions).T
|
| 228 |
+
voted_predictions = []
|
| 229 |
+
for row in stacked:
|
| 230 |
+
counts = {}
|
| 231 |
+
for prediction in row:
|
| 232 |
+
counts[prediction] = counts.get(prediction, 0) + 1
|
| 233 |
+
voted_predictions.append(
|
| 234 |
+
max(row, key=lambda prediction: counts[prediction])
|
| 235 |
+
)
|
| 236 |
+
return np.asarray(voted_predictions)
|
| 237 |
+
|
| 238 |
+
def joblib_summary(self, max_items=5):
|
| 239 |
+
if not self.estimators:
|
| 240 |
+
raise ValueError("No model is loaded.")
|
| 241 |
+
|
| 242 |
+
summaries = []
|
| 243 |
+
model_files = getattr(self.config, "model_files", None) or [self.config.model_file]
|
| 244 |
+
for model_file, estimator in zip(model_files, self.estimators):
|
| 245 |
+
summary = {
|
| 246 |
+
"type": type(estimator).__name__,
|
| 247 |
+
"model_file": model_file,
|
| 248 |
+
"model_kind": getattr(self.config, "model_kind", None),
|
| 249 |
+
"feature_names": getattr(self.config, "feature_names", None),
|
| 250 |
+
"n_features_in": getattr(estimator, "n_features_in_", None),
|
| 251 |
+
"n_samples_fit": getattr(estimator, "n_samples_fit_", None),
|
| 252 |
+
}
|
| 253 |
+
if getattr(self.config, "model_kind", None) == "formula_regressor":
|
| 254 |
+
summary["output_elements"] = list(ELEMENTS)
|
| 255 |
+
|
| 256 |
+
classes = getattr(estimator, "classes_", None)
|
| 257 |
+
if classes is not None:
|
| 258 |
+
summary["classes_preview"] = classes[:max_items].tolist()
|
| 259 |
+
|
| 260 |
+
fit_x = getattr(estimator, "_fit_X", None)
|
| 261 |
+
if fit_x is not None:
|
| 262 |
+
summary["fit_masses_preview"] = np.asarray(fit_x[:max_items]).reshape(-1).tolist()
|
| 263 |
+
|
| 264 |
+
summaries.append(summary)
|
| 265 |
+
|
| 266 |
+
if len(summaries) == 1:
|
| 267 |
+
return summaries[0]
|
| 268 |
+
return {
|
| 269 |
+
"type": "Ensemble",
|
| 270 |
+
"model_name": self.config.model_name,
|
| 271 |
+
"models": summaries,
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
def kneighbors(self, features, n_neighbors=None, as_numpy=False):
|
| 275 |
+
if getattr(self.config, "model_kind", None) == "formula_regressor":
|
| 276 |
+
raise ValueError("Nearest-neighbor lookup is only available for KNN models.")
|
| 277 |
+
if not self.estimators:
|
| 278 |
+
raise ValueError("No KNN model is loaded.")
|
| 279 |
+
values = self._to_numpy(features)
|
| 280 |
+
if len(self.estimators) > 1:
|
| 281 |
+
model_files = getattr(self.config, "model_files", None) or []
|
| 282 |
+
distances = {}
|
| 283 |
+
indices = {}
|
| 284 |
+
for model_file, estimator in zip(model_files, self.estimators):
|
| 285 |
+
model_distances, model_indices = estimator.kneighbors(
|
| 286 |
+
values,
|
| 287 |
+
n_neighbors=n_neighbors,
|
| 288 |
+
)
|
| 289 |
+
distances[model_file] = self._maybe_tensor(
|
| 290 |
+
model_distances,
|
| 291 |
+
as_numpy,
|
| 292 |
+
dtype=torch.float32,
|
| 293 |
+
)
|
| 294 |
+
indices[model_file] = self._maybe_tensor(
|
| 295 |
+
model_indices,
|
| 296 |
+
as_numpy,
|
| 297 |
+
dtype=torch.long,
|
| 298 |
+
)
|
| 299 |
+
return distances, indices
|
| 300 |
+
|
| 301 |
+
distances, indices = self.estimators[0].kneighbors(
|
| 302 |
+
values,
|
| 303 |
+
n_neighbors=n_neighbors,
|
| 304 |
+
)
|
| 305 |
+
return (
|
| 306 |
+
self._maybe_tensor(distances, as_numpy, dtype=torch.float32),
|
| 307 |
+
self._maybe_tensor(indices, as_numpy, dtype=torch.long),
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
def neighbor_indices(self, features, n_neighbors=None, as_numpy=False):
|
| 311 |
+
_, indices = self.kneighbors(
|
| 312 |
+
features,
|
| 313 |
+
n_neighbors=n_neighbors,
|
| 314 |
+
as_numpy=as_numpy,
|
| 315 |
+
)
|
| 316 |
+
return indices
|
| 317 |
+
|
| 318 |
+
def forward(
|
| 319 |
+
self,
|
| 320 |
+
features=None,
|
| 321 |
+
input_features=None,
|
| 322 |
+
return_neighbors=False,
|
| 323 |
+
n_neighbors=None,
|
| 324 |
+
as_numpy=False,
|
| 325 |
+
**kwargs,
|
| 326 |
+
):
|
| 327 |
+
if features is None:
|
| 328 |
+
features = input_features
|
| 329 |
+
if features is None:
|
| 330 |
+
raise ValueError("Pass model inputs with features=... or input_features=....")
|
| 331 |
+
|
| 332 |
+
predictions = self.predict(features)
|
| 333 |
+
formula_counts = None
|
| 334 |
+
formulas = None
|
| 335 |
+
distances = None
|
| 336 |
+
indices = None
|
| 337 |
+
|
| 338 |
+
if getattr(self.config, "model_kind", None) == "formula_regressor":
|
| 339 |
+
formula_counts = self.predict_counts(features, as_numpy=as_numpy)
|
| 340 |
+
formulas = predictions
|
| 341 |
+
elif return_neighbors:
|
| 342 |
+
distances, indices = self.kneighbors(
|
| 343 |
+
features,
|
| 344 |
+
n_neighbors=n_neighbors,
|
| 345 |
+
as_numpy=as_numpy,
|
| 346 |
+
)
|
| 347 |
+
|
| 348 |
+
return DomMLOutput(
|
| 349 |
+
predictions=predictions,
|
| 350 |
+
formula_counts=formula_counts,
|
| 351 |
+
formulas=formulas,
|
| 352 |
+
distances=distances,
|
| 353 |
+
indices=indices,
|
| 354 |
+
)
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
numpy==2.4.1
|
| 2 |
+
scikit-learn==1.8.0
|
| 3 |
+
joblib
|
| 4 |
+
huggingface_hub
|
| 5 |
+
transformers
|
| 6 |
+
torch
|
| 7 |
+
pandas
|