MassSpecGym

repository·main·Indexed 40 days ago

https://github.com/pluskal-lab/massspecgym

A benchmarking framework for machine learning models designed for molecular discovery and identification from MS/MS spectra. It provides standardized challenges for de novo generation, molecule retrieval, and spectrum simulation, featuring a modular infrastructure with PyTorch Lightning integration and specialized abstract classes like DeNovoMassSpecGymModel, RetrievalMassSpecGymModel, and SimulationMassSpecGymModel.

Tokens
19.9K
Snippets
48
Records
55
Agent score
100%

What's inside MassSpecGym

  1. How MassSpecGym components work together

    main

    MassSpecGym provides a modular infrastructure for benchmarking molecular discovery tasks. The workflow typically follows this pattern:

    1. Data Loading: Use load_massspecgym() to get the dataset as a pandas DataFrame, or use MassSpecDataset to load it directly from Hugging Face into a PyTorch-compatible format.
    2. Preprocessing: Apply transforms (like SpecTokenizer for spectra or MolFingerprinter for molecules) via the MassSpecDataset class.
    3. Data Management: Use MassSpecDataModule (a PyTorch Lightning LightningDataModule) to handle data splitting (train/val/test) and batching.
    4. Model Implementation: Inherit from a challenge-specific abstract class (e.g., RetrievalMassSpecGymModel) and implement the forward and step methods to define your architecture and training/inference logic.
    5. Training/Evaluation: Use a standard PyTorch Lightning Trainer to run the training and testing loops.
    from massspecgym.data import MassSpecDataset
    from massspecgym.data.transforms import SpecTokenizer, MolFingerprinter
    from massspecgym.data import MassSpecDataModule
    
    # 1. Define transforms and dataset
    dataset = MassSpecDataset(
        spec_transform=SpecTokenizer(n_peaks=60),
        mol_transform=MolFingerprinter(),
    )
    
    # 2. Initialize data module for Lightning
    data_module = MassSpecDataModule(
        dataset=dataset,
        batch_size=32
    )
  2. Install MassSpecGym

    main

    You can install MassSpecGym via pip. For conda users, it is recommended to create a new environment first.

    To include optional dependencies for running Jupyter notebooks or contributing to the project, use the [notebooks, dev] extra.

    # Standard installation
    pip install massspecgym
    
    # Recommended conda setup
    conda create -n massspecgym python==3.11
    conda activate massspecgym
    pip install massspecgym
    
    # Installation with optional dependencies for notebooks and development
    pip install "massspecgym[notebooks, dev]"
  3. Implement a custom model for MassSpecGym challenges

    main

    To implement a custom model, you must inherit from one of the three challenge-specific abstract classes:

    • DeNovoMassSpecGymModel (for de novo molecule generation)
    • RetrievalMassSpecGymModel (for molecule retrieval)
    • SimulationMassSpecGymModel (for spectrum simulation)

    You are required to implement:

    1. forward(self, x): Defines the prediction logic (e.g., mapping spectra to a fingerprint).
    2. step(self, batch, stage): Defines the custom logic for training and inference, including unpacking the batch, calculating loss, and computing similarity scores.
    from massspecgym.models.retrieval.base import RetrievalMassSpecGymModel
    from massspecgym.models.base import Stage
    import torch
    import torch.nn as nn
    
    class MyModel(RetrievalMassSpecGymModel):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            # Define architecture here
    
        def forward(self, x: torch.Tensor) -> torch.Tensor:
            # Implement prediction logic
            return x
    
        def step(self, batch: dict, stage: Stage) -> tuple[torch.Tensor, torch.Tensor]:
            # Unpack inputs from batch
            x = batch["spec"]
            fp_true = batch["mol"]
            cands = batch["candidates"]
            batch_ptr = batch["batch_ptr"]
    
            # Predict
            fp_pred = self.forward(x)
    
            # Calculate loss and scores
            loss = nn.functional.mse_loss(fp_true, fp_pred)
            fp_pred_repeated = fp_pred.repeat_interleave(batch_ptr, dim=0)
            scores = nn.functional.cosine_similarity(fp_pred_repeated, cands)
    
            return dict(loss=loss, scores=scores)
  4. Analyze dataset leakage using Tanimoto similarity

    main

    To quantify data leakage, calculate the maximum Tanimoto similarity between molecules in the test set and all molecules in the training/validation sets. High Tanimoto similarity (e.g., > 0.95) between test and training molecules indicates potential leakage where the model might be memorizing similar structures rather than learning generalizable features.

    This process typically involves:

    1. Generating Morgan fingerprints for all molecules using massspecgym.utils.morgan_fp.
    2. Using rdkit.Chem.DataStructs.FingerprintSimilarity to compute similarities.
    3. Iterating through the test set to find the maximum similarity score relative to the training/validation pool.
    from rdkit import Chem
    from rdkit.Chem import DataStructs
    import massspecgym.utils as utils
    
    # Logic for finding max similarity
    def max_train_tanimoto(test_mol_and_fp, train_mols_and_fps):
        tanimotos = [
            DataStructs.FingerprintSimilarity(test_mol_and_fp[1], train_mol_and_fp[1])
            for train_mol_and_fp in train_mols_and_fps
        ]
        return max(tanimotos)
  5. Construct retrieval candidates by formula or mass

    main

    When building retrieval tasks, you can group query molecules with potential candidates from a database using two primary strategies:

    1. Formula-based (cand_type='formula'): Matches molecules that share the exact same molecular formula.
    2. Mass-based (cand_type='mass'): Matches molecules within a specific mass tolerance (e.g., 10 ppm) of the query molecule's exact weight.

    Note: If a query molecule is not found in the database, it is appended to its own candidate list to ensure the ground truth is always present.

    def get_candidates(query_smiles, df, cand_type='formula'):
        # ... implementation details ...
        # cand_type options: 'formula' or 'mass'
        # 'mass' uses a 10 ppm tolerance
        pass
    
    # Usage examples:
    cands_formula = get_candidates(smiles, df_1M, cand_type='formula')
    cands_mass = get_candidates(smiles, df_1M, cand_type='mass')
  6. Candidate selection strategies: 'formula' vs 'mass'

    main

    When using get_candidates_parallel, you can choose how candidates are selected based on the cand_type parameter:

    1. Formula-based (cand_type='formula'): Matches molecules that share the exact same molecular formula. This is a strict structural constraint based on elemental composition.

    2. Mass-based (cand_type='mass'): Matches molecules within a specific mass tolerance. The implementation uses a tolerance of 10 ppm (parts per million) relative to the query molecule's exact mass. This is useful for capturing molecules that are isobaric or very close in mass.

    In both cases, the query molecule itself is included as the first candidate, and molecules with the same inchi_key_2D (effectively the same molecule) are excluded from the new candidates found to ensure diversity.

  7. Aggregate and evaluate model performance metrics

    main

    To evaluate different models in MassSpecGym, you can aggregate results from multiple runs (stored in Weights & Biases) into a single DataFrame. This allows for comparative analysis of metrics such as cosine similarity and hit rates.

    Key metrics typically used for evaluation include:

    • cos_sim: Cosine similarity
    • cos_sim_sqrt: Square root of cosine similarity
    • js_sim: Jensen-Shannon similarity
    • test_hit_rate@1: Hit rate at top 1
    • test_hit_rate@5: Hit rate at top 5
    • test_hit_rate@20: Hit rate at top 20

    Note that hit rate metrics are often scaled by 100 to represent percentages.

    metric_cols = [
        "cos_sim",
        "cos_sim_sqrt",
        "js_sim",
        "test_hit_rate@1",
        "test_hit_rate@5",
        "test_hit_rate@20"
    ]
    
    # After aggregating, scale hit rates to percentages
    for col in ["test_hit_rate@1", "test_hit_rate@5", "test_hit_rate@20"]:
        df[col] = df[col] * 100
  8. Remove duplicate spectra using InChIKeys

    main

    To clean a dataset of duplicate mass spectra, you can group spectra by their inchikey metadata and use similarity scoring to identify and remove redundant entries.

    1. Group spectra by inchikey.
    2. For each group, calculate similarity scores (e.g., using CosineGreedy from matchms).
    3. Identify duplicates where the similarity score is exactly 1.0 (excluding the diagonal/self-comparison).
    4. Retain only one unique spectrum per group.
    from matchms.similarity.CosineGreedy import CosineGreedy
    from matchms import calculate_scores
    import numpy as np
    
    # Assuming cleaned_spectra is a list of matchms Spectrum objects
    # and unique_inchikeys is a dict mapping inchikey -> list of indices
    
    for inchikey in unique_inchikeys:
        matching_spectra = [cleaned_spectra[idx] for idx in unique_inchikeys[inchikey]]
        
        # Calculate similarity scores within the group
        scores = calculate_scores(references=matching_spectra,
                                  queries=matching_spectra,
                                  similarity_function=CosineGreedy())
        
        scores_array = scores.to_array(name="CosineGreedy_score")
        
        # Find indices where similarity is 1.0 (duplicates)
        indices = np.argwhere(scores_array == 1.0)
    
        # Filter out diagonal indices (where index[0] == index[1]) to avoid self-matching
        non_diagonal_indices = [tuple(index) for index in indices if index[0] != index[1]]
        
        # Logic follows to remove identified duplicates from the list
  9. Train and evaluate models using PyTorch Lightning

    main

    MassSpecGym models are designed to work with the PyTorch Lightning Trainer.

    Important Lifecycle Note: When performing validation before training (e.g., to check metrics on a test set), you must explicitly call data_module.prepare_data() and data_module.setup() before calling trainer.validate().

    Standard workflow:

    1. trainer.validate(model, datamodule=data_module)
    2. trainer.fit(model, datamodule=data_module)
    3. trainer.test(model, datamodule=data_module)
    # Explicit setup required for validation before fit
    data_module.prepare_data()
    data_module.setup()
    
    # Validate
    trainer.validate(model, datamodule=data_module)
    
    # Train
    trainer.fit(model, datamodule=data_module)
    
    # Test
    trainer.test(model, datamodule=data_module)
  10. Set up the De Novo Generation task

    main

    For de novo generation tasks, use MassSpecDataset. Unlike retrieval, mol_transform is typically set to None because the goal is to generate molecules from spectra rather than retrieve them from a list.

    Common transformations:

    • SpecTokenizer(n_peaks=...): Tokenizes spectra.

    Wrap the dataset in a MassSpecDataModule for training.

    from massspecgym.data import MassSpecDataset, MassSpecDataModule
    from massspecgym.data.transforms import SpecTokenizer
    
    # 1. Initialize the dataset
    dataset = MassSpecDataset(
        pth=mgf_pth,
        spec_transform=SpecTokenizer(n_peaks=60),
        mol_transform=None
    )
    
    # 2. Initialize the data module
    data_module = MassSpecDataModule(
        dataset=dataset,
        split_pth=split_pth,
        batch_size=2
    )
  11. Set up the MassSpecGym data pipeline

    main

    The data pipeline is constructed using three main components:

    1. RetrievalDataset: The core dataset object. It requires two transforms:
      • spec_transform: A transformation for the spectra (e.g., SpecTokenizer(n_peaks=...)).
      • mol_transform: A transformation for the molecules (e.g., MolFingerprinter(fp_size=...)).
    2. MassSpecDataModule: Wraps the dataset for PyTorch Lightning. It takes the dataset, batch_size, and num_workers as arguments.
    3. Transforms:
      • SpecTokenizer: Converts raw spectra into a tokenized format with a fixed number of peaks.
      • MolFingerprinter: Converts molecules into fixed-size fingerprints.
    from massspecgym.data import RetrievalDataset, MassSpecDataModule
    from massspecgym.data.transforms import SpecTokenizer, MolFingerprinter
    
    n_peaks = 60
    fp_size = 4096
    batch_size = 32
    
    # 1. Create the dataset with transforms
    dataset = RetrievalDataset(
        spec_transform=SpecTokenizer(n_peaks=n_peaks),
        mol_transform=MolFingerprinter(fp_size=fp_size),
    )
    
    # 2. Create the data module
    data_module = MassSpecDataModule(
        dataset=dataset,
        batch_size=batch_size,
        num_workers=4
    )